欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

机器学习笔记4-0:PyTorch简明教程

程序员文章站 2022-06-11 22:00:54
...

*注:本博客参考李宏毅老师2020年机器学习课程. 视频链接


1 Tensor

Tensor是PyTorch中最基础的一种数据结构,与Numpy中的ndarray相似,用来存储数据,并封装了许多对于数据和向量的操作。

1.1 Tensor创建

1.1.1 从数据创建

创建Tensor的基本方式是从数据创建:

torch.tensor(data, #数据内容,可以是list,tuple或ndarray类型
             dtype=None, #Tensor中数据的类型,Numpy中也有一样的参数
             device=None, #该Tensor所在的cuda/cpu设备,设置为cuda即可使用GPU加速
             requires_grad=False) #是否需要对该Tensor求梯度
t1=torch.tensor([1,2,3]) #从list创建
t2=torch.tensor(np.ones((1,2))) #从ndarray创建
print(t1)
print(t2)
tensor([1, 2, 3])
tensor([[1., 1.]], dtype=torch.float64)

此外,还有一种方式是使用torch.as_tensor,也可以从list,tuple,ndarray等数据结构创建,但是使用该方法创建的Tensor与ndarray内存共享,这意味着其中一个改变时,另一个也将改变,而torch.tensor则是创建一个新的变量,不会共享内存。

a=np.array([1,2,3])
t1=torch.as_tensor(a)
t2=torch.tensor(a)
print(t1,t2)
a*=3 #改变ndarray的值
print(t1,t2)
tensor([1, 2, 3], dtype=torch.int32) tensor([1, 2, 3], dtype=torch.int32)
tensor([3, 6, 9], dtype=torch.int32) tensor([1, 2, 3], dtype=torch.int32)

使用torch.from_numpy也可以从ndarray创建Tensor,且共享内存,但是该方法的参数只能是ndarray。对三种方法总结如下:

方法名 参数 共享内存
torch.tensor list,tuple,或ndarray
torch.as_tensor list,tuple,或ndarray
torch.from_numpy ndarray

1.1.2 创建特殊值的Tensor

在Numpy中可以使用zeros、ones等方法创建全为0或1的ndarray,PyTorch中也有类似的操作,见下表:

方法名 参数 功能解释
torch.ones size 创建指定size的全为1的Tensor
torch.zeros size 创建指定size的全为0的Tensor
torch.full size,value 创建被value填充的指定size的Tensor
torch.eye n,m=None 创建n行m列的对角线为1,其余全为0的Tensor,m默认等于n
torch.empty size,out=None 创建size大小的全为0的Tensor,若指定out,则以out对应索引填充

上述使用到size参数的方法均可以在方法名之后添加*_like*,既可以将size参数换成一个Tensor,创建与该Tensor的size相同的Tensor。

1.1.3 创建指定步长的Tensor

有时我们需要在两个值之间等距离地取点,从而创建一个列表。在PyTorch中,可以使用如下两种方式实现:

t1=torch.linspace(0,100,20) #在[0,100]中等间距地取20个点
t2=torch.arange(0,100,20) #在[0,100]中每隔20取一个点
print(t1)
print(t2)
tensor([  0.0000,   5.2632,  10.5263,  15.7895,  21.0526,  26.3158,  31.5789,
         36.8421,  42.1053,  47.3684,  52.6316,  57.8947,  63.1579,  68.4211,
         73.6842,  78.9474,  84.2105,  89.4737,  94.7368, 100.0000])
tensor([ 0, 20, 40, 60, 80])

1.2 Tensor组合与变换

1.2.1 两个Tensor拼接

使用torch.cat拼接两个Tensor,默认从第0个维度拼接,使用参数dim可以指定维度。但是必须保证在dim之后的维度的size一致。例如size分别为(2,3,2)和(1,3,2)的两个Tensor不能不能在第0维度拼接,但是第1和第2都可以。

