深度学习 Pytorch 张量的线性代数运算
pytorch
中并未设置单独的矩阵对象类型,因此pytorch
中,二维张量就相当于矩阵对象,并且拥有一系列线性代数相关函数和方法。
在实际机器学习和深度学习建模过程中,矩阵或者高维张量都是基本对象类型,而矩阵所涉及到的线性代数理论也是深度学习用户必备的基本数学基础。
import torch
import numpy as np
16 矩阵的形变及特殊矩阵构造方法
矩阵的形变方法其实也就是二维张量的形变方法,再此基础上补充转置的基本方法。
另外,在实际线性代数运算过程中,经常涉及一些特殊矩阵,如单位矩阵、对角矩阵等。
Tensor矩阵运算
函数 | 描述 |
---|---|
torch.t(t) | t转置 |
torch.eye(n) | 创建包含n个分量的单位矩阵 |
torch.diag(t) | 以t中各元素,创建对角矩阵 |
torch.triu(t) | 取矩阵t中的上三角矩阵 |
torch.tril(t) | 取矩阵t中的下三角矩阵 |
# 创建一个2*3的矩阵
t1 = torch.arange(1, 7).reshape(2, 3).float()
t1
# output :
tensor([[1., 2., 3.],
[4., 5., 6.]])
# 转置
torch.t(t1)
# output :
tensor([[1., 4.],
[2., 5.],
[3., 6.]])
# 也可以调用方法
t1.t()
# output :
tensor([[1., 4.],
[2., 5.],
[3., 6.]])
矩阵的转置就是每个元素行列位置互换。
# 创建单位阵
torch.eye(3)
# output :
tensor([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
# 创建对角矩阵
t = torch.arange(1,6)
torch.diag(t)
# output :
tensor([[1, 0, 0, 0, 0],
[0, 2, 0, 0, 0],
[0, 0, 3, 0, 0],
[0, 0, 0, 4, 0],
[0, 0, 0, 0, 5]])
# 对角线向右上偏移一位
torch.diag(t, 1)
# output :
tensor([[0, 1, 0, 0, 0, 0],
[0, 0, 2, 0, 0, 0],
[0, 0, 0, 3, 0, 0],
[0, 0, 0, 0, 4, 0],
[0, 0, 0, 0, 0, 5],
[0, 0, 0, 0, 0, 0]])
# 注意偏移后依然是一个方阵,从五行五列变成了六行六列
# 对角线向右上偏移一位
torch.diag(t, -1)
# output :
tensor([[0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0],
[0, 2, 0, 0, 0, 0],
[0, 0, 3, 0, 0, 0],
[0, 0, 0, 4, 0, 0],
[0, 0, 0, 0, 5, 0]])
t1 = torch.arange(1, 9).reshape(3, 3)
t1
# output :
tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 取上三角矩阵
torch.triu(t1)
# output :
tensor([[1, 2, 3],
[0, 5, 6],
[0, 0, 9]])
# 上三角矩阵向左下偏移一位
torch.triu(t1, -1)
# output :
tensor([[1, 2, 3],
[4, 5, 6],
[0, 8, 9]])
# 上三角矩阵向右上偏移一位
torch.triu(t1, 1)
# output :
tensor([[0, 2, 3],
[0, 0, 6],
[0, 0, 0]])
# 下三角矩阵
torch.tril(t1)
# output :
tensor([[1, 0, 0],
[4, 5, 0],
[7, 8, 9]])
17 矩阵的基本运算
矩阵不同于普通的二维数组,其具备一定的线性代数含义。
而这些特殊的性质,其实就主要体现在矩阵的基本运算上。
矩阵的基本运算
函数 | 描述 |
---|---|
torch.dot(t1, t2) | 计算t1, t2张量内积 |
torch.mm(t1, t2) | 矩阵乘法 |
torch.mv(t1, t2) | 矩阵乘向量 |
torch.bmm(t1, t2) | 批量矩阵乘法 |
torch.addmm(t, t1, t2) | 矩阵相乘后相加 |
torch.addbmm(t, t1, t2) | 批量矩阵相乘后相加 |
dot/vdot:点积计算
注意:
dot
和vdot
只能作用于一维张量 ,且对于数值型对象,二者计算结果并没有区别,两种函数只在进行复数计算时会有区别。
t = torch.arange(1, 4)
t
# output :
tensor([1, 2, 3])
torch.dot(t, t)
# output :
tensor(14)
torch.vdot(t, t)
# output :
tensor(14)
# 不能进行除了一维张量以外的计算
t1 = torch.arange(1, 9).reshape(3, 3)
torch.dot(t1, t1)
# output :
RuntimeError: shape '[3, 3]' is invalid for input of size 8
mm:矩阵乘法
t1 = torch.arange(1, 7).reshape(2, 3)
t2 = torch.arange(1, 10).reshape(3, 3)
# 对应元素相乘
t1*t1
# output :
tensor([[ 1, 4, 9],
[16, 25, 36]])
# 矩阵乘法
torch.mm(t1, t2)
# output :
tensor([[30, 36, 42],
[66, 81, 96]])
矩阵乘法执行过程如下所示:
mv:矩阵和向量相乘
矩阵和向量相乘可以看作是先将向量转化为列向量然后再相乘。
在实际执行向量和矩阵相乘的过程中,需要矩阵的列数和向量的元素个数相同。
met = torch.arange(1, 7).reshape(2, 3)
met
# output :
tensor([[1, 2, 3],
[4, 5, 6]])
vec = torch.arange(1, 4)
vec
# output :
tensor([1, 2, 3])
torch.mv(met, vec)
# output :
tensor([14, 32])
bmm:批量矩阵相乘
批量矩阵相乘指的是三维张量的矩阵乘法。
例如,一个(3,2,2)的张量,本质上就是一个包含了3个2*2矩阵的张量。而三维张力的矩阵相乘,则是三维张量内部各对应位置的矩阵相乘。
注意:
- 三维张量包含的矩阵个数需要相同
- 每个内部矩阵,需要满足矩阵乘法的条件,也就是左乘矩阵的行数要等于右乘矩阵的列数
t3 = torch.arange(1, 13).reshape(3, 2, 2)
t3
# output :
tensor([[[ 1, 2],
[ 3, 4]],
[[ 5, 6],
[ 7, 8]],
[[ 9, 10],
[11, 12]]])
t4 = torch.arange(1, 19).reshape(3, 2, 3)
t4
# output :
tensor([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]],
[[13, 14, 15],
[16, 17, 18]]])
torch.bmm(t3, t4)
# output :
tensor([[[ 9, 12, 15],
[ 19, 26, 33]],
[[ 95, 106, 117],
[129, 144, 159]],
[[277, 296, 315],
[335, 358, 381]]])
addmm:矩阵相乘后相加
addmm
函数结构:addmm(input, mat 1 , mat 2 , beta = 1, alpha = 1)
输出结果:beta * input + alpha * (mat 1 * mat 2)
t1
# output :
tensor([[1, 2, 3],
[4, 5, 6]])
t2
# output :
tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
t = torch.arange(3)
t
# output :
tensor([0, 1, 2])
torch.mm(t1, t2)
# output :
tensor([[30, 36, 42],
[66, 81, 96]])
torch.addmm(t, t1, t2)
# output :
tensor([[30, 37, 44],
[66, 82, 98]])
torch.addmm(t, t1, t2, beta = 0, alpha = 10)
# output :
tensor([[300, 360, 420],
[660, 810, 960]])
addbmm:批量矩阵相乘后相加
和addmm
类似,都是先乘后加,并且可以设置权重。不同的是addbmm
是批量矩阵相乘,并且,再相乘的过程中而是矩阵相加,而非向量加矩阵。
t = torch.arange(6).reshape(2, 3)
t
# output :
tensor([[0, 1, 2],
[3, 4, 5]])
t3
# output :
tensor([[[ 1, 2],
[ 3, 4]],
[[ 5, 6],
[ 7, 8]],
[[ 9, 10],
[11, 12]]])
t4
# output :
tensor([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]],
[[13, 14, 15],
[16, 17, 18]]])
torch.bmm(t3, t4)
# output :
tensor([[[ 9, 12, 15],
[ 19, 26, 33]],
[[ 95, 106, 117],
[129, 144, 159]],
[[277, 296, 315],
[335, 358, 381]]])
torch.addbmm(t, t3, t4)
# output :
tensor([[381, 415, 449],
[486, 532, 578]])
注: addbmm
会在原来三维张量基础之上,对其内部矩阵进行求和。
18 矩阵的线性代数运算
如果说矩阵的基本运算是矩阵基本性质,那么矩阵的线性代数运算,则是外面利用矩阵数据类型在求解实际问题过程中经常涉及到的线性代数方法。
矩阵的线性代数运算
函数 | 描述 |
---|---|
torch.trace(A) | 矩阵的迹 |
torch.linalg.matrix_rank(A) | 矩阵的秩 |
torch.det(A) | 计算矩阵A的行列式 |
torch.inverse(A) | 矩阵求逆 |
torch.lstsq(A, B) | 最小二乘法 |
矩阵的迹(trace)
就是求矩阵对角线之和
A = torch.tensor([[1, 2], [4, 5]]).float()
A
# output :
tensor([[1., 2.],
[4., 5.]])
torch.trace(A)
# output :
tensor(6.)
当然,计算过程不需要是方阵
B = torch.arange(1, 7).reshape(2, 3)
B
# output :
tensor(6)
矩阵的秩(rank)
矩阵的秩是指矩阵中行或列的极大线性无关组,且矩阵中行、列极大无关数总是相同的,任何矩阵的秩都是唯一值。
满秩指的是方阵(行数和列数相同的矩阵)中行数、列数和秩相同,满秩矩阵有线性唯一解等重要特性,而其他矩阵也能通过求解秩来降维。
同时,秩也是奇异值分解等运算中涉及到的重要概念。
注: 秩的计算要求浮点型张量
A = torch.arange(1, 5).reshape(2, 2).float()
A
# output :
tensor([[1., 2.],
[3., 4.]])
torch.torch.linalg.matrix_rank(A)
# output :
tensor(2)
对于矩阵B来说,第一列和第二列明显线性相关,最大线性无关组只有1组,因此矩阵的秩结果为1。
B = torch.tensor([[1, 2], [2, 4]]).float()
B
# output :
tensor([[1., 2.],
[2., 4.]])
torch.torch.linalg.matrix_rank(B)
# output :
tensor(1)
矩阵的行列式(det)
我们可以将行列式简单理解为矩阵的一个基本性质或属性。通过行列式的计算,我们能够知道矩阵是否可逆,从而可以进一步求解矩阵所对应的线性方程。
更加专业的解释,行列式作为一个基本数学工具,实际上就是矩阵进行线性变换的伸缩因子。
对于任何一个n
维方阵,行列式计算过程如下
D
=
∣
a
11
a
12
…
a
1
n
a
21
a
22
…
a
2
n
⋮
⋮
⋱
⋮
a
n
1
a
n
2
…
a
n
n
∣
D = \left| \begin{array}{cccc} a_{11} & a_{12} & \ldots & a_{1n} \\ a_{21} & a_{22} & \ldots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \ldots & a_{nn} \\ \end{array} \right|
D=
a11a21⋮an1a12a22⋮an2……⋱…a1na2n⋮ann
D = ∑ ( − 1 ) k a 1 k 1 a 2 k 2 … a n k n D = \sum (-1)^k a_{1k_1} a_{2k_2} \ldots a_{nk_n} D=∑(−1)ka1k1a2k2…ankn
更为简单的情况,如果对于一个2*2
的矩阵,行列式的计算就是主对角线元素之积减去另外两个元素之积。
A = torch.tensor([[1, 2], [4, 5]]).float()
A
# output :
tensor([[1., 2.],
[4., 5.]])
torch.det(A)
# output :
tensor(-3.)
A的行列式计算过程如下
对于行列式的计算,要求二维张量必须是方阵,也就是行列数必须一致。
B = torch.arange(1, 7).reshape(2, 3)
B
# output :
tensor([[1, 2, 3],
[4, 5, 6]])
torch.det(B)
# output :
RuntimeError: linalg.det: A must be batches of square matrices, but they are 2 by 3 matrices
19 线性方程组的矩阵表达式
在正式进入到更进一步矩阵运算的讨论之前,我们需要对矩阵建立一个更加形象化的理解。
通常来说,我们会把高维空间中的一个个数看成是向量,而由这些向量组成的数组看成是一个矩阵。
例如:(1,2)
,(3,4)
是二维空间中的两个点,矩阵A
就代表这两个点所组成的矩阵。
A = torch.arange(1, 5).reshape(2, 2).float()
A
# output :
tensor([[1., 2.],
[3., 4.]])
import matplotlib as mpl
import matplotlib.pyplot as plt
plt.plot(A[:, 0], A[:, 1], 'o')
# output :
如果更近一步,我们希望在二维空间中找到一条直线,来拟合这两个点,也就是所谓的构建线性回归模型,我们可以设置线性回归方程如下:
y
=
a
x
+
b
y = ax + b
y=ax+b
带入(1,2)和(3,4)两个点后,我们还可以进一步将表达式改写成矩阵表示形式,改写过程如下:
而用矩阵表示线性方程组,则是矩阵的另一种常见用途。接下来,我们就可以通过上述矩阵方程组来求解系数向量x
。
首先一个基本思路是,如果有个和A矩阵相关的另一个矩阵,假设为A−1,可以使得二者相乘之后等于1,也就是A∗A−1=1,那么在方程组左右两边同时左乘该矩阵,等式右边的计算结果A−1∗B就将是x系数向量的取值。而此处的A−1就是所谓的A的逆矩阵。
逆矩阵定义:如果存在两个矩阵A、B,并在矩阵乘法运算下,A * B = E(单位矩阵),则我们称A、B互为逆矩阵。
在上述线性方程组求解场景中,我们已经初步看到了逆矩阵的用途,而一般来说,我们往往会通过伴随矩阵来进行逆矩阵的求解。由于伴随矩阵本身并无其他核心用途,且PyTorch
中也未给出伴随矩阵的计算函数(目前),因此我们直接调用inverse
函数来进行逆矩阵的计算。
当然,并非所有矩阵都有逆矩阵,对于一个矩阵来说,首先必须是方正,其次矩阵的秩不能为零,满足两个条件才能求解逆矩阵。
inverse函数:求解逆矩阵
首先根据上述矩阵表达式重新定义A和B
A = torch.tensor([[1.0, 1], [3, 1]])
A
# output :
tensor([[1., 1.],
[3., 1.]])
B = torch.tensor([2.0, 4])
B
# output :
tensor([2., 4.])
然后使用inverse
函数进行逆矩阵求解
torch.inverse(A)
# output :
tensor([[-0.5000, 0.5000],
[ 1.5000, -0.5000]])
简单试探逆矩阵的基本特性
torch.mm(torch.inverse(A), A)
# output :
tensor([[ 1.0000e+00, -5.9605e-08],
[-1.1921e-07, 1.0000e+00]])
乍一看不像是单位阵,实际上除了对角线元素是1外,其他元素取值都非常非常小,在实际运算过程中可以忽略不计。
在方程组左右两边同时左乘
A
−
1
A^{-1}
A−1,求解x
A
−
1
⋅
A
⋅
x
=
A
−
1
⋅
B
E
⋅
x
=
A
−
1
⋅
B
x
=
A
−
1
⋅
B
\begin{align*} A^{-1} \cdot A \cdot x &= A^{-1} \cdot B \\ E \cdot x &= A^{-1} \cdot B \\ x &= A^{-1} \cdot B \end{align*}
A−1⋅A⋅xE⋅xx=A−1⋅B=A−1⋅B=A−1⋅B
torch.mv(torch.inverse(A), B)
# output :
tensor([1.0000, 1.0000])
最终得到线性方程为:
y
=
x
+
1
y = x+1
y=x+1
上述计算过程只是一个简化的线性方程组求解系数的过程,同时也是一个简单的一元线性方程拟合数据的过程。
20 矩阵的分解
矩阵的分解也是矩阵运算中的常规计算,矩阵分解也有很多种类,常见的例如QR分解、LU分解、特征分解、SVD分解等等等等,虽然大多数情况下,矩阵分解都是在形式上将矩阵拆分成几种特殊矩阵的乘积,但本质上,矩阵的分解是去探索矩阵更深层次的一些属性。
本节将主要围绕特征分解和SVD分解展开。
值得一提的是,此前的逆矩阵,其实也可以将其看成是一种矩阵分解的方式,分解之后的等式如下:
A
=
A
∗
A
−
1
∗
A
A = A * A^{-1} * A
A=A∗A−1∗A
而大多数情况下,矩阵分解都是分解成形如下述形式
A
=
V
U
D
A = VUD
A=VUD
特征分解
特征分解中,矩阵分解形式为:
A
=
Q
Λ
Q
−
1
A = Q\Lambda Q^{-1}
A=QΛQ−1
其中,Q和
Q
−
1
Q^{-1}
Q−1互为逆矩阵,并且Q的列就是A的特征值所对应的特征向量,而
Λ
\Lambda
Λ为矩阵A的特征值按照降序排列组成的对角矩阵。
torch.eig函数:特征分解
A = torch.arange(1, 10).reshape(3, 3).float()
A
# output :
tensor([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]])
torch.eig(A, eigenvectors = True)
# output :
torch.return_types.eig(
eigenvalues=tensor([[ 1.6117e+01, 0.0000e+00],
[-1.1168e+00, 0.0000e+00],
[-1.2253e-07, 0.0000e+00]]),
eigenvectors=tensor([[-0.2320, -0.7858, 0.4082],
[-0.5253, -0.0868, -0.8165],
[-0.8187, 0.6123, 0.4082]]))
输出结果中,eigenvalues
表示特征值向量,即A
矩阵分解后的Λ
矩阵的对角线元素值,并按照由大到小依次排列,eigenvectors
表示A
矩阵分解后的Q
矩阵。
此处需要理解特征值,所谓特征值,可简单理解为对应列在矩阵中的信息权重,如果该列能够简单线性变换来表示其他列,则说明该列信息权重较大,反之则较小。
特征值概念和秩的概念有点类似,但不完全相同,矩阵的秩表示矩阵列向量的最大线性无关数,而特征值的大小则表示某列向量能多大程度解读矩阵列向量的变异度,即所包含信息量,秩和特征值关系可用下面这个例子来进行解读。
B = torch.tensor([1, 2, 2, 4]).reshape(2, 2).float()
B
# output :
tensor([[1., 2.],
[2., 4.]])
torch.matrix_rank(B)
# output :
tensor(1)
torch.eig(B) # 返回结果中只有一个特征
# output :
torch.return_types.eig(
eigenvalues=tensor([[0., 0.],
[5., 0.]]),
eigenvectors=tensor([]))
C = torch.tensor([[1, 2, 3], [2, 4, 6], [3, 6, 9]]).float()
C
# output :
tensor([[1., 2., 3.],
[2., 4., 6.],
[3., 6., 9.]])
torch.eig(C) # 只有一个特征的有效值
# output :
torch.return_types.eig(
eigenvalues=tensor([[ 1.4000e+01, 0.0000e+00],
[-1.6447e-07, 0.0000e+00],
[ 2.8710e-07, 0.0000e+00]]),
eigenvectors=tensor([]))
特征值一般用于表示矩阵对应线性方程组解空间以及数据降维,当然,由于特征分解只能作用于方阵,而大多数实际情况下矩阵行列数未必相等,此时要进行类似的操作就需要采用和特征值分解思想类似的奇异值分解(SVD)
。
奇异值分解(SVD)
奇异值分解(SVD)来源于代数学中的矩阵分解问题,对于一个方阵来说,我们可以利用矩阵特征值和特征向量的特殊性质(矩阵点乘特征向量等于特征值数乘特征向量),通过求特征值与特征向量来达到矩阵分解的效果
A
=
Q
Λ
Q
−
1
A = Q\Lambda Q^{-1}
A=QΛQ−1
这里,Q是由特征向量组成的矩阵,而Λ是特征值降序排列构成的一个对角矩阵(对角线上每个值是一个特征值,按降序排列,其他值为0),特征值的数值表示对应的特征的重要性。 在很多情况下,最大的一小部分特征值的和即可以约等于所有特征值的和,而通过矩阵分解的降维就是通过在Q、Λ 中删去那些比较小的特征值及其对应的特征向量,使用一小部分的特征值和特征向量来描述整个矩阵,从而达到降维的效果。 但是,实际问题中大多数矩阵是以奇异矩阵形式,而不是方阵的形式出现的,奇异值分解是特征值分解在奇异矩阵上的推广形式,它将一个维度为m×n的奇异矩阵A分解成三个部分 :
A
=
U
∑
V
T
A = U\sum V^{T}
A=U∑VT
其中U、V是两个正交矩阵,其中的每一行(每一列)分别被称为左奇异向量和右奇异向量,他们和∑中对角线上的奇异值相对应,通常情况下我们只需要保留前k个奇异向量和奇异值即可,其中U是m×k矩阵
,V是n×k矩阵
,∑是k×k的方阵
,从而达到减少存储空间的效果,即
A
m
∗
n
=
U
m
∗
m
∑
m
∗
n
V
n
∗
n
T
≈
U
m
∗
k
∑
k
∗
k
V
k
∗
n
T
A_{m*n} = U_{m*m}\sum_{m*n}V^{T}_{n*n}\approx U_{m*k}\sum_{k*k}V^{T}_{k*n}
Am∗n=Um∗mm∗n∑Vn∗nT≈Um∗kk∗k∑Vk∗nT
奇异值分解函数
C
# output :
tensor([[1., 2., 3.],
[2., 4., 6.],
[3., 6., 9.]])
torch.svd(C)
# output :
torch.return_types.svd(
U=tensor([[-0.2673, -0.8018, -0.5345],
[-0.5345, -0.3382, 0.7745],
[-0.8018, 0.4927, -0.3382]]),
S=tensor([14.0000, 0.0000, 0.0000]),
V=tensor([[-0.2673, 0.0000, 0.9636],
[-0.5345, -0.8321, -0.1482],
[-0.8018, 0.5547, -0.2224]]))
CU, CS, CV = torch.svd(C) # 验证SVD分解
torch.mm(torch.mm(CU, torch.diag(CS)), CV.t())
# output :
tensor([[1.0000, 2.0000, 3.0000],
[2.0000, 4.0000, 6.0000],
[3.0000, 6.0000, 9.0000]])
能够看出,上述输出完整还原了C矩阵,此时我们可根据svd
输出结果对C进行降维,此时C可只保留第一列(后面的奇异值过小),即k = 1
U1 = CU[:, 0].reshape(3, 1) # U的第一列
U1
# output :
tensor([[-0.2673],
[-0.5345],
[-0.8018]])
C1 = CS[0] # C的第一个值
C1
# output :
tensor(14.0000)
V1 = CV[:, 0].reshape(1, 3) # V的第一行
V1
# output :
tensor([[-0.2673, -0.5345, -0.8018]])
torch.mm((U1 * C1), V1)
# output :
tensor([[1.0000, 2.0000, 3.0000],
[2.0000, 4.0000, 6.0000],
[3.0000, 6.0000, 9.0000]])
此时输出的Cd矩阵已经和原矩阵C高度相似了,损失信息在R的计算中基本可以忽略不计,经过SVD分解
,矩阵的信息能够被压缩至更小的空间内进行存储,从而为PCA
(主成分分析)、LSI
(潜在语义索引)等算法做好了数学工具层面的铺垫。