Операции над тензорами

Урок про операции над тензорами: сложение, умножение поэлементно и матрично, агрегации.

Поэлементная операция применяется к каждой паре соответствующих элементов независимо, а матричное умножение комбинирует строки и столбцы по правилам линейной алгебры.

Нейросеть — это по сути цепочка операций над тензорами: умножить вход на матрицу весов, прибавить смещение, применить активацию, повторить. Поэтому стоит твёрдо различать два вида умножения, которые легко спутать.

Поэлементно против матрично

Поэлементное умножение (*) перемножает элементы один к одному и требует одинаковой формы. Матричное (tf.matmul или @) перемножает по правилу «строка на столбец». Проиллюстрируем оба на чистом Python:

a = [[1, 2], [3, 4]]
b = [[5, 6], [7, 8]]

# поэлементно
elem = [[a[i][j] * b[i][j] for j in range(2)] for i in range(2)]

# матрично (строка на столбец)
mat = [[sum(a[i][k] * b[k][j] for k in range(2)) for j in range(2)] for i in range(2)]

print("поэлементно:", elem)
print("матрично:   ", mat)

Вывод:

поэлементно: [[5, 12], [21, 32]]
матрично:    [[19, 22], [43, 50]]

Видно: результаты разные. В нейросетях вход на веса умножают именно матрично.

Агрегации и редукции

Редукции «схлопывают» тензор: сумма, среднее, максимум по оси. Они нужны постоянно — например, чтобы усреднить ошибку по батчу. Посчитаем среднее и сумму на stdlib:

import statistics

values = [4, 8, 15, 16, 23, 42]
print("сумма:", sum(values))
print("среднее:", statistics.mean(values))
print("максимум:", max(values))

Вывод:

сумма: 108
среднее: 18
максимум: 42

Те же операции в TensorFlow

В TF это выглядит так (требует TF, в браузере не идёт):

import tensorflow as tf

a = tf.constant([[1.0, 2.0], [3.0, 4.0]])
b = tf.constant([[5.0, 6.0], [7.0, 8.0]])

print(a * b)            # поэлементно
print(tf.matmul(a, b))  # матрично, то же что a @ b
print(tf.reduce_sum(a)) # 10.0
print(tf.reduce_mean(a, axis=0))  # среднее по столбцам

Как работает под капотом

TF не выполняет операции в Python-цикле — он вызывает оптимизированные C++/CUDA-ядра, которые обрабатывают миллионы чисел параллельно (это называется векторизацией). Поэтому никогда не пишите ручные циклы по элементам тензора: один tf.matmul на тысячи раз быстрее эквивалентного Python-цикла.

Частые ошибки

  • Путать * и tf.matmul. Звёздочка — поэлементно, для линейного слоя нужно матричное умножение.
  • Несогласованные формы при matmul. Внутренние размерности обязаны совпадать: (m,k)·(k,n).
  • Ручные циклы по тензору. Это медленно; используйте векторные операции TF.

Итог

  • Поэлементные операции (*) работают над одинаковыми формами элемент к элементу.
  • Матричное умножение (tf.matmul, @) комбинирует строки и столбцы — основа линейных слоёв.
  • Редукции (reduce_sum, reduce_mean) схлопывают тензор по оси.
  • Все операции векторизованы — ручные циклы по элементам не нужны и вредны для скорости.
Проверьте себя
1. Какая операция нужна, чтобы умножить вход слоя на матрицу весов?
AПоэлементное умножение (*)
BМатричное умножение (tf.matmul)
CСложение
DРедукция reduce_sum
2. Что делает tf.reduce_mean?
AПеремножает тензоры
BУсредняет элементы по оси, схлопывая тензор
CМеняет форму тензора
DСоздаёт обучаемую переменную
3. Почему не стоит писать ручные Python-циклы по элементам тензора?
AЭто запрещено синтаксисом
BВекторизованные операции TF на C++/CUDA в разы быстрее
CЦиклы дают неверный результат
DTF не поддерживает циклы