fine-tuning.md 8.5 KB
Newer Older
M
muli 已提交
1
# 微调
M
muli 已提交
2

A
Aston Zhang 已提交
3
之前介绍了如何在只有6万张图像的FashionMNIST上训练模型。我们也介绍了ImageNet这个当下学术界使用最广的大数据集,它有超过一百万的图像和一千类的物体。但我们平常接触到数据集的规模通常在两者之间。
M
muli 已提交
4

A
Aston Zhang 已提交
5
假如你想从图像中识别出各种凳子,然后推荐购买链接给用户。一个可能的做法是先找一百种常见的凳子,为每种凳子拍摄一千张不同角度的图像,然后在收集到的数据上训练一个分类器。这个数据集虽然可能比FashionMNIST要庞大,但仍然比ImageNet小10倍。这可能导致适用于ImageNet的复杂模型在这个数据上会过拟合。同时因为数据量有限,最终我们得到的模型的精度也可能达不到实用的要求。
M
muli 已提交
6

7
一个解决办法是收集更多的数据。但是收集和标注数据会花费大量的时间和资金。例如为了收集ImageNet这个数据集,花费了数百万美元的研究经费。虽然目前的数据采集成本降低了十倍以上,但其成本仍然不可忽略。
M
muli 已提交
8

A
Aston Zhang 已提交
9
另外一种解决办法是应用迁移学习(transfer learning),将从源数据集学到的知识迁移到目标数据集上。例如,虽然ImageNet的图像基本跟椅子无关,但在其上训练的模型可以抽取通用图像特征,然后用来帮助识别边缘、纹理、形状和物体组成。类似的,它对于识别椅子也可能同样有效。
M
muli 已提交
10

11
本小节我们介绍迁移学习中的一个常用技术:微调(fine tuning)。如图9.1所示,微调由下面四步构成:
M
muli 已提交
12

M
muli 已提交
13
1. 在源数据(例如ImageNet)上训练一个神经网络$A$。
14
2. 创建一个新的神经网络$B$,它复制了$A$上除了输出层外的所有模型参数。我们假设这些模型参数含有源数据上学习到的知识,且这些知识同样适用于目标数据集。但最后的输出层跟源数据标注紧密相关,所以不被重用。
M
muli 已提交
15 16
3. 为$B$添加一个输出大小为目标数据集类别数目(例如一百类椅子)的输出层,并将其权重初始化成随机值。
4. 在目标数据集(例如椅子数据集)上训练$B$。我们将从头开始学习输出层,但其余层都是基于源数据上的模型参数进行微调。
M
muli 已提交
17

M
Mu Li 已提交
18
![微调。](../img/finetune.svg)
M
muli 已提交
19

A
Aston Zhang 已提交
20

M
muli 已提交
21

M
muli 已提交
22
## 热狗识别
M
Mu Li 已提交
23

A
Aston Zhang 已提交
24
接下来我们来看一个具体的例子,它使用ImageNet上训练好的ResNet用来微调一个我们构造的小数据集:其含有数千张包含热狗和不包含热狗的图像。
A
Aston Zhang 已提交
25

M
Mu Li 已提交
26
### 获取数据
M
muli 已提交
27

A
Aston Zhang 已提交
28
我们使用的热狗数据集是从网上抓取的,它含有$1400$张包含热狗的正类图像,和同样多包含其他食品的负类图像。各类的$1000$张图像被用作训练,其余的作为测试。
M
Mu Li 已提交
29

30
我们首先将数据下载到`../data`。在下载目录将下载好的数据集进行解压后得到`hotdog/train``hotdog/test`这两个文件夹。在这两个文件夹下面均有`hotdog``not-hotdog`两个类别文件夹,每个类别文件夹里面是对应的图像文件。
M
muli 已提交
31 32