t1=torch.ones((1,2))
t2=torch.zeros((1,2))
print(t1,t2)
print(torch.cat((t1,t2)))#在第0个维度上拼接两个Tensor
print(torch.cat((t1,t2),dim=1))#在第1个维度上拼接两个Tensor
tensor([[1., 1.]]) tensor([[0., 0.]])
tensor([[1., 1.],
        [0., 0.]])
tensor([[1., 1., 0., 0.]])

1.2.2 Tensor变形

将一个size为(1,2,3)的Tensor变形为size(3,2)的Tensor,可以使用如下两种方式,两种方式均不会改变Tensor本来的size,而是将变形后的Tensor以克隆的方式返回。

t=torch.ones((1,2,3))
print(t.reshape(3,2).size())
print(t.view(3,2).size())
torch.Size([3, 2])
torch.Size([3, 2])

1.2.3 Tensor维度操作

对维度的操作包括增减维度,交换维度等,以下给出了一些例子:

t=torch.ones((1,2,3,1))
print(t.squeeze().size()) #去除Tensor中所有长为1的维度
print(t.unsqueeze(dim=1).size()) #在指定的dim上添加一个维度
print(t.transpose(0,1).size()) #交换第0和第1个维度
print(t.T.size()) #转置,相当于把所有维度颠倒
print("-"*15)
for t_ in t.unbind(dim=1):
    print(t_.size()) #按指定dim拆分Tensor,相当于去掉这一个维度,若对应dim的长度为n,则会返回一个长为n的tuple
torch.Size([2, 3])
torch.Size([1, 1, 2, 3, 1])
torch.Size([2, 1, 3, 1])
torch.Size([1, 3, 2, 1])
---------------
torch.Size([1, 3, 1])
torch.Size([1, 3, 1])

1.3 Tensor运算

1.3.1 四则运算

Tensor之间直接使用四则运算符即可进行四则运算:

a=torch.tensor([12,4,6])
b=torch.tensor([4,2,3])
print("a+b={}".format(a+b))
print("a-b={}".format(a-b))
print("a*b={}".format(a*b))
print("a/b={}".format(a/b))
a+b=tensor([16,  6,  9])
a-b=tensor([8, 2, 3])
a*b=tensor([48,  8, 18])
a/b=tensor([3., 2., 2.])

1.3.2 矩阵乘法与向量内积

按照线性代数矩阵乘法的要求,如果两个矩阵可积,则可以使用.dot函数或者@运算符进行矩阵乘法运算;特别的,如果进行运算的两个矩阵均为一维,且具有相同的长度,则进行向量内积:

print("a.shape:{}".format(a.shape))
print("b.shape:{}".format(b.shape))
print([email protected]) #向量内积
c=torch.tensor([[1,2,3],[3,4,5]])
d=torch.tensor([[1,2],[3,4],[5,6]])
print("c.shape:{}".format(c.shape))
print("d.shape:{}".format(d.shape))
print([email protected]) #矩阵乘法
a.shape:torch.Size([3])
b.shape:torch.Size([3])
tensor(74)
c.shape:torch.Size([2, 3])
d.shape:torch.Size([3, 2])
tensor([[22, 28],
        [40, 52]])

1.3.3 求导

如果要计算一个式子对于某一个变量的导数,需要以下三个步骤:

  1. 声明需要求导的Tensor,设置其requires_grad属性为True;
  2. 执行所需的运算,对运算所得的Tensor调用backward()方法;
  3. 对需要求导的Tensor调用grad方法。

注意:PyTorch只能对dtype为浮点类型的Tensor求导,所以如果是手动输入数据,需要添加一个小数点。

x=torch.tensor(5.,requires_grad=True)
y=3*x+4*x**2
y.backward()
print(x.grad)
tensor(43.)

backward()方法会对Tensor的所有标记为requires_grad的Tensor x求导,但是如果x并不是一个一维的变量,则需要传入一个参数,指示x的size,一般可以使用.clone().detach()方法获得一个与x相同size的Tensor,传入函数。

x=torch.tensor([[2.,5.],[4.,6.]],requires_grad=True)
y=3*x+4*x**2
y.backward(x.clone().detach())
print(x.grad)
tensor([[ 38., 215.],
        [140., 306.]])

