ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从纳什均衡到SFE:电力市场报价博弈的建模、求解与验证

从纳什均衡到SFE:电力市场报价博弈的建模、求解与验证

简介:电力市场与博弈论交叉方向的课程讲义PPT,面向电气工程、能源经济等专业学生及需要理解发电侧竞价逻辑的入门读者,重点回答博弈论基本概念如何用于电力市场报价分析。压缩包仅含1个PPT文件,大小约847KB,属于轻量单文件课程资料,便于课堂展示、复习翻阅和汇报前快速梳理框架。已有173人学习,说明其作为概念入门材料具备一定参考价值。内容从参与人、行动、信息、战略、支付、结果与均衡等要素切入,串联博弈的战略式表述、静态与动态博弈、完全与不完全信息博弈及对应纳什均衡,并进一步介绍电力寡头市场的古诺、伯特兰德和供给函数均衡模型。读者可借此建立从博弈要素到市场均衡的分析路径,理解发电厂商报价策略与均衡求解之间的关系,适合概念速览和知识串联。

1. 发电厂的报价单,本质是别人战略的函数

一份讲电力市场的课程资源,最容易被快速翻过去的就是博弈论那几页 PPT——参与人、行动、信息、战略、支付,一页一个名词,看着像考前背诵的清单。可真坐到报价席上,问题立刻变了味:你报 320 元/兆瓦时,同片区的机组报 300,你的电就上不了网;你把价格压到 290,对方跟着压到 280,你依然出局。这不是一台机组对着自己的成本曲线做单目标优化,而是一群人同时被卷进同一个方程组。博弈论给的从来不是某个“最优报价”,而是一个判据:什么样的报价组合,谁单方面改动谁吃亏。这份讲义把判据拆成了五个要素、四个象限和三个寡头模型,下面一层层往下挖。

2. 五要素拆解:为什么支付函数里必须出现别人的战略

2.1 参与人与行动集:决策变量究竟是「量」还是「价」

参与人是博弈里的决策主体,目标是靠选行动或战略把自己的支付推到最高。落到电力市场,参与人就是那些报价的发电厂商;一个人控制多台机组时,也可以把整个投资组合当成一个参与人处理。这里第一个容易含糊的地方在于:参与人的身份边界必须和你建模的决策边界一致。如果你把同一个集团下的两台机组拆成两个参与人,它们之间的内部协调就会被误当成市场竞争,均衡结果会明显偏离现实。

行动是参与人在某个时点的决策变量,可以离散,也可以连续。电力市场里最典型的行动有三类:申报价格、申报电量、机组启停状态。离散化之后,行动集的大小直接决定了计算规模。报价从 0 到 1000 元/兆瓦时按 1 元一档,就是 1001 个行动;两台机组做战略式展开,组合数立刻过百万。很多人建模时随手把粒度设成 0.1 元,然后抱怨求解器跑不动,根子就在这里。

n 个参与人各选一个行动,拼起来的有序组叫行动组合。支付函数吃的就是这个组合,而不是某一个参与人自己的行动。这一点是博弈和普通优化的分水岭:普通优化里目标函数只依赖自己的决策变量,博弈里它依赖整个向量。

from itertools import product # 参与人:两台机组的报价决策 players = ("G1", "G2") # 行动集:把连续报价离散成三档,粒度决定了后续组合规模 actions = { "G1": [280, 300, 320], "G2": [280, 300, 320], } # 行动组合 = 所有参与人行动集的笛卡尔积 profiles = list(product(*(actions[p] for p in players))) print(len(profiles)) # 9,两台机组各三档 print(profiles[0]) # (280, 280) # 粒度每加一档,组合数按参与人数指数增长 def combo_count(players, k): return k ** len(players) print(combo_count(players, 10)) # 100 print(combo_count(("G1","G2","G3","G4"), 10)) # 10000

这段代码只做一件事:把“行动集 → 行动组合”这层关系摊开。actions是每个参与人可选的行动集合,product生成的是行动组合而非战略组合,静态博弈里两者恰好重合。combo_count用来看规模:4 台机组、10 档报价就是 1 万种组合,每种的支付都要算一遍;如果把档位加到 100,就是 1 亿。所以讲义里反复强调“行动可以是离散的也可以是连续的”,不是啰嗦,是在提醒你:离散化粒度是一个必须提前算清楚的成本项。

