Урок курса

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

NumPy и pandas: практический тренажёр

В предыдущем уроке мы складывали числа по осям и схлопывали массивы до одного значения. Теперь задача противоположная: как выполнять поэлементные операции над массивами, у которых формы не совпадают — и при этом не копировать данные вручную. Именно это и делает broadcasting.

Правила broadcasting: выравнивание форм и растяжение размера 1

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

Всё поведение описывается двумя правилами, которые применяются последовательно.

Правило 1: выравнивание форм справа. Если у массивов разное число осей, к форме с меньшим числом осей добавляются единицы слева — до тех пор, пока число осей не совпадёт.

Например, массив с формой (4,) и массив с формой (3, 4): первый становится (1, 4). Выравнивание всегда идёт справа — именно поэтому вектор-строка из 4 элементов совместим с матрицей из 3 строк по 4 столбца, а не наоборот.

Правило 2: размер 1 растягивается. После выравнивания NumPy смотрит на каждую ось по очереди. Если размеры совпадают — всё хорошо. Если один из них равен 1 — он «растягивается» до размера другого. Результирующий размер по этой оси равен большему из двух.

Операция возможна, если после выравнивания для каждой пары размеров выполняется хотя бы одно условие:

  • размеры равны,
  • или один из них равен 1.

Если ни то ни другое — NumPy выбросит ошибку. Но об этом в следующем разделе.

Покажем, как работает логика на практике:

import numpy as np

# Случай 1: (3, 4) и (4,)
# Шаг 1 — выравнивание: (3, 4) и (1, 4)
# Шаг 2 — растяжение: ось 0 у второго операнда — размер 1, растягиваем до 3
# Результат: (3, 4)
A = np.ones((3, 4))
v = np.array([1, 2, 3, 4])  # shape (4,)
result = A + v
print(result.shape)  # (3, 4)

Вектор v прибавляется к каждой строке матрицы A — без явного копирования, просто по правилу растяжения.

# Случай 2: (3, 1) и (1, 4)
# Оба операнда уже двумерны, выравнивание не нужно
# Ось 0: 3 и 1 → растягиваем 1 до 3
# Ось 1: 1 и 4 → растягиваем 1 до 4
# Результат: (3, 4)
col = np.array([[10], [20], [30]])  # shape (3, 1)
row = np.array([[1, 2, 3, 4]])      # shape (1, 4)
result2 = col + row
print(result2.shape)  # (3, 4)
print(result2)
# [[11 12 13 14]
#  [21 22 23 24]
#  [31 32 33 34]]

Здесь каждый элемент col складывается с каждым элементом row — получается полная комбинаторная таблица из 12 значений. Это классический паттерн broadcasting: два одномерных набора, каждый явно сделан двумерным в разных осях, дают двумерный результат.

Чтобы быстро проверить совместимость любых двух форм в голове, достаточно трёх шагов:

  1. Выписать оба shape.
  2. Дополнить более короткий единицами слева.
  3. Пройтись по осям справа налево: каждая пара должна быть равна или содержать 1.

Если все пары прошли проверку — операция сработает, а результирующая форма — это максимум по каждой оси.

Вот несколько пар для быстрой проверки интуиции:

Форма AФорма BПосле выравниванияСовместимы?Результат
(5,)(3, 5)(1, 5) и (3, 5)Да(3, 5)
(3, 1)(3, 4)Без измененийДа(3, 4)
(3, 4)(3, 4)Без измененийДа(3, 4)
(3, 4)(3, 5)Без измененийНет—
(2, 3)(4,)(2, 3) и (1, 4)Нет—

Последние два случая — несовместимые формы. Почему именно там возникает сбой и как его прочитать в тексте ошибки — разберём дальше.

Диагностика ошибки несовместимости форм

Когда два массива не проходят проверку совместимости, NumPy бросает ValueError с сообщением вида:

ValueError: operands could not be broadcast together with shapes (3,) (3, 4)

В тексте ошибки всегда написаны обе формы — именно с ними и нужно работать.

Диагностика делается в три шага.

Шаг 1. Выписать оба shape из сообщения об ошибке (или проверить через .shape в коде).

Шаг 2. Выровнять формы справа: к более короткой добавить единицы слева до совпадения числа осей.

Шаг 3. Пройтись по осям слева направо и найти ту, где размеры не равны и ни один из них не равен 1. Это и есть причина ошибки.

Разберём конкретный случай из сообщения выше:

import numpy as np

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

# a + b  →  ValueError