```{.python .input  n=4}
33 34 35
import sys
sys.path.insert(0, '..')

A
Aston Zhang 已提交
36
%matplotlib inline
M
muli 已提交
37
import zipfile
M
muli 已提交
38
import gluonbook as gb
A
Aston Zhang 已提交
39 40 41
from mxnet import gluon, init, nd
from mxnet.gluon import data as gdata, loss as gloss, model_zoo
from mxnet.gluon import utils as gutils
A
Aston Zhang 已提交
42
import os
M
Mu Li 已提交
43

A
Aston Zhang 已提交
44
data_dir = '../data'
M
update  
muli 已提交
45
base_url = 'https://apache-mxnet.s3-accelerate.amazonaws.com/'
A
Aston Zhang 已提交
46
fname = gutils.download(
A
Aston Zhang 已提交
47
    base_url + 'gluon/dataset/hotdog.zip',
M
muli 已提交
48
    path=data_dir, sha1_hash='fba480ffa8aa7e0febbb511d181409f899b9baa5')
M
Mu Li 已提交
49

A
Aston Zhang 已提交
50 51
with zipfile.ZipFile(fname, 'r') as z:
    z.extractall(data_dir)
M
Mu Li 已提交
52 53
```

究其根本's avatar
究其根本 已提交
54
我们使用`ImageFolderDataset`类来读取数据。它将每个文件夹当做一个类,并读取下面所有的图像。
M
Mu Li 已提交
55

M
muli 已提交
56
```{.python .input  n=6}
A
Aston Zhang 已提交
57 58 59 60
train_imgs = gdata.vision.ImageFolderDataset(
    os.path.join(data_dir, 'hotdog/train'))
test_imgs = gdata.vision.ImageFolderDataset(
    os.path.join(data_dir, 'hotdog/test'))
M
Mu Li 已提交
61 62
```

A
Aston Zhang 已提交
63
下面画出前8张正例图像和最后的8张负例图像,可以看到它们的大小和长宽比各不相同。
M
Mu Li 已提交
64

M
muli 已提交
65 66
```{.python .input}
hotdogs = [train_imgs[i][0] for i in range(8)]
67
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
A
Aston Zhang 已提交
68
gb.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4);
M
muli 已提交
69 70
```

A
Aston Zhang 已提交
71
在训练时,我们先从图像中剪裁出随机大小,随机长宽比的一块,然后将它们统一缩放为长宽都是224的输入。测试时,则使用简单的中心剪裁。此外,我们对输入的RGB通道数值进行了归一化。
M
muli 已提交
72 73

```{.python .input  n=3}
A
Aston Zhang 已提交
74
# 指定 RGB 三个通道的均值和方差来将图像通道归一化。
A
Aston Zhang 已提交
75
normalize = gdata.vision.transforms.Normalize(
M
muli 已提交
76 77
    [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])

A
Aston Zhang 已提交
78 79 80 81
train_augs = gdata.vision.transforms.Compose([
    gdata.vision.transforms.RandomResizedCrop(224),
    gdata.vision.transforms.RandomFlipLeftRight(),
    gdata.vision.transforms.ToTensor(),
M
muli 已提交
82 83 84
    normalize,
])

A
Aston Zhang 已提交
85 86 87 88
test_augs = gdata.vision.transforms.Compose([
    gdata.vision.transforms.Resize(256),
    gdata.vision.transforms.CenterCrop(224),
    gdata.vision.transforms.ToTensor(),
M
muli 已提交
89 90 91
    normalize
])
```
M
Mu Li 已提交
92

M
muli 已提交
93
### 微调模型
M
Mu Li 已提交
94

95
我们用在ImageNet上预先训练的ResNet-18作为基础模型。这里指定`pretrained=True`来自动下载并加载预先训练的权重。
M
Mu Li 已提交
96

M
muli 已提交
97
```{.python .input  n=6}
A
Aston Zhang 已提交
98
pretrained_net = model_zoo.vision.resnet18_v2(pretrained=True)
M
Mu Li 已提交
99 100
```

101
预训练好的模型由两部分构成:`features``output`。前者包含从输入开始的所有卷积和全连接层,后者主要包括最后一层全连接层。这样划分的主要目的是为了更方便做微调。我们来看一下`output`的内容:
M
muli 已提交
102

M
muli 已提交
103
```{.python .input  n=7}
M
muli 已提交
104
pretrained_net.output
M
muli 已提交
105 106
```

M
muli 已提交
107
它将ResNet最后的全局平均池化层输出转化成1000类的输出。
M
Mu Li 已提交
108

M
muli 已提交
109
在微调中,我们新建一个网络,它的定义跟之前训练好的网络一样,除了最后的输出数等于当前数据的类别数。就是说新网络的`features`被初始化成前面训练好网络的权重,而`output`则是从头开始训练。
M
Mu Li 已提交
110