2.2 战略是相机行动方案,不是一张报价单

战略和行动的区分,是整个讲义里最容易被读快而漏掉的一段。行动是某个时点的一个决策值;战略是一份规则,规定在每一个可能的信息集上该选什么行动。用讲义的原话说,战略是“相机行动方案”。静态博弈里所有人同时动手、谁也看不到谁,战略就退化成单个行动,两个词可以混着用;一旦进入动态场景,混淆这两个词会直接把模型算错。

举个具体的:日前市场先出清,实时市场再平衡。你在日前报了一个量价对,在实时市场还要根据日前出清价、机组实际出力、负荷预测偏差再报一次。这时你的战略不是“日前报 300”,而是“日前报 300;如果日前出清价高于 320 就实时减报,低于 280 就实时增报”。前者是一个数,后者是一张 if-then 表。

这个区分带来的计算后果是数量级的。你有 3 档行动,对手先动且你能观察到他选了哪一档,那么你的战略空间不是 3,而是 3 的 3 次方——每一个观察到的对手行动,你都要预备一个回应。这是动态博弈求解难的根本原因,也是子博弈精炼这类概念存在的理由:它要在一张指数膨胀的表里,把不可信的回应剔除掉。

from itertools import product def strategy_space(own_actions, observed_actions=None): """静态博弈:战略 == 行动;动态博弈:战略是观察结果的函数""" if observed_actions is None: return [(a,) for a in own_actions] # 对对手的每一种可能行动,都要指定一个自己的回应 return list(product(own_actions, repeat=len(observed_actions))) own = [280, 300, 320] rival = [280, 300, 320] print(len(strategy_space(own))) # 3 静态 print(len(strategy_space(own, rival))) # 27 动态,指数膨胀

own_actions是自己的可行行动,observed_actions是能观察到的对手行动节点。静态分支直接返回单元素元组,动态分支返回所有“回应组合”。第二行输出 27 这个数字值得盯一会儿:机组数量再多一台,或者报价档位再细一档,战略空间就会膨胀到求解器无法处理。现实建模里大家会用“限制回忆长度”“只对关键节点分支”这类手段剪枝,本质是在给这张表降维。

2.3 完美信息与完全信息:一字之差的两个假定

信息是参与人关于博弈的知识,尤其是关于“自然”的选择、其他参与人的特征和行动的知识。讲义把两个特别容易混的概念并排放了:完美信息说的是你能准确看到别人已经做了什么;完全信息说的是你对别人的特征、战略空间、支付函数有完整了解,事前没有不确定性。前者管行动可见性,后者管类型可知性,两者可以独立成立或不成立。

这个区分在电力市场里非常实在。报价保密制度下,别的机组报了什么价你看不到,这是不完美信息;但你大致知道对方的燃料成本区间、机组容量、检修计划,这可以算完全信息。反过来,有些市场披露历史报价,行动可见,但对手的边际成本可能因为燃料长协价格不公开而无法确知,那就是完美但不完全。把这两个维度搞混,会直接把你送到错误的均衡概念上去。

还有一个更强的假定:共同知识。它指的是“所有参与人知道,所有参与人知道所有参与人知道……”,无限递归下去。这个假定在教科书里一句话带过,在工程建模里却是一个硬约束:只要有一家厂商不确定别人是否知道自己知道,理性的推理链条就断了。现实中报价策略的“玄学”感,很大一部分来自共同知识链条的断裂——大家并不真的确信对方和自己对规则的理解完全一致。

提示:建模时把“信息结构”当成一个独立参数写进配置文件,别写死在代码里。同一个求解器,换个信息假定就是另一个均衡概念,硬编码会让复用变成灾难。

3. 战略式表述与四象限:均衡概念为什么要跟着信息结构走

3.1 把博弈压缩成 (N, S, u) 三元组

战略式表述干的事情是“抹掉时间”。它只保留三样东西:参与人集合、每个人的战略空间、每个人的支付函数。所有关于“谁先动、谁看到了什么”的时序信息,都被压进了战略的定义里——因为战略本身就是一份覆盖所有信息集的行动规则,时序信息已经编码在规则中了。

