59PyTorch 入门与实战--迁移学习

从图片文件中加载训练数据

引入相关包

首先导入所有需要的软件包:
本次实验中会用到的 torchvision 包,相信大家通过上节实验中的“数据加载三件套”已经有些熟悉了。
不过这个包中不仅包括数据加载工具,还包括一些成熟的视觉模型,在本次实验中会用到 torchvision 中包含的 ResNet 模型。另外,torchvision 中的 transforms 包提供了很多数据转换的功能,在本次实验中会使用它来对图片数据进行数据增强。

# 下载实验所需数据并解压
!wget http://labfile.oss.aliyuncs.com/courses/1073/transfer-data.zip
!unzip transfer-data.zip
import torch
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
import torch.nn.functional as F
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import copy
import os

从硬盘文件夹中加载图像数据集

在上节实验中演示了使用 PyTorch 的 datasets 加载自带的 MNIST 数据集的方法。
在本次实验中不使用 datasets 自带的数据集,而是用 datasets 加载本地硬盘上存在的图片数据。在本环境中,图片数据都已经为大家准备好,训练数据集在 ./data/train 中,校验数据集在 ./data/val 中。数据集中的图片都是分开存放的,比如所有蜜蜂图片保存在 bees 文件夹中,所有蚂蚁图片保存在 ants 文件夹中。
使用 datasets 的 ImageFolder 方法就可以实现自动加载以上数据,方法如下。

# 数据存储总路径
data_dir = 'transfer-data'
# 图像的大小为224*224
image_size = 224
# 从data_dir/train加载文件
# 加载的过程将会对图像自动作如下的图像增强操作:
# 1. 随机从原始图像中切下来一块224*224大小的区域
# 2. 随机水平翻转图像
# 3. 将图像的色彩数值标准化
train_dataset = datasets.ImageFolder(os.path.join(data_dir, 'train'),
                                    transforms.Compose([
                                        transforms.RandomResizedCrop(image_size),
                                        transforms.RandomHorizontalFlip(),
                                        transforms.ToTensor(),
                                        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                    ])
                                    )

# 加载校验数据集,对每个加载的数据进行如下处理:
# 1. 放大到256*256像素
# 2. 从中心区域切割下224*224大小的图像区域
# 3. 将图像的色彩数值标准化
val_dataset = datasets.ImageFolder(os.path.join(data_dir, 'val'),
                                    transforms.Compose([
                                        transforms.Resize(256),
                                        transforms.CenterCrop(image_size),
                                        transforms.ToTensor(),
                                        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])

数据集的作用并不是进行数据的读取和迭代,所以下面要为每个数据集创建数据加载器。

# 创建相应的数据加载器
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size = 4, shuffle = True, num_workers=4)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size = 4, shuffle = True, num_workers=4)

# 读取得出数据中的分类类别数
# 如果只有蜜蜂和蚂蚁,那么是2
num_classes = len(train_dataset.classes)
num_classes

关于 GPU 运算

在这有个知识点要讲一下,那就是关于使用 GPU 加速运算的问题。
大家应该都听说过深度学习可以通过 GPU 并行运算加速模型的训练。PyTorch 是支持使用 GPU 并行运算的。但是能不能使用 GPU 加速运算还取决于你的硬件。支持 GPU 的硬件(显卡)一般是比较昂贵的,目前本实验环境还不支持使用 GPU 运算,但是未来会根据大家的需求,看是否要加入 GPU 运算功能。
总之,如果你想让自己的程序能够自动识别 GPU 计算环境,并且在 GPU 不具备的情况下也能自动使用 CPU 正常运行,那么你应该:

# 检测本机器是否安装GPU,将检测结果记录在布尔变量use_cuda中
use_cuda = torch.cuda.is_available()

# 当可用GPU的时候,将新建立的张量自动加载到GPU中
dtype = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
itype = torch.cuda.LongTensor if use_cuda else torch.LongTensor

在后面的程序中都会用到这三个变量,来灵活判断是否需要采用 GPU 运算。

查看并绘制数据集中的图片

下面将定义一个函数,使用它可以将数据集中的某张图片打印出来。

def imshow(inp, title=None):
    # 将一张图打印显示出来,inp为一个张量,title为显示在图像上的文字

    # 一般的张量格式为:channels * image_width * image_height
    # 而一般的图像为 image_width * image_height * channels 
    # 所以,需要将张量中的 channels 转换到最后一个维度
    inp = inp.numpy().transpose((1, 2, 0)) 

    #由于在读入图像的时候所有图像的色彩都标准化了,因此我们需要先调回去
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1) 

    #将图像绘制出来
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)  # 暂停一会是为了能够将图像显示出来。

