学习D2L深度学习课程2:线性代数

线性代数

对于一个标量aNa \in N,可以考虑以下内容:

  • 简单操作,例如c=a+bc = a + bc=abc = a \cdot bc=sinac = \sin a等计算
  • 长度判定,对于一个标量,可以将其长度理解为:

a={a , if a>0a , otherwise|a| = \begin{cases} a \space ,\space if \space a > 0 \\ -a \space , \space otherwise \end{cases}

同时对于一个标量的长度,还有以下结论:

a+ba+b|a + b| \leq |a| + |b|

ab=ab|a \cdot b| = |a| \cdot |b|

对于一个向量aNma \in N^m,其将上述简单操作分别转化为:

c=a+b , where ci=ai+bi(向量逐元素相加)c = a + b \space , \space where \space c_i = a_i + b_i (向量逐元素相加)

c=αb , where ci=αbi(向量乘标量)c = \alpha \cdot b \space , \space where \space c_i = \alpha b_i (向量乘标量)

c=sina , where ci=sinai(向量逐元素求正弦)c = \sin a \space , \space where \space c_i = \sin a_i (向量逐元素求正弦)

对一个向量,其长度被定义为元素平方和开根号:

a2=i=1mai2\|a\|_2 = \sqrt{\sum^{m}_{i=1}a^2_i}

同时对于一个向量,其长度也有以下结论:

a0 , for all a(所有向量长度均为非负值)\|a\| \geq 0 \space , \space for \space all \space a (所有向量长度均为非负值)

a+ba+b(三角形法则,向量相加后长度小于各向量长度相加)\|a + b\| \leq \|a\| + \|b| (三角形法则,向量相加后长度小于各向量长度相加)

αb=αb(向量数乘后的长度等于其长度乘以标量绝对值,相当于延长或反向延长)\|\alpha \cdot b\| = |\alpha| \cdot \|b\| (向量数乘后的长度等于其长度乘以标量绝对值,相当于延长或反向延长)

向量运算的直观理解如下图所示:

向量还可以进行点乘操作:

ab=ab=iaibia \cdot b = a^{\top}b = \sum_{i}a_ib_i

当两个向量正交(垂直)时,其点乘结果为0:

ab=ab=iaibi=0(当两向量正交时)a \cdot b = a^{\top}b = \sum_{i}a_ib_i = 0 (当两向量正交时)

矩阵ANm×nA \in N^{m \times n}同样可以适用类似向量的简单操作:

C=A+B , where Cij=Aij+Bij(矩阵逐元素相加)C = A + B \space , \space where \space C_{ij} = A_{ij} + B_{ij} (矩阵逐元素相加)

C=αB , where Cij=αBij(矩阵乘标量)C = \alpha \cdot B \space , \space where \space C_{ij} = \alpha B_{ij} (矩阵乘标量)

C=sinA , where Cij=sinAij(矩阵逐元素求正弦)C = \sin A \space , \space where \space C_{ij} = \sin A_{ij} (矩阵逐元素求正弦)

矩阵和向量的乘法需要注意矩阵每行的元素数(维度)要等于向量的元素数(维度),或若将向量也看作矩阵,可以认为左侧矩阵的列数需等于右侧矩阵的行数:

c=Ab , where ci=jAijbjc = Ab \space , \space where \space c_i = \sum_{j} A_{ij}b_j

其具体乘法过程如下图所示:

矩阵和向量的乘法在集合上的意义是使用矩阵进行了空间的扭曲,也就是说,在进行乘法之前的向量受到了其所乘的矩阵的影响,导致其几何形态发生了改变。如下图所示:

矩阵和矩阵的乘法相当于矩阵和向量的乘法的合并,其要求同样为可以认为左侧矩阵的列数需等于右侧矩阵的行数:

C=AB , where Cik=jAijBjkC = AB \space , \space where \space C_{ik} = \sum_j A_{ij}B_{jk}

其具体过程如下图所示:

矩阵还有范数概念。我认为范数概念相当于向量的长度概念的引申,都是将一个多元素的对象映射为一个标量。然而对于一个矩阵,其范数定义可以是多样的(映射到标量的方法多样),由于有:

c=Ab hence cAbc = A \cdot b \space hence \space \|c\| \leq \|A\| \cdot \|b\|

可以定义的范数包括:

  • 矩阵范数:最小的满足上式的值即为A\|A\|
  • F范数(一般使用):可以理解为将矩阵所有对象取出并填入一个向量后向量的长度。如下图:

AFrob=ijAij2\|A\|_{Frob} = \sqrt{\sum_{ij}A^2_{ij}}

一些特殊的矩阵特质:

  • 对称:即主对角线两侧元素相等。判定式:Aij=AjiA_{ij} = A_{ji}
  • 反对称:即主对角线两侧元素分别为相反数。判定式:Aij=AjiA_{ij} = -A_{ji}
  • 正定:即对于任意向量xx,此矩阵均可满足xAx0x^{\top}A x \geq 0(即任意向量二次型值非负)
  • 正交矩阵:所有行都相互正交,所有行都为单位长度,判别为:UU=EUU^\top = E
  • 置换矩阵:每行、每列中恰好只有一个元素为1,其余元素均为的矩阵。即:P where Pij=1 if and only if j=π(i)P \space where \space P_{ij} = 1 \space if \space and \space only \space if \space j = \pi(i)。注意所有置换矩阵均为正交矩阵(可以视作一组标准正交基)

重要概念:特征向量和特征值

其定义为:Ax=λxAx = \lambda x,其中xx为特征向量,λ\lambda为对应特征值(特征向量与特征值一一对应)。从几何角度理解,特征向量就是不因矩阵扭曲而改变方向的向量,而特征值就是矩阵作用在特征向量上使其长度发生的变化倍率(因为无法改变方向,只能改变长度),示意图如下:

注意,并非所有矩阵都能找到特征向量。然而,对称矩阵永远可以找到特征向量。

后续线性代数相关概念在Pytorch中的实现见:线性代数实现。

线性代数实现

  • 可以通过生成只有一个元素的张量来表示标量,标量也可以进行基本操作:
1
2
3
4
5
import torch
x = torch.tensor([3.0])
y = torch.tensor([2.0])
print(x + y, x * y, x / y, x ** y)
# 输出:tensor([5.]) tensor([6.]) tensor([1.5000]) tensor([9.])
  • 向量可以被视为标量值组成的列表,且可以通过张量的索引访问其中的任意元素:
1
2
x = torch.arange(4)
print(x[3]) # 输出:tensor(3)
  • 可以获知一个张量的长度及一个张量的形状。由于张量只有一个轴(一维),因此其形状由单个元素表示:
1
2
print(len(x)) # 输出:4
print(x.shape) # 输出:torch.Size([4])
  • 可以通过首先创建一个张量,再使用数据操作实现中提到的reshape()函数的方式将其转化为一个指定行列数的矩阵:
1
2
3
4
5
6
7
8
9
10
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]])
"""
  • 可以通过代表转置符号的T实现对矩阵的转置(行变列、列变行):
1
2
3
4
5
6
7
8
print(A.T)

"""输出:
tensor([[ 0, 4, 8, 12, 16],
[ 1, 5, 9, 13, 17],
[ 2, 6, 10, 14, 18],
[ 3, 7, 11, 15, 19]])
"""

(可以使用Python测试,一个对称矩阵等于其转置,即A=AA = A^\top,验证代码如下)

1
2
3
4
5
6
7
8
B = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]]) # B为对称矩阵
print(B == B.T)

"""输出:
tensor([[True, True, True],
[True, True, True],
[True, True, True]])
"""
  • 类似向量是标量的推广、矩阵是向量的推广,可以向上构建具有更多轴(更多维度)的数据结构,例如此代码可以构建一个三维数据结构:
1
2
3
4
5
6
7
8
9
10
11
12
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]]])
"""