Применяем алгоритм:

  • Shape a: (3,) → после выравнивания: (1, 3)
  • Shape b: (3, 4) → без изменений
  • Ось 0: 1 и 3 → ок, единица растянется
  • Ось 1: 3 и 4 → 3 ≠ 4, ни один не равен 1 → несовместимы

Именно ось 1 ломает операцию. Вектор a из трёх элементов выровнялся как строка (1, 3), а у b по оси 1 стоит 4 — растянуть невозможно.

Ещё один пример — чуть менее очевидный:

x = np.ones((2, 3))  # shape (2, 3)
y = np.ones((4,))    # shape (4,)

# x + y  →  ValueError

Диагностика:

  • y после выравнивания: (1, 4)
  • Ось 0: 2 и 1 → ок
  • Ось 1: 3 и 4 → 3 ≠ 4, ни один не равен 1 → ошибка

Поверхностно кажется, что 2D-массив и вектор из 4 элементов должны как-то сочетаться. Но после выравнивания вектор встаёт во вторую ось как (1, 4), а там уже стоит 3 — несовместимо.

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

Исправление несовместимости через 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 с оператором @ (матричное умножение) работает по другим правилам — там форма согласуется не поэлементно, а через внутренние размеры матриц. Это отдельная тема следующего урока.

Закрепите тему на задачах

Сравнивайте размерности справа налево: каждая пара должна совпадать либо содержать 1. До вычисления запишите ожидаемую форму результата, особенно для сочетания (n,) и (n, 1).

NumPy broadcasting: какая форма будет у суммы массивов

Broadcasting NumPy: нормализация строк и столбцов

NumPy broadcasting: outer-таблица и центрирование строк

Откройте условие и решите задачу перед чтением следующей темы. В задачах на код проверяется программа, в тестовых заданиях — выбранный ответ. Для последовательного обучения вернитесь в программу бесплатного курса.

Задача 1

Массивы a и b имеют shape (5, 1) и (1, 4) соответственно. Чему равен shape результата a + b?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Массив a имеет shape (5,), массив b имеет shape (3, 5). Запишите одно выражение, которое вычитает a из b через broadcasting без ручного изменения shape. Ответ должен начинаться с b -.

Без регистрации · результат не сохраняется

Задача 3

Найди и исправь: несовместимые формы при broadcasting

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.

В редакторе уже есть сломанный код. Он пытается выполнить три операции с broadcasting, но в каждой из трёх допущена ошибка: либо формы несовместимы и нужно добавить ось, либо ось добавлена не туда, либо вместо np.newaxis использован reshape с неверными аргументами.

Что нужно сделать

Найдите и исправьте все три ошибки так, чтобы программа:

  1. Читала из stdin одну строку с четырьмя целыми числами: R, C, a_val, b_val.
  2. Создавала матрицу M = np.ones((R, C)) * a_val — shape (R, C).
  3. Создавала вектор row = np.arange(C) + b_val — shape (C,).
  4. Создавала вектор col = np.arange(R) + b_val — shape (R,).
  5. Вычисляла три результата и выводила их shape в три строки:
  • s1: M + row — прибавить row к каждой строке M; ожидаемый shape (R, C). Ошибка: row ошибочно преобразован в row.reshape(R, 1) вместо row.reshape(1, C) или оставлен как (C,).
  • s2: M + col[:, np.newaxis] — прибавить col к каждому столбцу M; ожидаемый shape (R, C). Ошибка: написано col[np.newaxis, :] вместо col[:, np.newaxis].
  • s3: outer-произведение col и row без матрицы M; ожидаемый shape (R, C). Ошибка: написано col * row вместо col[:, np.newaxis] * row.

Исправьте ровно эти три места. Не меняйте ничего другого.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Одна строка с четырьмя целыми числами через пробел: R C a_val b_val.

Гарантируется: R >= 2, C >= 2, R ≠ C.

Формат вывода

Три строки — shape каждого результата в виде кортежа Python, например (3, 4).

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 4

Broadcasting: нормализация строк и столбцов матрицы

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin два целых числа R и C — число строк и столбцов матрицы.
  2. Читает следующие R строк; каждая содержит ровно C целых чисел через пробел. Создаёт двумерный массив M через np.array(..., dtype=float) формы (R, C).
  3. Нормализация по строкам. Вычисляет сумму каждой строки через np.sum(M, axis=1) — результат shape (R,). Делит M на эти суммы через broadcasting, добавив ось через [:, np.newaxis] или reshape(-1, 1), чтобы делитель стал формы (R, 1). Сохраняет результат в norm_rows — shape (R, C). Выводит shape norm_rows в одну строку.
  4. Нормализация по столбцам. Вычисляет максимум каждого столбца через np.max(M, axis=0) — результат shape (C,). Делит M на эти максимумы через broadcasting (форма (C,) совместима с (R, C) напрямую). Сохраняет результат в norm_cols — shape (R, C). Выводит shape norm_cols в одну строку.
  5. Выводит через пробел два значения: norm_rows[0, 0] и norm_cols[0, 0], округлённых до одного знака после запятой.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Первая строка: два целых числа R и C через пробел.