下面试一下刚刚编写的函数,将训练数据集的第一个 batch 绘制出来:

#获取第一个图像batch和标签
images, labels = next(iter(train_loader))

# 将这个batch中的图像制成表格绘制出来
out = torchvision.utils.make_grid(images)

imshow(out, title=[train_dataset.classes[x] for x in labels])

实验参照:训练一个普通的卷积神经网络

判断蚂蚁还是蜜蜂,这是个简单任务吗?

判断一张图片是蚂蚁还是蜜蜂,这是一个标准的图像分类任务,或许只需要像上节实验一样,训练一个卷积神经网络就可以解决问题了。那么下面开始搭建一个简单的卷积神经网络模型,它的结构与上节实验中的模型是一样的。

# 用于手写数字识别的卷积神经网络
depth = [4, 8]

class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 4, 5, padding = 2) #输入通道为1,输出通道为4,窗口大小为5,padding为2
        self.pool = nn.MaxPool2d(2, 2) #一个窗口为2*2的pooling运算
        self.conv2 = nn.Conv2d(depth[0], depth[1], 5, padding = 2) #第二层卷积,输入通道为depth[0], 输出通道为depth[1],窗口wei15,padding为2
        self.fc1 = nn.Linear(image_size // 4 * image_size // 4 * depth[1] , 512) #一个线性连接层,输入尺寸为最后一层立方体的平铺,输出层512个节点
        self.fc2 = nn.Linear(512, num_classes) #最后一层线性分类单元,输入为

    def forward(self, x):
        #神经网络完成一步前馈运算的过程,从输入到输出
        x = F.relu(self.conv1(x))
        x = self.pool(x)
        x = F.relu(self.conv2(x))
        x = self.pool(x)
        # 将立体的Tensor全部转换成一维的Tensor。两次pooling操作,所以图像维度减少了1/4
        x = x.view(-1, image_size // 4 * image_size // 4 * depth[1])
        x = F.relu(self.fc1(x)) #全链接,激活函数
        x = F.dropout(x, training=self.training) #以默认为0.5的概率对这一层进行dropout操作
        x = self.fc2(x) #全链接,激活函数
        x = F.log_softmax(x, dim=1) #log_softmax可以理解为概率对数值
        return x

准备训练

与上一节实验一样,编写一个计算预测错误率的函数,其中 predictions 是模型给出的一组预测结果。
batch_size 行 num_classes 列的矩阵,labels 是数据中的正确答案。

def rightness(predictions, labels):
    # 对于任意一行(一个样本)的输出值的第1个维度,求最大,得到每一行的最大元素的下标
    pred = torch.max(predictions.data, 1)[1] 
    # 将下标与labels中包含的类别进行比较,并累计得到比较正确的数量
    rights = pred.eq(labels.data.view_as(pred)).sum() 
    # 返回正确的数量和这一次一共比较了多少元素
    return rights, len(labels) 

然后实例化模型,定义损失函数,优化器。

# 加载网络
net = ConvNet()
# 如果有GPU就把网络加载到GPU中
net = net.cuda() if use_cuda else net
criterion = nn.CrossEntropyLoss() #Loss函数的定义
optimizer = optim.SGD(net.parameters(), lr = 0.0001, momentum=0.9)

把训练模型和验证模型的语句封装成函数。

# 参数:
# data : Variable
# target: Variable
def train_model(data, target):

    # 给网络模型做标记,标志说模型正在训练集上训练
    # 这种区分主要是为了打开 net 的 training 标志
    # 从而决定是否运行 dropout 与 batchNorm
    net.train() 

    output = net(data) # 神经网络完成一次前馈的计算过程,得到预测输出output
    loss = criterion(output, target) # 将output与标签target比较,计算误差
    optimizer.zero_grad() # 清空梯度
    loss.backward() # 反向传播
    optimizer.step() # 一步随机梯度下降算法

    # 计算准确率所需数值,返回数值为(正确样例数,总样本数)
    right = rightness(output, target) 

    # 如果计算在 GPU 中,打印的数据再加载到CPU中
    loss = loss.cpu() if use_cuda else loss
    return right, loss

下面是验证模型的方法。

# Evaluation Mode
def evaluation_model():
    # net.eval() 给网络模型做标记,标志说模型现在是验证模式
    # 此方法将模型 net 的 training 标志设置为 False
    # 模型中将不会运行 dropout 与 batchNorm
    net.eval() 

    vals = []
    #对测试数据集进行循环
    for data, target in val_loader:
        data, target = Variable(data, requires_grad=True), Variable(target)
        # 如果GPU可用,就把数据加载到GPU中
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        output = net(data) #将特征数据喂入网络,得到分类的输出
        val = rightness(output, target) #获得正确样本数以及总样本数
        vals.append(val) #记录结果

    return vals  

开始训练模型

下面开始使用蚂蚁和蜜蜂的数据训练这个简单的卷积神经网络模型。训练的方法与之前的实验相同,相信大家都已经轻车熟路了。

record = [] #记录准确率等数值的容器

#开始训练循环
num_epochs = 20
best_model = net
best_r = 0.0

for epoch in range(num_epochs):
    #optimizer = exp_lr_scheduler(optimizer, epoch)
    train_rights = [] #记录训练数据集准确率的容器
    train_losses = []
    for batch_idx, (data, target) in enumerate(train_loader):  #针对容器中的每一个批进行循环
        data, target = Variable(data), Variable(target) #将Tensor转化为Variable,data为图像,target为标签
        # 如果有GPU就把数据加载到GPU上
        if use_cuda:
            data, target = data.cuda(), target.cuda()

        # 调用训练函数
        right, loss = train_model(data, target)

        train_rights.append(right) #将计算结果装到列表容器中

        train_losses.append(loss.data.numpy())


    # train_r 为一个二元组,分别记录训练集中分类正确的数量和该集合中总的样本数
    train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))

    #在测试集上分批运行,并计算总的正确率
    vals = evaluation_model()

    #计算准确率
    val_r = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals]))
    val_ratio = 1.0*val_r[0].numpy()/val_r[1]

    if val_ratio > best_r:
        best_r = val_ratio
        best_model = copy.deepcopy(net)
    #打印准确率等数值,其中正确率为本训练周期Epoch开始后到目前撮的正确率的平均值
    print('训练周期: {} \tLoss: {:.6f}\t训练正确率: {:.2f}%, 校验正确率: {:.2f}%'.format(
        epoch, np.mean(train_losses), 100. * train_r[0].numpy() / train_r[1], 100. * val_r[0].numpy()/val_r[1]))       
    record.append([np.mean(train_losses), 1. * train_r[0].data.numpy() / train_r[1], 1. * val_r[0].data.numpy() / val_r[1]])