(给定具有相同形状的任意两个张量,任何按元素二元运算的结果张量将会与原来的两个张量同形。举例如下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone() # 此处相当于分配了一个新内存,将A的一个副本分配给B
print(A, A + B)

"""输出:
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]])
tensor([[ 0., 2., 4., 6.],
[ 8., 10., 12., 14.],
[16., 18., 20., 22.],
[24., 26., 28., 30.],
[32., 34., 36., 38.]])
"""
  • 两个矩阵的按元素乘法不同于矩阵相乘,在数学上被称为“哈达玛积”,数学符号为\odot。然而在计算机领域只会被简单表达为按元素相乘:
1
2
3
4
5
6
7
8
9
print(A * B)

"""输出:
tensor([[ 0., 1., 4., 9.],
[ 16., 25., 36., 49.],
[ 64., 81., 100., 121.],
[144., 169., 196., 225.],
[256., 289., 324., 361.]])
"""
  • 一个标量和一个矩阵的按元素操作,即为该标量和矩阵所有元素执行一次按元素操作。同样,这种操作的结果的形状和输入的矩阵形状依然相同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = 2
X = torch.arange(24).reshape(2, 3, 4)
print((a * X).shape) # 输出:torch.Size([2, 3, 4])
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]]])
"""
  • sum()可以计算张量的元素和,也可以被认为是张量降维的一种手段,同时其计算不仅限于一维向量,对于矩阵也可以进行元素和计算:
1
2
print(A.shape) # torch.Size([5, 4])
print(A.sum()) # tensor(190.)
  • 默认情况下求和函数会沿着所有的轴进行加和以降低张量维度,最终变成一个标量;然而还可以通过制定张量沿着哪一个轴求和来降低维度。例如一个矩阵,如果想要通过坍缩第一维的方式进行降维,可以在求和函数中指定轴0(axis=0)。如此,计算之后轴0的维度参数在形状输出中消失,同时,将每一列元素加和(要想理解主要要把握住将指定维坍缩的概念,例如将行坍缩,那么最后生成的矩阵就应该只有一行多列,那么实现这种的方式就是处于同一列的元素相加以压缩,或者说所有行逐元素相加;或者可以考虑压缩前后矩阵的形状和维度会如何改变):
1
2
3
A_sum_axis0 = A.sum(axis=0)
print(A_sum_axis0.shape) # 输出:torch.Size([4])
print(A_sum_axis0) # 输出:tensor([40., 45., 50., 55.])
  • 同样,也可以指定轴1(axis=1),以通过坍缩第二维(列)进行降维。如此,计算之后轴1的维数在输出形状中消失,同时,将每一行元素加和:
1
2
3
A_sum_axis1 = A.sum(axis=1)
print(A_sum_axis1.shape) # 输出:torch.Size([5])
print(A_sum_axis1) # 输出:tensor([ 6., 22., 38., 54., 70.])
  • 如果同时指定轴0和轴1(即对于一个高维张量同时选择加和压缩其所有维度),那么计算后的效果与无参数sum()相同:
1
2
print(A.sum(axis=[0, 1]).shape) # 输出:torch.Size([])
print(A.sum(axis=[0, 1])) # 输出:tensor(190.)
  • 一个和求和相关的运算是求均值mean(),当不指定轴时均值即为元素求和结果除元素个数(可以通过numel()shape()等方式获取);当指定轴时相当于对每一个被压缩的向量进行求均值。示例如下:
1
2
3
4
print(A.mean()) # 输出:tensor(9.5000)
print(A.sum() / A.numel()) # 输出:tensor(9.5000)
print(A.mean(axis=0)) # 输出:tensor([ 8., 9., 10., 11.])
print(A.sum(axis=0) / A.shape[0]) # 输出:tensor([ 8., 9., 10., 11.])
  • 使用sum()进行加和时,会进行降维。例如指定轴0,那么原先的第0维(行)就会在加和后消失。可以使用keepdims参数设置,让加和操作不进行降维,即将要压缩的向量求和后存入原向量第一个位置。这样只是相当于将原向量长度缩短为1,而并非将其转化为一个标量,因此其轴数(维数)不会变化:
1
2
3
4
5
6
7
8
9
10
sum_A = A.sum(axis=1, keepdims=True)
print(sum_A)

"""输出:
tensor([[ 6.],
[22.],
[38.],
[54.],
[70.]])
"""
  • 不降维的优势是,后续可以使用sum_A与广播机制(见[[数据操作]]),将Asum_A进行逐元素操作。只要两个张量维数一致就可以操作,然而如果压缩了维度便无论如何不能进行逐元素操作:
1
2
3
4
5
6
7
8
9
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]])
"""
  • 可以使用cumsum()进行累加求和。例如,如果指定轴0,相当于将原0行不动,新1行改为原0行与原1行相加,新2行改为新1行与原2行相加(原1、2、3行相加),以此类推:
1
2
3
4
5
6
7
8
9
print(A.cumsum(axis=0))

"""输出:
tensor([[ 0., 1., 2., 3.],
[ 4., 6., 8., 10.],
[12., 15., 18., 21.],
[24., 28., 32., 36.],
[40., 45., 50., 55.]])
"""
  • 可以使用dot()实现矩阵(张量)间的点积运算。对于两个同形向量,点积是同位置元素乘积之和(结果是一个标量):
1
2
3
4
x = torch.arange(4, dtype=torch.float32)
y = torch.ones(4, dtype=torch.float32)
print(x, y) # 输出:tensor([0., 1., 2., 3.]) tensor([1., 1., 1., 1.])
print(torch.dot(x, y)) # 输出:tensor(6.)
  • 当然,两个向量点乘也可以使用先按元素相乘再加和的方式实现:
1
print(torch.sum(x * y)) # 输出:tensor(6.)
  • 矩阵向量积,即AxAx,其结果向量长度同AA行数,其中第ii个元素值由AA的第ii行与xx进行向量点乘得到,即aixa_{i}^{\top}x。在pytorch中可以使用torch.mv()进行计算:
1
2
3
4
print(A.shape) # 输出:torch.Size([5, 4])
print(x.shape) # 输出:torch.Size([4])
print(torch.mv(A, x)) # 注意矩阵列数要等于向量元素数才能计算矩阵向量积
# 输出:tensor([ 14., 38., 62., 86., 110.])
  • 矩阵-矩阵乘法,即ABAB,可以看作将BB拆分为多个列向量,并将这些列向量与AA矩阵进行矩阵向量积,再将结果向量拼接,最终形成一个形状为行同AA行数,列同BB列数的新矩阵。在pytorch中使用torch.mm()计算:
1
2
3
4
5
6
7
8
9
10
B = torch.ones(4, 3) # 注意A的列数需要等于B的行数,才能进行矩阵-矩阵乘法
print(torch.mm(A, B))

"""输出:
tensor([[ 6., 6., 6.],
[22., 22., 22.],
[38., 38., 38.],
[54., 54., 54.],
[70., 70., 70.]])
"""
  • 向量的L2L_2范数可以理解为向量的长度,即向量元素平方和的平方根:

x2=i=1nxi2\|x\|_2 = \sqrt{\sum^{n}_{i=1}x^2_i}

  • 在pytorch中,可以使用torch.norm计算向量的L2L_2范数:
1
2
u = torch.tensor([3.0, -4.0])
print(torch.norm(u)) # 输出:tensor(5.)
  • 向量的L1L_1范数,表示向量各元素的绝对值之和:

x1=i=1nxi\|x\|_1 = \sum^{n}_{i=1}|x_i|

  • 在pytorch中,没有直接求L1L_1范数的方法,但是可以使用abs()绝对值方法与sum()求和方法结合来求此范数:
1
print(torch.abs(u).sum()) # 输出:tensor(7.)
  • 矩阵中最常用的范数为FF范数,即矩阵所有元素的平方和的平方根:

XF=i=1mj=1nxij2\|X\|_F = \sqrt{\sum^m_{i=1}\sum^n_{j=1}x^2_{ij}}

  • 在pytorch中,矩阵的FF范数与向量共用torch.norm进行计算:
1
print(torch.norm(torch.ones((4, 9)))) # 输出:tensor(6.)