一、向量:从箭头到信息的载体
第一层:概念溯源------力学家的烦恼
向量这个概念,最初是十九世纪物理学家为了描述力和速度这类"既有大小又有方向"的量搞出来的。你想啊,牛顿那会儿研究力学,光说"这个力是10牛顿"不够用啊,你得说清楚这力往哪个方向使劲吧?
1843年,爱尔兰数学家哈密顿(对,就是那个搞出哈密顿量的家伙)正式提出了向量的数学定义。后来格拉斯曼又把它推广到多维空间。这玩意儿最开始就是个物理工具,谁能想到两百年后它会成为神经网络的通用语言呢?
历史定位一句话:向量是从经典力学中生长出来的数学语言,后来成了整个线性代数的主角。
第二层:深层直觉------它到底在描述什么?
向量的本质是什么?它是空间中的一个箭头。
在二维平面上,一个向量v⃗=(3,4)\vec{v} = (3, 4)v =(3,4)就是从原点出发,先向右走3步,再向上走4步,最后那支箭就是你的向量。注意到没?向量不关心你从哪儿出发,它只关心方向 和长度。你把这支箭平移到任何地方,只要方向和长度不变,它还是同一个向量。
换句话说,向量是空间中的位移指令。
但到了机器学习里,向量的含义更抽象了------它变成了信息的容器。一张图片可以表示成一个784维的向量(28×28像素展开),一句话可以表示成一个300维的词向量,一个用户可以表示成一个包含年龄、收入、购买记录的特征向量。
所以别把向量只当成几何箭头,它更像是一个多维档案袋,每个维度装着一种信息。
第三层:具体内容------向量的表示与运算
3.1 向量的表示
向量通常写成列的形式(虽然为了节省空间,我们常横着写):
v⃗=[v1v2v3⋮vn]\vec{v} = \begin{bmatrix} v_1 \\ v_2 \\ v_3 \\ \vdots \\ v_n \end{bmatrix}v = v1v2v3⋮vn
比如一个三维向量:
a⃗=[2−15]\vec{a} = \begin{bmatrix} 2 \\ -1 \\ 5 \end{bmatrix}a = 2−15
本质一句话:向量就是有序的一排数字,顺序不能乱。
Python里最常用numpy来表示:
python
import numpy as np
# 创建一个向量
v = np.array([2, -1, 5])
print(f"向量v: {v}")
print(f"维度: {v.shape}") # 输出 (3,)
# 也可以明确创建列向量
v_col = np.array([[2], [-1], [5]])
print(f"列向量:\n{v_col}")
print(f"维度: {v_col.shape}") # 输出 (3, 1)
3.2 向量加法------箭头接龙
两个向量相加,几何意义就是把两支箭首尾相接:
a⃗+b⃗=[a1a2a3]+[b1b2b3]=[a1+b1a2+b2a3+b3]\vec{a} + \vec{b} = \begin{bmatrix} a_1 \\ a_2 \\ a_3 \end{bmatrix} + \begin{bmatrix} b_1 \\ b_2 \\ b_3 \end{bmatrix} = \begin{bmatrix} a_1 + b_1 \\ a_2 + b_2 \\ a_3 + b_3 \end{bmatrix}a +b = a1a2a3 + b1b2b3 = a1+b1a2+b2a3+b3
本质一句话:向量加法就是对应位置的数字分别相加,几何上是路径叠加。
python
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
# 向量加法
c = a + b
print(f"a + b = {c}") # [5 7 9]
# 几何意义:如果a代表"向东1km,向北2km,向上3km"
# b代表"向东4km,向北5km,向上6km"
# 那c就是"向东5km,向北7km,向上9km"
3.3 数量乘法------拉伸箭头
一个数乘以向量,就是把箭头拉长或缩短:
k⋅v⃗=k⋅[v1v2v3]=[k⋅v1k⋅v2k⋅v3]k \cdot \vec{v} = k \cdot \begin{bmatrix} v_1 \\ v_2 \\ v_3 \end{bmatrix} = \begin{bmatrix} k \cdot v_1 \\ k \cdot v_2 \\ k \cdot v_3 \end{bmatrix}k⋅v =k⋅ v1v2v3 = k⋅v1k⋅v2k⋅v3
本质一句话 :数量乘法改变向量的长度,但不改变方向(除非k<0k<0k<0会反向)。
python
v = np.array([1, 2, 3])
k = 2.5
# 数量乘法
scaled_v = k * v
print(f"2.5 * v = {scaled_v}") # [2.5 5. 7.5]
# 负数会反向
negative_v = -1 * v
print(f"-v = {negative_v}") # [-1 -2 -3]
3.4 点积(内积)------相似度检测器
这是向量运算里最重要的操作之一:
a⃗⋅b⃗=a1b1+a2b2+⋯+anbn=∑i=1naibi\vec{a} \cdot \vec{b} = a_1 b_1 + a_2 b_2 + \cdots + a_n b_n = \sum_{i=1}^{n} a_i b_ia ⋅b =a1b1+a2b2+⋯+anbn=i=1∑naibi
几何意义 :a⃗⋅b⃗=∣a⃗∣∣b⃗∣cosθ\vec{a} \cdot \vec{b} = |\vec{a}| |\vec{b}| \cos\thetaa ⋅b =∣a ∣∣b ∣cosθ,其中θ\thetaθ是两个向量的夹角。
本质一句话:点积衡量两个向量的"同向程度",结果是一个数字。
python
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
# 点积计算
dot_product = np.dot(a, b)
print(f"a·b = {dot_product}") # 1*4 + 2*5 + 3*6 = 32
# 或者用@运算符(Python 3.5+)
dot_product_2 = a @ b
print(f"a@b = {dot_product_2}") # 32
# 如果两个向量垂直,点积为0
perpendicular_1 = np.array([1, 0])
perpendicular_2 = np.array([0, 1])
print(f"垂直向量点积: {perpendicular_1 @ perpendicular_2}") # 0
3.5 向量的长度(范数)
向量的长度用L2L^2L2范数(欧几里得范数)定义:
∥v⃗∥=v12+v22+⋯+vn2\|\vec{v}\| = \sqrt{v_1^2 + v_2^2 + \cdots + v_n^2}∥v ∥=v12+v22+⋯+vn2
本质一句话:长度就是向量从原点到终点的直线距离。
python
v = np.array([3, 4])
# 计算长度
length = np.linalg.norm(v)
print(f"向量长度: {length}") # 5.0 (因为3²+4²=25,√25=5)
# 单位向量(长度为1)
unit_v = v / length
print(f"单位向量: {unit_v}") # [0.6 0.8]
print(f"单位向量长度: {np.linalg.norm(unit_v)}") # 1.0
第四层:现代应用------向量在机器学习中的角色
应用1:词嵌入(Word Embedding)
在NLP中,每个词被表示成一个高维向量。语义相近的词,向量方向相似:
similarity(w1,w2)=w1⃗⋅w2⃗∥w1⃗∥∥w2⃗∥\text{similarity}(w_1, w_2) = \frac{\vec{w_1} \cdot \vec{w_2}}{\|\vec{w_1}\| \|\vec{w_2}\|}similarity(w1,w2)=∥w1 ∥∥w2 ∥w1 ⋅w2
这就是余弦相似度,取值范围[−1,1][-1, 1][−1,1]。
python
# 简化示例:词向量
king = np.array([0.5, 0.8, 0.3])
queen = np.array([0.48, 0.82, 0.28])
apple = np.array([-0.3, 0.1, 0.9])
def cosine_similarity(v1, v2):
return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
print(f"king-queen相似度: {cosine_similarity(king, queen):.3f}") # 接近1
print(f"king-apple相似度: {cosine_similarity(king, apple):.3f}") # 较小
应用2:神经网络的输入
一张28×28的灰度图像,展开成784维向量:
x⃗=[x1,x2,...,x784]T\vec{x} = [x_1, x_2, \ldots, x_{784}]^Tx =[x1,x2,...,x784]T
每个xix_ixi是一个像素值(0-255)。
python
# 模拟图像数据
image = np.random.randint(0, 256, size=(28, 28))
print(f"图像形状: {image.shape}")
# 展平成向量
image_vector = image.flatten()
print(f"向量形状: {image_vector.shape}") # (784,)
print(f"前10个像素: {image_vector[:10]}")
应用3:梯度向量
在优化过程中,梯度是损失函数对参数的偏导数组成的向量:
∇θL=[∂L∂θ1∂L∂θ2⋮∂L∂θn]\nabla_\theta L = \begin{bmatrix} \frac{\partial L}{\partial \theta_1} \\ \frac{\partial L}{\partial \theta_2} \\ \vdots \\ \frac{\partial L}{\partial \theta_n} \end{bmatrix}∇θL= ∂θ1∂L∂θ2∂L⋮∂θn∂L
梯度方向是函数上升最快的方向,所以我们反着走(梯度下降):
θnew=θold−α∇θL\theta_{new} = \theta_{old} - \alpha \nabla_\theta Lθnew=θold−α∇θL
第五层:关键洞察
向量是信息的最小运输单位,点积是测量共鸣的标尺。
在机器学习里,向量不仅仅是数字的排列,它是特征的编码、语义的凝聚、方向的指引。每一次点积计算,都是在询问:"这两个信息包有多像?"每一次向量加法,都是在说:"让我们合并这些证据。"
二、矩阵:向量的舞台与变换的魔法
第一层:概念溯源------联立方程的简化记号
矩阵的故事要追溯到中国古代的《九章算术》,那里面已经有了用表格解方程组的思想。但现代矩阵理论的真正奠基人是英国数学家凯莱(Arthur Cayley),他在1858年发表的《矩阵论备忘录》中首次系统地定义了矩阵运算。
当时数学家们面临一个烦恼:联立方程组越来越复杂,写起来又长又乱。比如:
{2x+3y−z=5x−y+4z=−23x+2y+z=7 \begin{cases} 2x + 3y - z = 5 \\ x - y + 4z = -2 \\ 3x + 2y + z = 7 \end{cases} ⎩ ⎨ ⎧2x+3y−z=5x−y+4z=−23x+2y+z=7
能不能有个简洁的记号?矩阵应运而生:
Ax⃗=b⃗A\vec{x} = \vec{b}Ax =b
其中AAA是系数矩阵,x⃗\vec{x}x 是未知数向量,b⃗\vec{b}b 是常数项向量。一下子清爽多了!
历史定位一句话:矩阵是为了简化代数运算而发明的符号系统,后来成了描述线性变换的最佳语言。
第二层:深层直觉------矩阵是变换机器
很多教材会说"矩阵是一个二维数组"------没错,但这只是表象。矩阵的深层含义是:它是一台变换机器。
想象一下,你把一个向量v⃗\vec{v}v 丢进矩阵AAA这台机器,出来一个新向量Av⃗A\vec{v}Av 。这个过程可能是:
- 旋转:把向量转个角度
- 缩放:拉长或压扁向量
- 投影:把三维压到二维
- 反射:像照镜子一样翻转
换句话说,矩阵是空间几何变换的指令集。
在机器学习里,这个比喻更实在了------神经网络的每一层,本质上就是一个矩阵变换:
h⃗=σ(Wx⃗+b⃗)\vec{h} = \sigma(W\vec{x} + \vec{b})h =σ(Wx +b )
权重矩阵WWW把输入x⃗\vec{x}x 变换到新的特征空间,激活函数σ\sigmaσ再加点非线性调料。整个深度网络,就是一系列矩阵变换的接力赛。
第三层:具体内容------矩阵的表示与基础运算
3.1 矩阵的定义与表示
矩阵是一个m×nm \times nm×n的数字表格:
A=[a11a12⋯a1na21a22⋯a2n⋮⋮⋱⋮am1am2⋯amn]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}A= a11a21⋮am1a12a22⋮am2⋯⋯⋱⋯a1na2n⋮amn
- mmm是行数(rows)
- nnn是列数(columns)
- aija_{ij}aij表示第iii行第jjj列的元素
本质一句话:矩阵是向量的排列组合,可以看成一堆列向量并排站,也可以看成一堆行向量叠罗汉。
python
import numpy as np
# 创建一个3×4的矩阵
A = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
])
print(f"矩阵A:\n{A}")
print(f"形状: {A.shape}") # (3, 4)
print(f"第2行第3列的元素: {A[1, 2]}") # 7 (注意索引从0开始)
# 提取某一行
row_2 = A[1, :]
print(f"第2行: {row_2}") # [5 6 7 8]
# 提取某一列
col_3 = A[:, 2]
print(f"第3列: {col_3}") # [ 3 7 11]
3.2 矩阵加减法------对位相加
两个同型矩阵(行列数都相同)才能相加减:
A+B=[a11+b11a12+b12a21+b21a22+b22]A + B = \begin{bmatrix} a_{11} + b_{11} & a_{12} + b_{12} \\ a_{21} + b_{21} & a_{22} + b_{22} \end{bmatrix}A+B=[a11+b11a21+b21a12+b12a22+b22]
规则:对应位置的元素分别相加。
本质一句话:矩阵加法就是"格子对格子"的加法,没有花样。
python
A = np.array([
[1, 2, 3],
[4, 5, 6]
])
B = np.array([
[7, 8, 9],
[10, 11, 12]
])
# 矩阵加法
C = A + B
print(f"A + B =\n{C}")
# [[ 8 10 12]
# [14 16 18]]
# 矩阵减法
D = A - B
print(f"A - B =\n{D}")
# [[-6 -6 -6]
# [-6 -6 -6]]
# 尝试加不同形状的矩阵会报错
E = np.array([[1, 2], [3, 4]])
# A + E # 会报错!形状不匹配
注意:广播(broadcasting)机制可能让你觉得numpy可以加不同形状,但那是另一回事,不是严格意义的矩阵加法。
3.3 数量乘法------整体缩放
一个标量乘以矩阵,就是每个元素都乘以这个数:
k⋅A=[k⋅a11k⋅a12k⋅a21k⋅a22]k \cdot A = \begin{bmatrix} k \cdot a_{11} & k \cdot a_{12} \\ k \cdot a_{21} & k \cdot a_{22} \end{bmatrix}k⋅A=[k⋅a11k⋅a21k⋅a12k⋅a22]
本质一句话:数量乘法是矩阵的全局缩放,不改变结构关系。
python
A = np.array([
[1, 2],
[3, 4]
])
k = 3
# 数量乘法
B = k * A
print(f"3 * A =\n{B}")
# [[ 3 6]
# [ 9 12]]
# 在机器学习中,学习率就是这样作用的
learning_rate = 0.01
gradient = np.array([[1.5, 2.3], [0.8, 1.2]])
update = learning_rate * gradient
print(f"参数更新量:\n{update}")
3.4 矩阵乘法------最关键的操作
这是整个线性代数最重要的操作,也是最容易搞混的。
规则 :Am×nA_{m \times n}Am×n乘以Bn×pB_{n \times p}Bn×p,得到Cm×pC_{m \times p}Cm×p。关键是**AAA的列数必须等于BBB的行数**。
Cij=∑k=1nAik⋅BkjC_{ij} = \sum_{k=1}^{n} A_{ik} \cdot B_{kj}Cij=k=1∑nAik⋅Bkj
记忆口诀:"行遇列,内相消,外保留"。
- (m×n)×(n×p)=(m×p)(m \times \color{red}{n}) \times (\color{red}{n} \times p) = (m \times p)(m×n)×(n×p)=(m×p)
- 红色的nnn必须相等,它们"消掉"
- 外侧的mmm和ppp保留
几何意义 :矩阵乘法是复合变换 。先用BBB变换,再用AAA变换,相当于一次性用ABABAB变换。
本质一句话 :矩阵乘法不是简单的"对应位置相乘",而是"AAA的行"和"BBB的列"之间的内积。
python
# 例子1:标准矩阵乘法
A = np.array([
[1, 2, 3], # 2行3列
[4, 5, 6]
])
B = np.array([
[7, 8], # 3行2列
[9, 10],
[11, 12]
])
# A(2×3) × B(3×2) = C(2×2)
C = A @ B # 或者 np.dot(A, B) 或 np.matmul(A, B)
print(f"A @ B =\n{C}")
# [[ 58 64]
# [139 154]]
# 详细计算第一个元素:
# C[0,0] = 1*7 + 2*9 + 3*11 = 7 + 18 + 33 = 58
# 例子2:矩阵乘向量(神经网络的核心)
W = np.array([
[0.5, 0.3, 0.2],
[0.1, 0.8, 0.4]
])
x = np.array([[1], [2], [3]])
# W(2×3) × x(3×1) = y(2×1)
y = W @ x
print(f"Wx =\n{y}")
# [[1.7]
# [3.7]]
# 例子3:不匹配的矩阵乘法会报错
try:
wrong = B @ A # B(3×2) × A(2×3) 可以!结果是3×3
print(f"B @ A 可以计算:\n{wrong}")
except:
print("维度不匹配!")
# 但反过来A @ B和B @ A结果不同!
print(f"A @ B的形状: {(A @ B).shape}") # (2, 2)
print(f"B @ A的形状: {(B @ A).shape}") # (3, 3)
重要提醒 :矩阵乘法不满足交换律 !AB≠BAAB \neq BAAB=BA(大多数情况)。但满足结合律:(AB)C=A(BC)(AB)C = A(BC)(AB)C=A(BC)。
3.5 矩阵转置------翻转的艺术
转置就是把矩阵沿主对角线翻转,行变列,列变行:
(AT)ij=Aji(A^T){ij} = A{ji}(AT)ij=Aji
如果AAA是m×nm \times nm×n,那ATA^TAT就是n×mn \times mn×m。
A=[123456]⇒AT=[142536]A = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix} \quad \Rightarrow \quad A^T = \begin{bmatrix} 1 & 4 \\ 2 & 5 \\ 3 & 6 \end{bmatrix}A=[142536]⇒AT= 123456
几何意义 :转置相当于把列向量的视角换成行向量的视角,或者说是信息流向的反转。
本质一句话:转置是矩阵的镜像操作,行列互换,信息结构不变。
python
A = np.array([
[1, 2, 3],
[4, 5, 6]
])
# 转置
A_T = A.T # 或者 np.transpose(A)
print(f"A =\n{A}")
print(f"A^T =\n{A_T}")
# 验证形状变化
print(f"A的形状: {A.shape}") # (2, 3)
print(f"A^T的形状: {A_T.shape}") # (3, 2)
# 转置两次回到原矩阵
A_TT = A.T.T
print(f"(A^T)^T == A? {np.array_equal(A_TT, A)}") # True
转置的重要性质:
- (AT)T=A(A^T)^T = A(AT)T=A(转置两次回到自己)
- (A+B)T=AT+BT(A + B)^T = A^T + B^T(A+B)T=AT+BT(加法分配律)
- (kA)T=kAT(kA)^T = kA^T(kA)T=kAT(数量乘法兼容)
- (AB)T=BTAT(AB)^T = B^T A^T(AB)T=BTAT(注意顺序反了!)
第4条特别重要,记住:转置会反转乘法顺序。
python
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# 验证 (AB)^T = B^T A^T
AB = A @ B
AB_T = AB.T
B_T_A_T = B.T @ A.T
print(f"(AB)^T =\n{AB_T}")
print(f"B^T A^T =\n{B_T_A_T}")
print(f"相等吗? {np.array_equal(AB_T, B_T_A_T)}") # True
第四层:现代应用------矩阵运算在深度学习中的实战
应用1:全连接层(Dense Layer)
全连接层的前向传播就是一个矩阵乘法:
z⃗=Wx⃗+b⃗\vec{z} = W\vec{x} + \vec{b}z =Wx +b
其中:
- WWW是权重矩阵(nout×nin)(n_{out} \times n_{in})(nout×nin)
- x⃗\vec{x}x 是输入向量(nin×1)(n_{in} \times 1)(nin×1)
- b⃗\vec{b}b 是偏置向量(nout×1)(n_{out} \times 1)(nout×1)
- z⃗\vec{z}z 是输出向量(nout×1)(n_{out} \times 1)(nout×1)
python
# 模拟一个简单的全连接层
n_input = 784 # 输入维度(如28×28的图像)
n_hidden = 128 # 隐藏层神经元数量
# 初始化权重和偏置
W = np.random.randn(n_hidden, n_input) * 0.01
b = np.zeros((n_hidden, 1))
# 输入数据(一个样本)
x = np.random.randn(n_input, 1)
# 前向传播
z = W @ x + b
print(f"输入形状: {x.shape}") # (784, 1)
print(f"权重形状: {W.shape}") # (128, 784)
print(f"输出形状: {z.shape}") # (128, 1)
print(f"输出的前5个值:\n{z[:5]}")
批量处理 :如果有batch_size=32个样本,输入变成(784×32)(784 \times 32)(784×32)的矩阵:
Z=WX+bZ = WX + bZ=WX+b
这时ZZZ是(128×32)(128 \times 32)(128×32),每一列是一个样本的输出。
应用2:反向传播中的转置
梯度回传用到大量转置。如果前向传播是:
z⃗=Wx⃗\vec{z} = W\vec{x}z =Wx
那梯度是:
∂L∂W=∂L∂z⃗x⃗T\frac{\partial L}{\partial W} = \frac{\partial L}{\partial \vec{z}} \vec{x}^T∂W∂L=∂z ∂Lx T
注意这里x⃗T\vec{x}^Tx T的转置!这样维度才能匹配。
python
# 简化的反向传播示例
# 假设损失对输出的梯度
dL_dz = np.random.randn(n_hidden, 1)
# 计算损失对权重的梯度
dL_dW = dL_dz @ x.T # (128×1) @ (1×784) = (128×784)
print(f"梯度dL/dW的形状: {dL_dW.shape}") # (128, 784)
print(f"与权重W形状相同? {dL_dW.shape == W.shape}") # True
# 计算损失对输入的梯度
dL_dx = W.T @ dL_dz # (784×128) @ (128×1) = (784×1)
print(f"梯度dL/dx的形状: {dL_dx.shape}") # (784, 1)
关键洞察:反向传播本质上是一系列转置矩阵乘法,把梯度从输出层往回传。
应用3:协方差矩阵------数据的分布肖像
给定数据矩阵Xn×dX_{n \times d}Xn×d(nnn个样本,ddd个特征),协方差矩阵是:
Σ=1n−1(X−Xˉ)T(X−Xˉ)\Sigma = \frac{1}{n-1}(X - \bar{X})^T(X - \bar{X})Σ=n−11(X−Xˉ)T(X−Xˉ)
这是一个d×dd \times dd×d的对称矩阵,Σij\Sigma_{ij}Σij表示第iii个特征和第jjj个特征的协方差。
python
# 生成随机数据
n_samples = 100
n_features = 3
X = np.random.randn(n_samples, n_features)
# 中心化(减去均值)
X_centered = X - X.mean(axis=0)
# 计算协方差矩阵
cov_matrix = (X_centered.T @ X_centered) / (n_samples - 1)
print(f"协方差矩阵:\n{cov_matrix}")
print(f"形状: {cov_matrix.shape}") # (3, 3)
# 对角线是方差,非对角线是协方差
print(f"第1个特征的方差: {cov_matrix[0, 0]:.3f}")
print(f"特征1和特征2的协方差: {cov_matrix[0, 1]:.3f}")
# numpy的内置函数(注意要设置rowvar=False)
cov_matrix_np = np.cov(X, rowvar=False)
print(f"用numpy计算的协方差矩阵:\n{cov_matrix_np}")
应用4:注意力机制(Attention)
Transformer中的自注意力用到了一堆矩阵乘法:
Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)VAttention(Q,K,V)=softmax(dk QKT)V
其中QQQ、KKK、VVV都是矩阵,分别代表查询(Query)、键(Key)、值(Value)。
python
# 简化的注意力计算
seq_len = 10 # 序列长度
d_model = 64 # 模型维度
d_k = 8 # Key的维度
# 随机生成Q, K, V
Q = np.random.randn(seq_len, d_k)
K = np.random.randn(seq_len, d_k)
V = np.random.randn(seq_len, d_model)
# 计算注意力分数
scores = (Q @ K.T) / np.sqrt(d_k) # (10×8) @ (8×10) = (10×10)
print(f"注意力分数形状: {scores.shape}") # (10, 10)
# Softmax归一化(简化版,实际上要沿某个轴)
attention_weights = np.exp(scores) / np.exp(scores).sum(axis=1, keepdims=True)
# 加权求和
output = attention_weights @ V # (10×10) @ (10×64) = (10×64)
print(f"输出形状: {output.shape}") # (10, 64)
第五层:关键洞察
矩阵是线性世界的操作系统,乘法是变换的语法,转置是时光倒流的按钮。
在机器学习的宇宙里,矩阵不仅仅是数字的方阵,它是神经元之间对话的语言、信息流动的通道、梯度回传的桥梁。每一次矩阵乘法,都是一次特征空间的跃迁;每一次转置,都是一次信息视角的翻转。
三、特殊矩阵:工具箱里的标准件
第一层:概念溯源------从特例中发现规律
数学家们在研究矩阵时,发现某些特殊形态的矩阵有着超级好用的性质。就像工程师的工具箱里有扳手、螺丝刀这些标准工具,矩阵家族也有几位"标准成员"。
这些特殊矩阵的概念主要在19世纪末20世纪初系统化,它们简化了大量计算,甚至有些深刻的理论(比如特征值分解)都建立在这些特殊矩阵的性质之上。
历史定位一句话:特殊矩阵是线性代数中的"基础元件库",它们的简洁性质让复杂问题变得可解。
第二层:深层直觉------特殊矩阵是变换的极端情况
普通矩阵是各种变换的混合体,但特殊矩阵是纯粹的:
- 单位矩阵是"什么都不做"的变换(恒等变换)
- 零矩阵是"全部抹杀"的变换(零化变换)
- 对角矩阵是"分别缩放"的变换(各维度独立)
换句话说,特殊矩阵是变换家族中的"原子操作"。
第三层:具体内容------三大标准矩阵
3.1 单位矩阵(Identity Matrix)
单位矩阵III是主对角线全为1,其余位置全为0的方阵:
I3=[100010001]I_3 = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}I3= 100010001
核心性质 :IA=AI=AIA = AI = AIA=AI=A(乘以任何矩阵都保持不变)
本质一句话:单位矩阵是矩阵乘法中的"1",是保持原状的魔法。
python
# 创建单位矩阵
I3 = np.eye(3) # 3×3单位矩阵
print(f"3×3单位矩阵:\n{I3}")
# 也可以用identity
I5 = np.identity(5)
print(f"5×5单位矩阵:\n{I5}")
# 验证性质:I @ A = A
A = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
result = I3 @ A
print(f"I @ A =\n{result}")
print(f"等于A吗? {np.array_equal(result, A)}") # True
在机器学习中的应用:
-
正则化:岭回归(Ridge Regression)加的就是单位矩阵的倍数:
W^=(XTX+λI)−1XTy\hat{W} = (X^TX + \lambda I)^{-1}X^T yW^=(XTX+λI)−1XTy
-
残差连接 (ResNet):y⃗=F(x⃗)+Ix⃗\vec{y} = F(\vec{x}) + I\vec{x}y =F(x )+Ix
python
# 岭回归中的正则化项
lambda_reg = 0.1
n_features = 10
# 正则化矩阵
reg_term = lambda_reg * np.eye(n_features)
print(f"正则化项:\n{reg_term[:3, :3]}") # 只显示前3×3
3.2 零矩阵(Zero Matrix)
零矩阵OOO是所有元素都为0的矩阵:
O2×3=[000000]O_{2 \times 3} = \begin{bmatrix} 0 & 0 & 0 \\ 0 & 0 & 0 \end{bmatrix}O2×3=[000000]
核心性质:
- A+O=AA + O = AA+O=A(加法单位元)
- AO=OAO = OAO=O,OA=OOA = OOA=O(乘以零矩阵得零矩阵)
本质一句话:零矩阵是矩阵加法中的"0",是一切的终结者。
python
# 创建零矩阵
Z = np.zeros((3, 4))
print(f"3×4零矩阵:\n{Z}")
# 验证性质
A = np.array([[1, 2], [3, 4]])
Z2 = np.zeros((2, 2))
print(f"A + O =\n{A + Z2}") # 还是A
print(f"A @ O =\n{A @ Z2}") # 全是0
在机器学习中的应用:
- 权重初始化 :虽然不能把权重全初始化为0(会导致对称性问题),但偏置bbb常初始化为0
- 梯度清零:每次反向传播前要把梯度归零
python
# 梯度清零示例
class SimpleLayer:
def __init__(self, input_dim, output_dim):
self.W = np.random.randn(output_dim, input_dim) * 0.01
self.b = np.zeros((output_dim, 1)) # 偏置初始化为0
# 梯度
self.dW = None
self.db = None
def zero_grad(self):
"""清空梯度"""
self.dW = np.zeros_like(self.W)
self.db = np.zeros_like(self.b)
layer = SimpleLayer(10, 5)
print(f"偏置b:\n{layer.b}")
3.3 对角矩阵(Diagonal Matrix)
对角矩阵只有主对角线上有非零元素:
D=[d1000d2000d3]D = \begin{bmatrix} d_1 & 0 & 0 \\ 0 & d_2 & 0 \\ 0 & 0 & d_3 \end{bmatrix}D= d1000d2000d3
核心性质:
- 对角矩阵相乘非常简单:(D1D2)ii=d1,i⋅d2,i(D_1 D_2){ii} = d{1,i} \cdot d_{2,i}(D1D2)ii=d1,i⋅d2,i
- 对角矩阵的逆也是对角矩阵:Dii−1=1/diD^{-1}_{ii} = 1/d_iDii−1=1/di
- 对角矩阵的特征值就是对角线元素
几何意义 :对角矩阵代表各坐标轴独立缩放,不产生旋转和剪切。
本质一句话:对角矩阵是最简单的变换,各维度自扫门前雪,互不干扰。
python
# 创建对角矩阵
diag_elements = [2, 3, 5]
D = np.diag(diag_elements)
print(f"对角矩阵D:\n{D}")
# 也可以从现有矩阵提取对角线
A = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
diag_A = np.diag(np.diag(A)) # 先提取对角线元素,再构造对角矩阵
print(f"A的对角部分:\n{diag_A}")
# 对角矩阵乘法很高效
D1 = np.diag([2, 3])
D2 = np.diag([4, 5])
D_product = D1 @ D2
print(f"D1 @ D2 =\n{D_product}")
# [[8 0]
# [0 15]] # 2*4=8, 3*5=15
在机器学习中的应用:
应用1:特征缩放
python
# 标准化时,每个特征除以标准差,相当于乘以对角矩阵
std_devs = np.array([2.0, 5.0, 1.5])
D_scale = np.diag(1.0 / std_devs)
X = np.array([
[4, 10, 3],
[6, 15, 4.5],
[2, 5, 1.5]
])
X_scaled = X @ D_scale # 每一列(特征)独立缩放
print(f"缩放后的数据:\n{X_scaled}")
应用2:协方差矩阵的特例
如果特征之间完全不相关,协方差矩阵就是对角矩阵:
Σ=[σ12000σ22000σ32]\Sigma = \begin{bmatrix} \sigma_1^2 & 0 & 0 \\ 0 & \sigma_2^2 & 0 \\ 0 & 0 & \sigma_3^2 \end{bmatrix}Σ= σ12000σ22000σ32
应用3:学习率的自适应调整
AdaGrad等优化器用对角矩阵来存储各参数的历史梯度信息:
θt=θt−1−αGt+ϵ⊙gt\theta_t = \theta_{t-1} - \frac{\alpha}{\sqrt{G_t + \epsilon}} \odot g_tθt=θt−1−Gt+ϵ α⊙gt
其中GtG_tGt是对角矩阵,存储累积的平方梯度。
python
# 简化的AdaGrad示例
class AdaGrad:
def __init__(self, params_shape, lr=0.01, epsilon=1e-8):
self.lr = lr
self.epsilon = epsilon
# G是对角矩阵,这里简化为向量
self.G = np.zeros(params_shape)
def update(self, params, grad):
# 累积平方梯度
self.G += grad ** 2
# 自适应学习率
adjusted_grad = grad / (np.sqrt(self.G) + self.epsilon)
# 更新参数
params -= self.lr * adjusted_grad
return params
# 模拟使用
params = np.array([1.0, 2.0, 3.0])
optimizer = AdaGrad(params.shape)
grad = np.array([0.5, 0.2, 0.8])
params = optimizer.update(params, grad)
print(f"更新后的参数: {params}")
3.4 其他重要的特殊矩阵(快速浏览)
对称矩阵 (Symmetric Matrix):A=ATA = A^TA=AT
A=[123245356]A = \begin{bmatrix} 1 & 2 & 3 \\ 2 & 4 & 5 \\ 3 & 5 & 6 \end{bmatrix}A= 123245356
协方差矩阵、Gram矩阵都是对称的。
正交矩阵 (Orthogonal Matrix):QTQ=IQ^TQ = IQTQ=I
旋转矩阵是正交矩阵的典型例子,它保持向量长度不变。
上三角/下三角矩阵(Triangular Matrix):
U=[u11u12u130u22u2300u33]U = \begin{bmatrix} u_{11} & u_{12} & u_{13} \\ 0 & u_{22} & u_{23} \\ 0 & 0 & u_{33} \end{bmatrix}U= u1100u12u220u13u23u33
在LU分解、回代算法中很重要。
python
# 创建上三角矩阵
A = np.random.randn(4, 4)
U = np.triu(A) # upper triangle
print(f"上三角矩阵:\n{U}")
# 创建下三角矩阵
L = np.tril(A) # lower triangle
print(f"下三角矩阵:\n{L}")
第四层:现代应用------特殊矩阵的智能用法
应用1:BatchNorm中的缩放平移
Batch Normalization本质上用对角矩阵做缩放:
x^=γ⊙x−μσ+β\hat{x} = \gamma \odot \frac{x - \mu}{\sigma} + \betax^=γ⊙σx−μ+β
其中γ\gammaγ和β\betaβ是可学习的对角矩阵(或向量)。
python
# 简化的BatchNorm
class SimpleBatchNorm:
def __init__(self, num_features):
self.gamma = np.ones(num_features) # 缩放参数
self.beta = np.zeros(num_features) # 平移参数
def forward(self, x):
# x: (batch_size, num_features)
mu = x.mean(axis=0)
sigma = x.std(axis=0)
# 标准化
x_normalized = (x - mu) / (sigma + 1e-8)
# 缩放和平移
out = self.gamma * x_normalized + self.beta
return out
bn = SimpleBatchNorm(3)
x = np.random.randn(10, 3) # 10个样本,3个特征
out = bn.forward(x)
print(f"BatchNorm输出形状: {out.shape}")
应用2:对角优势矩阵与收敛性
在优化问题中,如果Hessian矩阵是对角占优的(对角元素绝对值大于非对角元素之和),梯度下降更容易收敛。
∣Hii∣>∑j≠i∣Hij∣|H_{ii}| > \sum_{j \neq i} |H_{ij}|∣Hii∣>j=i∑∣Hij∣
这就是为什么有时候加正则化(给对角线加值)能帮助训练稳定。
应用3:稀疏矩阵------对角矩阵的表亲
在图神经网络(GNN)中,邻接矩阵通常是稀疏的:
A=[0100101101000100]A = \begin{bmatrix} 0 & 1 & 0 & 0 \\ 1 & 0 & 1 & 1 \\ 0 & 1 & 0 & 0 \\ 0 & 1 & 0 & 0 \end{bmatrix}A= 0100101101000100
度矩阵DDD是对角矩阵,DiiD_{ii}Dii是节点iii的度(边的数量):
D=[1000030000100001]D = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 3 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}D= 1000030000100001
归一化的拉普拉斯矩阵:L=I−D−1/2AD−1/2L = I - D^{-1/2}AD^{-1/2}L=I−D−1/2AD−1/2
python
# 图的邻接矩阵
A = np.array([
[0, 1, 0, 0],
[1, 0, 1, 1],
[0, 1, 0, 0],
[0, 1, 0, 0]
])
# 度矩阵(对角矩阵)
degree = A.sum(axis=1)
D = np.diag(degree)
print(f"度矩阵:\n{D}")
# 归一化
D_inv_sqrt = np.diag(1.0 / np.sqrt(degree))
L_norm = np.eye(4) - D_inv_sqrt @ A @ D_inv_sqrt
print(f"归一化拉普拉斯矩阵:\n{L_norm}")
第五层:关键洞察
单位矩阵是静止的锚点,零矩阵是消散的终点,对角矩阵是独立的哲学。
特殊矩阵不只是数学上的巧合,它们代表了线性世界的极端与纯粹。在复杂的神经网络中,当我们加入单位矩阵,是在说"保留一些原始信息";当我们初始化为零,是在说"从空白开始";当我们构造对角矩阵,是在说"每个维度都是独立的故事"。
四、矩阵的深层魔法:为什么它是ML的核心语言?
从计算效率说起
你可能会问:为什么非要用矩阵?我能不能就用for循环一个个算?
答案是:可以,但你的GPU会哭。
现代深度学习之所以能训练亿级参数的模型,核心原因之一就是矩阵运算可以高度并行化。GPU上有成千上万个核心,它们可以同时计算矩阵乘法中的不同元素。
python
import time
# 用循环计算矩阵乘法(慢!)
def matmul_loop(A, B):
m, n = A.shape
n2, p = B.shape
assert n == n2, "维度不匹配"
C = np.zeros((m, p))
for i in range(m):
for j in range(p):
for k in range(n):
C[i, j] += A[i, k] * B[k, j]
return C
# 生成测试数据
A = np.random.randn(200, 300)
B = np.random.randn(300, 250)
# 测试循环版本
start = time.time()
C_loop = matmul_loop(A, B)
time_loop = time.time() - start
# 测试numpy版本(调用优化的BLAS库)
start = time.time()
C_numpy = A @ B
time_numpy = time.time() - start
print(f"循环版本耗时: {time_loop:.4f}秒")
print(f"NumPy版本耗时: {time_numpy:.6f}秒")
print(f"加速比: {time_loop/time_numpy:.1f}x")
# 通常能看到几百倍的差异!
矩阵视角:批量思维
深度学习的另一个核心思想是批量处理(Batch Processing)。我们不是一次处理一个样本,而是一批一起处理:
X=[---x⃗1T------x⃗2T---⋮---x⃗mT---]m×nX = \begin{bmatrix} --- & \vec{x}_1^T & --- \\ --- & \vec{x}_2^T & --- \\ & \vdots & \\ --- & \vec{x}m^T & --- \end{bmatrix}{m \times n}X= ---------x 1Tx 2T⋮x mT--------- m×n
每一行是一个样本,每一列是一个特征。一次矩阵乘法Y=XWY = XWY=XW,就同时完成了mmm个样本的前向传播。
python
# 批量处理示例
batch_size = 32
input_dim = 784
output_dim = 10
# 权重矩阵
W = np.random.randn(input_dim, output_dim) * 0.01
b = np.zeros((1, output_dim))
# 批量输入(32个样本)
X = np.random.randn(batch_size, input_dim)
# 一次矩阵乘法完成所有样本的计算
Y = X @ W + b
print(f"输入形状: {X.shape}") # (32, 784)
print(f"权重形状: {W.shape}") # (784, 10)
print(f"输出形状: {Y.shape}") # (32, 10)
print("一次计算完成32个样本的前向传播!")
矩阵分解:看透本质的X光
很多高级技术都基于矩阵分解,比如:
SVD(奇异值分解):
A=UΣVTA = U\Sigma V^TA=UΣVT
把任何矩阵分解成"旋转-缩放-旋转"三步。PCA、推荐系统都用它。
特征值分解:
A=QΛQ−1A = Q\Lambda Q^{-1}A=QΛQ−1
揭示矩阵的"主方向"和"能量分布"。
QR分解:
A=QRA = QRA=QR
在数值稳定性和Gram-Schmidt正交化中很重要。
这些分解不是数学游戏,它们是压缩信息、降维、去噪的利器。
python
# SVD示例:图像压缩
from scipy import misc
import matplotlib.pyplot as plt
# 加载灰度图像(或创建随机矩阵模拟)
img = np.random.randint(0, 256, size=(100, 100)).astype(float)
# SVD分解
U, s, Vt = np.linalg.svd(img, full_matrices=False)
print(f"U形状: {U.shape}") # (100, 100)
print(f"s形状: {s.shape}") # (100,) 奇异值
print(f"Vt形状: {Vt.shape}") # (100, 100)
# 重建:只保留前k个奇异值
def reconstruct(U, s, Vt, k):
return U[:, :k] @ np.diag(s[:k]) @ Vt[:k, :]
# 用不同数量的奇异值重建
k_values = [5, 10, 20, 50]
for k in k_values:
img_k = reconstruct(U, s, Vt, k)
error = np.linalg.norm(img - img_k) / np.linalg.norm(img)
print(f"保留{k}个奇异值,重建误差: {error:.3%}")
五、实战案例:手写一个简单的神经网络层
说了这么多理论,咱们来点实战的。下面用纯numpy写一个全连接层,看看矩阵运算怎么支撑起整个网络。
python
import numpy as np
class DenseLayer:
"""全连接层"""
def __init__(self, input_dim, output_dim, activation='relu'):
# He初始化(针对ReLU)
self.W = np.random.randn(input_dim, output_dim) * np.sqrt(2.0 / input_dim)
self.b = np.zeros((1, output_dim))
self.activation = activation
# 缓存(用于反向传播)
self.X = None
self.Z = None
self.A = None
# 梯度
self.dW = None
self.db = None
def forward(self, X):
"""前向传播
X: (batch_size, input_dim)
返回: (batch_size, output_dim)
"""
self.X = X
# 线性变换:Z = XW + b
self.Z = X @ self.W + self.b # 矩阵乘法!
# 激活函数
if self.activation == 'relu':
self.A = np.maximum(0, self.Z)
elif self.activation == 'sigmoid':
self.A = 1 / (1 + np.exp(-self.Z))
elif self.activation == 'linear':
self.A = self.Z
else:
raise ValueError(f"未知激活函数: {self.activation}")
return self.A
def backward(self, dL_dA, learning_rate=0.01):
"""反向传播
dL_dA: 损失对输出的梯度 (batch_size, output_dim)
返回: 损失对输入的梯度 (batch_size, input_dim)
"""
batch_size = self.X.shape[0]
# 激活函数的梯度
if self.activation == 'relu':
dA_dZ = (self.Z > 0).astype(float)
elif self.activation == 'sigmoid':
dA_dZ = self.A * (1 - self.A)
elif self.activation == 'linear':
dA_dZ = np.ones_like(self.Z)
else:
raise ValueError(f"未知激活函数: {self.activation}")
# 链式法则:dL/dZ = dL/dA * dA/dZ
dL_dZ = dL_dA * dA_dZ
# 计算梯度(注意矩阵转置!)
self.dW = self.X.T @ dL_dZ / batch_size # (input_dim, batch_size) @ (batch_size, output_dim)
self.db = np.sum(dL_dZ, axis=0, keepdims=True) / batch_size
# 传递给前一层的梯度
dL_dX = dL_dZ @ self.W.T # (batch_size, output_dim) @ (output_dim, input_dim)
# 更新参数
self.W -= learning_rate * self.dW
self.b -= learning_rate * self.db
return dL_dX
def __repr__(self):
return f"DenseLayer(W: {self.W.shape}, activation={self.activation})"
# 测试
np.random.seed(42)
# 创建两层网络
layer1 = DenseLayer(4, 8, activation='relu')
layer2 = DenseLayer(8, 3, activation='sigmoid')
# 生成假数据
X = np.random.randn(5, 4) # 5个样本,4个特征
y_true = np.array([[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
[1, 0, 0],
[0, 1, 0]])
# 前向传播
print("=== 前向传播 ===")
h1 = layer1.forward(X)
print(f"第一层输出形状: {h1.shape}")
y_pred = layer2.forward(h1)
print(f"第二层输出(预测):\n{y_pred}")
# 计算损失(简单的MSE)
loss = np.mean((y_pred - y_true) ** 2)
print(f"损失: {loss:.4f}")
# 反向传播
print("\n=== 反向传播 ===")
dL_dy = 2 * (y_pred - y_true) / y_true.shape[0]
dL_dh1 = layer2.backward(dL_dy, learning_rate=0.1)
print(f"第二层梯度dW形状: {layer2.dW.shape}")
dL_dX = layer1.backward(dL_dh1, learning_rate=0.1)
print(f"第一层梯度dW形状: {layer1.dW.shape}")
# 再做一次前向传播,看损失是否下降
h1_new = layer1.forward(X)
y_pred_new = layer2.forward(h1_new)
loss_new = np.mean((y_pred_new - y_true) ** 2)
print(f"\n更新后的损失: {loss_new:.4f}")
print(f"损失下降: {loss - loss_new:.6f}")
关键观察:
- 前向传播:两次矩阵乘法
X @ W - 反向传播:两次转置矩阵乘法
X.T @ dZ和dZ @ W.T - 整个神经网络就是矩阵的接力赛
六、常见陷阱与调试技巧
陷阱1:维度不匹配
python
# 错误示范
A = np.random.randn(3, 4)
B = np.random.randn(5, 6)
try:
C = A @ B
except ValueError as e:
print(f"错误: {e}")
print(f"A形状: {A.shape}, B形状: {B.shape}")
print(f"A的列数({A.shape[1]})必须等于B的行数({B.shape[0]})")
调试技巧 :在每次矩阵运算前,用print或assert检查形状:
python
def safe_matmul(A, B):
assert A.shape[1] == B.shape[0], \
f"维度不匹配: ({A.shape}) @ ({B.shape})"
return A @ B
陷阱2:行向量 vs 列向量
python
# 行向量(1, n)
row_vec = np.array([[1, 2, 3]])
print(f"行向量形状: {row_vec.shape}") # (1, 3)
# 列向量(n, 1)
col_vec = np.array([[1], [2], [3]])
print(f"列向量形状: {col_vec.shape}") # (3, 1)
# 一维数组(n,) - 容易混淆!
vec = np.array([1, 2, 3])
print(f"一维数组形状: {vec.shape}") # (3,)
# 转置行为不同
print(f"行向量转置: {row_vec.T.shape}") # (3, 1)
print(f"一维数组转置: {vec.T.shape}") # (3,) 没变!
建议:在神经网络中,统一使用二维数组,明确是行还是列。
陷阱3:广播(Broadcasting)的副作用
python
# numpy的广播很方便,但有时会掩盖错误
A = np.random.randn(3, 4)
b = np.random.randn(4) # 一维数组
# 这能工作,但可能不是你想要的
C = A + b # b会自动扩展成(1, 4),然后广播到(3, 4)
print(f"A + b的形状: {C.shape}") # (3, 4)
# 如果b是列向量呢?
b_col = np.random.randn(3, 1)
D = A + b_col # b_col广播到(3, 4)
print(f"A + b_col的形状: {D.shape}") # (3, 4)
# 在神经网络中要小心偏置的形状!
陷阱4:转置的连锁反应
python
# 在反向传播中,忘记转置是常见错误
X = np.random.randn(32, 784) # batch_size=32, input_dim=784
W = np.random.randn(784, 128) # output_dim=128
# 前向
Z = X @ W # (32, 784) @ (784, 128) = (32, 128) ✓
# 假设我们有梯度dL_dZ
dL_dZ = np.random.randn(32, 128)
# 反向传播:计算dL_dW
# 错误写法:
# dL_dW = X @ dL_dZ # (32, 784) @ (32, 128) 维度不匹配!
# 正确写法:
dL_dW = X.T @ dL_dZ # (784, 32) @ (32, 128) = (784, 128) ✓
print(f"梯度dL_dW的形状: {dL_dW.shape}")
print(f"应该与W形状相同: {W.shape}")
assert dL_dW.shape == W.shape, "梯度形状必须和参数形状一致!"
七、进阶话题:向量化思维
什么是向量化?
向量化 (Vectorization)是指用矩阵/向量运算替代循环的编程思想。在Python中,向量化的代码不仅简洁,而且快得多。
python
import time
# 任务:计算1000个数的平方和
n = 1000000
# 方法1:循环(慢)
data = list(range(n))
start = time.time()
result_loop = 0
for x in data:
result_loop += x ** 2
time_loop = time.time() - start
# 方法2:向量化(快)
data_vec = np.arange(n)
start = time.time()
result_vec = np.sum(data_vec ** 2)
time_vec = time.time() - start
print(f"循环结果: {result_loop}, 耗时: {time_loop:.4f}秒")
print(f"向量化结果: {result_vec}, 耗时: {time_vec:.4f}秒")
print(f"加速比: {time_loop / time_vec:.1f}x")
向量化的典型模式
模式1:逐元素操作
python
# 不好:循环
X = np.random.randn(1000, 100)
Y = np.zeros_like(X)
for i in range(X.shape[0]):
for j in range(X.shape[1]):
Y[i, j] = np.tanh(X[i, j])
# 好:向量化
Y_vec = np.tanh(X)
模式2:条件操作
python
# ReLU激活函数
# 不好:
def relu_loop(X):
Y = np.zeros_like(X)
for i in range(X.shape[0]):
for j in range(X.shape[1]):
Y[i, j] = max(0, X[i, j])
return Y
# 好:
def relu_vec(X):
return np.maximum(0, X)
X = np.random.randn(100, 50)
assert np.allclose(relu_loop(X), relu_vec(X))
模式3:归约操作
python
# 计算每行的和
# 不好:
row_sums_loop = []
for i in range(X.shape[0]):
row_sum = 0
for j in range(X.shape[1]):
row_sum += X[i, j]
row_sums_loop.append(row_sum)
# 好:
row_sums_vec = X.sum(axis=1)
print(f"向量化结果形状: {row_sums_vec.shape}")
实战:向量化的Softmax
Softmax是神经网络常用的激活函数:
softmax(zi)=ezi∑jezj\text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{j} e^{z_j}}softmax(zi)=∑jezjezi
python
def softmax_loop(Z):
"""未向量化的Softmax"""
result = np.zeros_like(Z)
for i in range(Z.shape[0]): # 对每个样本
exp_z = np.array([np.exp(Z[i, j]) for j in range(Z.shape[1])])
result[i, :] = exp_z / np.sum(exp_z)
return result
def softmax_vec(Z):
"""向量化的Softmax"""
# 数值稳定性:减去最大值
Z_shifted = Z - np.max(Z, axis=1, keepdims=True)
exp_Z = np.exp(Z_shifted)
return exp_Z / np.sum(exp_Z, axis=1, keepdims=True)
# 测试
Z = np.random.randn(100, 10)
start = time.time()
result_loop = softmax_loop(Z)
time_loop = time.time() - start
start = time.time()
result_vec = softmax_vec(Z)
time_vec = time.time() - start
print(f"循环版本耗时: {time_loop:.4f}秒")
print(f"向量化版本耗时: {time_vec:.4f}秒")
print(f"结果相同? {np.allclose(result_loop, result_vec)}")
# 验证概率性质:每行和为1
print(f"每行和为1? {np.allclose(result_vec.sum(axis=1), 1.0)}")
八、总结:矩阵与向量的哲学
向量是信息的原子,它把一堆零散的数字组织成一个有序的整体。一个词向量不只是300个数字,它是语义空间中的一个坐标;一个图像向量不只是784个像素,它是视觉世界的数字化身。
矩阵是变换的蓝图,它定义了如何从一个空间跳到另一个空间。神经网络的每一层,都是一次空间的折叠与展开、压缩与投影。学习的过程,就是在寻找最佳的变换序列。
转置是视角的切换 ,它让我们从"按样本看"变成"按特征看",从"前向传播"变成"反向传播"。一个T^TT符号,承载着信息流向的反转。
特殊矩阵是纯粹的本质,单位矩阵说"我保持原样",零矩阵说"我归于虚无",对角矩阵说"我独立自主"。它们是复杂世界中的简单真理。
最后送你一句话:矩阵不是冰冷的数字,它是信息流动的河床,是智能涌现的舞台。 当你在调试模型、查看梯度、分析特征时,记得你正在操控的是一个个向量和矩阵------它们是连接数据与智能的桥梁。
掌握了矩阵,你就掌握了深度学习的语法;理解了向量,你就理解了数据的本质。接下来的学习中,无论是卷积、循环、注意力,还是优化、正则、归一化,它们的底层都逃不出矩阵运算的魔掌。
所以别怕这些数字方阵,它们是你最好的朋友。
附录:速查表
矩阵维度速记
| 操作 | 输入维度 | 输出维度 |
|---|---|---|
| 矩阵乘法 | (m×n)(m \times n)(m×n), (n×p)(n \times p)(n×p) | (m×p)(m \times p)(m×p) |
| 转置 | (m×n)(m \times n)(m×n) | (n×m)(n \times m)(n×m) |
| 向量点积 | (n×1)(n \times 1)(n×1), (n×1)(n \times 1)(n×1) | (1×1)(1 \times 1)(1×1) 标量 |
| 外积 | (m×1)(m \times 1)(m×1), (n×1)(n \times 1)(n×1) | (m×n)(m \times n)(m×n) |
NumPy速查
python
# 创建
np.array([1, 2, 3]) # 一维数组
np.zeros((3, 4)) # 零矩阵
np.ones((2, 3)) # 全1矩阵
np.eye(5) # 单位矩阵
np.diag([1, 2, 3]) # 对角矩阵
np.random.randn(3, 4) # 随机矩阵(标准正态)
# 运算
A + B # 加法
A @ B # 矩阵乘法
A * B # 逐元素乘法(Hadamard积)
A.T # 转置
np.dot(a, b) # 点积/矩阵乘法
np.linalg.norm(v) # 向量范数
# 形状操作
A.shape # 查看形状
A.reshape(2, 6) # 改变形状
A.flatten() # 展平成一维
A.T # 转置
# 索引
A[0, :] # 第0行
A[:, 2] # 第2列
A[1:3, :] # 第1-2行(切片)