这份讲义只给了战略式表述,没展开扩展式,这在课程场景下是合理的取舍:战略式适合静态博弈和纯策略分析,扩展式适合画博弈树、讲子博弈精炼。但从工程角度看,扩展式更贴近电力市场的实际决策流程,因为机组启停、日前-实时两级市场天然是分阶段的。

支付函数 u_i(s) 的自变量是整个战略组合 s,不是 s_i。这一行符号看着平淡,实际是整个理论的支点。它意味着:你把自己的战略调到最优,前提是别人的战略已经固定;别人一改,你的最优解就变了。所谓“求解”,找的不是一个最优点,而是一组互相咬合的点。

从优化理论的角度看,纳什均衡可以写成一个最优化问题的解:s_i* 是在给定其他所有人选择 s_-i* 的条件下,第 i 个参与人能取到的最大支付对应的战略。n 个参与人就有 n 个这样的最优化问题,它们耦合在一起,必须联立求解。

3.2 行动顺序 × 信息完全性,切出四种博弈四种均衡

讲义给出的四象限是这套理论的骨架,缺了它后面三个寡头模型就没法定位。

信息结构静态动态
完全信息纳什均衡子博弈精炼纳什均衡
不完全信息贝叶斯纳什均衡精炼贝叶斯纳什均衡

四格不是四套平行理论,而是同一套逻辑在两种不确定性下的两次升级。静态完全信息下,纳什均衡够用,因为你不需要考虑“我说了要做但事后不会做”这种问题。一旦进入动态,先动者可能放出一个不可信的威胁来影响后动者,纳什均衡管不住它,于是要用逆向归纳把不可信的部分从后往前剪掉,剩下的才叫子博弈精炼。再往右一格,参与人连对手的类型都不确定,就得把类型先验和贝叶斯更新引进来。

映射到电力市场:日前市场多家机组同时申报,可以按完全信息静态博弈处理,找纳什均衡;如果某家先公布检修计划、其余机组再报价,就要按动态处理;如果燃料成本属于私有信息,就得走贝叶斯那条线。选错格子,解出来的东西在数学上自洽,在业务上却站不住。

3.3 纳什均衡的不动点本质:没人愿意单方面改

纳什均衡的定义一句话能说完:给定其他所有人的战略,每个参与人选择的都是自己的最优战略。形式化就是,对任意参与人 i 和任意可行战略 s_i,都有 u_i(s_i*, s_-i*) ≥ u_i(s_i, s_-i*)。换句话说,没有任何人存在“单方面偏离能变得更好”的动机。

这个性质和“全局最优”完全是两回事。全局最优要求所有人协调起来把总支付推到最大,纳什均衡只要求每个人都对别人的选择做出最优回应。囚徒困境里双方都招供是纳什均衡,但显然不是联合最优——讲义里那套支付函数“依赖所有人战略”的表述,讲的就是这个。

从数学上看,纳什均衡是最优反应映射 BR(s) = (BR_1(s_-1), ..., BR_n(s_-n)) 的不动点。存在性靠角谷不动点定理:只要战略空间是紧凸集、支付函数对自身战略连续拟凹,这个映射就有不动点。这条定理保证了博弈至少有一个均衡(可能混在混合战略里),但不保证唯一,也不保证一定落在纯战略上。所以在代码里暴力搜索纯策略纳什均衡时,搜不到不等于模型错了,可能只是均衡在混合战略里。

from itertools import product def best_response(i, profile, players, actions, payoff): """固定其他人不动,扫描参与人 i 的全部可行行动,返回支付最高的那个""" best_a, best_u = None, float("-inf") for a in actions[players[i]]: cand = list(profile) cand[i] = a u = payoff[tuple(cand)][i] if u > best_u: best_a, best_u = a, u return best_a def find_pure_nash(players, actions, payoff): out = [] for prof in product(*(actions[p] for p in players)): # 每个人都在对别人的选择做最优回应 → 该组合是不动点 if all(best_response(i, prof, players, actions, payoff) == prof[i] for i in range(len(players))): out.append(prof) return out players = ("G1", "G2") actions = {"G1": ["低", "中", "高"], "G2": ["低", "中", "高"]} # 支付表:payoff[(a1, a2)] = (u1, u2) payoff = { ("低", "低"): (6, 6), ("低", "中"): (8, 3), ("低", "高"): (9, 1), ("中", "低"): (3, 8), ("中", "中"): (5, 5), ("中", "高"): (7, 2), ("高", "低"): (1, 9), ("高", "中"): (2, 7), ("高", "高"): (4, 4), } print(find_pure_nash(players, actions, payoff)) # [('低', '低')]

