ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Windows批处理驱动MATLAB:参数扫描批量仿真自动化实战

Windows批处理驱动MATLAB:参数扫描批量仿真自动化实战 这段时间给一组振动仿真做参数扫描频率取了5个值、幅值取了3个值加起来一共15组。如果按老办法来就是手动改一次参数、点一次运行、等结果出来再记数据15组下来人基本就废了而且特别容易出错。后来我干脆写了一批Windows批处理脚本让系统自己循环调用MATLAB、不断更改参数把15组全部跑完日志自动留底。这套做法解决的不只是“少点几次鼠标”的问题它把参数扫描、批量仿真、结果归档变成了一条龙操作。如果你也需要在Windows下批量跑MATLAB仿真又不想为此额外装Python或搞一套复杂的调度框架那这篇内容应该正合你的胃口。我需要提前说明一点下面写的所有脚本和命令都是我在实际项目里用过的方案有些坑是反复踩过之后才总结出来的。批处理虽然看起来很“老派”但它在Windows环境下的零依赖特性至今仍然是做批量仿真最省事的调度工具之一。1. 先想清楚批处理在这个场景里到底解决什么问题1.1 手工调参跑仿真看着简单其实很坑很多人刚开始做参数扫描时第一反应是手动改代码。一个.m文件打开找到参数定义那几行改成新值保存切到MATLAB窗口按F5等仿真跑完再手动把结果记到Excel里。参数组合少的时候还好一旦超过10组这个流程就变得非常折磨人。我遇到过几个实际痛点。第一是“忘改回参数”。有一次我连续跑了好几组最后整理结果时才发现有一组使用了错误参数之前跑的全废了必须重来。第二是“跑不完”。一组仿真如果耗时几分钟15组就是半个多小时人不可能一直盯着。有时候想去吃个饭又不放心程序走也不是留也不是。第三是“记录容易出错”。手工记录结果时很容易把参数组合和结果对应错尤其是参数值相近的时候比如频率5和10手一抖就写串行了。这些问题本质上不是“我不够细心”而是手工操作和批量任务天然不匹配。参数扫描这种事标准解法就是让机器自己循环。1.2 批处理适合当“调度者”MATLAB专注计算你可能会问为什么不用MATLAB自己写个for循环把所有参数组合一次性跑完这当然可以我自己也这么干过。但有一种情况很常见仿真脚本本身就已经很复杂可能需要初始化一堆模型、加载数据、调外部工具把它完整地塞进一个巨大的循环里运行中途一旦某个参数组合让脚本报错整个循环就停在那里前面的成果很难保留。批处理的定位不是替代MATLAB而是当“调度者”。它负责三件事循环遍历所有参数组合每次调用一个全新的MATLAB进程为每个组合生成独立的日志、独立的结果目录。这样做的好处是隔离性极强。每一组参数都从干净的MATLAB状态开始上一组哪怕崩溃了也不影响下一组。批处理按照脚本逻辑继续往下走或者把错误记录下来之后继续跑下一组。这种模型非常适合参数之间完全独立、互不影响的扫描任务。有人会说那直接用Python的subprocess岂不是更优雅。确实可以但现实里很多实验室、工作站的Windows机器上并没有现成的Python环境装一个又涉及权限、网络、环境变量一堆事情。而批处理是Windows自带的任何一个环境都能跑双击就能用。对于“把参数扫描跑起来”这个朴素目标批处理反而是最低门槛的方案。1.3 三种参数传递方案先做个对比批处理控制MATLAB之后核心问题就变成了“参数怎么传进去”。我在实际项目中用过三种方式各有适用场景方案实现思路优点风险/限制推荐场景命令行参数传值批处理把参数拼在-batch的调用语句里最简单直接不生成多余文件适合少量参数参数多了之后命令行长且难维护特殊字符容易踩坑参数个数少、循环结构清晰的扫描参数文件传值批处理为每个case写一个参数文件MATLAB启动后读取参数数量不受限制可读性好便于事后核查多一步“生成文件读文件”的IO操作文件编码要统一参数多、参数结构常变、需要留档动态生成临时脚本批处理把参数值写进一个临时的.m文件再调用该脚本兼容老版本MATLAB迁移改动小中文注释容易乱码临时文件需要清理MATLAB版本较老不支持新调用方式或脚本封装困难这三种方案没有绝对的优劣关键是匹配自己的场景。后面第3章我会把每种方式的写法完整拆开讲一遍。2. 批处理调用MATLAB命令选型和环境准备2.1 用 -batch 而不是 -r能省掉一堆麻烦很多老教程里写的都是matlab -r run_test这种调用方式。-r本身没问题但它的默认行为是执行完命令后不退出MATLAB窗口会一直挂着。你在批处理里用了-r经常发现批处理卡在某个地方不往下走就是因为MATLAB跑完了但进程还在。从R2019a开始MATLAB提供了-batch参数它的行为正好解决了这个痛点启动MATLAB、执行指定代码、完成后自动退出。更关键的是-batch模式会把退出码设置好脚本执行成功返回0执行出错返回非0值这样批处理就能根据退出码判断这次运行是否成功决定是继续下一组还是停下来报警。所以我的建议是能用-batch就用-batch。如果你必须用老版本MATLAB也可以退而求其次用-wait -nosplash -nodesktop -r run_test; exit这种方式让批处理等待MATLAB执行完再退出。注意-r后面的代码要加exit否则批处理会一直等。2.2 让系统能找到 matlab 命令批处理里直接写matlab -batch前提是matlab.exe所在目录已经在系统PATH里。正常安装MATLAB时并不一定会自动加PATH所以第一步先确认一下。在命令行窗口敲where matlab如果能输出matlab.exe的路径说明没问题。如果提示“找不到文件”你就有两个选择一是手动把bin目录加进PATH二是在批处理脚本里写死完整路径。考虑到批处理脚本要尽量一次配置、到处复用我更推荐第二种set MATLAB_EXEC:\Program Files\MATLAB\R2022b\bin\matlab.exe这里有两处细节容易踩坑。第一路径里如果有空格一定要用引号包住整个路径第二set赋值时等号两侧不要加多余空格否则变量名或值会带上空格调用时就会报错。我习惯写set VARvalue把引号加在变量名和值外面这样即使值尾部有空格也不会被吞掉。2.3 循环里的延迟变量setlocal enabledelayedexpansion写批处理只要涉及for循环十有八九会遇到“变量展开”问题。简单说批处理在解析一段带括号的复合命令时会把代码里所有的%变量%一次性展开成当时的数值而不是每执行一行再重新读取。所以你在循环里给变量赋值紧接着用echo %var%去查看看到的往往还是上一次的旧值。解决办法是开启延迟变量展开并把取值符从%改成!echo off setlocal enabledelayedexpansion for %%i in (1 2 3) do ( set basecase_%%i echo !base! ) endlocal这里setlocal enabledelayedexpansion是开关!base!是延迟读取。我在写参数扫描脚本时几乎每一版都会用这个特性比如动态生成输出目录名、动态拼接日志路径都必须靠!变量!才能拿到循环内的新值。如果你在脚本里看到某个case的日志目录永远都一样比如全部输出到case_而不是case_1、case_2那不用怀疑就是变量展开的问题。2.4 判断MATLAB是否跑成功errorlevel批处理判断上一次命令是否成功靠的是errorlevel。对于MATLAB的-batch调用正常完成时退出码是0脚本运行出错时退出码非零。因此可以在每次调用后做判断%MATLAB_EXE% -batch vib_sim(5, 0.5) case.log 21 if !errorlevel! neq 0 ( echo 出错了退出码是 !errorlevel! )在for循环内部判断时同样要用!errorlevel!而不是%errorlevel%原因和延迟变量一样。另外注意重定向写法 case.log 21可以把MATLAB在命令行窗口输出的内容连同错误信息一起写进文件这个动作在批量跑批时非常重要。没有它一旦某个case报错你看到的只是一个闪过的命令行窗口完全不知道具体错在哪里。3. 实操三种参数传递方式怎么选、怎么写3.1 方式一命令行参数直接传给MATLAB函数如果参数数量少而且参数组合结构简单我首选命令行传参。具体思路是在MATLAB里写一个函数参数作为函数入参批处理循环时把参数值拼进调用字符串。比如MATLAB端定义function vib_sim(freq, amp) % 仿真主体 fprintf(freq%g, amp%g\n, freq, amp); % do something end批处理端这样调用echo off setlocal enabledelayedexpansion set MATLAB_EXEC:\Program Files\MATLAB\R2022b\bin\matlab.exe for %%f in (1 2 5 10 20) do ( for %%a in (0.1 0.5 1.0) do ( echo 开始运行 freq%%f amp%%a %MATLAB_EXE% -batch vib_sim(%%f, %%a) log_f%%f_a%%a.txt 21 ) ) endlocal这里有两个细节。第一for %%f in (...)后面的%%f是for循环变量批处理脚本文件里写两个百分号如果直接在命令行手敲就写一个百分号。第二%%f和%%a在传给-batch时会先展开成具体数值所以MATLAB收到的就是vib_sim(5, 0.5)这样的完整调用。这个方式还有个变体如果参数本身不是整数值比如频率需要从1到10取5个对数间隔点批处理里不方便直接列出来可以用for /L生成整数索引然后在拼接时做换算for /L %%k in (1,1,5) do ( %MATLAB_EXE% -batch vib_sim(10^(%%k/2), 0.5) )注意这里10^(%%k/2)是字符串拼接MATLAB启动后会执行对应的数学表达式。这样就能绕开批处理不支持浮点步进的问题。3.2 方式二参数文件传参适合参数多的情况当参数数量超过五六个再往命令行里拼就会变得非常痛苦。命令行长、容易在转义时出错而且参数含义不直观。这时候我习惯改用参数文件。思路也很简单批处理在循环里为每个case生成一个独立的参数文件比如case_1.param文件内容就是一组键值对。MATLAB启动后读这个文件解析出参数值。批处理生成参数文件echo off setlocal enabledelayedexpansion set MATLAB_EXEC:\Program Files\MATLAB\R2022b\bin\matlab.exe for %%f in (1 2 5 10 20) do ( for %%a in (0.1 0.5 1.0) do ( set OUToutput\f%%f_a%%a if not exist !OUT! mkdir !OUT! !OUT!\params.ini echo freq%%f !OUT!\params.ini echo amp%%a !OUT!\params.ini echo notesscan case %MATLAB_EXE% -batch read_param_and_run(!OUT!\params.ini, !OUT!) !OUT!\run.log 21 ) ) endlocalMATLAB端对应的读取函数function read_param_and_run(paramfile, outdir) fid fopen(paramfile, r); if fid -1 error(cannot open %s, paramfile); end raw textscan(fid, %s%s, Delimiter, ); fclose(fid); % 生成参数名到值的映射 params containers.Map(); for i 1:length(raw{1}) parts strsplit(raw{1}{i}, ); if length(parts) 2 params(parts{1}) str2double(parts{2}); end end freq params(freq); amp params(amp); % 调用真正的仿真函数 vib_sim(freq, amp, outdir); end这里有个比较实际的好处以后想加参数批处理里加一行echo xxxyyy params.iniMATLAB侧在读取部分加一行映射就行不用改动整个调用逻辑。而且每个case都保留了一份参数文件过几个月回来看结果依然能清楚知道那组结果对应的完整参数不用靠记忆猜。3.3 方式三动态生成临时.m脚本老版本兼容方案有些极端情况比如MATLAB版本太老没有-batch或者函数封装遇到困难就可以用“动态生成临时脚本”的方式。批处理把参数值写进一个临时.m文件再调用matlab -wait -r run_temp去执行。临时脚本可以用echo一行一行生成echo off setlocal enabledelayedexpansion set OUTtemp_case if not exist !OUT! mkdir !OUT! !OUT!\run_temp.m echo freq 5; !OUT!\run_temp.m echo amp 0.5; !OUT!\run_temp.m echo vib_sim(freq, amp); C:\Program Files\MATLAB\R2022b\bin\matlab.exe -wait -nosplash -nodesktop -r cd(!OUT!); run_temp; exit这种方式的优点是直观临时脚本本身就是一个合法的MATLAB脚本你甚至可以先用记事本打开检查一遍再运行。缺点也很明显如果脚本里有中文注释很容易因为编码不一致出现乱码所以临时脚本里我尽量不用中文echo输出内容如果包含、|、、等特殊字符批处理会理解错需要加^转义比如echo set x1 ^^ y2临时文件需要在最后清理否则残留多了会污染工作目录。一般情况下我还是推荐前两种方式这个方案作为“最后的保底手段”来用。3.4 三种方式怎么选我给一个简单决策参考参数少于等于3个直接用命令行传值参数在3到8个之间或者你希望每个case有独立参数留档用参数文件整个项目是拿老版本MATLAB跑且脚本不方便改成函数式调用那就用临时脚本。强调一点无论选哪种方式尽量让每一个case的结果和日志都落在独立的目录里这是整套方案稳定性的基础。后面所有经验和技巧都建立在“一个case一个目录”这个前提上。4. 完整实战15组双参数扫描自动跑完4.1 任务定义和目录结构用一个具体的例子把这些内容串起来。假设我在做某个结构的振动响应分析需要扫描激励频率f和幅值A两个参数频率f1、2、5、10、20 Hz幅值A0.1、0.5、1.0 mm总共15个参数组合。每个组合要独立输出一个结果文件、一个运行日志、一份参数记录。目录结构规划成这样D:\sim\vibration │ run_scan.bat │ vib_sim.m │ └─ output ├─ f01_a0.1 │ ├─ params.ini │ ├─ result.mat │ └─ run.log ├─ f01_a0.5 │ └─ ... └─ ...目录名里直接包含参数值好处是只看文件系统就能定位到某个case不需要打开日志去翻。4.2 批处理脚本 run_scan.bat 完整代码echo off setlocal enabledelayedexpansion set MATLAB_EXEC:\Program Files\MATLAB\R2022b\bin\matlab.exe set BASE_DIRD:\sim\vibration cd /d %BASE_DIR% if not exist output mkdir output for %%f in (1 2 5 10 20) do ( for %%a in (0.1 0.5 1.0) do ( set OUToutput\f%%f_a%%a if not exist !OUT! mkdir !OUT! !OUT!\params.ini echo freq%%f !OUT!\params.ini echo amp%%a echo [%date% %time%] start freq%%f amp%%a run_log.txt %MATLAB_EXE% -batch vib_sim(%%f, %%a, !OUT!) !OUT!\run.log 21 set ERR!errorlevel! if !ERR! equ 0 ( echo [%date% %time%] done freq%%f amp%%a err0 run_log.txt ) else ( echo [%date% %time%] error freq%%f amp%%a err!ERR! run_log.txt ) ) ) echo All finished. endlocal逐段解释一下关键点。cd /d %BASE_DIR%里的/d参数允许跨盘符切换目录。如果没有它从C盘切到D盘会不生效。set OUToutput\f%%f_a%%a每次循环根据当前参数生成独立目录因为开了延迟变量展开所以后面!OUT!能正确取到当前这一轮的值。这里特意把参数写入独立的params.ini虽然MATLAB已经在命令行拿到了参数但留一份原始参数文件便于事后核对。运行日志使用 !OUT!\run.log 21将stdout和stderr合并进同一个文件避免信息分散。set ERR!errorlevel!这种做法是把退出码先存起来再判断避免后续命令修改errorlevel之后丢失。4.3 MATLAB端函数 vib_sim.m 的写法function vib_sim(freq, amp, outdir) % 仿真参数写入独立文件 fid fopen(fullfile(outdir, params.ini), a); if fid ~ -1 fprintf(fid, run_time%s\n, datestr(now, yyyy-mm-dd HH:MM:SS)); fclose(fid); end % 关闭绘图显示后台运行 set(0, DefaultFigureVisible, off); % 模拟一段耗时计算 t (0:0.01:10); response amp * sin(2 * pi * freq * t) 0.05 * randn(length(t), 1); % 保存结果 result.freq freq; result.amp amp; result.t t; result.response response; save(fullfile(outdir, result.mat), result); % 打印关键信息 fprintf(freq%g amp%g finished, results saved to %s\n, freq, amp, outdir); end这里有几个对批处理友好的设计。第一函数不接受任何交互不弹图因为在-batch模式下即使弹图也没人看还可能导致挂起。第二返回前把所有结果用save落盘。第三函数运行结束后MATLAB会自动退出不需要在函数里写exit-batch模式天然做到这一点。4.4 结果收集与断点续跑15组跑完以后如果你不想一个个打开output子目录可以写一个MATLAB收集脚本files dir(fullfile(output, f*_a*, result.mat)); for k 1:length(files) s load(fullfile(files(k).folder, files(k).name)); fprintf(%s: freq%g amp%g max%g\n, ... files(k).folder, ... s.result.freq, s.result.amp, ... max(abs(s.result.response))); end这个收集脚本不依赖批处理单独在MATLAB里跑一遍就能汇总所有结果。如果你担心跑批中途断电或某个case卡死可以在批处理里加上“断点续跑”的判断某个case的结果文件已经存在就跳过不必从头再来。if exist !OUT!\result.mat ( echo [%date% %time%] skip freq%%f amp%%a run_log.txt ) else ( %MATLAB_EXE% -batch vib_sim(%%f, %%a, !OUT!) !OUT!\run.log 21 )这个改动很小但价值巨大。参数扫描耗时越长这个续跑功能就越重要。我后来跑一个200组的批量任务跑到第150组时断电了恢复后重新双击脚本它自动跳过已经完成的149组只补跑最后一组。5. 常见问题排查和稳定性技巧5.1 高频问题和排查速查表批处理跑MATLAB过程中我踩过的坑可以整理成下面这张表现象可能原因解决办法提示“matlab不是内部或外部命令”matlab不在PATH中在批处理里用完整路径调用matlab.exe循环中每个case的日志/目录都一样变量展开时机不对开启setlocal enabledelayedexpansion用!var!批处理停在一个case不继续用了-r且没有加exit改用-batch或加exitMATLAB报“找不到函数”工作目录不对批处理先cd /d到MATLAB脚本所在目录日志文件是空的没有合并stderr重定向写成 log.txt 21参数值里带导致命令被拆分特殊字符未转义用echo ... ^ ...或避免特殊字符某个case报错但批处理仍继续脚本没判断errorlevel用if !errorlevel! neq 0记录错误并行启动多个MATLAB报license错误同时打开的进程数超限控制并发数或者串行运行日志里的中文乱码代码文件或控制台编码不一致尽量用英文输出日志输出目录生成后为空MATLAB启动时cd到了别的位置函数内使用绝对路径或批处理前先cd这张表看起来条目多但真正高频的其实就前四行。特别是第一行和第三行几乎每个刚接触批处理跑MATLAB的人都会遇到。5.2 让整批跑批更稳的经验先跑通单个case再放全量。我最早犯的错就是写完批处理直接跑全量跑到中间挂了才发现是MATLAB函数本身有个bug。排查的时候就非常被动到底是参数传错了还是脚本写错了还是MATLAB抽风了后来学乖了先在批处理里临时把参数固定成一组调通后再把for循环放开。日志一定要保留退出码。哪怕你暂时不处理错误也要把每次运行的退出码记录下来。这个习惯能帮你事后快速定位是哪个case出了问题而不是靠肉眼一个个翻日志。给每个case一个独立目录。这句话我重复很多次了但确实是所有经验里最重要的一条。独立目录不仅避免文件互相覆盖还让断点续跑、结果收集、问题追溯都变得非常简单。如果一开始为了省事把所有结果都写到同一个文件里后面整理数据时会付出更大的代价。5.3 并行跑批该不该用串行跑最稳妥每个case之间完全隔离不会抢资源日志也不会互相干扰。但是遇到单组仿真耗时较长的情况串行总时长可能让人抓狂。批处理本身也可以做简单的并行开多个cmd窗口每个窗口跑不同的参数段。或者用start /b把MATLAB进程放到后台。我个人的做法是先看CPU和内存。如果单个MATLAB计算已经比较吃CPU并行开两个可能直接让内存爆掉。如果计算是轻量的、主要耗在启动上并行才有意义。更简单的并行方案是手动复制几个批处理每个批处理负责一部分参数范围同时双击运行。虽然听起来不够“自动化”但在没有更多工具的情况下非常可靠。并行时注意各进程不要写同一个输出文件只要每个case独立目录一般不会出问题。还有一个容易忽略的点MATLAB正版授权通常有并发数限制。并行启动太多MATLAB进程可能直接报license不可用。遇到这种情况不要硬扛把并发数降到2到3个试一下或者干脆改回串行跑半夜挂着跑完第二天看结果。结尾一点个人体会这套批处理方案我前前后后用了好几年从老版本的-wait -r写法一直换到现在的-batch感触最深的一条经验是先把单个MATLAB函数手动调通再交给批处理。脚本出错时你最难面对的是“不知道是批处理写错了还是MATLAB本身有问题”而这个痛苦完全可以通过先单跑一组来避免。另一个心得是日志和退出码一定要写全别嫌麻烦。批量任务跑一次可能就是一两个小时没有日志出了问题你只能对着黑窗口发呆有了日志扫一眼就能知道第几个case挂了、退出码是多少、当时是什么时间。批处理确实不是什么新潮技术但它在Windows环境下的“零依赖”特性和调度能力至今仍然很适合参数扫描这类批量仿真任务。如果你也有类似的场景不妨从最简单的命令行传参版本开始试跑通之后再逐步加上独立目录、断点续跑、日志留痕这些功能。这套方法不一定是最华丽的但一定是最实用、最容易落地的那一种。
RELATED READING

延伸阅读

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