参考了唐进民的《深度学习之PyTorch实战计算机视觉》7 部分,及
这里的代码。

用两种方法来通过搭建卷积神经网络模型对生活中的普通图片进行分类:

  • 自定义结构的卷积神经网络模型
  • 通过使用迁移学习方法得到的模型
    通过这两种方法,解决同样的问题,以此来看看在效果上是传统的方法更出色,还是迁移学习方法更出色。

1 迁移学习入门

出现诱因:希望用耗费很多资源训练出来的模型能够解决同一类问题,而不是只能解决一个问题。

我们通过对一个训练好的模型进行细微调整,就能将其应用到相似的问题中,最后还能取得很好的效果;另外,对于原始数据较少的问题,我们也能够通过采用迁移模型进行有效解决。

优势:通过迁移学习可以节省大量的时间和精力,而且最终得到的结果不会太差。

需要注意的是,在使用迁移学习的过程中有时会导致迁移模型出现负迁移,我们可以将其理解为模型的泛化能力恶化。假如我们将迁移学习用于解决两个毫不相关的问题,则极有可能使最后迁移得到的模型出现负迁移。

2 数据集处理

数据集(来自 Kaggle 网站上的“Dogs vs. Cats”竞赛项目,百度网盘 提取码: 5uh6):

  • 训练数据集:25000 张猫和狗的图片( 12500 张猫的图片和 12500 张狗)
  • 测试数据集:12500 张无序混杂的猫狗图片,而且没有对应的标签
  • 验证数据集:从训练数据集的猫狗图片中各抽出 2500 张图片

这些数据集将被用于:训练模型、参数优化、训练模型的泛化能力。

2.1 验证、测试数据集

评估泛化能力:在实践中,我们不会直接使用测试数据集对搭建的模型进行训练和优化,而是在训练数据集中划出一部分作为验证集,来评估在每个批次的训练后模型的泛化能力。

原因:如果我们使用测试数据集进行模型训练和优化,那么模型最终会对测试数据集产生拟合倾向,换而言之,我们的模型只有在对测试数据集中图片的类别进行预测时才有极强的准确率,而在对测试数据集以外的图片类别进行预测时会出现非常多的错误,这样的模型缺少泛化能力。

所以,为了防止这种情况的出现,我们会把测试数据集从模型的训练和优化过程中隔离出来,只在每轮训练结束后使用。

如果模型对验证数据集和测试数据集的预测同时具备高准确率低损失值,就基本说明模型的参数优化是成功的,模型将具备极强的泛化能力。

2.2 数据预览

'''导入必要的包'''
import torch
import torchvision
from torchvision import datasets,transforms
import os
import matplotlib.pyplot as plt 
import time
from torch.autograd import Variable
model_path = 'model_name.pth'
model_params_path = 'params_name.pth'

'''数据载入'''
# data_dir = os.getcwd() 
# data_dir = os.path.join(data_dir,"DogsVSCats")
data_dir = "C:/Users/xinyu/Desktop/data/DogsVSCats/"

data_transform = {
    x:transforms.Compose(
        [
            transforms.Scale([64,64]),    #Scale类将原始图片的大小统一缩放至64×64
            transforms.ToTensor()
        ]
    )
    for x in ["train","valid"]
}


image_datasets = {
    x:datasets.ImageFolder(
        root=os.path.join(data_dir,x),  #将输入参数中的两个名字拼接成一个完整的文件路径
        transform=data_transform[x]
    )
    for x in ["train","valid"]
}


dataloader = {  
    #注意:标签0/1自动根据子目录顺序以及目录名生成
    #如:{'cat': 0, 'dog': 1} #{'狗dog': 0, '猫cat': 1}
    #如:['cat', 'dog']  #['狗dog', '猫cat']
    x:torch.utils.data.DataLoader(
        dataset=image_datasets[x],
        batch_size=16,
        shuffle=True
    )
    for x in ["train","valid"]
}

os.path.join的作用是将输入参数中的两个名字拼接成一个完整的文件路径,其他也有一些常用的 os.path 类方法

下面获取一个批次的数据并进行数据预览和分析,以下代码通过 next 和 iter 迭代操作获取一个批次的装载数据,不过因为受到我们之前定义的 batch_size 值的影响,这一批次的数据只有 16 张图片,所以 X_example 和 y_example 的长度也全部是 16,可以通过打印这两个变量来确认。

 

X_example, y_example = next(iter(dataloader["train"]))
len(X_example), type(X_example), len(y_example), type(y_example)


(16, torch.Tensor, 16, torch.Tensor)

