☰
开集领域适应(OSDA)核心原理与工程实践解析
2026/10/24 3:34:29 网站建设 项目流程

前阵子帮某团队调试一个视觉质检模型,训练数据里只有流水线上常见的几十种已知缺陷类别,可部署到新产线后,模型遇上一批从没见过的缺陷类型,依然会老老实实地把它划进某个“看着最像”的已知类别,然后整个质检流程就出错了。这就是开集领域适应(Open Set Domain Adaptation,简称OSDA)要处理的核心问题:源域和目标域不仅数据分布不同,连类别集合都可能对不上。

这篇文章从问题定义、经典方法、实验评估到部署踩坑,把这套技术讲透。适合正在做迁移学习、域适应相关工作的同学,也适合只想搞清楚“未知类到底怎么从模型里检测出来”的工程人员。

1. 为什么“类别对不上”比“数据分布不同”更难搞

1.1 领域适应的演进:从默认类别一致到被迫面对新类别

传统的无监督领域适应(UDA)假设很美好:源域和目标域的类别空间完全一样,只是数据分布不一样。比如源域里标好的“猫、狗、车”,目标域里也只会出现“猫、狗、车”,模型要做的只是跨过拍摄风格、光照、背景这些视觉差异。

但真实世界根本不按这个剧本来。某团队在实验里用了一个很典型的设定:源域是某个公开数据集里的40类日常物体,目标域是另一个场景下的同40类,看起来一切正常,可跑完UDA流程后他们随便挑了一个类做测试——把目标域里没见过的几类新物体喂给模型,模型硬把它们全部当作这40类里的某几类输出了。问题不是分布偏移,而是类别根本没对齐。

所以领域适应领域内部也分出了几档设定:

  • 闭集领域适应(Closed Set DA):源域和目标域类别完全一致。
  • 部分领域适应(Partial DA):目标域类别是源域类别的子集。
  • 开集领域适应(OSDA):目标域中除了源域的已知类,还包含源域从未见过的未知类。

OSDA比前两档都现实。因为落地的时候,你没法保证目标环境里不会出现新东西。自动驾驶训练集里没有的异形工程车、工业质检里没标注过的表面瑕疵、医学影像里没见过的病变形态,都是“开集”问题。

1.2 OSDA的形式化定义:到底在优化什么

把OSDA用数学语言写清楚之后,很多模糊争论都会消失。

假设源域给出带标签的数据集 D_s = { (x_i^s, y_i^s) },其中 y 属于类别集合 C_s,共 K 个已知类。目标域给出无标签数据集 D_t = { x_j^t },它的真实类别集合 C_t 由两部分组成:一部分是源域里也有的已知类,另一部分是源域完全没有的未知类。未知类有多少个、长什么样、样本量多大,训练时一概不知道。

模型要做两件事:

  1. 对目标域里的已知类样本,给出正确的已知类预测。
  2. 对目标域里的未知类样本,输出“我不认识”,而不是硬分到某一个已知类里。

这里有个容易被低估的难点:未知类从来没有出现在标注里,模型从源域那里学到的所有监督信号都来自已知类。因此,模型只能从“负面信息”里学习未知:一个样本如果怎么看都不像任何已知类,那它大概率是未知类。换句话说,未知类检测在本质上是一个“分布外检测 + 对抗领域对齐”的复合问题。

1.3 三类设置的直观对比:差异在哪里

设定源域类别目标域类别训练时能否看到目标域标签核心难点
闭集UDAA、B、CA、B、C否领域偏移,但类别可对齐
部分DAA、B、C、DA、B、C否目标域确实没有D,模型不能硬学
开集OSDAA、B、CA、B、C、X、Y否既不知道X和Y存在,又要从已知类里把它们分出来

从这个表能看出来,OSDA的操作空间比前两者都小。闭集UDA可以放心地把目标域所有样本都拉到源域类别里,因为理论上所有样本都落在这K个类内;OSDA如果也这么做,就把未知类样本全部“吞”进了已知类,等于完全放弃未知类检测。

所以很多做UDA的人第一次写OSDA实验时,都会产生同一个直觉误区:给模型额外开一个“未知类”输出位,目标域样本不是已知K类就是第K+1类。这个方案听起来合理,实现起来却会翻车,原因放到下一节细说。

2. 经典方法的底层逻辑:分类器输出之和如何暴露未知样本

