Skip to content

14. Матричное умножение (2D) и симуляция полносвязных слоев ​

Линейная алгебра vs Поэлементная математика ​

Разница между операторами * и @ (numpy.dot) ​

В NumPy существует принципиальное различие между двумя видами умножения двумерных матриц:

  1. Поэлементное умножение (оператор *): Перемножает элементы матриц, находящиеся на одинаковых позициях. Требует строгого совпадения геометрии (shape) обеих матриц.
  2. Настоящее матричное умножение (оператор @ или функция numpy.dot()): Выполняет классическую операцию из линейной алгебры — умножение «строка на столбец» (скалярное произведение строк первой матрицы на столбцы второй).
python
import numpy

matrix_A = numpy.array([[1, 2], [3, 4]])
matrix_B = numpy.array([[2, 0], [1, 2]])

# Element-wise multiplication
element_wise = matrix_A * matrix_B
print("Element-wise:\n", element_wise)
# [[2 0]
#  [3 8]]

# True matrix multiplication (dot product)
dot_product = matrix_A @ matrix_B
print("Matrix product:\n", dot_product)
# [[ 4  4]
#  [10  8]]

dot_product_method = numpy.dot(matrix_A, matrix_B)
print("Dot product method:\n", dot_product_method)

Главное правило совместимости матриц

Для выполнения матричного умножения A @ B должно соблюдаться жесткое математическое правило: количество столбцов первой матрицы должно быть строго равно количеству строк второй матрицы. Если матрица A имеет форму (M, N), а матрица B — форму (N, K), то результирующая матрица всегда будет иметь геометрию (M, K). Если условия не соблюдены, NumPy выбросит ошибку ValueError.

python
import numpy

matrix_mn = numpy.array([[1, 2, 3], [3, 4, 5]])
matrix_nk = numpy.array([[2, 0], [1, 2], [3, 4]])

# True matrix multiplication (dot product)
dot_product = matrix_mn @ matrix_nk
print("Matrix product:\n", dot_product)
# [[ 4  4]
#  [10  8]]

dot_product_method = numpy.dot(matrix_mn, matrix_nk)    # [[13 16] [25 28]]
print("Dot product method:\n", dot_product_method)      # [[13 16] [25 28]]

Операция транспонирования матриц (.T) ​

Транспонирование — это операция, которая «зеркально» поворачивает матрицу относительно её главной диагонали. Строки становятся столбцами, а столбцы — строками. В NumPy для этого используется быстрый атрибут .T, который не копирует данные физически, а просто меняет метаданные шагов (strides) в памяти. Геометрия (M, N) после транспонирования меняется на (N, M). Это незаменимо для согласования размерностей матриц признаков и весов.

python
import numpy

matrix_rect = numpy.array([[1, 2, 3], [4, 5, 6]])
print("Shape before:\n", matrix_rect)           # Shape before: [[1 2 3] [4 5 6]]
print("Shape before:\n", matrix_rect.shape)     # (2, 3)

# Transposing
transposed_matrix = matrix_rect.T
print("Shape after:\n", transposed_matrix)      # Shape after: [[1 4] [2 5] [3 6]]
print("Shape after:\n", transposed_matrix.shape)  # (3, 2)

Симуляция полносвязного слоя (Linear/Dense) ​

Реализация базового прямого прохода нейросети (Forward Pass) ​

Полносвязный (Dense) слой является базовым кирпичиком большинства нейросетевых архитектур. Математически прямой проход через такой слой описывается классической формулой линейной комбинации:

Y=XW+b
  • X — входная матрица признаков (батч данных) размерности (batch_size, input_features).
  • W — матрица обучаемых весов слоя размерности (input_features, output_neurons).
  • b — вектор смещения (bias) размерности (output_neurons,), который автоматически добавляется к каждой строке матрицы результата благодаря механизму трансляции NumPy.
  • Y — выходная матрица активаций размерности (batch_size, output_neurons).
python
import numpy

# Simulated batch of 2 samples, each having 3 features (shape: 2x3)
X_input = numpy.array([[1.0, 2.0, 3.0], 
                       [0.5, 1.5, 2.5]])

