PyTorch深度学习实战(2)——PyTorch快速入门

PyTorch的简洁设计使得它易于入门,在深入介绍PyTorch之前,本文先介绍一些PyTorch的基础知识,以便读者能够对PyTorch有一个大致的了解,并能够用PyTorch搭建一个简单的神经网络。

1 Tensor

Tensor是PyTorch中最重要的数据结构,它可以是一个数(标量)、一维数组(向量)、二维数组(如矩阵、黑白图片等)或者更高维的数组(如彩色图片、视频等)。Tensor与NumPy的ndarrays类似,但Tensor可以使用GPU加速。下面通过几个示例了解Tensor的基本使用方法:

复制代码
In:` `import torch as t`
`    t.__version__ # 查看pytorch的版本信息Out:'1.8.0'In: # 构建一个2×3的矩阵,只分配了空间未初始化,其数值取决于内存空间的状态`
`    x = t.Tensor(2,` `3)` `# 维度:2×3`
`    xOut:tensor([[7.9668e-37,` `4.5904e-41,` `7.9668e-37],`
        	`[4.5904e-41,` `0.0000e+00,` `0.0000e+00]])`
`

注意: **torch.Tensor()**可以使用int类型的整数初始化矩阵的行、列数, **torch.tensor()**需要确切的数据值进行初始化。

复制代码
In: y = t.Tensor(5)`
    `print(y.size())`
`    z = t.tensor([5])` `# torch.tensor需要确切数值进行初始化`
    `print(z.size())`
`Out:torch.Size([5])`
`	torch.Size([1])`
`In:` `# 使用正态分布初始化二维数组`
`    x = t.rand(2,` `3)`  
`    x`
`Out:tensor([[0.1533,` `0.9600,` `0.5278],`
        	`[0.5453,` `0.3827,` `0.3212]])`
        
`In:` `print(x.shape)` `# 查看x的形状`
`    x.size()[1], x.size(1)` `# 查看列的个数, 这两种写法等价Out:torch.Size([2, 3])`
	`(3,` `3)`
`In: y = t.rand(2,` `3)`
    `# 加法的第一种写法`
`    x + y`
`Out:tensor([[1.1202,` `1.6476,` `1.1220],`
            `[1.0161,` `1.1325,` `0.3405]])`
`In:` `# 加法的第二种写法`
`    t.add(x, y)`
`Out:tensor([[1.1202,` `1.6476,` `1.1220],`
            `[1.0161,` `1.1325,` `0.3405]])`
`In:` `# 加法的第三种写法:指定加法结果的输出目标为result`
`    result = t.Tensor(2,` `3)` `# 预先分配空间`
`    t.add(x, y, out=result)` `# 输入到result`
`    result`
`Out:tensor([[1.1202,` `1.6476,` `1.1220],`
            `[1.0161,` `1.1325,` `0.3405]])`
`In:` `print('初始的y值')`
    `print(y)`
    
    `print('第一种加法,y的结果')`
`    y.add(x)` `# 普通加法,不改变y的值`
    `print(y)`
    
    `print('第二种加法,y的结果')`
`    y.add_(x)` `# inplace加法,y改变了`
    `print(y)`
`Out:初始的y值`
`    tensor([[0.9669,` `0.6877,` `0.5942],`
            `[0.4708,` `0.7498,` `0.0193]])`
`第一种加法,y的结果`
`    tensor([[0.9669,` `0.6877,` `0.5942],`
            `[0.4708,` `0.7498,` `0.0193]])`
`第二种加法,y的结果`
`    tensor([[1.1202,` `1.6476,` `1.1220],`
            `[1.0161,` `1.1325,` `0.3405]])`
