DARTS:CNN代码分享

发布时间:2026/11/2 18:58:35
DARTS:CNN代码分享

论文作者: 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.py

CNN

分析顺序为:

  1. genotypes.py:候选操作的搜索空间和搜索结果存储格式
  2. operations.py:候选操作的算法
  3. model_search.py:搜索阶段的网络
  4. architect.py:负责架构参数α的优化
  5. train_search.py:启动搜索的主文件
  6. model.py:搜索结束后,性能评估使用的网络
  7. train.py / train_imagenet.py:训练性能评估使用的网络
  8. utils.py:工具函数
  9. visualize.py:根据genotype画出Cell的有向图
  10. 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层的仿射参数。
仅选择几个代表进行分析

  1. 零操作


    将输入张量逐元素乘以浮点数0,输出的全零张量在后续计算中无影响。
    分为两种情况,如果在普通单元中,则步长为1,原尺寸输出,如果是在降采样单元中,则步长为2,逐步长降采样,使零操作输出的尺寸和其他操作的输出尺寸相同。

  2. 跳跃连接

    1. 恒等映射

      如果在普通单元中,则跳跃连接为恒等映射,将输入张量原样输出。
    1. 分解式降采样

      如果在降采样单元中,则跳跃连接为分解式降采样,使用两个1×1卷积核分别采集张量位于偶数、奇数坐标点的特征,输出两个尺寸减半的张量,再将两个张量沿通道维度拼接,归一化后输出。
  1. 深度可分离卷积


    两层堆叠的3×3深度可分离卷积。先进行空间提取,再进行通道融合。第一层深度卷积,一个卷积核只负责一个通道,并可完成降采样,第一层逐点卷积,跨通道融合信息,第二层深度卷积,步长为1不再降采样,第二层逐点卷积,将通道映射到目标输出通道。
  2. 空洞深度可分离卷积


    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}wi​MixedOp(x,α)​=softmax(α)i​=i=1∑N​wi​⋅opi​(x)​

单元
  1. 初始化代码

    steps:单元内需要生成几个中间节点;multiplier:最终从后往前依次选择中间节点的数量,将其输出拼接为单元的输出
    c_prev_prev:前前一个单元的通道数,c_prev:前一个单元的通道数
    reduction:该单元是否为降采样单元,reduction_prev:前一个单元是否为降采样单元
    预处理函数的定义,如果与单元相连的前一个单元是降采样单元,则将前前一个单元的输入张量经过分解式降采样与单元的通道对齐;如果为普通单元,则经过ReLUConvBN函数与单元的通道对齐。对于前一个单元的输入,经过ReLUConvBN函数与单元的通道对齐。
    ops存储所有边对应的混合操作,bns存储部分操作后配置的归一化操作(旧写法,新写法中在混合操作定义时已经添加了归一化操作,所以这里的bns没有被使用)。
    for循环,第 i 个中间节点,有 1+i 条待搜索的边,边在ops中逐个储存,每条边对应一个混合操作。如果该单元为降采样单元,则当中间节点的边与两个输入节点相连时,步长为2,进行降采样。

  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个节点的结果沿通道维度拼接为单元的输出。

