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

第一次接触机器学习时,看到满屏的矩阵运算和希腊字母公式,我的大脑直接宕机了三分钟。直到在Jupyter Notebook里用NumPy画出第一个向量旋转动画,才突然理解为什么说"线性代数是机器学习的语法"。本文将用 可交互的代码 动态可视化 ,带你从几何视角重新认识那些抽象概念——你会发现,特征值分解不过是空间变形的"体检报告",而PCA降维本质上是在给数据找最舒服的睡姿。

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

打开你的Jupyter Notebook,我们先从最基础的向量开始。运行下面这段代码,你会看到一个箭头在三维空间中的舞动:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.animation import FuncAnimation

fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

# 创建初始向量
vector = np.array([2, 3, 1])
ax.quiver(0, 0, 0, vector[0], vector[1], vector[2], 
          color='r', arrow_length_ratio=0.1)

# 动画更新函数
def update(frame):
    ax.cla()
    theta = np.radians(frame)
    rotation_z = np.array([
        [np.cos(theta), -np.sin(theta), 0],
        [np.sin(theta), np.cos(theta), 0],
        [0, 0, 1]
    ])
    rotated_vector = rotation_z @ vector
    ax.quiver(0, 0, 0, rotated_vector[0], rotated_vector[1], rotated_vector[2],
              color='r', arrow_length_ratio=0.1)
    ax.set_xlim([-4, 4])
    ax.set_ylim([-4, 4])
    ax.set_zlim([-4, 4])
    ax.set_title(f'向量旋转 (角度: {frame}°)')

ani = FuncAnimation(fig, update, frames=np.arange(0, 360, 2), interval=50)
plt.close()
from IPython.display import HTML
HTML(ani.to_jshtml())

这个旋转的红色箭头就是三维向量的几何表示。在机器学习中, 每个向量都是特征空间中的一个点 ——比如在房价预测中,向量 [面积, 卧室数, 房龄] 就代表一套房子的"数学身份证"。

矩阵则是更强大的空间变形器。试试用下面这个变换矩阵对一组数据点进行线性变换:

# 创建网格点
x = np.linspace(-3, 3, 20)
y = np.linspace(-3, 3, 20)
X, Y = np.meshgrid(x, y)
points = np.vstack([X.ravel(), Y.ravel()]).T

# 定义变换矩阵
transform = np.array([[1.5, 0.8], 
                      [0.3, 1.2]])

# 应用变换
transformed = points @ transform.T

# 可视化
plt.figure(figsize=(12, 5))
plt.subplot(121)
plt.scatter(points[:,0], points[:,1], alpha=0.6)
plt.title("原始数据空间")
plt.grid(True)

plt.subplot(122)
plt.scatter(transformed[:,0], transformed[:,1], alpha=0.6, color='r')
plt.title("矩阵变换后的空间")
plt.grid(True)
plt.show()

你会看到整齐的网格被"拉扯"成了平行四边形——这就是矩阵的 线性变换 本质。在机器学习中:

  • 权重矩阵 决定了特征如何组合
  • 协方差矩阵 描述了数据分布的形状
  • 投影矩阵 实现了降维操作

小技巧:在Jupyter中运行 %matplotlib notebook 可以获取交互式可视化,用鼠标旋转3D图形观察不同角度

2. 矩阵运算的机器学习语义

矩阵乘法不是随意的数学游戏,每个操作在机器学习中都有明确的应用场景。让我们用实际数据来演示:

2.1 矩阵乘法与特征交叉

假设我们有以下用户行为数据:

# 用户-物品交互矩阵 (5用户 × 4物品)
interactions = np.array([
    [1, 0, 1, 0],  # 用户1
    [0, 1, 1, 1],  # 用户2
    [1, 1, 0, 0],  # 用户3
    [0, 0, 1, 1],  # 用户4
    [1, 0, 0, 1]   # 用户5
])

# 物品-特征矩阵 (4物品 × 3特征)
item_features = np.array([
    [0.9, 0.2, 0.1],  # 物品1:电子产品
    [0.1, 0.8, 0.3],  # 物品2:家居用品
    [0.3, 0.4, 0.9],  # 物品3:运动装备
    [0.2, 0.7, 0.5]   # 物品4:厨房用具
])

# 计算用户特征偏好
user_preferences = interactions @ item_features
print("用户特征偏好矩阵:\n", user_preferences)

这个矩阵乘法操作实际上完成了:

  1. 将用户行为与物品特征关联
  2. 自动生成用户的隐含特征偏好
  3. 为推荐系统提供可解释的特征向量

2.2 逆矩阵与线性回归

在多元线性回归中,正规方程的解就依赖于矩阵求逆:

# 生成模拟数据
np.random.seed(42)
X = np.random.rand(100, 3)  # 100样本, 3特征
true_weights = np.array([2.5, -1.2, 0.8])
y = X @ true_weights + np.random.normal(0, 0.1, 100)

