news 2026/10/1 23:10:46

主动学习与半监督学习组合实战:MATLAB实现与查询策略解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
主动学习与半监督学习组合实战:MATLAB实现与查询策略解析

简介:这是一份针对主动学习与半监督学习的MATLAB算法例程包,面向机器学习初学者及需要在少量标注数据场景下建模的研究者。包内以单个.m源文件形式,集中展示了不确定性采样、查询策略以及自训练、协同训练等经典思路的实现过程,并可能包含特征提取、模型交叉验证等配套处理环节,适合结合理论学习逐行调试与改写。压缩包仅含1个源码文件,类型为MATLAB程序,整体大小约9KB,轻量便于快速运行验证。目前已有195人学习下载,适合希望用最小依赖理解两类学习策略核心流程的读者。通过解读这份例程,可直观掌握在无标注数据中挑选有价值样本、利用大量未标注数据提升模型性能的方法,同时积累MATLAB编程与模型调优的实战经验。

1. 标得越多反而越差:主动学习与半监督的组合制胜点

标注成本高、未标注样本堆积如山时,很多人第一反应是把能拿到的标签全塞进模型。但真实项目里,标注预算花完了,测试集准确率反而往下掉。这类翻车不是玄学,根源在于随机采样把大量冗余样本送进了训练集,而那些能真正改变分类边界的样本始终躺在角落里。主动学习(Active Learning)负责把有限的标注预算花在刀刃上,半监督学习(Semi-Supervised Learning)则把剩下的未标注样本通过伪标签和一致性约束利用起来。手里这份“完整的主动学习和半监督学习的多项算法例程.zip”,核心脚本是 fenleng.m,通过它你能直接看到两种策略如何组合、如何迭代、如何把 20% 标注量推向接近全量标注的效果。适合正在做小样本分类实验、竞赛调参,或者想在项目里控制标注预算的工程师。

2. 主动学习查询策略拆解:熵、间隔与多样性的MATLAB取舍

2.1 三种查询策略在数学上到底算什么

主动学习的关键动作不是训练模型,而是“选哪些未标注样本去让人工标注”。选样策略决定每一轮标注预算能不能换来信息增益。最常见的做法是先把当前模型对所有未标注样本的后验概率算出来,再按某个度量排序。熵采样(Entropy Sampling)计算的是每个样本在所有类别上的信息熵,熵值越高,说明模型对这个样本越“说不清”,选它的理由就是“这一条样本最能让模型从混乱走向确定”。最小间隔采样(Margin Sampling)则取最大后验概率与次大后验概率之差,差越小,说明两个类别概率接近,样本刚好压在分类边界附近,这一条对 SVM 这类边界模型特别有效。

多样性采样(Diversity Sampling)思路不太一样,它先对未标注特征空间做聚类,再从每个簇中挑代表性的样本。目的是避免前面两种策略反复选到同一片“难分类区域”,导致预算浪费在局部。实际工程中,我不建议一上来就纯用多样性采样,它在低维可分数据集上表现尚可,但到了高维稀疏特征上,聚类本身就会引入新的噪声。更稳妥的做法是先把熵采样和间隔采样跑若干轮,等模型稳定后再加多样性权重。

2.2 写一个可复用的 active_learn_query.m

对于 MATLAB 环境,主动学习查询逻辑可以独立成函数,这样 fenleng.m 里的迭代主循环只需调用它,不需要每轮重写查询逻辑。下面的代码兼容 fitcecoc 等支持后验概率输出的模型:

function selIdx = active_learn_query(X_u, mdl, budget, strategy) % X_u 未标注特征矩阵,每行一个样本 % mdl 已训练分类器,要求 predict 能输出后验概率 % budget 本轮最多抽取多少个样本去人工标注 % strategy 查询策略: 'entropy' | 'margin' | 'diversity' K = size(X_u, 1); if budget > K budget = K; end switch strategy case 'entropy' [~, score] = predict(mdl, X_u, 'Probability', true); score = max(score, 1e-12); % 截断零概率,防 log(0) H = -sum(score .* log2(score), 2); % 信息熵 [~, ord] = sort(H, 'descend'); selIdx = ord(1:budget); case 'margin' [~, score] = predict(mdl, X_u, 'Probability', true); s = sort(score, 2); margin = s(:, end-1) - s(:, end); [~, ord] = sort(margin, 'ascend'); % 间隔越小越接近边界 selIdx = ord(1:budget); case 'diversity' km = kmeans(X_u, budget, 'MaxIter', 500); selIdx = zeros(budget, 1); for i = 1:budget tmp = find(km == i, 1); selIdx(i) = tmp; end otherwise error('未知策略: %s', strategy); end selIdx = unique(selIdx(selIdx > 0)); end