由于在线环境资源有限,此步骤的代码执行需要较长的时间,需要大家耐心等待。

训练效果展示

最后来看看训练后的网络是否像识别 MNIST 一样,能够达到接近 99% 的正确率哪?

#在测试集上分批运行,并计算总的正确率
net.eval() #标志模型当前为运行阶段
test_loss = 0
correct = 0
vals = []

#对测试数据集进行循环
for data, target in val_loader:
    data, target = Variable(data, requires_grad=True), Variable(target)
    if use_cuda:
        data, target = data.cuda(), target.cuda()
    output = net(data) #将特征数据喂入网络,得到分类的输出
    val = rightness(output, target) #获得正确样本数以及总样本数
    vals.append(val) #记录结果

#计算准确率
rights = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals]))
right_rate = 1.0 * rights[0].data.numpy() / rights[1]
right_rate

事实表明,这种简单结构的卷积神经网络并不能将蚂蚁和蜜蜂这种复杂的图片分类正确,正确率勉强能达到 50% 上下,和瞎猜差别不大。

为什么模型预测的效果那么差?
究其原因,是在于:
1、蚂蚁和蜜蜂的图像数据极其复杂,人类肉眼都不太容易一下子区分,因此简单的 CNN 无法应付这个分类任务
2、整个训练数据集仅仅有 244 个训练样本,这么小的数据量是无法训练大的卷积神经网络的

仍然观察一下模型的训练误差曲线。

# 绘制误差率曲线
x = [x[0] for x in record]
y = [1 - x[1] for x in record]
z = [1 - x[2] for x in record]
#plt.plot(x)
plt.figure(figsize = (10, 7))
plt.plot(y)
plt.plot(z)
plt.xlabel('Epoch')
plt.ylabel('Error Rate')

可以观察到模型的训练过程是非常的不稳定的,或许延长训练周期,增加神经网络深度可以让模型预测更加精确?
先来看一下迁移学习能够达到怎样的效果吧。

加载已训练好的 ResNet 进行迁移学习

加载已训练的大型神经网络 ResNet

