当前位置: 首页 > 科技观察

PyTorch最佳实践,如何写出漂亮的代码

时间:2023-03-15 16:29:26 科技观察

虽然这是一份非官方的PyTorch指南,但本文总结了一年多使用PyTorch框架的经验,尤其是使用它开发深度学习相关工作***解决方案.请注意,我们分享的大部分经验都是从研究和实践的角度出发的。这是一个开发中的项目,欢迎其他读者改进文档:https://github.com/IgorSusmelj/pytorch-styleguide。本文档主要由三部分组成:首先,本文将简要盘点Python中的最新配置。然后,本文将介绍一些使用PyTorch的技巧和建议。***,我们分享了一些使用其他框架的见解和经验,这些框架通常可以帮助我们改进工作流程。一、Python设备盘点1、推荐使用Python3.6以上版本。根据我们的经验,我们建议使用Python3.6或更高版本,因为它们具有以下特性,可以方便我们编写简洁的代码:从Python3.6及更高版本开始,“typing”模块从Python3.6开始支持格式化字符串(fstring).2.Python风格指南我们尝试遵循Google的Python编程风格。请参阅Google出色的Python编码风格指南:https://github.com/google/styleguide/blob/gh-pages/pyguide.md。在这里,我们总结一下最常用的命名规范:3.集成开发环境一般来说,我们推荐使用visualstudio或PyCharm等集成开发环境。虽然VSCode在相对轻量级的编辑器中提供语法高亮显示和自动完成功能,但PyCharm具有许多用于处理远程集群任务的高级功能。4.JupyterNotebooksVSPython脚本一般而言,我们建议使用JupyterNotebooks进行初步探索,或者尝试新的模型和代码。如果你想在更大的数据集上训练模型,你应该使用Python脚本,其中可重复性更重要。我们推荐以下工作流程:一开始,使用JupyterNotebook探索数据和模型在笔记本单元中构建类/方法将代码移植到Python脚本在服务器上训练/部署5.开发常备库中常用的程序库有:6.文件组织不要将所有图层和模型放在同一个文件中。最好的方法是将最终的网络分离成一个单独的文件(networks.py),并将层、损失函数和各种操作保留在它们自己的文件中(layers.py、loss.py、ops.py)。生成的模型(由一个或多个网络组成)应以模型命名(例如,yolov3.py、DCGAN.py)并引用各个模块。主程序、单独的训练和测试脚本应该只需要导入带有模型名称的Python文件。2.PyTorch开发风格和技术我们建议将网络分解成更小的可重用部分。nn.Module网络包含各种操作或其他构建块。损失函数也包含在nn.Module中,因此它们可以直接集成到网络中。继承自nn.Module的类必须有一个“forward”方法,实现各个层或操作的前向传递。nn.module可以通过“self.net(input)”处理输入数据。这里,对象的“call()”方法直接用于将输入数据传递给模块。output=self.net(input)1.PyTorch环境下的一个简单网络一个简单的网络只有一个输入和输出可以使用下面的模式来实现:classConvBlock(nn.Module):def__init__(self):super(ConvBlock,self).__init__()block=[nn.Conv2d(...)]block+=[nn.ReLU()]block+=[nn.BatchNorm2d(...)]self.block=nn.Sequential(*block)defforward(self,x):returnself.block(x)classSimpleNetwork(nn.Module):def__init__(self,num_resnet_blocks=6):super(SimpleNetwork,self).__init__()#hereweaddtheindividuallayerslayers=[ConvBlock(...)]foriinrange(num_resnet_blocks):layers+=[ResBlock(...)]self.net=nn.Sequential(*layers)defforward(self,x):returnsself.net(x)请注意以下几点:我们重用了一个简单的循环构建块(如ConvBlocks),由相同的循环模式(卷积、激活函数、归一化)组成,加载到单独的nn.Module中。我们构建所需层的列表,最后使用“nn.Sequential()”将所有层组合成一个模型。我们在列表对象前使用“*”运算符来扩展它。在正向传递期间,我们直接在输入数据上运行模型。2.PyTorch环境下的简单残差网络.)defbuild_conv_block(self,...):conv_block=[]conv_block+=[nn.Conv2d(...),norm_layer(...),nn.ReLU()]ifuse_dropout:conv_block+=[nn.Dropout(...)]conv_block+=[nn.Conv2d(...),norm_layer(...)]returnnn.Sequential(*conv_block)defforward(self,x):out=x+self.conv_block(x)returnHere,ResNet模块的skipconnections直接在forwardpass中实现,PyTorch允许在forwardpass中进行动态操作。3.PyTorch环境下的多输出网络对于多输出网络(比如使用预训练的VGG网络构建感知损失),我们使用以下模式:classVgg19(torch.nn.Module):def__init__(self,requires_grad=False):super(Vgg19,self).__init__()vgg_pretrained_features=models.vgg19(pretrained=True).featuresself.slice1=torch.nn.Sequential()self.slice2=torch.nn.Sequential()self.slice3=torch.nn.Sequential()forxinrange(7):self.slice1.add_module(str(x),vgg_pretrained_features[x])forxinrange(7,21):self.slice2.add_module(str(x),vgg_pretrained_features[x])forxinrange(21,30):self.slice3.add_module(str(x),vgg_pretrained_features[x])ifnotrequires_grad:forparaminself.parameters():param.requires_grad=Falseeffort(self,x):h_relu1=self.slice1(x)h_relu2=self.slice2(h_relu1)h_relu3=self.slice3(h_relu2)out=[h_relu1,h_relu2,h_relu3]returnout请注意以下几点:我们使用“torchvision”包提供的预训练模型,我们划分一个网络成三e个模块,每个模块由预训练模型中的层组成我们通过设置“requires_grad=False”来固定网络权重我们返回一个包含三个模块输出的列表4。自定义损失函数即使PyTorch拥有大量标准损失函数,您有时可能需要创建自己的损失函数。为此,您需要创建一个单独的“losses.py”文件并通过扩展“nn.Module”创建您的自定义损失函数:classCustomLoss(torch.nn.Module):def__init__(self):super(CustomLoss,self).__init__()defforward(self,x,y):loss=torch.mean((x-y)**2)returnloss5.训练模型的***代码结构对于训练的***代码结构,我们需要使用以下两种模式:使用prefetch_generator中的BackgroundGenerator来加载下一批数据使用tqdm来监控训练过程和显示计算效率,这可以帮助我们找到数据加载过程中的瓶颈#importstatementsimporttorchimporttorch.nnasnnfromtorch.utilsimportdata...#setflags/seedstorch.backends.cudnn.benchmark=Truenp.random.seed(1)torch.manual_seed(1)torch.cuda.manual_seed(1)...#Startwithmaincodeif__name__=='__main__':#argparseforadditionalflagsforexperimentparser=argparse.ArgumentParser(description="Trainanetworkfor...")...opt=parser.parse_args()#addcodefordatasets(wealwaystrainandvalidation/testset)data_transforms=transforms.Compose([transforms.Resize((opt.img_size,opt.img_size)),transforms.RandomHorizo??ntalFlip(),transforms.ToTensor(),transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0。5))])train_dataset=datasets.ImageFolder(root=os.path.join(opt.path_to_data,"train"),transform=data_transforms)train_data_loader=data.DataLoader(train_dataset,...)test_dataset=datasets.ImageFolder(root=os.path.join(opt.path_to_data,"test"),transform=data_transforms)test_data_loader=data.DataLoader(test_dataset...)...#instantiatenetwork(whichhasbeenimportedfrom*networks.py*)net=MyNetwork(。..)...#createlosses(criterioninpytorch)criterion_L1=torch.nn.L1Loss()...#ifrunningonGPUandwewanttousecudamovemodelthereuse_cuda=torch.cuda.is_available()ifuse_cuda:netnet=net.cuda()...#createoptimizersoptim=torch。optim.Adam(net.parameters(),lr=opt.lr)...#loadcheckpointifneeded/wantedstart_n_iter=0start_epoch=0ifopt.resume:ckpt=load_checkpoint(opt.path_to_checkpoint)#custommethodforloadinglastcheckpointnet.load_state_dict(ckpt['net'])start_epoch=ckpt['epoch']start_n_iter=ckpt['n_iter']optim.load_state_dict(ckpt['optim'])print("lastcheckpointtrestored")...#ifwewanttorunexperimentonmultipleGPUswemovethemodelstherenet=torch.nn.DataParallel(net)...#typicallyweusetensorboardXtokeeptrackofexperimentswriter=SummaryWriter(...)#nowwestartthemainloopn_iter=start_n_iterforepochinrange(start_epoch,opt.epochs):#setmodelstotrainmodenet.train()...#useprefetch_generatorandtqdmforiteratingthroughdatapbar=tqdm(枚举(BackgroundGenerator(train_data_loader,...)),total=len(train_data_loader))start_time=time.time()#forloopgoingthroughdatasetfori,datainpbar:#datapreparationimg,label=dataifuse_cuda:imgimg=img.cuda()labellabel=label.cuda()...#使用tqdm来跟踪准备时间和计算时间是非常好的做法,可以在您的数据加载器中找到任何问题prepare_time=start_time-time.time()#forwardandbackwardpassoptim.zero_grad()...loss.backward()optim.step()...#udpatetensorboardXwriter.add_scalar(...,n_iter)...#computecomputationtimeand*compute_efficiency*process_time=start_time-time.time()-prepare_timepbar.set_description("Computeefficiency:{:.2f},epoch:{}/{}:".format(process_time/(process_time+prepare_time),epoch,opt.epochs))start_time=time.time()#maybedoatestpasseveryxepochsifepoch%x==x-1:#bringmodelstoevaluationmodenet.eval()...#dosometestspbar=tqdm(enumerate(BackgroundGenerator(test_data_loader,...)),total=len(test_data_loader))fori,datainpbar:...#savecheckpointifneeded..3.PyTorch中的多GPU训练PyTorch中有两种多GPU训练模式。根据我们的经验,这两种方式都是有效的。但是,第一种方法产生更好的结果并且需要更少的代码。由于GPU间通信较少,第二种方法似乎有轻微的性能优势。1.最常见的划分每个网络输入的batch的方式是直接将所有网络的输入分成不同的batch数据,分发给每个GPU。这样,在1个GPU上运行的批量大小为64的模型在2个GPU上运行时变为每批32个。可以使用“nn.DataParallel(model)”包装器自动执行此过程。2.将所有网络打包成一个超级网络并拆分输入批次。这种模式不太常用。下面的repo显示了Nvidia对pix2pixHD的实现,它有一个实现这个方法。地址:https://github.com/NVIDIA/pix2pixHD四、在PyTorch中该做什么和不该做什么1、避免在“nn.Module”的“forward”方法中使用Numpy代码。Numpy在CPU上运行。它比torch的代码运行得慢。由于torch的开发方式与numpy类似,因此PyTorch已经支持Numpy中的大多数功能。2.将“DataLoader”与主程序代码分开加载数据的工作流程应该独立于你的主训练程序代码。PyTorch使用“后台”进程在不干扰主要训练进程的情况下更有效地加载数据。3.不要在每一步都记录结果通常,我们训练模型需要数千步。因此,为了减少计算开销,每n步记录一次loss等计算结果就足够了。特别是,在训练期间将中间结果保存为图像非常昂贵。4.使用命令行参数使用命令行参数设置代码执行中使用的参数(batchsize,learningrate等)非常方便。一个简单的实验参数跟踪方法,即直接打印出从“parse_args”收到的字典(dict数据):#savesargumentstoconfig.txtfileopt=parser.parse_args()withopen("config.txt","w")asf:f.write(opt.__str__())5.如果可能,使用“使用.detach()”从计算图中释放张量。对于自动微分,PyTorch跟踪所有涉及张量的操作。请使用“.detach()”来防止记录不必要的操作。6.使用“.item()”打印标量张量可以直接打印变量。但是,我们建议您使用“variable.detach()”或“variable.item()”。在早期版本的PyTorch(<0.4)中,您必须使用“.data”来访问变量中的张量值。7.在“nn.Module”中使用“call”方法而不是“forward”方法这两种方式并不完全相同,如下GitHub问题指出:https://github.com/IgorSusmelj/pytorch-styleguide/issues/3output=self.net.forward(input)#theyarenotequal!output=self.net(input)原文链接:https://github.com/IgorSusmelj/pytorch-styleguide【本文为专栏《HeartoftheMachine》,微信公众号《机器之心(id:almosthuman2014)》】点此查看作者更多好文