Altifigence Academy

22 / 24 · Практика

Лабораторная работа: испытайте угловые случаи

В примере дополнения замените каждый вес ядра единицей. Каков первый выход и почему теста только центра недостаточно?

Выполните два файла

Создайте model.mjs из первого блока кода и corner-lab.mjs из второго,затем выполните node corner-lab.mjs. Нужен только встроенный assert Node. Можно вручную создать и исследовать файлы через поддерживаемый процесс установленной Digital Design Studio. Свидетельство выполнения здесь — команда Node; оно не включает симуляцию DDS,синтез или подключение удалённого проекта.

model.mjs предоставляет целочисленную прямую свёртку одного пакета, im2col без дополнения, округление к ближайшему чётному и максимальный пулинг 2×2 без дополнения. Используется общий целочисленный шаг/прореживание обеих пространственных осей с четырьмя независимыми дополнениями. Проверяются группы и прямоугольные формы, небезопасные целочисленные аккумуляторы отклоняются. Это не общий инструмент обмена моделями или полная среда ONNX. Коды входа, коды весов и bias должны быть безопасными целыми числами JavaScript (модуль не более 9007199254740991). Каждое произведение и каждая промежуточная частичная сумма также должны оставаться в этом диапазоне; иначе вычисление отклоняется с ошибкой. Этот целочисленный контракт применяется к накоплению в conv2d и dot; он не заменяет правила масштаба и округления с плавающей точкой в quantize и requantize.

Разобранные граничные примеры

Вход [1,2,3;4,5,6;7,8,9] с асимметричным ядром 2×2 даёт [37,47;67,77] обоими путями. При дополнении сверху 1, слева 0, снизу 0, справа 1 первое окно — [0,0;1,2], дающее 11. При прореживании два без дополнения первое окно — [1,3;7,9], дающее 1+6+21+36=64. Запишите изменённые координаты вручную.

Два канала со значениями 2 и 3 при двух группах и весах 4 и 5 дают 8 и 15. Повторное использование этой формы весов с одной группой должно вызвать исключение: каждому выходу теперь нужны два входных канала, а хранится лишь одно ядро. Также проверяйте код действительного нуля 128 и обе границы ограничения int8.

Выполните исходные проверки,затем временно переверните индексы ядра и наблюдайте провал первого утверждения. Восстановите их. Отклонение последней допустимой строки неверной проверкой границы — ещё одна полезная мутация. Удаление проверок групп должно влиять на тест отклонения формы. Сохраните успешный запуск после восстановления.

Эти примеры начинают регрессионный набор; они не исчерпывающие. Добавьте многоканальный 1×1, дополнение с ненулевой нулевой точкой, отрицательные ядра, нечётный размер пулинга, пустой выход и большой bias. Укажите, какое требование наблюдает каждое утверждение. Итоговый проект повторно использует этот эталон для полного малого пути.

model.mjs

javascript
// 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

javascript
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');

Попробуйте сами

В примере дополнения замените каждый вес ядра единицей. Каков первый выход и почему теста только центра недостаточно?

Прочитать объяснение

Первое окно [0,0;1,2] даёт сумму три. Центральное окно не обращается вне границ и может пропустить ошибки значения дополнения или координат сверху/слева.

Lesson files

model.mjs ↓corner-lab.mjs ↓

Выбор действует в этом браузере. Его можно изменить внизу страницы.