2 网络模型

在前面的章节中,我们手动实现了线性回归和逻辑回归,目的是更深入的理解这些模型,但是在实际科研场景下,PyTorch能够为我们提供更为简洁、方便的实现方式,很多网络模型在PyTorch的torch.nn模块中已经实现了。

2.1 线性模型

首先来看线性模型,torch.nn.Linear(),该模型的输入是一个Tensor,该方法需要指定两个参数,in_feature和out_feature,这两个参数决定了模型的输入维度和输出维度的长度,相当于一个单层的神经网络。当输入包含多个维度时,线性模型将最后一个维度视为模型的输入,因此模型输入的最后一维的长度必须与模型参数的in_feature相同。


当调用torch.nn.Linear()的同时,这个线性模型的权重和偏置就已经初始化为随机值了,因此可以访问其值。

model=torch.nn.Linear(3,4) #模型输出维度长为4
x=torch.ones((2,3)) #size为(2,3)的输入
y=model(x)
print(y.shape) #输出
print(model.weight.shape) #模型权重
print(model.bias.shape) #模型偏置
torch.Size([2, 4])
torch.Size([4, 3])
torch.Size([4])

2.2 **函数

在前面的章节中,我们介绍了sigmoid、ReLU等**函数,这些函数在torch.nn模块中也已经定义完成,直接调用即可。

activation_fn=torch.nn.Sigmoid()
z=torch.ones([2,3])
y=activation_fn(z)
print(y)
tensor([[0.7311, 0.7311, 0.7311],
        [0.7311, 0.7311, 0.7311]])

2.3 模型堆叠

假设我们需要设计一个比较复杂的模型,该模型包含很多层线性模型,同时每一个线性模型之后都要使用到**函数,还可能需要套经过三角函数、求和、求平均等等一系列操作。一种可行的做法是将每一个步骤都调用torch.nn中的对应模块,再将每一个模块的输入和下一个模块的输出连接起来。但其实PyTorch为我们提供了更为简洁的方式,即使用torch.nn.Sequential方法,该方法的参数可以包含任意个torch.nn中的模块,调用该方法之后PyTorch会自动将前一个模块的输出作为下一个模块的输入。

model = torch.nn.Sequential(
    torch.nn.Linear(3, 4),
    torch.nn.Sigmoid(),
    torch.nn.Linear(4, 8),
    torch.nn.Sigmoid(),
    torch.nn.Linear(8, 2),
    torch.nn.Softmax(1))
x = torch.ones((2, 3))
y = model(x)
print(y.shape)
print(y)
torch.Size([2, 2])
tensor([[0.4249, 0.5751],
        [0.4249, 0.5751]], grad_fn=<SoftmaxBackward>)

使用torch.nn.Sequential构建的模型的参数库使用.parameters()方法得到。

for p in model.parameters():
    print(p.shape)
torch.Size([4, 3])
torch.Size([4])
torch.Size([8, 4])
torch.Size([8])
torch.Size([2, 8])
torch.Size([2])

2.4 损失函数

在torch.nn模块中,还定义了许多典型的损失函数可供选择,如均方差torch.nn.MSELoss,交叉熵torch.nn.CrossEntropyLoss等等。

2.5 优化器

有了上述知识之后,其实我们已经可以动手实现一个神经网络,并通过梯度下降算法来更新模型的参数。但是实际上,这个步骤在PyTorch中也可以更简单,下面我们来看这样一个例子:

# 定义数据集
x = torch.ones(2)
true_w = torch.tensor([[2., 3]])
y = [email protected]
# 定义模型,取消bias
model = torch.nn.Linear(2, 1, bias=False)
# 定义损失函数
mse_loss = torch.nn.MSELoss()
# 定义优化器,使用随机梯度下降算法
optim = torch.optim.SGD(model.parameters(), lr=1e-2)
# 查看初始的模型参数:
print("--before trainning:")
print(model.weight[0].detach().numpy())
# 将输入送入模型,计算损失函数值
for i in range(100):
    y1 = model(x)  # 计算输出
    loss = mse_loss(y, y1)  # 计算损失函数
    loss.backward()  # 反向传播
    optim.step()  # 该步骤将会利用反向传播得到的偏导更新参数
    optim.zero_grad()  # 清空梯度,否则每次迭代都会导致上一行计算的梯度堆叠