best_response是定义的一比一翻译:把参与人 i 的行动逐个替换进战略组合,看哪个支付最高。find_pure_nash遍历所有组合,用“每个人都在做最优回应”做筛选。最后输出[('低', '低')],说明这个收益结构下只有这一组是稳定的:任何一方单独从“低”换到“中”或“高”,收益都会掉下来。注意>是严格比较,如果出现收益相等的情况,按定义也算最优回应,但这段代码只会返回第一个遇到的,遇到平台期需要额外规则——这是数值求解多解的一个常见来源。

4. 古诺、伯特兰德与 SFE:三种寡头模型差在哪一行一阶条件

4.1 古诺:把对手产量当常数,反应函数怎么解出来

古诺模型的本质是把对手的产量水平当作固定值,再决定自己生产多少。它被称为产量竞争模型,各厂商同时决策,都清楚自己的产量会影响价格,但都认为自己的产量决定不影响别人的产量决定。

设反需求曲线 P = a - bQ,Q 是市场总产量;每家厂商边际成本为常数 c。厂商 i 的利润是 π_i = P·q_i - c·q_i = [a - b(q_i + Q_-i)]·q_i - c·q_i,其中 Q_-i 是除 i 以外所有人的产量之和。对 q_i 求一阶条件:

  • 展开:π_i = (a - c)q_i - b q_i² - b Q_-i q_i
  • 求导:dπ_i/dq_i = (a - c) - 2b q_i - b Q_-i = 0
  • 反应函数:q_i = (a - c - b Q_-i) / (2b)

二阶条件 d²π_i/dq_i² = -2b < 0,只要 b > 0 就满足,所以一阶条件给出的确实是最大值而不是最小值。这一步讲义专门提了一句“所以上式小于等于 0”,就是在确认极大值条件,很多讲义会跳过它,但跳过之后你无法判断解出来的是均衡还是鞍点。

对称均衡下所有厂商产量相同,Q_-i = (n-1)q,代入反应函数解得 q* = (a - c) / [b(n+1)]。n 趋于无穷时 q* 趋于 0、价格趋于边际成本;n = 1 时退化成垄断解 (a-c)/(2b)。这两个极限正好把古诺夹在垄断和完全竞争之间,是它最直观的经济含义。

def cournot_symmetric(a, b, c, n): """对称古诺均衡:q* = (a - c) / (b * (n + 1))""" return (a - c) / (b * (n + 1)) def cournot_price(a, b, c, n): q = cournot_symmetric(a, b, c, n) return a - b * n * q # 总产量代回反需求 a, b, c = 100.0, 1.0, 10.0 for n in (1, 2, 4, 10): q = cournot_symmetric(a, b, c, n) p = cournot_price(a, b, c, n) print(f"n={n:2d} q={q:6.2f} P={p:6.2f} 单厂利润={(p - c) * q:7.2f}") # 用最优反应迭代验证不动点(两家对称情形) q1 = q2 = 0.0 for _ in range(200): q1 = (a - c - b * q2) / (2 * b) q2 = (a - c - b * q1) / (2 * b) print(f"迭代收敛: q1={q1:.4f} q2={q2:.4f}") # 30, 30

cournot_symmetric是解析解,参数n是参与人数,b是需求斜率(越大表示价格对产量越敏感,均衡产量越小)。循环打印的四个 n 值展示了收敛路径:n=1 时价格 55、产量 45,是垄断;n=2 时价格 40、产量 30;n=10 时价格已经逼近边际成本 10。第二段用最优反应迭代从 (0,0) 出发反复代入反应函数,200 轮后停在 (30, 30),和解析解一致。这个迭代过程本身就是“不动点”三个字的可视化:两条反应曲线在平面上交于一点,从任何起点出发都会被吸过去。

4.2 伯特兰德:决策变量一换,利润为什么塌到零

