首页 / 资讯中心 / 文章详情

LINGO建模实战:从集合语法到求解器选型与灵敏度分析

LINGO建模实战:从集合语法到求解器选型与灵敏度分析 ★ FEATURED ARTICLE
简介LINGO线性与非线性全局优化器是一款功能强大的数学建模优化软件本资源提供其破解安装版本面向工程、经济、金融、运筹学等领域的研究者、工程师及建模竞赛参与者用于求解线性、非线性与整数规划问题。软件内置专门的建模语言用户可通过简洁语句定义变量、目标函数和约束快速搭建大规模优化模型求解器性能强劲支持混合整数规划和结果敏感性分析还提供数据接口与报告生成功能便于多场景建模与对比。压缩包为rar格式大小约15.9MB轻量易获取目前已有650人学习/下载适合从入门到进阶的不同层级用户。借助此资源可直接体验LINGO 11版本的增强求解能力与界面优化结合官方文档和实例在供应链调度、投资组合优化、能源系统设计等实际场景中快速构建最优解方案提升建模与决策效率。1. LINGO 不是计算器它到底在解什么LINGO 这个工具表面看是把数学公式敲进去、点 Solve 就出结果的“计算器”但真正拉开建模差距的是它对集合、数据段和求解器分组这三个设计点的处理。同一道排产题有人用几十行脚本勉强跑通有人用几行集合写法在秒级拿到最优解差别不在数学功底而在建模方式。这篇文章不打算按教科书顺序讲语法而是从建模语法、求解器选型、报告解读到批量联调完整走一遍我在真实项目里验证过的流程。适合手头有优化问题的从业者——供应链调度、生产计划、定价决策都算——也适合想快速验证算法思路的同学。建议开着 LINGO 边看边跑大部分示例改两行数据就能直接复现。2. 集合与数据段把模型从纸面搬进 LINGO 的两种写法2.1 原始集与派生集先把“对谁建模”写清楚LINGO 和直接写 LP/MPS 文件的习惯很不一样。从纸面公式搬到 LINGO第一件事不是写目标函数而是定义集合。集合相当于告诉求解器我要对哪些对象建模型每个对象带哪些属性。以最经典的运输问题为例需求端是 4 个客户供给端是 3 个仓库每个仓库有供应量每个客户有需求量每条仓库到客户的路径有单位运输成本和运输量。MODEL: SETS: WAREHOUSE /W1, W2, W3/: SUPPLY; CUSTOMER /C1, C2, C3, C4/: DEMAND; ROUTE(WAREHOUSE, CUSTOMER): COST, VOLUME; ENDSETS DATA: SUPPLY 30 40 50; DEMAND 20 25 30 45; COST 2 4 5 3 3 5 4 6 4 2 3 5; ENDDATA MIN SUM(ROUTE(I, J): COST(I, J) * VOLUME(I, J)); FOR(CUSTOMER(J): SUM(WAREHOUSE(I): VOLUME(I, J)) DEMAND(J)); FOR(WAREHOUSE(I): SUM(CUSTOMER(J): VOLUME(I, J)) SUPPLY(I)); FOR(ROUTE(I, J): VOLUME(I, J) 0); END这段代码里的三类写法建议记牢。第一原始集定义用名字 /成员/ : 属性成员之间用逗号或空格隔开属性跟在冒号后面一个属性就是一个和集合同长度的参数或变量数组。第二派生集ROUTE(WAREHOUSE, CUSTOMER)表示两个原始集的笛卡尔积展开后共 12 个成员COST和VOLUME都挂在这个派生集上分别读入 12 个成本数据和生成 12 个决策变量。第三FOR和SUM是 LINGO 的循环与求和函数FOR(CUSTOMER(J): ...)表示对每个客户单独生成一条等式约束SUM(WAREHOUSE(I): ...)表示对仓库下标求和。我一般会先在草稿纸上画一张“对象关系图”把集合画成方框、属性画成方框里的行、跨集合的连线就是派生集。这样做直接的好处是写FOR时不容易搞混下标。比如上面第二个FOR里外层循环下标是I仓库内部求和下标是J客户意思是每个仓库发出去的总量等于它的供应量如果把两者位置对调模型含义就从“运出量守恒”变成“运入量等于别的东西”LINGO 不会报警告只有结果跟业务对不上时你才会发现。2.2 数据段与外部文件把参数和模型结构分开上一节的运输问题里数据全写在DATA/ENDDATA段中这是我最推荐的起始方式模型结构一眼能看完改数据也方便。但数据量超过几十个、或者同一套模型要套用到多个区域时就应该把参数挪到外部文件。LINGO 的FILE函数就是干这个的。DATA: SUPPLY FILE(sup_data.txt); DEMAND FILE(dem_data.txt); COST FILE(cost_data.txt); ENDDATAsup_data.txt里只需要按集合成员顺序写数值例如30 40 50文件里可以有换行和空格数值之间不需要逗号。这样模型文件里只剩集合和约束结构数据归数据。我做过的一次模拟项目 X就是同一套库存补货模型把华东、华南、华北三组数据分别存成三个文本跑三次模型一行没改。把数据外置还有一个隐藏优点做参数扫描时你只需要用脚本替换文本里的某个值然后重新求解。后面第 6 章我会给完整的批量做法。如果你想让结果导出成可读文件DATA段里也可以写TEXT(result.txt)把某个集合属性的值按成员顺序写到文本文件里。这个函数我在前面的选址问题里常用至少比从密密麻麻的 Solution Report 里手工复制要稳。2.3 从纸面公式到 LINGO 表达的三个习惯最后补三个我踩过坑练出来的习惯。第一目标函数写在最前面MIN或MAX开头接下来所有等号、不等式、BND都是约束。第二约束尽量加行标签比如FOR(CUSTOMER(J): [DEMAND_BALANCE] SUM(...) DEMAND(J));加标签之后报告里的 Row 编号就会带着名字排查结果时能少一半时间去对行号。第三决策变量和参数不要混在一个集合属性里参数在DATA段赋值变量只声明不赋值如果非要给变量初值可以在DATA段赋但别把初值当业务参数用否则下次换数据时容易漏改。3. 求解器选型LP、MIP、NLP 跑不对时先查这三处3.1 线性模型与整数模型变量类型声明和 Gap 设置LINGO 会根据模型自动分派求解器纯线性连续问题是 LP带整数变量是 MIP目标或约束里有非线性表达式就是 NLP。自动分派省事但很多“跑出来不对”的翻车现场恰恰是因为模型被自动分派到了错误的求解器。最典型的是整数变量没声明模型被当成 LP 解得到小数方案。设施选址问题能同时展示 LP 到 MIP 的差别。MODEL: SETS: PLANT /P1, P2, P3/: FIXED, CAPACITY, OPEN; CUSTOMER /C1, C2, C3, C4/: DEMAND; ROUTE(PLANT, CUSTOMER): TRANSCOST, SHIP; ENDSETS DATA: FIXED 1000 800 1200; CAPACITY 500 400 600; DEMAND 150 180 120 200; TRANSCOST 5 4 6 3 7 3 5 6 4 5 4 7; ENDDATA MIN SUM(PLANT(I): FIXED(I) * OPEN(I)) SUM(ROUTE(I, J): TRANSCOST(I, J) * SHIP(I, J)); FOR(CUSTOMER(J): SUM(PLANT(I): SHIP(I, J)) DEMAND(J)); FOR(PLANT(I): SUM(CUSTOMER(J): SHIP(I, J)) CAPACITY(I) * OPEN(I)); FOR(ROUTE(I, J): SHIP(I, J) 0); FOR(PLANT(I): BIN(OPEN(I))); END关键在最后两行。BIN(OPEN(I))把OPEN声明成 0/1 变量模型才是 MIP倒数第二条容量约束 CAPACITY(I) * OPEN(I)的含义是OPEN为 1 时该厂产能正常生效为 0 时上限变成 0运输量被迫为 0。这个“乘法开关”模式几乎适用于所有带固定成本的选择问题。MIP 跑得慢或者太久不出最优解时先别急着加机器去 Options 里的 MIP Solver 参数看 Dual Gap。Gap 表示当前最好的整数可行解与松弛上界之间的相对差距设成 0 表示一定要证明最优才停代价是分支定界可能跑几个小时设成 1% 通常能在十几分钟内给出工程上可接受的解。我在做某跨平台系统的排产模块时就把 Gap 设成 0.5%业务方完全接受求解时间从 40 分钟压到 3 分钟。要注意如果模型本身只有几千个变量Gap 设大点无所谓一旦变量上十万Gap 开 1% 和开 5% 的耗时差能到 10 倍以上量力设。3.2 非线性模型与全局求解器默认只是局部收敛非线性模型的默认行为容易让人误判。LINGO 碰到非线性目标或约束时默认走 NLP 求解器本质是局部搜索从一个初始点出发沿可行方向逐步改进找到一个“站在附近看确实最优”的解就停。如果模型是凸的局部即全局但现实中定价、选址、排程模型很少有凸的于是局部解就变成一个大坑。一个能快速复现的示例是圆柱储罐用料最小化给定容积 100 立方米求半径 R 和高度 H 使表面积最小。MODEL: ! 目标最小化圆柱储罐的用料表面积 V 100; ! 容量固定 100 立方米 MIN 2 * 3.14159 * R * R 2 * 3.14159 * R * H; BND(1, R, 10); BND(1, H, 50); 3.14159 * R * R * H V; END这个模型手算就能验证把 H 换成 100 / (3.14159 * R^2)目标函数变成一元函数最优在 R 约 2.5、H 约 5.0材料面积大概 118 平方米。默认 NLP 从初值 R1、H20 也能找到这个点因为可行域曲面上这个点是唯一谷底。但如果把目标函数换成带SIN或EXP的生产计划模型谷底可能就不止一个。这时候要在 LINGO 菜单里打开 Options→Global Solver勾选启用全局求解器它会在后台做类似分支定界的过程反复逼近并证明全局最优。代价是时间变量一多、函数一复杂跑几十分钟甚至几天都正常。我一般先把局部解跑出来看结果是否在业务合理区间再用全局求解器验证一次而不是一开始就全局硬算。3.3 三个参数与数值尺度TIMLIM、Scale Model 和线性化选项求解器参数里我最常动的是三个。第一个是时间上限 TIMLIM单位秒设到 300 到 600 很常见它防止 MIP 或全局求解器无限跑下去。第二个是迭代上限 ITERLIM只对单纯形法有意义内点法和分支定界主要看时间。第三个是 Scale Model 选项遇到目标上百万、约束系数只有 0.001 这类数值尺度失衡时勾上它经常会让 Infeasibilities 从 0.01 突然变成 0.000000。原理很简单矩阵里同时存在 1e6 和 1e-4 级的数求解器的内积、求逆精度会吃紧缩放后条件数改善结果立刻稳。另外 LINGO 有自动线性化能力小规模非线性表达式会在预求解阶段尝试线性化但一旦变量超过几百个就别指望它全自动人工引入辅助变量改写反而更快这部分在第 5 章会给改写例子。4. 报告与灵敏度分析从 Solution Report 里读出真实边界4.1 先看懂求解状态和三个关键字段每次求解完弹出的 Solution Report大多数人只扫一眼目标值就关掉。我建议至少看四个地方。首先是报告第一行Global optimal solution found说明已经证明全局最优Local optimum found说明只是局部解Feasible solution found更弱只保证找到可行解。这三者差一个级别业务决策的置信度就差一个档次。其次看Infeasibilities它不为 0 说明模型找不到严格满足所有约束的点这时目标值再好看都不能用。下面是一个简化后的 LP 小案例报告用来解释字段含义不必对应前面的某个具体模型。Global optimal solution found. Objective value: 870.0000 Objective bound: 870.0000 Infeasibilities: 0.000000 Total solver iterations: 5 Variable Value Reduced Cost X1 40.00000 0.000000 X2 20.00000 5.000000 Row Slack or Surplus Dual Price 1 870.0000 1.000000 2 0.000000 7.000000 3 10.00000 0.000000变量区里的Reduced Cost对基变量通常为 0对非基变量表示如果把这个变量强行加进最优解一个单位目标值会恶化多少。Row 区里的Slack or Surplus是约束的剩余量等于 0 说明该约束紧贴边界是真正的限制因素大于 0 说明约束还有余量暂时不影响目标。Dual Price就是影子价格下面单独说。Row 后面的编号对应模型里的约束顺序所以我在第 2.3 节强调要加行标签就是为了让这里的编号能一眼对上约束名。我在实战里的习惯是改动模型后先看求解状态那行字再看 Infeasibilities最后才看目标值。状态不对时数值再漂亮也只是黑匣子输出。4.2 灵敏度 Range 表影子价格能走多远默认报告不会给完整的灵敏度 Range 表。要在求解前把 Options 里 General Solver 的 Dual Computations 设成 Prices Ranges重新求解后才会出现两组区间目标系数变化范围和右侧 RHS 变化范围。目标系数范围表长这样。变量当前系数允许增加允许减少X150.0010.0020.00X240.008.006.00RHS 范围表单独一张。约束当前 RHS允许增加允许减少约束2100.005.008.00约束380.0012.0020.00这两张表回答的问题是我不重新求解的情况下业务参数在多大范围内波动当前这组最优决策仍然是“结构最优”。注意“结构最优”不等于“变量值不变”它只表示最优基的变量集合不变。一旦某个系数的实际增幅超出允许增加最优基会变影子价格会跳到另一个值这时候再去引用原 Dual Price 就作废了。影子价格业务化是最容易翻车的地方。某一次我给一个仓储项目做资源评估报告里约束 2 的 Dual Price 是 7意思是该约束右侧再给 1 个单位目标值能改善 7。业务方很兴奋说多放 10 个货架就多赚 70 万。我拿 Range 表一看这个约束的 RHS 允许增加只有 5第 6 个货架开始 Shadow Price 已经不是 7 了。所以我把影子价格当“边际价值信号”不当“线性外推的乘法口诀表”并且只对它生效的范围负责。4.3 用手工可解案例做回归报告说最优不等于逻辑对还有一层验证是很多人跳过的报告说最优不等于模型建得没问题。我习惯在正式跑大规模数据前先构造一个规模小到能手工算的案例塞进同一套模型确认结果和手算一致。比如运输问题我做过 2 个仓库送 2 个客户的小案例手算最优值再让模型跑对上了才换真实数据。这件事花 10 分钟能挡住一大半“集合写错但报告照出最优解”的隐性 bug。5. 避坑实录五个反复出现的建模翻车场景与处置5.1 整数声明缺失解出 13.7 台设备业务当场不认现象生产计划模型跑完报告里生产量是 13.7 台、开启 0.4 条产线业务方直接拒收结果。原因LINGO 变量默认是连续变量GIN、BIN没写模型被当成 LP 解了。解决凡是决策量必须是整数的声明时给集合属性补一行GIN(PRODUCE(P))或BIN(OPEN(I))。跑完先看报告底部的 Model Statistics核对整数变量个数。我后来养成的习惯是需求评审阶段就把“哪些量必须是整数”写进建模文档避免写代码时漏掉。5.2 集合维度错配Error 72 的锅十有八九在这现象加载模型报 Runtime Error错误码 72指向某一行FOR或SUM。原因下标集合写错了维度。比如在FOR(CUSTOMER(J): ...)内部却用了SUM(WAREHOUSE(J): SHIP(I, J))把客户集合的成员J当成了仓库集合的成员用。解决先定位报错行检查每个循环变量所属的集合。FOR(A(I): ...)里可以安全使用的下标只有I和它所在约束里通过SUM跨集合引用的其他集合下标。一个笨但有效的办法把模型里所有FOR和SUM的下标变量名列出来逐个对照 SETS 定义。另外派生集下标的顺序要和定义时一致ROUTE(WAREHOUSE, CUSTOMER)里第一维是仓库第二维是客户写ROUTE(J, I)却把 I 当仓库、J 当客户就会维度错配。5.3 IF 和 SMAX 把问题拆成非光滑悬崖现象模型能解但换一组合理初值结果跳到一个完全不同的解业务怎么调参数都达不到报告里的“最优”。原因目标或约束里用了IF、SMAX、ABS这类函数可行域被切成不连续的多块非凸不可导默认 NLP 局部搜索只能爬到其中一个谷底。解决把非光滑项改写成线性不等式。比如约束里出现SMAX(0, PRICE(I) - BID(I))可以引入辅助变量OVER(I)替换成下面两条约束。OVER(I) PRICE(I) - BID(I); OVER(I) 0;原来用SMAX的地方直接换成OVER(I)。数学上等价数值上却从“不可导悬崖”变成“两条线性边界”全局求解器才真正能用。类似地IF(COND, A, B)可以分解成一个 0/1 变量加两个大 M 不等式。需要提醒的是大 M 不要拍脑袋给 1e6取“业务上该量级最大可能值的 3 倍”就够太大反而把数值条件数拖坏。5.4 数值尺度悬殊手算满足约束报告却报 Infeasibilities现象目标函数动辄上百万约束系数小到 0.0001报告里 Infeasibilities 显示 0.01可人工逐个约束验算发现都满足。原因模型矩阵的条件数太差求解器在数值容差内判断违反约束产生了“数值伪不可行”。这在小数单位和万元单位混用的成本核算模型里很常见。解决先统一业务单位把元换成万元或把千克换成吨让目标值和系数都落在 1e-2 到 1e4 之间再去 Options 里勾选 Scale Model。我在某物流成本模型里遇到过 Infeasibilities0.03把单箱成本从“元”改成“万元”后直接归零一秒没多算。如果改完还不行重点查是否有同时除以两个量级相差很大的分母的表达式。5.5 局部最优当全局最优同一个模型换初值换结果现象同一个 NLP 模型同事用另一套求解器算出来的目标值明显比我的好换初始点再跑结果又变一个样。原因默认 NLP 是局部搜索非凸问题从不同起点收敛到不同局部解根本没有“唯一答案”。解决重要决策用全局求解器跑一遍同时设时间上限比如 600 秒看它找到的目标值和默认 NLP 是否一致。如果全局求解时间完全不可接受退一步做多起点扫描人工挑 5 到 10 个有业务含义的初始点跑局部 NLP记录每个起点对应的目标值和决策量取业务上最合理的那组并在文档里注明这是“验证过的局部解”不是全局最优。从那以后我对“最优”这个词都加限定语是全局证明还是局部搜索加经验判断。5.6 排查速查表症状最先查的语法/参数本章对应场景解出小数GIN / BIN 声明5.1Runtime Error 72FOR/SUM 的下标集合匹配5.2换初值结果大变IF/SMAX/ABS 非线性改写5.3Infeasibilities 不归零Scale Model、单位归一化5.4报告只有 Local optimum开 Global Solver5.56. 把 LINGO 嵌进工作流Excel 联调与批量参数扫描6.1 用 OLE 让 Excel 当数据界面Excel 里先给区域命名LINGO 的OLE就能按名字读写。数据段读入参数CALC 段写回结果业务方改 Excel 就行不碰模型文件。DATA: SUPPLY OLE(C:\supply_plan.xlsx, supply); DEMAND OLE(C:\supply_plan.xlsx, demand); COST OLE(C:\supply_plan.xlsx, cost); ENDDATA CALC: OLE(C:\supply_plan.xlsx, ship_out) SHIP; ENDCALC注意路径里别用中文名Excel 文件要事先打开或在名称管理器里确认区域存在。第一次联调失败九成是 Excel 里的命名区域尺寸和集合成员数不一致。提示Excel 命名区域的尺寸必须和集合成员数一致否则 OLE 读入会在运行时报告维度不匹配先把区域重新框选再命名一次。6.2 参数扫描改文件、跑求解、读结果需要做敏感性扫描时我不喜欢手动改 LINGO 文件。用一个模板文件template.lng把要扫描的参数写成占位值再用 Python 循环替换调lingo.exe一次跑一个版本。import subprocess import re rates [0.05, 0.10, 0.15] for i, rate in enumerate(rates): with open(template.lng, r, encodingutf-8) as f: text f.read() text text.replace(DISCOUNT 0;, fDISCOUNT {rate};) with open(frun_{i}.lng, w, encodingutf-8) as f: f.write(text) subprocess.run([lingo.exe, frun_{i}.lng], capture_outputTrue, textTrue, timeout300) with open(lingsol.txt, r, encodingutf-8) as f: out f.read() obj re.search(rObjective value:\s([\d.]), out).group(1) print(frate{rate}, objective{obj})这套流程我在某定价模型上跑过 20 组参数全部自动落盘不用守着求解器点按钮。有一点要提醒不同 LINGO 版本命令行输出的重定向行为不完全一样如果lingsol.txt没生成先在 Options 里把 Output Level 调到 Verbose再确认当前目录权限。把这一套从集合建模、求解器选型、报告解读到批量联调走通之后你会发现大部分卡壳不是数学不会而是建模习惯没改过来。那次我把 Gap 设成 0 硬跑十万变量 MIP四个小时没出结果从那以后我每建一个新模型都强制先过三件事变量整数声明写完没、求解器状态是 Global 还是 Local、Range 表里要用的影子价格在不在允许范围内。这几个习惯救了我很多次希望也能帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站