(本文所使用的 Python 库和版本号: Python 3.6, Numpy 1.14, scikit-learn 0.19, matplotlib 2.2, keras 2.1.6, Tensorflow 1.9.0)
很多文章和教材都是用 MNIST 数据集作为深度学习届的 "Hello World" 程序, 但是这个数据集有一个很大的特点: 它是一个典型的多分类问题(一共有 10 个分类), 在我们刚刚开始接触深度学习时, 我倒是觉得应该从最简单的二分类问题着手.
在深度学习框架方面, 目前比较流行的是 Tensorflow,Keras,PyTorch,Theano 等, 但是我建议新手入门, 可以从 Keras 入手, 然后进阶时转移到 Tensorflow 上, 实际上, Keras 的后端是可以支持 Tensorflow 和 Theano, 可以说, Keras 是在 Tensorflow 和 Theano 的基础上进一步封装, 更加的简单实用, 更容易入门, 通常几行简单的代码就可以解决一个小型的项目问题.
我这篇博文主要参考了: keras 系列︱图像多分类训练与利用 bottleneck features 进行微调(三), 这篇博文也是参考的 Building powerful image classification models using very little data, 但我发现这两篇博文有很多地方的代码跑不起来, 主要原因可能是 Keras 或 Tensorflow 升级造成的, 所以我做了一些必要的修改.
1. 准备数据集
最经典的二分类数据集就是 Kaggle 竞赛中的 "猫狗大战" 数据集(train set 有 25K 张图片, test set: 12.5K), 此处按照原始博文的做法, 我从 train_set 中选取 1000 张 Dog 的照片 + 1000 张 Cat 照片作为我们新的 train set, 选取 400 张 Dog+400 张 Cat 照片作为新的 test set. 所以 train 和 test 两个文件夹下都有两个子文件夹(cats 和 dogs 子文件夹). 当然, 选取是随机的, 也是用代码来实现的, 准备小数据集的代码如下:
- def dataset_prepare(raw_set_folder,dst_folder,train_num_per_class=1000,test_num_per_class=400):
- '''
- 准备小数据集, 从原始的 raw_set_folder 数据集中提取 train_num_per_class(每个类别)的照片放入 train 中,
- 提取 val_num_per_class(每个类别)放入到 validation 文件夹中
- :param raw_set_folder: 含有猫狗的照片, 这些照片的名称必须为 cat.101.jpg 或 dog.102.jpg 形式
- :param dst_folder: 将选取之后的图片放置到这个文件夹中
- :param train_num_per_class:
- :param test_num_per_class:
- :return:
- ''' all_imgs=glob(os.path.join(raw_set_folder,'*.jpg'))
- img_len = len(all_imgs)
- assert img_len> 0, '{} has no jpg image file'.format(raw_set_folder)
- cat_imgs=[]
- dog_imgs=[]
- for img_path in all_imgs:
- img_name=os.path.split(img_path)[1]
- if img_name.startswith('cat'):
- cat_imgs.append(img_path)
- elif img_name.startswith('dog'):
- dog_imgs.append(img_path)
- random.shuffle(cat_imgs)
- random.shuffle(dog_imgs)
- [ensure_folder_exists(os.path.join(dst_folder,type_folder,class_folder)) for type_folder in ['train','test']
- for class_folder in ['dogs','cats']]
- # 下面的代码可以进一步优化....
- for cat_img_path in cat_imgs[:train_num_per_class]: # 最开始的 N 个图片作为 train
- _, fname = os.path.split(cat_img_path) # 获取文件名和路径
- shutil.copyfile(cat_img_path, os.path.join(dst_folder, 'train', 'cats',fname))
- print('imgs saved to train/cats folder')
- for dog_img_path in dog_imgs[:train_num_per_class]:
- _, fname = os.path.split(dog_img_path) # 获取文件名和路径
- shutil.copyfile(dog_img_path, os.path.join(dst_folder, 'train', 'dogs',fname))
- print('imgs saved to train/dogs folder')
- for cat_img_path in cat_imgs[-test_num_per_class:]: # 最末的 M 个图片作为 test
- _, fname = os.path.split(cat_img_path) # 获取文件名和路径
- shutil.copyfile(cat_img_path, os.path.join(dst_folder, 'test', 'cats',fname))
- print('imgs saved to test/cats folder')
- for dog_img_path in dog_imgs[-test_num_per_class:]: # 最末的 M 个图片作为 test
- _, fname = os.path.split(dog_img_path) # 获取文件名和路径
- shutil.copyfile(dog_img_path, os.path.join(dst_folder, 'test', 'dogs',fname))
- print('imgs saved to test/dogs folder')
- print('finished...')
运行该函数即可完成小数据集的构建, 下面为 Keras 创建图片数据流, 为模型的构建做准备.
- # 2, 准备训练集, keras 有很多 Generator 可以直接处理图片的加载, 增强等操作, 封装的非常好
- from keras.preprocessing.image import ImageDataGenerator
- train_datagen = ImageDataGenerator( # 单张图片的处理方式, train 时一般都会进行图片增强
- rescale=1. / 255, # 图片像素值为 0-255, 此处都乘以 1/255, 调整到 0-1 之间
- shear_range=0.2, # 斜切
- zoom_range=0.2, # 放大缩小范围
- horizontal_flip=True) # 水平翻转
- train_generator = train_datagen.flow_from_directory(# 从文件夹中产生数据流
- train_data_dir, # 训练集图片的文件夹
- target_size=(IMG_W, IMG_H), # 调整后每张图片的大小
- batch_size=batch_size,
- class_mode='binary') # 此处是二分类问题, 故而 mode 是 binary
- # 3, 同样的方式准备测试集
- val_datagen = ImageDataGenerator(rescale=1. / 255) # 只需要和 trainset 同样的 scale 即可, 不需增强
- val_generator = val_datagen.flow_from_directory(
- val_data_dir,
- target_size=(IMG_W, IMG_H),
- batch_size=batch_size,
- class_mode='binary')
上面构建的 generator 就是 keras 需要的数据流, 该数据流使用 flow_from_directory 首先从图片文件夹 (比如 train_data_dir) 中加载图片到内存中, 然后使用 train_datagen 来对图片进行预处理和增强, 最终得到处理完成之后的 batch size 大小的数据流, 这个数据流会无限循环的产生, 直到达到一定的训练 epoch 数量为止.
上面用到了 ImageDataGenerator 来进行图片增强, 里面的参数说明为:(可以参考 Keras 的官方文档 https://keras.io/zh/backend/ )
rotation_range 是一个 0~180 的度数, 用来指定随机选择图片的角度.
width_shift 和 height_shift 用来指定水平和竖直方向随机移动的程度, 这是两个 0~1 之间的比例.
rescale 值将在执行其他处理前乘到整个图像上, 我们的图像在 RGB 通道都是 0~255 的整数, 这样的操作可能使图像的值过高或过低, 所以我们将这个值定为 0~1 之间的数.
shear_range 是用来进行剪切变换的程度
zoom_range 用来进行随机的放大
horizontal_flip 随机的对图片进行水平翻转, 这个参数适用于水平翻转不影响图片语义的时候
fill_mode 用来指定当需要进行像素填充, 如旋转, 水平和竖直位移时, 如何填充新出现的像素
2. 构建并训练 Keras 模型
由于 Keras 已经封装了很多 Tensorflow 的函数, 所以在使用上更加简单容易, 当然, 如果想调整里面的结构和参数等, 也比较麻烦一些, 所以对于高手, 想要调整模型的结构和自定义一些函数, 可以直接用 Tensorflow.
2.1 Keras 模型的构建
不管是 Keras 模型还是 Tensorflow 模型, 我个人认为其构建都包括两个部分: 模型的搭建和模型的配置, 所以可以从这两个方面来建立一个小型的模型. 代码如下:
- # 4, 建立 Keras 模型: 模型的建立主要包括模型的搭建, 模型的配置
- from keras.models import Sequential
- from keras.layers import Conv2D, MaxPooling2D
- from keras.layers import Activation, Dropout, Flatten, Dense
- from keras import optimizers
- def build_model(input_shape):
- # 模型的搭建: 此处构建三个 CNN 层 + 2 个全连接层的结构
- model = Sequential()
- model.add(Conv2D(32, (3, 3), input_shape=input_shape))
- model.add(Activation('relu'))
- model.add(MaxPooling2D(pool_size=(2, 2)))
- model.add(Conv2D(32, (3, 3)))
- model.add(Activation('relu'))
- model.add(MaxPooling2D(pool_size=(2, 2)))
- model.add(Conv2D(64, (3, 3)))
- model.add(Activation('relu'))
- model.add(MaxPooling2D(pool_size=(2, 2)))
- model.add(Flatten())
- model.add(Dense(64))
- model.add(Activation('relu'))
- model.add(Dropout(0.5)) # Dropout 防止过拟合
- model.add(Dense(1)) # 此处虽然是二分类, 但是不能用 Dense(2), 因为后面的 activation 是 sigmoid, 这个函数只能输出一个值, 即 class_0 的概率
- model.add(Activation('sigmoid')) #二分类问题用 sigmoid 作为 activation function
- # 模型的配置
- model.compile(loss='binary_crossentropy', # 定义模型的 loss func,optimizer,
- optimizer=optimizers.RMSprop(lr=0.0001),
- metrics=['accuracy'])# 主要优化 accuracy
- # 二分类问题的 loss function 使用 binary_crossentropy, 此处使用准确率作为优化目标
- return model # 返回构建好的模型
这个函数就搭建了模型的结构, 对模型进行了配置, 主要配置了 loss function, optimzer, 优化目标等, 当然可以做更多其他配置.
此处, 为了简单说明, 只是建立了三层卷积层 + 两层全连接层的小型网络结构, 当然, 对于一些比较简单的图像问题, 这个小型模型也能解决. 如果需要构建更为复杂的模型, 只需要自定义这个函数, 修改里面的模型构建和配置方法即可.
2.2 模型的训练
由于此处我们使用 generator 来产生数据流, 故而训练时要使用 fit_generator 函数. 代码如下:
- model=build_model(input_shape=(IMG_W,IMG_H,IMG_CH)) # 输入的图片维度
- # 模型的训练
- model.fit_generator(train_generator, # 数据流
- steps_per_epoch=train_samples_num // batch_size,
- epochs=epochs,
- validation_data=val_generator,
- validation_steps=val_samples_num // batch_size)
由于我在自己的笔记本上训练, 没有独立显卡, 更没有英伟达那么 NB 的显卡, 故而速度很慢, 但是的确能运行下去. 运行的具体结果可以去我的 GitHub https://github.com/RayDean/DeepLearning 上看.
------------------------------------- 输 --------- 出 --------------------------------
- Epoch 1/20 62/62 [==============================] - 136s 2s/step - loss: 0.6976 - acc: 0.5015 - val_loss: 0.6937 - val_acc: 0.5000 Epoch 2/20 62/62 [==============================] - 137s 2s/step - loss: 0.6926 - acc: 0.5131 - val_loss: 0.6846 - val_acc: 0.5813 Epoch 3/20 62/62 [==============================] - 152s 2s/step - loss: 0.6821 - acc: 0.5544 - val_loss: 0.6735 - val_acc: 0.6100
- ...
- Epoch 18/20 62/62 [==============================] - 140s 2s/step - loss: 0.5776 - acc: 0.6880 - val_loss: 0.5615 - val_acc: 0.7262 Epoch 19/20 62/62 [==============================] - 143s 2s/step - loss: 0.5766 - acc: 0.6971 - val_loss: 0.5852 - val_acc: 0.6800 Epoch 20/20 62/62 [==============================] - 140s 2s/step - loss: 0.5654 - acc: 0.7117 - val_loss: 0.5374 - val_acc: 0.7450
-------------------------------------------- 完 -------------------------------------
从训练后的 loss 和 acc 上可以大致看出, loss 在不断减小, acc 也不断增大, 趋势比较平稳.
此处我们可以将训练过程中的 loss 和 acc 绘图, 看看他们的变化趋势.
- # 画图, 将训练时的 acc 和 loss 都绘制到图上
- import matplotlib.pyplot as plt
- %matplotlib inline
- def plot_training(history):
- plt.figure(12)
- plt.subplot(121)
- train_acc = history.history['acc']
- val_acc = history.history['val_acc']
- epochs = range(len(train_acc))
- plt.plot(epochs, train_acc, 'b',label='train_acc')
- plt.plot(epochs, val_acc, 'r',label='test_acc')
- plt.title('Train and Test accuracy')
- plt.legend()
- plt.subplot(122)
- train_loss = history.history['loss']
- val_loss = history.history['val_loss']
- epochs = range(len(train_loss))
- plt.plot(epochs, train_loss, 'b',label='train_loss')
- plt.plot(epochs, val_loss, 'r',label='test_loss')
- plt.title('Train and Test loss')
- plt.legend()
- plt.show()
很明显, 由于 epoch 次数太少, acc 和 loss 都没有达到平台期, 后续可以增大 epoch 次数来达到一个比较好的结果. 在原始博文中, 作者在 50 个 epoch 之后达到了约 80% 左右的准确率, 此处我 20 个 epoch 后的准确率为 74%.
2.3 预测新样本
单张图片的预测
模型训练好之后, 就需要用来预测新的图片, 看看它能不能准确的给出结果. 预测函数为:
- # 用训练好的模型来预测新样本
- from PIL import Image
- from keras.preprocessing import image
- def predict(model, img_path, target_size):
- img=Image.open(img_path) # 加载图片
- if img.size != target_size:
- img = img.resize(target_size)
- x = image.img_to_array(img)
- x *=1./255 # 相当于 ImageDataGenerator(rescale=1. / 255)
- x = np.expand_dims(x, axis=0) # 调整图片维度
- preds = model.predict(x) # 预测
- return preds[0]
用这个函数可以预测单张图片:
- predict(model,'E:\PyProjects\DataSet\FireAI\DeepLearning/FireAI005/cat11.jpg',(IMG_W,IMG_H))
- predict(model,'E:\PyProjects\DataSet\FireAI\DeepLearning//FireAI005/dog4.jpg',(IMG_W,IMG_H))
------------------------------------- 输 --------- 出 --------------------------------
- array([0.14361556], dtype=float32)
- array([0.9942463], dtype=float32)
-------------------------------------------- 完 -------------------------------------
可以看出, 对于单张图片 cat11.jpg 得到的概率为 0.14, 而 dog4.jpg 的概率为 0.99, 可以看出第 0 个类别是 dog, 第 1 个类别是 cat, 模型能够很好的区分开来.
多张图片的预测
如果想用这个模型来预测一个文件夹中的所有图片, 那么该怎么办了?
- # 预测一个文件夹中的所有图片
- new_sample_gen=ImageDataGenerator(rescale=1. / 255)
- newsample_generator=new_sample_gen.flow_from_directory(
- 'E:\PyProjects\DataSet\FireAI\DeepLearning',
- target_size=(IMG_W, IMG_H),
- batch_size=16,
- class_mode=None,
- shuffle=False)
- predicted=model.predict_generator(newsample_generator)
- print(predicted)
------------------------------------- 输 --------- 出 --------------------------------
Found 4 images belonging to 2 classes. [[0.14361556] [0.5149474 ] [0.71455824] [0.9942463 ]]
-------------------------------------------- 完 -------------------------------------
上面的结果中第二个 0.5149 对应的应该是 cat, 应该小于 0.5, 这个预测是错误的, 不过粗略估计正确率有 3/4=75%.
2.4 模型的保存和加载
模型一般要及时保存到硬盘上, 防止数据丢失, 下面是保存的代码:
- # 模型保存
- # model.save_weights('E:\PyProjects\DataSet\FireAI\DeepLearning//FireAI005/FireAI005_Model.h5') # 这个只保存 weights, 不保存模型的结构
- model.save('E:\PyProjects\DataSet\FireAI\DeepLearning//FireAI005/FireAI005_Model2.h5') # 对于一个完整的模型, 应该要保存这个
- # 模型的加载, 预测
- from keras.models import load_model
- saved_model=load_model('E:\PyProjects\DataSet\FireAI\DeepLearning//FireAI005/FireAI005_Model2.h5')
- predicted=saved_model.predict_generator(newsample_generator)
- print(predicted) # saved_model 的结果和前面的 model 结果一致, 表面模型正确保存和加载
此处得到的结果和上面 model 预测的结果一模一样, 表明模型被正确保存和加载.
######################## 小 ********** 结 ###############################
1, 本篇文章讲解了: 准备一个简单的小数据集, 从数据集中建立数据流, 将该数据流引入到 Keras 的模型中进行训练, 并使用训练后的模型进行新图片的预测, 然后将模型进行保存, 加载保存好的模型到内存中.
2, 此处使用的模型是我们自己搭建的, 结构比较简单, 只有三层卷积层和两层全连接层, 故而模型的准确率不太高, 而且此处由于时间关系, 我只训练了 20 个 epoch, 训练并没有达到平台期.
#################################################################
注: 本部分代码已经全部上传到 (我的 GitHub https://github.com/RayDean/DeepLearning ) 上, 欢迎下载.
来源: https://juejin.im/post/5bea9e946fb9a049d235976a