# Weights for a layer with 2 hidden neurons (shape: 3x2)
W_weights = numpy.array([[0.1, 0.2], 
                         [0.3, 0.4], 
                         [0.5, 0.6]])

# Biases for the 2 neurons (shape: 2,)
b_bias = numpy.array([0.1, 0.2])

# Forward pass simulation: X @ W + b
layer_output = (X_input @ W_weights) + b_bias
print("Layer Output:\n", layer_output)
# Outputs shape (2, 2):
# [[2.3 3.2]
#  [1.8 2.5]]

Линейная алгебра: пакет numpy.linalg ​

Помимо матричного умножения, NumPy предоставляет готовые C-реализации классических операций линейной алгебры. Все они собраны в подпакет numpy.linalg и работают поверх высокоскоростных BLAS/LAPACK-библиотек.

Определитель матрицы (numpy.linalg.det) ​

Определитель — скалярная характеристика квадратной матрицы. В ML его используют для проверки вырожденности пространства признаков: если определитель равен нулю, строки (столбцы) матрицы линейно зависимы, и обратная матрица не существует.

python
import numpy

A = numpy.array([[2.0, 1.0],
                 [2.0, 3.0]])

print("Determinant of A:", numpy.linalg.det(A))  # Outputs: 4.0

Обратная матрица (numpy.linalg.inv) и ловушка вырожденных матриц ​

Обратная матрица A−1 удовлетворяет тождеству A⋅A−1=I.

Вырожденная (сингулярная) матрица

Если det(A) = 0, обратная матрица не существует. Современный NumPy корректно прерывает вычисление и выбрасывает исключение numpy.linalg.LinAlgError: Singular matrix. Всегда проверяйте определитель (или ранг) матрицы перед вызовом inv.

python
import numpy

A = numpy.array([[2.0, 1.0],
                 [2.0, 3.0]])

inverse_A = numpy.linalg.inv(A)
print("Inverse matrix:\n", inverse_A)
# Outputs:
# [[ 0.75 -0.25]
#  [-0.5   0.5 ]]

# Verification identity: A * A^-1 = I
print("A @ A^-1:\n", A @ inverse_A)
# Outputs:
# [[1. 0.]
#  [0. 1.]]

# Degenerate matrix: the second row equals 3 * the first row
degenerate_matrix = numpy.array([[2.0, 1.0],
                                 [6.0, 3.0]])
print("Determinant of degenerate matrix:", numpy.linalg.det(degenerate_matrix))  # Outputs: 0.0

# numpy.linalg.inv(degenerate_matrix) raises:
# numpy.linalg.LinAlgError: Singular matrix

Решение систем линейных алгебраических уравнений (СЛАУ) ​

На практике прямое вычисление обратной матрицы — неэффективный и менее точный способ решить систему A⋅x=v. Для этого существует специализированная функция numpy.linalg.solve(A, v), которая находит вектор x напрямую, методами матричного разложения, без вычисления A−1:

python
import numpy

A = numpy.array([[2.0, 1.0],
                 [2.0, 3.0]])
v = numpy.array([5.0, -10.0])

x_solution = numpy.linalg.solve(A, v)
print("Solution x:", x_solution)  # Outputs: [ 6.25 -7.5 ]

# Cross-check via the inverse matrix
inverse_A = numpy.linalg.inv(A)
print("Cross-check A^-1 @ v:", inverse_A @ v)  # Outputs: [ 6.25 -7.5 ]

Собственные значения и собственные векторы (numpy.linalg.eig) ​

Собственный вектор — это ненулевой вектор x, направление которого не изменяется при умножении на матрицу, а лишь масштабируется собственным значением λ: A⋅x=λ⋅x.

python
import numpy

A = numpy.array([[2.0, 1.0],
                 [2.0, 3.0]])

