Broadcasting: как NumPy сопоставляет массивы разных форм

Урок детально разбирает broadcasting — механизм, позволяющий выполнять операции над массивами разных форм без явного копирования и циклов.

Broadcasting — правило, по которому NumPy автоматически «растягивает» массивы меньшей формы до большей, сопоставляя их оси справа налево, чтобы выполнить поэлементную операцию.

Зачем нужен broadcasting

Самый простой случай вы уже видели: a + 1 прибавляет единицу к каждому элементу. Здесь скаляр 1 «растягивается» до формы массива. Broadcasting обобщает эту идею на массивы: он позволяет, например, прибавить вектор к каждой строке матрицы или нормировать столбцы — без циклов и без ручного копирования. Это один из самых элегантных и одновременно запутывающих механизмов NumPy.

Ключевая мысль: при broadcasting данные физически не копируются. NumPy лишь делает вид, что ось длины 1 повторяется нужное число раз, используя нулевой stride. Память не растёт, скорость остаётся высокой.

Правило выравнивания: справа налево

Чтобы понять, совместимы ли две формы, NumPy сопоставляет их оси начиная с последней (самой правой) и двигаясь влево. Если у массивов разное число осей, более короткую форму мысленно дополняют единицами слева. Затем для каждой пары осей проверяется совместимость.

Две оси совместимы, если выполнено одно из двух:

  • их размеры равны, либо
  • одна из них равна 1 (она «растянется» до размера другой).

Если хотя бы одна пара осей не удовлетворяет этому, NumPy выбрасывает ValueError: operands could not be broadcast together. Итоговая форма по каждой оси — максимум из двух размеров.

Форма AФорма BРезультат
(4, 3)(3,)(4, 3)
(4, 3)(1,)(4, 3)
(4, 1)(1, 3)(4, 3)
(15, 3, 5)(15, 1, 5)(15, 3, 5)
(15, 3, 5)(3, 5)(15, 3, 5)
(3,)(4,)ОШИБКА
(2, 1)(8, 4, 3)ОШИБКА

Самый частый случай: вектор и матрица

Прибавление вектора длины 3 к матрице 4×3. Выравниваем справа: последняя ось матрицы (3) совпадает с осью вектора (3) — совместимо. У вектора нет второй оси, она дополняется до 1, а 1 растягивается до 4. Результат — каждая строка матрицы получает один и тот же вектор.

import numpy as np
m = np.array([[0,  0,  0],
              [10, 10, 10],
              [20, 20, 20],
              [30, 30, 30]])     # (4, 3)
v = np.array([1, 2, 3])          # (3,)

print(m + v)   # v прибавляется к каждой строке

Вывод:

[[ 1  2  3]
 [11 12 13]
 [21 22 23]
 [31 32 33]]

Без broadcasting пришлось бы писать цикл по строкам. Воспроизведём смысл на чистом Python, чтобы стало видно, что именно «делает» NumPy:

m = [[0, 0, 0], [10, 10, 10], [20, 20, 20], [30, 30, 30]]
v = [1, 2, 3]

# v растягивается на каждую строку
result = [[m[i][j] + v[j] for j in range(3)] for i in range(len(m))]
for row in result:
    print(row)

Вывод:

[1, 2, 3]
[11, 12, 13]
[21, 22, 23]
[31, 32, 33]

Заметьте побочное наблюдение: broadcasting делает код не только короче, но и устойчивее к ошибкам. Цикл по индексам легко написать с опечаткой в границах или перепутать i и j; векторное выражение m + v такой возможности не даёт — либо формы совместимы и операция корректна, либо NumPy сразу сообщит об ошибке. Меньше ручного управления индексами — меньше мест для багов.

Растяжение по двум осям сразу: столбец + строка

Когда у одного массива форма (4, 1), а у другого (1, 3), broadcasting растягивает обе единичные оси: первая до 4, вторая до 3, давая (4, 3). Так строятся таблицы из двух векторов. Здесь видно, почему в прошлом разделе мы добавляли оси через np.newaxis.