逻辑上,熵采样和间隔采样都依赖分类器输出的后验概率,区别只是排序方向:熵是降序、间隔是升序。多样性采样则用 kmeans 把未标注样本切成 budget 个簇,每个簇取第一个样本作为代表,这样选出的样本在特征空间里分布得更开。参数方面,budget 不要超过未标注池总量的 10%,超过以后聚类和排序都会变得不稳定;kmeans 的 MaxIter 设 500 是保守值,低维数据一般迭代几十次就收敛,高维数据也不会被迭代上限卡住。

注意:kmeans 函数属于 Statistics and Machine Learning Toolbox,如果你只装了基础 MATLAB,运行到 diversity 分支会报错。

2.3 策略对比与选型建议

策略核心依据优点常见坑
entropy后验熵通用性强,分类器不确定度量化直观高维稀疏特征下熵容易齐平,区分度不够
margin最大与次大后验之差对边界样本敏感,适合 SVM/线性模型模型置信度整体偏高时,间隔普遍偏大
diversity聚类中心代表避免局部密集采样无监督聚类本身不稳定,每轮结果可能漂移
hybrid熵 + 多样性权重兼顾难例与分布覆盖需要额外调 lam 权重,多一个超参数

我一般会把 fenleng.m 里的默认查询策略先记下来,再用 active_learn_query 包一层,这样后续做策略对比时只需要改一个字符串,不用重复复制粘贴主脚本。

3. 半监督学习配合主动学习:伪标签、自训练与低密度分离

3.1 半监督在主动学习循环里的位置

很多人误以为主动学习和半监督学习是两条独立路线,实际项目中它们更像上下游关系。主动学习每次只挑少量高价值样本给人工标注,标注完以后训练集仍然很小。此时半监督学习出场,用当前模型对剩余未标注样本打伪标签,挑置信度最高的那批并入训练集,再重新训练模型。这样一轮下来,人工标注只增加了 budget 条样本,但模型见到的有效样本可能多了几十条。自训练(Self-Training)是最容易实现的一种半监督思路,先把模型在标注集上训练好,再去预测未标注集,然后按阈值筛选高置信度样本加入训练集,循环往复。

协同训练(Co-Training)要求特征能分成两个独立视图,在文本分类里可以拆成内容特征和链接特征,在图像任务里可以拆成颜色特征和纹理特征。fenleng.m 这种单文件例程通常不会实现完整的双视图协同训练,但会保留自训练和低密度分离这类核心逻辑。低密度分离的思想是:让分类边界尽量穿过特征空间里样本稀疏的区域,那些远离边界的、预测置信度高的样本,伪标签可信度才高。

3.2 用自训练实现伪标签增强

半监督增强函数可以单独写成 ssl_self_train.m,它的输入是当前标注集、未标注集和分类器,输出是增强后的训练集。核心代码是伪标签筛选:

function [X_aug, y_aug, mask] = ssl_self_train(X_lab, y_lab, X_u, mdl, t_pl) % t_pl 伪标签置信度阈值,建议从 0.90 起步 [~, score] = predict(mdl, X_u, 'Probability', true); [max_p, max_idx] = max(score, [], 2); mask = max_p >= t_pl; % 只有达到阈值的样本才被打伪标签 X_aug = [X_lab; X_u(mask, :)]; y_aug = [y_lab; max_idx(mask)]; end

参数 t_pl 是整个半监督增强里最敏感的一个。设高了,伪标签数量太少,增强形同虚设;设低了,错误标签混入训练集,模型会在错误方向上越走越远。常见做法是先设 0.9 跑两轮,看增强后的准确率变化;如果准确率还在涨,就逐步降到 0.85、0.8,一旦出现先涨后跌,立即回到上一档阈值。

返回的 mask 变量很重要,外部主脚本要用它把已打伪标签的样本从未标注池中移除,否则下一轮模型会对同一样本反复打伪标签,训练集里出现大量副本,等价于把错误标签的权重放大了好几倍。

