深入浅出Embedding:原理解析与应用实践
上QQ阅读APP看书,第一时间看更新

3.4 使用PyTorch实现数据迁移实例

本节通过一个使用PyTorch实现数据迁移的实例,帮助大家加深对相关知识的理解。

3.4.1 特征提取实例

在特征提取中,可以在预先训练好的网络结构后修改或添加一个简单的分类器,然后将源任务上预先训练好的网络作为另一个目标任务的特征提取器,只对最后增加的分类器参数进行重新学习,而预先训练好的网络参数不被修改或冻结。

在完成新任务的特征提取时使用的是源任务中学习到的参数,而不用重新学习所有参数。关于如何使用PyTorch实现冻结,将在本节后续介绍。

下面我们将用一个实例具体说明如何通过特征提取的方法进行图像分类。这里预训练模型采用retnet18网络,准确率提升到75%左右。以下是具体实现过程。

1. 导入模块

导入需要的模块。

import torch
from torch import nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from torchvision import models
from torchvision.datasets import ImageFolder
from datetime import datetime

2. 加载数据

对应数据已下载在本地,故设置download=False。为适配预训练模型,这里增加一些预处理功能,如数据标准化、对图片进行裁剪等。

trans_train = transforms.Compose(
    [transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                 std=[0.229, 0.224, 0.225])])

trans_valid = transforms.Compose(
    [transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                 std=[0.229, 0.224, 0.225])])
#如果是Linux环境,root的值改为root='./data',其他不变
trainset = torchvision.datasets.CIFAR10(root='.\data', train=True,
download=False, transform=trans_train)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,
shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='.\data', train=False,
download=False, transform=trans_valid)
testloader = torch.utils.data.DataLoader(testset, batch_size=64,
shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

3. 下载预训练模型

这里将自动下载预训练模型,该模型网络架构为resnet18,且已经在ImageNet大数据集上训练好了,该数据集有1000个类别。

# 使用预训练的模型
net = models.resnet18(pretrained=True)

4. 冻结模型参数

冻结这些参数,且在反向传播时,不会更新。

for param in net.parameters():
    param.requires_grad = False

5. 修改最后一层的输出类别数

原来输出为512×1000,现在我们把输出改为512×10,新数据集有10个类别。

# 将最后的全连接层改成十分类
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")
net.fc = nn.Linear(512, 10)

6. 查看冻结前后的参数情况

使用如下代码查看冻结前后的参数情况:

# 查看总参数及训练参数
total_params = sum(p.numel() for p in net.parameters())
print('原总参数个数:{}'.format(total_params))
total_trainable_params = sum(p.numel() for p in net.parameters() if p.requires_grad)
print('需训练参数个数:{}'.format(total_trainable_params))

运行结果:

原总参数个数:11181642
需训练参数个数:5130

由运行结果可知,如果不冻结,需要更新的参数会非常多,冻结后,只需要更新全连接层的相关参数即可。

7. 定义损失函数及优化器

定义损失函数及优化器的代码如下所示:

criterion = nn.CrossEntropyLoss()
#只需要优化最后一层参数
optimizer = torch.optim.SGD(net.fc.parameters(), lr=1e-3,
weight_decay=1e-3, momentum=0.9)

8. 训练及验证模型

训练及验证模型的代码如下所示:

rain(net, trainloader, testloader, 20, optimizer, criterion)

运行结果(后10个循环的结果):

Epoch 10. Train Loss: 1.115400, Train Acc: 0.610414, Valid Loss: 0.731936, Valid
    Acc: 0.748905, Time 00:03:22
Epoch 11. Train Loss: 1.109147, Train Acc: 0.613551, Valid Loss: 0.727403, Valid
    Acc: 0.750896, Time 00:03:22
Epoch 12. Train Loss: 1.111586, Train Acc: 0.609235, Valid Loss: 0.720950, Valid
    Acc: 0.753583, Time 00:03:21
Epoch 13. Train Loss: 1.109667, Train Acc: 0.611333, Valid Loss: 0.723195, Valid
    Acc: 0.751692, Time 00:03:22
Epoch 14. Train Loss: 1.106804, Train Acc: 0.614990, Valid Loss: 0.719385, Valid
    Acc: 0.749005, Time 00:03:21
Epoch 15. Train Loss: 1.101916, Train Acc: 0.614970, Valid Loss: 0.716220, Valid
    Acc: 0.754080, Time 00:03:22
Epoch 16. Train Loss: 1.098685, Train Acc: 0.614650, Valid Loss: 0.723971, Valid
    Acc: 0.749005, Time 00:03:20
Epoch 17. Train Loss: 1.103964, Train Acc: 0.615010, Valid Loss: 0.708623, Valid
    Acc: 0.758161, Time 00:03:21
Epoch 18. Train Loss: 1.107073, Train Acc: 0.609815, Valid Loss: 0.730036, Valid
    Acc: 0.746716, Time 00:03:20
Epoch 19. Train Loss: 1.102967, Train Acc: 0.616568, Valid Loss: 0.713578, Valid
    Acc: 0.752687, Time 00:03:22

从上述结果可以看出,验证准确率达到75%左右。虽然准确率有比较大的提升,但还不够理想,下面我们将采用微调+数据增强的方法继续提升准确率。

3.4.2 微调实例

微调允许修改预先训练好的网络参数来学习目标任务,所以,训练时间要比特征抽取方法长,但精度更高。微调的大致过程是在预先训练过的网络上添加新的随机初始化层,此外预先训练的网络参数也会被更新,但会使用较小的学习率以防止预先训练好的参数发生较大改变。

常用方法是固定底层的参数,调整一些顶层或具体层的参数。这样可以减少训练参数的数量,也可以避免过拟合现象的发生。尤其是在目标任务的数据量不够大的时候,该方法会很有效。实际上,微调要优于特征提取,因为它能够对迁移过来的预训练网络参数进行优化,使其更加适合新的任务。

1. 数据预处理

这里对训练数据添加了几种数据增强方法,如图片裁剪、旋转、颜色改变等方法。测试数据与特征提取的方法一样,这里不再赘述。

trans_train = transforms.Compose(
    [transforms.RandomResizedCrop(size=256, scale=(0.8, 1.0)),
     transforms.RandomRotation(degrees=15),
     transforms.ColorJitter(),
     transforms.RandomResizedCrop(224),
     transforms.RandomHorizontalFlip(),
     transforms.ToTensor(),
     transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])])

