22 / 24 · Praktikum
Praktikum: tantang kasus sudut
Dalam fixture padding, ganti setiap bobot kernel dengan satu. Berapa keluaran pertama dan mengapa pengujian hanya di tengah tidak cukup?
Pelajaran dapat dibaca gratis. Daftar untuk menyimpan progres.
Jalankan dua berkas
Buat model.mjs dari blok kode pertama dan corner-lab.mjs dari blok kedua,lalu jalankan node corner-lab.mjs. Hanya assert bawaan Node yang diperlukan. Anda boleh membuat dan memeriksa berkas ini secara manual melalui alur kerja Digital Design Studio terpasang yang didukung. Bukti eksekusi di sini adalah perintah Node; tidak mencakup simulasi DDS,sintesis,atau koneksi proyek jarak jauh.
model.mjs menyediakan konvolusi langsung bilangan bulat satu batch, im2col valid, pembulatan ke terdekat genap, dan max pooling 2×2 tanpa padding. Model menggunakan stride/dilation bilangan bulat yang sama pada kedua sumbu spasial, dengan empat padding independen. Model memeriksa kelompok dan bentuk persegi panjang serta menolak akumulator bilangan bulat yang tidak aman. Ini bukan alat pertukaran model umum atau runtime ONNX lengkap. Kode masukan, kode bobot, dan bias harus berupa bilangan bulat aman JavaScript (nilai absolut paling besar 9007199254740991). Setiap hasil perkalian dan jumlah parsial antara juga harus tetap dalam rentang itu; jika tidak, perhitungan ditolak dengan kesalahan. Kontrak bilangan bulat ini berlaku untuk akumulasi dalam conv2d dan dot; kontrak ini tidak mengganti aturan skala dan pembulatan floating-point pada quantize dan requantize.
Contoh fixture batas
Masukan [1,2,3;4,5,6;7,8,9] dengan kernel 2×2 asimetris memberi [37,47;67,77] melalui kedua jalur. Dengan padding atas 1, kiri 0, bawah 0, kanan 1, jendela pertama adalah [0,0;1,2], memberi 11. Dengan dilation dua tanpa padding, jendela pertama [1,3;7,9], memberi 1+6+21+36=64. Tuliskan koordinat yang berubah secara manual.
Dua kanal yang berisi 2 dan 3 dengan groups dua serta bobot 4 dan 5 menghasilkan 8 dan 15. Menggunakan ulang bentuk bobot itu dengan groups satu harus melempar kesalahan: setiap keluaran sekarang memerlukan dua kanal masukan tetapi hanya memiliki satu kernel tersimpan. Periksa juga kode nol riil 128 dan kedua batas pemotongan int8.
Jalankan pemeriksaan asli,lalu balik sementara indeks kernel dan amati assertion pertama gagal. Pulihkan. Menolak baris valid terakhir dengan uji batas yang salah juga merupakan mutasi berguna. Menghapus pemeriksaan kelompok seharusnya memengaruhi fixture penolakan bentuk. Simpan eksekusi lulus setelah pemulihan.
Fixture ini memulai kumpulan regresi; cakupannya tidak menyeluruh. Tambahkan multikanal 1×1, padding dengan titik nol bukan nol, kernel negatif, ukuran pooling ganjil, keluaran kosong, dan bias besar. Nyatakan kontrak mana yang diamati setiap assertion. Proyek akhir menggunakan kembali acuan ini untuk jalur kecil lengkap.
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');Coba sendiri
Dalam fixture padding, ganti setiap bobot kernel dengan satu. Berapa keluaran pertama dan mengapa pengujian hanya di tengah tidak cukup?
Baca penjelasan
Jendela pertama [0,0;1,2] berjumlah tiga. Jendela tengah tidak memiliki akses di luar batas dan dapat melewatkan kesalahan nilai padding atau koordinat atas/kiri.