其中,X_example 是 Tensor 数据类型的变量,因为做了图片大小的缩放变换,所以现在图片的大小全部是 64×64 了,那么 X_example 的维度就是(16, 3, 64, 64): 16 张图片,3 条色彩通道(R、G、B),宽度和高度是64。

y_example 也是 Tensor 数据类型的变量,不过其中的元素全部是 0 和 1(在进行数据装载时已经对 dog 文件夹和 cat 文件夹下的内容进行了独热编码(One-Hot Encoding)),所以这时的 0 和 1 不仅是每张图片的标签,还分别对应猫的图片和狗的图片。我们可以做一个简单的打印输出,来验证这个独热编码的对应关系:


index_classes = image_datasets["train"].class_to_idx #显示类别对应的独热编码
index_classes


{‘cat’: 0, ‘dog’: 1}

为了增加之后绘制的图像标签的可识别性,我们还需要通过 image_datasets[“train”].classes 将原始标签的结果存储在名为 example_clasees 的变量中:

example_classes = image_datasets["train"].classes  #将原始图像的类别保存起来
example_classes  #是一个列表


[‘cat’, ‘dog’]

'''使用 Matplotlib 对一个批次的图片进行绘制'''
img = torchvision.utils.make_grid(X_example)
img = img.numpy().transpose([1,2,0])
print([example_classes[i] for i in y_example])
plt.imshow(img)
plt.show()


[‘dog’, ‘dog’, ‘dog’, ‘cat’, ‘cat’, ‘dog’, ‘cat’, ‘cat’, ‘dog’, ‘dog’, ‘cat’, ‘dog’, ‘cat’, ‘cat’, ‘dog’, ‘dog’]

在这里插入图片描述

3 模型搭建和参数优化

使用三个模型:

  • 基于一个简化的 VGGNet 架构搭建卷积神经网络模型并进行模型训练和参数优化
  • 迁移一个完整的 VGG16 架构的卷积神经网络模型
  • 迁移一个 ResNet50 架构的卷积神经网络模型

对比这三个模型在预测结果上的准确性和在泛化能力上的差异。

3.1 自定义 VGGNet

3.1.1 搭建一个简化版的 VGGNet 模型

我们首先需要搭建一个卷积神经网络模型,考虑到训练时间的成本,我们基于 VGG16 架构来搭建一个简化版的 VGGNet 模型,这个简化版模型要求输入的图片大小全部缩放到 64×64,而在标准的 VGG16 架构模型中输入的图片大小应当是 224×224 的;
同时简化版模型删除了 VGG16 最后的三个卷积层和池化层,也改变了全连接层中的连接参数,这一系列的改变都是为了减少整个模型参与训练的参数数量。代码如下(承接上文的代码):

class Models(torch.nn.Module):
    
    def __init__(self):
        super(Models, self).__init__()
        self.Conv = torch.nn.Sequential(torch.nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.MaxPool2d(kernel_size=2, stride=2),
                                       
                                       torch.nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.MaxPool2d(kernel_size=2, stride=2),
                                       
                                       torch.nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.MaxPool2d(kernel_size=2, stride=2),
                                       
                                       torch.nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
                                       torch.nn.ReLU(),
                                       torch.nn.MaxPool2d(kernel_size=2, stride=2))
        
        self.Classes = torch.nn.Sequential(torch.nn.Linear(4*4*512, 1024),
                                          torch.nn.ReLU(),
                                          torch.nn.Dropout(p=0.5),
                                          torch.nn.Linear(1024, 1024),
                                          torch.nn.ReLU(),
                                          torch.nn.Dropout(p=0.5),
                                          torch.nn.Linear(1024, 2))
        def forward(self, input):
            x = self.Conv(input)
            x = x.view(-1, 4*4*512)
            x = self.Classes(x)
            return x
        
model = Models()
print(model)

Models(
  (Conv): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU()
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU()
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU()
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU()
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU()
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU()
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU()
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU()
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU()
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (Classes): Sequential(
    (0): Linear(in_features=8192, out_features=1024, bias=True)
    (1): ReLU()
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=1024, out_features=1024, bias=True)
    (4): ReLU()
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=1024, out_features=2, bias=True)
  )
)

3.1.2 完成参数迁移

Use_gpu = torch.cuda.is_available()
print("torch.cuda.is_available():",torch.cuda.is_available())

#print(len((image_datasets["train"])))#25000
#print(len(image_datasets["valid"]))#5000
#print("陈旭旗 "*10)

'''定义损失函数和优化函数'''
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),lr=0.00001)

if Use_gpu:
    model = model.cuda()

has_been_trained = os.path.isfile(model_path)
if has_been_trained:
    epoch_n = 0
else:
    epoch_n = 2  #10
 
