DatasetDivision虽然这次比赛帮助我们划分了训练集和验证集,这里还是对数据集的划分和交叉验证做一些说明。Baseline建立后,通常需要将训练数据分成训练集和验证集。训练集用于训练模型,验证集用于调整参数。验证集的划分有如下几种方式。Hold-outHold-out也叫hold-out法,是最简单的划分方法。即把训练数据按一定比例随机分成训练集和验证集。fromglobimportglobimportrandom#保证每次划分的结果一致random.seed(0)img_paths=glob('/content/data/mchar_train/'+'*.png')random.shuffle(img_paths)train_cnt=int(len(img_paths)*0.8)train_imgs,val_imgs=img_paths[:train_cnt],img_paths[train_cnt:]K-FoldK-Fold将训练数据分成K份,取其中的K-1份作为训练集,rest作为验证集,依次循环K次,通过训练得到K个不同的模型,最后将K个模型的准确率平均得到模型的准确率。超参数K可以根据数据集的大小来设置。如果数据集比较大,可以适当设置小一些,甚至可以直接设置为1,此时相当于Hold-out;数据集比较小,可以适当设置大一些,甚至可以直接设置为1,相当于留一法。importrandomrandom.seed(0)fromglobimportglobimportnumpyasnpdefk_fold(data,k=10):"""Params:data(listornumpy.ndarray):dataneedsplittotrainsetandval_setk(integer):"""fold=len(data)//kfold=[iforiinrange(0,len(data),fold)]fold[-1]=len(data)idxes=list(range(len(data)))random.shuffle(idxes)ifisinstance(data,list):forfinrange(len(fold)-1):temp=idxes[fold[0]:fold[f]]temp.extend(idxes[fold[f+1]:fold[-1]])yieldtemp,idxes[fold[f]:fold[f+1]]elifisinstance(data,np.ndarray):forfinrange(len(fold)-1):yieldnp.concatenate([idxes[fold[0]:fold[f]],idxes[fold[f+1]:fold[-1]]],0),\idxes[fold[f]]:fold[f+1]]当然更方便的做法是直接调用sklearn中的kfold函数fromsklearn.model_selectionimportKFoldkfold=KFold(3,shuffle=True,random_state=0)arr=np.random.randint(0,20,(10,4))#返回生成器res=kfold.split(arr)bootstrapself-servicesampling通过有放回的抽样方式得到新的训练集和验证集。每个训练集和验证集都是不同的。这种划分方法一般适用于数据量较小的情况。如下图所示,代表三种划分方式。在这个项目中,数据量很大,官方已经帮我们分好了。个人感觉直接使用划分好的训练集进行训练,调整验证集的参数就可以了。不需要交叉验证,差别应该不大。模型训练模型训练专门定义了一个类,叫做Trainer。其所有代码在上一篇文章中均有展示,这里对其主要部分进行一些说明。构建数据集#训练集加载器self.train_loader=DataLoader(DigitsDataset(data_dir['train_data'],data_dir['train_label']),batch_size=config.batch_size,\num_workers=8,pin_memory=True,drop_last=True)#测试setloaderself.val_loader=DataLoader(DigitsDataset(data_dir['val_data'],data_dir['val_label'],aug=False),batch_size=config.batch_size,\num_workers=8,pin_memory=True,drop_last=False)定义优化器和损失函数#损失函数使用标签平滑交叉熵损失函数self.criterion=LabelSmoothEntropy().to(self.device)#优化器使用SGDself.optimizer=SGD(self.model.parameters(),lr=config.lr,momentum=config.momentum,weight_decay=config.weights_decay,nesterov=True)#学习率调整策略使用Warmup+Cosineself.lr_scheduler=CosineAnnealingWarmRestarts(self.optimizer,10,2,eta_min=10e-4)执行training下面是训练一个epoch的代码deftrain_epoch(self,epoch):total_loss=0corrects=0tbar=tqdm(self.train_loader)#每次都让模型回到训练模式很重要self.model.train()fori,(img,label)inenumerate(tbar):"""img(tensor):shape[N,C,H,W]label(tensor):shape[N,5],第一列代表第一个数"""img=img.to(self.device)label=label.to(self.device)self.optimizer.zero_grad()#pred(tensor):shape[N,11]pred=self.model(img)loss=self.criterion(pred[0],label[:,0])+\self.criterion(pred[1],label[:,1])+\self.criterion(pred[2],label[:,2])+\self.criterion(pred[3],label[:,3])+\self.criterion(pred[4],label[:,4])total_loss+=loss.item()loss.backward()self.optimizer.step()temp=t.stack([\pred[0].argmax(1)==label[:,0],\pred[1].argmax(1)==label[:,1],\pred[2].argmax(1)==label[:,2],\pred[3].argmax(1)==label[:,3],\pred[4].argmax(1)==label[:,4]\],dim=1)#只有5个数都预测正确才是正确的+=t.all(temp,dim=1).sum().item()tbar.set_description('loss:%.3f,acc:%.3f'%(loss/(i+1),corrects*100/((i+1)*config.batch_size)))if(i+1)%config.print_interval==0:self.lr_scheduler.step()模型保存和加载Pytorch中的模型默认保存为字典形式的pth文件,另外保存你需要的额外参数也很方便,比如当前模型的准确率和误差,优化器参数,网络相关的配置等等。defsave_model(self,save_path,save_opt=False,save_config=False):"""Params:save_path(string):模型保存路径save_opt(bool):是否保存优化器相关参数,可用于恢复训练状态latersave_config(bool):是否保存网络配置参数,可以用于后面查看网络参数"""dicts={}dicts['model']=self.model.state_dict()ifsave_opt:dicts['opt']=self.optimizer.state_dict()ifsave_config:dicts['config']={s:config.__getattribute__(s)forsindir(config)ifnots.startswith('_')}t.save(dicts,save_path)defload_model(self,load_path,save_opt=False,save_config=False):dicts=t.load(load_path)self.model.load_state_dict(dicts['model'])如果save_opt:self.optimizer.load_state_dict(dicts['opt'])#如果保存了网络配置参数,需要将保存的参数值设置为config的对应属性ifsave_config:fork,vindicts['config'].items():配置.__setattr__(k,v)model验证模型在验证过程中,使用t.no_grad可以大大节省内存。defeval(self):"""model.eval():会影响BatchNorm、Dropout等网络的前向传播过程。t.no_grad():会禁用反向传播过程,从而大大节省内存。(在模型验证期间推荐)"""self.model.eval()corrects=0witht.no_grad():tbar=tqdm(self.val_loader)fori,(img,label)inenumerate(tbar):img=img.to(self.device)label=label.to(self.device)pred=self.model(img)temp=t.stack([pred[0].argmax(1)==label[:,0],\pred[1].argmax(1)==标签[:,1],\pred[2].argmax(1)==标签[:,2],\pred[3].argmax(1)==label[:,3],\pred[4].argmax(1)==label[:,4]\],dim=1)校正+=t.all(temp,dim=1).sum().item()tbar.set_description('ValAcc:%.2f'%(corrects*100/((i+1)*config.batch_size)))self.model.train()returncorrects/(len(self.val_loader)*config.batch_size)模型预测并生成结果defpredicts(model_path):test_loader=DataLoader(DigitsDataset(data_dir['val_data'],None,aug=False),batch_size=config.batch_size,shuffle=False,\num_workers=8,pin_memory=True,drop_last=False)results=[]model=DigitsMobilenet(config.class_num).cuda()model.load_state_dict(t.load(model_path)['model'])print('从加载模型%s成功'%model_path)tbar=tqdm(test_loader)model.eval()witht.no_grad():fori,(img,img_names)inenumerate(tbar):img=img.cuda()pred=model(img)结果+=[[name,code]forname,codeinzip(img_names,parse2class(pred))]#result.sort(key=results)results=sorted(results,key=lambdax:x[0])write2csv(results)returnresultsdefparse2class(prediction):"""将预测的类型解析为字符串Params:prediction(tupleoftensor):分别对应"""ch1,ch2,ch3,ch4,ch5=predictionchar_list=[str(i)foriinrange(10)]char_list.append('')ch1,ch2,ch3,ch4,ch5=ch1.argmax(1),ch2.argmax(1),ch3.argmax(1),ch4.argmax(1),ch5.argmax(1)ch1,ch2,ch3,ch4,ch5=[char_list[i.item()]foriinch1],[char_list[i.item()]foriinch2],\[char_list[i.item()]fori在ch3],[char_list[i.item()]foriinch4],\[char_list[i.item()]foriinch5]res=[c1+c2+c3+c4+c5forc1,c2,c3,c4,c5inzip(ch1,ch2,ch3,ch4,ch5)]returnresdefwrite2csv(results):"""将结果写入csv文件Params:results(list):"""df=pd.DataFrame(results,columns=['file_name','file_code'])df.file_name=df.file_name.apply(lambdax:x.split('/')[-1])save_name='/content/drive/我的Drive/Data/Datawhale-DigitsRecognition/results.csv'df.to_csv(save_name,sep=',',index=None)print('Results.savedto%s'%save_name)是在全局池化层之后汇总的lastbn层可以在一定程度上防止过拟合,在验证集上的准确率更接近于训练集的准确率。使用LabelSmooth后,模型可以更快收敛。使用更大尺寸的输出(从64*128到128*256)后,网络的准确率有了显着提高。原因可能是我们使用了ImageNet的预训练模型。预训练模型使用224*224的大小作为输入,所以我们可以结合数据集的特点,尽可能调整输入大小,使其接近预训练模型的输入。这通常效果更好。模型训练后期,在训练集上的准确率已经接近100%,但在验证集上的准确率几乎没有上升,基本在70%左右的水平。还是有一定的过拟合效果。这种过拟合效应也可能是测试集和训练集分布不一致造成的,所以下一步可以尝试重新划分训练集和测试集训练。如果条件允许,可以进行K折交叉返回验证。