ResNet 是微软亚洲研究院何凯明团队开发的一种极深的特殊的卷积神经网络。
该网络的原始版本曾号称是“史上最深的网络”,有 152 层,在物体分类等任务上具有较高的准确度。
一般的,深度网络模型在层数过多时往往会表现得更差。
那么,为什么 ResNet 可以做得如此之深呢?秘诀就在把每两个相邻的 CNN 模块(block)之间加上了一条捷径(shortcut)。
单个 ResNet 模块的结构如下图所示:


image.png

这个“捷径”将第一个模块的输入和第二个模块的输出连接到了一起,并与模块的输出进行张量求和,从而将这两个模块“短路”。这样做的好处就是大大提高了学习的效率,从而可以使得网络可以变得非常深,但是效果却不会下降。考虑到原始的 ResNet 具有较大的复杂性,在本次实验中,实际迁移的是一个具有 18 层的精简版的 ResNet。该网络由 18 个串联在一起的卷积模块构成,其中每一个卷积模块都包括一层卷积一层池化。
下面将加载 ResNet 模型,并观察模型的组成部分。如果是第一次运行,那么模型会被下载到 ~/.torch/models/ 文件夹中。

torch.utils.model_zoo.load_url('http://labfile.oss.aliyuncs.com/courses/1073/resnet18-5c106cde.pth')
# 加载模型库中的residual network,并设置pretrained为true,这样便可加载相应的权重
net = models.resnet18(pretrained=True)
#如果存在GPU,就将网络加载到GPU上
net = net.cuda() if use_cuda else net
# 将网络的架构打印出来
net

从模型的组成部分中,可以看到最后有一层全连接层,也就是 (fc): Linear(in_features=512, out_features=1000)。下面就对它进行“外科手术”。

构建迁移网络

下面把 ResNet18 中的卷积模块作为特征提取层迁移过来,用于提取局部特征。同时,将 ResNet18 中最后的全连接层(fc)替换,构建一个包含 512 个隐含节点的全连接层,后接两个结点的输出层,用于最后的分类输出。最终构建一个 20 层的深度网络。


image.png
# 读取最后线性层的输入单元数,这是前面各层卷积提取到的特征数量
num_ftrs = net.fc.in_features

# 重新定义一个全新的线性层,它的输出为2,原本是1000
net.fc = nn.Linear(num_ftrs, 2)

#如果存在GPU则将网络加载到GPU中
net.fc = net.fc.cuda() if use_cuda else net.fc

criterion = nn.CrossEntropyLoss() #Loss函数的定义
# 将网络的所有参数放入优化器中
optimizer = optim.SGD(net.parameters(), lr = 0.0001, momentum=0.9)

迁移学习的两种模式

搭建好了模型,下面就可以进行迁移学习模型的训练了,这里有个知识点需要注意。现在整个模型的前面大部分的结构都是 ResNet,最后两层被替换成了自定义的全连接层。在训练阶段,迁移过来的 ResNet 模块的结构和所有超参数都可以保持不变,但是权重参数则有可能被新的数据重新训练。
是否要更新这些旧模块的权重参数完全取决于我们采取的迁移学习方式,它主要包括有两种:预训练模式和固定值模式。接下来,将分别应用预训练和固定值两种模式来对这个深度网络进行训练。

预训练模式

在这种模式下,从 ResNet 迁移过来的权重视作新网络的初始权重,但是在训练的过程中则会被梯度下降算法改变数值。使用这种方式,既可以保留迁移过来的知识(已被编码到了权重中),又保证了足够灵活的适应性。使得迁移过来的知识可以通过新网络在新数据上的训练而灵活调整。
预训练模式的梯度传播示意图如下图所示:


image.png
record = [] #记录准确率等数值的容器