time_open = time.time()
for epoch in range(epoch_n):
    print("Epoch {}/{}".format(epoch+1,epoch_n))
    print("--"*10)

    for phase in ["train","valid"]:
        if phase == "train":
            print("Training...")
            model.train(True)     #启用 BatchNormalization 和 Dropout
        else:
            print("Validing...")
            model.train(False)
        
        running_loss = 0.0
        running_correct = 0

        for batch, data in enumerate(dataloader[phase],1):# start=1 指明下标起始位置
            
            # if batch == 10:
            #     break

            X, y = data #torch.Size([16, 3, 64, 64]) torch.Size([16]
            if Use_gpu:
                X, y = Variable(X.cuda()), Variable(y.cuda())
            else:
                X, y = Variable(X), Variable(y)

            y_pred = model(X)

            _, pred = torch.max(y_pred,1)

            optimizer.zero_grad()

            loss = loss_f(y_pred,y)

            if phase == "train":
                loss.backward()
                optimizer.step()
            
            running_loss += loss.item()
            running_correct += torch.sum(pred == y.data)

            if batch%500 == 0 and phase == "train":
                print(
                    "Batch{},Train Loss:{:.4f},Train ACC:{:.4f}%".format(
                        batch,running_loss/batch, 100.0*running_correct/(16*batch)
                    )
                )
            
        epoch_loss = running_loss*16/len(image_datasets[phase])
        epoch_acc = 100.0*running_correct/len(image_datasets[phase])
        #print(len((image_datasets["train"])))#25000
        #print(len(image_datasets["valid"]))#5000
        print("{} Loss:{:.4f} Acc:{:.4f}%".format(phase,epoch_loss,epoch_acc))
time_end = time.time() - time_open
print("程序运行时间:",int(time_end/60),"分钟")

    
if has_been_trained:
    model = torch.load(model_path)
else:
    torch.save(model, model_path)

结果是:

torch.cuda.is_available(): False
Epoch 1/2
--------------------
Training...
Batch500,Train Loss:0.6402,Train ACC:63.7875%
Batch1000,Train Loss:0.6344,Train ACC:64.3375%
train Loss:0.6344 Acc:64.2550%
Validing...
valid Loss:0.6241 Acc:65.7400%
Epoch 2/2
--------------------
Training...
Batch500,Train Loss:0.6206,Train ACC:65.6375%
Batch1000,Train Loss:0.6237,Train ACC:65.4375%
train Loss:0.6219 Acc:65.8500%
Validing...
valid Loss:0.6117 Acc:66.6600%
程序运行时间: 32 分钟

3.1.3 举例说明

X_example, Y_example = next(iter(dataloader['train']))
#print('X_example个数{}'.format(len(X_example)))   #X_example个数16 torch.Size([16, 3, 64, 64])
#print('Y_example个数{}'.format(len(Y_example)))   #Y_example个数16 torch.Size([16]

#X, y = data #torch.Size([16, 3, 64, 64]) torch.Size([16]
if Use_gpu:
    X_example, Y_example = Variable(X_example.cuda()), Variable(Y_example.cuda())
else:
    X_example, Y_example = Variable(X_example), Variable(Y_example)

y_pred = model(X_example)

index_classes = image_datasets['train'].class_to_idx   # 显示类别对应的独热编码
#print(index_classes)     #{'cat': 0, 'dog': 1}

example_classes = image_datasets['train'].classes     # 将原始图像的类别保存起来
#print(example_classes)       #['cat', 'dog']

img = torchvision.utils.make_grid(X_example)
img = img.cpu().numpy().transpose([1,2,0])
print("实际:",[example_classes[i] for i in Y_example])
#['cat', 'cat', 'cat', 'cat', 'dog', 'cat', 'cat', 'dog', 'cat', 'cat', 'dog', 'dog', 'cat', 'dog', 'dog', 'cat']
_, y_pred = torch.max(y_pred,1)
print("预测:",[example_classes[i] for i in y_pred])

plt.imshow(img)
plt.show()

结果是:

实际: ['dog', 'cat', 'cat', 'dog', 'dog', 'dog', 'dog', 'cat', 'cat', 'dog', 'cat', 'dog', 'cat', 'cat', 'dog', 'dog']
预测: ['dog', 'cat', 'dog', 'dog', 'dog', 'cat', 'dog', 'cat', 'dog', 'cat', 'dog', 'dog', 'dog', 'dog', 'dog', 'dog']

在这里插入图片描述

3.2 迁移 VGG16

看这里:迁移学习——猫狗分类(PyTorch:迁移 VGG16 方法)

3.3 迁移 ResNet50

看这里:迁移学习——猫狗分类(PyTorch:迁移 ResNet50 方法)