M
muli 已提交
111
```{.python .input  n=9}
A
Aston Zhang 已提交
112
finetune_net = model_zoo.vision.resnet18_v2(classes=2)
A
Aston Zhang 已提交
113
finetune_net.features = pretrained_net.features
M
muli 已提交
114
finetune_net.output.initialize(init.Xavier())
M
Mu Li 已提交
115 116
```

M
muli 已提交
117
## 训练
M
Mu Li 已提交
118

M
muli 已提交
119
我们先定义一个可以重复使用的训练函数。
M
Mu Li 已提交
120

M
muli 已提交
121
```{.python .input  n=12}
A
Aston Zhang 已提交
122
def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5):
A
Aston Zhang 已提交
123
    train_iter = gdata.DataLoader(
M
muli 已提交
124
        train_imgs.transform_first(train_augs), batch_size, shuffle=True)
A
Aston Zhang 已提交
125
    test_iter = gdata.DataLoader(
M
muli 已提交
126
        test_imgs.transform_first(test_augs), batch_size)
M
muli 已提交
127

M
muli 已提交
128
    ctx = gb.try_all_gpus()
M
Mu Li 已提交
129 130
    net.collect_params().reset_ctx(ctx)
    net.hybridize()
A
Aston Zhang 已提交
131
    loss = gloss.SoftmaxCrossEntropyLoss()
M
Mu Li 已提交
132
    trainer = gluon.Trainer(net.collect_params(), 'sgd', {
M
muli 已提交
133
        'learning_rate': learning_rate, 'wd': 0.001})
A
Aston Zhang 已提交
134
    gb.train(train_iter, test_iter, net, loss, trainer, ctx, num_epochs)
M
Mu Li 已提交
135 136
```

M
muli 已提交
137
因为微调的网络中的主要层的已经训练的足够好,所以一般采用比较小的学习率,防止过大的步长对训练好的层产生过多影响。
M
Mu Li 已提交
138

M
muli 已提交
139
```{.python .input  n=13}
A
Aston Zhang 已提交
140
train_fine_tuning(finetune_net, 0.01)
M
Mu Li 已提交
141 142
```

143
作为对比,我们训练一个同样的模型,但将所有参数都初始化为随机值。我们使用较大的学习率来加速收敛。
M
muli 已提交
144

M
muli 已提交
145
```{.python .input  n=14}
A
Aston Zhang 已提交
146
scratch_net = model_zoo.vision.resnet18_v2(classes=2)
M
muli 已提交
147
scratch_net.initialize(init=init.Xavier())
A
Aston Zhang 已提交
148
train_fine_tuning(scratch_net, 0.1)
M
Mu Li 已提交
149 150
```

151 152
可以看到,微调的模型因为初始值更好,在相同迭代周期下能够取得更好的结果。在很多情况下,微调的模型最终也会比非微调的模型取得更好的结果。

M
Mu Li 已提交
153

A
Aston Zhang 已提交
154
## 小结
M
muli 已提交
155

156
* 微调通过将模型部分权重初始化成在源数据集上预训练的模型权重,从而将模型在源数据集上学到的知识迁移到目标数据上。
M
Mu Li 已提交
157 158 159

## 练习

160
- 试着增大`finetune_net`的学习率看看收敛变化。
A
Aston Zhang 已提交
161
- 多跑几个`num_epochs`直到收敛(其他参数可能也需要微调),看看`scratch_net``finetune_net`最后的精度是不是有区别
M
Mu Li 已提交
162
- 这里`finetune_net`重用了`pretrained_net`除最后全连接外的所有权重,试试少重用些权重,有会有什么区别
163
- 事实上`ImageNet`里也有`hotdog`这个类,它对应的输出层参数可以用如下代码拿到。试试如何使用它。
M
muli 已提交
164

M
muli 已提交
165
```{.python .input  n=16}
M
muli 已提交
166
weight = pretrained_net.output.weight
M
muli 已提交
167 168 169 170
hotdog_w = nd.split(weight.data(), 1000, axis=0)[713]
hotdog_w.shape
```

M
muli 已提交
171
- 试试不让`finetune_net`里重用的权重参与训练,也就是不更新他们的权重。
S
Sheng Zha 已提交
172

A
Aston Zhang 已提交
173
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/2272)
S
Sheng Zha 已提交
174

A
Aston Zhang 已提交
175
![](../img/qr_fine-tuning.svg)