22 / 24 · Esercitazione
Laboratorio: metti alla prova i casi limite
Nel caso di riempimento, sostituisci ogni peso del kernel con uno. Qual è la prima uscita e perché un test soltanto al centro è insufficiente?
Le lezioni sono gratuite. Iscriviti per salvare i progressi.
Esegui due file
Crea model.mjs dal primo blocco di codice e corner-lab.mjs dal secondo,poi esegui node corner-lab.mjs. Occorre soltanto assert integrato di Node. Puoi creare e ispezionare manualmente questi file tramite un flusso supportato di Digital Design Studio installato. La prova di esecuzione qui è il comando Node; non include simulazione DDS,sintesi o connessione a un progetto remoto.
model.mjs fornisce convoluzione diretta intera con un lotto, im2col senza riempimento, arrotondamento al pari e max pooling 2×2 senza riempimento. Usa passo/dilatazione interi comuni su entrambi gli assi spaziali, con quattro riempimenti indipendenti. Verifica gruppi e forme rettangolari e rifiuta accumulatori interi non sicuri. Non è uno strumento generale di scambio modelli o un runtime ONNX completo. I codici di ingresso, i codici dei pesi e il bias devono essere interi sicuri JavaScript (valore assoluto massimo 9007199254740991). Anche ogni prodotto e somma parziale intermedia deve rimanere in tale intervallo; altrimenti il calcolo viene rifiutato con un errore. Questo contratto intero si applica all’accumulo in conv2d e dot; non sostituisce le regole di scala e arrotondamento in virgola mobile di quantize e requantize.
Casi limite svolti
L'ingresso [1,2,3;4,5,6;7,8,9] con un kernel 2×2 asimmetrico dà [37,47;67,77] in entrambi i percorsi. Con riempimenti superiore 1, sinistro 0, inferiore 0, destro 1, la prima finestra è [0,0;1,2], dando 11. Con dilatazione due e senza riempimento, la prima finestra è [1,3;7,9], dando 1+6+21+36=64. Scrivi a mano le coordinate cambiate.
Due canali contenenti 2 e 3 con due gruppi e pesi 4 e 5 producono 8 e 15. Riutilizzare quella forma dei pesi con un gruppo deve generare un errore: ogni uscita ora richiede due canali di ingresso ma ha soltanto un kernel memorizzato. Verifica anche codice 128 dello zero reale ed entrambi i limiti di limitazione int8.
Esegui le verifiche originali,poi inverti temporaneamente gli indici del kernel e osserva fallire la prima asserzione. Ripristinala. Rifiutare l'ultima riga valida con un test di limite errato è un'altra mutazione utile. Rimuovere le verifiche dei gruppi deve influenzare il caso di rifiuto della forma. Salva un'esecuzione riuscita dopo il ripristino.
Questi casi iniziano un insieme di regressione; non sono esaustivi. Aggiungi 1×1 multicanale, riempimento con punto zero non nullo, kernel negativi, dimensione dispari del pooling, uscita vuota e bias grande. Dichiara quale contratto osserva ogni asserzione. Il progetto finale riutilizza questo riferimento per l'intero piccolo percorso.
model.mjs
// Small, dependency-free teaching model. Layout: X[C][H][W], W[M][C/group][R][S].
export function outputSize(size, kernel, stride = 1, padBefore = 0, padAfter = 0, dilation = 1) {
if (![size, kernel, stride, dilation].every(v => Number.isInteger(v) && v > 0) ||
![padBefore, padAfter].every(v => Number.isInteger(v) && v >= 0)) throw new RangeError('Invalid shape');
return Math.max(0, Math.floor((size + padBefore + padAfter - dilation * (kernel - 1) - 1) / stride) + 1);
}
export function roundEven(value) {
if (!Number.isFinite(value)) throw new RangeError('Finite number required');
const floor = Math.floor(value), fraction = value - floor;
return fraction < 0.5 ? floor : fraction > 0.5 ? floor + 1 : floor % 2 === 0 ? floor : floor + 1;
}
export const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
export function quantize(value, scale, zeroPoint = 0, low = -128, high = 127) {
if (!(scale > 0) || !Number.isFinite(scale)) throw new RangeError('Positive finite scale required');
return clamp(roundEven(value / scale) + zeroPoint, low, high);
}
export const dequantize = (value, scale, zeroPoint = 0) => (value - zeroPoint) * scale;
export function requantize(acc, multiplier, zeroPoint = 0, low = -128, high = 127) {
return clamp(roundEven(acc * multiplier) + zeroPoint, low, high);
}
export function conv2d(x, weights, {stride = 1, pads = [0, 0, 0, 0], dilation = 1, groups = 1, bias = null} = {}) {
const channels = x.length, height = x[0]?.length, width = x[0]?.[0]?.length;
const outputs = weights.length, r = weights[0]?.[0]?.length, s = weights[0]?.[0]?.[0]?.length;
if (!Number.isInteger(groups) || groups < 1 || channels % groups || outputs % groups) throw new RangeError('Invalid groups');
if (pads.length !== 4) throw new RangeError('Four explicit pads required');
if (!height || !width || !r || !s || x.some(c => c.length !== height || c.some(row => row.length !== width)) ||
weights.some(m => m.length !== channels / groups || m.some(c => c.length !== r || c.some(row => row.length !== s))))
throw new RangeError('Ragged or incompatible tensor');
if (bias && bias.length !== outputs) throw new RangeError('Bias shape');
if (x.flat(2).some(value => !Number.isSafeInteger(value)) ||
weights.flat(3).some(value => !Number.isSafeInteger(value)) ||
(bias && bias.some(value => !Number.isSafeInteger(value))))
throw new RangeError('Safe integer operands and bias required');
const [top, left, bottom, right] = pads;
const outH = outputSize(height, r, stride, top, bottom, dilation);
const outW = outputSize(width, s, stride, left, right, dilation);
const y = Array.from({length: outputs}, () => Array.from({length: outH}, () => Array(outW).fill(0)));
for (let m = 0; m < outputs; m++) for (let h = 0; h < outH; h++) for (let w = 0; w < outW; w++) {
let acc = bias?.[m] ?? 0;
const firstC = Math.floor(m / (outputs / groups)) * (channels / groups);
for (let c = 0; c < channels / groups; c++) for (let kh = 0; kh < r; kh++) for (let kw = 0; kw < s; kw++) {
const ih = h * stride - top + kh * dilation, iw = w * stride - left + kw * dilation;
if (ih >= 0 && ih < height && iw >= 0 && iw < width) {
const product = x[firstC + c][ih][iw] * weights[m][c][kh][kw];
if (!Number.isSafeInteger(product)) throw new RangeError('Unsafe integer product');
const next = acc + product;
if (!Number.isSafeInteger(next)) throw new RangeError('Unsafe integer partial sum');
acc = next;
}
}
if (!Number.isSafeInteger(acc)) throw new RangeError('This teaching model requires safe integer accumulators');
y[m][h][w] = acc;
}
return y;
}
export function im2col(x, kernelH, kernelW) {
const height = x[0].length, width = x[0][0].length, rows = [];
for (let h = 0; h <= height - kernelH; h++) for (let w = 0; w <= width - kernelW; w++) {
const row = [];
for (const channel of x) for (let r = 0; r < kernelH; r++) for (let s = 0; s < kernelW; s++) row.push(channel[h + r][w + s]);
rows.push(row);
}
return rows;
}
export function dot(a, b) {
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
throw new RangeError('Incompatible dot vectors');
let acc = 0;
for (let i = 0; i < a.length; i++) {
if (!Number.isSafeInteger(a[i]) || !Number.isSafeInteger(b[i]))
throw new RangeError('Safe integer dot operands required');
const product = a[i] * b[i];
if (!Number.isSafeInteger(product)) throw new RangeError('Unsafe integer dot product');
const next = acc + product;
if (!Number.isSafeInteger(next)) throw new RangeError('Unsafe integer dot partial sum');
acc = next;
}
return acc;
}
export function maxPool2x2(matrix) {
const output = [];
for (let h = 0; h + 1 < matrix.length; h += 2) {
const row = [];
for (let w = 0; w + 1 < matrix[0].length; w += 2)
row.push(Math.max(matrix[h][w], matrix[h][w + 1], matrix[h + 1][w], matrix[h + 1][w + 1]));
output.push(row);
}
return output;
}corner-lab.mjs
import assert from 'node:assert/strict';
import {conv2d, im2col, dot, outputSize, quantize, requantize} from './model.mjs';
const x = [[[1, 2, 3], [4, 5, 6], [7, 8, 9]]], w = [[[[1, 2], [3, 4]]]];
assert.deepEqual(conv2d(x, w), [[[37, 47], [67, 77]]]);
assert.deepEqual(im2col(x, 2, 2).map(row => dot(row, [1, 2, 3, 4])), [37, 47, 67, 77]);
assert.equal(conv2d(x, w, {pads:[1, 0, 0, 1]})[0][0][0], 11);
assert.equal(conv2d(x, w, {dilation:2})[0][0][0], 64);
assert.equal(outputSize(5, 3, 2, 1, 1, 1), 3);
assert.equal(outputSize(5, 3, 2, 1, 1, 2), 2);
assert.equal(quantize(0, 0.25, 128, 0, 255), 128);
assert.equal(requantize(300, 1), 127);
assert.equal(requantize(-300, 1), -128);
const channels = [[[2]], [[3]]], groupWeights = [[[[4]]], [[[5]]]];
assert.deepEqual(conv2d(channels, groupWeights, {groups:2}), [[[8]], [[15]]]);
assert.throws(() => conv2d(channels, groupWeights), /incompatible/);
// Guard every arithmetic step: cancellation must not hide lost precision.
const safe = Number.MAX_SAFE_INTEGER, pairWeights = [[[[1, 1]]]];
assert.throws(() => conv2d([[[2, -2]]], pairWeights, {bias:[safe]}), /partial sum/);
assert.throws(() => conv2d([[[-2, 2]]], pairWeights, {bias:[-safe]}), /partial sum/);
assert.throws(() => conv2d([[[safe + 1]]], [[[[1]]]]), /operands/);
assert.throws(() => conv2d([[[0]]], [[[[safe + 1]]]]), /operands/);
assert.throws(() => conv2d([[[-1]]], [[[[1]]]], {bias:[safe + 1]}), /bias/);
assert.throws(() => conv2d([[[0.5]]], [[[[2]]]]), /operands/);
assert.throws(() => conv2d([[[100000000]]], [[[[100000000]]]]), /product/);
// Safe boundary arithmetic and zero output dimensions remain supported.
assert.deepEqual(conv2d([[[2, -2]]], pairWeights, {bias:[safe - 2]}), [[[safe - 2]]]);
assert.deepEqual(conv2d([[[1]]], [[[[1, 1], [1, 1]]]]), [[]]);
assert.deepEqual(conv2d([[[1]]], pairWeights), [[[]]]);
// The independent patch-dot path uses the same integer safety contract.
assert.throws(() => dot([safe, 2, -2], [1, 1, 1]), /partial sum/);
assert.throws(() => dot([safe + 1], [0]), /operands/);
assert.throws(() => dot([100000000], [100000000]), /product/);
assert.throws(() => dot([1], [1, 2]), /vectors/);
assert.equal(dot([safe - 2, 2, -2], [1, 1, 1]), safe - 2);
assert.equal(dot([], []), 0);
console.log('corner-lab: 27 checks passed');Prova tu
Nel caso di riempimento, sostituisci ogni peso del kernel con uno. Qual è la prima uscita e perché un test soltanto al centro è insufficiente?
Leggi la spiegazione
La prima finestra [0,0;1,2] somma a tre. Una finestra centrale non accede fuori dai limiti e può non rilevare errori nel valore di riempimento o nelle coordinate superiore/sinistra.