伯特兰德模型把决策变量从产量换成价格:生产同一产品的厂商同时定价,产品之间没有差异,也没有容量限制。在这个设定下,只要有一家的价格高于边际成本,另一家略微降价就能抢走全部市场,于是价格被一路压到等于边际成本。此时每个厂商都是价格的接受者,利润为零,所以这个解也叫准竞争解。

数学上很干脆:厂商 i 面对的需求是一个阶跃函数——价格低于对手时拿走全部市场,高于对手时一单没有,等于对手时按某个规则分配。利润最大化的一阶条件在这种情况下退化为“定价不能高于对手,也不能低于边际成本”,唯一稳定的点是 P = MC。

这里有一个经常被忽略的前提:没有容量限制。如果每家机组的装机容量加起来还不够覆盖总负荷,那么价格压到边际成本时市场根本出清不了,价格会被重新抬起来。电力市场的现实恰恰是容量约束、网络阻塞、启停成本三者同时存在,所以纯伯特兰德均衡在电力市场里往往只能作为参照系,不能直接拿来当预测。真正接近伯特兰德逻辑的,是统一出清价机制下的边际机组定价——所有中标机组按最后一个满足负荷的报价结算,这是规则设计出来的价格一致性,而不是厂商博弈出来的。

4.3 SFE:报价曲线当战略,为什么线性解才稳定唯一

供给函数均衡模型最贴近电力市场的实际规则:市场要求各发电商递交供给函数,表明在不同价格下愿意提供的发电量,市场价格由总需求和各家供给函数共同决定。这跟古诺(报量)、伯特兰德(报价)都不一样,决策变量是一条曲线。

讲义提到,很多文献证明线性供给函数均衡才是稳定且唯一的。这句话背后的直觉是:如果供给函数可以任意非线性,厂商就有无穷多个自由度去构造“在均衡价格附近陡峭、在其他区间平缓”的形状,解会变得不唯一且对扰动极其敏感。把供给函数限制成线性,等于把无穷维的战略空间压成两维(截距和斜率),唯一性才有条件成立。

推导对称线性均衡:设各家的供给函数为 s_i(p) = βp(截距取零),市场需求 D(p) = A - Bp,边际成本 c。对厂商 i 来说,其他 n-1 家提供 (n-1)βp,剩余需求是

  • R(p) = (A - Bp) - (n-1)βp = A - Kp,其中 K = B + (n-1)β
  • 反解价格:p = (A - q_i) / K
  • 利润:π_i = (p - c)q_i = [(A - q_i)/K - c]·q_i
  • 一阶条件:dπ_i/dq_i = (A - 2q_i)/K - c = 0,得 q_i = (A - cK)/2

把 q_i 代回价格表达式,得到 p* = (A + cK)/(2K),于是自己的供给函数斜率必须是 β = q_i/p* = K(A - cK)/(A + cK)。而对称性又要求 β = (K - B)/(n-1)。两个式子联立,得到关于 K 的一元方程:

(K - B)/(n-1) = K(A - cK)/(A + cK)

这个方程在有效区间内可以用数值方法解出来,再回代得到 β 和均衡价格。

from scipy.optimize import brentq def sfe_symmetric(A, B, c, n): """对称线性供给函数均衡 s_i(p)=beta*p,需求 D(p)=A-B*p 不动点条件:(K-B)/(n-1) = K(A-cK)/(A+cK),K = B+(n-1)*beta """ def f(K): return (K - B) / (n - 1) - K * (A - c * K) / (A + c * K) lo, hi = B * 1.0001, (A / c) * 0.9999 # 需保证 A-cK>0,价格为正 K = brentq(f, lo, hi) beta = (K - B) / (n - 1) p = (A + c * K) / (2 * K) q = beta * p # 单厂产量 return beta, p, q A, B, c = 100.0, 1.0, 10.0 for n in (2, 4, 10): beta, p, q = sfe_symmetric(A, B, c, n) print(f"n={n:2d} beta={beta:6.3f} P={p:6.2f} q_i={q:6.2f} 总产量={n*q:7.2f}")

brentq是求根器,lo和hi的选择不是随便定的:K 必须大于需求斜率 B(否则 β 为负,供给曲线向下倾斜),同时必须小于 A/c(否则一阶条件给出的产量为负)。这两个边界正好框住可行域,超出范围brentq会报错,这本身就是一个有用的自检。n=2 时算出 β=1.5、P=25、单厂产量 37.5,和手算结果一致。

