做综合评价的朋友,大概率都遇到过这样一个尴尬:层次分析法(AHP)算出来的权重看着挺合理,可判断矩阵的一致性比率CR就是压不进0.1。手动改矩阵吧,改一处又影响另一处,越调越乱。我当时被这个问题卡了两天,最后把粒子群算法(PSO)塞进AHP的权重求解环节,才算真正把流程理顺。这篇文章就把这套PSO-AHP模型的构建思路、适应度设计、Matlab代码与一个供应商评估实例完整写出来,希望对正在做方案评价或论文建模的人有帮助。
1. 为什么要把粒子群算法和层次分析法放在一起
1.1 AHP在综合评价里的两个老毛病
AHP应该是做多指标评价绕不开的一套框架,它把人的主观经验拆成两两比较,最终算出一组成权重。思路确实经典,但实际用起来有两个问题非常折磨人。
第一个是指标一致性。专家打分本质上是主观的,每个人对“重要性”的理解都有偏差。比如一位专家说质量比交付重要2倍,交付比服务重要3倍,按数学逻辑质量相对服务应该是6倍,但专家打表的时候可能随手填了个4或者5。一个5阶矩阵里这样的偏差只要出现两三处,CR就很容易超过0.1。《层次分析法》教材都会告诉你“此时应当调整判断矩阵”,但调过的人都懂:这是个牵一发动全身的工程,改一个数,权重变了,别人问起来还说不清为什么改。
第二个问题更隐蔽:权重对打分的变化极度敏感。同一个判断矩阵,某个元素从3改成4,最终权重可能移动好几个百分点,靠后的方案甚至可能直接翻到前面。做综合评价时,评审专家盯着的是最终排名清单,你很难解释“为什么我只是微调了一个标度,第一名就换人了”。这也让我意识到,手工调整判断矩阵这条路其实走不通,必须引入某种自动搜索机制,在保留专家原始意见的基础上做“最小修正”。
1.2 两种主流的PSO-AHP结合方式
把PSO和AHP结合,网上搜索会发现有两条路线,很多人都容易搞混。我在这里把两者对比清楚:
| 结合方式 | 粒子编码内容 | 适应度函数 | 最大优点 | 主要缺点 | 适用场景 |
|---|---|---|---|---|---|
| 方式一:修正判断矩阵 | 判断矩阵上三角元素 | CR + 偏离惩罚项 | 保留专家打分结构,解释性强 | 搜索维度偏高 | 论文建模、评估报告 |
| 方式二:直接搜索权重向量 | 归一化权重向量 | 一致性偏差 + 业务约束罚项 | 容易加权重上下限 | 比例约束难处理,粒子容易飞出可行域 | 权重有硬约束的决策 |
两种方式我都写过测试代码,最终在项目里采用的是方式一:让粒子去搜索判断矩阵的上三角元素,适应度函数包含两部分,一部分是修正后矩阵的一致性比率CR,另一部分是修正后矩阵与专家原始打分之间的偏离程度。为什么用方式一?因为综合评价项目里,评审专家看到“你把我1到9标度打的分做了小幅修正”比“你直接给一组我不知道从哪来的权重”更容易接受,而且方式一保证了每个判断矩阵元素最终都落在原始打分的附近,决策逻辑可追溯。方式二虽然也能用,但要处理权重和等于1以及每个权重必须非负这些约束,粒子在演化过程中经常把约束破坏掉,代码量和调参难度反而更大。
1.3 什么情况下才值得上PSO
并不是所有AHP场景都值得上PSO,我在刚接触这个方法的时候走过弯路,拿一个3阶矩阵去跑PSO,浪费时间又没有必要。建议你按下面三条来对号入座:
判断矩阵阶数n在3到9之间的时候,PSO效果很稳定;一旦超过10,上三角元素个数会迅速膨胀到45个以上,搜索空间太大,收敛速度和结果稳定性都明显下降。如果指标层超过10个指标,先做指标合并,或者把整个评价拆成几个子模型分别处理。
判断矩阵经常出现CR不合格,手工调整几轮都压不下来的情况,PSO-AHP能直接把“调整”这个过程自动化,省的时间非常可观。
综合评价结果要作为投标、立项、排名等正式依据,需要把权重的生成过程固化成一段可复现的脚本。评审时直接拿出一份“修正前CR、修正后CR、每个元素改动幅度”的对比表,比靠嘴解释有说服力得多。
反过来讲,如果CR本身在0.05以下,矩阵只有3阶或者4阶,手工处理几分钟就能搞定,那就没必要引入PSO。不要把算法当成装饰品,解决实际问题的成本才是第一位的。
2. 模型构建前需要吃透的两个基础模块
2.1 层次分析法:从判断矩阵到一致性指标CR
要用好PSO-AHP,AHP本身的底层逻辑必须先吃透。AHP的完整流程大致是四步:建立层次结构、构造判断矩阵、求权重、一致性检验。
构造判断矩阵时,常用的是1到9标度,a_ij表示指标i相对指标j的重要程度,a_ij = 5就表示i比j明显重要,同时a_ji取1/5。判断矩阵的主对角线永远是1。求权重的方法有很多,特征向量法和几何平均法用得最多。几何平均法的公式不复杂:先把每一行的元素连乘后开n次方,得到一组中间值,再把这组值归一化。比如对某个矩阵的第i行,它的权重估计就是:
w_i = (a_i1 * a_i2 * ... * a_in)^(1/n),然后除以所有行中间值的总和。
得到权重w之后,再计算最大特征值λmax,通常用公式λmax = (1/n) * Σ(Aw)_i / w_i。接着算CI = (λmax - n) / (n - 1),再引入随机一致性指标RI来消除阶数影响,最终得到CR = CI / RI。
RI的标准值建议直接查表,1到10阶分别是:0、0、0.58、0.90、1.12、1.24、1.32、1.41、1.45、1.49。判断规则很简单:CR < 0.1认为一致性可以接受,否则就需要修正判断矩阵。
我拿一个3阶矩阵演示一下CR超标的实际现象。假设专家给出矩阵:
[1 3 1/3 1/3 1 1/3 3 3 1]
用几何平均法算出来权重大约是0.28、0.14、0.58左右,最后算出的CR约0.12,刚好超过0.1的门槛。问题出在哪里?矩阵里指标1相对指标3打了3倍重要,而指标3相对指标1又打了3倍重要,这本身就是互相矛盾的一对判断。3阶矩阵都算不出合格的一致性,说明人工打分出现循环偏好是很自然的事情。这也是后面PSO要发挥价值的地方。
2.2 粒子群优化:位置、速度与全局最优
PSO的灵感来自鸟群觅食,理解起来并不难。每个粒子代表解空间里的一个候选点,它有三个关键信息:当前位置x_i,当前速度v_i,自己历史最优位置pbest_i;整个种群还有一个共享的全局最优位置gbest。
每一轮迭代,粒子根据下面两条规则更新自己:
v_i(t+1) = w * v_i(t) + c1 * r1 * (pbest_i - x_i(t)) + c2 * r2 * (gbest - x_i(t))
x_i(t+1) = x_i(t) + v_i(t+1)
这里的w是惯性权重,控制粒子保留多少原来的速度;c1和c2是个体学习因子与社会学习因子,分别代表粒子向自己经验和群体经验靠拢的倾向;r1和r2是0到1之间的随机数,给搜索过程加一点随机性。
PSO在这类问题上的优势很明显:不需要目标函数可导,只需要能算出数值的大小;参数少,实现起来比遗传算法省很多事;在维度不高的情况下,收敛也比较快。权重求解本质上就是连续变量优化,判断矩阵里的每个元素都是连续值,PSO天然适合。像是“修正后矩阵与原始矩阵偏离尽量小”这种平滑目标,用PSO在局部范围内搜索,效果比穷举或者随机试错都好得多。
2.3 适应度函数的选取是整个模型最关键的一步
很多初版PSO-AHP代码跑出来的结果让人想摔键盘,不是因为PSO没收敛,而是适应度函数设计得不合理。适应度函数就是给每个粒子打分的函数,它决定了“什么样的判断矩阵才是好矩阵”。
我在项目里用的适应度函数是:
F = CR(M) + α * D(M, A_original)
其中M是粒子解码后得到的判断矩阵,A_original是专家原始判断矩阵,D是两个矩阵之间的偏离程度,α是惩罚系数。这个函数有两层意思:第一层,一致性要优先满足,CR必须尽量小;第二层,也不能为了把CR压到0,把原始判断矩阵改得面目全非,所以在目标里加入偏离惩罚。
为什么不能用线性域的距离来度量偏离程度?因为1到9标度是乘性区间,不是加性区间。专家认为3和5的差距,和1/3与1/5的差距在语义上应该是对称的,但线性差一个等于2,另一个等于0.133,相差15倍。如果直接用线性距离,算法会偏向修改较小的倒数那侧,结果很诡异。解决方法是把判断矩阵元素放到以2为底的对数域里比较,log2(5) - log2(3) = 0.737,log2(1/3) - log2(1/5)也等于0.737,两边完全对称。这就是判断矩阵的代数结构决定的,代码里我对上三角元素做log2变换,也正是这个原因。
α的取值也值得说。我自己的经验是0.01到0.05之间比较合适。α太小,算法会把矩阵拉成某个近似一致的形态,但和专家打分的距离很远;α太大,CR又会修不干净。实际项目里可以先固定CR为高优先级,然后观察修正后矩阵每个元素的变化幅度,如果某个元素被动了超过两个标度,就把α调大一点。
3. Matlab代码实现与关键环节拆解
3.1 整体代码架构:一个脚本加三个局部函数
完整代码我整理成了四个部分:一个入口脚本和三个局部函数。入口脚本负责输入判断矩阵、设置PSO参数和输出结果;三个函数分别承担适应度计算、AHP权重与CR计算、PSO主循环搜索。
| 模块 | 作用 | 核心输入 | 核心输出 |
|---|---|---|---|
| PSO_AHP_demo.m | 入口脚本 | 判断矩阵A、参数opts | 最优权重、CR、修正矩阵 |
| psObj | 计算粒子适应度 | 粒子对数编码x_log、原矩阵A、阶数n、惩罚系数α | 适应度值 |
| ahpCR | 计算AHP权重与CR | 判断矩阵M | 权重w、CR |
| PSO_AHP_opt | PSO主循环 | 原矩阵A、参数opts | 最优对数编码、最优适应度、修正矩阵 |
这里要提一个兼容性问题:Matlab R2016b之前不支持在脚本里直接写局部函数,如果用的是旧版本,需要把下面这些函数逐个保存成独立的function文件,文件名和函数名保持一致。我在公司和学校导师合作的项目里,经常看到旧版本报错,就是这个原因。
3.2 主循环与粒子初始化逻辑
粒子编码是整个实现的精髓。我的做法是只对判断矩阵上三角元素进行编码,因为一个n阶正互反矩阵只需要上三角的n(n-1)/2个元素,下三角完全由倒数关系确定。例如5阶矩阵,上三角正好10个元素,粒子维度就是10。
把上三角元素做log2变换后,粒子每个维度的取值范围就在log2(1/9)到log2(9)之间,也就是约-3.1699到3.1699。初始化种群时,我不建议完全随机撒点,更稳的做法是以专家原始矩阵为基准生成扰动:
% 上三角索引位置 idx = find(triu(ones(n), 1)); dim = numel(idx); % 以专家矩阵对应的对数值为均值,加20%扰动 center = log2(A(idx))'; X = repmat(center, pop, 1) .* (1 + 0.2 * randn(pop, dim)); X = min(max(X, log2(1/9)), log2(9)); V = zeros(pop, dim);为什么不完全随机初始化?两个原因。第一,以专家矩阵为中心,相当于告诉算法“不要跑太远,就在原始判断附近找最优解”,收敛速度会快很多。第二,修正后的矩阵更容易向评审解释,你拿到的是一个“在专家打分基础上做了微调”的版本,而不是凭空生成的另一个矩阵。扰动系数0.2是我常用的经验值,如果原始矩阵CR离0.1差距很大,可以放到0.4到0.5,但最好不要超过0.5,否则修正幅度过大,专家会不认账。
主循环的PSO更新部分也比较常规,重点说一下惯性权重的处理。我用的是线性递减策略,从0.9慢慢降到0.4,这样前期的搜索范围大,后期收敛稳:
for it = 1:maxiter w_cur = 0.9 - (0.9 - 0.4) * it / maxiter; for i = 1:pop V(i,:) = w_cur * V(i,:) ... + c1 * rand(1,dim) .* (pbestX(i,:) - X(i,:)) ... + c2 * rand(1,dim) .* (gbestX - X(i,:)); X(i,:) = X(i,:) + V(i,:); % 边界截断,保证所有元素仍在有效标度内 X(i,:) = min(max(X(i,:), log2(1/9)), log2(9)); val = psObj(X(i,:), A, n, alpha); % 更新个体最优和全局最优 if val < pbestVal(i) pbestVal(i) = val; pbestX(i,:) = X(i,:); end end end边界截断这个动作很关键。如果不截断,粒子可能在解空间中飞出去,解码出来的判断矩阵会出现超过1到9标度的元素,这在AHP框架里是没有意义的。整个搜索过程跑完之后,全局最优粒子的对数编码再通过指数变换映射回判断矩阵上三角。
3.3 适应度计算与权重还原
适应度函数是这段代码里最需要理解清楚的部分,我直接贴完整代码:
function fitness = psObj(x_log, A, n, alpha) idx = find(triu(ones(n), 1)); M = ones(n); M(idx) = 2 .^ x_log(:); % 对数编码还原成标度值 for i = 1:n for j = 1:n if i > j M(i,j) = 1 / M(j,i); % 下三角取倒数 end end end [~, CR] = ahpCR(M); dist = norm(x_log(:) - log2(A(idx)), 2) / sqrt(numel(idx)); fitness = CR + alpha * dist; end function [w, CR] = ahpCR(A) n = size(A, 1); w = prod(A, 2) .^ (1/n); % 几何平均法求权重 w = w / sum(w); Aw = A * w; lambda_max = mean(Aw ./ w); CI = (lambda_max - n) / (n - 1); RI = [0 0 0.58 0.90 1.12 1.24 1.32 1.41 1.45 1.49]; if n > 10 RI_n = 1.49; else RI_n = RI(n); end CR = CI / RI_n; end为什么这里用几何平均法求权重而不是直接用eig函数做特征向量法?特征向量法在理论上最标准,但需要在代码里对特征值排序、取最大特征值对应的特征向量,再归一化,对不熟悉Matlab线性代数接口的读者而言,代码理解成本会高不少。几何平均法本质上是特征向量法在一致性接近时的良好近似,在综合评价场景里足够稳定,而且代码一眼就能看懂。如果你面对的是要发核心期刊的学术场景,可以用eig替换掉ahpCR内部实现,两种方法算出来的权重差异一般只有千分之几。
这里还藏着一个细节:dist计算时除以sqrt(numel(idx)),目的是让偏离惩罚项不受矩阵阶数影响。一个5阶矩阵的10个元素和7阶矩阵的21个元素,如果不对距离做归一化,高阶矩阵的累计偏离天然就更大,α的通用性会很差。做一次平均处理后,α的取值在3到9阶之间都能直接复用。
3.4 运行输出与结果解读
把代码整理好后,调用方式可以封装成下面这个样子:
opts = struct('pop', 30, 'maxiter', 300, 'w', 0.6, ... 'c1', 2.0, 'c2', 2.0, 'alpha', 0.01, 'runs', 20); [w0, CR0] = ahpCR(A); fprintf('原始判断矩阵:CR = %.4f\n', CR0); [bestX, bestScore, M_opt] = PSO_AHP_opt(A, opts); [w_opt, CR_opt] = ahpCR(M_opt); fprintf('PSO修正后:CR = %.4f\n', CR_opt); disp(w_opt);运行结果要怎么读?首先看CR0,这是不修正的直接结果。以我的项目经验,5阶以上专家打分CR落在0.05到0.10区间是非常常见的。PSO跑完后看修正后的CR,一般能压到0.04以下,有些矩阵甚至能到0.02。这里有个度的问题:不要追求CR无限接近0,因为判断矩阵修得越漂亮,偏离专家原始判断就越多,评审反而会质疑你改动了他们的意思。把CR压到0.1以下、最好0.05左右,同时每个元素的改动幅度不超过一个标度,这才是理想状态。
4. 综合评价实例:从指标打分到方案排序
4.1 指标体系与数据准备
用一个我实际做过的设备采购评估案例来演示整个流程。目标是评估5家供应商,指标体系选择五个维度:质量、价格竞争力、交付速度、服务满意度、创新能力。这五个指标都是正向量,评分越高越好,于是省去了负向指标倒扣的麻烦。
原始评估数据如下:
| 供应商 | 质量 | 价格竞争力 | 交付速度 | 服务满意度 | 创新能力 |
|---|---|---|---|---|---|
| A | 92 | 80 | 85 | 88 | 90 |
| B | 88 | 92 | 82 | 85 | 82 |
| C | 85 | 88 | 90 | 80 | 88 |
| D | 90 | 85 | 86 | 92 | 84 |
| E | 86 | 90 | 88 | 86 | 86 |
由于各指标的取值范围都在0到100之间,我直接用最大值归一化处理,每个值除以该列最大值,得到一个无量纲的0到1矩阵,便于加权平均。比如质量列最大值为92,A的质量标准化后为1.0000,B为0.9565,以此类推。
4.2 用PSO-AHP计算权重的过程
接下来需要专家对五个指标的重要性做两两比较。这里我模拟一组判断矩阵,质量相对价格竞争力是2倍重要,相对交付速度是4倍重要,价格竞争力相对服务是2倍重要,服务相对创新是3倍重要,整体构造合理且带有少量不一致:
A = [1 2 4 2 5; 1/2 1 2 1 3; 1/4 1/2 1 1/2 2; 1/2 1 2 1 3; 1/5 1/3 1/2 1/3 1]
把这段代码接入PSO_AHP_demo后,会得到原始CR和修正后CR两组数据。从实际经验来看,这类5阶矩阵的原始CR通常在0.06到0.08之间,PSO跑完后能压到0.04以下。修正后的权重大概在质量0.37到0.40、价格竞争力0.21到0.25、交付速度0.11到0.13、服务满意度0.19到0.21、创新能力0.06到0.08这个区间附近。因为PSO是随机算法,每次运行权重在小数点后第三位会有波动,建议用opts.runs多次独立运行取历史最优。
4.3 综合排序与结果解读
得到权重w_opt后,综合得分就是标准化矩阵乘以权重向量:score = X_norm * w_opt。最终可以整理出排名表:
| 排名 | 供应商 | 综合得分 |
|---|---|---|
| 1 | D | 0.9658 |
| 2 | A | 0.9573 |
| 3 | B | 0.9507 |
| 4 | E | 0.9500 |
| 5 | C | 0.9313 |
D胜出的原因很直观:质量和服务满意度两个高分维度权重恰好都在前列,B的价格竞争力虽然最高,但质量和交付相对弱,综合下来被D反超。这里想提醒一句:权重对排名的影响非常直接,如果调整判断矩阵导致权重波动,排序可能会随之变化。所以综合评估报告里应当附上权重来源、CR修正前后的对比,以及最终每个供应商得分的构成明细,这样结论才是可考证的。
5. 常见问题、参数调优与避坑笔记
5.1 一致性怎么都修不过去
遇到过几次PSO跑完CR仍然在0.1以上的情况,排查后大多指向两类原因。
第一类是判断矩阵存在明显的循环偏好。比如a_12 = 5表示指标1比指标2重要很多,a_23 = 5表示指标2比指标3重要很多,但a_13却只打了1,这种互相矛盾的信息强度太高,单纯做局部的“最小修正”已经救不回来。我的处理方式是先把原始打分记录翻出来,找专家确认是否存在填错标度的可能,同时把alpha从0.01降到0.005,让算法更侧重于一致性。如果矩阵本身冲突太剧烈,则需要重新开会打分。
第二类是指标层设计问题。当阶数n大于9时,上三角元素个数超过36,搜索空间太大,PSO的收敛质量明显下降。AHP本身就不适合在同一个准则层下面挂超过9个指标,建议先合并同类指标,或者把一个大模型拆成两个子模型分别评价。与其强行让算法去解决超过能力范围的搜索,不如回到指标体系设计层面做减法。
5.2 PSO结果不稳定,每次跑权重都不一样
这个问题很多第一次用PSO的人都会遇到。原因是随机初始化加上目标函数本身存在多个局部最优解,粒子群在单次运行中很容易被某个局部最优困住。这不是代码写错,是随机优化算法的天然属性。
我的做法是把runs设到20到50,每次独立运行后记录全局最优结果,最终在所有运行里取适应度最好的那一次。如果你还关心权重的稳定性,可以把每次运行的最优权重都存下来,计算每组权重所有维度上的均值与标准差。标准差超过0.02,就说明判断矩阵中某些信息冲突强烈,权重估计不够稳,需要重新审视指标设计或者增加专家打分轮次。收敛曲线的绘制也很实用,把每代全局最优适应度存到数组里,用plot画出来,可以直观判断有没有陷入早熟。
5.3 权重被拉得太平或过于集中
有朋友问过我,为什么PSO跑出来的权重全部落在0.1到0.3之间,区分度很低。这个问题的根源通常不在算法,而在判断矩阵的标度跨度。如果专家打分时给出的都是2、1、3这种小标度,矩阵本身就表达不出“某个指标特别重要”的信息,权重自然挤在一起。建议让专家把“相对重要性”理解为比值,而不是“打一个分数”,比如“质量的重要性是价格竞争力的3倍”而不是“质量打3分,价格打1分”。
如果业务上确实要求某指标权重大于等于0.2,可以在解码后加投影处理,把低于下限的维度抬上去再重新归一化;或者直接在适应度里加罚项,违反权重约束就给一个很大的惩罚值。后者更保险,因为粒子在搜索过程中就会避开不可行区域,而不是靠最后一步硬修正。不过这类额外约束加得越多,搜索空间越复杂,能不加就不加。
5.4 参数速查表与使用建议
最后整理一份PSO-AHP参数调节速查表,是我跑项目时沉淀下来的经验取值:
| 参数 | 推荐默认值 | 调节方向 |
|---|---|---|
| pop 种群规模 | 30 | 结果不稳定就调大,但别超过80 |
| maxiter 最大迭代次数 | 300 | 收敛曲线平坦后可用200 |
| w 惯性权重 | 0.6,配合线性递减 | 前期全局搜索不够就调大起始值 |
| c1 个体学习因子 | 2.0 | 收敛太慢可调大 |
| c2 社会学习因子 | 2.0 | 陷入局部最优就调大c2 |
| alpha 偏离惩罚 | 0.01 | CR修不掉往小调,矩阵变形大往大调 |
| runs 独立运行次数 | 20 | 成果要求越高越要调大,最多到50 |
我自己的习惯是先用默认参数试跑两三次,看一眼CR和权重是否落在合理范围。如果CR合格但矩阵改动超过两个标度,把alpha往0.03方向调。如果CR不合格而改动不大,把alpha往0.005方向调。把收敛曲线画出来观察最后50代的变化也非常关键,如果曲线已经平了很久,加大maxiter也不会有本质提升,问题大概率出在初始化策略上。
用了PSO-AHP一段时间之后,我另一个深刻的体会是:把每轮专家打分的原始矩阵、PSO参数、修正后矩阵和CR前后对比全部存档,这件事比调算法还要重要。综合评价最后面对的问题,往往不是“算法好不好”,而是“你这权重怎么来的、凭什么这么改”。PSO-AHP的优势在于修正量最小、目标函数可解释,配合完整存档,才能让评审真正认可整个评价过程。这套代码后面还可以继续扩展,比如结合熵权法做组合赋权,或者把修正过程做成界面工具,但先把判断矩阵跑通,后面自然就顺了。