2.1 直接加第K+1类:为什么行不通

给分类器加一个“未知类”输出,让模型在训练时把“看起来不像已知类”的样本标记成第K+1类,这是新手最容易想到的方案,也是最早被证伪的方案。

问题在于:未知类在源域里一个正样本都没有。分类器的第K+1类完全是在“真空”里学出来的,它的决策边界没有数据支撑,本质上是一个随机的空间划分。训练时模型确实会在某些目标域样本上输出第K+1类,但这是因为模型“碰巧”觉得这些样本边缘化,而不是因为真正学会了区分未知类。换一个随机种子跑一遍,分界线就会完全不同。

业界把这个问题叫“开放空间风险”:已知类的分布是有限区域,未知类可能在已知类周围的无限空间里任何一个位置出现。你没法用有限数据去限定无限空间的边界。

2.2 经典的对抗式开集适应:F负责圆场,C负责抓包

现在被广泛复现的经典OSDA方法,核心是一套对抗式逻辑。整个模型只输出K个已知类的分数,不单独设第K+1维度。关键是重新定义了“这个样本有多像已知类”。

具体做法是这样的:

  • 特征提取器 F 把输入图片映射成特征向量。
  • 分类器 C 在特征向量上输出K个logit,分别对应K个已知类。
  • 定义该样本属于已知类的置信度,等于这K个logit的和。

这个置信度对源域样本来说应该很高,因为源域样本一定是某个已知类。对目标域未知类样本来说,理想情况下应该很低,因为它在已知类的特征空间中缺乏支撑。

接下来是调兵遣将的对抗部分。特征提取器希望目标域所有样本的已知置信度都尽量高,也就是强行把所有目标域样本都往已知类空间里“推”,不看它是已知还是未知。分类器则希望目标域样本的已知置信度尽量低,尤其是那些确实不对劲的样本。

这个交手过程可以类比成两个角色的博弈:特征提取器像售楼处的销售,不管进门的顾客想买别墅还是公寓,先统一往“我们的楼盘很合适”的方向忽悠;分类器像负责核验身份的保安,凡是发现顾客动机不明、证件存疑的,就坚决不给通过。两者对抗到最后,真正落在已知类特征空间里的目标域样本会被留下,而那些怎么推也推不进已知空间的样本,就会被分类器标记出来。

  • 对目标域已知类样本:特征提取器成功把它们拉到已知类区域,分类器也接受它们,最终能正确分类。
  • 对目标域未知类样本:特征提取器尽力推,但它们在源域已知类特征空间里没有足够相似的支撑,分类器最终会把它们的已知置信度压得很低。

2.3 为什么用logit之和而不是最大softmax概率

很多人第一次看这个方案时都会问:为什么不直接用softmax后所有类别概率的和?反正softmax概率和也是1,这不是脱裤子放屁吗?

问题就在于softmax的归一化。K类的softmax对所有输入都会输出一组和为1的概率,不管这个输入是已知还是未知,输出的概率和永远是1。也就是说,如果你用softmax后的概率来判断“有多像已知类”,几乎所有样本都会得到“非常像”这个结论,因为softmax把未知类样本也硬塞进了已知类的概率份额里。

直接用logit求和就不一样了。logit是未归一化的原始得分,当一个样本与已知类特征空间完全不沾边时,它的K个logit都会偏低,求和之后自然得到一个很低的已知置信度。换句话讲,logit之和是一个没有被强制归一化的“已知性”信号,它保留了“这个样本离所有已知类都远”的信息。

这个细节不只是理论洁癖,它直接影响训练稳定性。原始实现里对抗损失要用sigmoid二分类来处理这个置信度,一旦你错误地把softmax概率和当作已知置信度来算对抗损失,梯度信号会变得极其不稳定,UNK指标往往很难看。

2.4 训练循环的骨架:核心步骤伪代码

把上面这套机制落成代码骨架,大概是这样的:

# 简化伪代码,假设已经定义好特征提取器 fe、分类器 clf for x_s, y_s, x_t in loader: f_s = fe(x_s) # 源域特征 f_t = fe(x_t) # 目标域特征 logits_s = clf(f_s) # K 个已知类 logit logits_t = clf(f_t) # 分类损失:源域上的标准交叉熵 loss_cls = CE(logits_s, y_s) # 已知置信度:K 个 logit 求和 score_s = logits_s.sum(dim=1) # 源域置信度,预期较高 score_t = logits_t.sum(dim=1) # 目标域置信度,用于对抗 # 对抗损失:sigmoid 二分类 loss_adv = BCE(score_s, ones) + BCE(score_t, zeros) # 分类器更新时只回传分类损失 # 特征提取器更新时,回传分类损失和带负号的对抗损失 # 负号是让特征提取器往提升目标域置信度的方向走 loss_total = loss_cls - lambda_ * loss_adv loss_total.backward() optimizer.step()

这个伪代码里最耐人寻味的,就是那个“lambda_”。它控制着对抗强度的比例。我做过一个简单但直观的消融实验:lambda_取0的时候,模型退化成普通闭集UDA,未知类基本全被吞掉,UNK接近零;lambda_取值过大时,模型会走极端,把所有目标域样本都判成未知类,UNK冲得很高但OS*惨不忍睹;只有取到合适的中间值时,HOS才会站上高点。

测试时的判定也很直接:计算目标域样本的logit之和,也就是已知置信度,与设定的阈值比较。低于阈值判为未知类,高于阈值则取logit里最大的一维作为已知类预测。

def predict(x, fe, clf, threshold): f = fe(x) logits = clf(f) known_score = logits.sum(dim=1) pred = logits.argmax(dim=1) # 置信度低于阈值,判为未知 unknown_mask = known_score < threshold pred[unknown_mask] = UNKNOWN_LABEL return pred

阈值怎么定,是实验阶段最容易翻车的地方,第三部分集中讲。

3. 实验评估里容易被带偏的三个关键点

3.1 OS*、UNK和HOS:为什么不能用准确率说话

评估OSDA模型不能只看总体准确率。举个极端例子:如果目标域里未知类样本占比很低,模型把所有样本都判成“已知类里的某一个”,总准确率可能会不低,但实际上未知类一个都没找出来,这在质检场景里等于是重大事故。

所以OSDA实验报告基本都围绕三个指标:

  • OS*(Known-Class Accuracy):已知类的分类准确率。它衡量模型对目标域已知类样本的分类能力。
  • UNK(Unknown Recall):未知类的召回率。它衡量模型能把多大比例的未知类样本识别出来。
  • HOS(Harmonic Score):调和平均,公式是 2 × OS* × UNK / (OS* + UNK)。

为什么用调和平均而不是算术平均?因为OS和UNK之间天然存在博弈关系。你放宽阈值,更多样本判成未知类,UNK涨了,但已知类样本也被误伤,OS会跌。反过来收紧阈值,OS*涨但UNK跌。调和平均对这种“一个涨一个跌”的指标很敏感,任何一个被拖垮都会让总分明显下降。大家看模型好坏,先看HOS,再看两个分项是否均衡。

这里有个小教训:某次我在一个目标域已知类只有几十个样本的小数据集上跑实验,UNK指标因为未知类样本太少,波动幅度能到正负十几个点。后来把全部实验重复跑了三遍取平均,又加上不同随机种子下的方差,报告才稳定下来。做实验评测时一定要清楚自己的样本量处于什么数量级,否则很容易被一次运气好/运气差的随机种子带偏结论。

3.2 实验设定怎么搭:谁当未知类、留多大比例

搭建OSDA实验标准流程时,第一步是把数据集按类别劈开:源域保留全部类别,目标域则从中抽取一部分类别充当“未知类”。比如某个有40类的数据集,可以设定目标域的30类与源域共享、剩下10类在目标域中出现但在源域中完全消失。目标域中这10类的样本就是模型训练时看不到、测试时突然冒出来的东西。

类别抽取比例对难度影响很大。把未知类比例调大,比如目标域一半类别都是未知的,UNK指标很容易冲到很高,因为大比例未知类会让模型更容易总结出“别乱认”的规律。把未知类比例压低到10%以内,模型又会倾向于把一切目标域样本全归到已知类里,UNK经常掉到个位数。正因为如此,同类实验要在相同设置下比较结果才公平,跨比例看HOS数字没有意义。

我建议做对比实验时把目标域未知类比例固定在一个标准值,并额外跑一个偏低的“困难版”和一个偏高的“容易版”,观察模型在压力下的变化趋势。只报告一个比例的做法,很难说明模型到底有多鲁棒。

3.3 阈值和lambda:两个超参数的不同调法