3.3 低密度分离的工程直觉

低密度分离在 fenleng.m 里通常不单独做一个函数,而是体现在伪标签阈值上。当分类器对某个样本输出均匀分布或中间概率时,说明这个样本处于类间密度较低的区域,模型本身也不确定。此时不要急于打伪标签,等后面轮次模型越来越自信再回看这些样本。我在实际项目里的习惯是随着迭代轮次增大,让阈值从 0.95 线性退火到 0.75,每一轮伪标签的规模逐渐扩大。这样做的好处是早期避免噪声固化,后期又能充分利用模型收敛后的高置信度输出。

4. 从 fenleng.m 到可跑流水线:文件清单、主循环与评估读法

4.1 先把例程包落成标准目录

拿到压缩包后,第一步不是打开脚本就开始跑,而是建立清晰的工作目录。解压以后的目录结构建议是:

文件/目录作用
fenleng.m原始例程,保留只读
active_learn_query.m上一章实现的查询策略函数
ssl_self_train.m伪标签半监督增强函数
al_ssl_main.m主驱动脚本,串联全部流程

把这个目录放在纯英文路径下,避免中文目录或者带空格的路径。MATLAB 的 addpath 对中文路径支持一直不算好,遇到“未定义函数或变量 fenleng”这类错误,十有八九是路径问题。用addpath(genpath('D:/al_ssl_demo'))一次性把子目录全部加进工作区,是最省事的方式。

4.2 主循环脚本 al_ssl_main.m 全解析

完整的主动学习加半监督循环,可以落成一个独立主脚本。这里我用内置的 fisheriris 数据集做演示,保证任何人拿到都能直接跑通:

clear; clc; rng(2024); load fisheriris; X = meas; lab = grp2idx(species); % 划分干净测试集,测试集不参与训练和未标注池 cv = cvpartition(lab, 'HoldOut', 0.2); X_test = X(cv.test, :); y_test = lab(cv.test); X_pool = X(cv.training, :); y_pool = lab(cv.training); % 标准化,让 SVM 的 RBF 核不至于被量纲带偏 X_pool = zscore(X_pool); X_test = zscore(X_test); % 初始人工标注 30 条,其余全部进未标注池 init_num = 30; X_lab = X_pool(1:init_num, :); y_lab = y_pool(1:init_num); X_u_idx = (init_num+1):size(X_pool, 1); U_pool = X_pool(X_u_idx, :); budget = 8; % 每轮人工标注预算 max_round = 8; % 迭代轮次 t_pl = 0.95; % 伪标签阈值 acc_hist = zeros(max_round, 1); for r = 1:max_round % 训练基分类器,打开概率输出 mdl = fitcecoc(X_lab, y_lab, ... 'Learners', templateSVM('KernelFunction', 'rbf', 'Standardize', true), ... 'Probability', true); % 主动学习选样 selIdx = active_learn_query(U_pool, mdl, budget, 'entropy'); % 模拟人工标注:从原始标签里取对应类别 oracle_y = y_pool(X_u_idx(selIdx)); X_lab = [X_lab; U_pool(selIdx, :)]; y_lab = [y_lab; oracle_y]; U_pool(selIdx, :) = []; X_u_idx(selIdx) = []; % 半监督自训练,伪标签样本也从未标注池移除 [X_lab, y_lab, tmask] = ssl_self_train(X_lab, y_lab, U_pool, mdl, t_pl); U_pool(tmask, :) = []; X_u_idx(tmask) = []; % 在干净测试集上评估 pred = predict(mdl, X_test); acc_hist(r) = sum(pred == y_test) / length(y_test); fprintf('round %d, acc=%.4f, labelled=%d\n', r, acc_hist(r), length(y_lab)); end figure; plot(1:max_round, acc_hist, '-o', 'LineWidth', 1.5); grid on; xlabel('round'); ylabel('accuracy'); title('AL + SSL accuracy curve');

逐段看逻辑。cvpartition 先把数据隔离出 20% 测试集,避免主动学习在选择过程中间接“看见”测试集的信息。标准化必须在划分之后做,否则测试集信息会泄漏到训练集。初始人工标注只有 30 条,对 150 条样本的数据集来说是 20% 标注量,这正是主动学习发挥作用的比例区间。

