ResNet(Residual Neural Network)由微软研究院的Kaiming He等四名华人提出,通过使用ResNet Unit成功训练出了152层的神经网络,并在ILSVRC2015比赛中取得冠军,同时参数量比VGGNet低,效果非常突出。ResNet的结构可以极快的加速神经网络的训练,模型的准确率也有比较大的提升。
ResNet只是在CNN上面增加了shortcut,所以,ResNet和CNN是很相似的。下表为不同深度的ResNet。
1. ResNet简明原理——深度残差网络
残差神经单元:假定某段神经网络的输入是x,期望输出是H(x),如果我们直接将输入x传到输出作为初始结果,那么我们需要学习的目标就是F(x) = H(x) - x,这就是一个残差神经单元,相当于将学习目标改变了,不再是学习一个完整的输出H(x),只是输出和输入的差别 H(x) - x ,即残差。
下面将要实现的是resnet-50。下面是网络模型的整体模型图。其中的CONV表示卷积层,Batch Norm表示Batch 归一化层,ID BLOCK表示Identity块,由多个层构成,具体见第二个图。Conv BLOCK表示卷积块,由多个层构成。为了使得model个结构更加清晰,才提取出了conv block 和id block两个‘块’,分别把它们封装成函数。
2. Keras ResNet50网络模型
2.1. Identity块
identity块是ResNet中的标准模块
如果不了解batch norm,可以暂时滤过这部分的内容,可以把它看作是一个特殊的层,它不会改变数据的维度。这将不影响对resnet实现的理解。
def identity_block(X, f, filters, stage, block):
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch'
F1, F2, F3 = filters
X_shortcut = X
X = Conv2D(filters = F1, kernel_size = (1,1), strides = (1,1), padding = 'valid', name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X)
X = Activation('relu')(X)
X = Conv2D(filters = F2, kernel_size = (f,f), strides = (1,1), padding = 'same', name = conv_name_base + '2b', kernel_initializer = glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2b')(X)
X = Activation('relu')(X)
X = Conv2D(filters = F3, kernel_size = (1,1), strides = (1,1), padding = 'valid', name = conv_name_base + '2c', kernel_initializer = glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2c')(X)
X = Add()([X, X_shortcut])
X = Activation('relu')(X)
return X
2.2. convolutional块
当输入输出的维度不同时,就需要使用convolutional块来解决这个问题,它与identity块最大的不同是在shortcut回路增加了CONV2D操作
def convolutional_block(X, f, filters, stage, block, s=2):
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch'
F1, F2, F3 = filters
X_shortcut = X
X = Conv2D(filters = F1, kernel_size = (1,1), strides = (s,s), name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X)
X = Activation('relu')(X)
X = Conv2D(filters = F2, kernel_size = (f,f), strides = (1,1), padding = 'same', name = conv_name_base + '2b', kernel_initializer = glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2b')(X)
X = Activation('relu')(X)
X = Conv2D(filters = F3, kernel_size = (1,1), strides = (1,1), name = conv_name_base + '2c', kernel_initializer = glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis = 3, name = bn_name_base + '2c')(X)
X_shortcut = Conv2D(F3, (1,1), strides = (s,s), name = conv_name_base + '1', kernel_initializer = glorot_uniform(seed=0))(X_shortcut)
X_shortcut = BatchNormalization(axis = 3, name=bn_name_base + '1')(X_shortcut)
X = Add()([X, X_shortcut])
X = Activation('relu')(X)
return X
2.3. ResNet50
通过两个模块,组建ResNet50网络模型。ResNet大致包括了5个Stage,或者叫做5种参数不同的卷积阶段,如下图所示。
- Zero-padding pads the input with a pad of (3,3)
- Stage 1:
- The 2D Convolution has 64 filters of shape (7,7) and uses a stride of (2,2). Its name is “conv1”.
- BatchNorm is applied to the channels axis of the input.
- MaxPooling uses a (3,3) window and a (2,2) stride.
- Stage 2:
- The convolutional block uses three set of filters of size [64,64,256], “f” is 3, “s” is 1 and the block is “a”.
- The 2 identity blocks use three set of filters of size [64,64,256], “f” is 3 and the blocks are “b” and “c”.
- Stage 3:
- The convolutional block uses three set of filters of size [128,128,512], “f” is 3, “s” is 2 and the block is “a”.
- The 3 identity blocks use three set of filters of size [128,128,512], “f” is 3 and the blocks are “b”, “c” and “d”.
- Stage 4:
- The convolutional block uses three set of filters of size [256, 256, 1024], “f” is 3, “s” is 2 and the block is “a”.
- The 5 identity blocks use three set of filters of size [256, 256, 1024], “f” is 3 and the blocks are “b”, “c”, “d”, “e” and “f”.
- Stage 5:
- The convolutional block uses three set of filters of size [512, 512, 2048], “f” is 3, “s” is 2 and the block is “a”.
- The 2 identity blocks use three set of filters of size [256, 256, 2048], “f” is 3 and the blocks are “b” and “c”.
- The 2D Average Pooling uses a window of shape (2,2) and its name is “avg_pool”.
def ResNet50(input_shape = (64, 64, 3), classes = 6):
X_input = Input(input_shape)
X = ZeroPadding2D((3, 3))(X_input)
X = Conv2D(64, (7, 7), strides = (2,2), name = 'conv1', kernel_initializer = glorot_uniform(seed=0))(X)
X = BatchNormalization(axis = 3, name = 'bn_conv1')(X)
X = Activation('relu')(X)
X = MaxPooling2D((3, 3), strides = (2,2))(X)
X = convolutional_block(X, f = 3, filters = [64,64,256], stage = 2, block = 'a', s = 1)
X = identity_block(X, 3, [64,64,256], stage=2, block='b')
X = identity_block(X, 3, [64,64,256], stage=2, block='c')
X = convolutional_block(X, f = 3, filters = [128,128,512], stage = 3, block = 'a', s = 2)
X = identity_block(X, 3, [128,128,512], stage=3, block='b')
X = identity_block(X, 3, [128,128,512], stage=3, block='c')
X = identity_block(X, 3, [128,128,512], stage=3, block='d')
X = convolutional_block(X, f = 3, filters = [256,256,1024], stage = 4, block = 'a', s = 2)
X = identity_block(X, 3, [256,256,1024], stage=4, block='b')
X = identity_block(X, 3, [256,256,1024], stage=4, block='c')
X = identity_block(X, 3, [256,256,1024], stage=4, block='d')
X = identity_block(X, 3, [256,256,1024], stage=4, block='e')
X = identity_block(X, 3, [256,256,1024], stage=4, block='f')
X = convolutional_block(X, f = 3, filters = [512,512,2048], stage = 5, block = 'a', s = 2)
X = identity_block(X, 3, [512,512,2048], stage=5, block='b')
X = identity_block(X, 3, [512,512,2048], stage=5, block='c')
X = AveragePooling2D((2, 2), name='avg_pool')(X)
X = Flatten()(X)
X = Dense(classes, activation = 'softmax', name = 'fc' + str(classes), kernel_initializer = glorot_uniform(seed=0))(X)
model = Model(inputs = X_input, outputs = X, name = 'ResNet50')
return model
3. Keras ResNet50模型的使用
3.1. 应用环境
通过“pip install keras”等安装必备环境内容略过,详见参考内容。初始化导入代码如下:
import numpy as np
from keras import layers
from keras.layers import Input, Add, Dense, Activation, ZeroPadding2D, BatchNormalization, Flatten, Conv2D, AveragePooling2D, MaxPooling2D, GlobalMaxPooling2D
from keras.models import Model, load_model
from keras.preprocessing import image
from keras.utils import layer_utils
from keras.utils.data_utils import get_file
from keras.applications.imagenet_utils import preprocess_input
#import pydot
from IPython.display import SVG
from keras.utils.vis_utils import model_to_dot
from keras.utils import plot_model
from nets.resnet_utils import *
from keras.initializers import glorot_uniform
import scipy.misc
import matplotlib.pyplot as plt
from matplotlib.pyplot import imshow
from datasource import load_datas
from datasource import generator_data
from datasource import load_test_datas
import keras.backend as K
K.set_image_data_format('channels_last') #通道在后是Tensorflow的数据格式
K.set_learning_phase(1)
其中,from datasource是自定义训练数据源,在此略过。而from nets.resnet_utils import * 给我带来麻烦”ModuleNotFoundError: No module named ‘resnets_utils’“,涉及到egg解决方案,关键内容如下:
python setup.py build
python setup.py install
D:\06Study\models-master\research\slim>python setup.py build
running build
running build_py
creating build
error: could not create ‘build’: 当文件已存在时,无法创建该文件。
原因是git clone下来的代码库中有个BUILD文件,而build和install指令需要新建build文件夹,名字冲突导致问题。暂时不清楚BUILD文件的作用。将该文件移动到其他目录,再运行上述指令,即可成功安装。
D:\06Study\models-master\research\slim>python setup.py build
最后提示:拷贝egg文件到Python系统目录中。
Processing slim-0.1-py3.6.egg
Removing d:\python\python36\lib\site-packages\slim-0.1-py3.6.egg
Copying slim-0.1-py3.6.egg to d:\python\python36\lib\site-packages
Adding slim 0.1 to easy-install.pth file
Installed d:\python\python36\lib\site-packages\slim-0.1-py3.6.egg
Processing dependencies for slim==0.1
Finished processing dependencies for slim==0.1
需要在编译器的外部资源中,引用此egg文件。本文实践使用Eclipse PyDev,并在Windows 10和Cent OS7上测试通过。
添加环境变量PYTHONPATH,值为slim目录:然而该方法只能用于linux系统,Windows下不适用,即使添加环境变量也没起到效果。
3.2. 模型的使用
X_train, Y_train, X_test, Y_test, classes = load_datas()
'''
print ("number of training examples = " + str(X_train.shape[0]))
print ("number of test examples = " + str(X_test.shape[0]))
print ("X_train shape: " + str(X_train.shape))
print ("Y_train shape: " + str(Y_train.shape))
print ("X_test shape: " + str(X_test.shape))
print ("Y_test shape: " + str(Y_test.shape))
'''
model = ResNet50(input_shape = (120, 160, 3), classes = 5)
model.compile(optimizer='adam', loss = 'categorical_crossentropy', metrics=['accuracy'])
#model.fit(X_train, Y_train, epochs = 2, batch_size = 32)
model.fit_generator(generator_data(X_train,Y_train,50),
steps_per_epoch=700,#这里700代表训练集的总数,700*50=35000个样本,设置为比总数小的都可以
epochs=10
)
model.save('my_model.h5')
#model = load_model('my_model.h5')
test_x, test_y = load_test_datas(X_test, Y_test)
preds = model.evaluate(test_x, test_y)
print('Loss = ' + str(preds[0]))
print('Test Accuracy =' + str(preds[1]))
if __name__ == '__main__':
pass
其中,注释掉部分代码是把整体数据集加载到内存,但是,当数据量大的时候,GPU内存将不够,出现OOM(out of memory)问题:
If you want to see a list of allocated tensors when OOM happens, add report_tensor_allocations_upon_oom to RunOptions for current allocation info.
因此,将采用发生器generator解决方案,后面将展开。
3.3. 训练模型过程中所遇到的问题
3.3.1. 图片格式设置问题
keras代码中”K.set_image_data_format(‘channels_last’)“,用来设置图片宽和高及通道。
ValueError: Negative dimension size caused by subtracting 2 from 1 for ‘avg_pool/AvgPool’ (op: 'AvgP
在表示一组彩色图片的问题上,Theano和Caffe使用(样本数,通道数,行或称为高,列或称为宽)通道在前的方式,称为channels_first;而TensorFlow使用(样本数,行或称为高,列或称为宽,通道数)通道在后的方式,称为channels_last。
使用代码中:model = ResNet50(input_shape = (120, 160, 3), classes = 5),表示宽160,高120的彩色图片为输入图片。
3.3.2 当验证集的loss不再下降时,如何中断训练?
可以定义EarlyStopping来提前终止训练(注:网上摘抄,还未实践)
from keras.callbacks import EarlyStopping
early_stopping = EarlyStopping(monitor=‘val_loss’, patience=2)
model.fit(X, y, validation_split=0.2, callbacks=[early_stopping])
3.3.3. 如何在每个epoch后记录训练/测试的loss和正确率?
model.fit在运行结束后返回一个History对象,其中含有的history属性包含了训练过程中损失函数的值以及其他度量指标。
hist = model.fit(X, y, validation_split=0.2)
print(hist.history)
3.3.4. 数据量大,内存不足问题解决方案
利用Python的生成器,逐个生成数据的batch并进行训练。
3.3.4.1. 自己定义生成器
generator:生成器函数,生成器的输出应该为:
#分批次加载训练数据
def generator_data(trainfile,trainlables,batch_size):
count = 0
lent = len(trainfile)
while True:
s = count
count = count + batch_size
if count >= lent:
count = 0
s = count
count = count + batch_size
X = trainfile[s:count]
Y = trainlables[s:count]
train_x,train_y = get_batch_data(X,Y)
yield (train_x,train_y)
一个形如(inputs,targets)的数据组,所有的返回值都应该包含相同数目的样本。生成器将无限在数据集上循环。每个epoch以经过模型的样本数达到samples_per_epoch时,记一个epoch结束。
3.3.4.2. 模型使用生成器输入训练数据
model.fit_generator(generator_data(X_train,Y_train,50),
steps_per_epoch=700,#这里700代表训练集的总数,700*50=35000个样本,设置为比总数小的都可以
epochs=10
)
steps_per_epoch:整数,当生成器返回steps_per_epoch次数据时计一个epoch结束,执行下一个epoch,它表示是将一个epoch分成多少个batch_size, 如果训练样本数N=35000,steps_per_epoch = 700,那么相当于一个batch_size=50
epochs:整数,数据迭代的轮数
verbose:日志显示,0为不在标准输出流输出日志信息,1为输出进度条记录,2为每个epoch输出一行记录
3.3.4.3. 关于yield()
生成器函数——yield(),在python 里就是一个生成器。当你使用一个yield的时候,对应的函数就是一个生成器了。生成器的功能就是在yield的 区域进行迭代处理。
yield 是一个类似 return 的关键字,迭代一次遇到yield时就返回yield后面(右边)的值。重点是:下一次迭代时,从上一次迭代遇到的yield后面的代码(下一行)开始执行。return 的作用:如果没有 return,则默认执行至函数完毕,返回的值 一般是 yield的变量。
3.3.5. epochs与训练时长
随着epochs的增大,后期训练时间逐步增长,例如60000个图片样本,其中训练集为70%,训练过程中,从最开始5多分钟一轮,很快增加到6、7分钟一轮。这是端午节前一天开始训练大批数据,由于准备不充分,晚上20点多时,饥饿、头晕难耐,只好放弃本轮训练。
4. 训练优化
4.1. 优化学习率
model.compile(optimizer=Adam(lr=0.0001), loss = 'categorical_crossentropy', metrics=['accuracy'])
Adam
keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False)
Adam 优化器。
默认参数遵循原论文中提供的值。
参数
lr: float >= 0. 学习率。
beta_1: float, 0 < beta < 1. 通常接近于 1。
beta_2: float, 0 < beta < 1. 通常接近于 1。
epsilon: float >= 0. 模糊因子. 若为 None, 默认为 K.epsilon()。
decay: float >= 0. 每次参数更新后学习率衰减值。
amsgrad: boolean. 是否应用此算法的 AMSGrad 变种,来自论文 “On the Convergence of Adam and Beyond”。
4.2. Batch
如果Batch 设置过大,可能发生OOM问题,例如设置Batch=100时,运行一段时间后,报GPU内存不足(OOM)错误。然后,把batch设置为batch_size=50,就可以了。
GPU内存为4G。
4.3. 训练集模型准确度高,而测试集准确度要低些
当把epochs设置大些情况下,多轮学习的效果很好,例如本文设置到epochs=20.
8000样本,分4类,训练准确率98%,测试才71%。一般来说,最有可能是因为样本少的原因,也可能过拟合?后续继续优化。
5. 参考知识
5.1. 关于Batch Normalization
机器学习领域有个很重要的假设:IID独立同分布假设,就是假设训练数据和测试数据是满足相同分布的,这是通过训练数据获得的模型能够在测试集获得好的效果的一个基本保障。那BatchNorm的作用是什么呢?BatchNorm就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同分布的。
5.2. 关于Loss损失函数(又称目标函数)
- mean_squared_error或mse
- mean_absolute_error或mae
- mean_absolute_percentage_error或mape
- mean_squared_logarithmic_error或msle
- squared_hinge
- hinge
- binary_crossentropy(亦称作对数损失,logloss)
- categorical_crossentropy:亦称作多类的对数损失,注意使用该目标函数时,需要将标签转化为形如(nb_samples, nb_classes)的二值序列
- sparse_categorical_crossentrop:如上,但接受稀疏标签。注意,使用该函数时仍然需要你的标签与输出值的维度相同,你可能需要在标签数据上增加一个维度:np.expand_dims(y,-1)
- kullback_leibler_divergence:从预测值概率分布Q到真值概率分布P的信息增益,用以度量两个分布的差异.
- cosine_proximity:即预测值与真实标签的余弦距离平均值的相反数
6. 总结
VGG、GoogLeNet、ResNet是目前广泛使用的网络,ResNet的效果略优。如FaceNet、Im2Tx等应用。
网络层本身的设计和跳过若干层,将是今后一段时间的研究热点。
- 改进特征和梯度的流转路径;
- 重点关注网络深度、宽度、残差连接三方面。
参考:
[1]《Keras 中文文档》 BigMoyan
[2]《Keras Documentation》 Keras: 基于 Python 的深度学习库
[3]《TensorFlow 指南》
[4]《model.fit_generator()函数参数》 CSDN博客 qq_32951799 2018年10月
[5]《TensorFlow和Keras解决大数据量内存溢出问题》 CSDN博客 LeadAI学院 2018年4月
[6]《使用Tensorflow实现残差网络ResNet-50》 CSDN博客 蜗牛爱上星星 2018年1月
[7]《基于Keras的ResNet实现》 CSDN博客 冲动老少年 2018年7月
[8]《Windows10(64bit)环境下Python3 + Tensorflow安装》 CSDN博客 肖永威 2018年10月
[9]《TensorFlow CNN卷积神经网络实现工况图分类识别(一)》 CSDN博客 肖永威 2019年3月
[10]《ResNet50及其Keras实现》 CSDN博客 从流域到海域 2018年12月