# 向量、矩阵与运算

> 每一个神经网络，无非是多绕了几道弯的矩阵乘法。

**类型：** Build
**语言：** Python、Julia
**前置要求：** 阶段 1，第 01 课（线性代数直觉）
**预计时间：** ~60 分钟

## 学习目标

- 构建一个 Matrix 类，支持逐元素运算、矩阵乘法、转置、行列式和逆
- 区分逐元素乘法和矩阵乘法，并讲清楚各自什么时候用
- 只用从零写的 Matrix 类，实现一个稠密神经网络层（`relu(W @ x + b)`）
- 解释广播规则，以及偏置加法在神经网络框架里是怎么运作的

## 问题背景

你想构建一个神经网络。读代码时看到这么一行：

```
output = activation(weights @ input + bias)
```

那个 `@` 是矩阵乘法。`weights` 是矩阵。`input` 是向量。如果你不知道这些运算在做什么，这行就是魔法。如果你知道，它就是一层网络的完整前向传播，三个操作搞定。

模型处理的每张图都是像素值矩阵。每个词 embedding 都是向量。每个神经网络的每一层都是矩阵变换。不熟练矩阵运算就没法构建 AI 系统，就像不懂变量就没法写代码一样。

本节课从零打磨这份熟练度。

## 核心概念

### 向量：有序的数字列表

向量是一串带有方向和模长的数字。在 AI 里，向量表示数据点、特征或参数。

```
v = [3, 4]        -- a 2D vector
w = [1, 0, -2]    -- a 3D vector
```

二维向量 `[3, 4]` 指向平面上的坐标 (3, 4)。它的长度（模长）是 5（就是 3-4-5 直角三角形）。

### 矩阵：数字的网格

矩阵是一个二维网格，有行有列。一个 m x n 的矩阵有 m 行 n 列。

```
A = | 1  2  3 |     -- 2x3 matrix (2 rows, 3 columns)
    | 4  5  6 |
```

在神经网络里，权重矩阵把输入向量变换成输出向量。一个输入 784、输出 128 的层，用的是一个 128x784 的权重矩阵。

### 形状为什么重要

矩阵乘法有条铁律：`(m x n) @ (n x p) = (m x p)`。内层维度必须匹配。

```
(128 x 784) @ (784 x 1) = (128 x 1)
  weights       input       output

Inner dimensions: 784 = 784  -- valid
```

如果你在 PyTorch 里碰到形状不匹配的报错，原因就在这。

### 运算对照表

| 运算 | 它做什么 | 神经网络中的用途 |
|-----------|-------------|-------------------|
| 加法 | 逐元素相加 | 给输出加偏置 |
| 标量乘法 | 缩放每个元素 | 学习率 * 梯度 |
| 矩阵乘法 | 变换向量 | 层的前向传播 |
| 转置 | 翻转行和列 | 反向传播 |
| 行列式 | 浓缩成一个数 | 检查可逆性 |
| 逆 | 撤销一次变换 | 解线性方程组 |
| 单位矩阵 | 什么都不做的矩阵 | 初始化、残差连接 |

### 逐元素乘法 vs 矩阵乘法

这个区别经常把新手绊倒。

逐元素：对应位置相乘。两个矩阵必须形状相同。

```
| 1  2 |   | 5  6 |   | 5  12 |
| 3  4 | * | 7  8 | = | 21 32 |
```

矩阵乘法：行与列做点积。内层维度必须匹配。

```
| 1  2 |   | 5  6 |   | 1*5+2*7  1*6+2*8 |   | 19  22 |
| 3  4 | @ | 7  8 | = | 3*5+4*7  3*6+4*8 | = | 43  50 |
```

不同的运算，不同的结果，不同的规则。

### 广播

当你把一个偏置向量加到一个输出矩阵上时，形状对不上。广播会把较小的数组拉伸到合适的尺寸。

```
| 1  2  3 |   +   [10, 20, 30]
| 4  5  6 |

Broadcasting stretches the vector across rows:

| 1  2  3 |   | 10  20  30 |   | 11  22  33 |
| 4  5  6 | + | 10  20  30 | = | 14  25  36 |
```