import numpy as np
col = np.array([0, 10, 20, 30])[:, np.newaxis]   # (4, 1)
row = np.array([1, 2, 3])[np.newaxis, :]          # (1, 3)

print((col + row).shape)
print(col + row)

Вывод:

(4, 3)
[[ 1  2  3]
 [11 12 13]
 [21 22 23]
 [31 32 33]]

Реализуем правило broadcasting сами

Чтобы окончательно закрепить правило, напишем на чистом Python функцию, которая по двум формам вычисляет результирующую (или сообщает об ошибке). Это ровно та логика, что внутри NumPy:

def broadcast_shape(s1, s2):
    # Выравниваем справа: разворачиваем и идём по парам
    r1, r2 = list(reversed(s1)), list(reversed(s2))
    n = max(len(r1), len(r2))
    out = []
    for i in range(n):
        a = r1[i] if i < len(r1) else 1   # недостающие оси = 1
        b = r2[i] if i < len(r2) else 1
        if a == b or a == 1 or b == 1:
            out.append(max(a, b))
        else:
            return "ОШИБКА: %d и %d несовместимы" % (a, b)
    return tuple(reversed(out))

print(broadcast_shape((4, 3), (3,)))
print(broadcast_shape((4, 1), (1, 3)))
print(broadcast_shape((15, 3, 5), (3, 5)))
print(broadcast_shape((3,), (4,)))

Вывод:

(4, 3)
(4, 3)
(15, 3, 5)
ОШИБКА: 3 и 4 несовместимы

Этот код — и есть «алгоритм broadcasting» в миниатюре. Поняв его, вы сможете в уме предсказывать форму результата и причину любой ошибки.

Стоит проговорить, почему правило именно такое — выравнивание справа, а не слева. Последние оси массива обычно «самые конкретные»: для таблицы это столбцы-признаки, для изображения — каналы цвета, для временного ряда — отсчёты. Первые же оси чаще «контейнерные»: номер строки, номер картинки, номер партии. Выравнивая справа, NumPy сопоставляет однотипные «конкретные» оси и позволяет добавлять «контейнерные» оси слева бесплатно. Поэтому вектор-признаков естественно растягивается на все строки таблицы: его единственная ось совпадает с последней осью матрицы (признаки), а недостающая слева ось (строки) дополняется и растягивается. Это правило не произвольно — оно отражает типичную семантику осей в реальных данных.

Broadcasting со скаляром — это тоже broadcasting

Самый первый пример, который все видят — a + 1 — это уже broadcasting в вырожденной форме. Скаляр можно мыслить как массив формы () (ноль осей), который растягивается до любой формы. Поэтому правила, которые мы разобрали, едины: и для «массив + скаляр», и для «матрица + вектор», и для «столбец + строка» работает один и тот же механизм выравнивания осей и растяжения единиц. Это приятная цельность: не нужно держать в голове отдельные правила для скаляров — они частный случай общего. Когда вы пишете data * 255 или (data - mean) / std, вы пользуетесь broadcasting, даже если не называете это так.

Память: broadcasting не копирует, но результат — полноразмерный

Ключевое для производительности: само растяжение по broadcasting не выделяет память. Ось длины 1 «повторяется» через нулевой stride — это иллюзия повторения без реальных копий, как мы обсуждали в уроке про strides. Но результат операции — это уже настоящий полноразмерный массив. Сложив столбец (1000, 1) со строкой (1, 1000), вы не потратите память на растяжение операндов, но получите матрицу-результат на миллион элементов (8 МБ для float64). Это важно осознавать: broadcasting позволяет элегантно записать операцию, но не отменяет стоимость хранения её результата. Классическая ошибка — нечаянно породить гигантскую матрицу из двух больших векторов через внешнюю операцию, не заметив, что результат на порядки больше входов. Перед такими операциями прикиньте форму и размер результата.

Почему возникает ValueError и как чинить

Самая частая ошибка — попытка сложить, скажем, массив (4, 3) с вектором (4,). Выравнивание справа сопоставляет 3 и 4 — не равны и ни одна не 1, значит ошибка. Намерение здесь обычно «прибавить вектор к каждому столбцу», и чинится оно превращением вектора в столбец: (4,)(4, 1) через v[:, np.newaxis]. Тогда формы (4, 3) и (4, 1) совместимы.