`

注意:函数名后面带下划线 ****的函数称为inplace操作,会修改Tensor本身。例如, **x.add(y)** x.t_()会改变 x x.add(y) **x.t()返回一个新的Tensor, **x不变。

复制代码
In:` `# Tensor的索引操作与NumPy类似`
`    x[:,` `1]`
`Out:tensor([0.8969,` `0.7502,` `0.7583,` `0.3251,` `0.2864])Tensor和NumPy数组之间的相互操作非常容易且快速。对于Tensor不支持的操作,可以先转为NumPy数组进行处理,之后再转回Tensor。`
`In: a = t.ones(5)` `# 新建一个全1的Tensor`
`    a`
`Out:tensor([1.,` `1.,` `1.,` `1.,` `1.])`
`In: b = a.numpy()` `# Tensor → NumPy`
`    b`
`Out:array([1.,` `1.,` `1.,` `1.,` `1.], dtype=float32)`
`In:` `import numpy as np`
`    a = np.ones(5)`
`    b = t.from_numpy(a)` `# NumPy → Tensor`
    `print(a)`
    `print(b)` 
`Out:[1.` `1.` `1.` `1.` `1.]`
`    tensor([1.,` `1.,` `1.,` `1.,` `1.], dtype=torch.float64)`
`

因为Tensor和NumPy对象大多数情况下共享内存,所以它们之间的转换很快,几乎不会消耗资源。这也意味着,其中一个发生了变化,另外一个会随之改变。

复制代码
In: b.add_(1)` `# 以下划线结尾的函数会修改自身`
    `print(b)`
    `print(a)`  `# Tensor和NumPy共享内存Out:tensor([2., 2., 2., 2., 2.], dtype=torch.float64)`
    `[2.` `2.` `2.` `2.` `2.]`
`

如果想获取Tensor中某一个元素的值,那么可以使用索引操作得到一个零维度的Tensor(一般称为scalar),再通过scalar.item()获取具体数值。

复制代码
In: scalar = b[0]`
`    scalar`
`Out:tensor(2., dtype=torch.float64)`
`In: scalar.shape # 0-dim `
`Out:torch.Size([])`
`In: scalar.item()` `# 使用scalar.item()可以从中取出Python对象的数值Out:2.0`
`In: tensor = t.tensor([2]) # 注意和scalar的区别`
`    tensor, scalar`
`Out:(tensor([2]), tensor(2., dtype=torch.float64))`
`In: tensor.size(), scalar.size()`
`Out:(torch.Size([1]), torch.Size([]))`
`In:` `# 只有一个元素的tensor也可以调用tensor.item()`
`    tensor.item(), scalar.item()`
`Out:(2,` `2.0)`
`In: tensor = t.tensor([3,4])` `# 新建一个包含3,4两个元素的Tensor`
`    old_tensor = tensor`
`    new_tensor = old_tensor.clone()`
`    new_tensor[0]` `=` `1111`
`    old_tensor, new_tensor`
`Out:(tensor([3,` `4]), tensor([1111,`    `4]))`
`

注意: t.tensor()与tensor.clone()总是会进行数据拷贝,新的Tensor和原来的数据不再共享内存。如果需要共享内存,那么可以使用torch.from_numpy()或者tensor.detach()新建一个Tensor。

复制代码
In: new_tensor = old_tensor.detach()`
`    new_tensor[0]` `=` `1111`
`    old_tensor, new_tensorOut:(tensor([1111,`    `4]), tensor([1111,`    `4]))`
`

在深度学习中,Tensor的维度特征十分重要。有时需要对Tensor的维度进行变换,针对该问题,PyTorch提供了许多快捷的变换方式,例如维度变换view、reshape,维度交换permute、transpose等。

在维度变换中,可以使用view操作与reshape操作来改变Tensor的维度,二者之间有以下区别。

  • view只能用于内存中连续存储的Tensor。如果Tensor使用了transpose、permute等维度交换操作,那么Tensor在内存中会变得不连续。此时不能直接使用view操作,应该先将其连续化,即tensor.contiguous.view()。
  • reshape操作不要求Tensor在内存中是连续的,直接使用即可。

下面举例说明几种维度变换操作:

复制代码
In: x = t.randn(4,` `4)`
`    y = x.view(16)`
`    z = x.view(-1,` `8)` `# -1表示由其他维度计算决定`
    `print(x.size(), y.size(), z.size())`