print("--after trainning:")
print(model.weight[0].detach().numpy())

--before trainning:
[-0.63558334 -0.05096328]
--after trainning:
[2.1597238 2.744343 ]

在上述代码中,我们首先定义了数据集,尽管该数据集只包含一个数据点。接着我们定义了一个简单的线性模型,该模型的参数会被自动标记为require_grad。下一行将损失函数定义为均方差。紧接着我们定义了一个SGD优化器,该优化器即使用了SGD算法,在计算梯度之后,只需调用优化器的step()方法,就能根据梯度对模型的各个参数进行求导。但是需要注意,声明优化器对象时,需要将网络的参数传递给优化器对象,这样它才能知道需要更新哪些参数,此外,还可以通过lr参数指定学习速率。之后再循环中的步骤就是一轮迭代所需要做的事情:计算输出,计算loss,反向传播,更新参数。值得注意的是,由于model的参数被标记为需要求导,因此PyTorch将会自动记录所有与其参数有关的运算,在反向传播的时候依照链式求导法则计算偏导值,因此如果不消除上一次迭代对模型参数的操作,将会导致求梯度的链越来越深,因此我们在每一次迭代中都调用了优化器的.zero_grad()方法,该方法能够清空之前对模型参数的操作。


下面我们以一种更为现实的例子来说明堆叠模型所产生的线性结构的有效性。

  • 数据集方面,我们使用-2到2的100个元素构成的等差数列,y= s i n ( x π ) − c o s ( x π ) sin(x\pi)-cos(x\pi) sin(xπ)cos(xπ)
  • 构建模型的过程中,我们使用到了一种新的**函数——双曲正切函数,torch.nn.Tanh;
  • 损失函数依然使用均方差;
  • 优化器选择SGD优化器,学习速率被设置为0.05,除此之外,momentum参数表示动量,即更新参数的过程中,上一次参数更新的变化值对这一次参数更新的印象程度,这里设为0.5;
  • 在每一轮迭代中,我们仅使用数据集中的随机40个元素来训练,以此减少模型的过拟合问题;

在上述配置下训练5000个epoch,打印loss的变化,并在图中画出模型所拟合的曲线和参考值。

import matplotlib.pyplot as plt
X = torch.linspace(-2, 2, 100).reshape(100, 1)
Y = torch.sin(np.pi*X)-torch.cos(np.pi*X)
model = torch.nn.Sequential(
    torch.nn.Linear(1, 8),
    torch.nn.Tanh(),
    torch.nn.Linear(8, 4),
    torch.nn.Tanh(),
    torch.nn.Linear(4, 1)
)
loss_fn = torch.nn.MSELoss()
optim = torch.optim.SGD(model.parameters(), lr=0.05, momentum=0.5)
for i in range(5000):
    idx = np.random.choice(100, 40)
    Y1 = model(X[idx])
    loss = loss_fn(Y1, Y[idx])
    optim.zero_grad()
    loss.backward()
    optim.step()
    if i % 500 == 0:
        print("iter:{} loss:{:.4f} w".format(
            i, loss))
plt.scatter(X, Y, c="red")
plt.plot(X, model(X).detach().numpy())
iter:0 loss:1.0948 w
iter:500 loss:0.1151 w
iter:1000 loss:0.0191 w
iter:1500 loss:0.0122 w
iter:2000 loss:0.0030 w
iter:2500 loss:0.0042 w
iter:3000 loss:0.0062 w
iter:3500 loss:0.0018 w
iter:4000 loss:0.0020 w
iter:4500 loss:0.0007 w

机器学习笔记4-0:PyTorch简明教程

Reference

  1. PyTorch学习笔记1:PyTorch基础知识 - 张浩驰的文章 - 知乎
  2. 李宏毅2020机器学习深度学习(完整版)国语-P16 Pytorch Tutorial