Chapter2 Pytorch基础

本文介绍了PyTorch中的张量操作,包括创建、修改形状、索引、广播机制、逐元素操作、归并操作、比较操作和矩阵操作。此外,详细阐述了张量与自动求导的关系,讨论了计算图、反向传播的原理,并通过一个简单的线性回归例子展示了如何使用PyTorch进行模型训练。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、Numpy与Tensor

Tensor自称为神经网络界的Tensor,与Numpy相似,二者可以共享内存,且之间的的转换非常方便和高效。Numpy中的ndarray智能在CPU中进行运算,Torch中的Tensor既可以在CPU也可以在GPU上运行。

1.1、Tensor概述

Tensor的操作很多,从接口的角度划分,可以分成两类:

  1. torch.function, 如torch.sumtorch.add等。
  2. tensor.function, 如tensor.viewtensor.add等。

这些操作对大部分Tensor都是等价的,如torch.add(x,y)x.add(y)等价。

从修改方式的角度来划分,可以分为以下两类

  1. 不修改自身数据,如x.add(y),x的数据不变,返回一个新的Tensor
  2. 修改自身数据,如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.Tensortorch.tensor的区别

  1. torch.Tensortorch.emptytorch.tensor之间的一种混合,但是,当传入数据时,torch.Tensor使用全局默认dtype(FloatTensor),而torch.tensor是从数据中推断出数据类型。
  2. 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的异同

  1. reshape()可以由torch.reshape(),也可由torch.Tensor.reshape()调用。view()只可由torch.Tensor.view()来调用。
  2. 对于一个将要被viewTensor,新的size必须与原来的sizestride兼容。否则,在view之前必须调用contiguous()方法。
  3. 同样也是返回与input数据量相同,但形状不同的tensor。若满足view的条件,则不会copy,若不满足,则会copy。
  4. 如果您只想重塑张量,请使用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]])
  1. 根据索引获取数据
#获取第一行的最后一列的所有数据
print(x[0,:])
print(x[:,-1])
tensor([ 0.3607, -0.2859, -0.3938])
tensor([-0.3938, -2.3134])
  1. masked_select(input,mask)获取数据
#生成是否大于0的Byter张量
mask=x>0
#获取大于0的值
print(torch.masked_select(x,mask))
tensor([0.3607, 0.2429])
  1. 获取非零元素下标,即行列索引
print(torch.nonzero(x))
tensor([[0, 0],
        [0, 1],
        [0, 2],
        [1, 0],
        [1, 1],
        [1, 2]])
  1. 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]])
  1. 为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分解
  1. TorchdotNumpydot有点不同,Torch中的dot是对两个为1D张量进行点积运算,Numpy中的dot无此限制。
  2. mm是对2D的矩阵进行点积,bmm对含batch的3D进行点积运算。
  3. 转置运算会导致存储空间不连续,需要调用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自动求导,需要考虑以下几点。

  1. 创建叶子节点的Tensor,使用requires_grad参数指定是否记录对其的操作,以便之后利用backward()方法进行梯度求解。requires_grad参数的缺省值为False。如果要对其求导需要设置为True,然后与之有依赖关系的节点会自动变为True
  2. 可以利用requires_grad_()方法将修改Tensorrequires_grad属性。可以调用detach()with torch.no_grad():,将不再计算张量的梯度,跟踪张量的历史记录。在评估模型、测试模型阶段中经常使用到。
  3. 通过运算创建的Tensor(非叶子节点),会被自动赋予grad_fn属性。该属性表示梯度函数。叶子节点的grad_fn为None。
  4. 最后得到的Tensor执行backward()函数,此时自动计算各变量的梯度,并将梯度累加结果保存到grad属性中。计算完成后,非叶子节点的梯度自动释放。
  5. backward()函数接收参数,该参数应该和调用backward()函数的Tensor的维度相同,或者是可broadcast的维度。如果求导的Tensor为标量,则backward中的参数可以省略。
  6. 反向传播的中间缓存会被清空,如果需要进行多次反向传播,需要指定backward中的参数retain_graph=True。多次反向传播时,梯度是累加的。
  7. 非叶子节点的梯度backward调用后即被清空。
  8. 可以通过使用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 xz=yzxy=w
∂ z ∂ w = ∂ z ∂ y ∂ y ∂ w = x \frac{\partial {z}}{\partial {w}}=\frac{\partial {z}}{\partial {y}}\frac{\partial {y}}{\partial {w}}=x wz=yzwy=x
∂ z ∂ b = 1 \frac{\partial {z}}{\partial {b}}=1 bz=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} wloss ∂ l o s s b \frac{\partial {loss}}{b} bloss。为了计算这些导数,我们调用loss.backward(),然后从w.gradb.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 lossvT标量的求导。可以看成把原来 ∂ l o s s ∂ x \frac{\partial {loss}}{\partial {x}} xloss得到的雅克比矩阵乘以张量 v T v^T vT,便可以得到我们需要的矩阵。

backward函数的格式为:backward(gradient=None,retain_graph=None,create_graph=False)

下面通过一个实例说明:

  1. 定义叶子节点及计算节点
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>)
  1. 手工计算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=(x1y1x1y2x2y1x2y2)=(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)

  1. 调用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} JTvT=(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的值。

  1. 导入所需要的库
%matplotlib inline
import torch
from matplotlib import pyplot as plt
  1. 生成训练数据,并可视化数据分布情况
#生成数据的函数
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()


在这里插入图片描述

  1. 初始化模型参数
#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)
  1. 定义模型,损失函数,优化算法
#定义模型
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_()
  1. 训练模型
#学习率
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
  1. 可视化训练结果
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()


在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值