Inspection and custom layers#
Implement a differentiable custom layer, register non-trainable state, inspect its hierarchy, and use the pure state boundary.
These outputs were saved from local execution. The documentation renderer does not execute notebook cells.
JAXsaw 0.1.0 · JAX 0.5.3
[CudaDevice(id=0)]
Implement an affine layer#
The layer uses ordinary JAX operations. Parameters receive gradients automatically; no manual backward method is needed.
class Affine(nn.Module):
def __init__(self,features,*,rngs):
super().__init__()
self.features = features
self.scale = nn.Parameter(jnp.ones(features))
self.offset = nn.Parameter(jnp.zeros(features))
def forward(self,x):
return x*self.scale.value + self.offset.value
model = nn.Sequential(Affine(3,rngs=js.Rngs(params=0)),
nn.Linear(3,1,rngs=js.Rngs(params=1)))
print(inspection.summary(model))
Module | Type | Own parameters
items.0 | Affine | 6
items.1 | Linear | 4
<root> | Sequential | 0
Total unique parameters: 10
Inspect without mutating the live model#
output_spec runs an evaluation clone. The architecture description records ownership, shapes, configuration, and aliases; it is not a full operation-level execution graph.
print(inspection.output_spec(model,jnp.ones((4,3))))
import json
print(json.dumps(inspection.describe(model),indent=2))
{'shape': [4, 1], 'dtype': 'float32'}
{
"schema_version": 1,
"scope": "module hierarchy; not an execution graph",
"root": {
"type": "jaxsaw.nn.module.Sequential",
"fields": {
"items": [
{
"type": "__main__.Affine",
"fields": {
"features": 3,
"scale": {
"kind": "Param",
"shape": [
3
],
"dtype": "float32"
},
"offset": {
"kind": "Param",
"shape": [
3
],
"dtype": "float32"
}
}
},
{
"type": "jaxsaw.nn.layers.Linear",
"fields": {
"in_features": 3,
"out_features": 1,
"weight": {
"kind": "Param",
"shape": [
1,
3
],
"dtype": "float32"
},
"bias": {
"kind": "Param",
"shape": [
1
],
"dtype": "float32"
}
}
}
]
}
}
}
Differentiate custom parameters#
Only Parameter leaves are differentiated by default. Buffers such as running statistics are persistent state but not optimizer parameters.
loss,grads = js.value_and_grad(lambda m:jnp.square(m(jnp.ones((4,3)))).mean())(model)
optimizer = optim.SGD(model.parameters(),lr=.01)
optimizer.step(grads)
print('Loss:',float(loss))
print('Learned affine scale:',np.asarray(model[0].scale.value))
Loss: 0.08691398799419403
Learned affine scale: [0.9986205 1.0018053 0.9978359]
Cross the pure functional boundary#
split separates static structure from dynamic state. apply returns output and updated state without mutating the original model. This is useful when integrating pure JAX computations.
graph,state = js.split(model)
x = jnp.ones((2,3))
output,next_state = js.apply(graph,state,x)
np.testing.assert_allclose(output,model(x))
print('Pure output:',np.asarray(output))
js.update(model,next_state)
Pure output: [[0.2679077]
[0.2679077]]
Register persistent buffers#
Use buffers for state you intend to checkpoint but not optimize. Side effects outside registered state, such as appending to a Python log, do not belong inside compiled steps.
class Center(nn.Module):
def __init__(self,mean):
super().__init__()
self.mean = nn.Buffer(jnp.asarray(mean,dtype=jnp.float32))
def forward(self,x):
return x-self.mean.value
center = Center([1.,2.,3.])
print('Buffers:',[(name,value.value.shape) for name,value in center.named_buffers()])
print('Trainable arrays:',len(list(center.parameters())))
print(center(jnp.array([[2.,4.,6.]])))
Buffers: [('mean', (3,))]
Trainable arrays: 0
[[1. 2. 3.]]