提交 72322a23 编写于 作者: M muli

update fine-tuning

上级 fe8dbb81
# 迁移学习
# 微调
之前章节里我们通过大量样例演示了如何在只有6万张图片的FashionMNIST上训练模型。我们也介绍了ImageNet这个当下学术界使用最广的大数据集,它有超过一百万的图片和一千类的物体。但我们平常接触到数据集的规模通常在两者之间。
在前面的章节里我们展示了如何训练神经网络来识别小图片里的问题。我们也介绍了ImageNet这个学术界默认的数据集,它有超过一百万的图片和一千类的物体。这个数据集很大的改变计算机视觉这个领域,展示了很多事情虽然在小的数据集上做不到,但在数GB的大数据上是可能的。事实上,我们目前还不知道有什么技术可以在类似的但小图片数据集上,例如一万张图片,训练出一个同样强大的模型
想象一下开发一个应用来从图片中识别里面的凳子然后提供购买链接给用户。一个可能的做法是先去找一百把常见的凳子,对每个凳子收集一千张不同的图片,然后在收集到的数据上训练一个分类器。这个数据集虽然可能比FashionMNIST要复杂,但仍然比ImageNet小10倍。这可能导致针对ImageNet提出的模型在这个数据上会过拟合。同时因为数据量有限,最终我们得到的分类器的模型的精度也许达不到实用的要求
所以这是一个问题。尽管深度卷积神经网络在ImageNet上有了很惊讶的结果,但大部分人不关心Imagenet这个数据集本身。他们关心他们自己的问题。例如通过图片里面的人脸识别身份,或者识别图片里面的10种不同的珊瑚。通常大部分在非BAT类似大机构里的人在解决计算机视觉问题的时候,能获得的只是相对来说中等规模的数据。几百张图片很正常,找到几千张图片也有可能,但很难同Imagenet一样获得上百万张图片
一个解决办法是收集更多的数据。但注意到收集和标注数据均会花费大量的人力和财力。例如ImageNet这个数据集花费了数百万美元的研究经费。虽然目前的数据采集成本降低了十倍以上,但其成本仍然不可忽略
于是我们会有一个很自然的问题,如何使用在百万张图片上训练出来的强大的模型来帮助提升在小数据集上的精度呢?这种在源数据上训练,然后将学到的知识应用到目标数据集上的技术通常被叫做**迁移学习**。幸运的是,我们有一些有效的技术来解决这个问题
另外一种解决办法是迁移学习(transfer learning),它通过将其他数据集来帮助学习当前数据集。例如,虽然ImageNet的图片基本跟椅子无关,但其上训练到的模型可能能做一些通用的图片特征抽取,例如识别边缘、纹理、形状和物体组成。这个对于识别椅子也可能同样有效
对于深度神经网络来首,最为流行的一个方法叫做微调(fine-tuning)。它的想法很简单但有效
本小节我们介绍迁移学习里面的一个常用技术:微调(fine tuning)。它由下面四步构成
1. 在源数据(例如ImageNet)上训练一个神经网络$A$。
2. 创建一个新的神经网络$B$,它复制$A$上除了输出层外的所有模型参数。这里的假设是这些模型参数含有源数据上学习到的知识,这些知识同样适用于目标数据集。但最后的输出层跟源数据标注紧密相关,所以不被重用。
3. 为$B$添加一个输出大小为目标数据集类别数目(例如一百类椅子)的输出层,并将其权重初始化成随机值。
4. 在目标数据集(例如椅子数据集)上训练$B$。我们将从头开始学习输出层,但其余层都是基于源数据上的模型参数进行微调。
* 在源数据 $S$ 上训练一个神经网络。
* 砍掉它的头,将它的输出层改成适合目标数据 $S$ 的大小
* 将输出层的权重初始化成随机值,但其它层保持跟原先训练好的权重一致
* 然后开始在目标数据集开始训练
![微调。](../img/fine-tuning.svg)
下图图示了这个算法:
![](../img/fine-tuning.svg)
接下来我们来看一个具体的例子,它使用ImageNet上训练好的ResNet用来微调一个我们构造的小数据集:其含有数千张包含热狗和不包含热狗的图片。
## 热狗识别
这一章我们将通过[ResNet](../chapter_convolutional-neural-networks/resnet-gluon.md)来演示如何进行微调。因为通常不会每次从0开始在ImageNet上训练模型,我们直接从Gluon的模型园下载已经训练好的。然后将其迁移到一个我们感兴趣的问题上:识别热狗。
![hot dog](../img/comic-hot-dog.png)
热狗识别是一个二分类问题。我们这里使用的热狗数据集是从网上抓取的,它有$1400$张正类和同样多的负类,负类主要是食品相关图片。我们将各类的$1000$张作为训练集合,其余的作为测试集合。
### 获取数据
我们首先从网上下载数据并解压到`../data/hotdog`。每个文件夹下会有对应的`png`文件
我们使用的热狗数据集是从网上抓取的,它含有$1400$张包含热狗的正类图片,和同样多包含其他食品的负类图片。各类的$1000$张图片被用作训练,其余的作为测试
```{.python .input n=17}
from mxnet import gluon
我们首先将数据下载到`../data`。在当前目录解压后得到`hotdog/train``hotdog/test`这两个文件夹。每个下面有`hotdog``not-hotdog`这两个类别文件夹,里面是对应的图片文件。
```{.python .input n=4}
%matplotlib inline
import sys
sys.path.insert(0, '..')
import zipfile
import gluonbook as gb
from mxnet import nd, image, gluon, init
from mxnet.gluon.data.vision import transforms
data_dir = '../data'
data_dir = '../data/'
fname = gluon.utils.download(
'https://apache-mxnet.s3-accelerate.amazonaws.com/gluon/dataset/hotdog.zip',
path=data_dir, sha1_hash='fba480ffa8aa7e0febbb511d181409f899b9baa5')
......@@ -45,158 +45,110 @@ with zipfile.ZipFile(fname, 'r') as f:
f.extractall(data_dir)
```
我们使用[图片增强](../image-augmentation.md)里类似的方法来处理图片。
```{.python .input n=18}
from mxnet import nd
from mxnet import image
from mxnet import gluon
我们使用使用`ImageFolderDataset`类来读取数据。它将每个类别文件夹当做一个类,并读取下面所有的图片。
train_augs = [
image.HorizontalFlipAug(.5),
image.RandomCropAug((224,224))
]
test_augs = [
image.CenterCropAug((224,224))
]
def transform(data, label, augs):
data = data.astype('float32')
for aug in augs:
data = aug(data)
data = nd.transpose(data, (2,0,1))
return data, nd.array([label]).asscalar().astype('float32')
```{.python .input n=6}
train_imgs = gluon.data.vision.ImageFolderDataset(data_dir+'/hotdog/train')
test_imgs = gluon.data.vision.ImageFolderDataset(data_dir+'/hotdog/test')
```
读取文件夹下的图片,并且画出一些图片
下面画出前8张正例图片和最后的8张负例图片,可以看到他们性质和高宽各不相同。
```{.python .input n=20}
%matplotlib inline
import sys
sys.path.append('..')
import gluonbook as gb
train_imgs = gluon.data.vision.ImageFolderDataset(
data_dir+'/hotdog/train',
transform=lambda X, y: transform(X, y, train_augs))
test_imgs = gluon.data.vision.ImageFolderDataset(
data_dir+'/hotdog/test',
transform=lambda X, y: transform(X, y, test_augs))
data = gluon.data.DataLoader(train_imgs, 32, shuffle=True)
for X, _ in data:
X = X.transpose((0,2,3,1)).clip(0,255)/255
gb.show_images(X, 4, 8)
break
```{.python .input}
hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i-1][0] for i in range(8)]
gb.show_images(hotdogs+not_hotdogs, 2, 8, scale=1.4)
```
### 模型和训练
我们将训练图片首先扩大到高宽为480,然后随机剪裁出高宽为224的输入。测试图片则是简单的中心剪裁。此外,我们对输入的RGB通道数值进行了归一化。
```{.python .input n=3}
# 指定 RGB 三个通道的均值和方差来将图片通道归一化。
normalize = transforms.Normalize(
[0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
train_augs = transforms.Compose([
transforms.Resize(480),
transforms.RandomResizedCrop(224),
transforms.RandomFlipLeftRight(),
transforms.ToTensor(),
normalize,
])
test_augs = transforms.Compose([
transforms.CenterCrop(224),
transforms.ToTensor(),
normalize
])
```
这里我们将使用Gluon提供的ResNet18来训练。我们先从模型园里获取改良过ResNet。使用`pretrained=True`将会自动下载并加载从ImageNet数据集上训练而来的权重。
### 微调模型
```{.python .input n=21}
from mxnet.gluon.model_zoo import vision as models
我们用在ImageNet上训练好了ResNet 18来作为基础模型。这里指定`pretrained=True`来自动下载并加载训练好的权重。
pretrained_net = models.resnet18_v2(pretrained=True)
```{.python .input n=6}
pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True)
```
通常预训练好的模型由两块构成,一是`features`,二是`output`。后者主要包括最后一层全连接层,前者包含从输入开始的大部分层。这样的划分的一个主要目的是为了更方便做微调。我们先看下`output`的内容:
预训练好的模型由两块构成,一是`features`,二是`output`。前者包含从输入开始的大部分卷积和全连接层,后者主要包括最后一层全连接层。这样的划分的主要目的是为了更方便做微调。下面查看下`output`的内容:
```{.python .input n=22}
```{.python .input n=7}
pretrained_net.output
```
我们可以看一下第一个卷积层的部分权重。
```{.python .input n=23}
pretrained_net.features[1].weight.data()[0][0]
```
在微调里,我们一般新建一个网络,它的定义跟之前训练好的网络一样,除了最后的输出数等于当前数据的类别数。新网络的`features`被初始化前面训练好网络的权重,而`output`则是从头开始训练。
它将ResNet最后的全局平均池化层输出转化成1000类的输出。
```{.python .input n=24}
from mxnet import init
在微调中,我们新建一个网络,它的定义跟之前训练好的网络一样,除了最后的输出数等于当前数据的类别数。就是说新网络的`features`被初始化成前面训练好网络的权重,而`output`则是从头开始训练。
finetune_net = models.resnet18_v2(classes=2)
```{.python .input n=9}
finetune_net = gluon.model_zoo.vision.resnet18_v2(classes=2)
finetune_net.features = pretrained_net.features
finetune_net.output.initialize(init.Xavier())
```
我们先定义一个可以重复使用的训练函数。
## 训练
```{.python .input n=25}
我们先定义一个可以重复使用的训练函数。
def train(net, ctx, batch_size=64, epochs=10, learning_rate=0.01, wd=0.001):
train_data = gluon.data.DataLoader(train_imgs, batch_size, shuffle=True)
test_data = gluon.data.DataLoader(test_imgs, batch_size)
```{.python .input n=12}
def train(net, learning_rate, batch_size=128, epochs=5):
train_data = gluon.data.DataLoader(
train_imgs.transform_first(train_augs), batch_size, shuffle=True)
test_data = gluon.data.DataLoader(
test_imgs.transform_first(test_augs), batch_size)
# 确保net的初始化在ctx上
ctx = gb.try_all_gpus()
net.collect_params().reset_ctx(ctx)
net.hybridize()
loss = gluon.loss.SoftmaxCrossEntropyLoss()
# 训练
trainer = gluon.Trainer(net.collect_params(), 'sgd', {
'learning_rate': learning_rate, 'wd': wd})
'learning_rate': learning_rate, 'wd': 0.001})
gb.train(train_data, test_data, net, loss, trainer, ctx, epochs)
```
现在我们可以训练了。
```{.python .input n=10}
ctx = gb.try_all_gpus()
train(finetune_net, ctx)
```
对比起见我们尝试从随机初始值开始训练一个网络。
```{.python .input n=11}
scratch_net = models.resnet18_v2(classes=2)
scratch_net.initialize(init=init.Xavier())
train(scratch_net, ctx)
```
可以看到,微调版本收敛比从随机值开始的要快很多。
### 图片预测
```{.python .input n=12}
from matplotlib import pyplot as plt
def classify_hotdog(net, fname):
with open(fname, 'rb') as f:
img = image.imdecode(f.read())
data, _ = transform(img, -1, test_augs)
plt.imshow(data.transpose((1,2,0)).asnumpy()/255)
data = data.expand_dims(axis=0)
out = net(data.as_in_context(ctx[0]))
out = nd.SoftmaxActivation(out)
pred = int(nd.argmax(out, axis=1).asscalar())
prob = out[0][pred].asscalar()
label = train_imgs.synsets
return 'With prob=%f, %s'%(prob, label[pred])
```
接下来我们用训练好的模型来预测几张图片:
因为微调的网络中的主要层的已经训练的足够好,所以一般采用比较小的学习率,防止过大的步长对训练好的层产生过多影响。
```{.python .input n=13}
classify_hotdog(finetune_net, '../img/real_hotdog.jpg')
train(finetune_net, 0.01)
```
为了对比起见,我们训练同样的一个模型,但所有参数都初始成随机值。我们使用较大的学习率来加速收敛。
```{.python .input n=14}
classify_hotdog(finetune_net, '../img/leg_hotdog.jpg')
scratch_net = gluon.model_zoo.vision.resnet18_v2(classes=2)
scratch_net.initialize(init=init.Xavier())
train(scratch_net, 0.1)
```
```{.python .input n=15}
classify_hotdog(finetune_net, '../img/dog_hotdog.jpg')
```
可以看到,微调的模型因为初始值更好,它的收敛比从头开始训练要快很多。在很多情况下,微调的模型最终的收敛到的结果也可能比非微调的模型更好。
## 小结
* 我们看到,通过一个预先训练好的模型,我们可以在即使较小的数据集上训练得到很好的分类器。这是因为这两个任务里面的数据表示有很多共通性,例如都需要如何识别纹理、形状、边等等。而这些通常被在靠近数据的层有效的处理。因此,如果你有一个相对较小的数据在手,而且担心它可能不够训练出很好的模型,你可以寻找跟你数据类似的大数据集来先训练你的模型,然后再在你手上的数据集上微调
微调通过将模型部分权重初始化成在源数据集上预训练好的模型参数,从而将模型在源数据集上学到的知识迁移到目标数据上
## 练习
-`finetune_net`试着增大学习率看看收敛变化。
- 多跑几个`epochs`直到收敛(你可以也需要调调参数),看看`scratch_net``finetune_net`最后的精度是不是有区别
- 这里`finetune_net`重用了`pretrained_net`除最后全连接外的所有权重,试试少重用些权重,有会有什么区别
- 事实上`ImageNet`里也有`hotdog`这个类,它的index是713。例如它对应的weight可以这样拿到。试试如何重用这个权重
......@@ -207,8 +159,7 @@ hotdog_w = nd.split(weight.data(), 1000, axis=0)[713]
hotdog_w.shape
```
- 试试不让`finetune_net`里重用的权重参与训练,就是不更新权重
- 如果图片预测这一章里我们训练的模型没有分对所有的图片,如何改进?
- 试试不让`finetune_net`里重用的权重参与训练,也就是不更新他们的权重。
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/2272)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册