前文提到的阈值和lambda是OSDA实验最耗时间的地方。

阈值的作用在测试阶段体现。理论上,如果模型对已知类的置信度都集中在高位、未知类置信度都集中在低位,那阈值放在两者之间任何位置都能得到不错的结果。但实际问题中两类置信度分布常有重叠,阈值放低一点UNK掉,放高一点OS*掉。实操时应该先用一部分训练数据做模拟验证,画出已知置信度分布曲线,再在曲线上找一个能让HOS最大化的切点,而不是拍脑袋定0.5。

lambda的作用在训练阶段体现。lambda太小,对抗作用弱,模型退化成普通UDA;lambda太大,特征提取器的优化方向被对抗损失主导,源域分类精度也会下滑。我自己的经验是:先用小lambda(比如0.1)跑通整个流程,确认各项指标没有异常,再从0.1、0.5、1.0、2.0这个量级上做粗搜,找到峰值范围后细化。粗搜时一定要同时观察OS和UNK,不要只盯着HOS,否则偶尔会出现HOS看起来还行、但OS和UNK相差悬殊的假均衡。

4. 复现与部署时的踩坑记录:从现象到根因

4.1 未知类被“吞掉”:UNK低得离谱的时候先查什么

最典型的失败现象是:UNK始终在10%以下,模型几乎把所有未知类样本都当成已知类给分了。很多人第一反应是“lambda不够大”,直接上调对抗强度,结果发现UNK没怎么涨,OS*倒是掉了一截。

正确的排查链路应该是这样的:

  1. 先看训练日志里对抗损失曲线。如果对抗损失收敛得太平滑,说明分类器可能没有真正“挣扎”——它在战场上躺平了,特征提取器说什么它都接受。这时候应该先检查对抗损失里目标域置信度的梯度方向是否正确。
  2. 再画目标域样本的logit之和分布图。正常训练好的模型应该看到两个峰:一个代表已知类,一个代表未知类;如果只有一个峰,说明特征空间里已知和未知还没有被拉开。
  3. 最后再看lambda。确定前两项都没有问题时,再调lambda才有意义。

我在某次实验里发现,UNK低并不全是lambda的锅,真正的问题是我在分类器上用了Dropout且测试时忘记关闭,导致置信度分布被噪声打散,阈值怎么选都切不准。这种细节很难从指标上直接看出来,只能靠经验排查。

4.2 已知类被误判成未知类:OS*崩了,UNK虚高

另一个典型现象正好相反:UNK虚高,OS*崩盘。模型把大量已知类样本判成了未知类,警告满屏,但实际上是误报。

这种情况最常出现在源域类别之间距离过大的时候。比如源域里“车、飞机、轮船”三个类,彼此差异很大,源域分类器很容易把它们分得很开,特征空间里类与类之间留出了大片空地。目标域里的已知类一旦落在这些空地里,特征提取器再怎么推、分类器也觉得它不够像任何一个已知类,于是被判定为未知。

处理这类问题的关键在源域一侧。我开始尝试在源域训练时增强类别间的边界紧凑性,让已知类的特征分布更聚拢,这样目标域中落在空隙里的样本就能被拉到对应的已知类附近。另一个有效的操作是加一个二期伪标签筛选:先用训练好的模型给目标域样本打分,把高置信度的样本视为已知类,加入源域一起训练,低置信度的样本才继续作为未知类候选处理。

注意:伪标签筛选的阈值要动态调整。训练初期模型不可靠,过早加入大量错误伪标签会污染源域分类边界;训练后期模型相对可靠,可以适当放宽筛选条件。

4.3 训练初期的不稳定:波动大,收敛慢,怎么办

经典对抗式OSDA方法还有一个常见的工程问题:训练前期非常不稳定。特征提取器还没学会良好的特征表示时,对抗损失就开始左右优化方向,很容易出现训练前几十轮HOS在极低区域反复横跳、甚至一度完全丧失分类能力的情况。

我在实操中常用的三招:

  1. 前若干个epoch冻结对抗损失,只做源域分类训练,让特征提取器先把“已知类长什么样”建立起来。
  2. 对lambda做warm-up,从0线性增加到目标值,而不是一开始就上满强度。
  3. 给分类器一个单独的、更低的学习率,避免它在对抗中被特征提取器带节奏。

