运行环境:

        pytorch  1.1.0

        torchvision 0.2.2

        cuda 9.0

        cudnn v7.5

介绍       

       本案例中,你可以学习如何使用迁移学习来训练自己的网络,本博客参考pytorch官网(pytorch.org)中的官方教程,并进行了独立的实现与代码的梳理。

       实际工程中,很少有人从零开始训练整个卷积神经网络,因为拥有足够大小的数据集相对较少。相反,通常会在非常大的数据集上对卷积网络进行预训练,然后使用预训练好的网络作为固定的特征提取器来执行感兴趣的任务,根据已有需要训练的类别只将最后的全连接层进行修改。实际工程中,两种迁移学习的方式如下:

        1.  Finetuning the convnet:使用预先训练的网络进行初始化,在原始的网络结构上进行参数调整,而不是随机初始化,对整个网络节点的参数进行训练更新。

        2.  ConvNet as fixed feature extrctor: 固定除全连接层之外的所有层级的参数。且全连接层被一个随机权重的新层取代,并且只有该层被训练。 

Load package and Data

       在介绍完迁移学习的基本概念之后,进行代码块的讲解,首先需要进行包的导入,以及数据的读入操作,代码的相关讲解会在注释中进行讲解,前提是你需要对python的基本数据结构(元组、列表、字典)有一定的了解以及numpy数据结构和torch中的tensor结构有一定的概念,数据结构毕竟是算法的基础。

       我们使用torchvision 和 torch.utils.data 包来进行数据的导入,关于torchvision包的介绍可参考该博客(),案例中是要训练一个对蚂蚁和蜜蜂进行分类的模型,我们大约有120张针对蚂蚁和蜜蜂的训练图像。每个类有75张val验证图像。如果从零开始训练,这是一个非常小的数据集。因此可以使用迁移学习来进行合理的概括。数据集下载地址(https://pan.baidu.com/s/1JKCkc9yjdWuHOBbakmDRGA    提取码:itor),对数据相关操作的代码如下:

#encoding:utf-8
from __future__ import print_function, division

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy

#在程序运行到plt.show()之后可以继续往下执行,不会发生阻塞
plt.ion()   # interactive(交互式的) mode

# Data augmentation(增加) and normalization(标准化) for training
# Just normalization for validation(确认)

#通过Compose函数将多个步骤整合到一起
data_transforms = {
    'train': transforms.Compose([
        transforms.RandomResizedCrop(224),  #将PIL图像裁剪成任意大小和纵横比
        transforms.RandomHorizontalFlip(),  #以0.5的概率水平翻转给定的PIL图像
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(256),             #把给定的图片resize到given size
        transforms.CenterCrop(224),         #在图片的中间区域进行裁剪
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

data_dir = 'data/hymenoptera_data'
#将训练和测试图像路径以字典的形式进行存储(存储期间图片以一定的规则进行变换)
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
                                          data_transforms[x])
                  for x in ['train', 'val']}
#image_datasets[x]:导入的数据集, batch_size:每次读取图片的个数
# shuffle:是否将数据打乱  num_workers:线程数
#返回的dataloader是一个可迭代的对象
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
                                             shuffle=True, num_workers=4)
              for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}   #训练集和测试集的个数
class_names = image_datasets['train'].classes                           #种类  0:ants,1:bees
#支持cuda版本
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

Visualize a few images

        通过对一些训练图像进行可视化操作以了解上述数据操作

def imshow(inp,title=None):
    #imshow for tensor
    #(1,2,0)理解为读入的第二维的数放到第一维,第三维的数放到第二维,第一维的数放到第三维的转置
    #由N*H*W->H*W*N
    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)  # 小于0 ->0 ;大于1 ->1
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(1)  # pause a bit so that plots are updated   

#get a batch of training data,读取4张图像(batch=4)
inputs,classes=next(iter(dataloaders['train']))
#make a grid from batch
out=torchvision.utils.make_grid(inputs)
#imshow
imshow(out,title=[class_names[x] for x in classes])

pytorch的transformer输入 pytorch transfer_数据

Train the model

      在进行数据的读取操作后,进行模型训练代码的讲解,这里用到如下两个知识点: 

      1.学习率的调整(可参考:)

      2.模型的保存

#train the model,parameter scheduler is an LR scheduler object from torch.optim.lr_scheduler
#注意,只有那些参数可以训练的layer才会被保存到模型的state_dict中,如卷积层,线性层等等)
#优化器对象Optimizer也有一个state_dict,它包含了优化器的状态以及被使用的超参数
# (如lr, momentum,weight_decay等
def train_model(model,criterion,optimizer,scheduler,num_epochs=25):
    since=time.time()   #  strat time
    best_model_wts=copy.deepcopy(model.state_dict())##state_dict是一个简单的字典对象,
                                #此处表示model的每一层的weights及偏置等等
    best_acc = 0.0
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # Each epoch has a training and validation phase(训练集和验证集)
        for phase in ['train','val']:
            if phase == 'train':
                scheduler.step()#按照Pytorch的定义是用来更新优化器的学习率的,一般是按照epoch为单位进行更换
                model.train()   #set model to training model
            else:
                model.eval()    #set model to evaluate model

            running_loss = 0.0
            running_corrects = 0

            #iterate over data
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                #zero the parameter gradients
                optimizer.zero_grad()

                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):  #执行完毕自动释放内存。
                    outputs = model(inputs)
                    #print(outputs)  #4*2的tensor,表示4个图像,每个图像数据的类别2
                    _, preds = torch.max(outputs, 1)#1表示按行,总共4行,返回每行的最大值以及最大值对应的索引
                    loss = criterion(outputs, labels)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()  #对训练参数进行更新操作。

                # statistics(统计)
                running_loss += loss.item() * inputs.size(0)
                #print(inputs.size(0))   #[4,3,224,224],size(0)=4
                running_corrects += torch.sum(preds == labels.data)

            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects.double() / dataset_sizes[phase]

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))

            # deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())

        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))
    # load best model weights
    model.load_state_dict(best_model_wts)#保存最好的模型参数
    return model

