别怕数学!用Python和NumPy图解机器学习里的线性代数(附代码)

第一次接触机器学习时,看到满屏的矩阵运算和希腊字母公式,我的大脑直接宕机了三分钟。直到在Jupyter Notebook里用Matplotlib画出第一个旋转的向量,才突然理解矩阵乘法原来是在做空间变换——这种"啊哈时刻"正是我想分享给你的体验。本文将用Python代码和动态可视化,带你从几何角度重新认识线性代数。

1. 从几何视角理解向量与矩阵

1.1 向量:不只是数字列表

在NumPy中创建一个二维向量只需要一行代码:

import numpy as np
v = np.array([3, 1])

但更直观的理解方式是把它看作平面上的箭头。用Matplotlib可视化:

import matplotlib.pyplot as plt

plt.quiver(0, 0, v[0], v[1], angles='xy', scale_units='xy', scale=1, color='r')
plt.xlim(-5, 5)
plt.ylim(-5, 5)
plt.grid()
plt.show()

你会看到一个从原点指向(3,1)的红色箭头。这就是向量的几何本质—— 具有大小和方向的量

1.2 矩阵:空间变换的魔法师

当我们将矩阵与向量相乘时,实际上是在对向量进行变换。看这个例子:

A = np.array([[2, 0], [0, 2]])  # 缩放矩阵
transformed_v = A @ v  # 矩阵乘法

把变换前后的向量画在一起:

plt.quiver(0, 0, v[0], v[1], angles='xy', scale_units='xy', scale=1, color='r')
plt.quiver(0, 0, transformed_v[0], transformed_v[1], angles='xy', scale_units='xy', scale=1, color='b')
# [...] 省略其他绘图代码

你会发现蓝色向量是红色向量的两倍——这个矩阵实现了2倍缩放。常见的矩阵变换类型包括:

矩阵类型 变换效果 示例矩阵
缩放矩阵 按比例放大/缩小 [[2,0],[0,2]]
旋转矩阵 旋转特定角度 [[0,-1],[1,0]] (90°)
剪切矩阵 使形状倾斜 [[1,1],[0,1]]

2. 矩阵乘法:变换的叠加

2.1 矩阵乘法的几何意义

连续应用两个矩阵变换,等价于两个矩阵相乘后的单次变换。让我们用动画演示:

from matplotlib.animation import FuncAnimation

fig, ax = plt.subplots()
def update(i):
    angle = np.radians(i)
    R = np.array([[np.cos(angle), -np.sin(angle)], 
                 [np.sin(angle), np.cos(angle)]])  # 旋转矩阵
    transformed = R @ v
    ax.clear()
    ax.quiver(0, 0, v[0], v[1], color='r')
    ax.quiver(0, 0, transformed[0], transformed[1], color='b')
    ax.set_xlim(-4, 4); ax.set_ylim(-4, 4)
    
ani = FuncAnimation(fig, update, frames=360, interval=50)
plt.close()

这段代码会生成向量v旋转360度的动画(实际使用时需要保存为gif或视频)。你会发现矩阵乘法完美描述了旋转这种连续变换。

2.2 矩阵乘法的不可交换性

与数字乘法不同,矩阵乘法顺序会影响最终结果:

A = np.array([[1, 1], [0, 1]])  # 剪切矩阵
B = np.array([[0, -1], [1, 0]])  # 旋转矩阵

print("A×B:\n", A @ B)
print("B×A:\n", B @ A)

输出结果完全不同。这解释了为什么神经网络中层的顺序如此重要——不同的矩阵乘法顺序会导致完全不同的特征变换。

3. 特征分解:抓住变换的本质

3.1 特征向量与特征值

特征向量是在矩阵变换下方向保持不变的向量,特征值则是其缩放比例。计算它们:

C = np.array([[4, 1], [2, 3]])
eigenvalues, eigenvectors = np.linalg.eig(C)

print("特征值:", eigenvalues)
print("特征向量:\n", eigenvectors)

可视化特征向量:

origin = np.zeros(2)
plt.quiver(*origin, *eigenvectors[:,0], color=['r'], scale=5)
plt.quiver(*origin, *eigenvectors[:,1], color=['b'], scale=5)
# [...] 省略绘图代码

红色和蓝色箭头代表两个特征向量方向。即使经过矩阵C变换,它们也只会伸长或缩短,不会改变方向。

3.2 特征分解的应用

特征分解在PCA降维中至关重要。假设我们有一个数据矩阵X,PCA的核心步骤就是计算协方差矩阵的特征分解:

# 假设X是我们的数据矩阵,每行是一个样本
X_centered = X - X.mean(axis=0)
cov_matrix = np.cov(X_centered.T)
eig_vals, eig_vecs = np.linalg.eig(cov_matrix)

# 按特征值大小排序
sorted_idx = np.argsort(eig_vals)[::-1]
principal_components = eig_vecs[:, sorted_idx[:2]]  # 取前两个主成分

通过投影到主成分方向,我们就能实现降维:

X_pca = X_centered @ principal_components

4. 奇异值分解(SVD):更强大的工具

4.1 SVD的几何解释

任何矩阵都可以分解为旋转→缩放→旋转的组合:

D = np.random.randn(3, 2)  # 随机矩阵
U, S, Vt = np.linalg.svd(D)

print("U (左奇异向量):\n", U)
print("S (奇异值):\n", S)
print("Vt (右奇异向量转置):\n", Vt)

可视化这个过程:

  1. 用Vt旋转输入空间
  2. 用S缩放坐标轴
  3. 用U旋转到输出空间

4.2 SVD在推荐系统中的应用

SVD是协同过滤推荐算法的核心。假设用户-物品评分矩阵为R:

# 假设R是m×n的评分矩阵
U, S, Vt = np.linalg.svd(R, full_matrices=False)
k = 10  # 选择前10个奇异值
R_approx = U[:, :k] @ np.diag(S[:k]) @ Vt[:k, :]

这个低秩近似R_approx可以预测缺失的评分,实现个性化推荐。在实际项目中,我们通常会使用更高级的变体如FunkSVD来处理稀疏矩阵。

5. 线性代数在神经网络中的体现

5.1 全连接层的矩阵表示

神经网络的每一层本质上都是线性变换加非线性激活。考虑一个简单的两层网络:

# 输入层到隐藏层
W1 = np.random.randn(input_size, hidden_size) * 0.01
b1 = np.zeros(hidden_size)
hidden = np.maximum(0, X @ W1 + b1)  # ReLU激活

# 隐藏层到输出层
W2 = np.random.randn(hidden_size, output_size) * 0.01
scores = hidden @ W2

反向传播中的梯度计算也大量使用矩阵运算:

# 假设dscores是损失函数对scores的梯度
dW2 = hidden.T @ dscores
db2 = np.sum(dscores, axis=0)

5.2 卷积的Toeplitz矩阵形式

虽然卷积通常用滑动窗口实现,但它也可以表示为特殊的Toeplitz矩阵乘法:

def conv_to_matrix(kernel, input_size):
    # 为简单起见,考虑1D情况
    output_size = input_size - len(kernel) + 1
    T = np.zeros((output_size, input_size))
    for i in range(output_size):
        T[i, i:i+len(kernel)] = kernel
    return T

kernel = [1, -1, 0.5]
T = conv_to_matrix(kernel, 10)
input_signal = np.random.randn(10)
conv_result = T @ input_signal  # 等价于卷积运算

理解这种表示方式有助于分析CNN的性质。

更多推荐