Training and evaluation#
Write an explicit optimization step, clip gradients, separate training from evaluation, and aggregate held-out metrics.
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)]
Prepare a controlled classification task#
The two-dimensional synthetic task makes the loop easy to inspect. Training and held-out examples use independent random draws. This is an API exercise, not a dataset benchmark.
rng = np.random.default_rng(8)
x = rng.normal(size=(256,2)).astype(np.float32)
y = (x[:,0] + .5*x[:,1] > 0).astype(np.int32)
train = (jnp.asarray(x[:192]), jnp.asarray(y[:192]))
valid = (jnp.asarray(x[192:]), jnp.asarray(y[192:]))
class Classifier(nn.Module):
def __init__(self, *, rngs):
super().__init__()
self.hidden = nn.Linear(2,16,rngs=rngs)
self.dropout = nn.Dropout(.1)
self.head = nn.Linear(16,2,rngs=rngs)
def forward(self,x,*,rngs=None):
return self.head(self.dropout(F.relu(self.hidden(x)),rngs=rngs))
model = Classifier(rngs=js.Rngs(params=8))
optimizer = optim.Adam(model.parameters(),lr=.02)
runtime = js.Rngs(dropout=9)
Differentiate, clip, update#
The step returns gradients explicitly, then applies one optimizer update. Auxiliary predictions supply metrics without a second stochastic forward pass.
@js.jit
def train_step(model, optimizer, batch, rngs):
x,y = batch
def objective(candidate):
logits = candidate(x,rngs=rngs)
return F.cross_entropy(logits,y), (logits.argmax(-1)==y).sum()
(loss,correct), grads = js.value_and_grad(objective,has_aux=True)(model)
grads,norm = optim.clip_grad_norm(grads,1.)
optimizer.step(grads)
return {'loss':loss,'correct':correct,'count':y.size,'gradient_norm':norm}
model.train()
for step in range(100):
result = train_step(model,optimizer,train,runtime)
print({name:float(value) for name,value in result.items()})
print('Optimizer updates:',int(optimizer.step_count))
{'correct': 191.0, 'count': 192.0, 'gradient_norm': 0.02445269748568535, 'loss': 0.02627979777753353}
Optimizer updates: 100
Evaluate without updating state#
Evaluation mode changes dropout and normalization behavior; it is not a gradient switch. Validation uses an explicit, separate function and a held-out batch.
@js.jit
def evaluate(model,batch):
x,y = batch
logits = model(x)
return {'loss':F.cross_entropy(logits,y),
'correct':(logits.argmax(-1)==y).sum(),'count':y.size}
model.eval()
result = evaluate(model,valid)
print('Held-out loss:',float(result['loss']))
print('Held-out accuracy:',float(result['correct']/result['count']))
Held-out loss: 0.031200522556900978
Held-out accuracy: 1.0
Aggregate by valid item count#
Do not average batch means with equal weight when the final batch is smaller. Masked sequence tasks should report valid-token counts.
from jaxsaw.metrics import ClassificationAccumulator
accumulator = ClassificationAccumulator()
for start in range(0,len(valid[0]),15):
batch = tuple(array[start:start+15] for array in valid)
accumulator.update(evaluate(model,batch))
print(accumulator.compute())
{'loss': 0.031184522405965254, 'accuracy': 1.0}
Adapt the objective#
Change the scalar objective for regression, reconstruction, or multiple supervised heads. In adversarial training, use separate optimizers and select the differentiated module explicitly. See the Fashion-MNIST example for both phases.