社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  机器学习算法

百度深度学习图像识别决赛代码分享(OCR)

大数据挖掘DT机器学习 • 5 年前 • 882 次点击  


向AI转型的程序员都关注了这个号👇👇👇

大数据挖掘DT机器学习  公众号: datayx



相关文章

深度学习不定长文字的识别与定位:车牌号识别(keras)


使用 Keras搭建一个深度卷积神经网络来识别 c验证码


大赛官网:http://meizu.baiducloud.top/ps/web/index.html
初赛内容:从图片中识别四则运算式,算式可能包含数字0~9、运算符+-*、括号()。并且,算式的长度固定为5或7,包含三个数字,两个运算符,0或1对括号。下面是几个样例:

(4*8)+8




(0-2)+5



2*8-7


要求参赛者给出每张图片中的算式和运算结果。

训练集共100,000张图片,并附带标签。测试集共200,000张图片,无标签,预测结果上传后计算正确率,作为初赛的排名。


本文初赛、决赛代码 github 地址、初赛数据集获取方式:

关注微信公众号 datayx 然后回复 图像识别  即可获取。


问题描述

本次竞赛目的是为了解决一个 OCR 问题,通俗地讲就是实现图像到文字的转换过程。

数据集

初赛数据集一共包含10万张180*60的图片和一个labels.txt的文本文件。每张图片包含一个数学运算式,运算式包含:

3个运算数:3个0到9的整型数字; 2个运算符:可以是+、-、*,分别代表加法、减法、乘法 0或1对括号:括号可能是0对或者1对

图片的名称从0.png到99999.png,下面是一些样例图片(这里只取了一张):

文本文件 labels.txt 包含10w行文本,每行文本包含每张图片对应的公式以及公式的计算结果,公式和计算结果之间空格分开,例如图片中的示例图片对应的文本如下所示:


评价指标

官方的评价指标是准确率,初赛只有整数的加减乘运算,所得的结果一定是整数,所以要求序列与运算结果都正确才会判定为正确。

我们本地除了会使用官方的准确率作为评估标准以外,还会使用 CTC loss 来评估模型。

使用 captcha 进行数据增强

官方提供了10万张图片,我们可以直接使用官方数据进行训练,也可以通过Captcha,参照官方训练集,随机生成更多数据,进而提高准确性。根据题目要求,label 必定是三个数字,两个运算符,一对或没有括号,根据括号规则,只有可能是没括号,左括号和右括号,因此很容易就可以写出数据生成器的代码。

生成器

生成器的生成规则很简单:

相信大家都能看懂。当然,我写文章的时候又想到一种更好的写法:

除了生成算式以外,还有一个值得注意的地方就是初赛所有的减号(也就是“-”)都是细的,但是我们直接用 captcha 库生成图像会得到粗的减号,所以我们修改了 image.py 中的代码,在 _draw_character 函数中我们增加了一句判断,如果是减号,我们就不进行 resize 操作,这样就能防止减号变粗:

if c != '-':
    im = im.resize((w2, h2))
    im = im.transform((w, h), Image.QUAD, data)

我们继而使用生成器生成四则运算验证码:

上图就是原版生成器生成的图,我们可以看到减号是很粗的。

上图是修改过的生成器,可以看到减号已经不粗了。

模型结构 

本文来自 微信公众号 datayx  【大数据挖掘DT机器学习


模型结构像之前写的文章一样,只是把卷积核的个数改多了一点,加了一些 BN 层,并且在四卡上做了一点小改动以支持多GPU训练。如果你是单卡,可以直接去掉 base_model2 = make_parallel(base_model, 4) 的代码。

BN 层主要是为了训练加速,实验结果非常好,模型收敛快了很多。

base_model 的可视化:

model 的可视化:

模型训练

在经过几次测试以后,我已经抛弃了 evaluate 函数,因为在验证集上已经能做到 100% 识别率了,所以只需要看 val_loss 就可以了。在经过之前的几次尝试以后,我发现在有生成器的情况下,训练代数越多越好,因此直接用 adam 跑了50代,每代10万样本,可以看到模型在10代以后基本已经收敛。

