线性代数是深度学习的核心基础,理解其基本概念和运算对于掌握神经网络至关重要。本文将详细介绍线性代数中的关键内容,并结合PyTorch框架进行示例。
一、标量
想象一下你在餐厅支付账单时,你可能已经在无形中运用了线性代数的基本知识,比如加法和乘法。比如,北京的温度以华氏度表示为52°F,这个数字本身就是一个标量。如果你想把华氏度转换为更常见的摄氏度,你可以使用一个简单的数学公式:。
在这个公式中,所有的数值,比如华氏度和摄氏度,都是标量。符号通常用小写字母(如 x、y)表示,代表这些未知的数值。我们用一个符号空间来表示所有可能的实数标量,比如 R。而符号“属于”表示某个数在这个集合中,比如 x∈ R 意味着 x 是一个实数。
在数学中,标量可以用只有一个元素的张量来表示。下面的代码展示了如何在PyTorch中创建两个标量,并进行加法、乘法、除法和指数运算:
import torch
x = torch.tensor(3.0) # 创建标量3.0
y = torch.tensor(2.0) # 创建标量2.0
# 执行算术运算
result_add = x + y # 加法
result_mul = x * y # 乘法
result_div = x / y # 除法
result_pow = x ** y # 指数
print(result_add, result_mul, result_div, result_pow)
"""
tensor(5.) tensor(6.) tensor(1.5000) tensor(9.)
"""
运行这段代码,得到的结果分别是:5.0(加法),6.0(乘法),1.5(除法),和9.0(指数)。这些基本运算为理解更复杂的线性代数概念奠定了基础。
二、向量
向量可以看作是由标量值组成的列表,这些值称为元素或分量。在实际应用中,当向量表示数据集中的样本时,每个分量都具有重要意义。例如,如果我们在训练一个贷款违约风险预测模型,每个申请人可以用一个向量表示,其中的分量可能包括收入、工作年限、过往违约次数等。如果我们研究患者的心脏病发作风险,则可能用一个向量表示每个患者,分量包括最新的生命体征、胆固醇水平、每日运动时间等。
在数学中,向量通常用粗体小写字母(如 v、x、y)表示。向量可以用一维张量表示,其长度可以根据机器的内存限制变化。下面的代码示例展示了如何在PyTorch中创建一个向量:
import torch
x = torch.arange(4) # 创建一个包含0到3的向量
print(x) # 输出: tensor([0, 1, 2, 3])
我们可以通过下标访问向量的任一元素,例如 x[2] 引用第三个元素。值得注意的是,引用的元素是一个标量,因此不需要加粗。文献中通常将列向量视为默认形式。在数学上,向量可以表示为:
其中 是向量的元素。我们可以通过张量的索引来访问这些元素:
element = x[3]
print(element) # 输出:tensor(3)
长度、维度和形状
向量是数字的数组,每个数组都有一个长度,向量同样如此。如果我们想表示一个包含
𝑛 个实值标量的向量,可以写作 v 。向量的长度也被称为维度。
在Python中,我们可以使用内置的 len() 函数来获取向量的长度:
length = len(x) # 获取向量长度
print(length) # 输出: 4
当用张量表示一个向量(即只有一个轴时),我们也可以通过 .shape 属性访问长度。形状(shape)是一个包含元素数量的元组,对于一维张量,形状只有一个元素:
shape = x.shape
print(shape) # 输出: torch.Size([4])
需要注意的是,“维度”这个词在不同上下文中可能会产生混淆。这里我们明确:向量的维度指的是向量的长度,即元素的数量。而张量的维度则指的是张量的轴数。在这种意义上,某个轴的维度表示这个轴的长度。
三、矩阵
矩阵可以看作是将向量从一阶推广到二阶的结构。我们通常用粗体大写字母表示矩阵(例如 A、B、C)。在代码中,矩阵被表示为具有两个轴的张量。
在数学表示中,一个矩阵 𝐴 表示为一个 𝑚 × 𝑛 的实值标量的集合,其中 𝑚 是行数,𝑛 是列数。矩阵的元素 位于第 𝑖 行第 𝑗 列:
对于任意矩阵 𝐴,它的形状为 (𝑚, 𝑛)。当矩阵的行数与列数相等时,称其为方阵。通过指定矩阵的行数和列数,我们可以用代码来创建矩阵。例如,在PyTorch中:
# 创建一个5行4列的矩阵
A = torch.arange(20).reshape(5, 4)
print(A)
"""
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19]])
"""
我们可以通过行索引和列索引来访问矩阵的元素,例如 A[1, 2] 访问的是矩阵第二行第三列的元素。注意,矩阵中的元素是标量,所以在引用时不需要加粗。若不特别标注,矩阵通常以行优先方式表示。
element = A[1, 2]
print(element) # 输出:tensor(6)
矩阵的转置
当我们交换矩阵的行和列时,得到的结果称为矩阵的转置,用 表示。例如,对于上面的矩阵 𝐴,其转置可以在代码中计算:
print(A.T)
"""
tensor([[ 0, 4, 8, 12, 16],
[ 1, 5, 9, 13, 17],
[ 2, 6, 10, 14, 18],
[ 3, 7, 11, 15, 19]])
"""
对称矩阵
一个特殊类型的矩阵是对称矩阵,它等于其转置,即 。例如:
import torch
B = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
print(B)
"""
tensor([[1, 2, 3],
[2, 0, 4],
[3, 4, 5]])
"""
print(B.T)
"""
tensor([[1, 2, 3],
[2, 0, 4],
[3, 4, 5]])
"""
print(B == B.T)
"""
tensor([[True, True, True],
[True, True, True],
[True, True, True]])
"""
矩阵在实际中的应用
矩阵是非常有用的数据结构,可以用来组织不同模式的数据。例如,矩阵的行可以表示不同的房屋(数据样本),而列可以表示这些房屋的属性。如果你曾经使用过电子表格软件,那么对这种结构应该比较熟悉。
四、张量:多维数据的扩展
就像向量是标量的推广,矩阵是向量的推广一样,张量是我们处理多维数据的更广泛工具。张量可以有任意数量的轴,因此,它被视为一种多维数组的通用表示方法。
- 向量 是一阶张量,表示一组数排成的列表。
- 矩阵 是二阶张量,表示二维数据表格。
- 高阶张量 是超过两轴的数据结构。
我们用特殊的大写字母表示张量(例如 𝑋, 𝑌, 𝑍)。与矩阵类似,张量中的元素可以通过多个索引(例如 )来引用。
张量在图像处理中的应用:当处理图像时,张量尤为重要。图像通常以 三维数组 形式表示,其中:
- 高度(图片的垂直尺寸)
- 宽度(图片的水平尺寸)
- 通道(颜色通道:红、绿、蓝)
例如,一张彩色图像可以表示为一个三维张量,每个通道对应于不同的颜色强度值。
创建和查看张量
让我们在 PyTorch 中创建一个简单的三维张量,形状为 2 × 3 × 4(2个“表”,每个“表”包含3行4列的数值):
import torch
# 创建一个形状为 (2, 3, 4) 的张量
X = torch.arange(24).reshape(2, 3, 4)
print(X)
"""
tensor([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]])
"""
张量的结构
- 第一维表示 2 个二维矩阵(或图像)。
- 第二维是矩阵的 3 行。
- 第三维是每行的 4 个元素。
这样,我们就构建了一个三维的张量,可以轻松处理各种维度的复杂数据。未来在图像、视频处理等任务中,张量将成为非常重要的工具。
五、张量算法的基本性质
无论是标量、向量、矩阵还是任意维度的张量,它们都有一些 按元素操作 的特性。换句话说,当我们在张量上执行按元素的运算时,操作不会改变张量的形状,输出张量的形状与输入张量的形状一致。
一元运算
例如,对于一元操作(如对张量的每个元素进行加、减或取反操作),结果的形状与输入相同。
# 取反(Negation)
x = torch.tensor([1.0, -2.0, 3.0])
negated_x = -x
print(negated_x) # 输出:tensor([-1., 2., -3.])
# 指数(Exponential)
x = torch.tensor([0.0, 1.0, 2.0])
exp_x = torch.exp(x)
print(exp_x) # 输出:tensor([1.0000, 2.7183, 7.3891])
# 取绝对值(Absolute Value)
x = torch.tensor([-1.0, -2.0, 3.0])
abs_x = torch.abs(x)
print(abs_x) # 输出:tensor([1., 2., 3.])
二元运算
同样,任何两个形状相同的张量,在进行按元素的二元运算时,结果仍然会是一个相同形状的张量。一个常见的例子是矩阵的元素相加运算:
# 创建一个5x4的矩阵A,并克隆A到B
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone() # 复制A到B
print(A)
"""
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]])
"""
print(A + B) # 矩阵A和B相加
"""
tensor([[ 0., 2., 4., 6.],
[ 8., 10., 12., 14.],
[16., 18., 20., 22.],
[24., 26., 28., 30.],
[32., 34., 36., 38.]])
"""
Hadamard积
当我们对两个矩阵按元素相乘时,得到的运算称为 Hadamard积,记作 𝐴 ∘ 𝐵:
对于矩阵 𝐴 和 𝐵,它们的 Hadamard 积计算如下:
print(A * B)
"""
tensor([[ 0., 1., 4., 9.],
[ 16., 25., 36., 49.],
[ 64., 81., 100., 121.],
[144., 169., 196., 225.],
[256., 289., 324., 361.]])
"""
张量与标量的运算
将张量乘以或加上一个标量不会改变张量的形状,标量运算会应用到张量的每个元素上:
import torch
a = 2
X = torch.arange(24).reshape(2, 3, 4)
print(X)
"""
tensor([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]])
"""
# 标量加操作
print(a + X)
"""
tensor([[[ 2, 3, 4, 5],
[ 6, 7, 8, 9],
[10, 11, 12, 13]],
[[14, 15, 16, 17],
[18, 19, 20, 21],
[22, 23, 24, 25]]])
"""
# 标量乘操作
print(a * X)
"""
tensor([[[ 0, 2, 4, 6],
[ 8, 10, 12, 14],
[16, 18, 20, 22]],
[[24, 26, 28, 30],
[32, 34, 36, 38],
[40, 42, 44, 46]]])
"""
print((a * X).shape) # 输出:torch.Size([2, 3, 4])
六、降维
我们可以对任意张量进行的一个有用操作是计算其元素的和。数学表示法使用 Σ 符号表示求和。为了表示长度为 𝑛 的向量中元素的总和,可以记为:
import torch
x = torch.arange(4, dtype=torch.float32)
print(x) # tensor([0., 1., 2., 3.])
print(x.sum()) # tensor(6.)
我们可以表示任意形状张量的元素和。例如,矩阵 𝐴 中元素的和可以记为:
A = torch.arange(20).reshape(5, 4)
print(A)
"""
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19]])
"""
print(A.shape) # torch.Size([5, 4])
print(A.sum()) # tensor(190)
默认情况下,调用求和函数会沿所有轴降低张量的维度,使其变为一个标量。我们还可以指定张量沿哪一个轴来通过求和降低维度。以矩阵为例,为了通过求和所有行的元素来降维(轴0),可以在调用函数时指定 axis=0 或 dim=0。
A_sum_axis0 = A.sum(dim=0)
print(A_sum_axis0, A_sum_axis0.shape)
# tensor([40, 45, 50, 55]) torch.Size([4])
指定 axis=1 将通过汇总所有列的元素降维(轴1)。
A_sum_axis1 = A.sum(dim=1)
print(A_sum_axis1, A_sum_axis1.shape)
# tensor([ 6, 22, 38, 54, 70]) torch.Size([5])
沿着行和列对矩阵求和,等价于对矩阵的所有元素进行求和:
print(A.sum(dim=[0, 1])) # tensor(190),结果和A.sum()相同
一个与求和相关的量是平均值(mean或average)。我们通过将总和除以元素总数来计算平均值:
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
print(A)
"""
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]])
"""
print(A.mean()) # tensor(9.5000)
print(A.sum() / A.numel()) # tensor(9.5000)
同样,计算平均值的函数也可以沿指定轴降低张量的维度:
print(A.mean(dim=0)) # tensor([ 8., 9., 10., 11.])
print(A.sum(dim=0) / A.shape[0]) # tensor([ 8., 9., 10., 11.])
非降维求和
有时,在调用函数来计算总和或均值时,保持轴数不变会很有用:
"""
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]])
"""
sum_A = A.sum(dim=1, keepdim=True)
print(sum_A)
"""
tensor([[ 6.],
[22.],
[38.],
[54.],
[70.]])
"""
print(sum_A.shape) # torch.Size([5, 1])
由于 sum_A 在对每行进行求和后仍保持两个轴,我们可以通过广播将 𝐴 除以 sum_A:
print(A / sum_A)
"""
tensor([[0.0000, 0.1667, 0.3333, 0.5000],
[0.1818, 0.2273, 0.2727, 0.3182],
[0.2105, 0.2368, 0.2632, 0.2895],
[0.2222, 0.2407, 0.2593, 0.2778],
[0.2286, 0.2429, 0.2571, 0.2714]])
"""
如果我们想沿某个轴计算 𝐴 元素的累积总和,比如 axis=0(按行计算),可以调用 cumsum 函数:
"""
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]])
"""
print(A.cumsum(dim=0))
"""
tensor([[ 0., 1., 2., 3.],
[ 4., 6., 8., 10.],
[12., 15., 18., 21.],
[24., 28., 32., 36.],
[40., 45., 50., 55.]])
"""
七、点积
我们已经学习了按元素操作、求和及平均值。另一个最基本的操作之一是点积。给定两个向量 𝑥, 𝑦,它们的点积(dot product)(或 ⟨𝑥,𝑦⟩)是相同位置的按元素乘积的和:
import torch
x = torch.arange(4, dtype=torch.float32)
print(x) # tensor([0., 1., 2., 3.])
y = torch.ones(4, dtype=torch.float32)
print(y) # tensor([1., 1., 1., 1.])
print(torch.dot(x, y)) # tensor(6.)
注意,我们可以通过执行按元素乘法,然后进行求和来表示两个向量的点积:
print(x * y) # tensor([0., 1., 2., 3.])
print(torch.sum(x * y)) # tensor(6.)
点积在很多场合都很有用。 例如,给定一组由向量 𝑥 表示的值,和一组由 𝑤 表示的权重,𝑥 中的值根据权重 𝑤 的加权和,可以表示为点积 𝑥⋅𝑤。当权重为非负数且和为1(即 )时,点积表示加权平均(weighted average)。将两个向量规范化得到单位长度后,点积表示它们夹角的余弦。本节后面的内容将正式介绍长度(length)的概念。
八、矩阵-向量积
现在我们知道如何计算点积,可以开始理解矩阵-向量积(matrix-vector product)。 前面定义的矩阵 𝐴 和向量 𝑥。 让我们将矩阵 𝐴 用它的行向量表示:
其中每个 都是行向量,表示矩阵的第 𝑖 行。矩阵向量积 Ax 是一个长度为 𝑚 的列向量,其第 𝑖 个元素是点积 :
我们可以把一个矩阵乘法看作一个从 到 向量的转换。这些转换是非常有用的,例如可以用方阵的乘法来表示旋转。后续将讲到,我们也可以使用矩阵-向量积来描述在给定前一层的值时,求解神经网络每一层所需的复杂计算。
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
print(A, A.shape)
"""
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]]) torch.Size([5, 4])
"""
x = torch.arange(4, dtype=torch.float32)
print(x, x.shape) # tensor([0., 1., 2., 3.]) torch.Size([4])
print(torch.mv(A, x))
# tensor([ 14., 38., 62., 86., 110.])
九、矩阵乘法
在掌握点积和矩阵-向量积的知识后,矩阵-矩阵乘法(matrix-matrix multiplication)应该很简单。假设有两个矩阵 和 :
用行向量 表示矩阵 的第 行,并让列向量 作为矩阵 的第 列。要生成矩阵积 ,最简单的方法是考虑 的行向量和 的列向量:
当我们简单地将每个元素 计算为点积 :
我们可以将矩阵-矩阵乘法 看作简单地执行 次矩阵-向量积,并将结果拼接在一起,形成一个 矩阵。 在下面的代码中,我们在A和B上执行矩阵乘法。 这里的A是一个5行4列的矩阵,B是一个4行3列的矩阵。 两者相乘后,我们得到了一个5行3列的矩阵。
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
print(A)
"""
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]])
"""
B = torch.ones(4, 3)
print(B)
"""
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
"""
print(torch.mm(A, B))
"""
tensor([[ 6., 6., 6.],
[22., 22., 22.],
[38., 38., 38.],
[54., 54., 54.],
[70., 70., 70.]])
"""
矩阵-矩阵乘法可以简单地称为矩阵乘法,不应与 “ Hadamard积 ” 混淆。
十、范数
我们已经介绍了线性代数中一些基本的运算,如按元素操作、点积、矩阵乘法等。接下来要介绍的是范数 (norm)——向量和矩阵的大小度量。
在线性代数中,向量的范数是将向量映射到标量的函数。给定任意向量 ,其范数需要满足以下性质:
-
比例性 (Scaling): 如果我们将向量 按常数因子 缩放,其范数按 的绝对值缩放:
-
三角不等式 (Triangle inequality): 对于任意向量 和 ,有
-
非负性 (Non-negativity): 向量范数 ,且当且仅当 时, 。
-
正齐次性 (Homogeneity): 对于任何实数 和向量 ,有
最常见的范数之一是 范数,也称为欧几里得范数 (Euclidean norm),定义为向量元素平方和的平方根:
在代码中,我们可以按如下方式计算向量的 范数:
u = torch.tensor([3.0, -4.0])
print(torch.norm(u)) # tensor(5.)
除了 范数, 范数表示向量元素绝对值之和:
在代码中,我们可以使用如下代码计算 范数:
u = torch.tensor([3.0, -4.0])
print(torch.abs(u).sum()) # tensor(7.)
范数 是 和 范数的推广形式:
此外,矩阵的 Frobenius 范数 定义为矩阵元素平方和的平方根,类似于向量的 范数:
A = torch.ones([4, 9])
print(torch.norm(A)) # tensor(6.)