
简介Matlab模拟退火工具箱面向需要实现全局优化算法的学生、科研人员与工程开发者围绕模拟退火在组合优化中的落地提供可复用的脚本与说明文档适合具备MATLAB基础、希望快速复现算法流程的初中级用户。压缩包共82个文件约211KB其中71个.m函数与示例脚本覆盖初始化、能量计算、Metropolis接受准则、温度调度及多种降温策略10个.txt文件提供模板、版权与使用说明另有1份.pdf详解函数用法、参数设置和案例便于对照查阅。已有1317人学习下载说明其在教学与项目实践中具有一定参考价值。读者可从中获得较完整的算法实现骨架、可调参数示例和结果可视化辅助用于旅行商、路径规划、作业调度等问题的快速试验并借助文档理解各环节的衔接与调参思路。1. 先在干净目录里解压不要急着双击 demo拿到 MATLAB 模拟退火工具箱含 pdf之后大多数人的第一反应是解压、双击 demo、看着曲线跑起来然后关掉去改自己的工程。我以前也这么干过结果在一台已经装过两个优化包的机器上后加进搜索路径的同名函数覆盖了原来的入口文件第二天打开工程直接报“输入参数太多”。模拟退火在课程设计、参数寻优、排产调度这类场景里使用频率极高装不好并不会立刻报错它会安静地让你拿到一条看起来已经收敛、实际上目标函数已经被替换掉的曲线。这篇记录写给第一次装这套工具箱、又不想动到既有工程的人先确定版本、依赖、路径这三件事再谈温度参数怎么配。整个过程不复杂麻烦的是每一步都有“看起来成功了”的假象。2. 环境与版本MATLAB R2018b 起步和三个容易忽略的硬性条件2.1 MATLAB 版本门槛与操作系统差异这套工具箱的核心是一个纯 .m 实现的主循环加几个测试函数不依赖编译型 MEX所以理论上任何能打开 .m 文件的 MATLAB 都能读。真正卡人的是语言特性包里如果用了 string 类型、隐式扩展、isfolder、contains这些函数R2016b 以前的版本会直接报未定义。我一般把下限卡在 R2018b理由是isfolderR2017b 引入、containsR2016b和dir的**递归通配R2016b在 R2018b 上都已经稳定装完之后不需要为了兼容去改源码。操作系统这一层Windows 上最容易出问题的是路径解压到“桌面\新建文件夹 (2)\模拟退火工具箱”这种带空格和中文的目录addpath能加进去但调用时某些老版本在拼接路径时会截断报的错还特别绕。Linux 上要注意 .m 文件的权限位从压缩包解出来如果丢了读权限MATLAB 会把它当成不存在。macOS 上少见但确实存在的是解压工具自动转换行尾导致 .m 文件里出现 CR 混 LF注释行整段变灰。% env_probe.m —— 装之前先摸一遍底 v ver(matlab); fprintf(MATLAB: %s 平台: %s\n, v.Release, computer(arch)); % 三个包内常用函数是否存在缺哪个就说明版本太老 need {isfolder, contains, strsplit}; for k 1:numel(need) fprintf(%-10s %d\n, need{k}, exist(need{k}, file) 2 || exist(need{k}, builtin) 5); endver(matlab)返回的是结构体Release字段形如(R2018b)computer(arch)给出win64、glnxa64、maci64这类标识排查跨平台问题时比ispc更有用。三个函数的存在性判断用exist的返回值2 表示 .m 文件5 表示内建两个都不满足就说明版本需要往上抬。2.2 包里到底有哪些文件pdf 该先读哪几页这类工具箱的压缩包解开后结构大同小异一个主入口 .m 或一组功能相近的 .m、一个demo或example脚本、几个测试函数Rastrigin、Sphere、TSP 数据这类、再加上那份 pdf 说明。先用dir把结构列清楚比直接翻 pdf 快得多。root D:\matlab_tools\sa_toolbox; f dir(fullfile(root, **, *.m)); for k 1:numel(f) fprintf(%-30s %8d bytes %s\n, f(k).name, f(k).bytes, f(k).folder); enddir带**会递归所有子目录bytes字段能帮你一眼认出哪个是主函数通常最大、哪个是 demo最小且名字带 example。这一步的意义在于pdf 里写的函数名和你手上的 .m 文件名对不上的情况相当常见尤其是经过几手转存的老包里面可能同时躺着新旧两版入口。pdf 不必通读先看三处函数索引页确认入口函数名和参数个数、参数默认值表确认 T0、alpha 这些的出厂值、示例代码页确认调用语法是位置参数还是结构体。这三页看完再回头看算法描述部分尤其是 Metropolis 判据那段后面调参的时候要靠它判断你的接受率是不是正常。提示pdf 里的示例代码直接复制到编辑器里执行最快能暴露“文档版本和源码版本不一致”的问题。2.3 装前检查清单与路径备份真正动手加路径之前先把当前的搜索路径存一份。MATLAB 的路径是全局的一次savepath写错影响的不是这一次会话而是之后每一次启动。回滚成本低得可以忽略但很多人就是懒得做。% 路径快照 pathdef 备份 p0 strsplit(path, pathsep); fprintf(当前搜索路径 %d 项\n, numel(p0)); save(path_snapshot.mat, p0); % 会话级快照 pd fullfile(matlabroot, toolbox, local, pathdef.m); if isfile(pd) copyfile(pd, [pd .bak], f); % 写坏了直接覆盖回来 fprintf(pathdef 已备份: %s.bak\n, pd); endpath返回的是用pathsep连接的长字符串拆成元胞才好统计和比对。pathdef.m是 MATLAB 启动时加载的路径定义文件位置随安装方式不同可能在matlabroot\toolbox\local或用户目录下找不到就跳过——会话级快照已经够用重开 MATLAB 就恢复默认。还有一条清单项容易被跳过确认有没有现成的同名函数。which -all会列出搜索路径上所有同名文件装之前跑一次装之后跑一次两次结果对比就知道新增了什么、覆盖了什么。3. 路径配置addpath、savepath 与 startup.m 的三种取舍3.1 解压位置与目录命名规则路径配置的错误有一半是解压位置造成的。我给这套工具箱定的规矩是放到一个全英文、无空格、层级不超过三级的目录例如D:\matlab_tools\sa_toolbox。理由不是 MATLAB 不支持空格而是路径一旦进入genpath、savepath和第三方脚本的字符串拼接任何一个环节没加引号就会断在空格上报错信息还指不到源头。Linux 和 macOS 上对应的做法是~/matlab_tools/sa_toolbox避免放到/tmp重启丢失或系统目录权限问题。如果这套工具箱要在多台机器上用把整个目录放进版本控制用相对路径的startup.m去挂载比每台机器手动addpath可靠得多。3.2 addpath 的写法与 genpath 的坑addpath是最直接的写法但一次只加一个目录。很多人图省事直接addpath(genpath(root))这行命令会把root下面所有层级都塞进搜索路径包括你根本不想暴露的backup、old、.git、测试数据目录。后果是同名函数冲突以及which输出一大串你从没调用过的东西。% add_paths_sa.m —— 只加需要的子目录不用 genpath root D:\matlab_tools\sa_toolbox; if ~isfolder(root) error(目录不存在检查解压位置: %s, root); end subs {, src, demo, testfun}; % 按包内实际结构改这一行 cur strsplit(path, pathsep); for k 1:numel(subs) p fullfile(root, subs{k}); if isfolder(p) ~any(strcmp(cur, p)) addpath(p); fprintf(已添加: %s\n, p); end endsubs里的空字符串代表根目录本身fullfile会正确处理成root。去重那一步用strcmp逐项比对比contains(path, p)稳——contains遇到sa_toolbox和sa_toolbox_old这种前缀相同的目录会误判成已存在结果新目录根本没加进去你还以为加过了。如果包里的主函数散落在多个子目录、互相调用那就得老老实实把所有含 .m 的目录列全。快速列出来的办法是对 2.2 那次dir的结果取folder字段去重。3.3 用 startup.m 固化路径再用 which -all 验证addpath只对当前会话生效关掉 MATLAB 就没了savepath写进pathdef.m永久生效但会污染全局换机器或者包升级之后容易留下指向已删除目录的脏路径。我的取舍是开发机上用startup.m固化临时对照用addpath。startup.m要放在userpath指向的目录下MATLAB 每次启动会自动执行它。userpath返回的字符串末尾有时带路径分隔符直接fullfile会拼出双反斜杠先清一下。% 生成/编辑 userpath 下的 startup.m up userpath; up regexprep(up, [\\/;:]$, ); % 去掉尾部可能的分隔符 fprintf(userpath %s\n, up); fid fopen(fullfile(up, startup.m), a); fprintf(fid, \n%% 模拟退火工具箱路径\n); fprintf(fid, addpath(D:\\matlab_tools\\sa_toolbox);\n); fprintf(fid, addpath(D:\\matlab_tools\\sa_toolbox\\src);\n); fclose(fid);用a追加模式而不是覆盖避免把已有的startup.m内容清掉——这一条是踩过坑之后才改的习惯。路径加完验证不要靠“demo 能跑”要靠which -all和path的组合% 确认入口函数解析到了你刚加的目录 w which(sa_opt, -all); % 把 sa_opt 换成 pdf 里写的入口函数名 disp(w);which -all返回元胞数组第一项是 MATLAB 实际会调用的那个文件。如果它指向的不是sa_toolbox下面说明搜索路径顺序有问题新加的目录排在后面被老的同名函数盖住了。这时候要么用rmpath清掉旧的要么调整addpath的-end/-begin选项。3.4 pdf 签名与源码签名对不上时怎么核对pdf 里写的调用形式通常是[x,f] sa_opt(fun, x0, opt)但源码里可能是function [x,f,hist] sa_opt(fun, x0, lb, ub, opt)。参数个数不一致调用就报“输入参数太多”。核对办法是直接读函数声明行别猜。% 抓入口函数的第一行声明和 pdf 对照 fn which(sa_opt); fid fopen(fn, r); line fgetl(fid); while ischar(line) isempty(regexp(line, ^\s*function, once)) line fgetl(fid); end fclose(fid); disp(line);fgetl逐行读到function开头那行为止regexp的once保证只找第一个匹配。拿到声明行之后位置参数的顺序、可选参数的默认值、输出参数个数一目了然。包里如果同时存在sa_opt.m和sa_opt_v2.m两个都读一遍选和 pdf 描述一致的那个把另一个挪出搜索路径。4. 参数配置初始温度、降温系数与 Markov 链长的取值逻辑4.1 参数结构体与默认值的来源这类工具箱的参数传递方式有两种位置参数和结构体。结构体更好管因为加参数不用改调用处。下面这份是我在多个包里都验证过能对应的字段名实际以你手上 pdf 的参数表为准字段名对不上就用 3.4 的方法读源码。opt struct(); opt.T0 1000; % 初始温度见 4.2 的取值办法 opt.Tf 1e-3; % 终止温度低于它退出外循环 opt.alpha 0.95; % 降温系数T T * alpha opt.L 200; % 每个温度下的内循环次数Markov 链长 opt.maxIter 5000; % 兜底迭代次数防止 alpha 接近 1 时跑不完 opt.seed 20240101; % 随机种子见 4.4 opt.verbose true; % 是否打印每轮最优值Tf和maxIter是两道保险任意一个先满足就退出。很多人只设Tfalpha写成 0.999结果外循环要跑几千轮中间去泡了杯咖啡回来发现还在第一个测试函数上。加maxIter不优雅但实用。4.2 三个核心参数的经验取值初始温度定得太低第一次迭代就几乎全拒等于在最优点附近随机游走定得太高前几百轮全接受退化成随机搜索。一个可操作的办法是先随机采一批解算出目标值的标准差T0取这个标准差的 5 到 10 倍。参数常用取值偏大的后果偏小的后果T0目标值标准差的 5~10 倍前期全接受浪费时间跳出局部最优的能力丧失alpha0.90 ~ 0.99收敛慢迭代数暴涨降温过快卡在局部解L50 ~ 500随维数增加单温度耗时长每温度采样不足统计意义弱Tf1e-3 ~ 1e-6迭代数增加提前退出未充分收敛L和维数挂钩的粗糙经验是L ≈ 20 * nn 为变量维数二维问题取 40 到 100 就够十维以上再往 200 以上加。这个数不是理论最优但它能保证每个温度点上马氏链足够长Metropolis 的接受率统计不会因为样本太少而剧烈抖动。4.3 目标函数的写法与句柄传递目标函数写得慢整个 SA 就慢因为它是被调用次数最多的函数。三条硬要求输入按行向量处理、返回标量、不要在函数里画图。function f obj_rastrigin(x) % 输入 x 为 1×n 行向量返回标量目标值 x x(:); % 强制成行向量防止列向量引发维度错 n numel(x); f 10*n sum(x.^2 - 10*cos(2*pi*x)); % 标准 Rastrigin全局最优在原点值 0 endx(:)那一行看着多余其实是为了兼容调用方传列向量的情况——sum对列向量求和的结果是标量没问题但包内如果有x - best这类运算行列混用会触发隐式扩展算出一个矩阵再转标量速度掉一个量级还查不出原因。带额外参数时用匿名函数包一层a 3; fun (x) obj_with_param(x, a); % 把 a 固定在句柄里匿名函数捕获的是定义时的变量值a之后再改不影响句柄这一点和 Python 的默认参数陷阱类似写循环里生成多个句柄时尤其要注意。4.4 随机种子与结果复现不固定随机种子两次运行的收敛曲线能差出好几倍调参根本无从谈起。固定种子用rng但别只写rng(42)就完事——它会改掉当前会话的全局随机流影响调用你这套代码的其它脚本。标准做法是存一份、设种子、跑完还回去。oldState rng; % 保存当前随机流状态 rng(opt.seed, twister); % 指定梅森旋转跨版本行为一致 [xbest, fbest] sa_opt(fun, x0, opt); rng(oldState); % 恢复别污染调用方第二个参数twister别省。不同 MATLAB 版本默认的生成器可能不同显式指定之后同一份种子在 R2018b 和 R2023a 上跑出来的随机序列才一致。如果目标函数里用了parfor全局随机流会被各 worker 分割rng(seed)不再保证结果可复现得改成在循环内部用RandStream给每个 worker 单独建流。5. 避坑记录五条装完跑不通的现场5.1 报“未定义函数或变量”但which明明找得到现象命令行里which sa_opt有输出demo一跑就报未定义。原因which查的是当前会话的搜索路径demo 脚本里可能自己带了一段cd或者addpath把工作目录切走了另一种情况是主函数调用了子目录里的辅助函数而你只加了根目录没加src。解决用which(sa_opt,-all)看完整列表再在 demo 脚本开头加一句disp(pwd)确认真实工作目录。子目录用 3.2 的脚本章节逐个加不要用genpath一把梭。5.2 调用报“输入参数太多”pdf 和源码版本对不上现象完全照 pdf 示例抄的调用报参数个数错。原因包里同时存在新旧两版入口文件which解析到了旧的那份或者 pdf 是上一版文档源码已经改过签名。解决按 3.4 读出function声明行和 pdf 的示例逐参数比对。确认版本后用rmpath把旧目录移出搜索路径别直接删文件——万一新版本有问题还能退回去。5.3 每次运行结果都不一样没法判断调参有没有效果现象同一个参数组合跑三次最优值差两三个数量级。原因没固定种子或者目标函数内部自己调了rand而 SA 主循环也在用全局流两边互相干扰。解决主脚本开头按 4.4 的写法设种子并保存旧状态。目标函数里如果需要随机扰动单独建一个RandStream传进去和主循环的流隔离开。5.4 跑十分钟没有任何输出以为死循环现象命令行光标一直闪没有进度打印CPU 占用接近满。原因verbose没开或者目标函数里有plot、drawnow、pause每次调用都刷新图窗实际计算只占一成时间还有一种少见情况是alpha写成 0.9999 且Tf设成 1e-10外循环上万轮。解决先把opt.verbose true打开看轮次再在目标函数里临时加计数器和tic/toc定位是计算慢还是绘图慢。绘图挪到循环外的回调里或者用drawnow limitrate。5.5 中文注释变乱码pdf 里的公式符合不上源码现象打开 .m 文件注释全是问号或方块pdf 里的变量名和源码里的对不上。原因老包的 .m 文件是 GBK 编码R2020a 之后 MATLAB 默认按 UTF-8 读写编码不匹配就乱码变量名对不上通常是作者在某一版改过命名pdf 没同步。解决先用feature(DefaultCharacterSet)确认当前会话编码读文件时显式指定fid fopen(sa_main.m, r, n, GBK); % 第四参数指定源文件编码 txt fread(fid, *char); fclose(fid);确认内容无误后另存为 UTF-8别在原文件上反复转码转两次就彻底救不回来了。变量名对不上的以源码为准pdf 只当算法说明看。6. 装完怎么验证三个测试函数跑通才算数验证不要只看 demo 的图好不好看图能画出来只说明没报错不说明算法是对的。我用三个有解析最优解的测试函数做验收每个都要求最终值和理论值的误差在容差内并且三次不同种子下结果稳定。% verify_sa.m —— 三个用例全部通过才算装好 cases { sphere, (x) sum(x.^2), zeros(1,2), -5.12*ones(1,2), 5.12*ones(1,2), 1e-4 rastrigin, obj_rastrigin, zeros(1,2), -5.12*ones(1,2), 5.12*ones(1,2), 1e-2 rosenbrock, (x) 100*(x(2)-x(1)^2)^2 (1-x(1))^2, ones(1,2), -2*ones(1,2), 2*ones(1,2), 1e-2 }; opt struct(T0,1000,Tf,1e-3,alpha,0.95,L,200,maxIter,5000,verbose,false); for c 1:size(cases,1) name cases{c,1}; fun cases{c,2}; x0 cases{c,3}; lb cases{c,4}; ub cases{c,5}; tol cases{c,6}; best inf; for t 1:3 % 三个种子各跑一次看稳定性 rng(20240101 t, twister); [~, f] sa_opt(fun, x0, lb, ub, opt); % 按你包里的实际签名改这一行 best min(best, f); end fprintf(%-12s 最优值 %10.4e 理论值 %8.4f %s\n, ... name, best, fun(x0*0 (c3)), string(best tol abs(fun(zeros(1,2))))); end三个用例的分工是Sphere 是单峰凸函数用来验证“能收敛”跑不到 1e-4 说明降温策略或内循环长度有问题Rastrigin 是多峰用来验证“能跳出局部最优”卡在 10 以上基本可以断定接受率算错了或者T0太低Rosenbrock 是窄长谷用来验证“步长和维度的匹配”它最容易暴露坐标更新范围写死的问题。每个用例跑三个种子取最好值是为了区分“参数不合适”和“运气不好”。还有两个辅助检查值得一起做。一是画收敛曲线横轴是外循环轮次、纵轴是当前最优值正常的曲线应该是台阶状下降如果从头到尾一条直线说明接受率是 0如果剧烈震荡回升说明最优解的记录逻辑写错了。二是打印接受率exp(-delta/T)在高温段应该接近 1低温段趋近 0接受率一直是常数就是温度没降下去。从那以后我每次换机器装这类工具箱都会先跑这三个用例再动工程代码宁可多花十分钟也不愿在两周后对着一份结果说不清来源的报告。希望帮到你。本文还有配套的精品资源点击获取