`Out:torch.Size([4,` `4]) torch.Size([16]) torch.Size([2,` `8])`

`In: p = x.reshape(-1,` `8)`
    `print(p.shape)`
`Out:torch.Size([2,` `8])`

`In: x1 = t.randn(2,` `4,` `6)`
`    o1 = x1.permute((2,` `1,` `0))`
`    o2 = x1.transpose(0,` `2)`
    `print(f'o1 size {o1.size()}')`
    `print(f'o2 size {o2.size()}')`
`Out:o1 size torch.Size([6,` `4,` `2])`
`	o2 size torch.Size([6,` `4,` `2])`
`

除了对Tensor进行维度变换,还可以针对Tensor的某些维度进行其他的操作。例如,tensor.squeeze()

可以进行Tensor的维度压缩、tensor.unsqueeze()可以扩展Tensor的维度、torch.cat()可以在Tensor指定维度上进行拼接等。

复制代码
In: x = t.randn(3,` `2,` `1,` `1)`
`    y = x.squeeze(-1)`    `# 将最后一维进行维度压缩`
`    z = x.unsqueeze(0)`   `# 在最前面增加一个维度`
`    w = t.cat((x, x),` `0)` `# 在第一维度连接两个x`
    `print(f'y size {y.shape}')`
    `print(f'z size {z.shape}')`
    `print(f'w size {w.shape}')Out:y size torch.Size([3,` `2,` `1])`
`	z size torch.Size([1,` `3,` `2,` `1,` `1])`
`	w size torch.Size([6,` `2,` `1,` `1])`
`

Tensor可以通过.cuda()方法或者.to(device)方法转为GPU的Tensor,从而享受GPU带来的加速运算。

复制代码
In:` `# 在不支持CUDA的机器下,下一步还是在CPU上运行`
`    device = t.device("cuda:0"` `if t.cuda.is_available()` `else` `"cpu")`
`    x = x.to(device)`
`    y = y.to(x.device)`
`    z = x + y`
`

此时,读者可能会发现GPU运算的速度并未提升太多,这是因为x和y的规模太小、运算简单,而且将数据从内存转移到显存需要额外的开销。GPU的优势需要在大规模数据和复杂运算下才能体现出来。

2 autograd:自动微分

在深度学习中,反向传播算法被用来计算梯度,其主要流程为通过梯度下降法来最小化损失函数,以此更新网络参数。PyTorch中的autograd模块实现了自动反向传播的功能,optim模块实现了常见的梯度下降优化方法。几乎所有的Tensor操作,autograd都能为它们提供自动微分,避免手动计算导数的复杂过程。

如果想要使用autograd功能,那么需要对求导的Tensor设置tensor.requries_grad=True,下面举例说明autograd模块的用法:

复制代码
In:` `# 为Tensor设置requires_grad标识,代表着需要求导数`
    `# PyTorch会自动调用autograd对Tensor求导`
`    x = t.ones(2,` `2, requires_grad=True)`
    
    `# 上一步等价于`
    `# x = t.ones(2,2)`
    `# x.requires_grad = True`
`    xOut:tensor([[1.,` `1.],`
        	`[1.,` `1.]], requires_grad=True)In: y = x.sum()`
`    yOut:tensor(4., grad_fn=<SumBackward0>)In: y.grad_fn`
`Out:<SumBackward0 at 0x7fca878c8748>`
`In: y.backward()` `# 反向传播,计算梯度In: # y = x.sum() = (x[0][0] + x[0][1] + x[1][0] + x[1][1])`
    `# 每个值的梯度都为1`
`    x.grad `
`Out:tensor([[1.,` `1.],`
        	`[1.,` `1.]])`
`

注意: grad****在反向传播过程中是累加的(accumulated)。也就是说,反向传播得到的梯度会累加之前的梯度。因此,每次在进行反向传播之前需要把梯度清零。

复制代码
In: y.backward()`
`    x.gradOut:tensor([[2.,` `2.],`
        	`[2.,` `2.]])In: y.backward()`
