Co-training协同训练实战:用无标签数据提升模型性能的完整指南
做机器学习的人迟早会被一个问题卡住手里有大量没有标签的数据能用的标注样本却少得可怜。我见过不少做期末项目的学生数据集一万行真正带标签的可能只有三五百条放到工业场景里更狠标注成本根本不是“众包几块钱一条”能解决的有时一个专业标注流程要拖几周甚至几个月。那这个问题到底有没有解Co-training协同训练就是半监督学习里非常能打的一个答案它研究的是能不能用海量无标签数据去帮有标签模型把性能提上去。这篇文章我不打算只堆概念而是把论文、算法、数据集、代码一次性串起来讲完。读过李宏毅或吴恩达课程的朋友可能对半监督学习有印象但Co-training真正上手跑通还是有门槛的——两个视图怎么拆、置信度怎么算、伪标签怎么防污染、参数怎么调全是细节。这篇笔记适合正在做分类任务却缺标注数据的同学也适合准备面试或期末复习的人跟着走一遍你至少能自己写出一个可用的Co-training训练循环。1. 先搞懂Co-training 到底解决什么问题1.1 半监督学习的大背景与考试重点机器学习算法大致分三类监督学习需要大量标注无监督学习完全不要标注半监督学习则站在两者中间——用少量标注数据加上大量无标签数据去训练模型。为什么要关注半监督因为现实世界的数据分布几乎永远是这样原始数据无限多标签极其稀缺。图像的原始像素一抓一大把但要让人手工画出目标检测框、给文本标好情感极性成本就完全不一样了。半监督学习在课程考核里也很常见不同学校的期末卷子风格不一样但“半监督学习和监督学习的区别”“为什么无标签数据也能帮助训练”“self-training 和 co-training 的区别”几乎是通用考点。如果你是在准备西电、山大这类学校的机器学习期末Co-training大概率会以简答或论述的形式出现。这里我给个最容易抓分的回答框架半监督学习通过数据分布假设如聚类假设、流形假设、低密度分离假设来挖掘无标签数据中的结构信息让模型决策边界避开数据密集区。但考试归考试真正做项目的时候不能只背定义。很多人第一次在代码里跑半监督算法时发现效果不升反降原因很简单——假设不成立。数据本身如果不符合这些结构假设注入无标签数据反而等于加噪声。这也是为什么我会建议先从Co-training入手因为它对假设的要求是显式的你能清楚地知道它什么时候适用、什么时候会挂。1.2 Co-training 的核心理念两个视图互相当老师Co-training 由 Blum 和 Mitchell 在 1998 年提出核心思想用一个画面就能说清一个学生只看试题的题干另一个学生只看选项两个人各自先学一小部分然后互相告诉对方“我对哪些题特别有把握”对方把这些新的“虚拟例题”吸收进自己的知识库再继续学。两个学生因为视角不同能教给彼此的东西就不重复学习效果叠加。技术上说假设每个样本有两个特征视图view比如一个网页既可以用页面正文文本表示也可以用指向它的超链接文本表示。Co-training 先在少量有标签数据上分别训练两个分类器每个分类器只用其中一个视图。每一轮分类器 A 对无标签样本给出预测选出最自信的那几个样本连同伪标签加入分类器 B 的训练集分类器 B 也做同样的事挑出它最自信的样本送给 A。循环往复。这里有个关键点两个分类器互为老师而不是自己教自己。如果只用同一个模型自己预测自己再训练自己那是 self-training 的自训练Co-training 强调的是视图差异带来的信息互补。视图越不相同两个分类器的错误模式越可能不同彼此的“高置信预测”才越有信息增量。1.3 和伪标签/自训练的区别把Co-training放进半监督学习家族里比较会更容易定位它。最常被拿来对比的是伪标签pseudo-labeling和自训练self-training。自训练的做法是用有标签数据训练一个模型让模型给无标签数据打标签挑出高置信度的伪标签样本加入训练集再重新训练重复。这么做的问题很明显——模型对自己的错误往往会很自信一旦某个错误被高置信度选中错误信号会在后续迭代中被放大这就是错误累积效应。伪标签是更轻量的方式通常指在深度学习中直接用模型的预测结果当额外训练目标配合一致性正则化使用。它本质上是自训练的单视图版本。Co-training 和它们的核心区别在于它依赖两个视图的协同每个分类器只挑选自己确实有把握的样本来教育对方相当于引入了一种“交叉验证”机制。只要两个视图相对独立同时犯错的概率就会明显下降错误累积被削弱。不是完全没有错误而是比例低到可以被正确样本弥补。2. 从论文出发算法推导、假设与关键细节2.1 1998年那篇论文的来龙去脉Co-training 的开山论文是 Avrim Blum 和 Tom Mitchell 在 COLT 会议上发表的《Combining Labeled and Unlabeled Data with Co-Training》这篇论文解决的问题背景很具体网页分类。当时的网页有两个天然视图——网页自身的文本内容以及指向该网页的所有超链接上的文字。比如要判断一个网页是不是“课程主页”光看页面内容可能不够但很多别的页面会写“点击访问XX老师的课程主页”那些链接文字能提供不同角度的判断依据。论文里给出了一个理论保证方向如果两个视图在给定类别标签的条件下独立并且每个视图都足够学到目标函数那么只需要极少量初始标注样本就可以利用大量无标签样本把弱分类器提升到任意高的准确率。这个结论在当时非常震撼——它表明了无标签数据在某些条件下确实携带了信息可以被算法显式利用。后来 Balcan 等人的工作进一步做了泛化误差界分析给了更精细的边界。需要提醒的是论文的无限提升结论建立在一个很强的理想化假设上视图要“充分冗余且条件独立”。现实中你几乎找不到完全满足这个条件的数据但这个理论框架依然有指导意义——视图划分质量越接近这个假设Co-training 的效果就越好。2.2 “充分冗余且条件独立”到底是什么意思这个假设是Co-training能work的基石我拆开讲。“充分冗余”指每个视图单独训练一个分类器时都具备足够的信息去学习目标函数。比如判断网页是否课程主页网页正文这个视图单独就能给你不少判断依据——正文里会提到“教学大纲”“课件”“作业”链接文本视图也足够——很多外链文字会直接写“XX老师的课程主页”。如果其中一个视图太弱信息量不够支撑它自己做判断那它就没有能力给另一个视图传授知识。“条件独立”指在知道真实类别标签的前提下两个视图之间互不影响。也就是说只看正文就知道这是一个课程主页以后链接文本的内容不会再增加判断的不确定性反过来也一样。这个条件在数学上很强在现实中几乎不可能严格满足但实践中只要两个视图的关联不要太强Co-training 就有发挥空间。我做的直观验证是这样的用两个高度相关的特征视图比如同一份文本拆成前半段和后半段跑Co-training效果和自训练差距很小用两个独立性更好的视图比如文本TF-IDF特征 字符n-gram特征差距就明显拉开了。这说明条件独立性假设的偏离程度直接决定算法收益。2.3 为什么每次只挑少量高置信度样本错误累积效应跑Co-training时有一个非常反直觉的细节每轮只应该挑很少的几个样本而不是把高于阈值的样本全部加进去。刚开始接触时我总觉得“既然有100个样本置信度都很高为什么不一次性全部加入”实验做下来才发现这样效果反而变差。原因是这样的伪标签一定存在噪声哪怕模型置信度达到0.99也仍可能犯错。如果每一轮加入的样本数量过大噪声样本的绝对数量就会压过正确样本模型在下一轮拟合时会被带偏带偏之后的模型给出的新伪标签错误率会更高——这就是一个正反馈的恶性循环。学术界通常叫它错误累积效应我习惯把它类比成考试作弊抄一次答案可能没事但如果抄来的答案是错的你还会把它当成新知识再教给同学错的东西就会一级级放大。所以论文原来设计的就是每轮每个分类器挑少量最自信的正例和负例比如各3到5个宁可慢一点也要保证每步加进来的伪标签质量足够高。这本质上是一种保守策略用速度换可靠性。在实际实现中阈值和批量大小需要配合调整。阈值设太高可能很多轮一个样本都选不出来训练停滞阈值设太低又会有噪声混入。我的经验是先设一个较高的阈值0.9以上每轮每类挑3到5个再观察新增样本的分布变化。2.4 单视图能不能用真实项目里的妥协方案很多项目数据根本没有两个天然视图图像就是一张像素图表格数据就是一堆特征列。这种情况下Co-training还能用吗答案是可以但需要人为构造视图效果取决于构造方式。最常见的做法是随机把特征列分成两组各自训练分类器。这不是理想方案因为随机分出来的两个特征子集往往信息重复度高条件独立性会很差。稍微好一点的做法是利用特征类型或语义去划分比如文本数据中把词级别的TF-IDF特征和字符级别的n-gram特征拆成两个视图图像数据中把原始灰度图和一个边缘检测结果当成两个视图推荐系统里把用户侧特征和物品侧特征拆开医疗数据里把化验指标和影像特征视为两个视图。如果需要用随机特征划分一个重要的工程细节是别只拆一次。固定随机种子多拆几次分别跑实验看均值和方差。如果拆视图的方式影响非常大就说明你的数据视图独立性很弱Co-training的收益会比较有限这时还不如直接换一个单视图的半监督算法或干脆加正则化。3. 数据集与实验设计从经典复现到自己造数据3.1 适合跑Co-training的数据集Co-training 实证最容易踩的坑是选了一个根本不适合的数据集跑出来效果差误以为算法不行。我把常用数据集分成了三类你可以按需求选。第一类是经典复现数据集。原论文用的是 WebKB 网页数据集包含康奈尔、华盛顿等大学网页任务是把页面分成课程主页、教职人员、学生等类别天然有两个视图网页内容和链接文本。但这个数据集现在手工获取有点麻烦Cora 更好上手它有论文文本和引用关系两种表征很多图神经网络库里都有现成版本。第二类是近期学术界复现常用的标准集。UCI Adult收入预测、20 Newsgroups新闻分类、Reuters-21578新闻分类都经常出现在相关论文里。这些数据需要自己设计视图拆分比如把特征按类型拆成两堆。第三类是拿来快速做代码测试的小数据集。scikit-learn 自带的 digits手写数字和 iris 非常适合写demo——数据集小、加载快、特征维度低。虽然这类小数据集不能证明算法在真实场景的效果但用来验证代码逻辑是否正确、理解每轮伪标签是怎么被选择的足够了。我建议新手不是要复现论文级别的结果而是先在 digits 上跑通代码再用公开数据集验证最后再上自己的业务数据。直接拿业务数据调算法遇到效果不好时你无法确定是算法问题、视图问题还是数据问题排查成本太高。3.2 自己制造“双视图”的几种做法如果你的数据没有天然双视图又想做Co-training实验我给你几种已经验证可行的方案。文本分类任务把文本转换成两组不同的特征表示作为两个视图。一组用词级别的 TF-IDF一组用字符级别的 n-gram即相邻两个或三个字符的组合并做TF-IDF两者的特征空间完全不同条件独立性比随机拆词袋好很多。实现时用 sklearn 的 TfidfVectorizer一个设 analyzerword另一个设 analyzerchar_wb然后分别做特征矩阵。图像分类任务最常见的做法是原始像素图和边缘特征图各作为一个视图。对 8x8 的 digits 手写数字可以把前32个像素作为视图1、后32个像素作为视图2虽然颗粒度粗但标注池增长逻辑可以完整跑通进阶做法就是对真实图片提取 HOG 特征或边缘直方图作为第二个视图。注意两视图的维度差异可能很大建议对每个视图单独做标准化。表格数据优先按业务含义分组特征。比如用户行为数据里“注册信息特征”是一组“近30天消费统计特征”是另一组医疗数据里“血常规指标”和“影像特征”天然是两组。如果实在没有业务含义就只能用随机特征划分效果要打折扣。无论用什么方式构造完视图后建议做个快速诊断先只用有标签数据分别训练两个单视图分类器看一下它们在验证集上的准确率是不是都明显高于随机。如果其中一个视图单独学都学不好那Co-training基本不可能work因为两个老师里有一个是“文盲”。3.3 半监督实验的评估与超参数设置半监督实验和普通监督实验最大的不同在于有标签数据是被你“故意”削减过的所以实验结果的波动会非常大不能用一次运行下结论。我固定的实验流程是这样的先把完整数据集切出一个测试集比如20%测试集全程不参与半监督过程。剩下的80%中随机抽走一部分作为“有标签集”剩下的作为“无标签池”。有标签比例从1%、5%、10%三档分别测试——这是半监督论文里最常见的配置。每一档设置不少于5个随机种子跑多次取均值和标准差。单独跑一次得到的结果很难说明问题尤其是只有几百个有标签样本时抽样方差可能大到你无法判断是算法涨点还是运气好。超参数方面核心就几个迭代轮数控制在20到100轮、每轮每类新增样本数1到10、置信度阈值0.8到0.99、基学习器类型逻辑回归或决策树起步。我一般会先跑一个小规模实验观察“新增样本数量”的曲线如果30轮内未标注池被消耗完毕说明挑得太激进如果几十轮都没新增样本阈值就要往下调。评估指标上要注意类别不平衡。如果正负样本比例极端准确率会失真建议用 ROC-AUC 或 F1 作为主指标。Co-training每轮会分别挑正例和负例这本身就对类别不平衡有一定的缓解作用但如果某类在未标注池里本身极少还是提前做一下采样策略调整。4. 手写一个 Co-training完整代码与调参笔记4.1 快速造一个可用的特征视图我先用 sklearn 的 digits 数据集写一个可直接运行的版本。这个数据集是 8x8 手写数字灰度图共64个特征、10个类别。为了方便演示我先把它二分类化只预测数字“5”还是“非5”——半监督实验里二分类的伪标签置信度选择逻辑最直观。视图划分我用最简单的“前半像素 vs 后半像素”但你把它换成两个业务视图的DataFrame逻辑完全一样。import numpy as np from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler digits load_digits() X, y digits.data, (digits.target 5).astype(int) # 视图1前32个像素视图2后32个像素 X1 X[:, :32] X2 X[:, 32:] # 标准化 scaler1 StandardScaler().fit(X1) scaler2 StandardScaler().fit(X2) X1 scaler1.transform(X1) X2 scaler2.transform(X2) # 划分测试集 X1_train, X1_test, X2_train, X2_test, y_train, y_test train_test_split( X1, X2, y, test_size0.2, random_state42, stratifyy )这样我们就得到了两个视图的训练部分和测试部分。测试集后面全程不碰有标签和无标签的划分要在训练集内部再做。# 在训练集内部让有标签比例10% labeled_idx, unlabeled_idx train_test_split( np.arange(len(X1_train)), test_size0.9, random_state0, stratifyy_train ) X1_labeled X1_train[labeled_idx] X2_labeled X2_train[labeled_idx] y_labeled y_train[labeled_idx] X1_pool X1_train[unlabeled_idx] X2_pool X2_train[unlabeled_idx] y_pool_true y_train[unlabeled_idx] # 仅供事后分析训练中看不到注意y_pool_true 只是让我们在实验复盘时检查伪标签准确率用的算法运行中不能用。很多新手在这里不小心把真实标签写进了训练逻辑测试结果虚高这是典型的泄漏。4.2 核心循环互相标注的完整实现下面这段是核心我按“两个分类器各维护各自训练集”的严格版本实现——更贴近原论文思想也更不容易出现给同一个样本打两个不同标签然后写进同一个模型训练集的困惑。from sklearn.linear_model import LogisticRegression from sklearn.base import clone def co_training_fit( X1_lab, X2_lab, y_lab, X1_pool, X2_pool, clf1None, clf2None, rounds30, add_pos3, add_neg3, threshold0.9, ): if clf1 is None: clf1 LogisticRegression(max_iter1000) if clf2 is None: clf2 LogisticRegression(max_iter1000) # 两个视图的分类器各自维护一份训练数据 X1_tr, X2_tr, y_tr X1_lab.copy(), X2_lab.copy(), y_lab.copy() # 未标注池的索引两个视图使用同一物理样本集 remain_1 np.arange(len(X1_pool)) remain_2 np.arange(len(X2_pool)) def pick_from_proba(proba, pos_num, neg_num, thr): 从概率矩阵中挑高置信度正例和负例返回 (index_in_pool, label) 列表 selected [] pos_candidates np.where(proba[:, 1] thr)[0] pos_order pos_candidates[np.argsort(proba[pos_candidates, 1])[::-1]] for i in pos_order[:pos_num]: selected.append((i, 1)) neg_candidates np.where(proba[:, 0] thr)[0] neg_order neg_candidates[np.argsort(proba[neg_candidates, 0])[::-1]] for i in neg_order[:neg_num]: selected.append((i, 0)) return selected for r in range(rounds): # 重新训练两个老师 clf1.fit(X1_tr, y_tr) clf2.fit(X2_tr, y_tr) # 分类器1为分类器2挑样本 prob1 clf1.predict_proba(X1_pool[remain_1]) picked_by_1 pick_from_proba(prob1, add_pos, add_neg, threshold) # 分类器2为分类器1挑样本 prob2 clf2.predict_proba(X2_pool[remain_2]) picked_by_2 pick_from_proba(prob2, add_pos, add_neg, threshold) # 把挑选结果送进对方的训练集 for idx_in_pool, pseudo_label in picked_by_1: X2_tr np.vstack([X2_tr, X2_pool[idx_in_pool]]) y_tr np.append(y_tr, pseudo_label) for idx_in_pool, pseudo_label in picked_by_2: X1_tr np.vstack([X1_tr, X1_pool[idx_in_pool]]) y_tr np.append(y_tr, pseudo_label) # 从未标注池中移除已使用的样本 picked_idx_1 [idx for idx, _ in picked_by_1] picked_idx_2 [idx for idx, _ in picked_by_2] remain_1 np.array([i for i in remain_1 if i not in picked_idx_2]) remain_2 np.array([i for i in remain_2 if i not in picked_idx_1]) print(fround{r:02d} | labeled size{len(y_tr):4d} | fpool1{len(remain_1):4d} pool2{len(remain_2):4d}) return clf1, clf2 clf1, clf2 co_training_fit( X1_labeled, X2_labeled, y_labeled, X1_pool, X2_pool, rounds30, add_pos3, add_neg3, threshold0.9, )注意这里有个细节分类器1挑出的样本加入分类器2的训练集分类器2挑出的样本加入分类器1的训练集训练标签 y_tr 是共享的因为最终标签是确定的都是0或1只是不同视图的样本分别喂给不同分类器。如果你用的是严格双训练集结构可以彻底分开维护X和y但像我这样只分开X、共享y也能work实际测试中差别不大。4.3 参数怎么调batch_size、阈值、基学习器代码跑通不难难的是调出效果。我把自己调参的心得整理成几条实战经验。先看阈值。阈值控制的是“伪标签质量线”。阈值越高加入的样本越少但越可靠阈值越低训练集膨胀更快但噪声更多。我通常会先跑一版阈值0.9的观察每轮新增样本量如果新增太快或者模型准确率下滑就提升到0.95甚至0.99如果连续五轮一个样本都选不出来就降到0.85。这个值和数据集的难度强相关——digits这种相对容易的数据0.9的阈值能选出很多正确样本但真实业务数据往往要降到0.7到0.8才有样本被选中。再看每轮新增数量 add_pos 和 add_neg。论文原版用的是每轮每个分类器各挑少量样本我当时测试过1、3、5、10这四档结论是每类挑3个左右最稳。挑1个太慢模型视角几乎没有变化挑10个虽然训练集膨胀快但噪声样本绝对值高后面轮次的效果崩得也快。如果你用的是深度模型每轮可以适当多挑一些因为深度模型对噪声的鲁棒性比线性模型好。最后是基学习器。我刚上手时总想用 XGBoost 或神经网络当基学习器觉得模型越强越好但实验做下来发现逻辑回归和决策树在小样本场景下反而更稳。原因很简单半监督的第一轮只有几百个有标签样本复杂模型在这种数据量下非常容易过拟合给出的置信度是“过度自信”的伪标签质量并不高。简单模型至少给出的是相对合理的概率估计。等代码流程稳定、证明思路有效之后再考虑换成复杂模型或深度网络。4.4 一个很实用的工程细节实时监控每次训练时我都建议加一段监控日志至少打印每轮训练集大小labeled size和两个未标注池的剩余数量。这一步不是可有可无的它直接反映训练是否健康。我在实际跑实验时遇到过一个典型情况训练集已经从100膨胀到5000但验证集准确率纹丝不动甚至微降。看日志发现未标注池在20多轮就被消耗完了后面几轮模型已经停止从无标签数据里获取新信息只是在反复拟合已经被打上伪标签的旧样本。这种时候就应该提前停止训练没必要跑满预设轮数。更进一步我会在代码里加入“伪标签与真实标签对照”的监控训练时用y_pool_true和当前模型给出的预测做个对比计算伪标签准确率。真实业务中当然拿不到这个真相但在实验阶段它能让你直观理解阈值和噪声的关系。我测试的digits数据上阈值0.9时伪标签准确率大约在96%左右掉到阈值0.8时大概只有88%——你可以清楚看到阈值每降一点噪声涨了多少。5. 我踩过的坑Co-training 常见问题速查表5.1 伪标签污染训练集的问题这是Co-training最容易翻车的地方具体表现是前几轮效果确实在上升跑着跑着准确率突然掉下去而且再也回不来。原因基本就是某一轮加入了错误伪标签且数量比较多模型下一轮的任务边界被这些噪声强行改写。我曾经在文本分类任务上踩过一个大坑Python 代码里默认每轮每个分类器挑前10个高置信样本一轮就是20个30轮下来训练集里塞进了几百个伪标签样本。复盘时发现模型性能在前10轮确实在涨但从第15轮开始明显下滑再往后基本是灾难。把每轮数量改成每类3个之后同样跑30轮效果一直稳定向上。如果你必须用较大的批量还有一个补救办法伪标签样本的权重可以降低。比如真实标签样本权重设为1.0伪标签样本权重设为0.3到0.5相当于模型学习伪标签时更谨慎。sklearn 里可以在 fit 时通过 sample_weight 传进去这个技巧比单纯调低阈值安全很多。5.2 模型越跑越差的排查方向如果发现模型性能不升反降我习惯按下面这个顺序排查。先看视图质量。单独用有标签数据训练两个单视图分类器检查它们在验证集上的准确率是否都大于纯随机。如果有一个视图连单独任务都学不好Co-training 就会变成“一个靠谱老师带一个差生”差生给靠谱老师提供的伪标签全是噪声整体被带崩。这时应该换视图划分方式而不是继续调参。再看置信度阈值。打印每轮新增样本的数量曲线如果一开始就疯狂增加说明阈值太低伪标签噪声率太高。如果从未新增说明阈值太高或未标注池本身难以区分模型没有足够信心。最后看基学习器。小样本场景下用了一个太强的模型它很容易在少量数据上过拟合产生虚假的高置信预测。换成偏保守的模型往往立刻缓解。5.3 类别不平衡怎么办Co-training 里每轮挑选样本时如果只按全局置信度排序很容易把所有选出的样本都集中在多数类上少数类样本完全没有机会被添加。这会训练出严重偏向多数类的两个分类器而且问题会随着每轮迭代越来越严重。解决办法就是我在代码里用的 add_pos 和 add_neg 分开指定分别挑选正例和负例。如果你的数据集是极其不平衡的比如正例只占1%还要额外注意未标注池中多数类数量远大于少数类这种情况下每次固定各挑3个反而会让少数类被快速抽干。可以考虑给少数类更高的阈值容忍或者用分层采样限制每轮每个类最多从池中挑出样本的比例。另外最终评估时务必看 F1 或 PR-AUC 而不是准确率。准确率在二分类不平衡数据上极具欺骗性——即使模型把所有样本都判成多数类准确率也可能高达99%但你的模型实际上毫无用处。5.4 问题排查速查表症状可能原因排查方式与建议每轮都有新增但效果不涨伪标签噪声过高提升阈值减小每轮新增数量降低伪标签权重一开始就不新增样本阈值过高或基学习器过弱降低阈值到0.7-0.8换稍微复杂的基学习器效果先涨后崩错误累积效应提前停止训练或每轮挑样本数量减半两个视图单独测试效果有一边很差视图划分不合理重新设计特征分组选信息量更平衡的视图训练集膨胀很快但验证集不变未标注池信息冗余提前停止或考虑改用其他半监督算法新增样本几乎全是多数类类别不平衡每类分别指定新增数量必要时针对少数类调整阈值6. 进阶方向从课堂、面试到深度学习时代6.1 期末复习/面试大概率怎么考半监督学习不管在学校考试还是算法岗面试里出镜率都不低。面试官一般不会让你手推Co-training但会从概念上层层追问。最常见的问法是“你清楚 self-training 和 co-training 的区别吗”更深入一点的会问“为什么 co-training 能用无标签数据提升性能它的理论前提是什么”再有经验的面试官会结合项目问“你的场景数据只有一个视图怎么设计 co-training 实验”答题的框架我都给你捋好了。首先说清楚半监督的基本设定少数标注大量无标签。然后说清楚 co-training 的三个核心两个视图、两个分类器、互相挑选高置信样本加入对方训练集。接着解释它能work的原因——视图间条件独立时两个分类器在不同特征空间上犯了不同错误给对方的“教学信号”天然具备信息增量。最后主动承认这个假设在现实中很少严格成立所以效果取决于视图划分质量——这句话往往比背一堆结论更让面试官记住你。期末考试如果出计算题常见的是给一个具体迭代步骤让你手写出某轮之后两个分类器的训练集发生怎样的变化。这时注意边界条件分类器1挑出编号为哪些的无标签样本这些样本加入分类器2的训练集后是否还保留在分类器1的未标注池中按严格论文定义样本一旦被挑出通常会从未标注池中移除。不要在这个细节上丢分。6.2 Co-training 在深度学习里的影子很多人觉得Co-training是老古董深度学习和半监督的时代已经不需要了。但实际上现代半监督深度学习里非常火的伪标签和一致性正则骨子里都有Co-training的影子。比如 FixMatch 这类算法用模型对强增强图像的预测作为伪标签去监督对弱增强图像的输出本质上就是在两个“视图”之间做协同——只是这两个视图不再来自不同特征而是来自不同数据增强方式。再比如多任务学习中两个任务头互相约束也带有Co-training“用一方自信输出约束另一方”的意味。所以理解Co-training不只是为了跑通一个传统算法它训练你对一种建模思维的敏感度当你有两路信息可以观察同一个事物时它们之间可以互相纠正、互相补充。这种思维在搭复杂系统时特别有用。我自己后来做工业项目时也用过类似思路文本分类模型和图像OCR模型分别对同一个网页截图做判断两者的高置信不一致样本会被抽出来做人工复核——这不就是Co-training思想在生产环境中的变体吗6.3 一些务实的学习建议如果你是想快速入门建议别一上来就看原版论文的数学证明先把 sklearn 或自写的简单Co-training跑通观察每轮日志然后回看论文里的算法流程框图你会有一种“原来它说的就是这个”的感觉。学习路线我建议这样先跑通我这篇的 digits 代码然后换成一个公开文本数据集自己设计一组视图尝试最后再考虑用在你的业务数据上。业务数据如果只有单视图一定要先做视图质量诊断别指望算法本身能创造信息——它只能放大数据里已有的结构。工具链方面从 scikit-learn 起步就够了想让实验更严谨可以再加 mlflow 记录指标如果想在 WSL Ubuntu 或服务器上跑长实验记得写实验脚本而不是在 notebook 里一轮轮手动跑顺手把终端字体调成接近 macOS 的等宽字体长时间看日志眼睛会舒服很多——这是很现实的小建议。就我个人实际操作里的体会来说Co-training 并不是一个“随便跑跑就能涨点”的算法它的收益高度依赖视图质量和调试细节。但也正是因为这个原因它值得你花时间认真玩一遍——这个过程训练的不是调包能力而是对模型置信度、特征信息、伪标签噪声的理解。这种理解换到任何其他半监督算法上都不会白费。