赞
踩
CycleGAN是一款实现风格迁移的模型,其论文可以在各大平台找到。我们在aixiv上可以找到:https://arxiv.org/pdf/1703.10593.pdf。
我们复现的的代码是来自下面这个github仓库:https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix。
虽然看起来很简单,不过对于刚入门的新手来说难度还真不低,下面让我们来仔细看看代码的结构。
我们将目光聚集到根目录这个位置。
这里面,我们先来看根目录下的文件:
conda env create -f environment.yml
来创建一个environment.yml文件里指定的环境(里头什么样的包、环境名是什么、Python版本是多少)。当然,如果你现在就有这样的一个环境,想要导出这个环境所对应的environment.yml,只需要下面的命令就可以conda env export | grep -v "^prefix: " > environment.yml
。python train.py --dataroot ./datasets/maps --name maps_cyclegan --model cycle_gan
。python test.py --dataroot ./datasets/maps --name maps_cyclegan --model cycle_gan
来实现,其中的–model cycle_gan会将数据导入的模式变为双向。对于CycleGAN的单项检验,可以利用命令python test.py --dataroot datasets/horse2zebra/testA --name horse2zebra_pretrained --model test --no_dropout
来实现。–no_dropout指的是不需要dropout;–model test指的是单向验证CycleGAN模型,这将使得–dataset_mode自动变成single,也就是导入单一集合的数据。21-25行不说了,都是导入一些基本的类。
第27行的意思是,如果这个脚本作为主脚本使用,那么就运行下方的东西。28行是先把TrainOption实例化成对象,然后用parse进行解析,这样形成一个结果,赋给opt,也就是说,opt解析出来的结果。29行是根据这个结果去创建数据集。30行获取数据集中样本的数量。31行不说了。
33行是创建模型。34行是根据opt创建合适的学习率调整策略、导入网络并打印。第35行是根据opt创建可视化实例。36行是训练迭代次数。
第38行是迭代过程的开始,opt.epoch_count是从哪个epoch开始,opt.n_epochs_decay是持续多少epoch。39-40行不说了,获取这个epoch开始的时间和本轮epoch导入数据的时间。41行是在本轮epoch当中的第几次迭代。42行是可视化机器的重置,保证在每个epoch里它至少有一次保存图片。43行是在每次epoch之前率先更新一下学习率。
第44行就是每个epoch内部的循环了,enumerate函数的作用是同时列出数据和下标,这个无需多说,注意这里的i是batch的编号,而data也不是一张图,而是一个batch的图。45行是本次iteration开始的时间。46-47行是说如果总迭代次数total_iters到了opt.print_freq的整倍数,就计算t_data,也就是本轮iteration开始的时刻到本轮epoch导入数据的时间已经过去了多久。49-50行指的是,一共多少个数据参与了训练以及本轮epoch里有多少数据参与了迭代。51行是把每一个数据解包,52行是参数优化,这些都是在model当中的basemodel.py当中定义的。
54-57行是数据可视化的部分,如果本轮epoch当中已经参与迭代的样本总数是opt.display_freq的整数倍,那么执行55-57行的操作。55行是返回一个叫save_result的布尔值,用于判定是否需要存出结果到html文件里。第56行是只有在着色任务中才有用,是展示图片的命令,其他的模型中compute_visuals函数只有一个命令,那就是pass。第57行则是存储到html文件里的命令,其中save_result就决定本行是否执行,可以参见util文件夹里的visualizer.py。
59-64行是打印的部分。如果本轮epoch当中已经参与迭代的样本总数是opt.print_freq的整数倍,那么执行60-64行的操作。第60行是获取当前的损失函数。第61行是计算每个图片所用的时间。第62行是输出当前的损失值,后面的参数含义大家可以点击util文件夹下的visuallizer.py去查看相应的函数。63-64行是损失值可视化的部分,如果window id of the web display这个值大于0,那么就利用plot_current_losses函数输出,参数的含义可以点击util文件夹下的visuallizer.py去查看相应的函数。
66-69行是保存权重文件的部分,如果本轮epoch当中已经参与迭代的样本总数是opt.save_latest_freq的整数倍,那么执行67-69行的操作。67行不说了,68行是设置保存后缀,69行是保存模型。
71行是重新获取时间。72-75行也是在保存模型,不过这次是在每个epoch结束的时候。
77行是输出,无需多言。
它将从’–checkpoints_dir’加载保存的模型,并将结果保存到’–results_dir’。它首先在给定opt选项的情况下创建模型和数据集。它将硬编码一些参数。然后,它对“–num_test”图像运行推断,并将结果保存到HTML文件中。
29-34行不用多说了,导入包。
36-39行是导入wandb包,可以帮我们记录超参数指标。
42-43行不说了,和上面的train.py有异曲同工之妙。45行,测试模式仅能使用单线程,至于哪一个线程,你可以去自己指定;46行,batch_size只能为1;47行是确定数据需不需要打乱;48行则是是否翻转;49行是放弃展示图片;50-52行不说了,上面的train.py里解释过是什么含义。
54-57行,没太看懂。不太熟悉wandb这个包的含义。
59-行,是在创建网站。60行是在确定地址。61-62行是要根据本轮迭代来确定网址的域名(整体)。63行不说了,就是打印一下结果。64行是确定网页的地址和标题(这块可能得在util文件夹里头找html.py)。68-69行是评估模式开启。
70-72行比较简单,不再重复。73-74行也比较容易理解,分别是解包数据、测试。75-78行可以参考注释,获取图像结果、获取图像路径,每隔5个图片打印一次。79行是保存图片到html中,参数的含义可以点击util文件夹下的visuallizer.py去查看相应的函数。最后80行,保存html。
之后,我们再来看看各个文件夹都是怎么回事。
docs文件夹不多说了,里头是各种说明文档。
.git文件夹也不多说了,这是用于分布式版本管理的工具,具体什么是git请自行百度。在我【教程搬运】的专栏下也有专门介绍git的博文。
data文件夹,里头是各种和数据加载、处理的模块。里头的__init__.py是一个接口文件,basedataset.py是一个基础文件(包含一些常见的转换功能,有点相当于公用的“基类”,不知道怎么描述了),template_dataset.py这是一个模板,相当于示例文件。其它的都是具体的数据集对应的文件了。
首先让我们聚焦一个模板文件,也就是template_dataset.py,在这里我们仅仅给出一些说明,读完之后觉得抽象也没关系,我们后面还有例子(1.2.3.2节之后),慢慢体会,慢慢读就可以了。
这个文件主要起到一个模板的作用,是一个参考,具体说明如下:
好了,刚刚我们已经说明了这个模板函数的作用,下面让我们详细地说一下要实现的是个具体功能:
为了便于大家理解__init__函数,我列举了single_dataset.py这个脚本里的内容进行举例。
def __init__(self, opt):
"""Initialize this dataset class.
Parameters:
opt (Option class) -- stores all the experiment flags; needs to be a subclass of BaseOptions
"""
# 调用BaseDataset.__init__方法,将创建好的对象self和你在训练命令里的opt传入。
BaseDataset.__init__(self, opt)
# 用opt.dataroot解析出数据路径,opt.max_dataset_size解析出最大允许数据集大小。
# make_dataset函数是用来制作数据集,返回值是一个图片组成的列表。
# 最后使用sorted函数对图片进行一下排序。
self.A_paths = sorted(make_dataset(opt.dataroot, opt.max_dataset_size))
# 这是对输入进行处理的部分。
input_nc = self.opt.output_nc if self.opt.direction == 'BtoA' else self.opt.input_nc
self.transform = get_transform(opt, grayscale=(input_nc == 1))
读完上面我写的,你可能一头雾水,没事,我们马上就利用示例来分析。
这个脚本主要是提供接口。里头分成两部分,第一部分有三个函数,第二部分是一个类,里头也有几个函数。
让我们先进入第一部分。
dataset_filename = "data." + dataset_name + "_dataset"
就是一个简单的拆解,便于第二行datasetlib = importlib.import_module(dataset_filename)
进行导入,有人会好奇这第二行是在干什么,这一行是在动态导入对象,dataset_filename只不过就是一个索引而已,导入之后的结果是一个类实例化后的对象datasetlib。这时候小朋友们可能又要问我了,为什么是一个类实例化后的对象?因为你注意看左侧的data文件夹下,任何一个数据集(比如single)是不是都对应着一个脚本(例如:single_dataset.py),而这个脚本里是不是有一个类名字叫做SingleDataset。以后,凡导入这种类实例化后的对象,都是需要动态导入对象的。有些小朋友又会问了,为什么我们要导入这个类的对象,你不是要导入数据集么?很好,我们来看这个类是不是有__getitem__函数,你的数据都是这么被读入的,你可以理解为,数据集被存到这个类实例化后的对象里了。第三行定义一个变量并初始化dataset = None
。第四行target_dataset_name = dataset_name.replace('_', '') + 'dataset'
,简单的文字编辑。第五行到第八行就要正式进入循环了,我们来看datasetlib.__dict__.items()
,感兴趣的同学可以去查查__dict__是干什么的python中__dict__的作用是什么? 请参见__dict__的用法datasetlib.__dict__.items()
其实就是方便这个循环遍历这个字典,有小朋友可能会问,为什么我们要遍历?我来告诉你,因为这个类里有很多的属性或方法形成的键值对,而我们需要的只有那些图片名称和图片组成的键值对,所以我们才要在这个循环中的if函数判断其键名name是不是我们所需要的类名,然后如果它同时也是BaseDataset的子集,就说明遍历过程中当前这个cls就是我们所需要的数据集所对应的类。把它读出来给dataset。第九行到第十行是报错信息,如果第九行的条件语句被触发了,那么第十行的raise就会自动执行错误信息,raise是Python一个常见的语法。最后,这个方法显然返回的是第十一行的dataset,return dataset
。注意,这个地方的dataset其实还是一个类形成的实例化对象,千万不要认为里头只有图片。来让我们进入第二部分,CustomDatasetDataLoader这个类。
dataset_class = find_dataset_using_name(opt.dataset_mode)
,这是在做什么?右侧的函数返回值是一个实例化对象,也就是说dataset_class只是一个由相应的Dataset这个类实例化之后的对象(这一点从上面 find_dataset_using_name函数的解释就可窥知一二),然后转化成self.dataset,而self.dataset也是个实例化的对象,这个是要传入到torch.utils.data.DataLoader中的,而且你阅读一下torch.utils.data.DataLoader的使用方法就会发现,第一个参数一定是一个实例化之后的对象。torch.utils.data.DataLoader这个是一个非常常用的Pytorch导入数据之用的东西,具体的用法请参见torch.utils.data.DataLoader的用法,而在这个函数里头,第75-79行这几行代码不难理解,因此我不再赘述。总而言之,这个脚本就是围绕着create_dataset这样一个函数展开的,目的就是根据opt制作数据集,只是一个接口而已。
这个文件实现了一些基础的数据读取、转换功能。在BaseDataset类中,所有的函数基本都留空,这个是要根据具体的数据集来确定的,所以base_dataset.py里的这块是空的,可以在template_dataset.py查到每个函数对应的用法示例,而实际的应用就是single_dataset.py、colorizationo_dataset.py等文件里的用法。
我们来看看在BaseDataset类之后有什么函数。
random.randint(0, np.maximum(0, new_w - opt.crop_size))
具体代表什么含义呢?我们不妨这么想,20×20的图片,我们要把它裁剪成10×10,那么裁剪的位置(左上角坐标)只能在左上方10×10的区域之内,要不然的话,被裁剪出的图片就无法成为10×10的图片了,这两行随机数生成就是干这个的;flip参数则是通过生成随机数的方式来计算翻转概率。这里我要多说一句,Transform是torchvision里用于图片预处理的中药模块,里头规定了很多歌变换,使用者经常通过Transform.Compose将其组合起来,如果我们需要的功能源代码里头不能实现,就需要自己定义函数,有关这部分内容可以参见Torchvision中Transform的使用。opt, params=None, grayscale=False, method=Image.BICUBIC, convert=True
,opt不说了,就是指定的各个选项;params是上一个函数获取的,默认情况下是None;grayscale应该是是否进行灰度处理;method指的是放大图片的方法,Image.BICUBIC是双三次插值的意思。注意第83-84行,如果grayscale参数是Ture,就执行transforms.Grayscale(1),这个1指的是一个通道;85-87行是说,如果resize为True,则通过双三次插值把图片resize到指定大小;88-89行是等比调整大小,注意前面是elif而不是if,说明和前面的85-87行是有关联的;91-95行是关于裁剪的设定,先要判定params是不是None,如果不是,那就不能采用随机裁剪,而是指定裁剪左上角的点,并指定裁剪大小;97-98行的意思是,如果什么预处理操作都没有,那么就要通过人工调整把图片的宽、高数值调整成4的倍数;100-104行是翻转的相关操作;106-111行属于细微的调整,执行了两个操作,分别是张量化、标准化。最后将所有的变换操作compose到一起。这是一个和图片读取有关的脚本。1-16行不说了,很容易读懂,我们从后面说起。
先插一句,为什么要准备对齐的数据,这是因为pix2pix模型要用这个。
aligned_dataset.py包含一个可以加载图像对的数据集类。它设置好了一个图像目录/path/to/data/train,其中包含 {A,B} 形式的图像对。在测试期间,您需要准备一个目录/path/to/data/test作为测试数据。那么如何准备对齐的数据集呢,方法在这里。您也可以参阅/pytorch-CycleGAN-and-pix2pix/datasets/combine_A_and_B.py这个脚本,它就是我们在准备对齐数据的时候需要执行的脚本。我们在这里权且先不提,后面还会再说。
让我们来逐个分析在Aligned_dataset类里面的3个函数。
unaligned_dataset.py包含一个可以加载未对齐/未配对数据集的数据集类。我们可以使用数据集标志训练模型--dataroot /path/to/data
。
依然是同样的三个函数,让我们来一一解读。
single_dataset.py包含一个数据集类,可以加载由path指定的一组单个图像–dataroot /path/to/data。它只能用于使用模型选项为一侧生成CycleGAN结果-model test。
里面的三个函数完全就是前面1.2.3.6的翻版,我就不再一一赘述了。
colorization_dataset.py实现了一个数据集类,可以加载一组 RGB 的自然图像,并将 RGB 格式转换为Lab颜色空间中的 (L, ab) 对。基于 pix2pix 的着色模型 ( --model colorization) 需要它。
这里是两个示例图片,也可以被用来存放效果图。
模型目录包含与目标函数、优化和网络架构相关的模块。如果要添加一个名为的自定义模型类dummy,那么必须要添加一个名为的文件dummy_model.py并定义一个DummyModel类,这个类继承父类BaseModel。您需要实现四个功能:__init__初始化类(您需要先调用BaseModel.init(self, opt))、set_input(从数据集中解包数据并应用预处理)、forward(生成中间结果)、optimize_parameters(计算损失、梯度和更新网络权重),以及可选的modify_commandline_options(添加特定于模型的选项并设置默认选项)。现在您可以通过指定 flag 来使用模型类–model dummy。有关示例,请参见我们的模板模型类。下面我们详细解释每个文件。
_init_.py 实现了这个包与训练和测试脚本之间的接口。 train.py并在给定选项的情况下test.py调用from models import create_modelandmodel = create_model(opt)创建模型opt。您还需要调用model.setup(opt)以正确初始化模型。
base_model.py为模型实现了一个抽象基类 ( ABC )。它还包括常用的辅助函数(例如 , setup, test, update_learning_rate, ) save_networks,load_networks以后可以在子类中使用。
我们来看看BaseModel这个类里包含了什么东西。
for optimizer in self.optimizers
指的是对于self.optimizers的每一个优化器,都要获取一下学习率更新的策略,怎么样才能获取呢?就是使用networks.get_scheduler(optimizer, opt)
这个方法,注意到该方法源码就在networks.py的get_scheduler方法里头,我们下面的network.py的讲解里头会提到。第86-88行,如果self.isTrain这个标志不为真,或者只是继续训练(opt.continue_train为真),那么则要加载已有的网络,当然这块我也有个疑问,就是load_networks这个函数的输入明显应该是int类型(下面定义里头有注释),但为什么第88行其输入是一个字符串呢。不论如何,第89行都要根据opt.verbose是否为真打印一下网络。'net' + name
这样一个属性,如果有,则把这个属性值赋给net变量,如果没有则报错,有关getattr()的用法请单击这里。当然,这里的属性值指的就是一个网络,也就是说net代表的是一个网络,所以第96行其实就是开启评估模式。这个脚本主要是用来做模板之用。该模块为用户提供了一个模板来实现自定义模型,可以指定“–model template”来使用此模型,类名应与文件名及其模型选项一致。文件名应该是_dataset.py,类名应该是Dataset.py。它实现了一个简单的基于回归损失的图像到图像的转换baseline。给定输入输出对(data_A,data_B),它学习可以最小化以下L1损失的网络netG,使得:
min_<netG> ||netG(data_A) - data_B||_1
base_model.get_current_losse
来返回损失函数值并存储。第55行是在调用一个函数去存储,这些图片的保存是通过base_model.get_current_visuals
来实现的。第58行是指定需要存储/调用的模型的名字,通过base_model.save_networks
和base_model.load_networks
来实现,你可以使用opt.isTrain
去控制训练和测试模式,这二者往往是不同的。第60行是在定义生成器G,在同一级目录的network里有一个define_G的函数,我们在这里就是调用了这个函数,需要传入的参数包括输入、输出、最后一个卷积层里有多少filter、网络名、使用哪块GPU去训练。第61行,是判断语句,无需多言,注意self.isTrain
是在base_model.py里定义的。第64行是定义一个损失函数,把这个损失函数赋值给self.criterionLoss。第67、68行是在定义一个优化器,并把优化器存入优化器列表里,这里我们选用了Adam优化器,具体的公式请参见这里。self.device
这个装置里,第81行是找到输入图片的路径。criterionLoss
就是L1损失,后面再去算一个回归损失。self.loss_G.backward()
就是计算梯度,并且实现反向传播。第1-5行是导入一些基础的包。前两行比较简单,第三行的作用是让我们可以使用torch.nn.init进行初始化参数,第四行这个包是一个针对函数进行操作的函数,第五行是导入优化器。
下面介绍Identity这个类。
这个类是用来创建不同的GAN对象的,按照注释内所说的,他把“创建与输入大小相同的目标标签张量”这一任务抽象化了。换言之这是用来创建标签用的。
至此,network.py长达616行的脚本解读完毕。
第2行的itertool是一个Python提供的迭代工具箱,第3行image_pool.py实现了一个存储先前生成的图像的图像缓冲区。这个缓冲区使我们能够使用生成图像的历史而不是最新生成器生成的图像来更新鉴别器。后面就是CycleGANModel这个类,第12-15行有非常明确的概述,dataset模式要使用“非对齐、未配对”,它会使用带有9个残差块的生成器网络结构,并使用PatchGAN这样的判别器结构,以及一个最小方根的GANs对象(就是说损失函数使用LSGAN,平方损失),我们来逐个分析其中的函数。
'D_A'
和'D_B
两个判别器的损失,'G_A'
和'G_B'
是两个生成器的损失,上面这四项在公式(1)及公式(1)的变体里边都有体现;'cycle_A'
和'cycle_B'
这两个是循环一致性损失,见公式(2);'idt_A'
和'idt_B'
是公式(5)里出现的,针对图片→照片任务的损失项。第56-61行指的是要展示/保存的图片,57行是真实的A、经cyclegan转化出的B、经cyclegan重建的A,第58行则是换成B;第59、60行是针对着色任务开辟出来的两项;第63行没什么可说的,就是组合而已。第65-68行是模型的组合情况,就是有哪些模型会被使用。第73-76行没啥可说的,是定义生成器的地方,define_G
这里面的参数是什么意思,在1.2.5.4节中有介绍。第79-82行更是类似,生成判别器的地方,define_D
这里面的参数是什么意思,在1.2.5.4节中有介绍。第84行跳过,85-86行指的是,如果有“图片→照片”的任务,那么应该保证输入图片和输出图片的尺寸一致,否则报错;87-88行是给训练过程中制作一个imagepool,用于存储历史图片,fakeApool和fakeBpool分别是“BtoA”和“AtoB”情况下的imagepool。第90行是根据你选择的gan模式(opt.gan_mode)确定一个模型,并将其输送到self.device里,剩下两个损失完全都采用了L1损失函数。第94-95行是设定优化器,注意其中的itertools.chain是迭代,也就是把里面所有的参数组合起来,每轮训练过后一起迭代,lr和betas可以自己去查Adam优化器的相关知识,不再赘述;第96-97行是把设定好的优化器加在既有的self.optimizers列表里,这个列表来自basemodel.py。该任务和我们的CycleGAN任务暂时无关,是pix2pix里的任务,我们暂时不谈。
该任务和我们的CycleGAN任务暂时无关,是pix2pix里的任务,我们暂时不谈。
将于后续补充。
这个文件夹里的4个文件都是用来规定训练命令里的选项之用。init.py是一个接口类型的文件,没有什么用。base_option.py是一些基础性的选项,它还实现了一些辅助功能,例如解析、打印和保存选项。它还收集modify_commandline_options数据集类和模型类的函数中定义的附加选项;而剩下两个文件则分别对应着训练时、测试时的一些选项,原脚本已经把里面的参数解释的明明白白了。
这一部分不是重点,建议大家不要过分拘泥在这里。
这里存放了一些.sh脚本,关于shell的教程可以参见这里。
这一部分不是重点,建议大家不要过分拘泥在这里。
这个文件夹内是一些辅助功能。
这一部分不是重点,建议大家不要过分拘泥在这里。
git clone https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix
并将当前目录调整到项目根目录下:cd pytorch-CycleGAN-and-pix2pix
pip install -r requirements.txt
bash ./datasets/download_cyclegan_dataset.sh maps
,当然你可以将maps换成其它数据集的名字,也可以自己制作数据集。下载之后的数据集被自动存储在了datasets文件夹下面相应的文件夹里,例如:map。然后,这个文件夹中有train、test、val、trainA、trainB、testA、testB、valA、valB九个文件夹,A、B分别象征着风格A和风格B,而不带AB的则是两个风格合并到一起的图片。python train.py --dataroot ./datasets/maps --name maps_cyclegan --model cycle_gan
。训练结果是这样的:
里面的含义是这样的:第几个epoch、第几次迭代、总用时、训练一个data的用时,后面就是各种损失。
训练后的结果(包括模型和示例图片)都保存在根目录下一个叫checkpoint的文件夹里。大家可以查看,image文件夹下是图片,其它.pth文件都是模型权重文件。如下图所示:
python test.py --dataroot ./datasets/maps --name maps_cyclegan --model cycle_gan
。测试结束后,结果可以在下面标蓝的文件夹里找到。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。