每轮循环里,训练模型用的是当前标注集,查询函数返回未标注池中的行索引。取 oracle_y 时要用 X_u_idx(selIdx) 映射回原始训练集标签,因为 U_pool 是原始训练池的子集,行号并不对齐。选中的样本移出 U_pool 后,X_u_idx 同步移除,保证下一轮的映射始终正确。

伪标签增强放在主动学习标注之后,这样模型刚获得几轮新标注,置信度更可靠。tmask 接收 ssl_self_train 返回的伪标签样本掩码,同样从未标注池移除。整个过程里,未标注池在持续收缩,训练集在增长,但增长来源被严格区分成人工标注和伪标签两条路子。

4.3 从输出曲线判断实验是否正常

运行结束后,准确率曲线如果前两轮明显上升,中间偶有平台期,后几轮趋平,这是正常形态。真正需要警惕的是曲线出现“过山车式”起伏,准确率第四轮达到 0.95、第五轮又跌回 0.88,这通常说明伪标签阈值太低或者基分类器过拟合。fprintf 输出的 labelled 数量也能给线索,如果 labeled 从 30 直接跳到 120,说明伪标签阈值设得太宽松,大量样本被一次性打标,训练集膨胀过快。

我一般会同时记录人工标注数量与伪标签数量,画成两个堆叠柱状图。人工标注应该线性增长,伪标签数量则呈先快后慢的曲线。如果伪标签数量从某轮开始骤降,说明未标注池里已经没什么高置信度样本了,此时再跑主动学习的意义也不大,可以提前终止。

5. 避坑实录:从迭代崩溃到 zip 伪加密的五条真实踩坑记录

5.1 前两轮涨,第四轮直接掉回基线

  • 现象:主动学习加半监督跑出来的准确率,前三轮分别是 0.82、0.87、0.88,第四轮突然跌到 0.78,后面再也回不去。
  • 原因:伪标签阈值固定 0.9,前三轮模型越来越自信,把大量错误伪标签当成“高置信度”样本放进了训练集。这些错误样本在第四轮开始主导梯度更新,模型被带偏。
  • 解决:把固定阈值改成轮次衰减,比如每轮衰减 0.03,让早期伪标签数量少而精,后期再逐步放开。同时把每轮新增伪标签的上限设为当前人工标注总数的 30%,从数量上限制噪声扩散。

5.2 主动学习选出的样本,有 40% 是上一轮已经标注过的

  • 现象:log 里出现同一个样本的索引被连续两轮选中,人工标注工作量翻倍,但训练集并没有新增信息。
  • 原因:查询函数返回的是未标注池的行索引,但主脚本里 U_pool 的删除顺序和同一轮伪标签样本移除顺序不一致,导致索引错位,某些样本漏删。
  • 解决:强制规定每次从 U_pool 移除样本时,必须同时执行 U_pool(selIdx, :) = [] 和 X_u_idx(selIdx) = [],两个数组保持同步。active_learn_query 内部也可以再加一道 unique,能挡住同一轮内重复选择,但跨轮重复只能靠主脚本的同步删除来解决。

5.3 伪标签不生效,增强前后准确率完全一样

  • 现象:训练集明明多了几十条伪标签样本,测试准确率却纹丝不动。
  • 原因:ssl_self_train 返回的 X_aug 被赋值给了 X_lab,但 U_pool 没有删除对应样本。下一轮主动学习又把这批样本选出来人工标注,人工标签把伪标签覆盖了,等于伪标签白打了。
  • 解决:检查主脚本里是否接收并使用了 mask 变量。伪标签样本必须从 U_pool 中移除,才能让它们以训练样本的身份存在,而不是继续留在未标注池里等待下一轮主动查询。

5.4 zip 包解压时提示需要密码

  • 现象:压缩包下载完整性没问题,但解压时弹出密码输入框,输入空密码依然报错。
  • 原因:这是典型的 zip 伪加密现象。zip 文件头里有一个 general purpose bit flag 被置位,解压软件读到这个标志就认为是加密文件。实际这个包可能根本没有真正加密,只是文件头被改动过。
  • 解决:对于自己下载的、来源可信的包,不要急着找密码移除工具。用 WinRAR 的“修复压缩文件”功能把文件头复位,或者直接在 MATLAB 命令窗口执行unzip('完整的主动学习和半监督学习的多项算法例程.zip', './al_ssl_demo'),MATLAB 的 unzip 函数一般不会理会伪加密标志,可以直接解出 fenleng.m。

