首次用Colab完成AI研习社的题目,写的详细一点。
——AI研习社:猫狗大战—经典图像分类题

1.数据导入

首先从AI研习社下载数据集(下载链接 : https://static.leiphone.com/cat_dog.rar)
下载完后解压,然后做调整。
一般来说,数据集中包好两个文件夹,train和val,每个文件夹下面包含N个子文件夹,N是你的分类类别数,且每个子文件夹里面存放的就是这个类别的图像。解压完可以发现,这两个文件夹下都没有给你分类,所以需要用最笨的手工方法先分为两个类:cats和dogs。
train:image.png
val:image.png

分完类之后压缩数据集,压缩为.zip压缩包,上传到Google云端硬盘,记住你的路径。(过程巨慢巨卡)

image.png
至此准备工作完成,开始撸代码。

连上GPU。

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. import os
  4. import torch
  5. import torch.nn as nn
  6. import torchvision
  7. from torchvision import models, transforms, datasets
  8. import time
  9. import json
  10. # 判断是否存在GPU设备
  11. device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  12. print('Using gpu: %s ' % torch.cuda.is_available())

image.png
解压数据集

  1. !unzip '/content/drive/My Drive/app/cat_dog/cat_dog.zip'

在此之前,要装载云端硬盘
image.png
此处我已经装载完成
解压完成
image.png
image.png

2.数据处理

  1. normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
  2. vgg_format = transforms.Compose(
  3. [
  4. transforms.CenterCrop(224),
  5. transforms.ToTensor(),
  6. normalize
  7. ]
  8. )
  9. data_dir = './cat_dog'
  10. dsets = {x: datasets.ImageFolder(os.path.join(data_dir, x), vgg_format) for x in['train', 'val', 'test']}
  11. dset_sizes = {x: len(dsets[x]) for x in['train', 'val', 'test']}
  12. dset_classes = dsets['train'].classes

这里采用官方写好的torchvision.datasets.ImageFolder接口实现数据导入。这个接口需要你提供图像所在的文件夹,就是data_dir = ‘./cat_dog’。
这样torchvision.datasets.ImageFolder就会返回一个列表(比如下面代码中的image_datasets[‘train’]或者image_datasets[‘val’]),列表中的每个值都是一个tuple,每个tuple包含图像和标签信息。

通过下面代码可以查看 dsets 的一些属性

  1. print(dsets['train'].classes)
  2. print(dsets['train'].class_to_idx)
  3. print(dsets['train'].imgs[:5])
  4. print('dset_sizes: ', dset_sizes)

image.png
前面torchvision.datasets.ImageFolder只是返回list,list是不能作为模型输入的,因此在PyTorch中需要用另一个类来封装list,那就是:torch.utils.data.DataLoader。torch.utils.data.DataLoader类可以将list类型的输入数据封装成Tensor数据格式,以备模型使用。

  1. loader_train = torch.utils.data.DataLoader(dsets['train'], batch_size=64, shuffle=True, num_workers=6)
  2. loader_val = torch.utils.data.DataLoader(dsets['val'], batch_size=5, shuffle=False, num_workers=6)
  3. loader_test = torch.utils.data.DataLoader(dsets['test'], batch_size=5, shuffle=False, num_workers=6)
  4. '''
  5. val 数据一共有2000张图,每个batch是5张,因此,下面进行遍历一共会输出到 400
  6. 同时,把第一个 batch 保存到 inputs_try, labels_try,分别查看
  7. '''
  8. count = 1
  9. for data in loader_val:
  10. if count == 1:
  11. inputs_try, labels_try = data
  12. count += 1
  13. print(labels_try)
  14. print(inputs_try.shape)

image.png

  1. # 显示图片的小程序
  2. def imshow(inp, title=None):
  3. # Imshow for Tensor.
  4. inp = inp.numpy().transpose((1, 2, 0))
  5. mean = np.array([0.485, 0.456, 0.406])
  6. std = np.array([0.229, 0.224, 0.225])
  7. inp = np.clip(std * inp + mean, 0,1)
  8. plt.imshow(inp)
  9. if title is not None:
  10. plt.title(title)
  11. plt.pause(0.001) # pause a bit so that plots are updated
  12. # 显示 labels_try 的5张图片,即valid里第一个batch的5张图片
  13. out = torchvision.utils.make_grid(inputs_try)
  14. imshow(out, title=[dset_classes[x] for x in labels_try])

image.png

3.创建VGG Model

torchvision中集成了很多在 ImageNet (120万张训练数据) 上预训练好的通用的CNN模型,可以直接下载使用。
在本课程中,我们直接使用预训练好的 VGG 模型。同时,为了展示 VGG 模型对本数据的预测结果,还下载了 ImageNet 1000 个类的 JSON 文件。
在这部分代码中,对输入的5个图片利用VGG模型进行预测,同时,使用softmax对结果进行处理,随后展示了识别结果。可以看到,识别结果是比较非常准确的。

  1. !wget https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json

image.png

  1. model_vgg = models.vgg16(pretrained=True)
  2. with open('./imagenet_class_index.json') as f:
  3. class_dict = json.load(f)
  4. dic_imagenet = [class_dict[str(i)][1] for i in range(len(class_dict))]
  5. inputs_try = inputs_try.to(device)
  6. labels_try = labels_try.to(device)
  7. model_vgg = model_vgg.to(device)
  8. outputs_try = model_vgg(inputs_try)
  9. print(outputs_try)
  10. print(outputs_try.shape)

image.png
可以看到结果为5行,1000列的数据,每一列代表对每一种目标识别的结果,有正数也有负数。
为了将VGG网络输出的结果转化为对每一类的预测概率,我们把结果输入到 Softmax 函数

  1. m_softm = nn.Softmax(dim=1)
  2. probs = m_softm(outputs_try)
  3. vals_try, pred_try = torch.max(probs, dim=1)
  4. print( 'prob sum: ', torch.sum(probs,1))
  5. print( 'vals_try: ', vals_try)
  6. print( 'pred_try: ', pred_try)

image.png

4.修改最后一层,冻结前面层的参数

我们的目标是使用预训练好的模型,因此,需要把最后的 nn.Linear 层由1000类,替换为2类。为了在训练中冻结前面层的参数,需要设置 required_grad=False。这样,反向传播训练梯度时,前面层的权重就不会自动更新了。训练中,只会更新最后一层的参数。

  1. #查看vgg16模型
  2. print(model_vgg)

image.png
image.png
不对网络详细说明了。训练的时候将更新最后一层的参数,并增加一层logsoftmax
image.png

  1. model_vgg_new = model_vgg
  2. for param in model_vgg_new.parameters():
  3. param.requires_grad = False
  4. model_vgg_new.classifier._modules['6'] = nn.Linear(4096, 2)
  5. model_vgg_new.classifier._modules['7'] = torch.nn.LogSoftmax(dim=1)
  6. model_vgg_new = model_vgg_new.to(device)
  7. print(model_vgg_new.classifier)

image.png

5.训练并测试全连接层

包括三个步骤:第1步,创建损失函数和优化器;第2步,训练模型;第3步,测试模型。

  1. '''
  2. 第一步:创建损失函数和优化器
  3. 损失函数 NLLLoss()的输入是一个对数概率向量和一个目标标签.
  4. 它不会为我们计算对数概率,适合最后一层是log_softmax()的网络.
  5. '''
  6. criterion = nn.NLLLoss()
  7. # 学习率
  8. lr = 0.001
  9. # 随机梯度下降
  10. optimizer_vgg = torch.optim.Adam(model_vgg_new.classifier[6].parameters(), lr=lr)
  11. '''
  12. 第二步:训练模型
  13. '''
  14. def train_model(model, dataloader, size, epochs=1, optimizer=None):
  15. model.train()
  16. for epoch in range(epochs):
  17. running_loss = 0.0
  18. running_corrects = 0
  19. count = 0
  20. for inputs, classes in dataloader:
  21. inputs = inputs.to(device)
  22. classes = classes.to(device)
  23. outputs = model(inputs)
  24. loss = criterion(outputs, classes)
  25. optimizer = optimizer
  26. optimizer.zero_grad()
  27. loss.backward()
  28. optimizer.step()
  29. _,preds = torch.max(outputs.data, 1)
  30. running_loss += loss.data.item()
  31. running_corrects += torch.sum(preds == classes.data)
  32. count += len(inputs)
  33. print('Training: No. ', count, ' process ... total: ', size)
  34. epoch_loss = running_loss / size
  35. epoch_acc = running_corrects.data.item() / size
  36. print('Loss: {:.4f} Acc: {:.4f}'.format(epoch_loss, epoch_acc))
  37. # 模型训练
  38. train_model(model_vgg_new, loader_train, size=dset_sizes['train'], epochs=1, optimizer=optimizer_vgg)

image.png
测试模型:

  1. def test_model(model, dataloader, size):
  2. model.eval()
  3. predictions = np.zeros(size)
  4. all_classes = np.zeros(size)
  5. all_proba = np.zeros((size, 2))
  6. i = 0
  7. running_loss = 0.0
  8. running_corrects = 0
  9. for inputs, classes in dataloader:
  10. inputs = inputs.to(device)
  11. classes = classes.to(device)
  12. outputs = model(inputs)
  13. loss = criterion(outputs, classes)
  14. _,preds = torch.max(outputs.data, 1)
  15. running_loss += loss.data.item()
  16. running_corrects += torch.sum(preds == classes.data)
  17. predictions[i:i+len(classes)] = preds.to('cpu').numpy()
  18. all_classes[i:i+len(classes)] = classes.to('cpu').numpy()
  19. all_proba[i:i+len(classes)] = outputs.data.to('cpu').numpy()
  20. i += len(classes)
  21. print('Testing: No. ', i, ' process ... total: ', size)
  22. epoch_loss = running_loss / size
  23. epoch_acc = running_corrects.data.item() / size
  24. print('Loss: {:.4f} Acc: {:.4f}'.format(epoch_loss, epoch_acc))
  25. return predictions, all_proba, all_classes
  26. predictions, all_proba, all_classes = test_model(model_vgg_new, loader_valid, size=dset_sizes['valid'])

踩坑指南——model.train()和model.eval()的作用:

如果模型中有BN层(Batch Normalization)和Dropout,需要在训练时添加model.train(),在测试时添加model.eval()。其中model.train()是保证BN层用每一批数据的均值和方差,而model.eval()是保证BN用全部训练数据的均值和方差;而对于Dropout,model.train()是随机取一部分网络连接来训练更新参数,而model.eval()是利用到了所有网络连接。
需要联系Batch Normalization和Dropout的原理之后去理解为何要这么做。

(总之,套用框架就是在训练开始之前写上model.train(),在测试时写上model.eval())
image.png
测试test数据:

  1. def t_model(model, dataloader, size):
  2. model.eval()
  3. predictions = np.zeros(size)
  4. i = 0
  5. for inputs, classes in dataloader:
  6. inputs = inputs.to(device)
  7. classes = classes.to(device)
  8. outputs = model(inputs)
  9. loss = criterion(outputs, classes)
  10. _,preds = torch.max(outputs.data, 1)
  11. predictions[i:i+len(classes)] = preds.to('cpu').numpy()
  12. i += len(classes)
  13. print('Testing: No. ', i, ' process ... total: ', size)
  14. return predictions
  15. predictions = t_model(model_vgg_new, loader_test, size=dset_sizes['test'])
  16. print(predictions)

image.png

6.可视化模型预测结果(主观分析)

主观分析就是把预测的结果和相对应的测试图像输出出来看看,一般有四种方式:

  • 随机查看一些预测正确的图片
  • 随机查看一些预测错误的图片
  • 预测正确,同时具有较大的probability的图片
  • 预测错误,同时具有较大的probability的图片
  • 最不确定的图片,比如说预测概率接近0.5的图片 ```python

    单次可视化显示的图片个数

    n_view = 8 correct = np.where(predictions==all_classes)[0] from numpy.random import random, permutation idx = permutation(correct)[:n_view] print(‘random correct idx: ‘, idx) loader_correct = torch.utils.data.DataLoader([dsets[‘val’][x] for x in idx],
                batch_size = n_view,shuffle=True)
    
    for data in loader_correct: inputs_cor,labels_cor = data

    Make a grid from batch

    out = torchvision.utils.make_grid(inputs_cor) imshow(out, title=[l.item() for l in labels_cor])

类似的思路,可以显示错误分类的图片,这里不再重复代码

![image.png](https://cdn.nlark.com/yuque/0/2020/png/2722003/1604404075113-ac70c8ca-9e34-4082-ad63-3b2c9c4a62b2.png#align=left&display=inline&height=158&margin=%5Bobject%20Object%5D&name=image.png&originHeight=158&originWidth=681&size=80357&status=done&style=none&width=681)
<a name="eixpk"></a>
### 7.处理测试集
将结果导入到csv文件中
```python
import csv
with open('./cat_dog/cats_vs_dogs.csv','w',newline="")as f:
  writer = csv.writer(f)
  for index,cls in enumerate(predictions):
    path = datasets.ImageFolder(os.path.join(data_dir,'test'),vgg_format).imgs[index][0]
    l = path.split("/")
    img_name = l[-1]
    order = int(img_name.split(".")[0])
    writer.writerow([order,int(predictions[index])])

image.png
下载文件cats_vs_dogs.csv,排序,提交到AI研习社
image.png
准确率还挺高的

参考博文:

  1. https://github.com/OUCTheoryGroup/colab_demo/blob/master/05_04_Transfer_VGG_for_dogs_vs_cats.ipynb
  2. https://blog.csdn.net/u014380165/article/details/78525273
  3. https://blog.csdn.net/Qy1997/article/details/106455717