
简介面向多准则决策分析与评价建模的学习者和研究者这份开源 MATLAB 代码包实现贝叶斯最佳-最差方法Bayesian BWM可用于在成对比较中估计准则权重并给出后验推断。包内共 8 个文件以 6 个 .m 脚本为主另含 1 个 readme.md 与 1 个示例数据 txt压缩后约 15KB脚本涉及主算法、JAGS 调用接口、MCMC 链重构与收敛诊断、结果绘图以及示例运行入口结构紧凑便于按流程阅读和改造。使用前需在本机安装 JAGS 并配置环境变量重启 MATLAB 后即可在 runMe.m 中替换准则名称、最优-最差比较数据等变量开展实验。已有 2174 人学习适合希望把贝叶斯 BWM 用于权重测算、方案排序或论文复现的读者参考也能帮助理解先验设定、后验采样与收敛判断之间的衔接。1. 从一次评审会上的追问说起去年帮一个做设备选型的团队看他们的评分模型会上有人问了一句这套权重如果换个专家重新打分会不会整个排序都翻过来负责模型的同事当场把最优准则的偏好值从 8 改成 7一跑原本排第二的方案跳到了第一。会议室安静了两秒。这件事之后我开始认真看贝叶斯 BWM 这类实现——传统 BWMBest-Worst Method给的是权重的点估计而贝叶斯版本把专家打分当带噪观测输出的是每个权重的后验分布。这份 MATLAB 资源BayesianBWM走的就是这条路用 BWM 的最优/最差两两比较降低提问量再用 MCMC 把不确定性显式采样出来。适合两类人手上有多准则评价场景、被问过权重稳不稳的算法同学以及想把 BWM 从 Excel 搬进脚本、又不想只拿一个干巴巴数字的工程开发者。2. 项目定位与技术选型BWM 为什么值得用贝叶斯重写一遍2.1 BWM 的输入是一对向量不是一张矩阵AHP 那套要专家填完整的判断矩阵n 个准则就是 n(n-1)/2 次两两比较准则一多专家就开始乱填。BWM 的思路是把提问量压到 2n-3 次先让专家指认哪个准则最重要、哪个最不重要然后只问两件事——最优准则相对其余每个准则重要多少其余每个准则相对最差准则重要多少。得到两个向量之后权重就由一组带约束的优化问题解出来。落到数据上就是三个东西准则名列表、最优准则下标、最差准则下标加上两条长度为 n 的整数向量。下面的结构是这类项目最常见的输入约定看代码前先把它记住变量维度含义取值范围criteria1×n cell准则名称只用于结果展示任意字符串idxBest标量最优准则在列表中的位置1..nidxWorst标量最差准则在列表中的位置1..naB1×n最优准则相对各准则的偏好1..9 整数自身位置为 1aW1×n各准则相对最差准则的偏好1..9 整数自身位置为 1偏好刻度沿用 Saaty 的 1-9 语义1 表示同等重要9 表示极端重要。这两条向量里有两个位置是占位的aB(idxBest)和aW(idxWorst)按定义恒为 1不携带信息建模时要排除否则会把 1 当成一次真实观测喂进似然里权重会被无端拉平。这是后面代码里第一个容易看漏的细节。2.2 点估计的软肋恰好是贝叶斯的长处传统 BWM 的目标函数是最小化最大绝对偏差形式上是 min max_j { |w_B/w_j − a_Bj|, |w_j/w_W − a_jW| }线性化之后用一个辅助变量 ξ 把它转成线性规划解出来的 ξ* 还能当一致性指标用。这套做法成熟、快、可解释问题在于它只回答给定这组打分权重是多少不回答这组打分本身有多可信。专家打分天然带噪。同一个专家隔一周再填8 可能变成 7换个专家最优准则的指认都可能不同。点估计把这些波动全部吸收进一个数字里你看不出哪个权重的估计是稳的、哪个是勉强凑出来的。贝叶斯化的做法是把观测写成 w_B/w_j 的带噪版本给权重配一个单纯形上的先验跑 MCMC 得到后验。这样每个权重是一个分布可以算 95% 可信区间可以算方案 A 优于方案 B 的后验概率评审会上被追问时拿得出东西。代价也很明确从解一个线性规划变成跑几千次迭代的采样慢而且要处理收敛诊断。所以这类项目一般不会把贝叶斯版当成唯一入口常见做法是保留传统 BWM 作为快速粗筛和一致性前置检查贝叶斯版负责在关键决策上给出不确定性刻画。2.3 MATLAB 配 JAGS是这条路线上最省事的组合采样器可以自己写 Metropolis-Hastings但在这个场景里没必要。权重要满足非负且和为 1落在单纯形上Dirichlet 分布正好是它的共轭先验JAGS 直接支持ddirch再配一个观测模型模型文件十来行就能写完剩下的交给 Gibbs 采样。MATLAB 侧用 matjags 这个接口把数据推过去、把链拉回来整个流程不需要离开 MATLAB 生态。选型上有几个现实考虑。一是团队如果本来就在 MATLAB 里做评价和画图加一个 JAGS 比引入 Python 或 R 的整条工具链轻。二是 JAGS 的模型文件是纯文本审稿或者交接时别人能直接读比一堆采样器代码好解释。三是 MATLAB 的矩阵操作在处理多专家、多方案的后验汇总时确实顺手quantile、prctile、排序这些一行就够。提示matjags 是第三方接口不是 MathWorks 官方工具箱。它依赖本机已安装的 JAGS 可执行程序装完 JAGS 之后要把它的 bin 目录加进系统 PATH否则 MATLAB 里调用会直接报找不到可执行文件。至于为什么用 JAGS 而不是 Stan实际差别没那么大两者都能写这个模型。JAGS 的语法更接近 BUGS 的老写法模型短调试时打印中间节点方便Stan 的 HMC 在高维下效率更好但这个模型维度低n 一般不超过十几个采样效率不是瓶颈。我一般按团队已有环境选别为了一个十几维的模型去折腾新工具链。3. 目录结构与数据流从准则打分到后验权重3.1 仓库拆开之后是四层这类项目的目录不会太深但分层要清楚否则改一个观测分布要翻三个文件。下面是我按这类资源常见组织方式整理的结构实际拿到手可能文件名略有出入但职责划分基本一致BayesianBWM/ ├── data/ │ ├── criteria.csv # 准则名称一行一个 │ └── comparisons.csv # 最优/最差下标与两条偏好向量 ├── model/ │ └── bwm.jags # JAGS 模型定义观测分布和先验都在这里 ├── src/ │ ├── run_bwm.m # 主入口串起整个流程 │ ├── check_consistency.m # 传统 BWM 一致性比率粗检 │ ├── prep_jags_data.m # 把表格转成 JAGS 能吃的 list │ └── summarize_posterior.m # 后验汇总、排序、出图 ├── results/ # 采样结果与图表落盘位置 └── README.md四个目录对应四个职责data存原始输入model存统计模型src存流程代码results存输出。把模型单独抽成一个.jags文件而不是硬编码在 MATLAB 字符串里是这类项目里我认为最值得保留的习惯——换似然、换先验只动这一个文件MATLAB 侧完全不用改。src里四个脚本的依赖关系是单向的run_bwm.m调用其余三个反过来不行。这意味着你可以单独拿check_consistency.m去做批量打分筛查不用启动采样器这在数据量大的时候很实用。3.2 输入文件的两个约定criteria.csv就是一行一个准则名读进来转成 cell 数组。comparisons.csv稍微讲究一点它要同时承载下标和两条向量。下面是一种能用的写法用两行分别放aB和aW前面用一列标记类型type,idxBest,idxWorst,c1,c2,c3,c4,c5 aB,3,5,4,3,1,2,7 aW,3,5,2,3,9,5,1第一行说明最优准则是第 3 个、最差是第 5 个aB行里第 3 个位置是 1占位表示准则 3 相对自己aW行里第 5 个位置是 1。读的时候要做的校验至少三条两条向量长度等于准则数、aB(idxBest)与aW(idxWorst)都为 1、所有值落在 1 到 9 的整数区间内。校验这一步看着琐碎但它是后面所有玄学问题的分水岭。我见过最典型的一次翻车是aB和aW放反了程序照样跑完、照样出图权重排序全错直到有人拿手算结果对了一遍才发现。所以读入之后立刻断言别等到采样完再看。3.3 一次运行里数据经过了什么把主脚本从头读到尾数据流是这样的从data/读入准则与比较向量做合法性断言调check_consistency.m跑传统 BWM 线性规划拿到 ξ* 和一致性比率比率超阈值就打警告并中止否则继续prep_jags_data.m剔除两个占位位置把向量、下标、样本量打包成 struct调 matjags 读model/bwm.jags传入数据和初值跑若干条链summarize_posterior.m把链合并、算后验均值与分位数、按均值排序出图落盘。这个顺序有个隐含前提一致性检查在前采样在后。逻辑上说得通——打分本身自相矛盾的话后验也只是把矛盾分布化了没有意义。但第 2 步用的是线性规划第 5 步用的是 MCMC两者对同一组数据的一致性判断未必完全同步后面排查章会专门讲这个差异怎么处理。4. 核心模块拆解BWM 约束、贝叶斯模型与 MCMC 采样链路4.1 一致性前置检查先确认打分不是自相矛盾的传统 BWM 的线性规划形式是最小化 ξ约束为 |w_B − a_Bj·w_j| ≤ ξ·a_Bj 和 |w_j − a_jW·w_W| ≤ ξ·a_jW加上权重非负、和为 1。MATLAB 里用linprog直接写。下面这段是这类脚本的骨架function [w, xi, CR] check_consistency(aB, aW, idxBest, idxWorst) % 传统 BWM 线性规划求解返回权重、最小最大偏差和一致性比率 n numel(aB); % 决策变量顺序[w1..wn, xi]共 n1 个 f [zeros(1, n), 1]; % 不等式约束 A*x b逐条展开绝对值 A []; b []; for j 1:n if j idxBest, continue; end % 占位位置不产生约束 row zeros(1, n 1); row(idxBest) 1; row(j) -aB(j); row(end) -aB(j); % 移项后 xi 的系数 A [A; row]; b [b; 0]; row zeros(1, n 1); row(j) 1; row(idxWorst) -aW(j); row(end) -aW(j); A [A; row]; b [b; 0]; end % 等式约束权重和为 1 Aeq [ones(1, n), 0]; beq 1; lb [zeros(1, n), 0]; ub []; opts optimoptions(linprog, Display, off); [w, xi] linprog(f, A, b, Aeq, beq, lb, ub, opts); % 一致性比率xi* 除以同规模下的最大可能 xi CI consistency_index(n); CR xi / CI; end约束的展开是关键。原始式子是 |w_B − a_Bj·w_j| ≤ ξ·a_Bj拆成两条线性不等式之后把 ξ 当成第 n1 个决策变量放进系数矩阵符号要跟着移项一起翻。第 5 行的row(end) -aB(j)就是那个容易写错的位置如果漏了负号linprog会给出一个 ξ 为负的解看起来收敛了实际毫无意义。参数上aB和aW是两条偏好向量idxBest、idxWorst用来跳过占位位置。返回值CR一般跟 0.1 到 0.2 之间的阈值比超过就提示专家重新打分。consistency_index(n)是按准则数量查表得到的最大偏差参考值不同文献给的表略有出入工程上按项目约定固定一张表就行别每次换。4.2 组装 JAGS 数据占位位置必须剔除JAGS 侧只认它需要的观测所以 MATLAB 到模型的这一步是减法把占位的位置去掉只留下真实比较。同时要把最优、最差准则的下标单独传过去因为模型里的比值是围绕这两个位置组织的。function jdata prep_jags_data(aB, aW, idxBest, idxWorst) % 剔除 aB(idxBest) 与 aW(idxWorst) 两个占位观测 n numel(aB); % 最优准则向量的真实观测位置 maskB true(1, n); maskB(idxBest) false; % 最差准则向量的真实观测位置 maskW true(1, n); maskW(idxWorst) false; jdata.n n; jdata.B idxBest; jdata.W idxWorst; jdata.aB aB(maskB); % 长度 n-1 jdata.aW aW(maskW); % 长度 n-1 jdata.idxB find(maskB); % 记录对应的是哪个准则 jdata.idxW find(maskW); end为什么一定要保存idxB和idxW因为剔除之后aB的第 k 个元素对应的已经不是准则 k而是idxB(k)。模型里要用这个映射把观测挂到正确的权重上。丢了映射观测会被错配到相邻准则结果看起来差不多对实际全错位。aB和aW剔除后长度都是 n−1这也解释了为什么 BWM 的提问量是 2n−3两条向量合计 2n 个位置减去两个占位就是 2n−2再减去最优准则相对最差准则这个被两条向量重复覆盖的偏好剩下 2n−3 次独立比较。4.3 JAGS 模型逐段读先验、似然、导出量模型文件是整个项目的核心十行左右但每一行都有讲究。下面这版用的是对数正态观测把aB(j)看作w_B/w_j的带噪版本model { # 1) 权重先验单纯形上的均匀 Dirichlet for (i in 1:n) { alpha[i] - 1 } w[1:n] ~ ddirch(alpha[]) # 2) 最优准则的观测log(aB) ~ N(log(w_B/w_j), tauB) for (k in 1:(n-1)) { j - idxB[k] log(muB[k]) - log(w[B]) - log(w[j]) log(aB[k]) ~ dnorm(muB[k], tauB) } # 3) 最差准则的观测log(aW) ~ N(log(w_j/w_W), tauW) for (k in 1:(n-1)) { j - idxW[k] log(muW[k]) - log(w[j]) - log(w[W]) log(aW[k]) ~ dnorm(muW[k], tauW) } # 4) 噪声精度先验弱信息 tauB ~ dgamma(0.01, 0.01) tauW ~ dgamma(0.01, 0.01) }第 1 段是权重先验。ddirch要求参数向量每个分量大于 0全取 1 就是单纯形上的均匀分布等价于没有任何准则被预先认为更重要。如果你有历史数据支撑某个准则一贯更重要把这组 alpha 改成非均匀的模型会自动往那个方向偏这是二次开发里最常用的一个旋钮。第 2、3 段是似然。为什么取对数因为aB是 1 到 9 的正整数而w_B/w_j也是正数直接在原尺度上建正态模型方差会随均值一起变大残差结构不对。取对数之后比值变成差值尺度统一正态假设更站得住。muB[k]用的是-而不是因为它是确定性节点不参与采样。第 4 段的tauB、tauW是精度参数方差的倒数用 Gamma 弱信息先验让数据自己决定噪声大小。这里有个坑dgamma(0.01, 0.01)这种极小参数在低维模型里通常没问题但如果观测数很少比如 n4只有 3 个观测后验可能贴着 0 跑采样器会抖动。后面排查章会讲怎么判断。注意JAGS 里dnorm的第二个参数是精度不是标准差。把标准差填进去是新手最常见的错误症状是后验窄得离谱、所有链几乎重合看着收敛很好其实是先验把数据压死了。4.4 MATLAB 侧采样与后验汇总模型写完之后MATLAB 这边就是调接口、设参数、收结果。下面这段是主流程里的采样和汇总部分% 采样参数4 条链每条 20000 次迭代前 5000 次预热丢弃 nChains 4; nBurnin 5000; nSamples 20000; % 初值每条链给一个不同的随机 Dirichlet 起点帮助诊断收敛 init cell(1, nChains); for c 1:nChains init{c}.w drchrnd(ones(1, jdata.n), 1); % 和为 1 的随机向量 init{c}.tauB 1; init{c}.tauW 1; end monitors {w, tauB, tauW}; samples matjags(jdata, fullfile(model, bwm.jags), init, ... nchains, nChains, nburnin, nBurnin, nsamples, nSamples, ... monitorparams, monitors, thin, 1, dic, true); % 合并链算后验均值与 95% 可信区间 W samples.w; % 维度 [迭代数, 准则数, 链数] W reshape(W, [], jdata.n); % 合并所有链 wMean mean(W, 1); wCI prctile(W, [2.5 97.5], 1); % 按后验均值排序输出带区间的表 [~, order] sort(wMean, descend); fprintf(%-10s %10s %20s\n, 准则, 后验均值, 95% 可信区间); for i order fprintf(%-10s %10.4f [%.4f, %.4f]\n, ... criteria{i}, wMean(i), wCI(1,i), wCI(2,i)); end初值那一段值得单独说。ddirch采样出来的向量天然和为 1但如果你给初值一个和为 2 的向量JAGS 会在第一次迭代时报节点 w 的初始值无效。所以初始化必须走 Dirichlet 采样或者手工归一化drchrnd是常见的一个小工具函数没有的话用-log(rand(...))归一化也能替代。monitors里我只留了w和两个精度参数。刚上手时容易把所有中间节点都加进去结果内存爆掉、读回来一堆用不上的东西。建议默认只监控最终关心的量需要排查再加。thin设为 1 是因为迭代数不算大保留全部样本便于算分位数。如果 nSamples 加到十万级thin设 5 到 10 能省内存对后验估计几乎没影响。prctile那行算的是可信区间注意它和置信区间的语义不同这里说的是权重落在这个区间的后验概率是 95%不是频率派的重复抽样含义。汇报的时候措辞要准确否则容易被懂统计的人挑刺。5. 跑通与常见问题排查环境、参数与五类翻车现场5.1 环境准备与最小可运行闭环跑起来需要三样东西MATLAB带 Optimization Toolboxlinprog要用、JAGS 可执行程序、matjags 接口。前两个是外部依赖第三个是一个 .m 文件集合放进路径即可。下面这段是环境自检我一般存成一个env_check.m换机器先跑它function ok env_check() % 检查三项依赖是否就位返回是否可继续 ok true; % 1) Optimization Toolbox 是否可用 if isempty(which(linprog)) warning(缺少 linprog传统 BWM 检查无法运行); ok false; end % 2) matjags 是否在路径上 if isempty(which(matjags)) warning(matjags 不在路径上检查 src 目录是否 addpath); ok false; end % 3) JAGS 可执行文件是否可见靠系统调用试探 [status, ~] system(jags --version); if status ~ 0 warning(系统找不到 jags 命令检查 PATH); ok false; end end三项检查对应三类失败。第一项挂了说明 MATLAB 装的是精简版第二项挂了是路径问题which直接告诉你有没有第三项最隐蔽因为system走的是操作系统 shellPATH 配的是系统级环境变量而不是 MATLAB 的path两者不是一回事。Windows 上装完 JAGS 一般要重启一次 MATLAB 才能读到新的 PATH这一点很多说明里不写但确实会卡人。自检通过之后最小闭环是把data/里的样例数据喂给run_bwm.m看它能否不报错地吐出results/下的后验表和图。第一次跑不要改任何参数先确认默认路径能通再动数据。5.2 采样参数怎么定别拍脑袋nChains、nBurnin、nSamples三个参数没有放之四海皆准的值但有一组经验起点链数 4预热不少于 5000有效样本至少几千。链数少于 3 就没法算 R-hat等于放弃了收敛诊断不建议。预热不够的典型症状是某条链从完全不同的位置出发、还没走到后验主体就停了合并之后分布出现双峰。判断收敛我一般看两个量R-hat 和有效样本量。R-hat 接近 1经验阈值 1.1 以下说明链之间一致有效样本量太小说明自相关严重需要加迭代或者加 thin。这两个量都可以从samples结构里手算也可以装个诊断小函数。下面是一个够用的 R-hat 计算片段function rhat calc_rhat(X) % X 维度[迭代数, 参数数, 链数] [m, p, c] size(X); chainMean squeeze(mean(X, 1)); % [p, c] chainVar squeeze(var(X, 0, 1)); % [p, c] W mean(chainVar, 2); % 链内方差 B m * var(chainMean, 0, 2); % 链间方差 varPlus (m - 1) / m * W B / m; % 后验方差估计 rhat sqrt(varPlus ./ W); % [p, 1] endW是链内方差的平均B是链均值的方差乘上迭代数varPlus把两者加权成总体方差估计。R-hat 大于 1.1 就该回头查模型别硬着头皮解读结果。5.3 五类高频翻车现场下面这几条是我在类似实现上实际遇到或者见别人踩过的按现象 → 原因 → 解决写碰上了直接对号入座。第一类启动就报找不到 jags 命令。现象是 MATLAB 里调matjags直接抛错提示无法执行外部程序。原因基本是两个JAGS 没装或者装了但 bin 目录没进系统 PATH。解决是先单独在终端里跑jags --version确认命令行能用能用之后重启 MATLAB让它重新读环境变量。如果终端能用、MATLAB 还是不行检查是不是在 IDE 里启动的 MATLAB某些启动方式会继承旧的环境副本。第二类初始值无效。现象是采样器在第一轮就报某个节点的初始值不在支撑集内。原因几乎都是权重初值不满足单纯形约束比如手工写了[0.2 0.2 0.2 0.2 0.2]但 n 是 6和不为 1或者写了负数。解决是统一用 Dirichlet 采样生成初值别手写常量。另外tauB、tauW的初值必须为正Gamma 先验的支撑集是正半轴。第三类后验窄得不正常。现象是所有准则的 95% 可信区间都窄到小数点后三位看起来结论非常确定。原因通常是观测模型里的精度参数被填成了标准差方差被系统性低估也可能是tauB的先验设得太强比如写成了dgamma(100, 1)。解决是回头确认dnorm第二个参数确实是精度并把 Gamma 先验放弱让数据主导。第四类传统 CR 通过贝叶斯排序却翻了。现象是前置一致性检查显示 0.08、远低于阈值但贝叶斯后验给出的排序和linprog的点估计不一致。原因是两套方法优化的目标不同线性规划最小化最大绝对偏差贝叶斯建模的是对数尺度的残差尺度变换会改变谁离谁更近。解决不是二选一而是把两者都作为证据看——如果某个准则在两套方法下的区间重叠严重就老老实实说明这个位次不确定别硬排。第五类改了准则数量但模型跑不动。现象是数据里加了第 6 个准则prep_jags_data之后采样器报维度不匹配。原因是模型文件里的循环上界写成了常数或者idxB、idxW的长度没跟着更新。解决是把所有硬编码的长度换成n或者n-1并在prep_jags_data里加一条断言assert(numel(jdata.aB) jdata.n - 1)。这类错误一旦被抓到改起来很快但如果没有断言它会在采样阶段以莫名其妙的维度报错出现查起来反而绕。6. 二次开发换先验、扩群决策、接自己的业务拿到这份代码之后最值得动的三个地方我都试过按投入产出排一下。换先验是最轻的改动只动alpha[]那几行。如果你手上有一批历史评审数据统计各准则被指认为最优的频率用频率加一个小常数当 alpha就是一个经验贝叶斯先验。这样新一次评审的结果会向历史均值收缩样本少的时候特别管用。下面的写法把先验强度做成可调参数% 传入历史最优准则频次构造经验 Dirichlet 先验 histCount [12 5 20 8 3]; % 各准则历史被指认为最优的次数 alpha0 2; % 收缩强度越大越依赖历史 alpha histCount alpha0; jdata.alpha alpha / sum(alpha) * numel(alpha); % 保持总强度稳定扩成群决策是这类方法真正的用武之地。多个专家各自给一组aB、aW不要先平均再建模——平均会抹掉专家之间的分歧而这部分分歧恰恰是你想知道的。更合适的做法是层次模型每个专家有自己的权重向量都来自一个共同的群体分布MCMC 同时估计个体权重和群体均值。改动量不小但 JAGS 里加一层循环就能表达比在 MATLAB 里手工做加权平均诚实得多。接业务数据管道是工程侧的活。现有实现的输入是 CSV实际项目里通常从数据库或者接口取。我的习惯是把prep_jags_data的签名改成接收一个 struct 而不是两条向量读数据的那一层单独抽出去这样模型代码永远不需要知道数据从哪来。改完之后写一组单元测试用固定输入对固定输出验证重构没改变数值结果。验证方法上我一般做三件事一是拿传统 BWM 的linprog结果和后验均值比差得离谱就说明观测模型有问题二是用已知权重的模拟数据反向生成aB、aW看能不能把真值捞回来这是检验模型设定最直接的办法三是把后验样本按排序统计输出各方案排第一的概率这个数比均值排序更能说服做决策的人。有次改完观测分布忘了同步改tau的先验跑出来的区间宽到所有准则都重叠差点把结论写成这几个准则重要性差不多。从那以后我每次改模型文件都强制先用模拟数据做一遍真值回收过了再碰真实数据。希望这些能帮到你少走一遍我踩过的路。本文还有配套的精品资源点击获取