笔者在使用CRNN完成长文本识别的过程中,用了keras的api搭建了神经网络,并对于其中的代码进行了详读,简单作些笔记,供有同样需求的同学进行学习,完成神经网络搭建。在此以CRNN为例(CNN+RNN+CTC)
(根据CRNN的论文描述,CRNN是由CNN-》RNN-》CTC三大部分架构而成,分别对应卷积层、循环层和转录层。首先CNN部分用于底层的特征提取,RNN采取了BiLSTM,用于学习关联序列信息并预测标签分布,CTC用于序列对齐,输出预测结果。)
为了将特征输入到Recurrent Layers,做如下处理:
~首先会将图像缩放到 32×W×3 大小
~然后经过CNN后变为 1×(W/4)× 512,本文中W=256。
~接着针对LSTM,设置 T=(W/4) , D=512 ,即可将特征输入LSTM。
深度学习的初始化参数指的是在网络训练之前,对各个节点的权重和偏置进行初始化的过程,很多时候我们以为这个初始化是无关紧要的,不需要什么讲究,但是实际上,一个参数的初始化关系到网络能否训练出好的结果或者是以多快的速度收敛,这都是至关重要的,有时候因为参数初始化的缘故,甚至得不到好的训练结果。本文就来讨论一下参数初始化到底有什么讲究以及常见的参数初始化的一些策略方法。阅读本文需要神经网络相关背景,能够理解误差反向传播算法的实现过程。
下面展示了正态化的kaiming初始化——he_normal
。
He 正态分布初始化器:它从以 0 为中心,标准差为 stddev = sqrt(2 / fan_in) 的截断正态分布中抽取样本, 其中 fan_in是权值张量中的输入单位的数量,在keras中的实现为
keras.initializers.he_normal(seed=None)
#He 正态分布初始化器它从以 0 为中心,标准差为 stddev = sqrt(2 / fan_in) 的截断正态分布中抽取样本, 其中 fan_in是权值张量中的输入单位的数量
initializer = initializers.he_normal()
CRNN论文提到的网络输入是归一化好的100×32大小的灰度图像,即高度统一为32个像素。本文中是256×32大小的灰度图像。下面是CRNN的深度神经网络结构图,CNN采取了经典的VGG16,值得注意的是,在VGG16的第3第4个max pooling层CRNN采取的是1×2的矩形池化窗口(w×h),这有别于经典的VGG16的2×2的正方形池化窗口,这个改动是因为文本图像多数都是高较小而宽较长,所以其feature map也是这种高小宽长的矩形形状,如果使用1×2的池化窗口则更适合英文字母识别(比如区分i和l)。VGG16部分还引入了BatchNormalization模块,旨在加速模型收敛(具体原理自行搜索,BN的原理笔者看完之后还是觉得很有意思的hhh)。
Input():用来实例化一个keras张量
Input(shape=None,batch_shape=None,name=None,dtype=K.floatx(),sparse=False,tensor=None)
#参数:
shape: 形状元组(整型),不包括batch size。for instance, shape=(32,) 表示了预期的输入将是一批32维的向量。
batch_shape: 形状元组(整型),包括了batch size。for instance, batch_shape=(10,32)表示了预期的输入将是10个32维向量的批次。
name: 对于该层是可选的名字字符串。在一个模型中是独一无二的(同一个名字不能复用2次)。如果name没有被特指将会自动生成。
dtype: 预期的输入数据类型
sparse: 特定的布尔值,占位符是否为sparse
tensor: 可选的存在的向量包装到Input层,如果设置了,该层将不会创建一个占位张量。
#返回 一个张量
inputs = Input(shape=(img_height, img_width, 1), name='img_inputs')
#实例化一个keras张量,表示输入将是一批256*32大小的灰色图像(1维)
这里的搭建就非常简单了,每个函数都有相关的说明,按照CNN网络搭建的要领以及网络架构(上图)直接进行复现:
# CNN(VGG)
inputs = Input(shape=(img_height, img_width, 1), name='img_inputs') #实例化一个keras张量,表示输入将是一批256*32大小的灰色图像
x = Conv2D(64, (3, 3), padding="same", kernel_initializer=initializer, name='conv1')(inputs)#卷积核数量64(channel大小64),卷积核大小3*3,采用全零填充,卷积核初始化方式按之前的定义。
x = BatchNormalization()(x)#BN操作,批标准化
x = Activation("relu")(x)#relu激活函数
x = MaxPooling2D(pool_size=(2, 2), strides=2, name='maxpool1')(x)#最大池化,2*2池化核,步长为2
x = Conv2D(128, (3, 3), padding="same", kernel_initializer=initializer, name='conv2')(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = MaxPooling2D(pool_size=(2, 2), strides=2, name='maxpool2')(x)
x = Conv2D(256, (3, 3), padding="same", kernel_initializer=initializer, name='conv3')(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = Conv2D(256, (3, 3), padding="same", kernel_initializer=initializer, name='conv4')(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = MaxPooling2D(pool_size=(2, 1), strides=(2, 1), name='maxpool3')(x)
x = Conv2D(512, (3, 3), padding="same", kernel_initializer=initializer, name='conv5')(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = Conv2D(512, (3, 3), padding="same", kernel_initializer=initializer, name='conv6')(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = MaxPooling2D(pool_size=(2, 1), strides=(2, 1), name='maxpool4')(x)
x = Conv2D(512, (2, 2), padding='same', activation='relu', kernel_initializer=initializer, name='conv7')(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
conv_out = MaxPooling2D(pool_size=(2, 1), name="conv_output")(x)
特别需要注意的是原网络是按照(W×H)先宽后高进行编写的,而在代码段是按照H×W,因此1×2(w×h)的池化窗口,在代码段应该写为2×1(h×w)的格式,其他地方对照网络结构,完全一致。
RNN部分使用了双向LSTM,隐藏层单元数为256,CRNN采用了两层BiLSTM来组成这个RNN层,RNN层的输出维度将是(s,b,class_num) ,其中class_num为文字类别总数。
在这里用到了Permute层,可以看到Keras的中文文档里关于该层是这样介绍的:
简单来说,Permute就是来转换维度的,在上述示例中把第一维和第二维坐了交换。而在本文中则是多维进行交换,具体见下条代码。
LSTM的输入
input of shape (seq_len, batch, input_size): tensor containing the features of the input sequence.
The input can also be a packed variable length sequence.
input shape(a,b,c)
a:seq_len -> 序列长度
b:batch
c:input_size 输入特征数目
根据LSTM的输入要求,我们要对CNN的输出做些调整,即把CNN层的输出调整为[seq_len, batch, input_size]形式。
也就是说,我们只需根据CNN的输出重新排列,使其能够送入RNN网络即可,在这里我用了debug查看了各输出的shape,方便理解。
# CNN to RNN
x = Permute((2, 3, 1))(conv_out) #x shape(64,512,1)
rnn_input = TimeDistributed(Flatten())(x)#rnn_input(?,64,512),?是批表示的维度
首先了解Bidirextional函数:
关于LSTM函数的解释
我们根据网络结构以及上述函数,搭建出双层BILSTM:
y = Bidirectional(LSTM(256, kernel_initializer=initializer, return_sequences=True),
merge_mode='sum', name='LSTM_1')(rnn_input)#return_sequences:默认 False。在输出序列中,返回单个 hidden state值还是返回全部time step 的 hidden state值。 False 返回单个, true 返回全部。return_state:默认 False。是否返回除输出之外的最后一个状态。
y = BatchNormalization()(y)
y = Bidirectional(LSTM(256, kernel_initializer=initializer, return_sequences=True), name='LSTM_2')(y)
最后输出的y的shape是(?,?,512)
最后把RNN的输出接全连接层,过softmax函数便可得到预测值了,本代码的预测是从0-9再加一个“-”共11类,因此由下图可知最后的输出是11分类,取最大的结果输出。这里的MAX_LABEL_LENGTH取最大的标签长度。输入长度和标签长度均为一维张量,之后根据ctc损失函数进行训练即可。
代码如下:
y_pred = Dense(NUM_CLASSES, activation='softmax', name='y_pred')(y)
labels = Input(shape=[MAX_LABEL_LENGTH], name='labels')
input_length = Input(shape=[1], name='input_length')
label_length = Input(shape=[1], name='label_length')
ctc_loss_output = Lambda(ctc_loss_layer, output_shape=(1,), name='ctc_loss_output')(
[labels, y_pred, input_length, label_length]
)
if is_training:
model = Model(inputs=[labels, inputs, input_length, label_length], outputs=ctc_loss_output)
# model.summary()
return model
else:
base_model = Model(inputs=inputs, outputs=y_pred)
# base_model.summary()
return base_model
##(4)CTC损失函数
关于CTC loss,在笔者观看的多篇帖子中都是通过从Github下载别人实现的CTC loss来实现,操作比较复杂,如果使用Pytorch实现这部分,现在已经有内置的CTC loss可以直接调用,一行代码便可以完成(import相关库),不必进行繁琐的安装操作。在此我们也给出本文实现的方法:
def ctc_loss_layer(args):
labels, y_pred, input_length, label_length = args
# the 2 is critical here since the first couple outputs of the RNN tend to be garbage
y_pred = y_pred[:, 2:, :]
return K.ctc_batch_cost(labels, y_pred, input_length, label_length)
代码共训练了10个epoch作为demo,后续还可以改变参数,使得损失函数进一步减小,训练出更好的模型。
关于神经网络搭建八股的Keras版详细内容,可参考北京大学软件与微电子学院曹健副教授的精品课程《人工智能实践:Tensorflow笔记》里面的第三讲有关于搭建神经网络的详细内容《人工智能实践:Tensorflow笔记》.
例如文中的批处理BN操作,最大池化操作等等的小概念。在视频中均有讲解,内容详细。