Урок курса

Матрично-векторные произведения через оператор @

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). Они не совпадают, отсюда и ошибка.

Систематическая диагностика занимает три шага:

  1. Напечатать shape обоих операндов прямо перед вызовом @:
   print(A.shape)  # (2, 3)
   print(B.shape)  # (2, 3)

  1. Выписать пару «последний размер левого» и «первый размер правого» и сравнить:
   (2, 3) @ (2, 3)
         ^   ^
         3 ≠ 2  → ValueError

  1. Решить, что нужно исправить: транспонировать один из операндов, изменить форму через 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)
РешениеPython

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

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

Задача 4

Матрица весов и матрица объектов: предсказания через @

Что уже дано

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

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

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

  1. Читает из stdin три целых числа в одной строке: M, K, N — где M — число объектов, K — число признаков, N — число выходов.
  2. Читает следующие M строк; каждая содержит ровно K вещественных чисел через пробел. Создаёт двумерный массив X через np.array(..., dtype=float) формы (M, K) — матрица объектов (строка = один объект).
  3. Читает следующие K строк; каждая содержит ровно N вещественных чисел через пробел. Создаёт двумерный массив W через np.array(..., dtype=float) формы (K, N) — матрица весов.
  4. Вычисляет матрицу предсказаний Y = X @ W оператором @. Форма Y должна быть (M, N).
  5. Выводит:
  • первую строку: shape X, shape W, shape Y через пробел в виде (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 выводится с ровно двумя знаками после запятой.

РешениеPython

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

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

Задача 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)

Программа читает данные из стандартного потока ввода в следующем порядке:

  1. Первая строка — четыре вещественных числа через пробел: t00 t01 t10 t11. Это элементы матрицы преобразования T построчно: первая строка T равна [t00, t01], вторая — [t10, t11]. Создайте T = np.array([[t00, t01], [t10, t11]], dtype=float), shape (2, 2).
  2. Вторая строка — целое число n (гарантируется n >= 2) — количество точек.
  3. Следующие n строк — каждая содержит два вещественных числа x y через пробел. Создайте матрицу точек P = np.array(..., dtype=float) shape (n, 2), где каждая строка — одна точка.

Вычисления

Вычислите матрицу преобразованных точек: Q = P @ T.T (оператором @). Форма результата Q — (n, 2).

Выходные данные (stdout)

Выведите в стандартный поток вывода:

  1. Первая строка — четыре shape-метки через пробел в формате Python-кортежей:
(n, 2) (2, 2) (2, 2) (n, 2)

Порядок строго фиксирован: P.shape, затем T.shape, затем T.T.shape, затем Q.shape.

  1. Следующие 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]
РешениеPython

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

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

Задача 6

A.T @ A: попарные скалярные произведения столбцов матрицы

Что уже дано

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

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

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

  1. Читает из stdin два целых числа R и C — число строк и столбцов матрицы.
  2. Читает следующие R строк; каждая содержит ровно C вещественных чисел через пробел. Создаёт двумерный массив A через np.array(..., dtype=float) формы (R, C).
  3. Вычисляет G = A.T @ A оператором @. Здесь A.T имеет форму (C, R), поэтому G имеет форму (C, C): каждый элемент G[i, j] — это сумма попарных произведений i-го и j-го столбцов A, то есть их скалярное произведение.
  4. Выводит в 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]

Каждое число выводится с ровно двумя знаками после запятой.

РешениеPython

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

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

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

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

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

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

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