eigenvalues, eigenvectors = numpy.linalg.eig(A)
print("Eigenvalues:", eigenvalues)  # Outputs: [1.+0.j 4.+0.j]
print("Eigenvectors:\n", eigenvectors)
  • Функция возвращает кортеж из двух объектов: массив скалярных значений eigenvalues и массив столбцов-векторов eigenvectors (каждый столбец — собственный вектор, соответствующий значению на той же позиции).
  • Результат имеет комплексный тип даже для вещественной матрицы: в общем случае собственные значения вещественной матрицы могут быть комплексно-сопряженной парой. Если все значения вещественные, мнимая часть равна нулю (+0.j).
  • Собственные значения матриц A и AT всегда совпадают (при транспонировании меняться могут только собственные векторы):
python
import numpy

A = numpy.array([[2.0, 1.0],
                 [2.0, 3.0]])

eigenvalues_A, _ = numpy.linalg.eig(A)
eigenvalues_T, _ = numpy.linalg.eig(A.T)

print("eig(A):", eigenvalues_A)    # Outputs: [1.+0.j 4.+0.j]
print("eig(A.T):", eigenvalues_T)  # Outputs: [1.+0.j 4.+0.j]

# Identity matrix: every vector is an eigenvector with lambda = 1
identity_eigenvalues, _ = numpy.linalg.eig(numpy.eye(3))
print("eig(I):", identity_eigenvalues)  # Outputs: [1.+0.j 1.+0.j 1.+0.j]

Упражнения базовые (5 шт) (только с опорой на объясненную в данном уроке теорию) ​

1. Нахождение матричного произведения (Dot Product) ​

Задача: Даны две матрицы: matrix_1 = numpy.array([[2, 1], [0, 3]]) и matrix_2 = numpy.array([[1, 4], [2, 0]]). Вычислите их истинное матричное произведение с помощью оператора @ и выведите результат.

Посмотреть решение
python
import numpy

matrix_1 = numpy.array([[2, 1], [0, 3]])
matrix_2 = numpy.array([[1, 4], [2, 0]])

# Computing linear algebra dot product
product_result = matrix_1 @ matrix_2
print(product_result)
# Outputs:
# [[2 8]
#  [6 0]]

2. Согласование размерностей через транспонирование ​

Задача: Дана матрица признаков X_batch = numpy.array([[1, 2, 3]]) (форма 1x3) и матрица весов W_raw = numpy.array([[0.1, 0.2, 0.3]]) (форма 1x3). Напрямую умножить X_batch @ W_raw нельзя. Транспонируйте матрицу весов с помощью атрибута .T, выполните матричное умножение и выведите результат (скалярное произведение векторов).

Посмотреть решение
python
import numpy

X_batch = numpy.array([[1, 2, 3]])
W_raw = numpy.array([[0.1, 0.2, 0.3]])

# Transposing weights to align shapes: (1, 3) @ (3, 1) -> (1, 1)
output_scalar = X_batch @ W_raw.T
print(output_scalar)  # Outputs: [[1.4]]

3. Симуляция работы одного нейрона ​

Задача: Входной батч содержит два объекта: inputs = numpy.array([[2.0, 1.0], [0.0, 3.0]]) (форма 2x2). Веса одного нейрона заданы как вектор-столбец weights = numpy.array([[0.5], [1.0]]) (форма 2x1). Вычислите выход нейрона без учета смещения с помощью матричного оператора @. Выведите результат.

Посмотреть решение
python
import numpy

inputs = numpy.array([[2.0, 1.0], [0.0, 3.0]])
weights = numpy.array([[0.5], [1.0]])

# Multiplication yields column activation vector (2, 2) @ (2, 1) -> (2, 1)
neuron_activation = inputs @ weights
print(neuron_activation)
# Outputs:
# [[2.]
#  [3.]]

4. Поэлементное умножение маски на веса ​

Задача: Дана матрица весов W = numpy.array([[0.5, -0.2], [0.1, 0.8]]). Дана булева или целочисленная маска дропаута mask = numpy.array([[1, 0], [1, 1]]). Выполните поэлементное умножение весов на маску, чтобы занулить отключенный нейрон. Выведите результат.

Посмотреть решение
python
import numpy

W = numpy.array([[0.5, -0.2], [0.1, 0.8]])
mask = numpy.array([[1, 0], [1, 1]])

# Element-wise multiplication using '*' operator
masked_weights = W * mask
print(masked_weights)
# Outputs:
# [[0.5 -0. ]
#  [0.1  0.8]]