# 计算最优权重
XTX = X.T @ X
XTX_inv = np.linalg.inv(XTX)  # 矩阵求逆
optimal_weights = XTX_inv @ X.T @ y

print("真实权重:", true_weights)
print("估计权重:", optimal_weights)

虽然实际中我们更多使用梯度下降,但这个数学解揭示了模型参数学习的本质——寻找能最好解释输入输出关系的 线性变换

3. 特征值与特征向量的实战解读

特征分解可能是线性代数中最抽象的概念之一,但它的几何意义非常直观。运行以下代码观察特征向量如何揭示矩阵的"变形主轴":

# 创建一个有明确方向的变换矩阵
cov_matrix = np.array([
    [4, 2],
    [2, 3]
])

# 计算特征值和特征向量
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)

# 可视化
plt.figure(figsize=(10, 8))
circle = plt.Circle((0, 0), 1, fill=False, linestyle='--')

# 绘制单位圆
plt.gca().add_patch(circle)

# 绘制特征向量
for i, (value, vector) in enumerate(zip(eigenvalues, eigenvectors.T)):
    plt.quiver(0, 0, vector[0]*np.sqrt(value), vector[1]*np.sqrt(value), 
               angles='xy', scale_units='xy', scale=1, color=['r','b'][i],
               label=f'特征向量 {i+1} (λ={value:.2f})')

# 应用矩阵变换后的椭圆
theta = np.linspace(0, 2*np.pi, 100)
points = np.vstack([np.cos(theta), np.sin(theta)]).T
transformed = points @ cov_matrix
plt.plot(transformed[:,0], transformed[:,1], 'g-', label='变换后的椭圆')

plt.axis('equal')
plt.legend()
plt.grid()
plt.title("特征向量揭示矩阵变换的主方向")
plt.show()

在PCA降维中,我们正是沿着这些 特征向量方向 保留最大方差。试试用NumPy实现简易版PCA:

# 生成带相关性的二维数据
np.random.seed(42)
data = np.random.multivariate_normal(
    mean=[0, 0],
    cov=[[3, 2.8], [2.8, 3]],
    size=500
)

# 中心化数据
centered = data - data.mean(axis=0)

# 计算协方差矩阵
cov = centered.T @ centered / (len(data) - 1)

# 特征分解
eigenvals, eigenvecs = np.linalg.eig(cov)

# 选择主成分
principal_component = eigenvecs[:, np.argmax(eigenvals)]

# 投影数据
projected = centered @ principal_component

# 可视化
plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.scatter(centered[:,0], centered[:,1], alpha=0.6)
plt.quiver(0, 0, principal_component[0], principal_component[1], 
           angles='xy', scale_units='xy', scale=1, color='r')
plt.title("原始数据与主成分方向")

plt.subplot(122)
plt.scatter(projected, np.zeros_like(projected), alpha=0.6)
plt.title("数据在一维主成分上的投影")
plt.show()

4. 奇异值分解(SVD)的降维魔法

SVD是线性代数送给数据科学的超级礼物。让我们分解一张图片来理解它的威力:

from sklearn.datasets import load_sample_image

# 加载示例图片
china = load_sample_image("china.jpg")
gray = china.mean(axis=2) / 255  # 转为灰度

# 执行SVD
U, s, Vt = np.linalg.svd(gray)

# 逐步增加奇异值数量重建图像
components = [1, 5, 10, 50, 100]
plt.figure(figsize=(15, 8))
for i, k in enumerate(components):
    reconstructed = U[:, :k] @ np.diag(s[:k]) @ Vt[:k, :]
    
    plt.subplot(2, 3, i+1)
    plt.imshow(reconstructed, cmap='gray')
    plt.title(f"使用前{k}个奇异值")
    plt.axis('off')

plt.subplot(2, 3, 6)
plt.imshow(gray, cmap='gray')
plt.title("原始图像")
plt.axis('off')
plt.tight_layout()
plt.show()

这个实验展示了SVD如何用少量 奇异三元组 (U, Σ, Vᵀ)捕捉图像的主要信息。在推荐系统中,SVD同样能提取用户和物品的潜在特征:

# 继续使用之前的交互矩阵
U, s, Vt = np.linalg.svd(interactions, full_matrices=False)

# 使用低秩近似
k = 2  # 选择2个潜在因子
user_factors = U[:, :k] @ np.diag(s[:k])
item_factors = Vt[:k, :].T

print("用户潜在特征:\n", user_factors)
print("\n物品潜在特征:\n", item_factors)

# 重建近似矩阵
approx = user_factors @ item_factors.T
print("\n重建的交互矩阵:\n", np.round(approx, 2))

这种 潜在语义分析 正是协同过滤推荐的核心——通过降维发现用户和物品之间隐藏的关联模式。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