使用 Keras 进行无分割手写文本识别
Segmentation-free Handwritten Text Recognition with Keras
我目前正在开发一个免分割手写文本识别的应用程序。
因此,文本行是从输入文档中提取出来的,然后应该被识别。
出于开发目的,我使用 IAM Handwriting Database。它提供文本行图像以及相应的 ASCII 文本。
为了识别,我采用了论文“An End-to-End Trainable Neural Network for Image-based Sequence Recognition and Its Application to Scene Text Recognition" and "Can We Build Language-independent OCR Using LSTM Networks?”中的方法。
基本上,我使用双向 GRU 架构和前向后向算法将转录本与神经网络的输出对齐。
数据库中的图像如下所示:
图像呈现为像素值的一维序列,更准确地说,图像首先缩放到 32 像素的高度。
上图的 numpy 数组的维度为 597 x 32 的形状为:(597, 32).
numpy 数组表示大小为 n 的整体训练图像,其形状为:(n, w, 32) 其中 w 表示线条图像的可变宽度(例如 597)。
以下代码显示了训练图像和转录的表示方式:
x_train = []
y_train = []
line_height_normalized = 32
for i in range(sample_size):
transcription_train, image_train = self._get_next_sample()
image_train = convert_to_grayscale(image_train)
image_train = scale_y(image_train, line_height_normalized)
image_train_patches = sklearn_image.extract_patches_2d(image_train, (line_height_normalized, 1))
image_train_patches = numpy.reshape(image_train_patches, (image_train_patches.shape[0], -1))
x_train.append(image_train_patches)
y_train.append(transcription_train)
我使用Keras 循环神经网络的创建和CTC函数是基于this example .
charset = 68
number_of_memory_units = 512
time_steps = None
input_dimension = 32 # the height of a text line in pixel
# input shape see https://github.com/keras-team/keras/issues/3683
network_input = Input(name="input", shape=(time_steps, input_dimension))
gru_layer_1 = GRU(number_of_memory_units, return_sequences=True, kernel_initializer='he_normal',
name='gru_layer_1')(network_input)
gru_layer_1_backwards = GRU(number_of_memory_units, return_sequences=True, go_backwards=True,
kernel_initializer='he_normal',name='gru_layer_1_backwards')(network_input)
gru_layer_1_merged = add([gru_layer_1, gru_layer_1_backwards])
gru_layer_2 = GRU(number_of_memory_units, return_sequences=True, kernel_initializer='he_normal',
name='gru_layer_2')(gru_layer_1_merged)
gru_layer_2_backwards = GRU(number_of_memory_units, return_sequences=True, go_backwards=True, kernel_initializer='he_normal',
name='gru_layer_2_backwards')(gru_layer_1_merged)
output_layer = Dense(charset, kernel_initializer='he_normal',
name='dense_layer')(concatenate([gru_layer_2, gru_layer_2_backwards]))
prediction = Activation('softmax', name='output_to_ctc')(output_layer)
# create the ctc layer
input_length = Input(name='input_length', shape=[1], dtype='int64')
label_length = Input(name='label_length', shape=[1], dtype='int64')
max_line_length = 200 # see QUESTION 1
labels = Input(name='labels', shape=[max_line_length], dtype='float32')
loss_out = Lambda(RecurrentNeuralNetwork._ctc_function, name='ctc')(
[prediction, labels, input_length, label_length])
model = Model(inputs=[network_input, labels, input_length, label_length], outputs=loss_out)
sgd = SGD(lr=0.02, decay=1e-6, momentum=0.9, nesterov=True, clipnorm=5)
model.compile(loss={'ctc': lambda l_truth, l_prediction: prediction}, optimizer=sgd)
问题 1
在示例中使用了 max_line_length;正如我在互联网上阅读的那样(但我认为我不太理解它退出得很好)需要最大行长度,因为底层 CTC 函数需要知道应该创建多少张量。
什么长度适合可变行长度?这对看不见的文本行的识别有何影响?
此外,input_length 变量和 label_length 变量究竟代表什么?
下一步训练网络:
batch_size = 1
number_of_epochs = 4
size = 32 # line height? see QUESTION 2
input_length = numpy.zeros([size, 1])
label_length = numpy.zeros([size, 1])
for epoch in range(number_of_epochs):
for x_train_batch, y_train_batch in zip(x_train, y_train_labels):
x_train_batch = numpy.reshape(x_train_batch, (1, len(x_train_batch), 32))
inputs = {'input': x_train_batch, 'labels': numpy.array(y_train_batch),
'input_length': input_length, 'label_length': label_length}
outputs = {'ctc': numpy.zeros([size])} # dummy data for dummy loss function
self.model.fit(x=inputs, y=outputs, batch_size=batch_size, epochs=1, shuffle=False)
self.model.reset_states()
它以大小为 1 的批次进行训练,因为时间步长是可变的(文本行的宽度)。
文本行的转录由一个 numpy 数组 y_train_batch 表示;每个字符都是数字编码的。
上面图像示例的转录如下所示:
[26 62 38 40 47 30 62 19 14 62 18 19 14 15 62 38 17 64 62 32 0 8 19 18 10 4 11 11 62 5 17 14 12]
问题二
size 变量代表什么?它是单图像块的尺寸,因此是每个时间步长的特征吗?
错误
发生的错误如下:
- 预期标签的形状为 (200,) 但得到的数组形状为 (1,)
是否需要填充标签数组以包含 200 个元素?
当我将 max_line_length 的值替换为 1 时,出现下一个错误:
- 所有输入数组 (x) 应具有相同数量的样本。得到数组形状:[(1, 597, 32), (33, 1), (32, 1), (32, 1)]
其他三个数组是否需要reshape?
我不确定解决此问题的 "right" 方法是什么以及接下来可能发生的错误?
也许有人能给我指出正确的方向。
非常感谢!
好的,我无法用评论部分提供的 600 个字符来解释这一点,因此我将通过回答来做到这一点,但忽略您的 Q2。
您提到的论文的代码可以在以下位置找到:https://github.com/bgshih/crnn
这是手写文本识别的一个很好的起点。
但是,CRNN 实现可以识别 word-level 上的文本,而您想在 line-level 上进行识别,因此您需要更大的输入图像,例如我使用 800x64px 和最大文本长度 100。
正如已经说过的,将图像拉伸到所需的大小效果不是很好,在我的实验中,使用填充时准确性提高了(稍微随机化位置......这是一种进行数据扩充的简单方法)。
最大文本长度 L 和输入图像宽度 W 之间存在关系:神经网络 (NN) 通过固定比例因子 f 缩小输入图像的尺寸:L=W/f(在我的示例中:W=800px,L=100,f=8)。
所附插图显示了输入图像 (800x64px) 和字符概率矩阵(100 time-steps 中的每一个的 80 个可能字符中的每一个的概率)。
NN 将输入图像映射到该字符概率矩阵,作为 CTC 的输入。
由于矩阵中有 L many time-steps,最多可以有 L many characters:这当然适用于解码,但损失计算必须以某种方式将 ground truth 文本与该矩阵对齐,以及文本应该如何对齐L+1 个字符仅与矩阵中包含的 L time-steps 对齐!?
请注意,在 CTC 计算中,重复字符(如 "piZZa")必须由特殊字符分隔 - 因此每次重复可能的文本长度减少 1。
我认为通过这个解释,您应该能够弄清楚代码中的所有 length-variables 是如何相互关联的。
我目前正在开发一个免分割手写文本识别的应用程序。 因此,文本行是从输入文档中提取出来的,然后应该被识别。
出于开发目的,我使用 IAM Handwriting Database。它提供文本行图像以及相应的 ASCII 文本。
为了识别,我采用了论文“An End-to-End Trainable Neural Network for Image-based Sequence Recognition and Its Application to Scene Text Recognition" and "Can We Build Language-independent OCR Using LSTM Networks?”中的方法。
基本上,我使用双向 GRU 架构和前向后向算法将转录本与神经网络的输出对齐。
数据库中的图像如下所示:
图像呈现为像素值的一维序列,更准确地说,图像首先缩放到 32 像素的高度。
上图的 numpy 数组的维度为 597 x 32 的形状为:(597, 32).
numpy 数组表示大小为 n 的整体训练图像,其形状为:(n, w, 32) 其中 w 表示线条图像的可变宽度(例如 597)。
以下代码显示了训练图像和转录的表示方式:
x_train = []
y_train = []
line_height_normalized = 32
for i in range(sample_size):
transcription_train, image_train = self._get_next_sample()
image_train = convert_to_grayscale(image_train)
image_train = scale_y(image_train, line_height_normalized)
image_train_patches = sklearn_image.extract_patches_2d(image_train, (line_height_normalized, 1))
image_train_patches = numpy.reshape(image_train_patches, (image_train_patches.shape[0], -1))
x_train.append(image_train_patches)
y_train.append(transcription_train)
我使用Keras 循环神经网络的创建和CTC函数是基于this example .
charset = 68
number_of_memory_units = 512
time_steps = None
input_dimension = 32 # the height of a text line in pixel
# input shape see https://github.com/keras-team/keras/issues/3683
network_input = Input(name="input", shape=(time_steps, input_dimension))
gru_layer_1 = GRU(number_of_memory_units, return_sequences=True, kernel_initializer='he_normal',
name='gru_layer_1')(network_input)
gru_layer_1_backwards = GRU(number_of_memory_units, return_sequences=True, go_backwards=True,
kernel_initializer='he_normal',name='gru_layer_1_backwards')(network_input)
gru_layer_1_merged = add([gru_layer_1, gru_layer_1_backwards])
gru_layer_2 = GRU(number_of_memory_units, return_sequences=True, kernel_initializer='he_normal',
name='gru_layer_2')(gru_layer_1_merged)
gru_layer_2_backwards = GRU(number_of_memory_units, return_sequences=True, go_backwards=True, kernel_initializer='he_normal',
name='gru_layer_2_backwards')(gru_layer_1_merged)
output_layer = Dense(charset, kernel_initializer='he_normal',
name='dense_layer')(concatenate([gru_layer_2, gru_layer_2_backwards]))
prediction = Activation('softmax', name='output_to_ctc')(output_layer)
# create the ctc layer
input_length = Input(name='input_length', shape=[1], dtype='int64')
label_length = Input(name='label_length', shape=[1], dtype='int64')
max_line_length = 200 # see QUESTION 1
labels = Input(name='labels', shape=[max_line_length], dtype='float32')
loss_out = Lambda(RecurrentNeuralNetwork._ctc_function, name='ctc')(
[prediction, labels, input_length, label_length])
model = Model(inputs=[network_input, labels, input_length, label_length], outputs=loss_out)
sgd = SGD(lr=0.02, decay=1e-6, momentum=0.9, nesterov=True, clipnorm=5)
model.compile(loss={'ctc': lambda l_truth, l_prediction: prediction}, optimizer=sgd)
问题 1
在示例中使用了 max_line_length;正如我在互联网上阅读的那样(但我认为我不太理解它退出得很好)需要最大行长度,因为底层 CTC 函数需要知道应该创建多少张量。
什么长度适合可变行长度?这对看不见的文本行的识别有何影响?
此外,input_length 变量和 label_length 变量究竟代表什么?
下一步训练网络:
batch_size = 1
number_of_epochs = 4
size = 32 # line height? see QUESTION 2
input_length = numpy.zeros([size, 1])
label_length = numpy.zeros([size, 1])
for epoch in range(number_of_epochs):
for x_train_batch, y_train_batch in zip(x_train, y_train_labels):
x_train_batch = numpy.reshape(x_train_batch, (1, len(x_train_batch), 32))
inputs = {'input': x_train_batch, 'labels': numpy.array(y_train_batch),
'input_length': input_length, 'label_length': label_length}
outputs = {'ctc': numpy.zeros([size])} # dummy data for dummy loss function
self.model.fit(x=inputs, y=outputs, batch_size=batch_size, epochs=1, shuffle=False)
self.model.reset_states()
它以大小为 1 的批次进行训练,因为时间步长是可变的(文本行的宽度)。
文本行的转录由一个 numpy 数组 y_train_batch 表示;每个字符都是数字编码的。
上面图像示例的转录如下所示:
[26 62 38 40 47 30 62 19 14 62 18 19 14 15 62 38 17 64 62 32 0 8 19 18 10 4 11 11 62 5 17 14 12]
问题二
size 变量代表什么?它是单图像块的尺寸,因此是每个时间步长的特征吗?
错误
发生的错误如下:
- 预期标签的形状为 (200,) 但得到的数组形状为 (1,)
是否需要填充标签数组以包含 200 个元素?
当我将 max_line_length 的值替换为 1 时,出现下一个错误:
- 所有输入数组 (x) 应具有相同数量的样本。得到数组形状:[(1, 597, 32), (33, 1), (32, 1), (32, 1)]
其他三个数组是否需要reshape?
我不确定解决此问题的 "right" 方法是什么以及接下来可能发生的错误?
也许有人能给我指出正确的方向。
非常感谢!
好的,我无法用评论部分提供的 600 个字符来解释这一点,因此我将通过回答来做到这一点,但忽略您的 Q2。
您提到的论文的代码可以在以下位置找到:https://github.com/bgshih/crnn 这是手写文本识别的一个很好的起点。 但是,CRNN 实现可以识别 word-level 上的文本,而您想在 line-level 上进行识别,因此您需要更大的输入图像,例如我使用 800x64px 和最大文本长度 100。 正如已经说过的,将图像拉伸到所需的大小效果不是很好,在我的实验中,使用填充时准确性提高了(稍微随机化位置......这是一种进行数据扩充的简单方法)。
最大文本长度 L 和输入图像宽度 W 之间存在关系:神经网络 (NN) 通过固定比例因子 f 缩小输入图像的尺寸:L=W/f(在我的示例中:W=800px,L=100,f=8)。 所附插图显示了输入图像 (800x64px) 和字符概率矩阵(100 time-steps 中的每一个的 80 个可能字符中的每一个的概率)。 NN 将输入图像映射到该字符概率矩阵,作为 CTC 的输入。 由于矩阵中有 L many time-steps,最多可以有 L many characters:这当然适用于解码,但损失计算必须以某种方式将 ground truth 文本与该矩阵对齐,以及文本应该如何对齐L+1 个字符仅与矩阵中包含的 L time-steps 对齐!? 请注意,在 CTC 计算中,重复字符(如 "piZZa")必须由特殊字符分隔 - 因此每次重复可能的文本长度减少 1。
我认为通过这个解释,您应该能够弄清楚代码中的所有 length-variables 是如何相互关联的。