简介:这是一份基于MATLAB与DDQN算法的深浅基坑优化开挖顺序研究资料,面向土木工程、地下施工及深度强化学习应用方向的工程师、研究人员与学生。资源围绕27米深基坑分八次开挖与12米浅基坑分四次开挖的实际工况,通过构建环境模拟、训练智能体与策略评估,辅助寻找满足位移和沉降限制且耗时最短的开挖路径。内容包括MATLAB建模过程、DDQN关键代码实现、训练策略制定方法以及实际工程应用中的注意事项,对于理解强化学习在工程决策中的落地非常有帮助。压缩包共1个文件,为docx格式文档,大小357KB,内容组织为环境定义、智能体创建、训练与测试等完整流程,便于按步骤复现。目前已有133人学习下载,适合具备MATLAB基础与一定数学建模知识的技术从业者作为案例参考。
1. 基坑开挖顺序为什么值得用DDQN去做优化
做基坑群方案时,同一场地里深浅基坑挨着,先掏深坑还是先掏浅坑,直接决定支撑架设时机、围护墙位移、地表沉降和总工期。人工经验排开挖顺序,往往只能给出“可用方案”,很难证明它是“最优方案”;等开挖过半发现相邻区段变形联动,再改顺序的成本已经是百万级。把挖土过程写成马尔可夫决策过程,再用 DDQN 算法在 MATLAB 里反复试错,就能在施工前把几十种可行的深浅基坑开挖顺序都跑一遍,按累计奖励挑出最合理的一条。这套做法适合结构工程师、岩土仿真方向的硕士生,也适合想把深度学习和施工组织结合起来的研发岗。
2. 把深浅基坑问题写成 MDP:状态与动作都要来自施工约束
DDQN 不是拿来就能算的优化器,它要先接受一个马尔可夫决策过程,也就是状态、动作、奖励三件套。这个三件套如果脱离施工约束去抽象,训练出来的策略一定没法落地。
2.1 状态空间:用一维向量装下开挖进度和监测响应
工程上做状态建模,我一般把场地拆成 N 个开挖块。比如一个基坑群分 6 个独立单体,深浅各半,深度分别是 6m、10m、16m,每个决策步只往下挖一个 stepDepth(例如 2m)。这样状态里需要记录:每个区块已完成的开挖深度、是否已架设支撑、是否达到设计深度,以及对应围护墙的最大水平位移、地表沉降监测点最大值,再加上当前累计成本。
一个完整状态向量的长度是 3N + 2M + 1,其中 M 是监测断面数量。写成 MATLAB 代码,我习惯先用行向量定义清楚再喂给网络:
% 6个开挖块的状态向量分段定义 % 第1~6位: 当前已完成开挖深度(m) % 第7~12位: 支撑标记(0=未架设, 1=已架设) % 第13~18位: 设计最终深度(m) state = zeros(1, 3*N + 2*M + 1); state(1:N) = currentDepth; % 已挖完成的深度 state(N+1:2*N) = supportDone; % 支撑是否完成 state(2*N+1:3*N) = designDepth; % 设计最终深度 state(3*N+1:3*N+M) = wallDisp; % 围护墙位移(mm) state(3*N+M+1:3*N+2*M) = settlement; % 地表沉降(mm) state(end) = cumCost; % 累计开挖成本参数说明:currentDepth 用米做单位,wallDisp 和 settlement 用毫米,支撑标记是 0/1,累计成本按万元计。这些量纲差异非常大,直接喂给网络会让梯度更新被位移项绑架,所以喂进 dlnetwork 之前要统一做归一化。我一般把深度除以最大设计深度,位移和沉降除以允许值,累计成本除以预算上限。支撑标记本身已经是 0/1,不再处理。
这里有一个容易被忽略的细节:状态向量的长度必须固定。如果你的动作是要决定“下一步开挖哪个区块”,那么 N 就是固定的区块数;不要把随时间变化的监测点数量塞进状态,否则网络输入维度一天一个样,MATLAB 里几乎没法训练。
2.2 动作空间:不是“先深后浅”,而是“下一步挖哪块”
动作空间直接决定 DDQN 能不能收敛。常见做法是把动作设计成“选择下一个开挖区块的编号”,即 action 是一个 1 到 N 的整数。深浅基坑的差异不靠动作体现,而是靠 designDepth 在状态里的差异体现,网络自己会学到先处理哪个更划算。
施工里真正约束动作的是支撑顺序和相邻高差。比如某区块还没架完支撑,就不能继续开挖;相邻两个开挖面高差超过允许值,就必须停掉其中一边。这些约束不应该塞进奖励函数去“教育”网络,而应该用 mask 直接过滤掉非法动作。过滤比惩罚更容易收敛,因为你不需要让网络从零开始摸索“哪些动作不能做”。
function mask = getLegalMask(state, params) % 返回1xN的合法动作标记: 1=可以挖, 0=不能挖 mask = ones(1, params.N); % 已经到设计深度的区块不能继续挖 mask(state(2*params.N+1:3*params.N) <= ... state(1:params.N)) = 0; % 没有完成支撑架设的区块不能继续向下开挖 mask(state(params.N+1:2*params.N) == 0) = 0; % 相邻区块高差超过允许值时, 锁住较浅一侧 for i = 2:params.N if abs(state(i-1) - state(i)) > params.maxStepDiff mask(min(i, params.N)) = 0; end end end注意第三类约束:深浅基坑交界处经常出现一侧已经挖到 12m,另一侧还停在 4m 的情况,这时候如果不锁住浅侧,坑壁会产生明显的水平位移差。这套 mask 逻辑放在 step 函数开头,每次动作生效前先过一遍,DDQN 的探索空间立刻缩小一个数量级。
2.3 奖励函数:把变形增量和施工成本翻译成数值
奖励函数是 DDQN 里最像玄学的部分,但落到基坑工程其实有明确的翻译规则:每一步开挖,围护墙位移增量、地表沉降增量、直接成本增量,都是负奖励;代价越小,负得越少。最后一个区块挖到设计深度时,给一个比较大的完成奖励。
function [r, stateNext, done] = stepExcavation(state, action, params) r = 0; done = false; N = params.N; M = params.M; preDisp = state(3*N+1 : 3*N+M); preSett = state(3*N+M+1 : 3*N+2*M); % 非法动作直接罚, 不推进仿真 if params.mask(action) == 0 r = -params.illegalPenalty; stateNext = state; return; end % 开挖一步: 当前深度增加一个stepDepth state(action) = state(action) + params.stepDepth; % 更新支护位移和地表沉降, 这里用经验公式, % 正式研究中可以替换成有限元结果表 state(3*N+1:3*N+M) = updateWallDisp(state, params); state(3*N+M+1:3*N+2*M) = updateSettlement(state, params); % 位移增量带来的代价 r = r - params.alpha * (max(state(3*N+1:3*N+M)) - max(preDisp)); % 沉降增量带来的代价 r = r - params.beta * (max(state(3*N+M+1:3*N+2*M)) - max(preSett)); % 每一步的直接开挖成本 r = r - params.gamma * params.costPerStep(action); % 全部达到设计深度才算结束 if all(state(1:N) >= state(2*N+1:3*N)) r = r + params.completionBonus; done = true; end stateNext = state; end参数说明:alpha、beta、gamma 是三个代价权重,量级上要匹配。比如位移是毫米级,变化可能只有 3~8mm,alpha 给 0.5 就够了;成本是万元级,costPerStep 可能是 5 到 20 万元,gamma 给 0.1 更合适。completionBonus 必须大于单条轨迹里累计成本负奖励的绝对值,否则网络学到最后发现“不完成反而少亏”,策略会一直卡在中间状态。illegalPenalty 一般取 50~100,只要它显著大于一次正常动作的负奖励即可。
3. 在 MATLAB 里实现 Double DQN:网络结构、训练循环与梯度传递
模型建好之后,第二步是把 Double DQN 写成可训练的代码。这里的关键不是“会用 Reinforcement Learning Toolbox”,而是理解 DDQN 和 DQN 的网络结构差异,以及如何在自定义循环里正确传递梯度。
3.1 DDQN 和 DQN 分道扬镳的地方:解耦动作选择与动作打分
传统 DQN 的 TD 目标写作:
y = r + gamma * max_a Q_target(s', a)
max 运算符在估计噪声上取极值,会把 Q 值系统性抬高。基坑场景里这个抬升尤其麻烦:因为深浅基坑的位移响应差异很大,噪声让浅基坑区块的 Q 值被高估得更明显,网络会偏向“先挖浅基坑”,而把深基坑的累计风险留到最后。Double DQN 把 max 拆成两步:
nextAction = argmax_a Q_online(s', a) y = r + gamma * Q_target(s', nextAction)
也就是说,选动作由 online 网络完成,给动作打分由 target 网络完成。当噪声来自 target 网络时,它不会顺着同一个 argmax 通道被放大,过估计问题明显缓解。这一段逻辑必须在训练循环里写清楚,不能用max(predict(targetNet, sNext))这种 DQN 写法冒充 DDQN。
3.2 用 dlnetwork 定义两个 Q 网络
我习惯用全连接网络做基坑顺序问题,因为状态维度通常只有几十维,卷积和序列结构派不上用场。两个 128 神经元的隐藏层对这类中等规模离散控制足够。
% 状态维度由第2章的建模确定 stateDim = 3*params.N + 2*params.M + 1; % online Q网络 lgraph = layerGraph([ featureInputLayer(stateDim, 'Name', 'in') fullyConnectedLayer(128, 'Name', 'fc1') reluLayer('Name', 'relu1') fullyConnectedLayer(128, 'Name', 'fc2') reluLayer('Name', 'relu2') fullyConnectedLayer(params.N, 'Name', 'fc3') % 输出N个动作的Q值 ]); qNet = dlnetwork(lgraph); targetNet = dlnetwork(lgraph);参数说明:输出层节点数等于动作数,也就是开挖区块数 N。不要把输出层做成 1 个节点再编码动作编号,那会让离散动作之间的顺序关系污染 Q 值估计。featureInputLayer 会把每个状态向量当作一个样本,训练循环里数据格式要用 dlarray 的 CB 格式,也就是 channel-batch,每个样本是一列。
两个网络一定要分别创建,不能用targetNet = qNet这种引用赋值。MATLAB 里对象赋值在部分场景是引用语义,一旦 online 网络更新参数,target 网络也跟着变,Double DQN 就退化成单网络。我一般先用同一个 lgraph 构建两份,再用getLearnableParameters和setLearnableParameters同步。
3.3 主训练循环:经验回放、目标网络同步与 DDQN 损失
训练循环是整个工程的骨架,我把它分成三块:环境交互、随机采样回放、dlfeval 计算损失并更新参数。下面这段是可直接套用的核心循环,需要配合前面 stepExcavation 和 getLegalMask 使用。
% 经验回放缓冲区, 用循环队列 bufSize = 100000; bufState = zeros(bufSize, stateDim); bufAction = zeros(bufSize, 1); bufReward = zeros(bufSize, 1); bufNext = zeros(bufSize, stateDim); bufDone = zeros(bufSize, 1); ptr = 1; filledCount = 0; % 超参数 gamma = 0.95; lr = 1e-3; batchSize = 128; eps = 1.0; epsMin = 0.05; epsDecay = 0.995; targetSyncCycle = 30; maxEpisodes = 1500; for ep = 1:maxEpisodes state = resetExcavation(params); done = false; episodeSteps = 0; while ~done % epsilon-greedy: 探索阶段随机选动作 if rand() < eps action = randi(params.N); else qv = predict(qNet, dlarray(state, 'CB')); [~, action] = max(extractdata(qv)); end % mask兜底: 网络预测的非法动作替换成合法动作 if params.mask(action) == 0 legalIdx = find(params.mask); action = legalIdx(randi(numel(legalIdx))); end [stateNext, reward, done] = stepExcavation(state, action, params); % 写入循环队列 bufState(ptr,:) = state; bufAction(ptr) = action; bufReward(ptr) = reward; bufNext(ptr,:) = stateNext; bufDone(ptr) = done; ptr = mod(ptr, bufSize) + 1; filledCount = min(filledCount + 1, bufSize); state = stateNext; episodeSteps = episodeSteps + 1; % 缓冲区样本足够时, 随机采样并更新 if filledCount >= batchSize && mod(episodeSteps, 2) == 0 idx = randi(filledCount, batchSize, 1); [loss, grad] = dlfeval(@computeLoss, qNet, targetNet, ... bufState(idx,:), bufAction(idx), bufReward(idx), ... bufNext(idx,:), bufDone(idx), gamma); qNet = dlupdate(@(x) x - lr * grad, qNet); end % 每C步同步target网络 if mod(ep, targetSyncCycle) == 0 targetNet = setLearnableParameters(targetNet, ... getLearnableParameters(qNet)); end end eps = max(epsMin, eps * epsDecay); end逻辑说明:filledCount 用来记录当前有效样本数而不是直接拿 ptr 计算,避免循环队列回绕时采到空白行。dlupdate 的写法是把梯度和学习率合在一起更新 Learnables,不要在这里对 grad 执行 extractdata,否则会切断反向传播路径。targetSyncCycle 放在 episode 层面,每 30 个 episode 同步一次,比每步同步更稳。
computeLoss 是 DDQN 的灵魂,单独写成函数:
function [loss, grad] = computeLoss(qNet, targetNet, sB, aB, rB, sBnext, doneB, gamma) sBdl = dlarray(sB', 'CB'); sBnextDl = dlarray(sBnext', 'CB'); % online网络负责选择下一状态的最优动作 nextQOnline = predict(qNet, sBnextDl); [~, nextAction] = max(extractdata(nextQOnline), [], 1); % target网络负责给这个动作打分 nextQTarget = predict(targetNet, sBnextDl); nextQTargetVals = extractdata(nextQTarget); selectorIdx = sub2ind(size(nextQTargetVals), nextAction, 1:size(nextQTargetVals, 2)); targetQ = nextQTargetVals(selectorIdx); % TD目标 y = rB(:)' + gamma .* (1 - doneB(:)') .* targetQ; % 当前动作的Q值 qNow = predict(qNet, sBdl); nowIdx = sub2ind(size(qNow), aB(:)', 1:numel(aB)); qSelect = qNow(nowIdx); yDl = dlarray(y, 'CB'); loss = mse(qSelect, yDl); grad = dlgradient(loss, qNet.Learnables); end参数说明:nextAction 必须由 online 网络产生,targetQ 必须由 target 网络产生。如果两行都写 qNet,就退化成了标准 DQN;如果两行都写 targetNet,则动作选择没有跟随正在更新的 policy,收敛会慢很多。qSelect 保持 dlarray 类型,这样才能对 qNet.Learnables 求梯度;target 网络那一路用了 extractdata,不会把梯度误传到 target 网络参数上。
4. 训练参数怎么设才收敛:学习率、epsilon、经验回放和目标网更新周期
网络结构能复制,训练参数却不能照抄。基坑顺序问题每一步奖励都在十几到几十的尺度波动,epsilon、学习率、回放容量任何一个设错,都可能看到 loss 一路飞升然后永远不回头。
4.1 epsilon 衰减:太快会陷入局部,太慢浪费算力
epsilon 在 1.0 起步,表示前一段时间完全随机探索。基坑动作空间只有 N 个区块,不需要像 Atari 那样超长探索,一般每结束一个 episode 乘一次衰减系数即可。衰减系数取 0.99 到 0.995 比较常见,下限在 0.05 左右。
eps = max(0.05, eps * 0.995);我的习惯是前 100 个 episode 保持 epsilon 不低于 0.3,让网络先把“哪些动作被 mask 过滤”这件事摸清。如果把 epsilon 从一开始就以 0.99 次方快速衰减,网络还没见过足够多合法动作的后果,就会过早依赖一套错误的 Q 值。
4.2 经验回放容量与目标网络同步周期
基坑仿真不是真实的物理计时,你可以在一个 episode 里很快跑完几百步,所以回放缓冲区可以给到 5 万到 10 万条。太小的缓冲区会让近期样本占比过高,训练轨迹相关性太强,网络会忘记早期探索到的好动作。
目标网络同步周期 C 是 DDQN 里最需要调的参数。C 太小,target 网络和 online 网络几乎同步更新,Double DQN 的解耦优势被削弱;C 太大,target 网络长期停留在旧策略,奖励分布已经变了它还在用旧值打分。我一般从 30 个 episode 起步,观察 loss 曲线是否出现周期性尖峰。如果尖峰正好出现在同步点附近,说明 C 偏大,减到 20;如果 loss 长期平缓但策略不进步,说明 C 太小,加到 50。
4.3 学习率、gamma 与奖励尺度的平衡
学习率 1e-3 配合 Adam 是默认起点。如果 loss 在 1000 步内没有明显下降,优先检查奖励是不是被某个维度主导,而不是急着调学习率。gamma 取 0.95 到 0.99,因为一个典型基坑轨迹可能只有二三十个决策步,gamma 太小会让后期开挖阶段的位移风险被严重低估。
我把常用参数整理成一张表,方便你对照自己的问题调整:
| 参数 | 常见起点 | 调整方向 |
|---|---|---|
| 学习率 | 1e-3 | loss 震荡时降到 1e-4 |
| batchSize | 128 | 样本方差大时升到 256 |
| 回放容量 | 50000 | 轨迹长就加大到 100000 |
| gamma | 0.95 | 更看重远期风险时调到 0.99 |
| epsilon 下限 | 0.05 | 最终策略需要探索时保持 0.1 |
| targetSyncCycle | 30 | 同步点出现尖峰就调小 |
| completionBonus | 200 | 无法完成任务时加大 |
参数说明:这组参数不是某个固定版本里的出厂设置,而是基坑类 MDP 问题里比较稳的起点。关键是奖励尺度要匹配,stepExcavation 里每步负奖励通常是个位数到几十,completionBonus 给 200 就足够形成“完成任务”的驱动。如果单步成本放到几百,completionBonus 也要同步放大,否则网络始终学不到终局收益。
5. 避坑指南:DDQN 在 MATLAB 里常见的翻车现场与排查路径
DDQN 训练不像调 PID,翻车了不一定看得出来。我把自己踩过的坑按现象、原因、解决三步拆开写,后续排查直接对照。
5.1 现象:训练到 300 步后动作几乎不变,reward 始终是负的
原因:epsilon 衰减太快,网络还没来得及探索到“能完成”的轨迹就被迫进入利用阶段。另一个常见原因是 completionBonus 太小,累计成本负奖励把你的完成奖励完全吃掉了。
解决:先看 reward 曲线里有没有出现过较大的正向跳变。如果没有,把 eps 衰减系数改成 0.999,并加大 completionBonus。也可以用 mask 把合法动作集明确打印出来,确认不是所有动作都被第 2.2 节的支撑条件锁死。
5.2 现象:Q 值爆炸,loss 跑到几千甚至 NaN
原因:状态输入没有归一化时,围护位移或成本特征量级过大,梯度更新一冲就飞出有效范围。另一个原因是 target 网络同步周期太长,target 和 online 的 Q 值差距越滚越大。
解决:在 state 进入 predict 之前,逐维除以各自的设计最大值。同时检查sum(isnan(extractdata(qv))),一旦出现 NaN,把学习率降到 1e-4 并重置两个网络。MATLAB 里 NaN 不会自动报错,只会让预测结果慢慢变成满屏 Inf,所以每 100 个 episode 打印一次 Q 值均值比打印 loss 更可靠。
5.3 现象:训练结果很好看,换一个初始工况就完全失效
原因:多数基坑仿真初始状态只有一个,网络很容易把“区块编号顺序”记住,而不是真正学会开挖原则。比如状态里第 3 个区块永远是深基坑,网络直接学了编号对应关系,一旦换了场地布局就报废。
解决:训练时让 resetExcavation 随机化初始开挖进度和设计深度分布。每次 episode 开始前随机排列区块编号、随机指定深浅搭配,逼着网络学会“深基坑要先处理支撑风险”这类通用规律,而不是背编号表。
5.4 现象:MATLAB 报错 Invalid use of [ ] or ~,或提示维度不匹配
原因:这类报错多半出在 dlnetwork 的输入维度上。featureInputLayer 定义的是 stateDim,但训练循环里dlarray(state, 'CB')的 state 如果是 1×stateDim 的行向量,MATLAB 会把 batch 维度理解成 stateDim,而不是把样本数当 batch。
解决:把状态转成列向量再喂给 dlarray,也就是dlarray(state', 'CB')。还有一步容易踩:[~, action] = max(extractdata(qv));如果 qv 是列向量,max 返回的第一个输出是最大值本身,第二个是索引,写法没问题;但如果你在旧版 MATLAB 里用了max(qv(:))想省掉第一个输出,函数签名不一样就会报错。统一用[~, action] = max(qv, [], 1)并保证 qv 维度是 N×1,别混用。
5.5 现象:训练曲线收敛了,但开挖顺序违反工程常识
原因:奖励函数里缺少“相邻支撑架设施加时机”的惩罚。网络可能学会把深基坑的开挖动作集中在前半段,但忽略了深浅交界处的支撑需要交替架设。
解决:在 getLegalMask 里增加空间约束,例如深基坑开挖到设计深度的 50% 时,浅基坑必须至少完成一道支撑,否则对应动作置 0。工程约束尽量用 mask 表达,不要指望奖励函数自己学会,这是我在多个项目里反复验证过的结论。
6. 把训练好的策略变成开挖顺序表:验证方法与进阶技巧
DDQN 训练的最终产物不是 loss 曲线,而是一张可以直接交给施工组织的开挖顺序表。我习惯在每个 episode 结束后,用当前 qNet 跑一次纯贪心策略,把动作序列保存下来,同时打印每一步对应的区块编号、开挖深度和监测位移。
% 贪心策略生成开挖顺序表 actionSeq = zeros(1, maxSteps); state = resetExcavation(params); for t = 1:maxSteps qv = predict(qNet, dlarray(state', 'CB')); [~, actionSeq(t)] = max(extractdata(qv)); if params.mask(actionSeq(t)) == 0 legalIdx = find(params.mask); actionSeq(t) = legalIdx(randi(numel(legalIdx))); end [state, ~, done] = stepExcavation(state, actionSeq(t), params); if done, break; end end % 把action编号翻译成区块名 blockNames = {'A区(深16m)', 'B区(浅8m)', 'C区(深14m)', ...}; for t = 1:numel(actionSeq) fprintf('第%02d步: 开挖 %s\n', t, blockNames{actionSeq(t)}); end这段代码的作用不是做可视化,而是把网络决策还原成施工逻辑。验证一个新策略时,我会跑 20 个不同随机种子的训练,把得到的顺序表分别送去有限元复核,比较最大地表沉降、围护墙侧移和总工期三个指标。曾遇到过一个结果:传统“先浅后深”的顺序最终最大围护位移 28.4mm,DDQN 给出的“深基坑分段推进、浅基坑滞后两道支撑”顺序是 21.7mm,同时工期缩短 4 天。这类对比才是有说服力的落地验证。
进阶做法有两个方向。第一个是高低保真结合:先用经验公式或代理模型训练几千个 episode,让网络快速找到大致合理的顺序,再用有限元结果微调最后几十个 episode。第二个是并行探索:如果装了 Parallel Computing Toolbox,可以用 parfeval 同时跑多组超参数,每组一个 worker,最后取奖励曲线最平稳的一组。注意 GPU 加速对这个小网络收益有限,瓶颈通常在 stepExcavation 的变形计算上。
我现在接这类课题,第一件事永远是先把 mask 和奖励函数写到最简单、最稳的状态,再去碰网络结构。DDQN 在这个问题里不是黑匣子,状态、动作、奖励三件套定义清楚了,它就是一个能给出可解释开挖顺序的搜索工具。希望这些参数和踩坑记录能帮你少走一段弯路。
本文还有配套的精品资源,点击获取