<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Xiaohei&apos;s Blog</title><description>红鼻子小黑</description><link>https://xiaohei-blog.vercel.app</link><item><title>周记 · 第一周</title><link>https://xiaohei-blog.vercel.app/blog/journal-2</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/journal-2</guid><description>2026-03-30 ~ 2026-04-05.</description><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;
import { RatingCriteria, ArxivRating } from &apos;@/components/advanced&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;waiting...&lt;/p&gt;
&lt;h2&gt;thinking...&lt;/h2&gt;
&lt;p&gt;waiting...&lt;/p&gt;
&lt;h2&gt;科研&lt;/h2&gt;
&lt;p&gt;waiting...&lt;/p&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;p&gt;waiting...&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;waiting...&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CXEwB4jO.jpg"/><enclosure url="/_astro/heroimage.CXEwB4jO.jpg"/></item><item><title>周记 · 开始</title><link>https://xiaohei-blog.vercel.app/blog/journal-1</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/journal-1</guid><description>2026-03-23 ~ 2026-03-29.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;
import { RatingCriteria, ArxivRating } from &apos;@/components/advanced&apos;&lt;/p&gt;
&lt;h2&gt;周记&lt;/h2&gt;
&lt;p&gt;《致橡树》&lt;/p&gt;
&lt;p&gt;作者：舒婷&lt;/p&gt;
&lt;p&gt;绝不像攀援的凌霄花，&lt;/p&gt;
&lt;p&gt;借你的高枝炫耀自己；&lt;/p&gt;
&lt;p&gt;我如果爱你 ——&lt;/p&gt;
&lt;p&gt;绝不学痴情的鸟儿，&lt;/p&gt;
&lt;p&gt;为绿荫重复单调的歌曲；&lt;/p&gt;
&lt;p&gt;也不止像泉源，&lt;/p&gt;
&lt;p&gt;常年送来清凉的慰藉；&lt;/p&gt;
&lt;p&gt;也不止像险峰，&lt;/p&gt;
&lt;p&gt;增加你的高度，衬托你的威仪。&lt;/p&gt;
&lt;p&gt;甚至日光，&lt;/p&gt;
&lt;p&gt;甚至春雨。&lt;/p&gt;
&lt;p&gt;不，这些都还不够！&lt;/p&gt;
&lt;p&gt;我必须是你近旁的一株木棉，&lt;/p&gt;
&lt;p&gt;作为树的形象和你站在一起。&lt;/p&gt;
&lt;p&gt;根，紧握在地下；&lt;/p&gt;
&lt;p&gt;叶，相触在云里。&lt;/p&gt;
&lt;p&gt;每一阵风过，&lt;/p&gt;
&lt;p&gt;我们都互相致意，&lt;/p&gt;
&lt;p&gt;但没有人，&lt;/p&gt;
&lt;p&gt;听懂我们的言语。&lt;/p&gt;
&lt;p&gt;你有你的铜枝铁干，&lt;/p&gt;
&lt;p&gt;像刀，像剑，也像戟；&lt;/p&gt;
&lt;p&gt;我有我红硕的花朵，&lt;/p&gt;
&lt;p&gt;像沉重的叹息，&lt;/p&gt;
&lt;p&gt;又像英勇的火炬。&lt;/p&gt;
&lt;p&gt;我们分担寒潮、风雷、霹雳；&lt;/p&gt;
&lt;p&gt;我们共享雾霭、流岚、虹霓。&lt;/p&gt;
&lt;p&gt;仿佛永远分离，&lt;/p&gt;
&lt;p&gt;却又终身相依。&lt;/p&gt;
&lt;p&gt;这才是伟大的爱情，&lt;/p&gt;
&lt;p&gt;坚贞就在这里：&lt;/p&gt;
&lt;p&gt;爱 ——&lt;/p&gt;
&lt;p&gt;不仅爱你伟岸的身躯，&lt;/p&gt;
&lt;p&gt;也爱你坚持的位置，足下的土地。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;可能会很奇怪，明明是写周记，为什么开头是一首诗。但我觉得这首诗很适合我现在的状态：虽然我很想和大佬们一样在科研路上飞速前进，但我也知道自己还处在一个“攀援的凌霄花”的阶段，不能急于求成。就像诗里说的，我希望自己能像一株木棉一样，先踏实的在土地上扎下根来，积累力量，有我自己的铜枝铁干，同样，我也可以有我的红硕的花朵。就慢慢做一个长期主义者吧，仍然可以像小孩一样，对这个世界充满好奇，依然可以很纯粹，依然可以很简单。但是我也希望，我可以成为一个可以保护好自己，然后可以保护身边力所能及的人的大人。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CHTCkhja.jpg"/><enclosure url="/_astro/heroimage.CHTCkhja.jpg"/></item><item><title>GitHub Pages + Vercel 双仓部署指南</title><link>https://xiaohei-blog.vercel.app/blog/deploy-gitpage</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/deploy-gitpage</guid><description>GitHub Pages与Vercel双仓发布教程：源码私有、站点公开、Actions 自动推送</description><pubDate>Thu, 12 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside, StepIndent } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;如果说用 Vercel 部署个人博客是一种“随手即得”的快乐，那么使用 GitHub Pages 部署博客，更像是一种“更接近底层逻辑”的练习。它没有那么多平台帮你兜底的自动推断，也正因为如此，你会更清楚地理解：构建产物是什么、发布仓库是什么、为什么要区分源码和站点、为什么有些东西该提交、有些东西不该提交。&lt;/p&gt;
&lt;p&gt;这篇文章记录的，就是我把博客做成“源码私有仓库 + 公开 Pages 发布仓库”的全过程。与上一篇一样这既是一个给我自己的记录，又是一个写给大家的操作教程。但这一切都有一些前提条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已经有一个本地能跑起来的博客项目；&lt;/li&gt;
&lt;li&gt;希望源码仓库保持私有，但站点必须公开访问；&lt;/li&gt;
&lt;li&gt;想把 GitHub Pages 和 Actions 这套流程真正打通；&lt;/li&gt;
&lt;li&gt;不想每次手动复制 &lt;code&gt;dist/&lt;/code&gt;，而是希望 push 代码后自动发布。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Part 1：先理解这套方案到底在做什么&lt;/h2&gt;
&lt;p&gt;如果你是第一次接触 GitHub Pages，很容易把“源码仓库”和“发布仓库”混在一起。实际上，在双仓部署方案里，它们扮演的是完全不同的角色。&lt;/p&gt;
&lt;h3&gt;为什么要拆成两个仓库？&lt;/h3&gt;
&lt;p&gt;简单来说，我们希望同时满足两个目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源码尽量私有：因为博客源码里会包含主题配置、工作流、一些尚未发布的内容，甚至有时会带上实验中的功能。&lt;/li&gt;
&lt;li&gt;站点必须公开：因为 GitHub Pages 最终是给别人访问的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果把所有东西都堆在一个仓库里，当然也不是不行，但你会在“源码是否公开”“发布产物是否提交”“Pages 从哪一个目录读取文件”这些问题上不断纠结。双仓方案的好处就是职责清晰：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[用户名]/[仓库1]&lt;/code&gt;：私有源码仓库&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[用户名]/[用户名].github.io&lt;/code&gt;：公开发布仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前者负责写代码、写文章、执行构建；后者只负责承载已经构建好的静态网页。&lt;/p&gt;
&lt;h3&gt;这套流程的本质是什么？&lt;/h3&gt;
&lt;p&gt;本质上，我们做的事情只有三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在私有的源码仓库里执行静态构建，得到 &lt;code&gt;dist/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过 GitHub Actions 把 &lt;code&gt;dist/&lt;/code&gt; 推送到公开仓库根目录&lt;/li&gt;
&lt;li&gt;让 GitHub Pages 从公开仓库的 &lt;code&gt;main / (root)&lt;/code&gt; 发布网站&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Part 2：开始之前，我们应该已经有什么&lt;/h2&gt;
&lt;p&gt;在往下操作之前，最好先确认下面这些前提已经满足。&lt;/p&gt;
&lt;h3&gt;准备好仓库&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;私有源码仓库：&lt;code&gt;[用户名]/[仓库1]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;公共发布仓库：&lt;code&gt;[用户名]/[用户名].github.io&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中第二个仓库必须是 public。这点很重要，因为用户站点型的 GitHub Pages 最终要稳定公开访问，而公开仓库在这方面是最省心的。&lt;/p&gt;
&lt;h3&gt;准备好本地项目&lt;/h3&gt;
&lt;p&gt;你的博客项目至少要满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地 &lt;code&gt;pnpm build&lt;/code&gt; 能成功；&lt;/li&gt;
&lt;li&gt;项目已经支持 GitHub Pages 模式构建；&lt;/li&gt;
&lt;li&gt;构建产物输出到 &lt;code&gt;dist/&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;你知道源码仓库和发布仓库分别是谁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果这些还没完成，那建议先根据我的&lt;a href=&quot;https://xiaohei94.github.io/blog/blog-deploy/&quot;&gt;博客上手指南&lt;/a&gt;将本地构建打通，然后再继续下面的自动发布。因为 Actions 本质上也只是把本地的那套流程搬到了云端执行一遍。&lt;/p&gt;
&lt;h2&gt;Part 3：生成 Deploy Key&lt;/h2&gt;
&lt;p&gt;这一步十分的关键，也是整个双仓发布里最容易让人卡住的点，但原理其实并不复杂。&lt;/p&gt;
&lt;p&gt;我们需要一把“钥匙”，让私有源码仓库里的 CI 可以合法地向公开发布仓库 push 内容。这把钥匙就是 SSH Deploy Key。听起来很复杂，但是不要怕，让我step by step的教你。&lt;/p&gt;
&lt;h3&gt;在本地生成密钥对&lt;/h3&gt;
&lt;p&gt;在你本地任意目录执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t ed25519 -C &quot;pages-deploy&quot; -f pages_deploy_key -N &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完成后会生成两个文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pages_deploy_key&lt;/code&gt;：私钥，绝对不能泄露&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pages_deploy_key.pub&lt;/code&gt;：公钥，可以交给 GitHub 仓库配置&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;把公钥和私钥分别放到正确的位置&lt;/h3&gt;
&lt;p&gt;这一步最容易搞错的，不是操作本身，而是“到底哪个钥匙要放在哪个仓库里”。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;公钥放到公开发布仓库（允许写入）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;打开：&lt;code&gt;Settings&lt;/code&gt; to &lt;code&gt;Deploy keys&lt;/code&gt; to &lt;code&gt;Add deploy key&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后填写：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Title&lt;/code&gt;：随意，例如 &lt;code&gt;deploy-from-Others&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Key&lt;/code&gt;：粘贴 &lt;code&gt;pages_deploy_key.pub&lt;/code&gt; 的全部内容&lt;/li&gt;
&lt;li&gt;勾选 &lt;code&gt;Allow write access&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一步的意义非常明确：允许 CI 用这把 key 向公开仓库推送构建产物。
&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;私钥放到私有源码仓库的 Actions Secret&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;打开：&lt;code&gt;Settings&lt;/code&gt; to &lt;code&gt;Secrets and variables&lt;/code&gt; to &lt;code&gt;Actions&lt;/code&gt; to &lt;code&gt;New repository secret&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;填写：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Name&lt;/code&gt;：&lt;code&gt;PAGES_DEPLOY_KEY&lt;/code&gt;（名称必须是这个）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Value&lt;/code&gt;：粘贴 &lt;code&gt;pages_deploy_key&lt;/code&gt; 私钥全文（包含 BEGIN / END）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Part 4：启用 GitHub Pages&lt;/h2&gt;
&lt;p&gt;到了这一步，发布仓库其实已经具备“被推送内容”的能力了。接下来要做的是告诉 GitHub Pages：你应该从哪里读取这些静态文件。&lt;/p&gt;
&lt;p&gt;进入仓库：&lt;code&gt;[用户名]/[用户名].github.io&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;打开：&lt;code&gt;Settings&lt;/code&gt; to &lt;code&gt;Pages&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Source&lt;/code&gt;：&lt;code&gt;Deploy from a branch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Branch&lt;/code&gt;：&lt;code&gt;main&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Folder&lt;/code&gt;：&lt;code&gt;/ (root)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保存之后，正常情况下 Pages 的地址会是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://[用户名].github.io/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Part 5：源码仓库里的自动发布&lt;/h2&gt;
&lt;p&gt;你当前的源码仓库里，实际上已经具备这套自动发布流程了。也就是说 workflow 不是从零开始设计的，而是已经被改造成下面这条链路：&lt;/p&gt;
&lt;h3&gt;自动发布链路&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;你在私有源码仓库提交并 push&lt;/li&gt;
&lt;li&gt;GitHub Actions 被触发&lt;/li&gt;
&lt;li&gt;Actions 以 &lt;code&gt;DEPLOYMENT_PLATFORM=github&lt;/code&gt; 进行静态构建&lt;/li&gt;
&lt;li&gt;构建产物输出到 &lt;code&gt;dist/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;workflow 使用 Deploy Key 把 &lt;code&gt;dist/&lt;/code&gt; 推送到 &lt;code&gt;[用户名]/[用户名].github.io&lt;/code&gt; 的 &lt;code&gt;main&lt;/code&gt; 分支根目录&lt;/li&gt;
&lt;li&gt;GitHub Pages 从根目录读取静态文件并刷新站点&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，就实现了 GitHub Pages + Vercel 双平台的个人博客部署，你可以根据他们的网站分别尝试进行打开，就好好欣赏你的劳动成果吧，我相信它会让你感到有趣并有成就感的！当然，接下来对博客的内容的构建与界面的搭建就要靠你自己辛勤耕耘了，坚持把它打造成有你自己风格的一块天地吧！&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;当你真的把这套流程走通之后，会发现 GitHub Pages 并没有想象中那样复杂。它真正复杂的地方，不在于某一条命令，而在于第一次建立完整心智模型的时候：谁负责构建、谁负责发布、谁负责公开访问、谁握着写权限。&lt;/p&gt;
&lt;p&gt;一旦这个模型搭起来，后面的事情反而会变得非常顺手，你只需要像平常一样写文章、改配置、提交代码，剩下的构建与发布都交给 Actions 自动完成。&lt;/p&gt;
&lt;p&gt;更重要的是，这套双仓方案并不只适用于博客。任何走静态构建路线的网站——产品主页、作品集、文档站、个人简历——其实都可以复用这一套思路。&lt;/p&gt;
&lt;p&gt;把源码留给自己，把成果交给世界。这就是这套发布方式最让我喜欢的地方。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CCDHr-wM.jpg"/><enclosure url="/_astro/heroimage.CCDHr-wM.jpg"/></item><item><title>博客上手指南</title><link>https://xiaohei-blog.vercel.app/blog/blog-deploy</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/blog-deploy</guid><description>一个比较详细的博客部署指南，写给我自己，也写给你。</description><pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { StepIndent } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在这个数字时代，我们习惯于在社交媒体上分享生活，在简历上罗列技能。但这些平台终究是别人的空间，充满了格式化的限制和信息的喧嚣。你是否想过，拥有一个完全由你掌控的、独特的线上空间？&lt;/p&gt;
&lt;p&gt;很多人可能会觉得，搭建一个看起来很炫酷的网站是件非常复杂和昂贵的事情，需要学习 HTML/CSS/JS 全家桶，还要懂后端、数据库和服务器运维。但事实是，&lt;strong&gt;借助现代化的工具，这一切比你想象的要简单得多，甚至可以完全免费&lt;/strong&gt;。本教程将作为一个完整的指南，带你从零开始，尽可能一步步搭建并部署一个高性能、易于维护的个人网站。即使你没有任何编程基础，只要跟着步骤走，也能拥有一个属于自己的主页。但是这并不意味着你不需要任何的技术积累，当然就像我对这篇博客的介绍一样，我会尽可能的从新手的角度写的详细一些，感谢那些开源的贡献者，是他们帮助了我，我也将这份精神传递下去来帮助更多的人。&lt;/p&gt;
&lt;h2&gt;Part 1: 现代网站搭建的基本逻辑&lt;/h2&gt;
&lt;p&gt;就像上学时学习新课程一样，在实际动手解题之前，我们都需要了解一下其中的概念和其背后的逻辑，正所谓基础不牢，地动山摇。那么在动手之前，我们先花几分钟了解一些核心概念，这会让你在后续操作中更加得心应手。&lt;/p&gt;
&lt;h3&gt;什么是静态网站？&lt;/h3&gt;
&lt;p&gt;你可能听过前端、后端、数据库这些词。简单来说，后端和数据库负责处理动态的、需要实时交互的数据（比如用户登录、评论提交）。而很多个人主页和博客的核心内容——文章、图片、个人介绍——并不会频繁变动。&lt;/p&gt;
&lt;p&gt;静态网站（Static Website）的核心思想就是，把这些内容提前生成为标准的 HTML、CSS 和 JavaScript 文件。当用户访问时，服务器直接把这些现成的文件发送给用户的浏览器，无需任何服务器端的动态计算。&lt;/p&gt;
&lt;h3&gt;为什么选择静态网站搭建个人主页？&lt;/h3&gt;
&lt;p&gt;又一个新的问题，当然答案我也不太懂，就听听AI大人怎么说吧。对于个人项目，静态网站是近乎完美的选择，因为它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极致的速度&lt;/strong&gt;：用户直接下载成品文件，没有服务器处理和数据库查询的等待，加载速度飞快。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更高的安全性&lt;/strong&gt;：没有后端和数据库，大大减少了被攻击的风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极低的成本&lt;/strong&gt;：静态文件托管的费用非常低，甚至有大量优秀的平台提供免费托管服务（比如我们后面要用的 Vercel）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简单的部署&lt;/strong&gt;：你只需要把生成好的文件上传到任何一个能放文件的地方，网站就能访问了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见的诸如 Github Pages 或者 Vercel 等平台，都是支持部署静态网页的，因此我们只需要将网页的构建产物上传到这些平台，就可以实现网页的部署。&lt;/p&gt;
&lt;h3&gt;拥抱现代前端框架&lt;/h3&gt;
&lt;p&gt;现代前端框架（如 &lt;strong&gt;Astro, Vue, React&lt;/strong&gt;）允许开发者用更高效、更模块化的方式来组织代码（比如写 Markdown 文章，或者用组件拼装页面）。虽然你在开发时写的是&lt;code&gt;.jsx&lt;/code&gt;、&lt;code&gt;.astro&lt;/code&gt;、&lt;code&gt;.vue&lt;/code&gt;等文件，但这些框架最终会通过构建工具（如 &lt;strong&gt;Vite、Webpack、Rollup&lt;/strong&gt; 等）将你的代码转换为浏览器可以识别的 &lt;strong&gt;HTML、CSS 和 JavaScript&lt;/strong&gt; 文件。这个构建过程就像“翻译”一样，把开发者写的高层代码翻译成浏览器能“听懂”的语言。&lt;/p&gt;
&lt;p&gt;而更进一步，无数的开发者为了便利博客等网站的构建，在这些现代框架的基础上构建了模板。这意味着你无需从零开始，只需要找到一个你喜欢的&lt;strong&gt;模板&lt;/strong&gt;，在作者预设好的框架里，像填写个人信息、写文章一样去填充内容即可。模板已经帮你处理了99%的复杂技术细节。剩下的内容就当然要自己写了，写博客却是挺磨砺一个人的心性的，尤其是现在生成式AI这么方便的情况下，我们需要为自己找一个能让自己坚持下去的信念，我想我的信念就是通过写博客来拯救我的分享欲吧。&lt;/p&gt;
&lt;h2&gt;Part 2: 动手！从零到一的建站实战&lt;/h2&gt;
&lt;p&gt;ok，理论终于讲完了，让我们卷起袖子开始实战！整个过程分为七个步骤。&lt;/p&gt;
&lt;p&gt;跟上脚步，顺着这些步骤，你可以搭建一个自己的网页，在本地进行编辑并且可以上传到云上，自动部署，同时绑定自己的域名（或者使用服务商提供的免费的域名）。&lt;/p&gt;
&lt;p&gt;对于完全的萌新来说，下面的步骤可能因为涉及了你没有接触的知识而显得有些晦涩。不用担心，先不要理解，而是跟着步骤走，先搭建起来，等到步骤三之后，你已经可以获得一个可以访问的网站了，收获一些正反馈，再沉下心继续走。&lt;/p&gt;
&lt;h3&gt;步骤一：准备本地开发环境 (Node.js)&lt;/h3&gt;
&lt;p&gt;要运行现代前端模板，在的电脑需要一个叫做 &lt;strong&gt;Node.js&lt;/strong&gt; 的环境，它是整个 JavaScript 工具链的基础。虽然其实在本地直接写文本并且上传也不失为一种办法，但是假如想要实时获得你修改后的内容的反馈，还是需要一个本地环境。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装 Node.js&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fnodejs.BRI-EfCa.png&amp;#x26;w=1375&amp;#x26;h=805&amp;#x26;f=webp&quot; alt=&quot;Node.js 安装界面&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ubuntu&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update
sudo apt install -y nodejs npm
# 推荐使用 n 来管理 Node.js 版本，可以轻松切换到最新稳定版
sudo npm install -g n
sudo n stable
hash -r # 刷新 shell 缓存，让新版本生效
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;验证安装并安装pnpm&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;node -v
npm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能看到版本号，说明 Node.js 和它的包管理器 &lt;code&gt;npm&lt;/code&gt; 已经就绪。我们推荐使用一个更现代、更快速的包管理器 &lt;code&gt;pnpm&lt;/code&gt; 。同时，对于一些框架，也推荐使用 &lt;code&gt;bun&lt;/code&gt; 来安装依赖。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm install -g pnpm
npm install -g bun
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤二：选择并下载一个模板&lt;/h3&gt;
&lt;p&gt;我们不需要从零开始设计。网上有海量的优秀开源模板。你可以从这些博客&lt;a href=&quot;https://astro.js.cn/themes/&quot;&gt;主题网站&lt;/a&gt;中挑选一个你喜欢的，或者在 GitHub 上搜索“astro theme”, “vue portfolio”等关键词。&lt;/p&gt;
&lt;p&gt;找到喜欢的模板之后可以直接 Fork 到你的 Github 账号中，之后在本地克隆到你的电脑上并且进行编辑。（关于 Git 的教程以及 Github 的教程将来会单独写 Blog，暂时本博客面向有 Git 以及 Github 基础的读者）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Ffork.DTnXliXJ.png&amp;#x26;w=1815&amp;#x26;h=914&amp;#x26;f=webp&quot; alt=&quot;github的fork&quot;&gt;&lt;/p&gt;
&lt;h3&gt;步骤三：在本地运行你的网站&lt;/h3&gt;
&lt;p&gt;在上一步，将blog的源程序clone到我们自己的电脑之后，你的网站代码就已经在你的电脑里了。接下来我们就用几个简单的命令让它动起来。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;进入项目目录&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd path/to/your/project
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;安装依赖（&lt;code&gt;install&lt;/code&gt;）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;如果项目推荐使用&lt;code&gt;pnpm&lt;/code&gt;(根目录有&lt;code&gt;pnpm-lock.yaml&lt;/code&gt;文件)，执行：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm install
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果项目推荐使用&lt;code&gt;bun&lt;/code&gt;(根目录有&lt;code&gt;bun.lockb&lt;/code&gt;文件)，执行：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun install
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果都没有，使用&lt;code&gt;npm&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令会自动下载所有依赖项到&lt;code&gt;node_modules&lt;/code&gt;文件夹中,如果clone下来的项目中已有&lt;code&gt;node_modules&lt;/code&gt;文件夹，那么最好先删除了这个文件夹,再执行上面的命令。
&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;启动开发服务器（&lt;code&gt;dev&lt;/code&gt;）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;执行命令之后，终端会显示一个本地网址，通常是&lt;code&gt;http://localhost:4321&lt;/code&gt;或类似的地址。可以将他复制下来并在浏览器中打开它，恭喜你，你的网站已经在本地成功运行了！&lt;/p&gt;
&lt;p&gt;更棒的是，它支持&lt;strong&gt;热更新（HMR）&lt;/strong&gt;。现在你修改任何源文件（比如一篇文章），浏览器里的页面都会自动刷新，并实时展示你的改动。
&lt;/p&gt;
&lt;h3&gt;步骤四：个性化你的内容&lt;/h3&gt;
&lt;p&gt;本地网站跑起来了，现在是把它变成你自己的东西的时候了。作为模板使用者，你的工作非常简单，主要集中在两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;修改配置文件&lt;/strong&gt;：在项目根目录找到 &lt;code&gt;astro.config.mjs&lt;/code&gt; , &lt;code&gt;_config.yml&lt;/code&gt; 或类似名字的配置文件。打开它，把里面的网站标题、作者名、社交链接等改成你自己的信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理内容文件&lt;/strong&gt;：对于博客，通常会有一个 &lt;code&gt;src/content/blog/&lt;/code&gt; 或 &lt;code&gt;posts/&lt;/code&gt; 目录。你只需要在这个目录里添加、修改或删除 Markdown (&lt;code&gt;.md&lt;/code&gt;或&lt;code&gt;.mdx&lt;/code&gt;) 文件，网站的文章列表和页面就会自动更新。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;步骤五：将你的成果推送到 GitHub&lt;/h3&gt;
&lt;p&gt;在本地进行个性化修改满意后，我们需要把代码上传到 GitHub。这不仅是备份的好习惯，更是我们实现自动化部署的关键一步。&lt;/p&gt;
&lt;p&gt;假如说你直接&lt;code&gt;git clone&lt;/code&gt;了源码，可以直接删掉&lt;code&gt;.git&lt;/code&gt;文件夹，然后执行&lt;code&gt;git init&lt;/code&gt;初始化一个新的仓库。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在&lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt;官网上创建一个新的空仓库（New Repository）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fgithubku.Cuzf-sDW.png&amp;#x26;w=1830&amp;#x26;h=919&amp;#x26;f=webp&quot; alt=&quot;github建库&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;根据 GitHub 页面的提示，在你本地的项目文件夹中，通过终端执行以下命令，将代码推送到你的新仓库：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git init
git add .
git commit -m &quot;Initial commit: My personal website setup&quot;
git branch -M main
git remote add origin [你的仓库HTTPS或SSH地址]
git push -u origin main   # 或 git push --set-upstream origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;假如说本来就是从自己的仓库中克隆的，则可以直接执行 &lt;code&gt;git push&lt;/code&gt; 将代码推送到远程仓库。&lt;/p&gt;
&lt;h3&gt;步骤六：使用 Vercel 一键部署&lt;/h3&gt;
&lt;p&gt;代码已上传到 GitHub上了，接下来是最激动人心的一步：让全世界都能访问你的网站。我们将使用 Vercel 这个强大的免费平台。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们暂时先不介绍GitHub Pages的配置方法&lt;/strong&gt;，我们先讲Vercel的配置方法，让我们的体系更完整呢。还有一个原因是GitHub Pages虽然免费，但它需要你手动配置构建流程（GitHub Actions），且自定义域名等操作较为繁琐。而Vercel 真正做到了“零配置”自动化部署。（不过后续我也会介绍到的，实现GitHub Pages与Vercel的双平台部署）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用 GitHub 登录 Vercel&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./image/vercel1.webp&quot; alt=&quot;vercel登陆&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;导入我们的项目&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fvercel2.kZSu_cdW.png&amp;#x26;w=1585&amp;#x26;h=750&amp;#x26;f=webp&quot; alt=&quot;vercel导入&quot;&gt;&lt;/p&gt;
&lt;p&gt;Vercel 会列出你的 GitHub 仓库，找到你刚刚创建的网站仓库，点击旁边的 &lt;code&gt;Import&lt;/code&gt; 按钮。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fvercel3.-pqDdZqj.png&amp;#x26;w=1500&amp;#x26;h=710&amp;#x26;f=webp&quot; alt=&quot;vercel导入2&quot;&gt;
&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;部署！ Vercel 会自动识别你的项目是什么框架（Astro, Next.js, etc.），并帮你填好所有构建设置。你什么都不用改，直接点击&lt;code&gt;Deploy&lt;/code&gt;按钮。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fvercel4.DEE20cd5.png&amp;#x26;w=881&amp;#x26;h=896&amp;#x26;f=webp&quot; alt=&quot;vercel部署&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fvercel5.d8LT06hq.png&amp;#x26;w=1545&amp;#x26;h=770&amp;#x26;f=webp&quot; alt=&quot;vercel展示&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个时候你已经可以把这个链接分享给你的朋友了。当然，你也可以选择绑定自己的域名，之后在 Vercel 的设置中进行配置。
&lt;/p&gt;
&lt;p&gt;从此以后，你只需要在本地修改代码，然后&lt;code&gt;git push&lt;/code&gt;到 GitHub，Vercel 就会自动拉取最新代码，重新构建和部署你的网站。完全自动化！&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;恭喜你！从一个想法开始，到现在拥有一个简单部署的个人网站，你已经走完了现代 Web 开发最主流、最高效的一条路径。当然，你使用体验之后就会感觉到，这里并不是终点，如果想让我们的个人站功能更加的全面我们还有一些路要走，如果你想继续深入的进行探索，跟上我的脚步，我们继续！&lt;/p&gt;
&lt;p&gt;同时，你会发现，这个流程的核心是“关注内容，而非工具”。你未来的大部分时间，都将花在写文章、更新作品这些创造性的工作上，而不是和服务器配置作斗争。&lt;/p&gt;
&lt;p&gt;更重要的是，今天你所学的这套流程是高度可扩展的。它不仅仅适用于个人博客，任何拥有类似构建方式的静态网站模板——无论是产品展示页、在线简历还是文档网站——都可以用完全相同的方法进行部署。&lt;/p&gt;
&lt;p&gt;从这里开始。尽情创造吧！&lt;/p&gt;
&lt;p&gt;以及，假如教程帮到了你，你也成功搭建了自己的网站，非常欢迎和我交换友链（link）。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CvRCrjxw.jpg"/><enclosure url="/_astro/heroimage.CvRCrjxw.jpg"/></item><item><title>Git进阶 · 自定义</title><link>https://xiaohei-blog.vercel.app/blog/git-4</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/git-4</guid><description>Git 的进阶使用教程</description><pubDate>Fri, 19 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;定义一个适合自己风格的 Git，我想这应该没人能拒绝吧，再来试试吧。&lt;/p&gt;
&lt;h2&gt;忽略特殊文件&lt;/h2&gt;
&lt;p&gt;有些时候，你必须把某些文件放到 Git 工作目录中，但又不能提交它们，比如保存了数据库密码的配置文件啦等等。好在 Git 考虑到了大家的感受，这个问题解决起来也很简单，在 Git 工作区的根目录下创建一个特殊的 &lt;code&gt;.gitignore&lt;/code&gt; 文件，然后把要忽略的文件名填进去，Git 就会自动忽略这些文件。&lt;/p&gt;
&lt;p&gt;很便利的是，我们不需要从头写 &lt;code&gt;.gitignore&lt;/code&gt; 文件，GitHub 已经为我们准备了各种配置文件，只需要组合一下就可以使用了。所有配置文件可以直接在 &lt;a href=&quot;https://github.com/github/gitignore&quot;&gt;gitignore&lt;/a&gt;上浏览。&lt;/p&gt;
&lt;p&gt;忽略文件的原则是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;忽略操作系统自动生成的文件，比如缩略图等。&lt;/li&gt;
&lt;li&gt;忽略编译生成的中间文件、可执行文件等，也就是如果一个文件是通过另一个文件自动生成的，那自动生成的文件就没必要放进版本库，比如 Java 编译产生的 &lt;code&gt;.class&lt;/code&gt; 文件。&lt;/li&gt;
&lt;li&gt;忽略你自己的带有敏感信息的配置文件，比如存放口令的配置文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有些时候，你想添加一个文件到 Git，但发现添加不了，原因是这个文件被 &lt;code&gt;.gitignore&lt;/code&gt; 忽略了。此时，如果你确实想添加该文件，可以用 &lt;code&gt;-f&lt;/code&gt; 强制添加到 Git。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add -f App.class   # git add -f &amp;#x3C;filename&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者你发现，可能是 &lt;code&gt;.gitignore&lt;/code&gt; 写得有问题，需要找出来到底哪个规则写错了，可以用 &lt;code&gt;git check-ignore&lt;/code&gt; 命令检查。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git check-ignore -v App.class
.gitignore:3:*.class	App.class
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git 会告诉我们，&lt;code&gt;.gitignore&lt;/code&gt; 的第 3 行规则忽略了该文件，于是我们就可以知道应该修订哪个规则。但有些时候，当我们编写了规则排除了部分文件时，我们发现 &lt;code&gt;.*&lt;/code&gt; 这个规则把 &lt;code&gt;.gitignore&lt;/code&gt; 也排除了，并且 &lt;code&gt;App.class&lt;/code&gt; 需要被添加到版本库，但是被 &lt;code&gt;*.class&lt;/code&gt; 规则排除了。虽然可以用 &lt;code&gt;git add -f&lt;/code&gt; 强制添加进去，但有强迫症的童鞋还是希望不要破坏 &lt;code&gt;.gitignore&lt;/code&gt; 规则，这个时候，可以添加两条例外规则。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 排除所有.开头的隐藏文件:
.*
# 排除所有.class文件:
*.class

# 不排除.gitignore和App.class:
!.gitignore
!App.class
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把指定文件排除在 &lt;code&gt;.gitignore&lt;/code&gt; 规则外的写法就是 &lt;code&gt;!+文件名&lt;/code&gt;，所以，只需把例外文件添加进去即可。同样，我们可以通过&lt;a href=&quot;https://gitignore.puppylab.org/&quot;&gt;GitIgnore Online Generator&lt;/a&gt;在线生成 &lt;code&gt;.gitignore&lt;/code&gt; 文件并直接下载。&lt;/p&gt;
&lt;p&gt;最后一个问题，&lt;code&gt;.gitignore&lt;/code&gt;文件放哪？答案是放 Git 仓库根目录下，但其实一个 Git 仓库也可以有多个 &lt;code&gt;.gitignore&lt;/code&gt; 文件，&lt;code&gt;.gitignore&lt;/code&gt; 文件放在哪个目录下，就对哪个目录（包括子目录）起作用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Ftree.DfFfAHgN.png&amp;#x26;w=530&amp;#x26;h=171&amp;#x26;f=webp&quot; alt=&quot;tree&quot;&gt;&lt;/p&gt;
&lt;h2&gt;搭建 Git 服务器&lt;/h2&gt;
&lt;p&gt;GitHub 就是一个免费托管开源代码的远程仓库。但是对于某些视源代码如生命的商业公司来说，既不想公开源代码，又舍不得给 GitHub 交保护费，那就只能自己搭建一台 Git 服务器作为私有仓库使用。搭建 Git 服务器需要准备一台运行 Linux 的机器，强烈推荐用 Ubuntu 或 Debian，这样，通过几条简单的apt命令就可以完成安装。假设你已经有 sudo 权限的 Linux 用户账号，下面，正式开始安装。&lt;/p&gt;
&lt;p&gt;第一步，安装 git：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo apt install git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二步，创建一个 git 用户，用来运行 git 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo adduser git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三步，创建证书登录：&lt;/p&gt;
&lt;p&gt;收集所有需要登录的用户的公钥，就是他们自己的 &lt;code&gt;id_rsa.pub&lt;/code&gt; 文件，把所有公钥导入到 &lt;code&gt;/home/git/.ssh/authorized_keys&lt;/code&gt; 文件里，一行一个。&lt;/p&gt;
&lt;p&gt;第四步，初始化 Git 仓库：&lt;/p&gt;
&lt;p&gt;先选定一个目录作为 Git 仓库，假定是 &lt;code&gt;/srv/sample.git&lt;/code&gt;，在 &lt;code&gt;/srv&lt;/code&gt; 目录下输入命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo git init --bare sample.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git 就会创建一个裸仓库，裸仓库没有工作区，因为服务器上的 Git 仓库纯粹是为了共享，所以不让用户直接登录到服务器上去改工作区，并且服务器上的 Git 仓库通常都以 &lt;code&gt;.git&lt;/code&gt; 结尾。然后，把 owner 改为 &lt;code&gt;git&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo chown -R git:git sample.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第五步，禁用 shell 登录：&lt;/p&gt;
&lt;p&gt;出于安全考虑，第二步创建的 &lt;code&gt;git&lt;/code&gt; 用户不允许登录 shell，这可以通过编辑 &lt;code&gt;/etc/passwd&lt;/code&gt; 文件完成。找到类似下面的一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git:x:1001:1001:,,,:/home/git:/bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，&lt;code&gt;git&lt;/code&gt; 用户可以正常通过 ssh 使用 git，但无法登录 shell，因为我们为 &lt;code&gt;git&lt;/code&gt; 用户指定的 &lt;code&gt;git-shell&lt;/code&gt; 每次一登录就自动退出。&lt;/p&gt;
&lt;p&gt;第六步，克隆远程仓库：&lt;/p&gt;
&lt;p&gt;现在，可以通过 &lt;code&gt;git clone&lt;/code&gt; 命令克隆远程仓库了，在各自的电脑上运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git clone git@server:/srv/sample.git
Cloning into &apos;sample&apos;...
warning: You appear to have cloned an empty repository.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;剩下的推送就简单了。&lt;/p&gt;
&lt;h3&gt;管理公钥&lt;/h3&gt;
&lt;p&gt;如果团队很小，把每个人的公钥收集起来放到服务器的 &lt;code&gt;/home/git/.ssh/authorized_keys&lt;/code&gt; 文件里就是可行的。如果团队有几百号人，就没法这么玩了，这时，可以用 &lt;a href=&quot;https://github.com/res0nat0r/gitosis&quot;&gt;Gitosis&lt;/a&gt;来管理公钥。这里我们不介绍怎么玩 Gitosis 了，因为我也不会，暂时就留在等我可以管理上百人团队时，我再来补课吧。&lt;/p&gt;
&lt;h3&gt;管理权限&lt;/h3&gt;
&lt;p&gt;有很多不但视源代码如生命，而且视员工为窃贼的公司/团队/课题组，会在版本控制系统里设置一套完善的权限控制，每个人是否有读写权限会精确到每个分支甚至每个目录下。因为 Git 是为 Linux 源代码托管而开发的，所以 Git 也继承了开源社区的精神，不支持权限控制。不过，因为 Git 支持钩子（hook），所以，可以在服务器端编写一系列脚本来控制提交等操作，达到权限控制的目的。&lt;a href=&quot;https://github.com/sitaramc/gitolite&quot;&gt;Gitolite&lt;/a&gt;就是这个工具。这里我也不介绍Gitolite了，因为目前我就孤身一个，暂时还用不到。&lt;/p&gt;
&lt;h2&gt;补充&lt;/h2&gt;
&lt;p&gt;在我尝试往自己搭建的 Git 服务器仓库中 push 相关项目程序时，出现了下面一些问题，可以&lt;a href=&quot;https://segmentfault.com/q/1010000011416500&quot;&gt;前去了解&lt;/a&gt;和&lt;a href=&quot;https://blog.csdn.net/ZXGuang521/article/details/109804235&quot;&gt;借鉴&lt;/a&gt;。查找了几个大佬的博客回答，现在将相关的解决方法记录在下。整体的一个我理解的逻辑如下。&lt;/p&gt;
&lt;p&gt;服务器端存放的叫裸库(bare)，你可以认为就是你的仓库的 &lt;code&gt;.git/&lt;/code&gt; 目录下面的那些东西，不包含工作区(working space)。解决办法：需要在 &lt;code&gt;demo.git&lt;/code&gt; 里的 &lt;code&gt;hooks&lt;/code&gt; 里创建一个 &lt;code&gt;post-receive&lt;/code&gt; 指定项目文件目录，和 git 仓库地址。例如：我在 &lt;code&gt;/home/git/&lt;/code&gt; 下面建的裸库 &lt;code&gt;tmp.git&lt;/code&gt;，去 &lt;code&gt;tmp.git/hook&lt;/code&gt; 里面设置项目文件目录比如设置为 &lt;code&gt;home/git&lt;/code&gt; ,然后你再重新 push 文件就可以在服务器 &lt;code&gt;/home/git/&lt;/code&gt; 下面看到你 push 的文件了。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.DexWCesP.jpg"/><enclosure url="/_astro/heroimage.DexWCesP.jpg"/></item><item><title>Git进阶 · 标签篇</title><link>https://xiaohei-blog.vercel.app/blog/git-3</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/git-3</guid><description>Git 的进阶使用教程</description><pubDate>Thu, 18 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;接上一篇内容，我们再来看看不一样的 commit。&lt;/p&gt;
&lt;h2&gt;标签管理&lt;/h2&gt;
&lt;p&gt;发布一个版本时，我们通常先在版本库中打一个标签（tag），这样，就唯一确定了打标签时刻的版本。将来无论什么时候，取某个标签的版本，就是把那个打标签的时刻的历史版本取出来。所以，标签也是版本库的一个快照。Git 的标签虽然是版本库的快照，但其实它就是指向某个 commit 的指针（跟分支很像对不对？但是分支可以移动，标签不能移动），所以，创建和删除标签都是瞬间完成的。&lt;/p&gt;
&lt;p&gt;Git 有 commit，为什么还要引入 tag？就比如下面这个场景，“请把上周一的那个版本打包发布，commit 号是 6a5819e...”，“一串乱七八糟的数字不好找！”&lt;/p&gt;
&lt;p&gt;如果换一个办法：“请把上周一的那个版本打包发布，版本号是 v1.2”，“好的，按照 tag v1.2 查找 commit 就行！”&lt;/p&gt;
&lt;p&gt;所以，tag 就是一个让人容易记住的有意义的名字，它跟某个 commit 绑在一起。即标签就是省略复杂 commit 号。&lt;/p&gt;
&lt;h3&gt;创建标签&lt;/h3&gt;
&lt;p&gt;在 Git 中打标签非常简单，首先，切换到需要打标签的分支上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git branch
* dev
  master
$ git checkout master
Switched to branch &apos;master&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，敲命令 &lt;code&gt;git tag &amp;#x3C;name&gt;&lt;/code&gt; 就可以打一个新标签：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git tag v1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以用命令 &lt;code&gt;git tag&lt;/code&gt; 查看所有标签：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git tag
v1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认标签是打在最新提交的 commit 上的。有时候，如果忘了打标签，比如，现在已经是周五了，但应该在周一打的标签没有打，怎么办？方法是找到历史提交的 commit id ，然后打上就可以了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git log --pretty=oneline --abbrev-commit
12a631b (HEAD -&gt; master, tag: v1.0, origin/master) merged bug fix 101
4c805e2 fix bug 101
e1e9c68 merge with no-ff
f52c633 add merge
cf810e4 conflict fixed
5dc6824 &amp;#x26; simple
14096d0 AND simple
b17d20e branch test
d46f35e remove test.txt
b84166e add test.txt
519219b git tracks changes
e43a48b understand how stage works
1094adb append GPL
e475afc add distributed
eaadf4e wrote a readme file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比方说要对 &lt;code&gt;add merge&lt;/code&gt; 这次提交打标签，它对应的 commit id 是 &lt;code&gt;f52c633&lt;/code&gt;，敲入命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git tag v0.9 f52c633
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时再用 &lt;code&gt;git tag&lt;/code&gt; 查看现有的标签你会发现，现在出现了 &lt;code&gt;v0.9&lt;/code&gt; 与 &lt;code&gt;v1.0&lt;/code&gt; 两个 tag。注意，标签不是按时间顺序列出，而是按字母排序的。可以用 &lt;code&gt;git show &amp;#x3C;tagname&gt;&lt;/code&gt; 查看标签信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git show v0.9
commit f52c63349bc3c1593499807e5c8e972b82c8f286 (tag: v0.9)
Author: Michael Liao &amp;#x3C;askxuefeng@gmail.com&gt;
Date:   Fri May 18 21:56:54 2018 +0800

    add merge

diff --git a/readme.txt b/readme.txt
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还可以创建带有说明的标签，用 &lt;code&gt;-a&lt;/code&gt; 指定标签名， &lt;code&gt;-m&lt;/code&gt; 指定说明文字：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git tag -a v0.1 -m &quot;version 0.1 released&quot; 1094adb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们再用命令 &lt;code&gt;git show &amp;#x3C;tagname&gt;&lt;/code&gt; 可以看到说明文字：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git show v0.1
tag v0.1
Tagger: Michael Liao &amp;#x3C;askxuefeng@gmail.com&gt;
Date:   Fri May 18 22:48:43 2018 +0800

version 0.1 released

commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (tag: v0.1)
Author: Michael Liao &amp;#x3C;askxuefeng@gmail.com&gt;
Date:   Fri May 18 21:06:15 2018 +0800

    append GPL

diff --git a/readme.txt b/readme.txt
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;操作标签&lt;/h3&gt;
&lt;p&gt;如果标签打错了，也可以删除：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git tag -d v0.1    # 命令 git tag -d &amp;#x3C;tagname&gt; 可以删除一个本地标签；
Deleted tag &apos;v0.1&apos; (was f15b0dd)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为创建的标签都只存储在本地，不会自动推送到远程。所以，打错的标签可以在本地安全删除。如果要推送某个标签到远程，使用命令 &lt;code&gt;git push origin &amp;#x3C;tagname&gt;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git push origin v1.0   # 命令 git push origin &amp;#x3C;tagname&gt; 可以推送一个本地标签；
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
 * [new tag]         v1.0 -&gt; v1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者，一次性推送全部尚未推送到远程的本地标签：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git push origin --tags      # 命令 git push origin --tags 可以推送全部未推送过的本地标签；
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
 * [new tag]         v0.9 -&gt; v0.9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果标签已经推送到远程，要删除远程标签就麻烦一点，先从本地删除：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git tag -d v0.9   # 删除本地标签
Deleted tag &apos;v0.9&apos; (was f52c633)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，从远程删除。删除命令也是 push，但是格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git push origin :refs/tags/v0.9    # 命令 git push origin :refs/tags/&amp;#x3C;tagname&gt; 可以删除一个远程标签。
To github.com:michaelliao/learngit.git
 - [deleted]         v0.9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要看看是否真的从远程库删除了标签，可以登陆 GitHub 查看。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.BmFlVG73.jpg"/><enclosure url="/_astro/heroimage.BmFlVG73.jpg"/></item><item><title>Git 进阶 · 分支篇</title><link>https://xiaohei-blog.vercel.app/blog/git-2</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/git-2</guid><description>Git 的进阶使用教程</description><pubDate>Wed, 17 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;这是对前篇没有展示完全的 git 内容的继续，是真正 git 的开始，继续来吧。&lt;/p&gt;
&lt;h2&gt;Git 分支管理&lt;/h2&gt;
&lt;p&gt;分支在实际中有什么用呢？假设你准备开发一个新功能，但是需要两周才能完成，第一周你写了 50% 的代码，如果立刻提交，由于代码还没写完，不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交，又存在丢失每天进度的巨大风险。&lt;/p&gt;
&lt;p&gt;现在有了分支，就不用怕了。你创建了一个属于你自己的分支，别人看不到，还继续在原来的分支上正常工作，而你在自己的分支上干活，想提交就提交，直到开发完毕后，再一次性合并到原来的分支上，这样，既安全，又不影响别人工作。&lt;/p&gt;
&lt;p&gt;Git 的分支是与众不同的，无论创建、切换和删除分支，Git 在 1 秒钟之内就能完成！无论你的版本库是 1 个文件还是 1 万个文件。&lt;/p&gt;
&lt;h3&gt;创建与合并分支&lt;/h3&gt;
&lt;p&gt;下面开始实战。首先，我们创建 &lt;code&gt;dev&lt;/code&gt; 分支，然后切换到 &lt;code&gt;dev&lt;/code&gt; 分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git checkout -b dev   # 创建并切换分支
Switched to a new branch &apos;dev&apos;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git checkout&lt;/code&gt; 命令加上 &lt;code&gt;-b&lt;/code&gt; 参数表示创建并切换，相当于以下两条命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git branch dev
$ git checkout dev
Switched to branch &apos;dev&apos;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，用 &lt;code&gt;git branch&lt;/code&gt; 命令查看当前分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git branch  # 查看当前分支
* dev
  master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git branch&lt;/code&gt; 命令会列出所有分支，当前分支前面会标一个 &lt;code&gt;*&lt;/code&gt; 号。然后，我们就可以在 &lt;code&gt;dev&lt;/code&gt; 分支上正常提交，比如对 &lt;code&gt;readme.txt&lt;/code&gt; 做个修改，加上一行文字后，然后进行提交：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add readme.txt 
$ git commit -m &quot;branch test&quot;
[dev b17d20e] branch test
 1 file changed, 1 insertion(+)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，&lt;code&gt;dev&lt;/code&gt; 分支的工作完成，我们就可以切换回 &lt;code&gt;master&lt;/code&gt; 分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git checkout master   # 切换分支
Switched to branch &apos;master&apos;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;切换回 &lt;code&gt;master&lt;/code&gt; 分支后，再查看一个 &lt;code&gt;readme.txt&lt;/code&gt; 文件，刚才添加的内容不见了！因为那个提交是在 &lt;code&gt;dev&lt;/code&gt; 分支上，而 &lt;code&gt;master&lt;/code&gt; 分支此刻的提交点并没有变：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fbranch.BpSdXzs4.png&amp;#x26;w=339&amp;#x26;h=281&amp;#x26;f=webp&quot; alt=&quot;branch&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在，我们把 &lt;code&gt;dev&lt;/code&gt; 分支的工作成果合并到 &lt;code&gt;master&lt;/code&gt; 分支上，使用命令 &lt;code&gt;git merge&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git merge dev   # 合并分支
Updating d46f35e..b17d20e
Fast-forward
 readme.txt | 1 +
 1 file changed, 1 insertion(+)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git merge&lt;/code&gt; 命令用于合并指定分支到当前分支。合并后，再查看 &lt;code&gt;readme.txt&lt;/code&gt; 的内容，就可以看到，和 &lt;code&gt;dev&lt;/code&gt; 分支的最新提交是完全一样的。注意到上面的 Fast-forward 信息，Git 告诉我们，这次合并是“快进模式”，也就是直接把 master 指向 &lt;code&gt;dev&lt;/code&gt; 的当前提交，所以合并速度非常快。当然，也不是每次合并都能 Fast-forward ，我们后面会讲其他方式的合并。合并完成后，就可以放心地删除 &lt;code&gt;dev&lt;/code&gt; 分支了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git branch -d dev   # 删除分支
Deleted branch dev (was b17d20e).

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除后，使用 &lt;code&gt;git branch&lt;/code&gt; 查看分支，就只剩下 &lt;code&gt;master&lt;/code&gt; 分支了&lt;/p&gt;
&lt;h4&gt;switch&lt;/h4&gt;
&lt;p&gt;我们注意到切换分支使用 &lt;code&gt;git checkout &amp;#x3C;branch&gt;&lt;/code&gt; ，而前面讲过的撤销修改则是 &lt;code&gt;git checkout -- &amp;#x3C;file&gt;&lt;/code&gt; ，同一个命令，有两种作用，确实有点令人迷惑。实际上，切换分支这个动作，用 &lt;code&gt;switch&lt;/code&gt; 更科学。因此，最新版本的 Git 提供了新的 &lt;code&gt;git switch&lt;/code&gt; 命令来切换分支。创建并切换到新的 dev 分支，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git switch -c dev

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接切换到已有的 master 分支，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git switch master

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用新的 &lt;code&gt;git switch&lt;/code&gt; 命令，比 &lt;code&gt;git checkout&lt;/code&gt; 要更容易理解。&lt;/p&gt;
&lt;h3&gt;解决冲突&lt;/h3&gt;
&lt;p&gt;人生不如意之事十之八九，合并分支往往也不是一帆风顺的。下面就让我们来实际演示一下，先准备新的 &lt;code&gt;feature1&lt;/code&gt; 分支，然后继续我们的新分支开发：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git switch -c feature1
Switched to a new branch &apos;feature1&apos;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后修改一个文件内容，例如，修改readme.txt最后一行，改为： &lt;code&gt;Creating a new branch is quick AND simple.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;之后在 &lt;code&gt;feature1&lt;/code&gt; 分支上提交：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add readme.txt

$ git commit -m &quot;AND simple&quot;
[feature1 14096d0] AND simple
 1 file changed, 1 insertion(+), 1 deletion(-)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;切换到 &lt;code&gt;master&lt;/code&gt; 分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git switch master
Switched to branch &apos;master&apos;
Your branch is ahead of &apos;origin/master&apos; by 1 commit.
  (use &quot;git push&quot; to publish your local commits)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，通过输出就可以看出，Git 还会自动提示我们当前 &lt;code&gt;master&lt;/code&gt; 分支比远程的 &lt;code&gt;master&lt;/code&gt; 分支要超前 1 个提交。但不要慌张，这并不是错误冲突。&lt;/p&gt;
&lt;p&gt;下面我们来看下面这种新的情况，如果此时在 &lt;code&gt;master&lt;/code&gt; 分支上把 &lt;code&gt;readme.txt&lt;/code&gt; 文件的最后一行改为：&lt;code&gt;Creating a new branch is quick &amp;#x26; simple.&lt;/code&gt;这时在 &lt;code&gt;master&lt;/code&gt; 分支下进行提交，会出现什么情况呢。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add readme.txt    # 在 master 分支下进行提交
$ git commit -m &quot;&amp;#x26; simple&quot;
[master 5dc6824] &amp;#x26; simple
 1 file changed, 1 insertion(+), 1 deletion(-)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;答案揭晓，现在，master分支和feature1分支各自都分别有新的提交，变成了这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fbranch1.DUAaYK1t.png&amp;#x26;w=365&amp;#x26;h=380&amp;#x26;f=webp&quot; alt=&quot;branch1&quot;&gt;&lt;/p&gt;
&lt;p&gt;这种情况下，Git 就无法执行“快速合并”命令 &lt;code&gt;git merge&lt;/code&gt;，只能试图把各自的修改合并起来，但这种合并就可能会有冲突，我们试试看：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出显示果然冲突了！Git 告诉我们，readme.txt 文件存在冲突，必须手动解决冲突后再提交。&lt;code&gt;git status&lt;/code&gt; 也可以告诉我们冲突的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git status
On branch master
Your branch is ahead of &apos;origin/master&apos; by 2 commits.
  (use &quot;git push&quot; to publish your local commits)

You have unmerged paths.
  (fix conflicts and run &quot;git commit&quot;)
  (use &quot;git merge --abort&quot; to abort the merge)

Unmerged paths:
  (use &quot;git add &amp;#x3C;file&gt;...&quot; to mark resolution)

	both modified:   readme.txt

no changes added to commit (use &quot;git add&quot; and/or &quot;git commit -a&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用编辑器 VS code 打开有问题冲突的文件 readme.txt，你会看到 Git 标记的冲突内容。我们可以直接查看 readme.txt 的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C; HEAD
Creating a new branch is quick &amp;#x26; simple.
=======
Creating a new branch is quick AND simple.
&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature1

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git 用 &lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&lt;/code&gt;，&lt;code&gt;=======&lt;/code&gt;，&lt;code&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&lt;/code&gt; 标记出不同分支的内容，我们根据项目的实际需要选择使用哪个修改后进行文件保存。之后即可进行再次提交。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add readme.txt 
$ git commit -m &quot;conflict fixed&quot;
[master cf810e4] conflict fixed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，&lt;code&gt;master&lt;/code&gt; 分支和 &lt;code&gt;feature1&lt;/code&gt; 分支变成了下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fbranch2.CeKNVuZL.png&amp;#x26;w=450&amp;#x26;h=381&amp;#x26;f=webp&quot; alt=&quot;branch2&quot;&gt;&lt;/p&gt;
&lt;p&gt;用带参数的 &lt;code&gt;git log&lt;/code&gt; 也可以看到分支的合并情况，或用 &lt;code&gt;git log --graph&lt;/code&gt; 命令可以看到分支合并图：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git log --graph --pretty=oneline --abbrev-commit
*   cf810e4 (HEAD -&gt; master) conflict fixed
|\  
| * 14096d0 (feature1) AND simple
* | 5dc6824 &amp;#x26; simple
|/  
* b17d20e branch test
* d46f35e (origin/master) remove test.txt
* b84166e add test.txt
* 519219b git tracks changes
* e43a48b understand how stage works
* 1094adb append GPL
* e475afc add distributed
* eaadf4e wrote a readme file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，分支使用完毕之后即可删除 feature1 分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git branch -d feature1
Deleted branch feature1 (was 14096d0).
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分支管理策略&lt;/h3&gt;
&lt;p&gt;通常，合并分支时，如果可能，Git 会用 &lt;code&gt;Fast forward&lt;/code&gt; 模式，但这种模式下，删除分支后，会丢掉分支信息。如果要强制禁用 &lt;code&gt;Fast forward&lt;/code&gt; 模式，Git 就会在 merge 时生成一个新的 commit，这样，从分支历史上就可以看出分支信息。下面我们实战一下 &lt;code&gt;--no-ff&lt;/code&gt; 方式的 &lt;code&gt;git merge&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先，仍然创建并切换 &lt;code&gt;dev&lt;/code&gt; 分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git switch -c dev
Switched to a new branch &apos;dev&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 readme.txt 文件，并提交一个新的 commit：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add readme.txt 
$ git commit -m &quot;add merge&quot;
[dev f52c633] add merge
 1 file changed, 1 insertion(+)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们切换回 &lt;code&gt;master&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git switch master
Switched to branch &apos;master&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;准备合并 &lt;code&gt;dev&lt;/code&gt; 分支，请注意 &lt;code&gt;--no-ff&lt;/code&gt; 参数，表示禁用 &lt;code&gt;Fast forward&lt;/code&gt;，因为本次合并要创建一个新的 commit，所以加上 &lt;code&gt;-m&lt;/code&gt; 参数，把 commit 描述写进去：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git merge --no-ff -m &quot;merge with no-ff&quot; dev
Merge made by the &apos;recursive&apos; strategy.
 readme.txt | 1 +
 1 file changed, 1 insertion(+)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;合并后，我们用 &lt;code&gt;git log&lt;/code&gt; 看看分支历史：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git log --graph --pretty=oneline --abbrev-commit
*   e1e9c68 (HEAD -&gt; master) merge with no-ff
|\  
| * f52c633 (dev) add merge
|/  
*   cf810e4 conflict fixed
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，不使用 Fast forward 模式，merge 后就像这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fbranch3.l9eTdk1a.png&amp;#x26;w=405&amp;#x26;h=385&amp;#x26;f=webp&quot; alt=&quot;branch3&quot;&gt;&lt;/p&gt;
&lt;h4&gt;分支策略&lt;/h4&gt;
&lt;p&gt;实际开发中，我们应该按照几个基本原则进行分支管理：&lt;/p&gt;
&lt;p&gt;首先，&lt;code&gt;master&lt;/code&gt; 分支应该是非常稳定的，也就是仅用来发布新版本，平时不能在上面干活；那在哪干活呢？干活都在 &lt;code&gt;dev&lt;/code&gt; 分支上，也就是说，&lt;code&gt;dev&lt;/code&gt; 分支是不稳定的，到某个时候，比如 1.0 版本发布时，再把 &lt;code&gt;dev&lt;/code&gt; 分支合并到 &lt;code&gt;master&lt;/code&gt; 上，在 &lt;code&gt;master&lt;/code&gt; 分支发布 1.0 版本；&lt;/p&gt;
&lt;p&gt;你和你的小伙伴们每个人都在 &lt;code&gt;dev&lt;/code&gt; 分支上干活，每个人都有自己的分支，时不时地往 &lt;code&gt;dev&lt;/code&gt; 分支上合并就可以了。所以，团队合作的分支看起来就像这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fbranch4.BisoG5ow.png&amp;#x26;w=626&amp;#x26;h=160&amp;#x26;f=webp&quot; alt=&quot;branch4&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Bug 分支&lt;/h3&gt;
&lt;p&gt;下面我们先引入一个场景，当你在开发 &lt;code&gt;dev&lt;/code&gt; 分支时，突然有一个要修改 bug 的任务。并且此时你的 &lt;code&gt;dev&lt;/code&gt; 分支的工作也才进行了一半，还没办法提交，但项目负责人现在必须让你修复 bug，这怎么办呢。&lt;/p&gt;
&lt;p&gt;还好，Git 提供了一个 &lt;code&gt;stash&lt;/code&gt; 功能，可以把当前工作现场“储藏”起来，等以后恢复现场后继续工作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，用 &lt;code&gt;git status&lt;/code&gt; 查看工作区，就是干净的（除非有没有被Git管理的文件），因此可以放心地创建分支来修复 bug。&lt;/p&gt;
&lt;p&gt;首先确定要在哪个分支上修复 bug，假定需要在 &lt;code&gt;master&lt;/code&gt; 分支上修复，就从 &lt;code&gt;master&lt;/code&gt; 创建临时分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git checkout master
Switched to branch &apos;master&apos;
Your branch is ahead of &apos;origin/master&apos; by 6 commits.
  (use &quot;git push&quot; to publish your local commits)

$ git checkout -b issue-101
Switched to a new branch &apos;issue-101&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在修复 bug，修改完成之后，进行提交。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add readme.txt 
$ git commit -m &quot;fix bug 101&quot;
[issue-101 4c805e2] fix bug 101
 1 file changed, 1 insertion(+), 1 deletion(-)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修复完成后，切换到 &lt;code&gt;master&lt;/code&gt; 分支，并完成合并，最后删除 &lt;code&gt;issue-101&lt;/code&gt; 分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git switch master
Switched to branch &apos;master&apos;
Your branch is ahead of &apos;origin/master&apos; by 6 commits.
  (use &quot;git push&quot; to publish your local commits)

$ git merge --no-ff -m &quot;merged bug fix 101&quot; issue-101
Merge made by the &apos;recursive&apos; strategy.
 readme.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;太棒了，bug 修复完成了！现在，是时候接着回到 &lt;code&gt;dev&lt;/code&gt; 分支干活了！先用 &lt;code&gt;git switch dev&lt;/code&gt; 切换到 &lt;code&gt;dev&lt;/code&gt; 分支，然后再 &lt;code&gt;git status&lt;/code&gt; 看一下工作区状态。你会发现，工作区是干净的，刚才的工作现场存到哪去了？用 &lt;code&gt;git stash list&lt;/code&gt; 命令看看。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git stash list
stash@{0}: WIP on dev: f52c633 add merge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工作现场还在，Git 把 &lt;code&gt;stash&lt;/code&gt; 内容存在某个地方了，但是需要恢复一下，有两个办法：一是用 &lt;code&gt;git stash apply&lt;/code&gt; 恢复，但是恢复后，&lt;code&gt;stash&lt;/code&gt; 内容并不删除，你需要用 &lt;code&gt;git stash drop&lt;/code&gt; 来删除；另一种方式是用 &lt;code&gt;git stash pop&lt;/code&gt;，恢复的同时把 stash 内容也删了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git stash pop
On branch dev
Changes to be committed:
  (use &quot;git reset HEAD &amp;#x3C;file&gt;...&quot; to unstage)

	new file:   hello.py

Changes not staged for commit:
  (use &quot;git add &amp;#x3C;file&gt;...&quot; to update what will be committed)
  (use &quot;git checkout -- &amp;#x3C;file&gt;...&quot; to discard changes in working directory)

	modified:   readme.txt

Dropped refs/stash@{0} (5d677e2ee266f39ea296182fb2354265b91b3b2a)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，再用 &lt;code&gt;git stash list&lt;/code&gt; 查看，就看不到任何 &lt;code&gt;stash&lt;/code&gt; 内容了。有需要的话，你可以多次 &lt;code&gt;stash&lt;/code&gt; ，恢复的时候，先用 &lt;code&gt;git stash list&lt;/code&gt; 查看，然后恢复指定的 &lt;code&gt;stash&lt;/code&gt;，用命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git stash apply stash@{0}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 master 分支上修复了 bug 后，我们要想一想，dev 分支是早期从 master 分支分出来的，所以，这个 bug 其实在当前 dev 分支上也存在。那怎么在 dev 分支上修复同样的 bug？一个比较容易想到的方法是重复操作一次，提交不就行了。那有没有更简单的方法呢。&lt;/p&gt;
&lt;p&gt;同样的 bug，要在 dev 上修复，我们只需要把 &lt;code&gt;4c805e2 fix bug 101&lt;/code&gt; 这个提交所做的修改“复制”到 dev 分支。注意：我们只想复制 &lt;code&gt;4c805e2 fix bug 101&lt;/code&gt; 这个提交所做的修改，并不是把整个 master 分支 &lt;code&gt;merge&lt;/code&gt; 过来。&lt;/p&gt;
&lt;p&gt;为了方便操作，Git 专门提供了一个 &lt;code&gt;cherry-pick&lt;/code&gt; 命令，让我们能复制一个特定的提交到当前分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git branch
* dev
  master
$ git cherry-pick 4c805e2
[master 1d4b803] fix bug 101
 1 file changed, 1 insertion(+), 1 deletion(-)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git 自动给 dev 分支做了一次提交，注意这次提交的 commit 是 &lt;code&gt;1d4b803&lt;/code&gt; ，它并不同于 master 的 &lt;code&gt;4c805e2&lt;/code&gt; ，因为这两个 commit 只是改动相同，但确实是两个不同的 commit。用 &lt;code&gt;git cherry-pick&lt;/code&gt; ，我们就不需要在 dev 分支上手动再把修 bug 的过程重复一遍。既然可以在 master 分支上修复 bug 后，在 dev 分支上可以“重放”这个修复过程，那么直接在 dev 分支上修复 bug，然后在 master 分支上“重放”行不行？当然可以，不过你仍然需要 &lt;code&gt;git stash&lt;/code&gt; 命令保存现场，才能从 dev 分支切换到 master 分支。&lt;/p&gt;
&lt;h3&gt;Feature 分支&lt;/h3&gt;
&lt;p&gt;操作和原理与 bug 分支类似，在这里就先不过多赘述了。后续有需要再进行补充。&lt;/p&gt;
&lt;h3&gt;多人协作&lt;/h3&gt;
&lt;p&gt;当你从远程仓库克隆时，实际上 Git 自动把本地的 master 分支和远程的 master 分支对应起来了，并且，远程仓库的默认名称是origin。要查看远程库的信息，用 &lt;code&gt;git remote&lt;/code&gt; 或者，用 &lt;code&gt;git remote -v&lt;/code&gt; 显示更详细的信息，会显示可以抓取和推送的origin的地址。如果没有推送权限，就看不到push的地址。&lt;/p&gt;
&lt;p&gt;推送分支的命令很简单，在推送时，要指定本地分支，这样，Git就会把该分支推送到远程库对应的远程分支上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git push origin master   # 推送到 master 分支
$ git push origin dev      # 推送到 dev 分支
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，并不是一定要把本地分支往远程推送，那么，哪些分支需要推送，哪些不需要呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;master 分支是主分支，因此要时刻与远程同步；&lt;/li&gt;
&lt;li&gt;dev 分支是开发分支，团队所有成员都需要在上面工作，所以也需要与远程同步；&lt;/li&gt;
&lt;li&gt;bug 分支只用于在本地修复 bug，就没必要推到远程了;&lt;/li&gt;
&lt;li&gt;feature 分支是否推到远程，取决于你是否和你的小伙伴合作在上面开发。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之一个原则就是在 Git 中，分支完全可以在本地自己藏着玩，是否推送，视你的心情而定！&lt;/p&gt;
&lt;p&gt;当你的小伙伴已经向 &lt;code&gt;origin/dev&lt;/code&gt; 分支推送了他的提交，而碰巧你也对同样的文件作了修改，并试图推送（即现有远程版本与你的本地版本不一样，因为你们两个修改了同一个文件的，并且他先于你进行推送，导致你的校验失败），就可能出现如下这个问题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git push origin dev
To github.com:michaelliao/learngit.git
 ! [rejected]        dev -&gt; dev (non-fast-forward)
error: failed to push some refs to &apos;git@github.com:michaelliao/learngit.git&apos;
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: &apos;git pull ...&apos;) before pushing again.
hint: See the &apos;Note about fast-forwards&apos; in &apos;git push --help&apos; for details.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;推送失败，因为你的小伙伴的最新提交和你试图推送的提交有冲突，解决办法也很简单，Git 已经提示我们，先用 &lt;code&gt;git pull&lt;/code&gt; 把最新的提交从 &lt;code&gt;origin/dev&lt;/code&gt; 抓下来，然后，在本地合并，解决冲突，再推送：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git pull
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

    git pull &amp;#x3C;remote&gt; &amp;#x3C;branch&gt;

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=origin/&amp;#x3C;branch&gt; dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git pull&lt;/code&gt; 也失败了，原因是没有指定本地 dev 分支与远程 &lt;code&gt;origin/dev&lt;/code&gt; 分支的链接，根据提示，设置 dev 和 origin/dev 的链接：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git branch --set-upstream-to=origin/dev dev
Branch &apos;dev&apos; set up to track remote branch &apos;dev&apos; from &apos;origin&apos;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再 pull：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git pull
Auto-merging env.txt
CONFLICT (add/add): Merge conflict in env.txt
Automatic merge failed; fix conflicts and then commit the result.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这回 &lt;code&gt;git pull&lt;/code&gt; 成功，但是合并有冲突，需要手动解决，解决的方法和分支管理中的解决冲突完全一样。解决后，提交，再 push：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git commit -m &quot;fix env conflict&quot;
[dev 57c53ab] fix env conflict

$ git push origin dev
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 621 bytes | 621.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
   7a5e5dd..57c53ab  dev -&gt; dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，多人协作的工作模式通常是这样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，可以尝试用 &lt;code&gt;git push origin &amp;#x3C;branch-name&gt;&lt;/code&gt; 推送自己的修改；&lt;/li&gt;
&lt;li&gt;如果推送失败，则因为远程分支比你的本地更新，需要先用 &lt;code&gt;git pull&lt;/code&gt; 试图合并；&lt;/li&gt;
&lt;li&gt;如果合并有冲突，则解决冲突，并在本地提交；&lt;/li&gt;
&lt;li&gt;没有冲突或者解决掉冲突后，再用 &lt;code&gt;git push origin &amp;#x3C;branch-name&gt;&lt;/code&gt; 推送就能成功！&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;git pull&lt;/code&gt;提示 &lt;code&gt;no tracking information&lt;/code&gt; ，则说明本地分支和远程分支的链接关系没有创建，用命令 &lt;code&gt;git branch --set-upstream-to &amp;#x3C;branch-name&gt; origin/&amp;#x3C;branch-name&gt;&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是多人协作的工作模式，一旦熟悉了，就非常简单。&lt;/p&gt;
&lt;h3&gt;Rebase&lt;/h3&gt;
&lt;p&gt;这个东西说实话我暂时没怎么明白，一个可预见的场景是代码改完了，到网页端检查一下，发现 README 没写好，就直接在网页上改了。这时本地远程不同步。本地下一次提交前就要先合并分支，再提交到远程。这是 commit 记录里就会多一条 merge 记录，看着很丑。有时候写完代码，前脚刚提交，后脚就发现 bug，有时候缝缝补补就出了好多个 commit。看着很丑，也不利于查看提交记录。就比如成为了下面这个样子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git log --graph --pretty=oneline --abbrev-commit
* d1be385 (HEAD -&gt; master, origin/master) init hello
*   e5e69f1 Merge branch &apos;dev&apos;
|\  
| *   57c53ab (origin/dev, dev) fix env conflict
| |\  
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
| |/  
* |   12a631b merged bug fix 101
|\ \  
| * | 4c805e2 fix bug 101
|/ /  
* |   e1e9c68 merge with no-ff
|\ \  
| |/  
| * f52c633 add merge
|/  
*   cf810e4 conflict fixed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就是很乱不好看的问题，这个时候，&lt;code&gt;rebase&lt;/code&gt; 就派上了用场。我们输入命令 &lt;code&gt;git rebase&lt;/code&gt; 试试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git rebase
First, rewinding head to replay your work on top of it...
Applying: add comment
Using index info to reconstruct a base tree...
M	hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py
Applying: add author
Using index info to reconstruct a base tree...
M	hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出了一大堆操作，到底是啥效果？再用 &lt;code&gt;git log&lt;/code&gt; 看看：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -&gt; master) add author
* 3611cfe add comment
* f005ed4 (origin/master) set exit=1
* d1be385 init hello
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原本分叉的提交现在变成一条直线了！这种神奇的操作是怎么实现的？其实原理非常简单。我们注意观察，发现 Git 把我们本地的提交“挪动”了位置，放到了 &lt;code&gt;f005ed4 (origin/master) set exit=1&lt;/code&gt; 之后，这样，整个提交历史就成了一条直线。rebase 操作前后，最终的提交内容是一致的，但是，我们本地的 commit 修改内容已经变化了，它们的修改不再基于 &lt;code&gt;d1be385 init hello&lt;/code&gt;，而是基于 &lt;code&gt;f005ed4 (origin/master) set exit=1&lt;/code&gt;，但最后的提交 &lt;code&gt;7e61ed4&lt;/code&gt; 内容是一致的。这就是 rebase 操作的特点：把分叉的提交历史“整理”成一条直线，看上去更直观。缺点是本地的分叉提交已经被修改过了。最后，通过 push 操作把本地分支推送到远程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;~/learngit michael$ git push origin master
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 576 bytes | 576.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To github.com:michaelliao/learngit.git
   f005ed4..7e61ed4  master -&gt; master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再用 &lt;code&gt;git log&lt;/code&gt; 看看效果，你会发现，远程分支的提交历史也是一条直线。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -&gt; master, origin/master) add author
* 3611cfe add comment
* f005ed4 set exit=1
* d1be385 init hello
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是有点不太理解，就写一个小结吧。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先我们同步远程最新代码，开始工作...&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git push&lt;/code&gt; 产生冲突，说明有人先你一步同步了他的本地代码到远程。&lt;/li&gt;
&lt;li&gt;这时候，你需要先拉取代码，可以使用命令 &lt;code&gt;git pull&lt;/code&gt; , 该命令会将远程的提交和你本地的提交 merge，如果有冲突需要手动解决并提交，会产生 merge 的记录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git pull -- rebase&lt;/code&gt; 该命令会把你的提交“放置”在远程拉取的提交之后，即改变基础（变基），如果有冲突，解决所有冲突的文件，&lt;code&gt;git add &amp;#x3C;冲突文件&gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git rebase --continue&lt;/code&gt; 完美解决问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;这一篇的内容非常多，因为分支是 git 区别于其他版本管理软件的一个最显著的功能。所以你我，就常常复习，细细精进吧。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.BP2smwy6.jpg"/><enclosure url="/_astro/heroimage.BP2smwy6.jpg"/></item><item><title>About Git</title><link>https://xiaohei-blog.vercel.app/blog/git</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/git</guid><description>Git 的一些命令与用法</description><pubDate>Tue, 16 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;为了补下原来所欠缺的那节计算机课，并且想长期并且很有必要去经营一下我的github，那么就很有必要去学一下 git。这是我根据网上的资料，以及我自己所理解的关于 git 的初级知识，不算很专业，但我觉得对于大多数从业者也都是够用的。那就这样，开始吧，同样的，写给你也写给我自己。&lt;/p&gt;
&lt;h2&gt;Git 是什么&lt;/h2&gt;
&lt;p&gt;Linus 在1991年创建了开源的 Linux，从此，Linux 系统不断发展，已经成为最大的服务器系统软件了。Linus 虽然创建了 Linux，但 Linux 的壮大是靠全世界热心的志愿者参与的，这么多人在世界各地为 Linux 编写代码，那 Linux 的代码是如何管理的呢？事实是，在 2002 年以前，世界各地的志愿者把源代码文件通过 diff 的方式发给 Linus，然后由 Linus 本人通过手工方式合并代码！&lt;/p&gt;
&lt;p&gt;你也许会想，为什么 Linus 不把 Linux 代码放到版本控制系统里呢？不是有 CVS、SVN 这些免费的版本控制系统吗？因为 Linus 坚定地反对 CVS 和 SVN，这些集中式的版本控制系统不但速度慢，而且必须联网才能使用。有一些商用的版本控制系统，虽然比 CVS、SVN 好用，但那是付费的，和 Linux 的&lt;strong&gt;开源精神&lt;/strong&gt;不符。不过，到了 2002 年，Linux 系统已经发展了十年了，代码库之大让 Linus 很难继续通过手工方式管理了，社区的弟兄们也对这种方式表达了强烈不满，于是 Linus 选择了一个商业的版本控制系统 BitKeeper，BitKeeper 的东家 BitMover 公司出于人道主义精神，授权 Linux 社区免费使用这个版本控制系统。&lt;/p&gt;
&lt;p&gt;安定团结的大好局面在 2005 年就被打破了，原因是 Linux 社区牛人聚集，不免沾染了一些梁山好汉的江湖习气。开发 Samba 的 Andrew 试图破解 BitKeeper 的协议（这么干的其实也不只他一个），被 BitMover 公司发现了（监控工作做得不错！），于是 BitMover 公司怒了，要收回 Linux 社区的免费使用权。&lt;/p&gt;
&lt;p&gt;Linus 可以向 BitMover 公司道个歉，保证以后严格管教弟兄们，嗯，这是不可能的。实际情况是这样的：&lt;/p&gt;
&lt;p&gt;Linus 花了两周时间自己用 C 写了一个分布式版本控制系统，这就是 Git！一个月之内，Linux 系统的源码已经由 Git 管理了！牛是怎么定义的呢？大家可以体会一下。Git 迅速成为最流行的分布式版本控制系统，尤其是 2008 年，GitHub 网站上线了，它为开源项目免费提供 Git 存储，无数开源项目开始迁移至 GitHub，包括 jQuery，PHP，Ruby 等等。&lt;/p&gt;
&lt;p&gt;历史就是这么偶然，如果不是当年 BitMover 公司威胁 Linux 社区，可能现在我们就没有免费而超级好用的 Git 了。&lt;/p&gt;
&lt;h2&gt;安装Git&lt;/h2&gt;
&lt;h3&gt;在 Linux 上安装 Git&lt;/h3&gt;
&lt;p&gt;首先，你可以试着输入 &lt;code&gt;git&lt;/code&gt;，看看系统有没有安装 Git：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git
The program &apos;git&apos; is currently not installed. You can install it by typing:
sudo apt-get install git

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;像上面的命令，有很多 Linux 会友好地告诉你 Git 没有安装，还会告诉你如何安装 Git。&lt;/p&gt;
&lt;p&gt;如果你碰巧用 Debian 或 Ubuntu Linux，通过一条 &lt;code&gt;sudo apt install git&lt;/code&gt; 就可以直接完成 Git 的安装，非常简单。&lt;/p&gt;
&lt;p&gt;如果是其他 Linux 版本，请参考发行版说明，例如，RedHat Linux 可以通过命令 &lt;code&gt;sudo yum install git&lt;/code&gt; 安装。没有包管理器的发行版可以自行下载源码编译安装，仅适合老鸟。&lt;/p&gt;
&lt;h3&gt;在 macOS 上安装 Git&lt;/h3&gt;
&lt;p&gt;如果你正在使用 Mac 做开发，有两种安装 Git 的方法。&lt;/p&gt;
&lt;p&gt;一是先安装包管理器 Homebrew，然后通过 Homebrew 安装 Git（推荐）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ brew install git

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种方法更简单，但需要下载一个巨大的XCode。直接从AppStore安装Xcode，Xcode集成了Git，不过默认没有安装，你需要运行Xcode，选择菜单“Xcode”-&gt;“Preferences”，在弹出窗口中找到“Downloads”，选择“Command Line Tools”，点“Install”就可以完成安装了。&lt;/p&gt;
&lt;h3&gt;在 Windows 上安装 Git&lt;/h3&gt;
&lt;p&gt;在 Windows 上使用 Git，我建议直接从 Git 官网直接下载&lt;a href=&quot;https://git-scm.com/install/windows&quot;&gt;安装程序&lt;/a&gt;，然后按默认选项安装即可。安装完成后，在开始菜单里找到 “Git”-&gt;“Git Bash” ，蹦出一个类似命令行窗口的东西，就说明 Git 安装成功。&lt;/p&gt;
&lt;p&gt;当然还有另一种安装方法，但是我懒得研究了，就不详细写了，这个方法的安装操作原理为包管理器，名为 &lt;a href=&quot;https://scoop.sh&quot;&gt;Scoop&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;配置 Git&lt;/h2&gt;
&lt;p&gt;安装好Git后，还需要最后一步设置，在命令行输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git config --global user.name &quot;Your Name&quot;
$ git config --global user.email &quot;email@example.com&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 Git 是分布式版本控制系统，所以，每个机器都必须自报家门：你的名字和 Email 地址。你也许会担心，如果有人故意冒充别人怎么办？这个不必担心，首先我们相信大家都是善良无知的群众，其次，真的有冒充的也是有办法可查的。&lt;/p&gt;
&lt;p&gt;注意 &lt;code&gt;git config&lt;/code&gt; 命令的 &lt;code&gt;--global&lt;/code&gt; 参数，用了这个参数，表示你这台机器上所有的 Git 仓库都会使用这个配置，当然也可以对某个仓库指定不同的用户名和 Email 地址。&lt;/p&gt;
&lt;h2&gt;创建版本库&lt;/h2&gt;
&lt;p&gt;什么是版本库呢？版本库即 github 上的仓库（Repository），你可以简单理解成一个目录，这个目录里面的所有文件都可以被 Git 管理起来，每个文件的修改、删除，Git 都能跟踪，以便任何时刻都可以追踪历史，或者在将来某个时刻可以“还原”。&lt;/p&gt;
&lt;p&gt;所以，创建一个版本库非常简单，首先，选择一个合适的地方，创建一个空目录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ mkdir learngit
$ cd learngit
$ pwd
/Users/michael/learngit

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二步，通过 &lt;code&gt;git init&lt;/code&gt; 命令把这个目录变成 Git 可以管理的仓库：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git init
Initialized empty Git repository in /Users/michael/learngit/.git/

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;瞬间 Git 就把仓库建好了，而且告诉你是一个空的仓库 （empty Git repository），细心的读者可以发现当前目录下多了一个 &lt;code&gt;.git&lt;/code&gt; 的目录，这个目录是 Git 来跟踪管理版本库的，没事千万不要手动修改这个目录里面的文件，不然改乱了，就把 Git 仓库给破坏了。&lt;/p&gt;
&lt;p&gt;如果你没有看到 &lt;code&gt;.git&lt;/code&gt; 目录，那是因为这个目录默认是隐藏的，用 &lt;code&gt;ls -ah&lt;/code&gt; 命令就可以看见。&lt;/p&gt;
&lt;p&gt;也不一定必须在空目录下创建 Git 仓库，选择一个已经有东西的目录也是可以的。不过，不建议你使用自己正在开发的涉密项目来学习 Git，否则造成的一切后果概不负责。&lt;/p&gt;
&lt;h2&gt;把文件添加到版本库&lt;/h2&gt;
&lt;p&gt;首先这里再明确一下，所有的版本控制系统，其实&lt;strong&gt;只能跟踪文本文件的改动&lt;/strong&gt;，比如 TXT 文件，网页，所有的程序代码等等，Git 也不例外。版本控制系统可以告诉你每次的改动，比如在第 5 行加了一个单词 “Linux”，在第 8 行删了一个单词 “Windows”。而图片、视频这些二进制文件，虽然也能由版本控制系统管理，但没法跟踪文件的变化，只能把二进制文件每次改动串起来，也就是只知道图片从 100KB 改成了 120KB，但到底改了啥，版本控制系统不知道，也没法知道。&lt;/p&gt;
&lt;p&gt;不幸的是，Microsoft 的 Word 格式是二进制格式，因此，版本控制系统是没法跟踪 Word 文件的改动的，前面我们举的例子只是为了演示，如果要真正使用版本控制系统，就要以纯文本方式编写文件。&lt;/p&gt;
&lt;p&gt;因为文本是有编码的，比如中文有常用的 GBK 编码，日文有 Shift_JIS 编码，如果没有历史遗留问题，强烈建议使用标准的 UTF-8 编码，所有语言使用同一种编码，既没有冲突，又被所有平台所支持。&lt;/p&gt;
&lt;p&gt;和把大象放到冰箱需要 3 步相比，把一个文件放到 Git 仓库只需要两步。&lt;/p&gt;
&lt;p&gt;第一步，用命令 &lt;code&gt;git add&lt;/code&gt; 告诉 Git，把文件添加到仓库：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add readme.txt  # 或者 git add .  将所有更新文件添加到推送队列

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的命令，没有任何显示，这就对了，Unix的哲学是“没有消息就是好消息”，说明添加成功。&lt;/p&gt;
&lt;p&gt;第二步，用命令 &lt;code&gt;git commit&lt;/code&gt; 告诉 Git，把文件提交到仓库：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git commit -m &quot;wrote a readme file&quot;
[master (root-commit) eaadf4e] wrote a readme file
 1 file changed, 2 insertions(+)
 create mode 100644 readme.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单解释一下 &lt;code&gt;git commit&lt;/code&gt; 命令，&lt;code&gt;-m&lt;/code&gt; 后面输入的是本次提交的说明，可以输入任意内容，当然最好是有意义的，这样你就能从历史记录里方便地找到改动记录。&lt;/p&gt;
&lt;p&gt;嫌麻烦不想输入 &lt;code&gt;-m &quot;xxx&quot;&lt;/code&gt; 行不行？确实有办法可以这么干，但是强烈不建议你这么干，因为输入说明对自己对别人阅读都很重要。实在不想输入说明请自行 Google，我懒得写了，因为记录是个好习惯。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git commit&lt;/code&gt; 命令执行成功后会告诉你，&lt;code&gt;1 file changed&lt;/code&gt;：1个文件被改动（我们新添加的 readme.txt 文件）；&lt;code&gt;2 insertions&lt;/code&gt;：插入了两行内容（readme.txt 有两行内容）。&lt;/p&gt;
&lt;p&gt;为什么 Git 添加文件需要 &lt;code&gt;add&lt;/code&gt;，&lt;code&gt;commit&lt;/code&gt; 一共两步呢？因为 &lt;code&gt;commit&lt;/code&gt; 可以一次提交很多文件，所以你可以多次 &lt;code&gt;add&lt;/code&gt; 不同的文件，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add file1.txt
$ git add file2.txt file3.txt
$ git commit -m &quot;add 3 files.&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;状态查看&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git status&lt;/code&gt; 命令可以让我们时刻掌握仓库当前的状态，下面的命令输出告诉我们，readme.txt 被修改过了，但还没有准备提交的修改。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git status
On branch master
Changes not staged for commit:
  (use &quot;git add &amp;#x3C;file&gt;...&quot; to update what will be committed)
  (use &quot;git checkout -- &amp;#x3C;file&gt;...&quot; to discard changes in working directory)

	modified:   readme.txt

no changes added to commit (use &quot;git add&quot; and/or &quot;git commit -a&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然 Git 告诉我们 &lt;code&gt;readme.txt&lt;/code&gt; 被修改了，但如果能看看具体修改了什么内容，自然是很好的。比如你休假两周从国外回来，第一天上班时，已经记不清上次怎么修改的 &lt;code&gt;readme.txt&lt;/code&gt; ，所以，需要用 &lt;code&gt;git diff&lt;/code&gt; 这个命令看看：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git diff readme.txt 
diff --git a/readme.txt b/readme.txt
index 46d49bf..9247db6 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
-Git is a version control system.
+Git is a distributed version control system.
 Git is free software.

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git diff&lt;/code&gt; 顾名思义就是查看 difference，显示的格式正是 Unix 通用的 diff 格式，可以从上面的命令输出看到，我们在第一行添加了一个 &lt;code&gt;distributed&lt;/code&gt; 单词。&lt;/p&gt;
&lt;h2&gt;版本回退&lt;/h2&gt;
&lt;p&gt;一旦你把文件改乱了，或者误删了文件，还可以从最近的一个 &lt;code&gt;commit&lt;/code&gt; 恢复，然后继续工作，而不是把几个月的工作成果全部丢失。实现一个版本回退与跳转的功能。&lt;/p&gt;
&lt;p&gt;当然了，在实际工作中，我们脑子里怎么可能记得一个几千行的文件每次都改了什么内容，不然要版本控制系统干什么。版本控制系统肯定有某个命令可以告诉我们历史记录，在 Git 中，我们用 &lt;code&gt;git log&lt;/code&gt; 命令查看：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git log
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -&gt; master)
Author: Michael Liao &amp;#x3C;askxuefeng@gmail.com&gt;
Date:   Fri May 18 21:06:15 2018 +0800

    append GPL

commit e475afc93c209a690c39c13a46716e8fa000c366
Author: Michael Liao &amp;#x3C;askxuefeng@gmail.com&gt;
Date:   Fri May 18 21:03:36 2018 +0800

    add distributed

commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Michael Liao &amp;#x3C;askxuefeng@gmail.com&gt;
Date:   Fri May 18 20:59:18 2018 +0800

    wrote a readme file

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git log&lt;/code&gt; 命令显示从最近到最远的提交日志，我们可以看到 3 次提交，最近的一次是 &lt;code&gt;append GPL&lt;/code&gt;，上一次是 &lt;code&gt;add distributed&lt;/code&gt;，最早的一次是 &lt;code&gt;wrote a readme file&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果嫌输出信息太多，看得眼花缭乱的，可以试试加上 &lt;code&gt;--pretty=oneline&lt;/code&gt; 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git log --pretty=oneline
1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -&gt; master) append GPL
e475afc93c209a690c39c13a46716e8fa000c366 add distributed
eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0 wrote a readme file

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要友情提示的是，你看到的一大串类似 &lt;code&gt;1094adb...&lt;/code&gt; 的是 &lt;code&gt;commit id&lt;/code&gt;（版本号），和 SVN 不一样，Git 的 &lt;code&gt;commit id&lt;/code&gt; 不是 1，2，3…… 递增的数字，而是一个 SHA1 计算出来的一个非常大的数字，用十六进制表示，而且你看到的 &lt;code&gt;commit id&lt;/code&gt; 和我的肯定不一样，以你自己的为准。为什么 &lt;code&gt;commit id&lt;/code&gt; 需要用这么一大串数字表示呢？因为 Git 是分布式的版本控制系统，后面我们还要研究多人在同一个版本库里工作，如果大家都用 1，2，3…… 作为版本号，那肯定就冲突了。&lt;/p&gt;
&lt;p&gt;好了，现在我们启动时光穿梭机，我们想把 &lt;code&gt;readme.txt&lt;/code&gt; 回退到上一个版本，也就是 &lt;code&gt;add distributed&lt;/code&gt; 的那个版本，怎么做呢？&lt;/p&gt;
&lt;p&gt;首先，Git 必须知道当前版本是哪个版本，在 Git 中，用 &lt;code&gt;HEAD&lt;/code&gt; 表示当前版本，也就是最新的提交 &lt;code&gt;1094adb...&lt;/code&gt;（注意我的提交 ID 和你的肯定不一样），上一个版本就是 &lt;code&gt;HEAD^&lt;/code&gt;，上上一个版本就是 &lt;code&gt;HEAD^^&lt;/code&gt;，当然往上 100 个版本写 100 个&lt;code&gt;^&lt;/code&gt;比较容易数不过来，所以写成 &lt;code&gt;HEAD~100&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在，我们要把当前版本 &lt;code&gt;append GPL&lt;/code&gt; 回退到上一个版本 &lt;code&gt;add distributed&lt;/code&gt;，就可以使用 &lt;code&gt;git reset&lt;/code&gt; 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git reset --hard HEAD^
HEAD is now at e475afc add distributed

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--hard&lt;/code&gt; 参数有啥意义？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--hard&lt;/code&gt; 会回退到上个版本的已提交状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--soft&lt;/code&gt; 会回退到上个版本的未提交状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--mixed&lt;/code&gt; 会回退到上个版本已添加但未提交的状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在，先放心使用&lt;code&gt;--hard&lt;/code&gt;。现在你再使用&lt;code&gt;git log&lt;/code&gt;会发现最新的那个版本 &lt;code&gt;append GPL&lt;/code&gt; 已经看不到了！好比你从 21 世纪坐时光穿梭机来到了 19 世纪，想再回去已经回不去了，肿么办？&lt;/p&gt;
&lt;p&gt;办法其实还是有的，只要上面的命令行窗口还没有被关掉，你就可以顺着往上找啊找啊，找到那个 &lt;code&gt;append GPL&lt;/code&gt; 的 &lt;code&gt;commit id&lt;/code&gt; 是 &lt;code&gt;1094adb...&lt;/code&gt; ，于是就可以指定回到未来的某个版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git reset --hard 1094a
HEAD is now at 83b0afe append GPL

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;版本号没必要写全，前几位就可以了，Git 会自动去找。当然也不能只写前一两位，因为 Git 可能会找到多个版本号，就无法确定是哪一个了。Git 的版本回退速度非常快，因为 Git 在内部有个指向当前版本的 &lt;code&gt;HEAD&lt;/code&gt; 指针，当你回退版本的时候，Git 仅仅是把 &lt;code&gt;HEAD&lt;/code&gt; 从指向 &lt;code&gt;append GPL&lt;/code&gt; 改为指向 &lt;code&gt;add distributed&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在，你回退到了某个版本，关掉了电脑，第二天早上就后悔了，想恢复到新版本怎么办？找不到新版本的 &lt;code&gt;commit id&lt;/code&gt; 怎么办？&lt;/p&gt;
&lt;p&gt;在 Git 中，总是有后悔药可以吃的。当你用 &lt;code&gt;$ git reset --hard HEAD^&lt;/code&gt; 回退到 &lt;code&gt;add distributed&lt;/code&gt; 版本时，再想恢复到 &lt;code&gt;append GPL&lt;/code&gt;，就必须找到 &lt;code&gt;append GPL&lt;/code&gt; 的 commit id。Git 提供了一个命令 &lt;code&gt;git reflog&lt;/code&gt; 用来记录你的每一次命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git reflog
e475afc HEAD@{1}: reset: moving to HEAD^
1094adb (HEAD -&gt; master) HEAD@{2}: commit: append GPL
e475afc HEAD@{3}: commit: add distributed
eaadf4e HEAD@{4}: commit (initial): wrote a readme file

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从输出可知，&lt;code&gt;append GPL&lt;/code&gt; 的 commit id 是 &lt;code&gt;1094adb&lt;/code&gt;，现在，你又可以乘坐时光机回到未来了。&lt;/p&gt;
&lt;h2&gt;工作区与暂存区&lt;/h2&gt;
&lt;p&gt;Git 的版本库里存了很多东西，其中最重要的就是称为 stage（或者叫index）的暂存区，还有 Git 为我们自动创建的第一个分支 master，以及指向 master 的一个指针叫 HEAD。分支和 HEAD 的概念我们以后再讲。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git add&lt;/code&gt; 命令实际上就是把要提交的所有修改放到暂存区（Stage），然后，执行 &lt;code&gt;git commit&lt;/code&gt; 就可以一次性把暂存区的所有修改提交到分支。其他的我觉得没什么需要理解的，直接看图。当然很好理解的是工作区就是你本地的项目整体文件夹。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Frepos.BF3w_wu4.png&amp;#x26;w=585&amp;#x26;h=300&amp;#x26;f=webp&quot; alt=&quot;workspace&quot;&gt;&lt;/p&gt;
&lt;h2&gt;撤销修改&lt;/h2&gt;
&lt;p&gt;当你发现错误时，如果你没有提交，就可以很容易地纠正它。你可以删掉最后一行，手动把文件恢复到上一个版本的状态。&lt;/p&gt;
&lt;p&gt;另外，还有一个命令也可以实现这个效果，&lt;code&gt;git checkout -- file&lt;/code&gt; 可以丢弃工作区的修改。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git checkout -- readme.txt

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令 &lt;code&gt;git checkout -- readme.txt&lt;/code&gt; 意思就是，把 readme.txt 文件在工作区的修改全部撤销，这里有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一种是 readme.txt 自修改后还没有被放到暂存区，现在，撤销修改就回到和版本库一模一样的状态；&lt;/li&gt;
&lt;li&gt;一种是 readme.txt 已经添加到暂存区后，又作了修改，现在，撤销修改就回到添加到暂存区后的状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之，就是让这个文件回到最近一次 &lt;code&gt;git commit&lt;/code&gt; 或 &lt;code&gt;git add&lt;/code&gt; 时的状态。&lt;/p&gt;
&lt;p&gt;如果你将一些错误信息，&lt;code&gt;git add&lt;/code&gt; 到暂存区了，但在 commit 之前，你发现了这个问题，此时修改只是添加到了暂存区，还没有提交。Git 就告诉我们，用命令 &lt;code&gt;git reset HEAD &amp;#x3C;file&gt;&lt;/code&gt; 可以把暂存区的修改撤销掉（unstage），重新放回工作区。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git reset HEAD readme.txt
Unstaged changes after reset:
M	readme.txt

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt; 命令既可以回退版本，也可以把暂存区的修改回退到工作区。当我们用 &lt;code&gt;HEAD&lt;/code&gt; 时，表示最新的版本。执行之后，就可以看到暂存区终于干净了，只有工作区进行了修改。&lt;/p&gt;
&lt;p&gt;还记得如何丢弃工作区的修改吗？三不之内必有解药，答案就在上面。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git checkout -- readme.txt

$ git status
On branch master
nothing to commit, working tree clean

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除文件&lt;/h2&gt;
&lt;p&gt;一般情况下，通常直接在文件管理器中把没用的文件删了，或者用 &lt;code&gt;rm&lt;/code&gt; 命令删了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ rm test.txt

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候，Git 知道你删除了文件，因此，工作区和版本库就不一致了，如果直接 push 到远程仓库就可能会有一些问题。就可以先用 &lt;code&gt;git status&lt;/code&gt; 命令来看看哪些文件被删除了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git status
On branch master
Changes not staged for commit:
  (use &quot;git add/rm &amp;#x3C;file&gt;...&quot; to update what will be committed)
  (use &quot;git checkout -- &amp;#x3C;file&gt;...&quot; to discard changes in working directory)

	deleted:    test.txt

no changes added to commit (use &quot;git add&quot; and/or &quot;git commit -a&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在就有两个选择，一是确实要从版本库中删除该文件，那就用命令 &lt;code&gt;git rm&lt;/code&gt; 删掉，并且 &lt;code&gt;git commit&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git rm test.txt
rm &apos;test.txt&apos;

$ git commit -m &quot;remove test.txt&quot;
[master d46f35e] remove test.txt
 1 file changed, 1 deletion(-)
 delete mode 100644 test.txt

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，文件就从版本库中被删除了。即先手动删除文件，然后使用 &lt;code&gt;git rm &amp;#x3C;file&gt;&lt;/code&gt; 和 &lt;code&gt;git add&amp;#x3C;file&gt;&lt;/code&gt; 效果是一样的。&lt;/p&gt;
&lt;p&gt;另一种情况是删错了，因为版本库里还有呢，所以可以很轻松地把误删的文件恢复到最新版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git checkout -- test.txt

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git checkout&lt;/code&gt; 其实是用版本库里的版本替换工作区的版本，无论工作区是修改还是删除，都可以“一键还原”。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;当然，故事到这里还没有结束，上述的所有内容，你在其他的版本管理器中也可以用到，完全没有发挥出 git 的真正优势。跟紧脚步，我们下一篇内容整点高级的，让你真正体会到 git 的魅力。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.Bb_O8kSs.jpg"/><enclosure url="/_astro/heroimage.Bb_O8kSs.jpg"/></item><item><title>ESP32 小型四旋翼开发指南</title><link>https://xiaohei-blog.vercel.app/blog/esp32-uav</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/esp32-uav</guid><description>一份详尽的 ESP32 无人机开发上手教程。从环境搭建到硬件解析，再到 PID 调参。</description><pubDate>Wed, 10 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { StepIndent } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;还记得小时候仰望蓝天，看着飞机划过留下的白线，以及看着空军飞行员从天上俯瞰大地时那种广阔与豁达的视野，心里总会萌生出一种对飞翔的渴望。可惜的是我终究没有一个飞行员的身体，也算是些许的遗憾吧。庆幸的是科技发展的今天，借助开源硬件的蓬勃发展，我不必成为航天工程师，也能亲手打造属于自己的飞行器，也能简单的窥见梦想中的场景。&lt;/p&gt;
&lt;p&gt;这篇博客将带你走进 ESP32 小型四旋翼无人机的制作过程，这不是一个犹如大疆无人机那样完美的作品。但这里的每一行代码、每一个焊点，都是通向天空的阶梯。哪怕你之前只是点亮过一颗 LED，只要跟着这篇教程走，你也一定能亲手让这些电路板和电机在空中飞翔。&lt;/p&gt;
&lt;p&gt;我不想把这写成一篇枯燥的技术文档，更想它可以成为一份陪你一起探索未知的飞行日记。准备好了吗？Let&apos;s go for it！&lt;/p&gt;
&lt;h2&gt;软件安装&lt;/h2&gt;
&lt;p&gt;在实现真正的飞行之前，我们需要先搭建好我们的软件编译环境。这里我们选择乐鑫官方的 ESP-IDF，它虽然上手曲线稍陡，但能让我们更深入地掌控硬件，也更好的能在后续找到相关的问题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载安装包&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./image/down.png&quot; alt=&quot;ESP-IDF 下载界面&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;环境配置向导&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;安装过程其实并不复杂，按照向导一步步来即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;选择语言&lt;/strong&gt;：安装程序启动后的第一件事就是选择语言，选一个你看着顺眼的就行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/language.png&quot; alt=&quot;语言&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;环境检测&lt;/strong&gt;：程序会自动检测你当前的系统环境。如果缺了什么组件（比如 Python、Git），它会提示你补全。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/env.png&quot; alt=&quot;环境&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路径设置&lt;/strong&gt;：如果你是第一次安装，在后续页面中选择一个合适的路径即可。但&lt;strong&gt;切记不要包含中文或空格&lt;/strong&gt;，尽量使用全英文路径，否则后续编译时可能会报错报到让你怀疑人生。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/path.png&quot; alt=&quot;路径&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;选择组件&lt;/strong&gt;：在这一步，我们选择&lt;strong&gt;完全安装&lt;/strong&gt;版本。这是目前比较稳定且功能完善的版本，直接下一步即可，这里非必要情况尽量不要去改动。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/select.png&quot; alt=&quot;组件&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;等待安装与驱动修复&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;点击“安装”后，安装程序会自动下载并配置所需的工具链。当看到“安装完成”的界面时，开发环境已经配置完成！此时你的桌面上应该会出现两个黑色的图标，&lt;code&gt;ESP-IDF 5.3 PowerShell&lt;/code&gt; 和 &lt;code&gt;ESP-IDF 5.3 CMD&lt;/code&gt;。它们是命令行的开发工具，另外还有一个图形化工具 &lt;code&gt;Espressif IDF&lt;/code&gt;，这个可能更适合新手玩家，也更方便一些。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/finish.png&quot; alt=&quot;安装&quot;&gt;&lt;/p&gt;
&lt;h2&gt;创建项目&lt;/h2&gt;
&lt;p&gt;环境装好了，不跑个程序怎么行。在嵌入式世界里，点灯是仪式感，输出 &quot;Hello World&quot; 是信仰。让我们先让 ESP32 对这个世界打个招呼。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动与初始化&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;双击打开桌面上的 &lt;code&gt;Espressif IDF&lt;/code&gt;。双击启动软件，创建工作空间。之后再在软件中创建 IDF 项目工程。填写一个项目，并创建一个文件夹来存放项目，然后点击 “下一步”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/workspace.png&quot; alt=&quot;工作空间&quot;&gt;
&lt;img src=&quot;./image/project.png&quot; alt=&quot;工程&quot;&gt;
&lt;img src=&quot;./image/project1.png&quot; alt=&quot;工程1&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;编译与烧录&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;接下来进行工程的编译烧录，并运行监测，记得接入开发板设备，选择所要使用的设备型号，以及设备端口号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/input.png&quot; alt=&quot;烧录&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译、烧录运行监视项目，下面红框圈出来的三个工具就对应着编译、烧录、监视器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/make.png&quot; alt=&quot;编译&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;点击上方菜单栏左上角的小锤子，&lt;strong&gt;编译&lt;/strong&gt;项目。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/make1.png&quot; alt=&quot;编译1&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当看到下方的 “Console” 栏出现如下字样，就表示编译完成，可以进行烧录。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/Console.png&quot; alt=&quot;Console&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;点击左上角的三角图标进行&lt;strong&gt;烧录&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/allin.png&quot; alt=&quot;烧录&quot;&gt;
&lt;img src=&quot;./image/allin1.png&quot; alt=&quot;烧录1&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接下来，我们打开&lt;strong&gt;监视器看看运行结果&lt;/strong&gt;。点击上方菜单栏中的“小电视”，会弹出下面的选项框，正常来说默认选项就好，直接点击 ”OK“ 即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/result.png&quot; alt=&quot;结果&quot;&gt;&lt;/p&gt;
&lt;h2&gt;硬件解析&lt;/h2&gt;
&lt;p&gt;软件环境搭建完成了，现在我们再来看看硬件部分该怎么操作。关于焊接贴片部分我就不那么详细介绍了，根据我&lt;a href=&quot;https://github.com/xiaohei94/ESP32_UAV&quot;&gt;开源项目仓库&lt;/a&gt;中的 BOM 表和 PCB 文件进行相关物料的准备即可。下面就了解一下在准备齐全之后仍要面对的一些细碎的知识内容。&lt;/p&gt;
&lt;h3&gt;硬件设计方案&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;电源方案&lt;/strong&gt;：先通过 ps7516 芯片，从电池电压 3.7v - 4.2v 升压成 5v 再通过 AMS1117-3.3 减压成 3.3v 主要起到一个缓冲的作用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;电机方案&lt;/strong&gt;：使用8520空心杯电机，&lt;strong&gt;电机的轴径&lt;/strong&gt;分为 1.0 mm 和 1.2mm，&lt;strong&gt;要和螺旋桨的内径相同&lt;/strong&gt;不然会装不上，就算硬怼上了螺旋桨也会偏心。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;电池方案&lt;/strong&gt;：用 1s 动力电池（长度小于 65，宽小于 17.5，厚小于 7.5），一般有 3.7V 和 3.8V 两种，选择 3.7v 的即可，两种都可以使用，但 3.8v 的容易鼓包。接头类型选择 PH2.0 容易插拔。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LED 指示灯&lt;/strong&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;蓝灯&lt;/strong&gt;飞机状态指示灯，用来&lt;strong&gt;显示当前飞控内部循环是在哪部分&lt;/strong&gt;，例如：初始化或初始完成未解锁。&lt;/li&gt;
&lt;li&gt;电量指示灯**(黄)&lt;strong&gt;显示&lt;/strong&gt;电池电压是否足够**。&lt;/li&gt;
&lt;li&gt;连接指示灯**(绿)&lt;strong&gt;用来显示&lt;/strong&gt;飞控与地面站和遥控的连接状态**。&lt;/li&gt;
&lt;li&gt;电源灯**(红)&lt;strong&gt;就是&lt;/strong&gt;上电就亮表示电源正常**。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;进入下载模式&lt;/h3&gt;
&lt;p&gt;我们的小飞机采用了 &lt;strong&gt;ESP32-S3-WROOM-1（D2N8）&lt;/strong&gt; 模组。这不仅仅是一个 Wi-Fi 芯片，它双核 240MHz 的算力足够同时处理姿态解算和 PID 控制。对于该芯片程序烧写模式的触发，我们在电路上是这么设置的。&lt;strong&gt;下载电路&lt;/strong&gt;是手动下载，通过飞控上 &lt;code&gt;boot&lt;/code&gt; 和 &lt;code&gt;rst&lt;/code&gt; 按钮进入下载模式（先按住不放 &lt;code&gt;boot&lt;/code&gt;，再按一下 &lt;code&gt;rst&lt;/code&gt; 就可以进入下载模式了），然后可以直接通过串口下载（购买一个&lt;strong&gt;串口下载器&lt;/strong&gt;按照四个引脚 &lt;code&gt;rx&lt;/code&gt;，&lt;code&gt;tx&lt;/code&gt;，&lt;code&gt;3.3V&lt;/code&gt;，&lt;code&gt;gnd&lt;/code&gt;连接上即可）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/download.png&quot; alt=&quot;连线图解&quot;&gt;&lt;/p&gt;
&lt;h3&gt;硬件拼装接线方案&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;电机接口&lt;/strong&gt;：四个电机接口 &lt;code&gt;M1, M2, M3, M4&lt;/code&gt; 分别对应飞机的四个螺旋桨。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;桨叶安装与电机接线&lt;/strong&gt;：一定要注意正反桨的安装顺序！通常是对角线电机转向相同（比如 M1/M3 顺时针，M2/M4 逆时针），桨叶也要对应安装（A桨配顺时针电机，B桨配逆时针电机）。装反了，飞机就会原地打转或者直接栽地里。另外，我们使用的全部都是&lt;strong&gt;红蓝线电机&lt;/strong&gt;，因为现在所卖的黑线电机的转向与程序定义不符，如果想要使用&lt;strong&gt;黑白线电机则需要手动将两根线的线序进行交换&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/drone.jpeg&quot; alt=&quot;小飞机&quot;&gt;&lt;/p&gt;
&lt;h2&gt;软件架构&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;姿态解算（AHRS）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;飞机要飞得稳，首先得知道自己“歪”了没。我们使用 &lt;strong&gt;MPU6050&lt;/strong&gt; 六轴传感器（三轴加速度计 + 三轴陀螺仪）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;获取数据&lt;/strong&gt;：通过 I2C 总线，我们不断读取 MPU6050 的原始数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互补滤波/卡尔曼滤波&lt;/strong&gt;：原始数据是有噪声的。加速度计在剧烈运动时不可信，陀螺仪会有温漂。我们需要把两者结合起来，算出一个干净、准确的&lt;strong&gt;欧拉角&lt;/strong&gt;（Pitch, Roll, Yaw）。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;PID 控制&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这是整篇博客最核心的算法设计。PID（比例-积分-微分）控制器的设计是让飞机对抗重力和扰动的一个最重要的环节。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;外环（角度环）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标是遥控器给的期望角度（比如我想让飞机向前倾 10 度）。&lt;/li&gt;
&lt;li&gt;输入是当前的角度，输出是期望的角速度。&lt;/li&gt;
&lt;li&gt;简单说：我想让飞机歪一点，PID 算出需要多快的转动速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内环（角速度环）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标是外环给的期望角速度。&lt;/li&gt;
&lt;li&gt;输入是陀螺仪实测的角速度，输出是直接给电机的 PWM 油门值。&lt;/li&gt;
&lt;li&gt;简单说：为了达到那个转动速度，我要给电机加多大的劲儿。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/main.png&quot; alt=&quot;框架&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;电机混控&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;算出了 Pitch、Roll、Yaw 三个轴需要的控制量后，如何分配给四个电机？这就需要&lt;strong&gt;混控算法&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;motor1 = throttle + pitch_out + roll_out - yaw_out;
motor2 = throttle + pitch_out - roll_out + yaw_out;
motor3 = throttle - pitch_out - roll_out - yaw_out;
motor4 = throttle - pitch_out + roll_out + yaw_out;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（注：具体正负号取决于你的电机布局和转向定义，这里仅为示意）
&lt;/p&gt;
&lt;h2&gt;上位机调试&lt;/h2&gt;
&lt;p&gt;在实现飞行的时候，我们很难肉眼看出 PID 参数的好坏。这时候就需要&lt;strong&gt;上位机&lt;/strong&gt;（Ground Control Station）来帮忙。推荐使用&lt;strong&gt;匿名上位机 (AnoTC)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/anotc.png&quot; alt=&quot;上位机&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;连接&lt;/strong&gt;：通过 USB 或 Wi-Fi 数传连接 ESP32。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;波形观察&lt;/strong&gt;：在上位机里，你可以看到实时的姿态曲线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数在线调整&lt;/strong&gt;：不用每次改参数都重新烧录代码。通过上位机协议，我们可以实时修改 PID 参数，实时看效果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./image/anotc_pid.png&quot; alt=&quot;上位机1&quot;&gt;&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;从一行编译烧录命令到飞机第一次颤颤巍巍地离地，这中间可能会经历无数次的炸机、断桨和代码报错。但当你亲手写的算法让飞行器稳稳地悬停在空中的那一刻，那种成就感是无与伦比的。&lt;/p&gt;
&lt;p&gt;这篇博客主要带大家理清了 ESP32 无人机开发的脉络。后续我会在对应的位置补充更多实战图片和详细的接线图。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.h2t7fPYM.jpg"/><enclosure url="/_astro/heroimage.h2t7fPYM.jpg"/></item><item><title>Paper Reading：Robot learning 3</title><link>https://xiaohei-blog.vercel.app/blog/paper-reading-3</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/paper-reading-3</guid><description>我要和他们站在一起。</description><pubDate>Fri, 15 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;
import { RatingCriteria, ArxivRating } from &apos;@/components/advanced&apos;&lt;/p&gt;
&lt;h2&gt;Large-scale RL Exploration&lt;/h2&gt;
&lt;h2&gt;Agile Flight from Pixels&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;ArxivRating
id=&apos;Agile Flight from Pixels&apos;
url=&apos;http://www.roboticsproceedings.org/rss20/p082.pdf&apos;
tldr=&apos;非对称演员-评论家+门边缘视觉抽象+无状态估计像素控制&apos;
rank={5}&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;p&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;  ![show1](./image/framework1.png)
  该文实现首个无显式状态估计的四旋翼视觉敏捷飞行系统，采用非对称演员-评论家框架让评论家获取特权状态信息提升训练效果，以赛道门内边缘作为视觉抽象简化像素级RL训练，搭配SwinTransformer门检测器，仅靠机载摄像头视频流即可实现最高40km/h、2g加速度的竞速飞行，完成零样本仿真到现实迁移。
&amp;#x3C;/p&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HOLA-Drone&lt;/h2&gt;
&lt;h2&gt;OmniDrones&lt;/h2&gt;
&lt;h2&gt;Multi-UAV Pursuit-Evasion&lt;/h2&gt;
&lt;h2&gt;PKCC&lt;/h2&gt;
&lt;h2&gt;Pixel Motion&lt;/h2&gt;
&lt;h2&gt;Simple Flight&lt;/h2&gt;
&lt;h2&gt;Whole-Body Control Gap&lt;/h2&gt;
&lt;h2&gt;YOPO&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;ArxivRating
id=&apos;YOPO&apos;
url=&apos;https://ieeexplore.ieee.org/document/10528860&apos;
tldr=&apos;单阶段规划+引导学习+运动基元，四旋翼无地图实时轨迹生成&apos;
rank={5}&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;p&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;  ![show9](./image/framework9.png)
  四旋翼单阶段学习规划器YOPO，将感知、路径搜索、轨迹优化融合为单一网络，以运动基元覆盖规划解空间，创新引导学习方法利用数值梯度训练网络，推理时延仅1.6ms，仿真与实机实验中实现复杂森林环境高速安全飞行，性能优于传统梯度优化方法。
&amp;#x3C;/p&gt;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/heroimage.CnrNGdDU.jpg"/><enclosure url="/_astro/heroimage.CnrNGdDU.jpg"/></item><item><title>Paper Reading：Robot learning 2</title><link>https://xiaohei-blog.vercel.app/blog/paper-reading-2</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/paper-reading-2</guid><description>我要和他们站在一起。</description><pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;
import { RatingCriteria, ArxivRating } from &apos;@/components/advanced&apos;&lt;/p&gt;
&lt;h2&gt;Planar Pose Graph&lt;/h2&gt;
&lt;h2&gt;Whole-Body Scale Optimization&lt;/h2&gt;
&lt;h2&gt;UAV Payload Transportation&lt;/h2&gt;
&lt;h2&gt;Robotic Relative Localization&lt;/h2&gt;
&lt;h2&gt;Path Planning&lt;/h2&gt;
&lt;h2&gt;GS-Planner&lt;/h2&gt;
&lt;h2&gt;Back to Newton&apos;s Laws&lt;/h2&gt;
&lt;h2&gt;ARiADNE&lt;/h2&gt;
&lt;h2&gt;CTSAC&lt;/h2&gt;
&lt;h2&gt;DARE&lt;/h2&gt;</content:encoded><h:img src="/_astro/heroimage.BxnIeE2d.jpg"/><enclosure url="/_astro/heroimage.BxnIeE2d.jpg"/></item><item><title>Paper Reading：Robot learning 1</title><link>https://xiaohei-blog.vercel.app/blog/paper-reading-1</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/paper-reading-1</guid><description>我要和他们站在一起。</description><pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;
import { RatingCriteria, ArxivRating } from &apos;@/components/advanced&apos;&lt;/p&gt;
&lt;h2&gt;learning-based 电机故障控制&lt;/h2&gt;
&lt;h2&gt;TACO&lt;/h2&gt;
&lt;h2&gt;End-to-end Learning Approach&lt;/h2&gt;
&lt;h2&gt;LiDAR-based Quadrotor&lt;/h2&gt;
&lt;h2&gt;ROG-Map&lt;/h2&gt;
&lt;h2&gt;STAF-Navi&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;ArxivRating
id=&apos;STAF-Navi&apos;
url=&apos;https://ieeexplore.ieee.org/document/11217207&apos;
tldr=&apos;基于DRL的无人机导航框架，集成了内部记忆和增强的感知能力&apos;
rank={2}&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;p&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;  ![show5](./image/framework5.png)
  Single-Station-Transformer-Actor (SSTA) model（单站Transformer执行器模型），该模型将位置编码与基于Transformer的注意力机制相结合，以融合随时间变化的深度图像输入和低维状态信息。基于SSTA的执行器（SSTA-based actor）和基于GRU的评价器（GRU-based critic）都处理历史数据，使得智能体能够整合来自过去观测和动作的上下文信息，最终使无人机能够执行需要多步规划和持续反馈的扩展任务。
&amp;#x3C;/p&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flight in Clutter&lt;/h2&gt;
&lt;h2&gt;Drone Swarm&lt;/h2&gt;
&lt;h2&gt;STD-Trees&lt;/h2&gt;
&lt;h2&gt;Radar Cross-Modal Diffusion Model&lt;/h2&gt;</content:encoded><h:img src="/_astro/heroimage.BTiOoGhT.jpg"/><enclosure url="/_astro/heroimage.BTiOoGhT.jpg"/></item><item><title>无人机的 RL 端到端方法</title><link>https://xiaohei-blog.vercel.app/blog/uav-rl</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/uav-rl</guid><description>更偏向大研究方向的介绍</description><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;
import { RatingCriteria, ArxivRating } from &apos;@/components/advanced&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;h2&gt;传统方法实现无人机自主导航和智能飞行&lt;/h2&gt;
&lt;p&gt;自主导航是无人机自主化的关键基石，是需要攻关的核心技术。传统无人机自主导航框架主要如下所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/framework.png&quot; alt=&quot;frame&quot;&gt;
&lt;img src=&quot;./image/framework1.png&quot; alt=&quot;frame&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;状态估计&lt;/strong&gt;：无人机根据环境中的图片或其他传感器信息去推算出自己当前的一个状态，这个状态包括了位置、姿态、速度及角速度等高阶的运动状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;环境感知&lt;/strong&gt;：基于估计出的状态，再次结合环境中采集到的传感器数据，做一个对环境的三维重建，让无人机知道哪些地方可以通行哪些可能是这次任务感兴趣的兴趣点，构建出一个三维的高精度的态势地图。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运动规划&lt;/strong&gt;：基于构建出的地图和自身状态的推断来做运动规划，这个规划要有两个要求一个是安全（无人机不能撞到东西），一个是可行（这个规划出的路径要是物理有意义的）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运动控制&lt;/strong&gt;：保证我的无人机处于一个平稳飞行状态，速度要达到期望的速度，姿态要尽可能的平稳。在机载计算载板计算完成之后下发到整个电机，具体转速的指令通过底层飞控芯片发下去。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;something else&lt;/strong&gt;：可以看到这些都是一些模块化开发的东西，彼此之间是比较独立的。坏处也很明显，模块和模块之间很难协同优化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;RL端到端和传统方法的区别&lt;/h2&gt;
&lt;p&gt;强化学习的方法没有，先进行感知构建一个地图再在上面进行轨迹规划，再沿着规划的轨迹去做控制这一个过程。实际上的一整个过程是一个闭环，是一个端到端的过程了，前面一端传感器采集数据，后面一端即为控制指令。&lt;/p&gt;
&lt;p&gt;从本质上看就是在系统层面的对导航算法的优化。与传统模块化开发的算法在一个复杂系统中，即便他达到了局部最优，那当他们连在一起时也不能说明他达到了系统上的全局最优。因为模块和模块之间的梯度是不能流淌的、不连通的。&lt;/p&gt;
&lt;p&gt;RL 端到端的方法是直接优化整个模型，用一个模型就完全完成了一端到另一端的问题，理论上限是更高的。RL 的网络更加轻量，通过利用离线大量算力和计算时间，获得在线的推.理效率。（传统方法需要有很强的在线的优化和专用的数学求解工具），那么 RL 就可以得到更高的计算效率和更低的对机载芯片的功耗和造价的要求。&lt;/p&gt;
&lt;p&gt;传统方法的分模块化设计会出现累计误差（例如视觉传感器，他一定会有噪声和定位误差的，那么在后面建图与规划的过程中误差就会累计，让整个系统的表现变差）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统方法的优势&lt;/strong&gt;：每个模块都是精心设计的，是透明的，可解释性强，易于分模块调试（知道问题出现在哪个模块上）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RL 方法的劣势&lt;/strong&gt;：黑盒，整个系统就是一个模型，可能系统全部都能 work，也可能全部都不能 work，调试开发难度大。sim to real 和 real to sim 需要大量的工程测试和一些实际部署上的技巧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/compare.png&quot; alt=&quot;compare&quot;&gt;&lt;/p&gt;
&lt;h2&gt;结合传统方法与强化学习（更完备、泛化性更强）&lt;/h2&gt;
&lt;p&gt;把强化学习网络的一部分换成一个轨迹规划器（比以往完全是一个黑盒要好）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/work1.png&quot; alt=&quot;work1&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果是原来完全强化学习的方法，更改目标函数之后可能就会要重新去训练强化学习网络。但现在我引入了轨迹优化的方法，就可以只通过改变轨迹优化的目标函数来实现对网络的调整。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基于强化学习的自适应导航框架&lt;/strong&gt;：强化学习输出的是轨迹优化所需要的参数，具体来说是最大速度（速度上限）。&lt;strong&gt;用强化学习做无人机飞行的自动调速器&lt;/strong&gt;（当感知到障碍物环境复杂和视觉盲区的时候无人机是可以稍微降低速度的，保证安全）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/work.png&quot; alt=&quot;work&quot;&gt;&lt;/p&gt;
&lt;h2&gt;强化学习在无人机追逃博弈中的应用&lt;/h2&gt;
&lt;p&gt;博弈的基本要素与概念。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/compate.png&quot; alt=&quot;boyi&quot;&gt;
&lt;img src=&quot;./image/compate1.png&quot; alt=&quot;boyi1&quot;&gt;
&lt;img src=&quot;./image/compate2.png&quot; alt=&quot;boyi2&quot;&gt;
&lt;img src=&quot;./image/compate3.png&quot; alt=&quot;boyi3&quot;&gt;&lt;/p&gt;
&lt;h3&gt;经典 RL + 博弈算法 —— PSRO&lt;/h3&gt;
&lt;p&gt;训练出的 RL 算法要具有泛化性，注意在训练的时候不要出现过拟合的现象（不是针对特定对手策略的过拟合）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PSRO 的流程&lt;/strong&gt;：他不是单一的追逐者策略和逃跑者策略，而是维护了一个策略池，相当于有多个不同的追逐者，和多个不同的逃跑者，在训练其中一方时，我们从对手的策略池中，采样一个作为对手，这样就保持了对手的多样性，从对手的策略池中采样的这样一个策略。固定对手此时的策略，然后不断的训练我方策略直至收敛，使该策略成为在别的玩家策略不变的情况下，近似的最优的一个响应，然后将其加入该玩家的策略池中，这样来维护策略池的多样性。我们就在不断的博弈训练中，既提高了逃跑者也提高了追逐者的水平，并通过对手策略多样性保证自己的策略是通用的，有更好的泛化性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/psro1.png&quot; alt=&quot;psro&quot;&gt;
&lt;img src=&quot;./image/psro2.png&quot; alt=&quot;psro1&quot;&gt;&lt;/p&gt;
&lt;h2&gt;强化学习在大机动飞行中的应用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;基于视觉的窄缝穿越&lt;/strong&gt;：网络输入图片，通过一个视觉的 &lt;code&gt;encoder&lt;/code&gt; 编码器进行解析编码，最后直接输出控制指令（角速度和推力），一个难点是我们输入的图片是高维的，直接让网络同时去学习图片的感知理解与高精度运动控制是很难的，需要大量的数据，同时对解空间的探索效率是很低的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/fly1.png&quot; alt=&quot;fly&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法&lt;/strong&gt;：用 &lt;code&gt;teach-student&lt;/code&gt; 这个框架，把希望无人机穿越的框（窄缝）的角点提取出来，去训练一个 &lt;code&gt;teacher&lt;/code&gt;，它只需要关注无人机的运动控制，然后通过在线蒸馏，去监督 &lt;code&gt;teacher&lt;/code&gt; 的输出和 &lt;code&gt;student&lt;/code&gt; 的输出，得到一个真正要使用的 &lt;code&gt;student&lt;/code&gt; 网络。这种方法可以提供一个更加明确的进化方向，显著提升网络的学习效率。并在网络结构上使用 &lt;code&gt;GRU&lt;/code&gt;（一个 &lt;code&gt;RNN&lt;/code&gt; 的网络结构），使训练出的 policy 对感知数据是有记忆的（因为这个网络里需要隐式的做窄缝的估计和识别），得到的多帧信息要比单帧信息要好，也可凭借之前的记忆（之前的观测），去完成后续的动作。&lt;/p&gt;
&lt;p&gt;实现无人机特技飞行——自动逆课程学习（ARCL）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/fly2.png&quot; alt=&quot;fly1&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CKByLajT.jpg"/><enclosure url="/_astro/heroimage.CKByLajT.jpg"/></item><item><title>自主无人机的感知模块配置教程</title><link>https://xiaohei-blog.vercel.app/blog/learning-base-uav</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/learning-base-uav</guid><description>开源无人机硬件项目中关于感知模块的连接配置。</description><pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { StepIndent } from &apos;@/components/user&apos;
import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;感知模块的配置，往往是无人机开发中最考验人心性的环节。它不像飞控算法那样有确定的数学推导，也不像机械结构那样所见即所得，它更像是一场与软硬件兼容性、通信协议以及物理连接的漫长博弈。通俗的讲这个过程其实并不涉及什么很深奥的原理和推导，更多的其实就是有人手把手的带你走过一遍就可以，然后你就什么都懂了。&lt;/p&gt;
&lt;p&gt;在对自主无人机感知模块的搭建过程中，你可能会在“线没插好”和“驱动没装对”之间反复横跳。三小时前，你可能还对着散落在桌面的航空插头、稳压模块和杂乱的线束感到迷茫；三小时后，当 RViz 里终于跳动起第一帧清晰的点云时，那种心跳与帧率共振的喜悦又会让你觉得一切都值得。&lt;/p&gt;
&lt;p&gt;这篇文章并不是一份枯燥的参数说明书，而是一份**“心法”与“招式”并重**的实战复盘。我将从最基础的物理连接讲起，贯穿 NUC 环境配置、ROS 驱动安装，直到 Mid-360 跑通 FAST-LIO 建图与 Octomap 栅格化。希望这份记录能成为你我未来的备忘录：在面对纷繁复杂的报错时，不要慌张，按图索骥。&lt;/p&gt;
&lt;h2&gt;Part 1：硬件组装&lt;/h2&gt;
&lt;p&gt;我们先来学习一下各个传感器与主要的算力设备之间是怎样实现通信连接的，让我们先明白每个部分的位置和数据流过程。&lt;/p&gt;
&lt;h3&gt;NUC 与飞控模块连接&lt;/h3&gt;
&lt;p&gt;用飞控连接线（双左弯 0.15m）USB一端接 NUC ，另一端接飞控 Type-C 接口。这样就能实现飞控和机载电脑的通信。接线的话就先不放图了，选对线插上去就行。&lt;/p&gt;
&lt;h3&gt;双目深度相机（Realsense D435i）与 NUC 连接&lt;/h3&gt;
&lt;p&gt;直接使用 USB 转 Type-C 线，把深度相机连到 NUC 上就行，但注意做 VINS 类型的算法的话最好选一根支持 USB 3.0 的线，因为其他协议类型的线在运行 VINS 时可能会出现数据量传输不够的情况，导致定位丢失。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/camera.png&quot; alt=&quot;相机到NUC&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Mid-360 雷达供电与机载电脑连接&lt;/h3&gt;
&lt;p&gt;雷达接口时一个12P的航空接头，其中8路为信号线，其中4路为网口线接到机载电脑NUC上，另外预留的4个功能线可接GPS等其他设备（也可不接任何东西），还有2路为电源线，可以根据所选电池电压来选择直接接到电池上还是接到稳压模块上。当然，如果比较有条件的情况下，还是推荐把雷达的航空接头给改一下，这样对机器人的轻量化和供电的方便性上会很有优势，相关的教程在B站上有很多，你可以搜索一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/lidar.png&quot; alt=&quot;MID360&quot;&gt;
&lt;img src=&quot;./image/lidar1.png&quot; alt=&quot;MID360_1&quot;&gt;
&lt;img src=&quot;./image/lidar2.png&quot; alt=&quot;MID360_2&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Part 2：机载电脑（NUC）配置&lt;/h2&gt;
&lt;h3&gt;Ubuntu 20.04 系统安装&lt;/h3&gt;
&lt;p&gt;拿到一个新的空系统的 NUC，我们首先要解决的就是装系统的问题，这对小白来说听起来好像很复杂，但实际上按照操作来还是很有逻辑的，只要不乱动一些东西步奏还是比较清晰的。没啥好怕的，跟着教程来试试吧。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载 Ubuntu 20.04 镜像&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以从&lt;a href=&quot;https://mirrors.aliyun.com/ubuntu-releases/20.04/&quot;&gt;阿里云镜像站&lt;/a&gt;下载 ubuntu-20.04.6-desktop-amd64.iso的.iso镜像文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/ubuntu20.png&quot; alt=&quot;ubuntu20.04&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;烧录镜像到 U 盘&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先准备一个空 U 盘，容量尽量在8G以上，在电脑中右键格式化 U 盘（卷标写不写都行），目的其实就是清空 U 盘，如果是一个全新的 U 盘也可以不格式化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/u1.png&quot; alt=&quot;u盘&quot;&gt;
&lt;img src=&quot;./image/u2.png&quot; alt=&quot;u盘1&quot;&gt;&lt;/p&gt;
&lt;p&gt;下载 &lt;a href=&quot;https://www.ultraiso.com/download.html&quot;&gt;UltraISO&lt;/a&gt;软件，用它来将下载好的镜像烧写到U盘中（一定要选中你插入的U盘）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/iso.png&quot; alt=&quot;iso&quot;&gt;
&lt;img src=&quot;./image/iso_d.png&quot; alt=&quot;iso1&quot;&gt;
&lt;img src=&quot;./image/iso_s.png&quot; alt=&quot;iso2&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;安装 Ubuntu&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;NUC 插上电源、U 盘、鼠标键盘，按电源键开机。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/show.png&quot; alt=&quot;安装接线&quot;&gt;&lt;/p&gt;
&lt;p&gt;看到安装界面后选择 &lt;strong&gt;Install Ubuntu&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/install.png&quot; alt=&quot;install&quot;&gt;&lt;/p&gt;
&lt;p&gt;在安装过程中，语言建议选择中文，初期为了加快安装速度可以暂时不联网，选择正常安装即可。对于新设备来说，最关键的一步是&lt;strong&gt;创建分区&lt;/strong&gt;，如果你之前没有经验，可以参考 B 站上相关的 Ubuntu 安装教程或者我下面这张图中展示的分区建议来操作。之后按提示选择时区、设置用户名和密码，这里强烈建议勾选&lt;strong&gt;自动登录&lt;/strong&gt;，这样可以省去每次开机输密码的繁琐，对后续跑自动启动脚本也更友好。全部设置完成后等待安装结束，重启进入桌面，你就拥有了一台崭新的 Ubuntu 机器了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/swap.png&quot; alt=&quot;分区建议&quot;&gt;&lt;/p&gt;
&lt;h3&gt;WiFi的连接问题&lt;/h3&gt;
&lt;p&gt;因为 Ubuntu 20.04已经出来挺长时间了，并且可能也马上就不在维护了，所以他对Intel AX211的网卡可能会出现不识别的问题，所以刚装好系统后会出现WiFi无法连接的问题。但是不要慌，总是会有办法的，下面我们再来解决这个问题吧。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用另一台能上网的电脑从&lt;a href=&quot;https://www.asus.com.cn/displays-desktops/nucs/nuc-mini-pcs/asus-nuc-13-pro/helpdesk_bios?model2Name=ASUS-NUC-13-Pro-Mini-PC-NUC13ANK&quot;&gt;ASUS NUC&lt;/a&gt;下载 NUC13 最新的 BIOS 0033，解压缩放在 U 盘后取下 U 盘，然后将 U 盘插到 NUC 上。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./image/bios.png&quot; alt=&quot;bios&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;NUC 开机按 &lt;strong&gt;F7&lt;/strong&gt; 选定 U 盘上 bios 文件 0033 中 &amp;#x3C; Capsue File forCFlash through F7&gt; 后回车。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启按 &lt;strong&gt;F2&lt;/strong&gt; 进 BIOS，把 &lt;code&gt;Boot → Secure Boot&lt;/code&gt; 设为 &lt;strong&gt;Disable&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用一根USB连接线连接手机和NUC，在手机设置中找到通过USB连接线进行网络共享，使用手机为NUC提供网络才可以进行后续操作（没有网络的话NUC无法与国内服务器进行通信，无法完成任何命令）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后在这个&lt;a href=&quot;https://launchpad.net/ubuntu/+source/backport-iwlwifi-dkms&quot;&gt;网址&lt;/a&gt;下载驱动 backport-iwlwifi-dkms package : Ubuntu。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./image/wifi.png&quot; alt=&quot;wifi驱动&quot;&gt;&lt;/p&gt;
&lt;p&gt;下载之后，在文件的同级目录安装该驱动。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo dpkg -i backport-iwlwifi-dkms_9858-0ubuntu3_all.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装的时候如果报以下错误：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/fix.png&quot; alt=&quot;wifi驱动fix&quot;&gt;&lt;/p&gt;
&lt;p&gt;运行下方的命令。之后再次重复上方步奏，再次安装驱动即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt --fix-broken install
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;最后跑一次：&lt;code&gt;sudo apt update&lt;/code&gt; 和 &lt;code&gt;sudo apt upgrade&lt;/code&gt;即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Part 3：机载电脑 NUC 环境配置&lt;/h2&gt;
&lt;p&gt;搞定了硬件连接和系统安装，接下来就是安装一些必不可少的组件，让我们的后续开发更顺利。这一部分可能会涉及到很多终端命令，建议细心拷贝，避免出错。&lt;/p&gt;
&lt;h3&gt;ROS 安装与测试&lt;/h3&gt;
&lt;p&gt;对于 Ubuntu 20.04 系统，我们需要安装的是 ROS Noetic 版本。安装过程其实就是添加源、添加密钥、更新列表然后安装桌面完整版。建议没有 ROS 基础的同学可以根据我的项目&lt;a href=&quot;https://my.feishu.cn/wiki/Bj2qwldHtiVJEEkovbpc3mGmnQb?from=from_copylink&quot;&gt;ROS机器人开发学习库&lt;/a&gt;的 ROS 教程，跟着每一个小demo边做边学，磨刀不误砍柴工。&lt;/p&gt;
&lt;p&gt;依次在终端执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 添加源
sudo sh -c &apos;echo &quot;deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main&quot; &gt; /etc/apt/sources.list.d/ros-latest.list&apos;

# 添加密钥
sudo apt-key adv --keyserver &apos;hkp://keyserver.ubuntu.com:80&apos; --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654

# 安装
sudo apt update
sudo apt install ros-noetic-desktop-full

# 设置环境变量
echo &quot;source /opt/ros/noetic/setup.bash&quot; &gt;&gt; ~/.bashrc
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，我们通过经典的小乌龟案例来测试一下。打开三个终端窗口，分别输入 &lt;code&gt;roscore&lt;/code&gt;（启动 ROS 主节点）、&lt;code&gt;rosrun turtlesim turtlesim_node&lt;/code&gt;（启动乌龟仿真节点）和 &lt;code&gt;rosrun turtlesim turtle_teleop_key&lt;/code&gt;（启动键盘控制节点）。如果能用键盘方向键控制小乌龟移动，恭喜你，ROS 安装成功！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/turtle.png&quot; alt=&quot;小乌龟&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Realsense 驱动与 Mavros 安装&lt;/h3&gt;
&lt;p&gt;ROS 装好后，我们要让 NUC 能驱动双目相机，并能与飞控通信。这里我们需要安装 Realsense 的 SDK 以及 Mavros 包。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Realsense 驱动安装&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同样是添加密钥和源，然后安装必要的依赖包：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 添加密钥（如果有报错可以用 || 后面的备选命令）
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key F6E65AC044F831AC80A06380C8B3A55A6F3EFCDE || sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-key F6E65AC044F831AC80A06380C8B3A55A6F3EFCDE

# 添加源
sudo add-apt-repository &quot;deb https://librealsense.intel.com/Debian/apt-repo $(lsb_release -cs) main&quot; -u

# 安装库和工具
sudo apt-get install librealsense2-dkms
sudo apt-get install librealsense2-utils
sudo apt-get install librealsense2-dev
sudo apt-get install librealsense2-dbg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，输入 &lt;code&gt;realsense-viewer&lt;/code&gt; 启动测试工具。&lt;strong&gt;特别注意&lt;/strong&gt;：查看软件左上角显示的连接状态，必须显示为 &lt;strong&gt;USB 3.x&lt;/strong&gt;。如果是 2.x，请检查你的线材是否支持 3.0，或者是否插在了 NUC 的蓝色 USB 口上（3.0 的线和口都是蓝色的）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/realsense.png&quot; alt=&quot;realsense&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Mavros 安装&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Mavros 是 ROS 与 MAVLink 协议之间的桥梁,将来要通过 Mavros 来连接机载电脑与飞控的通信。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 执行这些命令可能会要等的时间比较长，请耐心等待
sudo apt-get install ros-noetic-mavros
cd /opt/ros/noetic/lib/mavros
sudo ./install_geographiclib_datasets.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Part 4：Mid-360 雷达配置 &amp;#x26; FAST-LIO 建图&lt;/h2&gt;
&lt;h3&gt;Mid-360 雷达驱动配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;安装 &lt;a href=&quot;https://github.com/Livox-SDK/Livox-SDK2&quot;&gt;Livox-SDK2&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/Livox-SDK/Livox-SDK2.git
cd ./Livox-SDK2/
mkdir build
cd build
cmake .. &amp;#x26;&amp;#x26; make -j
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;安装 &lt;a href=&quot;https://github.com/Livox-SDK/livox_ros_driver2&quot;&gt;livox_ros_driver2&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/Livox-SDK/livox_ros_driver2.git ws_livox/src/livox_ros_driver2
cd livox_ros_driver2
source /opt/ros/noetic/setup.sh
./build.sh ROS1 
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;连接 Mid-360 与 NUC&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先将雷达的连接线分别插到对应的接口上，然后打开系统设置，跟着我下面的图片步奏一步一步做就可以。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/set1.png&quot; alt=&quot;lidar_set&quot;&gt;
&lt;img src=&quot;./image/set2.png&quot; alt=&quot;lidar_set2&quot;&gt;&lt;/p&gt;
&lt;p&gt;之后修改 &lt;code&gt;/src/livox_ros_driver2/config/MID360_config.json&lt;/code&gt; 文件中对应的 IP 地址。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/config.png&quot; alt=&quot;lidar_config&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;运行 launch 文件检查是否安装成功&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 &lt;code&gt;[work_space]&lt;/code&gt; 的工作空间下，运行下面的 &lt;code&gt;roslaunch&lt;/code&gt; 程序，检查是否有点云图生成，则雷达配置成功。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/point.png&quot; alt=&quot;lidar_point&quot;&gt;&lt;/p&gt;
&lt;h3&gt;实现 FAST-LIO&lt;/h3&gt;
&lt;p&gt;上面的步奏只是让雷达驱动起来，但是他并没有什么很强的实用价值。要想让雷达真正作为机器人的感知设备，那就不得不用大名鼎鼎的 &lt;code&gt;FAST-LIO&lt;/code&gt; 算法了，SLAM领域毋庸置疑的皇冠之星。&lt;/p&gt;
&lt;p&gt;实现 FAST-LIO 最好不要新建工作空间，为了减少麻烦，建议直接装在雷达配置时使用的工作空间里。首先在 &lt;code&gt;[work_space]/src/&lt;/code&gt; 文件夹中下载 FAST_LIO 源码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/hku-mars/FAST_LIO.git
cd FAST_LIO
git submodule update --init
cd ../..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在你就遇到了第一个比较坑的点了，现在 FAST_LIO 源码里使用的还是 &lt;code&gt;livox_ros_driver&lt;/code&gt;，但你实际用的是 &lt;code&gt;livox_ros_driver2&lt;/code&gt;。所以在 CMake 编译时需要按教程把对应引用改掉。具体修改的点如下几张图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/fix1.png&quot; alt=&quot;lio_fix1&quot;&gt;
&lt;img src=&quot;./image/fix2.png&quot; alt=&quot;lio_fix2&quot;&gt;
&lt;img src=&quot;./image/fix3.png&quot; alt=&quot;lio_fix3&quot;&gt;
&lt;img src=&quot;./image/fix4.png&quot; alt=&quot;lio_fix4&quot;&gt;&lt;/p&gt;
&lt;p&gt;修改完成之后就可以开始编译了。 FAST_LIO 的编译依赖于 livox_ros_driver2 包，因此需要确保 livox_ros_driver2 在编译 FAST_LIO 之前编译，然后再编译整个工程。如果 livox_ros_driver2 还未编译，需要先将 FAST_LIO 移出 src 文件夹，编译 livox_ros_driver2 后再将 FAST_LIO 移回来。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;catkin_make
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译成功之后，进行运行测试，要打开两个终端，先在终端 1 中运行如下命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source devel/setup.bash
roslaunch livox_ros_driver2 msg_MID360.launch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要关闭这个正在运行的终端，在终端 2 中运行下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source devel/setup.bash
roslaunch fast_lio mapping_mid360.launch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行成功之后，就会显示出如下的点云效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/sucess.png&quot; alt=&quot;fastlio&quot;&gt;&lt;/p&gt;
&lt;p&gt;至此对雷达的开发就基本完成，你可以在上面的这些优秀的开源算法上实现许多有意思的工作，真正能让你的机器人动起来。&lt;/p&gt;
&lt;h2&gt;Part 5：Octomap 栅格建图&lt;/h2&gt;
&lt;p&gt;FAST-LIO 跑通后的点云图虽然在视觉上非常酷炫，但在路径规划算法（如 A* 或 JPS）的眼里，那只是一堆离散的、没有明确边界的坐标点。为了让无人机能够进行自主避障和导航，我们需要将这些稀疏的点云转换成机器能理解的语言——&lt;strong&gt;占据栅格地图（Occupancy Grid Map）&lt;/strong&gt;。Octomap 就像是《我的世界》（Minecraft）一样，它通过概率更新的方式，将空间划分成一个个状态明确（占用/空闲/未知）的小方块，这是后续所有规划算法的基础。&lt;/p&gt;
&lt;h3&gt;为什么要源码安装？&lt;/h3&gt;
&lt;p&gt;虽然通过 &lt;code&gt;apt&lt;/code&gt; 安装预编译包非常省事，但在 Octomap 的配置上，我建议采用源码安装。实际开发中，我们经常需要调整地图的分辨率（Resolution）、传感器最大量程甚至是由于坐标系定义不同而修改源码中的投影逻辑。预编译的黑盒无法提供这种灵活性。此外，在源码编译时，最好手动检查并修改头文件的引用名称，这虽然繁琐，但能有效避免与系统自带旧版本库发生冲突。&lt;/p&gt;
&lt;h3&gt;启动与数据流的协奏&lt;/h3&gt;
&lt;p&gt;以下是整理好的“起飞”指令序列，建议按照此顺序分终端执行：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动传感器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先确保雷达正常工作，能在 RViz 中看到原始数据流。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd catkin_livox_ros_driver2
source devel/setup.sh
roslaunch livox_ros_driver2 rviz_MID360.launch
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;启动定位算法（位姿解算）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这是系统的核心，FAST-LIO 将为我们提供高频率的里程计信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd catkin_livox_ros_driver2
source devel/setup.bash
roslaunch livox_ros_driver2 msg_MID360.launch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保持上一个终端运行，开启新终端启动核心算法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd catkin_livox_ros_driver2
source devel/setup.bash
roslaunch fast_lio mapping_mid360.launch
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;生成栅格地图&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当定位稳定后，Octomap 节点开始接收点云和位姿，实时构建环境模型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd octomap_mapping_ws
source devel/setup.bash
roslaunch octomap_server octomap_mapping.launch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image/octmap.png&quot; alt=&quot;octmap&quot;&gt;&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;至此，我们完成了从物理世界的接线、系统底层的驱动安装，到数字世界中点云与栅格地图构建的全部流程。&lt;/p&gt;
&lt;p&gt;与科研理论的学习相比，这种偏工程实践的魅力在于，它永远不会像教程里写得那样一帆风顺。在未来的调试中，你可能会遇到 USB 供电不足导致的设备掉线，可能会遇到 WiFi 驱动莫名消失的焦虑，也可能会在 TF 坐标系变换中迷失方向。但请记住，每一个红色的 Error 报错，都是系统在试图告诉你它的真实状态。每一次解决问题的过程，都是你对机器人底层逻辑理解的一次升华。&lt;/p&gt;
&lt;p&gt;经常有人会这么说，你多看几个报错日志、多查几次文档、多做积累，做一个长期主义的人，总有一天会做到融会贯通的。希望这篇实战记录能成为你起飞前的最后一次检查清单。祝你的无人机，感知精准，飞行稳健。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.DN-SvCbu.jpg"/><enclosure url="/_astro/heroimage.DN-SvCbu.jpg"/></item><item><title>飞行机器人：从物理智能到具身智能</title><link>https://xiaohei-blog.vercel.app/blog/uav-phy2em</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/uav-phy2em</guid><description>对高飞老师演讲的引申与思考</description><pubDate>Tue, 15 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;
import { RatingCriteria, ArxivRating } from &apos;@/components/advanced&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;h2&gt;飞行机器人研究与应用发展路径&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;现阶段&lt;/strong&gt;：都是飞来飞去采集一些信息，基本仅仅是限制到这部分，不能像双臂机器人等做各种事情。在此之前关于四旋翼无人机都是构建一些模块化的功能性开发，每个模块的开发都是依据一些物理 principle 来构建的，然后构建精巧的数学模型，再设计专门的求解器去优化他，将这种范式总结叫做物理智能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;新的方案&lt;/strong&gt;：具身智能（上限较高的方案），这个机器人智能体可以主动 collect some data and get some useful information（清洗数据得到一些有用的信息，自己去学习，去 learning to evolution ，从而获得某项技能）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/path.png&quot; alt=&quot;path&quot;&gt;&lt;/p&gt;
&lt;h2&gt;研究思路&lt;/h2&gt;
&lt;p&gt;从单体感知决策到集群自主协同，从数学驱动建模优化到数据驱动学习进化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/develop.png&quot; alt=&quot;dev&quot;&gt;&lt;/p&gt;
&lt;h3&gt;轨迹规划感知：复杂环境下全状态轨迹生成&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;难点1：建模难精确（扣一个比较精确的几何形状，这个形状可能会非凸，会有最大的保留原始解空间）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/hard1.png&quot; alt=&quot;hard&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;难点2：轨迹难求解（要考虑各种各样的约束，非线性约束、耦合的约束等等）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/hard2.png&quot; alt=&quot;hard2&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;难点3：旋翼无人机特定的问题，位姿难解耦。位置和姿态对旋翼无人机来说是一个耦合的元素，但是它在特定的姿态下，就一定会有特定的加速，这个加速就会让我们的位置产生变化，所以位置和姿态是无法分开来做的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image/hard3.png&quot; alt=&quot;hard3&quot;&gt;&lt;/p&gt;
&lt;p&gt;三个难点的对应解决方法：质点模型、时空交替解耦、人为指定轨迹。&lt;/p&gt;
&lt;h3&gt;无人机动态环境下的感知规划闭环与建模&lt;/h3&gt;
&lt;p&gt;为了实现在复杂情况下从信息采集到规划的整体系统的闭环，就是要实现无人机在各种各样的场景下去飞。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/planner.png&quot; alt=&quot;plan&quot;&gt;&lt;/p&gt;
&lt;p&gt;代表性成果：主动感知规划与飞行走廊高效提取（不需要让无人机去知道哪里是障碍物，只需要让无人机知道哪里是free space，就要找到一种障碍物分布的估计）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/get.png&quot; alt=&quot;get&quot;&gt;&lt;/p&gt;
&lt;p&gt;动态感知（更多的取决于相机的进步，事件相机的使用。但是以事件相机为中心来构建自主导航无人机很难，因为事件相机有他天然的缺陷，所以高飞他们提出的一个方法是增强物体成像的灵敏度，设计了这样的一个相机）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/get2.png&quot; alt=&quot;get2&quot;&gt;&lt;/p&gt;
&lt;h3&gt;无人机集群的全自主导航&lt;/h3&gt;
&lt;p&gt;不依赖外部的设备，也不依赖提前编程，有共同的目标可进行分布式计算。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/go1.png&quot; alt=&quot;go&quot;&gt;
&lt;img src=&quot;./image/go2.png&quot; alt=&quot;go&quot;&gt;&lt;/p&gt;
&lt;h2&gt;飞行机器人技术发展路径&lt;/h2&gt;
&lt;p&gt;第一个理念就是飞行机器人它智能的获取方式的变化，我们不能让再针对每一个特定的 task 去建立精巧的数学模型，而是我们要设计一个仿真环境一些学习策略，一些信息采集、数据清洗、数据生成的。还有 sim2real 部署的方法，让无人机可以自己习得某项技能。&lt;/p&gt;
&lt;p&gt;第二个理念是我们希望他们可以有一些操作交互的能力，可以做更多的具身交互，操作执行理解（例如 VLA 、甚至 VTLA ）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/linian.png&quot; alt=&quot;nian&quot;&gt;&lt;/p&gt;
&lt;h3&gt;高飞的一些基于这些理念的工作&lt;/h3&gt;
&lt;p&gt;从“飞行的眼”到“飞行的手”（在变形时质心或转动惯量的变化，就会让这个结构的自适应控制较难实现）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/work1.png&quot; alt=&quot;work&quot;&gt;&lt;/p&gt;
&lt;p&gt;从“数学物理驱动”到“数据驱动”，让机器人习得一个技能，靠神经网络、靠learning、靠强化学习，然后让他能自主导航完成任务（传统路径规划遇到的一个问题，整个轨迹路径的求解本质上就是来解一个离散的组合优化问题或者是一个连续域的一个数值优化问题）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/work2.png&quot; alt=&quot;work1&quot;&gt;&lt;/p&gt;
&lt;p&gt;基于RL的自主导航和避障、用视觉做端到端的RL、激光雷达做端到端的RL和避障、用RL 做特技飞行（某些不是很要求非常精确的控制，反而要求更好的实时性的控制，这些还是交给RL来做好一些）（RL对于控制就像是离线的sampling-based MPC是具备一定的可解释性的，可能在数学上不是很好的证明他的完备性的瑕疵）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/work3.png&quot; alt=&quot;work2&quot;&gt;
&lt;img src=&quot;./image/work4.png&quot; alt=&quot;work3&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.BH0Sd6cZ.jpg"/><enclosure url="/_astro/heroimage.BH0Sd6cZ.jpg"/></item><item><title>XTDrone 无人机仿真平台搭建教程</title><link>https://xiaohei-blog.vercel.app/blog/xtdrone</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/xtdrone</guid><description>一份详细的 XTDrone 平台搭建教程，完整的技术路线与常见报错解决方案。</description><pubDate>Mon, 30 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { StepIndent, Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;XTDrone 是一个基于 PX4、ROS 和 Gazebo 联合构建的无人机通用仿真平台。该平台允许我们在 Gazebo 物理仿真环境中运行多机或单机的复杂飞行仿真，并通过 MAVROS 将底层的 PX4 软件在环（SITL）飞控逻辑与上层的 ROS 生态体系完美连接。借助这一完整的技术链路，开发者可以在上层进行路径规划、飞行控制、多传感器感知甚至复杂的视觉 SLAM 算法验证，而无需承担真机试飞的坠机风险与高昂成本。&lt;/p&gt;
&lt;p&gt;由于包含多个底层框架的耦合，搭建这套仿真环境最大的挑战往往不在于某个特定软件的编译安装本身，而在于如何理清系统间的版本依赖组合。例如 Ubuntu 系统的版本、ROS 的分发版、Gazebo 的迭代版本、PX4 源码的稳定发行版、MAVROS 的兼容性，以及在集成视觉算法时 OpenCV 与 cv_bridge 的底层冲突问题。如果版本搭配不当，极易陷入无穷尽的依赖地狱中。因此，这篇文章根据我实践心路历程写成一个可复现少踩坑的简单教程。&lt;/p&gt;
&lt;h2&gt;环境准备与系统配置&lt;/h2&gt;
&lt;p&gt;仿真平台的稳定运行高度依赖于底层的环境支持，对于使用 VMware 等虚拟机软件进行部署的开发者而言，优先解决网络连接、输入法配置以及磁盘容量这三个基础问题能够大幅提升后续配置的效率，&lt;strong&gt;如果你是双系统玩家或者你是纯血Linux使用者，可以跳过这一步奏&lt;/strong&gt;。网络的稳定性直接决定了各类开源依赖包能否顺利通过 APT 与 GitHub 等途径下载。由于后续涉及 Gazebo 大型物理模型库、PX4 完整源码仓库的拉取以及大量的 C++ 编译产物，适当扩充虚拟机的磁盘容量（建议分配 60GB 及以上的动态空间）能有效避免因磁盘空间不足导致的文件构建失败或系统崩溃。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;网络与通信配置&lt;/strong&gt;：
在虚拟机中安装 Ubuntu 后，首要任务是确保桥接模式或 NAT 模式下的网络通信正常。确保系统的 &lt;code&gt;apt&lt;/code&gt; 包管理器可以无阻碍地连接外网更新软件源列表，必要时可考虑更换为中科大、清华等国内高速镜像源以加速包下载过程。如果在更新源或拉取代码时受到网络限制，可能还需要配置相应的代理规则以保障全局网络联通。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;语言与输入法环境&lt;/strong&gt;：
为了开发与调试期间更顺畅地搜索报错、记录笔记和复制指令，配置原生的中文环境和合适的中文输入法十分必要。您可以根据习惯安装 Fcitx 或 IBus 框架下的搜狗或谷歌拼音输入法，配置完成后重启相关服务即可在终端与编辑器中无缝切换输入。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;存储空间规划&lt;/strong&gt;：
考虑到 ROS 的完整安装包体积较大，且源码编译将产生海量中间文件，在 VMware 等层级直接通过虚拟磁盘设置提升容量上限后，还需在 Ubuntu 内部使用 GParted 等硬盘管理工具进行分区拓展，以保证新扩容的空间被正确挂载至根目录下。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;资料索引&lt;/h2&gt;
&lt;p&gt;下面是一些你可能需要的资料，以及你可能遇到的问题的解决方法和图文教程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XTDrone 项目（Gitee 搜索）：&lt;a href=&quot;https://gitee.com/search?type=repository&amp;#x26;q=XTDrone&quot;&gt;gitee 搜索 XTDrone&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;XTDrone 基础配置文档（语雀搜索）：&lt;a href=&quot;https://www.yuque.com/search?q=%E4%BB%BF%E7%9C%9F%E5%B9%B3%E5%8F%B0%20%E5%9F%BA%E7%A1%80%E9%85%8D%E7%BD%AE%20PX4%201.13%20XTDrone&quot;&gt;yuque 搜索 仿真平台 基础配置 PX4 1.13 XTDrone&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;VMware Ubuntu 联网（知乎搜索）：&lt;a href=&quot;https://www.zhihu.com/search?q=VMware%20Ubuntu%20%E8%81%94%E7%BD%91&quot;&gt;zhihu 搜索 VMware Ubuntu 联网&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ubuntu 中文输入法（知乎搜索）：&lt;a href=&quot;https://www.zhihu.com/search?q=Ubuntu20.04%20%E4%B8%AD%E6%96%87%E8%BE%93%E5%85%A5%E6%B3%95&quot;&gt;zhihu 搜索 Ubuntu20.04 中文输入法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;VMware Ubuntu 扩容（简书搜索）：&lt;a href=&quot;https://www.jianshu.com/search?q=VMware%20Ubuntu%2020.04%20%E6%89%A9%E5%AE%B9&quot;&gt;jianshu 搜索 VMware Ubuntu 20.04 扩容&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;核心软件基础架构安装&lt;/h2&gt;
&lt;p&gt;搭建 XTDrone 的本质是建立一套自底向上的数据与控制链路。在这条链路中，基础的编译链是根基，ROS 提供了组件间通信的中间件层，而后续的各个独立仿真节点都将依赖这一整套软硬件设施。本部分涵盖了整个技术栈安装过程的核心内容。&lt;/p&gt;
&lt;h3&gt;编译依赖与 ROS Noetic 部署&lt;/h3&gt;
&lt;p&gt;一切编译操作的前提是完备的 C++ 与 Python 开发套件。首先，我们需要安装包含 &lt;code&gt;build-essential&lt;/code&gt;、&lt;code&gt;cmake&lt;/code&gt;、&lt;code&gt;ninja-build&lt;/code&gt; 等在内的一揽子基础依赖，并补充 &lt;code&gt;git&lt;/code&gt;、&lt;code&gt;wget&lt;/code&gt; 以及 &lt;code&gt;python3-pip&lt;/code&gt; 等常用的代码获取与包管理工具。完成基础工具链配置后，即可切入 ROS Noetic 的正式安装环节。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update
sudo apt install -y \
	build-essential cmake ninja-build \
	git curl wget unzip \
	python3-pip python3-venv \
	pkg-config \
	software-properties-common \
	lsb-release
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ROS Noetic 官方推荐支持于 Ubuntu 20.04，属于 ROS 1 生态中的最后一个长期支持版本。在配置时，需要将官方的 &lt;code&gt;packages.ros.org&lt;/code&gt; 软件源添加到系统的 &lt;code&gt;sources.list&lt;/code&gt; 文件中，并通过 &lt;code&gt;apt-key&lt;/code&gt; 导入可信密钥，从而顺利执行 &lt;code&gt;ros-noetic-desktop-full&lt;/code&gt; 桌面完整版的安装。完整版不仅包含了常用的通信库，还顺带集成了各类基础层仿真环境所需的可视化工具。同时，记得将相应的 &lt;code&gt;setup.bash&lt;/code&gt; 加载命令追加至用户的 &lt;code&gt;~/.bashrc&lt;/code&gt; 初始化脚本中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo sh -c &apos;echo &quot;deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main&quot; &gt; /etc/apt/sources.list.d/ros-latest.list&apos;
sudo apt-key adv --keyserver &apos;hkp://keyserver.ubuntu.com:80&apos; --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654
sudo apt update
sudo apt install -y ros-noetic-desktop-full
echo &quot;source /opt/ros/noetic/setup.bash&quot; &gt;&gt; ~/.bashrc
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用下面命令验证一下ros是否安装成功：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;roscore
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在建立名为 &lt;code&gt;catkin_ws&lt;/code&gt; 的 ROS 工作空间后，执行 &lt;code&gt;catkin_make&lt;/code&gt; 进行初次构建。这一工作空间未来将负责收容所有你编写的控制算法包以及克隆下来的自定义依赖组件。当然，如果你还不会创建工作空间，可以先用下面的命令来创建，这将是你后续其他项目的一个隔离区。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/catkin_ws/src
cd ~/catkin_ws
catkin_make
echo &quot;source ~/catkin_ws/devel/setup.bash&quot; &gt;&gt; ~/.bashrc
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Gazebo 仿真节点及 ROS 桥接与 MAVROS&lt;/h3&gt;
&lt;p&gt;部分版本的 ROS 默认安装中可能会携带与其捆绑的旧版或特供版 Gazebo。为避免兼容性问题，更推荐的做法是在清理掉不需要的版本后，针对性地安装 Gazebo 9，这在搭配 Noetic 体系时被广泛验证为极具健壮性的版本组合。为了将 Gazebo 中发生的物理运动反馈同步入 ROS 网络，必须安装一系列 &lt;code&gt;gazebo_ros_pkgs&lt;/code&gt; 插件，以及诸如 &lt;code&gt;moveit-msgs&lt;/code&gt;、&lt;code&gt;octomap-msgs&lt;/code&gt; 与各类型控制器接口库的附加依赖。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get install -y \
	ros-noetic-moveit-msgs \
	ros-noetic-object-recognition-msgs \
	ros-noetic-octomap-msgs \
	ros-noetic-camera-info-manager \
	ros-noetic-control-toolbox \
	ros-noetic-polled-camera \
	ros-noetic-controller-manager \
	ros-noetic-transmission-interface \
	ros-noetic-joint-limits-interface
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;紧接着需要着手配置 MAVROS。它可以理解为 ROS 话题体系直接对接各种硬件飞控所使用的 MAVLink 协议的一座重要桥梁。由于无人机仿真涉及到大量的地理空间运算，因此在用 APT 完成 MAVROS 软件包基础安装的同时，务必运行内部配套的地理数据集下载脚本。该脚本耗时较长，经常受限网络问题失败，需要反复执行直至完整下完相应的全球坐标补偿字典。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get install -y ros-noetic-mavros
cd /opt/ros/noetic/lib/mavros
sudo ./install_geographiclib_datasets.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PX4 SITL 与 XTDrone 源码编译&lt;/h3&gt;
&lt;p&gt;配置完外部支持库后，接下来处理 PX4 核心固件与 XTDrone 分发版源码。这里选用基于 1.13 版本甚至更高适配版的 PX4 作为主要的控制策略提供商。配置 SITL（Software In The Loop，软件在环）环境本质上是让物理机利用内部网络地址在虚拟环境中运行原本固化在飞控内部芯片上的嵌入式代码。&lt;/p&gt;
&lt;p&gt;当 XTDrone 源码仓库被顺利克隆到已建立好的工作空间后，常常需要清理旧有的中间缓存，使用特定的编译命令重新生成所需的 PX4 平台包并挂载好配套的控制话题及多机协同运行脚本。通过执行相应的 &lt;code&gt;build&lt;/code&gt; 脚本，开发者最终能够实现在终端内以键盘操作指令驱动 Gazebo 中的虚拟无人机离地并执行预定的三维动作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build.sh
./build_ros.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在此时通过 QGroundControl（QGC）这款主流地面站软件建立连接，常常会面临离线地图加载不出的问题，这一般是由于 QGC 的内置地图源处于特定网络的访问限制范围中，可以通过更换其他地图源或是调整网络代理方式来解决。&lt;/p&gt;
&lt;h2&gt;视觉 SLAM（ORB-SLAM2）接入&lt;/h2&gt;
&lt;p&gt;仅仅能让无人机起飞并接收键盘控制并不是仿真的最终目的。很多研究和工业级应用需要在无人机底盘上搭载虚拟相机并运行同步定位与建图算法，如非常经典的 ORB-SLAM2 模型。为接入此类算法，通常需要再引入三套极其重要的外部依赖库：用作特征矩阵求解的 Eigen 库、用作渲染界面与交互框架的 Pangolin 库，以及提供大量机器视觉运算的基础构件 OpenCV。&lt;/p&gt;
&lt;h3&gt;OpenCV 与 cv_bridge 版本的冲突&lt;/h3&gt;
&lt;p&gt;在融合视觉处理节点时，最常见且具有毁灭性的工程灾难就是 OpenCV 版本冲突。由于安装 ROS Noetic 时，官方通过源拉取的预编译依赖链中常常自带了一套基于其默认规则构建的 OpenCV（比如 4.2.x 系列），并以此生成了串联 ROS 图像消息机制的 &lt;code&gt;cv_bridge&lt;/code&gt;。然而，ORB-SLAM2 等大量独立工程在其内部 CMake 列表中，往往严格锁死了如 3.2.0 或其它旧版 OpenCV。这种版本上的分歧导致在最终链接（linking）或运行时出现大量的符号缺失报错，因为系统无法确定应当将程序的内存指针导向系统底层的 4.2 库还是开发者手动编译的 3.2 源码库。&lt;/p&gt;
&lt;p&gt;要彻底解决这一问题存在多套思路：
一部分开发者倾向于在整个工作空间的最前端统一 OpenCV 头文件的引用规范，自行下载包含 4.2 或所需版本的 OpenCV 源代码并手工替换掉由 ROS APT 安装自带的 &lt;code&gt;cv_bridge&lt;/code&gt; 对应编译包。而如果追求快速的工程复刻与止损出包，临时将高权限系统目录 &lt;code&gt;/usr/local/share/&lt;/code&gt; 下自己刚刚编译完成的 &lt;code&gt;cv_bridge&lt;/code&gt; 库文件直接暴力覆盖替换至 ROS 系统路径 &lt;code&gt;/opt/ros/noetic/share/&lt;/code&gt; 中也是一种被屡试不爽的常见手法。此操作虽略显粗糙且不利于跨平台迁移，但在应急验证无人机是否能将摄像头采集的图片帧传给目标检测与追踪模块这一链路测试中起到了奇效。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 紧急止血方案，慎用于生产环境
sudo cp -rf /usr/local/share/cv_bridge /opt/ros/noetic/share/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;追踪编译报错的核心方法论&lt;/h3&gt;
&lt;p&gt;在此阶段如果持续遭到编译中断，其底层的逻辑都可以归结为两类情况：一是缺头文件与宏定义，这要求开发者耐心地追溯 C++ 编译器的错误输出日志，通过定向检索寻找到确实遗漏了哪些上游依赖包并执行补充安装；二是所谓的链接库错位，排查此类问题的核心工具是 &lt;code&gt;ldd&lt;/code&gt; 命令，通过它来探测当前生成的动态链接库或是执行文件实际挂载的依赖路径，一旦发现挂载路径与自己预想的不符或是出现了多重引用导致 ABI 崩溃，便需要果断介入重新调整 CMake 文件的查找优先次序。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;经过了环境搭建、底层依赖装载、ROS 中间件串通到最终的高级算法集成这一整套庞大而繁杂的开发生命周期，成功在 Gazebo 中点亮那一台 XTDrone 无人机将是一次极其深刻的全栈工业仿真体验。希望本篇基于实际环境部署踩坑记录的技术长文能够作为清晰的参考路径，极大削减各位新手开发者在初次尝试搭建协同飞行仿真体系时的迷茫与无谓的精力损耗。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CWs5ji24.jpg"/><enclosure url="/_astro/heroimage.CWs5ji24.jpg"/></item><item><title>UAV Review</title><link>https://xiaohei-blog.vercel.app/blog/uav-review</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/uav-review</guid><description>一份面向无人机方向的速查笔记。</description><pubDate>Fri, 27 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;趁着最近有空，我把最开始接触无人机科研时所收集的各个方向的文献和技术笔记整个翻出来，做了一次系统性梳理，也构建了一个我的Mind Map。&lt;/p&gt;
&lt;p&gt;平时看论文、跑开源代码，笔记往往记得很碎。这次我试着拔高一点视角，把它们按&lt;strong&gt;方法范式&lt;/strong&gt;、&lt;strong&gt;经典任务&lt;/strong&gt;、&lt;strong&gt;平台设计&lt;/strong&gt;以及&lt;strong&gt;底层技术栈&lt;/strong&gt;重新串联起来，权当是一份面向研究与工程的速查手册。&lt;/p&gt;
&lt;h2&gt;Data-driven vs Model-based&lt;/h2&gt;
&lt;p&gt;在无人机领域，data-driven 的方法与 model-based/modular 的方法在不同任务中的优势不同，仍处于分庭抗礼的阶段。它们其实并不是非此即彼的关系，而是各自适应了不同的任务难度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么传统模块化方案依然坚挺？&lt;/strong&gt;
主要是因为无人机的动力学模型（尤其像四旋翼）在工程上不仅相对简单，而且十分容易在真实世界中进行校准。加上无人机多数任务是“穿越环境”而非“产生强物理交互”，依靠成熟的状态机、轨迹规划和底层控制优化，就能得到非常出色的飞行性能。我们日常看到的绝大多数商业无人机，底座依然是这一套。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;端到端学习赢在哪儿？&lt;/strong&gt;
传统依赖于非常精准的&lt;strong&gt;状态估计&lt;/strong&gt;和&lt;strong&gt;感知建模&lt;/strong&gt;，但在小型化平台上，受限于算力、载重和传感器噪声，整套 pipeline 经常被逼到极限。这时候，基于学习的方法（特别是用强化学习处理感知驱动的敏捷飞行）就能跳过繁琐的显式建图和状态推导，展现出超越传统路线的反应速度和鲁棒性。&lt;/p&gt;
&lt;h2&gt;支持无人机 RL 的仿真器&lt;/h2&gt;
&lt;p&gt;做深度学习/强化学习，离不开好用的仿真器，在无人机领域，有几个高频出现的“生产力工具”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://microsoft.github.io/AirSim/&quot;&gt;AirSim&lt;/a&gt;：基于虚幻引擎（UE4/UE5），视觉效果极佳，动力学仿真也很逼真。不过底层改动门槛有点高，运行帧率对大规模 RL 训练来说偏低。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/uzh-rpg/flightmare&quot;&gt;Flightmare&lt;/a&gt;：主打就是一个快，非常适合需要海量数据采样的强化学习任务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AerialGym&lt;/strong&gt;：这是一类高度针对强化学习定制的环境封装，尤其在做 Sim2Real（仿真到现实转移）的研究里非常受欢迎。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;经典技能与代表性工作&lt;/h2&gt;
&lt;p&gt;主要介绍data-driven方法在经典任务上的应用。值得一提的是，以下的工作中，出现了一些摆脱了对SLAM系统和里程计依赖的方法，而无人机最初的兴起正是依靠 SLAM /里程计系统的日益成熟，将成为无人机技能学习中有趣的进展方向。&lt;/p&gt;
&lt;h3&gt;未知场景避障与敏捷飞行&lt;/h3&gt;
&lt;p&gt;无人机怎么在充满未知障碍物的森林、废墟或者狭窄走廊里穿梭，这是一个极具代表性的难题。从早期到现在，大家想出了不少奇招：&lt;/p&gt;
&lt;p&gt;早些年受自动驾驶启发，CMU 在 ICRA 2013 就尝试用监督学习把单目图像直接映射到离散控制指令上。后来，UCB 的 CAD2RL 登场，完全依靠仿真器里的单目 RGB 图像训练，结合 Domain Randomization（域随机化），成功在真实长廊里飞了起来。&lt;/p&gt;
&lt;p&gt;后来苏黎世大学（UZH）的工作更是把这个方向推向了高潮：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/uzh-rpg/rpg_public_dronet&quot;&gt;DroNet 源码&lt;/a&gt;：巧妙借用自动驾驶汽车的数据集来教无人机输出速度指令。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/uzh-rpg/agile_autonomy&quot;&gt;Agile Autonomy 项目&lt;/a&gt;：发表在 SciRob 上，核心思路是用 DAgger 算法融合传统轨迹规划的专家数据，主张端到端网络的极低延迟可以大幅提升未知环境下的飞行速度极限。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;国内的高校在这个方向也产出了极为亮眼的工作。比如上海交大的团队（Back to Newton&apos;s Laws: Learning Vision-based Agile Flight via Differentiable Physics），提出利用可微物理模型提供一阶梯度来进行策略优化，抛弃了对显式位置速度估计的依赖，文章用低分辨率深度图，训练避障比 RL 更高效，实现高速飞行。&lt;/p&gt;
&lt;p&gt;同样的，浙江大学 FAST 实验室更是将强化学习与机载雷达结合，实现了极致的自主避障；他们最新的 &lt;a href=&quot;https://arxiv.org/abs/2503.00496&quot;&gt;Flying on Point Clouds with Reinforcement Learning&lt;/a&gt; 工作，使用机载雷达和 sim2real RL 实现自主避障。虽然学习类方法进展一日千里，但如果你去实际工程项目里转一圈会发现，&lt;a href=&quot;https://github.com/ZJU-FAST-Lab/ego-planner&quot;&gt;Ego-Planner&lt;/a&gt; 等传统的轨迹规划方案依旧是中流砥柱。原因很简单：它们在大部分场景下足够靠谱，且排查问题直观，而端到端方案的数据闭环和验证成本依然是个不小的坎。&lt;/p&gt;
&lt;h3&gt;其他经典任务实现代表性工作&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;无人机目标识别与追捕&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2409.08767&quot;&gt;HOLA-Drone: Hypergraphic Open-ended Learning for Zero-Shot Multi-Drone Cooperative Pursuit&lt;/a&gt;. Arxiv 2024, University of Manchester.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/thu-uav/Multi-UAV-pursuit-evasion&quot;&gt;Multi-UAV Pursuit-Evasion with Online Planning in Unknown Environments by Deep Reinforcement Learning&lt;/a&gt;. Arxiv 2024, THU.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;无先验地图的无人机自主探索&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/10476684&quot;&gt;Deep Reinforcement Learning-based Large-scale Robot Exploration&lt;/a&gt;. Arxiv2024, National University of Singapore (NUS). 利用注意力机制学习不同空间尺度的依赖关系，对未知区域进行隐式预测，优化已知空间探索策略，提高探索效率。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/10160565&quot;&gt;ARiADNE: A Reinforcement learning approach using Attention-based Deep Networks for Exploration&lt;/a&gt;. Arxiv2023, National University of Singapore (NUS). 学习已知不同区域在多个空间尺度上的相互依赖关系，并隐式预测探索这些区域可能获得的潜在收益。这使得代理能够安排行动顺序，以平衡在已知区域对地图进行开发/细化与探索新区域之间的自然权衡。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2410.16687&quot;&gt;DARE: Diffusion Policy for Autonomous Robot Exploration&lt;/a&gt;. Arxiv2024, National University of Singapore (NUS). DARE方法利用self-attention学习地图空间信息，并通过diffusion生成通往未知区域的轨迹，以提高自主机器人的探索效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;无人机竞速与大机动/特技飞行&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2409.00895&quot;&gt;Whole-Body Control Through Narrow Gaps From Pixels to Action&lt;/a&gt;. ICRA 2025, ZJU. 使用强化学习实现视觉端到端窄缝穿越，不需要显式的位置和速度估计，超越传统方法性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;新构型无人机设计&lt;/h2&gt;
&lt;p&gt;除了让算法变聪明，很多人也在尝试让无人机与机械臂结合或者具备变形能力。有了这些硬件构型上的创新，无人机的任务边界被大大拉宽了。&lt;/p&gt;
&lt;h3&gt;空中机械臂（Aerial Manipulator）&lt;/h3&gt;
&lt;p&gt;空中机械臂，也叫空中操作无人机，兼具无人机的快速空间移动能力和机械臂的精确操纵能力，是具身智能的一种理想载体。能飞的同时还能抓东西、操作物体。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/9462539&quot;&gt;Past, Present, and Future of Aerial Robotic Manipulators.&lt;/a&gt; TRO 2022. 空中机械臂领域目前最全的综述文章，入门了解必备。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/abstract/document/10339889&quot;&gt;Millimeter-Level Pick and Peg-in-Hole Task Achieved by Aerial Manipulator&lt;/a&gt; TRO 2023, BHU. 使用四旋翼加串联机械臂实现毫米精度 peg-in-pole 任务。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2501.06122&quot;&gt;NDOB-Based Control of a UAV with Delta-Arm Considering Manipulator Dynamics&lt;/a&gt; ICRA 2025, SYU. 使用四旋翼加并联机械臂实现毫米精度抓取。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.springer.com/article/10.1007/s10846-024-02090-7&quot;&gt;A Compact Aerial Manipulator: Design and Control for Dexterous Operations&lt;/a&gt; JIRS 2024, BHU. 用空中机械臂做一些有趣的应用，比如抓鸡蛋、开门等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;全驱动无人机（Fully-Actuated UAV）&lt;/h3&gt;
&lt;p&gt;常见的四旋翼无人机具有欠驱动特性，即位置与姿态耦合。而具有&lt;strong&gt;位置姿态解耦控制&lt;/strong&gt;的全驱动无人机，理论上更适合作为空中操作的飞行平台。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/8978486?arnumber=8978486&quot;&gt;Fully Actuated Multirotor UAVs: A Literature Review&lt;/a&gt;  RAM 2020. 全驱动无人机领域目前最全的综述文章，入门了解必备。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/7487497&quot;&gt;Design, modeling and control of an omni-directional aerial vehicle&lt;/a&gt; ICRA 2016, ETH. 第一个实现全向飞行的固定倾角全驱动无人机。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/8485627&quot;&gt;The Voliro Omniorientational Hexacopter: An Agile and Maneuverable Tiltable-Rotor Aerial Vehicle&lt;/a&gt; RAM 2018, ETH. 第一个实现全向飞行的可变倾角全驱动无人机。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2503.00785&quot;&gt;FLOAT Drone: A Fully-actuated Coaxial Aerial Robot for Close-Proximity Operations&lt;/a&gt; Arxiv 2025, ZJU. 适合近端作业的小尺寸全驱动无人机。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;可变形无人机（Deformable UAV）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/8258850&quot;&gt;Design, Modeling, and Control of an Aerial Robot DRAGON: A Dual-Rotor-Embedded Multilink Robot With the Ability of Multi-Degree-of-Freedom Aerial Transformation&lt;/a&gt;. RAL 2018，东京大学. Best paper award on UAV in ICRA 2018，多关节可变形无人机。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/8567932?arnumber=8567932&quot;&gt;The Foldable Drone: A Morphing Quadrotor That Can Squeeze and Fly&lt;/a&gt;. RAL 2019, Uzh. 四旋翼每个机臂上安装一个舵机，实现机体变形飞行。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/10044964&quot;&gt;Ring-Rotor: A Novel Retractable Ring-Shaped Quadrotor With Aerial Grasping and Transportation Capability&lt;/a&gt;. RAL 2023, ZJU. 一种可变形的环形四旋翼，可用于抓取、运输等任务。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/8794373&quot;&gt;Design and Control of a Passively Morphing Quadcopter&lt;/a&gt;. ICRA 2019, UCB. 一种被动变形的四旋翼无人机&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多模态无人机（Multi-Modal UAV）&lt;/h3&gt;
&lt;p&gt;关注多模态无人机的构型设计、运动控制以及自主导航。多模态无人机具备空中、地面、水下等多域运动能力。这不仅能解决无人机的续航问题，也能让无人机具有更多应用潜力。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.science.org/doi/10.1126/scirobotics.abf8136&quot;&gt;A bipedal walking robot that can fly, slackline, and skateboard&lt;/a&gt;. SR 2021, Caltech. 多模态空地足式机器人。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nature.com/articles/s41467-023-39018-y&quot;&gt;Multi-Modal Mobility Morphobot (M4) with appendage repurposing for locomotion plasticity enhancement&lt;/a&gt;. NC 2023, Northeastern University. 具有很多种运动模式的多模态无人机。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/10538378&quot;&gt;Skater: A Novel Bi-Modal Bi-Copter Robot for Adaptive Locomotion in Air and Diverse Terrain&lt;/a&gt;. RAL 2024, ZJU. 适应多样地形的多模态空地双旋翼无人机。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/document/9691888&quot;&gt;Autonomous and Adaptive Navigation for Terrestrial-Aerial Bimodal Vehicles&lt;/a&gt;. RAL 2022, ZJU. 实现空地多模态无人机的自主导航。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;重要的技术方案&lt;/h2&gt;
&lt;p&gt;对于任何一台无人机来说，能飞多快、多稳，下限是由状态估计（定位）系统决定的。这就绕不开最为人所熟悉的里程计和同步定位与建图（Odometry &amp;#x26; SLAM）技术。&lt;/p&gt;
&lt;p&gt;里程计（Odometry）用于为机器人实时提供定位，里程计常常基于&lt;strong&gt;扩展卡尔曼滤波（EKF）&lt;strong&gt;实现，&lt;strong&gt;融合IMU、相机、激光雷达、码盘、毫米波雷达、光流传感器&lt;/strong&gt;等等各种常用于&lt;/strong&gt;机器人位姿感知传感器&lt;/strong&gt;中的多种观测，以较高的频率实现对机器人位姿的估计。&lt;/p&gt;
&lt;p&gt;视觉惯性里程计（VIO）领域，最经典的代表莫过于港科大的 &lt;a href=&quot;https://github.com/HKUST-Aerial-Robotics/VINS-Fusion&quot;&gt;VINS-Mono / VINS-Fusion 项目&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;激光惯性里程计（LIO）这边，港大的系列工作堪称标杆，从经典的 &lt;a href=&quot;https://publications.ri.cmu.edu/storage/publications/pub_files/2014/7/Ji_LidarMapping_RSS2014_v8.pdf&quot;&gt;LOAM&lt;/a&gt;，到后来火遍全网的 &lt;a href=&quot;https://github.com/hku-mars/FAST_LIO&quot;&gt;FAST-LIO&lt;/a&gt;，再到 &lt;a href=&quot;https://github.com/hku-mars/FAST-LIVO2&quot;&gt;FAST-LIVO2&lt;/a&gt;，一步步把实时建图与定位的效率推向了新高度。&lt;/p&gt;
&lt;p&gt;另外，如果要在大型环境里长期飞行，具备回环检测的 SLAM（同时定位与建图）系统也是不可或缺的前端和后端基建。&lt;/p&gt;
&lt;p&gt;SLAM（Simultaneous Locolization And Mapping）在定位的同时完成地图的构建，使得回环（Loop Closure）检测成为可能，回环检测的存在使得当机器人重新访问到某个位置时可以修正一部分的累计误差，提高在长时间作业时的定位精度。SLAM 的实现主要有 filter-based 和 optimization-based 两种，实现中一般又分前端和后端，基于不同传感器的 SLAM 又各有其特点。&lt;/p&gt;
&lt;p&gt;除了建图算法，一些通用的机器人开发工具也是无人机研发的必备口粮：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;经典的 &lt;code&gt;ROS / ROS2&lt;/code&gt; 生态，特别是多传感器间的时间戳对齐（比如 &lt;code&gt;message_filters&lt;/code&gt; 的 &lt;code&gt;TimeSynchronizer&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;涉及到空中机械臂等强动力学场景时，大家也常会用到像 NVIDIA 的 &lt;a href=&quot;https://curobo.org/&quot;&gt;cuRobo&lt;/a&gt;（CUDA 加速碰撞检测与规划）、IKFast 或者是 &lt;a href=&quot;https://github.com/haosulab/ManiSkill&quot;&gt;ManiSkill 生态里的 mplib&lt;/a&gt; 等求解库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;方向&lt;/h2&gt;
&lt;p&gt;梳理完这些，我个人的一个最核心的感受是，Sim2Real（从仿真到现实）可能是现阶段对个人开发者或小团队来说，最容易切入并且能看到实际成果的路线。也就是说，无人机也需要迈向从物理智能到具身智能的方向的转换，后续我会在这部分再继续深入分析的，也会再写一篇 blog 进行总结。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.5QAp35mJ.jpg"/><enclosure url="/_astro/heroimage.5QAp35mJ.jpg"/></item><item><title>基于实例分割的黑色素瘤病灶区域检测系统</title><link>https://xiaohei-blog.vercel.app/blog/melanoma</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/melanoma</guid><description>Makerizon-黑色素瘤项目作品展示集</description><pubDate>Tue, 27 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside, StepIndent } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;h2&gt;项目简介&lt;/h2&gt;
&lt;p&gt;项目设计了一款用于采集恶性皮肤肿瘤图片的实验装置，主要是由摄像头、LED 柔光灯和皮肤镜组成。通过 USB 连接到电脑或树莓派等嵌入式设备上，全高清光学放大倍数高达 50 倍。由皮肤镜进行图像采集之后，将视频传输至 Qt 应用程序，在端侧进行图像的预处理，然后通过 HTTP 协议将图像传输至 ECS 服务端，在服务端有部署好的深度学习模型，进行检测，同时将数据存储至 RDS 数据库，图像归档备份至 OBS 对象存储服务。识别结果会反馈至 Qt 界面，将诊断结果可视化。在 APP 端，患者也可以查看自己的诊疗结果。识瘤者 APP 使用 ArkUI 方舟开发框架，轻量级存储等，开发效率提高 30%。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/system.png&quot; alt=&quot;系统简介&quot;&gt;&lt;/p&gt;
&lt;h2&gt;项目背景与痛点问题&lt;/h2&gt;
&lt;p&gt;皮肤是人体面积最大的器官。皮肤具有屏障作用，保持着人体内环境的稳定，同时皮肤也参与人体的代谢过程。一个成年人的皮肤展开面积在 2 平方米左右，重量约为人体重量的 16%。然而在阳光照射、化学性致癌物质、遗传等因素下会引起恶性皮肤肿瘤发病。它是一种起源于表皮基底细胞的低度恶性肿瘤。全球平均每年新发 152 万例，在所有的皮肤肿瘤中恶性皮肤肿瘤占第三位。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/bg.png&quot; alt=&quot;项目背景&quot;&gt;&lt;/p&gt;
&lt;p&gt;根据我们的调查显示随着年份的增加 社区医院的数量也在逐年增加。但在右侧，这个以泾洋中心医院为代表的社区医院中，发现高中和大专的人员比例已经达到了 63.8%，通过了解我们可以知道对于皮肤病的诊断，专业性是尤为重要的。由于恶性皮肤肿瘤早期难以区分的痛点在其早期并不能被及时发现，大部分病人在发现患病后已经处于晚期。但是恶性皮肤肿瘤的晚期五年生存率只有 4.6%，早期生存期有 89%，所以急需早发现、早治疗、早复查。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/data.png&quot; alt=&quot;项目数据&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们在进行信息搜集后发现，对于皮肤病的诊断，医院一般都会使用皮肤镜进行观察，皮肤镜易于使用、减少不必要的活检。但是这种方式存在一定的问题，比如皮肤镜中图像对比度低、肉眼难以区分。人工检查费时费力、效率低下。相关专家数量不足，大型三甲级医院医患比 1：70000，同时比较依赖专家经验。&lt;/p&gt;
&lt;h2&gt;项目架构&lt;/h2&gt;
&lt;p&gt;我们的团队就致力于解决这些问题。该检测系统是通过皮肤镜作为采集端采集病灶区域的相关信息，在设备端经过数据处理后，传输到服务端进行实例分割等操作，最后将结果返回至用户端的系统设备。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/tech.png&quot; alt=&quot;技术支持&quot;&gt;&lt;/p&gt;
&lt;p&gt;那么我们具体是怎样实现的呢？首先皮肤镜图像采集，图像传输到 Qt 应用程序，在端侧进行图像的预处理，然后发送至华为云 ECS 服务器进行识别，同时数据存储至 RDS 数据库，图像归档备份至 OBS 对象存储服务。识别结果会反馈至 Qt 界面。我们的识瘤者 APP 使用 ArkUI 方舟开发框架，轻量级存储等，开发效率提高超过 30%。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/tech2.png&quot; alt=&quot;技术架构&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/algo.png&quot; alt=&quot;算法设计&quot;&gt;&lt;/p&gt;
&lt;p&gt;视频可以实时传输至Qt界面。在确认具体病灶区域进行识别，返回病灶区域分割和类别输出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/QT.png&quot; alt=&quot;功能演示&quot;&gt;&lt;/p&gt;
&lt;p&gt;同时患者可以在鸿蒙APP中查看自己的诊疗记录。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/APP.png&quot; alt=&quot;功能演示2&quot;&gt;&lt;/p&gt;
&lt;p&gt;医生可以在网页端对病人数据进行管理，同时在网页端答题，提高医生和医院的专业水平。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/web.png&quot; alt=&quot;功能演示3&quot;&gt;&lt;/p&gt;
&lt;h2&gt;功能演示&lt;/h2&gt;
&lt;p&gt;使用 DAYU200 鸿蒙季开发板端侧部署基于 OpenHarmony 的软件交互 APP，实现可视化建议操作进行病患信息采集。保证快速上手。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;手机端拍照&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./image/show1.gif&quot; alt=&quot;功能演示4&quot;&gt;
&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;图传识别&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./image/show2.gif&quot; alt=&quot;功能演示5&quot;&gt;
&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;查看检测结果&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./image/show3.gif&quot; alt=&quot;功能演示5&quot;&gt;
&lt;/p&gt;
&lt;h2&gt;产品价值&lt;/h2&gt;
&lt;p&gt;本项目致力于通过端云协同的 AI 辅助诊疗平台，打破基层医疗机构在微小皮肤病变诊断上的专业资源壁垒。面对恶性皮肤肿瘤早期肉眼查验难、三甲医院专家资源极度匮乏的现实痛点，系统将先进的深度学习实例分割算法与便携式皮肤镜硬件相融合，把高精度的AI筛查能力下沉至社区，极大地提升了病灶识别的准确性与效率，帮助患者把握住能显著提高生存率的早期干预窗口。此外，依托于华为云强大的算力底座与 OpenHarmony 生态的轻量化交互体验，“识瘤者”系统不仅为患者提供了触手可及的健康管理追踪通道，也为基层医生搭建了集病例管理与学测结合的数字化赋能平台，真正实现了科技普及与医疗向善的社会价值。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.BlZ3cqs7.jpg"/><enclosure url="/_astro/heroimage.BlZ3cqs7.jpg"/></item><item><title>LQR 控制算法</title><link>https://xiaohei-blog.vercel.app/blog/lqr</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/lqr</guid><description>LQR 的简易学习笔记</description><pubDate>Mon, 19 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Linear Quadratic Regulator - LQR&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Linear（线性模型）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Flinear.DH5vHZdQ.png&amp;#x26;w=800&amp;#x26;h=210&amp;#x26;f=webp&quot; alt=&quot;Linear&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;Quadratic（二次）&lt;/strong&gt;，LQR就是根据二次函数的性质，找到最低点，也就是找到一个能量最优的点/能量最优的控制方案。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fquadratic.MHFgYfJt.png&amp;#x26;w=1782&amp;#x26;h=792&amp;#x26;f=webp&quot; alt=&quot;Quadratic&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;Regulator（调节器）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fregulator.Czo95td8.png&amp;#x26;w=1280&amp;#x26;h=550&amp;#x26;f=webp&quot; alt=&quot;Regulator&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;优缺点（更适用于多输入多输出的模型）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fyou.oBGywqmx.png&amp;#x26;w=1824&amp;#x26;h=752&amp;#x26;f=webp&quot; alt=&quot;point&quot;&gt;&lt;/p&gt;
&lt;h2&gt;与PID的区别&lt;/h2&gt;
&lt;p&gt;PID为输出反馈控制，LQR为状态反馈控制，有了多维度的控制信息。下图为LQR的控制流程框图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fframework.C2wf6-oQ.png&amp;#x26;w=1931&amp;#x26;h=1069&amp;#x26;f=webp&quot; alt=&quot;frame&quot;&gt;&lt;/p&gt;
&lt;h2&gt;求增益矩阵K&lt;/h2&gt;
&lt;p&gt;首先明确一个代价函数J，这个代价函数来衡量这个方法是不是最优的一个方法。其中Q1、Q2、Q3（权重）这三个因数的大小决定了x中每个量的重要性&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fk.Cd6UKf_n.png&amp;#x26;w=2040&amp;#x26;h=700&amp;#x26;f=webp&quot; alt=&quot;k&quot;&gt;&lt;/p&gt;
&lt;p&gt;黎卡提方程（Riccati）：目的就是在有了代价函数的基础上，找到能量最优的解，从而求出K增益矩阵（在matlab中直接调用函数即可）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fr.Bdz9bHW9.png&amp;#x26;w=800&amp;#x26;h=120&amp;#x26;f=webp&quot; alt=&quot;r&quot;&gt;&lt;/p&gt;
&lt;h2&gt;观测器的作用&lt;/h2&gt;
&lt;p&gt;需要观测器本质还是因为状态反馈控制，需要知道我们反馈的每个状态变量。有时候我们的一些状态变量是无法直接从传感器获取的。&lt;/p&gt;
&lt;p&gt;无法从传感器直接获取，我们就需要用观测器把未知的一个状态变量给估计出来（如果只是单纯的用数学方法进行微分积分变换，会出现很多的噪声，同时也可能会产生相位滞后。甚至有些数据是无法从传感器得到或者说直接微分积分就得到的）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fobvision.CHMcQPIp.png&amp;#x26;w=1409&amp;#x26;h=729&amp;#x26;f=webp&quot; alt=&quot;ob&quot;&gt;&lt;/p&gt;
&lt;h2&gt;LQR 仿真&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;先写状态方程和状态空间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fsim1.BEv5XvCp.png&amp;#x26;w=800&amp;#x26;h=430&amp;#x26;f=webp&quot; alt=&quot;sim1&quot;&gt;
&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fsim2.Dsnv6Q2v.png&amp;#x26;w=800&amp;#x26;h=601&amp;#x26;f=webp&quot; alt=&quot;sim2&quot;&gt;
&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fsim3.CyVmuiCE.png&amp;#x26;w=800&amp;#x26;h=365&amp;#x26;f=webp&quot; alt=&quot;sim3&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;搭建simulink仿真系统&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;先设置状态空间方程模型
&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fsim4.KEHnAoVO.png&amp;#x26;w=800&amp;#x26;h=589&amp;#x26;f=webp&quot; alt=&quot;sim4&quot;&gt;&lt;/li&gt;
&lt;li&gt;再添加卡尔曼观测器
&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fsim5.CStXeNwx.png&amp;#x26;w=800&amp;#x26;h=593&amp;#x26;f=webp&quot; alt=&quot;sim5&quot;&gt;&lt;/li&gt;
&lt;li&gt;完整系统结构
&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fsim6.YUUOXg8f.png&amp;#x26;w=800&amp;#x26;h=408&amp;#x26;f=webp&quot; alt=&quot;sim6&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;QR矩阵调参&lt;/h2&gt;
&lt;p&gt;通过调QR矩阵得到新的增益矩阵K，从而达到闭环再来做状态反馈控制。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fnote.Djwe76VD.png&amp;#x26;w=800&amp;#x26;h=100&amp;#x26;f=webp&quot; alt=&quot;note&quot;&gt;&lt;/p&gt;
&lt;p&gt;R的调节举例：u为控制量。t为时间。所以当R变大时，控制量u会变小，导致一个结果是控制量会比以前更小，从而响应的时间更长，响应的速度更慢。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fnote1.Dggw-LtJ.png&amp;#x26;w=800&amp;#x26;h=420&amp;#x26;f=webp&quot; alt=&quot;note1&quot;&gt;&lt;/p&gt;
&lt;p&gt;QR矩阵影响控制系统的方法：QR矩阵改变的是他们所对应的状态变量的收敛速度，即趋近于0的速度。如果想要让这个状态变量收敛的更快，更早的趋近于零，那就通过增大状态变量对应的权重系数即可。（例如：让X1收敛的更快就增大Q1，让X2收敛的更快就增大Q2）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fnote2.B_68IBfJ.png&amp;#x26;w=800&amp;#x26;h=513&amp;#x26;f=webp&quot; alt=&quot;note2&quot;&gt;&lt;/p&gt;
&lt;h2&gt;跟踪参考输入&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;参考输入放在哪里？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;假设给定一个单位阶跃信号的输入，希望&lt;strong&gt;输出可以跟踪我们的参考输入&lt;/strong&gt;（如果只按照PID的控制流程再结合上LQR的控制方法发现无法完成跟踪参考输入）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在LQR控制框前加上积分环节，之后控制系统如果存在误差（实际值与目标值的差距），就会被一直的累加，直到误差被消除。从而解决LQR状态反馈引起的输出无法跟踪的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fnote3.D87x-Sp4.png&amp;#x26;w=800&amp;#x26;h=441&amp;#x26;f=webp&quot; alt=&quot;note3&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;为什么要额外加输出反馈：LQR本身作为状态反馈控制，要想跟踪输出就必须用上输出反馈控制，引入积分环节就可以达到这个目的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fnote4.C-LDe6Dy.png&amp;#x26;w=800&amp;#x26;h=279&amp;#x26;f=webp&quot; alt=&quot;note4&quot;&gt;
&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fnote5.DE8u1ncC.png&amp;#x26;w=800&amp;#x26;h=282&amp;#x26;f=webp&quot; alt=&quot;note5&quot;&gt;&lt;/p&gt;
&lt;h2&gt;实物中的LQR对应的C语言程序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;变量初始化&lt;/li&gt;
&lt;li&gt;观测器运算&lt;/li&gt;
&lt;li&gt;反馈、增益矩阵k乘以对应的几个状态变量&lt;/li&gt;
&lt;li&gt;得到控制量u&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://xiaohei-blog.vercel.app/_image?href=%2F_astro%2Fcode.C611junR.png&amp;#x26;w=800&amp;#x26;h=420&amp;#x26;f=webp&quot; alt=&quot;code&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.bHVSuNBk.jpg"/><enclosure url="/_astro/heroimage.bHVSuNBk.jpg"/></item><item><title>帕金森患者康复疗效预估系统</title><link>https://xiaohei-blog.vercel.app/blog/parkinson</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/parkinson</guid><description>Makerizon-帕金森项目作品展示集。</description><pubDate>Sat, 17 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside, StepIndent } from &apos;@/components/user&apos;
import { WebVideo } from &apos;@/components/advanced&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;这个项目的全称叫做&lt;strong&gt;基于 OpenHarmony 的帕金森病症多数据融合监测系统&lt;/strong&gt;，是我在本科时参加&lt;strong&gt;李一浩老师&lt;/strong&gt;的&lt;a href=&quot;https://bbs.huaweicloud.com/blogs/380630&quot;&gt;梅科尔工作室&lt;/a&gt;时所参与并负责的项目之一。由于之前需要保密的原因，关于这个项目的展示材料一直没办法对外公布，现在随着项目的发展与我们第一批老成员的离开，这个项目也慢慢走到了尽头。&lt;/p&gt;
&lt;p&gt;我曾一直在想，该用什么样的一种形式去展示这个项目，让它即不张扬又不被埋没。因为它身上不仅承载着我们的付出，还有我们项目成员之间的友谊，以及我们那回忆起来还算美好的大学时光。对它的感情是复杂的，可能从一开始就没有把它只当成一个打卡式的任务或者一个“活”来对待吧。&lt;/p&gt;
&lt;p&gt;终于，慢慢的，我想我找到了让它可以永远被记录，但又不那么受人瞩目的展示形式了——将它记录到我的博客里。我想那种对于这个项目的感情可能只是我一个人的臆想，因为我是一个爱活在回忆里的人，同时我又有着自由主义者的那种偏功利的心理状态。我希望这个项目可以再一次成就我，作为我的一个可以展示的闪光点。&lt;/p&gt;
&lt;p&gt;把它写成一个作品集（介绍博客），不断有一个声音提醒我，好像如果我不记录下这个项目，它就要溜走了一样。但我仍然是拖到了现在才来真正去写它，去阐述它。那就来吧，用我好久没读过书而显得拙劣的文笔，来展示我这个即爱又艾的项目。&lt;/p&gt;
&lt;h2&gt;项目简介&lt;/h2&gt;
&lt;p&gt;本项目是一个将大数据与物联网和现代医疗相结合的基于华为云 OpenHarmony 的帕金森病症多数据融合监测系统。该系统可以帮助帕金森轻症患者在家完成手颤抖动、手指弯曲、肌电信号等数据的采样，及时发现帕金森病情恶化趋势，实现疾病的早诊断、早治疗，提高了诊断效率，降低了医疗机构评估工作强度，减轻了患者经济与心理负担。&lt;/p&gt;
&lt;p&gt;该项目基于华为云 IoT 的端侧规则引擎技术，通过 IoTDA 设置联动规则，基于 SQL 语句的规则引擎云端一键下发给设备，在设备之间通过 OpenHarmony 的分布式软总线，直接在端侧完成设备间的联动，降低网络质量的依赖，提高整体设备联动效率。此外将特征提取算子、滤波算子等云端算子一键下发至传感设备中，在端侧设备上就能完成无效数据的过滤和屏蔽。该项目通过华为云 IoT 设备接入云服务，不仅仅实现设备通过物模型的极简上云，还让设备具备主动“思考”的能力，从而大幅降低帕金森项目的开发工作量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/system.png&quot; alt=&quot;系统简介&quot;&gt;&lt;/p&gt;
&lt;h2&gt;项目背景与痛点问题&lt;/h2&gt;
&lt;p&gt;我国的帕金森患者 300 万以上，65 岁以上老人患者率高达 1.7% 。帕金森患者有&lt;strong&gt;抖、僵、慢、倒四大核心症状&lt;/strong&gt;。静止性震颤、肌强直、运动迟缓、步态障碍，给患者和家人带来极大的痛苦。因此我们团队在想有没有一种方法将我们所学到的技术用于这方面，帮助医生患者进行评估。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/bg.png&quot; alt=&quot;项目背景&quot;&gt;&lt;/p&gt;
&lt;p&gt;目前市面上的对帕金森病检测的设备很少，且功能单一。不能结合医疗实现医生对患者日常运动状态的监控。已有的评估方案是对帕金森病的运动障碍评估，这种评估基于病史信息、患者自述和神经科医师的临床检查，并且借助统一帕金森评估量表、改良的运动迟缓评估量表进行评估。此类方法虽然简单易用，全面地反映患者的病情变化，但是也存在着一些不足。第一，准确性差，因为通过医师等评估，依赖主观经验。第二，及时性差，临床评估只能针对患者的当前状态，日常状态无法反映。&lt;/p&gt;
&lt;p&gt;此外还有基于计算机视觉的方法，主要是利用摄像机和光学运动捕捉系统记录患者的运动过程，计算运动学参数，虽然精度较高，但是主要应用于临床试验。关于惯性传感器采集患者的运动信号，从而对症状进行分析和评估。但是存在数据的单一，造成误判等情况。&lt;/p&gt;
&lt;p&gt;市面上帕金森疾病检测产品，存在以下痛点:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检测过程复杂&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;市面上已存在的帕金森设备存在检测不灵敏、检测过程复杂等问题，且对于患者的检测周期较长。
&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;前期症状不明显&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;社会上的帕金森患者在早期的帕金森病表现状况不够明显，也没有相应的日常检测设备。
&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;患者线下数据收集难&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;随着互联网医疗体系的完善，医生对患者数据的管理与分析变得迫切。与互联网医疗配套的家用医疗器械缺乏，患者数据无法第一时间向医生反馈。
&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/heart.png&quot; alt=&quot;项目痛点&quot;&gt;&lt;/p&gt;
&lt;h2&gt;开发挑战&lt;/h2&gt;
&lt;p&gt;我们团队在开发时遇到的挑战:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;私有协议开发&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;自行开发私有协议，并且需要在硬件端和后台同时部署，开发周期达 3.5 人\月，不利于产品的快速开发。
&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;数据量庞大冗余&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;针对目前帕金森场景下，每次上传的数据量达到 1000 条，急需将部分滤波、降维算子进行下发到端侧，减少冗余无用的数据。
&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;场景数据杂乱&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对帕金森场景下的数据获取，没有统一的物模型;涉及 30 多项设备属性、数据类型和数量大小的确定，后期移植困难。
&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;多设备的数据联动&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;针对多个帕金森评估设备之间的信息传输和联动，存在数据传输过程复杂，更新不及时的问题。
&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/chanllege.png&quot; alt=&quot;项目挑战&quot;&gt;&lt;/p&gt;
&lt;p&gt;基于以上我们团队结合自身的技术力量，针对上述的问题，使用华为云 IoT 端侧规则引擎，实现联动规则云端下发、端侧快速执行，提高设备联动效率，提出了一套基于 OpenHarmony 结合物联网、数据交叉融合算法等技术构建的可穿戴帕金森病症多数据融合监测系统。和市面上相比，我们产品可以实现对帕金森患者的手部震颤加速度数据、肌电信号、手指弯曲的信号和压力数据进行多数据采集，并且采用数据融合技术，实现高准确率的评估。而且在后期，我们可以实现日常情况的随时检测和评估，方便患者的日常使用。&lt;/p&gt;
&lt;h2&gt;项目架构&lt;/h2&gt;
&lt;p&gt;帕金森病运动迟缓量化测评系统主控方面是由 BearPi-HM Nano 组成。通过 MPU6050 (小熊派套件 E53_SC2 模块)、弯曲传感器、肌电传感器以及压力传感器采集手部数据。结合华为云 IoT 平台 ModelArts 深度学习平台进行交叉验证分析，在预测数据集上取得最好性能的患者模型。并结合 UPDRS、H&amp;#x26;Y 量化评估制定了帕金森全定评估量表，对患者病情程度进行判断。将数据以 MQTT 协议通过 WiFi 传输到华为云 IoT 平台，结合物联网技术以及数据交叉算法，进行数据融合。数据传递到 IoT 平台后，通过 Django 后台存储数据到数据库，并提供给前端调用的接口，实现将数据发送到手机或平板终端，实现智能化设计。通过鸿蒙App实现患者日常状态的数据记录和展示，能够及时分析病情并及时调整。项目的架构如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/build.png&quot; alt=&quot;项目架构&quot;&gt;&lt;/p&gt;
&lt;p&gt;该项目基于华为云 IoT 的端侧规则引擎技术，通过 IoTDA 设置联动规则，基于 SQL 语句的规则引擎云端一键下发给设备，在设备之间通过 OpenHarmony 的分布式软总线，直接在端侧完成设备间的联动，降低对网络质量的依赖，提高整体设备联动效率。此外将特征提取算子、滤波算子等云端算子一键下发至传感设备中，在端侧设备上就能完成无效数据的过滤和屏蔽。该项目通过华为云 IoT 设备接入云服务，不仅仅实现设备通过物模型的极简上云，还让设备具备主动“思考”的能力，从而大幅降低项目的开发工作量。&lt;/p&gt;
&lt;p&gt;帕金森数据采集模块可以实现对帕金森手部的相关数据进行采集，包括手部禁止性震颤、手指弯曲情况、手部肌电信号以及手指压力等数据，实现无线远程采集。在患者日常生活中，可以使用采集设备进行日常采集和评估。内部由加速度 MPU6050 (小熊派套件 E53_SC2 模块)、弯曲传感器以及肌电传感器组成，主控方面是由 BearPi-HM Nano 组成，穿戴节点为手腕、手指以及手臂，用多个传感器来采集全方位的数据，并且搭载 WiFi 模块，硬件具有无线传输、工作时间长、微负荷、便携等特点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/mokuai.webp&quot; alt=&quot;采集模块&quot;&gt;&lt;/p&gt;
&lt;p&gt;数据分析模块主要包括预处理和 ModelArts 平台自动学习分析。对于采集到的患者诸多手部数据中，加速度传感器采集数据特征较为显著，因为我们着重分析加速度等数据，并且进行预处理。在处理过程中，我们通过加速度传感器、肌电传感器、弯曲传感获取手部震颤数据以及表面肌电信号等特征数据使用 IIR 滤波器进行滤波去除异常数据、滤波滤趋势、滤掉重力加速度，进行点积运算，对数据进行归一化处理，采用分布式容合的数据处理方式最终获得真正有代表性的数据。本团队不仅对数据进行预处理和日常的数据记录，而且还使用深度算法挖掘更深的信息，包括患者的病情预估。在本项目中，团队使用华为 AI 平台 ModelArts 作深度学习训练的平台，利用自动学习功能，对数据进行自主分析。&lt;/p&gt;
&lt;p&gt;数据展示部分主要是通过鸿蒙 App 和网页端实时分析并展示分析结果。获得待推送给用户的数据，实时分析数据并得出当前病情状态分析结果。再结合患者历史数据，基于大数据综合分析，第一时间分析并推送科学的疗养方案和就医建议。从而使用户居家也可以通过鸿蒙 App 实时了解自己的病情状态，及时获得疗养方案和就医建议，在日常中根据给出对应的训练方案来延缓病情的发展。解决不能及时发现病情变化导致病情快速发展，和医生不能全面了解患者病情状态导致诊断偏差的情况。&lt;/p&gt;
&lt;h2&gt;设计思路&lt;/h2&gt;
&lt;h3&gt;算法设计&lt;/h3&gt;
&lt;p&gt;在算法设计上。我们基于华为云 IoT 平台，将特征值提取算子和滤波算子直接下沉到我们的 OpenHarmony 设备上，数据在端侧进行过滤和有效提取，然后数据上传至华为云 IoT 平台转发到 ECS 服务器识别，输入训练好的深度学习模型，得出帕金森病情分级情况。比如伸开一个手指的动作，至少要上传 100 条左右的数据，现在采用这种方式，整个的数据传输量减少 50%，提高数据传输的效率，降低了对于网络的依赖。数据模型也更加完整。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/algo.png&quot; alt=&quot;算法设计&quot;&gt;&lt;/p&gt;
&lt;h3&gt;数据管理与后台&lt;/h3&gt;
&lt;p&gt;帕金森康复数据传输和标注物模型设定。采用海思的 Hi3861 V100 作为主控芯片，基于 OpenHarmony 操作系统，通过华为云 IoTDA 设备接入服务实现数据的传输，采用 MQTT 协议，将加速度数据、弯曲传感器数据、肌电传感器数据和压力数据打包成 json 格式 WiFi 传输到华为 IoT 平台上。在华为云平台上实现帕金森康复手套的物模型建立。实现了平台的二次开发和一键导入。本项目的物模型包括加速度、角速度、压力、弯曲角度、肌电信号、训练时长等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/wumodel.webp&quot; alt=&quot;物模型&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用华为云 IoT 云平台的数据转发服务，实现数据转发到华为云 ECS 服务器，服务器端使用的是基于 Django 的 Web 应用框架，实现从数据采集到传输至云平台，再到 ECS 服务器，实现数据的持久化存储。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/yuntai.webp&quot; alt=&quot;云平台&quot;&gt;&lt;/p&gt;
&lt;p&gt;命令下发和算法下发实现通过平板调用华为云 IoT 平台下发和算子下发的 AP 接口，实现手指弯曲、手指捏合、手指震颤、手臂弯曲等范式动作的控制命令，并通过对应的传感器获取数据，另外将原先部署在云端的滤波算法下发到端侧设备中，实现了远程更新端侧算法和参数等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/suanzi.webp&quot; alt=&quot;算子下发&quot;&gt;&lt;/p&gt;
&lt;h3&gt;范式动作测试方法&lt;/h3&gt;
&lt;p&gt;由于设备在采集数据时，受到各种因素影响，如采集的环境、不同人群等，此类因素可能会造成数据集质量不高以及数据分析时造成不准确的情况。因此团队和河南省中医院展开合作，基于帕金森患者实际的症状表现情况分析，制定适用于 60 岁以上人群的范式动作测试方法，并且和本团队设计的可穿戴硬件相结合，实现在家庭环境下，进行数据采集和分析。&lt;/p&gt;
&lt;p&gt;范式动作测试方法将根据每一个范式动作进行检测，按照手指弯曲检测、手指捏合检测、手部震颤检测、手臂弯曲的肌申信号检测为顺序执行整个范式动作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image/fanshi.png&quot; alt=&quot;范式动作&quot;&gt;&lt;/p&gt;
&lt;h2&gt;实现原理&lt;/h2&gt;
&lt;p&gt;产品基于 OpenHarmony 与华为云 IoT 端侧规则引擎，实现联动规则云端下发。通过使用华为云 IoT 的端侧规则引擎能力，将设备间的联动规则直接通过云端一键下发给设备，在设备之间通过 OpenHarmony 的分布式软总线，直接在端侧完成设备间的联动，降低网络质量的依赖，提高整体设备联动效率。&lt;/p&gt;
&lt;p&gt;另外本产品是基于 ModelArts 自动学习结合全定评估量表的方式，实现帕金森病的康复情况监测，提出当前的就医建议，并且将结果实时展示给用户。并且设备结合物联网技术，使用 WiFi 和 4G 模块，实现了无线远程数据传输。此外我们对本产品的数据收集，设计了一套标准的范式动作测试方法，可以准确实现高质量数据采集。&lt;/p&gt;
&lt;p&gt;通过压力传感器、弯曲传感器、加速度传感器以及肌电传感器，对帕金森手臂和手部的数据同时采集，并通过多源异质融合算法实现对帕金森病情的异常数据进行排查，如当帕金森患者的震颤频率小于 4Hz，触发肌电传感器二次识别患者是否处于静止性震颤状态。&lt;/p&gt;
&lt;h2&gt;应用场景&lt;/h2&gt;
&lt;p&gt;目前帕金森患者无法对自身的病情进行有效的预防和监控。在日常生活中，只能到医院进行相关检查才能够给获取自行的病情。那么如何快速有效的获取帕金森患者的病情以及后期预控是迫切解决的问题。此外，医护人员需要借助互联网进行数据收集、传输、分析，给予患者以相对持续、无创便携的病情监控，提高医护人员的工作效率，节约了时间和成本。而且如果出现了问题，还可以直接反馈给医生进一步降低医疗成本，释放传统医疗资源。&lt;/p&gt;
&lt;p&gt;因此使用穿戴式传感器设备，通过多传感器多方面检测患者运动状态数据，将得到的传感器数据送往数据处理中心进行交叉分析验证，并结合量化评估表达到对患者病情程度进行判断。从而在一定程度上可以预测以及监控帕金森病的具体发展情况。并且结合物联网，将数据推送到鸿蒙 APP 中，提供给患者以及医护人员随时查看。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;患者居家&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;作品聚焦于65岁以上前中期帕金森惠者，而且主要面向居家患者。对帕金森患者可以通过医疗辅助器材商店或者网上购买本产品，使用产品设备每天一到三次，按照我们自主设计的范式测量方法进行简短时间的数据采集，可以实时通过鸿蒙APP查看病情状况和分析及科学的疗养方案。
&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;医院&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;医生面对新就诊患者时，本产品检测病情程度的功能，可以辅助医生准确判断患者当前病情情况;医生面对复查病情的患者时，可以参考产品记录患者的日常数据、分析的结果，及实时检测的结果对患者进行准确的病情状态把握，有利于准确诊断。并且和河南省中医院等省市级医院达成合作后，通过省市级医院的辐射作用推广到下级医院。
&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;社区康养中心&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;作品将推广到社区康养中心，实现区域化推广和销售。患者可以在康养中心的专业人员指导下使用，更加专业和科学。
&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;医疗科研机构&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于各大高校，研究机构等，需要相关的软硬件作为数据采集终端，本作品将面向科研人员进行推广售卖。
&lt;/p&gt;
&lt;h2&gt;产品价值&lt;/h2&gt;
&lt;p&gt;本作品主要用于采集帕金森患者日常运动状态数据并记录方便医生了解患者病情情况并及时给予康复建议。并目主要的客户来源是中老年人帕金森患者，并且有一定时间的患病经历。目的旨在解决早发现帕金森病的前驱症状，实现疾病的早诊断、早治疗。以及医生与患者突破时间，空间限制的交流，实现帕金森患者居家获得及时的医生康复建议。&lt;/p&gt;
&lt;p&gt;基于以上我们团队结合自身的技术力量，针对上述的问题，使用华为云 IoT 端侧规则引擎，实现联动规则云端下发、端侧快速执行，提高设备联动效率，提出了一套结合物联网、数据交叉融合算法等技术构建的基于可穿戴传感器的帕金森病运动迟缓量化测评系统。和市面上相比，我们产品可以实现对帕金森患者的手部震颤加速度数据、肌电信号、手指弯曲的信号和压力数据进行多数据采集，并且采用数据融合技术，实现高准确率的评估。而且在后期，我们可以实现日常情况的随时检测和评估，方便患者的日常使用。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;</content:encoded><h:img src="/_astro/heroimage.DCZs3pCj.jpg"/><enclosure url="/_astro/heroimage.DCZs3pCj.jpg"/></item><item><title>RL 学习笔记（12）：DDPG</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-12</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-12</guid><description>DDPG</description><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;我第一次把 DQN 的思路搬到连续控制上时，最直观的挫败感来自一句话：&lt;strong&gt;“你让我在连续动作空间里取 $\max_a Q(s,a)$，那我怎么取？”&lt;/strong&gt; 离散动作里，动作就那么几个，枚举一下就能选；连续动作里动作不可数，max 变成了一个真正意义上的优化问题。你可以采样动作来近似，也可以每一步都对动作做梯度上升，但这两种路都很难做得既准又快。&lt;/p&gt;
&lt;p&gt;DDPG 在我看来是一种非常“工程化”的妥协：它不再硬算 $\arg\max$，而是直接用一个网络学出“在状态 $s$ 下我应该输出哪个连续动作”，把选动作这件事变成一次前向传播。这样一来，Q 网络负责评估（critic），策略网络负责输出动作（actor），两个网络互相牵引，形成连续控制里最经典的一套 actor-critic 骨架。&lt;/p&gt;
&lt;h2&gt;离散动作 vs 连续动作：为什么“输出层”就不一样了&lt;/h2&gt;
&lt;p&gt;文档先从动作空间的差异讲起，我觉得这一步非常必要，因为很多实现 bug 都来自“你以为动作是概率，结果它是一个浮点数”。离散动作是可数的，网络通常输出一组 logits，再接 softmax 得到 $\pi_\theta(a|s)$，每个动作对应一个概率，概率和为 1。连续动作则是不可数的，我们更常见的做法是直接输出动作值本身，即确定性策略 $a = \mu_\theta(s)$。&lt;/p&gt;
&lt;p&gt;为了让输出落在环境允许的范围里，工程上经常在输出层加一层 &lt;code&gt;tanh&lt;/code&gt;，先把网络输出压到 $[-1,1]$，再按环境动作上界/下界做线性缩放。文档举的小车例子非常贴地气：网络生输出可能是 2.8，经 tanh 变成 0.99，再按动作范围从 $[-1,1]$ 映射到 $[-2,2]$，最终动作就是 1.98。这个细节看起来很小，但它决定了你后面加噪声、做 target policy smoothing 时到底是在“正确的尺度”上扰动。&lt;/p&gt;
&lt;h2&gt;DDPG：Deep + Deterministic + Policy Gradient&lt;/h2&gt;
&lt;p&gt;文档里把名字拆得很清楚：Deep 说明用了神经网络；Deterministic 说明输出确定性动作；Policy Gradient 说明更新 actor 时用的是策略梯度思想。更关键的是一句：&lt;strong&gt;DDPG 可以看作 DQN 的连续动作扩展&lt;/strong&gt;。这句话如果从实现角度理解，就会落到三件事上：经验回放、目标网络、以及 actor-critic 双网络。&lt;/p&gt;
&lt;p&gt;DDPG 里通常有四个网络：&lt;/p&gt;
&lt;p&gt;actor $\mu_\theta(s)$ 负责在给定状态时直接输出动作，critic $Q_w(s,a)$ 负责对“这个状态下做这个动作到底值不值”给出可微的评分；为了让 TD target 不至于在训练中飘来飘去，我们还会放一套延迟更新的 target actor $\mu_{\theta^-}(s)$ 和 target critic $Q_{w^-}(s,a)$ 来提供更稳定的 bootstrap 目标。&lt;/p&gt;
&lt;p&gt;critic 的更新很像 DQN：用 TD target 回归 Q 值；actor 的更新则是“让 critic 认为更好的动作更可能被输出”，也就是沿着 $\nabla_a Q(s,a)$ 方向去推 actor 参数。&lt;/p&gt;
&lt;h2&gt;为什么 DDPG 必须加噪声：确定性策略的探索困境&lt;/h2&gt;
&lt;p&gt;文档把“要加噪声的原因”解释得很直接：DDPG 是异策略（off-policy）训练确定性策略。如果没有噪声，当策略参数固定时，同一个状态永远输出同一个动作，初期几乎不可能覆盖足够的动作空间，学习信号会非常稀薄。&lt;/p&gt;
&lt;p&gt;所以训练时我们会对动作加噪声，让行为策略变成：&lt;/p&gt;
&lt;p&gt;$$
a_t = \mu_\theta(s_t) + \epsilon_t
$$&lt;/p&gt;
&lt;p&gt;文档提到“均值为 0 的高斯噪声效果很好”，这也是许多实现的默认选项。噪声往往还会随训练逐渐减小：前期大一些帮助探索，后期小一些让数据质量更高。测试时则不加噪声，用来评估纯粹的策略利用能力。&lt;/p&gt;
&lt;h2&gt;DDPG 的痛点：超参敏感与 Q 过估计&lt;/h2&gt;
&lt;p&gt;文档指出 DDPG 的典型缺点：对超参数非常敏感，且已经学好的 Q 函数可能开始显著高估，导致策略被破坏。这个现象在实战里很常见：你会看到 Q 值一路变大，但回报突然塌掉，像是模型“自我催眠”相信某些动作非常好，然后把 actor 带偏。&lt;/p&gt;
&lt;p&gt;DDPG 的这类不稳定，后来很大程度上被 TD3 解决。&lt;/p&gt;
&lt;h2&gt;TD3：用三招把 DDPG 变稳&lt;/h2&gt;
&lt;p&gt;文档把 TD3 的三大技巧列得很清楚，我把它们翻成更“实现友好”的直觉。&lt;/p&gt;
&lt;p&gt;第一招是 &lt;strong&gt;截断的双 Q 学习（clipped double Q-learning）&lt;/strong&gt;。TD3 学两个 critic：$Q_{\phi_1}$ 和 $Q_{\phi_2}$，构造 target 时取两者的较小值。这个动作非常简单，但对“过估计”是立竿见影的，因为你不再相信任何一个 critic 的乐观偏差。&lt;/p&gt;
&lt;p&gt;第二招是 &lt;strong&gt;延迟策略更新（delayed policy updates）&lt;/strong&gt;。TD3 不是每次更新 critic 都同步更新 actor，而是让 critic 先多学几步，把 Q 估得更靠谱，再偶尔更新一次 actor。文档提到的经验是“通常每更新两次 critic 更新一次策略”，这也是很多实现的默认设置。&lt;/p&gt;
&lt;p&gt;第三招是 &lt;strong&gt;目标策略平滑（target policy smoothing）&lt;/strong&gt;。在计算 TD target 时，不直接用 target actor 输出的动作，而是给它加一点小噪声并做截断，让 Q target 在动作维度上更平滑，减少 actor 利用 critic 误差的空间。&lt;/p&gt;
&lt;h2&gt;本章小结：连续控制的“最短路径”&lt;/h2&gt;
&lt;p&gt;如果你把这一章的要点压缩成一句工程总结，我会这么说：DDPG 把“连续动作的 argmax”交给 actor 网络来近似，用 replay + target 稳住 critic，再用探索噪声补上确定性策略的探索能力；而 TD3 用双 critic 取 min、延迟更新与目标动作平滑，把 DDPG 最常见的失稳点逐个堵上。&lt;/p&gt;
&lt;p&gt;如果你接下来要把这一章落到代码上，我建议优先保证三件事是对的：动作缩放（tanh + scale）、噪声尺度（与动作范围匹配）、以及 done 边界处理（终止状态别 bootstrap）。很多“算法问题”最后都会落到这三个实现细节上。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CHUxoORI.jpg"/><enclosure url="/_astro/heroimage.CHUxoORI.jpg"/></item><item><title>RL 学习笔记（11）：模仿学习</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-11</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-11</guid><description>模仿学习</description><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;很多人学 RL 是从游戏开始的：reward 很明确，失败成本也低。&lt;/p&gt;
&lt;p&gt;但一旦你把目光放到真实世界（机器人、自动驾驶、医疗），你会发现：&lt;/p&gt;
&lt;p&gt;reward 往往很难写清楚（比如“开得像人”到底对应哪些可计算指标），探索成本还极高（撞一次就可能直接报废）。这两个现实约束会把纯 RL 的试错路线逼得很窄。&lt;/p&gt;
&lt;p&gt;文档在这一章给出一个非常现实的答案：&lt;strong&gt;模仿学习&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它的输入不是奖励，而是专家示范：专家怎么做，你先学会怎么做。&lt;/p&gt;
&lt;h2&gt;模仿学习的两条主线&lt;/h2&gt;
&lt;p&gt;文档提到两大方法：&lt;/p&gt;
&lt;p&gt;第一条路线是行为克隆（Behavior Cloning, BC），第二条路线是逆强化学习（Inverse Reinforcement Learning, IRL）。一个更像监督学习，另一个更像“先学偏好再学决策”。&lt;/p&gt;
&lt;p&gt;我一般这样理解：&lt;/p&gt;
&lt;p&gt;BC 是把“模仿”当监督学习，直接学 $\pi(a|s)$；IRL 则更像先从专家行为里反推一个“奖励函数/偏好”，再在这个奖励下跑 RL。&lt;/p&gt;
&lt;h2&gt;1) 行为克隆（BC）：最像监督学习的模仿&lt;/h2&gt;
&lt;p&gt;文档描述得很清楚：专家做什么，智能体就做一模一样的事。&lt;/p&gt;
&lt;p&gt;实现上就是一个监督学习问题：&lt;/p&gt;
&lt;p&gt;实现上它就是一个监督学习问题：输入是观测 $s$，标签是专家动作 $a$。如果动作是离散的，你通常会用交叉熵；如果动作是连续的，就更常见 MSE 或负 log-likelihood。&lt;/p&gt;
&lt;h2&gt;2) DAgger：用数据集聚合修分布偏移&lt;/h2&gt;
&lt;p&gt;文档提到 DAgger 的思路：记录专家在“模型会遇到的状态”下应该做什么。&lt;/p&gt;
&lt;p&gt;流程可以理解为：&lt;/p&gt;
&lt;p&gt;你可以把它理解成一个“边犯错边请教”的循环：先用当前策略跑一段，让自己真实地走到那些会犯错的状态；再把这些状态拿去问专家“你会怎么做”；然后把新得到的数据并入数据集里重新训练。这个过程重复下去，数据分布就会越来越贴近你模型在部署时真正会遇到的世界。&lt;/p&gt;
&lt;p&gt;这会不断把数据分布拉回到“你真实会走到的地方”。&lt;/p&gt;
&lt;h2&gt;3) 逆强化学习（IRL）：从示范里反推奖励&lt;/h2&gt;
&lt;p&gt;文档最后提到 IRL 的动机：行为克隆解决不了全部问题，因此引入 IRL。&lt;/p&gt;
&lt;p&gt;IRL 的直觉是：&lt;/p&gt;
&lt;p&gt;专家行为背后通常隐含着某种“偏好”或“奖励”，IRL 的做法就是从示范中把这种偏好反推出来，然后再用普通的 RL 在这个奖励下学习策略。它适合那些你很难写 reward，但能拿到足够高质量示范的场景。&lt;/p&gt;
&lt;p&gt;这在“奖励难写但示范容易拿到”的场景里很有价值。&lt;/p&gt;
&lt;h2&gt;本章小结：当你不想写奖励，就去找示范&lt;/h2&gt;
&lt;p&gt;模仿学习的价值我总结成一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在真实世界里，示范往往比奖励更便宜。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;BC 让你快速得到一个可用策略，DAgger 让它不那么容易跑偏，IRL（或 GAIL）让你在没有显式奖励的情况下也能学习“像专家一样”的行为。&lt;/p&gt;
&lt;p&gt;到这里，EasyRL-base 的 11 章就完成了一个从基础概念到常用算法再到高级主题（稀疏奖励、模仿学习）的闭环。后续如果你要继续往下写，我建议优先补上：连续控制三件套（DDPG/TD3/SAC）与离线 RL 的基本范式，它们与模仿学习在工程上经常会接到一起。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.DLZBnTCB.jpg"/><enclosure url="/_astro/heroimage.DLZBnTCB.jpg"/></item><item><title>RL 学习笔记（10）：稀疏奖励</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-10</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-10</guid><description>稀疏奖励</description><pubDate>Sun, 11 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;我觉得稀疏奖励是“最能把人逼疯”的 RL 场景之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你训练了几百万步，reward 仍然是 0；&lt;/li&gt;
&lt;li&gt;你甚至不知道是算法错了，还是环境本来就太难；&lt;/li&gt;
&lt;li&gt;你想 debug，但没有信号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文档在这一章给了一个非常实用的思路：&lt;/p&gt;
&lt;p&gt;你要么自己把奖励设计得更“密集”一些（reward shaping），让智能体能更早看到进步；要么引入内在奖励（curiosity-driven reward），在外在反馈几乎为零时，先用“新奇感”把学习信号撑起来。&lt;/p&gt;
&lt;p&gt;我会按这个顺序讲清楚，并重点拆解 ICM 的结构，因为它是很多“内在动机”方法的原型。&lt;/p&gt;
&lt;h2&gt;1) 奖励塑形（Reward Shaping）&lt;/h2&gt;
&lt;p&gt;文档说“设计奖励就是引导奖励”，这句话很准确：你自己给环境加一些更密集的反馈，让智能体知道自己有没有进步。&lt;/p&gt;
&lt;p&gt;常见例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;机器人走路：除了“没摔倒”给奖励，还可以按前进距离给奖励&lt;/li&gt;
&lt;li&gt;导航任务：按与目标距离的减少量给奖励&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2) 好奇心驱动（Curiosity-driven Reward）与 ICM&lt;/h2&gt;
&lt;p&gt;文档提到 ICM：给智能体加一个“好奇心”的奖励函数。&lt;/p&gt;
&lt;p&gt;ICM 的核心直觉特别漂亮：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果下一状态很难被预测，说明你到了一个“新奇”的地方，那就给你奖励。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;ICM 的输入与输出&lt;/h3&gt;
&lt;p&gt;文档给出的结构是：输入 $(s_t, a_t, s_{t+1})$，输出内在奖励 $r_t^i$。&lt;/p&gt;
&lt;p&gt;并且训练时总奖励是：&lt;/p&gt;
&lt;p&gt;$$
r_t^{\text{total}} = r_t + \beta r_t^i
$$&lt;/p&gt;
&lt;p&gt;其中 $\beta$ 是内在奖励权重。&lt;/p&gt;
&lt;h3&gt;加特征提取器：过滤无意义噪声&lt;/h3&gt;
&lt;p&gt;文档也指出一个关键问题：仅靠好奇心不够，智能体可能沉迷于“噪声”（比如电视雪花）。&lt;/p&gt;
&lt;p&gt;解决方法：加 feature extractor，把状态映射到更有意义的特征空间，再在特征空间里做预测误差。&lt;/p&gt;
&lt;h2&gt;本章小结：没有信号，就先造信号&lt;/h2&gt;
&lt;p&gt;稀疏奖励问题，本质是“学习信号太少”。reward shaping 与 curiosity 是两种造信号的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;shaping 更直接，但更依赖人工设计&lt;/li&gt;
&lt;li&gt;curiosity 更通用，但更容易跑偏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一章我们会谈模仿学习：当你连奖励都不想设计，或者奖励很难定义时，&lt;strong&gt;用专家示范直接教智能体怎么做&lt;/strong&gt;。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.BdlT0_-u.jpg"/><enclosure url="/_astro/heroimage.BdlT0_-u.jpg"/></item><item><title>RL 学习笔记（9）：Actor-Critic</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-9</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-9</guid><description>Actor-Critic</description><pubDate>Sat, 10 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;策略梯度的痛点我们在第 4、5 章已经感受过：它能学，但方差大、抖得像心电图。&lt;/p&gt;
&lt;p&gt;Actor-Critic 的出发点很朴素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;actor（演员）负责策略 $\pi_\theta(a|s)$&lt;/li&gt;
&lt;li&gt;critic（评论员）负责价值 $V(s)$ 或 $Q(s,a)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文档在这一章给了一个非常工程友好的总结：借助价值函数，actor-critic 可以做单步更新，不必等回合结束。&lt;/p&gt;
&lt;h2&gt;actor 与 critic 各自做什么&lt;/h2&gt;
&lt;p&gt;文档里写得很清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;actor：学习一个策略以得到尽可能高的回报&lt;/li&gt;
&lt;li&gt;critic：估计当前策略的价值，评估 actor 的好坏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是一种“分工协作”的结构：actor 负责决定怎么做，critic 负责告诉它做得对不对。&lt;/p&gt;
&lt;h2&gt;为什么 actor-critic 更稳定：用优势降低方差&lt;/h2&gt;
&lt;p&gt;文档提到了优势演员-评论员（A2C）。优势的核心就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不是用原始回报 $G_t$（方差大）&lt;/li&gt;
&lt;li&gt;而是用优势 $A_t$（相对基线的改变量）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见的优势形式：&lt;/p&gt;
&lt;p&gt;$$
A_t = r_{t+1} + \gamma V(s_{t+1}) - V(s_t)
$$&lt;/p&gt;
&lt;p&gt;这其实就是 TD residual。&lt;/p&gt;
&lt;h2&gt;A2C 的实现闭环（按我写代码的顺序）&lt;/h2&gt;
&lt;p&gt;如果把 A2C 写成一段你真正会维护的训练循环，它通常长这样：先用 actor 从当前策略里采样动作并与环境交互，然后让 critic 给出 $V(s)$ 的估计；接着你用 TD 或 GAE 把优势 $A_t$ 算出来，把它当成权重去更新 actor（loss 形如 $-\log\pi(a|s)\cdot A$），同时 critic 也做一个回归，把 $V(s)$ 拟合到 bootstrap 目标（loss 形如 $|V(s) - \text{target}|^2$）。最后很多实现都会顺手加一个 entropy bonus，目的不是“更聪明”，而是别让策略太早坍缩到一个几乎确定的输出。&lt;/p&gt;
&lt;p&gt;文档提到一个技巧：探索机制可以通过对策略分布的熵加约束，保证 entropy 不要太小。&lt;/p&gt;
&lt;h2&gt;文档提到的一个风险：两个网络都可能估不准&lt;/h2&gt;
&lt;p&gt;文档说 A2C 的缺点是要估计两个网络，风险变成两倍。这话一点不夸张：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;critic 估错，优势就错，actor 会被带跑&lt;/li&gt;
&lt;li&gt;actor 乱跑，critic 的分布也在漂&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工程上常见的缓解策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;value loss 加权（value_coef）别太大，避免 critic 主导训练&lt;/li&gt;
&lt;li&gt;归一化优势（advantage normalization）&lt;/li&gt;
&lt;li&gt;梯度裁剪（clip grad norm）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本章小结：Actor-Critic 是深度 RL 的“通用骨架”&lt;/h2&gt;
&lt;p&gt;后面很多大名鼎鼎的连续控制算法（DDPG/TD3/SAC）都是 actor-critic 的不同实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;critic 学 Q&lt;/li&gt;
&lt;li&gt;actor 负责给出（近似）最优动作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而在稀疏奖励、模仿学习等更“难的任务”里，actor-critic 也常常作为底座出现。&lt;/p&gt;
&lt;p&gt;下一章我们会谈稀疏奖励：当环境几乎不给反馈时，你要怎么让智能体继续学习？&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.YJJ3Fy_I.jpg"/><enclosure url="/_astro/heroimage.YJJ3Fy_I.jpg"/></item><item><title>RL 学习笔记（8）：连续动作下的 Q 方法</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-8</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-8</guid><description>DQN 擅长离散动作，但在连续动作下 $\max_a Q(s,a)$ 变得难以计算。本章按文档给的四种方案讲清楚它们的直觉、代价与为什么最终很多人会走向 Actor-Critic。</description><pubDate>Fri, 09 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;如果你做过一点机器人或控制任务，很快会发现：动作是连续的（扭矩、角度、速度），而 DQN 的世界观是离散动作。&lt;/p&gt;
&lt;p&gt;文档在这一章把问题点得很透：DQN 更适用于离散动作，因为它需要计算 $\max_a Q(s,a)$。连续动作里，这个 max 不是枚举就能搞定的。&lt;/p&gt;
&lt;p&gt;于是出现了四种思路。我也按“从最朴素到最工程”的顺序讲：采样、对动作做梯度上升、设计更复杂的网络架构、以及——不使用 DQN（也就是转向 Actor-Critic）。&lt;/p&gt;
&lt;h2&gt;方案一：对动作进行采样&lt;/h2&gt;
&lt;p&gt;文档说得很直接：&lt;/p&gt;
&lt;p&gt;你可以先采样 N 个候选动作 ${a_1,\dots,a_N}$，把它们逐个代入 $Q(s,a)$ 做一次评分，然后从里面挑最大的那个动作执行。它朴素到像暴力搜索，所以实现起来很顺手，也很适合当 baseline；但它同样会在高维动作空间里迅速变得昂贵而且不精确。&lt;/p&gt;
&lt;p&gt;优点：实现简单。
缺点：不精确、算力开销大，维度一高就爆炸。&lt;/p&gt;
&lt;h2&gt;方案二：对动作做梯度上升（把 a 当作待优化变量）&lt;/h2&gt;
&lt;p&gt;文档把它描述为一个优化问题：最大化目标函数 $Q(s,a)$。&lt;/p&gt;
&lt;p&gt;做法是：初始化一个动作 $a$，对 $a$ 做梯度上升迭代，找到局部最大。&lt;/p&gt;
&lt;p&gt;问题也很明显：&lt;/p&gt;
&lt;p&gt;它既绕不开局部最优与全局最优的老问题，也会在工程上变得很慢——因为每次决策都要做若干轮迭代，你等于把“选动作”变成了一个小型优化过程。&lt;/p&gt;
&lt;h2&gt;方案三：设计更复杂的网络架构&lt;/h2&gt;
&lt;p&gt;文档提到“数学方法复杂，但思路好”：通过变换把动作处理得更像离散。&lt;/p&gt;
&lt;p&gt;这一类方法更多出现在学术或特定结构（比如量化、分层动作）里；工程上我更常见的是下面的路线：直接用 actor 学一个动作。&lt;/p&gt;
&lt;h2&gt;方案四：不使用 DQN（转向别的方法）&lt;/h2&gt;
&lt;p&gt;文档最后一句“哈哈”非常真实：很多时候，连续动作最省心的方案就是不硬套 DQN，而是用 Actor-Critic。&lt;/p&gt;
&lt;p&gt;原因是 Actor-Critic 允许：&lt;/p&gt;
&lt;p&gt;原因在于 actor-critic 的分工非常自然：actor 负责直接输出连续动作（或连续动作分布），critic 负责评估并提供梯度信号。这样你就不需要在连续空间里每一步都去硬算一个 max，而是把它近似成一次网络前向传播。&lt;/p&gt;
&lt;p&gt;这比“在连续空间里求 max”自然得多。&lt;/p&gt;
&lt;h2&gt;本章小结：连续动作把你推向 Actor-Critic&lt;/h2&gt;
&lt;p&gt;如果你读到这里觉得“连续动作下硬用 Q 方法好别扭”，那说明你理解对了。&lt;/p&gt;
&lt;p&gt;下一章我们就进入文档的第九章：演员-评论员（Actor-Critic）算法。你会看到它如何把策略梯度和 TD 学习拼在一起，让连续控制与稳定训练变得更可行。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.DdP44Rba.jpg"/><enclosure url="/_astro/heroimage.DdP44Rba.jpg"/></item><item><title>RL 学习笔记（7）：DQN 进阶</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-7</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-7</guid><description>DQN 进阶</description><pubDate>Thu, 08 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;DQN 跑起来之后，你很快会遇到一种“很烦的稳定性”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reward 上来一点又掉回去；&lt;/li&gt;
&lt;li&gt;Q 值看起来越来越大，但表现没有变好；&lt;/li&gt;
&lt;li&gt;训练特别慢，像在原地打转。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时你去翻论文和代码库，就会看到一堆 DQN 变形：DDQN、Dueling、PER、NoisyNet……&lt;/p&gt;
&lt;p&gt;我强烈建议不要把它们当作“背名词”，而是像文档那样抓住它们要解决的问题：&lt;strong&gt;每一个技巧都对应一个具体痛点&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;1) Double DQN：解决 Q 值过估计&lt;/h2&gt;
&lt;p&gt;文档直接点出第一个问题：Q 值总是被高估。&lt;/p&gt;
&lt;p&gt;原因很直觉：你用同一个网络既选动作又估计价值，于是噪声会被 max 操作放大。&lt;/p&gt;
&lt;p&gt;Double DQN 的做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 online network 选动作：$a^* = \arg\max_a Q_\theta(s&apos;,a)$&lt;/li&gt;
&lt;li&gt;用 target network 估值：$Q_{\theta^-}(s&apos;, a^*)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样“选”与“评”分开，过估计会明显缓解。&lt;/p&gt;
&lt;h2&gt;2) Dueling DQN：把表示能力花在刀刃上&lt;/h2&gt;
&lt;p&gt;文档描述 Dueling 的结构：不直接输出 $Q$，而是分两条路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$V(s)$：状态本身值不值钱&lt;/li&gt;
&lt;li&gt;$A(s,a)$：在这个状态下某个动作相对好多少&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再组合成 $Q(s,a)$。&lt;/p&gt;
&lt;p&gt;直觉：在很多状态下，动作差异不大，但状态好坏差异很大；Dueling 能更高效地学到“状态价值”。&lt;/p&gt;
&lt;h2&gt;3) Prioritized Experience Replay（PER）：把学习预算花在“最有用”的样本上&lt;/h2&gt;
&lt;p&gt;普通 replay buffer 是均匀采样，但很多样本其实没信息量。&lt;/p&gt;
&lt;p&gt;PER 的核心思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TD error 大的样本更值得学&lt;/li&gt;
&lt;li&gt;多学这些样本，收敛更快&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然它也引入偏差，通常会配重要性采样权重来修正。&lt;/p&gt;
&lt;h2&gt;4) NoisyNet：比 ε-greedy 更细腻的探索&lt;/h2&gt;
&lt;p&gt;文档提到噪声网络：在参数空间上加噪声，以改进探索。&lt;/p&gt;
&lt;p&gt;它的优点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;探索是状态相关的&lt;/li&gt;
&lt;li&gt;不需要手动设计 ε 衰减曲线&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直觉：与其“偶尔随机一下动作”，不如让策略本身带一点随机性。&lt;/p&gt;
&lt;h2&gt;本章小结：技巧的顺序建议&lt;/h2&gt;
&lt;p&gt;如果你在项目里想逐步把这些技巧加进去，我更推荐一种“先堵大洞，再追上限”的节奏：Double DQN 几乎是必选项，因为它对过估计的缓解往往是立刻可见的；Dueling 通常改动不大、收益还算稳定，可以作为第二步；PER 的收益可能很大，但它确实更敏感，最好在基础版本已经稳定后再引入；NoisyNet 则更像探索维度的升级，尤其在探索特别困难的任务里会更有价值。&lt;/p&gt;
&lt;p&gt;下一章我们会面对一个更棘手的问题：&lt;strong&gt;动作是连续的&lt;/strong&gt;。你会发现 DQN 的“max over actions”在连续动作下几乎不可用，于是要么用采样/优化近似，要么干脆换一条路线（Actor-Critic）。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CZwDGBDX.jpg"/><enclosure url="/_astro/heroimage.CZwDGBDX.jpg"/></item><item><title>RL 学习笔记（6）：DQN</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-6</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-6</guid><description>DQN</description><pubDate>Wed, 07 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;表格型 Q-learning 很美，但它有一个致命前提：$Q(s,a)$ 能用表存下来。&lt;/p&gt;
&lt;p&gt;一旦状态是图像、连续向量，或者组合爆炸，表格法就直接破产。DQN 的思路很简单：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用神经网络来近似 $Q(s,a)$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但你真写起来会发现：把监督学习那套直接套过来，会非常不稳定。文档提到 DQN 的两大关键工程件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标网络（target network）&lt;/li&gt;
&lt;li&gt;经验回放（experience replay）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这俩几乎就是 DQN 能跑起来的原因。&lt;/p&gt;
&lt;h2&gt;DQN 是什么：深度版 Q-learning&lt;/h2&gt;
&lt;p&gt;文档里给的定义很标准：DQN 是基于深度学习的 Q-learning，结合价值函数近似与神经网络技术。&lt;/p&gt;
&lt;p&gt;我们仍然在学 $Q(s,a)$，仍然会用贪心或 ε-greedy 选动作：&lt;/p&gt;
&lt;p&gt;$$
a_t = \arg\max_a Q_\theta(s_t, a)
$$&lt;/p&gt;
&lt;p&gt;差别是 $Q$ 现在不再是表格，而是网络 $Q_\theta$。&lt;/p&gt;
&lt;h2&gt;为什么会不稳定：自举 + 非独立样本 + 移动目标&lt;/h2&gt;
&lt;p&gt;DQN 的训练目标通常是 TD target：&lt;/p&gt;
&lt;p&gt;$$
y_t = r_{t+1} + \gamma \max_{a&apos;} Q_{\theta^-}(s_{t+1}, a&apos;)
$$&lt;/p&gt;
&lt;p&gt;如果你用同一个网络同时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预测 $Q_\theta$&lt;/li&gt;
&lt;li&gt;产生 target $y_t$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那 target 会跟着你训练一起跑（移动目标），很容易发散。&lt;/p&gt;
&lt;h2&gt;目标网络（Target Network）&lt;/h2&gt;
&lt;p&gt;文档提到用目标网络训练，这是第一个稳定性补丁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;online network：$Q_\theta$&lt;/li&gt;
&lt;li&gt;target network：$Q_{\theta^-}$（参数延迟拷贝）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每隔 N 步把 $\theta$ 复制给 $\theta^-$&lt;/li&gt;
&lt;li&gt;或者做 soft update（更平滑）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;经验回放（Replay Buffer）&lt;/h2&gt;
&lt;p&gt;文档说“经历回放”，它解决的是：样本是序列相关的，直接在线更新会让梯度很偏。&lt;/p&gt;
&lt;p&gt;Replay Buffer 的作用：&lt;/p&gt;
&lt;p&gt;一方面，它通过随机采样 mini-batch 打破序列相关性，让梯度更像在做“近似 i.i.d.”的监督学习；另一方面，它可以反复使用同一条经验，提高样本利用率——这在环境交互昂贵的时候尤其关键。&lt;/p&gt;
&lt;p&gt;工程经验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;buffer 太小会过拟合最近经验&lt;/li&gt;
&lt;li&gt;buffer 太大又会“太离线”，学习变慢&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;DQN 的训练循环（伪代码）&lt;/h2&gt;
&lt;p&gt;你可以把它当成三件事的循环：采样、存、学。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 ε-greedy 从 $Q_\theta$ 选动作&lt;/li&gt;
&lt;li&gt;与环境交互得 $(s,a,r,s&apos;,done)$&lt;/li&gt;
&lt;li&gt;存入 replay buffer&lt;/li&gt;
&lt;li&gt;从 buffer 采样 batch，构造 TD target，用 MSE 回归 $Q_\theta(s,a)$&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;本章小结：DQN 是“稳定性工程”的开端&lt;/h2&gt;
&lt;p&gt;从这一章开始你会发现：深度 RL 的很多算法创新，本质都是在处理同一个难题——&lt;strong&gt;训练不稳定&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下一章我们继续沿着 DQN 往前走：Double / Dueling / PER / NoisyNet……这些“变形”每一个都在对着一个具体痛点开刀。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.BwKKUTN0.jpeg"/><enclosure url="/_astro/heroimage.BwKKUTN0.jpeg"/></item><item><title>RL 学习笔记（5）：PPO</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-5</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-5</guid><description>PPO</description><pubDate>Tue, 06 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;如果说 REINFORCE 的体验是“能学但很抖”，那 PPO 的体验往往是“终于像个工程算法了”。&lt;/p&gt;
&lt;p&gt;PPO 解决的是一个特别现实的问题：&lt;/p&gt;
&lt;p&gt;on-policy 方法要的数据很新鲜，但代价是采样量像无底洞一样被消耗；off-policy 方法能复用数据，样本效率高，可一旦更新把策略推离了数据分布太远，训练又会非常容易崩。PPO 基本就是在这两个诉求之间做平衡：既想把数据用得更充分，又不想让策略每次跨太大一步。&lt;/p&gt;
&lt;p&gt;于是文档里先引入重要性采样，再引出 PPO 的关键点：&lt;strong&gt;限制新旧策略差距&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;重要性采样：用旧数据估计新策略&lt;/h2&gt;
&lt;p&gt;文档说“通过重要性采样，把同策略换成异策略”，直观可以理解为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据是用旧策略 $\pi_{\theta&apos;}$ 采的；&lt;/li&gt;
&lt;li&gt;但我们想优化新策略 $\pi_\theta$；&lt;/li&gt;
&lt;li&gt;用一个比率把分布差异补回来：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
r_t(\theta)=\frac{\pi_\theta(a_t|s_t)}{\pi_{\theta&apos;}(a_t|s_t)}
$$&lt;/p&gt;
&lt;p&gt;问题就在这里：如果新旧策略差太大，这个比率会爆炸或趋近 0，更新会非常不稳定。&lt;/p&gt;
&lt;h2&gt;PPO 的核心：别更新太猛&lt;/h2&gt;
&lt;p&gt;文档描述 PPO 的目标：避免 $p_\theta(a|s)$ 与 $p_{\theta&apos;}(a|s)$ 相差太多。&lt;/p&gt;
&lt;p&gt;TRPO 的做法是用 KL 散度做约束（但实现复杂）；PPO 把“约束”塞进目标函数里，让优化变得像普通的梯度法。&lt;/p&gt;
&lt;p&gt;PPO 最常用的是 clipped objective（文档里虽然没展开公式，但思想一致）：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}^{\text{CLIP}}(\theta) = \mathbb{E}\big[\min(r_t(\theta) A_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) A_t)\big]
$$&lt;/p&gt;
&lt;p&gt;这里 $A_t$ 是优势（通常来自 critic 或 GAE）。&lt;/p&gt;
&lt;h2&gt;PPO 实战要点（我觉得比公式更重要）&lt;/h2&gt;
&lt;h3&gt;1) PPO 仍然是 on-policy&lt;/h3&gt;
&lt;p&gt;文档强调：虽然用了重要性采样，但 PPO 通常只用上一轮策略的数据，所以行为策略和目标策略非常接近，可认为是同策略。&lt;/p&gt;
&lt;p&gt;这是你写代码时必须遵守的约束：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;采样 buffer 不能像 DQN 那样攒很多轮；&lt;/li&gt;
&lt;li&gt;最多复用 K 个 epoch（mini-batch 多轮优化），但还是基于同一批 rollout。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) 优势估计决定了稳定性&lt;/h3&gt;
&lt;p&gt;PPO 本体只是“更新约束”，真正决定学习信号质量的是 $A_t$。&lt;/p&gt;
&lt;p&gt;工程上常见做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 critic 估计 $V(s)$&lt;/li&gt;
&lt;li&gt;用 GAE($\lambda$) 计算优势&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;【图片占位：GAE 计算流程图（TD residual 逐步衰减累加）】&lt;/p&gt;
&lt;h3&gt;3) 两个最常调的旋钮&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;clip epsilon（比如 0.1 ~ 0.3）&lt;/li&gt;
&lt;li&gt;entropy bonus 系数（鼓励探索，避免过早坍缩）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本章小结：PPO 的贡献是“可控”&lt;/h2&gt;
&lt;p&gt;PPO 在我看来最重要的贡献不是它有多“新”，而是它让策略梯度的更新变得可控：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你可以用 KL / clip 来约束更新幅度；&lt;/li&gt;
&lt;li&gt;你可以通过多 epoch 复用同一批数据，提高样本利用率；&lt;/li&gt;
&lt;li&gt;它和 actor-critic 架构天然契合。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一章我们会回到 value-based 阵营：&lt;strong&gt;DQN&lt;/strong&gt;。你会发现它也在做“稳定性工程”，只是手段完全不同：经验回放 + 目标网络。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.DlJe2FB9.jpg"/><enclosure url="/_astro/heroimage.DlJe2FB9.jpg"/></item><item><title>RL 学习笔记（4）：策略梯度</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-4</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-4</guid><description>策略梯度</description><pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;我当年第一次从 Q-learning 切到策略梯度时，最大的爽点是：&lt;strong&gt;我终于不用再绕着 $\arg\max$ 去“间接”得到策略了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在策略梯度里，想法简单到近乎粗暴：&lt;/p&gt;
&lt;p&gt;如果某条轨迹的回报是正的，就让这条轨迹里出现过的动作在对应的状态下更容易被采样到；反过来，如果回报是负的，就把这些动作的概率往下压。你可以把它理解成一种“事后表扬/批评”的机制：等整局结束再回头看，整体表现好的轨迹会把里面的决策都带着一起加分。&lt;/p&gt;
&lt;p&gt;这一章我们就沿着文档的脉络，把这个想法落到可实现的形式：梯度上升、baseline、为每一步分配合适“分数”（credit assignment），以及最经典的 REINFORCE。&lt;/p&gt;
&lt;h2&gt;策略梯度的核心直觉&lt;/h2&gt;
&lt;p&gt;文档里这段话很关键：在轨迹 $\tau$ 中的某一步 $(s_t, a_t)$，如果最终发现轨迹奖励是正的，我们就增加在 $s_t$ 执行 $a_t$ 的概率；反之减少。&lt;/p&gt;
&lt;p&gt;要把它变成可训练的东西，通常我们会：&lt;/p&gt;
&lt;p&gt;实现上我们通常先用一个参数化策略 $\pi_\theta(a|s)$（神经网络输出一个动作分布）来表示“我在某个状态下倾向做什么”，再把目标写成最大化期望回报 $J(\theta)=\mathbb{E}[G]$，然后用梯度上升去更新参数：&lt;/p&gt;
&lt;p&gt;$$
\theta \leftarrow \theta + \eta \nabla_\theta J(\theta)
$$&lt;/p&gt;
&lt;p&gt;这里的 $\eta$ 就是学习率（可以用 Adam / RMSProp）。&lt;/p&gt;
&lt;h2&gt;两个常用技巧：baseline 与 credit assignment&lt;/h2&gt;
&lt;h3&gt;技巧 1：添加基线（baseline）&lt;/h3&gt;
&lt;p&gt;文档里提到一个看似反直觉的问题：有些动作没被采样到，并不代表它不好，但它的概率可能会被“挤下去”。更本质的说法是：&lt;/p&gt;
&lt;p&gt;回报 $G$ 的方差往往很大，而你又直接用 $G$ 去乘 log_prob，这会让更新非常抖。baseline 相当于提供了一个参照系，让你关注“相对这条基线我到底赚没赚”，从而显著降低方差。&lt;/p&gt;
&lt;p&gt;baseline 的作用就是“减去一个不影响期望但能降方差的量”。最经典的 baseline 就是状态价值 $V(s)$，于是出现优势函数：&lt;/p&gt;
&lt;p&gt;$$
A(s,a)=Q(s,a)-V(s)
$$&lt;/p&gt;
&lt;h3&gt;技巧 2：为每一步分配合适的分数&lt;/h3&gt;
&lt;p&gt;同一局游戏里，早期动作可能决定路线，后期动作决定收尾。我们希望每一步都乘以不同的权重，反映“这一步对结果贡献有多大”。&lt;/p&gt;
&lt;p&gt;最常见的做法就是用每一步的未来折扣回报：&lt;/p&gt;
&lt;p&gt;$$
G_t = \sum_{k=0}^{T-t-1} \gamma^k r_{t+k+1}
$$&lt;/p&gt;
&lt;p&gt;这也直接引出 REINFORCE。&lt;/p&gt;
&lt;h2&gt;REINFORCE：蒙特卡洛策略梯度&lt;/h2&gt;
&lt;p&gt;文档的描述很到位：REINFORCE 是回合更新的方式，先收集每一步 reward，再计算每一步 $G_t$，然后用它优化每一步动作输出。&lt;/p&gt;
&lt;p&gt;用更“码农”的语言：&lt;/p&gt;
&lt;p&gt;你可以把实现想象成一次非常朴素的采样-回传：先 rollout 一整个 episode，把 $(s_t, a_t, r_{t+1}, \log\pi(a_t|s_t))$ 都记下来；然后从后往前把每一步的 $G_t$ 算出来；最后把 loss 写成梯度下降形式去反向传播：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L} = - \sum_t \log\pi_\theta(a_t|s_t) \cdot G_t
$$&lt;/p&gt;
&lt;p&gt;然后反向传播。&lt;/p&gt;
&lt;h2&gt;本章小结：先让策略“能学”&lt;/h2&gt;
&lt;p&gt;策略梯度的魅力在于它很自然地处理连续动作，也能把探索写进分布本身。但它也更抖、更依赖工程细节。&lt;/p&gt;
&lt;p&gt;下一章我们会进入一个在实战里更常用的版本：&lt;strong&gt;PPO&lt;/strong&gt;。你可以把 PPO 当成“给策略梯度加上安全带”：让策略每次更新别跨太大步，这对稳定性是质变。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.DTpvinEV.jpg"/><enclosure url="/_astro/heroimage.DTpvinEV.jpg"/></item><item><title>RL 学习笔记（3）：从 MC、TD(0) 到 Sarsa / Q-learning</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-3</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-3</guid><description>当状态动作规模还扛得住时，先做预测（MC/TD），再做控制（Sarsa/Q-learning），顺便把 on-policy/off-policy 的差别讲清楚。</description><pubDate>Sun, 04 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;如果你只靠读公式去学 RL，很容易“听懂了，但不会写”。我一直觉得表格型方法是最好的训练场：&lt;/p&gt;
&lt;p&gt;它的好处在于，你可以把注意力从神经网络训练那堆噪声里抽出来：不需要 optimizer 的小技巧，不会被归一化和梯度爆炸分心；你写下的每一次更新，都几乎是在把贝尔曼方程“翻译成代码”；而 on-policy/off-policy 这类概念，也会因为表格世界足够透明而变得一眼能看出来。&lt;/p&gt;
&lt;p&gt;这一章我们从文档给的脉络出发：先做免模型预测（MC / TD），再过渡到免模型控制（Sarsa / Q-learning）。最后我会补一点“表格法在工程中怎么不翻车”的小技巧。&lt;/p&gt;
&lt;h2&gt;表格型方法的前提：查找表能装下你的世界&lt;/h2&gt;
&lt;p&gt;文档说得很直白：最简单的策略表示就是查找表（look-up table），所以表格型方法的核心资源是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态数量 $|S|$&lt;/li&gt;
&lt;li&gt;动作数量 $|A|$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只要 $|S| \times |A|$ 大到装不下内存，或者根本没法枚举（比如连续状态），你就要去深度方法。&lt;/p&gt;
&lt;p&gt;但在入门与验证直觉时，表格法仍然无敌。&lt;/p&gt;
&lt;h2&gt;免模型预测：MC vs TD&lt;/h2&gt;
&lt;h3&gt;蒙特卡洛策略评估（MC）&lt;/h3&gt;
&lt;p&gt;MC 的关键特点：&lt;strong&gt;等一个 episode 跑完，再用真实回报更新&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;优点：无偏（在足够采样下）。
缺点：方差大、更新慢、必须等回合结束。&lt;/p&gt;
&lt;h3&gt;一步时序差分 TD(0)&lt;/h3&gt;
&lt;p&gt;文档里给了 TD target：&lt;/p&gt;
&lt;p&gt;$$
\text{TD target} = r_{t+1} + \gamma V(s_{t+1})
$$&lt;/p&gt;
&lt;p&gt;它的气质就是：“我不等结局了，我先用下一步的估计来更新现在。”&lt;/p&gt;
&lt;p&gt;优点：在线更新，效率高。
缺点：自举带偏差，且对初始化和学习率更敏感。&lt;/p&gt;
&lt;h2&gt;免模型控制：从 V(s) 走向 Q(s,a)&lt;/h2&gt;
&lt;p&gt;要做控制（找最优策略），仅有 $V(s)$ 不够，因为你需要比较动作。&lt;/p&gt;
&lt;p&gt;文档里强调：用 $Q(s,a)$ 来判断“在什么状态下采取什么动作能拿到最大奖励”。&lt;/p&gt;
&lt;p&gt;在表格世界里，$Q$ 就是一张“状态 × 动作”的表。&lt;/p&gt;
&lt;h2&gt;探索：为什么要 ε-greedy&lt;/h2&gt;
&lt;p&gt;文档提到一个重要假设：为了保证策略迭代能收敛，通常需要“探索性开始”或足够探索。&lt;/p&gt;
&lt;p&gt;工程上最常用的就是 &lt;strong&gt;ε-greedy&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以概率 $\epsilon$ 随机选动作（探索）&lt;/li&gt;
&lt;li&gt;以概率 $1-\epsilon$ 选当前最优动作（利用）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个现实建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一开始 $\epsilon$ 可以大一点（0.8/1.0）；&lt;/li&gt;
&lt;li&gt;然后逐渐衰减到一个小但不为 0 的值（0.05/0.1）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Sarsa：典型 on-policy 控制&lt;/h2&gt;
&lt;p&gt;Sarsa 的名字来自于更新所用的五元组：&lt;/p&gt;
&lt;p&gt;$(s_t, a_t, r_{t+1}, s_{t+1}, a_{t+1})$&lt;/p&gt;
&lt;p&gt;它用“下一步实际会执行的动作”来做更新，所以是 on-policy。&lt;/p&gt;
&lt;p&gt;直觉：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你执行的是 ε-greedy，那么你更新时也会把“偶尔犯傻的随机动作”考虑进去；&lt;/li&gt;
&lt;li&gt;因此它更“胆小”，在一些危险环境里反而更安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q-learning：典型 off-policy 控制&lt;/h2&gt;
&lt;p&gt;文档把 off-policy 的两个策略说得很好：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;行为策略（behavior policy）：负责探索、采数据（可以 ε-greedy）。&lt;/li&gt;
&lt;li&gt;目标策略（target policy）：负责学习最优（通常是贪心）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Q-learning 的更新用的是 $\max_a Q(s_{t+1}, a)$，对应“我假设未来总能走最优动作”。&lt;/p&gt;
&lt;p&gt;直觉：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它可以更大胆地探索，因为学习目标是贪心最优；&lt;/li&gt;
&lt;li&gt;但也更容易出现过估计（这在深度版本 DQN 里会更明显）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本章小结：表格法是“可解释的强化学习”&lt;/h2&gt;
&lt;p&gt;表格法的最大价值不在于它能解决多复杂的问题，而在于它能把 RL 的关键问题讲得很透明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你是否在探索？&lt;/li&gt;
&lt;li&gt;你更新用的是实际动作（on-policy）还是最优动作（off-policy）？&lt;/li&gt;
&lt;li&gt;你的 $\alpha$、$\gamma$ 是否合理？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从下一章开始，我们会切到另一条主线：&lt;strong&gt;策略梯度&lt;/strong&gt;。当动作空间变得连续、或者我们想直接学一个随机策略分布时，PG 会比 Q 表格更自然。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.N8kj42Ad.jpg"/><enclosure url="/_astro/heroimage.N8kj42Ad.jpg"/></item><item><title>RL 学习笔记（2）：MDP、MRP 与贝尔曼方程</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-2</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-2</guid><description>从马尔可夫性质出发，串起 Markov Chain、MRP、MDP、预测与控制、动态规划，以及策略迭代与价值迭代的直觉与实现注意点。</description><pubDate>Sat, 03 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;MDP 这章很容易被写成“符号堆砌”，但我更喜欢把它当成一种工程语言：当你抱怨“奖励太延迟、我不知道该怪哪一步动作”时，MDP 给了你一套把问题拆清楚的坐标系。&lt;/p&gt;
&lt;p&gt;读完这一章，你应该能回答三个实用问题：&lt;/p&gt;
&lt;p&gt;我更希望你带着三个很“落地”的问题往下读：马尔可夫性到底是什么，它为什么是很多算法正确性的前提；贝尔曼方程在工程里到底在干嘛，为什么大家都执着于反复迭代它；以及“预测”和“控制”的边界应该怎么划分——也就是策略迭代和价值迭代分别在什么场景下更趁手。&lt;/p&gt;
&lt;h2&gt;马尔可夫性质：把“历史”压缩到当前状态&lt;/h2&gt;
&lt;p&gt;文档对马尔可夫性质的描述很经典：给定当前状态和所有过去状态，未来只依赖当前状态。&lt;/p&gt;
&lt;p&gt;换成更工程的说法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;只要你的 state 设计得够好，你就不需要记住全部历史。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果环境提供的 observation 不满足马尔可夫性（部分可观测），你就需要在算法外做补救：堆叠帧、RNN、或者构造 belief。&lt;/p&gt;
&lt;h2&gt;从 Markov Chain 到 MRP：先学“在给定策略下评估”&lt;/h2&gt;
&lt;h3&gt;马尔可夫链（Markov Chain）&lt;/h3&gt;
&lt;p&gt;只有状态转移，没有奖励和动作。&lt;/p&gt;
&lt;h3&gt;马尔可夫奖励过程（MRP）&lt;/h3&gt;
&lt;p&gt;在马尔可夫链上加了奖励函数，核心产物是状态价值：&lt;/p&gt;
&lt;p&gt;$$
V(s) = \mathbb{E}[G_t | s_t=s]
$$&lt;/p&gt;
&lt;p&gt;其中回报 $G_t$ 是折扣奖励累积：&lt;/p&gt;
&lt;p&gt;$$
G_t = r_{t+1} + \gamma r_{t+2} + \gamma^2 r_{t+3} + \cdots
$$&lt;/p&gt;
&lt;p&gt;折扣因子 $\gamma$ 在实战里几乎就是“长远程度”的旋钮。&lt;/p&gt;
&lt;h2&gt;贝尔曼方程：把“未来”写成“递归”&lt;/h2&gt;
&lt;p&gt;文档里提到贝尔曼方程定义了当前与未来的关系。它的意义在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你不用真的跑到无穷远去算 $G_t$；&lt;/li&gt;
&lt;li&gt;你可以用“下一步的价值”来更新“当前价值”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 MRP 下的形式可以理解为：&lt;/p&gt;
&lt;p&gt;$$
V(s) = \mathbb{E}[r_{t+1} + \gamma V(s_{t+1}) | s_t=s]
$$&lt;/p&gt;
&lt;p&gt;这句式子就是后面动态规划、TD 学习、甚至深度 RL 的祖宗。&lt;/p&gt;
&lt;h2&gt;三种估值路线：DP / Monte Carlo / TD&lt;/h2&gt;
&lt;p&gt;文档把三类方法并列得很好，我再补一点“你写代码时会怎么选”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;动态规划（DP）&lt;/strong&gt;：需要环境完全已知（$P, R$），能遍历所有状态。优点是稳定、可证明；缺点是现实中很少满足。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;蒙特卡洛（MC）&lt;/strong&gt;：不需要模型，用完整 episode 的回报做估计。优点是无偏；缺点是方差大、必须等回合结束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时序差分（TD）&lt;/strong&gt;：介于两者之间，一边采样一边自举（bootstrapping）。优点是在线更新；缺点是有偏但通常更高效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;从 MRP 到 MDP：加入动作，问题从“评估”变成“决策”&lt;/h2&gt;
&lt;p&gt;MDP 相比 MRP 多了动作：未来不仅依赖当前状态，也依赖智能体在当前状态采取的动作。&lt;/p&gt;
&lt;p&gt;MDP 的核心对象是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;policy $\pi(a|s)$&lt;/li&gt;
&lt;li&gt;value function $V^\pi(s)$ 或 $Q^\pi(s,a)$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;预测（Prediction）与控制（Control）&lt;/h2&gt;
&lt;p&gt;文档这段是考试高频，但对工程也很关键：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预测&lt;/strong&gt;：给定 MDP + 给定策略 $\pi$，求 $V^\pi$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;控制&lt;/strong&gt;：给定 MDP（但策略未知），求最优策略 $\pi^&lt;em&gt;$ 与最优价值 $V^&lt;/em&gt;$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预测 = 你让我评估这个策略靠谱不靠谱；&lt;/li&gt;
&lt;li&gt;控制 = 我自己想办法找一个最靠谱的策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;动态规划解控制：策略迭代 vs 价值迭代&lt;/h2&gt;
&lt;h3&gt;策略迭代（Policy Iteration）&lt;/h3&gt;
&lt;p&gt;两步循环：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;策略评估&lt;/strong&gt;：固定 $\pi$，求 $V^\pi$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略改进&lt;/strong&gt;：用 $V^\pi$ 推导更好的策略（对 $Q$ 做贪心）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;价值迭代（Value Iteration）&lt;/h3&gt;
&lt;p&gt;直接对贝尔曼最优方程迭代，得到 $V^&lt;em&gt;$，再提取 $\pi^&lt;/em&gt;$。&lt;/p&gt;
&lt;h2&gt;本章小结：MDP 是一张地图&lt;/h2&gt;
&lt;p&gt;我一直觉得 MDP 像“地图”：它告诉你 RL 里有哪些变量、哪些依赖关系是算法成立的前提。&lt;/p&gt;
&lt;p&gt;后面你会看到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表格型方法（Sarsa/Q-learning）是在 MDP 上给出“可在线迭代”的控制算法；&lt;/li&gt;
&lt;li&gt;策略梯度/PPO 是绕开显式 $Q$ 的另一条路；&lt;/li&gt;
&lt;li&gt;DQN、Actor-Critic 则把这些对象用神经网络做近似。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一章我们就从最朴素的地方开始：&lt;strong&gt;如果状态空间不大，直接用表格去学&lt;/strong&gt;，会发生什么？&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CmW_oGd9.jpg"/><enclosure url="/_astro/heroimage.CmW_oGd9.jpg"/></item><item><title>RL 学习笔记（1）：强化学习到底在学什么</title><link>https://xiaohei-blog.vercel.app/blog/rl-learning-1</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-learning-1</guid><description>从“监督学习不适用”讲起，梳理强化学习的输入输出、探索与利用、状态与观测，以及 value-based / policy-based / actor-critic 的基本分工。</description><pubDate>Fri, 02 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我一直想把强化学习的“概念—算法—工程直觉”串成一条能照着走的路线，而不是东一榔头西一棒子，今天看 DQN，明天又被 PPO 劝退。&lt;/p&gt;
&lt;p&gt;所以这次我把我曾经学习时所记录的笔记与思考整理为按章节拆成 &lt;strong&gt;12 篇博客&lt;/strong&gt;。你可以把它当成一个从入门到进阶的最小闭环。先把概念讲清楚，再把经典算法跑通。&lt;/p&gt;
&lt;p&gt;另外，需要说明的是，这个系列的博客是由我的幕布笔记转化而来，如果你更喜欢图文并茂的阅读，你可以去我的&lt;a href=&quot;EasyRL-base%EF%BC%9Ahttps://share.mubu.com/doc/7J8Kb2-DFkG&quot;&gt;幕布空间&lt;/a&gt;进行阅读,受限于篇幅的原因，第十二章&lt;a href=&quot;EasyRL-DDPG%EF%BC%9Ahttps://share.mubu.com/doc/61Hn_ilTsQG&quot;&gt;幕布笔记&lt;/a&gt;在这。如果你发现有哪些地方由逻辑错误，可以通过评论告知我，十分感谢！&lt;/p&gt;
&lt;h2&gt;本系列文章目录（每章一篇）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;第 1 章：强化学习基础&lt;/li&gt;
&lt;li&gt;第 2 章：MDP、MRP 与贝尔曼方程&lt;/li&gt;
&lt;li&gt;第 3 章：表格型方法&lt;/li&gt;
&lt;li&gt;第 4 章：策略梯度与 REINFORCE&lt;/li&gt;
&lt;li&gt;第 5 章：PPO&lt;/li&gt;
&lt;li&gt;第 6 章：DQN&lt;/li&gt;
&lt;li&gt;第 7 章：DQN 进阶技巧&lt;/li&gt;
&lt;li&gt;第 8 章：连续动作下的 Q 方法困境与思路&lt;/li&gt;
&lt;li&gt;第 9 章：Actor-Critic / A2C&lt;/li&gt;
&lt;li&gt;第 10 章：稀疏奖励（reward shaping / ICM）&lt;/li&gt;
&lt;li&gt;第 11 章：模仿学习（BC / DAgger / IRL）&lt;/li&gt;
&lt;li&gt;第 12 章：深度确定性策略梯度算法（deep deterministic policy gradient，DDPG）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;我建议的阅读方式&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先读 1~3 章&lt;/strong&gt;：把闭环与 on-policy/off-policy 的直觉建立起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;再读 4~6 章&lt;/strong&gt;：理解两条主线（PG/PPO 与 DQN）的差异。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最后读 8~12 章&lt;/strong&gt;：把“连续控制、稀疏奖励、模仿学习”这些更真实的难点补齐。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你后续打算把它们落到代码项目里，我也建议按这个顺序做：表格法先跑通 → DQN/PPO 二选一深入 → 再上 actor-critic 连续控制。&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;我第一次认真写强化学习代码时，最困惑的不是公式，而是一个很朴素的问题：&lt;strong&gt;它到底在学什么？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;监督学习里你犯错了，老师会告诉你“正确答案是什么”；强化学习里你犯错了，环境最多给你一个很晚才到的反馈（甚至一句“你很菜”都不说，只给 0 分）。于是很多“在监督学习里理所当然”的假设，在这里都会失效：样本不是独立同分布，奖励还会延迟，训练过程像在黑屋子里摸索。&lt;/p&gt;
&lt;p&gt;这一章我会按我自己理解 RL 的顺序，把最基础的概念串起来：为什么 RL 和监督学习不同、状态/观测怎么区分、策略/价值函数/模型分别扮演什么角色，以及三类常见智能体（value-based、policy-based、actor-critic）各自适合什么任务。&lt;/p&gt;
&lt;h2&gt;强化学习为什么“比监督学习难”&lt;/h2&gt;
&lt;p&gt;在文档里有一句话我很认同：强化学习的难点，很多来自于监督学习的两个假设在这里站不住。&lt;/p&gt;
&lt;p&gt;最关键的第一个变化是&lt;strong&gt;数据不再是 i.i.d.&lt;/strong&gt;：智能体拿到的是一段连续轨迹，上一帧和下一帧强相关，你把它当作打乱后的监督数据去喂网络，梯度往往会偏得离谱。第二个变化是&lt;strong&gt;奖励会延迟&lt;/strong&gt;：你今天的一个动作，可能要过几十步才知道好坏，环境也不会像老师一样告诉你“正确动作是什么”。这两点叠在一起，会把很多工程细节都放大——你写 loss 时很容易对不上因果，训练曲线看起来在下降但策略不动；同时你会更依赖训练监控去判断到底有没有学到东西，比如 reward 曲线、episode length、value/Q 的数值范围与方差。&lt;/p&gt;
&lt;h2&gt;标准强化学习 vs 深度强化学习&lt;/h2&gt;
&lt;p&gt;所谓&lt;strong&gt;标准强化学习&lt;/strong&gt;，更像是先把状态写成你能处理的特征，再用表格或相对简单的函数去估计价值或策略；而**深度强化学习（DRL）**则更“端到端”，让神经网络直接从状态/观测映射到动作或价值。这个区分在项目里很实用，因为它决定了你调参到底在调什么：表格法更像数学题，探索策略、学习率与折扣因子往往是主角；深度方法则更像“深度学习 + 强化学习”叠加，除了 RL 的不稳定，你还得同时照顾优化器、归一化、网络容量、梯度爆炸这类深度学习常见问题。&lt;/p&gt;
&lt;p&gt;这一点在项目里很重要，因为它决定了你调参的重点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表格法更像“数学题”，很多时候是探索策略、学习率、折扣因子在起作用；&lt;/li&gt;
&lt;li&gt;深度方法更像“深度学习 + 强化学习”，除了 RL 的不稳定，还叠加了网络训练的不稳定（梯度爆炸、归一化、网络容量等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;状态（State）与观测（Observation）&lt;/h2&gt;
&lt;p&gt;文档的区分很关键：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;状态&lt;/strong&gt;：对世界的完整描述，没有隐藏信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;观测&lt;/strong&gt;：状态的部分描述，可能遗漏信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这会影响你是否需要“记忆”。如果观测不满足马尔可夫性（比如只看到局部画面），那你可能需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆叠帧（frame stacking）&lt;/li&gt;
&lt;li&gt;加 RNN（LSTM/GRU）&lt;/li&gt;
&lt;li&gt;或者显式构建 belief state&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;强化学习智能体的三大组件：策略、价值函数、模型&lt;/h2&gt;
&lt;h3&gt;1) 策略（Policy）&lt;/h3&gt;
&lt;p&gt;策略回答的是：在状态 $s$ 下要选什么动作 $a$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;随机策略&lt;/strong&gt;：输出动作的概率分布 $\pi(a|s)$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定性策略&lt;/strong&gt;：直接输出一个动作 $a = \mu(s)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实践里我更推荐先用随机策略做探索（尤其是连续动作），因为探索是策略的一部分，不需要额外“贴 epsilon”。&lt;/p&gt;
&lt;h3&gt;2) 价值函数（Value Function）&lt;/h3&gt;
&lt;p&gt;价值函数是对未来奖励的预测，用来评估“当前好不好”。文档提到的折扣因子 $\gamma$ 是核心超参之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\gamma$ 小：更“近视”，更关注短期奖励。&lt;/li&gt;
&lt;li&gt;$\gamma$ 大：更“远视”，更关注长期回报。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见的两类价值函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态价值：$V(s)$&lt;/li&gt;
&lt;li&gt;动作价值：$Q(s, a)$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3) 模型（Model）&lt;/h3&gt;
&lt;p&gt;模型描述环境如何演化：从 $(s, a)$ 到下一步 $(s&apos;, r)$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;有模型（model-based）&lt;/strong&gt;：试图学习或已知转移与奖励。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;免模型（model-free）&lt;/strong&gt;：不显式学习转移，直接学策略或价值&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三类常见智能体：value-based / policy-based / actor-critic&lt;/h2&gt;
&lt;p&gt;如果按“你到底显式学了什么”来划分，我更喜欢把它看成三种气质：&lt;strong&gt;value-based&lt;/strong&gt; 会先把 $Q/V$ 学出来，然后策略往往是隐式提取的（比如在离散动作里做 $\arg\max_a Q(s,a)$），典型代表就是 Q-learning 和 DQN；&lt;strong&gt;policy-based&lt;/strong&gt; 则干脆直接学 $\pi_\theta(a|s)$ 这件事本身，代表是 REINFORCE 和 PPO；而 &lt;strong&gt;actor-critic&lt;/strong&gt; 把两者拼在一起，让策略（actor）和价值（critic）同时学习、互相加速，像 A2C、DDPG、SAC 都属于这一类。&lt;/p&gt;
&lt;h2&gt;本章小结：先把“闭环”想清楚&lt;/h2&gt;
&lt;p&gt;如果你只记住一件事，我希望是这句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;强化学习永远在做一个闭环：&lt;strong&gt;采样（交互）→ 估计（价值/优势）→ 更新（策略/价值）→ 再采样&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;后面的章节无论是 MDP、表格法、还是 PPO/DQN 这些大名鼎鼎的算法，本质都在这个闭环里换不同的估计方式和更新方式。&lt;/p&gt;
&lt;p&gt;下一章我们会把这个闭环写成数学对象：马尔可夫过程、奖励过程、MDP、贝尔曼方程——你会发现，很多“看起来很抽象”的符号，其实是在回答工程里非常具体的问题：&lt;strong&gt;我怎么把未来折扣奖励写成一个可迭代的更新式？&lt;/strong&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CreDR_rs.jpg"/><enclosure url="/_astro/heroimage.CreDR_rs.jpg"/></item><item><title>强化学习算法程序实践（1）：通用训练框架 + Q-Learning / Sarsa</title><link>https://xiaohei-blog.vercel.app/blog/rl-algorithm-1</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-algorithm-1</guid><description>从一个可落地 Q-Learning 与 Sarsa、epsilon-greedy、回合训练循环、以及保存与加载的最小实践。</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我在做强化学习实验时，最痛苦的不是推公式，而是：&lt;strong&gt;同一个算法换个环境就要重写一堆训练脚手架&lt;/strong&gt;。后来我索性把常用套路总结成一个“可复用的算法骨架”，再把每个算法的差异点（几乎都集中在 &lt;code&gt;sample&lt;/code&gt; 和 &lt;code&gt;update&lt;/code&gt;）填进去。&lt;/p&gt;
&lt;p&gt;更具体一点说，我踩过的坑基本都集中在“工程细节被忽略”上：同样的算法，有时候不是你公式写错了，而是你训练循环里 &lt;code&gt;terminated/truncated&lt;/code&gt; 没处理好、epsilon 衰减太快导致过早收敛、或者你没有把每回合最大步数限制住导致训练数据统计完全不一致。于是到最后你只能对着一条乱七八糟的 reward 曲线发呆，完全不知道是环境问题还是代码问题。&lt;/p&gt;
&lt;p&gt;所以这篇我会很刻意地把“程序骨架”写得死一点：有哪些函数、训练/测试循环怎么摆、每一步该打印什么，尽量让你在写第二个、第三个算法时不用再从头搭脚手架。&lt;/p&gt;
&lt;p&gt;这篇是系列第一篇，目标很明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给出一个&lt;strong&gt;可复用的 RL 项目代码结构&lt;/strong&gt;（训练 / 测试 / 环境 / 参数）&lt;/li&gt;
&lt;li&gt;用它实现经典的 &lt;strong&gt;Q-Learning&lt;/strong&gt;（off-policy）&lt;/li&gt;
&lt;li&gt;以及和它只有一处关键差别的 &lt;strong&gt;Sarsa&lt;/strong&gt;（on-policy）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续文章我会按“值函数系（DQN 家族）”和“策略梯度 / Actor-Critic 系”继续拆分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 2 篇：DQN / Double DQN / Dueling DQN / Noisy DQN / PER-DQN&lt;/li&gt;
&lt;li&gt;第 3 篇：Policy Gradient（REINFORCE）/ PPO / A2C&lt;/li&gt;
&lt;li&gt;第 4 篇：DDPG / TD3 / SAC（连续控制三件套）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;一个通用的 RL 代码骨架&lt;/h2&gt;
&lt;p&gt;不管是表格型方法（Q-table），还是深度强化学习（DQN 及其变体），我习惯把智能体抽象成 4 个核心动作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sample(state)&lt;/code&gt;：训练时采样动作（带探索，Exploration）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;predict(state)&lt;/code&gt;：测试时输出动作（不探索，Exploitation）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update(transition)&lt;/code&gt;：用交互数据更新策略&lt;/li&gt;
&lt;li&gt;&lt;code&gt;save()/load()&lt;/code&gt;：保存与加载（可选，但强烈推荐）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中&lt;strong&gt;最关键的提醒&lt;/strong&gt;是：对不同算法而言，&lt;code&gt;sample&lt;/code&gt; 和 &lt;code&gt;update&lt;/code&gt; 的实现差异很大；其它（训练循环、日志、保存/加载）通常大同小异。&lt;/p&gt;
&lt;h3&gt;训练循环（定义训练）&lt;/h3&gt;
&lt;p&gt;一个“回合制（episode-based）”的训练循环，基本就是下面这个顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;回合开始：&lt;code&gt;state = env.reset()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设定每回合最大步数 &lt;code&gt;max_steps&lt;/code&gt;（帮助更快收敛，也避免一直跑不结束）&lt;/li&gt;
&lt;li&gt;循环交互直到终止：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;action = agent.sample(state)&lt;/code&gt;（探索策略）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next_state, reward, terminated, truncated, info = env.step(action)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;构造 &lt;code&gt;transition&lt;/code&gt;（必要时写入 memory）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;agent.update(transition)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state = next_state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;terminated or truncated&lt;/code&gt; 则结束回合&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;用几行伪代码把它写死（推荐你直接复制作为项目模板）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def train(env, agent, num_episodes: int, max_steps: int):
	for ep in range(num_episodes):
		state, info = env.reset()
		ep_return = 0.0

		for t in range(max_steps):
			action = agent.sample(state)
			next_state, reward, terminated, truncated, info = env.step(action)

			agent.update(state, action, reward, next_state, terminated)

			ep_return += reward
			state = next_state
			if terminated or truncated:
				break

		print(f&quot;episode={ep} return={ep_return:.1f} eps={agent.epsilon:.3f}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试循环（定义测试）&lt;/h3&gt;
&lt;p&gt;测试和训练长得很像，但有两点必须改：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不要更新&lt;/strong&gt;：测试只是评估性能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要 sample&lt;/strong&gt;：用 &lt;code&gt;predict&lt;/code&gt; 走纯利用（Exploitation）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def evaluate(env, agent, num_episodes: int, max_steps: int):
	returns = []
	for ep in range(num_episodes):
		state, info = env.reset()
		ep_return = 0.0
		for t in range(max_steps):
			action = agent.predict(state)
			next_state, reward, terminated, truncated, info = env.step(action)
			ep_return += reward
			state = next_state
			if terminated or truncated:
				break
		returns.append(ep_return)
		print(f&quot;[eval] episode={ep} return={ep_return:.1f}&quot;)
	return sum(returns) / len(returns)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;环境：用 Gym 就够了（需要自定义时只看 reset/step）&lt;/h3&gt;
&lt;p&gt;大多数情况下我不会自己造环境：Gym/Gymnasium 已经足够。&lt;/p&gt;
&lt;p&gt;如果你必须自定义环境，最关键就是对齐这两个接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reset()&lt;/code&gt;：回合开始，返回初始状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;step(action)&lt;/code&gt;：执行动作，返回 &lt;code&gt;(next_state, reward, terminated, truncated, info)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q-Learning：从“骨架”到可跑的算法&lt;/h2&gt;
&lt;h3&gt;Q-Learning 的三个核心函数&lt;/h3&gt;
&lt;p&gt;Q-Learning（表格法）里我们维护一个 $Q(s,a)$ 表。把它塞进“骨架”里，你会发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;predict(state)&lt;/code&gt;：选 $\arg\max_a Q(s,a)$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sample(state)&lt;/code&gt;：在 &lt;code&gt;predict&lt;/code&gt; 基础上加探索（epsilon-greedy / UCB etc.）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update(...)&lt;/code&gt;：就是贝尔曼更新（TD）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;epsilon-greedy（最常用的探索策略）&lt;/h4&gt;
&lt;p&gt;训练过程一般是：前期探索多、后期逐步收敛，也就是让 $\epsilon$ 从大到小。&lt;/p&gt;
&lt;p&gt;通常会设三元组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;epsilon_start&lt;/code&gt;：初始探索率（常见 0.95）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epsilon_end&lt;/code&gt;：最小探索率（常见 0.01，留一点探索避免错过更优策略）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epsilon_decay&lt;/code&gt;：衰减速度（太快容易“过早收敛/过拟合”，太慢收敛会拖）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个简单衰减：
$$
\epsilon \leftarrow \max(\epsilon_{end}, \epsilon \cdot \epsilon_{decay})
$$&lt;/p&gt;
&lt;h3&gt;一份“能直接套用”的 Q-Learning 类&lt;/h3&gt;
&lt;p&gt;下面代码是我常用的最小实现（离散状态/动作）。写成类是为了和 DQN 等深度算法保持一致的接口。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random
from collections import defaultdict
from dataclasses import dataclass


@dataclass
class QLearningConfig:
	gamma: float = 0.99
	lr: float = 0.1
	epsilon_start: float = 0.95
	epsilon_end: float = 0.01
	epsilon_decay: float = 0.995


class QLearningAgent:
	def __init__(self, n_actions: int, cfg: QLearningConfig):
		self.n_actions = n_actions
		self.cfg = cfg

		self.Q = defaultdict(lambda: [0.0 for _ in range(n_actions)])
		self.epsilon = cfg.epsilon_start

	def sample(self, state):
		# Exploration + exploitation
		if random.random() &amp;#x3C; self.epsilon:
			return random.randrange(self.n_actions)
		return self.predict(state)

	def predict(self, state):
		q = self.Q[state]
		return int(max(range(self.n_actions), key=lambda a: q[a]))

	def update(self, state, action, reward, next_state, terminated: bool):
		q_sa = self.Q[state][action]
		next_q_max = 0.0 if terminated else max(self.Q[next_state])
		target = reward + self.cfg.gamma * next_q_max
		self.Q[state][action] = q_sa + self.cfg.lr * (target - q_sa)

		# decay epsilon once per step (or per episode, both ok; step 更细)
		self.epsilon = max(self.cfg.epsilon_end, self.epsilon * self.cfg.epsilon_decay)

	def save(self, path: str):
		import json
		with open(path, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
			json.dump({str(k): v for k, v in self.Q.items()}, f, ensure_ascii=False)

	def load(self, path: str):
		import json
		with open(path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
			obj = json.load(f)
		self.Q = defaultdict(lambda: [0.0 for _ in range(self.n_actions)])
		for k, v in obj.items():
			self.Q[k] = v
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sarsa：和 Q-Learning 只差一个 update&lt;/h2&gt;
&lt;p&gt;Sarsa 和 Q-Learning 的核心差别，一句话总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sarsa&lt;/strong&gt;：用“实际执行的下一步动作”更新（on-policy）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q-Learning&lt;/strong&gt;：用“假设下一步最优动作”更新（off-policy）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写成更新目标（只看差异就好）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q-Learning：$r + \gamma \max_{a&apos;} Q(s&apos;, a&apos;)$&lt;/li&gt;
&lt;li&gt;Sarsa：$r + \gamma Q(s&apos;, a_{next})$，其中 $a_{next}$ 是用当前策略在 $s&apos;$ 上采样的动作&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sarsa 的最小实现（只展示 update 差异）&lt;/h3&gt;
&lt;p&gt;Sarsa 的代码组织和 Q-Learning 几乎一致，主要差别在 &lt;code&gt;update&lt;/code&gt; 需要 &lt;code&gt;next_action&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class SarsaAgent(QLearningAgent):
	def update(self, state, action, reward, next_state, next_action, terminated: bool):
		q_sa = self.Q[state][action]
		next_q = 0.0 if terminated else self.Q[next_state][next_action]
		target = reward + self.cfg.gamma * next_q
		self.Q[state][action] = q_sa + self.cfg.lr * (target - q_sa)
		self.epsilon = max(self.cfg.epsilon_end, self.epsilon * self.cfg.epsilon_decay)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应训练循环也要改一行：先拿到 &lt;code&gt;next_action&lt;/code&gt; 再更新。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;action = agent.sample(state)
next_state, reward, terminated, truncated, info = env.step(action)
next_action = agent.sample(next_state)
agent.update(state, action, reward, next_state, next_action, terminated)
state = next_state
action = next_action
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;你如果只记住一件事，那就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;训练/测试循环可以固定成模板&lt;/strong&gt;，主要改 &lt;code&gt;sample/predict/update&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一篇我会把这个模板升级到“深度强化学习”版本：引入 Replay Buffer、目标网络，逐步讲清楚 DQN 以及它的几种常见改进（Double / Dueling / Noisy / PER）。&lt;/p&gt;
&lt;h3&gt;我常用的调参/调试顺序（很实用）&lt;/h3&gt;
&lt;p&gt;最后留一段我自己最常用的“排障流程”。强化学习的坏处是：它不会像监督学习那样一眼看出你是不是 overfit，它更像一锅粥，哪一步有问题都可能表现成“reward 不涨”。我一般按下面顺序来定位：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先把环境跑通&lt;/strong&gt;：随机策略能不能结束回合？&lt;code&gt;terminated/truncated&lt;/code&gt; 是否合理？reward 的量级大概是多少？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把日志打印得足够具体&lt;/strong&gt;：每回合 return、每步/每回合 epsilon、以及“是否提前结束”。如果是 Q 表法，我还会打印 &lt;code&gt;max(Q[state])&lt;/code&gt; 的量级看有没有爆炸。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;epsilon 先别衰减太快&lt;/strong&gt;：我最常犯的错误是 &lt;code&gt;epsilon_decay&lt;/code&gt; 设太狠，导致前几十回合就开始“自信地瞎走”。如果你看到 reward 一开始有波动、很快就僵住，优先怀疑探索不足。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gamma 和 max_steps 要配套&lt;/strong&gt;：&lt;code&gt;gamma&lt;/code&gt; 大的时候，回报有效视野更长；如果你 &lt;code&gt;max_steps&lt;/code&gt; 又很短，很多环境会变成“怎么都学不出来”。反过来也是：&lt;code&gt;max_steps&lt;/code&gt; 太长会让训练变慢且方差更大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先在最小环境验证&lt;/strong&gt;：像 FrozenLake / Taxi 这种可以快速验证“训练循环是否正确”。骨架确认没问题，再搬到复杂环境会省掉很多时间。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/heroimage.DR_jwJiT.jpg"/><enclosure url="/_astro/heroimage.DR_jwJiT.jpg"/></item><item><title>强化学习算法程序实践（2）：DQN 及其改进</title><link>https://xiaohei-blog.vercel.app/blog/rl-algorithm-2</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-algorithm-2</guid><description>从 Q-table 走向深度强化学习</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;第 1 篇我们把“回合训练骨架”固定了，并用 Q-Learning/Sarsa 验证：很多算法的工程结构可以复用。&lt;/p&gt;
&lt;p&gt;这篇开始进入深度强化学习：&lt;strong&gt;DQN（Deep Q-Network）&lt;/strong&gt;。它的本质很简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用神经网络近似 $Q(s,a)$，替代原来的 Q-table&lt;/li&gt;
&lt;li&gt;为了让训练稳定、样本利用率更高，引入两件关键装备：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;经验回放（Replay Buffer）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标网络（Target Network）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我下面会按我自己做实验时的“演化路线”把它们串起来：先把最朴素的 DQN 跑通，再逐个替换模块，最后得到一套更稳、更省心的版本（Double / Dueling / Noisy / PER）。你会发现这些改进并没有把 DQN 变得面目全非，它们大多只是在某个关键环节多加了一块垫片：要么让 target 更可信，要么让探索更自然，要么让 replay 更“记重点”。&lt;/p&gt;
&lt;p&gt;我自己真正开始“理解 DQN”是从一次失败开始的：代码能跑、loss 也在降，但 reward 纹丝不动；甚至有时候 reward 还会先上去一小段，然后突然崩得一干二净。后来我才意识到，DQN 的难点从来不在“会不会写反传”，而在“你的训练信号到底稳不稳”。如果 target 网络没更新好、replay 的采样没有打散相关性、或者奖励尺度和学习率不匹配，你得到的梯度就像在噪声里摸黑，越走越偏。&lt;/p&gt;
&lt;p&gt;所以这篇我会尽量用工程语言把它讲清楚：&lt;strong&gt;DQN 为什么要 Replay、为什么要 Target、update 的 target 到底在干什么&lt;/strong&gt;，以及你可以按什么顺序迭代，把一个“能跑的 DQN”逐步打磨成一个“训练稳定的 DQN”。&lt;/p&gt;
&lt;h2&gt;DQN：相对 Q-Learning 改了什么？&lt;/h2&gt;
&lt;p&gt;如果把 Q-Learning 看作“边走边把 Q 表格填完整”，那 DQN 就是“我不填表了，我训练一个函数去拟合这张表”。为了让这个函数（神经网络）训练得稳定，我一般会把 DQN 的变化拆成三件事来记：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用网络替代表&lt;/strong&gt;：$Q(s,a;\theta)$，输入是 state，输出是每个动作的 Q 值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replay Buffer&lt;/strong&gt;：把交互数据存起来，更新时随机采样一批（batch），提升样本效率并打破相关性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略网络 + 目标网络&lt;/strong&gt;：用 $\theta$ 在线更新策略网络，用 $\theta^-$ 的目标网络去计算 target，定期拷贝参数来稳住训练。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;一个可复用的 DQN Agent 接口&lt;/h3&gt;
&lt;p&gt;和第 1 篇一致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sample(state)&lt;/code&gt;：训练用（通常 epsilon-greedy）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;predict(state)&lt;/code&gt;：测试用（argmax）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update()&lt;/code&gt;：从 Replay Buffer 采样 batch 更新网络&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：从 DQN 开始，&lt;code&gt;update()&lt;/code&gt; 往往不再接受单条 transition，而是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先 &lt;code&gt;push(transition)&lt;/code&gt; 到 replay&lt;/li&gt;
&lt;li&gt;再在合适时机（buffer 足够、间隔到达）调用 &lt;code&gt;update()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我个人很推荐把 “push 交互数据” 和 “update 如果条件满足就更新” 分开写。因为你后面无论是加 PER（采样变了）、还是加 Noisy（探索变了）、还是加 Double（target 算法变了），这一层的训练主循环都不需要大改，改动会被限制在 buffer 或 update 的内部。&lt;/p&gt;
&lt;h2&gt;Replay Buffer：push + sample 就够用&lt;/h2&gt;
&lt;p&gt;我一般把经验回放（Replay Buffer）的实现压缩成两个方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push&lt;/code&gt;：按顺序存 transition，满了就挤掉最旧的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sample&lt;/code&gt;：随机采样出一个 batch&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最小结构通常是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random
from collections import deque


class ReplayBuffer:
	def __init__(self, capacity: int):
		self.buffer = deque(maxlen=capacity)

	def push(self, transition):
		self.buffer.append(transition)

	def sample(self, batch_size: int):
		batch = random.sample(self.buffer, batch_size)
		# 真实代码会在这里做 unzip + tensor 化
		return batch

	def __len__(self):
		return len(self.buffer)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DQN 的 update：损失怎么来的？&lt;/h2&gt;
&lt;p&gt;我第一次实现 DQN 时，最容易卡住的点其实不是反传，而是“target 到底应该怎么算”。把它想清楚后，整个 update 就很机械：拿一批数据算出 target，再让网络的输出去贴近这个 target。&lt;/p&gt;
&lt;p&gt;在最基础的 DQN 里，损失就是“期望值 $y_i$”和“实际值 $Q(s_i,a_i;\theta)$”的均方差。&lt;/p&gt;
&lt;h3&gt;target（期望值）&lt;/h3&gt;
&lt;p&gt;基础 DQN 常用：
$$
y_i = r_i + \gamma \max_{a&apos;} Q(s&apos;_i, a&apos;; \theta^-)
$$&lt;/p&gt;
&lt;p&gt;并且要处理终止状态：如果 &lt;code&gt;terminated==True&lt;/code&gt;，没有下一个状态，就直接 $y_i=r_i$。&lt;/p&gt;
&lt;h3&gt;loss（均方差）&lt;/h3&gt;
&lt;p&gt;$$
\mathcal{L}(\theta)=\frac{1}{N}\sum_i (y_i - Q(s_i,a_i;\theta))^2
$$&lt;/p&gt;
&lt;p&gt;然后照常：定义 optimizer，&lt;code&gt;loss.backward()&lt;/code&gt;，&lt;code&gt;optimizer.step()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Dueling DQN：把 Q 网络拆成 V + A&lt;/h2&gt;
&lt;p&gt;有些环境里（尤其状态复杂、但动作影响没那么明显的阶段），我会发现“学哪个动作更好”这件事很难，反而“这个状态整体值不值得继续待”更重要。Dueling 的直觉就是把这两件事分开学：先学状态价值 $V(s)$，再学动作相对优势 $A(s,a)$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Value&lt;/strong&gt;：估计状态价值 $V(s)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advantage&lt;/strong&gt;：估计每个动作相对优势 $A(s,a)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后组合得到 $Q(s,a)$。&lt;/p&gt;
&lt;p&gt;工程上你只需要关注两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;网络 forward 输出从“直接输出 Q”变成“输出 V 和 A，再合成 Q”&lt;/li&gt;
&lt;li&gt;其它（Replay、target、update）基本不变&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Double DQN：缓解过估计&lt;/h2&gt;
&lt;p&gt;基础 DQN 的一个老毛病是过估计：因为我们用同一个网络（或同一个估计过程）既“选最大动作”，又“评估这个最大动作的值”，这很容易把噪声也当成真相。&lt;/p&gt;
&lt;p&gt;Double DQN 的关键改动是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用策略网络选动作（argmax）&lt;/li&gt;
&lt;li&gt;用目标网络评估该动作的价值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话的工程翻译：只改 target 的计算方式，其它不动。&lt;/p&gt;
&lt;h2&gt;Noisy DQN：用可学习噪声做探索&lt;/h2&gt;
&lt;p&gt;Noisy DQN 的重点在“模型定义”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;Linear&lt;/code&gt; 层里引入 &lt;code&gt;mu/sigma&lt;/code&gt; 参数&lt;/li&gt;
&lt;li&gt;每次 forward 都注入噪声（训练时），并能 reset&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它的好处是：在很多任务里，比手动调 epsilon 更省心。&lt;/p&gt;
&lt;p&gt;你可以把它理解为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不再由外部策略（epsilon-greedy）给动作加随机性，而是让网络自己学会在哪些状态该更“抖”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;PER-DQN：优先经验回放（SumTree）&lt;/h2&gt;
&lt;p&gt;PER 的工程要点我通常拆成两大块：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SumTree&lt;/strong&gt;：用 $O(\log n)$ 管理样本优先级，并按优先级采样&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Importance Sampling&lt;/strong&gt;：用权重修正“非均匀采样”带来的偏差&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实现上可以拆成三个类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SumTree&lt;/code&gt;：维护二叉树和优先级更新&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReplayTree&lt;/code&gt;：基于 SumTree 的 replay buffer（push、sample、batch_update）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PERDQNAgent&lt;/code&gt;：update 后把 TD-error 回写到 replay 里更新优先度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你想先做一个“最小可用”的 PER，我的建议是：别一上来就写得太花。把 SumTree 写对、把 &lt;code&gt;batch_update&lt;/code&gt; 的优先级回写逻辑写对，就能看到明显收益。之后再去补重要性采样权重、再去做 beta 的退火（anneal），会更顺畅。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这一篇把 DQN 家族的“程序结构”串了起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DQN 的稳定性来自 Replay + Target&lt;/li&gt;
&lt;li&gt;Double/Dueling 改的是 target 或结构&lt;/li&gt;
&lt;li&gt;Noisy 改的是探索的实现方式&lt;/li&gt;
&lt;li&gt;PER 改的是 replay 的采样分布&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一篇我会切到策略梯度与 Actor-Critic：REINFORCE、PPO、A2C —— 你会发现它们的核心接口仍然可以复用，只是 &lt;code&gt;update()&lt;/code&gt; 里优化的对象从 $Q$ 变成了 $\pi_\theta$。&lt;/p&gt;
&lt;h3&gt;我常用的 DQN 调参/调试清单（建议按顺序来）&lt;/h3&gt;
&lt;p&gt;如果你发现 DQN “不收敛 / 忽好忽坏 / Q 值爆炸”，我一般按下面顺序排查，基本不会走弯路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先看 reward 尺度，再定学习率&lt;/strong&gt;：奖励如果在 $[0,1]$，学习率可以大胆一点；奖励如果动辄几十上百，先考虑 reward clipping 或把学习率降两档。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把 Q 值范围打出来&lt;/strong&gt;：每隔 N 个 episode 打印一次 &lt;code&gt;q_mean/q_max&lt;/code&gt;（以 batch 为单位）。如果 Q 值从几十飙到几万，通常是 target 计算/终止状态处理/学习率出问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replay 先保证“足够大 + 随机采样”&lt;/strong&gt;：buffer 太小就更新，训练非常不稳；我一般会设一个 &lt;code&gt;min_buffer_size&lt;/code&gt;，不够就只 push 不 update。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Target network 更新频率别太激进&lt;/strong&gt;：更新太频繁等于没用 target，更新太慢又会学得很慢。一个常用起点：每 500~2000 个 env step 同步一次（具体随环境而变）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;epsilon 衰减别太快&lt;/strong&gt;：你看到 reward 前期抖动、很快停滞，优先怀疑探索不足；宁愿衰减慢一点，也不要“自信地随机”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;batch_size、gamma、update 频率要成套&lt;/strong&gt;：batch 太小梯度噪声大；gamma 太大又没配足够长的 episode/steps，容易学不到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先跑基础版再加花活&lt;/strong&gt;：Double/Dueling/Noisy/PER 都是锦上添花。基础 DQN 如果你连 “target 正确 + done 处理正确 + replay 正常” 都没确认，上改进往往只会更乱。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/heroimage.DbVa9Sil.jpg"/><enclosure url="/_astro/heroimage.DbVa9Sil.jpg"/></item><item><title>强化学习算法程序实践（3）：策略梯度与 Actor-Critic</title><link>https://xiaohei-blog.vercel.app/blog/rl-algorithm-3</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-algorithm-3</guid><description>策略分布（Softmax / Gaussian）设计，回报累积与并行采样。</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;前两篇我们一直站在“值函数”的视角：学 $Q(s,a)$，再用 $\arg\max$ 导出策略。&lt;/p&gt;
&lt;p&gt;策略梯度（Policy Gradient）反过来：&lt;strong&gt;直接优化策略 $\pi_\theta(a|s)$&lt;/strong&gt;。这件事在实战里很“香”，一方面它天然适配连续动作（不用把动作硬离散、再用 DQN 去拟合），另一方面策略本身就是概率分布，探索不再是外部贴一层 epsilon 的补丁，而是模型输出的一部分。&lt;/p&gt;
&lt;p&gt;这一篇我会按我自己写代码的顺序，把三类最常见的策略方法串成一条清晰的路线：先用 REINFORCE 把“策略分布 + log_prob + 回报”这套最小闭环跑通，再用 PPO 把更新变得克制和稳定，最后用 A2C 把采样效率与方差控制一起做到一个更均衡的状态。&lt;/p&gt;
&lt;p&gt;我个人愿意花时间学这套东西，主要是因为它解决了我在值函数方法里反复遇到的两个尴尬：第一，连续动作任务里“离散化动作”会让策略变得很笨拙，很多时候你离散得再细也不如直接学一个连续分布；第二，很多环境的探索不是“偶尔随机一下”就够了，策略梯度把探索写进分布里以后，你会更容易用概率/熵去量化“我到底在不在探索”。&lt;/p&gt;
&lt;p&gt;当然，策略方法也有自己的一堆坑：最常见的是分布参数化不对（比如 std 太小导致几乎不探索）、回报/优势算错（done 边界处理错一位就能让训练完全失真），以及 log_prob 和 action 对不上（这种 bug 最可怕，loss 还能降，但学到的东西是错的）。所以这篇我会把这些“我踩过的雷”也一并写下来。&lt;/p&gt;
&lt;h2&gt;策略梯度的关键：先把策略分布设计对&lt;/h2&gt;
&lt;p&gt;策略梯度的公式可以写很长，但我真正开始写实现时，脑子里只留一句话：&lt;strong&gt;让网络输出一个“可采样、可求 log_prob、可反传”的分布&lt;/strong&gt;。只要这件事做对了，后面无论是 REINFORCE 还是 PPO，本质都是在用 &lt;code&gt;log_prob&lt;/code&gt; 去乘某个权重（回报/优势），然后反向传播。&lt;/p&gt;
&lt;p&gt;实践里最常见的两类分布就是下面这两个。&lt;/p&gt;
&lt;h3&gt;1) Softmax（离散动作）&lt;/h3&gt;
&lt;p&gt;网络输出 logits，策略分布：
$$
\pi_\theta(a|s) = \text{softmax}(f_\theta(s))
$$&lt;/p&gt;
&lt;p&gt;PyTorch 对应就是 &lt;code&gt;Categorical(logits=...)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;2) Gaussian（连续动作）&lt;/h3&gt;
&lt;p&gt;常见做法是网络输出均值 $\mu(s)$，再配一个方差（或 log_std）：
$$
a \sim \mathcal{N}(\mu_\theta(s), \sigma^2)
$$&lt;/p&gt;
&lt;p&gt;PyTorch 对应 &lt;code&gt;Normal(loc=mu, scale=sigma)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果你的动作空间只有两个动作（0/1），我更喜欢把它看成 Bernoulli：网络输出一个概率 $p\in(0,1)$（用 sigmoid），然后 &lt;code&gt;action ~ Bernoulli(p)&lt;/code&gt;。这个写法会比 softmax(2) 更“直觉”，而且在调试时也更容易看策略到底在偏向哪一边。&lt;/p&gt;
&lt;h2&gt;REINFORCE（Monte-Carlo Policy Gradient）：最小可跑的策略梯度&lt;/h2&gt;
&lt;p&gt;REINFORCE 是我用来“打通策略梯度的第一块砖”。它的结构非常干净：采一整条轨迹，算每一步的折扣回报 $G_t$，然后用 $G_t$ 去加权 &lt;code&gt;log_prob&lt;/code&gt; 做梯度下降。你写完它，就会对“为什么要保存 log_prob、为什么要等回合结束”形成肌肉记忆。&lt;/p&gt;
&lt;h3&gt;回报 $G_t$：从后往前的动态规划&lt;/h3&gt;
&lt;p&gt;一条轨迹 $(s_t,a_t,r_t)$ 采完之后，从后往前算：
$$
G_t = r_t + \gamma G_{t+1}
$$&lt;/p&gt;
&lt;p&gt;代码里通常这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def compute_returns(rewards, gamma: float):
	G = 0.0
	returns = []
	for r in reversed(rewards):
		G = r + gamma * G
		returns.append(G)
	returns.reverse()
	return returns
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;损失：用 log_prob 加权&lt;/h3&gt;
&lt;p&gt;REINFORCE 的一个常见写法：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}(\theta) = -\sum_t G_t \cdot \log \pi_\theta(a_t|s_t)
$$&lt;/p&gt;
&lt;p&gt;工程上就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;采样时保存 &lt;code&gt;log_prob&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;回合结束后算 &lt;code&gt;returns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;做一个加权求和再反传&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;PPO：Actor-Critic + 剪切（clip）让更新别太激进&lt;/h2&gt;
&lt;p&gt;当我开始在稍复杂一点的环境里训练策略时，REINFORCE 的“不稳定”会很快把我劝退。PPO 是我最常用的替代方案：它仍然是 Actor-Critic，但它会用一个很朴素的思想约束更新——&lt;strong&gt;别一口气把策略改太猛&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;工程实现上，我通常把它拆成两块网络：Actor 输出策略分布（支持 sampling 和 log_prob），Critic 输出 $V(s)$ 作为 baseline，用于优势估计。&lt;/p&gt;
&lt;h3&gt;PPO 的核心：update&lt;/h3&gt;
&lt;p&gt;PPO 的实现细节有很多版本，这里我只抓住我认为最“管用”的核心差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;采样/预测和之前一样，但要输出概率（或 log_prob）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update()&lt;/code&gt; 里用 PPO 的目标函数更新 Actor，并用 value loss 更新 Critic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你在实现里容易迷路，可以用一句话自检：&lt;strong&gt;PPO 的 update 吃进去的是 rollout（states/actions/rewards/dones + old_log_probs + values），吐出来的是更新后的 actor/critic 参数。&lt;/strong&gt; 只要数据形状对齐、优势算对、log_prob 对齐旧策略，PPO 就能跑起来。&lt;/p&gt;
&lt;h3&gt;PPO 专用的“经验回放”&lt;/h3&gt;
&lt;p&gt;PPO 里也会有一个“buffer”，但它和 DQN 的 replay 完全不是一回事：DQN 是 off-policy，buffer 可以很大、数据可以存很久；PPO 更接近 on-policy，通常只保留最近一段 rollout，拿这段数据更新若干 epoch 后就丢。&lt;/p&gt;
&lt;p&gt;因此 PPO rollout buffer 多保存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;states, actions, rewards&lt;/li&gt;
&lt;li&gt;old_log_probs&lt;/li&gt;
&lt;li&gt;values（critic 输出）&lt;/li&gt;
&lt;li&gt;dones&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;A2C：优势 Actor-Critic（常见做法：并行环境 + n-step）&lt;/h2&gt;
&lt;p&gt;A2C（Advantage Actor-Critic）在我心里像是“更实用的 REINFORCE”：同样是用采样数据去更新策略，但它用 Critic 做 baseline 来降方差，并且常常配合并行环境一次收集更多轨迹来提高吞吐。&lt;/p&gt;
&lt;p&gt;实现上它仍然是 Actor + Critic 两个网络，更新 Actor 时常用优势函数 $A_t = G_t - V(s_t)$，更新 Critic 时回归到 return 或 bootstrap target。很多坑都集中在回报/优势的计算：done 的边界、最后一个 state 是否 bootstrap，以及并行环境里 time dimension 和 env dimension 的整理。&lt;/p&gt;
&lt;h3&gt;训练函数可以拆成几块理解&lt;/h3&gt;
&lt;p&gt;我更喜欢把 A2C 的训练代码按职责拆开（不然很容易越写越乱）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;参数与环境设置：动作/状态维度、初始化网络、优化器&lt;/li&gt;
&lt;li&gt;训练循环初始化：准备各种缓存数组&lt;/li&gt;
&lt;li&gt;收集轨迹（固定步数）+ 定期评估/记录&lt;/li&gt;
&lt;li&gt;计算回报与优势&lt;/li&gt;
&lt;li&gt;计算 loss 并更新参数，返回 reward 曲线&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这一篇我想传达的核心其实就三件事：第一，策略梯度写对的第一步是“分布建模”，要可采样、可求 log_prob、可反传；第二，REINFORCE 用一条轨迹打通最小闭环，但方差大；第三，PPO/A2C 通过 value/advantage 这些工程手段，把训练的波动压下来，让你能在更复杂的任务上持续迭代。&lt;/p&gt;
&lt;h3&gt;我调试策略梯度时一定会看的 7 个信号&lt;/h3&gt;
&lt;p&gt;策略梯度最折磨人的地方是：你“看起来”什么都在动（loss 有值、梯度在回传、参数在更新），但策略可能就是不变好。下面这些是我最常用的自检清单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;log_prob 是否有限&lt;/strong&gt;：一旦出现 &lt;code&gt;inf/NaN&lt;/code&gt;，优先查分布参数（尤其是 std/log_std）有没有爆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;entropy（熵）是否在合理范围&lt;/strong&gt;：熵很快掉到接近 0，通常意味着策略过早变得确定（探索没了）；熵一直很高又说明策略基本在乱采。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;std/log_std 的数值&lt;/strong&gt;：连续动作里 std 太小 ≈ 不探索；太大 ≈ 动作到处乱飞。很多实现会 clamp log_std。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回报/优势的量级&lt;/strong&gt;：我会打印 return 的均值/方差，并且经常做 normalize（尤其是 REINFORCE 和 PPO 的 advantage）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;done 边界与 bootstrap&lt;/strong&gt;：A2C/PPO 里优势和回报的边界非常敏感，done 处理错一位就能让优势估计整体偏移。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;action 与 log_prob 是否匹配同一次采样&lt;/strong&gt;：不要在采样后又对 action 做 clip/变换但忘了同步 log_prob（SAC 里尤其常见）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先从最简单环境验证闭环&lt;/strong&gt;：CartPole 这类任务能快速验证“分布采样 + log_prob + 更新”是否正确，别一上来就怼高维连续控制。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下一篇进入连续控制的三件套：DDPG / TD3 / SAC。它们和 PPO/A2C 的共同点是都有 Actor-Critic，但一个更偏 off-policy，一个更偏熵正则化。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.DTT8jEEi.jpg"/><enclosure url="/_astro/heroimage.DTT8jEEi.jpg"/></item><item><title>强化学习算法程序实践（4）：连续控制（DDPG / TD3 / SAC）</title><link>https://xiaohei-blog.vercel.app/blog/rl-algorithm-4</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/rl-algorithm-4</guid><description>Actor/Critic 输入输出、Replay Buffer、探索噪声、以及各自 update 的关键差异</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;到了连续控制（continuous action space），你会发现 DQN 这套“输出离散动作 Q 值”的方式没那么顺手了。&lt;/p&gt;
&lt;p&gt;我第一次做连续控制任务时最大的感受是：&lt;strong&gt;动作不再是“选 A 还是选 B”，而是“输出一段连续向量”&lt;/strong&gt;。这时再用 DQN 去离散化动作，往往既不优雅也不高效。更顺的路线是 Actor-Critic：Actor 直接产出连续动作，Critic 负责评价这个动作在当前状态下到底值不值。&lt;/p&gt;
&lt;p&gt;我自己当时的“翻车现场”也很典型：把动作离散成 11 档、21 档，看起来很细了，但策略依然像在抖腿——要么动作不够细导致控制很粗糙，要么离散太细导致动作维度膨胀，学习变得又慢又不稳定。更要命的是，reward 曲线的抖动和环境随机性混在一起，你根本不知道是探索问题、还是值估计的问题。于是我就彻底转到 Actor-Critic：动作直接从网络里出来，探索要么靠噪声（DDPG/TD3），要么靠熵正则（SAC），思路一下子清爽很多。&lt;/p&gt;
&lt;p&gt;这一篇我就沿着我自己的实践顺序，把三套最常用的 off-policy 连续控制算法串起来：DDPG 是最基础的 deterministic Actor-Critic；TD3 主要是修 DDPG 容易高估、容易抖的问题；SAC 则在目标函数里把“熵”引入进来，让探索更自然、训练更稳。&lt;/p&gt;
&lt;p&gt;这篇我按“你写代码时要实现哪些模块”来整理。&lt;/p&gt;
&lt;h2&gt;DDPG：deterministic Actor-Critic 的最小工程结构&lt;/h2&gt;
&lt;p&gt;DDPG 的结构非常“工科”：Actor 输出动作 $a$，Critic 评估 $(s,a)$ 的 Q 值。这里有一个我认为必须牢牢记住的关键点：&lt;strong&gt;Critic 的输入是 state + action 的拼接&lt;/strong&gt;，不是只喂 state。&lt;/p&gt;
&lt;h3&gt;网络结构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Actor（策略网络）：输入 state，输出连续动作；输出层常用 &lt;code&gt;tanh&lt;/code&gt; 把动作限制到 $[-1,1]$（再映射到环境动作范围）&lt;/li&gt;
&lt;li&gt;Critic（价值网络）：输入 &lt;code&gt;[state, action]&lt;/code&gt; 拼接向量，输出 $Q(s,a)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现时我会额外注意一个特别容易被忽略的小细节：Actor/Critic 的输出层初始化不要太“放飞”。如果初始动作幅度很大，或者 Critic 初始 Q 值异常夸张，训练经常会在前几千步就开始发散。很多实现会对最后一层的权重做更小范围的初始化，就是为了解决这个。&lt;/p&gt;
&lt;h3&gt;Replay Buffer&lt;/h3&gt;
&lt;p&gt;和 DQN 类似（push+sample），但 transition 里的 action 是连续向量。&lt;/p&gt;
&lt;h3&gt;探索噪声&lt;/h3&gt;
&lt;p&gt;DDPG 是确定性策略（deterministic policy），所以探索通常靠&lt;strong&gt;给动作加噪声&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;action = actor(state) + noise&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我的习惯是：训练阶段加噪声促进探索；测试阶段完全关掉噪声，只看学到的确定性策略到底能拿多少回报。否则你会出现一个很“玄学”的现象：测试时看起来也很随机，根本不知道模型到底学没学会。&lt;/p&gt;
&lt;h3&gt;update：先 critic 再 actor，再软更新 target&lt;/h3&gt;
&lt;p&gt;DDPG 的 &lt;code&gt;update()&lt;/code&gt; 我通常按固定顺序写死：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 replay 采样一批 transition&lt;/li&gt;
&lt;li&gt;用贝尔曼方程算 critic target&lt;/li&gt;
&lt;li&gt;更新 critic（最小化 TD error）&lt;/li&gt;
&lt;li&gt;更新 actor（最大化 critic 对当前 actor 动作的 Q 值）&lt;/li&gt;
&lt;li&gt;更新各自目标网络参数（soft update）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;TD3：Twin Delayed DDPG（解决高估与不稳定）&lt;/h2&gt;
&lt;p&gt;当我用 DDPG 在更复杂的连续控制任务上跑起来以后，很常见的痛点是：回报曲线一会儿很好看，一会儿又突然崩掉；或者 Critic 的 Q 值虚高，导致 Actor 被带偏。TD3 的出现基本就是为了解决这些问题，它把 Double Q-learning 的“取较小估计更保守”的思想带进了 DDPG。&lt;/p&gt;
&lt;p&gt;工程上 TD3 的“显眼差别”主要体现在三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;双 Critic&lt;/strong&gt;：两个 $Q_1, Q_2$，算 target 时取较小者缓解过估计&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟更新 Actor&lt;/strong&gt;：critic 更新更频繁，actor 慢一点（delayed policy update）
3.（常见实现里还有 target policy smoothing：对 target 动作加一点小噪声）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实际写代码时，你只要把“双 critic”写对、把“actor 延迟更新”写对，就能看到稳定性明显提升；其它技巧（比如 target policy smoothing）可以作为锦上添花后面再加。&lt;/p&gt;
&lt;h2&gt;SAC：Soft Actor-Critic（熵正则化让探索更“软”）&lt;/h2&gt;
&lt;p&gt;SAC 是我在连续控制里最喜欢的一套，因为它对“探索”这件事的处理更自然：不是靠外部噪声硬塞随机性，而是在优化目标里直接鼓励策略保持一定随机性（最大化熵）。很多时候这会让训练更稳，也更不容易陷入坏的局部最优。&lt;/p&gt;
&lt;p&gt;从工程上看，SAC 依然离不开三块：网络、replay、update。常见的网络拆分是 ValueNet / QNet / PolicyNet（也有实现会省略 ValueNet，用 twin Q + target V 替代，但主旨不变）。&lt;/p&gt;
&lt;p&gt;你选哪种网络拆法都可以，关键是 update 的目标里要正确引入熵正则项，以及 Policy 更新时要能拿到 action 的 &lt;code&gt;log_prob&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;SAC 的直觉&lt;/h3&gt;
&lt;p&gt;如果你把 PPO 理解成“动作概率分布 + on-policy 约束”，那 SAC 更像：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 off-policy 的 Q 学习里，鼓励策略保持一定随机性（最大化熵），让探索更稳定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;工程差异点：PolicyNet 的动作采样&lt;/h3&gt;
&lt;p&gt;我在实现 SAC 时，会把策略网络的动作相关函数明确拆成两种：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;evaluate()&lt;/code&gt; 用于训练：返回采样动作以及它的 &lt;code&gt;log_prob&lt;/code&gt;（因为熵项要用到它）；&lt;code&gt;get_action()&lt;/code&gt; 用于交互/测试：给定 state 输出动作（测试时通常用更确定的方式，比如取均值，或者关掉随机性）。&lt;/p&gt;
&lt;p&gt;你可以把它们理解为我们前面文章中的 &lt;code&gt;sample/predict&lt;/code&gt; 的连续动作版本：训练期需要概率信息，测试期只需要行为本身。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;把连续控制这三套算法压缩成一句工程结论：&lt;/p&gt;
&lt;p&gt;DDPG/TD3/SAC 都是“Actor 产动作、Critic 评估 $(s,a)$、Replay 做 off-policy”，差异主要体现在 &lt;code&gt;update()&lt;/code&gt; 的 target 计算方式、是否用双 critic、是否延迟更新 actor、以及是否引入熵正则。&lt;/p&gt;
&lt;p&gt;如果你后面要把这些算法写成统一框架，我建议保留同样的接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sample(state)&lt;/code&gt;：训练交互（DDPG/TD3 加噪声，SAC 采样分布）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;predict(state)&lt;/code&gt;：测试（不加噪声/取均值动作）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update()&lt;/code&gt;：从 replay 采样并更新网络&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续我会看情况把这个系列继续补成“通用工程模板仓库”：统一日志、统一评估、统一保存/加载与配置管理。&lt;/p&gt;
&lt;h3&gt;连续控制里我最常踩的坑 &amp;#x26; 排障顺序&lt;/h3&gt;
&lt;p&gt;连续控制的训练“崩”起来比离散动作更快，尤其是 DDPG/TD3 这种 deterministic 方法，稍微一个尺度不对就会直接发散。我一般按下面顺序排查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;动作缩放是否正确&lt;/strong&gt;：Actor 输出常是 &lt;code&gt;tanh&lt;/code&gt; 的 $[-1,1]$，环境动作空间可能是别的范围。这个映射一旦错了，训练会表现得非常诡异（像是永远学不会某个动作方向）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;噪声别太大/别太小&lt;/strong&gt;：噪声太大动作乱飞，buffer 里全是坏数据；噪声太小又不探索。建议先把噪声单独打印出来，看它的量级相对动作范围是否合理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先盯 Q 值是否爆炸&lt;/strong&gt;：每隔一段时间打印 &lt;code&gt;Q1/Q2&lt;/code&gt; 的均值、最大值；如果 Q 值飙升，优先怀疑学习率、reward 尺度、done 处理、target 计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;target 的软更新系数（tau）要保守&lt;/strong&gt;：tau 太大，target 跟着在线网络跑，稳定性下降；tau 太小，又会学得很慢。建议先用保守设置跑通，再微调。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TD3 的双 critic/取 min 要写对&lt;/strong&gt;：这是它解决过估计的核心。如果你写成了 max，或者 target 用错了 critic，会直接退化/发散。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SAC 重点看 entropy / alpha&lt;/strong&gt;：熵项权重不合适，会出现“动作太随机”或“过早确定”。如果实现支持自动调 alpha，一般会更省心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先用短 horizon 验证闭环&lt;/strong&gt;：把 episode length 缩短、reward 简化、甚至先关掉部分随机性，确认 update 不会爆，再逐步加回真实设置。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/heroimage.BMCKMFQL.jpg"/><enclosure url="/_astro/heroimage.BMCKMFQL.jpg"/></item><item><title>GitHub + Vercel 部署（推荐）</title><link>https://xiaohei-blog.vercel.app/blog/deploy-vercel</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/deploy-vercel</guid><description>使用 GitHub 管理代码，并用 Vercel 自动构建与部署（含环境变量与常见坑）。</description><pubDate>Sat, 11 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 为什么推荐 GitHub + Vercel&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;PR 预览：每个分支/PR 都能拿到可访问的预览链接&lt;/li&gt;
&lt;li&gt;自动构建：push 即部署&lt;/li&gt;
&lt;li&gt;HTTPS / CDN：默认就有&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;详细流程与说明：&lt;/p&gt;
&lt;h2&gt;2. 部署前检查&lt;/h2&gt;
&lt;p&gt;本地先跑通：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm install
pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置域名（建议至少填主域名）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/site.config.ts&lt;/code&gt; → &lt;code&gt;theme.personal.domains.main&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. Vercel 部署要点&lt;/h2&gt;
&lt;p&gt;主题会读取 &lt;code&gt;DEPLOYMENT_PLATFORM&lt;/code&gt; 来选择适配器与输出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vercel&lt;/code&gt;（默认）：Vercel adapter，输出通常为 &lt;code&gt;server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;github&lt;/code&gt;：用于 GitHub Pages，输出为 &lt;code&gt;static&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cloudflare&lt;/code&gt;：用于 Cloudflare Pages，输出为 &lt;code&gt;static&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Vercel 项目里设置环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;DEPLOYMENT_PLATFORM=vercel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建命令建议使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出目录（Output Directory）保持默认即可（Astro 会由适配器处理）。&lt;/p&gt;
&lt;h2&gt;4. 静态站点（可选）&lt;/h2&gt;
&lt;p&gt;如果你希望生成纯静态站点（例如 GitHub Pages），使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;DEPLOYMENT_PLATFORM=github
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并确保你的部署平台支持静态产物 &lt;code&gt;dist/&lt;/code&gt;。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.CpJZWsBS.jpg"/><enclosure url="/_astro/heroimage.CpJZWsBS.jpg"/></item><item><title>Friend Circle（朋友圈）：接入与配置</title><link>https://xiaohei-blog.vercel.app/blog/friend-circle</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/friend-circle</guid><description>使用 Friend-Circle-Lite 生成数据源，并在 Links 页面展示朋友圈动态。</description><pubDate>Sat, 11 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Friend Circle 是什么&lt;/h2&gt;
&lt;p&gt;“朋友圈”页面会展示友链站点的最新文章聚合，适合在 Links 页让访问者快速看到朋友们的新内容。&lt;/p&gt;
&lt;p&gt;本主题的 Links 页集成入口在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/pages/links/index.astro&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 先准备数据源（Friend-Circle-Lite）&lt;/h2&gt;
&lt;p&gt;本主题使用 Friend-Circle-Lite 的接口数据（&lt;code&gt;all.json&lt;/code&gt; 等）。搭建与使用方式建议参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.liushen.fun/posts/4dc716ec/&quot;&gt;Friend Circle 参考文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你需要得到一个可访问的域名，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;fc.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并确保以下地址可访问：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;https://fc.example.com/all.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 在主题中启用&lt;/h2&gt;
&lt;p&gt;编辑 &lt;code&gt;src/site.config.ts&lt;/code&gt;，设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;theme.personal.domains.friendCircle = &apos;fc.example.com&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置完成后，&lt;code&gt;/links&lt;/code&gt; 页面会自动显示 “Small Circle / 朋友圈” 区块。&lt;/p&gt;
&lt;h2&gt;4. 友链 RSS 建议&lt;/h2&gt;
&lt;p&gt;如果某个站点在友链里但朋友圈里没有内容，通常是该站点未提供 RSS 或 RSS 不可访问。建议为友链站点补全 RSS。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.BR9vw3ZO.jpg"/><enclosure url="/_astro/heroimage.BR9vw3ZO.jpg"/></item><item><title>MDX 组件使用：User &amp; Advanced</title><link>https://xiaohei-blog.vercel.app/blog/mdx-components</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/mdx-components</guid><description>文章内组件（Aside/Tabs 等）与高级组件（LinkPreview/GithubCard 等）的使用方式。</description><pubDate>Sat, 11 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside, Tabs, TabItem, Spoiler } from &apos;@/components/user&apos;
import { GithubCard, LinkPreview, QRCode, ImageGroup, WebVideo } from &apos;@/components/advanced&apos;&lt;/p&gt;
&lt;h2&gt;1. 必须使用 MDX&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.md&lt;/code&gt; 文章无法 &lt;code&gt;import&lt;/code&gt; 组件；请使用 &lt;code&gt;.mdx&lt;/code&gt;（如 &lt;code&gt;src/content/blogs/&amp;#x3C;slug&gt;/index.mdx&lt;/code&gt;）。&lt;/p&gt;
&lt;h2&gt;2. User 组件示例&lt;/h2&gt;
&lt;h2&gt;3. Advanced 组件示例&lt;/h2&gt;
&lt;h3&gt;GitHub 卡片&lt;/h3&gt;
&lt;h3&gt;链接预览&lt;/h3&gt;
&lt;h3&gt;二维码&lt;/h3&gt;
&lt;h3&gt;图片组（等高拼图）&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;ImageGroup
images={[
{ src: &apos;https://picr2.axi404.top/1767811093734_image.webp&apos;, alt: &apos;Image A&apos;, aspectRatio: 16 / 9 },
{ src: &apos;https://picr2.axi404.top/1767811093734_image.webp&apos;, alt: &apos;Image B&apos;, aspectRatio: 1 },
]}
/&gt;&lt;/p&gt;
&lt;h3&gt;内嵌视频&lt;/h3&gt;</content:encoded><h:img src="/_astro/heroimage.MTMZ7R1c.jpg"/><enclosure url="/_astro/heroimage.MTMZ7R1c.jpg"/></item><item><title>Waline 评论系统：部署与接入</title><link>https://xiaohei-blog.vercel.app/blog/waline</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/waline</guid><description>部署 Waline 服务端，并在 Theme 中启用评论与访问量统计。</description><pubDate>Sat, 11 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 部署 Waline 服务端&lt;/h2&gt;
&lt;p&gt;请参考这篇完整教程（包含服务端部署与配置项说明）：&lt;a href=&quot;https://zoooooone.github.io/posts/waline/#2-%E8%AF%84%E8%AE%BA%E9%80%9A%E7%9F%A5%E5%BC%80%E5%90%AF&quot;&gt;Waline 评论系统&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;部署完成后你会得到一个服务端地址，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;https://waline.example.com/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 在主题中启用 Waline&lt;/h2&gt;
&lt;p&gt;编辑 &lt;code&gt;src/site.config.ts&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;integ.waline.enable&lt;/code&gt;: 设为 &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;integ.waline.server&lt;/code&gt;: 填你的 Waline Server URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;waline: {
  enable: true,
  server: &apos;https://waline.example.com/&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 常见说明&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;评论区组件：&lt;code&gt;src/components/advanced/Comment.astro&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;页面访问量/评论数：部分页面会加载 Waline 的 &lt;code&gt;pageview&lt;/code&gt; 统计（见 &lt;code&gt;src/pages/*&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;单篇文章是否显示评论：由文章 Frontmatter 的 &lt;code&gt;comment&lt;/code&gt; 控制（默认 &lt;code&gt;true&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/heroimage.DtXCi-sH.jpg"/><enclosure url="/_astro/heroimage.DtXCi-sH.jpg"/></item><item><title>写作指南：Markdown / MDX</title><link>https://xiaohei-blog.vercel.app/blog/writing-markdown-mdx</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/writing-markdown-mdx</guid><description>主题内支持的 Markdown 扩展、数学公式、代码高亮与一些写作约定。</description><pubDate>Sat, 11 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Markdown 支持范围&lt;/h2&gt;
&lt;p&gt;主题默认启用了常用扩展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GFM：表格、删除线、任务列表等&lt;/li&gt;
&lt;li&gt;数学公式：KaTeX（&lt;code&gt;$...$&lt;/code&gt; / &lt;code&gt;$$...$$&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;代码高亮：Shiki（支持标题、差异标注等）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 数学公式（KaTeX）&lt;/h2&gt;
&lt;p&gt;行内：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;欧拉公式：$e^{i\\pi}+1=0$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;块级：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;$$
\\int_0^1 x^2 dx = \\frac{1}{3}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 代码块增强（差异/高亮）&lt;/h2&gt;
&lt;p&gt;你可以在代码中使用注释标记来展示变更（示例来自主题的高亮 transformer）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const a = 1 // [!code --]
const a = 2 // [!code ++]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以做行高亮：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const token = &apos;secret&apos; // [!code highlight]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 什么时候用 MDX&lt;/h2&gt;
&lt;p&gt;当你需要在文章里使用组件（例如 &lt;code&gt;Aside&lt;/code&gt;、&lt;code&gt;Tabs&lt;/code&gt;、&lt;code&gt;GithubCard&lt;/code&gt; 等）时，请使用 &lt;code&gt;.mdx&lt;/code&gt;，并在文件顶部 &lt;code&gt;import&lt;/code&gt; 组件。&lt;/p&gt;
&lt;p&gt;组件用法示例文档：见 “MDX 组件使用”。&lt;/p&gt;</content:encoded><h:img src="/_astro/heroimage.DKXwZ6L7.jpg"/><enclosure url="/_astro/heroimage.DKXwZ6L7.jpg"/></item><item><title>Astro Theme 基础使用与配置</title><link>https://xiaohei-blog.vercel.app/blog/theme-basics</link><guid isPermaLink="true">https://xiaohei-blog.vercel.app/blog/theme-basics</guid><description>从本地启动到站点配置：了解内容结构、配置入口与常见改动点。</description><pubDate>Sat, 11 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前置条件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 18+（建议 20+）&lt;/li&gt;
&lt;li&gt;包管理器：&lt;code&gt;pnpm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm install
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 目录结构（最常用）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/site.config.ts&lt;/code&gt;：主题配置入口（站点信息、导航、集成配置等）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/content/blogs/&amp;#x3C;slug&gt;/index.mdx&lt;/code&gt;：中文文章&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/content/blogs/&amp;#x3C;slug&gt;/index-en.mdx&lt;/code&gt;：英文文章（可选，没有则英文列表会回退到中文）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public/&lt;/code&gt;：静态资源（&lt;code&gt;/images/*&lt;/code&gt;、&lt;code&gt;/avatar/*&lt;/code&gt; 等）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 配置站点信息（&lt;code&gt;src/site.config.ts&lt;/code&gt;）&lt;/h2&gt;
&lt;p&gt;常改字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;theme.title&lt;/code&gt; / &lt;code&gt;theme.description&lt;/code&gt;：站点标题/描述&lt;/li&gt;
&lt;li&gt;&lt;code&gt;theme.personal.domains.main&lt;/code&gt;：主域名（用于生成绝对链接、RSS 等）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;theme.header.menu&lt;/code&gt;：导航菜单&lt;/li&gt;
&lt;li&gt;&lt;code&gt;integ.pagefind&lt;/code&gt;：站内搜索（Pagefind）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;integ.waline&lt;/code&gt;：评论系统（见 Waline 文档）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 写一篇文章（中英双语）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;新建文件夹：&lt;code&gt;src/content/blogs/my-first-post/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;写中文：&lt;code&gt;src/content/blogs/my-first-post/index.mdx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;写英文：&lt;code&gt;src/content/blogs/my-first-post/index-en.mdx&lt;/code&gt;（可选）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最小 Frontmatter（两种语言都要有）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;---
title: My Title
publishDate: 2026-01-11
description: Short summary.
tags: [&apos;docs&apos;]
---
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 构建与产物&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm build
pnpm preview
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;构建产物默认在 &lt;code&gt;dist/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;astro.config.mjs&lt;/code&gt; 会根据 &lt;code&gt;DEPLOYMENT_PLATFORM&lt;/code&gt; 选择适配器与输出模式（详见部署文档）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 部署推荐（GitHub + Vercel）&lt;/h2&gt;
&lt;p&gt;建议使用 GitHub 托管代码 + Vercel 自动部署（PR 预览、回滚、CDN、HTTPS 都更省心）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;部署思路与注意事项：&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/heroimage.DXx_8_1r.jpg"/><enclosure url="/_astro/heroimage.DXx_8_1r.jpg"/></item></channel></rss>