0 赛题背景
赛题链接:广告图片素材分类算法挑战赛
讯飞AI营销是科大讯飞集团在数字广告领域发展的重要业务,基于深耕多年的人工智能技术和大数据积累,赋予营销智慧创新的大脑,以健全的产品矩阵和全方位的服务,帮助广告主用AI技术实现营销效能的全面提升,打造数字营销新生态。
目前,AI营销平台除自有媒体外,已在功能社交、休闲娱乐、专业进阶、衣食住行等类型的TOP媒体实现规模化覆盖,并且覆盖媒体数量仍在快速增长。如何确保在成百上千的媒体上投放符合要求的广告素材(例如,教育类APP需要投放适合青少年的广告内容),是素材审核迫切需要解决的问题。而作为最常见的广告素材类型之一,图片的自动分类将会极大提高审核效率。
0.1 赛事任务
本次分类算法任务中,讯飞AI营销将提供海量现网流量中的广告图片素材作为训练数据,参赛选手基于提供的训练数据构建模型,实现自动化广告图片素材分类。
0.2 实现思路
整体采用paddleclas2.2进行图像分类,然后训练过程中VisualDL可视化观察训练结果,然后适度调参。
本项目采用快速上手方案中的ShuffleNetV2_x0_25模型进行训练。
1. 数据说明
本次比赛为参赛选手提供的数据分为训练集、测试集、提交样例三类文件:
训练集:包含5万+广告素材图片,100多个类别,几十种图片尺寸;且图片已按类别放在不同文件夹内,文件夹名称即为素材图片的category_id。
测试集:包含6千余张广告素材图片,放在同一个文件夹内,图片文件的名称即为image_id。
提交样例:表头为image_id和category_id的CSV文件,选手提交数据时需要将测试集的图片id与模型预测的类别id按样例格式填入CSV中,进行提交。
比赛分为初赛和复赛两个阶段,初赛会公布训练集与初赛测试集;通过初赛的选手,进入复赛后会获取新的复赛测试集。
声明:本次提供的广告素材图片来源于讯飞、淘宝、京东、拼多多、苏宁、唯品会、快手、抖音、饿了么等众多广告主,任何人不可以任何形式使用、传播、披露、授权他人使用,请仅用于研究和学术目的。
2. 评估指标
本次比赛根据提交的结果文件,采用分类准确率作为评估指标:预测正确的图片占总测试集图片的比例。
3. 评测及排行
1、初赛和复赛均提供下载数据,选手在本地进行算法调试,在比赛页面提交结果。
2、每支团队每天最多提交3次。
3、排行按照得分从高到低排序,排行榜将选择团队的历史最优成绩进行排名。
4. 作品提交要求
- 文件格式:按照csv格式提交
- 文件大小:无要求
- 提交次数限制:每支队伍每天最多3次
- 文件详细说明:
以csv格式提交,编码为UTF-8,第一行为表头;
提交格式见样例
5.不需要上传其他文件
5. 赛程规则和奖励
参见官方比赛网站。
1 数据处理
仍是老规矩
- 观察数据集结构
- 改造成两个txt文件
#解压数据集到跟目录先 数据量挺大的耐心等待45秒左右 !unzip -oq /home/aistudio/data/data102279/科大讯飞股份有限公司_广告图片素材分类算法挑战赛.zip
正如赛题所说,结构为,
训练集:包含137个类别,且图片已按类别放在不同文件夹内,文件夹名称即为素材图片的category_id。
测试集:包含6千余张广告素材图片,放在同一个文件夹内,图片文件的名称即为image_id。
那么就简单了,正如看过我上一个PaddleClas2.2实现奥特曼分类的人应该都知道如何解决了!
# 先把paddleclas安装上再说 # 安装paddleclas以及相关三方包(好像studio自带的已经够用了,无需安装了) !git clone https://gitee.com/paddlepaddle/PaddleClas.git -b release/2.2 # 我这里安装相关包时,花了30几分钟还有错误提示,不管他即可 #!pip install --upgrade -r PaddleClas/requirements.txt -i https://mirror.baidu.com/pypi/simple
Cloning into 'PaddleClas'... remote: Enumerating objects: 538, done.[K remote: Counting objects: 100% (538/538), done.[K remote: Compressing objects: 100% (323/323), done.[K remote: Total 15290 (delta 344), reused 349 (delta 210), pack-reused 14752[K Receiving objects: 100% (15290/15290), 113.56 MiB | 8.20 MiB/s, done. Resolving deltas: 100% (10236/10236), done. Checking connectivity... done.
# 先导入库 (这个项目不一定都用得到,我习惯了先把这一堆搬过来) from sklearn.utils import shuffle import os import pandas as pd import numpy as np from PIL import Image import paddle import paddle.nn as nn from paddle.io import Dataset import random
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/layers/utils.py:26: DeprecationWarning: `np.int` is a deprecated alias for the builtin `int`. To silence this warning, use `int` by itself. Doing this will not modify any behavior and is safe. When replacing `np.int`, you may wish to use e.g. `np.int64` or `np.int32` to specify the precision. If you wish to review your current use, check the release note link for additional information. Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations def convert_to_list(value, n, name, dtype=np.int):
# 忽略(垃圾)警告信息 # 在python中运行代码经常会遇到的情况是——代码可以正常运行但是会提示警告,有时特别讨厌。 # 那么如何来控制警告输出呢?其实很简单,python通过调用warnings模块中定义的warn()函数来发出警告。我们可以通过警告过滤器进行控制是否发出警告消息。 import warnings warnings.filterwarnings("ignore")
# 更改文件夹名字,规范一些 !mv 初赛_测试集 testdata !mv 训练集 traindata
# -*- coding: utf-8 -*- # 根据官方paddleclas的提示,我们需要把图像变为两个txt文件 # train_list.txt(训练集) # val_list.txt(验证集) # 先把路径搞定 比如:训练集/0/77398.jpg ,读取到并写入txt dirpath = "traindata" # 先得到总的txt后续再进行划分,因为要划分出验证集,所以要先打乱,因为原本是有序的 def get_all_txt(): all_list = [] i = 0 for root,dirs,files in os.walk(dirpath): # 分别代表根目录、文件夹、文件 for file in files: i = i + 1 # 文件中每行格式: 图像相对路径 图像的label_id(注意:中间有空格)。 # traindata/0/77398.jpg 0 # traindata/101/6259.jpg 101 #all_list.append(os.path.join(root,file)+" "+dirs+"\n") imgpath = os.path.join(root,file) category_id = os.path.join(root,file).split("/") all_list.append(imgpath+" "+category_id[1]+"\n") allstr = ''.join(all_list) f = open('all_list.txt','w',encoding='utf-8') f.write(allstr) return all_list , i all_list,all_lenth = get_all_txt() print(all_lenth)
51273
# 先把数据打乱,然后按照比例划分数据集 数据有5万多,所以9:1即可 random.shuffle(all_list) train_size = int(all_lenth * 0.85) train_list = all_list[:train_size] val_list = all_list[train_size:] print(len(train_list)) print(len(val_list))
43582 7691
# 运行cell,生成txt train_txt = ''.join(train_list) f_train = open('train_list.txt','w',encoding='utf-8') f_train.write(train_txt) f_train.close() print("train_list.txt 生成成功!") # 运行cell,生成txt val_txt = ''.join(val_list) f_val = open('val_list.txt','w',encoding='utf-8') f_val.write(val_txt) f_val.close() print("val_list.txt 生成成功!")
train_list.txt 生成成功! val_list.txt 生成成功!
# 将图片移动到paddleclas下面的数据集里面 # 至于为什么现在移动,也是我的一点小技巧,防止之前移动的话,生成的txt的路径是全路径,反而需要去掉路径的一部分 !mv traindata/ PaddleClas/dataset/ !mv testdata/ PaddleClas/dataset/
!mv all_list.txt PaddleClas/dataset/traindata !mv train_list.txt PaddleClas/dataset/traindata !mv val_list.txt PaddleClas/dataset/traindata
2 采用paddleclas进行训练
数据集核实完搞定成功的前提下,可以准备更改原文档的参数进行实现自己的图片分类了!
#windows在cmd中进入PaddleClas根目录,执行此命令 %cd PaddleClas !ls
/home/aistudio/PaddleClas dataset hubconf.py MANIFEST.in README_ch.md requirements.txt deploy __init__.py paddleclas.py README_en.md setup.py docs LICENSE ppcls README.md tools
2.1 修改配置文件
主要是以下几点:分类数、图片总量、训练和验证的路径、图像尺寸、训练和预测的num_workers: 0才可以在aistudio跑通。
路径如下:
PaddleClas/ppcls/configs/ImageNet/ResNet/ResNet50.yaml
# global configs Global: checkpoints: null pretrained_model: null output_dir: ./output/ device: gpu save_interval: 10 eval_during_train: True eval_interval: 3 epochs: 150 print_batch_step: 10 use_visualdl: True # used for static mode and model export image_shape: [3, 224, 224] save_inference_dir: ./inference # training model under @to_static to_static: False # model architecture Arch: name: ResNet50 class_num: 137 # loss function config for traing/eval process Loss: Train: - CELoss: weight: 1.0 Eval: - CELoss: weight: 1.0 Optimizer: name: Momentum momentum: 0.9 lr: name: Piecewise learning_rate: 0.015 decay_epochs: [30, 60, 90] values: [0.1, 0.01, 0.001, 0.0001] regularizer: name: 'L2' coeff: 0.0005 # data loader for train and eval DataLoader: Train: dataset: name: ImageNetDataset image_root: ./dataset/ cls_label_path: ./dataset/traindata/train_list.txt transform_ops: - DecodeImage: to_rgb: True channel_first: False - ResizeImage: resize_short: 256 - CropImage: size: 224 - RandFlipImage: flip_code: 1 - NormalizeImage: scale: 1.0/255.0 mean: [0.485, 0.456, 0.406] std: [0.229, 0.224, 0.225] order: '' sampler: name: DistributedBatchSampler batch_size: 128 drop_last: False shuffle: True loader: num_workers: 0 use_shared_memory: True Eval: dataset: name: ImageNetDataset image_root: ./dataset/ cls_label_path: ./dataset/traindata/val_list.txt transform_ops: - DecodeImage: to_rgb: True channel_first: False - ResizeImage: resize_short: 256 - CropImage: size: 224 - NormalizeImage: scale: 1.0/255.0 mean: [0.485, 0.456, 0.406] std: [0.229, 0.224, 0.225] order: '' sampler: name: DistributedBatchSampler batch_size: 128 drop_last: False shuffle: True loader: num_workers: 0 use_shared_memory: True Infer: infer_imgs: dataset/testdata/a237.jpg batch_size: 10 transforms: - DecodeImage: to_rgb: True channel_first: False - ResizeImage: resize_short: 256 - CropImage: size: 224 - NormalizeImage: scale: 1.0/255.0 mean: [0.485, 0.456, 0.406] std: [0.229, 0.224, 0.225] order: '' - ToCHWImage: PostProcess: name: Topk topk: 5 class_id_map_file: dataset/label_list.txt Metric: Train: - TopkAcc: topk: [1, 5] Eval: - TopkAcc: topk: [1, 5]
2.2 修改类别文件
其实,这个图片分类也没有给出具体的类别的名称,但是为了输出的结果好看,还是原样放上一份。
已经文件夹左侧
# 回到主目录 %cd ..
/home/aistudio
!mv label_list.txt PaddleClas/dataset
# 重新回到paddleclas下 %cd PaddleClas !ls
[Errno 2] No such file or directory: 'PaddleClas' /home/aistudio/PaddleClas dataset hubconf.py MANIFEST.in ppcls README.md tools deploy __init__.py output README_ch.md requirements.txt docs LICENSE paddleclas.py README_en.md setup.py
2.3 训练
# 开始训练 200轮 # 提示,运行过程中存在坏图的情况,但是不用担心,训练过程不受影响。后续预测的时候会有影响会告知你如何处理坏图 !python tools/train.py \ -c ./ppcls/configs/ImageNet/ResNet/ResNet50.yaml
200轮的训练图如下
3 评估
简单的记录一下评估记录
Column 1 | Column 2 |
best_model | [Avg]CELoss: 1.32663, loss: 1.32663, top1: 0.71421, top5: 0.87973 |
epoch_60 | [Avg]CELoss: 1.33068, loss: 1.33068, top1: 0.71005, top5: 0.87700 |
epoch_80 | [Avg]CELoss: 1.33054, loss: 1.33054, top1: 0.71122, top5: 0.87882 |
epoch_120 | [Avg]CELoss: 1.32960, loss: 1.32960, top1: 0.70940, top5: 0.87895 |
epoch_200 | [Avg]CELoss: 1.32990, loss: 1.32990, top1: 0.70862, top5: 0.87635 |
# 模型评估 !python tools/eval.py \ -c ./ppcls/configs/ImageNet/ResNet/ResNet50.yaml \ -o Global.pretrained_model=./output/ResNet50/epoch_200
4 预测
训练完数据,肯定要进行预测,拿到结果才可,这里先拿一张一张图片进行验证,后续更改predict函数进行批量预测。
4.1 先验证一张
!python3 tools/infer.py \ -c ./ppcls/configs/ImageNet/ResNet/ResNet50.yaml \ -o Infer.infer_imgs=dataset/traindata/0/77398.jpg \ -o Global.pretrained_model=output/ResNet50/best_model
输出:
[{'class_ids': [0, 38, 6, 21, 85], 'scores': [0.99787, 0.00048, 0.00039, 0.00023, 0.00021], 'file_name': 'dataset/traindata/0/77398.jpg', 'label_names': ['0', '38', '6', '21', '85']}]
我取用的图片是0分类里面的,这里先不考虑我预测是取自训练用的图,先保证整体正确即可。
从上面的输出可以看出0分类概率最大,得出该图片为0分类的图片正确。
4.2 批量预测
!python3 tools/infer.py \ -c ./ppcls/configs/ImageNet/ResNet/ResNet50.yaml \ -o Global.pretrained_model=output/ResNet50/best_model \ -o Infer.infer_imgs=dataset/testdata
运行之后发现有损坏的文件出现,导致预测不能完整的进行,这时候,就要找出来是哪个文件有问题。
尝试去看预测文件:
PaddleClas/output/ShuffleNetV2_x0_25/infer.log是哪里出了问题,
结果意外发现:
paddleclas2.2不具备预测结果输出到日志的功能
自行加上,很简单
PaddleClas/ppcls/engine/trainer.py 580行
logger.info(result)
但这时还是没有找到哪个图片出现了问题,编写函数,再去找!
# 得到预测的数量是多少 path = "dataset/testdata" def get_test_txt_list(): """ return test_text_list,test_txt_list , i 分别是:返回的数组,txt,文件数量 """ test_text_list = [] for root,dirs,files in os.walk(path): # 分别代表根目录、文件夹、文件 # 遍历每个文件 i = 0 for file in files: i = i + 1 test_text_list.append(os.path.join(root,file)+"\n") test_txt_list = ''.join(test_text_list) f = open('dataset/test_txt_list.txt','w',encoding='utf-8') f.write(test_txt_list) return test_text_list,test_txt_list , i test_text_list,test_txt_list,test_lenth = get_test_txt_list() print(test_lenth)
6409
通过下部分代码块找到问题图片
import cv2 i = 0 imgshape = [] wentitupian = [] for oneimg in test_text_list: oneimg = oneimg.split("\n")[0] img =cv2.imread(oneimg) try: imgshape.append(oneimg+" "+str(img.shape[0])+" "+str(img.shape[1])+" "+str(img.shape[2])+"\n") except Exception: print(wentitupian.append(oneimg+"\n")) i = i+1 print(i) print(wentitupian)
None 6409 ['dataset/testdata/a2411.jpg\n']
#找到问题图片之后将其删除即可 !rm dataset/testdata/a2411.jpg
# 再次预测,得到全部结果,除了那个坏图 # 这里,运行之前我先把yaml的预测结果topk改成了1,防止生成太多数据,毕竟只有第一个数据才是我们需要的值 !python3 tools/infer.py \ -c ./ppcls/configs/ImageNet/ResNet/ResNet50.yaml \ -o Global.pretrained_model=output/ResNet50/best_model \ -o Infer.infer_imgs=dataset/testdata
终于得到了我们想要的预测结果
5 处理预测的log
得到的预测文件中包含了很多冗余信息,我们只需要文件名和类别即可
此外特别强调一下,每一行的日志不仅仅是一个文件的预测
刚开始我也以为,642行,预测结果仍然不够啊
后来发现每一行的结果是多个的!
# 将其复制一分出来,方便处理,看着也舒服 !cp output/ResNet50/infer.log output/ResNet50/infer.txt
import json # 手动先把txt里面的如下之前全删了(包含这一行)再说 # [2021/07/30 16:15:03] root INFO: train with paddle 2.0.2 and device CUDAPlace(0)这行删了 # 这样下面处理就是统一的样式了 result = [] save_path = "output/ResNet50/submit_sample.csv" f = open("output/ResNet50/infer.txt","r") lines = f.readlines() #读取全部内容 ,并以列表方式返回 for line in lines: # 先把前面的时间都干掉,然后将单引号换成双引号,就变成了json格式的文本了 txt = line.split(" root INFO: ")[1] print(txt) jsontxt = txt.replace("\'","\"") # print(jsontxt) list_txt = json.loads(jsontxt) for i in range(len(list_txt)): name, ids = list_txt[i]['file_name'],list_txt[i]['class_ids'] onefilepath = name.split("/")[2] result.append([onefilepath,ids[0]]) df = pd.DataFrame(result, columns=['image_id', 'category_id']) df.sort_values(by="image_id",inplace=True,ascending=True) #千万记得排序! df.to_csv(save_path, index=None)
提示:要进行排序,这里我是在csv文件进行了排序,因为挺复杂的,头有a后面有jpg
印象里可以用re进行解决
后续会处理了我再补上吧~
总结
项目总结
- 暂时基线是采用的是经典RESNET50网络进行的训练,虽然快,但是准确率并没有想象的那么好
- 参数还没有进行大调整,还有上升空间
- paddleclas2.2YYDS