我们可以看到模型先分为四份,在四个显卡上并行计算,然后合并结果,计算最后的 ctc loss,进而训练模型。

结果可视化

这里我们对生成的数据进行了可视化,可以看到模型基本已经做到万无一失,百发百中。

打包成 docker 以后提交到比赛系统中,经过十几分钟的运行,我们得到了完美的1分。

总结

初赛是非常简单的,因此我们才能得到这么准的分数,之后官方进一步提升了难度,将初赛测试集提高到了20万张,在这个集上我们的模型只能拿到0.999925的成绩,可行的改进方法是将准确率进一步降低,充分训练模型,将多个模型结果融合等。

官方扩充测试集的难点

在扩充数据集上,我们发现有一些图片预测出来无法计算,比如 [629,2271,6579,17416,71857,77631,95303,102187,117422,142660,183693] 等,这里我们取 117422.png 为例。

我们可以看到肉眼基本无法认出这个图,但是经过一定的图像处理,我们可以显现出来它的真实面貌:

IMAGE_DIR='image_contest_level_1_validate'index =117422img =cv2.imread( '%s/%d.png'%IMAGE_DIR, index))gray =cv2.cvtColor(img, cv2. COLOR_BGR2GRAY)h =cv2.equalizeHist(gray)

然后我们可以看到这样的结果:

当然,还有一张图是无法通过预处理得到结果的,142660,这有可能是程序的 bug 造成的小概率事件,所以初赛除了我们跑了一个 docker 得到满分以外,没有第二个人达到满分。


决赛时的所有思路

数据集

决赛数据集一共包含10万张图片和一个labels.txt的文本文件。每张图片包含一个数学运算式,运算式中包含:

  1. 图片大小不固定

  2. 图片中的某一块区域为公式部分

  3. 图片中包含二行或者三行的公式

  4. 公式类型有两种:赋值和四则运算的公式。两行的包括由一个赋值公式和一个计算公式,三行的包括两个赋值公式和一个计算公式。加号(+) 即使旋转为 x ,仍为加号, * 是乘号

  5. 赋值类的公式,变量名为一个汉字。 汉字来自两句诗(不包括逗号): 君不见,黄河之水天上来,奔流到海不复回 烟锁池塘柳,深圳铁板烧

  6. 四则运算的公式包括加法、减法、乘法、分数、括号。 其中的数字为多位数字,汉字为变量,由上面的语句赋值。

  7. 输出结果的格式为:图片中的公式,一个英文空格,计算结果。 其中: 不同行公式之间使用英文分号分隔 计算结果时,分数按照浮点数计算,计算结果误差不超过0.01,视为正确。

  8. 整个label文件使用UTF8编码


决赛样例:

初赛的题不难,只需要识别文本序列即可,决赛的算式比较复杂,需要先经过图像处理,然后才能输入到神经网络中进行端到端的文本序列识别。

评价指标

官方的评价指标是准确率,初赛只有整数的加减乘运算,所得的结果一定是整数,所以要求序列与运算结果都正确才会判定为正确。

但决赛的数字通常都是五位数,并且会有很多乘法和加法,以及一定会存在的一个分数,所以结果很容易超出64位浮点数所能表示的范围,因此官方在经过讨论后决定只考虑文本序列的识别,不评价运算结果。

而我们本地除了会使用官方的准确率作为评估标准以外,还会使用 CTC loss 来评估模型。

数据的探索

定义

决赛的数据集探索就复杂得多,我们先明确两个概念:

流=42072;圳=86;(圳-(97510*45921))*流/35864

在这个式子中,流=42072;圳=86;被称为赋值式,(圳-(97510*45921))*流/35864被称为表达式,赋值式和表达式统称为公式,+-*/被称为运算符。

分析

首先我们对样本的每个字出现的次数进行了统计:

可以看到数字的分布很有意思,0出现的次数比其他数字都低,其他的数字出现次数基本一样,所以立即推这是直接按随机数生成的,0不能出现在首位,所以概率变低。

