学人工智能一次竞赛都不打?闹呢?

各位人工智能爱好者,大家好!

由TinyMind发起的

#第一届汉字书法识别挑战赛#

正在火热进行中,比赛才开始2周,已有数只黑马冲进榜单。目前TOP23全部为90分以上了,可谓竞争激烈,高手如林。不是比赛太简单,是大佬们太厉害了啊!

4.24 榜单

本次比赛主要是以学习交流为目的,吸引了不少萌新们报名参赛~虽是入门级别的赛题,对于没动手实战过的同学,还是有些不知所措。为此TinyMind特邀战场中奋勇拼搏的三名前锋,为大家整理了一些经验心得,用不同的解题思路,以启发新手们如何开动,参与到本次书法识别比赛中。

以下为参赛ID:真的学不会 的经验分享

汉字书法识别入门

前段时间参加了一次TinyMind举办的汉字书法识别挑战赛,说是挑战赛其实就是一场练习赛。为一些刚刚入门的同学和没有比赛经验的同学提供了一个探索图像识别领域的平台。我目前是暂列榜首(没想到转眼就被超越了-。-),所以把自己的思路和想法稍微做一个分享,给有需要的人提供一个base line。

先来看数据集~~

100个汉字的训练集

10000张书法图片的测试集

上面的训练集总共有100个汉字,每一个汉字都有400张不同字体的图片,数据量上来看算是一个比较小的数据集。

等等,看到的确定是汉字吗,第一眼望过去我是真的emmmmm.....甲骨文,篆体各种字体都冒出来了。先喝口水冷静一下,仔细看一看发现图片都是gray的。想了一想突然觉得这个和mnist并没有太大的区别只是字体更加复杂一些,可能要用稍微深一点的网络来训练。

图片看完了,那么开始撸代码了。分析终究是分析,还是实践才能说明一切。

数据集划分

竞赛中只给了train和test,所以需要自己手动划分一个val来做模型训练的验证测试。在这里简单说明一下经常用的两种划分数据集的方法。

本地划分

内存划分

本地划分:图片是按照文件夹分类的,所以只需要从每个文件夹中按ratio抽取部分图片到val中即可,当然不要忘记了shuffle。

内存划分:把所有图片和标签读进内存中,存为list或者array然后shuffle后按长度划分。前提是把数据读进去内存不会爆炸掉。内存划分只适合小型数据集,不然会Boom!!!

注:划分数据集的时候一定要打乱数据,shuffle很重要!!!

1defmove_ratio(data_list, original_str, replace_str):

2forxindata_list:

3fromImage = Image.open(x)

4x = x.replace(original_str, replace_str)

5fromImage.save(x)

注:这里只给出部分代码,文章最下面github有完整链接。

1fordin$(ls datadir);do

2forfin$(ls datadir/$d| shuf |head -n100);do

3mkdir -p valid_dir/$d/

4mv datadir/$d/$f valid_dir/$d/;

5done;

6done

注:这里引用dwSun的linux shell脚本,如果想用简单脚本实现也可以采用他的代码~

模型建立与数据预处理

对于CNN网络来说,大的数据集对应大的训练样本,如果小的数据集想要用深层次的网络来训练的话,那么必不可少的一步就是数据增强。

数据增强的大部分方法,所有深度学习框架都已经封装好了。这里我采用的是keras自带的数据增强方法。

由于汉字是具有笔画顺序的,所以做了翻转以后训练的效果不是很好。这里就做了一个宽度和高度的偏移,由于给的数据集图片长宽不是固定的而且字体的内容也是有长有短。所以用这两种增强方式可以提高模型的准确率,结果测试这两种方式还是有效的。

数据处理完了,那么下面就是我们可爱的CNN网络模型了

cnn一把梭

嗯,就是干。

1# bn + prelu

2defbn_prelu(x):

3x = BatchNormalization()(x)

4x = PReLU()(x)

5returnx

6# build baseline model

7defbuild_model(out_dims, input_shape=(128,128,1)):

8inputs_dim = Input(input_shape)

9x = Conv2D(32, (3,3), strides=(2,2), padding='valid')(inputs_dim)

10x = bn_prelu(x)

11x = Conv2D(32, (3,3), strides=(1,1), padding='valid')(x)

12x = bn_prelu(x)

13x = MaxPool2D(pool_size=(2,2))(x)

14x = Conv2D(64, (3,3), strides=(1,1), padding='valid')(x)

