Умножение матриц: смысл и реализация вручную

Умножение матриц — это склейка двух преобразований в одно: сначала применить B, потом A.

Произведение матриц A·B: элемент на позиции (i, j) — скалярное произведение i-й строки A на j-й столбец B. Размеры должны согласоваться: (m×n)·(n×p) = (m×p).

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

Если матрица — это преобразование, то произведение матриц — это два преобразования подряд. Применить к вектору сначала B, потом A — то же самое, что применить одну матрицу A·B. Поэтому в нейросети несколько линейных слоёв подряд — это перемножение их матриц. Порядок важен: A·B и B·A в общем случае разные (сначала повернуть, потом растянуть — не то же, что наоборот).

Правило «строка на столбец»

Чтобы получить элемент результата в строке i и столбце j, берут i-ю строку левой матрицы и j-й столбец правой, перемножают поэлементно и складывают — обычное скалярное произведение. Размеры обязаны согласоваться: число столбцов левой = числу строк правой.

def matmul(A, B):
    n = len(A)          # строк в A
    m = len(B)          # строк в B = столбцов в A
    p = len(B[0])       # столбцов в B
    result = []
    for i in range(n):
        row = []
        for j in range(p):
            s = sum(A[i][k] * B[k][j] for k in range(m))
            row.append(s)
        result.append(row)
    return result

A = [[1, 2],
     [3, 4]]
B = [[5, 6],
     [7, 8]]

print("A * B =", matmul(A, B))
print("B * A =", matmul(B, A))   # другой результат!

Вывод:

A * B = [[19, 22], [43, 50]]
B * A = [[23, 34], [31, 46]]

Проверим композицию на векторе

Убедимся, что «применить B, потом A» равно «применить A·B». Сначала растянем вектор матрицей S, потом повернём матрицей R; затем сделаем то же одной матрицей R·S.

def matmul(A, B):
    m = len(B)
    return [[sum(A[i][k] * B[k][j] for k in range(m)) for j in range(len(B[0]))]
            for i in range(len(A))]

def matvec(M, v):
    return [sum(M[i][j] * v[j] for j in range(len(v))) for i in range(len(M))]

S = [[2, 0], [0, 2]]      # растяжение в 2 раза
R = [[0, -1], [1, 0]]     # поворот на 90
v = [1, 0]

# Путь 1: применяем по очереди
step1 = matvec(S, v)
step2 = matvec(R, step1)

# Путь 2: одна склеенная матрица R*S
RS = matmul(R, S)
once = matvec(RS, v)

print("По очереди:", step2)
print("Через R*S :", once)

Вывод:

По очереди: [0, 2]
Через R*S : [0, 2]

Согласованность размеров — частая ошибка

ABA·B
2×33×42×4 (ок, 3 = 3)
2×32×4ошибка (3 ≠ 2)
m×nn×pm×p

В реальном коде на numpy это пишется одним символом @, но размеры всё равно надо держать в голове — несогласованные формы дают самую частую ошибку при работе с нейросетями.

# В numpy то же умножение — оператор @ (в браузере не запустится)
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(A @ B)   # [[19 22] [43 50]]

Итог

  • Умножение матриц = композиция преобразований (сначала B, потом A).
  • Элемент (i, j) — скалярное произведение i-й строки A на j-й столбец B.
  • Размеры: (m×n)·(n×p) = (m×p); внутренние числа обязаны совпасть.
  • Порядок важен: A·B ≠ B·A в общем случае.
Проверьте себя
1. Элемент (i, j) произведения A·B — это...
Aпроизведение A[i][j] и B[i][j]
Bскалярное произведение i-й строки A на j-й столбец B
Cсумма i-й строки A и j-го столбца B
Dмаксимум из строки и столбца
2. Можно ли перемножить матрицы размеров 2×3 и 2×4?
AДа, получится 2×4
BНет: число столбцов левой (3) не равно числу строк правой (2)
CДа, получится 3×4
DДа, но только если они квадратные
3. Что означает A·B на уровне смысла?
AСложение двух таблиц
BКомпозицию преобразований: сначала применить B, затем A
CПоэлементное умножение чисел
DТранспонирование
Поддержать проект