22 / 24 · Übung
Praktikum: Grenzfälle herausfordern
Ersetzen Sie im Auffüllungstestfall jedes Kernelgewicht durch eins. Wie lautet die erste Ausgabe und weshalb reicht ein Test nur der Mitte nicht?
Lektionen sind frei lesbar. Schreibe dich ein, um deinen Fortschritt zu speichern.
Zwei Dateien ausführen
Erstellen Sie model.mjs aus dem ersten Codeblock und corner-lab.mjs aus dem zweiten und führen Sie dann node corner-lab.mjs aus. Nur Nodes eingebautes assert wird benötigt. Sie können diese Dateien über einen unterstützten Ablauf im installierten Digital Design Studio manuell erstellen und untersuchen. Der Ausführungsnachweis hier ist der Node-Befehl; er enthält keine DDS-Simulation,Synthese oder Verbindung zu einem entfernten Projekt.
model.mjs bietet direkte Ganzzahlfaltung für einen Stapel, im2col ohne Auffüllung, Rundung zur nächsten Ganzzahl mit geradem Ergebnis bei Gleichstand und ungefülltes 2×2-Max-Pooling. Es verwendet eine gemeinsame ganzzahlige Schrittweite/Dilatation für beide Raumachsen mit vier unabhängigen Auffüllungen. Es prüft Gruppen und rechteckige Formen und lehnt unsichere Ganzzahlakkumulatoren ab. Dies ist weder ein allgemeines Modellaustauschwerkzeug noch eine vollständige ONNX-Laufzeit. Eingabecodes, Gewichtscodes und Bias müssen sichere JavaScript-Ganzzahlen sein (Betrag höchstens 9007199254740991). Jedes Produkt und jede Zwischensumme muss ebenfalls in diesem Bereich bleiben; andernfalls wird die Berechnung mit einem Fehler abgelehnt. Diese Ganzzahlregeln gelten für die Akkumulation in conv2d und dot; sie ersetzen nicht die Gleitkomma-Skalierungs- und Rundungsregeln von quantize und requantize.
Durchgerechnete Grenztestfälle
Eingabe [1,2,3;4,5,6;7,8,9] mit asymmetrischem 2×2-Kernel ergibt auf beiden Pfaden [37,47;67,77]. Mit Auffüllung oben 1, links 0, unten 0, rechts 1 ist das erste Fenster [0,0;1,2] und ergibt 11. Bei Dilatation zwei ohne Auffüllung ist das erste Fenster [1,3;7,9] und ergibt 1+6+21+36=64. Schreiben Sie die veränderten Koordinaten von Hand auf.
Zwei Kanäle mit 2 und 3, zwei Gruppen und Gewichten 4 und 5 erzeugen 8 und 15. Dieselbe Gewichtsform bei einer Gruppe muss einen Fehler werfen: Jede Ausgabe benötigt jetzt zwei Eingabekanäle, besitzt aber nur einen gespeicherten Kernel. Prüfen Sie auch Code 128 für reelle Null und beide int8-Begrenzungsgrenzen.
Führen Sie die ursprünglichen Prüfungen aus, kehren Sie dann vorübergehend Kernelindizes um und beobachten Sie das Fehlschlagen der ersten Assertion. Stellen Sie sie wieder her. Die Ablehnung der letzten gültigen Zeile durch eine falsche Grenzprüfung ist eine weitere nützliche Mutation. Entfernte Gruppenprüfungen sollten den Testfall zur Formablehnung beeinflussen. Speichern Sie nach Wiederherstellung einen erfolgreichen Lauf.
Diese Testfälle beginnen einen Regressionstestsatz und sind nicht vollständig. Ergänzen Sie Mehrkanal-1×1, Auffüllung mit von Null verschiedenem Nullpunkt, negative Kernel, ungerade Poolinggröße, leere Ausgabe und großen Bias. Geben Sie an, welche Regel jede Assertion beobachtet. Das Abschlussprojekt verwendet diese Referenz für den vollständigen kleinen Pfad wieder.
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');Selbst ausprobieren
Ersetzen Sie im Auffüllungstestfall jedes Kernelgewicht durch eins. Wie lautet die erste Ausgabe und weshalb reicht ein Test nur der Mitte nicht?
Erklärung lesen
Das erste Fenster [0,0;1,2] summiert sich zu drei. Ein mittleres Fenster greift nicht außerhalb der Grenzen zu und kann Fehler beim Auffüllwert oder bei oberen/linken Koordinaten übersehen.