目录

  • 常见的卷积神经网络架构
  • 卷积网络的平移不变性
  • 卷积网络的识别原理简述
    • 卷积神经网络的缺陷
  • CNN的迁移学习
    • 迁移学习简介
    • 数据集
      • 使用dataloader生成batch
    • 设置超参数
    • 使用dataloader
    • 加载预训练模型与模型参数
    • 训练
    • 与随机初始化的模型对比训练效果

常见的卷积神经网络架构

20世纪60年代初,David Hubel,Torsten Wiesel和Steven Kuffler在哈佛医学院建立了神经生物学系。他们在论文《Receptive fields, binocular interaction and functional architecture in the cat’s visual cortex》中提出了Receptive fields的概念;1980年,日本科学家福岛邦彦在论文《Neocognitron: A self-organizing neural network model for a mechanism of pattern recognition unaffected by shift in position》提出了一个包含卷积层、池化层的神经网络结构。但是计算量巨大,且未能找到一个好的参数更新方法;
直到1998年,在这个基础上,Yann Lecun在论文《Gradient-Based Learning Applied to Document Recognition》中提出了LeNet-5,将BackPropogation应用到这个神经网络结构的训练上,就形成了当代卷积神经网络的雏形。

LeNet虽然也在阅读支票、识别手写数字体一类的任务上很有效果,但在一般的实际任务中表现不如SVM、Boosting等算法,所以一直处于学术界边缘的地位;
直到2012年的Imagenet图像识别竞赛中,Hinton组的论文《ImageNet Classification with Deep Convolutional Neural Networks》中提到的Alexnet引入了全新的深层结构和dropout方法,颠覆了图像识别领域,使深度学习受到广泛关注:

后面就来到了CNN蓬勃发展的年代,出现了VGG Net(Very Deep Convolutional Network for large-scale Image Recognition),模型逐渐变深:

但面临着一个问题,模型过深不仅难以训练,还必须要巨大体量的数据集,在2015年,何凯明老师提出了至今都很出名的ResNet(Deep Residual Learning for Image Recognition):

最初,MSRA的任少卿、何凯明、孙剑老师,尝试把identity加入到神经网络中,但最简单的identity却出人意料的有效,直接使CNN能够深化到152层、1202层:

这样的设计可以让学习过程中的feature转变为特征的残差,而不是直接变换特征,某种程度上可以降低学习的难度,所以网络也可以变得很深;
紧接着,就从ResNet演变出DenseNet,将残差的优势进一步发挥:

使用DenseNet块再组合得到自定义网络:

卷积网络的平移不变性

卷积网络计算时,filter在特征上滑动,在第七课中提到,卷积层相当于filter个数的全连接网络组合,每个全连接网络最后一层只有一个神经元;
张量输入全连接网络相当于向量之间的点积(Product),而点积是衡量相似程度的一种方式,其物理意义是越相似的向量,点积结果越大;回到卷积网络,一组filter相当于一组局部物体的模板,当filter滑动到对应局部物体上时,在输出特征的某一层上,该区域会得到一个较大的值;另外也体现了:不论局部物体在图像中的哪个位置,CNN都能检测到;
但是,CNN不能解决旋转问题,比如一个局部物体旋转后,CNN就不能检测到,因为filter只认识没有旋转过的局部物体,所以一种解决办法是扩充数据集,对图像进行旋转,强迫CNN学习到局部物体旋转后的检测能力;

卷积网络的识别原理简述

其实通过上面的描述,容易想象到CNN的识别物体的原理:
filter只是探测局部物体,输入张量通过一层CNN后得到输出张量,假设输出张量为(N,c,h,w)(N,c,h,w)(N,c,h,w),从batch中取出一组feature(c,h,w)(c,h,w)(c,h,w);ccc不仅是通道数也是卷积层的filter数量,一个filter代表着一个局部物体模板,即ccc个通道就分别代表ccc个局部物体,而想判断局部物体iii是否存在(是否被CNN检测到),就看通道iii对应的张量(h,w)(h,w)(h,w)中有没有哪个区域的值很大;
当进行全局的MaxPooling后,相当于取出每个通道的最大局物体与模板的相似度,得到(N,c)(N,c)(N,c)的张量,同样,从batch中取出一组feature(c)(c)(c),它是局部物体与模板相似度组合而成的序列,这个序列反应了它可能含有哪些局部物体;另外,不同类别的物体由不同的局部物体组成,所以,这组特征可以输入全连接网络进行分类,从而得到物体类别(原理也是点积);

卷积神经网络的缺陷


通过以上描述,会发现CNN存在一个缺陷:
对于下图:

CNN将会检测到人脸上的局部物体"鼻子,眼睛,嘴巴",经过全连接网络后显然会分类为人脸:

但这在现实中,很难让人说:“这是人脸”


CNN的迁移学习

迁移学习简介

在训练一个新的图像分类任务时,往往不会从完全随机初始化的模型开始,通常会利用在ImageNet上预训练的模型加速训练,可以认为预训练模型已经具有提取Local feature(边,角等局部信息)的能力,而恰好这种local feature也存在于别的任务中,所以可以将模型迁移到其他任务,继续训练;这是一种transfer learning的方法;
迁移学习通常有以下两种方式:

  • fine tuning:从预训练模型开始,改变一些模型的架构,继续训练整个模型的参数;
  • feature extraction:不改变预训练模型的参数,只更新自己后增添的模型参数,形象理解为将预训练模型当做特征提取的工具;

之所以在ImageNet上预训练,是因为ImageNet是一个种类丰富的数据集,这可以使预训练模型具备提取大部分事物特征的能力,从而利于迁移学习能在各种任务上展开;

迁移学习的一般步骤:

  • 1.初始化预训练模型;
  • 2.更改最后一层输出层;
  • 3.可以重新定义一个optimizer更新参数,主要是选择更新哪些参数以及学习率的调整;
  • 4.模型训练;

数据集

使用hymenoptera_data数据集,这个数据集包括两类图片, beesants, 这些数据都被处理成了可以使用torchvision.datasets.ImageFolder来读取的格式。

需要的参数有:

  • num_classes表示数据集分类的类别数;
  • batch_size mini-batch的大小;
  • num_epochs 数据集遍历次数;
  • feature_extract 表示训练的时候使用fine tuning方式还是feature extraction方式;

使用dataloader生成batch

使用dataloader生成batch:

from torchvision import datasets,transforms
import torch.utils.data as tud
import osDATA_DIR = "./hymenoptera_data"# Batch size for training (change depending on how much memory you have)
BATCH_SIZE = 32
# Number of epochs to train for
NUM_EPOCHS = 15INPUT_SIZE = 224# 对数据预处理,并使用dataloader
# ImageFolder以目录名作为类别
train_images=datasets.ImageFolder(os.path.join(DATA_DIR,"train"),transforms.Compose([#随便从图片中截取input_size*input_size的图片transforms.RandomResizedCrop(INPUT_SIZE),#以概率水平翻转PIL图像或张量,shape应该为[...,H,W],默认概率为0.5transforms.RandomHorizontalFlip(),transforms.ToTensor(),]))trainloader=tud.DataLoader(train_images,batch_size=BATCH_SIZE,shuffle=True, # 每个epoch打乱一次num_workers=0)img=next(iter(trainloader))
print(img[0].size()) # [32,3,224,224]
print(img[1].size()) # [32]

通过transforms.ToPILImage()将张量转为PIL image,实现可视化:

def imageshow(tensor, title=None):import matplotlib.pyplot as pltfrom torchvision import transformsuncode = transforms.ToPILImage()plt.figure()# clone,梯度会流向原tensor# 注意区别clone和detach:回顾pytorch记事本image = tensor.cpu().clone()image = image.squeeze(0)  # 去除batch_size维度image = uncode(image)plt.imshow(image)if title is not None:plt.title(title)plt.show()

从batch中选一个image的张量进行可视化:

imageshow(img[0][31], title='image')


改进写法,重新定义dataloader,并对输入图像的张量进行标准化,已知三个通道的均值和标准差分别为:

mean:[0.485, 0.456, 0.406],
std: [0.229, 0.224, 0.225]

把训练集和验证集的dataloader保存到字典里:

# 改进写法,重新定义dataloader
data_transforms = {"train": transforms.Compose([  # 随便从图片中截取input_size*input_size的图片transforms.RandomResizedCrop(INPUT_SIZE),# 以概率水平翻转PIL图像或张量,shape应该为[...,H,W],默认概率为0.5transforms.RandomHorizontalFlip(),transforms.ToTensor(),# 已知该数据集归一化后的mean和stdtransforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),# 验证和训练时对数据的操作是不同的"val": transforms.Compose([  # 从中心裁剪input_size*input_size的图片transforms.CenterCrop(INPUT_SIZE),transforms.ToTensor(),# 已知该数据集归一化后的mean和stdtransforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
}image_datasets = {x: datasets.ImageFolder(os.path.join(DATA_DIR, x), data_transforms[x]) for x in ["train", "val"]}dataloaders_dict = {x:tud.DataLoader(image_datasets[x],batch_size=BATCH_SIZE,shuffle=True,  # 每个epoch打乱一次num_workers=0) for x in ["train", "val"]}

同样地,可以利用之前定义的函数imageshow(tensor, title=None)对张量可视化:

# 获取一个样本图片的张量
img=next(iter(dataloaders_dict["val"]))# 可视化
imageshow(img[0][31], title='image')

设置超参数

先导入必要的包,比如使用torchvision的models可以选择预训练模型:

import numpy as np
import torchvision
import torch# 使用torchvision的models选择预训练模型
from torchvision import datasets, transforms, modelsimport torch.utils.data as tud
import torch.nn as nnimport matplotlib.pyplot as plt
import time
import os
import copy

设置超参数:

DATA_DIR = "./hymenoptera_data"
# 可选择 [resnet, alexnet, vgg, squeezenet, densenet, inception]
MODEL_NAME = "resnet"
#参数下载地址 https://download.pytorch.org/models/resnet18-5c106cde.pth
MODEL_STATE_PATH="./resnet18-5c106cde.pth"NUM_CLASSES = 2BATCH_SIZE = 32NUM_EPOCHS = 15FEATURE_EXTRACT = TrueUSE_PRETRAINED=TrueINPUT_SIZE = 224USE_CUDA=torch.cuda.is_available()
DEVICE=torch.device("cuda" if USE_CUDA else "cpu")

注意resnet18的模型参数需要提前下载,下载地址:resnet18-5c106cde.pth

使用dataloader

和数据集部分的内容一样,使用dataloader生成batch,只是注意改进写法:

data_transforms = {"train": transforms.Compose([  # 随便从图片中截取input_size*input_size的图片transforms.RandomResizedCrop(INPUT_SIZE),# 以概率水平翻转PIL图像或张量,shape应该为[...,H,W],默认概率为0.5transforms.RandomHorizontalFlip(),transforms.ToTensor(),# 已知该数据集归一化后的mean和stdtransforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),# 验证和训练时对数据的操作是不同的"val": transforms.Compose([  # 从中心裁剪input_size*input_size的图片transforms.CenterCrop(INPUT_SIZE),transforms.ToTensor(),# 已知该数据集归一化后的mean和stdtransforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
}image_datasets = {x: datasets.ImageFolder(os.path.join(DATA_DIR, x), data_transforms[x]) for x in ["train", "val"]}dataloaders_dict = {x: tud.DataLoader(image_datasets[x],batch_size=BATCH_SIZE,shuffle=True,  # 每个epoch打乱一次num_workers=0) for x in ["train", "val"]}

加载预训练模型与模型参数

加载预训练模型只是获得模型的框架,还需要载入来自官网训练的参数 resnet18-5c106cde.pth 才是完整的预训练模型;
如果是feature extraction的训练方式,需要定义一个函数冻结模型的参数,在训练时,就不再追踪预训练模型的梯度,节省显存:

def set_parameters_requires_grad(model, feature_extract):if feature_extract:# feature extraction方式的迁移学习不需要更新预训练模型,不用计算预训练模型的梯度for param in model.parameters():param.requires_grad = Falseelse:pass

如果默认让torchvision保存预训练参数,会不便于管理文件,所以我先加载模型,再单独加载参数,模型及对应参数文件在文档里找:
torchvision/models

# 初始化预训练模型
def initialize_model(model_name, model_state_path, feature_extract, num_classes, use_pretrained=True):if model_name == "resnet":# 如果pretrained=False,得到是一个完全随机初始化的resnet18model = models.resnet18(pretrained=False)# 如果默认让torchvision保存预训练参数,不便于管理文件,所以我先加载模型,再单独加载参数# 模型及对应参数文件在文档里找:# https://github.com/pytorch/vision/tree/master/torchvision/models# 加载预训练模型的参数if use_pretrained:model.load_state_dict(torch.load(model_state_path))# 根据是否需要fine tuning设置参数set_parameters_requires_grad(model, feature_extract)# 获取模型最后fc层的输入特征数num_features = model.fc.in_features# 虽然前面已经将parameters()的requires_grad全设为False,但现在相当于重新定义了fc# 凡是使用nn下的模块,该模块的待学习参数都是requires_grad=True# 即,模型将只更新fc层model.fc = nn.Linear(num_features, num_classes)else:print("model not found")return model

实例化这个模型:

model = initialize_model(MODEL_NAME,MODEL_STATE_PATH,FEATURE_EXTRACT,NUM_CLASSES,USE_PRETRAINED)print(model.layer1[0].conv1.weight.requires_grad)
print(model.fc.weight.requires_grad)

打印结果:

False,True

这确实和initialize_model函数内的内容吻合:
虽然前面已经将parameters()的requires_grad全设为False,但由于使用nn.Linear重新定义了fc(凡是使用nn下的模块,该模块的待学习参数都是requires_grad=True),所以模型只有最后fc层的weight和bias是会追踪梯度的

训练

基于前面的设置工作,现在定义函数用于训练:

def train_model(model, dataloaders_dict, device, loss_fn, optimizer, num_epochs=5):# 深拷贝:拷贝父子对象best_model_weights = copy.deepcopy(model.state_dict())best_acc = 0.val_acc_history = []for epoch in range(num_epochs):for mode in ["train", "val"]:running_loss = 0.running_correct = 0.if mode == "train":model.train()else:model.eval()for inputs, labels in dataloaders_dict[mode]:inputs, labels = inputs.to(device), labels.to(device)# 当torch.autograd.set_grad_enabled(True),会计算梯度# 否则相当于torch.no_gradwith torch.autograd.set_grad_enabled(mode == "train"):outputs = model.forward(inputs)  # [batch_size,2]loss = loss_fn(outputs, labels)# 获取索引preds = torch.argmax(outputs, dim=1)# 如果mode为train,进行参数更新if mode == "train":loss.backward()optimizer.step()model.zero_grad()running_loss += loss.item() * inputs.size(0)running_correct += torch.sum(preds.view(-1) == labels.view(-1)).item()epoch_loss = running_loss / len(dataloaders_dict[mode].dataset)epoch_accuracy = running_correct / len(dataloaders_dict[mode].dataset)print("epoch:{},mode:{},epoch_loss:{},epoch_accuracy:{}".format(epoch, mode, epoch_loss, epoch_accuracy))# 记录accuracyif mode == "val":val_acc_history.append(epoch_accuracy)# 保存模型if mode == "val" and epoch_accuracy > best_acc:best_acc = epoch_accuracy# 深拷贝:拷贝父子对象best_model_weights = copy.deepcopy(model.state_dict())# 加载最好的模型参数model.load_state_dict(best_model_weights)return model, val_acc_history

为了规范,重新进行一次模型实例化:

model=initialize_model(MODEL_NAME,MODEL_STATE_PATH,FEATURE_EXTRACT,NUM_CLASSES,USE_PRETRAINED)
model=model.to(DEVICE)

选择优化方法为SGD,但更新的参数只限于requires_grad=True的张量,所以可以借助filter函数进行过滤,filter参考python笔记本的函数部分

"""
filter函数是对可迭代对象进行过滤,返回一个新对象
filter(function or None,iterable)->filter object
"""
optimizer=torch.optim.SGD(filter(lambda param:param.requires_grad,model.parameters()),lr=1e-3,momentum=0.9)

这是分类问题,损失函数就简单使用交叉熵:

loss_fn=nn.CrossEntropyLoss(reduction="mean")

调用train_model进行训练:

model,val_acc_history=train_model(model,dataloaders_dict,DEVICE,loss_fn,optimizer,NUM_EPOCHS)

与随机初始化的模型对比训练效果

为了体现迁移学习的优势,同样加载resnet18,并改变最后的全连接fc层,唯一区别在于模型参数为随机初始化,然后进行训练:

# 选择一个没有预训练的模型作为对比
model_scratch=initialize_model(MODEL_NAME,MODEL_STATE_PATH,False,#FEATURE_EXTRACT,NUM_CLASSES,use_pretrained=False)
model_scratch=model_scratch.to(DEVICE)model_scratch,val_acc_history_scratch=train_model(model_scratch,dataloaders_dict,DEVICE,loss_fn,optimizer,NUM_EPOCHS)

使用训练返回的验证集准确率,对比迁移学习和非迁移学习的效果:

plt.figure()
plt.title("Validation Accuracy vs. Number of Training Epochs")
plt.xlabel("Training Epochs")
plt.ylabel("Validation Accuracy")
plt.plot(range(1,NUM_EPOCHS+1),val_acc_history,label="Pretrained")
plt.plot(range(1,NUM_EPOCHS+1),val_acc_history_scratch,label="Scratch")
#设置y轴的极值
plt.ylim(0,1.0)
#设置x轴的刻度
plt.xticks(np.arange(1, NUM_EPOCHS+1, 1.0))
plt.legend()
plt.savefig("./acc_Compared")
plt.show()


明显看出,迁移学习加快了收敛,随机初始化模型的训练难度很大,效果明显不如迁移学习

第八课.简单的图像分类(二)相关推荐

  1. 第七课.简单的图像分类(一)

    第七课目录 图像分类基础 卷积神经网络 Pooling layer BatchNormalization BatchNormalization与归一化 torch.nn.BatchNorm2d MNI ...

  2. 第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理

    第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理 tags: k8s 源码学习 categories: 源码学习 二次开发 文章目录 第八课 ...

  3. 《幸福就在你身边》第八课、幸福比成功更重要【哈佛大学幸福课精华】

    一.财富与幸福 "财富不是幸福之源".什么会让你感到幸福,一所大房子,一辆好车,还是一位更性感或者更善解人意的伴侣?积极心理学权威米哈伊·西卡森特米哈伊曾问过一个问题:" ...

  4. 斯坦福大学机器学习第八课“神经网络的表示(Neural Networks: Representation)”

    斯坦福大学机器学习第八课"神经网络的表示(Neural Networks: Representation)" 斯坦福大学机器学习第八课"神经网络的表示(Neural Ne ...

  5. python3 scrapy框架,Python3爬虫(十八) Scrapy框架(二)

    对Scrapy框架(一)的补充 Infi-chu: Scrapy优点: 提供了内置的 HTTP 缓存 ,以加速本地开发 . 提供了自动节流调节机制,而且具有遵守 robots.txt 的设置的能力. ...

  6. 生命的形成《基督教与科学》第十八课

    黄牧师 一.生命的奇妙 从罗1:19-21,宇宙与地球都是精密的设计,才能有人和哺乳动物等高等动物的存在,生命的形成更为奇妙. 二.进化论的基本理论 1.传统学校教的进化论是:从无生物变成简单的生物, ...

  7. NeHe OpenGL教程 第四十八课:轨迹球

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  8. 五年级上册计算机课如何拉表格,川教版小学信息技术五年级上册第八课 调整表格...

    第八课调整表格 教材分析: 本课是川教版小学<信息技术>第八课内容.学生已经通过前一节课的学习了掌握了在word中制作简单表格的基本方法,本课则是在前一节课的基础上,对表格进行调整与修饰, ...

  9. NeHe OpenGL第二十八课:贝塞尔曲面

    NeHe OpenGL第二十八课:贝塞尔曲面 贝塞尔曲面: 这是一课关于数学运算的,没有别的内容了.来,有信心就看看它吧. 贝塞尔曲面 作者: David Nikdel ( ogapo@ithink. ...

最新文章

  1. 基因组表达分析:如何选择RNA-seq vs. 芯片
  2. RabbitMQ 3.6 安装
  3. JavaScript中getter/setter的实现
  4. java实用教程——组件及事件处理——对话框(消息对话框,输入对话框,确认对话框)
  5. STM32 初学不知道外设对应的APB1还是APB2
  6. Design-patterns-JS:用JavaScript实现23种设计模式
  7. JSON 数据格式(基础知识)
  8. 我读的第一本书《梦断代码》
  9. 【Java面试系列】Java微服务面试题
  10. hexo搭建博客系列(六)百度,必应,谷歌收录
  11. 怎样在计算机桌面上安装驱动器,驱动安装好了却不知该怎么查看 如何找到驱动安装的位置 - 驱动管家...
  12. 3.26 文字工具的使用 [原创Ps教程]
  13. 王小云计算机,王小云,密码专家——神一样的存在( 开讲了 49′55″)
  14. 在 Windows 系统下常用的 bat 脚本分享
  15. 怎样用c语言写高速超速罚款标准,如何做到科一满分一把过?超速扣分题必须掌握!...
  16. C#中的委托和事件网上最好的解释
  17. 用 Python 制作家用防盗工具
  18. git、GitHub、Gitee(码云)、GitBook、Copilot、GitLab概述
  19. eclipse升级adt
  20. 向mysql写入时间_Python向Mysql写入时间类型数据

热门文章

  1. 如何打造一个经常宕机的业务系统?
  2. 云原生时代,Kubernetes让应用落地的N种招式(附PPT)
  3. 这10道Java面试题!95%的人回答不出来!
  4. 图解负载均衡 LVS、Nginx及HAProxy--云平台技术栈14
  5. 程序员过年被亲戚鄙视:月薪1万5很一般,在大城市很难养活自己吧?
  6. 为什么分布式一定要有一致性方案?
  7. 推荐8款有趣实用的软件,建议你先收藏,总有一天你会用到
  8. 技术架构委员需要关注哪些问题
  9. Scrum团队初建的十一件事——Scrum中文网
  10. 调用训练好的模型(tensorflow)