import numpy as np
m = np.arange(12).reshape(4, 3)
v = np.array([1, 2, 3, 4])      # (4,)

# m + v  -> ValueError: (4,3) и (4,) несовместимы
print(m + v[:, np.newaxis])     # (4,3) + (4,1) -> ок, по столбцам

Вывод:

[[ 1  2  3]
 [ 5  6  7]
 [ 9 10 11]
 [13 14 15]]

Правило диагностики: если получили «could not be broadcast together», выпишите обе формы, выровняйте справа и найдите первую пару осей, которая не «равна или одна = 1». Почти всегда лекарство — добавить ось через np.newaxis в нужное место.

Подводные камни

  • Выравнивание идёт справа, а не слева. (4, 3) и (4,) НЕ совместимы, хотя «4» есть у обоих — потому что сопоставляются 3 и 4.
  • Молчаливое неожиданное растяжение. Иногда формы совместимы случайно, и операция «срабатывает», но даёт не то, что задумано. Проверяйте форму результата.
  • Память для итога. Сам broadcasting не копирует, но результат операции — полноразмерный массив. (1000, 1) + (1, 1000) даст матрицу на миллион элементов.

Практический смысл: нормировка и центрирование

Чтобы broadcasting перестал быть абстракцией, посмотрим, где он незаменим в реальной работе. Допустим, у вас матрица «образцы × признаки», и каждый признак (столбец) нужно привести к нулевому среднему и единичному разбросу — стандартная предобработка для машинного обучения. Среднее по столбцам — это вектор длины «число признаков», и его нужно вычесть из каждой строки. Благодаря broadcasting это пишется как X - X.mean(axis=0): вектор средних (форма как у строки) автоматически растягивается на все строки. Деление на стандартное отклонение / X.std(axis=0) — точно так же. Без broadcasting понадобился бы цикл по строкам с повторным вычитанием — медленно и громоздко. Если же нормировать нужно по строкам (например, каждый образец к единичной длине), среднее берут с axis=1, keepdims=True, чтобы получить столбец (n, 1), который растянется по столбцам. Эта пара приёмов — вычитание вектора-строки и вектора-столбца — покрывает огромную долю задач предобработки данных, и все они держатся на broadcasting.

Лучшие практики

  • Перед операцией мысленно выровняйте формы справа и проверьте совместимость осей.
  • Управляйте broadcasting явно через np.newaxis/reshape, делая столбцы и строки.
  • При ошибке выписывайте формы операндов — причина почти всегда в одной паре несовместимых осей.
  • Следите за размером результата: broadcasting легко породить огромную матрицу.

Итог

  • Broadcasting сопоставляет оси справа налево; недостающие оси дополняются единицами слева.
  • Оси совместимы, если равны или одна равна 1; ось-1 растягивается без копирования данных.
  • Результирующий размер по оси — максимум из двух; несовместимость даёт ValueError.
  • Чинят ошибки добавлением осей через np.newaxis, превращая векторы в строки/столбцы.
Проверьте себя
1. В каком направлении NumPy сопоставляет оси массивов при broadcasting?
AСлева направо, от первой оси
BСправа налево, от последней оси; недостающие оси дополняются единицами слева
CПо возрастанию размеров осей
DПо алфавиту имён осей
2. Совместимы ли формы (4, 3) и (4,) для broadcasting?
AДа, потому что у обоих есть размерность 4
BНет: при выравнивании справа сопоставляются 3 и 4, они не равны и ни одна не равна 1
CДа, NumPy автоматически дополнит вектор до (4, 3)
DДа, но результат будет одномерным
3. Что произойдёт при сложении массивов форм (4, 1) и (1, 3)?
AОшибка несовместимости форм
BРезультат формы (4, 3): обе единичные оси растягиваются (4 и 3 соответственно)
CРезультат формы (4,)
DРезультат формы (1, 1)
Поддержать проект