扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
源代码地址: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_modconv
bool变量,后续用到的时候再展开,这里只需要知道只有测试的时候才有可能是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,然后styles
reshape成[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]
的向量,会自动广播,其实就是把styles
repeat成x
的形状,再乘以x
conv2d_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
中不需要加上noise
img
,一个是条件向量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
是1x
reshape成[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元起,快前往官网查看详情吧
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流