网络
  1. 网络初始化

    C:通道数,num_classes:分类的类别数量,layers:网络堆叠的层数,criterion:损失函数,steps:中间节点数量,multiplier:拼接节点的个数,stem_multiplier:输入主干的放大倍数。

    定义输入主干,将原始图片转换为单元可以接受的特征图。

    for循环,将单元堆叠为完整的卷积网络,网络三分之一和三分之二处的单元为降采样单元。卷积网络输出的特征图经过全局自适应池化,被压缩成 1×1 的特征图,消除图像长宽的影响,传递给全连接层转换成类别打分,完成最终分类。
    self._initialize_alphas()初始化架构参数α
  2. 新建网络的副本

    新建一个一模一样结构的网络副本,取副本的原因在architect的分析中解释。
  3. 前向计算

    将输入图像经过输入主干转化为单元可以接受的特征图。s0为前前一个单元的输出,s1为前一个单元的输出。
    for循环通过逐个单元计算得到最后的特征图,经过全连接层转换成类别打分。
  4. 损失函数

    input:输入图像,target:图像类别
    将图像传入全连接层获得类别打分,与图像类别经过损失函数计算得到损失。
  5. 初始化架构参数α

    k:一个单元内部所有候选边的数量,num_ops:候选操作的数量
    初始化架构参数,并声明参数可以通过梯度进行优化,最后将架构参数打包成列表。
  6. 取出架构参数的函数
  7. 保存网络结构
  • 定义gene
    连接的记录
    weights:架构参数α计算得到的操作强度,x:参数中边对应的位置,k:一条边中操作对应的位置
    edges:元素数量为第 i 个中间节点的 2 条边
    第 i 个中间节点有 1+i 条边,排除无操作的边,按照一条边最大的操作强度的大小排序,取出最大的两条边即为edges,在for循环中,排除无操作,得到操作强度最大的操作,并将操作和其对应的边添加入gene中。
  • 定义concat
    最终从后往前依次的multiplier个中间节点

得到普通节点的gene和降采样节点的gene,普通节点的concat和降采样节点的concat,按照正确的格式存入genotype中,保存网络结构

04 architect.py

核心目的:优化架构参数α

拼接函数


将模型的网络权重展平成一维的长向量,便于对网络权重进行线性运算和二阶计算前取副本。用循环可以实现相同的作用,但是代码会更啰嗦。

Architect
  1. 初始化代码

    为α准备优化器。
  2. 二阶梯度计算前置步骤

    首先计算损失,求出权重梯度,取权重副本,计算更新后的网络权重副本,模拟一次更新。代码中的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+∇w​Ltrain​(w,α)+λw)
    最后的_construct_model_from_theta将网络权重从一维长向量还原。
    在这里得到:近似最优网络权重w’。

取副本的原因:
理想情况:
pytorch自动微分计算的限制,倘若本地修改网络权重,计算图的依赖链路断开,无法正确算出α的梯度,因此需要取网络副本和权重副本,计算权重副本得到近似最优网络权重w’,将其传入副本模型(在后面还原一维长向量处进行)计算得到验证损失进行反向传播,避免本地修改网络权重。
使用有限近似差分(在后面海森项的计算处进行):
前面相同,在反向传播时,使用近似最优网络权重w’的梯度vector计算方向向量,扰动w,可以类似理想情况的计算链路图理解。
计算的依赖链路图示:

  1. 逐步进行训练

    分为二阶梯度计算(unrolled)和一阶近似梯度计算(rolled)。二阶梯度计算得到架构参数α的梯度,然后使用优化器进行优化。

  2. 一阶近似梯度计算

    ∇α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′,α)−ξ∇α,w2​Ltrain​(w,α)∇w′​Lval​(w′,α)
在这里得到:架构参数α的梯度。

.parameters()取网络权重的张量
.arch_parameters()取架构权重的张量
.named_parameters()取网络权重的参数名称和张量

  1. 还原一维长向量

    model_new得到原模型的复制,model_dict是储存原模型参数名称和数值的字典,params是储存新模型参数名称和数值的字典,offset是游标。for循环逐个取出原模型中的参数,计算v_lenth,得到原模型中参数在一维长向量中对应的长度,以offset为起点取出对应向量再变形为原参数形状,即为更新后的参数。断言offset与一维长向量长度相同,确定获的参数元素总数等于一维长向量的长度。最后更新模型的参数。

  2. 海森项的有限近似差分

    将架构参数的梯度拼接为一维长向量,并求范数,计算得到归一化后的扰动步长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}∇α,w2​Ltrain​(w,α)∇w′​Lval​(w′,α)≈2ϵ∇α​Ltrain​(w+,α)−∇α​Ltrain​(w−,α)​
    在这里得到:海森项的计算结果