目录
1、Numpy与Tensor
Tensor
自称为神经网络界的Tensor
,与Numpy
相似,二者可以共享内存,且之间的的转换非常方便和高效。Numpy
中的ndarray
智能在CPU中进行运算,Torch
中的Tensor
既可以在CPU也可以在GPU上运行。
1.1、Tensor概述
对Tensor
的操作很多,从接口的角度划分,可以分成两类:
torch.function
, 如torch.sum
、torch.add
等。tensor.function
, 如tensor.view
、tensor.add
等。
这些操作对大部分Tensor
都是等价的,如torch.add(x,y)
与x.add(y)
等价。
从修改方式的角度来划分,可以分为以下两类
- 不修改自身数据,如
x.add(y)
,x的数据不变,返回一个新的Tensor
。 - 修改自身数据,如
x.add_(y)
(运算符带下划线后缀),运算结果存在x中,x被修改。
import torch
x=torch.tensor([1,2])
y=torch.tensor([3,4])
#不改变自身数据
z=x.add(y)
print(z)
print(x)
#改变自身数据
x.add_(y)
print(x)
tensor([4, 6])
tensor([1, 2])
tensor([4, 6])
1.2、创建Tensor
创建tensor
的方法有很多,可以从列表或者ndarray
等类型进行构建,也可以根据指定的形状构建。下面是常见的创建Tensor
的方法。
函数 | 功能 |
---|---|
Tensor(*size) | 直接从参数构造一个张量,支持List,Numpy数组 |
eye(row,cloumn) | 创建指定行数,列数的二位单位Tensor |
linspace(start,end,steps) | 从start到end,均匀切分成steps份 |
logspace(start,end,steps) | 从10start到10end,均匀切分成steps份 |
rand/randn(*size) | 生成[0,1)均匀分布/标准正态分布数据 |
ones(*size) | 返回指定shape的张量,元素初始为1 |
zeros(*size) | 返回指定shape的张量,元素初始为0 |
ones_like(t) | 返回与T的shape相同的张量,且元素初始为1 |
zeros_like(t) | 返回与T的shape相同的张量,且元素初始为0 |
arange(start,end,step) | 在区间[start,end)上以间隔step生成一个序列张量 |
from_Numpy(ndarray) | 从ndarray创建一个Tensor |
#根据list数据生成Tensor
a1=torch.Tensor([1,2,3,4,5,6])
print(a1)
#根据指定形状生成Tensor,值是随机的
a2=torch.Tensor(2,3)
print(a2)
#根据给定的数组
t=torch.Tensor([[1,2,3],[4,5,6]])
print(t)
#查看Tensor的形状
print(t.size())
#shape与size()效果一样
print(t.shape)
#根据已有形状创建Tensor
print(torch.Tensor(t.size()))
tensor([1., 2., 3., 4., 5., 6.])
tensor([[1., 2., 3.],
[4., 5., 6.]])
tensor([[1., 2., 3.],
[4., 5., 6.]])
torch.Size([2, 3])
torch.Size([2, 3])
tensor([[2.5966e+20, 1.3178e-08, 1.3533e+22],
[6.7111e+22, 2.7028e-06, 6.5623e-10]])
总结:torch.Tensor()
可以通过传入数组或者Numpy数组生成tensor;也可以直接传入张量的大小,其张量值是随机生成的。
torch.Tensor
和torch.tensor
的区别
torch.Tensor
是torch.empty
和torch.tensor
之间的一种混合,但是,当传入数据时,torch.Tensor
使用全局默认dtype
(FloatTensor
),而torch.tensor
是从数据中推断出数据类型。torch.tensor(1)
返回一个固定值1,而torch.Tensor(1)
返回一个大小为1的张量,它的值是随机初始化的。
t1=torch.Tensor(1)
t2=torch.tensor(1)
t3=torch.Tensor([1,2])#默认为FloatTensor,认为可以使用该方法生成,这个在训练时就不会出现数据类型不匹配的现象。
t4=torch.tensor([1,2])#从数值中进行推断
print("t1的值{},t1的数据类型{}".format(t1,t1.type()))
print("t2的值{},t2的数据类型{}".format(t2,t2.type()))
print("t3的值{},t3的数据类型{}".format(t3,t3.type()))
print("t4的值{},t4的数据类型{}".format(t4,t4.type()))
t1的值tensor([7.3750e-39]),t1的数据类型torch.FloatTensor
t2的值1,t2的数据类型torch.LongTensor
t3的值tensor([1., 2.]),t3的数据类型torch.FloatTensor
t4的值tensor([1, 2]),t4的数据类型torch.LongTensor
根据一些规则,自动生成Tensor
#生成一个单位矩阵
print('生成一个单位矩阵:')
print(torch.eye(2,2))
#生成全是0的矩阵
print('生成全是0的矩阵:')
print(torch.zeros(2,3))
#根据规则生成数据
print('根据规则生成数据:')
print(torch.linspace(1,10,4))
#生成满足均匀分布的随机数
print('生成满足均匀分布的随机数:')
print(torch.rand(2,3))
#生成满足标准分布随机数
print('成满足标准分布随机数:')
print(torch.randn(2,3))
#返回所给类型相同,值为.的张量
print('返回所给类型相同,值为.的张量:')
print(torch.zeros_like(torch.rand(2,3)))
生成一个单位矩阵:
tensor([[1., 0.],
[0., 1.]])
生成全是0的矩阵:
tensor([[0., 0., 0.],
[0., 0., 0.]])
根据规则生成数据:
tensor([ 1., 4., 7., 10.])
生成满足均匀分布的随机数:
tensor([[0.6261, 0.1458, 0.8802],
[0.5442, 0.2488, 0.6998]])
成满足标准分布随机数:
tensor([[-0.9153, 2.4684, 0.0518],
[ 1.2625, -0.2756, -0.8811]])
返回所给类型相同,值为.的张量:
tensor([[0., 0., 0.],
[0., 0., 0.]])
1.3、修改Tensor的形状
如下为修改Tensor
的形状的部分函数。
函数 | 功能 |
---|---|
shape | 查看张量的形状 |
size() | 返回张量的shape属性值,与函数shape等价 |
numel() | 计算Tensor的元素个数 |
reshape() | 修改Tensor的形状 |
view(*shape) | 修改Tensor的形状,与reshape类似,但view返回的对象与源Tensor共享内存,修改一个另一个同时修改。reshape将生成一个新的tensor,而且不要求源Tensor是连续的。view(-1)展平数组 |
resize() | 类似于view,但在size超出时会重新分配内存空间 |
item() | 若Tensor为单元素,则返回Python的标量 |
unsqueeze() | 在指定维度增加一个“1” |
squeeze() | 在指定维度压缩一个“1” |
dim() | 查看张量的维度 |
flatten | 将张量展平 |
#生成一个形状为2×3的矩阵
x=torch.randn(2,3)
#查看矩阵的形状
print("使用size()方法:",x.size())
print("使用shape方法:",x.shape)
#查看x的维度
print("x的维度为",x.dim())
#把x变为3×2的矩阵
print('把x变为3×2的矩阵:')
print(x.view(3,2))
#吧x展平为1维向量
print('把x展平为1维向量')
y=x.view(-1)
print(y)
#添加一个维度
z=torch.unsqueeze(y,0)
print('y的形状:',y.size())
#查看z的形状
print('z的形状:',z.size())
#计算z的元素个数
print('z的元素个数:',z.numel())
使用size()方法: torch.Size([2, 3])
使用shape方法: torch.Size([2, 3])
x的维度为 2
把x变为3×2的矩阵:
tensor([[-0.3172, -0.8660],
[ 1.7482, -0.2759],
[-0.9755, 0.4790]])
把x展平为1维向量
tensor([-0.3172, -0.8660, 1.7482, -0.2759, -0.9755, 0.4790])
y的形状: torch.Size([6])
z的形状: torch.Size([1, 6])
z的元素个数: 6
说明:torch.view与torch.reshpae的异同
reshape()
可以由torch.reshape()
,也可由torch.Tensor.reshape()
调用。view()
只可由torch.Tensor.view()
来调用。- 对于一个将要被
view
的Tensor
,新的size
必须与原来的size
与stride
兼容。否则,在view
之前必须调用contiguous()
方法。 - 同样也是返回与input数据量相同,但形状不同的tensor。若满足view的条件,则不会copy,若不满足,则会copy。
- 如果您只想重塑张量,请使用
torch.reshape
。 如果您还关注内存使用情况并希望确保两个张量共享相同的数据,请使用torch.view
。
1.4、索引操作
Tensor的索引与Numpy类似,获取元素除了可以通过索引,还可以借助如下的一些函数。
函数 | 功能 |
---|---|
index_select(input,dim,index) | 在指定维度上选择一些行或列 |
nonzero(input) | 获取非0元素的下标 |
masked_select(input,mask) | 使用二元值进行选择 |
gather(input,dim,index) | 在指定维度上选择数据,输出的形状与index(index的类型继续是LongTensor类型)一致 |
scatter_(input,dim,index,src) | 为gather的反操作,根据指定索引补充数据 |
#设置一个随机种子
torch.manual_seed(100)
#生成一个形状为2×3的矩阵
x=torch.randn(2,3)
print(x)
tensor([[ 0.3607, -0.2859, -0.3938],
[ 0.2429, -1.3833, -2.3134]])
- 根据索引获取数据
#获取第一行的最后一列的所有数据
print(x[0,:])
print(x[:,-1])
tensor([ 0.3607, -0.2859, -0.3938])
tensor([-0.3938, -2.3134])
masked_select(input,mask)
获取数据
#生成是否大于0的Byter张量
mask=x>0
#获取大于0的值
print(torch.masked_select(x,mask))
tensor([0.3607, 0.2429])
- 获取非零元素下标,即行列索引
print(torch.nonzero(x))
tensor([[0, 0],
[0, 1],
[0, 2],
[1, 0],
[1, 1],
[1, 2]])
- gather(input,dim,index)`
在指定维度上选择数据,输出的形状与index(index的类型继续是LongTensor类型)一致
index=torch.LongTensor([[0,1,0]])
a=torch.gather(x,1,index)
print(x)
print(a)
tensor([[ 0.3607, -0.2859, -0.3938],
[ 0.2429, -1.3833, -2.3134]])
tensor([[ 0.3607, -0.2859, 0.3607]])
- 为gather的反操作,根据指定索引补充数据|
#把a的值返回到一个2×3的0矩阵中
z=torch.zeros(2,3)
z.scatter_(1,index,a)
print(z)
tensor([[ 0.3607, -0.2859, 0.0000],
[ 0.0000, 0.0000, 0.0000]])
1.5、广播机制
pytorch的广播机制与Numpy的相同
A=torch.arange(0,40,10).reshape(4,1)
B=torch.arange(0,3)
#s输出A,B的形状
print(A.shape,B.shape)
#Tensor自动实现广播
C=A+B
#输出C的形状和值
print(C)
print(C.shape)
torch.Size([4, 1]) torch.Size([3])
tensor([[ 0, 1, 2],
[10, 11, 12],
[20, 21, 22],
[30, 31, 32]])
torch.Size([4, 3])
1.6、逐元素操作
大部分数学运算都属于逐元素操作,其输入与输出的形状相同,常见的逐元素操作有以下几个
注意:这些操作均会创建新的Tensor
,如果需要就地操作,可以使用这些方法的下划线版本,例如abs_
。
函数 | 功能 |
---|---|
abs/add | 绝对值/加法 |
addcdiv(t,v,t1,t2) | t1与t2按元素除后,乘v加t |
addcmul(t,v,t1,t2) | t1与t2按元素乘后,乘v加t |
ceil/floor | 向上取整/向下取整 |
clamp(t,min,max) | 将张量元素限制在制定区间 |
exp/log/pow | 指数/对数/幂 |
mul(或*)/neg | 逐元素乘法/取反 |
sigmoid/tanh/softmax | 激活函数 |
sign/sqrt | 取符号/开根号 |
部分操作代码实例
t=torch.randn(1,3)
print("t:",t)
t1=torch.randn(3,1)
t2=torch.randn(1,3)
#计算t+0.1*(t1,t2)
ans1=torch.addcdiv(t,0.1,t1,t2)#使用了广播机制
print("计算t+0.1*(t1,t2):",ans1)
#计算sigmoid
print('计算sigmoid:',torch.sigmoid(t))
#将t限制在[0,1]之间
print(torch.clamp(t,0,1))
#对t+2进行就地运算
t.add_(2)
print("就地运算后的值:",t)
t: tensor([[-1.5035, -1.2893, 0.7983]])
计算t+0.1*(t1,t2): tensor([[-1.5613, -1.9269, 1.1673],
[-1.5439, -1.7351, 1.0563],
[-1.5575, -1.8859, 1.1435]])
计算sigmoid: tensor([[0.1819, 0.2160, 0.6896]])
tensor([[0.0000, 0.0000, 0.7983]])
就地运算后的值: tensor([[0.4965, 0.7107, 2.7983]])
1.7、归并操作
归并操作就是对输入进行归并或合计的操作,一般输入大于输出的形状,归并操作可以对整个Tensor,也可以沿着某个维度进行归并。下列是常用的归并操作。
函数 | 功能 |
---|---|
cumprod(t,axis) | 在指定维度上对t进行累积 |
cumsum(t,axis) | 在制定维度上对t进行累加 |
dist(a,b,p=2) | 返回a,b之间的p阶范数 |
mean/median | 均值/中位数 |
std/var | 标准差/方差 |
norm(t,p=2) | 返回t的p阶范数 |
prod(t)/sum(t) | 返回t的所有元素的积/和 |
说明:归并操作一般涉及一个dim
参数,指定沿哪个维度进行归并。另一个参数是keepdim
,说明输出结果中是否保留维度1,缺省情况是False,即不保留。
#生成一个含6个数的张量
a=torch.linspace(0,10,6)
print(a)
#使用view方法,把a变为2×3矩阵
a=a.view((2,3))
print(a)
#沿y轴方向累加,即dim=0
b1=a.sum(dim=0)
print("b1:",b1)
#k可以写成下式
b2=torch.sum(a,dim=0)
print("b2:",b2)
#沿y轴方向累加,即dim=0,并保留含1的维度
b3=torch.sum(a,dim=0,keepdim=True)
print("b3:",b3)
print("b3形状:",b3.shape)
tensor([ 0., 2., 4., 6., 8., 10.])
tensor([[ 0., 2., 4.],
[ 6., 8., 10.]])
b1: tensor([ 6., 10., 14.])
b2: tensor([ 6., 10., 14.])
b3: tensor([[ 6., 10., 14.]])
b3形状: torch.Size([1, 3])
1.8、比较操作
比较操作一般是进行逐元素比较,有些是按指定方向比较。下列是常用的比较函数。
函数 | 功能 |
---|---|
eq | 比较Tensor是否相等,支持broadcast |
equal | 比较Tensor是否有相同的shape与值 |
ge/le/gt/lt | 大于/小于比较/大于等于 |
max/min(t,axis) | 返回最值,若指定axis,则额外返回下标 |
topk(t,k,axis) | 在指定维度上取最大的K个值 |
部分函数的代码
#构造一个2×3的张量
x=torch.linspace(0,10,6).reshape(2,3)
print("x:",x)
#求所有元素的最大值
res1=torch.max(x)
print('所有元素最大值:',res1)
#求y轴方向的最大值
res2=torch.max(x,dim=0)
print('求y轴方向的最大值:',res2)
#求y轴方向最大的两个元素
res3=torch.topk(x,1,dim=0)
print('y轴方向最大的两个元素:',res3)
x: tensor([[ 0., 2., 4.],
[ 6., 8., 10.]])
所有元素最大值: tensor(10.)
求y轴方向的最大值: torch.return_types.max(
values=tensor([ 6., 8., 10.]),
indices=tensor([1, 1, 1]))
y轴方向最大的两个元素: torch.return_types.topk(
values=tensor([[ 6., 8., 10.]]),
indices=tensor([[1, 1, 1]]))
1.9、矩阵操作
常用的矩阵运算有两种:一种是逐元素乘法,另一种是点积乘法。常用的矩阵运算有。
函数 | 功能 |
---|---|
dot(t1,t2) | 计算张量(1D)的内积或点积 |
matmul(mat1,mat2)/mm(mat1,mat2)/bmm(batch1,batch2) | 计算矩阵乘法/含矩阵的3D矩阵乘法 |
mv(t1,v1) | 计算矩阵与向量的乘法 |
t() | 转置 |
svd(t) | 计算t的SVD分解 |
Torch
的dot
与Numpy
的dot
有点不同,Torch
中的dot
是对两个为1D张量进行点积运算,Numpy
中的dot
无此限制。mm
是对2D的矩阵进行点积,bmm
对含batch的3D进行点积运算。- 转置运算会导致存储空间不连续,需要调用
contiguous
方法转为连续。
a=torch.tensor([2,3])
b=torch.tensor([3,4])
print("a:",a)
print("a:",b)
#张量的点积或内积
print('a,b内积:',torch.dot(a,b))
x=torch.randint(10,(2,3))#生成一定范围的整数
y=torch.randint(6,(3,4))
print("x:",x)
print("y:",y)
print('X,Y矩阵乘法:',torch.mm(x,y))
x1=torch.randint(10,(2,2,3))
y1=torch.randint(6,(2,3,4))
print("x1:",x1)
print("y1:",y1)
print('x1,y1 3D矩阵乘法:',torch.bmm(x1,y1))
a: tensor([2, 3])
a: tensor([3, 4])
a,b内积: tensor(18)
x: tensor([[5, 6, 5],
[9, 8, 5]])
y: tensor([[2, 5, 0, 2],
[3, 1, 4, 3],
[4, 3, 5, 2]])
X,Y矩阵乘法: tensor([[48, 46, 49, 38],
[62, 68, 57, 52]])
x1: tensor([[[7, 4, 6],
[6, 7, 1]],
[[1, 3, 4],
[1, 4, 9]]])
y1: tensor([[[4, 0, 4, 1],
[1, 5, 1, 3],
[0, 5, 0, 4]],
[[2, 3, 2, 5],
[4, 1, 3, 3],
[3, 1, 2, 3]]])
x1,y1 3D矩阵乘法: tensor([[[32, 50, 32, 43],
[31, 40, 31, 31]],
[[26, 10, 19, 26],
[45, 16, 32, 44]]])
2、Tensor与Autograd
在训练神经网络时,最常用的算法是反向传播。在该算法中,参数根据损失函数相对于给定参数的梯度进行调整。为了计算这些梯度,Pytorch有一个内置的微分引擎,称为torch.autograd
。它支持任何计算图的梯度自动计算。
2.1、自动求导要点
为实现对Tensor
自动求导,需要考虑以下几点。
- 创建叶子节点的
Tensor
,使用requires_grad
参数指定是否记录对其的操作,以便之后利用backward()
方法进行梯度求解。requires_grad
参数的缺省值为False
。如果要对其求导需要设置为True
,然后与之有依赖关系的节点会自动变为True
。 - 可以利用
requires_grad_()
方法将修改Tensor
的requires_grad
属性。可以调用detach()
或with torch.no_grad():
,将不再计算张量的梯度,跟踪张量的历史记录。在评估模型、测试模型阶段中经常使用到。 - 通过运算创建的
Tensor(非叶子节点)
,会被自动赋予grad_fn
属性。该属性表示梯度函数。叶子节点的grad_fn
为None。 - 最后得到的
Tensor
执行backward()
函数,此时自动计算各变量的梯度,并将梯度累加结果保存到grad属性中。计算完成后,非叶子节点的梯度自动释放。 backward()
函数接收参数,该参数应该和调用backward()
函数的Tensor
的维度相同,或者是可broadcast
的维度。如果求导的Tensor
为标量,则backward
中的参数可以省略。- 反向传播的中间缓存会被清空,如果需要进行多次反向传播,需要指定
backward
中的参数retain_graph=True
。多次反向传播时,梯度是累加的。 - 非叶子节点的梯度
backward
调用后即被清空。 - 可以通过使用
torch.no_grad()
包裹代码块的形式来阻止autograd
去跟踪那些标记为.requesgrad=True
的张量的历史记录。这步在测试阶段经常使用。
2.2、计算图
计算图是一种有向无环图,用图形的方式来表示算子与变量之间的关系,直观高效。如下图,圆形代表变量,矩形代表算子。如表达式z=wz+b
,可以写成两个表达式:y=wx
,z=y+b
,其中x,w,b
为变量,是用户创建的变量,不依赖与其他变量,故称为叶子节点。为了计算各叶节点的梯度,需要把对应的张量参数requires_grad
属性设置为True
,(这样与之有依赖关系的y和z的requires_grad
属也为True
),就可以跟踪其历史记录。y,z是计算得到的变量,非叶子节点,z为根节点。mul和add是算子。由这些变量及算子,就构成了一个完整的计算过程(或前向传播过程)。
可以根据链式法则计算各叶子节点的梯度。
∂
z
∂
x
=
∂
z
∂
y
∂
y
∂
x
=
w
\frac{\partial {z}}{\partial {x}}=\frac{\partial {z}}{\partial {y}}\frac{\partial {y}}{\partial {x}}=w
∂x∂z=∂y∂z∂x∂y=w
∂
z
∂
w
=
∂
z
∂
y
∂
y
∂
w
=
x
\frac{\partial {z}}{\partial {w}}=\frac{\partial {z}}{\partial {y}}\frac{\partial {y}}{\partial {w}}=x
∂w∂z=∂y∂z∂w∂y=x
∂
z
∂
b
=
1
\frac{\partial {z}}{\partial {b}}=1
∂b∂z=1
Pytorch
调用backward()
方法,将自动计算各节点的梯度。在反向传播过程中,autograd
沿着下图,从当前根节点z反向溯源,利用导数链式法则,计算所有叶子节点的梯度,将梯度值累加到grad
属性中。非叶子节点的计算操作记录在grad_fn
属性中,叶子节点的grad_fn
值为None
。
2.3、反向传播
考虑最简单的一层神经网络,具有输入x、参数w和b和一些损失函数,通过以下方式在Pytorch
中进行定义。
import torch
x=torch.ones(5)#全为1,长度为5向量
y=torch.zeros(3)
#定义参数w,requires_grad设置为True,可计算梯度
w=torch.randn(5,3,requires_grad=True)
#定义参数b,requires_grad设置为True,可计算梯度
b=torch.randn(3,requires_grad=True)
#计算结果
#torch.matmul是tensor的乘法,与tensor.mm类似
z=torch.matmul(x,w)+b
loss=torch.nn.functional.binary_cross_entropy_with_logits(z,y)
此代码定义的计算图为:
在这个网络中,w和b是需要优化的参数。因此,需要能够计算损失函数相对于这些变量的梯度。为了做到这一点,为这些张量设置了requires_grad
属性。
可以在创建张量的时候使用requires_grad
设置,也可以在稍后使用x.requires_grad_(True)
方法设置。
我们应用于张量来构造计算图的函数实际上是一个函数类的对象。这个对象知道如何在前进方向上计算函数,也知道如何在后向传播步骤中计算其导数。对后向传播函数的引用被存储在张量的grad_fn属性中。
print(f"z的梯度函数={z.grad_fn}")
print(f"loss的梯度函数={loss.grad_fn}")
z的梯度函数=<AddBackward0 object at 0x00000228BE6AF688>
loss的梯度函数=<BinaryCrossEntropyWithLogitsBackward0 object at 0x00000228BE6AF2C8>
计算梯度
为了优化神经网络中参数的权重,我们需要计算我们的损失函数对参数的导数,在x和y的一些固定值下,计算
∂
l
o
s
s
w
\frac{\partial {loss}}{w}
w∂loss和
∂
l
o
s
s
b
\frac{\partial {loss}}{b}
b∂loss。为了计算这些导数,我们调用loss.backward()
,然后从w.grad
和b.grad
中检索出数值。
#反向传播
loss.backward()
#输出导数
print(w.grad)
print(b.grad)
tensor([[0.3231, 0.0049, 0.3013],
[0.3231, 0.0049, 0.3013],
[0.3231, 0.0049, 0.3013],
[0.3231, 0.0049, 0.3013],
[0.3231, 0.0049, 0.3013]])
tensor([0.3231, 0.0049, 0.3013])
注意:
- 我们只能获得计算图的叶子节点的梯度属性,这些节点的
requires_grad
属性设置为True
。对于我们图中的所有其他节点,梯度将不可用。 - 出于性能方面的考虑,我们在一个给定的图形上只能使用后向计算一次。如果我们需要在同一个图形上进行多次后向调用,我们需要在后向调用中传递 retain_graph=True。
禁用梯度跟踪
默认情况下,所有带有require_grad=True
的张量都在跟踪其计算历史,并支持梯度计算。然而,在有些情况下,我们不需要这样做,例如,当我们已经训练好了模型,只是想把它应用于一些输入数据,即我们只想通过网络进行前向计算。我们可以通过用torch.no_grad()
块包围我们的计算代码来停止跟踪计算。
z=torch.matmul(x,w)+b
print(z.requires_grad)
with torch.no_grad():
z=torch.matmul(x,w)+b
print(z.requires_grad)
True
False
另一种实现相同结果的方法是在张量上使用detach()
方法。
z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
False
2.4、非标量反向传播
目标是否为标量,如损失函数为标量
当目标张量为标量的时候,可以调用backward()
方法且无需传入参数。目标张量一般都是标量,如经常使用的损失Loss。当然也会有非标量的情况,这时应该如何进行反向传播?Python
有个规定,不让张量对张量求导,只允许标量对张量进行求导,因此,如果目标张量对一个非标量调用backward()
,则需要传入一个gradient
参数,该参数也是张量,而且需要与调用backward()
的张量形状相同。
传入这个参数就是为了把张量对张量的求导转换成标量对张量的求导。例如,假设目标值为 l o s s = ( y 1 , y 2 , c d o t s , y m ) loss=(y_1,y_2,cdots,y_m) loss=(y1,y2,cdots,ym),传入的参数为 v = ( v 1 , v 2 , c d o t s , v m ) v=(v_1,v_2,cdots,v_m) v=(v1,v2,cdots,vm),那么就可以把对loss的求导,转换成对 l o s s ∗ v T loss*v^T loss∗vT标量的求导。可以看成把原来 ∂ l o s s ∂ x \frac{\partial {loss}}{\partial {x}} ∂x∂loss得到的雅克比矩阵乘以张量 v T v^T vT,便可以得到我们需要的矩阵。
backward
函数的格式为:backward(gradient=None,retain_graph=None,create_graph=False)
下面通过一个实例说明:
- 定义叶子节点及计算节点
import torch
#定义叶子节点张量x,形状为1×2
x=torch.tensor([[2,3]],dtype=torch.float,requires_grad=True)
#初始化雅克比矩阵
J=torch.zeros(2,2)
#初始化目标张量,形状为1×2
y=torch.zeros(1,2)
#定义与x之间的关系:
#y1=x1**2+3*x2 y2=x2**2+2*x1
y[0,0]=x[0,0]**2+3*x[0,1]
y[0,1]=x[0,1]**2+2*x[0,0]
print(y)
tensor([[13., 13.]], grad_fn=<CopySlices>)
- 手工计算y对x的梯度
y对x的矩阵是一个雅克比矩阵,通过下面的方法进行计算。
假设 x = ( x 1 = 2 , x 2 = 3 ) , y = ( y 1 = x 1 2 + 3 x 2 , y 2 = x 2 2 + 2 x 1 ) x=(x_1=2,x_2=3),y=(y_1=x_1^2+3x_2,y_2=x_2^2+2x_1) x=(x1=2,x2=3),y=(y1=x12+3x2,y2=x22+2x1),可以得到:
J
=
(
∂
y
1
∂
x
1
∂
y
1
∂
x
2
∂
y
2
∂
x
1
∂
y
2
∂
x
2
)
=
(
2
x
1
3
2
2
x
2
)
J=\begin{pmatrix}\frac{\partial{y_1}}{\partial{x_1}} & \frac{\partial{y_1}}{\partial{x_2}} \\ \frac{\partial{y_2}}{\partial{x_1}} & \frac{\partial{y_2}}{\partial{x_2}} \\ \end{pmatrix}=\begin{pmatrix}2x_1 & 3 \\ 2 & 2x_2\\ \end{pmatrix}
J=(∂x1∂y1∂x1∂y2∂x2∂y1∂x2∂y2)=(2x1232x2)
当
x
1
=
2
,
x
2
=
3
x_1=2,x_2=3
x1=2,x2=3时,
J
=
(
4
3
2
6
)
J=\begin{pmatrix}4 & 3 \\ 2 & 6\\ \end{pmatrix}
J=(4236)
所以:
J
T
=
(
4
2
3
6
)
J^T=\begin{pmatrix}4 & 2 \\ 3 & 6\\ \end{pmatrix}
JT=(4326)
- 调用backward来获取y对x的梯度
#进行多次反向传播的时候需要设置:retain_graph=True
y.backward(torch.Tensor([[1,1]]),retain_graph=True)
print(x.grad)
tensor([[6., 9.]])
这个结果于手工运算的不符,这个结果是错误的,这个结果的计算过程为:
J
T
⋅
v
T
=
(
4
2
3
6
)
(
1
1
)
J^T\cdot v^T=\begin{pmatrix}4 & 2 \\ 3 & 6\\ \end{pmatrix}\begin{pmatrix}1 \\ 1\\ \end{pmatrix}
JT⋅vT=(4326)(11)
可以看出,错在v的取值,通过这种方式得到的并不是y对x的梯度。因此可以分成两步计算。首先让
v
=
(
1
,
0
)
v=(1,0)
v=(1,0)得到
y
1
y_1
y1对x的梯度,然后使
v
=
(
0
,
1
)
v=(0,1)
v=(0,1),得到
y
2
y_2
y2对x的梯度。这里因需要重复使用backward()
,需要使用retain_graph=True
,具体代码如下:
#对x的梯度清零
x.grad.zero_()
#生成y1对x的梯度
y.backward(torch.Tensor([[1,0]]),retain_graph=True)
J[0]=x.grad
#梯度是累加的,故需要对x的梯度清零
x.grad.zero_()
#生成y2对x的梯度
y.backward(torch.Tensor([[0,1]]))
J[1]=x.grad
#显式雅克比矩阵
print(J)
tensor([[4., 3.],
[2., 6.]])
3、使用Pytorch实现简单线性回归
使用Pytorch
实现一个简单的线性回归任务。
首先给出一个数组 x x x,然后基于表达式 y = 3 x 2 + 2 y=3x^2+2 y=3x2+2,加上一些噪声数据得到另一组数据y。
然后构建机器学习模型,学习表达式 y = w x 2 + b y=wx^2+b y=wx2+b的两个参数w、b。利用数组x,y的数据作为训练数据。
最后使用梯度下降算法,通过多次迭代,学习到w、b的值。
- 导入所需要的库
%matplotlib inline
import torch
from matplotlib import pyplot as plt
- 生成训练数据,并可视化数据分布情况
#生成数据的函数
def synthetic_data(w,b,num_example):
#为CPU设置种子用于生成随机数,以使得结果是确定的
torch.manual_seed(100)
#生成x坐标数据,区间[-1,1)的num_example个数据
x=torch.linspace(-1,1,num_example)
#需要吧x的形状转为100X1
x=torch.unsqueeze(x,dim=1)
#生成y坐标数据,并为其加上噪声
y=w*x.pow(2)+b
y=y+0.2*torch.rand(x.size())
return x,y
#生成数据
x,y=synthetic_data(3,2,100)
#绘图,吧tensor转为numpy数据
plt.scatter(x.numpy(),y.numpy())
plt.show()
- 初始化模型参数
#w初始化为一个随机的1×1的张量,requires_grad设置为True
w=torch.randn(1,1,dtype=torch.float,requires_grad=True)
#b初始化为0,requires_grad设置为True
b=torch.zeros(1,1,dtype=torch.float,requires_grad=True)
- 定义模型,损失函数,优化算法
#定义模型
def model(x,w,b):
#wx^2+b
return x.pow(2).mm(w)+b
#定义损失函数,使用平方损失函数
def squared_loss(y_hat,y):
return (y_hat-y.reshape(y_hat.shape))**2/2
#使用批量梯度下降
def bgd(params,lr):
#批量梯度下降
with torch.no_grad():
for param in params:
param-=lr*param.grad
param.grad.zero_()
- 训练模型
#学习率
lr=0.001
#训练轮次
num_epochs=400
net=model
loss=squared_loss
for epoch in range(num_epochs):
#前向传播
y_hat=model(x,w,b)
#计算损失
l=loss(y_hat,y).sum()
#自动计算梯度
l.backward()
#更新参数
bgd([w,b],lr)
if (epoch+1)%50==0:
with torch.no_grad():
train_l=loss(model(x,w,b),y)
#mean()函数功能:求取均值
print(f'epoch{epoch+1},loss{float(train_l.mean()):f}')
epoch50,loss0.061276
epoch100,loss0.027782
epoch150,loss0.013114
epoch200,loss0.006685
epoch250,loss0.003867
epoch300,loss0.002632
epoch350,loss0.002090
epoch400,loss0.001853
- 可视化训练结果
plt.plot(x.numpy(),y_hat.detach().numpy(),'r-',label='predict')
plt.plot(x.numpy(),y.numpy(),marker='o',label='true')
plt.xlim(-1,1)
plt.ylim(2,6)
plt.legend()
plt.show()