#开始训练循环
num_epochs = 20
net.train(True) # 给网络模型做标记,标志说模型在训练集上训练
best_model = net
best_r = 0.0
for epoch in range(num_epochs):
    #optimizer = exp_lr_scheduler(optimizer, epoch)
    train_rights = [] #记录训练数据集准确率的容器
    train_losses = []
    for batch_idx, (data, target) in enumerate(train_loader):  #针对容器中的每一个批进行循环
        data, target = Variable(data), Variable(target) #将Tensor转化为Variable,data为图像,target为标签
        #如果存在GPU则将变量加载到GPU中
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        output = net(data) #完成一次预测
        loss = criterion(output, target) #计算误差
        optimizer.zero_grad() #清空梯度
        loss.backward() #反向传播
        optimizer.step() #一步随机梯度下降
        right = rightness(output, target) #计算准确率所需数值,返回正确的数值为(正确样例数,总样本数)
        train_rights.append(right) #将计算结果装到列表容器中
        loss = loss.cpu() if use_cuda else loss
        train_losses.append(loss.data.numpy())


        #if batch_idx % 20 == 0: #每间隔100个batch执行一次
     #train_r为一个二元组,分别记录训练集中分类正确的数量和该集合中总的样本数
    train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))

    #在测试集上分批运行,并计算总的正确率
    net.eval() #标志模型当前为运行阶段
    test_loss = 0
    correct = 0
    vals = []
    #对测试数据集进行循环
    for data, target in val_loader:
        #如果存在GPU则将变量加载到GPU中
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        data, target = Variable(data, requires_grad=True), Variable(target)
        output = net(data) #将特征数据喂入网络,得到分类的输出
        val = rightness(output, target) #获得正确样本数以及总样本数
        vals.append(val) #记录结果

    #计算准确率
    val_r = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals]))
    val_ratio = 1.0*val_r[0].numpy()/val_r[1]

    if val_ratio > best_r:
        best_r = val_ratio
        best_model = copy.deepcopy(net)
    #打印准确率等数值,其中正确率为本训练周期Epoch开始后到目前撮的正确率的平均值
    print('训练周期: {} \tLoss: {:.6f}\t训练正确率: {:.2f}%, 校验正确率: {:.2f}%'.format(
        epoch, np.mean(train_losses), 100. * train_r[0].numpy() / train_r[1], 100. * val_r[0].numpy()/val_r[1]))       
    record.append([np.mean(train_losses), 1. * train_r[0].data.numpy() / train_r[1], 1. * val_r[0].data.numpy() / val_r[1]])

由于在线环境资源有限,此步骤的代码执行需要较长的时间,需要大家耐心等待。(大概预计在30分钟左右)需要提醒的就是,在线环境有效时间是1个小时,若时间不够,请记得点击工具栏的延时。绘制训练误差曲线,观察训练过程。

x = [x[0] for x in record]
y = [1 - x[1] for x in record]
z = [1 - x[2] for x in record]
#plt.plot(x)
plt.figure(figsize = (10, 7))
plt.plot(y)
plt.plot(z)
plt.xlabel('Epoch')
plt.ylabel('Error Rate')

将预训练的模型用于测试数据,并举例绘制出分类效果。