15x = bn_prelu(x)

16x = Conv2D(64, (3,3), strides=(1,1), padding='valid')(x)

17x = bn_prelu(x)

18x = MaxPool2D(pool_size=(2,2))(x)

19x = Conv2D(128, (3,3), strides=(1,1), padding='valid')(x)

20x = bn_prelu(x)

21x = MaxPool2D(pool_size=(2,2))(x)

22x = Conv2D(128, (3,3), strides=(1,1), padding='valid')(x)

23x = bn_prelu(x)

24x = AveragePooling2D(pool_size=(2,2))(x)

25x_flat = Flatten()(x)

26fc1 = Dense(512)(x_flat)

27fc1 = bn_prelu(fc1)

28dp_1 = Dropout(0.3)(fc1)

29fc2 = Dense(out_dims)(dp_1)

30fc2 = Activation('softmax')(fc2)

31model = Model(inputs=inputs_dim, outputs=fc2)

32returnmodel

这里用了6个简单的卷积层,和PRelu+bn层。

下面是一个比较大的模型ResNet50,模型是已经merge在了keras的applications中,可以直接用。不过需要调整分类层。

1defresnet50_100(feat_dims, out_dims):

2# resnett50 only have a input_shape=(128, 128, 3), if use resnet we must change

3# shape at least shape=(197, 197, 1)

4resnet_base_model = ResNet50(include_top=False, weights=None, input_shape=(128,128,1))

5# get output of original resnet50

6x = resnet_base_model.get_layer('avg_pool').output

7x = Flatten()(x)

8fc = Dense(feat_dims)(x)

9x = bn_prelu(fc)

10x = Dropout(0.5)(x)

11x = Dense(out_dims)(x)

12x = Activation("softmax")(x)

13# buid myself model

14input_shape = resnet_base_model.input

15output_shape = x

16resnet50_100_model = Model(inputs=input_shape, outputs=output_shape)

17returnresnet50_100_model

好了,炼丹炉有了接下来就是你懂的。

训练模型

训练模型和调参真的是一个技术活,这里我跑了共40个epoch。思路只有一个那就是先把train的数据跑到loss下降并且先过拟合再说。只要过拟合了后面的一切都好调整了,如果训练数据都不能到过拟合或者99以上那么要仔细想想数据量够不够和模型的选择了。

loss

acc

可以很清楚的看出来,训练数据集已经过拟合了。我用的优化器是sgd,学习率设置的是lr=0.01。val_acc可以跑到了0.94左右,这是一个比较正常的训练水平。还可以进一步的提高。

提高方法

数据增强:采取其他的数据增强方法进一步扩大训练数据,这里可以采用GAN来生成近似于真实图片的数据。

衰减学习率:当到达一定epoch的时候,loss不再下降了这个时候可以通过减小学习率的方法进一步训练。

模型融合:模型融合的方法在大部分数据集上都会有所提高,这个是最常用的一种竞赛方式。

以上就是我自己做的流程和思路,提交结果和评测的代码写在我的github上面了,有兴趣参加比赛练手的同学可以参考一下。

github地址:https://github.com/FlyEgle/chinese_font_recognition/

好多小伙伴是从开发或者是其他工程上转到AI的,所以下面我给有需要的同学列举出一些必要的基础知识点.

基础知识

数学:线性代数和概率论是必须要会的,而且基本的概念和计算都要懂。可以把高数,线性代数和概率论看一遍,这里推荐李航的统计学习方法。

图像处理:如果是做图像方面的小伙伴,那么需要把冈萨雷斯的图像处理那本巨作看一遍,需要懂基本的图像概念和基本方法。

机器学习:周志华的西瓜书

深度学习:

如果能把这几本书完全吃透那也很厉害了,当然学习知识点的途径还有很多。

加油!!

以下为参赛ID:Link 的经验分享

深度学习入门指南:从零开始TinyMind汉字书法识别

环境搭建

数据导入

启动网络

环境搭建:

对入门来说,最容易的还是在windows下进行开发。而且现在各种深度学习架构大都支持windows,因此如果只是入门深度学习,最好还是从windows开始。不过因为github上提交的代码全都运行在linux环境下,因此希望大家最终能转向linux下,话不多说,现在开始。

我们选用的深度学习架构是pytorch, 相比于tensorflow,pytorch更加简单易用,而且符合python的编程习惯,官网的支持也足够完善。

环境搭建步骤

安装Anaconda, 装python3.6版本的,至于为啥用python3这都2018年了,就别用上古版本了

安装pycharm,将pycharm的解释器改为anaconda安装目录下的python。当然用别的IDE也可以,但是我习惯用pycahrm了,如果大家用别的IDE这步另当别论

安装深度学习架构pytorch, 到了最重要的步骤了,如果没有英伟达显卡,或者显卡不支持请忽略1-3步

安装英伟达显卡驱动

安装CUDA

安装Cuddn

安装pytorch gpu版 (没有显卡的装cpu版)具体方法参见知乎这篇文章 https://zhuanlan.zhihu.com/p/26871672 选择自己对应的版本、系统、cuda版本,按照命令直接装就可以

数据导入

数据下载在TinyMind的比赛网站 http://www.tinymind.cn/competitions/41 下载解压后是两部分,分别是train和test1,其中train是训练集,test1是用来提交评分的测试集 为了导入图片数据,需要调用opencv,没装opencv的话就先装opencv

1condainstall -c https://conda.binstar.org/menpo opencv

1importos

2importnumpyasnp

3importtorch

4importtorch.utils.dataasdata

5importcv2

6fromPILimportImage

7fromtqdmimporttqdm

8trainpath ='E:\\Code\\TMD1st\\train\\'#这是我的储存路径,windows下的路径是需要用\\隔开的,linux是反斜杠/

9testpath ='E:\\Code\\TMD1st\\test1\\'

10words = os.listdir(trainpath)# 按时间排序 从早到晚

11category_number = len(words)# 一共有多少个字

12img_size = (256,256)#将图片大小统一设定为这个值

13defloadOneWord(order):

14path = trainpath + words[order] +'\\'

15files = os.listdir(path)

16datas = []

17forfileinfiles:

18file = path + file

19img = np.asarray(Image.open(file))

20img = cv2.resize(img, img_size)

21datas.append(img)

22datas = np.array(datas)

23labels = np.zeros([len(datas), len(words)], dtype=np.uint8)

24labels[:, order] =1

25returndatas, labels

26deftransData():#将所有数据转存,以后就不用每次都从原始数据读取了

27num = len(words)

28datas = np.array([], dtype=np.uint8)

29datas.shape =-1,256,256

30labels = np.array([], dtype=np.uint8)

31labels.shape =-1,100

32forkinrange(num):

33data, label = loadOneWord(k)

34datas = np.append(datas, data, axis=)

35labels = np.append(labels, label, axis=)

36print('loading', k)

37np.save('data.npy', datas)#将数据和标签分别存为data和label

38np.save('label.npy', labels)

将转存完的结果读出来看一下

1if__name__ =='__main__':

2datas = np.load('data.npy')

3labels = np.load('label.npy')

4index = np.arange(,len(datas),1, dtype=np.int)

5print(datas.shape, labels.shape)

(40000, 256, 256) (40000, 100)

我是将40000个图像的label按照one-hot编码存的,这么干其实浪费空间,但是反正也没几兆,就懒得改了,index那一行就是专为将ong-hot转label

这才将数据转存,为了训练时给pytorch使用,最方便的方法是使用pytorch做好的loader工具,为此需要实现自己的data.Dataset。只需继承data.Dataset,并且重写__getitem__和__len__两个方法就可以。

1classTrainSet(data.Dataset):

2def__init__(self, eval=False):

3datas = np.load('data.npy')#装载

4labels = np.load('label.npy')

5index = np.arange(, len(datas),1, dtype=np.int)#换one-hot为label

6np.random.seed(123)

7np.random.shuffle(index)

8ifeval:#如果eval为真,就取10%作为验证集,设定随机数种子是为了每次取出来的都是固定的10%,以免将验证集用于训练

9index = index[:int(len(datas) *.1)]

10else:

11index = index[int(len(datas) *.1):]

12self.data = datas[index]

13self.label = labels[index]

14np.random.seed()

15def__getitem__(self, index):

16returntorch.from_numpy(self.data[index]), \

17torch.from_numpy(self.label[index])

18def__len__(self):

19returnlen(self.data)

启动网络

无论网络结构如何,用网络进行训练的整个过程是相同的

1importtorch

2importtorch.optimasoptim

3fromtorch.autogradimportVariable

4importtorch.nnasnn

5importdata

6importtorch.nn.functionalasF

7n_epoch, batch_size =25,8# 设置遍历次数及每个batch的大小

8trainset = data.TrainSet(eval=False)#实例化上面定义的数据集对象

9trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)#用trainset实例化loader

10evalset = data.TrainSet(eval=True)#验证集

11evalloader = torch.utils.data.DataLoader(evalset, batch_size=batch_size, shuffle=True)

12net = Net()# 实例化模型

13iftorch.cuda.is_available():# 将模型移到GPU上

14net.cuda()

15criterion = nn.CrossEntropyLoss()#损失函数使用交叉熵

16optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=1e-1, weight_decay=1e-4)#优化器使用SGD 学习率1e-3

17deftrain(epoch):

18net.train()# 经模型切换到训练模式

19correct =

20sum =

21forbatch_index, (datas, labels)inenumerate(trainloader,):#从loader装载数据

22labels = labels.max(1)[1]

23datas = Variable(datas).float()

24datas = datas.view(-1,1,256,256)

25labels = Variable(labels).long()

26iftorch.cuda.is_available():#数据转移到GPU

27datas = datas.cuda()

28labels = labels.cuda()

29optimizer.zero_grad()# 每次前项计算之前,将优化器梯度清零

30outputs = net(datas)# 前项计算

31loss = criterion(outputs, labels)# 根据结果和label计算损失函数

32loss.backward()# 做反向传播

33optimizer.step()# 用优化器进行一次更新

34pred_choice = outputs.data.max(1)[1]# 前向输出计算最大的一个作为最可能的输出

35correct += pred_choice.eq(labels.data).cpu().sum()# 统计正确个数

36sum += len(labels)# 总数

37# 输出每次计算的信息

38print('batch_index: [%d/%d]'% (batch_index, len(trainloader)),

39'Train epoch: [%d]'% (epoch),

40# 'acc:%.4f p:%.4f r:%.4f F1:%.4f' % (acc, p, r, F1),

41'correct/sum:%d/%d, %.4f'% (correct, sum, correct / sum))

42defeval(epoch):# 用验证集做类似过程,只是不计算梯度、不更新参数

43net.eval()

44correct =

45sum =

46forbatch_index, (datas, labels)inenumerate(evalloader,):

47labels = labels.max(1)[1]

48datas = Variable(datas).cuda().float()

49datas = datas.view(-1,1,256,256)

50labels = Variable(labels).cuda().long()

51# optimizer.zero_grad()

52outputs = net(datas)

53# loss = criterion(outputs, labels)

54# loss.backward()

55# optimizer.step()

56pred_choice = outputs.data.max(1)[1]

57correct += pred_choice.eq(labels.data).cpu().sum()

58sum += len(labels)

59print('batch_index: [%d/%d]'% (batch_index, len(evalloader)),

60'Eval epoch: [%d]'% (epoch),

61# 'acc:%.4f p:%.4f r:%.4f F1:%.4f' % (acc, p, r, F1),

62'correct/sum:%d/%d, %.4f'% (correct, sum, correct / sum))

63if__name__ =='__main__':

64forepochinrange(n_epoch):

65train(epoch)

66eval(epoch)

如此,我们就完成了从原始数据制作dataset送入loader并且启动网络的所有代码。 等等,我们忘记了最重要的部分,我们没有定义网络的结构。 Net这里,这是一个继承自nn.Moudule的类,只要在这个类中定义网络的前向计算即可,反向计算会由pytorch自动实现。

为了简单起见,我们只举一个简单的例子,这个网络是随便写的,完全没有任何合理性的考虑,但至少能开始训练了。

1classnet(nn.Module):

2def__init__(self):

3super(net,self).__init__()

4self.pool = nn.MaxPool2d(2)

5self.drop = nn.Dropout(p=.5)

6self.conv1 = nn.Conv2d(1,32,7, stride=2, padding=3)

7self.norm1 = nn.BatchNorm2d(32)

8self.conv2 = nn.Conv2d(32,32,3, stride=1, padding=1)

9self.norm2 = nn.BatchNorm2d(32)

10self.conv3 = nn.Conv2d(32,64,3, stride=1, padding=1)

11self.norm3 = nn.BatchNorm2d(64)

12# Sequential 是连续操作的写法

13self.convs = nn.Sequential(nn.Conv2d(64,128,3, stride=1, padding=1),

14nn.BatchNorm2d(128),

15nn.ReLU(),

16nn.Conv2d(128,128,3, stride=1, padding=1),

17nn.BatchNorm2d(128),

18nn.ReLU(),

19)

