The full encode
# grade this stage (from your course directory)
npx theinference test --stage encode --watchNow compose the whole projection head for a batch of already-pooled sentences: linear1 → tanh → linear2 → L2-normalize. The output is the [B, 32] matrix of unit-length sentence embeddings — the thing the contrastive loss will operate on.
pooled[b] ──linear(W1,b1)──▶ h1 ──tanh──▶ a1
──linear(W2,b2)──▶ h2 ──L2-norm──▶ out[b] (‖out[b]‖ = 1)
What to build
Implement encode
Implement runEncode(pooled, W1, b1, W2, b2): given a [B, 32] pooled batch and the MLP weights, return the [B, 32] matrix of L2-normalized sentence embeddings. Apply the head row by row, reusing your linear and tanh.
export function runEncode(
pooled: number[][], W1: number[][], b1: number[], W2: number[][], b2: number[],
): number[][] {
// each row: linear(W1,b1) -> tanh -> linear(W2,b2) -> l2normalize
}
Adapter mode: your [B, 32] matrix is compared to the reference with allclose, atol 1e-5, rtol 1e-4. The fixture uses the frozen init weights and a fixed pooled batch.
L2-normalize is the final step, after the second linear. Normalizing earlier (e.g. right after pooling, or between the linears) changes the math and the gradient — the reference normalizes exactly once, at the very end.