22 / 24 · Thực hành
Thực hành: thử thách trường hợp biên
Trong dữ liệu đệm, thay mọi trọng số kernel bằng một. Đầu ra đầu là gì và vì sao chỉ kiểm tra giữa chưa đủ?
Bài học miễn phí. Đăng ký để lưu tiến độ học tập.
Chạy hai tệp
Tạo model.mjs từ khối mã đầu và corner-lab.mjs từ khối thứ hai,rồi chạy node corner-lab.mjs. Chỉ cần assert tích hợp của Node. Có thể tạo thủ công và xem các tệp qua quy trình Digital Design Studio đã cài được hỗ trợ. Bằng chứng thực thi ở đây là lệnh Node; không gồm mô phỏng DDS,tổng hợp hay kết nối dự án từ xa.
model.mjs cung cấp tích chập trực tiếp số nguyên một lô, im2col không đệm, làm tròn gần nhất chọn chẵn và gộp cực đại 2×2 không đệm. Nó dùng bước trượt/độ giãn nguyên chung trên cả hai trục không gian, với bốn đệm độc lập. Kiểm tra nhóm, kích thước hình chữ nhật và từ chối bộ tích lũy số nguyên không an toàn. Đây không phải công cụ trao đổi mô hình tổng quát hay runtime ONNX đầy đủ. Mã đầu vào, mã trọng số và bias phải là số nguyên an toàn của JavaScript (giá trị tuyệt đối tối đa 9007199254740991). Mỗi tích và mọi tổng riêng trung gian cũng phải nằm trong khoảng này; nếu không, phép tính bị từ chối với lỗi. Quy ước số nguyên này áp dụng cho tích lũy trong conv2d và dot; nó không thay thế quy tắc scale và làm tròn dấu phẩy động của quantize và requantize.
Tính dữ liệu biên
Đầu vào [1,2,3;4,5,6;7,8,9] với kernel 2×2 bất đối xứng cho [37,47;67,77] bằng cả hai đường. Với đệm trên 1,trái 0,dưới 0,phải 1, cửa sổ đầu là [0,0;1,2], cho 11. Với độ giãn hai và không đệm, cửa sổ đầu là [1,3;7,9], cho 1+6+21+36=64. Viết tay tọa độ đã đổi.
Hai kênh chứa 2 và 3 với nhóm hai, trọng số 4 và 5 tạo 8 và 15. Tái dùng kích thước trọng số đó với nhóm một phải gây lỗi: mỗi đầu ra giờ cần hai kênh đầu vào nhưng chỉ có một kernel lưu. Cũng kiểm tra mã số 0 thực 128 và cả hai biên cắt int8.
Chạy kiểm tra gốc,rồi tạm đảo chỉ số kernel và quan sát assertion đầu thất bại. Khôi phục. Từ chối hàng hợp lệ cuối bằng điều kiện biên sai là đột biến hữu ích khác. Bỏ kiểm tra nhóm phải ảnh hưởng dữ liệu từ chối kích thước. Lưu lần chạy vượt kiểm tra sau khôi phục.
Các dữ liệu này bắt đầu tập hồi quy; chưa đầy đủ. Thêm 1×1 nhiều kênh,đệm có điểm 0 khác 0,kernel âm,kích thước gộp lẻ,đầu ra trống và bias lớn. Nêu quy ước mỗi assertion quan sát. Dự án tổng kết tái dùng tham chiếu này cho đường nhỏ hoàn chỉnh.
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');Tự thử
Trong dữ liệu đệm, thay mọi trọng số kernel bằng một. Đầu ra đầu là gì và vì sao chỉ kiểm tra giữa chưa đủ?
Đọc giải thích
Cửa sổ đầu [0,0;1,2] có tổng ba. Cửa sổ giữa không truy cập ngoài giới hạn và có thể bỏ sót lỗi giá trị đệm hoặc tọa độ trên/trái.