每个现代框架都会自动做这件事。理解它能让你在形状看起来不对、代码却照跑不误时不犯迷糊。

```figure
vector-projection
```

## 动手构建

### 第 1 步：Vector 类

```python
class Vector:
    def __init__(self, data):
        self.data = list(data)
        self.size = len(self.data)

    def __repr__(self):
        return f"Vector({self.data})"

    def __add__(self, other):
        return Vector([a + b for a, b in zip(self.data, other.data)])

    def __sub__(self, other):
        return Vector([a - b for a, b in zip(self.data, other.data)])

    def __mul__(self, scalar):
        return Vector([x * scalar for x in self.data])

    def dot(self, other):
        return sum(a * b for a, b in zip(self.data, other.data))

    def magnitude(self):
        return sum(x ** 2 for x in self.data) ** 0.5
```

### 第 2 步：带核心运算的 Matrix 类

```python
class Matrix:
    def __init__(self, data):
        self.data = [list(row) for row in data]
        self.rows = len(self.data)
        self.cols = len(self.data[0])
        self.shape = (self.rows, self.cols)

    def __repr__(self):
        rows_str = "\n  ".join(str(row) for row in self.data)
        return f"Matrix({self.shape}):\n  {rows_str}"

    def __add__(self, other):
        return Matrix([
            [self.data[i][j] + other.data[i][j] for j in range(self.cols)]
            for i in range(self.rows)
        ])

    def __sub__(self, other):
        return Matrix([
            [self.data[i][j] - other.data[i][j] for j in range(self.cols)]
            for i in range(self.rows)
        ])

    def scalar_multiply(self, scalar):
        return Matrix([
            [self.data[i][j] * scalar for j in range(self.cols)]
            for i in range(self.rows)
        ])

    def element_wise_multiply(self, other):
        return Matrix([
            [self.data[i][j] * other.data[i][j] for j in range(self.cols)]
            for i in range(self.rows)
        ])

    def matmul(self, other):
        return Matrix([
            [
                sum(self.data[i][k] * other.data[k][j] for k in range(self.cols))
                for j in range(other.cols)
            ]
            for i in range(self.rows)
        ])

    def transpose(self):
        return Matrix([
            [self.data[j][i] for j in range(self.rows)]
            for i in range(self.cols)
        ])

    def determinant(self):
        if self.shape == (1, 1):
            return self.data[0][0]
        if self.shape == (2, 2):
            return self.data[0][0] * self.data[1][1] - self.data[0][1] * self.data[1][0]
        det = 0
        for j in range(self.cols):
            minor = Matrix([
                [self.data[i][k] for k in range(self.cols) if k != j]
                for i in range(1, self.rows)
            ])
            det += ((-1) ** j) * self.data[0][j] * minor.determinant()
        return det

    def inverse_2x2(self):
        det = self.determinant()
        if det == 0:
            raise ValueError("Matrix is singular, no inverse exists")
        return Matrix([
            [self.data[1][1] / det, -self.data[0][1] / det],
            [-self.data[1][0] / det, self.data[0][0] / det]
        ])

    @staticmethod
    def identity(n):
        return Matrix([
            [1 if i == j else 0 for j in range(n)]
            for i in range(n)
        ])
```

### 第 3 步：跑起来看看

```python
A = Matrix([[1, 2], [3, 4]])
B = Matrix([[5, 6], [7, 8]])

print("A + B =", (A + B).data)
print("A @ B =", A.matmul(B).data)
print("A^T =", A.transpose().data)
print("det(A) =", A.determinant())
print("A^-1 =", A.inverse_2x2().data)

I = Matrix.identity(2)
print("A @ A^-1 =", A.matmul(A.inverse_2x2()).data)
```

### 第 4 步：和神经网络对接

```python
import random

inputs = Matrix([[0.5], [0.8], [0.2]])
weights = Matrix([
    [random.uniform(-1, 1) for _ in range(3)]
    for _ in range(2)
])
bias = Matrix([[0.1], [0.1]])

def relu_matrix(m):
    return Matrix([[max(0, val) for val in row] for row in m.data])

pre_activation = weights.matmul(inputs) + bias
output = relu_matrix(pre_activation)

print(f"Input shape: {inputs.shape}")
print(f"Weight shape: {weights.shape}")
print(f"Output shape: {output.shape}")
print(f"Output: {output.data}")
```