`    x.gradOut:tensor([[3.,` `3.],`
        	`[3.,` `3.]])In:` `# 以下划线结束的函数是inplace操作,会修改自身的值,如add_`
`    x.grad.data.zero_()Out:tensor([[0.,` `0.],`
        	`[0.,` `0.]])In: y.backward()`
`    x.grad # 清零后计算得到正确的梯度值Out:tensor([[1., 1.],`
        	`[1.,` `1.]])In: a = t.randn(2,` `2)`
`    a =` `((a *` `3)` `/` `(a -` `1))`
    `print(a.requires_grad)`
`    a.requires_grad_(True)`
    `print(a.requires_grad)`
`    b =` `(a * a).sum()`
    `print(b.grad_fn)`
`Out:False`
	`True`
	`<SumBackward0 object at 0x7fca87873128>`

`

3 神经网络

虽然autograd实现了反向传播功能,但是直接用它来写深度学习的代码还是稍显复杂。torch.nn是专门为神经网络设计的模块化接口,它构建于autograd之上,可以用来定义和运行神经网络。nn.Module是nn中最重要的类,它可以看作是一个神经网络的封装,包含神经网络各层的定义以及前向传播(forward)方法,通过forward(input)可以返回前向传播的结果。下面以最早的卷积神经网络LeNet1为例,来看看如何用nn.Module实现该网络结构,LeNet的网络结构如图2-10所示。

复制代码
[^1]:` 
`@article{lecun1998gradient,`
`  title={Gradient-based learning applied to document recognition},`
`  author={LECUN Y, BOTTOU L, BENGIO Y, et al},`
`  journal={Proceedings of the IEEE},`
`  volume={86},`
`  number={11},`
`  pages={2278--2324},`
`  year={1998},`
`  publisher={Ieee}`
`}`
`

LeNet共有7层,它的输入图像的大小为32 \\times 32,共经过2个卷积层、2次下采样操作以及3个全连接层得到最终的10维输出。在实现该网络之前,这里先对神经网络的通用训练步骤进行说明。

(1)定义一个包含可学习参数的神经网络。

(2)加载用于训练该网络的数据集。

(3)进行前向传播得到网络的输出结果,计算损失(网络输出结果与正确结果的差距)。

(4)进行反向传播,更新网络参数。

(5)保存网络模型。

3.1 定义网络

在定义网络时,模型需要继承nn.Module,并实现它的forward方法。其中,网络里含有可学习参数的层应该放在构造函数__init__()中,如果某一层(如ReLU)不含有可学习参数,那么它既可以放在构造函数中,又可以放在forward方法中。这里将这些不含有可学习参数的层放在forward方法中,并使用nn.functional实现:

复制代码
In:` `import torch.nn as nn`
    `import torch.nn.functional as F`
    
    `class` `Net(nn.Module):`
        `def` `__init__(self):`
            `# nn.Module子类的函数必须在构造函数中执行父类的构造函数`
            `# 下式等价于nn.Module.__init__(self)`
            `super().__init__()`
            
            `# 卷积层,'1'表示输入图片为单通道, '6'表示输出通道数,'5'表示卷积核为5×5`
`            self.conv1 = nn.Conv2d(1,` `6,` `5)` 
            `# 卷积层,'6'表示输入图片为单通道, '16'表示输出通道数,'5'表示卷积核为5×5`
`            self.conv2 = nn.Conv2d(6,` `16,` `5)` 
            `# 仿射层/全连接层,y = Wx + b`
`            self.fc1 = nn.Linear(16` `*` `5` `*` `5,` `120)` 
`            self.fc2 = nn.Linear(120,` `84)`
`            self.fc3 = nn.Linear(84,` `10)`
    
        `def` `forward(self, x):` 
            `# 卷积 -> 激活 -> 池化 `