分号和等号出现的次数一样,这是因为每个赋值式都有一个等号和一个分号。它出现的概率是 1.65807,因此可以猜出一个赋值式和两个赋值式的比例是 1:2。

运算符出现的概率都是一样的,所以可以推断它们是直接随机取的。

括号出现的概率是 1.36505,我们统计了一下括号出现的所有可能:

1+1+1+1

(1+1)+1+1
1+(1+1)+1
1+1+(1+1)
(1+1+1)+1
1+(1+1+1)

((1+1)+1)+1
(1+(1+1))+1

1+((1+1)+1)
1+(1+(1+1))

(1+1)+(1+1)

一共有11种可能,按括号的数量统计括号出现的频率可以得出 2*5/11.0+5/11.0 = 1.3636,因此括号也是从上面几种模板随机取的。

中文除了“不”字出现了两次,概率翻倍,其他字概率基本相等。中文字取自于下面两句诗:“君不见,黄河之水天上来,奔流到海不复回 烟锁池塘柳,深圳铁板烧”,所以也可以推断出是按字直接随机取的。

总结

  • 中文直接等概率取,“不”概率加倍

  • 括号从11种情况中随机取

  • 运算符每次必出四个

  • 1/3概率取一个赋值式,2/3概率取2个赋值式

  • 运算符/永远都会出现一次,中文在上

  • 运算符+-*随机取,概率都是1/3

  • 数字取值范围是[0, 100000]

数据预处理

由于原始的图像十分巨大,直接输入到 CNN 中会有90%以上的区域是没有用的,所以我们需要对图像做预处理,裁剪出有用的部分。然后因为图像有两到三个式子,因此我们采取的方案是从左至右拼接在一起,这样的好处是图像比较小。(900*80=72000 vs 600*270=162000)

我主要使用了以下几种技术:

  • 转灰度图

  • 直方图均衡

  • 中值滤波

  • 开闭运算

  • 二值化

  • 轮廓查找

  • 边界矩形


首先先进行初步的关键区域提取:

def plot(index):
    img = cv2.imread('%s/%d.png'%(IMAGE_DIR, index))
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    eq = cv2.equalizeHist(gray)
    b = cv2.medianBlur(eq, 9)
    
    m, n = img.shape[:2]
    b2 = cv2.resize(b, (n//4, m//4))

    m1 = cv2.morphologyEx(b2, cv2.MORPH_OPEN, np.ones((7, 40)))
    m2 = cv2.morphologyEx(m1, cv2.MORPH_CLOSE, np.ones((4, 4)))
    _, bw = cv2.threshold(m2, 127, 255, cv2.THRESH_BINARY_INV)
    
    bw = cv2.resize(bw, (n, m))

    r = img.copy()
    img2, ctrs, hier = cv2.findContours(bw, cv2.RETR_EXTERNAL, 
      cv2.CHAIN_APPROX_SIMPLE)    for ctr in ctrs:
        x, y, w, h = cv2.boundingRect(ctr)
        cv2.rectangle(r, (x, y), (x+w, y+h), (0, 255, 0), 10)


去噪

首先要将图像转灰度图,然后用初赛使用的直方图均衡提高图像的对比度,这里噪点还在,所以需要进行滤波,我们这里使用了中值滤波,它能很好地滤掉噪点和干扰线。(上图的 blur)

连接公式

现在我们只关心公式的提取,而不在意字符的提取(因为无法保证准确提取),所以我们需要将这些字符连接起来。这里首先对图像进行了4倍的缩放,然后使用了一种叫做开闭运算的算法来连接字符。因为我们要的是横向连接,纵向不需要连接,所以我们选择了 (7, 40) 大小的开运算,然后为了滤掉不必要的噪声,我们使用了 (4, 4) 的闭运算。(位于上图中间的 m2)

关键区域提取

在拼接好公式以后,我们就可以对图像使用轮廓查找的算法了,很容易我们就可以抓到图像的三个边缘点集,然后我们使用边界矩形函数得到矩形的 (x, y, w, h),完成关键区域提取。提取之后我们将绿色的矩形画在了原图上。(位于上图右下角的 rect)

微调

由于之前使用了很大的 kernel 进行滤波,所以这里需要进行一个微调的操作:

# 微调三个公式d = 20d2 = 5imgs = []
sizes = []for i, ctr in enumerate


    
(ctrs):
    x, y, w, h = cv2.boundingRect(ctr)
    roi = img[max(0, y-d):min(m, y+h+d),max(0, x-d):min(n, x+w+d)]
    p, q, _ = roi.shape
    
    x = b[max(0, y-d):min(m, y+h+d),max(0, x-d):min(n, x+w+d)]
    x = cv2.morphologyEx(x, cv2.MORPH_CLOSE, np.ones((3, 3)))
    _, x = cv2.threshold(x, 127, 255, cv2.THRESH_BINARY_INV)
    _, x, _ = cv2.findContours(x, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    x, y, w, h = cv2.boundingRect(np.vstack(x))
    roi2 = roi[max(0, y-d2):min(p, y+h+d2),max(0, x-d2):min(q, x+w+d2)]
    imgs.append(roi2)
    sizes.append(roi2.shape)

首先通过之前的矩形,扩充20像素,然后裁剪出关键区域,这里是直接对滤波的图裁剪,所以分辨率很高。然后经过简单的闭运算滤波,二值化,提取边框,这里即使有噪点也不用担心,裁多了不要紧,裁少了才麻烦,然后裁出来的图可能会比较小,因为滤波过了,所以再扩充5个像素,达到不错的效果。

以下是几个例子:


连接三个公式

裁出来准确的公式以后,我们就可以直接进行横向连接了:

# 连接三个公式sizes = np.array(sizes)
img2 = np.zeros((sizes[:,0].max(), sizes[:,1].sum()+2*(len(sizes)-1), 3),                dtype=np.uint8)
x = 0for a in imgs[::-1]:
    w = a.shape[1]
    img2[:a.shape[0], x:x+w] = a
    x += w + 2



下图是拼接好的图像:

并行预处理

如果直接使用 python 的 for 循环去跑,只能占用一个核的 CPU 利用率,为了充分利用 CPU,我们使用了多进行并行预处理的方法让每个 CPU 都能满载运行。为了能够实时查看进度,我使用了 tqdm 这个进度条的库。

p = Pool(12)

n = 100000if __name__ == '__main__':
    rs = []    for r in tqdm(p.imap_unordered(f, range(n)), 
total=n): rs.append(r)


总结

这里我们把各个量之间的关系都画出来了,很有意思。

pd.plotting.scatter_matrix(df, alpha=0.1, figsize=(14,8),
diagonal='kde');



其中的 x, y 表示公式的起始坐标,w, h 表示公式的宽和高,n, m 表示原图的宽和高,r 表示有几个公式。我们可以从图中看到,x, y 没有明显的规律,稍微有一点规律就是越宽的图能得到的 x 越大(废话,宽1000的图不可能有公式出现在1200)。

w 也没有明显的规律,是典型的正态分布,而 h 则有两个峰,这是因为公式有两个和三个的差别。

m, n 很有规律,它们是按某几个固定的数随机取的,m 的取值是从 [400, 500, 600, 700, 800, 900, 1000] 中随机选取的,n 是从 [800, 1600, 2400, 3200, 4000] 中随机取的。

Counter(df['m'])
Counter({400: 14233,         500: 14414,         600: 14332,         700: 14304,         800: 14293,         900: 14299,         1000: 14125})

Counter(df['n'])
Counter({800: 19872, 1600: 19937, 2400: 20128, 3200: 19975, 4000: 20088})

模型结构

在经过多次的代码迭代以后,我将 cnn 打包为了一个 model,这样模型会简洁很多:


模型思路是这样的:首先输入一张图,然后通过 cnn 导出 (112, 10, 128) 的特征图,其中112就是输入到 rnn 的序列长度,10 指的是每一条特征的高度是10像素,将后面 (10, 128) 的特征合并成1280,然后经过一个全连接降维到128维,就得到了 (112, 128) 的特征,输入到 RNN 中,然后经过两层双向 GRU 输出112个字的概率,然后用 CTC loss 去优化模型,得到能够准确识别字符序列的模型。

CNN

CNN 的结构如下图:


理论最大序列长度为46个字符(数字可能为100000,所以是 2*9+3*6+4+4+2=46,对于 CTC 来说,我们最好要输入大于最大长度2倍的序列,才能收敛得比较好。之前我直接卷积到50左右了,然后对于连续字符来说,没有空白能将它们分隔开来,所以收敛效果会差很多。这里的最大序列长度我之前总是算错,因为我用的是 Python2,没有 decode 成 utf-8 的话,一个中文占三个字节。

CNN 的结构由原来的两层卷积一层池化,改为了多层卷积,一层池化的结构,由于卷积层分别是3,4和6层,我称之为 346 结构。

GRU

为什么使用 RNN 呢,这里我举一个很经典的例子:研表究明,汉字的序顺并不定一能影阅响读,比如当你看完这句话后,才发这现里的字全是都乱的。

人眼去阅读一段话的时候,是会顾及到上下文的,不是依次单个字符的识别,因此引入 RNN 去识别上下文能够极大提升模型的准确率。在决赛中,序列有几个地方都是有上下文关系的:

  • 前面一个或两个赋值式一定是 中文=数字; 这样的形式

  • 左括号一定会有右括号

  • 括号的位置是有语法规则的

  • 一定会有一个分式

  • 分式的分子一定是中文

  • 如果只有一个赋值式,那么表达式中的中文一定是赋值式的中文

  • 如果有两个赋值式,赋值式容易看清,表达式不容易看清,那么可以通过赋值式的中文去修正表达式的中文,特别是分子中文被裁掉的时候

其他参数

相比之前初赛的模型,这里进行了一些修改:

  • padding 变为了 same,不然我觉得特征图的高度不够,无法识别分数

  • 增加了 l2 正则化,loss loss 变得更大了,但是准确率变得更高了(添加 l2 的部分包括卷积层的 kernel,BN 层的 gamma 和 beta,以及全连接层的 weights 和 bias)

  • 各个层的初始化变为了 he_uniform,效果比之前好

  • 去掉了 dropout,不清楚影响如何,但是反正有生成器,应该不会出现过拟合的情况

  • 修改过 GRU 的 implementation 为2,原因是希望显卡能加速 GRU 的速度,但是似乎速度还不如设置为0,使用 CPU 来跑,所以又改回来了

l2 正则化的参数直接参考了 Xception 论文的 4.3 节给的参数:

Weight decay: The Inception V3 model uses a weight decay (L2 regularization) rate of 4e-5, which has been carefully tuned for performance on ImageNet. We found this rate to be quite suboptimal for Xception and instead settled for 1e-5.

生成器

为了得到更多的数据,提高模型的泛化能力,我使用了一种很简单的数据扩充办法,那就是根据表达式中的中文随机挑选赋值式,组成新的样本。这里我们取了前 350*256=89600 个样本来生成,用之后的 10240 个样本来做验证集,还有一点零头因为太少就没有用了。

导入数据的时候,先读取运算式的图像,然后按中文导入赋值式的图像到字典中。因为字典中的 key 是无序的,所以我们在字典中存的是 list,列表是有序的。

from collections import defaultdict

cn_imgs = defaultdict(list)
cn_labels = defaultdict(list)
ss_imgs = []
ss_labels = []for i in tqdm(range(n1)):
    ss = df[0][i].decode('utf-8').split(';')
    m = len(ss)-1
    ss_labels.append(ss[-1])
    ss_imgs.append(cv2.imread('crop_split2/%d_%d.png'%(i, 0)).transpose(1, 0, 2))    for j in range(m):
        cn_labels[ss[j][0]].append(ss[j])
        cn_imgs[ss[j][0]].append(cv2.imread('crop_split2/%d_%d.png'%(i, m-


    
j)).transpose(1, 0, 2))

然后实现生成器,这里继承了 keras 里的 Sequence 类:

from keras.utils import Sequenceclass SGen(Sequence):    def __init__(self, batch_size):        self.batch_size = batch_size        self.X_gen = np.zeros((batch_size, width, height, 3), dtype=np.uint8)        self.y_gen = np.zeros((batch_size, n_len), dtype=np.uint8)        self.input_length = np.ones(batch_size)*rnn_length        self.label_length = np.ones(batch_size)*38
    
    def __len__(self):        return 350*256 // self.batch_size    
    def __getitem__(self, idx):        self.X_gen[:] = 0
        for i in range(self.batch_size):            try:
                random_index = random.randint(0, n1-1)                cls = []
                ss = ss_labels[random_index]
                cs = re.findall(ur'[\u4e00-\u9fff]', df[0][random_index].decode('utf-8').split(';')[-1])
                random.shuffle(cs)
                x = 0
                for c in cs:
                    random_index2 = random.randint(0, len(cn_labels[c])-1)                    cls.append(cn_labels[c][random_index2])
                    img = cn_imgs[c][random_index2]
                    w, h, _ = img.shape                    self.X_gen[i, x:x+w, :h] = img
                    x += w+2
                img = ss_imgs[random_index]
                w, h, _ = img.shape                self.X_gen[i, x:x+w, :h] = img                cls.append(ss)

                random_str = u';'.join(cls)                self


    
.y_gen[i,:len(random_str)] = [characters.find(x) for x in random_str]                self.y_gen[i,len(random_str):] = n_class-1
                self.label_length[i] = len(random_str)            except:                pass
        
        return [self.X_gen, self.y_gen, self.input_length, self.label_length], np.ones(self.batch_size)

首先随机取一个表达式,然后用正则表达式找里面的中文,再从{中文:图像数组}的字典中随机取图像,经过之前预处理的方式拼接成一个新的序列。

比如随机取了一个 85882*(河/76020-37023)-铁,然后我们从铁的赋值式中随机取一个,再从河的赋值式中随便取一个,拼起来就能得到下图:

可以看到背景颜色是不同的,但是并不影响模型去识别。

训练

我们训练的策略是先用 Adam() 默认的学习率 1e-3 快速收敛50代,然后用 Adam(1e-4) 跑50代,达到一个不错的 loss,最后用 Adam(1e-5)微调50代,每一代都保存权值,并且把验证集的准确率跑出来。图中的绿色的线 0.9977 就是按上面的方法训练的模型,

当然我们还尝试过先按 1e-3 的学习率训练20代,然后 1e-4 和 1e-5 交替训练2次,每次训练取验证集 loss 最低的结果继续训练,也就是图中红色的线,虽然速度快,但是准确率不够好。

之后我们将全部训练集都用于训练,得到了蓝色的线,效果和绿色差不多。

预测结果

读取测试集的样本,然后用 base_model 进行预测,这个过程很简单,就不讲了。

X = np.zeros((n, width, height, channels), dtype=np.uint8)for i in tqdm(range(n)):
    img = cv2.imread('crop_split2_test/%d.png'%i).transpose(1, 0, 2)
    a, b, _ = img.shape
    X[i, :a, :b] = img

base_model = load_model('model_346_split2_3_%s.h5' % z)
base_model2 = make_parallel(base_model, 4)

y_pred = base_model2.predict(X, batch_size=500, verbose=1)
out = K.get_value(K.ctc_decode(y_pred[:,2:], input_length=np.ones(y_pred.shape[0])*rnn_length)[0][0])[:, :n_len]

输出到文件的部分有一点值得一提,就是如何计算出真实值:

ss = map(decode, out)

vals = []
errs = []
errsid = []for i in tqdm(range(100000)):
    val = '


    
'
    try:
        a = ss[i].split(';')
        s = a[-1]        for x in a[:-1]:
            x, c = x.split('=')
            s = s.replace(x, c+'.0')
        val = '%.2f' % eval(s)    except:#         disp3(i)
        errs.append(ss[i])
        errsid.append(i)
        ss[i] = ''
    
    vals.append(val)    
with open('result_%s.txt' % z, 'w') as f:
    f.write('\n'.join(map(' '.join, list(zip(ss, vals)))).encode('utf-8'))    
print len(errs)print 1-len(errs)/100000.# output220.99978

其中的思路说起来也很简单,就是将表达式中的赋值式中文替换为赋值式的数字,然后直接用 python eval 得到结果,算不出来的直接留空即可。这个0.9977模型的可算率达到了0.99978,也就是说十万个样本里面只有22个样本不可算,当然,实际上还是有一些样本即使可算,也会因为各种原因识别错,比如5和6就是错误的重灾区,某些数字被干扰线切过,导致肉眼都辨认不清等。

模型结果融合

模型结果融合的规则很简单,对所有的结果进行次数统计,先去掉空的结果,然后取最高次数的结果即可,其实就是简单的投票。

import globimport numpy as npfrom collections import Counterdef fun(x):
    c = Counter(x)
    c[' '] = 0
    return c.most_common()[0][0]

ss = [open(fname, 'r').read().split('\n') for fname in glob.glob('result_model*.txt')]
s = np.array(ss).Twith open('


    
result.txt', 'w') as f:
    f.write('\n'.join(map(fun, s)))

将上面 loss 图中的三个模型结果融合以后,最后得到了0.99868的测试集准确率。

其他尝试

不定长图像识别

在比赛刚开始的时候,尝试过将图像的宽度设置为 None,也就是不定长的宽度,但是由于无法解决 reshape 的问题,这个方案被否了。

分别识别

之前尝试过图像切成几块,分别识别,赋值式和表达式的模型分开,考虑到由于无法得到上下文的信息,可能会丢失一定的准确率,做到一半否掉了这个方案。

生成器尝试

我们尝试过写一个生成器,但是由于和官方给的图像差太远,并且实际测试的时候要么是生成的准确率高,官方的准确率低,要么反过来,所以没有投入使用。

上图第一个是官方的图像,后面五个是我们的生成器生成的,可以看到我们的字没有官方的紧凑,等号也不太一样,分式我们的字又太紧凑了。

其他 CNN 模型的尝试

除了自己搭模型,我还尝试过用 ResNet,DenseNet 替换 CNN,然后去训练,但是由于本身这些模型就很大,训练起来速度很慢,然后主要问题又不在模型不够复杂,因为从绘制出来的 loss 曲线来看,虽然前面的 val_loss 一直在抖,但是在第50代学习率下降以后就非常平缓了,这模型是没有过拟合的:

替换 GRU 为 LSTM

在比赛最后尝试过将 GRU 替换为 LSTM,得到的结果是十分类似的,但是提交上去以后准确率有轻微下降(多错了几个样本,可能是运气问题),之前做验证码识别的时候也是替换过,效果差不多,因此没有继续尝试。理论上这个序列长度并没有很长,GRU 和 LSTM 影响不大。

总结

对项目的思考

本项目中,需要注意以下几个重要的点:

  • 数据准备:

  • 深度学习同传统图像处理技术结合,可以达到更好的准确率

  • 文本识别可以构造验证码生成器进行数据增强,增加训练样本数

  • 模型优化:

  • 如何根据项目特点,对模型结构进行调整,如CNN 部分减少池化层使用,等等

  • 为了防止过拟合,在模型中引入 L2 正则化

  • 模型训练:

  • 使用学习率衰减策略,训练模型

  • 对复杂的模型,可以将同一批次输入数据分摊给多个GPU进行计算。


本文初赛、决赛代码 github 地址、初赛数据集获取方式:

关注微信公众号 datayx 然后回复 图像识别  即可获取。



不断更新资源

深度学习、机器学习、数据分析、python

 搜索公众号添加: datayx  

长按图片,识别二维码,点关注


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/Iupky2nR9t
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/21526
 
882 次点击