这三招本质上都是在做同一件事:先稳住底层特征,再开放对抗。底层特征不稳,所有上层对抗都是空中楼阁。

另外说一个很多人忽略的坑:源域和目标域的数据量差距。如果目标域样本远多于源域,每一轮迭代里目标域产生的对抗梯度会压倒性地主导整个优化,模型很容易进入“只学目标域、忘了源域”的状态。遇到这种情况,我会在采样器里做源域的过采样,保证每个batch里源域和目标域的样本比例不至于失衡。

4.4 一张表总结故障模式

现象可能原因建议排查动作
UNK过低对抗损失未生效检查梯度方向、增大lambda
UNK过低阈值切点偏高重画置信度分布
OS*过低已知类落入特征空隙增强源域类内紧凑性
OS*过低对抗过度,源域分类被干扰降低lambda、增加warm-up
训练震荡特征提取器未充分预热先冻结对抗损失

这张表是我做排障时最常用的速查清单。顺序也很重要,先排查机制性错误(对抗损失方向、梯度传播),再调阈值,最后才动lambda和模型结构,不要一上来就大改模型。

5. 2024年前后的新变化:源域不可用与未知类聚类

5.1 Source-Free OSDA:没有源域数据也要开集适配

传统OSDA有个前提:训练时源域数据在手上。但很多真实场景里,源域数据因为隐私、存储、商业授权等限制,根本不能带到新的部署环境中。市面上因此出现了源域不可用的开集领域适应方向:手里只有源域训练好的模型权重,加上一堆无标签目标域数据,要在目标域上直接做开集判别。

这个方向的实际做法一般分两步。先用源域模型给目标域样本打分,把高置信度样本当作已知类伪标签,对模型做一轮目标域上的自训练;再用低置信度样本的分布特征来校准“未知类”判定阈值。难点在于伪标签总会带噪,如果目标域中已知类的样本比例小,伪标签质量会很差,模型很容易在自训练里把这些误差固化放大。

我的建议是,使用Source-Free方案时,别忘了做一个未知类比例的粗略估算。如果目标域里未知类真的很多,那么低置信度样本会成为主流,自训练时至少要保证源域模型输出的类别先验不会严重偏离真实比例。

5.2 从“判为未知”到“理解未知”:给未知类做聚类

最近几年有个很有意思的转向:检测出未知类样本之后,不再简单地把它们扔出系统,而是尝试对未知类内部再做细分,挖掘未知类之间的结构。

这类方法常见的设计是:用CLIP这类大规模预训练视觉模型提取目标域未知样本的语义特征,把语义上相近的未知样本聚成若干组,每组对应一个“疑似新类”。模型产出不再是“未知”二值标签,而是一份“可能有这几个新类”的候选清单。这样在生产环境里,质检人员就能基于新类分组去建新的标注集,而不需要从头在一个巨大、杂乱的未知样本集合里做二次挖掘。

这个方向的实验评估目前还没有统一标准,很多工作会同时报告聚类准确率、新类发现数量、以及未知类检出的纯度。做工程选型时,如果下游流程确实需要“发现并归纳新类”,这类方案可能比纯二值判定有更高落地价值。

5.3 实际项目里到底该选哪条路线

根据我自己的经历,落地选型要看下游需求:

  • 只要求“发现异常、拦截未知”,而且目标环境相对稳定:经典对抗式OSDA方案足够。训练可解释性好、复现容易、指标清晰。
  • 源域数据不能带到现场:选Source-Free方案,但一定要先评估目标域里未知类比例,别指望零样本情况下模型还能完美适应。
  • 场景里天然分不清“多个未知类”怎么办:考虑带未知类聚类能力的扩展方案。这类方案通常更贵、更难调,但能给后续数据运营带来增量信息。

其实不管选哪条路线,开集领域适应的核心逻辑没有变:已知类的特征空间要足够紧凑,未知类检测信号要独立于分类器本身,评估时要同时盯住OS*和UNK。只要这三个基本面不出错,模型基本不会跑偏。

我自己的习惯是,新开一个OSDA项目时,先用经典方法把baseline跑出来,再做Source-Free或聚类扩展的迭代。理由是经典方法路线最成熟,踩坑文档也多,出了问题能快速定位;等baseline的HOS稳住之后,再往扩展方向发散,风险会小很多。这套“先经典、后扩展”的节奏,在实际项目中帮我省掉了大量的无效调试时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询