前言
借鉴Francois Chollet 教授所著的《Deep Learning with Python》和Coursera平台隶属于DeepLearning.ai的系列课程《Tensorflow in practice》
数据集
数据集包含两个文件夹,训练数据和测试数据。训练数据包含25000张猫和狗的图片,猫和狗各12500张。每张图片的文件名标记了所属类别。测试数据包含12500张无标签的猫狗图片,文件名是数字id。我们的任务是预测测试数据集中的图片类别,将狗标记为1,猫标记为0。这是下载链接:https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/data
下载数据并解压后,我将训练数据分为了三部分:12500个样本的数据集,6250个样本的验证集和6250个样本的测试集。文件结构如下。train为kaggle提供的训练集,test为kaggle提供的测试集。
将图像复制到训练、验证和测试的目录。
import os
import shutil
import random
from shutil import copyfile
# 基路径
base_path = 'D:/Study/Machine Learning/dataset/dog vs cat'
# 数据源
data_source = os.path.join(base_path, 'train')
# 训练集路径
training_dir = os.path.join(base_path, 'training')
os.makedirs(training_dir)
# 验证集路径
validation_dir = os.path.join(base_path, 'validation')
os.makedirs(validation_dir)
# 测试集路径
testing_dir = os.path.join(base_path, 'testing')
os.makedirs(testing_dir)
# 猫的训练集路径
train_cat_dir = os.path.join(training_dir, 'cat')
os.makedirs(train_cat_dir)
# 狗的训练集路径
train_dog_dir = os.path.join(training_dir, 'dog')
os.makedirs(train_dog_dir)
# 猫的验证集路径
validate_cat_dir = os.path.join(validation_dir, 'cat')
os.makedirs(validate_cat_dir)
# 狗的验证路径
validate_dog_dir = os.path.join(validation_dir, 'dog')
os.makedirs(validate_dog_dir)
# 猫的测试集路径
test_cat_dir = os.path.join(testing_dir, 'cat')
os.makedirs(test_cat_dir)
# 狗的测试集路径
test_dog_dir = os.path.join(testing_dir, 'dog')
os.makedirs(test_dog_dir)
# 将原训练数据转移到我们创建的训练集文件夹中,按照文件名分好类
for file_name in os.listdir(data_source):
path = os.path.join(data_source, file_name)
if 'cat' in file_name:
copyfile(path, os.path.join(train_cat_dir, file_name))
else:
copyfile(path, os.path.join(train_dog_dir, file_name))
# 接下来从训练集中分离一部分数据到验证集和测试集
# 分割数据集的方法
def split_data(source, destination, split_size):
files = os.listdir(source)
trans_files = random.sample(files, int(len(files) * split_size))
for trans_file in trans_files:
shutil.move(os.path.join(source, trans_file), os.path.join(destination, trans_file))
split_data(train_cat_dir, validate_cat_dir, 0.5)
split_data(validate_cat_dir, test_cat_dir, 0.5)
split_data(train_dog_dir, validate_dog_dir, 0.5)
split_data(validate_dog_dir, test_dog_dir, 0.5)
构建网络
构建一个简单的两层卷积加一层全连接的神经网络。全连接层后接一个0.2的dropout。
import tensorflow as tf
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(128, (3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(1024, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(1, activation='sigmoid')
])
配置模型用于训练,使用RMSprop优化器。因为网络最后一层是单一sigmoid模型,所以使用二元交叉熵作为损失函数。
from tensorflow.keras.optimizers import RMSprop
model.compile(loss='binary_crossentropy',
optimizer=RMSprop(lr=0.0001),
metrics=['acc'])
定义回调,当验证精确度持续3个epoch没有上升时停止训练,并保存最佳模型
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
callbacks = [
EarlyStopping(monitor='val_acc', patience=3, verbose=2),
ModelCheckpoint('dog_vs_cat_1.h5', monitor='val_acc', save_best_only=True, verbose=0)
]
数据预处理
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# 训练集图片缩放到[0, 1]区间, 并进行图像增强
train_datagen = ImageDataGenerator(
rescale=1/255.0,
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest'
)
# 将验证集和测试集图片缩放到[0, 1]区间
validate_datagen = ImageDataGenerator(rescale=1/255.0)
test_datagen = ImageDataGenerator(rescale=1/255.0)
# 使用ImageGenerator 从目录中读取图像
train_generator = train_datagen.flow_from_directory(
training_dir,
target_size=(150, 150), # 将所有图像大小调整到 150 * 150
batch_size=20, # 采用较小的训练批次为了加快拟合
class_mode='binary'
)
validate_generator = validate_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary'
)
test_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary'
)
训练网络
利用批量生成器拟合模型
history = model.fit(
train_generator,
epochs=30,
validation_data=validate_generator,
validation_steps=100,
verbose=1,
callbacks=callbacks
)
最后保存了最优模型,验证集的预测正确率为0.823
评估模型
from tensorflow.keras.models import load_model
model = load_model('dog_vs_cat_1.h5')
loss, acc = model.evaluate(test_generator)
print(loss, acc)
>>>0.4749448895454407 0.7847999930381775
准确率为0.785, 并不令人满意。
通过进一步使用正则化方法以及调节网络参数(比如每个卷积层的过滤器个数或网络中的层数),我们可以得到更高的精度。但只靠从头开始训练自己的卷积神经网络,再想提高精度就十分困难,因为可用的数据太少。想要在这个问题上进一步提高精度,下一步需要使用与训练的模型。
使用预训练的神经网络
想要将深度学习应用于小型图像数据集,一种常用且高效的方法是使用预训练网络。预训练网络是一个保存好的网络,之前已在大型数据集(通常是大规模图像分类任务)上训练好。如果这个原始数据集足够大且足够通用,那么预训练网络学到的特征的空间层次结构可以有效地作为视觉世界的通用模型,因此这些特征可用于各种不同的计算机视觉问题,即使这些新问题涉及的类别和原始任务完全不同。例如,我们可以用在ImageNet上训练的一个网络(其类别主要是动物和日常用品), 应用于某个不相干的任务,比如在图像中识别家具。这种学到的特征在不同问题间的可移植性,是深度学习与许多早期浅层学习方法相比的重要优势,它使得深度学习对小数据问题非常有效。
使用预训练网络有两种方法: 特征提取和微调模型。
特征提取
特征提取是使用之前网络学到的表示来从新样本中提取出有趣的特征。然后将这些特征输入一个新的分类器,从头开始训练。
我们知道,用于图像分类的卷积神经网络包含两部分: 首先是一系列池化层和卷积层,最后是一个密集连接分类器。第一部分叫做模型的卷积基。对于卷积神经网络而言,特征提取就是取出之前训练好的网络的卷积基,在上面运行新数据,然后在输出上面训练一个新的分类器。
为什么仅重复使用卷积基?我们能否也重复使用密集连接分类器?一般来说,应该避免这么做。原因在于卷积基学到的表示可能更加通用,因此更适合重复使用。卷积神经网络的特征图表示通用概念在图像中是否存在,无论面对什么样的计算机视觉问题,这种特征图都可能很有用。但是,分类器学到的表示必然是针对于模型训练的类别,其中仅包含某个类别出现在整张图像中的概率信息。此外,密集连接层的表示不再包括物体在输入图像中的位置信息。密集连接层舍弃了空间的概念,而物体位置信息仍然有卷积特征图所描述。如果物体位置对于问题很重要,那么密集连接层的特征在很大程度上是无用的。
注意,某个卷积层提取的表示的通用性(以及可复用性)取决于该层在模型中的深度。模型中更靠近底部的层提取的是局部的、高度通用的特征图(比如视觉边缘、颜色和纹理),而更靠近顶部的层提取的是更加抽象的概念(比如“猫耳朵”和“狗眼睛”)。(底部的层是指在定义模型时先添加到模型中的层,而更靠近顶部的层是指后添加到模型中的层)因此,如果我们的新数据集和原始模型训练的数据集有很大差异,那么最好只是用模型的前几层来做特征提取,而不是使用整个卷积基。
本例中,由于ImageNet的类别中包含多种狗和猫的类别,所以重复使用原始模型密集连接层中所包含的信息可能很有用。但我们选择不这么做,以便涵盖新问题的类别与原始模型的类别不一致的更一般情况。
在本例中,我们使用在ImageNet上训练的InceptionV3网络的卷积基从猫狗图像中提取特征,然后再这些特征上训练一个猫狗分类器。
ImceptionV3模型内置于Keras中。我们可以从tensorflow,keras,applications模块中导入。
先将InceptionV3卷积基实例化。
pre_trained_model = InceptionV3(input_shape=(150, 150, 3),
include_top=False,
weights='imagenet')
这里向构造函数中传入了三个参数:
weights: 设为none时将随机初始化权重,设为imagenet时载入预训练好的权重。这里我们是需要预训练好的卷积基权重的。
include_top:指定模型最后是否包含密集连接分类器。因为我们打算使用自己的密集连接分类器(只有两个类别:cat和dog),所以不需要包含它。
input_shape: 是输入到网络中的图像张量的形状。这个参数是完全可选的,如果不传入这个参数,那么网络能够处理任意形状的输入。
在训练和编译模型之前,我们将卷积基冻结。冻结一个或多个层是指在训练过程中保持其权重不变。如果不这么做,那么卷积基之前学到的表示将会在训练过程中被修改。因为其上面添加的Dense层是随机初始化的,所以非常大的权重更新将会在网络中传播,对之前学到的表示造成很大的破坏。
from tensorflow.keras import layers
for layer in pre_trained_model.layers:
layer.trainable = False
这里《Tensorflow in practice》的课程只截取inceptionV3模型到’mixed7’层。本例也效仿这样做,推测理由其一是为了降低训练成本,其二最后几层抽取的特征过于抽象,不具备普适性。
last_layer = pre_trained_model.get_layer('mixed7')
last_output = last_layer.output
接着加入我们自己的密集连接层
last_layer = pre_trained_model.get_layer('mixed7')
last_output = last_layer.output
x = layers.Flatten()(last_output)
# Add a fully connected layer with 1,024 hidden units and ReLU activation
x = layers.Dense(512, activation='relu')(x)
# Add a dropout rate of 0.2
x = layers.Dropout(0.2)(x)
# Add a final sigmoid layer for classification
x = layers.Dense(1, activation='sigmoid')(x)
from tensorflow.keras import Model
model = Model( pre_trained_model.input, x)
接下来的模型配置和数据预处理都是和之前一样的,唯一需要改变的是回调函数。这里把最优模型存到另一个文件中。
callbacks = [
EarlyStopping(monitor='val_acc', patience=3, verbose=2),
ModelCheckpoint('dog_vs_cat_2.h5', monitor='val_acc', save_best_only=True, verbose=0)
]
拟合模型
利用批量生成器拟合模型
# history = model.fit(
train_generator,
epochs=30,
validation_data=validate_generator,
verbose=1,
callbacks=callbacks
)
最优模型验证集的正确率为0.9725。比之前高出很多。
还是在我们的测试集上评估一下模型。
model = load_model('dog_vs_cat_2.h5')
loss, acc = model.evaluate(test_generator)
print(loss, acc)
>>>0.09980710595846176 0.9724799990653992
比验证集正确率略低,但也还不错。
微调模型
模型微调是另一种广泛使用的模型复用方法,与特征提取互为补充。对于用于特征提取的冻结的模型基,微调是将其顶部的几层“解冻”,并将这解冻的几层和新增加的部分(本例中式全连接分类器)联合训练。之所以叫作微调,是因为它只是略微调整了所复用模型中更加抽象的表示,以便让这些表示与手头的问题更加相关。
前面说过,冻结卷积基是为了能够在上面训练一个随机初始化的分类器。同理,只有上面的分类器已经训练好了,才能微调卷积基的顶部几层。如果分类器没有训练好,那么训练期间通过网络传播的误差信号会特别大,微调的几层之前学到的表示都会被破坏。因此,微调网络的步骤如下。
1.在已经训练好的基网络上添加自定义网络。
2.冻结基网络。
3.训练所添加的部分。
4.解冻基网络的一些层。
5.联合训练解冻的这些层和所添加的部分。
我们已经完成了前三步,现在做第四步。这里我们只解冻inceptionV3的’mixed7’层。同时,由于先前模型已经收敛了,所以我们选取一个更小的学习率。
model = load_model('dog_vs_cat_2.h5')
set_trainable = False
# 解冻mixed7层
for layer in pre_trained_model.layers:
if layer.name == 'mixed7':
set_trainable = True
if set_trainable:
layer.trainable = True
else:
layer.trainable = False
# 选取较小的学习率
model.compile(optimizer = RMSprop(lr=1e-5),
loss='binary_crossentropy',
metrics=['acc'])
当前,仍旧要修改一下回调函数,将最优模型存到另一个文件。
callbacks = [
EarlyStopping(monitor='val_acc', patience=3, verbose=2),
ModelCheckpoint('dog_vs_cat_3.h5', monitor='val_acc', save_best_only=True, verbose=0)
]
经过训练,最优模型的验证集正确率为0.9744,比微调前略有提升。
还是在测试集上评估一下。
model = load_model('dog_vs_cat_3.h5')
loss, acc = model.evaluate(test_generator)
print(loss, acc)
>>>0.09062275290489197 0.974399983882904
正确率有很微小的提升。
我们用这个训练好的模型来预测kaggle提供的测试集,提交csv文件。
import numpy as np
import pandas as pd
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.inception_v3 import preprocess_input
model = load_model('dog_vs_cat_3.h5')
test_dir = os.path.join(base_path, 'test')
imgs = []
id = []
labels = []
for name in os.listdir(test_dir):
id.append(name.split('.')[0])
img_path = os.path.join(test_dir, name)
img = image.load_img(img_path, target_size=(150, 150))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
label = model.predict(x)
if label[0] > 0.5:
labels.append(0.995)
else:
labels.append(0.005)
imgs.append(x)
df = pd.DataFrame({'id': id, 'label': labels})
df = df.sort_values(axis=0, ascending=True, by='id')
df.to_csv(os.path.join(base_path, 'submission.csv'), index=None)
这里我们将预测值缩放到[0.005, 0.995],因为误差是这样计算的:
LogLoss
=
−
1
n
∑
i
=
1
n
[
y
i
log
(
y
^
i
)
+
(
1
−
y
i
)
log
(
1
−
y
^
i
)
]
\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]
LogLoss=−n1i=1∑n[yilog(y^i)+(1−yi)log(1−y^i)]
预测正确时,log(0.995)和log(1)的差别很小,但是当预测错误时,log(0)和log(0.005)的差别很大。因此这样的缩放可以有效地降低误差。