原则 :每一个符号都解释,每一步推导都显式写出,几何直觉与代数公式并行。
数学对象体系
我们先认识三种”数表”对象——标量、向量、矩阵,以及它们的运算;随后抽象出它们共同赖以存在的母体向量空间 ,再以此为地基依次展开线性映射、行列式、特征值与分解。
\[
\text{标量} \to \text{向量} \to \text{矩阵}
\quad
\text{维度:} 0,\; 1,\; 2
\]
张量 \(\mathcal{T}\) 是矩阵的高维推广,作为延伸 放在 notebook 最后;本笔记主线只到矩阵为止。
标量 Scalar
\[
s \in \mathbb{R}
\]
就是一个单独的数 。如温度 \(T=3.2\) ,学习率 \(\eta = 0.001\) 。
表示 :小写斜体 \(s, t, \alpha, \beta\)
所属集合 :\(\mathbb{R}\) (实数集)
向量 Vector
\[
\mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{bmatrix} \in \mathbb{R}^n
\]
\(n\) 个标量排成一列 。
表示 :小写粗体 \(\mathbf{x}, \mathbf{y}, \mathbf{v}\) ,或箭头 \(\vec{x}\)
\(\mathbb{R}^n\) = \(n\) 维实数空间
\(x_i\) = 向量的第 \(i\) 个分量/元素
几何直觉
\(\mathbb{R}^1\)
数轴上的一个点
\([3]\)
\(\mathbb{R}^2\)
平面上的一个点/箭头
\([3, 4]^T\)
\(\mathbb{R}^3\)
空间中的一个点/箭头
\([1, 2, 3]^T\)
\(\mathbb{R}^{768}\)
768维空间中的一个点
BERT 的词向量
行向量 vs 列向量
\[
\text{列向量:} \mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ x_3 \end{bmatrix} \quad
\text{行向量:} \mathbf{x}^T = \begin{bmatrix} x_1 & x_2 & x_3 \end{bmatrix}
\]
默认:向量都是列向量 。行向量用转置 \(\mathbf{x}^T\) 表示。
代码表示:向量的数组表示与打印
练习 :声明向量 \(\mathbf{x} = [1.0, 2.0, 3.0]^T\) ,分别以列格式和行格式打印。
#include <stdio.h>
// 用 C 数组表示向量
// 列向量 x = [1.0, 2.0, 3.0]^T
int main() {
double x[] = { 1.0 , 2.0 , 3.0 };
int n = sizeof ( x) / sizeof ( x[ 0 ]);
printf( "Vector x (column): \n " );
for ( int i = 0 ; i < n; i++)
printf( " x[ %d ] = %.1f\n " , i, x[ i]);
printf( " \n Vector x^T (row): \n [" );
for ( int i = 0 ; i < n; i++)
printf( " %.1f%s " , x[ i], i < n- 1 ? ", " : "] \n " );
return 0 ;
}
Vector x (column):
x[0] = 1.0
x[1] = 2.0
x[2] = 3.0
Vector x^T (row):
[1.0, 2.0, 3.0]
矩阵 Matrix
\[
A = \begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \cdots & a_{mn} \end{bmatrix} \in \mathbb{R}^{m \times n}
\]
\(m\) 行 \(n\) 列的数表 。
表示 :大写 \(A, B, W\)
\(a_{ij}\) = 第 \(i\) 行第 \(j\) 列的元素
\(A \in \mathbb{R}^{m \times n}\) :\(m\) 行 \(n\) 列的实数矩阵
等价理解
列向量的集合
\(A = [\mathbf{a}_1 \vert \mathbf{a}_2 \vert \cdots \vert \mathbf{a}_n]\) ,每列是一个 \(\mathbb{R}^m\) 中的向量
行向量的集合
\(A\) 每行是一个 \(\mathbb{R}^n\) 中的向量
线性变换(预告)
\(A\) 把 \(\mathbb{R}^n\) 中的向量映射成 \(\mathbb{R}^m\) 中的向量(详见”线性变换”节)
代码表示:矩阵的行主序存储与打印
练习 :编写 print_matrix(name, A, m, n),以行主序一维数组存储并打印 \(2 \times 3\) 矩阵。
#include <stdio.h>
// 打印 m×n 矩阵
void print_matrix( const char * name, double * A, int m, int n) {
printf( " %s ( %d x %d ): \n " , name, m, n);
for ( int i = 0 ; i < m; i++) {
for ( int j = 0 ; j < n; j++)
printf( " %8.2f " , A[ i * n + j]);
printf( " \n " );
}
}
int main() {
// A = [[1, 2, 3], [4, 5, 6]] (2×3)
double A[] = { 1 , 2 , 3 , 4 , 5 , 6 };
print_matrix( "A" , A, 2 , 3 );
return 0 ;
}
A (2x3):
1.00 2.00 3.00
4.00 5.00 6.00
常用基本矩阵:单位、零、对角
后续几乎每个公式都会用到三种特殊矩阵,先在此一次性定义清楚。
单位矩阵 \(I\)
\[
I_n = \begin{bmatrix} 1 & 0 & \cdots & 0 \\ 0 & 1 & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & 1 \end{bmatrix}, \qquad (I_n)_{ij} = \begin{cases} 1, & i=j \\ 0, & i\ne j \end{cases}
\]
对角线全为 1、其余全为 0 的方阵。
它是矩阵世界里的”1” :对任意矩阵 \(A\) ,\(AI = IA = A\) ,正如 \(1\cdot a = a\) 。
逆矩阵的定义就建立在它之上:\(A^{-1}A = AA^{-1} = I\) 。
特征方程 \((A-\lambda I)\mathbf{v}=\mathbf{0}\) 、矩阵指数 \(e^{A}=I+A+\tfrac{A^2}{2!}+\cdots\) 里的 \(I\) 都是它。
零矩阵 \(0\)
所有元素都是 0 的矩阵,矩阵世界里的”0”:\(A + 0 = A\) ,\(A\cdot 0 = 0\) 。零向量是它只有一列的特例(向量空间中的零元 \(\mathbf{0}\) 见”向量空间”节);零空间 \(\ker(A)=\{\mathbf{x}\mid A\mathbf{x}=\mathbf{0}\}\) 里的 \(\mathbf{0}\) 也是它。
张量 Tensor
本节是主线之外的延伸。线性代数第一轮只研究到矩阵(2 阶);张量严格说属于多重线性代数 ,但深度学习里处处可见,故在此补上。
\[
\mathcal{T} \in \mathbb{R}^{d_1 \times d_2 \times \cdots \times d_k}
\]
多维数组 ,是矩阵的高维推广——把”标量 → 向量 → 矩阵”的维度链条继续往下推:
0阶
标量
\(s\)
1阶
向量
\(\mathbf{x} \in \mathbb{R}^n\)
2阶
矩阵
\(A \in \mathbb{R}^{m \times n}\)
3阶
3D张量
\(\mathcal{T} \in \mathbb{R}^{B \times n \times d}\) (batch × seq_len × dim)
4阶
4D张量
\(\mathcal{T} \in \mathbb{R}^{B \times C \times H \times W}\) (图像 batch)
核心运算
向量加向量
\[
\mathbf{x} + \mathbf{y} = \begin{bmatrix} x_1 + y_1 \\ x_2 + y_2 \\ \vdots \\ x_n + y_n \end{bmatrix}
\]
几何 :平行四边形法则 / 三角形法则。
y
/
/ x+y
/_____
x
要求 :两个向量维度必须相同。
代码表示:向量加向量
练习 :实现 vec_add(z, x, y, n),逐元素计算 \(\mathbf{z} = \mathbf{x} + \mathbf{y}\) ,验证 \([1,2,3] + [4,5,6] = [5,7,9]\) 。
#include <stdio.h>
// 向量加法: z = x + y
void vec_add( double * z, const double * x, const double * y, int n) {
for ( int i = 0 ; i < n; i++)
z[ i] = x[ i] + y[ i];
}
int main() {
double x[] = { 1.0 , 2.0 , 3.0 };
double y[] = { 4.0 , 5.0 , 6.0 };
double z[ 3 ];
vec_add( z, x, y, 3 );
printf( "x = [ %.0f , %.0f , %.0f ] \n " , x[ 0 ], x[ 1 ], x[ 2 ]);
printf( "y = [ %.0f , %.0f , %.0f ] \n " , y[ 0 ], y[ 1 ], y[ 2 ]);
printf( "x + y = [ %.0f , %.0f , %.0f ] \n " , z[ 0 ], z[ 1 ], z[ 2 ]);
return 0 ;
}
x = [1, 2, 3]
y = [4, 5, 6]
x + y = [5, 7, 9]
向量乘标量
\[
\alpha \mathbf{x} = \begin{bmatrix} \alpha x_1 \\ \alpha x_2 \\ \vdots \\ \alpha x_n \end{bmatrix}
\]
几何 :
\(\alpha \gt 1\) :沿原方向拉伸
\(0 \lt \alpha \lt 1\) :沿原方向缩短
\(\alpha \lt 0\) :反方向
代码表示:标量乘法
练习 :实现 scalar_mult(y, alpha, x, n),计算 \(\mathbf{y} = \alpha \mathbf{x}\) 。测试 \(\alpha = 2.0\) 和 \(\alpha = -0.5\) 。
#include <stdio.h>
// 标量乘法: y = alpha * x
void scalar_mult( double * y, double alpha, const double * x, int n) {
for ( int i = 0 ; i < n; i++)
y[ i] = alpha * x[ i];
}
int main() {
double x[] = { 1.0 , 2.0 , 3.0 };
double y[ 3 ];
scalar_mult( y, 2.0 , x, 3 );
printf( "x = [ %.0f , %.0f , %.0f ] \n " , x[ 0 ], x[ 1 ], x[ 2 ]);
printf( "2.0 * x = [ %.0f , %.0f , %.0f ] \n " , y[ 0 ], y[ 1 ], y[ 2 ]);
scalar_mult( y, - 0.5 , x, 3 );
printf( "-0.5 * x = [ %.1f , %.1f , %.1f ] \n " , y[ 0 ], y[ 1 ], y[ 2 ]);
return 0 ;
}
x = [1, 2, 3]
2.0 * x = [2, 4, 6]
-0.5 * x = [-0.5, -1.0, -1.5]
向量内积(点积 / Dot Product)
\[
\mathbf{x} \cdot \mathbf{y} = \mathbf{x}^T \mathbf{y} = \sum_{i=1}^{n} x_i y_i
\]
展开写 :
\[
\begin{bmatrix} x_1 & x_2 & x_3 \end{bmatrix} \begin{bmatrix} y_1 \\ y_2 \\ y_3 \end{bmatrix} = x_1 y_1 + x_2 y_2 + x_3 y_3
\]
行向量 × 列向量 = 标量
几何意义 :
\[
\mathbf{x} \cdot \mathbf{y} = \|\mathbf{x}\| \|\mathbf{y}\| \cos\theta
\]
\(\mathbf{x}^T \mathbf{y} \gt 0\)
夹角 \(\lt 90°\) ,大致同向
\(\mathbf{x}^T \mathbf{y} = 0\)
正交 (垂直)
\(\mathbf{x}^T \mathbf{y} \lt 0\)
夹角 \(\gt 90°\) ,大致反向
上述 \(\cos\theta\) 在任意维度都成立。任意两个非零向量 \(\mathbf{x}, \mathbf{y} \in \mathbb{R}^n\) 必定张成一个 2 维平面(它们的 span),\(\theta\) 就是该平面内两向量的夹角,定义与 2D/3D 完全一致。\(\cos\theta\) 衡量的始终是”两向量有多对齐”,与空间维度无关。4 维及以上无法直观画出,但数学上是同一个定义。
在深度学习中的角色 :Self-Attention 的核心 \(QK^T\) 就是批量内积,衡量每对 token 的相似度!
代码表示:点积、夹角与正交
练习 :实现 dot(x, y, n) 和 norm2(x, n),计算 \(\mathbf{x} \cdot \mathbf{y}\) 、\(\cos\theta\) 、\(\theta\) (度),其中 \(\mathbf{x} = [1,2,3]\) ,\(\mathbf{y} = [4,5,6]\) 。额外验证 \([1,0] \cdot [0,1] = 0\) (正交)。
#include <stdio.h>
#include <math.h>
// 点积: x^T y
double dot( const double * x, const double * y, int n) {
double s = 0.0 ;
for ( int i = 0 ; i < n; i++)
s += x[ i] * y[ i];
return s;
}
// L2 范数
double norm2( const double * x, int n) {
return sqrt( dot( x, x, n));
}
int main() {
double x[] = { 1.0 , 2.0 , 3.0 };
double y[] = { 4.0 , 5.0 , 6.0 };
double d = dot( x, y, 3 );
double cos_theta = d / ( norm2( x, 3 ) * norm2( y, 3 ));
double theta = acos( cos_theta) * 180.0 / 3.14159265358979323846 ;
printf( "x · y = %.2f\n " , d);
printf( "||x|| = %.4f\n " , norm2( x, 3 ));
printf( "||y|| = %.4f\n " , norm2( y, 3 ));
printf( "cos(theta) = %.4f\n " , cos_theta);
printf( "theta = %.2f degrees \n " , theta);
// 正交示例
double a[] = { 1.0 , 0.0 };
double b[] = { 0.0 , 1.0 };
printf( " \n [1,0]·[0,1] = %.1f (正交) \n " , dot( a, b, 2 ));
return 0 ;
}
x · y = 32.00
||x|| = 3.7417
||y|| = 8.7750
cos(theta) = 0.9746
theta = 12.93 degrees
[1,0]·[0,1] = 0.0 (正交)
矩阵-向量乘法
\[
A\mathbf{x} = \mathbf{y}, \quad A \in \mathbb{R}^{m \times n}, \mathbf{x} \in \mathbb{R}^n, \mathbf{y} \in \mathbb{R}^m
\]
以3行2列(m=3,n=2)矩阵与2维向量相乘为例: :
\[
\begin{bmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \\ a_{31} & a_{32} \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} a_{11}x_1 + a_{12}x_2 \\ a_{21}x_1 + a_{22}x_2 \\ a_{31}x_1 + a_{32}x_2 \end{bmatrix}
\]
两种理解方式 :
行视角
\(y_i = \mathbf{r}_i^T \mathbf{x}\)
\(y_i\) 是 \(A\) 的第 \(i\) 行与 \(\mathbf{x}\) 的内积
列视角
\(A\mathbf{x} = x_1 \mathbf{a}_1 + x_2 \mathbf{a}_2 + \cdots + x_n \mathbf{a}_n\)
输出是列向量的线性组合
列视角是最深刻的理解 :矩阵乘法 = 用输入向量的分量作为权重,对矩阵的列做加权求和。
例 :全连接层 \(y = Wx\) ,输出就是 \(W\) 的列向量的线性组合。\(W\) 的每一列可以理解为一个”特征模板”。
代码表示:矩阵-向量乘法
练习 :实现 mat_vec(y, A, x, m, n) 计算 \(\mathbf{y} = A\mathbf{x}\) 。用 \(A_{3\times 2}\) 、\(\mathbf{x} = [1, -1]^T\) 验证列视角:\(\mathbf{y} = 1 \cdot \mathbf{a}_1 + (-1) \cdot \mathbf{a}_2\) 。
#include <stdio.h>
// 矩阵-向量乘法: y = A * x
// A: m×n (row-major), x: n×1, y: m×1
void mat_vec( double * y, const double * A, const double * x, int m, int n) {
for ( int i = 0 ; i < m; i++) {
y[ i] = 0.0 ;
for ( int j = 0 ; j < n; j++)
y[ i] += A[ i * n + j] * x[ j]; // 行视角: y_i = row_i^T * x
}
}
void print_vec( const char * name, const double * v, int n) {
printf( " %s = [" , name);
for ( int i = 0 ; i < n; i++)
printf( " %.1f%s " , v[ i], i < n- 1 ? ", " : "] \n " );
}
int main() {
// A: 3×2
double A[] = { 1 , 2 ,
3 , 4 ,
5 , 6 };
double x[] = { 1.0 , - 1.0 }; // 2×1
double y[ 3 ]; // 3×1
mat_vec( y, A, x, 3 , 2 );
// 列视角验证: y = x[0]*col0 + x[1]*col1
// col0 = [1,3,5]^T, col1 = [2,4,6]^T
// y = 1*[1,3,5] + (-1)*[2,4,6] = [-1, -1, -1]
print_vec( "x" , x, 2 );
printf( "A * x = [ %.1f , %.1f , %.1f ] \n " , y[ 0 ], y[ 1 ], y[ 2 ]);
printf( " \n Column view: 1*[1,3,5] + (-1)*[2,4,6] = [-1,-1,-1] \n " );
return 0 ;
}
x = [1.0, -1.0]
A * x = [-1.0, -1.0, -1.0]
Column view: 1*[1,3,5] + (-1)*[2,4,6] = [-1,-1,-1]
矩阵-矩阵乘法
\[
C = AB, \quad A \in \mathbb{R}^{m \times k}, B \in \mathbb{R}^{k \times n}, C \in \mathbb{R}^{m \times n}
\]
\[
c_{ij} = \sum_{l=1}^{k} a_{il} b_{lj}
\]
四种等价理解 :
元素
\(c_{ij} = \mathbf{a}_{i\cdot}^T \mathbf{b}_{\cdot j}\)
\(C\) 的每个元素是 \(A\) 的行和 \(B\) 的列的内积
列
\(\mathbf{c}_j = A \mathbf{b}_j\)
\(C\) 的每列是 \(A\) 乘 \(B\) 的对应列
行
\(\mathbf{r}_i^C = \mathbf{r}_i^A B\)
\(C\) 的每行是 \(A\) 的对应行乘 \(B\)
外积和
\(C = \sum_{l=1}^k \mathbf{a}_{\cdot l} \mathbf{b}_{l \cdot}\)
\(C\) 是 \(k\) 个秩1矩阵之和
外积(outer product) :列向量 \(\mathbf{u}\in\mathbb{R}^m\) 乘行向量 \(\mathbf{v}^T\in\mathbb{R}^{1\times n}\) ,得到 \(m\times n\) 的矩阵 \(\mathbf{u}\mathbf{v}^T\) (与内积 \(\mathbf{u}^T\mathbf{v}=\text{标量}\) 正好相反)。外积一定是秩1矩阵 (见”秩”节);上表”外积和”视角即把 \(AB\) 拆成 \(k\) 个外积之和。
关键规则 :
内维必须匹配:\(A_{m \times k} B_{k \times n}\) ,中间的 \(k\) 必须相同
一般不满足交换律 :\(AB \neq BA\)
满足结合律:\((AB)C = A(BC)\)
满足分配律:\(A(B+C) = AB + AC\)
举例
(\(A_{2\times 3}\) 乘 \(B_{3\times 2}\) ,内维 \(k=3\) ,结果 \(2\times 2\) ):
\[
\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix} \begin{bmatrix} 7 & 8 \\ 9 & 10 \\ 11 & 12 \end{bmatrix} = \begin{bmatrix} 1\cdot7+2\cdot9+3\cdot11 & 1\cdot8+2\cdot10+3\cdot12 \\ 4\cdot7+5\cdot9+6\cdot11 & 4\cdot8+5\cdot10+6\cdot12 \end{bmatrix} = \begin{bmatrix} 58 & 64 \\ 139 & 154 \end{bmatrix}
\]
反过来
(\(B_{3\times 2}\) 乘 \(A_{2\times 3}\) ,内维 \(k=2\) ,结果 \(3\times 3\) ):
\[
\begin{bmatrix} 7 & 8 \\ 9 & 10 \\ 11 & 12 \end{bmatrix} \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix} = \begin{bmatrix} 7\cdot1+8\cdot4 & 7\cdot2+8\cdot5 & 7\cdot3+8\cdot6 \\ 9\cdot1+10\cdot4 & 9\cdot2+10\cdot5 & 9\cdot3+10\cdot6 \\ 11\cdot1+12\cdot4 & 11\cdot2+12\cdot5 & 11\cdot3+12\cdot6 \end{bmatrix} = \begin{bmatrix} 39 & 54 & 69 \\ 49 & 68 & 87 \\ 59 & 82 & 105 \end{bmatrix}
\]
\(AB\) 是 \(2\times 2\) ,\(BA\) 是 \(3\times 3\) ——形状都不同,更别说数值了,所以 \(AB \neq BA\) 。
在深度学习中的角色 :nn.Linear 的前向传播 \(Y = XW^T + b\) 。
代码表示:矩阵-矩阵乘法
练习 :实现 mat_mul(C, A, B, m, k, n) 计算 \(C = AB\) 。用 \(A_{2\times 3}\) 、\(B_{3\times 2}\) 验证,再计算 \(BA\) 确认 \(AB \neq BA\) 。
#include <stdio.h>
// 矩阵乘法: C = A * B
// A: m×k, B: k×n, C: m×n (row-major)
void mat_mul( double * C, const double * A, const double * B, int m, int k, int n) {
for ( int i = 0 ; i < m; i++)
for ( int j = 0 ; j < n; j++) {
C[ i * n + j] = 0.0 ;
for ( int l = 0 ; l < k; l++)
C[ i * n + j] += A[ i * k + l] * B[ l * n + j];
}
}
void print_matrix( const char * name, const double * M, int m, int n) {
printf( " %s ( %d x %d ): \n " , name, m, n);
for ( int i = 0 ; i < m; i++) {
for ( int j = 0 ; j < n; j++)
printf( " %8.2f " , M[ i * n + j]);
printf( " \n " );
}
}
int main() {
// A: 2×3, B: 3×2 → C: 2×2
double A[] = { 1 , 2 , 3 ,
4 , 5 , 6 };
double B[] = { 7 , 8 ,
9 , 10 ,
11 , 12 };
double C[ 4 ];
mat_mul( C, A, B, 2 , 3 , 2 );
print_matrix( "A" , A, 2 , 3 );
print_matrix( "B" , B, 3 , 2 );
print_matrix( "C = A*B" , C, 2 , 2 );
// 验证 AB != BA (即使维度允许)
double D[ 9 ]; // B(3×2) * A(2×3) → 3×3
mat_mul( D, B, A, 3 , 2 , 3 );
print_matrix( "B*A (≠ A*B!)" , D, 3 , 3 );
return 0 ;
}
A (2x3):
1.00 2.00 3.00
4.00 5.00 6.00
B (3x2):
7.00 8.00
9.00 10.00
11.00 12.00
C = A*B (2x2):
58.00 64.00
139.00 154.00
B*A (≠ A*B!) (3x3):
39.00 54.00 69.00
49.00 68.00 87.00
59.00 82.00 105.00
转置与对称性
转置 Transpose
\[
(A^T)_{ij} = A_{ji}
\]
行变列,列变行。
关键性质 :
\[
(AB)^T = B^T A^T \quad \text{(翻转顺序!)}
\]
对称矩阵
\[
A = A^T \iff a_{ij} = a_{ji}
\]
协方差矩阵 \(\Sigma = X^T X\) 是对称的
对称矩阵的特征值都是实数
为什么 \(\Sigma = X^T X\) 对称? 因为 \((X^T X)^T = X^T (X^T)^T = X^T X\) ,自己等于自己的转置。
代码表示:转置与对称性检验
练习 :实现 transpose(B, A, m, n) 和 is_symmetric(A, n)。构造数据矩阵 \(X_{4\times 3}\) (4 个样本,3 个特征),计算协方差矩阵 \(\Sigma = X^T X\) ,验证其为对称矩阵。
#include <stdio.h>
// 转置: B = A^T
void transpose( double * B, const double * A, int m, int n) {
for ( int i = 0 ; i < m; i++)
for ( int j = 0 ; j < n; j++)
B[ j * m + i] = A[ i * n + j];
}
// 矩阵乘法: C = A * B
void mat_mul( double * C, const double * A, const double * B, int m, int k, int n) {
for ( int i = 0 ; i < m; i++)
for ( int j = 0 ; j < n; j++) {
C[ i * n + j] = 0.0 ;
for ( int l = 0 ; l < k; l++)
C[ i * n + j] += A[ i * k + l] * B[ l * n + j];
}
}
// 检查是否对称
int is_symmetric( const double * A, int n) {
for ( int i = 0 ; i < n; i++)
for ( int j = i + 1 ; j < n; j++)
if ( A[ i * n + j] != A[ j * n + i])
return 0 ;
return 1 ;
}
void print_matrix( const char * name, const double * M, int m, int n) {
printf( " %s ( %d x %d ): \n " , name, m, n);
for ( int i = 0 ; i < m; i++) {
for ( int j = 0 ; j < n; j++)
printf( " %8.2f " , M[ i * n + j]);
printf( " \n " );
}
}
int main() {
// 数据矩阵 X: 4个样本 × 3个特征
double X[] = { 1.0 , 2.0 , 1.0 ,
2.0 , 3.0 , 1.0 ,
3.0 , 4.0 , 2.0 ,
4.0 , 5.0 , 2.0 };
double Xt[ 12 ]; // 3×4
transpose( Xt, X, 4 , 3 );
// 协方差矩阵 Σ = X^T X (3×4 × 4×3 → 3×3)
double Sigma[ 9 ];
mat_mul( Sigma, Xt, X, 3 , 4 , 3 );
print_matrix( "X" , X, 4 , 3 );
printf( " \n " );
print_matrix( "Xt" , X, 3 , 4 );
printf( " \n " );
print_matrix( "Sigma = X^T X" , Sigma, 3 , 3 );
printf( " \n " );
// 验证: (X^T X)^T = X^T X
printf( "Symmetric? %s\n " , is_symmetric( Sigma, 3 ) ? "Yes" : "No" );
return 0 ;
}
X (4x3):
1.00 2.00 1.00
2.00 3.00 1.00
3.00 4.00 2.00
4.00 5.00 2.00
Xt (3x4):
1.00 2.00 1.00 2.00
3.00 1.00 3.00 4.00
2.00 4.00 5.00 2.00
Sigma = X^T X (3x3):
30.00 40.00 17.00
40.00 54.00 23.00
17.00 23.00 10.00
Symmetric? Yes
逆矩阵
\[
A^{-1}A = AA^{-1} = I
\]
几何意义 :\(A^{-1}\) 是 \(A\) 的逆变换——如果 \(A\) 旋转了 \(30°\) ,\(A^{-1}\) 就旋转 \(-30°\) 。
什么时候可逆?
\[
A \text{ 可逆} \iff \text{rank}(A) = n \iff \det(A) \neq 0 \iff \ker(A) = \{\mathbf{0}\}
\]
只有变换没有”丢失信息”(没有把空间压扁),才能逆转。
关键性质
\[
(AB)^{-1} = B^{-1} A^{-1}, \quad (A^T)^{-1} = (A^{-1})^T
\]
代码表示:2×2 矩阵求逆
练习 :实现 \(2\times 2\) 矩阵求逆 inverse2x2。验证 \(AA^{-1} = I\) 。测试 \(\det = 0\) 的不可逆情况。
#include <stdio.h>
// 2×2 矩阵求逆
// det = ad - bc
// A^-1 = (1/det) * [[d, -b], [-c, a]]
int inverse2x2( double * inv, const double * A) {
double det = A[ 0 ] * A[ 3 ] - A[ 1 ] * A[ 2 ];
if ( det == 0.0 ) return 0 ; // 不可逆
double inv_det = 1.0 / det;
inv[ 0 ] = A[ 3 ] * inv_det;
inv[ 1 ] = - A[ 1 ] * inv_det;
inv[ 2 ] = - A[ 2 ] * inv_det;
inv[ 3 ] = A[ 0 ] * inv_det;
return 1 ;
}
int main() {
double A[] = { 1 , 2 ,
3 , 4 };
double inv[ 4 ];
printf( "A = [[1,2],[3,4]] \n " );
if ( inverse2x2( inv, A)) {
printf( "A^-1 = [[ %.4f , %.4f ], \n " , inv[ 0 ], inv[ 1 ]);
printf( " [ %.4f , %.4f ]] \n " , inv[ 2 ], inv[ 3 ]);
// 验证 A * A^-1 = I
double I00 = A[ 0 ]* inv[ 0 ] + A[ 1 ]* inv[ 2 ];
double I01 = A[ 0 ]* inv[ 1 ] + A[ 1 ]* inv[ 3 ];
double I10 = A[ 2 ]* inv[ 0 ] + A[ 3 ]* inv[ 2 ];
double I11 = A[ 2 ]* inv[ 1 ] + A[ 3 ]* inv[ 3 ];
printf( " \n A * A^-1 = [[ %.4f , %.4f ], \n " , I00, I01);
printf( " [ %.4f , %.4f ]] (≈ I) \n " , I10, I11);
}
// 不可逆的情况
double B[] = { 1 , 2 ,
2 , 4 }; // det = 0
printf( " \n B = [[1,2],[2,4]], det = %.1f\n " , B[ 0 ]* B[ 3 ] - B[ 1 ]* B[ 2 ]);
printf( "Invertible? %s\n " , inverse2x2( inv, B) ? "Yes" : "No (det=0)" );
return 0 ;
}
A = [[1,2],[3,4]]
A^-1 = [[-2.0000, 1.0000],
[1.5000, -0.5000]]
A * A^-1 = [[1.0000, 0.0000],
[0.0000, 1.0000]] (≈ I)
B = [[1,2],[2,4]], det = 0.0
Invertible? No (det=0)
向量空间(线性代数的真正地基)
前面我们把向量看成”一列数”、矩阵看成”一张数表”。但线性代数的真正主角不是数表,而是一个抽象的向量空间(vector space) ——前面的 \(\mathbb{R}^n\) 只是它最常见的实例。
公理化定义
定义 :设 \(V\) 是非空集合,\(\mathbb{R}\) 是实数域。若 \(V\) 上定义了向量加法 \(\mathbf{x}+\mathbf{y}\in V\) 与标量乘法 \(\alpha\mathbf{x}\in V\) ,且对任意 \(\mathbf{x},\mathbf{y},\mathbf{z}\in V\) 、\(\alpha,\beta\in\mathbb{R}\) 满足以下 8 条公理,则称 \(V\) 是 \(\mathbb{R}\) 上的向量空间 ,其元素称为向量 。
加法 4 条
1
交换律
\(\mathbf{x}+\mathbf{y}=\mathbf{y}+\mathbf{x}\)
2
结合律
\((\mathbf{x}+\mathbf{y})+\mathbf{z}=\mathbf{x}+(\mathbf{y}+\mathbf{z})\)
3
零元
存在 \(\mathbf{0}\in V\) ,使 \(\mathbf{x}+\mathbf{0}=\mathbf{x}\)
4
负元
存在 \(-\mathbf{x}\in V\) ,使 \(\mathbf{x}+(-\mathbf{x})=\mathbf{0}\)
标量乘 4 条
5
标量对向量加法分配
\(\alpha(\mathbf{x}+\mathbf{y})=\alpha\mathbf{x}+\alpha\mathbf{y}\)
6
向量对标量加法分配
\((\alpha+\beta)\mathbf{x}=\alpha\mathbf{x}+\beta\mathbf{x}\)
7
标量乘结合
\(\alpha(\beta\mathbf{x})=(\alpha\beta)\mathbf{x}\)
8
单位元
\(1\mathbf{x}=\mathbf{x}\)
为什么如此抽象? 因为”向量”远不止数列:多项式、函数、矩阵、概率分布都可以是向量,只要满足这 8 条。抽象出向量空间,一个定理就同时适用于 \(\mathbb{R}^n\) 、多项式空间、函数空间……这正是数学系强调公理化的用意。\(\mathbb{R}^n\) 按分量逐项加法与标量乘显然满足 8 条,所以前面所有讨论都合法地”活在”向量空间里。
子空间 Subspace
向量空间 \(V\) 的一个子空间 \(W\subseteq V\) ,是 \(V\) 中对加法和标量乘都封闭的非空子集——它自己也是向量空间。
判定 :\(W\) 是子空间 \(\iff\) \(\mathbf{0}\in W\) ,且对任意 \(\mathbf{x},\mathbf{y}\in W\) 、\(\alpha\in\mathbb{R}\) 有 \(\mathbf{x}+\mathbf{y}\in W\) 且 \(\alpha\mathbf{x}\in W\) 。
\(\{\mathbf{0}\}\)
原点(零维)
过原点的直线 \(L=\{t\mathbf{v}\}\)
一维
过原点的平面 \(\Pi\)
二维
\(\mathbb{R}^3\) 本身
全空间
“过原点”是关键 :不过原点的平面(如 \(z=1\) )对加法不封闭(\((0,0,1)+(0,0,1)=(0,0,2)\notin\{z=1\}\) ),不是 子空间。
伏笔 :后面”线性变换”节的核 \(\ker(A)\) 与像 \(\operatorname{Im}(A)\) 都将是子空间——子空间正是理解它们的工具。
线性组合、线性无关、基
线性组合
给定一组向量 \(\mathbf{v}_1, \mathbf{v}_2, \ldots, \mathbf{v}_k\) 和任意标量 \(\alpha_1, \ldots, \alpha_k\) ,把它们加权相加得到的
\[
\alpha_1 \mathbf{v}_1 + \alpha_2 \mathbf{v}_2 + \cdots + \alpha_k \mathbf{v}_k
\]
就称为这组向量的一个线性组合 ,\(\alpha_i\) 称为系数 。
直觉 :把一组向量当作“原料”,通过缩放和相加,能“够到”哪些点?
线性表示 。如果某个向量 \(\mathbf{b}\) 恰好能写成上面这种形式,即存在一组系数使得
\[
\mathbf{b} = \alpha_1 \mathbf{v}_1 + \alpha_2 \mathbf{v}_2 + \cdots + \alpha_k \mathbf{v}_k
\]
就称 \(\mathbf{b}\) 可由 向量组 \(\{\mathbf{v}_1, \ldots, \mathbf{v}_k\}\) 线性表示 。
矩阵视角 :把向量组按列拼成矩阵 \(V = [\mathbf{v}_1\ \mathbf{v}_2\ \cdots\ \mathbf{v}_k]\) ,系数写成向量 \(\boldsymbol{\alpha}\) ,线性表示就变成矩阵–向量乘法
\[
\mathbf{b} = V\,\boldsymbol{\alpha} =
\begin{bmatrix} \mathbf{v}_1 & \mathbf{v}_2 & \cdots & \mathbf{v}_k \end{bmatrix}
\begin{bmatrix} \alpha_1 \\ \alpha_2 \\ \vdots \\ \alpha_k \end{bmatrix}
= \alpha_1 \mathbf{v}_1 + \cdots + \alpha_k \mathbf{v}_k
\]
这正是前面“三种视角”里的列视角 :\(V\boldsymbol{\alpha}\) 就是把 \(V\) 的各列按系数加权后相加。于是“\(\mathbf{b}\) 能否被线性表示”等价于问:方程 \(V\boldsymbol{\alpha} = \mathbf{b}\) 是否有解。
线性张成的空间 Span
固定一组向量,让系数取遍所有实数,所有可能的线性组合构成一个集合,称为这组向量的张成空间 :
\[
\operatorname{span}(\mathbf{v}_1, \ldots, \mathbf{v}_k) = \{\alpha_1 \mathbf{v}_1 + \cdots + \alpha_k \mathbf{v}_k \mid \alpha_i \in \mathbb{R}\}
\]
一个向量的 span → 一条过原点的直线;
两个不共线向量的 span → 一个过原点的平面。
于是:“\(\mathbf{b}\) 能被 \(\{\mathbf{v}_1,\ldots,\mathbf{v}_k\}\) 线性表示” \(\iff\) “\(\mathbf{b}\) 落在它们的 span 里”。
线性相关与线性无关
线性相关 :如果存在不全为零 的系数 \(\alpha_1, \ldots, \alpha_k\) ,使得
\[
\alpha_1 \mathbf{v}_1 + \alpha_2 \mathbf{v}_2 + \cdots + \alpha_k \mathbf{v}_k = \mathbf{0}
\]
就称向量组 \(\{\mathbf{v}_1, \ldots, \mathbf{v}_k\}\) 线性相关 。反过来,若上式只有 当 \(\alpha_1 = \cdots = \alpha_k = 0\) 时才成立,则称该组线性无关 。
矩阵视角 :写成 \(V\boldsymbol{\alpha} = \mathbf{0}\) ,这是一个齐次方程组
\[
\begin{bmatrix} \mathbf{v}_1 & \mathbf{v}_2 & \cdots & \mathbf{v}_k \end{bmatrix}
\begin{bmatrix} \alpha_1 \\ \alpha_2 \\ \vdots \\ \alpha_k \end{bmatrix}
= \mathbf{0} \tag{1.1}
\]
线性相关 \(\iff\) 齐次方程组 \((1.1)\) 存在非零解 (\(\boldsymbol{\alpha} \neq \mathbf{0}\) );
线性无关 \(\iff\) 齐次方程组 \((1.1)\) 只有零解 。
为什么要区分这两者? 线性相关意味着组内有冗余 :必有某个向量能被其余向量线性表示,可以“化”掉。例如 \(\mathbf{v}_3 = 2\mathbf{v}_1 - \mathbf{v}_2\) 时,\(\{\mathbf{v}_1, \mathbf{v}_2, \mathbf{v}_3\}\) 必线性相关。反过来也成立:若 \(\mathbf{b}\) 能由 \(\{\mathbf{v}_1, \ldots, \mathbf{v}_k\}\) 线性表示,则把 \(\mathbf{b}\) 加进该组后,新组必定线性相关。
而线性无关意味着组内每个向量都不可替代 ——这正是“基”要求“没有冗余”的原因。
基 Basis
把上面两件事合起来,就得到基 。向量空间的一组基 同时满足:
线性无关 (没有冗余);
张成整个空间 (够得着所有点)。
\(\mathbb{R}^n\) 最熟悉的一组基是标准基 :
\[
\mathbf{e}_1 = \begin{bmatrix}1\\0\\0\\ \vdots\end{bmatrix},\quad \ldots,\quad \mathbf{e}_n = \begin{bmatrix}0\\0\\ \vdots\\1\end{bmatrix}
\]
但基不唯一 :任意 \(n\) 个线性无关的 \(n\) 维向量都构成 \(\mathbb{R}^n\) 的一组基。同一个向量,换一组基,坐标就不同——这正是后面“坐标变换”的出发点。
预告 :后续会遇到“基”的两个重要特例——“特征值”节里,某些矩阵有一组由特征向量 构成的基;“SVD”节里,任何矩阵都关联到一组正交基 (奇异向量)。
代码练习
实现 linear_comb2(y, a1, v1, a2, v2, n):用标准基 \(\{e_1, e_2\}\) 和非标准基 \(\{[1,1],[1,-1]\}\) 分别组合出 \([3, 4]\) ,并构造一组线性相关的向量加以验证。
#include <stdio.h>
// 线性组合: y = a1*v1 + a2*v2
void linear_comb2( double * y, double a1, const double * v1,
double a2, const double * v2, int n) {
for ( int i = 0 ; i < n; i++)
y[ i] = a1 * v1[ i] + a2 * v2[ i];
}
int main() {
// R^2 中的两个向量
double v1[] = { 1.0 , 0.0 }; // e1
double v2[] = { 0.0 , 1.0 }; // e2
double y[ 2 ];
// y = 3*e1 + 4*e2 = [3, 4]
linear_comb2( y, 3.0 , v1, 4.0 , v2, 2 );
printf( "3*e1 + 4*e2 = [ %.0f , %.0f ] \n " , y[ 0 ], y[ 1 ]);
// 用非标准基: v1=[1,1], v2=[1,-1]
double w1[] = { 1.0 , 1.0 };
double w2[] = { 1.0 , - 1.0 };
linear_comb2( y, 3.5 , w1, - 0.5 , w2, 2 );
printf( "3.5*[1,1] + (-0.5)*[1,-1] = [ %.1f , %.1f ] \n " , y[ 0 ], y[ 1 ]);
// 检验线性相关: v3 = 2*v1 - v2 → {v1,v2,v3} 线性相关
double v3[] = { 2.0 , - 1.0 }; // = 2*[1,0] - [0,1]
linear_comb2( y, 2.0 , v1, - 1.0 , v2, 2 );
printf( "2*v1 - v2 = [ %.0f , %.0f ] = v3 \n " , y[ 0 ], y[ 1 ]);
printf( "→ v3 可由 v1,v2 线性表出,{v1,v2,v3} 线性相关 \n " );
return 0 ;
}
线性变换
矩阵不是数表,矩阵是函数(变换)。
\[
T: \mathbb{R}^n \to \mathbb{R}^m, \quad T(\mathbf{x}) = A\mathbf{x}
\]
2D 变换
旋转 :\(R_\theta = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}\)
拉伸 :\(S = \begin{bmatrix} s_x & 0 \\ 0 & s_y \end{bmatrix}\)
剪切 :\(H = \begin{bmatrix} 1 & k \\ 0 & 1 \end{bmatrix}\)
投影 (到 x 轴):\(P = \begin{bmatrix} 1 & 0 \\ 0 & 0 \end{bmatrix}\)
任何线性变换都可以分解为:旋转 → 拉伸 → 旋转(这就是 SVD!)
核与像
核/零空间
\(\ker(A) = \{\mathbf{x} \mid A\mathbf{x} = \mathbf{0}\}\)
被变换”消灭”的输入
像/列空间
\(\text{Im}(A) = \{A\mathbf{x} \mid \mathbf{x} \in \mathbb{R}^n\}\)
变换能”够到”的输出
核与像的维度关系——秩-零度定理 \(\dim(\ker A)+\text{rank}(A)=n\) ——见下一节”秩”。
代码表示:2D 线性变换
练习 :对 \(\mathbf{x} = [1, 0]^T\) 依次施加:旋转 \(90°\) 、拉伸 \([2x, 3y]\) 、投影到 x 轴。再对 \([1,0]\) 做剪切变换。
秩 Rank
\[
\text{rank}(A) = \text{列向量中线性无关的最大个数} = \text{行向量中线性无关的最大个数}
\]
\(\text{rank}(A) = \min(m,n)\)
满秩 ,信息最丰富
\(\text{rank}(A) \lt \min(m,n)\)
低秩 ,存在冗余/依赖
\(\text{rank}(A) = 1\)
秩1矩阵 ,可写为外积 \(A = \mathbf{u}\mathbf{v}^T\)
\(\text{rank}(A) = r\) 意味着 \(A\mathbf{x}\) 的所有输出都挤在一个 \(r\) 维子空间里(即像 \(\text{Im}(A)\) 的维度 \(=r\) )。
秩-零度定理(Rank-Nullity)
线性变换 \(A: \mathbb{R}^n \to \mathbb{R}^m\) 把输入空间拆成互不重叠的两部分——被”消灭”的核,与”够得着”的像:
\[
\dim(\ker A) \;+\; \text{rank}(A) \;=\; n
\]
\(\dim(\ker A)\) (零度 nullity)
输入中被压成 \(\mathbf{0}\) 的方向数
\(\text{rank}(A)=\dim(\text{Im}\,A)\)
输出空间的维度
两者之和 \(=n\)
输入维度守恒:要么被消灭,要么被保留到输出
这就是把上一节”核与像”串起来的核心等式:\(A\) 可逆 \(\iff \ker A=\{\mathbf{0}\}\iff \text{rank}(A)=n\) (见”逆矩阵”节)。
在深度学习中的角色 :LoRA 用低秩矩阵 \(AB\) (\(r \ll m,n\) )近似大权重矩阵。
代码表示:秩的计算(高斯消元法)
练习 :通过高斯消元法实现 rank(A, m, n),计算满秩矩阵、低秩矩阵(\(\text{rank}=1\) )和外积矩阵 \(\mathbf{u}\mathbf{v}^T\) 的秩。
#include <stdio.h>
#include <math.h>
// 通过高斯消元法(行化简)计算矩阵的秩
// 对 A 的副本操作(不修改原矩阵)
int rank( double * A, int m, int n) {
// 复制矩阵到临时数组
double tmp[ 64 ];
for ( int i = 0 ; i < m * n; i++) tmp[ i] = A[ i];
int r = 0 ;
for ( int col = 0 ; col < n && r < m; col++) {
// 找主元
int pivot = - 1 ;
for ( int row = r; row < m; row++) {
if ( fabs( tmp[ row * n + col]) > 1e-10 ) {
pivot = row;
break ;
}
}
if ( pivot < 0 ) continue ;
// 交换行
if ( pivot != r) {
for ( int j = 0 ; j < n; j++) {
double t = tmp[ r * n + j];
tmp[ r * n + j] = tmp[ pivot * n + j];
tmp[ pivot * n + j] = t;
}
}
// 消元
for ( int row = r + 1 ; row < m; row++) {
double factor = tmp[ row * n + col] / tmp[ r * n + col];
for ( int j = col; j < n; j++)
tmp[ row * n + j] -= factor * tmp[ r * n + j];
}
r++;
}
return r;
}
void print_matrix( const char * name, const double * M, int m, int n) {
printf( " %s ( %d x %d ): \n " , name, m, n);
for ( int i = 0 ; i < m; i++) {
for ( int j = 0 ; j < n; j++)
printf( " %8.2f " , M[ i * n + j]);
printf( " \n " );
}
}
int main() {
// 满秩矩阵 (rank = 2)
double A[] = { 1 , 2 ,
3 , 4 };
print_matrix( "A" , A, 2 , 2 );
printf( "rank(A) = %d\n\n " , rank( A, 2 , 2 ));
// 低秩矩阵 (rank = 1): 第2行 = 2 * 第1行
double B[] = { 1 , 2 ,
2 , 4 };
print_matrix( "B" , B, 2 , 2 );
printf( "rank(B) = %d (row2 = 2*row1) \n\n " , rank( B, 2 , 2 ));
// 秩1矩阵 = 外积 uv^T
double u[] = { 1 , 2 , 3 };
double v[] = { 2 , 1 };
double C[ 6 ]; // 3×2
for ( int i = 0 ; i < 3 ; i++)
for ( int j = 0 ; j < 2 ; j++)
C[ i * 2 + j] = u[ i] * v[ j];
print_matrix( "C = u*v^T (rank-1)" , C, 3 , 2 );
printf( "rank(C) = %d\n " , rank( C, 3 , 2 ));
return 0 ;
}
A (2x2):
1.00 2.00
3.00 4.00
rank(A) = 2
B (2x2):
1.00 2.00
2.00 4.00
rank(B) = 1 (row2 = 2*row1)
C = u*v^T (rank-1) (3x2):
2.00 1.00
4.00 2.00
6.00 3.00
rank(C) = 1
行列式
2×2 的显式公式
\[
\det\begin{bmatrix} a & b \\ c & d \end{bmatrix} = ad - bc
\]
几何意义
\[
|\det(A)| = \text{变换后单位立方体的体积}
\]
\(\gt 0\)
体积放大,方向保持
\(\lt 0\)
体积放大,方向翻转
\(= 0\)
体积坍缩为零 → 空间被压扁 → 不可逆
\[
\det(AB) = \det(A) \cdot \det(B)
\]
代码表示:行列式计算与性质
练习 :实现 det2x2。计算旋转、拉伸、投影矩阵的行列式。验证 \(\det(AB) = \det(A) \cdot \det(B)\) 。
#include <stdio.h>
#include <math.h>
double det2x2( const double * A) {
return A[ 0 ] * A[ 3 ] - A[ 1 ] * A[ 2 ];
}
int main() {
// 旋转 45° → det = 1(保体积)
double theta = 3.14159265358979323846 / 4 ;
double R[] = { cos( theta), - sin( theta),
sin( theta), cos( theta)};
printf( "Rotation 45°: det = %.6f (≈1, volume preserved) \n " , det2x2( R));
// 拉伸 → det = 面积缩放
double S[] = { 2.0 , 0.0 , 0.0 , 3.0 };
printf( "Scale [2x,3y]: det = %.1f (area ×6) \n " , det2x2( S));
// 投影 → det = 0(面积坍缩)
double P[] = { 1.0 , 0.0 , 0.0 , 0.0 };
printf( "Project x: det = %.1f (collapsed) \n " , det2x2( P));
// det(AB) = det(A)*det(B)
double A[] = { 1 , 2 , 3 , 4 };
double B[] = { 5 , 6 , 7 , 8 };
double AB[] = {
A[ 0 ]* B[ 0 ]+ A[ 1 ]* B[ 2 ], A[ 0 ]* B[ 1 ]+ A[ 1 ]* B[ 3 ],
A[ 2 ]* B[ 0 ]+ A[ 3 ]* B[ 2 ], A[ 2 ]* B[ 1 ]+ A[ 3 ]* B[ 3 ]
};
printf( " \n det(A)= %.1f , det(B)= %.1f , det(A)*det(B)= %.1f\n " ,
det2x2( A), det2x2( B), det2x2( A)* det2x2( B));
printf( "det(AB)= %.1f → equal! \n " , det2x2( AB));
return 0 ;
}
范数 (Norm)
词源与历史:从”木工直角尺”到”长度”
“norm” 的来历。 英文 norm ← 法文 norme ← 拉丁文 norma ,原义是木工的直角尺(carpenter’s square) ,引申为”规则、模型、标准”。一把直角尺的作用,就是给出一个衡量”够不够直、够不够方”的标准 ——这正是”norm”在数学中的灵魂:一把用来度量某数学量”有多大”的标尺 。
中文”范数”的”范”。 古汉语里”范(範)“意为模型、模子、典范、法则、度量器具。所以”范数”可直译为”用于度量某个数学量的模型/标尺 ”——与拉丁词源惊人地呼应。
同一个词,数学里至少有三种含义 (前两种最常被混淆):
① 代数/数论范数
Gauss (1832),研究高斯整数 \(a+bi\)
\(N(a+bi)=a^2+b^2=\lvert a+bi\rvert^2\)
是长度的平方 ;满足乘性 \(N(zw)=N(z)N(w)\) ;不要求三角不等式
② 分析/向量范数
Banach (1922),抽象线性空间
\(\lVert x\rVert\) ,见下方三公理
要求三角不等式 ——本节主角
③ 黎曼积分中分割的范数
H. J. S. Smith (1875)
\(\lVert P\rVert=\max_i(x_i-x_{i-1})\)
指分割的”细度/网格”(mesh),与向量范数无关 ,只是同名
关键区分 :① 高斯”代数范数”是 \(|z|^2\) (数的平方、乘性),② 分析”范数”才是”长度”(带三角不等式)。我们说深度学习里的 \(\lVert\cdot\rVert\) ,一律指 ②。
向量范数的双竖线记号。 Erhard Schmidt 在 1908 年论文里,把一个无穷维空间中的向量记为 \(A(x)\) ,定义其长度为一个正量 \(\lVert A\rVert\) (欧氏范数),并称长度为 1 的向量为 normirt (“已规范化”,normalized)。双竖线 \(\lVert\cdot\rVert\) 大概率是为了与标量的绝对值 \(\lvert\cdot\rvert\) 区分开 ——一个量标量、一个量向量;不致混淆时也常简写。
矩阵范数。 Householder (1964) 曾指出,范数概念是泛函分析的基础,但直到约 1940 年才频繁出现在数值分析与矩阵理论中(Hotelling 1943、Bowker 1947);Wedderburn (1934) 则把这一概念上溯到 1887 年。
直觉:范数 = 向量的”长度/大小”
范数 \(\lVert\cdot\rVert\) 把任意向量 \(\mathbf{x}\) 映射为一个非负实数 \(\lVert\mathbf{x}\rVert \in \mathbb{R}_{\ge 0}\) ,回答的唯一问题是”这个向量有多长?“——就像用尺子量一段带方向的箭头:
\[
\lVert\cdot\rVert : V \longrightarrow \mathbb{R}_{\ge 0}
\]
但并非任意一个”把向量变成数”的函数都叫范数。下面三条公理才是范数的严格定义 。
官方定义(公理化)
定义 :设 \(V\) 是实数域 \(\mathbb{R}\) 上的向量空间,\(\mathbf{0}\) 为零向量。函数 \(\lVert\cdot\rVert : V \to \mathbb{R}_{\ge 0}\) 称为 \(V\) 上的范数(norm) ,当且仅当对任意 \(\mathbf{x}, \mathbf{y} \in V\) 与任意标量 \(\alpha \in \mathbb{R}\) ,以下三条公理同时成立:
1
正定性 positive definiteness
\(\lVert\mathbf{x}\rVert \ge 0\) ,且 \(\lVert\mathbf{x}\rVert=0 \iff \mathbf{x}=\mathbf{0}\)
长度非负;只有零向量长度为 0
2
绝对齐次性 absolute homogeneity
\(\lVert\alpha\mathbf{x}\rVert = \lvert\alpha\rvert\,\lVert\mathbf{x}\rVert\)
向量拉伸 \(k\) 倍,长度也变 \(k\) 倍
3
次可加性 / 三角不等式 subadditivity
\(\lVert\mathbf{x}+\mathbf{y}\rVert \le \lVert\mathbf{x}\rVert+\lVert\mathbf{y}\rVert\)
两边之和 \(\ge\) 第三边
三条同时满足,\(\lVert\cdot\rVert\) 才有资格叫”范数”;装备范数的空间 \((V, \lVert\cdot\rVert)\) 称为赋范向量空间(normed vector space) 。
严谨性说明
① 非负性 \(\lVert\mathbf{x}\rVert\ge 0\) 其实可由公理 2、3 推出。 取 \(\alpha=-1\) ,由齐次性 \(\lVert-\mathbf{x}\rVert=\lVert\mathbf{x}\rVert\) ,且 \(\lVert\mathbf{0}\rVert=\lVert 0\cdot\mathbf{x}\rVert=0\) ;对 \(\mathbf{0}=\mathbf{x}+(-\mathbf{x})\) 用三角不等式:
\[
0=\lVert\mathbf{0}\rVert=\lVert\mathbf{x}+(-\mathbf{x})\rVert\le \lVert\mathbf{x}\rVert+\lVert-\mathbf{x}\rVert=2\lVert\mathbf{x}\rVert
\;\Longrightarrow\;
\lVert\mathbf{x}\rVert\ge 0
\]
② 但 “\(\lVert\mathbf{x}\rVert=0 \Rightarrow \mathbf{x}=\mathbf{0}\) ”(确定性)推不出,必须独立要求。 放宽它,就退化成半范数(seminorm) ——允许某个非零向量长度也为 0。
半范数例 :\(s(\mathbf{x})=|x_1|\) (只看第一个分量)。对 \(\mathbf{x}=[0,5]^T\) 有 \(s(\mathbf{x})=0\) 但 \(\mathbf{x}\ne\mathbf{0}\) → \(s\) 不是范数,只是半范数。
③ 反向三角不等式 (由公理 2、3 推出):
\[
\big|\,\lVert\mathbf{x}\rVert-\lVert\mathbf{y}\rVert\,\big| \le \lVert\mathbf{x}-\mathbf{y}\rVert
\]
刻画”两向量长度之差,不超过它们自身的距离”。
反例:为什么这三条是底线
取 \(\mathbf{x}=[x_1,x_2]^T\) ,令 \(f(\mathbf{x})=\sqrt{|x_1|}\) 。它满足非负,但不满足齐次性 :
\[
f(2\mathbf{x})=\sqrt{|2x_1|}=\sqrt{2}\,\sqrt{|x_1|}\;\ne\;2\sqrt{|x_1|}=2f(\mathbf{x})
\]
向量拉长 2 倍,“长度”却只增 \(\sqrt{2}\) 倍 → \(f\) 不是范数。
范数诱导距离(度量)
范数天然给出两点间的”距离”:
\[
d(\mathbf{x},\mathbf{y}) = \lVert\mathbf{x}-\mathbf{y}\rVert
\]
它满足距离三公理(非负、对称、三角不等式),所以每个赋范空间自动是度量空间 ——这正是范数既能当”长度”又能当”距离”用的根源。
最常见的一族:Lp 范数
\[
\lVert\mathbf{x}\rVert_p = \left( \sum_{i=1}^n |x_i|^p \right)^{1/p}, \qquad p \ge 1
\]
不同的 \(p\) = 不同的”距离观”:
\(p=1\)
L1 范数
\(\sum \vert x_i \vert\)
曼哈顿距离
\(p=2\)
L2 范数(欧几里得)
\(\sqrt{\sum x_i^2}\)
直线距离
\(p=\infty\)
L∞ 范数
\(\max_i \vert x_i \vert\)
最大分量
单位”范数球” \(\{\mathbf{x} : \lVert\mathbf{x}\rVert_p \le 1\}\) 的形状直观区分三者:L1 是菱形 ◇,L2 是圆 ⬭,L∞ 是方形 ▢ 。
注意:\(0 < p < 1\) 时 \(\lVert\cdot\rVert_p\) 不满足三角不等式 ,严格来说不是范数(称为 quasi-norm)。
为什么必须 \(p\ge 1\) ? 公理 3(三角不等式)此时正是 Minkowski 不等式 :
\[
\lVert\mathbf{x}+\mathbf{y}\rVert_p \le \lVert\mathbf{x}\rVert_p + \lVert\mathbf{y}\rVert_p
\]
它对 \(p\ge 1\) 成立,\(0<p<1\) 时反向。Lp 范数”是不是范数”,就卡在这一条上。
L2 范数简写:
\[
\lVert\mathbf{x}\rVert = \lVert\mathbf{x}\rVert_2 = \sqrt{\mathbf{x}^T \mathbf{x}}
\]
有限维空间里,所有范数都”等价”
在有限维 空间 \(\mathbb{R}^n\) 上,任意两个范数 \(\lVert\cdot\rVert_a,\lVert\cdot\rVert_b\) 都等价 :存在常数 \(0<c\le C\) 使得
\[
c\,\lVert\mathbf{x}\rVert_a \le \lVert\mathbf{x}\rVert_b \le C\,\lVert\mathbf{x}\rVert_a,\qquad \forall \mathbf{x}\in\mathbb{R}^n
\]
例如把 L∞ 与 L2 互相夹住:\(\lVert\mathbf{x}\rVert_\infty \le \lVert\mathbf{x}\rVert_2 \le \sqrt{n}\,\lVert\mathbf{x}\rVert_\infty\) 。
意义 :有限维下,收敛、连续、有界、单位球紧致 这些拓扑性质与具体选哪个范数无关 。所以算法里把 L2 换成 L1,不会改变”序列是否收敛”,但会改变优化几何(稀疏性、条件数)——这是 L1/L2 正则效果不同的根源。注意:无穷维空间里这条结论不成立 。
矩阵范数
矩阵有两种思路来定”范数”:
① 元素型(把矩阵拍平当向量)—— Frobenius 范数 :
\[
\lVert A\rVert_F = \sqrt{\sum_{i=1}^m \sum_{j=1}^n a_{ij}^2} = \sqrt{\text{tr}(A^T A)}
\]
② 算子型(把矩阵当”放大器”,由向量范数诱导)—— 诱导/算子范数 :
\[
\lVert A\rVert_p = \sup_{\mathbf{x}\ne\mathbf{0}} \frac{\lVert A\mathbf{x}\rVert_p}{\lVert\mathbf{x}\rVert_p} = \max_{\lVert\mathbf{x}\rVert_p=1}\lVert A\mathbf{x}\rVert_p
\]
它衡量”矩阵最多能把向量拉长多少倍”。常见取值:
\(\lVert A\rVert_2\) (谱范数 )
\(=\sigma_{\max}(A)\) ,最大奇异值 ← 直接连到 SVD 分解
\(\lVert A\rVert_1\)
\(=\max_j\) (第 \(j\) 列绝对值之和)
\(\lVert A\rVert_\infty\)
\(=\max_i\) (第 \(i\) 行绝对值之和)
两条关键性质 : - 次可加乘性 \(\lVert AB\rVert \le \lVert A\rVert\,\lVert B\rVert\) (诱导范数必满足;Frobenius 也满足)。 - 谱半径被范数控制 \(\rho(A)=\max_i|\lambda_i| \le \lVert A\rVert\) (对任意诱导范数)。这是幂迭代、收敛性分析的地基。
对偶范数与 Hölder 不等式
给定范数 \(\lVert\cdot\rVert\) ,它的对偶范数 定义为
\[
\lVert\mathbf{x}\rVert_* = \sup_{\mathbf{y}\ne\mathbf{0}} \frac{\mathbf{x}^T\mathbf{y}}{\lVert\mathbf{y}\rVert} = \max_{\lVert\mathbf{y}\rVert\le 1}\mathbf{x}^T\mathbf{y}
\]
Lp 的对偶是 Lq ,其中 \(\dfrac{1}{p}+\dfrac{1}{q}=1\) (\(p,q\ge 1\) ,互称共轭指数):\(\lVert\cdot\rVert_p^*=\lVert\cdot\rVert_q\) 。特别地 L2 自对偶 (\(p=q=2\) ),L1 的对偶是 L∞。
由此得 Hölder 不等式 (对偶范数的本质):
\[
\mathbf{x}^T\mathbf{y} \le \lVert\mathbf{x}\rVert\,\lVert\mathbf{y}\rVert_*
\quad\Longleftrightarrow\quad
\sum_i |x_i y_i| \le \lVert\mathbf{x}\rVert_p\,\lVert\mathbf{y}\rVert_q
\]
\(p=q=2\) 的特例就是 Cauchy–Schwarz 不等式 \(\lvert\mathbf{x}^T\mathbf{y}\rvert \le \lVert\mathbf{x}\rVert_2\lVert\mathbf{y}\rVert_2\) ——余弦相似度里”夹角”能有意义的数学保证。
在深度学习中的角色
权重衰减 / L2 正则
\(\tfrac{\lambda}{2}\lVert W\rVert_F^2\)
逼权重变小,防过拟合
L1 正则
\(\lambda\lVert W\rVert_1\)
产生稀疏 权重(很多归零,可做特征选择)
梯度裁剪
\(\lVert\nabla\rVert>\) 阈值时缩放
防梯度爆炸
余弦相似度
\(\dfrac{\mathbf{x}^T\mathbf{y}}{\lVert\mathbf{x}\rVert\lVert\mathbf{y}\rVert}\)
衡量方向对齐程度
谱归一化(GAN)
约束 \(\lVert W\rVert_2\le 1\)
限制判别器 Lipschitz 常数,稳定训练
L1 vs L2 正则的几何根源 :L1 约束的”球”是菱形 ◇,顶点落在坐标轴上 → 优化易在顶点(稀疏解) 相切;L2 约束的”球”是圆 ⬭ → 倾向均匀缩小 所有权重。这是”为什么 L1 产生稀疏、L2 不产生”的最干净解释。
代码表示:L1/L2/L∞ 范数与 Frobenius 范数
练习 :实现 norm_l1、norm_l2、norm_linf 和 frobenius_norm。计算 \(\mathbf{x} = [3, 4]\) 的三种范数和 \(A_{2\times 3}\) 的 Frobenius 范数。
#include <stdio.h>
#include <math.h>
double dot( const double * x, const double * y, int n) {
double s = 0.0 ;
for ( int i = 0 ; i < n; i++) s += x[ i] * y[ i];
return s;
}
double norm_l1( const double * x, int n) {
double s = 0.0 ;
for ( int i = 0 ; i < n; i++) s += fabs( x[ i]);
return s;
}
double norm_l2( const double * x, int n) {
return sqrt( dot( x, x, n));
}
double norm_linf( const double * x, int n) {
double mx = 0.0 ;
for ( int i = 0 ; i < n; i++)
if ( fabs( x[ i]) > mx) mx = fabs( x[ i]);
return mx;
}
double frobenius_norm( const double * A, int m, int n) {
double s = 0.0 ;
for ( int i = 0 ; i < m * n; i++) s += A[ i] * A[ i];
return sqrt( s);
}
int main() {
double x[] = { 3.0 , 4.0 };
printf( "x = [3, 4] \n " );
printf( "||x||_1 = %.2f (Manhattan) \n " , norm_l1( x, 2 ));
printf( "||x||_2 = %.2f (Euclidean) \n " , norm_l2( x, 2 ));
printf( "||x||_inf = %.2f (Max) \n " , norm_linf( x, 2 ));
double A[] = { 1 , 2 , 3 , 4 , 5 , 6 }; // 2×3
printf( " \n A = [[1,2,3],[4,5,6]] \n " );
printf( "||A||_F = %.4f\n " , frobenius_norm( A, 2 , 3 ));
return 0 ;
}
x = [3, 4]
||x||_1 = 7.00 (Manhattan)
||x||_2 = 5.00 (Euclidean)
||x||_inf = 4.00 (Max)
A = [[1,2,3],[4,5,6]]
||A||_F = 9.5394
迹 Trace
\[
\text{tr}(A) = \sum_{i=1}^n a_{ii}
\]
就是把主对角线上的元素加起来 。例:
\[
\text{tr}\begin{bmatrix}2&0\\0&3\end{bmatrix}=2+3=5,\qquad
\text{tr}\begin{bmatrix}1&2\\3&4\end{bmatrix}=1+4=5
\]
(只看对角线,非对角元素完全不参与。)
它代表什么? 和行列式一样,迹也是方阵的一个”标量不变量”——把整个矩阵压缩成一个数,衡量线性变换”沿各方向总共拉伸了多少”。下一节将看到,迹等于特征值之和 (\(\text{tr}(A)=\sum_i\lambda_i\) ),而行列式是特征值之积 :迹管”线性总量”,行列式管”乘性总量”(体积缩放)。这也是上一节 Frobenius 范数 \(\|A\|_F^2=\text{tr}(A^TA)\) (矩阵”总能量”)背后的同一件事。
关键性质
\[
\text{tr}(AB) = \text{tr}(BA) \quad \text{(循环性质:尽管 } AB\ne BA \text{,两者的迹却相等)}
\]
代码表示:迹的计算与性质
练习 :实现 trace(A, n)。验证 \(\text{tr}(AB) = \text{tr}(BA)\) 与 \(\|A\|_F = \sqrt{\text{tr}(A^TA)}\) 。
#include <stdio.h>
#include <math.h>
double trace( const double * A, int n) {
double s = 0.0 ;
for ( int i = 0 ; i < n; i++)
s += A[ i * n + i];
return s;
}
int main() {
double A[] = { 2 , 1 ,
1 , 3 };
printf( "A = [[2,1],[1,3]] \n " );
printf( "tr(A) = %.1f\n\n " , trace( A, 2 ));
// 验证循环性质: tr(AB) = tr(BA)
double B[] = { 5 , 6 , 7 , 8 };
double AB[] = { A[ 0 ]* B[ 0 ]+ A[ 1 ]* B[ 2 ], A[ 0 ]* B[ 1 ]+ A[ 1 ]* B[ 3 ],
A[ 2 ]* B[ 0 ]+ A[ 3 ]* B[ 2 ], A[ 2 ]* B[ 1 ]+ A[ 3 ]* B[ 3 ]};
double BA[] = { B[ 0 ]* A[ 0 ]+ B[ 1 ]* A[ 2 ], B[ 0 ]* A[ 1 ]+ B[ 1 ]* A[ 3 ],
B[ 2 ]* A[ 0 ]+ B[ 3 ]* A[ 2 ], B[ 2 ]* A[ 1 ]+ B[ 3 ]* A[ 3 ]};
printf( "tr(AB) = %.1f , tr(BA) = %.1f (equal even though AB ≠ BA!) \n " ,
trace( AB, 2 ), trace( BA, 2 ));
// Frobenius norm: ||A||_F^2 = tr(A^T A)
double AtA[] = { A[ 0 ]* A[ 0 ]+ A[ 2 ]* A[ 2 ], A[ 0 ]* A[ 1 ]+ A[ 2 ]* A[ 3 ],
A[ 0 ]* A[ 1 ]+ A[ 2 ]* A[ 3 ], A[ 1 ]* A[ 1 ]+ A[ 3 ]* A[ 3 ]};
double frob = sqrt( trace( AtA, 2 ));
printf( " \n ||A||_F = sqrt(tr(A^T A)) = %.4f\n " , frob);
return 0 ;
}
A = [[2,1],[1,3]]
tr(A) = 5.0
tr(AB) = 47.0, tr(BA) = 47.0 (equal even though AB ≠ BA!)
||A||_F = sqrt(tr(A^T A)) = 3.8730
特征值与特征向量
\[
A\mathbf{v} = \lambda \mathbf{v}, \quad \mathbf{v} \neq \mathbf{0}
\]
\(\mathbf{v}\)
特征向量
在变换 \(A\) 下方向不变的向量
\(\lambda\)
特征值
沿 \(\mathbf{v}\) 方向的缩放倍数
求法
\((A - \lambda I)\mathbf{v} = \mathbf{0}\) 有非零解 \(\iff \det(A - \lambda I) = 0\) (特征方程 )
特征分解
\[
A = Q \Lambda Q^{-1}, \quad Q = [\mathbf{v}_1 | \cdots | \mathbf{v}_n], \quad \Lambda = \text{diag}(\lambda_1, \ldots, \lambda_n)
\]
名词:可对角化 。若 \(A\) 有 \(n\) 个线性无关的特征向量(能拼成可逆的 \(Q\) ),就称 \(A\) 可对角化 ,即可写成 \(A=Q\Lambda Q^{-1}\) 。并非所有方阵都可对角化:例如剪切矩阵 \(\begin{bmatrix}1&1\\0&1\end{bmatrix}\) 只有一个独立特征方向,拼不出可逆的 \(Q\) 。
迹与行列式的特征值刻画
在特征基下 \(A\) 化为对角阵 \(\Lambda=\text{diag}(\lambda_1,\ldots,\lambda_n)\) ,对角线正是各 \(\lambda_i\) ——于是两个看似只是”矩阵算术”的量获得了深刻含义:
\[
\text{tr}(A) = \sum_{i=1}^n \lambda_i \quad \text{(迹 = 特征值之和)}, \qquad
\det(A) = \prod_{i=1}^n \lambda_i \quad \text{(行列式 = 特征值之积)}
\]
这印证了上一节的说法:迹虽只是对角线之和,却等于 特征值之和;行列式同理等于特征值之积。也立刻看出 \(A\) 可逆 \(\iff \det(A)\ne0\iff\) 没有 \(\lambda_i=0\) (没有方向被压成零)。
特征值的物理意义(SSM 中的关键!)
\(\dot{\mathbf{h}} = A\mathbf{h}\) 的解:\(\mathbf{h}(t) = e^{At} \mathbf{h}(0)\)
\(\lambda_i \lt 0\) (实部)
\(\to 0\)
衰减 (遗忘)
\(\lambda_i = 0\)
\(= 1\)
保持 (完美记忆)
\(\lambda_i \gt 0\) (实部)
\(\to \infty\)
爆炸 (不稳定)
HiPPO 初始化让 \(A\) 的特征值分布在负实轴上,使得 SSM 系统能维持多尺度的记忆。
代码表示:2×2 对称矩阵特征值
练习 :实现 \(2\times 2\) 对称矩阵特征值计算 eigen2x2。验证 \(\text{tr}(A) = \lambda_1 + \lambda_2\) 和 \(\det(A) = \lambda_1 \lambda_2\) 。
#include <stdio.h>
#include <math.h>
// 2×2 对称矩阵的特征值
// det(A - λI) = 0 → λ² - tr(A)λ + det(A) = 0
void eigen2x2( const double * A, double * lambda1, double * lambda2) {
double tr = A[ 0 ] + A[ 3 ]; // trace = a + d
double det = A[ 0 ]* A[ 3 ] - A[ 1 ]* A[ 2 ]; // ad - bc
double disc = sqrt( tr* tr - 4 * det);
* lambda1 = ( tr + disc) / 2.0 ;
* lambda2 = ( tr - disc) / 2.0 ;
}
int main() {
// 对称矩阵
double A[] = { 2 , 1 ,
1 , 3 };
double l1, l2;
eigen2x2( A, & l1, & l2);
printf( "A = [[2,1],[1,3]] \n " );
printf( "eigenvalues: λ1 = %.4f , λ2 = %.4f\n " , l1, l2);
// 验证: tr = λ1 + λ2, det = λ1 * λ2
double tr = A[ 0 ] + A[ 3 ];
double det = A[ 0 ]* A[ 3 ] - A[ 1 ]* A[ 2 ];
printf( "tr(A) = %.1f = λ1+λ2 = %.4f\n " , tr, l1+ l2);
printf( "det(A) = %.1f = λ1*λ2 = %.4f\n " , det, l1* l2);
// 投影矩阵: P = [[1,0],[0,0]] → λ = 1, 0
double P[] = { 1 , 0 , 0 , 0 };
eigen2x2( P, & l1, & l2);
printf( " \n Projection: λ1 = %.1f (kept), λ2 = %.1f (killed) \n " , l1, l2);
return 0 ;
}
A = [[2,1],[1,3]]
eigenvalues: λ1 = 3.6180, λ2 = 1.3820
tr(A) = 5.0 = λ1+λ2 = 5.0000
det(A) = 5.0 = λ1*λ2 = 5.0000
Projection: λ1 = 1.0 (kept), λ2 = 0.0 (killed)
正交矩阵
\[
Q^T Q = QQ^T = I \iff Q^{-1} = Q^T
\]
名词:标准正交(orthonormal) 。一组向量若两两正交 (任意两个内积为 0)且每个长度都为 1 ,称为标准正交。正交矩阵 \(Q\) 的各列(也是各行)恰好构成 \(\mathbb{R}^n\) 的一组标准正交基。
\(\|Q\mathbf{x}\| = \|\mathbf{x}\|\)
保长度(等距变换)
\((Q\mathbf{x})^T(Q\mathbf{y}) = \mathbf{x}^T\mathbf{y}\)
保内积(保角)
\(\det(Q) = \pm 1\)
保体积
\(Q^T = Q^{-1}\)
逆就是转置,计算极快
几何 :正交变换 = 旋转(\(\det = +1\) )或旋转+镜像(\(\det = -1\) ),不拉伸不压缩 。
代码表示:正交矩阵的性质验证
练习 :构造 \(60°\) 旋转矩阵 \(Q\) ,验证 \(Q^TQ = I\) 、\(\det(Q) = 1\) 、\(\|Q\mathbf{x}\| = \|\mathbf{x}\|\) 。
#include <stdio.h>
#include <math.h>
int main() {
// 旋转矩阵(正交矩阵)
double theta = 3.14159265358979323846 / 3 ; // 60°
double Q[] = { cos( theta), - sin( theta),
sin( theta), cos( theta)};
printf( "Rotation matrix Q (60°): \n " );
printf( " [[ %.4f , %.4f ], \n " , Q[ 0 ], Q[ 1 ]);
printf( " [ %.4f , %.4f ]] \n\n " , Q[ 2 ], Q[ 3 ]);
// Q^T Q = I ?
double QtQ_00 = Q[ 0 ]* Q[ 0 ]+ Q[ 2 ]* Q[ 2 ];
double QtQ_01 = Q[ 0 ]* Q[ 1 ]+ Q[ 2 ]* Q[ 3 ];
double QtQ_11 = Q[ 1 ]* Q[ 1 ]+ Q[ 3 ]* Q[ 3 ];
printf( "Q^T*Q = [[ %.4f , %.4f ], \n " , QtQ_00, QtQ_01);
printf( " [ %.4f , %.4f ]] (≈ I) \n\n " , QtQ_01, QtQ_11);
// det(Q) = 1
double det = Q[ 0 ]* Q[ 3 ] - Q[ 1 ]* Q[ 2 ];
printf( "det(Q) = %.4f\n\n " , det);
// 保长度: ||Qx|| = ||x||
double x[] = { 3.0 , 4.0 };
double Qx[] = { Q[ 0 ]* x[ 0 ]+ Q[ 1 ]* x[ 1 ], Q[ 2 ]* x[ 0 ]+ Q[ 3 ]* x[ 1 ]};
double norm_x = sqrt( x[ 0 ]* x[ 0 ]+ x[ 1 ]* x[ 1 ]);
double norm_Qx = sqrt( Qx[ 0 ]* Qx[ 0 ]+ Qx[ 1 ]* Qx[ 1 ]);
printf( "||x|| = %.4f\n " , norm_x);
printf( "||Qx|| = %.4f (preserved!) \n " , norm_Qx);
return 0 ;
}
Rotation matrix Q (60°):
[[0.5000, -0.8660],
[0.8660, 0.5000]]
Q^T*Q = [[1.0000, 0.0000],
[0.0000, 1.0000]] (≈ I)
det(Q) = 1.0000
||x|| = 5.0000
||Qx|| = 5.0000 (preserved!)
谱定理:对称矩阵的正交对角化
一般特征分解 \(A=Q\Lambda Q^{-1}\) 中,特征向量矩阵 \(Q\) 不一定正交 (\(Q^{-1}\ne Q^T\) )。但对实对称矩阵 ,会发生一件极漂亮的事——特征向量可取成两两正交的。
定理(实对称矩阵的谱定理)
定理 :设 \(A\in\mathbb{R}^{n\times n}\) 对称(\(A=A^T\) ),则
\(A\) 的特征值全是实数 ;
存在正交矩阵 \(Q\) (\(Q^TQ=I\) )和对角阵 \(\Lambda\) ,使
\[ A = Q\Lambda Q^T, \qquad Q^{-1}=Q^T \]
即:对称矩阵可被正交对角化 。
几何意义
一般矩阵的作用 = 在一组(可能歪斜的)轴上缩放;对称矩阵的作用 = 在一组相互正交的轴上纯缩放 ,没有”剪切”成分:
\[
A\mathbf{x} = Q\Lambda Q^T\mathbf{x}
= \underbrace{Q}_{\text{旋转到正交特征轴}}\;
\underbrace{\Lambda}_{\text{各轴独立缩放}}\;
\underbrace{Q^T}_{\text{旋转回去}}
\]
与一般特征分解对比
特征值
可能是复数
全是实数
特征向量
一般不正交
可取成标准正交
求 \(Q^{-1}\)
需单独求逆
\(=Q^T\) ,转置即逆
名词:正定 。对称矩阵 \(A\) 称为正定 ,指对任意 \(\mathbf{x}\ne\mathbf{0}\) 都有 \(\mathbf{x}^T A\mathbf{x}>0\) ;等价于 \(A\) 的所有特征值 \(>0\) 。(半正定则放宽为 \(\ge0\) ;协方差矩阵 \(X^TX\) 恒半正定。)
为什么重要
协方差矩阵 \(\Sigma=X^TX\) 对称 ⟹ 可正交对角化 ⟹ PCA 的基石(主成分 = 正交特征向量,方差 = 特征值)。
海森矩阵 \(H=\nabla^2 f\) 对称 ⟹ 其特征值符号刻画极值(正定 ⟹ 局部极小)。
这正是下一节 SVD 对任意矩阵 都能给出正交基的根源:\(A^TA\) 恒对称,必有正交特征基。
SVD 分解
任何 矩阵 \(A \in \mathbb{R}^{m \times n}\) 都可以分解为:
\[
A = U \Sigma V^T
\]
其中:
\(U \in \mathbb{R}^{m \times m}\) :正交矩阵(左奇异向量),\(U^TU = I\)
\(\Sigma \in \mathbb{R}^{m \times n}\) :对角矩阵(奇异值 \(\sigma_1 \geq \sigma_2 \geq \cdots \geq 0\) )
\(V \in \mathbb{R}^{n \times n}\) :正交矩阵(右奇异向量),\(V^TV = I\)
几何意义
\[
A = \underbrace{U}_{\text{旋转}} \underbrace{\Sigma}_{\text{拉伸}} \underbrace{V^T}_{\text{旋转}}
\]
任何线性变换 = 旋转 → 拉伸 → 旋转
低秩近似(Eckart-Young 定理)
\[
A_r = \sum_{i=1}^{r} \sigma_i \mathbf{u}_i \mathbf{v}_i^T
\]
这是秩为 \(r\) 的矩阵中对 \(A\) 的最佳近似。
适用范围
方阵,且可对角化
任何矩阵
正交性
\(Q\) 一般不正交
\(U, V\) 总是正交的
几何
在一组轴上缩放
旋转+缩放+旋转
代码表示:2×2 SVD 分解
练习 :实现 \(2\times 2\) SVD 分解。对 \(A = [[3,1],[1,3]]\) 求奇异值,验证 \(A \approx U\Sigma V^T\) 和 \(U^TU = I\) 。
#include <stdio.h>
#include <math.h>
// 2×2 SVD 分解 (用解析公式)
// A = U * Σ * V^T
void svd_2x2( const double * A, double * U, double * S, double * V) {
// A^T * A
double ata[ 4 ] = {
A[ 0 ]* A[ 0 ]+ A[ 2 ]* A[ 2 ], A[ 0 ]* A[ 1 ]+ A[ 2 ]* A[ 3 ],
A[ 0 ]* A[ 1 ]+ A[ 2 ]* A[ 3 ], A[ 1 ]* A[ 1 ]+ A[ 3 ]* A[ 3 ]
};
// 奇异值: σ² = eigenvalues of A^T*A
double tr = ata[ 0 ] + ata[ 3 ];
double det = ata[ 0 ]* ata[ 3 ] - ata[ 1 ]* ata[ 2 ];
double disc = sqrt( tr* tr/ 4.0 - det);
double s1sq = tr/ 2.0 + disc;
double s2sq = tr/ 2.0 - disc;
S[ 0 ] = sqrt( s1sq > 0 ? s1sq : 0 );
S[ 1 ] = sqrt( s2sq > 0 ? s2sq : 0 );
// V: eigenvectors of A^T*A
if ( fabs( ata[ 1 ]) > 1e-10 ) {
double v1n = sqrt( ata[ 1 ]* ata[ 1 ] + ( s1sq - ata[ 0 ])*( s1sq - ata[ 0 ]));
V[ 0 ] = ata[ 1 ] / v1n;
V[ 2 ] = ( s1sq - ata[ 0 ]) / v1n;
double v2n = sqrt( ata[ 1 ]* ata[ 1 ] + ( s2sq - ata[ 0 ])*( s2sq - ata[ 0 ]));
V[ 1 ] = ata[ 1 ] / v2n;
V[ 3 ] = ( s2sq - ata[ 0 ]) / v2n;
} else {
V[ 0 ] = 1 ; V[ 1 ] = 0 ;
V[ 2 ] = 0 ; V[ 3 ] = 1 ;
}
// U = A * V * Σ^-1
if ( S[ 0 ] > 1e-10 ) {
U[ 0 ] = ( A[ 0 ]* V[ 0 ] + A[ 1 ]* V[ 2 ]) / S[ 0 ];
U[ 2 ] = ( A[ 2 ]* V[ 0 ] + A[ 3 ]* V[ 2 ]) / S[ 0 ];
}
if ( S[ 1 ] > 1e-10 ) {
U[ 1 ] = ( A[ 0 ]* V[ 1 ] + A[ 1 ]* V[ 3 ]) / S[ 1 ];
U[ 3 ] = ( A[ 2 ]* V[ 1 ] + A[ 3 ]* V[ 3 ]) / S[ 1 ];
}
}
int main() {
double A[] = { 3 , 1 ,
1 , 3 };
double U[ 4 ], S[ 2 ], V[ 4 ];
svd_2x2( A, U, S, V);
printf( "A = [[3,1],[1,3]] \n\n " );
printf( "Singular values: σ1= %.4f , σ2= %.4f\n " , S[ 0 ], S[ 1 ]);
// 验证: A ≈ U * diag(S) * V^T
double SVt[ 4 ] = { S[ 0 ]* V[ 0 ], S[ 0 ]* V[ 1 ], S[ 1 ]* V[ 2 ], S[ 1 ]* V[ 3 ]};
double recon[ 4 ] = {
U[ 0 ]* SVt[ 0 ]+ U[ 1 ]* SVt[ 2 ], U[ 0 ]* SVt[ 1 ]+ U[ 1 ]* SVt[ 3 ],
U[ 2 ]* SVt[ 0 ]+ U[ 3 ]* SVt[ 2 ], U[ 2 ]* SVt[ 1 ]+ U[ 3 ]* SVt[ 3 ]
};
printf( " \n Reconstruction A ≈ UΣV^T: \n " );
printf( " [[ %.4f , %.4f ], \n " , recon[ 0 ], recon[ 1 ]);
printf( " [ %.4f , %.4f ]] \n " , recon[ 2 ], recon[ 3 ]);
// 验证正交性: U^T U ≈ I
printf( " \n U^T*U diagonal: %.4f , %.4f (≈1) \n " ,
U[ 0 ]* U[ 0 ]+ U[ 2 ]* U[ 2 ], U[ 1 ]* U[ 1 ]+ U[ 3 ]* U[ 3 ]);
return 0 ;
}
A = [[3,1],[1,3]]
Singular values: σ1=4.0000, σ2=2.0000
Reconstruction A ≈ UΣV^T:
[[3.0000, 1.0000],
[1.0000, 3.0000]]
U^T*U diagonal: 1.0000, 1.0000 (≈1)
矩阵指数
\[
e^{A} = \sum_{k=0}^{\infty} \frac{A^k}{k!} = I + A + \frac{A^2}{2!} + \frac{A^3}{3!} + \cdots
\]
为什么需要矩阵指数?
连续线性系统 \(\dot{\mathbf{h}}(t) = A\mathbf{h}(t)\) 的解为:
\[
\mathbf{h}(t) = e^{At} \mathbf{h}_0
\]
利用特征分解计算
若 \(A = Q\Lambda Q^{-1}\) :
\[
e^{At} = Q e^{\Lambda t} Q^{-1} = Q \begin{bmatrix} e^{\lambda_1 t} & & \\ & \ddots & \\ & & e^{\lambda_n t} \end{bmatrix} Q^{-1}
\]
计算矩阵指数 → 转化为计算 \(n\) 个标量指数!
代码表示:矩阵指数的幂级数近似
练习 :用幂级数实现 mat_exp_2x2(result, A, t, terms),求解 \(\dot{\mathbf{h}} = A\mathbf{h}\) ,\(A = [[-1,0],[0,-0.1]]\) ,观察不同特征值的衰减速度。
#include <stdio.h>
#include <math.h>
// 矩阵指数的幂级数近似 (2×2)
// e^(At) ≈ I + At + (At)²/2! + ... + (At)^N/N!
void mat_exp_2x2( double * result, const double * A, double t, int terms) {
// result = I
result[ 0 ] = 1 ; result[ 1 ] = 0 ;
result[ 2 ] = 0 ; result[ 3 ] = 1 ;
// power = (At)^k / k!
double power[ 4 ];
double At[ 4 ] = { A[ 0 ]* t, A[ 1 ]* t, A[ 2 ]* t, A[ 3 ]* t};
power[ 0 ] = 1 ; power[ 1 ] = 0 ;
power[ 2 ] = 0 ; power[ 3 ] = 1 ; // starts at (At)^0 / 0! = I
for ( int k = 1 ; k <= terms; k++) {
// power = power * At / k
double tmp[ 4 ];
tmp[ 0 ] = ( power[ 0 ]* At[ 0 ] + power[ 1 ]* At[ 2 ]) / k;
tmp[ 1 ] = ( power[ 0 ]* At[ 1 ] + power[ 1 ]* At[ 3 ]) / k;
tmp[ 2 ] = ( power[ 2 ]* At[ 0 ] + power[ 3 ]* At[ 2 ]) / k;
tmp[ 3 ] = ( power[ 2 ]* At[ 1 ] + power[ 3 ]* At[ 3 ]) / k;
power[ 0 ] = tmp[ 0 ]; power[ 1 ] = tmp[ 1 ];
power[ 2 ] = tmp[ 2 ]; power[ 3 ] = tmp[ 3 ];
result[ 0 ] += power[ 0 ]; result[ 1 ] += power[ 1 ];
result[ 2 ] += power[ 2 ]; result[ 3 ] += power[ 3 ];
}
}
int main() {
// h'(t) = A*h(t), A = [[-1, 0], [0, -0.1]]
// 解: h(t) = e^(At) * h0
double A[] = {- 1.0 , 0.0 ,
0.0 , - 0.1 };
double h0[] = { 1.0 , 1.0 };
printf( "System: h'(t) = A*h(t), A = [[-1,0],[0,-0.1]] \n " );
printf( "Initial: h(0) = [1, 1] \n\n " );
for ( double t = 0 ; t <= 5.0 ; t += 1.0 ) {
double E[ 4 ];
mat_exp_2x2( E, A, t, 20 );
double h1 = E[ 0 ]* h0[ 0 ] + E[ 1 ]* h0[ 1 ];
double h2 = E[ 2 ]* h0[ 0 ] + E[ 3 ]* h0[ 1 ];
// 解析解: h1 = e^(-t), h2 = e^(-0.1t)
double exact1 = exp(- t);
double exact2 = exp(- 0.1 * t);
printf( "t= %.0f : h = [ %.4f , %.4f ] exact = [ %.4f , %.4f ] \n " ,
t, h1, h2, exact1, exact2);
}
printf( " \n λ=-1 decays fast (short memory), λ=-0.1 decays slow (long memory) \n " );
return 0 ;
}
System: h'(t) = A*h(t), A = [[-1,0],[0,-0.1]]
Initial: h(0) = [1, 1]
t=0: h = [1.0000, 1.0000] exact = [1.0000, 1.0000]
t=1: h = [0.3679, 0.9048] exact = [0.3679, 0.9048]
t=2: h = [0.1353, 0.8187] exact = [0.1353, 0.8187]
t=3: h = [0.0498, 0.7408] exact = [0.0498, 0.7408]
t=4: h = [0.0183, 0.6703] exact = [0.0183, 0.6703]
t=5: h = [0.0067, 0.6065] exact = [0.0067, 0.6065]
λ=-1 decays fast (short memory), λ=-0.1 decays slow (long memory)
矩阵求导
标量对向量求导
\[
\frac{\partial f}{\partial \mathbf{x}} = \nabla_{\mathbf{x}} f = \begin{bmatrix} \frac{\partial f}{\partial x_1} \\ \vdots \\ \frac{\partial f}{\partial x_n} \end{bmatrix}
\]
常用公式
\(\mathbf{a}^T \mathbf{x}\)
\(\mathbf{a}\)
\(\mathbf{x}^T \mathbf{x}\)
\(2\mathbf{x}\)
\(\mathbf{x}^T A \mathbf{x}\)
\((A + A^T)\mathbf{x}\) (若 \(A\) 对称:\(2A\mathbf{x}\) )
标量对矩阵求导
\[
\frac{\partial f}{\partial W}_{ij} = \frac{\partial f}{\partial w_{ij}}
\]
\(f = \text{tr}(AW)\)
\(A^T\)
\(f = \mathbf{x}^T W \mathbf{y}\)
\(\mathbf{x}\mathbf{y}^T\)
\(f = \|W\mathbf{x} - \mathbf{y}\|^2\)
\(2(W\mathbf{x}-\mathbf{y})\mathbf{x}^T\)
链式法则
若 \(\mathbf{y} = W\mathbf{x}\) :
\[
\frac{\partial \mathcal{L}}{\partial W} = \underbrace{\frac{\partial \mathcal{L}}{\partial \mathbf{y}}}_{\text{上游梯度}} \underbrace{\mathbf{x}^T}_{\text{局部梯度}}
\]
这就是 PyTorch autograd 在 nn.Linear 中做的事!
代码表示:数值梯度验证与梯度下降
练习 :实现数值梯度 numerical_grad,与解析梯度 \(\nabla(\mathbf{x}^T\mathbf{x}) = 2\mathbf{x}\) 对比验证。再用梯度下降最小化 \(f(\mathbf{x}) = \mathbf{x}^T\mathbf{x}\) 。
#include <stdio.h>
#include <math.h>
// 数值梯度验证
// f(x) = x^T x, grad = 2x
double quadratic( const double * x, int n) {
double s = 0.0 ;
for ( int i = 0 ; i < n; i++) s += x[ i] * x[ i];
return s;
}
void numerical_grad( double * grad, double (* f)( const double *, int ),
const double * x, int n, double eps) {
for ( int i = 0 ; i < n; i++) {
double x_plus[ 10 ], x_minus[ 10 ];
for ( int j = 0 ; j < n; j++) {
x_plus[ j] = x[ j];
x_minus[ j] = x[ j];
}
x_plus[ i] += eps;
x_minus[ i] -= eps;
grad[ i] = ( f( x_plus, n) - f( x_minus, n)) / ( 2 * eps);
}
}
int main() {
double x[] = { 1.0 , 2.0 , 3.0 };
int n = 3 ;
// 解析梯度: ∇(x^T x) = 2x
printf( "f(x) = x^T x \n " );
printf( "f([1,2,3]) = %.1f\n\n " , quadratic( x, n));
printf( "Analytic grad: [" );
for ( int i = 0 ; i < n; i++)
printf( " %.1f%s " , 2 * x[ i], i < n- 1 ? ", " : "] \n " );
double grad[ 10 ];
numerical_grad( grad, quadratic, x, n, 1e-5 );
printf( "Numerical grad: [" );
for ( int i = 0 ; i < n; i++)
printf( " %.4f%s " , grad[ i], i < n- 1 ? ", " : "] \n " );
// 简单梯度下降: min x^T x, x_new = x - lr * grad
printf( " \n Gradient descent on x^T x: \n " );
double lr = 0.1 ;
double gx[ 3 ] = { 1.0 , 2.0 , 3.0 };
for ( int step = 0 ; step <= 5 ; step++) {
double fx = quadratic( gx, n);
printf( " step %d : x=[ %.4f , %.4f , %.4f ], f= %.6f\n " ,
step, gx[ 0 ], gx[ 1 ], gx[ 2 ], fx);
for ( int i = 0 ; i < n; i++)
gx[ i] -= lr * 2 * gx[ i]; // grad = 2x
}
return 0 ;
}
总览:概念之间的逻辑链
向量 + 标量乘法 + 加法
│
▼
线性组合 ──→ 线性无关 ──→ 基 ──→ 维度
│
▼
矩阵 = 线性变换
│
├──→ 核(被消灭的)+ 像(能到达的)──→ 秩
│
├──→ 行列式(体积缩放)──→ 逆矩阵(可逆 ⟺ det ≠ 0)
│
└──→ 特征值/特征向量(不动轴)──→ 特征分解(对称 ⟹ 正交对角化)
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
矩阵指数 e^At 正交矩阵 SVD
│ (旋转 / 保形) (旋转+拉伸+旋转)
▼ │
SSM 连续系统 低秩近似 ──→ LoRA
贯穿全程的「度量」:迹 tr(·) · 范数 ‖·‖(长度 / 距离 / 正则)
速查表:符号约定
\(x, \alpha, \lambda\)
标量
\(0\)
\(\mathbf{x}, \mathbf{v}\)
向量(列向量)
\(\mathbb{R}^n\)
\(\mathbf{x}^T\)
行向量
\(\mathbb{R}^{1 \times n}\)
\(x_i\)
向量第 \(i\) 个分量
标量
\(A, B, W\)
矩阵
\(\mathbb{R}^{m \times n}\)
\(A^T\)
转置
\(\mathbb{R}^{n \times m}\)
\(A^{-1}\)
逆矩阵
\(\mathbb{R}^{n \times n}\)
\(a_{ij}\)
矩阵第 \(i\) 行第 \(j\) 列
标量
\(I_n\)
\(n\) 阶单位矩阵
\(\mathbb{R}^{n \times n}\)
\(\mathbf{x}^T\mathbf{y}\)
内积
标量
\(\|\mathbf{x}\|\)
L2 范数
标量
\(\det(A)\)
行列式
标量
\(\text{tr}(A)\)
迹
标量
\(\lambda\)
特征值
标量
\(\mathbf{v}\)
特征向量
\(\mathbb{R}^n\)
\(\sigma\)
奇异值
标量
\(\text{rank}(A)\)
秩
整数