Урок курса
Матрично-векторные произведения через оператор @
NumPy и pandas: практический тренажёрВ прошлом уроке мы разбирались с broadcasting — как NumPy согласовывает формы массивов при поэлементных операциях. Оператор @, который появляется в этом уроке, работает по совсем другой логике: он не растягивает размеры и не работает поэлементно. Он вычисляет матричное произведение, и правила совместимости у него свои.
Оператор @ и его эквивалент np.matmul
Оператор @ появился в Python 3.5 специально для матричного умножения. До него приходилось писать np.matmul(A, B) или np.dot(A, B) — @ просто делает запись короче и читаемее. Под капотом A @ B и np.matmul(A, B) — одно и то же.
Важно сразу разграничить три оператора, которые легко перепутать:
*— поэлементное умножение: каждый элемент A умножается на соответствующий элемент B, форма массивов должна быть совместима по broadcasting.np.dot— для одномерных массивов ведёт себя как скалярное произведение, для двумерных — как матричное умножение, но поведение на трёхмерных и выше отличается от@.@(np.matmul) — матричное умножение в строгом смысле: строки левого операнда умножаются на столбцы правого и суммируются.
Разница между * и @ хорошо видна на простом примере:
import numpy as np
A = np.array([[1, 2],
[3, 4]])
B = np.array([[1, 0],
[0, 1]])
print(A * B) # поэлементно: [[1, 0], [0, 4]]
print(A @ B) # матричное: [[1, 2], [3, 4]]
Для единичной матрицы B разница особенно наглядна: * обнуляет недиагональные элементы, тогда как @ возвращает саму матрицу A — ровно так, как работает умножение на единичную матрицу в линейной алгебре.
Ещё одна деталь: @ не изменяет исходные массивы — результат записывается в новый объект. A и B после операции остаются прежними.
Правило согласования внутренних размеров
Прежде чем запускать @, полезно уметь предсказать результат только по форме операндов — без вычислений.
В этом уроке мы работаем с одно- и двумерными массивами, и для них правило звучит так: последняя ось левого операнда должна совпадать с предпоследней (или единственной, если правый операнд — вектор) осью правого. Именно эти два числа называют «внутренними размерами» — они как бы смотрят друг на друга в записи (m, k) @ (k, n). Если они равны, операция пройдёт. Если нет — ValueError.
Shape результата определяется «внешними» размерами — теми, что остались после сокращения внутренних:
(m, k) @ (k, n)→(m, n)— внутренний размерk«схлопывается», остаётся(m, n)(m, k) @ (k,)→(m,)— вектор shape(k,)не имеет второго измерения, поэтому результат одномерный(n,) @ (n,)→ скаляр shape()— оба измерения «схлопываются», ничего не остаётся
Чтобы проверить совместимость до запуска, достаточно выписать оба shape и мысленно поставить их рядом:
(2, 3) @ (3, 4) → внутренние: 3 == 3 ✓ → результат (2, 4) (2, 3) @ (3,) → внутренние: 3 == 3 ✓ → результат (2,) (2, 3) @ (2, 3) → внутренние: 3 ≠ 2 ✗ → ValueError
Обратите внимание: @ — не поэлементная операция и не broadcasting в том смысле, который мы разбирали в прошлом уроке. Правило внутренних размеров работает независимо от того, насколько похожи формы массивов в целом. Матрицы (2, 3) и (2, 3) имеют одинаковый shape — но перемножить их через @ нельзя, потому что внутренние размеры 3 и 2 не совпадают.
Примечание. Для трёхмерных и выше массивов
np.matmulприменяет то же правило к двум последним осям каждого операнда, а ведущие («пакетные») измерения согласуются по правилам broadcasting. Например,(5, 2, 3) @ (5, 3, 4)корректно даёт(5, 2, 4). В этом уроке мы не углубляемся в пакетное умножение — достаточно знать, что оно существует и работает согласованно с изученным правилом.
Три случая оператора @: вектор–вектор, матрица–вектор, матрица–матрица
Правило внутренних размеров абстрактно, пока не видишь, что именно происходит с числами и формой результата в каждом из трёх возможных случаев.
Случай 1: вектор @ вектор → скаляр
Когда оба операнда одномерные и имеют одинаковую длину, @ вычисляет скалярное произведение — попарно умножает элементы и суммирует. Результат — NumPy-скаляр с shape ():
import numpy as np
w = np.array([0.5, 0.3, 0.2])
x = np.array([80.0, 90.0, 70.0])
result = w @ x
print(result) # 81.0
print(result.shape) # ()
Здесь 0.5*80 + 0.3*90 + 0.2*70 = 40 + 27 + 14 = 81.0. Это и есть взвешенная сумма: w — веса, x — значения. Удобный случай на практике: итоговый балл экзамена, портфельная доходность, взвешенное среднее — всё это один вызов @. Обратите внимание, что result.shape возвращает (), а не (1,): NumPy-скаляр не имеет измерений вообще.
Случай 2: матрица @ вектор → вектор
Если левый операнд — матрица shape (m, k), а правый — вектор shape (k,), результат — вектор shape (m,). Каждая строка матрицы независимо перемножается на вектор и суммируется:
A = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
v = np.array([1.0, 0.0, -1.0]) # shape (3,)
result = A @ v
print(result) # [-2. -2.]
print(result.shape) # (2,)
Первая строка A: 1*1 + 2*0 + 3*(-1) = -2. Вторая: 4*1 + 5*0 + 6*(-1) = -2. Форма (2, 3) @ (3,) даёт (2,) — внутренний размер 3 исчезает, остаётся m=2.
Этот случай — рабочая лошадка линейных моделей: матрица признаков умножается на вектор весов и даёт вектор предсказаний.
Случай 3: матрица @ матрица → матрица
Два двумерных операнда shape (m, k) и (k, n) дают матрицу shape (m, n). Каждый элемент результата — скалярное произведение строки левой матрицы и столбца правой:
A = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
B = np.array([[1, 0],
[0, 1],
[1, 1]]) # shape (3, 2)
result = A @ B
print(result)
# [[ 4 5]
# [10 11]]
print(result.shape) # (2, 2)
Результат (2, 3) @ (3, 2) → (2, 2): внутренний размер 3 «схлопнулся», внешние 2 и 2 образовали новую матрицу.
Сводка по shape результата:
(n,) @ (n,)→()— скаляр(m, k) @ (k,)→(m,)— вектор(m, k) @ (k, n)→(m, n)— матрица
Во всех трёх случаях работает одно и то же правило: внутренний размер сокращается, внешние формируют shape результата.
Диагностика и исправление ошибки несовпадения размеров
Рано или поздно вы напишете что-то вроде A @ B и получите стену красного текста. Разберём, как её читать.
Типичное сообщение об ошибке выглядит так:
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0
with gufunc signature (n?,k),(k,m?)->(n?,m?)
Это не очень дружелюбно, но сообщение говорит ровно одно: внутренние размеры операндов не совпали. «Core dimension 0» правого операнда — это его первое измерение, которое должно равняться последнему измерению левого.
Воспроизведём ошибку:
import numpy as np
A = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
B = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
result = A @ B # ValueError!
Обе матрицы имеют одинаковый shape (2, 3) — казалось бы, должно работать. Но внутренние размеры здесь 3 (последний у A) и 2 (первый у B). Они не совпадают, отсюда и ошибка.
Систематическая диагностика занимает три шага:
- Напечатать shape обоих операндов прямо перед вызовом
@:
print(A.shape) # (2, 3)
print(B.shape) # (2, 3)
- Выписать пару «последний размер левого» и «первый размер правого» и сравнить:
(2, 3) @ (2, 3)
^ ^
3 ≠ 2 → ValueError
- Решить, что нужно исправить: транспонировать один из операндов, изменить форму через
reshape, или пересмотреть логику.
Для случая выше, если нужно перемножить A на A по правилам матричной алгебры, правильный вариант — A @ A.T или A.T @ A:
result1 = A @ A.T # (2, 3) @ (3, 2) → (2, 2)
result2 = A.T @ A # (3, 2) @ (2, 3) → (3, 3)
print(result1.shape) # (2, 2)
print(result2.shape) # (3, 3)
Оба варианта корректны с точки зрения @, но дают разные результаты — какой нужен, определяет задача.
Ещё один частый сценарий: перепутать порядок операндов. Матричное умножение не коммутативно, и A @ B в общем случае не равно B @ A. Более того, если A @ B работает, B @ A может выбросить ValueError из-за несовместимых внутренних размеров:
C = np.ones((3, 4)) # shape (3, 4)
D = np.ones((4, 2)) # shape (4, 2)
print((C @ D).shape) # (3, 2) — OK
print((D @ C).shape) # ValueError: 2 ≠ 3
Если ValueError возник неожиданно, первое действие — print(A.shape, B.shape). Почти всегда проблема сразу становится видна без дополнительной отладки.
Задача 1
Массивы A и B имеют shape (4, 3) и (3, 5) соответственно. Чему равен shape результата A @ B?
Задача 2
Матрица W имеет shape (5, 3), вектор x имеет shape (3,). Запишите выражение на Python, которое вычисляет матрично-векторное произведение через оператор @, и shape результата — через запятую. Например: W @ x, (5,)
Задача 3
Найди и исправь: три неверных выражения матричного умножения
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.
В редакторе есть сломанный код. Он пытается выполнить три операции с оператором @, но в каждой из трёх строк допущена ошибка: неверный операнд или порядок операндов приводит к тому, что shape результата не тот, что требует задача (а в некоторых случаях — к ValueError из-за несовпадения внутренних размеров).
Читаются три целых числа M, N, K (все не менее 2). Затем создаются три массива:
A— shape(M, K), заполненная единицами;b— shape(K,), заполненный единицами;B— shape(K, N), заполненная единицами.
Что нужно сделать
Найдите и исправьте ровно три ошибки в строках, помеченных # ОШИБКА, так чтобы:
r1 = A @ b— shape(M,): матрица(M, K)умножается на вектор(K,);r2 = b @ B— shape(N,): вектор(K,)умножается на матрицу(K, N);r3 = A @ B— shape(M, N): матрица(M, K)умножается на матрицу(K, N).
Программа выводит shape каждого результата в три строки в формате r1: <shape>, r2: <shape>, r3: <shape>.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
M N K
Гарантируется M >= 2, N >= 2, K >= 2, а также M, N, K могут быть различными.
Формат вывода
r1: (M,)
r2: (N,)
r3: (M, N)
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 4
Матрица весов и матрица объектов: предсказания через @
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая:
- Читает из
stdinтри целых числа в одной строке:M,K,N— гдеM— число объектов,K— число признаков,N— число выходов. - Читает следующие
Mстрок; каждая содержит ровноKвещественных чисел через пробел. Создаёт двумерный массивXчерезnp.array(..., dtype=float)формы(M, K)— матрица объектов (строка = один объект). - Читает следующие
Kстрок; каждая содержит ровноNвещественных чисел через пробел. Создаёт двумерный массивWчерезnp.array(..., dtype=float)формы(K, N)— матрица весов. - Вычисляет матрицу предсказаний
Y = X @ Wоператором@. ФормаYдолжна быть(M, N). - Выводит:
- первую строку: shape
X, shapeW, shapeYчерез пробел в виде(M, K) (K, N) (M, N); - затем
Mстрок — строки матрицыY, значения через пробел, округлённые до 2 знаков после запятой.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
M K N
X[0][0] ... X[0][K-1]
...
X[M-1][0] ... X[M-1][K-1]
W[0][0] ... W[0][N-1]
...
W[K-1][0] ... W[K-1][N-1]
Гарантируется M >= 2, K >= 2, N >= 2.
Формат вывода
(M, K) (K, N) (M, N)
Y[0][0] ... Y[0][N-1]
...
Y[M-1][0] ... Y[M-1][N-1]
Каждое значение Y выводится с ровно двумя знаками после запятой.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 5
Система мониторинга дронов: матрица линейного преобразования для облака точек
Контекст задачи
Компания «АэроЛог» разрабатывает систему мониторинга беспилотных дронов на складском комплексе. Каждый дрон оснащён лидаром, который периодически фиксирует облако опорных точек в локальной системе координат дрона. Когда дрон разворачивается или меняет ориентацию, бортовой компьютер должен пересчитать координаты всех точек в новой системе отсчёта — иначе карта склада «поедет» и дрон врежется в стеллаж.
Оператор наземной станции Виктор получает от дрона два блока данных: матрицу линейного преобразования (она кодирует поворот, отражение или масштабирование осей) и список двумерных точек в старой системе координат. Виктор хочет автоматически получать пересчитанные координаты, чтобы отображать их на интерактивной карте склада в реальном времени.
Откуда берётся матрица преобразования
Инженеры «АэроЛог» договорились о следующем соглашении: матрица T размером 2×2 хранится построчно, и каждая строка T задаёт коэффициенты для одной выходной координаты результата. То есть первая строка T[0] содержит веса (a, b), по которым вычисляется первая координата преобразованной точки: x' = a·x + b·y. Вторая строка T[1] содержит веса для второй координаты: y' = c·x + d·y.
Чтобы применить такое преобразование сразу ко всем точкам одновременно, инженеры используют матричное умножение P @ T.T, где P — матрица точек (каждая строка — одна точка (x, y)), а T.T — транспонированная матрица T. Транспонирование необходимо: оно переводит строки T (задающие компоненты результата) в столбцы, чтобы при умножении строки-точки из P на столбец T.T получалась нужная выходная координата. Без транспонирования результат будет математически неверным.
Что нужно Виктору
Виктор хочет видеть не только сами пересчитанные координаты, но и форму (shape) всех промежуточных матриц — это помогает его коллегам-аналитикам проверять корректность пайплайна обработки данных. Поэтому первая строка вывода всегда содержит четыре shape-метки через пробел: для P, для T, для T.T и для результата Q. Хотя T и T.T имеют одинаковый shape (2, 2), их метки выводятся отдельно — это требование внутреннего регламента аудита.
Далее выводятся пересчитанные координаты всех точек. Карта склада работает с точностью до сантиметра, поэтому координаты округляются до двух знаков после запятой.
Формальный контракт
Входные данные (stdin)
Программа читает данные из стандартного потока ввода в следующем порядке:
- Первая строка — четыре вещественных числа через пробел:
t00 t01 t10 t11. Это элементы матрицы преобразованияTпострочно: первая строкаTравна[t00, t01], вторая —[t10, t11]. СоздайтеT = np.array([[t00, t01], [t10, t11]], dtype=float), shape(2, 2). - Вторая строка — целое число
n(гарантируетсяn >= 2) — количество точек. - Следующие
nстрок — каждая содержит два вещественных числаx yчерез пробел. Создайте матрицу точекP = np.array(..., dtype=float)shape(n, 2), где каждая строка — одна точка.
Вычисления
Вычислите матрицу преобразованных точек: Q = P @ T.T (оператором @). Форма результата Q — (n, 2).
Выходные данные (stdout)
Выведите в стандартный поток вывода:
- Первая строка — четыре shape-метки через пробел в формате Python-кортежей:
(n, 2) (2, 2) (2, 2) (n, 2)
Порядок строго фиксирован: P.shape, затем T.shape, затем T.T.shape, затем Q.shape.
- Следующие
nстрок — строки матрицыQ. Каждая строка содержит два числа через пробел, каждое округлено до ровно двух знаков после запятой.
Шаблон формата вывода
(n, 2) (2, 2) (2, 2) (n, 2)
Q[0][0] Q[0][1]
Q[1][0] Q[1][1]
...
Q[n-1][0] Q[n-1][1]
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 6
A.T @ A: попарные скалярные произведения столбцов матрицы
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая:
- Читает из
stdinдва целых числаRиC— число строк и столбцов матрицы. - Читает следующие
Rстрок; каждая содержит ровноCвещественных чисел через пробел. Создаёт двумерный массивAчерезnp.array(..., dtype=float)формы(R, C). - Вычисляет
G = A.T @ Aоператором@. ЗдесьA.Tимеет форму(C, R), поэтомуGимеет форму(C, C): каждый элементG[i, j]— это сумма попарных произведенийi-го иj-го столбцовA, то есть их скалярное произведение. - Выводит в
stdoutровно четыре строки:
- Строка 1:
A.shapeиG.shapeчерез пробел в виде(R, C) (C, C). - Строка 2: значение элемента
G[0, 0]в формате%.2f— скалярное произведение первого столбцаAс собой. - Строка 3: значение элемента
G[-1, -1]в формате%.2f— скалярное произведение последнего столбцаAс собой. - Строка 4: значения элементов первой строки матрицы
Gчерез пробел в формате%.2f.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
R C
A[0][0] ... A[0][C-1]
...
A[R-1][0] ... A[R-1][C-1]
Гарантируется R >= 2, C >= 2.
Формат вывода
(R, C) (C, C)
G[0,0]
G[-1,-1]
G[0,0] G[0,1] ... G[0,C-1]
Каждое число выводится с ровно двумя знаками после запятой.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задачи к уроку
- Умножение матриц NumPy: оператор @, размеры и пример
Как умножить матрицы NumPy через @ и проверить совместимость shape? Отличие от умножения по элементам и задача с проверкой Python-кода.
- NumPy A.T @ A: попарные скалярные произведения столбцов
Вычислите A.T @ A и разберите значения матрицы попарных скалярных произведений столбцов. Задача с проверкой.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