def visualize_model(model, num_images=6):
    images_so_far = 0
    fig = plt.figure(figsize=(15,10))

    for i, data in enumerate(val_loader):
        inputs, labels = data
        inputs, labels = Variable(inputs), Variable(labels)
        if use_cuda:
            inputs, labels = inputs.cuda(), labels.cuda()
        outputs = model(inputs)
        _, preds = torch.max(outputs.data, 1)
        preds = preds.cpu().numpy() if use_cuda else preds.numpy()
        for j in range(inputs.size()[0]):
            images_so_far += 1
            ax = plt.subplot( 2,num_images//2, images_so_far)
            ax.axis('off')

            ax.set_title('predicted: {}'.format(val_dataset.classes[preds[j]]))
            imshow(data[0][j])

            if images_so_far == num_images:
                return
visualize_model(net)

plt.ioff()
plt.show()

固定值模式

在这种模式下,迁移过来的部分网络在结构和权重上都保持固定的数值不会改变。训练过程仅针对迁移模块后面的全链接网络。当使用反向传播算法的时候,误差反传过程会在迁移模块中停止,从而不改变迁移模块中的权重数值。采用这种方式,可以很大程度的保留被迁移部分的知识不被破坏,对新信息的适应完全体现在迁移模块后面的全链接网络上。因此,它的灵活适应性会差一些。然而,由于迁移模块不需要信息,因此,需要调节的参数少了很多,学习的收敛速度也理应会更快。


image.png

要想让模型在固定值模式下训练,需要先锁定网络模型相关位置的参数。锁定的方法非常简单,只要把网络的梯度反传标志 requires_grad 设置为 False 就可以了。

# 加载residual网络模型
net = torchvision.models.resnet18(pretrained=True)
# 将模型放入GPU中
net = net.cuda() if use_cuda else net

# 循环网络,将所有参数设为不更新梯度信息
for param in net.parameters():
    param.requires_grad = False

# 将网络最后一层线性层换掉
num_ftrs = net.fc.in_features
net.fc = nn.Linear(num_ftrs, 2)
net.fc = net.fc.cuda() if use_cuda else net.fc

criterion = nn.CrossEntropyLoss() #Loss函数的定义
# 仅将线性层的参数放入优化器中
optimizer = optim.SGD(net.fc.parameters(), lr = 0.001, momentum=0.9)

下面正式开始训练。

record = [] #记录准确率等数值的容器

#开始训练循环
num_epochs = 4
net.train(True) # 给网络模型做标记,标志说模型在训练集上训练
best_model = net
best_r = 0.0
for epoch in range(num_epochs):
    #optimizer = exp_lr_scheduler(optimizer, epoch)
    train_rights = [] #记录训练数据集准确率的容器
    train_losses = []
    for batch_idx, (data, target) in enumerate(train_loader):  #针对容器中的每一个批进行循环
        data, target = Variable(data), Variable(target) #将Tensor转化为Variable,data为图像,target为标签
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        output = net(data) #完成一次预测
        loss = criterion(output, target) #计算误差
        optimizer.zero_grad() #清空梯度
        loss.backward() #反向传播
        optimizer.step() #一步随机梯度下降
        right = rightness(output, target) #计算准确率所需数值,返回正确的数值为(正确样例数,总样本数)
        train_rights.append(right) #将计算结果装到列表容器中
        loss = loss.cpu() if use_cuda else loss
        train_losses.append(loss.data.numpy())


     #train_r为一个二元组,分别记录训练集中分类正确的数量和该集合中总的样本数
    train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))

    #在测试集上分批运行,并计算总的正确率
    net.eval() #标志模型当前为运行阶段
    test_loss = 0
    correct = 0
    vals = []
    #对测试数据集进行循环
    for data, target in val_loader:
        data, target = Variable(data, requires_grad=True), Variable(target)
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        output = net(data) #将特征数据喂入网络,得到分类的输出
        val = rightness(output, target) #获得正确样本数以及总样本数
        vals.append(val) #记录结果

    #计算准确率
    val_r = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals]))
    val_ratio = 1.0*val_r[0].numpy()/val_r[1]

    if val_ratio > best_r:
        best_r = val_ratio
        best_model = copy.deepcopy(net)
    #打印准确率等数值,其中正确率为本训练周期Epoch开始后到目前撮的正确率的平均值
    print('训练周期: {} \tLoss: {:.6f}\t训练正确率: {:.2f}%, 校验正确率: {:.2f}%'.format(
        epoch, np.mean(train_losses), 100. * train_r[0].numpy() / train_r[1], 100. * val_r[0].numpy()/val_r[1]))       
    record.append([np.mean(train_losses), 1. * train_r[0].data.numpy() / train_r[1], 1. * val_r[0].data.numpy() / val_r[1]])

绘制训练误差曲线,观察训练过程。

# 打印误差曲线
x = [x[0] for x in record]
y = [1 - x[1] for x in record]
z = [1 - x[2] for x in record]
#plt.plot(x)
plt.figure(figsize = (10, 7))
plt.plot(y)
plt.plot(z)
plt.xlabel('Epoch')
plt.ylabel('Error Rate')

展示分类结果。

visualize_model(best_model)

plt.ioff()
plt.show()

系统化试验结果

为了比较不同迁移学习方法对于识别准确度的影响,我们系统化地做了实验,每一种迁移方法都做了 10 次实验并做平均。每种模型都做 10 次训练,即使是在 GPU 上运行也要花费大量时间,所以在这里只把实验结果给大家分析一下。如果想要在自己的环境中运行这个耗时的实验,可以从本书附带的源码文件中找到这部分实验的源码。

简单卷积神经网络取得的效果

首先先看一下作为实验参照的简单卷积神经网络取得的效果。


image.png

黄色曲线是测试数据集错误率,蓝色曲线是训练数据集错误率。

预训练迁移模型取得的效果

image.png

首先可以看到整体的错误率比简单卷积神经网络低了很多。训练错误率可以稳定在 0.02 之下,测试错误率大约在 0.07 左右。因为在预训练模式下,模型对训练数据的拟合性比较强,所以训练错误率与测试错误率差别较大。

固定值迁移模型取得的效果

image.png

可以看到在固定值迁移模式下。训练错误率可以在 0.02 ~ 0.04 之间,比预训练模式稍高。测试错误率大约在 0.07 左右,与预训练模式差不多。固定值模式锁定了大部分权重,模型对训练数据的拟合性没那么强,所以训练错误率与测试错误率的差别也没那么大。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容