最近在做OCR相关的东西,关于OCR真的是有悠久了历史了,最开始用tesseract然而效果总是不理想,其中字符分割真的是个博大精深的问题,那么多年那么多算法,然而应用到实际总是有诸多问题。比如说非等间距字体的分割,汉字的分割,有光照阴影的图片的字体分割等等,针对特定的问题,特定的算法能有不错的效果,但也仅限于特定问题,很难有一些通用的结果。于是看了Xlvector的博客之后,发现可以端到端来实现OCR,他是基于mxnet的,于是我想把它转到tensorflow这个框架来,顺便还能熟悉一下这个框架。本文主要介绍实现思路,更加细节的实现方法见另一篇。
利用captcha来生成验证码,具体生成验证码的代码请见这里,共生成4-6
位包含数字和英文大小写的训练图片128000张和测试图片400张。命名规则就是num_label.png
,生成的图片如下图
code = image_name.split('/')[2].split('_')[1].split('.')[0] code = [SPACE_INDEX if code == SPACE_TOKEN else maps[c] for c in list(code)] self.labels.append(code) print(image_name,' ',code) @property def size(self): return len(self.labels) def input_index_generate_batch(self,index=None): if index: image_batch=[self.image[i] for i in index] label_batch=[self.labels[i] for i in index] else: # get the whole data as input image_batch=self.image label_batch=self.labels def get_input_lens(sequences): lengths = np.asarray([len(s) for s in sequences], dtype=np.int64) return sequences,lengths batch_inputs,batch_seq_len = get_input_lens(np.array(image_batch)) batch_labels = sparse_tuple_from_label(label_batch) return batch_inputs,batch_seq_len,batch_labels
需要注意的是tensorflow lstm输入格式的问题,其label tensor应该是稀疏矩阵,所以读取图片和label之后,还要进行一些处理,具体可以看代码
关于载入图片,发现12.8w张图一次读进内存,内存也就涨了5G,如果训练数据加大,还是加一个pipeline来读比较好。
然后是网络结构
1234567891011121314151617181920212223242526272829303132333435363738 |
graph = tf.Graph()with graph.as_default(): inputs = tf.placeholder(tf.float32, [None, None, num_features]) labels = tf.sparse_placeholder(tf.int32) seq_len = tf.placeholder(tf.int32, [None]) # Stacking rnn cells stack = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.LSTMCell(FLAGS.num_hidden,state_is_tuple=True) for i in range(FLAGS.num_layers)] , state_is_tuple=True) # The second output is the last state and we will no use that outputs, _ = tf.nn.dynamic_rnn(stack, inputs, seq_len, dtype=tf.float32) shape = tf.shape(inputs) batch_s, max_timesteps = shape[0], shape[1] # Reshaping to apply the same weights over the timesteps outputs = tf.reshape(outputs, [-1, FLAGS.num_hidden]) # Truncated normal with mean 0 and stdev=0.1 W = tf.Variable(tf.truncated_normal([FLAGS.num_hidden, num_classes], stddev=0.1),name='W') b = tf.Variable(tf.constant(0., shape=[num_classes],name='b')) # Doing the affine projection logits = tf.matmul(outputs, W) + b # Reshaping back to the original shape logits = tf.reshape(logits, [batch_s, -1, num_classes]) # Time major logits = tf.transpose(logits, (1, 0, 2)) global_step = tf.Variable(0,trainable=False) loss = tf.nn.ctc_loss(labels=labels,inputs=logits, sequence_length=seq_len) cost = tf.reduce_mean(loss) #optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate, # momentum=FLAGS.momentum).minimize(cost,global_step=global_step) optimizer = tf.train.AdamOptimizer(learning_rate=FLAGS.initial_learning_rate, beta1=FLAGS.beta1,beta2=FLAGS.beta2).minimize(loss,global_step=global_step) # Option 2: tf.contrib.ctc.ctc_beam_search_decoder # (it's slower but you'll get better results) #decoded, log_prob = tf.nn.ctc_greedy_decoder(logits, seq_len,merge_repeated=False) decoded, log_prob = tf.nn.ctc_beam_search_decoder(logits, seq_len,merge_repeated=False) # Inaccuracy: label error rate lerr = tf.reduce_mean(tf.edit_distance(tf.cast(decoded[0], tf.int32), labels)) |
这里我参考了stackoverflow的一篇帖子写的,根据tensorflow 1.0.1的版本做了微调,使用了Adam作为optimizer。
需要注意的是ctc_beam_search_decoder是非常耗时的,见下图
和greedy_decoder的区别是,greedy_decoder根据当前序列预测下一个字符,并且取概率最高的作为结果,再此基础上再进行下一次预测。而beam_search_decoder每次会保存取k个概率最高的结果,以此为基础再进行预测,并将下一个字符出现的概率与当前k个出现的概率相乘,这样就可以减缓贪心造成的丢失好解的情况,当k=1的时候,二者就一样了。
—update—
稍微调一调,网络可以跑到85%以上。
把网络用在识别身份证号,试了73张网上爬的(不同分辨率下的)真实图片,错了一张,准确率在98%左右(不过毕竟身份证号比较简单)
大概14个epoch后,准确率过了50%,现在跑到了73%的正确率。
最后,代码托管在Github上。
百度出了一个warpCTC可以加速CTC的计算,试用了一下CPU的版本发现好像没什么速度的提升,不知道是不是姿势不对,回头再试试GPU的版本。
对于更加细节的实现方法(输入输出的构造,以及warpCTC和内置ctc_loss的异同)放在了另一篇博客。
如果有发现问题,请前辈们一定要不吝赐教,在下方留言指出,或者在github上提出issue