5. Прямой проход слоя (XW + b) для одного объекта ​

Задача: Напишите код для расчета активации слоя для одного объекта, где X = numpy.array([[1.0, 2.0]]) (форма 1x2), матрица весов W = numpy.array([[0.2, 0.4], [0.1, 0.3]]) (форма 2x2) и вектор смещения b = numpy.array([0.5, 0.5]). Выведите результат операции X @ W + b.

Посмотреть решение
python
import numpy

X = numpy.array([[1.0, 2.0]])
W = numpy.array([[0.2, 0.4], [0.1, 0.3]])
b = numpy.array([0.5, 0.5])

# Forward pass calculation
layer_output = (X @ W) + b
print(layer_output)  # Outputs: [[0.9 1.5]]

Упражнения смешанные (6 шт) (включают материал прошлых уроков) ​

6. Матричное умножение отслайсенных подматриц датасета ​

Задача: Создайте две матрицы: matrix_A (числа от 0 до 11, форма 4x3) и matrix_B (числа от 0 до 11, форма 3x4) с помощью arange и reshape (Урок 3 / Урок 11). Выделите из matrix_A подматрицу из первых двух строк (форма 2x3), а из matrix_B — подматрицу из первых двух столбцов (форма 3x2), используя двумерные срезы (Урок 13). Перемножьте полученные срезы матрично (Урок 14) и выведите результат.

Посмотреть решение
python
import numpy

# Generating base 2D workspaces (Lesson 3 & Lesson 11)
matrix_A = numpy.arange(12).reshape(4, 3)
matrix_B = numpy.arange(12).reshape(3, 4)

# Slicing matching dimensions subsets (Lesson 13)
sub_A = matrix_A[0:2, :]  # Shape: (2, 3)
sub_B = matrix_B[:, 0:2]  # Shape: (3, 2)

# Matrix multiplication (Lesson 14)
result_matrix = sub_A @ sub_B  # Shape: (2, 2)
print(result_matrix)
# Outputs:
# [[ 20  23]
#  [ 56  68]]

7. Прямой проход полносвязного слоя с квантованием весов в FP16 ​

Задача: Сгенерируйте входную матрицу признаков батча X = numpy.ones((5, 4)) (Урок 11). Создайте случайную (или заполненную единицами) матрицу весов W = numpy.ones((4, 3)) и вектор b = numpy.ones(3), принудительно задав им низкобитный тип данных numpy.float16 для экономии памяти GPU (Урок 4 / Урок 11). Выполните операцию слоя X @ W + b. Выведите полученную матрицу активаций и проверьте её итоговый объем в байтах через .nbytes (Урок 2).

Посмотреть решение
python
import numpy

# Initializing dimensions with standard and half-precision types (Lesson 4 & Lesson 11)
X = numpy.ones((5, 4)) # Default float64
W = numpy.ones((4, 3), dtype=numpy.float16)
b = numpy.ones(3, dtype=numpy.float16)

# Forward pass. X (float64) @ W (float16) causes automatic upcasting of results to float64 (Lesson 7 & Lesson 14)
output_activations = (X @ W) + b

print("Output grid:\n", output_activations)
print("Resulting matrix size in RAM:", output_activations.nbytes, "bytes") # 15 elements * 8 bytes = 120 bytes

8. Модификация весов на месте (In-place) перед умножением ​

Задача: Дана матрица входных сигналов X = numpy.array([[1.0, 2.0], [3.0, 4.0]]) (Урок 11). Дана базовая матрица весов слоя W = numpy.array([[0.5, 0.5], [1.0, 1.0]]). Чтобы применить регуляризацию, уменьшите матрицу весов на месте (in-place) на скаляр 0.1 с помощью оператора -= для оптимизации RAM (Урок 8). После этого выполните матричное умножение X @ W (Урок 14). Выведите итоговый результат.

Посмотреть решение
python
import numpy

X = numpy.array([[1.0, 2.0], [3.0, 4.0]])
W = numpy.array([[0.5, 0.5], [1.0, 1.0]])

