做小样本目标识别(few-shot object recognition)方向有一段时间,我对“样本合成”这条路线一直是又爱又怕。爱是因为它直接补数据,怕是因为大多数生成方法在小样本场景下要么崩、要么不涨点。这两天重读论文《∆-encoder: an effective sample synthesis method for few-shot object recognition》,发现它把问题转换得很巧妙:不直接合成新样本,而是合成“样本之间的差异”。这篇笔记我会按自己的阅读习惯来写——先交代它解决了什么困境,再拆解∆-encoder的网络结构和训练流程,然后是合成新类样本的具体做法和实验结论,最后聊聊我认为这个方法成立的前提、容易踩的坑,以及它思路在今天有哪些延伸价值。适合正在调研小样本方法、或者打算自己动手实现样本合成的读者;如果只关心效果和结论,可以直接跳到实验那一节。
1. 小样本识别为什么难:三类主流做法各自的短板
1.1 任务定义与核心矛盾
所谓 few-shot object recognition,通常这样设定:我们手里有很多标注数据的类别叫基类(base classes),每个类有几百上千张图;而真正要解决的类别叫新类(novel classes),每个类只给 1 张或 5 张标注图。模型要在基类上完成训练,再在几乎“没见过”的新类上做分类。经典的 1-shot / 5-shot 评估就是反复随机抽类别和样本,取多次平均准确率。
这里最要命的不是“没见过类别”,而是“看不见类内差异”。一个类别的核心信息不只是“它长什么样”,还包括“它有多少种姿态、多少种光线、多少种背景”。基类能学到这些变化,新类只有几个孤零零的样本,类内分布几乎是未知的。哪怕给你一个完美的特征提取器,你用 1 张“坐着的猫”去代表“猫”这个类,测试时来了一只站着的、背光的猫,照样很容易分错。
这个问题可以拆成两层:第一层是特征好不好,第二层是类别模型怎么从头估。很多方法都把重心放在第二层,试图在没有数据的情况下硬推出一个可靠的类别边界。
1.2 度量学习、元学习为什么没根治数据稀缺
目前主流的三条路线,各有各的“治标不治本”。
度量学习(metric learning)的思路是:训练一个嵌入空间,让同类样本靠近、异类样本远离。测试时把新类样本映射进去,用最近邻或原型中心做分类。它的好处是直观、稳定,因为嵌入空间是从大量基类数据上学出来的,有很强的泛化能力。但问题是,新类只有几个样本时,它们的中心估计偏差很大,嵌入空间里“某个方向代表着什么变化”这件事没法快速传递到新类上。
元学习(meta-learning)的思路更激进:把“学会学习”本身当作训练目标,构造大量和测试同分布的小任务,让模型学会几步之内适应新任务。代表作包括各种基于梯度的快速适应方法和基于外部记忆的方法。它们理论上很漂亮,但实际对超参数敏感、训练复杂度高,而且在类别差异很大的数据集上,适应机制经常会退化成“学一个初始特征然后微调”,并没有真正解决样本太少的问题。
样本合成(sample synthesis)是最直觉的一条路:既然新类图片不够,那就制造一些出来。可是在 ∆-encoder 之前,这条路一直不被主流信任。原因我在下一节展开。总之一句话:三类方法都没有回答那个根本问题——新类样本长什么样,我们凭什么知道?
1.3 样本合成路线为何长期不被信任
早期有代表性的“幻觉特征”类方法,思路是先用基类训好一个分类模型,提取特征后把新类的几个样本映射到特征空间,再把从基类学到的“类内方差”叠加到新类原型上,伪造出更多新类特征。这类方法在当时确实涨了点,但问题也很明显:特征不可解释,你没法直观判断伪造的特征是不是真的合理;幻觉特征很容易落在分类器特征的退化区域,模型学了这些假特征反而会过拟合到假的统计规律上。
如果直接在图像空间从头生成呢?那就要训练一个大生成模型,然后告诉它“生成一张新类别图片”。问题是新类别根本没有足够数据来约束“类别语义”,生成器只能靠基类信息硬猜,结果通常就是一张张失真、模糊、跟基类高度相似的图——放到分类器里不但不涨点,还会污染支持集。
所以 ∆-encoder 提出的时候,我觉得它的核心贡献不是“又发明了一个生成器”,而是换了一个合成对象:不再合成“内容”,而是合成“内容之间的操作”。这个角度的转变,才是整篇论文最值得记的地方。
2. 核心思想拆解:“以图生图”的编码器解码器如何学出差异
2.1 一张图看懂 ∆-encoder 的结构
论文的模型结构很简单:一个编码器 E 和一个解码器 D。输入不是单张图,而是两张图。设支持图为 x_s,目标图为 x_t,编码器输出一个向量 δ:
δ = E(x_s, x_t)
然后解码器接收 x_s 和 δ,尝试重构出 x_t:
x̂_t = D(x_s, δ)
训练目标就是让 x̂_t 和 x_t 尽量一致,比如像素级的均方误差。我没有把公式写得更复杂,因为这篇论文的精髓恰恰在这么简单的操作里:δ 向量表示的是“从 x_s 变成 x_t 需要施加的差异”,它应该是内容无关的——不管这张图是一只猫还是一辆车,“转个角度”“换个光线”“换个背景”这些操作对应的 δ 是可以通用的。
用生活中的例子来类比,这就像在修图软件里录一个“动作宏”:你给软件看一张原图和一张修好的图,软件把中间的操作步骤记下来;之后把这段操作录用到任何一张完全不同的照片上,也能达到类似风格的变化。δ 就是这个“动作宏”在神经网络里的一种隐式表示。
2.2 为什么“学差异”比“学生成”稳定得多
这是我认为全文最重要的动机分析。如果直接学“生成一张猫的新图片”,模型需要建模整个图像分布的高维流形,而且几乎没有新类监督信号,很容易崩溃。但如果只学“两张图之间的变化方向”,问题就变成了一个受控的重构任务:内容已经由 x_s 给定,生成器只需要补上变化部分。这个任务有明确监督(x_t 就是标签),且变化空间通常远小于内容空间,学起来稳定得多。
更深一层,论文作者赌了一个很强的先验:类内变化的模式在不同类别之间是共享的。比如“绕某个轴旋转”“改变光照强度”“叠加某种背景纹理”,这些操作在狗身上成立、在鸟身上也成立、在花瓶上也成立。基类数据量大,能把这些操作模式学全;新类样本提供了具体的“内容锚点”(那几张支持图),把学到的操作套上去,就能得到看起来属于新类的合理样本。
对比普通自编码器就更能看出区别。V AE/AE 学的是“如何把一张图压缩成内容编码再解压”,编码里装着内容;∆-encoder 里内容由 x_s 直接提供,δ 空间装的是“变换”而不是“内容”,所以 δ 的维度可以压得很小,而且天然具有跨类迁移的潜力。
2.3 它和常见生成模型的边界在哪里
有朋友拿它和图像翻译模型类比,比如从线稿生成照片这类条件生成模型。区别在于:这类模型通常要求输入域和输出域有固定的配对关系,而 ∆-encoder 的输入是任意两张图,δ 是数据相关的,同一个解码器要学会利用不同的 δ 实现不同类型的变换。也有人拿它和风格迁移比,风格迁移通常提取的是全局风格统计量,而 δ 更像一个稠密但紧凑的“编辑向量”,可以表达更细颗粒度的变化——比如“这张图里主体向左偏转 20 度并换了地面背景”,而不只是“整体色调变了”。
理解这些边界,对后面判断“什么场景适合用它”很有帮助:凡是变换模式相对固定、且在不同类别间可复用的任务,都是它的主场;凡是变换本身高度依赖类别语义的任务,它就容易失灵。
3. 训练阶段的三个关键细节:配对策略、损失函数、训练节奏
3.1 样本对怎么构造:同类对与跨类对
训练 ∆-encoder 不需要额外标注,只需要在基类里任意抽两张图组成一对。但“怎么抽”直接决定学到什么样的 δ。按我对论文逻辑的推断,训练时要同时混合两种对:
- 同类对:比如两张不同姿态的狗图。模型学会的是“类内差异”——姿势、角度、背景、光线变化。这类 δ 是天然可迁移的,也是合成新类样本时最想用的。
- 跨类对:比如一张狗和一张猫。模型要学会把“走姿的狗”变成“趴着的猫”。这类 δ 里既有变换也有内容差异,它的意义在于防止编码器偷懒:如果不加跨类对,模型完全可以在 δ 里编码“类别身份”,比如给 δ 塞入“这是第二种猫”,解码器再靠 δ 还原内容,那 δ 就不干净了。混入跨类对,相当于加了一个正则,逼迫 δ 只能表达“结构上的变换”,而不是“某个类别的完整信息”。
这里有个实际操作经验:同类对和跨类对的比例不要太极端。我印象中论文没有给一个特别硬的比例,但按这类模型的经验,同类对占 60%~80%,跨类对占剩下的,整体最稳。如果全是同类对,δ 容易退化成类内模板;如果跨类对太多,模型会花大量容量去学“跨物种变形”,合成时产生的样本可能形态怪异。
3.2 重构损失之外,要不要加对抗损失和先验约束
基础版本只用像素重构损失,训练非常稳定,但有个众所周知的问题:像素均值回归会让生成图变糊。为了实现更清晰的合成效果,论文里也探讨了加入判别器的变体,让合成图在真实感上更接近真实图像。加上判别器后,损失变成重构损失和对抗损失的加权:
L = L_recon + λ_g · L_GAN
λ_g 通常取一个很小的值,比如 0.01 到 0.1。太大的话,生成质量会变得“好看但失真”,分类器反而被带偏。
另一个值得注意的点是 δ 空间的先验。如果训练时对 δ 完全不加约束,那 δ 的分布可能是任意形状,采样时就只能依赖训练时见过的 δ 集合。论文的合成流程里,我记得更多是从基类对里收集 δ 或对 δ 建模后采样。实操中,如果想方便地从连续分布里采样,可以考虑给 δ 加一个高斯先验,或者训练后用一个高维高斯去拟合已经收集到的 δ。这样合成时就能“平滑地插值”出新变换,而不是只在有限几个离散 δ 上打转。
3.3 完整训练流程与参考参数
我自己在理解这类模型时,习惯把流程写成伪代码,这样不容易漏环节。核心训练循环大致如下:
# 训练阶段的伪代码框架 for epoch in range(max_epochs): for batch_pairs in train_loader: x_a = batch_pairs["source"] # 支持图 x_b = batch_pairs["target"] # 目标图 delta = encoder(x_a, x_b) # 编码出差异向量 x_rec = decoder(x_a, delta) # 用支持图 + δ 重构目标图 loss_rec = mse_loss(x_rec, x_b) if use_gan: real_score = discriminator(x_b) fake_score = discriminator(x_rec.detach()) loss_dis = bce_loss(fake_score, 0) + bce_loss(real_score, 1) loss_adv = bce_loss(discriminator(x_rec), 1) loss = loss_rec + lambda_g * loss_adv else: loss = loss_rec optimizer.zero_grad() loss.backward() optimizer.step() if use_gan: optimizer_dis.zero_grad() loss_dis.backward() optimizer_dis.step()一些我习惯用的参考配置:图像分辨率先统一到 84×84(有些数据集是 32×32,效果会打折扣,因为 δ 要表达的细节太粗糙);编码器用常见卷积主干,输出 δ 维度在 256 左右;解码器用反卷积或者上采样加卷积;优化器选 Adam,学习率 2e-4,批量大小 32 或 64;编码器解码器不要共享权重,否则容量受限。
训练节奏上,一个常见的误区是“训到重构损失完全收敛”。其实不需要,重构收敛到很低往往意味着模型已经开始记住基类样本,反而过拟合。最好的判断方式是训练过程中随时抽几对验证对,看到生成图结构清晰但不过度锐利时就可以停了。
4. 新类样本合成:从基类学到的差异如何迁移到新类
4.1 三种可落地的合成策略
模型在基类上训练完成后,E 和 D 都冻结。接下来要做的是给新类“变”出足够多的样本。按我的理解,论文的有效策略可以归纳成三种,实际操作中常常混用:
第一种,直接用新类自身的样本对计算 δ。假设新类给了 5 张支持图,我们就可以构造 5×4 个有向对,算出这批 δ,然后重复用到支持图上合成新样本。这种 δ 来自于新类自身,最精确,但问题是只有这么几个 δ,多样性很差,合成出来的图很容易只是支持图的轻微扰动。
第二种,从基类池子里搬 δ。在训练阶段,把基类各种图像对算出的 δ 全部收集起来,形成一个 δ 池;合成新类样本时,随机(或按某种相似度)从池子里抽 δ,再配合新类的不同支持图去解码。因为 δ 是类无关的,搬过来的变换模式对新类同样适用,多样性一下子就上来了。这是论文最核心的迁移逻辑。
第三种,对 δ 建模后采样。把基类 δ 池拟合成一个高斯分布,合成时直接从分布里采样。平滑插值的好处是能产生训练对里没出现过的中间变换,但风险是可能采到低概率区域,生成不稳定。更稳的做法是采样后加一个简单的过滤,比如用基类训练过的分类器看一眼生成图,置信度太低的扔掉。
4.2 合成数量、混合比例与后续分类器
合成的伪代码可以写成下面这样:
# 合成阶段的伪代码框架 # 训练结束后收集 delta_pool delta_pool = [] for x_a, x_b in base_pairs: with torch.no_grad(): delta = encoder(x_a, x_b) delta_pool.append(delta) # 处理每个新类 for c in novel_classes: support_images = support_set[c] # K 张真实图 generated = [] for gen_idx in range(num_synthesized): s = random.choice(support_images) # 选一张真实支持图 d = random.choice(delta_pool) # 抽一个差异向量 with torch.no_grad(): x_gen = decoder(s, d) # 合成新样本 generated.append(x_gen) final_support[c] = concat(support_images, generated)合成数量建议从每类 100 张左右开始调。太少,效果不够;太多,合成噪声会被分类器吸收,边际收益递减,甚至下降。合成样本和真实支持图混合后,下游分类器怎么训也很关键。一个稳妥做法是用“基类预训练特征 + 线性分类头”,在混合支持集上做几次迭代的微调;如果直接用 softmax 从头训,合成样本的作用会被稀释。
还有一个容易踩的坑:千万不要把新类任何一张图混进基类训练对里。这类模型的泛化依赖严格的类别划分,一旦数据泄漏,δ 里就包含了新类信息,看起来涨点很快,换到真正的新类就崩。实验评估时也最好分多个随机种子反复跑,因为合成过程带随机性,一次结果说明不了问题。
5. 实验结论里值得细看的几组对比
5.1 数据集和基线的选择
论文的实验横跨了不同类型的识别难度。一类是粗粒度通用物体,样本以裁剪后的 84×84 图像为主,类别之间差异较大;另一类是细粒度物种分类,不同类之间的外形差距很小,类内变化主要是姿态、角度、背景这类“非语义”因素;还有一类涉及纹理或材质,类别差异更依赖于局部细节。
基线方面,除了最朴素的最近邻、原型分类器之外,论文还会对比当时主流的度量学习方法和元学习方法。为了体现纯合成方法的贡献,还会加一个“不合成、只微调”的基线,这样涨点才能归因到合成样本上。这里要提醒一句:小样本评测对实验设置非常敏感,随机划分的类别、支持集采样方式、特征初始化都会波动几个点。所以看论文结果时,我更关注相对趋势而不是绝对数值。
5.2 涨点、消融分析和可视化效果
从整体趋势看,∆-encoder 合成样本带来的提升在 1-shot 上最明显,往往能把好几个基线的准确率往上推几个到十几个百分点;5-shot 时提升幅度变小,因为真实样本已经能覆盖一部分类内变化。这符合直觉:样本越少,合成补充的边际价值越大。细粒度任务上的收益通常比粗粒度更突出,原因也简单——细粒度类的类间差异本来就在“动作、姿势、局部纹理”这些共享变换上,合成出来的姿态变化正好补在痛点。
消融实验里,我认为最值得关注的是两点。第一,把编码器换成固定的随机噪声源、只用解码器去生成,效果明显下降,这证明了“δ 应该由真实图像对提供”而不是随机捏造。第二,去掉跨类对训练后,合成样本会出现奇怪的内容混叠,说明跨类对确实起到了净化 δ 空间的作用。至于加入判别器的变体,生成图肉眼可见更锐利,在细粒度任务上通常表现更好,但训练曲线更抖,需要花时间调权重。
可视化效果上,这种“以图生图”方法的优势是透明。我建议想尝试这个方法的开发者,在训练好后先做一个小实验:随机取一张支持图和几个不同的 δ,解码生成几张图,看看是不是“同一只鸟换了姿态/背景”而主体类别没变。如果生成的图内容漂移成了狗或猫,说明 δ 空间没有学好,这时候去调配对策略比单纯堆训练时长更有效。
6. 我的评判与现实落地提醒
6.1 方法成立的三个前提
基于对论文的阅读,我认为它成立要满足三个前提。第一,基类必须足够大且类内变化足够丰富,否则学不出通用的变换模式。基类里如果全是一个朝向、一个光线的“证件照”,δ 池的多样性就很差,合成等于白搭。第二,新类的变换模式必须和基类的共用。如果新类有一种基类完全没有的姿态模式,比如基类都是正脸、新类测试时全是侧脸,那合成的侧脸就是瞎蒙。第三,图像分辨率不能太低。32×32 的图上,δ 表达的细节非常有限,合成效果很难有质的飞越;这也是为什么很多这类工作都在 84×84 或更高分辨率上做。
如果违背这些前提,方法很容易失败。最典型的失败模式是:合成样本虽然看着多样,但分类器只是“看了更多假图”,没有真正学到新类判别特征,最终准确率不升反降。遇到这种情况,先别急着调模型结构,回头看看 δ 池的多样性和合成图的真实性。
6.2 从这篇论文延伸出去的后续方向
这篇论文最值得记下的,不是那个具体的网络结构,而是“把样本合成问题转成差异迁移问题”的范式。后来很多工作其实都在吃这个红利:有的在隐空间里对真实特征做加减操作,这就相当于在特征域复用“差异向量”的思路;有的利用大规模预训练条件生成模型,通过提示词控制变换模式,本质上也是给内容锚点施加一个可靠的变换。我甚至觉得,这篇论文的思想在图像编辑、风格变换、域泛化这些领域都可以复用——只要有“少量内容 + 大量可迁移变换”的场景,就能套用这个框架。
如果想在这篇论文基础上继续做,我会优先考虑三个改进:一是把像素重构换成感知损失,生成图在语义上会更合理;二是让 δ 带有语义标签,比如“向左转 30 度”“光照变暗”,这样可以定向合成特定变换,而不只是随机抽样;三是把合成和分类器联合训练,让分类器的反馈指导哪些 δ 值得保留。
6.3 动手复现时最容易踩的几个坑
最后集中说几个实操层面的坑。第一,合成阶段的随机性很烦人。同一个支撑图、同一个 δ,解码结果也可能忽好忽坏,所以评估时要固定随机种并平均多次。第二,判别器版本的训练要控制 λ_g,否则模型可能变成“只重颜值不重内容”,生成图是美的,但类别语义漂移了。第三,内存管理别大意。每类合成几百张图,看起来不多,但如果是 84×84 的 RGB 图,存储和特征提取的耗时会在多次实验中滚雪球。第四,也是最容易被忽略的:下游分类器的训练步数要少。合成样本本质上是“近似真实”,训练太久分类器会把合成样本的伪影学进去,微调十几轮就够。
我个人读完这篇论文最大的体会是:在数据稀缺的场景里,与其费劲去“变出”不属于任何已知类别的新内容,不如认真研究“已知内容之间是怎么变化的”。这个视角让样本合成从玄学变成了可控制、可解释的工程问题——这是它比同期很多生成类工作高明的地方。如果你也在做小样本方向,建议亲自跑一遍这个基线,哪怕只是把 δ 池那套流程搭起来,也会对“什么才是可迁移的视觉知识”有更具体的理解。