Операции с тензорами через ort.Tensor и ручной постпроцессинг сложных выходов

ort.Tensor представляет собой ключевой объект библиотеки ONNX Runtime Web (ORT Web) для хранения и обработки данных модели. Каждый тензор содержит три основных элемента:

  • Данные (data) — одно- или многомерный массив значений, соответствующих типу данных тензора (например, Float32Array, Int32Array и др.).
  • Форма (dims) — массив целых чисел, задающий размерность тензора по каждой оси.
  • Тип данных (type) — строковое обозначение типа данных, например float32, int32, bool.

Создание тензора вручную осуществляется через конструктор ort.Tensor:

const tensor = new ort.Tensor('float32', new Float32Array([1.0, 2.0, 3.0, 4.0]), [2, 2]);

В этом примере создается тензор размером 2×2 с четырьмя элементами типа float32.


Чтение и преобразование данных тензора

Доступ к данным тензора осуществляется через свойство tensor.data. Оно возвращает плоский массив значений. Для работы с многомерными структурами необходимо самостоятельно реализовать развертку индексов или использовать вспомогательные функции.

Пример преобразования плоского массива в матрицу 2×2:

function reshape2D(data, rows, cols) {
  const result = [];
  for (let i = 0; i < rows; i++) {
    const row = [];
    for (let j = 0; j < cols; j++) {
      row.push(data[i * cols + j]);
    }
    result.push(row);
  }
  return result;
}

const matrix = reshape2D(tensor.data, 2, 2);

reshape2D позволяет создавать более наглядное представление тензора для последующей постобработки.


Выполнение операций над тензорами

ONNX Runtime Web не предоставляет встроенных методов для сложной матричной арифметики, поэтому операции над ort.Tensor выполняются вручную через массивы TypedArray. Наиболее часто используемые операции:

Сложение и вычитание

function addTensors(a, b) {
  if (a.data.length !== b.data.length) {
    throw new Error("Размеры тензоров должны совпадать");
  }
  const result = new Float32Array(a.data.length);
  for (let i = 0; i < a.data.length; i++) {
    result[i] = a.data[i] + b.data[i];
  }
  return new ort.Tensor('float32', result, a.dims);
}

Аналогично реализуется вычитание, умножение и деление поэлементно.

Матрица на матрицу

Для операций типа matmul (матрица × матрица) необходимо явно учитывать форму тензоров:

function matmul(a, b) {
  const [m, k1] = a.dims;
  const [k2, n] = b.dims;
  if (k1 !== k2) throw new Error("Несовпадение размерностей для умножения матриц");
  
  const result = new Float32Array(m * n);
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      let sum = 0;
      for (let k = 0; k < k1; k++) {
        sum += a.data[i * k1 + k] * b.data[k * n + j];
      }
      result[i * n + j] = sum;
    }
  }
  return new ort.Tensor('float32', result, [m, n]);
}

Этот метод подходит для любых двумерных тензоров и обеспечивает полное управление вычислением.


Ручной постпроцессинг сложных выходов модели

Модели ONNX часто возвращают несколько выходов или тензоры со сложной структурой (например, батч прогнозов, bounding boxes, вероятности классов). В таких случаях необходима ручная десериализация и агрегация данных:

Пример: извлечение прогнозов классификации

const outputTensor = session.run(inputs)['output'];
const probabilities = outputTensor.data;
const topIndex = probabilities.indexOf(Math.max(...probabilities));

Для батчей прогнозов необходимо учитывать дополнительную размерность:

function batchArgmax(tensor) {
  const [batchSize, numClasses] = tensor.dims;
  const result = [];
  for (let i = 0; i < batchSize; i++) {
    let maxIndex = 0;
    let maxValue = tensor.data[i * numClasses];
    for (let j = 1; j < numClasses; j++) {
      const value = tensor.data[i * numClasses + j];
      if (value > maxValue) {
        maxValue = value;
        maxIndex = j;
      }
    }
    result.push(maxIndex);
  }
  return result;
}

Обработка bounding boxes

Модели детекции объектов возвращают несколько тензоров: координаты, вероятности и классы. Объединение их в структуру объектов:

function parseDetections(boxesTensor, scoresTensor, classesTensor, threshold = 0.5) {
  const detections = [];
  const numBoxes = boxesTensor.dims[0];
  for (let i = 0; i < numBoxes; i++) {
    const score = scoresTensor.data[i];
    if (score < threshold) continue;
    const box = boxesTensor.data.slice(i * 4, i * 4 + 4);
    const classId = classesTensor.data[i];
    detections.push({ box, score, classId });
  }
  return detections;
}

Такой подход позволяет полностью контролировать логику фильтрации, нормализации координат и объединения результатов.


Применение пользовательских функций активации и нормализации

В некоторых случаях модель возвращает сырые предсказания, требующие дополнительной активации (softmax, sigmoid) или нормализации:

function softmax(arr) {
  const max = Math.max(...arr);
  const exps = arr.map(x => Math.exp(x - max));
  const sum = exps.reduce((a, b) => a + b, 0);
  return exps.map(v => v / sum);
}

Для батчей применяется по каждой строке тензора:

function batchSoftmax(tensor) {
  const [batchSize, numClasses] = tensor.dims;
  const result = new Float32Array(tensor.data.length);
  for (let i = 0; i < batchSize; i++) {
    const slice = tensor.data.slice(i * numClasses, (i + 1) * numClasses);
    const soft = softmax(slice);
    result.set(soft, i * numClasses);
  }
  return new ort.Tensor('float32', result, tensor.dims);
}

Итоговый подход к сложным выходам

Работа с ort.Tensor требует аккуратного сочетания:

  1. Явного контроля размерностей для предотвращения ошибок при индексировании.
  2. Ручной реализации арифметических операций или функций агрегации.
  3. Постпроцессинга выходов, включая фильтрацию, нормализацию и конвертацию в удобные структуры.

Применение этих методов позволяет обрабатывать любые выходные данные модели, управляя ими полностью в коде на JavaScript без зависимости от сторонних библиотек.