NumPy: вычисления и устройство массивов

Broadcasting: правила совместимости форм и исправление несовместимости

Содержание курса

Исправление несовместимости через np.newaxis и reshape

Диагностика из предыдущего раздела даёт конкретный ответ: какому операнду нужна единица и в какой позиции. Осталось эту единицу поставить — и операция сработает.

Два инструмента делают это: np.newaxis и reshape.

np.newaxis вставляет единичную ось через индексацию и возвращает view — новый объект-массив, смотрящий на те же данные в памяти.

  • arr[:, np.newaxis] — добавляет ось после первой: (n,)(n, 1)
  • arr[np.newaxis, :] — добавляет ось перед первой: (n,)(1, n)

reshape(-1, 1) и reshape(1, -1) меняют форму явным указанием целевых размеров. -1 означает «вычисли этот размер автоматически». reshape возвращает view, когда это позволяет раскладка данных в памяти, и копию — когда нет (например, для транспонированного массива). Проверить, разделяют ли два массива одну и ту же память, можно через np.shares_memory:

import numpy as np

a = np.array([1, 2, 3])       # обычный contiguous массив
a_col = a.reshape(-1, 1)
print(np.shares_memory(a, a_col))   # True — общая память, это view

a_T = np.ones((3, 4)).T            # транспонированный, не contiguous
a_T_flat = a_T.reshape(-1)
print(np.shares_memory(a_T, a_T_flat))  # False — NumPy создал копию

np.shares_memory надёжнее проверки через .base, потому что view может ссылаться на данные через промежуточный базовый объект, и arr.base is source в таком случае даст False даже при реально общей памяти.

Для broadcasting-задач вопрос view-или-копия обычно не критичен: вы вычисляете новый массив, а не изменяете исходные данные через результат. Но знать о np.shares_memory полезно при профилировании памяти.

Какой инструмент выбрать — дело вкуса: np.newaxis читается как намерение («добавить ось»), reshape — как явное указание целевой формы.

Сквозной пример: полный цикл

Возьмём случай из раздела про диагностику: a формы (3,) и b формы (3, 4) — несовместимы.

import numpy as np

a = np.array([1, 2, 3])  # shape (3,)
b = np.ones((3, 4))      # shape (3, 4)

# Диагностика (уже сделана):
# a → после выравнивания (1, 3)
# b → (3, 4)
# Ось 1: 3 ≠ 4, ни один не равен 1 → ValueError

# Что нужно: сделать a формой (3, 1), тогда:
# Ось 0: 3 и 3 → совпадают
# Ось 1: 1 и 4 → единица растянется до 4
# Результат: (3, 4)

a_col = a[:, np.newaxis]   # shape (3, 1)
print(a_col.shape)          # (3, 1)

result = a_col + b
print(result.shape)         # (3, 4)
print(result)
# [[2. 2. 2. 2.]
#  [3. 3. 3. 3.]
#  [4. 4. 4. 4.]]

Каждый элемент a — 1, 2, 3 — прибавляется ко всей соответствующей строке b. Строка 0 получила +1, строка 1 — +2, строка 2 — +3.

Тот же результат через reshape:

a_col_v2 = a.reshape(-1, 1)  # shape (3, 1)
print((a_col_v2 + b).shape)   # (3, 4)

Второй пример: операция по столбцам

Теперь обратная задача — прибавить к каждому столбцу матрицы (3, 4) отдельный вектор из 4 элементов. Это тривиальный broadcasting: (4,) выравнивается до (1, 4) и растягивается по строкам. Никакого np.newaxis не нужно:

v = np.array([10, 20, 30, 40])  # shape (4,)
result2 = b + v
print(result2.shape)  # (3, 4)

Здесь исправлять нечего — формы уже совместимы. np.newaxis нужен только тогда, когда диагностика показала реальную проблему.

Третий пример: (3, 1) и (1, 4) → полная комбинаторная таблица

col = np.array([10, 20, 30]).reshape(-1, 1)  # shape (3, 1)
row = np.array([1, 2, 3, 4]).reshape(1, -1)  # shape (1, 4)

table = col + row
print(table.shape)  # (3, 4)
print(table)
# [[11 12 13 14]
#  [21 22 23 24]
#  [31 32 33 34]]

Оба операнда специально сформированы в разных осях: один — столбец, другой — строка. В результате каждый элемент первого складывается с каждым элементом второго. Это стандартный паттерн для построения таблиц попарных операций без вложенных циклов.

Алгоритм исправления в одном месте

  1. Выписать оба shape.
  2. Выровнять справа, добавив единицы слева к более короткому.
  3. Найти ось, где размеры не равны и ни один не равен 1.
  4. Решить, какому операнду нужна единица в этой позиции.
  5. Добавить ось через arr[:, np.newaxis] / arr[np.newaxis, :] или arr.reshape(-1, 1) / arr.reshape(1, -1).
  6. Проверить .shape результата — он должен совпасть с ожидаемым.

Шаг 6 стоит делать явно, особенно пока интуиция ещё не отработана: print((a_fixed + b).shape) дешевле, чем разбирать неправильный результат позже.

Broadcasting с оператором @ (матричное умножение) работает по другим правилам — там форма согласуется не поэлементно, а через внутренние размеры матриц. Это отдельная тема следующего урока.