20self.out_layers = nn.Sequential(nn.Linear(128*8*8,1024),

21nn.BatchNorm1d(1024),

22nn.ReLU(),

23nn.Linear(1024,256),

24nn.BatchNorm1d(256),

25nn.ReLU(),

26nn.Linear(256,100),

27nn.BatchNorm1d(100),

28nn.ReLU(),

29)

30defforward(self, x):

31x = F.relu(self.norm1(self.conv1(x)))# 卷积 BN ReLU

32x =self.pool(x)# 池化

33x = F.relu(self.norm2(self.conv2(x)))# 卷积 BN ReLU

34x = F.relu(self.norm3(self.conv3(x)))# 卷积 BN ReLU

35x =self.pool(x)

36x =self.convs(x)# 连续操作,里面是 conv -> BN -> ReLU -> conv -> BN -> ReLU

37x =self.pool(x)

38x = x.view(-1,128*8*8)# 将图像拉直为向量

39x =self.drop(x)

40x =self.out_layers(x)

41returnx

这样,代码就完整了,运行开始以后我么你就恩能够看到训练正确率从0慢慢的向上爬。当然,这个网络是随意写的,性能肯定极其的差,但至少举了一个栗子。

我们都知道,深度学习也叫炼丹。所以接下来的活便是研究拜读各个大牛级炼丹师的的炼丹秘籍(论文),学习人家先进的炼丹手法(trick),把我们的栗子给炼成金丹。

五年炼丹,三年悟道,炼丹一道,非大毅力大智慧者不可成。吾等当昼夜苦修,方有机缘窥得一丝丹道真谛,与诸君共勉。

源代码

链接地址:https://github.com/Link2Link/TinyMind-start-with-0

以下为参赛ID:microfat_htu 的经验分享

前一段时间在用迁移学习实现图像分类Github,正好赶上这次书法识别比赛,就想尝试用迁移学习的方法实现书法图像分类。本来没有抱太大希望,因为根据迁移学习理论,训练源域模型的数据应与训练目标域的数据有相似的特征分布,然而,ILSVRC数据集中并没有汉字符号类别,所以,期望的最好结果是能够收敛。但,事实出乎意料,不仅收敛,而且得到99.01%的测试准确率(看来我是低估深度模型的特征提取能力了)。

“link”和“真的学不会”同学的总结可以说是非常全面,由于时间能力有限,我在这只是简要介绍一下我的大致思路,供大家参考。

和“真的学不会”同学使用的keras不同,我使用的是slim,keras应用更广,slim更专一。在slim的readme上,提供了几乎所有目前主流深度模型的预训练模型,你可以选择任意模型下载,供你的项目使用。

首先,在Github的代码基础上,针对各自环境,对路径、类别数量、超参数(Github上的即比赛中我所使用的超参数)进行设置;另外,数据增广方法也需要修改(./preprocessing/inception_preprocessing.py),将随机翻转、剪裁取消,否则训练会特别慢;最后,在两个terminal下训练、验证,训练过程中会每隔一定时间(可自定义)输出checkpoint,模型验证程序会自动验证该checkpoint。截图如下,是训练,是验证。

训练过程如下图所示:

从图中可以看到,loss并未完全收敛,还有继续下降的趋势,因而性能应该还有提升空间。

Note:

建议训练样本:验证样本=39:1,即,每类提取10幅图像构成验证集,以最大限度提高训练样本数;

我大概跑了100个epoch,选取比较新的checkpoint在测试集上测试,从测试结果上看,准确率相差不大,应该是收敛了,但继续跑的话应该还会有提升;

除了“真的学不会”同学推荐的书籍,我还比较推荐的是微博大v“爱可可-爱生活”,这个由北邮陈老师维护的公众号我关注了两年,受益颇多;

还有一个训练监控工具——Hyperdash,它可以实现网页、iOS、安卓端的训练实时监控,具体操作可以看他们官网(五一假期快来了,这个工具应该能帮到你)。

最后,祝大家都能在此次比赛中有所收获。

源代码

链接地址:https://github.com/MacwinWin/Deep-Model-Transfer.git

感谢以上三位同学的分享,希望能给还在围观的同学一些参考借鉴。

本次竞赛持续到6月5日,欢迎感兴趣的同学报名参赛~共同进步!!

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180425A1MJX800?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券