论文作者: Hanxiao Liu∗ Karen Simonyan Yiming Yang
代码链接: https://github.com/quark0/darts
环境需求:Python >= 3.5.5, PyTorch == 0.3.1, torchvision == 0.2.0
代码结构: darts/ └── cnn/ ├── architect.py ├── genotypes.py ├── model.py ├── model_search.py ├── operations.py ├── test.py ├── test_imagenet.py ├── train.py ├── train_imagenet.py ├── train_search.py ├── utils.py └── visualize.pyCNN
分析顺序为:
- genotypes.py:候选操作的搜索空间和搜索结果存储格式
- operations.py:候选操作的算法
- model_search.py:搜索阶段的网络
- architect.py:负责架构参数α的优化
- train_search.py:启动搜索的主文件
- model.py:搜索结束后,性能评估使用的网络
- train.py / train_imagenet.py:训练性能评估使用的网络
- utils.py:工具函数
- visualize.py:根据genotype画出Cell的有向图
- test.py / test_imagenet.py:性能评估
01 genotypes.py
存储候选操作的清单,定义候选操作的搜索空间,和搜索结果的存储格式,还存储着已经搜索好的网络,在性能评估时调用,包括NASNet、AmoebaNet、DARTS_V1、DARTS_V2.
候选操作的清单
特殊的容器类型,搜索结果的存储格式。
所存储的网络示例,NASNet网络。所存储网络均有一个普通单元和一个降采样单元,均为两个输入节点,每两条边对应一个中间节点,每个元组是表示一条边,第一个元素是操作名称,第二个元素是该边连接的前一个节点,concat指定哪些中间节点的通道拼接,得到单元的输出。
02 operations.py
存储实现候选操作的算法
实现候选操作的算法,sep_conv_7x7和conv_7x1_1x7并不在候选名单中,搜索阶段并不会被选中。算法由lambda函数封装,形参为C:通道,stride:步长和affine:控制是否启用BN层的仿射参数。
仅选择几个代表进行分析
零操作
将输入张量逐元素乘以浮点数0,输出的全零张量在后续计算中无影响。
分为两种情况,如果在普通单元中,则步长为1,原尺寸输出,如果是在降采样单元中,则步长为2,逐步长降采样,使零操作输出的尺寸和其他操作的输出尺寸相同。跳跃连接
- 恒等映射
如果在普通单元中,则跳跃连接为恒等映射,将输入张量原样输出。
- 恒等映射
- 分解式降采样
如果在降采样单元中,则跳跃连接为分解式降采样,使用两个1×1卷积核分别采集张量位于偶数、奇数坐标点的特征,输出两个尺寸减半的张量,再将两个张量沿通道维度拼接,归一化后输出。
- 分解式降采样
- 深度可分离卷积
两层堆叠的3×3深度可分离卷积。先进行空间提取,再进行通道融合。第一层深度卷积,一个卷积核只负责一个通道,并可完成降采样,第一层逐点卷积,跨通道融合信息,第二层深度卷积,步长为1不再降采样,第二层逐点卷积,将通道映射到目标输出通道。 - 空洞深度可分离卷积
dilation:扩张系数,为 i 时采样点间隔为 i-1
单层空洞深度可分离卷积。采样点间隔为1,在不增加卷积参数量前提下扩大感受野。与深度可分离卷积相同,一层深度卷积,一个卷积核只负责一个通道,并可完成降采样,一层逐点卷积,跨通道融合信息。
03 model_search.py
定义搜索阶段的网络
混合操作
.ModuleList():模块列表,专门用来存放网络层的列表容器
定义混合算子。建立模块列表_ops存放一条边候选操作,并为池化层增加归一化。前向阶段为混合操作的计算,对所有操作输出加权求和,作为混合操作的输出。
x为输入张量,weights为架构权重,即α
wi=softmax(α)iMixedOp(x,α)=∑i=1Nwi⋅opi(x) \begin{aligned} w_i &= \mathrm{softmax}(\boldsymbol{\alpha})_i \\ \mathrm{MixedOp}(x,\boldsymbol{\alpha}) &= \sum_{i=1}^{N} w_i \cdot \mathrm{op}_i(x) \end{aligned}wiMixedOp(x,α)=softmax(α)i=i=1∑Nwi⋅opi(x)
单元
初始化代码
steps:单元内需要生成几个中间节点;multiplier:最终从后往前依次选择中间节点的数量,将其输出拼接为单元的输出
c_prev_prev:前前一个单元的通道数,c_prev:前一个单元的通道数
reduction:该单元是否为降采样单元,reduction_prev:前一个单元是否为降采样单元
预处理函数的定义,如果与单元相连的前一个单元是降采样单元,则将前前一个单元的输入张量经过分解式降采样与单元的通道对齐;如果为普通单元,则经过ReLUConvBN函数与单元的通道对齐。对于前一个单元的输入,经过ReLUConvBN函数与单元的通道对齐。
ops存储所有边对应的混合操作,bns存储部分操作后配置的归一化操作(旧写法,新写法中在混合操作定义时已经添加了归一化操作,所以这里的bns没有被使用)。
for循环,第 i 个中间节点,有 1+i 条待搜索的边,边在ops中逐个储存,每条边对应一个混合操作。如果该单元为降采样单元,则当中间节点的边与两个输入节点相连时,步长为2,进行降采样。前向计算
s0为第一个输入节点的输入,s1为第二输入节点的输入,weights为架构参数,即α
offset为游标,j为节点在states里的下标,h为节点的特征张量
首先对两个输入张量进行预处理。因为ops中的边是逐个存储的,所以第 i 个中间节点的第 1 条边在ops中的位置为(i)?-1。分析for循环可知,offset == (i)?-1,即为第 i 个中间节点的第 1 条边在ops中的位置,对于第 i 个中间节点,一共有 1+i 个states分别对应 1+i 条边的输入,经过每条边的混合操作输出,求和后即为节点的输出 s 。
s(j)=∑i<jo(i,j)(s(i)) s^{(j)}=\sum_{i<j}o^{(i,j)}(s^{(i)})s(j)=i<j∑o(i,j)(s(i))
最终从后往前选择multiplier个节点的结果沿通道维度拼接为单元的输出。
网络
- 网络初始化
C:通道数,num_classes:分类的类别数量,layers:网络堆叠的层数,criterion:损失函数,steps:中间节点数量,multiplier:拼接节点的个数,stem_multiplier:输入主干的放大倍数。
定义输入主干,将原始图片转换为单元可以接受的特征图。
for循环,将单元堆叠为完整的卷积网络,网络三分之一和三分之二处的单元为降采样单元。卷积网络输出的特征图经过全局自适应池化,被压缩成 1×1 的特征图,消除图像长宽的影响,传递给全连接层转换成类别打分,完成最终分类。
self._initialize_alphas()初始化架构参数α - 新建网络的副本
新建一个一模一样结构的网络副本,取副本的原因在architect的分析中解释。 - 前向计算
将输入图像经过输入主干转化为单元可以接受的特征图。s0为前前一个单元的输出,s1为前一个单元的输出。
for循环通过逐个单元计算得到最后的特征图,经过全连接层转换成类别打分。 - 损失函数
input:输入图像,target:图像类别
将图像传入全连接层获得类别打分,与图像类别经过损失函数计算得到损失。 - 初始化架构参数α
k:一个单元内部所有候选边的数量,num_ops:候选操作的数量
初始化架构参数,并声明参数可以通过梯度进行优化,最后将架构参数打包成列表。 - 取出架构参数的函数
- 保存网络结构
- 定义gene
连接的记录
weights:架构参数α计算得到的操作强度,x:参数中边对应的位置,k:一条边中操作对应的位置
edges:元素数量为第 i 个中间节点的 2 条边
第 i 个中间节点有 1+i 条边,排除无操作的边,按照一条边最大的操作强度的大小排序,取出最大的两条边即为edges,在for循环中,排除无操作,得到操作强度最大的操作,并将操作和其对应的边添加入gene中。 - 定义concat
最终从后往前依次的multiplier个中间节点
得到普通节点的gene和降采样节点的gene,普通节点的concat和降采样节点的concat,按照正确的格式存入genotype中,保存网络结构
04 architect.py
核心目的:优化架构参数α
拼接函数
将模型的网络权重展平成一维的长向量,便于对网络权重进行线性运算和二阶计算前取副本。用循环可以实现相同的作用,但是代码会更啰嗦。
Architect
- 初始化代码
为α准备优化器。 - 二阶梯度计算前置步骤
首先计算损失,求出权重梯度,取权重副本,计算更新后的网络权重副本,模拟一次更新。代码中的theta对应公式中的w,eta对应ξ,dtheta对应w的梯度,权重衰减系数对应λ。
w′=w−ξ(moment+∇wLtrain(w,α)+λw) w' = w - \xi \big(\text{moment} + \nabla_w \mathcal L_{\text{train}}(w,\alpha) + \lambda w\big)w′=w−ξ(moment+∇wLtrain(w,α)+λw)
最后的_construct_model_from_theta将网络权重从一维长向量还原。
在这里得到:近似最优网络权重w’。
取副本的原因:
理想情况:
pytorch自动微分计算的限制,倘若本地修改网络权重,计算图的依赖链路断开,无法正确算出α的梯度,因此需要取网络副本和权重副本,计算权重副本得到近似最优网络权重w’,将其传入副本模型(在后面还原一维长向量处进行)计算得到验证损失进行反向传播,避免本地修改网络权重。
使用有限近似差分(在后面海森项的计算处进行):
前面相同,在反向传播时,使用近似最优网络权重w’的梯度vector计算方向向量,扰动w,可以类似理想情况的计算链路图理解。
计算的依赖链路图示:
逐步进行训练
分为二阶梯度计算(unrolled)和一阶近似梯度计算(rolled)。二阶梯度计算得到架构参数α的梯度,然后使用优化器进行优化。一阶近似梯度计算
∇αLval(w′,α) \nabla_{\alpha}\mathcal{L}_{val}(w', \alpha)∇αLval(w′,α)
5.** 二阶梯度计算**
由近似网络最优权重w’,计算得到α的梯度,对应代码中的dalpha,vector为w’的梯度,后进入海森项计算模块得到海森项计算结果,第一个for循环将α的梯度和海森项相减,得到公式中的形式,eta对应ξ,第二个for循环将计算完成的架构梯度传回模型的架构参数α。
∇αLval(w′,α)−ξ∇α,w2Ltrain(w,α)∇w′Lval(w′,α) \nabla_{\alpha}\mathcal{L}_{val}(w', \alpha) -\xi \nabla_{\alpha,w}^2 \mathcal{L}_{train}(w,\alpha) \nabla_{w'}\mathcal{L}_{val}(w', \alpha)∇αLval(w′,α)−ξ∇α,w2Ltrain(w,α)∇w′Lval(w′,α)
在这里得到:架构参数α的梯度。
.parameters()取网络权重的张量
.arch_parameters()取架构权重的张量
.named_parameters()取网络权重的参数名称和张量
还原一维长向量
model_new得到原模型的复制,model_dict是储存原模型参数名称和数值的字典,params是储存新模型参数名称和数值的字典,offset是游标。for循环逐个取出原模型中的参数,计算v_lenth,得到原模型中参数在一维长向量中对应的长度,以offset为起点取出对应向量再变形为原参数形状,即为更新后的参数。断言offset与一维长向量长度相同,确定获的参数元素总数等于一维长向量的长度。最后更新模型的参数。海森项的有限近似差分
将架构参数的梯度拼接为一维长向量,并求范数,计算得到归一化后的扰动步长R。
第一个for循环,得到 w+R·v,计算得到正向扰动的梯度,第二个for循环,(w+R·v)-2R·v 得到 w-R·v,计算得到负向扰动的梯度,第三个for循环,(w-R·v)+R·v 得到 w,将模型参数还原。
最后返回值为公式的计算结果(原论文中ε为r,工程中改为R,变为动态缩放系数,使得数值稳定,用以应对梯度过大或梯度过小的问题):
∇α,w2Ltrain(w,α)∇w′Lval(w′,α)≈∇αLtrain(w+,α)−∇αLtrain(w−,α)2ϵ \nabla_{\alpha,w}^2\mathcal{L}_{train}(w,\alpha)\nabla_{w'}\mathcal{L}_{val}(w',\alpha) \approx \frac{\nabla_{\alpha}\mathcal{L}_{train}(w^+,\alpha)-\nabla_{\alpha}\mathcal{L}_{train}(w^-,\alpha)}{2\epsilon}∇α,w2Ltrain(w,α)∇w′Lval(w′,α)≈2ϵ∇αLtrain(w+,α)−∇αLtrain(w−,α)
在这里得到:海森项的计算结果