源代码地址:https://github.com/NVlabs/stylegan2-ada-pytorch
创新互联建站始终坚持【策划先行,效果至上】的经营理念,通过多达十年累计超上千家客户的网站建设总结了一套系统有效的全网推广解决方案,现已广泛运用于各行各业的客户,其中包括:石雕等企业,备受客户好评。
subprocess_fn函数,因此下一步就看这个函数。这里稍微展开说一下这个多线程是怎么回事,就是利用了torch.multiprocessing实现了每个GPU分配一个线程,并且多线程之间是用spawn方式创建的。也就是说,你有多少个GPU,就会同时运行多少个subprocess_fn函数,并且spawn方式意味着这些线程都有独立的python解释器程序,资源是复制的,有自己的独立内存而非全部共享内存,而529行是指定了一个临时路径用来给这些线程进行交流,在这个路径下实现需要共享的部分变量。construct_class_by_name函数。后面会讲解construct_class_by_name这个函数,这里只需要知道它是个根据输入的参数,返回一个根据参数确定的类的方法即可。最近越来越多的深度学习代码使用这种包装方式,本质上就算想用字符串来调用类,又为了代码统一和简洁,包装得一层接一层,读起来是真的麻烦,而且类名隐藏起来了,甚至无法用vscode的智能追踪来找这里用到的到底是什么类。G和D根据train.py的176 177行分别是training.networks.Generator类和training.networks.Discriminator类的对象。而G_ema是G的一个指数移动平均版本,在训练过程中,G的参数会随着step而更新,而G_ema是G的迭代过程中各个时期的参数的指数移动平均版本,相比G,G_ema的变化更加柔和,这是个常用的技巧。augment_pipe是train.py 287行 training.augment.AugmentPipe类的对象kimg_per_tick*1000)张图片才会运行322行以后的内容一次。实现方式是cur_nimg会一直增加,而tick_start_nimg只有在下面的代码会被设置为cur_nimg,这样一旦运行了一次下面的代码,下次判断小于号就会成立,直到cur_nimg增加了4000使得小于号不成立,然后又会运行一次下面的代码。而done条件是因为,break出循环之前需要运行一次下面的代码,所以设置了当迭代图像数满足图像总数的1000倍的时候,就要退出了,这时候不管是不是每4000次的间隔到了,我都要往下走。abort_fn所以应该这一段代码是没有用到),可以为training_loop传一个有效的abort_fn,使得如果准确率等满足条件返回True,从而不需要跑满1000epoch可以退出。call_func_by_name函数并以其返回值作为自身的返回值。call_func_by_name函数定义在279行,调用了get_obj_by_name函数,并进一步调用得到的func_obj,以func_obj的返回值作为call_func_by_name的返回值。所以这里其实就是调用了get_obj_by_name函数得到了类,func_obj保存的就是得到的类,然后实例化并返回,所以返回的是类的实例化对象。get_obj_by_name函数在273行,调用了get_module_from_obj_name和get_obj_from_module。有点绕,其实是因为,name是xx.yy.zz的格式,zz才是类名,xx.yy是模块名,所以先调用222行的get_module_from_obj_name从xx.yy.zz中提取出xx.yy和zz,然后再借助get_obj_from_module函数从xx.yy模块中调用zz类。get_module_from_obj_name函数的核心就在231-239行,231-232行其实就是给出根据“.”的位置对字符串划分成两部分的全部可能,所以如果是xx.yy.zz就会被拆成xx和yy.zz或者xx.yy和zz。然后在235到239行,对每种可能性都进行尝试,尝试从xx.yy中import zz,尝试从xx中import yy.zz,因为用的是try,试不出来可以继续,直到试出来,就知道正确的划分方法是什么。get_obj_from_module函数是通过269行的getattr函数来获取模块中的类的。image和label。image根据210-220行的重写,是一个CHW的unit8(0-255)的np array。label是onehot的float32的np arraytraining.networks.Generator的时候,实质上返回的是persistence.persistent_class(Generator),这个装饰器只是为这个类添加了一些辅助功能,不影响接下来的理解,所以先跳过,后续会解释这个装饰器,先接着看模型w_avg的变量,它不会随着step更新值,但会在一些特殊的时刻进行值的更新和被使用。lr_multiplier不为1时(208行定义的就不为1,是0.01),这些层的参数的学习率和其它参数的学习率相比会乘以一个lr_multiplier(具体实现其实就是把参数直接乘以一个lr_multiplier再去用,实际效果就等同于学习率乘了一个倍数,因为计算这些参数的梯度的时候也是会因此乘以一个lr_multiplier导致step的时候步长会乘以一个lr_multiplier的)normalize_2nd_moment函数看21行,其实就是先统计这些特征值的标准差(每个样本单独统计),接着除以标准差进行归一化。其实这么说不太准确,因为没有减去均值,仅仅是先平方,然后平均,然后开根,然后除(rsqrt是1/sqrt)。而20行的装饰器仅仅是使得torch.autograd.profiler.record_function能跟踪到这个函数而已。至于torch.autograd.profiler后续会介绍是个什么东西。lerp是根据w_avg_beta对w_avg和x进行插值的函数)到w_avg变量中num_ws份x,放在dimension 1上,也就是说现在shape是(B,num_ws,w_dim),具体num_ws是什么下面介绍SynthesisNetwork时会展开说明truncation_psi设为非1的值,所以理论上正常情况这部分代码是不会运行到的。看意思应该是利用w_avg对x进行进一步移动平均,这里的移动平均就是对x做了,影响的是x的值,前面的移动平均只是存下来而已,对实际训练过程不会有什么影响。之所以说是截断,是因为当x在训练过程中突然出现异常大或者异常小的值时,这段代码可以通过移动平均限制这些值不要偏离正常范围太远。fp16_resolution的变量。FP16是一个降低运算量和内存占用的技巧,将32位浮点运算用半精度运算来近似。模型对分辨率最高的num_fp16_res个block进行FP16计算,所以这里是在算开始进行FP16计算的block的resolution。在448行当block的resolution大于等于这里算出来的fp16_resolution时,意味着这个block要进行FP16计算而非全精度的计算。setattr函数,是一种通过字符串变量定义类成员名的方法,比如setattr(a,'hah',1),那么当调用a.hah的时候,返回值会是1,也可以用464行的getattr函数实现调用。num_ws遍后的编码特征,shape为(B,num_ws,w_dim),也就是说对于每个batch,有num_ws个重复的w_dim维的特征。为什么要重复,我的理解是这些副本在后续会被各个模块分别使用,可能是为了避免相互影响?architecture就是’skip’,除了cfg定义为’cifar’时,cfg默认是’auto’)所以每个block都定义了一个ToRGBLayer,后续会介绍。所以到这里可以看出,除了第一个SynthesisBlock为一个SynthesisLayer加一个ToRGBLayer外,其它的SynthesisBlock为两个SynthesisLayer加一个ToRGBLayer。unbind就是把重复的那个维度解出来,再套上iter变成迭代器,那么每次next(w_iter)都会生成一个特征向量,并且每次next生成的特征向量不是同一个,但是内容相同。这个特征向量其实就是MappingNetwork生成的编码特征向量。fused_modconvbool变量,后续用到的时候再展开,这里只需要知道只有测试的时候才有可能是true。x是None,所以这里就根据init中自己生成的随机变量来定义x。从这个实现方式可以看出来一个关键信息,即只要模型定义了,随着训练过程,每次迭代,第一个block的输入x都是固定的,不会改变。并且由于const是torch.nn.Parameter,所以加载resume和load 已训练好的参数的时候也是生成和之前训练的时候同一个x。x的传递和img是无关的,每个x都只需要根据前一个block的x(第一个block则根据一个固定的x)和MappingNetwork生成的ws,即可生成本block的输出x。而img则是根据前一个block的img和本block的x以及MappingNetwork生成的ws来生成的。resample_filter,affine,weight,noise_const(默认不会用到),noise_strength(定义为0)和bias。注意use_noise在这份代码中默认是True的,所以if是一定会执行的。这几个参数的类型前面都介绍过,而具体作用看forwardaffine就是个把输入的w变成styles的全连接层。这里的w其实就是SynthesisBlock分给这个SynthesisLayer的ws的其中一条特征向量。noise_mode就是’random‘而且use_noise是True,所以296不会运行,就是294行,重新生成了一个随机的噪声,满足0均值noise_strength标准差的正态分布。值得注意的是,虽然noise_strength在init中定义为0了,所以这里乘出来的noise最初是一个全0的tensor,但noise_strength是可训练参数,会随着训练过程变化,导致noise后面还是会不为0的。modulated_conv2d函数中,可以先到下面看完再回来这里看。gain就是1,self.act_gain根据276行是bias_act.activation_funcs['lrelu'].def_gain,再看torch_utils/ops/bias_act.py的26行,是0.2,所以act_gain就是0.2self.conv_clamp根据train.py182行是256x先加上self.bias再进一个lrelu的激活函数,具体后面会展开,到此SynthesisLayer介绍完成,可以到ToRGBLayerw和styles向量都除以了其各自的无穷范数(元素大值)进行归一化,同时w还除以了维度。demodulate在SynthesisLayer中都没有设定,所以SynthesisLayer的modulated_conv2d的demodulate参数都是True。也就是说54到60行都会运行。weight和styles两个tensor来构建w。weight是在SynthesisLayer定义的shape为[out_channels, in_channels, kernel_size, kernel_size]的卷积核,styles是affine这个全连接层输出的shape为[batch_size, in_channels]的tensor。所以55行把weight扩展了batch size那一维,变成了[1, out_channels, in_channels, kernel_size, kernel_size]的tensor,然后stylesreshape成[batch_size, 1, in_channels, 1, 1]的tensor,这两个tensor相乘,得到的是[batch_size, out_channels, in_channels, kernel_size, kernel_size],广播操作在这里的作用其实就相当于,把两个tensor都repeat成[batch_size, out_channels, in_channels, kernel_size, kernel_size],然后element-wise地相乘。直观上理解,这个相乘的作用是两个,一个是为同一个batch的不同sample分配不同的kernel,另一个是根据styles对每个channels的kernel进行rescale。dcoefs,是w的二范数的倒数,对第2 3 4维度分别算的,所以dcoefs的维度是[batch_size, out_channels]dcoefs来归一化w,但这段代码和62-72行的代码只有一个会运行。当fused_modconv为True时就运行60行的代码,否则运行62-72行的代码。这里用到了前面跳过的fused_modconv,在386行,这个bool值只有在测试的时候,并且是在前面的FP32层才是True,在FP16层则必须当batch size为1时才为True。所以如果单看训练阶段,60行的代码是不会运行的,只有62-72行会运行。x是[batch_size, in_channels, H, W]的,乘以一个[batch_size, in_channels]的向量,会自动广播,其实就是把stylesrepeat成x的形状,再乘以xconv2d_resample.conv2d_resample来实现weight对x的卷积,具体后面会展开说,这里就先暂且当作普通的卷积。modulated_conv2d,noise都是tensor,不是None,所以只会运行67行,fma.fma是自定义的一个函数,其实就是x乘以dcoefs加noise。之所以这么写,而没有直接x * dcoefs.to(x.dtype).reshape(batch_size, -1, 1, 1) + noise.to(x.dtype),是因为可以利用torch.addcmul来加速。fused_modconv为True才会运行。相比63-72行差别在于卷积核从weight改成了w,并且因为乘以了styles,每个样本有一个单独的卷积核,卷积结果也不需要乘以dcoefs(应该是因为w本身就是weight乘以dcoefs的缘故)fused_modconv的作用就很明显了。如果fused_modconv是True,那么进行卷积的x和w,x不做操作,w是weight乘以styles再乘以dcoefs的结果,并且对每个样本有一个卷积核;如果fused_modconv是False,那么进行的是普通卷积,卷积的双方是x和weight,x要乘以styles,weight不做操作,但卷积结果要乘以dcoefs再输出。Noise,所以它调用的modulated_conv2d中不需要加上noiseimg,一个是条件向量c,根据719-721行,首先是按顺序调用堆叠的DiscriminatorBlock对输入的img进行处理,然后调用一个MappingNetwork对输入的c进行处理,最后用一个DiscriminatorEpilogue以DiscriminatorBlock的输出和MappingNetwork的输出作为输入,产生Discriminator最后的输出。architecture是'resnet'(只有cfg为'cifar'的时候为'orig')。而其它的,如FP16的设定和resolution都和Generator类似,就不细说了。next(trainable_iter)都会返回一个bool值,用来判断当前层是否freeze,freeze多少层由freeze_layers决定,默认参数下是0,也就是没有层会被freeze。如果要设定freeze多少层,可以通过train.py的freezed参数设定,是一个int,指向从Discriminator的第一个block的第一个conv开始的全局层序数,也就是说如果freezed设为5,那么从Discriminator的第一个block的第一个conv开始数,前5个conv都要freeze,后面的全都可训练。in_channels会在第一个DiscriminatorBlock为0,所以第一个block(分辨率大的那个)是会运行543-544行的,而后面的block则不会。所以第一个block有一个额外的Conv2dLayerx就是None,所以这段代码就是把输入的img经过一个Conv2dLayer产生x,同时把img设为None,后续的block再也不需要用到img了x分了两个支路,一个经过一层Conv2dLayer(在这个类内部会进行一次下采样使得分辨率变为原来的二分之一),一个经过两层Conv2dLayer(在第二层内部会进行一次下采样使得分辨率变为原来的二分之一),得到的两个支路的结果相加可以得到DiscriminatorBlock的输出。conv2d_resample.conv2d_resample函数中,其它没什么难点,就偷懒一下不展开了。cmap和最后一个DiscriminatorBlock的输出x作为输入,产生最终的分类值。637-640定义了4个layer,具体作用看forward函数x依次经过一个MinibatchStdLayer和一个Conv2dLayer,然后展平,送进两个全连接层,得到的结果和cmap进行element-wise的相乘,然后全部求和并归一化得到最终的输出。这个输出同时也是Discriminator的输出,是一个[batch_size, 1]的tensor,就是对图片进行二分类的逻辑值,越高表示越real,越低表示越fake。x增加一些通道,这里初始化的group_size是4,num_channels是1xreshape成[4, N/4, 1, c, h, w],即把样本分成了N/4组,每组4个样本,然后对特征的各个维度分别计算组内标准差,再对每个位置每个通道的标准差取平均,得到[N/4, 1]的向量,代表了每组的标准差,然后repeat到各个空间位置和组内样本上成为[N,1,H,W]的向量,concatenate到x上成为新的一个通道,所以x变成了[N,C+1,H,W]输出出去。G_mapping是Generator的MappingNetwork成员,G_synthesis是Generator的SynthesisNetwork成员,D是Discriminator。注意,这个Loss函数是有成员的,有一个初始化为0的pl_mean变量,在后续的accumulate_gradients函数中会对这个向量进行移动平均。run_G函数,这个函数在accumulate_gradients中被调用。41行的style_mixing_prob是0.9(只有cfg为'cifar'时才为0)。ws.shape[1].这里的ws是Generator的MappingNetwork的输出。ws.shape[1]之间的随机整数(均匀分布);第三个参数就是ws.shape[1]。然后看where函数,where函数的三个参数按顺序依次是condition、input、other,意思是,如果condition是True,那么where函数的输出就是input,如果condition是False,那么where函数的输出就是other。所以这一行的意思是,cutoff有0.9的几率会被置为1到ws.shape[1]之间的随机整数,有0.1的几率会被置为ws.shape[1]ws在cutoff后的那些向量替换成另一个根据随机z和同一c重新生成的向量。这里要回想一下ws的生成过程,其实是一堆重复的特征向量堆叠而成的,也就是说原本ws[:, i]和ws[:, j]都是相同的特征向量。z生成的ws,有0.9的几率会把其中随机数量的w替换成另一个随机向量z2生成的w,所以此时ws内就有两种w,一种是z生成的w,一种是z2生成的w,并且比例是随机的。ws调用Generator的SynthesisNetwork生成图片并返回。run_D很简单,就是先对图片augment,然后调用Discriminator判断图像的真假,产生逻辑值。accumulate_gradients函数,这个函数在每个阶段都会调用一次,所以进来的时候可能是四个阶段的其中一个。pl_batch_shrink是2,所以是把batch_size变成了原来的二分之一。这段代码的意义在于它能够使得Greg阶段的batchsize比Gmain阶段的batchsize小。pl_noise作为随机扰动。由于create_graph设为了True,所以这个算出来的梯度项也是可以用来计算损失并backward的,会根据二阶导来更新SynthesisNetwork的参数。这里得到的pl_grads的shape和gen_ws的shape是一样的,都是[batch_size, num_ws, dim_w]pl_grads每个sample对不同w的平均向量二范数,得到的shape是[batch_size, ]pl_lengths对pl_mean进行移动平均,pl_decay是0.01,即是pl_mean = pl_mean + 0.01 * (pl_lengths - pl_mean),这里pl_mean初始值是0,所以随着训练的迭代,pl_mean会是一个保存了历次迭代的pl_lengths的移动平均。pl_mean从梯度图中分离出来,以防止被梯度更新改变值,这个变量只是用来保存pl_lengths的移动平均的,不应该被其它过程更新参数。pl_penalty变量,这个变量就是Greg阶段的损失了。所以可以看出,Greg阶段主要是惩罚pl_grads的变化。89行pl_weight是2,92行gain在这一阶段是4name为'Dreal',Dreg阶段name为'Dr1'你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