Skip to content

Commit a2c5866

Browse files
authored
Create README.md
1 parent a6a16ac commit a2c5866

1 file changed

Lines changed: 303 additions & 0 deletions

File tree

README.md

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
# 猫狗大战
2+
3+
数据集来自 kaggle 上的一个竞赛:[Dogs vs. Cats](https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition),训练集有25000张,猫狗各占一半。测试集12500张,没有标定是猫还是狗。
4+
5+
```
6+
➜ 猫狗大战 ls train | head
7+
cat.0.jpg
8+
cat.1.jpg
9+
cat.10.jpg
10+
cat.100.jpg
11+
cat.1000.jpg
12+
cat.10000.jpg
13+
cat.10001.jpg
14+
cat.10002.jpg
15+
cat.10003.jpg
16+
cat.10004.jpg
17+
➜ 猫狗大战 ls test | head
18+
1.jpg
19+
10.jpg
20+
100.jpg
21+
1000.jpg
22+
10000.jpg
23+
10001.jpg
24+
10002.jpg
25+
10003.jpg
26+
10004.jpg
27+
10005.jpg
28+
```
29+
30+
下面是训练集的一部分例子:
31+
32+
![](https://raw.githubusercontent.com/ypwhs/resources/master/dataset.png)
33+
34+
# 数据预处理
35+
36+
由于我们的数据集的文件名是以`type.num.jpg`这样的方式命名的,比如`cat.0.jpg`,但是使用 Keras 的 ImageDataGenerator 需要将不同种类的图片分在不同的文件夹中,因此我们需要对数据集进行预处理。这里我们采取的思路是创建符号链接(symbol link),这样的好处是不用复制一遍图片,占用不必要的空间。
37+
38+
```py
39+
import os
40+
import shutil
41+
42+
train_filenames = os.listdir('train')
43+
train_cat = filter(lambda x:x[:3] == 'cat', train_filenames)
44+
train_dog = filter(lambda x:x[:3] == 'dog', train_filenames)
45+
46+
def rmrf_mkdir(dirname):
47+
if os.path.exists(dirname):
48+
shutil.rmtree(dirname)
49+
os.mkdir(dirname)
50+
51+
rmrf_mkdir('train2')
52+
os.mkdir('train2/cat')
53+
os.mkdir('train2/dog')
54+
55+
rmrf_mkdir('test2')
56+
os.symlink('../test/', 'test2/test')
57+
58+
for filename in train_cat:
59+
os.symlink('../../train/'+filename, 'train2/cat/'+filename)
60+
61+
for filename in train_dog:
62+
os.symlink('../../train/'+filename, 'train2/dog/'+filename)
63+
```
64+
65+
我们可以从下面看到文件夹的结构,train2里面有两个文件夹,分别是猫和狗,每个文件夹里是12500张图。
66+
67+
```
68+
├── test [12500 images]
69+
├── test.zip
70+
├── test2
71+
│   └── test -> ../test/
72+
├── train [25000 images]
73+
├── train.zip
74+
└── train2
75+
├── cat [12500 images]
76+
└── dog [12500 images]
77+
```
78+
79+
# 导出特征向量
80+
81+
对于这个题目来说,使用预训练的网络是最好不过的了,经过前期的测试,我们测试了 ResNet50 等不同的网络,但是排名都不高,现在看来只有一两百名的样子,所以我们需要提高我们的模型表现。那么一种有效的方法是综合各个不同的模型,从而得到不错的效果,兼听则明。如果是直接在一个巨大的网络后面加我们的全连接,那么训练10代就需要跑十次巨大的网络,而且我们的卷积层都是不可训练的,那么这个计算就是浪费的。所以我们可以将多个不同的网络输出的特征向量先保存下来,以便后续的训练,这样做的好处是我们一旦保存了特征向量,即使是在普通笔记本上也能轻松训练。
82+
83+
```py
84+
from keras.models import *
85+
from keras.layers import *
86+
from keras.applications import *
87+
from keras.preprocessing.image import *
88+
89+
import h5py
90+
91+
def write_gap(MODEL, image_size, lambda_func=None):
92+
width = image_size[0]
93+
height = image_size[1]
94+
input_tensor = Input((height, width, 3))
95+
x = input_tensor
96+
if lambda_func:
97+
x = Lambda(lambda_func)(x)
98+
99+
base_model = MODEL(input_tensor=x, weights='imagenet', include_top=False)
100+
model = Model(base_model.input, GlobalAveragePooling2D()(base_model.output))
101+
102+
gen = ImageDataGenerator()
103+
train_generator = gen.flow_from_directory("train2", image_size, shuffle=False,
104+
batch_size=16)
105+
test_generator = gen.flow_from_directory("test2", image_size, shuffle=False,
106+
batch_size=16, class_mode=None)
107+
108+
train = model.predict_generator(train_generator, train_generator.nb_sample)
109+
test = model.predict_generator(test_generator, test_generator.nb_sample)
110+
with h5py.File("gap_%s.h5"%MODEL.func_name) as h:
111+
h.create_dataset("train", data=train)
112+
h.create_dataset("test", data=test)
113+
h.create_dataset("label", data=train_generator.classes)
114+
115+
write_gap(ResNet50, (224, 224))
116+
write_gap(InceptionV3, (299, 299), inception_v3.preprocess_input)
117+
write_gap(Xception, (299, 299), xception.preprocess_input)
118+
119+
```
120+
121+
为了复用代码,我觉得写一个函数是非常有必要的,那么我们的函数就需要输入模型,输入图片的大小,以及[预处理函数](https://github.com/fchollet/keras/blob/master/keras/applications/inception_v3.py#L389-L393),因为 Xception 和 Inception V3 都需要将数据限定在 `(-1, 1)` 的范围内,然后我们利用 `GlobalAveragePooling2D` 将卷积层输出的每个激活图直接求平均值,不然输出的文件会非常大,且容易过拟合。然后我们定义了两个 generator,利用 `model.predict_generator` 函数来导出特征向量,最后我们选择了 ResNet50, Xception, Inception V3 这三个模型(如果有兴趣也可以导出 VGG 的特征向量)。每个模型导出的时间都挺长的,在 aws p2.xlarge 上大概需要用**十分钟到二十分钟**。 这三个模型都是在 [ImageNet](http://www.image-net.org/) 上面预训练过的,所以每一个模型都可以说是身经百战,通过这三个老司机导出的特征向量,可以高度概括一张图片有哪些内容。
122+
123+
最后导出的 h5 文件包括三个 numpy 数组:
124+
125+
* train (25000, 2048)
126+
* test (12500, 2048)
127+
* label (25000,)
128+
129+
参考资料:
130+
131+
* [ResNet](https://arxiv.org/abs/1512.03385) 15.12
132+
* [Inception v3](https://arxiv.org/abs/1512.00567) 15.12
133+
* [Xception](https://arxiv.org/abs/1610.02357) 16.10
134+
135+
# 载入特征向量
136+
137+
经过上面的代码以后,我们获得了三个特征向量文件,分别是:
138+
139+
* gap_ResNet50.h5
140+
* gap_InceptionV3.h5
141+
* gap_Xception.h5
142+
143+
我们需要载入这些特征向量,并且将它们合成一条特征向量,然后记得把 X 和 y 打乱,不然之后我们设置`validation_split`的时候会出问题。这里设置了 numpy 的随机数种子为2017,这样可以确保每个人跑这个代码,输出都能是一样的结果。
144+
145+
```py
146+
import h5py
147+
import numpy as np
148+
from sklearn.utils import shuffle
149+
np.random.seed(2017)
150+
151+
X_train = []
152+
X_test = []
153+
154+
for filename in ["gap_ResNet50.h5", "gap_Xception.h5", "gap_InceptionV3.h5"]:
155+
with h5py.File(filename, 'r') as h:
156+
X_train.append(np.array(h['train']))
157+
X_test.append(np.array(h['test']))
158+
y_train = np.array(h['label'])
159+
160+
X_train = np.concatenate(X_train, axis=1)
161+
X_test = np.concatenate(X_test, axis=1)
162+
163+
X_train, y_train = shuffle(X_train, y_train)
164+
```
165+
166+
# 构建模型
167+
168+
模型的构建很简单,直接 dropout 然后分类就好了。
169+
170+
```py
171+
from keras.models import *
172+
from keras.layers import *
173+
174+
np.random.seed(2017)
175+
176+
input_tensor = Input(X_train.shape[1:])
177+
x = Dropout(0.5)(input_tensor)
178+
x = Dense(1, activation='sigmoid')(x)
179+
model = Model(input_tensor, x)
180+
181+
model.compile(optimizer='adadelta',
182+
loss='binary_crossentropy',
183+
metrics=['accuracy'])
184+
```
185+
186+
我们还可以对模型进行可视化:
187+
188+
```dot
189+
digraph G{
190+
node [shape=record]
191+
a[label="ResNet50|{input:|output:}|{(224, 224, 3)|(2048)}"]
192+
b[label="InceptionV3|{input:|output:}|{(299, 299, 3)|(2048)}"]
193+
c[label="Xception|{input:|output:}|{(299, 299, 3)|(2048)}"]
194+
Merge[label="Merge|{input:|output:}|{(3, 2048)|(6144)}"]
195+
Dropout[label="Dropout|Rate:|0.5"]
196+
Output[label="Output|{input:|output:}|{(6144)|(1)}"]
197+
Image -> a -> Merge
198+
Image -> b -> Merge
199+
Image -> c -> Merge
200+
Merge -> Dropout -> Output
201+
}
202+
```
203+
204+
![](https://raw.githubusercontent.com/ypwhs/resources/master/model.png)
205+
206+
# 训练模型
207+
208+
模型构件好了以后,我们就可以进行训练了,这里我们设置验证集大小为 20% ,也就是说训练集是20000张图,验证集是5000张图。
209+
210+
```py
211+
model.fit(X_train, y_train, batch_size=128, nb_epoch=8, validation_split=0.2)
212+
```
213+
214+
```
215+
Train on 20000 samples, validate on 5000 samples
216+
Epoch 1/8
217+
20000/20000 [==============================] - 1s - loss: 0.1193 - acc: 0.9591 - val_loss: 0.0283 - val_acc: 0.9936
218+
Epoch 2/8
219+
20000/20000 [==============================] - 0s - loss: 0.0319 - acc: 0.9898 - val_loss: 0.0181 - val_acc: 0.9952
220+
Epoch 3/8
221+
20000/20000 [==============================] - 0s - loss: 0.0252 - acc: 0.9916 - val_loss: 0.0172 - val_acc: 0.9934
222+
Epoch 4/8
223+
20000/20000 [==============================] - 0s - loss: 0.0214 - acc: 0.9936 - val_loss: 0.0140 - val_acc: 0.9956
224+
Epoch 5/8
225+
20000/20000 [==============================] - 0s - loss: 0.0200 - acc: 0.9926 - val_loss: 0.0139 - val_acc: 0.9954
226+
Epoch 6/8
227+
20000/20000 [==============================] - 0s - loss: 0.0189 - acc: 0.9933 - val_loss: 0.0129 - val_acc: 0.9956
228+
Epoch 7/8
229+
20000/20000 [==============================] - 0s - loss: 0.0170 - acc: 0.9946 - val_loss: 0.0123 - val_acc: 0.9960
230+
Epoch 8/8
231+
20000/20000 [==============================] - 0s - loss: 0.0163 - acc: 0.9945 - val_loss: 0.0119 - val_acc: 0.9958
232+
Out[4]:
233+
234+
```
235+
236+
我们可以看到,训练的过程很快,十秒以内就能训练完,准确率也很高,在验证集上最高达到了99.6%的准确率,这相当于一千张图只错了4张,可以说比我还厉害。
237+
238+
# 预测测试集
239+
240+
模型训练好以后,我们就可以对测试集进行预测,然后提交到 kaggle 上看看最终成绩了。
241+
242+
```py
243+
y_pred = model.predict(X_test, verbose=1)
244+
y_pred = y_pred.clip(min=0.005, max=0.995)
245+
246+
import pandas as pd
247+
from keras.preprocessing.image import *
248+
249+
df = pd.read_csv("sample_submission.csv")
250+
251+
gen = ImageDataGenerator()
252+
test_generator = gen.flow_from_directory("test2", (224, 224), shuffle=False,
253+
batch_size=16, class_mode=None)
254+
255+
for i, fname in enumerate(test_generator.filenames):
256+
index = int(fname[fname.rfind('/')+1:fname.rfind('.')])
257+
df.set_value(index-1, 'label', y_pred[i])
258+
259+
df.to_csv('pred.csv', index=None)
260+
df.head(10)
261+
```
262+
263+
预测这里我们用到了一个小技巧,我们将每个预测值限制到了 [0.005, 0.995] 个区间内,这个原因很简单,kaggle 官方的评估标准是 [LogLoss](https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/details/evaluation),对于预测正确的样本,0.995 和 1 相差无几,但是对于预测错误的样本,0 和 0.005 的差距非常大,是 15 和 2 的差别。参考 [LogLoss 如何处理无穷大问题](https://www.kaggle.com/wiki/LogLoss),下面的表达式就是二分类问题的 LogLoss 定义。
264+
265+
$$\textrm{LogLoss} = - \frac{1}{n} \sum_{i=1}^n \left[ y_i \log(\hat{y}_i) + (1 - y_i) \log(1 - \hat{y}_i)\right]$$
266+
267+
![](https://raw.githubusercontent.com/ypwhs/resources/master/logloss.png)
268+
269+
还有一个值得一提的地方就是测试集的文件名不是按 1, 2, 3 这样排的,而是按下面的顺序排列的:
270+
271+
```
272+
['test/1.jpg',
273+
'test/10.jpg',
274+
'test/100.jpg',
275+
'test/1000.jpg',
276+
'test/10000.jpg',
277+
'test/10001.jpg',
278+
'test/10002.jpg',
279+
'test/10003.jpg',
280+
......
281+
```
282+
283+
因此我们需要对每个文件名进行处理,然后赋值到 df 里,最后导出为 csv 文件。
284+
285+
```
286+
id label
287+
0 1 0.995
288+
1 2 0.995
289+
2 3 0.995
290+
3 4 0.995
291+
4 5 0.005
292+
5 6 0.005
293+
6 7 0.005
294+
7 8 0.005
295+
8 9 0.005
296+
9 10 0.005
297+
```
298+
299+
# 总结
300+
301+
我们可以从上图中看到,模型对于前十个样本都给出了很肯定的预测,提交到 kaggle 以后,得分也是很棒,0.04141,在全球排名中可以排到20/1314。我们如果要继续优化模型表现,可以使用更棒的预训练模型来导出特征向量,或者对预训练模型进行微调(fine-tune),或者进行数据增强(data augmentation)等。
302+
303+
参考链接:[面向小数据集构建图像分类模型](http://keras-cn.readthedocs.io/en/latest/blog/image_classification_using_very_little_data/)

0 commit comments

Comments
 (0)