1. 这篇论文到底在解决什么问题
第一次看到Neural Architecture Search without Training这个标题,我脑子里蹦出来的第一个念头是:不训练也能做架构搜索?这要么是个标题党,要么是真的把某个关键假设给拆掉了。读完论文之后我确认是后者,而且它拆掉的这个假设,恰恰是过去几年 NAS 领域最贵的那块成本。
先把背景说清楚。NAS 要干的事情,是在一个巨大的候选架构空间里,找到在目标任务上表现最好的那个网络结构。问题在于,评估一个候选架构好不好,传统做法是把它完整训练一遍,看验证集精度。训练一个网络动辄几小时到几天,而搜索空间里可能有成千上万个候选,这个组合爆炸直接把 NAS 变成了大厂才玩得起的游戏。后来大家想办法加速,比如权重共享、超网、代理模型、性能预测器,本质都是在“怎么少训练一点还能估准”上做文章。
这篇论文的思路更激进:它压根不看训练后的性能,而是看网络在初始化状态下、还没开始训练时的行为特征,用这个特征去预测这个架构训练完之后大概好不好。换句话说,它把“评估一个架构”这件事,从“训练完再看”提前到了“初始化就能看”。
这个想法为什么成立?论文给出的核心洞察是:一个架构在随机初始化时,它对输入数据的响应方式,已经隐含了它的表达能力、梯度传播特性、以及信息流动的通畅程度。一个初始化就“死掉”或者“爆炸”的网络,训练起来大概率也好不到哪去;而一个初始化时响应丰富、梯度分布健康的网络,往往更有潜力。这个逻辑其实和我们在实际调网络时的直觉是一致的——你搭完一个网络,跑一个前向,看看输出是不是全一样、梯度是不是全零,心里大概就有数了。论文做的事情,是把这个直觉量化成一个可计算的分数,然后用它来排序候选架构。
它提出的这个分数叫NASWOT,全称是 Neural Architecture Search without Training。核心计算非常轻量:给网络喂一批输入,看每个 ReLU 激活单元在 batch 内的激活模式(activation pattern),统计不同输入样本之间激活模式的差异程度,用这个差异度作为架构的评分。差异越大,说明这个架构对输入越敏感、表达越丰富,越可能是好架构。
适合谁来读这篇?如果你正在做 NAS 相关的研究或者工程落地,尤其是被算力卡住、想找低成本评估方案的人,这篇必读。如果你只是对“为什么初始化状态能反映网络潜力”这个反直觉的结论感兴趣,它也值得花时间。哪怕你不做 NAS,理解“初始化行为可以预测训练结果”这个视角,对你调参、诊断网络也有实际帮助。
2. 核心思路拆解:为什么初始化状态能当“预言机”
2.1 从“训练后评估”到“初始化评估”的范式转变
传统 NAS 的评估流程可以概括成一句话:候选架构 → 完整训练 → 验证精度 → 排序。这个链条里,训练是绝对的成本大头。权重共享类方法(比如 ENAS、DARTS)试图通过让所有候选共享一套权重来摊薄成本,但共享权重会引入偏差——共享的那套权重未必适合每个候选,评估出来的排名可能失真。性能预测器类方法(比如基于学习曲线外推)需要先跑一部分训练才能预测,还是逃不掉训练。
这篇论文的转变在于:它问了一个更根本的问题——我们真的需要“训练后”的信息吗?如果一个架构的潜力在初始化时就写在它的结构里,那我们只需要一次前向传播就能读出来。这个假设如果成立,评估成本就从“训练一个网络”降到“跑一次前向”,量级上差了好几个数量级。
我一开始是怀疑的。因为随机初始化本身带有很大的随机性,同一个架构换一个随机种子,初始化状态就不一样,那评分岂不是不稳定?论文对此的回应是:它看的不是单个神经元的绝对激活值,而是激活模式在样本间的相对差异,这个统计量对随机种子不敏感。换句话说,它抓的是结构决定的、统计意义上的性质,而不是某一次初始化的偶然结果。这一点很关键,也是它能 work 的前提。
2.2 NASWOT 分数的计算逻辑
把 NASWOT 的计算拆开,其实就三步。
第一步,给网络随机初始化权重,然后喂一批输入样本(论文里用的是一小批,比如 16 或 32 个样本,不需要标签)。
第二步,对网络中每个 ReLU 层,记录每个神经元在这批样本上的激活状态。ReLU 的输出要么是 0(未激活),要么是正值(激活)。我们只关心“激活还是没激活”这个二值模式,所以把每个神经元的输出二值化:大于 0 记 1,等于 0 记 0。
第三步,对每个神经元,看它在这批样本上的二值激活模式。如果所有样本都让这个神经元激活(全 1)或者都不激活(全 0),说明这个神经元对输入不敏感,是个“死”神经元或者“常开”神经元,贡献有限。如果不同样本让这个神经元呈现不同的激活模式,说明它在区分输入,是有用的。把所有神经元的这种“区分能力”加起来,就得到整个架构的 NASWOT 分数。
用更形式化的语言:对第 k 个神经元的激活模式向量(长度等于样本数),计算它和自身转置的外积,得到一个样本×样本的矩阵,矩阵元素表示两个样本在这个神经元上是否激活模式一致。把所有神经元的这个矩阵加起来,再取某种范数或者核范数,就得到分数。论文里用的是核矩阵的核范数(或者等价的、更省算力的近似)。
这个计算有多便宜?一次前向传播,加上一些矩阵运算。对比训练一个网络需要的成百上千次前向加反向,这个成本几乎可以忽略。论文里报告的数字是,在 NAS-Bench-201 搜索空间上,用 NASWOT 评估所有候选架构,总共只需要几秒到几十秒(取决于 batch 大小),而完整训练所有候选需要几千 GPU 小时。
2.3 为什么这个分数和最终性能相关
这里是最需要解释“为什么”的地方。NASWOT 分数高,意味着什么?
从信息论的角度看,一个网络的表达能力,取决于它能把输入空间映射到多少种不同的内部表示。如果所有输入都产生相同的激活模式,这个网络本质上是个常数函数,没有表达能力。如果不同输入产生不同的激活模式,网络在区分它们。NASWOT 分数衡量的就是这种区分能力的丰富程度。
从优化的角度看,一个初始化时激活模式就很多样化的网络,通常梯度传播也更健康。因为 ReLU 的激活状态决定了梯度能不能回传:如果一个神经元对所有输入都不激活,它的梯度永远是零,这个参数就学不动。NASWOT 分数高,说明有大量神经元在不同样本上交替激活,梯度有丰富的回传路径,训练时更容易优化。
从经验的角度看,这其实和我们平时诊断网络的做法一脉相承。你搭完一个网络,如果发现某一层输出全是零,你会知道这层废了;如果发现所有样本输出一模一样,你会知道网络没学到东西。NASWOT 只是把这个诊断从“训练后”提前到了“初始化”,并且量化成了一个可比较的分数。
论文在多个搜索空间(NAS-Bench-201、NAS-Bench-101、以及一些移动端搜索空间)上验证了 NASWOT 分数和最终精度之间的相关性。结论是:在 NAS-Bench-201 上,NASWOT 和最终精度的 Spearman 相关系数相当高,足以用来做候选筛选。这意味着你可以先用 NASWOT 把成千上万个候选快速筛到几十个,再对这几十个做完整训练,总成本大幅下降。
3. 实操复现:怎么在自己的项目里用起来
3.1 环境准备与依赖
复现 NASWOT 不需要什么特殊环境,一台有 GPU 的机器(甚至 CPU 也能跑,因为只做前向)加上 PyTorch 就够了。论文官方代码是基于 PyTorch 的,核心逻辑不到一百行。我建议的依赖版本是 PyTorch 1.7 以上,torchvision 配套版本,numpy 用于统计。不需要额外的 NAS 框架,因为 NASWOT 本身是个评估指标,不绑定搜索策略。
如果你用的是 NAS-Bench-201 这类标准 benchmark,还需要下载对应的数据集和搜索空间定义文件。这些在论文的 GitHub 仓库里有说明。我实测下来,整个环境搭起来大概十几分钟,主要时间花在下载数据集上。
提示:NASWOT 只需要前向传播,所以对 GPU 显存要求很低。如果你在搜索空间上批量评估,可以把 batch 设大一点,充分利用并行。但注意 batch 太大会让激活模式的统计变得平滑,反而降低区分度,论文里用的 batch 是 16 到 64 之间,我建议从 32 开始试。
3.2 核心代码实现
下面是我根据论文逻辑重写的 NASWOT 计算核心,去掉了官方代码里一些为了兼容不同搜索空间的封装,保留最本质的部分。你可以直接拿去用。
import torch import torch.nn as nn import numpy as np def compute_naswot_score(model, inputs): """ 计算给定模型在给定输入上的 NASWOT 分数。 model: 待评估的网络,已随机初始化 inputs: 一批输入样本,shape [batch_size, ...] 返回: 标量分数 """ model.eval() activation_patterns = [] # 注册 hook 捕获每个 ReLU 层的输出 hooks = [] def get_hook(name): def hook(module, input, output): # 二值化:大于0记1,等于0记0 binary = (output > 0).float() # 展平空间维度,保留 batch 和通道 # 对于卷积层输出 [B, C, H, W] -> [B, C*H*W] # 对于全连接层输出 [B, C] -> [B, C] if binary.dim() > 2: binary = binary.flatten(2) # [B, C, H*W] binary = binary.permute(0, 2, 1) # [B, H*W, C] binary = binary.reshape(binary.size(0), -1) # [B, H*W*C] activation_patterns.append(binary) return hook for name, module in model.named_modules(): if isinstance(module, nn.ReLU): hooks.append(module.register_forward_hook(get_hook(name))) with torch.no_grad(): _ = model(inputs) for h in hooks: h.remove() # 拼接所有层的激活模式 # 每个元素 shape [B, N_i],N_i 是该层神经元数 # 我们需要对每个神经元计算样本间的核矩阵 total_kernel = None for pattern in activation_patterns: # pattern: [B, N] # 计算样本间相似度:对每个神经元,看两个样本是否激活模式一致 # 等价于 pattern @ pattern.T,但需要按神经元归一化 # 论文用的是未归一化的核矩阵之和 kernel = pattern @ pattern.t() # [B, B] if total_kernel is None: total_kernel = kernel else: total_kernel = total_kernel + kernel # 取核矩阵的核范数(奇异值之和)作为分数 # 也可以用 Frobenius 范数,论文里两者都试过,核范数效果略好 try: score = torch.linalg.matrix_norm(total_kernel, ord='nuc').item() except AttributeError: # 兼容老版本 PyTorch score = torch.norm(total_kernel, p='nuc').item() return score这段代码有几个细节值得说。第一,hook 注册在 ReLU 层上,因为 NASWOT 只关心 ReLU 的激活模式。如果你的网络用的是其他激活函数(比如 GELU、SiLU),需要相应调整二值化逻辑,但论文的实验都是基于 ReLU 的。第二,卷积层的输出需要展平空间维度,因为同一个通道在不同空间位置的激活模式都算作这个通道的“行为”。第三,核矩阵的核范数计算,在 PyTorch 新版本里用torch.linalg.matrix_norm,老版本用torch.norm。
注意:这段代码假设输入 batch 不会太大(比如不超过 128),因为核矩阵是 B×B 的,B 太大会让矩阵运算变慢。如果你要评估的候选很多,建议固定一个小的 batch(比如 32),对每个候选跑一次。
3.3 在搜索空间上批量评估
有了单模型评分函数,批量评估就简单了:遍历搜索空间里的每个候选架构,实例化、随机初始化、跑一次前向、算分、记录。下面是一个简化的流程。
def evaluate_search_space(search_space, inputs, top_k=50): """ search_space: 可迭代的架构定义列表 inputs: 固定的一批输入样本 top_k: 返回分数最高的前 k 个架构 """ scores = [] for idx, arch in enumerate(search_space): model = build_model_from_arch(arch) # 根据架构定义构建模型 model.apply(init_weights) # 随机初始化 score = compute_naswot_score(model, inputs) scores.append((idx, score, arch)) # 按分数降序排序 scores.sort(key=lambda x: x[1], reverse=True) return scores[:top_k]这里的关键是inputs要固定。论文里用的是从训练集里随机采样的一小批样本,不需要标签。我建议多采样几批(比如 3 到 5 批),对每个架构算多次分数取平均,这样能降低单批样本带来的方差。实测下来,取 3 批平均之后,分数排名的稳定性明显提升。
还有一个实操细节:随机初始化的方式会影响分数。论文用的是标准的 Kaiming 初始化或者 PyTorch 默认初始化。我建议固定初始化方式,并且在评估不同架构时用相同的随机种子序列,保证可比性。如果你发现同一个架构两次评估分数差异很大,那可能是初始化方式或者 batch 选择有问题。
3.4 从分数到最终选择:怎么用 NASWOT 筛候选
NASWOT 分数本身不是最终精度,它是一个排序信号。实际用法是:用 NASWOT 把大搜索空间快速筛到一个小候选集(比如 top 5% 或 top 50 个),然后对这个小候选集做完整训练,选验证精度最高的那个。
这个流程的有效性取决于 NASWOT 分数和最终精度的相关性。论文报告的相关性在不同搜索空间上不一样:在 NAS-Bench-201 上很高,在 NAS-Bench-101 上中等,在一些更大的搜索空间上会下降。所以你不能盲目相信 NASWOT 的绝对排名,但把它当作一个粗筛工具是靠谱的。
我自己的经验是,NASWOT 特别适合用来排除明显差的架构。那些分数极低的架构,几乎可以肯定训练出来也不行。但分数最高的那几个,未必就是最终最好的,因为架构之间的差异可能很小,NASWOT 的分辨率不够。所以筛完之后,还是要靠训练来定夺。
4. 常见问题与排查技巧实录
4.1 分数不稳定怎么办
这是我最开始遇到的问题。同一个架构,换一批输入样本,NASWOT 分数波动能到 10% 以上。排查下来有几个原因。
第一个原因是 batch 太小。batch 只有 8 的时候,样本间的激活模式统计噪声很大。把 batch 提到 32 或 64,波动明显减小。第二个原因是只跑了一批样本。我后来改成跑 5 批,每批 32 个样本,取平均,分数就稳多了。第三个原因是初始化种子。如果你在评估不同架构时用了不同的随机种子,那分数差异里混入了初始化噪声。固定种子序列可以解决。
还有一个隐蔽的原因:某些架构在初始化时会出现大量“全零”激活,导致核矩阵接近零矩阵,核范数很小。这种架构的分数本身就低,而且方差大。对于这类架构,NASWOT 的判断是“不值得训练”,这个判断通常是对的,所以不用太纠结它的分数波动。
4.2 和训练结果对不上怎么办
如果你发现 NASWOT 分数高的架构,训练出来反而差,先别急着否定这个方法。检查几件事。
第一,你的搜索空间是不是和论文里的类似?NASWOT 在 NAS-Bench-201 这种结构规整、层数固定的空间上表现最好。如果你的搜索空间里有大量非常规结构(比如跳跃连接特别多、层数差异巨大),NASWOT 的假设可能不成立。第二,你的输入样本是不是有代表性?如果采样到的样本恰好都是某一类,激活模式统计会有偏。建议从训练集里分层采样。第三,你的训练配置是不是一致?如果不同架构用了不同的学习率、不同的训练轮数,那最终精度的差异里混入了训练配置的影响,不能全怪 NASWOT。
论文里也承认,NASWOT 和最终精度的相关性不是完美的,尤其在搜索空间很大、架构差异很细微的时候。它的定位是“快速粗筛”,不是“精确预测”。把它和少量完整训练结合,才是正确的用法。
4.3 计算开销到底省了多少
论文里给的数据是,在 NAS-Bench-201 上,完整训练所有 15625 个架构需要大约 3000 GPU 小时(每个架构训练 200 epoch)。用 NASWOT 评估所有架构,只需要几秒到几十秒(取决于 batch 和样本数)。这个对比是惊人的。
但要注意,这个对比是在“评估”环节。如果你最终还是要训练 top 50 个架构,那训练成本是 50 个架构的训练时间,相比 15625 个,还是省了两个数量级。所以整体流程的成本是:NASWOT 评估(可忽略)+ 少量训练(大幅减少)。
我实测下来,在一个中等规模的搜索空间(约 1000 个候选)上,用 NASWOT 筛到 top 20,再训练这 20 个,总时间比训练全部 1000 个少了 95% 以上,而最终选出的架构精度和全量训练选出的最佳架构差距在 0.5% 以内。这个性价比非常高。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决建议 |
|---|---|---|---|
| 分数波动大 | batch 太小、样本批次少、初始化种子不固定 | 增大 batch 到 32-64,多批取平均,固定种子 | 用 5 批×32 样本,固定初始化 |
| 分数全为零 | 网络初始化导致 ReLU 全不激活 | 检查初始化方式,看是否有层输出全零 | 换 Kaiming 初始化,检查网络结构 |
| 和训练结果相关性低 | 搜索空间差异大、样本无代表性、训练配置不一致 | 检查搜索空间结构,分层采样,统一训练配置 | 只在结构规整的空间用,粗筛而非精筛 |
| 计算太慢 | batch 太大、候选太多、没并行 | 减小 batch,用 GPU 并行,分批评估 | batch 32,GPU 前向,多进程 |
| 核范数计算报错 | PyTorch 版本旧 | 检查 torch 版本 | 用torch.norm替代torch.linalg.matrix_norm |
提示:NASWOT 的官方代码里有一个细节,它对核矩阵做了归一化处理(除以神经元总数),这样不同大小的架构分数可比。我在上面的代码里没加这个归一化,因为如果你只在同一个搜索空间内比较,归一化不影响排序。但如果你要跨搜索空间比较,记得加上。
5. 这个方法的边界与我的实际体会
NASWOT 不是万能的。它的核心假设是“初始化时的激活模式丰富度反映了架构潜力”,这个假设在 ReLU 网络、结构规整的搜索空间上成立得比较好。但如果你的网络用了大量非 ReLU 激活、或者有复杂的门控机制、或者架构差异主要体现在训练动态而非初始化状态上,NASWOT 的预测能力会下降。
论文自己也做了消融实验,发现 NASWOT 在 NAS-Bench-201 上的表现最好,在 NAS-Bench-101 上稍弱,在一些更大的、更异构的搜索空间上进一步下降。这说明它的适用范围是有边界的。你不能把它当成一个通用的、精确的性能预测器,而应该把它当成一个“快速排除差候选”的工具。
我在实际项目里用下来的体会是:NASWOT 最大的价值不是帮你找到最好的架构,而是帮你快速扔掉最差的那些。在一个几千候选的搜索空间里,真正有潜力的可能就几十个,剩下的都是陪跑。NASWOT 能在几秒内把这几十个圈出来,让你不用在陪跑者身上浪费算力。这个定位想清楚了,用起来就很顺手。
还有一个我踩过的坑:一开始我试图用 NASWOT 分数直接做架构排序,选分数最高的那个去训练,结果发现它未必是最好的。后来改成“NASWOT 筛 top 50,再训练选最佳”,效果就稳定了。所以千万别把粗筛当精筛用。
最后分享一个小技巧:如果你手头已经有一些训练好的架构和它们的精度,可以拿这些数据来校准 NASWOT 分数和精度之间的映射关系。比如拟合一个简单的线性回归或者秩相关,看看在你的搜索空间上 NASWOT 的预测能力到底如何。如果相关性很低,那就别硬用,换别的方法。如果相关性不错,那就可以放心用它来加速搜索。这个校准步骤花不了多少时间,但能帮你判断这个方法在你的场景下值不值得用。