# In-place regularization weight decay step (Lesson 8)
W -= 0.1  # W becomes [[0.4, 0.4], [0.9, 0.9]]

# Performing true matrix multiplication (Lesson 14)
layer_signals = X @ W
print(layer_signals)
# Outputs:
# [[2.2 2.2]
#  [4.8 4.8]]

9. Симуляция полносвязного слоя с занулением отрицательных выходов (ReLU) ​

Задача: Дан батч признаков X = numpy.array([[1.0, -1.0], [2.0, 0.0]]) и веса W = numpy.array([[0.5, -0.5], [1.5, 1.0]]) (Урок 11). Вычислите линейную комбинацию Y = X @ W (Урок 14). Примените к полученной матрице Y функцию активации ReLU: с помощью булева маскирования найдите все элементы матрицы, которые строго меньше нуля, и замените их на месте на 0.0 (Урок 10). Выведите итоговую матрицу активированных сигналов.

Посмотреть решение
python
import numpy

X = numpy.array([[1.0, -1.0], [2.0, 0.0]])
W = numpy.array([[0.5, -0.5], [1.5, 1.0]])

# Step 1: Linear algebra combination (Lesson 14)
Y_logits = X @ W  # Yields: [[-1.0, -1.5], [ 1.0, -1.0]]

# Step 2: Vectorized ReLU activation mapping (Lesson 10)
Y_logits[Y_logits < 0] = 0.0
print("Activated layer output:\n", Y_logits)
# Outputs:
# [[0. 0.]
#  [1. 0.]]

10. Прямой проход слоя отслайсенного батча с обработкой аномалий (NaN) ​

Задача: Дан сырой табличный датасет dataset = numpy.array([[1.0, 2.0, numpy.nan], [3.0, 4.0, 0.5]]) типа numpy.float32 (Урок 4). Выделите из него матрицу признаков X, отбросив последний столбец через срезы (Урок 13). Сделайте её изолированную копию через .copy() (Урок 6). Перемножьте полученную чистую матрицу X на матрицу весов W = numpy.array([[0.1], [0.2]], dtype=numpy.float32) (Урок 14). Выведите результат и с помощью numpy.isnan() докажите, что на выходе нет никаких скрытых вычислительных ошибок (Урок 9).

Посмотреть решение
python
import numpy

# Setting up float32 data array containing an isolated NaN outlier (Lesson 4 & Lesson 11)
dataset = numpy.array([[1.0, 2.0, numpy.nan], [3.0, 4.0, 0.5]], dtype=numpy.float32)

# Slicing feature columns and securing memory allocation (Lesson 6, 13)
X_features = dataset[:, :-1].copy()  # Extracts [[1.0, 2.0], [3.0, 4.0]]

W_weights = numpy.array([[0.1], [0.2]], dtype=numpy.float32)

# Matrix dot product execution (Lesson 14)
predictions = X_features @ W_weights  # Yields shapes (2, 1)

print("Predictions column vector:\n", predictions)
print("Are there any invalid math_oge tokens?", numpy.any(numpy.isnan(predictions)))  # Outputs: False (Lesson 9)

11. Решение СЛАУ и кросс-проверка через обратную матрицу ​

Задача: Дана система A⋅x=v, где A = numpy.array([[3.0, 1.0], [1.0, 2.0]]) (Урок 12) и v = numpy.array([9.0, 8.0]) (Урок 3). Решите систему с помощью функции numpy.linalg.solve (Урок 14). Затем вычислите обратную матрицу numpy.linalg.inv(A) и убедитесь, что произведение A_inverse @ v дает тот же вектор-решение (Урок 14). Выведите обе копии решения.

Посмотреть решение
python
import numpy

A = numpy.array([[3.0, 1.0],
                 [1.0, 2.0]])
v = numpy.array([9.0, 8.0])

# Direct linear solver (the preferred method)
x_direct = numpy.linalg.solve(A, v)

# Cross-verification through the inverse matrix
A_inverse = numpy.linalg.inv(A)
x_via_inverse = A_inverse @ v

print("Direct solution:", x_direct)          # Outputs: [2. 3.]
print("Solution via A^-1:", x_via_inverse)   # Outputs: [2. 3.]