`            x = F.max_pool2d(F.relu(self.conv1(x)),` `(2,` `2))`
`            x = F.max_pool2d(F.relu(self.conv2(x)),` `2)` 
            `# 改变Tensor的形状,-1表示自适应`
`            x = x.view(x.size()[0],` `-1)` 
`            x = F.relu(self.fc1(x))`
`            x = F.relu(self.fc2(x))`
`            x = self.fc3(x)`        
            `return x`
    
`    net = Net()`
    `print(net)`
 
 `Out:Net(`
        `(conv1): Conv2d(1,` `6, kernel_size=(5,` `5), stride=(1,` `1))`
        `(conv2): Conv2d(6,` `16, kernel_size=(5,` `5), stride=(1,` `1))`
        `(fc1): Linear(in_features=400, out_features=120, bias=True)`
        `(fc2): Linear(in_features=120, out_features=84, bias=True)`
        `(fc3): Linear(in_features=84, out_features=10, bias=True)`
    `)`
`

用户只需要在nn.Module的子类中定义了forward函数,backward函数就会自动实现(利用autograd)。在forward函数中不仅可以使用Tensor支持的任何函数,还可以使用if、for、print、log等Python语法,写法和标准的Python写法一致。

使用net.parameters()可以得到网络的可学习参数,使用net.named_parameters()可以同时得到网络的可学习参数及其名称,下面举例说明:

复制代码
In: params =` `list(net.parameters())`
    `print(len(params))`
`Out:10`

`In:` `for name, parameters in net.named_parameters():`
        `print(name,` `':', parameters.size())`
`Out:conv1.weight : torch.Size([6,` `1,` `5,` `5])`
`    conv1.bias : torch.Size([6])`
`    conv2.weight : torch.Size([16,` `6,` `5,` `5])`
`    conv2.bias : torch.Size([16])`
`    fc1.weight : torch.Size([120,` `400])`
`    fc1.bias : torch.Size([120])`
`    fc2.weight : torch.Size([84,` `120])`
`    fc2.bias : torch.Size([84])`
`    fc3.weight : torch.Size([10,` `84])`
`    fc3.bias : torch.Size([10])`
    
`In:` `input` `= t.randn(1,` `1,` `32,` `32)`
`    out = net(input)`
`    out.size()`
`Out:torch.Size([1,` `10])`

`In: net.zero_grad()` `# 所有参数的梯度清零`
`    out.backward(t.ones(1,` `10))` `# 反向传播`
`

注意: torch.nn****只支持输入mini-batch,不支持一次只输入一个样本。如果只输入一个样本,那么需要使用 **input.unsqueeze(0)**将batch_size设为1。 例如, nn.Conv2d的输入必须是4维,形如\\text{nSamples} \\times \\text{nChannels} \\times \\text{Height} \\times \\text{Width} 。如果一次输入只有一个样本,那么可以将\\text{nSample} 设置为1,即1 \\times \\text{nChannels} \\times \\text{Height} \\times \\text{Width}

3.2 损失函数

torch.nn实现了神经网络中大多数的损失函数,例如nn.MSELoss用来计算均方误差,nn.CrossEntropyLoss用来计算交叉熵损失等,下面举例说明:

复制代码
In: output = net(input)`
`    target = t.arange(0,` `10).view(1,` `10).float()` 
`    criterion = nn.MSELoss()`
`    loss = criterion(output, target)`
`    loss `
`Out:tensor(28.1249, grad_fn=<MseLossBackward>)`
`

对loss进行反向传播溯源(使用gradfn属性),可以看到上文实现的LeNet的计算图如下:

复制代码
input` `-> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d  `
      `-> view -> linear -> relu -> linear -> relu -> linear `
      `-> MSELoss`
      `-> loss`
`

当调用loss.backward()时,计算图会动态生成并自动微分,自动计算图中参数(parameters)的导数,示例如下:

复制代码
In:` `# 运行.backward,观察调用之前和调用之后的grad`
`    net.zero_grad()` `# 把net中所有可学习参数的梯度清零`
    `print('反向传播之前 conv1.bias的梯度')`
    `print(net.conv1.bias.grad)`
