多类别动物图片分类任务(上)
在学习了大约2周的机器学习和深度学习的基础知识,并跑了十多个模型之后,老师给我布置了一项真正的实际任务:利用已经收集到的图片信息,构建并训练模型,一期目标使得精度达到84%,二期目标使得精度达到90%。
一开始并没有认识到,真正的实际数据会和demo中最后的结果差距如此之大,使得自己消沉了一段时间,不过经过将近15天的努力,总算是完成了任务,亦有所收获。
故,在此把我这段时间踩过的坑都一一标注出来,希望可以帮助到那些初次接触卷积神经网络的新人(虽然我也是啦)
- 观察数据
拿到数据后,要做的第一件事绝不是直接建模(血的教训),而是观察数据的分布情况,在真正的实际任务中,数据往往不是你在猫狗大战这样的demo中那样,分布的那么均衡的。
比如我这次的任务数据,总计30064张彩色图片,分为168类,而最少的类图片只有103张,最多的有1684张,差距非常大。
解决方法:
过采样是一个很好的处理手段。 - 建立一个简单的卷积神经网络
无论你是否懂得如何利用已经与训练好的模型进行建模,自己写一个简单的卷积神经网络来测试下数据都是可行的。一般,我们采用2+2的形式来实现。
实现代码:
# 建立模型
model = Sequential()
# 这里使用卷积神经网络,传入100*100像素的彩色图片,传出时为94*94*32
model.add(Conv2D(32, (7, 7), strides = (1, 1), name = 'conv0', input_shape = (100, 100, 3)))
# 使用批标准化
model.add(BatchNormalization(axis = 3, name = 'bn0'))
# 激活函数为ReLU(线性整流函数)
model.add(Activation('relu'))
# 对于空间数据的最大池化
model.add(MaxPooling2D((2, 2), name='max_pool'))
model.add(Conv2D(64, (3, 3), strides = (1, 1), name="conv1"))
model.add(Activation('relu'))
# 对于空间数据的平均池化
model.add(AveragePooling2D((3, 3), name='avg_pool'))
model.add(Flatten())
model.add(Dense(500, activation="relu", name='rl'))
model.add(Dropout(0.5))
model.add(Dense(y.shape[1], activation='softmax', name='sm'))
# 编译模型
model.compile(loss='categorical_crossentropy', optimizer="adam", metrics=['accuracy'])
当然,在此之前我们要先处理图片数据和标签
实现代码:
train_df = pd.read_csv("input/train.csv")
# 这个函数用来预处理图片数据,把图片转化为100*100像素
# 的彩色图片,其中data是数据集,m是数据集中图片的数目,dataset是图片存放的目录
def prepareImages(data, m, dataset):
print("Preparing images")
X_train = np.zeros((m, 100, 100, 3))
count = 0 # 这个count用来记录图片处理的数目,函数中实现了每处理500张就通报一声,其实没啥用
for fig in data['Image']:
# fig读到的是Image这一特征下的具体数据,也就是图片名(一个接一个的)
img = image.load_img("input/" + dataset + "/" + fig, target_size=(100, 100, 3))
x = image.img_to_array(img)
# preprocess_input()函数完成数据预处理的工作
x = preprocess_input(x)
# 然后把处理过后的图片信息记录到x_train中去
X_train[count] = x
if count % 500 == 0:
print("Processing image: ", count + 1, ", ", fig)
count += 1
return X_train
# 这个函数用来处理标签,返回两个输出,y和label_encoder,
# 前者是one-hot编码形式,如:[1;2;3]会编码成[1 0 0; 0 1 0; 0 0 1]。
# 后者是分组的组号,如:[1;2;2;4;2]会编码成[1;2;2;3;2]。
def prepare_labels(y):
values = np.array(y)
# 为标签分组,并用label_encoder记录,为以后测试集分好组之后重新从组号转变成对应的描述做准备
label_encoder = LabelEncoder()
integer_encoded = label_encoder.fit_transform(values)
# print(integer_encoded)
# 将分好组的标签转化为one-hot编码,并用y记录
onehot_encoder = OneHotEncoder(sparse=False)
integer_encoded = integer_encoded.reshape(len(integer_encoded), 1)
onehot_encoded = onehot_encoder.fit_transform(integer_encoded)
# print(onehot_encoded)
y = onehot_encoded
return y, label_encoder
# 处理训练数据,并归一化
X = prepareImages(train_df, train_df.shape[0], "train")
X /= 255
# 处理标签
y, label_encoder = prepare_labels(train_df['Id'])
值得注意的是,此时的图片数据分为两个文件夹,train和valication,其信息被写在两个.csv文件中。
而跑起来之后,我们会发现,最终的验证准确率非常低,仅仅有23.31%
- 确定部分参数
1).在之前观察原始数据的过程中,我发现问题不仅仅是图片的分布不均匀,不同的图片的分辨率也截然不同,最小的图片大约有250 * 400,最大的甚至达到4000 * 4250,这就使得将图片以怎样的大小传入网络显得尤为重要。
经过多次试验,最终在平衡了训练时间和验证集准确率之后,选定图片统一裁定到356*356。
2).而原始图片决定以7:3的比例,随机分配给train和valication。
3).batch_size的选择是一门艺术:
batch_size 太小,算法难以收敛。
随着 batch_size 增大,处理相同数据量的速度越快。
随着 batch_size 增大,达到相同精度所需要的 epoch 数量越来越多。
由于上述两种因素的矛盾,batch_size 增大到某个时候,达到时间上的最优。
由于最终收敛精度会陷入不同的局部极值,因此batch_size 增大到某些时候,达到最终收敛精度上的最优。
经过多次试验,最终选择batch_size为128。
此处,一般选择2^n的大小,据说是可以从某种程度上提高显卡对数据的处理速度。
一些确定的参数
class_num = 168
num_train_sample = 20967
num_validation_sample = 9097
img_width = 356
img_height = 356
epochs = 40
batch_size = 128
至此,第一阶段结束。
- 开始使用VGG16进行预训练模型
预训练网络(pretrained network)是一个保存好的网络,之前已在大型数据集(通常是大规模图像分类任务)上训练好。如果这个原始数据集足够大且足够通用,那么预训练网络学到的特征的空间层次结构可以有效地作为视觉世界的通用模型,因此这些特征可用于各种不同的计算机视觉问题,即使这些新问题涉及的类别和原始任务完全不同。
本次任务,我首先选用了VGG16来进行预训练。
实现代码
# 实例化一个VGG16卷积基
base_model = VGG16(weights='imagenet',
include_top=False,
input_shape=(img_width, img_height, 3))
# 冻结卷积基
base_model.trainable = False
# 自定义顶层网络
top_model = Sequential()
top_model.add(layers.Flatten(input_shape=base_model.output_shape[1:]))
top_model.add(layers.Dense(4096, activation='relu'))
top_model.add(layers.Dropout(0.5))
top_model.add(layers.Dense(1024, activation='relu'))
top_model.add(layers.Dropout(0.5))
top_model.add(layers.Dense(class_num, activation='softmax'))
# 实例化这个模型
model = Model(inputs=base_model.input,
outputs=top_model(base_model.output))
# 编译模型
model.compile(loss='categorical_crossentropy',
optimizer=optimizers.RMSprop(lr=2e-5),
metrics=['acc'])
训练模型后,验证集的准确率只有54.52%
- 一些简单的调整
很明显,如此低的准确率完全无法满足任务的要求,我们需要对模型进行一定的调整,首先,要对数据进行预处理,此处为归一化。
代码实现
# 可以对训练集进行数据增强处理
train_datagen = ImageDataGenerator(rescale=1./255)
# 测试集不许动,归一化完了之后不许动
validation_datagen = ImageDataGenerator(rescale=1./255)
训练模型后,验证集的准确率上涨到60.17%。
进一步,我们可以调整学习率lr,经过多次调试,最终我们将初试学习率调整为lr = 1e-5。
训练模型后,验证集的准确率上涨到63.66%。
可以的话,我们可以在回调函数中加入对学习率的调整。
代码实现
# 回调函数用来监控val_loss,一旦超过5次迭代没有下降,降低学习率
callback_list = [
keras.callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.1,
patience=5,
verbose=1,
)
]
训练模型后,验证集的准确率上涨到66.34%。
至此,我们将自己能做的一些简单的调整执行完毕,很显然,不仅最终的结果依旧不能满足任务的要求,而且由于VGG16网络的特点,需要训练大量的参数,不仅让整个网路臃肿,而且训练效率很低。
故而,我选择换一种预训练网络,这里,就让我们请出本次任务的最大功臣:ResNet50。
有关ResNet50的内容太多,我们下一期慢慢讲
关于深度学习中的batch_size
https://www.cnblogs.com/gengyi/p/9853664.html