2022DCIC基于文本字符的验证码识别竞赛—参赛总结
摘要
比赛网址
:https://www.dcic-china.com/competitions/10023赛题任务
:本次大赛以已标记字符信息的实例字符验证码图像数据为训练样本,参赛选手需基于提供的样本构建模型,对测试集中的字符验证码图像进行识别,提取有效的字符信息。训练数据集不局限于提供的数据,可以加入公开的数据集。数据分析
:简单分析知道,数据集是标准的4个字符的验证码,单看各个字符的占比,还是比较均衡的,但仔细观察,可以发现官方使用了三种类型的验证码,且占比不均衡。(比赛群大佬找到的leak:验证码生成时的时间戳可以划分这三种类型的验证码、测试集中是按顺序生成的)模型选择
:初期简单建立了baseline后,timm中全部模型都跑了一遍,选择了性价比最好的resnetrs50
模型来做进一步迭代优化。后期又尝试了群里大佬一直吹的tf_efficientnet_b7
模型,以及更改模型结构等等。数据增强
:Resize((256,512))、RandomRotation((-5,5))、aug_lib.TrivialAugment()。期间试过字体扭曲、亮度/饱和度修改等等,都不怎么提升。训练方案
:第一使用MultiLabelSoftMarginLoss
损失函数、Adam
优化器、StepLR
学习率衰减、resnetrs50
模型5折交叉训练;第二改用CrossEntropyLoss
损失函数、tf_efficientnet_b7
模型并且设置4个全连接层和Dropout
层5折交叉训练;第三全量训练。模型融合
:两个5折交叉训练的模型单独加权平均得出预测结果,全量训练得到的3个模型则平均后得出预测结果,最后3个结果进行单字符投票得出最终预测结果。期间试过TTA
,提升不明显(可能是我的姿势不对)。赛题得分
:A榜最终得分:0.9732,排名:36/163;B榜:0.8069,22/48。总结结论
:“有点水”的比赛,A/B榜测试集不同分布,shake幅度让很多大佬都难以接受,比赛群都吵到炸开锅了,官方也没什么回应。总结一句话,比赛体验极差。但A榜前排大佬的方案还是学习到了。
赛题任务
模型选择
依然是使用timm
模块下的预训练模型,适当修改或者增加一些模块。具体模型代码如下:
from torch.nn import Module
from torch.nn import Sequential
from torch.nn import Linear
from torch.nn import Dropout
try:
import timm
except:
! pip install timm
import timm
#resnet26d,0.68
#resnetrs50,0.76
#gluon_resnet152_v1d,0.76
class TimmModels(Module):
def __init__(self, model_name='resnetrs50',pretrained=True, num_classes=62*4):
super(TimmModels, self).__init__()
self.m = timm.create_model(model_name,pretrained=pretrained)
model_list = list(self.m.children())
model_list[-1] = Linear(
in_features=model_list[-1].in_features,
out_features=num_classes,
bias=True
)
# model_list.insert(-1,Dropout(0.5))
self.m = Sequential(*model_list)
def forward(self, image):
out = self.m(image)
return out
class TimmModels_4fc(Module):
def __init__(self, model_name='resnetrs50',pretrained=True, num_classes=62):
super(TimmModels_4fc, self).__init__()
self.m = timm.create_model(model_name,pretrained=pretrained)
model_list = list(self.m.children())
in_features = model_list[-1].in_features
self.m = Sequential(*model_list[:-1])
self.fc1 = Sequential(Dropout(0.25),Linear(in_features=in_features, out_features=num_classes, bias=True))
self.fc2 = Sequential(Dropout(0.25),Linear(in_features=in_features, out_features=num_classes, bias=True))
self.fc3 = Sequential(Dropout(0.25),Linear(in_features=in_features, out_features=num_classes, bias=True))
self.fc4 = Sequential(Dropout(0.25),Linear(in_features=in_features, out_features=num_classes, bias=True))
def forward(self, image):
out = self.m(image)
y1 = self.fc1(out)
y2 = self.fc2(out)
y3 = self.fc3(out)
y4 = self.fc4(out)
return y1,y2,y3,y4
训练方案
- 方案一:模型:resnetrs50 1fc;损失函数:MultiLabelSoftMarginLoss;优化器:Adam;学习率衰减:StepLR;batch_size:32;5折交叉训练,每折训练20个epoch,保存验证集准确率最高的模型。
- 方案二:模型:tf_efficientnet_b7 4fc;损失函数:CrossEntropyLoss;优化器:Adam;学习率衰减:StepLR;batch_size:10;5折交叉训练,每折训练30个epoch,保存验证集准确率最高的模型。
- 方案三:模型:tf_efficientnet_b7 4fc;损失函数:CrossEntropyLoss;优化器:Adam;学习率衰减:StepLR;batch_size:10;全量训练38个epoch,保存准确率最高的模型、loss最小的模型和最后一个epoch的模型。
模型融合
- 方案一得到的5个模型分别预测测试集,然后加权平均得出结果一;
- 方案二得到的5个模型分别预测测试集,然后加权平均得出结果二;
- 方案三得到的3个模型分别预测测试集,然后算术平均得出结果三;
- 最后,对三个结果进行单字符投票(若三个都不一样,则用方案二的预测值),得到最终的预测结果。
具体代码如下:
import os
import string
from collections import defaultdict
import torch
import pandas as pd
from tqdm import tqdm
from torch.utils.data import DataLoader
from torchvision import transforms
from torch.autograd import Variable
from utils import one_hot_decode
from dataset import ImageDataSet
from models import TimmModels,TimmModels_4fc
def main():
all_str = {v: k for k, v in enumerate(list(string.digits + string.ascii_letters))}
# test_path='../input/2022dcic-capture/B_dataset/B榜/'
test_path='../input/2022dcic-capture/test_dataset/test_dataset/'
batch_size=128
img_width = 512
img_height = 256
num_workers=os.cpu_count()
test_transform = transforms.Compose([
transforms.Resize((img_height,img_width)),
transforms.ToTensor(),
])
test_dataset=ImageDataSet(test_path,test_transform,is_train=False)
test_dataloader=DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=num_workers)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# resnetrs50 1fc 5fold 加权融合
data=defaultdict(int)
acc_map={1:0.9404,2:0.9461,3:0.9394,4:0.9314,5:0.9415}
with torch.no_grad():
for idx in range(5):
model=TimmModels(model_name='resnetrs50',pretrained=False).to(device)
model.load_state_dict(torch.load(f'../input/2022dcic-capture/5fold/resnetrs50_kfold{idx+1}.pth'))
model.eval()
for i, (images,stem) in tqdm(enumerate(test_dataloader),total=len(test_dataloader),desc=f'resnetrs50_kfold{idx+1}'):
images = Variable(images).to(device)
predict_labels = model(images / 255).detach()
for each in range(len(stem)):
data[stem[each]]+=predict_labels[each] * acc_map[idx+1]
submit=[]
for num,pred in tqdm(data.items()):
p=pred / sum(acc_map.values())
predict=one_hot_decode(p.view(4,62).max(1)[1])
submit.append({'num':int(num),'tag':predict})
df_resnet_5fold=pd.DataFrame(submit)
# tf_efficientnet_b7 4fc 5fold 加权融合
data=defaultdict(int)
acc_map={1:0.9608,2:0.9624,3:0.9565,4:0.96,5:0.9607}
with torch.no_grad():
for idx in range(5):
model=TimmModels_4fc(model_name='tf_efficientnet_b7',pretrained=False).to(device)
model.load_state_dict(torch.load(f'../input/2022dcic-capture/b7_5fold/tf_efficientnet_b7_kfold{idx+1}.pth'))
model.eval()
for i, (images,stem) in tqdm(enumerate(test_dataloader),total=len(test_dataloader),desc=f'tf_efficientnet_b7_kfold{idx+1}'):
images = Variable(images).to(device)
pred1,pred2,pred3,pred4=model(images / 255)
predict=torch.hstack((pred1,pred2,pred3,pred4))
for each in range(len(stem)):
data[stem[each]]+=predict[each] * acc_map[idx+1]
submit=[]
for num,pred in tqdm(data.items()):
p=pred / sum(acc_map.values())
predict=one_hot_decode(p.view(4,62).max(1)[1])
submit.append({'num':int(num),'tag':predict})
df_b7_5fold=pd.DataFrame(submit)
# best_acc_epoch:33,best_acc:98.71%
# best_loss_epoch:37,best_loss:0.06261
# the_last_epoch:38
# all train 算术平均融合
data=defaultdict(int)
with torch.no_grad():
for name in ['train_acc_best','train_loss_best','the_last_epoch']:
model=TimmModels_4fc(model_name='tf_efficientnet_b7',pretrained=False).to(device)
model.load_state_dict(torch.load(f'../input/2022dcic-capture/b7_all_train/{name}.pth'))
model.eval()
for i, (images,stem) in tqdm(enumerate(test_dataloader),total=len(test_dataloader),desc=name):
images = Variable(images).to(device)
pred1,pred2,pred3,pred4=model(images / 255)
predict=torch.hstack((pred1,pred2,pred3,pred4))
for each in range(len(stem)):
data[stem[each]]+=predict[each]
submit=[]
for num,pred in tqdm(data.items()):
p=pred / 3
predict=one_hot_decode(p.view(4,62).max(1)[1])
submit.append({'num':int(num),'tag':predict})
df_b7_all_train=pd.DataFrame(submit)
# 最后,三个结果单字符投票,得出最终的结果
def ronghe(x):
tag_resnet_5folds=x['tag_resnet_5fold']
tag_b7_5folds=x['tag_b7_5fold']
tag_b7_all_trains=x['tag_b7_all_train']
pred=''
for i in range(4):
p=defaultdict(int)
tag_resnet_5fold = tag_resnet_5folds[i]
tag_b7_5fold = tag_b7_5folds[i]
tag_b7_all_train = tag_b7_all_trains[i]
p[tag_resnet_5fold]+=1
p[tag_b7_5fold]+=1
p[tag_b7_all_train]+=1
sort_p=sorted(p.items(),key=lambda x:x[1],reverse=True)
if len(sort_p) <= 2:
pred+=sort_p[0][0]
else:
# 三个都不一样的话用<tf_efficientnet_b7 4fc 5fold 加权融合>的结果
pred+=tag_b7_5fold
return pred
df=(
df_resnet_5fold.rename(columns={'tag':'tag_resnet_5fold'})
.merge(df_b7_5fold.rename(columns={'tag':'tag_b7_5fold'}),on='num',how='left')
.merge(df_b7_all_train.rename(columns={'tag':'tag_b7_all_train'}),on='num',how='left')
)
df['tag']=df.apply(ronghe,axis=1)
df[['num','tag']].sort_values(by='num').reset_index(drop=True).to_csv('submit.csv',index=False)
if __name__ == '__main__':
main()
赛题得分
- A榜最终得分:0.9732,排名:36/163
2. B榜最终得分:0.8069,排名:22/48
前排分享
- https://github.com/xiaoxiaokuaile/2022DCIC_OCR
- https://www.kaggle.com/code/wptouxx/dcic-ocr-vit-final/notebook
以上即为本文的全部内容,若需要全部源代码的,请关注公众号《Python王者之路》,回复关键词:20220409
,即可获取。
写在最后
在看到这个比赛题目时,我立马就报名了。
一开始我直接套用我之前写的python生成验证码→处理验证码→建立CNN模型训练→测试模型准确率→识别验证码的代码,然后提交得分只有0.68+
然后我换了一下模型、调整一下Resize尺寸、得分为0.88+
接着将模型改为性价比较好的resnetrs50,即本文的方案一,得分为0.95+
这个时候感觉到瓶颈了我基本已经放弃了,到了最后几天,看到群里的大佬一直吹b4、b7模型,越大越好,然后我就抱着试一试的想法
将模型改为大佬们都在用的tf_efficientnet_b7,即本文的方案二,得分为0.97+
因为用的是kaggle平台上的资源,所以模型不能用太大的,而且把batch_size从32降到了10才勉强跑得动
最后再全量训练了,即本文的方案三,可惜有提交次数限制,未能验证模型得分
虽然没能追上前排0.99+的大佬们,但是我已经尽力了。还有,虽然A/B榜shake幅度难以接受,硬生生把A榜0.99+的大佬掉到0.8左右,但是好在有些大佬分享了他们的方案,还是有学习到东西,感谢感谢~
最后,还是那句话,希望疫情早日结束,一切恢复正常吧!!!