2021科大讯飞广告图片素材分类算法挑战赛—参赛总结
摘要
比赛网址
:https://challenge.xfyun.cn/topic/info?type=ad-2021&ch=dc-zmt-05赛题任务
:本次分类算法任务中,讯飞AI营销将提供海量现网流量中的广告图片素材作为训练数据,参赛选手基于提供的训练数据构建模型,实现自动化广告图片素材分类。数据分析
:对类别标签进行可视化得知,类别标签不均衡,有些类别有几千张图片,而有的只有十几二十张。接着计算出整个数据集的均值和标准差,用于后续的归一化处理。模型选择
:一开始用了之前验证码识别的模型去做,结果准确率只有不到60%,然后接着试了下baseline的模型efficientnet_b0,准确率也只有70+。后来从resnet, alexnet, vgg, squeezenet, densenet, inception模型中试了个遍,最后确定了resnext50_32x4d模型最优。训练方案
:基于resnext50_32x4d模型构建自己的baseline。期间,试过常规数据增强、以及针对类别不均衡做的自定义数据增强:即根据类别占比来确定以多大的概率执行数据增强,占比越小则数据增强就越大,得到用于训练的图片就越多;试过用FocalLoss来应对类别不均衡;试过用不同的损失函数和优化器;试过使用灰度图和彩色图进行训练;试过用不同大小的图片尺寸输入网络;试过……,最后才确定了最终的方案。模型融合
:最终通过改变学习率训练不同的模型,采用加权平均进行模型融合,得到最终的预测输出。赛题得分
:初赛最终成绩:0.88391分,排名:第22名;复赛最终成绩:0.87551分,排名:第23名。总结结论
:这是我参加比赛以来第一次进入复赛阶段的比赛,很有纪念意义,虽然跟前排大佬还有很大的差距,但是从中也学习到了很多知识,有机会下次再来~
赛题任务
数据分析
-
首先查看各个类别的数据量情况:
可以看到,类别越往后数据量越少。 -
在求数据集的均值和标准差时,如果convert为RGB,那么有些图片会报警告:
UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
,如图片:训练集/1/87982.jpg,我不清楚这样去算三通道RGB的均值和标准差会不会有什么影响,所以就全部先convert为RGBA,再用opencv将RGBA转为RGB去计算了(强迫症不想看到任何warning)。 -
计算均值和标准差的代码如下:
from PIL import Image
import cv2
import numpy as np
from tqdm import tqdm
def print_img_mean_std():
img_filenames = get_data().path.values.tolist()
m_list, s_list = [], []
img_h, img_w = 320, 320
for img_filename in tqdm(img_filenames):
img=Image.open(img_filename).convert('RGBA')
img = cv2.cvtColor(np.asarray(img),cv2.COLOR_RGBA2BGR)
img = cv2.resize(img, (img_h, img_w))
img = img / 255.0
m, s = cv2.meanStdDev(img)
m_list.append(m.reshape((3,)))
s_list.append(s.reshape((3,)))
m_array = np.array(m_list)
s_array = np.array(s_list)
m = m_array.mean(axis=0, keepdims=True)
s = s_array.mean(axis=0, keepdims=True)
print('均值:',m[0][::-1])
print('标准差:',s[0][::-1])
模型选择
本次比赛所用到的模型均来自与timm
模块(在kaggle上很受欢迎),是一个类似torchvision.models
模块一样,可直接调用各种预训练模型。下面定义自己的模型:
from torch.nn import Module
from torch.nn import Sequential
from torch.nn import Linear
try:
import timm
except:
! pip install timm
import timm
class TimmModels(Module):
def __init__(self, model_name='resnext50_32x4d',pretrained=True, num_classes=137):
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
)
self.m = Sequential(*model_list)
def forward(self, image):
out = self.m(image)
return out
训练方案
构建自己的baseline方案:
- 训练周期:17
- 学习率:0.0001
- 批次大小:50
- 类别不均衡:不作相应处理
- 数据集划分:训练集的90%训练模型,10%验证模型
- 数据增强:不增强,只做简单缩放以及归一化处理
- 损失函数:多标签损失函数MultiLabelSoftMarginLoss
- 优化器:Adam
基于baseline,各个点所做的尝试:
- 训练周期,限于kaggle单次session的时间限制(9h),最大训练周期只能设置为17(一个周期大约0.5h)
- 批次大小,限于kaggle内存的限制,最大限度的批次大小为50
- 数据集划分,前期采用固定seed来更好地比较不同改变下的得分情况;后期不固定seed去训练多个模型;以及最后全量训练
- 数据增强,一套组合拳:RandomHorizontalFlip、RandomVerticalFlip、ColorJitter、RandomErasing
- 类别不均衡,按类别占比赋予权重,再根据概率是否执行这套组合拳,即这套组合拳打下来样本越少的类别数据增强就越厉害
- 损失函数,使用FocalLoss损失函数去应对类别不均衡的现象,即如果预测错了样本量越少的类别,其损失值就越大;尝试使用交叉熵损失函数CrossEntropyLoss
- 优化器,尝试改用随机梯度下降SGD
- 学习率,在使用Adam时,学习率在0.0001 ~ 0.001范围内尝试;在使用SGD时,学习率在0.01 ~ 0.5范围内尝试
最终方案:
from torch.utils.data import DataLoader
from torchvision import transforms
import torch
import numpy as np
import pandas as pd
import time
import os
from torch.nn import CrossEntropyLoss
from torch.autograd import Variable
from torch.optim import SGD,lr_scheduler
epochs = 17 # 训练周期
batch_size = 50 # 批次大小
rate = 0.01 # 学习率
device = 'cuda' if torch.cuda.is_available() else 'cpu'
verbose=100 # 打印loss、accuracy等信息
image_width=320 # 缩放大小
image_height=320 # 缩放大小
# 320
norm_mean = (0.63790344, 0.56811579, 0.5704457)
norm_std = (0.24307405, 0.2520139, 0.25256122)
def spilt_train_vaild_test(fusai=False):
'''前期采用固定seed来更好地比较不同改变下的得分情况;后期不固定seed去训练多个模型;以及最后全量训练'''
df=get_data()
train_df=df[df['is_train']==1].copy().reset_index(drop=True)
test=df[df['is_train']==0].copy().reset_index(drop=True)
if fusai:
test=get_fusai_test()
ind_vaild=[]
for i in range(train_df.category_id.nunique()):
ind_vaild.extend(train_df[train_df.category_id==i].sample(frac=0.1,random_state=2021).index.to_list())
ind_train=train_df.drop(index=ind_vaild).index.to_list()
train=train_df.loc[ind_train,:].copy().reset_index(drop=True)
vaild=train_df.loc[ind_vaild,:].copy().reset_index(drop=True)
vaild['is_train']=0
train=train.sample(frac=1,random_state=2021).reset_index(drop=True)
vaild=vaild.sample(frac=1,random_state=2021).reset_index(drop=True)
return train,vaild,test
class ImageDataSet2(Dataset):
'''图片加载和处理'''
def __init__(self,df,transform):
self.df=df
self.transform=transform
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
image_path = self.df.iloc[idx].path
image_id = self.df.iloc[idx].image_id
order = self.df.iloc[idx].category_id
image = Image.open(image_path).convert('RGBA').convert('RGB')
label = torch.from_numpy(np.array(order))
image=self.transform(image)
return image, label, order, image_id
train,vaild,test=spilt_train_vaild_test()
# 数据不增强
test_transform = transforms.Compose([
transforms.Resize((image_width,image_height)),
transforms.ToTensor(),
transforms.Normalize(norm_mean,norm_std)
])
train_dataloader=DataLoader(ImageDataSet2(train,test_transform), batch_size=batch_size, shuffle=True, num_workers=32)
vaild_dataloader=DataLoader(ImageDataSet2(vaild,test_transform), batch_size=batch_size, shuffle=True, num_workers=32)
test_dataloader=DataLoader(ImageDataSet2(test,test_transform), batch_size=1, shuffle=False, num_workers=32)
model=TimmModels(pretrained=True).to(device)
# 损失函数
criterion = CrossEntropyLoss().to(device)
# SGD算法
optimizer = SGD(model.parameters(), lr=rate)
模型融合
根据最终方案,调整学习率训练多个模型;调整学习率再全量训练多个模型。然后尝试采用以下融合方法进行模型融合,得出最终的预测结果。
- 投票法
- 加权平均法
最后发现加权平均法要略胜一筹,具体的模型融合核心代码:
models_mapping=get_models() # 获取所有训练好的模型,权重即为该模型的验证集的accuracy
print('模型数量:',len(models_mapping))
print('模型如下:','、'.join(list(models_mapping.keys())))
with torch.no_grad():
result_all=[]
for images, labels, orders, image_id in tqdm(test_dataloader):
pred_array_all=np.zeros(137,dtype=np.float32)
total_acc_all = 0
for name,(model,acc) in models_mapping.items():
model.eval()
predict_label = model(Variable(images.reshape(-1,3,image_width,image_height)).to(device))
predict = predict_label.data.cpu().numpy().reshape(-1) * acc
pred_array_all += predict
total_acc_all += acc
pred_all = np.argmax(pred_array_all / total_acc_all)
result_all.append({'image_id':image_id[0],'category_id':pred_all})
submit=pd.DataFrame(result_all)
submit.to_csv('submit.csv',index=False)
赛题得分
-
初赛,排名:22/197,最终得分:0.88391,晋级复赛(前20%名晋级)
-
复赛,排名:23/30,最终得分:0.87551,没能进入决赛(前三名)
以上即为本文的全部内容,若需要全部源代码的,请关注公众号《Python王者之路》,回复关键词:20210928
,即可获取。
写在最后
这次比赛的时间跨度算是比较长的了,初赛两个月,复赛一个月,虽然不是每天都有在研究赛题上分,而是用周末空闲的时间来打比赛都觉得时间是足够的。
还有,这是我第一次打CV类比赛,很多东西学的不是很全面(只是完成过验证码识别),不知道我哪来的自信,就报名了这次比赛。
中间遇到几次瓶颈,第一次是我用验证码识别的模型来对比赛数据集进行拟合,分数连0.6都上不去,我那时候一度怀疑我之前写的验证码识别的模型,实在是太垃圾了,这种模型就只能勉强用在这种规格小的验证码图片上了。
第二次是在我逛kaggle时无意中发现了有人在使用timm模块,并且用的模型是resnext50_32x4d即本文最终确定的模型,于是我抱着试一试的想法用了一下这个模型去训练,结果分数直接上到了0.80,我一下子觉得自己似乎又可以了,然而不管怎么调参,分数都上不去了。
第三次也是最后一次,是在我看了datawhale他们写的baseline之后,我惊奇地发现,他们竟然也是用timm模块里的模型,仔细看了一遍后,我注意到他们的损失函数用的是CrossEntropyLoss。因为当时我一直是沿用了验证码识别时的损失函数和优化器,于是我试着去更换这两个东西,最后才确定了损失函数用CrossEntropyLoss,优化器用SGD是最好的,分数一直在涨,最高分是达到了线上0.86,可是再怎么调参分数又都上不去了。
最后采用了竞赛中的杀手锏模型融合,分数最终定格在了0.88391,当然这是初赛的成绩,进入复赛后,只是更换了测试集,然而使用初赛时最好成绩的模型来预测提交分数只有0.87441,直接降了一个百分点。然后我尝试了将初赛的测试集作伪标签,然而后来举办方说不允许使用,后面也就不考虑了。接着试了一下全量训练集训练模型,然而全量训练的话很难确定训练周期,于是我选择了最大的训练周期即17,结果分数有一定的上升,最终复赛分数定格在了0.87551。
最后,提前祝我们的祖国生日快乐,大家国庆节快乐~