5.5 脚本路径带中文,MATLAB 报“未定义函数或变量”

  • 现象:文件明明在名单里,运行 fenleng.m 却报错,命令行显示当前文件夹无法识别脚本。
  • 原因:MATLAB 对中文路径支持不完善,尤其在高版本 R2022b 之后,中文目录下的脚本经常无法被 addpath 正确索引。
  • 解决:把整个例程包解压到纯英文目录,比如 D:/al_ssl_demo,然后用addpath(genpath('D:/al_ssl_demo'))添加路径。这是成本最低的解决方案,比改 MATLAB 编码设置可靠得多。

6. 进阶验证:预算扫描法判断组合是否真的提分

到了这一步,代码能跑、曲线能出,但还有一个核心问题没回答:提升到底是主动学习和半监督共同带来的,还是仅仅因为训练样本总量增加了?要验证这件事,最直接的方法是预算扫描法。

把初始人工标注预算从 5% 到 20% 依次扫描,分别跑完整流程,记录每个预算下的最终准确率。真正的组合策略应该呈现出“预算越低,相对收益越高”的特征。因为主动学习的价值恰恰在标注稀缺时体现,如果 5% 预算下准确率能接近 15% 预算的水平,说明选择策略在起作用;如果 5% 和 20% 的最终准确率几乎没有差距,大概率是伪标签增强一直在背刺主线,把选择策略的优势抵消了。

实际验证时,我会把主循环抽成一个函数,预算作为参数传入。用固定 rng 保证每次实验只在预算维度有差异,其他随机因素完全一致。跑完以后打印预算与准确率的对应关系,如果曲线在 10% 到 15% 之间出现明显拐点,说明半监督增强的阈值还有下调空间;如果曲线从一开始就很平,优先检查伪标签样本是否真正进入了训练集,而不是被后续主动学习覆盖。

从那次以后,我每次拿到这类例程包,先不动默认参数,第一件事就是复制一份脚本做预算扫描。有时候结果会推翻我最初的判断,比如某份代码在 20% 预算下效果很好,但降到 5% 就彻底崩了,那我会直接放弃它,而不是花时间调参。把预算扫描写进自己的标准流程,能少走很多弯路。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 23:10:44

变步长电导增量法MPPT仿真:原理、Simulink搭建与参数整定

光伏系统里的MPPT这个话题,做光伏逆变器、DC-DC变换器或者微电网的人都绕不开。大家普遍说的最大功率点跟踪,本质上就是让光伏组件一直工作在它当前环境条件下的“最佳出力点”上。电导增量法(INC,Incremental Conductance&#x…

作者头像 李华
网站建设 2026/10/1 23:10:18

N叉树层序遍历核心解法:BFS队列快照与递归DFS模拟

1. 题目定位与核心思路拆解这道题是我刷力扣时除了二叉树之外,第一个觉得“有意思”的层序类题目。力扣429的定位很简单,就是让你实现N叉树的层序遍历,输出的不是一维数组,而是二维数组,每一层的结果单独放一个子数组里…

作者头像 李华
网站建设 2026/10/1 23:10:03

Android Studio“Loading Devices”卡住?一文拆解ADB设备排查全流程

搞Android开发的人,八成都在Android Studio里见过那个转圈圈的“Loading Devices”。真机插上了、调试模式也开了,结果设备列表死活不加载,要么一直转,要么干脆空空如也。这个问题的恶心之处在于,它不像编译报错那样有…

作者头像 李华
网站建设 2026/10/1 23:08:03

ROS rostopic pub tab补全与快捷单元测试实战

1. 这不是“命令补全”而是ROS开发者效率的底层基建你有没有在终端里敲到一半rostopic pub /chatter std_msgs/String "data: hello",突然卡住——不确定消息字段名是不是data还是msg?或者刚写完一个发布器节点,想快速验证话题是否…

作者头像 李华
网站建设 2026/10/1 23:04:33

Qwen3.8-27B本地部署实战:MLX Swift量化推理与短思考模式调优

1. 先聊聊这波“短思考”潮流:为什么大家盯着Qwen3.8-27B不放最近圈子里都在传一份Qwen3.8-27B的跑分和实测,标题也够直白——“雷霆思考少,成绩好”。我第一眼看到这个27B规格的时候还愣了一下,毕竟大家手上用得多的还是7B、14B、…

作者头像 李华