Следующие R строк: по C целых чисел через пробел.

Гарантируется: R >= 2, C >= 2, все элементы матрицы положительны.

Формат вывода

Три строки:

  • shape norm_rows в виде кортежа Python, например (2, 4);
  • shape norm_cols в виде кортежа Python, например (2, 4);
  • два значения через пробел: norm_rows[0, 0] и norm_cols[0, 0], каждое с одним знаком после запятой.
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Broadcasting: outer-таблица и центрирование матрицы по строкам

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая выполняет два broadcasting-вычисления над одними и теми же данными.

  1. Читает из stdin два целых числа R и C — число строк и столбцов.
  2. Читает следующие R строк; каждая содержит ровно C целых чисел через пробел. Создаёт двумерный массив M через np.array(..., dtype=float) формы (R, C).
  3. Outer-таблица разностей. Создаёт вектор row_means — среднее каждой строки через np.mean(M, axis=1), shape (R,). Создаёт вектор col_means — среднее каждого столбца через np.mean(M, axis=0), shape (C,). Вычисляет outer-таблицу diff = row_means[:, np.newaxis] - col_means через broadcasting — shape должен быть (R, C). Выводит shape diff в одну строку, затем значение элемента diff[0, 0] в следующую строку (целое число, если дробная часть нулевая, иначе float с одним знаком после запятой — используйте g-формат: f"{val:.1g}" не нужен, просто int(val) если val == int(val) иначе round(val, 1)).
  4. Центрирование строк. Вычитает из M среднее каждой строки через broadcasting: centered = M - row_means[:, np.newaxis] — shape (R, C). Выводит shape centered в одну строку, затем значение элемента centered[0, 0] в следующую строку (то же правило вывода числа).

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Первая строка: два целых числа R и C через пробел.

Следующие R строк: по C целых чисел через пробел.

Гарантируется: R >= 2, C >= 2.

Формат вывода

Четыре строки:

  • shape diff в виде кортежа Python, например (3, 4);
  • значение diff[0, 0];
  • shape centered в виде кортежа Python, например (3, 4);
  • значение centered[0, 0].
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 6

Broadcasting с тензором (B, R, C): поправки по последней оси и коэффициенты по первой

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin три целых числа B, R, C — размеры тензора.
  2. Читает следующие B * R строк; каждая содержит C целых чисел через пробел. Создаёт трёхмерный массив T через np.array(..., dtype=float).reshape(B, R, C) — shape (B, R, C).
  3. Читает одну строку с C числами через пробел — создаёт вектор поправок bias через np.array(..., dtype=float), shape (C,).
  4. Читает одну строку с B числами через пробел — создаёт вектор коэффициентов coeff через np.array(..., dtype=float), shape (B,).
  5. Шаг A. Прибавляет bias к T через broadcasting. Вектор bias shape (C,) совместим с T shape (B, R, C) напрямую — выравнивание справа даёт (1, 1, C). Сохраняет результат в T_biased — shape (B, R, C). Выводит T_biased.shape.
  6. Шаг B. Умножает T_biased на coeff, добавив к coeff две оси через coeff[:, np.newaxis, np.newaxis] — shape становится (B, 1, 1), что совместимо с (B, R, C). Сохраняет результат в T_scaled — shape (B, R, C). Выводит T_scaled.shape.
  7. Вычисляет total = np.sum(T_scaled) и выводит глобальную сумму элементов T_scaled с ровно двумя знаками после запятой.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Строка 1: три целых числа B R C через пробел.

Следующие B * R строк: по C целых чисел через пробел (данные тензора, строка за строкой по всем батчам).

Следующая строка: C чисел через пробел — вектор bias.

Последняя строка: B чисел через пробел — вектор coeff.

Гарантируется: B >= 2, R >= 2, C >= 2.

Формат вывода

Три строки:

  • shape T_biased в виде кортежа Python, например (2, 3, 4);
  • shape T_scaled в виде кортежа Python, например (2, 3, 4);
  • глобальная сумма T_scaled с ровно двумя знаками после запятой, например 16.00.
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку