Appearance
14. Матричное умножение (2D) и симуляция полносвязных слоев
Линейная алгебра vs Поэлементная математика
Разница между операторами * и @ (numpy.dot)
В NumPy существует принципиальное различие между двумя видами умножения двумерных матриц:
- Поэлементное умножение (оператор
*): Перемножает элементы матриц, находящиеся на одинаковых позициях. Требует строгого совпадения геометрии (shape) обеих матриц. - Настоящее матричное умножение (оператор
@или функция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) слой является базовым кирпичиком большинства нейросетевых архитектур. Математически прямой проход через такой слой описывается классической формулой линейной комбинации:
— входная матрица признаков (батч данных) размерности (batch_size, input_features).— матрица обучаемых весов слоя размерности (input_features, output_neurons).— вектор смещения (bias) размерности (output_neurons,), который автоматически добавляется к каждой строке матрицы результата благодаря механизму трансляции NumPy.— выходная матрица активаций размерности (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) и ловушка вырожденных матриц
Обратная матрица
Вырожденная (сингулярная) матрица
Если 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Решение систем линейных алгебраических уравнений (СЛАУ)
На практике прямое вычисление обратной матрицы — неэффективный и менее точный способ решить систему numpy.linalg.solve(A, v), которая находит вектор x напрямую, методами матричного разложения, без вычисления
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)
Собственный вектор — это ненулевой вектор
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). - Собственные значения матриц
и всегда совпадают (при транспонировании меняться могут только собственные векторы):
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 bytes8. Модификация весов на месте (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 = 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.]