Repository navigation
Expand file tree
/
Copy pathindex.json
More file actions
1 lines (1 loc) · 478 KB
/
Copy pathindex.json
File metadata and controls
1 lines (1 loc) · 478 KB
1
[{"content":" How to Get Started with Embodied AI Research # 转载时间:2025-11-13 © PKU EPIC Lab. All rights reserved. Commercial distribution prohibited.\n© PKU EPIC Lab. 版权所有。禁止商业传播。\n原文主页\n作者:吕江燃,张嘉曌,邓胜亮,陈嘉毅,严汨,李忆唐\n指导老师:王鹤,弋力\nO、前言 # 随着具身智能的关注度不断升高,越来越多的研究者涌入这一领域,相关论文数量也呈现井喷式增长。然而,其中不少工作的质量令人担忧:有的只是单纯“讲故事”,有的则一味追求“刷榜”,但这些却反而获得了大量的关注和追捧。在这样的科研环境下,为新同学提供正确的引导,帮助他们少走弯路、健康成长,是一个重要的任务。\n因此,我们撰写本文的目的,就是希望为刚进入具身智能科研领域的同学提供一个清晰的 guide,帮助大家理解具身智能究竟该研究什么,以及如何正确地入门。希望能帮助到初学者积累必要基础知识的同时,建立起正确的研究认知。\n本文章也将作为 PKU EPIC Lab 本科生的入门材料,并会在实践和培养的过程中不断更新和完善。\n一、基础概念 (Basic Concepts) # 1. 什么是具身智能 (What is Embodied AI) # 具身智能(Embodied AI)是指能够在物理或虚拟环境中通过感知、行动和交互来学习与完成任务的人工智能。不同于仅在静态数据(文本、图像、语音等)上进行训练和推理的传统 AI,具身智能的智能体(agent)往往有一个“身体”(body)或“化身”(avatar),它们可以与环境交互,改变环境,并随着环境的改变自己作出调整。\n典型的具身智能研究对象包括机器人和虚拟环境中的智能体,本文主要面向机器人领域(Robotics)。\n核心特征:\n拥有多模态感知能力(视觉、触觉、语音等) 能够执行动作并影响环境 学习可以通过与环境交互而不仅仅是被动监督完成 2. 具身智能与其他 AI 的区别 (Differences from Traditional AI) # 具身智能与传统 AI 的主要区别在于它的主动性、交互性,以及对动作数据的依赖。传统 AI 可以利用互联网上丰富的图像、文本、语音等大规模数据集进行训练(参考 LLM 的成功),而具身智能体所需的动作数据必须通过与环境的真实交互来收集,这使得数据获取代价高昂且规模有限。一言以蔽之,数据问题是具身智能目前最大的 bottleneck。那么很自然的两个关键问题是,\n如何 scale up 机器人数据? 例如:GraspVLA(在仿真中以合成的方式猛猛造), pi0和AgiBot-World(在真实世界猛猛遥操采), UMI和AirExo(可穿戴设备,如外骨骼的高效数据采集装置) 在不能 scale up 机器人数据的情况下,如何利用好已有的数据实现你的目的? 例如:Diffusion Policy (100 条机器人数据训一个特定任务的 policy), Being-H0(利用 human video 参与 policy 训练),MimicGen、DemoGen、Robosplat(从一条机器人轨迹中 augment 得到更多数据) 3. 研究具身智能的核心原则 (Core Principles) # 首先把任务定义(task formulation)想清楚,而不是一开始就盯着模型。在 CV 领域,研究者之所以可以直接关注模型,是因为任务往往已经被定义得很清晰,数据集也由他人整理好, 比如图像分类就是输入图片输出类别标签,检测就是输出四个数的 bounding box;\n但在具身智能中,如何合理地建模任务、确定目标与评价指标,往往比模型选择更为关键。说白了,你得知道你想让机器人学会什么样的技能,输入是啥,输出是啥,用的什么传感器?你所研究的问题是否在合理的 setting 下?有没有有可能通过更好的 setting 来解决问题(比如机器人头部相机对场景观测不全,那我们可以考虑加装腕部相机,或者使用鱼眼相机)\n必须认识到用学习(learning)来解决机器人问题并不是理所当然的选择。在许多场景中,传统的控制(Control)、规划(Planning)或优化方法(Optimization)依然高效且可靠,而学习方法更多是在任务复杂、环境多变(泛化性) 或缺乏解析建模手段时才展现优势。因此,做具身智能研究时,首先要想回答,为什么你研究的这件事传统 robotics 解决不了?为什么非得用 learning?\n二、前置技能 # 这些工具是一个当代 CS researcher 的必备技能(不局限于方向),主要学习资料可以参考 https://missing-semester-cn.github.io/\nPython, Conda and Pytorch Linux Shell, Git, SSH LLM, Cursor Docker 三、AI and Robotics Basis # 以下三门课是基础课程,对于初学者希望能详细的掌握内容,不要“不求甚解”,对于课程 Lab 的 project 最好做到完整实现,而不仅局限于做“代码填空”。\n1. Intro-to-Embodied-AI # 实验室内部课程,主要内容是 robotics 的基础概念和基于 learning 的 robotics,源于王鹤老师《具身智能导论》,外界同学自行寻找类似课程替代\n2. Intro-to-CV # 实验室内部课程,主要内容是 cv 基础和 deep learning,源于王鹤老师《计算机视觉导论》,外界同学可以学习 Stanford CS231N 替代\n3. (Optional) Deep Reinforcement Learning (CS285) # Berkeley 的 RL 课程,涵盖了 Imitation Learning,Online RL, Offline RL 等 Policy Learning 范式\n四、研究平台与工具 (Research Platforms and Tools) # 1. 模拟环境 (Simulation Environments) # Simulation 的意义:在大多情况下,真机部署机器人是不方便的,所以 simulation 提供了一个很好的代替。主要有两大用途,1.作为高效的数据源,解决真实世界机器人数据少的问题 2.真机测试 policy 不方便,很难复现,作为一个更通用的 benchmark evaluation 平台\n对于 simulation 的掌握:需要至少一种 simulation 框架,通过阅读 tutorial 跑他的 example,加深对 robotics 的理解,不要等到上真机才发现有很多坑,会有很大的安全隐患\nIssacLab (Recommend)\nhttps://isaac-sim.github.io/IsaacLab/main/index.html\nhttps://playground.mujoco.org/\n2. 机器人平台 (Robotic Platforms) # 真机实验下的机械臂通讯接口,至少熟悉一类常用的 API\n基于 ros1 的 franka 通讯:\nrail-berkeley/serl_franka_controllers Cartesian impedance controller with reference limiting for Franka Emika Robot C\u0026#43;\u0026#43; 187 27 原始地址 https://github.com/rail-berkeley/serl_franka_controllers 3. Embodied AI Daily ArXiv (Advanced) # 具身智能每日最新的论文,按 manipulation,VLA, dexterous,humanoid 等关键词进行划分,推荐基础入门后的同学每日最终最新进展,丰富自己的认知和视野\n你可以访问每日具身智能论文查看最新进展。\njiangranlv/robotics_arXiv_daily Fetching Embodied AI Paper from ArXiv automatically Python 190 14 原始地址 https://github.com/jiangranlv/robotics_arXiv_daily 五、对机器人技能的研究 (Research on Robot Skills) # 1. Grasping # 抓取(Grasping)是机器人学中最基础且最重要的任务之一,通常指让机器人末端牢牢抓紧物体以达到力闭合(force closure),成功完成抓取后可将物体视作机器人的一部分进行后续的移动和操作。\n常见任务有(难度依次递增):\nSingle object grasping(单物体抓取):抓取一个物体,物体通常放在桌子上。 Clutter scene grasping(堆叠场景抓取):抓取堆叠场景中的物体,通常要求清台(全部抓完)。难点在物体的互相遮挡和干扰。 Functional grasping(带语义抓取):根据语言指令进行抓取。对于单物体抓取而言,语言通常指定物体要抓的 part 和抓取的手势;对于堆叠场景而言,还可以指定要抓的物体。难点在语言模态的引入。 常用机械手末端有(难度依次递增):\nSuction cup(吸盘):控制维度最低,除了末端整体的旋转和平移的自由度之外,只有是否施加吸力的 0/1 控制信号。 Parallel gripper(平行夹抓):类似吸盘。学术上通常认为吸盘/平行夹抓+堆叠场景抓取已经被DexNet和GraspNet两个系列工作几乎解决(思路:大规模仿真抓取位姿 + 学习位姿预测网络 + sim2real) Multi-fingered hand(多指手),又称Dexterous hand(灵巧手):更高的可控自由度和更高的潜力,但也极大地增加了数据构造与学习的难度,导致其发展远落后于前两者。大规模仿真抓取位姿的进展/Dataset:DexGraspNet、Dexonomy(覆盖多样化手型)。 常见的做法:\nOpen-loop methods(开环执行):通过一次性预测抓取位姿并直接执行,不依赖执行过程中的感知反馈。可以直观理解为“看一次决定怎么抓”,执行时全程不再依赖视觉,仅依靠运动规划达到目标位姿。因此开环方法的核心是 grasping pose estimation。Data Source:Grasp Synthesis,如 DexNet、GraspNet-1B. Learning Approaches:GSNet。 Closed-loop methods(闭环执行):在执行过程中持续使用视觉或触觉反馈进行动态调整,从而提升抓取的鲁棒性。这类闭环模型可视为 policy,持续输入视觉信息并输出机械臂动作。代表工作:GraspVLA。 2. Manipulation # 操作(Manipulation)比抓取的含义更广,允许手和物体间有频繁的接触点变化,不像抓取任务中接触点形成后就固定不变了。通常只要是改变了物体状态的任务就可以叫操作。\nArticulated Object Manipulation:铰链物体操作(如开门、拉抽屉、开柜子)。该类任务通常被简化成抓取任务来处理:1.Part 理解(GAPartNet)2.抓取(Grasping)3.抓取后的操作轨迹规划 4.拉取力度控制(Impedance Control) Deformable Object Manipulation:柔性物体操作(如叠衣服、挂衣服)。难点在于柔性物体自由度极高、难以精确建模和仿真。常见做法通常基于人工设计的原子操作(action primitives),最近也有一些公司(pai,dyna)开始用数采+端到端学习的方式来直接做。 Non-prehensile Manipulation:非抓握操作,指通过推、拨、翻转等方式在无抓握的情况下操控物体至指定姿态。难点在于 contact-rich 的动力学特性,机器人、物体与环境存在多重接触与碰撞,如何生成成功的操作轨迹是当前研究重点。 Dexterous Manipulation:灵巧操作,与 non-prehensile 类似,但通常有更多的 contact 和更高的控制维度。一个经典的任务是 in-hand reorientation,虽然它已经几乎被 RL 解决,但如何提升学习效率、拓展到更一般的灵巧操作任务上依旧是研究难点。 Bimanual Manipulation:双臂操作,重点在于如何实现双臂的协调与配合。 Mobile Manipulation:移动操作,强调移动系统为操作提供更大、更灵活的工作空间,移动如何为操作服务,两者如何协同 3. Navigation # Navigation 导航研究机器人如何在物理环境中移动,以完成给定任务。导航能力是一种综合能力,从高层次来看,包括对视觉、深度信息和指令的理解,以及对历史信息(如地图、Tokens 等)的建模;从低层次来看,还包含路径规划与避障。导航通常涉及场景级别的移动,是硬件、传感器与控制算法综合能力的体现。\n常见任务包括:\nPoint Goal Navigation (PointNav):给定目标点坐标或相对方向,机器人需从起始位置导航至目标点。不涉及语义理解,属于低层任务。 Object Goal Navigation (ObjectNav):根据目标物体类别(如“椅子”),在未知环境中寻找并导航至目标物体。 Vision-Language Navigation (VLN):根据自然语言指令(如“走到厨房的桌子旁”),结合视觉感知完成导航任务。 Embodied Question Answering (EQA):机器人需在环境中探索、感知并回答与场景相关的问题(如“卧室里有几张床?”)。 Tracking:机器人持续感知并跟随动态目标(如人或移动物体)。 常见做法:\nMap-based Navigation, 基于地图的导航算法会利用深度图,里程计等信息构建地图,从而基于地图规划路径完成导航任务。基于地图的方法在静态或者易结构化的场景下表现非常好。相关工作包括: Object Goal Navigation using Goal-Oriented Semantic Exploration Prompting-Large-Model Navigation,通过对物理世界进行解释得到 prompting,然后以现成(off-the-shelf)的大模型作为规划决策的中心。这种方法不需要训练复杂的大模型,且可以利用大模型的智能优势实现复杂的导航任务。相关工作包括: NavGPT, CogNav Video-based VLM Navigation, 通过端到端训练基于视频输入的视觉语言大模型,通过 tokens 来建模导航历史,和用 VLM 直接输出未来导航动作。相关工作NaVid Unified Embodied Navigation:最新研究趋势是将多种导航任务统一建模,常使用纯 RGB 输入,并将目标描述转换为语言指令。代表性工作:Uni-Navid,统一多种导航任务。NavFoM,统一导航任务和 embodiment。\n4. Locomotion # Locomotion 强调机器人在多样环境中的运动与机动能力。狭义上通常指基于 Whole-body Control (WBC) 的控制方法,用于实现 四足(Quadrupedal) 与 双足(Bipedal / Humanoid) 运动。\n技术路线上,2019 年以前主要靠传统的 MPC 控制实现(例如波士顿动力),目前主流的方法是 Sim2Real RL, 以下主要讨论这类主流范式。 既然谈及 RL,又分为\nLearning from manually designed reward 自己写 reward 提供 desired behavior, 例如WoCoCo\u0026mdash;-任务目的:通过 reward 设计让机器人完成某些特定任务 Learning from human data (data 提供 desired behavior,也叫做 tracking,例如ASAP)\u0026mdash;-任务目的:模仿某一段人类数据中的动作(输入:现在的 state 和目标的 state;输出这一步的 action) 如果人形机器人能完成对特定人类动作的 tracking,那么接下来就有了一个很主流的研究方向,general motion tracking -\u0026gt; whole-body teleopration,人在做任何一段动作的时候,机器人可以复现人的动作(这里的难点就很多了,动作输入形式的多样性,减少延时,长程复现人的动作,复现的精准度) 这一系列的工作是 H2O, OmniH2O, HOMIE, TWIST, CLONE, HOVER, GMT, Unitrack 等等,至此 Control 最基本的问题应该 well-defined 了\n下一个阶段会涉及到一点除了 control 之外的东西,就是\n引入【视觉】实现户外自主化(perceptive locomotion);例如,根据视觉来进行上楼梯,迈台阶,难点:vision sim2real VisualMimic 引入【物体】实现 loco-manipulation;例如人型机器人搬箱子,难点:物体的 dynamics. HDMI 对上述两种 task 的组合 强调【语义的泛化性】,希望能根据各种各样的场景/物体【自主决策】做出相应的动作(whole body VLA)LeVERB 强调一些特殊的 capability(比如HuB做极端平衡,Any2Track受很大的力干扰摔不倒, HITTER做一个特殊的乒乓球 task) 六、基于 learning 的主要研究方向 # 1. Few-shot Imitation Learning # 该方向主要聚焦于 小模型 (small-model) 场景:给定一个特定任务,以及数量有限的专家轨迹数据集(比如 50 条轨迹),学习一个策略来模仿专家轨迹完成任务。能够在一定范围内实现泛化,例如在同一张桌面上对同一物体的不同初始位置泛化。\n传统方法:Behavior Cloning、DAgger\n当前主流方法:ACT、Diffusion Policy\n这些方法通过引入时序建模与生成式策略学习,有效提升了模仿学习在视觉控制任务中的表现。\n2. Robot Foundation Model # 该方向属于 大模型 (foundation model) 范式,旨在通过统一的模型架构与大规模数据学习,使机器人具备跨任务、跨场景、跨模态的泛化能力。不同于传统在特定任务上单独训练的策略模型,这类模型试图构建“通用机器人智能(generalist robot)”,让机器人能够像语言模型一样,通过大规模预训练与下游微调实现“涌现式”的智能行为。\n目前主流的做法是 Vision-Language-Action Models (VLA), 借助 VLM 的预训练知识将视觉、语言与动作建模统一在同一框架下。代表性工作:\nOpenVLA:第一个开源且易于 follow 的 VLA。 Pi0 / Pi0.5:目前公认最 work 的 VLA,10K+ hours teleop data 训练的。 GraspVLA:基于纯仿真数据的抓取任务的 VLA。 还有少量工作没有借助 VLM,单纯靠机器人数据做 scaling,代表有 RDT-1B 和 Large Behavior Model (LBM)\n3. Sim-to-Real Reinforcement Learning (Distillation) # 从仿真到真实 (Sim-to-Real) 是强化学习在具身智能中的关键挑战之一。\n目前最成功的落地应用集中在 Locomotion(运动控制),而在 Manipulation(操作任务) 上仍面临 sim2real Gap 过大的问题。\n核心思路通常包括 策略蒸馏 (policy distillation)、域随机化 (domain randomization) 与 现实校准 (real calibration) 等技术。\n4. Real-World Reinforcement Learning # Real-world RL 指直接在现实环境中进行探索式学习。\n这类方法通常用于解决高度挑战性的具体任务(如插入 USB),目标是将成功率优化至接近 100%。\n从零开始的真实世界强化学习:Hil-Serl 基于 VLA 的真实世界微调 (Fine-tuning):部分近期工作尝试利用预训练 VLA 进行现实强化学习微调,但仍处于早期探索阶段。 5. World Models # World Model 最早起源于 基于模型的强化学习 (Model-based RL),旨在通过内部世界建模来提升采样效率。\n代表性工作包括 Dreamer 系列(Dreamer, DreamerV2, DreamerV3),通过学习潜在动态模型,实现“在脑中想象未来”式的策略更新。\n在具身智能的最新语境中,World Model 的概念被拓展为 条件视频生成模型 (conditioned video generation model),用于模拟未来观测、预测任务后果,并与规划模块或语言模型结合以实现长期推理。\n七、相关领域 # 1. Graphics # 图形学在机器人与具身智能中的两大重要应用是 simulation(仿真) 与 rendering(渲染)。\nSimulation:用于搭建虚拟的物理交互环境,是机器人强化学习、控制算法和策略验证的重要工具。如上述 IsaacLab 等 Rendering:用于生成高质量的图像或视频,支撑感知模型(如视觉 Transformer)的训练与评估。例如:Blender:开源的三维建模与渲染软件。 系统性学习图形学推荐课程:Games 101, 103 2. Hardware # 硬件是具身智能的“身体基础”,涵盖操作、感知与反馈等环节。\nTele-operation(遥操作)\n末端操作设备:如 Space Mouse,用于控制机械臂的末端姿态。 主从臂系统:如 Gello,实现高精度的力控遥操作。 可穿戴设备:如 AirExo 或 UMI,通过外骨骼或手部设备实现自然交互与示教。 Sensors(传感器)\nCamera(视觉):RGB / RGB-D 相机,如 RealSense、ZED、Azure Kinect。 Force Sensor(力传感器):用于检测接触力矩,常安装于末端。 Tactile Sensor(触觉传感器):如 GelSight、DIGIT,用于捕捉表面接触信息。 Mocap System(动作捕捉系统)\n用于精确追踪人体或机器人位姿,常用于收集示教数据或标定\n3. Mainstream Models # Transformer\nDiffusion、Flow Matching 由于能够有效建模多峰分布的生成模型 sota。\n4. Foundation Models # LLM(Large Language Model) 通过大规模文本训练获得强大的语言理解与推理能力,是具身智能中语言规划与高层决策的重要基石。代表模型包括:GPT / Claude / Gemini:通用语言推理模型。\nVision Encoder\nDINO 系列:通过大规模的自监督学习 (self-supervised learning) 提取图像的细粒度语义表示,在机器人视觉任务中常用于特征提取与场景理解。 CLIP:通过大规模的图文匹配对上的 对比学习 (contrastive learning) ,将图像与文本映射到共享的多模态语义空间,成为视觉语言理解的核心模型。 VLM(Vision-Language Model) 通过大规模的图文理解数据进行训练,获得强大的视觉语言理解能力,在机器人视觉任务中常用于 VLA 模型的初始化,或用于场景理解与任务规划。代表模型包括:Qwen-VL 系列、GPT4-o、Gemini。\n5. 3D Vision # 详见 Intro-to-CV 课程,此处仅给出一些具身任务中常用的三维视觉技术。\n三维生成与重建 相机标定:利用标定版构建多组约束,从而求解相机参数,常用于获取机器人坐标系与相机坐标系之间的变换矩阵。 单目三维生成:根据单张 RGB 图片生成对应物体的三维几何,在 real-to-sim 中是一种常用的获得物体几何的方法。 单目深度估计:通过单张 RGB 图片估计场景深度,常用于将互联网或是二维生成模型的输出结果转换为三维视觉信号。 位姿估计与追踪:通过单张或多张 RGB 图片估计物体或相机的位姿,常用于提取二维图片或视频中的物体或是人手位姿,进一步作为 action 的一种表征。 三维表示 网格(Mesh):通过三角形网格表示三维几何,物理仿真中最常用的三维表示方式。 点云(Point Cloud):通过物体表面的点的集合来表示三维几何。现有的点云处理网络具有很好的捕捉局部几何的能力,因此 GraspNet 使用点云作为输入,实现了非常鲁棒的抓取位姿预测。 Gaussian Splatting:通过高斯分布表示三维几何,由于其可微渲染与快速计算的特点,成为沟通二维与三维的桥梁。在 real-to-sim 中是一种常用的重建场景几何的表示。 三维理解 包括三维分类、场景分割、实例检测、空间推理等任务,常用于机器人视觉任务中的场景理解与任务规划。 八、(Optional) 科研工作中的必备能力 # Sharp Mind:戳穿别人工作的包装,看到本质\nWriting and Presentation: 包装自己的工作,别让别人拆穿\nWarm Mind:不要一味的批评别人的工作,能够欣赏到别人的亮点\n相关仓库 # TianxingChen/Embodied-AI-Guide [Lumina Embodied AI] 具身智能技术指南 Embodied-AI-Guide null 10192 692 原始地址 https://github.com/TianxingChen/Embodied-AI-Guide ","date":"2025年11月13日","externalUrl":null,"permalink":"/posts/%E5%A6%82%E4%BD%95%E5%85%A5%E9%97%A8%E5%85%B7%E8%BA%AB%E6%99%BA%E8%83%BD%E7%A0%94%E7%A9%B6/","section":"文章","summary":"","title":"【转载】如何入门具身智能研究","type":"posts"},{"content":"","date":"2025年11月13日","externalUrl":null,"permalink":"/tags/embodied-ai/","section":"Tags","summary":"","title":"Embodied-Ai","type":"tags"},{"content":"","date":"2025年11月13日","externalUrl":null,"permalink":"/","section":"MyBlog","summary":"","title":"MyBlog","type":"page"},{"content":"","date":"2025年11月13日","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2025年11月6日","externalUrl":null,"permalink":"/series/mit-6.s184/","section":"Series","summary":"","title":"MIT 6.S184","type":"series"},{"content":" 是我对课程笔记的梳理和理解,会包含大量的原文内容,英语基础好的建议先自行阅读课程笔记 PDF,然后再阅读我的笔记。 MIT Computer Science Class 6.S184: Generative AI with Stochastic Differential Equations 课程笔记 PDF 同时,推荐在学习的过程中完成对应的作业。 0 数学基础 # A.1 随机向量(Random Vectors,RVs) # 考虑 $d$ 维欧几里得空间中的数据 $x=(x^1,\\ldots,x^d)\\in\\mathbb{R}^d$,其内积为 $\\langle x,y\\rangle=\\sum_{i=1}^d x^i y^i$,相应的范数为 $\\|x\\|=\\sqrt{\\langle x,x\\rangle}$。\n我们将考虑取值于 $\\mathbb{R}^d$ 的随机变量(random variables,RVs)$X$,其具有连续型概率密度函数(probability density function,PDF),定义为连续函数\n$$ p_X:\\mathbb{R}^d\\to\\mathbb{R}_{\\ge 0}, $$并使得事件 $A$ 的概率为\n$$ \\mathbb{P}(X\\in A)=\\int_A p_X(x)\\,dx. \\quad (80) $$其中 $\\int p_X(x)\\,dx=1$。按约定,当对整个空间积分时,我们省略积分区间(即将 $\\int$ 视为 $\\int_{\\mathbb{R}^d}$)。为简洁起见,我们把随机变量 $X_t$ 的概率密度 $p_{X_t}$ 简记为 $p_t$。我们使用记号 $X\\sim p$(或 $X\\sim p_X$)表示 $X$ 服从密度 $p$。在生成式建模中,一个常见的 PDF 是 $d$ 维各向同性高斯分布:\n$$ \\mathcal{N}(x;\\mu,\\sigma^2 I)=(2\\pi\\sigma^2)^{-d/2}\\exp\\!\\left(-\\frac{\\|x-\\mu\\|_2^2}{2\\sigma^2}\\right), \\quad (81) $$其中 $\\mu\\in\\mathbb{R}^d$、$\\sigma\\in\\mathbb{R}_{\u003e0}$ 分别表示分布的均值与标准差。\n随机变量的期望是在最小二乘意义下最接近 $X$ 的常向量:\n$$ \\mathbb{E}[X] = \\mathop{\\mathrm{arg\\,min}}_{z\\in\\mathbb{R}^d} \\int \\lVert x - z \\rVert^2 p_X(x)\\,\\mathrm{d}x = \\int x\\, p_X(x)\\,\\mathrm{d}x. \\quad (82) $$计算随机变量函数期望的一个有用工具是无意识统计学家法则(law of the unconscious statistician,常简称为 LOTUS):\n$$ \\mathbb{E}[f(X)]=\\int f(x)\\,p_X(x)\\,dx. \\quad (83) $$必要时,我们将用 $\\mathbb{E}_X f(X)$ 来显式标明期望所对应的随机变量。\nA.2 条件密度与期望(Conditional Densities and Expectations) # 图 16:联合概率密度函数 $p_{X,Y}$(阴影区域)及其边缘分布(亦称边际分布)$p_X$ 和 $p_Y$(黑色曲线)。\n给定两个随机变量 $X,Y\\in\\mathbb{R}^d$,其联合概率密度函数(PDF)$p_{X,Y}(x,y)$ 的边缘密度满足\n$$ \\int p_{X,Y}(x,y)\\,dy \\;=\\; p_X(x),\\qquad \\int p_{X,Y}(x,y)\\,dx \\;=\\; p_Y(y). \\quad (84) $$参见图 16($d=1$)以获得两个随机变量联合 PDF 的示意图。条件 PDF $p_{X\\mid Y}$ 描述在事件 $Y=y$(且 $p_Y(y)\u003e0$)条件下随机变量 $X$ 的 PDF:\n$$ p_{X\\mid Y}(x\\mid y)\\;:=\\;\\frac{p_{X,Y}(x,y)}{p_Y(y)},\\qquad \\Longrightarrow\\qquad p_{X,Y}(x,y)\\;=\\;p_{X\\mid Y}(x\\mid y)\\,p_Y(y). \\quad (85) $$同理可得 $p_{Y\\mid X}$。贝叶斯法则把 $p_{Y\\mid X}$ 用 $p_{X\\mid Y}$ 表示为\n$$ p_{Y\\mid X}(y\\mid x)\\;=\\;\\frac{p_{X\\mid Y}(x\\mid y)\\,p_Y(y)}{p_X(x)},\\qquad p_X(x)\u003e0. \\quad (86) $$条件期望 $\\mathbb{E}[X\\mid Y]$ 是在最小二乘意义下逼近 $X$ 的最佳函数 $g_\\star(Y)$:\n$$ \\begin{aligned} g_\\star \u0026:= \\mathop{\\mathrm{arg\\,min}}_{g:\\mathbb{R}^d\\to\\mathbb{R}^d} \\mathbb{E}\\!\\left[\\|X-g(Y)\\|^2\\right] \\\\ \u0026= \\mathop{\\mathrm{arg\\,min}}_{g:\\mathbb{R}^d\\to\\mathbb{R}^d} \\iint \\|x-g(y)\\|^2\\,p_{X,Y}(x,y)\\,\\mathrm{d}x\\,\\mathrm{d}y\\\\ \u0026= \\mathop{\\mathrm{arg\\,min}}_{g:\\mathbb{R}^d\\to\\mathbb{R}^d} \\int\\!\\Big[\\int \\|x-g(y)\\|^2\\,p_{X\\mid Y}(x\\mid y)\\,\\mathrm{d}x\\Big]\\,p_Y(y)\\,\\mathrm{d}y . \\end{aligned} \\quad (87) $$当 $y\\in\\mathbb{R}^d$ 且 $p_Y(y)\u003e0$ 时,条件期望函数因此为\n$$ \\mathbb{E}[X\\mid Y=y]\\;:=\\;g_\\star(y)\\;=\\;\\int x\\,p_{X\\mid Y}(x\\mid y)\\,dx, \\quad (88) $$其中第二个等号来自于对式 (87) 内层括号(取 $Y=y$)的极小化,做法与式 (82) 相同。把 $g_\\star$ 与随机变量 $Y$ 复合得到\n$$ \\mathbb{E}[X\\mid Y]\\;:=\\;g_\\star(Y), \\quad (89) $$它是一个取值于 $\\mathbb{R}^d$ 的随机变量。容易混淆的是,$\\mathbb{E}[X\\mid Y=y]$ 与 $\\mathbb{E}[X\\mid Y]$ 均被称为“条件期望”,但二者并非同一对象:前者是从 $\\mathbb{R}^d\\!\\to\\!\\mathbb{R}^d$ 的函数,后者则是一个 $\\mathbb{R}^d$ 值的随机变量。以下均使用上述记法以避免歧义。\n塔式性质(tower property) 有助于化简涉及条件期望的推导:\n$$ \\mathbb{E}\\!\\left[\\mathbb{E}[X\\mid Y]\\right]\\;=\\;\\mathbb{E}[X]. \\quad (90) $$由于 $\\mathbb{E}[X\\mid Y]$ 本身是随机变量(即 $Y$ 的函数),外层期望就是对其再取期望。可由上述定义直接验证:\n$$ \\begin{aligned} \\mathbb{E}\\!\\left[\\mathbb{E}[X\\mid Y]\\right] \u0026= \\int\\!\\Big(\\int x\\,p_{X\\mid Y}(x\\mid y)\\,dx\\Big)\\,p_Y(y)\\,dy \\\\ \u0026\\overset{(85)}{=} \\iint x\\,p_{X,Y}(x,y)\\,dx\\,dy \\\\ \u0026\\overset{(84)}{=} \\int x\\,p_X(x)\\,dx \\;=\\;\\mathbb{E}[X]. \\end{aligned} $$最后,考虑两个任意随机变量 $X$ 与 $Y$ 以及它们的某个函数 $f(X,Y)$。利用无意识统计学家法则并结合式 (88),有\n$$ \\mathbb{E}\\!\\left[f(X,Y)\\mid Y=y\\right]\\;=\\;\\int f(x,y)\\,p_{X\\mid Y}(x\\mid y)\\,dx. \\quad (91) $$ 1 介绍(Introduction) # 1.1 概览(Overview) # 近几年,我们都见证了人工智能(AI)领域的一场巨大变革:图像生成器(如 Stable Diffusion 3)能够生成写实或艺术风格的图像,视频模型(如 Meta 的 Movie Gen Video)能够生成高度逼真的短片,大语言模型(如 ChatGPT)能够生成近似“人类水平”的文本回应。这场革命的核心,是 AI 系统获得了一种新的能力:生成对象。相较于过去主要用于“预测”的 AI,新一代生成式 AI 更“有创造力”:它们能够基于用户给定的输入“做梦”,生成新的对象。\n本课程的目标是介绍两类最广泛使用的生成式 AI 算法:去噪扩散模型(denoising diffusion models)与 流匹配(flow matching)。它们是当前最强图像、音频、视频生成模型的基础组件(例如 Stable Diffusion 3、Movie Gen Video),并且也已成为某些科学应用中的 SOTA 方法(例如蛋白质结构建模中的 AlphaFold3 使用扩散模型)。因此,系统理解这两类模型是一项非常有价值的能力。\n这两类生成模型有一个共同特征:它们通过迭代地将噪声转化为数据来生成对象;这一“从噪声到数据”的演化过程,通常由常微分方程或随机微分方程(ODE/SDE)的数值模拟来实现。流匹配与去噪扩散模型可以看作一族方法:它们允许我们在深度神经网络的帮助下,大规模地构造、训练并模拟相应的 ODE/SDE。虽然实现上并不复杂,但由于 SDE 的技术性细节,理解这些模型往往并不容易。本课程将提供一套自洽的数学工具箱(尤其是微分方程相关内容),帮助你系统地理解这些模型背后的原理。\n备注 1(补充资源,Additional Resources)\n虽然本文笔记力求自洽,但仍推荐你配合以下两类资源一起学习:\n课程录像(Lecture recordings):以讲授形式串联每一节内容; 实验课(Labs):指导你从零实现自己的扩散模型,强烈建议“动手写代码”。 以上资源可在课程主页获取:https://diffusion.csail.mit.edu/。\n1.2 课程结构(Course Structure) # 这里对本文档(课程笔记)的结构做一个简要导读:\n第 1 节:采样视角下的生成式建模(Generative Modeling as Sampling):形式化“生成”的含义。我们把诸如“如何生成一张狗的图片?”这类问题,转化为更精确的数学问题——从某个概率分布中采样。 第 2 节:流与扩散模型(Flow and Diffusion Models):解释“如何生成”的机制。正如课程名所暗示,这套机制本质上是对常微分方程与随机微分方程(ODE/SDE)的数值模拟;我们会介绍微分方程的基本概念,并说明如何用神经网络来构造这些方程。 第 3 节:构建训练目标(Constructing a Training Target):训练生成模型前,必须先明确模型要逼近的“真值”到底是什么(ground truth)。我们将引入著名的福克–普朗克方程(Fokker–Planck equation),并推导训练目标的形式。 第 4 节:训练生成模型(Training the Generative Model):在给定训练目标后,说明如何训练神经网络以学习相应的 ODE/SDE。 第 5 节:构建图像生成器(Building an Image Generator):把前述理论落到具体的图像生成系统,包括引导(guidance)与常用网络架构等。 背景要求(Required background)\n由于主题较为技术性,我们建议读者具备一定的数学基础,尤其是对概率论的基本概念有所了解。因此,原文在附录 A 中加入了概率论回顾(A Reminder on Probability Theory)。如果其中有些概念你暂时不熟悉也没关系:后续内容会反复用到它们,你可以按需回看。\n1.3 采样视角下的生成式建模(Generative Modeling As Sampling) # 先从我们可能遇到的各种数据类型(或者称为模态,Modalities)以及如何用数字来表示它们开始:\n图像(Image):考虑一幅具有 $H \\times W$ 个像素的图像,其中 $H$ 为高、$W$ 为宽,每个像素包含三个颜色通道(RGB)。对每个像素与每个颜色通道,我们都有一个实数强度值。因此,一幅图像可表示为 $z \\in \\mathbb{R}^{H \\times W \\times 3}.$\n视频(Video):视频就是随时间排列的一系列图像。若有 $T$ 个时间点或帧,则视频可表示为 $z \\in \\mathbb{R}^{T \\times H \\times W \\times 3}.$\n分子结构(Molecular Structure):一种朴素表示方式是用矩阵 $z=(z^1,\\ldots,z^N)\\in\\mathbb{R}^{3\\times N},$ 其中 $N$ 是分子中的原子数,每个 $z^i\\in\\mathbb{R}^3$ 描述该原子的空间位置。当然,还有更复杂的表示方式。\n在以上所有例子中,我们要生成的对象在数学上都可以(在必要时展平后)表示为一个向量。因此,本文将采用如下约定:\n核心要点 1(对象即向量,Objects as Vectors)\n我们把要生成的对象视为向量 $z\\in\\mathbb{R}^d$。\n一个值得注意的例外是文本数据。它通常被建模为离散对象,并通过自回归语言模型(Autoregressive Language Models,ARLMs,例如 ChatGPT)来处理。虽然已有针对离散数据的流模型(flow models)与扩散模型(Diffusion Models,DMs)等方法被发展出来,但本文仅聚焦于连续数据的应用。\n将“生成”表述为“采样”(Generation as Sampling) # 让我们定义“生成”的含义。比如我们想生成“一张狗的图片”。显然,满足要求的图片有很多,并不存在唯一“最好”的那张。更贴切的说法是:存在一个对所有可能狗图像的概率分布。我们称它为数据分布(Data Distribution,$p_{\\text{data}}$)。在狗图像的例子中,该分布会对“更像狗”的图像赋予更高的概率。因此,“好不好看”的主观描述可以被“在 $p_{\\text{data}}$ 下的可能性有多大”所替代。于是,生成可以被数学化为从(未知的)分布 $p_{\\text{data}}$ 采样。\n核心要点 2(将生成视为采样,Generation as Sampling)\n生成一个对象 $z$ 被建模为从数据分布中采样:$z \\sim p_{\\text{data}}$。\n生成模型(Generative Model,GM)是一类机器学习模型,它允许我们从 $p_{\\text{data}}$ 生成样本。在机器学习中,训练模型需要数据;在生成建模中,我们通常假设能获得一个有限的样本集合,这些样本独立同分布(Independent and Identically Distributed,i.i.d.)地来自 $p_{\\text{data}}$,并共同作为真实分布的代理。\n核心要点 3(数据集,Dataset)\n一个数据集由有限个样本构成:$z_1,\\ldots,z_N \\stackrel{\\text{i.i.d.}}{\\sim} p_{\\text{data}}$。\n对于图像,我们可以通过整理互联网上公开可用的图片来构建数据集;对于视频,可以把 YouTube 作为数据库来使用;对于蛋白质结构,我们可以使用诸如蛋白质数据库(Protein Data Bank)等汇集了数十年科学测量的实验数据库。随着数据集规模的增大,它对底层分布 $p_{\\text{data}}$ 的刻画会越来越充分。\n条件生成(Conditional Generation) # 很多时候,我们希望在给定某些信息 $y$ 的条件下生成对象。例如,我们可能希望在条件 $y=$ “一条狗在被雪覆盖的山坡上奔跑,背景是群山”的描述下生成图像。我们可以把这改写为从一个条件分布采样:\n核心要点 4(条件生成,Conditional Generation)\n条件生成是从 $z \\sim p_{\\text{data}}(\\cdot \\mid y)$ 采样,其中 $y$ 是条件变量(conditioning variable)。\n我们把 $p_{\\text{data}}(\\cdot \\mid y)$ 称为条件数据分布(Conditional Data Distribution)。条件生成建模通常要求模型能够对任意而非固定的 $y$ 进行条件化。延续上面的例子,我们也可能希望在 $y=$ “一张写实风格的猫在吹生日蜡烛的图片”这样的文本提示下进行条件化。因此,我们希望有一个单一的模型,可以在任何给定的 $y$ 上进行条件化。事实证明,无条件生成的技术很容易推广到条件情形。于是,在前 3 个小节中,我们几乎只讨论无条件情形(同时牢记我们的目标是条件生成)。\n从噪声到数据(From Noise to Data) # 到目前为止,我们讨论了生成建模的“是什么”:从 $p_{\\text{data}}$ 生成样本。下面简要讨论“如何做”。我们假设可以从某个易于采样的初始分布(Initial Distribution,$p_{\\text{init}}$)中取样,例如高斯分布$p_{\\text{init}}=\\mathcal{N}(0,I_d).$。生成建模的目标是把来自 $x \\sim p_{\\text{init}}$ 的样本变换成来自 $p_{\\text{data}}$ 的样本。需要注意的是,$p_{\\text{init}}$ 不一定必须是简单的高斯;我们将看到,利用更灵活的 $p_{\\text{init}}$ 也有一些有趣的用例。尽管如此,在绝大多数应用中,我们仍将其取作简单的高斯分布,这一点需要牢记。\n总结 # 下面对本节内容做一小结。\n总结 2(将生成视为采样,Summary 2)\n我们对本节结论进行如下概括:\n本文把要生成的对象视为向量 $z\\in\\mathbb{R}^d$,例如图像、视频或分子结构。 生成任务是在训练期间能够获取一个样本数据集 $z_1,\\ldots,z_N \\stackrel{\\text{i.i.d.}}{\\sim} p_{\\text{data}}$ 的前提下,从概率分布 $p_{\\text{data}}$ 生成样本。 条件生成假设我们在标签 $y$ 的条件下建模,并希望从 $p_{\\text{data}}(\\cdot\\mid y)$ 采样;训练时可获得成对数据 $(z_1,y_1),\\ldots,(z_N,y_N)$。 我们的目标是训练一个生成模型,将来自简单分布 $p_{\\text{init}}$(例如高斯)的样本变换为来自 $p_{\\text{data}}$ 的样本。 2 流与扩散模型(Flow and Diffusion Models) # 在上一节中,我们将生成式建模形式化为:从数据分布 $p_{\\text{data}}$ 中采样。进一步地,我们看到采样可以通过把来自一个简单分布 $p_{\\text{init}}$(例如高斯 $\\mathcal{N}(0, I_d)$)的样本,变换为来自目标分布 $p_{\\text{data}}$ 的样本来实现。本节描述:如何把所需的变换表示为某个恰当构造的微分方程的模拟。例如,流匹配与扩散模型分别涉及模拟常微分方程(Ordinary Differential Equations,ODEs)与随机微分方程(Stochastic Differential Equations,SDEs)。\n本节的目标是定义并构造这些生成模型,并在后续部分中一直使用它们。具体做法是:先定义 ODE 与 SDE,并讨论它们的数值模拟;其次说明如何用深度神经网络参数化一个 ODE/SDE;这将引出流模型(flow model)与扩散模型(diffusion model)以及用于从这类模型中采样的基本算法;在后续章节中,我们再探讨如何训练这些模型。\n2.1 流模型(Flow Models) # 图 1:一个流 $\\psi_t : \\mathbb{R}^d \\to \\mathbb{R}^d$(红色方格网)由一个速度场 $u_t : \\mathbb{R}^d \\to \\mathbb{R}^d$(用蓝色箭头表示)所定义,该速度场给出了在所有位置处的瞬时运动(这里 $d = 2$)。我们展示了三个不同的时间 $t$。可以看出,一个流是一个“扭曲”空间的微分同胚。图引自 [^15]。\n我们从定义常微分方程(ODEs)开始。ODE 的解由一条轨迹(trajectory)给出,即一个函数\n$$ X:[0,1]\\to\\mathbb{R}^d,\\quad t\\mapsto X_t, $$它把时间 $t$ 映射到空间 $\\mathbb{R}^d$ 中的某个位置。每个 ODE 都由一个向量场(vector field) $u$ 定义,即\n$$ u:\\mathbb{R}^d\\times[0,1]\\to\\mathbb{R}^d,\\quad (x,t)\\mapsto u_t(x), $$也就是说:对每个时间 $t$ 与位置 $x$,我们得到一个向量 $u_t(x)\\in\\mathbb{R}^d$,表示空间中的速度(见图 1)。ODE 对轨迹施加条件:我们希望一条轨迹 $X$ “沿着”向量场 $u_t$ 的方向,从点 $x_0$ 出发。形式化地,这样的轨迹是下面方程的解:\n$$ \\frac{d}{dt}X_t = u_t(X_t) \\quad (1a)\\quad\\text{(ODE)} $$$$ X_0 = x_0 \\quad (1b)\\quad\\text{(初始条件)} $$式(1a)要求:$X_t$ 的导数由 $u_t$ 指定的方向给出。式(1b)规定:在 $t=0$ 时从 $x_0$ 出发。\n现在我们可以问:如果从 $t=0$ 起始于 $x_0$,那么在任一时刻 $t$(也即 $X_t$)会在哪里?这个问题由称为流(flow)的函数回答,它是 ODE 的一个解:\n$$ \\psi:\\mathbb{R}^d\\times[0,1]\\to\\mathbb{R}^d,\\quad (x_0,t)\\mapsto \\psi_t(x_0) \\quad (2a) $$$$ \\frac{d}{dt}\\,\\psi_t(x_0)=u_t\\!\\big(\\psi_t(x_0)\\big) \\quad (2b)\\quad\\text{(由向量场诱导的流的 ODE)} $$$$ \\psi_0(x_0)=x_0 \\quad (2c)\\quad\\text{(流的初始条件)} $$给定初始条件 $X_0=x_0$,ODE 的轨迹由 $X_t=\\psi_t(X_0)$ 恢复。因此,向量场定义 ODE,而 ODE 的解即为流。\n如同处理每个方程一样,我们应当问:ODE 的解是否存在?若存在是否唯一?在对 $u_t$ 施加较弱假设的情况下,数学中的一个基本结果给出了肯定答案:\n定理 3(流的存在性与唯一性,Flow existence and uniqueness)\n若 $u:\\mathbb{R}^d\\times[0,1]\\to\\mathbb{R}^d$ 连续可微且其导数有界,则(2)中的 ODE 存在唯一由流 $\\psi_t$ 给出的解。在这种情况下,对所有 $t$,$\\psi_t$ 是一个微分同胚(diffeomorphism),即 $\\psi_t$ 连续可微且其逆映射 $\\psi_t^{-1}$ 也连续可微。\n在机器学习中,用神经网络来参数化 $u_t(x)$ 时,上述假设几乎总能满足(神经网络的导数有界)。因此,对我们感兴趣的情形来说,流存在且是 ODE 的唯一解。证明可见文献 1 2。\n例 4(线性向量场,Linear Vector Fields)\n考虑一个简单的向量场 $u_t(x)$,其在 $x$ 上为线性:$u_t(x)=-\\theta x$,其中 $\\theta\u003e0$。则 $$\\psi_t(x_0)=\\exp(-\\theta t)\\,x_0 \\quad (3)$$定义了一个解(流)并满足(2)。可直接验证:$\\psi_0(x_0)=x_0$,且\n$$ \\frac{d}{dt}\\psi_t(x_0)=\\frac{d}{dt}\\big(\\exp(-\\theta t)\\,x_0\\big)=-\\theta \\exp(-\\theta t)\\,x_0 =-\\theta \\psi_t(x_0)=u_t\\!\\big(\\psi_t(x_0)\\big), $$其中用到了链式法则。在图 3 中,我们可视化了该形式的流以指数方式收敛到 $0$。\n模拟 ODE(Simulating an ODE) # 一般来说,如果 $u_t$ 并非像上述那样简单的线性函数,我们无法显式计算流 $\\psi_t$。此时需使用数值方法来模拟 ODE。幸运的是,这是数值分析中的经典且研究充分的主题,已有大量强有力的方法 3。最简单、最直观的方法之一是欧拉方法(Euler method)。在欧拉方法中,从 $X_0=x_0$ 初始化,并按如下方式更新:\n$$ X_{t+h}=X_t+h\\,u_t(X_t)\\qquad (t=0,h,2h,3h,\\ldots,1-h) \\quad (4) $$其中 $h=n^{-1}\u003e0$ 是一个步长超参数(step size hyperparameter),$n\\in\\mathbb{N}$。在本课程中,欧拉方法已经足够。作为更复杂方法的示例,考虑海恩方法(Heun\u0026rsquo;s method),其更新规则为\n$$ X_{t+h}'=X_t+h\\,u_t(X_t)\\quad\\text{(新状态的初始猜测)} $$$$ X_{t+h}=X_t+\\frac{h}{2}\\Big(u_t(X_t)+u_{t+h}\\big(X_{t+h}'\\big)\\Big)\\quad\\text{(用当前与猜测状态的平均方向修正)} $$直观地说,海恩方法先得到下一步的第一次猜测 $X_{t+h}'$,再利用修正后的平均方向给出更新。\n流模型(Flow models) # 现在我们可以通过 ODE 构造一个生成模型。回忆我们的目标:把一个简单分布 $p_{\\text{init}}$ 变换为一个复杂分布 $p_{\\text{data}}$。因此,模拟 ODE 是实现该变换的自然选择。一个流模型由下面的 ODE 描述:\n$$ X_0\\sim p_{\\text{init}}\\quad\\text{(随机初始化)} $$$$ \\frac{d}{dt}X_t=u_t^{\\theta}(X_t)\\quad\\text{(ODE)} $$其中向量场 $u_t^{\\theta}$ 是带参数 $\\theta$ 的神经网络(neural network)。目前,我们把 $u_t^{\\theta}$ 视为一般的神经网络,即一个连续函数\n$$ u_t^{\\theta}:\\mathbb{R}^d\\times[0,1]\\to\\mathbb{R}^d $$(参数为 $\\theta$)。稍后我们将讨论具体的网络结构选择。我们的目标是使轨迹的终点 $X_1$ 服从分布 $p_{\\text{data}}$,即\n$$ X_1\\sim p_{\\text{data}} \\quad\\Longleftrightarrow\\quad \\psi_1^{\\theta}(X_0)\\sim p_{\\text{data}}, $$其中 $\\psi_t^{\\theta}$ 表示由 $u_t^{\\theta}$ 诱导的流。需要注意:虽然称为流模型,但神经网络参数化的是向量场而非流本身。要计算该流,我们必须数值模拟该 ODE。算法 1 总结了用欧拉方法从流模型采样的过程。\n算法 1:用欧拉方法从流模型采样(Sampling from a Flow Model with Euler method)输入:神经网络向量场 $u_t^{\\theta}$,步数 $n$\n设 $t=0$ 设步长 $h=\\frac{1}{n}$ 采样 $X_0\\sim p_{\\text{init}}$ 循环 $i=0,\\ldots,n-1$(对应 $t=0,h,2h,\\ldots,1-h$ 共 $n$ 次更新): $X_{t+h}=X_t+h\\,u_t^{\\theta}(X_t)$ 更新 $t\\leftarrow t+h$ 返回 $X_t$(此时 $t=1$,也即 $X_t=X_1$) 2.2 扩散模型(Diffusion Models) # 随机微分方程(Stochastic Differential Equations,SDEs)把常微分方程(Ordinary Differential Equations,ODEs)的确定性轨迹扩展为随机轨迹。随机轨迹通常称为随机过程 $(X_t)_{0\\le t\\le 1}$,可表述为\n对任意 $0\\le t\\le 1$,$X_t$ 是一个随机变量; $X:[0,1]\\to\\mathbb{R}^d,\\quad t\\mapsto X_t$ 是一次对 $X$ 的抽样得到的随机轨迹。 特别地,即使我们两次模拟同一个随机过程,结果也可能不同,因为其动力学被设计为随机的。\n布朗运动(Brownian Motion,亦称 Wiener 过程) # 图 2:一维布朗运动 $(W_t)$ 的若干样本轨迹,使用式 (5) 进行模拟得到。\nSDE 是通过布朗运动构造的——它是研究物理扩散过程时得到的一类基础随机过程。可以把布朗运动看作连续时间的随机游走。定义如下:布朗运动 $W=(W_t)_{0\\le t\\le 1}$ 满足 $W_0=0$,轨迹 $t\\mapsto W_t$ 连续,并满足:\n正态增量(Normal increments):对所有 $0\\le s","date":"2025年11月6日","externalUrl":null,"permalink":"/posts/mit-6.s184-%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/","section":"文章","summary":"","title":"MIT 6.S184 课程笔记","type":"posts"},{"content":"","date":"2025年11月6日","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"2025年10月20日","externalUrl":null,"permalink":"/tags/archlinux/","section":"Tags","summary":"","title":"Archlinux","type":"tags"},{"content":"","date":"2025年10月20日","externalUrl":null,"permalink":"/tags/gnome/","section":"Tags","summary":"","title":"Gnome","type":"tags"},{"content":" 0.写在开头 # 这是一个很小众的问题,应该很少有人和我一样,将MacOS的图标用在archlinux+gnome上。而问题出现的原因是:gdk-pixbuf 2.42.11(2024-04-19)起,ICN/ICNS等多种加载器默认就不再构建,很多发行版(含 Arch)沿用了这个默认设置,所以Nautilus 49.1不会为.icns生成缩略图。\n修复方法 # 安装必需工具 BASH不换行 paru -S --needed libicns imagemagick 写入 thumbnailer 描述文件(用户级) BASH不换行 mkdir -p ~/.local/share/thumbnailers cat \u0026gt; ~/.local/share/thumbnailers/icns.thumbnailer \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; [Thumbnailer Entry] TryExec=icns2png # 用 ImageMagick 7 的入口 `magick`(Arch 默认) Exec=bash -lc \u0026#39;tmp=$(mktemp -d); icns2png -x -o \u0026#34;$tmp\u0026#34; %i \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; f=$(ls -1 \u0026#34;$tmp\u0026#34;/* 2\u0026gt;/dev/null | sort -V | tail -n1); test -n \u0026#34;$f\u0026#34; \u0026amp;\u0026amp; magick \u0026#34;$f\u0026#34; -thumbnail %s \u0026#34;%o\u0026#34;; rm -rf \u0026#34;$tmp\u0026#34;\u0026#39; MimeType=image/x-icns; EOF 重启文件管理器并清理旧缩略图缓存 BASH不换行 nautilus -q || true rm -rf ~/.cache/thumbnails/* 修复结果 # 现在已经可以和之前一样正常显示了。 ","date":"2025年10月20日","externalUrl":null,"permalink":"/posts/gnome49-.icns%E6%96%87%E4%BB%B6%E9%A2%84%E8%A7%88%E4%BF%AE%E5%A4%8D/","section":"文章","summary":"","title":"Gnome49 .Icns文件预览修复","type":"posts"},{"content":"","date":"2025年10月20日","externalUrl":null,"permalink":"/tags/icns/","section":"Tags","summary":"","title":"Icns","type":"tags"},{"content":"","date":"2025年10月20日","externalUrl":null,"permalink":"/tags/nautilus/","section":"Tags","summary":"","title":"Nautilus","type":"tags"},{"content":"","date":"2025年10月20日","externalUrl":null,"permalink":"/tags/act/","section":"Tags","summary":"","title":"ACT","type":"tags"},{"content":"","date":"2025年10月20日","externalUrl":null,"permalink":"/tags/il/","section":"Tags","summary":"","title":"IL","type":"tags"},{"content":" 0. 写在开头 # 作为 SO-101 基础实践的理论部分,本文将深入浅出地介绍 ACT(Action Chunking with Transformers)模型。\n实践系统介绍 # 主臂(Leader Arm):\n由人控制; 记录关节姿态,称为“动作(Action)”,训练时模型需要预测该动作。 从臂(Follower Arm):\n装有摄像头; 由程序控制: 数据采集时:使用 PID 跟随已记录的 Action; 测试时:使用 PID 跟随模型预测的 Action; 记录关节姿态与相机图像,统称为“观测(Observation)”,作为模型输入。 数据收集流程 # 操作员通过主臂执行演示,系统以固定频率同步记录三类信息:\n场景视觉——机载/外置摄像头的当前图像; 从臂状态——七自由度从臂的当前关节位置向量(含末端夹爪开合自由度); 主臂控制——同为七维的主臂关节目标(作为动作标签/ground truth)。 采集中,主从之间存在通信时延与 PID 跟踪误差,因此从臂在时刻 $t$ 的实际位置更接近于主臂在 $t-1$ 的目标。为提升预测稳定性,观测(Observation)选取“当前从臂关节 + 当前图像”,而非“上一帧主臂动作”,因为前者能直接反映已执行结果、首帧不缺失,且允许模型自适应学习主从系统性细微差异。\n训练阶段以(图像、从臂关节)为输入、以主臂关节目标为动作标签;测试/推理阶段在相同观测输入上预测虚拟主臂动作,并下发该动作,由从臂通过 PID 跟随完成实际执行。\nACT 模型 # ACT 模型核心创新点 # Action Chunking Policy(动作分块策略) # ACT 的核心思想:缩短有效决策时域(reduce the effective horizon of a long trajectory)。 Action Chunking Policy:在时刻 $t$ 观测一次后,一次性输出接下来 $K$ 个时间步的动作序列;系统每经过 $K$ 步才进行一次新的决策。 痛点:累计误差(Compounding Error)。在长时程、多步控制任务中,早期或连续的小偏差会相互叠加,最终放大为不可挽回的偏差,导致任务失败。 传统做法: 单步策略(Single-Step Policy)在每个时间步都重新观测并决策。一旦连续多次出现轻微预测偏差,误差会以高频方式累积。 为什么 Action Chunking Policy 能缓解累计误差? 降低决策频率:由“每步决策”降为“每 $K$ 步决策”,有效决策次数显著减少,误差触发与积累机会随之降低。 抑制连续小错:单次偏差通常可在下一次(块级)决策时被纠正;真正致命的是高频、连续的小错。减少决策频率可直接降低“连续错误链”出现概率。 提高轨迹稳定性:成块输出的动作在时间上更一致,抑制逐步预测抖动,降低轨迹漂移风险。 痛点:非马尔可夫环境(Non-Markovian)。在马尔可夫环境中,最优策略可仅依赖当前状态;但现实任务常含隐含时间依赖。以炒菜为例:倒油 → 等待加热一段时间(在达到目标油温前后外观几乎无明显变化)→ 投入食材。 传统做法: 单步策略(Single-Step Policy):由于关键信息无法从单帧观测直接辨识,难以判断“何时执行下一步”。 历史条件化策略(History-Conditioned Policy):通过引入过往观测与动作来弥补非马尔可夫信息,但其条件域从“当前观测”扩展到“较长历史”,容易产生因果误识(causal misidentification)。 因果误识(Causal Misidentification):指将与行动高度共现、却非因果的线索当作真正原因,从而学习到错误的决策规则。比如:刹车时刹车灯会亮,但“灯亮”并非“刹车原因”,若据此做决策即为因果误识。 为什么 Action Chunking Policy 能提高非马尔可夫环境中的表现? 将“倒油 → 等待加热 → 投入食材”封装为一个 action chunk,使模型能够显式/隐式地建模其中的等待时长与触发条件。 用面向未来的动作序列直接对时间依赖进行结构化约束,更容易学到真正的时序因果(如“等待时长”)而非表面共现。 Temporal Ensemble(时间集成) # Temporal Ensemble\n为避免 Action Chunking “每 $K$ 步一次决策”带来的卡顿执行,使用 Temporal Ensemble 在每个时间步都预测一个长度为 $K$ 的动作块。 每次执行到第 $t$ 步时,将所有历史时刻对“第 $t$ 步”的预测进行加权融合,得到最终动作,相当于一种滑动加权平均的平滑器。 权重通常对更早产生的预测赋予更大权重(更稳定、噪声更小),从而显著提升轨迹的连续性与顺滑度。 代价是需要额外的前向推理次数(计算量上升),但无需改动训练目标或系统结构。 ACT 伪代码与算法流程 # 请先学习 VAE 和 CVAE 模型,再继续阅读。 记号约定:令 $a_{t:t+K}\\!\\triangleq\\!(a_t,\\dots,a_{t+K-1})$ 表示长度为 $K$ 的动作序列;$o_t$ 为时刻 $t$ 的观测,$\\bar{o}_t$ 为去除图像模态后的观测;$z$ 为潜变量。\n一、ACT 训练 # 给定:演示数据集 $\\mathcal{D}$、块大小(预测跨度)$K$、正则权重 $\\beta$。\n初始化编码器(后验)$q_\\phi\\!\\left(z\\,\\middle|\\,a_{t:t+K},\\bar{o}_t\\right)$。 初始化解码器(策略)$\\pi_\\theta\\!\\left(\\hat{a}_{t:t+K}\\,\\middle|\\,o_t,z\\right)$。 对迭代轮次 $n=1,2,\\ldots$ 重复: 从 $\\mathcal{D}$ 采样样本对 $(o_t, a_{t:t+K})$; 从 $q_\\phi\\!\\left(z\\,\\middle|\\,a_{t:t+K},\\bar{o}_t\\right)$ 采样 $z$; 用 $\\pi_\\theta\\!\\left(\\hat{a}_{t:t+K}\\,\\middle|\\,o_t,z\\right)$ 预测 $\\hat{a}_{t:t+K}$; 计算重构损失 $$ \\mathcal{L}_{\\text{reconst}}=\\mathrm{MSE}\\!\\left(\\hat{a}_{t:t+K},\\,a_{t:t+K}\\right) $$ 计算正则(KL)项 $$ \\mathcal{L}_{\\text{reg}}=D_{\\mathrm{KL}}\\!\\bigl(q_\\phi(z\\,|\\,a_{t:t+K},\\bar{o}_t)\\,\\|\\,\\mathcal{N}(0,I)\\bigr) $$ 用 Adam 优化器更新 $\\theta,\\phi$,总损失 $$ \\mathcal{L}=\\mathcal{L}_{\\text{reconst}}+\\beta\\,\\mathcal{L}_{\\text{reg}}\\,. $$ 说明:重构项对应最大似然思想——在给定观测 $o_t$ 与潜变量 $z$ 下,使真实动作序列 $a_{t:t+K}$ 的生成概率最大。工程上常用距离度量近似该原则,常见为 $MSE$ 或 $L_1$;经验上 $L_1$ 往往更稳健,许多工作报告 $L_1$ 优于 $MSE$。\n二、ACT 推理 # 给定:训练好的策略 $\\pi_\\theta$,轨迹长度(episode 长度)$T$,指数衰减系数 $m$。\n初始化先入先出(FIFO)缓冲区 $\\mathcal{B}[0{:}T]$,其中 $\\mathcal{B}[t]$ 存储对时刻 $t$ 的多次候选动作预测(来自不同起点的重叠块)。 对 $t=1,2,\\ldots,T$: 令 $z=\\mathbf{0}$(使用先验均值进行推理),用 $\\pi_\\theta\\!\\left(\\hat{a}_{t:t+K}\\,\\middle|\\,o_t,z\\right)$ 预测长度为 $K$ 的动作序列; 将 $\\hat{a}_{t:t+K}$ 的各元素分别追加进对应槽位的缓冲区:$\\mathcal{B}[t{:}t+K]$; 设 $\\mathcal{B}[t]=\\{A_t[i]\\}_{i=0}^{N_t-1}$ 为当前步 $t$ 聚合到的候选动作集合($N_t$ 为该槽累计的候选数); 用指数加权平均得到最终执行动作 $$ a^{\\text{exec}}_t=\\frac{\\sum_{i=0}^{N_t-1}w_i\\,A_t[i]}{\\sum_{i=0}^{N_t-1}w_i}\\,,\\quad w_i=\\exp(-m\\cdot i)\\,. $$ 注:$i$ 按加入缓冲区的先后次序编号,$i=0$ 表示最早预测,$m\u003e0$ 控制对旧候选的衰减强度。\n三、损失函数 # 重构损失:$\\displaystyle \\mathcal{L}_{\\text{reconst}}=\\mathrm{MSE}\\!\\left(\\hat{a}_{t:t+K},\\,a_{t:t+K}\\right)$ 正则损失:$\\displaystyle \\mathcal{L}_{\\text{reg}}=D_{\\mathrm{KL}}\\!\\bigl(q_\\phi(z\\,|\\,a_{t:t+K},\\bar{o}_t)\\,\\|\\,\\mathcal{N}(0,I)\\bigr)$ 总损失:$\\displaystyle \\mathcal{L}=\\mathcal{L}_{\\text{reconst}}+\\beta\\,\\mathcal{L}_{\\text{reg}}$。 ACT 模型架构图 # 请先学习 VAE 和 CVAE 模型,再继续阅读。 ACT 训练 # ACT Training\n总体思想与 CVAE 相似,但在 ACT 中,输入与输出均为动作序列(action sequence):将 CVAE 中的“图像”角色替换为“动作序列”。同时,我们在 encoder 与 decoder 中都使用 condition(条件),即 observation(观测),包含摄像头图像与从臂的关节信息。为加快训练,encoder 的条件中不使用图像,仅使用从臂关节信息。\n为什么条件要包含从臂的关节信息?\n让模型专注于增量(在已知关节状态基础上的变化),而非直接从图像回归绝对位姿。 让模型感知主臂与从臂之间细微而系统性的差异。 Step 1:数据与监督信号 # 采集数据集。每个 batch(批次)的 input(输入)为 observation(观测,包含摄像头图像与从臂关节信息),ground-truth label(真实值标签)为 action sequence(动作序列)。该动作序列的维度等于“每步 7 维主臂动作”与 action chunk 大小 $K$ 的组合($7\\times K$ 维)。\nStep 2:潜变量后验(encoder)与采样 # CLS token(分类标记):一个可学习的 512 维向量(可视作模型参数)。 embedded joints(关节信息嵌入):从臂 7 个关节值,经线性层映射为 512 维向量。 embedded action sequence(动作序列嵌入):将长度为 $K$ 的动作序列逐步经线性层映射为 512 维向量,并加上 Sinusoidal Positional Encoding(正弦位置编码),得到 $K$ 个 512 维 token。 因此,送入 Transformer Encoder 的共为 $K+2$ 个 512 维 token(含 1 个 CLS、1 个 joints、以及 $K$ 个 action 序列 token)。Encoder 通过 self-attention(自注意力)融合全局信息,我们取 CLS 对应的 512 维输出,经线性层得到 32 维高斯的均值与方差,随后用 reparameterization trick(重参数化技巧)采样潜变量 $z\\in\\mathbb{R}^{32}$。\nStep 3:条件解码与动作预测(decoder) # 图像分支:4 个摄像头、每帧分辨率 480×640、RGB 三通道,经 ResNet-18 得到特征张量(例如 15×20×$C$,通常 $C\\approx512$),flatten 为 $300\\times C$,再经线性层映射到 512 维,并加上 Sinusoidal Positional Encoding。四路拼接共得到 $1200$ 个 512 维向量(cam1 ~ cam4 合计)。 joints 分支:与 encoder 侧相同处理,经线性层得到 512 维向量。 latent 分支:将潜变量 $z$ 经线性层映射为 512 维向量。 最终,Transformer Encoder 的输入 token 数为 $1200+2=1202$ 个 512 维向量。其输出作为 key/value 供 Transformer Decoder 做 cross-attention(交叉注意力)。Transformer Decoder 接收 $K$ 个 query 并输出长度为 $K$ 的 predicted action sequence(预测动作序列)。\nACT 推理 # ACT Testing\n推理流程与训练阶段的 Step 3 类似,但此处直接将潜变量设为零向量 $\\mathbf{0}$:在先验假设 $z\\sim\\mathcal{N}(0,I)$ 下,$\\mathbf{0}$ 是对称中心和众数(实作中常见的推理近似),据此可直接生成 predicted action sequence(预测动作序列)作为输出。\nACT 消融实验 # Ablation\nAction Chunk(动作分块)的有效性 将“单步预测”扩展为“一次预测 $K$ 步”能显著提升成功率。随着 chunk size 从 1 增至接近整段(≈400 步),性能总体上升;当 chunk 过长时略有回落,可能由于一次性输出过长、与环境交互与反馈不足。 Temporal Ensemble(时间集成) 对相邻输出片段在重叠区加权平均,可带来稳定的小幅增益,使动作更连贯。 CVAE 目标($KL$ 对齐)的作用 对潜变量 $z$ 施加 $KL$ 正则,使其与标准正态对齐: 脚本演示数据:去掉 $KL$ 影响较小,因数据更规整、噪声少。 人类演示数据:去掉 $KL$ 后性能显著下降,说明 $KL$ 正则对建模人类噪声/多样性至关重要。 远程操作的控制频率与数据采集效率 提高示教/遥操作频率可显著缩短单次演示时长:例如从 5 Hz 提升至 50 Hz,采集同一任务由约 30–40 s 降至 ≈20 s。作者建议高控制频率对高效采集至关重要。 实践 # 标准 ACT 通常按单任务训练,跨任务泛化有限,且不内置语言接口;若需多任务或语言条件,可在更高层做扩展。\nACT 相对其他模仿学习方法,具有:\n减少复合误差:通过预测动作块降低误差累积; 提高成功率:在精细操作任务上表现优异; 端到端训练:无需手工设计特征; 多模态融合:有效融合视觉与状态信息。 LeRobot ACT 代码阅读 # LeRobot 仓库经常性变动,请务必参考官方教程和仓库代码;本文撰写时版本为 d57d1aa1(2025-10-31)。 SHELL不换行 lerobot/src/lerobot/policies/act/ ├── configuration_act.py ├── modeling_act.py ├── processor_act.py └── README.md -\u0026gt; ../../../../docs/source/policy_act_README.mdLeRobot 的 ACT 由 configuration_act.py、modeling_act.py、processor_act.py 三个文件实现。\nconfiguration_act.py:ACT 配置 # PYTHON不换行 #!/usr/bin/env python # Copyright 2024 Tony Z. Zhao and The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # 以上是版权与开源许可声明,表明本代码遵循 Apache 2.0 许可证 from dataclasses import ( dataclass, field, ) # dataclass 用于简化配置类的定义,field 可定义默认值/工厂 from lerobot.configs.policies import ( PreTrainedConfig, ) # 项目内基类:预训练策略的通用配置抽象 from lerobot.configs.types import ( NormalizationMode, ) # 枚举:输入/输出的归一化模式(如 MEAN_STD、MIN_MAX) from lerobot.optim.optimizers import AdamWConfig # 优化器配置对象(AdamW 的超参集合) # 使用注册器将该配置类注册为 \u0026#34;act\u0026#34; 类型,便于通过字符串查找/构造对应配置。 @PreTrainedConfig.register_subclass(\u0026#34;act\u0026#34;) @dataclass # dataclass 会自动生成 __init__/__repr__/__eq__ 等,从字段定义中推导构造参数 class ACTConfig(PreTrainedConfig): \u0026#34;\u0026#34;\u0026#34;Configuration class for the Action Chunking Transformers policy. Defaults are configured for training on bimanual Aloha tasks like \u0026#34;insertion\u0026#34; or \u0026#34;transfer\u0026#34;. The parameters you will most likely need to change are the ones which depend on the environment / sensors. Those are: `input_shapes` and \u0026#39;output_shapes`. Notes on the inputs and outputs: - Either: - At least one key starting with \u0026#34;observation.image is required as an input. AND/OR - The key \u0026#34;observation.environment_state\u0026#34; is required as input. - If there are multiple keys beginning with \u0026#34;observation.images.\u0026#34; they are treated as multiple camera views. Right now we only support all images having the same shape. - May optionally work without an \u0026#34;observation.state\u0026#34; key for the proprioceptive robot state. - \u0026#34;action\u0026#34; is required as an output key. Args: n_obs_steps: Number of environment steps worth of observations to pass to the policy (takes the current step and additional steps going back). chunk_size: The size of the action prediction \u0026#34;chunks\u0026#34; in units of environment steps. n_action_steps: The number of action steps to run in the environment for one invocation of the policy. This should be no greater than the chunk size. For example, if the chunk size size 100, you may set this to 50. This would mean that the model predicts 100 steps worth of actions, runs 50 in the environment, and throws the other 50 out. input_shapes: A dictionary defining the shapes of the input data for the policy. The key represents the input data name, and the value is a list indicating the dimensions of the corresponding data. For example, \u0026#34;observation.image\u0026#34; refers to an input from a camera with dimensions [3, 96, 96], indicating it has three color channels and 96x96 resolution. Importantly, `input_shapes` doesn\u0026#39;t include batch dimension or temporal dimension. output_shapes: A dictionary defining the shapes of the output data for the policy. The key represents the output data name, and the value is a list indicating the dimensions of the corresponding data. For example, \u0026#34;action\u0026#34; refers to an output shape of [14], indicating 14-dimensional actions. Importantly, `output_shapes` doesn\u0026#39;t include batch dimension or temporal dimension. input_normalization_modes: A dictionary with key representing the modality (e.g. \u0026#34;observation.state\u0026#34;), and the value specifies the normalization mode to apply. The two available modes are \u0026#34;mean_std\u0026#34; which subtracts the mean and divides by the standard deviation and \u0026#34;min_max\u0026#34; which rescale in a [-1, 1] range. output_normalization_modes: Similar dictionary as `normalize_input_modes`, but to unnormalize to the original scale. Note that this is also used for normalizing the training targets. vision_backbone: Name of the torchvision resnet backbone to use for encoding images. pretrained_backbone_weights: Pretrained weights from torchvision to initialize the backbone. `None` means no pretrained weights. replace_final_stride_with_dilation: Whether to replace the ResNet\u0026#39;s final 2x2 stride with a dilated convolution. pre_norm: Whether to use \u0026#34;pre-norm\u0026#34; in the transformer blocks. dim_model: The transformer blocks\u0026#39; main hidden dimension. n_heads: The number of heads to use in the transformer blocks\u0026#39; multi-head attention. dim_feedforward: The dimension to expand the transformer\u0026#39;s hidden dimension to in the feed-forward layers. feedforward_activation: The activation to use in the transformer block\u0026#39;s feed-forward layers. n_encoder_layers: The number of transformer layers to use for the transformer encoder. n_decoder_layers: The number of transformer layers to use for the transformer decoder. use_vae: Whether to use a variational objective during training. This introduces another transformer which is used as the VAE\u0026#39;s encoder (not to be confused with the transformer encoder - see documentation in the policy class). latent_dim: The VAE\u0026#39;s latent dimension. n_vae_encoder_layers: The number of transformer layers to use for the VAE\u0026#39;s encoder. temporal_ensemble_coeff: Coefficient for the exponential weighting scheme to apply for temporal ensembling. Defaults to None which means temporal ensembling is not used. `n_action_steps` must be 1 when using this feature, as inference needs to happen at every step to form an ensemble. For more information on how ensembling works, please see `ACTTemporalEnsembler`. dropout: Dropout to use in the transformer layers (see code for details). kl_weight: The weight to use for the KL-divergence component of the loss if the variational objective is enabled. Loss is then calculated as: `reconstruction_loss + kl_weight * kld_loss`. \u0026#34;\u0026#34;\u0026#34; # 以上三引号是类的文档字符串(docstring),用于解释该配置类的用途和各参数含义。 # 文档内提到的 `input_shapes` / `output_shapes` 等键,属于父类/策略使用的约定。 # Input / output structure. # 观测/动作的时序结构配置 n_obs_steps: int = 1 # 传入策略的观测步数(时间维度),目前实现只支持 1(当前步) chunk_size: int = 100 # 一次预测的“动作块”的长度(以环境步数计) n_action_steps: int = ( 100 # 每次调用策略实际执行到环境中的动作步数,不能超过 chunk_size ) # 归一化模式的默认映射:按模态选择 NormalizationMode normalization_mapping: dict[str, NormalizationMode] = field( default_factory=lambda: { \u0026#34;VISUAL\u0026#34;: NormalizationMode.MEAN_STD, # 图像模态:减均值/除方差 \u0026#34;STATE\u0026#34;: NormalizationMode.MEAN_STD, # 状态模态:减均值/除方差 \u0026#34;ACTION\u0026#34;: NormalizationMode.MEAN_STD, # 动作模态:减均值/除方差(训练目标也会用到) } ) # Architecture. # Vision backbone. vision_backbone: str = ( \u0026#34;resnet18\u0026#34; # 图像编码使用的 ResNet 主干名称(需为 torchvision 的 resnet 变体) ) pretrained_backbone_weights: str | None = ( \u0026#34;ResNet18_Weights.IMAGENET1K_V1\u0026#34; # 主干的预训练权重标识;None 表示不加载 ) replace_final_stride_with_dilation: int = False # 是否用空洞卷积替换 ResNet 最后一个 2x2 stride(类型标注为 int,但实际布尔使用) # Transformer layers. pre_norm: bool = False # Transformer 是否使用 pre-norm 结构(LayerNorm 在子层之前) dim_model: int = 512 # Transformer 主通道隐藏维度 d_model n_heads: int = 8 # 多头注意力的头数 dim_feedforward: int = 3200 # 前馈网络的扩展维度(通常为 d_model 的若干倍) feedforward_activation: str = \u0026#34;relu\u0026#34; # 前馈网络的激活函数类型 n_encoder_layers: int = 4 # Transformer 编码器层数 # Note: Although the original ACT implementation has 7 for `n_decoder_layers`, there is a bug in the code # that means only the first layer is used. Here we match the original implementation by setting this to 1. # See this issue https://github.com/tonyzhaozh/act/issues/25#issue-2258740521. n_decoder_layers: int = ( 1 # Transformer 解码器层数(按原实现的实际效果设置为 1,以对齐行为) ) # VAE. use_vae: bool = ( True # 训练时是否使用 VAE 目标(引入额外 Transformer 作为 VAE 编码器) ) latent_dim: int = 32 # VAE 潜变量维度 n_vae_encoder_layers: int = 4 # VAE 编码器的 Transformer 层数 # Inference. # Note: the value used in ACT when temporal ensembling is enabled is 0.01. temporal_ensemble_coeff: float | None = ( None # 时间集成(temporal ensembling)的指数加权系数;None 表示关闭 ) # Training and loss computation. dropout: float = 0.1 # Transformer 层内的 dropout 比例(防止过拟合) kl_weight: float = ( 10.0 # 使用 VAE 时,KL 散度项的损失权重(总损失 = 重构损失 + kl_weight * KL) ) # Training preset # 训练预设:优化器相关超参数 optimizer_lr: float = 1e-5 # 主体学习率 optimizer_weight_decay: float = 1e-4 # 权重衰减(L2 正则) optimizer_lr_backbone: float = ( 1e-5 # 视觉主干的学习率(可能与主体不同,用于微调/冻结策略) ) def __post_init__(self): # dataclass 的钩子:在 __init__ 之后自动调用。 # 这里首先调用父类的 __post_init__ 来完成通用初始化(如解析输入/输出特征等)。 super().__post_init__() \u0026#34;\u0026#34;\u0026#34;Input validation (not exhaustive).\u0026#34;\u0026#34;\u0026#34; # ——以下是对配置进行基本校验的逻辑(非穷尽)—— # 校验视觉主干名称:必须是 ResNet 家族,否则抛出 ValueError if not self.vision_backbone.startswith(\u0026#34;resnet\u0026#34;): raise ValueError( f\u0026#34;`vision_backbone` must be one of the ResNet variants. Got {self.vision_backbone}.\u0026#34; ) # 若启用时间集成(temporal_ensemble_coeff 非 None),则 n_action_steps 必须为 1 # 原因:时间集成需要每一步都查询策略以形成集成 if self.temporal_ensemble_coeff is not None and self.n_action_steps \u0026gt; 1: raise NotImplementedError( \u0026#34;`n_action_steps` must be 1 when using temporal ensembling. This is \u0026#34; \u0026#34;because the policy needs to be queried every step to compute the ensembled action.\u0026#34; ) # n_action_steps 不能超过 chunk_size(一次调用预测的最大可用步数上限) if self.n_action_steps \u0026gt; self.chunk_size: raise ValueError( f\u0026#34;The chunk size is the upper bound for the number of action steps per model invocation. Got \u0026#34; f\u0026#34;{self.n_action_steps} for `n_action_steps` and {self.chunk_size} for `chunk_size`.\u0026#34; ) # 目前实现不支持多观测步(时间窗口 \u0026gt; 1) if self.n_obs_steps != 1: raise ValueError( f\u0026#34;Multiple observation steps not handled yet. Got `nobs_steps={self.n_obs_steps}`\u0026#34; ) def get_optimizer_preset(self) -\u0026gt; AdamWConfig: # 返回一个 AdamW 优化器的配置预设,供上层训练器构造实际优化器实例 return AdamWConfig( lr=self.optimizer_lr, weight_decay=self.optimizer_weight_decay, ) def get_scheduler_preset(self) -\u0026gt; None: # 返回 None 表示不使用学习率调度器(或由外部训练脚本自行指定) return None def validate_features(self) -\u0026gt; None: # 检查特征输入是否满足最小要求: # 必须至少提供一种图像特征(来自摄像头)或环境状态特征。 # 这些属性(image_features、env_state_feature)通常在父类 __post_init__ 中解析并赋值。 if not self.image_features and not self.env_state_feature: raise ValueError( \u0026#34;You must provide at least one image or the environment state among the inputs.\u0026#34; ) @property def observation_delta_indices(self) -\u0026gt; None: # 观测的“增量索引”定义(若用于计算时间差分等)。这里返回 None,表示不定义/不使用。 return None @property def action_delta_indices(self) -\u0026gt; list: # 动作的“增量索引”定义:这里返回 [0, 1, ..., chunk_size-1] # 常见用途:指示哪些时间步上的动作需要被预测/计算,或用于构建目标序列的索引。 return list(range(self.chunk_size)) @property def reward_delta_indices(self) -\u0026gt; None: # 奖励的“增量索引”定义:此处不使用奖励差分(返回 None) return None processor_act.py:ACT 流水线 # PYTHON不换行 #!/usr/bin/env python # Copyright 2024 Tony Z. Zhao and The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # 以上是版权与开源许可声明,表明本代码遵循 Apache 2.0 许可证 # 小结: # - 本文件的核心函数 make_act_pre_post_processors 会根据 ACTConfig 和可选的数据集统计,构造两个流水线对象: # 1) 前处理流水线(PRE):重命名 -\u0026gt; 加 batch 维 -\u0026gt; 移到设备 -\u0026gt; 归一化 # 2) 后处理流水线(POST):反归一化 -\u0026gt; 移回 CPU # - 这样做的好处:将数据工程与模型推理解耦,保证输入输出的形状、设备与数值尺度都符合模型与下游使用方的预期 # - features 与 norm_map 的一致性非常重要:保证前处理与后处理的变换可逆且匹配 # limitations under the License. from typing import ( Any, ) # 从 typing 导入 Any,表示“任意类型”,常用于类型提示中表示通用容器 import torch # 导入 PyTorch,用于张量(Tensor)及设备(device)管理 from lerobot.policies.act.configuration_act import ACTConfig # 导入 ACT 策略的配置类 # 从 lerobot.processor 导入一系列“处理步骤(ProcessorStep)”与管道(Pipeline)相关类 from lerobot.processor import ( AddBatchDimensionProcessorStep, # 处理步骤:为输入添加 batch 维度(例如从 [C,H,W] 变为 [B,C,H,W]) DeviceProcessorStep, # 处理步骤:将数据移动到指定设备(如 \u0026#34;cuda:0\u0026#34; 或 \u0026#34;cpu\u0026#34;) NormalizerProcessorStep, # 处理步骤:对特征做归一化(根据统计量,如均值/方差) PolicyAction, # 策略输出动作的数据结构(类型别名/封装) PolicyProcessorPipeline, # 通用的策略处理“流水线”定义,包含一系列有序步骤 RenameObservationsProcessorStep, # 处理步骤:重命名观测字典中的键(key),以适配模型预期的输入名 UnnormalizerProcessorStep, # 处理步骤:把归一化后的输出反归一化回原始尺度 ) from lerobot.processor.converters import ( policy_action_to_transition, # 转换函数:将策略动作结构转为“transition”结构(过渡/样本格式) transition_to_policy_action, # 转换函数:与上相反,将 transition 转回策略动作结构 ) from lerobot.utils.constants import ( POLICY_POSTPROCESSOR_DEFAULT_NAME, # 常量:后处理流水线的默认命名 POLICY_PREPROCESSOR_DEFAULT_NAME, # 常量:前处理流水线的默认命名 ) def make_act_pre_post_processors( config: ACTConfig, # ACT 策略配置对象,内含设备、特征配置、归一化映射关系等信息 dataset_stats: dict[str, dict[str, torch.Tensor]] | None = None, # 数据集统计信息(如 mean/std),按特征名组织;可为 None ) -\u0026gt; tuple[ PolicyProcessorPipeline[dict[str, Any], dict[str, Any]], PolicyProcessorPipeline[PolicyAction, PolicyAction], ]: \u0026#34;\u0026#34;\u0026#34;Creates the pre- and post-processing pipelines for the ACT policy. The pre-processing pipeline handles normalization, batching, and device placement for the model inputs. The post-processing pipeline handles unnormalization and moves the model outputs back to the CPU. Args: config (ACTConfig): The ACT policy configuration object. dataset_stats (dict[str, dict[str, torch.Tensor]] | None): A dictionary containing dataset statistics (e.g., mean and std) used for normalization. Defaults to None. Returns: tuple[PolicyProcessorPipeline[dict[str, Any], dict[str, Any]], PolicyProcessorPipeline[PolicyAction, PolicyAction]]: A tuple containing the pre-processor pipeline and the post-processor pipeline. \u0026#34;\u0026#34;\u0026#34; # 上面的英文文档说明: # - 本函数构造并返回“前处理(pre)”与“后处理(post)”两个流水线,用于 ACT 策略的输入和输出数据处理。 # - 前处理:将原始观测做重命名、补齐 batch 维度、移动到指定设备、并按数据集统计进行归一化,使之适配模型输入。 # - 后处理:对模型输出做反归一化(还原到原尺度),并移动回 CPU(便于后续使用或与非 GPU 代码交互)。 # - dataset_stats:通常包含每个特征的 mean/std,用于 Normalizer/Unnormalizer;为 None 时可能使用默认策略或跳过部分操作。 # - 返回值是一个元组:(前处理流水线, 后处理流水线) # 定义前处理流水线中包含的“步骤”列表(按顺序执行) input_steps = [ RenameObservationsProcessorStep( rename_map={} ), # 重命名观测键的步骤:这里给了空映射,表示当前不需要改名 AddBatchDimensionProcessorStep(), # 添加 batch 维:当输入是单样本时,变为批大小为 1 的张量,便于模型统一处理 DeviceProcessorStep( device=config.device ), # 设备迁移:将(可能包含张量的)输入移动到 config.device(如 \u0026#34;cuda\u0026#34; 或 \u0026#34;cpu\u0026#34;) NormalizerProcessorStep( # 归一化步骤:对输入/输出特征集合按 norm_map 和 stats 做标准化/归一化 features={ **config.input_features, **config.output_features, }, # 指定需要归一化的特征集合:将输入与输出特征合并 norm_map=config.normalization_mapping, # 指定特征名到“归一化配置/方式”的映射(例如使用哪组统计量) stats=dataset_stats, # 数据集统计(如 mean/std),用于归一化参数 device=config.device, # 将统计量和运算放在相同设备上,避免跨设备拷贝/错误 ), ] # 以上前处理的意图: # 1) RenameObservationsProcessorStep:有些数据集的键名与模型期望不一致,通过重命名统一接口(此处为空映射,意味着保持原样) # 2) AddBatchDimensionProcessorStep:即便是单条数据也要添加 batch 维度,满足大多数深度学习模型形状要求 # 3) DeviceProcessorStep:统一把数据移动到 config 指定的设备(GPU/CPU),确保后续张量运算在同一设备上 # 4) NormalizerProcessorStep:将输入(甚至包含模型要预测的目标特征)进行标准化,使训练/推理更稳定 # 定义后处理流水线步骤:将模型输出从标准化空间映射回原空间,并迁移到 CPU output_steps = [ UnnormalizerProcessorStep( # 反归一化:把模型输出(先前按统计量标准化过)还原到原始数值范围 features=config.output_features, # 仅对输出相关的特征进行反归一化(不会动输入特征) norm_map=config.normalization_mapping, # 使用与前处理一致的归一化映射表,保证前后处理对齐 stats=dataset_stats, # 使用相同的数据集统计参数进行反变换 ), DeviceProcessorStep( device=\u0026#34;cpu\u0026#34; ), # 将最终结果统一移回 CPU:便于日志记录、与非 GPU 组件交互或序列化 ] # 返回前/后处理两个 PolicyProcessorPipeline 实例: # - 对于前处理流水线:输入与输出都是字典(键到任意类型),因为输入通常是多模态观测的字典结构 # - 对于后处理流水线:输入与输出是 PolicyAction(策略动作)结构/对象 return ( PolicyProcessorPipeline[dict[str, Any], dict[str, Any]]( steps=input_steps, # 指定流水线包含的步骤序列 name=POLICY_PREPROCESSOR_DEFAULT_NAME, # 使用默认的“前处理”名称,便于日志或调试 ), PolicyProcessorPipeline[PolicyAction, PolicyAction]( steps=output_steps, # 指定后处理步骤序列 name=POLICY_POSTPROCESSOR_DEFAULT_NAME, # 使用默认的“后处理”名称 to_transition=policy_action_to_transition, # 指定如何把 PolicyAction 转换为 transition(内部可能用于统一接口) to_output=transition_to_policy_action, # 指定如何把 transition 转回 PolicyAction(与上相反的方向) ), ) modeling_act.py:ACT 模型 # PYTHON不换行 #!/usr/bin/env python # Copyright 2024 Tony Z. Zhao and The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # 以上是版权与开源许可声明,表明本代码遵循 Apache 2.0 许可证 \u0026#34;\u0026#34;\u0026#34;Action Chunking Transformer Policy As per Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware (https://huggingface.co/papers/2304.13705). The majority of changes here involve removing unused code, unifying naming, and adding helpful comments. \u0026#34;\u0026#34;\u0026#34; # 说明: # 本文件实现 ACT(Action Chunking Transformer)策略与其底层神经网络。ACT 旨在在机器人强化学习/模仿学习中, # 一次性预测一段连续的动作序列(“动作块”/chunk),以减少每步都要前向一次模型的开销,并改善时序一致性。 # 代码支持两种模式: # 1) 纯 Transformer 预测动作序列; # 2) 可选 VAE(变分自编码器)训练目标:使用一个 VAE encoder 产生隐变量,再用 Transformer(此时相当于 VAE 的解码器)预测动作序列。 # 另外支持可选的“时间集成(Temporal Ensembling)”在推理时对多步动作进行指数加权平均,从而平滑并提升鲁棒性。 # 模块结构总览: # - ACTPolicy:策略封装(选择动作、训练前向、优化参数分组、时间集成/动作队列) # - ACTTemporalEnsembler:在线指数加权时间集成器 # - ACT:核心模型(视觉骨干网络 + Transformer 编码器/解码器 + 各种投影/位置编码 + 动作回归头) # - ACTEncoder / ACTDecoder / 对应 Layer:标准 Transformer 层(支持 pre-norm/post-norm) # - 位置编码(1D/2D 正弦位置编码) # - 实用函数:get_activation_fn、create_sinusoidal_pos_embedding # # 术语与张量形状约定: # - B: batch size # - S: 序列长度(这里多指 chunk_size,即要预测的动作步数) # - D: 隐藏维度(dim_model) # - L: 潜变量维度(latent_dim) # - action_dim: 动作维度 # - 图片特征:从视觉骨干网络输出的 feature map (B, C, H, W),随后会被重排为序列。 # - Transformer 采用 PyTorch 标准接口,序列维度在最前(Seq, Batch, Channel)。 # # 限制:仅添加注释,不修改任何原始代码逻辑或接口。 import math from collections import deque from collections.abc import Callable from itertools import chain import einops # 张量重排工具库,便于通道/维度变换 import numpy as np import torch import torch.nn.functional as F # noqa: N812 import torchvision from lerobot.policies.act.configuration_act import ( ACTConfig, ) # 配置对象,集中管理所有超参数 from lerobot.policies.pretrained import PreTrainedPolicy # 通用策略基类 from lerobot.utils.constants import ( ACTION, OBS_ENV_STATE, OBS_IMAGES, OBS_STATE, ) # 约定的数据字典键名 from torch import Tensor, nn from torchvision.models._utils import ( IntermediateLayerGetter, ) # 从骨干网络中提取中间层输出 from torchvision.ops.misc import ( FrozenBatchNorm2d, ) # 冻结的 BN,常用于迁移学习避免数值漂移 class ACTPolicy(PreTrainedPolicy): \u0026#34;\u0026#34;\u0026#34; Action Chunking Transformer Policy as per Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware (paper: https://huggingface.co/papers/2304.13705, code: https://github.com/tonyzhaozh/act) \u0026#34;\u0026#34;\u0026#34; config_class = ACTConfig name = \u0026#34;act\u0026#34; def __init__( self, config: ACTConfig, ): \u0026#34;\u0026#34;\u0026#34; Args: config: Policy configuration class instance or None, in which case the default instantiation of the configuration class is used. \u0026#34;\u0026#34;\u0026#34; super().__init__(config) config.validate_features() # 校验特征配置是否一致/可用(例如是否提供所需的键) self.config = config self.model = ACT(config) # 核心模型:视觉 + Transformer if config.temporal_ensemble_coeff is not None: # 如果配置了时间集成,就用指数加权的在线方法平滑动作序列 self.temporal_ensembler = ACTTemporalEnsembler( config.temporal_ensemble_coeff, config.chunk_size ) self.reset() # 初始化动作队列或时间集成器状态 def get_optim_params(self) -\u0026gt; dict: # TODO(aliberts, rcadene): As of now, lr_backbone == lr # Should we remove this and just `return self.parameters()`? # 将参数分组以设置不同学习率(例如对视觉骨干设置较小 LR) return [ { \u0026#34;params\u0026#34;: [ p for n, p in self.named_parameters() if not n.startswith(\u0026#34;model.backbone\u0026#34;) and p.requires_grad ] }, { \u0026#34;params\u0026#34;: [ p for n, p in self.named_parameters() if n.startswith(\u0026#34;model.backbone\u0026#34;) and p.requires_grad ], \u0026#34;lr\u0026#34;: self.config.optimizer_lr_backbone, }, ] def reset(self): \u0026#34;\u0026#34;\u0026#34;This should be called whenever the environment is reset.\u0026#34;\u0026#34;\u0026#34; # 环境重置时需清空时间集成器或动作队列,以免使用旧状态 if self.config.temporal_ensemble_coeff is not None: self.temporal_ensembler.reset() else: # 无时间集成时,使用一个定长队列缓存已预测的动作块,逐步弹出 self._action_queue = deque([], maxlen=self.config.n_action_steps) @torch.no_grad() def select_action(self, batch: dict[str, Tensor]) -\u0026gt; Tensor: \u0026#34;\u0026#34;\u0026#34;Select a single action given environment observations. This method wraps `select_actions` in order to return one action at a time for execution in the environment. It works by managing the actions in a queue and only calling `select_actions` when the queue is empty. \u0026#34;\u0026#34;\u0026#34; # 选择动作时确保 eval 模式(禁用 Dropout/BN 统计更新) self.eval() # keeping the policy in eval mode as it could be set to train mode while queue is consumed if self.config.temporal_ensemble_coeff is not None: # 使用时间集成:每次对最新的动作块进行在线融合,并返回当前应执行的单步动作 actions = self.predict_action_chunk(batch) action = self.temporal_ensembler.update(actions) return action # 无时间集成:维护一个动作队列(n_action_steps 个),当队列为空时,再预测新的动作块填充 if len(self._action_queue) == 0: actions = self.predict_action_chunk(batch)[:, : self.config.n_action_steps] # 模型输出形状为 (B, n_action_steps, action_dim),而队列按时间步推进,等价 (n_action_steps, B, *) # 因此需要转置再按时间步扩展到队列 self._action_queue.extend(actions.transpose(0, 1)) return self._action_queue.popleft() @torch.no_grad() def predict_action_chunk(self, batch: dict[str, Tensor]) -\u0026gt; Tensor: \u0026#34;\u0026#34;\u0026#34;Predict a chunk of actions given environment observations.\u0026#34;\u0026#34;\u0026#34; self.eval() if self.config.image_features: # 若配置了图像特征,将用户提供的多路图像拼到统一键 OBS_IMAGES 下(浅拷贝避免改动原 batch) batch = dict( batch ) # shallow copy so that adding a key doesn\u0026#39;t modify the original batch[OBS_IMAGES] = [batch[key] for key in self.config.image_features] actions = self.model(batch)[0] # 仅取预测的动作(忽略 VAE 参数返回) return actions def forward(self, batch: dict[str, Tensor]) -\u0026gt; tuple[Tensor, dict]: \u0026#34;\u0026#34;\u0026#34;Run the batch through the model and compute the loss for training or validation.\u0026#34;\u0026#34;\u0026#34; # 训练/验证前向:输出预测动作与损失 if self.config.image_features: batch = dict( batch ) # shallow copy so that adding a key doesn\u0026#39;t modify the original batch[OBS_IMAGES] = [batch[key] for key in self.config.image_features] actions_hat, (mu_hat, log_sigma_x2_hat) = self.model(batch) # L1 行为克隆损失(对 padding 掩码为 True 的时间步不计入损失) l1_loss = ( F.l1_loss(batch[ACTION], actions_hat, reduction=\u0026#34;none\u0026#34;) * ~batch[\u0026#34;action_is_pad\u0026#34;].unsqueeze(-1) ).mean() loss_dict = {\u0026#34;l1_loss\u0026#34;: l1_loss.item()} if self.config.use_vae: # 当使用 VAE 目标时,额外计算 KL 散度(对潜变量逐维求和,再对 batch 求均值) # log_sigma_x2 是 2*log(sigma),保持与原实现一致 mean_kld = ( ( -0.5 * (1 + log_sigma_x2_hat - mu_hat.pow(2) - (log_sigma_x2_hat).exp()) ) .sum(-1) .mean() ) loss_dict[\u0026#34;kld_loss\u0026#34;] = mean_kld.item() loss = l1_loss + mean_kld * self.config.kl_weight else: loss = l1_loss return loss, loss_dict class ACTTemporalEnsembler: def __init__(self, temporal_ensemble_coeff: float, chunk_size: int) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34;Temporal ensembling as described in Algorithm 2 of https://huggingface.co/papers/2304.13705. The weights are calculated as wᵢ = exp(-temporal_ensemble_coeff * i) where w₀ is the oldest action. They are then normalized to sum to 1 by dividing by Σwᵢ. Here\u0026#39;s some intuition around how the coefficient works: - Setting it to 0 uniformly weighs all actions. - Setting it positive gives more weight to older actions. - Setting it negative gives more weight to newer actions. NOTE: The default value for `temporal_ensemble_coeff` used by the original ACT work is 0.01. This results in older actions being weighed more highly than newer actions (the experiments documented in https://github.com/huggingface/lerobot/pull/319 hint at why highly weighing new actions might be detrimental: doing so aggressively may diminish the benefits of action chunking). Here we use an online method for computing the average rather than caching a history of actions in order to compute the average offline. For a simple 1D sequence it looks something like: ``` import torch seq = torch.linspace(8, 8.5, 100) print(seq) m = 0.01 exp_weights = torch.exp(-m * torch.arange(len(seq))) print(exp_weights) # Calculate offline avg = (exp_weights * seq).sum() / exp_weights.sum() print(\u0026#34;offline\u0026#34;, avg) # Calculate online for i, item in enumerate(seq): if i == 0: avg = item continue avg *= exp_weights[:i].sum() avg += item * exp_weights[i] avg /= exp_weights[: i + 1].sum() print(\u0026#34;online\u0026#34;, avg) ``` \u0026#34;\u0026#34;\u0026#34; # 中文补充:时间集成器对一个动作块内的每个时间步位置 i(0 为最旧)分配权重 w_i = exp(-m * i)。 # 在线更新避免缓存历史所有动作,大幅节省内存/计算,适合推理时逐步滑动窗口融合。 self.chunk_size = chunk_size self.ensemble_weights = torch.exp( -temporal_ensemble_coeff * torch.arange(chunk_size) ) # 累积和用于在线“归一化”更新 self.ensemble_weights_cumsum = torch.cumsum(self.ensemble_weights, dim=0) self.reset() def reset(self): \u0026#34;\u0026#34;\u0026#34;Resets the online computation variables.\u0026#34;\u0026#34;\u0026#34; # 清零内部缓存:当前融合的动作序列与对应的计数(每个时间步融合了多少次) self.ensembled_actions = None # (chunk_size,) count of how many actions are in the ensemble for each time step in the sequence. self.ensembled_actions_count = None def update(self, actions: Tensor) -\u0026gt; Tensor: \u0026#34;\u0026#34;\u0026#34; Takes a (batch, chunk_size, action_dim) sequence of actions, update the temporal ensemble for all time steps, and pop/return the next batch of actions in the sequence. \u0026#34;\u0026#34;\u0026#34; # 将权重张量放到与输入相同的 device 上(CPU/GPU 兼容) self.ensemble_weights = self.ensemble_weights.to(device=actions.device) self.ensemble_weights_cumsum = self.ensemble_weights_cumsum.to( device=actions.device ) if self.ensembled_actions is None: # 第一次调用:直接把预测的动作块克隆为当前融合序列 self.ensembled_actions = actions.clone() # 记录每个时间步目前的“融合次数”=1(形状对齐为 (S,1),便于广播) self.ensembled_actions_count = torch.ones( (self.chunk_size, 1), dtype=torch.long, device=self.ensembled_actions.device, ) else: # 对已有的融合序列(除了最后一个时间步)进行在线更新: # old_avg * sum(w[:i]) + new * w[i] 再除以 sum(w[:i+1]),形如“带权平均”的递推公式 self.ensembled_actions *= self.ensemble_weights_cumsum[ self.ensembled_actions_count - 1 ] self.ensembled_actions += ( actions[:, :-1] * self.ensemble_weights[self.ensembled_actions_count] ) self.ensembled_actions /= self.ensemble_weights_cumsum[ self.ensembled_actions_count ] # 融合计数自增,封顶为 chunk_size self.ensembled_actions_count = torch.clamp( self.ensembled_actions_count + 1, max=self.chunk_size ) # 将“最新一步”的原始动作直接拼到末尾(该位置没有历史平均) self.ensembled_actions = torch.cat( [self.ensembled_actions, actions[:, -1:]], dim=1 ) # 对应的计数也拼接 1 self.ensembled_actions_count = torch.cat( [ self.ensembled_actions_count, torch.ones_like(self.ensembled_actions_count[-1:]), ] ) # 消费/弹出融合序列的第一个动作(当前要执行的动作),并滑动窗口 action, self.ensembled_actions, self.ensembled_actions_count = ( self.ensembled_actions[:, 0], self.ensembled_actions[:, 1:], self.ensembled_actions_count[1:], ) return action class ACT(nn.Module): \u0026#34;\u0026#34;\u0026#34;Action Chunking Transformer: The underlying neural network for ACTPolicy. Note: In this code we use the terms `vae_encoder`, \u0026#39;encoder\u0026#39;, `decoder`. The meanings are as follows. - The `vae_encoder` is, as per the literature around variational auto-encoders (VAE), the part of the model that encodes the target data (a sequence of actions), and the condition (the robot joint-space). - A transformer with an `encoder` (not the VAE encoder) and `decoder` (not the VAE decoder) with cross-attention is used as the VAE decoder. For these terms, we drop the `vae_` prefix because we have an option to train this model without the variational objective (in which case we drop the `vae_encoder` altogether, and nothing about this model has anything to do with a VAE). Transformer Used alone for inference (acts as VAE decoder during training) ┌───────────────────────┐ │ Outputs │ │ ▲ │ │ ┌─────►┌───────┐ │ ┌──────┐ │ │ │Transf.│ │ │ │ │ ├─────►│decoder│ │ ┌────┴────┐ │ │ │ │ │ │ │ │ │ │ ┌───┴───┬─►│ │ │ │ VAE │ │ │ │ │ └───────┘ │ │ encoder │ │ │ │Transf.│ │ │ │ │ │ │encoder│ │ └───▲─────┘ │ │ │ │ │ │ │ │ └▲──▲─▲─┘ │ │ │ │ │ │ │ │ inputs └─────┼──┘ │ image emb. │ │ state emb. │ └───────────────────────┘ \u0026#34;\u0026#34;\u0026#34; def __init__(self, config: ACTConfig): # BERT 风格的 VAE 编码器输入: [CLS, 机器人当前关节状态(可选), 动作序列]。 # CLS token 经过投影后输出潜变量分布参数(mean 与 log_sigma_x2)。 super().__init__() self.config = config if self.config.use_vae: # VAE 编码器(仅在使用 VAE 目标且训练阶段时启用) self.vae_encoder = ACTEncoder(config, is_vae_encoder=True) self.vae_encoder_cls_embed = nn.Embedding(1, config.dim_model) # 机器人关节状态投影到 Transformer 隐藏维度 if self.config.robot_state_feature: self.vae_encoder_robot_state_input_proj = nn.Linear( self.config.robot_state_feature.shape[0], config.dim_model ) # 动作(目标关节位姿/速度等)投影到隐藏维度 self.vae_encoder_action_input_proj = nn.Linear( self.config.action_feature.shape[0], config.dim_model, ) # 将 VAE 编码器的 CLS 输出映射为潜变量分布参数(均值和对数方差 * 2) self.vae_encoder_latent_output_proj = nn.Linear( config.dim_model, config.latent_dim * 2 ) # 固定正弦位置编码(1D),长度 = 1(CLS) + S(动作步) + [1(关节状态,可选)] num_input_token_encoder = 1 + config.chunk_size if self.config.robot_state_feature: num_input_token_encoder += 1 self.register_buffer( \u0026#34;vae_encoder_pos_enc\u0026#34;, create_sinusoidal_pos_embedding( num_input_token_encoder, config.dim_model ).unsqueeze(0), ) # 视觉骨干网络(例如 ResNet),用于提取图像特征 if self.config.image_features: backbone_model = getattr(torchvision.models, config.vision_backbone)( replace_stride_with_dilation=[ False, False, config.replace_final_stride_with_dilation, ], weights=config.pretrained_backbone_weights, norm_layer=FrozenBatchNorm2d, ) # 使用 IntermediateLayerGetter 从指定层(这里为 layer4)获取特征图 # 输出为字典 {\u0026#34;feature_map\u0026#34;: output} self.backbone = IntermediateLayerGetter( backbone_model, return_layers={\u0026#34;layer4\u0026#34;: \u0026#34;feature_map\u0026#34;} ) # Transformer:在使用 VAE 时相当于解码器(cross-attend 到条件输入) self.encoder = ACTEncoder(config) self.decoder = ACTDecoder(config) # Transformer 编码器的输入投影与位置编码: # token 顺序:[latent, (robot_state), (env_state), (image_feature_map_pixels...)] if self.config.robot_state_feature: self.encoder_robot_state_input_proj = nn.Linear( self.config.robot_state_feature.shape[0], config.dim_model ) if self.config.env_state_feature: self.encoder_env_state_input_proj = nn.Linear( self.config.env_state_feature.shape[0], config.dim_model ) self.encoder_latent_input_proj = nn.Linear(config.latent_dim, config.dim_model) if self.config.image_features: # 将骨干网络的通道维投影到 dim_model(1x1 卷积作为线性投影) self.encoder_img_feat_input_proj = nn.Conv2d( backbone_model.fc.in_features, config.dim_model, kernel_size=1 ) # 1D token(latent/robot_state/env_state)的可学习位置嵌入 n_1d_tokens = 1 # latent if self.config.robot_state_feature: n_1d_tokens += 1 if self.config.env_state_feature: n_1d_tokens += 1 self.encoder_1d_feature_pos_embed = nn.Embedding(n_1d_tokens, config.dim_model) if self.config.image_features: # 2D 正弦位置编码(对 feature map 的每个像素提供位置信息) self.encoder_cam_feat_pos_embed = ACTSinusoidalPositionEmbedding2d( config.dim_model // 2 ) # Transformer 解码器:为 S 个要预测的时间步提供可学习查询(DETR 风格) self.decoder_pos_embed = nn.Embedding(config.chunk_size, config.dim_model) # 最终线性头:将 decoder 输出映射为动作维度 self.action_head = nn.Linear( config.dim_model, self.config.action_feature.shape[0] ) self._reset_parameters() def _reset_parameters(self): \u0026#34;\u0026#34;\u0026#34;Xavier-uniform initialization of the transformer parameters as in the original code.\u0026#34;\u0026#34;\u0026#34; # 对 Transformer 层进行 Xavier 均匀初始化,提升训练稳定性 for p in chain(self.encoder.parameters(), self.decoder.parameters()): if p.dim() \u0026gt; 1: nn.init.xavier_uniform_(p) def forward( self, batch: dict[str, Tensor] ) -\u0026gt; tuple[Tensor, tuple[Tensor, Tensor] | tuple[None, None]]: \u0026#34;\u0026#34;\u0026#34;A forward pass through the Action Chunking Transformer (with optional VAE encoder). `batch` should have the following structure: { [robot_state_feature] (optional): (B, state_dim) batch of robot states. [image_features]: (B, n_cameras, C, H, W) batch of images. AND/OR [env_state_feature]: (B, env_dim) batch of environment states. [action_feature] (optional, only if training with VAE): (B, chunk_size, action dim) batch of actions. } Returns: (B, chunk_size, action_dim) batch of action sequences Tuple containing the latent PDF\u0026#39;s parameters (mean, log(σ²)) both as (B, L) tensors where L is the latent dimension. \u0026#34;\u0026#34;\u0026#34; # 当使用 VAE + 训练模式时,要求 batch 中必须包含监督的动作序列 ACTION if self.config.use_vae and self.training: assert ACTION in batch, ( \u0026#34;actions must be provided when using the variational objective in training mode.\u0026#34; ) # 估计 batch 大小:优先从图像,若无图像则从环境状态推断 batch_size = ( batch[OBS_IMAGES][0].shape[0] if OBS_IMAGES in batch else batch[OBS_ENV_STATE].shape[0] ) # 1) 准备潜变量(latent) if self.config.use_vae and ACTION in batch and self.training: # 训练 + VAE:通过 VAE encoder 从 [CLS, 关节状态(可选), 动作序列] 推断潜变量分布参数 cls_embed = einops.repeat( self.vae_encoder_cls_embed.weight, \u0026#34;1 d -\u0026gt; b 1 d\u0026#34;, b=batch_size ) # (B, 1, D) if self.config.robot_state_feature: robot_state_embed = self.vae_encoder_robot_state_input_proj( batch[OBS_STATE] ) robot_state_embed = robot_state_embed.unsqueeze(1) # (B, 1, D) action_embed = self.vae_encoder_action_input_proj( batch[ACTION] ) # (B, S, D) if self.config.robot_state_feature: vae_encoder_input = [ cls_embed, robot_state_embed, action_embed, ] # (B, S+2, D) else: vae_encoder_input = [cls_embed, action_embed] vae_encoder_input = torch.cat(vae_encoder_input, axis=1) # 固定位置编码(与原实现保持一致,使用 clone().detach()) pos_embed = self.vae_encoder_pos_enc.clone().detach() # (1, S+2, D) # key_padding_mask:前面 CLS 和关节状态不是 padding,后面根据 action_is_pad 指示 # False 表示不是 pad;形状 (B, S+1 或 S+2) cls_joint_is_pad = torch.full( (batch_size, 2 if self.config.robot_state_feature else 1), False, device=batch[OBS_STATE].device, ) key_padding_mask = torch.cat( [cls_joint_is_pad, batch[\u0026#34;action_is_pad\u0026#34;]], axis=1 ) # (bs, seq+1 or 2) # 送入 VAE 编码器,取 CLS 位置的输出(包含全序列信息),再映射得到 (mu, log_sigma_x2) cls_token_out = self.vae_encoder( vae_encoder_input.permute(1, 0, 2), pos_embed=pos_embed.permute(1, 0, 2), key_padding_mask=key_padding_mask, )[0] # select the class token, with shape (B, D) latent_pdf_params = self.vae_encoder_latent_output_proj(cls_token_out) mu = latent_pdf_params[:, : self.config.latent_dim] # 注意:这里返回的是 2*log(sigma),与原实现一致 log_sigma_x2 = latent_pdf_params[:, self.config.latent_dim :] # 重参数化采样 latent: z = mu + sigma * eps latent_sample = mu + log_sigma_x2.div(2).exp() * torch.randn_like(mu) else: # 推理或未使用 VAE:latent 设为全零(代表“无信息”的先验) mu = log_sigma_x2 = None # TODO(rcadene, alexander-soare): remove call to `.to` to speedup forward ; precompute and use buffer latent_sample = torch.zeros( [batch_size, self.config.latent_dim], dtype=torch.float32 ).to(batch[OBS_STATE].device) # 2) 准备 Transformer 编码器输入 token 与位置编码 encoder_in_tokens = [self.encoder_latent_input_proj(latent_sample)] # 1D token 的可学习位置嵌入(先堆为 list,后面与图像 token 一起 stack) encoder_in_pos_embed = list( self.encoder_1d_feature_pos_embed.weight.unsqueeze(1) ) # 机器人关节状态 token if self.config.robot_state_feature: encoder_in_tokens.append( self.encoder_robot_state_input_proj(batch[OBS_STATE]) ) # 环境状态 token if self.config.env_state_feature: encoder_in_tokens.append( self.encoder_env_state_input_proj(batch[OBS_ENV_STATE]) ) if self.config.image_features: # 多相机图像:各自经过骨干提特征,再通过 1x1 conv 投影至 dim_model # 注意:对 MPS 设备做过数值稳定性注意(保持与原实现的注释一致) for img in batch[OBS_IMAGES]: cam_features = self.backbone(img)[ \u0026#34;feature_map\u0026#34; ] # (B, C_backbone, H, W) cam_pos_embed = self.encoder_cam_feat_pos_embed(cam_features).to( dtype=cam_features.dtype ) cam_features = self.encoder_img_feat_input_proj( cam_features ) # -\u0026gt; (B, D, H, W) # 重排为 (Seq, B, D),其中 Seq = H*W cam_features = einops.rearrange(cam_features, \u0026#34;b c h w -\u0026gt; (h w) b c\u0026#34;) cam_pos_embed = einops.rearrange(cam_pos_embed, \u0026#34;b c h w -\u0026gt; (h w) b c\u0026#34;) # 直接 extend(列表形式)以避免先积累后再 concat 的额外开销 encoder_in_tokens.extend(list(cam_features)) encoder_in_pos_embed.extend(list(cam_pos_embed)) # 将所有 token 按序列维 stack 成张量:(ES, B, D) encoder_in_tokens = torch.stack(encoder_in_tokens, axis=0) encoder_in_pos_embed = torch.stack(encoder_in_pos_embed, axis=0) # 3) 经过 Transformer 编码器/解码器 encoder_out = self.encoder(encoder_in_tokens, pos_embed=encoder_in_pos_embed) # 解码器输入初始化为全零(S, B, D),再加上可学习的 decoder_pos_embed 作为查询 # TODO(rcadene, alexander-soare): remove call to `device` ; precompute and use buffer decoder_in = torch.zeros( (self.config.chunk_size, batch_size, self.config.dim_model), dtype=encoder_in_pos_embed.dtype, device=encoder_in_pos_embed.device, ) decoder_out = self.decoder( decoder_in, encoder_out, encoder_pos_embed=encoder_in_pos_embed, decoder_pos_embed=self.decoder_pos_embed.weight.unsqueeze(1), ) # (S, B, D) -\u0026gt; (B, S, D) decoder_out = decoder_out.transpose(0, 1) # 动作回归头:(B, S, D) -\u0026gt; (B, S, action_dim) actions = self.action_head(decoder_out) # 返回动作与(可选)VAE 参数(训练使用) return actions, (mu, log_sigma_x2) class ACTEncoder(nn.Module): \u0026#34;\u0026#34;\u0026#34;Convenience module for running multiple encoder layers, maybe followed by normalization.\u0026#34;\u0026#34;\u0026#34; # 一个封装的 Transformer Encoder 堆叠模块,支持 pre-norm 配置,便于复用(包括作为 VAE encoder) def __init__(self, config: ACTConfig, is_vae_encoder: bool = False): super().__init__() self.is_vae_encoder = is_vae_encoder num_layers = ( config.n_vae_encoder_layers if self.is_vae_encoder else config.n_encoder_layers ) self.layers = nn.ModuleList( [ACTEncoderLayer(config) for _ in range(num_layers)] ) self.norm = nn.LayerNorm(config.dim_model) if config.pre_norm else nn.Identity() def forward( self, x: Tensor, pos_embed: Tensor | None = None, key_padding_mask: Tensor | None = None, ) -\u0026gt; Tensor: # 逐层前向;支持外部传入位置编码与 padding 掩码 for layer in self.layers: x = layer(x, pos_embed=pos_embed, key_padding_mask=key_padding_mask) x = self.norm(x) return x class ACTEncoderLayer(nn.Module): def __init__(self, config: ACTConfig): super().__init__() self.self_attn = nn.MultiheadAttention( config.dim_model, config.n_heads, dropout=config.dropout ) # 前馈网络 FFN:Linear -\u0026gt; 激活 -\u0026gt; Dropout -\u0026gt; Linear self.linear1 = nn.Linear(config.dim_model, config.dim_feedforward) self.dropout = nn.Dropout(config.dropout) self.linear2 = nn.Linear(config.dim_feedforward, config.dim_model) # 残差层归一化(支持 pre-norm 或 post-norm) self.norm1 = nn.LayerNorm(config.dim_model) self.norm2 = nn.LayerNorm(config.dim_model) self.dropout1 = nn.Dropout(config.dropout) self.dropout2 = nn.Dropout(config.dropout) self.activation = get_activation_fn(config.feedforward_activation) self.pre_norm = config.pre_norm def forward( self, x, pos_embed: Tensor | None = None, key_padding_mask: Tensor | None = None ) -\u0026gt; Tensor: # 自注意力子层 skip = x if self.pre_norm: x = self.norm1(x) q = k = x if pos_embed is None else x + pos_embed x = self.self_attn(q, k, value=x, key_padding_mask=key_padding_mask) x = x[0] # note: [0] to select just the output, not the attention weights x = skip + self.dropout1(x) # 残差 # 前馈子层 if self.pre_norm: skip = x x = self.norm2(x) else: x = self.norm1(x) skip = x x = self.linear2(self.dropout(self.activation(self.linear1(x)))) x = skip + self.dropout2(x) if not self.pre_norm: x = self.norm2(x) return x class ACTDecoder(nn.Module): def __init__(self, config: ACTConfig): \u0026#34;\u0026#34;\u0026#34;Convenience module for running multiple decoder layers followed by normalization.\u0026#34;\u0026#34;\u0026#34; super().__init__() self.layers = nn.ModuleList( [ACTDecoderLayer(config) for _ in range(config.n_decoder_layers)] ) self.norm = nn.LayerNorm(config.dim_model) def forward( self, x: Tensor, encoder_out: Tensor, decoder_pos_embed: Tensor | None = None, encoder_pos_embed: Tensor | None = None, ) -\u0026gt; Tensor: # 逐层 Decoder,包含自注意力与跨注意力(对 encoder_out 进行 cross-attention) for layer in self.layers: x = layer( x, encoder_out, decoder_pos_embed=decoder_pos_embed, encoder_pos_embed=encoder_pos_embed, ) if self.norm is not None: x = self.norm(x) return x class ACTDecoderLayer(nn.Module): def __init__(self, config: ACTConfig): super().__init__() self.self_attn = nn.MultiheadAttention( config.dim_model, config.n_heads, dropout=config.dropout ) self.multihead_attn = nn.MultiheadAttention( config.dim_model, config.n_heads, dropout=config.dropout ) # FFN self.linear1 = nn.Linear(config.dim_model, config.dim_feedforward) self.dropout = nn.Dropout(config.dropout) self.linear2 = nn.Linear(config.dim_feedforward, config.dim_model) # 三个归一化/Dropout 对应三处残差连接 self.norm1 = nn.LayerNorm(config.dim_model) self.norm2 = nn.LayerNorm(config.dim_model) self.norm3 = nn.LayerNorm(config.dim_model) self.dropout1 = nn.Dropout(config.dropout) self.dropout2 = nn.Dropout(config.dropout) self.dropout3 = nn.Dropout(config.dropout) self.activation = get_activation_fn(config.feedforward_activation) self.pre_norm = config.pre_norm def maybe_add_pos_embed(self, tensor: Tensor, pos_embed: Tensor | None) -\u0026gt; Tensor: # 若提供了位置编码,则与输入相加(Transformer 常见用法) return tensor if pos_embed is None else tensor + pos_embed def forward( self, x: Tensor, encoder_out: Tensor, decoder_pos_embed: Tensor | None = None, encoder_pos_embed: Tensor | None = None, ) -\u0026gt; Tensor: \u0026#34;\u0026#34;\u0026#34; Args: x: (Decoder Sequence, Batch, Channel) tensor of input tokens. encoder_out: (Encoder Sequence, B, C) output features from the last layer of the encoder we are cross-attending with. encoder_pos_embed: (ES, 1, C) positional embedding for keys (from the encoder). decoder_pos_embed: (DS, 1, C) positional embedding for the queries (from the decoder). Returns: (DS, B, C) tensor of decoder output features. \u0026#34;\u0026#34;\u0026#34; # 1) 自注意力(Decoder 内部 token 之间交互) skip = x if self.pre_norm: x = self.norm1(x) q = k = self.maybe_add_pos_embed(x, decoder_pos_embed) x = self.self_attn(q, k, value=x)[ 0 ] # select just the output, not the attention weights x = skip + self.dropout1(x) # 2) 跨注意力(对 Encoder 输出进行查询) if self.pre_norm: skip = x x = self.norm2(x) else: x = self.norm1(x) skip = x x = self.multihead_attn( query=self.maybe_add_pos_embed(x, decoder_pos_embed), key=self.maybe_add_pos_embed(encoder_out, encoder_pos_embed), value=encoder_out, )[0] # select just the output, not the attention weights x = skip + self.dropout2(x) # 3) FFN if self.pre_norm: skip = x x = self.norm3(x) else: x = self.norm2(x) skip = x x = self.linear2(self.dropout(self.activation(self.linear1(x)))) x = skip + self.dropout3(x) if not self.pre_norm: x = self.norm3(x) return x def create_sinusoidal_pos_embedding(num_positions: int, dimension: int) -\u0026gt; Tensor: \u0026#34;\u0026#34;\u0026#34;1D sinusoidal positional embeddings as in Attention is All You Need. Args: num_positions: Number of token positions required. Returns: (num_positions, dimension) position embeddings (the first dimension is the batch dimension). \u0026#34;\u0026#34;\u0026#34; # 标准的 1D 正弦/余弦位置编码实现,频率按几何级数变化(温度=10000),偶数维使用正弦,奇数维使用余弦。 def get_position_angle_vec(position): return [ position / np.power(10000, 2 * (hid_j // 2) / dimension) for hid_j in range(dimension) ] sinusoid_table = np.array( [get_position_angle_vec(pos_i) for pos_i in range(num_positions)] ) sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1 return torch.from_numpy(sinusoid_table).float() class ACTSinusoidalPositionEmbedding2d(nn.Module): \u0026#34;\u0026#34;\u0026#34;2D sinusoidal positional embeddings similar to what\u0026#39;s presented in Attention Is All You Need. The variation is that the position indices are normalized in [0, 2π] (not quite: the lower bound is 1/H for the vertical direction, and 1/W for the horizontal direction. \u0026#34;\u0026#34;\u0026#34; # 为 2D 特征图(H,W)生成二维正弦位置编码。与常见实现不同,位置索引被缩放到 [0, 2π] 区间(近似), # 然后同样以几何级数频率生成正/余弦分量,最后在通道维上拼接(y 分量在前,x 分量在后)。 def __init__(self, dimension: int): \u0026#34;\u0026#34;\u0026#34; Args: dimension: The desired dimension of the embeddings. \u0026#34;\u0026#34;\u0026#34; super().__init__() self.dimension = dimension self._two_pi = 2 * math.pi self._eps = 1e-6 # 频率几何级数的“温度”(与 1D 的 10000 一致) self._temperature = 10000 def forward(self, x: Tensor) -\u0026gt; Tensor: \u0026#34;\u0026#34;\u0026#34; Args: x: A (B, C, H, W) batch of 2D feature map to generate the embeddings for. Returns: A (1, C, H, W) batch of corresponding sinusoidal positional embeddings. \u0026#34;\u0026#34;\u0026#34; # 仅需 H、W 形状,因此构造一个形状 (1, H, W) 的“非 mask” not_mask = torch.ones_like(x[0, :1]) # (1, H, W) # y/x 方向的累计和相当于 1..H 与 1..W(原实现从 1 开始,而不是 0) y_range = not_mask.cumsum(1, dtype=torch.float32) x_range = not_mask.cumsum(2, dtype=torch.float32) # 归一化到 [0, 2π](加入 eps 避免分母为 0) y_range = y_range / (y_range[:, -1:, :] + self._eps) * self._two_pi x_range = x_range / (x_range[:, :, -1:] + self._eps) * self._two_pi # 频率几何序列(偶数/奇数通道分别对应 sin/cos) inverse_frequency = self._temperature ** ( 2 * (torch.arange(self.dimension, dtype=torch.float32, device=x.device) // 2) / self.dimension ) # 扩展最后一维以按通道除以频率:(1, H, W, 1) x_range = x_range.unsqueeze(-1) / inverse_frequency # (1, H, W, 1) y_range = y_range.unsqueeze(-1) / inverse_frequency # (1, H, W, 1) # 交错堆叠 sin/cos,并在通道维上展平:(1, H, W, C//2) pos_embed_x = torch.stack( (x_range[..., 0::2].sin(), x_range[..., 1::2].cos()), dim=-1 ).flatten(3) pos_embed_y = torch.stack( (y_range[..., 0::2].sin(), y_range[..., 1::2].cos()), dim=-1 ).flatten(3) pos_embed = torch.cat((pos_embed_y, pos_embed_x), dim=3).permute( 0, 3, 1, 2 ) # (1, C, H, W) return pos_embed def get_activation_fn(activation: str) -\u0026gt; Callable: \u0026#34;\u0026#34;\u0026#34;Return an activation function given a string.\u0026#34;\u0026#34;\u0026#34; # 将字符串名称映射到对应的激活函数实现 if activation == \u0026#34;relu\u0026#34;: return F.relu if activation == \u0026#34;gelu\u0026#34;: return F.gelu if activation == \u0026#34;glu\u0026#34;: return F.glu raise RuntimeError(f\u0026#34;activation should be relu/gelu/glu, not {activation}.\u0026#34;) ACT 模型微调 # 关键参数 # 详见 configuration_act.py。\n常用项如下(括号内为配置项名称):\n每次预测的动作数量(chunk_size):通常 50–100; 快速任务:10–30 中等任务:50–100 慢速任务:100–200 一般建议从 50 起步 实际执行的动作步数(n_action_steps):必须满足 n_action_steps ≤ chunk_size,推荐二者相同(如均为 100)。 历史观测步数/上下文长度(n_obs_steps)。 Transformer 维度(dim_model、dim_feedforward)、层数(n_encoder_layers、n_decoder_layers)、注意力头数(n_heads)。 视觉主干网络(vision_backbone,如 resnet18)。 参考命令 # 多摄像头配置 SHELL不换行 # 针对多摄像头设置的 ACT 训练 lerobot-train \\ --policy.type act \\ --dataset.repo_id ${HF_USER}/your_dataset \\ --batch_size 4 \\ --steps 100000 \\ --output_dir outputs/train/act_multicam \\ --job_name act_multicam_training \\ --policy.device cuda \\ --policy.chunk_size 100 \\ --policy.n_action_steps 100 \\ --policy.n_obs_steps 2 \\ --policy.vision_backbone resnet18 \\ --policy.dim_model 512 \\ --policy.dim_feedforward 3200 \\ --policy.n_encoder_layers 4 \\ --policy.n_decoder_layers 1 \\ --policy.n_heads 8 \\ --policy.optimizer_lr 1e-5 \\ --policy.optimizer_weight_decay 1e-4 \\ --policy.push_to_hub false \\ --save_checkpoint true \\ --wandb.enable true 内存优化配置 SHELL不换行 # 针对显存较小的 GPU lerobot-train \\ --policy.type act \\ --dataset.repo_id io-ai-data/lerobot_data \\ --batch_size 2 \\ --steps 75000 \\ --output_dir outputs/train/act_memory_opt \\ --job_name act_memory_optimized \\ --policy.device cuda \\ --policy.chunk_size 100 \\ --policy.n_action_steps 100 \\ --policy.n_obs_steps 1 \\ --policy.vision_backbone resnet18 \\ --policy.dim_model 256 \\ --policy.optimizer_lr 1e-5 \\ --policy.use_amp true \\ --num_workers 2 \\ --policy.push_to_hub false \\ --save_checkpoint true \\ --wandb.enable true 结语 # 本文对 ACT 模型的原理、架构、训练与推理流程、消融实验、代码实现与调参与实践建议进行了梳理,期望对你的应用落地有所帮助。\n参考资料 # 点击展开查看参考资料 ACT 论文地址 ACT 论文在线阅读 LeRobot ACT 文档 LeRobot ACT 代码实现 ACT 模型微调 ACT 代码实现 【VA 系列】ALOHA(ACT) 【精析】ACT 模型 | ALOHA | Action Chunking Transformer ","date":"2025年10月20日","externalUrl":null,"permalink":"/posts/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-act/","section":"文章","summary":"","title":"深入浅出 ACT","type":"posts"},{"content":"","date":"2025年10月20日","externalUrl":null,"permalink":"/series/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%85%B7%E8%BA%AB%E6%99%BA%E8%83%BD/","section":"Series","summary":"","title":"深入浅出具身智能","type":"series"},{"content":"","date":"2025年10月19日","externalUrl":null,"permalink":"/tags/cvae/","section":"Tags","summary":"","title":"CVAE","type":"tags"},{"content":"","date":"2025年10月19日","externalUrl":null,"permalink":"/tags/vae/","section":"Tags","summary":"","title":"VAE","type":"tags"},{"content":" 0. 写在开头 # 在 ACT 模型 中,我们提到了 VAE(Variational Autoencoder,变分自编码器) 和 CVAE(Conditional Variational Autoencoder,条件变分自编码器)。本文对这两类生成模型做一个直观梳理。\nVAE:把自编码器放进概率图景,用变分推断与重参数化技巧把“学习可采样的潜变量分布”转成可微的重建项 + KL 正则,从而既能压缩又能生成。 CVAE:在 VAE 的潜变量与/或解码器中显式加入条件 $c$,使生成分布变为 $p_\\theta(x\\mid z,c)$,从而实现受控生成(按标签、文本等定向出样本)。 AE / VAE / CVAE 对比示意图\nAE(Autoencoder,自编码器) # AE 结构示意图\n结构:编码器将输入 $x$ 映射为确定性的潜在向量 $z=f_\\phi(x)$;解码器重建 $\\hat x=g_\\theta(z)$。\n训练目标:最小化重建误差(无先验与 $\\mathrm{KL}$ 正则)。常见损失:\n$$ \\mathcal{L}_{\\mathrm{AE}}(\\theta,\\phi) = \\mathbb{E}_{x\\sim\\mathcal{D}}\\big[\\ell\\!\\left(x,\\; g_\\theta\\!\\big(f_\\phi(x)\\big)\\right)\\big], $$ 其中 $\\ell$ 可取均方误差(MSE)或交叉熵等。 作用:学习有效表示与压缩;但潜在空间往往不连续、难以平滑采样生成。\nVAE(Variational Autoencoder,变分自编码器) # VAE 结构示意图\n核心想法:让编码器输出分布参数(如均值 $\\mu_\\phi(x)$、方差 $\\sigma_\\phi^2(x)$),定义近似后验 $q_\\phi(z\\mid x)=\\mathcal N\\!\\big(\\mu_\\phi(x),\\operatorname{diag}(\\sigma_\\phi^2(x))\\big)$,再从中采样 $z$ 送入解码器。 重参数化技巧:为使采样可反传,写作 $$ z=\\mu_\\phi(x)+\\sigma_\\phi(x)\\odot\\varepsilon,\\quad \\varepsilon\\sim\\mathcal N(0,I). $$ 目标函数(最大化 ELBO,等价于最小化负 ELBO): $$ \\mathcal L_{\\mathrm{VAE}}(\\theta,\\phi) = \\mathbb{E}_{q_\\phi(z\\mid x)}\\!\\left[\\log p_\\theta(x\\mid z)\\right] - \\mathrm{KL}\\!\\left(q_\\phi(z\\mid x)\\,\\|\\,p(z)\\right) $$ 其中先验常取 $p(z)=\\mathcal N(0,I)$。 第一项是重建项,鼓励复原输入; 第二项是正则项,将近似后验拉向先验,使潜在空间平滑、可插值、可采样。 优点:潜在空间连续、可控插值与随机采样生成新样本。 局限:基本 VAE 是无条件生成,难以指定生成“某一类”样本;且重建往往偏模糊(对像素级似然的高斯假设等有关)。 VAE 数学推导 # 设有 $N$ 个独立同分布样本\n$$ X=(x^1,x^2,\\cdots,x^N). $$我们的目标是对生成模型的参数 $\\theta$ 进行极大似然估计。似然写为 $p_\\theta(X)$。在 i.i.d. 假设下,有\n$$ \\log p_\\theta(X)=\\sum_{i=1}^N \\log p_\\theta(x^i). $$先考察单个样本 $x$ 的对数似然。对于任意潜变量 $z$,恒等式\n$$ \\log p_\\theta(x)=\\log \\frac{p_\\theta(x,z)}{p_\\theta(z\\mid x)} $$成立(当 $p_\\theta(z\\mid x)\u003e0$ 时)。\n引入变分分布 $q_\\phi(z\\mid x)$(用以近似难以求解的后验 $p_\\theta(z\\mid x)$)。对上述等式两侧以 $q_\\phi(z\\mid x)$ 积分:\n左边\n$$ \\int q_\\phi(z\\mid x)\\,\\log p_\\theta(x)\\,dz=\\log p_\\theta(x), $$右边\n$$ \\begin{aligned} \\int q_\\phi(z\\mid x)\\,\\log \\frac{p_\\theta(x,z)}{p_\\theta(z\\mid x)}\\,dz \u0026=\\int q_\\phi(z\\mid x)\\,\\log \\frac{p_\\theta(x,z)}{q_\\phi(z\\mid x)}\\,dz \\;+\\;\\mathrm{KL}\\!\\left(q_\\phi(z\\mid x)\\,\\|\\,p_\\theta(z\\mid x)\\right). \\end{aligned} $$因此得到\n$$ \\log p_\\theta(x) =\\underbrace{\\int q_\\phi(z\\mid x)\\,\\log \\frac{p_\\theta(x,z)}{q_\\phi(z\\mid x)}\\,dz}_{\\text{ELBO}} +\\mathrm{KL}\\!\\left(q_\\phi(z\\mid x)\\,\\|\\,p_\\theta(z\\mid x)\\right). $$由 $\\mathrm{KL}\\ge 0$ 可知,下界(ELBO)满足\n$$ \\log p_\\theta(x)\\;\\ge\\;\\mathcal{L}(\\theta,\\phi;x) :=\\int q_\\phi(z\\mid x)\\,\\log \\frac{p_\\theta(x,z)}{q_\\phi(z\\mid x)}\\,dz. $$将联合分布按链式分解 $p_\\theta(x,z)=p_\\theta(x\\mid z)\\,p(z)$,并对 ELBO 重写为标准形式:\n$$ \\begin{aligned} \\mathcal{L}(\\theta,\\phi;x) \u0026=\\mathbb{E}_{z\\sim q_\\phi(z\\mid x)}\\big[\\log p_\\theta(x\\mid z)\\big] -\\mathrm{KL}\\!\\left(q_\\phi(z\\mid x)\\,\\|\\,p(z)\\right). \\end{aligned} $$其中第一项是重构项(期望对数似然),第二项是将近似后验拉向先验的正则项。\n对整批样本,目标为\n$$ \\max_{\\theta,\\phi}\\;\\sum_{i=1}^N \\mathcal{L}(\\theta,\\phi;x^i) =\\max_{\\theta,\\phi}\\;\\sum_{i=1}^N\\left( \\mathbb{E}_{z\\sim q_\\phi(z\\mid x^i)}\\big[\\log p_\\theta(x^i\\mid z)\\big] -\\mathrm{KL}\\!\\left(q_\\phi(z\\mid x^i)\\,\\|\\,p(z)\\right)\\right). $$由于 $p_\\theta(z\\mid x)$ 难以直接计算,常用的做法是直接最大化 ELBO(等价于在固定 $x$ 与 $\\theta$ 时最小化 $\\mathrm{KL}\\!\\left(q_\\phi(z\\mid x)\\,\\|\\,p_\\theta(z\\mid x)\\right)$,因为 $\\log p_\\theta(x)$ 为常数)。优化方式上既可以联合对 $(\\theta,\\phi)$ 做梯度上升,也可以采用变分 EM 的交替策略:\n步骤 ①:固定 $\\theta$,更新 $\\phi$,以最大化 $\\mathcal{L}(\\theta,\\phi;x)$;\n步骤 ②:固定 $\\phi$,更新 $\\theta$,以最大化 $\\mathcal{L}(\\theta,\\phi;x)$。\n直观上,第一项 $\\mathbb{E}_{z\\sim q_\\phi(z\\mid x)}[\\log p_\\theta(x\\mid z)]$ 可理解为从 $q_\\phi(z\\mid x)$ 采样 $z$ 后的重构对数似然的期望,最大化它等价于最小化重构误差;第二项 $-\\mathrm{KL}\\!\\left(q_\\phi(z\\mid x)\\,\\|\\,p(z)\\right)$ 鼓励近似后验接近先验。\n细化目标函数 # 证据下界(ELBO)写作:\n$$ \\mathcal{L}(x) = \\mathbb{E}_{q_\\phi(z|x)}\\![\\log p_\\theta(x|z)]- D_{KL}\\!\\bigl(q_\\phi(z|x)\\,\\|\\,p(z)\\bigr). $$最大化 $\\mathcal{L}(x)$ 等价于最小化第二项的 $D_{KL}$。取先验 $p(z)=\\mathcal{N}(0,I)$,并设 $q_\\phi(z|x)=\\mathcal{N}\\!\\bigl(\\mu_\\phi(x),\\operatorname{diag}(\\sigma^2_\\phi(x))\\bigr)$,即各维独立的高斯近似。\n对单个维度 $z_j$,记 $q(z_j|x)=\\mathcal{N}(\\mu,\\sigma^2)$、$p(z_j)=\\mathcal{N}(0,1)$(为简洁省略下标与参数),有\n$$ \\begin{aligned} D_{KL}\\bigl(q(z_j|x)\\,\\|\\,p(z_j)\\bigr) \u0026= \\int q(z_j|x)\\,\\log\\frac{q(z_j|x)}{p(z_j)}\\,dz_j \\\\ \u0026= \\int q(z_j|x)\\,\\log \\frac{\\tfrac{1}{\\sqrt{2\\pi}\\sigma}\\exp\\!\\left(-\\tfrac{(z_j-\\mu)^2}{2\\sigma^2}\\right)} {\\tfrac{1}{\\sqrt{2\\pi}}\\exp\\!\\left(-\\tfrac{z_j^2}{2}\\right)}\\,dz_j \\\\ \u0026= \\frac{1}{2}\\int q(z_j|x) \\left( z_j^2 - \\log\\sigma^2 - \\frac{(z_j-\\mu)^2}{\\sigma^2} \\right) dz_j. \\end{aligned} $$将期望分解为三项并分别计算:\n$$ \\int q(z_j|x)\\,z_j^2\\,dz_j=\\mathbb{E}[z_j^2] =\\operatorname{Var}(z_j)+\\mathbb{E}[z_j]^2=\\sigma^2+\\mu^2, $$$$ \\int q(z_j|x)\\,\\log\\sigma^2\\,dz_j=\\log\\sigma^2, $$$$ \\int q(z_j|x)\\,\\frac{(z_j-\\mu)^2}{\\sigma^2}\\,dz_j =\\frac{1}{\\sigma^2}\\,\\mathbb{E}\\!\\left[(z_j-\\mu)^2\\right]=1. $$故单维的 KL 为\n$$ D_{KL}\\bigl(q(z_j|x)\\,\\|\\,p(z_j)\\bigr) =\\frac{1}{2}\\left(\\mu^2+\\sigma^2-\\log\\sigma^2-1\\right). $$在 $J$ 维独立假设下,对所有维度求和可得\n$$ D_{KL}\\bigl(q_\\phi(z|x)\\,\\|\\,p(z)\\bigr) =\\frac{1}{2}\\sum_{j=1}^{J}\\left(\\mu_j^2+\\sigma_j^2-\\log\\sigma_j^2-1\\right). $$因此,最大化 $\\mathcal{L}(x)$ 等价于最小化上式;这也是 VAE 训练中常用的闭式 KL 项。\n最小重构代价 # 给定单个样本 $x$,重构项定义为\n$$ \\mathbb{E}_{z\\sim q_\\phi(z|x)}\\!\\left[\\log p_\\theta(x|z)\\right] = \\int \\log p_\\theta(x|z)\\, q_\\phi(z|x)\\,dz \\approx \\frac{1}{n}\\sum_{i=1}^{n} \\log p_\\theta\\!\\left(x\\,\\big|\\,z^{(i)}\\right), $$其中 $z^{(i)}\\sim q_\\phi(z|x)$ 是蒙特卡罗采样。\n常见的假设是 $p_\\theta(x|z)=\\mathcal N(\\mu_\\theta(z),\\sigma_\\theta^2(z)I)$。更简单地,也可固定协方差为常数 $c\u003e0$:\n$$ p_\\theta(x|z)=\\mathcal N\\!\\left(f_\\theta(z),\\,cI\\right). $$此时\n$$ \\log p_\\theta(x|z) = -\\frac{D}{2}\\log(2\\pi) - \\frac{D}{2}\\log c - \\frac{1}{2c}\\,\\|x-f_\\theta(z)\\|^2, $$因此最大化重构项等价于最小化均方误差的期望(忽略与 $\\theta,\\phi$ 无关的常数):\n$$ \\max\\,\\mathbb{E}_{q_\\phi}\\!\\left[\\log p_\\theta(x|z)\\right] \\ \\Longleftrightarrow\\ \\min\\,\\frac{1}{2c}\\,\\mathbb{E}_{q_\\phi}\\!\\left[\\|x-f_\\theta(z)\\|^2\\right] \\ \\approx\\ \\min\\,\\frac{1}{2c}\\cdot\\frac{1}{n}\\sum_{i=1}^{n}\\big\\|x-f_\\theta\\!\\left(z^{(i)}\\right)\\big\\|^2. $$将其与 KL 项合并(取先验 $p(z)=\\mathcal N(0,I)$,且 $q_\\phi(z|x)=\\mathcal N\\!\\big(\\mu(x),\\operatorname{diag}\\sigma^2(x)\\big)$),得到单样本的负 ELBO(待最小化):\n$$ \\min_{\\theta,\\phi}\\ \\left[ \\underbrace{\\frac{1}{2}\\sum_{j=1}^{J}\\!\\left(\\sigma_j^2+\\mu_j^2-\\log\\sigma_j^2-1\\right)}_{\\text{KL}\\big(q_\\phi(z|x)\\,\\|\\,\\mathcal N(0,I)\\big)} \\ +\\ \\underbrace{\\frac{1}{2c}\\cdot\\frac{1}{n}\\sum_{i=1}^{n}\\big\\|x-f_\\theta\\!\\left(z^{(i)}\\right)\\big\\|^2}_{-\\mathbb E_{q_\\phi}\\![\\log p_\\theta(x|z)]} \\right]. $$关于可导性:直接对 $z$ 采样使得对编码器参数 $\\phi$ 的梯度不可传递。为此使用重参数化技巧\n$$ z=\\mu_\\phi(x)+\\sigma_\\phi(x)\\odot\\varepsilon,\\qquad \\varepsilon\\sim\\mathcal N(0,I), $$从而梯度可经由 $\\varepsilon$ 的确定性变换传回编码器。\n重参数化技巧 # 假定 $q(z\\mid x)=\\mathcal N(\\mu,\\sigma^2)$,其中 $\\mu=\\mu(x)$、$\\sigma=\\sigma(x)\u003e0$。令 $\\epsilon\\sim\\mathcal N(0,1)$ 且与 $x$ 独立,定义重参数化\n$$ z=\\mu+\\sigma\\,\\epsilon. $$于是,只需先从 $\\epsilon\\sim\\mathcal N(0,1)$ 采样,再代入上式,就得到了等价于从 $q(z\\mid x)$ 采样的样本。\n证明(条件于 $x$):\n$$ \\mathbb E[z\\mid x]=\\mu+\\sigma\\,\\mathbb E[\\epsilon]=\\mu, $$$$ \\mathrm{Var}(z\\mid x)=\\mathbb E\\big[(z-\\mu)^2\\mid x\\big] =\\mathbb E\\big[(\\sigma\\epsilon)^2\\mid x\\big] =\\sigma^2\\,\\mathbb E[\\epsilon^2]=\\sigma^2. $$ 模型图 # %%{init: {'theme':'base','themeVariables':{ 'background':'#E8F3FF', 'primaryColor':'#FFFFFF', 'primaryBorderColor':'#3B82F6', 'primaryTextColor':'#0F172A', 'lineColor':'#2563EB', 'fontFamily':'Inter, Arial' }}}%% flowchart LR %% --- Classes \u0026 link style --- classDef node fill:#FFFFFF,stroke:#3B82F6,stroke-width:1.5px,rx:10px,ry:10px,color:#0F172A; classDef soft fill:#F0F7FF,stroke:#60A5FA,stroke-width:1.5px,rx:10px,ry:10px,color:#0F172A; linkStyle default stroke:#2563EB,stroke-width:2px; %% --- Encoder --- subgraph ENCGRP[\"编码器 $q_φ(z|x)$\"] X[\"训练样本 $x$\"] ENC[\"神经网络\"] MU[\"均值 $μ_φ(x)$\"] LOGVAR[\"对数方差 $log σ_φ²(x)$\"] X --\u003e ENC ENC --\u003e MU ENC --\u003e LOGVAR end MU -- \"重参数化\" --\u003e Z((\"隐变量 $z$\")) LOGVAR -- \"重参数化\" --\u003e Z %% --- Decoder --- subgraph DECGRP[\"解码器 $p_θ(x|z)$\"] Z --\u003e DEC[\"神经网络 $f_θ$\"] DEC --\u003e XHAT[\"重构 $x̂$\"] end %% --- Apply styles --- class X,ENC,MU,LOGVAR,DEC,XHAT node; class Z soft; style ENCGRP fill:#E6F0FF,stroke:#60A5FA,stroke-width:2px; style DECGRP fill:#E6F0FF,stroke:#60A5FA,stroke-width:2px; $$ \\text{ELBO}(\\theta,\\phi;x) = \\mathbb{E}_{q_\\phi(z\\mid x)}\\!\\big[\\log p_\\theta(x\\mid z)\\big]- \\mathrm{KL}\\!\\big(q_\\phi(z\\mid x)\\,\\|\\,p(z)\\big). $$最大化 ELBO 等价于最小化其相反数(常作为训练损失):\n$$ \\min_{\\theta,\\phi}\\;\\mathcal{L}(x)= \\underbrace{-\\,\\mathbb{E}_{q_\\phi(z\\mid x)}\\!\\big[\\log p_\\theta(x\\mid z)\\big]}_{\\text{重构损失}}+ \\underbrace{\\mathrm{KL}\\!\\big(q_\\phi(z\\mid x)\\,\\|\\,\\mathcal{N}(0,I)\\big)}_{\\text{先验对齐}}. $$当采用各向异性高斯后验近似 \\(q*\\phi(z\\mid x)=\\mathcal N\\!\\big(\\mu*\\phi(x),\\mathrm{diag}(\\sigma\\_\\phi^2(x))\\big)\\) 且先验 \\(p(z)=\\mathcal N(0,I)\\) 时,KL 的闭式解为\n$$ \\mathrm{KL}\\!\\big(q_\\phi(z\\mid x)\\,\\|\\,\\mathcal N(0,I)\\big) = \\frac{1}{2}\\sum_{j=1}^{J}\\Big(\\sigma_j^2+\\mu_j^2-\\log\\sigma_j^2-1\\Big), $$其中 $\\mu*j,\\sigma_j$ 为 $\\mu*\\phi(x),\\sigma*\\phi(x)$ 的分量。实践中常直接预测 $\\log\\sigma*\\phi^2(x)$。\n训练阶段(learning) # 编码器 $q\\_{\\phi}(z\\mid x)$ 输入样本 $x$,神经网络输出近似后验参数:\n$$ \\mu_\\phi(x),\\;\\log\\sigma_\\phi^2(x). $$由此得到 $\\sigma*\\phi(x)=\\exp\\!\\big(\\tfrac{1}{2}\\log\\sigma*\\phi^2(x)\\big)$。\n重参数化采样(可导) 从标准正态采样 $\\epsilon \\sim \\mathcal N(0, I)$,构造\n$$ z=\\mu_\\phi(x)+\\sigma_\\phi(x)\\odot \\epsilon, $$使得梯度可经由 $z$ 传回到 $\\phi$。\n解码器 $p\\_{\\theta}(x\\mid z)$ 将 $z$ 输入解码器 $f*\\theta$,得到重构 $\\hat x=f*\\theta(z)$,对应似然 $p*\\theta(x\\mid z)$。\n损失函数(最小化)\n$$ \\mathcal L(x) = -\\,\\mathbb E_{q_\\phi(z\\mid x)}[\\log p_\\theta(x\\mid z)] + \\mathrm{KL}\\!\\big(q_\\phi(z\\mid x)\\,\\|\\,\\mathcal N(0,I)\\big) $$ 若建模为伯努利似然 $p\\_\\theta(x\\mid z)=\\mathrm{Bernoulli}(\\hat x)$(图像已归一化至 $[0,1]$), 重构项对应 BCE。 若建模为各向同性高斯 $p\\_\\theta(x\\mid z)=\\mathcal N(\\hat x,\\sigma_x^2 I)$(常取固定 $\\sigma_x$), 重构项等价于 MSE(差一个常数因子)。 优化 对 $(\\phi,\\theta)$ 进行反向传播与参数更新,直至收敛。\n生成阶段(inference) # 直接从先验采样 $z \\sim \\mathcal N(0,I)$,送入解码器 $f*\\theta$ 得到新样本 $\\tilde x=f*\\theta(z)$。 CVAE(Variational Autoencoder,条件变分自编码器) # CVAE 结构示意图\n条件化思路:引入条件 $c$(如类别、文本特征),并将其注入编码器与解码器: 编码器条件化:$\\text{Encoder}(x,c)\\to(\\mu_z,\\sigma_z)$,即 $q_\\phi(z\\mid x,c)$; 解码器条件化:$\\text{Decoder}(z,c)\\to \\hat x$,即使用 $p_\\theta(x\\mid z,c)$。 目标函数(条件 ELBO): $$ \\mathcal L_{\\mathrm{CVAE}}(\\theta,\\phi) = \\mathbb{E}_{q_\\phi(z\\mid x,c)}\\!\\left[\\log p_\\theta(x\\mid z,c)\\right] - \\mathrm{KL}\\!\\left(q_\\phi(z\\mid x,c)\\,\\|\\,p(z\\mid c)\\right) $$ 实践中常取 $p(z\\mid c)=\\mathcal N(0,I)$(与 $c$ 无关)以简化。 效果:通过在生成阶段指定 $c$,即可按条件受控生成(例如手写体指定数字“7”)。 CVAE 数学推导 # VAE 训练流程图\nCVAE 训练流程图\n设生成模型为 $p_\\theta(x\\mid z,c)$,先验为 $p_\\theta(z\\mid c)$,变分分布为 $q_\\phi(z\\mid x,c)$。由\n$$ p(x,z\\mid c)=p(z\\mid x,c)\\,p(x\\mid c) $$可得恒等式\n$$ \\log p(x\\mid c)=\\log\\frac{p(x,z\\mid c)}{p(z\\mid x,c)}. $$两边对 $q_\\phi(z\\mid x,c)$ 积分,左边为\n$$ \\int q_\\phi(z\\mid x,c)\\,\\log p(x\\mid c)\\,\\mathrm{d}z=\\log p(x\\mid c), $$右边为\n$$ \\int q_\\phi(z\\mid x,c)\\,\\log\\frac{p(x,z\\mid c)}{p(z\\mid x,c)}\\,\\mathrm{d}z. $$在右侧加并减去同一项 $\\int q_\\phi(z\\mid x,c)\\,\\log q_\\phi(z\\mid x,c)\\,\\mathrm{d}z$,得到\n$$ \\begin{aligned} \\log p(x\\mid c) \u0026=\\int q_\\phi(z\\mid x,c)\\,\\log\\frac{p(x,z\\mid c)}{q_\\phi(z\\mid x,c)}\\,\\mathrm{d}z +\\int q_\\phi(z\\mid x,c)\\,\\log\\frac{q_\\phi(z\\mid x,c)}{p(z\\mid x,c)}\\,\\mathrm{d}z \\\\ \u0026=\\underbrace{\\int q_\\phi(z\\mid x,c)\\,\\log\\frac{p(x,z\\mid c)}{q_\\phi(z\\mid x,c)}\\,\\mathrm{d}z}_{\\text{ELBO}(\\theta,\\phi;x,c)} +\\underbrace{KL\\!\\left(q_\\phi(z\\mid x,c)\\,\\|\\,p(z\\mid x,c)\\right)}_{\\ge 0}. \\end{aligned} $$由 $KL\\ge 0$,得变分下界\n$$ \\log p(x\\mid c)\\;\\ge\\;\\mathcal{L}_{\\text{ELBO}}(\\theta,\\phi;x,c) =\\int q_\\phi(z\\mid x,c)\\,\\log\\frac{p(x,z\\mid c)}{q_\\phi(z\\mid x,c)}\\,\\mathrm{d}z. $$将联合分布分解为\n$$ p(x,z\\mid c)=p_\\theta(x\\mid z,c)\\,p_\\theta(z\\mid c), $$可将下界写成更常见的“重构项 − KL 项”形式:\n$$ \\begin{aligned} \\mathcal{L}_{\\text{ELBO}}(\\theta,\\phi;x,c) \u0026=\\int q_\\phi(z\\mid x,c)\\,\\Big[\\log p_\\theta(x\\mid z,c)+\\log p_\\theta(z\\mid c)-\\log q_\\phi(z\\mid x,c)\\Big]\\,\\mathrm{d}z \\\\ \u0026=\\mathbb{E}_{z\\sim q_\\phi(z\\mid x,c)}\\!\\big[\\log p_\\theta(x\\mid z,c)\\big] -KL\\!\\left(q_\\phi(z\\mid x,c)\\,\\|\\,p_\\theta(z\\mid c)\\right). \\end{aligned} $$其中第一项为“重构项”,第二项为“正则项”(KL)。由于对给定的 $x,c$,$\\log p(x\\mid c)$ 与变分参数 $\\phi$ 无关,因此最大化 $\\mathcal{L}_{\\text{ELBO}}$ 等价于最小化 $KL\\!\\left(q_\\phi(z\\mid x,c)\\,\\|\\,p(z\\mid x,c)\\right)$;在实践中通过同时优化 $\\theta,\\phi$ 来最大化该下界。\n极大似然 # 对比 VAE 与 CVAE 的目标函数(最大化 ELBO 以近似最大化对数似然):\nCVAE:\n$$ \\log p_\\theta(x\\mid c)\\;\\ge\\; \\mathbb{E}_{q_\\phi(z\\mid x,c)} \\!\\big[\\underbrace{\\log p_\\theta(x\\mid z,c)}_{\\text{重构项 ①}}\\big] \\;-\\; \\underbrace{\\mathrm{KL}\\!\\left(q_\\phi(z\\mid x,c)\\,\\|\\,p(z\\mid c)\\right)}_{\\text{正则项 ②}} $$VAE:\n$$ \\log p_\\theta(x)\\;\\ge\\; \\mathbb{E}_{q_\\phi(z\\mid x)} \\!\\big[\\underbrace{\\log p_\\theta(x\\mid z)}_{\\text{重构项 ①}}\\big] \\;-\\; \\underbrace{\\mathrm{KL}\\!\\left(q_\\phi(z\\mid x)\\,\\|\\,p(z)\\right)}_{\\text{正则项 ②}} $$说明:\n假设 $q_\\phi(z\\mid x,c)$ 与 $q_\\phi(z\\mid x)$ 为高斯分布;在 CVAE 中,推断网络 $q_\\phi(z\\mid x,c)$ 相比 VAE 额外接收条件 $c$ 作为输入。 生成分布 $p_\\theta(x\\mid z,c)$ 与 $p_\\theta(x\\mid z)$ 常按数据类型选取:连续数据常用高斯,二值数据常用伯努利等;在 CVAE 中,生成网络 $p_\\theta(x\\mid z,c)$ 同样额外接收条件 $c$。 CVAE 的先验常写作 $p(z\\mid c)$(也可在实践中使用标准正态 $p(z)$ 作为条件无关的简化)。 两种理解方式 # 理解 1:设 $p(z)=\\mathcal{N}(0,I)$,并取 $p(z\\mid c)=p(z)=\\mathcal{N}(0,I)$。\n理解 2:假设 $z$ 与 $c$ 独立,则有 $p(z\\mid c)=p(z)$;若再设 $p(z)=\\mathcal{N}(0,I)$,则 $p(z\\mid c)=\\mathcal{N}(0,I)$。\n结语 # 至此,我们从 AE 出发,走到能“可采样”的 VAE,再到可“按条件定向生成”的 CVAE:前者解决重构与连续潜在空间,后者让生成受控、更实用。训练中把握好重构—KL 的平衡(如 KL 退火、β-VAE),并将条件 $c$ 同时注入编码器与解码器,往往就能得到稳健的基线。\n参考资料 # 点击展开查看参考资料 VAE 变分自编码器原理解析 VAE 问题解答+CVAE 原理解析 VAE 论文地址 CVAE 论文地址 ","date":"2025年10月19日","externalUrl":null,"permalink":"/posts/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-vae--cvae/","section":"文章","summary":"","title":"深入浅出 VAE \u0026 CVAE","type":"posts"},{"content":"","date":"2025年10月19日","externalUrl":null,"permalink":"/tags/math/","section":"Tags","summary":"","title":"Math","type":"tags"},{"content":" 数学基础 # 本文旨在通俗易懂地介绍基本概念,部分表述可能不完全严谨。\n随机事件 # 概念:在试验中可能发生也可能不发生的事件称为随机事件。随机事件通常用大写字母 $A$、$B$、$C$ 等表示。\n$P(A)$ 表示事件 $A$ 发生的概率。\n概率运算法则 # 事件独立\n定义:如果两个事件的发生互不影响,则称这两个事件独立。\n公式:$P(A, B) = P(A)P(B)$ 其中 $P(A, B)$ 表示事件 $A$ 和事件 $B$ 同时发生的概率。\n条件概率\n定义:在事件 $B$ 发生的条件下,事件 $A$ 发生的概率称为条件概率,记作 $P(A|B)$。\n公式:$P(A|B) = \\frac{P(A, B)}{P(B)}$\n全概率公式\n定义:如果事件 $B_1, B_2, \\dots, B_n$ 是样本空间 $S$ 的一个划分(即它们互不相交,且并集为 $S$),则有:\n$$P(A) = \\sum_{i=1}^n P(A|B_i)P(B_i)$$也就是说,事件 $A$ 的概率等于其在各个互斥事件 $B_i$ 上发生的条件概率与 $B_i$ 本身概率乘积之和。\n贝叶斯公式\n定义:如果事件 $B_1, B_2, \\dots, B_n$ 是样本空间 $S$ 的一个划分,则有:\n$$P(B_i|A) = \\frac{P(A|B_i)P(B_i)}{\\sum_{j=1}^n P(A|B_j)P(B_j)}$$也就是说,贝叶斯公式计算的是在事件 $A$ 已经发生的条件下,它是由某个特定原因(事件 $B_i$)引起的概率。\n随机变量 # 概念:随机变量是将随机事件的结果映射到一个数值的函数。\n随机变量通常用大写字母 $X$、$Y$、$Z$ 等表示。\n试验结果的全体称为样本空间,记作 $S$。\n离散随机变量和连续随机变量 # 离散随机变量\n定义:如果随机变量 $X$ 的取值是有限或可数的,则称 $X$ 为离散随机变量。\n连续随机变量\n定义:如果随机变量 $X$ 的取值在一个或多个区间内,是不可数的,则称 $X$ 为连续随机变量。\n对随机变量 $X=a$ 的概率,记作 $P(X=a)$,在不引起混淆的情况下,有时也简写为 $P(a)$。\n多维随机变量 # 设随机变量 $X$、$Y$ 为离散型,求边缘概率的公式为:\n$$ P(Y=y) = \\sum_{i=1}^n P(X=x_i, Y=y) = \\sum_{x} P(X=x, Y=y) $$ 概率分布 # 概率分布:指用于表述随机变量取值的概率规律。\n离散随机变量常用分布:伯努利分布 (Bernoulli Distribution) 即一次试验中,随机变量 $X$ 只有两种可能的取值(例如成功/失败)。\n$X$ $0$ $1$ $P(X=k)$ $1-p$ $p$ 其概率质量函数为:\n$$ P(X=k) = p^k(1-p)^{1-k}, \\quad k \\in \\{0, 1\\} $$该式子表示,当结果为 1(成功)的概率为 $p$ 时,随机变量取值为 $k$ 的概率。\n连续随机变量常用分布:正态分布(高斯分布,Gaussian Distribution) 一维正态分布的概率密度函数:\n$$ f(x) = \\frac{1}{\\sqrt{2\\pi\\sigma^2}} e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}} $$图像为:\n图像的对称轴位于 $x=\\mu$ 处,此处也是概率密度的峰值点,$\\mu$ 是均值。 当随机变量 $X$ 服从标准正态分布时,写作 $X \\sim N(0,1)$,此时均值为 0,方差为 1。\n期望、方差、协方差 # 期望 # 数学期望(均值):用于衡量随机变量取值的平均水平。\n设随机变量 $X$ 的概率分布为 $P(X=x_i) = p_i, i=1, 2, \\dots, n$。\n离散型:$E(X) = \\sum_{i=1}^n x_i p_i$ 连续型:$E(X) = \\int_{-\\infty}^{\\infty} x f(x) dx$ 随机变量函数的数学期望:\n离散型:$E(g(X)) = \\sum_{i=1}^n g(x_i) p_i$ 连续型:$E(g(X)) = \\int_{-\\infty}^{\\infty} g(x) f(x) dx$ 其中 $g(X)$ 是随机变量 $X$ 的函数。\n数学期望的性质:\n常数的期望是其本身,即 $E(C)=C$。 若 $C$ 是常数,则 $E(CX) = C \\cdot E(X)$。 $E(X_1+X_2) = E(X_1) + E(X_2)$。 如果 $X$ 和 $Y$ 相互独立,则 $E(XY) = E(X)E(Y)$。 数学期望在论文中的常用表达: 一般地,数学期望可能会写成这样:\n$$ E_{X \\sim P_{data}}[X] $$这表示我们所求的数学期望的随机变量 $X$ 服从概率分布 $P_{data}$。\n还有一些会写成这样:\n$$ E_{(X,Y) \\sim P}[X] $$这表示对随机变量 $X$ 关于 $(X,Y)$ 的联合概率分布 $P$ 求期望。它等价于求 $X$ 的边缘期望,即 $E[X]$。\n方差 # 方差:用于衡量随机变量的离散程度或波动性。\n$$ D(X) = E[(X - E(X))^2] = E(X^2) - [E(X)]^2 $$其中 $E(X^2)$ 表示随机变量 $X$ 的平方的数学期望。\n方差的性质:\n常数的方差为 0,即 $D(C) = 0$。 设 $C$ 是常数,则 $D(CX) = C^2 D(X)$。 设 $X$、$Y$ 为随机变量,则 $$ D(X \\pm Y) = D(X) + D(Y) \\pm 2Cov(X,Y) $$ 当 $X$、$Y$ 相互独立时, $Cov(X,Y)=0$,有 $$ D(X \\pm Y) = D(X) + D(Y) $$ 协方差 # 协方差:反映随机变量之间的线性关系强度和方向。\n假设有随机变量 $X$、$Y$,其协方差为:\n$$ Cov(X,Y) = E\\{[X - E(X)][Y - E(Y)]\\} $$协方差的性质:\n任意随机变量与常数的协方差为 0。 设 $C$ 是常数,则 $Cov(CX,Y) = C \\cdot Cov(X,Y)$。 设 $X$、$Y$、$Z$ 为随机变量,则 $Cov(X \\pm Y, Z) = Cov(X, Z) \\pm Cov(Y, Z)$。 极大似然估计 # 概念:一种根据样本数据来估计模型参数的方法,其目标是找到能使观测样本出现概率最大的参数值。\n具体步骤:\n写出似然函数。 对似然函数取对数,并整理得到对数似然函数。 对对数似然函数关于参数求导数,并令其为零。 求解该方程组,得到参数的估计值。 似然函数:对于单个观测值 $x$,其似然函数 $L(\\theta|x)$ 在形式上等于概率(密度)函数。离散型时为 $P(x|\\theta)$,连续型时为 $f(x|\\theta)$。其中 $\\theta$ 表示所要求的概率分布的参数。\n极大似然估计通常假设样本是独立同分布的(i.i.d.)。\n信息熵 # 概念:描述一个随机变量取值的不确定性或信息量的大小。\n$$ H(X)=-\\sum_{i=1}^n P(x_i) \\log_{2} P(x_i) = -E[\\log_{2}P(X)] $$ KL 散度(相对熵) # 概念:一种用于衡量两个概率分布之间差异的非对称性指标。\n$$ KL(q||p)=\\sum_{i=1}^n q(x_i) \\log_{2} \\frac{q(x_i)}{p(x_i)}=\\int q(x) \\log_{2} \\frac{q(x)}{p(x)} dx $$性质:\n非负性:$KL(q||p) \\geq 0$。当且仅当两个概率分布完全相同时,KL 散度为 0。 非对称性:通常 $KL(q||p) \\neq KL(p||q)$。 参考资料 # 点击展开查看参考资料 数学基础——生成模型必备知识 ","date":"2025年10月19日","externalUrl":null,"permalink":"/posts/%E4%B8%80%E4%BA%9B%E6%95%B0%E5%AD%A6%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5/","section":"文章","summary":"","title":"一些数学基础概念","type":"posts"},{"content":"","date":"2025年10月7日","externalUrl":null,"permalink":"/tags/lerobot/","section":"Tags","summary":"","title":"LeRobot","type":"tags"},{"content":" 0.写在开头 # LeRobot 仓库经常性变动,请务必参考官方教程和仓库代码;本文撰写时版本为 a6ff3cfe(2025-10-14)。 本文记录LeRobot SO-101机械臂实践,包括从硬件连接、环境搭建到基础的校准、采集数据、训练(基于 ACT)、推理(基于 ACT),具体的端口、路径等仅作参考,以实际环境为准。\n硬件连接 # 成品说明书 WOWROBO SO-ARM101 双臂使用说明书(成品版本专用)\n连接与安装 请按以下说明连接设备电源:主控需连接 5V/6A 电源,随从臂连接 12V/8A 电源。 相机组件的安装方法请参考以下视频,在 1 小时 6 分 32 秒(1:06:32) 处开始演示: 链接:https://www.bilibili.com/video/BV13bLyzKES8/\n操作指南阅读建议 本产品出厂前已完成舵机 ID、波特率、磁编码器中位设置及遥操作功能测试。建议在完全熟悉产品前不要手动更改舵机参数。 请根据以下文档完成环境配置和端口设置后,从步骤 “Calibrate” 开始操作。 链接:https://huggingface.co/docs/lerobot/so101\n重要事项 错误的标定方向可能导致舵机朝错误的限位方向运行,进而产生过载,造成舵机损坏,尤其是从臂的 3 号舵机。因此,在首次进行遥操作时,请仔细观察每个舵机的运动方向是否与预期一致。如发现方向异常,请立即停止并重新运行标定流程。 这款机械臂的建议最大负载重量为 400 克,请勿超出这个重量,超载可能会损坏舵机。 因操作不当导致的舵机损坏(如过载、烧毁)不在保修范围内,敬请知悉。\n由于直接购买的成品LeRobot SO-101,所以无需自己组装零件和设置舵机。 机械臂连接电源和电脑 相机尽量直连电脑,机械臂两条信号线可以通过集线器连接电脑。 安装摄像头 摄像头连接教程 环境搭建 # 安装好miniforge和配置conda设置后,参考官方-安装文档安装lerobot环境。\n创建虚拟环境:conda create -y -n lerobot python=3.10 激活虚拟环境:conda activate lerobot 安装ffmpeg:conda install ffmpeg -c conda-forge 更新pip:python -m pip install -U pip setuptools wheel 拉取lerobot仓库并进入:git clone https://github.com/huggingface/lerobot.git \u0026amp;\u0026amp; cd lerobot 安装lerobot:pip install 'lerobot[all]' 或者基础的:pip install 'lerobot[feetech]' 更新到最新 lerobot 库 暂存本地修改:git stash 拉取最新代码:git pull 恢复本地修改:git stash pop 如果有冲突,需要手动解决 校准 # 由于我们购买的成品LeRobot SO-101,所以无需自己进行舵机 ID、波特率、磁编码器中位设置,直接进行校准。\n参考视频: LeRobot 具身智能机械臂实操入门课程-01:软件环境配置和双臂标定 官方-校准示例视频 找到USB端口: 官方-找到机械臂的 USB 端口 校准: 官方-校准 Seeed Studio 教程-校准机械臂 连接机械臂电源和电脑后,激活lerobot环境,进入lerobot仓库。\n找到两个机械臂的端口:\n通过运行lerobot-find-port,并断开 找到两个机械臂的端口: 主臂(LeaderArm):/dev/ttyACM0 从臂(FollowerArm):/dev/ttyACM1 设置串口设备权限:sudo chmod 666 /dev/ttyACM*\n如果不想每次连接设备都设置权限,可以把自己加入对应设备组: 查看设备组: BASH不换行 [xiadengma@archlinux ~]$ ls -l /dev/ttyACM* crw-rw---- 1 root uucp 166, 0 10月16日 13:15 /dev/ttyACM0 crw-rw---- 1 root uucp 166, 1 10月16日 13:15 /dev/ttyACM1 把自己加入对应设备组(根据ls -l /dev/ttyACM*输出修改),并验证是否加入成功: 加入设备组 uucp 并刷新环境 BASH不换行 sudo usermod -aG uucp \u0026#34;$USER\u0026#34; \u0026amp;\u0026amp; exec su -l \u0026#34;$USER\u0026#34; 验证是否加入成功 BASH不换行 id结果中组有对应设备组即可。 设置串口设备别名:\n如果不想每次连接设备都需要运行lerobot-find-port来查找端口,可以设置别名:\n找到设备的唯一属性(/dev/ttyACM0请自行修改):\nBASH不换行 udevadm info --query=property --name=/dev/ttyACM0 | sed -n \u0026#39;s/^ID_SERIAL_SHORT=\\(.*\\)$/ATTRS{serial}==\u0026#34;\\1\u0026#34;/p\u0026#39;输出示例:\nPLAINTEXT不换行 ATTRS{serial}==\u0026#34;5A7C118736\u0026#34; 创建udev规则文件:\nBASH不换行 sudo touch /etc/udev/rules.d/99-my-robot.rules写入下面内容(serial部分内容请根据实际修改):\nPLAINTEXT不换行 # Rule for the Leader Arm SUBSYSTEM==\u0026#34;tty\u0026#34;, ATTRS{serial}==\u0026#34;5A7C118736\u0026#34;, SYMLINK+=\u0026#34;leader_arm\u0026#34;, MODE=\u0026#34;0666\u0026#34; # Rule for the Follower Arm SUBSYSTEM==\u0026#34;tty\u0026#34;, ATTRS{serial}==\u0026#34;5A7C118880\u0026#34;, SYMLINK+=\u0026#34;follower_arm\u0026#34;, MODE=\u0026#34;0666\u0026#34;简单解释一下规则:\nSUBSYSTEM==\u0026quot;tty\u0026quot;: 表示规则应用于串行设备。 ATTRS{serial}==\u0026quot;...\u0026quot;: 匹配我们找到的唯一设备序列号。 SYMLINK+=\u0026quot;leader_arm\u0026quot;: 创建一个名为 /dev/leader_arm 的符号链接(别名)。 MODE=\u0026quot;0666\u0026quot;: 这一步将设备文件的权限设置为所有人可读可写,可以避免很多 \u0026ldquo;Permission denied\u0026rdquo; 的错误。 重新加载 udev规则并触发\nBASH不换行 sudo udevadm control --reload-rules \u0026amp;\u0026amp; sudo udevadm trigger 验证是否设置成功 拔插一下你的设备,然后运行 ls -l /dev/leader_arm /dev/follower_arm 。\nBASH不换行 [xiadengma@archlinux ~]$ ls -l /dev/leader_arm /dev/follower_arm lrwxrwxrwx 1 root root 7 Oct 26 15:30 /dev/leader_arm -\u0026gt; ttyACM0 lrwxrwxrwx 1 root root 7 Oct 26 15:30 /dev/follower_arm -\u0026gt; ttyACM1 执行校准\n注意:下面命令都指定了校准文件保存路径,默认路径在~/.cache/huggingface/lerobot/calibration/,如果不指定,可以去掉下面所有命令的--robot.calibration_dir和--teleop.calibration_dir。 校准过程:运行校准程序,然后将机器人移动到中间位,按下Enter后,再将每个关节在其完整的运动范围内移动。 校准主臂: 主臂名称(可自定义):my_leader_arm 主臂校准文件保存路径(可自定义):./data/calibration BASH不换行 lerobot-calibrate \\ --robot.type=so101_leader \\ --robot.port=/dev/leader_arm \\ --robot.id=my_leader_arm \\ --robot.calibration_dir=./data/calibration 校准从臂: 从臂名称(可自定义):my_follower_arm 从臂校准文件保存路径(可自定义):./data/calibration BASH不换行 lerobot-calibrate \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.id=my_follower_arm \\ --robot.calibration_dir=./data/calibration 标定数据解读\nJSON不换行 { \u0026#34;shoulder_pan\u0026#34;: { #肩部旋转关节 \u0026#34;id\u0026#34;: 1, # 电机的唯一标识符,用于在总线通信时精准定位和控制特定电机 \u0026#34;drive_mode\u0026#34;: 0, # 电机的驱动模式,取值为 0 表示特定的驱动模式,不同驱动模式会影响电机的运动特性与控制方式 \u0026#34;homing_offset\u0026#34;: 56, # 归位偏移量,指电机从物理零点位置到校准零点位置的偏移量。此参数能保证电机在每次启动时都能回到准确的零点位置,从而提升运动精度 \u0026#34;range_min\u0026#34;: 829, #电机运动范围的最小值和最大值,以数值形式呈现。这两个参数限定了电机的运动边界,避免因超出范围而导致硬件损坏或者运动异常 \u0026#34;range_max\u0026#34;: 2866 }, \u0026#34;shoulder_lift\u0026#34;: { #肩部升降关节 \u0026#34;id\u0026#34;: 2, \u0026#34;drive_mode\u0026#34;: 0, \u0026#34;homing_offset\u0026#34;: 463, \u0026#34;range_min\u0026#34;: 836, \u0026#34;range_max\u0026#34;: 3136 }, \u0026#34;elbow_flex\u0026#34;: { #肘部弯曲关节 \u0026#34;id\u0026#34;: 3, \u0026#34;drive_mode\u0026#34;: 0, \u0026#34;homing_offset\u0026#34;: -100, \u0026#34;range_min\u0026#34;: 894, \u0026#34;range_max\u0026#34;: 3100 }, \u0026#34;wrist_flex\u0026#34;: { #腕部弯曲关节 \u0026#34;id\u0026#34;: 4, \u0026#34;drive_mode\u0026#34;: 0, \u0026#34;homing_offset\u0026#34;: -582, \u0026#34;range_min\u0026#34;: 928, \u0026#34;range_max\u0026#34;: 3213 }, \u0026#34;wrist_roll\u0026#34;: { #腕部旋转关节 \u0026#34;id\u0026#34;: 5, \u0026#34;drive_mode\u0026#34;: 0, \u0026#34;homing_offset\u0026#34;: -650, \u0026#34;range_min\u0026#34;: 140, \u0026#34;range_max\u0026#34;: 3955 }, \u0026#34;gripper\u0026#34;: { #夹爪关节 \u0026#34;id\u0026#34;: 6, \u0026#34;drive_mode\u0026#34;: 0, \u0026#34;homing_offset\u0026#34;: -1229, \u0026#34;range_min\u0026#34;: 2044, \u0026#34;range_max\u0026#34;: 3461 } } 遥操作/示教 # 官方-模仿学习教程 无摄像头遥操作 # 执行遥操作: BASH不换行 lerobot-teleoperate \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.id=my_follower_arm \\ --robot.calibration_dir=./data/calibration \\ --teleop.type=so101_leader \\ --teleop.port=/dev/leader_arm \\ --teleop.id=my_leader_arm \\ --teleop.calibration_dir=./data/calibration 运行后摆动主臂,从臂会跟随运动,按Ctrl+C退出 安装摄像头 # 如果在硬件连接中安装了摄像头,可以进行摄像头遥操作。\n连接摄像头和电脑(最好有两个,一个摄像头拍操作区域,一个摄像头安装在机械臂腕部)\n查看摄像头索引和输出\nBASH不换行 lerobot-find-cameras opencv --output-dir ./data/cameras_images结果输出如下:\nBASH不换行 --- Detected Cameras --- # 检测到的相机列表 Camera #0: # 相机 0 Name: OpenCV Camera @ /dev/video0 # 相机名称及设备路径 Type: OpenCV # 相机类型:OpenCV Id: /dev/video0 # 相机 ID,即设备路径 Backend api: V4L2 # 使用的后端 API:V4L2 Default stream profile: # 默认流配置 Format: 0.0 # 格式:0.0(默认设置) Width: 640 # 图像宽度:640 像素 Height: 480 # 图像高度:480 像素 Fps: 30.0 # 每秒帧数:30 帧 -------------------- Camera #1: Name: OpenCV Camera @ /dev/video2 Type: OpenCV Id: /dev/video2 Backend api: V4L2 Default stream profile: Format: 0.0 Width: 640 Height: 480 Fps: 30.0 -------------------- Camera #2: Name: OpenCV Camera @ /dev/video6 Type: OpenCV Id: /dev/video6 Backend api: V4L2 Default stream profile: Format: 0.0 Width: 640 Height: 480 Fps: 30.0 -------------------- Camera #3: Name: OpenCV Camera @ /dev/video8 Type: OpenCV Id: /dev/video8 Backend api: V4L2 Default stream profile: Format: 0.0 Width: 640 Height: 480 Fps: 30.0 -------------------- Finalizing image saving... Image capture finished. Images saved to data/cameras_images运行完成后会在.data/cameras_images目录下保存图片\n根据输出记录摄像头:\n手腕左摄像头: 名称:wrist_left 索引:2 宽度:640 高度:480 帧率:30 机械臂正前方摄像头: 名称:front_rgb 索引:8 宽度:640 高度:480 帧率:30 使用摄像头进行遥操作 # 执行遥操作: BASH不换行 lerobot-teleoperate \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.id=my_follower_arm \\ --robot.cameras=\u0026#34;{ wrist_left: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, front_rgb: {type: opencv, index_or_path: 8, width: 640, height: 480, fps: 30}}\u0026#34; \\ --teleop.type=so101_leader \\ --teleop.port=/dev/leader_arm \\ --teleop.id=my_leader_arm \\ --display_data=true 运行后会打开rerun窗口,可以查看机械臂和摄像头画面 摆动主臂,从臂会跟随运动,按Ctrl+C退出 ReRun Web 可视化 在src/lerobot下创建extra文件夹,并添加web_visualization_utils.py和lerobot_teleoperate_web.py两个文件,内容如下:\nweb_visualization_utils.py:\nPYTHON不换行 # Copyright 2024 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. \u0026#34;\u0026#34;\u0026#34;Web-based visualization utilities using Rerun web viewer.\u0026#34;\u0026#34;\u0026#34; import numbers import os from typing import Any import numpy as np import rerun as rr from lerobot.utils.constants import OBS_PREFIX, OBS_STR def init_rerun_web( session_name: str = \u0026#34;lerobot_control_loop\u0026#34;, port: int = 9090, open_browser: bool = True, memory_limit: str = \u0026#34;10%\u0026#34;, ) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; 初始化 Rerun SDK 用于 Web 浏览器可视化控制循环。 Initializes the Rerun SDK for visualizing the control loop in a web browser. 参数 Args: session_name: Rerun 会话名称 / Name of the Rerun session port: Web 服务器端口 / Web server port (default: 9090) open_browser: 是否自动打开浏览器 / Whether to automatically open browser (default: True) memory_limit: 内存限制 / Memory limit for Rerun (default: \u0026#34;10%\u0026#34;) 使用示例 Example: ```python from lerobot.extra.web_visualization_utils import init_rerun_web # 在浏览器中启动可视化 / Start visualization in browser init_rerun_web(session_name=\u0026#34;my_teleoperation\u0026#34;, port=9090) ``` 访问 Access: 在浏览器中打开 / Open in browser: http://localhost:9090 \u0026#34;\u0026#34;\u0026#34; batch_size = os.getenv(\u0026#34;RERUN_FLUSH_NUM_BYTES\u0026#34;, \u0026#34;8000\u0026#34;) os.environ[\u0026#34;RERUN_FLUSH_NUM_BYTES\u0026#34;] = batch_size rr.init(session_name) print(\u0026#34;🌐 启动 Rerun Web Viewer / Starting Rerun Web Viewer\u0026#34;) print(f\u0026#34;📍 访问地址 / Access URL: http://localhost:{port}\u0026#34;) print(f\u0026#34;💾 内存限制 / Memory limit: {memory_limit}\u0026#34;) rr.serve( open_browser=open_browser, web_port=port, server_memory_limit=memory_limit, ) def init_rerun_connect( addr: str = \u0026#34;127.0.0.1:9876\u0026#34;, session_name: str = \u0026#34;lerobot_control_loop\u0026#34;, ) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; 连接到远程 Rerun Viewer (适用于远程服务器)。 Connect to a remote Rerun Viewer (useful for remote servers). 参数 Args: addr: 远程 Rerun Viewer 地址 / Remote Rerun Viewer address (default: \u0026#34;127.0.0.1:9876\u0026#34;) session_name: Rerun 会话名称 / Name of the Rerun session 使用示例 Example: ```python from lerobot.extra.web_visualization_utils import init_rerun_connect # 连接到远程 Rerun Viewer / Connect to remote Rerun Viewer # 首先在另一个终端运行: rerun --port 9876 # First run in another terminal: rerun --port 9876 init_rerun_connect(addr=\u0026#34;127.0.0.1:9876\u0026#34;) ``` \u0026#34;\u0026#34;\u0026#34; batch_size = os.getenv(\u0026#34;RERUN_FLUSH_NUM_BYTES\u0026#34;, \u0026#34;8000\u0026#34;) os.environ[\u0026#34;RERUN_FLUSH_NUM_BYTES\u0026#34;] = batch_size rr.init(session_name) print(\u0026#34;🔗 连接到远程 Rerun Viewer / Connecting to remote Rerun Viewer\u0026#34;) print(f\u0026#34;📍 地址 / Address: {addr}\u0026#34;) rr.connect_tcp(addr=addr) def _is_scalar(x): return isinstance(x, (float | numbers.Real | np.integer | np.floating)) or ( isinstance(x, np.ndarray) and x.ndim == 0 ) def log_rerun_data( observation: dict[str, Any] | None = None, action: dict[str, Any] | None = None, ) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; 将观测和动作数据记录到 Rerun 用于实时可视化。 Logs observation and action data to Rerun for real-time visualization. This function iterates through the provided observation and action dictionaries and sends their contents to the Rerun viewer. It handles different data types appropriately: - Scalar values (floats, ints) are logged as `rr.Scalar`. - 3D NumPy arrays that resemble images (e.g., with 1, 3, or 4 channels first) are transposed from CHW to HWC format and logged as `rr.Image`. - 1D NumPy arrays are logged as a series of individual scalars, with each element indexed. - Other multi-dimensional arrays are flattened and logged as individual scalars. Keys are automatically namespaced with \u0026#34;observation.\u0026#34; or \u0026#34;action.\u0026#34; if not already present. 参数 Args: observation: 包含观测数据的可选字典 / An optional dictionary containing observation data to log. action: 包含动作数据的可选字典 / An optional dictionary containing action data to log. \u0026#34;\u0026#34;\u0026#34; if observation: for k, v in observation.items(): if v is None: continue key = k if str(k).startswith(OBS_PREFIX) else f\u0026#34;{OBS_STR}.{k}\u0026#34; if _is_scalar(v): rr.log(key, rr.Scalar(float(v))) elif isinstance(v, np.ndarray): arr = v # Convert CHW -\u0026gt; HWC when needed if arr.ndim == 3 and arr.shape[0] in (1, 3, 4) and arr.shape[-1] not in (1, 3, 4): arr = np.transpose(arr, (1, 2, 0)) if arr.ndim == 1: for i, vi in enumerate(arr): rr.log(f\u0026#34;{key}_{i}\u0026#34;, rr.Scalar(float(vi))) else: rr.log(key, rr.Image(arr), static=True) if action: for k, v in action.items(): if v is None: continue key = k if str(k).startswith(\u0026#34;action.\u0026#34;) else f\u0026#34;action.{k}\u0026#34; if _is_scalar(v): rr.log(key, rr.Scalar(float(v))) elif isinstance(v, np.ndarray): if v.ndim == 1: for i, vi in enumerate(v): rr.log(f\u0026#34;{key}_{i}\u0026#34;, rr.Scalar(float(vi))) else: # Fall back to flattening higher-dimensional arrays flat = v.flatten() for i, vi in enumerate(flat): rr.log(f\u0026#34;{key}_{i}\u0026#34;, rr.Scalar(float(vi))) lerobot_teleoperate_web.py:\nPYTHON不换行 #!/usr/bin/env python3 # Copyright 2024 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. \u0026#34;\u0026#34;\u0026#34; 通过遥操作控制机器人的脚本 (使用 Web 可视化)。 Simple script to control a robot from teleoperation (with Web visualization). \u0026#34;\u0026#34;\u0026#34; import logging import time from dataclasses import asdict, dataclass from pprint import pformat import rerun as rr from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig from lerobot.cameras.realsense.configuration_realsense import RealSenseCameraConfig from lerobot.configs import parser from lerobot.processor import ( RobotAction, RobotObservation, RobotProcessorPipeline, make_default_processors, ) from lerobot.robots import ( Robot, RobotConfig, bi_so100_follower, hope_jr, koch_follower, make_robot_from_config, so100_follower, so101_follower, ) from lerobot.teleoperators import ( Teleoperator, TeleoperatorConfig, bi_so100_leader, gamepad, homunculus, koch_leader, make_teleoperator_from_config, so100_leader, so101_leader, ) from lerobot.utils.robot_utils import busy_wait from lerobot.utils.utils import init_logging, move_cursor_up from .web_visualization_utils import init_rerun_web, log_rerun_data @dataclass class TeleoperateWebConfig: teleop: TeleoperatorConfig robot: RobotConfig # Limit the maximum frames per second. # 限制最大帧率 fps: int = 60 teleop_time_s: float | None = None # Display all cameras on screen # 在屏幕上显示所有摄像头 display_data: bool = False # Web viewer port # Web 查看器端口 web_port: int = 9090 # Auto open browser # 自动打开浏览器 open_browser: bool = True def teleop_loop( teleop: Teleoperator, robot: Robot, fps: int, teleop_action_processor: RobotProcessorPipeline[tuple[RobotAction, RobotObservation], RobotAction], robot_action_processor: RobotProcessorPipeline[tuple[RobotAction, RobotObservation], RobotAction], robot_observation_processor: RobotProcessorPipeline[RobotObservation, RobotObservation], display_data: bool = False, duration: float | None = None, ): \u0026#34;\u0026#34;\u0026#34; This function continuously reads actions from a teleoperation device, processes them through optional pipelines, sends them to a robot, and optionally displays the robot\u0026#39;s state. The loop runs at a specified frequency until a set duration is reached or it is manually interrupted. Args: teleop: The teleoperator device instance providing control actions. robot: The robot instance being controlled. fps: The target frequency for the control loop in frames per second. display_data: If True, fetches robot observations and displays them in the console and Rerun. duration: The maximum duration of the teleoperation loop in seconds. If None, the loop runs indefinitely. teleop_action_processor: An optional pipeline to process raw actions from the teleoperator. robot_action_processor: An optional pipeline to process actions before they are sent to the robot. robot_observation_processor: An optional pipeline to process raw observations from the robot. \u0026#34;\u0026#34;\u0026#34; display_len = max(len(key) for key in robot.action_features) start = time.perf_counter() while True: loop_start = time.perf_counter() # Get robot observation # 获取机器人观测 obs = robot.get_observation() # Get teleop action # 获取遥操作动作 raw_action = teleop.get_action() # Process teleop action through pipeline # 通过处理流水线处理遥操作动作 teleop_action = teleop_action_processor((raw_action, obs)) # Process action for robot through pipeline # 通过处理流水线生成发送给机器人的动作 robot_action_to_send = robot_action_processor((teleop_action, obs)) # Send processed action to robot # 发送处理后的动作给机器人 _ = robot.send_action(robot_action_to_send) if display_data: # Process robot observation through pipeline # 通过处理流水线处理机器人观测 obs_transition = robot_observation_processor(obs) log_rerun_data( observation=obs_transition, action=teleop_action, ) print(\u0026#34;\\n\u0026#34; + \u0026#34;-\u0026#34; * (display_len + 10)) # 分隔线 print(f\u0026#34;{\u0026#39;NAME\u0026#39;:\u0026lt;{display_len}} | {\u0026#39;NORM\u0026#39;:\u0026gt;7}\u0026#34;) # 标题:名称与归一化值 # Display the final robot action that was sent # 显示已发送给机器人的最终动作 for motor, value in robot_action_to_send.items(): print(f\u0026#34;{motor:\u0026lt;{display_len}} | {value:\u0026gt;7.2f}\u0026#34;) # 每个电机的动作值(保留 2 位小数) move_cursor_up(len(robot_action_to_send) + 5) # 上移光标以覆盖刷新 dt_s = time.perf_counter() - loop_start busy_wait(1 / fps - dt_s) # 忙等待以维持目标帧率(若剩余时间为正) loop_s = time.perf_counter() - loop_start print(f\u0026#34;\\ntime: {loop_s * 1e3:.2f}ms ({1 / loop_s:.0f} Hz)\u0026#34;) # 打印单次循环耗时与频率 if duration is not None and time.perf_counter() - start \u0026gt;= duration: return @parser.wrap() def teleoperate_web(cfg: TeleoperateWebConfig): init_logging() # 初始化日志 logging.info(pformat(asdict(cfg))) # 记录当前配置 if cfg.display_data: # 使用 Web 可视化 init_rerun_web( session_name=\u0026#34;teleoperation_web\u0026#34;, port=cfg.web_port, open_browser=cfg.open_browser, ) teleop = make_teleoperator_from_config(cfg.teleop) robot = make_robot_from_config(cfg.robot) teleop_action_processor, robot_action_processor, robot_observation_processor = make_default_processors() teleop.connect() # 连接遥操作设备 robot.connect() # 连接机器人 try: teleop_loop( teleop=teleop, robot=robot, fps=cfg.fps, display_data=cfg.display_data, duration=cfg.teleop_time_s, teleop_action_processor=teleop_action_processor, robot_action_processor=robot_action_processor, robot_observation_processor=robot_observation_processor, ) except KeyboardInterrupt: pass # 捕获 Ctrl+C 中断,正常退出 finally: if cfg.display_data: rr.rerun_shutdown() # 关闭 Rerun 会话 teleop.disconnect() # 断开遥操作设备 robot.disconnect() # 断开机器人 def main(): teleoperate_web() if __name__ == \u0026#34;__main__\u0026#34;: main() 执行遥操作:\nBASH不换行 python -m lerobot.extra.lerobot_teleoperate_web \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.id=my_follower_arm \\ --robot.calibration_dir=./data/calibration \\ --robot.cameras=\u0026#34;{ wrist_left: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, front_rgb: {type: opencv, index_or_path: 8, width: 640, height: 480, fps: 30}}\u0026#34; \\ --teleop.type=so101_leader \\ --teleop.port=/dev/leader_arm \\ --teleop.id=my_leader_arm \\ --teleop.calibration_dir=./data/calibration \\ --display_data=true \\ --web_port=9090 运行后默认情况下会在浏览器打开http://localhost:9090/?url=ws://localhost:9877,可以查看机械臂和摄像头画面\n摆动主臂,从臂会跟随运动,按Ctrl+C退出\n录制数据集 # LeRobot 具身智能机械臂实操入门课程-03:机械臂的数据集录制与模型训练\n官方-优质数据集要求\n官方-录制数据集教程\n每个回合录制流程:等待程序提示录制当前回合,通过主臂遥操作机械臂进行抓取,抓取结束后,将机械臂恢复到休息位再结束当前回合,等待程序记录数据的同时重置抓取环境,等待程序提示录制下一个回合。\n键盘控制说明:\n按键 何时使用 作用 右箭头 (→) 在当前回合采集期间,并且你已成功完成任务 成功并提前结束 当前 回合,保存数据,然后进入重置阶段。 左箭头 (←) 在当前回合采集期间,但你犯了个错误 作废并重新开始 当前 回合。这次的录制数据会被丢弃。 ESC 键 任何时候 完全终止 整个采集会话。程序会保存已完成的数据并退出。 录制测试数据集 # 采集次数:5 次 采集任务(请自定义修改):Put the red pepper toy in the cardboard box 采集数据保存路径:./data/datasets/xiadengma/record-test-so101 BASH不换行 lerobot-record \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.id=my_follower_arm \\ --robot.calibration_dir=./data/calibration \\ --robot.cameras=\u0026#34;{ wrist_left: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, front_rgb: {type: opencv, index_or_path: 8, width: 640, height: 480, fps: 30}}\u0026#34; \\ --teleop.type=so101_leader \\ --teleop.port=/dev/leader_arm \\ --teleop.id=my_leader_arm \\ --teleop.calibration_dir=./data/calibration \\ --display_data=true \\ --dataset.repo_id=xiadengma/record-test-so101 \\ --dataset.num_episodes=5 \\ --dataset.single_task=\u0026#34;Put the red pepper toy in the cardboard box\u0026#34; \\ --dataset.push_to_hub=false \\ --dataset.root=./data/datasets/xiadengma/record-test-so101 ReRun Web 可视化 在src/lerobot下的extra文件夹中添加web_visualization_utils.py和lerobot_record_web.py两个文件,内容如下:\nweb_visualization_utils.py:\nPYTHON不换行 # Copyright 2024 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. \u0026#34;\u0026#34;\u0026#34;Web-based visualization utilities using Rerun web viewer.\u0026#34;\u0026#34;\u0026#34; import numbers import os from typing import Any import numpy as np import rerun as rr from lerobot.utils.constants import OBS_PREFIX, OBS_STR def init_rerun_web( session_name: str = \u0026#34;lerobot_control_loop\u0026#34;, port: int = 9090, open_browser: bool = True, memory_limit: str = \u0026#34;10%\u0026#34;, ) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; 初始化 Rerun SDK 用于 Web 浏览器可视化控制循环。 Initializes the Rerun SDK for visualizing the control loop in a web browser. 参数 Args: session_name: Rerun 会话名称 / Name of the Rerun session port: Web 服务器端口 / Web server port (default: 9090) open_browser: 是否自动打开浏览器 / Whether to automatically open browser (default: True) memory_limit: 内存限制 / Memory limit for Rerun (default: \u0026#34;10%\u0026#34;) 使用示例 Example: ```python from lerobot.extra.web_visualization_utils import init_rerun_web # 在浏览器中启动可视化 / Start visualization in browser init_rerun_web(session_name=\u0026#34;my_teleoperation\u0026#34;, port=9090) ``` 访问 Access: 在浏览器中打开 / Open in browser: http://localhost:9090 \u0026#34;\u0026#34;\u0026#34; batch_size = os.getenv(\u0026#34;RERUN_FLUSH_NUM_BYTES\u0026#34;, \u0026#34;8000\u0026#34;) os.environ[\u0026#34;RERUN_FLUSH_NUM_BYTES\u0026#34;] = batch_size rr.init(session_name) print(\u0026#34;🌐 启动 Rerun Web Viewer / Starting Rerun Web Viewer\u0026#34;) print(f\u0026#34;📍 访问地址 / Access URL: http://localhost:{port}\u0026#34;) print(f\u0026#34;💾 内存限制 / Memory limit: {memory_limit}\u0026#34;) rr.serve( open_browser=open_browser, web_port=port, server_memory_limit=memory_limit, ) def init_rerun_connect( addr: str = \u0026#34;127.0.0.1:9876\u0026#34;, session_name: str = \u0026#34;lerobot_control_loop\u0026#34;, ) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; 连接到远程 Rerun Viewer (适用于远程服务器)。 Connect to a remote Rerun Viewer (useful for remote servers). 参数 Args: addr: 远程 Rerun Viewer 地址 / Remote Rerun Viewer address (default: \u0026#34;127.0.0.1:9876\u0026#34;) session_name: Rerun 会话名称 / Name of the Rerun session 使用示例 Example: ```python from lerobot.extra.web_visualization_utils import init_rerun_connect # 连接到远程 Rerun Viewer / Connect to remote Rerun Viewer # 首先在另一个终端运行: rerun --port 9876 # First run in another terminal: rerun --port 9876 init_rerun_connect(addr=\u0026#34;127.0.0.1:9876\u0026#34;) ``` \u0026#34;\u0026#34;\u0026#34; batch_size = os.getenv(\u0026#34;RERUN_FLUSH_NUM_BYTES\u0026#34;, \u0026#34;8000\u0026#34;) os.environ[\u0026#34;RERUN_FLUSH_NUM_BYTES\u0026#34;] = batch_size rr.init(session_name) print(\u0026#34;🔗 连接到远程 Rerun Viewer / Connecting to remote Rerun Viewer\u0026#34;) print(f\u0026#34;📍 地址 / Address: {addr}\u0026#34;) rr.connect_tcp(addr=addr) def _is_scalar(x): return isinstance(x, (float | numbers.Real | np.integer | np.floating)) or ( isinstance(x, np.ndarray) and x.ndim == 0 ) def log_rerun_data( observation: dict[str, Any] | None = None, action: dict[str, Any] | None = None, ) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; 将观测和动作数据记录到 Rerun 用于实时可视化。 Logs observation and action data to Rerun for real-time visualization. This function iterates through the provided observation and action dictionaries and sends their contents to the Rerun viewer. It handles different data types appropriately: - Scalar values (floats, ints) are logged as `rr.Scalar`. - 3D NumPy arrays that resemble images (e.g., with 1, 3, or 4 channels first) are transposed from CHW to HWC format and logged as `rr.Image`. - 1D NumPy arrays are logged as a series of individual scalars, with each element indexed. - Other multi-dimensional arrays are flattened and logged as individual scalars. Keys are automatically namespaced with \u0026#34;observation.\u0026#34; or \u0026#34;action.\u0026#34; if not already present. 参数 Args: observation: 包含观测数据的可选字典 / An optional dictionary containing observation data to log. action: 包含动作数据的可选字典 / An optional dictionary containing action data to log. \u0026#34;\u0026#34;\u0026#34; if observation: for k, v in observation.items(): if v is None: continue key = k if str(k).startswith(OBS_PREFIX) else f\u0026#34;{OBS_STR}.{k}\u0026#34; if _is_scalar(v): rr.log(key, rr.Scalar(float(v))) elif isinstance(v, np.ndarray): arr = v # Convert CHW -\u0026gt; HWC when needed if arr.ndim == 3 and arr.shape[0] in (1, 3, 4) and arr.shape[-1] not in (1, 3, 4): arr = np.transpose(arr, (1, 2, 0)) if arr.ndim == 1: for i, vi in enumerate(arr): rr.log(f\u0026#34;{key}_{i}\u0026#34;, rr.Scalar(float(vi))) else: rr.log(key, rr.Image(arr), static=True) if action: for k, v in action.items(): if v is None: continue key = k if str(k).startswith(\u0026#34;action.\u0026#34;) else f\u0026#34;action.{k}\u0026#34; if _is_scalar(v): rr.log(key, rr.Scalar(float(v))) elif isinstance(v, np.ndarray): if v.ndim == 1: for i, vi in enumerate(v): rr.log(f\u0026#34;{key}_{i}\u0026#34;, rr.Scalar(float(vi))) else: # Fall back to flattening higher-dimensional arrays flat = v.flatten() for i, vi in enumerate(flat): rr.log(f\u0026#34;{key}_{i}\u0026#34;, rr.Scalar(float(vi))) lerobot_record_web.py:\nPYTHON不换行 #!/usr/bin/env python3 # Copyright 2024 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. \u0026#34;\u0026#34;\u0026#34; 录制数据集 (使用 Web 可视化)。机器人动作可由遥操作或策略生成。 Records a dataset (with Web visualization). Actions for the robot can be either generated by teleoperation or by a policy. \u0026#34;\u0026#34;\u0026#34; import logging import time from dataclasses import asdict, dataclass, field from pathlib import Path from pprint import pformat from typing import Any from lerobot.cameras import ( # noqa: F401 CameraConfig, # noqa: F401 ) from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig # noqa: F401 from lerobot.cameras.realsense.configuration_realsense import RealSenseCameraConfig # noqa: F401 from lerobot.configs import parser from lerobot.configs.policies import PreTrainedConfig from lerobot.datasets.image_writer import safe_stop_image_writer from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.pipeline_features import aggregate_pipeline_dataset_features, create_initial_features from lerobot.datasets.utils import build_dataset_frame, combine_feature_dicts from lerobot.datasets.video_utils import VideoEncodingManager from lerobot.policies.factory import make_policy, make_pre_post_processors from lerobot.policies.pretrained import PreTrainedPolicy from lerobot.policies.utils import make_robot_action from lerobot.processor import ( PolicyAction, PolicyProcessorPipeline, RobotAction, RobotObservation, RobotProcessorPipeline, make_default_processors, ) from lerobot.processor.rename_processor import rename_stats from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, bi_so100_follower, # noqa: F401 hope_jr, # noqa: F401 koch_follower, # noqa: F401 make_robot_from_config, so100_follower, # noqa: F401 so101_follower, # noqa: F401 ) from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, bi_so100_leader, # noqa: F401 homunculus, # noqa: F401 koch_leader, make_teleoperator_from_config, so100_leader, so101_leader, ) from lerobot.teleoperators.keyboard.teleop_keyboard import KeyboardTeleop from lerobot.utils.constants import ACTION, OBS_STR from lerobot.utils.control_utils import ( init_keyboard_listener, is_headless, predict_action, sanity_check_dataset_name, sanity_check_dataset_robot_compatibility, ) from lerobot.utils.import_utils import register_third_party_devices from lerobot.utils.robot_utils import busy_wait from lerobot.utils.utils import ( get_safe_torch_device, init_logging, log_say, ) # 导入 Web 可视化工具 / Import web visualization utilities from .web_visualization_utils import init_rerun_web, log_rerun_data @dataclass class DatasetRecordConfig: # Dataset identifier. By convention it should match \u0026#39;{hf_username}/{dataset_name}\u0026#39; (e.g. `lerobot/test`). # 数据集标识符,按约定为\u0026#34;用户名/数据集名\u0026#34; repo_id: str # A short but accurate description of the task performed during the recording (e.g. \u0026#34;Pick the Lego block and drop it in the box on the right.\u0026#34;) # 对录制任务的简短准确描述 single_task: str # Root directory where the dataset will be stored (e.g. \u0026#39;dataset/path\u0026#39;). # 数据集保存的根目录 root: str | Path | None = None # Limit the frames per second. # 限制帧率 fps: int = 30 # Number of seconds for data recording for each episode. # 每个 episode 的录制时长(秒) episode_time_s: int | float = 60 # Number of seconds for resetting the environment after each episode. # 每个 episode 后重置环境的时长(秒) reset_time_s: int | float = 60 # Number of episodes to record. # 录制的 episode 数量 num_episodes: int = 50 # Encode frames in the dataset into video # 是否将帧编码为视频 video: bool = True # Upload dataset to Hugging Face hub. # 是否上传到 Hugging Face Hub push_to_hub: bool = True # Upload on private repository on the Hugging Face hub. # 是否上传到私有仓库 private: bool = False # Add tags to your dataset on the hub. # 为数据集添加标签 tags: list[str] | None = None # Number of subprocesses handling the saving of frames as PNG. Set to 0 to use threads only; # 负责保存 PNG 帧的子进程数;设为 0 仅用线程 # set to ≥1 to use subprocesses, each using threads to write images. The best number of processes # 设为 ≥1 时使用子进程,每个子进程内使用线程 # and threads depends on your system. We recommend 4 threads per camera with 0 processes. # 进程与线程数取决于系统,建议每个相机 4 线程、0 子进程 # If fps is unstable, adjust the thread count. If still unstable, try using 1 or more subprocesses. # fps 不稳先调线程数;若仍不稳尝试增加子进程 num_image_writer_processes: int = 0 # Number of threads writing the frames as png images on disk, per camera. # 每个相机写 PNG 的线程数 # Too many threads might cause unstable teleoperation fps due to main thread being blocked. # 线程过多可能阻塞主线程导致遥操作 fps 不稳 # Not enough threads might cause low camera fps. # 线程过少可能导致相机 fps 低 num_image_writer_threads_per_camera: int = 4 # Number of episodes to record before batch encoding videos # 批量编码视频前要累计的 episode 数 # Set to 1 for immediate encoding (default behavior), or higher for batched encoding # 1 表示立即编码(默认),更大表示批量 video_encoding_batch_size: int = 1 # Rename map for the observation to override the image and state keys # 覆盖图像与状态键名的重命名映射 rename_map: dict[str, str] = field(default_factory=dict) def __post_init__(self): if self.single_task is None: raise ValueError(\u0026#34;You need to provide a task as argument in `single_task`.\u0026#34;) @dataclass class RecordWebConfig: robot: RobotConfig dataset: DatasetRecordConfig # Whether to control the robot with a teleoperator # 是否使用遥操作控制机器人 teleop: TeleoperatorConfig | None = None # Whether to control the robot with a policy # 是否使用策略控制机器人 policy: PreTrainedConfig | None = None # Display all cameras on screen # 是否在屏幕显示所有摄像头 display_data: bool = False # Use vocal synthesis to read events. # 是否用语音播报事件 play_sounds: bool = True # Resume recording on an existing dataset. # 继续录制已有数据集 resume: bool = False # Web viewer port # Web 查看器端口 web_port: int = 9090 # Auto open browser # 自动打开浏览器 open_browser: bool = True def __post_init__(self): # HACK: We parse again the cli args here to get the pretrained path if there was one. # 再次解析 CLI 以获取预训练路径(若存在) policy_path = parser.get_path_arg(\u0026#34;policy\u0026#34;) if policy_path: cli_overrides = parser.get_cli_overrides(\u0026#34;policy\u0026#34;) self.policy = PreTrainedConfig.from_pretrained(policy_path, cli_overrides=cli_overrides) self.policy.pretrained_path = policy_path if self.teleop is None and self.policy is None: raise ValueError(\u0026#34;Choose a policy, a teleoperator or both to control the robot\u0026#34;) @classmethod def __get_path_fields__(cls) -\u0026gt; list[str]: \u0026#34;\u0026#34;\u0026#34;This enables the parser to load config from the policy using `--policy.path=local/dir`\u0026#34;\u0026#34;\u0026#34; # 允许解析器通过 --policy.path 加载配置 return [\u0026#34;policy\u0026#34;] \u0026#34;\u0026#34;\u0026#34; --------------- record_loop() data flow -------------------------- [ Robot ] V [ robot.get_observation() ] ---\u0026gt; raw_obs V [ robot_observation_processor ] ---\u0026gt; processed_obs V .-----( ACTION LOGIC )------------------. V V [ From Teleoperator ] [ From Policy ] | | | [teleop.get_action] -\u0026gt; raw_action | [predict_action] | | | | | V | V | [teleop_action_processor] | | | | | | \u0026#39;---\u0026gt; processed_teleop_action \u0026#39;---\u0026gt; processed_policy_action | | \u0026#39;-------------------------.-------------\u0026#39; V [ robot_action_processor ] --\u0026gt; robot_action_to_send V [ robot.send_action() ] -- (Robot Executes) V ( Save to Dataset ) V ( Rerun Log / Loop Wait ) \u0026#34;\u0026#34;\u0026#34; @safe_stop_image_writer def record_loop( robot: Robot, events: dict, fps: int, teleop_action_processor: RobotProcessorPipeline[ tuple[RobotAction, RobotObservation], RobotAction ], # runs after teleop # 在遥操作之后运行 robot_action_processor: RobotProcessorPipeline[ tuple[RobotAction, RobotObservation], RobotAction ], # runs before robot # 在发送到机器人之前运行 robot_observation_processor: RobotProcessorPipeline[ RobotObservation, RobotObservation ], # runs after robot # 在机器人观测之后运行 dataset: LeRobotDataset | None = None, teleop: Teleoperator | list[Teleoperator] | None = None, policy: PreTrainedPolicy | None = None, preprocessor: PolicyProcessorPipeline[dict[str, Any], dict[str, Any]] | None = None, postprocessor: PolicyProcessorPipeline[PolicyAction, PolicyAction] | None = None, control_time_s: int | None = None, single_task: str | None = None, display_data: bool = False, ): if dataset is not None and dataset.fps != fps: raise ValueError(f\u0026#34;The dataset fps should be equal to requested fps ({dataset.fps} != {fps}).\u0026#34;) teleop_arm = teleop_keyboard = None if isinstance(teleop, list): teleop_keyboard = next((t for t in teleop if isinstance(t, KeyboardTeleop)), None) teleop_arm = next( ( t for t in teleop if isinstance( t, (so100_leader.SO100Leader | so101_leader.SO101Leader | koch_leader.KochLeader), ) ), None, ) if not (teleop_arm and teleop_keyboard and len(teleop) == 2 and robot.name == \u0026#34;lekiwi_client\u0026#34;): raise ValueError( \u0026#34;For multi-teleop, the list must contain exactly one KeyboardTeleop and one arm teleoperator. Currently only supported for LeKiwi robot.\u0026#34; ) # Reset policy and processor if they are provided if policy is not None and preprocessor is not None and postprocessor is not None: policy.reset() preprocessor.reset() postprocessor.reset() timestamp = 0 start_episode_t = time.perf_counter() while timestamp \u0026lt; control_time_s: start_loop_t = time.perf_counter() if events[\u0026#34;exit_early\u0026#34;]: events[\u0026#34;exit_early\u0026#34;] = False break # Get robot observation # 获取机器人观测 obs = robot.get_observation() # Applies a pipeline to the raw robot observation, default is IdentityProcessor # 对观测应用处理流水线(默认恒等) obs_processed = robot_observation_processor(obs) if policy is not None or dataset is not None: observation_frame = build_dataset_frame(dataset.features, obs_processed, prefix=OBS_STR) # Get action from either policy or teleop # 从策略或遥操作获取动作 if policy is not None and preprocessor is not None and postprocessor is not None: action_values = predict_action( observation=observation_frame, policy=policy, device=get_safe_torch_device(policy.config.device), preprocessor=preprocessor, postprocessor=postprocessor, use_amp=policy.config.use_amp, task=single_task, robot_type=robot.robot_type, ) act_processed_policy: RobotAction = make_robot_action(action_values, dataset.features) elif policy is None and isinstance(teleop, Teleoperator): act = teleop.get_action() # Applies a pipeline to the raw teleop action, default is IdentityProcessor # 对遥操作动作应用处理流水线(默认恒等) act_processed_teleop = teleop_action_processor((act, obs)) elif policy is None and isinstance(teleop, list): arm_action = teleop_arm.get_action() arm_action = {f\u0026#34;arm_{k}\u0026#34;: v for k, v in arm_action.items()} keyboard_action = teleop_keyboard.get_action() base_action = robot._from_keyboard_to_base_action(keyboard_action) act = {**arm_action, **base_action} if len(base_action) \u0026gt; 0 else arm_action act_processed_teleop = teleop_action_processor((act, obs)) else: logging.info( \u0026#34;No policy or teleoperator provided, skipping action generation.\u0026#34; \u0026#34;This is likely to happen when resetting the environment without a teleop device.\u0026#34; \u0026#34;The robot won\u0026#39;t be at its rest position at the start of the next episode.\u0026#34; ) continue # Applies a pipeline to the action, default is IdentityProcessor # 对动作应用处理流水线(默认恒等) if policy is not None and act_processed_policy is not None: action_values = act_processed_policy robot_action_to_send = robot_action_processor((act_processed_policy, obs)) else: action_values = act_processed_teleop robot_action_to_send = robot_action_processor((act_processed_teleop, obs)) # Send action to robot # 发送动作到机器人 # Action can eventually be clipped using `max_relative_target`, # 动作可能被裁剪(max_relative_target) # so action actually sent is saved in the dataset. action = postprocessor.process(action) # 实际发送的动作会存入数据集 # TODO(steven, pepijn, adil): we should use a pipeline step to clip the action, so the sent action is the action that we input to the robot. # 建议在流水线中裁剪,保持一致 _sent_action = robot.send_action(robot_action_to_send) # Write to dataset # 写入数据集 if dataset is not None: action_frame = build_dataset_frame(dataset.features, action_values, prefix=ACTION) frame = {**observation_frame, **action_frame, \u0026#34;task\u0026#34;: single_task} dataset.add_frame(frame) if display_data: log_rerun_data(observation=obs_processed, action=action_values) dt_s = time.perf_counter() - start_loop_t busy_wait(1 / fps - dt_s) timestamp = time.perf_counter() - start_episode_t @parser.wrap() def record_web(cfg: RecordWebConfig) -\u0026gt; LeRobotDataset: init_logging() logging.info(pformat(asdict(cfg))) if cfg.display_data: # 使用 Web 可视化 / Use web visualization init_rerun_web( session_name=\u0026#34;recording_web\u0026#34;, port=cfg.web_port, open_browser=cfg.open_browser, ) robot = make_robot_from_config(cfg.robot) teleop = make_teleoperator_from_config(cfg.teleop) if cfg.teleop is not None else None teleop_action_processor, robot_action_processor, robot_observation_processor = make_default_processors() dataset_features = combine_feature_dicts( aggregate_pipeline_dataset_features( pipeline=teleop_action_processor, initial_features=create_initial_features( action=robot.action_features ), # TODO(steven, pepijn): in future this should be come from teleop or policy use_videos=cfg.dataset.video, ), aggregate_pipeline_dataset_features( pipeline=robot_observation_processor, initial_features=create_initial_features(observation=robot.observation_features), use_videos=cfg.dataset.video, ), ) if cfg.resume: dataset = LeRobotDataset( cfg.dataset.repo_id, root=cfg.dataset.root, batch_encoding_size=cfg.dataset.video_encoding_batch_size, ) if hasattr(robot, \u0026#34;cameras\u0026#34;) and len(robot.cameras) \u0026gt; 0: dataset.start_image_writer( num_processes=cfg.dataset.num_image_writer_processes, num_threads=cfg.dataset.num_image_writer_threads_per_camera * len(robot.cameras), ) sanity_check_dataset_robot_compatibility(dataset, robot, cfg.dataset.fps, dataset_features) else: # Create empty dataset or load existing saved episodes sanity_check_dataset_name(cfg.dataset.repo_id, cfg.policy) dataset = LeRobotDataset.create( cfg.dataset.repo_id, cfg.dataset.fps, root=cfg.dataset.root, robot_type=robot.name, features=dataset_features, use_videos=cfg.dataset.video, image_writer_processes=cfg.dataset.num_image_writer_processes, image_writer_threads=cfg.dataset.num_image_writer_threads_per_camera * len(robot.cameras), batch_encoding_size=cfg.dataset.video_encoding_batch_size, ) # Load pretrained policy policy = None if cfg.policy is None else make_policy(cfg.policy, ds_meta=dataset.meta) preprocessor = None postprocessor = None if cfg.policy is not None: preprocessor, postprocessor = make_pre_post_processors( policy_cfg=cfg.policy, pretrained_path=cfg.policy.pretrained_path, dataset_stats=rename_stats(dataset.meta.stats, cfg.dataset.rename_map), preprocessor_overrides={ \u0026#34;device_processor\u0026#34;: {\u0026#34;device\u0026#34;: cfg.policy.device}, \u0026#34;rename_observations_processor\u0026#34;: {\u0026#34;rename_map\u0026#34;: cfg.dataset.rename_map}, }, ) robot.connect() if teleop is not None: teleop.connect() listener, events = init_keyboard_listener() with VideoEncodingManager(dataset): recorded_episodes = 0 while recorded_episodes \u0026lt; cfg.dataset.num_episodes and not events[\u0026#34;stop_recording\u0026#34;]: log_say(f\u0026#34;Recording episode {dataset.num_episodes}\u0026#34;, cfg.play_sounds) record_loop( robot=robot, events=events, fps=cfg.dataset.fps, teleop_action_processor=teleop_action_processor, robot_action_processor=robot_action_processor, robot_observation_processor=robot_observation_processor, teleop=teleop, policy=policy, preprocessor=preprocessor, postprocessor=postprocessor, dataset=dataset, control_time_s=cfg.dataset.episode_time_s, single_task=cfg.dataset.single_task, display_data=cfg.display_data, ) # Execute a few seconds without recording to give time to manually reset the environment # 暂停录制以便手动重置环境 # Skip reset for the last episode to be recorded # 最后一个 episode 可跳过重置 if not events[\u0026#34;stop_recording\u0026#34;] and ( (recorded_episodes \u0026lt; cfg.dataset.num_episodes - 1) or events[\u0026#34;rerecord_episode\u0026#34;] ): log_say(\u0026#34;Reset the environment\u0026#34;, cfg.play_sounds) record_loop( robot=robot, events=events, fps=cfg.dataset.fps, teleop_action_processor=teleop_action_processor, robot_action_processor=robot_action_processor, robot_observation_processor=robot_observation_processor, teleop=teleop, control_time_s=cfg.dataset.reset_time_s, single_task=cfg.dataset.single_task, display_data=cfg.display_data, ) if events[\u0026#34;rerecord_episode\u0026#34;]: log_say(\u0026#34;Re-record episode\u0026#34;, cfg.play_sounds) events[\u0026#34;rerecord_episode\u0026#34;] = False events[\u0026#34;exit_early\u0026#34;] = False dataset.clear_episode_buffer() continue dataset.save_episode() recorded_episodes += 1 log_say(\u0026#34;Stop recording\u0026#34;, cfg.play_sounds, blocking=True) robot.disconnect() if teleop is not None: teleop.disconnect() if not is_headless() and listener is not None: listener.stop() if cfg.dataset.push_to_hub: dataset.push_to_hub(tags=cfg.dataset.tags, private=cfg.dataset.private) log_say(\u0026#34;Exiting\u0026#34;, cfg.play_sounds) return dataset def main(): register_third_party_devices() record_web() if __name__ == \u0026#34;__main__\u0026#34;: main() 执行遥操作:\nBASH不换行 python -m lerobot.extra.lerobot_record_web \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.id=my_follower_arm \\ --robot.calibration_dir=./data/calibration \\ --robot.cameras=\u0026#34;{ wrist_left: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, front_rgb: {type: opencv, index_or_path: 8, width: 640, height: 480, fps: 30}}\u0026#34; \\ --teleop.type=so101_leader \\ --teleop.port=/dev/leader_arm \\ --teleop.id=my_leader_arm \\ --teleop.calibration_dir=./data/calibration \\ --display_data=true \\ --dataset.repo_id=xiadengma/record-test-so101 \\ --dataset.num_episodes=3 \\ --dataset.single_task=\u0026#34;Put the red pepper toy in the cardboard box\u0026#34; \\ --web_port=9090 \\ --dataset.push_to_hub=false \\ --dataset.root=./data/datasets/xiadengma/record-test-so101 运行后默认情况下会在浏览器打开http://localhost:9090/?url=ws://localhost:9877,可以查看机械臂和摄像头画面\n可视化数据集 # 我们可以对录制的数据集进行可视化。\n查看指定回合: BASH不换行 lerobot-dataset-viz \\ --repo-id xiadengma/record-test-so101 \\ --root ./data/datasets/xiadengma/record-test-so101 \\ --episode-index 0 查看多个回合: BASH不换行 lerobot-dataset-viz \\ --repo-id xiadengma/record-test-so101 \\ --root ./data/datasets/xiadengma/record-test-so101 \\ --episodes 0 1 2 3 4 ReRun Web 可视化 在src/lerobot下的extra文件夹中添加lerobot_dataset_viz_web.py文件,内容如下:\nlerobot_dataset_viz_web.py: PYTHON不换行 #!/usr/bin/env python # Copyright 2024 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. \u0026#34;\u0026#34;\u0026#34; 使用 Web 浏览器可视化 LeRobotDataset 中任意 episode 的所有帧数据。 Visualize data of **all** frames of any episode of a dataset of type LeRobotDataset in a web browser. 注意 Note: - Episode 的最后一帧不一定对应最终状态 / The last frame doesn\u0026#39;t always correspond to a final state - 图像可能存在压缩伪影 / Images may show compression artifacts from mp4 encoding 访问 Access: 浏览器打开 / Open in browser: http://localhost:PORT \u0026#34;\u0026#34;\u0026#34; import argparse import gc import logging from collections.abc import Iterator from pathlib import Path import numpy as np import rerun as rr import torch import torch.utils.data import tqdm from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.utils.constants import ACTION, DONE, OBS_STATE, REWARD class EpisodeSampler(torch.utils.data.Sampler): \u0026#34;\u0026#34;\u0026#34;用于采样单个 episode 的所有帧 / Sampler for all frames of a single episode.\u0026#34;\u0026#34;\u0026#34; def __init__(self, dataset: LeRobotDataset, episode_index: int): from_idx = dataset.meta.episodes[\u0026#34;dataset_from_index\u0026#34;][episode_index] to_idx = dataset.meta.episodes[\u0026#34;dataset_to_index\u0026#34;][episode_index] self.frame_ids = range(from_idx, to_idx) def __iter__(self) -\u0026gt; Iterator: return iter(self.frame_ids) def __len__(self) -\u0026gt; int: return len(self.frame_ids) def to_hwc_uint8_numpy(chw_float32_torch: torch.Tensor) -\u0026gt; np.ndarray: \u0026#34;\u0026#34;\u0026#34; 将 PyTorch CHW float32 图像转换为 NumPy HWC uint8 格式。 Convert PyTorch CHW float32 image to NumPy HWC uint8 format. \u0026#34;\u0026#34;\u0026#34; assert chw_float32_torch.dtype == torch.float32 assert chw_float32_torch.ndim == 3 c, h, w = chw_float32_torch.shape assert c \u0026lt; h and c \u0026lt; w, ( f\u0026#34;期望通道优先格式,但得到 / expect channel first images, but got {chw_float32_torch.shape}\u0026#34; ) hwc_uint8_numpy = (chw_float32_torch * 255).type(torch.uint8).permute(1, 2, 0).numpy() return hwc_uint8_numpy def visualize_episode( dataset: LeRobotDataset, episode_index: int, batch_size: int = 32, num_workers: int = 0, ) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; 在 Rerun 中可视化单个 episode 的所有帧。 Visualize all frames of a single episode in Rerun. Args: dataset: LeRobot 数据集 / LeRobot dataset episode_index: Episode 索引 / Episode index batch_size: 批处理大小 / Batch size for dataloader num_workers: 数据加载进程数 / Number of worker processes \u0026#34;\u0026#34;\u0026#34; logging.info( f\u0026#34;📊 加载 Episode {episode_index} 的数据加载器 / Loading dataloader for Episode {episode_index}\u0026#34; ) episode_sampler = EpisodeSampler(dataset, episode_index) dataloader = torch.utils.data.DataLoader( dataset, num_workers=num_workers, batch_size=batch_size, sampler=episode_sampler, ) total_frames = len(episode_sampler) logging.info( f\u0026#34;📈 Episode {episode_index} 共有 {total_frames} 帧 / Episode {episode_index} has {total_frames} frames\u0026#34; ) # 记录数据到 Rerun / Log data to Rerun for batch in tqdm.tqdm(dataloader, total=len(dataloader), desc=f\u0026#34;Episode {episode_index}\u0026#34;): # 遍历批次中的每一帧 / iterate over the batch for i in range(len(batch[\u0026#34;index\u0026#34;])): rr.set_time_sequence(\u0026#34;frame_index\u0026#34;, batch[\u0026#34;frame_index\u0026#34;][i].item()) rr.set_time_seconds(\u0026#34;timestamp\u0026#34;, batch[\u0026#34;timestamp\u0026#34;][i].item()) # 显示相机图像 / display camera images for key in dataset.meta.camera_keys: rr.log(f\u0026#34;cameras/{key}\u0026#34;, rr.Image(to_hwc_uint8_numpy(batch[key][i]))) # 显示动作空间的每个维度 / display each dimension of action space if ACTION in batch: for dim_idx, val in enumerate(batch[ACTION][i]): rr.log(f\u0026#34;{ACTION}/dim_{dim_idx}\u0026#34;, rr.Scalar(val.item())) # 显示观测状态空间的每个维度 / display each dimension of observed state space if OBS_STATE in batch: for dim_idx, val in enumerate(batch[OBS_STATE][i]): rr.log(f\u0026#34;state/dim_{dim_idx}\u0026#34;, rr.Scalar(val.item())) # 显示完成标志 / display done flag if DONE in batch: rr.log(DONE, rr.Scalar(batch[DONE][i].item())) # 显示奖励 / display reward if REWARD in batch: rr.log(REWARD, rr.Scalar(batch[REWARD][i].item())) # 显示成功标志 / display success flag if \u0026#34;next.success\u0026#34; in batch: rr.log(\u0026#34;success\u0026#34;, rr.Scalar(batch[\u0026#34;next.success\u0026#34;][i].item())) logging.info(f\u0026#34;✅ Episode {episode_index} 可视化完成 / Episode {episode_index} visualization complete\u0026#34;) def visualize_dataset_web( dataset: LeRobotDataset, episode_indices: list[int], batch_size: int = 32, num_workers: int = 0, port: int = 9090, open_browser: bool = True, memory_limit: str = \u0026#34;25%\u0026#34;, ) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; 使用 Web 界面可视化数据集。 Visualize dataset using web interface. Args: dataset: LeRobot 数据集 / LeRobot dataset episode_indices: 要可视化的 episode 索引列表 / List of episode indices to visualize batch_size: 批处理大小 / Batch size for dataloader num_workers: 数据加载进程数 / Number of worker processes port: Web 服务器端口 / Web server port open_browser: 是否自动打开浏览器 / Whether to automatically open browser memory_limit: Rerun 内存限制 / Memory limit for Rerun \u0026#34;\u0026#34;\u0026#34; repo_id = dataset.repo_id # 初始化 Rerun Web 界面 / Initialize Rerun web interface logging.info(\u0026#34;🌐 启动 Rerun Web 界面 / Starting Rerun Web interface\u0026#34;) logging.info(f\u0026#34;📍 访问地址 / Access URL: http://localhost:{port}\u0026#34;) logging.info(f\u0026#34;💾 内存限制 / Memory limit: {memory_limit}\u0026#34;) rr.init(f\u0026#34;{repo_id}_web_viz\u0026#34;, spawn=False) # 手动触发垃圾回收,避免阻塞 / Manually call garbage collector to avoid blocking gc.collect() # 启动 Web 服务器 / Start web server rr.serve_web( open_browser=open_browser, web_port=port, server_memory_limit=memory_limit, ) # 可视化每个 episode / Visualize each episode for episode_idx in episode_indices: if episode_idx \u0026gt;= len(dataset.meta.episodes): logging.warning( f\u0026#34;⚠️ Episode {episode_idx} 不存在,跳过 / Episode {episode_idx} does not exist, skipping\u0026#34; ) continue # 为每个 episode 创建记录路径 / Create recording path for each episode rr.log(f\u0026#34;episode_{episode_idx}/info\u0026#34;, rr.TextLog(f\u0026#34;Episode {episode_idx}\u0026#34;), static=True) # 设置时间序列标记当前 episode / Set time sequence for current episode rr.set_time_sequence(\u0026#34;episode\u0026#34;, episode_idx) visualize_episode( dataset=dataset, episode_index=episode_idx, batch_size=batch_size, num_workers=num_workers, ) logging.info(\u0026#34;✨ 所有 episode 可视化完成 / All episodes visualization complete\u0026#34;) logging.info(\u0026#34;🌐 Web 服务器持续运行中,按 Ctrl+C 退出 / Web server running, press Ctrl+C to exit\u0026#34;) # 保持服务器运行 / Keep server running try: import time while True: time.sleep(1) except KeyboardInterrupt: logging.info(\u0026#34;👋 收到 Ctrl-C,正在退出 / Ctrl-C received, exiting\u0026#34;) def main(): parser = argparse.ArgumentParser( description=\u0026#34;使用 Web 浏览器可视化 LeRobot 数据集 / Visualize LeRobot dataset in web browser\u0026#34;, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( \u0026#34;--repo-id\u0026#34;, type=str, required=True, help=\u0026#34;数据集仓库 ID / Dataset repository ID (e.g. `lerobot/pusht` or `xiadengma/record-test-so101`)\u0026#34;, ) # Episode 选择参数 / Episode selection arguments group = parser.add_mutually_exclusive_group(required=True) group.add_argument( \u0026#34;--episode-index\u0026#34;, type=int, help=\u0026#34;要可视化的单个 episode 索引 / Single episode index to visualize\u0026#34;, ) group.add_argument( \u0026#34;--episodes\u0026#34;, type=int, nargs=\u0026#34;+\u0026#34;, help=\u0026#34;要可视化的多个 episode 索引 / Multiple episode indices to visualize (e.g. 0 1 2 3)\u0026#34;, ) parser.add_argument( \u0026#34;--root\u0026#34;, type=Path, default=None, help=\u0026#34;本地数据集根目录 / Root directory for local dataset (e.g. `--root ./data/datasets/xiadengma/record-test-so101`)\u0026#34;, ) parser.add_argument( \u0026#34;--batch-size\u0026#34;, type=int, default=32, help=\u0026#34;DataLoader 批处理大小 / Batch size for DataLoader (default: 32)\u0026#34;, ) parser.add_argument( \u0026#34;--num-workers\u0026#34;, type=int, default=4, help=\u0026#34;DataLoader 进程数 / Number of DataLoader worker processes (default: 4)\u0026#34;, ) parser.add_argument( \u0026#34;--port\u0026#34;, type=int, default=9090, help=\u0026#34;Web 服务器端口 / Web server port (default: 9090)\u0026#34;, ) parser.add_argument( \u0026#34;--open-browser\u0026#34;, type=lambda x: str(x).lower() in (\u0026#34;true\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;yes\u0026#34;), default=True, help=\u0026#34;是否自动打开浏览器 / Whether to automatically open browser (default: True)\u0026#34;, ) parser.add_argument( \u0026#34;--memory-limit\u0026#34;, type=str, default=\u0026#34;25%\u0026#34;, help=\u0026#34;Rerun 内存限制 / Memory limit for Rerun (default: 25%%)\u0026#34;, ) parser.add_argument( \u0026#34;--tolerance-s\u0026#34;, type=float, default=1e-4, help=\u0026#34;时间戳容差(秒)/ Tolerance in seconds for timestamps (default: 1e-4)\u0026#34;, ) args = parser.parse_args() # 配置日志 / Configure logging logging.basicConfig( level=logging.INFO, format=\u0026#34;%(asctime)s - %(levelname)s - %(message)s\u0026#34;, datefmt=\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;, ) # 确定要可视化的 episode 列表 / Determine episode list if args.episode_index is not None: episode_indices = [args.episode_index] else: episode_indices = args.episodes logging.info(\u0026#34;=\u0026#34; * 80) logging.info(\u0026#34;🤖 LeRobot 数据集 Web 可视化工具 / LeRobot Dataset Web Visualizer\u0026#34;) logging.info(\u0026#34;=\u0026#34; * 80) logging.info(f\u0026#34;📦 数据集 / Dataset: {args.repo_id}\u0026#34;) logging.info(f\u0026#34;📂 根目录 / Root: {args.root if args.root else \u0026#39;HuggingFace Cache\u0026#39;}\u0026#34;) logging.info(f\u0026#34;📊 Episodes: {episode_indices}\u0026#34;) logging.info(\u0026#34;=\u0026#34; * 80) # 加载数据集 / Load dataset logging.info(\u0026#34;🔄 正在加载数据集 / Loading dataset...\u0026#34;) dataset = LeRobotDataset( repo_id=args.repo_id, episodes=episode_indices, root=args.root, tolerance_s=args.tolerance_s, ) logging.info(\u0026#34;✅ 数据集加载成功 / Dataset loaded successfully\u0026#34;) logging.info(f\u0026#34;📈 数据集总帧数 / Total frames: {len(dataset)}\u0026#34;) logging.info(f\u0026#34;📹 相机数量 / Number of cameras: {len(dataset.meta.camera_keys)}\u0026#34;) logging.info(f\u0026#34;🎥 相机列表 / Camera keys: {dataset.meta.camera_keys}\u0026#34;) # 启动 Web 可视化 / Start web visualization visualize_dataset_web( dataset=dataset, episode_indices=episode_indices, batch_size=args.batch_size, num_workers=args.num_workers, port=args.port, open_browser=args.open_browser, memory_limit=args.memory_limit, ) if __name__ == \u0026#34;__main__\u0026#34;: main() 查看数据集:\n查看单个回合: BASH不换行 python -m lerobot.extra.lerobot_dataset_viz_web \\ --repo-id xiadengma/record-test-so101 \\ --root ./data/datasets/xiadengma/record-test-so101 \\ --episode-index 0 \\ --port 9090 查看多个回合: BASH不换行 python -m lerobot.extra.lerobot_dataset_viz_web \\ --repo-id xiadengma/record-test-so101 \\ --root ./data/datasets/xiadengma/record-test-so101 \\ --episodes 0 1 2 3 4 \\ --port 9090 回放数据集 # 不稳定,可跳过,可尝试。 你也可以让机械臂根据数据集进行回放:\n回放之前测试录制数据集的第一个回合: BASH不换行 lerobot-replay \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.id=my_follower_arm \\ --dataset.repo_id=xiadengma/record-test-so101 \\ --dataset.root=./data/datasets/xiadengma/record-test-so101 \\ --dataset.episode=0 你应该可以看到机械臂按照数据集进行回放。 录制完整数据集 # 现在,我们开始录制数据集,并将这个数据集训练后用于推理。\n每个回合录制流程:等待程序提示录制当前回合,通过主臂遥操作机械臂进行抓取,抓取结束后,将机械臂恢复到休息位再结束当前回合,等待程序记录数据的同时重置抓取环境,等待程序提示录制下一个回合。 录制要求: 录制数量:至少 50 组数据,确保数据充分性 录制频次:每个位置重复录制 10 次,以提高数据的多样性和鲁棒性 录制位置:至少选择 5 个不同的位置,涵盖更多动作场景 键盘控制说明: 按键 何时使用 作用 右箭头 (→) 在当前回合采集期间,并且你已成功完成任务 成功并提前结束 当前 回合,保存数据,然后进入重置阶段。 左箭头 (←) 在当前回合采集期间,但你犯了个错误 作废并重新开始 当前 回合。这次的录制数据会被丢弃。 ESC 键 任何时候 完全终止 整个采集会话。程序会保存已完成的数据并退出。 录制数据集:\n采集次数:50 次 采集任务(请自定义修改):Put the red pepper toy in the cardboard box 采集数据保存路径(请自定义修改):./data/datasets/xiadengma/so101-red-pepper BASH不换行 python -m lerobot.extra.lerobot_record_web \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.id=my_follower_arm \\ --robot.calibration_dir=./data/calibration \\ --robot.cameras=\u0026#34;{ wrist_left: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, front_rgb: {type: opencv, index_or_path: 8, width: 640, height: 480, fps: 30}}\u0026#34; \\ --teleop.type=so101_leader \\ --teleop.port=/dev/leader_arm \\ --teleop.id=my_leader_arm \\ --teleop.calibration_dir=./data/calibration \\ --display_data=true \\ --dataset.repo_id=xiadengma/so101-red-pepper \\ --dataset.num_episodes=50 \\ --dataset.single_task=\u0026#34;Put the red pepper toy in the cardboard box\u0026#34; \\ --web_port=9090 \\ --dataset.push_to_hub=false \\ --dataset.root=./data/datasets/xiadengma/so101-red-pepper 恢复录制(仅供参考):\nRecording episode 15时程序报错:Waiting for image writer to terminate...和TimeoutError: Timed out waiting for frame from camera OpenCVCamera(8) after 200 ms. Read thread alive: True.\n问题原因:在机械臂运动过程中,拉扯到机械臂腕部摄像头的连接处,导致摄像头连接不稳定。\n解决方法:重新连接摄像头,并预留足够长度的线缆,确保摄像头连接稳定,接着运行下面命令恢复录制。\nBASH不换行 python -m lerobot.extra.lerobot_record_web \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.id=my_follower_arm \\ --robot.calibration_dir=./data/calibration \\ --robot.cameras=\u0026#34;{ wrist_left: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, front_rgb: {type: opencv, index_or_path: 8, width: 640, height: 480, fps: 30}}\u0026#34; \\ --teleop.type=so101_leader \\ --teleop.port=/dev/leader_arm \\ --teleop.id=my_leader_arm \\ --teleop.calibration_dir=./data/calibration \\ --display_data=true \\ --dataset.repo_id=xiadengma/so101-red-pepper \\ --dataset.num_episodes=35 \\ --dataset.single_task=\u0026#34;Put the red pepper toy in the cardboard box\u0026#34; \\ --web_port=9090 \\ --dataset.push_to_hub=false \\ --dataset.root=./data/datasets/xiadengma/so101-red-pepper \\ --resume=true 在记录过程中会自动创建检查点。 如果记录过程中断,可以通过重新运行相同的命令并添加 \u0026ndash;resume=true 来恢复记录。 ⚠️ 重要提示:在恢复时,需将 \u0026ndash;dataset.num_episodes 设置为要额外记录的剧集数量(而不是数据集中目标的总剧集数量) 录制结束后,数据集大小为390M\n训练 # 如果使用SwanLab训练,必须修改,官方暂时没有合并分支 在extra文件夹添加swanlab_utils.py、train_swanlab.py和lerobot_train_swanlab.py。\nswanlab_utils.py:\nPYTHON不换行 #!/usr/bin/env python # Copyright 2024 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import re from glob import glob from pathlib import Path from typing import TYPE_CHECKING from huggingface_hub.constants import SAFETENSORS_SINGLE_FILE from termcolor import colored from lerobot.utils.constants import PRETRAINED_MODEL_DIR if TYPE_CHECKING: from lerobot.extra.train_swanlab import TrainPipelineSwanLabConfig def cfg_to_group(cfg: \u0026#34;TrainPipelineSwanLabConfig\u0026#34;, return_list: bool = False) -\u0026gt; list[str] | str: \u0026#34;\u0026#34;\u0026#34;Return a group name for logging. Optionally returns group name as list.\u0026#34;\u0026#34;\u0026#34; lst = [ f\u0026#34;policy:{cfg.policy.type}\u0026#34;, f\u0026#34;seed:{cfg.seed}\u0026#34;, ] if cfg.dataset is not None: lst.append(f\u0026#34;dataset:{cfg.dataset.repo_id}\u0026#34;) if cfg.env is not None: lst.append(f\u0026#34;env:{cfg.env.type}\u0026#34;) return lst if return_list else \u0026#34;-\u0026#34;.join(lst) def get_swanlab_run_id_from_filesystem(log_dir: Path) -\u0026gt; str: # Get the SwanLab run ID. paths = glob(str(log_dir / \u0026#34;swanlab/latest-run/run-*\u0026#34;)) if len(paths) != 1: raise RuntimeError(\u0026#34;Couldn\u0026#39;t get the previous SwanLab run ID for run resumption.\u0026#34;) match = re.search(r\u0026#34;run-([^\\.]+).swanlab\u0026#34;, paths[0].split(\u0026#34;/\u0026#34;)[-1]) if match is None: raise RuntimeError(\u0026#34;Couldn\u0026#39;t get the previous SwanLab run ID for run resumption.\u0026#34;) swanlab_run_id = match.groups(0)[0] return swanlab_run_id def get_safe_swanlab_artifact_name(name: str): \u0026#34;\u0026#34;\u0026#34;SwanLab artifacts don\u0026#39;t accept \u0026#34;:\u0026#34; or \u0026#34;/\u0026#34; in their name.\u0026#34;\u0026#34;\u0026#34; return name.replace(\u0026#34;:\u0026#34;, \u0026#34;_\u0026#34;).replace(\u0026#34;/\u0026#34;, \u0026#34;_\u0026#34;) class SwanLabLogger: \u0026#34;\u0026#34;\u0026#34;A helper class to log object using swanlab.\u0026#34;\u0026#34;\u0026#34; def __init__(self, cfg: \u0026#34;TrainPipelineSwanLabConfig\u0026#34;): self.cfg = cfg.swanlab self.log_dir = cfg.output_dir self.job_name = cfg.job_name self.env_fps = cfg.env.fps if cfg.env else None self._group = cfg_to_group(cfg) import swanlab swanlab_run_id = ( cfg.swanlab.run_id if cfg.swanlab.run_id else get_swanlab_run_id_from_filesystem(self.log_dir) if cfg.resume else None ) self._run = swanlab.init( project=self.cfg.project, experiment_name=swanlab_run_id, description=self.cfg.notes, tags=cfg_to_group(cfg, return_list=True), logdir=str(self.log_dir), config=cfg.to_dict(), save_code=False, resume=cfg.resume, mode=self.cfg.mode if self.cfg.mode in [\u0026#34;cloud\u0026#34;, \u0026#34;offline\u0026#34;, \u0026#34;local\u0026#34;, \u0026#34;disabled\u0026#34;] else \u0026#34;cloud\u0026#34;, ) run_id = self._run.public.run_id # NOTE: We will override the cfg.swanlab.run_id with the swanlab run id. # This is because we want to be able to resume the run from the swanlab run id. cfg.swanlab.run_id = run_id # Handle custom step key for rl asynchronous training. self._swanlab_custom_step_key: set[str] | None = None print(colored(\u0026#34;Logs will be synced with swanlab.\u0026#34;, \u0026#34;blue\u0026#34;, attrs=[\u0026#34;bold\u0026#34;])) logging.info( f\u0026#34;Track this run --\u0026gt; {colored(self._run.public.cloud.experiment_url, \u0026#39;yellow\u0026#39;, attrs=[\u0026#39;bold\u0026#39;])}\u0026#34; ) self._swanlab = swanlab def log_policy(self, checkpoint_dir: Path): \u0026#34;\u0026#34;\u0026#34;Checkpoints the policy to swanlab.\u0026#34;\u0026#34;\u0026#34; if self.cfg.disable_artifact: return step_id = checkpoint_dir.name artifact_name = f\u0026#34;{self._group}-{step_id}\u0026#34; artifact_name = get_safe_swanlab_artifact_name(artifact_name) # SwanLab doesn\u0026#39;t have direct artifact logging like wandb # We\u0026#39;ll log the model file path as a text log for now model_path = str(checkpoint_dir / PRETRAINED_MODEL_DIR / SAFETENSORS_SINGLE_FILE) self._swanlab.log({\u0026#34;model_checkpoint\u0026#34;: self._swanlab.Text(model_path)}, step=step_id) def log_dict( self, d: dict, step: int | None = None, mode: str = \u0026#34;train\u0026#34;, custom_step_key: str | None = None ): if mode not in {\u0026#34;train\u0026#34;, \u0026#34;eval\u0026#34;}: raise ValueError(mode) if step is None and custom_step_key is None: raise ValueError(\u0026#34;Either step or custom_step_key must be provided.\u0026#34;) # NOTE: This is not simple. SwanLab step must always monotonically increase and it # increases with each swanlab.log call, but in the case of asynchronous RL for example, # multiple time steps is possible. For example, the interaction step with the environment, # the training step, the evaluation step, etc. So we need to define a custom step key # to log the correct step for each metric. if custom_step_key is not None: if self._swanlab_custom_step_key is None: self._swanlab_custom_step_key = set() new_custom_key = f\u0026#34;{mode}/{custom_step_key}\u0026#34; if new_custom_key not in self._swanlab_custom_step_key: self._swanlab_custom_step_key.add(new_custom_key) for k, v in d.items(): if not isinstance(v, (int, float, str)): logging.warning( f\u0026#39;SwanLab logging of key \u0026#34;{k}\u0026#34; was ignored as its type \u0026#34;{type(v)}\u0026#34; is not handled by this wrapper.\u0026#39; ) continue # Do not log the custom step key itself. if self._swanlab_custom_step_key is not None and k in self._swanlab_custom_step_key: continue if custom_step_key is not None: value_custom_step = d.get(custom_step_key) if value_custom_step is None: logging.warning( f\u0026#39;Custom step key \u0026#34;{custom_step_key}\u0026#34; not found in the dictionary. Skipping logging for this key.\u0026#39; ) continue data = {f\u0026#34;{mode}/{k}\u0026#34;: v, f\u0026#34;{mode}/{custom_step_key}\u0026#34;: value_custom_step} self._swanlab.log(data) continue self._swanlab.log(data={f\u0026#34;{mode}/{k}\u0026#34;: v}, step=step) def log_video(self, video_path: str, step: int, mode: str = \u0026#34;train\u0026#34;): if mode not in {\u0026#34;train\u0026#34;, \u0026#34;eval\u0026#34;}: raise ValueError(mode) # SwanLab media logging - using Media.Video for video logging swanlab_video = self._swanlab.Video(video_path, fps=self.env_fps) self._swanlab.log({f\u0026#34;{mode}/video\u0026#34;: swanlab_video}, step=step) train_swanlab.py:\nPYTHON不换行 #!/usr/bin/env python # Copyright 2024 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. \u0026#34;\u0026#34;\u0026#34; SwanLab 训练配置扩展 - 扩展 TrainPipelineConfig 以支持 SwanLab 这个模块为 LeRobot 训练管道提供 SwanLab 日志记录支持。 它继承了原始的 TrainPipelineConfig,并添加了 tracker 和 swanlab 配置字段。 \u0026#34;\u0026#34;\u0026#34; from dataclasses import dataclass, field from lerobot.configs.train import TrainPipelineConfig from lerobot.extra.default_swanlab import SwanLabConfig @dataclass class TrainPipelineSwanLabConfig(TrainPipelineConfig): \u0026#34;\u0026#34;\u0026#34;支持 SwanLab 的训练管道配置 继承自 TrainPipelineConfig,添加了以下字段: tracker: 日志跟踪器选择,可选值: \u0026#39;wandb\u0026#39;, \u0026#39;swanlab\u0026#39;, \u0026#39;both\u0026#39;, \u0026#39;none\u0026#39; swanlab: SwanLab 配置对象 使用示例: ```python config = TrainPipelineSwanLabConfig( dataset=DatasetConfig(repo_id=\u0026#34;my/dataset\u0026#34;), tracker=\u0026#34;swanlab\u0026#34;, swanlab=SwanLabConfig(project=\u0026#34;my-project\u0026#34;, mode=\u0026#34;cloud\u0026#34;), ) ``` \u0026#34;\u0026#34;\u0026#34; # Tracker selection: \u0026#39;wandb\u0026#39;, \u0026#39;swanlab\u0026#39;, \u0026#39;both\u0026#39;, or \u0026#39;none\u0026#39; # 跟踪器选择: \u0026#39;wandb\u0026#39;, \u0026#39;swanlab\u0026#39;, \u0026#39;both\u0026#39;, 或 \u0026#39;none\u0026#39; tracker: str = \u0026#34;wandb\u0026#34; # SwanLab configuration # SwanLab 配置 swanlab: SwanLabConfig = field(default_factory=SwanLabConfig) lerobot_train_swanlab.py:\nPYTHON不换行 #!/usr/bin/env python # Copyright 2024 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # 训练脚本:构建数据集/环境/策略与优化器,执行训练循环并定期评估与保存 # --tracker=swanlab \\ # --swanlab.project=my_lerobot \\ # --swanlab.mode=cloud import logging import time from contextlib import nullcontext from pprint import pformat from typing import Any import torch from termcolor import colored from torch.amp import GradScaler from torch.optim import Optimizer from lerobot.configs import parser from lerobot.datasets.factory import make_dataset from lerobot.datasets.sampler import EpisodeAwareSampler from lerobot.datasets.utils import cycle from lerobot.envs.factory import make_env from lerobot.envs.utils import close_envs from lerobot.extra.swanlab_utils import SwanLabLogger from lerobot.extra.train_swanlab import TrainPipelineSwanLabConfig from lerobot.optim.factory import make_optimizer_and_scheduler from lerobot.policies.factory import make_policy, make_pre_post_processors from lerobot.policies.pretrained import PreTrainedPolicy from lerobot.policies.utils import get_device_from_parameters from lerobot.rl.wandb_utils import WandBLogger from lerobot.scripts.lerobot_eval import eval_policy_all from lerobot.utils.logging_utils import AverageMeter, MetricsTracker from lerobot.utils.random_utils import set_seed from lerobot.utils.train_utils import ( get_step_checkpoint_dir, get_step_identifier, load_training_state, save_checkpoint, update_last_checkpoint, ) from lerobot.utils.utils import ( format_big_number, get_safe_torch_device, has_method, init_logging, ) def update_policy( train_metrics: MetricsTracker, policy: PreTrainedPolicy, batch: Any, optimizer: Optimizer, grad_clip_norm: float, grad_scaler: GradScaler, lr_scheduler=None, use_amp: bool = False, lock=None, ) -\u0026gt; tuple[MetricsTracker, dict]: \u0026#34;\u0026#34;\u0026#34; Performs a single training step to update the policy\u0026#39;s weights. This function executes the forward and backward passes, clips gradients, and steps the optimizer and learning rate scheduler. It also handles mixed-precision training via a GradScaler. Args: train_metrics: A MetricsTracker instance to record training statistics. policy: The policy model to be trained. batch: A batch of training data. optimizer: The optimizer used to update the policy\u0026#39;s parameters. grad_clip_norm: The maximum norm for gradient clipping. grad_scaler: The GradScaler for automatic mixed-precision training. lr_scheduler: An optional learning rate scheduler. use_amp: A boolean indicating whether to use automatic mixed precision. lock: An optional lock for thread-safe optimizer updates. Returns: A tuple containing: - The updated MetricsTracker with new statistics for this step. - A dictionary of outputs from the policy\u0026#39;s forward pass, for logging purposes. \u0026#34;\u0026#34;\u0026#34; start_time = time.perf_counter() device = get_device_from_parameters(policy) policy.train() with torch.autocast(device_type=device.type) if use_amp else nullcontext(): loss, output_dict = policy.forward(batch) # TODO(rcadene): policy.unnormalize_outputs(out_dict) grad_scaler.scale(loss).backward() # Unscale the gradient of the optimizer\u0026#39;s assigned params in-place **prior to gradient clipping**. # 在裁剪梯度之前反缩放优化器参数的梯度 grad_scaler.unscale_(optimizer) grad_norm = torch.nn.utils.clip_grad_norm_( policy.parameters(), grad_clip_norm, error_if_nonfinite=False, ) # Optimizer\u0026#39;s gradients are already unscaled, so scaler.step does not unscale them, # 优化器梯度已反缩放,scaler.step 不再反缩放 # although it still skips optimizer.step() if the gradients contain infs or NaNs. # 若梯度含 inf/NaN,会跳过 optimizer.step() with lock if lock is not None else nullcontext(): grad_scaler.step(optimizer) # Updates the scale for next iteration. grad_scaler.update() optimizer.zero_grad() # Step through pytorch scheduler at every batch instead of epoch # 每个 batch 调度学习率而非每个 epoch if lr_scheduler is not None: lr_scheduler.step() if has_method(policy, \u0026#34;update\u0026#34;): # To possibly update an internal buffer (for instance an Exponential Moving Average like in TDMPC). policy.update() train_metrics.loss = loss.item() train_metrics.grad_norm = grad_norm.item() train_metrics.lr = optimizer.param_groups[0][\u0026#34;lr\u0026#34;] train_metrics.update_s = time.perf_counter() - start_time return train_metrics, output_dict @parser.wrap() def train(cfg: TrainPipelineSwanLabConfig): \u0026#34;\u0026#34;\u0026#34; Main function to train a policy with SwanLab support. This function orchestrates the entire training pipeline, including: - Setting up logging, seeding, and device configuration. - Creating the dataset, evaluation environment (if applicable), policy, and optimizer. - Handling resumption from a checkpoint. - Running the main training loop, which involves fetching data batches and calling `update_policy`. - Periodically logging metrics, saving model checkpoints, and evaluating the policy. - Pushing the final trained model to the Hugging Face Hub if configured. - Supporting both WandB and SwanLab for experiment tracking. Args: cfg: A `TrainPipelineSwanLabConfig` object containing all training configurations. \u0026#34;\u0026#34;\u0026#34; cfg.validate() logging.info(pformat(cfg.to_dict())) # Initialize loggers based on tracker selection wandb_logger = None swanlab_logger = None if cfg.tracker in [\u0026#34;wandb\u0026#34;, \u0026#34;both\u0026#34;] and cfg.wandb.project: wandb_logger = WandBLogger(cfg) if cfg.tracker in [\u0026#34;swanlab\u0026#34;, \u0026#34;both\u0026#34;] and cfg.swanlab.project: swanlab_logger = SwanLabLogger(cfg) if cfg.tracker == \u0026#34;none\u0026#34; or (not wandb_logger and not swanlab_logger): logging.info(colored(\u0026#34;Logs will be saved locally.\u0026#34;, \u0026#34;yellow\u0026#34;, attrs=[\u0026#34;bold\u0026#34;])) if cfg.seed is not None: set_seed(cfg.seed) # Check device is available device = get_safe_torch_device(cfg.policy.device, log=True) torch.backends.cudnn.benchmark = True torch.backends.cuda.matmul.allow_tf32 = True logging.info(\u0026#34;Creating dataset\u0026#34;) # 正在创建数据集 dataset = make_dataset(cfg) # Create environment used for evaluating checkpoints during training on simulation data. # 用于在仿真训练中评估检查点 # On real-world data, no need to create an environment as evaluations are done outside train.py, # 真实数据上评估在 train.py 外进行 # using the eval.py instead, with gym_dora environment and dora-rs. # 使用 eval.py 与 gym_dora/dora-rs eval_env = None if cfg.eval_freq \u0026gt; 0 and cfg.env is not None: logging.info(\u0026#34;Creating env\u0026#34;) # 正在创建环境 eval_env = make_env(cfg.env, n_envs=cfg.eval.batch_size, use_async_envs=cfg.eval.use_async_envs) logging.info(\u0026#34;Creating policy\u0026#34;) # 正在创建策略 policy = make_policy( cfg=cfg.policy, ds_meta=dataset.meta, ) # Create processors - only provide dataset_stats if not resuming from saved processors processor_kwargs = {} postprocessor_kwargs = {} if (cfg.policy.pretrained_path and not cfg.resume) or not cfg.policy.pretrained_path: # Only provide dataset_stats when not resuming from saved processor state processor_kwargs[\u0026#34;dataset_stats\u0026#34;] = dataset.meta.stats if cfg.policy.pretrained_path is not None: processor_kwargs[\u0026#34;preprocessor_overrides\u0026#34;] = { \u0026#34;device_processor\u0026#34;: {\u0026#34;device\u0026#34;: device.type}, \u0026#34;normalizer_processor\u0026#34;: { \u0026#34;stats\u0026#34;: dataset.meta.stats, \u0026#34;features\u0026#34;: {**policy.config.input_features, **policy.config.output_features}, \u0026#34;norm_map\u0026#34;: policy.config.normalization_mapping, }, } postprocessor_kwargs[\u0026#34;postprocessor_overrides\u0026#34;] = { \u0026#34;unnormalizer_processor\u0026#34;: { \u0026#34;stats\u0026#34;: dataset.meta.stats, \u0026#34;features\u0026#34;: policy.config.output_features, \u0026#34;norm_map\u0026#34;: policy.config.normalization_mapping, }, } preprocessor, postprocessor = make_pre_post_processors( policy_cfg=cfg.policy, pretrained_path=cfg.policy.pretrained_path, **processor_kwargs, **postprocessor_kwargs, ) logging.info(\u0026#34;Creating optimizer and scheduler\u0026#34;) # 正在创建优化器与调度器 optimizer, lr_scheduler = make_optimizer_and_scheduler(cfg, policy) grad_scaler = GradScaler(device.type, enabled=cfg.policy.use_amp) step = 0 # number of policy updates (forward + backward + optim) if cfg.resume: step, optimizer, lr_scheduler = load_training_state(cfg.checkpoint_path, optimizer, lr_scheduler) num_learnable_params = sum(p.numel() for p in policy.parameters() if p.requires_grad) num_total_params = sum(p.numel() for p in policy.parameters()) logging.info(colored(\u0026#34;Output dir:\u0026#34;, \u0026#34;yellow\u0026#34;, attrs=[\u0026#34;bold\u0026#34;]) + f\u0026#34; {cfg.output_dir}\u0026#34;) # 输出目录 if cfg.env is not None: logging.info(f\u0026#34;{cfg.env.task=}\u0026#34;) logging.info(f\u0026#34;{cfg.steps=} ({format_big_number(cfg.steps)})\u0026#34;) logging.info(f\u0026#34;{dataset.num_frames=} ({format_big_number(dataset.num_frames)})\u0026#34;) logging.info(f\u0026#34;{dataset.num_episodes=}\u0026#34;) logging.info(f\u0026#34;{num_learnable_params=} ({format_big_number(num_learnable_params)})\u0026#34;) logging.info(f\u0026#34;{num_total_params=} ({format_big_number(num_total_params)})\u0026#34;) # create dataloader for offline training # 为离线训练创建数据加载器 if hasattr(cfg.policy, \u0026#34;drop_n_last_frames\u0026#34;): shuffle = False sampler = EpisodeAwareSampler( dataset.meta.episodes[\u0026#34;dataset_from_index\u0026#34;], dataset.meta.episodes[\u0026#34;dataset_to_index\u0026#34;], drop_n_last_frames=cfg.policy.drop_n_last_frames, shuffle=True, ) else: shuffle = True sampler = None dataloader = torch.utils.data.DataLoader( dataset, num_workers=cfg.num_workers, batch_size=cfg.batch_size, shuffle=shuffle and not cfg.dataset.streaming, sampler=sampler, pin_memory=device.type == \u0026#34;cuda\u0026#34;, drop_last=False, prefetch_factor=2, ) dl_iter = cycle(dataloader) policy.train() train_metrics = { \u0026#34;loss\u0026#34;: AverageMeter(\u0026#34;loss\u0026#34;, \u0026#34;:.3f\u0026#34;), \u0026#34;grad_norm\u0026#34;: AverageMeter(\u0026#34;grdn\u0026#34;, \u0026#34;:.3f\u0026#34;), \u0026#34;lr\u0026#34;: AverageMeter(\u0026#34;lr\u0026#34;, \u0026#34;:0.1e\u0026#34;), \u0026#34;update_s\u0026#34;: AverageMeter(\u0026#34;updt_s\u0026#34;, \u0026#34;:.3f\u0026#34;), \u0026#34;dataloading_s\u0026#34;: AverageMeter(\u0026#34;data_s\u0026#34;, \u0026#34;:.3f\u0026#34;), } train_tracker = MetricsTracker( cfg.batch_size, dataset.num_frames, dataset.num_episodes, train_metrics, initial_step=step ) logging.info(\u0026#34;Start offline training on a fixed dataset\u0026#34;) # 开始在固定数据集上进行离线训练 for _ in range(step, cfg.steps): start_time = time.perf_counter() batch = next(dl_iter) batch = preprocessor(batch) train_tracker.dataloading_s = time.perf_counter() - start_time train_tracker, output_dict = update_policy( train_tracker, policy, batch, optimizer, cfg.optimizer.grad_clip_norm, grad_scaler=grad_scaler, lr_scheduler=lr_scheduler, use_amp=cfg.policy.use_amp, ) # Note: eval and checkpoint happens *after* the `step`th training update has completed, so we # 评估与保存发生在完成该步更新之后 # increment `step` here. # 因此此处递增 step step += 1 train_tracker.step() is_log_step = cfg.log_freq \u0026gt; 0 and step % cfg.log_freq == 0 is_saving_step = step % cfg.save_freq == 0 or step == cfg.steps is_eval_step = cfg.eval_freq \u0026gt; 0 and step % cfg.eval_freq == 0 if is_log_step: logging.info(train_tracker) if wandb_logger: wandb_log_dict = train_tracker.to_dict() if output_dict: wandb_log_dict.update(output_dict) wandb_logger.log_dict(wandb_log_dict, step) if swanlab_logger: swanlab_log_dict = train_tracker.to_dict() if output_dict: swanlab_log_dict.update(output_dict) swanlab_logger.log_dict(swanlab_log_dict, step) train_tracker.reset_averages() if cfg.save_checkpoint and is_saving_step: logging.info(f\u0026#34;Checkpoint policy after step {step}\u0026#34;) # 保存检查点 checkpoint_dir = get_step_checkpoint_dir(cfg.output_dir, cfg.steps, step) save_checkpoint( checkpoint_dir, step, cfg, policy, optimizer, lr_scheduler, preprocessor, postprocessor ) update_last_checkpoint(checkpoint_dir) if wandb_logger: wandb_logger.log_policy(checkpoint_dir) if swanlab_logger: swanlab_logger.log_policy(checkpoint_dir) if cfg.env and is_eval_step: step_id = get_step_identifier(step, cfg.steps) logging.info(f\u0026#34;Eval policy at step {step}\u0026#34;) # 评估策略 with ( torch.no_grad(), torch.autocast(device_type=device.type) if cfg.policy.use_amp else nullcontext(), ): eval_info = eval_policy_all( envs=eval_env, # dict[suite][task_id] -\u0026gt; vec_env policy=policy, preprocessor=preprocessor, postprocessor=postprocessor, n_episodes=cfg.eval.n_episodes, videos_dir=cfg.output_dir / \u0026#34;eval\u0026#34; / f\u0026#34;videos_step_{step_id}\u0026#34;, max_episodes_rendered=4, start_seed=cfg.seed, max_parallel_tasks=cfg.env.max_parallel_tasks, ) # overall metrics (suite-agnostic) aggregated = eval_info[\u0026#34;overall\u0026#34;] # optional: per-suite logging for suite, suite_info in eval_info.items(): logging.info(\u0026#34;Suite %s aggregated: %s\u0026#34;, suite, suite_info) # meters/tracker eval_metrics = { \u0026#34;avg_sum_reward\u0026#34;: AverageMeter(\u0026#34;∑rwrd\u0026#34;, \u0026#34;:.3f\u0026#34;), \u0026#34;pc_success\u0026#34;: AverageMeter(\u0026#34;success\u0026#34;, \u0026#34;:.1f\u0026#34;), \u0026#34;eval_s\u0026#34;: AverageMeter(\u0026#34;eval_s\u0026#34;, \u0026#34;:.3f\u0026#34;), } eval_tracker = MetricsTracker( cfg.batch_size, dataset.num_frames, dataset.num_episodes, eval_metrics, initial_step=step ) eval_tracker.eval_s = aggregated.pop(\u0026#34;eval_s\u0026#34;) eval_tracker.avg_sum_reward = aggregated.pop(\u0026#34;avg_sum_reward\u0026#34;) eval_tracker.pc_success = aggregated.pop(\u0026#34;pc_success\u0026#34;) if wandb_logger: wandb_log_dict = {**eval_tracker.to_dict(), **eval_info} wandb_logger.log_dict(wandb_log_dict, step, mode=\u0026#34;eval\u0026#34;) wandb_logger.log_video(eval_info[\u0026#34;overall\u0026#34;][\u0026#34;video_paths\u0026#34;][0], step, mode=\u0026#34;eval\u0026#34;) if swanlab_logger: swanlab_log_dict = {**eval_tracker.to_dict(), **eval_info} swanlab_logger.log_dict(swanlab_log_dict, step, mode=\u0026#34;eval\u0026#34;) swanlab_logger.log_video(eval_info[\u0026#34;overall\u0026#34;][\u0026#34;video_paths\u0026#34;][0], step, mode=\u0026#34;eval\u0026#34;) if eval_env: close_envs(eval_env) logging.info(\u0026#34;End of training\u0026#34;) # 训练结束 if cfg.policy.push_to_hub: policy.push_model_to_hub(cfg) preprocessor.push_to_hub(cfg.policy.repo_id) postprocessor.push_to_hub(cfg.policy.repo_id) def main(): init_logging() train() if __name__ == \u0026#34;__main__\u0026#34;: main() 安装 SwanLab: BASH不换行 pip install swanlab 在官网登录后获得API Key,运行 swanlab login 登录 创建训练日志文件夹: BASH不换行 mkdir -p ./logs 开始训练(100000 步): BASH不换行 stdbuf -oL -eL nohup python -m lerobot.extra.lerobot_train_swanlab \\ --dataset.repo_id=xiadengma/so101-red-pepper \\ --dataset.root=./data/datasets/xiadengma/so101-red-pepper \\ --policy.type=act \\ --output_dir=./data/train/act_so101_red_pepper \\ --job_name=act_so101_red_pepper_$(date +%Y%m%d_%H%M%S) \\ --policy.device=cuda \\ --wandb.enable=false \\ --policy.push_to_hub=false \\ --steps=100000 \\ --tracker=swanlab \\ --swanlab.project=so101-red-pepper \\ --swanlab.mode=cloud \\ \u0026gt; ./logs/train_$(date +\u0026#34;%Y-%m-%d-%H-%M-%S\u0026#34;).log 2\u0026gt;\u0026amp;1 \u0026amp; echo $! \u0026gt; ./logs/train.pid 恢复训练: BASH不换行 stdbuf -oL -eL nohup lerobot-train \\ --config_path=./data/train/act_so101_red_pepper/checkpoints/last/pretrained_model/train_config.json \\ --resume=true \\ \u0026gt; ./logs/resume_$(date +\u0026#34;%Y-%m-%d-%H-%M-%S\u0026#34;).log 2\u0026gt;\u0026amp;1 \u0026amp; echo $! \u0026gt; ./logs/train.pid 查看最新日志: BASH不换行 tail -f $(ls -t ./logs/*.log | head -n 1) 中断训练: BASH不换行 kill -TERM $(cat ./logs/train.pid) || kill -KILL $(cat ./logs/train.pid) 运行推断并评估 # 运行推断 BASH不换行 python -m lerobot.extra.lerobot_record_web \\ --robot.type=so101_follower \\ --robot.port=/dev/follower_arm \\ --robot.cameras=\u0026#34;{ wrist_left: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, front_rgb: {type: opencv, index_or_path: 8, width: 640, height: 480, fps: 30}}\u0026#34; \\ --robot.id=my_follower_arm \\ --robot.calibration_dir=./data/calibration \\ --display_data=false \\ --dataset.repo_id=xiadengma/eval_so101-red-pepper \\ --dataset.single_task=\u0026#34;Put the red pepper toy in the cardboard box\u0026#34; \\ --policy.path=./data/train/act_so101_red_pepper/checkpoints/last/pretrained_model \\ --policy.device=cuda \\ --dataset.root=./data/datasets/xiadengma/eval_so101-red-pepper \\ --web_port=9090 \\ --dataset.episode_time_s=30 \\ --dataset.reset_time_s=0 \\ --dataset.num_episodes=10 评估 对于数据集要求高 ACT 只针对特定任务,无泛化能力 ACT推理演示视频(3个回合,抓取红辣椒玩偶并放入纸箱) 您的浏览器不支持 video 标签。 88. 参考资料 # 点击展开查看参考资料 LeRobot 仓库 官方-安装文档 官方-so101 文档 seeedstudio-基于 LeRobot 的 SO-ARM100 and SO-ARM101 机械臂入门教程 wowrobo-摄像头连接教程 wowrobo-LeRobot 具身智能机械臂实操入门课程 ","date":"2025年10月7日","externalUrl":null,"permalink":"/posts/lerobot-so-101-%E6%9C%BA%E6%A2%B0%E8%87%82%E5%AE%9E%E8%B7%B5/","section":"文章","summary":"","title":"LeRobot SO-101 机械臂实践","type":"posts"},{"content":"","date":"2025年10月7日","externalUrl":null,"permalink":"/tags/so-101/","section":"Tags","summary":"","title":"SO-101","type":"tags"},{"content":"","date":"2025年9月19日","externalUrl":null,"permalink":"/tags/algorithm/","section":"Tags","summary":"","title":"Algorithm","type":"tags"},{"content":"","date":"2025年9月19日","externalUrl":null,"permalink":"/tags/kalman-filter/","section":"Tags","summary":"","title":"Kalman Filter","type":"tags"},{"content":" 0. 写在开头 # 点进这篇博客的读者应该都是对卡尔曼滤波有所兴趣的,或者在工作中遇到卡尔曼滤波的问题,这里我就不介绍卡尔曼滤波的背景了,直接介绍最基础的卡尔曼滤波(KF)。\n%%{init: {'theme':'base', 'themeVariables': { 'edgeLabelBackground':'#ffffff', 'lineColor':'#d0d0d0'}}}%% graph TB A[\"卡尔曼滤波家族------贝叶斯递推框架预测 → 更新\"] A -.-\u003e B[\"线性高斯系统\"] A -.-\u003e C[\"非线性高斯系统\"] A -.-\u003e D[\"集合方法\"] A -.-\u003e E[\"非高斯/任意分布\"] A -.-\u003e F[\"多模型方法\"] A -.-\u003e G[\"自适应与鲁棒方法\"] B -.-\u003e B1[\"KF标准卡尔曼滤波\"] B -.-\u003e B2[\"IF信息滤波\"] B -.-\u003e B3[\"RTS固定间隔平滑器\"] C -.-\u003e C1[\"近似线性化\"] C -.-\u003e C2[\"无迹变换\"] C -.-\u003e C3[\"数值稳定\"] C1 -.-\u003e C11[\"EKF扩展卡尔曼一阶线性化\"] C1 -.-\u003e C12[\"IEKF迭代扩展卡尔曼\"] C2 -.-\u003e C21[\"UKF无迹卡尔曼Sigma点\"] C2 -.-\u003e C22[\"CKFCubature卡尔曼球积规则\"] C3 -.-\u003e C31[\"SR-KF平方根卡尔曼\"] C3 -.-\u003e C32[\"SR-UKF平方根无迹卡尔曼\"] D -.-\u003e D1[\"EnKF集合卡尔曼样本协方差\"] D -.-\u003e D2[\"EnKS集合平滑器\"] D1 -.-\u003e D11[\"随机EnKF\"] D1 -.-\u003e D12[\"确定性EnKF\"] E -.-\u003e E1[\"PF粒子滤波SIS/SIR\"] E -.-\u003e E2[\"RBPF边缘化粒子滤波\"] E2 -.-\u003e E21[\"线性部分: KF\"] E2 -.-\u003e E22[\"非线性部分: 粒子\"] F -.-\u003e F1[\"IMM交互多模型\"] F1 -.-\u003e F11[\"IMM-KF\"] F1 -.-\u003e F12[\"IMM-EKF\"] F1 -.-\u003e F13[\"IMM-UKF\"] G -.-\u003e G1[\"AKF自适应卡尔曼在线估参\"] G -.-\u003e G2[\"H∞滤波最坏情况界\"] G -.-\u003e G3[\"鲁棒KFM-estimator\"] classDef level0 fill:#4A90E2,stroke:#2E5C8A,stroke-width:2.5px,color:#fff,font-weight:bold classDef level1 fill:#7CB9E8,stroke:#5A8FC7,stroke-width:2px,color:#000 classDef level2 fill:#B3D9F2,stroke:#8AB8E0,stroke-width:1.5px,color:#000 classDef level3 fill:#E6F2FA,stroke:#B8D9EE,stroke-width:1px,color:#333 class A level0 class B,C,D,E,F,G level1 class B1,B2,B3,C1,C2,C3,D1,D2,E1,E2,F1,G1,G2,G3 level2 class C11,C12,C21,C22,C31,C32,D11,D12,E21,E22,F11,F12,F13 level3 卡尔曼滤波,用直白的话来讲,就是:多个不确定的结果,经过分析、推理和计算,获得相对准确的结果。\n多个是指数据来源可以是模型推理得出,也可以是通过仪器测量获得。 不确定是指由于模型本身是一种近似,或者是测量仪器本身的精度误差,或者测量过程不可避免地引入了噪声,甚至因为所需要的特征无法直接获取,只能间接推导获得。 分析、推理和计算,则指的是卡尔曼滤波算法,也是本文接下来将会重点阐述的部分。 相对准确,指的是经过卡尔曼滤波算法获得的结果,比原有的多个不确定的结果更逼近客观真实值,但依然存在误差。 原理 # 数学基础与符号约定 # 下面的内容不需要在事前理解,只需要在遇到新内容的时候查询即可。\n加粗的小写字母表示向量(通常为列向量),如 $\\mathbf{x}_{k}$、$\\mathbf{u}_{k}$。 加粗的大写字母表示矩阵,如 $\\mathbf{F}$、$\\mathbf{B}$、$\\mathbf{H}$、$\\mathbf{Q}$、$\\mathbf{R}$。 头顶为^的字母表示估计值(后验估计),如 $\\hat{\\mathbf{x}}_{k}$。 右上角为-的字母表示预测值(先验估计),如 $\\mathbf{x}_{k}^{-}$。 期望(表示数据分布的中心位置):对于离散随机向量 $\\mathbf{X}$,其期望为 $E[\\mathbf{X}] = \\sum_{i=1}^{n} \\mathbf{X}_{i} P(\\mathbf{X}_{i})$(连续情形为积分形式)。 协方差矩阵(表示数据分布的不确定性和形状,在卡尔曼滤波中度量后验估计值的精确程度):对于随机向量 $\\mathbf{X}$,其协方差矩阵为 $Cov(\\mathbf{X}) = E[(\\mathbf{X} - E[\\mathbf{X}])(\\mathbf{X} - E[\\mathbf{X}])^{T}]$。 先验:指在获得新证据之前,对未知事件的预先判断、信念或概率。 后验:指在获得新证据之后,将新证据与先验判断结合,得出的更新后的判断、信念或概率。 转置:对于矩阵 $\\mathbf{A}$,其转置为 $\\mathbf{A}^{T}$。 单位矩阵:$\\mathbf{I}$,是方阵,且对角线上的元素为 1,其余元素为 0。 迹:对于矩阵 $\\mathbf{A}$,其迹为 $tr(\\mathbf{A}) = \\sum_{i=1}^{n} \\mathbf{A}_{ii}$。 逆:对于方阵 $\\mathbf{A}$,其逆矩阵记为 $\\mathbf{A}^{-1}$,满足 $\\mathbf{A}\\mathbf{A}^{-1} = \\mathbf{A}^{-1}\\mathbf{A} = \\mathbf{I}$。 基本模型 # (线性)卡尔曼滤波的应用基于以下三个假设前提:\n马尔可夫性:当前时刻状态只和上一时刻状态有关。 线性模型:系统的状态转移和观测过程均满足线性关系。 高斯噪声:过程噪声和测量噪声都符合高斯分布。 基于上述假设,我们可以得到如下两个公式:\n$$ 过程模型: \\mathbf{x}_{k} = \\mathbf{F}_{k} \\mathbf{x}_{k-1} + \\mathbf{B}_{k} \\mathbf{u}_{k} + \\mathbf{w}_{k-1} \\quad\\quad ① $$$$ 观测模型: \\mathbf{z}_{k} = \\mathbf{H}_{k} \\mathbf{x}_{k} + \\mathbf{v}_{k} \\quad\\quad ② $$其中:\n$\\mathbf{x}_{k}$ 表示 $k$ 时刻的真实状态值,是待估计的未知量; $\\mathbf{x}_{k-1}$ 表示 $k-1$ 时刻的真实状态值; $\\mathbf{u}_{k}$ 表示 $k$ 时刻的控制输入量(作用于区间 $(k-1, k]$),描述在 $k-1$ 时刻至 $k$ 时刻这段时间里生效的输入; $\\mathbf{w}_{k-1}$ 表示过程噪声,其均值为 $0$,协方差矩阵为 $\\mathbf{Q}_{k-1}$,即 $\\mathbf{w}_{k-1} \\sim N(0, \\mathbf{Q}_{k-1})$; $\\mathbf{z}_{k}$ 表示 $k$ 时刻的观测值,是已知的测量量; $\\mathbf{v}_{k}$ 表示观测噪声,其均值为 $0$,协方差矩阵为 $\\mathbf{R}_{k}$,即 $\\mathbf{v}_{k} \\sim N(0, \\mathbf{R}_{k})$; $\\mathbf{F}_{k}$ 表示状态转移矩阵,描述状态如何从 $k-1$ 时刻演化到 $k$ 时刻; $\\mathbf{B}_{k}$ 表示控制矩阵,描述区间 $(k-1, k]$ 内生效的控制输入 $\\mathbf{u}_{k}$ 如何影响 $k$ 时刻的状态; $\\mathbf{H}_{k}$ 表示观测矩阵,描述系统的 $k$ 时刻真实状态 $\\mathbf{x}_{k}$ 与我们得到的测量值 $\\mathbf{z}_{k}$ 之间的关系; 然而现实中,我们无法得到真实的噪声 $\\mathbf{w}_{k-1}$ 和 $\\mathbf{v}_{k}$ ,因此即便我们建立了精确的模型,也无法得到准确的真实状态值 $\\mathbf{x}_{k}$。所以我们希望找到一个最优估计值 $\\hat{\\mathbf{x}}_{k}$ 来尽可能地逼近 $\\mathbf{x}_{k}$,使得估计误差最小。\n预测步骤:推导先验估计值 # 根据过程模型 ① $\\mathbf{x}_{k} = \\mathbf{F}_{k} \\mathbf{x}_{k-1} + \\mathbf{B}_{k} \\mathbf{u}_{k} + \\mathbf{w}_{k-1}$,我们可以对 $k$ 时刻的状态进行预测。\n由于我们无法得知上一时刻的真实状态 $\\mathbf{x}_{k-1}$ 和当前的过程噪声 $\\mathbf{w}_{k-1}$,因此我们使用上一时刻的最优估计值 $\\hat{\\mathbf{x}}_{k-1}$ 来代替 $\\mathbf{x}_{k-1}$。同时,由于过程噪声 $\\mathbf{w}_{k-1}$ 的期望为零,在预测时我们取其期望值,即 $E[\\mathbf{w}_{k-1}]=0$。\n由此,我们得到 $k$ 时刻状态的先验估计值(预测值) $\\mathbf{x}_{k}^{-}$ 的计算公式:\n$$ \\mathbf{x}_{k}^{-} = \\mathbf{F}_{k} \\hat{\\mathbf{x}}_{k-1} + \\mathbf{B}_{k} \\mathbf{u}_{k} $$其中:\n$\\mathbf{x}_{k}^{-}$ 表示 $k$ 时刻状态的预测值,也叫先验估计值(因为它是在获得 $k$ 时刻测量值之前计算的); $\\hat{\\mathbf{x}}_{k-1}$ 表示 $k-1$ 时刻的最优估计值(后验估计值); $\\mathbf{u}_{k}$ 表示 $k$ 时刻的控制输入量(作用于区间 $(k-1, k]$); $\\mathbf{F}_{k}$ 表示状态转移矩阵,描述真实状态 $\\mathbf{x}$ 如何从 $k-1$ 时刻演化到 $k$ 时刻; $\\mathbf{B}_{k}$ 表示控制矩阵,描述在区间 $(k-1, k]$ 生效的控制输入 $\\mathbf{u}_{k}$ 如何影响 $k$ 时刻的状态 $\\mathbf{x}$; 这个预测值 $\\mathbf{x}_{k}^{-}$ 仅仅基于系统的动态模型和上一时刻的状态估计,它没有包含当前 $k$ 时刻的任何测量信息。因此,它是一个有待修正的初步估计。下一步,我们将利用 $k$ 时刻的测量值 $\\mathbf{z}_{k}$ 来修正这个预测值,以获得更精确的后验估计值 $\\hat{\\mathbf{x}}_{k}$。\n更新步骤:融合测量值修正预测 # 在预测步骤中,我们得到了一个 $k$ 时刻的先验预测值 $\\mathbf{x}_{k}^{-}$。这个值仅基于系统模型和上一时刻的状态,尚未利用 $k$ 时刻的测量数据。现在,我们的任务是利用 $k$ 时刻的实际测量值 $\\mathbf{z}_{k}$ 来修正这个预测,从而得到一个更准确的后验估计值 $\\hat{\\mathbf{x}}_{k}$。\n修正的核心思路是比较“实际测量”与“预测的测量”之间的差距。\n$k$ 时刻的实际测量值:$\\mathbf{z}_{k}$ $k$ 时刻的预测测量值:$\\mathbf{H}_{k}\\mathbf{x}_{k}^{-}$(这是将状态预测值 $\\mathbf{x}_{k}^{-}$ 通过观测模型 $\\mathbf{H}_{k}$ 映射到测量空间得到的值) $k$ 时刻的实际测量值 $\\mathbf{z}_{k}$ 与 $k$ 时刻的预测测量值 $\\mathbf{H}_{k}\\mathbf{x}_{k}^{-}$ 之差,被称为测量残差(Innovation) $Y_{k}$:\n$$ Y_{k} = \\mathbf{z}_{k} - \\mathbf{H}_{k}\\mathbf{x}_{k}^{-} $$这个测量残差 $Y_{k}$ 反映了我们的预测与现实的差距。如果残差很小,说明预测准确;如果残差很大,则说明预测存在较大偏差,需要进行显著修正。\n卡尔曼滤波通过一个核心的状态更新方程来完成修正,该方程将最终的后验估计值表示为先验预测值与加权后的测量残差之和:\n$$ 状态更新方程: \\hat{\\mathbf{x}}_{k} = \\mathbf{x}_{k}^{-} + \\mathbf{K}_{k}(\\mathbf{z}_{k} - \\mathbf{H}_{k}\\mathbf{x}_{k}^{-}) \\quad\\quad ③ $$其中:\n$\\hat{\\mathbf{x}}_{k}$ 是 $k$ 时刻的最优估计值(或称后验估计值),因为它融合了 $k$ 时刻的测量信息。 $\\mathbf{K}_{k}$ 是卡尔曼增益。它的核心作用是一个权重矩阵,用于权衡我们应该在多大程度上相信“预测值”和“测量值”。 如果测量噪声很大(即我们不信任测量值),$\\mathbf{K}_{k}$ 的值会变小,我们就更倾向于相信预测值 $\\mathbf{x}_{k}^{-}$。 如果预测本身很不确定(即我们不信任预测值),$\\mathbf{K}_{k}$ 的值会变大,我们就更倾向于利用测量信息进行大幅修正。 至此,状态更新的逻辑已经建立。即从 $k-1$ 时刻的后验估计值 $\\hat{\\mathbf{x}}_{k-1}$ 经过 $k$ 时刻状态的先验估计值(预测值) $\\mathbf{x}_{k}^{-}$ 到 $k$ 时刻的最优估计值 $\\hat{\\mathbf{x}}_{k}$ 的更新过程已经完成。\n但卡尔曼增益 $\\mathbf{K}_{k}$ 还是未知的,后续的推导将致力于如何计算这个使估计误差最小的最优卡尔曼增益 $\\mathbf{K}_{k}$。\n目标函数的建立与转化 # 将上述思路转化为数学语言,我们首先定义后验估计误差 $\\mathbf{e}_{k} = \\mathbf{x}_{k} - \\hat{\\mathbf{x}}_{k}$,即 $k$ 时刻真实状态值与后验估计值的差。卡尔曼滤波的目标是寻找一个最优的增益矩阵 $\\mathbf{K}_k$ 来最小化该误差的均方值 $E[||\\mathbf{e}_k||^2]$。\n这个均方误差可以通过后验误差协方差矩阵 $\\mathbf{P}_{k}$ 的迹来表示,其中 $\\mathbf{P}_{k}$ 定义为:\n$$ \\mathbf{P}_{k} = E[\\mathbf{e}_{k}\\mathbf{e}_{k}^{T}] $$它们之间的关系如下:\n$$ E[||\\mathbf{e}_k||^2] = E[\\mathbf{e}_k^T \\mathbf{e}_k] = \\text{tr}(E[\\mathbf{e}_k \\mathbf{e}_k^T]) = \\text{tr}(\\mathbf{P}_k) $$因此,优化目标函数可以明确地表示为:\n$$ \\min_{\\mathbf{K}_k}{\\text{tr}(\\mathbf{P}_{k})} $$为了求解这个最小化问题,我们需要推导出 $\\mathbf{P}_{k}$ 关于 $\\mathbf{K}_{k}$ 的具体表达式。这需要我们先从 $\\mathbf{e}_{k}$ 的表达式入手。根据状态更新方程 ③ $\\hat{\\mathbf{x}}_{k} = \\mathbf{x}_{k}^{-} + \\mathbf{K}_{k}(\\mathbf{z}_{k} - \\mathbf{H}_{k}\\mathbf{x}_{k}^{-})$ 和观测模型 ② $\\mathbf{z}_{k} = \\mathbf{H}_{k} \\mathbf{x}_{k} + \\mathbf{v}_{k}$,我们有:\n$$ \\begin{aligned} \\mathbf{e}_{k} \u0026= \\mathbf{x}_{k} - \\hat{\\mathbf{x}}_{k} \\\\ \u0026= \\mathbf{x}_{k} - \\left( \\mathbf{x}_{k}^{-} + \\mathbf{K}_{k}(\\mathbf{z}_{k} - \\mathbf{H}_{k}\\mathbf{x}_{k}^{-}) \\right) \\\\ \u0026= (\\mathbf{x}_{k} - \\mathbf{x}_{k}^{-}) - \\mathbf{K}_{k}(\\mathbf{H}_{k}\\mathbf{x}_{k} + \\mathbf{v}_{k}-\\mathbf{H}_{k}\\mathbf{x}_{k}^{-} ) \\\\ \u0026= (\\mathbf{x}_{k} - \\mathbf{x}_{k}^{-}) - \\mathbf{K}_{k}\\mathbf{H}_{k}(\\mathbf{x}_{k} - \\mathbf{x}_{k}^{-}) - \\mathbf{K}_{k}\\mathbf{v}_{k} \\\\ \u0026= (\\mathbf{I} - \\mathbf{K}_{k}\\mathbf{H}_{k})(\\mathbf{x}_{k} - \\mathbf{x}_{k}^{-} ) - \\mathbf{K}_{k}\\mathbf{v}_{k} \\end{aligned} $$为简化表示,我们引入先验估计误差 $\\mathbf{e}_{k}^{-} = \\mathbf{x}_{k} - \\mathbf{x}_{k}^{-}$($k$ 时刻真实状态值与先验估计值的差),则上式变为:\n$$ \\mathbf{e}_{k} = (\\mathbf{I} - \\mathbf{K}_{k}\\mathbf{H}_{k})\\mathbf{e}_{k}^{-} - \\mathbf{K}_{k}\\mathbf{v}_{k} \\quad\\quad ④ $$现在,我们可以利用公式 ④ 来计算后验误差协方差矩阵 $\\mathbf{P}_{k}$。在这个过程中,我们还需要用到先验误差协方差矩阵 $\\mathbf{P}_{k}^{-}$,其定义为:\n$$ \\mathbf{P}_{k}^{-} = E[\\mathbf{e}_{k}^{-}\\mathbf{e}_{k}^{-T}] $$根据协方差矩阵的定义,$\\mathbf{P}_{k}$ 和 $\\mathbf{P}_{k}^{-}$ 都是对称矩阵。对公式 ④ 两边乘以其自身的转置,并取期望,可得:\n$$ \\begin{aligned} \\mathbf{P}_{k} \u0026= E[\\mathbf{e}_{k} \\mathbf{e}_{k}^{T}] \\\\ \u0026= E\\!\\left[ \\big((\\mathbf{I} - \\mathbf{K}_{k}\\mathbf{H}_{k})\\mathbf{e}_{k}^{-} - \\mathbf{K}_{k}\\mathbf{v}_{k}\\big) \\big((\\mathbf{I} - \\mathbf{K}_{k}\\mathbf{H}_{k})\\mathbf{e}_{k}^{-} - \\mathbf{K}_{k}\\mathbf{v}_{k}\\big)^{T} \\right] \\\\ \u0026= (\\mathbf{I} - \\mathbf{K}_{k}\\mathbf{H}_{k})\\mathbf{P}_{k}^{-}(\\mathbf{I} - \\mathbf{K}_{k}\\mathbf{H}_{k})^{T} + \\mathbf{K}_{k}\\mathbf{R}_{k}\\mathbf{K}_{k}^{T} \\\\ \u0026= \\mathbf{P}_{k}^{-} - \\mathbf{K}_{k}\\mathbf{H}_{k}\\mathbf{P}_{k}^{-} - \\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}\\mathbf{K}_{k}^{T} + \\mathbf{K}_{k}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k})\\mathbf{K}_{k}^{T} \\quad\\quad ⑤ \\end{aligned} $$其中,$\\mathbf{R}_{k}$ 是测量噪声协方差。\n至此,我们将随机变量的最优化问题转化成为了关于矩阵 $\\mathbf{K}_k$ 的函数最小化问题。很多文章在此直接对后验误差协方差矩阵 $\\mathbf{P}_{k}$ 取迹,然后对卡尔曼增益 $\\mathbf{K}_k$ 求导,并令其为零,以获得使 $\\text{tr}(\\mathbf{P}_{k})$ 最小的 $\\mathbf{K}_k$ 值。如此做的理由有以下三点:\n$\\text{tr}(\\mathbf{P}_{k})$ 在物理上表示系统状态估计误差的总方差,即均方误差 $E[||\\mathbf{e}_k||^2]$。最小化迹就是最小化整体的估计误差能量,这是一个非常直观且合理的优化目标。 $\\text{tr}(\\mathbf{P}_{k})$ 是关于 $\\mathbf{K}_k$ 的一个标量函数,并且可以证明它是一个二次函数,因此是凸函数,存在唯一的全局最小值。 对标量函数 $\\text{tr}(\\mathbf{P}_{k})$ 进行矩阵求导($\\frac{\\partial \\text{tr}(\\mathbf{P}_{k})}{\\partial \\mathbf{K}_k}$)比直接对矩阵 $\\mathbf{P}_{k}$ 求导要简单得多,并且有成熟的矩阵迹求导法则(如 $\\frac{\\partial \\text{tr}(AB)}{\\partial A} = B^T$)可以使用。 上述三点的扩展证明 $ \\text{tr}(\\mathbf{P}_{k}) = \\sum_{i=1}^{n} E[(x_{ik}-\\hat{x}_{ik})^{2}] $\n$\\text{tr}(\\mathbf{P}_{k})$ 表示的是后验误差协方差矩阵 $\\mathbf{P}_{k}$ 主对角线元素之和,在估计无偏时,恰好是状态向量所有分量估计误差的均方误差(Mean Squared Error)之和。在卡尔曼滤波中,我们的目标是找到最优的增益 $K_k$,使得这个均方误差总和最小,这是一个典型的最小均方误差(MMSE)优化问题。\n为了更直观地理解,我们将 $\\mathbf{P}_{k}$ 以矩阵形式展开(假设状态向量维度为 $n$):\n$$ \\mathbf{P}_{k}=\\begin{pmatrix} E[(x_{1k}-\\hat{x}_{1k} )^{2}] \u0026 E[(x_{1k}-\\hat{x}_{1k} )(x_{2k}-\\hat{x}_{2k} )] \u0026 \\cdots \u0026 E[(x_{1k}-\\hat{x}_{1k} )(x_{nk}-\\hat{x}_{nk} )] \\\\ E[(x_{2k}-\\hat{x}_{2k} )(x_{1k}-\\hat{x}_{1k} )] \u0026 E[(x_{2k}-\\hat{x}_{2k} )^{2}] \u0026 \\cdots \u0026 E[(x_{2k}-\\hat{x}_{2k} )(x_{nk}-\\hat{x}_{nk} )] \\\\ \\vdots \u0026 \\vdots \u0026 \\ddots \u0026 \\vdots \\\\ E[(x_{nk}-\\hat{x}_{nk} )(x_{1k}-\\hat{x}_{1k} )] \u0026 E[(x_{nk}-\\hat{x}_{nk} )(x_{2k}-\\hat{x}_{2k} )] \u0026 \\cdots \u0026 E[(x_{nk}-\\hat{x}_{nk} )^{2}]\\end{pmatrix} $$ 标量函数(迹)对矩阵求导的基础知识\n我们来推导 $\\frac{\\partial \\text{tr}(\\mathbf{A}\\mathbf{X})}{\\partial \\mathbf{X}} = \\mathbf{A}^{T}$。设 $\\mathbf{A}$、$\\mathbf{X}$ 均为 $n \\times n$ 的方阵,且 $\\mathbf{A}$ 与 $\\mathbf{X}$ 无关。\n根据矩阵乘法和迹的定义,我们有:\n$$ \\text{tr}(\\mathbf{A}\\mathbf{X}) = \\sum_{i=1}^{n} (\\mathbf{A}\\mathbf{X})_{ii} = \\sum_{i=1}^{n} \\sum_{j=1}^{n} a_{ij}x_{ji} $$标量函数 $f(\\mathbf{X})$ 对矩阵 $\\mathbf{X}$ 的导数定义如下(按分母布局):\n$$ \\frac{\\partial f(\\mathbf{X})}{\\partial \\mathbf{X} } =\\begin{pmatrix} \\frac{\\partial f}{\\partial x_{11}} \u0026\\frac{\\partial f}{\\partial x_{12}} \u0026\\cdots \u0026\\frac{\\partial f}{\\partial x_{1n}} \\\\ \\frac{\\partial f}{\\partial x_{21}} \u0026 \\frac{\\partial f}{\\partial x_{22}}\u0026 \\cdots \u0026\\frac{\\partial f}{\\partial x_{2n}} \\\\ \\vdots \u0026 \\vdots \u0026 \\ddots \u0026 \\vdots \\\\ \\frac{\\partial f}{\\partial x_{n1}} \u0026 \\frac{\\partial f}{\\partial x_{n2}} \u0026\\cdots \u0026 \\frac{\\partial f}{\\partial x_{nn}} \\end{pmatrix} $$现在,我们求迹 $\\text{tr}(\\mathbf{A}\\mathbf{X})$ 对 $\\mathbf{X}$ 的任意一个元素 $x_{kl}$ 的偏导数:\n$$ \\frac{\\partial \\text{tr}(\\mathbf{A}\\mathbf{X})}{\\partial x_{kl}} = \\frac{\\partial}{\\partial x_{kl}} \\left( \\sum_{i=1}^{n} \\sum_{j=1}^{n} a_{ij}x_{ji} \\right) $$在这个双重求和中,变量 $x_{kl}$ 只在 $j=k$ 且 $i=l$ 这一项中出现,该项为 $a_{lk}x_{kl}$。因此,对 $x_{kl}$ 求导的结果就是它的系数 $a_{lk}$。\n所以,导数矩阵的第 $k$ 行第 $l$ 列的元素为:\n$$ \\left(\\frac{\\partial \\text{tr}(\\mathbf{A}\\mathbf{X})}{\\partial \\mathbf{X}}\\right)_{kl} = a_{lk} = (\\mathbf{A}^T)_{kl} $$由于这对所有的 $k, l$ 都成立,我们得到:\n$$ \\frac{\\partial \\text{tr}(\\mathbf{A}\\mathbf{X})}{\\partial \\mathbf{X}} = \\mathbf{A}^{T} $$ 类比 2 的推导过程,我们不加证明地给出以下关于迹的求导结论:\n结论 1: $\\frac{\\partial \\text{tr}(\\mathbf{A}\\mathbf{X})}{\\partial \\mathbf{X}} = \\mathbf{A}^{T}$ 结论 2: $\\frac{\\partial \\text{tr}(\\mathbf{X}\\mathbf{A})}{\\partial \\mathbf{X}} = \\mathbf{A}^{T}$(利用 $\\text{tr}(\\mathbf{X}\\mathbf{A})=\\text{tr}(\\mathbf{A}\\mathbf{X})$) 结论 3: $\\frac{\\partial \\text{tr}(\\mathbf{X}\\mathbf{A}\\mathbf{X}^{T})}{\\partial \\mathbf{X}} = \\mathbf{X}(\\mathbf{A}+\\mathbf{A}^{T})$ 结论 4: $\\frac{\\partial \\text{tr}(\\mathbf{X}^{T}\\mathbf{A}\\mathbf{X})}{\\partial \\mathbf{X}} = (\\mathbf{A}+\\mathbf{A}^{T})\\mathbf{X}$ 结论 5: 对于任何一个作为 $\\mathbf{X}$ 函数的矩阵 $\\mathbf{P}(\\mathbf{X})$,恒有 $\\frac{\\partial \\text{tr}(\\mathbf{P})}{\\partial \\mathbf{X}} = \\frac{\\partial \\text{tr}(\\mathbf{P}^{T})}{\\partial \\mathbf{X}}$,因为矩阵的迹等于其转置的迹。 卡尔曼增益求解和协方差矩阵化简 # 根据上述扩展知识,我们求解最优卡尔曼增益 $\\mathbf{K}_{k}$,\n$$ \\begin{align} 令 \\frac{\\partial \\text{tr}(\\mathbf{P}_{k})}{\\partial \\mathbf{K}_{k}} \u0026= \\frac{\\partial \\text{tr}(\\mathbf{P}_{k}^{-}) }{\\partial \\mathbf{K}_{k}} -\\frac{\\partial \\text{tr}(\\mathbf{K}_{k}\\mathbf{H}_{k}\\mathbf{P}_{k}^{-})}{\\partial \\mathbf{K}_{k}}-\\frac{\\partial \\text{tr}(\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}\\mathbf{K}_{k}^{T})}{\\partial \\mathbf{K}_{k}}+\\frac{\\partial \\text{tr}(\\mathbf{K}_{k}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k})\\mathbf{K}_{k}^{T})}{\\partial \\mathbf{K}_{k}} \\\\ \u0026= 0 - (\\mathbf{H}_{k}\\mathbf{P}_{k}^{-})^{T} - (\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}) + 2\\mathbf{K}_{k}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k}) \\\\ \u0026= -2\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T} + 2\\mathbf{K}_{k}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k}) \\\\ \u0026= 0 \\end{align} $$我们得到\n$$ \\mathbf{K}_{k} = \\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k})^{-1} \\quad\\quad ⑥ $$其中, $\\mathbf{R}_{k}$ 是测量噪声协方差。\n代入到后验误差协方差公式 $\\mathbf{P}_{k} = (\\mathbf{I}-\\mathbf{K}_{k}\\mathbf{H}_{k})\\mathbf{P}_{k}^{-}(\\mathbf{I}-\\mathbf{K}_{k}\\mathbf{H}_{k})^T + \\mathbf{K}_{k}\\mathbf{R}_{k}\\mathbf{K}_{k}^T$ 中,展开后可得:$\\mathbf{P}_{k} = \\mathbf{P}_{k}^{-} - \\mathbf{K}_{k}\\mathbf{H}_{k}\\mathbf{P}_{k}^{-} - \\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}\\mathbf{K}_{k}^{T} + \\mathbf{K}_{k}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k})\\mathbf{K}_{k}^{T}$。\n利用公式 ⑥ 的结论 $\\mathbf{K}_{k}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k}) = \\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}$,对上式进行化简,则有后验误差协方差矩阵:\n$$ \\begin{align} \\mathbf{P}_{k} \u0026= \\mathbf{P}_{k}^{-} - \\mathbf{K}_{k}\\mathbf{H}_{k}\\mathbf{P}_{k}^{-} - \\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}\\mathbf{K}_{k}^{T} + (\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T})\\mathbf{K}_{k}^{T} \\\\ \u0026= \\mathbf{P}_{k}^{-} - \\mathbf{K}_{k}\\mathbf{H}_{k}\\mathbf{P}_{k}^{-} \\\\ \u0026= (\\mathbf{I}-\\mathbf{K}_{k}\\mathbf{H}_{k})\\mathbf{P}_{k}^{-} \\quad\\quad ⑦ \\end{align} $$借鉴上述整个推导过程,对于由运动模型得到的 $k$ 时刻状态的先验估计值(预测值)公式 $\\mathbf{x}_{k}^{-} = \\mathbf{F}_{k} \\hat{\\mathbf{x}}_{k-1} + \\mathbf{B}_{k}\\mathbf{u}_{k}$。\n为了构建先验误差 $\\mathbf{e}_{k}^{-}$,按定义两式相减:\n$$ \\begin{align} \\mathbf{e}_{k}^{-} \u0026= \\mathbf{x}_{k}-\\mathbf{x}_{k}^{-} \\\\ \u0026= \\mathbf{x}_{k}-\\mathbf{F}_{k} \\hat{\\mathbf{x}}_{k-1} - \\mathbf{B}_{k}\\mathbf{u}_{k} \\\\ \u0026= \\mathbf{F}_{k} \\mathbf{x}_{k-1} + \\mathbf{B}_{k}\\mathbf{u}_{k} +\\mathbf{w}_{k-1}-\\mathbf{F}_{k} \\hat{\\mathbf{x}}_{k-1} - \\mathbf{B}_{k}\\mathbf{u}_{k} \\\\ \u0026= \\mathbf{F}_{k}( \\mathbf{x}_{k-1}-\\hat{\\mathbf{x}}_{k-1})+\\mathbf{w}_{k-1} \\end{align} $$即\n$$ \\mathbf{e}_{k}^{-} = \\mathbf{F}_{k}\\mathbf{e}_{k-1}+\\mathbf{w}_{k-1} $$类似构建后验误差协方差矩阵 $\\mathbf{P}_{k}$ 的方式,同样构建先验误差协方差矩阵 $\\mathbf{P}_{k}^{-}$,两边同时乘以自身的转置并取期望,再考虑到 $\\mathbf{w}_{k-1}$ 和 $\\mathbf{e}_{k-1}$ 互相独立(即 $E[\\mathbf{F}_{k}\\mathbf{e}_{k-1}\\mathbf{w}_{k-1}^T]=0$),有\n$$ \\begin{aligned} E[\\mathbf{e}_{k}^{-}\\mathbf{e}_{k}^{-T}] \u0026= E\\!\\left[(\\mathbf{F}_{k}\\mathbf{e}_{k-1}+\\mathbf{w}_{k-1})(\\mathbf{F}_{k}\\mathbf{e}_{k-1}+\\mathbf{w}_{k-1})^{T}\\right] \\\\ \u0026= \\mathbf{F}_{k}\\,E[\\mathbf{e}_{k-1}\\mathbf{e}_{k-1}^{T}]\\,\\mathbf{F}_{k}^{T}+E[\\mathbf{w}_{k-1}\\mathbf{w}_{k-1}^{T}] \\end{aligned} $$则有先验误差协方差矩阵:\n$$ \\mathbf{P}_{k}^{-}= \\mathbf{F}_{k}\\mathbf{P}_{k-1}\\mathbf{F}_{k}^{T}+\\mathbf{Q}_{k-1} \\quad\\quad ⑧ $$其中, $\\mathbf{Q}_{k-1}$ 为过程噪声协方差。\n至此,我们已经获得了完整的卡尔曼滤波预测、更新的公式。\n梳理 # 我们对上文做一个系统性的总结。\n首先,我们对实际问题进行建模,获得运动模型和观测模型:\n运动模型: $\\mathbf{x}_{k} = \\mathbf{F}_{k} \\mathbf{x}_{k-1} + \\mathbf{B}_{k}\\mathbf{u}_{k} +\\mathbf{w}_{k-1} \\quad\\quad ①$\n观测模型: $\\mathbf{z}_{k} = \\mathbf{H}_{k} \\mathbf{x}_{k} +\\mathbf{v}_{k} \\quad\\quad ②$\n其中,$\\mathbf{w}_{k-1}$ 和 $\\mathbf{v}_{k}$ 分别是过程噪声和观测噪声,它们是互不相关的高斯白噪声,其协方差矩阵分别为 $\\mathbf{Q}_{k-1}$ 和 $\\mathbf{R}_{k}$。\n其次,我们通过无偏估计的假设和误差定义,获得最优估计值和协方差矩阵的表达式(更新步骤):\n状态更新: $\\hat{\\mathbf{x}}_{k} = \\mathbf{x}_{k}^{-} + \\mathbf{K}_{k}(\\mathbf{z}_{k}-\\mathbf{H}_{k}\\mathbf{x}_{k}^{-} ) \\quad\\quad ③$\n后验误差: $\\mathbf{e}_{k} = (\\mathbf{I} -\\mathbf{K}_{k}\\mathbf{H}_{k})\\mathbf{e}_{k}^{-} - \\mathbf{K}_{k}\\mathbf{v}_{k} \\quad\\quad ④$\n后验误差协方差矩阵(展开形式): $\\mathbf{P}_{k} = E[\\mathbf{e}_{k}\\mathbf{e}_{k}^{T}]= \\mathbf{P}_{k}^{-} - \\mathbf{K}_{k}\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}-\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}\\mathbf{K}_{k}^{T} + \\mathbf{K}_{k}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k})\\mathbf{K}_{k}^{T} \\quad\\quad ⑤$\n再次,我们通过最小化后验误差协方差矩阵 $\\mathbf{P}_{k}$ 的迹,推导出卡尔曼增益:\n卡尔曼增益: $\\mathbf{K}_{k}=\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k})^{-1} \\quad\\quad ⑥$\n将上式代入 ⑤ 并化简,可得后验误差协方差矩阵的更简洁形式:\n后验误差协方差矩阵(常用形式): $\\mathbf{P}_{k} = (\\mathbf{I}-\\mathbf{K}_{k}\\mathbf{H}_{k})\\mathbf{P}_{k}^{-} \\quad\\quad ⑦$\n最后,我们进行预测步骤,计算下一时刻的先验量:\n先验误差协方差矩阵: $\\mathbf{P}_{k}^{-}= \\mathbf{F}_{k}\\mathbf{P}_{k-1}\\mathbf{F}_{k}^{T}+\\mathbf{Q}_{k-1} \\quad\\quad ⑧$\n状态预测: $\\mathbf{x}_{k}^{-} = \\mathbf{F}_{k} \\hat{\\mathbf{x}}_{k-1} + \\mathbf{B}_{k}\\mathbf{u}_{k} \\quad\\quad ⑨$\n先验误差: $\\mathbf{e}_{k}^{-} = \\mathbf{F}_{k}\\mathbf{e}_{k-1}+\\mathbf{w}_{k-1}$\n总结 # 那么,卡尔曼滤波就可以按下面的 5 个公式理解并说明:\n实现过程:使用上一次的最优结果预测出当前值,同时使用当前观测值修正,得到当前最优结果。\n我们将卡尔曼滤波的完整流程总结为两个阶段:预测和更新。\n预测\n预测状态 $$ \\mathbf{x}_{k}^{-} = \\mathbf{F}_{k} \\hat{\\mathbf{x}}_{k-1} + \\mathbf{B}_{k} \\mathbf{u}_{k} \\quad\\quad (1) $$ 预测先验误差协方差矩阵 $$ \\mathbf{P}_{k}^{-} = \\mathbf{F}_{k}\\mathbf{P}_{k-1}\\mathbf{F}_{k}^{T} + \\mathbf{Q}_{k-1} \\quad\\quad (2) $$ 更新\n计算卡尔曼增益 $$ \\mathbf{K}_{k} = \\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}(\\mathbf{H}_{k}\\mathbf{P}_{k}^{-}\\mathbf{H}_{k}^{T}+\\mathbf{R}_{k})^{-1} \\quad\\quad (3) $$ 更新状态估计 $$ \\hat{\\mathbf{x}}_{k} = \\mathbf{x}_{k}^{-} + \\mathbf{K}_{k}(\\mathbf{z}_{k} - \\mathbf{H}_{k}\\mathbf{x}_{k}^{-}) \\quad\\quad (4) $$ 更新后验误差协方差矩阵 $$ \\mathbf{P}_{k} = (\\mathbf{I}-\\mathbf{K}_{k}\\mathbf{H}_{k})\\mathbf{P}_{k}^{-} \\quad\\quad (5) $$ 在设定好系统的模型参数(状态转移矩阵:$\\mathbf{F}_k$、控制矩阵:$\\mathbf{B}_k$、观测矩阵:$\\mathbf{H}_k$)和噪声协方差(过程噪声协方差:$\\mathbf{Q}_k$,测量噪声协方差:$\\mathbf{R}_k$)后,这个预测-更新的循环从初始状态 $\\hat{\\mathbf{x}}_0$ 和初始误差协方差矩阵 $\\mathbf{P}_0$ 开始,不断迭代,实现对系统状态的实时最优估计。\n实践 # 调节超参数 # 过程噪声协方差:$\\mathbf{Q}_k$ 和测量噪声协方差:$\\mathbf{R}_k$\n初始状态 $\\hat{\\mathbf{x}}_0$ 和初始误差协方差矩阵 $\\mathbf{P}_0$\n卡尔曼滤波的使用步骤 # 选择状态量、观测量 构建方程 初始化参数($\\mathbf{Q}_k$、$\\mathbf{R}_k$、$\\hat{\\mathbf{x}}_0$(一般取 0)、$\\mathbf{P}_0$(一般取 1,不可为 0)) 代入公式迭代 调节超参数($\\mathbf{Q}_k$、$\\mathbf{R}_k$) 案例分析一:一维时序信号的追踪与滤波 # 本节将以一个常见的工程问题——从带噪声的观测中恢复纯净的正弦信号——为例,详细演示如何为特定问题构建卡尔曼滤波模型。核心挑战在于,正弦波本身并非一个简单的动态系统,我们需要通过数学推导,将其转换为卡尔曼滤波框架所要求的线性状态空间形式。\n对应代码和演示视频 # 请直接下载: 01_kalman_filter_sine_wave.ipynb\n卡尔曼滤波正弦波跟踪演示(Q=1.0e-05, R=0.16) 您的浏览器不支持 video 标签。 第一步:建立信号的动态模型 # 卡尔曼滤波要求系统满足马尔可夫性,即当前状态仅与上一状态有关。幸运的是,一个纯净的、离散采样的正弦波,其未来值 $y_{t+1}$ 可以由它当前和过去的值 $y_t$, $y_{t-1}$ 精确地线性表示。我们的首要任务就是找到这个线性递推关系。\n我们从离散正弦波的定义出发:\n$$ y_t = A \\sin(\\omega t + \\phi) $$其中 $\\omega$ 是数字角频率( $\\omega = 2\\pi f / f_s$ ,f 是信号频率, $f_s$ 是采样频率)。\n我们写出 $t+1$ 和 $t-1$ 时刻的表达式:\n$y_{t+1} = A \\sin(\\omega(t+1) + \\phi) = A \\sin((\\omega t+\\phi) + \\omega)$ $y_{t-1} = A \\sin(\\omega(t-1) + \\phi) = A \\sin((\\omega t+\\phi) - \\omega)$ 使用三角函数的和角公式 $\\sin(\\alpha \\pm \\beta) = \\sin(\\alpha)\\cos(\\beta) \\pm \\cos(\\alpha)\\sin(\\beta)$ 展开上述两式:\n$y_{t+1} = A [\\sin(\\omega t+\\phi)\\cos(\\omega) + \\cos(\\omega t+\\phi)\\sin(\\omega)]$ $y_{t-1} = A [\\sin(\\omega t+\\phi)\\cos(\\omega) - \\cos(\\omega t+\\phi)\\sin(\\omega)]$ 将这两个式子相加,$\\cos(\\omega t+\\phi)\\sin(\\omega)$ 项会正负抵消:\n$$ y_{t+1} + y_{t-1} = 2 A \\sin(\\omega t+\\phi) \\cos(\\omega) $$注意到 $A \\sin(\\omega t+\\phi)$ 正好就是 $y_t$,将其代入上式:\n$$ y_{t+1} + y_{t-1} = 2 y_t \\cos(\\omega) $$整理得到 $y_{t+1}$ 的表达式,这便是我们需要的线性递推关系:\n$$ y_{t+1} = 2\\cos(\\omega) y_t - y_{t-1} $$这个公式是建模的关键。它揭示了正弦信号内在的、二阶的线性动态特性,为我们构建状态转移模型铺平了道路。\n第二步:构建状态空间方程 # 我们得到的递推关系是二阶的(依赖于 $y_t$ 和 $y_{t-1}$ ),而标准卡尔曼滤波的状态转移是一阶的($\\mathbf{x}_k = \\mathbf{F}_k \\mathbf{x}_{k-1} + \\dots$)。解决这个问题的经典方法是“状态增广”:通过增加状态向量的维度,将高阶系统转化为一阶系统。\n定义状态向量 $\\mathbf{x}_t$ 为实现一阶递推,状态向量必须包含计算 $y_{t+1}$ 所需的所有历史信息,即 $y_t$ 和 $y_{t-1}$。因此,我们定义 $t$ 时刻的状态向量为:\n$$ \\mathbf{x}_t = \\begin{bmatrix} y_t \\\\ y_{t-1} \\end{bmatrix} $$ 构建状态转移矩阵 $\\mathbf{F}$ 我们的目标是找到一个 2x2 矩阵 $\\mathbf{F}$,使得 $\\mathbf{x}_{t+1} = \\mathbf{F} \\mathbf{x}_t$ 成立。首先写出 $\\mathbf{x}_{t+1}$ 的定义:\n$$ \\mathbf{x}_{t+1} = \\begin{bmatrix} y_{t+1} \\\\ y_t \\end{bmatrix} $$现在,将状态方程展开:\n$$ \\begin{bmatrix} y_{t+1} \\\\ y_t \\end{bmatrix} = \\begin{bmatrix} F_{11} \u0026 F_{12} \\\\ F_{21} \u0026 F_{22} \\end{bmatrix} \\begin{bmatrix} y_t \\\\ y_{t-1} \\end{bmatrix} $$我们逐行确定 $\\mathbf{F}$ 的元素:\n第一行:$y_{t+1} = F_{11} y_t + F_{12} y_{t-1}$ 根据第一步的递推关系 $y_{t+1} = (2\\cos(\\omega)) y_t + (-1) y_{t-1}$,可得: $F_{11} = 2\\cos(\\omega)$ 和 $F_{12} = -1$ 第二行:$y_t = F_{21} y_t + F_{22} y_{t-1}$ 这是一个恒等关系:$t+1$ 时刻状态的第二个元素($y_t$)就是 $t$ 时刻状态的第一个元素。所以: $y_t = (1) y_t + (0) y_{t-1}$ 可得:$F_{21} = 1$ 和 $F_{22} = 0$ 组合起来,我们就得到了状态转移矩阵 $\\mathbf{F}$: $$ \\mathbf{F} = \\begin{bmatrix} 2\\cos(\\omega) \u0026 -1 \\\\ 1 \u0026 0 \\end{bmatrix} $$ 第三步:定义观测模型 # 模型建立的最后一步是关联我们定义的内部状态与外部的实际测量值。\n定义观测量 $\\mathbf{z}_t$ 在我们的问题中,每次的测量值 $\\mathbf{z}_t$ 就是带有噪声的、当前时刻的正弦波幅值。\n构建观测矩阵 $\\mathbf{H}$ 我们需要一个矩阵 $\\mathbf{H}$,它能从状态向量 $\\mathbf{x}_t = \\begin{bmatrix} y_t \\\\ y_{t-1} \\end{bmatrix}$ 中“提取”出我们理论上要测量的值 $y_t$。\n$$ \\mathbf{z}_t \\approx \\mathbf{H} \\mathbf{x}\\_t = \\begin{bmatrix} H_1 \u0026 H_2 \\end{bmatrix} \\begin{bmatrix} y_t \\\\ y_{t-1} \\end{bmatrix} = H_1 y_t + H_2 y_{t-1} $$为了让 $\\mathbf{H} \\mathbf{x}_t$ 等于 $y_t$,我们只需令 $H_1 = 1$ 和 $H_2 = 0$。因此,观测矩阵 $\\mathbf{H}$ 是:\n$$ \\mathbf{H} = \\begin{bmatrix} 1 \u0026 0 \\end{bmatrix} $$ 模型总结 # 至此,我们成功地将一个看似复杂的正弦波信号,转化为了标准的线性高斯系统模型,可以完美地套用卡尔曼滤波的五个核心公式。\n过程模型:$\\mathbf{x}_{k} = \\mathbf{F} \\mathbf{x}_{k-1} + \\mathbf{w}_{k-1}$\n状态向量 $\\mathbf{x}_{k} = \\begin{bmatrix} y_k \\\\ y_{k-1} \\end{bmatrix}$,代表信号当前和上一时刻的真实值。 状态转移矩阵 $\\mathbf{F} = \\begin{bmatrix} 2\\cos(\\omega) \u0026 -1 \\\\ 1 \u0026 0 \\end{bmatrix}$,描述了正弦波的内在演化规律。 过程噪声 $\\mathbf{w}_{k-1}$ 代表了模型的不确定性(例如频率的微小漂移),其协方差为 $\\mathbf{Q}$。 观测模型:$\\mathbf{z}_{k} = \\mathbf{H} \\mathbf{x}_{k} + \\mathbf{v}_{k}$\n观测矩阵 $\\mathbf{H} = \\begin{bmatrix} 1 \u0026 0 \\end{bmatrix}$,表示我们只能直接测量到信号的当前值。 观测噪声 $\\mathbf{v}_{k}$ 代表了传感器的测量误差,其协方差为 $\\mathbf{R}$。 在实际应用中,我们只需根据信号的频率确定 $\\omega$ 并设置好 $\\mathbf{F}$ 矩阵,再根据经验调节噪声协方差 $\\mathbf{Q}$ 和 $\\mathbf{R}$,就可以实现对带噪正弦波的高效滤波。\n案例分析二:二维平面运动轨迹的估计与预测 # 在许多实际应用中,如雷达跟踪、视频监控或机器人导航,我们都需要根据一系列不精确的观测点来估计目标的真实运动轨迹。本节将以一个在二维平面上做近似匀加速运动的目标为例,展示如何构建卡尔曼滤波器来实时估计其精确的位置和速度,并对未来位置进行预测。\n对应代码 # 请直接下载: 02_kalman_filter_2d_tracking.ipynb\n第一步:建立目标的动态模型 # 我们假设目标在每个采样时间间隔 $\\Delta t$ 内,其加速度是近似恒定的,但会受到一些随机扰动(例如,轻微的机动、空气阻力变化等)。基于牛顿运动学定律,我们可以建立目标的状态模型。\n定义状态向量 $\\mathbf{x}_k$ 为了完整描述目标在二维平面上的运动状态,我们需要知道它的位置和速度。因此,状态向量应包含 $x$ 和 $y$ 两个方向上的位置和速度分量:\n$$ \\mathbf{x}_k = \\begin{bmatrix} p_{x,k} \\\\ p_{y,k} \\\\ v_{x,k} \\\\ v_{y,k} \\end{bmatrix} $$其中,$p$ 代表位置 (position),$v$ 代表速度 (velocity)。\n构建状态转移矩阵 $\\mathbf{F}$ 状态转移矩阵 $\\mathbf{F}$ 描述了在没有噪声的情况下,状态如何从 $k-1$ 时刻演化到 $k$ 时刻。根据匀加速运动公式:\n$p_k = p_{k-1} + v_{k-1}\\Delta t + \\frac{1}{2}a_{k-1}\\Delta t^2$ $v_k = v_{k-1} + a_{k-1}\\Delta t$ 为简化模型,我们通常将加速度作为过程噪声的一部分来处理,而不是直接放入状态向量。这被称为恒速模型 (Constant Velocity, CV),它假设在每个 $\\Delta t$ 内速度恒定。这个模型足以应对大多数机动性不强的目标。\n$p_{x,k} = p_{x,k-1} + v_{x,k-1}\\Delta t$ $p_{y,k} = p_{y,k-1} + v_{y,k-1}\\Delta t$ $v_{x,k} = v_{x,k-1}$ $v_{y,k} = v_{y,k-1}$ 将上述关系写成矩阵形式 $\\mathbf{x}_k = \\mathbf{F} \\mathbf{x}_{k-1}$:\n$$ \\begin{bmatrix} p_{x,k} \\\\ p_{y,k} \\\\ v_{x,k} \\\\ v_{y,k} \\end{bmatrix} = \\begin{bmatrix} 1 \u0026 0 \u0026 \\Delta t \u0026 0 \\\\ 0 \u0026 1 \u0026 0 \u0026 \\Delta t \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} \\begin{bmatrix} p_{x,k-1} \\\\ p_{y,k-1} \\\\ v_{x,k-1} \\\\ v_{y,k-1} \\end{bmatrix} $$因此,状态转移矩阵为:\n$$ \\mathbf{F} = \\begin{bmatrix} 1 \u0026 0 \u0026 \\Delta t \u0026 0 \\\\ 0 \u0026 1 \u0026 0 \u0026 \\Delta t \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} $$ 定义过程噪声协方差矩阵 $\\mathbf{Q}$ 恒速模型是一个近似。目标的实际运动会因为未建模的加速度(随机扰动)而偏离该模型。过程噪声 $\\mathbf{w}_{k-1}$ 正是用来描述这种不确定性。\n一个常用的方法是假设加速度是一个零均值、方差为 $\\sigma_a^2$ 的随机过程。加速度在 $\\Delta t$ 时间内对位置和速度的影响分别是 $\\frac{1}{2}a\\Delta t^2$ 和 $a\\Delta t$。经过推导,可以得到一个适用于离散时间系统的过程噪声协方差矩阵 $\\mathbf{Q}$:\n$$ \\mathbf{Q} = \\begin{bmatrix} \\frac{\\Delta t^4}{4} \u0026 0 \u0026 \\frac{\\Delta t^3}{2} \u0026 0 \\\\ 0 \u0026 \\frac{\\Delta t^4}{4} \u0026 0 \u0026 \\frac{\\Delta t^3}{2} \\\\ \\frac{\\Delta t^3}{2} \u0026 0 \u0026 \\Delta t^2 \u0026 0 \\\\ 0 \u0026 \\frac{\\Delta t^3}{2} \u0026 0 \u0026 \\Delta t^2 \\end{bmatrix} \\sigma_a^2 $$其中 $\\sigma_a^2$ 是一个可调节的超参数,代表了我们对目标机动能力(即加速度变化剧烈程度)的预估。$\\sigma_a^2$ 越大,滤波器对模型预测的信任度就越低,从而更依赖于测量值。\n第二步:定义观测模型 # 接下来,我们需要建立内部状态与外部测量之间的关系。\n定义观测量 $\\mathbf{z}_k$ 假设我们的传感器(如 GPS 或摄像头)只能直接测量目标的位置,而无法直接测量其速度。因此,观测向量为:\n$$ \\mathbf{z}_k = \\begin{bmatrix} p_{x, measured} \\\\ p_{y, measured} \\end{bmatrix} $$ 构建观测矩阵 $\\mathbf{H}$ 观测矩阵 $\\mathbf{H}$ 的作用是从状态向量 $\\mathbf{x}_k$ 中提取出与观测量相对应的部分。\n$$ \\mathbf{z}_k \\approx \\mathbf{H} \\mathbf{x}_k = \\mathbf{H} \\begin{bmatrix} p_{x,k} \\\\ p_{y,k} \\\\ v_{x,k} \\\\ v_{y,k} \\end{bmatrix} $$为了提取出位置信息 $(p_{x,k}, p_{y,k})$,$\\mathbf{H}$ 矩阵应为:\n$$ \\mathbf{H} = \\begin{bmatrix} 1 \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 1 \u0026 0 \u0026 0 \\end{bmatrix} $$ 定义测量噪声协方差矩阵 $\\mathbf{R}$ 任何传感器都存在测量误差。我们假设 $x$ 和 $y$ 方向的测量噪声是独立的,并且都服从高斯分布,其方差分别为 $\\sigma_x^2$ 和 $\\sigma_y^2$。这些值通常可以从传感器的规格手册中获得。\n$$ \\mathbf{R} = \\begin{bmatrix} \\sigma_x^2 \u0026 0 \\\\ 0 \u0026 \\sigma_y^2 \\end{bmatrix} $$ 模型总结 # 通过以上步骤,我们为二维目标跟踪问题构建了完整的卡尔曼滤波模型:\n过程模型:$\\mathbf{x}_{k} = \\mathbf{F} \\mathbf{x}_{k-1} + \\mathbf{w}_{k-1}$\n状态向量 $\\mathbf{x}_{k}$ 包含了位置和速度信息。 状态转移矩阵 $\\mathbf{F}$ 基于恒速运动模型。 过程噪声协方差 $\\mathbf{Q}$ 反映了目标随机机动的可能性。 观测模型:$\\mathbf{z}_{k} = \\mathbf{H} \\mathbf{x}_{k} + \\mathbf{v}_{k}$\n观测矩阵 $\\mathbf{H}$ 表明我们只能测量到位置。 测量噪声协方差 $\\mathbf{R}$ 代表了传感器的精度。 将这些矩阵代入卡尔曼滤波的五个核心公式,我们就可以通过迭代,从一系列带噪声的位置观测中,得到平滑的、包含速度信息的状态估计,从而实现对目标轨迹的精确跟踪和未来位置的短期预测。\n案例分析三:三维空间中的刚体姿态跟踪问题 # 前面两个案例展示了卡尔曼滤波在线性系统中的强大威力。然而,当我们将目光投向更复杂的现实世界问题,如无人机飞行姿态、机器人手臂末端朝向或卫星在轨姿态的估计时,我们会遇到一个根本性的挑战: 旋转运动本质上是非线性的。\n本节将探讨为什么三维姿态跟踪问题无法直接套用标准卡尔曼滤波,并简述解决此类问题的基本思路,以此展示标准 KF 的局限性,并引出卡尔曼滤波家族中的非线性分支。\n第一步:姿态的数学表示 # 与位置可以用简单的向量 $(x, y, z)$ 表示不同,描述三维空间中的姿态(旋转)要复杂得多。常用的表示方法有:\n欧拉角(Euler Angles):如滚转-俯仰-偏航角 $(\\phi, \\theta, \\psi)$。这种表示直观,但在特定姿态(如俯仰角为 ±90°)时会出现万向节死锁(Gimbal Lock)问题,导致奇异性,无法正确表示旋转。 旋转矩阵(Rotation Matrix):一个 3x3 的正交矩阵 $\\mathbf{C}$。它没有奇异性,但有 9 个参数,而姿态只有 3 个自由度,这带来了冗余和约束($\\mathbf{C}^T\\mathbf{C} = \\mathbf{I}, \\det(\\mathbf{C})=1$),计算复杂。 四元数(Quaternion):一个四维向量 $\\mathbf{q} = [q_w, q_x, q_y, q_z]^T$,满足范数为 1 的约束($||\\mathbf{q}||=1$)。它既没有奇异性,也比旋转矩阵更紧凑,计算效率高,是姿态动力学中最常用的表示方法。 无论选择哪种表示,姿态的演化规律都无法用一个简单的线性方程 $\\mathbf{x}_k = \\mathbf{F}\\mathbf{x}_{k-1}$ 来描述。\n第二步:非线性的根源——姿态运动学 # 刚体的姿态变化率(角速度)与姿态本身的关系是非线性的。以四元数为例,其随时间变化的微分方程为:\n$$ \\dot{\\mathbf{q}}(t) = \\frac{1}{2} \\mathbf{\\Omega}(\\boldsymbol{\\omega}(t)) \\mathbf{q}(t) $$其中,$\\mathbf{q}(t)$ 是姿态四元数,$\\boldsymbol{\\omega}(t) = [\\omega_x, \\omega_y, \\omega_z]^T$ 是机体坐标系下的角速度向量,而 $\\mathbf{\\Omega}(\\boldsymbol{\\omega})$ 是一个与角速度相关的斜对称矩阵:\n$$ \\mathbf{\\Omega}(\\boldsymbol{\\omega}) = \\begin{bmatrix} 0 \u0026 -\\omega_x \u0026 -\\omega_y \u0026 -\\omega_z \\\\ \\omega_x \u0026 0 \u0026 \\omega_z \u0026 -\\omega_y \\\\ \\omega_y \u0026 -\\omega_z \u0026 0 \u0026 \\omega_x \\\\ \\omega_z \u0026 \\omega_y \u0026 -\\omega_x \u0026 0 \\end{bmatrix} $$这是一个典型的非线性微分方程,因为状态的导数 $\\dot{\\mathbf{q}}$ 是状态 $\\mathbf{q}$ 和输入 $\\boldsymbol{\\omega}$ 的乘积。将其离散化后,得到的状态转移方程 $\\mathbf{q}_k = f(\\mathbf{q}_{k-1}, \\boldsymbol{\\omega}_{k-1})$ 也是一个复杂的非线性函数,无法表示为 $\\mathbf{F}\\mathbf{q}_{k-1}$ 的形式。\n第三步:非线性的观测模型 # 姿态的观测通常也呈非线性。例如,无人机通过加速度计和磁力计来确定姿态:\n加速度计:在静止时测量重力向量 $\\mathbf{g}$ 的方向。 磁力计:测量地球磁场向量 $\\mathbf{m}$ 的方向。 当无人机姿态为 $\\mathbf{q}$ 时,它在机体坐标系下观测到的重力向量 $\\mathbf{a}_{body}$ 和磁场向量 $\\mathbf{m}_{body}$ 是通过姿态将世界坐标系下的参考向量 $\\mathbf{g}_{world}$ 和 $\\mathbf{m}_{world}$ 旋转得到的。这个旋转操作本身就是非线性的。观测模型可以写为:\n$$ \\mathbf{z}_k = \\begin{bmatrix} \\mathbf{a}_{body} \\\\ \\mathbf{m}_{body} \\end{bmatrix} = h(\\mathbf{q}_k) + \\mathbf{v}_k = \\begin{bmatrix} \\mathbf{C}(\\mathbf{q}_k)^T \\mathbf{g}_{world} \\\\ \\mathbf{C}(\\mathbf{q}_k)^T \\mathbf{m}_{world} \\end{bmatrix} + \\mathbf{v}_k $$其中 $\\mathbf{C}(\\mathbf{q}_k)$ 是从四元数 $\\mathbf{q}_k$ 转换得到的旋转矩阵。显然,这里的观测函数 $h(\\cdot)$ 是一个复杂的非线性函数,无法用一个固定的观测矩阵 $\\mathbf{H}$ 来表示。\n挑战与解决思路 # 由于过程模型 $f(\\cdot)$ 和观测模型 $h(\\cdot)$ 都是非线性的,标准卡尔曼滤波的基石——线性假设——被打破了。高斯分布经过非线性变换后不再是高斯分布,这使得协方差矩阵的传播和更新公式失效。\n为了解决这个问题,工程师和数学家们发展出了卡尔曼滤波的非线性扩展版本:\n扩展卡尔曼滤波 (Extended Kalman Filter, EKF)\n核心思想:在当前状态的估计值附近,使用泰勒级数展开将非线性函数 $f(\\cdot)$ 和 $h(\\cdot)$ 线性化。具体来说,是用它们的雅可比矩阵 (Jacobian Matrix) 来代替标准 KF 中的 $\\mathbf{F}$ 和 $\\mathbf{H}$ 矩阵。 优点:实现相对简单,计算量适中。 缺点:线性化会引入截断误差,仅在系统“近似线性”时效果较好。对于强非线性系统,可能会导致滤波发散。 无迹卡尔曼滤波 (Unscented Kalman Filter, UKF)\n核心思想:放弃直接对非线性函数进行线性化,而是采用一种更巧妙的方法——无迹变换 (Unscented Transform)。它通过一组精心选择的“Sigma 点”来近似状态的概率分布,将这些点通过真实的非线性函数传递,然后重新计算变换后的均值和协方差。 优点:精度通常比 EKF 高(至少到二阶),无需计算复杂的雅可比矩阵,对强非线性系统更鲁棒。 缺点:计算量比 EKF 稍大,参数选择(如 Sigma 点的分布参数)需要一些经验。 总结 # 三维姿态跟踪问题是通向高级滤波算法的门户。它深刻地揭示了标准卡尔曼滤波的适用边界,并清晰地指明了学习路径:当面临非线性系统时,我们需要从 EKF 和 UKF 等工具箱中寻找解决方案。这正是卡尔曼滤波家族如此庞大且充满活力的原因所在。\n结语 # 至此,我们已经完整地走过了标准卡尔曼滤波(KF)的“深入浅出”之旅。从其作为贝叶斯滤波在线性高斯系统下的特例出发,我们详细推导了预测与更新两大阶段的五个核心公式。通过一维正弦波追踪和二维平面运动跟踪这两个经典案例,我们不仅巩固了理论知识,更学会了如何将实际问题抽象、建模为卡尔曼滤波能够处理的状态空间形式。\n然而,正如第三个案例“三维空间姿态跟踪”所揭示的,现实世界中充满了非线性问题。当系统的状态转移或观测过程无法用简单的矩阵乘法来描述时,标准卡尔曼滤波的线性假设便不再成立,其性能也会急剧下降甚至发散。\n这恰恰是卡尔曼滤波家族精彩的延伸所在。为了应对非线性挑战,研究者们提出了多种近似最优的解决方案,其中最负盛名的便是扩展卡尔曼滤波 (Extended Kalman Filter, EKF) 和无迹卡尔曼滤波 (Unscented Kalman Filter, UKF) 。\n希望本文为你打开了通往状态估计理论的大门。在“常见算法深入浅出”系列的下一篇文章中,我们将继续深入探索 EKF 与 UKF 的世界,解开它们在非线性系统中的滤波之谜。\n参考资料 # 点击展开查看参考资料 深入浅出理解卡尔曼滤波【实例、公式、代码和图】 wikipedia-zh-卡尔曼滤波 从放弃到精通!卡尔曼滤波从理论到实践~ GitHub-pykalman 解密卡尔曼滤波(Kalman Filter)算法:深入解析卡尔曼滤波算法原理与在线可视化实例 DR_CAN 卡尔曼滤波器系列视频 ","date":"2025年9月19日","externalUrl":null,"permalink":"/posts/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%8D%A1%E5%B0%94%E6%9B%BC%E6%BB%A4%E6%B3%A2/","section":"文章","summary":"","title":"深入浅出卡尔曼滤波","type":"posts"},{"content":"","date":"2025年9月19日","externalUrl":null,"permalink":"/series/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95/","section":"Series","summary":"","title":"深入浅出常见算法","type":"series"},{"content":" 一、求极限入门 # 1. 高中常用数列公式 # 等差数列前 $n$ 项和: $1+2+3+\\cdots+n=\\frac{n(n+1)}{2}$ 等比数列前 $n$ 项和: $a_1+a_1q+\\cdots+a_1q^{n-1}=\\frac{a_1(1-q^n)}{1-q}$ 等比数列前 $n$ 项和的极限 $(|q|\u003c1)$: $$\\lim_{n \\to \\infty}(a_1+a_1q+\\cdots+a_1q^{n-1})=\\lim_{n \\to \\infty}\\frac{a_1(1-q^n)}{1-q}=\\frac{a_1}{1-q}\\quad(|q|\u003c1)$$ 2. 无穷小量 # 若 $\\lim_{x \\to x_0}f(x)=0$,则称 $f(x)$ 是 $x \\to x_0$ 时的无穷小。\n3. 涉及无穷大、无穷小的运算 # 涉及无穷大,仅“无穷大乘无穷大”确定为无穷大; 其他情况正常算\n额外说明:数字0和极限0(无穷小量) $\\lim_{x \\to \\infty} (0 \\cdot x) = 0$,其中$(0 \\cdot x)$中的 0 是数字 0 而$\\lim_{x \\to 0}(x \\cdot \\ln x) = 0$,其中$x$是极限 0(无穷小量),整个极限为“$0 \\cdot \\infty$”未定型,需要额外计算 4. 解极限每一步前先“判断极限类型” # 7 种未定型: $\\frac{0}{0}$ $\\frac{\\infty}{\\infty}$ $\\infty - \\infty$ $0 \\cdot \\infty$ $1^\\infty$ $0^0$ $\\infty^0$ 已定式 代入得结果 5. 极限的四则运算 # 当$x \\to x_0$时,若$\\lim_{x \\to x_0}f(x)=A$,$\\lim_{x \\to x_0}g(x)=B$,则可正常四则运算\n6. 极限拆分 # 理论:两个极限都存在才能拆分\n实际解题极限可拆:\n两项相加减 都存在 一个存在,一个无穷大 两项相乘除 都存在 有一个非零存在 辨析:为什么书上说只有两个极限都存在才能拆分,但是做题求极限时我们不要求这样呢? 两项相加减时,为什么“一个存在,一个无穷大”可以拆分?\n若 $\\lim f(x)=A$,$\\lim g(x)=\\infty$,\n则 $\\lim [f(x)+g(x)]=\\lim f(x)+\\lim g(x)=A+\\infty=\\infty$\n这实际上不是标准的极限\u0026quot;拆分\u0026quot;,而是利用了以下性质:\n$$ 有限值 ± 无穷大 = ± 无穷大 $$例如,若 $\\lim f(x) = 5$ 且 $\\lim g(x) = \\infty$,则 $\\lim[f(x) + g(x)] = \\infty$\n这不违背原理,而是对无穷大情况的特殊处理。\n所以,两项相加减时,“一个存在,一个无穷大”就可以拆分\n两项相乘除时,为什么“有一个非零存在”就可以拆分?\n在乘除法的极限运算中,\u0026ldquo;有一个非零存在\u0026quot;可以拆分是因为存在以下特殊情况处理规则:\n乘法情况\n一个极限为非零有限值,另一个为不存在\n若 $\\lim f(x)=A \\neq 0$,\n则 $\\lim [f(x)\\cdot g(x)]=\\lim f(x) \\cdot \\lim g(x)=A\\cdot\\lim g(x)$\n除法情况\n分子极限为有限值,分母极限为不存在\n若 $\\lim f(x) = A$(有限值),\n则 $\\lim\\frac{f(x)}{g(x)} = \\frac{A}{\\lim g(x)}$\n注:分子极限存在时特别包括$A=0$ 分子极限为不存在,分母极限为非零有限值\n若 $\\lim g(x) = B \\neq 0$,\n则 $\\lim\\frac{f(x)}{g(x)} = \\frac{\\lim f(x)}{B}$\n所以,两项相乘除时,“有一个非零存在”就可以拆分\n7. 极限存在充要条件 # 极限存在充要条件:“左右极限存在且相等”\n【常用反例成分】常见左右极限不相等情况 考研中常见的区分左右极限的情况:\n$$ \\lim_{x \\to \\infty} e^x = \\begin{cases} \\lim_{x \\to +\\infty} e^x = +\\infty, \\\\\\\\ \\lim_{x \\to -\\infty} e^x = 0. \\end{cases} $$ $$ \\lim_{x \\to \\infty} \\arctan x = \\begin{cases} \\lim_{x \\to +\\infty} \\arctan x = \\frac{\\pi}{2}, \\\\\\\\ \\lim_{x \\to -\\infty} \\arctan x = -\\frac{\\pi}{2}. \\end{cases} $$ $$ \\lim_{x \\to 0} |x| = \\begin{cases} \\lim_{x \\to 0^+} x, \\\\\\\\ \\lim_{x \\to 0^-} (-x). \\end{cases} $$ $$ F(x) = \\begin{cases} f(x) \u0026 x \\ge x_0 \\\\\\\\ g(x) \u0026 x \u003c x_0 \\end{cases} \\hspace{10pt} [f(x) \\neq g(x)] $$ $$ \\lim_{x \\to x_0} F(x) = \\begin{cases} \\lim_{x \\to x_0^+} f(x), \\\\\\\\ \\lim_{x \\to x_0^-} g(x). \\end{cases} $$ 8. 极限不存在与极限无穷大的关系 # “极限不存在包括极限无穷大的情况”\n极限不存在三种情况 极限不存在包括:\n极限为无穷大 左极限和右极限不相等 极限震荡不存在 二、利用泰勒公式求极限 # 1. 麦克劳林公式(泰勒公式在$x_0=0$时的特殊形式) # $$ f(x) = f(0) + f'(0)x + \\cdots + \\frac{f^{(n)}(0)}{n!}x^n + o(x^n) $$ 2. 泰勒公式求极限解题原则:(k 确定)上下同阶、(k 不确定)抵消不了 # 上下同阶:如果分式中分子或分母中有一个阶数确定,那么另外一部分需要展开到这个确定阶数的同阶。 抵消不了:如果分式中分子或分母中阶数都不确定,那么它们要展开到彼此抵消不了的幂次为止。 3. 常见的麦克劳林公式(7 组 14 个) # 类别 常用麦克劳林公式 一、分式 函数展开式$\\frac{1}{1-x}$$=1+x+x^2+\\cdots+x^n+o(x^n)$$\\frac{1}{1+x}$$=1-x+x^2-\\cdots+(-1)^{n}x^n+o(x^n)$ 二、对数函数 函数展开式$\\ln(1+x)$$=x-\\frac{x^2}{2}+\\frac{x^3}{3}-\\cdots+(-1)^{n-1}\\frac{x^n}{n}+o(x^n)$$\\ln(1-x)$$=-x-\\frac{x^2}{2}-\\frac{x^3}{3}-\\cdots-\\frac{x^n}{n}+o(x^n)$$-\\ln(1-x)$$=x+\\frac{x^2}{2}+\\frac{x^3}{3}+\\cdots+\\frac{x^n}{n}+o(x^n)$ 三、分式 (偶次幂) 函数展开式$\\frac{1}{1-x^2}$$=1+x^2+x^4+\\cdots+x^{2n}+o(x^{2n+1})$$\\frac{1}{1+x^2}$$=1-x^2+x^4-\\cdots+(-1)^{n}x^{2n}+o(x^{2n+1})$ 四、三角与反三角 (常用) 函数展开式 (常用前几项)$\\arctan x$$=x-\\frac{x^3}{3}+o(x^3)$$\\tan x$$=x+\\frac{x^3}{3}+o(x^3)$$\\arcsin x$$=x+\\frac{x^3}{6}+o(x^3)$ 五、基本三角函数 函数展开式$\\sin x$$=x-\\frac{x^3}{3!}+\\frac{x^5}{5!}-\\cdots+(-1)^{n}\\frac{x^{2n+1}}{(2n+1)!}+o(x^{2n+1})$$\\cos x$$=1-\\frac{x^2}{2!}+\\frac{x^4}{4!}-\\cdots+(-1)^{n}\\frac{x^{2n}}{(2n)!}+o(x^{2n})$ 六、二项式 $(1+x)^{\\alpha}=1+\\alpha x+\\frac{\\alpha(\\alpha-1)}{2!}x^2+\\cdots+\\frac{\\alpha(\\alpha-1)\\cdots(\\alpha-n+1)}{n!}x^n+o(x^n)$ 七、指数函数 $e^x=1+x+\\frac{x^2}{2!}+\\frac{x^3}{3!}+\\cdots+\\frac{x^n}{n!}+o(x^n)$ 常见等价无穷小和泰勒公式的关系及推导 首先要知道,等价无穷小是泰勒公式展开的一种特殊情况。 然后我们先写出常见的 8 种等价无穷小及其对应的泰勒公式展开式:\n$sin○ \\sim tan○ \\sim arcsin○ \\sim arctan○ \\sim e^{○}-1 \\sim ln(1+○) \\sim ○$\n$sinx =x-\\frac{x^3}{3!}+\\frac{x^5}{5!}-\\cdots+(-1)^{n}\\frac{x^{2n+1}}{(2n+1)!}+o(x^{2n+1})$ $tanx = x+\\frac{x^3}{3}+o(x^3)$ $arcsinx = x+\\frac{x^3}{6}+o(x^3)$ $arctanx = x-\\frac{x^3}{3}+o(x^3)$ $e^{x} = 1+x+\\frac{x^2}{2!}+\\frac{x^3}{3!}+\\cdots+\\frac{x^n}{n!}+o(x^n)$ $ln(1+x) = x-\\frac{x^2}{2}+\\frac{x^3}{3}+\\cdots+(-1)^{n-1}\\frac{x^n}{n}+o(x^n)$ $a^{○}-1 \\sim ○lna$\n$a^x = 1+xlna+\\frac{(lna)^2}{2!}x^2+\\frac{(lna)^3}{3!}x^3+o(x^3)$ $1-cos○ \\sim \\frac{1}{2}○^2$\n$cosx = 1-\\frac{x^2}{2!}+\\cdots+(-1)^{n}\\frac{x^{2n}}{(2n)!}+o(x^{2n})$ $(1+○)^{a} -1 \\sim a○$\n$(1+x)^a = 1+ax+\\frac{a(a-1)}{2!}x^2+\\cdots+\\frac{a(a-1)\\cdots(a-n+1)}{n!}+o(x^n)$ $○-ln(1+○) \\sim \\frac{1}{2}○^2$\n$ln(1+x) = x-\\frac{x^2}{2}+\\frac{x^3}{3}+\\cdots+(-1)^{n-1}\\frac{x^n}{n}+o(x^n)$ $tan○-○ \\sim ○-arctan○ \\sim \\frac{1}{3}○^3$\n$○-sin○ \\sim arcsin○-○ \\sim \\frac{1}{6}○^3$\n$tan○-sin○ \\sim arcsin○-arctan○ \\sim \\frac{1}{2}○^3$\n三、利用无穷小替换求极限 # 1. 常见的等价无穷小(8 个) # 类别 等价无穷小 (当 $○ \\to 0$ 时) 一、 $\\sin ○ \\sim \\tan ○ \\sim \\arcsin ○ \\sim \\arctan ○ \\sim e^{○}-1 \\sim \\ln(1+○) \\sim ○$ 二、 $a^{○}-1 \\sim ○\\ln a$ 三、 $1-\\cos ○ \\sim \\frac{1}{2}○^2$ 四、 $(1+○)^{a} -1 \\sim a○$ 五、 $○-\\ln(1+○) \\sim \\frac{1}{2}○^2$ 六 $\\tan ○ - ○ \\sim ○ - \\arctan ○ \\sim \\frac{1}{3}○^3$$○ - \\sin ○ \\sim \\arcsin ○ - ○ \\sim \\frac{1}{6}○^3$$\\tan ○ - \\sin ○ \\sim \\arcsin ○ - \\arctan ○ \\sim \\frac{1}{2}○^3$ 2. 等价无穷小变形例子 # $x \\to 0$ 时,$\\sqrt{1+sinx}$ 和 $\\sqrt[3]{1+sinx}$ 用 $(1+○)^{a} -1 \\sim a○$ $x \\to 0$ 时,$f(x) \\to 1$,$ln(f(x))$ 化为 $ln(1+(f(x)-1))$后 用 $○-ln(1+○) \\sim \\frac{1}{2}○^2$ $x \\to 0$ 时,$ln(x+\\sqrt{1+x^2})$ 化为 $ln(1+(x+\\sqrt{1+x^2}-1))$ 后先用 $\\ln(1+○) \\sim ○$,再用$(1+○)^{a} -1 \\sim a○$,最后得到$x+x^2$等价为$x$ $x \\to 0$ 时,$e^{f(x)}-e^{g(x)}$ 化为 $e^{g(x)}(e^{f(x)-g(x)}-1)$ 后用 $e^{○}-1 \\sim ○$ 首先知道 $x \\to 0$,然后给我一个式子求极限,先判断能否使用等价无穷小(包括使用哪个,判断方法为是否符合常见的等价无穷小的因子,再看运算方式,只有“加减且相消为 0”才不可以使用),可以的话就尝试变形后用等价无穷小代换,不可以的话就用泰勒展开。\n解题技巧总结 # 1. 根号$\\sqrt{}$提平方要注意正负:$\\sqrt{x^2}$提平方后为$|x|$ # 特别是,$x\\rightarrow 负$时,$\\sqrt{x^2}$提平方后为$|x|=-x$\n2. 求极限中,求左右极限的情况 # 任何时候,当你怀疑函数在极限点左侧和右侧的行为可能不同时,都应该分别计算左右极限。 特别是,见到以下情况时,应该分别求左右极限:\n$e^{\\frac{1}{x}}$ $arctan{\\frac{1}{x}}$ $|x|$ 分母趋于 0 且分子不为 0 涉及到偶次根式,且根号下的表达式在极限点处为 0 涉及定义域边界或单侧定义 3. 泰勒公式求极限书面要求 # 写泰勒公式时,必须把最后的 $o(x^{n})$ 带上,但是在草稿中可以忽略,最后只要把等价结果写入答案即可\n4. 泰勒公式求极限中,遇到 $(f(x))^2-(g(x))^2$可化为$(f(x)+g(x))(f(x)-g(x))$ ,而不用计算 $(f(x))^2$和$(g(x))^2$ 的泰勒 # 5. 见到 $\\sqrt{f(x)}中f(x)\\to 1$ 可化为 $\\sqrt{1+f(x)-1}$ ,用等价无穷小 $“(1+○)^{a} -1 \\sim a○”$ 得到 $\\frac{1}{2}\\sqrt{f(x)-1}$ # 6. 求极限中,分式用同时有 $x$ 和$ g(x)$ ,可以尝试将 $x$ 化为 $f(x)$ ,再用 $t=f(x)$ 代换 # 7. “ $\\infty-\\infty$ ”型中 # 见到 $\\sqrt{\\frac{1}{x^2}+1}$ 类似可化为 $\\frac{\\sqrt{1+x^2}}{x}$ ,而不用有理化\n见到 $\\sqrt[n]{x^n+ax^{n-1}+…}$ ,可直接代换为其渐进式 $x+\\frac{a}{n}$\n比如 $lim_{x \\to \\infty} \\sqrt[3]{x^3+x^2+1}-\\sqrt[2]{x^2+x+1}$ ,可直接代换为 $(x+\\frac{1}{3})-(x+\\frac{1}{2})$ ,立刻得到结果为 $-\\frac{1}{6}$\n8. 求极限计算题步骤规范 # 有变量代换先写代换,然后写变形式,再写化简后式子,最后写答案 求极限计算题步骤规范分析 等价无穷小:乘除可以直接替换,加减可以直接替换(但是主要项不能相消为 0)【最好使用泰勒验证一遍结果】,这步必须写 快速变量代换:必须写$令x=t…,则t=…x。当x \\to …时,t \\to 0$ 9. 求极限中见到$x^x$ # 使用$u^v=e^{v\\ln u}$\n","date":"2025年5月8日","externalUrl":null,"permalink":"/postgraduate/%E4%B8%89%E5%A4%A7%E8%AE%A1%E7%AE%97%E6%B1%82%E6%9E%81%E9%99%90/","section":"学习思考","summary":"","title":"【三大计算】求极限","type":"postgraduate"},{"content":"","date":"2025年5月8日","externalUrl":null,"permalink":"/tags/study/","section":"Tags","summary":"","title":"Study","type":"tags"},{"content":"","date":"2025年5月8日","externalUrl":null,"permalink":"/series/%E4%B8%89%E5%A4%A7%E8%AE%A1%E7%AE%97/","section":"Series","summary":"","title":"三大计算","type":"series"},{"content":"","date":"2025年5月8日","externalUrl":null,"permalink":"/tags/%E6%95%B0%E4%BA%8C/","section":"Tags","summary":"","title":"数二","type":"tags"},{"content":"","date":"2025年5月6日","externalUrl":null,"permalink":"/tags/bark/","section":"Tags","summary":"","title":"Bark","type":"tags"},{"content":"","date":"2025年5月6日","externalUrl":null,"permalink":"/tags/ios/","section":"Tags","summary":"","title":"IOS","type":"tags"},{"content":"","date":"2025年5月6日","externalUrl":null,"permalink":"/tags/iphone/","section":"Tags","summary":"","title":"IPhone","type":"tags"},{"content":"","date":"2025年5月6日","externalUrl":null,"permalink":"/tags/tools/","section":"Tags","summary":"","title":"Tools","type":"tags"},{"content":" 0.写在开头 # 想说这个工具很好用,但是话到嘴边却发现没什么好说的。Bark 是一个好用但非必要的工具,就像一个打字更顺畅的键盘、一个更合手的鼠标,你不会天天挂在嘴边,但你确实每天都在用。\n服务器 Docker Compose 部署 # 请自行替换下面内容:\n\u0026lt;mysql_root_password\u0026gt; 为你的 MySQL 密码 \u0026lt;bark_server_port\u0026gt; 为你的 Bark 服务器端口 YAML不换行 volumes: bark_data: mysql_data: services: bark-server: image: finab/bark-server container_name: bark-server restart: unless-stopped ports: - \u0026#39;\u0026lt;bark_server_port\u0026gt;:8080\u0026#39; environment: - MYSQL_DSN=root:\u0026lt;mysql_root_password\u0026gt;@tcp(mysql:3306)/bark?charset=utf8mb4\u0026amp;parseTime=True\u0026amp;loc=Local volumes: - bark_data:/data depends_on: mysql: condition: service_healthy mysql: image: mysql:8.0 container_name: bark-mysql restart: unless-stopped environment: - MYSQL_ROOT_PASSWORD=\u0026lt;mysql_root_password\u0026gt; - MYSQL_DATABASE=bark # ports: # - \u0026#34;3306:3306\u0026#34; # 已注释掉,不对外暴露MySQL端口 volumes: - mysql_data:/var/lib/mysql healthcheck: test: [\u0026#39;CMD\u0026#39;, \u0026#39;mysqladmin\u0026#39;, \u0026#39;ping\u0026#39;, \u0026#39;-h\u0026#39;, \u0026#39;127.0.0.1\u0026#39;] interval: 10s timeout: 5s retries: 3 start_period: 30s cap_add: - SYS_NICE然后运行:\nSHELL不换行 docker-compose up -d对你的 \u0026lt;bark_server_port\u0026gt; 设置反向代理,访问即可。\n使用方法 # 剩下的一切请参考 Bark 使用教程。\n这里给出一种示例使用方式:\nSHELL不换行 curl -X \u0026#34;POST\u0026#34; \u0026#34;https://\u0026lt;你的bark_server_url\u0026gt;/push\u0026#34; \\ -H \u0026#39;Content-Type: application/json; charset=utf-8\u0026#39; \\ -d $\u0026#39; { \u0026#34;title\u0026#34;: \u0026#34;我是title\u0026#34;, \u0026#34;body\u0026#34;: \u0026#34;我是body\u0026#34;, \u0026#34;device_key\u0026#34;: \u0026#34;\u0026lt;你的device_key\u0026gt;\u0026#34; }\u0026#39;","date":"2025年5月6日","externalUrl":null,"permalink":"/posts/%E4%BD%BF%E7%94%A8bark%E6%8E%A8%E9%80%81%E9%80%9A%E7%9F%A5%E5%88%B0%E4%BD%A0%E7%9A%84iphone/","section":"文章","summary":"","title":"使用Bark推送通知到你的iPhone","type":"posts"},{"content":"","date":"2025年4月9日","externalUrl":null,"permalink":"/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":"","date":"2025年4月9日","externalUrl":null,"permalink":"/tags/mitmproxy/","section":"Tags","summary":"","title":"Mitmproxy","type":"tags"},{"content":"","date":"2025年4月9日","externalUrl":null,"permalink":"/tags/sing-box/","section":"Tags","summary":"","title":"Sing-Box","type":"tags"},{"content":"","date":"2025年4月9日","externalUrl":null,"permalink":"/tags/tun/","section":"Tags","summary":"","title":"Tun","type":"tags"},{"content":" 0.写在开头 # 这里不介绍具体操作步骤,主要说明一个思路。\n1.抓包方案对比 # 当前常见的抓包方案有以下几种:\n序号 方案 代表工具 说明 1 基于代理的 HTTP/HTTPS 抓包 Fiddler\nCharles\nBurpSuite\nReqable\nmitmproxy 安装中间人证书(自签),解密 https 流量,对外会开放一个代理,发起请求时指定该代理就能抓包 2 基于网卡嗅探的全协议抓包 Wireshark\nTcpdump 啥包都能抓,唯一的缺点是无法解密 tls 流量,可以设置环境变量 SSLKEYLOGFILE,但这种方式只能解密浏览器发起的 https,对于某些应用内的请求还是没法解密。 但是这两个方案在遇到特定场景时:\n接口使用 https 协议 包括应用内部请求和浏览器请求 应用无法指定代理 部分请求需要通过梯子 这个时候,使用 tun 虚拟网卡抓包是一个不错的选择。在网络层就拦截请求,再交给指定的代理进行抓包。\n2.使用 Tun 虚拟网卡抓包流程示意 # flowchart TD %% 主节点定义 APP(\"应用程序\") BROWSER(\"浏览器\") TUN(\"TUN入站\n(虚拟网卡)\") TUN_ROUTE(\"TUN专用路由\n全部转发到代理出站\") TO_MITM(\"To-Mitmproxy出站\n(转发到9999端口)\") HTTP_PROXY(\"HTTP代理服务\n(9999端口)\") MITM(\"mitmproxy\n解密与分析HTTPS流量\") MIXED(\"Mixed入站\n接收8888端口流量\nHTTP+HTTPS\") ROUTER(\"主路由规则\n国内/国外/广告分流\") DIRECT(\"直连出站\n(国内服务器)\") PROXY(\"代理出站\n(国外服务器)\") BLOCK(\"阻断出站\n(Block广告)\") WEBUI(\"mitmweb界面\n(8081端口)\") INTERNET(\"目标服务\") %% 子图分组 subgraph deviceEnv[\"设备环境\"] APP BROWSER end subgraph singbox[\"sing-box\"] subgraph inbounds[\"入站规则\"] TUN MIXED end subgraph routing[\"路由规则\"] TUN_ROUTE ROUTER end subgraph outbounds[\"出站规则\"] TO_MITM DIRECT PROXY BLOCK end end subgraph mitmproxy[\"mitmproxy\"] HTTP_PROXY MITM WEBUI end subgraph external[\"外部网络\"] INTERNET end %% 请求路径 - 实线 APP --\u003e|\"应用请求\"| TUN TUN --\u003e|\"TUN流量\"| TUN_ROUTE TUN_ROUTE --\u003e|\"全部流量\"| TO_MITM TO_MITM --\u003e|\"转发到9999端口\"| HTTP_PROXY HTTP_PROXY --\u003e|\"上游代理到8888端口\"| MITM MITM --\u003e|\"返回到8888端口\"| MIXED MIXED --\u003e|\"解密后的流量\"| ROUTER %% 从路由到三个并列出站的线条 - 三个完全独立 ROUTER --\u003e|\"国内网站\"| DIRECT ROUTER --\u003e|\"国外网站\"| PROXY ROUTER --\u003e|\"广告请求\"| BLOCK %% 从出站到互联网的线条 DIRECT --\u003e|\"直连请求\"| INTERNET PROXY --\u003e|\"代理请求\"| INTERNET %% 响应路径 - 虚线 INTERNET -.-\u003e|\"国内响应\"| DIRECT INTERNET -.-\u003e|\"国外响应\"| PROXY %% 从出站返回到路由的线条 - 三个完全独立 DIRECT -.-\u003e|\"直连响应\"| ROUTER PROXY -.-\u003e|\"代理响应\"| ROUTER BLOCK -.-\u003e|\"阻断响应\"| ROUTER %% 后续响应流程 ROUTER -.-\u003e|\"分类后响应\"| MIXED MIXED -.-\u003e|\"加密后响应\"| MITM MITM -.-\u003e|\"代理响应\"| HTTP_PROXY HTTP_PROXY -.-\u003e|\"返回9999端口\"| TO_MITM TO_MITM -.-\u003e|\"转发响应\"| TUN_ROUTE TUN_ROUTE -.-\u003e|\"TUN响应\"| TUN TUN -.-\u003e|\"应用响应\"| APP %% 监控线路 - 点虚线 MITM -...-\u003e|\"流量分析\"| WEBUI BROWSER -...-\u003e|\"访问界面\"| WEBUI %% 节点样式 classDef appNode fill:#6a329f,stroke:#9d78c2,color:#ffffff,stroke-width:2px classDef browserNode fill:#4682b4,stroke:#87ceeb,color:#ffffff,stroke-width:2px classDef tunNode fill:#2472c8,stroke:#5cb3ff,color:#ffffff,stroke-width:2px classDef mixedNode fill:#8b4513,stroke:#d2691e,color:#ffffff,stroke-width:2px classDef routerNode fill:#a52a2a,stroke:#cd5c5c,color:#ffffff,stroke-width:2px classDef proxyNode fill:#6b8e23,stroke:#9acd32,color:#ffffff,stroke-width:2px classDef directNode fill:#483d8b,stroke:#7b68ee,color:#ffffff,stroke-width:2px classDef blockNode fill:#8b0000,stroke:#ff6347,color:#ffffff,stroke-width:2px classDef httpNode fill:#c95f00,stroke:#ffa452,color:#ffffff,stroke-width:2px classDef mitmNode fill:#298a3c,stroke:#5fd46c,color:#ffffff,stroke-width:2px classDef webUINode fill:#800080,stroke:#da70d6,color:#ffffff,stroke-width:2px classDef internetNode fill:#696969,stroke:#d3d3d3,color:#ffffff,stroke-width:2px classDef toMitmNode fill:#ff7f50,stroke:#ffb347,color:#ffffff,stroke-width:2px classDef tunRouteNode fill:#db7093,stroke:#ffb6c1,color:#ffffff,stroke-width:2px %% 应用节点样式 class APP appNode class BROWSER browserNode class TUN tunNode class MIXED mixedNode class TUN_ROUTE tunRouteNode class ROUTER routerNode class TO_MITM toMitmNode class PROXY proxyNode class DIRECT directNode class BLOCK blockNode class HTTP_PROXY httpNode class MITM mitmNode class WEBUI webUINode class INTERNET internetNode %% 子图样式 style deviceEnv fill:#2d2d2d,stroke:#666666,color:#ffffff,stroke-width:1px style singbox fill:#1a1a1a,stroke:#555555,color:#ffffff,stroke-width:1px style inbounds fill:#333333,stroke:#888888,color:#ffffff,stroke-width:1px style routing fill:#333333,stroke:#888888,color:#ffffff,stroke-width:1px style outbounds fill:#333333,stroke:#888888,color:#ffffff,stroke-width:1px style mitmproxy fill:#2a2a2a,stroke:#777777,color:#ffffff,stroke-width:1px style external fill:#2d2d2d,stroke:#666666,color:#ffffff,stroke-width:1px 3.整体配置 # 3.1 sing-box 配置 # 两个入站 tun 虚拟网卡 mixed 混合入站:接收 8888 端口流量 四个出站 To-Mitmproxy:接收 tun 流量转发到 9999 端口 国外代理:接收 mixed 流量转发到国外代理 国内代理:接收 mixed 流量转发到国内代理 广告拦截:接收 mixed 流量转发到广告拦截 四个路由 TUN 专用路由:接收 tun 流量,全部转发到 To-Mitmproxy 出站 国内网站:接收 mixed 流量转发到国内代理 国外网站:接收 mixed 流量转发到国外代理 广告请求:接收 mixed 流量转发到广告拦截 3.2 mitmproxy 配置 # web 界面 --set web_port=8081 打开于8081端口 代理模式 --mode=upstream:http://127.0.0.1:8888@9999 代理模式为上游代理模式 在本地9999端口监听客户端连接,将所有拦截到的流量转发到上游代理http://127.0.0.1:8888 4. 快速使用 # 使用UIF来快速使用 sing-box 单独编写uif_capture.json,在要抓包的时候,先备份正常的uif.json,然后复制uif_capture.json到/usr/bin/uif/uif.json 单独编写core_config_capture.json,在要抓包的时候,先备份正常的core_config.json,然后复制core_config_capture.json到/usr/bin/uif/core_config.json 启动mitmweb: SHELL不换行 mitmweb --set web_port=8081 --mode=upstream:http://127.0.0.1:8888@9999 启动uif 开始抓包 抓包结束后,恢复uif.json和core_config.json 5.参考资料 # 点击展开查看参考资料 https://linux.do/t/topic/528371 https://linux.do/t/topic/127658 可能更方便的方案:Proxifier 搭配任一 http(s) 抓包工具 ","date":"2025年4月9日","externalUrl":null,"permalink":"/posts/%E4%BD%BF%E7%94%A8tun%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E6%8A%93%E5%8C%85/","section":"文章","summary":"","title":"使用Tun虚拟网卡抓包","type":"posts"},{"content":"","date":"2025年2月7日","externalUrl":null,"permalink":"/tags/onedrive/","section":"Tags","summary":"","title":"OneDrive","type":"tags"},{"content":"","date":"2025年2月7日","externalUrl":null,"permalink":"/tags/rclone/","section":"Tags","summary":"","title":"Rclone","type":"tags"},{"content":"","date":"2025年2月7日","externalUrl":null,"permalink":"/tags/webdav/","section":"Tags","summary":"","title":"WebDav","type":"tags"},{"content":" 0.写在开头 # 由于新购入了一台腾讯云首尔 200M 的小鸡,所以准备多搞搞以前受网速影响没搞的个人服务。\n又在土耳其区订阅了 Miscrosoft 365,里面有稳定的 1T 空间 OneDrive可以使用,平时使用占用不多,所以想到这个方案。\ngraph TD A[本地设备/外部服务] B(OneDrive服务器) C(服务器) A --\u003e|rclone直接挂载| B A --\u003e|WebDAV访问| C C --\u003e|rclone连接| B C --\u003e|提供WebDAV服务| A 以下信息请根据实际情况全局替换:\n项目路径:/projectpath/rclone_onedrive_webdav 项目名:rclone_onedrive_webdav 内部端口:10001 WebDav 用户名:webdavusername WebDav 密码:webdavpassword 本地设备用户名:username webdav 网址:youtwebdav.site 1.服务器部署 # 基本软件安装\nSHELL不换行 sudo apt update \u0026amp;\u0026amp; sudo apt install rclone apache2-utils -y Rclone 连接 OneDrive\n自行搜索 linux服务器使用rclone连接onedrive\n注意:rclone 新建的 name 应为 onedrive1T 运行 rclone config结果应包含如下:\nSHELL不换行 Current remotes: Name Type ==== ==== onedrive1T onedrive 项目文件夹创建\nSHELL不换行 mkdir -p /projectpath/rclone_onedrive_webdav mkdir -p /projectpath/rclone_onedrive_webdav/cache mkdir -p /projectpath/rclone_onedrive_webdav/temp 项目文件\nrclone-webdav.service\nINI不换行 [Unit] Description=Rclone WebDAV service After=network-online.target Wants=network-online.target [Service] Type=simple User=root ExecStart=/projectpath/rclone_onedrive_webdav/start_rclone_webdav.sh Restart=on-failure RestartSec=30 StartLimitInterval=60s StartLimitBurst=3 [Install] WantedBy=multi-user.target start_rclone_webdav.sh\nSH不换行 #!/bin/bash /usr/bin/rclone serve webdav onedrive1T: \\ --addr=127.0.0.1:10001 \\ --vfs-cache-mode writes \\ --vfs-cache-max-size 1G \\ --vfs-cache-max-age 6h \\ --buffer-size 16M \\ --dir-cache-time 30m \\ --poll-interval 1h \\ --tpslimit 30 \\ --bwlimit 150M \\ --log-level WARNING \\ --log-file /projectpath/rclone_onedrive_webdav/rclone.log \\ --cache-dir=/projectpath/rclone_onedrive_webdav/cache \\ --htpasswd /projectpath/rclone_onedrive_webdav/htpasswd \\ --no-checksum \\ --timeout 1m 创建 htpasswd文件:\nSHELL不换行 sudo htpasswd -c /projectpath/rclone_onedrive_webdav/htpasswd webdavusername 确保 htpasswd文件权限为 644\nSHELL不换行 sudo chmod 644 /projectpath/rclone_onedrive_webdav/htpasswd然后输入 webdavpassword\n创建 rclone.log文件\nSHELL不换行 touch /projectpath/rclone_onedrive_webdav/rclone.log 启动服务\nSHELL不换行 sudo cp /projectpath/rclone_onedrive_webdav/rclone-webdav.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable rclone-webdav.service sudo systemctl start rclone-webdav.service 设置反向代理,并配置 HTTPS。\nNGINX不换行 server { listen 80; listen 443 ssl http2; server_name youtwebdav.site; # SSL配置优化 ssl_certificate /www/sites/youtwebdav.site/ssl/fullchain.pem; ssl_certificate_key /www/sites/youtwebdav.site/ssl/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # OCSP Stapling ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=60s; resolver_timeout 2s; # HTTP跳转HTTPS if ($scheme = \u0026#34;http\u0026#34;) { return 301 https://$host$request_uri; } error_page 497 https://$host$request_uri; # 日志配置 access_log /www/sites/youtwebdav.site/log/access.log combined buffer=512k flush=1m; error_log /www/sites/youtwebdav.site/log/error.log warn; # 上传限制 client_max_body_size 10240M; client_body_timeout 3600s; # 代理基础配置 proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \u0026#34;\u0026#34;; proxy_set_header Authorization $http_authorization; proxy_pass_header Authorization; # ACME验证 location ^~ /.well-known/acme-challenge { allow all; root /usr/share/nginx/html; } # WebDAV配置 location / { proxy_pass http://127.0.0.1:10001; # 超时设置优化 proxy_connect_timeout 60s; proxy_send_timeout 3600s; proxy_read_timeout 3600s; send_timeout 3600s; # 缓冲区优化 proxy_buffer_size 16k; proxy_buffers 8 16k; proxy_busy_buffers_size 32k; # 关闭缓冲提高传输效率 proxy_buffering off; proxy_request_buffering off; proxy_max_temp_file_size 0; # WebDAV方法 limit_except GET HEAD POST PUT DELETE MKCOL COPY MOVE PROPFIND PROPPATCH LOCK UNLOCK OPTIONS { deny all; } # 大文件传输优化 client_body_buffer_size 512k; # 启用压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; } # 安全头优化 add_header Strict-Transport-Security \u0026#34;max-age=63072000; includeSubDomains; preload\u0026#34; always; add_header X-Content-Type-Options \u0026#34;nosniff\u0026#34; always; add_header X-Frame-Options \u0026#34;SAMEORIGIN\u0026#34; always; add_header X-XSS-Protection \u0026#34;1; mode=block\u0026#34; always; add_header Referrer-Policy \u0026#34;strict-origin-when-cross-origin\u0026#34; always; # 禁止访问隐藏文件 location ~ /\\. { deny all; access_log off; log_not_found off; } } 配置完成后,访问 https://youtwebdav.site即可。\n2.本地挂载 # 2.1 基础使用 # 创建项目路径 SHELL不换行 mkdir -p /home/username/Documents/OneDrive mkdir -p /home/username/.cache/rclone touch /home/username/.cache/rclone/rclone.log 挂载 SHELL不换行 rclone mount onedrive1T: /home/username/Documents/OneDrive \\ --vfs-cache-mode full \\ --vfs-cache-max-size 30G \\ --vfs-cache-max-age 168h \\ --vfs-cache-poll-interval 15m \\ --buffer-size 1G \\ --vfs-read-chunk-size 256M \\ --vfs-read-chunk-size-limit 8G \\ --vfs-read-ahead 3G \\ --fast-list \\ --dir-cache-time 12h \\ --poll-interval 2m \\ --transfers 16 \\ --checkers 32 \\ --multi-thread-cutoff 1G \\ --multi-thread-streams 8 \\ --cache-dir /home/username/.cache/rclone \\ --use-mmap \\ --max-read-ahead 1G \\ --log-file=/home/username/.cache/rclone/rclone.log \\ --log-level NOTICE \\ --allow-non-empty \\ --daemon \\ --allow-other 2.2 进阶使用 # 默认挂载脚本:mount_Cloud_Drive.sh\nSH不换行 #!/bin/bash # 在这里定义要挂载的云盘,用空格分隔 CLOUD_DRIVES=\u0026#34;OneDrive\u0026#34; # 生成选项列表 options=() for drive in $CLOUD_DRIVES; do options+=(\u0026#34;挂载$drive\u0026#34;) done options+=(\u0026#34;挂载全部\u0026#34;) # 使用zenity创建选择对话框 choice=$(zenity --list \\ --title=\u0026#34;挂载云盘\u0026#34; \\ --column=\u0026#34;选项\u0026#34; \\ \u0026#34;${options[@]}\u0026#34; \\ --width=300 --height=400 \\ --cancel-label=\u0026#34;取消\u0026#34; \\ --ok-label=\u0026#34;确定\u0026#34;) # 如果用户取消了选择,则退出脚本 if [ $? -ne 0 ]; then exit 0 fi # 定义挂载函数 mount_drive() { local script=\u0026#34;\u0026lt;你放脚本的地址\u0026gt;/mount_$1.sh\u0026#34; if [ -f \u0026#34;$script\u0026#34; ]; then \u0026#34;$script\u0026#34; else zenity --error --text=\u0026#34;找不到挂载脚本:$script\u0026#34; fi } # 处理用户选择 if [ \u0026#34;$choice\u0026#34; = \u0026#34;挂载全部\u0026#34; ]; then for drive in $CLOUD_DRIVES; do mount_drive \u0026#34;$drive\u0026#34; done else # 从选项中提取云盘名称 selected_drive=${choice#挂载} mount_drive \u0026#34;$selected_drive\u0026#34; fi 挂载 OneDrive 脚本:mount_OneDrive.sh\nSH不换行 #!/bin/bash SERVICE_NAME=\u0026#34;OneDrive\u0026#34; RCLONE_REMOTE=\u0026#34;onedrive1T:\u0026#34; MOUNT_POINT=\u0026#34;/home/username/Documents/OneDrive\u0026#34; ICON_PATH=\u0026#34;/home/username/Pictures/AppIcons/onedrive_1.icns\u0026#34; # 检查是否已经挂载 if mountpoint -q \u0026#34;$MOUNT_POINT\u0026#34;; then # 如果已经挂载,用nautilus打开文件夹 notify-send -t 1000 -u normal -i \u0026#34;$ICON_PATH\u0026#34; \u0026#34;${SERVICE_NAME}已挂载\u0026#34; \u0026#34;正在打开文件夹...\u0026#34; nautilus \u0026#34;$MOUNT_POINT\u0026#34; else # 如果未挂载,开始挂载流程 notify-send -t 1000 -u normal -i \u0026#34;$ICON_PATH\u0026#34; \u0026#34;正在挂载${SERVICE_NAME}\u0026#34; \u0026#34;请等待…………\u0026#34; # 执行rclone挂载命令 rclone mount \u0026#34;$RCLONE_REMOTE\u0026#34; \u0026#34;$MOUNT_POINT\u0026#34; \\ --vfs-cache-mode full \\ --vfs-cache-max-size 20G \\ --vfs-cache-max-age 168h \\ --vfs-cache-poll-interval 1m \\ --buffer-size 256M \\ --vfs-read-chunk-size 256M \\ --vfs-read-chunk-size-limit 8G \\ --vfs-read-ahead 2G \\ --fast-list \\ --dir-cache-time 12h \\ --poll-interval 1m \\ --transfers 8 \\ --checkers 16 \\ --multi-thread-cutoff 512M \\ --multi-thread-streams 8 \\ --cache-dir /home/username/.cache/rclone/${SERVICE_NAME} \\ --use-mmap \\ --max-read-ahead 512M \\ --log-file=/home/username/.cache/rclone/rclone_${SERVICE_NAME}.log \\ --log-level NOTICE \\ --allow-non-empty \\ --daemon \\ --allow-other \\ --volname \u0026#34;$SERVICE_NAME\u0026#34; \\ --umask 002 \\ --attr-timeout 5s \\ --no-modtime # 等待rclone挂载完成 while ! mountpoint -q \u0026#34;$MOUNT_POINT\u0026#34;; do sleep 1 done # 显示挂载完成信息 notify-send -t 1000 -u normal -i \u0026#34;$ICON_PATH\u0026#34; \u0026#34;${SERVICE_NAME}挂载完成\u0026#34; \u0026#34;已挂载到 ${MOUNT_POINT}\u0026#34; fi 应用图标:mount_cloud_drive.desktop\nINI不换行 [Desktop Entry] Type=Application Name=Mount Cloud Drive Name[en]=Mount Cloud Drive Comment=挂载云盘到本地文件系统 Comment[en]=Mount Cloud Drive to local filesystem Exec=/home/username/APP/Scripts/mount_cloud_drive/mount_Cloud_Drive.sh Icon=/home/username/Pictures/AppIcons/drive.icns Terminal=false Categories=Utility; Actions=MountOneDrive; [Desktop Action MountOneDrive] Name=挂载OneDrive Name[en]=Mount OneDrive Exec=/home/username/APP/Scripts/mount_cloud_drive/mount_OneDrive.sh 搭配DwarFS使用\n","date":"2025年2月7日","externalUrl":null,"permalink":"/posts/%E4%BD%BF%E7%94%A8rclone%E5%BC%80%E6%94%BEonedrive/","section":"文章","summary":"","title":"使用Rclone开放OneDrive","type":"posts"},{"content":" 梳理 # 0. 问题 # 心态 考试心态紧张,不够放松和游刃有余 数二: 做题时,有时候会忽略题目中的一些细节或者计算错误一些跳一步计算,导致答案错误 平时做题,不思考和总结题型,不知道出题老头为什么要出这题、这题要考什么知识点 平时做题时草稿习惯不好,字大、乱 平时做题方法不对,遇到不会就去看答案,没有连续做题 英语: 词汇量不足,一些简单词记忆混淆、错误 阅读速度慢、方法不对,常忽视关键信息 对于一些题目的答案不敏感 政治: 平时积累不足,对于一些考点不透彻 1. 数二 # 1.1 高数 # graph LR A[高数] B[第一章:极限] C[第二章:连续性和间断点] D[第三章:导数和微分] E[第四章:微分中值定理] F[第五章:导数的应用] G[第六章:定积分] H[第七章:不定积分] I[第八章:二重积分] J[第九章:积分中值定理] K[第十章:积分常用技巧] L[第十一章:偏导数和全微分] M[第十二章:偏导数在计算极值上的应用] N[第十三章:一阶微分方程] O[第十四章:二阶微分方程] P[第十五章:高于二阶的常系数线性齐次微分方程] Q[第十六章:可降阶的微分方程] A --\u003e B A --\u003e C A --\u003e D A --\u003e E A --\u003e F A --\u003e G A --\u003e H A --\u003e I A --\u003e J A --\u003e K A --\u003e L A --\u003e M A --\u003e N A --\u003e O A --\u003e P A --\u003e Q 1.2 线代 # graph LR A[线代] B[第一章:行列式的性质] C[第二章:余子式和代数余子式] D[第三章:行列式按行或按列展开定理] E[第四章:计算具体型行列式的常用公式] F[第五章:计算抽象型行列式的常用公式] G[第六章:克拉姆法则] H[第七章:特殊的矩阵] I[第八章:矩阵的运算] J[第九章:伴随矩阵] K[第十章:逆矩阵] L[第十一章:初等变换和初等举证] M[第十二章:矩阵的秩] N[第十三章:n维向量的概念和运算] O[第十四章:向量组的线性表示与向量组的线性相关性定义] P[第十五章:线性相关与线性无关的结论] Q[第十六章:线性表出相关结论] R[第十七章:向量的内积及正交性] S[第十八章:齐次线性方程组] T[第十九章:非齐次线性方程组] U[第二十章:特征值和特征向量] V[第二十一章:矩阵的相似对角化] W[第二十二章:实对称矩阵的相似对角化] X[第二十三章:二次型及其标准型] Y[第二十四章:正定二次型] Z[第二十五章:矩阵等价、相似与合同间的异同] A --\u003e B A --\u003e C A --\u003e D A --\u003e E A --\u003e F A --\u003e G A --\u003e H A --\u003e I A --\u003e J A --\u003e K A --\u003e L A --\u003e M A --\u003e N A --\u003e O A --\u003e P A --\u003e Q A --\u003e R A --\u003e S A --\u003e T A --\u003e U A --\u003e V A --\u003e W A --\u003e X A --\u003e Y A --\u003e Z 2. 英二 # graph LR A[英二] B[第一部分:小作文] C[第二部分:大作文] D[第三部分:阅读4篇] E[第四部分:新题型] F[第五部分:翻译] G[第六部分:完型] A --\u003e B A --\u003e C A --\u003e D A --\u003e E A --\u003e F A --\u003e G 3. 信号 # graph LR A[信号] B[第一章:绪论与基础] C[第二章:连续系统的时域分析] D[第三章:傅里叶变换] E[第四章:拉氏变换和连续时间系统的s域分析] F[第五章:滤波、调制、抽样] G[第六章:能量信号与功率信号、能量谱与功率谱] H[第七章:离散时间系统的时域分析] I[第八章:Z变换离散时间系统的Z域分析] J[第九章:流图] K[第十章:系统的状态变量分析] L[第十一章:系统函数的补充] M[第十二章:电路分析专题] A --\u003e B A --\u003e C A --\u003e D A --\u003e E A --\u003e F A --\u003e G A --\u003e H A --\u003e I A --\u003e J A --\u003e K A --\u003e L A --\u003e M 4. 电路 # graph LR A[电路] B[第一章:电路的基本概念和定律] C[第二章:电阻电路分析] D[第三章:动态电路] E[第四章:正弦稳态电路] F[第五章:电路的频率响应和谐振电路] G[第六章:二端口电路] A --\u003e B A --\u003e C A --\u003e D A --\u003e E A --\u003e F A --\u003e G 5. 政治 # graph LR A[政治] B[第一章:马原] C[第二章:毛概] D[第三章:新思想] E[第四章:史纲] F[第五章:思修] G[第六章:时政] A --\u003e B A --\u003e C A --\u003e D A --\u003e E A --\u003e F A --\u003e G ","date":"2025年1月12日","externalUrl":null,"permalink":"/postgraduate/%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE/","section":"学习思考","summary":"","title":"【思维导图】数二英二+政治+信号+电路","type":"postgraduate"},{"content":"","date":"2024年12月31日","externalUrl":null,"permalink":"/tags/mac/","section":"Tags","summary":"","title":"Mac","type":"tags"},{"content":"","date":"2024年12月31日","externalUrl":null,"permalink":"/tags/win/","section":"Tags","summary":"","title":"Win","type":"tags"},{"content":" 一、为什么使用 DwarFS # DwarFS 和普通压缩方式相比有几个主要优势:\n重复数据共享\nDwarFS 会识别并合并相同的文件和数据块,只存储一份 对于备份来说特别有用,因为备份经常包含大量重复文件 随机访问能力\n可以直接访问压缩文件系统中的任意文件 普通压缩包需要先解压才能访问文件 只读特性\nDwarFS 是只读文件系统,数据不会被意外修改 非常适合长期归档存储 更好的压缩率\n针对特定文件类型优化的压缩算法 可以达到比普通压缩更高的压缩比 不过 DwarFS 也有一些局限:\n不支持写入,只能读取 创建过程较慢 需要额外的系统支持 二、基础使用 # 以下以 archlinux平台为例,其他系统可以略作参考。\n安装\nSHELL不换行 paru -S dwarfs-bin 创建镜像\nSHELL不换行 mkdwarfs -i \u0026lt;文件夹路径\u0026gt; -o \u0026lt;镜像文件路径/镜像文件名.dwarfs\u0026gt; 解压镜像\nSHELL不换行 dwarfsextract -i \u0026lt;镜像文件路径/镜像文件名.dwarfs\u0026gt; -o \u0026lt;文件夹路径\u0026gt; 挂载镜像(注意:内容为只读)\nSHELL不换行 sudo dwarfs \u0026lt;镜像文件路径/镜像文件名.dwarfs\u0026gt; \u0026lt;挂载路径\u0026gt; -o allow_other -o readonly 卸载挂载后的镜像\nSHELL不换行 sudo umount \u0026lt;挂载路径\u0026gt; 三、实践 # 3.1 安装 # SHELL不换行 paru -S dwarfs-bin然后编辑 /etc/fuse.conf文件,取消注释 user_allow_other行,以允许非 root 用户挂载文件系统。\n3.2 设置单独文件 MIME # 创建 /usr/share/mime/packages/x-dwarfs.xml\nSHELL不换行 sudo vim /usr/share/mime/packages/x-dwarfs.xml 添加内容\nXML不换行 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;mime-info xmlns=\u0026#34;http://www.freedesktop.org/standards/shared-mime-info\u0026#34;\u0026gt; \u0026lt;mime-type type=\u0026#34;application/x-dwarfs\u0026#34;\u0026gt; \u0026lt;comment\u0026gt;DwarFS Archive\u0026lt;/comment\u0026gt; \u0026lt;icon name=\u0026#34;application-zip\u0026#34;/\u0026gt; \u0026lt;glob-deleteall/\u0026gt; \u0026lt;glob pattern=\u0026#34;*.dwarfs\u0026#34;/\u0026gt; \u0026lt;/mime-type\u0026gt; \u0026lt;/mime-info\u0026gt; 更新 MIME 数据库\nSHELL不换行 sudo update-mime-database /usr/share/mime 3.3 其他 # 为了方便使用(gnome+wayland),我安装了 Actions For Nautilus,并且使用 notify-send发送通知。\n相关 json 配置如下(请替换 username为你的用户名):\nJSON不换行 { \u0026#34;actions\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;command\u0026#34;, \u0026#34;label\u0026#34;: \u0026#34;DwarFS 压缩\u0026#34;, \u0026#34;command_line\u0026#34;: \u0026#34;notify-send \\\u0026#34;DwarFS 压缩\\\u0026#34; \\\u0026#34;请等待压缩完成\\\u0026#34; \u0026amp;\u0026amp; mkdwarfs -i %f -o %f.dwarfs \u0026amp;\u0026amp; notify-send \\\u0026#34;DwarFS 压缩\\\u0026#34; \\\u0026#34;文件夹 %b 压缩成功\\\u0026#34; || notify-send \\\u0026#34;DwarFS 压缩\\\u0026#34; \\\u0026#34;文件夹 %b 压缩失败\\\u0026#34;\u0026#34;, \u0026#34;use_shell\u0026#34;: true, \u0026#34;filetypes\u0026#34;: [\u0026#34;directory\u0026#34;] }, { \u0026#34;type\u0026#34;: \u0026#34;command\u0026#34;, \u0026#34;label\u0026#34;: \u0026#34;DwarFS 解压\u0026#34;, \u0026#34;command_line\u0026#34;: \u0026#34;[ ! -d \\\u0026#34;%D/%W\\\u0026#34; ] \u0026amp;\u0026amp; mkdir -p \\\u0026#34;%D/%W\\\u0026#34; \u0026amp;\u0026amp; dwarfsextract -i \\\u0026#34;%f\\\u0026#34; -o \\\u0026#34;%D/%W\\\u0026#34; \u0026amp;\u0026amp; notify-send \\\u0026#34;DwarFS 解压成功\\\u0026#34; \\\u0026#34;%b 解压成功\\\u0026#34; || { [ -d \\\u0026#34;%D/%W\\\u0026#34; ] \u0026amp;\u0026amp; notify-send \\\u0026#34;DwarFS 解压失败\\\u0026#34; \\\u0026#34;文件夹 %D/%W 已存在\\\u0026#34; || notify-send \\\u0026#34;DwarFS 解压失败\\\u0026#34; \\\u0026#34;%b 解压失败\\\u0026#34;; }\u0026#34;, \u0026#34;use_shell\u0026#34;: true, \u0026#34;mimetypes\u0026#34;: [\u0026#34;application/x-dwarfs\u0026#34;] }, { \u0026#34;type\u0026#34;: \u0026#34;command\u0026#34;, \u0026#34;label\u0026#34;: \u0026#34;DwarFS 挂载\u0026#34;, \u0026#34;command_line\u0026#34;: \u0026#34;PASSWORD=$(zenity --password --title \\\u0026#34;挂载DwarFS镜像:%w\\\u0026#34; --width 500) \u0026amp;\u0026amp; echo \\\u0026#34;$PASSWORD\\\u0026#34; | sudo -S true \u0026amp;\u0026amp; sudo mkdir -p /run/media/username/%w \u0026amp;\u0026amp; sudo dwarfs %f /run/media/username/%w -o allow_other -o readonly \u0026amp;\u0026amp; notify-send \\\u0026#34;DwarFS 挂载\\\u0026#34; \\\u0026#34;%b 挂载成功\\\u0026#34; || notify-send \\\u0026#34;DwarFS 挂载\\\u0026#34; \\\u0026#34;%b 挂载失败\\\u0026#34;\u0026#34;, \u0026#34;use_shell\u0026#34;: true, \u0026#34;mimetypes\u0026#34;: [\u0026#34;application/x-dwarfs\u0026#34;] }, { \u0026#34;type\u0026#34;: \u0026#34;command\u0026#34;, \u0026#34;label\u0026#34;: \u0026#34;DwarFS 卸载\u0026#34;, \u0026#34;command_line\u0026#34;: \u0026#34;PASSWORD=$(zenity --password --title \\\u0026#34;卸载DwarFS镜像:%w\\\u0026#34; --width 500) \u0026amp;\u0026amp; echo \\\u0026#34;$PASSWORD\\\u0026#34; | sudo -S true \u0026amp;\u0026amp; sudo umount /run/media/username/%w \u0026amp;\u0026amp; sudo rm -rf /run/media/username/%w \u0026amp;\u0026amp; notify-send \\\u0026#34;DwarFS 卸载\\\u0026#34; \\\u0026#34;%b 卸载成功\\\u0026#34; || notify-send \\\u0026#34;DwarFS 卸载\\\u0026#34; \\\u0026#34;%b 卸载失败\\\u0026#34;\u0026#34;, \u0026#34;use_shell\u0026#34;: true, \u0026#34;mimetypes\u0026#34;: [\u0026#34;application/x-dwarfs\u0026#34;] } ], \u0026#34;sort\u0026#34;: \u0026#34;manual\u0026#34;, \u0026#34;debug\u0026#34;: true } 88. 参考资料 # 点击展开查看参考资料 https://tech.meituan.com/2021/01/07/pack-gzip-zstd-lz4.html https://zh.wikipedia.org/wiki/%E5%8E%8B%E7%BC%A9%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E5%88%97%E8%A1%A8 ","date":"2024年12月31日","externalUrl":null,"permalink":"/posts/%E4%BD%BF%E7%94%A8dwarfs%E5%8E%8B%E7%BC%A9%E6%96%87%E4%BB%B6/","section":"文章","summary":"","title":"使用DwarFS压缩文件","type":"posts"},{"content":"","date":"2024年12月17日","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":" 0. 写在开头 # 仅用于记录部署流程,用户名username、项目名projectname、文件夹名等信息均为示例,实际部署时请根据实际情况全局替换。\n用户名:username 项目名:projectname 1. 装配好服务器硬件并安装系统及基础环境 # 装配好服务器硬件,连接好电源、网线等外部设备 安装 Ubuntu20.04 系统 镜像:ubuntu-20.04.6-desktop-amd64.iso 更新和其他软件:正常安装、安装 Ubuntu 时下载更新、安装第三方软件 安装类型:清除整个磁盘并安装 Ubuntu、选择系统盘 姓名:username 用户名:username 选中自动登录 安装完成后,拷贝项目文件夹压缩包到服务器,解压到/home/username/username文件夹下,重命名为projectname 再打开软件和更新的附加驱动,选中最新的驱动(应该是nvidia-driver-5xx),等待下载,安装完成后重启 2. 安装并设置 conda # 下载 miniforge 并安装(ps:不知具体文件,见附件)\nSHELL不换行 cd /home/username/username/projectname chmod +x deploy/*.sh ./deploy/install_miniforge.sh 测试 conda 是否安装成功\n重新打开终端 SHELL不换行 echo \u0026#39;export PATH=\u0026#34;$HOME/miniforge3/bin:$PATH\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bashrc conda -V有类似conda xx.xx就说明 conda 安装成功 配置 conda 和 pip([见附件](ps:不知具体文件,见附件)\nSHELL不换行 cp deploy/.condarc /home/username/ mkdir -p /home/username/.config/pip/ cp deploy/pip.conf /home/username/.config/pip/ 3. 创建 conda 环境并安装项目依赖 # 请根据当前设备是否联网选择对应的安装方式\nps: 记得检查scripts内脚本是否为在 conda 指定环境运行:conda run -n projectname \u0026lt;正常命令\u0026gt;,没有则添加 ps: 记得检查scripts内脚本是否有export PATH=\u0026quot;/home/minifoge3/bin:$PATH\u0026quot; ,没有则添加 3.1 联网 # 创建 conda 环境(python 版本根据实际工程给定)\nSHELL不换行 conda create -n projectname python=3.9 conda activate projectname 安装项目依赖\nSHELL不换行 cd /home/username/username/projectname conda activate projectname pip install -r requirements.txt 检查包(有就说明无问题,无视出现ERROR: Pipe to stdout was broken):\nSHELL不换行 conda activate projectname (conda list | head -n 5 ) \u0026amp;\u0026amp; echo \u0026#34;======\u0026#34; \u0026amp;\u0026amp; (pip list | head -n 5 ) 测试运行程序:\n启动程序: SHELL不换行 cd /home/username/username/projectname \u0026amp;\u0026amp; chmod +x ./scripts/*.sh \u0026amp;\u0026amp; ./scripts/projectnamedetimgstart.sh 等待程序完全启动后,在新终端中测试: SHELL不换行 conda activate projectname cd /home/username/username/projectname python test/test.py 查看logs文件夹下最新的以日期命名的.log文件:\nSHELL不换行 tail -f /home/username/username/projectname/logs/\u0026lt;修改为日期最新的那一个\u0026gt;.log 关闭程序:\nSHELL不换行 cd /home/username/username/projectname ./scripts/projectnamedetimgstop.sh 3.2 不联网 # 3.2.1 项目依赖打包 # 激活 projectname 环境:\nSHELL不换行 conda activate projectname 导出 conda 显式依赖列表:\nSHELL不换行 mkdir -p /home/username/projectname/extra \u0026amp;\u0026amp; conda list --explicit \u0026gt; /home/username/projectname/extra/conda_env.txt 导出 pip 包依赖列表:\nSHELL不换行 pip freeze \u0026gt; /home/username/projectname/extra/pip_requirements.txt 下载 conda 包:\nSHELL不换行 mkdir -p /home/username/projectname/extra/conda_pkg \u0026amp;\u0026amp; wget -i /home/username/projectname/extra/conda_env.txt -P /home/username/projectname/extra/conda_pkg/ 下载 pip 包:\nSHELL不换行 mkdir -p /home/username/projectname/extra/pip_pkg \u0026amp;\u0026amp; pip download -r /home/username/projectname/extra/pip_requirements.txt -d /home/username/projectname/extra/pip_pkg/ 修改 conda 显式依赖文件conda_env.txt内包的路径为要安装的设备下的对应包位置的绝对路径\n3.2.2 本地开发环境测试 # 解压我发给你的压缩包,解压后文件夹应该是/home/username/projectname\n创建新的 conda 环境(输入 y 确认,一共 2 次):\nSHELL不换行 conda create --name local_test --no-default-packages --offline \u0026amp;\u0026amp; conda activate local_test 在新环境中安装 conda 包:\nSHELL不换行 conda install --name local_test --file /home/username/projectname/extra/conda_env_local.txt 安装 pip 包:\nSHELL不换行 pip install --no-index --find-links /home/username/projectname/extra/pip_pkg/ -r /home/username/projectname/extra/pip_requirements.txt 检查包(有就说明无问题,无视出现ERROR: Pipe to stdout was broken):\nSHELL不换行 conda activate projectname (conda list | head -n 5 ) \u0026amp;\u0026amp; echo \u0026#34;======\u0026#34; \u0026amp;\u0026amp; (pip list | head -n 5 ) 测试运行程序:\n启动程序: SHELL不换行 cd /home/username/projectname \u0026amp;\u0026amp; chmod +x ./scripts/*.sh \u0026amp;\u0026amp; ./scripts/projectnamedetimgstart.sh 等待程序完全启动后,在新终端中测试: SHELL不换行 conda activate projectname cd /home/username/projectname python test/test.py 查看logs文件夹下最新的以日期命名的.log文件:\nSHELL不换行 tail -f /home/username/projectname/logs/\u0026lt;修改为日期最新的那一个\u0026gt;.log 关闭程序:\nSHELL不换行 cd /home/username/projectname ./scripts/projectnamedetimgstop.sh 3.2.3 服务器部署 # 创建新的 conda 环境: SHELL不换行 conda create --name projectname --no-default-packages --offline conda activate projectname 在新环境中安装 conda 包: SHELL不换行 conda activate projectname conda install --name projectname --file /home/username/username/projectname/extra/conda_env_server.txt 安装 pip 包: SHELL不换行 conda activate projectname pip install --no-index --find-links /home/username/username/projectname/extra/pip_pkg/ --no-deps -r /home/username/username/projectname/extra/pip_requirements.txt 检查包(有就说明无问题,无视出现ERROR: Pipe to stdout was broken): SHELL不换行 conda activate projectname (conda list | head -n 5 ) \u0026amp;\u0026amp; echo \u0026#34;======\u0026#34; \u0026amp;\u0026amp; (pip list | head -n 5 ) 测试运行程序: 启动程序: SHELL不换行 cd /home/username/username/projectname \u0026amp;\u0026amp; chmod +x ./scripts/*.sh \u0026amp;\u0026amp; ./scripts/projectnamedetimgstart.sh 等待程序完全启动后,在新终端中测试: SHELL不换行 conda activate projectname cd /home/username/username/username/projectname python test/test.py 查看logs文件夹下最新的以日期命名的.log文件: SHELL不换行 tail -f /home/username/username/projectname/logs/\u0026lt;修改为日期最新的那一个\u0026gt;.log 关闭程序: SHELL不换行 cd /home/username/username/projectname ./scripts/projectnamedetimgstop.sh 4. 设置程序开机自启和守护 # 安装 supervisor 并设置开机自启动 SHELL不换行 sudo apt install -y supervisor sudo systemctl enable supervisor sudo systemctl start supervisor sudo systemctl status supervisor 修改/home/username/username/projectname/deploy/illegaloperatedetimg.conf内的路径为实际路径 设置程序守护 SHELL不换行 cd /home/username/username/projectname sudo cp deploy/illegaloperatedetimg.conf /etc/supervisor/conf.d/ sudo supervisorctl reread sudo supervisorctl update 检查效果 手动打开/home/username/username/projectname/logs文件夹,查看是否有illegaloperatedetimg.log和以时间命名的.log文件,如果有则说明程序已经在后台运行。 重启,等待程序自启动,重复上一步操作 66. BUG 修复 # 运行启动脚本正常,但是在测试时,出现非法指令 (核心已转储) 解决方法: requirements.txt里面的 paddlepaddle 版本号修改为2.5.2,然后运行 SHELL不换行 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 88. 附件 # 点击展开查看附件 install_miniforge.sh\nSH不换行 # 检测系统架构并自动下载对应的 Miniforge 安装包(国内加速) ARCH=$(uname -m) if [ \u0026#34;$ARCH\u0026#34; = \u0026#34;x86_64\u0026#34; ]; then MINIFORGE_URL=\u0026#34;https://gh-proxy.com/github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh\u0026#34; elif [ \u0026#34;$ARCH\u0026#34; = \u0026#34;aarch64\u0026#34; ]; then MINIFORGE_URL=\u0026#34;https://gh-proxy.com/github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-aarch64.sh\u0026#34; else echo \u0026#34;不支持的架构: $ARCH\u0026#34; exit 1 fi # 下载 Miniforge 安装脚本 wget $MINIFORGE_URL -O Miniforge3.sh # 赋予脚本执行权限 chmod +x Miniforge3.sh # 执行安装脚本 ./Miniforge3.sh -b -p $HOME/miniforge3 # 添加 Miniforge 到 PATH echo \u0026#39;export PATH=\u0026#34;$HOME/miniforge3/bin:$PATH\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bashrc # 重新加载 shell(启用 Miniforge) source ~/.bashrc # 验证安装是否成功 conda --version pip.conf\nINI不换行 [global] # 设置默认的 PyPI 镜像源为清华大学的镜像 index-url = https://pypi.tuna.tsinghua.edu.cn/simple # 设置额外的镜像源 extra-index-url = https://mirrors.aliyun.com/pypi/simple/ https://pypi.doubanio.com/simple/ # 设置受信任的主机,用于避免 SSL 证书验证错误 trusted-host = pypi.tuna.tsinghua.edu.cn mirrors.aliyun.com pypi.doubanio.com # 禁用 pip 版本检查 disable-pip-version-check = true # 设置重试次数为 3 次 retries = 3 # 设置超时时间为 30 秒 timeout = 30 # 要求使用 HTTPS require-https = true # 显示进度条 progress-bar = on # 允许彩色输出 no-color = false [install] # 优先使用预编译的二进制包 prefer-binary = true # 允许编译源码包 no-compile = false # 安装依赖 no-deps = false # 不忽略已安装的包 ignore-installed = false # 显示脚本安装位置的警告 no-warn-script-location = false [freeze] # 在 freeze 输出中排除可编辑安装的包 exclude-editable = true # 显示所有已安装的包,包括不是直接安装的包 all = true [search] # 限制搜索结果数量为 10 个 limit = 10 # 在 PyPI 上搜索包 index = true .condarc\nYAML不换行 # conda 配置文件 # 配置可用的包源/频道 channels: - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge # 清华大学镜像的conda-forge源 - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main # 清华大学镜像的官方main源 - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free # 清华大学镜像的官方free源 - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r # 清华大学镜像的R语言源 - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/pro # 清华大学镜像的pro源 - conda-forge # 默认的conda-forge作为兜底(可选) # 不使用ssl验证 ssl_verify: false # 显示软件包的来源频道信息 show_channel_urls: true # 设置频道优先级为严格模式 # strict模式下将严格按照channels的顺序查找包,只会从第一个找到的频道安装包 channel_priority: strict # 禁止自动激活base环境 auto_activate_base: false # 创建新环境时默认安装的包 create_default_packages: - pip # 在新环境中默认安装pip(之后一直使用pip来安装包) illegaloperatedetimg.conf\nINI不换行 [program:illegaloperatedetimg] # 设置程序的工作目录 directory=/home/username/username/projectname # 指定要执行的命令 command=bash scripts/projectnamedetimgstart.sh # 设置为 true,表示 supervisor 启动时自动启动该程序 autostart=true # 设置为 true,表示程序崩溃时自动重启 autorestart=true # 设置最大重启尝试次数为 200 次 startretries=200 # 设置停止程序时等待的秒数,超过后强制终止 stopwaitsecs=10 # 将标准错误重定向到标准输出 redirect_stderr=true # 指定标准输出日志文件的位置 stdout_logfile=/home/username/username/projectname/logs/illegaloperatedetimg.log ","date":"2024年12月17日","externalUrl":null,"permalink":"/posts/ai%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%83%A8%E7%BD%B2%E6%B5%81%E7%A8%8B%E8%AE%B0%E5%BD%95/","section":"文章","summary":"","title":"AI服务器部署流程记录","type":"posts"},{"content":"","date":"2024年12月17日","externalUrl":null,"permalink":"/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/","section":"Tags","summary":"","title":"服务器","type":"tags"},{"content":"","date":"2024年12月17日","externalUrl":null,"permalink":"/categories/%E8%87%AA%E7%94%A8/","section":"分类","summary":"","title":"自用","type":"categories"},{"content":"","date":"2024年12月17日","externalUrl":null,"permalink":"/tags/%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2/","section":"Tags","summary":"","title":"项目部署","type":"tags"},{"content":"","date":"2024年12月9日","externalUrl":null,"permalink":"/tags/poe/","section":"Tags","summary":"","title":"Poe","type":"tags"},{"content":" 前言 # 由于使用 POE 较少,更新不及时,脚本可能不能使用,大部分情况下自行修改几个页面参数即可。 点击直接安装 今年年初的时候,出于跟进先进生产力的需求,在众多 AI 平台中选中了Poe。\n新写了一个每天更新的Poe 机器人积分页面,感兴趣的可以看看。 当时,它的优势如下:\n序号 项目 优势 1 价格 当时尼区价格是 99900 奈拉,加上一些消耗,大概 500 一年。\n而且可以年付,相对 GPT 和 Claude 的月付,低价区的收益更高。 2 功能 支持多平台的 AI 服务,包括各版本 GPT、各版本 Claude、各服务的长上下文版本,更新的也较快。\n并且是平台方接入官方 API,质量方面相对其他第三方更有保障。 3 是否易用 支持对话、支持机器人 API、IP 限制少(ps: 不像 Claude 老封号) 4 用量是否足够 每月 100 万积分,每次对话消耗固定积分,默认 API 的上下文。 当时是一个人使用每个月都用不完积分,直到前几天 Poe 更新了计算点系统,等于是每次对话是在基础固定积分上加上 Token 的消耗,这样相对之前来说,消耗积分更多了,特别是长上下文情况下,积分消耗几乎是指数上升。\n不过,相对来说,Poe 价格也还算实惠。再相比最近 GPT 降智、Claude 封号,Poe 仍然是一个不错的选择。\n下面是 Poe 和官 Key 对比:\n涨价之后我简单算了一下,转变思想,把 Poe 当成个 6 折 API 省着用。使用习惯上,非必要不追问,随手清除上下文,问题尽量一段话问出来(甚至使用沉浸式翻译把输入翻译成英文),总比买官 key 便宜。\n但是呢,这 Poe 又不显示每次对话消耗的积分,只能在设置页面看到当前总积分,很难去看到每次对话消耗的积分。 所以,我写了一个脚本,用来显示每次对话的消耗积分,方便自己控制消耗。\n1. 脚本效果 # 显示内容:\n重置时间 当前积分 本次消耗积分 2. 脚本内容 # 点击安装 JAVASCRIPT不换行 // ==UserScript== // @name Poe积分显示 // @namespace http://tampermonkey.net/ // @version 1.4 // @author xiadengma // @description 在每次对话的下方显示当前积分和本次对话消耗的积分,并在页面加载时显示最新积分 // @match *://poe.com/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @icon https://psc2.cf2.poecdn.net/assets/favicon.svg // @updateURL https://gist.githubusercontent.com/XIADENGMA/62e1239fdbd9c9b7ca0da285c2756fd1/raw/Poe_show_points_usage.user.js // @downloadURL https://gist.githubusercontent.com/XIADENGMA/62e1239fdbd9c9b7ca0da285c2756fd1/raw/Poe_show_points_usage.user.js // ==/UserScript== (function () { \u0026#39;use strict\u0026#39;; const DEBUG = false; const log = DEBUG ? console.log.bind(console, \u0026#39;[Poe积分显示]\u0026#39;) : () =\u0026gt; { }; const error = DEBUG ? console.error.bind(console, \u0026#39;[Poe积分显示]\u0026#39;) : () =\u0026gt; { }; const SELECTORS = { messagePair: \u0026#39;.ChatMessagesView_messageTuple__Jh5lQ\u0026#39;, inputMessage: \u0026#39;.Message_rightSideMessageBubble__ioa_i\u0026#39;, outputMessage: \u0026#39;.Message_leftSideMessageBubble__VPdk6\u0026#39;, stopButton: \u0026#39;button[aria-label=\u0026#34;停止信息\u0026#34;]\u0026#39;, actionBar: \u0026#39;section.ChatMessageActionBar_actionBar__gyeEs\u0026#39;, pointsElement: \u0026#39;.SettingsSubscriptionSection_computePointsValue___DLOM\u0026#39;, resetElement: \u0026#39;.SettingsSubscriptionSection_subtext__cZuI6\u0026#39;, messagePointLimitElement: \u0026#39;.DefaultMessagePointLimit_computePointsValue__YYJkB\u0026#39; }; const CONFIG = { checkInterval: 200, stableCount: 1, cacheExpiry: 5 * 60 * 1000, retryLimit: 3, retryDelay: 1000, maxPointsFetchAttempts: 5 }; const state = { pointsBeforeOutput: null, resetDate: \u0026#39;\u0026#39;, processedInputNodes: new WeakSet(), processedOutputNodes: new WeakSet(), initialLoadCompleted: false, isFetching: false, isInitialized: false, observer: null, }; GM_addStyle(` .points-info { font-size: 12px; padding: 8px 16px; margin: 8px 0; font-family: -apple-system, BlinkMacSystemFont, \u0026#34;Segoe UI\u0026#34;, Roboto, \u0026#34;Helvetica Neue\u0026#34;, Arial, sans-serif; background: rgba(255, 255, 255, 0.05); border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; } .points-info-highlight { color: #fff !important; } `); const throttle = (func, limit) =\u0026gt; { let lastFunc; let lastRan; return function () { const context = this; const args = arguments; if (!lastRan) { func.apply(context, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(function () { if ((Date.now() - lastRan) \u0026gt;= limit) { func.apply(context, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } } } async function fetchPoints(retryCount = 0) { if (state.isFetching) { await new Promise(resolve =\u0026gt; setTimeout(resolve, 100)); return fetchPoints(retryCount); } state.isFetching = true; try { log(\u0026#39;正在获取积分信息...\u0026#39;); const response = await new Promise((resolve, reject) =\u0026gt; { GM_xmlhttpRequest({ method: \u0026#39;GET\u0026#39;, url: \u0026#39;https://poe.com/settings\u0026#39;, onload: resolve, onerror: reject }); }); const doc = new DOMParser().parseFromString(response.responseText, \u0026#39;text/html\u0026#39;); const pointsElement = doc.querySelector(SELECTORS.pointsElement); const resetElement = doc.querySelector(SELECTORS.resetElement); const messagePointLimitElement = doc.querySelector(SELECTORS.messagePointLimitElement); if (!pointsElement) { throw new Error(\u0026#39;积分元素丢失\u0026#39;); } const currentPoints = parseInt(pointsElement.textContent.replace(/,/g, \u0026#39;\u0026#39;), 10); log(\u0026#39;当前积分:\u0026#39;, currentPoints); if (resetElement) { state.resetDate = resetElement.textContent.trim(); log(\u0026#39;重置时间:\u0026#39;, state.resetDate); } if (messagePointLimitElement) { state.messagePointLimit = parseInt(messagePointLimitElement.textContent.replace(/,/g, \u0026#39;\u0026#39;), 10); log(\u0026#39;全局单条信息预算:\u0026#39;, state.messagePointLimit); } state.pointsBeforeOutput = currentPoints; return currentPoints; } catch (err) { error(\u0026#39;获取积分信息失败\u0026#39;, err); if (retryCount \u0026lt; CONFIG.retryLimit) { log(`重试获取积分 (${retryCount + 1}/${CONFIG.retryLimit})...`); await new Promise(resolve =\u0026gt; setTimeout(resolve, CONFIG.retryDelay)); return fetchPoints(retryCount + 1); } throw err; } finally { state.isFetching = false; } } function monitorMessages() { log(\u0026#39;开始监听消息...\u0026#39;); if (state.observer) { state.observer.disconnect(); } state.observer = new MutationObserver(throttledHandleMutations); state.observer.observe(document.body, { childList: true, subtree: true }); detectInitialLoadCompletion(); } function detectInitialLoadCompletion() { let messageCount = 0; let lastMessageCount = 0; let stableCount = 0; const checkComplete = () =\u0026gt; { messageCount = document.querySelectorAll(SELECTORS.messagePair).length; if (messageCount === lastMessageCount) { if (++stableCount \u0026gt;= CONFIG.stableCount) { log(\u0026#39;初始加载完成,开始忽略历史消息\u0026#39;); state.initialLoadCompleted = true; displayLatestPointsInfo(); return; } } else { stableCount = 0; } lastMessageCount = messageCount; setTimeout(checkComplete, CONFIG.checkInterval); }; checkComplete(); } const throttledHandleMutations = throttle(handleMutations, 200); function handleMutations(mutations) { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const messagePair = node.closest(SELECTORS.messagePair); if (messagePair) { processMessagePair(messagePair); } } } } } const isMessageGenerating = () =\u0026gt; !!document.querySelector(SELECTORS.stopButton); function waitForMessageCompletion(outputMessage) { return new Promise(resolve =\u0026gt; { let lastContent = outputMessage.textContent; let stableCount = 0; const checkComplete = () =\u0026gt; { const currentContent = outputMessage.textContent; if (currentContent === lastContent \u0026amp;\u0026amp; !isMessageGenerating()) { if (++stableCount \u0026gt;= CONFIG.stableCount) { log(\u0026#39;消息输出已完成\u0026#39;); resolve(); return; } } else { stableCount = 0; } lastContent = currentContent; setTimeout(checkComplete, CONFIG.checkInterval); }; checkComplete(); }); } async function processMessagePair(messagePair) { if (!state.isInitialized || !state.initialLoadCompleted) { log(\u0026#39;脚本尚未完全初始化或页面未加载完成,跳过消息处理\u0026#39;); return; } const inputMessage = messagePair.querySelector(SELECTORS.inputMessage); if (inputMessage \u0026amp;\u0026amp; !state.processedInputNodes.has(inputMessage)) { log(\u0026#39;检测到新的输入消息\u0026#39;); state.processedInputNodes.add(inputMessage); log(\u0026#39;输入前积分:\u0026#39;, state.pointsBeforeOutput); } const outputMessage = messagePair.querySelector(SELECTORS.outputMessage); if (outputMessage \u0026amp;\u0026amp; !state.processedOutputNodes.has(outputMessage)) { if (!outputMessage.textContent.trim()) { log(\u0026#39;输出消息尚未完整,等待加载...\u0026#39;); return; } log(\u0026#39;检测到新的输出消息\u0026#39;); state.processedOutputNodes.add(outputMessage); const pointsBeforeOutput = state.pointsBeforeOutput; log(\u0026#39;输出前积分:\u0026#39;, pointsBeforeOutput); try { await waitForMessageCompletion(outputMessage); log(\u0026#39;消息已完全输出,等待积分更新...\u0026#39;); await new Promise(resolve =\u0026gt; setTimeout(resolve, 500)); let pointsAfterOutput = pointsBeforeOutput; for (let i = 0; i \u0026lt; CONFIG.maxPointsFetchAttempts; i++) { const newPoints = await fetchPoints(); if (newPoints !== pointsBeforeOutput) { pointsAfterOutput = newPoints; break; } log(`第 ${i + 1} 次尝试获取积分,未发现变化`); if (i \u0026lt; CONFIG.maxPointsFetchAttempts - 1) { await new Promise(resolve =\u0026gt; setTimeout(resolve, 1000)); } } const pointsUsed = pointsBeforeOutput - pointsAfterOutput; log(\u0026#39;输出后积分:\u0026#39;, pointsAfterOutput); log(\u0026#39;本次对话消耗积分:\u0026#39;, pointsUsed); if (pointsUsed \u0026gt; 0) { displayPointsInfo(messagePair, pointsAfterOutput, pointsUsed); } state.pointsBeforeOutput = pointsAfterOutput; } catch (err) { error(\u0026#39;积分更新或消息完成失败:\u0026#39;, err); } } } function displayPointsInfo(messagePair, currentPoints, pointsUsed) { if (messagePair.querySelector(\u0026#39;.points-info\u0026#39;)) return; log(\u0026#39;显示积分信息\u0026#39;); const pointsInfo = createPointsInfoElement(currentPoints, pointsUsed); const actionBar = messagePair.querySelector(SELECTORS.actionBar); if (actionBar) { actionBar.parentNode.insertBefore(pointsInfo, actionBar); } else { messagePair.appendChild(pointsInfo); } } function createPointsInfoElement(currentPoints, pointsUsed = null, isInitialLoad = false) { const pointsInfo = document.createElement(\u0026#39;div\u0026#39;); pointsInfo.className = \u0026#39;points-info\u0026#39;; const infoItems = [ { text: `重置时间: ${state.resetDate}`, color: \u0026#39;#555\u0026#39; }, { text: `当前积分: ${currentPoints.toLocaleString()}`, color: \u0026#39;#888\u0026#39;, highlight: isInitialLoad } ]; if (pointsUsed !== null) { infoItems.push({ text: `本次消耗积分: ${pointsUsed}`, color: \u0026#39;#fff\u0026#39; }); } pointsInfo.innerHTML = infoItems.map(item =\u0026gt; `\u0026lt;div style=\u0026#34;color: ${item.color}\u0026#34; ${item.highlight ? \u0026#39;class=\u0026#34;points-info-highlight\u0026#34;\u0026#39; : \u0026#39;\u0026#39;}\u0026gt;${item.text}\u0026lt;/div\u0026gt;` ).join(\u0026#39;\u0026#39;); return pointsInfo; } async function displayLatestPointsInfo() { const messagePairs = document.querySelectorAll(SELECTORS.messagePair); if (messagePairs.length \u0026gt; 0) { const lastMessagePair = messagePairs[messagePairs.length - 1]; const currentPoints = await fetchPoints(); const pointsInfo = createPointsInfoElement(currentPoints, null, true); const existingPointsInfo = lastMessagePair.querySelector(\u0026#39;.points-info\u0026#39;); if (existingPointsInfo) { existingPointsInfo.replaceWith(pointsInfo); } else { const actionBar = lastMessagePair.querySelector(SELECTORS.actionBar); if (actionBar) { actionBar.parentNode.insertBefore(pointsInfo, actionBar); } else { lastMessagePair.appendChild(pointsInfo); } } } } function handleUrlChange() { log(\u0026#39;URL已更改,重置状态\u0026#39;); state.processedInputNodes = new WeakSet(); state.processedOutputNodes = new WeakSet(); state.initialLoadCompleted = false; state.pointsBeforeOutput = null; init(); } async function init() { try { const initialPoints = await fetchPoints(); state.pointsBeforeOutput = initialPoints; state.isInitialized = true; log(\u0026#39;初始化完成,当前积分:\u0026#39;, initialPoints); monitorMessages(); displayLatestPointsInfo(); // 在初始化时显示最新积分信息 window.addEventListener(\u0026#39;popstate\u0026#39;, handleUrlChange); let lastUrl = location.href; new MutationObserver(() =\u0026gt; { const url = location.href; if (url !== lastUrl) { lastUrl = url; handleUrlChange(); } }).observe(document, { subtree: true, childList: true }); } catch (err) { error(\u0026#39;初始化失败:\u0026#39;, err); } } init(); })(); 3. 更新记录 # 点击展开查看更新记录 2025-02-03:修复脚本;添加每次重新进入页面在最后一次对话下面显示当前积分等信息;添加脚本图标 2025-02-04:添加脚本发布和更新地址 ","date":"2024年12月9日","externalUrl":null,"permalink":"/posts/poe%E6%98%BE%E7%A4%BA%E6%B6%88%E8%80%97%E7%A7%AF%E5%88%86%E8%84%9A%E6%9C%AC/","section":"文章","summary":"","title":"Poe显示消耗积分脚本","type":"posts"},{"content":"","date":"2024年12月9日","externalUrl":null,"permalink":"/tags/tampermonkey/","section":"Tags","summary":"","title":"Tampermonkey","type":"tags"},{"content":"","date":"2024年12月9日","externalUrl":null,"permalink":"/tags/%E5%AE%9E%E7%94%A8%E5%B7%A5%E5%85%B7/","section":"Tags","summary":"","title":"实用工具","type":"tags"},{"content":"","date":"2024年12月9日","externalUrl":null,"permalink":"/tags/%E8%84%9A%E6%9C%AC/","section":"Tags","summary":"","title":"脚本","type":"tags"},{"content":"","date":"2024年12月9日","externalUrl":null,"permalink":"/tags/git/","section":"Tags","summary":"","title":"Git","type":"tags"},{"content":"","date":"2024年12月9日","externalUrl":null,"permalink":"/series/git%E6%8E%8C%E6%8F%A1%E4%B8%8E%E5%AE%9E%E8%B7%B5/","section":"Series","summary":"","title":"Git掌握与实践","type":"series"},{"content":" 1. Git 基础 # 1.1 为什么要用 Git?什么是 Git? # 当你想要使用 Git 的时候,你一定遇到了版本控制相关的问题。\n比如:想要修改 A.txt,但是又担心现在的 A.txt 文件内容仍然需要使用。一些人会选择复制一份 A.txt 为 A_1.txt 的,再去修改 A_1.txt。长此以往,你可能有了 A_99.txt、A_100.txt,并且过了一段时间,你已经不知道哪一文件夹修改了哪一部分。这个时候,点开 Git 教程,它会给你答案。\n再比如:你有一个 A.txt 需要给你的同事甲协作,让他修改部分内容,同时,你也要修改 A.txt。那情况就更麻烦了,如果没有合适的工具,一些人会选择把同事修改完的命名为 A_甲_1.txt,再把自己修改的命名为 A_1.txt,然后同时打开这两个文件,手动对比区别,最后归纳为 A_2.txt。这个时候,你想到之后还要很多次进行一样的操作,太耗费时间,点开 Git 教程,它会给你答案。\nGit 是一个分布式版本控制系统。\nGit 就像一个强大的时间机器,它能记录你项目的所有修改。想象一下,你正在写一篇文章,每修改一个段落,Git 都会帮你保存一个快照。这样,你可以随时回到任何一个历史版本,比较不同版本之间的差异,甚至可以把几个人的修改合并到一起。简单来说,Git 是一个让你安全、高效地管理项目代码的工具,它能帮你追踪修改、方便协作、并随时回到过去。\n下面是版本控制系统的对比和你选择 Git 的理由:\n特性/对比项 Git SVN (Subversion) Mercurial (Hg) CVS 架构 分布式 (Distributed) 集中式 (Centralized) 分布式 (Distributed) 集中式 (Centralized) 分支管理 强大且灵活,轻量级,鼓励频繁使用 相对笨重,创建分支成本高 较好,与 Git 类似 较弱,不鼓励频繁使用 性能 非常快,尤其在大型项目和大量提交时 较慢,性能瓶颈明显 相对较快,但不如 Git 性能较差,尤其在大型项目时 离线工作 支持,本地有完整仓库 不支持,依赖中央服务器 支持,本地有完整仓库 不支持,依赖中央服务器 资源占用 占用磁盘空间较多,但操作高效 占用磁盘空间较少,但操作相对较慢 占用磁盘空间较多,但操作高效 占用磁盘空间较少,但操作相对较慢 复杂性 学习曲线稍陡峭,但功能强大 相对简单易学,但功能较少 学习曲线较平缓,功能类似 Git 较为简单,但功能较少 流行度 目前是行业标准,非常流行 逐渐被 Git 取代,但仍有部分应用 受欢迎程度不及 Git 已经过时,不推荐使用 选择 Git 的理由 个人开发者 团队开发者 优势 - 版本控制的强大能力,方便回溯 - 高效协作,多人同时开发 - 本地操作,离线也能工作 - 强大的分支管理,灵活应对复杂需求 - 方便实验新想法,随时回滚 - 代码审查,提高代码质量 - 保护代码,防止误删和丢失 - 轻松处理合并冲突,避免开发混乱 - 学习现代开发标准,提高竞争力 - 广泛的社区支持,易于解决问题 - 为协作开发打下基础,方便未来团队合作 - 支持多种开发模式(如 Gitflow) 总结 更有效率地管理个人项目,保护代码资产 提高团队协作效率,确保项目高质量和快速交付 1.2 Git 安装 # 请自行搜索:“怎么在 xxx 系统安装 Git”。\n2. Git 基础使用 # 以下以 Linux 环境为例。\n让我们开始,新建文件夹 learn_git,并进入该文件夹:\nSHELL不换行 mkdir learn_git cd learn_git以后我们的操作都在 learn_git文件夹下进行。\n2.1 Git 基础配置 # 在正式使用 Git 创建并管理项目前,我们还需要对 Git 进行初始的配置。Git 提供了 git config工具,来帮助我们进行配置。在终端输入:\nSHELL不换行 git config --list --show-origin我们可以查看所有的配置以及它们所在的文件信息。\n终端中输出了配置文件名称、路径以及详细的配置项。Git 的配置文件总共有三个,分别存储在不同的地方,并对应不同的权限(优先级:系统\u0026gt;用户\u0026gt;仓库)。\n存储在安装目录下 etc路径下的 gitconfig文件,它是系统全局配置文件,它包含系统上每一个用户及他们仓库的通用配置。 在当前系统用户下的 .gitconfig文件,这是当前用户的全局配置文件,它存储了仓库都共享的通用配置选项。 存储在仓库目录下的 .git/config文件,是针对仓库的配置文件,它存储了仓库的配置信息。 如果你安装了 vscode,你可以设置 git的默认编辑器为 vscode:\nSHELL不换行 git config --global core.editor \u0026#34;code --wait” 2.2 Git 基础概念 # 2.2.1 三个功能区域 # graph LR A[工作区] --\u003e|git add| B(暂存区); B --\u003e|git commit| C[版本库]; C --\u003e|检测追踪项目文件| A 工作区(Working Directory) 工作区是指包含项目代码的本地目录,是我们平常在编辑器中修改和操作的目录。 暂存区(Stage/Index) 暂存区是用于存储即将被提交到版本库中的文件快照。我们可以多次预览和审查文件是否正确修改。 版本库(Repository) 版本库可以简单理解成一个目录,这个目录里面的所有文件都可以被 Git 管理起来,每个文件的修改、删除,Git 都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。 2.2.2 三种文件状态 # 已修改(modified): 表示文件已被修改但尚未被暂存。我们在工作区修改了文件,并点击了保存,这时候文件状态会变成已修改。也就是说,你已经对文件进行了更改,但还没有使用 git add命令将其添加到暂存区域。 已暂存(staged): 表示对一个已修改文件的当前版本做出了标记,以便在下一次提交时将其纳入版本控制。也就是说,使用 git add命令将修改的文件添加到暂存区域中。 已提交(committed): 表示数据已经被安全地保存在本地数据库中。也就是说,在你执行了 git commit命令之后,所有的更改都被保存到了 Git 仓库的历史记录中。 2.2.3 .git 文件夹简单介绍 # 当我们执行了 git init初始化仓库之后,Git 会创建一个 .git文件夹,下面是部分文件信息\nHEAD:指向当前活动分支的指针。 config:版本库的配置文件,包括用户名、邮件地址、编辑器等信息。 description:用于在 GitWeb 等工具中显示有关版本库的描述信息。 hooks/:包含可自定义的 Git 钩子脚本,用于实现特定功能或执行自动化任务。 objects/:包含 Git 对象数据库,其中存储了版本库中所有的文件和提交历史记录。 refs/:包含分支和标签的指针文件,其中保存了每个分支和标签及其所指向的提交 ID。 index:暂存区的索引文件,用于记录下一次提交要包括的文件。 2.3 学习掌握 # 2.3.1 创建一个版本库并提交第一个文件 # 在已经新建文件夹 learn_git并进入该文件夹的前提下。\n初始化一个 Git 仓库并设置用户名和邮箱\nSHELL不换行 git init git config user.name \u0026#34;yourname\u0026#34; git config user.email \u0026#34;youremail\u0026#34; 新建一个文件 test.txt\nSHELL不换行 echo \u0026#34;learn_git 2.3.1\u0026#34; \u0026gt; test.txt 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 尚无提交 未跟踪的文件: (使用 \u0026#34;git add \u0026lt;文件\u0026gt;...\u0026#34; 以包含要提交的内容) test.txt 提交为空,但是存在尚未跟踪的文件(使用 \u0026#34;git add\u0026#34; 建立跟踪) 将文件添加到暂存区\nSHELL不换行 git add test.txt 再次查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 尚无提交 要提交的变更: (使用 \u0026#34;git rm --cached \u0026lt;文件\u0026gt;...\u0026#34; 以取消暂存) 新文件: test.txt 提交文件到版本库\nSHELL不换行 git commit -m \u0026#34;add test.txt\u0026#34; 再次查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 无文件要提交,干净的工作区 2.3.2 修改文件并提交 # 修改文件 test.txt\nSHELL不换行 echo \u0026#34;learn_git 2.3.2\u0026#34; \u0026gt; test.txt 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 尚未暂存以备提交的变更: (使用 \u0026#34;git add \u0026lt;文件\u0026gt;...\u0026#34; 更新要提交的内容) (使用 \u0026#34;git restore \u0026lt;文件\u0026gt;...\u0026#34; 丢弃工作区的改动) 修改: test.txt 修改尚未加入提交(使用 \u0026#34;git add\u0026#34; 和/或 \u0026#34;git commit -a\u0026#34;) 这次我们使用 git commit -am命令,它会自动将所有已修改的文件添加到暂存区并提交\nSHELL不换行 git commit -a -m \u0026#34;modify test.txt to 2.3.2\u0026#34; 再次查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 无文件要提交,干净的工作区 2.3.3 查看提交历史 # 查看提交历史\nSHELL不换行 git log输出:\nSHELL不换行 commit SHA-1 40-character commit hash (HEAD -\u0026gt; main) Author: yourname \u0026lt;youremail\u0026gt; Date: Day of week Month Date hh:mm:ss Year +UTC modify test.txt to 2.3.2 commit SHA-1 40-character commit hash Author: yourname \u0026lt;youremail\u0026gt; Date: Day of week Month Date hh:mm:ss Year +UTC add test.txt 查看提交历史整洁版\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_2 year-month-day | modify test.txt to 2.3.2 (HEAD -\u0026gt; main) [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 2.3.4 撤销修改(切换版本) # 查看当前文件内容\nSHELL不换行 cat test.txt输出:\nSHELL不换行 learn_git 2.3.2 撤销修改,切换到指定版本\nSHELL不换行 git checkout 7_hash_1(为2.3.3查看提交历史整洁版输出结果的哈希值)输出:\nSHELL不换行 注意:正在切换到 \u0026#39;7_hash_1\u0026#39;。 您正处于分离头指针状态。您可以查看、做试验性的修改及提交,并且您可以在切换 回一个分支时,丢弃在此状态下所做的提交而不对分支造成影响。 如果您想要通过创建分支来保留在此状态下所做的提交,您可以通过在 switch 命令 中添加参数 -c 来实现(现在或稍后)。例如: git switch -c \u0026lt;新分支名\u0026gt; 或者撤销此操作: git switch - 通过将配置变量 advice.detachedHead 设置为 false 来关闭此建议 HEAD 目前位于 7_hash_1 add test.txt 查看当前文件内容\nSHELL不换行 cat test.txt输出:\nSHELL不换行 learn_git 2.3.1 切换回最新版本\nSHELL不换行 git checkout main输出:\nSHELL不换行 之前的 HEAD 位置是 7_hash_1 add test.txt 切换到分支 \u0026#39;main\u0026#39; 查看当前文件内容\nSHELL不换行 cat test.txt输出:\nSHELL不换行 learn_git 2.3.2 2.3.5 撤销修改-本地已保存状态 # 修改文件 test.txt\nSHELL不换行 echo \u0026#34;learn_git 2.3.5\u0026#34; \u0026gt; test.txt 查看当前文件内容\nSHELL不换行 cat test.txt输出:\nSHELL不换行 learn_git 2.3.5 恢复本地修改\nSHELL不换行 git checkout test.txt 查看当前文件内容\nSHELL不换行 cat test.txt输出:\nSHELL不换行 learn_git 2.3.2 2.3.6 撤销修改-已暂存状态下 # 修改文件 test.txt SHELL不换行 echo \u0026#34;learn_git 2.3.6\u0026#34; \u0026gt; test.txt 添加到暂存区\nSHELL不换行 git add test.txt 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 要提交的变更: (使用 \u0026#34;git restore --staged \u0026lt;文件\u0026gt;...\u0026#34; 以取消暂存) 修改: test.txt 撤销暂存文件\nSHELL不换行 git reset HEAD test.txt 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 尚未暂存以备提交的变更: (使用 \u0026#34;git add \u0026lt;文件\u0026gt;...\u0026#34; 更新要提交的内容) (使用 \u0026#34;git restore \u0026lt;文件\u0026gt;...\u0026#34; 丢弃工作区的改动) 修改: test.txt 修改尚未加入提交(使用 \u0026#34;git add\u0026#34; 和/或 \u0026#34;git commit -a\u0026#34;) 恢复本地修改\nSHELL不换行 git checkout test.txt 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 无文件要提交,干净的工作区 查看当前文件内容\nSHELL不换行 cat test.txt输出:\nSHELL不换行 learn_git 2.3.2 2.3.7 辨析 git checkout和 git reset # 特性 git checkout git reset 主要用途 切换分支或恢复工作树文件 重置当前 HEAD 到指定状态 影响范围 主要影响工作目录 可以影响暂存区和/或工作目录 常见用法 git checkout \u0026lt;branch\u0026gt; git checkout \u0026lt;file\u0026gt; git reset --soft \u0026lt;commit\u0026gt; git reset --mixed \u0026lt;commit\u0026gt; git reset --hard \u0026lt;commit\u0026gt; 对提交历史的影响 不改变提交历史 可以改变提交历史(使用 --hard) 文件层面操作 可以检出单个文件 主要用于提交层面,但也可以重置单个文件 分支操作 可以创建和切换分支 不能直接切换分支 数据安全性 相对安全,不会丢失数据 使用 --hard可能会丢失未提交的更改 撤销操作 可以撤销工作目录的修改 可以撤销提交、暂存的更改 在进行 Git 操作时,请根据您的需要选择正确的命令:\n如果您想要撤销某些更改并将历史记录回滚到旧的提交,则应使用 git reset命令。 如果您只是想查看其他提交的状态或切换到不同的分支,则应使用 git checkout命令。 2.3.8 撤销提交 # 修改文件 test.txt\nSHELL不换行 echo \u0026#34;learn_git 2.3.8\u0026#34; \u0026gt; test.txt 添加所有修改的文件到暂存区并提交\nSHELL不换行 git commit -am \u0026#34;modify test.txt to 2.3.8\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_3 year-month-day | modify test.txt to 2.3.8 (HEAD -\u0026gt; main) [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 撤销最后一次提交,并创建一个新的提交来还原更改。\nSHELL不换行 git revert HEAD --no-edit 查看当前文件内容\nSHELL不换行 cat test.txt输出:\nSHELL不换行 learn_git 2.3.2 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_4 year-month-day | Revert \u0026#34;modify test.txt to 2.3.8\u0026#34; (HEAD -\u0026gt; main) [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 恢复上一次撤销提交\nSHELL不换行 git reset --hard HEAD^ 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_3 year-month-day | modify test.txt to 2.3.8 (HEAD -\u0026gt; main) [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 查看当前文件内容\nSHELL不换行 cat test.txt输出:\nSHELL不换行 learn_git 2.3.8 撤销到指定提交\nSHELL不换行 git revert 7_hash_2 --no-edit输出:\nSHELL不换行 自动合并 test.txt 冲突(内容):合并冲突于 test.txt 错误:不能还原 6396343... modify test.txt to 2.3.2 提示: 冲突解决完毕后,用 \u0026#39;git add \u0026lt;路径\u0026gt;\u0026#39; 或 \u0026#39;git rm \u0026lt;路径\u0026gt;\u0026#39; 提示: 命令标记修正后的文件 提示: Disable this message with \u0026#34;git config advice.mergeConflict false\u0026#34; 这时发现有冲突,我们需要手动解决冲突,编辑文件 test.txt,修改为:\nPLAINTEXT不换行 learn_git 2.3.2即采用传入的更改\n标记解决并完成 revert\nSHELL不换行 git add test.txt git revert --continue 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_5 year-month-day | Revert \u0026#34;modify test.txt to 2.3.2\u0026#34; (HEAD -\u0026gt; main) [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 2.3.9 删除提交 # 修改文件并提交\nSHELL不换行 echo \u0026#34;learn_git 2.3.9\u0026#34; \u0026gt; test.txt git commit -am \u0026#34;modify test.txt to 2.3.9\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_6 year-month-day | modify test.txt to 2.3.9 (HEAD -\u0026gt; main) [yourname] * 7_hash_5 year-month-day | Revert \u0026#34;modify test.txt to 2.3.2\u0026#34; [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 删除提交(标记删除 7_hash_3后面所有提交)\nSHELL不换行 git reset --hard 7_hash_3 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_3 year-month-day | modify test.txt to 2.3.8 (HEAD -\u0026gt; main) [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 2.3.10 修改提交内容 # 修改文件并提交\nSHELL不换行 echo \u0026#34;learn_git 2.3.10\u0026#34; \u0026gt; test.txt git commit -am \u0026#34;modify test.txt to 2.3.10\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_7 year-month-day | modify test.txt to 2.3.10 (HEAD -\u0026gt; main) [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 再次修改文件并覆盖上一次提交\nSHELL不换行 echo \u0026#34;learn_git 2.3.10_1\u0026#34; \u0026gt; test.txt git commit --amend -m \u0026#34;modify test.txt to 2.3.10_1\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 (HEAD -\u0026gt; main) [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 2.3.11 移动文件 # 移动文件 test.txt到 test/test.txt\nSHELL不换行 mkdir test git mv test.txt test/test.txt 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 要提交的变更: (使用 \u0026#34;git restore --staged \u0026lt;文件\u0026gt;...\u0026#34; 以取消暂存) 重命名: test.txt -\u0026gt; test/test.txt 尚未暂存以备提交的变更: (使用 \u0026#34;git add \u0026lt;文件\u0026gt;...\u0026#34; 更新要提交的内容) (使用 \u0026#34;git restore \u0026lt;文件\u0026gt;...\u0026#34; 丢弃工作区的改动) 修改: test/test.txt 提交文件移动\nSHELL不换行 git commit -m \u0026#34;move test.txt to test/test.txt\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_9 year-month-day | move test.txt to test/test.txt (HEAD -\u0026gt; main) [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 恢复到上一次提交\nSHELL不换行 git reset --hard 7_hash_8 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 (HEAD -\u0026gt; main) [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 移动文件 test.txt到 test_1/test.txt\nSHELL不换行 mkdir test_1 mv test.txt test_1 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 尚未暂存以备提交的变更: (使用 \u0026#34;git add/rm \u0026lt;文件\u0026gt;...\u0026#34; 更新要提交的内容) (使用 \u0026#34;git restore \u0026lt;文件\u0026gt;...\u0026#34; 丢弃工作区的改动) 删除: test.txt 未跟踪的文件: (使用 \u0026#34;git add \u0026lt;文件\u0026gt;...\u0026#34; 以包含要提交的内容) test_1/ 修改尚未加入提交(使用 \u0026#34;git add\u0026#34; 和/或 \u0026#34;git commit -a\u0026#34;) 暂存文件\nSHELL不换行 git add test_1 删除文件\nSHELL不换行 git rm test.txt 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 要提交的变更: (使用 \u0026#34;git restore --staged \u0026lt;文件\u0026gt;...\u0026#34; 以取消暂存) 重命名: test.txt -\u0026gt; test_1/test.txt 暂存文件并提交\nSHELL不换行 git commit -am \u0026#34;move test.txt to test_1/test.txt\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_10 year-month-day | move test.txt to test_1/test.txt (HEAD -\u0026gt; main) [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 重命名文件 test_1/test.txt为 test_1/test_1.txt\nSHELL不换行 git mv test_1/test.txt test_1/test_1.txt 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 要提交的变更: (使用 \u0026#34;git restore --staged \u0026lt;文件\u0026gt;...\u0026#34; 以取消暂存) 重命名: test_1/test.txt -\u0026gt; test_1/test_1.txt 暂存文件并提交\nSHELL不换行 git commit -am \u0026#34;rename test_1/test.txt to test_1/test_1.txt\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt (HEAD -\u0026gt; main) [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 2.3.12 忽略文件(.gitignore) # 创建一个 .gitignore文件,并设置忽略 *.log文件\nSHELL不换行 echo \u0026#34;*.log\u0026#34; \u0026gt; .gitignore 暂存文件并提交\nSHELL不换行 git add .gitignore git commit -m \u0026#34;add .gitignore\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_12 year-month-day | add .gitignore (HEAD -\u0026gt; main) [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 创建测试文件 test.log\nSHELL不换行 echo \u0026#34;test.log 2.3.12\u0026#34; \u0026gt; test.log 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 main 无文件要提交,干净的工作区 2.3.13 标签操作 # 把最新提交打上标签\nSHELL不换行 git tag v1.0 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_12 year-month-day | add .gitignore (HEAD -\u0026gt; main, tag: v1.0) [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 删除标签\nSHELL不换行 git tag -d v1.0 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_12 year-month-day | add .gitignore (HEAD -\u0026gt; main) [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 3. Git 进阶使用 # 3.1 Git 对象存储机制 # Git 使用一种称为对象存储的机制来管理和处理所有文件和目录内容,以及它们之间的关系。\nGit 对象存储机制包括以下几个方面:\nGit 对象:Git 中的所有数据都被视为对象。一个对象可以是一个文件的内容(Blob)、目录结构(Tree)、提交记录(Commit)或标签(Tag)等。 SHA-1 哈希:每个 Git 对象都具有与其内容相关联的唯一标识符,该标识符由其内容的 SHA-1 哈希值生成。这使得 Git 可以轻松地检测文件内容的更改。 Git 数据库:Git 会将所有对象存储在一个数据库中,该数据库位于 .git/objects目录下。该目录包含一个名为 info的子目录和一个名为 pack的子目录,其中 info子目录包含有关对象的元数据,而 pack子目录包含经过压缩的对象。 3.1.1 Git 对象 # Git 数据库包含了许多不同类型的对象,这些对象相互关联形成一个有向无环图(DAG)。在 Git 中,每个对象都由 SHA-1 哈希值唯一标识,并按照其哈希值存储在 .git 目录下的 objects 目录中。\nGit 对象包括四种主要类型:blob、tree、commit 和 tag。\nBlob:Blob 对象代表一个文件的内容。每个 blob 都由一个唯一的 SHA-1 哈希值标识,并存储在 .git/objects 目录下。Blob 对象是 Git 数据库的基本单位,它们包含文件的原始内容而不包含任何元数据。 Tree:Tree 对象代表一个目录或文件夹,在 Git 中被称为“树”。它可以包含多个 blob 或其他 tree 对象,以及相关元数据,如文件名和权限等信息。Tree 对象也由一个 SHA-1 哈希值唯一标识,并存储在 .git/objects 目录下的 objects/trees 子目录中。 Commit:Commit 对象代表一个提交记录,包含了提交的作者、提交者、提交时间、提交信息等元数据,以及指向一个 tree 对象的指针。每个 commit 对象都由一个唯一的 SHA-1 哈希值标识,并存储在 .git/objects 目录下的 objects/commits 子目录中。 Tag:Tag 对象代表一个标签,用于标记某个特定的 commit 对象。Tag 对象包含了标签的名称、标签的类型、标签的作者、标签的创建时间等元数据,以及指向一个 commit 对象的指针。Tag 对象也由一个唯一的 SHA-1 哈希值标识,并存储在 .git/objects 目录下的 objects/tags 子目录中。 ps:有关 git cat-file会帮助你深入了解 Git 内部机制,这里不多做介绍,需要了解的可以自行查阅。\n3.2 分支管理 # 3.2.1 创建分支 # 在 Git 中,分支是指针,它指向某个提交记录。在 Git 存储库中,默认情况下有一个名为 main的主分支,该分支指向最新的提交记录。\n使用分支可以轻松地将代码库分成不同的版本,并在这些版本之间进行切换和合并操作。例如,如果你想尝试新功能或修复错误,可以创建一个新分支,在该分支上进行更改,而不会影响主分支。一旦更改准备好,就可以将其合并回主分支中。这使得协作变得更加容易,因为团队成员可以在自己的分支上开发新功能,而不必担心与其他人的更改冲突。\n创建一个新分支 feature\nSHELL不换行 git checkout -b feature 查看所有分支\nSHELL不换行 git branch输出:\nSHELL不换行 * feature main 查看仓库状态\nSHELL不换行 git status输出:\nSHELL不换行 位于分支 feature 无文件要提交,干净的工作区 新建文件 feature.txt并提交\nSHELL不换行 echo \u0026#34;feature branch\u0026#34; \u0026gt; feature.txt git add feature.txt git commit -m \u0026#34;add feature.txt\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_13 year-month-day | add feature.txt (HEAD -\u0026gt; feature) [yourname] * 7_hash_12 year-month-day | add .gitignore (main) [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 3.2.2 合并分支 # 在 Git 中,合并分支是一种将两个不同的分支中的代码更改合并到一个分支中的操作。这允许团队成员在不同的分支中开发功能和修复错误,并最终将它们合并到主分支或其他稳定分支中。\n切换到主分支\nSHELL不换行 git checkout main 合并 feature分支到 main分支\nSHELL不换行 git merge feature 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_13 year-month-day | add feature.txt (HEAD -\u0026gt; main, feature) [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 3.2.3 合并冲突 # 在 Git 中,当尝试将两个不同的分支合并时,可能会出现合并冲突。这通常发生在两个分支上都更改了同一文件的同一部分时。\n修改文件 feature.txt并提交\nSHELL不换行 echo \u0026#34;changed by main 3.2.3\u0026#34; \u0026gt; feature.txt git commit -am \u0026#34;modify feature.txt by main\u0026#34; 切换到 feature分支\nSHELL不换行 git checkout feature 修改文件 feature.txt并提交\nSHELL不换行 echo \u0026#34;changed by feature 3.2.3\u0026#34; \u0026gt; feature.txt git commit -am \u0026#34;modify feature.txt by feature\u0026#34; 合并 feature分支到 main分支\nSHELL不换行 git checkout main git merge feature输出:\nSHELL不换行 自动合并 feature.txt 冲突(内容):合并冲突于 feature.txt 自动合并失败,修正冲突然后提交修正的结果。 手动合并冲突\nSHELL不换行 cat feature.txt输出:\nSHELL不换行 \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD changed by main 3.2.3 ======= changed by feature 3.2.3 \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; feature由于我之前设置了 vscode作为默认编辑器,所以这里会自动打开 vscode编辑器,手动解决冲突,点击 保留双方更改:\nSHELL不换行 changed by main 3.2.3 changed by feature 3.2.3 提交合并结果\nSHELL不换行 git add feature.txt git commit -m \u0026#34;merge feature branch and resolve conflict\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_15 year-month-day | merge feature branch and resolve conflict (HEAD -\u0026gt; main) [yourname] |\\ | * 7_hash_14 year-month-day | modify feature.txt by feature (feature) [yourname] * | 7_hash_13 year-month-day | modify feature.txt by main [yourname] |/ * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 3.2.4 变基和合并 # Rebasing(变基) 和 Merging(合并) 是 Git 中常用的两种集成分支的方法。这两种方法都可以将一个分支的更改合并到另一个分支中,但它们的内部实现不同,因此其使用场景和结果也略有不同。\nRebasing Rebasing 是一种将分支更改应用于目标分支的方法,它会将分支的每个提交都转移到目标分支的顶部,并在每个提交之间将目标分支的更改应用于分支更改。这意味着,当你使用 Rebasing 方法时,最终的提交历史记录是一个线性的历史记录,其中所有更改都按照时间顺序排列。\n主要优点: 提交历史记录更加干净和有序。 可以快速解决由于分支变化而导致的代码冲突。 主要缺点: 可能需要耗费大量时间和精力来处理冲突。 对于多人协作开发而言,可能需要进行协调才能确保不出现问题。 Merging Merging 是一种将分支更改合并到目标分支的方法。它会创建一个新的合并提交,该提交包含了目标分支和要合并的分支的全部更改。这意味着,当你使用 Merging 方法时,最终的提交历史记录将包含合并提交以及两个分支的更改历史记录。\n主要优点: 容易理解和使用。 不需要手动处理冲突。 主要缺点: 提交历史记录可能会变得杂乱无序,难以阅读和理解。 如果分支更改频繁,可能会导致大量的冲突。 综上所述,Rebasing 和 Merging 都是有效的集成分支的方法,它们适用于不同的场景。\n如果你需要保持提交历史记录的整洁和有序,或者需要快速解决由于分支变化而导致的代码冲突,则可以选择使用 Rebasing 方法。 如果你需要简单地将一个分支的更改合并到另一个分支中,并且不关心最终的提交历史记录,则可以选择使用 Merging 方法。 3.2.5 使用变基合并分支 # 重置到合并前的最后一个提交\nSHELL不换行 git reset --hard 7_hash_13 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_13 year-month-day | modify feature.txt by main (HEAD -\u0026gt; main) [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 变基 feature分支到 main分支\nSHELL不换行 git checkout feature git rebase main 手动合并冲突\nSHELL不换行 cat feature.txt输出合并后结果:\nSHELL不换行 changed by main 3.2.3 changed by feature 3.2.3 提交合并结果\nSHELL不换行 git add feature.txt git commit -m \u0026#34;rebase feature branch and resolve conflict\u0026#34; git rebase --continue 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_16 year-month-day | rebase feature branch and resolve conflict (HEAD -\u0026gt; feature) [yourname] * 7_hash_13 year-month-day | modify feature.txt by main [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 切换到 main分支\nSHELL不换行 git checkout main 合并 feature分支到 main分支\nSHELL不换行 git merge feature 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_17 year-month-day | rebase feature branch and resolve conflict (HEAD -\u0026gt; main, feature) [yourname] * 7_hash_13 year-month-day | modify feature.txt by main [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 3.3 多存储库 # 到目前为止,我们一直在使用单个 git 存储库。然而,git 擅长处理多个存储库。这些额外的存储库可以本地存储,也可以通过网络连接访问。\n本节我们将创建一个名为 learn_git_cloned的新存储库。展示如何从一个存储库移动更改到另一个存储库,并且当两个存储库之间发生冲突时如何处理。\n目前,我们将使用本地存储库(即存储在本地硬盘上的存储库)进行工作,但是,在本节中学到的大多数内容都适用于多个存储库,无论它们是在本地还是通过网络远程存储。\n3.3.1 克隆存储库 # 在 learn_git同级目录下克隆 learn_git存储库\nSHELL不换行 git clone learn_git learn_git_cloned 进入 learn_git_cloned目录,并查看提交历史\nSHELL不换行 cd learn_git_cloned git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_17 year-month-day | rebase feature branch and resolve conflict (HEAD -\u0026gt; main, feature) [yourname] * 7_hash_13 year-month-day | modify feature.txt by main [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 查看远程存储库\nSHELL不换行 git remote -v输出:\nSHELL不换行 origin learn_git (fetch) origin learn_git (push) 查看详细信息\nSHELL不换行 git remote show origin输出:\nSHELL不换行 * 远程 origin 获取地址:learn_git 推送地址:learn_git HEAD 分支:main 远程分支: feature 已跟踪 main 已跟踪 为 \u0026#39;git pull\u0026#39; 配置的本地分支: main 与远程 main 合并 为 \u0026#39;git push\u0026#39; 配置的本地引用: main 推送至 main (最新) 查看分支\nSHELL不换行 git branch -a输出:\nSHELL不换行 * main remotes/origin/HEAD -\u0026gt; origin/main remotes/origin/feature remotes/origin/main 3.3.2 从原始存储库拉取更改 # 在 learn_git存储库中创建文件 test.txt并提交\nSHELL不换行 cd learn_git echo \u0026#34;add test.txt by learn_git 3.3.2\u0026#34; \u0026gt; test.txt git add test.txt git commit -m \u0026#34;add test.txt by learn_git 3.3.2\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_18 year-month-day | add test.txt by learn_git 3.3.2 (HEAD -\u0026gt; main) [yourname] * 7_hash_17 year-month-day | rebase feature branch and resolve conflict (feature) [yourname] * 7_hash_13 year-month-day | modify feature.txt by main [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 进入 learn_git_cloned目录,拉取 learn_git存储库的更改\nSHELL不换行 cd learn_git_cloned git pull origin main 在 learn_git存储库中修改文件 test.txt并提交\nSHELL不换行 cd learn_git echo \u0026#34;modify test.txt by learn_git 3.3.2\u0026#34; \u0026gt; test.txt git add test.txt git commit -m \u0026#34;modify test.txt by learn_git 3.3.2\u0026#34; 进入 learn_git_cloned目录,获取 learn_git存储库的更改\nSHELL不换行 cd learn_git_cloned git fetch 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short --all输出:\nSHELL不换行 * 7_hash_19 year-month-day | modify test.txt by learn_git 3.3.2 (origin/main, origin/HEAD) [yourname] * 7_hash_18 year-month-day | add test.txt by learn_git 3.3.2 (HEAD -\u0026gt; main) [yourname] * 7_hash_17 year-month-day | rebase feature branch and resolve conflict (origin/feature) [yourname] * 7_hash_13 year-month-day | modify feature.txt by main [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 合并已经获取的更改\nSHELL不换行 git merge origin/main 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_19 year-month-day | modify test.txt by learn_git 3.3.2 (HEAD -\u0026gt; main, origin/main, origin/HEAD) [yourname] * 7_hash_18 year-month-day | add test.txt by learn_git 3.3.2 [yourname] * 7_hash_17 year-month-day | rebase feature branch and resolve conflict (origin/feature) [yourname] * 7_hash_13 year-month-day | modify feature.txt by main [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 3.3.3 向原始存储库推送更改 # 设置用户名和邮箱,并且设置为裸仓库\nSHELL不换行 git config user.name \u0026#34;yourname\u0026#34; git config user.email \u0026#34;youremail\u0026#34; git config --bool core.bare true 在 learn_git_cloned存储库中修改文件 test.txt并提交\nSHELL不换行 echo \u0026#34;modify test.txt by learn_git 3.3.3\u0026#34; \u0026gt; test.txt git add cloned.txt git commit -m \u0026#34;modify test.txt by learn_git 3.3.3\u0026#34; 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_20 year-month-day | modify test.txt by learn_git 3.3.3 (HEAD -\u0026gt; main) [yourname] * 7_hash_19 year-month-day | modify test.txt by learn_git 3.3.2 (origin/main, origin/HEAD) [yourname] * 7_hash_18 year-month-day | add test.txt by learn_git 3.3.2 [yourname] * 7_hash_17 year-month-day | rebase feature branch and resolve conflict (origin/feature) [yourname] * 7_hash_13 year-month-day | modify feature.txt by main [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 设置learn_git存储库接收更改\nSHELL不换行 cd learn_git git config receive.denyCurrentBranch updateInstead 推送更改到 learn_git存储库\nSHELL不换行 cd learn_git_cloned git push origin main 查看提交历史\nSHELL不换行 git log --pretty=format:\u0026#39;%h %ad | %s%d [%an]\u0026#39; --graph --date=short输出:\nSHELL不换行 * 7_hash_20 year-month-day | modify test.txt by learn_git 3.3.3 (HEAD -\u0026gt; main, origin/main, origin/HEAD) [yourname] * 7_hash_19 year-month-day | modify test.txt by learn_git 3.3.2 [yourname] * 7_hash_18 year-month-day | add test.txt by learn_git 3.3.2 [yourname] * 7_hash_17 year-month-day | rebase feature branch and resolve conflict (origin/feature) [yourname] * 7_hash_13 year-month-day | modify feature.txt by main [yourname] * 7_hash_12 year-month-day | add .gitignore [yourname] * 7_hash_11 year-month-day | rename test_1/test.txt to test_1/test_1.txt [yourname] * 7_hash_10 year-month-day | move test.txt to test_1/test.txt [yourname] * 7_hash_8 year-month-day | modify test.txt to 2.3.10_1 [yourname] * 7_hash_3 year-month-day | modify test.txt to 2.3.8 [yourname] * 7_hash_2 year-month-day | modify test.txt to 2.3.2 [yourname] * 7_hash_1 year-month-day | add test.txt [yourname] 88. 补充资料 # 点击展开查看资料 廖雪峰的 Git 教程 Pro Git ","date":"2024年12月9日","externalUrl":null,"permalink":"/posts/git%E6%8E%8C%E6%8F%A1%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B8%80/","section":"文章","summary":"","title":"Git掌握与实践【一】","type":"posts"},{"content":"","date":"2024年12月9日","externalUrl":null,"permalink":"/categories/%E4%BB%A3%E7%A0%81/","section":"分类","summary":"","title":"代码","type":"categories"},{"content":"","date":"2024年11月25日","externalUrl":null,"permalink":"/tags/code/","section":"Tags","summary":"","title":"Code","type":"tags"},{"content":"","date":"2024年11月25日","externalUrl":null,"permalink":"/tags/shell/","section":"Tags","summary":"","title":"Shell","type":"tags"},{"content":"","date":"2024年11月25日","externalUrl":null,"permalink":"/tags/zsh/","section":"Tags","summary":"","title":"Zsh","type":"tags"},{"content":" 0. 写在开头 # 由于系统、环境等差异可能过大,这里只是记录个人的 zsh 配置,仅供参考。\n0.1 .zshrc # SHELL不换行 # My zsh settings with zinit # ------------------- 核心配置 ------------------- # 性能优化选项 skip_global_compinit=1 DISABLE_MAGIC_FUNCTIONS=true ZSH_DISABLE_COMPFIX=true # 补全系统设置 COMPLETION_WAITING_DOTS=\u0026#34;true\u0026#34; ZSH_AUTOSUGGEST_MANUAL_REBIND=1 ZSH_AUTOSUGGEST_USE_ASYNC=1 ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=20 # ------------------- Powerlevel10k 即时提示 ------------------- if [[ -r \u0026#34;${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\u0026#34; ]]; then source \u0026#34;${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\u0026#34; fi # ------------------- zinit 插件管理器 ------------------- source \u0026#34;/usr/share/zinit/zinit.zsh\u0026#34; autoload -Uz _zinit (( ${+_comps} )) \u0026amp;\u0026amp; _comps[zinit]=_zinit # zinit annexes zinit ice wait\u0026#34;1\u0026#34; lucid as\u0026#34;null\u0026#34; for \\ zdharma-continuum/zinit-annex-as-monitor \\ zdharma-continuum/zinit-annex-bin-gem-node \\ zdharma-continuum/zinit-annex-patch-dl \\ zdharma-continuum/zinit-annex-rust # ------------------- 主题 ------------------- zinit ice depth=1 zinit light romkatv/powerlevel10k # ------------------- 核心插件 ------------------- # fzf zinit ice from\u0026#34;gh-r\u0026#34; as\u0026#34;command\u0026#34; zinit light junegunn/fzf # 补全增强 \u0026amp; 补全初始化 zinit ice wait\u0026#34;0a\u0026#34; lucid atload\u0026#34;zicompinit; zicdreplay\u0026#34; blockf zinit light zsh-users/zsh-completions # fzf-tab zinit ice wait\u0026#34;0b\u0026#34; lucid zinit light Aloxaf/fzf-tab # 自动建议 zinit ice wait\u0026#34;0c\u0026#34; lucid \\ atload\u0026#39; _zsh_autosuggest_start bindkey \u0026#34;\\`\u0026#34; autosuggest-accept \u0026#39; zinit light zsh-users/zsh-autosuggestions # 历史命令 zinit ice wait\u0026#34;0d\u0026#34; lucid from\u0026#34;gh-r\u0026#34; as\u0026#34;program\u0026#34; \\ bpick\u0026#34;*x86_64-unknown-linux-gnu.tar.gz\u0026#34; \\ extract\u0026#34;\u0026#34; \\ mv\u0026#34;atuin*/atuin -\u0026gt; atuin\u0026#34; \\ atload\u0026#39; eval \u0026#34;$(atuin init zsh)\u0026#34; bindkey \u0026#34;^R\u0026#34; _atuin_search_widget \u0026#39; zinit light atuinsh/atuin # 语法高亮 zinit ice wait\u0026#34;0e\u0026#34; lucid atinit\u0026#34;zpcompinit;zpcdreplay\u0026#34; zinit light zdharma-continuum/fast-syntax-highlighting # ------------------- CLI 工具 ------------------- # 核心工具(无延迟加载) zinit ice wait\u0026#34;0\u0026#34; lucid from\u0026#34;gh-r\u0026#34; as\u0026#34;program\u0026#34; \\ bpick\u0026#34;*x86_64-unknown-linux-musl.tar.gz\u0026#34; \\ extract\u0026#34;\u0026#34; \\ mv\u0026#34;eza* -\u0026gt; eza\u0026#34; zinit light eza-community/eza zinit ice wait\u0026#34;0\u0026#34; lucid from\u0026#34;gh-r\u0026#34; as\u0026#34;program\u0026#34; \\ bpick\u0026#34;*x86_64-unknown-linux-gnu.tar.gz\u0026#34; \\ extract\u0026#34;\u0026#34; \\ mv\u0026#34;bat*/bat -\u0026gt; bat\u0026#34; zinit light sharkdp/bat # 延迟加载工具 zinit ice wait\u0026#34;1\u0026#34; lucid from\u0026#34;gh-r\u0026#34; as\u0026#34;program\u0026#34; \\ bpick\u0026#34;*x86_64-unknown-linux-musl.tar.gz\u0026#34; \\ extract\u0026#34;\u0026#34; \\ mv\u0026#34;ripgrep*/rg -\u0026gt; rg\u0026#34; zinit light BurntSushi/ripgrep zinit ice wait\u0026#34;1\u0026#34; lucid from\u0026#34;gh-r\u0026#34; as\u0026#34;program\u0026#34; \\ bpick\u0026#34;*x86_64-unknown-linux-gnu.tar.gz\u0026#34; \\ extract\u0026#34;\u0026#34; \\ mv\u0026#34;fd*/fd -\u0026gt; fd\u0026#34; zinit light sharkdp/fd zinit ice wait\u0026#34;1\u0026#34; lucid from\u0026#34;gh-r\u0026#34; as\u0026#34;program\u0026#34; \\ bpick\u0026#34;*Linux_x86_64.tar.gz\u0026#34; \\ extract\u0026#34;\u0026#34; zinit light jesseduffield/lazydocker zinit ice wait\u0026#34;1\u0026#34; lucid from\u0026#34;gh-r\u0026#34; as\u0026#34;program\u0026#34; \\ bpick\u0026#34;*linux-amd64.tar.gz\u0026#34; \\ extract\u0026#34;\u0026#34; \\ mv\u0026#34;fastfetch*/usr/bin/fastfetch -\u0026gt; fastfetch\u0026#34; \\ atclone\u0026#34;chmod +x fastfetch\u0026#34; \\ atpull\u0026#34;%atclone\u0026#34; zinit light fastfetch-cli/fastfetch zinit ice wait\u0026#34;1\u0026#34; lucid from\u0026#34;gh-r\u0026#34; as\u0026#34;program\u0026#34; \\ bpick\u0026#34;*linux_x86_64.tar.gz\u0026#34; \\ extract=\u0026#34;\u0026#34; zinit light muesli/duf zinit ice wait\u0026#34;1\u0026#34; lucid as\u0026#34;program\u0026#34; pick\u0026#34;prettyping\u0026#34; zinit load denilsonsa/prettyping # bat-extras zinit ice wait\u0026#34;1\u0026#34; lucid as\u0026#34;program\u0026#34; \\ pick\u0026#34;src/batgrep.sh\u0026#34; pick\u0026#34;src/batdiff.sh\u0026#34; \\ atload\u0026#39;alias batgrep=\u0026#34;batgrep.sh\u0026#34;; alias batdiff=\u0026#34;batdiff.sh\u0026#34;\u0026#39; zinit light eth-p/bat-extras # ------------------- 补全配置 ------------------- # 基础补全设置 zstyle \u0026#39;:completion:*\u0026#39; completer _expand _complete _ignored zstyle \u0026#39;:completion:*\u0026#39; matcher-list \u0026#39;m:{a-z}={A-Z}\u0026#39; zstyle \u0026#39;:completion:*\u0026#39; list-colors \u0026#34;${(s.:.)LS_COLORS}\u0026#34; zstyle \u0026#39;:completion:*\u0026#39; menu no # fzf-tab 基础设置 zstyle \u0026#39;:fzf-tab:*\u0026#39; use-fzf-default-opts yes # fzf-tab 预览设置 zstyle \u0026#39;:fzf-tab:complete:cd:*\u0026#39; fzf-preview \u0026#39;eza --icons -1 --color=always $realpath\u0026#39; zstyle \u0026#39;:fzf-tab:complete:cd:*\u0026#39; popup-pad 30 0 zstyle \u0026#39;:fzf-tab:complete:__zoxide_z:*\u0026#39; fzf-preview \u0026#39;eza --icons -1 --color=always $realpath\u0026#39; zstyle \u0026#39;:fzf-tab:complete:z:*\u0026#39; fzf-preview \u0026#39;eza --icons -1 --color=always $realpath\u0026#39; # 进程补全预览 zstyle \u0026#39;:fzf-tab:complete:kill:argument-rest\u0026#39; fzf-preview \u0026#39;ps --pid=$word -o cmd --no-headers -w -w\u0026#39; zstyle \u0026#39;:fzf-tab:complete:kill:argument-rest\u0026#39; fzf-flags \u0026#39;--preview-window=down:3:wrap\u0026#39; zstyle \u0026#39;:fzf-tab:complete:kill:*\u0026#39; popup-pad 0 3 # fzf-tab 快捷键 zstyle \u0026#39;:fzf-tab:*\u0026#39; fzf-bindings \u0026#39;`:accept\u0026#39; zstyle \u0026#39;:fzf-tab:*\u0026#39; switch-group \u0026#39;\u0026lt;\u0026#39; \u0026#39;\u0026gt;\u0026#39; # ------------------- 自动建议配置 ------------------- ZSH_AUTOSUGGEST_STRATEGY=(history completion) ZSH_AUTOSUGGEST_COMPLETION_IGNORE=\u0026#39;( |man |pikaur -S )*\u0026#39; ZSH_AUTOSUGGEST_HISTORY_IGNORE=\u0026#39;?(#c50,)\u0026#39; # ------------------- fzf 配置 ------------------- export FZF_DEFAULT_OPTS=\u0026#34; --ansi --layout=reverse --info=inline --height=50% --multi --cycle --preview-window=right:50% --preview-window=cycle --prompt=\u0026#39;λ -\u0026gt; \u0026#39; --pointer=\u0026#39;▷\u0026#39; --marker=\u0026#39;✓\u0026#39; --color=bg+:236,gutter:-1,fg:-1,bg:-1,hl:-1,hl+:-1,prompt:-1,pointer:105,marker:-1,spinner:-1 \u0026#34; export FZF_DEFAULT_COMMAND=\u0026#39;fd --type f --hidden --follow --exclude .git\u0026#39; # ------------------- Conda 配置 ------------------- export CONDA_AUTO_ACTIVATE_BASE=false conda() { unfunction conda eval \u0026#34;$(/opt/miniforge/bin/conda shell.zsh hook)\u0026#34; conda $@ } [[ \u0026#34;${CONDA_AUTO_ACTIVATE_BASE:-true}\u0026#34; == \u0026#34;true\u0026#34; ]] \u0026amp;\u0026amp; { eval \u0026#34;$(/opt/miniforge/bin/conda shell.zsh hook)\u0026#34; conda activate base } # ------------------- 其他工具配置 ------------------- # zoxide eval \u0026#34;$(zoxide init zsh --cmd cd)\u0026#34; # 加载自定义别名 source $HOME/.zsh_aliases # Powerlevel10k 配置 [[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh 0.2 .zsh_aliases # SHELL不换行 alias l=\u0026#34;eza --icons --long --header\u0026#34; alias ls=\u0026#34;eza --icons --grid\u0026#34; alias ll=\u0026#34;eza --icons --long --header\u0026#34; alias la=\u0026#34;eza --icons --long --header --all\u0026#34; alias lg=\u0026#34;eza --icons --long --header --all --git\u0026#34; alias tree=\u0026#34;eza --icons --tree -L1\u0026#34; alias cat=\u0026#34;bat\u0026#34; alias man=\u0026#34;tldr\u0026#34; alias mkdir=\u0026#39;mkdir -p\u0026#39; alias df=\u0026#34;duf -style unicode -hide-mp \u0026#39;/run/credentials/*\u0026#39;\u0026#34; alias top=\u0026#34;bpytop\u0026#34; alias gedit=\u0026#34;gnome-text-editor\u0026#34; alias nvidia-smi=\u0026#34;watch -n 3 -c nvidia-smi\u0026#34; alias x=\u0026#34;unar\u0026#34; alias ff=\u0026#34;fastfetch\u0026#34; # 快速显示fastfetch function tmp() { cd \u0026#34;$(mktemp -d /tmp/temp_XXX)\u0026#34; #创建临时目录 } 1. 基本信息 # System: Arch Linux x86_64 Terminal: kitty Shell: zsh Theme: powerlevel10k Plugins Manerger: zinit 2. 原有配置痛点 # 如果历史命令中有中文,按 up键,会出现历史命令错位和终止终端的问题。 有些时候,命令输入延迟很高,已经输入完一段命令,但是终端是慢慢一个字一个字跳的。 性能还是不够高。 3. 方案对比和选择 # 3.1 插件管理器或插件框架 # 序号 项目 说明 优势 劣势 1 Oh My Zsh 最流行的 Zsh 框架 - 300+预置插件和 150+主题\n- 配置简单直观\n- 社区庞大 - 启动速度慢\n- 插件数量增加会显著降低性能 2 Zinit 现代化插件管理器 - Turbo 模式启动速度提升 50-80%\n- 支持选择性禁用/启用功能\n- 完整兼容 OMZ 和 Prezto 插件 - 语法相对复杂\n- 不适合新手 3 Zim 轻量级框架 - 启动速度快\n- 模块化设计\n- 合理的默认配置\n- 完整支持 OMZ 插件 - 内置功能相对较少 4 Sheldon 可配置的插件管理器 - 配置灵活\n- 支持延迟加载\n- 支持多种 shell - 社区相对较小 5 zsh4humans 优化的 Zsh 环境 - 启动速度快\n- 默认配置合理\n- 性能优化出色 - 定制性相对较低 6 Antigen 传统插件管理器 - 类似包管理器的使用方式\n- 自动更新插件 - 维护不够活跃\n- 性能一般 7 zgen 轻量级管理器 - 生成静态加载文件\n- 启动相对较快 - 功能相对简单\n- 更新较少 8 zplug 全功能插件管理器 - 功能丰富\n- 并行安装插件 - 已基本停止维护\n- 性能一般 在这个里面,根据轻量化、性能好、社区维护活跃等因素,我最终选择了 Zinit作为插件管理器。\n3.2 插件 # 语法高亮:fast-syntax-highlighting\n命令补全:zsh-completions和 fzf-tab\n智能建议:zsh-autosuggestions\n目录跳转:zoxide\n序号 项目 说明 优势 劣势 1 zoxide Rust 编写的现代化目录跳转工具 - 性能最快,启动速度快\n- 使用简单直观\n- 支持所有主流 shell - 需要额外安装\n- 不支持相对路径跳转 2 autojump Python 编写的经典跳转工具 - 功能丰富完整\n- 高级特性多 - 学习曲线较陡\n- 性能相对较慢 3 z Zsh 内置的目录跳转插件 - 无需额外安装\n- 与 Zsh 深度集成 - 功能相对基础\n- 仅支持 Zsh 4 z.lua Lua 实现的跳转工具 - 跨平台支持好\n- 配置灵活 - 性能比 zoxide 慢\n- 需要 Lua 环境 5 zsh-z Zsh 专用的 z 实现 - 轻量级\n- 安装简单 - 仅支持 Zsh\n- 功能较少 6 ZLOcation Shell 原生实现的跳转工具 - 无外部依赖\n- 性能稳定 - 功能相对简单\n- 社区较小 历史命令:atuin\n序号 项目 说明 优势 劣势 1 atuin 基于 SQLite 的现代化 shell 历史记录工具 - 支持多设备同步\n- 支持端到端加密\n- 可按目录/主机等过滤搜索\n- 记录命令执行时间和退出码\n- 支持自托管同步服务器\n- 配置简单,安装便捷 - 首次打开仪表板较慢\n- 在 mosh 下可能出现屏幕显示问题\n- 同步机制不够直观\n- 配置无法跨设备同步 2 zsh-histdb 基于 SQLite 的 Zsh 历史记录插件 - 记录命令工作目录\n- 记录命令执行主机\n- 支持多数据库合并\n- 与 zsh-autosuggestions 集成良好\n- 支持按目录搜索历史 - 不使用传统文本文件存储\n- 项目维护不够活跃\n- 仅支持 Zsh\n- 配置相对复杂 主题美化:powerlevel10k\n3.2 第三方命令行工具管理 # 由于改配置会同步 Docker深度学习环境、远程服务器使用,系统包管理器不同(有ubuntu、archlinux、debian等),故第三方命令行工具使用 zinit管理,而非使用包管理器管理。 则新建一系统时安装zsh和zinit,再同步配置即可,其余的交给zinit管理。\nTODO: 之后用chezimo管理配置文件并同步到各个系统。\n4. 测试工具 # 这里我们使用zsh-bench基准工具来测试 zsh 交互性能,再进行对比。\n测量指标 first_prompt_lag_ms (首次提示符延迟) 打开新终端时,显示第一个命令提示符(如 user@host:~$)所需的时间 良好: \u0026lt; 50ms,几乎瞬间显示提示符 较差: \u0026gt; 100ms,打开终端后明显等待才看到提示符 first_command_lag_ms (首次命令延迟) 输入第一个命令(如 ls)到执行完成的时间 良好: \u0026lt; 200ms,输入命令后立即执行 较差: \u0026gt; 500ms,输入第一个命令后明显卡顿 command_lag_ms (命令延迟) 执行后续命令的平均响应时间 良好: \u0026lt; 10ms,命令执行无感知延迟 较差: \u0026gt; 50ms,每次执行命令都能感觉到轻微卡顿 input_lag_ms (输入延迟) 按键到字符显示在屏幕上的时间 良好: \u0026lt; 30ms,打字流畅自然 较差: \u0026gt; 50ms,打字时感觉明显延迟或卡顿 exit_time_ms (退出时间) 输入 exit 或按 Ctrl+D 到终端完全关闭的时间 良好: \u0026lt; 100ms,终端立即关闭 较差: \u0026gt; 200ms,关闭终端时出现明显延迟 测试命令如下: SHELL不换行 ./zsh-bench -i 30 -l yes -g yes 5. 测试结果 # SHELL不换行 ==\u0026gt; benchmarking login shell of user xiadengma ... creates_tty=0 has_compsys=0 has_syntax_highlighting=0 has_autosuggestions=0 has_git_prompt=1 first_prompt_lag_ms=18.829 first_command_lag_ms=78.340 command_lag_ms=4.930 input_lag_ms=3.903 exit_time_ms=45.429可以看到当前 zsh 配置在性能上表现良好,响应速度快。\n参考资料 # 点击展开查看参考资料 https://github.com/crivotz/dot_files/blob/master/linux/zinit/zshrc https://github.com/synthpop123/dotfiles/blob/main/.zshrc https://github.com/Aloxaf/dotfiles/blob/master/zsh/.config/zsh/zshrc.zsh https://www.aloxaf.com/2024/02/manage_zsh_shell_with_atuin/ https://blog.lkwplus.com/posts/macos-dev-setup/#zsh-%E9%85%8D%E7%BD%AE https://notes.fe-mm.com/workflow/terminal/toolkit ","date":"2024年11月25日","externalUrl":null,"permalink":"/posts/%E4%B8%AA%E4%BA%BAzsh%E9%85%8D%E7%BD%AE/","section":"文章","summary":"","title":"个人zsh配置","type":"posts"},{"content":"","date":"2024年11月25日","externalUrl":null,"permalink":"/series/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/","section":"Series","summary":"","title":"开发环境配置","type":"series"},{"content":"","date":"2024年11月22日","externalUrl":null,"permalink":"/tags/blowfish/","section":"Tags","summary":"","title":"Blowfish","type":"tags"},{"content":"详细见https://blowfish.page/zh-cn/docs/shortcodes/#button\n1. 展开折叠 # 点击展开 这是折叠的内容 默认展开的内容 这个内容块默认是展开的 2. 各类型提示框 # 这是一个信息提示框 这是一个警告提示框 这是一个危险提示框 3. 画廊 # 用于生成可交互且具有视觉吸引力的方式展示多个图像的画廊。这允许用户滑动浏览多个图像,同时仅占用单个图像的垂直空间。所有图像均使用父组件的完整宽度并使用预定义的宽高比 16:9 、 21:9 或 32:9 之一显示。\n参数 功能 images 必填 用于匹配图像名称的正则表达式或 URL。 aspectRatio 可选 画廊的纵横比。 16-9 、 21-9 或 32-9 。默认设置为 16-9 。 interval 可选 自动滚动的时间间隔,以毫秒为单位指定。默认为 2000 (2 秒)。 4. 图表 # 支持chart.js,具体见Chart.js 官方文档。\n5. 外部代码 # 此短代码用于轻松从外部源导入代码,无需复制和粘贴\nParameter Description url 必需的 外部托管代码文件的 URL. type 用于语法突出显示的代码类型. startLine 可选 从代码文件中导入的起始行. endLine 可选 从代码文件中导入的结束行. 6. Github 卡片 # 允许您快速链接到 github Repo,同时显示和更新有关它的实时统计信息,例如它的 star 和 fork 数。\n参数 功能 repo [String] 格式为 username/repo 的 github repo nunocoracao/blowfish Personal Website \u0026amp; Blog Theme for Hugo HTML 2481 643 7. KaTeX 数学公式 # katex 简码可用于使用 KaTeX 包向文章内容添加数学表达式。有关可用语法,请参阅支持的 TeX 函数的在线参考。\n要在文章中加入数学表达式,只需将简码放在任意位置即可。每篇文章只需加入一次,KaTeX 将自动呈现该页面上的任何标记。支持内联和块表示法。\n可以通过将表达式包装在 \\\\( 和 \\\\) 分隔符中来生成内联表示法。或者,可以使用 $$ 分隔符生成块符号。\n\\(f(a,b,c) = (a^2+b^2+c^2)^3\\)\n8. Mermaid 图表和流程图 # mermaid 允许您使用文本绘制可视化的图表。底层使用 Mermaid,并支持各种图表、图表和其他输出格式。\n只需在 mermaid 简码中编写您的 Mermaid 语法,然后让插件完成其余的工作。\n已经支持 markdown 语法,直接在 markdown 中编写即可。\n有关语法和支持的图表类型的详细信息,请参阅官方 Mermaid 文档。\ngraph LR; A[Lemons]--\u003eB[Lemonade]; B--\u003eC[Profit] 9. 本地视频 # 用于在文章中嵌入本地视频文件,支持响应式设计和多种配置选项。 最简单的使用方式:\nPLAINTEXT不换行 {{\u0026lt; video src=\u0026#34;video.mp4\u0026#34; \u0026gt;}} 参数说明 # 参数 说明 默认值 src 必填 视频文件路径(本地或 URL) - poster 可选 视频封面图路径 - aspectRatio 可选 视频纵横比。16-9、21-9 或 32-9 16-9 controls 可选 是否显示控制条 true autoplay 可选 是否自动播放 false loop 可选 是否循环播放 false muted 可选 是否静音 false playsinline 可选 是否在移动端内联播放 false preload 可选 预加载方式(metadata、auto、none) metadata caption 可选 视频说明文字(支持 Markdown) - src-webm 可选 WebM 格式视频源(提供多格式以提高兼容性) - src-ogg 可选 OGG 格式视频源 - class 可选 自定义 CSS 类 - 10. 单张图片 figure(推荐) # 参考 content/posts/MIT 6.S184 课程笔记/index.md 的写法:本站推荐用 figure 简码统一插图格式(可选图注/宽度/懒加载等)。该简码模板位于 layouts/shortcodes/figure.html。\n参数说明 # 参数 说明 src 必填 图片路径(相对当前文章 Page Bundle,或站点资源路径/外链 URL) alt 可选 图片 alt(缺省会回退到 caption 的纯文本) width 可选 图片宽度(CSS 值,例如 70%、40%、520px) caption 可选 图注(支持 Markdown) loading 可选 传给 \u0026lt;img loading\u0026gt;,例如 lazy class 可选 追加到 \u0026lt;figure\u0026gt; 的 CSS class 用法示例 # PLAINTEXT不换行 {{\u0026lt; figure src=\u0026#34;imgs/example.png\u0026#34; alt=\u0026#34;示例图\u0026#34; width=\u0026#34;70%\u0026#34; caption=\u0026#34;图:示例图注(支持 Markdown)\u0026#34; loading=\u0026#34;lazy\u0026#34; \u0026gt;}} 11. 自定义:并排图片 figuregroup # 用于把多张图片并排排成一行(类似论文里一排多图的效果),并共享同一个图注。该简码是本站新增,模板位于 layouts/shortcodes/figuregroup.html。\n参数说明 # 参数 说明 src 必填 多图路径列表,用 ; 分隔,例如 a.png;b.png;c.png alt 可选 多图 alt 列表,用 ; 分隔;缺省会回退到 caption 或文件名 itemWidth 可选 单张图的宽度(CSS 值),例如 45%、260px caption 可选 图注(支持 Markdown) title 可选 标题 class 可选 追加到 \u0026lt;figure\u0026gt; 的 CSS class loading 可选 传给 \u0026lt;img loading=\u0026quot;...\u0026quot;\u0026gt;,例如 lazy 用法示例 # PLAINTEXT不换行 {{\u0026lt; figuregroup src=\u0026#34;imgs/a.png;imgs/b.png\u0026#34; alt=\u0026#34;图 a;图 b\u0026#34; itemWidth=\u0026#34;45%\u0026#34; caption=\u0026#34;两张图并排显示(共享同一条图注)\u0026#34; \u0026gt;}} 12. 自定义:算法框 algorithm # 用于在文章中插入“论文风格”的算法框(参考 arXiv HTML 的 Algorithm 样式:外框 + caption 上下双线)。该简码是本站新增,模板位于 layouts/shortcodes/algorithm.html,样式在 assets/css/custom.css 的 .bf-algorithm*。\n参数说明 # 参数 说明 title 可选 算法标题(建议包含“算法 X:\u0026hellip;”) id 可选 给 \u0026lt;figure\u0026gt; 设置 id(便于锚点引用) class 可选 追加 CSS class 用法示例 # PLAINTEXT不换行 {{\u0026lt; algorithm title=\u0026#34;算法 1:用欧拉方法从流模型采样\u0026#34; id=\u0026#34;alg1\u0026#34; \u0026gt;}} 输入:神经网络向量场 $u_t^{\\\\theta}$,步数 $n$ 1. 设 $t=0$ 2. 设步长 $h=\\\\frac{1}{n}$ 3. 迭代更新 $x_{t+h}=x_t + h\\\\,u_t^{\\\\theta}(x_t)$ 输出:$x_1$ {{\u0026lt; /algorithm \u0026gt;}}","date":"2024年11月22日","externalUrl":null,"permalink":"/posts/blowfish%E4%B8%BB%E9%A2%98%E5%B8%B8%E7%94%A8%E7%AE%80%E7%A0%81/","section":"文章","summary":"","title":"Blowfish主题常用简码","type":"posts"},{"content":"","date":"2024年11月22日","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"2024年11月22日","externalUrl":null,"permalink":"/tags/shortcode/","section":"Tags","summary":"","title":"Shortcode","type":"tags"},{"content":" 0. 写在开头 # 在配置好代理和软件源的情况下,配置开发环境。\n1. 基本信息与软件安装 # System: Arch Linux x86_64 IDE: VSCode LSP: clangd Compiler: clang Debugger: lldb Build System: xmake 在终端依次输入下列命令安装软件:\nSHELL不换行 sudo pacman -Syyu sudo pacman -S clang sudo pacman -S clangd sudo pacman -S lldb sudo pacman -S xmake sudo pacman -S visual-studio-code-bin 依次输入下列命令检查是否正确安装:\nSHELL不换行 clang -v clangd -v clang-tidy --version clang-format --version lldb --version xmake --version 2. vscode 基础配置与插件 # 基础配置作为所有配置的基础和平时的编辑器使用\n2.1 vscode 基础插件 # 进入vscode,点击扩展,依次输入下列内容安装显示的第一个插件\nMS-CEINTL.vscode-language-pack-zh-hans:中文语言包 intellsmi.comment-translate:注释翻译 deeplx-comment-translate.deeplx-comment-translate:注释翻译扩展,使其可以使用Deeplx usernamehw.errorlens:代码错误提示 eamodio.gitlens:git 工具 tamasfe.even-better-toml:TOML 语法高亮 cweijan.vscode-office:vscode 内 office 文件预览和 markdown 预览 github.copilot:AI 代码提示 github.copilot-chat:AI 代码对话 fisheva.eva-theme:vscode 颜色主题 miguelsolorio.fluent-icons:vscode 产品图标主题 wayou.vscode-icons-mac:vscode 文件图标主题 ms-vscode-remote.remote-ssh:SSH 远程连接 在安装完成后,在扩展列表依次右键点击插件,打开将扩展应用于所有配置文件选项\n2.2 vscode 基础配置 # 进入vscode,先点击左上角文件-\u0026gt;首选项-\u0026gt;Profile-\u0026gt;默认,再按快捷键Ctrl+Shift+P,在输入框内输入user json,选中Preferences: Open Settings (JSON),此时会有一个settings.json文件被打开,修改其内容为下面内容\nJSON不换行 { //------------------下列配置共享给所有其他配置------------------ \u0026#34;workbench.settings.applyToAllProfiles\u0026#34;: [ //---工作台--- \u0026#34;workbench.colorTheme\u0026#34;, \u0026#34;workbench.iconTheme\u0026#34;, \u0026#34;workbench.productIconTheme\u0026#34;, \u0026#34;workbench.startupEditor\u0026#34;, \u0026#34;workbench.list.smoothScrolling\u0026#34;, \u0026#34;workbench.tree.indent\u0026#34;, \u0026#34;workbench.editor.highlightModifiedTabs\u0026#34;, \u0026#34;workbench.editor.pinnedTabsOnSeparateRow\u0026#34;, \u0026#34;workbench.editor.wrapTabs\u0026#34;, \u0026#34;workbench.editor.empty.hint\u0026#34;, \u0026#34;workbench.editor.enablePreview\u0026#34;, \u0026#34;workbench.activityBar.location\u0026#34;, \u0026#34;workbench.editorAssociations\u0026#34;, \u0026#34;workbench.reduceMotion\u0026#34;, //---文本编辑器--- \u0026#34;editor.accessibilitySupport\u0026#34;, \u0026#34;editor.codeLensFontFamily\u0026#34;, \u0026#34;editor.codeLensFontSize\u0026#34;, \u0026#34;editor.cursorBlinking\u0026#34;, \u0026#34;editor.cursorSurroundingLines\u0026#34;, \u0026#34;editor.fontFamily\u0026#34;, \u0026#34;editor.fontLigatures\u0026#34;, \u0026#34;editor.fontSize\u0026#34;, \u0026#34;editor.fontWeight\u0026#34;, \u0026#34;editor.lineHeight\u0026#34;, \u0026#34;editor.formatOnPaste\u0026#34;, \u0026#34;editor.formatOnSave\u0026#34;, \u0026#34;editor.gotoLocation.multipleDeclarations\u0026#34;, \u0026#34;editor.gotoLocation.multipleDefinitions\u0026#34;, \u0026#34;editor.guides.bracketPairs\u0026#34;, \u0026#34;editor.guides.indentation\u0026#34;, \u0026#34;editor.inlayHints.enabled\u0026#34;, \u0026#34;editor.linkedEditing\u0026#34;, \u0026#34;editor.multiCursorModifier\u0026#34;, \u0026#34;editor.quickSuggestions.comments\u0026#34;, \u0026#34;editor.quickSuggestions.strings\u0026#34;, \u0026#34;editor.quickSuggestionsDelay\u0026#34;, \u0026#34;editor.renderWhitespace\u0026#34;, \u0026#34;editor.smoothScrolling\u0026#34;, \u0026#34;editor.snippetSuggestions\u0026#34;, \u0026#34;editor.suggest.insertMode\u0026#34;, \u0026#34;editor.suggest.preview\u0026#34;, \u0026#34;editor.suggestFontSize\u0026#34;, \u0026#34;editor.tokenColorCustomizations\u0026#34;, \u0026#34;editor.unicodeHighlight.allowedLocales\u0026#34;, \u0026#34;editor.acceptSuggestionOnEnter\u0026#34;, \u0026#34;editor.bracketPairColorization.enabled\u0026#34;, \u0026#34;editor.wordWrap\u0026#34;, \u0026#34;editor.wordWrapColumn\u0026#34;, \u0026#34;editor.mouseWheelZoom\u0026#34;, \u0026#34;editor.suggest.snippetsPreventQuickSuggestions\u0026#34;, \u0026#34;editor.suggestSelection\u0026#34;, \u0026#34;editor.stickyScroll.enabled\u0026#34;, \u0026#34;editor.wordSeparators\u0026#34;, \u0026#34;editor.dragAndDrop\u0026#34;, \u0026#34;editor.showFoldingControls\u0026#34;, \u0026#34;editor.hover.above\u0026#34;, \u0026#34;editor.hover.hidingDelay\u0026#34;, \u0026#34;editor.scrollbar.verticalScrollbarSize\u0026#34;, \u0026#34;editor.unicodeHighlight.nonBasicASCII\u0026#34;, \u0026#34;editor.minimap.maxColumn\u0026#34;, //---文件资源管理器--- \u0026#34;explorer.confirmDelete\u0026#34;, \u0026#34;explorer.enableDragAndDrop\u0026#34;, \u0026#34;explorer.confirmDragAndDrop\u0026#34;, \u0026#34;explorer.fileNesting.enabled\u0026#34;, \u0026#34;explorer.autoReveal\u0026#34;, //---文件--- \u0026#34;files.autoGuessEncoding\u0026#34;, \u0026#34;files.autoSave\u0026#34;, \u0026#34;files.autoSaveDelay\u0026#34;, \u0026#34;files.eol\u0026#34;, \u0026#34;files.exclude\u0026#34;, \u0026#34;files.trimFinalNewlines\u0026#34;, \u0026#34;files.trimTrailingWhitespace\u0026#34;, \u0026#34;files.insertFinalNewline\u0026#34;, \u0026#34;files.refactoring.autoSave\u0026#34;, \u0026#34;files.watcherExclude\u0026#34;, \u0026#34;files.hotExit\u0026#34;, //---终端--- \u0026#34;terminal.integrated.fontSize\u0026#34;, \u0026#34;terminal.integrated.fontWeight\u0026#34;, \u0026#34;terminal.integrated.stickyScroll.enabled\u0026#34;, \u0026#34;terminal.integrated.gpuAcceleration\u0026#34;, \u0026#34;terminal.integrated.defaultLocation\u0026#34;, \u0026#34;terminal.integrated.enableMultiLinePasteWarning\u0026#34;, \u0026#34;terminal.integrated.smoothScrolling\u0026#34;, \u0026#34;terminal.integrated.defaultProfile.linux\u0026#34;, \u0026#34;terminal.integrated.scrollback\u0026#34;, //---其他自带设置--- \u0026#34;breadcrumbs.icons\u0026#34;, \u0026#34;notebook.breadcrumbs.showCodeCells\u0026#34;, \u0026#34;notebook.output.fontFamily\u0026#34;, \u0026#34;notebook.output.fontSize\u0026#34;, \u0026#34;search.exclude\u0026#34;, \u0026#34;search.searchEditor.singleClickBehaviour\u0026#34;, \u0026#34;search.followSymlinks\u0026#34;, \u0026#34;search.collapseResults\u0026#34;, \u0026#34;scm.inputFontSize\u0026#34;, \u0026#34;window.dialogStyle\u0026#34;, \u0026#34;window.density.editorTabHeight\u0026#34;, \u0026#34;window.restoreWindows\u0026#34;, \u0026#34;window.newWindowProfile\u0026#34;, \u0026#34;window.customTitleBarVisibility\u0026#34;, \u0026#34;extensions.ignoreRecommendations\u0026#34;, \u0026#34;update.mode\u0026#34;, \u0026#34;[jsonc].editor.defaultFormatter\u0026#34;, \u0026#34;security.workspace.trust.untrustedFiles\u0026#34;, \u0026#34;git.openRepositoryInParentFolders\u0026#34;, \u0026#34;extensions.autoUpdate\u0026#34;, //----插件:Error Lens--- \u0026#34;errorLens.enabledDiagnosticLevels\u0026#34;, \u0026#34;errorLens.excludeBySource\u0026#34;, \u0026#34;errorLens.fontSize\u0026#34;, \u0026#34;errorLens.fontWeight\u0026#34;, //----插件:GitHub Copilot\u0026amp;Chat--- \u0026#34;github.copilot.enable\u0026#34;, \u0026#34;github.copilot.chat.localeOverride\u0026#34;, \u0026#34;github.copilot.advanced\u0026#34;, \u0026#34;github-enterprise.uri\u0026#34;, //----插件:Comment Translate\u0026amp;Deeplx comment translate--- \u0026#34;commentTranslate.targetLanguage\u0026#34;, \u0026#34;commentTranslate.hover.string\u0026#34;, \u0026#34;commentTranslate.multiLineMerge\u0026#34;, \u0026#34;commentTranslate.source\u0026#34;, \u0026#34;commentTranslate.maxTranslationLength\u0026#34;, \u0026#34;deeplxTranslate.authKey\u0026#34;, //----插件:Vscode Office--- \u0026#34;vscode-office.editorTheme\u0026#34;, \u0026#34;vscode-office.openOutline\u0026#34;, //----插件:gitlens--- \u0026#34;gitlens.currentLine.enabled\u0026#34;, \u0026#34;gitlens.blame.format\u0026#34;, \u0026#34;gitlens.hovers.currentLine.over\u0026#34;, \u0026#34;gitlens.hovers.enabled\u0026#34;, \u0026#34;gitlens.hovers.avatars\u0026#34;, \u0026#34;gitlens.blame.compact\u0026#34;, \u0026#34;gitlens.blame.separateLines\u0026#34;, \u0026#34;gitlens.codeLens.authors.enabled\u0026#34;, \u0026#34;gitlens.views.repositories.files.layout\u0026#34;, \u0026#34;gitlens.views.repositories.compact\u0026#34;, \u0026#34;gitlens.defaultDateLocale\u0026#34;, \u0026#34;gitlens.defaultDateFormat\u0026#34;, \u0026#34;gitlens.defaultDateStyle\u0026#34;, \u0026#34;gitlens.advanced.messages\u0026#34;, \u0026#34;gitlens.gitCommands.search.showResultsInSideBar\u0026#34;, \u0026#34;gitlens.advanced.fileHistoryShowAllBranches\u0026#34; ], //------------------------------------------------------------------------------------------------------------ // //---通用设置--- // //------------------工作台------------------ \u0026#34;workbench.colorTheme\u0026#34;: \u0026#34;Eva Dark\u0026#34;, //颜色主题 \u0026#34;workbench.iconTheme\u0026#34;: \u0026#34;vscode-icons-mac\u0026#34;, //工作台图标主题 \u0026#34;workbench.productIconTheme\u0026#34;: \u0026#34;fluent-icons\u0026#34;, //工作台产品图标主题 \u0026#34;workbench.startupEditor\u0026#34;: \u0026#34;newUntitledFile\u0026#34;, //在没有从上一会话中恢复出信息的情况下,控制启动时显示的编辑器 \u0026#34;workbench.list.smoothScrolling\u0026#34;: true, //列表和树视图启用平滑滚动 \u0026#34;workbench.tree.indent\u0026#34;: 14, //树缩进14pix \u0026#34;workbench.editor.highlightModifiedTabs\u0026#34;: false, //在修改后未保存的文件上方只显示点 \u0026#34;workbench.editor.pinnedTabsOnSeparateRow\u0026#34;: true, //固定的选项卡出现在顶端 \u0026#34;workbench.editor.wrapTabs\u0026#34;: true, //选项卡换行 \u0026#34;workbench.editor.empty.hint\u0026#34;: \u0026#34;hidden\u0026#34;, //隐藏在没有打开编辑器时显示的提示信息 \u0026#34;workbench.editor.enablePreview\u0026#34;: true, //启用预览编辑器 \u0026#34;workbench.activityBar.location\u0026#34;: \u0026#34;top\u0026#34;, //活动栏位置:顶部 \u0026#34;workbench.editorAssociations\u0026#34;: { //编辑器关联 \u0026#34;*.copilotmd\u0026#34;: \u0026#34;vscode.markdown.preview.editor\u0026#34;, //使用markdown预览编辑器打开.copilotmd文件 \u0026#34;*.md\u0026#34;: \u0026#34;default\u0026#34;, //使用默认编辑器打开.md文件 \u0026#34;{git,gitlens}:/**/*.{md,csv,svg}\u0026#34;: \u0026#34;default\u0026#34; //使用默认编辑器打开git和gitlens扩展中的.md、.csv、.svg文件 }, \u0026#34;workbench.reduceMotion\u0026#34;: \u0026#34;on\u0026#34;, //减少动画 // // //------------------文本编辑器------------------ \u0026#34;editor.accessibilitySupport\u0026#34;: \u0026#34;off\u0026#34;, //UI关闭屏幕阅读器优化 \u0026#34;editor.codeLensFontFamily\u0026#34;: \u0026#34;\u0026#39;Cascadia Code PL\u0026#39;\u0026#34;, //CodeLens(提示)字体 \u0026#34;editor.codeLensFontSize\u0026#34;: 13, //CodeLens字号 \u0026#34;editor.cursorBlinking\u0026#34;: \u0026#34;smooth\u0026#34;, //光标闪烁:平滑 \u0026#34;editor.cursorSurroundingLines\u0026#34;: 14, //光标周围始终可见x行 \u0026#34;editor.fontFamily\u0026#34;: \u0026#34;\u0026#39;MonoLisa Nerd Font\u0026#39;, \u0026#39;Cascadia Code PL\u0026#39;, \u0026#39;SF Pro Text\u0026#39;, \u0026#39;Jetbrains Mono\u0026#39;, \u0026#39;Fira Code\u0026#39;, monospace\u0026#34;, //字体 \u0026#34;editor.fontLigatures\u0026#34;: true, //启用字体连字 \u0026#34;editor.fontSize\u0026#34;: 14, //编辑器字号 \u0026#34;editor.fontWeight\u0026#34;: \u0026#34;300\u0026#34;, //编辑器字体粗细 \u0026#34;editor.lineHeight\u0026#34;: 1.5, //编辑器行高 \u0026#34;editor.formatOnPaste\u0026#34;: true, //粘贴内容时自动格式化代码 \u0026#34;editor.formatOnSave\u0026#34;: true, //保存文件时自动格式化代码 \u0026#34;editor.gotoLocation.multipleDeclarations\u0026#34;: \u0026#34;goto\u0026#34;, //代码导航时直接跳转到第一个声明 \u0026#34;editor.gotoLocation.multipleDefinitions\u0026#34;: \u0026#34;goto\u0026#34;, //代码导航时直接跳转到第一个定义 \u0026#34;editor.guides.bracketPairs\u0026#34;: true, //显示括号对齐的指引线 \u0026#34;editor.guides.indentation\u0026#34;: false, //不显示缩进的指引线 \u0026#34;editor.inlayHints.enabled\u0026#34;: \u0026#34;off\u0026#34;, //关闭内联提示 \u0026#34;editor.linkedEditing\u0026#34;: true, //启用联动编辑 \u0026#34;editor.multiCursorModifier\u0026#34;: \u0026#34;ctrlCmd\u0026#34;, //多光标操作辅助键设置为ctrl \u0026#34;editor.quickSuggestions\u0026#34;: { \u0026#34;comments\u0026#34;: \u0026#34;on\u0026#34;, //开启注释快速建议 \u0026#34;strings\u0026#34;: \u0026#34;on\u0026#34; //开启字符串快速建议 }, \u0026#34;editor.quickSuggestionsDelay\u0026#34;: 0, //立即显示快速建议 \u0026#34;editor.renderWhitespace\u0026#34;: \u0026#34;none\u0026#34;, //不渲染空白字符 \u0026#34;editor.smoothScrolling\u0026#34;: true, //启用平滑滚动 \u0026#34;editor.snippetSuggestions\u0026#34;: \u0026#34;top\u0026#34;, //在建议列表的顶部显示代码片段建议 \u0026#34;editor.suggest.insertMode\u0026#34;: \u0026#34;replace\u0026#34;, //建议插入模式:替换 \u0026#34;editor.suggest.preview\u0026#34;: false, //不预览建议 \u0026#34;editor.suggestFontSize\u0026#34;: 13, //控制建议列表字号 \u0026#34;editor.tokenColorCustomizations\u0026#34;: { //自定义语法高亮 \u0026#34;textMateRules\u0026#34;: [ { \u0026#34;scope\u0026#34;: \u0026#34;comment\u0026#34;, \u0026#34;settings\u0026#34;: { \u0026#34;fontStyle\u0026#34;: \u0026#34;italic\u0026#34; //所有语言的注释部分将以斜体显示 } } ] }, \u0026#34;editor.unicodeHighlight.allowedLocales\u0026#34;: { \u0026#34;zh-hans\u0026#34;: true, //简体中文环境,允许使用 Unicode 字符,不会被高亮显示 \u0026#34;zh-hant\u0026#34;: true //繁体中文环境,允许使用 Unicode 字符,不会被高亮显示 }, \u0026#34;editor.acceptSuggestionOnEnter\u0026#34;: \u0026#34;smart\u0026#34;, //智能接受建议 \u0026#34;editor.bracketPairColorization.enabled\u0026#34;: true, //启用括号对颜色化 \u0026#34;editor.wordWrap\u0026#34;: \u0026#34;bounded\u0026#34;, //在视区宽度和 \u0026#34;Editor: Word Wrap Column\u0026#34; 中的较小值处折行。 \u0026#34;editor.wordWrapColumn\u0026#34;: 240, //编辑器窗口宽度大于x列时,自动换行 \u0026#34;editor.mouseWheelZoom\u0026#34;: false, //关闭鼠标滚轮缩放字体大小 \u0026#34;editor.suggest.snippetsPreventQuickSuggestions\u0026#34;: false, //控制活动代码段不阻止快速建议 \u0026#34;editor.suggestSelection\u0026#34;: \u0026#34;recentlyUsedByPrefix\u0026#34;, //代码补全列表中,优先选择最近的建议 \u0026#34;editor.stickyScroll.enabled\u0026#34;: true, //父级自动吸附置顶 \u0026#34;editor.wordSeparators\u0026#34;: \u0026#34;`~!@%^\u0026amp;*()=+[{]}\\\\|;:\u0026#39;\\\u0026#34;,.\u0026lt;\u0026gt;/?(),。;:\u0026#34;, //双击选中被截断字符 \u0026#34;editor.dragAndDrop\u0026#34;: false, //关闭拖拽移动代码 \u0026#34;editor.showFoldingControls\u0026#34;: \u0026#34;always\u0026#34;, //在行号槽中始终显示折叠控件 \u0026#34;editor.hover.above\u0026#34;: false, //鼠标指针悬停在代码元素上时,悬停提示将显示在鼠标指针的下方 \u0026#34;editor.hover.hidingDelay\u0026#34;: 0, //鼠标指针从悬停提示移开后,悬停提示立即消失 \u0026#34;editor.scrollbar.verticalScrollbarSize\u0026#34;: 12, //垂直滚动条的宽度 \u0026#34;editor.unicodeHighlight.nonBasicASCII\u0026#34;: false, //禁用非基本 ASCII 字符高亮 \u0026#34;editor.minimap.maxColumn\u0026#34;: 40, //侧边缩略图最大列数 // // //------------------文件资源管理器------------------ \u0026#34;explorer.confirmDelete\u0026#34;: false, //删除文件时不显示确认对话框 \u0026#34;explorer.enableDragAndDrop\u0026#34;: false, //禁用鼠标拖放文件 \u0026#34;explorer.confirmDragAndDrop\u0026#34;: false, //拖放文件时不显示确认对话框 \u0026#34;explorer.fileNesting.enabled\u0026#34;: true, //启用文件嵌套 \u0026#34;explorer.autoReveal\u0026#34;: true, //打开文件时自动展开文件夹 // // //------------------文件------------------ \u0026#34;files.autoGuessEncoding\u0026#34;: true, //自动保存 \u0026#34;files.autoSaveDelay\u0026#34;: 1000, //在最后一次编辑后x秒自动保存 \u0026#34;files.eol\u0026#34;: \u0026#34;\\n\u0026#34;, //使用换行符(LF)作为行尾字符 \u0026#34;files.exclude\u0026#34;: { //文件资源管理器排除目录 \u0026#34;**/.idea\u0026#34;: true, \u0026#34;**/__pycache__\u0026#34;: true, \u0026#34;**/.DS_Store\u0026#34;: true, }, \u0026#34;files.trimFinalNewlines\u0026#34;: true, //保存文件时自动删除文件末尾的多余换行符 \u0026#34;files.trimTrailingWhitespace\u0026#34;: true, //保存文件时自动删除行尾的多余空白字符 \u0026#34;files.insertFinalNewline\u0026#34;: true, //保存文件时自动插入最后一行的换行符 \u0026#34;files.refactoring.autoSave\u0026#34;: true, //重命名文件时自动保存 \u0026#34;files.watcherExclude\u0026#34;: { //文件监视器排除目录 \u0026#34;**/node_modules/**\u0026#34;: true, \u0026#34;**/dist/**\u0026#34;: true, \u0026#34;**/.idea/**\u0026#34;: true }, \u0026#34;files.hotExit\u0026#34;: \u0026#34;onExitAndWindowClose\u0026#34;, //关闭窗口时保存所有文件 // // //------------------终端------------------ \u0026#34;terminal.integrated.fontSize\u0026#34;: 13, //集成终端字号 \u0026#34;terminal.integrated.fontWeight\u0026#34;: \u0026#34;300\u0026#34;, //集成终端 \u0026#34;terminal.integrated.stickyScroll.enabled\u0026#34;: false, //终端命令置顶 \u0026#34;terminal.integrated.gpuAcceleration\u0026#34;: \u0026#34;on\u0026#34;, //使用GPU加速 \u0026#34;terminal.integrated.defaultLocation\u0026#34;: \u0026#34;editor\u0026#34;, //终端默认位置:编辑器 \u0026#34;terminal.integrated.enableMultiLinePasteWarning\u0026#34;: \u0026#34;never\u0026#34;, //关闭多行粘贴警告 \u0026#34;terminal.integrated.smoothScrolling\u0026#34;: true, //终端平滑滚动 \u0026#34;terminal.integrated.defaultProfile.linux\u0026#34;: \u0026#34;zsh\u0026#34;, //默认终端shell:zsh \u0026#34;terminal.integrated.scrollback\u0026#34;: 1500, //终端滚动条最大行数 // // //------------------其他自带设置------------------ \u0026#34;breadcrumbs.icons\u0026#34;: false, //目录导航不显示文件/文件夹图标 \u0026#34;notebook.breadcrumbs.showCodeCells\u0026#34;: false, //面包屑导航中不显示代码单元格 \u0026#34;notebook.output.fontFamily\u0026#34;: \u0026#34;\u0026#39;Cascadia Code PL\u0026#39;\u0026#34;, //笔记本输出区域字体 \u0026#34;notebook.output.fontSize\u0026#34;: 14, //笔记本输出区域字号 \u0026#34;search.exclude\u0026#34;: { //搜索排除目录 \u0026#34;**/.git\u0026#34;: true, \u0026#34;**/.svn\u0026#34;: true, \u0026#34;**/.vscode\u0026#34;: true, \u0026#34;**/.gitignore\u0026#34;: true, \u0026#34;**/node_modules\u0026#34;: true, \u0026#34;**/dist\u0026#34;: true, \u0026#34;**/build\u0026#34;: true, \u0026#34;**/*.lock\u0026#34;: true }, \u0026#34;search.searchEditor.singleClickBehaviour\u0026#34;: \u0026#34;peekDefinition\u0026#34;, //单击搜索结果时,在上下文内查看定义 \u0026#34;search.followSymlinks\u0026#34;: false, //关闭搜索中跟踪符号链接 \u0026#34;search.collapseResults\u0026#34;: \u0026#34;auto\u0026#34;, //搜索结果自动折叠 \u0026#34;search.showLineNumbers\u0026#34;: true, //搜索结果显示行号 \u0026#34;scm.inputFontSize\u0026#34;: 13, //源代码管理字号 \u0026#34;window.dialogStyle\u0026#34;: \u0026#34;custom\u0026#34;, //使用VSCode自定义的对话框样式 \u0026#34;window.density.editorTabHeight\u0026#34;: \u0026#34;compact\u0026#34;, //紧凑布局选项卡 \u0026#34;window.restoreWindows\u0026#34;: \u0026#34;none\u0026#34;, //窗口恢复 \u0026#34;window.newWindowProfile\u0026#34;: \u0026#34;默认\u0026#34;, //新窗口配置文件(不同步到其他配置文件) \u0026#34;window.customTitleBarVisibility\u0026#34;: \u0026#34;never\u0026#34;, //隐藏标题栏 \u0026#34;extensions.ignoreRecommendations\u0026#34;: true, //忽略推荐的扩展 \u0026#34;update.mode\u0026#34;: \u0026#34;manual\u0026#34;, //VS Code手动更新 \u0026#34;[jsonc]\u0026#34;: { //jsonc文件格式化 \u0026#34;editor.defaultFormatter\u0026#34;: \u0026#34;vscode.json-language-features\u0026#34; }, \u0026#34;security.workspace.trust.untrustedFiles\u0026#34;: \u0026#34;open\u0026#34;, //打开不受信任的文件 \u0026#34;git.openRepositoryInParentFolders\u0026#34;: \u0026#34;always\u0026#34;, //在父文件夹中打开仓库 \u0026#34;git.enableSmartCommit\u0026#34;: true, // \u0026#34;extensions.autoUpdate\u0026#34;: false, //禁用自动更新扩展 // // //------------------插件:Error Lens------------------ \u0026#34;errorLens.enabledDiagnosticLevels\u0026#34;: [ \u0026#34;warning\u0026#34;, //启用警告级别的诊断 \u0026#34;error\u0026#34; //启用错误级别的诊断 ], \u0026#34;errorLens.excludeBySource\u0026#34;: [ \u0026#34;cSpell\u0026#34;, //排除 cSpell 产生的诊断信息 \u0026#34;Grammarly\u0026#34;, //排除 Grammarly 产生的诊断信息 ], \u0026#34;errorLens.fontSize\u0026#34;: \u0026#34;15\u0026#34;, //ErrorLens字号 \u0026#34;errorLens.fontWeight\u0026#34;: \u0026#34;300\u0026#34;, //ErrorLens字体粗细 // // //------------------插件:GitHub Copilot及Chat------------------ \u0026#34;github.copilot.enable\u0026#34;: { \u0026#34;*\u0026#34;: true, //启用github copilot \u0026#34;markdown\u0026#34;: true, //启用github copilot的markdown支持 \u0026#34;plaintext\u0026#34;: true, //启用github copilot的纯文本支持 \u0026#34;scminput\u0026#34;: true //启用github copilot的源代码管理输入支持 }, \u0026#34;github.copilot.chat.localeOverride\u0026#34;: \u0026#34;zh-CN\u0026#34;, //聊天窗口语言:简体中文 // // //=== 官方 === \u0026#34;github.copilot.advanced\u0026#34;: { \u0026#34;authProvider\u0026#34;: \u0026#34;github\u0026#34; }, // // //------------------插件:Comment Translate\u0026amp;Deeplx comment translate------------------ \u0026#34;commentTranslate.targetLanguage\u0026#34;: \u0026#34;zh-CN\u0026#34;, //翻译目标语言:简体中文 \u0026#34;commentTranslate.hover.string\u0026#34;: true, //悬停翻译字符串 \u0026#34;commentTranslate.multiLineMerge\u0026#34;: true, //多行合并翻译 \u0026#34;commentTranslate.source\u0026#34;: \u0026#34;deeplx-comment-translate.deeplx-comment-translate-deeplx\u0026#34;, //翻译源:deeplx-comment-translate \u0026#34;commentTranslate.maxTranslationLength\u0026#34;: 10000, //最大翻译长度:看作无上限 \u0026#34;deeplxTranslate.authKey\u0026#34;: \u0026#34;填上你的deeplx key\u0026#34;, //deeplx翻译插件的API密钥 // // //------------------插件:Vscode Office------------------ \u0026#34;vscode-office.editorTheme\u0026#34;: \u0026#34;Auto\u0026#34;, //编辑器主题 \u0026#34;vscode-office.openOutline\u0026#34;: false, //打开大纲 // // //------------------插件:gitlens------------------ \u0026#34;gitlens.currentLine.enabled\u0026#34;: false, //关闭gitlens当前行信息 \u0026#34;gitlens.blame.format\u0026#34;: \u0026#34;${author|7} ${agoOrDate|14-relative} ${message|50}\u0026#34;, //gitlens blame信息格式:作者 日期 消息 \u0026#34;gitlens.hovers.currentLine.over\u0026#34;: \u0026#34;line\u0026#34;, //gitlens当前行悬停信息显示在行上 \u0026#34;gitlens.hovers.enabled\u0026#34;: true, //启用gitlens悬停信息 \u0026#34;gitlens.hovers.avatars\u0026#34;: false, //关闭gitlens头像 \u0026#34;gitlens.blame.compact\u0026#34;: false, //关闭gitlens紧凑模式 \u0026#34;gitlens.blame.separateLines\u0026#34;: true, //gitlens blame信息分行显示 \u0026#34;gitlens.codeLens.authors.enabled\u0026#34;: false, //关闭gitlens代码镜头作者信息 \u0026#34;gitlens.views.repositories.files.layout\u0026#34;: \u0026#34;tree\u0026#34;, //gitlens文件布局:树形 \u0026#34;gitlens.views.repositories.compact\u0026#34;: true, //gitlens仓库视图紧凑模式 \u0026#34;gitlens.defaultDateLocale\u0026#34;: \u0026#34;zh-CN\u0026#34;, //gitlens默认日期语言:简体中文 \u0026#34;gitlens.defaultDateFormat\u0026#34;: \u0026#34;YYYY-MM-DD HH:mm\u0026#34;, //gitlens默认日期格式 \u0026#34;gitlens.defaultDateStyle\u0026#34;: \u0026#34;absolute\u0026#34;, //gitlens默认日期样式 \u0026#34;gitlens.advanced.messages\u0026#34;: { //gitlens高级消息 \u0026#34;suppressCommitHasNoPreviousCommitWarning\u0026#34;: true, //抑制提交没有上一个提交的警告 }, \u0026#34;gitlens.gitCommands.search.showResultsInSideBar\u0026#34;: true, //gitlens搜索结果显示在侧边栏 \u0026#34;gitlens.advanced.fileHistoryShowAllBranches\u0026#34;: true, //gitlens文件历史显示所有分支 } }在复制粘贴完上面内容后,主要需要修改以下自定义配置(主要是字体和插件 API):\neditor.codeLensFontFamily:CodeLens(提示)字体 editor.fontFamily:字体 editor.fontSize:字号 terminal.integrated.fontSize:终端字号 notebook.output.fontFamily:笔记本输出区域字体 scm.inputFontSize:源代码管理字号 errorLens.fontSize:ErrorLens 插件字号 github.copilot.advanced和github-enterprise.uri:GitHub Copilot 插件的外部 API 设置(如果是官方 copilot,可以不设置) deeplxTranslate.authKey:deeplx 翻译插件的 API 密钥 2.3 vscode 基础快捷键配置 # 按快捷键Ctrl+Shift+P,在输入框内输入keyboard json,选中Preferences: Open Keyboard Shortcuts (JSON),此时会有一个keybindings.json文件被打开,修改其内容为下面内容\nJSON不换行 [ { //终端复制 \u0026#34;key\u0026#34;: \u0026#34;ctrl+c\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.terminal.copySelection\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;terminalTextSelectedInFocused || terminalFocus \u0026amp;\u0026amp; terminalHasBeenCreated \u0026amp;\u0026amp; terminalTextSelected || terminalFocus \u0026amp;\u0026amp; terminalProcessSupported \u0026amp;\u0026amp; terminalTextSelected || terminalFocus \u0026amp;\u0026amp; terminalTextSelected \u0026amp;\u0026amp; terminalTextSelectedInFocused || terminalHasBeenCreated \u0026amp;\u0026amp; terminalTextSelected \u0026amp;\u0026amp; terminalTextSelectedInFocused || terminalProcessSupported \u0026amp;\u0026amp; terminalTextSelected \u0026amp;\u0026amp; terminalTextSelectedInFocused\u0026#34; }, { //终端粘贴 \u0026#34;key\u0026#34;: \u0026#34;ctrl+v\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.terminal.paste\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;terminalFocus \u0026amp;\u0026amp; terminalHasBeenCreated || terminalFocus \u0026amp;\u0026amp; terminalProcessSupported\u0026#34; }, { //切换配置文件 \u0026#34;key\u0026#34;: \u0026#34;ctrl+shift+[\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.profiles.actions.switchProfile\u0026#34; }, { //显示远程菜单 \u0026#34;key\u0026#34;: \u0026#34;ctrl+shift+]\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.remote.showMenu\u0026#34; } ] 3. vscode C++开发配置和插件 # 先按快捷键Ctrl+Shift+P,在输入框内输入new profile,选中Profiles: New Profile,此时会有一个配置文件页面打开,修改新出现的配置文件名称为C++;图标选一个喜欢的;复制自为无;Contents里面键盘快捷方式选择源自默认、其他都选择源自无,最后点击创建。\n再按快捷键Ctrl+Shift+[,选择C++(之前设置的配置文件名称),此时vscode会自动切换到C++配置文件,接下来的配置都是基于C++配置文件的。\n3.1 vscode C++开发插件 # 和之前一样,进入vscode,点击扩展,依次输入下列内容安装显示的第一个插件\nxaver.clang-format:使用.clang-format文件进行代码格式化 llvm-vs-code-extensions.vscode-clangd:clangd 插件 vadimcn.vscode-lldb:lldb 调试插件 ajshort.include-autocomplete:头文件自动补全 tboox.xmake-vscode:xmake 构建系统 alefragnani.project-manager:项目管理(注意:这个插件如果多个配置文件同时装,会显示全部项目) 过程中,如果插件提示你需要下载其他内容,请允许\n3.2 vscode C++开发配置 # 和之前一样,按快捷键Ctrl+Shift+P,在输入框内输入user json,选中Preferences: Open Settings (JSON),此时会有一个新的settings.json文件被打开,修改其内容为下面内容\nJSON不换行 { //----------------xmake设置----------------- \u0026#34;xmake.debugConfigType\u0026#34;: \u0026#34;codelldb\u0026#34;, //使用codelldb插件调试 \u0026#34;xmake.runMode\u0026#34;: \u0026#34;buildRun\u0026#34;, //运行前自动构建 \u0026#34;xmake.buildLevel\u0026#34;: \u0026#34;verbose\u0026#34;, //设置编译时输出信息级别,仅输出编译警告信息以及正常信息 \u0026#34;xmake.customDebugConfig\u0026#34;: { \u0026#34;console\u0026#34;: \u0026#34;integratedTerminal\u0026#34; // xmake调试时使用集成终端 }, // // //-----------------clangd设置----------------- \u0026#34;clangd.path\u0026#34;: \u0026#34;/usr/bin/clangd\u0026#34;, //指定 clangd 的路径 \u0026#34;clangd.arguments\u0026#34;: [ // compile_commands.json 生成文件夹 \u0026#34;--compile-commands-dir=${workspaceFolder}/.vscode\u0026#34;, // 让 Clangd 生成更详细的日志 \u0026#34;--log=verbose\u0026#34;, // 输出的 JSON 文件更美观 \u0026#34;--pretty\u0026#34;, // 全局补全 \u0026#34;--all-scopes-completion\u0026#34;, // 更详细的补全内容 \u0026#34;--completion-style=detailed\u0026#34;, // 建议风格:打包(重载函数只会给出一个建议) // 相反可以设置为detailed \u0026#34;--completion-style=bundled\u0026#34;, // 跨文件重命名变量 \u0026#34;--cross-file-rename\u0026#34;, // 允许补充头文件 \u0026#34;--header-insertion=iwyu\u0026#34;, // 输入建议中,已包含头文件的项与还未包含头文件的项会以圆点加以区分 \u0026#34;--header-insertion-decorators\u0026#34;, // 在后台自动分析文件 // 基于 complie_commands.json,生成命令 // xmake project -k compile_commands \u0026#34;--background-index\u0026#34;, // 启用 Clang-Tidy 以提供「静态检查」 \u0026#34;--clang-tidy\u0026#34;, \u0026#34;--clang-tidy-checks=cppcoreguidelines-*, performance-*, bugprone-*, misc-*, google-*, modernize-*, readability-*, portability-*\u0026#34;, // 默认格式化风格: 谷歌开源项目代码指南 // C++文件夹下.clang-format作为全部子文件夹的默认 \u0026#34;--fallback-style=file\u0026#34;, // 同时开启的任务数量 \u0026#34;-j=16\u0026#34;, // pch优化的位置(memory 或 disk,选择memory会增加内存开销,但会提升性能) 推荐在板子上使用disk \u0026#34;--pch-storage=memory\u0026#34;, // 启用这项时,补全函数时,将会给参数提供占位符,键入后按 Tab 可以切换到下一占位符,乃至函数末。我选择禁用 \u0026#34;--function-arg-placeholders=false\u0026#34;, \u0026#34;--query-driver=/usr/bin/g++*\u0026#34;, //设定 clang 编译器的路径。clangd启动时需要通过参数指定从那个目录搜索标准库头文件 \u0026#34;--completion-parse=auto\u0026#34;, //当 clangd 准备就绪时,用它来分析建议 \u0026#34;--enable-config\u0026#34;, \u0026#34;--function-arg-placeholders=true\u0026#34;, // 补全函数时,将会给参数提供占位符,键入后按 Tab 可以切换到下一占位符,乃至函数末 \u0026#34;--ranking-model=decision_forest\u0026#34; //建议的排序方案:hueristics (启发式), decision_forest (决策树) ], \u0026#34;clangd.checkUpdates\u0026#34;: true, \u0026#34;clangd.onConfigChanged\u0026#34;: \u0026#34;restart\u0026#34;, // 重启 clangd 时重载配置,具体方法: F1 + Fn 打开命令面板,然后搜索“clangd: restart\u0026#34; // \u0026#34;clangd.serverCompletionRanking\u0026#34;: true, // 借助网上的信息排序建议 \u0026#34;clangd.detectExtensionConflicts\u0026#34;: true, // 当其它拓展与 clangd 冲突时警告并建议禁用 \u0026#34;clangd.fallbackFlags\u0026#34;: [ \u0026#34;-pedantic\u0026#34;, //启用所有警告 \u0026#34;-Wall\u0026#34;, //启用所有警告 \u0026#34;-Wextra\u0026#34;, //启用额外警告 \u0026#34;-Wcast-align\u0026#34;, //强制转换时检查对齐 \u0026#34;-Wdouble-promotion\u0026#34;, //强制转换时检查 double 类型提升 \u0026#34;-Wformat=2\u0026#34;, //检查 printf 和 scanf 函数的参数 \u0026#34;-Wimplicit-fallthrough\u0026#34;, //检查 switch 语句中的隐式 fallthrough \u0026#34;-Wmisleading-indentation\u0026#34;, //检查缩进是否误导 \u0026#34;-Wnon-virtual-dtor\u0026#34;, //检查基类析构函数是否为虚函数 \u0026#34;-Wnull-dereference\u0026#34;, //检查空指针解引用 \u0026#34;-Wold-style-cast\u0026#34;, //检查 C 风格的强制转换 \u0026#34;-Woverloaded-virtual\u0026#34;, //检查虚函数重载 \u0026#34;-Wpedantic\u0026#34;, //启用所有警告 \u0026#34;-Wshadow\u0026#34;, //检查局部变量和全局变量同名 \u0026#34;-Wunused\u0026#34;, //检查未使用的变量 \u0026#34;-pthread\u0026#34;, //启用多线程支持 \u0026#34;-fuse-ld=lld\u0026#34;, //使用 lld 作为链接器 \u0026#34;-fsanitize=address\u0026#34;, //启用地址检查 \u0026#34;-fsanitize=undefined\u0026#34;, //启用未定义行为检查 \u0026#34;-stdlib=libc++\u0026#34;, //使用 libc++ 标准库 \u0026#34;-std=c++20\u0026#34; //使用 C++20 标准 ], // // //-----------------格式化设置----------------- \u0026#34;clang-format.executable\u0026#34;: \u0026#34;/usr/bin/clang-format\u0026#34;, //指定 clang-format 的路径 \u0026#34;[cpp]\u0026#34;: { \u0026#34;editor.defaultFormatter\u0026#34;: \u0026#34;xaver.clang-format\u0026#34; //设置默认格式化器 }, \u0026#34;[c]\u0026#34;: { \u0026#34;editor.defaultFormatter\u0026#34;: \u0026#34;xaver.clang-format\u0026#34; //设置默认格式化器 }, \u0026#34;files.associations\u0026#34;: { //文件关联 \u0026#34;*.cpp\u0026#34;: \u0026#34;cpp\u0026#34;, \u0026#34;*.hpp\u0026#34;: \u0026#34;cpp\u0026#34;, \u0026#34;*.h\u0026#34;: \u0026#34;cpp\u0026#34; }, // // //-----------------lldb设置----------------- \u0026#34;lldb.commandCompletions\u0026#34;: true, // LLDB 指令自动补全 \u0026#34;lldb.dereferencePointers\u0026#34;: true, // LLDB 指针显示解引用内容 \u0026#34;lldb.evaluateForHovers\u0026#34;: true, // LLDB 鼠标悬停在变量上时预览变量值 \u0026#34;lldb.launch.expressions\u0026#34;: \u0026#34;native\u0026#34;, // LLDB 监视表达式的默认类型 \u0026#34;lldb.showDisassembly\u0026#34;: \u0026#34;never\u0026#34;, // LLDB 不显示汇编代码 \u0026#34;lldb.verboseLogging\u0026#34;: true // LLDB 输出详细日志 }在复制粘贴完上面内容后,可能需要修改以下自定义配置:\nclangd.path:指定clangd的路径,根据自己的clangd安装路径进行修改 clang-format.executable:指定clang-format的路径,根据自己的clang-format安装路径进行修改 clangd.arguments内的-j=16:设置同时开启的任务数量,根据自己的电脑性能进行修改(可在终端输入nproc查看最高值) clangd.fallbackFlags内的-std=c++20:设置使用的 C++标准 4. 额外配置 # 接下来两个文件可以放在所有在项目文件夹同级目录下,这样就可以在所有项目中使用这两个文件了。\n示例文件树:\nSHELL不换行 /home/user/Code/C++ ├── .clang-format ├── .clangd ├── project1 ├── project2 .clang-format:具体配置可以参考https://clang.llvm.org/docs/ClangFormatStyleOptions.html\nYAML不换行 # 语言: None, Cpp, Java, JavaScript, ObjC, Proto, TableGen, TextProto Language: Cpp # BasedOnStyle: LLVM # 访问说明符(public、private等)的偏移 AccessModifierOffset: -4 # 开括号(开圆括号、开尖括号、开方括号)后的对齐: Align, DontAlign, AlwaysBreak(总是在开括号后换行) AlignAfterOpenBracket: Align # 连续赋值时,对齐所有等号 AlignConsecutiveAssignments: true # 连续声明时,对齐所有声明的变量名 AlignConsecutiveDeclarations: true # 左对齐逃脱换行(使用反斜杠换行)的反斜杠 AlignEscapedNewlinesLeft: true # 水平对齐二元和三元表达式的操作数 AlignOperands: true # 对齐连续的尾随的注释 AlignTrailingComments: true # 允许函数声明的所有参数在放在下一行 AllowAllParametersOfDeclarationOnNextLine: true # 允许短的块放在同一行 AllowShortBlocksOnASingleLine: false # 允许短的case标签放在同一行 AllowShortCaseLabelsOnASingleLine: false # 允许短的函数放在同一行: None, InlineOnly(定义在类中), Empty(空函数), Inline(定义在类中,空函数), All AllowShortFunctionsOnASingleLine: Empty # 允许短的if语句保持在同一行 AllowShortIfStatementsOnASingleLine: false # 允许短的循环保持在同一行 AllowShortLoopsOnASingleLine: false # 总是在定义返回类型后换行(deprecated) AlwaysBreakAfterDefinitionReturnType: None # 总是在返回类型后换行: None, All, TopLevel(顶级函数,不包括在类中的函数), # AllDefinitions(所有的定义,不包括声明), TopLevelDefinitions(所有的顶级函数的定义) AlwaysBreakAfterReturnType: None # 总是在多行string字面量前换行 AlwaysBreakBeforeMultilineStrings: false # 总是在template声明后换行 AlwaysBreakTemplateDeclarations: false # false表示函数实参要么都在同一行,要么都各自一行 BinPackArguments: true # false表示所有形参要么都在同一行,要么都各自一行 BinPackParameters: true # 大括号换行,只有当BreakBeforeBraces设置为Custom时才有效 BraceWrapping: # class定义后面 AfterClass: false # 控制语句后面 AfterControlStatement: false # enum定义后面 AfterEnum: false # 函数定义后面 AfterFunction: false # 命名空间定义后面 AfterNamespace: false # ObjC定义后面 AfterObjCDeclaration: false # struct定义后面 AfterStruct: false # union定义后面 AfterUnion: false # catch之前 BeforeCatch: true # else之前 BeforeElse: true # 缩进大括号 IndentBraces: false # 在二元运算符前换行: None(在操作符后换行), NonAssignment(在非赋值的操作符前换行), All(在操作符前换行) BreakBeforeBinaryOperators: NonAssignment # 在大括号前换行: Attach(始终将大括号附加到周围的上下文), Linux(除函数、命名空间和类定义,与Attach类似), # Mozilla(除枚举、函数、记录定义,与Attach类似), Stroustrup(除函数定义、catch、else,与Attach类似), # Allman(总是在大括号前换行), GNU(总是在大括号前换行, # 并对于控制语句的大括号增加额外的缩进), WebKit(在函数前换行), Custom # 注:这里认为语句块也属于函数 BreakBeforeBraces: Allman # 在三元运算符前换行 BreakBeforeTernaryOperators: true # 在构造函数的初始化列表的逗号前换行 BreakConstructorInitializersBeforeComma: false # 每行字符的限制,0表示没有限制 ColumnLimit: 200 # 描述具有特殊意义的注释的正则表达式,它不应该被分割为多行或以其它方式改变 CommentPragmas: \u0026#39;^ IWYU pragma:\u0026#39; # 构造函数的初始化列表要么都在同一行,要么都各自一行 ConstructorInitializerAllOnOneLineOrOnePerLine: false # 构造函数的初始化列表的缩进宽度 ConstructorInitializerIndentWidth: 4 # 延续的行的缩进宽度 ContinuationIndentWidth: 4 # 去除C++11的列表初始化的大括号{后和}前的空格 Cpp11BracedListStyle: false # 继承最常用的指针和引用的对齐方式 DerivePointerAlignment: false # 关闭格式化 DisableFormat: false # 自动检测函数的调用和定义是否被格式为每行一个参数(Experimental) ExperimentalAutoDetectBinPacking: false # 需要被解读为foreach循环而不是函数调用的宏 ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] # 对#include进行排序,匹配了某正则表达式的#include拥有对应的优先级, # 匹配不到的则默认优先级为INT_MAX(优先级越小排序越靠前), # 可以定义负数优先级从而保证某些#include永远在最前面 IncludeCategories: - Regex: \u0026#39;^\u0026#34;(llvm|llvm-c|clang|clang-c)/\u0026#39; Priority: 2 - Regex: \u0026#39;^(\u0026lt;|\u0026#34;(gtest|isl|json)/)\u0026#39; Priority: 3 - Regex: \u0026#39;.*\u0026#39; Priority: 1 # 缩进case标签 IndentCaseLabels: false # 缩进宽度 IndentWidth: 4 # 函数返回类型换行时,缩进函数声明或函数定义的函数名 IndentWrappedFunctionNames: false # 保留在块开始处的空行 KeepEmptyLinesAtTheStartOfBlocks: true # 开始一个块的宏的正则表达式 MacroBlockBegin: \u0026#39;\u0026#39; # 结束一个块的宏的正则表达式 MacroBlockEnd: \u0026#39;\u0026#39; # 连续空行的最大数量 MaxEmptyLinesToKeep: 1 # 命名空间的缩进: None, Inner(缩进嵌套的命名空间中的内容), All NamespaceIndentation: Inner # 使用ObjC块时缩进宽度 ObjCBlockIndentWidth: 4 # 在ObjC的@property后添加一个空格 ObjCSpaceAfterProperty: false # 在ObjC的protocol列表前添加一个空格 ObjCSpaceBeforeProtocolList: true # 在call(后对函数调用换行的penalty PenaltyBreakBeforeFirstCallParameter: 19 # 在一个注释中引入换行的penalty PenaltyBreakComment: 300 # 第一次在\u0026lt;\u0026lt;前换行的penalty PenaltyBreakFirstLessLess: 120 # 在一个字符串字面量中引入换行的penalty PenaltyBreakString: 1000 # 对于每个在行字符数限制之外的字符的penalty PenaltyExcessCharacter: 1000000 # 将函数的返回类型放到它自己的行的penalty PenaltyReturnTypeOnItsOwnLine: 60 # 指针和引用的对齐: Left, Right, Middle PointerAlignment: Left # 允许重新排版注释 ReflowComments: true # 允许排序#include SortIncludes: true # 在C风格类型转换后添加空格 SpaceAfterCStyleCast: false # 在赋值运算符之前添加空格 SpaceBeforeAssignmentOperators: true # 开圆括号之前添加一个空格: Never, ControlStatements, Always SpaceBeforeParens: ControlStatements # 在空的圆括号中添加空格 SpaceInEmptyParentheses: false # 在尾随的评论前添加的空格数(只适用于//) SpacesBeforeTrailingComments: 2 # 在尖括号的\u0026lt;后和\u0026gt;前添加空格 SpacesInAngles: false # 在容器(ObjC和JavaScript的数组和字典等)字面量中添加空格 SpacesInContainerLiterals: true # 在C风格类型转换的括号中添加空格 SpacesInCStyleCastParentheses: true # 在圆括号的(后和)前添加空格 SpacesInParentheses: false # 在方括号的[后和]前添加空格,lamda表达式和未指明大小的数组的声明不受影响 SpacesInSquareBrackets: false # 标准: Cpp03, Cpp11, Auto Standard: Cpp11 # tab宽度 TabWidth: 4 # 使用tab字符: Never, ForIndentation, ForContinuationAndIndentation, Always UseTab: Never .clangd:具体配置可以参考https://clang.llvm.org/extra/clang-tidy/\nYAML不换行 Index: Background: Build Diagnostics: ClangTidy: Add: [\u0026#39;*\u0026#39;] Remove: [ abseil*, fuchsia*, llvmlib*, zircon*, altera*, google-readability-todo, readability-braces-around-statements, hicpp-braces-around-statements, hicpp-signed-bitwise, modernize-use-trailing-return-type, readability-identifier-length, cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers, bugprone-easily-swappable-parameters ] 5. 项目使用测试 # 这里直接给出测试文件(C++的基础上测试 opencv 库、fmt 库、eigen 库使用,如果不需要,请手动去除对应部分)\n先新建项目文件夹,然后在项目文件夹下,依次保存下面文件:\nxmake.lua\nLUA不换行 add_rules(\u0026#34;mode.debug\u0026#34;, \u0026#34;mode.release\u0026#34;) set_toolchains(\u0026#34;clang\u0026#34;) set_languages(\u0026#34;c99\u0026#34;, \u0026#34;c++20\u0026#34;) add_rules(\u0026#34;plugin.compile_commands.autoupdate\u0026#34;, {outputdir = \u0026#34;.vscode\u0026#34;}) add_requires(\u0026#34;opencv\u0026#34;, {system = false ,configs = {shared = true, gtk = true}}) add_requires(\u0026#34;fmt\u0026#34;) add_requires(\u0026#34;eigen\u0026#34;) target(\u0026#34;template\u0026#34;) set_kind(\u0026#34;binary\u0026#34;) add_files(\u0026#34;src/*.cpp\u0026#34;) add_packages(\u0026#34;opencv\u0026#34;) add_packages(\u0026#34;fmt\u0026#34;) add_packages(\u0026#34;eigen\u0026#34;) src文件夹内main.cpp\nCPP不换行 #include \u0026lt;Eigen/Dense\u0026gt; #include \u0026lt;fmt/format.h\u0026gt; #include \u0026lt;opencv2/opencv.hpp\u0026gt; int main() { std::cout \u0026lt;\u0026lt; \u0026#34;[成功]:std库测试:hello world!\u0026#34; \u0026lt;\u0026lt; std::endl; Eigen::MatrixXd matrix1(2, 2); matrix1(0, 0) = 1; matrix1(0, 1) = 2; matrix1(1, 0) = 3; matrix1(1, 1) = 4; std::cout \u0026lt;\u0026lt; \u0026#34;[成功]:Eigen库矩阵测试:matrix1 = \u0026#34; \u0026lt;\u0026lt; std::endl \u0026lt;\u0026lt; matrix1 \u0026lt;\u0026lt; std::endl; fmt::print(\u0026#34;[{}]:fmt库测试:输出\\n\u0026#34;, \u0026#34;成功\u0026#34;); cv::Mat image = cv::imread(\u0026#34;../../../../data/1.jpg\u0026#34;); cv::imshow(\u0026#34;[成功]:OpenCV库测试:显示\u0026#34;, image); fmt::print(\u0026#34;按任意键退出..\\n\u0026#34;); cv::waitKey(0); return 0; } 然后在项目文件夹下新建data文件夹,放入一张名为1.jpg的图片\n5.1 编译 # 在vscode下方状态栏的XMake(扩展)中将Set build mode设置为debug;change the toolchain设置为clang\n然后看到Build the given target,点击编译\n第一次编译xmake需要安装外部库,一路选y就可以。\n5.2 运行 # 在编译完成后,点击Run the given target,即可运行\n5.3 调试 # 在编译完成后,点击Debug the given target,即可调试\n可以打几个断点,然后调试\n参考资料 # 点击展开查看参考资料 https://www.cnblogs.com/guoxuanhan/p/17864923.html ","date":"2024年11月12日","externalUrl":null,"permalink":"/posts/linux%E5%B9%B3%E5%8F%B0c++%E5%BC%80%E5%8F%91%E9%85%8D%E7%BD%AE/","section":"文章","summary":"","title":"ArchLinux平台 C/C++开发配置","type":"posts"},{"content":"","date":"2024年11月12日","externalUrl":null,"permalink":"/tags/c++/","section":"Tags","summary":"","title":"C++","type":"tags"},{"content":"","date":"2024年11月12日","externalUrl":null,"permalink":"/tags/clangd/","section":"Tags","summary":"","title":"Clangd","type":"tags"},{"content":"","date":"2024年11月12日","externalUrl":null,"permalink":"/tags/lldb/","section":"Tags","summary":"","title":"Lldb","type":"tags"},{"content":"","date":"2024年11月12日","externalUrl":null,"permalink":"/tags/vscode/","section":"Tags","summary":"","title":"Vscode","type":"tags"},{"content":"","date":"2024年11月12日","externalUrl":null,"permalink":"/tags/xmake/","section":"Tags","summary":"","title":"Xmake","type":"tags"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"分类","summary":"","title":"分类","type":"categories"},{"content":"","externalUrl":null,"permalink":"/postgraduate/","section":"学习思考","summary":"","title":"学习思考","type":"postgraduate"},{"content":"","externalUrl":null,"permalink":"/posts/","section":"文章","summary":"","title":"文章","type":"posts"}]