把四个模型的数字摆在一起,对比才有说服力:

模型决策变量均衡价格单厂产量单厂利润
垄断(对照)总产量55452025
古诺 n=2产量4030900
线性 SFE n=2供给函数2537.5562.5
伯特兰德价格10450

同一套成本和需求参数下,报价曲线竞争(SFE)比产量竞争(古诺)激烈得多,均衡价格 25 对 40,单厂利润腰斩。这个结果在电力市场分析里很关键:市场规则允许报供给曲线还是只允许报一个量价对,会显著改变均衡结果。规则设计者真正在调的,就是这张表里的某一行。

5. 建模避坑:五种翻车现场与排查路径

5.1 把纳什均衡当成联合最优

现象:模型跑出来的均衡利润,比把所有机组当成一家公司统一调度时低了将近一半,第一反应是求解器有 bug 或者目标函数写反了。

原因:纳什均衡要求的是互相最优回应,不是总支付最大。每个参与人只对自己的支付负责,没人有动机为了集体利益牺牲自己。囚徒困境是这个性质最纯粹的例子,寡头古诺模型里价格高于边际成本但低于垄断价,也是同一个道理。把均衡解和联合最优解并列一比,差出来的那部分就是所谓的“竞争损失”,它是理论预测的结果,不是数值误差。

解决:在代码里同时算两个解——联合最优用单一优化器解总利润最大化,均衡解用最优反应或方程联立。两者都算出来再对比,一旦均衡利润高于联合最优,那才是真出问题了。这个交叉验证我一般会写进回归测试,因为改参数的时候特别容易把支付函数改错方向。

5.2 求导时漏掉总产量里的自己

现象:古诺模型的反应函数斜率算出来是 -1/b,而理论值应该是 -1/(2b),均衡产量比预期大了一截。

原因:利润函数 π_i = [a - b(q_i + Q_-i)]q_i 对 q_i 求导时,总产量 Q 里的 q_i 也要参与求导。展开后是 (a-c)q_i - b q_i² - b Q_-i q_i,三项里有两项含 q_i。很多人在纸上写的时候把 P 当常数直接乘,忘了 P 本身是 Q 的函数,漏掉 -bq_i 这一项,一阶条件就从 a - c - 2bq_i - bQ_-i = 0 退化成了 a - c - bq_i - bQ_-i = 0。

解决:养成先展开再求导的习惯,别在心算阶段直接对含 P 的表达式求导。写代码时把利润函数单独定义成一个可调用的函数,用数值微分和解析导数对一遍,差超过 1e-6 就说明推导有问题。这个检查只花几行代码,能挡掉一大半符号错误。

5.3 完美信息与完全信息混用

现象:动态模型里让后手观察到了先手的具体报价,但求解出来的均衡和静态版本一模一样,逆向归纳完全没起作用。

原因:信息集没有真正更新。在战略式表述里,让后手“看到”先手行动,意味着后手的战略空间必须从 k 个行动膨胀成 k^k 个相机行动方案;如果代码里还是给后手一个固定的行动列表,那么所谓“观察”只是形式上的,求解器根本不知道后手有条件化选择的能力。另一个常见混淆是把“我知道你的成本”当成“我看到你的报价”,前者是完全信息,后者是完美信息,处理方式完全不同。

解决:建模前先把信息结构画成一张表,明确每个参与人在每个决策节点知道什么、不知道什么。代码里用strategy_space这类函数显式构造战略空间,别直接用行动集顶替。求解结果和静态版本相同的时候,先怀疑信息结构没生效,再怀疑模型本身。

5.4 伯特兰德结论直接套到电力市场

现象:用伯特兰德模型算某区域市场的均衡,得到价格等于边际成本、所有人利润为零,但实际市场出清价长期高于边际成本,模型预测完全失效。

原因:伯特兰德的三条前提——产品同质、无容量约束、无搜索成本——在电力市场里几乎全部不成立。机组有装机容量上限,输电线路有阻塞,启停有成本,不同机组的爬坡速率也不一样。容量约束一加上,价格压到边际成本时供给不足以覆盖负荷,价格必然被抬起来;网络阻塞会让局部市场隔离,形成节点价差。把这些前提忽略掉,模型就只剩一个理论参照系的作用。

