百度网盘AI大赛-文档图像方向识别赛第5名方案
⭐ ⭐ ⭐ 欢迎点个小小的Fork支持!⭐ ⭐ ⭐
本项目基于开源项目的流程,提出想法进行改进,荣获B榜第5,A榜第13。
一、赛题任务
人们在使用移动设备进行文档扫描、证照拍摄等过程中,有时受限于使用和拍摄场景,人们会将拍摄设备旋转后拍摄,导致得到的图片也是不同方向的。此时,标准的文档扫描与识别的流程并没有办法正常帮助用户处理文件。
因此,为了便于用户使用,需要各位选手通过技术对给定文档图像进行处理,识别并返回给定图像对比正向原图顺时针旋转的方向角度
二、数据集介绍(详见prepare_data.ipynb)
本次比赛不提供训练数据集,仅提供A榜测试集,B榜测试集不做公开, 因此本项目收集几个公开数据集,并对部分数据集进行三种旋转(90度,180度,270度)变换,并打上对应的分类角度标签,构成本项目模型最终采用的数据集,其中对每个公开数据集分别进行9:1的比例随机划分将数据集分成了训练集和验证集。接下来将介绍本项目收集的几个公开数据集。
2.1 公开数据集1
该数据集来源于十进制到二进制。该数据集基于 ICDAR2019-ArT、 XFUND 和 ICDAR2015 三个公开数据集构造了一个小规模含文字图像方向分类数据集。考虑到原始图片的分辨率较高,模型训练时间较长,该数据集已将所有数据预先进行了缩放处理,在保持长宽比不变的前提下,将短边缩放到了384。然后将数据进行顺时针旋转处理,分别生成90度、180度和270度的合成数据。其中,将 ICDAR2019-ArT 和 XFUND 生成的41460张数据按照 9:1 的比例随机划分成了训练集和验证集,图片路径及标签信息见PaddleClas/dataset/text_image_orientation/train_list.txt及PaddleClas/dataset/text_image_orientation/test_list.txt。图片示例如下:
2.2 公开数据集2
该数据集来源于被褐怀玉上传的VOC2012。因为测试集中包含自然图像,而公开数据集1主要是文档图像,因此该数据集基于用于目标检测的VOC2012数据集,并将该数据集图片分别旋转90度、180度和270度,得到最终的扩充数据集,此时数据集包含图片68500张,然后按照 9:1 的比例随机划分成了训练集和验证集,图片路径及标签信息见PaddleClas/dataset/text_image_orientation/jpeg_train_list.txt及PaddleClas/dataset/text_image_orientation/jpeg_test_list.txt。
2.3 公开数据集3
该数据集来源于鸿飞往里上传的文档增强数据集。由于公开数据集2主要增强自然图像的旋转角度识别,为了使数据集更均衡,该数据集对文档图像数据进行扩充和增强,因此该数据集进一步将本身包含的文档图片分别旋转90度、180度和270度,得到最终的扩充数据集,此时数据集包含图片4800张,然后按照 9:1 的比例随机划分成了训练集和验证集,图片路径及标签信息见PaddleClas/dataset/text_image_orientation/gt_blur_train_list.txt及PaddleClas/dataset/text_image_orientation/gt_blur_test_list.txt。
2.4 公开数据集4
该数据集来源于张牙舞爪上传的水印消除比赛标签图。由于公开数据集2主要增强自然图像的旋转角度识别,为了使数据集更均衡,该数据集继续对文档图像数据进行扩充和增强,因此该数据集进一步将本身包含的文档图片分别旋转90度、180度和270度,得到最终的扩充数据集,此时数据集包含图片7368张,然后按照 9:1 的比例随机划分成了训练集和验证集,图片路径及标签信息见PaddleClas/dataset/text_image_orientation/mask_train_list.txt及PaddleClas/dataset/text_image_orientation/mask_test_list.txt。
2.5 数据集构造
综合以上四个数据集,得到本文最终的数据集,其中包含图片125964张,然后按照 9:1 的比例随机划分成了训练集(109910张图片)和验证集(16054),图片路径及标签信息见PaddleClas/dataset/text_image_orientation/new_train_list.txt及PaddleClas/dataset/text_image_orientation/new_test_list.txt。
fork之后的第一次运行,用户需要按照prepare_data.ipynb流程执行挂载数据集、解压缩、旋转处理等操作获得最终的训练集及验证集图片及信息文件(包含每张图片路径和标签)。
三、调优历程
本节简述本项目曾做过的产生主要影响的几种尝试。
3.1 使用ssld预训练pplcnet_x1_0模型并采用默认训练配置+公共数据集1
由于是一卡运行,学习率由0.4调整为0.1。在A榜的结果为分数:0.625,用时:0.00875s。模型文件大小:6.5MB。
3.2 使用ssld预训练pplcnet_x1_0模型并采用默认训练配置+公共数据集1+公共数据集2
公共数据集1主要是文档图像,缺乏自然图像,引入公共数据集2。
在A榜的结果为分数:0.662,用时:0.00883s。模型文件大小:6.5MB。
3.3 使用ssld预训练pplcnet_x1_0模型并采用默认训练配置+公共数据集1+公共数据集2+resize短边256及中心224裁剪数据增强
ResizeImage(resize_short=256)及CropImage(size=224)在predict.py的实现如下所示:
img_h, img_w = img.shape[:2]
percent = 256 / min(img_w, img_h)
w = int(round(img_w * percent))
h = int(round(img_h * percent))
img = cv2.resize(img,(w,h))
w_start = (w - 224) // 2
h_start = (h - 224) // 2
w_end = w_start + 224
h_end = h_start + 224
img = img[h_start:h_end, w_start:w_end, :]
在A榜的结果为分数:0.722,用时:0.00883s。模型文件大小:6.5MB。
3.4 使用GENet变体模型并采用pplcnet_x1_0模型默认训练配置+公共数据集1+公共数据集2+3.3的预测数据增强
因为pplcnet_x1_0模型虽然满足推理时间小于等于10ms的要求,但是模型文件大小不满足小于等于3MB的要求,所以本项目基于论文复现赛的论文GENet,设计满足推理时间要求和模型文件大小要求的GENet变体模型。
本项目是基于上表对深度d及通道数c、卷积核大小、瓶颈比r进行调整以满足要求,GENet_lightV1的结构参数如下所示:
[{"d":1, "c":12, "s":2, "k":3, "e":1, "act":"relu"},
{"d":2, "c":24, "s":2, "k":3, "e":1, "act":"relu"},
{"d":3, "c":48, "s":2, "k":3, "e":1, "act":"relu"},
{"d":4, "c":128, "s":2, "k":3, "e":4, "act":"relu"},
{"d":2, "c":160, "s":2, "k":5, "e":2, "act":"hardswish"},
{"d":1, "c":192, "s":1, "k":3, "e":3, "act":"hardswish"},
{"d":1, "c":384, "s":1, "k":1, "e":1, "act":"hardswish"}
]
这里的e即为r,对于包含深度卷积的block,其激活函数本项目采用与pplcnet_x1_0模型一致的hardswish,除最后一个卷积层外,其他为relu。详细的模型实现见PaddleClas/ppcls/arch/backbone/legendary_models/genet.py
该模型在A榜的结果为分数:0.695,用时:0.00876s。模型文件大小:3.0MB。
3.5 相比3.4采用2倍的epochs+最终的数据集(包含公共数据集1~4)
考虑的数据量不够及相比自然图像文档图像过少,数据集不均衡,引入公共数据集3和4。又考虑到pplcnet_x1_0模型训练了360(ImageNet预训练)+60个epochs,本项目增加GENet_lightV1的训练epochs为120。此时该模型在A榜的结果为分数:0.841,用时:0.00861s。模型文件大小:3.0MB。
除了GENet_lightV1,还是设计了GENet_lightV2,如下所示:
[{"d":1, "c":12, "s":2, "k":3, "e":1, "act":"relu"},
{"d":1, "c":24, "s":2, "k":3, "e":1, "act":"relu"},
{"d":2, "c":48, "s":2, "k":3, "e":1, "act":"relu"},
{"d":4, "c":128, "s":2, "k":3, "e":4, "act":"relu"},
{"d":4, "c":160, "s":2, "k":3, "e":2, "act":"hardswish"},
{"d":3, "c":192, "s":1, "k":3, "e":2, "act":"hardswish"},
{"d":1, "c":384, "s":1, "k":1, "e":1, "act":"hardswish"}
]
该模型在A榜的结果为分数:0.839,用时:0.01069s。模型文件大小:4.5MB。
还有分别在最后一个DW块阶段增加se模块的SEGENet_lightV1和SEGENet_lightV2,但均没有提升分数。例如:
[{"d":1, "c":12, "s":2, "k":3, "e":1, "act":"relu"},
{"d":2, "c":24, "s":2, "k":3, "e":1, "act":"relu", "se":False},
{"d":3, "c":48, "s":2, "k":3, "e":1, "act":"relu", "se":False},
{"d":4, "c":128, "s":2, "k":3, "e":4, "act":"relu", "se":False},
{"d":2, "c":160, "s":2, "k":5, "e":2, "act":"hardswish", "se":False},
{"d":1, "c":192, "s":1, "k":3, "e":3, "act":"hardswish", "se":True},
{"d":1, "c":384, "s":1, "k":1, "e":1, "act":"hardswish"}
]
还有其他激活函数调整、数据增强调整等尝试,但均没起到提升作用。
对GENet的提升遇到瓶颈,遂重新回归到pplcnet_x1_0模型,利用pplcnet_x1_0模型的预训练权重。但我相信如果对GENet_lightV1同样进行360epochs的imagenet预训练,GENet_lightV1在本任务可能优于pplcnet_x1_0,因为不基于预训练,相同配置下训练这两模型,在A榜GENet_lightV1取得了更高的分数。
3.6 使用ssld预训练pplcnet_x1_0模型并采用pplcnet_x1_0模型默认训练配置+最终的数据集(包含公共数据集1~4)+3.3的预测数据增强
在A榜的结果为分数:0.847,用时:0.00883s。模型文件大小:6.5MB。
3.6 使用ssld预训练pplcnet_x1_0变体模型并采用pplcnet_x1_0模型默认训练配置+最终的数据集(包含公共数据集1~4)+3.3的预测数据增强
本项目基于pplcnet_x1_0模型增加一个模块来聚集三个阶段的输出特征信息,利用低层和高层的特征信息进行预测,如下所示:
class FPLayer(nn.Layer):
def __init__(self, in_c, out_c, ks):
super().__init__()
assert len(in_c)==len(ks)
layer = []
if len(out_c)!=len(in_c):
out_c = out_c*len(ks)
for (i, j, k) in zip(in_c, out_c, ks):
layer.append(nn.Sequential(nn.AvgPool2D(kernel_size=k, stride=k),
ConvBNLayer(i, 1, j, 1)))
self.layer = nn.LayerList(layer)
def forward(self, inputs):
out = []
for i, x in enumerate(inputs[:-1]):
out.append(self.layer[i](x))
out.append(inputs[-1])
out = paddle.concat(out, axis=1)
return out
pplcnet_x1_0变体模型(即pplcnet_fp_x1_0)的前向传播过程如下:
def forward(self, x):
x = self.conv1(x)
x = self.blocks2(x)
x = self.blocks3(x)
x = self.blocks4(x)
x1 = x
x = self.blocks5(x)
x2 = x
x = self.blocks6(x)
x = self.fp_layer([x1, x2, x])
x = self.avg_pool(x)
if self.last_conv is not None:
x = self.last_conv(x)
x = self.hardswish(x)
x = self.dropout(x)
x = self.flatten(x)
x = self.fc(x)
return x
详细实现见PaddleClas/ppcls/arch/backbone/legendary_models/pp_lcnet_fp.py
在A榜的结果为分数:0.85,用时:0.00882s。模型文件大小:12.2MB。
3.7 使用ssld预训练pplcnet_x1_0变体模型并采用pplcnet_x1_0模型默认训练配置+最终的数据集(包含公共数据集1~4)+3.3的预测数据增强+focal loss损失函数
相比3.6,在训练时使用focal loss损失函数自适应增大对困难图片的学习,实现如下所示:
class FocalLoss(nn.Layer):
"""
Focal loss
"""
def __init__(self, epsilon=None, alpha=1.0, gamma=2):
super().__init__()
if epsilon is not None and (epsilon <= 0 or epsilon >= 1):
epsilon = None
self.epsilon = epsilon
self.alpha = alpha
self.gamma = gamma
def _labelsmoothing(self, target, class_num):
if len(target.shape) == 1 or target.shape[-1] != class_num:
one_hot_target = F.one_hot(target, class_num)
else:
one_hot_target = target
soft_target = F.label_smooth(one_hot_target, epsilon=self.epsilon)
soft_target = paddle.reshape(soft_target, shape=[-1, class_num])
return soft_target, one_hot_target.squeeze(axis=1)
def forward(self, x, label):
if isinstance(x, dict):
x = x["logits"]
pt = F.softmax(x.detach(), axis=-1)
if self.epsilon is not None:
class_num = x.shape[-1]
label, one_hot_target = self._labelsmoothing(label, class_num)
x = -F.log_softmax(x, axis=-1)
loss = paddle.sum(x * label, axis=-1)
else:
if label.shape[-1] == x.shape[-1]:
label = F.softmax(label, axis=-1)
else:
label = F.one_hot(label, x.shape[-1]).squeeze(axis=1)
one_hot_target = label
loss = F.cross_entropy(x, label=label, soft_label=True, reduction="none")
pt = paddle.max(pt*one_hot_target, axis=-1, keepdim=False)
#print(loss.shape, pt.shape)
loss = ((1-pt)**self.gamma)*self.alpha*loss
loss = loss.mean()
return {"FocalLoss": loss}
此时在A榜的结果为分数:0.853,用时:0.00905s。模型文件大小:12.2MB。
这个验证集最优模型也是我们B榜采用的模型,最终B榜为:分数:0.791,用时:0.00908s,模型文件大小:12.2MB,推理时间满足要求,模型文件大小没有。
除此之外,我们还对分类器进行调整,设计两种双分类器加权融合预测方法,在A榜的最好结果为分数:0.852,用时:0.00882s,略逊于原单分类器结构,最终未采纳。例如:
if self.training:
if paddle.rand([1]).item()>=0.5:
out_w = 1.
else:
out_w = 0.
x = out_w*self.fc1(x)+(1.-out_w)*self.fc2(x)
else:
x = (F.softmax(self.fc1(x), axis=-1)+F.softmax(self.fc2(x), axis=-1))/2
return x
四、运行指令
4.1 数据准备
请阅读并运行prepare_ipynb准备好训练数据和验证数据
"""
# 安装paddleclas包快速体验
%cd ~
!git clone --depth=1 https://gitee.com/PaddlePaddle/PaddleClas.git
"""
# 不需要运行以上,本项目是基于PaddleClas增加配置文件、数据集、模型文件来实现,项目已内置修改后的PaddleClas套件
#运行解压项目已内置修改后的PaddleClas套件
!unzip -qo PaddleClas.zip
# 切换工作目录
%cd /home/aistudio/PaddleClas
/home/aistudio/PaddleClas
4.2 训练模型
# 一定要执行,不然训练会卡住不动
!export CUDA_VISIBLE_DEVICES=0
-c 后指定相应训练配置文件,参数修改可以进入到相应配置文件修改,PPLCNet_FP_x1_0_aug_fl.yaml为我们最终选择的模型的训练配置
# 开始训练
!python3 -m paddle.distributed.launch \
--gpus="0" \
tools/train.py \
-c ./ppcls/configs/PULC/text_image_orientation/PPLCNet_FP_x1_0_aug_fl.yaml
4.3 验证模型
-o Global.pretrained_model= 后指定想要验证的模型文件名,不包含后缀,模型文件需要与配置文件要求的模型保持一致。
# 快速验证
!python3 tools/eval.py \
-c ./ppcls/configs/PULC/text_image_orientation/PPLCNet_FP_x1_0_aug_fl.yaml \
-o Global.pretrained_model="output/pplcnet_fp_x1_0_aug_fl/PPLCNet_FP_x1_0/best_model"
4.4 导出推理模型
-o Global.pretrained_model= 后指定想要导出的模型文件名,不包含后缀,模型文件需要与配置文件要求的模型保持一致,-o Global.save_inference_dir= 后指定导出的推理模型的存放路径。
# 导出模型
!python3 tools/export_model.py \
-c ./ppcls/configs/PULC/text_image_orientation/PPLCNet_FP_x1_0_aug_fl.yaml \
-o Global.pretrained_model=output/pplcnet_fp_x1_0_aug_fl/PPLCNet_FP_x1_0/best_model \
-o Global.save_inference_dir=deploy/models/text_image_orientation_infer
使用推理模型预测测试集
%cd /home/aistudio/PaddleClas/deploy
/home/aistudio/PaddleClas/deploy
!pip install paddleclas
# 对测试数据集进行推理,输出结果
!python python/predict_cls.py -c configs/PULC/text_image_orientation/inference_text_image_orientation.yaml -o Global.infer_imgs="/home/aistudio/work/images/"
五、提交内容及格式
评测文件: 评测代码以及模型
- 在predict.py文件中实现模型推理逻辑,代码文件名称请一定要用predict.py;
- 模型文件放在predict.py的同级目录下,保证执行predict.py时能正确加载到模型;
- src_image_dir是测试图片文件夹,图片的格式均为.jpg格式;
- predict.py接收两个参数:一个是src_image_dir,另一个是结果保存文件名,请一定将结果保存到同级目录下名称为predict.txt文件中.
注意事项:
- 输出格式为WMgOhwZzacY023lCusqnBxIdibpkT5GP.jpg 0,其中,前半部分为 【图片名称】,后半部分为【类别编号】;
- 请将所有预测结果写入predict.txt文件中,提交的预测结果要与提供的图像名字与格式保持完全一致,否则上传将无法通过格式检查;
- 结果文件命名应为predict.txt,每行内容格式为:文件名 分类结果,文件名和分类结果使用英文空格" "分隔;
# 代码示例
# python predict.py [src_image_dir] [predict_filename]
import os
import sys
import glob
import cv2
def process(src_image_dir, output_filename):
current_path = os.path.dirname(__file__)
image_paths = glob.glob(os.path.join(src_image_dir, "*.jpg"))
with open(os.path.join(current_path, output_filename), 'w') as f:
for image_path in image_paths:
image_name = image_path.split('/')[-1]
# do something
# pred_label output
# 保存结果
f.write(f'{image_name} {pred_label}\n')
f.close()
if __name__ == "__main__":
assert len(sys.argv) == 3
src_image_dir = sys.argv[1]
output_filename = sys.argv[2]
process(src_image_dir, output_filename)
创建ZIP压缩包,所有提交内容均放在压缩包的根目录下,比如:result.zip 解压缩之后就是提交的内容文件,而不应该是predict.txt文件;压缩包中的内容可能如下:
|- root
- predict.py
|- model
- model.pdmodel
- model.pdiparams
本项目实现的predict.py文件有两种,一种是调用paddle的静态模型的,一种是调用onnx模型的(推荐且最终采用),详情分别见work/predict.py和work/onnx/predict.py
# 拷贝推理模型至work文件夹下
!cp -r /home/aistudio/PaddleClas/deploy/models/text_image_orientation_infer /home/aistudio/work
%cd /home/aistudio/work
/home/aistudio/work
"""
# 校验预测文件是否能正常推理
!python predict.py images predict.txt
"""
W0821 22:40:28.421113 3058 gpu_resources.cc:61] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 10.1
W0821 22:40:28.426131 3058 gpu_resources.cc:91] device: 0, cuDNN Version: 7.6.
5.1 调用paddle的静态模型
#打包下载
!zip -r submit.zip predict.py text_image_orientation_infer
5.2 调用onnx模型的(推荐且最终采用)
# 按照要求安装环境
!pip install onnx==1.10.1 onnxruntime-gpu==1.10 paddle2onnx
# 导出onnx模型
!paddle2onnx --model_dir text_image_orientation_infer/ --model_filename inference.pdmodel --params_filename inference.pdiparams --opset_version 11 --save_file onnx/result.onnx
# aistudio环境无法执行
#!python onnx/predict.py images predict.txt
%cd /home/aistudio/work/onnx/
!zip -r submit_onnx.zip predict.py result.onnx
/home/aistudio/work/onnx
adding: predict.py (deflated 58%)
adding: result.onnx (deflated 8%)
5.3 结果提交
保存的最好模型可见PaddleClas/output/pplcnet_fp_x1_0_aug_fl_my,训练日志见PaddleClas/output/pplcnet_fp_x1_0_aug_fl_my/PPLCNet_FP_x1_0/train.log。最终提交的静态推理checkpoint存放在work目录下的text_image_orientation_infer和onnx中的result.onnx,提交的压缩包为work/onnx/submit_onnx_testB.zip
将压缩包下载下来在比赛提交页面提交,等待几分钟即可六、总结
- 经过本次比赛,在轻量级网络结构设计方面受益颇多。
- 几乎所有前排选手A榜与B榜名次发生大反转,且分数下降许多,表明模型对A榜测试集过拟合了,再次提醒我提高模型的泛化能力的重要性,不能面向测试集分数优化,不然B榜会给予痛击!
- 也许GENet的变体泛化性可能会好一些
有任何问题,欢迎评论区留言交流。
此文章为搬运
原项目链接