这就是一个稠密层：`output = relu(W @ x + b)`。每个神经网络里的每个稠密层，干的就是这件事。

## 实际使用

上面这些 NumPy 用更少的代码、快好几个数量级就能做到。

```python
import numpy as np

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

print("A + B =\n", A + B)
print("A * B (element-wise) =\n", A * B)
print("A @ B (matrix multiply) =\n", A @ B)
print("A^T =\n", A.T)
print("det(A) =", np.linalg.det(A))
print("A^-1 =\n", np.linalg.inv(A))
print("I =\n", np.eye(2))

inputs = np.random.randn(3, 1)
weights = np.random.randn(2, 3)
bias = np.array([[0.1], [0.1]])
output = np.maximum(0, weights @ inputs + bias)

print(f"\nNeural network layer: {weights.shape} @ {inputs.shape} = {output.shape}")
print(f"Output:\n{output}")
```

Python 里的 `@` 运算符会调用 `__matmul__`。NumPy 用 C 和 Fortran 写的优化版 BLAS 例程来实现它。一样的数学，快 100 倍。

NumPy 里的广播：

```python
matrix = np.array([[1, 2, 3], [4, 5, 6]])
bias = np.array([10, 20, 30])
print(matrix + bias)
```

NumPy 自动把一维的 bias 广播到两行上。每个神经网络框架里的偏置加法都是这么干的。

## 拿去用

本节课产出一个通过几何直觉来教矩阵运算的提示词。参见 `outputs/prompt-matrix-operations.md`。

这里构建的 Matrix 类，是我们在阶段 3、第 10 课构建迷你神经网络框架的基础。

## 练习

1. **验证逆矩阵。** 计算 `A @ A.inverse_2x2()`，确认得到的是单位矩阵。换三个不同的 2x2 矩阵各试一遍。行列式为零时会发生什么？

2. **实现 3x3 求逆。** 扩展 Matrix 类，用伴随矩阵法计算 3x3 矩阵的逆。和 NumPy 的 `np.linalg.inv` 对照测试。

3. **搭一个两层网络。** 只用你的 Matrix 类（不用 NumPy），构建一个两层神经网络：输入 (3) -> 隐藏 (4) -> 输出 (2)。随机初始化权重，跑一次前向传播，确认所有形状都正确。

## 关键术语

| 术语 | 人们常说 | 它实际指什么 |
|------|----------------|----------------------|
| 向量 | "一个箭头" | 一串有序的数字。在 AI 里：高维空间中的一个点。 |
| 矩阵 | "一张数字表" | 一个线性变换。它把向量从一个空间映射到另一个空间。 |
| 矩阵乘法 | "把数字乘一乘" | 第一个矩阵的每一行与第二个矩阵的每一列做点积。顺序很重要。 |
| 转置 | "翻过来" | 交换行和列。把 m x n 矩阵变成 n x m。在反向传播里至关重要。 |
| 行列式 | "矩阵里算出的某个数" | 衡量矩阵把面积（二维）或体积（三维）放大了多少。为零意味着这个变换压扁了一个维度。 |
| 逆 | "撤销矩阵" | 逆转该变换的矩阵。只有行列式不为零时才存在。 |
| 单位矩阵 | "无聊的矩阵" | 矩阵里相当于乘以 1 的东西。用于残差连接（ResNet）。 |
| 广播 | "魔法形状修复" | 通过沿缺失维度重复，把较小的数组拉伸到与较大的数组匹配。 |
| 逐元素 | "普通的乘法" | 对应位置相乘。两个数组形状必须相同（或可广播）。 |

## 延伸阅读

- [3Blue1Brown: Essence of Linear Algebra](https://www.3blue1brown.com/topics/linear-algebra) - 为这里涉及的每个运算提供可视化直觉
- [NumPy documentation on broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html) - NumPy 遵循的确切规则
- [Stanford CS229 Linear Algebra Review](http://cs229.stanford.edu/section/cs229-linalg.pdf) - ML 专用线性代数的简明参考
