《动手学深度学习》(d2l-zh)线性代数基础:标量、向量、矩阵、张量与范数的数学符号和代码对照指南
《动手学深度学习》(d2l-zh)线性代数基础:标量、向量、矩阵、张量与范数的数学符号和代码对照指南
线性代数是为深度学习模型提供数学"骨架"的基础语言。本文以《动手学深度学习》中文版(d2l-zh)预备知识章节中线性代数为核心,系统讲解标量、向量、矩阵、张量四种基本数学对象,以及规约、点积、矩阵乘法、范数等核心运算,并给出它们在 MXNet、PyTorch、TensorFlow 与 PaddlePaddle 四套框架下的对应实现。读完本文,你将能够用张量 API 完成深度学习中最常用的线性代数操作(求和、求均值、内积、矩阵乘法、范数计算),并理解它们在后文线性回归、多层感知机等模型中的实际调用方式。
为什么深度学习必须先学线性代数
在预备知识章节中,ndarray 一节解决了"如何存储和操作数据"的问题,而本文要解决的则是"如何用数学语言描述数据和模型"的问题。机器学习中的数据通常表现为表格:行对应样本、列对应属性,而线性代数正是处理这类表格化数据最自然的一套数学工具。深度学习模型(例如后文的线性回归从零实现)本质上就是反复地对张量做线性运算,因此本节介绍的每一种对象和运算,都会在后续章节的模型代码中反复出现。
本文约定:数学符号与代码实现一一对照。你在公式中见到的每个对象,都可以在代码中找到对应的张量写法。
标量:只有一个数值的数学对象
如果你曾经在餐厅结账,就已经掌握了最基础的线性代数——数字间的相加与相乘。严格来说,仅包含一个数值的量被称为标量(scalar)。例如,北京的温度为 (华氏度),要转换为摄氏度可以计算 ,并将 赋为 。在这个式子中,、、 都是标量值,而 和 是变量(variable),代表未知的标量值。
在数学表示法中,标量变量用普通小写字母表示(如 、、),全体(连续)实值标量构成的空间记作 。表达式 是" 是一个实值标量"的正式写法,其中符号 读作"属于",表示集合成员关系;例如 表示 和 只能取 或 。
标量在代码中由一个只含单个元素的张量表示。下面的代码实例化两个标量,并执行加法、乘法、除法和指数运算:
# MXNet
from mxnet import np, npx
npx.set_np()
x = np.array(3.0)
y = np.array(2.0)
x + y, x * y, x / y, x ** y
# PyTorch
import torch
x = torch.tensor([3.0])
y = torch.tensor([2.0])
x + y, x * y, x / y, x**y
# TensorFlow
import tensorflow as tf
x = tf.constant([3.0])
y = tf.constant([2.0])
x + y, x * y, x / y, x**y
# PaddlePaddle
import warnings
warnings.filterwarnings(action='ignore')
import paddle
x = paddle.to_tensor([3.0])
y = paddle.to_tensor([2.0])
x + y, x * y, x / y, x**y
需要注意,各框架标量张量的构造方式略有差异:MXNet 与 TensorFlow 使用 np.array(3.0) / tf.constant(3.0),PyTorch 的 torch.tensor([3.0]) 和 PaddlePaddle 的 paddle.to_tensor([3.0]) 通常保留了一个长度为 1 的轴。这不影响运算结果,但会影响 shape 的输出形态。
向量:标量值组成的列表
向量(vector)可以被视为标量值组成的列表,这些标量值被称为向量的元素(element)、条目(entry)或分量(component)。当向量表示数据集中的样本时,分量具有现实意义:例如预测贷款违约风险时,可以将每个申请人与一个向量关联,其分量为收入、工作年限、过往违约次数等因素;研究心脏病风险时,可以用向量表示每位患者的最近生命体征、胆固醇水平、每天运动时长等。数学上,向量通常记为粗体小写字母(如 、、)。
在代码中,向量用一维张量表示,其长度受机器内存限制。用 arange(4) 创建包含 4 个连续整数的向量:
# MXNet
x = np.arange(4)
x
# PyTorch
x = torch.arange(4)
x
# TensorFlow
x = tf.range(4)
x
# PaddlePaddle
x = paddle.arange(4)
x
用下标访问元素
可以用下标引用向量的任一元素: 表示向量 的第 个元素。注意元素 本身是标量,因此引用它时不加粗。大量文献(以及本书)默认向量的方向为列向量,数学上写作:
其中 是向量的元素。在代码中,通过张量索引访问任一元素:
# 以 PyTorch 为例,其余框架写法相同
x[3]
长度、维度与形状
向量是一个数字数组,就像每个数组都有长度一样,每个向量也有长度。若向量 由 个实值标量组成,记作 ;向量的长度通常称为向量的维度(dimension)。
与普通 Python 数组一样,可以调用内置 len() 函数访问张量长度;当张量表示只有一个轴的向量时,还可以通过 .shape 属性访问长度。shape 是一个元素组(tuple),列出张量沿每个轴的长度:
# 以 PyTorch 为例
len(x)
x.shape
这里有一个常被混淆的概念需要澄清:向量或轴的维度指其长度(元素数量),而张量的维度指张量拥有的轴数。因此"某个轴的维数"其实就是这个轴的长度——例如形状为 (2, 3, 4) 的张量有 3 个轴,其中轴 0 的维度是 2。
矩阵:把向量推广到二阶
正如向量把标量从零阶推广到一阶,矩阵(matrix)把向量从一阶推广到二阶。矩阵通常用粗体大写字母表示(如 、、),代码中对应具有两个轴的张量。
数学上, 表示由 行 列实值标量组成的矩阵,元素 位于第 行第 列:
矩阵 的 shape 为 或 ;当行数与列数相等时即为方阵(square matrix)。创建 矩阵时,只需在实例化张量的函数中指定包含两个分量的形状:
# MXNet
A = np.arange(20).reshape(5, 4)
A
# PyTorch
A = torch.arange(20).reshape(5, 4)
A
# TensorFlow
A = tf.reshape(tf.range(20), (5, 4))
A
# PaddlePaddle
A = paddle.arange(20).reshape([5, 4])
A
矩阵元素 通过行下标 和列下标 访问,记作 ;当需要区分下标时用逗号隔开,例如 、。
转置与对称矩阵
交换矩阵的行和列得到转置(transpose)。若 ,则 ;因此 的转置是 矩阵:
代码中访问矩阵的转置:
# MXNet / PyTorch
A.T
# TensorFlow
tf.transpose(A)
# PaddlePaddle
paddle.transpose(A, [1, 0])
方阵有一种特殊类型:对称矩阵(symmetric matrix)满足 。下面的矩阵 B 沿主对角线对称:
# PyTorch
B = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
B
用 B == B.T 逐元素比较,可以得到一个全为 True 的布尔张量,从而验证对称性。
矩阵作为表格数据的组织方式
矩阵是极其有用的数据结构,因为它能组织具有不同变化模态的数据:行对应不同的数据样本(如不同的房子),列对应不同的属性(如面积、价格),这与电子表格或 pandas 章节中数据表的组织方式一致。因此,尽管单个向量的默认方向是列向量,但在表示表格数据集的矩阵中,惯例是把每个样本视为矩阵中的行向量。这一约定意义重大——沿张量最外层轴可以访问或枚举小批量数据样本,这正是后文深度学习代码中"按批次遍历数据"的基础。
张量:任意轴数的通用数据结构
正如向量推广标量、矩阵推广向量,张量(tensor,此处指代数对象)给出了描述任意轴数 维数组的通用方式:向量是一阶张量,矩阵是二阶张量。张量用特殊字体的大写字母表示(如 、、),索引机制与矩阵类似(如 、)。
张量在处理图像时尤为重要——图像是带有 3 个轴的 维数组,分别对应高度、宽度和通道轴(堆叠红、绿、蓝三个颜色通道)。例如创建一个形状为 (2, 3, 4) 的三阶张量:
# PyTorch
X = torch.arange(24).reshape(2, 3, 4)
X
# TensorFlow
X = tf.reshape(tf.range(24), (2, 3, 4))
X
# PaddlePaddle
X = paddle.arange(24).reshape([2, 3, 4])
X
张量算术的基本性质:元素级运算与 Hadamard 积
标量、向量、矩阵和任意轴数的张量有一些常用性质。从元素级运算(elementwise operation)的定义可知:
- 任何元素级一元运算都不会改变操作数的形状;
- 给定两个形状相同的张量,任何二元元素级运算的结果仍是同形状的张量。
例如,两个同形状矩阵相加就是逐元素相加:
# PyTorch
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone() # 通过分配新内存将 A 的副本赋给 B
A, A + B
# TensorFlow
A = tf.reshape(tf.range(20, dtype=tf.float32), (5, 4))
B = A # 没有克隆,B 与 A 共享内存
A, A + B
这里可以注意各框架在"复制张量"上的差异:PyTorch 用 A.clone() 显式分配新内存;TensorFlow 中 B = A 只是让两个名字指向同一张量。MXNet 则使用 A.copy()。在需要避免共享内存的场合(如梯度累积),这个区别很关键。
两个矩阵的元素级乘法被称为 Hadamard 积(Hadamard product),记作 。对于矩阵 和 :
在代码中直接使用 A * B 即可实现。张量与标量相加或相乘也不会改变张量的形状,标量会逐元素作用于操作数:
# PyTorch
a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
上述性质在深度学习代码中无处不在:例如神经网络某一层对输入做线性变换后再加偏置,本质上就是矩阵乘法加标量广播。
规约:求和与均值
对任意张量,一个常用的操作是计算所有元素之和。数学上用 符号表达:向量 (长度 )的元素和写作 ; 矩阵 的元素和写作 。
# PyTorch
x = torch.arange(4, dtype=torch.float32)
x, x.sum()
# TensorFlow
x = tf.range(4, dtype=tf.float32)
x, tf.reduce_sum(x)
# PaddlePaddle
x = paddle.arange(4, dtype=paddle.float32)
x, x.sum()
默认情况下,求和函数会沿所有轴把张量规约(reduce)成标量;也可以通过 axis 参数指定沿哪些轴规约。以矩阵为例:
A.sum(axis=0)沿行维(轴 0)求和,即把每一列的所有行元素相加,输出形状中轴 0 的维度消失;A.sum(axis=1)沿列维(轴 1)求和,输出形状中轴 1 的维度消失;A.sum(axis=[0, 1])同时沿两个轴求和,等价于A.sum()。
# PyTorch
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
A_sum_axis1 = A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape
A.sum(axis=[0, 1]) # 与 A.sum() 相同
TensorFlow 中对应 tf.reduce_sum(A, axis=0)、tf.reduce_sum(A, axis=[0, 1]);PaddlePaddle 与 PyTorch 写法一致。
**均值(mean / average)**是另一相关量,由总和除以元素总数得到,等价于 A.sum() / A.numel()(PyTorch 用 numel() 统计元素总数,MXNet 用 A.size,TensorFlow 用 tf.size(A).numpy()):
# PyTorch
A.mean(), A.sum() / A.numel()
# 指定轴
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
# TensorFlow
tf.reduce_mean(A), tf.reduce_sum(A) / tf.size(A).numpy()
tf.reduce_mean(A, axis=0), tf.reduce_sum(A, axis=0) / A.shape[0]
注意:A.shape[0] 是轴 0 的长度(行数),所以 A.sum(axis=0) / A.shape[0] 得到的是每列均值,恰好与 A.mean(axis=0) 一致。
非规约求和:keepdims 与 cumsum
有时我们希望求和/求均值时保持轴数不变,此时使用 keepdims=True:
# PyTorch
sum_A = A.sum(axis=1, keepdims=True)
sum_A
由于 sum_A 在按行求和后仍保留两个轴,可以借助广播(broadcasting)直接做 A / sum_A,得到每一行元素占该行总和的归一化比例——这是数据预处理中常见的"行归一化"操作。
另外,cumsum(累积求和)函数沿指定轴逐步累加,且不会规约掉任何轴。例如 A.cumsum(axis=0) 逐行累积,输出的第 行是原矩阵前 行的元素和:
# PyTorch
A.cumsum(axis=0)
# TensorFlow
tf.cumsum(A, axis=0)
点积:最基础的向量运算
如果线性代数只能做元素级运算、求和与求平均,它就不配拥有独立章节。点积(dot product)是其中最基础的运算之一:给定两个向量 ,其点积 (或 )是相同位置元素乘积之和:
# MXNet
y = np.ones(4)
x, y, np.dot(x, y)
# PyTorch
y = torch.ones(4, dtype = torch.float32)
x, y, torch.dot(x, y)
# TensorFlow
y = tf.ones(4, dtype=tf.float32)
x, y, tf.tensordot(x, y, axes=1)
点积也可以等价地由"元素级乘法 + 求和"两步实现:x * y 后再 .sum()(TensorFlow 为 tf.reduce_sum(x * y))。这条等价关系在推导中经常被利用。
点积的应用场景非常广泛:
- 加权和:给定值向量 和权重向量 ,按权重对 加权求和可表示为点积 ;
- 加权平均:当权重非负且总和为 1()时,点积表达的就是加权平均;
- 余弦相似度:把两个向量归一化为单位长度后,点积等于它们夹角的余弦值(长度概念将在范数小节正式引入)。
矩阵-向量积:从 到 的线性变换
有了点积就能理解矩阵-向量积(matrix-vector product)。把矩阵 按行向量表示:
其中每个 是第 行的行向量。矩阵-向量积 就是长度为 的列向量,其第 个元素是点积 :
可以把乘以矩阵 看作一个把向量从 投影到 的变换。这类变换极其有用:旋转可以用方阵乘法表示;更重要的是,后文神经网络中每一层最密集的计算(给定上一层输出,计算本层输出)都可以用矩阵-向量积描述。
代码中,矩阵-向量积复用点积所用的 dot 函数;前提是矩阵 A 的列维(沿轴 1 的长度)必须与向量 x 的维度(长度)相同:
# MXNet
A.shape, x.shape, np.dot(A, x)
# PyTorch
A.shape, x.shape, torch.mv(A, x)
# TensorFlow
A.shape, x.shape, tf.linalg.matvec(A, x)
# PaddlePaddle
A.shape, x.shape, paddle.mv(A, x)
矩阵-矩阵乘法:m 次矩阵-向量积的组合
设 与 ,记 为 的第 个行向量, 为 的第 个列向量,则矩阵乘积 的每个元素 都是点积 :
矩阵乘法 可以理解为依次执行 次矩阵-向量积、再把结果拼接成 矩阵。下面用 5×4 的 A 与 4×3 的 B 相乘,得到 5×3 的矩阵:
# MXNet
B = np.ones(shape=(4, 3))
np.dot(A, B)
# PyTorch
B = torch.ones(4, 3)
torch.mm(A, B)
# TensorFlow
B = tf.ones((4, 3), tf.float32)
tf.matmul(A, B)
# PaddlePaddle
B = paddle.ones([4, 3])
paddle.matmul(A, B)
矩阵-矩阵乘法通常直接简称矩阵乘法(matrix multiplication),务必与上文 Hadamard 积(元素级乘法)区分开——前者是点积的组合,后者是逐元素的乘积。
范数:度量向量"大小"的算子
范数(norm)是线性代数中最有用的算子之一。它度量的是一个向量有多"大",这里的"大小"指分量幅值而非维度。向量范数是一个把向量映射为标量的函数 ,满足四条性质:
- 绝对齐次性:缩放所有元素 倍,范数按 缩放,;
- 三角不等式:;
- 非负性:;
- 正定性:零向量是唯一范数为 0 的向量,。
范数与距离度量关系密切。欧几里得距离(勾股定理)就是一种范数—— 范数。
范数是向量元素平方和的平方根(下标 2 常省略, 等价于 ):
代码中直接调用范数函数:
# MXNet
u = np.array([3, -4])
np.linalg.norm(u)
# PyTorch
u = torch.tensor([3.0, -4.0])
torch.norm(u)
# TensorFlow
u = tf.constant([3.0, -4.0])
tf.norm(u)
例如向量 的 范数为 。深度学习中更常用的是 范数的平方(例如作为正则项加入损失)。
范数是向量元素绝对值之和,相比 范数受离群值影响更小:
# MXNet / PyTorch
np.abs(u).sum()
torch.abs(u).sum()
# TensorFlow
tf.reduce_sum(tf.abs(u))
与 范数都是更一般 范数的特例:
与向量的 范数类似,矩阵的 Frobenius 范数是矩阵元素平方和的平方根:
它满足向量范数的全部性质,行为如同把矩阵拉直成向量后的 范数:
# PyTorch
torch.norm(torch.ones((4, 9)))
# TensorFlow
tf.norm(tf.ones((4, 9)))
范数与优化目标
范数概念为何如此重要?因为深度学习的核心是求解优化问题:最大化观测数据被分配的概率,最小化预测与真实观测之间的距离;或者为词、商品、新闻等实体学习向量表示,使相似实体距离最小、不相似实体距离最大。这些目标——深度学习算法除数据外最重要的组成部分——经常以范数的形式表达。理解范数,是理解损失函数与正则化(后文权重衰减等章节)的起点。
源码印证:线性代数运算在 d2l 工具库中的落地
本文介绍的运算并非孤立概念,而是直接构成了 d2l 工具库的核心 API。以 PyTorch 后端 d2l/torch.py 为例,工具库通过别名把框架原生算子统一为 d2l. 前缀接口:
d2l.normal = torch.normal(生成服从正态分布的随机张量);d2l.matmul = torch.matmul(矩阵乘法);d2l.reduce_sum = lambda x, *args, **kwargs: x.sum(*args, **kwargs)(带axis、keepdims等参数规约求和)。
在线性回归从零实现中,模型前向与损失函数正是基于本文的运算构建的(见 d2l/torch.py):
def linreg(X, w, b):
"""线性回归模型"""
return d2l.matmul(X, w) + b
def squared_loss(y_hat, y):
"""均方损失"""
return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2
其中 d2l.matmul(X, w) 正是矩阵-矩阵乘法:X 是形状为(样本数,特征数)的输入矩阵,w 是形状为(特征数,1)的权重列向量;(y_hat - y) ** 2 / 2 则是对张量做元素级平方运算,再配合规约求均值得到标量损失。可以看到,本节的全部数学对象与运算在真实模型代码中被原样复用——这也是本书"先讲数学、后写模型"编排逻辑的体现。
小结
- 标量、向量、矩阵和张量是线性代数的基本数学对象,分别有零个、一个、两个和任意数量的轴;
- 向量推广了标量,矩阵推广了向量;
- 元素级运算(包括 Hadamard 积 )与"张量加/乘标量"都不会改变张量的形状;
- 张量可以通过
sum和mean沿指定轴规约,也可用keepdims=True保持轴数,或用cumsum做非规约累积求和; - 点积是元素乘积之和,矩阵乘法可以看作多次矩阵-向量积的组合,两者都不是元素级运算;
- 深度学习常用 范数、 范数和 Frobenius 范数,它们常被用作优化目标或正则项。
练习
- 证明矩阵转置的转置是原矩阵:。
- 给定矩阵 和 ,证明转置之和等于和的转置:。
- 对任意方阵 , 是否总是对称矩阵?为什么?
- 本节定义了形状为 (2, 3, 4) 的张量
X,len(X)的输出是什么? - 对任意形状的张量
X,len(X)是否总对应X某个轴的长度?是哪个轴? - 运行
A / A.sum(axis=1)观察结果,分析原因(提示:结合广播机制思考形状不匹配时会发生什么)。 - 在曼哈顿的两个地点间穿行,需要覆盖的"距离"如何用坐标(即大道与街道数)表达?能否斜向穿行?
- 对形状为 (2, 3, 4) 的张量,分别沿轴 0、1、2 求和的输出形状是什么?
- 把一个 3 轴及以上的张量喂给
linalg.norm函数并观察输出,分析它对任意形状张量计算的是什么。
若想进一步探索,可在仓库的预备知识章节中继续阅读ndarray 章节(张量创建与索引基础)、pandas 章节(表格数据预处理),并参考 d2l/torch.py 中 matmul、reduce_sum 等统一接口的别名定义。