2. 加载预训练模型

加载预训练模型,代码如下:

# 使用预训练的模型
net = models.resnet18(pretrained=True)
print(net)

这里显示模型参数的最后一部分:

(1): BasicBlock(
    (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1),
        bias=False)
    (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_
        stats=True)
    (relu): ReLU(inplace)
    (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1),
        bias=False)
    (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_
        stats=True)
    )
    )
    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
    (fc): Linear(in_features=512, out_features=1000, bias=True)

3. 修改分类器

修改最后全连接层,把类别数由原来的1000改为10。

# 将最后的全连接层改成十分类
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
net.fc = nn.Linear(512, 10)
#net = torch.nn.DataParallel(net)
net.to(device)

4. 选择损失函数及优化器

使用微调训练模型时,一般选择一个稍大一点的学习率,如果选择的学习率太小,效果要差一些。这里把学习率设为le-3。

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=1e-3, weight_decay=1e-3,momentum=0.9)

5. 训练及验证模型

训练及验证模型,代码如下:

train(net, trainloader, testloader, 20, optimizer, criterion)

运行结果(部分结果):

Epoch 10. Train Loss: 0.443117, Train Acc: 0.845249, Valid Loss: 0.177874, Valid
    Acc: 0.938495, Time 00:09:15
Epoch 11. Train Loss: 0.431862, Train Acc: 0.850324, Valid Loss: 0.160684, Valid
    Acc: 0.946158, Time 00:09:13
Epoch 12. Train Loss: 0.421316, Train Acc: 0.852841, Valid Loss: 0.158540, Valid
    Acc: 0.946756, Time 00:09:13
Epoch 13. Train Loss: 0.410301, Train Acc: 0.857757, Valid Loss: 0.157539, Valid
    Acc: 0.947950, Time 00:09:12
Epoch 15. Train Loss: 0.407030, Train Acc: 0.858975, Valid Loss: 0.153207, Valid
    Acc: 0.949343, Time 00:09:20
Epoch 16. Train Loss: 0.400168, Train Acc: 0.860234, Valid Loss: 0.147240, Valid
    Acc: 0.949542, Time 00:09:17
Epoch 17. Train Loss: 0.382259, Train Acc: 0.867168, Valid Loss: 0.150277, Valid
    Acc: 0.947552, Time 00:09:15
Epoch 18. Train Loss: 0.378578, Train Acc: 0.869046, Valid Loss: 0.144924, Valid
    Acc: 0.951334, Time 00:09:16

使用微调训练方式的时间明显大于使用特征提取方式的时间,实例中一个循环需要9分钟左右,但验证准确率高达95%,因时间关系这里只循环20次,如果增加循环次数,应该还可以把验证准确率再提升几个百分点。