Visualizing the model predictions

        在训练之后完,可以读入部分图像进行预测,并将结果进行可视化,具体操作代码如下:

#Visualizing the model predictions
def visualize_model(model, num_images=6):
    was_training = model.training
    model.eval()
    images_so_far = 0
    fig = plt.figure()

    with torch.no_grad():
        #enumerate:可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标
        for i, (inputs, labels) in enumerate(dataloaders['val']):
            inputs = inputs.to(device)
            #print(inputs.size())  #[4,3,244,244]
            #print(inputs.size(0)) #4
            #print(inputs.size()[0]) #4
            labels = labels.to(device)

            outputs = model(inputs)
            _, preds = torch.max(outputs, 1)

            for j in range(inputs.size()[0]):
                images_so_far += 1
                ax = plt.subplot(num_images//2, 2, images_so_far)   #//:向下取整除
                ax.axis('off')
                ax.set_title('predicted: {}'.format(class_names[preds[j]]))
                imshow(inputs.cpu().data[j])

                if images_so_far == num_images:
                    model.train(mode=was_training)
                    return
        model.train(mode=was_training)

Finetuning the convnet

       在框架搭好之后,这里使用第一种迁移学习的方式进行训练与测试,我们需要完成对已知模型的读入,用到前面需要大家看的关于torchvision包中已有的resnet18模型,具体操作如下:

# Load a pretrained model and reset final fully connected layer.
    model_ft = models.resnet18(pretrained=True)  #导入resnet模型
    num_ftrs = model_ft.fc.in_features     #全连接层
    model_ft.fc = nn.Linear(num_ftrs, 2)   #模型最后的输出类别2(不包括背景)

    model_ft = model_ft.to(device)   #在gpu上面运行

    criterion = nn.CrossEntropyLoss()#交叉熵损失

    # Observe that all parameters are being optimized
    optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)

    # Decay LR by a factor of 0.1 every 7 epochs(每7个周期,lr衰减0.1倍)
    exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

    #Train and evaluate
    model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,num_epochs=25)#25
    visualize_model(model_ft) #此时的model_ft为保存的最终训练模型

        训练和预测由于电脑配置有限,在gpu上跑了将近5分钟,显卡为940mx,显存比较低,可能耗时比较高,没有进行深入实验过,不敢妄下结论,输出结果如下:

Epoch 0/24
----------
train Loss: 0.6271 Acc: 0.6680
val Loss: 0.2139 Acc: 0.9346

Epoch 1/24
----------     ...迭代次数通过num_epochs进行设置。

pytorch的transformer输入 pytorch transfer_数据_02

ConvNet as fixed feature extractor

        此处使用迁移学习的第二种方式进行训练和测试,具体代码如下:

#Here, we need to freeze all the network except the final layer. We need to set requires_
    # grad == False to freeze the parameters so that the gradients are not computed in backward().
    model_conv = torchvision.models.resnet18(pretrained=True)
    for param in model_conv.parameters():
        param.requires_grad = False

    # Parameters of newly constructed modules have requires_grad=True by default
    num_ftrs = model_conv.fc.in_features
    model_conv.fc = nn.Linear(num_ftrs, 2)

    model_conv = model_conv.to(device)

    criterion = nn.CrossEntropyLoss()

    # Observe that only parameters of final layer are being optimized as
    # opposed to before.
    optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)

    # Decay LR by a factor of 0.1 every 7 epochs
    exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)

    model_conv = train_model(model_conv, criterion, optimizer_conv,
                             exp_lr_scheduler, num_epochs=25)
    visualize_model(model_conv)
    plt.ioff()
    plt.show()

        具体的输出结果与第一种方式大致相同,但是耗时更小,这是可以预期的,因为在大多数层级中不需要进行梯度的计算。

总结

        本文主要介绍了迁移学习的两种方式,并通过pytorch进行大致框架的实现,之前有做过关于tensorflow目标检测迁移学习的project,大致思想如此,本文主要提供了迁移学习的主要框架流程结构,帮助大家进行深度学习的应用打下基础,后续实际工程中会用到更加复杂的网络结构,比如faster—rcnn,ssd,mask-rcnn等,训练数据也需要自己采集。但是迁移学习的流程大致如此,还需要对神经网络的内部进行深入的研究。若在代码运行过程中出现bug,欢迎留言讨论。