[{"content":" 来自理想汽车团队的最新研究，2026年5月发表\n🎬 摘要：先立个 flag 想象一下，你是一位新司机，脑子里的第一个念头（\u0026ldquo;踩油门超过去！\u0026quot;）并不总是最好的选择。聪明的做法是：先打个草稿，再审视一遍，改掉不合理的地方，最终才真正打方向盘。\nReflectDrive-2 就是这样一位\u0026quot;会反思的自动驾驶大脑\u0026rdquo;。它的核心流程是三步走：决策 → 起草轨迹 → 自我修改（Reflect），全程在同一套\u0026quot;离散 token 空间\u0026quot;里完成，不需要额外的修改网络。\n论文最核心的发现是：光靠监督学习训练出来的\u0026quot;修改器\u0026quot;只会让性能提升 +0.3分（PDMS，一个综合驾驶质量分），但当引入强化学习让\u0026quot;起草\u0026quot;和\u0026quot;修改\u0026quot;联合优化之后，提升蹿到了 +1.9分。最终在 NAVSIM 基准测试中，仅用摄像头输入就达到了 91.0 PDMS，最优6选1时更达到了与人类持平的 94.8 PDMS，同时在 NVIDIA Thor 芯片上单帧只需 31.8ms。\n第一章：导言 —— 司机的\u0026quot;三步曲\u0026quot; 论文开篇抛出了 ReflectDrive-2 的核心工作流：\u0026ldquo;决策（Decision）—— 起草（Draft）—— 反思（Reflect）\u0026rdquo;。\n想象一下你正在开车：\n决策： 环顾四周的摄像头画面、导航信息和自身状态，你心里选定了一个大方向（生成一个 Goal Token 目标词元）。 起草： 你在脑海中快速画出一条大致的行车路线（通过掩码离散扩散并行解码出一条蓝色的初始轨迹）。 反思（AutoEdit）： 然发现前面有个小坑！你没有全盘否定路线，而是在脑海中对那一段方向盘微调了一下（在同一模型内就地修改词元，得到绿色的最终安全计划）。 这种在统一离散空间里的自我修改能力，不需要额外挂载\u0026quot;外挂\u0026quot;（如辅助微调网络），模型自己就能搞定！\n第二章：相关工作 —— 站在巨人的肩膀上找痛点 端到端与VLA（视觉-语言-动作）规划： 过去的端到端模型通常缺乏中间纠错机制。虽然有些VLA大模型引入了新思路，但 ReflectDrive-2 直接用\u0026quot;掩码离散扩散\u0026quot;取代了传统的自回归，几轮并行解码就能拿下一整条轨迹！\n扩散策略与强化学习（RL）： 之前最接近的工作是 DriveFine（2026），但它的\u0026quot;起草员\u0026quot;和\u0026quot;修改员\u0026quot;是分开训练的，就像写手和编辑各拿各的工资，配合不够默契。ReflectDrive-2 的杀手锏在于：用强化学习（RL）把\u0026quot;起草\u0026quot;和\u0026quot;修改\u0026quot;绑定在一个回合里（composed rollout）。两个人共享最终的驾驶奖励，真正做到了\u0026quot;有福同享，有难同当\u0026quot;，从而让编辑能力突飞猛进。\n第三章：预备知识 —— 把开车变成\u0026quot;拼乐高\u0026quot; 问题设定 系统的输入是全景相机画面、导航指令和自车状态；输出则是离散的\u0026quot;轨迹词元\u0026quot;（Tokens）。\n掩码离散扩散 (Masked Discrete Diffusion) 想象未来的轨迹是由一块块包含鸟瞰图（BEV）坐标的\u0026quot;乐高积木\u0026quot;拼成的。模型一开始拿到的是一堆被打上 assessments（马赛克）的未知积木，然后通过一个双向 Transformer，结合多模态上下文，一步步把马赛克替换成真实的坐标积木，实现有选择性的重新生成。\n第四章：核心方法 —— ReflectDrive-2 架构大揭秘 这是这篇论文的灵魂所在，分为五步绝杀：\n4.1 系统全貌 将\u0026quot;目标提议、轨迹起草、词元级轨迹纠错\u0026quot;全部统一在离散表示中。\n4.2 条件目标掩码轨迹扩散 模型不是死板地给出唯一终点，而是预测一个\u0026quot;目标点后验概率分布\u0026quot;，通过 top-k 采样和非极大值抑制（NMS）选出候选目标点，并把它们作为条件，并行起草完整的行驶轨迹。\n4.3 AutoEdit 轨迹修正 这是核心魔法！模型具备感知结构的扰动能力，在推理阶段，直接识别出轨迹中那些\u0026quot;不靠谱\u0026quot;的局部 Token，并在原地将它们重写（Rewrite），完成轨迹的微调。\n4.4 约束感知的监督目标 在监督学习阶段，引入了\u0026quot;可行驶区域场损失\u0026quot;（Drivable-area field loss），教模型不要把车开到马路牙子或草坪上。\n4.5 在\u0026quot;起草-编辑\u0026quot;回放上的强化学习 终极武器！ 系统把奖励（Reward）发给\u0026quot;编辑后\u0026quot;的最终轨迹，并通过策略梯度把功劳分配给前期的起草和后期的修改阶段。实验证明，这让模型的纠错能力产生了质的飞跃。\n第五章：高效推理 —— 把速度压榨到极致 自动驾驶是人命关天的事，计算再复杂也不能卡顿！作者在 NVIDIA Thor 芯片 上硬生生把平均延迟压到了 31.8 毫秒！\n优化手段汇总 优化手段 延迟变化 加速比 共享前缀 KV 缓存复用 0.28ms → 0.08ms 3.5× KV 缓存回退 + 合并重写 14.7ms → 11.5ms 1.28× Action-Expert FFN（隐层 4096→1024） 2.47ms → 0.95ms 2.6× 融合 CUDA Unmask 内核 0.45ms → 0.06ms 7.5× 交替步解码（ASD）时序 AutoEdit 26.2ms → 7.6ms 3.4× 端到端平均帧延迟 45.0ms → 31.8ms 1.42× 交替步解码 (Alternating Step Decode) 这个设计太聪明了！因为连续两帧画面的路况其实差不多，模型不需要每帧都\u0026quot;从零思考\u0026quot;。它采用了**全步（Full-step）和轻量步（Lite-step）**交替的策略。全步走一遍\u0026quot;决策-起草-反思\u0026quot;；轻量步则直接拿上一帧的轨迹往前挪一挪，做个极简版的 Token 到 Token 的 AutoEdit 快速更新。这就好比司机眨了下眼，只需凭肌肉记忆微调方向盘，省下了巨大的算力！\n第六章：实验 —— 用数字说话 6.1 实验设置 数据集：NAVSIM（基于 nuPlan），训练集 1192 个场景，测试集 136 个场景，任务是预测 4 秒、2Hz 采样的自车轨迹 评估指标 PDMS：综合了无责任碰撞（NC）、可行驶区域合规（DAC）、碰撞时间（TTC）、舒适性（Comf.）、自车进度（EP） 模型：0.7B 离散扩散语言主干 + 0.1B ViT 视觉编码器，全部微调 6.2 RL 对 AutoEdit 的增益 训练设置 无 AutoEdit 有 AutoEdit 增益 只有 DLM 损失 84.8 85.0 +0.2 + 可行驶区域场损失 87.2 87.3 +0.1 + AutoEdit 监督训练 87.7 88.0 +0.3 + 全流程 RL 89.1 91.0 +1.9 结论一目了然：没有 RL，AutoEdit 只是\u0026quot;摆设\u0026quot;；有了 RL，AutoEdit 变成真正有价值的能力。\n6.3 与其他方法对比 方法 输入 PDMS GoalFlow 相机+激光雷达 90.3 ReCogDrive 仅相机 90.8 ReflectDrive-2（我们） 仅相机 91.0 ReflectDrive-2（6选1 oracle） 仅相机 94.8 人类参考 — 94.8 最亮眼的指标是EP（自车进度）= 89.4，所有方法中最高——说明车走得更积极，同时 DAC 和舒适性依然保持高位，做到了\u0026quot;进可攻守可守\u0026quot;。\n第七章：结论 —— 学会\u0026quot;反思\u0026quot;的司机才是好司机 ReflectDrive-2 的核心启示是：自我纠正不是免费的午餐。一个训练好的编辑器，如果和起草器没有被共同的目标绑定，那它在实际中几乎毫无用处。只有当两者通过强化学习共享终局奖励，起草器才会\u0026quot;学会\u0026quot;生成可改善的草稿，编辑器才会\u0026quot;学会\u0026quot;做出真正有价值的修改。\n局限与未来方向 当前轨迹用固定分辨率的 BEV 坐标 token，精度受 bin 大小限制。未来可以考虑更细的词表、残差偏移或混合离散-连续动作头 RL 阶段的奖励是闭环规划代理分，尚非真实世界完整目标；接入更高保真仿真器和更丰富的安全奖励有望进一步改善 AutoEdit 的扰动目前只覆盖纵向和横向，未来可以扩展到交互层面的失败（让行时机、被加塞响应、间距选择） 附录：关键技术概念详解 离散的\u0026quot;轨迹词元\u0026quot;是什么？ 第一步：轨迹是什么 自动驾驶规划的输出是未来几秒内车该去哪，具体表示为一串路径点（waypoints）：\n(x₁, y₁) → (x₂, y₂) → ... → (x₈, y₈) 这 8 个点描述了车辆在鸟瞰图（BEV）坐标系中未来 4 秒的行驶轨迹，每 0.5 秒一个点。\n第二步：为什么要\u0026quot;离散化\u0026quot;？ 坐标本来是连续实数，比如 x = 3.742 米。但论文把坐标空间切成格子，就像经纬度变成邮政编码一样：\n真实坐标 x = 3.742m ↓ 量化 格子编号 x_bin = 47 （第47个格子） 这个格子编号就是一个词元（token），和语言模型里的字词 token 是同一个概念。\n每个路径点有 1 个纵向 token + 1 个横向 token，8 个路径点就是 16 个 token，构成整条轨迹的\u0026quot;词元序列\u0026quot;。\n第三步：和语言模型的词元有什么相似之处？ 语言模型 ReflectDrive-2 token 是什么 词/子词编号 BEV 坐标格子编号 序列长度 几十~几千 固定 16 个 词表大小 ~50,000 坐标格子数 生成方式 自回归/扩散 掩码扩散并行生成 两者在数学结构上完全一致，所以才能直接套用离散扩散语言模型（LLaDA 等）的整套框架。\n第四步：离散化带来了什么好处？ 正是因为轨迹变成了一串 token，才能做到：\n并行生成：不用一个点一个点地算，所有 token 同时从 assessments 开始，几轮并行 unmask 就完成 原地编辑：想改某段轨迹？直接把那几个 token 换掉，就像文字编辑器里改几个字，不需要重新生成整条轨迹 AutoEdit 成为可能：编辑操作和生成操作用的是同一套模型、同一套词表，没有任何\u0026quot;模态鸿沟\u0026quot; 一个直觉类比 把轨迹想象成一首歌的简谱。连续坐标就像模拟音频波形，你没法直接\u0026quot;改几个音符\u0026quot;；而离散 token 就像乐谱上的音符编号，你可以精确地找到第3小节第2个音，把它从 5 改成 6，其他音符完全不受影响。\n掩码离散扩散详解 第一层：扩散模型是在解决什么问题？ 先忘掉\u0026quot;扩散\u0026quot;这个词，想象一个更简单的问题：\n我想让 AI 画一张猫的图片。\nAI 怎么\u0026quot;生成\u0026quot;一张图？它不可能凭空变出像素。它需要某种从混乱到有序的过程。\n扩散模型的核心思路是：\n训练阶段，教 AI 学会\u0026quot;如何把一张乱图还原成好图\u0026quot;：\n好图 → 加噪声 → 加更多噪声 → ... → 纯噪声 ↑ AI 学会反着走这条路 推理阶段，从一堆纯噪声出发，AI 一步一步去噪，最终生成一张清晰的猫图。\n这就是扩散模型的本质：学习\u0026quot;去噪\u0026quot;这件事，然后用去噪来生成内容。\n第二层：连续扩散 vs 离散扩散 上面说的是连续扩散——图片的像素是连续的实数，噪声是高斯噪声（正态分布的随机数）。\n但如果你处理的不是图片，而是文字呢？\n文字是离散的：\u0026quot;猫\u0026quot; 这个字不能加上 0.3 变成 \u0026quot;猫.3\u0026quot;，这没有意义。你只能从一个词跳到另一个词。\n所以就有了离散扩散，它把\u0026quot;加噪声\u0026quot;换成了\u0026quot;替换成随机的其他词/符号\u0026quot;：\n\u0026#34;今天天气真好\u0026#34; → \u0026#34;今天[随机词]真好\u0026#34; → \u0026#34;[随机词][随机词]真好\u0026#34; → \u0026#34;[随机词][随机词][随机词][随机词]\u0026#34; AI 的任务同样是学会反着走：从一堆乱七八糟的词，还原出原本有意义的句子。\n第三层：掩码扩散 —— 用 assessments 代替随机词 离散扩散有很多种\u0026quot;加噪声\u0026quot;的方式。掩码扩散（Masked Diffusion） 选择了最简单粗暴的一种：\n不替换成随机词，直接盖住——换成一个特殊符号 assessments。\n\u0026#34;今天天气真好\u0026#34; → \u0026#34;今天 assessments 真好\u0026#34; → \u0026#34; assessments assessments 真好\u0026#34; → \u0026#34; assessments assessments assessments assessments \u0026#34; 你可能觉得这很眼熟——没错，这和 BERT 的完形填空几乎是同一个思路！\nAI 的任务：看到带 assessments 的句子，猜出被盖住的是什么。\n第四层：生成时怎么用？ 训练完之后，想生成内容，就把\u0026quot;正向加噪\u0026quot;反过来走：\n从全部都是 assessments 开始，每一轮让 AI 填一些空：\n第0轮： assessments assessments assessments assessments assessments assessments ↓ AI预测每个位置的候选词，选最有把握的填入 第1轮：今天 assessments assessments 真 assessments assessments ↓ 第2轮：今天 天气 assessments 真 好了 assessments ↓ 第3轮：今天 天气 真的 真 好了 啊 每轮都并行处理所有位置，所以比逐词生成的自回归模型快得多。\n这个\u0026quot;每轮选最有把握的 token 填入\u0026quot;的策略，就是 MaskGIT 提出的置信度驱动解码，也是 ReflectDrive-2 起草轨迹的核心机制。\n第五层：为什么天然支持\u0026quot;原地编辑\u0026quot;？ 这是掩码扩散最妙的地方，也是 ReflectDrive-2 选择它的根本原因。\n生成完成之后，你得到了一个完整的 token 序列。如果你觉得某几个 token 不对，想改——\n你只需要把那几个 token 重新盖成 assessments，然后让 AI 重新填空！\n原轨迹： x₁ y₁ x₂ y₂ x₃ y₃ x₄ y₄ 发现偏了： ↑这两个位置有问题 重新盖住：x₁ y₁ assessments assessments x₃ y₃ x₄ y₄ AI 重填： x₁ y₁ x₂\u0026#39; y₂\u0026#39; x₃ y₃ x₄ y₄ ✓ 其他 token 完全不受影响，模型不需要重新跑一遍，也不需要额外的网络。这就像用橡皮擦掉草稿上的几个字，重新写——而不是把整页纸揉掉重写。\n相比之下：\n连续扩散想修改，得把整个去噪过程重来 自回归模型想修改，得从出问题的那个词开始把后面全部重新生成 总结：一张图记住全部 【训练】 好的序列 → 随机盖住一些token → 让AI学会填空 【生成】 全 assessments → AI并行填最有把握的 → 几轮后得到完整序列 【编辑】（掩码扩散独有！） 完整序列 → 把想改的位置重新盖住 → AI重新填空 → 局部修正完毕 AutoEdit 如何识别\u0026quot;不靠谱\u0026quot;的局部 Token？ 这个问题分两个层面来回答。\n第一个层面：推理时靠\u0026quot;模型置信度\u0026quot; AutoEdit 不像草稿阶段那样从 assessments 开始填空。它的输入是一条已经完整的轨迹 token 序列，然后对每个位置都预测一个\u0026quot;替换 token\u0026quot;：\n当前轨迹： x₁ y₁ x₂ y₂ x₃ y₃ x₄ y₄ ↓ 模型对每个位置都预测替换值 预测替换： x₁\u0026#39; y₁\u0026#39; x₂\u0026#39; y₂\u0026#39; x₃\u0026#39; y₃\u0026#39; x₄\u0026#39; y₄\u0026#39; 但不是所有位置都会真正被替换——这就是\u0026quot;识别不靠谱 token\u0026quot;的问题所在。\n模型对每个 token 位置输出的不是一个确定的值，而是一个概率分布（softmax 输出）：\n位置 x₃ 的预测分布： 格子46号：0.72 ← 最高概率 格子47号：0.18 格子48号：0.07 其他格子：0.03 置信度高 = 模型对某个格子非常笃定（概率集中）\n置信度低 = 模型很纠结，概率分散在好几个格子上\nAutoEdit 选择置信度低的位置来替换——因为模型自己都不确定当前这个 token 对不对，说明这个位置可能有问题。\n置信度： 高 高 低 低 高 高 高 高 ↓ 只替换低置信度的位置 结果： x₁ y₁ x₂\u0026#39; y₂\u0026#39; x₃ y₃ x₄ y₄ 目标点 token（行为锚点）永远不替换，始终锁定。\n第二个层面：训练时靠\u0026quot;结构感知扰动\u0026quot; 光靠置信度其实有个问题：模型怎么知道当前这个 token 该不该被替换？\n如果 AutoEdit 从来没见过\u0026quot;有问题的轨迹长什么样\u0026quot;，它的置信度判断就没有意义——它可能对一条偏出车道的轨迹也信心满满。\n这就是结构感知扰动（SAP）训练的价值：\n训练时，故意喂给模型两类典型错误轨迹：\n纵向扰动（进度失误）： 原轨迹走了10米 → 扰动后只走6米（刹车太早） 原轨迹走了10米 → 扰动后走了14米（刹车太晚） 横向扰动（方向偏移）： 原轨迹直行 → 扰动后整体旋转5度（车道偏移） 然后要求模型把扰动轨迹直接映射回干净轨迹。\n这个训练过程让模型学会了：\n\u0026ldquo;当我看到一条在纵向/横向上有规律性偏差的轨迹时，我应该对哪些 token 没信心，以及应该改成什么。\u0026rdquo;\n两个层面怎么配合？ 可以这样理解它们的分工：\nSAP 训练塑造了模型内部的\u0026quot;错误感知能力\u0026quot;——它让模型的概率分布在面对有问题的轨迹时，自然地对相关 token 产生低置信度。\n置信度筛选是推理时的执行机制——用模型自己暴露出来的不确定性，决定哪些 token 值得替换。\n训练阶段： 喂扰动轨迹 → 模型学会\u0026#34;这类错误我应该不确定\u0026#34; → 置信度分布变得有意义 推理阶段： 喂草稿轨迹 → 模型对有问题的位置置信度低 → 筛出来替换 一个直觉类比 想象你是一位改卷老师，改了大量有规律错误的卷子（比如总是把加法做成减法）。久而久之，你扫一眼答案就能感觉到\u0026quot;这道题的答案看起来不对劲\u0026quot;——这种直觉来自训练。\nAutoEdit 也一样：见过足够多的纵向偏移、横向偏移轨迹后，它在内部形成了对\u0026quot;轨迹哪里不对劲\u0026quot;的直觉，并通过置信度把这种直觉暴露出来，再由筛选机制决定是否动手修改。\nAutoEdit 训练阶段伪代码 # ============================================================ # ReflectDrive-2 · AutoEdit 训练阶段伪代码 # 对应论文 Section 3.2 / 4.3 / 4.4 # ============================================================ # # 符号说明（与论文保持一致） # z0 : 干净的连续坐标路径点序列 [(x1,y1),...,(x8,y8)] # x0 : z0 离散化后的 token 序列 [x1,y1,...,x8,y8] 长度 L=16 # x̃0 : 扰动轨迹的 token 序列 # c : 多模态上下文 (视觉tokens, 导航指令, 自车状态) # pθ : 共享的条件 token 模型（Transformer 主干） # assessments : 掩码符号，用于草稿阶段 # t : 掩码比例 ∈ [0, 1]，决定盖住多少 token # ============================================================ import random import math # ---------------------------------------------------------- # 工具函数 # ---------------------------------------------------------- def tokenize(waypoints): \u0026#34;\u0026#34;\u0026#34; 把连续坐标路径点量化为离散 token 序列。 每个路径点 (x, y) → 两个整数 token (x_bin, y_bin)。 8 个路径点 → 长度 16 的 token 序列。 \u0026#34;\u0026#34;\u0026#34; tokens = [] for (x, y) in waypoints: tokens.append(quantize(x, axis=\u0026#39;longitudinal\u0026#39;)) # 纵向坐标 bin tokens.append(quantize(y, axis=\u0026#39;lateral\u0026#39;)) # 横向坐标 bin return tokens # 长度 L = 16 def detokenize(tokens): \u0026#34;\u0026#34;\u0026#34;tokenize 的逆操作：token 序列 → 连续坐标路径点。\u0026#34;\u0026#34;\u0026#34; waypoints = [] for i in range(0, len(tokens), 2): x = dequantize(tokens[i], axis=\u0026#39;longitudinal\u0026#39;) y = dequantize(tokens[i+1], axis=\u0026#39;lateral\u0026#39;) waypoints.append((x, y)) return waypoints # ---------------------------------------------------------- # Step 1 · 结构感知扰动（Structure-Aware Perturbation, SAP） # 论文公式 (5)(6) # ---------------------------------------------------------- def structure_aware_perturbation(z0, beta_range=(0.7, 1.3), alpha_max=0.1): \u0026#34;\u0026#34;\u0026#34; 对干净轨迹 z0 施加两类结构化扰动，模拟驾驶中最常见的错误模式。 Args: z0 : 干净路径点列表，[(x1,y1),...,(x8,y8)] beta_range : 纵向进度缩放系数范围 (βmin, βmax) alpha_max : 横向旋转角度上限 αmax（弧度） Returns: z̃0 : 扰动后的路径点列表 \u0026#34;\u0026#34;\u0026#34; # ── 扰动类型随机选一种（或两种都加）────────────────────── perturbation_type = random.choice([\u0026#39;longitudinal\u0026#39;, \u0026#39;lateral\u0026#39;, \u0026#39;both\u0026#39;]) z_tilde = [wp for wp in z0] # 先复制一份 # ── 纵向进度扰动：沿弧长缩放 ──────────────────────────── # 论文公式 (5)：z̃i = Interp(z0, β·dᵢ) # β \u0026lt; 1 → 进度不足（刹车过早） # β \u0026gt; 1 → 进度超前（刹车过晚 / 超速） if perturbation_type in (\u0026#39;longitudinal\u0026#39;, \u0026#39;both\u0026#39;): beta = random.uniform(*beta_range) # 计算每个路径点的弧长累积值 arc_lengths = compute_arc_lengths(z0) # [d1, d2, ..., d8] # 用缩放后的弧长重新插值轨迹 scaled_arc = [beta * d for d in arc_lengths] # β·dᵢ z_tilde = interpolate_by_arc(z0, scaled_arc) # 沿原轨迹重新采样 # ── 横向方向扰动：在自车坐标系中旋转 ─────────────────── # 论文公式 (6)：z̃i = R(α)·zᵢ # α 为随机旋转角，产生整体侧向偏移，但保持轨迹平滑性 if perturbation_type in (\u0026#39;lateral\u0026#39;, \u0026#39;both\u0026#39;): alpha = random.uniform(-alpha_max, alpha_max) cos_a, sin_a = math.cos(alpha), math.sin(alpha) z_tilde = [ (cos_a * x - sin_a * y, sin_a * x + cos_a * y) for (x, y) in z_tilde ] return z_tilde # z̃0：扰动后的连续坐标路径点 # ---------------------------------------------------------- # Step 2 · AutoEdit 损失（LSAP） # 论文公式 (7)(8) # ---------------------------------------------------------- def compute_autoedit_loss(model, x0, context): \u0026#34;\u0026#34;\u0026#34; 训练 AutoEdit：给定扰动 token 序列，预测干净 token 序列。 注意：AutoEdit 不使用 assessments！ 输入是完整的（扰动后的）具体 token，目标是干净的具体 token。 这与草稿阶段的掩码填空训练完全不同。 Args: model : 共享的条件 token 模型 pθ x0 : 干净轨迹的 token 序列，长度 L=16 context : 多模态上下文 c = (视觉tokens, 导航, 自车状态) Returns: L_SAP : AutoEdit 纠错损失（标量） \u0026#34;\u0026#34;\u0026#34; L = len(x0) # = 16 # 2-a. 生成扰动后的连续坐标路径点 z0 = detokenize(x0) # token → 连续坐标 z_tilde = structure_aware_perturbation(z0) # 施加纵向/横向扰动 # 2-b. 把扰动路径点重新离散化为 token 序列 x_tilde = tokenize(z_tilde) # 扰动 token 序列 x̃0 # 2-c. 用模型对扰动 token 序列做预测 # 输入：x̃0（扰动后的完整具体 token）+ 多模态上下文 c # 输出：每个位置上的替换 token 概率分布 # 论文公式 (7)：qθ(· | x̃0, c) = softmax( hθ(x̃0, c) ) logits = model(x_tilde, context) # shape: [L, vocab_size] probs = softmax(logits, dim=-1) # qθ(· | x̃0, c) # 2-d. 计算交叉熵损失（对所有 L 个位置平均） # 论文公式 (8)：L_SAP = -E[ 1/L · Σ log qθ(x⁰ᵢ | x̃0, c) ] # 目标是干净 token x0，而非扰动 token x̃0 L_SAP = 0.0 for i in range(L): clean_token_id = x0[i] # 第 i 位的干净 token L_SAP += -math.log(probs[i][clean_token_id] + 1e-9) L_SAP /= L # 对序列长度取平均 return L_SAP # ---------------------------------------------------------- # Step 3 · 草稿阶段掩码扩散损失（LDLM） # 论文公式 (1) # ---------------------------------------------------------- def compute_dlm_loss(model, x0, context): \u0026#34;\u0026#34;\u0026#34; 训练草稿生成：随机盖住若干 token，让模型预测所有位置的原始 token。 与 AutoEdit 损失的关键区别： - AutoEdit：输入是\u0026#34;有规律错误\u0026#34;的具体 token - DLM ：输入是\u0026#34;随机盖 assessments\u0026#34;的不完整序列 Args: model : 共享的条件 token 模型 pθ x0 : 干净轨迹的 token 序列 context : 多模态上下文 c Returns: L_DLM : 掩码扩散损失（标量） \u0026#34;\u0026#34;\u0026#34; L = len(x0) # = 16 # 3-a. 随机采样掩码比例 t ∈ [0, 1] t = random.uniform(0, 1) # 3-b. 按比例 t 独立随机将每个 token 替换为 assessments x_masked = [] for token in x0: if random.random() \u0026lt; t: x_masked.append(MASK_TOKEN_ID) # 盖住 else: x_masked.append(token) # 保留 # 3-c. 模型对所有位置（包括未盖住的）做预测 # 注意：论文选择\u0026#34;全位置监督\u0026#34;，而非仅对 assessments 位置监督 # 实验证明全位置监督让训练更稳定、草稿更连贯 logits = model(x_masked, context) # shape: [L, vocab_size] probs = softmax(logits, dim=-1) # 3-d. 对所有 L 个位置计算交叉熵（目标均为干净 token x0） # 论文公式 (1)：L_DLM = -E[ 1/L · Σ log pθ(x⁰ᵢ | xt, c) ] L_DLM = 0.0 for i in range(L): L_DLM += -math.log(probs[i][x0[i]] + 1e-9) L_DLM /= L return L_DLM # ---------------------------------------------------------- # Step 4 · 可行驶区域场损失（Lfield） # 论文公式 (11)(12)(13)(14) # ---------------------------------------------------------- def compute_field_loss(model, x0, context, dac_cost_field): \u0026#34;\u0026#34;\u0026#34; 空间惩罚：让模型在概率层面就\u0026#34;远离\u0026#34;不可行驶区域。 Args: model : 共享的条件 token 模型 pθ x0 : 干净轨迹的 token 序列 context : 多模态上下文 c dac_cost_field : 可行驶区域代价场 C ∈ R^[H×W] 非可行驶区域 = 高代价，可行驶区域 = 0 Returns: L_field : 可行驶区域场损失（标量） \u0026#34;\u0026#34;\u0026#34; # 4-a. 用带随机掩码的输入让模型输出 logits（与 DLM 阶段共用） logits = model(apply_random_mask(x0), context) # shape: [L, vocab_size] L_field = 0.0 # 4-b. 对每个路径点 t（共 8 个）计算空间惩罚 for t in range(8): x_logit = logits[2*t] # 第 t 个路径点的纵向 logit y_logit = logits[2*t + 1] # 第 t 个路径点的横向 logit p_x = softmax(x_logit) # 纵向坐标的概率分布 p^(t)_x shape:[W] p_y = softmax(y_logit) # 横向坐标的概率分布 p^(t)_y shape:[H] # 4-c. 构造联合空间分布（外积） # 论文公式 (11)：p^(t)_xy[i,j] = p^(t)_x[i] · p^(t)_y[j] # 假设纵横坐标独立——近似但够用 p_xy = outer_product(p_x, p_y) # shape: [H, W] # 4-d. 用代价场加权的对数障碍函数计算惩罚 # 论文公式 (12)：L_field = Σ_t Σ_{i,j} -log(1 - p^(t)_xy[i,j]) · C[i,j] # # 直觉： # · C[i,j] 大（靠近不可行驶区域）→ 这一项权重大 # · p_xy[i,j] 大（模型对这个位置置信高）→ 惩罚也大 # · 两个大值相乘 → 强烈惩罚\u0026#34;高置信度踩线\u0026#34; for i in range(H): for j in range(W): cost = dac_cost_field[i][j] if cost \u0026gt; 0: penalty = -math.log(1 - p_xy[i][j] + 1e-9) * cost L_field += penalty return L_field # ---------------------------------------------------------- # Step 5 · 总监督损失 \u0026amp; 训练主循环 # 论文公式 (15) # ---------------------------------------------------------- def supervised_training(model, dataloader, lambda_SAP=1.0, lambda_field=0.1): \u0026#34;\u0026#34;\u0026#34; 监督微调（SFT）主循环：同时优化三个目标。 总损失：L_sup = L_DLM + λ_SAP · L_SAP + λ_field · L_field 论文公式 (15) 三个损失的分工： L_DLM → 教模型从 assessments 还原干净轨迹（起草能力） L_SAP → 教模型将扰动 token 映射回干净 token（纠错能力） L_field → 在概率层面惩罚踩入不可行驶区域（空间约束） \u0026#34;\u0026#34;\u0026#34; optimizer = AdamW(model.parameters()) for batch in dataloader: x0 = batch[\u0026#39;trajectory_tokens\u0026#39;] # 干净轨迹 token 序列 context = batch[\u0026#39;context\u0026#39;] # 多模态上下文 c cost_field = batch[\u0026#39;dac_cost_field\u0026#39;] # 可行驶区域代价场 # ── 计算三个损失分量 ─────────────────────────────── L_DLM = compute_dlm_loss(model, x0, context) L_SAP = compute_autoedit_loss(model, x0, context) L_field = compute_field_loss(model, x0, context, cost_field) # ── 加权求和得到总监督损失 ───────────────────────── L_sup = L_DLM + lambda_SAP * L_SAP + lambda_field * L_field # ── 反向传播 \u0026amp; 更新参数 ──────────────────────────── optimizer.zero_grad() L_sup.backward() optimizer.step() log(L_DLM=L_DLM, L_SAP=L_SAP, L_field=L_field, L_sup=L_sup) # SFT 结束后，进入 RL 微调阶段（见论文 Section 4.5） return model 整体结构对应论文的训练三角形，用一张图看清楚各部分的关系：\n同一批干净轨迹 x0 │ ├─── 随机盖 assessments ──→ L_DLM （教模型：从残缺填完整） │ ├─── 纵向/横向扰动 ──→ L_SAP （教模型：从错误改正确） │ └─── 输出概率分布 ───→ L_field （教模型：概率质量远离禁区） │ └──── 三者加权求和 ──→ L_sup → 反向传播 有几个细节特别值得注意：\nL_SAP 和 L_DLM 的本质区别：两者用的是同一个模型，但输入性质完全不同。L_DLM 的输入是\u0026quot;信息随机缺失\u0026quot;（assessments），L_SAP 的输入是\u0026quot;信息系统性错误\u0026quot;（扰动坐标），后者才是 AutoEdit 真正的训练信号。\nL_field 作用在概率层面：它不是在惩罚模型\u0026quot;预测了出界的 token\u0026quot;，而是在惩罚模型\u0026quot;对出界位置赋予了高概率\u0026quot;，惩罚力度随置信度和距离边界的远近双重加权，比简单的离散惩罚更平滑。\n三个损失共享同一个模型参数：没有任何额外网络，SFT 结束后这个模型既能起草，又能纠错，还会绕开禁区——然后直接进入 RL 阶段做联合优化。\nRL 微调阶段伪代码 # ============================================================ # ReflectDrive-2 · RL 微调阶段伪代码 # 对应论文 Section 3.4 / 4.5 # ============================================================ # # 核心思想： # SFT 之后，起草器和 AutoEdit 各自都有能力，但互不知道对方的存在。 # RL 阶段把两者绑在一起：跑完\u0026#34;起草→AutoEdit\u0026#34;的完整 rollout， # 只用最终轨迹的驾驶得分作为奖励，同时回传给两个阶段。 # 结果：起草器学会生成\u0026#34;可被 AutoEdit 改好\u0026#34;的草稿； # AutoEdit 学会做\u0026#34;真正提升驾驶分\u0026#34;的修改。 # # 符号说明（与论文保持一致） # Ng : 每帧采样的目标点数量（论文实验中 Ng=3） # I : 每个目标点采样的草稿数（论文实验中 I=2） # G : 总候选轨迹数 = Ng × I（论文实验中 G=6） # Sdraft : 草稿阶段的 unmask 轮数 # Sedit : AutoEdit 阶段的修改轮数 # S : 总步数 = Sdraft + Sedit # ρg : 第 g 条候选的完整 token 转换序列（长度 S+1） # τg : 第 g 条候选的最终轨迹（连续坐标） # R(τg) : 闭环规划奖励（即 PDMS 得分） # Ag : 第 g 条候选的组相对优势 # πθ : 当前策略（即当前模型参数） # πθ_old : rollout 时冻结的旧策略（用于计算重要性采样比） # πref : SFT 之后冻结的参考策略（用于 KL 惩罚） # ============================================================ import math import copy # ---------------------------------------------------------- # Step 1 · 单条候选的完整 Draft→AutoEdit Rollout # 论文公式 (16)(17) # ---------------------------------------------------------- def run_single_rollout(model, goal_token, context, S_draft=3, S_edit=3): \u0026#34;\u0026#34;\u0026#34; 对一个目标点执行完整的\u0026#34;起草→AutoEdit\u0026#34;rollout， 并记录每一步的 token 转换序列（用于后续计算策略梯度）。 Args: model : 当前策略模型 πθ_old（rollout 时冻结） goal_token : 已选定的目标点 token（行为锚点） context : 多模态上下文 c S_draft : 草稿阶段 unmask 轮数 S_edit : AutoEdit 阶段修改轮数 Returns: trajectory_history : 完整 token 状态序列 [x⁰, x¹, ..., x^S] 共 S+1 个快照 前 S_draft 步是草稿阶段，后 S_edit 步是 AutoEdit 阶段 final_trajectory : 最终连续坐标轨迹 τg（送入仿真器求奖励） \u0026#34;\u0026#34;\u0026#34; L = 16 # token 序列长度（8个路径点 × 2坐标） # ── 初始化：全部 assessments，目标点 token 固定 ──────────────── x_current = [MASK_TOKEN_ID] * L x_current = set_goal_token(x_current, goal_token) # 锁定目标点位置 trajectory_history = [x_current.copy()] # x⁰ # ══════════════════════════════════════════════════════ # 阶段 A · 草稿生成（Masked Diffusion Drafting） # 从全 assessments 出发，每轮并行 unmask 最有把握的 token # ══════════════════════════════════════════════════════ for s in range(S_draft): # A-1. 模型预测每个位置的 token 概率分布 # 输入：带 assessments 的不完整序列 + 多模态上下文 logits = model(x_current, context) # shape: [L, vocab_size] probs = softmax(logits, dim=-1) # πθ_old(· | x_current, c) # A-2. 对每个仍是 assessments 的位置，采样一个 token x_predicted = [] confidence = [] for i in range(L): if x_current[i] == MASK_TOKEN_ID: token_id = sample_from(probs[i]) # 按概率采样（非贪心） conf = probs[i][token_id] # 该 token 的概率作为置信度 else: token_id = x_current[i] # 已确定的 token 保持不变 conf = 1.0 x_predicted.append(token_id) confidence.append(conf) # A-3. 按置信度从高到低排序，本轮只 unmask 最有把握的那一批 # 每轮 unmask 比例随步数递增（第 s 轮 unmask 约 s/S_draft 的 token） n_to_unmask = compute_unmask_count(s, S_draft, n_masked=count_masks(x_current)) unmask_indices = top_k_indices(confidence, k=n_to_unmask, only_masked_positions=True) # A-4. 把选中位置的 assessments 替换为预测 token，其余保持 assessments x_next = x_current.copy() for idx in unmask_indices: x_next[idx] = x_predicted[idx] x_current = x_next trajectory_history.append(x_current.copy()) # 记录 x^(s+1) # 草稿完成，x_current 此时应没有 assessments（全部填满） x_draft = x_current.copy() # x^(S_draft) # ══════════════════════════════════════════════════════ # 阶段 B · AutoEdit 修改（Token-to-Token Rewriting） # 从完整草稿出发，直接 token→token 重写低置信度位置 # 注意：不再引入 assessments，输入始终是完整的具体 token 序列 # ══════════════════════════════════════════════════════ for k in range(S_edit): # B-1. 模型对当前完整 token 序列预测替换 token # 输入：当前具体 token 序列（无 assessments）+ 多模态上下文 logits = model(x_current, context) # shape: [L, vocab_size] probs = softmax(logits, dim=-1) x_hat = argmax(probs, dim=-1) # 每个位置的最优替换 token confidence = [probs[i][x_hat[i]] for i in range(L)] # B-2. 构造 commit mask：选出置信度低的非目标点 token # 论文公式 (10)：x^(k+1) = m^(k) ⊙ x̂^(k+1) + (1-m^(k)) ⊙ x^(k) # 目标点 token 永远不进入候选（行为锚点锁定） commit_mask = compute_commit_mask( confidence = confidence, x_current = x_current, goal_positions = get_goal_positions(), # 目标点 token 的位置索引 threshold = CONFIDENCE_THRESHOLD # 低于此值才会被替换 ) # shape: [L]，1=替换，0=保留 # B-3. 按 commit mask 执行原地替换 x_next = [ x_hat[i] if commit_mask[i] == 1 else x_current[i] for i in range(L) ] x_current = x_next trajectory_history.append(x_current.copy()) # 记录 x^(S_draft + k + 1) # 最终轨迹（连续坐标）送入闭环仿真器求奖励 final_trajectory = detokenize(x_current) # τg # trajectory_history 包含 S_draft + S_edit + 1 个快照 # 论文公式 (16)：ρg = (x⁰g, x¹g, ..., x^(Sdraft+Sedit)g) return trajectory_history, final_trajectory # ---------------------------------------------------------- # Step 2 · 对一个场景采样 G 条候选 rollout # 论文公式 (16)(17)(18) # ---------------------------------------------------------- def sample_group_rollouts(model, context, N_g=3, I=2, S_draft=3, S_edit=3): \u0026#34;\u0026#34;\u0026#34; 对当前场景： 1. 采样 Ng 个目标点（top-k + NMS） 2. 每个目标点生成 I 条草稿 3. 每条草稿跑完完整的 AutoEdit rollout 4. 把最终轨迹送入仿真器，得到 G 个驾驶奖励 Args: model : 旧策略模型 πθ_old（rollout 期间完全冻结） context : 多模态上下文 c N_g : 目标点采样数（论文实验值 3） I : 每个目标点的草稿数（论文实验值 2） Returns: group : 长度 G = Ng×I 的列表，每项包含： - history : 完整 token 转换序列 ρg - final_traj : 最终连续坐标轨迹 τg - reward : 闭环驾驶奖励 R(τg) - advantage : 组相对优势 Ag（统一计算后填入） \u0026#34;\u0026#34;\u0026#34; G = N_g * I group = [] # ── 2-a. 预测目标点后验分布，top-k 采样 + NMS ──────────── goal_logits = model.goal_head(context) # 目标点概率分布 goal_tokens = top_k_nms_sample(goal_logits, k=N_g, nms_threshold=1.2) # NMS 阈值约 1.2m # ── 2-b. 对每个目标点生成 I 条 rollout ─────────────────── for goal_token in goal_tokens: # 外层：Ng 个目标点 for _ in range(I): # 内层：每个目标点 I 条草稿 history, final_traj = run_single_rollout( model, goal_token, context, S_draft, S_edit ) # 送入闭环仿真器（NAVSIM）计算 PDMS 奖励 # PDMS = 加权综合(NC, DAC, TTC, 舒适性, EP) reward = closed_loop_simulator.score(final_traj) # R(τg) group.append({ \u0026#39;history\u0026#39; : history, # ρg：[x⁰, x¹, ..., x^S] \u0026#39;final_traj\u0026#39; : final_traj, # τg \u0026#39;reward\u0026#39; : reward, # R(τg) \u0026#39;goal_token\u0026#39; : goal_token, }) # ── 2-c. 计算组相对优势（Group-Relative Advantage）──────── # 论文公式 (18)：Ag = R(τg) - 1/G · Σj R(τj) # # 直觉：比组内平均分高 → 正优势（这条路走对了，强化它） # 比组内平均分低 → 负优势（这条路走错了，抑制它） mean_reward = sum(item[\u0026#39;reward\u0026#39;] for item in group) / G for item in group: item[\u0026#39;advantage\u0026#39;] = item[\u0026#39;reward\u0026#39;] - mean_reward # Ag return group # 长度 G 的候选列表，每项含 ρg, τg, R(τg), Ag # ---------------------------------------------------------- # Step 3 · 计算单条 rollout 的策略梯度损失 # 论文公式 (2)(19) # ---------------------------------------------------------- def compute_pg_loss_single(model, model_old, item, epsilon=0.2): \u0026#34;\u0026#34;\u0026#34; 对一条候选 rollout 计算 PPO-clip 风格的策略梯度损失。 核心设计： · 只有\u0026#34;在这一步真正发生变化\u0026#34;的 token 才获得梯度信号 （草稿阶段：从 assessments 变成具体 token 的位置） （AutoEdit 阶段：被 commit mask 选中并替换的位置） · 使用重要性采样比 r = πθ / πθ_old 做 off-policy 修正 · clip 截断防止更新步幅过大（PPO 技巧） 论文公式 (2) 中的指示函数： δ^s_{g,p} = 1{ x^(s+1)_{g,p} ≠ x^s_{g,p} } 只有 token 在第 s 步发生了变化，才把这步的梯度纳入损失。 Args: model : 当前策略 πθ（需要更新） model_old : 旧策略 πθ_old（rollout 时冻结，用于计算比值） item : 单条 rollout 信息（含 history, advantage） epsilon : PPO clip 系数 Returns: loss_pg : 该条 rollout 的策略梯度损失（标量） \u0026#34;\u0026#34;\u0026#34; history = item[\u0026#39;history\u0026#39;] # [x⁰, x¹, ..., x^S] 共 S+1 个快照 Ag = item[\u0026#39;advantage\u0026#39;] # 组相对优势（标量） context = item[\u0026#39;context\u0026#39;] S = len(history) - 1 # 总步数 = S_draft + S_edit L = len(history[0]) # token 序列长度 = 16 loss_pg = 0.0 n_credited = 0 # 记录实际获得梯度的 (步, 位置) 对数量 for s in range(S): x_s = history[s] # 第 s 步的 token 状态（模型输入） x_s_next = history[s + 1] # 第 s+1 步的 token 状态（目标） # 3-a. 用当前策略和旧策略分别计算每个位置的 token 概率 logits_new = model(x_s, context) # πθ 的输出 logits_old = model_old(x_s, context) # πθ_old 的输出（不求梯度） probs_new = softmax(logits_new, dim=-1) # shape: [L, vocab_size] probs_old = softmax(logits_old, dim=-1) # shape: [L, vocab_size] for p in range(L): # 3-b. 指示函数：只有这个位置在这一步发生了变化，才纳入梯度 # 论文公式 (19)：δ^s_{g,p} = 1{ x^(s+1)_{g,p} ≠ x^s_{g,p} } # # 草稿阶段：assessments → 具体 token，发生变化 → δ=1 # AutoEdit：被替换的 token 位置，发生变化 → δ=1 # 两个阶段没变化的位置：δ=0，跳过，不贡献梯度 token_changed = (x_s_next[p] != x_s[p]) if not token_changed: continue target_token = x_s_next[p] # 这一步实际写入的 token # 3-c. 计算重要性采样比 r^s_{g,p} = πθ / πθ_old # 分子：当前策略对 target_token 的概率 # 分母：旧策略对 target_token 的概率（采样时的概率） prob_new = probs_new[p][target_token] prob_old = probs_old[p][target_token] ratio = prob_new / (prob_old + 1e-8) # r^s_{g,p} # 3-d. PPO-clip：截断过大的更新步幅 # 论文公式 (2)：min( r·Ag, clip(r, 1-ε, 1+ε)·Ag ) clipped_ratio = clip(ratio, 1 - epsilon, 1 + epsilon) objective = min(ratio * Ag, clipped_ratio * Ag) loss_pg += -objective # 最大化目标 = 最小化负目标 n_credited += 1 # 对所有 (步, 位置) 取平均 if n_credited \u0026gt; 0: loss_pg /= n_credited return loss_pg # ---------------------------------------------------------- # Step 4 · KL 散度惩罚项 # 论文公式 (2) 中的 λ_KL · DKL(πθ ‖ πref) # ---------------------------------------------------------- def compute_kl_penalty(model, model_ref, x_masked, context): \u0026#34;\u0026#34;\u0026#34; 防止 RL 微调跑偏太远，在 SFT 基础上保持合理的分布。 πref：SFT 结束后冻结的参考策略（不随 RL 更新） πθ ：当前正在更新的策略 对一个随机掩码输入，计算两者输出分布的 KL 散度。 Args: model : 当前策略 πθ model_ref : SFT 后冻结的参考策略 πref x_masked : 随机掩码后的 token 序列 context : 多模态上下文 Returns: kl : KL 散度（标量） \u0026#34;\u0026#34;\u0026#34; probs_theta = softmax(model(x_masked, context), dim=-1) # πθ probs_ref = softmax(model_ref(x_masked, context), dim=-1) # πref（无梯度） # KL(πθ ‖ πref) = Σ πθ · log(πθ / πref) kl = 0.0 L = probs_theta.shape[0] for i in range(L): for v in range(VOCAB_SIZE): p = probs_theta[i][v] q = probs_ref[i][v] if p \u0026gt; 1e-9: kl += p * math.log(p / (q + 1e-9)) return kl / L # 对位置取平均 # ---------------------------------------------------------- # Step 5 · RL 微调主循环 # 论文公式 (2) # ---------------------------------------------------------- def rl_finetuning(model_sft, dataloader, N_g=3, I=2, S_draft=3, S_edit=3, epsilon=0.2, lambda_kl=0.01, n_epochs=1): \u0026#34;\u0026#34;\u0026#34; 在 SFT 模型基础上做 RL 微调（Reinforcement Fine-Tuning, RFT）。 Args: model_sft : SFT 阶段训练好的模型（作为起点和参考策略） dataloader: 驾驶场景数据集 N_g : 每场景目标点数 I : 每目标点草稿数 S_draft : 草稿阶段步数 S_edit : AutoEdit 阶段步数 epsilon : PPO clip 系数 lambda_kl : KL 惩罚权重 n_epochs : RL 训练轮数 \u0026#34;\u0026#34;\u0026#34; # ── 初始化 ────────────────────────────────────────────── model = copy.deepcopy(model_sft) # πθ：当前策略（持续更新） model_ref = copy.deepcopy(model_sft) # πref：SFT 参考策略（永久冻结） freeze(model_ref) optimizer = AdamW(model.parameters(), lr=1e-6) for epoch in range(n_epochs): for scene in dataloader: context = scene[\u0026#39;context\u0026#39;] # 多模态上下文 c # ══════════════════════════════════════════════ # Phase 1 · Rollout（用旧策略采样，不求梯度） # ══════════════════════════════════════════════ # 冻结当前策略作为 πθ_old，用于 rollout 和计算重要性采样比 model_old = copy.deepcopy(model) freeze(model_old) # 采样 G = Ng × I 条完整 Draft→AutoEdit rollout # 并计算每条轨迹的驾驶奖励和组相对优势 with no_grad(): group = sample_group_rollouts( model_old, context, N_g, I, S_draft, S_edit ) # group 中每项已包含：history(ρg), final_traj(τg), reward(R), advantage(Ag) # ══════════════════════════════════════════════ # Phase 2 · 计算总损失（需要梯度） # ══════════════════════════════════════════════ total_loss = 0.0 # 2-a. 对 G 条 rollout 分别计算策略梯度损失，取平均 # 论文公式 (2) 的主项 loss_pg_total = 0.0 for item in group: item[\u0026#39;context\u0026#39;] = context loss_pg_total += compute_pg_loss_single( model, model_old, item, epsilon ) loss_pg = loss_pg_total / len(group) # 对 G 条取平均 # 2-b. KL 惩罚：防止策略偏离 SFT 太远 x_sample = sample_masked_sequence(scene[\u0026#39;trajectory_tokens\u0026#39;]) loss_kl = compute_kl_penalty(model, model_ref, x_sample, context) # 2-c. 合并 # L(θ) = L_PG + λ_KL · DKL(πθ ‖ πref) total_loss = loss_pg + lambda_kl * loss_kl # ══════════════════════════════════════════════ # Phase 3 · 更新参数 # ══════════════════════════════════════════════ optimizer.zero_grad() total_loss.backward() clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() log( epoch = epoch, loss_pg = loss_pg, loss_kl = loss_kl, total_loss = total_loss, mean_reward = sum(item[\u0026#39;reward\u0026#39;] for item in group) / len(group), autoedit_gain = compute_autoedit_gain(group), # autoedit_gain = 有 AutoEdit 的平均分 - 无 AutoEdit 的平均分 # RL 训练成功的标志：这个值从 ~0.3 增长到 ~1.9 ) return model # RL 微调完成，起草器和 AutoEdit 已联合优化 整个 RL 阶段分三个 Phase 循环执行，用一张图总结结构：\n每个场景迭代： Phase 1 · Rollout（冻结旧策略，不求梯度） ┌─────────────────────────────────────────────┐ │ 目标点 NMS 采样（Ng=3） │ │ × 每个目标点 I=2 条草稿 │ │ = G=6 条完整 Draft→AutoEdit rollout │ │ 每条送入 NAVSIM 得 R(τg) │ │ 统一计算组相对优势 Ag = R(τg) - mean(R) │ └─────────────────────────────────────────────┘ ↓ Phase 2 · 计算损失（当前策略，需要梯度） ┌─────────────────────────────────────────────┐ │ 对 G 条 rollout，逐步逐位置： │ │ 只看 token 真正改变的位置（指示函数 δ） │ │ 计算 r = πθ/πθ_old │ │ loss = -min(r·Ag, clip(r)·Ag) │ │ + λ_KL · DKL(πθ ‖ πref) │ └─────────────────────────────────────────────┘ ↓ Phase 3 · 反向传播更新 有三个设计决策最值得注意：\n指示函数 δ 是灵魂。草稿阶段里从 assessments 变成具体 token 的位置、AutoEdit 阶段里被替换的位置，两类\u0026quot;变化\u0026quot;用同一个 δ 函数统一处理，终局奖励自然流入两个阶段，不需要任何人为分拆。\n两套冻结模型各司其职。πθ_old 在每轮迭代开始时复制当前模型，用于 rollout 和计算重要性采样比（保证 off-policy 修正的正确性）；πref 是 SFT 结束后一次性冻结，全程不动，只用来算 KL 惩罚防止策略跑偏太远。\n奖励只在终点打分。仿真器只看最终修改后的轨迹，不会给草稿中间过程评分。这逼着起草器主动生成\u0026quot;留有改善余地\u0026quot;的草稿，而不是一开始就出最优解——正是这个机制让 AutoEdit 增益从 +0.3 跳到 +1.9。\nAction-Expert FFN 背景：Transformer 里的 FFN 是什么 标准 Transformer 的每一层都有两个子模块：\n输入 token → 自注意力（Attention）：让 token 互相\u0026#34;看\u0026#34;彼此 → FFN（前馈网络）：对每个 token 独立做非线性变换 → 输出 token FFN 是 Transformer 里参数量最大、计算量最重的部分，约占整个模型计算量的 2/3。\n核心问题：轨迹 token 需要这么大的 FFN 吗？ 模型里同时存在两类 token，它们的性质天差地别：\n语言/视觉 token 轨迹 token 词表大小 ~50,000 个词 几十个坐标 bin 语义复杂度 极高（理解场景、推理意图） 极低（就是一个坐标数字） 需要的表达能力 强 弱 占序列长度 大部分 固定 16 个 用一个 4096 维的 FFN 去处理\u0026quot;第 47 号坐标格子\u0026quot;这种极其简单的信息，就像用核弹炸蚊子——严重过剩。\nAction-Expert FFN 的解决方案 给轨迹 token 单独配一套更小的 FFN，语言/视觉 token 继续用原来的大 FFN：\n每一层 Transformer： 语言/视觉 token ──→ 主 FFN（d_ffn = 4096）──→ 继续 ↑ 按 token 类型路由 轨迹 token ──→ Action FFN（d_ffn = 1024）──→ 继续 两套 FFN 的参数完全独立，在同一层里并行存在，根据 token 的类型决定走哪条路。\n关键的设计洞察 Attention 不分路由，FFN 才分。这很重要——轨迹 token 需要通过 Attention\u0026quot;看到\u0026quot;整个场景（障碍物在哪、车道线在哪）才能生成正确的坐标，这部分绝不能缩减。FFN 只是事后对每个 token 独立做变换，轨迹 token 在这一步根本不需要理解语义，1024 维完全够用。\n路由是零成本的。轨迹 token 始终在序列末尾的固定位置，不需要任何路由网络或运行时判断，直接切片就完成了分流，没有额外开销。\n精度反升是最有趣的发现。大 FFN 让语言梯度和轨迹梯度在同一组参数里相互干扰；拆分之后 Action FFN 的梯度来源纯粹，加上容量受限带来的隐式正则化，模型反而学到了更干净的坐标变换规律。\nAction-Expert FFN 实现伪代码 # ============================================================ # Action-Expert FFN 实现伪代码 # 对应论文 Section 5（推理优化部分） # ============================================================ # # 核心思想： # 轨迹 token 的语义空间极小（只是坐标格子编号）， # 不需要和语言/视觉 token 一样大的 FFN。 # 用一个更小的专用 FFN 处理轨迹 token， # 在不损失（甚至提升）精度的前提下大幅降低计算量。 # # 论文数值： # 主 FFN d_ffn = 4096（论文实验中约为 d_model 的 2 倍） # Action FFN d_ffn = 1024（压缩到 1/4） # 加速比：2.6× # meanSADE：不降反升（说明大 FFN 对轨迹 token 反而是过拟合） # ============================================================ import torch import torch.nn as nn # ---------------------------------------------------------- # 1. 标准 FFN（用于语言/视觉 token） # ---------------------------------------------------------- class StandardFFN(nn.Module): \u0026#34;\u0026#34;\u0026#34; Transformer 主干的标准前馈网络。 处理语言 token、视觉 token、导航指令 token、自车状态 token。 维度：d_model → d_ffn(大) → d_model \u0026#34;\u0026#34;\u0026#34; def __init__(self, d_model=2048, d_ffn=4096, dropout=0.0): super().__init__() self.w1 = nn.Linear(d_model, d_ffn, bias=False) self.w2 = nn.Linear(d_ffn, d_model, bias=False) self.act = nn.SiLU() # 论文用 SwiGLU 变体，这里简化为 SiLU self.dropout = nn.Dropout(dropout) def forward(self, x): # x: [batch, seq_len, d_model] return self.dropout(self.w2(self.act(self.w1(x)))) # ---------------------------------------------------------- # 2. Action-Expert FFN（专用于轨迹 token） # ---------------------------------------------------------- class ActionExpertFFN(nn.Module): \u0026#34;\u0026#34;\u0026#34; 轨迹 token 专用的小型前馈网络。 只处理 16 个轨迹 token（8 个路径点 × 2 坐标）。 维度：d_model → d_action_ffn(小) → d_model d_action_ffn = 1024（是标准 FFN 的 1/4） 为什么更小也够用？ 轨迹 token 的 \u0026#34;语义\u0026#34; 极其简单：就是一个坐标格子的编号。 它不需要理解\u0026#34;转弯\u0026#34;、\u0026#34;让行\u0026#34;这类高层语义（那是 Attention 的工作）。 FFN 只需要做一个低维的坐标→隐空间的映射，1024 维绰绰有余。 \u0026#34;\u0026#34;\u0026#34; def __init__(self, d_model=2048, d_action_ffn=1024, dropout=0.0): super().__init__() self.w1 = nn.Linear(d_model, d_action_ffn, bias=False) self.w2 = nn.Linear(d_action_ffn, d_model, bias=False) self.act = nn.SiLU() self.dropout = nn.Dropout(dropout) def forward(self, x): # x: [batch, n_action_tokens, d_model] n_action_tokens ≤ 16 return self.dropout(self.w2(self.act(self.w1(x)))) # ---------------------------------------------------------- # 3. 混合 FFN 层：按 token 类型路由 # ---------------------------------------------------------- class MixedExpertFFNLayer(nn.Module): \u0026#34;\u0026#34;\u0026#34; 一个完整的 Transformer FFN 子层，内含两套 FFN： - StandardFFN → 处理所有非轨迹 token - ActionExpertFFN→ 处理所有轨迹 token 路由依据：token 位置索引（轨迹 token 始终在序列末尾固定位置）。 序列结构（论文 Section 4.1）： [视觉 token (V)] [导航 token (N)] [自车状态 token (E)] [目标点 token (G)] [轨迹 token (A)] ↑ 这 16 个位置走 Action FFN \u0026#34;\u0026#34;\u0026#34; def __init__(self, d_model=2048, d_ffn=4096, d_action_ffn=1024, n_action_tokens=16): super().__init__() self.standard_ffn = StandardFFN(d_model, d_ffn) self.action_expert_ffn = ActionExpertFFN(d_model, d_action_ffn) self.n_action_tokens = n_action_tokens # 轨迹 token 始终在序列末尾，位置固定 # 这使得路由无需任何运行时判断，直接切片即可 self.layer_norm = nn.LayerNorm(d_model) def forward(self, x, action_token_mask=None): \u0026#34;\u0026#34;\u0026#34; Args: x : [batch, seq_len, d_model] 完整序列（视觉+语言+目标点+轨迹 token 混在一起） action_token_mask: [batch, seq_len] bool tensor True 表示该位置是轨迹 token 如果为 None，则默认末尾 n_action_tokens 个是轨迹 token Returns: out: [batch, seq_len, d_model] \u0026#34;\u0026#34;\u0026#34; batch, seq_len, d = x.shape out = x.clone() # 输出在原位修改 # ── 构造路由 mask ───────────────────────────────────── if action_token_mask is None: # 默认：序列末尾 n_action_tokens 个位置是轨迹 token action_token_mask = torch.zeros(batch, seq_len, dtype=torch.bool) action_token_mask[:, -self.n_action_tokens:] = True context_mask = ~action_token_mask # 非轨迹 token 的 mask # ── 路由 1：非轨迹 token → StandardFFN ─────────────── # 把所有非轨迹位置的 token 聚合成一个 batch 送入大 FFN # 用 mask 索引实现，避免 for 循环 if context_mask.any(): x_context = x[context_mask] # [n_context, d_model] x_context = self.layer_norm(x_context) out_context = self.standard_ffn(x_context) # [n_context, d_model] out[context_mask] = out_context # ── 路由 2：轨迹 token → ActionExpertFFN ───────────── if action_token_mask.any(): x_action = x[action_token_mask] # [n_action, d_model] # n_action = batch × 16（或更少） x_action = self.layer_norm(x_action) out_action = self.action_expert_ffn(x_action) # [n_action, d_model] out[action_token_mask] = out_action return out # ---------------------------------------------------------- # 4. 将其嵌入完整 Transformer Block # ---------------------------------------------------------- class TransformerBlockWithActionExpert(nn.Module): \u0026#34;\u0026#34;\u0026#34; 完整的 Transformer 层，Attention 部分不变， FFN 部分替换为 MixedExpertFFNLayer。 注意：Attention 对所有 token 统一计算（轨迹 token 需要 \u0026#34;看\u0026#34;场景上下文才能生成正确坐标），只有 FFN 做了分路由。 \u0026#34;\u0026#34;\u0026#34; def __init__(self, d_model=2048, n_heads=16, d_ffn=4096, d_action_ffn=1024): super().__init__() # Attention 部分：所有 token 统一处理（双向注意力） self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True) self.norm1 = nn.LayerNorm(d_model) # FFN 部分：按 token 类型路由 self.ffn = MixedExpertFFNLayer(d_model, d_ffn, d_action_ffn) self.norm2 = nn.LayerNorm(d_model) def forward(self, x, attn_mask=None, action_token_mask=None): # ── Attention 子层（Pre-Norm + 残差）──────────────── x_norm = self.norm1(x) attn_out, _ = self.attn(x_norm, x_norm, x_norm, attn_mask=attn_mask) x = x + attn_out # 残差连接 # ── 混合 FFN 子层（Post-Norm + 残差）──────────────── ffn_out = self.ffn(self.norm2(x), action_token_mask) x = x + ffn_out # 残差连接 return x # ---------------------------------------------------------- # 5. 参数量 \u0026amp; 计算量对比（以单层 FFN 为例） # ---------------------------------------------------------- def compare_params_and_flops(d_model=2048, d_ffn=4096, d_action_ffn=1024, n_context_tokens=512, n_action_tokens=16): \u0026#34;\u0026#34;\u0026#34; 对比标准方案（所有 token 用大 FFN） vs Action-Expert 方案（轨迹 token 用小 FFN） 的参数量和 FLOPs 差异。 \u0026#34;\u0026#34;\u0026#34; # ── 参数量 ──────────────────────────────────────────── # 标准 FFN：W1(d_model→d_ffn) + W2(d_ffn→d_model) params_standard = 2 * d_model * d_ffn # 2 × 2048 × 4096 # Action-Expert 方案：大 FFN + 小 Action FFN params_action_expert = (2 * d_model * d_ffn # 大 FFN（不变） + 2 * d_model * d_action_ffn) # 小 Action FFN（新增） # 新增参数量很少：2 × 2048 × 1024 = 4M，相对于主干几乎可忽略 # ── FLOPs（以 batch_size=1 为例）──────────────────────── n_total = n_context_tokens + n_action_tokens # 标准方案：所有 token 都走大 FFN flops_standard = n_total * 2 * d_model * d_ffn * 2 # ↑token数 ↑两层线性 ↑每次乘加2op # Action-Expert 方案： flops_context = n_context_tokens * 2 * d_model * d_ffn * 2 flops_action = n_action_tokens * 2 * d_model * d_action_ffn * 2 flops_expert = flops_context + flops_action speedup = flops_standard / flops_expert print(f\u0026#34;标准方案 参数量：{params_standard/1e6:.1f}M\u0026#34;) print(f\u0026#34;Expert 方案 参数量：{params_action_expert/1e6:.1f}M（+{(params_action_expert-params_standard)/1e6:.1f}M）\u0026#34;) print() print(f\u0026#34;标准方案 FLOPs：{flops_standard/1e9:.2f}G\u0026#34;) print(f\u0026#34;Expert 方案 FLOPs：{flops_expert/1e9:.2f}G\u0026#34;) print(f\u0026#34;理论加速比：{speedup:.2f}×\u0026#34;) print() print(\u0026#34;直觉：轨迹 token 只占序列的 16/(512+16) ≈ 3%，\u0026#34;) print(\u0026#34; 但 Action FFN 是大 FFN 的 1/4，\u0026#34;) print(\u0026#34; 所以整体 FFN FLOPs 节省约 3% × 75% ≈ 2.25%。\u0026#34;) print(\u0026#34; 论文报告的 2.6× 是针对 Action FFN 模块本身的加速，\u0026#34;) print(\u0026#34; 不是全模型加速。\u0026#34;) # 实际上 2.6× 指的是 Action FFN 这个操作本身的延迟下降： # 原来每个 token 都调用大 FFN kernel，现在轨迹 token 调用小 FFN kernel # kernel launch + 矩阵乘法的延迟同时下降 → 2.6× # ---------------------------------------------------------- # 6. 为什么精度不降反升？ # ---------------------------------------------------------- # # 论文里提到一个反直觉的现象： # Action FFN 从 d_ffn=4096 压缩到 d_action_ffn=1024 之后， # meanSADE（平均 SAD 误差，越小越好）反而变好了。 # # 可能的解释： # # ① 过参数化导致\u0026#34;记忆\u0026#34;而非\u0026#34;泛化\u0026#34; # 大 FFN 有足够的容量把训练集里每条轨迹\u0026#34;背下来\u0026#34;， # 小 FFN 容量受限，被迫学习更通用的坐标变换规律。 # # ② 正则化效应 # 小 FFN 相当于对轨迹 token 的表示施加了隐式的信息瓶颈， # 逼着模型把高层语义（该往哪走）完全依赖 Attention 解决， # 而 FFN 只做纯粹的坐标数值变换。 # 两者分工更清晰，反而更健康。 # # ③ 梯度干净 # 大 FFN 里，语言和轨迹 token 共享参数，梯度相互干扰。 # 分离之后，Action FFN 的梯度只来自轨迹相关的损失， # 更新方向更纯粹。 用一张图把整个层的结构说清楚：\nTransformer 某一层的输入序列（共 ~528 个 token）： ┌──────────────────────────────────┬────────────────┐ │ 视觉/导航/状态/目标点 token │ 轨迹 token │ │ ~512 个，语义复杂 │ 固定 16 个 │ └──────────────────────────────────┴────────────────┘ ↓ 自注意力（所有 token 统一计算，不分路由） ↓ ┌────────┴────────┐ │ 按类型路由 │ ↓ ↓ StandardFFN ActionExpertFFN d_ffn = 4096 d_ffn = 1024（1/4 大小） ↓ ↓ └────────┬────────┘ ↓ 拼回原位，形状不变 KV 缓存的合并重写 第一层：普通 KV 缓存是什么 标准自回归语言模型（GPT 系列）每次只生成一个 token，是单向因果注意力：\n生成第 4 个 token 时，它只能看 token 1、2、3 所以 token 1、2、3 的 K 和 V 在之前已经算过了，直接缓存复用即可 这就是 KV 缓存：已经算过的 K/V 不重复算，每步只算新 token 的 K/V。\n第二层：掩码扩散为什么破坏了 KV 缓存 掩码扩散用的是双向注意力——每个 token 能看到序列里所有其他 token。而且每一步都有若干个 assessments 变成具体 token，序列在变化：\n步骤 0： assessments assessments assessments assessments assessments assessments 步骤 1： assessments x₂ assessments x₄ assessments assessments ← 位置 2、4 被填入 步骤 2： x₁ x₂ x₃ x₄ assessments assessments ← 位置 1、3 被填入 问题来了：步骤 1 中位置 2 从 assessments 变成了 x₂，这个变化会影响所有其他位置对它的注意力结果，理论上其他位置的 K/V 都要重算。\n朴素做法：每步都把整个序列的 K/V 从头计算一遍。这就是为什么表格里\u0026quot;无优化\u0026quot;的延迟是 14.7ms。\n第三层：合并重写的关键洞察 虽然双向注意力让 token 互相影响，但有一个重要的局部性质：\n某个 token 在第 l 层的 K 和 V，只依赖它自己在第 l-1 层的输出，不直接依赖同层其他 token 的值。\n第 l 层： K_i^l = W_K · h_i^(l-1) ← 只取决于 token i 自己的上层输出 V_i^l = W_V · h_i^(l-1) ← 同上 合并重写的近似假设：在同一步里，只有少数位置发生变化，大部分 token 的 K/V 变化很小，可以直接复用上一步的缓存，只对真正发生变化的位置重新计算 K/V 并写回缓存。\n这是一个以极小精度损失换取大幅速度提升的近似，论文验证精度影响可忽略不计。\n具体实现示意 草稿阶段第 s 步，位置 {3, 7, 12} 刚被 unmask： 旧 KV 缓存（步骤 s-1）： 位置 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 K/V [✓] [✓] [✓] [✗] [✓] [✓] [✓] [✗] [✓] [✓] [✓] [✓] [✗] [✓] [✓] [✓] ↑ ↑ ↑ 刚变化 刚变化 刚变化 合并重写操作： 位置 3、7、12 → 重新计算 K/V，写回缓存 （3 个位置的计算量） 其余 13 个位置 → 直接复用，零计算 新 KV 缓存（步骤 s）： 位置 0 1 2 3\u0026#39; 4 5 6 7\u0026#39; 8 9 10 11 12\u0026#39; 13 14 15 K/V [✓] [✓] [✓] [✓] [✓] [✓] [✓] [✓] [✓] [✓] [✓] [✓] [✓] [✓] [✓] [✓] KV 缓存合并重写伪代码 # ============================================================ # KV 缓存回退 + 合并重写 伪代码 # 对应论文 Section 5（推理优化部分） # ============================================================ # # 问题背景： # 掩码扩散用双向注意力，每步有若干 token 从 assessments 变成具体值。 # 朴素做法：每步把全序列 K/V 从头算一遍 → 14.7ms/step # 合并重写：只重算发生变化的位置 → 11.5ms/step，加速 1.28× # # 核心近似： # 当步骤 s 中只有少数位置 Δs 发生变化时， # 其他位置的 K/V 在 s 和 s+1 步之间变化很小， # 可以直接复用上一步的缓存值。 # ============================================================ import torch # ---------------------------------------------------------- # 数据结构：多层 KV 缓存 # ---------------------------------------------------------- class KVCache: \u0026#34;\u0026#34;\u0026#34; 存储所有 Transformer 层的 K 和 V 张量。 shape: [n_layers, batch, n_heads, seq_len, head_dim] 同时维护一个\u0026#34;版本戳\u0026#34;，记录每个位置的 token 在哪一步最后更新， 用于 AutoEdit 的回退操作。 \u0026#34;\u0026#34;\u0026#34; def __init__(self, n_layers, batch, n_heads, seq_len, head_dim): self.K = torch.zeros(n_layers, batch, n_heads, seq_len, head_dim) self.V = torch.zeros(n_layers, batch, n_heads, seq_len, head_dim) # 版本戳：position_version[i] = 该位置的 K/V 在第几步被算出来 self.position_version = torch.full((seq_len,), -1, dtype=torch.long) # 快照栈：用于 AutoEdit 阶段的回退（rollback） # 每次\u0026#34;保存快照\u0026#34;就往栈里压一帧，回退就弹出 self._snapshots = [] def update_positions(self, changed_positions, new_K, new_V, step): \u0026#34;\u0026#34;\u0026#34; 合并重写：只更新发生变化的位置，其余保持不变。 Args: changed_positions : List[int]，本步发生变化的 token 位置索引 new_K : [n_layers, batch, n_heads, len(changed), head_dim] new_V : [n_layers, batch, n_heads, len(changed), head_dim] step : 当前步骤编号 \u0026#34;\u0026#34;\u0026#34; for k, pos in enumerate(changed_positions): self.K[:, :, :, pos, :] = new_K[:, :, :, k, :] self.V[:, :, :, pos, :] = new_V[:, :, :, k, :] self.position_version[pos] = step # 记录版本 def save_snapshot(self): \u0026#34;\u0026#34;\u0026#34;保存当前完整 KV 状态（用于 AutoEdit 回退）。\u0026#34;\u0026#34;\u0026#34; self._snapshots.append({ \u0026#39;K\u0026#39;: self.K.clone(), \u0026#39;V\u0026#39;: self.V.clone(), \u0026#39;version\u0026#39;: self.position_version.clone() }) def rollback(self): \u0026#34;\u0026#34;\u0026#34; 回退到上一个快照。 在 AutoEdit 阶段，如果当前修改使得奖励变差， 可以撤销这一轮的 K/V 更新，重新选择要修改的位置。 \u0026#34;\u0026#34;\u0026#34; assert len(self._snapshots) \u0026gt; 0, \u0026#34;没有可回退的快照\u0026#34; snap = self._snapshots.pop() self.K = snap[\u0026#39;K\u0026#39;] self.V = snap[\u0026#39;V\u0026#39;] self.position_version = snap[\u0026#39;version\u0026#39;] # ---------------------------------------------------------- # 核心函数：计算变化位置的新 K/V # ---------------------------------------------------------- def recompute_kv_for_changed_positions(model, token_sequence, changed_positions, kv_cache): \u0026#34;\u0026#34;\u0026#34; 只对 changed_positions 上的 token 重新计算 K 和 V， 其余位置直接复用 kv_cache 中已有的值。 这就是\u0026#34;合并重写\u0026#34;的实质： - 旧缓存（不变位置）：直接读取，零计算 - 新计算（变化位置）：只算这几个 token，写回缓存 Args: model : Transformer 模型 token_sequence : 当前完整 token 序列 [batch, seq_len] changed_positions : 本步发生变化的位置索引列表 kv_cache : 当前 KV 缓存对象 Returns: updated_kv_cache : 更新后的 KV 缓存 \u0026#34;\u0026#34;\u0026#34; if len(changed_positions) == 0: return kv_cache # 没有变化，直接返回 # ── Step 1：只取出变化位置的 token embedding ────────── # 不需要对整个序列做 embedding，只做几个 token changed_token_ids = token_sequence[:, changed_positions] # [batch, n_changed] changed_embeddings = model.embed(changed_token_ids) # [batch, n_changed, d_model] # ── Step 2：逐层计算这些位置的新 K 和 V ─────────────── # # 关键近似在这里： # 每层的输入 h_i^(l-1) 理论上依赖上层所有 token 的注意力结果。 # 近似处理：对变化位置，用当前层已有的完整 K/V 做注意力， # 得到这些位置的新隐状态，再算新的 K/V。 # 对不变位置：直接复用缓存，不做任何计算。 # new_K_all_layers = [] new_V_all_layers = [] h = changed_embeddings # [batch, n_changed, d_model] 初始为 embedding for layer_idx, layer in enumerate(model.layers): # 2-a. 用当前层已缓存的完整 K/V 做 cross-attention： # 变化位置的 query 去 attend 整个序列（包括不变位置的缓存 K/V） # 这样变化位置的隐状态就能看到完整的上下文 K_full = kv_cache.K[layer_idx] # [batch, heads, seq_len, head_dim] V_full = kv_cache.V[layer_idx] # [batch, heads, seq_len, head_dim] # 只对变化位置做 attention（query 来自变化位置，key/value 来自完整缓存） Q_changed = layer.W_Q(h) # [batch, n_changed, d_model] h = layer.cross_attend(Q_changed, K_full, V_full) # [batch, n_changed, d_model] h = layer.ffn(h) # [batch, n_changed, d_model] # 2-b. 用更新后的隐状态计算这些位置的新 K 和 V new_K = layer.W_K(h) # [batch, n_changed, d_model] → reshape to [batch, heads, n_changed, head_dim] new_V = layer.W_V(h) new_K_all_layers.append(new_K) new_V_all_layers.append(new_V) # ── Step 3：合并写回缓存（只写变化的位置）───────────── # \u0026#34;合并\u0026#34;：新算的几个位置 + 原来缓存的其余位置 = 完整 KV 缓存 new_K_tensor = torch.stack(new_K_all_layers) # [n_layers, batch, heads, n_changed, head_dim] new_V_tensor = torch.stack(new_V_all_layers) kv_cache.update_positions(changed_positions, new_K_tensor, new_V_tensor, step=current_step) return kv_cache # ---------------------------------------------------------- # 草稿阶段：带合并重写的掩码扩散解码 # ---------------------------------------------------------- def draft_with_kv_merge_rewrite(model, context_tokens, S_draft=3): \u0026#34;\u0026#34;\u0026#34; 草稿阶段的推理循环。 每步只 unmask 少数 token，用合并重写更新 KV 缓存。 Args: context_tokens : 场景上下文 token（视觉/导航/状态）[batch, n_ctx] S_draft : 草稿阶段总步数 Returns: draft_tokens : 填满的轨迹 token 序列 [batch, L] kv_cache : 最终 KV 缓存（供 AutoEdit 阶段复用） \u0026#34;\u0026#34;\u0026#34; L = 16 # 轨迹 token 数量 batch = context_tokens.shape[0] # ── 初始化：轨迹部分全 assessments，和上下文拼在一起 ──────── traj_tokens = torch.full((batch, L), MASK_TOKEN_ID) full_sequence = torch.cat([context_tokens, traj_tokens], dim=1) # full_sequence: [batch, n_ctx + L] # ── 计算上下文前缀的 KV 缓存（只算一次，整个推理过程复用） # 上下文 token 用因果注意力，所以这部分可以完美缓存 kv_cache = compute_prefix_kv(model, context_tokens) # 见论文\u0026#34;共享前缀 KV 缓存\u0026#34; # ── 初始化轨迹 token 的 KV（全 assessments 的 embedding）───── mask_embedding = model.embed(traj_tokens) # [batch, L, d_model] initial_K, initial_V = model.compute_kv(mask_embedding) kv_cache.update_positions(list(range(L)), initial_K, initial_V, step=0) # ── 草稿循环 ───────────────────────────────────────────── unmasked_positions = set() # 已经填好的位置 for s in range(S_draft): # 用当前完整 KV 缓存做前向，得到所有位置的 logits # （只对仍是 assessments 的位置采样，已填好的位置直接保留） logits = model.forward_with_kv(full_sequence, kv_cache) # logits: [batch, n_ctx + L, vocab_size] traj_logits = logits[:, -L:, :] # 只取轨迹部分的 logits probs = softmax(traj_logits, dim=-1) # 选出本步要填入的位置（最高置信度的那批 assessments） n_to_unmask = compute_unmask_count(s, S_draft, n_masked=(L - len(unmasked_positions))) still_masked = [i for i in range(L) if i not in unmasked_positions] confidences = {i: probs[0, i, :].max().item() for i in still_masked} newly_unmasked = sorted(still_masked, key=lambda i: -confidences[i])[:n_to_unmask] # 采样并填入 for pos in newly_unmasked: sampled_token = sample_from(probs[0, pos]) full_sequence[0, -L + pos] = sampled_token unmasked_positions.add(pos) # ════════════════════════════════════════════════ # 合并重写：只对新填入的位置重算 K/V # 之前已 unmask 的位置 + 仍是 assessments 的位置 → 直接复用缓存 # ════════════════════════════════════════════════ kv_cache = recompute_kv_for_changed_positions( model = model, token_sequence = full_sequence, changed_positions = [n_ctx + pos for pos in newly_unmasked], # ↑ 转换为全序列索引（轨迹在末尾） kv_cache = kv_cache ) # 本步只重算了 n_to_unmask 个位置的 K/V，其余全部复用！ draft_tokens = full_sequence[:, -L:] # 提取轨迹 token return draft_tokens, kv_cache # ---------------------------------------------------------- # AutoEdit 阶段：带回退 + 合并重写的轨迹修正 # ---------------------------------------------------------- def autoedit_with_rollback(model, draft_tokens, context_tokens, kv_cache, S_edit=3): \u0026#34;\u0026#34;\u0026#34; AutoEdit 阶段的推理循环。 比草稿阶段多了一个\u0026#34;回退\u0026#34;能力： 如果某次修改后置信度反而更低，可以撤销这次修改。 Args: draft_tokens : 草稿轨迹 token [batch, L] context_tokens : 场景上下文 token [batch, n_ctx] kv_cache : 草稿阶段结束时的 KV 缓存（直接继承，无需重算上下文！） S_edit : AutoEdit 迭代轮数 Returns: edited_tokens : 修正后的轨迹 token [batch, L] \u0026#34;\u0026#34;\u0026#34; L = 16 full_sequence = torch.cat([context_tokens, draft_tokens], dim=1) for k in range(S_edit): # ── 保存当前快照，以备回退 ──────────────────────── kv_cache.save_snapshot() prev_sequence = full_sequence.clone() # ── 用当前完整 KV 做前向，得到每个位置的替换建议 ── logits = model.forward_with_kv(full_sequence, kv_cache) traj_logits = logits[:, -L:, :] probs = softmax(traj_logits, dim=-1) # 每个位置的\u0026#34;替换 token\u0026#34;和\u0026#34;置信度\u0026#34; x_hat = probs.argmax(dim=-1) # [batch, L] confidence = probs.max(dim=-1).values # [batch, L] # ── 构造 commit mask：选低置信度的非目标点位置 ──── goal_positions = get_goal_token_positions() commit_mask = torch.zeros(L, dtype=torch.bool) for i in range(L): if i in goal_positions: commit_mask[i] = False # 目标点永远不替换 elif confidence[0, i] \u0026lt; CONFIDENCE_THRESHOLD: commit_mask[i] = True # 置信度低 → 标记为替换 changed_positions = commit_mask.nonzero().squeeze(-1).tolist() if len(changed_positions) == 0: # 没有低置信度的位置，不需要修改，丢弃快照直接退出 kv_cache._snapshots.pop() break # ── 执行替换 ────────────────────────────────────── for pos in changed_positions: full_sequence[0, -L + pos] = x_hat[0, pos] # ── 合并重写：只更新被替换位置的 K/V ───────────── kv_cache = recompute_kv_for_changed_positions( model = model, token_sequence = full_sequence, changed_positions = [-L + pos for pos in changed_positions], kv_cache = kv_cache ) # ── 可选的质量检查：如果修改后置信度整体更低，回退 ─ new_logits = model.forward_with_kv(full_sequence, kv_cache) new_confidence = softmax(new_logits[:, -L:, :], dim=-1).max(dim=-1).values.mean() old_confidence = confidence.mean() if new_confidence \u0026lt; old_confidence * 0.95: # 修改让模型\u0026#34;更不确定\u0026#34;了，撤销这一轮 kv_cache.rollback() full_sequence = prev_sequence else: # 修改有效，丢弃快照（不再需要回退到这个点） kv_cache._snapshots.pop() edited_tokens = full_sequence[:, -L:] return edited_tokens # ---------------------------------------------------------- # 合并重写 vs 朴素重算：计算量对比 # ---------------------------------------------------------- # # 设： # seq_len = n_ctx + L = 512 + 16 = 528 # 每步变化位置 = n_changed（草稿阶段约 5~6，AutoEdit 阶段约 2~3） # n_layers = 28 # # 朴素重算（每步）： # 对完整序列重算所有 K/V # FLOPs ∝ n_layers × seq_len × d_model² # = 28 × 528 × 2048² ≈ 124G FLOPs # # 合并重写（每步）： # 只对 n_changed 个位置重算 K/V # FLOPs ∝ n_layers × n_changed × d_model² # = 28 × 6 × 2048² ≈ 1.4G FLOPs # （其余 522 个位置直接读缓存，零 FLOPs） # # 理论加速：124G / 1.4G ≈ 88× # # 实际加速 1.28×（14.7ms → 11.5ms）的原因： # · GPU kernel 启动开销、内存带宽瓶颈等 overhead 不随 FLOPs 线性缩减 # · KV 缓存的读写本身也有延迟 # · 合并重写引入了额外的 mask 判断和条件写操作 # 但在绝对延迟上节省了 3.2ms/帧，对 31.8ms 的总延迟已是可观的贡献 为什么 K 和 V 只依赖自己的上一层输出？ Transformer 每一层做了什么？ 每一层的计算可以拆成两步：\n输入：上一层的隐状态矩阵 H^(l-1)，形状 [seq_len, d_model] 每一行是一个 token 的向量表示 Step 1：自注意力（Attention） Step 2：前馈网络（FFN） 输出：本层的隐状态矩阵 H^(l)，形状 [seq_len, d_model] 关键在于：K 和 V 是在哪里算出来的？ K 和 V 是在第 l 层的注意力计算开始之前，直接从上一层的输出线性变换得到的：\nK^(l) = H^(l-1) · W_K ← 矩阵乘法，逐行独立 V^(l) = H^(l-1) · W_V ← 矩阵乘法，逐行独立 Q^(l) = H^(l-1) · W_Q ← 矩阵乘法，逐行独立 逐行独立是关键词。把上面的矩阵乘法写成逐行的形式：\nK^(l)_i = h^(l-1)_i · W_K ← 只用了第 i 行（第 i 个 token 的向量） V^(l)_i = h^(l-1)_i · W_V ← 同上 这个线性变换里完全没有其他 token 的信息，所以：\nK^(l)_i 和 V^(l)_i 只依赖 h^(l-1)_i，不依赖 h^(l-1)_j（j ≠ i）。\n那\u0026quot;互相影响\u0026quot;发生在哪里？ 发生在注意力的输出计算里，也就是 Q 和 K/V 做完点积之后：\n注意力输出： A^(l)_i = softmax( Q^(l)_i · (K^(l))ᵀ ) · V^(l) ↑ ↑ 这里 token i 的 Q 这里混入了所有 token 的 V 去和所有 token 的 K 做点积 所以注意力输出 A^(l)_i 确实包含了所有其他 token 的信息，但 K 和 V 本身在被计算出来的时候，还没有经历这一步混合。\n用一张流程图看清楚顺序 第 l 层： H^(l-1) │ ├─[×W_K]─→ K^(l)_i = h^(l-1)_i · W_K ← 此时只有自己 ├─[×W_V]─→ V^(l)_i = h^(l-1)_i · W_V ← 此时只有自己 └─[×W_Q]─→ Q^(l)_i = h^(l-1)_i · W_Q ← 此时只有自己 │ │ └──────┬───────┘ ↓ Attention(Q, K, V) ← 此处发生混合 ↓ A^(l)_i ← 此时包含所有 token 的信息 ↓ FFN(A^(l)_i) ↓ H^(l)_i ← 下一层的输入 用一个比喻来理解 想象一场圆桌会议，每个人（token）在发言前先准备自己的问题卡（Q）、名片（K）、资料包（V）。\n准备这三样东西的时候，每个人只看自己昨天（上一层）的笔记，不看别人的。所以名片和资料包只属于自己。\n然后会议开始：每个人拿着自己的问题卡去和所有人的名片做匹配，再根据匹配程度加权收集大家的资料包。混合发生在这一步，而不是准备名片和资料包的时候。\n相关论文 [[DiffusionDriveV2 论文阅读笔记]] [[DriveTransformer 论文阅读笔记]] [[RAD-2 论文阅读笔记]] [[World4Drive - 无需感知标注的端到端世界模型]] 总结一句话：ReflectDrive-2 教会了自动驾驶模型像人类老司机一样——先打草稿，再反思修改，最后稳稳开车。 而这种反思能力之所以有效，不是因为模型\u0026quot;天生就会改\u0026quot;，而是因为强化学习让起草和修改成了真正的搭档，共同对最终结果负责。\n","date":"2026-05-09T00:00:00Z","permalink":"/post/robotics/e2e/reflect-drive-2/","title":"ReflectDrive-2 论文阅读笔记"},{"content":"简单易学的攀岩绳收绳方法 来源： 小红书 链接： http://xhslink.com/o/1XzJuorEKFf 标题： 简单易学的攀岩绳收绳方法 学会这个方法，攀岩绳不在\u0026hellip; 标签： #攀岩 #户外 #收绳 #绳结 ","date":"2026-05-07T00:00:00+08:00","permalink":"/post/climbing/top-rope/rope-coiling/","title":"攀岩绳收绳方法"},{"content":"野攀先锋做顶（其一） 来源： 小红书 - 腾冲光合攀岩营地 链接： https://www.xiaohongshu.com/discovery/item/69fc40510000000038021748 标签： #野攀 #野攀课程 #攀岩新手 ","date":"2026-05-07T00:00:00+08:00","image":"/post/climbing/sport-climbing/anchor/cover.jpg","permalink":"/post/climbing/sport-climbing/anchor/","title":"野攀先锋做顶"},{"content":"X-Cache: Cross-Chunk Block Caching for Few-Step Autoregressive World Models Inference 🎯 一句话总结 X-Cache 发现了一个此前从未被利用的冗余维度——跨生成片段的 block 残差复用，在生产世界模型上以零训练代价实现了 2.6–2.7 倍加速，71% block 跳过率，画质无感知损失。\n📌 核心问题：为什么世界模型推理太慢？ 自动驾驶正越来越依赖世界模型——一种能根据车辆动作，实时生成逼真未来场景视频的 AI 系统。它相当于给汽车造了一个虚拟的沙盘世界，用来做强化学习训练和闭环评估。\n但问题来了：这些模型推理太慢了！不够实时，就没法真正投入使用。\n为什么旧方法不管用？ 已有的扩散加速方法（如 DeepCache、FlowCache、SCOPE）都是利用同一个视频片段、相邻去噪步骤之间的特征冗余来跳过计算。但小鹏的世界模型已经被蒸馏成\u0026quot;少步模型\u0026quot;（仅 4 步去噪），每一步都在做实质性的结构更新，几乎没有可复用的冗余，强行复用只会破坏画质。\n此外，交互式场景还有两个额外的拦路虎：\n驾驶动作不连续：刹车、转向在每个片段边界可能突变，破坏了\u0026quot;特征平滑\u0026quot;假设 无法并行：必须先生成当前帧、等决策系统给出下一步动作，才能生成下一帧 💡 X-Cache 的顿悟时刻 研究者发现了一个新的冗余维度：真实世界的场景在相邻片段之间变化是缓慢而平滑的！\n汽车从一帧到下一帧，路面、建筑、天空基本没变。因此，DiT（扩散变换器）中每个模块在\u0026quot;相同去噪步骤、相同模块编号\u0026quot;位置的输入，相邻片段之间高度相似。\n这种\u0026quot;跨片段冗余\u0026quot;来自物理世界的连续性，而不是去噪过程，因此完全不受少步蒸馏影响——是个全新的可利用轴！\n🔧 方法详解：X-Cache 是怎么工作的？ 基础设定：三个下标刻画每次计算 模型用三个下标刻画每次计算：\nn：当前在生成第几个 chunk（视频片段） t：当前在第几步去噪 b：当前在处理 DiT 的第几个 block 每个 block 做的事情是：新输出 = 旧输入 + block计算出的残差。X-Cache 的核心就是：把这个\u0026quot;残差\u0026quot;缓存下来，下次能用就直接复用，不用重算。\n2.1 跨片段残差缓存（核心灵魂） 思路极其简洁：\n当第 n 个片段的 (t, b) 位置计算完，把残差 r 存入缓存 到第 n+1 个片段的同一位置，如果判断\u0026quot;输入和上一次差不多\u0026quot;，就直接用缓存残差，跳过整个 block 的计算 第一个片段（n=0）是**\u0026ldquo;热身阶段\u0026rdquo;**，所有 block 都老老实实完整计算，把缓存填满，为后续复用打下基础。\n2.2 双指标门控机制：聪明地决定\u0026quot;跳不跳\u0026quot; 不能无脑跳，得先判断当前输入和上一次是不是真的\u0026quot;差不多\u0026quot;。X-Cache 设计了一套**紧凑指纹（fingerprint）**来快速评估。\n指纹怎么提取？ block 的输入是一个巨大的张量（帧数 × 高 × 宽 × 通道），全比太贵。于是按三维空间网格均匀采样 32 个 token，在帧、高、宽三个维度上按比例分配。\n关键设计：不是沿着扁平 token 序列均匀采样，而是在三维空间里分别均匀采样！\n为什么要这样？因为 1D 均匀采样对三个维度是\u0026quot;盲目的\u0026quot;——步长和维度大小的数值关系决定了采样会系统性地偏向某些坐标值。\n一个灾难性例子：假设 F=2帧，H=4高，W=16宽，采样 K=8 个点。1D 均匀采样步长是 128÷8 = 16，恰好等于 W。结果所有采样点的 w 值全是 0——永远落在最左边一列，w=1 到 w=15 完全没被采样到！\n正确的做法是按照三个维度的比例分别分配采样数量：\n$$k_F : k_H : k_W \\approx F : H : W, \\quad k_F \\cdot k_H \\cdot k_W \\approx K$$\n这样每个维度都有保证的覆盖，不会因为数字巧合而整体偏向某一侧。\n两个辅助频道 指纹还拼接两个\u0026quot;辅助频道\u0026quot;：\n全局均值频道：整个输入的均值，用来捕捉整体漂移 动作条件频道：把当前驾驶动作向量（油门、转向等）也放进指纹，让系统能直接感知\u0026quot;这次动作和上次有没有变\u0026quot;——这很关键，因为动作是通过 adaLN-Zero 注入每个 block 的，光看输入张量察觉不到 两个指标联合判断 有了指纹，再用两个指标联合判断是否跳过：\n指标 计算方式 捕捉什么变化 余弦相似度 取所有视角中的最小值 整体方向的漂移 最大 token 偏差 取所有视角的最大值 局部突变的异常点 只有当两个指标都通过，才允许跳过这个 block。这是一种\u0026quot;保守聚合\u0026quot;的安全设计。\n📐 余弦相似度：为什么取最小值？ 什么是余弦相似度？ 余弦相似度的本质，是衡量两个向量方向有多一致，而不是它们有多\u0026quot;大\u0026quot;。\n公式： $$s = \\frac{\\vec{A} \\cdot \\vec{B}}{|\\vec{A}| \\cdot |\\vec{B}|}$$\n分子是点积，分母是模长相乘——这个操作恰好等于两个向量夹角的余弦值。\n方向完全相同 → 值为 1 方向垂直 → 值为 0 方向相反 → 值为 -1 为什么不用欧氏距离？ 一个经典例子：\nA = [1, 2]，B = [100, 200]，C = [2, 1] A 和 B 的欧氏距离很远（差了 100 倍），但余弦相似度 = 1，因为方向完全相同 A 和 C 的欧氏距离很近，但余弦相似度 \u0026lt; 1，因为方向有偏差 在神经网络的特征空间里，特征的\u0026quot;意义\u0026quot;更多藏在方向里，而不是幅度里。\n为什么取最小值？ 场景还原：X-Cache 的世界是 7 个摄像头，分成 3 个视角组，每个视角组独立计算一个余弦相似度值。\n假设 7 个摄像头里，6 个的余弦相似度都是 0.999，但有 1 个（比如左后摄像头）突然出现了一辆加速超车的摩托车，相似度掉到了 0.80。\n取平均：≈ 0.97，可能还是超过阈值，系统认为\u0026quot;没问题，跳过吧\u0026quot;——但实际上左后视角已经发生了显著变化 取最小值：直接拿到 0.80，远低于阈值，系统正确地判断\u0026quot;不能跳，必须重算\u0026quot; 本质逻辑：这个 block 的计算是针对所有摄像头一起做的，它们共享同一个 (t, b) 位置的计算。如果任何一个视角发生了显著变化，整个 block 的输出都会受影响——因为网络内部的注意力机制会跨摄像头交互信息。\n生活类比：检查家里所有门窗是否都锁好了，判断标准不是\u0026quot;平均锁好程度\u0026quot;，而是\u0026quot;有没有一扇是开的\u0026quot;——只要有一扇没锁，整体就不安全。\n📊 最大 Token 偏差：捕捉局部突变 公式： $$d_{\\max} = \\frac{\\max\\left|\\phi(\\mathbf{x}^{(n)}) - \\phi(\\mathbf{x}^{(n-1)})\\right|}{\\text{mean}\\left|\\phi(\\mathbf{x}^{(n-1)})\\right| + \\epsilon}$$\n计算流程：\n对 32 个采样位置，每个位置算当前片段和上一片段的特征差值 找出差值最大的那一个位置 用上一片段特征的均值做归一化，得到相对偏差 为什么要关注\u0026quot;最大\u0026quot;的那个位置？ 一个典型场景：画面大部分区域（背景道路、天空）几乎没变，但某个局部位置（比如前方突然出现一辆急刹的卡车）发生了剧烈变化。\n余弦相似度：因为大多数位置都很稳定，整体方向差异不大，可能不会触发报警 最大 token 偏差：那个\u0026quot;卡车突然出现\u0026quot;的位置偏差极大，直接把最大值拉高，触发强制重算 所以两个指标是互补的：\n余弦相似度防的是\u0026quot;温水煮青蛙式的整体漂移\u0026quot; 最大 token 偏差防的是\u0026quot;局部突然爆炸式的异常变化\u0026quot; 2.3 自适应阈值：会自我调节的\u0026quot;松紧度\u0026quot; 固定一个全局阈值太粗暴——有些 block 天生就变化小（可以激进地跳），有些天生变化大（要保守）。\nX-Cache 为每个 (t, b) 位置单独维护一个指数移动平均（EMA），记录这个位置历史上的余弦相似度，然后把阈值设为略低于 EMA 的值（差一个边距 m=0.02）。\n效果是：\n跨片段几乎不变的模块 → 阈值越来越高，越来越激进地跳 变化较大的模块 → 阈值会自动保守 同时设定一个硬性安全底线（τ_floor=0.97），不管 EMA 多高，阈值都不会低于这个值。\n2.4 四重安全机制：强制重算的边界条件 即使指纹通过了测试，也有几种特殊情况必须\u0026quot;强制重算\u0026quot;：\n① 去噪步骤 t=0 保护（可选） 第 0 步的输入充满高斯噪声，条件信号（动作、文本等）对输出的影响最大，而且每次更新 KV 缓存时噪声都会重采样，导致相邻片段的 t=0 余弦相似度天然偏低。默认关闭跳过，确保条件信号能被充分吸收。\n消融实验表明：开启保护后跳过率从 71.3% 降到 53.5%，但画质毫无变化。自适应阈值已经能自己判断好 t=0 的情况。但研究者保留了这个开关，作为夜间、暴雨或极端轨迹变化下的额外安全边际。\n② 锚块（Anchor Blocks）保护 默认让第 0 个 block（前锚）永远不跳过。它的输出变化会像多米诺骨牌一样级联到后续 block，天然地迫使后续 block 重新计算它们自己的指纹——这是个优雅的级联保护机制。\n③ KV 更新帧保护（最重要！） 自回归生成时，每隔一段时间会有一个\u0026quot;KV 更新帧\u0026quot;，把当前生成的干净帧写入持久 KV 缓存，供未来所有片段的 cross-attention 使用。\n如果这次写入的 KV 带有缓存误差，未来所有帧都会受到污染，错误会永久传播！\n消融实验证明：关掉 KV 更新帧保护，PSNR 从 53.4 dB 崩塌到 21.5 dB，LPIPS 暴涨 3 个数量级——图像完全崩了！这是唯一不可妥协的硬性安全要求。\n④ 最大过期次数 如果某个 (t, b) 位置连续跳过次数超过阈值 M，强制重算一次，防止缓存太陈旧。\n🧪 实验结果：在真实赛道上跑一跑 实验设置 项目 详情 硬件 阿里 T-Head 真武（Zhenwu）810E AI 加速器，96GB HBM2e，BF16 精度 模型 小鹏自研 X-World（基于 WAN 2.2），7 个摄像头，12 帧/秒，4 步去噪 数据集 城市道路（7条）、高速公路（3条）、城市掉头（3条） 视频长度 每条 264 帧（约 22 秒） 核心结果 场景 PSNR SSIM LPIPS 跳过率 加速比 城市道路 51.37 dB 0.9990 3.3e-4 71.4% 2.65× 高速 54.67 dB 0.9991 1.9e-4 71.6% 2.66× 掉头 52.04 dB 0.9990 3.1e-4 71.3% 2.70× 加速效果稳如磐石：三种场景的跳过率只差 0.3 个百分点，推理时间只差 30ms——加速是由 DiT 的结构（block 位置）决定的，不是场景决定的。\n掉头反而比城市道路画质更好？ 看似反直觉，但有合理解释：掉头片段中大部分时间其实在直行；转弯时摄像头里物体本来就有运动模糊，高频纹理本来就少，缓存误差更难在像素上显现。\n误差分布分析 所有场景下：\nBlocks 1-19 的跨片段余弦相似度都在 0.95 以上 Blocks 20-26 降到约 0.90（这个模式完全由模型结构决定，与场景无关） 跳过率对应地：\nBlocks 1-19 稳定在约 0.75（1 个热身 chunk 对应的 4 个 chunk 复用窗口） Blocks 20-26 降到约 0.69 🔬 消融实验：拆开零件逐个测 实验 效果 t=0 保护开启 跳过率 71.3% → 53.5%，加速 2.59× → 1.84×，画质不变（自适应阈值已能自行判断） KV 更新帧保护关闭 PSNR 53.4 dB → 21.5 dB（崩塌！），LPIPS 暴涨 3 个数量级 前锚块 Fn=0（关闭） 跳过率多 1.9%，每 chunk 省 70ms，画质几乎不变（但保留为安全边际） τ_floor 从 0.90 → 0.96 所有指标完全不变（当前数据集中几乎没有 token 落在这个区间） ❓ 技术细节问答 Q1：余弦相似度中位数和 75 分位数都是 1.0，这合理吗？ 完全合理！\n物理世界连续性决定了：车辆以正常速度行驶时，相邻两个 chunk 之间可能只过了零点几秒：\n远处的建筑、天空、道路线几乎纹丝不动 前方车辆可能只移动了几厘米 大量摄像头画面的大部分区域像素级别上都几乎一样 神经网络的特征是从像素里提取的——如果输入像素都没怎么变，特征自然也不会变。\n测试集的高速公路和城市直行占了绝大多数时间，\u0026ldquo;场景几乎静止\u0026quot;的 chunk 占了绝大多数，对应的余弦相似度自然集中在 1.0。\n这个现象反过来印证了 X-Cache 的核心前提：驾驶世界模型的跨 chunk 冗余极高，不是\u0026quot;大概差不多\u0026rdquo;，而是\u0026quot;大多数情况下特征根本就没变\u0026quot;。\nQ2：Chunk 是什么？ Chunk 就是世界模型每次生成的一小段视频片段。\n世界模型不是一口气把整段视频全生成出来的，而是像说话一样一段一段往外\u0026quot;吐\u0026quot;：\nchunk 1 → chunk 2 → chunk 3 → chunk 4 → ... 每个 chunk 包含若干帧画面，生成完后交给决策模块，决策模块根据画面决定下一步动作（转向、油门），再把动作传回来，生成下一个 chunk。\n这就是闭环交互：\n生成 chunk n → 决策系统观察 → 发出动作 → 生成 chunk n+1 → ... 每个 chunk 内部还有去噪过程：\n噪声 →[t=0]→[t=1]→[t=2]→[t=3]→ 清晰画面 所以论文里有两个嵌套的循环：\n外层：chunk n → chunk n+1 → \u0026hellip;（自回归生成）← X-Cache 利用这个轴的冗余 内层：去噪步骤 t=0 → t=1 → t=2 → t=3（扩散去噪）← 旧方法盯着这个轴，但 4 步去噪里几乎没有冗余 🚧 局限性：诚实的声明 论文非常诚实地列出了边界条件：\n所有测试只在 22 秒以内的片段、X-World 训练分布内的场景上做过——夜间、暴雨、激进驾驶、长时间高速巡航等场景尚未验证 超参数是针对单个 held-out 片段调的，选择的是\u0026quot;安全而非激进\u0026quot;的配置，当前 PSNR 约 53 dB、SSIM\u0026gt;0.999 实际上有相当余量 Pareto 前沿尚未探索，降低 τ_floor、放松 τ_dev、减少前锚等操作可以换取更高跳过率 📝 结论与思考 X-Cache 的创新点在于找到了一个与现有方法正交、互补的冗余维度：\n现有方法：跨去噪步骤缓存（适用于多步扩散） X-Cache：跨生成片段缓存（适用于少步蒸馏后的模型） 两个轴可以叠加使用，而不是竞争关系。\n关键启示：\n工程优化要从数据本身的特性出发——物理世界连续性是真正可靠的冗余来源 安全机制要分层设计——KV 更新帧保护是硬性要求，其他是软性安全边际 自适应机制让系统在不同场景下自动调节行为，减少人工调参负担 这是一篇来自工业界、非常接地气的系统优化工作：发现了一个被学术界忽略的新冗余维度，用一套精心设计的安全机制把它安全地用起来，在真实生产系统上拿到了 2.6 倍的加速。\n","date":"2026-04-30T00:00:00+08:00","permalink":"/post/robotics/e2e/x-cache/","title":"X-Cache"},{"content":"核心贡献: 提出了一个统一的生成器-鉴别器框架，通过\u0026quot;扩散生成器放飞自我地想 + RL鉴别器冷静地挑好的 + BEV-Warp快速地练习\u0026quot;，让自动驾驶规划器既能建模多模态的未来不确定性，又能在闭环交互中为后果负责。 一、核心动机：为什么需要这套组合拳？ 1.1 模仿学习的三重困境 想象一下，你在教一个新手司机开车。用模仿学习（Imitation Learning）的方式，就像给他一大堆\u0026quot;老司机录像\u0026quot;反复看——他能学到很多驾驶技巧，但问题是，他只知道\u0026quot;该怎么开\u0026quot;，却从来没有为自己的失误付出过代价。\n作者首先梳理了自动驾驶运动规划领域的\u0026quot;江湖现状\u0026quot;：\n第一种流派——回归规划器（如 VAD、UniAD）：直接预测一条轨迹。听起来简单粗暴，问题是现实中司机会有很多合理选择（超车？还是跟车？），而它只会吐出一个\u0026quot;平均答案\u0026quot;，像个永远在走中间路线的优柔寡断者。\n第二种流派——选择型规划器（如 VADv2、Hydra-MDP）：预先定义一堆候选轨迹，然后从里面选一条。这就像出行前只有几条固定路线可选，灵活性大打折扣。\n第三种流派——扩散规划器（Diffusion-based）：用扩散模型来生成连续的多模态轨迹分布，理论上最优雅。但它们在纯模仿学习训练下面临关键挑战：真实驾驶数据有噪声和分布不均匀，导致扩散模型对某些轨迹区域学习不充分，偶尔会生成低质量或不稳定的轨迹——这在对安全极度敏感的规划任务中尤为致命。\n更要命的是，模仿学习存在**\u0026ldquo;因果混淆\u0026quot;问题**——模型学到的是状态和动作之间的相关性，而不是真正的因果关系，导致\u0026quot;走捷径\u0026quot;行为。而且它的开环训练范式与真实驾驶的闭环本质存在根本性的错配。\n1.2 强化学习的拦路虎 引入强化学习（RL）本是解药——让模型在闭环仿真中真正\u0026quot;为自己的行为负责\u0026rdquo;。但直接将RL应用于生成式规划器极具挑战性：\n信用分配困境：RL的奖励信号通常是低维标量，而动作空间是高维、时间结构化的轨迹，这使得信用分配极其复杂 优化不稳定：优化高维轨迹空间极易发散 仿真环境限制：游戏引擎仿真器（如CARLA）简化了智能体行为；基于重建的仿真器复杂且昂贵；学习型世界模型在长时序、多视角生成上力不从心 1.3 RAD-2 的解题思路 RAD-2 的核心洞见是：不要直接用稀疏的标量奖励去优化那个高维的扩散轨迹空间——那太难了！\n换个思路：\n让 RL 只优化鉴别器（discriminator）——因为鉴别器的输出天然就是低维的分数，和奖励信号\u0026quot;门当户对\u0026quot; 同时，用一种特殊的方法来渐进式地引导生成器向好的轨迹区域靠拢 一句话概括：扩散生成器放飞自我地想 + RL鉴别器冷静地挑好的 + BEV-Warp快速地练习 = 一个既有丰富想象力、又懂得为后果负责的自动驾驶大脑。\n二、方法详解：RAD-2 的核心武器库 这是论文最精华的部分，包含四件\u0026quot;法宝\u0026quot;。\n2.1 生成器-鉴别器框架 整个框架分两个组件，分工明确，各司其职：\n扩散生成器（Diffusion Generator）——\u0026ldquo;天马行空\u0026rdquo; 生成器的任务是生成多样化的候选轨迹。具体流程：\n第一步：场景编码\n把多视角摄像头图像压缩成鸟瞰图（BEV）特征 分别提取静态地图元素（车道线、路边界）、动态智能体（周围车辆、行人）和导航指令的 Token 嵌入 融合成统一的场景嵌入 $E_{\\text{scene}}$ 第二步：轨迹生成\n对 M 个独立模式，从高斯噪声出发 通过 K 步迭代去噪，每一步都在场景嵌入的条件下完成 最终得到 M 条连续的 (x, y) 轨迹候选 这就像同时给出 32 种不同的\u0026quot;下一步棋\u0026quot;供选择 RL鉴别器（RL-based Discriminator）——\u0026ldquo;严格把关\u0026rdquo; 鉴别器的任务是对生成器吐出的每条轨迹打一个 0~1 的分数，分数越高代表这条轨迹的长期驾驶质量越好。\n具体架构：\n每条轨迹通过 MLP 嵌入后，用一个带 [cls] Token 的 Transformer Encoder 提取轨迹级别的查询向量 通过交叉注意力与地图特征、BEV特征、智能体特征进行多源融合 最后经过 Sigmoid 输出一个标量分数 推理时扩展：这一架构天然支持推理时扩展——增加候选数量 M，让鉴别器探索更密集的动作空间，在不重新训练的情况下找到更高质量的解。\n2.2 BEV-Warp 仿真环境——\u0026ldquo;秘密武器\u0026rdquo; 这是 RAD-2 能够大规模训练的\u0026quot;秘密武器\u0026quot;。\n传统方法的痛点 传统方法想做闭环评估，要么开游戏引擎渲染图片，要么跑神经网络生成逼真画面——都慢得要命。\nRAD-2 的思路：不生成图片，直接变形特征图！ 在每个仿真步骤：\n提取参考 BEV 特征 $\\mathcal{B}^{\\text{ref}}_{t+1}$（来自真实录制的日志） 根据仿真自车位姿与参考位姿的偏差，推导变形矩阵 $\\mathbf{M}{t+1} = (\\mathcal{P}{t+1})^{-1}\\mathcal{P}^{\\text{ref}}_{t+1}$ 通过双线性插值将参考特征进行空间变形，合成新的观测特征 $\\mathcal{B}_{t+1}$ 比喻：就像玩拼图，你不需要重新印一张新图，只需要把原来的图平移一下、旋转一下，就能模拟出\u0026quot;车向前开了10米\u0026quot;的新视角——速度快到飞起！\n为什么能这样做？ BEV 特征具有强烈的空间等变性（Spatial Equivariance）：对特征施加变形矩阵，解码出的感知输出（如3D边界框）会随之精确对应物理世界的位移。这保证了模拟的可靠性。\n2.3 关于 BEV-Warp 动态障碍物的讨论 这是一个非常关键的细节讨论，值得深入理解。\n问题：动态障碍物怎么处理？ 直觉上，直接平移 BEV 的操作对静态障碍物有效，但动态障碍物本身也在移动啊！\nBEV-Warp 的处理方式 BEV-Warp 没有独立地对动态障碍物建模和预测运动。它的做法是：\n直接从真实日志里取下一时刻的完整 BEV 特征（包含了那一时刻周围所有车辆已经运动到的真实位置），然后只对这张完整特征图施加一个刚体平移/旋转变换，补偿自车偏离参考轨迹产生的视角差。\n用一个比喻理解：就像你有一段真实录像，每帧都记录了所有人的真实位置。BEV-Warp 不是去\u0026quot;演算\u0026quot;下一帧里别的车在哪，而是直接拿真实录像的下一帧，再把画面平移/旋转一下来对准你当前的视角。\n这意味着什么？\n维度 结论 动态障碍物的运动轨迹 ✅ 是真实的——因为它们的位置直接来自真实日志 动态障碍物与自车的交互 ❌ 是假的——真实日志里的其他车不会因为你的自车走了不同路径而改变行为 这个妥协带来的根本缺陷 场景A——自车偏离但障碍物不响应： 假设原本参考轨迹是跟车，但仿真自车选择了超车变道。真实日志里前车不会因此加速或让路，依然按原轨迹走。超车后自车与前车的空间关系在 BEV 特征里会出现一个几何偏差。\n场景B——变形引入的空间误差： 对动态元素，变形矩阵仅补偿了自车的视角变化，没有补偿动态障碍物本身在这段时间内的独立运动。\n为什么这个方案还能奏效？ 论文能自圆其说，依赖以下几点：\n1. 自车偏离量通常很小 BEV-Warp 的设计前提是：仿真自车不会和参考轨迹偏差太远。在短时序的 rollout 里（10~20秒），如果自车的偏差是米级的，对 BEV 特征的影响是局部的、可用刚体近似的。\n2. 奖励计算绕开了特征误差 安全奖励 TTC 的计算方式：$\\mathcal{V}_{\\text{env}}(t+k)$ 代表的是环境在绝对时间 $t+k$ 的真实状态（ground-truth occupancy），而不是从变形后的 BEV 特征里解码出的感知结果。\n关键洞察：奖励信号是直接查真实日志计算的，而不是依赖那张变形后可能有误差的特征图。特征图只是给规划器看的\u0026quot;输入视图\u0026quot;，碰没碰、安不安全，是对照真实数据算的。\n3. 它的目标是训练 RL，不是完美仿真 BEV-Warp 的定位从来不是\u0026quot;真实模拟所有交互\u0026quot;，而是给策略提供足够密度的、方向大致正确的梯度信号。\n总结对比表 静态障碍物 动态障碍物 位置来源 真实日志特征 真实日志特征 运动建模 无需（静止） ❌ 完全没有独立预测 视角校正 ✅ 刚体变换精确 ⚠️ 只校正了自车视角偏差 交互响应 N/A ❌ 其他车不响应自车行为变化 奖励计算 查真实日志 查真实日志（绕开了特征误差） 2.4 联合策略优化：三步迭代循环 整个训练过程分三个阶段循环推进，如同一个自我进化的闭环。\n2.4.1 时序一致性采样（Temporally Consistent Rollout） 问题：如果每一步都重新采样一条全新的轨迹来执行，那么\u0026quot;这一步走哪\u0026quot;和\u0026quot;后来撞没撞车\u0026quot;之间的关联就会被打散——强化学习的信用分配变得极其困难，就像一场考试里每道题都由不同的人作答，分数却只给到整份试卷一样。\n解法：一旦在时刻 $t$ 选定了最优轨迹，就把它对应的控制序列重用 $H_{\\text{reuse}}$ 步（设为 8 步最优），而不是每步都重新规划。这保证了\u0026quot;这条轨迹导致的结果\u0026quot;能被清晰地归因到这条轨迹的选择上。\n用时间轴画出来：\n时刻: t=0 1 2 3 4 5 6 7 8 9 10 11 ... 决策: ★ ★ ★ 执行: [← 锁定执行 A →][← 锁定执行 B →][← 锁定执行 C →] (Hreuse=8) (Hreuse=8) (Hreuse=8) ★ 是\u0026quot;真正做决策的时刻\u0026quot;——鉴别器重新对 M 条候选轨迹打分、选出最优的时刻。\n为什么 $H_{\\text{reuse}}=8$ 而不是 1？ 这是一个经过实验验证的关键设计。\n$H_{\\text{reuse}}$ 效果 太小（=1或2） 模式切换频繁，奖励被稀释到太多决策点，归因噪声大；rollout 间差异来源混乱，组内对比失效；轨迹跳变、不可跟踪 太大（=16+） 归因清晰但决策点太少，更新频率低；锁定时间过长导致策略对环境变化的响应迟钝 =8（最优） 让每个决策的影响持续足够长以便清晰归因，同时又不长到让策略对动态环境失去反应能力 比喻：你要评估\u0026quot;选A路还是选B路\u0026quot;哪个更好，最合理的方式是让两辆车各自沿着选定的路开一段时间再比较结果。而不是每走一步都重新随机选一次路。\n2.4.2 鉴别器的 RL 优化（TC-GRPO） 奖励设计：系统设计了两个互补的奖励：\n🛡️ 安全奖励（$r_{\\text{coll}}$）\n通过反事实插值量化碰撞风险 计算\u0026quot;碰撞时间（TTC）\u0026quot; 序列级奖励取整个 rollout 中最危险时刻的 TTC 边际 一次安全违规就能主导整个序列的奖励，强制执行严格保守的驾驶策略 🚀 效率奖励（$r_{\\text{eff}}$）\n评估自车相对于参考轨迹的行驶进度 用稳定窗口惩罚机制将车辆进度限制在目标效率区间（1.05~1.10倍参考速度）内 既惩罚磨磨蹭蹭，也惩罚冒进超速 TC-GRPO 优化目标：\n这是从 DeepSeek-R1 里借鉴灵感的 GRPO 方法的\u0026quot;驾驶版\u0026quot;。对于从同一初始状态出发的 G 条 rollout：\n$$\\mathcal{L}{i,t\\in\\mathcal{K}i} = \\min\\Big(\\rho{i,t} A_i,; \\text{clip}(\\rho{i,t}, 1-\\varepsilon, 1+\\varepsilon) A_i\\Big)$$\n其中：\n$A_i$：标准化优势（每条 rollout 奖励相对于组内均值和标准差） $\\rho_{i,t} = \\frac{\\mathcal{D}\\phi(\\hat{\\tau}^*{i,t} \\mid o_{i,t})}{\\mathcal{D}{\\phi{\\text{old}}}(\\hat{\\tau}^*{i,t} \\mid o{i,t})}$：重要性采样比 $\\mathcal{K}_i$：rollout 中\u0026quot;启动新的锁定执行区间的决策时刻\u0026quot;的集合 关键设计：策略梯度只施加在 ★ 时刻（决策点），这保证了优势信号强化的是时序一致的轨迹假设，而非时序上缺乏相干性的独立动作样本。\n梯度为什么只在 ★ 时刻施加？ 问题一：在非决策时刻（比如 t=1,2,3），鉴别器根本没有输出任何分数——它没有做选择，不存在一个 $\\mathcal{D}_\\phi(\\hat{\\tau}^* \\mid o_1)$ 这样的量。强行在 t=1 打梯度，等于是在惩罚一个\u0026quot;没有做过的决定\u0026quot;。\n问题二：整个 rollout 只有一个序列级奖励 $r_i$。如果强行把这个奖励分摊到每一步，t=0 的决策和最终碰撞隔了十几步，t=14 的决策和碰撞只隔了 1 步——把同一个奖励同等地归因给这两个时刻，是严重失真的。\n对比 LLM 的 GRPO：\nLLM 中的 GRPO RAD-2 的 TC-GRPO 决策单元 每个生成的 token 每个锁定执行区间的起点 ★ 奖励来源 整段回答的最终得分 整段 rollout 的碰撞/效率分数 梯度施加点 每个 token 位置 只有 ★ 时刻，跳过锁定执行步 奖励如何分配到多个决策时刻？ 假设一条 rollout 的 $A_i = -0.8$，有 t=0, 4, 8 三个决策时刻。三个时刻的损失项是：\n$$\\mathcal{L}{i,0} = \\min!\\Big(\\rho{i,0} \\cdot (-0.8),\\ \\text{clip}(\\rho_{i,0}, 1-\\varepsilon, 1+\\varepsilon)\\cdot(-0.8)\\Big)$$\n三项进入求和后被 $|\\mathcal{K}_i|=3$ 均等归一化，没有任何时刻拿到更大的权重。\n但每个时刻的实际梯度大小由各自的重要性采样比 $\\rho_{i,t}$ 决定：\n$$\\rho_{i,t} = \\frac{\\mathcal{D}\\phi(\\hat{\\tau}^*{i,t} \\mid o_{i,t})}{\\mathcal{D}{\\phi{\\text{old}}}(\\hat{\\tau}^*{i,t} \\mid o{i,t})}$$\n设计哲学：三个决策点对最终结果的贡献是\u0026quot;串联\u0026quot;的，而不是\u0026quot;并联独立\u0026quot;的。用统一的 $A_i$ + 各自不同的 $\\rho_{i,t}$ 来自然地区分惩罚强度，让数据本身说话。\n自适应熵正则化 什么是熵？\n熵来自信息论，衡量的是**\u0026ldquo;不确定性\u0026quot;或\u0026quot;混乱程度\u0026rdquo;**。\n用抛硬币理解：\n公平硬币（50%-50%）：熵最大 = 1 bit，完全猜不到结果 作弊硬币（99%-1%）：熵很小 ≈ 0.08 bit，几乎能确定结果 熵越高 = 分布越均匀 = 越不确定；熵越低 = 分布越集中 = 越确定。\n鉴别器的\u0026quot;分布\u0026quot;\n鉴别器对每条候选轨迹输出一个 0~1 的分数。在一批候选轨迹里，可以把这些分数理解为一个偏好分布：\n高熵状态：分数都差不多，鉴别器在说\u0026quot;我也不太确定哪条更好\u0026quot; 低熵状态：分数极度集中（比如一条0.99，其他都0.01），鉴别器在说\u0026quot;A 碾压所有\u0026quot; 低熵有什么问题？\n在 RL 训练中，鉴别器会自然趋向低熵——每次更新都在奖励高分、惩罚低分，分数越来越往两极分化。\n这带来两个后果：\n梯度消失：Sigmoid 在接近 0 或 1 时梯度几乎为零，学习停滞 探索停止：鉴别器不再给其他轨迹机会，系统失去发现更好解的可能性 自适应熵正则化——只在需要时介入\nRAD-2 的改进：用可学习的温度参数 $\\lambda$ 控制正则化强度，只在批次平均熵低于目标值时才激活：\n$$\\beta = \\exp(\\lambda) \\cdot \\mathbf{1}{[\\bar{\\mathcal{H}} \u0026lt; \\bar{\\mathcal{H}}{\\text{target}}]}$$\n大白话翻译：\n如果 当前平均熵 \u0026lt; 目标熵： β = exp(λ) → 熵正则化开启，踩刹车 否则： β = 0 → 熵正则化关闭，放手让它跑 比喻：想象鉴别器是招聘面试官。HR 只在面试官\u0026quot;过于武断\u0026quot;时出面提醒：\u0026ldquo;你确定看够了吗？\u0026quot;——平时不干涉，只在关键时刻介入。\n消融实验显示：加入熵项后碰撞率从 0.254 降至 0.234，Safety@1 从 0.697 提升至 0.730。\n2.4.3 生成器的在策优化（OGO） 鉴别器再厉害，也只是从已有候选里\u0026quot;选最好的\u0026rdquo;。如果生成器本身生成的轨迹质量就不高，鉴别器也巧妇难为无米之炊。OGO 就是专门来\u0026quot;提升生成器的上限\u0026quot;的。\n核心思路：不直接用 RL 去优化高维轨迹空间，而是把闭环反馈转化为结构化的纵向优化信号，仅调整速度/加速度剖面，保持横向路径不变：\n安全驱动减速：当 TTC 低于安全阈值时，压缩行驶距离（减速） 效率驱动加速：当自车落后参考轨迹且无碰撞风险时，延展行驶距离（加速） 为什么只动纵向？横向怎么办？\nOGO 对横向路径（转向、变道、绕障的空间曲线）完全不施加任何修正。\n设计者的核心论点是：在驾驶场景里，大多数安全和效率问题，首先体现在纵向维度上。\n想想日常驾驶中的典型危险：\n前车突然刹车 → 需要减速，而不是转向 路口黄灯 → 需要加速通过或减速停止 跟车太近 → 需要拉开纵向距离 横向问题（压线、走错车道）通常在生成阶段就会被鉴别器过滤掉——一条明显走错车道的轨迹在重排序时就会得到低分。\n分工逻辑：横向质量靠鉴别器的重排序来保证，纵向质量靠 OGO 来迭代改进。\n局限性：这种系统性横向偏差无法被 OGO 修正。只能靠最初的模仿学习预训练纠正，或靠积累足够多的真实数据让生成器自然学到正确的横向分布。\n具体怎么做到\u0026quot;只动纵向\u0026quot;？\n关键在于轨迹的参数化方式。把轨迹想象成一段铁路：\n横向路径 = 铁轨本身的弯曲形状（空间曲线） 纵向剖面 = 火车在这段铁轨上的速度和加速度（时间进度） OGO 的操作：铁轨的形状一分不动，只调整火车的速度表。\n纵向优化通过对原始加速度值施加常数偏移并重新积分：\n$$\\text{压缩后的行驶距离} = \\text{原始距离} \\times \\rho$$\n其中 $\\rho \\in (0,1)$ 用于减速，$\\rho\u0026rsquo; \u0026gt; 1$ 用于加速。\n数字例子：\n原始轨迹（空间坐标），总行驶距离 ≈ 9.0m： h=0: (0.0m, 0.0m) h=1: (2.0m, 0.3m) h=2: (4.2m, 0.5m) h=3: (6.5m, 0.6m) h=4: (9.0m, 0.6m) 压缩后（ρ=0.7），总行驶距离 ≈ 6.3m： h=0: (0.0m, 0.0m) h=1: (1.4m, 0.21m) ← 沿同一条曲线，走到70%的位置 h=2: (2.94m, 0.35m) h=3: (4.55m, 0.42m) h=4: (6.3m, 0.42m) 空间曲线的形状（弯曲程度、转向角）完全没变，只是车在同样的时间窗口里走得更慢了。\n技术原理：空间曲线可以用弧长参数化来描述。加速度的积分只影响沿弧长方向的进度，对垂直于运动方向的横向位移没有任何作用——这是微分几何上的天然解耦。\n优化循环的异步机制：\n每次新批次 rollout 入库就更新鉴别器 每积累满 8 批新 rollout（整个 buffer 刷新一遍）才优化生成器 形成 8:1 的训练频率比 确保鉴别器持续感知最新数据，生成器则在积累足够数据后做一次大步调整 三、3DGS 验证：真实感渲染下的二次检验 3.1 两个环境的本质区别 BEV-Warp 环境（日常训练）：\n\u0026ldquo;作弊版\u0026quot;仿真——不渲染任何图片，直接对 BEV 特征图做矩阵变形 极其高效，但本质上是一种几何近似 场景里缺少真实的光照、纹理、天气等视觉细节 3DGS 环境（验证环境）：\n使用 3D 高斯泼溅技术 直接从 3DGS 场景中，根据自车当前位姿 $\\mathcal{P}_t$，用可微渲染算子 $\\mathcal{R}(\\cdot)$ 渲染出真实的多视角摄像头图像 再正常走完整的感知→BEV 编码→规划流程 比喻：BEV-Warp 是让学生在\u0026quot;填涂卷\u0026quot;上练习，3DGS 是让他进真实考场做\u0026quot;实操题\u0026rdquo;。问题是：在简化版练习场里练出来的技能，在逼真考场里还能用吗？\n3.2 训练数据的分层结构 RAD-2 有两套并行的训练体系：\n第一套（主力训练，BEV-Warp 环境）\n从真实驾驶日志收集 5万条片段 筛选后保留安全类和效率类场景各 1万条 做 RL 训练，各 512条 做评估 生成器和鉴别器都在这里联合优化 这是论文方法的主战场 第二套（补充训练，3DGS 环境）\n来自 Senna-2 基准的 1044条训练片段 只在 3DGS 里对鉴别器做额外的 RL 训练 生成器在这里不做额外训练 3DGS 渲染太慢，无法支撑完整的联合优化 工程考量：3DGS 每一步都要调用可微渲染器生成多视角真实感图像，再走完整感知流水线，计算代价极高。所以只让相对轻量、参数少的鉴别器做增量适配——本质上是让鉴别器在\u0026quot;更逼真的考场\u0026quot;里做一次针对性微调，以弥合域差距。\n3.3 3DGS 里的闭环流程 感知输入是真实渲染图像：每一步用当前自车位姿向 3DGS 场景发出渲染请求 感知→BEV→规划走完整链路：送入预训练感知骨干网络，编码成 BEV 特征 iLQR 控制器执行轨迹：选定最优轨迹后，计算底层控制指令 奖励和指标与 BEV-Warp 一致：仍然计算碰撞率、安全边际 3.4 为什么能迁移？ 关键保障：BEV 特征的空间等变性。\n不管输入是变形后的 BEV 特征，还是由真实渲染图片走感知流程得到的 BEV 特征，骨干网络输出的特征语义是一致的——两个世界共享同一套\u0026quot;语言\u0026quot;。\n3.5 验证结果 在真实感 3DGS 基准评估中：\nRAD-2 取得最低碰撞率（0.250） 最高 Safety@1（0.723）和 Safety@2（0.644） 超越 Senna、Senna-2 和 RAD 等强劲竞争对手 结论：在 BEV 特征空间里训练出来的策略，没有因为换成真实感图像输入而\u0026quot;失灵\u0026quot;，证明泛化能力是真实的。\n四、安全奖励的计算：依赖标注数据吗？ 4.1 TTC 计算公式 $$T_t = \\inf{k \\in [0, T_{\\max}] \\mid \\mathcal{B}{\\text{ego}}(k;t) \\cap \\mathcal{V}{\\text{env}}(t+k) \\neq \\emptyset}$$\n其中 $\\mathcal{V}_{\\text{env}}(t+k)$ 是环境在绝对时间 $t+k$ 的所有障碍物的真实占用集合（ground-truth occupancy）。\n4.2 直接答案：是的，依赖预先标注好的真实障碍物位置 $\\mathcal{V}_{\\text{env}}$ 这个符号里藏着答案——ground-truth occupancy，即真实标注的占用区域。这不是模型感知出来的结果，而是从真实驾驶日志的标注数据里直接读取的。\n数据包含：\n每个时刻所有动态障碍物的 3D 边界框（位置、朝向、尺寸） 静态障碍物和道路边界的几何信息 这在自动驾驶数据集（nuScenes、Waymo）里是标准配置。\n4.3 TTC 计算流程 第一步：预测自车未来占用区域\n根据选定轨迹推算自车在未来 k 时刻的位置和朝向 结合车辆几何尺寸得到矩形占用框 第二步：查询环境真实占用\n直接从日志标注里读取 k 时刻所有障碍物的真实边界框 第三步：逐帧检测相交\n从 k=0 开始往后扫，找到第一个重叠时刻 4.4 这个设计的重要推论 BEV 特征图里动态障碍物的位置有误差 → 不影响奖励计算\n因为奖励用的是标注真值，不是从变形后的 BEV 特征里解码出来的感知结果。这是一个非常聪明的解耦——用近似的特征图喂给规划器看，但用精确的标注真值来评判决策好不好。\n4.5 潜在局限性 训练数据必须有高质量的 3D 标注，而标注本身是昂贵的。论文用了 5 万条片段做 RL 训练，这背后是规模庞大的标注工程。\n五、绕开人工标注的替代方案 对于资源有限的公司，有几条可行的替代路径：\n5.1 用传感器原始数据直接计算占用 激光雷达点云：\n点云本身就是障碍物存在的直接证据 体素化成占用栅格（Occupancy Grid） TTC 计算从\u0026quot;自车预测框 vs 标注边界框\u0026quot;变成\u0026quot;自车预测框 vs 占用栅格\u0026quot; 零标注成本 纯视觉的深度估计：\n用自监督深度估计模型（MonoDepth2、DepthAnything）从图像恢复深度 投影成伪点云，构建占用栅格 模型可以用无监督方式训练（利用多帧视差一致性） 5.2 用端到端模型的中间特征 BEV 占用预测头：\n很多端到端模型都有占用预测分支 可以用自监督或弱监督方式训练： 利用激光雷达点云作为伪标签 利用相邻帧光流一致性 5.3 从真实事故/险情数据中学习奖励 利用车辆自身的传感器信号：\n急刹车事件（加速度计突然大负值） AEB（自动紧急制动）触发记录 碰撞传感器触发 驾驶员接管事件 这些事件不需要任何人工标注，车辆 ECU 会自动记录。\n对比学习构建奖励模型：\n收集\u0026quot;好的驾驶\u0026quot;和\u0026quot;坏的驾驶\u0026quot;片段 用对比学习训练奖励模型 类似 RLHF 思路，把\u0026quot;人类偏好\u0026quot;换成\u0026quot;传感器采集的客观安全信号\u0026quot; 5.4 用世界模型替代标注环境 训练生成式世界模型，输入当前场景和自车轨迹，预测未来画面或 BEV 特征：\n原来：查标注数据 → 获得未来障碍物真实位置 替换：世界模型预测 → 获得未来场景的估计状态 世界模型本身可以用纯视频数据自监督训练（预测下一帧），不需要任何标注。\n代价：世界模型在长时序下会累积误差（时序漂移），导致奖励信号失真。\n5.5 用规则定义基于原始信号的奖励 不计算 TTC，直接用原始信号：\n纵向加速度 $|a_x| \u0026gt; \\text{阈值}$ → 惩罚 横向加速度 $|a_y| \u0026gt; \\text{阈值}$ → 惩罚 方向盘转角变化率过大 → 惩罚 自车速度与参考速度偏差 → 效率奖励 这些信号全部来自车辆 IMU 和 CAN 总线，零标注、零感知依赖。\n缺点：这类奖励是滞后的——只能在危险已发生后才给惩罚，而 TTC 是前瞻性的预测。\n六、实验结果 6.1 数据集规模 生成器预训练：约 5万小时真实世界驾驶数据 RL 闭环训练（BEV-Warp）：从真实驾驶日志收集 5万条片段，筛选后安全类和效率类场景各 1万条训练，各 512条评估 3DGS 环境：来自 Senna-2 基准，1044条训练，256条评估 6.2 闭环 BEV-Warp 评估 与 ResAD 基线对比：\n碰撞率（CR）从 0.533 降至 0.234（降低 56%） 自身责任碰撞率从 0.264 降至 0.092 Safety@1/2 从 0.418/0.281 提升至 0.730/0.596 EP@1.0 从 0.516 提升至 0.736 6.3 闭环 3DGS 评估 RAD-2 在所有方法中取得：\n最低碰撞率（0.250） 最高 Safety@1/2（0.723/0.644） 超越 Senna、Senna-2 和 RAD 6.4 开环 Senna-2 基准 总体碰撞率降至 0.142%（前最优为 Senna-2 的 0.288%） ADE/FDE 分别降至 0.208m/0.553m 6.5 消融实验摘要 消融项 结论 训练流程 单独 RL 生成器优化能提升安全性但稍降效率；单独 RL 鉴别器训练能大幅提升效率；二者联合才能取得最优 执行时域 $H_{\\text{reuse}}$ =8 最优；太小梯度不稳，太长反应迟钝 片段过滤 过滤低方差片段能显著提升效率（EP@1.0 从 0.662→0.728），使训练更稳定 鉴别器初始化 从预训练规划头初始化大幅提升安全性（Safety@1 从 0.512→0.615） TC-GRPO 组大小 组大小=4 时性能最优 熵正则化 加入熵项使碰撞率从 0.254 降至 0.234，Safety@1 从 0.697 提升至 0.730 训练场景组合 混合安全和效率场景训练效果最优；单一目标训练会导致严重性能偏差 推理时扩展 训练时 M=32，推理时增大 M 可持续提升效率（EP@1.0 从 0.667→0.814），无需重新训练 6.6 定性对比 场景一（安全性）：\n基线模型识别不到风险直接撞车 RAD-2 提前感知威胁，主动减速，等危险解除后平稳继续 场景二（效率性）：\n基线模型在另一辆车并入时过于保守，停下来等待 RAD-2 判断左侧有足够空间，果断变道超越慢车，EP=1.09（基线 EP=1.01） 七、局限性与未来展望 作者坦诚指出两大不足：\n7.1 表示方式的局限性 BEV-Warp 的高效率根植于对 BEV 特征图的操作。对于使用原始相机像素或统一潜在嵌入（没有显式空间等变栅格结构）的架构，几何变形机制需要更通用的变换模块或直接在潜在空间的世界模型才能适用。\n7.2 通向生成式世界模型 生成式世界模型提供更高灵活性和逼真感，但目前受：\n计算开销大 长时序生成中时间漂移严重 等问题制约，限制了在大规模 RL 训练中的实用性。未来将聚焦于优化基于潜在空间的世界模型的推理效率和时序一致性。\n八、总结 这篇论文解决了什么核心问题？ 解决了扩散规划器在纯模仿学习下的关键挑战——模型只知道\u0026quot;该怎么开\u0026quot;，却不知道为自己的失误付出代价。通过生成器-鉴别器框架，让 RL 只优化低维的鉴别器分数，同时渐进引导生成器向好的轨迹区域靠拢。\nRAD-2 的三件法宝 法宝 功能 扩散生成器 天马行空地生成多样化候选轨迹 RL 鉴别器 冷静地为后果负责，挑出最安全高效的轨迹 BEV-Warp 快速地练习，支撑大规模闭环训练 核心创新点 TC-GRPO：时序一致采样改善信用分配，只在决策时刻施加梯度 OGO：在策生成器优化，通过纵向反馈渐进精炼轨迹分布 自适应熵正则化：只在熵过低时介入，维持探索多样性 BEV-Warp：特征级仿真绕过昂贵渲染，高吞吐量闭环训练 相关论文 [[ResAD - 归一化残差轨迹建模]]（同团队的前作，生成器架构的基础） [[UAD - 无需3D标注的端到端自动驾驶]] [[World4Drive - 无需感知标注的端到端世界模型]] [[LAW - Latent World Model for E2E Driving]] ","date":"2026-04-24T00:00:00Z","permalink":"/post/robotics/e2e/rad-2/","title":"RAD-2: Scaling Reinforcement Learning in a Generator-Discriminator Framework"},{"content":" 一、这篇论文在讲什么？ 核心卖点：别再\u0026quot;闭着眼睛抓药\u0026quot;了！ 在物理人工智能（Physical AI）领域，特别是自动驾驶，业界一直以来的做法就是 \u0026ldquo;疯狂喂数据\u0026rdquo;。但这就带来了一个巨大的痛点：\u0026ldquo;偏科\u0026quot;与\u0026quot;效能黑盒\u0026rdquo;。\n评估一个端到端（E2E）自动驾驶规划器，我们有很多维度的\u0026quot;考试科目\u0026quot;——比如不压线、不闯红灯、不追尾等不同的合规指标。可是，当你把海量数据丢给模型时，你根本不知道 哪一条数据对哪一个特定的指标有帮助。\n现有的数据挑选策略就像是\u0026quot;闭着眼睛抓药\u0026quot;，它们无法解决不同指标间的冲突，也无法量化特定数据带来的收益。\nMOSAIC 的答案 用 \u0026ldquo;聚类分门别类\u0026rdquo; + \u0026ldquo;拟合神经缩放定律\u0026rdquo; + \u0026ldquo;迭代优化数据混合\u0026rdquo; 的组合拳，给自动驾驶的 AI 投喂过程装上一个\u0026quot;超强大脑\u0026quot;\n这篇入选 CVPR 2026 的论文，并没有在模型架构上\u0026quot;卷生卷死\u0026quot;，而是另辟蹊径地回答了一个更具商业价值和工程意义的问题：不要盲目迷信大数据，要相信\u0026quot;懂缩放规律\u0026quot;的好数据。\n二、背景与痛点 —— \u0026ldquo;驾校校长的烦恼\u0026rdquo; 为了让模型能上路，业界一直以来的做法就是堆数据。但这带来了几个致命问题：\n🛑 问题一：多指标间的\u0026quot;偏科困境\u0026quot; 自动驾驶的评估是一个多科目的\u0026quot;期末考试\u0026quot;：\n不压线（车道保持） 不闯红灯（交通规则合规） 不追尾（碰撞安全） 不逆行（方向合规） \u0026hellip; 痛点在于：当你把海量数据一股脑丢给模型时，你根本不知道哪条数据对哪个科目有帮助。可能某类数据对\u0026quot;不压线\u0026quot;有帮助，但对\u0026quot;不闯红灯\u0026quot;却毫无作用，甚至会因为数据分布偏差导致模型在某个指标上\u0026quot;偏科\u0026quot;。\n🛑 问题二：数据收益的\u0026quot;效能黑盒\u0026quot; 传统的数据挑选方法（比如基于难度、多样性、不确定性等）本质上都是启发式的。它们无法回答一个核心问题：\n\u0026ldquo;如果我再加 1000 条匹兹堡的数据，我的\u0026rsquo;不压线\u0026rsquo;指标能涨多少分？\u0026rdquo;\n没有量化的收益预测，数据挑选就成了\u0026quot;玄学\u0026quot;。\n三、破局之法 —— 揭秘 MOSAIC \u0026ldquo;数据挑选魔法阵\u0026rdquo; MOSAIC（Mixture Optimization via Scaling-Aware Iterative Collection，基于规模感知迭代收集的混合优化）框架不靠盲目堆料，而是精打细算。它的施法过程分为三大极其精细的招式：\n🗡️ 招式一：场景的\u0026quot;分门别类\u0026quot;（离散域聚类） 首先，MOSAIC 会把杂乱无章的原始数据池切分成一个个相互独立的 \u0026ldquo;特征簇\u0026rdquo;（Domains）。\n论文中举了一个非常生动的例子：\n数据簇类型 场景特征 匹兹堡郊区 蜿蜒起伏的郊区小路，弯道多，车流稀疏 拉斯维加斯城市 拥堵密集的城市车流，红绿灯多，行人密集 波士顿市区 复杂路口，道路狭窄 新加坡 高密度城市，热带气候特征 把数据按地理位置或描述文本（Caption）分类后，我们就有了清晰的 \u0026ldquo;专项训练题库\u0026rdquo;。每个题库对不同的驾驶技能指标有不同的提升潜力。\n🗡️ 招式二：摸透\u0026quot;成长规律\u0026quot;（拟合神经缩放定律） 有了题库还不够，教练得知道刷多少题能涨多少分。研究团队在每个数据簇上进行小规模的 试点训练（Pilot runs），并套用\u0026quot;神经缩放定律\u0026quot;（Neural Scaling Laws）。\n🧮 核心数学公式（论文公式 4） 作者发现，随着你在某一个领域里不断增加数据量，模型在该领域相关指标上的\u0026quot;涨分曲线\u0026quot;，完美符合一个 指数衰减的饱和模型：\n$$ \\Delta U_i(n) \\approx a_i \\times (1 - e^{-n/\\tau_i}) $$\n参数 含义 作用 $\\Delta U_i(n)$ 预期收益 从第 $i$ 个题库里加了 $n$ 条数据后，指标能涨多少 $a_i$（潜力上限） 曲线的最高天花板 把这个题库里的题全刷完，最多能涨几分 $\\tau_i$（饱和速度/衰减常数） 收益递减发生速度 AI 刷这套题有多快会\u0026quot;看腻\u0026quot; 🔬 两个参数是怎么算出来的？ 靠的就是 \u0026ldquo;两次试点运行（2 Pilot Runs）\u0026rdquo;：\n第一步：真实的两次\u0026quot;摸底测验\u0026quot;获取坐标点\n假设教练现在要评估\u0026quot;波士顿城市拥堵\u0026quot;这个数据簇的价值：\n第一次试跑：从波士顿数据池中抽出少量数据（比如 $n_1=100$ 条），让模型学一下，考个试，记录下分数涨幅 $y_1$。得到第一个坐标点 $(n_1, y_1)$。 第二次试跑：再稍微加一点数据（比如总共 $n_2=250$ 条），让模型再学一下，考个试，记录下分数涨幅 $y_2$。得到第二个坐标点 $(n_2, y_2)$。 第二步：解二元方程组（曲线拟合）\n在这个数学模型里，未知数只有两个：潜力上限 $a_i$ 和饱和速度 $\\tau_i$。\n有了两个真实的测试点，加上一个隐含的起点（0 条数据涨 0 分），系统通过最优化算法（非线性最小二乘法曲线拟合），直接把这两个点代入公式中，反向解出 $a_i$ 和 $\\tau_i$ 的具体数值。\n这套题库的\u0026quot;底裤\u0026quot;就被彻底看穿了！\n🗡️ 招式三：精打细算的\u0026quot;排兵布阵\u0026quot;（迭代优化数据混合） 这是最核心的一步。MOSAIC 不会一次性把数据选完，而是采取 迭代加码 的策略。\n在每一轮，系统会计算：\n\u0026ldquo;当前最能拉高总评分（总体效用期望）的数据源是哪个？\u0026rdquo;\n然后像一位精明的投资客一样，把\u0026quot;数据预算\u0026quot;投给收益率（ROI）最高的那一簇。\n🔄 动态挑选策略的精彩表现 论文在波士顿、新加坡、匹兹堡、拉斯维加斯四大场景对比中展示了极为精彩的现象：\n题库类型 特征 MOSAIC 的策略 波士顿/新加坡 见效快，但很快遇到瓶颈（小 $\\tau_i$） 早期疯狂抽取，预算不到 500 条时主力投入 匹兹堡郊区 慢热，但后劲十足（大 $a_i$ 和大 $\\tau_i$） 起步时不爱搭理，中期（500-3700）重心转移过来 当波士顿数据\u0026quot;饱和\u0026quot;后，系统基于公式计算出，继续拿匹兹堡的数据，边际收益反而更高。于是当预算加码到中后期，MOSAIC 的数据挑选重心会完美地自动向匹兹堡转移！\n四、Pilot Runs 的时机 —— \u0026ldquo;开局侦察技能\u0026rdquo; ⚠️ 关键澄清：只在最开始跑一次！ Pilot runs（试点运行）是在整个任务的最开始只跑一次的，而不是每一轮都跑。\n如果每一轮都去跑一遍，那计算成本就高到天上去了，完全违背了这篇论文\u0026quot;省钱省算力\u0026quot;的初衷。\n🧭 Pilot runs 的真实身份：开学前的\u0026quot;摸底测验\u0026quot; 在正式开始大规模挑选数据和训练之前，MOSAIC 框架会先进行 Pilot runs。这就好比学员刚报名驾校，教练为了摸清他的底子，从各个题库里各抽出极小的一部分，让 AI 先试着练一练。\n教练做这个\u0026quot;摸底测验\u0026quot;的目的只有一个：解方程，画曲线。\n通过这几次少量的试跑，系统会套用神经缩放定律的数学公式，计算出每个题库的潜力上限和饱和速度。\n💡 为什么后续迭代不需要再跑了？ 在\u0026quot;摸底测验\u0026quot;结束后，教练手里已经有了一套 精准的数学预测曲线。\n进入迭代挑选环节时，系统 完全是靠这套数学公式在纸上算账的：\n\u0026ldquo;如果我下一轮从拉斯维加斯的题库里再加 100 题，根据之前的曲线，它的边际收益是多少？对 EPDMS 指标的提升有多大？\u0026rdquo;\n系统会直接把预算分配给计算出来预期收益最高的数据簇。因为全靠数学公式推演，所以每一轮的挑选过程非常快，根本不需要再把模型拉出来真正跑一次训练验证。\n💰 \u0026ldquo;初始开销\u0026rdquo; vs \u0026ldquo;最终收益\u0026rdquo; 论文作者特意强调：\nPilot runs 确实会带来一次性的 \u0026ldquo;初始计算开销\u0026rdquo; 但只要用非常小的数据子集来做 Pilot runs，就能拟合出足够准确的曲线 这笔\u0026quot;报名费\u0026quot;花得极其划算——因为有了准确的曲线指导，在后续正式训练中可以省下高达 80% 的冗余数据 最终算总账时，MOSAIC 消耗的总算力远低于那些蒙着头瞎选数据的传统方法。\n五、终极路考 —— EPDMS 标准下的试炼 训练出来的\u0026quot;老司机\u0026quot;到底行不行，得看考卷。\n📝 考卷名称：EPDMS EPDMS（Extended Predictive Driver Model Score，扩展预测驾驶员模型评分） 是一个非常严苛的综合性指标，专门用来评估规划器在 驾驶规则合规性 上的表现（即：它到底有多守规矩）。\n🏫 考试场地 研究团队在两个知名基准测试平台上进行了详尽的验证跑分：\nOpenScene 数据集 Navtrain 数据集 六、令人惊艳的战果 —— \u0026ldquo;四两拨千斤\u0026quot;的数据奇迹 到了出成绩的环节，MOSAIC 框架交出了一份堪称降维打击的答卷：\n🏆 战绩一：降维碾压各类基线 在 OpenScene 和 Navtrain 这两个\u0026quot;考场\u0026quot;上，无论给定的数据预算是多少，MOSAIC 在 EPDMS 总分上始终稳压所有其他主流的数据挑选方法。\n🏆 战绩二：极致的\u0026quot;抠门\u0026quot;艺术 这是全篇最震撼的细节——\n成果 数据节省 达到相同合规性能指标 节省高达 80% 的数据量 部分具体测试 减少 42% 的样本需求，仍维持高水平表现 这意味着自动驾驶公司不再需要花天价去标注和训练那些毫无意义的\u0026quot;冗余垃圾数据\u0026rdquo;，把钱全花在了刀刃上。\n七、总结：用\u0026quot;懂缩放规律\u0026quot;的好数据替代大数据迷信 这篇入选顶会的论文，给自动驾驶领域带来了一个极具商业价值和工程意义的启示：\n✅ MOSAIC 的三大贡献 数据挑选不再是玄学：通过神经缩放定律，数据收益被完全量化 多指标冲突被优雅解决：迭代优化策略自动平衡不同指标的收益 成本效益最大化：节省高达 80% 的数据量，算力开销远低于传统方法 📚 核心方法论总结 聚类分门别类 → Pilot runs 摸底测验 → 拟合缩放定律 → 迭代优化数据混合 ↓ ↓ ↓ ↓ 建立题库 画曲线参数 预测收益 ROI 投资策略 🎯 一句话总结 不要盲目迷信大数据，要相信\u0026quot;懂缩放规律\u0026quot;的好数据。通过 MOSAIC 这套聚类、拟合、迭代挑选的组合拳，作者成功给自动驾驶的 AI 投喂过程装上了一个\u0026quot;超强大脑\u0026quot;，实现了\u0026quot;四两拨千斤\u0026quot;的数据精细化提效。\n","date":"2026-04-21T00:00:00Z","permalink":"/post/robotics/e2e/mosaic/","title":"MOSAIC: 基于规模感知的自动驾驶数据挑选魔法阵"},{"content":" 一、这篇论文在讲什么？ 传统系统的痛点：刻板的流水线工厂 想象一下，传统的自动驾驶系统就像一个刻板的流水线工厂：\n感知部门（看路）把报告交给预测部门（猜别人怎么走） 预测部门再交给规划部门（决定自己怎么开） 一旦感知部门看走了眼，整个工厂就会**\u0026ldquo;一步错，步步错\u0026rdquo;**（误差累积） 部门之间缺乏沟通，效率极低 他们还非要扛着一张无比沉重的\u0026quot;全局鸟瞰图\u0026quot;（Dense BEV）来工作，把电脑累得够呛 三大痛点剖析 作者在引言部分一针见血地指出了目前端到端自动驾驶（E2E-AD）的三大痛点：\n痛点 传统方案的问题 影响 顺序执行的魔咒 UniAD 等明星模型依然玩\u0026quot;感知-预测-规划\u0026quot;的串行游戏 误差累积 + 训练不稳定 缺乏协同效应 强行排了先后顺序 无法学会\u0026quot;为规划而感知\u0026quot;或\u0026quot;博弈论式互动\u0026quot; 沉重的 BEV 包袱 使用密集的鸟瞰图（Dense BEV）特征 想看远、想记历史 → 计算量爆炸 DriveTransformer 的破局宣言 彻底砸碎流水线，把所有部门拉到同一张圆桌上开会！\nDriveTransformer 抛弃了沉重的 BEV 特征，完全用纯 Transformer 架构搭建了一个 \u0026ldquo;并行、稀疏、流式\u0026rdquo; 的新世界：\n大大降低系统复杂度 模型极其容易扩大规模（Scale up） 用最优雅的 Transformer 语言统一检测、地图构建、运动预测和路径规划 二、核心方法：三大神功 + 三套连招 这是全篇最硬核、最精彩的部分！DriveTransformer 的成功秘诀在于它的架构设计。\n🥇 神功一：任务并行 (Task Parallelism) —— 圆桌会议 再也没有上下级之分！ 系统里有三种 Token 探针（Query），它们在每一个 Transformer 模块里直接互相交流，信息瞬间互通有无：\n探针类型 数量 负责任务 Agent Queries（智能体探针） 900个 盯住动态的车辆、行人 Map Queries（地图探针） 100个 识别静态的车道线、红绿灯等 Planning Queries（规划探针） 专属 决定自己的车怎么开 这彻底消除了层级带来的卡顿和误差传递！\n🥈 神功二：稀疏表示 (Sparse Representation) —— 直饮源头水 丢掉庞大笨重的全局 BEV 鸟瞰图！\n这些轻量级的探针直接一头扎进**原始的多视角传感器画面（Raw Sensor Features）**里去提取它们关心的信息。\n生动比喻：这就像你用谷歌搜索精准直达答案，而不是把整本大英百科全书背下来。\n🥉 神功三：流式处理 (Streaming Processing) —— 时光记忆背包 怎么记住过去的事情？传统方法是保存过去好几帧的沉重 BEV 图像。\nDriveTransformer 借用了先进的 FIFO（先进先出）队列：\n只把上一帧最有价值的 Top-K 个探针（K=50） 塞进队列里 当作历史先验信息传递给下一帧 既省内存，又保留了高级的语义信息 🔄 三套连招：统一注意力机制 在每一次\u0026quot;施法\u0026quot;（每个 Transformer Layer）时，所有任务只用这三种统一的操作：\n连招 操作名称 作用 生动比喻 第一招 Task Self-Attention（任务自注意力） 探针与探针之间的情报共享 探针们的\u0026quot;群聊\u0026quot; 第二招 Sensor Cross-Attention（传感器交叉注意力） 探针向原始传感器画面索取视觉线索 去\u0026quot;对图\u0026quot; 第三招 Temporal Cross-Attention（时序交叉注意力） 探针向历史探针取经，融合时间信息 \u0026ldquo;翻旧账\u0026rdquo; 细节彩蛋：因为探针们可以直接从传感器和历史中获取信息，DriveTransformer 根本不需要像老模型那样做分阶段的预训练，从头到尾一体化训练，一气呵成！\n三、时序魔法详解：运动补偿的精妙设计 核心问题：时间穿越时的\u0026quot;刻舟求剑\u0026quot; 你开车时不仅要记住 1 秒前有一辆车在你左边（历史记忆），还得猜到这 1 秒内它往前开了一段距离。传统模型经常犯\u0026quot;刻舟求剑\u0026quot;的错误。\nDriveTransformer 通过两步走策略，给历史探针打上了完美的\u0026quot;记忆补丁\u0026quot;：\n第一步：自车坐标系对齐 (Ego Transform) 解决\u0026quot;我\u0026quot;动了的问题\n在把过去的 Top-K 探针拿来用之前，首先得意识到：我们自己的车（Ego Vehicle）在这段时间里也往前开了！\n系统会根据自车在这段时间内的位移和旋转（通过里程计或定位信息得到），把历史探针携带的位置信息（PE），统一转换到当前时刻的自车坐标系下。\n这步搞定后，像红绿灯、车道线这种静态的地图探针就完全对齐了。\n第二步：智能体运动补偿 (Agent Motion Compensation) 解决\u0026quot;他\u0026quot;动了的问题\n对于动态的智能体探针，光做第一步是不够的——他们在动！\nDriveTransformer 施展了一个数学法术：\n算出\u0026quot;他跑了多远\u0026quot;\n历史帧里的智能体探针预测了物体的速度 $v^t_{agent}$ $速度 \\times 时间 = 位移$ 计算时间差 $\\Delta t = (t - t_0)$ 炼制\u0026quot;记忆补丁\u0026quot;\n把位移向量喂给 MLP 当\u0026quot;翻译官\u0026quot; MLP 输出两个参数：缩放因子 $\\gamma$ 和 偏移因子 $\\beta$ 施放\u0026quot;自适应层归一化\u0026quot;法术 $$ \\hat{PE}^t_{agent} = \\text{LayerNorm}(\\text{原始历史 } \\hat{PE}^t_{agent}, ; \\text{受 } [\\gamma, \\beta] \\text{ 控制}) $$\n这样做的绝妙之处 给模型戴上了一副 \u0026ldquo;基于物理运动规律的预判眼镜\u0026rdquo;\n模型在对历史探针说：\u0026ldquo;嘿哥们，我知道你 1 秒前在那里，但根据你当时的速度，你现在应该跑到这儿了。我就在这个新位置去匹配当前摄像头的画面！\u0026rdquo;\n优势对比：\n方案 传统 Dense BEV DriveTransformer 做法方法 把沉重 BEV 像素用空间变换网络扭曲平移 只对几十个探针的 PE 做乘法加法 计算开销 巨大 丝滑无比，瞬间完成 跟踪匹配 需要复杂的追踪算法 免去 Tracking-free，自然学习就能\u0026quot;相认\u0026quot; 四、探针设计的核心秘密：\u0026ldquo;干湿分离\u0026rdquo; 一个非常专业的问题 如果位置编码（PE）已经通过\u0026quot;加法\u0026quot;融化在特征（Feature）里了，那后来怎么可能再把它单独捞出来做坐标系转换呢？\nDriveTransformer（以及它借鉴的 StreamPETR 等现代架构）采用的是 \u0026ldquo;干湿分离\u0026quot;设计！\n探针的\u0026quot;双轨制\u0026rdquo;：公文包设计 在历史队列（FIFO Queue）里保存上一帧的探针时，存下来的不是一个融合向量，而是一个 \u0026ldquo;公文包\u0026rdquo;：\n文件 内容 说明 文件 A：语义特征 高维向量（如 256维） 记录\u0026quot;这是什么\u0026quot;（如：红色轿车，车灯亮着）—— 不需要坐标转换 文件 B：显式 3D 锚点 纯物理坐标 $(x, y, z)$ 记录\u0026quot;它在哪\u0026quot;—— 轻量、可直接做几何运算 运动补偿的实际流程 直接对\u0026quot;物理坐标\u0026quot;动手脚\n因为位置信息是作为显式坐标单独存放的 直接拿出 $(x, y, z)$，用位移和旋转矩阵做几何运算 嗖的一下！坐标就完美对齐到当前坐标系 位置编码的\u0026quot;现做现卖\u0026quot;\n有一个专门的\u0026quot;编码加工厂\u0026quot;（MLP 网络） 接收新坐标 $(x\u0026rsquo;, y\u0026rsquo;, z\u0026rsquo;)$，动态生成当前时刻的高维 PE 注入\u0026quot;他动\u0026quot;魔法\n在 PE 准备和语义特征相加之前，运动补偿闪亮登场 用 MLP 算出 $\\gamma$ 和 $\\beta$ 通过 Adaptive LayerNorm 作用在新鲜生成的 PE 上 终极合体\n所有空间转换和运动补偿都做完了 \u0026ldquo;终极进化版 PE\u0026quot;才与\u0026quot;语义特征\u0026quot;相加，投入注意力机制计算 核心设计哲学 我不提前泼水！\n通过将语义特征与显式 3D 坐标拆分保存：\n只对纯物理坐标做刚体变换 然后动态生成 PE 并进行运动补偿调制 最后才与特征合并 保证特征本身的纯洁性 五、实例级表达：DETR 范式的灵魂 核心问题 历史信息中为什么会有实例级别的速度信息？网络对周围障碍物和地图做了实例化输出吗？\n是的！网络不仅做了彻底的实例化输出，历史信息里的速度是模型在上一帧\u0026quot;自己亲口预测出来\u0026quot;的！\n抛弃\u0026quot;像素填色游戏\u0026rdquo;，拥抱\u0026quot;实例锁定\u0026quot; 以前基于 Dense BEV 的模型像是在玩\u0026quot;填色游戏\u0026quot;：把周围环境划分成无数小格子，然后告诉系统\u0026quot;这个格子里有车\u0026quot;。如果要提取\u0026quot;一辆车\u0026quot;，还需要额外的后处理（NMS、聚类）。\nDriveTransformer 掀翻了这个桌子！ 使用 DETR 的理念：\n探针 比喻 作用 Agent Queries（900个） 900枚\u0026quot;自导鱼雷\u0026quot; 每一枚死死咬住马路上的一个具体实例 Map Queries（100个） 100个测绘员 每个去认领一条具体的车道线、斑马线或红绿灯 模型不需要繁琐的后处理，输出天生就是高度结构化的\u0026quot;实例列表\u0026quot;！\n速度信息从哪来？探针的\u0026quot;述职报告\u0026quot; 每个智能体探针锁定一个物体后，在每一帧 Transformer 处理完毕后，都要提交一份 \u0026ldquo;述职报告\u0026rdquo;：\n物体的 3D 边界框（$x, y, z$, 长, 宽, 高, 旋转角） 物体的类别（车、人、自行车） 当前绝对速度 $(v_x, v_y)$ 和未来运动轨迹！ 时序闭环：\u0026ldquo;昨天的预测\u0026quot;变成\u0026quot;今天的先验\u0026rdquo; 最绝妙的逻辑闭环来了！历史信息里的速度是模型自己前一秒算出来的。\n完整的时间流：\n时间 动作 T-1 时刻 1号智能体探针锁定右前方车辆，输出报告：\u0026ldquo;卡车，位置 $(x, y, z)$，速度 10 m/s\u0026rdquo; 存入背包 系统把探针打包存入历史队列（语义特征 + 位置 + 预测的速度） T 时刻 系统从背包掏出探针：\u0026ldquo;你上一帧说速度 10 m/s，过去 0.1s，应该往前开了 1m\u0026rdquo; 运动补偿 用速度计算位移，通过 $\\gamma$ 和 $\\beta$ 魔法，把历史探针平移到预期位置 锁定当前帧 在交叉注意力中，直接去\u0026quot;新位置\u0026quot;附近寻找，降低搜索难度 完美流畅的端到端流式预测！\n六、稀疏 vs 稠密：为什么\u0026quot;丢掉背景\u0026quot;反而更强？ 一个\u0026quot;顶会审稿人级别\u0026quot;的质疑 前后帧的信息传递中损失了很多信息，有点像 1.5 段式端到端，相比 1 段式存在性能瓶颈\n这个担忧是学术界争论最激烈的话题之一。但为什么 DriveTransformer 依然能打破\u0026quot;瓶颈\u0026quot;？\n担忧一：是不是\u0026quot;1.5段式\u0026quot;？ 如果模型只把位置和速度传递给下一帧，确实退化成 1.5 段式。\n但别忘了\u0026quot;公文包\u0026quot;！除了显式的物理坐标，探针里还带着一个高维度的语义特征。\n这个高维特征是不被人类定义的黑盒，可能包含：\n\u0026ldquo;这辆车有打转向灯的意图\u0026rdquo; \u0026ldquo;那个行人看起来喝醉了\u0026rdquo; 无法用简单位置和速度描述的深层隐含信息 表面上戴着 1.5 段式（实例结构化）的面具，背地里依然在用纯正 1 段式的高维张量进行端到端的暗流涌动！\n担忧二：丢掉背景信息造成损失？ 如果上一帧的 Top-K 探针没有捕捉到路边草丛里准备窜出来的小狗，下一帧是不是就完全不知道了？\n关键认知误区！\n虽然模型在传递历史时是稀疏的（只传 Top-K），但在看当前世界时，它是绝对稠密且完整的！\n每一帧的 Sensor Cross-Attention 中，所有探针都会一头扎进当前帧完整的 2D 多视角摄像头图像中疯狂提取信息：\n如果历史探针漏掉了草丛里的小狗 → 没关系！ 当前帧新初始化的 900 个探针会在扫描高清画面时，直接把它\u0026quot;揪\u0026quot;出来 历史记忆是稀疏的（只记重点），但当下感知是稠密的（不漏死角）\n终极反转：为什么丢掉背景反而是破局关键？ 在真实的 AI 工业界，计算力和显存才是真正的瓶颈！\n传统 Dense 方案（把历史好几帧的巨大 BEV 堆叠）：\n问题 影响 算力爆炸 历史 10 帧 BEV 特征加在一起，显存直接撑爆 Scaling 灾难 BEV 占用太大，只能配很小的\u0026quot;眼睛\u0026quot;（如 ResNet50），看低分辨率照片 DriveTransformer 通过果断丢掉历史中无意义的背景（如\u0026quot;10 秒钟以前马路牙子上的一块砖\u0026quot;），把系统精简到极致。\n省下来的海量算力和显存，拿去干嘛了？\n换上了一双 \u0026ldquo;神之眼\u0026rdquo;！主干网络扩展到参数量极为庞大的视觉大模型 EVA02-CLIP-L，输入图像分辨率也拉到了超高清。\n实验证明：用一个超大模型去处理极其精准的稀疏探针，其性能（Scaling Law）远远超过用一个小模型去死磕庞大的稠密特征！\n潜在风险：OOD / Long Tail 场景 在一种极端情况下，你的顾虑依然是致命的：开放世界中的异形障碍物。\n如果路上出现了一个既不像车、也不像人的东西（如侧翻半挂车掉下来的巨大钢卷），而 Agent Queries 一直被训练去寻找\u0026quot;标准的车辆和行人\u0026quot;，稀疏的实例表达可能因\u0026quot;不认识\u0026quot;而忽略它。\n前沿研究方向：引入 Occupancy Network（占用网络） 作为稀疏探针的兜底方案 —— 既有稀疏探针去抓重点，又有粗糙但全覆盖的稠密网格去防止\u0026quot;漏网之鱼\u0026quot;。\n七、任务头输出：DETR 风格的终极归宿 核心机制 在经过了充分交互后，探针通过 DETR 风格的任务头直接输出自动驾驶指令。\n最大魅力：没有中间商赚差价！没有繁琐的锚框、没有让人头疼的 NMS 后处理，网络直接吐出结构化的实例结果。\n🚗 智能体头 (Agent Head) 处理那 900 个 Agent Queries，同时输出两份作业：\n作业 输出内容 检测作业 3D 边界框（$x, y, z$, 长, 宽, 高, 旋转角）+ 当前速度 $(v_x, v_y)$ + 语义类别 预言作业 未来几秒钟的运动轨迹 隐藏细节：预测其他车辆轨迹时，**以这辆车自己当前位置为原点（局部坐标系）**来预测未来位移。巧妙地把\u0026quot;检测\u0026quot;和\u0026quot;预测\u0026quot;解耦，大大降低学习难度。\n🗺️ 地图头 (Map Head) 处理那 100 个 Map Queries，在线构建局部矢量地图：\n输出不是像素，而是折线（Polylines） 一系列三维坐标点，连起来就是车道线、路面边界或斑马线 隐藏细节：同一条折线上的不同点，在 Sensor Cross Attention 时会带有不同的位置编码（PE）。即使是同一条车道线，它的头和尾也能分别精准去画面里寻找属于自己的视觉线索！\n🏎️ 规划头 (Planning Head) 最特殊，只处理专属的规划探针：\n输出极其干净利落：自车在未来 $T$ 秒内应该行驶的未来路点轨迹 底层控制器（PID）可以直接拿去踩油门、打方向盘 🌟 极大招：动态 PE 更新与深度监督 如果你以为这些头只是挂在网络最后面，那就大错特错了！\nDriveTransformer 内部有好几个串联的 Transformer Block（如 6 层），每一个 Block 后面，都挂着这三个任务头！\n深层监督 在训练时，每一层的任务头都要输出结果，并与真实标签（GT）做 DETR 招牌式的 \u0026ldquo;二分匹配\u0026rdquo; 来计算误差。这逼迫网络从第一层开始就得努力干活。\n探针的自我进化 在第 $N$ 层，网络预测出物体的位置、类别或自车意图。在进入第 $N+1$ 层之前，模型会把预测结果用 MLP 重新编码成新的位置编码！\nAgent 和 Map 的新 PE：融合了刚刚猜出的空间位置和语义类别 规划的 Ego 新 PE：融合了刚刚生成的自车未来意图 最美丽的闭环 探针进去 → 看图交流 → 用任务头输出预测 → 把预测结果变成新的起点(PE) → 进入下一层继续看图交流... 每一层都在修正上一层的误差，探针找得越来越准，预测得越来越远。当它走到最后一层输出最终结果时，整个场景已经在探针的\u0026quot;群聊\u0026quot;中被解构得明明白白了！\n八、Loss 设计：多任务协同的\u0026quot;评分标准\u0026quot; DriveTransformer 采用多任务联合训练的策略，所有任务共享同一个损失函数，通过权重平衡各任务的贡献。这种设计让模型能够同时学习感知、预测和规划，真正实现端到端的协同优化。\n总体损失函数架构 $$\\mathcal{L}{\\text{total}} = \\mathcal{L}{\\text{agent}} + \\lambda_{\\text{map}} \\mathcal{L}{\\text{map}} + \\lambda{\\text{motion}} \\mathcal{L}{\\text{motion}} + \\lambda{\\text{plan}} \\mathcal{L}_{\\text{plan}}$$\n其中 $\\lambda$ 权重系数用于平衡各任务的贡献。\n🚗 Agent Detection Loss（智能体检测损失） Agent Head 需要完成三个子任务：分类（识别物体类别）、检测（输出 3D 边界框）、速度预测。\n$$\\mathcal{L}{\\text{agent}} = \\lambda{\\text{cls}} \\mathcal{L}{\\text{cls}} + \\lambda{\\text{box}} \\mathcal{L}{\\text{box}} + \\lambda{\\text{vel}} \\mathcal{L}_{\\text{vel}}$$\n分类损失：Focal Loss $$\\mathcal{L}{\\text{cls}} = -\\sum{i} \\alpha_{c} (1 - p_i)^{\\gamma} \\log(p_i)$$\n$\\alpha_c$：类别平衡权重，缓解类别不平衡问题 $\\gamma$：聚焦参数（通常取 2），降低易分类样本的权重 $p_i$：预测为正确类别的概率 边界框回归：L1 Loss $$\\mathcal{L}{\\text{box}} = \\sum{i \\in \\mathcal{M}} \\left( | \\hat{b}_i - b_i |_1 \\right)$$\n其中 $\\hat{b}_i$ 是预测的 3D 边界框参数 $(x, y, z, l, w, h, \\theta)$，$b_i$ 是真实值，$\\mathcal{M}$ 是匹配成功的探针集合。\n速度回归：L1 Loss $$\\mathcal{L}{\\text{vel}} = \\sum{i \\in \\mathcal{M}} \\left( | \\hat{v}_i - v_i |_1 \\right)$$\n其中 $\\hat{v}_i = (\\hat{v}_x, \\hat{v}_y)$ 是预测速度，$v_i$ 是真实速度。\n关键细节：速度损失非常关键！因为预测出的速度会被存入 FIFO 队列，作为下一帧运动补偿的依据。如果速度预测不准，整个时序推理链条都会受影响。\n🗺️ Map Detection Loss（地图构建损失） Map Head 负责在线构建矢量地图，输出折线形式的地图元素。\n$$\\mathcal{L}{\\text{map}} = \\lambda{\\text{cls}} \\mathcal{L}{\\text{cls}} + \\lambda{\\text{pline}} \\mathcal{L}_{\\text{pline}}$$\n分类损失：Focal Loss $$\\mathcal{L}{\\text{cls}} = -\\sum{i} \\alpha_{c} (1 - p_i)^{\\gamma} \\log(p_i)$$\n区分车道线、路面边界、斑马线、红绿灯等地图元素类型。\n折线回归损失 $$\\mathcal{L}{\\text{pline}} = \\sum{i \\in \\mathcal{M}} \\sum_{j=1}^{N_{pts}} | \\hat{p}{i,j} - p{i,j} |_1$$\n其中：\n$N_{pts}$：每条折线的点数 $\\hat{p}_{i,j} = (x, y, z)$：预测的第 $i$ 条折线上第 $j$ 个点的坐标 $p_{i,j}$：真实的坐标点 设计要点：折线回归采用的是点级别的 L1 损失，每个折线点都需要精准定位。这保证了地图元素的几何精度，对后续规划至关重要。同一条折线上的不同点在训练时独立计算损失，实现了细粒度的监督。\n🔮 Motion Prediction Loss（运动预测损失） Agent Head 还需要预测周围物体的未来运动轨迹。\n$$\\mathcal{L}{\\text{motion}} = \\sum{i \\in \\mathcal{M}} \\sum_{t=1}^{T} | \\hat{traj}{i,t} - traj{i,t} |_1$$\n其中：\n$T$：预测的未来时间步数（如 12 步，对应 3 秒 @ 4Hz） $\\hat{traj}_{i,t} = (\\Delta x_t, \\Delta y_t)$：第 $i$ 个物体在第 $t$ 步的相对位移（局部坐标系） $traj_{i,t}$：真实的相对位移 隐藏细节：轨迹预测采用局部坐标系——以被预测物体当前位置为原点。这样做的好处是：\n把\u0026quot;检测\u0026quot;（物体在哪）和\u0026quot;预测\u0026quot;（物体往哪走）解耦 降低学习难度，提高训练稳定性 物体的绝对位置变化不会影响轨迹预测的学习 🏎️ Planning Loss（规划损失） Planning Head 输出自车未来应该行驶的轨迹路点。\n$$\\mathcal{L}{\\text{plan}} = \\sum{t=1}^{T} | \\hat{wp}_t - wp_t |_1$$\n其中：\n$T$：规划的未来时间步数 $\\hat{wp}_t = (x_t, y_t)$：第 $t$ 步的预测路点（自车坐标系） $wp_t$：真实的路点位置 设计哲学：规划损失直接监督最终输出，让整个网络为\u0026quot;开得好\u0026quot;而学习。所有感知和预测任务都通过梯度反向传播，间接服务于规划目标——这就是端到端的核心魅力！\n坐标系选择：规划路点通常在自车坐标系下定义，以当前自车位置为原点。这保证了规划的连续性——无论自车在世界坐标系中走到哪里，规划输出始终是\u0026quot;前方 X 米、左/右 Y 米\u0026quot;的相对描述。\n🔄 Hungarian Matching（匈牙利匹配） 在 DETR 范式中，每个探针（Query）需要和一个真实的 Ground Truth 物体进行匹配。DriveTransformer 使用 匈牙利算法（Hungarian Matching） 完成这一任务。\n匹配代价函数 $$\\mathcal{C}{\\text{match}}(\\hat{y}, y) = \\lambda{\\text{cls}} \\mathcal{C}{\\text{cls}} + \\lambda{\\text{box}} \\mathcal{C}{\\text{box}} + \\lambda{\\text{vel}} \\mathcal{C}{\\text{vel}} + \\lambda{\\text{motion}} \\mathcal{C}_{\\text{motion}}$$\n其中各代价项定义：\n代价项 公式 说明 $\\mathcal{C}_{\\text{cls}}$ $-\\log(p_{\\text{gt_class}})$ 分类代价，预测为 GT 类别的概率越高，代价越低 $\\mathcal{C}_{\\text{box}}$ $|\\hat{b} - b|_1$ 边界框代价，预测框与 GT 框的距离 $\\mathcal{C}_{\\text{vel}}$ $|\\hat{v} - v|_1$ 速度代价，预测速度与真实速度的差异 $\\mathcal{C}_{\\text{motion}}$ $|\\hat{traj} - traj|_1$ 轨迹代价，预测轨迹与真实轨迹的差异 最优匹配求解 $$\\hat{\\sigma} = \\arg\\min_{\\sigma \\in \\mathfrak{S}N} \\sum{i=1}^{N} \\mathcal{C}_{\\text{match}}(\\hat{y}i, y{\\sigma(i)})$$\n其中：\n$\\mathfrak{S}_N$ 是所有可能匹配方案的集合 $\\sigma$ 是匹配函数，将第 $i$ 个探针映射到第 $\\sigma(i)$ 个 GT 物体 $\\hat{y}_i$ 是第 $i$ 个探针的预测输出 $y_{\\sigma(i)}$ 是匹配到的 GT 物体 生动比喻：匈牙利匹配就像是\u0026quot;相亲大会\u0026quot;，每个探针（单身男士）都要找到一个最合适的 GT 物体（单身女士）。匹配代价就是\u0026quot;相处成本\u0026quot;，算法的目标是找到让总成本最低的配对方案——这就是经典的\u0026quot;二分图最优匹配\u0026quot;问题。\n📊 Deep Supervision（深层监督） DriveTransformer 在每一层 Transformer Block 后都挂了任务头，实现了 深层监督：\n每一层都输出预测：第 1 层、第 2 层\u0026hellip;第 6 层都要输出检测结果 每一层都计算损失：所有层的预测都与 GT 匹配并计算损失 总损失汇总： $$\\mathcal{L}{\\text{total}} = \\sum{l=1}^{L} \\mathcal{L}^{(l)}$$\n优势：深层监督逼迫网络从第一层就开始\u0026quot;干活\u0026quot;，而不是等到最后一层才输出有意义的结果。这大大加速了训练收敛，提高了模型的优化效率。\n⚖️ Loss 权重配置 论文中各损失的权重配置（参考 DETR 和 UniAD 的惯例）：\n损失类型 权重符号 典型数值 作用 Agent Classification $\\lambda_{\\text{cls}}$ 2.0 分类是基础任务 Agent Box $\\lambda_{\\text{box}}$ 5.0 / 0.1 边界框定位精度要求高 Agent Velocity $\\lambda_{\\text{vel}}$ 1.0 速度影响时序推理 Map Classification $\\lambda_{\\text{map_cls}}$ 2.0 地图元素分类 Map Polyline $\\lambda_{\\text{pline}}$ 1.0 折线点位精度 Motion Trajectory $\\lambda_{\\text{motion}}$ 1.0 预测轨迹精度 Planning $\\lambda_{\\text{plan}}$ 1.0 最终驾驶目标 权重设计原则：\n分类权重较高（Focal Loss 已内置类别平衡） 边界框权重分层配置（位置 5.0，尺寸/角度 0.1） 规划权重适中，避免过度约束感知任务 训练策略：所有任务从头开始联合训练，不需要分阶段预训练感知模块。这证明了 DriveTransformer 架构设计的优雅性——任务并行、统一优化。\n九、完整流程的 Python 伪代码 class DriveTransformer: def __init__(self, num_layers=6, history_k=50): self.backbone = VisionBackbone(\u0026#34;EVA02-CLIP-L\u0026#34;) # 强大的眼睛 self.layers = ModuleList([TransformerLayer() for _ in range(num_layers)]) self.fifo_queue = FIFOQueue(max_size=history_k) # 历史记忆背包 # 定义三种初始探针（Queries） self.agent_queries = Embedding(900, 256) # 智能体：看别人 self.map_queries = Embedding(100, 256) # 地图：看路 self.ego_query = Embedding(1, 256) # 规划：看自己 def forward(self, multi_view_images, ego_motion): \u0026#34;\u0026#34;\u0026#34; multi_view_images: 当前时刻 6 路摄像头画面 ego_motion: 自车从上一帧到这一帧的位移和旋转矩阵 \u0026#34;\u0026#34;\u0026#34; # 1. 提取视觉特征（Raw Sensor Features） image_features = self.backbone(multi_view_images) # 2. 时序魔法：从 FIFO 队列提取并补偿历史探针 history_queries, history_pe, history_vel = self.fifo_queue.get() # A. 自车运动补偿 (Ego Transform) aligned_history_pe = apply_ego_transform(history_pe, ego_motion) # B. 智能体运动补偿 (Agent Motion Compensation) dt = 1/10.0 # 假设 10Hz displacement = history_vel * dt compensated_history_pe = self.motion_mlp(aligned_history_pe, displacement) # 3. 准备当前帧的初始探针 curr_queries = concat([self.agent_queries, self.map_queries, self.ego_query]) curr_pe = self.initialize_pe() # 4. 核心：Transformer 循环层 (任务并行 + 统一注意力) for layer in self.layers: # (1) Task Self-Attention: 探针之间\u0026#34;圆桌会议\u0026#34; curr_queries = layer.self_attn(curr_queries) # (2) Sensor Cross-Attention: 探针去原始画面里\u0026#34;对图\u0026#34; curr_queries = layer.sensor_cross_attn(curr_queries, image_features) # (3) Temporal Cross-Attention: 向历史探针\u0026#34;取经\u0026#34; curr_queries = layer.temp_cross_attn(curr_queries, compensated_history_pe, history_queries) # (4) 动态位置更新：DETR 头的深度监督与反馈 temp_preds = layer.task_heads(curr_queries) curr_pe = layer.update_pe(temp_preds) # 5. 任务头输出最终结果 (DETR Style) final_results = { \u0026#34;detection\u0026#34;: self.agent_head.predict_box(curr_queries[:900]), \u0026#34;prediction\u0026#34;: self.agent_head.predict_trajectory(curr_queries[:900]), \u0026#34;map\u0026#34;: self.map_head.predict_lines(curr_queries[900:1000]), \u0026#34;planning\u0026#34;: self.ego_head.predict_waypoints(curr_queries[-1]) } # 6. 更新记忆背包 (FIFO Update) top_k_queries = select_top_k(curr_queries, final_results[\u0026#34;detection\u0026#34;].scores) self.fifo_queue.put(top_k_queries, curr_pe_of_top_k, final_results[\u0026#34;detection\u0026#34;].velocity) return final_results 时序交叉注意力的详细实现 class TemporalCrossAttention(nn.Module): def __init__(self, embed_dims=256, num_heads=8): super().__init__() self.mha = MultiheadAttention(embed_dims, num_heads) self.norm = LayerNorm(embed_dims) self.dropout = Dropout(0.1) def forward(self, curr_queries, curr_pe, hist_queries, compensated_hist_pe): \u0026#34;\u0026#34;\u0026#34; curr_queries: 当前帧正在进化的探针特征 [N, C] curr_pe: 当前帧探针的位置编码 [N, C] hist_queries: 从 FIFO 队列取出的历史探针特征 [K, C] (Top-K) compensated_hist_pe: 经过运动补偿后的历史位置编码 [K, C] \u0026#34;\u0026#34;\u0026#34; # 1. 准备 Query （这里用了临时变量q，所以curr_queries没有融合pe） q = curr_queries + curr_pe # 2. 准备 Key \u0026amp; Value (来自历史) k = hist_queries + compensated_hist_pe v = hist_queries # Value 使用原始历史特征，保留纯粹的语义信息 # 3. 执行多头交叉注意力计算 temporal_feat, attn_weights = self.mha( query=q.unsqueeze(1), key=k.unsqueeze(1), value=v.unsqueeze(1) ) # 4. 残差连接与层归一化 out = curr_queries + self.dropout(temporal_feat.squeeze(1)) out = self.norm(out) return out 十、实验结果 封闭环路虚拟仿真：Bench2Drive (CARLA v0.9.15.1) 配置 详情 训练数据 基于官方 1000 个片段 测试路线 220 条路线（含 Dev10 子集） 历史队列长度 $T=10$（10Hz 下的 1 秒钟） 战绩：\nDriving Score 最高可达 63.46 Success Rate 35.01% 大大降低了碰撞率 在传感器失效或长尾场景时，鲁棒性远超对手 开放环路真实世界：nuScenes 数据集 配置 详情 数据类型 真实世界的高清摄像头数据 历史队列长度 $T=4$（2Hz 下的 2 秒钟） 战绩：\n展现极高竞争力的 L2 规划误差 没有使用任何\u0026quot;刷榜作弊\u0026quot;手段（不依赖 Ego-status 偷鸡） 效率与扩展性 Backbone 性能影响 ResNet50 → ResNet101 → EVA02-CLIP-L 模型越大，规划能力越强 输入分辨率最高 (384, 1056) 超高清视觉输入 推理速度：\nDriveTransformer-Large 延迟低至 211.7 毫秒 轻松满足自动驾驶实时性需求 高 FPS，丝滑流畅 十一、总结与未来展望 DriveTransformer 就像是用\u0026quot;奥卡姆剃刀\u0026quot;剔除了自动驾驶中所有不必要的赘肉：\n任务并行 + 稀疏表示 + 流式时序处理\n用最优雅的 Transformer 语言统一了检测、地图构建、运动预测和路径规划。\n核心优势总结 特性 说明 不依赖\u0026quot;专家提点\u0026quot; 纯正端到端框架 不依赖\u0026quot;前置任务预训练\u0026quot; 一体化训练，一气呵成 训练稳定性大幅飙升 消除层级带来的误差累积 深不可测的 Scaling Law 潜力 模型越大，性能越强 未来方向 可以预见，未来的自动驾驶将越来越像 DriveTransformer 这样——聪明、敏捷、一气呵成！\n可能的演进方向：\n引入 Occupancy Network 作为稀疏探针的兜底方案 更强大的视觉 Backbone 更长的时序历史处理能力 与世界模型的结合 这就是 DriveTransformer 的全部奥秘！它用一支训练有素、协同作战的\u0026quot;赛车突击队\u0026quot;，展示了端到端自动驾驶的新范式。🏎️💨\n","date":"2026-04-20T00:00:00Z","permalink":"/post/robotics/e2e/drive-transformer/","title":"DriveTransformer: Unified Transformer for Scalable End-to-End Autonomous Driving"},{"content":" 一、这篇论文在讲什么？ 核心问题 端到端自动驾驶的多模态规划中，江湖上原本分为两派：\n派系 代表方法 优点 缺点 静态词表派 VADv2、Hydra-MDP、DriveSuprim 算力友好，简单高效 颗粒度太粗，动作空间覆盖不足 动态生成派 ipad（回归）、DiffusionDrive、GoalFlow（扩散/流匹配） 极其精细，性能霸榜 模型臃肿，需要额外网络或疯狂迭代降噪 作者的灵魂拷问 动态生成真的是必需的吗？如果我把静态词表塞得密不透风，能不能打败它们？\nSparseDriveV2 的答案 静态词表只要足够密，打分机制就能一统天下！ 纯粹的打分范式（Purely Scoring-based），照样拿 SOTA！\n形象的比喻：\n动态生成派：像一个\u0026quot;现场作画\u0026quot;的艺术家，每次都要从头画一幅精细的作品，耗时耗力 SparseDriveV2：像一个\u0026quot;菜单点菜\u0026quot;的食客，只要菜单够厚（词表够密），就能精准选中自己想要的那道菜 二、暴力扩容实验：用数据说话 为了验证\u0026quot;静态词表的潜力是无穷的\u0026quot;这个猜想，作者拿经典的 Hydra-MDP 模型做了一场\u0026quot;大力出奇迹\u0026quot;实验：\n轨迹锚点数量 NAVSIM v2 EPDMS 得分 1024 85.02 2048 ↑ 4096 ↑ 8192 ↑ 16384 87.35 32768 💥 OOM（显存爆了） 结论：性能一路看涨，完全没有遇到瓶颈！限制它的只是你的算力和显存！\n于是，SparseDriveV2 闪亮登场，它带来了两个改变游戏规则的核心创新，彻底打破了这个内存墙。\n三、核心方法：两大创新 创新一：可扩展的词汇表征 —— 轨迹\u0026quot;解剖术\u0026quot; 一条轨迹包含两个维度：\n空间几何：你去哪儿？（路径） 时间演进：你开多快？（速度） 既然完整的轨迹太多会导致内存爆炸，那就把它们拆了！\n轨迹因式分解（Factorization） 组成部分 定义 采样方式 几何路径 (Path, $p$) 剥离时间，只看地上的车辙印 在最大空间长度 $S_{max}$ 内，每隔固定空间间隔 $\\Delta s$ 采一个点 速度配置 (Velocity, $v$) 剥离空间，只看迈速表 在总时间 $T$ 内，每隔固定时间 $\\Delta t$ 记录一个平均速度 积木重组：如何还原轨迹？ 当需要还原轨迹时，通过计算时间步 $t$ 时的累计行驶距离： $$s_t = \\sum v_k \\Delta t$$\n然后在路径 $p$ 上进行距离插值，就能完美拼出完整的时空轨迹 $\\tau$！\n形象比喻：就像玩乐高积木，你不需要为每种组合单独准备一个成品，只需要准备有限数量的\u0026quot;路径积木\u0026quot;和\u0026quot;速度积木\u0026quot;，然后按需组装即可！\n创新二：丝滑的打分策略 —— 大浪淘沙 面对几十万条轨迹，一个一个打分肯定会死机，于是作者设计了**\u0026ldquo;漏斗式\u0026quot;打分法**：\n┌─────────────────────────────────────────────────────┐ │ 第一层：场景编码 (Scene Encoding) │ │ - 用 ResNet 提取多视角图像特征 │ │ - 编码自车状态 │ └─────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ 第二层：粗粒度解耦打分 (Coarse Factorized Scoring) │ │ - 路径打分：MLP + 场景特征 → Top-K_p 个路径 │ │ - 速度打分：MLP + 场景特征 → Top-K_v 个速度 │ │ - 筛掉离谱组合（高速上猛打方向盘、拥堵路段狂飙） │ └─────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ 第三层：细粒度轨迹打分 (Fine-Grained Scoring) │ │ - 轨迹重条件化（Re-conditioning） │ │ - 可变形聚合（Deformable Aggregation） │ │ - 高精度时空依赖推理 │ │ → 选出最终冠军轨迹 │ └─────────────────────────────────────────────────────┘ 为什么需要\u0026quot;轨迹重条件化\u0026rdquo;？ 高速行驶和急转弯之间存在物理冲突，路径和速度不是绝对独立的！\n所以作者引入了轨迹重条件化（Trajectory Re-conditioning），用可变形聚合让组合后的轨迹特征再次与场景特征互动，进行高精度的时空依赖推理，最终给出精准打分！\n四、词汇构建：从数据到锚点 两步聚类法 作者在海量人类驾驶数据中，用 K-Means 聚类算法 分别聚类出：\n$N_p$ 个路径锚点：代表人类常见的行驶路径形态 $N_v$ 个速度锚点：代表人类常见的速度配置 奇迹时刻：排列组合 把它们两两排列组合！\n$$\\text{总轨迹数} = N_p \\times N_v$$\n只需极小的内存，就能组合出包含几十万条候选轨迹的超高密度轨迹词表！\n五、训练与推理 训练秘籍 损失函数：基于距离的软分类交叉熵损失（CE Loss）\n评估维度 距离度量 路径评估 点到点的平均平方距离（$L_2$ 距离） 速度评估 绝对误差（$L_1$ 距离） 细粒度轨迹评估 $L_2$ 距离 额外监督：还请了一位\u0026quot;基于规则的老师（Rule-based Teacher）\u0026ldquo;来传授心法：\n安全性 行驶进度 舒适度 交通规则遵守度 使用二元交叉熵（BCE）损失进行监督。\n推理流程 综合这些分数，选出\u0026quot;选秀冠军\u0026quot;直接控车！\n六、实验设置：赛场与装备 主战场 NAVSIM 配置 数值 路径词表 $N_p$ 1024（1米间隔，最长50米） 速度词表 $N_v$ 256（0.5秒间隔，最长4秒） 总候选轨迹 262,144 条 与前人对比 比常用的 8192 个锚点密 32 倍！ 推流过滤魔法 26万条轨迹怎么算？\n层级 筛选策略 结果 第一层 Top 128 路径 + Top 64 速度 筛掉大部分离谱组合 第二层 Top 20 路径 + Top 20 速度（v2加速：Top 10 速度） 形成高质量子集 最终 细粒度打分 只给 400 条最靠谱的轨迹打分 轻量级装备 配置项 数值 骨干网络 ResNet-34（仅 21.8M 参数） 输入分辨率 256 x 512 训练设备 8 张 NVIDIA L20 GPU Batch Size 128 学习率 $1 \\times 10^{-4}$ 权重衰减 0 训练 Epochs 10 七、主打战绩：越级挑战 NAVSIM v1 榜单 PDMS 终极高分：92.0\n完爆其他打分方法和动态生成方法 \u0026ldquo;以下犯上\u0026rdquo;：用袖珍的 ResNet-34 干翻了配备 V2-99 骨干网络（96.9M参数）的 GoalFlow 和 Hydra-MDP！ NAVSIM v2 榜单 模型 EPDMS 分数 SparseDriveV2 90.1 DiffusionDriveV2（前任霸主） 87.5 特别是在 EP（驾驶进度） 指标上进步神速，证明了致密词表真的能覆盖更广的动作空间！\nBench2Drive（闭环测试） 指标 分数 驾驶得分 89.15 成功率 70.00% 多能力得分 67.67% 全面碾压 TCP-traj 和 DriveAdapter 等一众强敌，展现了强大的复杂场景泛化能力。\n八、消融实验：抽丝剥茧 词表密度实验 词表大小 EPDMS 分数 512 × 128 88.7 \u0026hellip; ↑ 1024 × 256 90.1 结论：词表越密，效果越好！\n关键组件实验 配置 效果 可变形聚合（DFA） ✅ 提升 轨迹重条件化（Re-conditioning） ✅ 提升 DFA + Re-conditioning 🏆 黄金搭档 九、附录彩蛋：Bench2Drive 闭环实验细节 配置调整 配置项 数值 路径锚点最大长度 15 米 速度视野 3 秒 骨干网络 ResNet-50 训练设备 16 张 L20 显卡 输入 6 个摄像头（256 x 704） 辅助任务 3D 目标检测、在线建图、运动预测 两段式训练 第一阶段：100 个 Epoch 的感知训练 第二阶段：10 个 Epoch 的感知与规划联合训练（纯模仿学习，没用规则老师监督） 学习率分别为 $4 \\times 10^{-4}$ 和 $3 \\times 10^{-4}$。\n如何控车？ 控制维度 策略 横向控制 \u0026ldquo;随速可变目标距离\u0026rdquo;：$d = 0.5 \\times \\text{自车速度} + 2.5$，寻找路径上的预瞄点打方向盘 纵向控制 直接取速度配置里的第一个速度作为目标油门/刹车 定性结果展示 与基线相比（红线），SparseDriveV2 的轨迹（蓝线）：\n更贴近人类老司机（绿线） 急转弯更平滑 交通效率更高 不会像呆子一样停在原地！ 坦诚的局限性：在缺乏导航信息的情况下，系统偶尔也会\u0026quot;迷路\u0026rdquo;。\n十、个人思考与疑问 值得学习的亮点 极简哲学：用最纯粹的打分思路，证明了\u0026quot;简单方法 + 大规模\u0026quot;可以打败\u0026quot;复杂方法 + 小规模\u0026quot; 因式分解的智慧：把轨迹拆成路径和速度两个维度，巧妙地绕过了显存爆炸的问题 漏斗式打分：三层筛选机制既保证了计算效率，又没有牺牲最终精度 待探索的问题 词表密度的极限：如果显存继续增大，词表密到什么程度会开始出现边际效应递减？ 路径和速度的耦合：重条件化机制是否可以进一步优化，更好地捕捉两者的深层关联？ 与扩散模型的融合：能否将 SparseDriveV2 的致密词表思想与扩散模型的生成能力结合起来？ 参考链接 论文原文：https://arxiv.org/pdf/2603.29163 相关工作：Hydra-MDP, VADv2, DiffusionDrive, GoalFlow ","date":"2026-04-20T00:00:00Z","permalink":"/post/robotics/e2e/sparse-drive-v2/","title":"SparseDriveV2: Scoring is All You Need for End-to-End Autonomous Driving"},{"content":" 一、研究背景：具身智能的\u0026quot;两难困境\u0026quot; 在机器人领域，一直存在着两派势力的较量：\nVLA 派（视觉-语言-动作模型） 代表：OpenVLA\n特点：\n反应极快，看到图像就直接输出动作 像\u0026quot;直觉派\u0026quot;选手，出手如电 缺点：\n不懂物理规律，不知道物体被碰后会怎样 容易\u0026quot;手残\u0026quot;，在精细操作上容易出错 WAM 派（世界动作模型） 特点：\n不仅预测动作，还要预测未来的视频画面 像\u0026quot;思辨派\u0026quot;选手，深思熟虑 缺点：\n太慢了！ 执行前要进行几十次 Diffusion 去噪步骤 机器人动一下可能要等几秒钟，简直是\u0026quot;慢动作重播\u0026quot; 核心问题 灵魂拷问 世界模型之所以强，是因为推理时需要\u0026quot;脑补\u0026quot;未来视频？还是因为训练时学过如何生成视频？\n作者大胆假设：视频生成的价值在于训练，而非推理。 能不能既要 WAM 的\u0026quot;脑子\u0026quot;（物理常识），又要 VLA 的\u0026quot;手速\u0026quot;？\n二、Fast-WAM 的核心思想 核心理念 训练时拼命\u0026quot;画画\u0026quot;，推理时只管\u0026quot;出招\u0026quot;。\n想象一下，你让一个机器人折毛巾：\n传统 WAM 是个\u0026quot;慢性子\u0026quot;：每动一下，都要先在脑子里花几秒钟\u0026quot;脑补\u0026quot;出接下来的动作会产生什么样的视频画面，然后再根据画面去出招 Fast-WAM 的做法：在训练场让你画画是为了让你理解肌肉发力的逻辑，等上了战场，直接出拳就行，别在那儿画画了！ 核心结论 💡 核心发现 世界模型的力量源泉在于联合训练带来的表征提升，而不是推理时那花里胡哨的视频生成。\n视频训练就像是给机器人读了《物理百科全书》，不读这套书，机器人就是个瞎子；但读过之后，干活时并不需要一边干一边复述书里的内容。\n三、方法论详解 3.1 底层基座：Wan2.2-5B 视频 Transformer Fast-WAM 不是从零开始的，它寄生在一个巨大的\u0026quot;智慧大脑\u0026quot;上——Wan2.2-5B，这是一个拥有 50亿参数 的视频生成大模型。\n它的角色：提供\u0026quot;常识\u0026quot;——它知道如果手往左拨，杯子就会向左倒。\n3.2 MoT（Mixture-of-Transformer）架构 这是 Fast-WAM 最核心的\u0026quot;手术刀\u0026quot;，精准解决了\u0026quot;大模型太笨重\u0026quot;和\u0026quot;机器人要灵敏\u0026quot;之间的矛盾。\n形象比喻 如果把普通的 Transformer 比作一个**\u0026ldquo;全科医生\u0026rdquo;（什么都干，但由于太胖，反应慢），那么 MoT 就像是一个\u0026ldquo;专家门诊部\u0026rdquo;**。\nMoT 在 Fast-WAM 中扮演了三个关键角色：\n1. 知识的\u0026quot;寄生与借力\u0026quot;（Knowledge Injection） 做法：在大模型内部\u0026quot;插入\u0026quot;了一个专门处理动作的 Action Expert（动作专家），约 10 亿参数 比喻：就像在一个精通物理规律的\u0026quot;老教授\u0026quot;（视频底座）大脑里，植入了一个\u0026quot;专业运动员\u0026quot;（动作专家）的运动神经 运动员在做动作时，能实时调用老教授脑子里的物理常识，但他不需要老教授亲自下场画图 2. 任务的\u0026quot;结构化解耦\u0026quot;（Task Decoupling） 视频预测和动作预测虽然有联系，但它们的\u0026quot;脑回路\u0026quot;是不一样的：\n视频预测：关注全局，比如背景变没变，光影漂不漂亮 动作预测：关注细节，比如夹爪现在离桌子几厘米，力道够不够 MoT 通过不同的 Router（路由） 或特殊的注意力掩码，让模型在处理数据时，一部分参数专门盯着\u0026quot;画面怎么变\u0026quot;，另一部分参数专门盯着\u0026quot;手该怎么动\u0026quot;。\n3. 实现\u0026quot;推理大瘦身\u0026quot;的开关（The Efficiency Switch） 这是 Fast-WAM 能跑得快的终极秘密：\n阶段 MoT 的工作状态 训练时 所有专家都上班。一边生成视频，一边预测动作。动作专家能学到：\u0026ldquo;当老教授预测毛巾会动时，我这一步的动作指令是正确的。\u0026rdquo; 推理时 只让动作专家上班。动作专家已经通过训练掌握了老教授的物理神髓，直接关掉负责\u0026quot;画图/渲染视频\u0026quot;的那些耗电、耗时的分支。 3.3 Token 的三分法 论文将输入 Token 明确划分为三大类，配合结构化掩码实现了精妙的信息流控制：\n🟦 第一帧干净隐变量 Token（共享视觉锚点） 角色：现实世界的\u0026quot;起点\u0026quot; 特点：干净的、不加噪声的。训练和推理都有 逻辑：模型唯一的真实参考，所有\u0026quot;脑补\u0026quot;都必须基于这个起点 注意力规则：孤芳自赏，谁也不看 🟨 未来帧噪声 Token（模拟实验室素材） 角色：供视频建模用 特点：只有训练时才塞进序列，被加了不同程度的噪声 命运：推理时彻底踢掉 注意力规则：可以相互双向注意，也可以看到第一帧；可以看到动作 Token 🟥 动作 Token（最终指令输出） 角色：由动作专家负责去噪生成动作序列 特点：采用动作离散化，变成类似\u0026quot;单词\u0026quot;的形式 注意力规则：只能看到第一帧，绝对不能看未来帧——防止信息泄露！ ⚠️ 关键设计 动作 Token 绝对不能看未来帧！\n原因：如果训练时动作 Token 看到了未来帧，它就会发现：\u0026ldquo;哦，未来毛巾已经折好了，那我反推一下现在的动作就行。\u0026ldquo;这叫信息泄露。这样学出来的机器人，一旦到了推理阶段（没有未来帧可以看），它就直接瘫痪了。\n3.4 精妙的\u0026quot;防作弊\u0026quot;注意力掩码 ┌─────────────────────────────────────────────────────┐ │ 注意力规则表 │ ├─────────────────────────────────────────────────────┤ │ 规则 A：动作 Token (🟥) 绝对不能看未来帧 (🟨) │ │ → 防止\u0026#34;偷看答案\u0026#34;，强迫学习物理直觉 │ │ │ │ 规则 B：未来帧 (🟨) 可以看动作 Token (🟥) 和起点 (🟦) │ │ → 保证视频生成分支学习因果关系 │ │ │ │ 规则 C：第一帧 (🟦) 谁也不看 │ │ → 它是给定的事实，不需要被任何预测信息干扰 │ └─────────────────────────────────────────────────────┘ 3.5 训练 Loss 设计 Fast-WAM 的 Loss 设计是一个**\u0026ldquo;双头并进、联合练兵\u0026rdquo;**的策略：\n$$L_{total} = L_{video} + \\lambda L_{action}$$\n$L_{video}$：物理规律的\u0026quot;强制灌输\u0026rdquo; 技术细节：基于 Flow Matching（流匹配） 或标准的 Diffusion Loss 通俗解释：给模型一张模糊的、加了噪声的未来画面，它必须想办法还原出清晰的画面 作用：让模型的大脑对物理世界保持敏感，练就\u0026quot;火眼金睛\u0026rdquo; $L_{action}$：肌肉记忆的\u0026quot;精准打磨\u0026quot; 技术细节：采用 Diffusion-based Loss 通俗解释：教练在旁边看着机器人折毛巾，每动一下就对比专业动作给出差评 精妙之处：这个损失作用在 Action Expert 那个 1B 大小的\u0026quot;专家插件\u0026quot;上，同时也会通过反向传播微调底座 $\\lambda$ 系数：动态平衡的\u0026quot;调音师\u0026quot; 决定模型是更想当一个\u0026quot;画家\u0026quot;（视频生成）还是\u0026quot;工人\u0026quot;（动作执行）。\n为什么这样设计能\u0026quot;借力打力\u0026quot;？ 在训练时，由于 $L_{video}$ 和 $L_{action}$ 在同一个 Transformer 结构中同时优化，它们会共享中间的 Hidden States：\n当 $L_{video}$ 强迫模型去理解\u0026quot;玻璃杯碎了\u0026quot;的物理过程时，Transformer 的中间层会产生**\u0026ldquo;具有物理感知力\u0026quot;的特征信号** 负责动作预测的支路会立刻\u0026quot;偷听到\u0026quot;这个信号 $L_{action}$ 的优化过程发现：如果借用视频分支分析出的物理特征，预测动作的准确率会大大提升！ 结果：训练结束后，动作分支已经彻底吸收了视频分支的\u0026quot;内功\u0026rdquo;。即便推理时砍掉未来帧 Token，动作分支依然能凭借已经练就的\u0026quot;肌肉记忆\u0026quot;做出符合物理规律的反应。\n形象比喻 Fast-WAM 的 Loss 设计就像是一个双人舞训练。视频 Loss 负责教舞步的逻辑和平衡感，动作 Loss 负责教力量和落点。虽然演出时只有动作分支上台，但他脑子里记着的，全都是排练时视频分支带给他的节奏感。\n3.6 推理逻辑：想象力\u0026quot;截断\u0026quot; 特性 传统视频动作模型 (WAM) Fast-WAM 推理步骤 去噪采样 单次前向传播 性能瓶颈 必须生成高清视频像素 只计算潜在特征 物理理解 靠\u0026quot;看图\u0026quot;来确认 靠\u0026quot;联合训练\u0026quot;内化到权重中 延迟 很高 (1s ~ 5s+) 极低 (190ms) 一句话逻辑：\nFast-WAM 通过 MoT 结构在 5B 视频大模型里安插了\u0026quot;动作特务\u0026quot;，在训练时利用视频生成任务来磨练这些特务的物理嗅觉，但在实战中卸磨杀驴（关掉视频生成），从而实现了\u0026quot;有着世界模型灵魂的 VLA 速度\u0026quot;。\n四、实验结果 4.1 仿真战场 LIBERO（长程规划和空间推理测试） Fast-WAM 成功率：97.6% 与慢悠悠的\u0026quot;完整脑补派\u0026quot;不相上下 RoboTwin 2.0（复杂环境） Fast-WAM 表现依然稳健 4.2 终极考验：现实世界折毛巾！ 机器人：Galaxea R1 Lite 数据：60 小时遥操作数据 惊人发现 训练方式 成功率 开启视频联合训练 90%+ 关闭视频联合训练 10% 关键发现 如果关掉训练时的\u0026quot;视频辅助\u0026quot;，成功率从 90%+ 暴跌到 10%！\n这证明了：视频训练的价值在于训练阶段，而非推理阶段。\n五、与 LeWorldModel 的对比讨论 5.1 问题引入：单帧预测的局限性 深刻质疑 如果任务比较难，需要预测的未来动作序列比较长，光看第一帧的隐变量 Token 预测后续动作的难度是不是大了些？\n就好像下围棋的时候，如果不进行多步的未来棋局推演，只看当前棋局就直接落子，落子质量应该会低很多吧？\n这个问题切中了具身智能界最大的争论之一：\u0026ldquo;直觉反应（System 1）\u0026ldquo;与\u0026quot;深思熟虑（System 2）\u0026ldquo;的路线之争。\n5.2 Fast-WAM 的真实工作模式 首先要澄清：Fast-WAM 在真正干活时，并不是只看一开始的那一帧，就盲目地把未来 10 分钟的动作全预测完。\n动作分块+ 闭环控制：\n看当前的\u0026quot;第一帧\u0026rdquo;，预测未来一小段（比如 16 步或 64 步）的动作序列 执行这几步动作后，会再次睁开眼睛，把最新的画面作为新的\u0026quot;第一帧\u0026rdquo;，再次预测 比喻：就像你打乒乓球——你不是在发球那一刻就算好接下来 10 个回合怎么打，你是看着球飞过来，瞬间做出挥拍动作，打完再看下一回合。\n5.3 Fast-WAM vs LeWorldModel：两种哲学 根据 LeWorldModel 论文（arXiv: 2603.19312，Yann LeCun 团队，2026年3月）：\n🔴 Fast-WAM：\u0026ldquo;大模型 + 肌肉记忆\u0026rdquo; 特性 描述 路线 找一个看懂所有物理规律的 50亿参数\u0026quot;巨人\u0026rdquo; 提速方法 干活时蒙上巨人的眼睛，不让他画图，只提取物理直觉直接输出动作 延迟 190ms 适用场景 折毛巾、接飞球、动态物体操作（高频、动态、物理交互） 🔵 LeWorldModel：\u0026ldquo;小模型 + 纯粹物理法则\u0026rdquo; 特性 描述 路线 不依赖庞大预训练视频模型，从零开始学习 提速方法 只有 15M 参数（Fast-WAM 的 1/333），抛弃复杂像素生成，运行在特征空间 延迟 0.98s（含 MPC 多步推演） 适用场景 迷宫寻宝、多步骤逻辑任务、复杂环境导航 5.4 延迟对比 模型 延迟 频率 适用场景 Fast-WAM 190ms ~5 Hz 毫秒级\u0026quot;物理操作时间\u0026quot;——手已抓到门把手，感受阻力瞬间调整发力 LeWM 0.98s ~1 Hz 1秒级\u0026quot;战术规划时间\u0026quot;——走到厨房门口，推演怎么绕过障碍物 5.5 终极思考：大小脑结合 💡 未来方向 单一模型无法通吃。未来的完美机器人，一定会把两者结合起来：\n大脑（System 2 - 类似 LeWorldModel）：负责在后台慢速运行。接到\u0026quot;做一顿年夜饭\u0026quot;的指令时，大脑开始推演步骤，进行 MPC，制定长程计划。\n小脑（System 1 - 类似 Fast-WAM）：负责前台的高速执行。当大脑决定\u0026quot;现在去切土豆\u0026quot;时，小脑接管身体，利用极速反应和物理直觉，稳准狠地把土豆切成丝。\n六、总结 核心贡献 打破迷思：证明了世界模型的力量源泉在于联合训练带来的表征提升，而非推理时的视频生成 架构创新：MoT 结构让\u0026quot;物理常识\u0026quot;和\u0026quot;动作反应\u0026quot;在同一架构里深度交融，又能在干活时\u0026quot;分家\u0026quot; 极致速度：用 50 亿参数模型的智慧，跑出轻量化模型的速度（190ms） 形象总结 这篇论文就像是一个高效的教练，告诉机器人：\u0026ldquo;我在训练场让你画画是为了让你理解肌肉发力的逻辑，等你上了战场，直接出拳就行，别在那儿画画了！\u0026rdquo;\n这种\u0026quot;重训练、轻推理\u0026quot;的思路，很可能会成为 2026 年之后机器人 foundation model 的标准范式。\n相关阅读 [[LeWorldModel]] - LeCun 团队的 JEPA 架构世界模型，提供了另一种\u0026quot;快\u0026quot;的哲学 ","date":"2026-04-16T00:00:00Z","permalink":"/post/robotics/worldmodel/fast-wam/","title":"Fast-WAM - 世界动作模型在推理时真的需要\"脑补\"未来吗？"},{"content":" 目录 [[#一、论文概述：大道至简的\u0026quot;绝世武功\u0026quot;]] [[#二、核心痛点：特征坍缩的幽灵]] [[#三、方法论：两步走练就火眼金睛]] [[#四、规划性能表现：赛道狂飙，稳如老狗]] [[#五、量化物理理解：不仅会看，还懂点物理]] [[#六、结论与局限性]] [[#七、深度问答：那些让人拍案叫绝的细节]] 一、论文概述：大道至简的\u0026quot;绝世武功\u0026quot; 1.1 核心贡献 这篇论文提出了史上首个能从纯像素端到端稳定训练的 JEPA（联合嵌入预测架构） 世界模型——LeWorldModel (LeWM)。\n它的核心创新可以用一句话概括：\n仅仅依靠两个损失项，把超参数从6个骤降到1个！\n1.2 惊人的数字 指标 数值 模型参数量 1500万 训练需求 单张GPU，几小时 规划速度 比 DINO-WM 快48倍 超参数数量 仅1个 1.3 JEPA vs 生成式世界模型 论文中对比了两大流派：\n生成派世界模型（如 Dreamer, Oasis）：\n直接预测未来的像素画面 算力开销大，高度依赖奖励信号 像是一个写实派画家——能画出极度逼真的未来，但画得太慢 JEPA派（如 I-JEPA, PLDM, DINO-WM）：\n在高维潜空间进行特征预测 无奖励、重特征 像是一个极简派战略家——推演速度极快，但容易偷懒作弊（特征坍缩） 而 LeWorldModel 做到了：既坚守了\u0026quot;战略家\u0026quot;极速、抓重点的优势，又用一套绝妙的数学法则彻底封死了偷懒作弊的后路！\n二、核心痛点：特征坍缩的幽灵 2.1 什么是特征坍缩？ 想象一下，我们想让AI通过观察像素画面来预测未来。JEPA架构不傻乎乎地去预测每一个像素点，而是提取画面核心特征，在\u0026quot;高维潜空间\u0026quot;里进行预测。\n但JEPA有个致命缺陷——\u0026ldquo;特征坍缩\u0026rdquo;：AI为了最小化预测误差，会疯狂偷懒，把所有画面都预测成同一个常数特征，直接罢工！\n2.2 为什么输出常数能最小化误差？ 用\u0026quot;考试\u0026quot;的比喻来解释：\n出题人（Encoder 编码器）：负责看未来的真实画面，提取出\u0026quot;标准答案\u0026quot;（特征向量） 答题人（Predictor 预测器）：负责看过去的画面和动作，猜出未来的\u0026quot;预测答案\u0026quot; 考试的目标只有一个：让\u0026quot;预测答案\u0026quot;和\u0026quot;标准答案\u0026quot;越接近越好。\n如果\u0026quot;出题人\u0026quot;和\u0026quot;答题人\u0026quot;可以私底下互相沟通（在端到端训练中，它们的参数是一起更新的），他们会怎么做？\n他们会直接串通作弊！\n出题人说：\u0026ldquo;兄弟，不管我看到什么复杂的画面，我都把标准答案写成 C。\u0026rdquo; 答题人说：\u0026ldquo;太好了，那我不管三七二十一，永远都猜 C。\u0026rdquo;\n结果：每次答题全都对得上，预测误差直接变成了绝对的0！ 但实际上，他们什么知识都没学到。\n2.3 为什么不直接预测像素？ 这是一个非常直观的想法：既然要对比，为什么不直接让预测器预测未来帧的图像，然后对比预测图像和真实图像的差异？\n这确实是另一大流派（生成式世界模型）的思路。但预测像素会带来三个致命问题：\n问题1：\u0026ldquo;风中的树叶\u0026quot;难题 想象你正在开一辆自动驾驶汽车，路边有一棵树，树叶在风中疯狂摇摆。\n如果你的大脑是一个\u0026quot;预测像素\u0026quot;的世界模型，为了让 Loss 变小，你的大脑必须耗费极其庞大的算力去预测\u0026quot;下一秒，这第一万零三十二片树叶会飘到什么角度\u0026rdquo;。\n但这对你开车有任何意义吗？毫无意义！\n问题2：\u0026ldquo;模糊的未来\u0026quot;难题 真实世界充满随机性。假设你扔了一枚硬币，下一秒它可能是正面，也可能是反面。\n如果让AI用 MSE 去预测下一帧画面的像素，它不敢画正面，也不敢画反面，而是会把正面和反面的像素叠加在一起取平均，最后画出一个\u0026quot;模糊的、半透明的幽灵硬币\u0026rdquo;。\n问题3：规划速度极其龟速 在做动作规划时，AI需要在脑海里撒出300条路线，每条路线往后推演好几步。如果用\u0026quot;预测像素\u0026quot;的模型，算力根本扛不住。\n而 LeWorldModel 因为完全抛弃了像素，只在极其轻量的高维特征空间里进行纯数学向量的推演，规划速度飙升了48倍！\n三、方法论：两步走练就火眼金睛 LeWM的内功心法非常清晰，主要分为**\u0026ldquo;世界模型训练\u0026rdquo;和\u0026ldquo;潜空间规划\u0026rdquo;**两步。\n3.1 训练世界模型 数据源 无需任何奖励标签的离线轨迹数据（纯像素画面 + 动作）。\n编码器（Encoder） 采用轻量的 ViT-Tiny（约500万参数）。\n魔鬼细节：作者特意把ViT最后一层的 Layer Normalization 替换成了\u0026quot;单层 MLP + Batch Normalization\u0026quot;。因为 LayerNorm 会阻碍后续抗坍缩正则化的发挥！\n预测器（Predictor） 采用 ViT-Small（约1000万参数，带10% Dropout）。它能看前N帧的记忆，通过带因果掩码的自回归来预测下一帧。\n动作如何输入？ 作者巧妙使用了 AdaLN（自适应层归一化） 融合动作信息，并将其参数初始化为0，让模型在训练初期更平稳。\n两大绝招（训练目标） 1. 预测损失（MSE）\n要求模型预测的未来特征和真实的未来特征尽量吻合。\n2. 防坍缩神技（SIGReg 正则化）\n为了不让模型偷懒输出同一个值，SIGReg强制要求潜空间的特征分布必须长得像一个\u0026quot;各向同性的高斯分布\u0026quot;。\n高维空间太难搞？根据数学上的 Cramér-Wold 定理，LeWM 将高维特征随机投影到 $M$ 个（默认1024个）一维方向上，然后用 Epps-Pulley 正态性检验公式去算一维分布的拟合度。\n3.2 为什么是正态分布？ 这是一个直击灵魂的问题：为什么不能让特征散成一个正方体（均匀分布）、一个甜甜圈、或者一颗五角星？\n信息论视角：\u0026ldquo;最能装\u0026quot;的行李箱 在方差固定的情况下，正态分布是所有分布中\u0026quot;熵\u0026quot;最大的。\n熵代表了\u0026quot;信息量\u0026rdquo;。如果特征呈现正态分布，意味着它们在给定的空间里，达到了\u0026quot;最大程度的无序与丰富\u0026quot;，把空间的每一个缝隙都利用到了极致。\n几何学视角：\u0026ldquo;绝对公平\u0026quot;的完美圆球 高维空间里的标准正态分布（各向同性高斯分布），就像是一个边缘模糊、绝对对称的\u0026quot;完美能量球\u0026rdquo;。\n无论你在球里的哪个位置，无论你想朝着哪个方向做物理推演，空间的几何性质都是绝对一致的。\n工程学视角：\u0026ldquo;算力地狱\u0026quot;里的救命稻草 正态分布是唯一能用极低算力写出漂亮 Loss 公式的分布！\nCramér-Wold 定理：无论多少维的正态分布，只要你用手电筒从任意方向照过去，它的\u0026quot;一维影子\u0026quot;都必定是正态分布！\nEpps-Pulley 检验公式：判断一个一维影子是不是正态分布，有一个极其优雅的公式，完全平滑、完美可导！\n3.3 SIGReg 如何防止作弊？ 有人可能会问：编码器和预测器能不能约定好一套正态分布的参数，然后随机生成分布内的点，假装自己学会了？\n答案是：不行！\n因为 LeWorldModel 精心设计了一个**\u0026ldquo;死局（Double Bind）\u0026rdquo;**：\n如果编码器输出纯随机的高斯分布，SIGReg Loss 是完美的，但预测器完全无法预测（MSE 爆炸） 如果编码器要满足 MSE（可预测性），特征必须遵循物理因果律 唯一的活路：把真实物理世界的规律，一比一地\u0026quot;拓印\u0026quot;到那个高维的正态分布空间里 3.4 LayerNorm vs BatchNorm 这是一个极其硬核的架构细节。\nLayerNorm 是\u0026quot;猪队友\u0026rdquo;：\nLN 只管单个样本内部的和谐，不管不同样本之间的差异 全班128个学生交出一模一样的答卷，LN 会觉得\u0026quot;完美\u0026quot; LN 在数学上把所有特征向量强行投影到一个\u0026quot;高维的空心球面\u0026quot;上，与 SIGReg 要求的\u0026quot;实心能量球\u0026quot;冲突 BatchNorm 是\u0026quot;神助攻\u0026quot;：\nBN 的工作逻辑和 SIGReg 在同一个频道上 BN 在物理架构上强制所有特征在 Batch 内必须散开（方差为1） BN 负责把数据\u0026quot;撑开\u0026quot;，SIGReg 只需要负责\u0026quot;精雕细琢\u0026quot; 为什么前面还要垫一个\u0026quot;单层 MLP\u0026quot;？\n因为 ViT 的内部结构极其依赖 LN。如果直接把 LN 换成 BN，ViT 内部的注意力机制会崩盘。\nMLP 作为\u0026quot;缓冲带\u0026quot;，把 ViT 内部的\u0026quot;LN 格式信号\u0026quot;翻译、转换到一个全新的特征空间里，摆脱\u0026quot;空心球壳\u0026quot;的死板几何束缚。\n3.5 潜空间规划 怎么用学好的模型来做事？LeWM 采用了 模型预测控制 (MPC) 搭配 交叉熵方法 (CEM)。\n在脑海中想象未来：\n给一个初始画面和目标画面 CEM 撒出 300 条随机动作序列 在潜空间里往后推演 5 步（由于跳帧设置=5，相当于环境里的 25 步） 挑出最接近目标特征的动作 执行一小段后，根据新画面重新规划 四、规划性能表现：赛道狂飙，稳如老狗 作者在四大测试场对 LeWM 进行了全面\u0026quot;拷问\u0026quot;：\n二维迷宫 TwoRoom 推方块 PushT 三维机械臂抓取 OGBench-Cube 二维机械臂 Reacher 4.1 硬核战绩 在难度极高的 PushT 任务上，LeWM 的成功率直接比 PLDM 高出18%！ 纯像素输入的 LeWM，面对带着庞大预训练知识的 DINO-WM，竟然实现了反超！ 规划速度实现了 48倍的飙升！完整规划不到1秒就能算完 4.2 稳如泰山的训练 超参地狱的终结：只有一个有效超参数——SIGReg权重 $\\lambda$。实验证明不管设多少（0.01~0.2），成功率都在80%以上。\n曲线丝滑：相比于 PLDM 那像过山车一样上下剧烈震荡的7项 Loss 曲线，LeWM 的2项 Loss 曲线平滑且单调下降。\n架构不敏感：即使把 ViT 编码器换成古老的 ResNet-18，LeWM 依然能打。\n五、量化物理理解：不仅会看，还懂点物理 LeWM 在脑海里默默构建了物理法则！\n5.1 读心术（特征探针 Probing） 用线性探测器去解析它的特征向量，发现里面清晰地编码了物体的位置、机械臂的角度等真实物理坐标（MSE误差极低），碾压了 PLDM。\n线性探测器是怎么做的？\n冻结编码器：把训练好的编码器请到审讯室，冻结所有参数 请一位\u0026quot;头脑极其简单\u0026quot;的警察：线性探测器就是一个最基础的线性回归方程，没有任何非线性激活函数，也没有多层网络结构 对账与判决：如果只会做加减乘除的警察能精准报出物理坐标，说明编码器已经把物理世界高度整理过了 5.2 脑内画面重现（Decoder） 如果在训练时外挂一个解码器（不参与反向传播），发现仅仅依靠被极度压缩的特征向量，就能完美重建出原始的像素画面！\n解码器是预先训练好的吗？\n不是！ 这个解码器必须是\u0026quot;零基础的小白\u0026quot;（随机初始化的全新网络）。\n为什么？如果用预训练好的强大解码器，它会利用自己的常识疯狂\u0026quot;脑补\u0026quot;，硬生生画出一张极度逼真的图，但科学家无法判断：这完美画面是因为特征提取得好，还是因为解码器太会脑补？\n为了防止作弊，科学家采取了极其严苛的手段：\n找一个随机初始化的笨蛋解码器：脑子里一片空白 装上一面\u0026quot;单向玻璃\u0026quot;（Stop-Gradient）：特征向量可以从编码器传给解码器；但当解码器画错像素产生 Loss 时，惩罚不准传回编码器 解码器会不会学会\u0026quot;脑补\u0026quot;？\n不会！因为有四道防线：\nMSE Loss 的\u0026quot;死板对账\u0026quot;：脑补逼真 ≠ 脑补正确，像素级核对必须精准 故意\u0026quot;饿死\u0026quot;解码器的脑容量：解码器设计得极其简陋，没有容量记忆环境常识 \u0026ldquo;平行宇宙\u0026quot;的突击考试：用从未见过的新测试集验证，解码器依然完美重构 证据链闭环：线性探测器也能从特征中提取精确物理坐标 5.3 涌现的\u0026quot;时间拉直\u0026rdquo;（Temporal Straightening） 超炫酷的现象！随着训练进行，模型在潜空间里预测的轨迹竟然自动变成了一条\u0026quot;直线\u0026quot;。\nLeWM 根本没加任何平滑约束，却比特意加了平滑 Loss 的 PLDM 还要\u0026quot;直\u0026quot;。这说明模型自发学会了最优雅的动态表征！\n5.4 \u0026ldquo;吓一跳\u0026quot;测试（期望违背 VoE） 借鉴心理学测试婴儿认知的方法，给模型看三种视频：\n正常的 物体突然变色的（视觉扰动） 物体瞬间瞬移的（物理扰动） 结果显示：面对变色，AI内心毫无波澜；但面对违背牛顿定律的\u0026quot;物体瞬移\u0026rdquo;，AI的预测误差瞬间暴增（吓了一大跳）！\n六、结论与局限性 6.1 总结 LeWM 是一套可扩展、有数学原理支撑、极具解释性的世界模型新范式。\n6.2 阿喀琉斯之踵 1. 短视\n目前规划视野还比较短（几十步），未来需要层次化的世界模型来解决长视野规划。\n2. 数据饥渴与特征维度局限\n如果环境太简单（比如极简的 TwoRoom 二维迷宫），环境本身内在维度太低，强行让它填满高维高斯分布会比较别扭，导致此时表现稍逊于 PLDM。\n3. 依赖动作标签\n目前必须输入标注好的 Action 才能训，未来如果能引入逆动力学（Inverse Dynamics）自学 Action 就完美了。\n6.3 附录中的\u0026quot;魔鬼细节\u0026quot; 配置项 推荐值 Frame-skip 5 输入帧数 4个画面帧 + 4个动作块 Batch Size 128 CEM规划器方差初始化 1 每次迭代撒轨迹数 300 精英数量 Top 30 PushT优化次数 30 其他环境优化次数 10 预测器 Dropout 0.1（甜点区） 预测器架构 ViT-S（最完美） 关键发现：\nDropout 设为 0.0 或 0.5，成功率都会断崖式下跌（从96%掉到78%或66%） 预测器用 ViT-Tiny 会欠拟合，用 ViT-Base 反而会轻微掉点 把重构画面的 Loss 加回训练中，成功率从96%降到了86% 七、深度问答：那些让人拍案叫绝的细节 7.1 CEM 动作序列在不同环境中的形式 在不同测试场中，CEM 撒出的300条动作序列，在数学形式和物理含义上完全不同：\n环境 动作维度 D 物理含义 TwoRoom 2 二维位移/速度向量 PushT 2 推杆在二维桌面上的目标坐标 (X, Y) Reacher 2 两个马达的关节扭矩（Torque） OGBench-Cube 4或7 三维末端位移 + 夹爪开合，或7个关节角度 CEM 生成的矩阵形状是 [300, 5, D]：\n300：300种可能的未来分支 5：往未来推演的5步 D：动作维度 预测器怎么消化这些不同形式的物理参数？\n通过 AdaLN 和 动作编码器：\n统一翻译：一个单层 MLP 把动作参数映射成高维的\u0026quot;动作特征向量\u0026quot; 灵魂注入：AdaLN 把动作特征融合到图像特征里 \u0026ldquo;无感\u0026quot;推演：预测器只处理抽象特征，不知道具体是什么动作 7.2 CEM 是怎么\u0026quot;大海捞针\u0026quot;的？ 短短1秒钟内的进化过程：\n第1轮（纯蒙）：CEM 纯随机生成300条乱七八糟的动作指令 预测器推演：把这300条指令全部在脑海里\u0026quot;播放\u0026quot;一遍 优胜劣汰：对比目标特征，只有Top 30勉强靠近目标 提炼规律：分析精英动作，生成新的正态分布 第2轮（精准撒网）：围绕规律再次撒出300条动作 在 PushT 环境中，这个过程会狂刷30次！\n总结 整篇论文看下来，LeWorldModel 宛如一把精钢铸就的利剑，不仅斩断了过去世界模型复杂的\u0026quot;正则化乱麻\u0026rdquo;，更让普通人在单张显卡上探索物理世界规律成为了可能！\n它的核心贡献可以概括为：\n用两个 Loss、一个超参数，实现了端到端、稳定、高效的世界模型训练——同时掌握了不可思议的物理直觉！\n#论文 #世界模型 #JEPA\n","date":"2026-04-08T00:00:00Z","permalink":"/post/robotics/worldmodel/le-world-model/","title":"LeWorldModel - 稳定的端到端联合嵌入预测架构世界模型"},{"content":"WorldDrive: Bridging Scene Generation and Planning 论文链接: https://arxiv.org/pdf/2603.14948 代码开源: https://github.com/TabGuigui/WorldDrive 作者团队: 澳门大学 × 阿法瑞智能 (Afari Intelligent Drive)\n💔 研究动机：被割裂的\u0026quot;导演\u0026quot;与\u0026quot;赛车手\u0026quot; 在当前的端到端自动驾驶（E2E-AD）领域中，存在一个巨大的鸿沟（Schism）：\n驾驶世界模型（DWMs） 像一位\u0026quot;导演\u0026quot;，能根据当前路况预测并生成未来的视频画面 但这位\u0026quot;导演\u0026quot;太关注如何把画面（视觉表征）画得逼真，完全不考虑如何把经验传授给真正负责开车的\u0026quot;赛车手\u0026quot;（运动规划器） 这就导致规划器无法继承世界模型的运动表征，大家各玩各的，无法协同。\n核心洞察 \u0026ldquo;能够用来生成未来画面的隐式特征（Latent Features），就应该直接用来决定未来的动作（Planning）！\u0026rdquo;\nWorldDrive 的核心哲学：通过**统一表征（Representation Unification）**搭建桥梁，让\u0026quot;生成\u0026quot;与\u0026quot;规划\u0026quot;无缝协同。\n🏗️ 整体架构：两阶段精密耦合 WorldDrive 将整个系统分成两大阶段：\n阶段一：场景生成（TA-DWM） ↓ 冻结编码器 阶段二：轨迹规划（Planner + FAR） 🛠️ 第一阶段：TA-DWM（轨迹感知驾驶世界模型） 核心目标 打造一个**\u0026ldquo;听指挥的魔法水晶球\u0026rdquo;**——能根据特定轨迹条件生成对应未来场景。\n三大精密齿轮 ⚙️ 齿轮一：3D Causal VAE（视觉表征提取） 技术细节：\n引入 3D 因果变分自编码器 将历史传感器观测数据（连续视频帧）进行时空压缩 提取浓缩的时空视觉隐式特征 \\(f\\) 生动理解： 这个特征 \\(f\\) 不是简单的图片拼凑，而是包含了\u0026quot;前车在减速\u0026quot;、\u0026ldquo;左边有行人在走动\u0026quot;等物理世界时空动态的\u0026quot;超级压缩包\u0026rdquo;。\n🤔 深度讨论：为什么必须用 VAE 而非 ResNet？ 问题：为什么要用 VAE 提取视觉特征，用 ResNet 是不是也行？\n如果只从\u0026quot;提取特征\u0026quot;这四个字来看，ResNet（残差网络）确实是老大哥，在传统的自动驾驶感知任务里（比如识别车道线、检测障碍物）用得非常多。\n但是，在 WorldDrive 这种\u0026quot;世界模型\u0026quot;的架构里，用普通的 ResNet 绝对不行！必须用 VAE（而且是 3D Causal VAE）。\n为什么？因为两者的\u0026quot;基因\u0026quot;完全不同——ResNet 是用来做**判别（Discriminative）的，而 VAE 是用来做生成（Generative）**的。我们用三个极具画面感的比喻来拆解原因：\n🚫 致命原因 1：ResNet 是\u0026quot;碎纸机\u0026quot;，VAE 是\u0026quot;压缩包\u0026quot; （能否还原画面的本质区别）\n如果用 ResNet：ResNet 是一种\u0026quot;单向通道（Encoder-only）\u0026quot;。它把高清图片输进去，一层层剥离细节，最后输出一堆极其抽象的特征向量（比如告诉你：这里有辆车、那里有个人）。这就好比把一份机密文件放进碎纸机，碎成纸屑后，你确实知道\u0026quot;这是一份文件\u0026quot;，但你永远无法把纸屑重新拼成原来那张写满字的纸。 为什么必须用 VAE：VAE 全称是\u0026quot;变分自编码器（Variational Autoencoder）\u0026quot;。它不仅有一个编码器（Encoder，压缩特征），还自带一个解码器（Decoder，还原特征）！世界模型（TA-DWM）的核心任务之一是**\u0026ldquo;生成未来的视频\u0026rdquo;**。模型在隐空间里推演完未来后，必须靠 VAE 的解码器把这些抽象的隐特征（Latent）重新解压缩、渲染成高清的 RGB 视频帧。用 ResNet？推演完未来之后，你就只能看着一堆数字干瞪眼，根本画不出视频来。 🎲 致命原因 2：扩散模型（DiT）有\u0026quot;严重的强迫症\u0026quot; （特征空间的平滑度与分布）\nResNet 的特征空间（千沟万壑）：ResNet 提取出的特征，在数学空间里是离散的、毫无分布规律的（只要能把猫和狗分开就行）。如果你把这种特征喂给扩散模型（Diffusion Model），扩散模型会彻底迷失，因为它无法在一个坑坑洼洼、没有边界的空间里稳定地\u0026quot;加噪\u0026quot;和\u0026quot;去噪\u0026quot;。 VAE 的特征空间（丝滑的平原）：VAE 里的\u0026quot;V\u0026quot;（Variational，变分）是它的灵魂。它在压缩特征时，会强行施加一个数学约束（通常是强制特征服从标准正态分布/高斯分布）。这相当于把杂乱无章的特征，整理成了一个平滑、连续、有规律的\u0026quot;多维高斯球体\u0026quot;。在这个完美的隐空间（Latent Space）里，TA-DiT 扩散模型才能极其顺滑地进行前向加噪和反向去噪。 💡 这就是目前所有顶流 AI 视频/生图模型（如 Sora、Stable Diffusion）全都必须依赖 VAE 的根本原因——Latent Diffusion 范式。\n⏳ 致命原因 3：时间维度的\u0026quot;因果律\u0026quot; （3D Causal VAE vs 2D ResNet）\nResNet 是\u0026quot;拍立得\u0026quot;（2D 空间）：传统的 ResNet 只能一张一张地处理单帧图片，它不懂什么叫\u0026quot;时间流逝\u0026quot;，更不懂\u0026quot;前一秒物体在哪，后一秒去了哪\u0026quot;。 3D Causal VAE 是\u0026quot;时空穿梭机\u0026quot;： 3D：它不仅在空间上（长宽）压缩图片，还在时间（Temporal）维度上压缩视频流！比如把 8 帧视频压缩成 2 个时间戳的特征，大幅降低了 DiT 预测未来的计算负担。 Causal（因果）：这是自动驾驶的保命机制。因果卷积保证了模型在提取第 \\(T\\) 秒的特征时，绝对不能偷看 \\(T+1\\) 秒的画面（不能违背因果律）。这就确保了特征是严格按照时间顺序演进的，能够无缝对接给自动驾驶的实时推理系统。 💡 总结一下 如果你只是想让车\u0026quot;看懂现在有什么\u0026quot;（纯感知/规划任务），用 ResNet 或类似的纯 Encoder 没毛病。\n但 WorldDrive 的野心是**\u0026ldquo;预演未来（生成视频）+ 指导开车（规划）\u0026rdquo;**。这就要求它的特征空间必须具备三大条件：\n能双向解压缩（为了画出视频） 绝对平滑、符合高斯分布（为了伺候好扩散模型） 包含时空动态且不破坏因果律（为了符合物理世界法则） 这三条，ResNet 一条都做不到，而 3D Causal VAE 完美契合。 这就是为什么它必须作为 WorldDrive 的第一道大门，把现实世界转化为\u0026quot;魔法水晶球\u0026quot;能看懂的高维隐空间密码！\n⚙️ 齿轮二：轨迹词表与双编码器（运动表征构建） 这是 TA-DWM 最核心的技术护城河！\n轨迹词表（Trajectory Vocabulary）：\n使用 K-Means 聚类对海量真实驾驶数据进行聚类 生成 256 个轨迹锚点（Anchors） 形成一本\u0026quot;256词标准动作新华字典\u0026quot; 双编码器设计：\nAnchor Encoder (\\(\\mathcal{E}_a\\))：把选中的标准动作（如\u0026quot;左转\u0026quot;）提取出基础特征 Offset Encoder (\\(\\mathcal{E}_o\\))：提取真实轨迹与标准轨迹的细微残差特征 特征融合： \\(c = \\mathcal{E}_a + \\mathcal{E}_o\\) 生动理解： 就像买西服，\\(\\mathcal{E}_a\\) 是挑\u0026quot;L码标准版\u0026quot;，\\(\\mathcal{E}_o\\) 是裁缝量体裁衣\u0026quot;袖子缩短1厘米\u0026quot;。\n⚙️ 齿轮三：TA-DiT（梦境生成器） 技术细节：\n基于 轨迹感知扩散 Transformer 架构 训练时加噪：将真实未来画面变成目标隐特征 \\(z_0\\)，加高斯噪声 去噪生成：在还原过程中，必须参考两个条件： 历史视觉特征包 \\(f\\) 意图字典特征 \\(c\\) 关键特性 - 运动敏感度（Motion Sensitivity）：\n模型具备\u0026quot;动作可控性\u0026quot; 输入的轨迹差别越大，生成的未来场景偏差越大 在 nuScenes 上达到 FID 12.8, FVD 131.7 🤔 深度讨论：为什么要用 Anchor + Offset？ 问题：为什么不直接编码 GT 轨迹？ 直接编码 Ground Truth 连续坐标会引发几场\u0026quot;算法灾难\u0026quot;：\n🚨 痛点 1：致命的\u0026quot;均值回归\u0026quot;（Regression to the Mean） 场景：十字路口，既可以左转也可以直行\n直接回归：模型会算出左转和直行的平均值——结果撞向路口中间的隔离带！ Anchor + Offset：问题变成\u0026quot;分类（选大方向）+ 局部回归（微调）\u0026quot; 输出两个概率：\u0026ldquo;60%选左转词条，40%选直行词条\u0026rdquo; 完美保留多模态性 📉 痛点 2：大幅降低学习难度 Coarse-to-Fine 策略：\n连续坐标搜索空间无限大且非线性 Anchor 把无限回归问题拆解成有边界的分类问题 + 小范围回归问题 极大提升收敛速度和特征提取稳定性 🎲 痛点 3：方便规划器\u0026quot;广撒网\u0026quot; 直接编码：纯回归模型只能输出唯一一条确定性轨迹 Anchor + Offset：可瞬间对 256 个锚点打分，输出 Top-K 候选轨迹 后续 FAR 对这 K 条路线打分，选出最优解 🎨 痛点 4：给扩散模型提供稳定条件信号 Anchor 本质是 Token（标记） 把连续物理轨迹转换成 256 个类似 NLP 的 Text Tokens DiT 处理 Token 得心应手，当看到\u0026quot;Anchor #42\u0026quot;时立刻知道\u0026quot;这是标准左转\u0026quot; 💥 训练与推理分布偏移问题 如果训练时直接编码 GT，推理时用可学习 Query 生成多模态轨迹：\n推理时预测的轨迹带有微小瑕疵 连续空间中，微小变化会导致编码器输出巨大偏移 世界模型拿到从未见过的\u0026quot;异形特征\u0026quot;，导致 OOD 失败 Anchor 的反杀：\n自带**\u0026ldquo;特征量化\u0026rdquo;和\u0026ldquo;容错纠偏\u0026rdquo;**能力 只要大方向没错，激活的永远是同一个 Anchor 特征 微小抖动只扔给 Offset 处理 🧭 第二阶段：Multi-modal Trajectory Planner 核心战略：完美\u0026quot;白嫖\u0026quot;冻结特征 规划器直接拿来第一阶段训练好的组件（完全冻结）：\n时空视觉编码器 \\(\\mathcal{E}_{vis}\\)：提取环境画面特征 \\(f\\) 轨迹编码器 \\(\\mathcal{E}_{traj}\\)：提取动作特征 \\(c\\) 为什么冻结？ 这些编码器为了逼真生成未来视频已被极限压榨，特征包含极其丰富的\u0026quot;物理规律、深度、动态变化甚至被遮挡物体的运动趋势\u0026quot;。\n思考与决策流程 Step 1: 自车状态查询（Ego Queries） 收集当前\u0026quot;身体状态\u0026quot;（速度、加速度、转向角等），通过 MLP 投射成 Ego Queries\nStep 2: Transformer 解码器交互 Ego Queries 与冻结视觉特征 \\(f\\) 进行交叉注意力交互\n\u0026ldquo;左边那辆车是不是要加塞？\u0026rdquo; \u0026ldquo;前方红绿灯是不是快变了？\u0026rdquo; 形成**\u0026ldquo;感知上下文\u0026rdquo;** Step 3: 多模态输出 对 256 个 Anchor 并行输出概率得分 预测每个 Anchor 对应的 Offset 筛选出 Top-K 候选轨迹 🕷️ 终极杀招：FAR（未来感知奖励器） 核心问题 扩散模型太慢！为 6 条候选路线渲染 6 段高清视频，车早就撞了。\n解决方案：面向规划的蒸馏机制 🎓 Step 1: 拜师学艺 冻结世界模型作为\u0026quot;导师\u0026quot; 导师能在隐空间推演物理演变，生成**\u0026ldquo;未来隐特征\u0026rdquo;** 🧲 Step 2: 特征对齐与蒸馏 FAR 设置一组可学习的 Future Scene Queries 计算学生 Query 特征与导师 Future Latents 的差异 逼迫学生对齐导师特征 形成条件反射般的直觉 ⚡ Step 3: 实战推理 世界模型（扩散生成）直接关掉！ 候选轨迹特征直接查询蒸馏出的未来场景特征 轻量 MLP 输出 Reward 分数 核心优势 极低延迟：绕过沉重视频生成，毫秒级响应 打破\u0026quot;盲人摸象\u0026quot;：站在\u0026quot;未来已经发生的视角\u0026quot;打分 🔮 FAR 如何处理 6 条候选轨迹？ 是的，FAR 确确实实预测了 6 种不同的未来！\nQuery 的\u0026quot;多重影分身\u0026quot; 可学习 Query 是一组固定维度的高维向量（\u0026ldquo;空白的未来画布\u0026rdquo;） 当有 6 条候选轨迹时，Query 瞬间复制成 6 个 Batch 各自领到专属的\u0026quot;动作剧本\u0026quot;（轨迹特征 \\(c_i\\)） 交叉注意力\u0026quot;渲染\u0026quot;未来 Q：可学习 Query K, V：视觉环境特征 + 轨迹特征的结合体 6 组 Query 问出不同问题，被染成不同颜色 成功预测 6 种未来 为什么不会卡死？ FAR 预测的 6 种未来全都是数学向量，不需要渲染成视频帧 GPU 同时算 6 个向量仅是矩阵乘法区别，耗时几毫秒！ 终局判卷 6 份预演好的隐空间特征 轻量 MLP 读取并输出 6 个打分 选分数最高的轨迹执行 🏆 实验结果 规划性能 在 NAVSIM、NAVSIM-v2、nuScenes 三个基准测试上：\n在仅使用视觉输入的方法中取得领导地位的规划性能 Vision-only SOTA 视频生成 保持高保真、受动作控制的视频生成能力 FID 12.8, FVD 131.7 (nuScenes) 💡 核心贡献总结 表征统一：让世界模型的\u0026quot;想象力\u0026quot;与规划器的\u0026quot;行动力\u0026quot;真正知行合一 Anchor + Offset 范式： 解决多模态均值回归问题 提供稳定的条件信号给扩散模型 天然具备容错纠偏能力 冻结编码器继承：规划器直接站在世界模型肩膀上 FAR 蒸馏机制：把\u0026quot;深思熟虑\u0026quot;的物理规律变成\u0026quot;脱口而出\u0026quot;的肌肉记忆 🔗 相关链接 [[E2E-AD 端到端自动驾驶概述]] [[Diffusion Model 在自动驾驶中的应用]] [[World Model 世界模型]] 一句话总结：WorldDrive 巧妙地用\u0026quot;统一表征\u0026quot;搭建了一座桥梁，让能预知未来的世界模型真正成为了规划器的最强辅助，开启了端到端自动驾驶\u0026quot;生成与规划\u0026quot;无缝协同的新篇章！\n","date":"2026-03-24T00:00:00+08:00","permalink":"/post/robotics/e2e/world-drive/","title":"WorldDrive"},{"content":" 一、这篇论文在讲什么？ 核心问题 自动驾驶规划面临一个经典的两难困境：多样性 vs 质量\n太保守：只会\u0026quot;死记硬背\u0026quot;人类司机的走法，遇到新情况就傻眼 太发散：想象力太丰富，画出各种会撞车的方案 DiffusionDriveV2 的答案 用扩散模型生成多条候选路径（多样性），再用强化学习像严厉的教练一样筛选（质量）\n形象的比喻：射箭比赛\n以前的模型：只盯着靶心射一箭，射偏了就没招了 DiffusionDriveV2：一次性朝靶心周围射出一把箭（多样性），然后剪掉那些射到观众席的箭（质量约束），最后选出最准的一支 二、核心方法：三招绝学 第一招：尺度自适应乘法噪声 —— 让\u0026quot;脑洞\u0026quot;更顺滑 以前的做法（加法噪声）：在路径上乱加干扰，结果路径变得像毛刺一样，车开起来会剧烈抖动\nV2 的创新： $$\\tau\u0026rsquo; = (1 + \\epsilon_{mul})\\tau$$\n生动理解：\n近处（车头）：抖动小，因为车头方向需要精确控制 远处（目标点）：抖动大，因为远处的路径本身就有不确定性 这符合开车时\u0026quot;微调方向、远方模糊\u0026quot;的真实物理规律。\n第二招：Intra-Anchor GRPO —— 窝里斗，选优胜者 核心逻辑：不比绝对高低，只比相对好坏\n流程：\n针对同一个意图模板（比如\u0026quot;左转\u0026quot;），生成 8 条略有不同的路径 裁判给这 8 条路径打分（是否撞车、是否开出马路、乘客稳不稳） 计算\u0026quot;相对优势\u0026quot;：$A = \\frac{\\text{你的分数} - \\text{平均分}}{\\text{差异度}}$ 表现比平均好的，优势值为正；比平均差的，为负 为什么要\u0026quot;组内\u0026quot;比？\n\u0026ldquo;左转\u0026quot;和\u0026quot;直行\u0026quot;的分数没有可比性。左转天生就比直行难。如果放在一起比，模型可能会为了拿高分而只学直行（模式崩溃）。\n临摹字帖的比喻：\n想象你在练书法，桌上有 64 张不同的字帖（Anchors）：一张是\u0026quot;一\u0026quot;字，一张是\u0026quot;之\u0026quot;字，一张是\u0026quot;永\u0026quot;字……\n生成过程：你拿一张\u0026quot;永\u0026quot;字的字帖，临摹了 8 遍。虽然每遍写得都有点细微差别（加了随机噪声），但因为你都是照着\u0026quot;永\u0026quot;字写的，这 8 张纸就被归为 \u0026ldquo;永字组\u0026rdquo; 组内 GRPO：老师（Reward）过来看这 8 张\u0026quot;永\u0026quot;字，选出写得最漂亮的一张，给你奖励 拒绝跨组比较：老师不会拿你写的\u0026quot;永\u0026quot;字去和隔壁桌写的\u0026quot;一\u0026quot;字比谁更漂亮，因为它们根本不是一个字，没法比 第三招：Inter-Anchor Truncated GRPO —— 守底线，撞车必罚 虽然\u0026quot;窝里斗\u0026quot;能选出最好的左转方案，但如果所有的左转方案都会撞上护栏怎么办？\n这一招建立了全局底线：不管你是哪种意图，只要撞车，通通判死刑（给极低的负分）。\n三、Mode Selector：最后拍板的那个人 Intra-Anchor GRPO 确保了每个意图都能产生高质量的路径，但车只能选一条路走。谁来拍板？\nMode Selector 就是那个\u0026quot;主裁判\u0026rdquo;，它的工作是：\n1. 怎么选？—— 综合多维度打分 结合三类关键信息进行\u0026quot;面试\u0026quot;：\n路径本身：弯曲程度如何？是否平滑？ 环境背景（BEV Features）：通过交叉注意力，看看路附近有没有障碍物、马路牙子或红绿灯 任务目标：导航让你左转，那左转组的路径天生得到更高的\u0026quot;意图分\u0026quot; 2. 技术实现 MLP 打分：经过特征融合后，通过 MLP 给每条路径打分 粗选+精选（Coarse-to-fine）：先快看一眼，淘汰明显不行的；剩下的再仔细对比 3. 训练标准 BCE Loss（对错分）：告诉它哪条路最接近人类司机的真值 Margin-Rank Loss（排序分）：不要求算出绝对好坏分，而是要求能排对顺序 如果没有这个 Mode Selector，光有 GRPO，车子可能会在\u0026quot;左转\u0026quot;和\u0026quot;直行\u0026quot;之间反复横跳，不知道该听谁的。\n四、GRPO vs DPO：两种\u0026quot;教导方式\u0026quot;的对比 特性 DPO (直接偏好优化) GRPO (组相对策略优化) 数据形式 成对数据（A \u0026gt; B） 组数据（A, B, C, D\u0026hellip;） 学习方式 离线学习 在线探索 核心逻辑 隐式奖励 显式奖励 比喻 看录像带学习 队内选拔赛 为什么论文选 GRPO 而不是 DPO？\n解决模式崩溃：GRPO 通过组内归一化，强制保留每个意图的独特性 不需要 Critic 网络：GRPO 利用\u0026quot;组平均分\u0026quot;代替了评论员，更简单、更省内存 动态进化：在扩散模型复杂的生成空间里，GRPO 能让模型不断发现\u0026quot;原来这样走比刚才那样走更好\u0026quot; 五、Reward 函数：交规考试式的打分 GRPO 的 Reward 是人工定义的规则计算的，不需要神经网络：\n具体规则 指标 含义 判定方式 NC (No Collision) 碰撞惩罚 如果撞到车/行人/护栏，给巨大负分 DAC (Drivable Area Compliance) 合规性 车轮是否压到马路牙子、是否逆行 EP (Ego Progress) 进度得分 鼓励往前走，走得越远且符合限速，得分越高 C (Comfort) 舒适度 路径平滑度、加速度和急转弯程度 TTC (Time To Collision) 时间到碰撞 离前车的距离和相对速度是否安全 为什么不用神经网络打分？ 客观标准明确：自动驾驶\u0026quot;撞没撞车\u0026quot;是物理事实，用规则算比用模型猜更准确、可靠 计算速度快：规则计算只是几行物理公式，不需要运行庞大的神经网络 避开\u0026quot;幻觉\u0026quot;：AI 模型可能产生幻觉觉得撞车也挺好，但物理规则是铁律 六、技术演进：三代扩散模型的对比 特性 DiffusionPlanner DiffusionDrive V1 DiffusionDriveV2 起始状态 纯随机噪声（从 0 到 1 创造） 预定义锚点（从半成品加工） 锚点 + 尺度自适应噪声 去噪步数 多步（通常 10+ 步，慢） 极少步（1-2 步，快） 极少步（1-2 步，快） 多样性保证 扩散模型天然属性（不稳定） 64 个锚点强制分区（稳定） 锚点分区 + GRPO 保护意图 学习范式 模仿学习 (IL) 模仿学习 (IL) 模仿学习 + 强化学习 (RL) 存在痛点 算得慢、容易模式崩溃 容易产生会撞车的低质轨迹 系统复杂度高（训练难） 生动理解三代的区别 A. 关于\u0026quot;噪声\u0026quot;的艺术\nDiffusionPlanner：在沙堆里找金子，范围太大，容易迷失 DiffusionDrive：给沙堆围了 64 个小栅栏，让你在栅栏里找，效率高了，但栅栏里可能混进了石头 DiffusionDriveV2：不仅有栅栏，还发明了\u0026quot;乘法噪声\u0026quot;，近处抖动小，远处抖动大，符合开车物理规律 B. 关于\u0026quot;教导方式\u0026quot;的变革\n前两代：看人画画，只知道老师没往墙上撞，不知道为什么不撞 V2 时代：引入\u0026quot;警察罚款\u0026quot;，如果你画到墙上，GRPO 教练会狠狠扣分 七、架构拓扑：从模糊到精确 DiffusionPlanner：模糊的一段式（Monolithic） 传感器数据 → 特征提取 → 扩散解码器 像一个\u0026quot;黑盒\u0026quot;，缺乏中间变量，人类很难理解它为什么要这么开。\nDiffusionDrive V1：清晰的二段式（Perception-then-Plan） 传感器 → BEV 特征图 + 目标检测 → 锚点扩散 有了明确的分工。先通过感知算法把世界变成\u0026quot;鸟瞰图\u0026quot;，规划器再在这张图上\u0026quot;画画\u0026quot;。\nDiffusionDriveV2：精密的\u0026quot;多级反馈\u0026quot;二段式 传感器 → 高精度 BEV → 组内并行扩散（GRPO） → 模式选择 在 V1 基础上增加了\u0026quot;选优级\u0026quot;，形成了\u0026quot;感知 → 粗规划 → 精规划\u0026quot;的三级跳。\n八、传感器融合：如何构建 BEV？ 特性 DiffusionPlanner DiffusionDrive V1 DiffusionDriveV2 输入源 单视角或视角特征拼接 多相机环视（6 颗摄像头） 相机 + LiDAR（多模态对齐） 特征空间 图像空间 BEV 空间（LSS 方案） 时序对齐 BEV（ResNet-34） 融合方式 简单的特征拼接 空间投影 时空注意力融合 V2 的技术细节 骨干网络：对齐的 ResNet-34，通过 LSS（Lift-Splat-Shoot）把 6 颗摄像头的图像\u0026quot;拍扁\u0026quot;到地面 时序对齐：把过去 2 秒的 BEV 特征根据自车运动进行\u0026quot;空间平移\u0026quot;，让不同时间点的特征在同一个坐标系下重叠 九、规划逻辑的信息流 三重交互机制 DCA（全景扫描）：轨迹看 BEV 特征图，快速定位周围是大街还是小巷 Agent-Wise Attention：轨迹与 50 个障碍物 Query 进行 1 对 1 谈话，进行厘米级碰撞检测 Map-Wise Attention：轨迹与车道线向量对齐，确保遵守交通规则 生动比喻：\nBEV 交互是让你\u0026quot;别撞墙\u0026quot; Map 交互是让你\u0026quot;守交规\u0026quot; 十、前后帧一致性：如何拒绝\u0026quot;精神分裂\u0026quot;？ 1. 时序特征融合 输入包含过去几帧（比如过去 2 秒）的图像或点云，通过 Temporal Attention 把过去的特征\u0026quot;存\u0026quot;在当前特征向量里。\n比喻：你开车时，脑子里其实存着前 2 秒路口的样子，你看的是一部\u0026quot;连贯的电影\u0026quot;。\n2. Anchors 作为\u0026quot;定海神针\u0026quot; 64 个锚点（直行、左转等模板）是固定不变的。每一帧都从相同的锚点出发，起步点不乱跳。\n3. 截断扩散的功劳 推理时只走 2 步（甚至 1 步），步数越少，生成过程就越接近确定性逻辑。\n4. GRPO 带来的\u0026quot;决断力\u0026quot; 通过组内对比，极大拉开了\u0026quot;好路径\u0026quot;和\u0026quot;坏路径\u0026quot;的分数差距，让决策变得非常\u0026quot;笃定\u0026quot;。\n十一、实验结果 在自动驾驶界最有名的考场 NAVSIM 进行考试：\nPDMS 分数：91.2，刷新世界纪录 骨干网络：即使使用较小的 ResNet-34，表现也超过了拥有巨大骨干网络的模型 十二、关键技术实现：Multi-Head Attention 为什么要用多头？ 单头注意力：像一把手电筒，盯着行人看，可能就没注意到红绿灯\n多头注意力：像一个专家顾问团\n1号头（安全专家）：盯着障碍物和距离 2号头（导航专家）：盯着车道线和路标 3号头（舒适专家）：盯着路面平整度和坡度 核心代码逻辑 # 多头拆分：把 [batch, seq_len, d_model] 变成 [batch, num_heads, seq_len, head_dim] Q = Q.view(batch_size, -1, num_heads, head_dim).transpose(1, 2) # 计算注意力分数 scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k) # 为什么除以 sqrt(d_k)？ # 防止分数值过大，导致 Softmax 进入饱和区（梯度消失） # 就像\u0026#34;音量调节旋钮\u0026#34;，让模型保持\u0026#34;理智\u0026#34; 十三、个人思考与疑问 值得学习的亮点 锚点机制：把扩散模型的\u0026quot;混沌\u0026quot;约束在 64 个意图分区里，既保证多样性又控制计算量 GRPO 的巧妙应用：借用 DeepSeek-R1 的技术，解决了模仿学习\u0026quot;不识好歹\u0026quot;的问题 乘法噪声：一个看似微小的数学改变，却让轨迹在物理平滑度上直接降维打击前代 待探索的问题 GRPO 的组大小如何选择？8 条还是 16 条？对性能和效率的影响如何？ Mode Selector 是否可以引入时序记忆，进一步稳定决策？ 如何处理极端场景（如突然出现的障碍物）？ 参考链接 论文原文：DiffusionDriveV2 相关工作：DiffusionPlanner, DiffusionDriveV1, DeepSeek-R1 (GRPO) 本笔记基于 AI 辅助的论文讨论整理而成，保留了讨论中的生动比喻和技术细节。\n","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/diffusion-drive-v2/","title":"DiffusionDriveV2: Truncated Diffusion Model for End-to-End Autonomous Driving"},{"content":"核心卖点：扔掉昂贵的3D标注包袱，用无监督/自监督大法实现端到端驾驶 🛑 痛点：被\u0026quot;模块化\u0026quot;和\u0026quot;高价标注\u0026quot;绑架的自动驾驶 在聊 UAD 之前，咱们先看看现有的端到端自动驾驶老大哥们（比如 UniAD）。虽然它们号称\u0026quot;端到端\u0026quot;，但骨子里还是在模仿传统流水线，设计了层层递进的 感知 → 预测 → 规划 子任务。\n这种设计的硬伤极其明显：\n疯狂烧钱的\u0026quot;数据紧箍咒\u0026quot;：为了训练感知和预测模块，你需要海量、极其精确的 3D 框（3D Bounding Boxes） 手工标注数据。这不仅是烧钱，更是限制模型规模扩展的致命瓶颈。 沉重的\u0026quot;显卡粉碎机\u0026quot;：一堆复杂的子网络堆叠在一起，导致模型在训练和推理时的计算开销极大，跑起来像背着沙袋跑步。 🚀 破局者 UAD：扔掉包袱，无监督/自监督大法好！ UAD 犹如一个轻装上阵的武林高手，它认为：既然规划（开好车）才是最终目的，何必纠结于完美的 3D 标注呢？ 于是，它直接抛弃了传统的监督式模块，用两个极具创意的\u0026quot;独门绝技\u0026quot;打通了从视觉输入到控制信号的任督二脉。\n🗡️ 绝技一：角度感知前置任务（Angular Perception Pretext） 核心思想：\u0026ldquo;切披萨\u0026quot;感知法 既然不给 3D 标注数据，模型怎么理解周围的世界呢？UAD 设计了一个非常巧妙的无监督前置任务。\n1. 空间表征学习（BEV 切披萨） 模型不去做精细的 3D 框检测了，而是把车辆周围的鸟瞰图（BEV）空间像切披萨一样，划分成多个 扇形区域（Angular Blocks）。\n为什么要分扇区？两大绝妙好处：\n极度压缩信息（省算力）：同一条光线（同一个扇区）上的信息被压缩进了一个特征向量（Angular Query）里。对于规划模块来说，它不需要知道障碍物是在这个扇区里精确到哪怕一厘米的位置，它只需要知道\u0026quot;这个方向有东西，别往那开\u0026quot;就足够了！ 契合相机视角：扇形的夹角天然对应了相机的视野角度（FOV），这为后面从 2D 直接白嫖标签打下了地基。 模型引入了一系列 角度查询向量（Angular Queries），每个查询专门负责盯着一个扇区，去预测这个扇区里\u0026quot;有没有障碍物\u0026rdquo;（即 Objectness，物体存在性）。\n2. 白嫖 2D 伪标签（借力打力） 没标注怎么训练物体存在性？作者机智地利用了现成的开源 2D 开放词汇目标检测大模型（比如 GroundingDINO）。\n极简投影术：角度对角度的\u0026quot;连连看\u0026quot;：\n传统把 2D 投影到 3D BEV 空间，最头疼的就是\u0026quot;深度（Depth）\u0026ldquo;算不准。UAD 巧妙地绕开了这个问题！\n一张 2D 图像的宽度，其实对应的就是一个固定的水平视野夹角 如果在 2D 图像的某个位置有一个边界框，这个框的左右边界，刚好对应了以自车为中心的一段特定夹角的射线 UAD 直接把这个 2D 框覆盖的水平角度范围，映射到 BEV 空间的对应扇形区域 在这个扇形范围内，模型就被打上 1 的标签（有物体），其他区域则是 0（空旷） 这个投影不需要猜测物体有多远，只看方向，非常稳！\n3. 什么算\u0026quot;有东西\u0026rdquo;？全靠 GroundingDINO 的\u0026quot;咒语\u0026quot; 这里的\u0026quot;有东西（Objectness）\u0026quot;，实际上是由你输入给 GroundingDINO 的文本提示词（Prompt）决定的：\n在自动驾驶中，作者通常会输入像 \u0026ldquo;car, pedestrian, bicycle, truck, bus, obstacle\u0026rdquo; 等类别 树和指示牌算吗？ 如果这些东西在马路边不影响行驶，通常不作为主要检测目标；但如果一棵倒塌的树横在路上，只要你的提示词里包含了\u0026quot;障碍物\u0026quot;或者泛化的词汇，GroundingDINO 就能框出它，相应的扇区就会被标记为 1 只要是规划需要避让的，都可以低成本地\u0026quot;喂\u0026quot;给大模型去生成标签 一分钱 3D 标注不花，就把空间感知做完了！\n4. 时序梦境解码器（Angular-wise Dreaming Decoder） 光看现在的静态画面不行，还得懂未来。UAD 强制模型去预测 未来不可见的状态。\n🧠 它在干什么？\nDreaming Decoder 的本质是一个 自回归的隐空间世界模型。\n假设我们需要规划未来 T 步的轨迹，解码器就包含 T 层。它会结合自车未来的驾驶意图，利用 GRU（门控循环单元），一步接一步地\u0026quot;脑补\u0026quot;未来 T 帧时，各个扇区里的特征状态会变成什么样。\n⚖️ 怎么监督未来的\u0026quot;梦\u0026quot;？（对答案机制）\n既然没有未来的 3D 标注框，怎么知道它\u0026quot;梦\u0026quot;得对不对？这就用到了世界模型中经典的 先验 vs 后验 博弈机制：\n\u0026ldquo;闭眼猜\u0026rdquo;（先验分布 Prior）：模型只看现在的图像，然后结合自车动作，推测下一秒的环境特征分布 \u0026ldquo;睁眼看\u0026rdquo;（后验分布 Posterior）：在训练阶段，模型其实是可以\u0026quot;作弊\u0026quot;看到下一秒的真实图像的！它把下一秒的真实图像输进去，提取出一个真实的环境特征分布 \u0026ldquo;对答案\u0026rdquo;（Dreaming Loss / KL 散度）：接下来，系统强迫\u0026quot;闭眼猜\u0026quot;的结果，去无限逼近\u0026quot;睁眼看\u0026quot;的真实结果 通过不断计算这两个特征分布的差异，模型被逼着学会了物理世界的运行规律（比如前车踩刹车了，它的特征在未来一秒会怎么变化）。全程不需要人类画一个框，模型自己通过\u0026quot;梦境对比\u0026quot;学会了预判未来！\n5. 跨区与运动关联：靠\u0026quot;脑补\u0026quot;机制全局掌控 如果一个行人上一帧在 A 扇区，下一帧走到了 B 扇区，UAD 怎么跟踪他？\n关键在于：UAD 并不是在做传统的\u0026quot;目标跟踪\u0026quot;（不需要给行人打个 ID），而是把整个场景当作一个流动的特征池。\n每一个扇区都有一个专属的 角度查询向量。在时间流转中，所有的 Query 都会通过 Transformer 的交叉注意力机制去全局扫描 BEV 空间，并且输入到后续的 Dreaming Decoder（包含时间记忆单元，如 GRU） 中。\n模型记住的不是\u0026quot;一个具体的行人\u0026quot;，而是\u0026quot;特征在相邻扇区之间的动态转移\u0026quot;。只要这个障碍物的特征从 A 扇区\u0026quot;流\u0026quot;到了 B 扇区，负责 B 扇区的 Query 就会立刻捕捉到这个变化并拉响警报。\n🛡️ 绝技二：方向感知规划与自监督一致性 感知搞定了，接下来就是关键的 路径规划。开车最怕什么？转向的时候画龙、不稳！\n1. 方向感知学习（Direction Prediction） 模型会先预测自车打算去哪（左转、直行还是右转），并设置方向阈值。明确了宏观意图后，再进行微观的轨迹生成，极大地增强了车辆在十字路口等复杂转向场景下的决策能力。\n2. 自监督轨迹一致性 —— 稳如老狗的\u0026quot;左右互搏术\u0026quot; ⚠️ 关键澄清：不是\u0026quot;裁剪缩放\u0026quot;，而是\u0026quot;空间旋转\u0026quot;！\n如果真的用大尺度的\u0026quot;裁剪\u0026quot;或\u0026quot;缩放\u0026quot;，把核心的车道线、红绿灯或者前车给\u0026quot;裁\u0026quot;掉了，那根本就是一个\u0026quot;无解\u0026quot;的问题。UAD 巧妙地避开了这个死胡同。\n具体是怎么操作的呢？\n原视角预测：首先，模型看着当前的正常画面，在 BEV 空间里规划出了一条原始的轨迹（$P_{orig}$） \u0026ldquo;转动脖子\u0026quot;的增强：接着，作者在特征层面，把整个 BEV 空间的特征旋转一个角度 θ（比如向左旋转 15 度、向右旋转 10 度等）。这相当于模拟了自车在当前位置，车头稍微偏左或偏右的状态。注意：在这个过程中，所有的道路、车辆信息都在，只是相对于自车的坐标系转了一个角度，没有任何核心信息被丢失！ 旋转视角的预测：模型看着这个被旋转过的 BEV 特征，再次进行规划，得出一个新的轨迹（$P_{rotated}$） 左右互搏的精髓：转回去对答案\n真正的\u0026quot;自监督一致性\u0026quot;是在这一步完成的：如果模型真的懂驾驶物理学，那么它在\u0026quot;旋转后视角\u0026quot;规划出的轨迹 $P_{rotated}$，只要在数学上做个简单的逆向旋转，就应该和最初的原始轨迹 $P_{orig}$ 完美重合！\n$$Loss = \\text{差距}( \\text{逆旋转}(P_{rotated}) , P_{orig} )$$\n为什么这种\u0026quot;旋转一致性\u0026quot;这么牛？\n信息零损耗：因为只是坐标系的旋转，马路还是那条马路，障碍物还是那个障碍物，规划条件是绝对充足的 专治\u0026quot;画龙\u0026quot;和\u0026quot;方向盘不稳\u0026rdquo;：现实开车中，很多端到端模型在过十字路口时，车头稍微一偏，模型就以为到了一个新场景，规划出的轨迹就会突变，导致车辆在路口\u0026quot;画龙\u0026quot;。通过这种旋转一致性训练，模型被逼着学会了：不管车头当前偏了多少度，我的宏观行驶轨迹必须死死锚定在那里，不能动摇！ 🕰️ 记忆模块：UAD 不是单帧规划器 过去的记忆：BEVFormer 风格的\u0026quot;流式时序融合\u0026quot; UAD 并没有把前几帧的图像原封不动地存下来（那样太吃显存了），而是把记忆存在了 特征空间（BEV 空间） 里。\n站在巨人的肩膀上：UAD 在图像特征转鸟瞰图这一步，沿用了自动驾驶经典大作 BEVFormer 的时序架构 流式记忆传递：当系统处理当前帧（第 T 帧）时，它不仅看当前的摄像头图像，还会把上一帧已经计算好的 BEV 特征图拿过来 时序自注意力：模型会通过注意力机制，将上一帧的 BEV 特征与当前帧的特征进行对齐和融合 效果：通过这种帧传帧的\u0026quot;接力赛\u0026quot;，当前帧的 BEV 特征里自然就蕴含了过去几秒的动态信息 未来的推演：Dreaming Decoder 里的\u0026quot;GRU 记忆单元\u0026quot; 梦境解码器的底层核心就是一个经典的 时间序列记忆模块——GRU：\n隐状态即记忆：在预测未来轨迹时，GRU 维护着一个包含环境动态的隐状态 Q 步步推演：当它去预测未来第 1 秒、第 2 秒、第 3 秒的状态时，GRU 会把第 1 秒预测完毕后的隐状态传递给第 2 秒的预测过程 脑补物理规律：通过 GRU 这种自带记忆和遗忘机制的循环网络，UAD 能够确保它规划出的轨迹和预测的环境变化在时间维度上是连贯且符合物理学常识的 ⚠️ 局限性：1.5秒魔咒 历史帧数量 UAD 以及它对标的 UniAD 这种端到端模型，通常只融合 3 帧 的历史 BEV 特征（加上当前帧一共 4 帧）。在 nuScenes 数据集的标准采样率（2Hz，即每 0.5 秒一帧）下，它的\u0026quot;有效记忆窗口\u0026quot;只有短短的 1.5 秒！\n现实问题：可变车道场景 问题场景：5秒前能看到空中可变车道指示牌，开过去之后该怎么办？\n如果指示牌在 5 秒前从相机的视野上方消失了，而模型只有 1.5 秒的 BEV 记忆：\n结论：它真的会变成\u0026quot;失忆症患者\u0026quot;！由于历史特征池里已经彻底把 5 秒前那个含有指示牌的 BEV 帧\u0026quot;挤压丢弃\u0026quot;了，UAD 在当前帧完全不知道身下的车道到底是直行还是左转。\n为什么不强行融合过去 5 秒？ 你可能会想，把历史帧数改成 10 帧不就行了吗？在工程上，这是一场灾难：\n💥 显卡原地爆炸：BEV 特征是一个极其庞大的高维张量。别说 10 帧，很多模型在训练时堆 4 帧，24G 显存的 RTX 3090 就已经塞满了 🌀 坐标系对齐崩溃：历史 BEV 特征要融合到当前帧，必须根据车辆的运动轨迹进行空间旋转和平移对齐。车在 5 秒内可能已经开出了 60 米，累积的里程计误差会让特征图\u0026quot;糊成一团\u0026quot; 现实中的破局之道 导航指令：即使是 UAD，在规划轨迹时也不是无头苍蝇，它会接收宏观的导航指令 降维记忆法：让模型先把信息提取成一个轻量级的文本标签或矢量坐标，存到长时记忆库里 妥协：轻地图辅助：在实际量产中，大部分车企依然会依赖标准导航地图的先验拓扑信息作为兜底 🏆 战绩结算：数据说话 指标 成绩 开环测试（nuScenes） 最佳开环评估表现 平均碰撞率 比 UniAD 相对降低 38.7% 闭环测试（CARLA） Town05 Long 基准 路网完成度 98.5% 驾驶得分 比 VAD 高 41.32分 训练资源 仅消耗 UniAD 的 44.3% 推理速度 提升 3.4倍 🏁 总结 UAD 这篇论文犹如给端到端自动驾驶做了一次完美的\u0026quot;断舍离\u0026quot;：\n✅ 一把扔掉了昂贵的 3D 标注包袱 ✅ 砍掉了冗余沉重的模块化预测塔 ✅ 用\u0026quot;切披萨\u0026quot;式的角度感知前置任务实现无监督空间感知 ✅ 用自监督的一致性规划保证转向稳定 ⚠️ 但在时间维度上的长效记忆依然受限于 BEV 时序架构的通病 核心结论：不烧钱堆 3D 标注，靠聪明的无监督架构设计，照样能把车开得又快、又稳、又安全！这绝对是通往\u0026quot;大规模、低成本、强泛化\u0026quot;自动驾驶大模型的一条硬核新路。\n相关论文 [[UniAD]] - 端到端自动驾驶标杆 [[BEVFormer]] - BEV 时序融合基础架构 [[VAD]] - 向量化端到端驾驶 [[LAW - Latent World Model for E2E Driving]] - 隐空间世界模型 [[World4Drive - 无需感知标注的端到端世界模型]] - 世界模型方法 ","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/uad/","title":"End-to-End Autonomous Driving without Costly Modularization and 3D Manual Annotation"},{"content":" 端到端自动驾驶的视频生成与轨迹规划\n🎯 一句话概括 Epona 是一个自回归扩散世界模型，它像拍连续剧一样根据历史画面预测未来，同时用扩散模型保证每一帧画质高清——不仅能\u0026quot;脑补\u0026quot;出未来 2 分钟的驾驶场景，还能学会\u0026quot;红灯停、避让行人\u0026quot;等物理规则。\n🧠 核心设计理念 为什么需要 Epona？ 在自动驾驶领域，存在两类模型各有优劣：\n模型类型 优势 劣势 扩散模型 画质逼真、细节丰富 短视，难以生成长视频，不懂数理逻辑 自回归 Transformer 懂因果、能长程推理 图像压缩粗糙，画质模糊 Epona 的思路：为什么不能兼得？于是采用 \u0026ldquo;自回归 + 扩散\u0026rdquo; 混合架构：\n像写连续剧一样（自回归）根据历史预测未来 同时用扩散模型保证每一帧画质高清 三大核心创新 分工明确：时空处理分离，效率大幅提升 异步生成：轨迹规划和视频生成分开进行 连锁前向训练：解决误差累积问题，能生成长达 2 分钟的视频 🏗️ 架构详解 Epona 由三个核心模块组成，像一个精密配合的团队：\n┌─────────────────────────────────────────────────────────────┐ │ Epona 架构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 历史 T 帧 ──┐ │ │ │ ┌─────────┐ │ │ 历史动作 ────┼───►│ MST │──► 特征 F │ │ │ │(记忆大师)│ │ │ │ ┘ └─────────┘ │ │ │ │ │ │ ┌──────────┴──────────┐ │ │ ▼ ▼ │ │ ┌──────────┐ ┌──────────┐ │ │ │ TrajDiT │ │ VisDiT │ │ │ │(领航员) │ │ (画师) │ │ │ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ ▼ ▼ │ │ 未来轨迹 下一帧画面 │ │ │ └─────────────────────────────────────────────────────────────┘ 📚 2.1 MST (Multimodal Spatiotemporal Transformer) 🎭 角色：超级记忆大师 MST 的任务是将过去复杂的视频画面和驾驶操作，压缩成一个精炼的特征向量。就像一个记忆力超群的人，看一眼就能记住所有关键信息。\n输入预处理 原始输入： ├── 视觉：过去 T 帧 (如 10 帧) 图像，分辨率 512×1024 └── 动作：每帧对应的历史轨迹（速度、方向盘转角等） DCAE 压缩处理： ├── 图像压缩 16 倍：512×1024 → 32×64 特征图 ├── 铺平成 Token：32×64 = 2048 个视觉 Token (记作 L) └── 动作投影：动作向量映射到同维度 Token 最终输入张量 E： ├── 形状：[Batch, T, (L+3), D] ├── L+3 = 2048 个视觉 Token + 3 个动作 Token └── D = 特征维度 🔄 时空分离处理 —— \u0026ldquo;先看时间，再看空间\u0026rdquo; MST 不是同时处理时空，而是交替进行，像这样：\n步骤 A：时间层 —— \u0026ldquo;串联历史\u0026rdquo; 目标：让图像中同一个坐标位置的像素点，去查阅自己在过去 $T$ 帧的变化。\n# 输入变换 原始形状：[B, T, S, D] # S 是空间 Token 数 L+3 变换后：[(B * S), T, D] # 把空间维度和 Batch 绑在一起 # 物理含义 现在模型眼里的\u0026#34;一个样本\u0026#34;，不再是整段视频， 而是视频中某个特定位置的像素点随时间的变化序列。 # 关键技术：Causal Mask（因果遮罩） 第 5 帧的像素只能看第 1, 2, 3, 4 帧的自己，不能偷看第 6 帧。 步骤 B：空间层 —— \u0026ldquo;理解当下\u0026rdquo; 目标：把每一帧看作独立的图片，让图像里的车、路、树木以及动作指令 Token 进行全注意力交互。\n# 输入变换 变换后：[(B * T), S, D] # 把时间维度和 Batch 绑在一起 # 多模态融合 视觉信息和动作意图在此处深度融合。 📍 3D 位置编码 (EmbedND) Epona 使用分块对角旋转位置编码 (RoPE) 来编码时空位置：\ndef EmbedND(dim, theta, axes_dim): \u0026#34;\u0026#34;\u0026#34; 为视频中的每个像素点生成 3D 位置嵌入 维度分配示例：[Time: 2维, Height: 2维, Width: 2维] \u0026#34;\u0026#34;\u0026#34; for i, (pos, dim) in enumerate(zip(axes, axes_dim)): out.append(rope(pos, dim, theta)) return torch.cat(out, dim=-1) 形象例子：假设要给坐标 (t=5, h=10, w=20) 的像素编码：\n循环 1 (Time): Embed(5) → [0.1, 0.9] 循环 2 (Height): Embed(10) → [0.5, 0.5] 循环 3 (Width): Embed(20) → [0.8, 0.2] 最终拼接：[0.1, 0.9, 0.5, 0.5, 0.8, 0.2] 这样，最终向量同时包含时间、高度和宽度信息，互不干扰。\n输出 经过 $N$ 层循环后，提取序列中最后一帧的特征 $\\mathbf{F}$。这是包含丰富历史语义和当前状态的高维特征向量，作为后续两个模块的基石。\n🚗 2.2 TrajDiT (Trajectory Planning DiT) 🎭 角色：决策中枢 \u0026amp; 老司机 拿到 MST 给的局面 $\\mathbf{F}$，在不生成图像的情况下，极速规划出未来 3 秒怎么开。\n架构：双流融合 这是一个专门\u0026quot;画线\u0026quot;（轨迹）的轻量级扩散模型。\n输入准备： ├── 条件 (Cond)：来自 MST 的特征 F └── 噪声 (Input)：随机高斯噪声 x_T（代表未来轨迹的草稿） 双流阶段 (Dual-Stream Phase)： ├── 环境流：处理特征 F ├── 轨迹流：处理噪声轨迹 └── 通过 Cross-Attention 交换信息 单流阶段 (Single-Stream Phase)： ├── 两条流拼接，深度混合推理 └── 确保轨迹与环境严丝合缝 🔧 Modulation 调制机制 Modulation 是将时间嵌入转化为神经网络控制参数的关键组件：\nclass Modulation: def __init__(self, dim, double): self.multiplier = 6 if double else 3 self.lin = nn.Linear(dim, dim * self.multiplier) def forward(self, vec): out = self.lin(vec.silu()).chunk(self.multiplier, dim=-1) return out # 返回 (shift, scale, gate) 组 参数含义：\nShift (β)：偏移量，平移特征 Scale (γ)：缩放因子，拉伸/压缩特征 Gate (α)：门控值，控制残差连接强度 📊 DoubleStreamBlock vs SingleStreamBlock 特性 DoubleStreamBlock SingleStreamBlock 数据流 两条独立流 (环境+轨迹) 一条混合流 调制参数 每条流 6 个，共 12 个 仅 3 个 结构 串行逻辑 并行逻辑 用途 TrajDiT 前期，保护环境特征 TrajDiT 后期/VisDiT，高效推理 在 DoubleStreamBlock 中：\n# 轨迹流 img_mod1 → 控制 Attention 的 AdaLN 和门控 img_mod2 → 控制 MLP 的 AdaLN 和门控 # 环境流 cond_mod1 → 控制 Attention 的 AdaLN 和门控 cond_mod2 → 控制 MLP 的 AdaLN 和门控 🎨 2.3 VisDiT (Next-frame Prediction DiT) 🎭 角色：超写实画师 根据 MST 的特征和 TrajDiT 的轨迹规划，生成下一帧图像。\n输入准备 画布噪声：随机高斯噪声潜变量 Z_{T+1} 环境参考：MST 的特征 F 动作指令：TrajDiT 预测的轨迹（关键！） 核心机制：动作调制 轨迹向量转化为控制神经网络的旋钮参数：\n# 轨迹向量转化为缩放因子和偏移量 Input = Input * Scale(a) + Shift(a) # 通过 AdaLN 注入到 Transformer 每一层 如果规划是\u0026quot;左转\u0026quot;，调制会强迫网络关注左侧特征 保证生成画面与规划动作一致 ⏱️ 分辨率感知的时间偏移 (get_schedule) 这是一个**\u0026ldquo;智能时间管理大师\u0026rdquo;**：\ndef get_schedule(num_steps, image_seq_len, base_shift=0.5, max_shift=1.15): timesteps = torch.linspace(1, 0, num_steps + 1) # 基础进度条 if shift: mu = get_lin_function(base_shift, max_shift)(image_seq_len) timesteps = time_shift(mu, 1.0, timesteps) # 偏移 return (1 - timesteps).tolist() 为什么需要它？\n痛点：画大图比画小图更难，需要在\u0026quot;宏观构图\u0026quot;阶段多花点时间 解决方案：根据序列长度自动调整时间表 序列长（大图）：在高噪声阶段停留更久，先定大轮廓 序列短（小图/轨迹）：匀速搞定即可 在 Epona 中：\nTrajDiT：序列短，时间表几乎不偏移 VisDiT：序列长（2048 Token），显著偏移 时序感知解码 使用 Temporal-aware DCAE Decoder 解压潜变量 参考上一帧的潜变量，消除频闪和抖动 输出 512×1024 高清图像 🎓 关键技术细节 3.1 傅立叶嵌入 (timestep_embedding) 将低维动作数据映射到高维空间，增强神经网络对细微变化的感知能力：\ndef timestep_embedding(t, dim, max_period=10000): t = time_factor * t half = dim // 2 freqs = exp(-log(10000) * arange(0, half) / half) args = t[:, None] * freqs[None] embedding = cat([cos(args), sin(args)], dim=-1) return embedding 原理：\n原始输入：低维向量 输出：高维特征，包含从低频到高频的丰富信号 效果：神经网络能\u0026quot;看到\u0026quot;微小变化 3.2 RoPE vs 正弦编码 特性 正弦编码 RoPE 相对位置感知 弱 强（点积只取决于相对距离） 长度外推性 差 好（周期性，不死记硬背） 维度解耦 难 优雅（分块对角矩阵） 3.3 连锁前向训练 (Chain-of-Forward Training) 痛点：自回归模式的误差累积——第一帧歪一点，第 100 帧就崩了\n解决方案：\n训练时偶尔用模型自己生成的（有瑕疵的）预测结果作为下一轮输入 模型被迫学会自我修正 效果：能生成长达 2 分钟不崩坏的视频\n🔄 完整推理流程 def step_eval(latents, rel_pose, rel_yaw): # 1. MST 编码：压缩历史信息 stt_features, pose_emb = model.evaluate(latents, poses, yaws) # 2. TrajDiT 规划：决定未来怎么走 noise_traj = randn(...) predict_traj = traj_dit.sample(noise_traj, traj_ids, stt_features) # 3. 提取下一步动作 predict_pose, predict_yaw = predict_traj[:, 0:1, ...] pose_emb = model.get_pose_emb(predict_pose, predict_yaw) # 4. VisDiT 生成：脑补下一帧画面 noise = randn(...) predict_latents = dit.sample(noise, img_ids, stt_features, pose_emb) return predict_traj, predict_latents 📊 实验成果 指标 结果 视频生成 FVD 7.4 (优于 Vista 7.9, 远超 DriveGAN 73.4) 视频长度 2 分钟 且逻辑连贯 物理理解 自学懂\u0026quot;红灯停\u0026quot;、\u0026ldquo;避让行人\u0026quot;等规则 规划能力 NAVSIM 评测超过多个专门规划模型 💡 总结 Epona 通过 MST（压缩理解）、TrajDiT（规划导航） 和 VisDiT（受控绘图） 三者的精密配合，实现了从\u0026quot;看懂路\u0026quot;到\u0026quot;决定怎么开\u0026quot;再到\u0026quot;脑补未来后果\u0026quot;的完整闭环。\n它不仅是一个视频生成器，更是一个具备潜力的端到端自动驾驶大脑。\n📎 相关链接 论文：Epona: Autoregressive Diffusion World Model for Autonomous Driving 相关工作：[[World4Drive - 无需感知标注的端到端世界模型]]、[[LAW - Latent World Model for E2E Driving]] ","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/epona/","title":"Epona: Autoregressive Diffusion World Model for End-to-End Autonomous Driving"},{"content":"核心贡献: 提出了隐式思维链世界模型，让端到端自动驾驶模型具备了\u0026quot;三思而后行\u0026quot;的深度推理能力 FutureX: 隐式思维链世界模型驱动的端到端自动驾驶 一、核心痛点：现在的自动驾驶模型都是\u0026quot;直肠子\u0026quot; 各位乘客请系好安全带！今天我们要深入了解一款拥有\u0026quot;老司机思维\u0026quot;的自动驾驶大模型——FutureX。\n这篇论文解决了一个自动驾驶界的痛点：现在的端到端（E2E）模型很多都是\u0026quot;条件反射式\u0026quot;——看到什么画面，立刻给出方向盘和油门指令。这种\u0026quot;直肠子\u0026quot;在简单路况还行，遇到复杂路况（比如前车突然变道、路口有行人探头）就容易抓瞎，因为它们不会预测未来。\n人类老司机是怎么开车的？遇到复杂路况，我们会在脑子里\u0026quot;推演\u0026quot;：\u0026ldquo;如果我现在加速，左边的车会不会挤过来？如果我变道，后面的车刹不刹得住？\u0026rdquo;\nFutureX 的核心魔法，就是赋予了AI这种\u0026quot;脑补\u0026quot;和\u0026quot;三思而后行\u0026quot;的能力！ 它提出了一个极其巧妙的隐式思维链世界模型（Latent Chain-of-Thought World Model）。\n二、整体架构：本能反应 vs. 深度思考 FutureX 的处理流程分为两个阶段：\n第一阶段：产生\u0026quot;直觉\u0026quot;（Initial Trajectory Proposal） 跟传统的自动驾驶一样，传感器（摄像头/激光雷达）的输入经过场景编码器（Scene Encoder），提取出当前时刻的隐状态（Current Latent $z_t$）。接着，策略网络（Policy Network）凭借直觉，先给出一个初始轨迹 $w_t$（包含未来 $T$ 步的位置和航向角）。\n第二阶段：聪明的\u0026quot;大脑门卫\u0026quot;（Auto-think Switch） AI如果每时每刻都在深度思考，车载芯片会被烧干的！所以 FutureX 引入了一个**\u0026ldquo;自动思考开关\u0026rdquo;** $\\mathcal{G}(\\cdot)$。\n它会看着当前的隐状态 $z_t$，评估一下当前场景的\u0026quot;难度得分\u0026quot; $d_t$（在0到1之间）：\nInstant（直觉模式）：难度低（比如空旷大直道），直接采用刚才的初始轨迹 $w_t$，省时省力。 Thinking（思考模式）：难度高，立刻唤醒\u0026quot;世界模型\u0026quot;，开始进行深度推演！ 技术细节：开关怎么训练的？ 作者定义了一个**\u0026ldquo;提升率 $r_t$\u0026rdquo;**： $$r_t = \\frac{e_{init} - e_{ref}}{e_{init} + \\varepsilon}$$\n$e_{init}$：初始轨迹与人类专家轨迹的 L1 误差 $e_{ref}$：经过思考后修正轨迹的 L1 误差 如果模型经过思考后，预测的轨迹比不思考的初始轨迹误差降低超过 25%（阈值 $\\alpha = 0.25$），这个场景的标签就被打上 $g_t = 1$（需要思考），否则为0。用交叉熵损失 $\\mathcal{L}_{auto}$ 来训练这个开关。\n三、核心方法：在脑海中\u0026quot;沙盘推演\u0026quot;（Latent CoT Reasoning） 一旦进入\u0026quot;思考模式\u0026quot;，FutureX 最核心的隐式思维链世界模型（Latent World Model, $\\mathcal{W}$） 就启动了！\n注意，它不像 ChatGPT 那样用文字思考，而是在高维度的**\u0026ldquo;隐空间（Latent Space）\u0026rdquo;**中思考。\n具体怎么做？—— \u0026ldquo;切蛋糕\u0026rdquo;！ 它把初始长轨迹 $w_t$（总长度为 $T$）均匀切分成 $K$ 段子轨迹（Sub-trajectories）。然后，世界模型开始一步步做\u0026quot;What-if（如果\u0026hellip;会怎样）\u0026ldquo;的沙盘推演：\n第一步推演：基于当前状态 $z_t^{(0)}$，如果我执行了第一段子轨迹 $w_t^{(1)}$，未来的世界会变成什么样？模型预测出未来的隐状态 $z_t^{(1)}$。 第二步推演：基于刚才预测的 $z_t^{(1)}$，如果我接着执行第二段子轨迹 $w_t^{(2)}$，世界又会变成什么样？得到 $z_t^{(2)}$。 以此类推…… 最终，模型得到了一串包含时空动态信息的**\u0026ldquo;思想链\u0026rdquo;**：$Z_{CoT} = {z_t^{(0)}, z_t^{(1)}, \u0026hellip;, z_t^{(K)}}$。这串数据完美捕捉了\u0026quot;自车行为\u0026quot;和\u0026quot;环境变化\u0026quot;的交互关系。\n四、修正路线：总结反思（Trajectory Refinement） 脑补完了未来，接下来就是拿这些\u0026quot;想法\u0026quot;来指导行动了。\nFutureX 有一个总结网络（Summarizer Network, $\\mathcal{S}$）。它把推演出来的所有\u0026quot;思想节点（$Z_{CoT}$）\u0026ldquo;和初始轨迹 $w_t$ 放在一起综合考量。\n就像老司机反思：\u0026ldquo;我原本想一脚油门过去（初始轨迹），但我脑补了一下发现左边那辆车可能会别我（隐式思想链），那我还是往右偏一点、减点速吧。\u0026rdquo;\n于是，总结网络会输出一个轨迹的**\u0026ldquo;偏移量（offsets）\u0026rdquo;，加在初始轨迹上，得到了最终安全、顺滑的修正轨迹 $w_t^{ref}$**。\n五、训练方法：三根\u0026quot;教鞭\u0026quot;共同发力 怎么把这个复杂的系统训练出来？核心是三个 Loss 函数：\n损失函数 作用 轨迹损失 $\\mathcal{L}_{traj}$ 用 L1 loss 比较预测轨迹和人类专家真实轨迹的差距。思考模式用修正轨迹算，直觉模式用初始轨迹算。 隐状态一致性损失 $\\mathcal{L}_{lat}$ 世界模型能准确预测未来的关键！要求模型\u0026quot;脑补\u0026quot;出来的未来状态 $\\hat{z}_t^{(k)}$，必须和真实世界到达那一刻时传感器提取出的真实状态 $z_t^{(k)}$ 尽可能一致。 自动思考损失 $\\mathcal{L}_{auto}$ 训练那个判断\u0026quot;要不要思考\u0026quot;的门卫开关。 六、实战成绩：分数飙升！ 论文在最权威的闭环仿真平台 NAVSIM 和 CARLA 上进行了极限测试：\nNAVSIM 榜单（Table 1） 模型 PDMS 分数 World4Drive 85.1 FutureX-Auto（纯视觉） 89.2 FutureX-All（TransFuser） 90.6 无论是只用摄像头（基于LTF），还是用摄像头+激光雷达（基于TransFuser），加入了 FutureX 框架后，PDMS 直接暴涨 5.4 和 6.2 分！\n实时性（Table 5 延迟消融实验） 自动驾驶最怕\u0026quot;思考太久车已经撞了\u0026rdquo;。实验表明，把轨迹切分成4段（$N=4$）时，FutureX 只比基础模型多花 17.0 毫秒 的推理时间，完全满足真实世界的实时性要求！\n七、深度讨论：与 World4Drive 的对比 两种不同的设计哲学 特性 World4Drive FutureX 策略 多线评估（择优录取） 单线修正（深度润色） 方式 并行推演 K 种意图，用 Selector 选最优 顺着初始轨迹做思维链推演，用 Summarizer 修正 优势 从根本上避免\u0026quot;一开始就走错路\u0026rdquo; \u0026ldquo;Refinement\u0026quot;的精度比\u0026quot;Selection\u0026quot;更高 分数 85.1 90.6 为什么单线修正反而更好？ 这是一个极其反直觉的问题！在非凸优化问题中，采样确实是对抗局部最优的经典武器。但为什么 FutureX 这种看起来容易\u0026quot;钻牛角尖\u0026quot;的局部修正流派，反而跑赢了全局采样流派？\n真相一：初始轨迹不是\u0026quot;盲目猜测\u0026rdquo;，而是\u0026quot;强力先验\u0026quot; FutureX 的初始轨迹 $w_t$ 是由像 TransFuser 这样已经训练得极其成熟的 Baseline 网络生成的。这些 SOTA 模型在海量专家数据的喂养下，它们的\u0026quot;直觉\u0026quot;已经非常接近全局最优了。绝大多数时候，它们已经落在了那个\u0026quot;全局最优解的深谷\u0026quot;里，只是离谷底还有几厘米的偏差。\n真相二：\u0026ldquo;采样空隙（Sampling Gap）\u0026ldquo;的无情折损 World4Drive 的困境：假设它采样了 10 种意图。在复杂的路口，可能最优的切入角度是 15.5 度，但采样出来的只有 10 度和 20 度。即便世界模型推演出 15.5 度最好，Selector 也选不出来，因为它只能从已有的 $K$ 个里挑。\nFutureX 的优势：Summarizer 网络输出的是连续空间的坐标偏移（Offsets）。它不是在做\u0026quot;选择题\u0026rdquo;，而是在做\u0026quot;微积分\u0026rdquo;。它可以在连续的空间里丝滑地移动坐标。\n真相三：\u0026ldquo;评价未来\u0026quot;比\u0026quot;修正未来\u0026quot;更难训练 World4Drive（评价流）：需要世界模型和裁判网络对任何乱七八糟的采样轨迹都能给出一个准确的评分。如果模型没见过某种奇怪的走法，评分就会失真（OOD问题）。 FutureX（修正流）：任务目标极其聚焦。它只看\u0026quot;我这一条路走下去会有什么后果\u0026rdquo;。CoT 得到的不是冷冰冰的分数，而是一串带有丰富语义的隐状态序列 $Z_{CoT}$。这种时序上的因果反馈，比一个单一的\u0026quot;得分\u0026quot;包含的信息量大得多。 延迟对比：为什么 FutureX 更\u0026quot;慢\u0026quot;？ 虽然大家都在隐空间里玩推演，但**推演的\u0026quot;姿势\u0026quot;**完全不同：\nWorld4Drive：批处理流（快） 它推演 $K$ 种未来时，这 $K$ 个意图全部是从当前时刻 $t_0$ 出发的。对于显卡来说，这只是把 Batch Size 从 1 变成了 $K$。显卡可以在同一个前向传播周期里，一把推算出这 $K$ 种方案的结局。\nFutureX：思维链流（慢） 它的推演是**自回归（Autoregressive）**的：\n先算第一段子轨迹，得到状态 $z_t^{(1)}$ 必须等 $z_t^{(1)}$ 算出来后，才能把它塞回模型，去算第二段 这就形成了 $K$ 次串行依赖 另外，FutureX 的世界模型是由一叠 Transformer 层构成的，比 World4Drive 的轻量级 MLP 更厚重。\n八、总结 FutureX 打破了端到端自动驾驶\u0026quot;只见树木不见森林\u0026quot;的局限，通过引入\u0026quot;自动切换\u0026quot;的\u0026quot;隐式思维链世界模型\u0026quot;，让AI学会在脑海中试错，从而在现实中开得更稳、更安全！\n形象比喻 World4Drive 是在茫茫大海上扔了 10 个救生圈，看哪个飘得近。 FutureX 是已经划着船到了岸边，然后用望远镜（世界模型）看清了暗礁，最后精准地推了一下舵。 目前的榜单告诉我们：现在的 AI 划船技术已经够好了，它们现在更缺的是那副望远镜。\n相关论文 [[World4Drive]] - 多线评估的对比论文 [[TransFuser]] - 基础 Backbone [[LTF]] - 基础 Backbone ","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/future-x/","title":"FutureX: Enhance End-to-End Autonomous Driving via Latent Chain-of-Thought World Model"},{"content":" 一、这篇论文在讲什么？ 核心问题：AI司机的\u0026quot;偏科\u0026quot;痛点 现在的端到端自动驾驶（E2E-AD）界有个普遍的怪现象：\u0026ldquo;应试教育\u0026quot;满分，\u0026ldquo;实战上路\u0026quot;拉胯。\n开环测试（开卷考试）：给一段历史视频，让 AI 画出未来的行驶轨迹（Waypoints）。大家都画得很好，碰撞率（Collision Rate）极低，甚至能降到 0.1%。 闭环测试（实车上路）：把 AI 扔进仿真软件（比如 Bench2Drive）里真刀真枪地开。结果呢？成功率不到 35%！遇到路口就犹豫不决（超时死机），遇到突发情况就抓瞎。 问题根源 作者一针见血地指出：目前的 AI 在\u0026quot;规划（Planning）\u0026ldquo;时，和\u0026quot;感知（Perception）\u0026ldquo;的交互太弱了！而且预测出来的轨迹太单调（只有稀疏的几个点），根本不够用来做精细的车辆控制。\nHiP-AD 的答案 用 \u0026ldquo;分层多粒度预测\u0026rdquo; + \u0026ldquo;顺着轨迹看图像（PDA）\u0026rdquo; + \u0026ldquo;三合一圆桌会议（统一解码器）\u0026rdquo;，彻底打通感知与规划的任督二脉\n二、核心方法：老司机的三大独门绝技 绝技一：多把量尺看世界 —— 分层与多粒度规划 以前的 AI 预测轨迹，就是每秒打几个点（时间路点，Temporal Waypoints）。这就好比司机只知道\u0026quot;我 3 秒后要到那个路口\u0026rdquo;，但他不知道这 3 秒内方向盘该打几度。\nHiP-AD 彻底改变了玩法，它不仅预测时间，还把轨迹拆解成了 三大维度（模态），并且每个维度都有 不同的颗粒度：\n模态 关注点 颗粒度划分 用途 空间路点 \u0026ldquo;路径\u0026quot;怎么走 密集（2m一个点）\u0026amp; 稀疏（5m一个点） 密集点做精准方向盘微调（横向控制），稀疏点看宏观大局 时间路点 \u0026ldquo;什么时候到哪\u0026rdquo; 高频（5Hz）\u0026amp; 低频（2Hz） 高频应对紧急情况，低频规划长程 驾驶风格路点 \u0026ldquo;车速和动作\u0026rdquo; 慢/中/快（0-4, 4-10, 10+ m/s） 应对超车、紧急刹车等复杂场景 控车策略：AI 会用空间路点控制方向盘（横向），用时间和驾驶风格路点控制油门刹车（纵向），完美解决由于轨迹点太稀疏导致的\u0026quot;车辆犹豫\u0026quot;问题！\n绝技二：指哪打哪的目光 —— 规划可变形注意力（PDA） 以前的 AI 规划器在看周围环境时，是\u0026quot;全局乱瞟\u0026rdquo;（Global Attention），不仅计算量大，还容易抓不到重点。\nHiP-AD 的做法极其聪明——它利用了几何学物理规律：\n先把系统预测出的 3D 未来轨迹点（Waypoints），通过相机参数，投影映射到 2D 的多视角环视图像上 然后，只在这些 \u0026ldquo;车轮即将压过\u0026rdquo; 的图像物理位置附近，去采样提取图像特征 生动比喻：\n这就像老司机在开车时，目光会死死盯住自己即将开过去的那条车道线和前方的障碍物，而不是去看天上的云彩。\n数学表达： $$\\text{PDA}(Q_p, F) = \\sum_{i \\in \\mathcal{V}} \\text{DeformAttn}(Q_p, \\mathcal{P}(A_p), F_i)$$\n规划主脑有一条预测出的未来 3D 轨迹（$A_p$）。它把这条未来轨迹用函数 $\\mathcal{P}$ 投影到多视角的图像（$F$）上，然后只在\u0026quot;未来车轮即将压过的图像像素点\u0026quot;周围提取特征！\n绝技三：超级大脑圆桌会议 —— 统一解码器（Unified Decoder） 传统的框架是流水线作业：先做感知（找车、找线）→ 再做预测 → 最后做规划。不仅慢，信息还会衰减。\nHiP-AD 搞了一个 \u0026ldquo;单解码器\u0026rdquo;，把所有任务拉进一个群里并行开会！\n入场的三大\u0026quot;代表\u0026rdquo;（输入配置） 代表 符号 职责 初始坐标（Anchors） 🚗 动态物体代表 Agent Query ($Q_a$) 找周围移动的车、人 3D 边界框（Box Anchors） 🛣️ 静态地图代表 Map Query ($Q_m$) 找车道线、斑马线 聚类算法生成的折线（Polyline Anchors） 🧠 自动驾驶主脑代表 Planning Query ($Q_p$) 决定咱们自己的车往哪开 未来 $T$ 个时间点的行驶轨迹折线 圆桌会议的三大议程 每个 Decoder Layer 都包含以下三个关键环节：\n🕒 议程一：翻阅历史案卷 —— 时序交互模块 老司机开车绝不能只看眼前，得记得上一秒旁边有辆车想加塞。\n技术动作：\n代表们（Queries）首先会和 上一帧历史保留下来的重要特征 进行 交叉注意力（Cross-Attention） 计算 为了防止历史信息太多把脑袋撑爆，系统用了一个 Top-$k$ 选择机制，只保留上一帧最有价值的线索 给主脑开小灶：规划代表（Planning Query）不但要回顾自己的历史，还会额外增加一次跨任务的 Cross-Attention，专门去盯住历史帧里的感知信息 🗣️ 议程二：圆桌激烈讨论 —— 协同交互模块 看完历史，大家得互相通气了。主脑需要知道哪里有车、哪里是实线，才能规划路线。\n核心技术细节 —— 几何物理融合（公式 1）：\n$$\\text{Attention}(Q, K, V) = \\text{Softmax}\\left(\\frac{QK^T}{\\sqrt{C}} - \\tau D\\right)V$$\n常规的注意力机制只有前半部分，决定谁和谁更相关。但这里多了一个极其关键的 \u0026ldquo;惩罚项\u0026rdquo;：$- \\tau D$\n$D$ 是什么？ 两个物体在真实 3D 物理世界里的 欧几里得距离（比如动态车与车之间、车与车道线之间） $\\tau$ 是什么？ 通过多层感知机（MLP）学习出来的系数 精妙之处：如果一辆车离你很远（$D$ 很大），那么它在注意力分数里就会被狠狠扣分。这强迫 AI 司机 \u0026ldquo;把有限的注意力集中在离自己最近、最危险的物体上\u0026rdquo;！ 主脑的特权：这个距离惩罚对感知代表有效，但对 规划代表没有距离限制！为什么？因为作为最高决策者，规划主脑必须有统揽全局的视野。\n🎯 议程三：去图像里找证据 —— 任务可变形注意力模块 讨论出了初步结果，代表们需要带着目前的猜测，去原始的摄像机画面里 \u0026ldquo;精准取证\u0026rdquo;，刷新自己的认知。\n感知代表的做法：把 3D 锚点顺着预设高度，利用相机参数 投影到 2D 环视图像上，然后在投影落下的那个点附近采样提取图像特征。\n规划主脑的终极奥义（PDA）：\n把预测的\u0026quot;未来 3D 行驶轨迹路点\u0026quot;投影到多个摄像头画面上 让主脑自己学习：在轨迹周围哪些像素点最值得关注？ 目光死死盯住\u0026quot;车轮即将压过的未来路线\u0026quot;周围的画面！提取图像特征。 三、伪代码实现：老司机的脑神经 import torch import torch.nn.functional as F def Unified_Decoder( image_features, # [多视角环视图像特征 F] (监控录像) queries, # [动态物体Qa, 静态地图Qm, 规划主脑Qp] (拿着清单的代表) anchors, # [动态框Aa, 静态线段Am, 未来轨迹Ap] (各自关注的3D物理坐标) history_memory, # [上一帧存下来的重要记忆] camera_params, # [相机的内外参矩阵] (用于3D到2D的投影) num_layers=6 # 会议通常要开好几轮 (堆叠6层Decoder) ): # --- 会议前奏：代表入场 --- Qa, Qm, Qp = queries Aa, Am, Ap = anchors # 🔄 开始循环开会：每一层 Decoder 都在进行信息的深度融合 for layer in range(num_layers): # ========================================================= # 🕒 议程一：时序交互模块 (Temporal Interaction Module) # 目标：\u0026#34;温故知新\u0026#34;，看看上一秒发生了什么。 # ========================================================= # 1. 脑容量有限，用 Top-k 机制挑选上一帧最有价值的记忆点 hist_k_agent, hist_k_map, hist_k_plan = select_top_k(history_memory) # 2. 感知代表各自查阅自己的历史卷宗 (Cross-Attention) Qa = CrossAttention(query=Qa, key=hist_k_agent, value=hist_k_agent) Qm = CrossAttention(query=Qm, key=hist_k_map, value=hist_k_map) # 💡 3. 【老司机的特权】规划主脑不仅看自己的历史轨迹， # 还要盯着历史的感知环境！ Qp_self_hist = CrossAttention(query=Qp, key=hist_k_plan, value=hist_k_plan) Qp_perc_hist = CrossAttention( query=Qp, key=concat(hist_k_agent, hist_k_map), value=concat(hist_k_agent, hist_k_map) ) Qp = combine_features(Qp_self_hist, Qp_perc_hist) # ========================================================= # 🗣️ 议程二：协同交互模块 (Collaborative Interaction Module) # 目标：统一的圆桌会议，互相通气，引入\u0026#34;物理距离惩罚\u0026#34;！ # ========================================================= # 1. 把所有人拉进一个群里 Q_all = concat(Qa, Qm, Qp) A_all = concat(Aa, Am, Ap) # 2. 计算纯粹的注意力分数 (谁跟谁有关联) attn_scores = torch.matmul(Q_all, Q_all.transpose(-2, -1)) / math.sqrt(C) # 💡 3. 【核心技术细节：公式(1)】计算真实物理世界中 3D 锚点之间的距离矩阵 D D_matrix = calc_euclidean_distance_3d(A_all, A_all) # ⚠️ 【主脑特权掩码】规划主脑(Qp)看所有人都不受距离限制 D_matrix = apply_planning_mask(D_matrix, mask_value=0.0) # 4. 用一个小网络学习一个动态系数 tau (控制惩罚力度) tau = MLP(Q_all) # 5. 施加物理距离惩罚！距离越远的物体，注意力得分被扣得越惨 penalized_scores = attn_scores - (tau * D_matrix) attn_weights = F.softmax(penalized_scores, dim=-1) # 6. 根据惩罚后的权重，大家交换情报 Q_all_updated = torch.matmul(attn_weights, Q_all) # 7. 散会，大家拿着更新后的情报各自归位 Qa, Qm, Qp = split_queries(Q_all_updated) # ========================================================= # 🎯 议程三：任务可变形注意力模块 (Task Deformable Attention Module) # 目标：拿着讨论结果，去图像画面里\u0026#34;精准取证\u0026#34;。 # ========================================================= # 1. 感知代表去找车和线：把 3D 的框和线投影到 2D 图像上 P_a_2d = project_3D_to_2D(Aa, camera_params) P_m_2d = project_3D_to_2D(Am, camera_params) Qa = DeformableAttention(query=Qa, reference_points=P_a_2d, features=image_features) Qm = DeformableAttention(query=Qm, reference_points=P_m_2d, features=image_features) # 💡 2. 【核心技术细节：公式(2) PDA】规划主脑的终极绝技！ # 把预测的\u0026#34;未来3D行驶轨迹路点\u0026#34;投影到多个摄像头画面上 P_p_2d = project_trajectory_to_2D(Ap, camera_params, predefined_heights) # 让主脑自己学习：在轨迹周围哪些像素点最值得关注？ sampling_offsets, sampling_weights = MLP_predict_offsets_weights(Qp) # 施展 PDA：目光死死盯住\u0026#34;车轮即将压过的未来路线\u0026#34;周围的画面！ Qp = PDA_DeformableAttention( query=Qp, reference_points=P_p_2d, offsets=sampling_offsets, weights=sampling_weights, features=image_features ) # 🔄 层级收尾：更新代表们的 3D 坐标锚点 Aa = update_anchors(Aa, Qa) Am = update_anchors(Am, Qm) Ap = update_anchors(Ap, Qp) # 轨迹越来越精确 # 🚪 整个开会流程结束，输出给外面的业务部门 (Heads) 去执行 update_history_memory(Qa, Qm, Qp) return Qa, Qm, Qp 伪代码里的三个\u0026quot;高光时刻\u0026rdquo; 议程一中的 combine_features(Qp_self_hist, Qp_perc_hist)：规划时不仅记得自己上一秒想怎么走，还 直接 调取了上一秒周围环境的原始记忆，没有中间商赚差价。\n议程二中的 penalized_scores = attn_scores - (tau * D_matrix)：这就是论文里最惊艳的 几何物理融合。它强迫网络变成一个真正的司机——\u0026ldquo;不要看天上飞的鸟，看离你保险杠只有半米远的那辆车！\u0026rdquo;\n议程三中的 PDA_DeformableAttention：传统的网络是在图像上撒网捞鱼，而这里是 \u0026ldquo;按图索骥\u0026rdquo;。沿着预测轨迹投影到 2D 上的路线，只在这个轨迹的左右两边提取图像特征。\n四、秘密训练法：对齐匹配（Align Matching） 因为搞出了几十种不同粒度、不同模态的预测轨迹，训练的时候 AI 容易\u0026quot;精神分裂\u0026rdquo;，到底哪个才是最准的？\n对齐匹配机制（公式 6）：\n在训练时，采用 \u0026ldquo;赢家通吃\u0026rdquo;（Winner-takes-all） 的策略 先在所有的轨迹预测组里，找到和真实人类轨迹（Ground Truth）差距最小（L2 距离最小）的那组作为\u0026quot;学霸（Reference）\u0026quot; 然后，强迫其他所有的粒度组，都向这个\u0026quot;学霸\u0026quot;的模态对齐，共享匹配结果，把梯度有效地反向传播回去 五、战绩揭晓（实验结果） 这位练成神功的 HiP-AD 老司机去考场了，结果直接\u0026quot;屠榜\u0026quot;！\n闭环终极测验（Bench2Drive 数据集） 指标 HiP-AD 第二名（DriveTransformer） 成功率 72.7% 35%（翻了一倍多） 驾驶得分 88.3 远超所有现有端到端模型 特殊技能考核 高难度科目 成功率 紧急刹车 83.33% 超车 84.44% 汇入车流 50% 不再像以前的模型那样遇到复杂路况就死机！\n开环基础测验（nuScenes 数据集） 碰撞率：0.01% - 0.05%（极低） 感知和预测任务也拿到了顶尖分数，证明底盘依然极度扎实 六、总结与局限 一句话总结 HiP-AD 通过 \u0026ldquo;分层多粒度预测\u0026rdquo; + \u0026ldquo;顺着轨迹看图像（PDA）\u0026rdquo; + \u0026ldquo;三合一圆桌会议（统一解码器）\u0026rdquo;，彻底打通了感知与规划的任督二脉，解决了端到端自动驾驶在闭环仿真中不敢开、不会开的难题。\n局限性（论文坦诚） 遇到后方突然高速冲过来的车辆时，系统有时候还是会反应不及（这确实也是人类司机的盲区） 目前还在仿真阶段，真车路测将是未来的星辰大海 参考来源 原论文：HiP-AD: Hierarchical and Multi-granularity Planning with Deformable Attention for End-to-End Autonomous Driving 讨论：Gemini 3.1 Pro Preview 对话记录 ","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/hi-p-ad/","title":"HiP-AD: Hierarchical and Multi-granularity Planning with Deformable Attention"},{"content":"一句话总结 通过在潜空间进行动作引导的未来特征预测，实现了无需标注的深度场景特征学习，显著提升了端到端驾驶的规划精度。\n研究动机 行业痛点 传统端到端自动驾驶存在两派困境：\n流派 特点 问题 重感知派 给每张图画框、标行人、画车道线 标注成本极其昂贵，数据量一大就难以维持 轻感知派 只看录像和司机操作，直接学驾驶 缺乏对世界物理规律的真正理解，悟性不稳定 LAW 的解决方案 核心思想：不依赖昂贵的标注，让\u0026quot;轻感知\u0026quot;选手也能获得\u0026quot;重感知\u0026quot;选手的深度理解力。\n实现方式：给自动驾驶系统装上一个**\u0026ldquo;预知未来的水晶球\u0026rdquo;**——让车子不仅能看清现在，还能在脑子里\u0026quot;排演\u0026quot;：如果我这么开，下一秒世界会变成什么样？\n核心技术方案 架构概览 LAW 的核心是一个自监督循环：\n当前画面特征(z_t) + 计划轨迹(a_t) ↓ [预测器] → 预测未来特征(z_{t+1}) ↓ [对比] ← 真实未来特征 四步技术实现 第一步：潜空间编码 (Feature Encoding) 将摄像头图像转换为\u0026quot;浓缩信号\u0026quot;：\n主干网络：ResNet 或 Swin-Transformer 提取图像特征 空间选择（灵活适配两种模式）： 2D 视角 (Perspective View)：直接在多摄像头平面图上提取特征 3D 视角 (BEV View)：通过 LSS (Lift-Splat-Shoot) 转换成俯瞰网格图 输出：Latent State $z_t$ — 当前时刻世界状态的\u0026quot;浓缩精华\u0026quot; 第二步：潜空间动力学 (Latent Dynamics) 这是 LAW 最硬核的部分——在脑子里模拟未来：\n预测器输入： 当前精华信号 $z_t$ 计划采取的轨迹 $a_t$ 功能：计算\u0026quot;如果我按 $a_t$ 开，下一秒的精华信号 $z_{t+1}$ 会是什么样？\u0026quot; 时序展开：连续预测未来几秒的特征序列，建立**\u0026ldquo;动作驱动的环境模拟器\u0026rdquo;** 第三步：轨迹规划 (Trajectory Planning) 规划器组成：多层感知机 (MLP) 或循环神经网络 (GRU) 输入：当前潜空间特征 $z_t$ 输出：未来几秒的坐标点序列 (Waypoints) 关键：因为 $z_t$ 已被\u0026quot;预言任务\u0026quot;训练得极具预判价值，规划器不需要复杂规则就能读出最安全路线 第四步：自监督损失函数 训练不需要人工标注，靠**\u0026ldquo;打脸教育\u0026rdquo;**：\n$$Loss = L_{plan} + \\lambda \\times L_{world}$$\n$L_{plan}$（规划损失）：模仿学习，与人类老司机的轨迹对比 $L_{world}$（世界模型损失）：拿\u0026quot;脑补的未来特征\u0026quot;与\u0026quot;真实发生的未来特征\u0026quot;对比 核心逻辑：为了猜准未来，被迫看清现在。\n关键技术细节 轨迹的来源 阶段 轨迹来源 说明 训练阶段 Ground Truth（人类驾驶员轨迹） 建立因果关系：动作→环境变化 推理阶段 Planner 自己生成的轨迹 \u0026ldquo;我想这么走\u0026rdquo; + 世界模型预测后果 轨迹输入方式 不是简单丢坐标，而是采用 Trajectory Encoding：\n坐标点 → 向量 MLP 升维 → 与图像特征同维度 Cross-Attention 或 Concatenation 与图像特征融合 每一处图像特征都会\u0026quot;询问\u0026quot;轨迹信号：\u0026ldquo;车子待会会靠近我吗？\u0026rdquo;\n推理时世界模型的角色 角色 是否运行预测器 说明 训练支架 ❌ 不运行 已完成使命——把编码器练强了 想象力实验室 ✅ 运行 MPC：生成多个候选动作，世界模型预测后果选优 安全守卫 ✅ 运行 冗余校验：预测未来若显示碰撞风险则紧急干预 LAW 论文的核心贡献是第一种——通过预测任务让编码器学到更好的特征表示，推理时可以不运行预测器。\n实验结果 在三个自动驾驶顶级 Benchmark 上达到 SOTA：\nnuScenes：真实世界公开数据集 NAVSIM：最新的端到端驾驶评估标准 CARLA：仿真环境闭环测试，长距离驾驶碰撞率极低 关键成就：在完全不使用检测、跟踪、地图分割等额外标注的情况下，超越了大量标注的强感知模型。\n技术演进与定位 历史脉络 World Models (2018) ↓ 首次提出\u0026#34;潜空间做梦\u0026#34;哲学 ↓ 但只在简单2D游戏验证 ↓ MILE (2022) ↓ 自动驾驶领域的初步实验 ↓ 主要在CARLA仿真环境 ↓ LAW (2024) ↓ 集大成 + 普适化 ↓ 真实世界数据集 + 跨视角统一 + 模块化设计 LAW 的三大突破 解决标注依赖：证明只要 Latent World Model 够好，Latent Embedding 已自动包含 3D 几何和动态规律 跨视角统一：一套通用动力学模型，适配 2D/BEV 任意表征 因果性优于相关性：Latent Embedding 从\u0026quot;静态快照\u0026quot;变为\u0026quot;动态因果引擎\u0026quot; 核心洞察 学习自动驾驶不一定非要老师划重点（人工标注），只要给车子一个**\u0026ldquo;反思机制\u0026rdquo;**——让它不断地用自己的\u0026quot;想象力\u0026quot;去挑战\u0026quot;现实\u0026quot;，它就能从海量的无标注视频中，自学成为老司机。\nLAW 代表了**\u0026ldquo;感知任务消失论\u0026rdquo;**的一种技术胜利——证明了高度抽象的 Latent Embedding 可以替代昂贵的人工标注。\n代码与资源 论文链接：arXiv:2406.08481 相关工作：[[World Models]]、[[MILE]]、[[UniAD]] 个人思考 如何将 LAW 思想应用到其他时序决策任务？ Latent Space 预测的可解释性如何保证？ 与 VLA (Vision-Language-Action) 模型的结合可能？ ","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/law/","title":"LAW - Enhancing End-to-End Autonomous Driving with Latent World Model"},{"content":" 论文标题：《Don\u0026rsquo;t Shake the Wheel: Momentum-Aware Planning in End-to-End Autonomous Driving》\n一、这篇论文在讲什么？ 核心问题：AI司机的\u0026quot;帕金森式哆嗦\u0026quot; 光看标题脑海里是不是就有画面了？没错，这篇论文解决的就是自动驾驶汽车**\u0026ldquo;疯狂画龙\u0026rdquo;、\u0026ldquo;方向盘乱抖\u0026rdquo;**的痛点！\n现在的端到端自动驾驶模型（如UniAD、VAD等）存在一个致命弱点：\u0026ldquo;单帧依赖（One-shot prediction）\u0026rdquo;。\n这就像一个新手司机，每开一秒钟都要重新做一次决定，完全不管上一秒自己是怎么想的。如果遇到短暂的视野遮挡（比如旁边大车挡了一下），或者感知模块稍微闪烁了一下，它就会立刻改变主意，导致车辆轨迹疯狂摇摆（Vehicle Trembling）。\nFigure 1 的直观对比 规划模式 问题表现 确定性规划（a） 只给一条路，遇到突发情况容易撞 多模态规划（b） 给很多条路让你选，但每一帧都在不同轨迹间横跳，导致方向盘乱打 MomAD方案 用\u0026quot;动量\u0026quot;保持轨迹连贯性，同时保留多模态应对突发情况 核心洞察：老司机的\u0026quot;动量\u0026quot;智慧 人类老司机是怎么开车的？是有\u0026quot;惯性\u0026quot;和\u0026quot;动量\u0026quot;的！我们不会因为别人闪了一下大灯就猛打方向盘，我们会根据之前的行驶意图保持连贯性。\nMomAD就是要赋予AI这种\u0026quot;老司机\u0026quot;的稳重感。\n二、核心方法：MomAD的三大法宝 MomAD框架是怎么做到\u0026quot;稳如老狗\u0026quot;的？核心在于三大技术模块：\n法宝一：拓扑轨迹匹配（TTM - Topological Trajectory Matching） 核心目标：在K个候选轨迹中，找出一个跟上一帧意图最\u0026quot;顺滑\u0026quot;衔接的轨迹。\n1. 坐标系转换 因为车一直在往前开，前一帧和当前帧的坐标系不一样了。TTM第一步就是把历史轨迹转换到当前的自车坐标系下：\n$$T_{past_aligned} = R^{-1}(T_{past} - \\Gamma)$$\n其中 $R^{-1}$ 是旋转矩阵的逆，$\\Gamma$ 是位移向量。\n2. 为什么不用简单的欧式距离？ 很多模型比对轨迹时用的是欧氏距离（按点算距离），但这玩意儿在过弯道时极其不准，容易被局部干扰。\n3. 引入豪斯多夫距离（Hausdorff Distance） TTM祭出了这个大杀器！豪斯多夫距离测量的是两条轨迹间最大偏差的最小值（最坏情况下的对齐程度）。\n$$d_H(T_a, T_b) = \\max{d_{forward}(T_a, T_b), d_{backward}(T_b, T_a)}$$\n生动比喻：\n豪斯多夫距离不仅看两个点离得近不近，更看整个轨迹的\u0026quot;形状（拓扑结构）\u0026ldquo;像不像。这就像比对两条弯曲的山路，不是看某个点对齐没，而是看整体走势是否吻合。\n通过豪斯多夫距离，TTM选出那条和历史轨迹最吻合的当前候选轨迹。\n法宝二：动量规划交互器（MPI - Momentum Planning Interactor） 核心目标：光选出来还不够，还要把历史的\u0026quot;经验（动量）\u0026ldquo;注入到当前的Query中。\n子模块A：长时序查询混合器（Long-horizon Query Mixer） 它把历史的规划Query取出来，通过多层感知机（MLP）和**LSTM（长短期记忆网络）**进行时序演化：\n# 用Sigmoid处理历史分数，并与历史Query做逐元素乘法（融合历史置信度） historical_fusion = torch.sigmoid(S_past) * self.mlp_mixer(Q_past) # 用LSTM模拟时序上的演化 Q_past_prime, _ = self.lstm(historical_fusion.unsqueeze(0)) 这就像是把过去几秒钟的环境理解和别人车辆的意图\u0026quot;浓缩\u0026quot;了起来。\n子模块B：交叉注意力融合 用选中的当前Query作为主动方，去和浓缩好的历史信息做交叉注意力计算：\n# Q: 当前选出的最优 Query # K, V: 经过时序演化的历史 Query Q_tilde_p_star_t, _ = self.cross_attention( query=Q_p_star_t, key=Q_past_prime, value=Q_past_prime ) 关键理解：\n当前帧只提供 query（此刻我想怎么走），而 key 和 value 全是上一帧经过LSTM提纯的记忆（我过去几秒是怎么打算的）。这一步让模型长了\u0026quot;脑子\u0026rdquo;，不再是只会看眼前的金鱼记忆！\n法宝三：鲁棒实例去噪（Robust Instance Denoising） 核心目标：锻炼模型对感知噪声的抵抗能力。\n既然感知模块提供的特征（比如其他车的位置、车道线）可能不准，那就在训练时\u0026quot;折磨\u0026quot;它！\n# 训练期间加入高斯噪声扰动 if self.training and self.use_noise: noise = torch.randn_like(features_t) * 0.1 # 论文消融实验中噪声比例为0.1最优 features_t = features_t + noise 作用机制：\n让模型在训练时就习惯看\u0026quot;模糊、抖动\u0026quot;的世界。在测试时，这个去噪能力让规划预测器对环境波动极度免疫，即使遇到临时遮挡或漏检，依然能画出平滑的轨迹。\n三、全新标尺：怎么量化\u0026quot;稳不稳\u0026rdquo;？ 传统指标的局限性 以前的评价指标只看\u0026quot;当前帧准不准\u0026quot;，不看\u0026quot;前后帧连不连贯\u0026quot;。\nTPC（轨迹预测一致性）定义 轨迹预测一致性（Trajectory Prediction Consistency）：在重叠的时间段内，计算\u0026quot;当前预测的轨迹\u0026quot;和\u0026quot;上一帧预测的轨迹\u0026quot;之间的平方差，然后再用真实的验证集轨迹做掩码过滤。\n$$TPC = \\frac{1}{N}\\sum_{t}||T_{pred}^{t} - T_{pred}^{t-1}||^2 \\cdot Mask$$\nTPC越低，说明车开得越平顺，乘客越不容易晕车！\n四、狂飙的成绩单 4.1 首创\u0026quot;弯道数据集（Turning-nuScenes）\u0026quot; 作者发现原来的nuScenes数据集里直道太多了，看不出谁更稳。于是专门把\u0026quot;转弯\u0026quot;的场景挑出来做测试（转弯最容易方向盘发抖）。\n惊人数据（Table 3）：\n预测未来6秒的轨迹，MomAD相比之前的SOTA模型（SparseDrive），碰撞率暴降了26% TPC指标大幅优化了0.97米（提升33.45%） 4.2 长时间预测远超同行 预测1-2秒不算啥，预测4-6秒还能保持稳定才叫牛。随着时间拉长，其他模型误差飙升，而MomAD依然保持着极高的平滑度。\n4.3 闭环路测（Bench2Drive） 在CARLA模拟器里跑闭环（Table 4），包含超车、避让等44个交互场景：\n成功率提高了惊人的16.3% 舒适度提升了7.2% 4.4 消融实验亮点 历史帧数消融（Table 7）：\n历史帧不是越多越好。作者发现融合前 $t=2$ 帧的历史信息时，效果达到巅峰；如果融合 $t=3$ 帧，反而因为历史太久远引入了不确定性，导致效果下降。这叫\u0026quot;恰到好处的记忆\u0026quot;。\n噪声注入消融（Table 6）：\n加了Robust Instance Denoising模块后性能稳步提升，验证了训练时加噪对抵抗感知闪烁的有效性。\n五、深度讨论：历史意图引入的\u0026quot;因果混淆\u0026quot;陷阱 一个极其犀利的专家级问题 引入历史意图（也就是上一帧的Query或者轨迹）虽然能让车开得\u0026quot;稳\u0026quot;，但如果不加限制，模型会变成一个\u0026quot;闭着眼睛开车的瞎子\u0026quot;。\n三大致命因果混淆陷阱 陷阱 现象 灾难后果 因果倒置（惯性覆盖） \u0026ldquo;上一秒我在直行 → 所以这一秒我继续直行\u0026rdquo; 无视前方突然出现的外卖小哥，直接撞上去 捷径退化（恒等映射） 网络发现直接复制上一帧输出Loss就够低 感知模块退化，变成只会根据昨天猜今天的时间序列外推器 误差雪球（蝴蝶效应） 第1帧的小误判被后续帧不断放大 即使视野恢复清晰，模型也因为历史执念拉不回来 MomAD的破局之道 1. 架构约束：用交叉注意力逼迫模型\u0026quot;看路\u0026quot; 历史意图（提纯后的 $Q_{past}$）作为Key和Value 当前帧的初筛意图作为Query 融合后的Query必须再和当前帧最新的感知实例特征（$F_{ins}$）做二次深度融合 原理：历史只提供\u0026quot;底色\u0026quot;，生杀大权依然牢牢掌握在当前帧的感知特征手里。\n2. 训练秘籍：历史Dropout 随机以一定概率把历史意图置为空，强制模型只凭当前帧的单帧图像去做规划。\n原理：斩断捷径，让网络明白\u0026quot;不能总是指望抄前一秒的作业\u0026quot;。\n3. 施加微扰：打破舒适区 Robust Instance Denoising实际上是一种因果干预，告诉模型要在噪声和历史之间找到真正的因果平衡点。\n4. 终极验证：闭环评测 如果模型严重因果混淆，在闭环里不出10秒就会撞树。MomAD在Bench2Drive闭环测试中成功率提升16.3%，是克服因果混淆的最硬核证据。\n六、为什么在\u0026quot;Query层面\u0026quot;融合历史？ 问题本质 为什么MomAD选择融合历史Query，而不是直接把前后两帧的感知实例（几十个框）融合在一起？\n直接融合感知实例的三大致命问题 1. 目标关联灾难 如果把 $t-1$ 帧和 $t$ 帧的实例特征直接堆叠，网络怎么知道上一帧的\u0026quot;框A\u0026quot;和这一帧的\u0026quot;框B\u0026quot;是同一辆车？\nMomAD的高明之处：不在\u0026quot;环境（框）\u0026ldquo;层面做跨帧融合，而在\u0026quot;自我意图\u0026quot;层面做融合，巧妙地绕开了显式的多目标跟踪难题。\n2. 信息过载与模式崩溃 把太多原始的环境特征直接丢给Planning Head，网络往往偷懒：\n学会直接忽略历史特征，退化回单帧模型 或者预测出不左不右、直接撞墙的\u0026quot;平均轨迹\u0026rdquo; MomAD通过TTM强制进行\u0026quot;信息漏斗\u0026quot;过滤：只把\u0026quot;你上一秒最想干的那件事\u0026quot;通过Query塞进去。\n3. 缺乏\u0026quot;主观动量\u0026quot; 动量是属于主体的，不是属于环境的。只有交互历史Query，网络才能记住\u0026quot;我原本打算干什么\u0026quot;。\n七、交叉注意力的工作机制 信息流动全景图 当前Query（我想怎么走） --查询--\u0026gt; 历史Key/Value（我过去怎么打算） ↓ 融合后的Query（吸收历史动量的\u0026#34;金丹\u0026#34;） ↓ 结合当前感知特征(F_ins) ↓ Planning Head再次裂变，输出K条多模态轨迹 关键理解 被选中的Query是一个\u0026quot;时空锚点\u0026quot;，代表的是\u0026quot;自车当前最稳定、最符合物理惯性的综合运动状态\u0026quot;。\n\u0026ldquo;一生万物\u0026quot;的多模态再生：这唯一一个被历史记忆开过光的Query，在和实例特征结合后，会再次裂变输出K条多模态轨迹。\n八、潜在的失败模式 模式一：稀疏表示的\u0026quot;先天不足\u0026rdquo;——信息漏斗带来的强制失忆 稀疏Query架构的本质是一个\u0026quot;极度势利的安检员\u0026quot;。\n预定义本体的诅咒：模型在设计之初，工程师会给它规定好能提取几类Query（车辆、行人、自行车、车道线、斑马线）。\n边缘场景灾难：\n当空中指示牌出现在画面里时，因为\u0026quot;空中指示牌的文字和状态\u0026quot;不在预定义的Query类别里，这个\u0026quot;安检员\u0026quot;把它当作垃圾扔掉了 越过指示牌后，历史记忆中根本不存在\u0026quot;刚才有个牌子\u0026quot;这件事 Planning Head完全不知道这是一条特定时段只能左转的可变车道 模式二：稀疏实例特征的材质丢失 $F_{ins}$ 虽然是一个256维的隐式特征向量（包含纹理、反光度、边缘锐利度等信息），但：\n如果图像分辨率不够，或夜间噪点大，Deformable Attention没采样准 砖块和塑料袋的256维特征就会混淆 出于安全保守策略，自动驾驶车大概率会选择急刹或猛打方向盘避让那个塑料袋 这就是幽灵刹车问题的根源。\n九、行业趋势与未来方向 从\u0026quot;空间单帧感知\u0026quot;到\u0026quot;时空连续决策\u0026quot;的跨越 四大解题流派 流派 代表作 核心思想 流式架构 StreamPETR, SparseDrive 不存庞大的图像特征，只存上一帧\u0026quot;提纯后的结果\u0026quot; 世界模型 GAIA-1, MILE, DriveDreamer 不仅预测方向盘和油门，还逼着预测未来的世界长什么样 反事实推断 基于CARLA的闭环端到端 故意把车推向危险边缘，看模型怎么救回来 4D占据栅格 OccNet, UniOcc 在3D体素空间里加上时间维度，预测场景流 终极前沿：Mamba（状态空间模型） 比Transformer更适合处理无限长序列：\nTransformer算注意力机制，时间序列越长，算力呈 $O(N^2)$ 爆炸 Mamba可以在保持恒定显存占用的情况下，将历史感受野拉长到过去几十上百帧 进化方向 方向一：视觉-语言-动作大模型（VLA + LLM）\n用大语言模型来记忆语义信息，用文本/概念来做记忆，而不是用几何框做记忆。\n方向二：混合记忆架构\n对动态物体用高效的Sparse Query，同时维护一个低分辨率的Dense BEV/Occupancy记忆网格作为兜底。\n十、总结 《Don\u0026rsquo;t Shake the Wheel》这篇论文极其精准地抓住了端到端自动驾驶\u0026quot;缺乏时序一致性\u0026quot;的命门：\nTTM（豪斯多夫拓扑匹配）：保证动作的连贯 MPI（交叉注意力记忆融合）：拓展视野的深度 TPC新指标：重新定义什么是\u0026quot;好\u0026quot;的自动驾驶 通过\u0026quot;Query与特征的强制二次校验（架构）\u0026quot; + \u0026ldquo;加噪与阻断（训练策略）\u0026quot;，在保证轨迹平滑的同时，保住了模型对突发危险的敬畏之心。\n下一次，当你坐在一辆变道丝滑、转弯稳当的自动驾驶汽车里时，它的算法底层，或许就闪烁着这种\u0026quot;动量感知（Momentum-Aware）\u0026ldquo;的智慧光芒！\n十一、深度追问：只输入历史环境特征能避免捷径学习吗？ 一个极具诱惑力的假设 既然输入历史轨迹或历史运动状态都会导致模型走捷径，那是不是意味着：我不直接输入历史的轨迹结果，也不输入历史的运动状态，只输入历史的环境特征，就不会导致模型走捷径的行为？\n这是一个非常敏锐的假设，逻辑看似完美：既然网络是个\u0026quot;偷懒的作弊狂\u0026rdquo;，那我干脆把\u0026quot;答案（历史轨迹）\u0026ldquo;和\u0026quot;公式（历史物理速度）\u0026ldquo;全给它藏起来，只给它看\u0026quot;历史风景（环境特征）\u0026quot;，逼着它每一帧都自己重新做题，这样不就能彻底根治\u0026quot;捷径学习（Shortcut Learning）\u0026ldquo;了吗？\n残酷的真相：神经网络依然有 100 种方法找到捷径！ 即使只输入历史的环境特征，神经网络依然能找到隐蔽的作弊路径：\n捷径一：化身\u0026quot;视觉里程计\u0026rdquo;，反向推导隐藏数据 作弊手法： 当你把 $t-1$ 和 $t-2$ 的环境特征（周围的树木、静止的建筑物、车道线）喂给模型时，网络内部的 Self-Attention 或 3D 卷积会瞬间计算出这些静态物体在相邻两帧之间的像素位移。 物理学的降维打击： 如果一棵树在 0.1 秒内向后移动了 1 米，网络根本不需要你告诉它速度，它自己就能计算出\u0026quot;自车速度是 10 m/s，且没有打方向盘\u0026rdquo;。 最终结果： 网络在几层隐藏层之后，原封不动地把你辛苦藏起来的\u0026quot;自车历史状态和轨迹\u0026quot;重新还原了出来！ 捷径二：患上\u0026quot;领头羊依赖症\u0026rdquo; 作弊手法： 训练数据集里，绝大部分时间车都是在跟车行驶。网络发现：\u0026ldquo;我只要死死咬住正前方那辆车，它的位置就是我的轨迹！\u0026rdquo; 致命灾难： 一旦前车是个闯红灯的疯子，你的模型会因为极度依赖\u0026quot;前车环境特征\u0026quot;而毫无判断力地跟上去，直接车毁人亡。 捷径三：\u0026ldquo;静态环境假象\u0026quot;导致的感知休眠 作弊手法： 网络发现 $t-1$ 的环境和 $t$ 的环境有 99% 是一模一样的，何必费劲去处理当前帧的那 1% 的变化？ 致命灾难： 这就是为什么很多只输入环境特征的模型，在遇到\u0026quot;鬼探头\u0026quot;时反应总是慢半拍。 核心结论 解决因果混淆，不能靠\u0026quot;堵（隐藏信息）\u0026quot;，只能靠\u0026quot;疏（机制约束）\u0026ldquo;和\u0026quot;骗（对抗训练）\u0026quot;！\n只要输入序列包含时间维度，神经网络就一定能复原出物理运动学捷径。因此，现代端到端自动驾驶的哲学已经变成了：我不怕你知道历史，我怕你沉迷历史。\n十二、Cross-Attention 的数学本质：为什么\u0026quot;当前为主，历史为辅\u0026rdquo;？ 权力的游戏：谁掌握 Query，谁就掌握\u0026quot;生杀大权\u0026rdquo; 在 Cross-Attention 的公式中： $$Attention(Q, K, V) = Softmax(\\frac{Q K^T}{\\sqrt{d}}) V$$\n这三个字母的地位是绝对不平等的：\nQuery (Q) 是\u0026quot;带资进组的甲方/大老板\u0026rdquo;：它是主动方，带着当下的需求去数据库里发起检索。 Key (K) 和 Value (V) 是\u0026quot;被动的资料库/顾问\u0026quot;：它们只能安静地躺在那里，等待被点名。 在 MomAD 的架构中：\n当前帧提取的初筛意图 = Query (大老板)：代表\u0026quot;我此时此刻眼前看到的战况\u0026quot; 历史意图 = Key/Value (历史顾问)：代表\u0026quot;我过去几秒钟的惯性和计划\u0026quot; 致命对比：如果用 Concat 或 Add 会怎样？ 假设网络设计为：最终特征 = MLP(当前特征 + 历史特征)\n网络的作弊手段： 网络会在 MLP 权重矩阵里，给\u0026quot;当前特征\u0026quot;分配极低权重（$0.01$），给\u0026quot;历史特征\u0026quot;分配极高权重（$0.99$）。 灾难结果： 不管当前帧发生什么，历史惯性都会强行碾压当前视觉，这就是典型的捷径学习。 Cross-Attention 凭什么能防作弊？ 因为它的融合是乘法约束（Dot Product $Q \\cdot K^T$），而不是加法线性组合！\n如果当前帧 $Q$ 看到前方空旷，历史 $K$ 也是直行，内积很大，历史 $V$ 被顺利吸收。 关键时刻： 如果当前帧 $Q$ 突然看到窜出来的行人，大老板 $Q$ 的特征向量会瞬间变成\u0026quot;紧急避让\u0026quot;。此时，\u0026ldquo;紧急避让的 $Q$\u0026rdquo; 和\u0026quot;历史直行的 $K$\u0026quot; 方向完全不一致，它们的内积会接近于 0！ 物理绞杀： 经过 Softmax 后，这个历史顾问的权重会被死死地压在 $0.00001$，历史信息直接被乘法物理清零！ 反向作死实验：如果把\u0026quot;历史\u0026quot;作为 Query 会怎样？ 这被称为**\u0026ldquo;确认偏误的架构放大器\u0026rdquo;**！\n历史（大老板）拿着\u0026quot;左转\u0026quot;的 $Q$，去当前帧的画面里死抠能支持它左转的证据。 哪怕当前画面里有一辆大卡车挡住了左转道，历史 $Q$ 也会对大卡车视而不见，只把注意力放在远处的左转红绿灯上，最终导致直接撞上卡车。 十三、模型的走捷径方法讨论 问题 有没有可能模型让当前帧的 Query变成一个无脑接受历史信息的傀儡，从而实现拷贝历史的捷径 ？ 答案 Cross-Attention机制下理论上不可行。因为为了让V完全被采纳，我们需要构造出一个和历史K的内积为单位阵的Q，但是我们在构造当前帧的Q时，没有提供任何关于历史的信息，所以它没办法和历史K“串供”。所以理论上通过Cross-Attention去融合历史信息是可以防止模型走捷径照抄历史的。 但是如果是通过concat和mlp去获取历史信息，模型只需要把mlp网络中对应到历史信息的那部分权重调到0.99即可。所以concat+mlp是个很糟糕的融合历史的设计，很容易让模型走捷径。 十四、为什么 Dropout 和扰动仍然必不可少？ 高级偷懒法：基于统计先验的\u0026quot;盲猜串供\u0026quot; 虽然 Cross-Attention 切断了 Q 提前看 K 的通道，但模型发现一个宇宙级统计规律：\n自动驾驶的长尾诅咒： 汽车 95% 的时间都在沿着车道线匀速直行。 K 的高度同质化： 95% 的训练样本里，历史 $K$ 长得几乎一模一样（都是\u0026quot;直行\u0026quot;向量）。 Q 的终极摆烂策略： 把所有 $Q$ 都无脑映射成能和\u0026quot;直行K\u0026quot;完美匹配的形状，不就能拿 95% 的高分吗？ 这叫\u0026quot;没有串供，但达成了默契\u0026quot;。 这种偷懒比直接改 MLP 权重更难被发现！\n为什么必须上\u0026quot;酷刑\u0026quot;？ 既然 Cross-Attention 无法阻止模型利用统计先验\u0026quot;盲猜\u0026quot;，就必须动用物理手段（拔网线和给历史下毒），打破这个 95% 的稳定预期：\n第一道金牌：信息隔离墙（不准串供） 在生成当前候选 Query 时，网络绝对接触不到历史状态！它只能从当前帧的图像像素里提特征，强行生成一组代表当前意图的 Query。只有当这组无法作弊的 Query 生成完毕后，大门才打开，让它去和历史做 Cross-Attention。\n第二道金牌：历史 Dropout（拔网线） 对付\u0026quot;摸鱼大师\u0026quot;最好的办法，就是时不时抽查它的真本事。\n在训练迭代中，设置一个概率（比如 30%），强行把传入的历史特征（Key 和 Value）全部清零。 当网络正准备\u0026quot;无脑抱历史大腿\u0026quot;时，突然发现大腿没了！Loss 直接爆炸。 为了在\u0026quot;断电时刻\u0026quot;活下来，网络被迫疯狂压榨当前帧的图像编码器，让它必须具备极强的单帧看路能力。 第三道金牌：给历史\u0026quot;下毒\u0026quot;（MomAD 的灵魂——Robust Instance Denoising） 既然网络喜欢\u0026quot;无脑信任历史\u0026quot;，那我就故意让历史变得不可信。\n在训练阶段，给输入的特征人为加上高斯噪声。 假设上一帧真实情况是\u0026quot;直行\u0026quot;，但扰动让历史顾问传递出\u0026quot;我要向左猛打方向盘\u0026quot;的错觉信号。 网络经过成千上万次\u0026quot;受骗\u0026quot;与\u0026quot;惩罚\u0026quot;，终于悟出：\u0026ldquo;历史顾问经常发神经，我必须坚决相信我这双眼睛看到的真相！\u0026rdquo; 十五、终局总结：架构防\u0026quot;小人\u0026quot;，训练防\u0026quot;懒汉\u0026quot; 防御层级 方法 防住什么 防不住什么 Concat + MLP 无防御 - 一切捷径 Cross-Attention (只用架构) 关上了串供的门 直接改参数躺平 统计先验盲猜 Cross-Attention + Dropout + 扰动 关门 + 放狗 + 拆门 几乎所有捷径 - 真正的护城河是组合拳： 物理隔离（不能提前偷看）+ 拔网线（History Dropout）+ 钓鱼执法（加噪声微扰）。\n这三招齐下，才把端到端自动驾驶网络，从一个\u0026quot;只会复读的录音机\u0026quot;，逼成了一个\u0026quot;既懂历史规律、又对当下极其警惕的老司机\u0026quot;！\n","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/mom-ad/","title":"MomAD: Momentum-Aware Planning in End-to-End Autonomous Driving"},{"content":" 🎯 一句话概括 把自动驾驶的多智能体轨迹预测，变成一场\u0026quot;文字接龙\u0026quot;游戏——用语言模型预测下一个动作词的方式，来预测车辆和行人的未来轨迹。\n🌟 核心洞察：马路上的\u0026quot;聊天室\u0026quot; 想象一下，繁忙的十字路口就像是一个喧闹的**\u0026ldquo;大型聊天室\u0026rdquo;**。汽车、自行车、行人都在用他们的肢体语言和移动轨迹进行着高频的\u0026quot;对话\u0026quot;——\u0026ldquo;我要变道了\u0026rdquo;、\u0026ldquo;你先走\u0026rdquo;、\u0026ldquo;我要加速了\u0026rdquo;。\n既然这些交互如此像人类的语言交流，Waymo 的研究员们脑洞大开：为什么不直接用大语言模型（LLM，比如 ChatGPT 的底层逻辑）的方式，来预测这些车辆和行人的未来轨迹呢？\n于是，MotionLM 诞生了。它抛弃了传统轨迹预测中那些繁琐的设定，直接把多智能体轨迹预测（Multi-Agent Motion Prediction）变成了一场\u0026quot;文字接龙\u0026quot;游戏。\n🔧 技术实现详解 1. 核心魔法：把\u0026quot;连续的轨迹\u0026quot;变成\u0026quot;离散的单词\u0026quot; 以前的模型在预测轨迹时，通常是在预测连续的坐标点（x, y）。但 MotionLM 说：\u0026ldquo;不，我要把它变成词汇表！\u0026rdquo;\n研究团队把连续的轨迹坐标转换成了离散的运动 Token（Discrete Motion Tokens）。这就好比把一段连续的路线切成了一个个特定的\u0026quot;动作单词\u0026quot;。\n技术收益： 这样一来，模型在每一个时间步预测下一步去哪，就不再是复杂的回归任务了，而是变成了一个纯粹的分类任务。直接在网络最后加上一个标准的 Softmax 层，算出下一个\u0026quot;动作单词\u0026quot;的概率分布即可，简单粗暴且极其有效。\nTokenization 实现细节 class MotionTokenizer: def __init__(self, vocab_size, grid_range): self.vocab_size = vocab_size # 例如 128x128 个网格类别 + 特殊 Token self.grid_range = grid_range # 物理位移极限，例如 [-18m, 18m] self.ZERO_ACTION_TOKEN = 0 # 特殊单词：代表\u0026#34;保持匀速直线运动\u0026#34; def encode(self, continuous_traj): \u0026#34;\u0026#34;\u0026#34; 输入: continuous_traj 形状[Agent数量, 时间步T, 2(x,y)] 输出: discrete_tokens 形状 [Agent数量, 时间步T] \u0026#34;\u0026#34;\u0026#34; tokens = [] # 计算每一步的位移变化 (Delta x, Delta y) displacements = compute_diff(continuous_traj) for t in range(T): if t \u0026gt; 0 and is_almost_equal(displacements[:, t], displacements[:, t-1]): # 核心魔法：如果当前速度/位移跟上一步一样，直接输出\u0026#34;零动作\u0026#34;单词 token = self.ZERO_ACTION_TOKEN else: # 否则，将 映射到均匀划分的离散网格中，得到对应的类别ID token = self.quantize_to_grid(displacements[:, t], self.grid_range) tokens.append(token) return torch.stack(tokens) 关键参数设定：\n预测频率：2Hz（每 0.5 秒预测一步） 位移范围：$[-18.0m, 18.0m]$ 网格数量：128 个 Bin 二维位移映射成离散的类别组合（如 $13 \\times 13 = 169$ 个核心动作 Token） 神来之笔——Verlet 积分技巧： 这是一个非常取巧的细节！由于真实的车辆和行人具有惯性，速度通常是平滑过渡的。MotionLM 设计了一个特殊的**\u0026ldquo;零动作 Token\u0026rdquo;。如果模型输出这个 Token，它的意思不是\u0026quot;停下\u0026quot;，而是\u0026ldquo;保持上一个时间步的相对位移（即匀速直线运动）\u0026rdquo;**。这极大压缩了有效词汇表的复杂度，让模型更容易学到平滑的轨迹。\n💡 Verlet 技巧的物理含义： 这相当于在词表里直接内嵌了牛顿第一定律（惯性）！模型不需要去费力学习最基础的运动学平滑性，可以把宝贵的网络容量用来学习更高级的场景理解。\n2. 扔掉历史包袱：无需锚点或隐变量 在自动驾驶中，未来的可能性是多样的（Multimodal distributions，比如到了路口可能左转、直行或右转）。过去为了让模型学会这种\u0026quot;多种可能性\u0026quot;，工程师们必须绞尽脑汁地设计预定义的\u0026quot;锚点轨迹\u0026quot;，或者使用非常复杂的\u0026quot;显式潜在变量优化\u0026quot;。\nMotionLM 直接掀桌子了！ 它根本不需要这些复杂的设定。它只用了一个最标准、最经典的语言模型目标函数——最大化序列 Token 的平均对数概率。就像 ChatGPT 预测下一个词一样，它通过海量数据的自回归训练，自然而然地就学会了所有可能的未来轨迹分布。\n3. 网络架构：Transformer 驱动的\u0026quot;听与说\u0026quot; MotionLM 的骨架是一个经典的 Transformer 架构，分为两大部分：\n场景编码器 任务： \u0026ldquo;察言观色\u0026rdquo;。采用早期融合网络的设计。\n输入大杂烩：\n矢量化的高精地图 红绿灯的实时状态和历史序列 目标智能体和周围所有其他车辆/行人的历史轨迹 输出： 经过深度 Transformer 编码，这些异构数据被融合压缩，输出一个带有极强空间和语义上下文的 Scene Embeddings。形状为 $R \\times N \\times \\dots \\times H$，其中 $R$ 是 Rollout 数量，$N$ 是联合建模的智能体数量，$H$ 是维度。\n这就像是给了模型一个\u0026quot;当前棋局的高清快照\u0026quot;。\n联合轨迹解码器 这是一个自回归解码器。它一边通过交叉注意力时刻盯着场景编码器给出的环境信息，一边通过自注意力关注各个智能体已经生成的运动 Token，然后一口气为多个智能体生成接下来的 $T$ 个动作 Token。\nclass MotionLM(nn.Module): def __init__(self, num_agents, vocab_size, embed_dim): super().__init__() self.num_agents = num_agents # 1. 场景编码器 (借用 Wayformer 的 Early Fusion 架构) self.scene_encoder = WayformerEncoder(embed_dim) # 2. 各种 Embedding 层 (为\u0026#34;单词\u0026#34;赋予意义) self.token_embed = nn.Embedding(vocab_size, embed_dim) # 动作值编码 self.time_embed = nn.Embedding(MAX_TIME_STEPS, embed_dim) # 时间位置编码 self.agent_embed = nn.Embedding(num_agents, embed_dim) # 智能体身份编码 # 3. 标准的 Transformer 解码器 self.transformer_decoder = nn.TransformerDecoder( decoder_layer=nn.TransformerDecoderLayer(d_model=embed_dim, nhead=8), num_layers=6 ) # 4. 输出头：预测下一个单词的概率分布 self.lm_head = nn.Linear(embed_dim, vocab_size) def forward(self, map_data, traffic_lights, history_traj, target_tokens): B, N, T = target_tokens.shape # 第一步：察言观色 scene_memory = self.scene_encoder(map_data, traffic_lights, history_traj) # 第二步：准备要接龙的\u0026#34;单词\u0026#34;序列（三种 Embedding 逐元素相加） val_emb = self.token_embed(target_tokens) time_emb = self.time_embed(torch.arange(T)) agent_emb = self.agent_embed(torch.arange(N)) combined_emb = val_emb + time_emb + agent_emb # 第三步：降维打击 (Flattening) - 把所有智能体在所有时间的 Token 拉平 flattened_sequence = combined_emb.view(B, N * T, -1) # 第四步：因果掩码（确保 t 时刻的预测只能看到 t-1 及以前的所有人的动作） causal_mask = generate_agent_time_causal_mask(N, T) # 第五步：Transformer 解码 decoder_out = self.transformer_decoder( tgt=flattened_sequence, memory=scene_memory, tgt_mask=causal_mask ) # 第六步：输出预测 logits = self.lm_head(decoder_out) return logits 输入嵌入三合一： 每一个输入解码器的 Token，由三个向量逐元素相加组成：\n动作本身的值嵌入（Value Embedding，比如\u0026quot;向左偏一点\u0026quot;） 时间位置编码：告诉模型现在预测的是未来第几秒 智能体身份编码：告诉模型这个动作是属于车辆 A 还是行人 B 全家桶式\u0026quot;展平自注意力\u0026quot;： 过去很多模型会分别算\u0026quot;时间轴上的注意力\u0026quot;和\u0026quot;智能体之间的注意力\u0026quot;。MotionLM 嫌麻烦，直接把所有智能体在所有时间步的 Token 拉平成一条长长的超级序列。在算自注意力时，只通过严格的因果掩码来限制：任何人在 $t$ 时刻的动作，只能参考自己和其他人 $t-1$ 时刻及以前的动作。绝对禁止\u0026quot;穿越\u0026quot;看未来，保证了严格的时序因果关系。\n4. 降维打击：单次自回归生成\u0026quot;联合分布\u0026quot; 这是 MotionLM 最引以为傲的一点。\n过去的主流做法往往是\u0026quot;事后诸葛亮\u0026quot;：先让每个智能体各顾各地生成几条边缘轨迹，然后再用启发式算法打分，看看它们会不会撞在一起。\nMotionLM 通过单一的自回归解码过程，直接输出所有交互智能体未来的联合分布。大家在每一步生成时都在互相\u0026quot;看着\u0026quot;对方，完全符合真实世界里大家边走边互相博弈的逻辑。\n5. 时序因果的条件推演 因为 MotionLM 在时间序列上是严格的时序因果分解——即未来的动作严格依赖过去的动作，它解锁了一个超强的功能：条件推演。\n这意味着你可以用它来做\u0026quot;如果\u0026hellip;那么\u0026hellip;\u0026ldquo;的沙盘推演。比如你可以在解码时，强行给车辆 A 设定一个动作（\u0026ldquo;如果 A 突然急刹车\u0026rdquo;），模型就能根据这个因果关系，顺滑地推演出后面跟着的车辆 B、C、D 会做出什么样的反应。这对于自动驾驶的规划系统来说，简直是梦寐以求的神器。\n6. 训练：极简的交叉熵损失 def train_step(model, batch_data, optimizer): model.train() optimizer.zero_grad() # 提取并分词真实未来的轨迹 ground_truth_traj = batch_data[\u0026#39;future_traj\u0026#39;] tokenizer = MotionTokenizer() target_tokens = tokenizer.encode(ground_truth_traj) # 前向传播 (Teacher Forcing 模式) input_tokens = shift_right(target_tokens, pad_value=\u0026#39;\u0026lt;BOS\u0026gt;\u0026#39;) logits = model(batch_data[\u0026#39;map\u0026#39;], batch_data[\u0026#39;tl\u0026#39;], batch_data[\u0026#39;hist\u0026#39;], input_tokens) # 【极致的极简主义】 # 抛弃 Huber, 抛弃 L2 距离，抛弃一切复杂的轨迹 loss # 只有最经典的交叉熵损失 loss_fn = nn.CrossEntropyLoss() loss = loss_fn(logits.view(-1, VOCAB_SIZE), target_tokens.view(-1)) loss.backward() optimizer.step() return loss.item() 核心： 丢掉一切复杂的轨迹回归损失函数（比如 Huber Loss、L2 距离）。整个庞大网络的训练目标只有一个极其纯粹的函数——标准的交叉熵损失，即最大化真实序列 Token 的平均对数概率。\n7. 推理与后处理 @torch.no_grad() def inference(model, scene_data, num_rollouts=512, top_k=6): model.eval() # 1. 编码场景 scene_memory = model.scene_encoder(...) scene_memory = scene_memory.repeat(num_rollouts, 1, 1) # 复制 512 份 # 2. 自回归解码 generated_tokens = torch.full((num_rollouts, N, 1), \u0026#39;\u0026lt;BOS\u0026gt;\u0026#39;) for t in range(MAX_TIME_STEPS): logits = model(scene_memory, generated_tokens) next_step_logits = logits[:, :, -1, :] # [Rollouts, N, Vocab] # 按概率分布采样（保证多样性） probs = torch.softmax(next_step_logits, dim=-1) next_tokens = torch.multinomial(probs.view(-1, VOCAB_SIZE), 1) next_tokens = next_tokens.view(num_rollouts, N, 1) generated_tokens = torch.cat([generated_tokens, next_tokens], dim=2) # 3. 翻译回坐标系 tokenizer = MotionTokenizer() continuous_rollouts = tokenizer.decode(generated_tokens) # 4. 后处理提炼 final_trajs, final_probs = aggregate_rollouts(continuous_rollouts, top_k) return final_trajs, final_probs def aggregate_rollouts(rollouts, k=6): \u0026#34;\u0026#34;\u0026#34;使用 NMS 和 K-Means 聚类\u0026#34;\u0026#34;\u0026#34; distance_matrix = compute_pairwise_distances(rollouts) filtered_rollouts = apply_nms(rollouts, distance_matrix, threshold=2.0) kmeans = KMeans(n_clusters=k) cluster_centers = kmeans.fit(filtered_rollouts) cluster_probs = compute_cluster_probabilities(kmeans.labels_) return cluster_centers, cluster_probs 推理狂飙： 在给定的场景下，模型会平行推演出 512 条不同的未来宇宙。因为是按照概率分布采样的，所以有的宇宙里车辆 A 抢行了，有的宇宙里车辆 A 刹车让行了。\n后期提炼： 面对几百条推演出来的联合轨迹，MotionLM 引入了 NMS（非极大值抑制） 结合 K-Means 聚类 算法，最终聚类出 6 个截然不同的核心\u0026quot;模式\u0026rdquo;，并根据每个簇包含的样本数量给出置信度概率。\n模型集成： 为了拿榜单第一，Waymo 还把几个独立训练的 MotionLM 模型集成在一起同时做 Rollout，利用认知不确定性让生成的聚类结果更稳固、更可靠。\n📊 核心张量维度解析 next_step_logits 的形状：[num_rollouts, N, vocab_size]\n维度 含义 示例值 num_rollouts \u0026ldquo;平行宇宙\u0026quot;数量 512 N 联合建模的智能体数量 8 vocab_size 动作词汇表大小 169 🤔 深度讨论：简单 Loss 的底气 为什么只有交叉熵损失就够了？ 你可能会担心：监督信号会不会太稀疏？模型会不会只是在\u0026quot;瞎猜盲盒\u0026rdquo;，根本不懂物理规律和交通规则？\nWaymo 的研究员们敢这么做，底气来自于四大杀手锏：\n1. 交叉熵不仅不稀疏，反而是\u0026quot;极其密集\u0026quot;的时序监督 传统模型的回归 Loss 往往只在轨迹终点或几个关键点算一次 L2 距离。而 MotionLM 的交叉熵是在每一个时间步、为每一个智能体都在做惩罚和奖励！\n如果一辆车未来有 80 个时间步，旁边有 8 辆车 传统方法：几个关键点的 Loss MotionLM：$80 \\times 8 = 640$ 个节点的步步紧逼核对 这种\u0026quot;沿途的每一步都在纠错\u0026quot;的机制，提供的梯度信号实际上比传统的回归 Loss 还要密集和强劲。\n2. \u0026ldquo;大力出奇迹\u0026rdquo;：用海量真实数据倒逼出物理规则 这是 ChatGPT 震惊世界的底层逻辑，也是大名鼎鼎的**\u0026ldquo;苦涩的教训\u0026rdquo;**：与其让人类专家去写复杂的物理规则，不如让模型自己从海量数据里去悟。\nWaymo Open Motion Dataset (WOMD) 包含了数以千万计的真实人类驾驶轨迹 人类司机的真实轨迹，本身就完美包含了所有的物理规律和交通规则 核心逻辑： 如果 MotionLM 只是死记硬背或者瞎猜，它在这么庞大且复杂的数据集上，交叉熵 Loss 绝对降不下来。它为了把 Loss 降到最低，唯一的出路就是——被迫在神经网络的参数里，内化这些物理法则和几何约束。 3. 交叉注意力的强制绑定 模型怎么知道哪是马路、哪是墙？全靠网络架构的\u0026quot;强制看图\u0026quot;机制。\n如果模型预测一辆车要\u0026quot;向左偏\u0026quot;，但地图特征显示左边是一堵墙 交叉熵误差会飙升，梯度顺着 Cross-Attention 的权重一路回传 模型被迫修正对地图的理解：地图的几何约束被隐式地刻进了注意力权重里 4. \u0026ldquo;联合序列\u0026quot;倒逼出博弈与交互理解 MotionLM 把所有车辆的动作拉平成一条长序列。当模型在预测 Agent B 第 3 秒的动作时，它的输入序列里已经包含了 Agent A 在前 2 秒的动作（比如 A 正在加速抢道）。\n为了降低预测误差，模型的 Self-Attention 机制被迫学会了去关注序列前面其他车辆的动作。它自己领悟出了\u0026quot;当别人抢道时，我必须减速\u0026quot;的因果交互逻辑。\n简单 Loss 的陷阱 但是，简单的 Loss 是一项昂贵的特权。如果以下几个\u0026quot;地基\u0026quot;没打好，简单的 Loss 就会变成一场噩梦：\n陷阱一：Tokenization 的灾难 如果不加设计的暴力切分，模型会觉得下一个动作的跨度极大、毫无规律，预测会变成抛硬币。MotionLM 的 Verlet 积分技巧是救命稻草——把绝大多数常规的平滑行驶，全都归结为一个固定的 Token。\n陷阱二：感知噪音导致的\u0026quot;不可解之谜\u0026rdquo; 简单的交叉熵 Loss 极其依赖高质量、无歧义的输入上下文。如果感知数据有延迟，或者高精地图有几厘米的偏移，模型会把人类的合理驾驶行为当成\u0026quot;随机噪音\u0026quot;。\n陷阱三：注意力崩溃与维度灾难 序列长度一翻倍，计算复杂度和寻找规律的难度呈平方级爆炸。MotionLM 通过因果掩码和预先训练好的 Wayformer 场景编码器来破局——把繁杂的地图和历史信息提前压缩成凝练的 Scene Embeddings。\n🏆 最终战绩 MotionLM 在目前最权威、最硬核的自动驾驶预测数据集 WOMD 上大杀四方：\n🥇 交互挑战排行榜第一名 📈 联合平均精度均值（ranking joint mAP metric）提升 6% 💎 总结 《MotionLM》的迷人之处，在于它做了一次极其优雅的\u0026quot;跨界\u0026quot;。它证明了，不用再去死磕复杂的几何约束和物理方程，只要把连续的驾驶动作变成\u0026quot;词汇\u0026quot;，用语言模型\u0026quot;预测下一个词\u0026quot;的自回归降维打击，就能让自动驾驶汽车学会看懂马路上的这盘\u0026quot;大棋\u0026quot;！\n整套组合拳：\n把坐标变成\u0026quot;格点\u0026quot;（Tokenization） → 加入\u0026quot;保持惯性\u0026quot;的快捷词汇 → 用 Wayformer 吃透地图 → 把所有车、所有时间的动作拉平成一条序列做接龙 → 狂暴采样 500 次 → 聚类提炼出 6 条核心剧本\n🔗 相关论文 [[Wayformer - Waymo的早期融合场景编码器]] [[MultiPath++ - 多模态轨迹预测]] ","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/motion-lm/","title":"MotionLM: Multi-Agent Motion Forecasting as Language Modeling"},{"content":"核心贡献: 提出了一种基于物理先验的残差轨迹建模方法，通过归一化技术解决了端到端自动驾驶中的训练难题 一、核心动机：为什么要用残差？ 1.1 现有方法的问题 目前的端到端自动驾驶模型（E2EAD）大多试图回答同一个问题：\u0026quot;未来的轨迹是什么？\u0026quot; 它们直接从传感器数据预测车辆未来几秒钟的绝对坐标点 $(x, y)$。\n作者指出这样做有两个大坑：\n痛点一：虚假关联（Spurious Correlations） 就像考试死记硬背答案。比如模型看到前车刹车灯亮了，它学会了刹车，但它可能没理解是因为前面是红灯。如果只学绝对坐标，数据太复杂，模型容易\u0026quot;偷懒\u0026quot;学到错误的因果关系。\n痛点二：规划视野困境（Planning Horizon Dilemma） 预测未来很远的地方（比如4秒后）是非常难的，误差会很大。这导致模型在训练时，为了降低远处的巨大误差，反而忽略了近处（0.5秒后）那些对安全至关重要的小调整。\n1.2 ResAD 的核心思想：换个问法 ResAD 不再问\u0026quot;未来在哪里\u0026quot;，而是问：\u0026quot;为什么要改变轨迹？\u0026quot;\n形象理解：想象你在高速公路上开车。如果你什么都不做（不踩油门不打方向），车会顺着惯性继续滑行。这叫\u0026quot;物理先验\u0026quot;。\nResAD 的做法：\n先计算出这个\u0026quot;惯性路径\u0026quot; 然后只学习\u0026quot;由于路况（红灯、变道、避让）你需要做的修正量（Residual）\u0026quot; 好处：把很难的\u0026quot;画全图\u0026quot;任务，变成了简单的\u0026quot;找不同\u0026quot;任务。\n二、方法论核心：三大关键模块 2.1 归一化残差轨迹建模 (Normalized Residual Trajectory Modeling) 这是整个算法的灵魂，分为三个步骤：\n第一步：计算惯性参考 (Inertial Reference) 假设车辆保持当前的速度 $v_0$ 和方向不变，未来的位置在哪里？\n$$ \\mathbf{p}_{t_i} = \\mathbf{p}_0 + \\mathbf{v}_0 \\cdot \\Delta t_i $$\n$\\mathbf{p}_{t_i}$：未来 $t_i$ 时刻的预测位置 $\\mathbf{p}_0$：当前位置 $\\mathbf{v}_0$：当前速度向量 $\\Delta t_i$：时间差 解读：这是初中物理公式 $S = Vt$。这一步不需要神经网络，纯物理计算，非常稳。\n时间差的具体设定：\n数据采样频率：2 Hz（每秒钟记录/预测两次数据） 时间间隔 ($\\Delta t$)：0.5秒 总预测步数 ($T_f$)：8个时间步 总预测时长：8步 × 0.5秒/步 = 4.0秒 具体运算中，时间差 $\\Delta t_i$ 依次代入：\n第1个点（0.5秒后）： $\\Delta t_1 = 0.5$ 秒 第2个点（1.0秒后）： $\\Delta t_2 = 1.0$ 秒 \u0026hellip;以此类推\u0026hellip; 第8个点（4.0秒后）： $\\Delta t_8 = 4.0$ 秒 第二步：计算残差 (Residual) 神经网络只预测真实轨迹 $\\tau_{gt}$ 和惯性轨迹 $\\tau_{ref}$ 之间的差值：\n$$ \\boldsymbol{r} = \\tau_{gt} - \\tau_{ref} $$\n解读：$\\boldsymbol{r}$ 代表了人类驾驶员的\u0026quot;操作意图\u0026quot;（比如为了避让行人向左打方向）。模型只需要学这个意图，而不是学整个坐标。\n第三步：逐点残差归一化 (Point-wise Residual Normalization, PRNorm) 问题：远处的坐标偏差通常很大（比如几米），近处的偏差很小（几厘米）。如果直接训练，模型会只关注远处的大数字，忽略近处的安全细节。\n解决：把每个时间步的数值都缩放到同一个范围（比如 -1 到 1）：\n$$ \\tilde{r}^d_t = 2\\gamma \\left( \\frac{r^d_t - r^d_{\\min}}{r^d_{\\max} - r^d_{\\min} + \\epsilon_0} \\right) - \\gamma $$\n$r^d_{\\min}, r^d_{\\max}$：该时间步在整个数据集里的最大最小值 $\\gamma$：缩放系数（比如设为1） 解读：这是一个标准的 Min-Max 归一化，但关键在于它是**Point-wise（逐点）**的。也就是第1秒的数据只和第1秒的比，第4秒的和第4秒的比。这样，近处的小偏差在归一化后，权重就和远处一样大了，模型就不会忽略近处的安全了。\n2.2 惯性参考微扰 (Inertial Reference Perturbation, IRP) 自动驾驶需要考虑多种可能性（Multimodal），比如前面有车，我可以左变道，也可以右变道。\n做法：作者不在最终结果上加噪声，而是在初始速度上加噪声：\n$$ \\delta_{v,k} \\sim \\mathcal{N}(\\mathbf{0}, \\boldsymbol{\\Sigma}) $$ $$ \\mathbf{v}\u0026rsquo;_{0,k} = \\mathbf{v}0 + \\delta{v,k} $$\n$\\delta_{v,k}$：随机生成的微小速度/方向变化 $\\mathbf{v}\u0026rsquo;_{0,k}$：扰动后的新速度 效果：通过改变初始速度，生成一堆略有不同的\u0026quot;惯性轨迹簇\u0026quot;。就像奇异博士看了未来的几种可能性，有的快一点，有的偏左一点。这迫使模型去学习不同初速度下的应对策略，增加了鲁棒性。\n2.3 扩散解码器与打分 (Diffusion Decoder \u0026amp; Ranker) 生成轨迹 使用 Diffusion Model（扩散模型，类似于生成图片的 Stable Diffusion）：\n输入：图像特征 + 扰动后的惯性参考 + 噪声 输出：去噪后的归一化残差 最终轨迹 = 惯性参考 + 去归一化后的残差 打分器 (Ranker) 模型生成了 $K$ 条可能的轨迹，哪条最好？\n作者训练了一个 Transformer 来给每条轨迹打分（考虑安全性、舒适度等），选出分数最高的那条作为最终决策。\n三、深入讨论 3.1 惯性参考为什么是直线？ 基于公式 $\\mathbf{p}_{t_i} = \\mathbf{p}_0 + \\mathbf{v}_0 \\cdot \\Delta t_i$ 算出来的这8个点，连起来确实是一条直线。\n这其实是对**牛顿第一定律（惯性定律）**的最直接应用：假设驾驶员在这一刻\u0026quot;双手离开方向盘，双脚离开踏板\u0026quot;，车辆会保持当前的速度和方向，顺着切线方向一直往前滑行。\n既然真实道路是弯的，一条直线怎么开车呢？\n这恰恰是作者最聪明的设计。作者的逻辑是：\u0026ldquo;直线部分不需要 AI 学，物理定律已经帮你画好了；AI 只需要学怎么把这条直线\u0026rsquo;掰弯\u0026rsquo;。\u0026rdquo;\n直线（惯性参考）：车辆的本能（顺着当前方向冲） 残差（神经网络预测的偏移量）：驾驶员打方向盘和踩踏板的动作 最终轨迹 = 直线 + 残差 具体是怎么\u0026quot;掰弯\u0026quot;的？\n遇到弯道（打方向盘）：如果前面是个左转弯，AI 就会在第1到第8个点上，预测出越来越大的横向残差（向左的偏移量）。把原本直线上点，一点点往左边\u0026quot;拉\u0026quot;，连起来就成了一条完美的左转曲线。 遇到红灯（踩刹车）：此时方向不变（不需要横向偏移），但速度要减慢。AI 就会预测出纵向的负残差（向后的偏移量）。这样原本在直线远处的点，就会被\u0026quot;拉\u0026quot;回近处，表示车辆在4秒内走不了那么远，停下来了。 进阶理解：一把\u0026quot;扇形的直线\u0026quot;\n在复杂的路口，只给AI一条直线的参考，它可能很难思考对策。所以，作者在初始速度 $\\mathbf{v}_0$ 上加了随机噪声（微小扰动），生成了比如 10 个不同的初始速度。\n这样一来，这 10 个速度就会画出 10 条指向不同角度的直线，看起来就像一把打开的折扇：\n有的射线偏左，有的偏右，有的长（速度快），有的短（速度慢） AI 看到这把\u0026quot;折扇\u0026quot;，就会针对每一根\u0026quot;扇骨\u0026quot;（射线），预测出对应的残差，把它们分别\u0026quot;掰\u0026quot;成左转、直行、避障等多种可能的曲线轨迹 最后，Ranker（打分器）出场，选出最安全、最平滑的那条曲线作为最终决定 3.2 扩散模型中的噪声是什么？ 在算法的世界里，这里的\u0026quot;噪声\u0026quot;不是我们平时听到的噪音，它更像是一块**\u0026ldquo;未被雕琢的原材料\u0026rdquo;**。\n形象理解：从\u0026quot;迷雾\u0026quot;中看清真相 想象你在清晨的大雾中开车，由于雾太大，你看不清前方的路，只能看到一片混沌（这就是噪声）。\n噪声（Noise）：就是这团笼罩在未来轨迹上的\u0026quot;浓雾\u0026quot; 图像特征 + 惯性参考：这是你脑子里的\u0026quot;地图\u0026quot;和\u0026quot;指南针\u0026quot; 扩散解码器的任务：就是根据\u0026quot;地图\u0026quot;和\u0026quot;指南针\u0026quot;，把这团\u0026quot;浓雾\u0026quot;一点点拨开，最后露出隐藏在雾里的、最合理的残差轨迹 技术细节：它到底长什么样？ 在 ResAD 论文中，这个噪声在数学上被称为 高斯白噪声（Gaussian Noise）。\n数学表达：$z \\sim \\mathcal{N}(0, \\mathbf{I})$，意思是从一个均值为0、方差为1的标准正态分布中随机抽取出来的数值 它的形状：和我们要预测的轨迹是一模一样的（8个时间步 × 2个坐标 = 16个数字组成的矩阵） 起始状态：在生成的最开始（第 $T$ 步），模型手里只有这 16 个随机生成的乱码数字 为什么需要这个噪声？ A. 实现\u0026quot;多样性\u0026quot;（Multi-modality）\n自动驾驶面对同一个路口，可能有多种走法（稍微偏左一点，或者稍微偏右一点）。\n如果模型是死板的回归，它可能只会给出一个\u0026quot;平均值\u0026quot;，导致轨迹正好撞在路沿上 有了噪声，每次我们换一个不同的噪声\u0026quot;种子\u0026quot;（Seed），扩散解码器就能从不同的角度\u0026quot;拨开迷雾\u0026quot;，生成不同的合理轨迹 B. 逆向建模的需要\n扩散模型的工作原理是**\u0026ldquo;去噪\u0026rdquo;**：\n训练时：我们把真实的轨迹残差（正确的答案）里慢慢加入噪声，直到它变成一团乱码 推理时：我们给模型一团乱码，并告诉它当前的环境特征。模型会根据经验，一步步猜出：\u0026ldquo;这团乱码如果变回正确的轨迹，第一步该怎么减掉噪声？\u0026rdquo; C. 提高容错率\n直接预测一个精确的坐标点很难，但如果让模型去**\u0026ldquo;修正\u0026rdquo;**一个带有噪声的数值，它会有更多的容错空间。通过多次微调（去噪迭代），轨迹会变得越来越平滑、自然。\n3.3 为什么不从惯性参考开始直接修正？ 这是一个很自然的直觉：既然有了\u0026quot;惯性参考\u0026quot;这个物理先验，为什么不把它当做\u0026quot;初稿\u0026quot;直接修，非要从\u0026quot;随机噪声\u0026quot;开始白手起家？\n这涉及到了 \u0026ldquo;隐变量生成\u0026rdquo; 和 \u0026ldquo;确定性细化（Refinement）\u0026rdquo; 在数学建模上的本质差异。\n原因一：\u0026ldquo;残差空间\u0026quot;已经非常\u0026quot;扁平\u0026quot;了 地平线 DiffusionDrive 等方案，很多时候是在绝对坐标空间（Trajectory Space）进行打磨。因为绝对坐标范围很大（可能几十米），直接从噪声生成很难，所以用 Anchor（锚点路径）作为起始位置可以大幅缩短搜索路径。\n但请注意 ResAD 的神来之笔——PRNorm（归一化）：\nResAD 预测的不是坐标，而是归一化后的残差 $\\tilde{r}$ 通过 PRNorm，所有的预测目标都被强行压缩到了 $[-1, 1]$ 这个极小的、以 0 为中心的超立方体空间里 标准高斯噪声的分布范围主要也在 $[-1, 1]$ 附近 这意味着：在 ResAD 的残差空间里，\u0026ldquo;随机噪声\u0026quot;和\u0026quot;最终答案\u0026quot;之间的距离，已经比绝对坐标空间近得多了！\n原因二：解决\u0026quot;多模态\u0026quot;的数学严谨性 如果从惯性参考开始修（确定性细化）： 这本质上是一个函数映射：$f(\\text{惯性参考}, \\text{环境特征}) = \\text{修正量}$。对于同一个路口，这个映射往往只能给出一个确定的结果。\n从噪声开始生成（生成式建模）： 扩散模型是在学习整个概率分布 $P(\\text{残差} | \\text{环境, 惯性})$。从噪声 $z$ 开始，是因为 $z$ 是概率分布的\u0026quot;采样源\u0026rdquo;。噪声不是负担，而是\u0026quot;可能性\u0026quot;的源泉。\n原因三：惯性参考是作为\u0026quot;条件约束\u0026quot;存在的 在 ResAD 的 Transformer Decoder 里：\n惯性参考（Inertial Reference）是作为\u0026quot;Condition（条件约束）\u0026ldquo;输入的 这就像是一个经验丰富的老师站在 AI 旁边。AI 手里拿着一块乱糟糟的泥巴（噪声），老师不断告诉它：\u0026ldquo;根据现在的惯性和前面的红灯，你应该把泥巴捏成一个向后拉伸的形状。\u0026rdquo; 3.4 两次多模态：IRP 与 扩散噪声的关系 确实引入了两次\u0026quot;随机性\u0026rdquo;，但它们是**\u0026ldquo;物理层面的多模态\u0026rdquo;与\u0026ldquo;决策层面的多模态\u0026rdquo;**的强强联手。\n初始速度扰动 (IRP)：解决\u0026quot;身体\u0026quot;的不确定性 它在干什么？ 模拟车辆状态观测的误差 它的角色（物理先验）：为模型提供多个**\u0026ldquo;赛道（Anchor Paths）\u0026rdquo;** 扰动 A：假设我现在的速度比传感器测得的快一点，惯性参考线就长一点 扰动 B：假设我现在的方向盘其实已经往左带了一点，惯性参考线就偏左一点 总结：IRP 是在物理空间里撒网，它决定了 AI \u0026ldquo;起跑\u0026quot;的基础姿态 扩散随机噪声：解决\u0026quot;灵魂\u0026quot;的多模态（决策意图） 它在干什么？ 模拟面对复杂环境时的选择权 它的角色（意图生成）： 在同一个惯性参考线上，噪声种子 1 可能让 AI 决定\u0026quot;左侧超车\u0026rdquo; 噪声种子 2 可能让 AI 决定\u0026quot;减速跟车\u0026rdquo; 总结：扩散噪声是在决策空间里探索，它决定了 AI 在既定物理基础上如何\u0026quot;起舞\u0026quot; 为什么要\u0026quot;双管齐下\u0026quot;？ 方案 A：只有 IRP（没有扩散噪声）： 你会有 10 条不同的射线，但每条射线上，AI 只能给出一个死板的修正。如果这 10 条射线都没能完美避开前方的障碍物，AI 就没招了。这叫**\u0026ldquo;物理多样，决策单一\u0026rdquo;**。\n方案 B：只有扩散噪声（没有 IRP）： 你只有 1 条笔直的参考线。虽然扩散模型可以把它变幻出无数花样，但由于起点太单一，模型需要耗费巨大的计算量去把这条直线\u0026quot;大幅度扭转\u0026quot;到侧面。这叫**\u0026ldquo;起点单一，决策费劲\u0026rdquo;**。\nResAD 的\u0026quot;黄金组合\u0026quot;逻辑：\n先用 IRP 铺路：用简单的物理扰动，快速占领未来的各种\u0026quot;物理阵地\u0026quot; 再用扩散模型雕琢：在每一个物理阵地上，利用随机噪声让 AI 灵活地思考\u0026quot;在这个位置我该怎么微调\u0026quot; 3.5 WTA Loss vs Diffusion：多模态策略对比 Winner-Take-All (WTA) 策略确实是目前解决\u0026quot;平均陷阱\u0026quot;最主流、最有效的手段之一。\n什么是 WTA？（形象理解：赛马机制） 想象你带了 6 个徒弟（6 个预测头），让他们预测车该怎么走：\n训练时：如果真实的司机右转了。你发现徒弟 C 猜得最准。这时候，你只奖励/惩罚徒弟 C，让他以后更像真实司机。而另外 5 个徒弟这次的任务就是\u0026quot;闭嘴\u0026quot;，不做任何更新 结果：久而久之，徒弟 A 成了\u0026quot;左转专家\u0026quot;，徒弟 B 成了\u0026quot;直行专家\u0026quot;，徒弟 C 成了\u0026quot;避让专家\u0026quot; 为什么还要 Diffusion？ A. 离散 vs. 连续（菜单与厨师）\nWTA 是\u0026quot;点菜制\u0026quot;：你预先设定了 $K$ 个头。无论路况多复杂，你永远只能从这 6 个选项里挑。如果路况需要一个\u0026quot;微小的左转再加速\u0026quot;，但你的 6 个头里只有\u0026quot;大左转\u0026quot;和\u0026quot;匀速直行\u0026quot;，你就调不出来 Diffusion 是\u0026quot;大厨现做\u0026quot;：只要噪声种子变一点，轨迹就变一点。它可以生成无数种、连续分布的轨迹 B. \u0026ldquo;死掉的头\u0026quot;问题（Mode Collapse）\nWTA 的噩梦：如果某一个预测头运气不好，从来没被选中过，它就永远得不到训练梯度。最终，你虽然准备了 6 个头，但可能只有 3 个在干活，剩下的全成了\u0026quot;废头\u0026rdquo; Diffusion：每一轮去噪过程，网络的所有参数都在参与计算。不存在\u0026quot;死掉的头\u0026quot; C. \u0026ldquo;打磨\u0026quot;的深度\nWTA 是\u0026quot;一锤子买卖\u0026rdquo;：神经网络一次性输出 6 条线 Diffusion 是\u0026quot;精雕细琢\u0026quot;：它是迭代的。每一步去噪，模型都会结合环境特征微调轨迹。这种多步细化的能力，让 Diffusion 出来的轨迹在运动学上通常比 WTA 直接回归出来的要好得多 ResAD 的\u0026quot;神操作\u0026quot;：把两者结合了 Diffusion（负责生成）：用噪声生成 64 条形状各异的轨迹 Ranker（负责挑选）：这个 Ranker 实际上就是在做类似 WTA 的筛选 所以，ResAD 的逻辑是：\n不靠 WTA 来\u0026quot;产生\u0026quot;多模态（因为 WTA 产生的模态有限且容易坏死） 靠 Diffusion \u0026ldquo;大规模制造\u0026quot;高质量的多模态候选项 靠 Ranker（类似选秀）来做最终决策 四、实验结果 作者在 NAVSIM 数据集（目前最权威的端到端评测基准之一）上进行了测试。\n4.1 核心指标 PDMS (PDM Score)：综合分数，越高越好。包含不碰撞率（NC）、舒适度（C）等 EPDMS (Extended PDMS)：NAVSIM v2 提出的更难的指标 4.2 战绩 SOTA：ResAD 在 NAVSIM v1 和 v2 上都拿到了第一梯队的成绩（PDMS 88.8 / 90.6） 对比：比之前的明星模型 Transfuser、UniAD、DiffusionDrive 都要好 效率：推理速度非常快，只需要 2 步去噪就能达到很好的效果 4.3 消融实验 即插即用：把\u0026quot;归一化残差建模（NRTM）\u0026ldquo;套用到旧模型上，旧模型效果也立刻提升 各组件的贡献： 只加 Ranker：提升一点点 加 轨迹残差建模 (TRM)：提升巨大！ 加 归一化 (PRNorm)：收敛更快，近距离避障更准 加 微扰 (IRP)：解决多模态问题，提升复杂场景处理能力 五、伪代码详解 import torch import torch.nn as nn class ResAD(nn.Module): def __init__(self): super().__init__() # 1. 特征提取器 (比如 Vision Transformer)，负责看路 self.backbone = VisualBackbone() # 2. 扩散解码器，负责从噪声中\u0026#34;捏\u0026#34;出残差 self.diffusion_decoder = ResidualDiffusionDecoder() # 3. 轨迹打分器，负责从一堆方案里选最好的 self.ranker = TrajectoryRanker() def forward(self, sensor_data, ego_state): \u0026#34;\u0026#34;\u0026#34; sensor_data: 摄像头图像、雷达数据等 ego_state: 当前车的状态 (位置 p0, 速度 v0, 朝向 theta) \u0026#34;\u0026#34;\u0026#34; # --- 第一阶段：感知与特征提取 --- # 提取环境特征（路口长啥样、红绿灯、其他车在哪里） env_features = self.backbone(sensor_data) # --- 第二阶段：物理先验（画\u0026#34;骨架\u0026#34;） --- # 1. 惯性参考扰动 (IRP)：生成 K 条略有不同的\u0026#34;初试射线条\u0026#34; # 我们不只画一条直线，我们给初始速度加点噪声，生成一把\u0026#34;扇形\u0026#34;射线 ref_trajectories = self.generate_perturbed_refs(ego_state, num_samples=64) # ref_trajectories 形状: [64条, 8个时间步, (x, y)] # --- 第三阶段：生成式决策 (核心魔术) --- # 2. 准备初始\u0026#34;迷雾\u0026#34; (随机噪声) # 注意：这里的噪声是在\u0026#34;残差空间\u0026#34;里的，范围大概在 -1 到 1 之间 # 形状与轨迹一致: [64, 8, 2] residual_noise = torch.randn(64, 8, 2) # 3. 扩散去噪循环 (Iterative Denoising) # ResAD 的优势在于只需要 2-5 步 current_residual = residual_noise for t in reversed(range(num_steps)): # 这里的 diffusion_decoder 就像一个雕塑家 # 输入：当前的乱码、环境特征、那一叠\u0026#34;物理射线\u0026#34; # 输出：预测的更清晰一点的\u0026#34;归一化残差\u0026#34; current_residual = self.diffusion_decoder( current_residual, env_features, ref_trajectories, t ) # --- 第四阶段：物理还原 (从残差回到世界) --- # 4. 去归一化 (Inverse PRNorm) # 把预测出的 [-1, 1] 之间的数值，放大回真实的米(m) real_residuals = self.inverse_prnorm(current_residual) # 5. 最终合成轨迹 = 物理骨架 + AI 预测的偏差量 # 这一步把直线\u0026#34;掰弯\u0026#34;成真正的避障/转弯曲线 final_candidate_trajectories = ref_trajectories + real_residuals # --- 第五阶段：选秀 (Ranking) --- # 6. 打分器给这 64 条备选曲线打分 # 考虑：是否撞车、是否越界、是否让乘客觉得晕车 scores = self.ranker(final_candidate_trajectories, env_features) # 7. 选分数最高的一条发给方向盘和油门 best_trajectory = final_candidate_trajectories[torch.argmax(scores)] return best_trajectory def generate_perturbed_refs(self, ego_state, num_samples): \u0026#34;\u0026#34;\u0026#34; 计算惯性参考轨迹：p_t = p0 + (v0 + delta_v) * t \u0026#34;\u0026#34;\u0026#34; dt = 0.5 # 时间间隔 0.5 秒 time_steps = torch.tensor([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0]) # 给速度加扰动 (IRP) v0 = ego_state.v0 # 产生一些微小的速度方向和大小的变化 perturbed_v = v0 + torch.randn(num_samples, 2) * 0.1 # 计算直线点：[num_samples, 8, 2] refs = v0.pos + perturbed_v.unsqueeze(1) * time_steps.unsqueeze(1) return refs def inverse_prnorm(self, normalized_residual): \u0026#34;\u0026#34;\u0026#34; 逐点残差去归一化 (PRNorm 的逆操作) normalized_residual: 模型预测出来的 [-1, 1] 之间的数 \u0026#34;\u0026#34;\u0026#34; # 从预先统计好的数据集分布里拿到每个时间步的最大最小值 # 越远的点，min/max 范围越大 res_min, res_max = self.dataset_stats.get_min_max() # 简单的线性拉伸：从 [-1, 1] 映射回 [min, max] real_res = (normalized_residual + 1) / 2 * (res_max - res_min) + res_min return real_res 六、总结 这篇论文解决了什么核心问题？ 解决了端到端自动驾驶中，直接预测绝对坐标带来的训练难、远近权重不平衡、容易学坏的问题。\n核心公式背后的逻辑 $$ \\text{最终轨迹} = \\underbrace{(\\text{当前位置} + \\text{速度} \\times \\text{时间})}{\\text{物理惯性，本来就会走的路}} + \\underbrace{\\text{神经网络预测的残差}}{\\text{AI根据路况做的智能微调}} $$\n为什么叫 \u0026ldquo;Normalized\u0026rdquo; (归一化)？ 因为远处偏差大，近处偏差小，不归一化会导致 AI 忽视近处更危险的偏差。\nResAD 的优势一句话总结 利用物理学定律（惯性）作为基准，让 AI 只需专注于学习\u0026quot;变化量\u0026rdquo;，并用归一化技术平衡远近视野，从而学得更快、更稳、更准。\n相关论文 [[UAD - 无需3D标注的端到端自动驾驶]] [[World4Drive - 无需感知标注的端到端世界模型]] [[LAW - Latent World Model for E2E Driving]] ","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/res-ad/","title":"ResAD: Normalized Residual Trajectory Modeling for End-to-End Autonomous Driving"},{"content":" 论文：Unveiling the Surprising Efficacy of Navigation Understanding in End-to-End Autonomous Driving（揭秘端到端自动驾驶中导航理解的惊人功效） 机构：复旦大学、清华大学、中科院、滴滴 基座模型：LLaVA + Qwen2.5-0.5B 数据集：SNG-QA（100,000个高质量问答样本）\n一、核心发现：一个令人震惊的\u0026quot;盲区\u0026quot; 🚗 灵魂拷问：你的导航仪，真的有用吗？ 如果你把一个老司机的导航仪关了，甚至给他瞎指路，他还能开得好吗？\n现实中，这肯定要出大事故。但这篇论文却发现了一个极其反直觉的事实：\n现在的端到端自动驾驶模型，把导航信息删掉或者随便篡改，驾驶表现非但没变差，有些甚至还开得更好了！\n这就像一个导航盲人，居然凭直觉把车开上了高速公路——这不是魔法，这是病！\n🎯 问题出在哪？ 当前自动驾驶系统对导航信息的利用，有两个致命缺陷：\n问题类型 描述 后果 死板标注导致的\u0026quot;指鹿为马\u0026quot; 指令标注只看固定的时间或空间范围。比如在复杂环岛，车辆稍微往前偏了一点产生横向位移，就可能被错误标成\u0026quot;左转\u0026quot; 模型一头雾水，学得晕头转向 因果混淆的\u0026quot;超视距灾难\u0026quot; 在超视距（BVR）场景中，老司机会为远处路口转弯而提前变道，但此时全局导航指令却显示\u0026quot;直行\u0026quot; 模型看到矛盾信息，直接\u0026quot;精神分裂\u0026quot; 于是，现有模型干脆选择了**\u0026ldquo;自暴自弃\u0026rdquo;**：屏蔽导航信息，全靠死记硬背场景！\n二、解决方案：SNG（序列导航引导）框架 作者团队祭出了SNG（Sequential Navigation Guidance，序列导航引导）框架，就像给我们手机里装了一个真正懂你的智能导航，彻底治好了模型的\u0026quot;导航盲症\u0026quot;。\n📱 两大黄金要素 1. 导航路径（Navigation Path） 就像手机导航上的那条蓝色引导线：\n在车前 40米 范围内提取道路中心线作为参考点 心机设计：为了防止模型\u0026quot;作弊\u0026quot;（过度拟合完美的路径），作者故意加入了大量定位噪音，真实模拟现实中的GPS误差 2. 逐向导航信息（Turn-by-Turn, TBT） 这就像副驾上坐了个碎嘴子教练，实时给你报方向：\n信息类型 内容 生成方式 8种当前动作 左转、右转、掉头、直行、靠左、靠右、进入环岛、无 根据车速和未来轨迹精确计算持续时间 未来动作 提前预判下一步要干嘛 由 Qwen2.5-VL 72B 自动生成 9种补充动作 进高速、进隧道、左右转专用道等 当未来动作有歧义时跳出来\u0026quot;解围\u0026quot; 三、数据集：SNG-QA 为了让模型学会真正的逻辑推理，作者用 Qwen 2.5 VL 72B 在 NAVSIM 平台上自动化标注了约 100,000个高质量问答样本。\n🎓 推理过程三段论 总结全局导航 ➡️ 结合全局信息和目标检测做局部规划解释 ➡️ 生成轨迹点 🔒 三道质检关卡 为了确保数据质量，作者设立了比高考阅卷还严格的三道防线：\n准确性验证：答案对不对？ 一致性验证：逻辑通不通？ 语言润色：话说得漂亮不漂亮？ 四、模型架构：SNG-VLA SNG-VLA 以 LLaVA 为骨架，无缝融合文本、图像、状态和路径，打造了一个高效视觉-语言-动作模型。\n🧠 场景表示（特征提取） 模态 编码方式 备注 文本（TBT信息） LLM 分词器 → 特征向量 自然语言理解 导航路径 多层感知机（MLP）编码 带噪音的路径坐标 视觉 SigLIP-So400M 视觉编码器 Patch大小14，图像384，前后置多视角摄像头 自车状态 4通道（车速、加速度等） 带 状态丢弃编码器（SDE），0.5 Dropout防止偷懒 🤔 状态丢弃编码器（SDE）—— 防作弊神器 作者发现模型容易\u0026quot;偷懒\u0026quot;，过度依赖状态信息（比如知道当前速度就瞎猜轨迹）。于是设计了 SDE：\n施加 0.5 的随机丢弃率 强迫模型学会真正理解场景 🎯 Transformer 解码器（思考与行动） 把以上所有特征加上\u0026quot;路点查询向量（Waypoint Query）\u0026quot;，一股脑扔进预训练的 Qwen2.5-0.5B 主干网络：\nStep 1: 自回归生成\u0026#34;规划推理文本\u0026#34;（像聊天一样先说说怎么开） ↓ Step 2: 通过交叉注意力机制 + MLP 输出最终轨迹路点 损失函数：简单粗暴但极为有效的 L1 Loss（预测轨迹与真实轨迹的差异）\n五、实验结果：霸榜现场 实验动用了 8 张 80GB NVIDIA A100 显卡，在 CARLA 的 Bench2Drive 和真实世界基准 NAVSIM 上大放异彩！\n🏆 NAVSIM 基准测试 把 Transfuser 模型的导航输入换成 SNG 后：\n可行驶区域合规性（DAC）：显著飙升 碰撞时间（TTC）：显著提升 单视角 SNG-VLA-QA 模型能在保持顶级规划性能的同时，顺带把推理问答给做了！ 🏆 Bench2Drive 基准测试 这是对同行的降维打击：\n对比指标 提升幅度 驾驶得分（Driving Score） 比UniAD-Base暴涨 46.6% 成功率 直接翻倍，提升 119.4% 综合多能力得分（并道、超车、避让等） 比VAD提升 110.7% 推理延迟 仅 159.6ms，在强悍性能下保持极佳实时性 🔍 定性分析 对比图直观显示：\n传统 Transfuser：到了复杂环岛就开始\u0026quot;画龙\u0026quot;，东摇西晃 SNG-VLA：像装了\u0026quot;上帝视角\u0026quot;，完美理解全局意图，丝滑驶出环岛 六、消融实验：深度剖析 🧪 实验1：离散指令真的废了 测试场景 结果 给传统指令加噪音 PDMS（综合得分）几乎不受影响——说明模型压根没用导航信息 在开放路口/环岛瞎塞指令（该左转给右转） 传统模型生成\u0026quot;逆向行驶\u0026quot;轨迹，酿成大祸 🧪 实验2：单挑指令 vs SNG 配置 表现 只用离散指令（ID 0） 和完全\u0026quot;盲开\u0026quot;成绩一样差 完全盲开（ID 1） 同上 只给2个相距20米的导航点（极度稀疏） 秒杀传统指令！ 结论：长程路径约束 + 实时决策逻辑，两者结合才是王道！\n🧪 实验3：寻找\u0026quot;黄金比例\u0026quot; 怎么搭配 SNG 最完美？作者测了各种密度：\n配置 结果 点太少（稀疏） 没参考价值 点太密（每5米一个） 像戴了镣铐，妨碍灵活避障 每10米采样1个点，共4个点 + TBT信息（ID 5） 最高分！ 🎉 七、真实世界部署：实车验证 模型不仅在模拟器里牛，还真的被装到了实车上！\n🚙 硬件配置 组件 规格 计算单元 双 Orin 激光雷达 1颗 Innovusion Falcon 300 摄像头 5个 120度视角 AR0820 + 2个 70度视角 AR0820 🎬 实车表现 ✅ 精准选择转弯车道 ✅ 通过生成的文本敏锐警告路边电动车、行人 ✅ 识别右侧施工区域 这\u0026quot;老司机\u0026quot;不仅开得稳，还会说话提醒！\n八、总结 作者完美地首尾呼应：现有的端到端系统对导航的利用极其糟糕。但这篇论文用 SNG（序列导航引导） 这种全新的信息表示法，融合了长程轨迹约束与实时决策逻辑，彻底扭转了局面！\n核心贡献 贡献 描述 发现\u0026quot;导航盲症\u0026quot; 首次揭示端到端自动驾驶模型对导航信息的严重忽视 SNG框架 序列导航引导，融合长程路径约束 + 实时TBT信息 SNG-QA数据集 100,000个高质量VQA样本，三阶段推理+三道质检 SNG-VLA模型 无需感知任务辅助损失，直接SOTA 实车验证 在真实世界中证明了鲁棒性和落地潜力 🌟 核心启示 不需要额外费力去教模型做复杂的感知任务，只要给它正确的导航方式，它就能在闭环测试和真实世界中开出一条 SOTA 之路！\nSNG-VLA 极高的鲁棒性和落地潜力，让它成为了自动驾驶规划领域的一颗新星！\n相关链接 论文链接：https://arxiv.org/pdf/2604.12208 数据集：SNG-QA（基于NAVSIM平台） 关联笔记：[[EMMA 论文阅读笔记]]、[[SparseDriveV2 论文阅读笔记]] #自动驾驶 #E2E #导航理解 #多模态大模型 #VLA #论文笔记\n","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/sng-vla/","title":"SNG-VLA: Navigation Understanding in End-to-End Autonomous Driving"},{"content":" 这篇论文的核心思想可以概括为：如何培养一个会自己\u0026quot;脑补\u0026quot;未来、且极具空间方向感的老司机。\n🎯 研究动机：为什么之前的 AI 是个\u0026quot;近视且单线程的笨徒弟\u0026quot;？ 传统的端到端自动驾驶模型就像是被\u0026quot;宠坏的温室花朵\u0026quot;，它们极其依赖昂贵的人工感知标注（比如 3D 边界框、高精地图）来理解世界。\n为了摆脱这种成本依赖，此前最先进的无监督方法 LAW（Latent World Model）尝试通过时间自监督学习，直接从原始图像中提取\u0026quot;单模态\u0026quot;的潜变量特征。但这带来了两个致命问题：\n\u0026ldquo;缺乏常识的近视眼\u0026rdquo;：单模态特征很难捕捉物理世界中复杂的空间结构和语义信息 \u0026ldquo;一根筋\u0026rdquo;：它无法处理人类驾驶时\u0026quot;向左、向右还是直行\u0026quot;的多模态意图不确定性 这导致 LAW 训练收敛极慢，且在复杂场景下表现不佳。\n为此，World4Drive 横空出世！它不仅无需任何人工感知标注，还能根据不同的驾驶意图在脑海中\u0026quot;预演\u0026quot;未来物理世界的演变，选出最安全的路。\n🏗️ 系统架构概览 World4Drive 的整体架构可以分为两大核心模块：\n┌─────────────────────────────────────────────────────────────────┐ │ Driving World Encoding │ │ ┌──────────────┐ ┌───────────────────┐ ┌─────────────────┐ │ │ │ Intention │ │ Physical Latent │ │ Temporal │ │ │ │ Encoder │ │ Encoder │ │ Aggregation │ │ │ │ (意图编码器) │ │ (物理世界编码器) │ │ (时间聚合) │ │ │ └──────┬───────┘ └─────────┬─────────┘ └────────┬────────┘ │ │ │ │ │ │ │ └────────────────────┼─────────────────────┘ │ │ ▼ │ │ 世界潜变量 L_t │ └──────────────────────────────┬──────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Intention-aware World Model │ │ ┌──────────────────────┐ ┌──────────────────────────────┐ │ │ │ Dreamer │ │ Selector │ │ │ │ (预测器/梦想家) │───▶│ (选择器/裁判) │ │ │ │ 生成 K 种未来 │ │ 选出最优轨迹 │ │ │ └──────────────────────┘ └──────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ 🧠 模块一：Driving World Encoding（给 AI 注入\u0026quot;空间与意图的灵魂\u0026quot;） 这个模块的终极目标，是从多视角图像和轨迹词汇表中提取出带有空间、语义和时间记忆的\u0026quot;世界潜变量表示\u0026quot;。\n1. 意图编码器 (Intention Encoder) —— \u0026ldquo;老司机的战术板\u0026rdquo; 系统预设了一个包含 N=8192 条轨迹的庞大\u0026quot;词汇表\u0026quot;。\n工作流程：\n模型首先用 K-means 聚类算法对轨迹终点进行聚类 针对 3 种驾驶指令（左转、右转、直行），每种提取出 K=6 个意图关键点 加上正弦位置编码生成意图查询向量 通过自注意力层 (Self-Attention) 将自车查询向量与意图融合 最终输出融合了多模态规划意图的查询向量 Q_plan 2. 物理世界潜变量编码器 (Physical Latent Encoder) —— \u0026ldquo;全知全能的神之眼\u0026rdquo; 这是 World4Drive 的点睛之笔。作者巧妙地引入视觉基础模型作为先验知识，解决之前模型\u0026quot;缺乏常识\u0026quot;的问题。\n2.1 语义理解 (Semantic Understanding) \u0026ldquo;如何让模型在不看任何人工标注的情况下，理解图像里的内容？\u0026rdquo;\n方案：请一位\u0026quot;万事通\u0026quot;视觉大模型来当\u0026quot;陪练\u0026quot;\n组件：Grounded-SAM（强大的视觉语言模型）+ 语义头（小型解码器网络） 训练阶段：Grounded-SAM 生成高精度的、像素级的伪语义标签 学习过程：语义头尝试预测分割图，计算交叉熵损失 (L_sem) 反向传播：这股\u0026quot;纠正信号\u0026quot;告诉主干网络：\u0026ldquo;你提取的特征必须能让我分辨出哪个像素是车、哪个是路\u0026rdquo; 部署阶段：Grounded-SAM 和语义头被完全丢弃，不占用任何推理算力 2.2 3D 空间编码 (3D Spatial Encoding) \u0026ldquo;光知道\u0026rsquo;是什么\u0026rsquo;还不够，必须知道它在三维空间中的精确\u0026rsquo;位置\u0026rsquo;。\u0026rdquo;\n工作流程：\n生成深度图：将摄像头图像输入 Metric3D v2，得到度量深度图 像素转点云 (Forward Projection)： 对每个像素 (u, v)，结合深度值 d 和相机内参 计算相机坐标系下的三维坐标 (x_cam, y_cam, z_cam) 利用外参转换到自车坐标系下的 (x_ego, y_ego, z_ego) 位置编码：将 3D 坐标输入 MLP，编码成位置嵌入向量 特征融合：与语义感知视觉特征融合 2.3 时间聚合 (Temporal Aggregation) —— \u0026ldquo;激活短期记忆\u0026rdquo; 组件：交叉注意力模块\n工作原理：\n当前时刻特征作为 Query 上一时刻的世界潜变量 L_{t-1} 作为 Key 和 Value 当前帧\u0026quot;查询\u0026quot;上一帧的记忆，提取最相关的历史信息 这就像开车时用余光和记忆确认刚才在左后方的那辆车，现在是不是快要超上来了。\n最终输出：世界潜变量 L_t——融合了物体语义、3D 空间位置和历史运动信息的\u0026quot;世界状态精华\u0026quot;。\n🎬 模块二：Intention-aware World Model（脑内小剧场的\u0026quot;未来预演\u0026quot;） 拥有了物理世界的精确感知后，World4Drive 开始像人类一样\u0026quot;做白日梦\u0026quot;（预判未来）。\n整个工作流程可以诗意地概括为：\u0026ldquo;一念生万法，择善而从之\u0026rdquo;\n1. 预测器 (Dreamer) —— \u0026ldquo;平行宇宙推演仪\u0026rdquo; 输入：\n当前世界状态 L_t（\u0026ldquo;梦境\u0026quot;的起点） 多模态规划意图 Q_plan（\u0026ldquo;梦境\u0026quot;的 K 个不同主题） 工作流程：\n步骤一：动作编码 (Action Encoding) \u0026ldquo;将意图转化为具体的行动方案\u0026rdquo;\n通过交叉注意力模块：\nQuery：K 个规划意图向量 Key \u0026amp; Value：当前世界状态潜变量 每一个\u0026quot;意图\u0026quot;都在审视当前的\u0026quot;世界状态\u0026rdquo;，问：\u0026ldquo;基于现在路上的情况，要实现我这个意图，应该采取什么样的动作？\u0026rdquo;\n输出 K 个动作特征令牌 (Action Tokens)，每个代表在当前世界状态下执行该意图的具体\u0026quot;操作方案\u0026rdquo;。\n步骤二：未来预测 (Future Prediction) \u0026ldquo;让时间流动起来\u0026rdquo;\n在通道维度上拼接 K 个动作令牌与当前世界状态 送入时空 Transformer 学习物理世界的动态演化规律 一次前向传播同时计算出所有 K 个未来世界 输出：K 个预测的未来世界潜变量，每个描绘了\u0026quot;如果执行第 k 个意图，n 个时间步后环境会变成什么样\u0026quot;。\n2. 选择器 (Selector) —— \u0026ldquo;洞悉真相的超级裁判\u0026rdquo; 训练阶段：\u0026ldquo;以史为镜，可以知兴替\u0026rdquo; 在训练时，我们拥有\u0026quot;上帝视角\u0026quot;（未来真实数据）：\n获取\u0026quot;标准答案\u0026quot;：将未来真实图像输入编码器，得到真实未来世界潜变量 评选\u0026quot;最佳梦境\u0026quot;：计算 K 个预测与真实未来的 MSE，找到最优者（索引 j） 两大损失函数驱动学习： 损失函数 作用 目标 重建损失 L_recon 最小 MSE 告诉预测器：\u0026ldquo;你的\u0026rsquo;做梦\u0026rsquo;能力还不够逼真！\u0026rdquo; 得分损失 L_score Focal Loss 训练 ScoreNet 学会判断哪个梦最靠谱 推理阶段：\u0026ldquo;当机立断\u0026rdquo; 在真实道路上，训练好的 ScoreNet 就派上用场：\nDreamer 生成 K 个未来轨迹及\u0026quot;梦境\u0026quot; ScoreNet 对 K 个选项打分 选择得分最高的意图对应轨迹输出 📊 损失函数总览 总损失是四项的加权和：\n$$L = 0.2 \\cdot L_{sem} + 0.2 \\cdot L_{recon} + 0.5 \\cdot L_{score} + 1.0 \\cdot L_{traj}$$\n损失项 权重 作用 L_sem 0.2 语义理解损失（交叉熵） L_recon 0.2 重建损失（MSE） L_score 0.5 得分损失（Focal Loss） L_traj 1.0 与专家轨迹对齐（L1） 🏆 实验结果：无需标注，吊打前浪 核心指标（对比 LAW 基线） 指标 LAW World4Drive 提升 L2 规划误差 0.61m 0.50m ↓ 18.1% 碰撞率 0.30% 0.16% ↓ 46.7% 训练收敛速度 基准 3.75x 更快 ↑ 375% 鲁棒性测试（夜间 \u0026amp; 雨天） 因为掌握了高维物理语义规律，模型丝毫不受光线干扰：\n场景 碰撞率下降 夜间 ↓ 63.7% 雨天 ↓ 68.8% 可扩展性 与以往模型不同，World4Drive 展现出极佳的可扩展性：\n增加隐层维度（128 → 384）：性能稳步提升 升级骨干网络（ResNet-34 → ResNet-101）：性能显著提升 ⚡ 推理效率分析：Thor 能跑吗？ 答案：绝对吃得消！\n对于英伟达 Drive Thor 这颗拥有 2000 TOPS 算力的\u0026quot;性能怪兽\u0026quot;，同时推演 K=6 种未来几乎连\u0026quot;热身\u0026quot;都算不上。\n为什么如此高效？ 1. 降维打击：在潜变量空间做白日梦 不预测高分辨率视频或稠密点云 只在抽象特征向量空间（D=256 或 384）操作 几百维度的浮点数矩阵乘法，对 GPU 来说轻而易举 2. 拒绝排队：并行交叉注意力 不是 先算左转 → 再算右转 → ...（循环 6 次） K=6 种意图在通道维度拼接，一次前向传播全算出来 交叉注意力层耗时以微秒计算 3. \u0026ldquo;卸磨杀驴\u0026rdquo;：推理阶段极度轻量化 Grounded-SAM 仅在训练阶段使用 部署上车时直接丢弃 推理算力全用于视觉主干网络和 Metric3D 耗时大盘 ~90% 算力：视觉主干网络处理 6 个摄像头 + Metric3D 深度估计 \u0026ldquo;脑补 6 种未来并打分\u0026rdquo;：网络末端几层轻量级 MLP 和 Transformer 可完全满足闭环控制对极低延迟（几十毫秒）的严苛要求 💡 核心创新总结 World4Drive 的伟大之处在于：\n\u0026ldquo;借力\u0026quot;视觉基础模型：Grounded-SAM 提供语义先验，Metric3D 提供空间先验 创新性的\u0026quot;意图-世界预演\u0026quot;机制：Dreamer-Critic 架构实现自监督学习 摆脱人类密集标注的拐杖：真正实现 perception annotation-free 这让自动驾驶 AI 学会了像老司机一样：\n\u0026ldquo;察言观色（深层语义与空间感知）\u0026rdquo; + \u0026ldquo;三思而后行（基于世界模型的未来推演）\u0026rdquo;\n这是通向下一代更智能、更通用的自动驾驶的一座重要里程碑！\n🔗 相关链接 论文链接：arXiv:2507.00603 相关论文：[[LAW - Latent World Model for E2E Driving]]（前身工作） ","date":"2026-03-17T00:00:00Z","permalink":"/post/robotics/e2e/world4-drive/","title":"World4Drive - 无需感知标注的端到端自动驾驶世界模型"},{"content":"核心 利用Bernstein polynomial轨迹表达式代替一般的polynomial轨迹表达式。 轨迹原始表达式 $$P_{j}(t)=p_{j}^{0}+p_{j}^{1}t+p_{j}^{2}t^{2}+\u0026hellip;+p_{j}^{n}t^{n}$$ Bernstein polynomial轨迹表达式 $$B_{j}(t)=c_{j}^{0}b_{n}^{0}(t)+c_{j}^{1}b_{n}^{1}(t)+\u0026hellip;+c_{j}^{n}b_{n}^{n}(t)$$ $$b_{n}^{i}(t)=\\binom{n}{i}\\cdot t^{i}\\cdot (1-t)^{n-i}$$ 其中，\\(j\\)表示该段轨迹的序号，\\(t\\)表示时间。 另外，原始表达式中\\(p_{j}^{i}\\)为多项式的系数，没有实际的物理意义。而Bernstein polynomial轨迹表达式中的\\(c_{j}^{i}\\)具有实际的物理意义，即控制点的坐标。 性质 Endpoint interpolation 在给出一系列控制点后，对应的Bezier曲线只保证穿过第一个和最后一个控制点，中间的其他控制点不要求穿过，但会对bezier曲线的形状产生影响。\nConvex hull 生成的Bezier曲线一定不会超出由控制点形成的凸多边形的包围框。\nHodograph \\(B_{j}(t)\\)的导数\\(B_{j}^{\u0026rsquo;}(t)\\)依然是一个Bezier曲线，且该曲线的控制点为\\(n\\cdot (c^{{i+1}}-c^{{i}})\\)。\nFixed time interval Bezier曲线的时间\\(t\\)永远定义在[0,1]区间内。\n工作流程 求解飞行走廊 构建栅格地图（比较好的比如八叉树格式），存储和搜索效率较高。 在栅格地图中利用A*算法求解无碰撞路径。 对路径中经过的每个栅格进行膨胀，四个边向四周扩张，直到遇到障碍物停止，形成安全走廊。若相邻两个安全走廊是一样的，就合并为一个。 得到一串有序的安全走廊。 基于飞行走廊和Bernstein多项式的轨迹优化 起点状态和终点状态等式约束 相邻两段轨迹的连接点处的状态连续性约束 相邻两段轨迹的连接点需要处于对应的两个安全走廊的重叠区域内 满足动力学（速度、加速度）约束，只要保证\\(B_{j}(t)\\)的导数表达式\\(B_{j}^{\u0026rsquo;}(t)\\)和\\(B_{j}^{\u0026rsquo;\u0026rsquo;}(t)\\)的控制点在\\(Vel_{min}\\)、\\(Vel_{max}\\)和\\(Acc_{min}\\)、\\(Acc_{max}\\)内。 扩展 基于B-spline的多项式表达，Bezier是一种特殊的B-spline。B-spline自动分段，不需要人为将完整轨迹分为N段，从而避免段与段之间的连续性分析。 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/path-planning/bezier-curve-optimization/","title":"Bezier Curve Optimization"},{"content":"CarPlanner: Consistent Auto-regressive Trajectory Planning for Large-scale Reinforcement Learning in Autonomous Driving 摘要 本文提出CarPlanner——一种基于强化学习生成多模态轨迹的一致性自回归规划器。 其自回归结构支持高效的大规模强化学习训练，而引入的一致性机制通过保持跨时间步长的时序一致性，确保了策略学习的稳定性。此外，CarPlanner采用生成-选择框架，结合专家引导的奖励函数与不变视角模块，既简化了强化学习训练流程，又提升了策略性能。 有效解决了训练效率与性能提升的双重挑战。 首次证明了基于强化学习的规划器能够在具有挑战性的大规模真实世界数据集nuPlan上超越基于模仿学习（IL）和基于规则的最先进方法（SOTA） 相关工作 IL存在分布偏移与因果混淆问题 RL存在1）训练效率低下 2)性能表现不足的问题 当前生成多步轨迹的方法主要分为两类：“初始化-优化”模型与“自回归模型” 方法A通过生成初始轨迹估计，并利用迭代式强化学习进行优化。然而，最新研究（如Gen-Drive）表明，该方法仍落后于基于模仿学习与规则的最优规划器。该框架的显著缺陷在于忽视了轨迹规划任务内在的时序因果性。此外，直接在高维轨迹空间进行优化所引发的复杂性，会制约强化学习算法的性能表现。 方法B采用自回归模型，通过在状态转移模型内运用单步策略循环生成自车位姿序列形成完整规划轨迹。由于考量了时序因果关系，现有自回归模型能够生成交互式驾驶行为。但此类方法的共性局限在于依赖从动作分布中自回归随机采样来生成多模态轨迹，这种基础自回归流程可能损害长期一致性并过度扩展强化学习的探索空间，最终导致性能下降。 针对自回归模型的固有缺陷，将一致性模式表征作为自回归的条件约束。 采用纵向-横向分解模式表示法：纵向模式通过标量参数捕获平均速度特征，横向模式则基于自车当前状态与地图信息生成所有可行路径集合。此类模式在轨迹生成的时间维度上保持恒定，为策略采样过程提供稳定一致的行为引导。 设计了适用于大规模多样化场景的通用奖励函数。 专家引导项：通过量化规划轨迹与专家轨迹的位移误差（结合一致性模式表征），有效缩小策略探索空间。 任务导向项：集成碰撞规避、可行驶区域约束等驾驶常识性指标。 此外，我们引入不变视角模块（Invariant-View Module, IVM），通过将智能体状态与横向模式信息转换至自车当前坐标系，并裁剪远端无关信息，为策略网络提供与时间无关的标准化输入，从而简化特征学习过程并增强泛化能力。 方案 强化学习基本范式 \\(S\\)：状态空间 \\(A\\)：动作空间 \\(P\\)：状态转移概率 \\(R\\)：奖励函数 \\(T\\)：规划时长 \\(\\gamma\\)：奖励衰减系数 \\(\\pi\\)：动作采样策略 \\(a_{t}\\)： \\(t\\)时刻采样的动作 \\(s_{t}\\)： \\(t\\)时刻状态 \\(\\tau\\)：状态-动作序列（轨迹） 目标是最大化奖励 向量化的状态表征 \\(s_{t}\\) map 道路拓扑、 交通信号灯（通过polylines和polygons表示） agent 历史状态 问题建模 将自回归模型解耦为policy model 和 transition model（本质上就是周围其他agent的轨迹预测模型） 将动作定义为下一个时刻的状态 \\(a_{t} = s_{t+1}^{0}\\) 从上面公式可以看到典型自回归方法相关的固有问题：动作来源于从策略分布中随机采样，进而导致前后帧的不一致行为。为了解决该问题，引入前后帧一致的模式信息 \\(c\\) 上面的公式展示了一个“生成-选择”架构的运行逻辑。模式选择器根据初始状态 \\(s_{0}\\)对候选模式进行打分，轨迹生成器根据模式的指导生成多模态轨迹 规划器架构 四个部分组成 非交互的状态转移模型 模式选择器 轨迹生成器 规则增强选择器 Map编码 每个lane通过polyline表示，包含 \\(3N_{p}\\)个点，中心点、左边界点、右边界点；每个点特征 \\(D_{m}=9\\)（x, y, heading, speed limit, category）。 intersections, crosswalks, stop lines通过polygon表示 通过PointNet提取特征后concat在一起得到 \\(N_{m}*D\\) Agent 编码 每个障碍物保留过去 \\(H\\)帧的状态，每个状态特征 \\(D_{a}=10\\)（x, y, heading, vel, bbox, timestamp, category）。 通过PointNet提取特征后concat在一起得到 \\(N_{a}*D\\) 非交互式的状态转移模型 状态转移模型负责给环境中所有agent生成下一个时刻的状态（就是每个time step给所有agent做一次推演）。这个过程比较耗时，并且相比直接拿历史信息一次性生成整段预测轨迹的无交互预测模型并没有引入性能上的优化，所以实际实现的时候这边直接用了一个预测模型给除自车外的agent一次性生成整段多模轨迹。（本质是一个decoder从BEV feature中生成障碍物预测轨迹） TODO：ego和obs的交互自洽如何满足？ 模式选择器 该模块以初始状态s₀及纵向-横向分解模式信息为输入，输出各模式的概率分布。 路径-速度解耦模式表征 为捕捉纵向行为特征，我们生成 \\(N_{lon}\\)个表征轨迹平均速度的模式。每个纵向模式 \\(c_{lon,j}\\)定义为标量值 \\(\\frac{j}{N_{lon}}\\)，沿维度 \\(D\\)重复扩展，形成维度为纵向 \\(N_{lon}\\times D\\)模式的矩阵。针对横向行为，我们通过图搜索算法从地图中提取 \\(N_{lat}\\)条可行路径，对应自车可用车道。原始路径表征维度为 \\(N_{lat}\\times N_{r} \\times D_{m}\\)（ \\(N_{r}\\)为路径点数， \\(D_{m}\\)为特征维度）。采用PointNet点云网络聚合路径点特征，生成维度 \\(N_{lat}\\times D\\)的横向模式。将纵向与横向模式拼接后形成维度 \\(N_{lat}\\times N_{lon} \\times 2D\\)的复合模式表征 \\(c\\)，经线性层映射至 \\(N_{lat}\\times N_{lon} \\times D\\)以对齐特征空间，最终模式总数 \\(N_{mode}=N_{lat}N_{lon}\\)。 基于查询的Transformer解码器 该解码器融合模式特征与 \\(s_{0}\\)中包含的地图/智能体特征。工作机制如下：以模式特征作为query，地图与智能体信息作为key-value。更新后的模式特征经MLP解码得到各模式评分，最终通过softmax归一化。 轨迹生成器 该模块以自回归方式运行，基于当前状态 \\(s_{t}\\)与恒定模式信息 \\(c\\)，循环解码自车下一时刻位姿 \\(a_{t}\\)。 不变视角模块（IVM） 每个time step对下个时刻的自车位姿进行估计之前都要执行该操作：对模式与状态输入网络前进行预处理以消除时间信息 近邻筛选：对状态 \\(s_{t}\\)中的地图与智能体信息，选择距离自车当前位姿最近的K近邻（KNN）作为输入（K分别取地图/智能体元素总数的50%） 路径裁剪：针对表征横向行为的路线，以距离自车当前位置最近的路径点为起点，保留 \\(K_{r}\\)个点（ \\(K_{r}\\)=单条路线 \\(N_{r}\\)点数的25%） 坐标系对齐：将路线、智能体及地图位姿转换至ego当前时刻 \\(t\\)的坐标系下 时序标准化：将历史时间步 \\(t-H:t\\)转换为相对当前时刻 \\(t\\)的 \\([-H:0]\\)区间 基于查询的Transformer解码器 采用与模式选择器相同的主干网络架构，但query维度不一样（因为IVM的作用，同一时刻下不同模态的下自车坐标系内环境信息是不同的，所以无法共享）： Query： \\(1 \\times D\\)维度（单模式特征） Key-Value： \\((N+Nₘ)\\times D\\)维度（ \\(N\\)智能体+ \\(N_{m}\\)地图要素） 输出特征： \\(1 \\times D\\)维度（保持与查询维度一致） 策略输出层 模式特征通过两个不同的head进行处理（A2C方法）： policy head（Actor网络）：通过MLP生成动作分布参数（高斯分布） 训练阶段：从分布中采样动作以促进探索 推理阶段：直接采用分布均值作为确定性输出 value head（Critic网络）：通过独立MLP估计状态价值 数学建模：动作分布建模为 \\(\\mu + \\sigma\\)的高斯形式，策略优化基于PPO等强化学习算法实现。 规则后处理 多模态输入：接收初始状态 \\(s_{0}\\)、自车多模态规划轨迹集、周边智能体预测轨迹 规则化评估：基于预设规则计算 安全性：轨迹与障碍物的最小距离 行进效率：轨迹终点纵向位移 舒适度：加速度/加加速度的L2范数 综合评分：将规则得分与模式选择器的模式评分加权求和 最优选择：选取综合评分最高的轨迹作为规划器最终输出 训练 采用分阶段训练架构，核心流程如下： 状态转移模型预训练 优先训练非交互式的状态转移模型（预测任务预训练） 训练模式选择器与轨迹生成器时冻结状态转移模型参数 模式选择\u0026amp;轨迹生成 不输入所有模式给到轨迹生成模型，只输入正样本模式\n正样本模式确定方法： 横向模式：根据真实轨迹终点所在路径段确定 纵向模式：将轨迹起点至终点的纵向距离等分为 \\(N_{lon}\\)区间，匹配对应模式\n损失函数设计 模式选择损失： ▫ 正样本模式交叉熵损失（负对数似然） ▫ 辅助任务：真实轨迹回归损失\n轨迹生成损失：PPO算法三要素 ▫ 策略改进项（Policy Improvement） ▫ 价值估计项（Value Estimation） ▫ 熵正则项（Entropy Regularization）\n奖励函数构建 基础奖励：自车未来位姿与真实轨迹的负位移误差（DE）\n增强约束： ▫ 碰撞检测：发生碰撞时奖励-1 ▫ 可行驶区域合规性：越界时奖励-1 ▫ 正常行驶时奖励保持0\n模式丢失正则化 动机：防止Transformer结构中的残差连接导致对mode或route过于依赖 实现：训练时随机屏蔽部分路径信息（mask概率=20%） 效果：增强模型对局部信息缺失的鲁棒性 实验 仿真平台 采用nuPlan仿真器 交通参与者行为模式： ▫ 非反应模式：日志回放（log-replay） ▫ 反应模式：IDM策略（智能驾驶员模型）\n自车控制架构：用户规划器生成轨迹 → LQR控制器跟踪生成控制指令\n仿真参数：15秒时长/10Hz更新频率\n基准测试集与评估指标 采用两大基准测试集： Test14-Random（来自PlanTF）：261个场景 Reduced-Val14（来自PDM）：318个场景 评估采用nuPlan官方闭环综合评分（CLS），包含： 安全指标：碰撞率（S-CR）、碰撞时间（S-TTC） 可行驶区域合规性（S-Area） 行进效率（S-PR） 舒适度指标 实验细节 数据划分（遵循PDM标准）： 训练集：176,218个场景（14类×4,000场景） 验证集：1,118个场景（14类×100场景） 训练配置： 硬件：2×NVIDIA 3090 GPU 训练轮次：50 epochs 批量大小：64/GPU 优化器：AdamW（初始学习率1×10⁻⁴，验证损失停滞时学习率衰减因子0.3） 强化学习参数： 折扣因子γ=0.1 GAE参数λ=0.9 损失权重：价值损失3，策略损失100，熵损失0.001 模式参数： 纵向模式数：12 最大横向模式数：5 SOTA对比 方法分类与对比基准 基于轨迹生成器类型，我们将现有方法划分为规则驱动（Rule）、模仿学习（IL）与强化学习（RL）三大类： PDM：2023年nuPlan挑战赛冠军 PDM-Closed：IL+规则混合框架，采用IDM生成候选轨迹，基于安全/效率/舒适度的规则选择器优选轨迹 PDM-Open：纯规则驱动版本 PLUTO：生成-选择框架，结合对比模仿学习（Contrastive IL）与数据增强技术训练生成器 GenDrive：预训练-微调流程，先通过IL预训练扩散模型规划器，再基于AI偏好训练的奖励模型进行RL微调去噪过程 实验结果分析\n如表1（Test14-Random）与表2（Reduced-Val14）所示，CarPlanner在无交互场景（障碍物不对自车行为作出反应）中展现显著优势：\n无交互式场景（CLS-NR）： ▫ 全指标领先，较PDM-Closed与PLUTO分别提升4.02与2.15分 ▫ 行进效率（S-PR）显著优于PDM-Closed（表2对比） ▫ 碰撞率（S-CR）与基准方法持平，验证安全驾驶保障能力 技术亮点：未使用数据增强、驾驶历史掩码等IL常用技术，彰显闭环任务解决能力\n交互式场景（CLS-R）： ▫ 性能略逊于PDM-Closed 原因分析：模型仅在无交互式场景训练，未接触IDM策略驱动的动态交互环境，导致对反应式智能体扰动鲁棒性不足\n总结 RL方法潜力验证：在无交互式场景全面超越IL与规则方法 效率-安全平衡：在保持碰撞率水平的同时显著提升行进效率 场景泛化局限：反应式场景性能揭示跨模式训练必要性 消融实验 奖励项作用 只生效质量奖励（Quality Only： collision check + drivable area check）： 规划器倾向于生成静态轨迹，行进效率（S-PR）显著降低 成因分析：自车初始处于安全可行驶状态，前行将面临碰撞/越界风险，导致策略陷入局部最优 质量+位移误差奖励（Quality+DE）： 碰撞率（S-CR）从97.49→99.22，提升1.73% 可行驶区域合规性（S-Area）从96.91→99.22，提升2.31% IVM模块有效性验证 坐标变换技术\u0026amp;KNN近邻筛选对最终表现的优化有较大收益 与IL的对比 mode dropout和selector side task对IL和RL训练都有较大的帮助 ego-history dropout 对IL帮助较大，提升碰撞率（S-CR）与可行驶区域合规性（S-Area）， 随机屏蔽历史位姿与当前速度，缓解因果混淆问题 对RL则不是很重要 较大程度影响生成器loss中的value评估部分，闭环性能下降 原因：RL通过奖励信号自动挖掘因果关系，无需人工干预 backbone sharing 常用于IL场景，通过特征共享，提升泛化能力 在RL场景起到反作用，引发不同任务之间的梯度冲突，轨迹生成器和模式选择器损失上升 TODO：轨迹生成那边是循环生成下一帧轨迹的，前后帧感觉无法共享特征？ 场景化定性分析 自车右转避让行人的复杂场景\n规则方法\n未能预判行人动态，采取紧急制动仍与行人轨迹交叠 IL方法\n感知到行人运动趋势，及时启动制动操作，但与行人保持纵向距离不足（\u0026lt;0.5m） RL方法\n提前横向避让，构建横向安全缓冲区（1.2m），行进效率与安全指标均达最优 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/e2e/car-planner/","title":"CarPlanner"},{"content":"知名研究机构 University of Pennsylvania GRASP Lab,Vijay Kumar Research Interests: planning, control, swarm HomePage Massachusetts Institute of Technology Jonathan How Research Interests: modelling, control, planning HomePage Nicholas Roy Research Interests: perception, learning HomePage Carnegie Mellon University Nathan Michael HomePage Sebastian Scherer Research Interests: perception, planning HomePage University of California, Berkeley Markus Mueller Research Interests: control, planning ETH Zurich ASL Team, Roland Siegwart Research Interests: perception, control Homepage Raffaello D’Andrea Research Interests : control, swarm Homepage University of Zurich Davide Scaramuzza Research Interests : perception, control Homepage Hong Kong University of Science Technology Shaojie Shen Research Interests : UAV Homepage Ming Liu Research Interests : UGV Homepage 年轻学者 Helen Oleynikova HomePage Sikang Liu HomePage 路径规划技术图谱 前端搜索 SEARCH-BASED PATH FINDING Graph Search Basis Dijkstra and A* Jump Point Search SAMPLING-BASED PATH FINDING Probabilistic Road Map Rapidly-exploring Random Tree (RRT) Optimal Sampling-based Methods Advanced Sampling-based Methods KINODYNAMIC PATH FINDING State-state Boundary Value Optimal Control Problem State Lattice Search Kinodynamic RRT* Hybrid A* 后端优化 MINIMUM SNAP TRAJECTORY GENERATION Differential Flatness Minimum Snap Optimization Closed-form Solution to Minimum Snap Time Allocation SOFT AND HARD CONSTRAINED TRAJECTORY OPTIMIZATION Soft Constrained Trajectory Optimization Hard Constrained Trajectory Optimization 地图 地图种类 占据栅格图(Occupancy grid map) 最稠密 结构化好 直接索引 源码链接 八叉树地图(Octo-map) 稀疏 结构化好 非直接索引 体素哈希地图(Voxel hashing) 最稀疏 结构化好 非直接索引 源码链接 点云地图(Point cloud map) PCL 截断的符号距离场地图(Truncated Signed Distance Functions map, TSDF) 源码链接 欧几里得符号距离场地图(Euclidean Signed Distance Functions map, ESDF) VoxBlox FIESTA TRR’s Local Map Free-space Roadmap 源码链接 维诺图(Voronoi Diagram map) 源码链接 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/path-planning/common/","title":"Common Knowledges on Path Planning"},{"content":"Cross-entropy Motion Planning 论文连接 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/path-planning/cross-entropy-motion-planning/","title":"Cross-entropy Motion Planning"},{"content":"DiffusionDrive: Truncated Diffusion Model for End-to-End Autonomous Driving 背景知识 条件扩散模型 forward diffusion process 不断往采样出来的数据上增加噪声 $$q(\\tau^{i}|\\tau^{0})=\\mathcal{N}(\\tau^{i};\\sqrt{\\bar{\\alpha}^{i}}\\tau^{0},(1-\\bar{\\alpha}^{i})I)$$ 其中 \\(\\tau^{0}\\)是未添加噪声的初始采样数据， \\(\\tau^{i}\\)是第 \\(i\\)次添加噪声后的数据 \\(\\bar{\\alpha}^{i}=\\prod_{s=1}^{i}\\alpha^{s}=\\prod_{s=1}^{i}(1-\\beta^{s})\\)， \\(\\beta^{s}\\)是noise schedule reverse diffusion process 训练一个噪声去除模型 \\(f_{\\theta}(\\tau^{i},z,i)\\)一步一步从 \\(\\tau^{i}\\)再还原到 \\(\\tau^{0}\\)， \\(\\theta\\)是训练模型的参数。 还原的过程中输入环境信息 \\(z\\)作为指导（条件），使得最终还原出来的 \\(\\tau^{0}\\)和周围的环境有合理的交互 $$p_{\\theta}(\\tau^{0}|z)=\\int p(\\tau^{T})\\prod_{i=1}^{T}p_{\\theta}(\\tau^{i-1}|\\tau^{i},z)d\\tau^{1:T}$$\n预备实验 改造Transfuser为生成式模型 将transfuser模型最后的MLP回归层替换为conditional diffusion model。 将一个随机初始化的noise采样通过20次reverse diffusion操作refine为一条精细的轨迹。 实验证明diffusion模型的结果比MLP回归模型的更好。 模态坍缩 为了探索驾驶行为的多模特性，采样了20个随机噪声（满足高斯分布），通过20次reverse diffusion进行去噪。 结果20个随机采样的噪声最后都收敛到了一个模态。 为了定量分析模态坍缩的现象，定义了一个模态多样性得分\\(D\\)（通过计算每条去噪后的轨迹和所有去噪后轨迹的平均重叠面积计算） $$D=1-\\frac{1}{N}\\sum_{i=1}^{N}{\\frac{Area(\\tau_{i}\\cap \\bigcup_{j=1}^{N}\\tau_{j})}{Area(\\tau_{i}\\cup \\bigcup_{j=1}^{N}\\tau_{j})}}$$ \\(\\tau_{i}\\)表示第 \\(i\\)条去噪轨迹， \\(N\\)是采样轨迹总数， \\(\\bigcup_{j=1}^{N} \\tau_{j} \\)是所有去噪轨迹的并集。 去噪开销 DDIM diffusion模型需要20次去噪将一个随机噪声refine为可用的轨迹，导致计算负担非常大，无法在实车上部署。 模块细节 截断扩散模型 人类驾驶是有一定的固定范式的，没必要从一个完全随机的噪声开始去噪，因此，本文提出的方案尝试从anchored Gaussian distribution开始进行去噪过程。而不是从一个标准的高斯分布（随机噪声）开始去噪。 训练过程 通过K-means算法将训练集中自车行驶的轨迹进行聚类，得到20条anchors \\(\\lbrace a_{k}\\rbrace_{k=1}^{N_{anchor}}\\)，其中 \\(a_{k}=\\lbrace (x_{t},y_{t})\\rbrace_{t=1}^{T_{f}}\\) 向anchor添加噪声： $$\\tau_{k}^{i}=\\sqrt{\\bar{\\alpha}^{i}}a_{k}+\\sqrt{1-\\bar{\\alpha}^{i}}\\epsilon,\\epsilon \\in \\mathcal{N}(0,I)$$ 其中 \\(i \\in [1,T_{trunc}],T_{trunc}\\ll T\\)是扩散步数 diffusion decoder \\(f_{\\theta}\\)输入带噪声的anchor轨迹 \\(\\lbrace \\tau_{k}^{i}\\rbrace_{k=1}^{N_{anchor}}\\)，输出分类score \\(\\lbrace{s_{k}}\\rbrace_{k=1}^{N_{anchor}}\\)和去噪轨迹 \\(\\lbrace\\tau_{k}\\rbrace_{k=1}^{N_{anchor}}\\) $$\\lbrace \\lbrace s_{k},\\tau_{k}\\rbrace\\rbrace_{k=1}^{N_{anchor}}=f_{\\theta}(\\lbrace \\tau_{k}^{i}\\rbrace_{k=1}^{N_{anchor}},z)$$ 其中， \\(z\\)表示条件信息。 将带噪轨迹中最接近GT的轨迹的分类标签设置为true（ \\(y_{k}=1\\)），其他为false（ \\(y_{k}=0\\)），loss设计如下： $$L=\\sum_{k=1}^{N_{anchor}}{[y_{k}L_{rec}(\\tau_{k},\\tau_{gt})+\\lambda BCE(s_{k},y_{k})]}$$ 其中， \\(\\lambda\\)用来平衡L1 reconstruction loss \\(L_{rec}\\)和binary cross-entropy classification loss 推理过程 去噪过程中，前一步去噪得到的轨迹作为输入传入下一步去噪过程，得到新的分类score \\(\\lbrace s_{k}\\rbrace_{k=1}^{N_{anchor}}\\)和去噪轨迹 \\(\\lbrace \\tau_{k}\\rbrace_{k=1}^{N_{anchor}}\\)。 新的去噪轨迹 \\(\\lbrace \\tau_{k}\\rbrace_{k=1}^{N_{anchor}}\\)会通过DDIM的更新规则采样轨迹给到下一步去噪过程。 算法架构 Diffusion decoder 给定一组符合anchored Gaussian distribution的带噪轨迹 \\(\\lbrace \\tau_{k}\\rbrace_{k=1}^{N_{infer}}\\)，通过 deformable spatial cross-attention将其与BEV和PV feature进行交互，得到trajectory feature。 将trajectory feature和感知模块中得到的agent/map query进行cross-attention。 为了对diffusion timestamp进行编码，引入Timestep Modulation layer 最后跟两个MLP，得到confidence score和trajectory offset（用上一层轨迹加上这个offset得到新的轨迹） 不同的diffusion decoder层共用一套模型参数 实验细节 在diffusion decoder层，只和BEV feature进行spatial cross-attention。由于transfuser模型没有map construction结果，只和agent query进行cross-attention。 采样20个anchor 通过2层diffusion decoder进行轨迹去噪。 diffusion schedule 定为50/1000。 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/e2e/diffusion-drive/","title":"DiffusionDrive"},{"content":"可视化链接 Dijkstra 优势：完备性、最优性得到保证 劣势：节点扩展时没有方向的引导 A* 增加了启发值的Dijkstra 启发值\\(H(n)\\)一般是指当前节点\\(n\\)到终点的距离(曼哈顿距离、欧氏距离)，\\(H(n)\\)一定要比真实的n到终点距离小(admissible)，否则A*算法不是最优解 曼哈顿距离不一定是admissible、欧氏距离一定是admissible Weighted A* $$f = g + w×h，w\u0026gt;1$$ 通过调节w，可以让搜索时间加快，但是最优性不能得到保证 启发函数的设计技巧 要尽可能接近当前节点n到终点的真实距离(tight) 在栅格环境下，可斜对角运动时，Diagonal Heuristic \u0026gt; Euclidean Heuristic \u0026gt; Manhattan Heuristic Tie Breaker 很多节点的f是一样的，增加了搜索量。通过修改h，可以让他们有细微的差别：\\(h = h*(1.0+p)\\) Hybrid A* 核心思路 A*是直接将grid的中点作为路径点，Hybrid A*在每个grid里面维护一个具体的点 可以用A*的搜索结果作为启发值，更加tight 扩展邻节点的时候，不是向四周扩展，而是向车辆的运动方向，在满足运动约束的运动范围内扩展 当接近终点时，以一定的概率触发one-shot heuristic，就是直接从当前节点到终点求解obvp，只要无碰撞就算是找到最优路径了。 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/path-planning/dijkstra-astar/","title":"Dijkstra and A*"},{"content":"GameFormer: Game-theoretic Modeling and Learning of Transformer-based Interactive Prediction and Planning for Autonomous Driving 问题建模 输入：\\(N\\)个agents，历史状态，地图（交通灯、车道中心线、边界线、斑马线等） 输出：每个agent输出 \\(M\\)条轨迹及对应score 多次迭代更新agent的预测轨迹 第 \\(i\\)个agent的第 \\(k\\)层策略为 \\(\\pi_{i}^{(k)}=min_{\\pi_{i}}L_{i}^{k}(\\pi_{i}^{(k)}|\\pi_{\\neg i}^{(k-1)})\\)，这边的 \\(\\pi \\)指的是每个agent的多模态预测轨迹（GMM模型） 模块细节 Scene Encoding Input Representation agent的历史状态 \\(S_{p}\\in R^{N*T_{h}*d_{s}}\\)， \\(d_{s}\\)是状态属性的维度 局部地图polyline \\(M\\in R^{N*N_{m}*N_{p}*d_{p}}\\)，对每个agent，找到其周围的 \\(N_{m}\\)个地图元素，每个元素包含 \\(N_{p}\\)个点， \\(d_{p}\\)是属性维度。通过DFS为每个agent构造候选参考线，遍历候选参考线里的车道内的每个路点，记录点的位置和类型、左右边界位置和类型、限速、信号灯、信号牌等. Agent History Encoding 利用LSTM编码历史状态，得到 \\(A_{p}\\in R^{N*D}\\)，包含所有agent的过去历史信息， \\(D\\)是隐藏层特征维度 Vectorized Map Encoding 利用MLP为所有agent编码周围地图信息 \\(M_{p}\\in R^{N*N_{m}*N_{p}*D}\\)，后面将同一个地图元素中的 \\(N_{p}\\)个点通过max pooling聚合，将 \\(N_{p}\\)维度压缩掉（类似于PointNet），新的shape为 \\(M_{r}\\in R^{N*N_{mr}*D}\\)， \\(N_{mr}\\)是每个agent周围的地图元素数量 Relation Encoding 针对每个agent，聚合所有agent（包括它自己）的历史信息和其周围的地图信息， \\(C_{i}=[A_{p},M_{p}^{i}]\\in R^{(N+N_{mr})*D}\\) 通过一个包含 \\(E\\)层的multi-head self-attention transformer计算彼此之间的交互，最终得到一个scene context encoding \\(C_{s}\\in R^{N*(N+N_{mr})*D}\\) Future Decoding with Level-k Reasoning Modality Embedding 为了描述不确定信引入多模态，初始化一个可学习的modality embedding \\(I \\in R^{N*M*D}\\)， \\(M\\)是模态的数量 Level-0 Decoding 输入 \\(I\\)和该agent在scene context encoding中的历史信息编码 \\(C_{s,A_{p}}\\)(by inflating a modality axis)，得到 \\((C_{s,A_{p}}+I)\\in R^{N*M*D}\\)作为query，然后 \\(C_{s}\\)作为key、value。 multi-head cross-attention Transformer作用在每个agent的modality axis上，得到query content features \\(Z_{L_{0}}\\in R^{N*M*D}\\)。 \\(Z_{L_{0}}\\)通过一个MLP解码得到预测轨迹的GMM参数 \\(G_{L_{0}}\\in R^{N*M*T_{f}*4}\\)，4个维度对应每个timestamp的 \\((\\mu_{x},\\mu_{y},log\\sigma_{x},log\\sigma_{y})\\)。再通过另一个MLP解码得到每组预测的score \\(P_{L_{0}}\\in R^{N*M*1}\\) Interaction Decoding \\(k\\)次迭代decode过程 第 \\(k\\)次迭代 取所有agent在 \\(k-1\\)次迭代中生成的轨迹点 \\(S_{f}^{L_{k-1}}\\in R^{N*M*T_{f}*2}\\)（每对点是GMM参数 \\(G_{L_{k-1}}\\)中的 \\(\\mu_{x}, \\mu_{y}\\)），通过MLP和max pooling操作在时间维度上对轨迹进行编码，得到agent的多模预测轨迹编码 \\(A_{mf}^{L_{k-1}}\\in R^{N*M*D}\\) 依据 \\(k-1\\)次迭代中得到的scores \\(P_{L_{k-1}}\\)对 \\(A_{mf}^{L_{k-1}}\\)在模态维度上对进行weighted-average-pooling操作，得到agent的future feature \\(A_{f}^{L_{k-1}}\\in R^{N*D}\\) 通过multi-head self-attention Transformer实现 \\(A_{f}^{L_{k-1}}\\)之间的交互 针对每个agnet，将交互完成后的future feature和编码过程中得到的scene context encoding合并在一起， \\(C_{L_{k}}^{i}=[A_{fi}^{L_{k-1}},C_{s}^{i}]\\in R^{(N+N_{m}+N)*D}\\) 将当前关注agent在第 \\(k-1\\)次迭代中得到的query content features \\(Z_{L_{k-1}}^{i}\\)和该agent多模预测轨迹编码 \\(A_{mf}^{L_{k-1}} \\)组合为query \\((Z_{L_{k-1}}^{i}+A_{mf}^{i,L_{k-1}})\\in R^{M*D}\\)，将更新过的scene context encoding \\(C_{L_{k}}^{i}\\)作为key和value过一遍multi-head cross-attention Transformer 通过mask策略阻止第 \\(i\\)个agent在第 \\(k\\)次迭代中获取到它自己在前面几次迭代中的future interaction features。只能获取周围其他障碍的future interaction features 最终得到的query content tensor \\(Z_{L_{k}}^{i}\\)通过两个MLP解码得到预测轨迹的GMM参数和每组预测的score 训练过程 Imitation Loss 采用imitation loss作为主要loss来规范agent的行为，以此来学习交通规则和驾驶风格等 通过negative log-likelihood loss，对比best prediction \\(m^{*}\\)（和gt最接近的预测结果）和gt来计算imitation loss $$L_{IL}=\\sum_{t=1}^{T_{f}}{L_{NLL}(\\mu_{m^{*}}^{t},\\sigma_{m^{*}}^{t},p_{m^{*}},s_{t})}$$\n$$L_{NLL}=log\\sigma_{x}+log\\sigma_{y}+\\frac{1}{2}((\\frac{dx}{\\sigma_{x}})^{2}+(\\frac{dy}{\\sigma_{y}})^{2})-log(p_{m^{*}})$$\n其中， \\(ds=s_{x}-\\mu_{x}\\)， \\(dy=s_{y}-\\mu_{y}\\)， \\((s_{x},s_{y})\\)是gt轨迹点； \\(p_{m^{*}}\\)是best prediction对应的网络输出概率 Auxiliary Loss 充分考虑了每个agent之间的交互 通过一个interaction loss鼓励每个agent避免和其他agent在 \\(k-1\\)次迭代中生成的future traj碰撞。 通过一个斥力场来拉开各个agent轨迹之间的距离 $$L_{inter}=\\sum_{m=1}^{M}{\\sum_{t=1}^{T_{f}}{\\max\\limits_{\\forall j\\neq i,\\forall n \\in 1:M}\\frac{1}{d(\\hat s_{m,t}^{(i,k)},s_{n,t}^{(j,k-1)})+1}}}$$ 其中 \\(d(\\cdot,\\cdot)\\)是预测轨迹点的 \\(L2\\)距离， \\(m\\)是agent \\(i\\)的预测轨迹模态， \\(n\\)是（ \\(k-1\\)次迭代中）agent \\(j\\)的预测轨迹模态。 为了确保安全loss只在近距离被激活，引入一个安全阈值，只有距离小于安全阈值才有loss惩罚 Total Loss $$L_{i}^{k}(\\pi_{i}^{(k)})=w_{1}L_{IL}(\\pi_{i}^{(k)})+w_{2}L_{inter}(\\pi_{i}^{(k)},\\pi_{\\neg i}^{(k-1)})$$\n实验 环境 数据集 Waymo open motion dataset(WOMD) nuPlan dataset 实验模型 预测模型 采用WOMD训练 每次只评价两个被标记的交互障碍物的未来8s轨迹和真值的差距 评价指标包括：最小平均位移误差minADE、最小最终位移误差minFDE、未命中率 miss rate、平均精度mAP 规划模型 采用WOMD和nuPlan训练\n开环\nplanning ADE、collision rate、miss rate、prediction ADE 闭环\n成功率（无碰撞且无偏航）、导航完成进度、纵向acc \u0026amp; jerk、横向acc、位置误差 迭代层数 \\(k=4\\)效果最好\n消融实验 agent future modeling的作用 第 \\(k-1\\)层得到的future trajectory不输入第 \\(k\\)层的解码过程 输入上一层的future trajectory，但不进行self-attention操作 训练过程中不引入interaction loss(Auxiliary Loss) 解码器结构的作用 用 \\(k\\)个共享权重的decoder代替当前设计中 \\(k\\)个独立的decoder 用一个multi-layer transformer代替当前设计中 \\(k\\)个独立的decoder 代码解读 文件结构 open_loop_planning data_process.py：以帧为单位打包ego, neighbors, lanes, crosswalk, ref_line数据 open_loop_test.py：开环方式跑测试集统计相关指标 train.py：规划模型训练 interaction_prediction data_process.py：同上 train.py：预测模型训练 model GameFormer.py：GF网络主体结构 modules.py：子网络的实现 utils inter_pred_utils.py：loss定义、metrics定义 data_utils.py：地图存储、搜错，data normalization cubic_spline_planner.py：Spline类定义 open_loop_test_utils.py：open_loop_test.py中工具函数的实现 open_loop_test_train.py：open_loop_planning/train.py中工具函数的实现 核心代码 GameFormer.py GF网络主体，encoder + decoder GM.encoder agent encoding( \\(B*N*D\\))\n这里的ego encoder和agent encoder都是AgentEncoder类，concat(LSTM(state), MLP(type)) map encoding( \\(B*N_{mr}*N_{pts}*D\\))\nLaneEncoder: PE(concat(MLP(pts), MLP(spd_limit), embeding(type)))，PE用的三角函数式绝对位置编码 CrosswalkEncoder: MLP(pts) fusion encoding( \\(N*(N+N_{mr})*D\\))\nsegment_map核心是max pooling，将 \\(N_{pts}\\)维度压缩掉 fusion_encoder是一个6层的transformer_encoder，计算每个agent和周围其他agent以及地图元素的关系，得到每个agent的周围环境的编码scene_context_encoding（循环 \\(N\\)次后stack）。 GM.decoder level 0 encoding\n核心是initial_stage函数\n这里输入的encoding是id对应的agent的周围环境信息 \\(\\in R^{(N+N_{mr})*D}\\) multi_modal_query \\(\\in R^{M*D}\\)，模态 \\(M\\)的值为6 agent_query \\(\\in R^{D}\\)也是取自一个随机初始化的可学习的embedding 前面两者相加得到一个多模态的agent query \\(\\in R^{M*D}\\)，再和GM.encoder中对每个agent计算的scene_context_encoding \\(\\in R^{(N+N_{mr})*D}\\)中该agent对应的历史状态维度 \\(\\in R^{N*None*D}\\)相加（在模态维度上新增一个维度），得到最终的query \\(\\in R^{M*D}\\) 将scene_context_encoding \\(\\in R^{(N+N_{mr})*D}\\)作为key和value，与上面的query在模态维度上进行cross attention，得到query content \\(\\in R^{M * D}\\)，表征该agent的不同预测模态和周围环境的交互编码 self.predictor即GMMPredictor类，本质上是2个MLP，一个回归GMM参数 \\(\\in R^{B*M*T*4}\\)，一个回归score \\(\\in R^{B*M}\\) Interaction Decoding\n核心是interaction_decoder函数（ \\(k\\)个相互独立的InteractionDecoder），它不断拿上一层计算出来的其他agent的predict和score去更新当前关注的agent的未来轨迹（先猜别人怎么走，再思考自己怎么应对）\nfuture encoding是编码其他agent在上一层（ \\(k-1\\)层）计算的prediction和score， \\(\\in R^{N*D}\\) 通过self attention计算上一层的预测之间的关联，并将结果不断concat到对应agent的scene context encoding当中（所以每个agent的scene context encoding包含了最开始的环境中每个障碍物的历史信息，地图信息，以及后面1～ \\(k-1\\)层的future interaction信息） 注意，这里有个mask，是为了让每个agent不看自己在前1～ \\(k-1\\)层的future encoding。 这里把当前关注agent在 \\(k-1\\)层的query content和该agent的预测轨迹编码相加作为query，该agent的scene context encoding作为key和value，通过multi-head cross-attention Transformer得到该agent在本层的query content 最后通过一个GMMPredictor回归最新的GMM prediction和scores ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/e2e/game-former/","title":"GameFormer"},{"content":"GenAD: Generative End-to-End Autonomous Driving 方案细节 Instance-Centric Scene Representation Image to BEV 初始化一个 \\(H\\times W\\)的BEV query \\(B_{0}\\)，将摄像头提取的multi-scaled feature \\(F\\)与之进行deformable cross-attention，得到BEV feature\n$$B=DA(B_{0},F,F)$$\n\\(DA(Q,K,V)\\)表示deformable cross-attention BEV to Map 借鉴VAD的思路，初始化一个learnable query \\(M_{0}\\)，集合BEV feature通过global cross-attention得到地图元素表达 \\(M\\)\n$$M=CA(M_{0},B,B)$$\nBEV to agent 类似地图元素，agent特征的提取方式为：\n$$A=DA(A_{0},B,B)$$\n\\(A_{0}\\)是learnable query 这边得到agent特征后，会接一个3D目标检测head进行agent的位置、朝向、种类的解码 Instance-centric scene representation 将上面学到的agent特征和ego query拼在一起， \\(I=concat(e,A)\\) 学习agent和ego之间的相互影响， \\(I=SA(I,I,I)\\)， \\(SA(Q,K,V)\\)表示self-attention 学习agent、ego和地图之间的交互， \\(I=CA(I,M,M)\\) Trajectory Prior Modeling 自车和障碍物的轨迹都是高度结构化的（连续的），并遵循一定的模式。例如，当车辆以恒定速度行驶时，大多数轨迹都是直线，当车辆右转或左转时，其中一些轨迹是曲率接近恒定的曲线。只有在极少数情况下，轨迹才会是曲折的。考虑到这一点，我们采用变分自动编码器（VAE）架构来学习latent spaec \\(Z\\)，以此来建模trajectory prior。\n这里利用一个GT轨迹encoder \\(e_{f}\\)来建模 \\(p(z|\\mathsf{\\mathbf{T}}(T,f))\\)，它将未来轨迹映射到latent spaec \\(Z\\)。encoder \\(e_{f}\\)输出一组 \\(\\mu_{f}\\)和 \\(\\sigma_{f}\\)来表示GMM的均值和方差。\n\\(e_{f}\\)代码实现\n$$p(z|\\mathsf{\\mathbf{T}}(T,f)) \\thicksim N(\\mu_{f},\\sigma_{f})$$\n分布 \\(p(z|\\mathsf{\\mathbf{T}}(T,f))\\)包含未来轨迹的先验，可用于提高障碍物和自车的运动预测和规划的真实性。\nLatent Future Trajectory Generation 有了未来轨迹的latent distribution作为先验后，我们需要将未来轨迹从latent spaec \\(Z\\)中解码出来。最直接的办法是用一个MLP在BEV space下解码出轨迹点，但是这种方法无法模拟障碍物和自车随时间变化产生的交互。为了建模每个实例在不同时间戳之间的时间上的联系，对联合概率分布 \\(p(\\mathsf{\\mathbf{T}}(T,f)|z)\\)进行分解：\n$$p(\\mathsf{\\mathbf{T}}(T,f)|z)=p(w^{T+1}|z^{T})\\cdot p(w^{T+2}|w^{T+1},z^{T})\u0026hellip;p(w^{T+f}|w^{T+1},\u0026hellip;,w^{T+f-1},z^{T})$$\n我们从分布 \\(N(\\mu_{f},\\sigma_{f})\\)中采样一组 \\(\\mu_{f}\\)和\\(\\sigma_{f}\\)作为当前时刻的 \\(z^{T}\\)的latent state。不同于其他方法中一次性将轨迹decode出来，这边是采用分步解码的方法。引入decoder \\(d_{w}\\)（MLP）从latent state中解码轨迹点。 \\(p(w^{T+1}|z^{T})\\)通过 \\(w=d_{w}(z)\\)来表示。\n采样代码实现： 我们通过GRU模型来建模状态随时间的演化。 \\(z^{T+1}=g(z^{T})\\)，然后再解码下个时刻的轨迹点， \\(w^{T+1}=g(z^{T+1})\\)， \\(T+f\\)时刻同理。\n从distribution里面采样出的latent state进行future state的预测\n这边的self.predict_model函数里包含了GRU的实现，但是从代码上看，没有按照T循环调用GRU，而是设计了一个4层（layer_num=4）的GRU模拟状态随时间的推演。\ndecoder是按照T循环解码出的轨迹点（下面的for循环），这里的self.ego_fut_decoder是个MLP，将GRU输出的latent future state转化为轨迹点\nGenerative End-to-End Autonomous Driving 轨迹loss\n$$J_{prior}=L_{traj}(\\hat{T_{e}},T_{e})+\\frac{1}{N_{a}}L_{traj}(\\hat{T_{a}},T_{a})+\\lambda_{c}L_{focal}(\\hat{C_{a}},C_{a})$$\n\\(L_{traj}()\\)衡量两条轨迹的L1 loss。 \\(N_{a}\\)是agent的数量， \\(C_{a}\\)是agent的预测类别， \\(T_{a}\\)是agent的预测轨迹。 将上面得到的instance tokens \\(I\\)通过encoder \\(e_{i}\\)映射到latent space \\(Z\\)，也类似地输出一组输出一组 \\(\\mu_{i}\\)和 \\(\\sigma_{i}\\)。\n\\(e_{i}\\)代码实现：\n$$p(z|I)\\thicksim N(\\mu_{i},\\sigma_{i})$$\n通过KL散度衡量 \\(p(z|I)\\)和GT轨迹 \\(p(z|\\mathsf{\\mathbf{T}}(T,f))\\)之间的差异\n$$J_{plan}=D_{KL}(p(z|I),p(z|\\mathsf{\\mathbf{T}}(T,f)))$$ KL散度衡量的就是上面的output_distribution中present和future的分布差异 总loss\n$$J_{GenAD}=J_{prior}+\\lambda_{plan}J_{plan}+\\lambda_{map}J_{map}+\\lambda_{det}J_{det}$$\n在infer阶段，弃用future trajectory encoder（因为没有GT），从 \\(p(z|I)\\)中采样latent state，作为轨迹生成模块的输入\n","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/e2e/gen-ad/","title":"GenAD"},{"content":"GoalFlow: Goal-Driven Flow Matching for Multimodal Trajectories Generation in End-to-End Autonomous Driving 贡献 设计了一种创新的目标点确立方法，通过实验验证了其在引导生成模型进行轨迹生成方面的有效性。 我们将flow matching技术首次引入端到端自动驾驶系统，实现了与目标点引导机制的无缝融合。 开发了基于影子轨迹的创新轨迹选择机制，通过虚拟轨迹分析有效缓解潜在目标点偏差问题。 在Navsim仿真平台上，本方法取得了当前最优的测试结果。 方案 预备知识 相较于扩散模型专注于学习逐步逆向消除时序叠加噪声以恢复数据，flow matching着力于学习数据分布间的可逆映射变换。设\\(\\pi_{0}\\)表示简单分布（通常为标准正态分布\\(p(x)=N(x|0,I)\\)），\\(\\pi_{1}\\)表示目标分布。设\\(x_{0}\\)采样自\\(\\pi_{0}\\)分布，\\(x_{1}\\)采样自\\(\\pi_{1}\\)分布，时间参数\\(t\\in\\lbrack0,1\\rbrack\\)。定义\\(x_{0}\\)到\\(x_{1}\\)的传输路径为直线形式，即中间状态\\(x_{t}=(1−t)x_{0}+tx_{1}\\)，其演化方向始终由\\(x_{1}−x_{0}\\)确定。通过构建神经网络\\(v_{\\theta}\\)基于当前状态\\(x_{t}\\)与时序参数\\(t\\)预测方向量\\(x_{1}−x_{0}\\)，可优化\\(v_{\\theta}(x_{t},t)\\)与\\(x_{1}−x_{0}\\)间的损失函数，从而获得从初始分布\\(\\pi_{0}\\)到目标分布\\(\\pi_{1}\\)的传输路径。 在此框架下，rectified flow(Flow straight and fast: Learning to generate and transfer data with rectified flow)通过最优传输位移量构建路径，该方法兼具简洁性与高效性。 概述 GoalFlow整体架构包含三个核心模块：在感知模块中，通过融合摄像头图像\\(I\\)与激光雷达点云\\(L\\)，提取蕴含环境信息的BEV特征\\(F_{bev}\\)；目标点构建模块专注于生成精确的轨迹引导信息，通过构建目标点词典并采用评分\\(V=\\lbrace g_{i}\\rbrace^{N}\\)机制筛选最优目标点\\(g\\)；轨迹规划模块首先生成多模态轨迹集合\\(T=\\lbrace \\hat{\\tau_{i}} \\rbrace^{M}\\)，随后通过轨迹评分机制确定最优轨迹\\(\\tau\\)。 感知模块 在第一步多模态数据融合中，我们融合图像与激光雷达数据生成BEV特征\\(F_{bev}\\)以捕获丰富的道路信息。单一传感器存在信息缺失问题：例如激光雷达无法获取交通灯状态，而摄像头难以精确定位物体。 本工作采用Transfuser架构实现模态融合。前视、左视、右视摄像头视角拼接为单张图像\\(I\\in R^{3\\times H_{1}\\times W_{1}}\\)，激光雷达点云数据构建为张量\\(L\\in R^{K\\times 3}\\)。两类数据经过独立主干网络提取特征后，通过多级Transformer模块进行跨模态特征融合，最终生成全面表征场景的BEV特征\\(F_{bev}\\)。为确保自车与周边物体及地图信息的有效交互，我们基于高精地图与边界框提供的损失函数对BEV特征实施辅助监督。 目标点构建模块 在本模块中，我们通过构建精确目标点为轨迹生成过程提供引导。无约束的基于扩散方法常导致轨迹过度发散，增加轨迹选择复杂度。我们的核心发现在于：目标点包含对短期未来位置的精准描述，可为生成模型施加强约束。因此，将传统规划模块拆分为两步执行：首先构建精确目标点，其次通过规划生成轨迹。 目标点库：受VADv2启发，我们通过对轨迹终点空间进行离散化构建候选目标点集，实现不依赖高精地图的解决方案。具体而言，在训练数据中对轨迹端点\\(p_{i}=(x_{i},y_{i},\\theta_{i})\\)进行聚类得到\\(N\\)个聚类中心，形成目标点词典\\(V\\)。为确保词典能表征细粒度的空间位置，通常将\\(N\\)设置为较大数值（4096或8192）。 目标点选择模块 高质量轨迹通常具备以下特征：与真实轨迹距离接近且位于可行驶区域内。为此，我们通过距离评分\\(\\hat{\\delta}^{dis}\\)与可行驶区域合规评分\\(\\hat{\\delta}^{dac}\\)对目标点词典\\(V\\)中的每个候选点\\(g_{i}\\)进行双重评估。距离评分衡量目标点\\(g_{i}\\)与真实轨迹终点\\(g^{gt}\\)的接近程度，采用连续值表示，取值范围\\(\\hat{\\delta}^{dis}\\in\\lbrack0,1\\rbrack\\)，数值越大表示与\\(g^{gt}\\)越接近；可行驶区域合规评分确保目标点位于合法驾驶区域，采用二元值\\(\\hat{\\delta}^{dac}\\in\\lbrace 0,1\\rbrace\\)，1表示目标点符合可行驶区域要求，0则表示处于非驾驶区域。 为构建目标距离评分\\(\\hat{\\delta_{i}}^{dis}\\)，我们采用softmax函数将目标点\\(g_{i}\\)与真实目标点\\(g^{gt}\\)之间的欧氏距离映射至\\(\\lbrack0,1\\rbrack\\)区间： $$\\hat{\\delta_{i}}^{dis}=\\frac{exp(-\\Vert g_{i}-g^{gt}\\Vert_{2})}{\\sum_{j} exp(-\\Vert g_{j}-g^{gt}\\Vert_{2})}$$\n针对可行驶区域合规评分\\(\\hat{\\delta_{i}}^{dac}\\)的构建，我们引入影子车辆概念——其边界框根据目标点\\(g_{i}\\)的位置与航向角\\((x_{i},y_{i},\\theta_{i})\\)以及自车几何参数确定。设\\(\\lbrace p^{j}\\rbrace^{4}\\)表示影子车辆四个角点的位置集合，\\(DA \\)为表征可行驶区域的多边形区域，则合规评分定义为： $$\\hat{\\delta_{i}}^{dac}=\\begin{cases} 1 \u0026amp;\\text{if } \\forall j,p^{j} \\in DA \\ 0 \u0026amp;\\text{if } otherwise \\end{cases}$$\n我们通过综合\\(\\hat{\\delta_{i}}^{dis}\\)与\\(\\hat{\\delta_{i}}^{dac}\\)计算最终评分，选取最终评分最高的目标点作为轨迹生成依据。 $$\\hat{\\delta_{i}}^{final}=w_{1}log\\hat{\\delta_{i}}^{dis}+w_{2}log\\hat{\\delta_{i}}^{dac}$$\n如图3(a)所示，基于Transformer的评分解码器以特征向量\\(F_{v}\\)与\\(F_{ego}\\)的加和结果作为查询向量，BEV特征\\(F_{bev}\\)作为键/值向量。解码输出经两个独立的多层感知机(MLP)处理后，为词典\\(V\\)中各候选点生成\\(\\hat{\\delta}^{dis}\\)与\\(\\hat{\\delta}^{dac}\\)评分。图3(b)展示了双评分分布可视化结果，其中暖色调点表示更高评分。实验观测表明：\\(\\hat{\\delta}^{dis}\\)有效指示期望未来位置，而\\(\\hat{\\delta}^{dac}\\)可精准识别目标点是否位于可行驶区域 轨迹规划模块 在轨迹规划模块中，我们通过生成模型产生受约束的高质量轨迹候选集，并基于评分机制筛选最优轨迹。基于扩散方法的生成模型如DDPM和DDIM通常需要复杂的去噪路径，导致推理阶段产生显著时间开销，难以满足自动驾驶等实时系统的需求。相比之下，基于flow matching中最优传输路径的rectified flow仅需少量推理步骤即可获得理想结果。本工作采用rectified flow作为生成模型，以BEV特征\\(F_{bev}\\)与目标点\\(g\\)为条件，生成多模态轨迹集合。 我们通过建模从噪声分布到目标轨迹分布的迁移过程生成多模态轨迹。在此分布迁移过程中，给定当前状态\\(x_{t}\\)及时序参数\\(t\\)预测轨迹偏移量\\(v_{t}\\)。 $$v_{t}=\\tau^{norm}-x_{0}$$\n$$x_{t} = (1-t)x_{0}+t\\tau^{norm}$$\n$$\\tau^{norm} = \\cal H(\\tau^{gt})$$\n其中\\(\\tau^{gt}\\)是轨迹GT，\\(\\tau^{norm}\\)是它的归一化形式。定义\\(\\cal H(\\cdot)\\)为轨迹归一化运算。\\(x_{0}\\)变量表征噪声分布，服从\\(x_{0}\\thicksim N(0,\\sigma^{2}I)\\)。通过\\(x_{0}\\)与\\(\\tau^{norm}\\)的线性插值获得中间状态\\(x_{t}\\)。 如图4所示，通过级联编码器提取多维度特征：\\(x_{t}\\)经线性层编码，时序参数\\(t\\)与目标点通过正弦编码转化为特征向量，BEV特征\\(F_{bev}\\)与自车特征\\(F_{ego}\\)经环境编码器融合获得环境特征\\(F_{env}\\)。 $$F_{env}=E_{env}(Q,(F_{BEV}+F_{ego}),(F_{BEV}+F_{ego}))$$\n其中，\\(E_{env}\\)指基于Transformer架构的编码器，\\( Q\\)表示可学习嵌入向量，\\(F_{ego}\\)表征自车状态特征，其中编码了自车的运动学信息（如速度、加速度等）。 通过将环境特征\\(F_{env}\\)、目标点特征\\(F_{goal}\\)、轨迹特征\\(F_{traj}\\)及时序特征\\(F_{t}\\)进行特征拼接，形成综合特征\\(F_{all}\\)——该特征集成当前状态、时序信息及场景上下文。组合特征随后输入多层注意力网络，最终预测分布迁移量\\(v_{t}\\)。 $$\\hat{v_{t}}=\\cal G(F_{all},F_{all},F_{all})$$\n$$F_{all} = Concat(F_{env}, F_{goal}, F_{traj}, F_{t})$$\n其中，网络\\(\\cal G\\)由\\(N\\)层注意力模块构成。我们基于噪声分布\\(x_{0}\\)与预测迁移量\\(\\hat{v_{t}}\\)重构轨迹分布。具体而言，通过修正流执行多次推理步骤，逐步将噪声分布\\(x_{0}\\)转换为目标分布\\(\\tau^{norm}\\)，最终对\\(\\tau^{norm}\\)实施去归一化处理得到最终轨迹\\(\\hat{\\tau}\\) $$\\hat{\\tau} = \\cal H^{-1}(\\hat{\\tau}^{norm})$$\n$$\\hat{\\tau}^{norm}=x_{0}+\\frac{1}{n}\\sum_{i}^n\\hat{v} _{t _{i}}$$\n式中，\\(n\\)表示总推理步数，\\(t_{i}\\)为第\\(i\\)步采样的时间步参数，满足\\(t_{i}\\in\\lbrack0,1\\rbrack\\)。\\(\\cal H^{-1}(\\cdot)\\)表示去归一化运算，将归一化轨迹恢复至实际物理尺度。 在轨迹选择环节，SparseDrive与Diffusion-ES等方法需对生成轨迹进行运动学仿真，预测其与周边交通参与者的潜在碰撞风险以筛选最优轨迹，该过程显著增加推理耗时。本方法通过目标点引导实现轨迹选择的流程简化：综合考虑轨迹与目标点的距离偏差及自车行驶进度，借助轨迹评分器完成最优轨迹选择。TODO：没有显示碰撞检测了，只关注导航进度和轨迹终点约束，真的能选出合理的轨迹吗? $$f(\\hat{x} _{i})=-\\lambda _{1}\\Phi(f _{dis}(\\hat{\\tau} _{i}))+\\lambda _{2}\\Phi(f _{pg}(\\hat{\\tau} _{i}))$$\n式中，\\(\\Phi(\\cdot)\\)表示极小极大运算。\\(f_{dis}( \\hat{\\tau_{i}} )\\)计算轨迹\\(\\hat{\\tau_{i}}\\)与目标点\\(g\\)的\\(L2\\)距离偏差，\\(f_{pg}(\\hat{\\tau_{i}})\\)评估轨迹\\(\\hat{\\tau_{i}}\\)形势进度的\\(L_{2}\\)距离指标。 此外，预测目标点可能存在偏差误导轨迹生成。为此，我们在生成阶段对目标点进行屏蔽处理，构建影子轨迹。若影子轨迹与主轨迹呈现显著偏离，则判定该目标点不可靠，转而采用影子轨迹作为最终输出。影子轨迹应该就是没有goal引导的情况下得到的一条fallback轨迹 训练Loss 首先，我们专门优化感知特征提取器，施加多感知监督损失，包括高精地图交叉熵损失\\(L_{HD}\\)、3D边界框分类损失\\(L_{bbox}\\)以及边界框位置回归的\\(L_{1}\\)损失\\(L_{loc}\\)。该阶段旨在通过多感知监督增强BEV特征的信息表征能力。具体损失函数定义如下： $$L_{perception}=w_{1}L_{HD}+w_{2}L_{bbox}+w_{3}L_{loc}$$\n其中，\\(w_{1}=10,w_{2}=1,w_{3}=10\\)。对目标生成器的distance score(\\(L_{dis}\\)) 和 DAC score(\\(L_{Ldac}\\))采用cross entropy loss，\\(w_{4}=1,w_{5}=0.005\\)。 $$L_{goal} = w_{4} L_{dis} + w_{5} L_{dac}$$\n$$L_{dis}=-\\sum_{i=i}^{N} \\delta_{i}^{dis}log(\\hat{\\delta}_{i}^{dis})$$\n$$L_{dac} = −\\delta^{dac}log\\hat{\\delta}^{dac}−(1−\\delta^{dac})log(1−\\hat{\\delta}^{dac})$$\n对多模态规划器施加\\(L_{1}\\)loss $$L_{planner} = \\vert v_{t} − \\hat{v}_{t}\\vert$$\n实验 数据集 本实验在Openscene数据集完成验证。该数据集包含120小时自动驾驶数据，其端到端仿真环境Navsim采用1192个训练验证场景与136个测试场景，共计超10万样本（采样频率2Hz）。每个样本包含：8个视角的摄像头图像、5个激光雷达的融合点云、自车状态信息以及高精地图与目标物的标注信息。 评价指标 在Navsim仿真环境中，生成的2Hz、4秒时长的初始轨迹通过LQR控制器进行插值处理，得到10Hz、4秒时长的平滑轨迹。通过以下闭环指标对轨迹进行综合评分：无责任碰撞率(\\(S_{NC}\\))、可行驶区域合规率(\\(S_{DAC}\\))、带边界约束的碰撞时间(\\(S_{TTC}\\))、自车行驶进度(\\(S_{EP}\\))、舒适性指数(\\(S_{CF}\\))及行驶方向合规率(\\(S_{DDC}\\))。最终评分通过加权聚合上述指标获得。由于实际约束条件限制，\\(S_{DDC}\\)指标未纳入最终评分计算。 $$S_{PDM}=S_{NC}\\times S_{DAC}\\times S_{TTC}\\times (\\frac{5\\times S_{EP} + 5 \\times S_{CF} + 2\\times S_{DDC} }{12})$$\n基线算法 恒定速度模型(Constant Velocity)：假设自车保持当前速度匀速前进 自车状态MLP(Ego Status MLP)：仅以自车当前状态为输入，通过多层感知机生成轨迹 PDM-Closed：以真实感知为输入，通过基于规则的IDM方法生成多条轨迹，并由PDM评分器筛选最优轨迹 Transfuser：融合摄像头与激光雷达输入，通过Transformer生成BEV特征用于轨迹生成 LTF：Transfuser的轻量版本，将激光雷达主干网络替换为可学习嵌入向量，在NavSim中取得与Transfuser相近效果 UniAD：采用多组Transformer架构差异化处理信息，通过查询机制实现规划专用信息传递 PARA-Drive：基于BEV特征并行执行地图构建、路径规划、运动预测及占用预测任务，相较UniAD实现方案创新 模型参数设置 rectified flow的训练遵循无分类器引导范式，通过在条件集中随机掩码特征以增强模型鲁棒性。训练阶段使用真实轨迹的终点指导流匹配过程，测试阶段则通过目标点词典中筛选最高评分点确定轨迹生成目标。采样过程采用非线性重缩放时间步长平滑方法（替代均匀间隔采样）。每次推理生成128/256条候选轨迹，经轨迹评分器筛选确定最优解。所有实验均在4个计算节点上完成，每个节点配置8块RTX 4090或RTX 3090显卡。 实验结果 如表1所示，我们将本方法与当前端到端自动驾驶领域多种前沿算法进行对比（最优指标以加粗显示）。Navsim环境测试表明，GoalFlow在综合评分上持续超越其他方法。具体而言，本方法在可行驶区域合规率(DAC)指标上较次优方法提升5.5分，自车行驶进度(EP)指标提升5.7分，证明GoalFlow对车辆行驶区域的约束力更强，有效提升系统安全性。此外，本方法在保障安全的前提下实现了更高的行驶速度。补充实验中，将预测目标点替换为真实轨迹终点时，评分达到92.1分（接近人类驾驶轨迹的94.8分），显著验证目标点在自动驾驶中的强引导能力。 消融实验： 如表2所示，我们通过消融实验验证各组件对系统性能的影响。其中，M0表示仅采用修正流生成轨迹的基础模型。实验结果显示，M0在Navsim平台上持续超越基线方法，尤其在可行驶区域合规率(DAC)与碰撞时间(TTC)指标表现突出，表明基于流匹配的基础模型有效学习了与地图信息及周边交通参与者的交互规律，验证了流模型自身具备强大的场景建模能力。 M1在M0基础上引入距离评分分布建模，筛选最高分目标点指导修正流。实验显示该方法带来最显著的性能提升，证实了将轨迹规划任务解耦为\u0026quot;目标点预测→目标点引导轨迹生成\u0026quot;两阶段策略的有效性。具体而言，通过将复杂任务分解为两个更简单的子任务（目标点预测与目标点引导轨迹生成），系统性能获得显著优化。 M2在M1基础上增加可行驶区域合规评分(DAC)分布预测。主要改进体现在DAC指标上，通过引入多视角评估器，模型得益于更鲁棒的评估体系，从而提升整体表现。 M3进一步整合包含轨迹选择与目标点验证机制的轨迹评分器，使GoalFlow系统可靠性得到全面提升。 推理步骤影响分析 如表3所示，我们测试了不同去噪步数对系统性能的影响。实验表明：当推理步数从20步递减至1步时，各项评分保持稳定。值得注意的是，即使仅执行单步推理，系统仍能保持优异性能。这凸显了流匹配相较扩散模型的优势——流匹配采用直线型传输路径，在推理阶段仅需少量步骤即可完成噪声分布到目标分布的迁移。当推理步数从20步减至1步时，单样本去噪时间降至原始时间的6%，这种高效推理特性对实时性要求严苛的自动驾驶系统至关重要。 初始噪声影响研究 实验中，初始噪声服从高斯分布\\(N(0,\\sigma^{2}I)\\)。如表4所示，我们探究了噪声方差对轨迹生成的影响。结果表明：噪声设置对评分具有显著影响。当噪声方差过高（\\(\\sigma=0.3\\)时），生成轨迹呈现过度抖动特性，舒适度评分骤降至2.23，表明轨迹形态失去连贯性；反之，当噪声方差过低时，流匹配易退化为回归模型，导致可供筛选的轨迹多样性降低，进而影响整体评分表现。 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/e2e/goal-flow/","title":"GoalFlow"},{"content":"JPS JPS的算法框架和A*是一样的，但是节点的扩展规则不一样。A*是无差别扩展每个节点的邻节点。而JPS是跳跃着扩展。 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/path-planning/jps/","title":"Jump Point Search"},{"content":"优化目标 Minimum Snap Optimization 轨迹的能量消耗最小 7次多项式拟合 Minimum Jerk Optimization 轨迹的舒适度最高 5次多项式拟合 约束 约束项 起点终点状态约束 中间点位置约束 连续性约束 障碍物避碰（带入非凸性) 约束类型 硬约束 一旦优化过程中出现不满足约束条件的状态，问题就不可解 对所有安全空间同等对待 对噪声非常敏感 软约束 即使出现不满足约束条件的状态，依然可以继续求解最优解 时间分配 在轨迹优化的过程中，相邻两个全局离散点中间的运动时间是需要事先设定的。 时间分配将很大程度影响到轨迹的形状 简单方法 假设两点间的运动都是加速到最大，减速到0的过程，计算消耗时间 假设两点间的运动是期望速度的匀速运动 简单方法看起来很傻，但是在基于飞行走廊的轨迹优化中效果很好，因为飞行走廊实际上给了中间点位置调整的空间。 最优方法 将整条轨迹总时间加入优化目标 将每一段所分配的时间加入待优化目标中 求解优化结果 凸优化问题解决方法 方法 闭式解 Linear Programming (LP) Quadratic Programming (QP) Quadratically Constrained QP (QCQP) Second-Order Cone Programming (SOCP) 库 CVX(matlab) Mosek(可以解决几乎所有的凸优化问题，非常鲁棒，只提供x86执行文件) OOQP(可求解二次规划，非常快速，代码开源) GLPK(可求解线性规划，非常快速，代码开源) OSQP(百度Apollo用的二次规划求解器) 优化器大全 数值稳定性 Normalization Time normalization 在对全局路径点进行分段优化的时候，每一段起点的时间\\(T_{n}\\)都设为0，终点时间(T_{n+1} = Tn+1 - Tn\\) Problem scale (spatial) normalization 假如轨迹优化是在一个大尺度场景下进行，路点的坐标在数值会有巨大的差异，需要通过某些手段将坐标值进行预处理 一些工程问题 对三个轴同时优化好还是分别优化好？ 分别优化更加稳定和快速 闭式解永远是更好的吗？ 当矩阵运算非常消耗资源时，数值优化更加鲁棒些 Mosek非常非常的稳定 多项式可以在任何场合下作为最优轨迹的表达式吗？ 大部分时候可以，但也有例外 当优化目标不是单一的平方项的时候(\\(jerk*jerk + snap*snap\\))，多项式形式就不是轨迹的最优解形式了 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/path-planning/optimization/","title":"Optimization"},{"content":"基本信息 官网 二次规划库，同OOQP 使用方式 问题描述 Python代码 import osqp import numpy as np from scipy import sparse # Define problem data P = sparse.csc_matrix([[4, 1], [1, 2]]) q = np.array([1, 1]) A = sparse.csc_matrix([[1, 1], [1, 0], [0, 1]]) l = np.array([1, 0, 0]) u = np.array([1, 0.7, 0.7]) # Create an OSQP object prob = osqp.OSQP() # Setup workspace and change alpha parameter prob.setup(P, q, A, l, u, alpha=1.0) # Solve problem res = prob.solve() 简单易懂 C/C++代码 #include \u0026#34;osqp.h\u0026#34; int main(int argc, char **argv) { // Load problem data c_float P_x[3] = {4.0, 1.0, 2.0, };//目标矩阵的非零值 c_int P_nnz = 3; //目标矩阵的非零值的个数 c_int P_i[3] = {0, 0, 1, }; //目标矩阵的非零值所在的row，与P_x一一对应 //\bP_p[i]=n,P_p[i+1]=m, 表示 //for k from n to m: // 将P_x[k]填在第i列，P_i[k]行 c_int P_p[3] = {0, 1, 3, }; //每一列的第一个非零元素所对应的P_x数组的indice，最后一个值肯定是P_nnz c_float q[2] = {1.0, 1.0, }; c_float A_x[4] = {1.0, 1.0, 1.0, 1.0, }; c_int A_nnz = 4; c_int A_i[4] = {0, 1, 0, 2, }; c_int A_p[3] = {0, 2, 4, }; c_float l[3] = {1.0, 0.0, 0.0, }; c_float u[3] = {1.0, 0.7, 0.7, }; c_int n = 2; c_int m = 3; // Exitflag c_int exitflag = 0; // Workspace structures OSQPWorkspace *work; OSQPSettings *settings = (OSQPSettings *)c_malloc(sizeof(OSQPSettings)); OSQPData *data = (OSQPData *)c_malloc(sizeof(OSQPData)); // Populate data if (data) { data-\u0026gt;n = n; data-\u0026gt;m = m; data-\u0026gt;P = csc_matrix(data-\u0026gt;n, data-\u0026gt;n, P_nnz, P_x, P_i, P_p); data-\u0026gt;q = q; data-\u0026gt;A = csc_matrix(data-\u0026gt;m, data-\u0026gt;n, A_nnz, A_x, A_i, A_p); data-\u0026gt;l = l; data-\u0026gt;u = u; } // Define solver settings as default if (settings) { osqp_set_default_settings(settings); settings-\u0026gt;alpha = 1.0; // Change alpha parameter } // Setup workspace exitflag = osqp_setup(\u0026amp;work, data, settings); // Solve Problem osqp_solve(work); // Cleanup if (data) { if (data-\u0026gt;A) c_free(data-\u0026gt;A); if (data-\u0026gt;P) c_free(data-\u0026gt;P); c_free(data); } if (settings) c_free(settings); return exitflag; }; Apollo基于osqp的minimum jerk path optimization void PiecewiseJerkPathProblem::CalculateKernel(std::vector\u0026lt;c_float\u0026gt;* P_data, std::vector\u0026lt;c_int\u0026gt;* P_indices, std::vector\u0026lt;c_int\u0026gt;* P_indptr) { const int n = static_cast\u0026lt;int\u0026gt;(num_of_knots_); const int num_of_variables = 3 * n; const int num_of_nonzeros = num_of_variables + (n - 1); std::vector\u0026lt;std::vector\u0026lt;std::pair\u0026lt;c_int, c_float\u0026gt;\u0026gt;\u0026gt; columns(num_of_variables); int value_index = 0; // x(i)^2 * (w_x + w_x_ref) for (int i = 0; i \u0026lt; n - 1; ++i) { columns[i].emplace_back( i, (weight_x_ + weight_x_ref_) / (scale_factor_[0] * scale_factor_[0])); ++value_index; } // x(n-1)^2 * (w_x + w_x_ref + w_end_x) columns[n - 1].emplace_back( n - 1, (weight_x_ + weight_x_ref_ + weight_end_state_[0]) / (scale_factor_[0] * scale_factor_[0])); ++value_index; // x(i)\u0026#39;^2 * w_dx for (int i = 0; i \u0026lt; n - 1; ++i) { columns[n + i].emplace_back( n + i, weight_dx_ / (scale_factor_[1] * scale_factor_[1])); ++value_index; } // x(n-1)\u0026#39;^2 * (w_dx + w_end_dx) columns[2 * n - 1].emplace_back(2 * n - 1, (weight_dx_ + weight_end_state_[1]) / (scale_factor_[1] * scale_factor_[1])); ++value_index; auto delta_s_square = delta_s_ * delta_s_; // x(i)\u0026#39;\u0026#39;^2 * (w_ddx + 2 * w_dddx / delta_s^2) columns[2 * n].emplace_back(2 * n, (weight_ddx_ + weight_dddx_ / delta_s_square) / (scale_factor_[2] * scale_factor_[2])); ++value_index; for (int i = 1; i \u0026lt; n - 1; ++i) { columns[2 * n + i].emplace_back( 2 * n + i, (weight_ddx_ + 2.0 * weight_dddx_ / delta_s_square) / (scale_factor_[2] * scale_factor_[2])); ++value_index; } columns[3 * n - 1].emplace_back( 3 * n - 1, (weight_ddx_ + weight_dddx_ / delta_s_square + weight_end_state_[2]) / (scale_factor_[2] * scale_factor_[2])); ++value_index; // -2 * w_dddx / delta_s^2 * x(i)\u0026#39;\u0026#39; * x(i + 1)\u0026#39;\u0026#39; for (int i = 0; i \u0026lt; n - 1; ++i) { columns[2 * n + i].emplace_back(2 * n + i + 1, (-2.0 * weight_dddx_ / delta_s_square) / (scale_factor_[2] * scale_factor_[2])); ++value_index; } CHECK_EQ(value_index, num_of_nonzeros); int ind_p = 0; for (int i = 0; i \u0026lt; num_of_variables; ++i) { P_indptr-\u0026gt;push_back(ind_p); for (const auto\u0026amp; row_data_pair : columns[i]) { P_data-\u0026gt;push_back(row_data_pair.second * 2.0); P_indices-\u0026gt;push_back(row_data_pair.first); ++ind_p; } } P_indptr-\u0026gt;push_back(ind_p); } ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/path-planning/osqp/","title":"OSQP"},{"content":"PLUTO: Pushing the Limit of Imitation Learning-based Planning for Autonomous Driving 动机 模型方法缺乏横向行为建模，导致不擅长横向任务 模仿学习的局限性： shortcut学习（只学到了递推自车历史状态完成规划） 分布转移（训练集中所有数据都是居中行驶的，但是闭环测试中可能偏离车道，ego未学习到从偏离状态纠正到居中状态的能力） 因果混淆 开创性思路 基于查询的模型架构，anchor-based横向+anchor-free-based纵向 提出了一种基于差分插值计算辅助损失的新方法 提出了对比模仿学习框架，和新的数据增强方法，避免模型学到捷径或因果混淆 问题建模 \\(N_{A}\\)* 动态agents \\(N_{S}\\)* 静态障碍物 高精地图\\(M\\) 其他交通环境信息（信号灯等）\\(C\\) 历史观测时长\\(T_{H}\\) 预测时长\\(T_{F}\\) 模块细节 特征编码 Agent History Encoding -\\(t\\)时刻障碍物的观测状态为\\(s_{i}^{t}=(p_{i}^{t},\\theta_{i}^{t},v_{i}^{t},b_{i}^{t},\\Pi_{i}^{t})\\)，其中\\(p\\)和\\(\\theta\\)表示agent的位置和角度，\\(v\\)表示速度矢量，\\(b\\)表示长宽尺寸，\\(\\Pi\\)是一个bool量，表示当前帧是否被观测到\n这里通过相邻两帧的状态量插值将历史状态序列转化为vector的形式，\\(s_{i}^{t}=(p_{i}^{t}-p_{i}^{t-1},\\theta_{i}^{t}-\\theta_{i}^{t-1},v_{i}^{t}-v_{i}^{t-1},b_{i}^{t},\\Pi_{i}^{t})\\)，最终得到历史特征向量\\(F_{A} \\in R^{N_{A}*(T_{H}-1)*8}\\) 这里通过一个neighbor attention-based Feature Pyramid Network(FPN)对agent的历史信息进行提取，得到embedding\\(E_{A} \\in R^{N_{A}*D}\\) Static Obstacles Encoding 静止障碍物的观测状态为\\(o_{i}=(p_{i},\\theta_{i},b_{i})\\)，聚合得到\\(F_{O} \\in R^{N_{S}*5}\\) 通过一个两层的MLP进行提取得到embedding\\(E_{O} \\in R^{N_{S}*D}\\) AV\u0026rsquo;s State Encoding 为了防止shortcuts学习，只输入ego当前帧的状态（位置、角度、速度、加速度和方向盘转角），为了避免生成的轨迹只是当前状态的简单外推，引入了attention-based state dropout encoder(SDE)进行编码，得到embedding\\(E_{AV} \\in R^{1*D}\\) Vectorized Map Encoding 地图由\\(N_{P}\\)条polyline组成。 我们对polyline上的每个点进行差分计算。第\\(i\\)个点的特征包括8个维度\\((p_{i}-p_{0},p_{i}-p_{i-1},p_{i}-p_{i}^{left},p_{i}-p_{i}^{right})\\)。其中 \\(p_{0}\\)表示polyline的第一个点\\(p_{i}^{left}\\)和\\(p_{i}^{right}\\)表示车道的左右边界点。 polyline的特征\\(F_{P} \\in R^{N_{p}*n_{p}*8}\\)，\\(n_{p}\\)是每条polyline包含的点的数量。 通过一个类PointNet网络提取embedding\\(E_{p} \\in R^{N_{p}*D}\\)。 Scene Encoding 由于前面的vector化过程，特征中只包含了相邻状态的差分信息，因此需要补充全局位置信息编码\\(PE\\)，这里是用的傅立叶位置编码。这里的全局位置点是取的动态agent当前（最新）帧的 \\(p\\)和 \\(\\theta\\)，静态障碍物的 \\(p\\)和 \\(\\theta\\)，地图元素polyline的第一个点。 初始化了一个可学习的embedding \\(E_{attr}\\)用于编码每个对象的属性类别 将各个来源的特征合并到一个tensor \\(E_{0} = concat(E_{AV},E_{A},E_{O},E_{P})+PE+E_{attr} \\in R^{(N_{A}+N_{S}+N_{P}+1)*D}\\) 随后通过 \\(N\\)层transform decoder进行特征提取 第 \\(i\\)层的decoder步骤如下： $$E_{i-1}^{\u0026rsquo;}=LayerNorm(E_{i-1})$$ $$E_{i}=E_{i-1}+MHA(E_{i-1}^{\u0026rsquo;},E_{i-1}^{\u0026rsquo;},E_{i-1}^{\u0026rsquo;})$$ $$E_{i}=E_{i}+FFN(LayerNorm(E_{i}))$$ 其中MHA是标准的multi-head attention操作 多模态轨迹解码 引入modality query生成多模态轨迹，根据之前的经验，anchor-free的learnable query会导致模态的坍塌和训练的不稳定，因此这里采用了semi-anchor based decoding结构，将横向和纵向解耦建模。 基于参考线的横向query 通过DFS连接lane segment形成参考线，通过和Vectorized Map类似的编码过程得到lateral query \\(Q_{lat} \\in R^{N_{R}*D}\\)，其中 \\(N_{R}\\)是参考线的数量 分解式横纵self attention 纵向query是初始化了一个anchor-free learnable embedding \\(Q_{lon} \\in R^{N_{L}*D}\\)\n合并 \\(Q_{lat}\\)和 \\(Q_{lon}\\)创建横纵联合query \\(Q_{0}=Projection(concat(Q_{lat},Q_{lon})) \\in R^{N_{R}*N_{L}*D}\\)，上面的 \\(Projection\\)是一个简单的MLP\n将 \\(Q_{lat}\\)通过unsqueeze和repeat操作复制 \\(N_{L}\\)份，得到 \\(Q_{lat}^{tmp}\\in R^{N_{R}*N_{L}*D}\\) 将 \\(Q_{lon}\\)通过unsqueeze和repeat操作复制 \\(N_{R}\\)份，得到 \\(Q_{lon}^{tmp}\\in R^{N_{R}*N_{L}*D}\\) concat得到 \\(Q^{tmp}\\in R^{N_{R}*N_{L}*2D}\\) 最后MLP将 \\(2D\\)维度变成 \\(D\\)维 这里如果直接对 \\(Q_{0}\\)进行global self attention，计算复杂度在 \\(O(N_{R}^{2}N_{L}^{2})\\)\n为了简化，先遍历 \\(N_{L}\\)维度，针对每个 \\(N_{R}*D\\)的query块计算self attention\n再遍历\\(N_{R}\\)维度，针对每个\\(N_{L}*D\\)的query块计算self attention\n最终的计算复杂度在 \\(O(N_{R}^{2}N_{L}+N_{R}N_{L}^{2})\\)\nQuery2Scene Cross Attention 将上面得到的Query和Scene Encoding（作为key和value）进行Cross Attention 轨迹解码 遍历 \\(L_{dec}\\)次self-attention和cross-attention操作: $$Q_{i-1}^{\u0026rsquo;}=SelfAttn(Q_{i-1},dim=0)$$ $$Q_{i-1}^{}=SelfAttn(Q_{i-1}^{\u0026rsquo;},dim=1)$$ $$Q_{i}=CrossAttn(Q_{i-1}^{},E_{enc},E_{enc})$$ 最终得到 \\(Q_{dec}\\)，通过两个MLP分别解码得到ego的未来轨迹和对应scores。其中每个轨迹点包含 \\([p_{x},p_{y},cos\\theta,sin\\theta,v_{x},v_{y}]\\) $$T_{0}=MLP(Q_{dec}),\\pi_{0}=MLP(Q_{dec})$$ 此外，为了应对某些没有参考线的场景，用一个MLP直接解码\\(E_{AV}\\)得到一条轨迹 $$\\tau^{free}=MLP(E^{\u0026rsquo;}_{AV})$$ Loss设计 Imitation Loss 首先将GT轨迹的末状态投影到参考线上，找横向距离最近的参考线作为目标参考线。 将目标参考线等分 \\(N_{L}-1\\)份，每一份对应上面 \\(Q_{lon}\\)的每个区域（格子）。包含GT状态投影的query被标记为目标query。 通过将目标参考线和目标纵向query结合，得到目标监督轨迹 \\(\\hat{\\tau}\\)，通过 \\(L1\\)loss进行轨迹监督，通过corss-entropy loss进行score classification监督。 $$L_{reg}=L1_{smooth}(\\hat{\\tau},\\tau^{gt})+L1_{smooth}(\\tau^{free},\\tau^{gt})$$ $$L_{cls}=CrossEntropy(\\pi_{0},\\pi_{0}^{*})$$ 其中， \\(\\pi_{0}^{*}\\)是根据 \\(\\hat{\\tau}\\)的编号计算得到的one-hot编码 最终的Imitation Loss是上面两个loss的加和 $$L_{i}=L_{reg}+L_{cls}$$ Prediction Loss 针对每个动态agent，通过一个简单的双层MLP生成单一模态的预测轨迹 $$P_{1:N_{A}}=MLP(E_{A}^{\u0026rsquo;})$$ 预测loss设计如下： $$L_{p}=L1_{smooth}(P_{1:N_{A}},P_{1:N_{A}}^{gt})$$ Efficient Differentiable Auxiliary Loss 根据过往经验，单纯靠模仿学习不足以杜绝不合理的规划结果，例如与静止障碍物碰撞或偏离可行驶路径等现象。因此，在训练阶段，将这些约束作为辅助loss至关重要。 问题的关键在于如何将这些约束设计成可导的形式，使其可以端到端的进行训练。常用的手段是一种叫做可微分光栅化的技术。 使用可微核函数将每个轨迹点转换为光栅化图像。 使用图像空间内的障碍物掩模计算损失。 但是该方法对计算量和内存的要求都比较大，限制较多。因此本文提出了一种基于differentiable interpolation的新方法。本文以可行驶区域约束为例来阐明该方法。 Cost Map Construction 第一步是将约束转换为一个queryable cost-map。对于可行驶区域约束来说，利用Euclidean Signed Distance Field (ESDF)进行cost表征。此过程包括将不可驾驶区域（例如路外区域）映射到H×W大小的栅格化二值mask上。 与现有方法相比，该方法的一个显著优势是它不需要将轨迹渲染成一系列图像，从而显著降低了计算需求。 Loss Calculation 用 \\(N_{C}\\)个覆盖ego的圆进行建模，ego轨迹点决定了这些圆的中心，这些圆可以用可微分的方式推导出来。 对于与轨迹点关联的每个覆盖圆 \\(i\\)，我们通过投影和双线性插值得到其有符号距离值 \\(d_{i}\\)。为了确保ego遵守可行驶区域约束，当 \\(d_{i}\\)低于圆的半径 \\(R_{c}\\)时，我们对模型施加惩罚： $$L_{aux}=\\frac{1}{T_{f}}\\sum_{t=1}^{T_{f}}{\\sum_{i=1}^{N_{c}}{max(0,R_{c}+\\epsilon-d_{i}^{t})}}$$ 其中 \\(\\epsilon\\)是安全阈值 Contrastive Imitation Learning Framework 为了解决分布转移和因果混淆问题 步骤： 假设当前有一个数据样本 \\(x\\)，通过一个positive data augmentation模块 \\(\\Gamma^{+}\\)和一个negative data augmentation模块 \\(\\Gamma^{-}\\)分别生成一个正样本 \\(x^{+}\\)和负样本 \\(x^{-}\\)。 前面的Scene Encoding部分会将样本 \\(x\\)编码为隐式特征 \\(h(\\cdot)\\)。这里将原始样本、正负样本的隐式编码通过一个两层的MLP分别映射到一个新的空间，表征为 \\(z,z^{+},z^{-}\\)。 利用一个triplet contrastive loss来增强 \\(z\\)和 \\(z^{+}\\)的相似度，降低 \\(z\\)和 \\(z^{-}\\)的相似度。 最后将样本 \\(x\\)和 \\(x^{+}\\)的解码轨迹用于imitation loss和auxiliary loss的计算 在实际操作中，我们随机采样 \\(N_{bs}\\)个样本，每个样本都过一遍positive和negative augmentation，从而得到 \\(3N_{bs}\\)个样本。 每个样本都过一遍encoder和projection head。softmax-based triple contrastive loss公式如下： $$L_{c}=-log\\frac{exp(sim(z,z^{+})/\\sigma)}{exp(sim(z,z^{+})/\\sigma) + exp(sim(z,z^{-})/\\sigma)}$$ $$sim(u,v)=u^{T}v/\\lVert u \\rVert \\lVert v \\rVert$$ \\(\\sigma\\)表示温度参数（temperature parameter） 注意，原始样本和正样本都利用未改动过的GT进行监督训练（ \\(L_{i}, L_{p},L_{aux},L_{c}\\)），但是负样本只用于contrastive loss（ \\(L_{c}\\)）的计算。因为在负样本中，原始的GT可能已经是无效的了。 Data augmentations 数据增强是对比学习发挥作用的关键。虽然基于扰动的增强很普遍，但替代增强策略的探索仍然不足。在此背景下，本文提出了六个精心设计的增强函数，用于定义对比任务。 State Perturbation \\(\\in \\Gamma^{+}\\) 对自动驾驶汽车的当前位置、速度、加速度和转向角引入轻微的随机干扰。这种增强旨在使模型能够学习与训练分布略有偏差的恢复策略。 Non-interactive Agents Dropout \\(\\in \\Gamma^{+}\\) 从输入场景中忽略不与ego交互的agent。判断agent与ego是否有交互是通过其未来bounding box与ego轨迹的交点来识别的。这种增强可以防止模型通过模仿非交互agent来学习行为，从而鼓励模型辨别与有交互关系的agent的真正因果关系。 Leading Agents Dropout \\(\\in \\Gamma^{-}\\) 删除位于ego前面的所有leading agent。这种增强训练了模型的跟驰前车行为，以防止追尾碰撞。 Leading Agent Insertions \\(\\in \\Gamma^{-}\\) 在ego未来会运动的路线上插入一个agent，使得ego如果按照GT走的话会发生碰撞。插入的车辆的轨迹数据来自当前小批量中随机选择的agent，以保持数据的真实性。 Interactive Agent Dropout \\(\\in \\Gamma^{-}\\) 删除与ego有直接或间接交互的agent。该功能旨在训练模型在复杂场景中与不太直观的交互agent之间的交互行为，如无保护的左转和变道场景。 Traffic Light Inversion \\(\\in \\Gamma^{-}\\) 在ego接近由交通灯控制的十字路口而没有前方车辆的情况下，人为反转交通灯状态（例如，从红色变为绿色）。用于教导模型遵守基本的交通灯规则。 Total Loss $$L=w_{1}L_{i}+w_{2}L_{p}+w_{3}L_{aux}+w_{4}L_{c}$$\n后处理 前面的轨迹解码器模块会输出多模态的自车规划轨迹 \\(T_{0} \\in R^{N_{R}N_{L}*T_{F}*6}\\)和对应score \\(\\pi_{0} \\in R^{N_{R}N_{L}}\\) 为了计算复杂度满足要求，先根据scores从前面的轨迹中选出top K 遍历K条轨迹，根据ego状态，利用一个LQR的优化器计算出跟踪轨迹的控制量，再用自行车模型积分控制量得到一条更精细的轨迹 对精细轨迹进行rule-based评估（引入其他agent的预测信息），评估项包括驾驶进度、舒适度、交规、碰撞 最后将规则得分和网络预测得分加权求和，排序得到最优轨迹 实验结果 与其他方法的对比实验 消融实验 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/e2e/pluto/","title":"Pluto"},{"content":"Probabilistic Road Map 介绍 结构：学习+查询 学习 在状态空间内撒点 删掉障碍物内的点 针对每个点，将其与周围一定半径内的其他点连接起来 若两点间的线段与障碍物发生碰撞，删除该连接 查询 利用Dijstra或者A*算法进行搜索 优劣势 概率完备 效率低 改进方式 Lazy collision-checking 撒点和构建两点之间的连接时不检查是否与障碍物碰撞 在路径已经搜索出来之后，查看当前路径有哪些路段发生了碰撞，删除碰撞路段 重新搜索 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/path-planning/prm/","title":"Probabilistic Road Map"},{"content":"Rapidly-exploring Random Tree 介绍 通过在状态空间内随机撒点，控制路径树的生长点和生长方向 算法流程 输入：x_init, x_goal, Map Tree.init() for i = 1 to n x_rand = Sample(Map) x_near = Near(x_rand, Tree) x_new = Steer(x_near, x_rand, StepSize) e[i] = Edge(x_new, x_steer) if CollisionFree(Map, e[i]) Tree.addNode(x_new) Tree.addEdge(e[i]) if x_new == x_goal Success() 优劣势 比RMP更加具有方向性 非最优解 效率低 在整个空间中采样 改进方式 用KDTree存储路径树中的点，加速查找x_near Bidirectional RRT，从起点终点一起生长，直到两者相交 RRT* 算法流程 输入：x_init, x_goal, Map Tree.init() for i = 1 to n x_rand = Sample(Map) x_near = Near(x_rand, Tree) x_new = Steer(x_near, x_rand, StepSize) if CollisionFree(Map, e[i]) xs_near = NearC(Tree, x_new) x_min = ChooseParent(xs_near) e[i] = Edge(x_new, x_min) Tree.addNode(x_new) Tree.addEdge(e[i]) Tree.rewire() if x_new == x_goal Success() 细节展示 ChooseParent，如图所示，x_new是由x_2生长出来的，但不是直接将x_2作为x_new的父，而是从一个固定的半径范围内选择到达x_new的总路程最短的节点作为父节点。 Tree.rewire() Kinodynamic-RRT* 核心 主体结构与RRT*相似，但细节需要改进 采样(Sample) 不同于在欧式空间内采样，该方法要求在全状态空间内采样(位置、速度、加速度、时间) 求Tree上最近节点 不同于用欧式距离衡量远近，这里用状态转移的代价来衡量，简单的说可以用状态转移的\\(jerk + T\\)。如果采样的时候没有对T进行采样，还需要加一步求最优\\(T\\)。这个也就是个OBVP过程。 技巧 在找Tree中最近节点的时候，实际上对树中的每个节点都求了一次OBVP，效率低下。 为了节省时间，我们可以设置一个cost tolerance \\(r\\)。 求解能够在消耗cost小于\\(r\\)的前提下到达x_rand的全状态边界范围(范围内的Tree上的节点构成后向可达集)，和x_rand所能到达的全状态边界范围(范围内的Tree上的节点构成前向可达集) Near()和ChooseParent()操作都在前后两个可达集里面进行，减小了遍历范围。 Rewire()操作在前向可达集中操作 AnyTime-RRT* 核心 Keep optimizing the leaf RRT tree when the robot executes the current trajectory Anytime Fashion 先快速构建一个RRT,获得一个可行解并记录其代价.之后算法会继续采样,但仅将有利于降低可行解代价的结点插入树中,从而逐渐获得较优的可行解 优势 提高实时性 Informed RRT* 核心 先快速构建一个RRT，获得一个可行路径。在可行路径的外包椭圆内继续采样点，构建新的，代价更低的路径 不断循环上一个步骤，通过缩小采样空间，提高了效率 ","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/path-planning/rrt/","title":"Rapidly-exploring Random Tree"},{"content":"Wayformer: Motion Forecasting via Simple \u0026amp; Efficient Attention Networks 总览 研究了3种场景encoder在不同位置的表现差异 研究了两种加速self-attention的方法：factorized attention和latent query attention 核心模块 环境编码 方法介绍 Late Fusion 最常用的方法，对于不同的信息，先各自做self attention。处理完后把这些feature concat起来给decoder，由decoder来做cross attention。\nEarly Fusion 相对早一点做fusion。还是要先经过projection，然后直接concat起来，做一个统一的self attention（实际是跨模去做attention了），每种信息给出了不同的重要程度。\nHierarchical Fusion 上面两种方式的混合。每种信息先过self attention encoder，concat起来再一起做self attention encoder。既然结构变复杂了，自然是要把模型容量控制到和上面两种做法一致才有比较的价值。需要把模型深度平分到两步attention中。\n结论 都在multi-axis attention的情况下做实验。发现低延迟情况下，late fusion性能更好。对于小model（参数少）来说，early fusion性能更好。 注意力机制 环境信息编码size：\\(A*T*S_{surrounding}*D\\)，其中 \\(A\\)是待预测的agent的数量，\\(T\\)是历史信息时长，\\(S_{surrounding}\\)是agent周围交互元素的数量（包括其他agent、地图、信号灯等） self attention的参数量级是和序列长度的平方成正比的，直接将所有信息进行self attention非常耗时。 方法介绍 Multi-Axis Attention\n这是baseline。做法是同时处理时空维度（\\(T*S_{surrounding}\\)），即把时间空间维度reshape到一起，最为耗时，所有的fusion方式的时间复杂度为\\(O(T^{2}S_{surrounding}^{2})\\)。 Factorized Attention\n为了解决baseline中提到的高复杂度问题，采用了分两次计算的方法。时间维度做一次self attention，空间维度做一次self attention，这样就把复杂度里的乘法变成了加法。这样带来的麻烦是要决定一下时间和空间self attention的顺序（这也不是运算复杂度，所以无所谓）。这里比较了两种方法：Sequential attention（N个layer里面前N/2是时间attention，后N/2是空间attention），Interleaved attention（N个layer，其中时间和空间attention交替使用N/2次）。 Latent Query Attention\n和第一种方法有点像，但是在第一个encoder block的\\(Q\\)项使用latent query。这个latent query把ST的维度降下来。\\(KV\\)的话保持了原来的\\(ST\\)维度，对\\(Q\\)而言把\\(ST\\)降到一个更小的维度\\(L_{out}\\)，也就是说总的运算复杂度降低为原来的\\(L_{out}/ST\\) 结论 Factorized Attention：sequential attention和interleaved attention的结果基本一样。在early和late fusion情况下，factorized attention是有帮助的。在late fusion情况下对于latency的降低有很大帮助，这是因为late fusion可以在维度tile之前做attention，attention的token数量能减少很多。\nLatent Queries：采用不同的降维系数，可以把模型的速度提升2到16倍，同时没有性能下降。early和hierarchical fusion的性能更好，表示了cross modal interaction的重要性。\n","date":"2025-04-02T11:08:42+08:00","permalink":"/post/robotics/e2e/way-former/","title":"WayFormer"},{"content":"数组 数组初始化方式 缺省初始化 聚合初始化 int a[3] = {1, 2}; int b[3] = {1, 2, 3}; int c[] = {1, 2, 3}; // c的类型是int[3] 注意 不能使用auto来声明数组类型 #include \u0026lt;typeinfo\u0026gt; #include \u0026lt;iostream\u0026gt; int main() { auto b = {1, 2, 3}; std::cout \u0026lt;\u0026lt; typeid(b).name() \u0026lt;\u0026lt; std::endl; // ./exe | c++filt -t 得到std::initializer_list\u0026lt;int\u0026gt; } 数组不能复制 int b[] = {1, 2}; auto a = b; // a的类型是int*，发生了类型退化 auto\u0026amp; c = b; // auto\u0026amp;可以防止类型退化，c的类型是int(\u0026amp;)[3] int d[3]; d = b; // 报错 元素个数必须是一个常量表达式（编译期可计算的值），且值必须大于0 int a[3]; // a是一个数组，类型是int[3] int x; std::cin \u0026gt;\u0026gt; x; int b[x]; // C语言从C99开始支持variable length array，但C++标准不支持 // 虽然C++标准不支持，但clang++或g++的部分版本的编译器实现中还是支持该方法，但不建议使用，在代码跨平台移植的时候会出问题 int和int[1]是两个不同的类型 字符串数组的特殊性 int main() { char str[] = \u0026#34;Hello\u0026#34;; // 类型是char[6]，隐式地在字符串最后加一个\u0026#39;\\0\u0026#39; char str[] = {\u0026#39;H\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;o\u0026#39;}; // 类型是char[5] } 数组的复杂声明 指针数组与数组指针 int* a[3]; // a是个数组，存了3个int*指针，类型是int*[3] int (*a)[3]; // a是个指针，指向一个int[3]类型的数组，类型是int(*)[3] 声明数组的引用 int a[3]; int (\u0026amp;b)[3] = a; // b是a的引用 // auto\u0026amp; b = a; // 同上 int x1; int\u0026amp; c[1] = {x1}; // C++不支持引用的数组 数组到指针的隐式转换 会丢失一些信息，比如数组的长度 可以通过声明引用来避免隐式类型转换 注意：不要用extern指针来声明数组 unknown bounded array声明 // source.cc int array[5] = {1, 2}; int array1[5] = {1, 2}; // main.cc extern int array[]; // unknown bounded array extern int* array1; int main() { array[0]; // 编译通过，运行正常 array1[0]; // 编译通过，运行不正常，强行用指针解释数组的内容，会出屎 } 获得指向数组开头和结尾的指针 // 这里的array不可以是指针 // extern int array[]; // 不可以是unknown bounded array extern int array[4]; // 这个声明可以 std::begin(array); // int* std::cbegin(array); // const int* std::end(array); // int* std::cend(array); // const int* 指针算数 增加、减少、比较 求距离 解引用 指针索引 int a[3] = {1, 2, 3}; auto ptr = a; *ptr; *a; ptr[1]; 其他操作 求元素的个数 extern int array[]; // 下面的三个方法都无法处理这种imcomplete type int a[3]; sizeof(a); // 12，int占四个字节，这里的a不会退化为指针，C语言的方法，编译期计算 std::size(a); // 3， 推荐使用该方法，C++方法，编译期计算 std::end(a) - std::begin(a); // 3，运行期计算 元素遍历 int a[3]; // 方法1 size_t i = 0; while (i \u0026lt; std::size(a)) { a[i]; ++i; } // 方法2 auto ptr = std::cbegin(a); while (ptr != std::cend(a)) { ++ptr; } // 方法3 for (int x : a) {} C字符串 本质上是数组char[] C语言提供了额外的操作 #include \u0026lt;cstring\u0026gt; char str[] = \u0026#34;Hello\u0026#34;; strlen(str); // strcmp 多维数组 类型推导 #include \u0026lt;type_traits\u0026gt; int x[3][4]; // x是一个数组，包含了3个 int[4]类型的元素 std::is_same_v(decltype(x[0], int(\u0026amp;)[4])); // true，x[0]是个表达式，decltype(表达式)会加上引用 auto ptr = x; // 类型退化只发生在第一维，ptr类型是int(*)[4]，只有这样在执行ptr+1的时候才知道要往后移动多少位 using A = int[4]; // 使用类型别名简化初始化 A x2[3]; // int x2[3][4] A* ptr = x2; 初始化 int x[2][3] = {1, 2, 3, 4}; // {123}{400} int x[2][3] = \\{\\{1, 2, 3\\}, \\{4, 5, 6\\}\\}; // {123}{456} int x[2][3] = \\{\\{1, 2, 3\\}, \\{4, 5\\}\\}; // // {123}{450} int x[2][3] = \\{\\{1, 2\\}, \\{4, 5\\}\\}; // // {120}{450} int x[][3] = {1, 2, 3, 4, 5}; // x的类型是int[2][3] int x[][] = \\{\\{1, 2, 3\\}, \\{4, 5, 6\\}\\}; // 报错，只有第一个[]里面的size可以自动推导，后面的都得显示标明 遍历 二维数组遍历的时候，尽量按行遍历，因为一行的元素往往存在同一块内存，cache命中的概率比较高 int x[2][3] = {1, 2, 3, 4}; for (auto\u0026amp; p : x) { // 这里的\u0026amp;不能少，如果没有\u0026amp;，p会从int(\u0026amp;)[3]退化为int*，无法进入下面的循环 for (auto\u0026amp; q : p) { // ... } } Vector 是内建数组的代替品 与数组比，更注重易用性 可复制 可在运行期改变元素个数 初始化 std::vector\u0026lt;int\u0026gt; x(3); std::vector\u0026lt;int\u0026gt; x(3, 1); // 1, 1, 1 std::vector\u0026lt;int\u0026gt; x{3, 1}; // 3, 1 std::vector\u0026lt;int\u0026gt; x = {1, 2, 3}; 索引与遍历 std::vector\u0026lt;int\u0026gt; x = {1, 2, 3}; std::cout \u0026lt;\u0026lt; x[20] \u0026lt;\u0026lt; std::endl; // 编译通过，运行可能不会报错 std::cout \u0026lt;\u0026lt; x.at(20) \u0026lt;\u0026lt; std::endl; // 编译通过，运行报错，所以这种写法更好 迭代器 模拟指针的行为 包含多种类别，每种类别支持的操作不同 vector对应随机访问迭代器 解引用与下标访问 移动 相减求距离 两个迭代器比较 其他 添加元素会导致迭代器失效 String 是内建字符串的代替品 string其实是std::basic_string\u0026lt;char\u0026gt;的类型别名，basic_string是一个类模板 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/array-vector-string/","title":"Array Vector String"},{"content":"前言 在阅读Apollo Planning代码的dp_st_graph部分的时候，遇到future这个多线程接口，特地来记录一下。 用法一：等待task执行完毕 #include \u0026lt;iostream\u0026gt; #include \u0026lt;future\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; void task(string c){ for (int i = 0; i \u0026lt; 10; i++){ cout\u0026lt;\u0026lt;c; } } int main(){ vector\u0026lt;future\u0026lt;void\u0026gt;\u0026gt; results; results.emplace_back(std::async(std::launch::async, task, \u0026#34;A\u0026#34;)); results.emplace_back(std::async(std::launch::async, task, \u0026#34;B\u0026#34;)); results.emplace_back(std::async(std::launch::async, task, \u0026#34;C\u0026#34;)); for (int i = 0; i \u0026lt; 10; i++){ cout\u0026lt;\u0026lt;\u0026#34;D\u0026#34;; } for (auto\u0026amp; result : results){ result.get();//强制阻塞main线程，直到对应的task线程执行完毕 } } 用法二：同步获取异步结果 #include \u0026lt;iostream\u0026gt; #include \u0026lt;future\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; int task(string c){ if (c == \u0026#34;A\u0026#34;){ std::this_thread::sleep_for(chrono::seconds(1)); cout\u0026lt;\u0026lt;c\u0026lt;\u0026lt;endl; std::this_thread::sleep_for(chrono::seconds(1)); cout\u0026lt;\u0026lt;c\u0026lt;\u0026lt;endl; std::this_thread::sleep_for(chrono::seconds(3)); cout\u0026lt;\u0026lt;c\u0026lt;\u0026lt;endl; return 1; } else if (c == \u0026#34;B\u0026#34;){ std::this_thread::sleep_for(chrono::seconds(1)); cout\u0026lt;\u0026lt;c\u0026lt;\u0026lt;endl; std::this_thread::sleep_for(chrono::seconds(1)); cout\u0026lt;\u0026lt;c\u0026lt;\u0026lt;endl; std::this_thread::sleep_for(chrono::seconds(2)); cout\u0026lt;\u0026lt;c\u0026lt;\u0026lt;endl; return 2; } else{ std::this_thread::sleep_for(chrono::seconds(1)); cout\u0026lt;\u0026lt;c\u0026lt;\u0026lt;endl; std::this_thread::sleep_for(chrono::seconds(1)); cout\u0026lt;\u0026lt;c\u0026lt;\u0026lt;endl; std::this_thread::sleep_for(chrono::seconds(1)); cout\u0026lt;\u0026lt;c\u0026lt;\u0026lt;endl; return 3; } } int main(){ vector\u0026lt;future\u0026lt;int\u0026gt;\u0026gt; results; results.emplace_back(std::async(std::launch::async, task, \u0026#34;A\u0026#34;)); results.emplace_back(std::async(std::launch::async, task, \u0026#34;B\u0026#34;)); results.emplace_back(std::async(std::launch::async, task, \u0026#34;C\u0026#34;)); cout\u0026lt;\u0026lt;\u0026#34;D\u0026#34;\u0026lt;\u0026lt;endl; for (auto\u0026amp; result : results){ int tmp = result.get();//异步结束的task，同步获取返回值 cout\u0026lt;\u0026lt;\u0026#34;get \u0026#34;\u0026lt;\u0026lt;tmp\u0026lt;\u0026lt;endl; } } 三个task结束的时间不一样，但是程序获取task返回值是一起获取的， ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/multi-thread/async-future/","title":"Async \u0026 Future"},{"content":"问题形式 原问题（一般只能考虑不等式约束）：\\(min_{x}f(x)\\)，\\(s.t.c_{i}(x)\\leq 0,i\\in I\\) 障碍函数形式 logarithmic barrier $$B_{ln}(x,\\sigma)=f(x)-\\sigma\\sum_{i\\in I}(-c_{i}(x))$$ inverse barrier $$B_{inv}(x,\\sigma)=f(x)+\\sigma\\sum_{i\\ I}inv(-c_{i}(x))$$ $$inv(x):=1/x,if(x\u0026gt;0)$$ exponential barrier $$B_{expi}(x,\\sigma)=f(x)+\\sigma\\sum_{i\\ I}expi(-c_{i}(x))$$ $$expi(x):=e^{1/x},if(x\u0026lt;0)$$ 几何解释 Barrier Methods与Penalty Methods的对比 Penalty Methods当\\(\\sigma\\to\\infty\\)时，得到的解接近于原问题的解 Barrier Methods当\\(\\sigma\\to0\\)时，得到的解接近于原问题的解 Penalty Methods是从约束外部逐渐收敛到约束边缘；Barrier Methods是用约束内部逐渐收敛到约束边缘；如果变量有实际的物理意义（在约束外依然有明确的定义）两种方法都可以用，否则只能用Barrier Methods 如果使用Sequential Barrier Methods，且每一轮迭代调用的是Newton Method，就是所谓的内点法（Primal Interior Point Method），因为该方法每一轮迭代求得的解严格在约束范围内 Barrier Methods与Penalty Methods的缺陷 当两者的解越来越贴近原问题解的同时，对应的无约束的函数的Hessian矩阵也越来越病态（条件数趋于无穷大，Hessian的最大奇异值没有上界），这将导致收敛速度非常慢（梯度下降法会发生震荡） sequential方法通过逐渐放大（缩小）\\(\\sigma\\)并用上一轮的解作为下一轮的初值可以一定程度缓解这个震荡的现象，但治标不治本 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/barrier_methods/","title":"Barrier Methods"},{"content":"参考书籍 最优化：建模、算法与理论 中文版 Numerical Optimization 理论很全 关注实数在程序中的表示方法导致的数值稳定性问题，给了很多工程上的实践，帮助读者写出更加鲁棒稳定的算法 Lectures on Convex Optimization 理论清晰，涵盖很全 Lecture on Modern Convex Optimization 对Conic Programming有比较好的分析和应用 优化问题 一般形式\n$$min(f(x))$$ $$s.t.g(x)\\leq 0, h(x)=0$$ 问题有解的条件\n\\(f(x)\\) is lower bounded(有下界) 在\\(x\\)的取值范围内，\\(f(x)\\geq \\alpha\\) \\(f(x)\\) is bounded level set 满足\\(f(x)\u0026lt; \\beta \\)的\\(x\\)的取值有上下界，\\(f(x)=\\frac{1}{x},(x\u0026gt;0)\\)就不满足，\\(x\\)到无穷大时\\(f(x)\\)最小 机器人领域中常见的优化问题\nSLAM: Nolinear Least Squares Trajectory: Nolinear Program Registration: Semi-Definite Program Time Optimal Path Parameterization: Second-Order Conic Program 凸集 如果集合内的任意两点的连线仍然在集合内，则集合是一个凸集 convex hull: 点集中所有点的convex combinations的并集 常见的凸集合 hyperplane: \\(Ax=b\\) half-space: \\(Ax\\geq b\\) sphere: \\(\\left | x-x_{0} \\right |=b\\) ball: \\(\\left | x-x_{0} \\right |\\leq b\\) polynomials: 凸包 cone: 锥(不一定是凸的) 半定锥(一定是凸的) 凸集的交集一定是凸的 凸集的并集不一定凸 凸集的叉乘一定是凸 函数的高阶导数 一阶导数: gradient 二阶导数: hessian hessian是gradient的jacobian 矩阵求导 网址 凸函数及其性质 $$f(ax+(1-a)y)\\leq af(x)+(1-a)f(y)$$ 如果严格\u0026lt;，称为strictly convex function 凸函数一定有convex sub level set quasi-convex(拟凸函数) $$f(x)=log(\\left | x \\right |+1)$$ 拟凸函数的线性加和不一定还是拟凸函数，但凸函数的线性加和肯定是凸函数 凸函数的局部最小值一定是全局最小值 凸函数的局部最小值的集合一定是凸集 如果一个光滑函数的Hessian矩阵(二阶导)是半正定的(\\(y^{T}Hy\\geq 0\\))，它一定是凸函数 对非凸函数而言，其局部极小值点处的二阶导一定是半正定的（正定和不定都是针对对称矩阵而言的） 如果函数在某点的一阶导数是0，但Hessian不定（特征值有正有负），该点为鞍点。 反过来不成立，比如\\(z=x^{4}-y^{4}\\)在(0, 0)处Hessian不是不定的，但它是个鞍点 可微凸函数一定在其任一个点的线性近似的上方，这意味着梯度为0的点就是全局极小值 如果函数的Hessian严格正定，最小特征值大于0，则为强凸函数，收敛速度快 可以看到，强凸函数比凸函数在定义上更加严格，比线性近似的上方还多了一个min curvature项(m\u0026gt;0) 强凸 \u0026gt; 严格凸 \u0026gt; 凸 可以通过将非强凸函数构造为强凸函数加速优化速率 lipschitz常数：任意两个点的梯度差值不会比两个点的距离的常数倍来的大 lipschitz常数和强凸都可以用来刻画可微凸函数的凸性 从上图可以看到，强凸性描述了凸函数的下界；lipschitz常数描述了凸函数的上界(如果一个函数的lipschitz常数存在，就可以找到一个二次函数来bound住函数的上界) 条件数 计算方式 对光滑函数而言，Hessian矩阵的SVD分解得到的最大奇异值除以最小奇异值就是该函数的条件数 对可导但不一定存在Hessian的函数而言，条件数\\(\\kappa =\\frac{M}{m}\\)，这里的M就是lipschitz常数，m就是强凸性的常数 对任意函数而言，可以通过绘制函数的等高线，将等高线拟合成一个椭圆，椭圆的长轴除以短轴就是该函数的条件数 用处 可以用来判断是否需要用到函数的高阶信息 有些算法对条件数很小的凸函数可以收敛的很快，对条件数较大的凸函数收敛比较慢，此时就需要用函数的曲率(高阶)信息更快收敛到最优解 次梯度 上图中，函数在\\(x_{1}\\)处不可微，该点处的导数有很多个，以左导数和右导数形成区间的导数集合就是次梯度 对于可微凸函数而言，判断最优点的条件是导数为0；对于不可微函数，判断不可微点是否是最优解就看该点的次梯度有没有把0包含在内 光滑函数的导致的负方向一定是函数值下降的方向，但沿着次梯度集合中某个方向的反方向走不一定让函数值下降；必须是次梯度集合中模长最小的那个方向的反方向才可以保证函数值的下降 梯度单调性 牛顿法求解无约束优化的时候，利用梯度单调性可以让算法更稳定 凸函数的很多operation是可以保留其凸性的 加权和(权重\u0026gt;0) 范数 仿射变换 point-wise max 绝对值 最大特征值 无穷范数 凸函数举例 $$f(x)=trace(A^{T}x)$$ 本质上是线性操作 $$f(x)=max\\left | x-y \\right |$$ 本质上是point-wise max，找凸集合距离x最远的点 $$f(x)=min\\left | x-y \\right |$$ 虽然是point-wise min，但也是凸函数，找凸集合距离x最近的点 $$f(x)=\\left | b+A_{i}x_{i} \\right |$$ 本质上是仿射变换 $$f(x)=min_{y}g(x,y)$$ \\(g(x,y)\\)是凸的 评价数值优化算法的指标 收敛速度（迭代次数） 对不同函数的收敛稳定性 每次迭代的计算量 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/basic/","title":"Basic of Optimization"},{"content":"优秀教程\n背景 很多算法允许通过可调用对象自定义计算逻辑的细节 transform / copy_if / sort 可调用对象 函数指针：概念直观，但是定义位置受限 #include \u0026lt;functional\u0026gt; bool BiggerThan(const int val) { return val \u0026gt; 3; } int main() { std::vector\u0026lt;int\u0026gt; x{0, 1, 2, 3, 4}; std::vector\u0026lt;int\u0026gt; y; // 不能把BiggerThan的定义写在这里，因为C++不支持在函数中定义函数 std::copy_if(x.begin(), x.end(), std::back_inserter(y), BiggerThan); // y = {4}; } 类：功能强大，但是书写麻烦 bind：基于已有的逻辑来灵活适配，但描述复杂逻辑时语法可能会比较复杂难懂 lambda表达式：小巧灵活，功能强大 Bind bind：通过绑定的方式修改可调用对象的调用方式(C++11) #include \u0026lt;functional\u0026gt; bool BiggerThan(const int val1, const int val2) { return val1 \u0026gt; val2; } int main() { // demo1 using namespace std::placeholders; std::vector\u0026lt;int\u0026gt; x{0, 1, 2, 3, 4}; std::vector\u0026lt;int\u0026gt; y; // 不能把BiggerThan的定义写在这里，因为C++不支持在函数中定义函数 std::copy_if(x.begin(), x.end(), std::back_inserter(y), std::bind(BiggerThan, _1, 3)); // y = {4}; // demo2 auto f = std::bind(BiggerThan, _1, 3); std::cout \u0026lt;\u0026lt; f(50) \u0026lt;\u0026lt; std::endl; // 1 // demo3 auto f = std::bind(BiggerThan, _2, _1); std::cout \u0026lt;\u0026lt; f(3, 2) \u0026lt;\u0026lt; std::endl; // 0 // demo4 int i = 0; auto b = std::bind(Proc, i); b(); std::cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; std::endl; // 0 b = std::bind(Proc, std::ref(i)); b(); std::cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; std::endl; // 1 } bind_front：std::bind的简化形式(C++20) Lambda 背景\n《C++ lambda story》建议阅读 为了更灵活地实现可调用对象而引入 C++11 ～ C++20持续更新 C++11引入lambda表达式 C++14支持初始化捕获、泛型lambda C++17引入constexpr lambda、*this捕获 C++20引入concepts、模版lambda lambda表达式会被C++翻译成类进行处理 基本组成\n参数与函数体 返回类型 auto x = [](int val) { if (val \u0026gt; 3) { return 1.0; } else { return 1.3f; } }; // 上面的代码编译会报错，因为1.0和1.5f不是同一个类型的值，无法自动推导，需要改成下面的显示指定返回类型 auto x = [](int val) -\u0026gt; double { if (val \u0026gt; 3) { return 1.0; } else { return 1.3f; } }; std::cout \u0026lt;\u0026lt; x(5) \u0026lt;\u0026lt; std::endl; 捕获： 针对函数体中使用的局部自动对象进行捕获; 局部静态对象或者全局对象是不需要捕获的，可以直接在lambda内调用 值捕获、引用捕获与混合捕获(C++11) [=]：将所有局部自动对象值捕获；[\u0026amp;]：将所有局部自动对象引用捕获 [\u0026amp;, z]：除了z之外的局部自动对象引用捕获，z值捕获 [\u0026amp;x, z]：x引用捕获，z值捕获 this捕获(C++11) struct Str { auto fun() { int val = 3; // auto lam = [val, x] () { // 编译失败，因为x在这里不是局部自动变量 // auto lam = [val] () { // 编译失败，因为不是全局变量或者局部静态变量 auto lam = [val, this] () { // 编译成功, this指向了Str的某个对象实例 return val \u0026gt; x; } return lam(); } int x; }; 初始化捕获(C++14) int x = 3; int y = 4; // method1 auto lam = [z = x + y](int val) { return val \u0026gt; z; }; // method2 auto lam = [x, y] (int val) { return val \u0026gt; x + y; } // method1的好处在于x+y这步计算不用每次调用lam的时候都重新算一遍 *this捕获(C++17) struct Str { auto fun() { int val = 3; auto lam = [val, this] () { // auto lam = [val, *this] () { return val \u0026gt; x; }; return lam; } int x; }; auto wrapper() { Str s; return s.fun(); } int main() { auto lam = wrapper(); // wrapper执行完之后，临时变量“s”被销毁 lam(); // 此时的lam包含了一个悬挂指针，指向了一个被销毁的对象，运行的结果就是未定义的 // 解决方式就是将this捕获改成上面的*this捕获，直接将对象的内容复制过来，好处是更加安全，坏处是复制对资源的消耗较大 } 说明符 mutable int main() { int y = 3; auto lam = [y] () { ++y; // 编译报错，因为编译器会将lambda表达式构造为一个类，而lam函数会被加上const修饰符，因此无法对类内部的成员变量进行修改 return val \u0026gt; y; } // 解决方案是加上mutable说明符 auto lam = [y] () mutable { // ... } } constexpr(C++17) auto lam = [](int val) constexpr { return val + 1; } constexpr int val = lam(100); // constexpr表示在编译期就可以确定值是多少 consteval(C++20) constexpr所修饰的函数，既可以在编译期调用，也可以在运行期调用 consteval修饰的函数只能在编译期调用 模板形参(C++20) auto lam = []\u0026lt;typename T\u0026gt;(T val) { return val + 1; } constexpr int val = lam(100); constexpr int val = lam(100.0); lambda深入\n捕获时计算(C++14)：就是上面举的初始化捕获的例子 即调用函数表达式(Immediately-Invoked Function Expression, IFE) int x = 3, y = 5; const auto val = [z = x + y]() { return z; }(); // 构造完立刻执行该lambda表达式 使用auto避免复制(C++14) std::map\u0026lt;int, int\u0026gt; m{{2, 3}}; auto lam = [](const std::pair\u0026lt;int, int\u0026gt;\u0026amp; p) { return p.first + p.second; }; std::cout \u0026lt;\u0026lt; lam(*m.begin()) \u0026lt;\u0026lt; std::endl; // 上面的case，*m.begin()的返回值类型是std::pair\u0026lt;const int, int\u0026gt; // 因此，系统在这里无法执行引用，而是进行了复制操作(隐式类型转换) // 解决方案1：auto lam = [](const std::pair\u0026lt;const int, int\u0026gt;\u0026amp; p) {/*...*/}; // 解决方案2：auto lam = [](const auto\u0026amp; p) {/*...*/}; Lifting(C++14) auto fun(int val) { return val - 1; } auto fun(doule val) { return val - 1; } int main() { auto b = std::bind(fun, 3); // 编译失败，因为bind无法区分调用哪个fun函数 b(); auto lam = [](auto x) { return fun(x); }; lam(3); // 4 lam(3.5); // 4.5 } 递归调用(C++14) // demo 1 int factorial(int n) { return n \u0026gt; 1 ? n * factorial(n - 1) : 1; } int main () { factorial(5); // 上面的代码是合法的，因为编译器看到“int factorial(int n)”的时候就已经知道了这个函数的输入输出信息，这就够了 } // demo 2 int main() { auto factorial = [](int n) { return n \u0026gt; 1 ? n * factorial(n - 1) : 1; }; // 上面的代码无法编译通过，因为编译器要理解factorial函数（输出输出信息）就需要先解析auto的值 // auto的值需要 = 后面内容来确定，但是 = 后面的内容中的factorial这个函数的行为和返回值目前又是未知的 // 变成了鸡生蛋，蛋生鸡的问题 } // demo 3 int main() { auto factorial = [](int n) { auto f_impl = [](int n, const auto\u0026amp; impl) -\u0026gt; int { //这里的-\u0026gt;int一定不能少，没有这个的话，编译器就会尝试解析下面一行的返回类型用于确定f_impl的类型。但下面一行的impl的返回类型又是未知的，所以就编译器就无法编译了 return n \u0026gt; 1 ? n * impl(n - 1, impl) : 1; }; return f_impl(n, f_impl); }; factorial(5); // 上面的代码是合法的，解析如下 // 首先编译器同样不知道factorial的类型，所以需要解析第一个 = 后面的内容 // 而第一个 = 后面的内容没有出现factorial，这就意味着鸡蛋问题不存在，后面的内容是可以正常解析的 // 接下来分析factorial内部如何解析 // 首先编译器需要解析f_impl的类型，而f_impl = 后面的内容中没有出现f_impl，也没有鸡蛋问题，所以后面的内容也是可以正常定义的 // 编译器在定义f_impl的时候并不需要知道后面传入的impl是个啥类型，反正auto字段会被解析为template，只有后面调用的时候才会实例化，才会确定具体的函数类型 // 由于前面的f_impl已经定义好了，所以f_impl(n, f_impl)也没啥问题，顺理成章 // 整个这段代码是所以能成功，关键就是我们使用了C++14中的lambda支持auto这个特性，否则就不能将impl定义成一个模板函数 // 如果不用auto，还是有其他方法可以做的，参考C++ lambda story这本书 } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/bind-lambda/","title":"Bind \u0026 Lambda"},{"content":"位域 显示表明对象尺寸（所占位数） struct Str { bool b1; bool b2; }; struct Str2 { bool b1 ： 1; bool b2 ： 1; }; int main() { sizeof(Str); // 2, 2个字节，空间浪费 sizeof(Str2); // 1, 1个字节（中的两位），剩下的6个位闲置 } 多个位域对象可能会被打包存取 上面的例子中，程序在处理Str2的时候会多执行1个步骤（取一个字节、位操作得到目标位的值），处理Str的时候只需要取对应的一个字节就行 因此，为了效率用Str，为了省内存用Str2 声明了位域的对象不能取地址，因此不能使用指针或非常量引用进行绑定 不能取地址的原因是位域对象不一定在一个字节的开始处 常量引用可以正常工作 struct Str { bool b1 ： 1; }; Str s; const auto\u0026amp; ref = s.b1; // 这里会把s.b1复制到一个字节大小的临时变量里，然后ref绑定到改临时变量 这就是为啥遍历vector\u0026lt;bool\u0026gt;的对象只能用\u0026quot;auto\u0026amp;\u0026amp;\u0026ldquo;或\u0026quot;const auto\u0026amp;\u0026rdquo; 位域指定的大小必须小于其类型的大小 char a : 2; // a的取值是0-3 char a : 10； // a的取值还是0-255 a = 1024; std::cout \u0026lt;\u0026lt; a; // 0 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/bit-field/","title":"Bit Field"},{"content":"sort #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;algorithm\u0026gt; bool cmp(int a, int b) { return a \u0026gt;= b; // 不满足strict weak ordering，导致快排中的while循环不停++，越界了 } int main() { std::vector\u0026lt;int\u0026gt; vec; for (int i = 0; i \u0026lt; 17; ++i) { // 17就会稳定core，16就不core // 对于std::sort()，当容器里面元素的个数大于_S_threshold 的枚举常量值时 // 会使用快速排序（stl的这个默认值是16），快速排序中的while循环没有检查越界 vec.push_back(1); } std::sort(vec.begin(), vec.end(), cmp); return 0; } Eigen \u0026amp; auto 参考链接 // 用auto接受Eigen的对象时会出错，不要使用 Eigen::VectorXd dir = (-1.0 * func.grad(x)); // auto dir = (-1.0 * func.grad(x)); ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/bugs/","title":"Bugs"},{"content":"访问限定符与友元 使用public/private/protected限定类成员的访问权限 类与结构体缺省访问权限的区别：类默认是private，结构体默认是public 使用友元打破访问权限限制——关键字friend 声明某个类或者某个函数是当前类的友元——慎用！\nfriend声明放在哪都行(public/private/protected都可以)\n在类内首次声明友元类或者友元函数（下面代码中最开头的“声明函数或类”部分可以删掉，可以直接把Str类中的“friend int main();”视作main函数的声明）\n注意使用限定名称引入友元并非友元类（友元函数）的声明 比如把Str中的代码改为“friend int ::main();”，编译就不通过了 因为::main()不能被解析为函数的声明，而::main()函数在这之前又没有声明，所以就无法通过编译了 解决方案就是在最前面加上“void main();”，或者把“friend int ::main();”改回“friend int main();” // 声明函数或类 int main(); class Str2; class Str { // 声明main函数和Str2类是当前类的友元 friend int main(); friend Str2; // friend class Str2也可以 // 默认是private inline static int x; }; class Str2 { void fun() { std::cout \u0026lt;\u0026lt; Str::x \u0026lt;\u0026lt; std::endl; // 合法访问 } }; int main() { std::cout \u0026lt;\u0026lt; Str::x \u0026lt;\u0026lt; std::endl; // 合法访问，声明了友元 } 友元函数的类内类外定义（注意：友元类不可以类内定义）\n类外定义 // 类外定义 class Str { inline static int x; int y; friend void fun(); }; void fun() { Str val; std::cout \u0026lt;\u0026lt; val.y \u0026lt;\u0026lt; std::endl; } 类内定义（隐藏友元hidden friend） 错误示例 // 类内定义 class Str { inline static int x; int y; // fun函数不是Str的成员函数，它是Str友元函数，所以fun的作用域是Str外部的全局域 // 但是fun函数在外部又无法调用，因为fun函数的定义在Str内部，有定义而无声明 friend void fun() { Str val; std::cout \u0026lt;\u0026lt; val.y \u0026lt;\u0026lt; std::endl; } }; int main() { fun(); // 编译失败，常规的名称查找是查找不到fun的 } 隐藏友元的正确打开方式 // 常规的名称查找找不到fun，Argument-Depende-Lookup(ADL)可以） // 类内定义 class Str { inline static int x; int y; friend void fun2(const Str\u0026amp; val) { std::cout \u0026lt;\u0026lt; val.y \u0026lt;\u0026lt; std::endl; } }; int main() { Str val; fun2(val); // 编译成功 // 我们在调用fun2的时候传入了val，其类型是Str // 所以编译器在执行这条语句的时候除了常规的名称查找，还会进行实参类型的依赖查找 // 就会扫描Str内部的内容，就会找到fun2 } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/class/access-friend/","title":"Class Access \u0026 Friend"},{"content":"结构体 仅有声明的结构体是不完整类型 struct Str; int main() { Str s; // 编译不通过，只有声明没有定义，不知道Str的内部结构，占多大内存 Str* s; // 编译通过，指针的内存大小是固定的，不需要知道Str的内部结构和大小 } 结构体（以及类）的一处定义原则：翻译单元级别 下面这份代码是可以通过编译并运行的，两个相同的结构体在不同的编译单元 source.cpp的内容 struct Str{ int x; }; void fun() { Str m_str; } main.cpp的内容 struct Str { int x; }; void fun(); int main() { fun(); Str str; } 数据成员的声明与初始化 (C++11)数据成员可以使用decltype来声明其类型，但不能使用auto 数据成员的声明可以引入const、引用、指针等限定 数据成员会在构造对象时定义 (C++11)支持类内成员初始化 结构体支持聚合初始化，但不建议使用 struct Str { int x; int y; }; int main() { Str s{3, 4}; // 上面的代码看起来没啥问题，但是万一哪天，结构体中的成员的顺序被移动了，或者x和y中间插入了一个新的成员，那么这里的聚合初始化都可能引发不想要的错误 } 为了解决上面的问题，C++20引入了指派初始化 Str s{.x=3, .y=4}; mutable限定符 struct Str { mutable int x; int y; }; int main() { const Str s; s.x = 3; // 编译通过 s.y = 4; // 编译不通过 } 静态数据成员 多个对象之前共享的数据成员\n定义方式的演化\nC++98：1. 类外定义 2. const静态成员的类内初始化 struct Str { static int x; int y; const static array_size = 100; // 如果没有使用const static，就无法定义Str，因为编译器不知道要分配多大的内存给对象 int buffer[array_size]; // C++98会把这里的array_size直接替换成100 }; int Str::x; // 这里的定义不能少 int main() { Str s1; Str s2; s1.x = 100; s2.x = 1; std::cout \u0026lt;\u0026lt; s1.x \u0026lt;\u0026lt; std::endl; // 1 std::cout \u0026lt;\u0026lt; \u0026amp;(s1.array_size) \u0026lt;\u0026lt; std::endl; // 编译不通过，因为C98标准的编译器直接把用到array_size的地方替换为100了，并没有真的分配一块内存来存放array_size // 如果一定要给它分配一个地址的话，需要在某个地方加一行定义：int Str::array_size; } C++17：内联静态成员的初始化 struct Str { inline static int array_size = 100; // 不需要const了，可修改 int buffer[array_size]; }; std::cout \u0026lt;\u0026lt; \u0026amp;(s1.array_size) \u0026lt;\u0026lt; std::endl; // 编译通过 静态数据成员可以使用auto推导类型\nstruct Str { inline static auto array_size = 100; }; 静态数据成员的访问\nstr.x str-\u0026gt;x Str::x 在类的内部声明相同类型的静态数据成员\nstruct Str { Str s; // 编译不通过，因为不知道要给Str分配多大内存，又是鸡蛋问题 static Str s; // 编译通过，因为它被所有的Str对象共享，所以它不属于任何一个对象，所以在构建Str对象的时候，不用考虑它所占的内存 }; Str Str::s; // 如果没有这一行，编译能通过，但是link不行，因为s没有被定义，就没有对应的内存 int main() { Str s1; Str s2; std::cout \u0026lt;\u0026lt; \u0026amp;(s1.s) \u0026lt;\u0026lt; std::endl; } inline使用的注意点\n错误使用方法 struct Str { inline static int x; // 编译通过 inline static Str s; // 编译不通过，因为使用inline后，这里就不是声明而是定义了，但程序执行到这一行的时候Str还是不完全类型，所以无法完成定义的操作 }; int main() { Str s1; Str s2; std::cout \u0026lt;\u0026lt; \u0026amp;(s1.s) \u0026lt;\u0026lt; std::endl; } 正确使用方法 struct Str { static Str s; }; inline static Str::s; int main() { Str s1; Str s2; std::cout \u0026lt;\u0026lt; \u0026amp;(s1.s) \u0026lt;\u0026lt; std::endl; } 成员函数 声明与定义 类内定义(隐式内联) // str.h struct Str { int x; void fun() {} }; // main.cc #include \u0026#34;str.h\u0026#34; //main2.cc #include \u0026#34;str.h\u0026#34; 类内声明+类外定义 // str.h struct Str { int x; void fun(); }; // str.cc void Str::fun() {} // main.cc #include \u0026#34;str.h\u0026#34; // main2.cc #include \u0026#34;str.h\u0026#34; 错误示范 // str.h struct Str { int x; void fun(); }; void Str::fun() {} // 报错，重复定义，不能简单将类外定义实现在头文件里，除非改成下面一行 inline void Str::fun() {} // 通过，内联函数（也可以直接把fun的定义写在类内，隐式内联） // main.cc #include \u0026#34;str.h\u0026#34; // main2.cc #include \u0026#34;str.h\u0026#34; 类与编译期的两遍处理 我们在定义类的时候，喜欢把成员函数放前面，成员变量放后面 当某些成员函数采用类内定义的时候，它用到的成员函数的定义还在后面 所以C++引入了两遍处理逻辑，看到类内定义的时候，只当作声明，等到把类的声明都过一遍之后再回来处理隐式内联函数的定义 成员函数与尾随返回类型(trail returning type) // str.h struct Str { using MyRes = int; MyRes fun(); int x; }; // str.cc #include \u0026#34;str.h\u0026#34; MyRes Str::fun() { // 错误，MyRes是属于Str类域的，需要改成Str::MyRes return x; } auto Str::fun() -\u0026gt; MyRes { // 合法，因为编译器从前面的Str::已经知道我们在处理Str类内的成员函数了，因此后面的MyRes就会从Str类域里去找定义了 return x; } 成员函数与this指针 每一个成员函数被调用的时候，编译器都会隐式地传一个this指针进去，用于确定函数里的成员变量是属于哪个对象的。 基于const的成员函数重载 this是被const修饰的，所以你不能执行this = nullptr这种，但是你可以操作this指向的内容 如果在成员函数的末尾加上const，传入的参数就变成const Str* const this，此时this和它指向的内容都无法被修改 struct Str { // 下面这两个函数能够形成重载，因为传入的this指针类型不一样 void fun(int x); // Str* const this void fun(int x) const; // const Str* const this }; 成员函数的名称查找与隐藏关系 函数内部（包括形参名称）隐藏函数外部 类内部名称隐藏类外部名称 使用this或域操作符引入依赖型名称查找(::x表示全局变量x) 静态成员函数 用于描述与类紧密相关，但又不需要一个对象实例就可以调用的函数 被所有该类型对象所共享 在调用该函数的时候，不会隐式传入this指针了 因为上一条特性，所以它不能调用一般成员变量，但可以返回静态的数据成员 成员函数基于引用限定符的重载(C++11) class Type { void foo() \u0026amp;; void foo() \u0026amp;\u0026amp;; void foo() const \u0026amp;; void foo() const \u0026amp;\u0026amp;; }; int main() { Type obj; obj.foo(); // void foo() \u0026amp;; Type().foo(); // void foo() \u0026amp;\u0026amp;; } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/class/basis/","title":"Class Basis"},{"content":"问题定义 每个时刻\\(t\\)\n智能体(Agent) 智能体执行动作\\(A_{t}\\)，并在环境中得到观测\\(O_{t}\\)和奖励\\(R_{t}\\) 环境(Environment) 环境会对智能体的动作\\(A_{t}\\)的做出反应,然后发送新的观测\\(O_{t+1}\\)和奖励\\(R_{t+1}\\) 核心概念 智能体 智能体是指强化学习需要优化的部分,是我们能够精确控制的部分 环境 环境是我们不能直接控制的部分 环境并不是指自然环境，不同的问题,智能体和环境的划分也有所区别 机器人探索房间 vs. 机器人行走控制 仿真环境中的控制 vs. 实际环境中的控制 区分智能体和环境是强化学习的第一步 奖励(Reward) 奖励是强化学习的核心 可以没有观测,但是不能没有奖励 奖励是强化学习区别其他机器学习的标志特征 特点 奖励\\(R_{t}\\)是一个标量反馈 它衡量了智能体在时间\\(t\\)上做得有多好 智能体的目标就是最大化累计奖励 强化学习组成 奖励 指智能体在执行某个动作\\(A_{t}\\)后得到的累计回报\\(G_{t}\\)，\\(G_{t}=w_{t}R_{t}+w_{t+1}R_{t+1}+w_{t+2}R_{t+2}+\u0026hellip;\\)，\\(w_{t+n}=\\gamma ^{n}\\)。其中\\(\\gamma \\)越小表示我们越关注短期奖励，\\(\\gamma \\)越大表示我们越关注长期奖励\n状态 环境状态 所有能够影响环境产生观测/奖励的数据都被认为是环境状态的一部分 环境状态一般是智能体观察不到的 智能体状态 所有能够影响智能体做出下一个动作的数据都被认为是智能体状态的一部分 一般情况下我们说的状态都是智能体状态 全观测 智能体能够直接观测到环境状态 或者说智能体状态等价于环境状态 这是强化学习的主要研究问题(马尔可夫决策过程) 部分观测 智能体不直接观测到环境状态 智能体状态 ≠ 环境状态 部分观测下的马尔可夫决策问题 动作 动作是智能体主动和环境交互的媒介 动作必须对环境起到一定的控制作用 智能体 策略(Policy) 从状态到动作的映射 最终的目的就是找到一个策略 行为策略是智能体与环境交互的策略 目标策略是我们要学习的策略(一般是最优策略) 值函数(value function) 主要用来评价不同状态的好坏，指导动作的选择 模型(model) 模型指智能体所拥有的对环境的预测模型 预测下一个状态是什么 预测下一奖励是多少 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/common/","title":"Common Knowledges on Reinforcement Learning"},{"content":"翻译单元 源文件 + 头文件（直接/间接）- 应该忽略的预处理语句 源文件所引用的所有头文件都会展开在该源文件中进行编译 一处定义原则 程序级：一般函数 翻译单元级：内联函数、类、模板(虽然可以在整个程序中有多处定义，但是需要保证一模一样) 因为编译期是以翻译单元为单位进行编译的，而上面三种类型的代码需要有具体的定义才能正常编译，光有声明是不够的。 这也是为什么Eigen全是头文件，因为它全是模板类，没办法定义在源文件中。链接 流程 预处理：file.cc -\u0026gt; file.i gcc -E ./main.cpp -o ./main.i\n将源文件转换为翻译单元的过程 防止头文件被循环展开 ifdef pragma once（推荐，用ifdef如果后面的文件宏写错了（比如两个不同h文件的宏写成一样了），那就只会include其中一个头文件）。 编译：file.i -\u0026gt; file.s g++ main.i -S -o main.s\n将翻译单元转换为相应的汇编语言表示（汇编语言还是有符号表示的） 编译优化 https://godbolt.org/z/zh9aqx 增量编译 V.S. 全部编译 有时候只修改了头文件，增量编译不知道需要重新编译，这时候就需要全部编译 汇编：file.s -\u0026gt; file.o g++ main.s -c -o main.o\n将汇编语言进一步转化为机器语言(二进制文件，看不到任何符号表示) 链接：file.o -\u0026gt; file.exe g++ main.o -S -o main 合并多个目标文件，关联声明与定义 种类：内部链接、外部链接、无链接 常见错误：找不到定义 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/compile-link/","title":"Compile \u0026 Link"},{"content":"背景 思考我们能否直接用之前提到的phr_alm方法对对称锥规划进行求解？当然可以，但我们没有用到锥的很多性质，如果可以利用起来，可以更快更准确地求得问题的解 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/conic_augmented_lagrangian/","title":"Conic Augmented Lagrangian"},{"content":"锥的定义 常见的几种Cone Nonnegative Orthant通过仿射变换可以变成线性规划LP Second Order Cones Rotatted Second Order Cones(本质上是个反射变换，不是旋转) Symmetric Cones 定义 一个锥是对称锥，当且仅当其可以表达为一个平方操作\\( \\lbrace x^{2}:=x\\circ x|x\\in\\mathbb{R}^{n}| \\rbrace \\) 其中，\\(\\circ\\)操作需要满足如下性质 \\(x\\circ%20y\\)是bilinear的 \\(x\\circ%20y=y\\circ%20x\\) \\(x^{2}\\circ(y\\circ%20x)=(x^{2}\\circ%20y)\\circ%20x\\) \\(\\langle x, y\\circ z\\rangle = \\langle x\\circ y,z\\rangle\\) \\((\\mathbb{R}^{n},\\circ)\\)所表示的这样一个集合被称为Euclidean Jordan algebra 上面三种锥都是对称锥(因为都可以转化为平方的形式) 具体来说，上面三种对称锥，各自的\\(\\circ\\)操作的定义也是不一样的 Spectral Decomposition 每个Euclidean Jordan algebra都有它的谱分解\\(x=\\sum_{i=1}^{\\theta}\\lambda_{i}q_{i}\\)，其中，\\(\\lambda_{i}\\)是特征值，\\(q_{i}\\)是特征向量 Euclidean Jordan algebra的谱分解具有如下性质 $$q_{i}^{2}=q_{i}$$ $$q_{i}\\circ q_{j(\\neq i)}=0$$ 由此我们可以推得 特征向量是互相正交的 $$\\langle q_{i}\\circ q_{j(\\neq i)} \\rangle =\\langle q_{i}\\circ q_{i},q_{j(\\neq i)}\\rangle =\\langle q_{i},q_{i}\\circ q_{j(\\neq i)}\\rangle =0$$ 一个向量属于一个对称锥，当且仅当他的所有特征值都是非负的 当所有特征值都是正数时，向量在对称锥的内部（不包含边界） 典型对称锥的谱分解 positive orthant $$\\lambda_{i}=x_{i}$$ $$q_{i}=e_{i}$$ second-order cone $$\\lambda_{i}=\\frac{x_{0}\\pm\\left|x_{1}\\right|_{2}}{\\sqrt{2}}$$ $$q_{i}=\\frac{1}{\\sqrt{2}}\\begin{bmatrix} 1 \\\\ \\pm x_{1}/\\left|x_{1}\\right|_{2} \\end{bmatrix}$$ positive semi-definite cone \\(\\lambda_{i}=\\lambda_{i}\\)，\\(q_{i}=vec(v_{i}v_{i}^{T})\\)，其中\\(\\lambda_{i}\\)和\\(v_{i}\\)是\\(mat(x)\\)的特征值和特征向量 对偶锥 定义 一个锥\\(\\kappa\\)的对偶锥的定义如下 $$\\kappa^{*}=\\lbrace x|\\langle x,y\\rangle \\geq 0, \\forall y\\in \\kappa | \\rbrace$$ 对称锥的对偶锥是它本身 优化问题的转化 QP问题的优点是可以解得很快，但缺点是有时会出错，但转化为SOCP问题后，求解的数值稳定性会更好 扩展 很多很难的优化问题，通过Lasserre hierarchy方法可以被构造成一个松弛问题（可以表达为SDP的形式） ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/conic_programming/","title":"Conic Programming"},{"content":"字面值类 可以构造编译期常量的类型 其数据成员需要是字面值类型(string就不可以，因为string不是字面值常量) 提供constexpr/consteval构造函数 class Str { public: constexpr Str(int val) : x(val) {} // constexpr表示该值既可以在编译期调用也可以在运行期调用 private: int x; }; int main() { constexpr Str a(3); // Str的构造函数一定要加constexpr修饰，否则报错 } 小心使用consteval class Str { public: consteval Str(int val) : x(val) {} // consteval表示该值可以在编译期调用 private: int x; }; int main() { int x; Str b(x); // 在低版本的编译器里可能不会报错，但是高版本的编译器会报错，导致同一份代码的编译结果不一致 } 提供constexpr和consteval成员函数（小心使用consteval） 注意：从C++14起，constexpr/consteval成员函数非const成员函数 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/class/constexpr/","title":"Constexpr of Class"},{"content":"基础 推荐书籍 Practical Augmented Lagrangian Methods for Constrained Optimization 非常重要的一本书 Randomized Algorithms 主讲Low-Dim精确算法，用Randomize降低算法的复杂度 约束优化的问题形式 $$minf(x)$$ $$g(x)\\leq0$$ $$h(x)=0$$ 常见的约束优化形式 各自的复杂度 考虑方法的时候不是要考虑理论复杂度最低的，而是要根据问题特性考虑最合适的 约束优化方法的分类 Approximation algorithm：无法得到精确解，解的精度下降到一定程度就停止了 Newton L-BFGS Exact algorithm：可以在有限迭代次数以内达到精确解 Simplex（LP、QP） 常用的都是Approximation algorithm，Exact algorithm是非常奢侈的，依赖我们对问题在几何上的理解 常见的约束优化方法 IPMs（内点法） 的特点 如果\\(m\\approx n\\)一般需要\\(O\\sqrt{m}\\)次迭代 在所有被证明了复杂度的方法里实用性最高的 虽然IPM的迭代次数是\\(\\sqrt{m}\\)但每次迭代的计算复杂度是quadratic/cubic（\\((m+n)^{2or3}\\)），计算非常稠密，所以不适合解大规模问题 有些IPM的实现用了sub-dual embeding的方法，可以判断LP/SOCP/SDP是否是infeasible的 精度相对比较高，速度相对慢 IPM有很多的工程实现 Ecos MOSEK：擅长锥规划 Gurobi：在QP问题上实现了稳定的IPM方法 HPIPM：实时性很高，从汇编开始优化线性求解器 有些情况下IPMs不是最好的选择 dim或constrain size很大 Hessian unavailable 求解精度不需要很高 求解精度需要非常高（精确解，迭代方法难以满足要求） 高频解小问题 OSQP底层是ADMM，属于一阶方法 收敛速度：IPM, SQP \u0026gt; ALM \u0026gt; ADMM ALM每次迭代的计算量较少，所以工程上很受欢迎 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/constrained_optimization/","title":"Constrained Optimization"},{"content":"构造函数 代理构造函数(C++11) 初始化列表 提高代码性能 类中的引用成员必须使用初始化列表进行初始化 类成员的初始化顺序与声明的顺序一致，与其在初始化列表中的顺序无关 class Str { public: Str(size_t input):y(input), x(y + 1) { std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; y \u0026lt;\u0026lt; std::endl; } private: size_t x; size_t y; }; // 打印结果是：4199057 4 // 因为x和y的初始化顺序是先初始化x，再初始化y，而初始化x时，y还没被初始化，所以y+1的结果是个随机值 使用初始化列表可以覆盖类内成员初始化的行为 缺省构造函数：不需要提供实际参数就可以调用的构造函数 如果类内没有提供任何构造函数，在条件允许的情况下，编译器会合成一个缺省构造函数 如果类内有构造函数，编译器就不会合成缺省构造函数了 如果类内有引用成员变量，编译器也不会合成缺省构造函数 调用缺省构造函数时避免most vexing parse struc Str { int x; }; int main() { Str m; // 合法 Str m(); // 不合法，编译器会把这行当作一个函数声明 Str m{}; // 合法 std::cout \u0026lt;\u0026lt; m.x \u0026lt;\u0026lt; std::endl; } 使用default关键字定义缺省构造函数 当我们在类内定义了一个带参数的构造函数后，编译器不会自动合成缺省构造函数了，这时候我们可以用Str() = default;来自己构造一个 单一构造函数 可以是为一种类型转换函数 struct Str { public: Str(int x) : val(x) {} private: int val; }; void fun(Str m) {} int main() { Str m(3); // 合法 Str m{3}; // 合法 Str m = 3; // 合法 fun(3); // 合法，发生了隐式类型转换 } explict关键字避免求值过程中的隐式类型转换 struct Str { public: explict Str(int x) : val(x) {} private: int val; }; void fun(Str m) {} int main() { Str m(3); // 合法 Str m{3}; // 合法 Str m = 3; // 不合法，explict起作用了 fun(3); // 不合法，explict起作用了 } 拷贝构造函数 会在涉及到拷贝初始化的场景被调用，比如：参数传递。因此需要注意拷贝构造函数的形参类型 拷贝构造函数需要传入引用，最好再加上const 如果不传引用，首先需要将实参拷贝给形参，就需要调用拷贝构造函数，而拷贝构造函数正在被定义中，就无限嵌套了 如果未显式提供，编译器会自动合成一个，合成版本会依次对每个数据成员调用拷贝构造函数，因此拷贝构造函数也可以使用default来构造 移动构造函数(C++11)：接受一个当前类右值引用对象的构造函数 std::string ori(\u0026#34;abc\u0026#34;); std::string new_str = ori; // 拷贝构造函数 std::cout \u0026lt;\u0026lt; ori \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; new_str \u0026lt;\u0026lt; std::endl; // abc abc std::string new_str2 = std::move(ori); // 移动构造函数 std::cout \u0026lt;\u0026lt; ori \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; new_str2 \u0026lt;\u0026lt; std::endl; // abc 自定义移动构造函数 struc Str { // 构造函数 Str() = default; Str(const Str\u0026amp;) = default; Str(Str\u0026amp;\u0026amp; x) : val(x.val), a(std::move(x.a)) {}; // void fun() {std::cout \u0026lt;\u0026lt; val \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; a \u0026lt;\u0026lt; std::endl;} int val = 3; std::string a = \u0026#34;abc\u0026#34;; }; int main() { Str m; m.fun(); // 3 abc Str m2 = std::move(m); m.fun(); // 3 m2.fun(); // 3 abc } 缺省移动构造函数 struct Str2 { Str2(const Str2\u0026amp;); }; struct Str { // 缺省构造 Str() = default; // 拷贝构造 Str(const Str\u0026amp;) = default; // 移动构造 Str(Str\u0026amp;\u0026amp;) = default; // 拷贝赋值 Str\u0026amp; operator= (const Str\u0026amp; x) { std::cout \u0026lt;\u0026lt; \u0026#34;copy assignment is called\u0026#34; \u0026lt;\u0026lt; std::endl; val = x.val; a = x.a; return *this; } // 移动赋值 Str\u0026amp; operator= (Str\u0026amp;\u0026amp; x) { std::cout \u0026lt;\u0026lt; \u0026#34;move assignment is called\u0026#34; \u0026lt;\u0026lt; std::endl; val = std::move(x.val); a = std::move(x.a); return *this; } int val; std::string a; Str2 m_str2; }; int main() { Str m; // 编译失败 // 因为Str2有拷贝构造函数了，编译器不会自动合成缺省构造函数，而Str的default构造函数又需要调用Str2的缺省构造函数，所以就报错了 // 解决方案：在Str2类内加入\u0026#34;Str2() = default;\u0026#34; Str m2 = std::move(m); //移动构造 // 这里会依次对Str类的成员变量调用移动构造，由于Str2没有定义移动构造，所以会执行Str2的拷贝构造 Str m3 = m; // 拷贝构造 Str m4; m4 = m; // 拷贝赋值 m4 = std::move(m); // 移动赋值 } 移动构造函数通常声明为不可抛出异常函数：在函数声明的后面加上\u0026quot;noexcept\u0026quot; 注意右值引用对象用作表达式时是左值 void fun(Str\u0026amp;\u0026amp; x) { std::cout \u0026lt;\u0026lt; x.val \u0026lt;\u0026lt; std::endl; // 在这一行里面，x是个左值 } 拷贝赋值函数(operator=) 代码如上 拷贝赋值不可以使用初始化列表 拷贝赋值函数通常返回该类型的引用 目的是为了支持\u0026quot;m1=m2=m3;\u0026ldquo;的使用方式 也可以返回void，比如把上面的\u0026quot;return *this\u0026quot;去掉，这样也可以赋值，但是不支持连等操作了 一些情况下编译器会自动合成 移动赋值函数 不可以使用初始化列表 通常返回该类型的引用 注意给自身赋值的情况 struct Str { Str\u0026amp; operator= (Str\u0026amp;\u0026amp; x) { if (\u0026amp;x == this) { return *this; } delete ptr; ptr = x.ptr; x.ptr = nullptr; } int* ptr; }; int main() { Str m1; m1 = std::move(m1); // 如果没有一开始的if判断，这里会把m1的指针给搞成nullptr，显然不是我们想要的 } 一些情况下编译器会自动合成 析构函数 无参数，无返回值，用于释放内存 内存回收是在析构函数执行完才进行 除非显示声明，否则编译器会自动合成一个，其内部逻辑为平凡的 析构函数通常不能抛出异常 因为C++在抛出异常的时候会把当前域内的数据都析构掉，如果析构过程又有新的异常，C++会直接退出，因为C++无法处理多个异常同时抛出的情况 ~Str() noexcept = default; 相关注意点 通常来说，一个类 如果需要定义析构函数，那么也需要定义拷贝构造和拷贝赋值函数 如果需要定义拷贝构造函数，那么也需要定义拷贝赋值函数 如果需要定义拷贝构造（赋值）函数，那么也需要定义移动构造（赋值）函数 default关键字 只对特殊成员函数有效（比如你只能对构造函数、析构函数等使用default方法） delete关键字 对所有函数都有效 void fun(int) = delete; void fun(double) { std::cout \u0026lt;\u0026lt; \u0026#34;double is called\u0026#34;; } class Str { public: Str() = default; ~Str() = delete; private: int* ptr; }; int main() { fun(3); // 程序报错，因为根据名称查找规则，这里会调用void fun(int)，但是该函数是delete修饰的，不能被调用 Str a; // 程序报错，因为变量a在main函数执行结束后会被销毁，但析构函数是delete修饰的，不能被调用 Str* p = new Str(); // 合法，因为new对象不会被自动销毁，所以不会调用析构函数，但是这么写是有内存泄漏的 // 同时，如果主动调用delete p;，也无法通过编译 } 注意，不要为移动构造（赋值）函数引入delete限定符 如果只允许拷贝，那就引入拷贝构造即可 如果不需要拷贝，那就将拷贝构造声明为delete即可 注意delete移动构造（赋值）对C++17的新影响 class Str { public: Str() = default; Str(const Str\u0026amp; val) = default; Str(Str\u0026amp;\u0026amp; val) = delete; }; voidf fun(Str val) {} int main() { fun(Str{}); // C++11会报错，因为传入了一个将亡值，会调用移动构造函数 // C++17不会报错，因为C++17会优化掉移动构造这一步，这就导致同一份代码在不同环境下表现不一样了 } 特殊成员的合成行为列表（红色的表示支持但可能会废除的行为） 横着看，最左侧一栏表示如果用户声明了xxx，右边的表示系统的行为。 2014年的标准 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/class/construct/","title":"Construction of Class"},{"content":"容器分类 pair \u0026amp; tuple 序列容器：对象有序排列，使用整数值进行索引 关联容器：对像顺序不重要，利用键进行索引 适配器：调整原有容器行为，使其对外展现出新的类型、接口或返回新的元素 生成器：构造元素序列 迭代器 获取迭代器 begin, end rbegin, rend crbegin, crend 迭代器分类 Input Iterator Output Iterator Forward Iterator = Input Iterator + Output Iterator，只能往前挪 Bidirection Iterator，可以双向挪动 Random Access Iterator，支持iter+N 支持迭代器的容器统称为range pair \u0026amp; tuple tuple \u0026amp; tie tuple tuple是类似pair的模板 每个pair都恰好有两个成员,tuple可以有任意数量的成员 我们希望将一些数据组合成单一对象，但又不想麻烦地定义一个新数据结构来表示这些数据时，std::tuple是非常有用的。 std::tuple中元素是被紧密地存储的(位于连续的内存区域)，而不是链式结构。 生成方式多样 auto my_tuple0 = std::make_tuple(\u0026#34;Peter\u0026#34;, 10, \u0026#34;1024\u0026#34;}; std::tuple\u0026lt;std::string, size_t, std::string\u0026gt; my_tuple1{\u0026#34;Mike\u0026#34;, 20, \u0026#34;24\u0026#34;}; std::tuple\u0026lt;std::string, size_t, std::string\u0026gt; my_tuple2{my_tuple0}; tuple 对象的成员函数 swap() 可以将它的元素和参数交换。 my_tuple2.swap (my_tuple1); 函数模板 get\u0026lt;\u0026gt;() 可以返回 tuple 中的一个元素的引用值。 auto my_tuple = std::make_tuple (Name {\u0026#34;Peter\u0026#34;,\u0026#34;Piper\u0026#34;}, 42, std::string {\u0026#34;914 626 7890\u0026#34;}); std::cout \u0026lt;\u0026lt; std::get\u0026lt;0\u0026gt;(my_tuple)\u0026lt;\u0026lt; \u0026#34;age = \u0026#34;\u0026lt;\u0026lt;std::get\u0026lt;1\u0026gt;(my_tuple)\u0026lt;\u0026lt; \u0026#34; tel: \u0026#34; \u0026lt;\u0026lt; std::get\u0026lt;2\u0026gt;(my_tuple) \u0026lt;\u0026lt; std::endl; 也可以用基于类型的 get\u0026lt;\u0026gt;() 从 tuple 获取元素，但要求 tuple 中只有一个这种类型的元素。 auto my_tuple = std::make_tuple(Name{\u0026#34;Peter\u0026#34;, \u0026#34;Piper\u0026#34;}, 42, std::string {\u0026#34;914 626 7890\u0026#34;}); std::cout \u0026lt;\u0026lt; std::get\u0026lt;Name\u0026gt;(my_tuple)\u0026lt;\u0026lt;\u0026#34; age = \u0026#34; \u0026lt;\u0026lt; std::get\u0026lt;int\u0026gt; (my_tuple)\u0026lt;\u0026lt; \u0026#34; tel: \u0026#34; \u0026lt;\u0026lt;std::get\u0026lt;std::string\u0026gt;(my_tuple) \u0026lt;\u0026lt; std::endl; tie tuple将几个不同类型的变量打包为一个对象，tie则负责将tuple类型的对象解构为几个变量 tuple\u0026lt;int,double,string\u0026gt; t3 = {1, 2.0, \u0026#34;3\u0026#34;}; int i; double d; string s; tie(i, d, s) = t3; tie(i, d, s) = {1, 2.0, \u0026#34;3\u0026#34;};//这一行会报错，因为tie只能解构tuple 序列容器 array:元素个数固定的序列容器，不支持添加和删除 定义时必须用常量指定array的大小，因为大小是模板参数之一，不可忽略 当容器中的元素是连续存储的时候，容器都会有一个data()接口，返回指向第一个元素的指针 swap的实现是元素复制，效率很低 vector:元素连续存储的序列容器 vector也有data()接口 swap是指针交换，效率很高 emplace_back()相比push_back()少了一次对象的拷贝或者移动，因此当对象是string或者自定义结构数据时，用emplace_back效率更高 (emplace_back vs push_back)[https://zhuanlan.zhihu.com/p/183861524] insert和emplace是在vector中间插入元素，但效率很低；emplace和insert的差异与上面的emplace_back和push_back的差异一致 会导致iter失效的操作：swap、push_back等写操作 list:双向链表的容器 插入和删除成本较低，随机访问成本较高 支持pop_front, push_front操作，但是不支持[]访问 提供了splice接口，A.splice(it, B)：将list B插入到A的it位置 写操作通常不改变iter有效性 如果你的程序有很多小的元素，且空间的额外开销很重要，则不要使用list或forward_list forward_list:基于单向链表的容器 只支持递增，无rbegin 不支持size 不支持push_back, pop_back 提供了xxx_after操作 deque:vector与list的折衷，它会将整个容器分成若干段，段内是连续存储，段间是链表 push_back和push_front比较快 在序列中间插入删除比较慢 通常情况下我们不会想要用deque，随机访问速度不如vector，插入删除不如list 只有在我们想要得到类似于vector的功能，但又希望push_front比较快的时候才会使用deque basic_string:提供了对字符串的专门支持 提供了数值与字符串转换的接口 短字符串优化，(short string optimization: SSO)[https://tigercosmos.xyz/post/2022/06/c++/sso/] 关联容器 按底层实现分为两类 set/map/multiset/multimap 底层用红黑树实现 unordered_xxx 底层用hash表实现 set 元素需要支持使用\u0026lt;比较大小，如果是自定义的元素，需要定义比较函数并在初始化的时候传入std::set\u0026lt;MyType, Cmp\u0026gt; #include \u0026lt;set\u0026gt; #include \u0026lt;type_traits\u0026gt; struct MyType { int x; }; bool MyCmp(const MyType\u0026amp; lhs, const MyType\u0026amp; rhs) { return lhs.x \u0026lt; rhs.x; } int main() { std::set\u0026lt;MyType, decltype(\u0026amp;MyCmp)\u0026gt; s({MyType{1}, MyType{2}}, MyCmp); return 0; } 插入insert/emplace/emplace_hint emplace_hint可以给出一些插入的提示，从而加速插入的速度，但是如果hint给错了，反而增加耗时 std::set\u0026lt;MyType, decltype(\u0026amp;MyCmp)\u0026gt; s({MyType{3}, MyType{5}}, MyCmp); s.insert(MyType{100}); s.emplace(100); // 两者等价，推荐使用emplace，减少拷贝和移动 提供了extract来修改元素(C++17)，但是操作很复杂。 set的迭代器是只读的，不能用于修改元素。 map 每个节点是个std::pair，其中pair.first是const类型，不能修改 key需要支持使用\u0026lt;比较大小，也支持自定义比较函数 支持k，v分别获取 std::map\u0026lt;int, bool\u0026gt; m{{3, true}}; for (auto\u0026amp; [k, v] : m) { std::cout \u0026lt;\u0026lt; k \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; v \u0026lt;\u0026lt; std::endl; } 访问元素：find/contain/[]/at map.at(key)若key不存在，会抛出异常 map[key]若key不存在，会插入一个新的pair\u0026lt;key,T()\u0026gt; multiset/multimap 允许重复key 元素访问： find：返回首个查到的元素 count：返回元素个数 lower_bound/upper_bound/equal_range：返回查找到的区间 std::multiset\u0026lt;int\u0026gt; s{1, 3, 1}; auto [b, e] = s.equal_range(100); for (auto iter = b; iter != e; ++iter) { // ... } auto b = s.lower_bound(100); auto e = s.upper_bound(100); unordered_xxx 底层实现： 新建一个size=N的bucket vector 对于新插入的元素，将其key转换为hash值n，index = n % N，往bucket vector[index]中维护的链表的尾部插入该元素 每一个bucket中都维护了一个链表，一般来说大部分bucket中的链表元素的长度都比较小 与set,map相比，查找性能更好，转换成hash后找对应的bucket，如果其中的链表元素个数是1，直接返回；即使大于1，也一般都比较小，做几次判断即可。 插入操作一些情况下会慢，比如一个bucket中包含的链表元素过多了，就会出发rehash，这个过程就比较耗时了 key需要支持两种操作 转换为hash 判等 除了!=和==，不支持容器级别的关系运算；同时!=和==的计算很慢，因为当一个bucket中的链表元素较多时，为了判断两个set是否相等，需要将两条链表判断是否相等 自定义hash转换 方法1 struct Str { int x; }; size_t MyHash(const Str\u0026amp; val) { return val.x; } bool MyEqual(const Str\u0026amp; lhs, const Str\u0026amp; rhs) { return lhs.x = rhs.x; } std::unordered_set\u0026lt;Str, decltype(\u0026amp;MyHash), decltype(\u0026amp;MyEqual)\u0026gt; s{1, MyHash, MyEqual}; // 1 是bucket vector的size，最小为1 方法2(推荐) class Str { int x; bool operate==(const Str\u0026amp; s) const { return (this-\u0026gt;x == s.x); } }; class MyHasFunction { public: size_t operator(const Str\u0026amp; s) const { return s.x; } }; std::unordered_set\u0026lt;Str, MyHasFunction\u0026gt; s; 适配器 类型适配器 basic_string_view (C++17) 代码demo void fun(std::string_view str) { // 这里不需要引用，因为string_view只记录了string开头和结尾的位置，无论字符串本身有多长，它的内存都很小 if (!str.empty()) { std::cout \u0026lt;\u0026lt; str[0] \u0026lt;\u0026lt; std::endl; } } fun(\u0026#34;1234\u0026#34;); // fun(char[6]) fun(std::string(\u0026#34;1234\u0026#34;)); // fun(std::string) std::string s(\u0026#34;12345\u0026#34;); fun(std::string_view(s.begin(), s.begin() + 3)); // 只传入\u0026#34;123\u0026#34; 提供较低成本的操作接口 比如std::string的substr会开辟一段新的内存来存放截取到的字符串，但std::string_view的substr只会初始化一个新的string_view来记录截取的字符串，string_view只占16个字节，所以很轻量 不能进行写操作 一般string_view只作为函数的输入，作为输出的时候需要小心，函数中的临时变量销毁导致string_view记录的指针位置失效 span (C++20) span就是string_view的功能在其他类型的容器上的扩展，用于提升代码的性能 只能处理连续存储的容器，比如vector、array 支持写操作，这与string_view不同 接口适配器 stack stack中维护了一个底层容器，然后封装了它的接口，只保留了push，pop，top这三个操作 std::stack\u0026lt;int\u0026gt; s; std::stack\u0026lt;int, std::vector\u0026lt;int\u0026gt;\u0026gt; s; // 指定使用vector作为stack的底层容器 queue priority_queue 输入的元素需要支持比较操作（比较操作用于确定优先级） 支持自定义比较函数 输入元素和queue一致，但输出的元素一定是所有元素中优先级最高的元素 数值适配器(C++20) std::ranges::XXX_view, std::ranges::views::XXX, std::views::XXX 可以将一个输出区间中的值变换后输出 demo1 std::vector\u0026lt;int\u0026gt; v{1, 2, 3, 4, 5}; int Square(int i) { return i * i; } for (auto p : std::ranges::transform_view(v, Square)) { std::cout \u0026lt;\u0026lt; p \u0026lt;\u0026lt; \u0026#34; \u0026#34;; // 打印出1, 4, 9, 16, 25 } std::cout \u0026lt;\u0026lt; std::endl; bool IsEven(int i) { return i % 2 == 0; } // 用法1 for (auto p : std::ranges::filter_view(v, isEven)) { std::cout \u0026lt;\u0026lt; p \u0026lt;\u0026lt; \u0026#34; \u0026#34;; // 只打印出偶数 } std::cout \u0026lt;\u0026lt; std::endl; // 用法2 auto x = std::views::filter(IsEven); for (auto p : x(v)) { std::cout \u0026lt;\u0026lt; p \u0026lt;\u0026lt; \u0026#34; \u0026#34;; // 只打印出偶数 } std::cout \u0026lt;\u0026lt; std::endl; // 用法3 auto x2 = std::views::filter(IsEven); auto y = std::views::transform(Square); for (auto p : v | x2 | y) { // 按位或，模拟linux bash中的pipe功能，这里可以无限后缀新操作 v | x2 | y | k std::cout \u0026lt;\u0026lt; p \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl; // 用法4 auto operate = std::views::filter(IsEven) ｜ std::views::transform(Square); for (auto p : v | operate) { std::cout \u0026lt;\u0026lt; p \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl; 数值适配器可以组合，引入复杂的数值适配逻辑 view这种算法和之前讨论的泛型算法有两点核心区别 view没有对输入的东西立即进行计算，而是需要的时候才进行计算, 这样可以提高性能（比如10000个数据，只有前几个元素会被view计算，后面的计算就省掉了） view模糊了算法和容器的概念，它可以被称作容器，也可以被称作算法 配合ranges还可以灵活组织程序逻辑 生成器(C++20) std::ranges::itoa_view for (int i : std::ranges::itoa_view{1, 10}) { std::cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl; for (int i : std::views::itoa(1, 10)) { std::cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl; // std::views::itoa(...)，如果这里的...只有一个元素，就表示生成一个以该元素为开头的无限长的容器，后面的take(n)表示取容器中的前9个元素 for (int i : std::views::itoa(1) | std::views::take(9)) { std::cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl; // 上面三种写法的输出都是1，2，3，4，5，6，7，8，9 注意事项 尽量使用at方法来访问元素 运算符[]不会对索引值进行检查，像调用myarray[-1]是不会报错的 使用at()，将在运行期间捕获非法索引，默认将程序中断 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/container/","title":"Container"},{"content":"基于值函数的深度学习网络 DQN 深度强化学习的鼻祖式工作 解决了两个问题 一段时间内的训练数据具有较强的相关性，如果按照顺序进行训练，会对当前状态下的情况产生过拟合。通过将历史训练数据存在一个buffer里，通过随机抽取的方式进行训练，解决了这一问题。 首次证明了能够通过raw pixels解决游戏问题，对所有游戏通用 关键特点 Q Learning + DNN Experience Replay Target Network 算法流程 Double DQN 核心思路 DQN中的TD目标值\\(r+\\gamma max_{a}Q({s}\u0026rsquo;,a)\\)存在max操作，会引入一个正向偏差 因此建模两个Q网络，一个用于选动作，一个用于评估动作： $$r+\\gamma Q^{B}({s}\u0026rsquo;,argmax_{a}Q^{A}({s}\u0026rsquo;,a))$$ 其实只要把DQN的target network也变成独立更新的就行了 算法流程 Dueling Network 核心思路 用一个网络，分别学习V函数和A(优势)函数，最后相加得到Q函数 优势 对于很多状态，不需要估计每个动作的Q值，每来一个样本都可以更新一次V函数，V函数的学习机会比Q函数高很多，- 泛化性能好，当新的动作进来时，不需要重新学习V，只需要重新学习A 减少了Q函数由于状态和动作维度差导致的噪声和突变 Prioritized Experience Replay 核心思路 DQN是从memory中均匀的采样，有时候，我们希望更多的去采样对学习有帮助的片段，给不同的experience提供不同的权重 利用TD误差去衡量权重 需要使用sum-tree以及binary heap data structure去实现 为了保证新加入的样本至少被采样一次，新样本的TD误差会被设置为最大 类似于DP中的优先清理 Experience Replay 使得更新不受限于实际经验的顺序 Prioritized Experience Replay 使得更新不受限于实际经验的频率 存在问题 TD 误差对噪声敏感 TD 误差小的 transition 长时间不更新 过分关注 TD 误差大的 transition 丧失了样本多样性 使用某种分布采样了 Experience, 会引入 Bias 解决方法 两种变体： $$p_{i}=|\\delta _{i}|+e$$ 其中，\\(\\delta _{i}\\)表示TD误差，\\(e\\)表示人为施加的噪声，通过这个噪声保证多样性 \\(p_{i}=\\frac{1}{rank(i)}\\)，序号的倒数来表示权重，对噪声的敏感度下降。简单的说，即使TD误差有0.1的误差，但顺序上依然是排第二，这时，误差就没有影响了。 重要性采样，消除Bias 算法流程 Rainbow 核心思想 将大量的前人的工作汇总实现，明确每一种改进所对应的效果，采用的工作有 DQN Double DQN Dueling DQN Prioritized Experience Replay NoiseNet(Noisy Netwoks for Exploration, AAAI2018) Distributional RL(A Distributional Perspective on Reinforcement Learning, 2017) 效果 Rainbow的效果比所有的base line好 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/dqn/","title":"Deep Q Learning"},{"content":"背景 起源 模拟人的大脑思考机制，每时每刻，人脑都在接受巨量的信息。人脑的容量无法同时处理如此海量的信息，因此，人会将大部分的脑力资源集中在部分需要特别关注的信息点上。 attention模型最早应用在seq2eq模型上。 Encoder-Decoder 这个框架可以看做是深度学习领域的研究模式，应用场景十分的广泛，可看作是由一个句子（或是篇章）生成另一个句子（或是篇章）的通用处理框架。 Encoder: 对输入句子Source进行编码，将句子通过非线性的变换转化成一个中间的语句表示，即\\(C=F(source)\\) Decoder: Decoder: 根据中间语义表示𝐶𝐶和之前的已经生成的历史信息\\(y_{1},y_{2},\u0026hellip;,y_{t-1}\\)来生成\\(t\\)时刻的内容，即\\(y_{t}=G(C,y_{t-1})\\) 对比 RNN：无法并行，由于误差只能有效的传递到前两个历史时刻，无法很好的学习到全局的结构信息。 CNN：很容易并行，容易捕捉到一些局部结构信息。 注意力机制：注意力机制直接利用整个Encoder序列对当前的目标进行计算，因此能够很好的获得全局结构信息。同时，对Decoder中每个单词的注意力计算相互之间没有依赖关系，因此也能够进行并行计算。 注意力机制 基本结构 上图中的\\(\\alpha_{ij}\\)就是描述的\\(c_{i}\\)对\\(h_{j}\\)的注意力大小 可以看到\\(\\alpha_{ij}\\)是通过对\\(e_{ij}\\)softmax操作得来的。\\(e_{ij}\\)。这边用\\(h_{i-1}^{\u0026rsquo;}\\)的原因在于，我们要求的是\\(h_{i}^{\u0026rsquo;}\\)，我们已知的是\\(\u0026hellip;, h_{i-2}^{\u0026rsquo;}, h_{i-1}^{\u0026rsquo;}\\)，所以我们就用最近的\\(h_{i-1}^{\u0026rsquo;}\\)和source中的每个\\(h_{i}\\)通过一个激活函数\\(a(x,y)\\)，计算彼此之间的关联度。关联度归一化后就变成了概率信息。 最后解码的时候利用到的是\\(c_{i}\\)，而不是原先的一个固定的\\(C\\) 分类 SoftAttention 由于每一个\\(c_{i}\\)都是通过对原始输入\\(x\\)在输出侧的\\(y_{i}\\)上的一个概率分布来进行计算的，因此获得是当前需要解码位置在原始输入序列上的一个注意力分布，可以被嵌入到模型里面去，直接训练。 HardAttention 是一个随机的过程，它不会选择整个encoder的输出作为其输入（注意看下图decoder层每个输出到encoder的连接，不是全连接），而是会依据概率来采样encoder端的某些隐藏层作为其attention。为了实现梯度的反向传播，需要使用蒙特卡洛的方法来估计模块的梯度。 GlobalAttention 与传统的Attention模式一样。所有的encoder端的hidden state都被用于计算背景向量的权重。 LocalAttention 将Soft \u0026amp; Hard Attention结合起来，每次先为decoder端当前的词，预测一个source端对齐位置（aligned position）\\(p_{t}\\)。然后基于\\(p_{t}\\)选择一个窗口，用于计算背景向量\\(c_{t}\\)。 $$p_{t}=S\\cdot sigmod(v_{p}^{t}tanh(W_{p}h_{t}))$$ $$a_{t}(s)=align(h_{t},h_{t}^{\u0026rsquo;})exp(-\\frac{(s-p_{t}^{2})}{2\\sigma^{2}})$$ 核心思想 本质上，Attention机制是对source中元素的value（指代source中的\\(h_{i}\\)或\\(x_{i}\\)）值进行加权求和，而query（上文中的\\(h_{i}^{\u0026rsquo;}\\)）和key（source中的\\(h_{j}\\)）用来计算对应value的权重系数 $$attention(query,key)=\\sum_{i=1}^{t}sim(query,key_{i})*value_{i}$$ 聚焦的过程体现在权重系数的计算上，权重越大越聚焦到其对应的value上，即权重代表了信息的重要性，而value是其对应的信息。 计算步骤 阶段一：根据query\u0026lt;-\u0026gt;key计算相似性或相关性 阶段二：原始分值归一化、概率化 阶段三：根据权重系数对value进行加权求和 $$Attention(Q,K,V)=softmax(\\frac{QK^{T}}{\\sqrt{d_{k}}})V$$ $$Q\\in R^{n\\times d_{k}}$$ $$K\\in R^{m\\times d_{k}}$$ $$V\\in R^{m\\times d_{v}}$$ 公式理解 如果忽略激活函数softmax的话，那么事实上它就是三个\\(n\\times d_{k}\\), \\(d_{k}\\times m\\), \\(m\\times d_{v}\\)的矩阵相乘，最后的结果也是一个\\(n\\times d_{v}\\)的矩阵。即\\(n\\times d_{k}\\)的序列\\(Q\\)编码成了一个新的\\(n\\times d_{v}\\) $$Attention(q_{t},K,V)=\\sum_{s=1}^{m}\\frac{1}{Z}exp(\\frac{\u0026lt;Q,k_{s}\u0026gt;}{d_{k}})v_{s}$$ \\(K\\)和\\(V\\)是一一对应的，每次拿一个\\(q_{t}\\)和各个key做内积并softmax，得到\\(q_{t}\\)与各个\\(v_{s}\\)的相似程度，然后加权求和，得到一个\\(d_{v}\\)的向量。 其中，因子\\(\\sqrt{d_{k}}\\)起到调节的作用，使得内积不至于太大（太大的话softmax就是非0即1了，不够soft） 这个定义只是注意力的一种形式，还有一些其他的选择，比如query和key的运算方式并不一定是点乘，还可以是拼接后再内积一个参数向量，甚至是权重都不一定要归一化等。 扩展 Mutil-Head Attention\nMutil-Head Attention是Google提出的新概念，是Attention机制的完善，不过从形式上看，它其实就是把Q,K,V通过参数矩阵映射一下，然后再做Attention，把这个过程重复做了h次，结果拼接起来就行了。 $$head_{i}=Attention(QW_{i}^{Q},KW_{i}^{K},VW_{i}^{V})$$ $$W_{i}^{Q}\\in R^{d_{k}\\times \\breve{d}{k}},W{i}^{K}\\in R^{d_{k}\\times \\breve{d}{k}},W{i}^{V}\\in R^{d_{v}\\times \\breve{d}_{v}}$$ $$MultiHead(Q,K,V)=Concat(head_{1},\u0026hellip;,head_{n})$$ 最后得到一个\\(n\\times (h\\breve{d}_{k})\\)的序列，所谓的“多头”，就是多做几次同样的事情（参数不共享），然后把结果进行拼接。 Self Attention\n传统的Attention是基于source端和target端的隐变量计算的。得到的结果是source端的每个单词与target端的每个词之间的依赖关系。 Self Attention分别在source端和target端进行，但是分别在两端捕捉自身的词与词之间的依赖关系，然后再把source端得到的self Attention加入到target端得到的self Attention中，捕捉source端和target端词与词之间的依赖关系。 Self Attention要比传统的Attention Mechanism效果好，主要原因之一是：传统的Attention机制忽略了source/target端自身的词之间的依赖关系。 在Google的论文中，大部分的Attention都是Self Attention。 Self Attention就是\\(Attention(X, X, X)\\), \\(X\\) 是前面说的输入序列，也就是在序列内部做Attention，寻找序列内部的联系。 Google论文的主要贡献之一就是它表明了内部注意力在机器翻译（甚至在一般的seq2seq）任务的序列编码上是相当重要的，而之前关于seq2seq的研究基本都只是把注意力机制用在解码端。 Posting Embedding\nAttention模型并不能捕捉序列的顺序，换句话说，如果将K,V按行打乱顺序（相当于句子中的词序打乱），那么Attention模型的结果还是一样，这就表明了它顶多是一个非常巧妙的BOW模型。 顺序对于时间序列至关重要，如果学习不到顺序信息，那么效果会大打折扣。 Position Embedding将每个位置编号，然后每个编号对应一个向量，通过结合位置向量和词向量，就给每个词都引入了一定的位置信息，这样Attention就可以分辨出不同位置的词了。 以前的RNN，CNN模型中都出现过Position Embedding，但是那些模型本身就能够学习到序列的位置信息，所以Position Embedding(PE)不是必须的。但是在Attention模型中，PE是位置信息的唯一来源，因此它是模型的核心成分之一，并非仅仅是简单的辅助手段。 在之前的PE中，基本都是根据任务训练出来的向量，而Google直接给出了一个构造PE的公式： $$PE_{2i}(p)=sin(\\frac{p}{10000^{\\frac{2i}{d_{pos}}}})$$ $$PE_{2i+1}(p)=cos(\\frac{p}{10000^{\\frac{2i}{d_{pos}}}})$$ Position Embedding 本身是一个绝对位置的信息，但是在语言中，相对位置也很重要，Google选择前述的位置向量公式一个重要的原因是：由于有\\(sin(\\alpha +\\beta )=sin\\alpha cos\\beta +cos\\alpha sin\\beta\\)和\\(cos(\\alpha +\\beta )=cos\\alpha cos\\beta -sin\\alpha sin\\beta\\)，这表明位置\\(p+k\\)的向量可以表明位置为\\(p\\)的向量的线性变换，这提供了表达相对位置的可能性。 结合位置向量和词向量的几个可选方案：可以把他们拼接起来作为一个新向量，也可以把位置向量定义为跟词向量一样大小，然后两者加起来。Facebook的论文和Google的论文中用的都是后者。直觉上相加会导致信息损失，似乎是不可取，但是实验证明相加也是很好的方案。 缺点 Attention层的好处就是能够一步到位的捕捉全局的联系，因为它直接把序列两两比较（代价是计算量变为\\(O(n^{2})\\)）。相比之下，RNN需要一步步地递推才能捕捉到，而CNN需要通过层叠扩大感受野，这是Attention层的明显优势。 Attention虽然跟CNN没有直接联系，但是事实上充分的借鉴了CNN的思想，比如Multi-Head Attention就是Attention做多次然后拼接，这个CNN的多个卷积核思想是一致的，论文用到的残差结构都源于CNN网络。 无法对位置信息进行很好的建模，PE的引入无法从根本上解决这个问题。 并非所有的问题都是需要长程的全局的依赖，也有很多的问题只是依赖于局部结构，这时候用纯的Attention不是很好。所以论文中还是提到了一个受限Attention的概念。 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/deep-learning/attention/","title":"DL Attention"},{"content":"基础知识 极大似然估计 $$p(x|\\theta)$$ 如果\\(\\theta\\)是已知的，则该函数叫做概率函数(Probability)，描述的是不同样本点 出现的概率； 如果\\(x\\)是已知的，则该函数叫做似然函数(Likelihood)，描述的是对于不同的模型参数，出现样本点\\(x\\)的概率。 反向传播 TODO 激活函数 作用：对输入空间进行“弯曲（非线性变换）“，提升神经网络的非线性表达能力。 设计原则 非线性函数，且连续可导 函数及其导函数易于计算 导函数的值域以及分布 典型函数 Z字型下降 如果输入神经元的数据总是正数（无论什么数值经过sigmoid之后都是正数），那么上图的\\(a_{i}^{(j)}\\)都是正数。梯度的符号取决于\\(\\delta _{2}^{(3)}\\)，要么全部是正数，要么全部是负数。但很多时候，\\(\\Theta _{ij}^{(l)}\\)的梯度更新方向是异号的，类似于右下角图片的黑色线段，同号就会导致更新需要花费很多步来回迭代到最优解（参数的运动路线像字母Z） 卷积 感受野(Local Receptive Fields)：卷积神经网络每一层输出的特征图(feature map)上的像素点在原始图像（网络开始的输入图像）上映射的区域大小 Max Pooling的作用 减少计算量，减少内存消耗 提高感受野的大小 减少参数数量 增加平移不变性 模型的评价 期望风险(Expected Risk)：评估当前模型（也就是映射函数）在真实数据分布下的预测损失Loss的期望。前提是已知真实数据分布下的误差，那也就是说模型的真实误差。 我们所能得到的观察数据是真实数据的一个真子集，因此可以利用模型在观察数据上的误差来近似反应模型在真实数据上的拟合能力，将这个误差称为经验误差。将在所有观察数据中得到的平均误差称为经验风险(Empirical Risk) 过拟合是因为模型在训练数据集上拟合能力太强，反而对于新的测试数据表现不佳。根据奥卡姆剃刀原则需要限制模型的能力(模型的参数量)，从而提高泛化能力，因此，我们追求的是结构风险最小化。 奥卡姆剃刀原则：如无必要，勿增实体。简单的模型泛化能力更好，如果有两个性能相近的模型，我们应该选择更简单的模型。 损失函数 偏差方差 为了避免过拟合，我们经常会在模型的拟合能力和复杂度之间进行权衡。 方差一般会随着训练样本的增加而减少。当样本比较多时，方差比较少，这时可以选择能力强的模型来减少偏差。 随着模型复杂度的增加，模型的拟合能力变强，偏差减少而方差增大，从而导致过拟合。 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/deep-learning/common/","title":"DL Common Knowledges"},{"content":"训练目标 期望风险(Expected Risk)：评估当前模型（也就是映射函数）在真实数据分布下的预测损失Loss的期望。前提是已知真实数据分布下的误差，那也就是说模型的真实误差。 我们所能得到的观察数据是真实数据的一个真子集，因此可以利用模型在观察数据上的误差来近似反应模型在真实数据上的拟合能力，将这个误差称为经验误差。将在所有观察数据中得到的平均误差称为经验风险(Empirical Risk) 过拟合是因为模型在训练数据集上拟合能力太强，反而对于新的测试数据表现不佳。根据奥卡姆剃刀原则需要限制模型的能力(模型的参数量)，从而提高泛化能力，因此，我们追求的是结构风险最小化。 奥卡姆剃刀原则：如无必要，勿增实体。简单的模型泛化能力更好，如果有两个性能相近的模型，我们应该选择更简单的模型。 损失函数 偏差方差 为了避免过拟合，我们经常会在模型的拟合能力和复杂度之间进行权衡。 方差一般会随着训练样本的增加而减少。当样本比较多时，方差比较少，这时可以选择能力强的模型来减少偏差。 随着模型复杂度的增加，模型的拟合能力变强，偏差减少而方差增大，从而导致过拟合。 数据集划分 把数据集全部作为训练集，然后用这个训练集训练模型，用训练集验证模型。 选择训练集误差最小的模型 把数据集随机分为训练集和测试集，训练集训练模型，测试集验证模型。 选择测试集误差最小的模型 把数据集分为训练集、验证集和测试集，训练集训练模型，验证集验证模型。 根据情况不断调整模型，选择出最好的模型。再用训练集和验证集训练出一个最终的模型，最后用测试集评估最终的模型。 交叉验证把原始数据集平均分为K组不重复的子集，每次选择K-1组子集作为训练集，剩下一组子集作为验证集。 K次实验得到K个模型，将K个模型在各自验证集上的错误率的平均作为分类器的评价 评价指标 典型指标 准确率（Accuracy） 准确率和错误率是在所有类别上整体的性能平均，由于数据类别的分布不平衡性，例如正负比例为9:1，那么模型只需要将所有样本全部分类为正例，也能获得90%的准确率。如果希望对每个类别都进行性能的评估，就需要计算精确率和召回率。 错误率（Error Rate） 精确率（Precision） $$P=\\frac{TP}{TP+FP}$$ 召回率（Recall） $$P=\\frac{TP}{TP+FN}$$ F值（F Measure） F值是一个综合指标，为精确率和召回率的调和平均 $$f=\\frac{(1+\\beta ^{2})\\times P\\times R}{\\beta ^{2}\\times P+R}$$ $$F=f(\\beta =1)$$ 宏平均（Macro Average） 微平均（Micro Average） 在实际应用中，我们也可以通过调整分类模型的阈值来进行更全面的评价，比如AUC(Area Under Curve)、ROC(Receiver Operating Characteristic)曲线、PR(Precision-Recall)曲线等。此外，很多任务还有自己专门的评价方式，比如TopN准确率。 网络优化 梯度下降算法 批量梯度下降 Batch Gradient Descent 又叫Vanilla Gradient Descent，每次训练更新模型时采用的是整个训练集合的所有样本点。 优点 每次更新朝着正确的方向进行 保证收敛于极值点，凸函数收敛于全局极值点 缺点 学习时间太长 消耗大量内存 随机梯度下降 Stochastic Gradient Descent 对每个训练样本进行参数更新。因为批量梯度下降在每次参数更新前重新计算类似样本的梯度，因此会对大数据集会有一些冗余的计算。而随机梯度下降由于每次只选取一个样本，所以会消除这种冗余 优点 运行速度快，允许在线更新模型 会跳出局部极小值点到另一个更好的局部极小值点 缺点 每次更新可能并不按照正确的方向进行 以较大的方差频繁地更新，目标函数剧烈波动 小批量梯度下降 Mini-Batch Gradient Descent 在每次更新模型时使用的样本量做了权衡，即每次用多个样本组成的数据集来计算梯度并更新 优点 降低了参数更新的方差，获得更加稳定的收敛 利用深度学习库中常用的高度优化的矩阵优化方法 缺点 学习率的选择困难 会陷入无限次的局部极小值，或鞍点 影响效果的主要因素 批量大小选择 一般而言，批量大小不影响随机梯度的期望，但是会影响随机梯度的方差。批量大小越大，随机梯度的方差越小，引入的噪声也越小，训练也越稳定，因此可以设置较大的学习率。而批量大小较小时，需要设置较小的学习率，否则模型会不收敛。 线性缩放规则(Linear Scaling Rule)：当批量大小增加𝑚𝑚倍时，学习率也增加𝑚𝑚倍。线性缩放规则往往在批量大小比较小时适用，当批量大小非常大时，线性缩放会使得训练不稳定。 学习率 学习率过大，模型不会收敛；过小的话，收敛速度太慢。 学习率衰减 从经验上看，学习率在刚开始的时候需要设置大一点保证收敛的速度，在收敛到最优点附近时要小一点避免来回震荡。 分段常数衰减（Piecewise Constant Decay） 逆时衰减（Inverse Time Decay） 指数衰减（Exponential Decay） 自然指数衰减（Natural Exponential Decay） 余弦衰减（Cosine Decay） 学习率预热 训练刚开始的时候模型的参数是随机设置的，因此在使用Mini-Batch梯度下降法时，将学习率设置较大的情况下梯度也往往比较大，使得训练不稳定。为了提高训练的稳定性，可以在最初几轮的时候，采用比较小的学习率，等梯度下降到一定程度之后再恢复到初始的学习率，这种方法称为学习率预热。当预热过程结束，再选择一种学习率衰减方法来逐渐降低学习率。 周期性学习率调整 为了使得梯度下降法能够逃离鞍点或尖锐最小值，一种经验性的方式是在训练过程中周期性地增大学习率。当参数处于尖锐最小值附近时，增大学习率有助于逃离尖锐最小值；当参数处于平坦最小值附近时，增大学习率依然有可能在该平坦最小值的吸引域(Basin of Attraction)内。因此，周期性地增大学习率虽然可能短期内损害优化过程，使得网络收敛的稳定性变差，但从长期来看有助于找到更好的局部最优解。 自适应调整学习率方法 AdaGrad 借鉴L2正则化的思想，每次迭代时自适应地调整每个参数的学习率，梯度平方越大，学习率越大。 缺点：经过一定次数迭代依然没有找到最优点时，由于学习率已经非常小，很难再继续找到最优点。 RMSprop 在有些情况下避免AdaGrad算法中学习率不断单调下降以至于过早衰减的缺点 AdaDelta 也是AdaGrad算法的一个改进。和RMSprop算法类似AdaDelta算法通过梯度平方的指数衰减移动平均来调整学习率。 梯度估计修正 在Mini-Batch梯度下降算法中，每次选取的样本数量比较小，损失会呈现震荡的方式下降(每次迭代时梯度的估计值和整个训练集上的最优梯度并不一致)。一种有效的方式是通过使用最近一段时间内的平均梯度来代替当前时刻的随机梯度来作为参数更新的方向，从而提高优化速度。 Momentum算法 利用之前积累的的动量来替代真正的梯度，每次迭代的梯度可以看作是加速度。在第N次迭代时，计算负梯度的“加权移动平均”作为参数的更新方. 每个参数的实际更新差值取决于最近一段时间内梯度的加权平均值。当某个参数在最近一段时间内的梯度方向不一致时，其真实的参数更新幅度变小；相反，当在最近一段时间内的梯度方向都一致时，其真实的参数更新幅度变大，起到加速作用。 Nesterov加速梯度法 是一种对动量法的改进。 Adam算法 可以看作Momentum算法和RMSprop算法的结合，不但使用动量作为参数更新方向，而且可以自适应调整学习率。 梯度截断法 在深度神经网络或循环神经网络中，除了梯度消失之外，梯度爆炸也是影响学习效率的主要因素。在基于梯度下降的优化过程中，如果梯度突然增大，用大的梯度更新参数反而会导致其远离最优点。为了避免这种情况，当梯度的模大于一定阈值时，就对梯度进行截断。 总结 名词解释 Epoch：使用训练集全部数据对模型进行一次完整的训练，称为“一代训练”。 Batch：使用训练集中一小部分样本利用MBGD更新一次参数，这部分样本被称为“一批数据”。 Iteration：使用一个Batch的数据对模型进行一次参数更新的过程，称为“一次训练”。 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/deep-learning/evaluation/","title":"DL Evaluation"},{"content":"sizeof sizeof 无法获取动态分配内存的大小, 因为sizeof是在编译期就确定的，而动态内存的大小是在运行期决定的 std::vector\u0026lt;int\u0026gt; x; x.push_back(10); x.push_back(10); std::cout \u0026lt;\u0026lt; sizeof(x) \u0026lt;\u0026lt; std::endl; // 无法获取x的大小 allocator 使用allocator来分配内存和释放内存\nstd::allocator\u0026lt;int\u0026gt; al; int* ptr = al.allocate(3); al.deallocate(ptr, 3); 缺陷：只能分配固定类型的内存，比如上面的例子只能分配int类型\n推荐使用allocator，因为allocator是C++标准，malloc，aligned_alloc是C标准\nmalloc \u0026amp; free 使用malloc和free来管理内存\nint* p1 = malloc(4 * sizeof(int)); int* p2 = malloc(sizeof(int[4])); // same space free(p1); free(p2); 优点：只关注分配类型的大小，不受限于类型\n缺陷：不能分配对齐内存\naligned_alloc 可以分配对齐内存 动态内存与异常安全 int* ptr = new int(3); /* 异常触发，跳转到异常处理语句，没有执行下面的delete，造成内存泄漏 建议使用智能指针，避免该问题 */ delete ptr; ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/dyn-memory/","title":"Dynamic Memory"},{"content":"概念定义 当有一个精确的环境模型时,可以用动态规划去解 将问题分解成子问题,通过解决子问题,来解决原问题 贝尔曼方程是关键 问题特性 最优子结构 满足最优性原理：不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略 最优的解可以被分解成子问题的最优解 交叠式子问题 子问题能够被多次重复 子问题的解要能够被缓存并再利用 策略评价 问题 给定一个策略\\(\\pi \\),求对应的值函数\\(v_{\\pi }(s)\\)或者\\(q_{\\pi }(s,a)\\)\n方法 直接解 可以直接求得精确解 时间复杂度 迭代解 利用贝尔曼期望方程迭代求解 可以收敛到精确解 策略提升 根据现有的策略评价结果\\(v_{\\pi }(s)\\)改进策略\\(\\pi \\) 典型的如贪婪策略提升 策略迭代 流程 {策略评价(迭代k次，直到接近收敛为止) + 策略提升(提升1次)}; \u0026hellip; v1 → π1 → v2 → π2 → v3 → π3 → \u0026hellip; 终止条件 提升停止 特点 有显式的策略 迭代过程中的值函数对应了某个具体的策略 效率较低 贝尔曼期望方程 + 贪婪策略提升 值迭代 流程 策略评价(迭代1次); \u0026hellip; v1 → v2 → v3 → \u0026hellip; 特点 没有显式的策略 迭代过程中的值函数可能不对应任何策略 效率较高 贝尔曼期望方程 扩展 异步动态规划 以某种顺序单独考虑每一个状态 能够大大减少计算量 只要所有的状态都能被持续的选择到,收敛性能够保证 常用的三种形式 就地(In-Place)动态规划 优先清理 实时动态规划 就地(In-Place)动态规划 同步值迭代存储了两个版本的值函数，在计算\\(v_{new}\\)的时候，使用了\\(v_{old}\\)的复制版本，在整个更新过程中，\\(v_{old}\\)是不变的，保持上一个循环的状态。 就地(In-Place)动态规划只对一个值函数\\(v_{new}\\)进行更新，因此，从左上角开始更新和从右下角开始更新，得到的结果是不一样的。 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/dynamic-planning/","title":"Dynamic Planning"},{"content":"MatrixXf 不能直接 * 需要把MatrixXf类型的数据赋值给Matrix3d这种确定维数的数据，然后再进行操作？ SVD分解 Eigen::JacobiSVD\u0026lt;Eigen::MatrixXf\u0026gt; svd(E, Eigen::ComputeFullV | Eigen::ComputeFullU); Eigen::MatrixXf singular_values = svd.singularValues(); Eigen::MatrixXf left_singular_vectors = svd.matrixU(); Eigen::MatrixXf right_singular_vectors = svd.matrixV(); ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/eigen/","title":"Eigen"},{"content":"枚举 一种取值受限的特殊类型 无作用域枚举 enum Color { Red, // Red的作用域不在Color内，而是整个文件，因此main函数中可以直接使用“Red” Yellow }; enum Color2 { Red, // Red的作用域不在Color内，而是整个文件，因此main函数中可以直接使用“Red” Yellow }; namespace ABC { enum Color { Red, // Red的作用域不在Color内，而是ABC域内 Yellow }; } class DEF { public: enum Color { Red, // Red的作用域不在Color内，而是DEF类内 Yellow }; }; int main() { Color x = Red; Color2 x2 = Red; // 这里的Red和上面的Red产生冲突了，这就是无作用域的坏处 ABC::Color y = ABC::Red; DEF::Color z = DEF::Red; } 有作用域枚举（C++11起） enum class Color { Red, // Red的作用域在Color内 Yellow }; enum class Color2 { Red, // Red的作用域在Color2内 Yellow }; int main() { Color x = Color::Red; Color x2 = Color2::Red; // 无冲突了 } 枚举项缺省使用0初始化，依次递增，可以使用常量表达式来修改缺省值 enum class Color { Red, Yellow = 100, Green }; class A { public: enum {x = 3}; // 早期的C++代码通过这种方式在类的内部定义一个编译期常量 // constexpr static int x = 3; // 这种方式更好，但早期C++不包含constexpr，所以用上面的方法代替 }; int main() { std::cout \u0026lt;\u0026lt; Color::Green \u0026lt;\u0026lt; std::endl; // 101 } 可以为枚举指定底层类型，表明了枚举项的尺寸 enum Color { Red, Yellow = 100, Green }; enum Color2 : char { Red, Yellow = 100, Green }; int main() { std::cout \u0026lt;\u0026lt; sizeof(Color) \u0026lt;\u0026lt; std::endl; // 4(int) std::cout \u0026lt;\u0026lt; sizeof(Color2) \u0026lt;\u0026lt; std::endl; // 1 } 无作用域的枚举项可以隐式转换为整数，也可以用static_cast互相转换；有作用域的枚举项不可以 enum Color { Red, Yellow = 100, Green }; void fun(Color x) { } enum class Color2 { Red, Yellow = 100, Green }; int main() { fun(100); // 编译失败 fun(static_cast\u0026lt;Color\u0026gt;(100)); // 编译成功 std::cout \u0026lt;\u0026lt; Red \u0026lt;\u0026lt; std::endl; // 0 std::cout \u0026lt;\u0026lt; Color2::Red \u0026lt;\u0026lt; std::endl; // 编译失败 std::cout \u0026lt;\u0026lt; static_cast\u0026lt;int\u0026gt;(Color2::Red) \u0026lt;\u0026lt; std::endl; // 编译成功 } 注意区分枚举的声明和定义 enum Color; enum Color2 : int; enum class Color3; int main() { Color x; // 即使在别的文件中有定义，还是失败；因为在这个翻译单元中，编译器不知道Color的具体大小是int还是char还是其他大小，无法分配内存 Color2 x; // 可以编译，Color2的大小确定 Color3 x; // 可以编译，有作用域的枚举类型默认是int型 } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/enum/","title":"Enum"},{"content":"基础 通过关键字try/catch/throw引入异常处理机制 void f1() { int x; double y; // y先被销毁，x后被销毁 throw 1; // 此处抛出异常 std::cout \u0026lt;\u0026lt; \u0026#34;1\u0026#34; \u0026lt;\u0026lt; std::endl; } int f2() { int x; double y; // f1函数中的局部对象被销毁后，开始销毁f2中的局部对象 try { f1(); } catch(double) { // f1抛出的异常不会在这里被捕获，int和double类型不匹配 std::cout \u0026lt;\u0026lt; \u0026#34;exception catched 2: \u0026#34; \u0026lt;\u0026lt; e \u0026lt;\u0026lt; std::endl; } std::cout \u0026lt;\u0026lt; \u0026#34;other logic in f2\u0026#34; \u0026lt;\u0026lt; std::endl; // 不会执行 } int f3() { try { f2(); } catch(int e) { std::cout \u0026lt;\u0026lt; \u0026#34;exception catched 3: \u0026#34; \u0026lt;\u0026lt; e \u0026lt;\u0026lt; std::endl; } std::cout \u0026lt;\u0026lt; \u0026#34;other logic in f3\u0026#34; \u0026lt;\u0026lt; std::endl; // 会执行 } int main() { try { f3(); } catch(int) { // 异常已经被处理了，这里不会捕获到异常 std::cout \u0026lt;\u0026lt; \u0026#34;exception catched 2!\u0026#34; \u0026lt;\u0026lt; std::endl; } catch(double) { // ... } } 异常触发时的系统行为——栈展开 上面代码中的f1抛出异常后，会一层一层往上一个栈帧找catch匹配代码，找不到就抛弃对应栈帧 抛出异常后续的代码不会被执行 局部对象会按照构造相反的顺序自动销毁 系统尝试匹配相应的catch代码段 如果匹配则执行其中的逻辑，之后执行catch后续的代码 如果不匹配则继续进行栈展开，直到“跳出”main函数，触发terminate结束运行 异常对象 系统会使用抛出的异常拷贝初始化一个临时对象，称为异常对象 异常对象会在栈展开过程中被保留，并最终传递给匹配的catch语句 try/catch语句块 一个try语句块后面可以跟一个到多个catch语句块 try { f3(); } catch(int) { // ... } catch(double) { // ... } 每个catch语句块用于匹配一种类型的异常对象 catch语句块的匹配按照从上到下进行 // demo 1 void f0() { throw 1; } void f1() { try { f0(); } catch(double) { std::cout \u0026lt;\u0026lt; \u0026#34;double\u0026#34; \u0026lt;\u0026lt; std::endl; } catch(int) { std::cout \u0026lt;\u0026lt; \u0026#34;int\u0026#34; \u0026lt;\u0026lt; std::endl; // 会被该catch捕获 } } // demo 2 struct Base {}; struct Derive : Base {}; void f2() { throw Derive{}; } void f3() { try { f2(); } catch(Base\u0026amp; e) { // 异常会被该catch捕获，系统会使用Derive尝试初始化Base类型的e，发现可以初始化，就被捕获了 // 只有派生类-\u0026gt;基类、数组-\u0026gt;指针、函数-\u0026gt;指针可以被匹配，int-\u0026gt;double这种不行 std::cout \u0026lt;\u0026lt; \u0026#34;base\u0026#34; \u0026lt;\u0026lt; std::endl; } catch(Derive\u0026amp; e) { std::cout \u0026lt;\u0026lt; \u0026#34;derive\u0026#34; \u0026lt;\u0026lt; std::endl; } } 使用catch(\u0026hellip;)匹配任意异常，通常放在多个catch语句块的最后，兜底 可以在catch中调用throw抛出相同类型的异常 void f0() { throw Str{}; } void f1() { try { f0(); } catch(...) { throw; // 把捕获到的异常继续向下一层栈帧抛出 } } int main() { try { f1(); } catch(Str\u0026amp; e) { // 捕获到最初由f0抛出的异常 } } 一个异常未处理完成(未被捕获)时抛出新的异常会导致程序崩溃 不要在析构函数或者operator delete函数重载版本中抛出异常 通常来说，catch所接收的异常类型为引用类型 如果不加\u0026amp;，就是用的拷贝初始化，而拷贝初始化过程可以抛出异常，所以存在程序崩溃风险 异常与构造\u0026amp;析构函数 使用function-try-block来保护初始化逻辑 struct Str { Str() { throw 100; } }; class Cla { public: Cla() try : mem() { // 这里的\u0026#34;: mem()\u0026#34;可以删掉，编译器会隐式初始化mem，不需要用户显示指明 // init logic } catch(int) { std::cout \u0026lt;\u0026lt; \u0026#34;exception catched in Cla::Cla\u0026#34; \u0026lt;\u0026lt; std::endl; // 编译器会在这里隐式地加一句\u0026#34;throw;\u0026#34; } int xxx; private: Str mem; }; int main() { try { Cla cla; cla.xxx; } catch(int) { // 下面这一行也会执行，因为C++规定，如果是在构造函数内捕获的异常，编译器会隐式地在catch语句块最后加上\u0026#34;throw;\u0026#34;命令 // 这样做的原因是: // 如果不继续向外吐出捕获，程序就会执行到上面的\u0026#34;cla.xxx;\u0026#34;指令，由于cla的初始化并没有成功，执行这条指令的行为是未定义的。 std::cout \u0026lt;\u0026lt; \u0026#34;exception catched in main\u0026#34; \u0026lt;\u0026lt; std::endl; } } function-try-block也支持一般函数 void fun() try { throw 123; } catch(...) { } 在构造函数中抛出异常时，已经构造的成员会被销毁，但析构函数不会被调用 构造函数没执行完，有些变量还没初始化，直接调用析构函数就存在未定义行为了 对于已经构造出来的变量，如果需要手动清理的话，应该在构造函数的catch语句块中进行销毁处理 描述函数是否会抛出异常 如果函数不会抛出异常，则应表明，为系统提供更多的优化空间 C++98的方式： throw()：不会抛出异常 throw(int, char)：可能会抛出异常 C++11后的改进： noexcept：不会抛出异常 noexcept(false)：可能会抛出异常 noexcept 限定符：接受false/true表示是否会抛出异常 操作符：接受一个表达式，根据表达式是否可能抛出异常返回false/true void fun() noexcept(false) {} void fun1() noexcept(noexcept(fun())) { fun(); } int main() { std::cout \u0026lt;\u0026lt; noexcept(fun()) \u0026lt;\u0026lt; std::endl; // 0 } 在声明了noexcept的函数中抛出异常会导致terminate被调用，程序终止，这里的异常无法在外部被捕获 不作为函数重载依据，但函数指针、虚拟函数重写时要保持形式兼容 void fun() {} int main() { void (*ptr)() noexcept = fun; // 报错，fun可能会抛出异常，这里的函数指针明确了不能抛出异常，冲突了 (*ptr)(); } 标准异常 异常类型 exception:异常 runtime_error: 运行期异常 overflow_error underflow_error logic_error: 逻辑异常 invalid_argument length_error out_of_range bad_alloc bad_cast bad_type_id bad_exception 尽量使用C++提供的标准异常 #include \u0026lt;stdexcept\u0026gt; void fun() { // throw 123; throw std::runtime_error(\u0026#34;Invalid Input!\u0026#34;); } int main() { try { fun(); } catch (const std::runtime_error\u0026amp; e) { std::cout \u0026lt;\u0026lt; e.what() \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // what是exception（基类）的虚函数 } catch (const std::bad_alloc\u0026amp; e) { // 如果在这里捕获到异常，意味着发生了内存分配失败的问题 } // 之所以标准定义了这么多种类的异常，就是让你在写代码的时候一眼就可以看出来异常类型和对应的处理逻辑 // 你也可以自己定义异常 } 正确对待异常 不要滥用：异常的成本非常高 异常是用于处理程序不应该发生的逻辑，正常的跳转不要用 不要不用：对真正的异常场景，异常处理是相对高效、简洁的处理方式 编写异常安全的代码 不安全的例子 void fun() { int* ptr = new int[3]; throw std::runtime_error(\u0026#34;Invalid Input!\u0026#34;); // 栈展开了，ptr被销毁，但指向的内存没有被释放，泄露了 delete []ptr; } 需要注意的点： 避免裸的资源分配（如上代码） 内存：使用智能指针 文件：使用C++提供的fstream 接口设计 stack的pop只返回void，top返回的是内容，这样设计就是为了异常安全 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/exception/","title":"Exception"},{"content":"表达式基础 操作符 操作符优先级 操作符重载：不改变接收操作数的个数、优先级与结合性 操作数求值顺序的不确定性 void fun(int p1, int p2) { std::cout \u0026lt;\u0026lt; p1 \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; p2 \u0026lt;\u0026lt; std::endl; } int main() { int x = 0; fun(x = x + 1, x = x + 1); // 前面的x = x + 1和后面的x = x + 1谁先执行是不确定的 /*第一种可能的执行顺序 * x = x + 1 * x = x + 1 * p1 = x * p2 = x * 打印结果 2 2 */ /*第二种可能的执行顺序 * x = x + 1 * p1 = x * x = x + 1 * p2 = x * 打印结果 1 2 */ } 乱序执行的目的是为了效率 int a = 1; int b = 2; a = 3; b = 4; // 上面这段代码，编译器在编译的时候可能会把b = 4;放到a = 3;前面一行去执行，这样的话cache命中率更高，效率更高 左值\u0026amp;右值 传统的左值与右值划分 来源于C语言：左值可以放在等号左边；右值只能放在等号右边。 所有的划分都是针对表达式的，不是针对对象或者数值 cppreference链接 泛左值(glvalue): 标识一个对象、位域或函数 纯右值(prvalue): 用于初始化对象或作为操作数 int x = 3; // 这是个初始化操作，不是个表达式，这里的的3是个纯右值（用于初始化对象） x = 4; // 这是个表达式，这里的x是个泛左值，3是个纯右值 将亡值(xvalue): 标识其资源可以被重新使用 void fun(std::vector\u0026lt;int\u0026gt;\u0026amp;\u0026amp; input) {} int main() { std::vector\u0026lt;int\u0026gt; x; fun(std::move(x)); // move操作把x转换为一个将亡值 } 在C++中，左值也不一定能放在等号左边；右值也可能放在等号左边 const int x = 3; // x是个泛左值，因为它标识了一个对象（一块内存）；x不是将亡值；所以x是个左值（根据上面的图推断） x = 4; // 虽然x是左值，但是它不能放在等号左边 struct Str {}; Str() = str(); // 编译可以通过，所以右值可以放在等号左边 遍历容器时使用右值引用 std::vector\u0026lt;bool\u0026gt; vec{false, true}; for (const auto\u0026amp; x : vec) { // 编译通过 // ... } for (auto\u0026amp; x : vec) { // 编译报错，vector\u0026lt;bool\u0026gt;的内存空间为节约存储空间是按照bit存储的 // 此时vec中的元素是std::vector\u0026lt; bool\u0026gt;:reference类型，不是左值无法引用获取 // 改成auto\u0026amp;\u0026amp; x : vec是可以的，为了不失一般性，遍历vector的时候就用右值引用即可 // ... } 左值与右值的转换 左值转换为右值 int x = 3; int y = x; // 这里y需要的是右值，所以C++会把x转换为右值用于y的初始化 临时具体化 struct Str { int x; }; int main() { Str().x; // 这里我们需要把Str()从一个纯右值转换为将亡值才能拿到x } 再讨论decltype 如果实参为无括号的标识表达式或者无括号的类成员访问表达式，则decltype产生以此表达式命名的实体的类型。\nint x; decltype(x) y = x; // int y = x; 如果实参是其他类型为T的表达式\n如果表达式的值类别为亡值，decltype产生T\u0026amp;\u0026amp; 如果表达式的值类别为左值，decltype产生T\u0026amp; 如果表达式的值类别为纯右值，decltype产生T decltype(3) x; // int x; int x; decltype((x)) y = x; // int\u0026amp; y = x; // 注意，x两侧没有括号的话就是实体了，加了括号就是表达式了 decltype(std::move(x)) y = std::move(x); // int\u0026amp;\u0026amp; y = std::move(x); 类型转换 隐式类型转换 自动发生 实际上是一个（有限长度的转型序列） 显式类型转换 static_cast static意味着转换是在编译期完成的 性能好，但安全性不高 const_cast 去除常量性 dynamic_cast dynamic意味着转换是在运行期执行的，如果转换失败会返回nullptr 相比static_cast更加：安全，但性能较差 一般用于子类和父类的转换 上行转换 在继承关系中 ，dynamic_cast由继承类向基类的转换与static_cast和隐式转换一样，都是非常安全的。 下行转换 class A { virtual void f(){}; }; class B : public A{ }; void main() { A* pA = new B; B* pB = dynamic_cast\u0026lt;B*\u0026gt;(pA); } 注意类A和类B中定义了一个虚函数，这是不可缺少的。因为类中存在虚函数，说明它可能有继承类，这样才有类型转换的情况发生，由于运行时类型检查需要运行时类型信息，而这个信息存储在类的虚函数表中，只有定义了虚函数的类才有虚函数表。 reinterpret_cast 字面意思就是重新解释一块内存 一般是对指针指向的内存进行操作 int x = 3; double y = reinterpret_cast\u0026lt;doule\u0026gt;(x); // 编译不通过，不支持这种转换操作 double* y = reinterpret_cast\u0026lt;double*\u0026gt;(\u0026amp;x); // 编译通过，但每次打出来的值都没啥意义，且在变化 // 因为int是4个字节，double是8个字节 // 每次执行的时候，后4个字节都在变化 C风格转换(不建议使用) int x = 3; auto y = (double)x; 还有其他类型的转换，但是用的不多，所以不赘述了 表达式深入 逻辑与关系操作符 \u0026lt;=\u0026gt;的返回类型 strong_ordering weak_ordering partial_ordering 通常不能将多个关系操作符串联：c \u0026gt; b \u0026gt; a是不行的 不要写出val == true这样的代码 编译器会把true隐式转换为1来进行比较 位操作符 移位操作符 \u0026gt;\u0026gt;就是除以2 \u0026laquo;就是乘以2 注意整数的符号与位操作符的相关影响 integral promotion会根据整数的符号影响其结果 unsigned char x = 0xff; // 11111111 auto y = ~x; // 首先会将x整型提升为00...011111111(int型)，然后再按位取反(int)11...100000000，结果是-256 signed char x = 0xff; // 11111111，由于是signed类型，所以其第一位是符号位 // 整型提升前后数值大小不变，所以这里在进行整型提升的时候，前面就是补符号位的1了 // (signed char)11111111 = (int)11...111111111 = -1 auto y = ~x; // 按位取反(int)00...000000000，结果就是0 右移保持符号，但左移不能保证 赋值操作符 右结合 求值结果为左操作数 可以引入大括号（初始化列表）以防止收缩转换(narrowing conversion) short x; x = 0x80000000; // 编译不会报错，但是short占2个字节，赋值结果是4个字节，这里会丢掉前面两个字节的数据，结果是x = 0 x = {0x80000000}; // 加了{}后，如果编译器发现收缩转换发生了，会报错 int y = 3; short x = {y}; // 报错，编译器觉得这里有收缩转换风险，因为y的值不确定，虽然实际上没有发生收缩转换 constexpr int y = 3; short x = {y}; // 通过，编译器直接把y替换为3，然后就发现没有发生收缩转换 复合赋值操作符 int x = 2; int y = 3; x^=y^=x^=y; // x = 3; y = 2; 两者内容互换，省了一块内存，但运行效率较低 // 0: x = 2, y = 3; // 1: x^=y(从最右边算起) x = 2^3, y = 3; // 2: y^=x x = 2^3, y = 3^2^3 = 3^3^2 = 0^2 = 2 // 3: x^=y x = 2^3^2 = 3, y = 2 自增自减操作符 int x = 3; ++++x; // 合法，因为前缀++返回的是左值 (x++)++; // 不合法，后缀++返回的是右值 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/expression/","title":"Expression"},{"content":"函数基础 函数声明可以出现多次，但函数定义通常只能出现一次（inline函数可以多次） 栈帧结构(stack frame) 每次调用一个函数就会在栈上开辟一个frame frame包含了如下信息 local variables 返回地址 实参args1, args2, \u0026hellip; 当前frame执行完后返回到前一个frame的地址，遵循stack先进后出的逻辑 拷贝过程的（强制）省略 返回值优化 C++17强制省略拷贝临时对象 函数的外部链接 编译器需要为C++中的所有函数，在符号表中生成唯一的标识符，来区分不同的函数。而对于同名不同参的函数，编译器在进行name mangling操作时，会通过函数名和其参数类型生成唯一标识符，来支持函数重载。 nm exetable_file nm exetable_file | c++filt -t: demangling， 得到函数的原始命名 demo // func.h int Add(int x, int y); // func.cc int Add(int x, int y) { return x + y; } 对上面的代码进行编译，mangling后，Add函数的标识符为_Z3addii，前缀不讨论，后缀的ii表示函数的形参类型是两个int型 C++之所以能实现重载，就是因为mangling之后的函数标识符在函数名的基础上追加了形参类型的后缀，这样相同名字的函数只要形参类型不同，编译器依然可以区分开 C语言没有这样的后缀，所以C不支持函数重载 如果我们想在一个C文件里调用上面的C++代码，是无法链接到Add函数的，因为C语言想找的函数标识符就是add，没有后缀，与_Z3addii不同 可以通过extern \u0026quot;C\u0026quot;去支持C文件的调用 // func.h extern \u0026#34;C\u0026#34; int Add(int x, int y); int Add(int x); // func.cc extern \u0026#34;C\u0026#34; int Add(int x, int y) { // 标识符为add return x + y; } int Add(int x) { // 标识符为addi return x; } 这样处理后，mangling得到的标识符就变成add了，外部的C文件可以找到该函数进行调用，但同样就不支持重载了 上面的代码两个Add函数都可以被正常调用，因为第二个函数没有extern \u0026quot;C\u0026quot;修饰，还是用的C++的nm方法，两者的标识符不一样 函数重载 相同的函数名，不同的参数列表 不能基于不同的返回值进行重载 函数重载与name mangling 名称查找 限定查找(qualified lookup)与非限定查找(unqualified lookup) void fun() {} namespace MyNS { void fun() {} } int main() { ::fun(); // 限定查找，有明确的域名global MyNS::fun(); // 限定查找，有明确的域名MyNS fun(); // 非限定查找，函数前没有指定明确的域 } 非限定查找会进行域的逐级查找——名称隐藏(hiding) void fun() {} void fun1() {} namespace MyNS { void fun() {} void g() { fun(); // 会调用MyNS中的fun } int fun1 = 3; void g2() { fun1(); // 会报错，因为MyNS::fun1隐藏掉了global::fun1，而MyNS::fun1是个变量，无法执行函数调用 } } int main() { MyNS::g(); } 查找通常只会在已声明的名称集合中进行 void fun() {} namespace MyNS { void g() { fun(); // 会调用global中的fun } void fun() {} } int main() { MyNS::g(); } 实参依赖查找(Argument Dependent Lookup: ADL) 只对自定义类型生效 struct Str2 {}; namespace MyNS { struct Str {}; void g(Str x) {} void g(Str2 x) {} } int main() { Str2 obj2; g(obj2); // 编译不通过，找不到g MyNS::Str obj; g(obj); // 非限定性查找一般来说不会去考虑名字空间内部的东西，但由于这个函数的形参的类型是定义于MyNS内部的一个结构体，编译器就会把对应名字空间内的对象纳入到考虑范围之内，这里的调用就是合法的 } 重载决议 当有多个函数都符合名称查找时，需要有一系列的规则去决定到底用哪个函数 过滤不能被调用的版本 参数个数不对 无法将实参转换为形参 实参不满足形参的限制条件（一般是用到模板时会遇到，比如C++20 concept限制） 在剩余版本中查找与调用表达式最匹配的版本，匹配级别越低越好（有特殊规则） 级别1：完美匹配 或 平凡转换（比如加一个const） 级别2：promotion或promotion+平凡转换 级别3：标准转换或标准转换+平凡转换 级别4（非C++标准引入）：自定义转换或自定义转换+平凡转换或自定义转换+标准转换 级别5（非C++标准引入）：调用形参为省略号的版本 void fun(...) {} // 可以传入任意参数 函数包含多个形参时，所选函数的所有形参的匹配级别都要优于或等于其他函数 特殊规则 void fun(int\u0026amp; x) {} void fun(const int\u0026amp; x) {} int main() { int x; fun(x); // 上面两个函数都属于级别1的匹配，但编译通过，没有发生冲突 // 规则是：x为左值，int\u0026amp;优于const int\u0026amp; } 内联函数 满足翻译单元的一处原则（为了在编译期展开），普通函数需要满足程序的一处原则，所以inline函数可以定义在header中 constexpr/consteval函数 满足翻译单元的一处原则（为了在编译期就可以计算） constexpr函数可以在编译期执行也可以在运行期执行（since C++11） constexpr int fun(int x) { return x + 1; } int fun2(int x) { return x + 1; } int main() { int y; cin \u0026gt;\u0026gt; y; constexpr int x = fun(y); // 运行期执行 constexpr int x2 = fun(3); // 编译期执行 constexpr int x3 = fun2(3); // 编译失败，因为fun不是常量函数，无法赋值给常量 return 0; } consteval函数只能在编译期执行（since C++20） consteval int fun(int x) { return x + 1; } int main() { int y; cin \u0026gt;\u0026gt; y; constexpr int x = fun(y); // 编译失败，无法在编译期执行 constexpr int x2 = fun(3); // 编译期执行 return 0; } 函数指针 函数类型 using K = int(int); // K就是一个函数类型的别名 K fun; // 函数声明，等同于int fun(int); std::function\u0026lt;void(int)\u0026gt; f; // 这里的模板参数就是函数类型 函数指针类型 int inc(int x) { return x + 1; } int dec(int x) { return x - 1; } using K = int(int); void Demo(K in) {} void Demo1(K* in) {} int main() { K* fun = \u0026amp;inc; (*fun)(100); // 101 auto fun2 = dec; // 等同于：using F = int(*)(int); F fun2 = dec; // 下面四种写法效果一样 Demo(inc); Demo(\u0026amp;inc); Demo1(inc); Demo1(\u0026amp;inc); } 函数指针重载 void fun(int) {} void fun(int, int) {} int main() { auto f = fun; // 报错，编译器不知道指向上面哪个fun函数 void(*f)(int) = fun; // 通过，类型明确 } 将函数指针作为函数参数 将函数指针作为函数的返回值 int inc(int x) { return x + 1; } int dec(int x) { return x - 1; } auto fun(bool input) { if (input) { return inc; } else { return dec; } } int main() { (*fun(true))(100); // 101 } 注意most vexing parse问题 该问题促使C++11引入T xx{};的初始化方式 使用{}而非()去初始化一个变量可以避免该问题的发生 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/function/","title":"Function"},{"content":"元编程的引入 从泛型编程到元编程 泛型编程：一套代码处理不同类型 对于一些特殊的类型需要引入额外的处理逻辑——引入操纵程序的程序 vector\u0026lt;bool\u0026gt;，bool只需要1个bit就可以表示，但是计算机中最小的单位是1个byte，因此，vector对bool类型做了特化 元编程与编译期计算 很多语言都有元编程 C++的元编程本质上就是编译期计算 第一个元编程示例 Erwin Unruh, 1994 这段代码能够在编译错误中产生质数，也就意味着编译的时候，就有计算被引入其中了，拉开了元编程的序幕 使用编译期运算辅助运行期计算 不是简单的将整个计算一分为二 需要详细设计一下，哪些内容可以放到编译期，哪些内容放到运行期 如果某种信息需要在运行期确定，那么通常无法利用编译期计算 元编程的编写 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/meta-programming/fundamental/","title":"Fundamentals of meta-programming"},{"content":"泛型算法简介 我们把一些常用的简单的算法抽象出来，然后用不同的迭代器去调用这些函数，这些算法被称为泛型算法 C++标准库中的泛型算法：algorithm, numeric, ranges 泛型算法的实现都不复杂，但优化足够好 一些泛型算法与方法同名，实现功能类似，此时建议调用方法而非算法 std::find VS. std::map::find 方法会根据对应数据结构的特性做优化，而泛型算法为了确保通用性，不会做特定优化 泛型算法分类 读算法：给定迭代区间，读取其中的元素并进行计算 accumulate/find/count 写算法：向迭代器中写入元素 单纯写：fill/fill_n 读+写：transform/copy 注意：写算法一定要保证目标区间足够大 排序算法：改变输入序列中元素的顺序 sort/unique 注意unique的用法，只把原序列中连在一起的相同元素合并为1个 迭代器的分类 泛型算法使用迭代器实现元素访问 常见迭代器 输入迭代器：可读，可递增，典型应用是find算法 输出迭代器：可写，可递增，典型应用是copy算法 前向迭代器：可读写，可递增，典型应用是replace算法 双向迭代器：可读写，可递增递减，典型应用是reverse算法 随机访问迭代器：可读写，可增减一个整数，典型应用是sort算法 一些算法会根据迭代器的类别不同引入相应的优化 distance算法 如果输入的是输入迭代器，就只能通过++的方式计算两个迭代器之间的距离 如果输入的是随机访问迭代器，就可以直接用last_it - begin_it得到距离 特殊的迭代器 插入迭代器：back_insert_iterator(back_inserter), front_insert_iterator(front_inseter), insert_iterator(inserter) std::vector\u0026lt;int\u0026gt; x; std::fill_n(x.begin(), 10, 3); // core，因为x的内存比10小 std::fill_n(std::back_insert_iterator\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt;(x), 10, 3); // 正确，因为back_insert_iterator把\u0026#34;=\u0026#34;操作重载为push_back了，所以这里能正确往x中填充10个3 std::fill_n(std::back_inserter(x), 10, 3); // 简化版本 for (const auto i : x) { std::cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl; 流迭代器 istream_iterator std::istringstream str(\u0026#34;1 2 3 4 5\u0026#34;); std::istream_iterator\u0026lt;int\u0026gt; x(str); std::cout \u0026lt;\u0026lt; *x \u0026lt;\u0026lt; std::endl; // 1 ++x; std::cout \u0026lt;\u0026lt; *x \u0026lt;\u0026lt; std::endl; // 2 std::istream_iterator\u0026lt;int\u0026gt; y{}; // 这一行不是类对象的定义，而是函数声明 // std::istream_iterator\u0026lt;int\u0026gt; y; // 这么写也可以 for (; x != y; ++x) { std::cout \u0026lt;\u0026lt; *x \u0026lt;\u0026lt; std::endl; // 1 2 3 4 5 } std::accumulate(x, y, 0); // 15 ostream_iterator std::vector\u0026lt;int\u0026gt; x{1, 2, 3}; std::copy(x.rbegin(), y.rbegin(), std::ostream_iterator\u0026lt;int\u0026gt;(std::cout, \u0026#34; \u0026#34;)); 反向迭代器 移动迭代器 move_iterator：功能类似于move操作，执行后原先内存的内容就没了 迭代器与哨兵(Sentinel) 哨兵一般指标识迭代器结束的标志，比如vector.end(), 以及上面的std::istream_iterator\u0026lt;int\u0026gt; y{}，都是哨兵 哨兵在ranges算法中是个非常重要的概念 并发算法(C++17/20) 有些算法可以通过指定执行policy来加速，下面是一些policy的举例 std::execution::seq：顺序执行 std::execution::par：并发执行 std::execution::par_unseq：并发非顺序执行 std::execution::unseq：非顺序执行 代码demo(注意：下面的代码可能需要在编译时加上-o3和-ltbb库的优化才能体现出加速效果) #include \u0026lt;iostream\u0026gt; #include \u0026lt;algorithm\u0026gt; #include \u0026lt;chrono\u0026gt; #include \u0026lt;random\u0026gt; #include \u0026lt;ratio\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;execution\u0026gt; int main() { std::random_device rd; std::vector\u0026lt;double\u0026gt; vals(100000000); for (auto\u0026amp; d : vals) { d = static_cast\u0026lt;int\u0026gt;(rd()); } for (int i = 0; i \u0026lt; 5; ++i) { using namespace std::chrono; std::vector\u0026lt;double\u0026gt; sorted(vals); const auto startTime = high_resolution_clock::now(); std::sort(std::execution::par, sorted.begin(), sorted.end()); // 并发执行，加速 std::sort(std::execution::unseq, sorted.begin(), sorted.end()); // 顺序执行，不加速 const auto endTime = high_resolution_clock::now(); std::cout \u0026lt;\u0026lt; \u0026#34;Latency: \u0026#34; \u0026lt;\u0026lt; duration_cast\u0026lt;duration\u0026lt;double, std::milli\u0026gt;\u0026gt;(endTime - startTime).count() \u0026lt;\u0026lt; std::endl; } return 1; } 泛型算法的改进\u0026ndash;ranges(C++20) ranges可以视为C++标准模块库的2.0版本 可以使用容器而非迭代器做为输入 int main() { std::vector\u0026lt;int\u0026gt; x{1, 2, 3, 4, 5}; auto it = std::find(x.begin(), x.end(), 3); it = std::ranges::find(x.begin(), x.end(), 3); it = std::ranges::find(x, 3); std::cout \u0026lt;\u0026lt; *it \u0026lt;\u0026lt; std::endl; } 通过std::ranges::dangling避免返回无效的迭代器 引入映射概念，简化代码编写 std::map\u0026lt;int, int\u0026gt; m{{2, 3}}; auto it = std::ranges::find(m.begin(), m.end(), 3, \u0026amp;std::pair\u0026lt;const int, int\u0026gt;::second); // projectionii，本质上是个指针，指向了pair的第二个元素 std::cout \u0026lt;\u0026lt; it-\u0026gt;first \u0026lt;\u0026lt; std::endl; 引入view，灵活组织程序逻辑 通过 | 这样的符号串联起来组成更加复杂的语句，这个在container部分有讲 从类型上区分迭代器与哨兵 Demo sort排序\nvector\u0026lt;Struct\u0026gt; A bool f1 (Struct a,Struct b) { return (a.x\u0026gt;b.x); } bool f2 (Struct a,Struct b) { return (a.x\u0026lt;b.x); } sort(A.begin(), A.end(), f1);//降序排列 sort(A.begin(), A.end(), f2);//升序排列 sort(A.begin(), A.end(), \\[\\](const struct\u0026amp; A, const struct\u0026amp; B){return A.a \u0026lt; B.b;}) //cmp函数需要输入额外变量时 bool f3(Struct a, Struct b, double c){ if(a.y - b.y \u0026gt; c){ return a.x \u0026lt; b.x; }else{ return a.x \u0026gt; b.x; } } double c = 10; sort(A.begin(), A.end(), std::bind(f3, std::placeholders::_1, std::placeholders::_2, c)); nth_element函数\nvector\u0026lt;int\u0026gt; pts; //只保证pts[6]是排名第6的元素,同时pts[0-5]\u0026lt;pts[6],pts[6-end]\u0026gt;pts[6] nth_element(pts.begin(), pts.begin()+6; pts.end()) compare(Ponit2d* a, Ponit2d* b) { return(a-\u0026gt;x \u0026lt; b-\u0026gt;x); } vector\u0026lt;point2d\u0026gt; pts; nth_element(pts.begin(), pts.begin()+6; pts.end(), compare); lower_bound\nauto compare_s = [](const std::pair\u0026lt;double, double\u0026gt;\u0026amp; point, const double s) { return point.first \u0026lt; s; }; vector\u0026lt;pair\u0026lt;double, double\u0026gt;\u0026gt; var; auto it_lower = std::lower_bound(var.begin(), var.end(), s, compare_s); for_each\nvoid add(int\u0026amp; lhs) { lhs= lhs + 1; } for_each(intVector.begin(),intVector.end(),add); void add(int\u0026amp; lhs,int rhs) { lhs= lhs + rhs; } for_each(intVector.begin(),intVector.end(),boost::bind(add,_1,100)); ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/generic-algorithm/","title":"Generic Algorithm"},{"content":"类的继承 通过类的继承（派生）来引入“是一个”的关系 通常采用public继承 class缺省情况下采用的是private继承 struct缺省情况下采用的是public继承 继承部分不是类的声明，声明的时候直接\u0026quot;class xxx;\u0026ldquo;即可，不可以在声明的时候带上\u0026rdquo;: public base\u0026quot; 使用基类的指针或引用可以指向派生类对象 struct Base {}; struct Derived : public Base {}; int main () { Derived d; Base\u0026amp; ref = d; Base* ptr = \u0026amp;d; } 静态类型 v.s. 动态类型 静态类型是编译期确定的变量类型，比如上面的ref静态类型是Base\u0026amp;，ptr静态类型是Base* 动态类型是运行期变量实际被赋予的类型，比如上面的ref动态类型是Derived\u0026amp;，ptr静态类型是Derived* 静态类型是给编译器看的，所以变量只能调用静态类型所拥有的成员变量或成员函数 protected限定符：派生类可访问，外部不可访问 类的派生会形成嵌套域 派生类所在域位于基类内部 派生类中的名称定义会覆盖基类 使用域操作符显示访问基类成员 struct Base { int val = 2; }; class Derived : public Base { public: void fun() { std::cout \u0026lt;\u0026lt; val \u0026lt;\u0026lt; std::endl; // 3, val的值被Derived中的val覆盖 std::cout \u0026lt;\u0026lt; Base::val \u0026lt;\u0026lt; std::endl; // 2, 限定了Base域之后，就会打印Base中val的值 } int val = 3; }; int main() { Derived d; d.fun(); } 在派生类中调用基类的构造函数 struct Base { Base(int) {} }; class Derived : public Base { Derived(int a) : Base(a) {} // 必须要在初始化列表中调用Base的初始化函数，如果放在后面的函数体中是无法通过编译的 } int main() { Derived d(3); } 虚函数 通过虚函数与引用（指针）实现动态绑定 使用关键字virtual引入 非静态、非构造函数可以声明为虚函数 虚函数会引入vtable结构 dynamic_cast struct Base { virtual void baseMethod() {} int baseMember; }; class myClassDerived : public Base { virtual void derivedMethod() {} int derivedMember; }; class myClassDerived2 : public myClassDerived { virtual void derivedMethod2() {} int derivedMember2; }; int main() { myClassDerived2 d; // 从派生类转换到基类很自然 Base* ptr = \u0026amp;d; Base\u0026amp; ref = d; // 从基类转换为派生类可能存在风险，安全的做法是使用dynamic_cast // 下面这段代码之所以可以编译并运行就是因为vtable里包含了typeinfo of myClassDerived2 myClassDerived2\u0026amp; ref2 = dynamic_cast\u0026lt;myClassDerived2\u0026amp;\u0026gt;(ref); // 如果ref的typeinfo中记录的确实是myClassDerived2类型的数据，转换就成功，否则这里会抛出异常 myClassDerived2* ptr2 = dynamic_cast\u0026lt;myClassDerived2*\u0026gt;(ptr); // 如果ptr的typeinfo中记录的确实是myClassDerived2类型的数据，转换就成功，否则这里会返回空指针 // 如果把上面的Base class中的虚函数注释掉，编译就会报错，因为vtable没了，typeinfo也就没了 // 如果把ptr的类型从Base*变成myClassDerived*，那就要保证myClassDerived的虚函数存在，Base中的虚函数则无关紧要 // 换句话说，从基类A转换到派生类B，需要保证A中存在虚函数 // 注意:下面的转换也是可以的，虽然ref和ptr指向的数据的类型是myClassDerived2，但是myClassDerived2是继承自myClassDerived，所以可以转换 myClassDerived\u0026amp; ref3 = dynamic_cast\u0026lt;myClassDerived\u0026amp;\u0026gt;(ref); myClassDerived* ptr3 = dynamic_cast\u0026lt;myClassDerived*\u0026gt;(ptr); // 从上面的例子可以看出，dynamic_cast的使用会占用很多动态过程的运算资源，所以在追求性能的场景下慎用 } 虚函数在基类中的定义 引入缺省逻辑 struct Base { virtual void fun() { std::cout \u0026lt;\u0026lt; Base::fun \u0026lt;\u0026lt; std::endl; } }; class Derived : public Base { void fun() { std::cout \u0026lt;\u0026lt; Derived::fun \u0026lt;\u0026lt; std::endl; } } int main() { Derived d; d.fun(); // Derived::fun Base\u0026amp; b = d; b.fun(); // Derived::fun // 虽然b的静态类型是Base\u0026amp;，但是b引用(指针)指向的数据内存里的vtable中的fun函数指针指向的是Derived::fun // 但是如果把上面的Base中的virtual关键字去掉，上面还是会打印Derived::fun（因为覆盖作用依然存在）；但是这里就会打印Base::fun，因为vtable不存在了，所有的函数调用会在编译期就被固定下来，而编译器只看静态类型（b的静态类型是Base\u0026amp;），所以b.fun就只会绑定到Base::fun } 虚函数的意义就在于我们可以通过同一个Base的接口，通过传入不同派生类的对象，实现不同的代码逻辑，也就是动态多态（区别于静态多态） 可以通过=0声明纯虚函数，相应地构造抽象基类 虚函数在派生类中的重写 函数签名保持不变（唯一可以变的是：返回类型可以是原始返回指针/引用类型的派生指针/引用类型） struct Base {}; struct Derived : public Base {}; struct Base2 { virtual Base\u0026amp; fun() { static Base b; return b; } }; struct Derived2 : public Base2 { Derived\u0026amp; fun() { // 通过，因为Derived继承自Base static Derived inst; return inst; } }; 纯虚函数可以被定义，且可以在派生类中调用基类的纯虚函数。 struct Base { virtual void fun() = 0; } void Base::fun() { std::cout \u0026lt;\u0026lt; \u0026#34;Base::fun\u0026#34; \u0026lt;\u0026lt; std::endl; // 如果基类过于抽象，导致某些函数无法被完整定义，就可以声明为纯虚函数，强制派生类去重写。但是基类的纯虚函数仍然可以被定义，实现一些通用的预处理逻辑，在派生类中被调用。 } struct Derived : public Base { void fun() { Base::fun(); // 调用基类的纯虚函数 std::cout \u0026lt;\u0026lt; \u0026#34;Derived::fun\u0026#34; \u0026lt;\u0026lt; std::endl; }; }; 虚函数特性保持不变 Base -\u0026gt; Derived -\u0026gt; Derived2，嵌套继承，Base中的虚函数fun在Derived中被重写后，依然是虚函数，所以Derived2再次重写fun后，还是会保留虚函数的特性 override关键字 让编译器检查是否基类中的虚函数确实被正确重写了 由虚函数所引入的动态绑定属于运行期行为，与编译期行为有所区别 虚函数的缺省实参只会考虑静态类型 struct Base { virtual void fun(int x = 3) { std::cout \u0026lt;\u0026lt; \u0026#34;Base: \u0026#34; \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; } }; struct Derived : public Base { void fun(int x = 4) override final { // 这里final的含义是，后面继承自Derived的所有派生类都不会再重写fun函数 std::cout \u0026lt;\u0026lt; \u0026#34;Derived: \u0026#34; \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; } }; struct Derived2 final : public Base {}; // 这里final的含义是，Derived2不会有派生类了 void Proc1(Base\u0026amp; ref) { ref.fun(); } void Proc2(Base b) { b.fun(); } int main() { Derived d; Proc1(d); // Derived: 3 // 由于编译器看到的ref是Base\u0026amp;类型，所以这里的ref.fun()会被翻译成ref.fun(3)，但是运行期调用的是Derived中的fun函数，所以才会出现上面的结果 Proc2(d); // Base: 3 // 由于b不是d的引用或指针，而是由d构造出来的Base类型的数据，所以只会调用Base的成员函数 } 虚函数的调用成本高于非虚函数 C++缺省情况下不会把函数声明为虚函数（为了性能），Java所有的函数都是虚函数 final关键字(见上面的代码) 要使用指针（或引用）引入动态绑定，比较上面代码中的Proc1和Proc2 在构造函数中调用虚函数要小心，在基类中调用虚函数，这个函数不是派生类的实现 struct Base { Base() { fun(); } virtual void fun() { std::cout \u0026lt;\u0026lt; \u0026#34;Base\u0026#34; \u0026lt;\u0026lt; std::endl; } }; struct Derived : public Base { void fun() override { std::cout \u0026lt;\u0026lt; \u0026#34;Derived\u0026#34; \u0026lt;\u0026lt; std::endl; } }; int main() { Derived d; // Base // 构造Derived的第一步是构造Base，而Base的构造函数中调用了fun，由于此时Base已经构造完成，Derived还没有构造完成，所以这里的fun是Base中的实现 } 派生类的析构函数会隐式调用基类的析构函数 通常来说要将基类的析构函数声明为virtual的 struct Base { ~Base() { std::cout \u0026lt;\u0026lt; \u0026#34;Base\u0026#34; \u0026lt;\u0026lt; std::endl; } }; struct Derived final : Base { ~Derived() { std::cout \u0026lt;\u0026lt; \u0026#34;Derived\u0026#34; \u0026lt;\u0026lt; std::endl; } }; int main() { Derived* d = new Derived(); Base* b = d; delete b; // 只会打印\u0026#34;Base\u0026#34; // 因为Base的析构函数不是虚函数，所以delete b这行代码在编译期就会被确定为调用Base的析构函数 // 但是这样就出问题了，因为Derived的析构函数没有被正常调用 // 解决方案就是将Base的析构函数定义为虚函数: virtual ~Base() {} // 但是并非任何情况下都需要将基类的析构函数定义为虚函数，比如下面这样不使用基类指针就行，只不过在大部分时候，我们之所以使用类的继承就是为了用基类的指针去挂载派生类的对象 delete d; // 会打印\u0026#34;Derived\u0026#34; \u0026#34;Base\u0026#34; } 在派生类中我们可以修改虚函数的访问权限 struct Base { protected: virtual void fun() {} }; struct Derived : Base { public: void fun() override {} }; int main() { Derived d; d.fun(); // 可以通过编译，Derived中的fun被重写为public函数了 Base\u0026amp; b = d; b.fun(); // 编译器只知道b是Base\u0026amp;类型，但fun在Base中是protected函数，当然无法通过编译 } 继承与特殊成员 派生类的系统自动合成的…… 缺省构造函数会隐式调用基类的缺省构造函数 拷贝构造函数会隐式调用基类的拷贝构造函数 赋值函数将隐式调用基类的赋值函数 派生类的析构函数会调用基类的析构函数 派生类的其他构造函数将隐式调用基类的缺省构造函数 比如说，如果派生类的拷贝构造函数不是default(系统自动合成)的，而是有显示定义的，那在调用派生类的拷贝构造函数时，不会隐式调用基类的拷贝构造函数，而是调用的缺省构造函数 所有的特殊成员函数在显式定义时都可能需要显示调用基类相关成员 原因就是上一条描述的内容，调用方式类似于这样：Derived(const Derived\u0026amp; input) : Base(input) {} 如果没有显示地调用\u0026quot;: Base(input)\u0026quot;，系统默认会调用Base的缺省构造函数，而非这里的拷贝构造函数 构造与销毁顺序 基类的构造函数会先调用，之后才涉及到派生类中数据成员的构造 派生类中的数据成员会被先销毁，之后才涉及到基类的析构函数调用 补充知识 public，protected与private继承 struct Base { public: // 基类、派生类、类外都可以访问 int x; private: // 基类可访问 int y; protected: // 基类、派生类可访问 int z; }; // 无论Derived采用什么继承方式(public, private, protected)，上面的三种访问权限都是不变的。 // 区别在如下几个点： // 1.public继承，Base中的public，protected和private属性在Derived中都保持不变，比如在Derived中x还是public，y还是private，z还是protected。 // 2.protected继承，Base中的public，protected在Derived中都变成protected，private属性则保持不变。 // 3.private继承，Base中的public，protected和private属性在Derived中都变成private。 struct Derived : public Base {}; public继承：描述“是一个”的关系 private继承：描述“根据基类实现出“的关系，但是有更好的实现方式，就是把Base对象创建为Derived的一个私有成员变量，所以private继承很少用 protected继承：几乎不会用 using与继承 使用using改变基类成员的访问权限 struct Base { public: int x; private: int y; protected: int z; void fun() {} }; struct Derived : public Base { public: using Base::z; using Base::fun; // 所有的fun函数都会变成public，如果fun有重载的话，会作用到所有的fun上 private: using Base::x; }; int main() { Derived d; d.z; // 有了上面“的using Base::z;”，这里就可以正常调用了 d.x; // 有了上面“的using Base::x;”，这里就会报错 d.fun(); // 可以正常调用 } 注意以下两点！！ 要想通过using改变权限，首先该成员要对派生类可见，比如上面如果想把Base::y改成public或者protected就不行，因为对Derived来说，Base::y就不可见 无法改变构造函数的访问权限 使用using继承基类的构造函数逻辑 Base中有多种自定义的构造函数(系统无法自动合成的)，且Derived和Base的数据成员和构造函数的实现逻辑又是一样的，如果再实现一遍就会很耗时，这时候就可以用using来把Base中的实现都复制过来 using与部分重写 struct Base { protected: virtual void fun() { std::cout \u0026lt;\u0026lt; \u0026#34;1\\n\u0026#34;; } virtual void fun(int) { std::cout \u0026lt;\u0026lt; \u0026#34;2\\n\u0026#34;; } }; struct Derived : public Base { public: using Base::fun; void fun(int) override { std::cout \u0026lt;\u0026lt; \u0026#34;3\\n\u0026#34;; } }; int main() { Derived d; d.fun(); // 1 d.fun(3); // 3 } 继承与友元 友元关系无法继承，但基类的友元可以访问派生类中的基类的相关成员 struct Derived; // 声明，否则下面编译不通过 struct Base { friend void fun(const Derived\u0026amp;); protected: int x = 10; }; struct Derived : public Base { private: int y = 20; }; void fun(const Derived\u0026amp; val) { std::cout \u0026lt;\u0026lt; val.x \u0026lt;\u0026lt; std::endl; // 通过, 基类的友元可以访问派生类中的基类的相关成员 std::cout \u0026lt;\u0026lt; val.y \u0026lt;\u0026lt; std::endl; // 不通过，y不属于Base } 派生类中的友元无法获得基类中成员的访问权限 struct Derived; // 声明，否则下面编译不通过 struct Base { protected: int x = 10; }; struct Derived : public Base { friend void fun(const Derived\u0026amp;); friend void fun(const Base\u0026amp;); private: int y = 20; }; void fun(const Base\u0026amp; val) { std::cout \u0026lt;\u0026lt; val.x \u0026lt;\u0026lt; std::endl; // 不通过, 派生类的友元不可以访问基类中的成员 } void fun(const Derived\u0026amp; val) { std::cout \u0026lt;\u0026lt; val.x \u0026lt;\u0026lt; std::endl; // 不通过, 派生类的友元不可以访问基类中的成员 std::cout \u0026lt;\u0026lt; val.y \u0026lt;\u0026lt; std::endl; // 通过 } // 如果上面的操作允许的话，所谓的protected和private的访问权限就形同虚设了，我只要搞个派生类，定义一个友元，就可以无限制地访问基类中所有成员，显然不合理 通过基类指针实现在容器中保存不同类型的对象 struct Base { virtual double GetValue() = 0; virtual ~Base() = default; }; struct Derived : public Base { Derive(int x) : val(x) {} double GetValue() override { return val; } private: int val; }; struct Derived2 : public Base { Derive2(double x) : val(x) {} double GetValue() override { return val; } private: double val; }; int main() { std::vector\u0026lt;std::shared_ptr\u0026lt;Base\u0026gt;\u0026gt; vec; vec.emplace_back(new Derived(1)); vec.emplace_back(new Derived2(3.14)); } 多重继承 Derived继承自Base1和Base2，我们就可以使用Base1或者Base2的指针来保存Derived对象 虚继承 错误示例 struct Base { virtual ~Base() = default; int x; }; struct Base1 : Base { virtual ~Base1() = default; }; struct Base2 : Base { virtual ~Base2() = default; }; struct Derived : public Base1, public Base2 {}; int main() { Derived d; std::cout \u0026lt;\u0026lt; \u0026amp;(d.Base1::x) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; std::cout \u0026lt;\u0026lt; \u0026amp;(d.Base2::x) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 通过，且地址一样的 d.x; // 报错，不知道是Base1中的x还是Base2中的x } 正确示例 struct Base { virtual ~Base() = default; int x; }; struct Base1 : virtual Base { virtual ~Base1() = default; }; struct Base2 : virtual Base { virtual ~Base2() = default; }; struct Derived : public Base1, public Base2 {}; int main() { Derived d; d.x; // 正确，使用了虚继承 std::cout \u0026lt;\u0026lt; \u0026amp;(d.Base1::x) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; std::cout \u0026lt;\u0026lt; \u0026amp;(d.Base2::x) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; std::cout \u0026lt;\u0026lt; \u0026amp;(d.x) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 正确，使用了虚继承 // 上面三个地址的输出都是一样的 } 空基类优化与[[no unique address]]属性 未优化代码 struct Base { void fun() {} // 成员函数不会占用类的大小 }; // 类所占内存的大小是由成员变量（静态变量除外）决定的，虚函数指针和虚基类指针也属于数据部分，成员函数是不计算在内的。 // 在编译器处理后，成员变量和成员函数是分离的。成员函数还是以一般的函数一样的存在。 // a.fun()是通过fun(a.this)来调用的。所谓成员函数只是在名义上是类里的。 struct Derived { int x; Base b; // 为了调用Base中的一些函数，把Base对象声明为了成员变量 }; int main() { std::cout \u0026lt;\u0026lt; sizeof(Base) \u0026lt;\u0026lt; std::endl; // 1，没有fun函数这里还是1 std::cout \u0026lt;\u0026lt; sizeof(Derived) \u0026lt;\u0026lt; std::endl; // 8，本来应该是5，编译器会做padding } 优化后代码 struct Base { void fun() {} }; struct Derived : Base { int x; }; int main() { std::cout \u0026lt;\u0026lt; sizeof(Base) \u0026lt;\u0026lt; std::endl; // 1 std::cout \u0026lt;\u0026lt; sizeof(Derived) \u0026lt;\u0026lt; std::endl; // 4，空基类优化，如果基类中不包含任何数据成员，占用的内存会被省去 } 上面的优化代码仍然不够好，因为Derived并不是真的想继承Base（Derived不是一个Base），只是想用Base中的方法。虽然代码功能实现了，但是表达的内容不是那么精确，所以C++20引入了[[no_unique_address]] struct Base { void fun() {} // 成员函数不会占用类的大小 }; struct Derived { int x; [[no_unique_address]] Base b; // 为了调用Base中的一些函数，把Base对象声明为了成员变量 }; int main() { std::cout \u0026lt;\u0026lt; sizeof(Base) \u0026lt;\u0026lt; std::endl; // 1，没有fun函数这里还是1 std::cout \u0026lt;\u0026lt; sizeof(Derived) \u0026lt;\u0026lt; std::endl; // 4 } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/class/inheritance/","title":"Inheritance of Class"},{"content":"基础知识 主要处理两个问题 表示形式的变化：使用格式化/解析在数据的内部表示与字符序列间转换 与外部设备的通信：针对不同的外部设备（终端、文件、内存）引入不同的处理逻辑 所涉及到的操作 第1步：格式化 / 解析 第2步：缓存 累积到一定数量再输出，提高程序性能 第3步：编码转换 第4步：传输 采用模板来封装字符特性，采用继承来封装设备特性 常用的类型实际上是类模板实例化的结果 输入与输出 分为格式化与非格式化两类 非格式化I/O：不涉及数据表示形式的变化 常用输入函数：get / read / getline / gcount 常用输出函数：put / write 用的比较少，大部分情况下我们都是输入或者输出人能看得懂的数据 格式化I/O：使用移位操作符来进行的输入(\u0026raquo;)与输出(\u0026laquo;) C++通过操作符重载以支持内建数据类型的格式化I/O 可以通过重载操作符以支持自定义类型的格式化I/O 格式控制 可接受位掩码类型(showpos)、字符类型(fill)与取值相对随意(width)的格式化参数 注意width方法的特殊性：触发后被重置 char a = \u0026#39;0\u0026#39;; int x = static_cast\u0026lt;char\u0026gt;(a); std::cout.setf(std::ios_base::showpos); // 显示正负号 std::cout.width(10); // 打印的内容占10个字符 std::cout.fill(\u0026#39;.\u0026#39;); // 空白处填上\u0026#39;.\u0026#39; std::cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; std::endl; // 打印出“.........0”，字符没有正负，所以这里没有显示正负号 std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; // 打印出“+48”，width被重置了 std::cout.width(10); // 打印的内容占10个字符 std::cout \u0026lt;\u0026lt; -x \u0026lt;\u0026lt; std::endl; // 打印出“.......-48” 操纵符 简化格式化参数的设置 触发实际的插入与提取操作 char a = \u0026#39;0\u0026#39;; int x = static_cast\u0026lt;char\u0026gt;(a); std::cout \u0026lt;\u0026lt; std::showposi \u0026lt;\u0026lt; std::setw(10) \u0026lt;\u0026lt; std::setfill(\u0026#39;.\u0026#39;) \u0026lt;\u0026lt; a \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34; \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; 提取会放松对格式的限制 比如cin输入+0010，它还是能解析为10。 提取C风格字符串时要小心内存越界 char x[5] = {}; std::cin \u0026gt;\u0026gt; x; // 输入“abcdefg”，程序会崩溃，如果x是std::string类型，就没有这个问题 std::cin \u0026gt;\u0026gt; std::setw(5) \u0026gt;\u0026gt; x; // 合法，由于最后一个字符要写\u0026#39;\\0\u0026#39;，所以这里会读(5-1)个字符进入x，就不会越界了 std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; 文件与内存操作 文件操作 basic_ifstream / basic_ofstream / basic_fstream\n文件流可以处于打开/关闭两种状态，处于打开状态时无法再次打开，只有打开时才能I/O\n// 打开和关闭本质就是是否和一个文件产生了关联 // 自动open，常用 std::ifstream inFile(\u0026#34;file_name\u0026#34;); std::cout \u0026lt;\u0026lt; inFile.is_open() \u0026lt;\u0026lt; std::endl; // 如果file_name存在，就打印“1”，否则打印“0” // 手动open，不常用 std::ifstream inFile2; std::cout \u0026lt;\u0026lt; inFile.is_open() \u0026lt;\u0026lt; std::endl; // \u0026#34;0\u0026#34; inFile2.open(\u0026#34;file_name\u0026#34;); std::cout \u0026lt;\u0026lt; inFile2.is_open() \u0026lt;\u0026lt; std::endl; // 如果file_name存在，就打印“1”，否则打印“0” // 手动关闭 inFile2.close(); std::ofstream outFile(\u0026#34;file_name\u0026#34;); outFile \u0026lt;\u0026lt; \u0026#34;hello\\n\u0026#34;; outFile.close(); // 除了断开和文件的关联之外，还会把缓存区中剩余的内容传输出去 // 当outFile对象被销毁的时候，会隐式调用close方法，确保缓存区的内容被传输出去，否则这部分内容就丢失了，所以上面一行代码也可以不要 文件流的打开模式\n标记名 作用 in 打开以供读取 out 打开以供写入 ate 表示起始位置位于文件末尾 app 附加文件，即总是向文件尾写入 trunc 截断文件，即删除文件中的内容 binary 二进制模式 每种文件流都有缺省的打开方式 // ifstream的缺省打开方式是ios_base::in // ofstream的缺省打开方式是ios_base::out | ios_base::trunc，trunc会导致在向文件写入的时候，文件里已有的内容会被删除; 可以设定为ios_base::out | ios_base::app来实现追加 // fstream的缺省打开方式是ios_base::in | ios_base::out std::ifstream inFile(\u0026#34;file_name\u0026#34;, std::ios_base::in); // 这里的ios_base::in也可以不加，因为ifstream对象的缺省打开方式就是这个 std::ifstream inFile(\u0026#34;file_name\u0026#34;, std::ios_base::in | std::ios_base::ate); // 从文件末尾开始读取 注意ate和app的异同 // 下面这种写法还是会清空文件里已有的内容 std::ofstream outFile(\u0026#34;filename\u0026#34;, std::ios_base::out | std::ios_base::ate); // 下面这种写法可以追加内容 std::ofstream outFile(\u0026#34;filename\u0026#34;, std::ios_base::out | std::ios_base::app); binary能禁止系统特定的转换 避免意义不明确的流使用方式（如ifstream + out） 推荐的打开方式 打开方式 效果 加结尾模式标记 加二进制模式标记 in 只读方式打开文本文件 初始文件位置位于文件末尾 禁止系统转换 out|trunc 如果文件存在，长度截断为0；否则创建文件供写入 初始文件位置位于文件末尾 禁止系统转换 out 如果文件存在，长度截断为0；否则创建文件供写入 初始文件位置位于文件末尾 禁止系统转换 out|app 附加：打开或创建文件，仅供文件末尾写入 初始文件位置位于文件末尾 禁止系统转换 in|out 打开文件供更新使用(支持读写) 初始文件位置位于文件末尾 禁止系统转换 in|out|trunc 如果文件存在，长度截断为0；否则创建文件供更新使用 初始文件位置位于文件末尾 禁止系统转换 文件读取代码例子\n//读取方式: 逐词读取, 词之间用空格区分 void ReadDataFromFileWBW() { ifstream fin(\u0026#34;data.txt\u0026#34;); string s; // c++的流析取器 \u0026gt;\u0026gt; 从流对象析取内容到右操作数。 // 它的默认分隔符是：\\t, space, enter. while(fin \u0026gt;\u0026gt; s) { cout \u0026lt;\u0026lt; \u0026#34;Read from file: \u0026#34; \u0026lt;\u0026lt; s \u0026lt;\u0026lt; endl; } } //读取方式: 逐行读取, 将行读入字符数组, 行之间用回车换行区分 void ReadDataFromFileLBLIntoCharArray() { ifstream fin(\u0026#34;data.txt\u0026#34;); const int LINE_LENGTH = 100; // 一:fstream.getline的第二个参数需要传入字符数，而非字节数，文档中没有明确说明。 // 二:如果单行超过了缓冲，则循环会结束。 // 总结：用getline的时候，一定要保证缓冲区够大，能够容纳各种可能的数据行。切记传入字符数。 // 在此例中则为创建\u0026#34;data.txt\u0026#34;的时候，每一行的字符数不要超过100，否则while循环会结束。 char str[LINE_LENGTH]; while(fin.getline(str, LINE_LENGTH)) { cout \u0026lt;\u0026lt; \u0026#34;Read from file: \u0026#34; \u0026lt;\u0026lt; str \u0026lt;\u0026lt; endl; } } //读取方式: 逐行读取, 将行读入字符串, 行之间用回车换行区分 void ReadDataFromFileLBLIntoString() { ifstream fin(\u0026#34;data.txt\u0026#34;); string s; // 这里的getline是C++ string里面的API，和上面的不一样 while(getline(fin,s)) { cout \u0026lt;\u0026lt; \u0026#34;Read from file: \u0026#34; \u0026lt;\u0026lt; s \u0026lt;\u0026lt; endl; } } 内存操作 内存流：basic_istringstream / basic_ostringstream / basic_stringstream 也会受打开模式：in / out / app的影响，不会受trunc和binary的影响 std::ostringstream buf(\u0026#34;test\u0026#34;); buf \u0026lt;\u0026lt; \u0026#39;1\u0026#39;; std::cout \u0026lt;\u0026lt; buf.str() \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 输出“1est” std::ostringstream buf2(\u0026#34;test\u0026#34;, std::ios_base::ate); buf2 \u0026lt;\u0026lt; \u0026#39;1\u0026#39;; std::cout \u0026lt;\u0026lt; buf2.str() \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 输出“test1” 使用str()方法获取底层所对应的字符串 小心避免使用str().c_str()的形式获取C风格字符串，因为str()返回的是一个右值，是个临时对象，该行语句执行完就会被销毁，所以拿str()返回值的指针进行操作是一件很危险的事 可以分两步写，先把str()存在一个局部变量里，再返回该局部对象的c_str() 基于字符串流的字符串拼接优化 // 下面这种写法的性能非常差 // 当x新加入一些字符的时候，它会判断当前x所拥有的内存是否够用，不够用的话，会新开辟一块大内存，把x中已经有的内容和新加入的内存一起放在新的大内存里，再去销毁掉原来占用的小内存 // 这种操作很像vector.emplace_back过程，不断在开辟和释放内存，非常占资源 std::string x; x += \u0026#34;hello\u0026#34;; x += \u0026#34;hello\u0026#34;; x += \u0026#34;hello\u0026#34;; // 改成这样就好很多 // 因为stream会在内部维护一个缓冲区，只有缓冲区填满了才会写入内存。 // 由于缓冲区一般比较大，所以相比上面的写法，下面的写法对内存的操作会变少很多。 std::ostringstream tmp; tmp \u0026lt;\u0026lt; \u0026#34;hello\u0026#34;; tmp \u0026lt;\u0026lt; \u0026#34;hello\u0026#34;; tmp \u0026lt;\u0026lt; \u0026#34;hello\u0026#34;; std::string x = tmp.str(); 流的定位 获取流位置 tellg() / tellp()可以用于获取输入(get) / 输出(put)流位置（pos_type类型） 两个方法可能会失败，此时返回pos_type(-1) 设置流位置 seekg() / seekp()用于设置输入/输出流的位置 这两个方法分别有两个重置版本 设置绝对位置：传入pos_type进行设置 设置相对位置：通过偏移量（字符格式ios_base::beg）+ 流位置符号的方式设置 ios_base::beg：流的开头 ios_base::cur：当前流的位置 ios_base::end：流的结尾 流的同步 基于flush() / sync() / unibuf的通过 flush()用于输出流同步，刷新缓冲区 // 下面的代码有一个问题，cout的缓冲区要满了才会输出到终端，如果像这里没有满的话，终端上就没有\u0026#34;What\u0026#39;s your name?\u0026#34;，用户就不知道该干嘛 std::cout \u0026lt;\u0026lt; \u0026#34;What\u0026#39;s your name?\u0026#34;; std::string name; std::cin \u0026gt;\u0026gt; name; // 解决方案1 std::cout \u0026lt;\u0026lt; \u0026#34;What\u0026#39;s your name?\u0026#34; \u0026lt;\u0026lt; std::flush; // 解决方案2 std::cout \u0026lt;\u0026lt; \u0026#34;What\u0026#39;s your name?\u0026#34;; std::cout.flush(); // 解决方案3，不建议用，性能会受较大影响，缓冲区机制就不存在了 std::cout \u0026lt;\u0026lt; std::unibuf \u0026lt;\u0026lt; \u0026#34;What\u0026#39;s your name?\u0026#34;; sync()用于输入流同步，其实现逻辑是编译器所定义的 输出流可以通过设置unibuf来保证每次输出后自动同步 基于绑定(tie)的同步 绑定的目标一定是个输出流 流可以绑定到一个输出流上，这样每次输入 / 输出前可以刷新输出流的缓冲区 比如cin绑定到了cout上 与C语言标准IO库的同步 cout维护了一个缓冲区，printf也维护了一个缓冲区，如果不同步，那cout和printf的输出顺序就和他们在代码中的执行顺序不一致了 缺省情况下，C++的输入输出操作会与C的输入输出函数同步 可以通过sync_with_stdio关闭该同步，因为为了这个同步，系统牺牲了一部分性能 流的状态 iostate failbit: 输入输出操作失败（格式化或者提取错误） // outFile本身就是close状态（没有关联到任何文件），执行close操作会失败 std::ofstream outFile; std::cout \u0026lt;\u0026lt; outFile.fail() \u0026lt;\u0026lt; std::endl; // 0 outFile.close(); std::cout \u0026lt;\u0026lt; outFile.fail() \u0026lt;\u0026lt; std::endl; // 1 badbit: 不可恢复的错误 std::ofstream outFile; outFile \u0026lt;\u0026lt; \u0026#34;hello\u0026#34;; std::cout \u0026lt;\u0026lt; outFile.bad() \u0026lt;\u0026lt; std::endl; // 1 eofbit: 关联的输入序列已抵达文件尾 goodbit: 无错误 检测流的状态 good() / fail() / bad() / eof()方法 流会隐式转换为bool值 int x; if (std::cin \u0026gt;\u0026gt; x) { std::cout \u0026lt;\u0026lt; \u0026#34;succ\u0026#34; \u0026lt;\u0026lt; std::endl; } std::cout \u0026lt;\u0026lt; static_cast\u0026lt;bool\u0026gt;(std::cin) \u0026lt;\u0026lt; std::endl; 注意 转换为bool值时不会考虑eof fail与eofkennel会被同时设置，但两者含义不同 通常来说，只要流处于某种错误状态时，后续的插入/提取操作就不会生效 设置流状态 clear(iostate): 设置流的状态为一个具体的数值（缺省为goodbit） setstate: 将某个状态附加到现有的流状态上 捕获流异常: exceptions方法 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/iostream/","title":"IOStream"},{"content":"定理 对于问题 $$min_{x}f(x)$$ $$s.t. h_{i}(x)\\leq0,i=1,\u0026hellip;,m;l_{i}(x)=0,j=1,\u0026hellip;,r$$ 如果该问题不是degenerate，那么最优解满足如下条件 stationarity: \\(0\\in\\partial_{x}\\left[f(x)+\\sum_{i=1}^{m}u_{i}h_{i}(x)+\\sum_{j=1}^{r}v_{i}l_{i}(x)\\right]\\) complementary slackness: \\(u_{i}\\cdot%20h_{i}=0,i=1,\u0026hellip;,m\\) primal feasibility: \\(h_{i}(x)\\leq0,l_{j}=0;i=1,\u0026hellip;,m;j=1,\u0026hellip;,r\\) dual feasibility: \\(u_{i}\\geq0,i=1,\u0026hellip;,m\\) 用途 通过构造KKT条件，如果条件中没有不等式，那就有机会直接求解优化问题的解，比如下图这种情况，我们只需要求解下面的线性方程组的解即可得到优化结果 可以拿来度量约束优化数值算法的解的精度。下面三个值是KKT残差，通过取其中的最大值，使其足够小，就可以得到精度较高的解 不等式约束违背程度：\\(max\\lbrace h_{i},0\\rbrace\\) 等式约束违背程度：\\( \\left|l_{j}\\right| \\) 梯度残差：\\( \\left|\\partial_{x}\\left[f(x)+\\sum_{i=1}^{m}u_{i}h_{i}(x)+\\sum_{j=1}^{r}v_{i}l_{i}(x)\\right]\\right| \\) ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/kkt/","title":"Karush-Kuhn-Tucker(KKT) Conditions"},{"content":"问题形式 原问题：\\(min_{x}f(x)\\)，\\(s.t.Ax=b\\)，其中\\(f(x)\\)是个凸函数 原问题的Lagrangian：\\(L(x,\\lambda):=f(x)+\\langle\\lambda,Ax-b\\rangle\\)，后面的尖括号是内积的操作符 显然：\\(max_{\\lambda}f(x)+\\langle\\lambda,Ax-b\\rangle= \\lbrace\\begin{matrix}f(x),\u0026amp;Ax=b \\\\ \\infty,\u0026amp;otherwise\\end{matrix} \\) 因此，原问题等于：\\(min_{x}f(x),s.t.Ax=b\\Leftrightarrow min_{x}max_{\\lambda}L(x,\\lambda)\\) 几何解释 最优解即Lagrangian函数的鞍点 从上图可以看到，\\(x\u0026gt;1\\)时，\\(\\lambda\\)取无穷大，即可让Lagrangian函数趋于无穷大；\\(x\u0026lt;1\\)时，\\(\\lambda\\)取负无穷大，即可让Lagrangian函数趋于无穷大；只有当\\(x=1\\)时，\\(\\lambda\\)无论取什么值，Lagrangian函数的值都保持在一个上界之下 Uzawa\u0026rsquo;s Method 推导过程 \\(min_{x}max_{\\lambda}L(x,\\lambda):=f(x)+\\left\u0026lt;\\lambda,Ax-b\\right\u0026gt;\\)的后半部分\\(max_{\\lambda}L(x,\\lambda):=f(x)+\\left\u0026lt;\\lambda,Ax-b\\right\u0026gt;\\)的取值是\\(f(x)\\)或\\(\\infty\\)，即对于前面的\\(x\\)来说，Lagrangian函数是不连续的，导致这个min问题很难求 现在假设我们把min和max交换一下顺序，\\(max_{\\lambda}min_{x}L(x,\\lambda):=f(x)+\\left\u0026lt;\\lambda,Ax-b\\right\u0026gt;\\)，并且，如果后半部分\\(min_{x}L(x,\\lambda):=f(x)+\\left\u0026lt;\\lambda,Ax-b\\right\u0026gt;\\)是严格凸的话，给定一个\\(\\lambda\\)就可以很轻易的求解\\(x^{*}\\) 注意！von Neumann提到：\\(max_{\\lambda}min_{x}L(x,\\lambda)\\leq min_{x}max_{\\lambda}L(x,\\lambda)\\)，当且仅当\\(f(x)\\)连续且凸时取等号 因此我们直接通过梯度上升法求解对\\(\\lambda\\)做maximize的对偶问题：\\(d(\\lambda):=min_{x}f(x)+\\left\u0026lt;\\lambda,Ax-b\\right\u0026gt;\\) 由于每确定一个\\(\\lambda\\)，都可以求得一个确定的\\(x^{*}\\)，所以\\(x^{*}\\)的值本质上是关于\\(\\lambda\\)的函数，因此上面的对偶问题又可以写成\\(d(\\lambda)=L(x^{*}(\\lambda),\\lambda)\\) 我们将它对\\(\\lambda\\)求导，\\(\\frac{\\partial d}{\\partial\\lambda}=\\frac{\\partial L}{\\partial x^{*}}\\cdot\\frac{\\partial x^{*}}{\\partial\\lambda}+\\frac{\\partial L}{\\partial\\lambda}\\)，注意\\(\\frac{\\partial L}{\\partial x^{*}}\\)就是0，对于凸函数来说，导数为0的点就是最优解所在的点，所以\\(\\frac{\\partial d}{\\partial\\lambda}=\\frac{\\partial L}{\\partial\\lambda}|_{x^{*}(\\lambda)}\\)，也就说，我们给定一个\\(\\lambda\\)，可以确定一个最优的\\(x^{*}\\)，然后用\\(x^{*}\\)求得\\(\\frac{\\partial d}{\\partial\\lambda}=Ax^{*}-b\\)后，通过梯度上升求得新的\\(\\lambda\\). 总结一下，迭代步骤如下： $$\\lbrace \\begin{matrix}x^{k+1}=argmin_{x}L(x,\\lambda^{k}) \\\\ \\lambda^{k+1}=\\lambda^{k}+\\alpha(Ax^{k+1}-b)\\end{matrix}$$ \\(\\alpha\\)是沿梯度方向前进的步长 缺陷 (致命)该方法要求\\(f(x)\\)是凸的 (致命)该方法要求\\(min_{x}L(x,\\lambda):=f(x)+\\left\u0026lt;\\lambda,Ax-b\\right\u0026gt;\\)关于原问题的优化变量\\(x\\)是严格凸的 梯度上升的步长需要调参 收敛速度不一定快，因为严格凸不保证函数的光滑性，如果函数不光滑，在某些点就只存在次梯度，根据前面的介绍，沿着次梯度的反（正）方向走不保证函数值下降（上升）。 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/lagrangian_relaxation/","title":"Lagrangian Relaxation"},{"content":"局部类 可以在函数中定义 可以访问外围函数中定义的类型声明，静态对象与枚举 可以定义成员函数，但成员函数的定义必须放在类内 不能定义静态数据成员 void fun() { using MyInt = int; int val; struct Helper { MyInt x; int y = val; // 编译失败 inline static int val2 = 100; // 编译失败 int inc(); int acc() { // 编译成功，函数定义在类内部 x++; } }; int Helper::inc() { // 编译失败，函数内部不能再定义函数 returen x++; } Helper h; } int main() { fun(); } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/class/local-class/","title":"Local Class"},{"content":"Low-Dimensional Linear Program 问题形式 其中的\\(d\\)特别的小，但c\\(n\\)可以很大 几何上的理解 相关方法对比 the simplex algorithm 能得到精确解，但最坏复杂度是指数时间的 GLPK用的是simplex方法解LP IPM 复杂度是多项式时间，但不能得到精确解 Seidel\u0026rsquo;s Algorithm 在低维、高约束量的前提下，具备线性时间复杂度，精确解的优势 Seidel\u0026rsquo;s Algorithm流程 random order可以通过Fisher-Yates方法生成 高斯消元本质是将\\(dim\\)降维成\\(dim-1\\)。 高斯消元的时候，每次选择系数的绝对值最大的元去消，可以保证算法的数值稳定性。从几何上理解，对于平面\\(ax+by+cz=d\\)来说，如果\\(z\\)的系数的绝对值最大，说明平面和z轴最垂直（和xy平面最平行），将z消去后，信息损失很少。 Seidel\u0026rsquo;s Algorithm应用 Linear Separability(点集碰撞检测) 本质上是找一个超平面，使得绿色点集中的所有点在超平面的一侧，红色点集中的所有点在超平面另一侧 Chebyshev Center(切比雪夫中心) 本质上是找一组超平面的最大内切圆，圆心即距离所有边都最远的点 假设这组超平面为\\(Ax\\leq b\\)，其中\\(A=\\left [a_{1}^{T},a_{2}^{T},\u0026hellip;,a_{m}^{T}\\right]^{T}\\)，\\(b=\\left [b_{1},b_{2},\u0026hellip;,b_{m}\\right]^{T}\\)，假设\\(\\left|a_{i}^{T}\\right|=1\\)，那这里的\\(a_{i}^{T}\\)就是第\\(i\\)个超平面的单位法向量 由于球在超平面内部，所以球上的每个点都满足\\(Ax\\leq b\\);为了让点距离每个超平面都最远，我们引入一个margin\\(y\\)，我们在保证\\(Ax+y\\leq b\\)的同时，让\\(y\\)尽可能大 写成向量形式就是\\(min_{\\bar{x}\\in R^{n+1}}-\\bar{x}^{T}e_{n+1},s.t.(A,1)\\bar{x}\\leq b\\)，其中\\(\\bar{x}=(x^{T},y)^{T}\\)，\\(e_{n+1}=(0,\u0026hellip;,0,1)^{T}\\) 利用切比雪夫中心进行凸包碰撞检测 已知一个凸包为\\(A_{1}x\\leq b_{1}\\)，另一个凸包为\\(A_{2}x\\leq b_{2}\\)，联立组成新的凸包，\\(\\begin{bmatrix}A_{1}\\\\A_{2}\\end{bmatrix}x\\leq \\begin{bmatrix}b_{1}\\\\b_{2}\\end{bmatrix}\\)并求新凸包的切比雪夫中心，如果有解，则说明两个原凸包有交集 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/ldlp/","title":"Low-Dimensional Linear Program"},{"content":"Low-Dimensional Quadratic Program 问题形式 \\(min_{x\\in R^{n}}\\frac{1}{2}x^{T}M_{Q}x+c_{Q}^{T}x,s.t. A_{Q}x\u0026lt;b_{Q} \\)，其中，\\(M_{Q} \\)严格正定。 问题形式转化 由于\\(M_{Q} \\)严格正定，根据Cholesky factorization可得\\(M_{Q}=L_{Q}L_{Q}^{T} \\)，其中\\(L_{Q} \\)是一个下三角矩阵 令\\(x=L_{Q}^{-T}y-(L_{Q}L_{Q}^{T})^{-1}c_{Q} \\)，原问题转化为\\(min_{y\\in R^{n}}\\frac{1}{2}y^{T}y,s.t.Ey\\leq f \\)，其中\\(E=A_{Q}L_{Q}^{-T},f=A_{Q}(L_{Q}L_{Q}^{T})^{-1}c_{Q}+b_{Q} \\) 至此，原QP问题转化为了minimum-norm问题，本质是在一个polytope中寻找最靠近原点的点 算法流程 \\(dim(H) \\)指的是\\(H \\)列的维度，当维度降低到1维的时候，就可以直接求结果 \\(H\u0026rsquo; \\)是\\(I \\)投影到\\(h \\)后的结果，满足\\(dim(H\u0026rsquo;)=dim(I)-1 \\) \\(v \\)是原来的原点投影到\\(h \\)边界上得到的新的坐标 下图描述了HouseholderProj原理，本质上是将\\(H \\)进行降维，在一维空间求得解后升维恢复到原维度 灰色的超平面是算法流程图中的\\(h \\)，由于\\(y\\notin h \\)，最优解\\(y_{N}^{*} \\)一定在其表面上。同时，\\(y^{*}_{N} \\)也需要满足超平面集合\\(I \\)的约束。 接下来要做的就是把当前的N维空间里的原点（N个0的向量）、超平面集合\\(I \\)（若干个\\(Ax\\leq b \\)组成的集合，其中\\(A \\)是\\(size=1*N \\)的向量，\\(b \\)是标量），都投影到\\(h \\)上。原点投影得到\\(v \\)（依然是N维空间下的一个坐标，\\(size=N*1 \\)），\\(I \\)投影得到\\(H\u0026rsquo; \\)（若干个\\(Ax\\leq b \\)组成的集合，其中\\(A \\)是\\(size=1*N-1 \\)的向量，\\(b \\)是标量）。 然后我们在超平面\\(h \\)上以\\(v \\)为原点，以正交向量\\(M \\)为坐标轴新建一个N-1维的坐标系（这样做的意义是：在这个坐标系下的任何一个点，都在\\(y\\notin h \\)平面上）。在这个新坐标系下，我们重新求解距离原点（N-1个0的向量）最近又满足约束\\(H\u0026rsquo; \\)的点\\(y_{N-1}^{*} \\)，这里的\\(y_{N-1}^{*} \\)是以\\(v \\)为原点，以正交向量\\(M \\)为基的坐标系下的相对坐标，所以通过公式\\(y_{N}^{*}=M\\cdot y^{*}_{N-1}+v \\)可以将N-1维空间里得到的解恢复到N维空间。 所以整个流程就是通过不断的投影，将问题降低到1维空间，得到解之后，再逐层恢复到N微空间。 现在有两个问题需要考虑，一是\\(v \\)如何求，二是\\(M \\)如何求 \\(v \\)就是求超平面外一个点在超平面上的投影，公式在上图已经给出了 \\(M \\)是超平面\\(h \\)上的一组标准正交基，我们已知超平面\\(h \\)的表达式为\\(g^{T}y=f \\)（这里的\\(g^{T} \\)等同于上面提到的\\(A \\)，同理\\(f \\)等同于\\(b \\)）。所以\\(h \\)的法向量是\\(g \\)，现在我们构造一组N维的向量\\(\\lbrace e_{0},e_{1},\u0026hellip;,e_{N}\\rbrace \\)，其中\\(e_{i} \\)表示一个第\\(e_{i} \\)位为1，其他位都为0的N维向量。我们将\\(e_{i} \\)的模长缩放到\\(\\left|g\\right| \\)，通过旋转\\(\\left|g\\right|e_{i} \\)，使得其与\\(g \\)重合。将该旋转施加到\\(e_{j},j\\neq i \\)上，\\(e_{j} \\)则贴合于超平面\\(h \\)。旋转变换的计算方式是通过householder reflection实现的，如下图所示。在实际操作中，\\(e_{i} \\)的模长缩放可以取负号，\\(i \\)选择\\(g \\)中绝对值最大的维度，有利于数值稳定。 复杂度分析 \\(O(n) \\)，\\(n \\)是约束的个数 代码 #include \u0026lt;Eigen/Eigen\u0026gt; #include \u0026lt;assert.h\u0026gt; #include \u0026lt;cmath\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;tuple\u0026gt; #include \u0026lt;typeinfo\u0026gt; #include \u0026lt;vector\u0026gt; int MaxId(const Eigen::VectorXd \u0026amp;h) { int max_id = -1; int id = 0; double max_ele = std::numeric_limits\u0026lt;double\u0026gt;::lowest(); while (id \u0026lt; h.rows() - 1) { double ele = std::abs(h(id)); if (ele \u0026gt; max_ele) { max_ele = ele; max_id = id; } id++; } assert(max_id \u0026gt;= 0); return max_id; } double sign(double c) { if (c \u0026gt;= 0.0) { return 1.0; } return -1.0; } struct Constrains { Constrains(int dim) { A = Eigen::MatrixXd::Zero(0, dim); b = Eigen::VectorXd::Zero(0); } Constrains(const Eigen::MatrixXd \u0026amp;_A, const Eigen::VectorXd \u0026amp;_b) { assert(A.rows() == b.rows()); A = _A; b = _b; } int dim() const { return A.cols(); } int size() const { return A.rows(); } void insert(const Eigen::VectorXd \u0026amp;_A, const double _b) { A.conservativeResize(A.rows() + 1, A.cols()); A.row(A.rows() - 1) = _A; b.conservativeResize(b.rows() + 1); b(b.rows() - 1) = _b; } Eigen::MatrixXd A; Eigen::VectorXd b; }; bool OneDimMinNorm(const Constrains \u0026amp;H, Eigen::VectorXd *y) { assert(H.A.cols() == 1); double low = std::numeric_limits\u0026lt;double\u0026gt;::lowest(); double up = std::numeric_limits\u0026lt;double\u0026gt;::max(); for (int i = 0; i \u0026lt; H.A.rows(); ++i) { if (H.A(i, 0) \u0026gt; 0) { up = std::min(up, H.b(i) / H.A(i, 0)); } else if (H.A(i, 0) \u0026lt; 0.0) { low = std::max(low, H.b(i) / H.A(i, 0)); } } if (low \u0026gt; up) { return false; } (*y)(0) = std::min(up, std::max(0.0, low)); return true; } std::tuple\u0026lt;Eigen::MatrixXd, Eigen::VectorXd, Constrains\u0026gt; HouseholderProj(const Constrains \u0026amp;I, const Eigen::VectorXd \u0026amp;g, double f) { int dim = g.rows(); // calcualte origin v Eigen::VectorXd v(dim); v = (f * g) / g.dot(g); // calcualte orth basis M int max_id = MaxId(g); Eigen::MatrixXd e = Eigen::MatrixXd::Identity(dim, dim); e(max_id, max_id) = (-sign(g(max_id)) * g.norm()); Eigen::VectorXd u = g - e.col(max_id); Eigen::MatrixXd H = Eigen::MatrixXd::Identity(dim, dim) - 2.0 * u * u.transpose() / (u.dot(u)); Eigen::MatrixXd transformed_e = H.transpose() * e; double dist = (transformed_e.col(max_id) - g).norm(); assert(dist \u0026lt;= 0.0000001); Eigen::MatrixXd M(dim, dim - 1); M \u0026lt;\u0026lt; transformed_e.leftCols(max_id), transformed_e.rightCols(dim - max_id - 1); // calcualte H_dot Constrains H_dot(I.A * M, I.b - I.A * v); return std::make_tuple(M, v, H_dot); } bool InConstrain(const Eigen::VectorXd \u0026amp;A, const double b, const Eigen::VectorXd \u0026amp;y) { assert(A.rows() == y.rows()); return A.dot(y) \u0026lt;= b; } // H: {a.T * x \u0026lt;= b} bool LowDimMinNorm(const Constrains \u0026amp;H, Eigen::VectorXd *y) { *y = Eigen::VectorXd::Zero(H.dim()); if (H.size() == 0) { return true; } if (H.dim() == 1) { return OneDimMinNorm(H, y); } Constrains I(H.dim()); for (int j = 0; j \u0026lt; H.size(); ++j) { if (!InConstrain(H.A.row(j), H.b(j), *y)) { Eigen::MatrixXd M; Eigen::VectorXd v; Constrains H_dot(H.dim() - 1); std::tie(M, v, H_dot) = HouseholderProj(I, H.A.row(j), H.b(j)); Eigen::VectorXd y_dot(H.dim() - 1); if (!LowDimMinNorm(H_dot, \u0026amp;y_dot)) { return false; } *y = M * y_dot + v; } I.insert(H.A.row(j), H.b(j)); } return true; } int main() { const int d = 3; int m = 7; Eigen::Matrix\u0026lt;double, 3, 3\u0026gt; Q; Eigen::Matrix\u0026lt;double, 3, 1\u0026gt; c; Eigen::Matrix\u0026lt;double, 3, 1\u0026gt; x; // decision variables Eigen::Matrix\u0026lt;double, -1, 3\u0026gt; A(m, 3); // constraint matrix Eigen::VectorXd b(m); // constraint bound Q \u0026lt;\u0026lt; 2.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 1.0, 2.0; c \u0026lt;\u0026lt; 1.2, 2.5, -10.0; A \u0026lt;\u0026lt; 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, -0.7, 0.5, 0.0, 0.5, -1.0, 0.0, 0.0, 0.13, -1.0, 0.1, -3.0, -1.3; b \u0026lt;\u0026lt; 10.0, 10.0, 10.0, 1.7, -7.1, -3.31, 2.59; // 将qp问题转化为min norm问题 Eigen::LLT\u0026lt;Eigen::Matrix\u0026lt;double, d, d\u0026gt;\u0026gt; llt(Q); if (llt.info() != Eigen::Success) { std::cout \u0026lt;\u0026lt; \u0026#34;infinity\\n\u0026#34;; return 0; } const Eigen::Matrix\u0026lt;double, -1, d\u0026gt; As = llt.matrixU().template solve\u0026lt;Eigen::OnTheRight\u0026gt;(A); const Eigen::Matrix\u0026lt;double, d, 1\u0026gt; v = llt.solve(c); const Eigen::Matrix\u0026lt;double, -1, 1\u0026gt; bs = A * v + b; // 求解min norm问题 Constrains H(As, bs); Eigen::VectorXd z(H.dim()); if (LowDimMinNorm(H, \u0026amp;z)) { llt.matrixU().template solveInPlace\u0026lt;Eigen::OnTheLeft\u0026gt;(z); z -= v; std::cout \u0026lt;\u0026lt; \u0026#34;optimal sol: \u0026#34; \u0026lt;\u0026lt; z.transpose() \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; \u0026#34;minobj: \u0026#34; \u0026lt;\u0026lt; 0.5 * (Q * z).dot(z) + c.dot(z) \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; \u0026#34;cons precision: \u0026#34; \u0026lt;\u0026lt; (A * z - b).maxCoeff() \u0026lt;\u0026lt; std::endl; } else { std::cout \u0026lt;\u0026lt; \u0026#34;infeasible\\n\u0026#34;; } return 0; } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/ldqp/","title":"Low-Dimensional Linear Program"},{"content":"MDPs 马尔可夫性 只要知道现在，将来和过去条件独立 每一时刻的状态只与上一时刻的状态有关 当前状态包含了所有的历史状态信息 要求环境全观测 任务类型定义 强化学习中,从初始状态\\(S_{1}\\)到终止状态的序列过程,被称为一个片段(episode)。 如果一个任务总以终止状态结束,那么这个任务被称为片段任务(episodic task) 如果一个任务会没有终止状态,会被无限执行下去,这被称为连续性任务 (continuing task) 终止状态等价于自身转移概率为 1,奖励为 0 的的状态 状态转移矩阵 s1 s2 s3 s4 转移 0.5 0.0 0.5 0.0 s1 0.1 0.2 0.3 0.4 s2 0.0 0.0 0.0 1.0 s3 0.0 0.0 0.0 1.0 s4 上图中\\(s1\\)转换到\\(s1\\)的概率是0.5，转换到s3的概率是0.5；s2转换到s1的概率是0.1，转换到s2的概率是0.2，转换到s3的概率是0.3，转换到s4的概率是0.4。\n奖励与回报 奖励值:对每一个状态的评价 回报值: 对每一个片段的评价 对于片断性任务，回报值是未来有限个状态的奖励值的和\\(G_{t}=\\sum_{k=0}^{T-t-1}\\gamma ^{k}R_{t+k+1}\\) 对于连续性任务，回报值是未来无限个状态的奖励值的和\\(G_{t}=\\sum_{k=0}^{\\infty }\\gamma ^{k}R_{t+k+1}\\) 回报值是从时间\\(t\\)处开始的累计衰减奖励 指数衰减值 对未来的把握也是逐渐衰减的 一般情况下,我们更关注短时间的反馈 值函数:某个状态所对应回报值的期望 贝尔曼方程 强化学习的核心 $$v(s)=R(s)+\\gamma \\sum_{s\u0026rsquo;}^{ }P_{s{s}\u0026rsquo;}v({s}\u0026rsquo;)$$ 策略 状态值函数(\\(v_{\\pi }(s)\\))：是从状态\\(s\\)开始，使用策略\\(\\pi\\)得到的期望回报值 状态动作值函数(\\(q_{\\pi }(s,a)\\))：是从状态\\(s\\)开始,执行动作\\(a\\)，然后使用策略\\(\\pi\\)得到的期望回报值 $$v_{\\pi }(s)=\\sum \\pi (a|s)q_{\\pi }(s,a)$$ $$q_{\\pi }(s,a)=R(s,a)+\\gamma \\sum_{s\u0026rsquo;\\in S}^{ }P_{s{s}\u0026rsquo;}^{a}v_{\\pi }(s)$$ 知识点 贝尔曼最优方程不是线性的 一般很难有闭式的解 可以使用迭代优化的方法去解 值迭代 策略迭代 Q 学习 SARSA POMDP 观测不等于状态\\(O ≠ S\\) POMDPs 由七元组构成 \\(\\langle S, A, O, P, R, Z, \\gamma \\rangle\\) \\(Z\\)是观测函数 观测不满足马尔可夫性,因此也不满足贝尔曼方程 状态未知,隐马尔可夫过程 有时对于 POMDPs 来说,最优的策略是随机性的 无衰减 MDPs 用于各态历经马尔可夫决策过程 存在独立于状态的平均奖赏 求值函数时,需要减去该平均奖赏,否则有可能奖赏爆炸 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/markov-decision-processes/","title":"Markov Decision Processes"},{"content":"成员指针 数据成员指针类型示例：int A::*; 成员函数指针类型示例：int (A::*)(double); class Str { public: int x; void fun() {}; void fun(double) {}; }; int main() { int Str::*ptr = \u0026amp;Str::x; // 数据成员指针 // 阅读方法：*表明ptr是个指针，能够访问Str::域内的内容，并且这个成员的类型是int int Str::*ptr = \u0026amp;(Str::x); // 报错，加了括号后含义就变了，就不再是取某个域内的地址了，而是取括号里的内容的地址，这就要求括号内的东西是有定义的，但Str::x目前还没有定义，只是声明，所以会报错 void (Str::* ptr_fun)() = \u0026amp;Str::fun; // 成员函数指针 // 阅读方法：*表明ptr是个指针，能够访问Str::域内的内容，()表示这个东西是个函数，函数的输入是void(省略了)，输出也是void auto ptr_fun = \u0026amp;Str::fun; // 报错，因为无法确定auto的内容，上面有两个fun函数可以选择 } 成员指针对象赋值：auto ptr = \u0026amp;A::x; 注意不能加()，上面有原因 成员指针的使用 class Str { public: int x; }; int main() { int Str::*ptr = \u0026amp;Str::x; *ptr = 1; // 报错，ptr指向的只是个声明，并没有明确的定义，所以就没有一块固定的内存来解引用 Str obj; obj.*prt = 1; // 合理 Str* p_obj = \u0026amp;obj; p_obj-\u0026gt;*ptr = 3; // 合理 } bind交互 使用bind + 成员指针构造可调用对象 class Str { public: int x; void fun(double) {}; }; int main() { auto ptr = \u0026amp;Str::fun(); Str obj; (obj.*ptr)(100.0); auto x = std::bind(ptr, obj, 100.0); // 没有obj的话会报错 x(); // 调用fun auto ptr2 = \u0026amp;Str::x; auto x2 = std::bind(ptr2, obj); x2(); // 返回obj.x } 注意这种方法也可以基于数据成员指针构造可调用对象(见上面代码中的ptr2) ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/class/member-ptr/","title":"Member Ptr of Class"},{"content":"前言 强化学习可以分为有模型和无模型的方法两大类 未知模型 学习法 通过智能体的交互，学习值函数和策略 代表方法：MC，TD 已知模型 规划法 无需智能体交互，直接从模型学习最优策略 代表方法：DP 基于模型的强化学习 核心思路 通过经验，学习出一个虚拟的环境模型 利用学到的环境模型，进行动态规划，计算价值函数或者策略 优势 可以通过监督学习，有效地学习环境模型 可以将学到的环境模型放在GPU内，快速得到大量的交互信息 没有任何真实损失 直接利用环境模型的不确定性 劣势 先学环境模型，再学值函数，存在两次近似误差 -\u0026gt; 累计误差 Dyna-q 算法流程 repeat \\(n\\) times的部分实际上是在用学习到的虚拟环境模型进行学习，这一部分可以新开一个线程，和外面真实的环境交互分开。 真实的环境交互1秒一次，而计算机中虚拟的环境交互0.1秒一次，那这里的\\(n\\)就是10。 蒙特卡洛树搜索Monte-Carlo Tree Search 特性 适用于Combinatorial Games Combinatorial Games特点：零和、完美信息、确定性、离散、序列化 算法核心 在MCTS中，仿真策略需要策略提升 每次仿真有两个阶段 树策略(提升)：选择动作，最大化\\(Q(S, A)\\) 默认策略(固定)：快速计算到终止状态 Repeat(每次仿真) 使用MC评价来估计\\(Q(S, A)\\) 提升树策略，比如epsilon-贪婪，UCB等 对仿真出来的经验做MC优化 搜索到最优的搜索树\\(Q(S,A)\\to q^{*}(S,A)\\) epsilon-贪婪对于非最优策略是均匀采样的 UCB既考虑了值函数，又考虑了探索的次数 = 回报值/仿真次数\n优势 Highly selective best-first search 动态评价状态 结合了采样去打破维度诅咒，用采样取代了暴力搜索 适合于各种黑盒模型，不需要满足马尔可夫性 计算有效，容易并行 算法流程 当前状态\\(S1\\)为叶子节点，直接从当前状态开始，用默认策略进行仿真 结果“赢了” 更新节点的UCB=获胜次数/仿真次数 依据UCB规则选择一个动作向下走，每一个动作都对应了一个UCB UCB其实是一个值，与值函数正相关，与仿真次数负相关 但是目前仿真次数都是0，所以根据值函数选一个动作，到达\\(S2\\) 结果“输了” 更新树策略路径上的每个节点的UCB 从S1开始，根据UCB选一个动作 由于第二步中，最终结果是“输了”，所以第二步中动作的UCB下降了，于是从\\(S1\\)重新选一个动作，到达\\(S3\\) 结果“赢了” 更新树策略路径上的每个节点的UCB 再从\\(S1\\)开始搜索，搜到\\(S3\\)后，再选一个动作，到达\\(S4\\) 结果“输了” 更新树策略路径上的每个节点的UCB 再从\\(S1\\)开始搜索，搜到\\(S3\\)后，没有去\\(S4\\)(因为\\(S4\\)刚刚输了，UCB下降)，再选一个动作，到达\\(S5\\) 结果“赢了” 更新树策略路径上的每个节点的UCB 后面的步骤依次类推 TD搜索 算法特性 有些情况下，没有终止状态，MC方法不适用 将MCTS中的MC评价换成TD评价 算法流程 从当前状态St开始采样片段 估计\\(Q(s, a)\\) 对于每一步的仿真，使用Sarsa方法更新Q函数 $$\\Delta Q(S,A)=\\alpha (R+\\gamma Q({S}\u0026rsquo;,{A}\u0026rsquo;)-Q(S,A))$$ 基于\\(Q(s, a)\\)选择动作 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/model-based-rl/","title":"Model based RL"},{"content":"基本概念 蒙特卡洛采样是无模型方法 行为策略是智能体与环境交互的策略 目标策略是我们要学习的策略 在策略（on-policy）学习 行为策略和目标策略是同一个策略 直接使用样本统计属性去估计总体 更简单,且收敛性更好 数据利用性更差(只有智能体当前交互的样本能够被利用) 限定了学习过程中的策略是随机性策略 离策略（off-policy）学习 行为策略和目标策略不是同一个策略 一般行为策略选用随机性策略，目标策略选用确定性策略 需要结合重要性采样才能使用样本估计总体 方差更大,收敛性更差 数据利用性更好 (可以使用其他智能体交互的样本) 行为策略需要比目标策略更具备探索性 重要性采样 是一种估计概率分布期望值的技术,它使用了来自其他概率分布的样本 主要用于无法直接采样原分布的情况 估计期望值时,需要加权概率分布的比值 算法特性 MC方法可以被用于任意涉及随机变量的估计 这里MC方法特指利用统计平均估计期望值的方法 MC方法从完整的片段中学习 MC方法仅仅用于片段性任务(必须有终止条件) 算法核心 通过不断的采样,然后统计平均回报值来估计值函数,方差较大\n从某个状态\\(S\\)开始，通过某种策略P进行探索，一直到终止状态，得到反馈Fk 重复以上步骤\\(n\\)次，\\(V(s)=(F1+F2+\u0026hellip;+Fn)/n\\) 蒙特卡洛评价 首次拜访(First-visit)MC策略评价 每次拜访(Every-visit)MC策略评价 s1,s2,s3,s1,s4,s2,s5 +1 s1,s2,s1,s5 +1 对于上面的两种采样轨迹，评价s1时，首次拜访只在s1在一条轨迹中第一次出现时N=N+1；每次拜访则是出现一次s1就N=N+1 首次拜访：(1+1)/2 = 1 每次拜访：(1+1)/4 = 0.25 Q函数的MC方法 每次是针对一个s和一个a进行评价 为了充分探索所有的\\(s,a\\)组合，随机选择初始状态和初始动作 离策略的MC策略评价 核心是利用重要性采样去加权回报值 $$G_{t}^{\\pi /\\mu }=\\prod_{k=t}^{T-1} \\frac{\\pi (A_{k}|S_{k}))}{\\mu (A_{k}|S_{k})}G_{t}$$ 使用重要性采样会显著增加方差, 可能到无限大 MC小结 偏差为 0,是无偏估计 方差较大,需要大量数据去消除 收敛性较好 没有利用马尔可夫性,有时可以用在非马尔可夫环境 增量式MC 之前的蒙特卡洛算法需要采样大量轨迹之后再统一计算平均数，能不能在每一条轨迹之后都得到值函数的估计值呢?\n$$N(S_{t})=N(S_{t})+1$$ $$V(S_{t})=V(S_{t})+\\frac{1}{N(S_{t})}(G_{t}-V(S_{t}))$$ 这里的\\(N(S_{t})\\)可以认为是更新的步长 很多时候，我们会把\\(N(S_{t})\\)替换为一个常数\\(\\alpha \\)，好处如下： 会逐渐遗忘过去的轨迹 对初始值敏感度更小 适用于不稳定环境 MC策略提升 不能使用贪婪策略提升，会导致部分状态永远不会遍历到 每次探索，有一定的几率随机选择动作，其他情况下都采取贪婪策略 无限探索下的极限贪婪(GLIE) 无限探索:所有的状态动作对能够被探索无穷次 极限贪婪:在极限的情况下,策略会收敛到一个贪婪的策略 GLIE 蒙特卡洛优化能收敛到最优的 Q 函数\n增量式离策略每次拜访蒙特卡洛评价 1: repeat \\(k=1,2,3,\u0026hellip;\\) 2: 使用策略\\(\\mu \\)采样第\\(k\\)条轨迹，\\(S_{1},A_{1},S_{2},A_{2},\u0026hellip;,S_{T}\\) 3: \\(G\\leftarrow 0, W\\leftarrow 1\\) 4: for \\(t=T-1,T-2,\u0026hellip;,0\\) do 5: \\(G\\leftarrow \\gamma G+R_{t+1}\\) 6: \\(C(S_{t},A_{t})\\leftarrow C(S_{t},A_{t})+W\\) 7: \\(Q(S_{t},A_{t})\\leftarrow Q(S_{t},A_{t})+\\frac{W}{C(S_{t},A_{t})}[G-Q(S_{t},A_{t})]\\) 8: \\(W\\leftarrow W\\frac{\\pi (A_{t}|S_{t})}{\\mu (A_{t}|S_{t})}\\) 9: if W=0, break 10: end for 11: until 收敛 增量式离策略每次拜访蒙特卡洛优化 1: repeat \\(k=1,2,3,\u0026hellip;\\) 2: 使用策略\\(\\mu \\)采样第\\(k\\)条轨迹，\\(S_{1},A_{1},S_{2},A_{2},\u0026hellip;,S_{T}\\) 3: \\(G\\leftarrow 0, W\\leftarrow 1\\) 4: for \\(t=T-1,T-2,\u0026hellip;,0\\) do 5: \\(G\\leftarrow \\gamma G+R_{t+1}\\) 6: \\(C(S_{t},A_{t})\\leftarrow C(S_{t},A_{t})+W\\) 7: \\(Q(S_{t},A_{t})\\leftarrow Q(S_{t},A_{t})+\\frac{W}{C(S_{t},A_{t})}[G-Q(S_{t},A_{t})]\\) 8: \\(\\pi (S_{t})\\leftarrow argmax_{a}Q(S_{t},a)\\) 9: if \\(A_{t}\\neq \\pi (S_{t})\\)，则退出for循环 10: \\(W\\leftarrow W\\frac{1}{\\mu (A_{t}|S_{t})}\\) 11: end for 12: until 收敛 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/monte-carlo-sampling/","title":"Monte-Carlo Sampling"},{"content":"锁基本背景 C++提供了四种锁：互斥锁、条件变量、自旋锁和读写锁 C++11引入了std::unique_lock与std::lock_guard boost库实现了读写锁 当一个线程中的函数获取了锁，在锁没有消失(超出作用域)前，另一个线程获取该锁的操作(函数)将无法进行。 读写锁 主要适合在于共享数据更新频率较低，但是读取共享数据频率较高的场合。 当 data 被线程A读取时，其他线程仍可以进行读取却不能写入 当线程A获得共享锁时，其他线程仍可以获得共享锁但不能获得独占锁 当 data 被线程A写入时，其他线程既不能读取也不能写入 当线程A获得独占锁时，其他线程既不能获得共享锁也不能获得独占锁 #include \u0026lt;boost/thread/shared_mutex.hpp\u0026gt; #include \u0026lt;boost/thread/shared_lock_guard.hpp... #include \u0026lt;boost/thread.hpp\u0026gt; void demo() { typedef boost::shared_lock\u0026lt;boost::shared_mutex\u0026gt; read_lock; typedef boost::unique_lock\u0026lt;boost::shared_mutex\u0026gt; write_lock; boost::shared_mutex read_write_mutex; int32_t data = 1; //线程A,读data { read_lock rlock(read_write_mutex); std::cout \u0026lt;\u0026lt; data \u0026lt;\u0026lt; std:; endl; } //线程B,读data { read_lock rlock(read_write_mutex); std::cout \u0026lt;\u0026lt; data \u0026lt;\u0026lt; std:; endl; } //线程C,写data { write_lock rlock(read_write_mutex); data = 2; } } 注意：上面的锁的作用域就是锁所在的 互斥锁 简单用法 std::lock_guard 和 std::unique_lock一样 当锁在生命周期之内，可以实现加锁，通常利用花括号控制加锁的范围 简单使用 #include \u0026lt;mutex\u0026gt; // std::mutex, std::lock_guard std::mutex mut; void insert_data(){ std::lock_guard\u0026lt;std::mutex\u0026gt; lk(mut); queue.push_back(data); } void process_data(){ std::unqiue_lock\u0026lt;std::mutex\u0026gt; lk(mut); queue.pop(); } 进阶用法 std::unique_lock更加灵活，但开销也更大 具体用法见下一节条件变量 条件变量 相关的类包括 std::condition_variable std::condition_variable_any std::cv_status枚举类型 std::notify_all_at_thread_exit() std::condition_variable 当std::condition_variable对象的某个wait 函数被调用的时候，它使用std::unique_lock(通过 std::mutex)来锁住当前线程。当前线程会一直被阻塞，直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。 std::condition_variable对象通常使用std::unique_lock 来等待，如果需要使用另外的lockable类型，可以使用std::condition_variable_any类 #include \u0026lt;thread\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;condition_variable\u0026gt; void demo() { std::mutex mtx; // 全局互斥锁. std::condition_variable cv; // 全局条件变量. bool ready = false; // 全局标志位. //线程A { std::unique_lock \u0026lt;std::mutex\u0026gt; lck(mtx); while(!ready) cv.wait(lck);// 当前线程被阻塞, 当全局标志位变为 true 之后,线程被唤醒, 继续往下执行打印线程编号id. std::cout \u0026lt;\u0026lt; \u0026#34;thread \u0026#34; \u0026lt;\u0026lt; id \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } //线程B { std::unique_lock \u0026lt;std::mutex\u0026gt; lck(mtx); ready = true; cv.notify_all(); // 唤醒所有线程. } } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/multi-thread/mutex/","title":"Mutex"},{"content":"嵌套名字空间 名字空间可以嵌套，嵌套名字空间形成嵌套域 注意同样的名字空间定义可以出现在程序多处，以向同一个名字空间中增加声明或定义 namespace A { int x; } namespace A { int y; } int main() { A::x; A::y; } C++17开始可以简化嵌套名字空间的定义 namespace A { int x; } namespace A::B { // C++17才支持这种写法 int y; } int main() { A::x; A::B::y; } 匿名名字空间 用于构造仅翻译单元可见的内容 // main.cc namespace { int y; } int main() { } // source.cc namespace { int y; // 这里的y和mian中的y都是对各自的cc文件可见，不冲突，在内存中是两个不同的地址；如果去掉匿名空间，链接的时候就会报错 } 想要构造仅翻译单元可见的内容还可以用static代替 static int x; // 这里的xyz都是仅对当前翻译单元可见 static int y; // 但这样的写法太啰嗦，每个变量都要写一遍static static int z; 匿名名字空间可以作为嵌套空间 namespace A { namespace { // 这里加匿名空间意义是让A::x这个对象只在当前翻译单元可见，在其他文件访问不到A::x int x; } } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/namespace/","title":"Namespace"},{"content":"嵌套类 在类中定义的类 嵌套类有自己的域，与外围类的域形成嵌套关系 嵌套类中的名称查找失败时会在其外围类中继续查找 class Out { using MyInt = int; inline static int val2 = 3; int xxx = 100; public: class In { public: inline static MyInt val = val2; int vvv = xxx; // 会报错，上面的int xxx不是定义，而是声明，只有在类被实例化的时候才会创建，所以这里无法对齐进行赋值； 如果有static关键字，就是定义了，所以上面一行合法 }; }; int main() { Out::In obj; Out::In::val; } 嵌套类和外围类单独拥有各自的成员 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/class/nested-class/","title":"Nested Class"},{"content":"Newton Conjugate Gradient Method 共轭梯度法 背景 本质上是一种求\\(Ax=b\\)的方法，它厉害在不需要知道\\(A\\)的具体值，只需要多调用几次\\(Ay\\)点积接口就可以把\\(x\\)求出来 计算复杂度 函数的梯度的计算复杂度一般是\\(O\\left ( n \\right )\\) 求函数的梯度的复杂度和求函数本身的复杂度是常数倍的关系 Hessian的计算复杂度是\\(O\\left ( n^{2} \\right )\\) Hessian的逆的计算复杂度是\\(O\\left ( n^{3} \\right )\\) Hessian-vec的复杂度是\\(O\\left ( n \\right )\\)，证明过程如下： 假设\\(\\xi \\)是一个已知的常向量 根据泰勒展开有：\\(\\triangledown f(x+\\alpha\\xi)=\\triangledown f(x)+\\alpha\\triangledown^{2}f(x)\\xi +o(\\left|\\alpha\\right|)\\) 简单变形：\\(\\triangledown^{2}f(x)\\xi\\approx\\frac{\\triangledown f(x+\\alpha\\xi)-\\triangledown f(x)}{\\alpha}\\) 可以发现，求Hessian-vec的近似解只需要求原函数的两次导即可 Linear Conjugate Gradient Method 针对问题\\(Ax=b\\)，我们可以将其转化成一个优化问题\\(argmin_{x}f(x)=\\frac{1}{2}x^{T}Ax-b^{T}x\\)，因为该优化问题的导数是\\(Ax-b\\)，最优解即令其导数为零的点。 梯度下降法和牛顿法求该问题 梯度法没法得到精确解，步长总会引入误差；牛顿法计算量太大，而且牛顿法需要求\\(A^{-1}\\)，而我们本来就是要求这个，鸡生蛋蛋生鸡了；我们需要一种折中的方法——LCG方法 假设\\(x\\in R^{n}\\)，LCG法就是找\\(n\\)个互相共轭（如果\\(A\\)是单位阵，共轭就是垂直）的向量，每次沿着一个向量（的方向）走到最低点，最终一定能走到全局最低点 下图是\\(A\\)为单位阵的情况 general的情况如下 LCG算法流程 求\\(n\\)个互相共轭的向量 初始化\\(n\\)个线性不相关的向量\\(v^{1},\u0026hellip;,v^{n}\\) 计算互相共轭的向量： 这里的投影操作也叫做Gram-Schmidt process，分别考虑\\(A\\)是单位阵（右）和\\(A\\)不是单位阵（左）的情况： 最终得到的\\(e^{1},\u0026hellip;,e^{n}\\)就是我们需要的互相共轭的向量 注意到计算\\(v^{n}\\)的时候需要对过去的\\(n-1\\)个向量做投影，这个计算量很大，我们可以增量地计算\\(v^{k}\\)（这也是lcg方法一个很大的贡献点）： 至于为什么用这种方法生成的\\(v^{k}\\)就可以保证\\(proj_{u^{j}}(v^{k})=0\\)可以去看论文证明 有了互相共轭向量后，继续看迭代流程 \\(\\alpha\\)的迭代公式是由公式\\(\\frac{1}{2}(x^{k}+\\alpha u^{k})A(x^{k}+\\alpha u^{k})-b^{T}(x^{k}+\\alpha u^{k})\\)的导数取0推导而来的 公式中的\\(Au^{k}\\)不需要把\\(A\\)（也是该优化目标函数的Hessian）完整的算出来，直接用Hessian-vec方法进行近似求解，计算复杂度和求导一样 最终的伪代码流程如下： LCG特点 很多时候需要在求LCG之前把\\(A\\) normalize一下，可以通过L-BFGS(memory size=8)去近似估计\\(B\\)（即Hessian的逆），令\\(\\tilde{A}=B^{\\frac{1}{2}}AB^{\\frac{1}{2}}\\)，对\\(\\tilde{A}x=b\\)进行lcg求解，最后将\\(x\\)做线性变换恢复到真实值。\\(\\tilde{A}\\)的条件数会比\\(A\\)更低，CG过程会收敛的更快。下图是一个例子，A的维度是\\(555\\times 555\\)，条件数是\\(10^{10}\\)，Preconditioned CG收敛速度比其他方法快很多 Newton-CG Method 回顾一下newton要解的问题\\((\\triangledown^{2}f)d=-\\triangledown f\\)，套用上面的LCG方法就可以解出\\(d\\)。但还有两个问题需要考虑： 如何处理Hessian不正定的问题？ Truncated CG（截断法），见下面的算法流程 我们是否需要求一个精确的\\(d\\)？ 答案是否。针对问题\\(Hd=-g\\)，我们只需要保证\\(\\displaystyle\\lim_{g\\to0}\\frac{\\left|d^{*}-\\tilde{d}\\right|}{\\left|g\\right|}=0\\)即可（\\(d^{*}\\)是最优解，\\(\\tilde{d}\\)是迭代得到的解），简单说就是一开始梯度还比较大的时候，我们求的\\(d\\)精度也不需要很高，越靠近最优点的时候，精度要求越高。这是牛顿法非常重要的一个结论。 算法流程（伪代码） 本质上就是将牛顿法中求解下降方向的步骤替换成蓝色框中的模块 注意点 \\(d\\)需要初始化为0向量，因为我们希望最优的direction尽可能稳定，所以需要它从0开始出发 当\\((u^{j})^{T}\\triangledown^{2}f(x^{k})u^{j}\\leq0\\)时，说明我们当前所处的位置的Hessian是不定的，分两种情况对待： 如果当前是第一次迭代，那可能是我们距离最优解太远了，这时候直接采用sgd方法更新direction（\\(d^{j}\\)） 如果当前不是第一次迭代，就直接break，依然使用上一次算出来的direction去更新外部的循环（因为本次的direction可能会让函数上升，上一次的虽然是旧的信息，但至少方向没错），这就是截断法 算法对比 两种都是Hessian-Free的方法 两种方法只能保证函数值是单调下降的，不能保证梯度的模是单调下降的 通常情况下，Newton-CG比L-BFGS的最终得到的梯度模长更小 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/newton_cg/","title":"Newton Conjugate Gradient Method"},{"content":"Newton\u0026rsquo;s Method 牛顿法 前提条件 一阶导和二阶导连续 具有严格正定的Hessian矩阵 原理 对函数进行二阶泰勒展开 $$f(x)\\approx f(x_{k})+\\triangledown f(x_{k})^{T}(x-x_{k})+\\frac{1}{2}(x-x_{k})^{T}\\triangledown ^{2}f(x_{k})(x-x_{k})$$ 导数为0时得到极值点 $$\\triangledown f(x)=\\triangledown ^{2} f(x_{k})(x-x_{k})+\\triangledown f(x_{k})=0$$ $$x=x_{k}-\\left [ \\triangledown ^{2}f(x_{k}) \\right ]^{-1}\\triangledown f(x_{k})$$ 更新公式 $$x_{k+1}=x_{k}-\\left [ \\triangledown ^{2}f(x_{k}) \\right ]^{-1}\\triangledown f(x_{k})$$ 减号后面的内容就是牛顿步长（Newton step），这里需要满足Hessian严格正定\\(\\triangledown^{2} f(x_{k})\u0026gt;0\\)，因为沿着梯度的负方向才可以保证函数值下降，牛顿步长是一个负号乘Hessian的逆再乘梯度，Hessian的逆必须要和梯度是同向的（正定）才行，从而Hessian也必须是正定的 特别的，如果目标函数本身就是二次的，那一步就可以迭代到最优解 与梯度下降法对比 下降路线更加平直 迭代次数更少 每次迭代计算量更大 缺点 很多时候Hessian矩阵是半正定或者不定的，会导更新方向不是梯度的反方向，函数值增大 Damped Newton\u0026rsquo;s Method 阻尼牛顿法 当初始点距离最优解较远时，Hessian不一定正定，迭代不一定收敛，因此引入步长因子\\(t\\) $$d=-H^{-1}g$$ $$x_{k+1}=x_{k}+td_{k}$$ Modified Newton\u0026rsquo;s Method 修正牛顿法 背景 提高牛顿方法在一般函数上的鲁棒性 对牛顿法的优化思路 考虑到Hessian不一定正定，尝试用一个正定的\\(M\\)拟合Hessian，只要拟合的够接近，也可以近似表示曲率信息 其实没必要求\\(M^{-1}\\)，本质上我们是想求线性方程\\(Md=-\\triangledown f(x)\\)的解\\(d\\)，可以用一些成熟的线性求解器进行求解，比解逆快很多 inexact line search不需要求Hessian，梯度也只是在最开始算一次，更新\\(t\\)的过程中不需要计算梯度，所以计算量很小 原问题 $$\\left [ \\triangledown ^{2}f(x) \\right ]d=-\\triangledown f(x)$$ 用\\(M\\)拟合Hessian 如果是凸函数（Hessian半正定） $$M=\\triangledown ^{2}f(x)+\\epsilon I,\\epsilon =min(1,\\left| \\triangledown f(x) \\right|_{\\infty })/10$$ \\(M\\)严格正定，搜索方向的求解可以用Cholesky factorization快速求解 \\(Md=-\\triangledown f(x),M=LL^{T}\\)，\\(L\\)是个下三角矩阵 如果是非凸函数（Hessian不定） 将Hessian进行Bunch-Kaufman Factorization，原问题转化为\\(LBL^{T}d=-\\triangledown f(x)\\) 其中，\\(L\\)是个下三角矩阵，\\(B\\)是个对角线由\\(1\\times 1\\)和\\(2\\times 2\\)的矩阵块组成的块对角阵 \\(1\\times 1\\)的标量一定是正数，\\(2\\times 2\\)的矩阵块的特征值是一正一负，我们需要把每个\\(2\\times 2\\)矩阵替换为和其最接近的\\(2\\times 2\\)正定矩阵，最终得到新的\\(\\tilde{B}\\)（正定），把\\(d\\)求解出来 上面\\(2\\times 2\\)矩阵的正定化就是把负的特征值都算出来，然后用一个\\(\\epsilon \\)去代替负特征值得到新的矩阵 缺点 仅仅保证了Hessian的正定，还是要把Hessian求出来，计算量大 Quasi Newton\u0026rsquo;s Method 拟牛顿法 牛顿法的问题 牛顿法需要函数在任何点的Hessian可逆且正定，条件比较苛刻 牛顿法的计算量太大 当\\(x\\)距离函数的最优解还比较远的时候，用二次函数进行近似的效果不好，这时候用牛顿法不仅计算量大，收敛还很慢；当\\(x\\)距离函数的最优解比较近的时候，二次函数的近似会好一些，收敛会很快。 Hessian拟合的函数的条件数可能会变得很大（poorly conditioned）。比如函数是一段直线和一段二次曲线拼接起来的，在直线部分计算Hessian去确定更新步长的话，会得到一个非常大的更新步长（曲率是0，对0取逆是无穷大） 拟牛顿法需要满足的一些特质 原理和修正阻尼牛顿法一样，设计一个\\(M\\)去近似\\(H\\) 收敛速度应该在牛顿法和最速梯度下降法之间 不需要计算完整的Hessian矩阵（低计算量） 线性方程\\(Md=-\\triangledown f(x)\\)存在闭式解 \\(M\\)不应该是一个稠密的阵，只需要在重要的的方向上对\\(H\\)做近似，尽可能稀疏 \\(d\\)一定得让函数下降（和梯度方向的夹角小于90度），其实就是\\(M\\)必须正定 \\(d\\)应该包含曲率信息（收敛要比梯度下降来的快），也就是满足\\(\\Delta g\\approx M^{k+1}\\Delta x\\) 拟牛顿法的核心思路 通过采样N对\\(\\Delta x\\)和\\(\\Delta g\\)来估计\\(\\Delta M\\) 同时，考虑到最终是要求\\(M^{-1}\\)，干脆直接估计\\(B=M^{-1}\\)，更新方向\\(\\Delta x=B\\Delta g \\) 估计\\(B\\)的时候避免计算Hessian矩阵 凸且光滑函数的BFGS方法 假设我们有了很多的\\(\\Delta x\\)和\\(\\Delta g\\)，怎么估计B呢？还是用优化迭代的思路： 初始化\\(B^{0}\\)为单位阵 迭代求解最优的\\(B\\)，迭代的思路如下： 我们希望迭代前后B的差距尽可能小：\\(min_{B^{k+1}}\\left| B^{k+1}-B^{k} \\right|^{2}\\) 其次，\\(B\\)需要满足一些约束： \\(B=B^{T}\\)，这是因为Hessian是对称阵，所以Hessian的逆也应该对称 \\(\\Delta x=B\\Delta g \\) 注意，单纯用差的二范数描述\\(B^{k}\\)和\\(B^{k+1}\\)的变化并不好，比如\\(\\begin{bmatrix} 100 \u0026amp; 1 \\\\ 1 \u0026amp; 100 \\end{bmatrix}\\)和\\(\\begin{bmatrix} 100 \u0026amp; 0.5 \\\\ 0.5 \u0026amp; 100 \\end{bmatrix}\\)的差值的二范数很小，但对于右上和左下角的元素来说变化和其自身的大小相比是巨大的，因此需要进行归一化 归一化后，优化目标变成：\\(min_{B^{k+1}}\\left| H^{\\frac{1}{2}}(B^{k+1}-B^{k})H^{\\frac{1}{2}} \\right|^{2}\\)，\\(B=H\\)为真实的Hessian矩阵，\\(H=\\int_{0}^{1}\\triangledown^{2}f\\left[(1-\\tau)x^{k}+\\tau x^{k+1}\\right]d\\tau\\) 我们本来就是套估计H，现在这里还要用到H，看起来是个鸡生蛋，蛋生鸡的问题，但实际上这个问题是有解析解的，与\\(H\\)无关。四个优化领域的大佬提出了BFGS方法，最终得到的更新公式如下： 注意：当\\(\\Delta g^{T}\\Delta x\u0026gt;0\\)时，我们可以保证BFGS更新的结果是正定的，从而保证\\(\\Delta x\\)的方向是函数值下降的方向，对凸函数而言，这是绝对成立的（可以回顾强凸性的定义）；非凸函数后面讨论 适用于凸且光滑函数的BFGS方法的流程 \\(g\\)是梯度 \\(d\\)是更新方向 \\(t\\)是line search方法确定的步长 缺点与问题 严格梯度单调性（严格凸函数）的条件过于苛刻，一般函数很难满足 曲率的计算在optimum附近有效，在远处反而是浪费算力 每次迭代的计算复杂度是优化变量的维度的平方，还是不够轻量 在非凸函数上是否能够保证收敛仍未知 在非光滑函数上能否正常使用仍未知 非凸但光滑函数的BFGS方法 在非凸函数上如何保证\\(\\Delta g^{T}\\Delta x\u0026gt;0\\)，从而保证更新方向是函数值的下降方向呢？答案是线搜索的时候满足Wolfe conditions weak wolfe conditions sufficient decrease condition保证了函数值的下降 curvature condition保证了这一步跨的足够大，从下山跨到上山 strong wolfe conditions strong和weak的区别在于对curvature condition加了个绝对值约束，不让这一步跨的太过头（跑到对面的山坡上），可以抑制震荡 但Wolfe conditions只能保证方向是下降方向，如何保证BFGS的收敛性呢？答案是cautious update(Li and Fukushima 2001) with mild conditions 只要函数满足如下两个条件，cautious update都可以保证BFGS的收敛性 函数有bounded sub-level sets 函数有lipschitz continuous grad 适用于非凸但光滑函数的BFGS方法的流程 与牛顿方法的收敛速度对比 速度上慢了一点点，但计算量少很多，综合看更具优势 Limited-memory BFGS(L-BFGS)方法 由于\\(B^{k+1}\\)是由\\(B^{k}\\)迭代计算得到的。所以\\(B^{k}\\)隐含了\\(B^{k-100}\\)的信息。但直觉上来说，\\(x^{k}\\)和\\(x^{k-100}\\)已经差的很远了，\\(x^{k-100}\\)处的曲率信息对\\(x^{k}\\)处的曲率信息的推导没有啥有效价值了，因此我们可以设置一个memory buffer，让\\(B^{k-m}\\)到\\(B^{k-1}\\)来决定\\(B^{k}\\)的取值，从而降低计算量。 L-BFGS方法的流程 就是把上面的Cautious-BFGS过程改成下面的\\(B^{k}\\)更新流程 上图左边方法的复杂度是\\(O(mn^{2})\\)，因为每个window size内的信息都被重复遍历并计算了。实际上每个循环中，我们只需要将窗口中的头元素去掉，末尾的新元素算进来即可，因此改成右边的计算过程后可以将复杂度简化到\\(O(mn)\\)，具体推导可以看Liu and Nocedal 1989. 与Newton和BFGS的对比 由于牺牲了部分历史信息，收敛速度相比BFGS更慢一些，但计算量从\\(O(n^{2})\\)降低到\\(O(mn)\\)，当\\(n\\)很大的时候，效率提升就非常大了，基本上是光滑非凸函数优化的第一选择 非凸且非光滑函数的L-BFGS方法 wolfe conditions方法选择 假设我们把strong wolfe conditions方法直接应用在非凸且非光滑函数上，看看会发生什么 回顾一下，strong wolfe conditions通过绝对值约束，将更新点的梯度压在0附近，但上图右侧的非光滑函数没有任何点的梯度在0附近，导致无法找到满足strong wolfe conditions的点 假设我们把weak wolfe conditions方法直接应用在非凸且非光滑函数上，看看效果 weak wolfe conditions方法可以保证能找到满足条件的更新点 结论：使用weak wolfe conditions方法处理nonsmooth函数 如何确定一个步长使得weak wolfe conditions被满足呢？ 对smooth函数，用拟合法确定步长 先初始化一个步长\\(\\alpha\\) 如果该步长满足weak wolfe conditions，直接返回 如果不满足weak wolfe conditions，根据\\(\\left ( x,{f}\\ \u0026lsquo;(x) \\right )\\)和\\(\\left ( x+\\alpha d,{f}\\ \u0026lsquo;(x+\\alpha d) \\right )\\)两个点去拟合二次函数，取二次函数的极值点作为新的步长，不断迭代，直到满足weak wolfe conditions 但是当函数nonsmooth（或者条件数很大）的时候，这种二次函数拟合的效果很差，导致求出来的极值点也很不理想，就不再适用了 对nonsmooth函数，用Lewis \u0026amp; Overton line search方法 注意点 \\(x_{0}\\)一定要取在可导的点，不能一上来就落在nonsmooth处 非凸且非光滑函数的L-BFGS方法流程 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/newton/","title":"Newton Methods"},{"content":"运算符重载 使用operator关键字引入重载函数 struct Str { int val = 3; auto operator () (int y = 3) { // 只有operator () 可以有缺省参数 return val + x; } auto operator + (const Str\u0026amp; x) { Str res; res.val = val + x.val; return res; } }; Str Add(const Str\u0026amp; x, const Str\u0026amp; y) { Str z; z.val = x.val + y.val; return z; } // auto自动推断返回值需要在C++11后面的版本才可以编译通过 auto operator + (const Str\u0026amp; x, const Str\u0026amp; y) { Str res; res.val = x.val + y.val; return res; } int main() { Str x; Str y; auto z = x + y; z = Add(x, y); z = x(4); // z.val = 3 + 4 z = x(); // z.val = 3 + 3 } 重载不能发明新的运算(比如不能创造一个@运算符)，不能改变运算的优先级与结合性，通常不改变运算含义。 函数参数个数与运算操作数个数相同，至少一个为类类型 除operator()外其他运算符不能有缺省参数 可以选择实现为成员函数与非成员函数 通常来说，实现为成员函数会以*this作为第一个操作数(注意==与\u0026lt;==\u0026gt;的重载可以不需要*this作为第一个操作数) 根据重载特性，可以将运算符进一步划分 可重载且必须实现为成员函数的运算符(=,[],(),-\u0026gt;与转型运算符) 可重载且可以实现为非成员函数的运算符 可重载但不建议重载的运算符(\u0026amp;\u0026amp;,||,逗号运算符) C++17中规定了相应的求值顺序但没有方式实现短路逻辑(短路逻辑即:A\u0026amp;\u0026amp;B,如果A为非，B不用执行) 不可重载的运算符(如 ?: 三元运算符) 相对来说比较特殊的运算符重载 对称运算符通常定义为非成员函数以支持首个操作数的类型转换 // 错误示范 struct Str { Str(int x) : val(x) {} auto operator + (const Str\u0026amp; input) { return Str(val + input.val); } int val; }; int main() { Str x(3); Str y = x + 4; // 通过，4会隐式转换为Str(4) Str z = 4 + x; // 不通过，最好不要将称运算符定义为成员函数 } // 正确示范 struct Str { Str(int x) : val(x) {} Str\u0026amp; operator= (const std::string\u0026amp; input) { val = static_cast\u0026lt;int\u0026gt;(input.size()); return *this; } // 定义为友元的原因是val是私有成员，而operator +是一个类外的运算符重载函数 friend auto operator + (const Str\u0026amp; input1, const Str\u0026amp; input2) { return Str(input1.val + input2.val); } // 注意这里要返回引用，因为输出流是不支持拷贝的 friend auto\u0026amp; operator \u0026lt;\u0026lt; (std::ostream\u0026amp; ostr, const Str\u0026amp; input) { ostr \u0026lt;\u0026lt; input.val; return ostr; } int\u0026amp; operator[] (int id) { // func1 return val; } int operator[] (int id) const { // func2 return val; } private: int val; }; int main() { Str x = 3; x = \u0026#34;1234\u0026#34;; Str y = 4 + x; // 合法，我们在类外定了operator + (const Str\u0026amp;, const Str\u0026amp;)函数，4会被隐式转换为Str(4) std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; y; // 合法 std::cout \u0026lt;\u0026lt; x[0]; // 合法 x[0] = 1; // 如果func1不是返回的引用，就不合法，因为右值一般不能出现在等号左边 const Str cx = 3; std::cout \u0026lt;\u0026lt; cx[0]; // 如果没有定义func2，就不合法，因为func1没用const修饰，可能对类本身进行修改 } 移位运算符一定要定义为非成员函数，因为其首个操作数类型需要是流类型(如上面代码所示) 赋值运算符也可以接受一般参数，比如上面代码中传入了一个string。 operator []通常返回引用，某些情况下不返回引用，比如上面的func2 自增、自减运算符的前缀、后缀重载法 Str\u0026amp; operator++ () { //如果()里是空的，对应前缀自增x++ ++val; return *this; } Str operator++ (int) { // 如果()里有个int变量，对应后缀自增++x，注意这里的()里面的变量没有任何意义，不会参与任何计算或者赋值 Str tmp(*this); ++val; return tmp; } // 从上面的代码可以看出，能用前缀自增就用前缀，更加高效 使用解引用运算符(*)与成员访问运算符(-\u0026gt;)模拟指针行为 struct Str { public: Str(int* p) : ptr(p) {} int\u0026amp; operator * () { return *ptr; } Str* operator -\u0026gt; () { return this; } int val = 5; private: int* ptr; }; int main() { int x = 100; Str ptr(\u0026amp;x); std::cout \u0026lt;\u0026lt; *ptr \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; ptr-\u0026gt;val \u0026lt;\u0026lt; std::endl; // C++编译的时候遇到-\u0026gt;就会去查看有没有operator-\u0026gt;重载，如果有重载的话，就把它当作一个类成员函数去调用 // 所以ptr-\u0026gt;val会被翻译为：ptr.operator-\u0026gt;()-\u0026gt;val，其中ptr.operator-\u0026gt;()的返回值就是一个Str*类型的指针 } 注意，\u0026quot;.\u0026ldquo;运算符不能重载 \u0026ldquo;-\u0026gt;\u0026ldquo;会递归调用\u0026rdquo;-\u0026gt;\u0026ldquo;操作 struct Str2 { Str2* operator -\u0026gt; () { return this; } int val2 = 123; }; struct Str { Str2 operator -\u0026gt; () { return Str2{}; } int val = 5; }; int main() { Str ptr = Str(); std::cout \u0026lt;\u0026lt; ptr-\u0026gt;val2 \u0026lt;\u0026lt; std::endl; // 由于Str的operator-\u0026gt;()函数返回的是Str2，不是一个指针类型，所以编译期还会继续调用Str2的operator-\u0026gt;()函数，直到返回一个指针为止 // ptr.operator-\u0026gt;().operator()-\u0026gt;val2 } 使用函数调用运算符构造可调用对象(lambda表达式就是通过这个手段实现的) struct Str { Str(int p) : val(p) {} bool operator () (int input) { return val++ \u0026lt; input; } private: int val; }; int main() { Str obj(100); std::cout \u0026lt;\u0026lt; obj(99) \u0026lt;\u0026lt; std::endl; // 0 std::cout \u0026lt;\u0026lt; obj(102) \u0026lt;\u0026lt; std::endl; // 1 std::cout \u0026lt;\u0026lt; obj(102) \u0026lt;\u0026lt; std::endl; // 0, 因为val++ } 类型转换运算符 函数声明为operator type() const struct Str { Str(int p) : val(p) {} operator int() const { return val; } friend auto operator + (Str a, Str b) { return Str(a.val + b.val); } private: int val; }; int main() { Str obj(100); static_cast\u0026lt;Str\u0026gt;(100); // 通过 static_cast\u0026lt;int\u0026gt;(obj); // 通过 int v = obj; // 通过 std::cout \u0026lt;\u0026lt; v \u0026lt;\u0026lt; std::endl; obj + 3; // 不通过，编译器既可以选择把obj转为int再相加，也可以把3转换为Str类型再相加（上面实现了Str的+友元重载） // 解决方案就是在Str的构造函数或者型转换运算符重载函数前面加上explicit关键字 // 如果在构造函数前加explicit，3就不能隐式转换为Str类型了 // 注意，不能两个函数前同时加上explicit，如果都加了也编译不过，因为没有任何隐式转换发生的话，Str和int无法执行+。 // 如果在两个函数前同时加上explicit，代码就要这么改 obj + static_cast\u0026lt;Str\u0026gt;(3); static_cast\u0026lt;int\u0026gt;(obj) + 3; } 与单参数构造函数一样，都引入了一种类型转换方式 注意避免引入歧义性与意料之外的行为 通过explicit引入显示类型转换(参考上面的代码) explicit bool的特殊性：用于条件表达式时会进行隐式类型转换 struct Str { explicit Str(int p) : val(p) {} explicit operator bool() const { return val == 0; } private: int val; }; int main() { Str obj(100); std::cout \u0026lt;\u0026lt; obj \u0026lt;\u0026lt; std::endl; // 不通过，因为上面有explicit修饰，不能隐式转换了 if (obj) { // 通过，即使定义了explicit，但这里仍然会进行隐式类型转换 std::cout \u0026lt;\u0026lt; 1 \u0026lt;\u0026lt; std::endl; } else { std::cout \u0026lt;\u0026lt; 0 \u0026lt;\u0026lt; std::endl; } auto var = obj ? 1 : 0; // 通过 std::cout \u0026lt;\u0026lt; var \u0026lt;\u0026lt; std::endl; } C++20中对==与\u0026lt;==\u0026gt;的重载 在C++20之前，如果我们想重载大小比较，我们需要实现6个重载，==, !=, \u0026gt;=, \u0026lt;=, \u0026gt;, \u0026lt;。 在C++20之后，只需要重载==, \u0026lt;==\u0026gt;这两个就可以完成对上面6个功能的定义 通过==定义!= 隐式交换操作数 #include \u0026lt;compare\u0026gt; struct Str { Str(int p) : val(p) {} friend bool operator == (Str obj, int obj2) { return obj.val == obj2; } auto operator \u0026lt;==\u0026gt; (int x) { return val \u0026lt;==\u0026gt; x; } private: int val; }; int main() { Str obj(100); std::cout \u0026lt;\u0026lt; (obj == 100) \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; (obj != 100) \u0026lt;\u0026lt; std::endl; // C++20之后通过 std::cout \u0026lt;\u0026lt; (100 == obj) \u0026lt;\u0026lt; std::endl; // C++20之后通过，C++20会先尝试找(int, Str)，如果找不到会继续找(Str, int) std::cout \u0026lt;\u0026lt; (100 \u0026gt;= obj) \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; (obj \u0026gt;= 100) \u0026lt;\u0026lt; std::endl; } 通过\u0026lt;==\u0026gt;定义多种比较逻辑(参考上main的代码) 注意\u0026lt;==\u0026gt;可以返回的类型，strong_ordering, weak_ordering, partial_ordering ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/class/operator/","title":"Operator"},{"content":"数值模版参数 \u0026amp; 模版模版参数 模版接收（编译期常量）数值作为模版参数 template \u0026lt;int a\u0026gt; template \u0026lt;int a\u0026gt; int fun(int x) { return x + a; } template\u0026lt;3\u0026gt;(5); // 8 template \u0026lt;T, T value\u0026gt; template \u0026lt;T, T a\u0026gt; int fun(int x) { return x + a; } template\u0026lt;int, 3\u0026gt;(5); // 8 template\u0026lt;bool, 3\u0026gt;(5); // 编译失败 template \u0026lt;auto value\u0026gt;，C++17开始 template \u0026lt;auto a\u0026gt; void fun() {} fun\u0026lt;3\u0026gt;(); // 通过 fun\u0026lt;true\u0026gt;(); // 通过 接收字面值对象和浮点值作为模板参数，C++20开始 clang 20目前不支持用浮点数作为模版参数 接收模版作为模版参数 template \u0026lt;typename T\u0026gt; class C{}; template\u0026lt;template \u0026lt;typename T\u0026gt; class T2\u0026gt; // 由于这里的T不重要，可以删掉 // template\u0026lt;template \u0026lt;typename\u0026gt; class T2\u0026gt; // C++17开始，这里的class可以换成typename // template\u0026lt;template \u0026lt;typename\u0026gt; typename T2\u0026gt; void fun() { T2\u0026lt;int\u0026gt; tmp; } int main() { fun\u0026lt;C\u0026gt;(); } C++17开始，模版的模版实参会考虑缺省实参(clang可能不行) 错误例子 template \u0026lt;typename T1, typename T2\u0026gt; class C{}; template \u0026lt;template \u0026lt;typename\u0026gt; typename T\u0026gt; void fun() {} int main() { fun\u0026lt;C\u0026gt;(); // 报错，C有两个模版形参，而fun的模板参数的模板形参只有1个 } 修正方法1 template \u0026lt;typename T1, typename T2\u0026gt; class C{}; template \u0026lt;template \u0026lt;typename, typename\u0026gt; typename T\u0026gt; void fun() {} int main() { fun\u0026lt;C\u0026gt;(); // 通过，C有两个模版形参，fun的模板参数的模板形参也是2个 } 修正方法2 template \u0026lt;typename T1, typename T2 = int\u0026gt; class C{}; template \u0026lt;template \u0026lt;typename\u0026gt; typename T\u0026gt; void fun() {} int main() { fun\u0026lt;C\u0026gt;(); // 通过，C有两个模版形参，fun的模板参数的模板形参只有1个，但是C的第二个模版形参是缺省值 } 别名模版 \u0026amp; 变长模版 可以使用using引入别名模版 为模版本身引入别名 // demo 1 template \u0026lt;typename T\u0026gt; using AddPointer = T*; int main() { AddPointer\u0026lt;int\u0026gt; x; // int* x } // demo 2 template \u0026lt;typename T\u0026gt; struct Alloc{}; template\u0026lt;typename T\u0026gt; using Vec = vector\u0026lt;T, Alloc\u0026lt;T\u0026gt;\u0026gt;; Vec\u0026lt;int\u0026gt; v; 为类模版成员引入别名 template \u0026lt;typename T\u0026gt; struct B { using TP = T*; }; template \u0026lt;typename T\u0026gt; using AddPointer = typename B\u0026lt;T\u0026gt;::TP; int main() { AddPointer\u0026lt;int\u0026gt; x; // int* x } 别名模版不支持特化 // 当T是int时返回T\u0026amp;，其他时候返回T*，但是下面这种写法不行 template \u0026lt;typename T\u0026gt; using MyPointer = T*; template \u0026lt;\u0026gt; using MyPointer\u0026lt;int\u0026gt; = int\u0026amp;; 可以基于类模版的特化引入别名，以实现类似于特化的功能 template \u0026lt;typename T\u0026gt; class B { using type = T*; }; template \u0026lt;\u0026gt; struct B\u0026lt;int\u0026gt; { using type = int\u0026amp;; }; template \u0026lt;typename T\u0026gt; using MyPointer = typename B\u0026lt;T\u0026gt;::type; int main() { MyPointer\u0026lt;float\u0026gt; x; // float* x; } 注意与实参推导的关系(有点复杂，可以复习C++课程视频) 变长模版(Variadic Template) 模版形参包 类型 \u0026hellip; Args template \u0026lt;int ... a\u0026gt; void fun() {} int main() { fun\u0026lt;1, 2, 3\u0026gt;(); // 可以输入多个int型参数 } typename | class \u0026hellip; Args template \u0026lt;typename ... a\u0026gt; void fun() {} int main() { fun\u0026lt;int, char, double\u0026gt;(); } template\u0026lt;形参列表\u0026gt; typename | class \u0026hellip; Args template \u0026lt;template\u0026lt;typename\u0026gt; class ... a\u0026gt; void fun() {} int main() { fun\u0026lt;template1, template2, template3\u0026gt;(); } 函数参数包(声明符的一种形式，出现于变参函数模版的函数形参列表中) Args \u0026hellip; args template \u0026lt;typename... T\u0026gt; void fun(T... args) {} int main() { fun\u0026lt;int, char, double\u0026gt;(); // 编译报错，模版形参包的参数数量与函数形参的输入数量要一致 fun\u0026lt;int, char, double\u0026gt;(4, \u0026#39;c\u0026#39;, 4.3); // 编译通过 } 注意变长模版参数的位置 在主类模版中，模版形参包必须是模版形参列表的最后一个形参 在特化模版中，模版形参包无需是模版形参列表的最后一个形参 template \u0026lt;typename... T\u0026gt; class C; template \u0026lt;typename T1, typename T2\u0026gt; class B; // 特化，合法 template \u0026lt;typename... T, typename T2\u0026gt; class B\u0026lt;C\u0026lt;T...\u0026gt;, T2\u0026gt;{}; 在函数模版中，模版函数包可以在列表中稍早出现，只要其后所有的形参均可以从函数实参推导或者拥有默认实参即可 sizeof\u0026hellip; (C++11)：获取形参包中参数的个数 template \u0026lt;typename... T\u0026gt; void fun(T... args) { std::cout \u0026lt;\u0026lt; sizeof...(T) \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; sizeof...(args) \u0026lt;\u0026lt; std::endl; } 包展开 \u0026amp; 折叠表达式 (C++11)通过包展开技术操作变长模版参数 void fun() {} template \u0026lt;typename U, typename... T\u0026gt; void fun(U u, T... args) { std::cout \u0026lt;\u0026lt; u \u0026lt;\u0026lt; std::endl; fun(args...); } int main() { fun(1, 2, \u0026#34;hello\u0026#34;, \u0026#39;c\u0026#39;); // 递归调用，依次打印出“1”，“2”，“hello”，“c” // 最后一次调用的fun是上面的无形参的fun函数 } (C++17)折叠表达式 对C++11版本的包展开技术进行了简化 template \u0026lt;typename... T\u0026gt; void fun(T... args) { ((std::cout \u0026lt;\u0026lt; agrs \u0026lt;\u0026lt; std::endl), ...); } int main() { fun(1, 2, \u0026#34;hello\u0026#34;, \u0026#39;c\u0026#39;); } 多种格式的折叠表达式语法（见cppreference） 折叠表达式用于表达式求值，无法处理输入（输出）是类型与模版的情况 完美转发 \u0026amp; lambda表达式模版 (C++11)完美转发：std::forward函数 void g(int\u0026amp;) { std::cout \u0026lt;\u0026lt; \u0026#34;l-ref\u0026#34; \u0026lt;\u0026lt; std::endl; } void g(int\u0026amp;\u0026amp;) { std::cout \u0026lt;\u0026lt; \u0026#34;r-ref\u0026#34; \u0026lt;\u0026lt; std::endl; } template \u0026lt;typename T\u0026gt; void fun(T\u0026amp;\u0026amp; input) { // 这里的T\u0026amp;\u0026amp;不是右值引用，是万能引用 // 当输入是左值的时候，会实例化为T\u0026amp; // 当输入是右值的时候，会实例化为T\u0026amp;\u0026amp;(这里是表示右值引用的意思) g(input); // l-ref，因为右值引用的变量是左值，但是这样就不合理了，因为我们传入的\u0026#34;3\u0026#34;是一个右值 g(std::forward\u0026lt;T\u0026gt;(input)); // r-ref，加上forward后，配合T\u0026amp;\u0026amp;，就可以完美转发 } int main() { fun(3); } 通常与万能引用结合使用 template \u0026lt;typename T\u0026gt; void fun(T input) { g(std::forward\u0026lt;T\u0026gt;(input)); } int main() { int x = 3; fun(x); // r-ref，因为T input是拷贝传递，不是万能引用了，所以完美转发就不存在了(需要求证) } 同时处理传入参数是左值和右值的情形 (C++20)lambda表达式模版 auto glambda = []\u0026lt;class T\u0026gt;(T a, auto\u0026amp;\u0026amp; b) { return a \u0026lt; b; }; auto f = []\u0026lt;typename ...Ts\u0026gt;(Ts\u0026amp;\u0026amp; ...ts) { return foo(std::foward\u0026lt;Ts\u0026gt;(ts), ...); }; 消除歧义 本质 在模版当中用了一个依赖名称，而这个依赖名称依赖于模版形参可能有不同的解释 如果这个依赖名称是个类型，就要加typename关键字进行描述 如果这个依赖名称是个模版，就要加template关键字进行描述 使用typename表示一个依赖名称是类型而非静态数据成员 template \u0026lt;typename T\u0026gt; void fun() { T::internal* p; // 如果internal是一个类型，那这里就是定义一个指针 // 如果internal是一个静态成员变量，那这里就是乘法语句 typename T::internal* p; // 用typename明确表明这里的T::internal是一个类型，就没有歧义了 } 使用template表示一个依赖名称是模版 struct Str { template \u0026lt;typename T\u0026gt; static void internal() {} // 静态成员函数 void internal2() {} // 一般成员函数 }; template \u0026lt;typename T\u0026gt; void fun() { T::internal\u0026lt;int\u0026gt;(); // 编译失败，编译器不能确定T::internal是否是一个变量，如果是的话这里的\u0026lt;会被认为是一个小于号 T::template internal\u0026lt;int\u0026gt;(); // 编译成功，指明这里是一个模版 T obj; obj.internal2\u0026lt;int\u0026gt;(); // 编译失败 obj.template internal2\u0026lt;int\u0026gt;(); // 编译成功 } int main() { fun\u0026lt;Str\u0026gt;(); } 变量模版(C++14) demo 1 template\u0026lt;typename T\u0026gt; T pi = (T)3.141592653; int main() { pi\u0026lt;float\u0026gt;; // 3.14159 pi\u0026lt;int\u0026gt;; // 3 } demo 2 template\u0026lt;typename T\u0026gt; unsigned MySize = sizeof(T); template\u0026lt;typename T, unsigned v\u0026gt; unsigned EqSize = (sizeof(T) == v); int main() { MySize\u0026lt;float\u0026gt;; // 4 MySize\u0026lt;int\u0026gt;; // 4 EqSize\u0026lt;int, 4\u0026gt;; // true EqSize\u0026lt;int, 2\u0026gt;; // false } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/template/others/","title":"Other knowledges of template"},{"content":"pragma once \u0026amp; ifndef 一般情况下用ifndef，两者功能一样，有细微差别。 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/others/","title":"Others"},{"content":"L2 Penalty Method 问题形式 等式约束 原问题： \\(min_{x}f(x) \\)， \\(s.t. c_{i}(x)=0,i\\in \\varepsilon \\) 罚函数形式： \\(P_{E}(x,\\sigma)=f(x)+\\frac{1}{2}\\sigma\\sum_{i\\in \\varepsilon}c_{i}^{2}(x) \\) 上面使用的是二阶罚函数法，求解精度取决于 \\(\\sigma \\)的大小，但无法求得精确解 不等式约束 原问题： \\(min_{x}f(x) \\)， \\(s.t.c_{i}(x)\\leq 0,i\\in I \\) 罚函数形式： \\(P_{I}(x,\\sigma)=f(x)+\\frac{1}{2}\\sigma\\sum_{i\\in I}max\\left[c_{i}(x),0\\right]^{2} \\) 上面使用的也是二阶罚函数法，但罚函数的二阶导不连续，无法求得精确解 迭代过程 直接法 直接取一个很大的 \\(\\sigma \\)，优化一次得到最优解 sequential方法 取 \\(\\sigma=1 \\)，优化得到最优解 \\(x^{1} \\) 取 \\(\\sigma=10 \\)，以 \\(x^{1} \\)为初值，优化得到最优解 \\(x^{2} \\) 重复该过程，直到 \\(\\sigma \\)足够大 具体用哪种方法取决于对耗时和精度的要求 使用场景 约束最好具有具体的物理意义，因为该方法是得不到精确解的，且最终解实际上会一定程度上违反约束 对精度要求不是很高，在 \\(10^{-3} \\)量级左右 L1 Penalty Method 问题形式 原问题： \\(min_{x}f(x) \\)， \\(s.t.c_{i}(x)=0,i\\in\\varepsilon \\)， \\(c_{j}(x)\\leq 0,j\\in I \\) 罚函数形式： \\(P(x,\\sigma)=f(x)+\\sigma\\sum_{i\\in \\varepsilon}\\left|c_{i(x)}\\right|+\\sigma\\sum_{j\\in I}max\\left[c_{j}(x),0\\right] \\) 上面使用的是一阶罚函数法，罚函数的一阶导不连续，当 \\(\\sigma \\)充分大时（不用像L2方法一样取那么大），可以得到精确解 虽然可以得到精确解，但由于其non smooth的性质，lbfgs在求解该类型问题时收敛速度是不能保证的 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/penalty_methods/","title":"Penalty Methods"},{"content":"背景 PHR是Powell、Hestenes和Rockafellar三个人名的缩写，前两个人在等式约束的优化问题中提出了增广拉格朗日乘子法，第三个作者将其推广到不等式约束上，并给出了新的解释 等式约束 问题形式 $$min_{x\\in\\mathbb{R^{n}}}f(x)$$ $$s.t.h(x)=0$$ 推导 Uzawa\u0026rsquo;s method 通过冯诺依曼定理将minmax问题转化为maxmin（对偶）问题，只需要满足原问题是严格凸的，原问题和对偶问题的最优解就是一致的 $$d(\\lambda):=min_{x}f(x)+\\lambda^{T}h(x)$$ 如果原问题不是严格凸，上面的式子求得的\\(x^{*}\\)就不是唯一的，导致\\(d(\\lambda)\\)不是光滑的，梯度\\(\\triangledown d(\\lambda)\\)有可能不存在 PHR Augmented Lagrangian Method 核心思路 不直接依赖冯诺依曼定理将minmax问题转化为maxmin问题。 再来观察一下原问题 $$min_{x}max_{\\lambda}f(x)+\\lambda^{T}h(x)$$ 现在的主要难点在于，函数的max部分，取值要么是无穷大要么是一个定值，他是一个non smooth的函数，一个很直观的想法是，把这部分近似一下，变成一个smooth的函数： 我们在右边加一个proximal point项。其思路是，我们并不知道\\(\\lambda\\)最优应该取什么值，但可以先假设\\(\\lambda\\)的最优解是\\(\\bar\\lambda\\)，那么我们在优化的时候尽可能让\\(\\lambda\\)靠近\\(\\bar\\lambda\\) $$min_{x}max_{\\lambda}f(x)+\\lambda^{T}h(x)-\\frac{1}{2\\rho}\\left|\\lambda-\\bar{\\lambda}\\right|$$ $$(\\rho\u0026gt;0)$$ 加上这个proximal point项（正则项）之后，原问题的max部分就是对\\(\\lambda\\)的线性项加上对\\(\\lambda\\)的二次项，这对\\(\\lambda\\)来说是一个连续且严格凸的函数，且二次函数的最优值是有解析解的 至此，该问题变成了一个针对\\(x\\)无约束优化问题 但目前我们只是得到了对原问题的minmax解的一个粗略估计，仍需要持续迭代提高精度，从两个角度进行提升 让正则项趋近于0，即\\(\\rho\\to\\infty\\) 不断更新更准确的\\(\\bar\\lambda\\)，\\(\\bar\\lambda\\leftarrow\\lambda^{*}(\\bar\\lambda)\\)。因为第一次优化时，\\(\\bar\\lambda\\)是我们猜的，优化结束后，我们得到的\\(\\lambda^{*}\\)比之前猜的\\(\\bar\\lambda\\)更接近最优解，所以第二次就把\\(\\lambda^{*}\\)作为下一轮的\\(\\bar\\lambda\\)来不断提高精度 注意，由于我们不断地在更新更精确的\\(\\bar\\lambda\\)，这意味着\\(\\left|\\lambda-\\bar{\\lambda}\\right|\\)本身不断在接近0，所以\\(\\rho\\)的取值不需要真的趋向无穷大， 慢慢增长到一定程度大即可,取到1000就可以了 现在我们不需要借助冯诺依曼定理最minmax问题进行对换了，也就不需要保证\\(f(x)\\)严格凸 原问题的拉格朗日函数 + 增广项 = 增广拉格朗日法 一般来说，我们常把上面的式子整理为如下形式（灰色部分可以省略），进行迭代 不等式约束 问题形式 $$min_{x\\in R^{n}}f(x)$$ $$s.t.g(x)\\leq0$$ 原问题变形（不等式变等式） $$min_{x\\in R^{n},s\\in R^{m}}f(x)$$ $$s.t.g(x)+\\left[s\\right]^{2}\\leq0$$ 其中\\(\\left[\\cdot\\right]^{2}\\)表示element-wise squaring 等式约束+不等式约束 问题形式 $$min_{x\\in R^{n}}f(x)$$ $$s.t.g(x)\\leq0,h(x)=0$$ PHR_Augmented Lagrangian $$\\pounds_{\\rho}(x,\\lambda,\\mu):=f(x)+\\frac{\\rho}{2}\\lbrace\\left|h(x)+\\frac{\\lambda}{\\rho}\\right|^{2}+\\left|max\\left[g(x)+\\frac{\\mu}{\\rho},0\\right])\\right|\\rbrace-\\frac{1}{2\\rho}\\lbrace\\left|\\lambda\\right|^{2}+\\left|\\mu\\right|^{2}\\rbrace$$ 丄式的最后一项一般都省略掉 $$\\rho\u0026gt;0,\\mu\\geq0$$ 迭代步骤 $$\\Bigg\\lbrace\\begin{matrix}x\\leftarrow argmin_{x}\\pounds_{\\rho}(x,\\lambda,\\mu) \\ \\lambda\\leftarrow\\lambda+\\rho h(x)\\\\mu\\leftarrow max\\left[\\mu+\\rho g(x),0\\right]\\\\rho\\leftarrow min\\left[(1+\\lambda)\\rho,\\beta\\right]\\end{matrix}$$ 参数初始化 $$\\rho=1,\\lambda=\\mu=0,\\gamma=1,\\beta=10^{3}$$ 内层循环退出条件（求\\(argmin_{x}\\)） \\(\\xi\\)从一个正数逐渐收敛到0 外层迭代退出条件 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/phr_alm/","title":"PHR Augmented Lagrangian Method"},{"content":"强化学习分类 策略梯度算法 直接用神经网络表示策略 神经网络输出N维的向量，每一维表示选择该动作的概率大小 $$Net(state)=[P_{action1},P_{action2},P_{action3},\u0026hellip;]$$ 值函数算法 用神经网络拟合Q或者V函数 得到Q之后，利用贪婪策略等选择下一步动作 \\(Net(state,action)=Q\\)或者\\(Net(state)=V\\) Actor-Critic 学习值函数 学习策略 介于上面两者之间 策略梯度算法优缺点 优点 更好的收敛性 有效处理高维和连续的动作空间 能够学到随机策略 不会导致策略退化 缺点 容易陷入局部最优 难以评价一个策略，评价结果方差很大 策略退化 模型的能力不够导致 值函数估计不准导致 我们从经典的A2C算法入手讲解策略梯度算法 优化目标 $$max_{\\theta }U(\\theta )=max\\sum_{\\tau }P(\\tau |\\theta )R(\\tau )$$ 其中，\\(\\theta \\)表示策略网络的参数；\\(\\tau \\)表示一段状态转移轨迹；\\(R(\\tau ) \\)表示该轨迹的最终回报值；\\(P(\\tau |\\theta )\\)表示当策略网络的参数为\\(\\theta \\)时，出现\\(\\tau \\)的概率大小。 在一个固定的环境再，一般来说，\\(R(\\tau ) \\)是稳定不变的。 优化方法 梯度表达式 $$\\frac{\\partial U(\\theta )}{\\partial \\theta }=\\frac{\\partial \\sum_{\\tau }P(\\tau |\\theta )R(\\tau )}{\\partial \\theta }$$ 似然率角度梯度求解 似然率梯度的理解 \\(\\frac{\\partial logP(\\tau |\\theta )}{\\partial \\theta }\\)是轨迹\\(\\tau \\)的出现概率随\\(\\theta \\)变化最陡的方向。 沿正方向，轨迹出现的概率会变大 沿负方向，轨迹出现的概率会变小 \\(R(\\tau )\\)控制了参数更新的方向和步长，R是正的，就让轨迹出现的概率变大，并且R越大，步长的幅度越大；相反亦然。 最终增大了高回报率轨迹出现的概率，减少了低回报率轨迹出现的概率 轨迹分解到状态 算法流程 Actor-Critic 上面的reinforce算法中，\\(g_{t}\\)的方差非常大，为了减小方差，我们引入了Critic函数\\(Q_{w}(s_{k},a_{k})\\approx \\sum_{t=k}^{T}\\gamma ^{t-k}R(s_{k},a_{k})\\)代替\\(g_{t}\\) 再进一步，由于每个Q都是正的，会导致网络对于任何轨迹都想提高其出现的概率，因此，引入一个基线。基线的选择为当前状态的V值。由此得到一个优势函数： $$A^{\\pi _{\\theta }}(s,a)=Q^{\\pi _{\\theta }}(s,a)-V^{\\pi _{\\theta }}(s)$$ 上面的方法需要设计一个Q函数一个V函数，为了简化，我们直接用TD误差代替优势函数。TD误差为： \\(\\delta ^{\\pi _{\\theta }}=r+V^{\\pi _{\\theta }}({s}\u0026rsquo;)-V^{\\pi _{\\theta }}(s)\\)其中，\\({s}\u0026rsquo; \\)是\\(s\\)的后一个状态 总结 其中，Advantage Actor-Critic又叫A2C，由于TD Actor-Critic是Advantage Actor-Critic的无偏估计，所以实际在使用A2C的时候，都是用的TD Actor-Critic A2C需要多进程来打破训练数据之间的相关性 其他策略梯度算法简单介绍 确定性梯度策略算法DPG 特性 直接采用确定性动作输出：\\(a=\\pi (s)\\) 可以用于高维和连续动作的情况 常规的策略梯度方法无法用到高维和连续动作空间 梯度求解 过去一直认为无模型情况下确定性策略梯度不存在 DPG证明了梯度存在，并建立了其与Q函数的关系 DDPG 核心思路 Continuous Control with Deep Reinforcement Learning (ICRL2016) 结合了 DQN 和 DPG 利用随机过程产生探索性动作 算法流程 A3C 论文来源 Asynchronous Methods for Deep Reinforcement Learning (ICML2016) 问题提出 Online 的算法和 DNN 结合后不稳定 (样本关联性) 解决方案 创建多个agent，在多个环境中执行异步学习构建batch(多线程) 来自不同环境的样本无相关性 不依赖于 GPU 和大型分布式系统 不同线程使用了不同的探索策略，增加了探索量 算法流程 A2C 来源 OpenAI对A3C进行了改进，把异步变成了同步，等所有线程的动作执行完毕得到reward后一起拿来更新，可以用GPU完成该动作，效率高 当batch_size较大时效果好 策略梯度知识图谱 扩展(其他策略梯度算法) 自然梯度法：寻找策略更新最快的方向 信赖域策略优化算法(TRPO)：研究了更新步长的选择，步长选择在策略梯度中非常重要，但实现非常复杂 近端策略优化(PPO)：对TRPO的改进，使实现非常简单，实际使用中，效果比较好甚至最好的方案 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/policy-gradient/","title":"Policy Gradient"},{"content":"前言 最近用pyqt写标注工具，记录一下学习的内容。\n头文件 import sys from python_qt_binding.QtGui import * from python_qt_binding.QtCore import * from python_qt_binding.QtWidgets import * UI类\u0026amp;主函数 class MyUI(QWidget): def __init__(self): QWidget.__init__(self) # 数据成员 self.buttons = {} self.jump_to = None # 界面初始化 self.layout = QVBoxLayout() # 从上到下竖直布局 self.init_ui() # 初始化界面 def init_ui(self): self.init_name_buttons() self.init_operation_buttons() self.init_select_buttons() self.setLayout(self.layout) def init_name_buttons(self): row = QHBoxLayout() # 从左到右水平布局 label = QLabel(self) label.setFixedSize(120, 30) label.setText(\u0026#34;Name\u0026#34;) # 标签名称 row.addWidget(label) button_group = QButtonGroup(self) # 构建一个按钮群组 self.buttons[\u0026#39;name\u0026#39;] = {} self.buttons[\u0026#39;name\u0026#39;][\u0026#39;luyifan\u0026#39;] = QRadioButton(\u0026#34;LuYifan\u0026#34;) # 新建一个radio按钮 self.buttons[\u0026#39;name\u0026#39;][\u0026#39;luyifan\u0026#39;].setChecked(False) # 默认为False button_group.addButton(self.buttons[\u0026#39;name\u0026#39;][\u0026#39;luyifan\u0026#39;]) # 添加到按钮群组中去 row.addWidget(self.buttons[\u0026#39;name\u0026#39;][\u0026#39;luyifan\u0026#39;]) # 添加到该行(row) self.buttons[\u0026#39;name\u0026#39;][\u0026#39;xuqi\u0026#39;] = QRadioButton(\u0026#34;XuQi\u0026#34;) self.buttons[\u0026#39;name\u0026#39;][\u0026#39;xuqi\u0026#39;].setChecked(False) button_group.addButton(self.buttons[\u0026#39;name\u0026#39;][\u0026#39;xuqi\u0026#39;]) row.addWidget(self.buttons[\u0026#39;name\u0026#39;][\u0026#39;xuqi\u0026#39;]) self.buttons[\u0026#39;name\u0026#39;][\u0026#39;unknow\u0026#39;] = QRadioButton(\u0026#34;Unknow\u0026#34;) self.buttons[\u0026#39;name\u0026#39;][\u0026#39;unknow\u0026#39;].setChecked(True) button_group.addButton(self.buttons[\u0026#39;name\u0026#39;][\u0026#39;unknow\u0026#39;]) row.addWidget(self.buttons[\u0026#39;name\u0026#39;][\u0026#39;unknow\u0026#39;]) self.layout.addLayout(row) # 将这一行控件添加到layout排布中去 def init_select_buttons(self): row = QHBoxLayout() # 水平排布 label = QLabel(self) label.setText(\u0026#34;Eat\u0026#34;) row.addWidget(label) self.food_button = QComboBox(self) self.food_button.setFixedSize(120, 30) self.food_button.addItem(\u0026#34;Healthy Food\u0026#34;) self.food_button.addItem(\u0026#34;Hot Pot\u0026#34;) self.food_button.addItem(\u0026#34;Rice\u0026#34;) self.food_button.addItem(\u0026#34;Noodle\u0026#34;) self.food_button.activated.connect(self.on_select_buttons) row.addWidget(self.food_button) self.layout.addLayout(row) def on_select_buttons(self): if self.food_button.currentText() == \u0026#34;Healthy Food\u0026#34;: print (\u0026#34;Healthy Food\u0026#34;) elif self.food_button.currentText() == \u0026#34;Hot Pot\u0026#34;: print (\u0026#34;Hot Pot\u0026#34;) elif self.food_button.currentText() == \u0026#34;Rice\u0026#34;: print (\u0026#34;Rice\u0026#34;) elif self.food_button.currentText() == \u0026#34;Noodle\u0026#34;: print (\u0026#34;Noodle\u0026#34;) def init_operation_buttons(self): row = QHBoxLayout() # 水平排布 button = QPushButton() button.setText(\u0026#34;jump to\u0026#34;) # 按钮名称 button.setFixedSize(80, 30) button.clicked.connect(self.on_jump_button) # 按下后触发on_save_button函数 row.addWidget(button) # 将该按钮添加到该行(row) self.jump_to = QTextEdit(self) self.jump_to.setFixedSize(80, 30) row.addWidget(self.jump_to) button = QPushButton() button.setText(\u0026#34;save\u0026#34;) # 按钮名称 button.setFixedSize(80, 30) button.clicked.connect(self.on_save_button) # 按下后触发on_save_button函数 row.addWidget(button) # 将该按钮添加到该行(row) button = QPushButton() button.setText(\u0026#34;reset\u0026#34;) # 按钮名称 button.setFixedSize(80, 30) button.clicked.connect(self.on_reset_button) # 按下后触发on_reset_button函数 row.addWidget(button) # 将该按钮添加到该行(row) self.layout.addLayout(row) # 将这一行控件添加到layout的竖直排布中去 def on_jump_button(self): pass def on_save_button(self): if self.buttons[\u0026#39;name\u0026#39;][\u0026#39;luyifan\u0026#39;].isChecked(): print (\u0026#39;luyifan\u0026#39;) elif self.buttons[\u0026#39;name\u0026#39;][\u0026#39;xuqi\u0026#39;].isChecked(): print (\u0026#39;xuqi\u0026#39;) elif self.buttons[\u0026#39;name\u0026#39;][\u0026#39;unknow\u0026#39;].isChecked(): print (\u0026#39;unknow\u0026#39;) def on_reset_button(self): self.buttons[\u0026#39;name\u0026#39;][\u0026#39;luyifan\u0026#39;].setChecked(False) self.buttons[\u0026#39;name\u0026#39;][\u0026#39;xuqi\u0026#39;].setChecked(False) self.buttons[\u0026#39;name\u0026#39;][\u0026#39;unknow\u0026#39;].setChecked(True) if __name__ == \u0026#39;__main__\u0026#39;: app = QApplication( sys.argv ) myui = MyUI() myui.resize( 500, 500 ) myui.show() app.exec_() 解释 程序简单易懂，其实就是从上到下，一行一行写控件。每一行又是从左到右写控件。 对于触发类型的按钮，按下后会触发对应的函数，这种按钮的定义可以使用局部变量。 对于记录用户输入信息的按钮，需要定义为全局变量(self.buttons)，用于在后期(不同的函数中)读取用户输入。 控件类型 QLabel 标签，用于显示文本信息。\nQRadioButton 按钮，点击可以改变其状态(True or False)。\nQButtonGroup button群组，管理一组QRadioButton，确保群组内每时刻只有一个QRadioButton被置为True。\nQComboBox 下拉选择框，提供了多个可能的选项，让用户选取其中一个\nQPushButton 触发按钮，按下后触发对应的函数\n","date":"2025-04-02T10:32:39+08:00","permalink":"/post/tools/gui-dev/pyqt/","title":"PyQt Notes"},{"content":"背景 模版的问题：没有很好的方法对模版参数引入相应的限制 template \u0026lt;typename T\u0026gt; void fun() { std::cout \u0026lt;\u0026lt; T \u0026lt;\u0026lt; std::endl; // 这一行是否能通过编译需要通过阅读代码才能判断出来 } 参数能否正常工作，通常需要阅读代码才能理解 编译报错友好性较差(vector\u0026lt;int\u0026amp;\u0026gt;报错很离谱) 为了解决上面的问题，C++20引入了concepts机制 编译期谓词，基于给定的输入，返回true或false 与constraints(require从句)一起使用限制模版参数 通常置于表示模版形参的尖括号后面进行限制 template \u0026lt;typename T\u0026gt; // IsAvail是在编译期求值的 concept IsAvail = std::is_same_v\u0026lt;T, int\u0026gt; || std::is_same_v\u0026lt;T, float\u0026gt;; std::cout \u0026lt;\u0026lt; IsAvail\u0026lt;int\u0026gt; \u0026lt;\u0026lt; std::endl; // 1 std::cout \u0026lt;\u0026lt; IsAvail\u0026lt;char\u0026gt; \u0026lt;\u0026lt; std::endl; // 0 template \u0026lt;typename T\u0026gt; requires IsAvail\u0026lt;T\u0026gt; void fun(T input) { // ... } int main() { fun(3); // 编译通过 fun(true); // 编译不通过，报错内容很少，很清晰 } 定义与使用 包含一个模版参数的concept 使用requires从句(见上面的代码) 直接替换template(更加清晰) template \u0026lt;typename T\u0026gt; concept IsIntOrFloat = std::is_same_v\u0026lt;T, int\u0026gt; || std::is_same_v\u0026lt;T, float\u0026gt;; template \u0026lt;IsIntOrFloat, T\u0026gt; void fun(T input) {} 包含多个模版参数的concept template \u0026lt;typename T1, typename T2\u0026gt; concept IsDiff = !std::is_same_v\u0026lt;T1, T2\u0026gt;; template \u0026lt;typename T1, typename T2\u0026gt; requires IsDiff\u0026lt;T1, T2\u0026gt; void fun(T1 input1, T2 input2) {} int main() { fun(3, 5); // 编译失败，类型相同 } 用作类型constraint时，少传递一个参数，推导出的类型将作为首个参数 template \u0026lt;class T, class U\u0026gt; concept Derived = std::is_base_of\u0026lt;U, T\u0026gt;::value; // 这里的Base是程序中前面定义的一个类 template\u0026lt;Derived\u0026lt;Base\u0026gt;, T\u0026gt; // 这里会把T当作Derived的第一个参数 void f(T); // T由Derived\u0026lt;T, Base\u0026gt;约束，也就是说T是Base的派生类则返回真 requires表达式 区分requires表达式和requires从句 表达式 template \u0026lt;typename T\u0026gt; concept Addable = requires(T x) { x + x; }; 从句 template \u0026lt;typename T\u0026gt; requires Addable\u0026lt;T\u0026gt; T add(T x, T y) { return x+ y; }; 从句+表达式 template \u0026lt;typename T\u0026gt; requires { requires(T x) { x + x; } } // 这里的表达式只有一行，所以最外面的大括号可以省略 T add(T x, T y) { return x+ y; }; 分类 简单要求 template \u0026lt;typename T\u0026gt; concept Addable = requires (T a, T b) { a + b; // T类型的数据需要满足可加性 }; template \u0026lt;Addable T\u0026gt; auto fun(T x, T y) { return x + y; } int main() { fun(3, 4); // 通过 fun(3, \u0026#34;1223\u0026#34;); // 不通过 } 类型要求 template \u0026lt;typename T\u0026gt; concept Avail = requires { typename T::inner; // 要求T::inner是一个合法类型 }; template \u0026lt;Avail T\u0026gt; auto fun(T x) {} struct Str { using inner = int; }; int main() { fun(3); // 不通过 fun(Str{}); // 通过 } 复合要求 template \u0026lt;typename T\u0026gt; concept Avail = requires (T x) { {x + 1} -\u0026gt; int; // 1.x需要支持+1操作；2.x+1的结果需要可以转换为int类型 }; template \u0026lt;Avail T\u0026gt; auto fun(T x) {} struct Str {}; int main() { fun(3); // 通过 fun(Str{}); // 不通过 } 嵌套要求 requires从句会影响重载解析与特化版本的选取 只有requires从句有效且返回为true时相应的模板才会被考虑 // 如果没有requires，下面两个模版是一样的，编译器不允许这种存在 // 引入requires后，系统会选择requires返回为true的模版 template \u0026lt;typename T\u0026gt; requires std::is_same_v\u0026lt;T, float\u0026gt; void fun(T) { std::cout \u0026lt;\u0026lt; 1 \u0026lt;\u0026lt; std::endl; } template \u0026lt;typename T\u0026gt; requires std::is_same_v\u0026lt;T, int\u0026gt; void fun(T) { std::cout \u0026lt;\u0026lt; 2 \u0026lt;\u0026lt; std::endl; } int main() { fun (3); // 2 } requires所引入的限定具有偏序特性，系统会选择限制最严格的版本 template \u0026lt;typename T\u0026gt; concept C1 = std::is_same_v\u0026lt;T, int\u0026gt;; template \u0026lt;typename T\u0026gt; concept C2 = std::is_same_v\u0026lt;T, int\u0026gt; || std::is_same_v\u0026lt;T, float\u0026gt;; template \u0026lt;C1, T\u0026gt; void fun(T) { std::cout \u0026lt;\u0026lt; 1 \u0026lt;\u0026lt; std::endl; } template \u0026lt;C2, T\u0026gt; void fun(T) { std::cout \u0026lt;\u0026lt; 2 \u0026lt;\u0026lt; std::endl; } int main() { fun(3); // 1 } 特化小技巧：在声明中引入\u0026quot;A||B\u0026quot;进行限制，之后分别对A和B引入特化 template \u0026lt;typename T\u0026gt; requires std::is_integral_v\u0026lt;T\u0026gt; || std::is_floating_point\u0026lt;T\u0026gt; class B; // 完全特化 template\u0026lt;\u0026gt; class B\u0026lt;int\u0026gt; {}; template\u0026lt;\u0026gt; class B\u0026lt;float\u0026gt; {}; // 部分特化（偏特化） template \u0026lt;typename T\u0026gt; requires std::is_integral_v\u0026lt;T\u0026gt; class B\u0026lt;T\u0026gt; {}; // int型偏特化 template \u0026lt;typename T\u0026gt; requires std::is_floating_point\u0026lt;T\u0026gt; class B\u0026lt;T\u0026gt; {}; // 浮点型偏特化 int main() { B\u0026lt;double\u0026gt; x; // 完全特化版本编译不通过，因为没有double类型的完全特化 // 部分特化版本编译通过，有浮点型偏特化 } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/template/requires-concepts/","title":"Requires \u0026 Concepts"},{"content":"指定内存回收逻辑 为shared_ptr指定自定义的回收逻辑，用于回收复杂的自定义结构 设计内存池的时候，自定义Deletor，可以不用真的执行delete操作，而是直接在Deletor函数中将指针交还给内存池 void fun(int* ptr) { std::cout \u0026lt;\u0026lt; \u0026#34;call delete\\n\u0026#34;; delete ptr; } int main() { std::shared_ptr\u0026lt;int\u0026gt;x(new int(3), fun); std::unique_ptr\u0026lt;int, decltype(\u0026amp;fun)\u0026gt; x(new int(3), fun); // 注意，unique_ptr需要传入两个模板参数，这是规定 } make_shared std::shared_ptr\u0026lt;int\u0026gt; x = new int(3); auto x = std::make_shared\u0026lt;int\u0026gt;(3); 优先使用make_shared，原因如下： 任何一个智能指针都包涵两块内存，一块是数据，一块是引用计数 使用new方式构造的智能指针，这两块内存可能离的非常远 我们在操作智能指针的时候，经常是访问完引用计数，就要去访问数据，如果离得远，就会造成cache miss make_shared会尽量将两块内存放在一起,cache命中率会增高，从而性能提升 智能指针数组 C++17之前需要自定义Deletor函数执行delete []\n从C++17开始支持\nstd::shared_ptr\u0026lt;int[]\u0026gt; x(new int[5]); 从C++20开始支持make_shared\u0026lt;T[]\u0026gt;\nauto x = std::make_shared\u0026lt;int[5]\u0026gt;(); auto x = std::make_shared\u0026lt;int[]\u0026gt;(5); // 两者相等 shared_ptr初始化陷阱 std::shared_ptr\u0026lt;int\u0026gt; x(new int(5)); std::shared_ptr\u0026lt;int\u0026gt; y(x.get()); // core, x和y互相不知道对方的存在，当x和y分别析构的时候，会delete两次int(5)这块内存 std::shared_ptr\u0026lt;int\u0026gt; y(x); // 合法，x和y知道彼此的存在，引用计数会变成2 循环引用 以下是shared_ptr的循环引用举例\nstruct Str{ std::shared_ptr\u0026lt;Str\u0026gt; nei; ~Str() { std::cout \u0026lt;\u0026lt; \u0026#34;~Str is called\\n\u0026#34;; } }; int main() { std::shared_ptr\u0026lt;Str\u0026gt; x(new Str); std::shared_ptr\u0026lt;Str\u0026gt; y(new Str); x-\u0026gt;nei = y; y-\u0026gt;nei = x; // 循环引用出现，x和y无法正常析构 } 引入weak_ptr解决循环引用问题:\nstruct Str{ std::weak_ptr\u0026lt;Str\u0026gt; nei; ~Str() { std::cout \u0026lt;\u0026lt; \u0026#34;~Str is called\\n\u0026#34;; } }; int main() { std::shared_ptr\u0026lt;Str\u0026gt; x(new Str); std::shared_ptr\u0026lt;Str\u0026gt; y(new Str); x-\u0026gt;nei = y; y-\u0026gt;nei = x; } weak_ptr的lock方法 weak_ptr的引入也会引发一些问题\nstruct Str{ std::weak_ptr\u0026lt;Str\u0026gt; nei; ~Str() { std::cout \u0026lt;\u0026lt; \u0026#34;~Str is called\\n\u0026#34;; } }; int main() { std::shared_ptr\u0026lt;Str\u0026gt; x(new Str); { std::shared_ptr\u0026lt;Str\u0026gt; y(new Str); x-\u0026gt;nei = y; // 语句块结束后，y被释放，此时的weak_ptr指向的内存不存在了 } } 为了解决上面的问题，在调用weak_ptr的时候需要调用它的lock方法\nlock会检查weak_ptr指向的内存是否还在\n如果不在了，返回shared_ptr\u0026lt;T\u0026gt;()\n如果还在，返回shared_ptr\u0026lt;T\u0026gt;(*this)\n最终的正确写法为\nstruct Str{ std::weak_ptr\u0026lt;Str\u0026gt; nei; ~Str() { std::cout \u0026lt;\u0026lt; \u0026#34;~Str is called\\n\u0026#34;; } }; int main() { std::shared_ptr\u0026lt;Str\u0026gt; x(new Str); { std::shared_ptr\u0026lt;Str\u0026gt; y(new Str); x-\u0026gt;nei = y; // 语句块结束后，y被释放，此时的weak_ptr指向的内存不存在了 } if (auto ptr = x-\u0026gt;nei.lock(); ptr) { std::cout \u0026lt;\u0026lt; \u0026#34;true branch\\n\u0026#34;; } else { std::cout \u0026lt;\u0026lt; \u0026#34;false branch\\n\u0026#34;; } } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/shared-ptr/","title":"SharedPtr"},{"content":"Apollo Singleton Macro #ifndef CYBER_COMMON_MACROS_H_ #define CYBER_COMMON_MACROS_H_ #include \u0026lt;iostream\u0026gt; #include \u0026lt;memory\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;type_traits\u0026gt; #include \u0026lt;utility\u0026gt; #include \u0026#34;cyber/base/macros.h\u0026#34; DEFINE_TYPE_TRAIT(HasShutdown, Shutdown) template \u0026lt;typename T\u0026gt; typename std::enable_if\u0026lt;HasShutdown\u0026lt;T\u0026gt;::value\u0026gt;::type CallShutdown(T *instance) { instance-\u0026gt;Shutdown(); } template \u0026lt;typename T\u0026gt; typename std::enable_if\u0026lt;!HasShutdown\u0026lt;T\u0026gt;::value\u0026gt;::type CallShutdown( T *instance) { (void)instance; } // There must be many copy-paste versions of these macros which are same // things, undefine them to avoid conflict. #undef UNUSED #undef DISALLOW_COPY_AND_ASSIGN #define UNUSED(param) (void)param #define DISALLOW_COPY_AND_ASSIGN(classname) \\ classname(const classname \u0026amp;) = delete; \\ classname \u0026amp;operator=(const classname \u0026amp;) = delete; #define DECLARE_SINGLETON(classname) \\ public: \\ static classname *Instance(bool create_if_needed = true) { \\ static classname *instance = nullptr; \\ if (!instance \u0026amp;\u0026amp; create_if_needed) { \\ static std::once_flag flag; \\ std::call_once(flag, \\ [\u0026amp;] { instance = new (std::nothrow) classname(); }); \\ } \\ return instance; \\ } \\ \\ static void CleanUp() { \\ auto instance = Instance(false); \\ if (instance != nullptr) { \\ CallShutdown(instance); \\ } \\ } \\ \\ private: \\ classname(); \\ DISALLOW_COPY_AND_ASSIGN(classname) #endif // CYBER_COMMON_MACROS_H_ 细节分析 为了能够让程序员显式的禁用某个函数，C++11 标准引入了一个新特性：\u0026quot;=delete\u0026quot;函数。程序员只需在函数声明后上“=delete;”，就可将该函数禁用。\nclass X3 { public: X3(); X3(const X3\u0026amp;) = delete; // 声明拷贝构造函数为 deleted 函数 X3\u0026amp; operator = (const X3 \u0026amp;) = delete; // 声明拷贝赋值操作符为 deleted 函数 }; 在多线程编程中，有一个常见的情景是某个任务只需要执行一次。在C++11中提供了很方便的辅助类once_flag，call_once。\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;mutex\u0026gt; std::once_flag flag; void do_once() { //也可以写在这里。要记住static，一旦flag被销毁了就不能保证只执行一次了 //static std::once_flag flag; std::call_once(flag, [](){ std::cout \u0026lt;\u0026lt; \u0026#34;Called once\u0026#34; \u0026lt;\u0026lt; std::endl; }); } int main() { std::thread t1(do_once); std::thread t2(do_once); std::thread t3(do_once); std::thread t4(do_once); t1.join(); t2.join(); t3.join(); t4.join(); } //可以看到，只会输出一行Called once ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/singleton/","title":"Singleton"},{"content":"Inf Convolution 用途 对凸的非光滑的函数进行光滑化的方法 定义 几何含义 Moreau envelope 定义 是Inf Convolution的一个特例，是将被卷积的函数取二次型 增广拉格朗日的增广项本质上就是用这个方法做smoothing \\(\\gamma\\)是光滑度的调节参数 性质 原函数和Moreau envelope操作后得到的光滑函数的minima是同一个点 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/smoothing/","title":"Smoothing Techniques"},{"content":"Steepest Gradient Descent 最速下降法 沿着梯度（grad或least-norm sub-grad）的负方向更新 $$x^{k+1}=x^{k}-\\tau \\triangledown f(x^{k})$$ 步长选择 Constant step size（固定步长） $$\\tau=c$$ Diminishing step size（渐消步长） $$\\tau=c/k$$ Robbins-Monro rule for expensive function 鲁棒性很强，一定可以收敛到local minimize 如果函数是非光滑的或者梯度的计算存在一个方差较大的噪声时，可以用这种方法，但收敛速度相对比较慢 Exact line search（精确线搜索） $$\\tau=argmin_{\\alpha}f(x^{k}+\\alpha d)$$ 找到一个\\(\\alpha\\)使得函数下降最多，这本身就是个优化问题（将多维优化变成了一维优化），求解很难，因此工程中常用下面一种方法 Inexact line search（非精确线搜索） $$\\tau\\in\\lbrace \\alpha | f(x^{k}) - f(x^{k}+\\alpha d)\\geq -c\\cdot \\alpha d^{T}\\triangledown f(x^{k})\\rbrace$$ Armijo condition（充分下降条件） \\(\\tau\\)初始化为1.0，\\(c\\in (0,1)\\) \\(\\tau\\)每个循环都减半，直到函数值第一次落到上图虚线的下方，结束循环，更新\\(x\\) 注意，函数只有次梯度的话需要沿着least norm grad的反方向更新，循环结束条件是次梯度包含0向量 Inexact line search虽然迭代次数比Exact line search更多，但每次迭代的耗时少很多，因此大部分情况下总时间也少 缺点 从上图可以看出，条件数为2的时候，sgd就需要震荡多次才能到最优解，当条件数非常大的时候（椭圆的上下沿接近平行），那更新过程就会震荡没完了。因此为了更快收敛，我们除了利用梯度信息，还需要用到曲率信息 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/sgd/","title":"Steepest Gradient Descent"},{"content":"基础 使用template关键字引入模版 类模版的声明与定义——翻译单元的一处定义原则 成员函数只有在调用时才会被实例化 template \u0026lt;typename T\u0026gt; Class B{ void fun(T input) { std::cout \u0026lt;\u0026lt; input \u0026lt;\u0026lt; std::endl; } auto fun2() { return B{}; // return B\u0026lt;T\u0026gt;{}的简化 } }; int main() { B\u0026lt;int\u0026gt; x; x.fun(3); B\u0026lt;Str\u0026gt; y; y.fun(Str{}); // 如果这行不加，编译通过，没有调用就不会实例化fun(Str input)函数 // 如果这行加了，编译不通过，Str类型没有重载标准输出 } 类内模版名称的简写(见fun2) 类模版成员函数的定义 类外 template\u0026lt;typename T\u0026gt; class B { public: void fun(); }; template\u0026lt;typename T\u0026gt; void B\u0026lt;T\u0026gt;::fun() { // 上面的template\u0026lt;typename T\u0026gt;和\u0026lt;T\u0026gt;都不能省略 } 类内相对简单，不多说了 成员函数模板 类的成员函数模版 class B { public: template \u0026lt;typename T\u0026gt; void fun() {} // 类内 template \u0026lt;typename T\u0026gt; void fun2(); }; template \u0026lt;typename T\u0026gt; void B::fun2() {} // 类外 int main() { B x; x.fun\u0026lt;int\u0026gt;(); x.fun2\u0026lt;int\u0026gt;(); } 类模版的成员函数模板 template \u0026lt;typename T\u0026gt; class B { public: template \u0026lt;typename T2\u0026gt; void fun() {} // 类内定义 template \u0026lt;typename T2\u0026gt; void fun2(); } template \u0026lt;typename T\u0026gt; template \u0026lt;typename T2\u0026gt; void B\u0026lt;T\u0026gt;::fun2() {} // 类外定义 int main() { B\u0026lt;int\u0026gt; x; x.fun\u0026lt;float\u0026gt;(); } 友元函数（模版） 类（模版）的友元 template \u0026lt;typename T\u0026gt; class B { friend auto operator + (B input1, B input2) { B res; res.x = input1.x + input2.x; return res; } int x = 3; }; int main() { B\u0026lt;int\u0026gt; val1; B\u0026lt;int\u0026gt; val2; B\u0026lt;int\u0026gt; res = val1 + val2; } 可以声明一个函数模版为某个类（模版）的友元 template \u0026lt;typename T2\u0026gt; void fun(); // 声明，用于类内的友元声明 template \u0026lt;typename T\u0026gt; class B { template \u0026lt;typename T2\u0026gt; friend void fun(); int x; }; template \u0026lt;typename T2\u0026gt; void fun() { B\u0026lt;int\u0026gt; tmp1; tmp1.x; B\u0026lt;float\u0026gt; tmp2; tmp2.x; } C++11支持声明模版参数为友元 template \u0026lt;typename T\u0026gt; class B { friend T; }; 类模版的实例化 与函数实例化很像 可以实例化整个类，或者类中的某个成员函数 类模版的（完全）特化/部分特化（偏特化） 函数模版的特化不建议使用，但是类模版的特化不一样，是一个非常重要的技术 特化版本和基础版本可以有完全不同的实现 // 完全特化 template \u0026lt;typename T\u0026gt; struct B { void fun() {} }; template \u0026lt;\u0026gt; struct B\u0026lt;int\u0026gt; { void fun2() {} // 这里直接重新定义了一个新的函数 }; 部分特化 template \u0026lt;typename T, typename T2\u0026gt; struct B { void fun() {} }; template \u0026lt;typename T\u0026gt; // 这里的T对应上面的T2 struct B\u0026lt;int, T\u0026gt; { void fun2() {} } 类模版的实参推导(C++17开始) 基于构造函数的实参推导 template \u0026lt;typename T\u0026gt; struct B { B(T input) {} // 构造函数 }; int main() { B x(3); } 用户自定义的推导指引 注意，引入实参推导并不意味着降低了类型限制 std::pair x(3, 3.14); // std::pair\u0026lt;int, double\u0026gt; x(3, 3.14); x.first = \u0026#34;123\u0026#34;; // 报错，int类型变量不可以用string赋值 C++17之前的解决方案：引入辅助模版函数 template \u0026lt;typename T1, typename T2\u0026gt; std::pair\u0026lt;T1, T2\u0026gt; make_pair(T1 first, T2 second) { return std::pair\u0026lt;T1, T2\u0026gt;(first, second); } int main() { auto res = make_pair(3, 3.14); } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/template/template-class/","title":"Template Class"},{"content":"使用template关键字引入模板 函数模板的声明与定义 // 声明 template\u0026lt;typename T\u0026gt; void fun(T); // 定义 template\u0026lt;typename T\u0026gt; void fun(T input){ std::cout \u0026lt;\u0026lt; input \u0026lt;\u0026lt; std::endl; } typename关键字可以替换为class，含义相同 函数模板中包含了两对参数：函数形参(上面的input)/实参；模版形参(上面的T)/实参 注意：函数模版不是函数(不能调用)，我们需要在编译期给模版形参赋值相应的实参，才能把函数模版实例化成一个函数(可以去cppinsights里面去看看上面这份代码被预处理后的样子) 函数形参是运行期赋值的 函数模板的显示实例化 fun\u0026lt;int\u0026gt;(3); // 会在终端打印出“3” 编译期的两阶段处理 模板语法检查 // 第一阶段只会检查下面这段代码有没有语法错误 template\u0026lt;typename T\u0026gt; void fun(T input){ std::cout \u0026lt;\u0026lt; input \u0026lt;\u0026lt; std::endl; } 模板实例化 struct Str {}; // 第二阶段会实例化对应的函数，这里会实例化出来两个函数，其中第二个函数会报错，因为Str没有重载\u0026lt;\u0026lt;运算符 fun\u0026lt;int\u0026gt;(3); fun\u0026lt;Str\u0026gt;(Str{}); 模板必须在实例化时可见——模板函数只需要满足翻译单元的一处定义原则(一般函数需要满足程序的一处定义原则) 注意与内联函数的异同 // header.h template\u0026lt;typename T\u0026gt; void fun(T input){ std::cout \u0026lt;\u0026lt; input \u0026lt;\u0026lt; std::endl; } inline void normal_fun() {} // main.cc #include \u0026#34;header.h\u0026#34; // source.cc #include \u0026#34;header.h\u0026#34; 上面的代码如果normal_fun不加inline，编译会报错重复定义，因为normal_fun需要满足程序的一处定义原则，加上inline后只需要满足翻译单元的一处定义原则即可 模板函数本身就只需要满足翻译单元的一处定义原则，所以不需要加inline 当然，模板函数也可以加inline，作用是告诉编译器在实例化函数的时候，顺便替换掉函数的调用，直接把函数逻辑嵌入到实例化的位置 函数模板的重载 template\u0026lt;typename T\u0026gt; void fun(T input){ std::cout \u0026lt;\u0026lt; input \u0026lt;\u0026lt; std::endl; } template\u0026lt;typename T\u0026gt; void fun(T* input){ std::cout \u0026lt;\u0026lt; *input \u0026lt;\u0026lt; std::endl; } template\u0026lt;typename T, typename T2\u0026gt; void fun(T input, T2 input2){ std::cout \u0026lt;\u0026lt; input \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; input2 \u0026lt;\u0026lt; std::endl; } int x = 3; fun\u0026lt;int\u0026gt;(\u0026amp;x); fun\u0026lt;int\u0026gt;(x); 模版实参的类型推导 如果函数模版在实例化时没有显示制定模版实参，那么系统会尝试进行推导 推导时基于函数实参（表达式）确定模版实参的过程，其基本原则与auto类型推导类似 函数形参是左值引用/指针： 忽略表达式类型中的引用 将表达式类型与函数形参模式匹配以确定模版实参 template\u0026lt;typename T\u0026gt; void fun(T\u0026amp; input) { std::cout \u0026lt;\u0026lt; input \u0026lt;\u0026lt; std::endl; } int main() { int x = 3; fun(3); // fun(int\u0026amp;); int\u0026amp; y = x; fun(y); // fun(int\u0026amp;); const int\u0026amp; z = x; fun(z); // fun(const int\u0026amp;); } 函数的形参是万能引用 如果实参表达式是右值，那么模版形参被推导为去掉引用的基本类型 如果实参表达式是左值，那么模版形参被推导为左值引用，触发引用折叠 template\u0026lt;typename T\u0026gt; void fun(T\u0026amp;\u0026amp; input) {} // 如果T是一个确定的类型（比如int\u0026amp;\u0026amp;，double\u0026amp;\u0026amp;），那\u0026amp;\u0026amp;就是右值引用 // 编译器在编译的时候，T还没被确定为一个具体的类型，那\u0026amp;\u0026amp;就被当作万能引用 // 既可以引用左值，也可以引用右值 int main() { int\u0026amp;\u0026amp; x = 3; fun(3); // fun(int\u0026amp;\u0026amp;) int y = 3; fun(y); // fun(int\u0026amp;) } 函数形参不包含引用(写起来最简单，模型推导最复杂) 忽略表达式中的引用 忽略顶层const 数组、函数转换成相应的指针类型 template\u0026lt;typename T\u0026gt; void fun(T input) {} int main() { fun(3); // fun(int) int x = 3; int\u0026amp; ref = x; fun(ref); // fun(int) const int\u0026amp; ref = x; fun(ref); // fun(int) const int* const ptr = \u0026amp;x; // 注意，这里的顶层const是后面那个const，直接修饰变量的那个 // 因为变量是拷贝进fun的，所以这个const会失效 fun(ptr); // fun(const int*); int x[3]; fun(x); // fun(int*) } 多个T情况 // 同样的T template\u0026lt;typename T\u0026gt; void fun(T input1, T input2) {} int main() { fun\u0026lt;int\u0026gt;(3, 5.0); // 通过，不存在模型推导，T显示指定为int fun(3, 5.0); // 不通过，3推导出T为int，5.0推导出T为double，int和double不是一个类型，报错 } // 不同的T template\u0026lt;typename T\u0026gt; void fun(T1 input1, T2 input2) {} int main() { fun\u0026lt;int\u0026gt;(3, 5.0); // 通过，T1显示指定为int，T2隐式指推断为double } 模版实参并非总是能够推导得到 如果模版形参与函数形参无关，则无法推导 template \u0026lt;typename T, typename Res\u0026gt; Res fun(T input) {} int main() { fun(3); // Res无法推导，编译不通过 } 即使相关，也不一定能进行推导，推导成功也可能存在因歧义而无法使用(见上面“同样的T”) template \u0026lt;typename T\u0026gt; void fun(typename std::remove_reference\u0026lt;T\u0026gt;::type input) {} int main() { fun(3); // 编译器在实例化fun函数的时候，发现std::remove_reference\u0026lt;T\u0026gt;::type的结果是int // T到底是啥不确定，可以是int，也可以是int\u0026amp;，int\u0026amp;\u0026amp;所以编译器报错 } 在无法推导时，编译器会选择使用缺省模版实参 template \u0026lt;typename T = int\u0026gt; void fun(typename std::remove_reference\u0026lt;T\u0026gt;::type input) {} int main() { fun(3); // 编译通过，有缺省值 } 只能处理推导不成功的场景，不能处理歧义而无法使用的场景 template\u0026lt;typename T = double\u0026gt; void fun(T input1, T input2) {} int main() { fun(3, 5.0); // 不通过，当编译器可以从变量推导T的类型的时候，就不会看T的缺省值 } 可以为任意位置的模版形参指定缺省模板实参——注意与函数缺省实参的区别 函数的缺省实参只能排在最后面 void fun(int x = 3.0, int y) {} // 编译不通过 template \u0026lt;typename Res = double, typename T\u0026gt; // 编译通过 Res fun(T input) {} int main() { fun(3); } 显示指定部分模版实参 显示指定的模版实参必须是从最左边开始，依次指定 模版形参的声明顺序会影响调用的灵活性 越是需要显示指定的T，就越要放在前面 // good case template \u0026lt;typename Res, typename T\u0026gt; Res fun(T x) {} int main() { fun\u0026lt;int\u0026gt;(5); // Res显示指定，T隐式指定 } // bad case template \u0026lt;typename T, typename Res\u0026gt; Res fun(T x) {} int main() { fun\u0026lt;int\u0026gt;(5); // T显示指定, Res无法推断 } 函数模板自动推导是会遇到的几种情况 函数形参无法匹配——SFINAE（替换失败并非错误） template \u0026lt;typename T\u0026gt; void fun(T x, T y) {} int main() { fun(3, 5.0); // 编译不通过，但是不代表template fun有错误，只是找不到合适的匹配模板 // 如果重载一个fun(T1, T2)，就没问题了 // SFINAE这个概念在元编程的时候很有用 } 模板与非模版同时匹配，匹配等级相同，系统会选择非模版函数 多个模版同时匹配，此时采用偏序关系确定选择“最特殊”版本 template \u0026lt;typename T\u0026gt; void fun(T x, float y) { std::cout \u0026lt;\u0026lt; 2 \u0026lt;\u0026lt; std::endl; } template \u0026lt;typename T, typename T2\u0026gt; void fun(T x, T2 y) { std::cout \u0026lt;\u0026lt; 1 \u0026lt;\u0026lt; std::endl; } template \u0026lt;typename T\u0026gt; void fun(T* x, float y) { std::cout \u0026lt;\u0026lt; 3 \u0026lt;\u0026lt; std::endl; } int main() { fun(3, 5.0); // 2 int x = 3; fun(\u0026amp;x, 5.0); // 3 } 如果一样特殊，那就编译不通过了 template \u0026lt;typename T, typename T2\u0026gt; void fun(T* x, T2 y) {} template \u0026lt;typename T, typename T2\u0026gt; void fun(T x, T2* y) {} int main() { int x = 3; fun(\u0026amp;x, \u0026amp;x); } 模版函数的实例化控制 只实例化，不调用函数 某些库中不想给出模版的内部实现逻辑，只给出模版的声明，此时就需要提前实例化用户想要使用的函数 显示实例化定义 // header.h template \u0026lt;typename T\u0026gt; void fun(T x) { std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; } template void fun\u0026lt;int\u0026gt;(int); // 实例化 // 也可以这么写：void fun(int); // main.cc #include \u0026#34;header.h\u0026#34; int main() { int x = 3; fun\u0026lt;int\u0026gt;(x); } 显示实例化声明 //header.h template \u0026lt;typename T\u0026gt; void fun(T x) { std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; } // source.cc #include \u0026#34;header.h\u0026#34; template void fun\u0026lt;int\u0026gt;(int); // main.cc #include \u0026#34;header.h\u0026#34; extern template void fun\u0026lt;int\u0026gt;(int); // extern表示已经在别的翻译单元实例化过了，这里不要再实例化一遍 // 链接的时候会直接链到source里面的实例 int main() { int x = 3; fun\u0026lt;int\u0026gt;(x); } 注意一处定义原则 隐式实例化可以在多处有实例化，编译器会选择其中一个，删除掉多余的 // header.h template \u0026lt;typename T\u0026gt; void fun(T x) { std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; } // source.cc #include \u0026#34;header.h\u0026#34; void fun2() { fun\u0026lt;int\u0026gt;(3); // source中隐式实例化一次 } // main.cc #include \u0026#34;header.h\u0026#34; int main() { int x = 3; fun\u0026lt;int\u0026gt;(x); // main中隐式实例化一次 } 显示实例化原则上在整个程序中只能有一处，但是有多处的话，编译器也不一定会报错，尽量不要这么写 // header.h template \u0026lt;typename T\u0026gt; void fun(T x) { std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; } // source.cc #include \u0026#34;header.h\u0026#34; template void fun\u0026lt;int\u0026gt;(int); // source中显示实例化一次 void fun2() { fun\u0026lt;int\u0026gt;(3); } // main.cc #include \u0026#34;header.h\u0026#34; template void fun\u0026lt;int\u0026gt;(int); // main中显示实例化一次 int main() { int x = 3; fun\u0026lt;int\u0026gt;(x); } 注意实例化过程中的模版形参推导 // 模版1 template \u0026lt;typename T\u0026gt; void fun(T x) { std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; } template void fun(int* x); // 显示实例化放在这里会调用模板1，因为程序从上到下执行，还没看到后面的模板2 // 模板2 template \u0026lt;typename T\u0026gt; void fun(T* x) { std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; std::endl; } template void fun(int* x); // 显示实例化放在这里会调用模板2 模版函数的（完全）特化 模版函数不支持部分特化 针对某种模版参数，引入特别的函数版本 template \u0026lt;typename T\u0026gt; void fun(T x) { // 如果T的实例化类型不是int，执行这里的逻辑 } template \u0026lt;\u0026gt; void fun(int x) { // 如果传入的参数类型是int，执行这里的逻辑 } 注意与函数模版重载的差异，特化并不引入新的（同名）名称，只是为某个模版函数针对特定的模版实参提供优化算法 避免使用特化 特化不参与重载解析，会产生反直觉的效果 通常使用重载来代替，直接实现相应的函数就行了，比如想对int型的入参引入逻辑优化，直接实现void fun(int) 有些情况无法通过重载来实现 template \u0026lt;typename T, typename Res\u0026gt; Res fun(T input) {} // 上面这个模板无法实现对应的重载函数，因为Res不在参数列表里，是返回值的类型，Res无法被重载 使用if constexpr来解决 template \u0026lt;typename T, typename Res\u0026gt; Res fun(T input) { if constexpr(std::is_same_v\u0026lt;Res, int\u0026gt;) { // ... } else { // ... } return Res; } 引入“假”函数形参 template \u0026lt;typename T, typename Res\u0026gt; Res fun(T inputi, const Res\u0026amp;) { std::cout \u0026lt;\u0026lt; 1 \u0026lt;\u0026lt; std::endl; return Res{}; } // 引入一个Res，函数里不使用 template \u0026lt;typename T\u0026gt; // 注意，\u0026lt;\u0026gt;里面是有东西的，所以这是重载，不是特化 int fun(T input, const int\u0026amp;) { std::cout \u0026lt;\u0026lt; 2 \u0026lt;\u0026lt; std::endl; return int{}; } int main() { int x = 0; fun(\u0026amp;x, int{}); // 2 } 通过类模版特化来解决问题 补充 (C++20)函数模板的简化形式：使用auto定义模板参数类型 优势：书写简洁 劣势：在函数内部需要间接获取参数类型信息 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/template/template-function/","title":"Template Function"},{"content":"Temporal Difference 增量式MC $$V(S_{t})=V(S_{t})+\\frac{1}{N(S_{t})}(G_{t}-V(S_{t}))$$ 时间差分TD $$V(S_{t})=V(S_{t})+\\frac{1}{N(S_{t})}(R_{t+1}+\\gamma V(S_{t+1})-V(S_{t}))$$ 核心差别： MC是根据[\\(s1\\to\\)终止状态]完整片段的最终回报更新s1的值函数 TD是根据[\\(s1\\to s2\\)]这一步片段的即时回报值\\(R\\)和\\(s2\\)的估计值函数更新\\(s1\\)的值函数 与DP的对比 DP是全宽备份 TD是样本备份 TD与MC的对比 TD 算法在知道结果之前学习 TD算法在每一步之后都能在线学习 MC算法必须等待最终回报值得到之后才能学习 TD算法即便没有最终结果也能学习 TD算法能够从不完整序列中学习 MC算法仅仅能够从完整序列中学习 TD算法适用于连续性任务和片段性任务 MC算法仅仅适用于片段性任务 TD算法有多个驱动力 MC算法只有奖励值作为更新的驱动力 TD算法有奖励值和状态转移作为更新的驱动力 MC有高方差,零偏差 收敛性较好 (即使采用函数逼近) 对初始值不太敏感 随着样本数量的增加,方差逐渐减少, 趋近于 0 TD 有低方差,和一些偏差 通常比 MC 效率更高 表格法下TD(0)收敛到\\(V_{\\pi }(s)\\)(函数逼近时不一定) 对初始值更敏感 随着样本数量的增加,偏差逐渐减少,趋近于 0 样本数量有限时，TD的结果与真实结果的偏差比较稳定。MC可能出现巨大偏差。 TD要求环境符合马尔科夫性，MC不要求 自举和采样 自举: 使用随机变量的估计去更新 MC 没有自举 DP 和 TD 都有自举 采样: 通过样本估计期望 MC 和 TD 采样 DP 不采样 TD的优化方法 整体思路是 策略评价+策略提升 策略评价 在策略评价SARSA 公式 $$Q(S_{t},A_{t})\\leftarrow Q(S_{t},A_{t})+\\alpha (R_{t+1}+\\gamma Q(S_{t+1},A_{t+1})-Q(S_{t},A_{t}))$$ 算法流程 1:初始化\\(Q(s,a), \\forall s\\in S, a\\in A(s)\\)且\\(Q(S_{end},\\cdot )=0\\) 2:repeat(对于每个片段) 3: 初始化状态\\(S\\) 4: 根据\\(Q\\)选择一个在\\(S\\)处的动作\\(A\\)(使用\\(\\varepsilon \\)-贪婪策略) 5: repeat(对于片段中每一步) 6: 执行动作\\(A\\)，观测\\(R,S^{\u0026rsquo;}\\) 7: 根据\\(Q\\)选择一个在\\(S^{\u0026rsquo;}\\)处的动作\\(A^{\u0026rsquo;}\\)(使用\\(\\varepsilon \\)-贪婪策略) 8: \\(Q(S_{t},A_{t})\\leftarrow Q(S_{t},A_{t})+\\alpha (R_{t+1}+\\gamma Q(S_{t+1},A_{t+1})-Q(S_{t},A_{t}))\\) 9: \\(S\\leftarrow S^{\u0026rsquo;};A\\leftarrow A^{\u0026rsquo;}\\) 10: until \\(S\\)是终止状态 11:until收敛 收敛性 在满足以下条件时,Sarsa 算法收敛到最优的状态动作值函数 策略序列\\(\\pi _{t}(a|s)\\)满足GLIE 步长序列\\(\\alpha _{t}\\)是一个Robbins-Monro序列 $$\\sum_{t=1}^{\\infty }\\alpha_{t}=\\infty ,\\sum_{t=1}^{\\infty }\\alpha_{t}^{2}=\\infty $$ GLIE 保证了 充分的探索 策略最终收敛到贪婪的策略 Robbins-Monro保证了 步长足够大,足以克服任意初始值 步长足够小,最终收敛 (常量步长不满足) 期望SARSA $$Q(S_{t},A_{t})\\leftarrow Q(S_{t},A_{t})+\\alpha (R_{t+1}+\\gamma \\sum_{a}\\pi (a|S_{t+1})Q(S_{t+1},a)-Q(S_{t},A_{t}))$$ 减少了由于\\(A^{\u0026rsquo;}\\)的选择带来的方差 在相同更新步数时,期望 Sarsa 比 Sarsa 的通用性更好 可以在在策略和离策略中切换 在策略:TD目标值中的\\(R_{t+1}+\\gamma \\sum_{a}\\pi (a|S_{t+1})Q(S_{t+1},a)\\)中的策略\\(\\pi \\)和采样的策略是同一个策略 离策略:TD目标值中的\\(R_{t+1}+\\gamma \\sum_{a}\\pi (a|S_{t+1})Q(S_{t+1},a)\\)中的策略\\(\\pi \\)和采样的策略是不同的策略 一种特殊情况,TD目标值中的策略选择贪婪策略, 采样的策略选用ε-贪婪策略——Q学习 离策略评价Q学习 公式 $$Q(S,A)\\leftarrow Q(S,A)+\\alpha (R+\\gamma \\max_{a^{\u0026rsquo;}}Q(S^{\u0026rsquo;},a^{\u0026rsquo;})-Q(S,A))$$ 算法流程 1:初始化\\(Q(s,a), \\forall s\\in S, a\\in A(s)\\)且\\(Q(S_{end},\\cdot )=0\\) 2:repeat(对于每个片段) 3: 初始化状态\\(S\\) 4: repeat(对于片段中每一步) 5: 根据\\(Q\\)选择一个在\\(S\\)处的动作\\(A\\)(使用\\(\\varepsilon \\)-贪婪策略) 6: 执行动作\\(A\\)，观测\\(R,S^{\u0026rsquo;}\\) 7: \\(Q(S,A)\\leftarrow Q(S,A)+\\alpha (R+\\gamma \\max_{a^{\u0026rsquo;}}Q(S^{\u0026rsquo;},a^{\u0026rsquo;})-Q(S,A))\\) 8: \\(S\\leftarrow S^{\u0026rsquo;}\\) 9: until \\(S\\)是终止状态 10:until收敛 策略提升 \\(\\varepsilon \\)-贪婪策略提升 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/temporal-difference/","title":"Temporal Difference"},{"content":"Temporal Difference Lambda 时间差分就是TD(0)算法，只向后采样一步；MC是向后采样整个片段；多步自举介于两者之间 TD: $$n=1,G_{t}^{(1)}=R_{t+1}+\\gamma V(S_{t+1})$$ $$n=2,G_{t}^{(2)}=R_{t+1}+\\gamma V(S_{t+1})$$ MC: $$n=3,G_{t}^{(\\infty )}=R_{t+1}+\\gamma R_{t+2}+\u0026hellip;+\\gamma ^{T-t-1}R_{T}$$ 经验认为\\(n=3-10\\)步左右，要好于TD(0)和MC n步TD策略评价 算法流程 1:repeat(对于每一个片段) 2: repeat对于片段中的每一步 3: 根据\\(\\pi (\\cdot ,S_{t})\\)选择动作\\(A_{t}\\) 4: 执行动作\\(A_{t}\\)，观察到\\(R_{t+1}, S_{t+1}\\)，并将其存储起来 5: if \\(\\tau =t-n+1\\geqslant 0\\), then 6: \\(G\\leftarrow \\sum_{i=\\tau +1}^{min(\\tau +n,T)}\\gamma ^{i-\\tau -1}R_{i}\\) 7: if \\(\\tau +n\u0026lt;T\\),then \\(G\\leftarrow G+\\gamma ^{n}V(S_{\\tau +n})\\) 8: \\(V(S_{\\tau })\\leftarrow V(S_{\\tau })+\\alpha [G-V(S_{\\tau })]\\) 9: end if 10: until直到终止状态 11:until收敛 两个注意点 为了计算\\(n\\)步回报值,需要维护\\(R\\),\\(S\\) 的存储空间 对于后继状态不足\\(n\\)个的,使用 MC 目标值 多步自举 前向视角 就是将\\(TD(0),TD(1),\u0026hellip;,TD(n)\\)求平均 $$G_{t}^{\\lambda }=(1-\\lambda )\\sum_{n=1}^{\\infty }\\lambda ^{n-1}G_{t}^{(n)}$$ \\(\\lambda =0\\)，退化成TD(0)；\\(\\lambda =1\\)，退化成MC \\(TD(\\lambda )\\)更新公式 $$V(S_{t})\\leftarrow V(S_{t})+\\alpha (G_{t}^{\\lambda }-V(S_{t}))$$ 后向视角\u0026ndash;基于资格迹(Eligibility Traces) 基本概念 状态转移片段：\\(s1\\to s1\\to s1\\to s2\\to s3\\) 信度分配(Credit assignment)问题:到底是\\(s1\\)还是\\(s2\\)造成了最后的\\(s3\\) 频率启发式: 归因到频数最高的状态 近因启发式: 归因到最近的状态 资格迹是两者的结合 资格迹的计算公式 $$E_{0}(s)=0$$ $$E_{t}(s)=\\gamma \\lambda E_{t-1}(s)+1(S_{t}=s)$$ 直观的感觉就是，第一次遇到这个状态s的时候，对它的记忆由0蹦到1，然后慢慢开始遗忘(对应\\(\\times \\gamma \\)这个动作)，下一次又遇到了，记忆就一下子清晰了(对应+1这个动作)，然后又慢慢遗忘。 利用资格迹实现多步自举 对于每一个状态s，维护一个资格迹\\(E(s)\\) 更新值函数\\(V(s)\\)时，会更新每一个状态\\(s\\) 使用TD误差\\(\\delta_{t}\\)和资格迹\\(E_{t}(s)\\) \\(\\delta_{t}=R_{t+1}+\\gamma V(S_{t+1})-V(S_{t})\\) \\(V(s)\\leftarrow V(s)+\\alpha \\delta_{t}E_{t}(s)\\) 资格迹本质上是记录了所有状态s对后继状态\\(S_{t+1}\\)的贡献度，被用来对TD误差进行加权 总结 前向视角 利用\\(t+1,t+2,\u0026hellip;t+n\\)时刻的\\(V(s)\\)求解t时刻的\\(V(s)\\) 容易理解 需要拥有完整的状态转移片段才能求解，跟MC一样离线更新 后向视角 利用\\(t+1\\)时刻的\\(V(s)\\)更新\\(t,t-1,t-2,t-3,\u0026hellip;0\\)时刻的\\(V(s)\\) 在线更新，每一个时刻都更新一遍之前所有时刻的\\(V\\) n步Sarsa ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/temporal-difference-lambda/","title":"Temporal Difference Lambda"},{"content":"创建线程 #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; using namespace std; void t1() //普通的函数，用来执行线程 { for (int i = 0; i \u0026lt; 20; ++i) { cout \u0026lt;\u0026lt; \u0026#34;t1111\\n\u0026#34;; } } void t2() { for (int i = 0; i \u0026lt; 20; ++i) { cout \u0026lt;\u0026lt; \u0026#34;t22222\\n\u0026#34;; } } int main() { thread th1(t1); //实例化一个线程对象th1，使用函数t1构造，然后该线程就开始执行了（t1()） thread th2(t2); cout \u0026lt;\u0026lt; \u0026#34;here is main\\n\\n\u0026#34;; return 0; } Tip: 上面的代码存在一个问题，主线程结束之后，其他两个线程也会直接结束，引起异常报错 join方法 join的作用是让主线程在join函数处等待，直到该子线程执行结束，再往后执行 #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; using namespace std; void t1() { for (int i = 0; i \u0026lt; 20; ++i) { cout \u0026lt;\u0026lt; \u0026#34;t1111\\n\u0026#34;; } } void t2() { for (int i = 0; i \u0026lt; 20; ++i) { cout \u0026lt;\u0026lt; \u0026#34;t22222\\n\u0026#34;; } } int main() { thread th1(t1); thread th2(t2); th1.join(); //等待th1执行完 th2.join(); //等待th2执行完 cout \u0026lt;\u0026lt; \u0026#34;here is main\\n\\n\u0026#34;; return 0; } 这样，当两个线程执行完之后，主程序才会执行cout并退出 detach方法 detach是用来和线程对象分离的，这样线程可以独立地执行，不过这样由于没有thread对象指向该线程而失去了对它的控制，当对象析构时线程会继续在后台执行，但是当主程序退出时并不能保证线程能执行完 int main() { thread th1(t1); thread th2(t2); th1.detach(); th2.detach(); cout \u0026lt;\u0026lt; \u0026#34;here is main\\n\\n\u0026#34;; return 0; } Tips: 如果没有良好的控制机制或者这种后台线程比较重要，最好不用detach而应该使用join 注意 调用 join 或 detach 之前需要调用 joinable() 判断一下线程是否运行. 如果 joinable() 返回 false, 则不需要. 需要注意的是线程对象执行了join后就不再joinable了，所以只能调用join一次 调用类内对象 class Demo { Public: Demo(; ~Demo(); void Run(int a, int b); } int main(char agrc, char** argv) { Demo* pDemo; std::Thread* pT; pDemo = new Demo(); pT = std::thread(\u0026amp;Demo::Run, pDemo, 1, 2); //第一个参数是函数,第二个参数是类(如果是在类内创建就用this), 后面就是函数传入的参数 if(pT-\u0026gt;joinable()) { pT-\u0026gt;join();//Program will wait here until the end of Run(). } } CMakeLists.txt 需要链接pthread库，否则编译无法通过: TARGET_LINK_LIBRARIES(your_executable pthread) ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/multi-thread/thread/","title":"Thread"},{"content":"代码 #include \u0026lt;vector\u0026gt; #include \u0026lt;queue\u0026gt; #include \u0026lt;memory\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;condition_variable\u0026gt; #include \u0026lt;future\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;stdexcept\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;chrono\u0026gt; class ThreadPool { public: ThreadPool(size_t); template\u0026lt;class F, class... Args\u0026gt; auto enqueue(F\u0026amp;\u0026amp; f, Args\u0026amp;\u0026amp;... args) -\u0026gt; std::future\u0026lt;typename std::result_of\u0026lt;F(Args...)\u0026gt;::type\u0026gt;; ~ThreadPool(); private: // need to keep track of threads so we can join them std::vector\u0026lt; std::thread \u0026gt; workers; // the task queue std::queue\u0026lt; std::function\u0026lt;void()\u0026gt; \u0026gt; tasks; // synchronization std::mutex queue_mutex; std::condition_variable condition; bool stop; }; inline ThreadPool::ThreadPool(size_t threads) : stop(false) { for(size_t i = 0; i \u0026lt; threads; ++i) { workers.emplace_back( [this] { for(;;) { std::function\u0026lt;void()\u0026gt; task; { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(this-\u0026gt;queue_mutex); this-\u0026gt;condition.wait(lock, [this]{ return this-\u0026gt;stop || !this-\u0026gt;tasks.empty(); }); if(this-\u0026gt;stop \u0026amp;\u0026amp; this-\u0026gt;tasks.empty()) return; task = std::move(this-\u0026gt;tasks.front()); this-\u0026gt;tasks.pop(); } task(); } } ); } } // the destructor joins all threads inline ThreadPool::~ThreadPool() { { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(queue_mutex); stop = true; } condition.notify_all(); for(std::thread \u0026amp;worker: workers) worker.join(); } // add new work item to the pool template\u0026lt;class F, class... Args\u0026gt; auto ThreadPool::enqueue(F\u0026amp;\u0026amp; f, Args\u0026amp;\u0026amp;... args) -\u0026gt; std::future\u0026lt;typename std::result_of\u0026lt;F(Args...)\u0026gt;::type\u0026gt; { using return_type = typename std::result_of\u0026lt;F(Args...)\u0026gt;::type; auto task = std::make_shared\u0026lt; std::packaged_task\u0026lt;return_type()\u0026gt; \u0026gt;( std::bind(std::forward\u0026lt;F\u0026gt;(f), std::forward\u0026lt;Args\u0026gt;(args)...) ); std::future\u0026lt;return_type\u0026gt; res = task-\u0026gt;get_future(); { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(queue_mutex); // don\u0026#39;t allow enqueueing after stopping the pool if(stop) throw std::runtime_error(\u0026#34;enqueue on stopped ThreadPool\u0026#34;); tasks.emplace([task](){ (*task)(); }); } condition.notify_one(); return res; } void DataProcessing(std::string\u0026amp; data, const std::string\u0026amp; operation) { data += operation; std::this_thread::sleep_for(std::chrono::seconds(1)); } int main(int argc, char** argv) { ThreadPool thread_pool(4); std::string data = \u0026#34;raw\u0026#34;; auto time1 = std::chrono::system_clock::now(); std::vector\u0026lt;std::future\u0026lt;void\u0026gt;\u0026gt; futures; for (int i = 0; i \u0026lt; 4; ++i) { futures.emplace_back(thread_pool.enqueue(std::bind(DataProcessing, std::ref(data), std::to_string(i)))); } for (auto\u0026amp; future : futures) { try { if (future.valid()) { future.get(); } else { std::cerr \u0026lt;\u0026lt; \u0026#34;Future is invalid\u0026#34;; } } catch (const std::future_error\u0026amp; ex) { std::cerr \u0026lt;\u0026lt; \u0026#34;Caught a future error with code[\u0026#34; \u0026lt;\u0026lt; ex.code() \u0026lt;\u0026lt; \u0026#34;] message[\u0026#34; \u0026lt;\u0026lt; ex.what() \u0026lt;\u0026lt; \u0026#34;].\u0026#34;; throw ex; } } auto time2 = std::chrono::system_clock::now(); std::chrono::duration\u0026lt;double\u0026gt; diff = time2 - time1; std::cout \u0026lt;\u0026lt; data \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; \u0026#34;Time Diff = \u0026#34; \u0026lt;\u0026lt; diff.count() * 1000\u0026lt;\u0026lt; \u0026#34; msec.\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/multi-thread/thread-pool/","title":"ThreadPool"},{"content":"Demo #include \u0026lt;chrono\u0026gt; auto time1 = std::chrono::system_clock::now(); auto time2 = std::chrono::system_clock::now(); std::chrono::duration\u0026lt;double\u0026gt; diff = time2 - time1; ADEBUG \u0026lt;\u0026lt; \u0026#34;Time Diff = \u0026#34; \u0026lt;\u0026lt; diff.count() * 1000\u0026lt;\u0026lt; \u0026#34; msec.\u0026#34;; ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/time-counting/","title":"Time Counting"},{"content":"tmux工作方式 tmux最上级的组织是session,操作如下: tmux new-session -d -s S -n W1 #构建一个叫S的会话且默认窗口名为W1,S:0或者S:W1都可以访问到该窗口,-d代表在后台运行,不加就会直接在终端显示 tmux detach #退出当前会话,依然在后台运行 tmux ls #列出所有会话i tmux kill-session -t S #杀掉一个叫S的会话 第二层组织是windows,构建方式如下: tmux new-window -t S:1 -n W2 #在S会话下,新建一个窗口S:1,重命名为W2,访问的时候S:1和S:W2都可以访问到该窗口 tmux select-window -t S:0 #选中第一个窗口 tmux select-window -t S:W1 #效果同上 tmux kill-window -t S:W2 #杀掉S会话的W2窗口 第三层组织是pane,操作方式如下: tmux selectp -t 0 #选中当前窗口下的第0个pane tmux splitw -h -p 50 #从当前pane(编号0)向右按照50:50的比例分裂出新的pane(编号1) tmux selectp -t 0 #选中当前窗口下的第0个pane tmux splitw -v -p 50 #从当前plane向下按照50:50的比例分裂出新的pane(编号1,原先的编号1--\u0026gt;2) tmux selectp -t 0 #选中第0个pane tmux send-keys -t \u0026#34;roscore\u0026#34; C-m #在当前选中的pane运行roscore命令 tmux kill-pane -t 2 #杀掉第2个pane 基本命令(如果个性化配置后,crtl+b要根据自己的配置修改) Ctrl+b \u0026quot; — 水平分割标签 Ctrl+b % — 竖直分割标签 Ctrl+b 方向键 — 选择标签 按住 Ctrl+b不放，并且按方向键 — 调整标签大小 Ctrl+b c — 创建 (c)reate 一个新窗口 Ctrl+b n — 转到下一个 (n)ext 窗口 Ctrl+b p — 转到之前的 (p)revious 窗口 ctrl+b \u0026amp; — 杀掉当前窗口 tmux 自启动(修改.bashrc) if [ $TERM != \u0026#34;screen-256color\u0026#34; ] \u0026amp;\u0026amp; [ $TERM != \u0026#34;screen\u0026#34; ]; then tmux attach || tmux new; exit fi 改造tmux cd ~ vim .tmux.conf 1.更改前缀(ctrl+b -\u0026gt; ctrl+a) unbind C-b set -g prefix C-a 2.Alt+方向键选择标签 bind -n M-Left select-pane -L bind -n M-Right select-pane -R bind -n M-Up select-pane -U bind -n M-Down select-pane -D 3.活动监听 如果你开了多个窗口，可能想当别的窗口发生什么的时候你能收到通知。粘贴这段命令： setw -g monitor-activity on set -g visual-activity on 4.用指定的颜色高亮显示当前窗口 set-window-option -g window-status-current-bg yellow setw -g monitor-activity on 一键启动脚本demo: 文件名start.sh # 创建会话和窗口 tmux new-session -d -s pickman -n control tmux select-window -t pickman:control # 先构建pane的布局 tmux selectp -t 0 tmux splitw -h -p 50 tmux selectp -t 0 tmux splitw -v -p 50 tmux selectp -t 2 tmux splitw -v -p 50 tmux selectp -t 2 tmux splitw -h -p 50 # 再ctrl+x q查看一下pane的编号,往对应编号的pane里写指令 tmux selectp -t 0 tmux send-keys \u0026#34;roscore\u0026#34; C-m tmux selectp -t 1 tmux send-keys \u0026#34;sleep 1;rosbag play /home/luyifan/BAG/BUG.bag --clock -s 20 -r 1.0\u0026#34; C-m tmux selectp -t 2 tmux send-keys \u0026#34;sleep 2;rosrun rviz rviz -d /home/luyifan/Project/PathPlanning/pickman_ws/src/rviz/pickman.rviz\u0026#34; C-m tmux selectp -t 3 tmux send-keys \u0026#34;sleep 3;rosrun create_map create_map\u0026#34; C-m tmux selectp -t 4 tmux send-keys \u0026#34;sleep 3;rosrun dstar dstar\u0026#34; C-m # 控制rosbag的播放 tmux selectp -t 1 tmux attach -t pickman Tips:用脚本来启动tmux和各类指令的时候,需要先-d再attach出来,否则指令不运行 一键清除脚本demo 文件名kill.sh tmux kill-session -t tiggo ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/tools/terminal/tmux/","title":"Tmux Configuration"},{"content":"背景 Transformer摆脱了NLP任务对于RNN、LSTM的依赖，使用了self-attention的方式对上下文进行建模，提高了训练和推理的速度。 模型结构 第一部分：输入层 主要包含两个主要输入，词向量与位置向量，按元素相加的方式作为模型的输入层。 第二部分：编码器 N表示编码层有N个同样的编码模块，其中每个模块包含有多头注意力层(Multi-Head Attention)与前向连接层(Feed Forward)，每个层之间有层归一化与残差连接层。 多头注意力层(Multi-Head Attention) 前向连接层(Feed Forward) $$FFN(x)=max(0,xW_{1}+b_{1})W_{2}+b_{2}$$ max操作就是Relu激活函数 $$W_{1}\\in R^{d_{model}\\times d_{ff}},W_{2}\\in R^{d_{ff}\\times d_{model}}$$ 层归一化(Norm) $$\\mu^{l}=\\frac{1}{H}\\sum_{i=1}^{H}\\alpha_{i}^{l}$$ $$\\sigma^{l}=\\sqrt{\\frac{1}{H}\\sum_{i=1}^{H}(\\alpha_{i}^{l}-\\mu^{l})^{2}}$$ $$x_{i}^{\u0026rsquo;}=\\frac{x_{i}-\\mu }{\\sigma}$$ 这一步的作用是前面的数据经过激活函数之后距离中心分布比较远，归一化后把这个数据分布又拉回到中心区域了，不至于越往后计算偏差越大 残差连接层 残差单元可以以跳层连接的形式实现，即将单元的输入直接与单元输出加在一起，然后再激活。因此残差网络可以轻松地用主流的自动微分深度学习框架实现，直接使用BP算法更新参数。 第三部分：解码器 解码层也有N个同样的编码模块，与编码层的每个模块不同的是，自注意层用的是一个Masked Multi-Head Attention层，主要是为了在解码的时候，屏蔽掉当前词后面的词（解码的时候只能获得当前单词的历史词）。 除此之外，自注意力层之上连接的是一个从Decoder到Encoder之间的一般注意力层(没有Masked的那个)。这里的\\(K,V\\)来自于Encoder最后一个模块的输出层输出，而\\(Q\\)则是来自Decoder自身上一层（Masked Multi-Head Attention）的输出。计算的是目标语言与源语言之间的注意力得分。 第四部分：输出层 通过Linear层将decoder得到的向量进行变换，得到一个跟词表同样大小的向量，然后经过softmax层进行概率归一化的表示。 此时，得到的向量中每一个元素表示的就是预测的单词为词表中每一个单词的概率。 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/deep-learning/transformer/","title":"Transformer"},{"content":"问题描述 策略梯度算法的更新步长很重要 步长太小，导致更新效率低下 步长太大，导致参数更新的策略比上次更差，通过更差的策略采 样得到的样本更差，导致学习再次更新的参数会更差，最终崩溃 如何选择一个合适的步长，使得每次更新得到的新策略所实现的回报 值单调不减 解决方案 信赖域 (Trust Region) 方法指在该区域内更新，策略所实现的回报 值单调不减 知识背景 自然梯度 Natural Gradient Works Efficiently in Learning, 1998 在黎曼空间里面，最快的下降方向不是梯度方向，而是自然梯度方向\\(G^{-1}(\\theta )J(\\theta )\\) 只有当坐标系统正交，才退化成欧式空间 神经网络中的参数空间是黎曼空间 其中\\(G\\)为 Reimannian metric tensor 统计问题中，\\(G\\)可以用 Hessian 矩阵去计算 保守策略迭代 CPI: Approximately Optimal Approximate Reinforcement Learning, 2002 给出策略性能增长的条件 策略更新后的所有优势函数非负 使用混合更新的方式更新策略 TRPO Trust Region Policy Optimization, ICML2015 以 CPI 为基础，推导出策略更新后性能的下界, 通过优化下界优化原函数 实际操作时用 KL 散度作为约束 求解带约束的优化问题时，利用自然梯度 自然梯度需要求2阶导数，在大规模的神经网络里极其难求 实际求解是利用了共轭梯度 + 线性搜索的方法, 避免求自然梯度 PPO 核心思想 Proximal Policy Optimization Algorithms, 2017 Openai blog(https://blog.openai.com/openai-baselines-ppo/) TRPO 太复杂，普通 PG 效果又不好 PPO 本质上是 TRPO 的简化版 移除了 KL 惩罚项和交替更新，把它变成了正则化项，写到目标函数里 由于性能好，且容易实现，已经成为默认的 OPENAI 算法 知识图谱 核心步骤 实现非常简单 其他信赖域算法 ACKTR Scalable trust-region method for deep reinforcement learning using Kronecker-factored approximation ACER Sample Efficient Actor-Critic with Experience Replay GAE High-Dimensional Continuous Control Using Generalized Advantage Estimation 在估计advantage函数的时候，不是用传统的TD误差值去更新，而是用一种迭代的形式去更新 基本上所有用到advantage的方法，用了这个trick之后，效果都会有所提升 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/reinforcement-learning/trust-region-based-drl/","title":"Trust Region based DRL"},{"content":"初始化\u0026amp;赋值 功能是将某个值与一个对象关联起来 值：字面值、对象（变量、常量）所表示的值 标识符：变量、常量、引用 初始化基本操作 在内存中开辟空间，保存相应的数值 在编译器中构造符号表，将标识符与相关内存空间关联起来 值和对象都有类型 初始化、赋值都可能涉及到内存转换 类型描述 类型是一个编译期概念，可执行文件中不存在类型的概念 C++是强类型语言 类型描述了 存储尺寸(sizeof) 取值空间(numric_limit) 对齐信息(align of) 可执行的操作 使用int8_t, int16_t, int32_t等去强制确定类型的大小 为字面值引入前后缀改变其类型 1.3(double), 1.3f(float) 2(int), 2ULL(unsigned long long) 可以自定义字面值后缀 int operator \u0026#34;\u0026#34; _ddd(long double x) { // 注意：这里能支持自定义后缀的数据类型就只有明确的几种，可以去cppreference里搜user_literal查看 return (int)x * 2; } int main() { int x = 3.14_ddd; // 6 } 变量及其类型 变量的初始化 int x; // 缺省初始化 int x(10); // 直接初始化 int x{10}; // 直接初始化 int x = 10; // 拷贝初始化 为变量赋值时可能发生隐式类型转换 unsigned int x = -1; // x为最大的整数 隐式类型转换不只发生在赋值时 if判断 if (3) { // true } if (-1) { // true } if (0) { // false } 数值比较 int x = -1; unsigned int y = 3; std::cout \u0026lt;\u0026lt; (x \u0026lt; y) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 当无符号数与带符号数进行比较时，C++都转化为无符号数进行比较，所以这里返回false C++20中引入了cmp_xxx操作去处理上面的异常 指针 void*指针 没有记录对象的尺寸信息，可以保存任意地址（某些情况下，我们只需要保存一个指针，而不关注这个指针指向对象的类型） void fun(void* param) { std::cout \u0026lt;\u0026lt; (param + 1) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 报错，不知道param的尺寸，不知道+1是向后移动多少位 } 可以转换为任意类型的指针，也可以由任意类型的指针转换得到 支持判等操作 引用 是对象的别名，不能绑定字面值 构造时绑定某个对象，生命周期之内不能绑定其他对象 不存在空引用，但存在非法引用——总的来说比指针安全 int\u0026amp; fun() { int x; return x; } int main() { int\u0026amp; res = fun(); // 编译能通过，但非法 } 属于编译期概念，底层还是通过指针去实现 指针的引用 指针是对象，可以引用 int* p = \u0026amp;val; int* \u0026amp; ref = p; // 类型信息从右向左引用 指针对比引用 指针可以为空，引用肯定指向某个内存对象 指针的地址可能非法 引用的本质就是不可变的、合法的、指向某块内存的指针 常量引用 constexpr T xx; // xx的数据类型还是const T，只不过constexpr表示它的初始值在编译期就确定了 constexpr const int* ptr = nullptr; // constexpr修饰ptr，所以ptr的类型是const * const 类型别名与类型的自动推导 两种引入类型别名的方法 typedef int MyInt; using MyInt = int; // (since C++11) 使用using引入类型别名更好 typedef char MyCharArr[4]; // 写法比较混乱 using MyCharArr = char[4]; 类型别名和指针、引用的关系 应该将指针的类型别名视为一个整体，在此基础上引入的长量表示指针为常量的类型 using IntPtr = int*; int main() { int x = 3; const IntPtr ptr = \u0026amp;x; const int* ptr = \u0026amp;x; // 上下两行不等价，上面的const修饰的是ptr，下面的const修饰的是指针指向的内容 } 不能通过类型别名构造引用的引用 #include \u0026lt;type_traits\u0026gt; using RefInt = int\u0026amp;; using RefRefInt = RefInt\u0026amp;; int main() { std::is_same_v(RefInt, RefRefInt); // true } 类型的自动推导 常见形式 auto: 最常用的形式，但会产生类型退化 int x = 3; int\u0026amp; ref = x; auto ref2 = ref; // auto 自动推导出来的类型不是int\u0026amp;，而是int const auto/constexpr auto: 推导出的是常量、常量表达式类型 auto\u0026amp;: 推导出引用类型，避免类型退化 const int x = 3; auto\u0026amp; y = x; // y的类型是const int\u0026amp; int x1[3] = {1, 2, 3}; auto x2 = x1; // x2的类型是int* auto\u0026amp; x3 = x1; // x3的类型是int(\u0026amp;)[3] decltype(exp): 返回exp表达式的类型（如果表达式是左值，会加引用） int x = 3; int\u0026amp; y1 = x; auto y2 = y1; // int decltype(y1) y3 = y1; // int\u0026amp; int* ptr = \u0026amp;x; decltype(*ptr); // int\u0026amp;, 因为*ptr是个左值，所以会加上引用 decltype(3.5 + 15l); // double，表达式是右值，所以不加引用 decltype(val): 返回val的类型 int x = 3; decltype(x); // 按照上面的说法，x是左值，所以这里是int\u0026amp;，但其实不是 // 原因是，有个约定，如果decltype后面跟的是一个变量名，不加引用 int* ptr = \u0026amp;x; decltype(ptr); // int*, ptr是个左值，但更是一个变量名，所以不加引用 const int y1 = 3; const int\u0026amp; y2 = y1; decltype(y1); // const int decltype(y2); // const int\u0026amp; decltype((y1)); // const int\u0026amp;, (y1)是一个表达式，且是一个左值，所以加上引用 decltype((y2)); // const int\u0026amp;, (y2)是一个表达式，且是一个左值，所以加上引用，但本身已经有引用，不会变成引用的引用 decltype(auto): since C++14 int x = 3; int\u0026amp; y1 = x; auto y2 = y1; // int，简洁 decltype(y1) y3 = y1; // int\u0026amp;，不会引起退化 decltype(auto) y3 = y1; // int\u0026amp;，简洁且不会引起退化 concept auto: since C++20 std::integral auto y = 3.5; // 编译失败，auto自动推导出来的类型(double、float)都不属于integral的范畴 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/type/","title":"Type"},{"content":"无约束优化方法一览 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/optimization/unconstrained_optimization/","title":"Unconstrained Optimization"},{"content":"联合 引入union的目的是节省内存 struct Str { int x; int y; }; union Str2 { int x; int y; }; union Str3 { char x; int y; }; int main() { std::cout \u0026lt;\u0026lt; sizeof(Str) \u0026lt;\u0026lt; std::endl; // 8 std::cout \u0026lt;\u0026lt; sizeof(Str2) \u0026lt;\u0026lt; std::endl; // 4 Str2 obj; obj.x = 100; std::cout \u0026lt;\u0026lt; obj.y \u0026lt;\u0026lt; std::endl; // 100， x和y就是同一片内存 std::cout \u0026lt;\u0026lt; sizeof(Str3) \u0026lt;\u0026lt; std::endl; // 4， 会选择int和char中较大的那个，这样两者都可以表示 } 通常与枚举一起使用 错误使用方法 union Str { char x; int y; }; int main() { Str obj; obj.x = \u0026#39;c\u0026#39;; std::cout \u0026lt;\u0026lt; obj.y \u0026lt;\u0026lt; std::endl; // 99 // 这里的obj.y会得到多少，是未定义的，因为x只有一个字节，x的内容存在4个字节的Str对象中，具体存在哪个字节里是编译器决定的。不同编译器标准不一样 } 正确使用方法 struct S { enum Type { Char, Int }; union Str { char x; int y; }; Type t; Str obj; }; int main() { S s; s.t = S::Char; s.obj = \u0026#39;c\u0026#39;; } 匿名联合 struct S { enum Type { Char, Int }; union { char x; int y; }; Type t; }; int main() { S s; s.t = S::Char; s.x = \u0026#39;c\u0026#39;; } 在联合中包含非内建类型(C++11起) ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/union/","title":"Union"},{"content":"volatile关键字 表明一个对象的可能会被当前程序以外的逻辑修改。 加了volatile修饰的对象，在读的时候，不能直接从缓存拿，需要去内存拿（因为内存可能已经被其他程序修改了，缓存中的数据存在延迟）；在写的时候，写入缓存后还需要立刻刷新到内存里。 注意慎重使用——一些情况下可以用atomic代替 ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/coding/c++/volatile/","title":"Volatile"},{"content":"背景 vscode vim插件的个人简单配置模版 配置内容 { // vim config \u0026#34;editor.lineNumbers\u0026#34;: \u0026#34;relative\u0026#34;, \u0026#34;workbench.list.automaticKeyboardNavigation\u0026#34;: false, // 绑定vim前导键 \u0026#34;vim.leader\u0026#34;: \u0026#34;\u0026lt;space\u0026gt;\u0026#34;, // 启用easymotion插件 \u0026#34;vim.easymotion\u0026#34;: true, // 启用系统粘贴板作为vim寄存器 \u0026#34;vim.useSystemClipboard\u0026#34;: true, // 由vim接管ctrl+any的按键，而不是vscode \u0026#34;vim.useCtrlKeys\u0026#34;: true, // 突出显示与当前搜索匹配的所有文本 \u0026#34;vim.hlsearch\u0026#34;: true, // 普通模式下的非递归按键绑定 \u0026#34;vim.normalModeKeyBindingsNonRecursive\u0026#34;: [ // zz展开收起括号内的内容 { \u0026#34;before\u0026#34;: [\u0026#34;z\u0026#34;, \u0026#34;z\u0026#34;], \u0026#34;commands\u0026#34;: [\u0026#34;editor.toggleFold\u0026#34;] }, // 保存\u0026amp;退出当前文本 { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;leader\u0026gt;\u0026#34;, \u0026#34;s\u0026#34;], \u0026#34;commands\u0026#34;:[\u0026#34;:w!\u0026#34;] }, { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;leader\u0026gt;\u0026#34;, \u0026#34;q\u0026#34;], \u0026#34;commands\u0026#34;: [\u0026#34;:q!\u0026#34;] }, { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;leader\u0026gt;\u0026#34;, \u0026#34;sq\u0026#34;], \u0026#34;commands\u0026#34;: [\u0026#34;:wq!\u0026#34;] }, // 上下跳5行 { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;C-j\u0026gt;\u0026#34;], \u0026#34;after\u0026#34;: [\u0026#34;5\u0026#34;, \u0026#34;j\u0026#34;] }, { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;C-k\u0026gt;\u0026#34;], \u0026#34;after\u0026#34;: [\u0026#34;5\u0026#34;, \u0026#34;k\u0026#34;] }, // 行首行尾 { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;C-h\u0026gt;\u0026#34;], \u0026#34;after\u0026#34;: [\u0026#34;^\u0026#34;] }, { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;C-l\u0026gt;\u0026#34;], \u0026#34;after\u0026#34;: [\u0026#34;$\u0026#34;] }, // 部分格式化 { \u0026#34;before\u0026#34;: [\u0026#34;f\u0026#34;, \u0026#34;f\u0026#34;], \u0026#34;commands\u0026#34;: [\u0026#34;editor.action.formatSelection\u0026#34;] }, ], // 插入模式下的非递归按键绑定 \u0026#34;vim.insertModeKeyBindings\u0026#34;: [], // 命令模式下的非递归按键绑定 \u0026#34;vim.commandLineModeKeyBindingsNonRecursive\u0026#34;: [], // 可视模式下的非递归按键绑定 \u0026#34;vim.visualModeKeyBindings\u0026#34;: [ // 上下跳5行 { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;C-j\u0026gt;\u0026#34;], \u0026#34;after\u0026#34;: [\u0026#34;5\u0026#34;, \u0026#34;j\u0026#34;] }, { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;C-k\u0026gt;\u0026#34;], \u0026#34;after\u0026#34;: [\u0026#34;5\u0026#34;, \u0026#34;k\u0026#34;] }, // 行首行尾 { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;C-h\u0026gt;\u0026#34;], \u0026#34;after\u0026#34;: [\u0026#34;^\u0026#34;] }, { \u0026#34;before\u0026#34;: [\u0026#34;\u0026lt;C-l\u0026gt;\u0026#34;], \u0026#34;after\u0026#34;: [\u0026#34;$\u0026#34;] }, // 部分格式化 { \u0026#34;before\u0026#34;: [\u0026#34;f\u0026#34;, \u0026#34;f\u0026#34;], \u0026#34;commands\u0026#34;: [\u0026#34;editor.action.formatSelection\u0026#34;] }, ], // 下面定义的按键将交由vscode进行处理，而不是vscode-vim插件 \u0026#34;vim.handleKeys\u0026#34;: { \u0026#34;\u0026lt;C-a\u0026gt;\u0026#34;: false, \u0026#34;\u0026lt;C-f\u0026gt;\u0026#34;: false, \u0026#34;\u0026lt;C-p\u0026gt;\u0026#34;: false, // \u0026#34;\u0026lt;C-k\u0026gt;\u0026#34;: false }, } ","date":"2025-04-02T10:32:39+08:00","permalink":"/post/tools/editor/vscode-vim/","title":"Vscode Vim Configuration"},{"content":" 论文：EMMA: End-to-End Multimodal Model for Autonomous Driving 机构：Waymo 时间：2024年10月 基座模型：Google Gemini\n一、核心思想：把大模型塞进方向盘 想象一下，如果把一个上知天文下知地理的 AI 大模型（比如 ChatGPT 或 Gemini）直接塞进汽车的方向盘里，让它代替所有复杂的传感器处理和控制模块，它能学会开车吗？\nWaymo 给出了响亮的回答：能！而且开得非常聪明！\n这篇论文在自动驾驶圈相当于投下了一颗\u0026quot;深水炸弹\u0026quot;，标志着自动驾驶向大一统多模态模型迈出了里程碑式的一步。\n二、架构革新：掀翻\u0026quot;流水线\u0026quot;，请来\u0026quot;老司机\u0026quot; 传统方案的困境 传统自动驾驶系统就像一个流水线工厂：\n感知部门（Perception）：专门负责\u0026quot;看\u0026quot; 预测部门（Prediction）：专门负责\u0026quot;猜\u0026quot; 规划部门（Planning）：专门负责\u0026quot;打方向盘\u0026quot; 这种分工明确的模式虽然好调试，但存在致命问题——误差传递（Error Propagation）：感知部门如果漏看了一个行人，后面的部门就全跟着完蛋。\nEMMA 的颠覆性设计 EMMA 直接把桌子掀了！\n它基于 Google 的 Gemini 多模态大语言模型，打造了一个纯粹的**端到端（End-to-End）**架构：\n摄像头拍到的原始画面一进去 模型大脑一转 直接输出汽车下一步该怎么走（轨迹坐标） 没有中间商赚差价，减少了信息损耗！\n三、核心必杀技：万物皆文本（Text-to-Float） 大语言模型是靠处理文字起家的，那它怎么理解物理世界里的距离、速度和三维坐标？\n答案：把开车变成做阅读理解和视觉问答（VQA）！\n输入端 类型 内容 形式 视觉 多视角原始摄像头视频流 图像 文本 导航指令（\u0026ldquo;前方右转\u0026rdquo;） 自然语言 文本 自车历史轨迹点坐标 (x, y) 文本形式 ⚠️ 关键点：EMMA 没有接入 LiDAR 激光雷达或毫米波雷达，是纯视觉主导的方案！\n输出端 模型不会输出晦涩的底层控制代码，而是直接用文本输出：\n未来的轨迹坐标点 (x, y) 3D 感知物体 道路图（Road Graph）元素 为什么用 Text-to-Float？ 作者尝试过为物理坐标发明专门的\u0026quot;控制 Token\u0026quot;，但最终发现：\n直接让模型\u0026quot;说\u0026quot;出带小数点的文本效果更好！\n比如：预测轨迹：(5.2, 3.1) and (6.0, 3.5)\n这样做的核心优势：\n所有驾驶任务都在同一个统一的语言空间里运行 最大化\u0026quot;白嫖\u0026quot; Gemini 预训练模型里如星辰大海般的世界知识（World Knowledge） 四、老司机的\u0026quot;内心戏\u0026quot;：思维链推理（Chain-of-Thought, CoT） 如果 AI 只会机械地输出坐标，那就是个危险的\u0026quot;黑盒\u0026quot;。为了让 EMMA 更靠谱、更有可解释性，Waymo 给它加入了 CoT（思维链） 提示技术。\n在决定怎么打方向盘之前，EMMA 会在脑子里进行严密的四步\u0026quot;碎碎念\u0026quot;：\n步骤 名称 内容示例 R1 Scene Description（场景描述） 环顾四周，弄清大环境：\u0026ldquo;现在是个拥堵的十字路口\u0026rdquo; R2 Critical Objects（关键物体） 精准定位会对自车产生影响的物体：\u0026ldquo;注意！右前方有个行人，他的 3D 坐标是 (X,Y,Z)\u0026rdquo; R3 Behavior Description（行为描述） 预测意图：\u0026ldquo;这个行人正准备横穿马路\u0026rdquo; R4 Meta Driving Decision（宏观决策） 下达高层级指令：\u0026ldquo;我得减速让行\u0026rdquo; 完成这套内心戏后，它才会输出最终的轨迹坐标。\n实验证明：加上这种逻辑推理后，运动规划质量大幅度提升！\n五、\u0026ldquo;十项全能王\u0026rdquo;：多任务联合训练 既然 EMMA 把任务都变成了统一的\u0026quot;视觉问答\u0026quot;，那它绝不满足于只当一个司机。\n通过输入不同的 Prompt（提示词），它可以同时干好几份工作！\n反直觉的发现 研究人员发现了一个极其反直觉的惊喜：\n联合训练（Co-training）不仅没有让模型\u0026quot;学杂了\u0026quot;，反而产生了强大的化学反应！\n当把以下三个任务绑在一起让 EMMA 学习时：\n端到端运动规划 3D 目标检测 道路图估计 各方面的表现都迎来了飞跃！例如，多任务联合训练下，目标检测性能甚至提升了 5.5%！\n跑分战绩 🏆 成绩单相当惊人：\n数据集 任务 表现 nuScenes 运动规划 SOTA（当前世界第一） WOMD 运动规划 超越此前顶尖方法 WOD 3D 目标检测 精确度和召回率领先 六、阿喀琉斯之踵：现存的局限 论文最后，作者非常坦诚地暴露了 EMMA 目前面临的几个硬伤：\n1. 算力和显存刺客 背后是一个庞大的大语言模型，推理成本极高，延迟较大。\n优化手段：\nSARA-RT 技术 移除显式推理链 优化效果：\n勉强把速度提到了 3 FPS 相比 UniAD 的 1.8 FPS 快了 67% 但在自动驾驶这种人命关天的毫秒级战场，这个速度还需要进一步优化。\n2. 短期记忆 为了控制计算量，目前只能吃进去极少量的历史图像帧，无法理解过长的时间序列。\n3. 缺少物理 3D 传感器 作为纯视觉模型，没有接入 LiDAR 或雷达，在面对极端恶劣天气或极其需要高精度深度信息的场景时，是个天然短板。\n七、总结 《EMMA》这篇论文证明了一个令人兴奋的事实：\n只要大模型的\u0026quot;底子\u0026quot;足够好，哪怕不用复杂的专用模块、不用复杂的特殊 Token、不带昂贵的激光雷达，仅仅靠着\u0026quot;看图像\u0026quot;和\u0026quot;讲大白话\u0026quot;，AI 也能成为一个极其优秀的\u0026quot;赛博老司机\u0026quot;！\n这是自动驾驶向大一统多模态模型迈出的里程碑式一步，为未来的发展指明了一个极具潜力的方向。\n相关链接 论文链接：https://arxiv.org/pdf/2410.23262 关联笔记：[[DiffusionDriveV2 论文阅读笔记]] #自动驾驶 #E2E #多模态大模型 #Waymo #论文笔记\n","date":"2024-10-01T00:00:00Z","permalink":"/post/robotics/e2e/emma/","title":"EMMA: End-to-End Multimodal Model for Autonomous Driving"}]