在上一篇文章中我们全面介绍了如何使用fast。ai构建自动驾驶卡车模拟器,最终这些方法可以处理任何你需要微调预训练模型或开发预测包围框和分类的情况。 现在我的目标是逐步了解训练和推理过程的一些更技术性的方面,并解释它们如何在PyTorch中实现的细节。你可以参考此Github存储库中的代码库。 回想上一篇文章,有两个神经网络在工作。预测转弯方向的DNN。预测汽车、人等的包围框和类别的DNN。1、微调转弯方向模型 两个网络都以预训练的resnet34网络开始,并针对适当的任务进行微调。 可以从torchvision。models获得预训练的resnet34:importtorchvision。modelsasmodelsarchmodels。resnet34(pretrainedTrue) 所有预训练模型都已在1000个分类的Imagenet数据集上进行了预训练。 为了微调预训练网络,我们基本上是从一组权重开始,这些权重已经包含了很多关于嵌入其中的Imagenet数据集的信息。所以我们可以用两种方法之一来做到这一点。一种方法是通过设置requiresgradFalse来冻结所有早期层,然后只为最后一层设置requiresgradTrue。另一种方法只使用所有权重作为初始化值并继续对我们的新训练数据进行训练。 对于冻结早期层并仅训练最终层的选项1,我们可以为所有层设置requiresgradFalse,然后删除并替换最后一层。无论何时将层分配给网络,它都会自动将requiresgrad属性设置为True。classFlatten(nn。Module):definit(self):super(Flatten,self)。init()defforward(self,x):xx。view(x。size(0),1)returnxclassnormalize(nn。Module):definit(self):super(normalize,self)。init()defforward(self,x):xF。normalize(x,p2,dim1)returnxlayerlistlist(arch。children())〔2:〕archnn。Sequential(list(arch。children())〔:2〕)arch。avgpoolnn。AdaptiveAvgPool2d(outputsize(1,1))arch。fcnn。Sequential(Flatten(),nn。Linear(infeatureslayerlist〔1〕。infeatures,outfeatures3,biasTrue),normalize())archarch。to(device) 如果查看resnet34的架构,你会发现最后一个conv块后跟一个AdaptiveAvgPool2d和一个Linear层。 我们可以使用nn。Sequential(list(arch。children())〔:2〕)删除最后两层,然后使用arch。avgpoolnn。AdaptiveAvgPool2d(outputsize(1,1))和另一个带有Flatten、Linear和normalize层的nn。Sequential。我们最终想要预测3个类别:左、右、直所以我们的outfeatures将为3。 现在我们将为方向模型创建数据集和数据加载器。由于我们的数据只是图像和分类〔left,right,straight〕,我们可以只使用内置的torch数据集类,但无论如何我都喜欢使用自定义类,因为我可以准确地看到数据是如何更容易地提取的。classDirectionsDataset(Dataset):Directionsdataset。definit(self,csvfile,rootdir,transformNone):Args:csvfile(string):Pathtothecsvfilewithlabels。rootdir(string):Directorywithalltheimages。transform(callable,optional):Optionaltransformself。labelpd。readcsv(csvfile)self。rootdirrootdirself。transformtransformdeflen(self):returnlen(self。label)defgetitem(self,idx):imgnameos。path。join(self。rootdir,self。label。iloc〔idx,0〕)imageio。imread(imgname。jpg)sampleimagelabelself。label。iloc〔idx,1〕ifself。transform:sampleself。transform(sample)returnsample,label 在csv文件中的图像名称没有扩展名,因此是imgname。jpg。tensordatasetDirectionsDataset(csvfiledatalabelsdirections。csv,rootdirdatatrain3,transformtransforms。Compose(〔transforms。ToTensor(),transforms。Normalize((0。5,0。5,0。5),(0。5,0。5,0。5))〕))dataloaderDataLoader(tensordataset,batchsize16,shuffleTrue) 所以我们准备开始训练模型。deftrainmodel(model,criterion,optimizer,scheduler,dataloader,numepochs25):sincetime。time()FTlosses〔〕bestmodelwtscopy。deepcopy(model。statedict())bestacc0。0iters0forepochinrange(numepochs):print(Epoch{}{}。format(epoch,numepochs1))print(10)scheduler。step()model。train()Setmodeltotrainingmoderunningloss0。0runningcorrects0Iterateoverdata。fori,(inputs,labels)inenumerate(dataloader):settrace()inputsinputs。to(device)labelslabels。to(device)zerotheparametergradientsoptimizer。zerograd()forwardtrackhistoryifonlyintrainmodel。eval()Setmodeltoevaluatemodewithtorch。nograd():outputsmodel(inputs)settrace(),predstorch。max(outputs,1)outputsmodel(inputs)losscriterion(outputs,labels)backwardoptimizeonlyifintrainingphaseloss。backward()optimizer。step()FTlosses。append(loss。item())statisticsrunninglossloss。item()inputs。size(0)runningcorrectstorch。sum(predslabels。data)settrace()iters1ifiters20:print(PrevLoss:{:。4f}PrevAcc:{:。4f}。format(loss。item(),torch。sum(predslabels。data)inputs。size(0)))epochlossrunninglossdatasetsizeepochaccrunningcorrects。double()datasetsizeprint(Loss:{:。4f}Acc:{:。4f}。format(epochloss,epochacc))deepcopythemodelifepochaccbestacc:bestaccepochaccbestmodelwtscopy。deepcopy(model。statedict())timeelapsedtime。time()sinceprint(Trainingcompletein{:。0f}m{:。0f}s。format(timeelapsed60,timeelapsed60))print(BestvalAcc:{:4f}。format(bestacc))loadbestmodelweightsmodel。loadstatedict(bestmodelwts)returnmodel,FTlosses 在这个训练循环中,如果epoch准确度是目前为止最好的,我们可以跟踪最佳模型权重。我们还可以跟踪每次迭代和每个时期的损失,并在最后返回以绘制并查看调试或演示的样子。 请记住,模型在每次迭代时都在接受训练,如果你停止训练循环,它将保留这些权重,只需再次运行trainmodel()命令即可再次继续训练。要再次从头开始,请返回并使用预训练架构重新初始化权重。criterionnn。CrossEntropyLoss()Observethatallparametersarebeingoptimizedoptimizerftoptim。SGD(arch。parameters(),lr1e2,momentum0。9)DecayLRbyafactorofgammaeverystepsizeepochsexplrschedulerlrscheduler。StepLR(optimizerft,stepsize7,gamma0。1)arch,FTlossestrainmodel(arch,criterion,optimizerft,explrscheduler,dataloader,numepochs5)2、微调包围框模型 同样,我们将使用预训练的resnet34架构。然而,这一次我们将不得不对其进行更实质性的编辑,以输出类别预测和边界框值。此外,这是一个多类别预测问题,因此可能有1个边界框,也可能有15个因此同时也有1个或15个类别。 我们将以类似于替换方向模型中的图层的方式为架构创建自定义头。classStdConv(nn。Module):definit(self,nin,nout,stride2,drop0。1):super()。init()self。convnn。Conv2d(nin,nout,3,stridestride,padding1)self。bnnn。BatchNorm2d(nout)self。dropnn。Dropout(drop)defforward(self,x):returnself。drop(self。bn(F。relu(self。conv(x))))defflattenconv(x,k):bs,nf,gx,gyx。size()xx。permute(0,2,3,1)。contiguous()returnx。view(bs,1,nfk)classOutConv(nn。Module):definit(self,k,nin,bias):super()。init()self。kkself。oconv1nn。Conv2d(nin,(len(id2cat)1)k,3,padding1)self。oconv2nn。Conv2d(nin,4k,3,padding1)self。oconv1。bias。data。zero()。add(bias)defforward(self,x):return〔flattenconv(self。oconv1(x),self。k),flattenconv(self。oconv2(x),self。k)〕drop0。4classSSDMultiHead(nn。Module):definit(self,k,bias):super()。init()self。dropnn。Dropout(drop)self。sconv0StdConv(512,256,stride1,dropdrop)self。sconv1StdConv(256,256,dropdrop)self。sconv2StdConv(256,256,dropdrop)self。sconv3StdConv(256,256,dropdrop)self。out0OutConv(k,256,bias)self。out1OutConv(k,256,bias)self。out2OutConv(k,256,bias)self。out3OutConv(k,256,bias)defforward(self,x):xself。drop(F。relu(x))xself。sconv0(x)xself。sconv1(x)o1c,o1lself。out1(x)xself。sconv2(x)o2c,o2lself。out2(x)xself。sconv3(x)o3c,o3lself。out3(x)return〔torch。cat(〔o1c,o2c,o3c〕,dim1),torch。cat(〔o1l,o2l,o3l〕,dim1)〕 现在我们需要将这个自定义头连接到resnet34架构,有一个方便的函数可以做到这一点:classConvnetBuilder():definit(self,f,c,ismulti,isreg,psNone,xtrafcNone,xtracut0,customheadNone,pretrainedTrue):self。f,self。c,self。ismulti,self。isreg,self。xtracutf,c,ismulti,isreg,xtracutxtrafc〔512〕ps〔0。25〕len(xtrafc)〔0。5〕self。ps,self。xtrafcps,xtrafccut,self。lrcut〔8,6〕specifictoresnet34archcutxtracutlayerscutmodel(f(pretrained),cut)self。nfnumfeatures(layers)2self。topmodelnn。Sequential(layers)nfclen(self。xtrafc)1self。ps〔self。ps〕nfcfclayers〔customhead〕self。nfclen(fclayers)self。fcmodelnn。Sequential(fclayers)。to(device)self。modelnn。Sequential((layersfclayers))。to(device)defcutmodel(m,cut):returnlist(m。children())〔:cut〕ifcutelse〔m〕defnumfeatures(m):cchildren(m)iflen(c)0:returnNoneforlinreversed(c):ifhasattr(l,numfeatures):returnl。numfeaturesresnumfeatures(l)ifresisnotNone:returnresdefchildren(m):returnmifisinstance(m,(list,tuple))elselist(m。children()) 使用这个ConvnetBuilder类,我们可以结合自定义头和resnet34架构。klen(anchorscales)headreg4SSDMultiHead(k,4。)fmodelmodels。resnet34modelssConvnetBuilder(fmodel,0,0,0,customheadheadreg4) k是9。 我们现在可以通过models的model属性访问模型。 损失函数必须能够接受分类(类)和连续值(边界框)并输出单个损失值。defssdloss(pred,targ,printitFalse):lcs,lls0。,0。forbc,bbb,bbox,clasinzip(pred,targ):locloss,claslossssd1loss(bc,bbb,bbox,clas,printit)llsloclosslcsclaslossifprintit:print(floc:{lls。data。item()},clas:{lcs。data。item()})returnllslcsdefssd1loss(bc,bbb,bbox,clas,printitFalse):bbox,clasgety(bbox,clas)aicactntobb(bbb,anchors)overlapsjaccard(bbox。data,anchorcnr。data)gtoverlap,gtidxmaptogroundtruth(overlaps,printit)gtclasclas〔gtidx〕posgtoverlap0。4posidxtorch。nonzero(pos)〔:,0〕gtclas〔1pos〕len(id2cat)gtbboxbbox〔gtidx〕locloss((aic〔posidx〕gtbbox〔posidx〕)。abs())。mean()claslosslossf(bc,gtclas)returnlocloss,claslossdefonehotembedding(labels,numclasses):returntorch。eye(numclasses)〔labels。data。long()。cpu()〕classBCELoss(nn。Module):definit(self,numclasses):super()。init()self。numclassesnumclassesdefforward(self,pred,targ):tonehotembedding(targ,self。numclasses1)tV(t〔:,:1〕。contiguous())。cpu()xpred〔:,:1〕wself。getweight(x,t)returnF。binarycrossentropywithlogits(x,t,w,sizeaverageFalse)self。numclassesdefgetweight(self,x,t):returnNonelossfBCELoss(len(id2cat))defgety(bbox,clas):bboxbbox。view(1,4)szbbkeep((bbox〔:,2〕bbox〔:,0〕)0)。nonzero()〔:,0〕returnbbox〔bbkeep〕,clas〔bbkeep〕defactntobb(actn,anchors):actnbbstorch。tanh(actn)actncenters(actnbbs〔:,:2〕2gridsizes)anchors〔:,:2〕actnhw(actnbbs〔:,2:〕21)anchors〔:,2:〕returnhw2corners(actncenters,actnhw)defintersect(boxa,boxb):maxxytorch。min(boxa〔:,None,2:〕,boxb〔None,:,2:〕)minxytorch。max(boxa〔:,None,:2〕,boxb〔None,:,:2〕)intertorch。clamp((maxxyminxy),min0)returninter〔:,:,0〕inter〔:,:,1〕defboxsz(b):return((b〔:,2〕b〔:,0〕)(b〔:,3〕b〔:,1〕))defjaccard(boxa,boxb):interintersect(boxa,boxb)unionboxsz(boxa)。unsqueeze(1)boxsz(boxb)。unsqueeze(0)interreturninterunion 一旦我们设置了数据集和数据加载器,就可以在bbox模型的批量输出上测试损失函数。 这里我们实际上需要一个自定义数据集类来处理这些数据类型。classBboxDataset(Dataset):Bboxdataset。definit(self,csvfile,rootdir,transformNone):Args:csvfile(string):Pathtocsvfilewithboundingboxes。rootdir(string):Directorywithalltheimages。transform(callable,optional):Optionaltransform。self。labelpd。readcsv(csvfile)self。rootdirrootdirself。transformtransformself。sz224deflen(self):returnlen(self。label)defgetitem(self,idx):imgnameos。path。join(self。rootdir,self。label。iloc〔idx,0〕)imageio。imread(imgname)sampleimageh,wsample。shape〔:2〕;newh,neww(224,224)bbnp。array(〔float(x)forxinself。label。iloc〔idx,1〕。split()〕,dtypenp。float32)bbnp。reshape(bb,(int(bb。shape〔0〕2),2))bbbb〔newhh,newww〕bbbb。flatten()bbT(np。concatenate((np。zeros((1894)len(bb)),bb),axisNone))189is219where9kifself。transform:sampleself。transform(sample)returnsample,bb 这个自定义数据集类处理边界框,但我们想要一个处理类和边界框的数据集类。bbdatasetBboxDataset(csvfiledatapascaltmpmbb。csv,rootdirdatapascalVOCdevkit2VOC2007JPEGImages,transformtransforms。Compose(〔transforms。ToPILImage(),transforms。Resize((224,224)),transforms。ToTensor(),transforms。Normalize((0。5,0。5,0。5),(0。5,0。5,0。5))〕))bbdataloaderDataLoader(bbdataset,batchsize16,shuffleTrue) 在这里,我们可以连接两个数据集类,以便为每个图像返回类和边界框。classConcatLblDataset(Dataset):definit(self,ds,y2):self。ds,self。y2ds,y2self。szds。szdeflen(self):returnlen(self。ds)defgetitem(self,i):self。y2〔i〕np。concatenate((np。zeros(189len(self。y2〔i〕)),self。y2〔i〕),axisNone)x,yself。ds〔i〕return(x,(y,self。y2〔i〕))trnds2ConcatLblDataset(bbdataset,mcs) 其中mcs是一个numpy数组,其中包含每个训练图像的类。PATHpascalPath(datapascal)trnjjson。load((PATHpascalpascaltrain2007。json)。open())catsdict((o〔id〕,o〔name〕)forointrnj〔categories〕)mc〔〔cats〔p〔1〕〕forpintrnanno〔o〕〕forointrnids〕id2catlist(cats。values())cat2id{v:kfork,vinenumerate(id2cat)}mcsnp。array(〔np。array(〔cat2id〔p〕forpino〕)foroinmc〕) 现在我们可以测试自定义损失。sz224x,ynext(iter(bbdataloader2))batchmodelss。model(x)ssdloss(batch,y,True)tensor(〔0。6254〕)tensor(〔0。6821,0。7257,0。4922〕)tensor(〔0。9563〕)tensor(〔0。6522,0。5276,0。6226〕)tensor(〔0。6811,0。3338〕)tensor(〔0。7008〕)tensor(〔0。5316,0。2926〕)tensor(〔0。9422〕)tensor(〔0。5487,0。7187,0。3620,0。1578〕)tensor(〔0。6546,0。3753,0。4231,0。4663,0。2125,0。0729〕)tensor(〔0。3756,0。5085〕)tensor(〔0。2304,0。1390,0。0853〕)tensor(〔0。2484〕)tensor(〔0。6419〕)tensor(〔0。5954,0。5375,0。5552〕)tensor(〔0。2383〕)loc:1。844399333000183,clas:79。79206085205078 Out〔1024〕:tensor(81。6365,gradfn) 现在训练ssd模型:beta10。5optimizeroptim。Adam(modelss。model。parameters(),lr1e3,betas(beta1,0。99))DecayLRbyafactorofgammaeverystepsizeepochsexplrschedulerlrscheduler。StepLR(optimizer,stepsize7,gamma0。1) 我们可以使用与以前基本相同的trainmodel()函数,但这次我们将边界框和类的列表传递给损失函数ssdloss()。 现在我们已经在新的训练数据集上训练了我们的两个模型,准备好使用它们对我们的卡车模拟器游戏进行推理。 玩得开心! 原文链接:http:www。bimant。comblogpytorchuavsimulator