简介:面向学习云模型与不确定性评价的MATLAB使用者,这套可直接运行的示例代码聚焦云模型的基本计算与权重应用,适合课程实验、论文复现前的入门验证,以及需要快速搭建演示脚本的读者。压缩包仅1KB,共2个文件,均为.m源码,体量轻便,包含主入口脚本与云变换相关函数,便于在MATLAB中直接调用、阅读和按需修改,省去从公式到代码的重复搭建。已有1581人学习,说明其在云模型入门与教学场景中具有一定参考价值。代码结构简洁,读者可据此理解云滴生成、隶属度计算与云参数变换的基本流程,并在此基础上替换数据、调整参数,用于权重计算或综合评价的小型实验;也适合作为课堂演示与自学练手材料,快速验证思路后再扩展到更复杂的模型实现。
1. 云模型代码能解决什么问题:三个数字特征、一张云图,把专家打分的不确定性摆上台面
评审会上被追问“为什么这个方案的可信度是 0.83 而不是 0.8”,如果只回一句“打分表算出来的”,基本等于没答。云模型给的是另一种回答方式:把一堆专家打分压缩成期望 Ex、熵 En、超熵 He 三个数,再用几千个云滴把“中心在哪、上下浮动多少、这个浮动本身有多不确定”一次画出来。这份 matlab 云模型代码做的就是这件事——正向云发生器负责出图,逆向云发生器负责从数据反推参数,云模型权重模块把打分表变成权重向量,每个函数都能直接调用,不是跑完就扔的演示片段。
它适合两类人:一类是写评价类论文或报告、需要把定性评语量化但又不想用死板隶属函数的;另一类是做方案比选、风险评估,需要给出“权重 + 可信区间”而不是单一数字的。只要手上有 Matlab 基础和一张专家打分表,拆开就能用。
2. 包内结构与方法链:正向云、逆向云、X 条件云和权重模块怎么串
2.1 目录与文件分工:十个 m 文件里哪几个是主力
这种“直接能用”的包,常见做法是一个发生器一个函数文件,文件名和函数名严格一致,主脚本只负责串流程,不写任何公式。解压之后的结构大致是这样,主力其实只有三个:cloud_forward、cloud_backward、cloud_weight,其余都是围绕它们的封装和画图工具。
| 文件 | 作用 | 主要输入 | 主要输出 |
|---|---|---|---|
cloud_forward.m | 正向云发生器 | Ex、En、He、云滴数 N | 云滴横坐标 x、确定度 mu |
cloud_backward.m | 逆向云发生器(无确定度) | 样本向量 x | Ex、En、He |
cloud_backward_c.m | 逆向云发生器(带确定度) | x 与对应 mu | Ex、En、He |
cloud_x_condition.m | X 条件云发生器 | Ex、En、He、给定 x0、采样次数 | N 个确定度 |
cloud_y_condition.m | Y 条件云发生器 | Ex、En、He、给定 mu | 对应的 x 取值 |
cloud_weight.m | 云模型赋权 | m 位专家 × n 个指标的打分矩阵 | 权重向量 w、权重云 |
cloud_eval.m | 云模型综合评价 | 指标矩阵 X、权重 w、等级云 | 隶属矩阵 B、综合得分 S |
plot_cloud.m | 云图绘制与导出 | 数字特征、云滴数 | 图句柄 / 图片文件 |
main_demo.m | 一键演示脚本 | 无 | 图 + 控制台输出 |
把这张表看明白,后面就不容易迷路:做可视化只碰cloud_forward和plot_cloud;已有数据要反推参数只碰cloud_backward;要把打分表变权重只碰cloud_weight;cloud_eval是上层应用,它是前三个的下游。
2.2 三个数字特征:Ex、En、He 各自管什么,边界在哪
云模型看起来玄,说到底就两个矩公式在支撑:E|X-Ex| = En·√(2/π)和Var(X) = En² + He²。第一条把“平均绝对偏差”和熵挂钩,第二条把“样本方差”拆成熵和超熵两部分,逆向云的全部推导都从这两条来。记住它们,看代码时就不用回头翻公式了。
真正容易翻车的是参数取值。下面这张表是我自己的经验区间,不是教科书原话,但按它调基本不会画出怪图:
| 参数 | 物理含义 | 常用取法 | 取错的表现 |
|---|---|---|---|
| Ex | 概念的中心值 | 等级区间的中点,或样本均值 | 云整体飘到坐标轴外 |
| En | 外延宽度,反映模糊度 | 等级评价取 (上限−下限)/6;纯出图取数据范围的 1/6~1/3 | 太小 → 云退化成一条竖线 |
| He | 熵的熵,反映云滴离散程度 | 取 En/10 ~ En/5,经验上不超过 En/3 | 太大 → 散成一团,看不出重心 |
| N | 云滴个数 | 出图 2000~5000,数值计算 500~1000 | 太少 → 图上有明显颗粒感 |
En取区间半宽的 1/3,是因为正态分布 3σ 覆盖 99.7%,把等级区间当成 6σ 来处理,云才不会越界压到相邻等级上。这个约定在评价类应用里几乎是行规,后面第 6 章换参数时还会用到。
2.3 一条典型调用链:从 main_demo 走一遍
演示脚本的逻辑非常线性,读一遍就知道数据是怎么流动的:先固定随机种子,再画三个等级云看形态,接着从打分表反推权重云,最后做一次综合评价。这种“先看图、再算数”的顺序不是随意排的,云图不对劲的时候,后面的权重和得分全是垃圾,先看图能省掉一半调试时间。
%% main_demo.m 主干(节选) clear; clc; rng(20240501, 'twister'); % 固定种子,保证图可复现 %% 1) 三个语言值等级云:差 / 中 / 好 lv = struct('name', {'差','中','好'}, ... 'Ex', {2, 5, 8}, ... 'En', {0.9, 0.9, 0.9}, ... 'He', {0.12, 0.12, 0.12}); figure; hold on; box on; for i = 1:numel(lv) [x, mu] = cloud_forward(lv(i).Ex, lv(i).En, lv(i).He, 3000); plot(x, mu, '.', 'MarkerSize', 2); end xlabel('指标值 x'); ylabel('确定度 \mu'); legend({lv.name}); %% 2) 专家打分表 -> 权重云 -> 归一化权重 load('data/expert_score.mat', 'score'); % m 位专家 × n 个指标 [w, wc] = cloud_weight(score); fprintf('权重:%s\n', num2str(w, '%.4f ')); %% 3) 综合评价:指标矩阵 X,权重 w,等级云 lv [B, S] = cloud_eval(X, w, lv, 500);这里要强调一个容易被忽略的性质:整条链路里唯一带随机性的是正向云和 X 条件云,cloud_backward和cloud_weight是纯确定性计算,同一份打分表进去永远出同一个结果。所以只要发现结果复现不了,问题一定出在randn上,不用去怀疑公式,这个判断能省下大量排查时间。
3. 正向云发生器:把 Ex/En/He 变成几千个云滴的那段循环
3.1 三步循环,逐行拆开看
正向云发生器的定义是“从定性到定量”:给定三个数字特征,生成一批带确定度的云滴。核心就是一个 N 次的三步循环,第一次看会觉得太短,短到怀疑它是不是漏了什么,但云模型区别于普通正态隶属函数的全部秘密就在第一步。
function [x, mu] = cloud_forward(Ex, En, He, N) % 正向云发生器:由数字特征生成 N 个云滴 % Ex 期望(云的重心);En 熵(云的宽度);He 超熵(云的厚度);N 云滴数 if nargin < 4 || isempty(N), N = 3000; end if He < 0, error('He 必须非负'); end x = zeros(N, 1); mu = zeros(N, 1); for k = 1:N En_k = En + He * randn; % ① 以 En 为期望、He 为标准差抽“临时熵” if abs(En_k) < eps, En_k = eps; end % 防除零 x(k) = Ex + abs(En_k) * randn; % ② 以 Ex 为期望、|En_k| 为标准差抽云滴横坐标 mu(k) = exp(-(x(k) - Ex)^2 / (2 * En_k^2)); % ③ 由云滴横坐标反算确定度 end end第 ① 步是整个函数的灵魂:熵本身不是一个固定值,而是服从以En为期望、He为标准差的正态分布。这一步让“概念的宽度”也带上了不确定性,云图才会有厚度。如果把He写成 0,En_k恒等于En,第 ② 步就退化成普通正态抽样,第 ③ 步画出来是一条光滑曲线——那已经不是云了。
第 ② 步里abs(En_k)的绝对值不能省。randn乘负数等价于翻转符号,分布上虽然一样,但显式取绝对值可以避免后面mu计算时出现负数平方之外的心智负担,也让代码在别人眼里“一眼是对的”。
第 ③ 步的确定度公式和正态密度函数同形,但没有1/(√(2π)·σ)那个归一化系数,所以mu落在 (0, 1] 区间内。这一点必须记牢:mu是确定度,不是概率密度,别把它和概率相加,也别拿它去做贝叶斯更新,那是另一个体系的东西。
参数上唯一需要动脑的是N。N只影响图的平滑度和统计量的稳定性,不影响分布形状,2000 以上肉眼就分辨不出颗粒了;但如果后面要用mean(mu)做数值计算,N给到 500~1000 就够,给太大只是拖慢速度。
3.2 云滴数、En 与 He 的组合怎么调才像“云”
真正决定云图形状的不是N,而是He/En这个比值。比值接近 0,云退化成隶属函数曲线;比值到 0.3 左右开始明显发散;超过 0.5 就散成一团,重心都糊了。与其凭感觉试,不如写个小脚本一次扫出来对比,这类“参数敏感性”的脚本我建议每个项目都留一份,比记笔记靠谱。
% 扫一遍 He/En 比值,直观看云怎么从“曲线”变成“散点” ratios = [0.01 0.10 0.30 0.50]; figure('Color','w'); for i = 1:numel(ratios) subplot(2, 2, i); [x, mu] = cloud_forward(5, 1.0, ratios(i) * 1.0, 3000); plot(x, mu, '.', 'MarkerSize', 2, 'Color', [0.15 0.45 0.75]); axis([1 9 0 1]); grid on; box on; title(sprintf('He/En = %.2f', ratios(i))); end跑完这四张图,你对He的直觉基本就建立起来了:它是控制“雾化程度”的旋钮,不是精度参数。很多资料把He说成“熵的熵”,听起来很绕,其实就一句话——它决定了云滴围绕那条理想曲线的散布有多宽。
3.3 出图与保存:坐标轴范围比配色重要
画云图最常犯的错不是配色难看,而是xlim给得太宽。云滴在 ±4En 之外还有零星分布,如果把横轴拉到 ±10En,几千个点里绝大多数挤在中间一小块,重心彻底糊掉,图就废了。合理的做法是固定 ±4En,超出部分自然截断,读者看到的是一个饱满的云团。
function fig = plot_cloud(Ex, En, He, N, fname) % 统一样式的云图绘制,fname 为空则不落盘 if nargin < 4 || isempty(N), N = 3000; end [x, mu] = cloud_forward(Ex, En, He, N); fig = figure('Color', 'w', 'Position', [100 100 560 400]); plot(x, mu, '.', 'Color', [0.15 0.45 0.75], 'MarkerSize', 2); xlabel('指标值 x'); ylabel('确定度 \mu'); xlim([Ex - 4*En, Ex + 4*En]); % 固定 4 倍熵,保证多张图能横向对比 ylim([0 1]); grid on; box on; if nargin == 5 && ~isempty(fname) exportgraphics(fig, fname, 'Resolution', 300); % 比 saveas 清晰,推荐 end end多个等级云画在同一张图上时,xlim必须手动统一,不能靠axis auto。原因是每个等级云的Ex不同,自动缩放会让三朵云的尺度不一致,看图的人会误判重叠程度,而这个重叠程度恰恰决定了后面评价结果会不会“全都落在同一等级”。
4. 逆向云与云模型权重:从一张专家打分表到归一化权重向量
4.1 逆向云发生器:不带确定度和带确定度两版
逆向云是“从定量到定性”:手里有一批样本,反推 Ex、En、He。它有两套实现,取决于你手上有没有确定度。绝大多数场景只有打分值、没有确定度,用第一版就够,代码只有五行,但每行都对应一个矩估计。
function [Ex, En, He] = cloud_backward(x) % 逆向云发生器(无需确定度):由样本估计三个数字特征 x = x(:); n = numel(x); if n < 3, error('样本太少,至少 3 个观测'); end Ex = mean(x); % 一阶原点矩 absd = mean(abs(x - Ex)); % 一阶绝对中心矩 En = sqrt(pi/2) * absd; % 由 E|X-Ex| = En*sqrt(2/pi) 反解 He = sqrt(max(var(x) - En^2, 0)); % 由 Var = En^2 + He^2 反解,max 防负 endmax(..., 0)这个保护一定要留。样本量少或者数据过于集中时,var(x)可能小于En²,开方直接出复数,后面所有计算全部污染。加个max至少能让程序跑下去,但出现这种情况本身就是信号:说明样本量不够,正常应该补到 20 个观测以上再算。
第二版带确定度,思路不一样:对每个云滴(x_i, mu_i)单独反解出它的En_i,再统计这些En_i的均值和标准差。它比第一版精度高,但对异常点极其敏感——一个mu接近 1 的点会让log(mu)接近 0,En_i直接发散。
function [Ex, En, He] = cloud_backward_c(x, mu) % 逆云发生器(带确定度):逐云滴反解 En_i,再做统计 x = x(:); mu = mu(:); mu = min(max(mu, 1e-6), 1 - 1e-6); % 截断,防 log(0) 与 log(1)=0 Ex = mean(x); % 期望仍用样本均值 En_i = abs(x - Ex) ./ sqrt(-2 * log(mu)); % 由 mu = exp(-(x-Ex)^2/(2En^2)) 反解 En_i = En_i(isfinite(En_i)); % 剔除 inf / NaN 点 if numel(En_i) < 3, error('有效点不足,检查 mu 是否过于集中在 1 附近'); end En = mean(En_i); He = std(En_i, 1); % 用总体标准差,除以 n 而不是 n-1 endHe这里用std(..., 1)而不是默认的0,是个小分歧点。云模型的定义里He是En_i的总体标准差,样本量本来就被N撑得很大,用总体标准差更贴合定义;如果你的数据只有几十个点,用样本标准差也说得过去,但要全篇统一,别两个地方两种写法。
4.2 云模型赋权:打分表 → 权重云 → 归一化权重
这部分是很多人搜“matlab 云模型 权重”真正想找的东西。云模型赋权的逻辑并不复杂:每位专家对每个指标打一个分,同一指标的多个打分构成一个样本,逆向云得到该指标的(Ex, En, He),再用Ex归一化就是权重。
function [w, cloud] = cloud_weight(score) % 云模型赋权:score 为 m 位专家 × n 个指标的打分矩阵 [m, n] = size(score); if m < 3, error('专家人数太少,至少 3 位'); end if any(score(:) <= 0) error('打分须为正数;量纲不一致时先做极差归一化'); end Ex = zeros(1, n); En = Ex; He = Ex; for j = 1:n [Ex(j), En(j), He(j)] = cloud_backward(score(:, j)); end w = Ex / sum(Ex); % 期望归一化即权重 cloud.Ex = Ex; cloud.En = En; cloud.He = He; cloud.w = w; end为什么用Ex归一化而不是别的?因为Ex是这批打分的“共识中心”,它天然反映了指标被认可的程度;En和He没有进入权重公式,它们是可信度标签——He大的指标说明专家意见分歧严重,这个权重虽然算出来了,但它的说服力要打折。我的习惯是在报告里把He/En作为一个“分歧度”列出来,超过 0.3 的指标单独说明,比强行加权更诚实。
归一化前那句any(score(:) <= 0)的检查不是多余的。打分表里混进 0 或者负值(有些问卷用 −1/0/1 编码),Ex求出来可能是负数或零,sum(Ex)一旦为 0 直接除出NaN,排查起来非常费劲,不如在入口就拦掉。
4.3 权重合理性检查:区分度、异常列与灵敏度
权重算出来不要急着往报告里贴,至少过三道检查。第一道是硬检查:sum(w)是否等于 1(浮点误差允许 1e-12),有没有NaN。第二道是区分度:max(w)/min(w)太小说明所有指标权重几乎一样,后面的综合评价等于没加权,这种情况要么是指标设计有问题,要么是专家打分过于中庸。
第三道是灵敏度,这个最容易被跳过,但它能暴露很多隐性分歧。做法很简单:把某个指标的打分整体抬高 1 分,看权重变化多少。变化越剧烈,说明该指标的专家意见越分裂。
% 权重灵敏度扫描:逐个指标整体抬分,看权重被拉动多少 w0 = cloud_weight(score); n = size(score, 2); delta = zeros(1, n); for j = 1:n s = score; s(:, j) = s(:, j) + 1; % 只动第 j 个指标 w1 = cloud_weight(s); delta(j) = max(abs(w1 - w0)); % 权重向量的最大偏移量 end [~, idx] = sort(delta, 'descend'); fprintf('最敏感指标:第 %d 个,偏移 %.4f\n', idx(1), delta(idx(1)));如果某个指标的delta远大于其他指标,同时它的He也偏大,那基本可以断定:这个指标的评分标准在专家之间没有统一。与其硬着头皮往下算,不如回头把评分细则重新对齐一遍,这比在模型里加任何修正都有效。
5. 避坑与排查:云图画成一条竖线、He 算出负数的 5 个真实原因
5.1 云图形状不对:一条竖线、一团糊、一片空白
现象一:云图画出来只有一条竖线或一个尖峰,完全不像“云”。原因通常是En给得太小,和Ex的量纲差了一到两个数量级——比如打分量程是 1~5,En却填了 0.05,所有云滴挤在中心附近,横轴上就是一根线。另一种可能是xlim手动卡在了Ex附近,云滴被裁掉了。解决办法是先打印[min(x), max(x)]看看云滴实际分布范围,再按Ex ± 4En设轴;En至少给到数据范围的 1/6,出图才像样。
现象二:云糊成一团,看不出重心在哪里。这是He太大的典型症状,He/En超过 0.5 之后云滴的散布已经盖过了熵本身的宽度,视觉上就是一个矩形散点区。按He = En/10起步,逐步加到En/5,一般就能得到既有厚度又有重心的图。还有一种隐蔽情况:En和He单位没错,但N给了 20000,点密度太高反而糊成一块色块,这种情况把MarkerSize调到 1 或降到 3000 个点即可。
5.2 逆向云与权重算崩:负的 He、NaN 权重
现象三:He = sqrt(var(x) - En^2)报错,或者结果是个复数。根因是var(x) < En²,数学上意味着样本的离散程度还不足以支撑“超熵”这个自由度。最常见于样本量不到 10 的情况,此时En用一阶绝对矩估计已经很不稳,var更是被单个极端值主导。处理方式有两层:代码上必须加max(..., 0)兜底,工程上把样本补到 20 个以上,如果条件不允许,就干脆把He固定成一个经验值并在报告里注明,别让程序自己编。
现象四:权重向量全是NaN,或者总和为 0。按出现频率排,原因主要有三种:打分表里混进了 0 或负数(问卷编码不同导致的,很常见);某一列打分完全相同导致En = 0(这个不影响Ex,但说明该指标没有区分度);各指标量纲不一致,有的用 1~5 分、有的用 0~100 分,Ex差两个数量级,归一化后小量纲指标的权重几乎为 0。逐列打印[min, max, mean]三列,问题一眼就能看出来,量纲先统一到同一区间再算。
5.3 复现不了:同一份代码两台机器两张图
现象五:同一份脚本,同事跑出来和你的图不一样;或者论文里的图过两周自己都复现不了。原因很直接——没有固定随机种子。老代码里常见的randn('seed', 0)这种写法在新版本里的行为和非预期的一致,应当统一换成rng(20240501, 'twister'),并且写在脚本最前面,不是函数里。同时把N也写死成常量,不要用“大约 2000”这种说法。这两件事做完,图就能逐像素复现,审稿或交接时省掉无数口舌。
顺带说一个性能问题:如果发现出图要等十几秒,多半是for循环里逐点调用导致的,可以把三个randn改成一次性生成N×1向量,循环整个去掉,3000 个云滴的计算时间会从秒级掉到毫秒级。这个优化在单次出图时感觉不明显,但做参数扫描或者蒙特卡洛重复实验时差别巨大。
提示:排查顺序永远是“先看云图形态,再看数字特征,最后看权重和得分”。云图是对的,后面一般不会错;云图不对,后面全是垃圾。
6. 二次开发:把云模型接进自己的评价流程,怎么改参数、怎么验结果
拿到这个包之后,真正要改的地方其实只有三处,其余照抄即可。第一处是等级云参数。演示里En = 0.9是拍出来的,正式项目应该由评语区间推出来:评语“好”对应 8~10 分,那En = (10-8)/6 ≈ 0.33,He取En/10。这样等级云之间的重叠区宽度是有依据的,不会出现“所有样本都落在同一等级”的尴尬。
% 按评语区间自动生成等级云,避免手填 En 拍脑袋 edges = [0 2; 2 4; 4 6; 6 8; 8 10]; % 五级评语的分值区间 lv = struct('name', {}, 'Ex', {}, 'En', {}, 'He', {}); for i = 1:size(edges, 1) lo = edges(i, 1); hi = edges(i, 2); lv(i).name = sprintf('L%d', i); lv(i).Ex = (lo + hi) / 2; % 区间中点作期望 lv(i).En = (hi - lo) / 6; % 半宽的三分之一,等价于 6σ 覆盖区间 lv(i).He = lv(i).En / 10; % 厚度取宽度的十分之一 end第二处是权重模块。cloud_weight只输出纯云权重,很多评审场合更认“主客观组合赋权”,那就把它和层次分析法的结果按系数混合,w = 0.6*w_cloud + 0.4*w_ahp,系数怎么定写进方法说明里。第三处是cloud_eval里的mean(mu),这个均值会被少数极端云滴拉偏,改成中位数或者去掉两端 5% 的截尾均值,结果会稳不少,尤其是He偏大的指标。
| 改造点 | 动哪个文件 | 风险 |
|---|---|---|
| 等级云由区间自动生成 | main_demo.m/ 配置脚本 | 区间边界重叠时需手动微调 |
| 云权重与 AHP 组合 | cloud_weight.m | 组合系数缺乏依据会被质疑 |
| 确定度统计量换成截尾均值 | cloud_eval.m | 等级区分度可能变钝 |
| 加批量重复实验 | 新建run_repeat.m | 运行时间成倍增加 |
改完之后必须验。最省事的验证方法是重复实验:固定输入,跑 100 次,看综合得分S的标准差有没有超过等级间隔的三分之一。超了,说明He给大了或者N给小了,结果不稳定,报告里不敢用。第二个验证方法是对照解析解:把He设成 0,云退化成正态隶属函数,此时任一x的确定度可以手算,拿它和cloud_x_condition的均值比对,误差应该在 1e-3 量级,这条检查能一次性验证公式实现有没有写错。
我自己踩过最疼的一次,是把一套调好的参数交给别人复现,对方跑出来的图和我的完全对不上,查了半天才发现他本地的旧脚本里有一行rand('seed', 0)残留。从那以后我每次交付带随机性的代码,都会把rng写死在脚本第一行,并且把N、He这些关键参数集中在一个配置块里,谁改了什么一目了然。希望帮到你。
本文还有配套的精品资源,点击获取