背景介绍
之前的文章我们已经介绍过了卷积神经网络,很多同学也可以自己写一些简单的卷积神经网络跑一下模型,这个时候我们会发现自己写的网络效果并不是那么好,有可能收敛速度很慢,甚至不收敛。这个时候,使用一些成熟的模型,比如vgg,inception net, resnet等等,就显得尤为重要了。这些成熟的网络都是大牛实验室做了无数次实验而实现的优秀的网络,并且通过 Imagenet 这个比赛拿了名次,发了论文的,所以现在深度学习的门槛越来越低,一方面得益于现在的框架让我们写网络变得很简单,另外一方面就是这些大牛愿意开源共享他们的模型和实验结果。
本着不用白不用的原则,我们可以使用现成的模型做一些微调来训练我们的数据,而且现在很多的实验研究也是从已有的模型出发的。这个时候,如果你拥有了一块Titan X 或者1080 的Nvidia显卡,那么你就能够快速的开始炼丹,相对于没有显卡,速度可以说是快得飞起。但是如果你学校的实验并没有配什么好的电脑,或者你个人也并没有这么土豪,可以砸很多钱进去玩deep learning,那么 transfer learning 可以说是一剂灵丹妙药了,可以让没有显卡的人也能玩转deep learning。
著名课程cs231n也有一章来讲解 transfer learning,有兴趣的同学可以看看。
下面我会用kaggle上面的一个比赛来实际应用 transfer learning,看看效果。比赛叫 dogs vs cats,可能需要科学上网才能访问,数据集我已经下好了,分享在百度网盘。
原理分析
transfer learning
首先我们介绍一下到底什么是 transfer learning,翻译成中文叫做迁移学习。根据这个名字其实很好理解,所谓迁移学习就是讲已经训练好的网络用到你自己的数据上。有的同学可能会很好奇,为什么别人在他的数据集上训练好的网络能够拿给我们直接使用呢?
这里有两点,第一点我们并不是将别人的网络拿过来一点不改进行训练,我们会改掉网络最后的全连接层进行训练,因为我们的分类问题并不一定和原来的分类问题一样;第二点为什么训练好的网络拿来直接应用到我们的数据上可行,因为卷积神经网络其实可以理解为两个部分,前面的卷积部分以及后面的分类部分,而前面的卷积部分主要做的事就是提取特征,而他训练好的网络对于图片的特征提取效果是特别好的,所以我们可以直接将他的网络的卷积部分拿过来提取我们自己图片的特征,而我们自己的数据集要实现的分类就是用我们自己的分类全连接层就可以了。
以上就是 transfer learning 的大概思想,是不是特别简单呢?总结起来就是将已有的网络和训练好的权重一起迁移过来,然后训练过程中只修改最后的全连接层部分的参数,实现最后我们的分类目的。
另外 transfer Learning 并不是万能的,它在相似数据集上的效果才是良好的,比如你用的预训练的参数是自然景物的图片分类得到的,你用这个参数来做人脸的识别,效果可能就没有那么好了,因为人脸的特征提取和自然景物的特征提取是不同的,所以相应的参数训练后也是不同的。如果这个是万能的,那我们就不用研究 deep learning 了,直接训练好一个参数然后应用到所有的数据集上就万事大吉了,但是事实上并没有太好的效果,归根结底这只是一种节约计算资源的做法,即相似的数据可以用训练好的参数,不用再重新训练了。
dogs vs cats
今天我们要实际应用的项目是一个猫狗的分类问题,即分别这个宠物是猫还是狗,预训练的参数是ImageNet比赛上的参数,为什么这个可以用呢?是因为Imagenet比赛里面有很多动物的分类,和我们要做的数据集具有相似性,所以我们可以用这个预训练的参数。
方法
1.我们可以导入预训练好的网络,将最后的全连接层改成我们自己的,然后开始训练,这样会特别快的收敛,达到 transfer learning 的目的。
2.我们可以锁定前面卷积的参数,而让网络只修改最后的全连接层的参数,这样可以让我们训练时间减少。第一种方法我们训练的时候每个epoch都需要跑一边所有的数据集,将数据集前向传播,通过卷积层到我们的全连接层,然后再输出结果,接着反向传播更新参数,这样是非常浪费计算资源的,所以我们可以将数据集经过一次卷积层得到的结果保存起来,这个结果我们称为特征向量,这样所有的数据集就只需要通过一次卷积网络,大大节约了我们的计算资源。
3.前面我们只使用一种预训练好的网络,其实我们可以使用多个预训练好的网络,将他们并联在一起,数据经过每个网络都会得到特征向量,然后将这些特征向量拼接在一起进入我们的全连接层。
可能你看了前面的东西还是有点迷糊,没关系,现在我要show you the code,看了代码之后可能你就会清楚到底是怎么一回事了。
实现
method-1
对于这一种方法,我们通过下面这样定义预训练网络就可以了。
transfer_model = models.resnet18(pretrained=True)
dim_in = transfer_model.fc.in_features
transfer_model.fc = nn.Linear(dim_in, img_classes)
这里我们使用的网络是一个小型的18层的resnet(残差网络),我们将最后的全连接层换成了适用于我们数据集分类的全连接层,详细的训练代码可以去我的github上 fix_train.py 查看。
method-2
定义预训练网络
首先我们需要定义好特征提取的预训练网络
class feature_net(nn.Module):
def __init__(self, model):
super(feature_net, self).__init__()
if model == 'vgg':
vgg = models.vgg19(pretrained=True)
self.feature = nn.Sequential(*list(vgg.children())[:-1])
self.feature.add_module('global average', nn.AvgPool2d(9))
elif model == 'inceptionv3':
inception = models.inception_v3(pretrained=True)
self.feature = nn.Sequential(*list(inception.children())[:-1])
self.feature._modules.pop('13')
self.feature.add_module('global average', nn.AvgPool2d(35))
elif model == 'resnet152':
resnet = models.resnet152(pretrained=True)
self.feature = nn.Sequential(*list(resnet.children())[:-1])
def forward(self, x):
"""
model includes vgg19, inceptionv3, resnet152
"""
x = self.feature(x)
x = x.view(x.size(0), -1)
return x
class classifier(nn.Module):
def __init__(self, dim, n_classes):
super(classifier, self).__init__()
self.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(dim, 1000),
nn.ReLU(True),
nn.Dropout(0.5),
nn.Linear(1000, n_classes)
)
def forward(self, x):
x = self.fc(x)
return x
这里的 feature_net 就是特征提取的网络,接受的参数是 vgg, inceptionv3, resnet152,表示分别用的19层vgg网络,或者inceptionv3网络或者152层的残差网络作为预训练的网络。
这里的classifier就是我们定义的对于我们自己的数据的全连接分类层。
定义特征提取的函数
接着我们可以写一个函数去提取特征,这样我们调用一次函数,传递给他的参数为不同的网络的时候就能够得到不同的特征。
def CreateFeature(model, phase, outputPath='.'):
"""
Create h5py dataset for feature extraction.
ARGS:
outputPath : h5py output path
model : used model
labelList : list of corresponding groundtruth texts
"""
featurenet = feature_net(model)
if use_gpu:
featurenet.cuda()
feature_map = torch.FloatTensor()
label_map = torch.LongTensor()
for data in tqdm(dataloader[phase]):
img, label = data
if use_gpu:
img = Variable(img, volatile=True).cuda()
else:
img = Variable(img, volatile=True)
out = featurenet(img)
feature_map = torch.cat((feature_map, out.cpu().data), 0)
label_map = torch.cat((label_map, label), 0)
feature_map = feature_map.numpy()
label_map = label_map.numpy()
file_name = '_feature_{}.hd5f'.format(model)
h5_path = os.path.join(outputPath, phase) + file_name
with h5py.File(h5_path, 'w') as h:
h.create_dataset('data', data=feature_map)
h.create_dataset('label', data=label_map)
将得到的特征保存到对应的h5py文件里面,注意训练集和验证集都需要提取特征,如果要做预测,那么测试集也需要提取特征,这样才能保证处理他们的方式是相同的。
定义h5数据集
将提取的特征保存成为h5文件之后,我们需要创建一个相对应的数据集,因为这个时候我们的输入不再是原始图片,而是这些图片对应的特征。
class h5Dataset(Dataset):
def __init__(self, h5py_list):
label_file = h5py.File(h5py_list[0], 'r')
self.label = torch.from_numpy(label_file['label'].value)
self.nSamples = self.label.size(0)
temp_dataset = torch.FloatTensor()
for file in h5py_list:
h5_file = h5py.File(file, 'r')
dataset = torch.from_numpy(h5_file['data'].value)
temp_dataset = torch.cat((temp_dataset, dataset), 1)
self.dataset = temp_dataset
def __len__(self):
return self.nSamples
def __getitem__(self, index):
assert index < len(self), 'index range error'
data = self.dataset[index]
label = self.label[index]
return (data, label)
最后我们通过训练这些特征得到我们优化的全连接层网络。
实验结果
method-1
我们可以看到这个网络刚刚训练完一次在训练集上就达到了92%的准确率,验证集上更高,到达了98%。
下面是网络训练10次之后的结果。
可以看到网络其实并没有收敛,这里我只是跑了10个epoch,如果想得到更好的结果,完全可以跑更多的epoch。
method-2
首先我们要导出三种网络的特征向量,每种网络都需要10到20分钟,如果不想自己跑的同学可以下载我放到百度网盘上的。
接着我们要训练的网络就只是一个特别简单的多层神经网络,跑20次运行的结果如下。
得到的结果非常好,能够达到99.68%的验证集准确率,而且通过导入的特征向量,训练所需要的时间被大大减少,每个epoch只需要2s就能够跑完,这个就好像我们把三个训练好的网络集成在了一起,有点集成学习的意味在里面。
结语
通过上面我们知道了如何用pytorch完成一个transfer learning,用已有的预训练网络来大大节省我们的运算资源,同时我们也完成了一个kaggle的比赛,怎么样,是不是跃跃欲试了,有点想去实际的比赛中完成我们的学习呢?那就赶紧去打比赛吧,可以学到很多平时看书上课学不到的实际经验。
本文的内容参考1,参考2
本文的所有代码都已上传到了github
欢迎查看我的知乎专栏深度炼丹
欢迎访问我的博客