`    loss.backward()`
    `print('反向传播之后 conv1.bias的梯度')`
    `print(net.conv1.bias.grad)`
`Out:反向传播之前 conv1.bias的梯度`
`    tensor([0.,` `0.,` `0.,` `0.,` `0.,` `0.])`
`    反向传播之后 conv1.bias的梯度`
`    tensor([` `0.0020,` `-0.0619,`  `0.1077,`  `0.0197,`  `0.1027,` `-0.0060])`
`

3.3 优化器

在完成反向传播中所有参数的梯度计算后,需要使用优化方法来更新网络的权重和参数。常用的随机梯度下降法(SGD)的更新策略如下:

weight = weight - learning_rate * gradient

用户可以手动实现这一更新策略:

复制代码
learning_rate =` `0.01`
`for f in net.parameters():`
`    f.data.sub_(f.grad.data * learning_rate)` `# inplace减法`
`

torch.optim中实现了深度学习中大多数优化方法,例如RMSProp、Adam、SGD等,因此,通常情况下用户不需要手动实现上述代码。下面举例说明如何使用torch.optim进行网络的参数更新:

复制代码
In:` `import torch.optim as optim`
    `#新建一个优化器,指定要调整的参数和学习率`
`    optimizer = optim.SGD(net.parameters(), lr =` `0.01)`
    
    `# 在训练过程中`
    `# 先梯度清零(与net.zero_grad()效果一样)`
`    optimizer.zero_grad()` 
    
    `# 计算损失`
`    output = net(input)`
`    loss = criterion(output, target)`
    
    `#反向传播`
`    loss.backward()`
    
    `#更新参数`
`    optimizer.step()`
`

3.4 数据加载与预处理

在深度学习中,数据加载及预处理是非常繁琐的过程。幸运的是,PyTorch提供了一些可以极大简化和加快数据处理流程的工具:Dataset与DataLoader。同时,对于常用的数据集,PyTorch提供了封装好的接口供用户快速调用,这些数据集主要保存在torchvision中。torchvision是一个视觉工具包,它提供了许多视觉图像处理的工具,主要包含以下三部分。

  • datasets:提供了常用的数据集,如MNIST、CIFAR-10、ImageNet等。
  • models:提供了深度学习中经典的网络结构与预训练模型,如ResNet、MobileNet等。
  • transforms:提供了常用的数据预处理操作,主要包括对Tensor、PIL Image等的操作。

读者可以使用torchvision方便地加载数据,然后进行数据预处理,这部分内容会在本书第5章进行详细介绍。

相关推荐
迅易科技1 小时前
借助腾讯云质检平台的新范式,做工业制造企业质检的“AI慧眼”
人工智能·视觉检测·制造
古希腊掌管学习的神2 小时前
[机器学习]XGBoost(3)——确定树的结构
人工智能·机器学习
ZHOU_WUYI3 小时前
4.metagpt中的软件公司智能体 (ProjectManager 角色)
人工智能·metagpt
靴子学长3 小时前
基于字节大模型的论文翻译(含免费源码)
人工智能·深度学习·nlp
AI_NEW_COME4 小时前
知识库管理系统可扩展性深度测评
人工智能
海棠AI实验室5 小时前
AI的进阶之路:从机器学习到深度学习的演变(一)
人工智能·深度学习·机器学习
hunteritself5 小时前
AI Weekly『12月16-22日』:OpenAI公布o3,谷歌发布首个推理模型,GitHub Copilot免费版上线!
人工智能·gpt·chatgpt·github·openai·copilot
IT古董6 小时前
【机器学习】机器学习的基本分类-强化学习-策略梯度(Policy Gradient,PG)
人工智能·机器学习·分类
centurysee6 小时前
【最佳实践】Anthropic:Agentic系统实践案例
人工智能
mahuifa6 小时前
混合开发环境---使用编程AI辅助开发Qt
人工智能·vscode·qt·qtcreator·编程ai