解决:先把伯特兰德解当成下界基准,再看实际价格偏离多少,偏离的部分用容量约束、阻塞、机组组合来解释。如果确实要用伯特兰德框架,至少加上容量约束做成容量约束价格竞争模型,或者引入产品差异化(比如调频、备用等辅助服务)让价格不至于塌到底。

5.5 供给函数均衡解出多组系数

现象:非线性方程组联立求解,从不同初值出发收敛到不同的 (α, β) 组合,每组都满足一阶条件,不知道哪个才是“真”均衡。

原因:一阶条件只是必要条件,不是充分条件。非线性方程组的解可能对应极大值,也可能对应极小值或鞍点;即使都是极大值,也可能存在多个局部最优。讲义里强调“线性供给函数均衡才是稳定唯一的”,恰恰是在给这个问题打补丁:把供给函数限制成线性,可行域变成凸的,一阶条件才同时是充分条件,不动点才唯一。

解决:三板斧——检查二阶条件(海森矩阵负定)、从多组初值出发看是否收敛到同一点、做扰动测试(把某个参数微调 1%,解是否连续变化)。三关都过才算数。如果三关过不了,说明模型设定本身有问题,该退回去检查是不是漏了容量约束或者成本函数的凸性假定。

6. 怎么验证一个均衡解是真的:最优反应、二阶条件与唯一性

解出一个均衡,和证明它真的是均衡,是两件事。前者是求解器给你的数字,后者需要你自己补验证。我一般会走三步:第一步,把解代回每个人的最优反应函数,看残差是不是接近零;第二步,检查二阶条件,确认一阶条件给的是极大值不是极小值;第三步,从多个不同初值重新求解,看是否收敛到同一个点。

第一步对离散博弈最直接。给定解出来的战略组合,把每个参与人的行动逐个替换成其他可行行动,如果没有任何一个替换能带来正的收益增量,那这个组合就是纯策略纳什均衡。这段检查代码只有十几行,但能挡掉绝大多数“求解器收敛了但解错了”的情况。

def verify_nash(profile, payoff, actions, players, tol=1e-9): """返回每个参与人的有利可图单边偏离;全部为空才是纯策略纳什均衡""" gaps = {} for i, p in enumerate(players): base = payoff[profile][i] better = [] for a in actions[p]: cand = list(profile) cand[i] = a gain = payoff[tuple(cand)][i] - base if gain > tol: # 只记录严格改进 better.append((a, round(gain, 4))) gaps[p] = better return gaps prof = ("低", "低") print(verify_nash(prof, payoff, actions, players)) # {'G1': [], 'G2': []} —— 两方都没有有利可图的偏离 print(verify_nash(("中", "中"), payoff, actions, players)) # {'G1': [('低', 1.0)], 'G2': [('低', 1.0)]} —— 双方都想往低走

tol是数值容差,浮点运算下残差不可能严格为零,设太小会误报,设太大会漏报,1e-9 对一般量级够用。返回空列表表示该参与人没有改进空间;返回非空就说明这个组合不是均衡,列表里的(行动, 收益增量)直接告诉你往哪个方向偏。

连续博弈的验证要换成反应函数残差。以古诺为例,解出 (q1, q2) 之后,把 q2 代进厂商 1 的反应函数算出理论最优产量,和 q1 一比,差值应该在数值精度内。这个检查比看求解器返回的success=True可靠得多——求解器说成功,只代表它没崩,不代表它找到了你要的东西。

最后一层是唯一性。均衡不唯一的时候,“求出解”这件事本身就失去意义,因为你不知道手上这个是哪一个。判断办法是从多个随机初值出发重复求解,如果收敛到不同的点,就要回到模型设定上去找原因:是不是可行域不凸、是不是成本函数非凸、是不是供给函数允许了非线性形状。多数情况下,把模型收紧(线性化、加凸性假定)比在求解器里调参数有效得多。

从那以后我每次跑完寡头模型,都会强制走一遍这三步验证,哪怕解析解已经手推过——因为改参数的时候,最先坏掉的往往不是求解器,而是我自己对模型的假设。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进