<?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>Laurie&apos;s Site</title><description>Stay hungry, stay foolish
feedId:112493734937864192+userId:98068239647977472</description><link>https://laurie-hxf.xyz</link><item><title>CS285 Lecture15&amp;16 Offline Reinforcement Learning</title><link>https://laurie-hxf.xyz/blog/cs285-l16-offline-reinforcement-learning</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l16-offline-reinforcement-learning</guid><description>CS285 Lecture15&amp;16 Offline Reinforcement Learning</description><pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.Bcb6tGWJ.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2016.00.40.png&quot; alt=&quot;Screenshot 2026-01-01 at 16.00.40&quot;&gt;&lt;/p&gt;
&lt;p&gt;这一章讲的是offline RL，主要想法就是之前RL算法都要让policy部署到新的环境来尝试从而进行不断的训练。但是有的场景可能并不能承受住模型的尝试，比如说医疗场景。那offline Rl解决的问题就是给定一个未知policy收集到的数据集，然后只在这个数据集上面来训练我的模型。&lt;/p&gt;
&lt;p&gt;那么我们想要的这个策略应该符合一些直觉，我们想要的模型不应该是跟模仿学习一样的，我们不只是先要模仿给定的数据集中的行动。我们想要从中学习到好的动作，从给定的数据集中整理出什么是好的动作，什么是不好的，从而得到比数据集中的数据更好的策略。&lt;/p&gt;
&lt;h2&gt;Problem&lt;/h2&gt;
&lt;p&gt;但是直接在离线的数据集上用Q-learning会有一些问题
&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2016.07.18.png&quot; alt=&quot;Screenshot 2026-01-01 at 16.07.18&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左图 (AverageReturn): 随着训练步数（TrainSteps）增加，智能体在实际环境中的表现（平均回报）不仅没有变好，反而急剧下降，甚至跌至负值。这代表“它实际做得有多差” (how well it does) 。&lt;/li&gt;
&lt;li&gt;右图 (log(Q)): 与此同时，Q 值（对数坐标）却在呈指数级上升（爆炸）。这意味着智能体认为自己会获得天文数字般的回报。这代表“它以为自己做得有多好” (how well it thinks it does)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么呢？
&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2016.10.15.png&quot; alt=&quot;Screenshot 2026-01-01 at 16.10.15&quot;&gt;
因为标准 Q-learning 的更新公式：
在 Q-learning 中，我们使用以下目标来更新 Q 值：
$$
Q(s,a) \leftarrow r(s,a) + E_{a&apos; \sim \pi_{new}}[Q(s&apos;,a&apos;)]
$$
其中，新策略 $\pi_{new}$ 通常是贪婪策略，即去选择那个让 Q 值最大的动作：
$$
\pi_{new} = \arg \max_{\pi} E_{a \sim \pi(a|s)}[Q(s,a)]
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分布偏移 (Distribution Shift)：
在离线设置中，我们只能看到行为策略 $\pi_{\beta}$（产生数据的策略）所覆盖的动作。对于未见过的动作（OOD actions），Q 网络（神经网络）的预测是未定义的，可能会有很大的误差（噪声）。&lt;/li&gt;
&lt;li&gt;最大化带来的问题 (The Maximization Bias)：
当你执行 $\arg \max$ 操作时，你实际上是在“寻找” Q 值最高的地方。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果 Q 函数在 OOD 区域恰好有正向的误差（预测值偏高）&lt;/strong&gt;，最大化操作就会直接选中这些动作 。
如果你去优化一个拟合好的函数 $x^* \leftarrow \arg \max_x f_{\theta}(x)$，而 $x$ 超出了训练数据的范围，你往往会利用模型的误差，找到一个虚假的“伪高点” 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个问题在之前的off policy中也有，解决办法就是
&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2016.12.56.png&quot; alt=&quot;Screenshot 2026-01-01 at 16.12.56&quot;&gt;
为了防止新学习的策略 $\pi_{new}$ 偏离数据分布太远（从而进入 Q 值估计不准的区域），我们在更新策略时增加一个约束条件：
$$
\pi_{new}(a|s) = \arg \max_{\pi} E_{a \sim \pi(a|s)}[Q(s,a)] \quad \text{s.t.} \quad D_{KL}(\pi || \pi_{\beta}) \le \epsilon
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义：&lt;/strong&gt; 我们依然希望最大化 Q 值，但是我们要求新策略 $\pi$ 和行为策略 $\pi_{\beta}$（即产生数据的策略）之间的差异（由 KL 散度衡量）不能超过 $\epsilon$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的：&lt;/strong&gt; 确保新策略只在数据覆盖的范围内进行优化，从而避免查询到那些 Q 值可能极其错误的未见动作（OOD actions）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然这个想法听起来很合理，但是有两个主要问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问题 1：我们通常不知道行为策略 $\pi_{\beta}(a|s)$
&lt;ul&gt;
&lt;li&gt;在离线 RL 中，数据集可能来源复杂，例如：
&lt;ul&gt;
&lt;li&gt;人类提供的演示数据 。&lt;/li&gt;
&lt;li&gt;手工设计的控制器 。&lt;/li&gt;
&lt;li&gt;过去多次 RL 实验混合的数据 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在这些情况下，没有一个明确的数学公式来表示 $\pi_{\beta}$，因此很难直接计算 KL 散度。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;问题 2：这种约束既“太悲观”又“不够悲观”
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;太悲观（Too pessimistic）：&lt;/strong&gt; 即使在某些区域 Q 值估计是准确的，KL 约束也会强行把策略拉回行为策略，限制了改进空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不够悲观（Not pessimistic enough）：&lt;/strong&gt; 单纯的 KL 散度可能无法精准地剔除那些 Q 值特别离谱的特定动作，或者如果 $\pi_{\beta}$ 本身覆盖面很广但数据稀疏，KL 约束可能不足以保证安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;既然我们要限制新策略 $\pi$ 不偏离行为策略 $\pi_{\beta}$，我们具体应该用什么样的数学形式来实现这种限制？
&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2016.23.47.png&quot; alt=&quot;Screenshot 2026-01-01 at 16.23.47&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;KL 散度 (KL-divergence)
$$
D_{KL}(\pi || \pi_{\beta})
$$
这种方法要求新策略 $\pi$ 和行为策略 $\pi_{\beta}$ 的&lt;strong&gt;分布形状&lt;/strong&gt;尽可能相似。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点 (+)：&lt;/strong&gt; 非常容易实现（Easy to implement）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点 (-)：&lt;/strong&gt; 这不一定是我们真正想要的（Not necessarily what we want）。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原因解析&lt;/strong&gt;：如上图所示，KL 散度会惩罚两个分布之间的差异。为了权衡KL散度，和Q value，KL散度得出来的policy可能就是浅绿色的$\pi$ ，但是这个策略同样给了一些不好的动作相对较高的可能性。实际上深绿色的那条线可能会更好，但是因为KL散度我们得不出来&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;支持集约束 (Support Constraint)
$$
\pi(a|s) \ge 0 \text{ only if } \pi_{\beta}(a|s) \ge \epsilon
$$
这种方法只要求：只要 $\pi_{\beta}$ 在某个动作上的概率大于一个阈值 $\epsilon$（即数据中存在该动作），$\pi$ 就可以选这个动作。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：他要求 $\pi$尽量不要跑到坏数据的地方去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点 (+)：&lt;/strong&gt; 这更接近我们真正想要的（Much closer to what we really want），即在保证安全（有数据支持）的前提下，最大化 Q 值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点 (-)：&lt;/strong&gt; 实现起来极其复杂（Significantly more complex to implement）。因为在连续动作空间中，很难硬性定义“支持集”的边界。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;隐式策略约束方法（Implicit Policy Constraint Methods）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2016.56.01.png&quot; alt=&quot;Screenshot 2026-01-01 at 16.56.01&quot;&gt;
出发点依然是我们之前定义的那个带约束的优化问题 ：
$$
\pi_{new}(a|s) = \arg \max_{\pi} E_{a \sim \pi(a|s)}[Q(s,a)] \quad \text{s.t.} \quad D_{KL}(\pi || \pi_{\beta}) \le \epsilon
$$
即：在不偏离行为策略 $\pi_{\beta}$ 太远的前提下，最大化 Q 值。&lt;/p&gt;
&lt;p&gt;上述问题实际上是一个经典的凸优化问题。通过使用拉格朗日乘子法（Lagrange Multipliers） 和对偶性（Duality），我们可以直接推导出最优策略 $\pi^&lt;em&gt;$ 的数学形式 2：
$$
\pi^&lt;/em&gt;(a|s) = \frac{1}{Z(s)} \pi_{\beta}(a|s) \exp\left(\frac{1}{\lambda} A^{\pi}(s,a)\right)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$\pi_{\beta}(a|s)$&lt;/strong&gt;：原本的数据分布（行为策略）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\exp(\dots)$&lt;/strong&gt;：指数项，用于调整概率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$A^{\pi}(s,a)$&lt;/strong&gt;：优势函数（Advantage Function），表示动作 $a$ 比平均水平好多少。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\lambda$&lt;/strong&gt;：拉格朗日乘子（温度参数），控制约束的强弱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$Z(s)$&lt;/strong&gt;：归一化常数（Partition Function），确保概率之和为 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;直观理解：&lt;/strong&gt; 最优策略其实就是&lt;strong&gt;把原来的数据分布 $\pi_{\beta}$ 进行“重新加权”（Re-weighting）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果某个动作的优势 $A(s,a)$ 很大（Q 值很高），它的权重就会通过 $\exp$ 函数指数级增加。&lt;/li&gt;
&lt;li&gt;如果优势很小或为负，权重就降低。&lt;/li&gt;
&lt;li&gt;$\pi^*$ 依然是在 $\pi_{\beta}$ 的支撑集（Support）内，只是把概率密度向高价值动作倾斜了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是这公式仍然要求我们知道$\pi_{\beta}$ ,于是我们可以用采样，从给定的数据集中采样
这可以通过加权最大似然估计（Weighted Maximum Likelihood Estimation） 来实现：
$$
\pi_{new} = \arg \max_{\pi} E_{(s,a) \sim \pi_{\beta}} \left[ \log \pi(a|s) \cdot \exp\left(\frac{1}{\lambda} A^{\pi_{old}}(s,a)\right) \right]
$$
&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2016.58.50.png&quot; alt=&quot;Screenshot 2026-01-01 at 16.58.50&quot;&gt;
这个方法中，我们用加权回归更新了策略 $\pi$，这确实避免了策略训练时的 OOD 问题。但是，第给出的 Critic（Q 函数）的损失函数：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}&lt;em&gt;C(\phi) = E&lt;/em&gt;{(s,a,s&apos;) \sim D} \left[ \left( Q_\phi(s,a) - (r(s,a) + \gamma E_{a&apos; \sim \pi_\theta(a&apos;|s&apos;)} [Q_\phi(s&apos;, a&apos;)] ) \right)^2 \right]
$$
注意公式中的目标值（Target Value）部分：$r + \gamma E_{a&apos; \sim \pi_\theta}[Q(s&apos;, a&apos;)]$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为了计算这个目标值，我们需要用当前正在学习的策略 $\pi_\theta$ 去采样下一个动作 $a&apos;$。&lt;/li&gt;
&lt;li&gt;然后把这个 $a&apos;$ 喂给 Q 网络去估值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;依然存在的 OOD 风险&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前的策略 $\pi_\theta$ 还没有训练得很好（或者在某些状态下稍微偏离了数据分布），它产生的动作 $a&apos;$ 就可能是分布外（OOD）的 。&lt;/li&gt;
&lt;li&gt;一旦 $a&apos;$ 是 OOD 的，Q 网络 $Q(s&apos;, a&apos;)$ 的输出就可能是错误的（通常是过高估计）。&lt;/li&gt;
&lt;li&gt;这个错误的 Q 值会被作为目标值（Target），反向传播去更新 Q 网络，导致误差累积。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么有什么方法让他不要产生OOD的动作吗&lt;/p&gt;
&lt;h2&gt;IQL&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2017.26.30.png&quot; alt=&quot;Screenshot 2026-01-01 at 17.26.30&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个方法的目的就是，逼近“数据集里表现最好的动作”：在不产生幻觉的前提下，尽可能贪婪（Greedy）。&lt;/p&gt;
&lt;p&gt;在标准的 Q-Learning (比如 DQN 或 SAC) 中，更新 Q 值时使用 Bellman Optimality Equation：
$$
Q(s, a) \leftarrow r + \gamma \max_{a&apos;} Q(s&apos;, a&apos;)
$$
请注意这个 $\max_{a&apos;}$ 操作。为了找到最大值，算法通常会做两件事之一：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;对于离散动作&lt;/strong&gt;：把所有可能的动作都输入 Q 网络，选最大的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于连续动作&lt;/strong&gt;（由 Actor-Critic 完成）：让当前的策略网络（Actor）生成一个它认为最好的动作 $a&apos;$，然后输入 Q 网络评估。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;在离线设置中，我们不能与环境交互。&lt;/li&gt;
&lt;li&gt;策略网络（Actor）可能会为了追求高分，生成一个&lt;strong&gt;从未在数据集里出现过的奇怪动作&lt;/strong&gt;（OOD 动作）。&lt;/li&gt;
&lt;li&gt;Q 网络（Critic）只见过数据集里的动作。对于这个未见过的 OOD 动作，Q 网络无法正确评估，往往会因为泛化误差给出一个&lt;strong&gt;虚高的估值（Overestimation）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果&lt;/strong&gt;：算法以为发现了一个好的操作，于是拼命往这个 OOD 动作的方向更新，导致训练崩溃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们不再让 Actor 生成一个 $a&apos;$ 然后去查 $Q(s&apos;, a&apos;)$，而是直接训练一个 $V(s)$ 函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：让 $V(s)$ 逼近 $\max_{a \in \text{dataset}} Q(s, a)$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手段&lt;/strong&gt;：Expectile Regression（那个非对称的 Loss）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;只看数据：
训练 $V(s)$ 的 Loss 函数里：
$$
\ell(V(s), Q(s, a))
$$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里的 $(s, a)$ 严格来自离线数据集。我们完全没有让 Policy 去生成新动作，也没有去查询任何未知的 $a&apos;$。
2. 隐式最大化（Implicit Maximization）：
虽然我们只用了数据集里的样本，但通过调节 Expectile 的参数 $\tau$（比如设为 0.9）：
- 如果 $Q(s, a)$ 比 $V(s)$ 小，Loss 权重很小（忽略差的动作）。
- 如果 $Q(s, a)$ 比 $V(s)$ 大，Loss 权重很大（强迫 $V$ 往大的 Q 值靠拢）。
- &lt;strong&gt;效果&lt;/strong&gt;：$V(s)$ 会自动过滤掉那些平庸的动作，收敛到数据集分布的&lt;strong&gt;上边缘&lt;/strong&gt;（即数据集中表现最好的那个 Q 值）。&lt;/p&gt;
&lt;h2&gt;CQL&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2018.00.41.png&quot; alt=&quot;Screenshot 2026-01-01 at 18.00.41&quot;&gt;
这是另一种思路，我们说遇到OOD的时候我们会可能会错误估计这个动作的Q value，于是乎我们的思路就是打压这种错误估计的分数
&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2018.01.58.png&quot; alt=&quot;Screenshot 2026-01-01 at 18.01.58&quot;&gt;&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;E_{(\mathbf{s}, \mathbf{a}, \mathbf{s&apos;}) \sim D} [(Q(\mathbf{s}, \mathbf{a}) - \text{target})^2]
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;这是标准的强化学习（Q-learning）目标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;它的意思：&lt;/strong&gt; “即使我在做离线学习，我也要尽量让预测的 Q 值准确，能够反映真实的奖励。”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“压低”陌生动作的分数（Push Down）
$$
\alpha E_{\mathbf{s} \sim D, \mathbf{a} \sim \mu(\mathbf{a}|\mathbf{s})}[Q(\mathbf{s}, \mathbf{a})]
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$\mathbf{a} \sim \mu$ 是什么意思？&lt;/strong&gt; 这里的 $\mu$ 通常代表模型当前认为“可能很高分”的动作，或者是随机采样的动作。重点是，这些动作&lt;strong&gt;不是&lt;/strong&gt;直接从数据集里拿出来的，而是模型自己“脑补”出来的。&lt;/li&gt;
&lt;li&gt;为什么要压低（Min）它？
在离线强化学习中，最大的坑就是**“不懂装懂”。模型没见过某个动作，却错误地以为这个动作能得 10000 分（这就是 OOD 高估问题）。
所以这一项说：“凡是你（模型）自己想出来的、数据集中没见过的动作，我都先假设它是坏的，强行把它的 Q 值压低。”**&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“抬高”已知数据的分数（Push Up）
$$
-\alpha E_{(\mathbf{s}, \mathbf{a}) \sim D}[Q(\mathbf{s}, \mathbf{a})]
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意前面的负号：&lt;/strong&gt; 整个大目标是最小化（Min），所以“减去”一项，等于是在&lt;strong&gt;最大化&lt;/strong&gt;这一项。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$\mathbf{a} \sim D$ 是什么意思？&lt;/strong&gt; 这些是&lt;strong&gt;真真切切存在于数据集里&lt;/strong&gt;的动作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;为什么要抬高它？
这一项说：“但是，如果这个动作是数据集里有的，那就是有事实依据的，我要把它保护起来，把它的分数推高。”
最终结果：
只有数据集覆盖的地方（Data Region），Q 值是凸出来的（高的）；而在没有数据的地方（OOD Region），Q 值都被压得低低的。
这样一来，当你用这个 Q 函数去选动作时，它会自动倾向于选择那些&lt;strong&gt;高的区域&lt;/strong&gt;（也就是数据集里的动作），从而避免去选那些未知的、可能有坑的区域。这就是所谓的**“Conservative（保守）”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Model base offline RL&lt;/h2&gt;
&lt;h3&gt;MOPO&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/laurie-hxf/image-bed/master/Screenshot%202026-01-01%20at%2018.30.35.png&quot; alt=&quot;Screenshot 2026-01-01 at 18.30.35&quot;&gt;
这张 PPT 介绍的是一种名为 &lt;strong&gt;MOPO (Model-Based Offline Policy Optimization)&lt;/strong&gt; 的强化学习算法。它的核心思想是如何在“离线强化学习”（Offline RL）中，利用基于模型（Model-Based）的方法来安全地优化策略。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Offline RL（离线强化学习）：&lt;/strong&gt; 智能体不能与环境交互，只能从一个固定的历史数据集（Dataset）中学习。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;问题（Exploiting）：&lt;/strong&gt; 如果智能体构建了一个环境模型（Model），并在该模型中进行规划，它往往会发现一些模型预测不准确但看起来奖励很高的区域（模型误差）。智能体容易“利用”这些误差，导致在真实环境中表现很差。这被称为 &lt;strong&gt;Distribution Shift（分布偏移）&lt;/strong&gt; 问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PPT 左侧提出了核心解决方案：&lt;strong&gt;“Punish the policy for exploiting”&lt;/strong&gt;（惩罚策略的利用行为）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;核心公式：
$$
\tilde{r}(\mathbf{s}, \mathbf{a}) = r(\mathbf{s}, \mathbf{a}) - \lambda u(\mathbf{s}, \mathbf{a})
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$\tilde{r}(\mathbf{s}, \mathbf{a})$&lt;/strong&gt;：修改后的奖励函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$r(\mathbf{s}, \mathbf{a})$&lt;/strong&gt;：原始的奖励函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$u(\mathbf{s}, \mathbf{a})$&lt;/strong&gt;：&lt;strong&gt;不确定性（Uncertainty）&lt;/strong&gt;。这代表模型对自己预测的置信度。如果当前状态和动作偏离了训练数据（即模型没见过），不确定性 $u$ 就会很大。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$\lambda$&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; 代表模型推演进入了未知区域（High Uncertainty）。在这种地方，MOPO 算法会通过上述公式给予高额惩罚，告诉智能体“这里很危险，不要往这里走”。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;COMBO&lt;/h3&gt;
&lt;p&gt;MOPO是通过“修改奖励”来避开风险，那么COMBO则是通过压低 Q 值来通过风险。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;借鉴 CQL：&lt;/strong&gt; PPT 开头提到“just like CQL...”。它的核心是：对于未见过的数据，直接把它们的 Q 值（预期回报）压得很低，以此来保持“保守”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;COMBO 的做法：&lt;/strong&gt; 它将 CQL 的思想应用到了 Model-Based 方法中。它不需要像 MOPO 那样去计算复杂的“不确定性惩罚”，而是简单粗暴地：凡是模型生成的（可能是假的）数据，我就在训练 Q 函数时，刻意去最小化它们的 Q 值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;COMBO 的核心目标函数，可以拆解为两部分看：&lt;/p&gt;
&lt;p&gt;$$
\hat{Q}^{k+1} \leftarrow \arg \min_Q \beta (\underbrace{\mathbb{E}&lt;em&gt;{s,a \sim \rho(s,a)}[Q(s,a)]}&lt;/em&gt;{\text{压低模型数据的 Q 值}} - \underbrace{\mathbb{E}&lt;em&gt;{s,a \sim \mathcal{D}}[Q(s,a)]}&lt;/em&gt;{\text{抬高真实数据的 Q 值}}) + \underbrace{\frac{1}{2}\dots}_{\text{标准 Q 学习}}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一部分（保守项）：&lt;/strong&gt; $\beta (\dots)$
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最小化&lt;/strong&gt;模型生成数据 ($\rho(s,a)$) 的 Q 值：因为模型推演的数据可能是错误的（幻觉），为了安全，算法假设这些路径是“坏”的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最大化&lt;/strong&gt;真实数据 ($\mathcal{D}$) 的 Q 值：因为真实数据是 Ground Truth，是可以信任的。&lt;/li&gt;
&lt;li&gt;通过这一减一加，人为地拉开了“真实数据”和“模型伪造数据”之间的价值差距。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二部分（标准项）：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这就是标准的 Bellman Error（TD Error），用于让 Q 函数学习正常的预测任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果模型生成了一些“离谱”的数据（即看起来和真实数据完全不一样，属于 Out-of-Distribution），通过上面的公式，Q 函数会很容易把这些离谱数据的价值（Q值）打得很低。这样一来，策略（Policy）在做决策时，自然就不会去选择那些动作，从而避免了模型误差带来的风险。&lt;/p&gt;</content:encoded><img src="/_astro/11.Bcb6tGWJ.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.Bcb6tGWJ.png" length="0" type="image/png"/></item><item><title>CS285 Lecture12 Model-Based Reinforcement Learning</title><link>https://laurie-hxf.xyz/blog/cs285-l12-model-based-reinforcement-learning</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l12-model-based-reinforcement-learning</guid><description>CS285 Lecture12 Model-Based Reinforcement Learning</description><pubDate>Wed, 17 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.CkKeD2J-.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;当我们用model来拟合状态转移函数的时候，我们可以直接通过这个模型进行反向传播
&lt;img src=&quot;./Screenshot%202025-12-16%20at%2010.57.17.png&quot; alt=&quot;alt text&quot;&gt;
但是这个就有问题
&lt;img src=&quot;./Screenshot%202025-12-16%20at%2010.59.28.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Shooting Method（打靶法）：&lt;/strong&gt; 在轨迹优化中，如果你只调整初始的控制量，试图让几百步之后的机器人达到某个状态，这就像打靶一样——&lt;strong&gt;初始角度极其微小的偏差，会被时间无限放大，导致最后偏离十万八千里&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;耦合问题（Coupling）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在传统的控制理论（如 LQR，线性二次调节器）中，我们可以利用动态规划（Dynamic Programming）倒着一步步解出最优控制，每一步是独立的。&lt;/li&gt;
&lt;li&gt;但在这种深度强化学习方法中，策略网络的参数 $\theta$ 是&lt;strong&gt;全局共享&lt;/strong&gt;的（同一个神经网络 $\pi_\theta$ 用在 $t, t+1, t+2...$ 所有时间步）。&lt;/li&gt;
&lt;li&gt;这意味着：你为了优化 $t+10$ 时刻的表现去修改 $\theta$，会同时改变 $t=1$ 时刻的行为。所有时间步被“耦合”在一起了，无法使用类似 LQR 那样高效稳定的解法。这导致优化极其困难，甚至呈病态（Ill-conditioned）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;梯度消失与爆炸（Vanishing/Exploding Gradients）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这和你训练 RNN 时遇到的问题一模一样。当你在时间轴上展开，反向传播经过 $f(s, a)$ 很多次。&lt;/li&gt;
&lt;li&gt;如果轨迹很长（比如几百步），梯度需要连乘几百个雅可比矩阵（Jacobian Matrix）。这会导致梯度要么变成 0（没信号了），要么变得无限大（数值溢出，网络崩溃）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;无法像 LSTM 那样“设计”动力学：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是最痛的一点。在深度学习中，我们发明了 LSTM、GRU 或 ResNet，通过添加“门控”或“残差连接”来人为地让梯度流更顺畅。我们&lt;strong&gt;设计&lt;/strong&gt;了网络结构。&lt;/li&gt;
&lt;li&gt;但在 RL 中，状态的转移（Dynamics）是由**环境（物理世界）**决定的（即 PPT 说的 &lt;em&gt;&quot;dynamics are chosen by nature&quot;&lt;/em&gt;）。&lt;/li&gt;
&lt;li&gt;如果物理世界本身是混沌的、不连续的（比如机器人脚底打滑、发生碰撞），那么动力学模型 $f(s,a)$ 的梯度就会非常难看。我们无法为了好训练而去修改物理定律。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们的方法就是，只用这个model来生成数据来加速model-free的RL算法
&lt;img src=&quot;./Screenshot%202025-12-16%20at%2011.02.34.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Model-Free Learning With a Model&lt;/h2&gt;
&lt;p&gt;我们把模型只当作“模拟器”用，而不是“计算图”的一部分。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;流程：
&lt;ol&gt;
&lt;li&gt;收集真实数据。&lt;/li&gt;
&lt;li&gt;训练动力学模型 $f(s,a)$。&lt;/li&gt;
&lt;li&gt;Step 3： 不求导，而是用模型 $f(s,a)$ 生成很多虚拟的轨迹（Trajectories）${\tau_i}$。&lt;/li&gt;
&lt;li&gt;Step 4： 把这些生成的虚拟数据当作“真数据”，喂给一个标准的 Model-Free 算法（如 Policy Gradient ）去更新策略。
&lt;img src=&quot;./Screenshot%202025-12-16%20at%2011.53.06.png&quot; alt=&quot;alt text&quot;&gt;
但是这就有问题，模型误差积累（Compounding Error）。跟模仿学习中的一样，如果你用有误差的模型预测未来 1000 步，第 1000 步的预测结果可能和真实世界完全无关。
&lt;img src=&quot;./Screenshot%202025-12-16%20at%2011.55.07.png&quot; alt=&quot;alt text&quot;&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;左图（Long Rollouts）：从初始状态开始，完全由模型长时间推演。缺点：误差积累导致红线（预测）和真实情况偏离十万八千里，策略学了一堆假东西。&lt;/li&gt;
&lt;li&gt;中图（Short Rollouts from start）： 只推演几步。&lt;strong&gt;缺点：&lt;/strong&gt; 误差小了，但智能体看不到未来的长远后果，变得短视。&lt;/li&gt;
&lt;li&gt;右图（Branched Rollouts / Short Rollouts from Replay Buffer）：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;做法：&lt;/strong&gt; 从真实数据集中采样真实状态（橙色圆点）作为起点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分支（Branching）：&lt;/strong&gt; 从这些真实的“中间状态”开始，用模型只往后推演很短的几步（比如 k=1 或 k=5）。&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;p&gt;所以最终他的算法流程大概就是这样
&lt;img src=&quot;./Screenshot%202025-12-16%20at%2012.02.13.png&quot; alt=&quot;alt text&quot;&gt;
这里特意提到了使用 Off-policy RL。因为生成的短数据并不构成完整的轨迹，且数据分布发生了变化，Off-policy 算法能更高效地利用这些零散的短数据片段。&lt;/p&gt;
&lt;h2&gt;Dyna-Style Algorithms&lt;/h2&gt;
&lt;p&gt;基本上思想就是类似的
&lt;img src=&quot;./Screenshot%202025-12-16%20at%2020.15.55.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./Screenshot%202025-12-16%20at%2020.17.08.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./Screenshot%202025-12-16%20at%2020.19.03.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;</content:encoded><img src="/_astro/11.CkKeD2J-.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.CkKeD2J-.png" length="0" type="image/png"/></item><item><title>CS285 Lecture11 Model-Based Reinforcement Learning</title><link>https://laurie-hxf.xyz/blog/cs285-l11-model-based-reinforcement-learning</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l11-model-based-reinforcement-learning</guid><description>CS285 Lecture11 Model-Based Reinforcement Learning</description><pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.C-W_BzN6.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;这一节正式进入model-base，之前的方法是我们知道状态转移函数，但是现在我们不知道，我们需要学这个。&lt;/p&gt;
&lt;p&gt;简单的方法就是利用监督学习那一套
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2019.40.42.png&quot; alt=&quot;alt text&quot;&gt;
但是这个方法会有分布偏移，于是可以改进一下
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2019.43.07.png&quot; alt=&quot;alt text&quot;&gt;
然后这种方法一但进入到错误的动作的话，可能表现就不是很好。于是乎，还可以改进为每走一步我就重新规划一次，这样虽然增加计算量，但是他的效果会更好
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2019.44.54.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是这种简单的 Model-Based他比不过model free的算法
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2019.57.31.png&quot; alt=&quot;alt text&quot;&gt;
当你数据不够多时（图中的黑点很少），神经网络（蓝色曲线）很容易为了强行拟合这几个点而变得扭曲（Wiggle）。
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2020.00.38.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;实际上那个波峰是模型“脑补”出来的幻觉（Hallucination），真实环境（直线）里那里并没有高分。这导致 Model-Based 方法在早期容易被模型的误差误导，且即使数据多了，模型也很难完美拟合复杂的真实物理世界（Model Bias）。&lt;/p&gt;
&lt;p&gt;既然模型在未探索区域的预测不可信，那么就让模型量化自己的不确定性。于是乎我们可以在算法第三步的时候只采取那些我们在考虑了不确定性之后，依然认为期望回报很高的动作&lt;/p&gt;
&lt;p&gt;那么怎么知道模型的不确定性有多少呢&lt;/p&gt;
&lt;h2&gt;Uncertainty-Aware Neural Net Models&lt;/h2&gt;
&lt;h3&gt;output entropy&lt;/h3&gt;
&lt;p&gt;第一种就是衡量模型输出动作分布的熵是多少，但是这种方法是不好的
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2020.04.46.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;原因在于单纯看输出的混乱程度，你无法知道模型是因为“数据太乱了”所以不确定，还是因为“我没学过这个”所以不确定（或者甚至它在“不懂装懂”）。&lt;/p&gt;
&lt;p&gt;它将不确定性拆解为两类：&lt;/p&gt;
&lt;h4&gt;A. 偶然不确定性 / 统计不确定性 (Aleatoric / Statistical Uncertainty)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对应图像&lt;/strong&gt;：PPT右下角的图。数据点本来就带有噪声，围绕着真实函数上下波动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：这是&lt;strong&gt;数据内在的随机性&lt;/strong&gt;（Data noise）。
&lt;ul&gt;
&lt;li&gt;例如：你扔一枚硬币，结果是不确定的。这是物理性质决定的，无论你的模型多么完美，你都无法消除这种不确定性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键点&lt;/strong&gt;：增加更多的数据&lt;strong&gt;无法&lt;/strong&gt;减少这种不确定性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;B. 认知不确定性 / 模型不确定性 (Epistemic / Model Uncertainty)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对应图像&lt;/strong&gt;：PPT左下角的图。蓝色的线（模型）为了穿过几个稀疏的黑点（数据），扭曲成了奇怪的形状（过拟合）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：这是由于&lt;strong&gt;缺乏知识&lt;/strong&gt;（Lack of knowledge）导致的不确定性。
&lt;ul&gt;
&lt;li&gt;模型可能在训练数据点上表现得很“自信”（方差小），但在没有数据覆盖的区域（Out-of-distribution），模型可能会给出完全错误的预测。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键点&lt;/strong&gt;：PPT引用的那句话 &lt;em&gt;&quot;the model is certain about the data, but we are not certain about the model&quot;&lt;/em&gt;（模型对数据很确定，但我们对模型本身不确定）就是指这种情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：增加更多的数据&lt;strong&gt;可以&lt;/strong&gt;减少这种不确定性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了这种办法不能区分这两种不确定性，他还无法捕捉“模型的无知”
PPT 左下角的图（那条扭来扭去的蓝色曲线）展示了&lt;strong&gt;过拟合&lt;/strong&gt;或模型本身的问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在这个区域，模型可能拟合出了一条非常确定的线（方差看起来很小，熵很低）。&lt;/li&gt;
&lt;li&gt;但实际上，如果我们换一组训练数据，或者稍微改变一下模型参数，这条线可能会剧烈变化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出熵&lt;/strong&gt;只能告诉你这条线目前的预测值是多少，它不能告诉你“&lt;strong&gt;如果我们重新训练一次，这条线会有多大的变化&lt;/strong&gt;”。&lt;/li&gt;
&lt;li&gt;真正的认知不确定性（Epistemic）应该衡量的是：“我对这个模型参数/这条曲线的形状有多大把握？”而不仅仅是 “这个点的预测值概率是多少？”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Bayesian neural networks&lt;/h3&gt;
&lt;p&gt;为了解决这些问题，可以用贝叶斯神经网络。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;传统深度学习）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公式：$\arg\max_{\theta} \log p(\theta|\mathcal{D})$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解释&lt;/strong&gt;：我们在训练神经网络时，目的是找到&lt;strong&gt;一组&lt;/strong&gt;“最好的”参数 $\theta$（即权重大模型），让它最能解释训练数据。这被称为&lt;strong&gt;点估计（Point Estimation）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：不管是通过MLE（最大似然）还是MAP（最大后验），我们最终只得到了&lt;strong&gt;一个&lt;/strong&gt;模型。如果这个模型“过度自信”但其实是错的，我们无从知晓。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;现在的想法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公式：estimate $p(\theta|\mathcal{D})$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解释&lt;/strong&gt;：我们不再寻找某一组特定的参数，而是试图去估计&lt;strong&gt;参数的分布&lt;/strong&gt;。也就是说，我们想知道：在给定数据 $\mathcal{D}$ 的情况下，参数 $\theta$ 可能是哪些值？这些值的概率是多少？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键点&lt;/strong&gt;：PPT箭头指出 &lt;em&gt;&quot;the entropy of this tells us the model uncertainty!&quot;&lt;/em&gt;。如果 $p(\theta|\mathcal{D})$ 这个分布很宽（熵很大），说明有很多种参数配置都能解释数据，这就意味着我们对模型到底长什么样&lt;strong&gt;很不确定&lt;/strong&gt;（即模型不确定性高）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\int p(s_{t+1}|s_t, a_t, \theta)p(\theta|\mathcal{D})d\theta
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们让&lt;strong&gt;所有可能&lt;/strong&gt;的参数配置（即无数个模型）都来进行预测，然后根据它们在后验分布 $p(\theta|\mathcal{D})$ 中的概率（即该组参数靠谱的程度）进行加权平均。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在数据充足的地方，所有模型预测都差不多，结果很确定。&lt;/li&gt;
&lt;li&gt;在数据稀疏的地方（OOD），不同的模型参数会给出天差地别的预测，平均下来的结果就会表现出很高的不确定性（如右下角图表中阴影变宽的部分）。
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2020.21.41.png&quot; alt=&quot;alt text&quot;&gt;
&lt;strong&gt;贝叶斯神经网络（Bayesian NN）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;连接线上不再是数字，而是高斯分布。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：每个权重不再是一个数，而是一个随机变量（Random Variable）**。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数变化&lt;/strong&gt;：对于每一个连接，我们不再只学一个 $w$，而是要学两个参数：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$\mu_i$ (均值/Expected weight)&lt;/strong&gt;：权重最可能的值是多少（比如 0.5）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\sigma_i$ (方差/Uncertainty)&lt;/strong&gt;：我们对这个权重有多不确定（比如 $\pm 0.1$）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;计算完整的联合后验分布 $p(\theta|\mathcal{D})$ 是几乎不可能的，因为参数 $\theta$可能有几百万个，它们之间可能还有复杂的依赖关系。&lt;/li&gt;
&lt;li&gt;简化的方案（Mean Field Approximation）：
公式：$p(\theta|\mathcal{D}) = \prod_i p(\theta_i|\mathcal{D})$
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;假设&lt;/strong&gt;：我们强制假设网络里的每一个权重 $\theta_i$ 都是&lt;strong&gt;相互独立&lt;/strong&gt;的。&lt;/li&gt;
&lt;li&gt;这样我们就可以把一个巨大的复杂分布，拆解成无数个简单的小分布的乘积。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;具体的分布形式：
公式：$p(\theta_i|\mathcal{D}) = \mathcal{N}(\mu_i, \sigma_i)$
&lt;ul&gt;
&lt;li&gt;我们假设每个权重都服从&lt;strong&gt;正态分布（高斯分布）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练目标&lt;/strong&gt;：以前我们训练是调整 $w$ 来降低 Loss；现在我们训练是调整 $\mu$ 和 $\sigma$，使得这个分布能最好地拟合数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Bootstrap ensembles&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-14%20at%2020.24.49.png&quot; alt=&quot;alt text&quot;&gt;
贝叶斯太复杂了，我们可以有一个更实用简单的方法就是训练多个相互独立的传统模型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一致（Low Variance）&lt;/strong&gt;：如果这 $N$ 个模型对同一个输入 $(s_t, a_t)$ 给出了几乎一样的预测，说明大家都很确定，&lt;strong&gt;不确定性低&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分歧（High Variance）&lt;/strong&gt;：如果有的模型说往左，有的说往右，大家意见不合，说明面对这个情况模型很迷茫，&lt;strong&gt;不确定性高&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数学上其实就相当于
$$
\int p(s_{t+1}|s_t, a_t, \theta)p(\theta|\mathcal{D})d\theta\approx \frac{1}{N} \sum_i p(s_{t+1}|s_t, a_t, \theta_i)
$$
然后怎么让模型相互独立呢，方法就是bootstrap。
假设原始数据集 $\mathcal{D}$大小是N，然后我可以重复抽N次就构成$\mathcal{D}_i$ 数据集，拿这个数据集训练一个模型。最终这些模型就是相互独立的&lt;/p&gt;
&lt;p&gt;我们可以用这个不确定性来规划我们的动作
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2020.29.21.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Model-Based RL with Images&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-14%20at%2021.45.55.png&quot; alt=&quot;alt text&quot;&gt;
当机器人或 Agent 只能看到图像（observation, $\mathbf{o}$），而无法直接获得真实的物理状态（state, $\mathbf{s}$）时，如何构建一个数学模型来描述世界的运作规律，并用于训练。&lt;/p&gt;
&lt;p&gt;我们很难说在图像像素层面预测未来，因为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High dimensionality（高维性）&lt;/strong&gt;：一张图片包含成千上万个像素，直接预测下一帧的每一个像素计算量巨大且极难收敛。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redundancy（冗余性）&lt;/strong&gt;：图片里有很多无关信息（比如机械臂背景里的墙、桌子的纹理），这些对任务并不重要，但占用了大量数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partial observability（部分可观测性）&lt;/strong&gt;：单张静态图片可能无法包含所有信息（比如物体的速度、被遮挡的物体），这使得 $s_t$ 实际上变成了 $o_t$（观测），而非完整的系统状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是乎我们想要将这个问题解耦&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$p(o_t|s_t)$ (Observation Model / Encoder-Decoder)&lt;/strong&gt;：负责处理“高维但非动态”的部分。即学习如何把复杂的图像压缩成简单的状态 $s$（或者从 $s$ 还原图像）。这解决了高维和冗余问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$p(s_{t+1}|s_t, a_t)$ (Dynamics Model / Transition)&lt;/strong&gt;：负责处理“低维但动态”的部分。即在压缩后的低维空间里预测未来。因为维度低，预测变得容易且高效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-14%20at%2021.40.16.png&quot; alt=&quot;alt text&quot;&gt;这张图展示了问题的核心结构，即一个 &lt;strong&gt;POMDP（部分可观测马尔可夫决策过程）&lt;/strong&gt; 的图模型。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;图模型结构 (PGM):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$\mathbf{s}_t$ (Latent State):&lt;/strong&gt; 这是一个不可见的“潜在状态”（比如机器人的关节角度、物体的真实物理位置）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\mathbf{o}_t$ (Observation):&lt;/strong&gt; 这是 Agent 实际看到的（比如摄像头拍到的像素图片）。$\mathbf{o}_t$ 由 $\mathbf{s}_t$ 生成（Observation model）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\mathbf{a}_t$ (Action):&lt;/strong&gt; 动作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\mathbf{s}_{t+1}$ (Dynamics):&lt;/strong&gt; 下一时刻的状态取决于当前状态和动作。这是我们需要学习的核心“动力学模型”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;训练目标 (How to train?):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;标准模型 (Standard):&lt;/strong&gt; 如果我们知道真实状态 $\mathbf{s}$，直接最大化 $p(\mathbf{s}_{t+1}|\mathbf{s}_t, \mathbf{a}_t)$ 的对数似然即可（类似监督学习）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;潜在空间模型 (Latent space model):&lt;/strong&gt; 因为 $\mathbf{s}$ 是未知的，我们无法直接训练。因此，目标变成了最大化观测数据 $\mathbf{o}$ 的似然概率。&lt;/li&gt;
&lt;li&gt;公式中的 $E[\dots]$ 表示我们需要针对潜在状态分布求期望。这实际上是在通过 &lt;strong&gt;变分推断（Variational Inference）&lt;/strong&gt; 的思路，最大化 &lt;strong&gt;ELBO（Evidence Lower Bound）&lt;/strong&gt;。
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2021.50.08.png&quot; alt=&quot;alt text&quot;&gt;
由于现在我们不知道s是多少，于是我们可以根据o去预测s，我们就训练一个神经网络（Encoder，$q$）来近似它。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;选项 A: Full smoothing posterior ($q(s|o_{1:T}, a_{1:T})$)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;做法&lt;/strong&gt;：利用&lt;strong&gt;所有&lt;/strong&gt;的历史数据（甚至未来的数据，如果是离线训练）来推测当前的 $s_t$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优缺点&lt;/strong&gt;：最准（Most accurate），因为信息量最大，但计算最复杂（Most complicated），通常需要用到 RNN 或 Transformer。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选项 B (红圈): Single-step encoder ($q_{\psi}(s_t|o_t)$)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;做法&lt;/strong&gt;：只看&lt;strong&gt;当前这一张图&lt;/strong&gt; $o_t$，直接推测 $s_t$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优缺点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simplest（最简单）&lt;/strong&gt;：这就是标准的 VAE Encoder，给一张图，出一个向量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Least accurate（最不准）&lt;/strong&gt;：因为它丢失了时序信息（比如无法通过一张静态图判断物体的速度/加速度）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们目前先看最简单的Single-step encoder，而且是deterministic的情况
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2021.50.40.png&quot; alt=&quot;alt text&quot;&gt;
我们就可以用o来估计s。公式上就用encoder和o来代替s。&lt;/p&gt;
&lt;p&gt;把奖励加上，我们就可以更新我们想要的训练公式
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2021.54.34.png&quot; alt=&quot;alt text&quot;&gt;
最终套用一下原本的训练框架，就得到了对应的训练流程
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2021.55.13.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;</content:encoded><img src="/_astro/11.C-W_BzN6.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.C-W_BzN6.png" length="0" type="image/png"/></item><item><title>CS285 Lecture10 Optimal Control and Planning</title><link>https://laurie-hxf.xyz/blog/cs285-l10-optimal-control-and-planning</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l10-optimal-control-and-planning</guid><description>CS285 Lecture10 Optimal Control and Planning</description><pubDate>Sun, 14 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.CubWzGf8.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;我们之前讲的都是model-free 的强化学习算法，现在转向model-base的算法。假设我们可以知道转移方程的话，我们的算法又应该怎么改进。这节课主要假设我们已经知道了准确的状态转移方程的情况下我们怎么做决策。
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.22.53.png&quot; alt=&quot;alt text&quot;&gt;
首先介绍一下两种场景，一种就是closed-loop，agent获取一个状态就做一次动作再获取状态再做一次动作，如此循环往复。与之对应的就是open-loop，获取一次状态然后做完所有的动作。&lt;/p&gt;
&lt;p&gt;乍一看可能open-loop有什么场景，肯定不如closed-loop好，但是不一定，在某些特定的场景，维度比较低的时候，场景规律简单的时候，用这个的效果也很好。&lt;/p&gt;
&lt;h2&gt;Open-Loop Planning&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.31.21.png&quot; alt=&quot;alt text&quot;&gt;
我们的目标是优化这个，找到最好的轨迹
$$
\mathbf{a}_1, \dots, \mathbf{a}&lt;em&gt;T = \arg \max&lt;/em&gt;{\mathbf{a}_1, \dots, \mathbf{a}&lt;em&gt;T} \sum&lt;/em&gt;{t=1}^T r(\mathbf{s}_t, \mathbf{a}&lt;em&gt;t) \quad \text{s.t.} \quad \mathbf{a}&lt;/em&gt;{t+1} = f(\mathbf{s}_t, \mathbf{a}_t)
$$
最简单的方法就是从一个分布中随机取一个动作序列，然后验证这个动作序列的好坏，然后最终选取抽取中最好的那个动作序列&lt;/p&gt;
&lt;h3&gt;CEM&lt;/h3&gt;
&lt;p&gt;进阶版就是我随机取完评估之后，改变概率分布，让好的动作序列那部分的概率变大，从而可以更容易选出好的动作出来
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.33.19.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;MCTS&lt;/h3&gt;
&lt;p&gt;还可以用蒙特卡洛方法
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.35.15.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Trajectory Optimization with Derivatives&lt;/h2&gt;
&lt;p&gt;我们还可以用导数来优化我们的策略，主要有两种方向。
一种是shooting methods：只优化动作 (Optimize over actions only)
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.39.30.png&quot; alt=&quot;alt text&quot;&gt;
另一种就是Collocation Method (配点法)：优化器不仅控制动作 (Action/Control, $\mathbf{u}$)，还可以直接修改状态 (State, $\mathbf{x}$)
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.41.03.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;这节课主要讲的是shooting methods&lt;/p&gt;
&lt;h2&gt;LQR&lt;/h2&gt;
&lt;p&gt;我们对前面那个复杂的shooting methods做了一个特例化的简化，我们假设系统的物理规律是线性，然后我们的代价是一个二次型。我们来求解这种情况下的解是多少。
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.44.32.png&quot; alt=&quot;alt text&quot;&gt;
为了找到最优解，我们不从 $t=1$ 开始算，而是先看最后一步 。以下是具体的逻辑推理
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.45.58.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们只看最后一项然后对最后时刻的动作$u_T$求导,然后解ppt下面的等式，最后用$x_T$来表达&lt;/p&gt;
&lt;p&gt;然后我们用$x_T$等式替换原先的$u_T$，之后化简一下，最终的cost就可以只有$x_T$这一个变量表达
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.48.48.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们计算前一个时刻的cost是多少，我们知道最后时刻的$x_T$是由前一时刻的$x_{T-1}$和$u_{T-1}$得到的，然后我们替换化简一下。前一个时刻的cost现在就变成只和$x_{T-1}$和$u_{T-1}$有关系
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.51.04.png&quot; alt=&quot;alt text&quot;&gt;
然后我们再整理一下，他的形式就又变成了二次型，跟一开始的形式一模一样，然后我们就用同样的套路对$x_{T-1}$求导，化简，于是乎这个就可以变成一个迭代的过程
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.53.55.png&quot; alt=&quot;alt text&quot;&gt;
我们可以从最后时刻一步步反推得到一开始最优的动作，然后再正推得到所有的动作，这就是LQR算法
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.56.06.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;LQR for Stochastic and Nonlinear Systems&lt;/h2&gt;
&lt;p&gt;我们之前的假设是deterministic的而且是线形的，就是给定$x_{T-1}$和$u_{T-1}$，下一时刻的状态$u_T$是确定的。现在拓展一下，先假设他不再是deterministic。&lt;/p&gt;
&lt;p&gt;我们可以给他加一个高斯噪声，但是这个对于我们原先的LQR算法没有影响，可以不用变
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2021.59.36.png&quot; alt=&quot;alt text&quot;&gt;
下面就看如果他不再是线形的，我们应该怎么改进我们的算法&lt;/p&gt;
&lt;h3&gt;DDP/iterative LQR&lt;/h3&gt;
&lt;p&gt;现在假设我们的$f(\mathbf{x}_t, \mathbf{u}_t)$不再是线形的，$c(\mathbf{x}_t, \mathbf{u}_t)$ 不再是二次型。我们可以用泰勒展开来在局部近似成线形和二次型
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2023.52.06.png&quot; alt=&quot;alt text&quot;&gt;
然后运行LQR
&lt;img src=&quot;./Screenshot%202025-12-13%20at%2023.53.05.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;初始化 (Initialization)&lt;/h4&gt;
&lt;p&gt;在进入循环之前，你需要先有一个&lt;strong&gt;初始猜测的轨迹&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你需要先选定一组初始的动作序列 $\hat{\mathbf{u}}_1, \dots, \hat{\mathbf{u}}_T$。&lt;/li&gt;
&lt;li&gt;然后用真实的非线性动力学模型 $f(\mathbf{x}, \mathbf{u})$ 跑一遍（Rollout），得到对应的初始状态序列 $\hat{\mathbf{x}}_1, \dots, \hat{\mathbf{x}}_T$&lt;/li&gt;
&lt;li&gt;这组 $(\hat{\mathbf{x}}, \hat{\mathbf{u}})$ 被称为&lt;strong&gt;标称轨迹 (Nominal Trajectory)&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;迭代循环 (The Iteration Loop)&lt;/h4&gt;
&lt;p&gt;算法的核心是一个循环，直到轨迹不再发生显著变化（收敛）为止。循环内包含三个主要步骤：&lt;/p&gt;
&lt;h5&gt;第一步：近似/线性化 (Approximation)&lt;/h5&gt;
&lt;p&gt;在当前的标称轨迹 $(\hat{\mathbf{x}}, \hat{\mathbf{u}})$ 附近，把复杂的非线性问题简化成一个 LQR 问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算导数&lt;/strong&gt;：对动力学函数 $f$ 和代价函数 $c$ 分别求导。
&lt;ul&gt;
&lt;li&gt;$\mathbf{F}&lt;em&gt;t = \nabla&lt;/em&gt;{\mathbf{x},\mathbf{u}} f$ (动力学的雅可比矩阵)。&lt;/li&gt;
&lt;li&gt;$\mathbf{c}&lt;em&gt;t = \nabla&lt;/em&gt;{\mathbf{x},\mathbf{u}} c$ (代价的梯度)。&lt;/li&gt;
&lt;li&gt;$\mathbf{C}&lt;em&gt;t = \nabla^2&lt;/em&gt;{\mathbf{x},\mathbf{u}} c$ (代价的海森矩阵)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建局部模型&lt;/strong&gt;：这一步构造出了关于&lt;strong&gt;偏差&lt;/strong&gt; ($\delta \mathbf{x}, \delta \mathbf{u}$) 的线性动力学方程 $\bar{f}$ 和二次代价函数 $\bar{c}$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;第二步：向后计算 (Backward Pass)&lt;/h5&gt;
&lt;p&gt;利用上一步算出的矩阵 ($\mathbf{F}, \mathbf{C}, \mathbf{c}$)，运行标准的 LQR 算法来规划“如何修正动作”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是一个&lt;strong&gt;从时间 $T$ 倒推回 $1$&lt;/strong&gt; 的过程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：计算出每一时刻的最优反馈控制律参数 $\mathbf{K}_t$ (反馈增益) 和 $\mathbf{k}_t$ (前馈项)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算逻辑&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;先算最后一步 $T$ 的 $\mathbf{K}_T, \mathbf{k}_T$。&lt;/li&gt;
&lt;li&gt;利用贝尔曼方程，把 $T$ 时刻的价值函数信息传递给 $T-1$，算出 $\mathbf{K}&lt;em&gt;{T-1}, \mathbf{k}&lt;/em&gt;{T-1}$。&lt;/li&gt;
&lt;li&gt;一直推导到 $t=1$。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;第三步：向前计算 (Forward Pass)&lt;/h5&gt;
&lt;p&gt;拿着刚才算出的控制律，在&lt;strong&gt;真实的非线性系统&lt;/strong&gt;上跑一遍，生成新的轨迹。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新动作：新的动作 $\mathbf{u}_t$ 由原来的动作加上修正量构成：
$$
\mathbf{u}_t = \hat{\mathbf{u}}_t + \mathbf{K}_t(\mathbf{x}_t - \hat{\mathbf{x}}_t) + \mathbf{k}_t
$$&lt;/li&gt;
&lt;li&gt;$\mathbf{k}_t$ 是开环的改进（前馈）。&lt;/li&gt;
&lt;li&gt;$\mathbf{K}_t(\mathbf{x}_t - \hat{\mathbf{x}}_t)$ 是闭环的反馈，用来修正因为模型线性化误差导致的偏离。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成新轨迹&lt;/strong&gt;：将这个新动作输入到真实的非线性动力学 $f(\mathbf{x}, \mathbf{u})$ 中，计算出新的状态序列。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;更新与收敛 (Update &amp;#x26; Convergence)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;更新&lt;/strong&gt;：用 Forward Pass 生成的这条&lt;strong&gt;新轨迹&lt;/strong&gt;，替换掉旧的标称轨迹，成为下一轮循环的起点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;终止&lt;/strong&gt;：如果新轨迹和旧轨迹的代价差不多（或者梯度几乎为0），说明已经找到了局部最优解，算法结束。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后指出iLQR是牛顿法的一种近似，如果我们也用二次展开$f(\mathbf{x}_t, \mathbf{u}_t)$，那么就是成了真正的全量牛顿法 (DDP)&lt;/p&gt;
&lt;p&gt;但是牛顿法就会有一个问题
&lt;img src=&quot;./Screenshot%202025-12-14%20at%2000.01.04.png&quot; alt=&quot;alt text&quot;&gt;
可以看左边那个图，牛顿法用二次函数对曲线进行拟合，这就有问题，我们朝着拟合的那个二次曲线优化而不是实际的蓝色那线，你会看到就有偏差，导致你的模型很难收敛。&lt;/p&gt;
&lt;p&gt;于是乎他在
$$
\mathbf{u}_t = \hat{\mathbf{u}}_t + \mathbf{K}_t(\mathbf{x}_t - \hat{\mathbf{x}}_t) + \alpha\mathbf{k}_t
$$
这个公式里面加了$\alpha$，用来控制运动的幅度，从而解决这个问题&lt;/p&gt;</content:encoded><img src="/_astro/11.CubWzGf8.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.CubWzGf8.png" length="0" type="image/png"/></item><item><title>CS285 Lecture8 Deep RL with Q-Functions</title><link>https://laurie-hxf.xyz/blog/cs285-l8-deep-rl-with-q-functions</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l8-deep-rl-with-q-functions</guid><description>CS285 Lecture8 Deep RL with Q-Functions</description><pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.BzPsWl-a.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-09%20at%2023.37.35.png&quot; alt=&quot;alt text&quot;&gt;
之前我们提到Q learning并不保证收敛，即使最后一步看着像梯度下降，但是他并不是。
因为
$$
\text{Target} = r(s, a) + \gamma \max_{a&apos;} Q_\phi(s&apos;, a&apos;)
$$
真正的梯度应该是：
$$
\nabla_\phi L = (Q - \text{Target}) \cdot (\nabla_\phi Q - \nabla_\phi \text{Target})
$$
但是第二部分我们在计算梯度时，把红圈里的项看作是一个常数。我们假装它只是一个普通的 Label，不让梯度反向传播穿过它。所以也称之为半梯度(Semi-Gradient)。&lt;/p&gt;
&lt;p&gt;在 Online Q-learning中，Target 是不稳定的，目标值 $y_i$ 本身包含网络参数 $\phi$，导致你在追一个移动的靶子，从而导致很难收敛。&lt;/p&gt;
&lt;p&gt;同时，智能体是在与环境实时交互的。每次的训练数据是高度连续的（Sequential），神经网络会发生“灾难性遗忘”（Catastrophic Forgetting）。每次神经网络只能看到局部的方框中的数据，于是当这个方框向后移动的时候，每次神经网络都会过拟合这部分，所以当这个方框走完这个轨迹的时候，神经网络只会最后的一段路，忘记前面的路径，导致在未来表现很差。
&lt;img src=&quot;./Screenshot%202025-12-09%20at%2023.48.11.png&quot; alt=&quot;alt text&quot;&gt;
我们可以借鉴之前的思路采用同步或异步的方式去获取更多的数据来训练，就像之前的Actor-Critic Algorithms一样。&lt;/p&gt;
&lt;p&gt;或者更好的我们也可以像之前一样用一个buffer来存数据，每次用从这个buffer里面抽一些batch来训练，这样保证了数据之间的独立同分布，然后计算梯度的时候他是一个batch的一起计算所以步骤3那里多了一个求和，这个也可以降低方差。
&lt;img src=&quot;./Screenshot%202025-12-10%20at%2000.00.51.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;DQN&lt;/h2&gt;
&lt;p&gt;我们通过buffer的方式解决了数据相关性问题，但是还有target在不断变化的问题
&lt;img src=&quot;./Screenshot%202025-12-10%20at%2009.32.52.png&quot; alt=&quot;alt text&quot;&gt;
DQN中的解决办法就是我计算目标的时候并不总是用最新的网络$\phi$,而是用$\phi&apos;$ ，也就是我们在循环内不改变target，等到每次循环完回到第一步的时候我再改变参数&lt;/p&gt;
&lt;p&gt;这种方法会有一定的延后(lagged)，所有就有人提出每次用$\phi&apos;\leftarrow \tau\phi&apos;+(1-\tau)\phi$ 来更新&lt;br&gt;
&lt;img src=&quot;./Screenshot%202025-12-10%20at%2009.36.11.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;DDNQ&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-10%20at%2016.33.37.png&quot; alt=&quot;alt text&quot;&gt;
Q-learning中有&quot;高估偏差&quot;（Overestimation Bias）的问题，指的是算法对动作价值函数（Q值）产生系统性过高估计。&lt;/p&gt;
&lt;p&gt;高估偏差的根本原因在于Q-learning更新规则中的 max 操作。当价值估计存在随机噪声时，取最大值操作（max）会倾向于选择那些被高估的Q值，而不是真实最优的值。从数学角度看，先对N个Q值取最大值再求期望，会比先求期望再取最大值得到更大的结果，这就是过估计的数学基础。
$$
E[\max(X_1, X_2)] \ge \max(E[X_1], E[X_2])
$$
也就是“噪声最大值的期望” 大于等于 “真实值的最大值”。&lt;/p&gt;
&lt;p&gt;Max 操作可以分为两步
$$
\max_{\mathbf{a}&apos;} Q_{\phi&apos;}(\mathbf{s}&apos;, \mathbf{a}&apos;) = Q_{\phi&apos;}(\mathbf{s}&apos;, \underline{\arg\max_{\mathbf{a}&apos;} Q_{\phi&apos;}(\mathbf{s}&apos;, \mathbf{a}&apos;)})
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;步骤 1（下划线部分 - 选动作）：&lt;/strong&gt; $\arg\max_{\mathbf{a}&apos;} Q_{\phi&apos;}(\mathbf{s}&apos;, \mathbf{a}&apos;)$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这就是在问：“哪个动作看起来最好？”&lt;/li&gt;
&lt;li&gt;PPT 注释说：&lt;strong&gt;&quot;action selected according to $Q_{\phi&apos;}$&quot;&lt;/strong&gt;（根据网络 $Q_{\phi&apos;}$ 选择动作）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;步骤 2（外层部分 - 算分数）：&lt;/strong&gt; $Q_{\phi&apos;}(\mathbf{s}&apos;, \dots)$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这就是在问：“这个被选中的动作价值多少？”&lt;/li&gt;
&lt;li&gt;PPT 注释说：&lt;strong&gt;&quot;value &lt;em&gt;also&lt;/em&gt; comes from $Q_{\phi&apos;}$&quot;&lt;/strong&gt;（价值&lt;strong&gt;也&lt;/strong&gt;来自于网络 $Q_{\phi&apos;}$）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在标准的 Q-learning（DQN）中，你会发现上面两个步骤用的都是&lt;strong&gt;同一个网络参数 $\phi&apos;$&lt;/strong&gt;（通常是 Target Network）。
这就导致了问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果网络因为噪声（Noise）错误地认为动作 A 是最好的（高估了 A），那么它不仅会选中 A（步骤 1），还会用那个高估的值作为目标（步骤 2）。&lt;/li&gt;
&lt;li&gt;这就是 PPT 底部所说的：&lt;strong&gt;&quot;these are correlated!&quot;&lt;/strong&gt;（噪声是相关的）。同一个网络的噪声会导致它在选动作和估值时犯同样的错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如何解决呢，办法就是让这两个步骤解偶，用两个模型来预测，如果这两者中的噪声是不相关的，问题就解决了
&lt;img src=&quot;./Screenshot%202025-12-10%20at%2016.46.16.png&quot; alt=&quot;alt text&quot;&gt;
具体做法就是复用一下DNQ中的现有的两个网络（Current Network 和 Target Network）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DQN中的解决办法就是我计算目标的时候并不总是用最新的网络$\phi$,而是用$\phi&apos;$ ，也就是我们在循环内不改变target，等到每次循环完回到第一步的时候我再改变参数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Multi-step Returns&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-10%20at%2016.52.11.png&quot; alt=&quot;alt text&quot;&gt;
它的核心目的是解决强化学习中一个经典的权衡问题：偏差（Bias）与方差（Variance）的平衡。
简单来说，就是我们在计算目标值（Target）时，应该“多看几步真实发生的奖励”，还是“早点依赖模型的预测”？&lt;/p&gt;
&lt;h3&gt;单步 Q-learning 的局限&lt;/h3&gt;
&lt;p&gt;看 PPT 最上面的公式：
$$
y_{j,t} = r_{j,t} + \gamma \max Q_{\phi&apos;}(s_{t+1}, \dots)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 $Q_{\phi&apos;}$ 很烂（Training初期）：&lt;/p&gt;
&lt;p&gt;PPT 左边的箭头指出：“these are the only values that matter if $Q_{\phi&apos;}$ is bad!”。
意思是，如果你的神经网络（Q函数）还没训练好，它输出的值基本就是瞎猜的垃圾。这时，整个公式里唯一真实可靠的信息，只有当前的这一步奖励 $r_{j,t}$。剩下的部分全是误差。这会导致学习非常慢，因为真实的奖励信号传递得很慢。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;偏差与方差（PPT 中间部分）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q-learning (1-step): 高偏差 (Max Bias)，低方差 (Min Variance)。因为它只用了一步真实奖励，剩下全靠估计（Bootstrapping）。估计是不准的（偏差），但因为没有引入太多随机的未来路径，所以比较稳定（方差低）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy Gradient (Monte Carlo):&lt;/strong&gt; &lt;strong&gt;无偏差 (No Bias)，高方差 (High Variance)&lt;/strong&gt;。看中间下面的公式，它用的是 $\sum \gamma^t r$（即等到游戏结束，把所有真实奖励加起来）。这是事实数据，没有偏差，但因为每次游戏的路径都不一样，波动非常大，导致方差极高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;折衷的 Multi-step Returns&lt;/h3&gt;
&lt;p&gt;PPT 最下方提出了问题：“Can we construct multi-step targets?”（我们可以构建多步目标吗？）
答案就是底部的公式（N-step Return Estimator）：
$$
y_{j,t} = \sum_{t&apos;=t}^{t+N-1} \gamma^{t&apos;-t} r_{j,t&apos;} + \gamma^N \max Q_{\phi&apos;}(s_{t+N}, \dots)
$$
这个公式的意思是：我不只看 1 步，也不看直到结束，而是看 N 步。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前一部分（求和号）：我先累加接下来 $N$ 步真实发生的奖励（Real Rewards）。这部分是事实，没有偏差。&lt;/li&gt;
&lt;li&gt;后一部分（Max Q）：在第 $N$ 步之后，我再用神经网络的预测值来作为剩下的估计。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种方法结合了两种极端的优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;传导更快（Faster Propagation）：
想象你在一把很长的游戏中，只有最后赢了才有奖励。
&lt;ul&gt;
&lt;li&gt;1-step Q-learning:倒数第一步知道赢了，倒数第二步要等下一步更新才知道……奖励信号需要迭代很多次才能传到起点。&lt;/li&gt;
&lt;li&gt;N-step Returns: 一次更新就能把奖励信号向前回传 $N$ 步。学习效率大大提高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;准确度平衡：
&lt;ul&gt;
&lt;li&gt;比 1-step 准确，因为用了更多真实的 $r$，减少了对不可靠 Q 值的依赖（减少偏差）。&lt;/li&gt;
&lt;li&gt;比 Monte Carlo 稳定，因为没有等到完全结束，减少了由于路径随机性带来的剧烈波动（控制方差）。
&lt;img src=&quot;./Screenshot%202025-12-10%20at%2016.55.37.png&quot; alt=&quot;alt text&quot;&gt;
但是他的局限就是数学上其实不满足off-policy策略，N&gt;1 时的问题：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;当你累加未来 N 步的奖励 $\sum r$ 时，你隐含地假设：“这 N 步都是按照我当前想要评估的那个策略（最优策略）走出来的。”&lt;/li&gt;
&lt;li&gt;但在 Replay Buffer 里的历史数据中，智能体可能在第 2 步或者第 3 步做了一个愚蠢的随机动作（非最优动作）。&lt;/li&gt;
&lt;li&gt;如果你把这个“愚蠢路径”上的奖励加起来，告诉神经网络“这就是最优策略的价值”，你就教错了&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q-Learning with Continuous Actions&lt;/h2&gt;
&lt;p&gt;之前我们讨论的都是在离散动作的情况下，当我们想拓展到连续动作的情况下时，问题就在于
&lt;img src=&quot;./Screenshot%202025-12-11%20at%2015.16.08.png&quot; alt=&quot;alt text&quot;&gt;
max的部分对于连续的动作来说不是很好处理&lt;/p&gt;
&lt;h3&gt;Stochastic optimization&lt;/h3&gt;
&lt;p&gt;最简单的方法就是在连续分布中采样，采样的结果就当作离散的动作，然后再运用原本的算法
&lt;img src=&quot;./Screenshot%202025-12-11%20at%2015.22.12.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Easily maximizable Q-functions&lt;/h2&gt;
&lt;p&gt;我们还可以采用更好优化的Q函数，如果我们的Q函数很容易知道他的最大值是多少，那也可以
&lt;img src=&quot;./Screenshot%202025-12-11%20at%2015.25.12.png&quot; alt=&quot;alt text&quot;&gt;
比如说这个工作将Q函数定义为二次函数，于是很容易就知道他的最大值是什么，但是问题就是，丧失了表达能力。因为它强制假设 Q 函数是一个单峰的二次函数（Unimodal）。但在复杂的强化学习环境中，真实的 Q 函数可能是多峰的（Multimodal），比如“向左走”和“向右走”都很好，但“中间不动”很差。NAF 无法很好地拟合这种复杂的函数形状。&lt;/p&gt;
&lt;h2&gt;DDPG&lt;/h2&gt;
&lt;p&gt;第三种方法就是训练另一个神经网络，专门来预测哪个动作可以使Q值最大。
因为$\max_{\mathbf{a}} Q(s, a)$可以写为
$$
\max_{\mathbf{a}&apos;} Q_{\phi&apos;}(\mathbf{s}&apos;, \mathbf{a}&apos;) = Q_{\phi&apos;}(\mathbf{s}&apos;, \arg\max_{\mathbf{a}&apos;} Q_{\phi&apos;}(\mathbf{s}&apos;, \mathbf{a}&apos;))
$$
所以可以训练一个模型专门预测哪个动作可以使Q值最大。也就是
$$
\mu_{\theta}(s) \approx \arg\max_{\mathbf{a}} Q_{\phi}(\mathbf{s}, \mathbf{a})
$$
&lt;img src=&quot;./Screenshot%202025-12-11%20at%2015.33.18.png&quot; alt=&quot;alt text&quot;&gt;
这就是DDPG算法的流程&lt;/p&gt;</content:encoded><img src="/_astro/11.BzPsWl-a.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.BzPsWl-a.png" length="0" type="image/png"/></item><item><title>CS285 Lecture7 Value Function Methods</title><link>https://laurie-hxf.xyz/blog/cs285-l7-value-function-methods</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l7-value-function-methods</guid><description>CS285 Lecture7 Value Function Methods</description><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.BP-vTiij.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;我们是否可以完全抛弃策略梯度（直接优化策略参数的方法），转而通过单纯最大化价值函数来寻找最优策略？
&lt;img src=&quot;./Screenshot%202025-12-07%20at%2014.42.59.png&quot; alt=&quot;alt text&quot;&gt;
思路就是&lt;/p&gt;
&lt;p&gt;既然$A^\pi(\mathbf{s}_t, \mathbf{a}_t)$的意思是：在状态 $\mathbf{s}_t$ 下，采取动作 $\mathbf{a}&lt;em&gt;t$ 比当前策略 $\pi$ 的平均表现要好多少，那根据$\arg \max&lt;/em&gt;{\mathbf{a}_t} A^\pi(\mathbf{s}_t, \mathbf{a}_t)$那我们直接选那个优势最大（即最好的）动作&lt;/p&gt;
&lt;p&gt;那么我们的新策略 $\pi&apos;(\mathbf{a}_t|\mathbf{s}_t)$ 可以定义为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果动作 $\mathbf{a}_t$ 是优势最大的那个（argmax），则概率设为 1。&lt;/li&gt;
&lt;li&gt;其他动作概率设为 0。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Dynamic Programming&lt;/h2&gt;
&lt;p&gt;那么现在的问题就是怎么评估$A^\pi(\mathbf{s}_t, \mathbf{a}_t)$，评估这个函数其实就是评估V，因为
$$
A^\pi(\mathbf{s}_t, \mathbf{a}_t) \approx r(\mathbf{s}_t, \mathbf{a}&lt;em&gt;t) + V^\pi(\mathbf{s}&lt;/em&gt;{t+1}) - V^\pi(\mathbf{s}_t)
$$
在特定条件下，如何具体实现“通过价值来寻找最优策略”。这个特定方法被称为 动态规划 (Dynamic Programming, DP)。
&lt;img src=&quot;./Screenshot%202025-12-07%20at%2015.03.23.png&quot; alt=&quot;alt text&quot;&gt;
假设我们知道&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$p(\mathbf{s}&apos;|\mathbf{s}, \mathbf{a})$：这意味着我们要完全知道环境的模型（Model-Based）。也就是说，我们知道“如果在状态 A 做动作 B，我有多少概率会跳到状态 C”。这就像玩游戏时你手里拿着一本详细的攻略书，知道每一步的所有后果。&lt;/li&gt;
&lt;li&gt;状态 $\mathbf{s}$ 和动作 $\mathbf{a}$ 是离散且少量的 (Discrete and small)：这意味着不需要神经网络，我们可以用简单的“表格”来存储数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;表格型强化学习 (Tabular RL)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;左侧的 $4 \times 4$ 网格&lt;/strong&gt;：这是一个典型的“网格世界”示例（16个状态）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;store full $V^\pi(\mathbf{s})$ in a table&quot;&lt;/strong&gt;：因为状态很少（比如只有16个），我们可以直接画一张表，把每个格子的价值 $V$ 填进去。不需要复杂的近似计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\mathcal{T}$ is $16 \times 16 \times 4$ tensor&lt;/strong&gt;：这是&lt;strong&gt;状态转移矩阵&lt;/strong&gt;。意思是 16个当前状态 $\times$ 16个下一个状态 $\times$ 4个动作。这就是那个“已知的环境模型”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更新形式就是
$$
V^\pi(\mathbf{s}) \leftarrow E [r + \gamma E( V^\pi(\mathbf{s}&apos;))]
$$
由于策略是固定的，哪个价值最大我就选哪个，于是我们可以把前面的期望给省去，变成
$$
V^\pi(\mathbf{s}) \leftarrow r + \gamma E( V^\pi(\mathbf{s}&apos;))
$$
于是我们的流程就变成
&lt;img src=&quot;./Screenshot%202025-12-07%20at%2015.07.59.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;价值迭代算法 (The Value Iteration Algorithm)&lt;/h3&gt;
&lt;p&gt;我们还可以进一步简化
&lt;img src=&quot;./Screenshot%202025-12-07%20at%2015.09.05.png&quot; alt=&quot;alt text&quot;&gt;
既然我们想要的是$\arg \max_{\mathbf{a}&lt;em&gt;t} A^\pi(\mathbf{s}&lt;em&gt;t, \mathbf{a}&lt;em&gt;t)$，这个其实就等价于$\arg \max&lt;/em&gt;{\mathbf{a}} Q^\pi(\mathbf{s}, \mathbf{a})$
$$
\arg \max&lt;/em&gt;{\mathbf{a}} A^\pi(\mathbf{s}, \mathbf{a}) = \arg \max&lt;/em&gt;{\mathbf{a}} Q^\pi(\mathbf{s}, \mathbf{a})
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;算 Q 值： 针对每一个动作，算它的即时奖励加上未来的折现价值。
$$
Q(\mathbf{s}, \mathbf{a}) \leftarrow r(\mathbf{s}, \mathbf{a}) + \gamma E[V(\mathbf{s}&apos;)]
$$&lt;/li&gt;
&lt;li&gt;更新 V 值（贪婪更新）： 既然算出了各个动作的 $Q$，那我肯定选最大的那个作为这个状态的价值 $V$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
V(\mathbf{s}) \leftarrow \max_{\mathbf{a}} Q(\mathbf{s}, \mathbf{a})
$$
这个过程不断循环，直到 $V$ 值不再变化，我们就得到了最优价值函数 $V^*$，同时也自然拥有了最优策略（每次都选 $Q$ 最大的那个动作）。&lt;/p&gt;
&lt;h2&gt;Fitted Value Iteration&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-07%20at%2015.37.03.png&quot; alt=&quot;alt text&quot;&gt;
我们肯定不可能维护一个Value table做出我们的决策因为你的state远远不可能用table表示完，于是乎我们就可以像之前的方法一样，用一个神经网络来预测Value，就像图中底下的部分。&lt;/p&gt;
&lt;p&gt;但是这个算法就有一个问题，他需要我们知道转移函数$p(\mathbf{s}&apos;|\mathbf{s}, \mathbf{a})$ 因为第一步的期望哪里，我们从
$$
E_{s&apos;\sim p(s&apos;|s,\pi(s))}[V(\mathbf{s}&apos;)]
$$
这里的分布中计算期望
$$
y_i \leftarrow \max_{\mathbf{a}_i} (r(\mathbf{s}_i, \mathbf{a}&lt;em&gt;i) + \gamma E[V&lt;/em&gt;\phi(\mathbf{s}&apos;_i)])
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;max_a&lt;/code&gt;：为了算出一个状态的目标价值 $y_i$，你需要计算所有可能动作 $\mathbf{a}_i$ 中哪个最好（Max）。&lt;/li&gt;
&lt;li&gt;因为你不知道环境模型，你在状态 $s_i$ 只能尝试做一个动作，看到一个结果。你无法凭空知道“如果我刚才做了别的动作，结果会是什么”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Fitted Q-Iteration&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-07%20at%2016.39.16.png&quot; alt=&quot;alt text&quot;&gt;
那我们的做法就是
$$
y_i \leftarrow r(\mathbf{s}&lt;em&gt;i, \mathbf{a}&lt;em&gt;i) + \gamma \max&lt;/em&gt;{\mathbf{a}&apos;} Q&lt;/em&gt;\phi(\mathbf{s}&apos;_i, \mathbf{a}&apos;_i)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它用 $\max_{\mathbf{a}&apos;} Q_\phi(\mathbf{s}&apos;_i, \mathbf{a}&apos;_i)$ 来近似 $E[V(\mathbf{s}&apos;_i)]$。&lt;/li&gt;
&lt;li&gt;不需要环境模型
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一步（现在）：&lt;/strong&gt; $r(\mathbf{s}_i, \mathbf{a}_i)$ 是你采样（sample）得到的真实数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二步（未来）：&lt;/strong&gt; $\max_{\mathbf{a}&apos;} Q_\phi(\mathbf{s}&apos;_i, \mathbf{a}&apos;_i)$ 是你自己对着网络（或者表格）算的。你不需要去环境里真的执行 $\mathbf{a}&apos;$，你只需要问你的神经网络：“如果我在下一步状态 $\mathbf{s}&apos;$，哪个动作分最高？”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Doesn&apos;t require simulation of actions!&lt;/strong&gt; 这行小字就是说：我们在计算未来价值时，完全是在脑子里（神经网络里）算 Max，不需要去现实世界试错。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;他最终的训练流程就是这样
&lt;img src=&quot;./Screenshot%202025-12-07%20at%2017.09.05.png&quot; alt=&quot;alt text&quot;&gt;
这个算法是Off-Policy的，因为$(s_i, a_i)$ 意味着如果你在状态 $s$ 做了动作 $a$，环境把你送到 $s&apos;$ 并给你奖励 $r$。这完全是由&lt;strong&gt;环境的物理规则&lt;/strong&gt;决定的（比如万有引力、游戏引擎代码），跟你是谁、你的策略 $\pi$ 是什么没有任何关系。因此，这条数据 $(s, a, s&apos;, r)$ 是一个客观事实。&lt;/p&gt;
&lt;p&gt;同时对于$\max_{\mathbf{a}&apos;&lt;em&gt;i} Q&lt;/em&gt;\phi(\mathbf{s}&apos;_i, \mathbf{a}&apos;_i)$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们在计算目标值 $y_i$ 时，使用的是 &lt;strong&gt;$\max$&lt;/strong&gt; 操作。&lt;/li&gt;
&lt;li&gt;这意味着：虽然在历史数据里，收集数据的人在下一步 $s&apos;$ 可能做了一个很蠢的动作（导致死掉了），但我不管他做了什么。&lt;/li&gt;
&lt;li&gt;我在计算价值时，假设我自己在下一步会做那个分值最高（max） 的动作。&lt;/li&gt;
&lt;li&gt;结论：我利用了你的经历（$s, a, r, s&apos;$），但我在评估未来时，抛弃了你的选择，假设了完美的未来。这就是“异策略”的本质——用别人的过去，规划自己的最优未来。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;online Q-Iteration algorithm&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-07%20at%2017.34.54.png&quot; alt=&quot;alt text&quot;&gt;
我们不必非要等到收集了一大堆数据才开始训练，我们可以一收集到一条数据，就实时更新我们的神经网络。转变成online的learning&lt;/p&gt;
&lt;p&gt;然后对于第一步，那时候我们的Q还没有训练的很好，可能还只是随机化的状态，如果此时根据贪婪策略的话，有可能会陷入局部最优&lt;/p&gt;
&lt;p&gt;那么可以采取以下的方法&lt;/p&gt;
&lt;h4&gt;Epsilon-Greedy ($\epsilon$-greedy)&lt;/h4&gt;
&lt;p&gt;这是最常用、最简单的方法。
$$
\pi(\mathbf{a}_t|\mathbf{s}_t) = \begin{cases} 1 - \epsilon &amp;#x26; \text{if } \mathbf{a}_t = \arg \max Q \quad \text{(利用)} \ \epsilon / (|A| - 1) &amp;#x26; \text{otherwise} \quad \text{(探索)} \end{cases}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机制：&lt;/strong&gt; 扔一个骰子。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;90% 的情况（$1-\epsilon$）：&lt;/strong&gt; 选当前认为最好的动作（Exploitation）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;10% 的情况（$\epsilon$）：&lt;/strong&gt; 闭着眼睛随机选一个动作（Exploration）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;好处：&lt;/strong&gt; 保证了每一个动作都有概率被选到。甚至可以让$\epsilon$在一开始的时候比较大然后随着迭代不断的减小&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Boltzmann Exploration&lt;/h4&gt;
&lt;p&gt;$$
\pi(\mathbf{a}_t|\mathbf{s}&lt;em&gt;t) \propto \exp(Q&lt;/em&gt;\phi(\mathbf{s}_t, \mathbf{a}_t))
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机制：&lt;/strong&gt; 根据 Q 值的大小来分配概率。
&lt;ul&gt;
&lt;li&gt;Q 值越大，被选中的概率越大。&lt;/li&gt;
&lt;li&gt;Q 值越小，被选中的概率越小（但不是 0）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区别：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;$\epsilon$-greedy 是“要么最好，要么瞎选”。&lt;/li&gt;
&lt;li&gt;Boltzmann 是“分高的常选，分低的少选，特别差的极少选”。它比完全随机要稍微“聪明”一点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Value Functions in Theory&lt;/h2&gt;
&lt;p&gt;这里想讨论的是上面的这些方法其实都不收敛，下面就是证明
&lt;img src=&quot;./Screenshot%202025-12-07%20at%2019.51.21.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义算子：
$$
\mathcal{B}V = \max_a r_a + \gamma \mathcal{T}_a V
$$
这里的 $\mathcal{B}$ 就是把上面的第1步和第2步合并成了一个数学操作。它把当前的价值函数向量 $V$ 映射成一个新的向量。&lt;/p&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;strong&gt;$r_a$ (箭头指出)&lt;/strong&gt;：是一个堆叠的&lt;strong&gt;向量&lt;/strong&gt;，包含了在动作 $a$ 下所有状态的奖励。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\mathcal{T}_a$ (箭头指出)&lt;/strong&gt;：是动作 $a$ 的&lt;strong&gt;状态转移矩阵&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;$\mathcal{T}_{a, i, j} = p(s&apos;=i | s=j, a)$：意思是在状态 $j$ 采取动作 $a$，跳到状态 $i$ 的概率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这一行公式其实就是用矩阵乘法简洁地表达了 &quot;即时奖励 + 转移概率 $\times$ 下一时刻价值&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$V^&lt;em&gt;$ 是 $\mathcal{B}$ 的不动点 (Fixed Point)：
$$
V^&lt;/em&gt; = \mathcal{B}V^*
$$
意思是：当你达到最优价值函数 $V^*$ 时，再对它应用一次算子 $\mathcal{B}$（也就是再做一次迭代），它的值不会再改变了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;公式展开：
$V^&lt;em&gt;(s) = \max_a r(s, a) + \gamma E[V^&lt;/em&gt;(s&apos;)]$
这就是著名的 贝尔曼最优方程 (Bellman Optimality Equation)。
PPT 强调了 $V^*$ 的三个关键属性：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Always exists&lt;/strong&gt;：最优价值函数一定存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is always unique&lt;/strong&gt;：它是唯一的（不管你初始值怎么设，只要收敛，终点都是这一个）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Always corresponds to the optimal policy&lt;/strong&gt;：一旦你求出了 $V^*$，你也就知道了最优策略（Optimal Policy），即每个状态下该怎么做。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;虽然理论上 $V^*$ 存在且唯一，但通过不断迭代（Value Iteration）是否一定能算出来？&lt;/p&gt;
&lt;p&gt;可以。因为贝尔曼算子 $\mathcal{B}$ 是一个 &lt;strong&gt;压缩映射 (Contraction Mapping)&lt;/strong&gt;。这意味着每做一次迭代，我们离 $V^&lt;em&gt;$ 的距离就会缩小一点（按 $\gamma$ 的比例缩小），所以只要迭代次数足够多，最终一定会收敛到 $V^&lt;/em&gt;$
&lt;img src=&quot;./Screenshot%202025-12-07%20at%2019.54.48.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;无穷范数&lt;/h3&gt;
&lt;p&gt;无穷范数（∞ 范数、最大范数、sup 范数）就是“看这个对象里绝对值最大的那一项”的范数。
对一个向量 $x=(x1,…,x_n)$，无穷范数定义为
$∥x∥&lt;em&gt;∞=\max&lt;/em&gt;{⁡1≤i≤n}∣xi∣$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;也就是说：把所有分量的绝对值算出来，取最大的那个数。&lt;/li&gt;
&lt;li&gt;例如 x=(2,−5,3)，则 $∥x∥_∞=5$。​&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;压缩映射&lt;/h3&gt;
&lt;p&gt;想象你手里有两张不同的价值表（两个向量），分别叫 $V$ 和 $U$。
我们定义它们之间的“距离”为它们在所有状态中差别最大的那个值的差（无穷范数）：
$$
||V - U||&lt;em&gt;\infty = \max_s |V(s) - U(s)|
$$
如果算子 $\mathcal{B}$ 是压缩的，意味着：
应用算子之后，$\mathcal{B}V$ 和 $\mathcal{B}U$ 之间的距离，一定要比原来的 $V$ 和 $U$ 之间的距离更小。
数学表达为：
$$
||\mathcal{B}V - \mathcal{B}U||&lt;/em&gt;\infty \le \gamma ||V - U||_\infty
$$
其中 $\gamma$ 是折扣因子，且 $0 \le \gamma &amp;#x3C; 1$。因为 $\gamma &amp;#x3C; 1$，所以距离被“压缩”了。&lt;/p&gt;
&lt;p&gt;前面讨论的是Value Iteration中的$\mathcal{B}$是压缩映射，现在迁移到fitted Value Iteration
&lt;img src=&quot;./Screenshot%202025-12-07%20at%2019.59.04.png&quot; alt=&quot;alt text&quot;&gt;
这是个监督学习 (Supervised Learning) 的过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第1步：生成目标 (Label Generation)
$$
y_i \leftarrow \max_{a_i} (r(s_i, a_i) + \gamma E[V_\phi(s&apos;_i)])
$$&lt;/li&gt;
&lt;li&gt;这一步计算出的 $y_i$ 其实就是 $\mathcal{B}V$ 的具体数值。&lt;/li&gt;
&lt;li&gt;这就是我们在训练神经网络时的 &lt;strong&gt;&quot;标签&quot; (Target/Label)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;第2步：最小化误差 (Regression)
$$
\phi \leftarrow \arg \min_\phi \frac{1}{2} \sum_i ||V_\phi(s_i) - y_i||^2
$$&lt;/li&gt;
&lt;li&gt;这是一个典型的 &lt;strong&gt;回归 (Regression)&lt;/strong&gt; 任务。&lt;/li&gt;
&lt;li&gt;它在调整神经网络的参数 $\phi$，让网络预测的输出 $V_\phi(s_i)$ 尽可能接近第1步算出来的目标 $y_i$。这就是数学上定义的“投影”操作（$\Pi$），使用的是 L2 范数（最小二乘法）。
左下角的图非常直观地解释了这个过程：&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;蓝线 ($\Omega$)&lt;/strong&gt;：代表你的神经网络能表达的所有可能的函数的集合（假设空间）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点 $V$&lt;/strong&gt;：当前的价值函数（在蓝线上）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点 $\mathcal{B}V$&lt;/strong&gt;：应用贝尔曼公式计算出的理想目标值。注意，它&lt;strong&gt;不在&lt;/strong&gt;蓝线上，说明神经网络无法完美拟合这个理想值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点 $V&apos;$&lt;/strong&gt;：这是 $\Pi \mathcal{B}V$。它是蓝线上离 $\mathcal{B}V$ 最近的点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\Pi$ (Projection)&lt;/strong&gt;：就是把理想的、复杂的 $\mathcal{B}V$ 强行&lt;strong&gt;拉回&lt;/strong&gt;（投影）到神经网络能表示的范围内，找一个误差最小的替代品。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-07%20at%2020.10.27.png&quot; alt=&quot;alt text&quot;&gt;
然而$\mathcal{B}$是压缩映射，$\Pi$也是压缩映射，因为两个点被投影到一条线上距离一定会小于等于原本的距离。&lt;/p&gt;
&lt;p&gt;但是$\Pi\mathcal{B}$并不一定是压缩映射，且大多情况都不是压缩映射，可以看左下角的图就是例子。&lt;/p&gt;
&lt;p&gt;所以这也证明了为什么fitted Value Iteration并不收敛。我们还可以用同样的思路证明Q-learning也并不收敛。&lt;/p&gt;</content:encoded><img src="/_astro/11.BP-vTiij.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.BP-vTiij.png" length="0" type="image/png"/></item><item><title>CS285 Lecture6 Actor Critic Algorithms</title><link>https://laurie-hxf.xyz/blog/cs285-l6-actor-critic-algorithms</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l6-actor-critic-algorithms</guid><description>CS285 Lecture6 Actor Critic Algorithms</description><pubDate>Sat, 06 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.kXxnmrE7.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;原本的基础策略梯度估计方法是
$$
\nabla_\theta J(\theta) \approx \frac{1}{N} \sum_{i=1}^N \sum_{t=1}^T \nabla_\theta \log \pi_\theta(\mathbf{a}&lt;em&gt;{i,t}|\mathbf{s}&lt;/em&gt;{i,t}) \left( \sum_{t&apos;=1}^T r(\mathbf{s}&lt;em&gt;{i,t&apos;}, \mathbf{a}&lt;/em&gt;{i,t&apos;}) \right)
$$
然后我们把它改进一下，引入 Q 函数
为了数学上的严谨性，PPT 引入了 $Q$ 函数的概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$\hat{Q}_{i,t}$&lt;/strong&gt;：这是对在状态 $\mathbf{s}&lt;em&gt;{i,t}$ 采取动作 $\mathbf{a}&lt;/em&gt;{i,t}$ 后，未来预期能拿到的奖励的&lt;strong&gt;估计值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;$Q(\mathbf{s}&lt;em&gt;t, \mathbf{a}&lt;em&gt;t)$：这是真实的期望剩余回报（True expected reward-to-go）。公式为：
$$
Q(\mathbf{s}&lt;em&gt;t, \mathbf{a}&lt;em&gt;t) = \sum&lt;/em&gt;{t&apos;=t}^T E&lt;/em&gt;{\pi&lt;/em&gt;\theta} [r(\mathbf{s}&lt;/em&gt;{t&apos;}, \mathbf{a}_{t&apos;})|\mathbf{s}_t, \mathbf{a}_t]
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意这里的t‘已经是从t开始计算了，根据Causality。然后意思就是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$|\mathbf{s}_t, \mathbf{a}_t$&lt;/strong&gt;：这是一个条件概率的写法。意思是“在这个前提下”——前提就是我们现在的脚正站在 $\mathbf{s}_t$，且现在的手正做动作 $\mathbf{a}_t$。这一步是确定的，已经发生了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$E_{\pi_\theta}$&lt;/strong&gt;：意思是“对于这一步之后的所有步骤，我们要按照策略 $\pi_\theta$ 产生的概率分布来计算平均值”。
&lt;img src=&quot;./Screenshot%202025-12-05%20at%2019.48.51.png&quot; alt=&quot;alt text&quot;&gt;
改进后的公式就是这样
$$
\nabla_\theta J(\theta) \approx \frac{1}{N} \sum_{i=1}^N \sum_{t=1}^T \nabla_\theta \log \pi_\theta(\mathbf{a}&lt;em&gt;{i,t}|\mathbf{s}&lt;/em&gt;{i,t}) Q(\mathbf{s}&lt;em&gt;{i,t}, \mathbf{a}&lt;/em&gt;{i,t})
$$
Q function的意思就是当前我在状态$s_t$的情况下，采取动作$a_t$之后的奖励是多少
V function的意思就是当前我在状态$s_t$的情况下,不管采取什么动作之后的平均奖励是多少
$$
V^\pi(\mathbf{s}&lt;em&gt;t) = E&lt;/em&gt;{\mathbf{a}&lt;em&gt;t \sim \pi}[Q^\pi(\mathbf{s}&lt;em&gt;t, \mathbf{a}&lt;em&gt;t)]
$$
然后我们就可以参考之前的baseline的思想改进我们的公式
$$
\nabla&lt;/em&gt;\theta J(\theta) \approx \frac{1}{N} \sum&lt;/em&gt;{i=1}^N \sum&lt;/em&gt;{t=1}^T \nabla_\theta \log \pi_\theta(\mathbf{a}&lt;em&gt;{i,t}|\mathbf{s}&lt;/em&gt;{i,t}) (Q(\mathbf{s}&lt;em&gt;{i,t}, \mathbf{a}&lt;/em&gt;{i,t})-V(s_i,t))
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;意义就是比较采取动作$a_t$之后的奖励和平均奖励的大小来优化模型，同时也减小方差&lt;/p&gt;
&lt;p&gt;然后我们就可以定义$A^\pi(\mathbf{s}_t, \mathbf{a}_t)$（优势函数，Advantage Function）**：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公式：$A^\pi(\mathbf{s}_t, \mathbf{a}_t) = Q^\pi(\mathbf{s}_t, \mathbf{a}_t) - V^\pi(\mathbf{s}_t)$&lt;/li&gt;
&lt;li&gt;含义：这步动作 $a$ 比平均水平好多少？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是目前我们并不知道$A^\pi(\mathbf{s}_t, \mathbf{a}_t)$是多少，我们想要拟合他，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们最终想求的是 $A$（优势），因为把它代入梯度公式（PPT左中位置）效果最好。&lt;/li&gt;
&lt;li&gt;但是 $Q$ 和 $V$ 都是未知的。&lt;/li&gt;
&lt;li&gt;利用 贝尔曼方程（Bellman Equation） 的思想，我们可以把 $Q$ 写成：
$$
Q(\mathbf{s}&lt;em&gt;t, \mathbf{a}&lt;em&gt;t) = r(s_t,a_t)+\sum&lt;/em&gt;{t&apos;=t+1}^T E&lt;/em&gt;{\pi_\theta} [r(\mathbf{s}&lt;em&gt;{t&apos;}, \mathbf{a}&lt;/em&gt;{t&apos;})|\mathbf{s}_t, \mathbf{a}_t]
$$
$$
Q^\pi(\mathbf{s}_t, \mathbf{a}_t) \approx r(\mathbf{s}_t, \mathbf{a}&lt;em&gt;t) + V^\pi(\mathbf{s}&lt;/em&gt;{t+1})
$$
(当前的 Q 值 $\approx$ 拿到手的奖励 + 下一步局面的价值)&lt;/li&gt;
&lt;li&gt;把这个代入 $A$ 的公式：
$$
A^\pi(\mathbf{s}_t, \mathbf{a}_t) \approx r(\mathbf{s}_t, \mathbf{a}&lt;em&gt;t) + V^\pi(\mathbf{s}&lt;/em&gt;{t+1}) - V^\pi(\mathbf{s}_t)
$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;：只要我们能预测 &lt;strong&gt;$V^\pi(\mathbf{s})$&lt;/strong&gt;，我们就可以算出 $Q$，进而算出 $A$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么问题来了：我们到底该如何计算或估计这个 $V^\pi(\mathbf{s})$ 呢？
方法：蒙特卡洛评估 (Monte Carlo Policy Evaluation)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：既然我们要算“期望”（平均值），那就&lt;strong&gt;多试几次，然后取平均&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;公式 1（单次采样）：
$$
V^\pi(\mathbf{s}&lt;em&gt;t) \approx \sum&lt;/em&gt;{t&apos;=t}^T r(\mathbf{s}&lt;em&gt;{t&apos;}, \mathbf{a}&lt;/em&gt;{t&apos;})
$$
这就是说：我不知道平均分是多少，但我刚刚玩了一把，拿了 50 分，那我就暂时认为这局面的价值是 50 分。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;公式 2（多次采样取平均）：
$$
V^\pi(\mathbf{s}&lt;em&gt;t) \approx \frac{1}{N} \sum&lt;/em&gt;{i=1}^N \sum_{t&apos;=t}^T r(\mathbf{s}&lt;em&gt;{t&apos;}, \mathbf{a}&lt;/em&gt;{t&apos;})
$$
这就是说：为了更准一点，我从 $\mathbf{s}_t$ 开始玩 $N$ 把，把这 $N$ 把的分数加起来除以 $N$。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是如果你想用上面的公式 2 准确估计某一个特定状态 $\mathbf{s}_t$ 的价值，你必须能够让时间倒流，回到 $\mathbf{s}_t$，重新玩一次，再回到 $\mathbf{s}_t$，再玩一次……&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在现实世界中这是不可能的（你不能让机器人摔倒后，时光倒流回摔倒前的那一刻重新尝试）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在模拟器中是可以的（比如游戏存盘点），但效率很低。
&lt;img src=&quot;./Screenshot%202025-12-05%20at%2020.02.39.png&quot; alt=&quot;alt text&quot;&gt;
然后我们训练一个神经网络来逼近这个函数，
要准确评估一个状态 $V(\mathbf{s})$，最好是从这个状态出发玩 $N$ 次取平均。但这太慢且不现实。
这张PPT提出了解决方案：&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;：我们收集很多次游戏的数据，然后训练一个神经网络 $\hat{V}_\phi^\pi$ 去拟合这些数据。即使每个数据点只有一次采样的结果（有噪声），神经网络也能学出整体的趋势。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;输入（Input）&lt;/strong&gt;：状态 $\mathbf{s}_{i,t}$。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;标签（Label / Target）&lt;/strong&gt;：$y_{i,t} = \sum_{t&apos;=t}^T r(\mathbf{s}&lt;em&gt;{i,t&apos;}, \mathbf{a}&lt;/em&gt;{i,t&apos;})$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里的 $y_{i,t}$ 就是我们在那一次实际游戏中，从 $t$ 时刻开始一直到结束真正拿到的总分。&lt;/li&gt;
&lt;li&gt;虽然这个 $y_{i,t}$ 只是“一次”的结果（Single Sample），并不是真正的期望值（Expectation），但PPT承认它 &lt;strong&gt;&quot;still pretty good&quot;（仍然很好用）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;既然有了输入和标签，这就变成了一个标准的监督回归问题（Supervised Regression）。
PPT 下方的公式展示了训练目标：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}(\phi) = \frac{1}{2} \sum_i | \hat{V}_\phi^\pi(\mathbf{s}_i) - y_i |^2
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\hat{V}_\phi^\pi(\mathbf{s}_i)$：神经网络当前的预测值（它认为这个状态值多少分）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$y_i$&lt;/strong&gt;：实际拿到的分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：调整网络参数 $\phi$，让预测值尽可能接近实际拿到的分（最小化均方误差）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;右下角的图非常形象地解释了为什么这样做行得通：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图中展示了多条黑色的轨迹（Samples）。&lt;/li&gt;
&lt;li&gt;红圈圈出的部分显示，不同的轨迹可能会经过相似的状态区域。&lt;/li&gt;
&lt;li&gt;这意味着，虽然我们在某一条轨迹上只看到了状态 $s$ 一次，但神经网络会通过学习成千上万条轨迹，把附近的点都“平滑”起来，从而学到一个比较准确的价值估计。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bootstrapped&lt;/h2&gt;
&lt;p&gt;我们还可以进一步改进&lt;img src=&quot;./Screenshot%202025-12-05%20at%2020.03.40.png&quot; alt=&quot;alt text&quot;&gt;
为了训练 Critic，我们要把游戏玩到底，把最后拿到的总分加起来作为标签（Label）。而这张PPT说：我们不需要等到游戏结束，可以利用 Critic 自己对下一步的预测来训练自己。
$$
y_{i,t} \approx r(\mathbf{s}&lt;em&gt;{i,t}, \mathbf{a}&lt;/em&gt;{i,t}) + \hat{V}&lt;em&gt;{\phi}^\pi(\mathbf{s}&lt;/em&gt;{i,t+1})
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：方差显著降低。原本你要累加未来100步的随机奖励，任何一步的波动都会影响总分。现在你只看当前这一步的奖励，剩下的用一个稳定的估值代替，训练会变得非常平稳。&lt;/li&gt;
&lt;li&gt;潜在风险：引入了偏差（Bias）。如果你的神经网络 $\hat{V}$ 一开始估得不准，那你算出来的标签也是错的，可能会导致误差传播。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;An actor-critic algorithm&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有些任务是有尽头的（比如右图的机械臂堆积木，堆完就结束，称为 &lt;strong&gt;Episodic tasks&lt;/strong&gt;）。但也有些任务是无限循环的（比如右图那个一直跑的小人，或者走路机器人，理论上可以永远走下去，称为 &lt;strong&gt;Continuous/cyclical tasks&lt;/strong&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果任务无限长，且每一步都有正向奖励（比如只要站着不倒就 +1 分），那么你把未来的分加起来，&lt;strong&gt;$V$ 值就会变成无穷大（Infinitely large）&lt;/strong&gt;。这就导致神经网络没法训练了（梯度爆炸）。
&lt;img src=&quot;./Screenshot%202025-12-05%20at%2023.59.12.png&quot; alt=&quot;alt text&quot;&gt;
于是乎我们可以给奖励乘一个折扣因子$\gamma$ ，折扣因子 $\gamma$（比如 0.99）是每过一步乘一次。离现在越远，打折越狠。
$$
V = r_0 + \gamma r_1 + \gamma^2 r_2 + \gamma^3 r_3 + \dots
$$
举个例子：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：一条无限长的跑道，分成了格子：格1 $\to$ 格2 $\to$ 格3 ...&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;规则&lt;/strong&gt;：每向前走一步，得 &lt;strong&gt;1分&lt;/strong&gt;奖励（$r=1$）。&lt;/p&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;strong&gt;$\gamma = 0.9$&lt;/strong&gt;（意味着未来的 1 分只值现在的 0.9 分）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Critic 神经网络（当前状态）&lt;/strong&gt;：刚开始训练，还有点笨，它对每个格子的价值估值得乱七八糟。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设 Critic 目前的心理估值（预测值 $\hat{V}$）如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它觉得在 &lt;strong&gt;格1&lt;/strong&gt; 的价值是 &lt;strong&gt;5.0 分&lt;/strong&gt; ($\hat{V}(s_1) = 5.0$)&lt;/li&gt;
&lt;li&gt;它觉得在 &lt;strong&gt;格2&lt;/strong&gt; 的价值是 &lt;strong&gt;4.0 分&lt;/strong&gt; ($\hat{V}(s_2) = 4.0$)
机器人站在 &lt;strong&gt;格1 ($s_1$)&lt;/strong&gt;，决定向前走一步 ($a_1$)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动作&lt;/strong&gt;：走到 &lt;strong&gt;格2 ($s_2$)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奖励&lt;/strong&gt;：拿到 &lt;strong&gt;1 分 ($r_1=1$)&lt;/strong&gt;。
现在我们手里有了一条数据：&lt;strong&gt;${s_1, a_1, r=1, s_2}$&lt;/strong&gt;。
Critic 要利用这条数据来检查自己刚才猜得准不准。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Critic 原本的预测&lt;/strong&gt;：我觉得站在 $s_1$ 能拿 &lt;strong&gt;5.0&lt;/strong&gt; 分。&lt;/li&gt;
&lt;li&gt;计算 TD Target：
实际上，我拿到了 1 分 现钞，而且我到了 $s_2$。
根据我（Critic）对 $s_2$ 的估值，未来还能拿 4.0 分，但这 4.0 分要打折。
$$
\begin{aligned} \text{目标值 (Target)} &amp;#x26;= \text{当前奖励} + \gamma \times \text{下一步的估值} \ y &amp;#x26;= r_1 + 0.9 \times \hat{V}(s_2) \ y &amp;#x26;= 1 + 0.9 \times 4.0 \ y &amp;#x26;= 1 + 3.6 \ y &amp;#x26;= \mathbf{4.6} \end{aligned}
$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Critic 原本以为 $s_1$ 值 &lt;strong&gt;5.0&lt;/strong&gt; 分。&lt;/li&gt;
&lt;li&gt;实际走了一步后发现，基于目前的认知，$s_1$ 其实只值 &lt;strong&gt;4.6&lt;/strong&gt; 分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;误差 (TD Error)&lt;/strong&gt;：$4.6 - 5.0 = -0.4$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练 Critic&lt;/strong&gt;：告诉神经网络，“你下次看到 $s_1$，别猜 5.0 了，往下调一点，猜 4.6 吧。”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在来看看蒙特卡洛的情况
&lt;img src=&quot;./Screenshot%202025-12-06%20at%2000.08.04.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以得到两个公式option 1和option 2
这两个公式在数学形式上非常相似，但在**“物理意义”和“训练效果”上有着本质的区别。
简单来说，差别在于：&lt;strong&gt;你是否认为“游戏后期的动作”比“游戏开头的动作”更不重要？&lt;/strong&gt;
我们来详细拆解一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Option 2：
$$
\dots \sum_{t=1}^T \mathbf{\gamma^{t-1}} \nabla_\theta \log \pi (\dots)
$$
注意这里有一个 $\gamma^{t-1}$（PPT 里用红圈或者箭头特别指出的部分）。这代表第 $t$ 步的梯度，要乘上一个随时间指数级衰减的系数。&lt;/li&gt;
&lt;li&gt;Option 1：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\dots \sum_{t=1}^T \nabla_\theta \log \pi (\dots)
$$&lt;/p&gt;
&lt;p&gt;这里没有 $\gamma^{t-1}$。这意味着第 1 步的梯度和第 1000 步的梯度，权重是一样的（都是 1）。&lt;/p&gt;
&lt;p&gt;如果你严格按照“最大化初始状态的期望回报”这个数学目标去求导，你会得到 Option 2。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;含义&lt;/strong&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;：假设 $\gamma = 0.99$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 步的权重是 $1$。&lt;/li&gt;
&lt;li&gt;第 100 步的权重是 $0.99^{99} \approx 0.37$。&lt;/li&gt;
&lt;li&gt;第 1000 步的权重是 $0.99^{999} \approx 0.00004$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;后果&lt;/strong&gt;：模型会&lt;strong&gt;极其重视开局&lt;/strong&gt;，但几乎&lt;strong&gt;完全忽略后期&lt;/strong&gt;。即使你在第 1000 步犯了一个导致“死亡”的低级错误，因为权重只有 0.00004，神经网络也懒得去改它。这会导致模型学不会处理长序列任务的后期阶段。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是option 1我们在代码里实际使用的方法。我们人为地去掉了那个 $\gamma^{t-1}$。&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;：虽然“未来的钱”要打折（计算 Reward-to-go 时依然有 $\gamma$），但是“学习的机会”不应该打折。第 1000 步的状态 $s_{1000}$ 和第 1 步的状态 $s_1$ 都是合法的状态，都需要学习最优策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：模型在整个游戏过程中都能均衡地学习&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以将这个策略运用到online的场景下，我们不收集一个batch来训练，而是每走一步我们就训练一次，实时和环境交互
&lt;img src=&quot;./Screenshot%202025-12-06%20at%2000.16.30.png&quot; alt=&quot;alt text&quot;&gt;
Online RL (在线强化学习)这是最经典的强化学习模式。&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;ol&gt;
&lt;li&gt;智能体根据当前的策略 $\pi$ 采取动作 $a$。&lt;/li&gt;
&lt;li&gt;环境反馈新的状态 $s&apos;$ 和奖励 $r$。&lt;/li&gt;
&lt;li&gt;智能体利用这些新产生的数据 $(s, a, r, s&apos;)$ 来更新策略。&lt;/li&gt;
&lt;li&gt;重复上述过程。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据是动态的：&lt;/strong&gt; 随着策略的变好，智能体产生的数据分布也会发生变化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;探索（Exploration）：&lt;/strong&gt; 智能体可以主动去尝试未知的动作，以发现更好的策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点：&lt;/strong&gt; 样本效率通常较低，且在真实物理环境（如昂贵的机器人或自动驾驶）中进行“试错”可能非常危险或昂贵。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Offline RL (离线强化学习)也被称为 &lt;strong&gt;Batch RL&lt;/strong&gt; (虽然这两个术语在学术界有细微差别，但通常通用)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;定义：&lt;/strong&gt; 智能体&lt;strong&gt;完全不与环境交互&lt;/strong&gt;，仅使用一个&lt;strong&gt;固定的、预先收集好的&lt;/strong&gt;静态数据集（Dataset）进行训练。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;核心流程：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给定一个历史数据集 $\mathcal{D}$（可能由人类专家、随机策略或其他旧策略产生）。&lt;/li&gt;
&lt;li&gt;智能体仅在 $\mathcal{D}$ 上进行训练，试图学习出一个最优策略。&lt;/li&gt;
&lt;li&gt;训练结束后，策略才会被部署到环境中进行测试。&lt;/li&gt;
&lt;/ol&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;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; 流程上很像标准的 Supervised Learning，但目标是最大化累积奖励，而不仅仅是模仿数据。&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;主要挑战：&lt;/strong&gt; &lt;strong&gt;分布偏移 (Distributional Shift)&lt;/strong&gt;。当智能体想要尝试一个数据集中没有的动作（OOD, Out-of-Distribution）时，由于无法通过与环境交互来验证这个动作的好坏，智能体可能会产生极其错误的乐观估计（Extrapolation Error），导致策略失效。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是在实际应用中，每次只用一个step来进行训练对于神经网络来说并不是很好，单步更新通常方差很大，且数据相关性强。
&lt;img src=&quot;./Screenshot%202025-12-06%20at%2016.15.07.png&quot; alt=&quot;alt text&quot;&gt;
为了解决上述的稳定性问题，PPT 展示了两种主流的并行 Actor-Critic 架构：&lt;/p&gt;
&lt;p&gt;左图：Synchronized Parallel Actor-Critic (同步并行) -&gt; 对应 &lt;strong&gt;A2C&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作原理：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;你有多个并行的环境（Worker，蓝色的竖条）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Get (s, a, s&apos;, r)&lt;/strong&gt;: 所有 Worker 同时与各自的环境交互，收集数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wait&lt;/strong&gt;: 系统必须等待&lt;strong&gt;所有&lt;/strong&gt; Worker 都完成这一步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Update θ&lt;/strong&gt;: 收集所有 Worker 的数据，取平均或总和，计算出一个总的梯度，然后更新全局参数。&lt;/li&gt;
&lt;li&gt;更新完后，所有 Worker 同步进入下一步。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点：&lt;/strong&gt; 协调一致，利用 GPU 批处理效率高，实现简单（通常比右边的更好用且效果不差）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;右图：Asynchronous Parallel Actor-Critic (异步并行) -&gt; 对应 &lt;strong&gt;A3C&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作原理：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;有一个全局参数服务器（灰色圆柱体 $\theta$）。&lt;/li&gt;
&lt;li&gt;每个 Worker（蓝色竖条）独立运行，互不等待。&lt;/li&gt;
&lt;li&gt;当某个 Worker 收集够一定量的数据（或者完成一步），它就计算自己的梯度，&lt;strong&gt;异步&lt;/strong&gt;地推送到全局服务器更新参数，并拉取最新的参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点：&lt;/strong&gt; 速度极快（因为不需要等待慢的 Worker），不需要 GPU 也可以在多核 CPU 上跑得很好。但由于参数更新是异步的，可能会出现“过时梯度”（Stale Gradients）的问题，导致训练有时不如 A2C 稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;off-policy Actor-Critic&lt;/h2&gt;
&lt;p&gt;我们还可以使用off-policy，这样子就可以避免用完一条数据就丢弃，提高数据的利用率。
&lt;img src=&quot;./Screenshot%202025-12-06%20at%2016.23.00.png&quot; alt=&quot;alt text&quot;&gt;
思想就是每次跑新的策略得到的结果放到buffer里面，然后每次训练就从buffer里面拿一个batch来训练。&lt;/p&gt;
&lt;p&gt;但是如果只是把这种做法嵌套进原有的框架里面会有问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Critic 更新的问题 (Step 3)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;公式含义： 这一步是在更新 Critic（价值函数 $V$），使用的是时序差分（TD）目标。
$$
y_i = r_i + \gamma \hat{V}_{\phi}^{\pi}(s&apos;_i)
$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;问题所在：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义冲突：&lt;/strong&gt; $V^\pi(s)$ 的定义是：从状态 $s$ 出发，&lt;strong&gt;严格按照当前策略 $\pi$&lt;/strong&gt; 行动所能获得的期望回报。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实际情况：&lt;/strong&gt; 这个 $y_i$ 里的 $r_i$ 是怎么来的？它是执行了旧动作 $a_{old}$ 得到的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这个 Target $y_i$ 实际上估计的是 $Q^\pi(s_i, a_{old})$（即：在状态 $s$ 强行执行旧动作 $a_{old}$，之后才遵循 $\pi$ 的价值）。&lt;/li&gt;
&lt;li&gt;如果我们直接用它来更新 $V^\pi(s)$，就会把 $V^\pi(s)$ 拉向 $Q^\pi(s, a_{old})$。&lt;/li&gt;
&lt;li&gt;除非 $a_{old}$ 恰好也是当前策略 $\pi_\theta$ 会选的动作，否则这个更新目标就是错的。这意味着 Critic 学不到当前策略的真实价值，只能学到“过去各种杂乱策略的混合价值”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Actor 更新的问题 (Step 5)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;公式含义： 这一步是在计算策略梯度 (Policy Gradient)。
$$
\nabla_\theta J(\theta) \approx \frac{1}{N} \sum_i \nabla_\theta \log \pi_\theta(a_i|s_i) \hat{A}^\pi(s_i, a_i)
$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;问题所在：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数学假设：&lt;/strong&gt; 策略梯度定理要求我们在期望 $\mathbb{E}$ 中使用的样本 $(s, a)$ 必须是由&lt;strong&gt;当前策略&lt;/strong&gt; $\pi_\theta$ 产生的。也就是说，我们要问的是：“在当前策略下，这个动作好不好？”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实际情况：&lt;/strong&gt; Replay Buffer 里的 $a_i$ 是很久以前的旧策略（可能是 $\pi_{old}$）选择的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果：&lt;/strong&gt; 你在拿着旧策略选择的动作，强行去更新新策略的参数。
&lt;ul&gt;
&lt;li&gt;例如：旧策略在 $s$ 选了 $a$，结果不错（Advantage 是正的）。公式会告诉新策略 $\pi_\theta$ ：“嘿，增加选 $a$ 的概率！”&lt;/li&gt;
&lt;li&gt;但现在的 $\pi_\theta$ 可能已经很聪明了，根本不会选 $a$，或者 $a$ 对于现在的策略来说其实是个坏动作（因为 Critic 也是旧的）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论：&lt;/strong&gt; 样本分布不对，算出来的梯度是&lt;strong&gt;有偏 (Biased)&lt;/strong&gt; 的，甚至完全错误的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了解决第一个问题
&lt;img src=&quot;./Screenshot%202025-12-06%20at%2016.31.15.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新的写法：
$$
y_i = r_i + \gamma \hat{Q}_{\phi}^{\pi}(s&apos;_i, a&apos;_i)
$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这里的 $a&apos;_i$ 从哪来？(核心重点)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;PPT 用箭头特别指出：&lt;strong&gt;&quot;not from replay buffer R!&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Buffer 里虽然存了下一步动作 $a&apos;$，但那是“旧策略”在当时选的，我们不能用。&lt;/li&gt;
&lt;li&gt;Correct Way: 我们要把 $s&apos;_i$ 拿出来，输入给当前最新的 Actor，让它现选一个动作：
$$
a&apos;&lt;em&gt;i \sim \pi&lt;/em&gt;{\theta}(a&apos;_i | s&apos;_i)
$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义：&lt;/strong&gt; 虽然 $s_i$ 和 $a_i$ 是历史数据，但在计算“未来期望”时，我是用&lt;strong&gt;现在的策略&lt;/strong&gt;去推演下一步的。这样算出来的 Target 才是属于当前策略的。然后让 $Q(s, a)$ 去拟合这个 Target。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于第二个问题
&lt;img src=&quot;./Screenshot%202025-12-06%20at%2016.34.40.png&quot; alt=&quot;alt text&quot;&gt;
既然旧动作不能用，那就扔掉它，只保留旧状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;保留状态 ($s_i$)&lt;/strong&gt;：从 Replay Buffer 中拿出当时的“场景” $s_i$。&lt;/li&gt;
&lt;li&gt;重采样动作 ($a_i^\pi$)：让当前的策略 $\pi_\theta$ 面对这个旧场景 $s_i$，重新做一次决策，生成一个新的动作 $a_i^\pi$
$$
a_i^\pi \sim \pi_\theta(a|s_i)
$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算梯度&lt;/strong&gt;：用这个&lt;strong&gt;新生成的动作&lt;/strong&gt; $a_i^\pi$ 和它对应的 Q 值 $\hat{Q}^\pi(s_i, a_i^\pi)$ 来计算梯度。
&lt;ul&gt;
&lt;li&gt;注意：这里不再需要 Importance Sampling（重要性采样），因为动作本来就是从当前分布采出来的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PPT 最下方给出了修正后的最终公式：&lt;/p&gt;
&lt;p&gt;$$
\nabla_\theta J(\theta) \approx \frac{1}{N} \sum_i \nabla_\theta \log \pi_\theta(\mathbf{a}_i^\pi|s_i) \hat{Q}^\pi(s_i, \mathbf{a}_i^\pi)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义：&lt;/strong&gt; 我们希望调整参数 $\theta$，使得策略 $\pi_\theta$ 更倾向于选择那些 &lt;strong&gt;Q 值（由 Critic 预测）高&lt;/strong&gt; 的动作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据流变化：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Old:&lt;/strong&gt; Buffer $(s, a, r, s&apos;) \rightarrow$ Update Actor using $a$.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New:&lt;/strong&gt; Buffer $(s, _, _, _) \rightarrow$ Sample new $a^\pi$ using Actor $\rightarrow$ Evaluate $Q(s, a^\pi)$ $\rightarrow$ Update Actor.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相比于直接使用 Buffer 里的真实回报（Return），这里我们使用了一个“采样出来的动作”加上“Critic 预测的 Q 值”来计算梯度。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Convenient:&lt;/strong&gt; 不需要处理复杂的 Importance Sampling 权重（那些权重容易爆炸）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Higher Variance:&lt;/strong&gt; 因为我们每次算的梯度都依赖于当次随机采样的动作 $a_i^\pi$，这是一次随机估计，所以会有方差。但在大规模数据训练中，我们可以不断的输入状态s然后得到很多的a，这个过程不需要仿真获得也不需要和真实世界交互所以获取很容易，这种方差是可以被接受和平均掉的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题现在就是，V相比Q到底是哪里不行呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$V^\pi(s)$ (状态价值)：
它的定义是：“如果不换算成具体动作，站在状态 $s$，按照策略 $\pi$ 平均能拿多少分。”
&lt;ul&gt;
&lt;li&gt;数学表达：$V^\pi(s) = \sum_{a} \pi(a|s) \cdot Q^\pi(s,a)$&lt;/li&gt;
&lt;li&gt;它把策略 $\pi$ &lt;strong&gt;“内卷”&lt;/strong&gt; 进了价值里。如果你换了策略（比如从 $\pi_{old}$ 换到 $\pi_{new}$），$V$ 的值本身就应该剧烈变化，因为动作分布变了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$Q^\pi(s, a)$ (动作价值)：
它的定义是：“在状态 $s$，强制执行动作 $a$（不管策略喜不喜欢这个动作），然后再按照策略 $\pi$ 继续走，能拿多少分。”
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优势：&lt;/strong&gt; 它把第一步的动作 $a$ &lt;strong&gt;“固定”&lt;/strong&gt; 了。这一步不再依赖策略的概率分布。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Critics as baselines&lt;/h2&gt;
&lt;h3&gt;State-dependent&lt;/h3&gt;
&lt;p&gt;我们还可以把Critics当作baseline
&lt;img src=&quot;./Screenshot%202025-12-06%20at%2016.52.23.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;1. Actor-Critic (最上方公式)&lt;/h4&gt;
&lt;p&gt;$$
\nabla J \approx ... (r + \gamma \hat{V}(s&apos;) - \hat{V}(s))
$$&lt;/p&gt;
&lt;p&gt;这是标准的 Actor-Critic 更新方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;做法：&lt;/strong&gt; 使用 &lt;strong&gt;Bootstrapping（自举）&lt;/strong&gt;。也就是用 Critic 对未来的预测 $V(s&apos;)$ 来代替真实的未来回报。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点 (+ lower variance):&lt;/strong&gt; 方差低。因为你只需要看一步真实的 $r$，剩下的都交给 Critic 预测，随机性小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点 (- not unbiased):&lt;/strong&gt; 有偏差。我们希望优化的目标是真实回报 $G_t$。而在 Actor-Critic 中，我们用 $r + \gamma \hat{V}(s&apos;)$ 来代替 $G_t$。
$$
\mathbb{E}[r + \gamma \hat{V}(s&apos;)] \neq \mathbb{E}[G_t]
$$
除非你的 $\hat{V}$ 完美等于真实价值函数 $V_{true}$（这在训练初期是不可能的），否则这个等式永远不成立。这个差值就是 Bias。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. Policy Gradient (中间公式)&lt;/h4&gt;
&lt;p&gt;$$
\nabla J \approx ... ((\sum r) - b)
$$
这是标准的 Policy Gradient (如 REINFORCE)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;做法：&lt;/strong&gt; 使用 &lt;strong&gt;Monte Carlo（蒙特卡洛）&lt;/strong&gt; 回报。也就是必须等到游戏结束，把后面所有的奖励加起来作为回报。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点 (+ no bias):&lt;/strong&gt; 无偏差。因为使用的是真实发生的累计回报，数学期望上是绝对准确的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点 (- higher variance):&lt;/strong&gt; 方差高。因为一局游戏很长，任何一步的随机因素都会累积，导致最终的 Sum Reward 波动极大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. State-Dependent Baseline (最下方公式)&lt;/h4&gt;
&lt;p&gt;$$
\nabla J \approx ... ((\sum r) - \hat{V}(s))
$$&lt;/p&gt;
&lt;p&gt;这是 PPT 提出的折中方案 —— &lt;strong&gt;带基线的策略梯度 (Policy Gradient with Baseline)&lt;/strong&gt;。它试图回答：“能不能既用 Critic 降方差，又保持无偏差？”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;做法：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Target 部分（被减数）：&lt;/strong&gt; 依然使用真实的蒙特卡洛回报 $\sum r$（保持无偏差）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Baseline 部分（减数）：&lt;/strong&gt; 使用 Critic $\hat{V}(s)$ 作为一个&lt;strong&gt;基线&lt;/strong&gt;减去。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么这样好？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;+ no bias:&lt;/strong&gt; 因为 Critic 只是作为 Baseline 被减去，根据数学推导（Control Variate 理论），减去一个不依赖于动作 $a$ 的项不会改变梯度的期望方向，所以依然是无偏的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;+ lower variance:&lt;/strong&gt; 虽然不如第一种方法方差那么低，但因为 $\hat{V}(s)$ 能够预测回报的大致范围，减去它之后，剩下的差值（Advantage）数值会变小，从而显著降低了梯度的波动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只要 Baseline $b(s)$ 只与状态 $s$ 有关，而与动作 $a$ 无关，它对梯度的期望贡献为 0。&lt;/strong&gt;
证明逻辑如下：
$$
\mathbb{E}&lt;em&gt;{a \sim \pi} [\nabla&lt;/em&gt;\theta \log \pi(a|s) \cdot b(s)]
$$
因为 $b(s)$ 和 $a$ 无关，可以提出来：
$$
= b(s) \cdot \mathbb{E}&lt;em&gt;{a \sim \pi} [\nabla&lt;/em&gt;\theta \log \pi(a|s)]
$$
$$
= b(s) \cdot \sum_a \pi(a|s) \frac{\nabla \pi(a|s)}{\pi(a|s)}
$$
$$
= b(s) \cdot \nabla \sum_a \pi(a|s)
$$
因为概率之和 $\sum \pi = 1$，常数的梯度是 0：
$$
= b(s) \cdot \nabla (1) = 0
$$
&lt;strong&gt;结论：&lt;/strong&gt; 在第三个公式中，$\hat{V}(s)$ 只是作为一个 Baseline 被减去。根据上述证明，它在数学期望上会被抵消掉（也就是&lt;strong&gt;不会改变梯度的平均方向&lt;/strong&gt;），所以它是 &lt;strong&gt;Unbiased（无偏）&lt;/strong&gt; 的。它唯一的作用就是降低方差。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;action-dependent&lt;/h3&gt;
&lt;p&gt;上述的方法只是将state引入预测然后作为baseline，如果我们能用 $Q(s,a)$ 做 Baseline 呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$Q(s,a)$ 是针对&lt;strong&gt;具体动作&lt;/strong&gt;的预测价值。&lt;/li&gt;
&lt;li&gt;如果我们计算 $Return - Q(s,a)$，理论上这个差值（Advantage）会非常小（接近于 0 或仅剩环境噪声）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;好处：&lt;/strong&gt; 极大地降低方差 (Lower Variance)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;坏处：&lt;/strong&gt; 直接减去 $Q(s,a)$ 会引入 &lt;strong&gt;Bias（偏差）&lt;/strong&gt;。
&lt;img src=&quot;./Screenshot%202025-12-06%20at%2016.59.34.png&quot; alt=&quot;alt text&quot;&gt;
我们的终极目标是计算 策略梯度 (Policy Gradient) 的无偏估计：
$$
\nabla_\theta J(\theta) = \mathbb{E}&lt;em&gt;{s \sim \rho, a \sim \pi&lt;/em&gt;\theta} [\nabla_\theta \log \pi_\theta(a|s) \cdot Q^\pi(s,a)]
$$
但是他方差大，所以我们引入Q作为baseline，但是这有造成他不能无偏了。
我们构造一个新的梯度估计量 $g_{new}$，利用数学恒等式：
$$
X = (X - Y) + Y
$$
取期望：
$$
\mathbb{E}[X] = \mathbb{E}[X - Y] + \mathbb{E}[Y]
$$
在这里：&lt;/li&gt;
&lt;li&gt;$X$ 是我们原本那个&lt;strong&gt;方差很大&lt;/strong&gt;的梯度估计 $g_{target}$。&lt;/li&gt;
&lt;li&gt;$Y$ 是基于 Critic 的梯度估计（作为控制变量），记为 $g_{critic} = \nabla_\theta \log \pi_\theta(a|s) \cdot Q_\phi^\pi(s,a)$。
我们希望构造一个新的估计量：
$$
g_{new} = \underbrace{(g_{target} - g_{critic})}&lt;em&gt;{\text{第一部分}} + \underbrace{\mathbb{E}[g&lt;/em&gt;{critic}]}&lt;em&gt;{\text{第二部分}}
$$
如果 $g&lt;/em&gt;{target}$ 和 $g_{critic}$ 高度相关（即 Critic 训练得准），那么 $(g_{target} - g_{critic})$ 的值会非常小，方差也就非常小。
带入一下就是
$$
\nabla_\theta J(\theta) \approx \underbrace{\frac{1}{N} \sum \nabla_\theta \log \pi_\theta(a|s) \left( \hat{Q} - Q_\phi^\pi(s,a) \right)}&lt;em&gt;{\text{Term 1: 蒙特卡洛残差 (低方差)}} + \underbrace{\frac{1}{N} \sum \nabla&lt;/em&gt;\theta \mathbb{E}&lt;em&gt;{a \sim \pi&lt;/em&gt;\theta} [Q_\phi^\pi(s,a)]}_{\text{Term 2: 期望修正项 (保证无偏)}}
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;n-step returns&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-06%20at%2017.17.39.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案 A: One-step Actor-Critic (只看一步)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;公式：$\hat{A}&lt;em&gt;C^\pi = r_t + \gamma V(s&lt;/em&gt;{t+1}) - V(s_t)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点：&lt;/strong&gt; 也就是 TD(0)。我们只用了一个真实的奖励 $r_t$，剩下的全靠 Critic 猜。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评价：&lt;/strong&gt; 方差极低（Lower variance），但偏差很大（Higher bias），因为 Critic 总是猜不准。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案 B: Monte Carlo (看到底)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;公式：$\hat{A}_{MC}^\pi = \sum \gamma r - V(s_t)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点：&lt;/strong&gt; 必须等到游戏结束，把所有奖励加起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评价：&lt;/strong&gt; 无偏差（No bias），但方差极高（Higher variance），因为这一路上任何一个随机扰动都会改变最终结果。
&lt;strong&gt;折中方案：N-step Returns (The Solution)&lt;/strong&gt;
如果我们不只看 1 步，也不看无限步，而是看 &lt;strong&gt;N 步&lt;/strong&gt;（比如 5 步或 10 步），会发生什么？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;公式
$$
\hat{A}&lt;em&gt;n^\pi = \underbrace{\sum&lt;/em&gt;{t&apos;=t}^{t+n} \gamma^{t&apos;-t} r(s_{t&apos;}, a_{t&apos;})}&lt;em&gt;{\text{前 N 步使用真实数据}} - V(s_t) + \underbrace{\gamma^n \hat{V}(s&lt;/em&gt;{t+n})}_{\text{N 步之后使用 Critic 预测}}
$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义：&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;你先在这个世界上真实地跑 $N$ 步，收集确凿的奖励证据（这部分是无偏的）。&lt;/li&gt;
&lt;li&gt;跑到第 $N$ 步的时候，你累了，剩下的路程你不想跑了，于是你问 Critic：“从这儿往后还能拿多少分？”（这部分是有偏的，但因为还要乘以衰减因子 $\gamma^n$，其错误的影响被缩小了）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;右边的树状图非常生动地解释了&lt;strong&gt;为什么要“Cut here”（在这里截断）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;树根 (Start):&lt;/strong&gt; 状态是确定的，方差很小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分叉 (Branching):&lt;/strong&gt; 每走一步，环境都会有随机性，动作也有随机性。路径像树枝一样发散。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bigger Variance:&lt;/strong&gt; 走得越远，可能发生的路径组合就越多，结果的波动（方差）就越不可控。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cut Here:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果我们一直走到头（红色大圈），方差会爆炸。&lt;/li&gt;
&lt;li&gt;如果我们在这里切一刀（红色横线），用 Critic 的预测值来“封口”，就可以把方差控制在一个合理的范围内（Smaller variance）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们还可以跟进一步，我们不设定n具体是多少，我们遍历n的所有情况，然后加权累加
$$
\hat{A}&lt;em&gt;n^\pi = \sum&lt;/em&gt;{t&apos;=t}^{t+n} \gamma^{t&apos;-t} r(s_{t&apos;}, a_{t&apos;}) - V(s_t) + \gamma^n \hat{V}(s_{t+n})
$$
$$
\hat{A}&lt;em&gt;{GAE}^\pi = \sum&lt;/em&gt;{n=1}^{\infty} w_n \hat{A}&lt;em&gt;n^\pi
$$
使用指数衰减：$w_n \propto \lambda^{n-1}$。
如果我们按照 GAE 的定义直接算，计算量会非常大且复杂。
GAE 的定义是把所有可能的 N-step 优势拿来取指数加权平均：
$$
A&lt;/em&gt;{GAE} = (1-\lambda) \big( A^{(1)} + \lambda A^{(2)} + \lambda^2 A^{(3)} + \dots \big)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里 $A^{(1)}$ 是只看一步的优势。&lt;/li&gt;
&lt;li&gt;$A^{(2)}$ 是看两步的优势。&lt;/li&gt;
&lt;li&gt;以此类推……&lt;/li&gt;
&lt;li&gt;$(1-\lambda)$ 是为了让权重之和归一化（等于1）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你直接写代码算这个，你需要先算出 $A^{(1)}$, 再算 $A^{(2)}$, 再算 $A^{(3)}$……这非常麻烦。
这里就可以有一个数学小trick
我们定义单步误差： $\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;看 1 步 ($A^{(1)}$):
$$
A^{(1)} = \delta_t
$$&lt;/li&gt;
&lt;li&gt;看 2 步 ($A^{(2)}$):
$$
A^{(2)} = \delta_t + \gamma \delta_{t+1}
$$
$A^{(2)}$ 包含了 $r_t + \gamma r_{t+1} + \gamma^2 V_{t+2} - V_t$，中间的 $V_{t+1}$ 会在 $\delta$ 的相加中被消掉。&lt;/li&gt;
&lt;li&gt;看 3 步 ($A^{(3)}$):
$$
A^{(3)} = \delta_t + \gamma \delta_{t+1} + \gamma^2 \delta_{t+2}
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在我们把这些 $\delta$ 代回到最开始的加权公式里
$$
A_{GAE} = (1-\lambda) \big( \underbrace{\delta_t}&lt;em&gt;{A^{(1)}} + \lambda (\underbrace{\delta_t + \gamma \delta&lt;/em&gt;{t+1}}&lt;em&gt;{A^{(2)}}) + \lambda^2 (\underbrace{\delta_t + \gamma \delta&lt;/em&gt;{t+1} + \gamma^2 \delta_{t+2}}&lt;em&gt;{A^{(3)}}) + \dots \big)
$$
我们不要按 $A^{(n)}$ 分组，我们&lt;strong&gt;按 $\delta$ 分组&lt;/strong&gt;（把所有的 $\delta_t$ 放在一起，所有的 $\delta&lt;/em&gt;{t+1}$ 放在一起...）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\delta_t$ 的系数是多少？
它在每一项里都有。
系数 = $(1-\lambda) (1 + \lambda + \lambda^2 + \dots)$
这是一个几何级数求和，$(1 + \lambda + \lambda^2 + \dots) = \frac{1}{1-\lambda}$。
所以 $\delta_t$ 的总系数 = $(1-\lambda) \cdot \frac{1}{1-\lambda} = \mathbf{1}$。&lt;/li&gt;
&lt;li&gt;$\delta_{t+1}$ 的系数是多少？
它从第二项开始出现，且带着 $\gamma$。
系数 = $(1-\lambda) (\lambda \gamma + \lambda^2 \gamma + \dots)$
提取公因数 $\gamma \lambda$，变成 $\gamma \lambda (1-\lambda)(1 + \lambda + \dots)$。
结果 = $\gamma \lambda \cdot 1 = \mathbf{\gamma \lambda}$。&lt;/li&gt;
&lt;li&gt;$\delta_{t+2}$ 的系数是多少？
同理推导，结果是 $\mathbf{(\gamma \lambda)^2}$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过上面的运算，复杂的加权平均 $A_{GAE}$ 最终变成了极其简单的形式：
$$
A_{GAE}(t) = \delta_t + (\gamma \lambda) \delta_{t+1} + (\gamma \lambda)^2 \delta_{t+2} + \dots
$$
$$
\hat{A}&lt;em&gt;{GAE}^\pi = \sum (\gamma \lambda)^{t&apos;-t} \delta&lt;/em&gt;{t&apos;}
$$&lt;/p&gt;
&lt;p&gt;RL对数学要求太高了吧。。。。。。(悲)&lt;/p&gt;</content:encoded><img src="/_astro/11.kXxnmrE7.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.kXxnmrE7.png" length="0" type="image/png"/></item><item><title>CS285 Lecture5 Ploicy Gradient</title><link>https://laurie-hxf.xyz/blog/cs285-l5-ploicy-gradient</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l5-ploicy-gradient</guid><description>CS285 Lecture5 Ploicy Gradient</description><pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.hMrysc2K.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;Ploicy algorithm&lt;/h2&gt;
&lt;h3&gt;REINFORCE&lt;/h3&gt;
&lt;p&gt;初衷是我们并不知道初始状态$p(s_1)$ 是多少，也不知道环境转移概率$p(s_{t+1}|s_t,a_t)$ ，然后直接对期望求导的话就不能求。于是乎就有一系列数学变化来去掉这两个不知道的部分&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-02%20at%2023.40.16.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./Screenshot%202025-12-02%20at%2023.40.28.png&quot; alt=&quot;alt text&quot;&gt;&lt;img src=&quot;./Screenshot%202025-12-02%20at%2023.43.11.png&quot; alt=&quot;alt text&quot;&gt;&lt;img src=&quot;./Screenshot%202025-12-02%20at%2023.43.23.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是policy gradient有一个缺陷就是他的方差很大
$$\nabla_\theta J \approx \sum (\text{梯度方向}) \times (\text{回报 } R)$$&lt;/p&gt;
&lt;p&gt;这里的 $R$ 是一个随机变量。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;理想情况（低方差）：&lt;/strong&gt; $R$ 总是稳定在 10 左右。那么梯度的长度就很稳定，更新很平滑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PG 的情况（高方差）：&lt;/strong&gt; $R$ 可能这次是 100，下次是 -50，再下次是 500。
&lt;ul&gt;
&lt;li&gt;这意味着你的梯度向量 $\nabla_\theta J$ 忽长忽短，甚至方向完全相反。&lt;/li&gt;
&lt;li&gt;神经网络的参数 $\theta$ 就会在参数空间里&lt;strong&gt;剧烈震荡&lt;/strong&gt;，像个没头苍蝇一样乱撞，很难收敛到最优解。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子，假如说图中绿色表示奖励，蓝色表示概率分布，那么看到当前场景，模型会尽力将概率分布从实线的变成虚线的，尽可能减小奖励为负的那部分。
&lt;img src=&quot;./Screenshot%202025-12-04%20at%2018.00.23.png&quot; alt=&quot;alt text&quot;&gt;
但是假如我们的奖励之间的相对差没有变，但是所有的奖励都变成正数，那么模型的概率分布就又会变。
&lt;img src=&quot;./Screenshot%202025-12-04%20at%2018.03.59.png&quot; alt=&quot;alt text&quot;&gt;
也就是我们的$R$ 的波动太大，导致他的方差变大不好训练，所以可能需要大量的训练数据来根据大数定律来平均掉波动。当然还有别的方式下面会介绍。&lt;/p&gt;
&lt;h2&gt;Reducing Variance&lt;/h2&gt;
&lt;h3&gt;1&lt;/h3&gt;
&lt;p&gt;显然的是当前的动作只会影响未来的奖励，不会影响之前的奖励，所以我们可以把红色圈中改成计算当前以及以后的奖励而不是从t=1开始计算。
&lt;img src=&quot;./Screenshot%202025-12-04%20at%2018.09.22.png&quot; alt=&quot;alt text&quot;&gt;
这样子的好处就是减少了求和总数，必然减少方差的大小。同时他还是无偏的&lt;/p&gt;
&lt;p&gt;证明无偏的核心思路是：&lt;/p&gt;
&lt;p&gt;我们需要证明被减掉的那部分（即“过去的奖励”与“当前动作梯度”的乘积）在数学期望上等于 0。&lt;/p&gt;
&lt;p&gt;如果我们证明了 $E[\text{被丢弃的项}] = 0$，那么：&lt;/p&gt;
&lt;p&gt;$$
E[\text{新公式}] = E[\text{原公式} - \text{被丢弃的项}] = E[\text{原公式}] - 0 = E[\text{原公式}]
$$&lt;/p&gt;
&lt;p&gt;既然原公式是无偏的（这是 Policy Gradient 的定义），那么新公式也就是无偏的。&lt;/p&gt;
&lt;p&gt;下面是详细的数学推导步骤。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Score Function 的期望为 0&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于任何概率分布 $\pi_\theta(x)$，都有：&lt;/p&gt;
&lt;p&gt;$$
E_{x \sim \pi_\theta} [\nabla_\theta \log \pi_\theta(x)] = 0
$$&lt;/p&gt;
&lt;p&gt;证明：&lt;/p&gt;
&lt;p&gt;利用对数导数技巧 $\nabla \log f(x) = \frac{\nabla f(x)}{f(x)}$：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned} E_{x \sim \pi_\theta} [\nabla_\theta \log \pi_\theta(x)] &amp;#x26;= \int \pi_\theta(x) \nabla_\theta \log \pi_\theta(x) , dx \ &amp;#x26;= \int \pi_\theta(x) \frac{\nabla_\theta \pi_\theta(x)}{\pi_\theta(x)} , dx \ &amp;#x26;= \int \nabla_\theta \pi_\theta(x) , dx \ &amp;#x26;= \nabla_\theta \left( \int \pi_\theta(x) , dx \right) \ &amp;#x26;= \nabla_\theta (1) \ &amp;#x26;= 0 \end{aligned}
$$&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(注：对于离散动作，积分 $\int$ 换成求和 $\sum$ 也是一样的)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论：&lt;/strong&gt; 只要这时候乘在这个梯度后面的是一个&lt;strong&gt;常数&lt;/strong&gt;（或者不依赖于当前 $x$ 的项），整个期望就是 0。&lt;/p&gt;
&lt;p&gt;原公式里的总回报 $\sum_{t&apos;=1}^T r_{t&apos;}$ 可以拆成两部分：&lt;strong&gt;过去的回报&lt;/strong&gt;（Past）和&lt;strong&gt;未来的回报&lt;/strong&gt;（Future/Reward-to-go）。&lt;/p&gt;
&lt;p&gt;针对某一个特定的时刻 $t$，原公式的期望项是：&lt;/p&gt;
&lt;p&gt;$$
E \left[ \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot \left( \underbrace{\sum_{t&apos;=1}^{t-1} r_{t&apos;}}&lt;em&gt;{\text{过去 (Past)}} + \underbrace{\sum&lt;/em&gt;{t&apos;=t}^{T} r_{t&apos;}}_{\text{未来 (Future)}} \right) \right]
$$&lt;/p&gt;
&lt;p&gt;利用期望的线性性质，我们可以把它拆开：&lt;/p&gt;
&lt;p&gt;$$
= \underbrace{E \left[ \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot \sum_{t&apos;=t}^{T} r_{t&apos;} \right]}&lt;em&gt;{\text{新公式 (PPT下半部分)}} + \underbrace{E \left[ \nabla&lt;/em&gt;\theta \log \pi_\theta(a_t|s_t) \cdot \sum_{t&apos;=1}^{t-1} r_{t&apos;} \right]}_{\text{我们需要证明这项为 0}}
$$&lt;/p&gt;
&lt;p&gt;我们要证明的是：&lt;/p&gt;
&lt;p&gt;$$
E \left[ \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot r_{\text{past}} \right] = 0
$$&lt;/p&gt;
&lt;p&gt;这里利用条件期望（Conditional Expectation）。&lt;/p&gt;
&lt;p&gt;想象我们在时刻 $t$，此时状态 $s_t$ 已经确定，之前的历史（包括过去的奖励 $r_{\text{past}}$）也都已经发生了，变成了既定事实（常数）。&lt;/p&gt;
&lt;p&gt;我们针对&lt;strong&gt;当前的动作 $a_t$&lt;/strong&gt; 求期望：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned} &amp;#x26; E_{\text{trajectory}} \left[ \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot r_{\text{past}} \right] \ &amp;#x26;= E_{\text{history}} \left[ E_{a_t \sim \pi_\theta(\cdot|s_t)} \left[ \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot r_{\text{past}} \mid s_t, \text{history} \right] \right] \end{aligned}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因果律 (Causality)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$r_{\text{past}}$ 是在时刻 $t$ 之前发生的，它不依赖于现在才要做的动作 $a_t$。&lt;/li&gt;
&lt;li&gt;所以在对 $a_t$ 求期望时，$r_{\text{past}}$ 可以像常数一样被提取出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
= E_{\text{history}} \left[ r_{\text{past}} \cdot \underbrace{E_{a_t \sim \pi_\theta(\cdot|s_t)} \left[ \nabla_\theta \log \pi_\theta(a_t|s_t) \right]}_{\text{这正是我们在第1步证明的恒等式，等于 0}} \right]
$$&lt;/p&gt;
&lt;p&gt;$$
= E_{\text{history}} \left[ r_{\text{past}} \cdot 0 \right] = 0
$$&lt;/p&gt;
&lt;p&gt;所以，把“过去的奖励”从公式里删掉，&lt;strong&gt;不会改变梯度的期望值（保持无偏）&lt;/strong&gt;，但因为少加了一堆随机数（$r_{\text{past}}$ 在不同轨迹中波动很大），所以&lt;strong&gt;显著降低了方差&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;2&lt;/h3&gt;
&lt;p&gt;还有方法就是给奖励设置baseline，控制奖励在一定范围内波动，最常见的应该是baseline设置为奖励的平均值。同时我们也可以计算这个变化也是无偏的。
&lt;img src=&quot;./Screenshot%202025-12-04%20at%2018.20.07.png&quot; alt=&quot;alt text&quot;&gt;
当然我们可以设置更好的baseline值，这就要对方差进行求导。
&lt;img src=&quot;./Screenshot%202025-12-04%20at%2018.20.59.png&quot; alt=&quot;alt text&quot;&gt;
不过在实际操作中，很少用这个。虽然这个公式是理论最优，但在深度强化学习中计算这个加权平均太麻烦了。&lt;/p&gt;
&lt;h2&gt;Off-Policy Policy Gradients&lt;/h2&gt;
&lt;p&gt;PG一开始是on policy的，为什么呢，从这两个公式就可以看到
$$
\nabla_\theta J(\theta) = \int \underbrace{p_\theta(\tau)}&lt;em&gt;{\text{这是概率分布}} \left[ \nabla&lt;/em&gt;\theta \log p_\theta(\tau) r(\tau) \right] d\tau
$$
$$
\nabla_\theta J(\theta) = E_{\tau \sim p_\theta(\tau)} \left[ \nabla_\theta \log p_\theta(\tau) r(\tau) \right]
$$
样本 $\tau$ 必须是从 $p_\theta(\tau)$ 这个分布里采样出来的&lt;/p&gt;
&lt;p&gt;$$
\tau \sim p_\theta(\tau)
$$
那么就意味着每次我更新一点参数，我就要重新采集一批数据，这个是非常低效的。于是就有Off-Policy Policy Gradients。&lt;/p&gt;
&lt;h3&gt;importance sampling&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-04%20at%2021.50.28.png&quot; alt=&quot;alt text&quot;&gt;
我们可以根据绿色方框中的公式变化应用到原本的训练目标中，使得
$$
J(\theta) = E_{\tau \sim \bar{p}(\tau)} \left[ \frac{p_\theta(\tau)}{\bar{p}(\tau)} r(\tau) \right]
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$r(\tau)$&lt;/strong&gt;：以前的回报。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$\frac{p_\theta(\tau)}{\bar{p}(\tau)}$&lt;/strong&gt;：修正系数。
&lt;ul&gt;
&lt;li&gt;如果某条轨迹在&lt;strong&gt;当前策略 $p_\theta$&lt;/strong&gt; 下发生的概率很高，但在&lt;strong&gt;旧策略 $\bar{p}$&lt;/strong&gt; 下很低，这个系数就会很大（说明这条数据对现在很重要，要重视）。&lt;/li&gt;
&lt;li&gt;反之系数就会很小。
计算那个修正系数 $\frac{p_\theta(\tau)}{\bar{p}(\tau)}$ 看起来很难，因为轨迹 $\tau$ 包含了一堆东西：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
p(\tau) = p(s_1) \prod \pi(a|s) p(s&apos;|s,a)
$$&lt;/p&gt;
&lt;p&gt;这里面包含了 环境的物理规律（Dynamics） $p(s&apos;|s,a)$，这通常是我们不知道的（比如风速怎么变、摩擦力是多少）。&lt;/p&gt;
&lt;p&gt;但是当我们做除法时：&lt;/p&gt;
&lt;p&gt;$$
\frac{p_\theta(\tau)}{\bar{p}(\tau)} = \frac{p(s_1) \prod \pi_\theta(a|s) p(s&apos;|s,a)}{p(s_1) \prod \bar{\pi}(a|s) p(s&apos;|s,a)}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始状态概率 $p(s_1)$：上下都有，消掉（红线划掉的部分）。&lt;/li&gt;
&lt;li&gt;环境转移概率 $p(s&apos;|s,a)$：客观世界规律不随你的策略改变，上下都有，消掉（红线划掉的部分）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终结果：
$$
\frac{p_\theta(\tau)}{\bar{p}(\tau)} = \frac{\prod \pi_\theta(a_t|s_t)}{\prod \bar{\pi}(a_t|s_t)}
$$&lt;/p&gt;
&lt;p&gt;之后我们将上述技巧用在PG中
&lt;img src=&quot;./Screenshot%202025-12-04%20at%2021.55.14.png&quot; alt=&quot;alt text&quot;&gt;
我们看最上面的公式：&lt;/p&gt;
&lt;p&gt;$$
\nabla_{\theta&apos;} J(\theta&apos;) = E_{\tau \sim p_\theta(\tau)} \left[ \frac{p_{\theta&apos;}(\tau)}{p_\theta(\tau)} \nabla_{\theta&apos;} \log \pi_{\theta&apos;}(\tau) r(\tau) \right]
$$&lt;/p&gt;
&lt;p&gt;这里有三个核心组件，我们需要把它们都按时间步 $t$ 展开：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重要性权重（Importance Weight） $\frac{p_{\theta&apos;}(\tau)}{p_\theta(\tau)}$&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;轨迹概率 $p(\tau) = p(s_1) \prod \pi(a|s) p(s&apos;|s,a)$。&lt;/li&gt;
&lt;li&gt;环境动力学 $p(s&apos;|s,a)$ 和初始分布 $p(s_1)$ 在分子分母中是一样的，&lt;strong&gt;直接消掉&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;只剩下策略的连乘：
$$
\frac{p_{\theta&apos;}(\tau)}{p_\theta(\tau)} = \prod_{t=1}^T \frac{\pi_{\theta&apos;}(a_t|s_t)}{\pi_{\theta}(a_t|s_t)}
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;梯度项 $\nabla_{\theta&apos;} \log \pi_{\theta&apos;}(\tau)$&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为 $\log(\prod x) = \sum \log x$。&lt;/li&gt;
&lt;li&gt;所以整条轨迹的 log 概率梯度，等于每个时间步动作概率梯度的和：
$$
\sum_{t=1}^T \nabla_{\theta&apos;} \log \pi_{\theta&apos;}(a_t|s_t)
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;总回报 $r(\tau)$&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就是所有时间步奖励的&lt;strong&gt;和&lt;/strong&gt;：$\sum_{t=1}^T r(s_t, a_t)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当我们把上面三个展开式代回去，就得到了 PPT 中间那个长公式：&lt;/p&gt;
&lt;p&gt;$$
= E \left[ \left( \prod_{t=1}^T \frac{\pi_{\theta&apos;}}{\pi_{\theta}} \right) \left( \sum_{t=1}^T \nabla \log \pi_{\theta&apos;} \right) \left( \sum_{t=1}^T r \right) \right]
$$&lt;/p&gt;
&lt;p&gt;为了分析第 $t$ 个时刻的梯度 $\nabla_{\theta&apos;} \log \pi_{\theta&apos;}(a_t|s_t)$ 到底该乘以什么权重，我们把连乘部分拆成了两半：过去和未来。&lt;/p&gt;
&lt;p&gt;公式变成了：&lt;/p&gt;
&lt;p&gt;$$
= E \left[ \sum_{t=1}^T \nabla_{\theta&apos;} \log \pi_{\theta&apos;}(a_t|s_t) \left( \underbrace{\prod_{t&apos;=1}^{t} \frac{\pi_{\theta&apos;}}{\pi_{\theta}}}&lt;em&gt;{\text{过去}} \right) \left( \sum r \right) \left( \underbrace{\prod&lt;/em&gt;{t&apos;&apos;=t}^{T} \frac{\pi_{\theta&apos;}}{\pi_{\theta}}}_{\text{未来}} \right) \right]
$$&lt;/p&gt;
&lt;p&gt;必须要保留的（左边的连乘）：
PPT 用箭头指出：&lt;strong&gt;&quot;future actions don&apos;t affect current weight&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：我们在时刻 $t$ 做决定时，能不能到达这个状态 $s_t$，完全取决于&lt;strong&gt;过去&lt;/strong&gt;（$1$ 到 $t$）发生了什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解释&lt;/strong&gt;：如果新策略 $\pi_{\theta&apos;}$ 和旧策略 $\pi_\theta$ 在过去差别很大，那么 $s_t$ 的出现概率就不一样，这个权重（Ratio）必须保留，用来修正状态分布的偏差。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;右边的部分忽视掉，后面的章节会讲解为什么可以。&lt;/p&gt;
&lt;p&gt;然后我们进一步处理转化后公式中连乘的那一项
&lt;img src=&quot;./Screenshot%202025-12-04%20at%2022.01.49.png&quot; alt=&quot;alt text&quot;&gt;
连乘的那个比值哪怕每一项都只偏离一点点（比如 1.1 或 0.9），如果你乘上 100 步（$1.1^{100}$），这个数值就会变得巨大或者接近于零。这会让梯度计算极其不稳定（方差极大）。&lt;/p&gt;
&lt;p&gt;我们可以根据下面这个公式
$$
\prod_{k=1}^{t-1} \frac{\pi_{\theta&apos;}(a_k|s_k)}{\pi_{\theta}(a_k|s_k)} \approx \frac{p_{\theta&apos;}(s_t)}{p_{\theta}(s_t)}
$$
转化一下
$$
W_t = \underbrace{\left( \prod_{k=1}^{t-1} \frac{\pi_{\theta&apos;}(a_k|s_k)}{\pi_{\theta}(a_k|s_k)} \right)}&lt;em&gt;{\text{Part A: 走到当前状态的概率比}} \cdot \underbrace{\left( \frac{\pi&lt;/em&gt;{\theta&apos;}(a_t|s_t)}{\pi_{\theta}(a_t|s_t)} \right)}&lt;em&gt;{\text{Part B: 当前动作的概率比}}
$$
这一长串过去的动作概率比值 $\prod&lt;/em&gt;{k=1}^{t-1} \dots$，决定了你有多大可能到达现在的状态 $s_t$
然后
$$
W_t \approx \frac{p_{\theta&apos;}(s_t)}{p_{\theta}(s_t)} \cdot \frac{\pi_{\theta&apos;}(a_t|s_t)}{\pi_{\theta}(a_t|s_t)}
$$
$$
\frac{\pi_{\theta&apos;}(s)}{\pi_{\theta}(s)}=\frac{p_{\theta&apos;}(s_t)}{p_{\theta}(s_t)}
$$
然后我们假设“前面的 $t-1$ 步里，新策略和旧策略的表现完全一样，没有产生任何累积的偏差。”，这也就是ppt中划掉的那一部分。于是乎，我们将连乘那部分解决了。&lt;/p&gt;
&lt;h2&gt;Advanced Policy Gradients&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-12-05%20at%2000.33.21.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们在使用标准梯度下降的时候公式是
$$
\theta \leftarrow \theta + \alpha \nabla_\theta J(\theta)
$$
但这个往往表现不好，原因就是参数空间（$\theta$）和概率分布空间（$\pi_\theta$）是不一样的。有的参数动一点点（比如方差 $\sigma$ 很小时的均值 $k$），概率分布会剧烈变化。有的参数动很大（比如方差 $\sigma$ 很大时的均值 $k$），概率分布几乎不变。如果我们不管三七二十一，对所有参数都用同样的步长 $\alpha$ 去更新，就会导致上梯度的“震荡”或“停滞”。&lt;/p&gt;
&lt;p&gt;从数学上定义这个优化过程就是
$$
\theta&apos; \leftarrow \arg \max_{\theta&apos;} (\theta&apos; - \theta)^T \nabla_\theta J(\theta) \quad \text{s.t.} \quad |\theta&apos; - \theta|^2 \leq \epsilon
$$
怎么理解这个公式呢&lt;/p&gt;
&lt;p&gt;目标函数：$\max_{\theta&apos;} (\theta&apos; - \theta)^T \nabla_\theta J(\theta)$
这一项其实是对 $J(\theta&apos;)$ 的&lt;strong&gt;一阶泰勒展开（线性近似）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;推导逻辑：
根据泰勒展开，新位置的函数值 $J(\theta&apos;)$ 约等于：
$$
J(\theta&apos;) \approx J(\theta) + (\theta&apos; - \theta)^T \nabla_\theta J(\theta)
$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;因为 $J(\theta)$ 是常数（起点的分数值），所以我们要最大化 $J(\theta&apos;)$，就等同于最大化后面那一坨：$(\theta&apos; - \theta)^T \nabla_\theta J(\theta)$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向量视角&lt;/strong&gt;：这也是两个向量的点积。要让点积最大，&lt;strong&gt;更新方向 $(\theta&apos; - \theta)$ 必须和 梯度方向 $\nabla_\theta J(\theta)$ 平行且同向&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;就是“我要沿着坡度最陡的方向往上爬。”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;约束条件：$\text{s.t.} \quad |\theta&apos; - \theta|^2 \leq \epsilon$
这一项限制了更新的&lt;strong&gt;步长&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;$|\cdot|^2$ 是&lt;strong&gt;欧几里得距离&lt;/strong&gt;的平方。&lt;/li&gt;
&lt;li&gt;$\epsilon$ 是一个很小的常数（可以理解为圆的半径）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;“但是，我不允许你一步走太远。你只能在以当前位置为中心、半径为 $\sqrt{\epsilon}$ 的圆圈（或者球体）里面找新的落脚点。”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你用拉格朗日乘子法去解上面这个带约束的优化问题：
$$
L = (\theta&apos; - \theta)^T \nabla J - \lambda (|\theta&apos; - \theta|^2 - \epsilon)
$$&lt;/p&gt;
&lt;p&gt;对 $\theta&apos;$ 求导并令其为 0，你会得到结果：
$$
\theta&apos; - \theta = \frac{1}{2\lambda} \nabla_\theta J(\theta)
$$&lt;/p&gt;
&lt;p&gt;令 $\alpha = \frac{1}{2\lambda}$（学习率），这就变成了我们最熟悉的公式：
$$
\theta&apos; = \theta + \alpha \nabla_\theta J(\theta)
$$
那么本质就是在欧几里得空间的一个小圆球内，寻找让目标函数线性增长最快的方向。&lt;/p&gt;
&lt;p&gt;但是问题就是我们尝试优化的这个参数空间并不是我们的策略空间&lt;/p&gt;
&lt;h4&gt;1. 参数空间 (Parameter Space, $\Theta$)&lt;/h4&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;：如果你有一个神经网络，它的所有权重和偏置组成了一个长向量 $\theta = [\theta_1, \theta_2, \dots, \theta_n]$。这个向量所在的 $n$ 维空间就是参数空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;度量方式&lt;/strong&gt;：通常用&lt;strong&gt;欧几里得距离&lt;/strong&gt;（Euclidean Distance）。
&lt;ul&gt;
&lt;li&gt;比如 $\theta$ 和 $\theta&apos;$ 的距离是 $|\theta - \theta&apos;|^2$。&lt;/li&gt;
&lt;li&gt;这就好比你在直角坐标系里量两个点的直线距离。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;你可以控制这里&lt;/strong&gt;：梯度下降算法（SGD, Adam）直接修改的就是这堆数字。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 策略分布空间 (Policy Distribution Space, $\Pi$)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：这是你的智能体（Agent）在面对环境时，输出的&lt;strong&gt;概率分布&lt;/strong&gt;的集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;具体样子&lt;/strong&gt;：对于每一个状态 $s$，智能体都会输出一个动作的概率分布 $\pi_\theta(a|s)$。所有可能的概率分布构成了这个空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;度量方式&lt;/strong&gt;：通常用 &lt;strong&gt;KL 散度&lt;/strong&gt;（KL Divergence）。
&lt;ul&gt;
&lt;li&gt;它衡量的是“两个概率分布有多不一样”。&lt;/li&gt;
&lt;li&gt;这实际上是一个弯曲的&lt;strong&gt;黎曼流形&lt;/strong&gt;（Riemannian Manifold），而不是平坦的欧几里得空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;你真正在乎的是这里&lt;/strong&gt;：你并不关心 $\theta$ 是 0.1 还是 0.2，你只关心“智能体向左走的概率”是 10% 还是 90%。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 核心矛盾：两个空间的“映射”是扭曲的&lt;/h4&gt;
&lt;p&gt;在标准的梯度下降（Vanilla PG）中，我们假设：参数变动一点点 $\approx$ 策略行为变动一点点。
但在强化学习里，这个假设经常完全崩溃。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标准梯度下降（Vanilla PG）：
它是在参数空间里走路。它说：“我要把参数 $\theta$ 挪动 0.01 的距离”。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;风险&lt;/em&gt;：它不知道这 0.01 在另一边（策略空间）意味着是“迈了一小步”还是“跳下了悬崖”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自然梯度 / TRPO / PPO：
它们是在策略分布空间里走路。它说：“我要让策略的概率分布改变 0.01（KL 散度）”。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;做法&lt;/em&gt;：它会反推回参数空间——“为了让概率只变 0.01，我的参数 $\theta$ 到底应该动多少？”
&lt;ul&gt;
&lt;li&gt;如果在敏感区，参数就只动 0.00001。&lt;/li&gt;
&lt;li&gt;如果在平原区，参数就大胆动 10.0。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;好处&lt;/em&gt;：&lt;strong&gt;稳定&lt;/strong&gt;且&lt;strong&gt;高效&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是我们可以改变约束条件，我们的优化目标还是不变
$$
\theta&apos; \leftarrow \arg \max_{\theta&apos;} (\theta&apos; - \theta)^T \nabla_\theta J(\theta) \quad \text{s.t.} \quad D(\pi_{\theta&apos;}, \pi_\theta) \leq \epsilon
$$
新的约束：$D(\pi_{\theta&apos;}, \pi_\theta) \leq \epsilon$。计算的是两个概率分布的KL散度，描述两个概率分布的差距。我们约束前后两个概率分布的差距在一定范围内然后找最优解。&lt;/p&gt;
&lt;p&gt;虽然 KL 散度很好，但它计算起来很复杂。为了在计算机里快速求解，我们需要对它进行近似。
PPT 下半部分展示了如何把 KL 散度转化为二次型（Quadratic Form）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;KL 散度的泰勒展开：
如果在 $\theta$ 附近对 $D_{KL}(\pi_{\theta&apos;} || \pi_\theta)$ 进行二阶泰勒展开，你会发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;零阶项（常数）是 0（因为自己和自己的距离是 0）。&lt;/li&gt;
&lt;li&gt;一阶项（梯度）是 0（因为 KL 散度在 $\theta&apos;=\theta$ 处取极小值）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二阶项（海森矩阵 Hessian）才是关键&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入 Fisher 信息矩阵（Matrix F）：
PPT 给出了近似公式：
$$
D_{KL}(\pi_{\theta&apos;} || \pi_\theta) \approx (\theta&apos; - \theta)^T \mathbf{F} (\theta&apos; - \theta)
$$&lt;/p&gt;
&lt;p&gt;这里的 $\mathbf{F}$ 就是 Fisher Information Matrix (FIM)。
它的定义在右下角：
$$
\mathbf{F} = E_{\pi_\theta} [\nabla_\theta \log \pi_\theta(\mathbf{a}|\mathbf{s}) \nabla_\theta \log \pi_\theta(\mathbf{a}|\mathbf{s})^T]
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以把 &lt;strong&gt;$\mathbf{F}$&lt;/strong&gt; 理解为一个“地形校正器”或“曲率矩阵”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;它是 KL 散度的二阶导数&lt;/strong&gt;：它告诉我们在当前的参数位置，策略分布对于参数变化有多敏感。
&lt;ul&gt;
&lt;li&gt;如果某个方向上 $\mathbf{F}$ 的值很大（曲率大），说明参数稍微动一下，KL 散度就剧增（策略变化很大）。&lt;/li&gt;
&lt;li&gt;如果某个方向上 $\mathbf{F}$ 的值很小（曲率小），说明参数动很多，KL 散度才变一点点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自然梯度更新（Natural Gradient Update）&lt;/p&gt;
&lt;p&gt;如果我们把这个新的约束（$(\theta&apos; - \theta)^T \mathbf{F} (\theta&apos; - \theta) \leq \epsilon$）代入优化问题求解，我们会得到&lt;strong&gt;自然梯度更新公式&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\theta_{new} = \theta_{old} + \alpha \mathbf{F}^{-1} \nabla_\theta J(\theta)
$$&lt;/p&gt;
&lt;p&gt;请注意那个 &lt;strong&gt;$\mathbf{F}^{-1}$&lt;/strong&gt;（Fisher 矩阵的逆）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这就是 PPT 问的 &quot;&lt;strong&gt;rescale&lt;/strong&gt;&quot;。&lt;/li&gt;
&lt;li&gt;它自动抵消了地形的崎岖：
&lt;ul&gt;
&lt;li&gt;在陡峭的地方（$\mathbf{F}$ 大），$\mathbf{F}^{-1}$ 会把梯度&lt;strong&gt;缩小&lt;/strong&gt;，防止步子迈太大。&lt;/li&gt;
&lt;li&gt;在平坦的地方（$\mathbf{F}$ 小），$\mathbf{F}^{-1}$ 会把梯度&lt;strong&gt;放大&lt;/strong&gt;，防止停滞不前。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><img src="/_astro/11.hMrysc2K.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.hMrysc2K.png" length="0" type="image/png"/></item><item><title>CS285 Lecture4 Introduction to Reinforcement Learning</title><link>https://laurie-hxf.xyz/blog/cs285-l4-introduction-to-reinforcement-learning</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l4-introduction-to-reinforcement-learning</guid><description>CS285 Lecture2 Introduction to Reinforcement Learning</description><pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.D9kPkauM.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;Markov chain&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-11-30%20at%2022.12.37.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Markov decision process(马尔可夫决策过程)&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-11-30%20at%2022.22.11.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./Screenshot%202025-11-30%20at%2022.19.20.png&quot; alt=&quot;alt text&quot;&gt;
相比于马尔可夫链，他多的就是下一次的状态不止是和当前状态有关系，还和采取的动作有关系。以及多了一个奖励函数&lt;/p&gt;
&lt;h2&gt;Partially observed Markov decision process(部分观测马尔可夫决策过程)&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-11-30%20at%2022.25.14.png&quot; alt=&quot;alt text&quot;&gt;
&lt;strong&gt;$\mathcal{O}$ - Observation Space (观测空间)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：所有可能观测到的数据的集合。&lt;/li&gt;
&lt;li&gt;比如：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$S$ (状态)&lt;/strong&gt;：可能是杯子在绝对坐标系下的 &lt;code&gt;(x, y, z)&lt;/code&gt; 坐标（这是上帝视角，你通常不知道）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$O$ (观测)&lt;/strong&gt;：是你的机器人摄像头拍到的&lt;strong&gt;像素图片&lt;/strong&gt;，或者是雷达的扫描点云。&lt;/li&gt;
&lt;li&gt;机器人只能拿到 $o \in \mathcal{O}$，拿不到 $s$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;$\mathcal{E}$ - Emission Probability (发射概率)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：$p(o_t | s_t)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：给定当前真实状态 $s_t$，观测到 $o_t$ 的概率是多少？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么叫“发射 (Emission)”？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这是一个来自&lt;strong&gt;隐马尔可夫模型 (HMM)&lt;/strong&gt; 的术语。你可以想象真实状态 $s$ 是隐藏在幕后的“发射源”，它向外“发射”出我们能看到的观测信号 $o$。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;例子&lt;/em&gt;：如果状态 $s$ 是“外面下雨了”，那么发射出的观测 $o$ 可能是“地面湿了”的概率是 90%，“有人打伞”的概率是 80%。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The goal of reinforcement learning&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-11-30%20at%2023.29.22.png&quot; alt=&quot;alt text&quot;&gt;
这个ppt就用数学语言定义了强化学习的目标是什么&lt;/p&gt;
&lt;p&gt;上面的公式就是
$\pi_\theta(\mathbf{a}_t|\mathbf{s}_t)$:面对当前画面 $\mathbf{s}_t$，智能体选择动作 $\mathbf{a}&lt;em&gt;t$ 的概率。
$p(\mathbf{s}&lt;/em&gt;{t+1}|\mathbf{s}_t, \mathbf{a}&lt;em&gt;t)$: 当你做了动作 $\mathbf{a}&lt;em&gt;t$ 后，世界根据物理定律，演变到下一个状态 $\mathbf{s}&lt;/em&gt;{t+1}$ 的概率。
然后从状态 $\mathbf{s}&lt;/em&gt;{1}$ 开始，一连串事件要连续发生，所代表的轨迹发生的概率是多少&lt;/p&gt;
&lt;p&gt;下面的那一个公式就是我们优化的参数目标
相同的参数我跑很多次，每次得到不同的轨迹和对应的概率，然后计算每个轨迹每一步的奖励，求和得到轨迹的总奖励，最终加权求和就是当前策略的奖励。我们的目标就是得到能使得这个奖励最大的策略。&lt;/p&gt;
&lt;p&gt;我们还可以用另一种方式来表示，我们可以将这个过程看作是一个马尔可夫链而不是马尔可夫决策过程，把(s,a)看做一个状态，就变成
&lt;img src=&quot;./Screenshot%202025-11-30%20at%2023.31.06.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;于是我们的优化就可以变成
$$
\begin{align*}
\theta^\star &amp;#x26;= \arg \max_\theta E_{\tau \sim p_\theta(\tau)} \left[ \sum_t r(\mathbf{s}&lt;em&gt;t, \mathbf{a}&lt;em&gt;t) \right] \
&amp;#x26;= \arg \max&lt;/em&gt;\theta \sum&lt;/em&gt;{t=1}^T E_{(\mathbf{s}_t, \mathbf{a}&lt;em&gt;t) \sim p&lt;/em&gt;\theta(\mathbf{s}_t, \mathbf{a}_t)} [r(\mathbf{s}_t, \mathbf{a}_t)]
\end{align*}
$$
这种转化的好处就是，当我的T是无穷的时候，他的奖励就不应该再是累加而应该是平均，因为趋于无穷，奖励和也趋于无穷就没有意义。&lt;/p&gt;
&lt;p&gt;然后当我有一个马尔可夫链满足遍历性(Ergodic: 有机会从任何一个状态跳到任何另一个状态)，他的分布最终就会平稳。所以说面对平均，T趋于无穷，他就会趋于E里面的内容，所以我们只看这部分就可以了。
&lt;img src=&quot;./Screenshot%202025-11-30%20at%2023.34.04.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;还有一点要提的就是，强化学习他的目标就是优化期望，即使我的奖励函数是离散的，但是他的期望是连续的，这就使得RL可以很好的进行优化&lt;/p&gt;
&lt;h2&gt;Algorithm&lt;/h2&gt;
&lt;p&gt;我们可以将强化学习算法分为三个部分，采样，评估，训练
&lt;img src=&quot;./Screenshot%202025-12-01%20at%2021.52.16.png&quot; alt=&quot;alt text&quot;&gt;
我们可以将之前的奖励拆分，定义Q function和value function，然后假设我们知道Q value，然后我们就可以根据这个来优化我们的模型
&lt;img src=&quot;./Screenshot%202025-12-01%20at%2021.51.44.png&quot; alt=&quot;alt text&quot;&gt;&lt;img src=&quot;./Screenshot%202025-12-01%20at%2021.53.53.png&quot; alt=&quot;alt text&quot;&gt;&lt;img src=&quot;./Screenshot%202025-12-01%20at%2021.54.00.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Tradeoffs Between Algorithms&lt;/h2&gt;
&lt;p&gt;我们有很多种RL算法，每种算法都有自己的优缺点，所以我们需要权衡每一种算法&lt;/p&gt;
&lt;h3&gt;RL算法&lt;/h3&gt;
&lt;h4&gt;1. Policy Gradients (策略梯度法)&lt;/h4&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; 我有一个神经网络（策略网络 $\pi_\theta$），它的输入是状态，输出是动作。我就盯着最终的得分看：
&lt;ul&gt;
&lt;li&gt;如果这局分高，我就调整参数 $\theta$，让刚才做的动作出现概率变大。&lt;/li&gt;
&lt;li&gt;如果这局分低，我就调整参数 $\theta$，让刚才做的动作出现概率变小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;它&lt;strong&gt;不直接去算 Q 值或 V 值&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;h4&gt;2. Value-based (基于价值的方法)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;怎么做：&lt;/strong&gt; 我根本&lt;strong&gt;不训练一个策略网络&lt;/strong&gt;（No explicit policy）。我不去学“什么时候该跳”，而是去学“在这个状态下跳能得多少分”（即学习 $Q(s,a)$）。
&lt;ul&gt;
&lt;li&gt;如果我们把完美的 $Q^*$ 函数学出来了，决策就超级简单：每次只选分最高的那个动作（Argmax）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;典型的算法是 DQN（Deep Q-Network）。&lt;/li&gt;
&lt;li&gt;这里学的是**最优策略（Optimal Policy）**的价值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. Actor-Critic (演员-评论家算法)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;怎么做：&lt;/strong&gt; 结合了前两种方法。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Actor（演员/策略）：&lt;/strong&gt; 负责根据当前状态做动作（像 Policy Gradient）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Critic（评论家/价值）：&lt;/strong&gt; 负责给演员的表现打分（像 Value-based）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键区别：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;注意 slide 里的细节：Value-based 学的是&lt;strong&gt;最优策略&lt;/strong&gt;的价值；而 Actor-Critic 里的 Critic 学的是&lt;strong&gt;当前策略&lt;/strong&gt;的价值。&lt;/li&gt;
&lt;li&gt;Critic 告诉 Actor：“你刚才那一步虽然拿了分，但比平均水平低（Advantage 是负的），下次少这么干。”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;比喻：&lt;/strong&gt; 运动员（Actor）和教练（Critic）。运动员负责跑，教练负责看录像并告诉运动员“刚才那个弯道你跑慢了”。运动员根据教练的反馈来调整姿势。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. Model-based RL (基于模型的 RL)&lt;/h4&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; 前面三种都是 Model-Free（无模型），也就是不关心世界是怎么运作的，只管试错。Model-based 则试图先在这个环境中学习一个&lt;strong&gt;世界模型（World Model）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;即学习 $p(s_{t+1} | s_t, a_t)$：如果我在 $s_t$ 做 $a_t$，下一个状态会变成什么？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;学会模型后能干嘛？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Planning (规划)：&lt;/strong&gt; 既然知道世界怎么变，我就不用真去跑，直接在脑子里推算（比如 MCTS 蒙特卡洛树搜索，AlphaGo 就用了这个）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improve Policy：&lt;/strong&gt; 在“假想”的环境里训练策略，省去了在真实世界试错的高昂成本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tradeoffs&lt;/h3&gt;
&lt;h4&gt;样本效率 (Sample Efficiency)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;“为了训练出一个好策略，我需要与环境交互多少次？”&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;1. 什么是样本效率？&lt;/h5&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; 需要玩几百万、几亿局才能学会（比如早期的 AI）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键点：&lt;/strong&gt; &lt;strong&gt;&quot;Wall clock time is not the same as efficiency!&quot;&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;有些算法（如 Model-based）虽然需要的&lt;strong&gt;样本少&lt;/strong&gt;（玩得少），但&lt;strong&gt;计算量大&lt;/strong&gt;（脑子想得多），所以跑得不一定快。&lt;/li&gt;
&lt;li&gt;有些算法（如 PPO）虽然跑得快（计算简单），但需要海量的样本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. 效率排行榜 (从高到低)&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Model-based (Shallow &amp;#x26; Deep):&lt;/strong&gt; &lt;strong&gt;效率最高&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原因：&lt;/strong&gt; 因为它学会了世界模型，可以在“脑子里”（模拟环境）训练，不需要每次都去真实世界撞墙。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Off-policy Q-learning (如 DQN, SAC):&lt;/strong&gt; &lt;strong&gt;效率较高&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原因：&lt;/strong&gt; 它有 Replay Buffer（回放池），以前的经验可以反复利用，榨干每一条数据的价值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actor-Critic:&lt;/strong&gt; &lt;strong&gt;中等&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;介于两者之间，通常也是 Off-policy 或近端策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-policy Policy Gradient (如 REINFORCE, PPO):&lt;/strong&gt; &lt;strong&gt;效率较低&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原因：&lt;/strong&gt; 它有“洁癖”，一旦策略更新了一点点，旧数据就作废了，必须重新去环境里采集新数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evolutionary / Gradient-free:&lt;/strong&gt; &lt;strong&gt;效率最低&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原因：&lt;/strong&gt; 类似于“瞎猜”或者遗传算法，几乎不利用梯度信息，纯靠大量的尝试来碰运气。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;稳定性与易用性 (Stability and Ease of use)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;“这个算法容易训练成功吗？会不会训练着训练着就崩了？”&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;1. 为什么这是个问题？(Why is any of this even a question???)&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;监督学习 (Supervised Learning)：&lt;/strong&gt; 非常稳定。因为由于数据是固定的，这就是在做一个简单的梯度下降，就像下山一样，总能走到谷底。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强化学习 (RL)：&lt;/strong&gt; 非常不稳定。因为**“数据分布取决于策略”**。你更新了策略，数据分布就变了；数据变了，梯度就变了。这就像你在下山，但山体本身在不断地震和变形。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. 各流派的“暴雷”风险：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Value Function Fitting (基于价值，如 Q-Learning):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理：&lt;/strong&gt; 最小化 Bellman Error（拟合误差）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风险：&lt;/strong&gt; &lt;strong&gt;At worst, doesn&apos;t optimize anything.&lt;/strong&gt; 在深度强化学习（非线性）中，Q-Learning &lt;strong&gt;不保证收敛&lt;/strong&gt;。你可能训练了一周，Loss 也没降下去，或者 Q 值发散到无穷大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;痛点：&lt;/strong&gt; 拟合误差小 $\neq$ 策略得分高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model-based RL:&lt;/strong&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;strong&gt;模型准 $\neq$ 策略好。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;即使模型只有一点点误差，经过多步推演后误差会指数级放大（Butterfly Effect），导致策略学偏。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy Gradient (策略梯度):&lt;/strong&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;strong&gt;真正&lt;/strong&gt;在做“梯度下降（上升）”去优化我们想要的目标（Reward）的算法。&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;h4&gt;假设条件 (Assumptions)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;“这些算法对环境有什么苛刻的要求？”&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;假设 #1: 完全可观测 (Full Observability)&lt;/strong&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;strong&gt;Value-based 方法&lt;/strong&gt;（Q-Learning）非常依赖这个。如果你看不全，状态 $s$ 就不准确，Q 值就算不对。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;假设 #2: 回合制学习 (Episodic Learning)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义：&lt;/strong&gt; 假设游戏是可以重置的（有 Game Over 和 Restart）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;谁依赖它：&lt;/strong&gt; &lt;strong&gt;纯 Policy Gradient 方法&lt;/strong&gt;（如 REINFORCE）通常需要跑完一整局才能算梯度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;谁无所谓：&lt;/strong&gt; 某些 Value-based 方法可以处理连续不断的任务（Continuing tasks）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;假设 #3: 连续性与平滑性 (Continuity or Smoothness)&lt;/strong&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;strong&gt;Model-based&lt;/strong&gt; 和 &lt;strong&gt;连续控制的 Actor-Critic&lt;/strong&gt;。因为它们需要求导，如果环境是不连续的、跳跃的，导数就没法求，模型也就学不了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><img src="/_astro/11.D9kPkauM.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.D9kPkauM.png" length="0" type="image/png"/></item><item><title>CS285 Lecture2 Imitation Learning</title><link>https://laurie-hxf.xyz/blog/cs285-l2-imitation-learning</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/cs285-l2-imitation-learning</guid><description>CS285 Imitation Learning</description><pubDate>Sat, 29 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.DictLvZ0.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;行为克隆&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-10-03%20at%2021.52.41.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;假设我们一开始有一个专家，他每次行驶同一个路线，重复很多遍。每一次他都有可能有不同的操作，比如在相同的时刻t，前一次和后一次在的位置不一样，他的操作也不一样，但是最终都能顺利行驶完这个路线。于是乎，我们就有了一个数据集，
$$
1：(o_{t-1}, a_{t-1}), (o_t, a_t), (o_{t+1}, a_{t+1})
$$
$$
2：(o_{t-1}, a_{t-1}), (o_t, a_t), (o_{t+1}, a_{t+1})
$$
$$
3：(o_{t-1}, a_{t-1}), (o_t, a_t), (o_{t+1}, a_{t+1})
$$
$$
...
$$
前面的1，2，3表示第几次行驶这个路线，然后括号内的一对表示当前时刻 t 观察到场景 $o_t$ 之后采取的动作$a_t$ 。&lt;/p&gt;
&lt;p&gt;由于每次时间t，你观察到的$o_t$不一定一样，所以这里就有一个概率分布，可能50次是这种，另外20次是那种。图中的$P_{data}(o_t)$表示的就是t时刻观察场景o的概率分布。&lt;/p&gt;
&lt;p&gt;$\pi_\theta(a_t|o_t)$表示的就是策略 $\pi_\theta$ 在给定 $o_t$ 的情况下输出$a_t$ 的概率。$P_{\pi_\theta}(o_t)$ 表示的就是我采用策略$\pi_\theta$的情况下我的概率分布会是什么。&lt;/p&gt;
&lt;p&gt;当然按理来说这两个概率分布P都应该是一条点状云，图中从中抽样出一次的数据就用线来表示了，只是方便演示。&lt;/p&gt;
&lt;p&gt;行为克隆就是将训练当成一个有监督训练，目标就是最大化给定专家数据中的输入$o_t$，输出对应的动作$a_t$的概率最大。
$$
\max_{\theta} \mathbb{E}_{\mathbf{o}&lt;em&gt;t \sim p&lt;/em&gt;{\text{data}}(\mathbf{o}&lt;em&gt;t)} [\log \pi&lt;/em&gt;{\theta}(\mathbf{a}_t | \mathbf{o}_t)]
$$
我们从数据集中随机抽一些数据可能是第4次的时间步5，第6次的时间步3等等，将他们抽出来让模型去预测，让他预测到对应动作的概率最大。&lt;/p&gt;
&lt;p&gt;于是这里就有一个问题，在有监督中，我们的先验假设就是数据点之间是独立同分布的，但是很显然这里不可能，这里就是一个隐患，模型学到的很可能就是单帧的预测，而不是连续的依赖。&lt;/p&gt;
&lt;p&gt;以及如果出现了以前没有见过的数据，那很可能模型的表现就会很不好，而且这种错误会不断累加。&lt;/p&gt;
&lt;p&gt;问题就是我们的训练数据都是在$P_{data}$中抽取的，而我们实际在乎的是在测试中的表现，然后由于行为克隆是本身上一次的决策会影响下一次的决策，这里面的错误可能不断累加，导致他最终的分布会不断的偏离原本的专家分布，这就叫做&lt;strong&gt;分布偏移&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;于是乎我们抛弃原本的思路，换一种思路
$$
c(\mathbf{s}_t, \mathbf{a}_t) =
\begin{cases}
0 &amp;#x26; \text{if } \mathbf{a}_t = \pi^{\ast}(\mathbf{s}&lt;em&gt;t) \
1 &amp;#x26; \text{otherwise}
\end{cases}
$$
$\pi^{\ast}(\mathbf{s}&lt;em&gt;t)$为专家在这种情况下做的动作，如果我们的策略预测的动作和专家的一样，那我们就不会有惩罚，否则就惩罚。然后我们的优化目标就变成
$$
\text{minimize} \quad \mathbb{E}&lt;/em&gt;{\mathbf{s}&lt;em&gt;t \sim p&lt;/em&gt;{\pi&lt;/em&gt;{\theta}}(\mathbf{s}_t)} [c(\mathbf{s}&lt;em&gt;t, \mathbf{a}&lt;em&gt;t)]
$$
注意这种情况下，我们的分布是在$p&lt;/em&gt;{\pi&lt;/em&gt;\theta}$下面.&lt;/p&gt;
&lt;h3&gt;Some analysis&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./Screenshot%202025-10-04%20at%2023.25.02.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;假设我们用自监督有一个很好的模型，在训练数据集中他的预测结果很精确。
$$
\text{assume: } \pi_{\theta}(\mathbf{a} \ne \pi^{\ast}(\mathbf{s})|\mathbf{s}) \le \epsilon ;
(\text{for all } \mathbf{s} \in \mathcal{D}_{\text{train}})
$$
他的预测概率失败概率小于$\epsilon$&lt;/p&gt;
&lt;p&gt;假设我的总时长为T，我们可以计算出他的平均惩罚的上界是多少。右边的公式就是假设我从一开始就预测错误，然后之后的动作全都跟着预测错误，然后第一步错的概率是$\epsilon$ ，然后后面跟着全错他的惩罚就是T，所以第一部分的期望就是$\epsilon T$ ，然后以此类推。第二项就是第一步没错的情况下概率是$1-\epsilon$ ,然后第二步开始错了$\epsilon(T-1)$ 一直往后。&lt;/p&gt;
&lt;p&gt;$$
\underbrace{E\left[\sum_tc(\mathbf{s}&lt;em&gt;t,\mathbf{a}&lt;em&gt;t)\right]}&lt;/em&gt;{O(\epsilon T^2)}\leq\underbrace{\epsilon T+(1-\epsilon)(\epsilon(T-1)+(1-\epsilon)(\ldots))}&lt;/em&gt;{T\text{ terms, each }O(\epsilon T)}
$$
所以说可以看到他最坏情况下复杂度是$O(\epsilon T^2)$ ,这显然不是一个很好的结果，我们可以接受的应该是线性的，高于线性的可能就不是这么好。&lt;/p&gt;
&lt;h3&gt;more analysis&lt;/h3&gt;
&lt;p&gt;上面的分析我们要求的条件太严格了，我们要求训练集中的所有数据他的出错率都要小于$\epsilon$，于是乎我们放宽一点，更general一点。&lt;/p&gt;
&lt;p&gt;我们假设我们从$P_{train}(s)$分布里面抽一些出来s，然后假设他的平均出错小于$\epsilon$，这种情况下更符合现实。
$$
E_{p_{\mathrm{train}}(\mathbf{s})}[\pi_\theta(\mathbf{a}\neq\pi^\star(\mathbf{s})|\mathbf{s})]\leq\epsilon
$$
于是乎我们可以进行如下推导
$$
\begin{aligned}
&amp;#x26; p_\theta(\mathbf{s}&lt;em&gt;t)=(1-\epsilon)^t{
\begin{array}
{c}
\end{array}}p&lt;/em&gt;{\mathrm{train}}(\mathbf{s}&lt;em&gt;t)+(1-(1-\epsilon)^t){p&lt;/em&gt;{\mathrm{mistake}}(\mathbf{s}&lt;em&gt;t)}&lt;/em&gt;{
\begin{array}
{c}
\end{array}}
\end{aligned}
$$
我们可以先将他的概率分布分成两部分，第一部分就是前t步都符合$P_{train}$概率分布他的概率就是$(1-\epsilon)^t$ 之后就是不符合的部分。$P_{mistake}$是个很复杂的分布，我们不知道他是什么。&lt;/p&gt;
&lt;p&gt;之后左右两边减去一个$P_{train}$ 可以化简一下，这里的2感觉有点奇怪，可能是为了进一步放缩一下。
$$
|p_\theta(\mathbf{s}&lt;em&gt;t)-p&lt;/em&gt;{\mathrm{train}}(\mathbf{s}&lt;em&gt;t)|=(1-(1-\epsilon)^t)|p&lt;/em&gt;{\mathrm{mistake}}(\mathbf{s}&lt;em&gt;t)-p&lt;/em&gt;{\mathrm{train}}(\mathbf{s}_t)|\leq2(1-(1-\epsilon)^t)
$$
之后根据伯努力不等式再放缩一下，$\text{useful identity: }(1-\epsilon)^t\geq1-\epsilon t\mathrm{~for~}\epsilon\in[0,1]$
右边部分就又$\leq2\epsilon t$&lt;/p&gt;
&lt;p&gt;之后进一步推导，我们想知道他的平均的惩罚是多少
$$
\begin{aligned}
\sum_tE_{p_\theta(\mathbf{s}&lt;em&gt;t)}[c_t]=\sum_t\sum&lt;/em&gt;{\mathbf{s}&lt;em&gt;t}p&lt;/em&gt;\theta(\mathbf{s}_t)c_t(\mathbf{s}&lt;em&gt;t) &amp;#x26; \leq\sum_t\sum&lt;/em&gt;{\mathbf{s}&lt;em&gt;t}(p&lt;/em&gt;{\mathrm{train}}(\mathbf{s}_t)c_t(\mathbf{s}&lt;em&gt;t)+|p&lt;/em&gt;\theta(\mathbf{s}&lt;em&gt;t)-p&lt;/em&gt;{\mathrm{train}}(\mathbf{s}&lt;em&gt;t)|c&lt;/em&gt;{\mathrm{max}}) \
&amp;#x26;
\begin{aligned}
\leq\sum_t(\epsilon+2\epsilon t)\leq\epsilon T+2\epsilon T^2
\end{aligned} \
&amp;#x26; O(\epsilon T^2)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;第一个小于号那里他将$p_\theta$变成了$p_{train}$，然后补上他们之间的差值，具体过程就是
$$
\begin{aligned}
p_\theta(\mathbf{s}_t)c_t(\mathbf{s}&lt;em&gt;t)&amp;#x26;=p&lt;/em&gt;{train}(\mathbf{s}_t)c_t(\mathbf{s}&lt;em&gt;t)+p&lt;/em&gt;\theta(\mathbf{s}_t)c_t(\mathbf{s}&lt;em&gt;t)-p&lt;/em&gt;{train}(\mathbf{s}_t)c_t(\mathbf{s}&lt;em&gt;t)\
&amp;#x26;=p&lt;/em&gt;{train}(\mathbf{s}_t)c_t(\mathbf{s}&lt;em&gt;t)+|p&lt;/em&gt;\theta(\mathbf{s}&lt;em&gt;t)-p&lt;/em&gt;{\mathrm{train}}(\mathbf{s}&lt;em&gt;t)|c_t(s_t)\
&amp;#x26;\le p&lt;/em&gt;{train}(\mathbf{s}_t)c_t(\mathbf{s}&lt;em&gt;t)+|p&lt;/em&gt;\theta(\mathbf{s}&lt;em&gt;t)-p&lt;/em&gt;{\mathrm{train}}(\mathbf{s}&lt;em&gt;t)|c&lt;/em&gt;{max}
\end{aligned}
$$
然后前面那一部分就是我们一开始的假设就$\le \epsilon$ ,后面那一部分就是之前推导出来的$\leq2\epsilon t$
进一步计算我们也可以得到他的上界复杂度也是$O(\epsilon T^2)$ 。&lt;/p&gt;
&lt;h3&gt;Paradox&lt;/h3&gt;
&lt;p&gt;先前我们将行为克隆比做走钢丝的人，一旦有一步走错，你就没有恢复的可能。这是因为标准的行为克隆，它的训练数据 $D_train$ 全是专家完美驾驶的黑色轨迹，数据里根本没有任何关于“如何从错误中恢复”的信息。你不可能学会你从未见过的东西。于是乎这里就有一个悖论：如果数据包含更多的错误（和恢复），模仿学习的效果可能会更好。&lt;/p&gt;
&lt;p&gt;不止是包含更多错误及恢复，数据增强也是可以的，比如训练一个无人机穿过森林的时候，人可以走一遍路线，然后佩戴一个左中右三个摄像头的头盔，然后左边摄像头采集的数据标注就是右转，右边的摄像头采集的数据标注就是左转，中间的就标注直走，用这些数据来训练无人机&lt;/p&gt;
&lt;h3&gt;Why might we fail to fit the expert?&lt;/h3&gt;
&lt;p&gt;为什么对于简单的行为克隆，基于当前的图像输入，然后获得输出他也不是很能很好的拟合专家呢，可以从两个方面入手&lt;/p&gt;
&lt;h4&gt;Non-Markovian behavior&lt;/h4&gt;
&lt;p&gt;人类的行为很多时候都不是一个马尔可夫的决策过程，当做出决策的时候人们不会只根据当前的状态来做出反应，而是强烈依赖历史（例如刚刚看到盲区有车、刚被人加塞情绪受影响等），因此同一画面在不同上下文下可能做出不同行为&lt;/p&gt;
&lt;p&gt;解决这种问题可能就是用CNN之类的编码图像然后把编码出的向量输入进一个序列模型（LSTM、Transformer、时序卷积等）把一段历史观测编码进策略，让策略根据整段历史输出当前动作&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;h4&gt;Multimodal behavior&lt;/h4&gt;
&lt;p&gt;当我们使用L2回归($L=∥a_{expert}​−\hat{a}(s)∥^2$)来训练模型的时候，他就会假设 $a_{expert}$ 的分布就是一个单峰的正态分布，这是为什么呢？&lt;/p&gt;
&lt;p&gt;首先假设我们的回归模型如下：&lt;/p&gt;
&lt;p&gt;$$
y_i = f_\theta(x_i) + \epsilon_i, \quad i=1,\dots,n
$$
其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x_i$：第 $i$ 个样本输入&lt;/li&gt;
&lt;li&gt;$y_i$：对应的真实输出&lt;/li&gt;
&lt;li&gt;$f_\theta(x_i)$：模型对 $x_i$ 的预测（参数是 $\theta$，可以是线性模型、神经网络等）&lt;/li&gt;
&lt;li&gt;$\epsilon_i$：噪声 / 误差&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键假设：&lt;/strong&gt; 误差服从高斯分布，且相互独立、同分布 (i.i.d.)：&lt;/p&gt;
&lt;p&gt;$$
\epsilon_i \sim \mathcal{N}(0, \sigma^2)
$$&lt;/p&gt;
&lt;p&gt;因为 $\epsilon_i = y_i - f_\theta(x_i)$，所以给定 $x_i$ 和参数 $\theta$ 时，$y_i$ 的条件分布是：&lt;/p&gt;
&lt;p&gt;$$
y_i \mid x_i, \theta \sim \mathcal{N}\big(f_\theta(x_i), \sigma^2\big)
$$&lt;/p&gt;
&lt;p&gt;根据高斯分布的概率密度函数 (PDF)：&lt;/p&gt;
&lt;p&gt;$$
p(y_i \mid x_i, \theta) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left( -\frac{(y_i - f_\theta(x_i))^2}{2\sigma^2} \right)
$$&lt;/p&gt;
&lt;p&gt;这表示：预测值 $f_\theta(x_i)$ 越接近真实值 $y_i$，这个样本在当前模型参数下出现的概率（似然）就越大。&lt;/p&gt;
&lt;p&gt;假设各个样本相互独立，那么总体的 &lt;strong&gt;似然函数&lt;/strong&gt; 为所有样本概率的乘积：&lt;/p&gt;
&lt;p&gt;$$
L(\theta) = \prod_{i=1}^{n} p(y_i \mid x_i, \theta) = \prod_{i=1}^{n} \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left( -\frac{(y_i - f_\theta(x_i))^2}{2\sigma^2} \right)
$$&lt;/p&gt;
&lt;p&gt;我们的目标是做 &lt;strong&gt;最大似然估计 (MLE)&lt;/strong&gt;，即找到一组参数 $\theta$，使得观测到当前数据的概率最大：&lt;/p&gt;
&lt;p&gt;$$
\theta^* = \arg\max_\theta L(\theta)
$$&lt;/p&gt;
&lt;p&gt;由于对数函数 $\log$ 是单调递增的，最大化 $L(\theta)$ 等价于最大化 $\log L(\theta)$（对数似然）：&lt;/p&gt;
&lt;p&gt;$$
\log L(\theta) = \sum_{i=1}^{n} \left[ \log \frac{1}{\sqrt{2\pi\sigma^2}} - \frac{(y_i - f_\theta(x_i))^2}{2\sigma^2} \right]
$$&lt;/p&gt;
&lt;p&gt;我们将常数项和与 $\theta$ 无关的部分分离出来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一项 $\log \frac{1}{\sqrt{2\pi\sigma^2}}$ 是常数，不依赖于 $\theta$。&lt;/li&gt;
&lt;li&gt;唯一包含 $\theta$ 的是第二项（平方误差项）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简化后可写为：&lt;/p&gt;
&lt;p&gt;$$
\log L(\theta) = C - \frac{1}{2\sigma^2} \sum_{i=1}^{n} (y_i - f_\theta(x_i))^2
$$&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(其中 $C$ 是与 $\theta$ 无关的常数)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;我们要最大化对数似然：&lt;/p&gt;
&lt;p&gt;$$
\theta^* = \arg\max_\theta \left[ C - \frac{1}{2\sigma^2} \sum_{i=1}^{n} (y_i - f_\theta(x_i))^2 \right]
$$&lt;/p&gt;
&lt;p&gt;注意公式中的&lt;strong&gt;负号&lt;/strong&gt;。要让整个式子最大，必须让减去的后面那部分&lt;strong&gt;最小&lt;/strong&gt;。同时，系数 $\frac{1}{2\sigma^2}$ 是正数，不影响极值点的位置。&lt;/p&gt;
&lt;p&gt;因此，问题等价于：&lt;/p&gt;
&lt;p&gt;$$
\theta^* = \arg\min_\theta \sum_{i=1}^{n} (y_i - f_\theta(x_i))^2
$$&lt;/p&gt;
&lt;p&gt;这就是标准的 &lt;strong&gt;最小二乘 (L2) 损失&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\boxed{ \mathcal{L}&lt;em&gt;{L2}(\theta) = \sum&lt;/em&gt;{i=1}^{n} (y_i - f_\theta(x_i))^2 }
$$&lt;/p&gt;
&lt;p&gt;假设误差服从高斯分布，使用最大似然估计 (MLE) 求得的最优参数，在数学上等价于最小化误差的平方和 (L2 Loss)。&lt;/p&gt;
&lt;p&gt;所以说当我们使用L2回归的时候就是假设我们的$a_{expert}$是一个单峰的正态分布，所以训练的目标就是不管数据的分布如何，单峰还是多峰，我都用一个单峰的正态分布根据最大似然去近似，输出其实就是这个正态分布的均值，目标就是让模型输出的这个均值所对应的正态分布能尽可能的拟合真实分布。&lt;/p&gt;
&lt;p&gt;但是，如果实际 $a_{expert}$ 是多峰的，假设有左右两个峰，那么我们的模型就是输出左右两个峰的中间值，这样他对应的正态分布可以最可能的贴合这个多峰的分布。这就有一个问题，可能左右峰对应的值是合理的，但是中间的输出值就是不合理的。就比如滑雪时中间有一颗树，我要绕过这个树，从左绕和从右绕都可以，面对同一个场景有两种动作，这些都在数据集里面，那么他的数据分布就是两个峰，如果这时候模型输出中间值，那就是不合理的。&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;h5&gt;Expressive continuous distributions&lt;/h5&gt;
&lt;h6&gt;mixture of gaussians（混合高斯）&lt;/h6&gt;
&lt;p&gt;第一种就是用混合高斯模型来表示 $a_{expert}$ 的分布, 用多元高斯的最大似然来当作损失函数
$$
\mathcal{L} = -\log \sum_{k=1}^{K} w_k(\boldsymbol{s}) \mathcal{N}(\boldsymbol{a}_{\text{expert}} | \boldsymbol{\mu}_k(\boldsymbol{s}), \boldsymbol{\Sigma}_k(\boldsymbol{s}))
$$
模型的输出就是&lt;/p&gt;
&lt;p&gt;| 项目            | 维度                     | 含义               |
| ------------- | ---------------------- | ---------------- |
| $\mu_k(s)$    | K × action_dim         | 每个模式的均值          |
| $\Sigma_k(s)$ | K × action_dim (或对角矩阵) | 每个模式的方差          |
| $w_k(s)$      | K 个                    | 每个模式的混合权重（sum=1） |
但是他的问题就是你的K是一个超参数，如果你的模态很多比如机器人可以上千种，那就很难训练了&lt;/p&gt;
&lt;h6&gt;latent variable models(潜变量模型)&lt;/h6&gt;
&lt;p&gt;简单来说就是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-scss&quot;&gt;          z (隐藏策略)
          ↓
s (状态) → policy → a (动作)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1. 以 CVAE + BC 为例（训练阶段）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在训练时，模型主要做两件事：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 编码器（Recognition Model）：学习 $z$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
q_\phi(z \mid s, a)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义：&lt;/strong&gt; 看到专家在状态 $s$ 采取了动作 $a$，逆向推断这个动作属于哪种“隐藏策略” $z$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;左绕树 $\rightarrow$ 编码器可能输出 $z \approx (-1, 0.2)$&lt;/li&gt;
&lt;li&gt;右绕树 $\rightarrow$ 编码器可能输出 $z \approx (1.1, -0.3)$&lt;/li&gt;
&lt;li&gt;$z$ 把动作背后的“模式”编码了出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;② 解码器（Policy）：学习从 $z$ 生成动作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
p_\theta(a \mid s, z)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义：&lt;/strong&gt; 给定状态 $s$ 和隐藏策略 $z$，生成专家会采取的动作 $a$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;输入 $s$（树在前面）+ $z$（左绕）$\rightarrow$ 输出负方向动作&lt;/li&gt;
&lt;li&gt;输入 $s$（树在前面）+ $z$（右绕）$\rightarrow$ 输出正方向动作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;③ 训练目标&lt;/strong&gt;
我们在训练中最大化 Evidence Lower Bound（ELBO）：
$$
\mathcal{L} = \mathbb{E}&lt;em&gt;{q&lt;/em&gt;\phi(z|s,a)}[\log p_\theta(a|s,z)] - \text{KL}(\dots)
$$
这背后的机制是：模型强制让 $z$ 存储“多模态结构”，让 $p(a|s,z)$ 存储“特定模式下的动作生成”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 推理（Test）阶段：$z$ 变成“策略选择器”&lt;/strong&gt;
这一步最关键。推理时没有专家动作 $a$，所以不能使用编码器，操作如下：
&lt;strong&gt;① 从先验分布采样&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
z \sim p(z) = \mathcal{N}(0, I)
$$
&lt;strong&gt;② 生成动作&lt;/strong&gt;
$$
a = p_\theta(a \mid s, z)
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 多模态策略的体现&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果采样多个不同的 $z$，会得到多个不同的动作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;采样 $z_1 \rightarrow$ 左绕动作&lt;/li&gt;
&lt;li&gt;采样 $z_2 \rightarrow$ 右绕动作&lt;/li&gt;
&lt;li&gt;采样 $z_3 \rightarrow$ 速度慢一点&lt;/li&gt;
&lt;li&gt;采样 $z_4 \rightarrow$ 速度快一点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是引入潜变量 $z$ 解决多模态问题的根本原因。&lt;/p&gt;
&lt;h6&gt;diffusion models（扩散模型）&lt;/h6&gt;
&lt;p&gt;不断去噪最终生成动作
&lt;img src=&quot;./Screenshot%202025-11-29%20at%2012.26.48.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;multi-task learning&lt;/h3&gt;
&lt;p&gt;主要讲了一种让行为克隆更“抗错”的思路：用多任务/多目标的“目标条件行为克隆”（goal‑conditioned behavioral cloning，GCB）和回顾重标（hindsight relabeling）&lt;/p&gt;
&lt;h4&gt;核心想法&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;传统行为克隆只学“从起点到固定目标 P1 的最佳轨迹”，数据覆盖的状态分布很窄，因此一旦策略犯错偏离演示轨迹，就会落到没见过的状态，误差会不断放大（分布偏移）。​&lt;/li&gt;
&lt;li&gt;讲者提出：与其费劲让专家“故意犯错、再恢复”，不如收集专家去很多不同目标位置的演示，然后训练一个“输入当前状态 s 和期望目标状态 g（如轨迹最后一帧）→ 输出动作 a”的目标条件策略，这样可以显著增加覆盖的状态空间并利用更多次优数据。​&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;目标条件行为克隆与回顾重标&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;数据收集：拿到很多轨迹 demo，每条只是状态–动作序列，并不显式告诉“任务是什么”。假设“这条轨迹对到达它最后到达的状态 $s_T$ 是一个好示范”。​&lt;/li&gt;
&lt;li&gt;训练方式：对每条 demo，把最后一个状态 sTs_TsT 当作“目标 g”，把“当前状态 s、目标 g”喂给策略，监督它输出专家动作 a，相当于最大化 $\log \pi_\theta(a \mid s, g)$。​&lt;/li&gt;
&lt;li&gt;这相当于用“后见之明”（hindsight）：不管专家当时本来想干什么，都当作是在“成功到达最后状态 g 的演示”，因此可以从大量“玩耍式”数据中提取出覆盖很广的目标到达能力。​&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;多任务带来的好处和理论问题&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;好处：
&lt;ul&gt;
&lt;li&gt;由于有很多不同目标，专家会访问更多多样的状态，因此策略更难遇到完全没见过的状态；即使用于特定目标 P1 时，也能在错误状态附近找到“类似于其他目标演示中的状态”，学会自我纠正。&lt;/li&gt;
&lt;li&gt;可以充分利用次优数据：即使专家没成功到 P1，只要成功到达某个别的位置，也能作为“到达那个位置”的正例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;理论上的隐患：这种方法在理论上产生了“两处分布偏移”（一个是策略状态分布 vs. 数据状态分布，另一个是由这种“后见之明标注目标”的方式引入的偏移）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;DAgger&lt;/h3&gt;
&lt;p&gt;分布偏移的本质是：训练时的数据分布 pdatapdata 和测试时在当前策略下访问到的状态分布 $p_{π_θ}$ 系统性不同，小错误会把策略带到“没见过的状态”，然后错误不断放大。&lt;/p&gt;
&lt;p&gt;可以看到，为了解决分布偏移的问题，也就是尽可能让$p_{data}=p_{π_θ}$，之前的策略都是让策略更少犯错，让$p_{π_θ}$ 去逼近$p_{data}$ 。但是我们可以反过来，通过在线收集数据让训练数据分布逐渐变成策略实际访问到的分布，让$p_{data}$去逼近。
&lt;img src=&quot;./Screenshot%202025-11-29%20at%2014.04.58.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一步，用专家 demonstration 先训练一个初始策略，然后在真实环境中运行这个策略，记录它看到的所有 observation（状态）。​&lt;/li&gt;
&lt;li&gt;第二步，请专家对这些 observation 逐一打标签：如果是 TA 操作，会在每个状态下采取什么动作，由此得到一个“基于当前策略分布”的带标签数据集。&lt;/li&gt;
&lt;li&gt;第三步，把这些新数据和原本示范数据 union 起来，重新训练策略，再次上线跑、再收集、再标注、再聚合，如此循环；可以证明数据中的状态分布会逐步逼近当前策略实际访问到的分布，理论上可以把错误界从随时间的二次增长降成线性增长，但代价是必须能不断获得这些额外专家标签。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是有一个主要问题是“离线打标签”的不自然：人类开车或操控无人机时有反应时间和时序感，在事后看单帧图像标动作，和真实操作时的决策可能不一致，这使得 DAGGER 在一些任务上不太自然。&lt;/p&gt;</content:encoded><img src="/_astro/11.DictLvZ0.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.DictLvZ0.png" length="0" type="image/png"/></item><item><title>VLA训练策略 论文笔记</title><link>https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87vla</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87vla</guid><description>Knowledge Insulating Vision-Language-Action Models - Train Fast, Run Fast, Generalize Better</description><pubDate>Fri, 15 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-08-13 19.25.04.CG_7WOzh.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-08-13%2019.25.04.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;LLMs的成功让人联想能不能把他的能力带到物理世界，于是就有VLA模型(vision language action model)。一开始的VLA的范式是自回归模型(Autoregressive)，本质上就是一个多模态的模型，就是一个VLM模型。输入视觉信息，语言指令，和机器人的状态信息然后就可以输出一系列token，这个token就是机器人下一步的动作。&lt;/p&gt;
&lt;p&gt;但是这个token是离散的，比如将关节能动180度分成180份，然后每份对应一个token之后，根据预测出的token来让机器人动对应的角度。这是其中一种tokenization的方法，还有更高级的tokenization将更多信息，动作压缩进一个token里面，可能一个token表示整组关节的组合动作，或者把一小段连续动作压缩成一个token。从而提高动作的连贯性和降低token的预测数量。&lt;/p&gt;
&lt;p&gt;然后这个模型是自回归的，意思就是当前的输出token取决于历史的token，就跟transformer一样，每次输出都要根据历史的token，直到最后生成完对应的token序列，也就是一个完整的动作序列。&lt;/p&gt;
&lt;p&gt;这样就有一些问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这个token是离散的，而机器人的动作是连续的。机器人的关节可以是0-180中的任意一个数字而不只是简单只有180个。所以这就造成机器人的运动可能不够灵活&lt;/li&gt;
&lt;li&gt;还有就是我的token是自回归的，我必须等前面的token生成完才能生成下一个，计算量大，然后慢，对于一些需要瞬时反应的一些场景，机器人就不够高频率&lt;/li&gt;
&lt;li&gt;物理系统比VLM复杂，机器人有多个摄像头，还有很多传感器。然而单纯的VLM模型可能只能包含图片和文字，所以可能需要在VLM的模型架构上进行修改来适配动作信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;于是针对前两个问题，就有人提出换个decoding，利用VLM模型的输出外加一个“动作专家”（action experts）或者“连续输出头”（continuous output heads），用于输出连续的控制。比较出名的就是diffusion prediction heads or flow-matching。用diffusion和flow-match用来连续输出。其一他们可以输出连续的数值，其二他们不像自回归，他们可以直接一次性输出所有的token。&lt;/p&gt;
&lt;p&gt;但是这个又有问题，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;训练变慢：这些新加的模块有很多参数，而且训练目标（loss）更复杂，所以整体训练过程往往比传统的自回归模型慢很多，需要更多时间才能收敛。&lt;/li&gt;
&lt;li&gt;知识迁移变差：这些结构在微调时，模型没法很好地继承/迁移来自互联网大数据（即VLM预训练）获得的知识，导致泛化和理解能力有所下降。这种现象叫做“web data transfer降低”。本质原因也是文章想要探讨的就是这个action expert是保留了还是削弱了VLM的语义信息，他对VLA训练有什么影响&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文章就告诉我们模型不听语言指令的话可能是由于action expert train from scratch，一开始的随机初始化在梯度下降的时候会污染原本预训练好的VLM的权重。&lt;/p&gt;
&lt;p&gt;还有问题就是一开始说的VLM一开始并没有在机器人的数据里面训练，直观上来说，最简单的就是冻结VLM的预训练权重，然后只训练action expert，但是尝试之后就是表现并不好。&lt;/p&gt;
&lt;p&gt;文章的想法就是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;π0 模型引入了“continuous action expert”，可以捕捉复杂的连续动作分布，在推理时效率高，能做高频精细控制（比如叠衣服）。但实验发现，直接对接 action expert，会导致语言理解能力下降、训练速度变慢，因为 action expert 的梯度会干扰原本预训练的 VLM（视觉语言模型）主干网络。&lt;/li&gt;
&lt;li&gt;π0-FAST 通过把动作用 DCT（离散余弦变换）进行分桶、离散化，用 token 表达复杂动作，可以更高效地离散动作token，但推理时还是要自回归生成（速度慢），而且这种离散方案对细腻、动态任务表现变差。&lt;/li&gt;
&lt;li&gt;π0.5 则先只用 FAST token 动作训练主干网络，然后在后期（post-training）引入 action expert 训练连续动作（用 joint-training 方式，主要用于移动操控任务）。这种方式相当于主干网络已经适应了机器人控制任务，再引入 action expert，但流程是两阶段的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本论文的贡献是进一步把 π0.5 的思路“正规化”并扩展，提出一种单阶段训练方案：主干网络用离散 token 训练以获得强表达能力，同时 action expert 同步训练连续动作，但用“知识绝缘”方法保护主干网络不被 action expert 的梯度干扰。这种方式融合了两者优点——训练速度快、知识保留好、又能支持高频率的连续动作控制。&lt;/p&gt;
&lt;h2&gt;自回归模型&lt;/h2&gt;
&lt;p&gt;之前的自回归范式就是一个多模态的transformer架构，流程就是我一开始有多个模态(图片，文字，状态)，然后我分别用不同的encoder来处理这些模态之后得到对应的相同长度的特征向量，这些特征向量就可以当作是这个图里面的X&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./111.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;乘一个query matrix，key matrix之后得到query vector，key vector，然后把一部分mask掉之后就得到相似度矩阵，之后softmax就得到attention score，最后加权平均value vector得到输出。有多少个输入我就有多少个输出，我们只取动作对应的vector进行下一步操作，可能是简单的mlp根据这个动作token生成多个具体的关节之类的token，从而使机器人运动。&lt;/p&gt;
&lt;p&gt;然后的话，我们提取出来的动作token作为新的X并入进去，再一轮计算query vector，key vector，value vector得到下一个动作token，直到结尾。所以这就是自回归，下一个动作token依赖历史的动作，随着历史token越来越多，计算的复杂度也越来越多，计算时间也就越来越慢。&lt;/p&gt;
&lt;p&gt;每次生成一个token他就执行对应的动作，也就是边生成边执行。于是乎，机器人的延迟就在于两个token之间的计算时间。而且随着动作做的越来越多他的延迟就越来越大。&lt;/p&gt;
&lt;h2&gt;Action expert架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;actionexpert.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个也是一个Transformer架构，可以看成分成两个部分一部分VLM，另一部分action expert。这两个集成在一个多模态Transformer中，然后这两个部分分别有各自的$W_Q,W_K,W_V$。&lt;/p&gt;
&lt;p&gt;VLM部分他就输入各种模态的数据，输出就是对应的文字token和离散的动作token。&lt;/p&gt;
&lt;p&gt;然后Action expert，他的输入就是一系列连续动作向量，也就是我们把视频里的动作切分成很多步然后对应不同的时间步，然后每一个向量里面的数值都是连续精细的小数。我们把这个经过加噪之后得到一系列对应的加噪动作向量作为这个他的输入。关键就在于他得到的query vector先乘他自己的key vector得到相似度分数，然后再用他的query vector去乘VLM的key vector，最后经过softmax之后分别得到两组attention score，然后乘对应的自己的Value vector和VLM的Value vector之后再加和得到最后的输出。&lt;/p&gt;
&lt;p&gt;公式表示就是
$$
softmax(Q_a*K_a)&lt;em&gt;V_a+softmax(Q_a&lt;/em&gt;K_b)*V_b
$$&lt;/p&gt;
&lt;p&gt;然后这一部分用的是flow matching。他的过程就是&lt;/p&gt;
&lt;p&gt;一个真实的、未来的动作块，它是一个连续向量的序列，我们称之为 &lt;code&gt;a_0_sequence = [a_0_1, a_0_2, ..., a_0_50]&lt;/code&gt;。
我们生成一个形状完全相同的纯噪声序列 &lt;code&gt;noise_sequence = [ω_1, ω_2, ..., ω_50]&lt;/code&gt;。
我们随机选择一个时间进度 &lt;code&gt;t&lt;/code&gt; (比如 &lt;code&gt;t=0.7&lt;/code&gt;)。然后，我们通过逐个向量线性插值来创建带噪声的输入序列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;input_vec_1 = 0.7 * ω_1 + (1 - 0.7) * a_0_1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;input_vec_2 = 0.7 * ω_2 + (1 - 0.7) * a_0_2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;&lt;code&gt;input_vec_50 = 0.7 * ω_50 + (1 - 0.7) * a_0_50&lt;/code&gt;
最终输入: 我们得到了一串50个带噪声的连续向量，我们称之为 &lt;code&gt;a_t_sequence&lt;/code&gt;。这&lt;strong&gt;就是&lt;/strong&gt;即将进入Transformer的初始输入序列。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过Transformer之后它会输出一个形状完全相同的新序列，我们称之为 &lt;code&gt;v_θ_sequence = [v_θ_1, v_θ_2, ..., v_θ_50]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;损失函数：
$$
L_{\mathrm{FM}}=\left|(\text{noise sequence}-a_{0_{sequence}})-v_{\theta_{sequence}}\right|^2
$$&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;&lt;code&gt;true_direction_1 = ω_1 - a_0_1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;true_direction_2 = ω_2 - a_0_2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;&lt;code&gt;true_direction_50 = ω_50 - a_0_50&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;我们得到了一个“标准答案”序列：&lt;code&gt;true_direction_sequence&lt;/code&gt;。&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;strong&gt;第一步的损失&lt;/strong&gt;: &lt;code&gt;|| true_direction_1 - v_θ_1 ||^2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二步的损失&lt;/strong&gt;: &lt;code&gt;|| true_direction_2 - v_θ_2 ||^2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第五十步的损失&lt;/strong&gt;: &lt;code&gt;|| true_direction_50 - v_θ_50 ||^2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最终总损失 &lt;code&gt;L_FM&lt;/code&gt; 就是所有这些损失的平均值或总和。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心思想就是我想要预测的是一开始没有加噪声的图片是怎么变到纯噪声的，所以我的输出是变化的方向而不是终点向量，我还要用一开始输入的加噪后的向量减去这个输出才得到一步步去噪的结果。&lt;/p&gt;
&lt;h2&gt;Improving VLAs with co-training, joint-training &amp;#x26; knowledge insulation&lt;/h2&gt;
&lt;p&gt;这一部分就是文章提出的解决之前问题的方法，&lt;/p&gt;
&lt;h3&gt;Co-training &amp;#x26; representation learning with joint discrete/continous action prediction&lt;/h3&gt;
&lt;p&gt;这一部分讲的就是联合训练，也就是VLM和action expert同时训练。具体流程就是我一开始有三种数据，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;VLM 数据 (纯图文数据)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内容：只有图片和相关的文字描述（比如，一张猫的照片和文字“一只猫在睡觉”）。&lt;/li&gt;
&lt;li&gt;作用：这就像是给模型看“互联网”。它让模型学习海量的通用知识，理解物体、场景和概念。这是在“温故”，防止模型忘记预训练学到的知识（灾难性遗忘）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;仅动作数据 (Action-only data)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内容：只有机器人的视觉输入和对应的动作序列，但没有文字指令。&lt;/li&gt;
&lt;li&gt;作用：这就像是给模型看“无声的机器人操作录像”。它让模型学习在特定视觉情境下的物理规律和动作可能性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;语言与动作结合的任务 (Combined language and action prediction tasks)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内容：同时包含视觉输入、文字指令和对应的机器人动作。&lt;/li&gt;
&lt;li&gt;作用：这就像是给模型看“带解说的机器人操作视频”（比如“机器人正在拿起红色的积木”）。这是最直接的训练数据，教模型如何将语言指令与物理动作联系起来。论文提到他们通过给“仅动作数据”人工添加文字描述来创造这类数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后我随机从这三种数据中抽取一个批次数据，这个批次里面包含这三种数据用来轮流训练，如果是&lt;/p&gt;
&lt;p&gt;对于VLM来说本质上都是输入一系列的token，然后进行Teacher Forcing，只是这个token可能来自不同的模态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VLM数据：那么VLM模型就根据输入的图片和文字输出对应文字补全(Teacher Forcing)，然后计算交叉熵损失。然后由于这个数据不涉及动作，所以后续的action expert部分的损失就被mask掉，设为0，语言部分的mask设为1&lt;/li&gt;
&lt;li&gt;Action-only数据：VLM模型根据输入视频和编码动作，来预测视频中对应的动作，一样也是Teacher Forcing。一开始的动作用fast 编码，然后VLM模型就预测这个token，然后后面的action expert就输入连续动作向量预测整个连贯的动作。前面一个用交叉熵损失，后面一个用流匹配。语言部分就mask为0，动作预测mask为1，action expert部分mask为1.&lt;/li&gt;
&lt;li&gt;combined 数据：VLM模型输入的是视频，这个视频一开始对应的完整命令和他的编码动作，然后也是Teacher Forcing的方法让模型去预测命令和动作。一样是VLM预测动作token，expert输入连续动作向量预测连贯动作。所有部分mask为1。
$$
\mathcal{L}&lt;em&gt;{\mathrm{CO-VLA}}(\theta)=\mathbb{E}&lt;/em&gt;{\mathcal{D},\tau,\omega}\left[-\sum_{j=1}^{n-1}M_j^\ell\log p_\theta(\hat{\ell}&lt;em&gt;{j+1}|x&lt;/em&gt;{1:j})+\alpha M^{\mathrm{act}}\left|\omega-a_{1:H}-f_\theta^a(a_{1:H}^{\tau,\omega})\right|^2\right]
$$
$M^\ell$ is a language loss mask (indicating locations in the token stream at which the language loss should be applied) and $M^{act}$ is an action mask indicator specifying whether or not actions should be predicted for the given example.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述讲的都是这个模型的训练过程，等到模型的推理的时候，一开始输入图片和完整命令，然后VLM模型就可以计算他对应的key vector和value vector。Action expert就输入存噪声进去，然后计算他对应的query vector，key vector和value vector。然后进行之前提到的交叉注意力，得到方向向量，然后和一开始的纯噪声相加后再次作为新的输入，输入到action expert中，不断迭代这个过程，最后逐渐去噪，得到对应的动作命令。&lt;/p&gt;
&lt;p&gt;于是乎相比于自回归架构，我的action expert计算之后可以一次性得到所有的输出，这样大大降低他的延迟。&lt;/p&gt;
&lt;h3&gt;Knowledge insulation &amp;#x26; gradient flow&lt;/h3&gt;
&lt;p&gt;这里就是梯度阻断，就是
$$
softmax(Q_a*K_a)&lt;em&gt;V_a+softmax(Q_a&lt;/em&gt;K_b)&lt;em&gt;V_b
$$
后半部分的$softmax(Q_a&lt;/em&gt;K_b)*V_b$的损失不回传给VLM的模型&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-08-13 19.25.04.CG_7WOzh.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-08-13 19.25.04.CG_7WOzh.png" length="0" type="image/png"/></item><item><title>像素级自监督 COVER ICCV 2025 论文笔记</title><link>https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87cover</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87cover</guid><description>Vector Contrastive Learning For Pixel-Wise Pretraining In Medical Vision</description><pubDate>Tue, 05 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-08-05 17.02.51._VhRE9HD.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-08-05%2017.02.51.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;这篇文章主要提出了一种像素级的自监督框架，感觉他摆脱了原本自监督正样本对和负样本对的思想，有点跳脱开来提出一种新的想法。原本的正负样本对属于二元对比学习，将正样本对拉近，然后将负样本对推开。但是论文指出这会有一个问题，&lt;strong&gt;over-dispersion(过度发散)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;模型在训练过程中，为了最大程度推开不同像素之间的特征，导致本来该相近、属于同一种结构/语义的像素（比如同一根血管、同一个区域），在特征空间里被拉得“彼此很远”，破坏了原有的空间或语义连续性。&lt;/p&gt;
&lt;p&gt;本质上来说就是正样本对太少了，负样本对太多了。于是模型设法将除了正样本对其他的所有像素都当作负样本对，都推开。这样就会有问题，图像中有空间连续性和结构一致性，如果这样的话，正样本对周围原本具有连续性应该靠近的都被推开了，这样就会导致特征空间碎片化。&lt;/p&gt;
&lt;p&gt;于是文章就提出一种思路，跳脱原本正负样本对的范式，他的想法是让模型学一个&lt;strong&gt;displacement vector field（DVF，位移矢量场）&lt;/strong&gt;。想法就是我对图片进行空间变化，然后我让模型去预测这个矢量场，使得其中一张图片根据这个矢量场能够越来越接近另一个图片，预测的差异作为损失函数来训练模型。&lt;/p&gt;
&lt;h2&gt;Self-vector regression for extendable self-learning(SeVR)&lt;/h2&gt;
&lt;p&gt;一开始的流程就是我从数据集中$x\sim\mathcal{D}$选一张图片。然后选两个图片增强方式，一个是颜色$t\sim\mathcal{T}&lt;em&gt;{ap}$，另一个是空间$\psi&lt;/em&gt;{ab}\sim\mathcal{T}&lt;em&gt;{sp}$ 。然后处理之后得到两张图$x&lt;/em&gt;{a}=t(x),x_{b}=\psi_{ab}(x)$ 。经过同一个backbone提取特征之后，$F_{a}={f_{a}^{l}}&lt;em&gt;{l=0}^{L}={\mathcal{N}&lt;/em&gt;{\theta}}(x_{a})$  ，$F_{b}={f_{b}^{l}}&lt;em&gt;{l=0}^{L}={\mathcal{N}&lt;/em&gt;{\theta}}(x_{b})$ 。他的想法就是我用经过空间增强后，我可以的到他的DVF，我用这个DVF作为ground truth $\psi_{ab}^{i}$，然后我用进行颜色增强的图片来预测这个DVF。想法就是我提取出特征向量之后，比较这两个图片的差异，然后为根据每一个像素转化成矢量。这就很神奇了，两个图片的差异或相似怎么得到一个矢量呢，&lt;a href=&quot;#vector-embedding-unit-veu&quot;&gt;后面&lt;/a&gt;会讲。&lt;/p&gt;
&lt;p&gt;一部分的损失函数就是
$$
\mathcal{L}&lt;em&gt;{vec}(\psi&lt;/em&gt;{ab},\psi&apos;&lt;em&gt;{ab},\epsilon&lt;/em&gt;{ab})=\sum_{i\in{\epsilon_{ab}=1}}|\psi_{ab}^{i}-\psi_{ab}^{&apos;i}|
$$
其中$\epsilon_{ab}=1$ 表示图像增强之后重叠有效的像素，一个mask。还有另一部分一致性损失
$$
\mathcal{L}&lt;em&gt;{con}(f&lt;/em&gt;{ab}^{4},f_{b}^{4},\epsilon_{ab})=-\sum_{i\in{\epsilon_{ab}=1}}\frac{f_{ab}^{4,i}\cdot f_{b}^{4,i}}{||f_{ab}^{4,i}||||f_{b}^{4,i}||}
$$
其中$f_{ab}^4=\psi_{ab}(f_a^4)$&lt;/p&gt;
&lt;p&gt;一致性损失也是很常见的概念，思想就是不管你对一幅图像做了什么变换（裁剪、旋转、空间扰动等），同一位置（或变换后位置）对应的像素/patch/区域，其表征特征应该不变或者尽可能相似。&lt;/p&gt;
&lt;p&gt;文中意思就是我图a提取出来的特征$f_a^4$ 经过ground truth的DVF处理之后$f_{ab}^4=\psi_{ab}(f_a^4)$，他和图b的语义信息应该一样。计算他们的余弦相似度，然后作为损失函数。然后还有奇怪的地方在于$f_a^4$ 这个4是什么意思，&lt;a href=&quot;#vector-pyramid-aggregation-adapts-granularity&quot;&gt;后面&lt;/a&gt;会讲。&lt;/p&gt;
&lt;p&gt;所以总的就是$$\mathcal{L}&lt;em&gt;{COVER}=\mathcal{L}&lt;/em&gt;{con}+\mathcal{L}_{vec}$$&lt;/p&gt;
&lt;h2&gt;Mixture of vectors with consistent optimization flow(MoV)&lt;/h2&gt;
&lt;h4&gt;Vector embedding unit (VEU)&lt;/h4&gt;
&lt;p&gt;这里就是具体讲怎么从特征图到向量。&lt;/p&gt;
&lt;p&gt;我们遍历图片中的所有像素，对于每一个像素，我们在另一张图上的相同位置取一个N×N的领域，然后我们用这个像素的特征向量去和这个领域里的特征向量做点积得到相似度矩阵，然后你用
这个和一个向量模版V去计算加权平均，得到最终这个像素的偏移矢量。以此类推，当遍历完一整个矩阵的时候最后得到DVF。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-08-05%2020.02.47.png&quot; alt=&quot;alt text&quot;&gt;
公式就是
$$
v_{ab}^{&apos;i} = \mathcal{U}(f_a^i, f_b^{N \times N}) = \mathrm{softmax}\left(\frac{f_a^i f_b^{T N \times N}}{\tau}\right) V^{N \times N}
$$&lt;/p&gt;
&lt;h4&gt;Multi-vector integration (MVI)&lt;/h4&gt;
&lt;p&gt;这一段的目的就是我并没有直接处理整个特征向量，而是我讲他分割成 j 个组(小向量)，然后每组跑一遍VEU，最后取平均。
$$
u&apos; = \frac{1}{J} \sum_{j=0}^{J} v&apos;j
$$
为什么要多此一举呢，论文中解释由于语义连续性和特征多样性，像素的空间对应可能本身就是模糊/歧义/多样的，因此需要用多组特征分别“关注不同语义属性”给出多向量预测，再做融合。&lt;/p&gt;
&lt;h2&gt;Vector pyramid aggregation adapts granularity&lt;/h2&gt;
&lt;p&gt;论文并没有只用一层的特征向量，他利用了backbone中的多层特征向量，从最顶层的特征向量开始逐步往下。&lt;/p&gt;
&lt;p&gt;具体而言，例如
$$
\psi_{ab}^{\prime1}=\mathcal{M}(\psi_{ab}^{\prime0}(f_{a}^{1}),f_{b}^{1})\bigodot\psi_{ab}^{\prime0}
$$
我得到第0层的DVF $\psi_{ab}^{\prime0}$ 然后我用这个去处理图a的第1层特征，然后我用处理后的特征向量来进行MoV，最后得到该层的初步DVF，然后再和前一层的DVF  $\psi_{ab}^{\prime0}$ 进行融合操作，最后得到这一层的DVF，以此类推，一直到最底层，精度越来越高。最终得到最后的DVF。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;#x26; \psi_{ab}^{\prime}=\mathcal{V}(F_{a},F_{b})=H({\psi_{ab}^{\prime l}}&lt;em&gt;{l=0}^{L}),\mathrm{~where} &amp;#x26;  \
&amp;#x26; \psi&lt;/em&gt;{ab}^{\prime0}=\mathcal{M}(f_a^0,f_b^0) \
&amp;#x26; \psi_{ab}^{\prime l}=\mathcal{M}(\psi_{ab}^{\prime l-1}(f_a^l),f_b^l)\bigodot\psi_{ab}^{\prime l-1},l=1,2,...,L-1,
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;那融合操作$\bigodot$ 是什么呢&lt;/p&gt;
&lt;p&gt;$$
\large \psi_{ab}^{&apos;1} \bigodot \psi_{ab}^{&apos;0} = \overbrace{ \psi_{ab}^{&apos;1} + \underbrace{ \psi_{ab}^{&apos;1} \overbrace{ (2 * \mathcal{I}&lt;em&gt;{H \times W}(\psi&lt;/em&gt;{ab}^{&apos;0})) }^{\text{Scale alignment}} }_{\text{Space alignment}} }^{\text{Vector fusion}}
$$&lt;/p&gt;
&lt;p&gt;(1) Scale alignment（尺度对齐）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把“上级层”（比如第 0 层）的 DVF 用双线性插值（bilinear interpolation）上采样到“下级层”——也就是让低分辨率的 DVF（向量场）和当前所处理的高分辨率 DVF 大小一致。&lt;/li&gt;
&lt;li&gt;然后把所有向量的数值“放大一倍”（乘 2，或按比例缩放），这样空间单位也对齐到高分辨率那一层的格点范围。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(2) Space alignment（空间对齐）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;即便分辨率对齐后，由于像素格点的排列逻辑不同，还要进一步对中心坐标调整（align），确保两个层级上每个像素/向量的几何对应关系完全对齐。&lt;/li&gt;
&lt;li&gt;这些操作保证原本低分辨率的空间位移场被“正确映射”到高分辨率的坐标系。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(3) Vector fusion（向量融合）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最后把对齐好的两个层级的 DVF &lt;strong&gt;逐元素相加&lt;/strong&gt;，就得到了融合后的新一层 DVF。&lt;/li&gt;
&lt;li&gt;直观上，就是“高分辨率细节+低分辨率全局趋势”既有整体又有细节的空间配准效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是乎我们就得到了最后的DVF。&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-08-05 17.02.51._VhRE9HD.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-08-05 17.02.51._VhRE9HD.png" length="0" type="image/png"/></item><item><title>MICE + Rubin&apos;s Rule</title><link>https://laurie-hxf.xyz/blog/mice--rubins-rule</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/mice--rubins-rule</guid><description>MICE + Rubin&apos;s Rule</description><pubDate>Thu, 31 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/mice.B_OTJ2hj.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近遇到了数据缺失需要填充的问题，一开始我以为数据填充没有什么，想法就是按照简单的来，用众数，中位数，平均数或平均数来填就行了。但是没想到这还是一个比较活跃的研究方向。于是记录一下。&lt;/p&gt;
&lt;h2&gt;概况&lt;/h2&gt;
&lt;p&gt;根据Rubin 1976 提出的分类，数据缺失一般分为三种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MCAR（Missing Completely At Random）完全随机缺失&lt;/strong&gt;&lt;br&gt;
缺失与任何变量都无关，例如系统崩溃导致某些记录丢失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MAR（Missing At Random）随机缺失&lt;/strong&gt;&lt;br&gt;
缺失依赖于其他观测到的变量，例如收入字段缺失，但缺失概率和受教育程度有关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MNAR（Missing Not At Random）非随机缺失&lt;/strong&gt;&lt;br&gt;
缺失依赖于未观测到的自身变量，例如重病患者可能不填写健康问卷。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;常见处理方法及适用情况&lt;/p&gt;
&lt;p&gt;|方法|适用场景|特点|
|---|---|---|
|删除缺失值|缺失样本很少、MCAR|简单粗暴，可能丢失重要信息|
|均值/中位数/众数填充|连续/类别变量、近似MCAR或MAR|简单有效，但可能引入偏差|
|KNN 填充|缺失值不是太多，数据分布规律明显|保留一定相似性，但计算成本高|
|多重插补（MICE）|MAR，且需要保持估计的不确定性|当前主流方法之一，适合科研应用|
|回归填充|缺失变量和其他变量关系强|可以精准填补，但风险是过拟合|
|自编码器/深度学习填充|大量数据缺失或复杂关系|在图像/表格数据中表现较好，但难以解释|
|模型内处理|如 XGBoost、LightGBM 支持缺失值|不需要额外处理，但可能影响解释性|&lt;/p&gt;
&lt;p&gt;然后统计界可能最认可就是多重插补（Multiple Imputation），下面讲一下多重插补的比较经典的方法MICE（Multivariate Imputation by Chained Equations）。&lt;/p&gt;
&lt;h2&gt;MICE&lt;/h2&gt;
&lt;h4&gt;第一步：初始填补&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;将有缺失的列先用一个&lt;strong&gt;初始值&lt;/strong&gt;（比如均值、中位数、回归预测等）填上，使得整张表没有缺失值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第二步：&lt;strong&gt;循环每一列（有缺失）进行建模&lt;/strong&gt;（关键）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;对每一列 $X_j$​ 有缺失的变量：
&lt;ol&gt;
&lt;li&gt;把这一列作为目标变量 y；&lt;/li&gt;
&lt;li&gt;其余所有列作为特征变量 X；&lt;/li&gt;
&lt;li&gt;只用那些在 $X_j$​ 上有观测值的行进行训练（因为 y 不能是 NaN）；&lt;/li&gt;
&lt;li&gt;用这个模型去预测 $X_j$​ 上原来为缺失值的地方；&lt;/li&gt;
&lt;li&gt;用预测值填补这些缺失；&lt;/li&gt;
&lt;li&gt;然后进入下一列，重复这个过程。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第三步：&lt;strong&gt;迭代多轮更新&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;上面这一过程会 &lt;strong&gt;循环进行多轮（如 10 次）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;每轮都重新建模、填补；&lt;/li&gt;
&lt;li&gt;因为每轮填补后的数据更完整，所以后续预测也可能更准确。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第四步：&lt;strong&gt;多重插补（Multiple Imputation）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;上面步骤完成的是&lt;strong&gt;一次插补（single imputation）&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;为了反映不确定性，MICE 会做 &lt;strong&gt;&lt;code&gt;m&lt;/code&gt; 次独立的插补&lt;/strong&gt;（每次加入随机性）：
&lt;ul&gt;
&lt;li&gt;用不同的 &lt;code&gt;random_state&lt;/code&gt; 和 &lt;code&gt;sample_posterior=True&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;得到 &lt;code&gt;m&lt;/code&gt; 组完整的数据集。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Rubin&apos;s Rule&lt;/h2&gt;
&lt;p&gt;这个衡量的是你的填补的结果的好坏&lt;/p&gt;
&lt;h4&gt;coef （平均系数）&lt;/h4&gt;
&lt;p&gt;$$
\bar{\beta} = \frac{1}{m} \sum_{i=1}^{m} \beta_i
$$
它反映了变量对目标的&lt;strong&gt;平均影响程度&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当你用填充过后的数据进行一次回归预测的时候，你就得到每个特征对应的回归系数。比如
$$
视力预测=0.8+(−0.02)⋅年龄+0.5⋅曲率+0.3⋅眼轴
$$
0.3,0.5这些就是对应特征的回归系数。然后因为我们进行了m次插补，每次的数据不同，每次的回归系数也不一样。所以就平均一下每个特征的m次平均回归系数 $\bar{\beta}$&lt;/p&gt;
&lt;h4&gt;std_error（系数标准误）&lt;/h4&gt;
&lt;p&gt;$$
T = W + \left(1 + \frac{1}{m}\right)B
$$
其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;W：每个模型内部估计误差的平均（即原始的 &lt;code&gt;bse**2&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;B：模型之间的回归系数差异（越大说明插补不确定性越大）；&lt;/li&gt;
&lt;li&gt;T：总方差，反映了&lt;strong&gt;模型内部误差 + 插补不确定性&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std_error = sqrt(T)&lt;/code&gt;：总标准误，越大表示估计越不稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;W：Within-imputation variance（模型内部误差）&lt;/h6&gt;
&lt;p&gt;每个插补数据集都会跑一次回归模型，每个模型都会给出自己的回归系数的标准误，比如第 iii 个模型中的第 j 个系数标准误为 $se_{ij}$，平方就是方差：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{Var}(\beta_{ij})=se_{ij}^2
$$&lt;/p&gt;
&lt;p&gt;把每个模型里这个系数的标准误平方取平均，就是：&lt;/p&gt;
&lt;p&gt;$$
W_j=\frac{1}{m}\sum_{i=1}^mse_{ij}^2
$$&lt;/p&gt;
&lt;p&gt;这反映了：如果数据本来是完整的，我们模型预测值的方差是多少。&lt;/p&gt;
&lt;h6&gt;B：Between-imputation variance（模型间差异）&lt;/h6&gt;
&lt;p&gt;你有 &lt;code&gt;m&lt;/code&gt; 组回归系数 $\beta_{2j}, ..., \beta_{mj}$，表示多次插补后在同一个变量上的估计值。它们之间会有差异。
所以我们计算这些系数之间的&lt;strong&gt;样本方差&lt;/strong&gt;：
$$
B_j = \frac{1}{m-1} \sum_{i=1}^{m} (\beta_{ij} - \bar{\beta_j})^2
$$&lt;/p&gt;
&lt;h6&gt;为什么加上 $(1+\frac{1}{m})B$？&lt;/h6&gt;
&lt;p&gt;这是 Rubin 提出的一个&lt;strong&gt;修正项&lt;/strong&gt;，来源如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单纯的 B 是 &lt;code&gt;样本方差&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;要估计总体方差，需要一个偏差修正项；&lt;/li&gt;
&lt;li&gt;$(1+\frac{1}{m})B$就是这个修正，考虑了：你只用了 &lt;code&gt;m&lt;/code&gt; 个插补结果，样本不够多，所以要略放大不确定性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;t_value（t 统计量）&lt;/h2&gt;
&lt;p&gt;$$
t=\frac{\bar{\beta}}{\mathrm{std~error}}
$$&lt;/p&gt;
&lt;p&gt;用于做假设检验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;零假设 $H_0$​：该变量对目标没有影响（即 $\beta = 0$）&lt;/li&gt;
&lt;li&gt;如果 $|t|$ 很大（比如 &gt; 2），说明这个变量对目标有显著影响。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><img src="/_astro/mice.B_OTJ2hj.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/mice.B_OTJ2hj.png" length="0" type="image/png"/></item><item><title>像素级自监督 SAM IEEE Trans 2023 论文笔记</title><link>https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87sam</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87sam</guid><description>SAM - Self-supervised Learning of Pixel-wise Anatomical Embeddings in Radiological Images</description><pubDate>Thu, 31 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-07-30 14.42.58.BPW8tG4H.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-07-30%2014.42.58.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;这篇文章的主要贡献就是在医学图像领域提出一个像素级的自监督解剖嵌入学习框架，主要采用coarse-to-fine结构以及文章中提出的负样本采样策略。&lt;/p&gt;
&lt;p&gt;然后在多个任务表现良好，比如lesion matching(病灶匹配)，image Registration(图片配准)。病灶配准就是一开始拍摄的图片中可能有个病变，然后一段时间之后再拍一张照片，就能定位原来的那个病变在哪里。image Registration就是把两幅或多幅不同来源、不同时间、不同角度的图像空间上对齐，使它们的同一结构或内容能够准确重合。&lt;/p&gt;
&lt;h2&gt;Coarse-to-Fine Network Architecture&lt;/h2&gt;
&lt;p&gt;这是文章中提出来的框架，主要用的是ResNet结合魔改过的FPN。补充一下FPN的背景&lt;/p&gt;
&lt;h4&gt;FPN&lt;/h4&gt;
&lt;p&gt;可以看&lt;a href=&quot;https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87fpn&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;SAM&lt;/h4&gt;
&lt;p&gt;这个架构的主要流程就是对于一张3D的CT图，我从中随机取两个随机大小的patch，然后将这两个patch放大到同一尺寸，分别为$x, x&apos; \in \mathbb{R}^{d \times h \times w}$ 。然后这两个patch就输入进3D的ResNet中，然后经过FPN得到最上层的特征图，以及最下层的特征图经过3*3的卷积和L2 正则化之后的到Global emb tensor $F^g$和Local emb tensor $F^l$。他们是128通道，也就是每一个像素对应一个128维大小的特征向量。然后就可以得到两个patch分别的local 和 global emb tensor。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-07-30%2015.37.41.png&quot; alt=&quot;alt text&quot;&gt;
与一开始的FPN不同，他的右边不是从最上面的特征往下的，他切断了顶层，从倒数第二层开始往下。还有他没有用到1*1的卷积核，他的通道数都是设计好的，不需要1*1的卷积核来改变通道数。&lt;/p&gt;
&lt;h2&gt;Pixel Pairs and Loss Function&lt;/h2&gt;
&lt;p&gt;然后我们随机取两个patch中重叠部分的同一个像素作为正样本对，分别为$(p_i,p&apos;&lt;em&gt;i)$ 他们对应的特征向量为$f_i,f&apos;&lt;em&gt;i$。然后选$n&lt;/em&gt;{neg}$ 个作为负样本对，用InfoNCE来作为损失函数。
$$
\mathcal{L} = -\sum&lt;/em&gt;{i=1}^{n_{\text{pos}}} \log \frac{\exp(\mathbf{f}_i \cdot \mathbf{f}&lt;em&gt;i&apos;/\tau)}{\exp(\mathbf{f}&lt;em&gt;i \cdot \mathbf{f}&lt;em&gt;i&apos;/\tau) + \sum&lt;/em&gt;{j=1}^{n&lt;/em&gt;{\text{neg}}} \exp(\mathbf{f}&lt;em&gt;i \cdot \mathbf{h}&lt;/em&gt;{ij}/\tau)}
$$
还有值得注意的是，Local 和 Global 分别计算损失函数，然后分别以$f_i$和$f&apos;&lt;em&gt;i$ 作为锚点(anchor)再计算损失函数（上式中分母那里用的是$f_i$为锚点，把他改成$f&apos;&lt;em&gt;i$就是$f&apos;&lt;em&gt;i$为锚点），因为对称。所以最终的损失函数应该有4部分。
$$
L&lt;/em&gt;\text{total} = L&lt;/em&gt;\text{global}^{f_i, \text{anchor}} + L&lt;/em&gt;\text{global}^{f&apos;&lt;em&gt;i, \text{anchor}} + L&lt;/em&gt;\text{local}^{f_i, \text{anchor}} + L&lt;/em&gt;\text{local}^{f&apos;_i, \text{anchor}}
$$&lt;/p&gt;
&lt;h2&gt;Hard and Diverse Negative Sampling&lt;/h2&gt;
&lt;p&gt;这部分就是怎么选他的负样本对，为了模型能学到更多的东西，文章提出他的策略。&lt;/p&gt;
&lt;p&gt;对于Global部分，我们用$f_i^g$ 和$F^g$，$F^{g&apos;}$ 做卷积得到余弦相似度，得到的similarity map为  $S_i^g$ 和 $S_i^{g&apos;}$
然后我们选$n_{neg}$个排除$f_i^g$ ，$f_i^{g&apos;}$ 最相似的像素作为hard negatives $h^g_{ij}$  。然后为了多样化，还会从同一个training batch中的不同path的随机$n_{rand}^g$个像素添加进负样本对中。&lt;/p&gt;
&lt;p&gt;对于Local部分，他同样计算从$F^l$，$F^{l&apos;}$中计算 similarity map为  $S_i^l$ 和 $S_i^{l&apos;}$ ，然后他将global 的similarity map $S_i^g$ 和 $S_i^{g&apos;}$ 上采样到  $S_i^l$ 和 $S_i^{l&apos;}$大小，然后直接相加得到combined similarity map  $S_i^l+S_i^l$ 和 $S_i^{l&apos;}+S_i^{l&apos;}$ ，然后先从中选 $n_{cand}^l &gt; n_{neg}$ 个分数最高的，然后才从中挑$n_{neg}$个作为最终负样本对。这个随机过程，是为了避免所有hard negative都扎堆在某个局部——即使相似，也能让每批hard negative点多样化，避免模型过拟合局部微差。&lt;/p&gt;
&lt;h2&gt;Application: Anatomical Point Matching&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-07-30%2016.33.41.png&quot; alt=&quot;alt text&quot;&gt;
这部分就是讲他怎么用在lesion matching(病灶匹配)，一开始给个样本图，然后计算出一开始病灶像素的Local 和 Global的向量，然后同时和后面的图片的tensor做卷积得到相似图，然后结合Local和Gobal的相似图找到后来的病灶位置&lt;/p&gt;
&lt;h2&gt;Application: Enhancing Image Registration&lt;/h2&gt;
&lt;p&gt;这部分讲的是怎么用于image Registration(图片配准)。&lt;/p&gt;
&lt;h4&gt;1. &lt;strong&gt;SAM-affine：稀疏点仿射配准&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设有两幅CT：A（固定影像）、B（要对齐的移动影像）。&lt;/li&gt;
&lt;li&gt;你在A上&lt;strong&gt;均匀采样一些网格点&lt;/strong&gt;（稀疏，比如只采几百/几千个点，而不是全部体素），这些点覆盖A上的各个解剖部位。&lt;/li&gt;
&lt;li&gt;把网格点外（身体外）的点丢弃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用SAM&lt;/strong&gt;，把A中每个采样点的embedding，去B中找“特征最相似的点”作为B上的配对。&lt;/li&gt;
&lt;li&gt;计算这些点对的相似度分数，只有“很可靠的高分”对才留下来。&lt;/li&gt;
&lt;li&gt;用这些点对，通过最小二乘法直接拟合一个&lt;strong&gt;全局仿射变换矩阵&lt;/strong&gt;（类似“坐标对齐”），实现A、B图的大致空间对正。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. &lt;strong&gt;SAM-coarse：稀疏点粗变形配准&lt;/strong&gt;&lt;/h4&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;所以，再&lt;strong&gt;从配准后的两幅图（A&apos;, B&apos;）中重新采样一批稀疏点&lt;/strong&gt;（局部更密集一些也可以）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;再用SAM&lt;/strong&gt;嵌入，对齐两图的稀疏点对。&lt;/li&gt;
&lt;li&gt;根据点对间的空间关系，&lt;strong&gt;插值得到一张粗略的变形场（deformation field）&lt;/strong&gt;（让身体结构能弯曲、拉伸配对）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. &lt;strong&gt;SAM-VoxelMorph：深度配准精调&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;经过前面两轮后，A,B已经基本“空间对应”了，但&lt;strong&gt;最后精细结构、微小差异&lt;/strong&gt;依然没法完全对齐。&lt;/li&gt;
&lt;li&gt;这一步用现有的深度学习配准网络（VoxelMorph）做细调，“吃进去”的输入包含SAM输出的空间相关特征+SAM相似性损失（即每个点对的embedding距离也影响最终效果）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SAM相当于做了强力辅助&lt;/strong&gt;，支持VoxelMorph网络学到更解剖学、判别性更强的变形映射。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><img src="/_astro/截屏2025-07-30 14.42.58.BPW8tG4H.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-07-30 14.42.58.BPW8tG4H.png" length="0" type="image/png"/></item><item><title>目标检测 FPN CVPR 2017 论文笔记</title><link>https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87fpn</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87fpn</guid><description>Feature Pyramid Networks for Object Detection</description><pubDate>Wed, 30 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-07-30 15.05.25.BMgoz_Fn.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在目标检测任务中，不同尺度下的目标识别是一个很大的挑战。原来多数的object detection算法都是只采用顶层特征（卷积最后一层）做预测，低层的特征语义信息比较少，但是目标位置准确；高层的特征语义信息比较丰富，但是目标位置比较粗略。&lt;/p&gt;
&lt;p&gt;那如果想引入不同尺度的特征，应该怎么做呢？&lt;/p&gt;
&lt;h2&gt;FPN&lt;/h2&gt;
&lt;p&gt;主要有这么几种方法
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-07-30%2015.03.22.png&quot; alt=&quot;alt text&quot;&gt;
(a) Featurized image pyramid：这种方式就是先把图片弄成不同尺寸的，然后再对每种尺寸的图片提取不同尺度的特征，再对每个尺度的特征都进行单独的预测，这种方式的优点是不同尺度的特征都可以包含很丰富的语义信息，但是缺点就是时间成本太高。&lt;/p&gt;
&lt;p&gt;(b) CNN网络最常用的结构，只用最后一层卷积后的feature map&lt;/p&gt;
&lt;p&gt;(c) SSD的做法，从conv4开始每一层的feature map都用来做预测，这样就得到了多尺度下的特征，显然这种做法是没有增加任何计算量的，不过FPN的作者说SSD没有用更底层的特征，而更底层的特征对小目标识别有优势。&lt;/p&gt;
&lt;p&gt;(d) FPN的网络架构。从图中可以看出，就是多了一个上采样的过程，feature map上采样，然后和更浅层的特征融合，再独立做预测。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-07-30%2015.05.25.png&quot; alt=&quot;alt text&quot;&gt;
主要想法就是&lt;/p&gt;
&lt;p&gt;左边部分就是传统的卷积操作，每次经过一层卷积图片的分辨率就会减小，然后越底层卷积之后的特征图他包含的细节就更多，越高层的特征图他包含的语义信息就越多，但是细节就没有这么多。左边逐渐往上的部分论文称之为bottom up。&lt;/p&gt;
&lt;p&gt;然后，左层的最上面的特征图经过一个1*1的卷积核改变通道数，不改变分辨率，然后就作为右边的最顶层。右边逐渐往下被称之为top down。主要是为了上采样然后融合covnet提取出的顶层和底层的信息，具体而言就是右边每一层的特征图经过*2的放大采样。&lt;/p&gt;
&lt;p&gt;采样方法是最近邻上采样，使得特征图扩大2倍。上采样的目的就是放大图片，在原有图像像素的基础上在像素点之间采用合适的插值算法插入新的像素，在本文中使用的是最近邻上采样(插值)&lt;/p&gt;
&lt;p&gt;从而使得特征图和左下层的特征图大小保持一致，然后左层的特征图经过1*1的卷积核匹配通道数之后与右层放大后的特征图直接相加，以此类推。&lt;/p&gt;
&lt;p&gt;然后对于右边的每一层经过一个3*3的卷积核可以直接输出用于predict，使用这个3*3卷积的目的是为了消除上采样产生的混叠效应(aliasing effect)，混叠效应应该就是指上边提到的‘插值生成的图像灰度不连续，在灰度变化的地方可能出现明显的锯齿状。这样就得到了多尺度的特征。&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-07-30 15.05.25.BMgoz_Fn.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-07-30 15.05.25.BMgoz_Fn.png" length="0" type="image/png"/></item><item><title>3D proxy task NeurIPS 2020 论文笔记</title><link>https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%873d_proxy_task</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%873d_proxy_task</guid><description>3D Self-Supervised Methods for Medical Imaging</description><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-07-24 17.05.29.DVW01A1Q.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-07-24%2017.05.29.png&quot; alt=&quot;alt text&quot;&gt;
这篇文章的主要贡献就是提出3D领域的5个proxy task(代理任务)，然后通过这些任务帮助预训练模型能更好的理解。&lt;/p&gt;
&lt;p&gt;那为什么作者要以医学图像来作为背景呢，而不是传统的3D图像呢，因为医学影像3D数据的应用需求和挑战非常突出。  医学图像领域（如MRI、CT等）天生就是三维数据，且“人工标注难、标注昂贵、数据隐私敏感、样本规模有限”这些问题尤为严重。当然医学图像只是背景，本质上这些都可以迁移到任何3D图像领域里。&lt;/p&gt;
&lt;h2&gt;3D Contrastive Predictive Coding (3D-CPC)&lt;/h2&gt;
&lt;p&gt;CPC是2018年就提出来的一个自监督学习方法，一开始是处理声音的，后续用到了2D图像领域，然后也影响了很多后来的自监督方法比如SimCLR，他的损失函数就是从这里提取灵感的。&lt;/p&gt;
&lt;p&gt;一开始的CPC：&lt;/p&gt;
&lt;p&gt;原始输入序列 $x_{1},x_{2},\ldots,x_{T}$ 被一个编码器 $f$ 映射成潜在表示：
$$
z_{t}=f(x_{t})
$$
这些 $z_{t}$ 是低维、压缩过的信息，代表输入的潜在特征。
上下文建模
使用一个自回归模型（如 GRU 或 Transformer）来聚合前面的信息形成上下文向量：
$$
c_{t}=g(z_{\leq t}) \quad \text{(即用前 t 个编码向量生成上下文)}
$$
这个 $c_{t}$ 表示“看到前 t 个输入后的理解”。
预测未来潜在表示
CPC 的目标不是直接预测未来的原始输入，而是预测未来的潜在表示 $z_{t+k}$（通常 $k=1,2,3,\ldots$）：
$$
\hat{z}&lt;em&gt;{t+k}=h(c&lt;/em&gt;{t})
$$
然后，用一个 对比损失（InfoNCE） 来让预测的 $\hat{z}&lt;em&gt;{t+k}$ 和真实的 $z&lt;/em&gt;{t+k}$ 尽可能接近，并和其他负样本区分开来。
InfoNCE 损失
在每一个时间点 $t$，我们尝试从一堆样本（一个正样本，多个负样本）中分辨出哪一个是正确的 $z_{t+k}$：
$$
\mathcal{L}&lt;em&gt;{t,k}=-\log \frac{\exp(z&lt;/em&gt;{t+k}^{\top}W_{k}c_{t})}{\sum_{z_{j}\in\mathcal{Z}}\exp(z_{j}^{\top}W_{k}c_{t})}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$z_{t+ k}$是正样本(未来的真实潜在表示)&lt;/li&gt;
&lt;li&gt;${\mathcal{Z}}$是一组正负样本&lt;/li&gt;
&lt;li&gt;$W_k$是学习得到的变换矩阵&lt;/li&gt;
&lt;li&gt;$c_t$是上下文向量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文提出的3D CPC：&lt;/p&gt;
&lt;p&gt;先将3D图片分成同等大小，且有重叠的patch $x_{i,j,k}$ ，然后用encoder网络$z_{i,j,k}=g_{enc}(x_{i,j,k})$将它转化成latent representation(潜在表示) ，然后用另一个网络提取上文的信息$$c_{i,j,k}=g_{cxt}({z_{u,v,w}}_{u\leq i,v,w})$$
这个公式的意思就是第 i 层及其以前的所有块，按照倒金字塔来取，（可以看上面的图）用一个网络提取出信息来。没有全部取而是按照倒金字塔取的目的就是为了节省算力。&lt;/p&gt;
&lt;p&gt;论文里面没有写，但实际上应该还有一个小的预测网络 $\hat z_{i+l,j,k} =  W_{pred​}(c_{i,j,k}​)$，用来预测下一个patch的特征向量。&lt;/p&gt;
&lt;p&gt;损失函数长这样
$$
\begin{aligned}
\mathcal{L}&lt;em&gt;{CPC} &amp;#x26; =-\sum&lt;/em&gt;{i,j,k,l}\log p(z_{i+l,j,k}\mid\hat{z}&lt;em&gt;{i+l,j,k},{z_n}) \
&amp;#x26; =-\sum&lt;/em&gt;{i,j,k,l}\log\frac{\exp(\hat{z}&lt;em&gt;{i+l,j,k}z&lt;/em&gt;{i+l,j,k})}{\exp(\hat{z}&lt;em&gt;{i+l,j,k}z&lt;/em&gt;{i+l,j,k})+\exp(\sum_n\hat{z}_{i+l,j,k}z_n)}
\end{aligned}
$$
但是有个很奇怪的点在于他的分母的求和为什么在exp的里面，而不是外面，常见的应该是在外面，不知道是不是笔误。&lt;/p&gt;
&lt;h2&gt;Relative 3D patch location (3D-RPL)&lt;/h2&gt;
&lt;p&gt;这个任务就是将3D图片分为多块，然后取一个作为中间块，然后随机选一个块，预测他和中心块的相对位置。然后用预测结果分布和one hot的真实结果做交叉熵，然后多次预测求和。
$$
\mathcal{L}&lt;em&gt;{RPL}=-\sum&lt;/em&gt;{k=1}^K\log p(y_q\mid\hat{y}&lt;em&gt;q,{y_n})
$$
这个公式其实跳了一下，对于第K个查询，他的交叉熵是
$$
L_k=-\sum&lt;/em&gt;{j=1}^{N-1}t_{k,j}\log(\hat{p}_{k,j})
$$
但是由于真实结果是one hot，所以这个只有一项，然后再求和一下多次查询求和，就是论文里的公式&lt;/p&gt;
&lt;h2&gt;3D Jigsaw puzzle Solving (3D-Jig)&lt;/h2&gt;
&lt;p&gt;这个任务更像是进阶版的RPL，我们还是将3D图像分割成多个n*n*n个不重叠的块，然后总共有$n^3!$ 种排列方式，然后我们选取其中的P种，然后随机从这P种里面挑一个打乱，然后让模型预测排列方式的概率分布，然后和one hot真实排列进行交叉熵计算。&lt;/p&gt;
&lt;p&gt;值得注意的是，为什么我们不采用全部的$n^3!$ 种排列方式，原因就是- 全部排列数太多（指数级增长），模型很难“记住”或判别。而且类别太多不仅带来巨大的参数和计算，最终训练时大多数类别都只出现一次，极其疏稀，不利于收敛。所以我们选P种，而且这P种使用最大化汉明距离（Hamming distance）得来的。&lt;/p&gt;
&lt;p&gt;汉明距离衡量两组排列之间有多少位置不同（比如abcd和abdc，距离为1）。最大化汉明距离采样的目标是：  选择的每两个排列应尽可能多地不一样。这样做有几个好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个类别之间显著不同，模型必须捕捉全局空间关系，不能偷懒记“细节小调整”。&lt;/li&gt;
&lt;li&gt;提高了任务判别难度和特征鲁棒性。&lt;/li&gt;
&lt;li&gt;减少类别混淆和标签不确定性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3D Rotation prediction (3D-Rot)&lt;/h2&gt;
&lt;p&gt;这个任务就是预测旋转了多少的角度，让3D图片沿着x，y，z三个坐标轴旋转，每个坐标轴可以旋转$(0^\circ,90^\circ,180^\circ,270^\circ)$ ，然后论文里只考虑单独轴的旋转，不考虑组合旋转，所以总共有12种旋转方式。然后有3种是相同的(每个轴旋转0度)，所以总共保留10种。还是预测旋转分布以及one hot的交叉熵。&lt;/p&gt;
&lt;h2&gt;3D Exemplar networks (3D-Exe)&lt;/h2&gt;
&lt;p&gt;这个任务就是对图片进行不同的处理(比如随机翻转，随机旋转，随机亮度/对比度变化，缩放)，然后对同一张图片的处理看作是正样本对，其他图片的处理看作负样本对。然后可以用交叉熵来作为损失函数。但是交叉熵的话，计算量就非常大，于是作者用了triplet loss。
$$
\mathcal{L}&lt;em&gt;{E x e}=\frac{1}{N_T} \sum&lt;/em&gt;{i=1}^{N_T} \max \left{0, D\left(z_i, z_i^{+}\right)-D\left(z_i, z_i^{-}\right)+\alpha\right}
$$
D是欧氏距离（L2范数）。意思就是将图片x以及他对应的处理后的图片进行编码后得到z，然后他想让 $z_i, z_i^{-}$ 之间的距离大于 $z_i, z_i^{+}$ 且如果大于的距离大于$\alpha$的话，损失就为0。&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-07-24 17.05.29.DVW01A1Q.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-07-24 17.05.29.DVW01A1Q.png" length="0" type="image/png"/></item><item><title>自监督学习 CSMAE ISBI 2025 论文笔记</title><link>https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87csmae</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87csmae</guid><description>CSMAE - Cataract Surgical Masked Autoencoder (MAE) based Pre-training</description><pubDate>Mon, 07 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-07-07 00.10.30.D4yx4vz5.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-07-07%2000.10.30.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个文章的主要用的就是自监督学习里面的给图像加mask然后让模型预测mask的内容这种思路&lt;/p&gt;
&lt;p&gt;主要方法就是MAE(VedioMAE)加一个token selection network，他的主要想法就是MAE选择图像mask的部分的时候，他是随机选的，但是图像的每一部分他所包含的信息是不一样的。所以他的想法就是我想要选信息最多的部分来mask，这样我的模型就更可能学到更多的东西。&lt;/p&gt;
&lt;p&gt;然后他的模型就分为4个部分&lt;/p&gt;
&lt;h2&gt;Tokenizer&lt;/h2&gt;
&lt;p&gt;这里他用3D的CNN来处理视频(T*C*H*W)，将视频转化为N个tokens&lt;/p&gt;
&lt;h2&gt;Token Selection Network&lt;/h2&gt;
&lt;p&gt;这里用一个多头注意力层(MHA)+linear layer+Softmax 来计算每个token选择的概率，然后根据一个$\alpha \in ( 0, 1)$ 掩码率，来决定选多少以及选哪个token来mask。&lt;/p&gt;
&lt;p&gt;$$
P=\mathrm{Softmax}(\mathrm{Linear}(\mathrm{MHA}(T))).
$$&lt;/p&gt;
&lt;p&gt;最终得到M个visible的。&lt;/p&gt;
&lt;h2&gt;Encoder &amp;#x26; Decoder&lt;/h2&gt;
&lt;p&gt;这里用的就是两个ViT来encode输入的token然后decode出缺失的图片的部分。&lt;/p&gt;
&lt;h2&gt;Training&lt;/h2&gt;
&lt;p&gt;这里的loss function用的和MAE一样，比较生成 $X_b$ 的和ground truth $X_e$ 的差别，然后作为损失函数。
$$
L_R(\phi)=\frac{1}{N-M}\sum_{i\in M_i^{^{\prime}}}|X_{b_i}-X_{e_i}|^2,
$$&lt;/p&gt;
&lt;p&gt;有意思的是文章还使用了一个loss function来专门训练token selection network，他们的想法来自于强化学习中的Gradient-Following algorithm。&lt;/p&gt;
&lt;p&gt;他将MAE的架构作为环境，然后$L_R$ 作为奖励，目标就是最大化 $E[L_R]$ 。为什么是最大化$L_R$，我们不应该让损失越来越小吗。答案就是token selection network的作用是为了找到最有价值的patch部分然后把他mask掉，如果我们选择一个信息很小的，模型很容易就预测出来的部分来mask，那就没有很大的作用。而当我们越选择有意义的部分，模型越难预测出来，他的$L_R$也就会更大，训练速度也会加快，所以对于token selection network而言，他的目的就是目标就是最大化 $E[L_R]$。
$$
L_{select}(\theta)=-E_\theta[L_R(\phi)]=-\sum_{i\in I_m}P_{i\theta}\cdot L_{iR}(\phi),
$$
这里取了一个负数，就转换成了我们常规优化中要最小化的目标函数。这里的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$P_i^\theta$: 第 i 个位置被选中(mask 掉)的概率(由参数$\theta$控制的策略网络输出)&lt;/li&gt;
&lt;li&gt;$L_i^R(\phi)$: 第 i 个 token 的重建误差(由 MAE 模型参数$\phi$产生)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是又有一个问题，模型的两个部分朝着两个方向进行优化，一个想要减小loss，一个想要增加loss，那么就会导致模型的训练一直摇摆，振荡。所以为了解决这个问题，方法就是梯度隔离，token selection network的梯度不回传给MAE，MAE只给个loss给token selection network，也不传给他梯度。他还使用了log缩放，避免概率太小导致的梯度太小。最后的结果应该是这样
$$
L_{\mathrm{select}}(\theta)=-\sum_{i\in I_m}\log(P_i^\theta+\epsilon)\cdot L_i^R(\phi)
$$&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-07-07 00.10.30.D4yx4vz5.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-07-07 00.10.30.D4yx4vz5.png" length="0" type="image/png"/></item><item><title>Lecture13 Attention</title><link>https://laurie-hxf.xyz/blog/deeplearning-l13</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/deeplearning-l13</guid><description>UMich EECS 498-007 Deep learning-Attention</description><pubDate>Sat, 28 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/attention.BHC4fMTo.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-26%2016.19.59.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;之前提到的RNN架构看起来挺好的，注意这里的图和Lecture 12的图是有一些差别的，他多了一个c向量，这里的 c 和 $s_0$ 都是 $h_4$ 。然后这里每次c还被连接到解码器的每一个时间步，与上一步的输出（y₀, y₁, y₂...）一起作为当前时间步的输入。因为如果 c 只用来初始化解码器，那么在生成很长的输出序列时，解码器可能会逐渐“忘记”最开始的上下文信息（因为RNN的隐藏状态在不断更新）。通过在每一步都将 c 作为输入，模型被不断地提醒原始句子的全局信息是什么。这极大地帮助模型保持翻译的一致性和准确性，防止信息丢失。&lt;/p&gt;
&lt;p&gt;而Lecture 12就没有这个 c，这里是一个小变化。&lt;/p&gt;
&lt;p&gt;但是即使这个架构看上去挺好的，但是如果你的上下文很长，你的所有信息都被压缩进这一个向量里面，这就有点不make sense。&lt;/p&gt;
&lt;h2&gt;Attention&lt;/h2&gt;
&lt;p&gt;终于来到大名鼎鼎的注意力了，他的思想就是当处理decoder的状态 $s_0$ 的时候，他会遍历encoder中的每一个状态 $h_i$ ，然后通过一个MLP $f_{att}$ 函数计算出他们的alignment scores，通过softmax归一化之后得到attention score。直观上理解就是得到当前模型觉得当前状态和之前的哪一个状态关系最密切，这样的一个分数。然后每一个attention score和对应的状态相乘再相加得到下一个状态的context vector，最终得到下一个状态，以此类推。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-26%2017.54.45.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;优势就是他解决了之前提到的bottlenecked的问题，而且上述的所有操作都是可导的，意思就是不是我们高速模型应该注意什么，而是模型自己决定自己要注意什么。&lt;/p&gt;
&lt;p&gt;有意思的是，在上述过程中，attention机制并没有用到 $h_i$ 是一个连续的这样一个特征，只是像从一个$h$集合中提取出一些 $h_i$，但并这不意味着attention没有用到连续这样一个特征，因为这个特征已经被包含在$h_i$ 本身中。所以attention更本质的就是可以自由地、非线性地从{h₁, h₂, h₃, h₄}这个信息池中直接提取，而不需要像RNN那样必须按h₁→h₂→h₃→h₄的顺序回溯。&lt;/p&gt;
&lt;p&gt;既然解码器只关心一个“向量集合”，那么这个集合不一定非要来自RNN编码器。它可以来自任何能将输入（如图片、声音等）转换成一组特征向量的模块。这极大地扩展了注意力机制的应用范围，使其成为一个非常灵活和强大的工具。&lt;/p&gt;
&lt;h3&gt;Image Captioning&lt;/h3&gt;
&lt;p&gt;上面说到，attention这种机制拓展了他的应用范围，于是我们就将它用来图像字幕生成，基本思想都是一样的，只不过我们将图片经过CNN之后会得到一个a grid of features for an image，然后就把每一块当作 $h_i$ 然后之后就类似，计算alignment score和attention score，然后重复。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-26%2020.23.45.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Attention Layer&lt;/h3&gt;
&lt;p&gt;这里我们定义一些东西ppt中的query vector就是q就是$s_i$，然后input vector就是之前的状态$h_i$。
随后我们对之前的架构进一步改进，我们将 $f_{att}$ 改成scaled dot product，trail and error发现点乘已经足够好的描述两个向量之间的相似度而且还简单。那什么是scaled呢，就是我们将两个向量点乘之后再除一个$\sqrt{D_Q}$ 目的就是为了避免当维度 $d$ 较大时，$Q$ 和 $K$ 的点积可能会变得&lt;strong&gt;非常大&lt;/strong&gt;，导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;经过 softmax 之后的分布非常“尖锐”；&lt;/li&gt;
&lt;li&gt;梯度会很小，容易引发梯度消失或训练不稳定；&lt;/li&gt;
&lt;li&gt;模型容易陷入局部最优。
例如，如果 $QK^\top$ 中的数值是几十甚至上百，softmax 之后就会非常偏向于某一个 token，其他信息会被忽略。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-26%2021.12.52.png&quot; alt=&quot;alt text&quot;&gt;
那么这时有一个问题，为什么不两个向量归一化后进行点乘，这样得出来的就是余弦相似。答案就是(来自chatgpt)&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;训练效率和表达能力更强
Dot product 是线性可微、连续可导、更适合训练。
余弦相似度中 $|q|$ 和 $|k|$ 是平方根操作，会造成非线性传播更复杂。
不对向量归一化，能让模型通过学习 $W^Q, W^K$ 自由地调节“相似度尺度”。
不归一化 = 模型可以同时学习“方向 + 强度”，表达能力更强&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;效率问题
点积可以通过矩阵乘法高效并行计算（在 GPU 上非常优化）
标准化（归一化）向量涉及除法和开根号，不好并行也会更慢
softmax 本身已经是归一化过程，没必要再归一化一次&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;学习能力更强（有“幅度”信息）
余弦相似度只看“角度”，忽略了向量长度。
而 Transformer 中的注意力，允许模型根据向量长度（幅度）来表达置信度或重要性。
例如：
模型可以让一个词的 Key 更长来表达它“特别重要”，即使它的方向不一定最匹配。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;softmax 本质上像是“可学习的归一化”
Softmax 会自动把 dot product 缩放到 [0, 1] 的权重上
我们只需要它的相对值，不需要真的做 cosine&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那问题又来了，既然scaled dot product这么好，为什么我们不所有的都使用这个呢，答案是(chatgpt)
就是有一些任务比如对比/分类中，我们希望忽略向量长度，只看方向&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“两个向量的语义是否类似” 是靠方向决定的&lt;/li&gt;
&lt;li&gt;向量有多长（置信度高低、频率高低）反而是噪声&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后我们再改进一点，首先就是我们多了两个东西，一个是key，另一个是value。他的作用就是一个和X相乘得到K，用K来获得attention score；另一个就是$XW_v$用来和attention score相乘，就相当于将X拆分成两个部分，原本就是用相同的X来获得attention score以及和attention score相乘，现在变成用两个X的线形变化来分别执行。&lt;/p&gt;
&lt;p&gt;为什么这样做呢？其一这两个矩阵是可学习的，那么其实就是相当于给神经网络更大的学习空间，更大的自由度；其二就是将它拆分就可以并行计算。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-26%2023.01.47.png&quot; alt=&quot;alt text&quot;&gt;
还有的改进就是他同时进行多个query的计算，也就是同时用多个$S$计算$C$。问题来了，我们难道不是用前一个$S_i$ 来推出下一个$S_{i+1}$吗，那这怎么并行计算。&lt;/p&gt;
&lt;p&gt;答案就是在训练的过程中我们采用的策略叫Teacher Forcing，我们实际上拿的是ground truth来作为输入，而不是上一次的输出作为下一次的输入。也就是说，所有的y都是ground truth，输出只是用来和groud truth比较然后计算损失函数，并没有作为下一次的输入。所以，这样子我们就可以并行的进行query计算。
下图描述的就是相应的流程
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-26%2023.21.39.png&quot; alt=&quot;alt text&quot;&gt;
这里描述的其实不是一个特定任务的架构而是一个general的Layer，你有两组向量X，Q你就可以找一些场景然后将这个拼接进去。也很容易理解，就是我们想把某个特定的方法让他变得更general一点，所以我们的attention机制也就越来越抽象和泛化。&lt;/p&gt;
&lt;h3&gt;Self-Attention Layer&lt;/h3&gt;
&lt;p&gt;这个属于特殊的情况，加入我们没有两组向量，我们只有一组X，那么思路就是我们再加一个矩阵$W_Q$用来生成query。然后他的架构就长下面这样
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-27%2015.56.34.png&quot; alt=&quot;alt text&quot;&gt;
我们可以很容易就可以推出，如果我的输入输入顺序换一下，从$X_1,X_2,X_3$ 换到$X_3,X_1,X_2$ 那么我们的输出也会从原来的 $Y_1,Y_2,Y_3$ 对应的换成 $Y_3,Y_1,Y_2$ ，也就是说，Self-attention layer是Permutation Equivariant(置换等变)。换句话说就是Attention不依赖输入顺序本身它只看 token 之间的相似性（通过 Query-Key 点积），而不在意 token 是第几位。这就让这个机制表达能力强，更通用，因为它可以处理各种顺序、不定长甚至无序的输入。Permutation Equivariance 说明 Attention 本身不偏向任何顺序结构——这是一种很强的结构对称性。&lt;/p&gt;
&lt;p&gt;如果你在意顺序的话，你就必须显式地加入位置编码（positional encoding），否则模型无法理解顺序。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-27%2016.26.40.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Masked Self-Attention Layer&lt;/h4&gt;
&lt;p&gt;有时候，我们想让这个模型类似RNN一样，他不会提前知道下一个输入是什么，所以我们加一个mask，具体比如对于$Q_1$ 他不会知道$K_2,K_3$ 是什么他只知道当前的输入$K_1$，对于$Q_2$他不会知道$K_3$，他只知道历史的输入$K_1,K_2$
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-27%2016.36.07.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Multihead Self-Attention&lt;/h4&gt;
&lt;p&gt;这个真的妙，多头注意力机制就是将一个query拆分，然后和别的query组合跑多个attention layer
每个头的$W^Q$, $W^K$, $W^V$ 是不一样的，自己学习来的。直观上理解，他比单头学的更细。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-27%2016.54.26.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;CNN with Self-Attention&lt;/h4&gt;
&lt;p&gt;之前说self attention已经可以general成一个层了，所以这是把他用在CNN上的一个例子
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-28%2015.52.30.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Transformer&lt;/h2&gt;
&lt;p&gt;这就是transformer layer的架构，将向量用多头注意力机制输入到Self-Attention layer中，然后经过Layer Normalization，MLP ，Layer Normalization，最终得到输出。中间还加了一些残差连接。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-28%2017.54.11.png&quot; alt=&quot;alt text&quot;&gt;
我们将transformer layer堆起来就组成transformer架构，这就是大名鼎鼎的Transformer。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-28%2017.55.55.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;</content:encoded><img src="/_astro/attention.BHC4fMTo.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/attention.BHC4fMTo.png" length="0" type="image/png"/></item><item><title>Note 软件开发</title><link>https://laurie-hxf.xyz/blog/note-%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/note-%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91</guid><description>A lightweight note app for macOS</description><pubDate>Tue, 24 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/Note.BlMN3GV6.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;大概从上学期开始就有一个想法就是开发一个便签，之前找过一些市面上的便签。之前一直用的是simple antnotes，用的到还可以，只是有时用的不是很爽，之前的电脑有时莫名其妙的重启不知道原因，但是怀疑和这个软件有关系，而且我觉得这软件做的不够优雅，苹果自带的便签也好丑。而且我还想要我的便签能支持markdown，就像obsidian和typora那样支持实时编译，那样可以满足我的需求。&lt;/p&gt;
&lt;p&gt;所以就有了一些想法，我一个学计算机的连软件都不会开发那就有点逊了。正好之前在数据库上老师讲过一些方案，主要介绍的就是js+electron，一方面他很方便，不用学习每个os的细节，基本上你只用会前端就差不多能开发。而且因为他是基于浏览器作为内核，所以基本上写一次就可以在不同的系统上跑。&lt;/p&gt;
&lt;p&gt;本博客就记录一下一个新手小白的开发流程。
by the way，这个项目放在&lt;a href=&quot;https://github.com/laurie-hxf/note&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;准备&lt;/h2&gt;
&lt;p&gt;一开始就了解到js的重要性，然后选了&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript&quot;&gt;这个网站&lt;/a&gt;作为入门，感觉挺好的，挺细致的。还有就是electron的&lt;a href=&quot;https://www.electronjs.org/docs/latest/&quot;&gt;网站&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;js的语法感觉没有很复杂，我看了一下他的网址，看完了第一个项目就不想看了，不如直接开发，边开发边学习，主要是我觉得没有很难理解，而且这种做法效率还高。&lt;/p&gt;
&lt;p&gt;基本上遇到什么问一下gpt，效率还是挺高的。&lt;/p&gt;
&lt;p&gt;然后就是electron，这个我感觉挺有意思的，就简单记录一下我用到的命令&lt;/p&gt;
&lt;p&gt;首先就是安装，这里我用了淘宝的镜像，之前一直都安装不上&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install electron --save-dev 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;快速创建一个默认的 &lt;code&gt;package.json&lt;/code&gt; 文件，用于管理你的 Node 项目依赖、脚本、版本等信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm init -y  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令本质是运行&lt;code&gt;package.json&lt;/code&gt;中&lt;code&gt;scripts.start&lt;/code&gt;的部分，取决于你的&lt;code&gt;package.json&lt;/code&gt;部分怎么写&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;scripts&quot;: {
  &quot;start&quot;: &quot;electron .&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后等价于运行electron .这个命令，它会用 Electron 启动你当前的项目目录，作为一个桌面应用运行。可以用来实时看你的软件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm run start 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要是想要将你的软件打包成一个app之类的话，就要安装electron-builder&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm install electron-builder --save-dev    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要想能在mac上打包成win的exe文件，就要先安装这个虚拟环境&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;brew install --cask --no-quarantine wine-stable    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用来将软件打包成win-x64架构的指令，如果不加后面的x64，他就会默认使用当前电脑的架构，像我就是ARM架构的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx electron-builder --win --x64   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行这个命令的话就是运行 &lt;code&gt;scripts.build&lt;/code&gt; 定义的命令，比如打包构建。本质上就是在运行electron-builder这个命令，然后顶层 &lt;code&gt;&quot;build&quot;&lt;/code&gt; 部分就是具体的配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm run build  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面贴一个我的&lt;code&gt;package.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
&quot;devDependencies&quot;: {
&quot;electron&quot;: &quot;^36.5.0&quot;,
&quot;electron-builder&quot;: &quot;^26.0.12&quot;
},
&quot;name&quot;: &quot;note&quot;,
&quot;version&quot;: &quot;1.0.0&quot;,
&quot;main&quot;: &quot;main.js&quot;,
&quot;dependencies&quot;: {
&quot;boolean&quot;: &quot;^3.2.0&quot;,
&quot;buffer-crc32&quot;: &quot;^0.2.13&quot;,
&quot;cacheable-lookup&quot;: &quot;^5.0.4&quot;,
&quot;cacheable-request&quot;: &quot;^7.0.4&quot;,
&quot;clone-response&quot;: &quot;^1.0.3&quot;,
&quot;debug&quot;: &quot;^4.4.1&quot;,
&quot;decompress-response&quot;: &quot;^6.0.0&quot;,
&quot;defer-to-connect&quot;: &quot;^2.0.1&quot;,
&quot;define-data-property&quot;: &quot;^1.1.4&quot;,
&quot;define-properties&quot;: &quot;^1.2.1&quot;,
&quot;detect-node&quot;: &quot;^2.1.0&quot;,
&quot;end-of-stream&quot;: &quot;^1.4.5&quot;,
&quot;env-paths&quot;: &quot;^2.2.1&quot;,
&quot;es-define-property&quot;: &quot;^1.0.1&quot;,
&quot;es-errors&quot;: &quot;^1.3.0&quot;,
&quot;es6-error&quot;: &quot;^4.1.1&quot;,
&quot;escape-string-regexp&quot;: &quot;^4.0.0&quot;,
&quot;extract-zip&quot;: &quot;^2.0.1&quot;,
&quot;fd-slicer&quot;: &quot;^1.1.0&quot;,
&quot;fs-extra&quot;: &quot;^8.1.0&quot;,
&quot;get-stream&quot;: &quot;^5.2.0&quot;,
&quot;global-agent&quot;: &quot;^3.0.0&quot;,
&quot;globalthis&quot;: &quot;^1.0.4&quot;,
&quot;gopd&quot;: &quot;^1.2.0&quot;,
&quot;got&quot;: &quot;^11.8.6&quot;,
&quot;graceful-fs&quot;: &quot;^4.2.11&quot;,
&quot;has-property-descriptors&quot;: &quot;^1.0.2&quot;,
&quot;http-cache-semantics&quot;: &quot;^4.2.0&quot;,
&quot;http2-wrapper&quot;: &quot;^1.0.3&quot;,
&quot;json-buffer&quot;: &quot;^3.0.1&quot;,
&quot;json-stringify-safe&quot;: &quot;^5.0.1&quot;,
&quot;jsonfile&quot;: &quot;^4.0.0&quot;,
&quot;keyv&quot;: &quot;^4.5.4&quot;,
&quot;lowercase-keys&quot;: &quot;^2.0.0&quot;,
&quot;matcher&quot;: &quot;^3.0.0&quot;,
&quot;mimic-response&quot;: &quot;^1.0.1&quot;,
&quot;ms&quot;: &quot;^2.1.3&quot;,
&quot;normalize-url&quot;: &quot;^6.1.0&quot;,
&quot;object-keys&quot;: &quot;^1.1.1&quot;,
&quot;once&quot;: &quot;^1.4.0&quot;,
&quot;p-cancelable&quot;: &quot;^2.1.1&quot;,
&quot;pend&quot;: &quot;^1.2.0&quot;,
&quot;progress&quot;: &quot;^2.0.3&quot;,
&quot;pump&quot;: &quot;^3.0.3&quot;,
&quot;quick-lru&quot;: &quot;^5.1.1&quot;,
&quot;resolve-alpn&quot;: &quot;^1.2.1&quot;,
&quot;responselike&quot;: &quot;^2.0.1&quot;,
&quot;roarr&quot;: &quot;^2.15.4&quot;,
&quot;semver&quot;: &quot;^6.3.1&quot;,
&quot;semver-compare&quot;: &quot;^1.0.0&quot;,
&quot;serialize-error&quot;: &quot;^7.0.1&quot;,
&quot;sprintf-js&quot;: &quot;^1.1.3&quot;,
&quot;sumchecker&quot;: &quot;^3.0.1&quot;,
&quot;type-fest&quot;: &quot;^0.13.1&quot;,
&quot;undici-types&quot;: &quot;^6.21.0&quot;,
&quot;universalify&quot;: &quot;^0.1.2&quot;,
&quot;wrappy&quot;: &quot;^1.0.2&quot;,
&quot;yauzl&quot;: &quot;^2.10.0&quot;
},
&quot;scripts&quot;: {
&quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;#x26;&amp;#x26; exit 1&quot;,
&quot;start&quot;: &quot;electron .&quot;,
&quot;build&quot;: &quot;electron-builder&quot;
},
&quot;build&quot;: {
&quot;appId&quot;: &quot;com.yourname.guessgame&quot;,
&quot;productName&quot;: &quot;Note&quot;,
&quot;icon&quot;: &quot;./icon&quot;,
&quot;files&quot;: [
&quot;**/*&quot;
],
&quot;directories&quot;: {
&quot;output&quot;: &quot;dist&quot;
},
&quot;mac&quot;: {
&quot;target&quot;: &quot;dmg&quot;
},
&quot;win&quot;: {
&quot;target&quot;: &quot;nsis&quot;
},
&quot;linux&quot;: {
&quot;target&quot;: &quot;AppImage&quot;
}
},
&quot;keywords&quot;: [],
&quot;author&quot;: &quot;&quot;,
&quot;license&quot;: &quot;ISC&quot;,
&quot;description&quot;: &quot;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;进展&lt;/h2&gt;
&lt;p&gt;目前大概就是一个普通的便签，还没有支持markdown，也没有实时编译，然后这个项目的大部份代码还是ai写的，前端css那些，~~一点没有设计天赋~~，包括一些js，所以还要去学习这些代码后续再进一步开发。而且window和mac上都有一些小bug，但不影响使用，后续还要进一步调整。&lt;/p&gt;
&lt;h2&gt;发布&lt;/h2&gt;
&lt;p&gt;初步完成之后，就上架了github，发布了release，加了一下开源协议，又学到新技能。&lt;/p&gt;
&lt;p&gt;然后就是将一些视频可以加进README里面，准确来说不是视频，而是gif，markdown支持播放gif。然后下面的命令就是将视频转成gif。ffmpeg nb，&lt;a href=&quot;https://bellard.org/&quot;&gt;bellard&lt;/a&gt; nb。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ffmpeg -i 录屏2025-06-23\ 20.01.35.mov -vf &quot;fps=15&quot; demo2.gif       
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有就是一个工具可以把你按的键位显示在屏幕上，&lt;a href=&quot;https://github.com/keycastr/keycastr&quot;&gt;这个工具&lt;/a&gt;也挺出名的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;brew install --cask keycastr    
&lt;/code&gt;&lt;/pre&gt;</content:encoded><img src="/_astro/Note.BlMN3GV6.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/Note.BlMN3GV6.png" length="0" type="image/png"/></item><item><title>Lecture12 Recurrent Networks</title><link>https://laurie-hxf.xyz/blog/deeplearning-l12</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/deeplearning-l12</guid><description>UMich EECS 498-007 Deep learning-Recurrent Networks</description><pubDate>Sun, 22 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-06-20 18.52.40.BMOurPgx.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-19%2021.59.07.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;之前我们讲的一直都是图像识别，也就是单图片单输出，但是我们还想用deeplearning来做些更多的事情，比如&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one to many 输入一张图片，然后输出一系列描述这个图片的语言&lt;/li&gt;
&lt;li&gt;many to one 输入一系列图片，然后识别。比如视频的识别&lt;/li&gt;
&lt;li&gt;many to many 比如翻译，以及每一帧的识别&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;RNN&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-19%2022.12.36.png&quot; alt=&quot;alt text&quot;&gt;
他的想法就是我们根据当前时间段的输入$x_t$和前一段时间的状态$h_{t-1}$来决定当前时间段的输出$h_t$，然后我们就要学两个参数$W_{hh},W_{xh}$ 。
$tanh(x)=\frac{e^x+e^{−x}}{e^x−e^{−x}}​$双曲正切是一个激活函数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-19%2022.22.35.png&quot; alt=&quot;alt text&quot;&gt;
所以他的流程就类似这种，值得注意的是，他的W对于每一个状态而言都是固定的且共享的。但是从直觉上来看，如果我对每一次的输入和状态都有一个特定的W的话，我的模型的能力不应该会更强吗。GPT解释道RNN的本质是要对任意长度的序列进行统一建模，如果我的每一个时间步都有一个独立的权重，那我似乎只能处理固定长度的输入；其二是训练参数的减少；其三是RNN更像是循环执行一个神经单元，就想人处理语言时，面对不同位置的词，其处理机制是类似的，只是记忆（上下文）不同。&lt;/p&gt;
&lt;p&gt;但这些吧，感觉似乎都可以被解决，anyway～&lt;/p&gt;
&lt;h4&gt;Many to Many&lt;/h4&gt;
&lt;p&gt;我们在每个时间步都输入数据，然后每个时间步输出，同时他又对应的损失函数，将每一个时间步的损失函数汇总在一起就可以得到总的损失函数
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-19%2022.42.38.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Many to One&lt;/h4&gt;
&lt;p&gt;我们只在最后一个时间步给出一个输出，比如用来做视频的分类
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-19%2022.44.19.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;One to Many&lt;/h4&gt;
&lt;p&gt;我们只在一开始给输入，然后后面就没有
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-19%2022.44.41.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Sequence to Sequence&lt;/h4&gt;
&lt;p&gt;上面提到的多对多一般是输出和输入的长度是一样的，但是有的时候我们想要的输入输出的长度可能不一样。比如翻译，两个不同的语言描述同一个事情所用的tokens可能是不同的，所以为了解决这个问题，就有Many to One + One to Many这两个模型拼接在一起，我们将一开始的输入输入到Many to One模型中，将最后得到的输出输入到One to Many的模型中，前面一个叫encoder，后面一个叫decoder。注意这里我们用两套不同的权重。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-19%2022.46.26.png&quot; alt=&quot;alt text&quot;&gt;
但是这就引出一个问题，第二个模型怎么控制输出的长度呢？&lt;/p&gt;
&lt;h5&gt;Language Modeling&lt;/h5&gt;
&lt;p&gt;这里以大模型举例子我觉得还是很好理解的，每次预测的输出当作下一次的输入，这样你就可以无限的输出
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-19%2023.12.10.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;这一部分主要讲的就是一开始我们用one hot作为输入，但是这个很明显这里面包含的信息太少且太稀疏了，所以就用一个embedding layer。这个本质就是一个矩阵，one hot乘了这个矩阵本质就是提取这个矩阵中对应的一列，他和one hot同样可以表示相同的值，但是他的维数会更低，信息会更密集，更主要的是这个layer是可以学习的，他可以学习词与词之间的关系，从而调整矩阵的权重，这样他里面的一列他就可以包含更多的信息。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-19%2023.13.38.png&quot; alt=&quot;alt text&quot;&gt;
那么他是怎么更新权重的呢？
首先自然而然的就是把所有时间步的损失加起来，然后最后反向传播更新权重，但是这样的做法有一个很大的弊端就是他太占用内存的空间了，没有那么多显存。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-20%2018.14.18.png&quot; alt=&quot;alt text&quot;&gt;
于是就有下一个方法
我们将这个序列分成一小块一小块的，每次计算完一小块的loss我们就更新他的权重，这样确实节省了很多内存，但是这时候就有一个问题就是他很难学习到很远的内容之间的联系，直观上看，他只是根据一小块里面的内容来更新权重，所以他学习到的权重应该只是那一小块的。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-20%2018.16.26.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h5&gt;Visual Modeling&lt;/h5&gt;
&lt;p&gt;回到视觉上面的话，这里我们考虑的是一对多的场景，给一幅图片然后生成描述文字，我们先用CNN提取出图像特征，然后将这个特征输入到RNN中，他的函数就多加一个$W_{ih}v$ 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-20%2018.52.40.png&quot; alt=&quot;alt text&quot;&gt;
然后值得一提的就是我们一开始输入了&amp;#x3C;START&gt;然后最后输出&amp;#x3C;END&gt;,当模型输出&amp;#x3C;END&gt;的时候我们就终止继续生成。由此解决之前的问题。&lt;/p&gt;
&lt;h4&gt;Vanilla RNN Gradient Flow&lt;/h4&gt;
&lt;p&gt;我们之前讲的属于Vanilla RNN模型，然后他的梯度计算就像是图中所示
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-21%2023.27.32.png&quot; alt=&quot;alt text&quot;&gt;
仔细推导一下他的反向传播，假设我们现在有3个时间步
$$
\begin{array}{r} h_1 = \tanh(W * h_0 + U * X_1) \ h_2 = \tanh(W * h_1 + U * X_2) \ h_3 = \tanh(W * h_2 + U * X_3) \end{array}
$$
整合一下就是
$$
h_3=\tanh(W\cdot\tanh(W\cdot\tanh(Wh_0+Ux_1)+Ux_2)+Ux_3)
$$&lt;/p&gt;
&lt;p&gt;然后令
$$
\begin{aligned}
&amp;#x26; &amp;#x26; &amp;#x26; a_1=Wh_0+Ux_1 \
&amp;#x26; &amp;#x26; &amp;#x26; h_1=\tanh(a_1) \
&amp;#x26; &amp;#x26; &amp;#x26; a_2=Wh_1+Ux_2 \
&amp;#x26; &amp;#x26; &amp;#x26; h_2=\tanh(a_2) \
&amp;#x26; &amp;#x26; &amp;#x26; a_3=Wh_2+Ux_3 \
&amp;#x26; &amp;#x26; &amp;#x26; h_3=\tanh(a_3)
\end{aligned}
$$
那么对W的梯度就应该是
$$
\frac{\partial h_3}{\partial W}=\frac{\partial h_3}{\partial a_3}\cdot\left(\frac{\partial a_3}{\partial W}+\frac{\partial a_3}{\partial h_2}\cdot\frac{\partial h_2}{\partial a_2}\cdot\left(\frac{\partial a_2}{\partial W}+\frac{\partial a_2}{\partial h_1}\cdot\frac{\partial h_1}{\partial a_1}\cdot\frac{\partial a_1}{\partial W}\right)\right)
$$
初步计算一下转化为
$$\frac{\partial h_3}{\partial W}=D_3\cdot\left(\frac{\partial a_3}{\partial W}+W\cdot D_2\cdot\left(\frac{\partial a_2}{\partial W}+W\cdot D_1\cdot\frac{\partial a_1}{\partial W}\right)\right)
$$
注意到这个是式子中相当于有很多的W连乘，然后对于一个矩阵W，我们可以对他进行奇异值分解
$$
W = U * Σ * V^T
$$
其中U,V属于旋转矩阵，Σ是个对角矩阵，他里面的值就是奇异值，他就会对向量进行伸缩，当我们对W进行不断的连乘的时候如果他的奇异值大于1，那么他就会一直放大某个方向上的分量，这就会导致所谓的梯度爆炸；相反，如果奇异值小于1，那么他就会一直压缩某一方向上的分量，这就叫梯度消失。&lt;/p&gt;
&lt;p&gt;对于RNN中的梯度爆炸问题，解决方法就是&lt;strong&gt;梯度裁剪（Gradient Clipping）&lt;/strong&gt;
设一个参数的总梯度是向量 $g$，我们限制它的 2-范数（L2 范数）不超过某个阈值 $\tau$。
设有函数$f(x_1,x_2,\ldots,x_n)$,其梯度为：&lt;/p&gt;
&lt;p&gt;$$
\nabla f=\left[\frac{\partial f}{\partial x_1},\frac{\partial f}{\partial x_2},\ldots,\frac{\partial f}{\partial x_n}\right]^\top
$$&lt;/p&gt;
&lt;p&gt;梯度向量的范数 (通常是 2-范数) 表示该梯度的“整体变化强度”:&lt;/p&gt;
&lt;p&gt;$$
|\nabla f|=\sqrt{\left(\frac{\partial f}{\partial x_1}\right)^2+\cdots+\left(\frac{\partial f}{\partial x_n}\right)^2}
$$
如果：
$$
| g |_2 &gt; \tau
$$
就进行缩放：
$$
g \leftarrow \tau \cdot \frac{g}{| g |_2}
$$
也就是按比例缩小它，使总长不超过 $\tau$。&lt;/p&gt;
&lt;p&gt;那么对于梯度消失而言呢，为什么不采用同等的方式对梯度进行放大呢，感觉这个就有点经验性了，有的解释就是他的梯度变得越来越小，也就是他里面有用的信息越来越少，如果我们对他放大，很有可能放大的就是噪声。而经验告诉更好的做法反倒是重新改进结构。&lt;/p&gt;
&lt;h2&gt;Long Short Term Memory (LSTM)&lt;/h2&gt;
&lt;p&gt;为了解决梯度消失的问题，人们就想出LSTM，这想法感觉很神奇。他引入了一个c，叫做cell state，主要思想就是将h和x结合起来和W相乘之后，将每一列取出来分别作为gate，然后执行不同的运算
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-22%2000.43.44.png&quot; alt=&quot;alt text&quot;&gt;
如下图所示，大概就是这样，然后他的解释就是上图那些gate的定义，感觉就很tricky
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-22%2000.46.22.png&quot; alt=&quot;alt text&quot;&gt;
所以他到底是怎么解决梯度消失的呢，主要思想就是对于C而言，这条通路上的梯度几乎没有什么损失，虽然说我们在对W进行梯度更新的时候还是会有激活函数，矩阵乘法这种求导会有损失，但他的思想就是他保留了一部分，至少没有损失的一部分。从而避免像Vanilla RNN中的W的更新一直是损失的，这个思想其实和ResNet很像，就像提供了一条高速路一样。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-22%2000.49.15.png&quot; alt=&quot;alt text&quot;&gt;
有意思的是上课有同学提到到底是怎么想到这种结构的，答案就是research，很搞笑，就是很多人不断的试，不断的验证新的点子，总有人最终会想到一个有用的点。好听一点叫Trial and error，地狱一点叫炼丹，这时候才想起来有人曾经说过的一句话，我们需要出现deeplearning中的香农，太有感觉了。&lt;/p&gt;
&lt;p&gt;最后的内容就是我们可以将CNN中多层的思想放到RNN中，我们将hidden state的输出直接作为下一层的输入。通常来说每一层的W是不一样的，然后一般不会像CNN一样放很多层。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-22%2001.01.50.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-06-20 18.52.40.BMOurPgx.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-06-20 18.52.40.BMOurPgx.png" length="0" type="image/png"/></item><item><title>学生期末考试排列启发式算法</title><link>https://laurie-hxf.xyz/blog/%E5%AD%A6%E7%94%9F%E6%9C%9F%E6%9C%AB%E8%80%83%E8%AF%95%E6%8E%92%E5%88%97%E5%90%AF%E5%8F%91%E5%BC%8F%E7%AE%97%E6%B3%95</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E5%AD%A6%E7%94%9F%E6%9C%9F%E6%9C%AB%E8%80%83%E8%AF%95%E6%8E%92%E5%88%97%E5%90%AF%E5%8F%91%E5%BC%8F%E7%AE%97%E6%B3%95</guid><description>一次有关学生期末考试排列算法的很小思考</description><pubDate>Sun, 15 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-06-15 17.40.33.BhkhL5RA.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&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;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;首先，我们将所有学生的选课放入一个集合中，每一个学生就有自己对应的一个集合。然后这些集合之间就有交集，然后我们就把所有学生的集合放到一起，组成一个复杂的图案。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-15%2017.40.33.png&quot; alt=&quot;alt text&quot;&gt;
如图就是就假设学生1这学期修的课程是O,P,Q,M,N，然后以此类推。那么我们可以根据这个有一些发现&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;/ol&gt;
&lt;p&gt;ok，那么我的思路就是把图中没有包含在任何交集中的课程拿出来，然后对于在同一个集合中的这些课程我们看作是一组。比如图中，O-P-Q是一组，G-H-I是一组以此类推，然后我把这一组叫做一条并行链，每一列表示一个时间段。那么我们可以画出4条并行链：&lt;/p&gt;
&lt;p&gt;| 链1  | A   | B   | C   |
| --- | --- | --- | --- |
| 链2  | O   | P   | Q   |
| 链3  | G   | H   | I   |
| 链4  | L   |     |     |&lt;/p&gt;
&lt;p&gt;随后，剩下的课程都是在集合中的交集部分，随后我们依次遍历所有集合中的交集部分的元素，然后添加进表中，比如对于链1而言，我们依次添加进F,J,D,E 同时也把该课程添加进该课程所在的别的链中。此时注意，如果我们在添加课程到别的链中的时候，发生冲突，则向后找合适时间段，找到第一个合适的时间段就放入。&lt;/p&gt;
&lt;p&gt;| 链1  | A   | B   | C   | F   | J   | D   | E   |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 链2  | O   | P   | Q   |     |     |     |     |
| 链3  | G   | H   | I   | F   | J   |     |     |
| 链4  | L   |     |     |     | J   | D   | E   |&lt;/p&gt;
&lt;p&gt;然后以此类推到链二&lt;/p&gt;
&lt;p&gt;| 链1  | A   | B   | C   | F   | J   | D   | E   |     |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 链2  | O   | P   | Q   | M   |     |     |     | N   |
| 链3  | G   | H   | I   | F   | J   |     |     |     |
| 链4  | L   |     |     | M   | J   | D   | E   | N   |&lt;/p&gt;
&lt;p&gt;链三&lt;/p&gt;
&lt;p&gt;| 链1  | A   | B   | C   | F   | J   | D   | E   |     |     |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 链2  | O   | P   | Q   | M   |     |     |     | N   |     |
| 链3  | G   | H   | I   | F   | J   |     |     |     | K   |
| 链4  | L   |     |     | M   | J   | D   | E   | N   | K   |&lt;/p&gt;
&lt;p&gt;链四没有变化&lt;/p&gt;
&lt;p&gt;于是乎我们就得到了一个没有冲突的安排表，并且一定程度上的并行了一些课程。&lt;/p&gt;
&lt;p&gt;当想到这一步的时候，我觉得我应该找到最优解，但是当我把这个问题问GPT的时候，他跟我说这是一个NP=P的问题，意思就是目前为止我们没办法得出一个不是O(n!)方法来找到最优解。&lt;/p&gt;
&lt;p&gt;确实如此，本质上来说，我的做法就是贪心，局部最优不一定全局最优，比如图中我可以这样安排&lt;/p&gt;
&lt;p&gt;| 链1  | A   | B   | C   | F   | J   | D   | E   |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 链2  | O   | P   | N   | M   | Q   |     |     |
| 链3  | K   | H   | I   | F   | J   | G   |     |
| 链4  | K   | L   | N   | M   | J   | D   | E   |&lt;/p&gt;
&lt;p&gt;这是一个全局最优解，因为&lt;code&gt;总的期末考试时间&gt;=最大学生的选课数&lt;/code&gt; ，所以至少需要安排7场考试才能满足所有学生。&lt;/p&gt;
&lt;p&gt;问题到这里，还没有结束，原本我以为我的贪心做法是最优的时候，我就继续去找安排课室数量的最优，但很可惜，在有限的时间内我想过将表中的点连起来，有共同的就用一根线连起来，在一条并行链中的也用线连起来，空的就用一个空符号连一起，这样就变成一个图的问题。类似这种：&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-15%2018.30.23.png&quot; alt=&quot;alt text&quot;&gt;
或者这种，类似华容道一样排列这些
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-15%2018.33.16.png&quot; alt=&quot;alt text&quot;&gt;
使得空间利用最大化&lt;/p&gt;
&lt;h2&gt;感悟&lt;/h2&gt;
&lt;p&gt;当思来想去，很复杂，也不是很清楚，于是到此我就查询了ChatGPT，于是才得知这是一个NP=P的问题，而且还有更简单的建模方法，叫图着色问题，然后有一系列的算法试图去解决它。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-15%2018.40.45.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-06-15%2018.41.04.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;查了才知道，我这种属于启发式算法，&lt;code&gt;在求解复杂问题时，不追求“绝对最优解”，而是使用基于经验、规则或贪心策略的近似方法，以较低计算代价快速得到“足够好”的可行解。&lt;/code&gt;还是有一定意义的。&lt;/p&gt;
&lt;p&gt;怎么说呢，有种凭自己接触到边界的感觉，这种感觉还挺少有的，当晚可能脑子太兴奋了，以至于晚上2，3点才睡着。&lt;/p&gt;
&lt;p&gt;怎么说呢，这篇博客写于一个应该复习算法考试的下午，只是简单的记录一下一次思考吧，我感觉我的博客也可以添加进去这些。&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-06-15 17.40.33.BhkhL5RA.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-06-15 17.40.33.BhkhL5RA.png" length="0" type="image/png"/></item><item><title>Lecture8 CNN Architectures</title><link>https://laurie-hxf.xyz/blog/deeplearning-l8</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/deeplearning-l8</guid><description>UMich EECS 498-007 Lecture8 CNN Architectures</description><pubDate>Wed, 30 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/111.NGrZ1MUk.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;AlexNet&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-30%2010.36.19.png&quot; alt=&quot;alt text&quot;&gt;
这是最早的卷积神经网络，参数如下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;227 x 227 inputs&lt;/li&gt;
&lt;li&gt;5 Convolutional layers&lt;/li&gt;
&lt;li&gt;Max pooling&lt;/li&gt;
&lt;li&gt;3 fully-connected layers&lt;/li&gt;
&lt;li&gt;ReLU nonlinearities
他这里Trained on two GTX 580 GPUs – only 3GB of memory each! Model split over two GPUs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;值得一提的是需要知道的是模型的计算量(FLOPs)，参数量(Params)，占用内存(Memory)。这些和训练及其相关，当你更改神经网络的结构的时候，如果你希望公平对比性能、避免引入冗余，你需要确保计算量（FLOPs）基本持平。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-30%2011.16.53.png&quot; alt=&quot;alt text&quot;&gt;
这里拿AlexNet的卷积层为例子&lt;/p&gt;
&lt;h4&gt;Memory：&lt;/h4&gt;
&lt;p&gt;$$
\begin{aligned}
\text { Number of output elements } &amp;#x26; =C * H^{\prime} * W^{\prime} \
&amp;#x26; =64 * 56^* 56=200,704
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;Bytes per element = 4 (for 32-bit floating point)
$$
\begin{aligned}
\mathrm{KB} &amp;#x26; =(\text { number of elements }) *(\text { bytes per elem }) / 1024 \
&amp;#x26; =200704 * 4 / 1024 \
&amp;#x26; =784
\end{aligned}
$$&lt;/p&gt;
&lt;h4&gt;Params：&lt;/h4&gt;
&lt;p&gt;$$
\begin{aligned} &amp;#x26; \text { Weight shape }=C_{\text {out }} \times C_{i n} \times K \times K \ &amp;#x26; =64 \times 3 \times 11 \times 11 \ &amp;#x26; \text { Bias shape }=C_{\text {out }}=64 \ &amp;#x26; \text { Number of weights }=64 * 3 * 11 * 11+64 \ &amp;#x26; \qquad \begin{aligned} &amp;#x26; =23,296\end{aligned}\end{aligned}
$$&lt;/p&gt;
&lt;h4&gt;FLOPs&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-30%2011.24.35.png&quot; alt=&quot;alt text&quot;&gt;
这里值得一提的是他们把一个乘法和一个加法看作一个运算，因为现在的设备已经可以在一个时钟周期里面完成这两个运算&lt;/p&gt;
&lt;h2&gt;ZFNet&lt;/h2&gt;
&lt;p&gt;更大的AlexNet
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-30%2011.01.31.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;VGG&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-30%2011.06.27.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;他的改进之处就是相比于前两个，他更规范，意思就是前两个每个层的超参数以及怎么知道在哪个地方放什么层，这些都是Trial and error反复试错的结果，一旦我想要更大规模的结果，我就要重新Trial and error这些配置。所以VGG就把这个规范化，就有一个design rules(如上图)，然后这样子就方便扩大网络的规模。&lt;/p&gt;
&lt;h2&gt;GoogleNet&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-30%2011.27.07.png&quot; alt=&quot;alt text&quot;&gt;
这里最值得提的是，他没有一昧的追求扩大模型规模，而是关注模型的效率，他把最后的全连接层去掉了，而是用一个全局平均池化（GAP）来代替
GAP 是一种 &lt;strong&gt;把整个特征图（feature map）直接变成一个数&lt;/strong&gt; 的方法，它对每个通道取平均值：
假设某一层输出为大小 H×W×C 的特征图&lt;br&gt;
→ GAP 会对每个通道 c 求：
$$y_c=\frac1{H⋅W}\sum_{i=1}^H\sum_{j=1}^Wx_{i,j}$$
最后变成一个长度为 C 的向量，作为最终的输出（可直接接 Softmax）。
好处就是极大的减少了计算量，因为全连接层的参数量是最大的，想想全连接层中的矩阵，节约了这些的计算量&lt;/p&gt;
&lt;p&gt;但是模型发展到这里遇到了瓶颈，当你堆叠很多层神经网络时，理论上模型应该越来越好，但实际中会出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;训练误差先下降后上升&lt;/strong&gt;（不是过拟合，而是梯度消失&lt;a href=&quot;%E5%9C%A8%E5%8F%8D%E5%90%91%E4%BC%A0%E6%92%AD%EF%BC%88Backpropagation%EF%BC%89%E6%97%B6%EF%BC%8C%E6%B7%B1%E5%B1%82%E7%BD%91%E7%BB%9C%E4%B8%AD%E7%9A%84%E6%A2%AF%E5%BA%A6%E5%9C%A8%E4%B8%80%E5%B1%82%E5%B1%82%E4%BC%A0%E6%92%AD%E5%9B%9E%E5%8E%BB%E7%9A%84%E8%BF%87%E7%A8%8B%E4%B8%AD%E5%8F%98%E5%BE%97%E8%B6%8A%E6%9D%A5%E8%B6%8A%E5%B0%8F%EF%BC%8C%E6%9C%80%E5%90%8E%E5%87%A0%E4%B9%8E%E4%B8%BA0%EF%BC%8C%E5%AF%BC%E8%87%B4%EF%BC%9A%E5%89%8D%E9%9D%A2%E5%87%A0%E5%B1%82%E7%9A%84%E6%9D%83%E9%87%8D%E5%87%A0%E4%B9%8E%E4%B8%8D%E6%9B%B4%E6%96%B0%EF%BC%8C%E6%94%B6%E6%95%9B%E9%9D%9E%E5%B8%B8%E6%85%A2%EF%BC%8C%E7%94%9A%E8%87%B3%E6%A0%B9%E6%9C%AC%E6%97%A0%E6%B3%95%E5%AD%A6%E4%B9%A0&quot;&gt;^1&lt;/a&gt;、优化困难）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络越深，反而效果变差&lt;/strong&gt;（退化问题&lt;a href=&quot;%E5%BD%93%E4%BD%A0%E5%8A%A0%E6%B7%B1%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%B1%82%E6%95%B0%E6%97%B6%EF%BC%8C%E7%90%86%E8%AE%BA%E4%B8%8A%E6%A8%A1%E5%9E%8B%E8%A1%A8%E7%8E%B0%E5%BA%94%E8%AF%A5%E6%9B%B4%E5%A5%BD%EF%BC%8C%E4%BD%86%E5%AE%9E%E9%99%85%E4%B8%8A%E4%BC%9A%E5%87%BA%E7%8E%B0:%E8%AE%AD%E7%BB%83%E8%AF%AF%E5%B7%AE%E5%8F%8D%E8%80%8C%E6%9B%B4%E9%AB%98,%E6%A8%A1%E5%9E%8B%E4%B8%8D%E5%A6%82%E6%B5%85%E5%B1%82%E7%BD%91%E7%BB%9C,%E4%B8%8D%E6%98%AF%E8%BF%87%E6%8B%9F%E5%90%88%EF%BC%8C%E5%9B%A0%E4%B8%BA%E8%AE%AD%E7%BB%83%E9%9B%86%E4%B8%8A%E9%83%BD%E5%AD%A6%E4%B8%8D%E5%A5%BD%E3%80%82%E8%BF%99%E6%98%AF%E5%9B%A0%E4%B8%BA%EF%BC%9A%E7%BD%91%E7%BB%9C%E5%A4%AA%E6%B7%B1%EF%BC%8C%E4%BC%98%E5%8C%96%E9%9A%BE%E5%BA%A6%E5%8F%98%E5%A4%A7%EF%BC%8C%E5%B1%82%E6%95%B0%E5%A4%9A%E4%BA%86%E5%8F%8D%E8%80%8C%E2%80%9C%E5%B9%B2%E6%89%B0%E2%80%9D%E4%BA%86%E5%B7%B2%E6%9C%89%E7%89%B9%E5%BE%81%E7%9A%84%E8%A1%A8%E8%BE%BE%E3%80%82&quot;&gt;^2&lt;/a&gt;）
为了解决这个问题，著名的残差神经网络(Residual Networks)就发明了(何恺明就提出的)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Residual Networks&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-30%2011.38.16.png&quot; alt=&quot;alt text&quot;&gt;
灵感就是越深的神经网络却不如浅的神经网络，意味着神经网络甚至很难学到恒等函数，直接将浅层搬到深层中，然后让其他层都输出输入，理论上就得到和浅层一摸一样的网络。&lt;/p&gt;
&lt;p&gt;但是问题就是很难学到恒等函数，残差神经网络的思想就是将输入直接跳过中间的层直接加到输出中，这样做的意义是：如果模型最优的输出就是输入本身，那它只需要学 F(x)=0 就可以了，比起直接拟合复杂函数更容易。&lt;/p&gt;
&lt;p&gt;然后这就有一个问题残差连接只保证深层网络“不比浅层差”，但它凭什么就一定能“更好”？&lt;/p&gt;
&lt;p&gt;答案就是：&lt;/p&gt;
&lt;p&gt;1. 残差连接解决的是优化困难&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;ol start=&quot;2&quot;&gt;
&lt;li&gt;为优化器提供了更大的函数空间，理论上模型能力更强&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;意思就是模型可以学到更多的函数，有时候不需要很复杂的函数，这让网络可以灵活决定要不要学习复杂变换&lt;/p&gt;
&lt;hr&gt;</content:encoded><img src="/_astro/111.NGrZ1MUk.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/111.NGrZ1MUk.png" length="0" type="image/png"/></item><item><title>无监督学习 SwAV NeurIPS 2020 论文笔记</title><link>https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87swav</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87swav</guid><description>Unsupervised Learning of Visual Features by Contrasting Cluster Assignments</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/1.BVZ_r7p7.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-28%2018.16.10.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;无监督学习以及其中的自监督学习目的是为了不使用人工标签来获得图像的特征，然后他已经迅速的缩小与监督学习之间的差距了。当前的SOTA自监督学习模型主要思想就是instance discrimination(实例判别)，他的主要思想就是我有一组图片，我将这一组图片中的每一张图片定义为一个实例（instance ），然后我将每个实例进行一定的图像变换（如裁剪、颜色变化、模糊等），然后将实例以及它的各种变换版本，都当成一个单独的类别。最终使得模型能够能够区分不同的图片，同时也对图片的一些变换保持一定的不变性（比如旋转、裁剪、颜色变化等，不影响它识别出图片是同一个）。&lt;/p&gt;
&lt;p&gt;然后实现这些的主要依赖有两个&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对比损失（contrastive loss）&lt;/li&gt;
&lt;li&gt;图像转化（ image transformations）
定义两个概念，正样本(Positive sample)和负样本(Negative sample)
正样本就是&quot;should be pull together&quot;的一对，也就是“同一个实例”的两个不同变换版本。
负样本就是&quot;should be push away&quot;的一对，也就是也就是“来自不同实例”的图片。&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;li&gt;把&lt;strong&gt;负样本对&lt;/strong&gt;在特征空间里推得很远（它们本来就是不同的实例）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样，模型学到的表示就能做到认出“即使样子变化了，还是同一个东西”；同时又能“区分不同的东西”。&lt;/p&gt;
&lt;p&gt;然后contrastive loss就是计算正负样本对特征向量的相似度，相当于一个损失函数。&lt;/p&gt;
&lt;p&gt;但是有问题就是，我的数据集很大的情况下我给每一个pair计算的话成本就很大，其中一种解决方法就是随机挑一些pair来进行训练。另一种就是就是简化要求，就是我们不区分每一张图片，而是先将图片进行聚类(clustering-based)，而是将具有相似特征的图片分到一个同一组（一个簇）中。然后，模型只需要区分不同簇之间的差异，而不是每张图片之间的差异。有点在于易于计算（即目标是分辨不同簇而非每一张图片），因此在某些情况下，它的目标函数会更&lt;strong&gt;容易优化&lt;/strong&gt;。但是这种方法的局限还是计算成本，这是因为聚类方法需要对整个数据集进行遍历，来为每张图片分配一个“编码”（即簇的分配）。这意味着它在训练时必须处理整个数据集，因此随着数据集增大，计算成本也会急剧上升。&lt;/p&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;h4&gt;SwAV&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;alt text&quot;&gt;
传统的对比学习方法就是从图片X中进行 image transformations得到$X_1$ $X_2$ 将这些转化后的图片经过$f_θ$神经网络提取出他们的特征向量$Z_1$,$Z_2$，然后进行归一化，然后损失函数就是这两个向量的差异，让同一类图片的特征向量接近，训练的是提取图片特征向量的那一部分神经网络$f_θ$。&lt;/p&gt;
&lt;p&gt;然后新的方法是将得出来的特征向量通过Prototypes C映射到code那里，然后使同一类图片的特征向量映射到同一个code那里，训练的是神经网络以及Prototypes C&lt;/p&gt;
&lt;p&gt;这里的$z_t$, $z_s$ 就是同一张图片的两张增强版本的特征向量，$q_s$, $q_t$就是对应得到的code，swapped的部分就体现在这里。定义损失函数如下：&lt;/p&gt;
&lt;p&gt;$$
L(z_t, z_s) = \mathcal{L}(z_t, q_s) + \mathcal{L}(z_s, q_t)\tag{1}
$$
$$
\ell\left(\mathbf{z}_t, \mathbf{q}_s\right)=-\sum_k \mathbf{q}_s^{(k)} \log \mathbf{p}_t^{(k)} ,\mathbf{p}_t^{(k)}=\frac{\exp \left(\frac{1}{\tau} \mathbf{z}_t^{\top} \mathbf{c}&lt;em&gt;k\right)}{\sum&lt;/em&gt;{k^{\prime}} \exp \left(\frac{1}{\tau} \mathbf{z}&lt;em&gt;t^{\top} \mathbf{c}&lt;/em&gt;{k^{\prime}}\right)}\tag{2}
$$
其中$τ$ 是 temperature parameter[^1],然后总的损失函数就如下&lt;/p&gt;
&lt;p&gt;$$
-\frac{1}{N} \sum_{n=1}^N \sum_{s, t \sim \mathcal{T}}\left[\frac{1}{\tau} \mathbf{z}&lt;em&gt;{n t}^{\top} \mathbf{C} \mathbf{q}&lt;/em&gt;{n s}+\frac{1}{\tau} \mathbf{z}&lt;em&gt;{n s}^{\top} \mathbf{C} \mathbf{q}&lt;/em&gt;{n t}-\log \sum_{k=1}^K \exp \left(\frac{\mathbf{z}&lt;em&gt;{n t}^{\top} \mathbf{c}&lt;em&gt;k}{\tau}\right)-\log \sum&lt;/em&gt;{k=1}^K \exp \left(\frac{\mathbf{z}&lt;/em&gt;{n s}^{\top} \mathbf{c}_k}{\tau}\right)\right]
$$&lt;/p&gt;
&lt;p&gt;现在我们知道了损失函数长什么样，现在我们就想知道Q怎么来的，文章中写了这样一条式子，每一轮forwarding想要的Q就要满足这样一条式子。
$$
\max _{\mathbf{Q} \in \mathcal{Q}} \operatorname{Tr}\left(\mathbf{Q}^{\top} \mathbf{C}^{\top} \mathbf{Z}\right)+\varepsilon H(\mathbf{Q})\tag{3}
$$
这里的Z是一个batch中所有图片的特征向量的集合，C是所有Prototypes的集合，或者称之为聚类中心。$\mathbf{C}^{\top} \mathbf{Z}$ 就是让Z中的向量和C中所有的向量做内积，换句话说就是每一个特征向量和这些聚类中心做内积，对于这里而言，内积描述了两个向量的相似性，内积越高这两个向量相似性越高。为什么呢，原因就在于内积$\mathbf{a} \cdot \mathbf{b}=|\mathbf{a}||\mathbf{b}| \cos \theta$ 而特征向量和Prototypes已经进行了归一化处理，所以他们的内积结果就是$\cos \theta$ ，反映的就是这两个向量的相似性。于是我们就得到了每一个特征向量和聚类中心相似程度的矩阵。&lt;/p&gt;
&lt;p&gt;然后，Q相当于一个分配矩阵，$Q_{ij}^{\top}$​表示的就是特征向量 i 分到 prototype j 的概率，那我们当然希望相似程度越高的这两个被分到一起的概率越大，所以就有了$\mathbf{Q}^{\top} \mathbf{C}^{\top} \mathbf{Z}$ 。怎么理解呢，对于$\mathbf{C}^{\top} \mathbf{Z}$这个矩阵，矩阵中的 (i,j) 位置的元素表示为第j个特征向量和第i个prototype的相关性，每一列表示一个特征向量和所有prototype的相关性。这样子和$Q^{\top}$进行内积得到的结果里，每一个对角线上的元素表示$Q^{\top}$中的第i行和$\mathbf{C}^{\top} \mathbf{Z}$中的第i列进行内积，这样子看的话，$Q^{\top}$中每一行的元素就是特征向量 i 分到 prototype j 的概率。同时之后只有对角线上的元素对我们有用，于是计算的是trace。&lt;/p&gt;
&lt;p&gt;后面的$\varepsilon H(\mathbf{Q})$ 表示的是Q的熵，目的在于让这个Q变得均匀，熵变大，然后$\varepsilon$就是正则化系数，控制Q均匀的程度，一般设置的很小。&lt;/p&gt;
&lt;p&gt;于是乎完整公式的含义就是找到一个Q使得Z更能分配到最接近的prototype C的同时尽量让这个Q更加均匀。&lt;/p&gt;
&lt;p&gt;当然这个Q还有一定的限制，这里的限制就是$Q^{\top}$每一行中所有元素相加都是$\frac1B$ ，意思就是对于任意一个特征向量，他属于不同prototype的概率加和应该是1，那这里是$\frac1B$的原因就在于这个Q需要的是归一化的，所以每一行的结果加起来要是1，每一行的结果就应该是$\frac1B$ 。
prototype每一列中所有元素相加都是$\frac1K$ ，意思就是对于任意一个特征向量，他被分到每一个prototype的概率是$\frac1K$，意思就是每个prototype都会有$\frac{B}K$个特征向量。所谓平均分配(equal partition)。
$$
\mathcal{Q}=\left{\mathbf{Q} \in \mathbb{R}_{+}^{K \times B} \left\lvert, \mathbf{Q} \mathbf{1}_B=\frac{1}{K} \mathbf{1}_K\right., \mathbf{Q}^{\top} \mathbf{1}_K=\frac{1}{B} \mathbf{1}_B\right}\tag{4}
$$&lt;/p&gt;
&lt;p&gt;现在的问题转化为在这些限制下，我要怎么得到我想要的Q，这就是最优输运问题(Optimal Transport),答案就是&lt;a href=&quot;https://coderlemon17.github.io/posts/2022/07-16-ot/&quot;&gt;&lt;strong&gt;Sinkhorn-Knopp算法&lt;/strong&gt;&lt;/a&gt;来快速求得近似解。这里作者选择的是软编码，意思是一个特征向量可以对应多个prototype，然后根据Sinkhorn-Knopp算法直接给出结果
$$
\mathbf{Q}^*=\operatorname{Diag}(\mathbf{u}) \exp \left(\frac{\mathbf{C}^{\top} \mathbf{Z}}{\varepsilon}\right) \operatorname{Diag}(\mathbf{v})\tag{5}
$$
然后u，v是算法中计算出来的&lt;/p&gt;
&lt;p&gt;现在回过头来看他的损失函数就明白这个很有意义，公式(2)中的右边，求的是特征向量$Z_t$和prototype $c_k$ 的softmax概率，相似性越高，这个概率也应该越高，左边$q_s$就是特征向量$Z_s$分到prototype $c_k$的概率，然后求这两个概率的交叉熵如果 q 和 p 很接近，交叉熵就小；如果差得很远，交叉熵就大。&lt;/p&gt;
&lt;h4&gt;multi-crop&lt;/h4&gt;
&lt;p&gt;对于图像转化问题，他们提出multi-crop，不光做两次大裁剪（大图像区域），他还加上&lt;strong&gt;好几次小裁剪&lt;/strong&gt;（很小的局部区域）。这样，一张图片就能变出很多种不同大小、不同范围的视角。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重点是&lt;/strong&gt;：&lt;br&gt;
因为小裁剪尺寸小，数据量也小，所以即使生成了更多视角，也不会占用太多显存或者增加很多计算量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么这样做有用？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;小视角&lt;/strong&gt;（小crop）和&lt;strong&gt;大视角&lt;/strong&gt;（大crop）能覆盖一张图像的不同层次的信息，比如小局部细节 vs. 整体布局。&lt;/li&gt;
&lt;li&gt;如果只用缩小尺寸的图片，特征容易有偏差（就是你只让模型看很小的低分辨率图片，它学出来的特征可能失真）。&lt;/li&gt;
&lt;li&gt;但是如果用&lt;strong&gt;大小混合&lt;/strong&gt;的方法（大crop + 小crop一起用），可以避免这种偏差。
所以原本的损失函数就变成了这样
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;L\left(\mathbf{z}&lt;em&gt;{t_1}, \mathbf{z}&lt;/em&gt;{t_2}, \ldots, \mathbf{z}&lt;em&gt;{t&lt;/em&gt;{V+2}}\right)=\sum_{i \in{1,2}} \sum_{v=1}^{V+2} \mathbf{1}&lt;em&gt;{v \neq i} \ell\left(\mathbf{z}&lt;/em&gt;{t_v}, \mathbf{q}&lt;em&gt;{t_i}\right) \tag{6}
$$
$1&lt;/em&gt;{v\neq i}​$ 是一个指示函数，当 $v\neq i$ 时，它的值为1，否则为0，表示排除与裁剪i相同的图像。还有值得注意的是这里的code只计算大分辨率crop，小分辨率不计算&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^1]: Temperature 控制概率分布的“平滑度”或“尖锐程度”。Temperature越大意味着原本的数变得靠拢，各选项的概率更接近，Temperature越小，原本的数变得越分散，经过指数的加成下，差异就变得越大，分布也就越尖锐&lt;/p&gt;</content:encoded><img src="/_astro/1.BVZ_r7p7.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/1.BVZ_r7p7.png" length="0" type="image/png"/></item><item><title>主动学习 ViewAL CVPR 2020 论文笔记</title><link>https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87viewal</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87viewal</guid><description>ViewAL - Active Learning With Viewpoint Entropy for Semantic Segmentation</description><pubDate>Wed, 23 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/11.BBXUWYaq.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;Authors: &quot;Yawar Siddiqui, Julien Valentin, Matthias Niessner&quot;&lt;/p&gt;
&lt;p&gt;Date:&apos;2020-06-01&apos;&lt;/p&gt;
&lt;p&gt;URL:&quot;https://github.com/nihalsid/ViewAL&quot;&lt;/p&gt;
&lt;h2&gt;Abstract&lt;/h2&gt;
&lt;p&gt;文章中提出ViewAL是一种&lt;strong&gt;主动学习&lt;/strong&gt;[^1]策略，对于多视角的图片，研究人员认为这是一个很好的一个非常可靠的不确定性度量方式（reliable measure of uncertainty)&lt;a href=&quot;%E5%A4%A7%E8%87%B4%E6%84%8F%E6%80%9D%E5%B0%B1%E6%98%AF%E6%A0%B9%E6%8D%AE%E6%A8%A1%E5%9E%8B%E5%9C%A8%E4%B8%8D%E5%90%8C%E8%A7%86%E8%A7%92%E4%B8%8B%E7%9A%84%E5%88%A4%E6%96%AD%E7%BB%93%E6%9E%9C%EF%BC%8C%E5%BE%88%E5%AE%B9%E6%98%93%E5%B0%B1%E7%9F%A5%E9%81%93%E8%BF%99%E4%B8%AA%E6%A8%A1%E5%9E%8B%E5%AF%B9%E6%9F%90%E4%B8%AA%E9%A2%84%E6%B5%8B%E6%9C%89%E5%A4%9A%E8%87%AA%E4%BF%A1%E3%80%82%E4%B8%BE%E4%B8%AA%E4%BE%8B%E5%AD%90%EF%BC%8C%E5%AF%B9%E4%BA%8E%E5%90%8C%E4%B8%80%E4%B8%AA%E5%9C%BA%E6%99%AF%E7%9A%84%E4%B8%8D%E5%90%8C%E8%A7%86%E8%A7%92%E7%9A%84%E5%87%A0%E5%BC%A0%E5%9B%BE%E7%89%87%EF%BC%8C%E5%AF%B9%E4%BA%8E%E5%90%8C%E4%B8%80%E4%B8%AA%E7%89%A9%E4%BD%93%EF%BC%8C%E8%A6%81%E6%98%AF%E6%A8%A1%E5%9E%8B%E5%9C%A8%E4%B8%8D%E5%90%8C%E8%A7%86%E8%A7%92%E4%B8%8B%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E7%9A%84%E4%B8%9C%E8%A5%BF%E4%B8%8D%E5%90%8C%EF%BC%8C%E9%82%A3%E5%85%B6%E5%AE%9E%E5%B0%B1%E5%BE%88%E5%8F%AF%E4%BB%A5%E8%AF%B4%E6%98%8E%E6%A8%A1%E5%9E%8B%E5%85%B6%E5%AE%9E%E5%B9%B6%E4%B8%8D%E6%98%AF%E5%BE%88%E8%83%BD%E2%80%9C%E7%A1%AE%E5%AE%9A%E2%80%9C%E8%BF%99%E4%B8%AA%E7%89%A9%E4%BD%93&quot;&gt;^2&lt;/a&gt;，然后他们将这种reliable measure of uncertainty纳入到视点熵[^3]公式，使之模型可以根据这种reliable measure of uncertainty来训练，调整自己的参数。然后他们还提出超级像素（superpixel）[^4]的不确定性计算方法，降低图片的标注成本。然后证明他们的模型在仅用很小的标注数据的情况下就可以达到95%的性能，用于语义分割。&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;就是说随着深度学习的发展，就需要更多的数据以及高质量的ground truth data[^5]，但是这有很大的局限性。所以就有了主动学习(active learning)，核心就是根据人们设定的某种策略来让模型主动选择某张图片，然后查询这张图片的标签。最常见的策略就是Uncertainty sampling，意思就是模型选最不确定的样本来查询这张图片的标签。（比如对于一个二元的区分模型，我们选概率最接近0.5的）然后当前Uncertainty sampling的局限就是大多只对单个图片进行，这显然满足不了现实世界这种3D，多视角的情况。&lt;/p&gt;
&lt;p&gt;然后研究人员认为现实这种情况，很好的提供了reliable measure of uncertainty，意思就是Uncertainty sampling选最不确定的样本，怎么衡量这种不确定，他们的想法就是同一个物体在不同视角的图片中他的标注应该是一样的，如果模型面对多视角的图片，他不能识别出同一个物体，那么可以说明模型其实不是很确定，判定为预测错误。&lt;/p&gt;
&lt;p&gt;因此，研究人员就将这种特性结合到他们提出的视点熵公式中，然后在superpixel的级别上进行不确定度检测，而不是在像素级别上进行检测，意思就是我们不需要对每个像素逐个判断是否值得标注，只需要看哪些“超像素区域”不确定然后就能重点标这些，这样可以节省人力标注成本。&lt;/p&gt;
&lt;h2&gt;Related Work&lt;/h2&gt;
&lt;p&gt;主动学习的策略&lt;/p&gt;
&lt;h4&gt;uncertainty-based approaches&lt;/h4&gt;
&lt;p&gt;模型选最不确定的样本来查询这张图片的标签
对于一个二元的区分模型，我们选概率最接近0.5的。
对于三元或更多元的区分，可以根据熵来进行判断
$$
H(p)=−\sum^K_{i=1}​p_i​log(p_i)​
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;模型输出：
[0.33, 0.33, 0.34]
H≈−(3×0.33log0.33)≈1.58
→ 熵高 → 不确定性高 → 模型在迷茫
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;模型输出：
[0.98, 0.01, 0.01]
H=−(0.98log0.98+0.01log0.01+0.01log0.01)≈0.1
→ 熵低 → **不确定性低** → 模型很自信
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有比较简单的策略就是，直接看结果中最大的那个概率（最小后验概率），如果这个最大值很小，说明模型对它的预测也不自信。&lt;/p&gt;
&lt;p&gt;还有比如选择结果中最大概率和第二大概率中差值最小的
还有利用多个模型对同一组数据进行预测，然后选择最少共识样本的进行标注，就是选择最多模型的预测不同的那一组数据&lt;/p&gt;
&lt;p&gt;等等...&lt;/p&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;p&gt;他们的思路就是在一开始利用有标注的图片集对模型进行训练，然后让模型跑没有标注的数据，选择最不确定的超像素进行标注，然后把这个再放到有标注的图片集训练中，再对模型进行训练。如此往复直到所有的数据都进行了标注或者达到标注的预算。
&lt;img src=&quot;./11.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Network Training&lt;/h4&gt;
&lt;p&gt;他们的这个可以用于各种的分割模型中，然后他们选择了DeepLabv3+和MobileNetv2作为backbone[^6]，因为前者的表现最好，后者的训练速度快，内存占用小。然后他们利用在ILSVRC这个数据集进行预训练之后的权重来初始化MobileNetv2，这样可以利用已学到的通用图像特征（如边缘、纹理、颜色等），从而提升模型在下游任务（如语义分割）中的表现。通过这种方式，网络不需要从头开始学习这些特征，能够加速训练并提高精度。然后他们的其他层利用了Kaiming initialization。为了避免过拟合，他们利用模糊、随机裁剪、随机翻转和高斯噪声作为数据增强手段。&lt;/p&gt;
&lt;h4&gt;Uncertainty Scoring&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-22%2016.06.50.png&quot; alt=&quot;alt text&quot;&gt;
更细一步的流程就是一开始我们有一个ground truth data，里面有很多图片，然后每一个场景都有不同视角的一组图片（比如A，B，C），然后每一张图片都有分好了superpixel以及这个superpixel对应的标签。然后我们将这个ground truth data分成两组，一组用来训练，另外一组用来当作unlabed的数据集进行预测。&lt;/p&gt;
&lt;p&gt;然后对于同一个场景，模型对这个场景的那一组包含不同视角的每一张图片（A,B,C）进行预测，会经过20轮的MC dropout[^7]，然后每个图片这时候就会有20种结果，模型跑出来得到的实际上是图片中每一个pixel的在对应标签的概率，然后将这些结果取平均。&lt;/p&gt;
&lt;p&gt;然后这时候，我们从这一组跑出来的结果图片中每次选一张，然后将这个图片的每一个像素反投影到3D的空间中，然后再投影到别的视角下的2D的图像，这样子交叉投影（比如我将A的结果反投影到3D的空间，然后再投影到B视角对应的2D图像上，C-&gt;B ;B-&gt;A ,C-&gt;A ;A-&gt;C ,B-&gt;C）。于是我们就得到多个视角下同一个物体的概率，然后我们根据这些计算视角熵和view divergence。&lt;/p&gt;
&lt;h6&gt;View Entropy Score&lt;/h6&gt;
&lt;p&gt;再具体说，对于一张图片里面的像素，我们得到其他视角同一个像素投影到该像素的概率，然后我们将这些概率取平均，之后计算视角熵。&lt;/p&gt;
&lt;p&gt;D表示MC dropout的总轮数，$P ^{(u,v)}_  i (c)$表示图片 i 经过分割模型后像素(u,v)对于标签 c 的概率，这公式就是将每次MC dropout的结果取平均。
$$
P ^{(u,v)}_  i (c) = \frac 1 D  \sum^D _{d=1}  P ^{(u,v)}  &lt;em&gt;{i,d} (c),
$$
这个公式的意思就是图片 j 中(x,y)这个像素投影到图片 i 中(u,v)这个像素，然后Ω就是这些像素对应概率的集合。
$$
Ω ^{(u,v )}&lt;/em&gt;  i = {P ^{(x ,y)}  _j , j | I_j(x, y) cross-projects to I_i(u, v)}
$$&lt;/p&gt;
&lt;p&gt;这个公式就是对图片i中像素（u，v）的概率进行求和
$$
Q ^{(u,v )}_  i =\frac1  {|Ω^ {(u,v)} _ i|}  \sum _{ P ∈Ω ^{(u,v)}  _i}  P ^{(u,v)}
$$&lt;/p&gt;
&lt;p&gt;这里就是计算视角熵
$$
VE ^{(u,v)}_  i = −\sum &lt;em&gt;c  Q^{ (u,v )} _ i (c) log (Q^{(u,v)}&lt;/em&gt;  i (c))
$$&lt;/p&gt;
&lt;h6&gt;View Divergence Score&lt;/h6&gt;
&lt;p&gt;视角熵表示的其实是某视角预测的“平均”不确定性，但是还没有衡量多个视角预测之间的一致性，况且每个视角下计算出的视角熵应该是一样的。于是文章用Pairwise KL Divergence（成对的KL散度）&lt;/p&gt;
&lt;p&gt;KL 散度是一种衡量两个概率分布差异的方式：
$$
D_{KL}(P ∣∣ Q)=\sum_iP(i)log⁡\frac{P(i)}{Q(i)}​
$$&lt;/p&gt;
&lt;p&gt;它衡量的是从分布 Q 转向分布 P 所需的信息量差异（不是对称的！）。&lt;/p&gt;
&lt;p&gt;然后在文章中的话，这里就计算图片 i 中像素（u，v）的平均的KL散度，高分数得分意味着，平均而言，当前视图中的预测与其他观测点的预测有显著不同
$$
VD ^{(u,v)}_  i =\frac1 { |Ω^{ (u,v)}_  i|}  \sum_  {P_j ∈Ω^{ (u,v)} _ i}  D_{KL}(P ^{(u,v)}_  i ||P ^{(u,v)}_  j )
$$&lt;/p&gt;
&lt;h4&gt;Region Selection&lt;/h4&gt;
&lt;p&gt;研究人员想的就是让标注者标注的是一个superpixel，因为一个superpixel大多情况下只与一个对象相关联，所以对标注者而言标注相对轻松。这也是不选择矩形来进行标注的原因，然后这里他们这里使用的是&lt;a href=&quot;https://scholar.google.com/scholar?hl=zh-CN&amp;#x26;as_sdt=0%2C5&amp;#x26;q=Michael+Van+den+Bergh%2C+Xavier+Boix%2C+Gemma+Roig%2C+Benjamin+de+Capitani%2C+and+Luc+Van+Gool.+Seeds%3A+Superpixels+extracted+via+energy-driven+sampling.+In+European+conference+on+computer+vision%2C+pages+13%E2%80%9326.+Springer%2C+2012.+5&amp;#x26;btnG=&quot;&gt;SEEDS&lt;/a&gt;算法进行superpixel分割。&lt;/p&gt;
&lt;p&gt;然后他们就计算每一个superpixel的视角熵和View Divergence Score，就是平均该superpixel r 中所有像素的视角熵和View Divergence Score&lt;/p&gt;
&lt;p&gt;$$
VE ^r_  i=\frac1 { |r|}  \sum_ { (u,v)∈r}  VE ^{(u,v)}_  i
$$
$$
VD^ r _ i=\frac 1 { |r|}  \sum &lt;em&gt;{ (u,v)∈r}  VD^{ (u,v)}&lt;/em&gt;  i
$$
之后为了挑选出最有价值的那个superpixel，我选择图片 i 中视角熵最大的superpixel，然后我在那一组图片中找所有的和这个superpixel重叠度大于50%的superpixel组成一个集合（包括原本的这个superpixel），然后我在这一组superpixel集合中找最大的那个view divergence，然后把这个选出来要求去标注，然后这张图上的其他superpixel标注为ignored。之后再把这个集合中的所有superpixel都给标注，因为这个集合里的所有superpixel描述的都是一个场景。&lt;/p&gt;
&lt;p&gt;这里有一个问题就是，既然这个集合里的所有superpixel描述的都是同一个事物，为什么我不随便从集合中选一个superpixel，然后进行标注，解释就是你只能“看”一个 superpixel 的图像。所以你当然要选一个“最值得你看的”，也就是 View Divergence 最大的那个视角。&lt;/p&gt;
&lt;h4&gt;Label Acquisition&lt;/h4&gt;
&lt;p&gt;事实上不需要人工标注的参与，用的是ground truth data，可以直接标注上，下一步就是将这些标注了的图片放到labeled dataset中，然后从unlabeled中删除，之后再重新训练网络。&lt;/p&gt;
&lt;p&gt;[^1]: &lt;strong&gt;主动学习&lt;/strong&gt;： 就是让模型自己“挑问题”问人，把最值得标注的数据交给人类专家来打标签，从而&lt;strong&gt;以更少的数据达到更好的效果&lt;/strong&gt;。模型先在未标注数据上做预测，&lt;strong&gt;找出它最不确定的那几张图&lt;/strong&gt;（比如分不清是猫还是狗），然后请你给这几张打标签。有利于更快提升模型效果以及节省人工标注成本&lt;/p&gt;
&lt;p&gt;[^3]: &lt;strong&gt;视点熵&lt;/strong&gt;是计算机图形学、计算机视觉和机器人领域里的一个概念，用于衡量&lt;strong&gt;从某个观察角度观察一个物体时，信息的丰富程度&lt;/strong&gt;。比如如果你只看到一个扁扁的正面轮廓（比如只看到背面），信息少 → 熵低。如果你从一个角度能同时看到正面、侧面、腿部细节 → 信息多 → 熵高&lt;/p&gt;
&lt;p&gt;[^4]: &lt;strong&gt;superpixel&lt;/strong&gt;是图像处理和计算机视觉中的一个重要概念，用来&lt;strong&gt;将图像划分成一些感知上更有意义的区域&lt;/strong&gt;，比像素更大，但比完整的物体小。将图像划分成颜色、纹理等相似的小区域，每个 superpixel 通常对应一个局部相似的区域。相比于原始的像素而言，他的优势在于&lt;strong&gt;降低计算量&lt;/strong&gt;：原来处理几十万像素，现在只用处理几千个 superpixels。&lt;strong&gt;结构更清晰&lt;/strong&gt;：比单独的像素更能表示物体边缘、纹理。&lt;strong&gt;更自然的分割单元&lt;/strong&gt;：适合用于图像分割、目标检测、语义分割等任务的预处理。&lt;/p&gt;
&lt;p&gt;[^5]: &lt;strong&gt;ground truth data&lt;/strong&gt;就是真实标签数据，是我们用来训练和评估模型的“标准答案”，这些从哪里来，很大一部分来自人工的标注。&lt;/p&gt;
&lt;p&gt;[^6]: &lt;strong&gt;backbone&lt;/strong&gt; 是指一个深度学习模型中负责提取特征（feature extraction）的主干网络部分。有点类似卷积神经网络中的卷积层，但是backbone也不全是CNN，也可以是tramsformer等等。&lt;/p&gt;
&lt;p&gt;[^7]: &lt;strong&gt;MC dropout&lt;/strong&gt;: &lt;strong&gt;Dropout 是一种防止神经网络过拟合的方法&lt;/strong&gt;。它的核心思想就是：在每次训练时，&lt;strong&gt;随机“丢弃”一部分神经元（神经元输出设为 0）&lt;/strong&gt;，让模型不会太依赖某些神经元，从而提高泛化能力。&lt;strong&gt;MC dropout&lt;/strong&gt;是在“测试阶段”也打开 Dropout，对同一张图片做多次预测，通过预测结果的波动来估计模型的“不确定性”。&lt;/p&gt;</content:encoded><img src="/_astro/11.BBXUWYaq.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/11.BBXUWYaq.png" length="0" type="image/png"/></item><item><title>Lecture7 Convolutional Networks</title><link>https://laurie-hxf.xyz/blog/deeplearning-l7</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/deeplearning-l7</guid><description>UMich EECS 498-007 lecture7 Convolutional Networks</description><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/CNN.DEmP4Oxi.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;Filter&lt;/h2&gt;
&lt;p&gt;当我们将一张图片压缩成一个一维的长向量的时候，这个图片的空间结构被破坏了。&lt;/p&gt;
&lt;p&gt;所以，为了解决这个问题，就有了Convolutional Networks（卷积网络），我们将图片转化成3*32*32的张量（假设图片是RGB三原色，32*32长和宽），其中3表示的是depth/channels。&lt;/p&gt;
&lt;p&gt;这时我们用filter（卷积核）来处理这个图片，filter也和图片的depth一样，大小这里取了5*5 然后用这个filter和图片的每一块进行内积，用这个filter扫过这个图片，每次扫的区域经过内积得到一个数，然后这些结果就可以形成另一个depth为1的张量，一共有28*28的区域相内积。所以我们得到一个1*28*28的图。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-05%2018.43.58.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Filter layer&lt;/h2&gt;
&lt;p&gt;事实上，我们并不是只有一个filter，我们有很多个不同的filter，每一个filter都扫一遍这张图片，得到多个activation map，当多个filter堆在一起的时候，我们就有卷积层。filter的数量就是一个超参数，可以自定义。这个卷积层就是一个4维的张量，计算出的activation map就是一个三维的张量。还有就是每一个filter都有一个偏移量，当filter和图片中的每一块内积完之后要加上这个偏移量，于是这些偏移量组成一个向量。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-05%2018.50.37.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;每一个activation map都可以看作是图片的某种特征的集合，他们通过filter将这些特征提取出来组成一个集合，这些特征反应的就是图片的空间信息。&lt;/p&gt;
&lt;p&gt;我们的输入可以不只是一个三维的张量，我们可以有多个三维的张量，形成一个四维的张量，对图像的进行批量处理。所以卷积层的一般性就长这样。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-05%2019.02.28.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后，我们就可以抛弃原本的那种神经网络，之前的权重矩阵W不再只是和一维向量的一次线性组合。而是变成filter，于是我们就由Linear Classifiers 转变成卷积神经网络。每一个图片经过filter的运算后再经过激活函数，这样就既可以保留图像的空间信息，又避免只是线性的进行分类。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-05%2019.14.41.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Pedding&lt;/h4&gt;
&lt;p&gt;值得注意的是，当我们将一个图片经过一个卷积核处理之后，他的大小就会减小，假如我们的图片是M*M的，我们的卷积核是K*K的，那么我们最后的图片就是（M-K+1）*（M-K+1）的，意味着每经过一层卷积层我们的图片大小就会减小。因此提出一种方法，我们对图片进行一个填充，给图片的外围填充一圈，然后再进行卷积操作。其中有不同的填充策略，比如外围全都用0来填充就叫zero pedding。一般来说zero pedding很通用，效果也很好。还有一个超参数P是填充层的层数，一般来说经过处理后图片的大小是（M-K+1+2P）*（M-K+1+2P），然后P就是$\frac{k-1}{2}$ ,这样可以保证输出的图像的大小和之前的一样。这种填充方式就叫same pedding。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-06%2012.03.07.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Receptive Field(感受野)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;感受野&lt;/strong&gt;（Receptive Field）指的是神经网络中某个神经元或卷积层神经元所能“感知”到的输入数据区域的大小。简单来说，感受野描述了每个神经元在输入图像上对应的区域大小，或者说它所能获取的信息量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;具体来说：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;卷积神经网络（CNN）中的感受野&lt;/strong&gt;：在卷积神经网络中，感受野通常指的是输入图像中经过若干卷积层后的某个特定神经元能够“看到”的区域大小。随着网络的深度增加，每一层的神经元会连接到更大的区域，从而扩大感受野。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;感受野的扩展&lt;/strong&gt;：通常情况下，通过增加卷积层的数量或者增加每一层的卷积核大小，可以扩展感受野。例如，如果第一层卷积核是3x3，第二层也是3x3，那么第二层的每个神经元就能感知到更大的输入区域。&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;/ol&gt;
&lt;h4&gt;Stride(步长)&lt;/h4&gt;
&lt;p&gt;有的时候我们想要快速的快速降低特征图尺寸的任务，或者希望更快速地捕捉全局信息的任务。比如我们有一个1024*1024的图片，那么我们就要有一个很多层的卷积层来对他进行处理。&lt;/p&gt;
&lt;p&gt;这时我们就引入一个新的超参数 步长（stride）这个的作用是控制每次卷积核扫过图片的速度。以前的话当我们用步长为1的话，他就会遍历，每次只移动一个单位，但是当我们调整我们的步长的时候，他可能就每次移动2个或更多的单位，我们就可以更快的扫完整个图片，快速的减少每一次卷积之后图片的大小，同时也保留图片的特征。这时他也就可以更快的增加感受野，然后处理过后的图片的大小就是（W-K+2P）/S+1。当然如果这个S不能刚好被整除，那么这就可以向上取整，截断等等。但是通常情况我们会将S设置的刚好可以被整除。&lt;/p&gt;
&lt;h4&gt;1*1 filter&lt;/h4&gt;
&lt;p&gt;在卷积神经网络中，&lt;strong&gt;1x1的卷积核&lt;/strong&gt;（filter）是指卷积核的尺寸为1x1，也就是说它在输入的每个位置只覆盖单个像素点。虽然它的尺寸很小，但在实际应用中，1x1卷积核非常重要，具有多种功能和用途。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作用：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;通道间的混合&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;最常见的用途之一是用于&lt;strong&gt;改变特征图的通道数&lt;/strong&gt;（也就是深度）。1x1卷积通过将每个像素点的多个通道（特征图的深度）组合在一起，生成新的特征图。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比如，假设输入有一个大小为 $H×W×C_{in}$​ 的特征图，其中 H 和 W 是空间维度，$C_in$​ 是输入的通道数。通过使用 1x1 卷积核，输出可以生成大小为 $H×W×C_{out}$​ 的特征图，其中 $C_{out}$​ 是输出的通道数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;例子&lt;/strong&gt;：假设输入特征图的大小是 32×32×64，我们使用一个 1x1 的卷积核，将通道数从 64 降到 32，那么输出的特征图大小将是 32×32×32。&lt;/p&gt;
&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;p&gt;1x1卷积的另一个优势是它可以&lt;strong&gt;减少计算量&lt;/strong&gt;和&lt;strong&gt;参数数量&lt;/strong&gt;，特别是在卷积网络中，其他大尺寸卷积（如3x3或5x5）的参数非常多。通过在卷积网络中引入1x1卷积，先减少通道数，再执行更大的卷积核操作，可以显著减少计算复杂度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如，假设输入特征图为 H×W×256，通过一个 1x1 的卷积核将通道数减少到 64，然后可以进行 3x3 或 5x5 的卷积操作，减少了后续卷积层的计算量。&lt;/p&gt;
&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;1x1卷积还能实现输入特征的&lt;strong&gt;非线性变换&lt;/strong&gt;，这类似于全连接层的作用，只不过是在卷积的框架下进行。每个像素点都经过一个1x1卷积进行映射，虽然每个位置的卷积核只有1个参数，但它能有效地将不同通道的信息融合。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;瓶颈层（Bottleneck layer）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在一些网络架构（比如ResNet和Inception）中，1x1卷积被用作&lt;strong&gt;瓶颈层&lt;/strong&gt;。瓶颈层通过减少特征图的通道数来降低计算量和内存需求，然后再通过较大的卷积核（如3x3）进行特征提取。这样的设计可以加速训练和推理。&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;通过在1x1卷积后加入激活函数（如ReLU），网络可以增加其表达能力，进行复杂的特征转换和组合。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Pooling Layers(池化层)&lt;/h2&gt;
&lt;p&gt;这部分的作用其实就是单纯的downsample（下采样），就是指通过某种方式减少数据的空间或时间维度，在图像处理中，通常是通过减少图像的分辨率来达到减少计算量和存储的目的。同时这一部分没有学习参数。&lt;/p&gt;
&lt;h4&gt;Max pooling&lt;/h4&gt;
&lt;p&gt;这个就是给定一个n*n的 max pooling ，然后就计算图片中n*n的最大值，将一个n*n的数据downsample为一个数，一般而言我们将max pooling的步长设置为和size一样的大小n。这种方法某种程度上引入了一定的不变性，假设图片上面的某些东西轻微的移动了一下，那么这部分的max value可能不会有变化。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-14%2020.25.14.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Average pooling&lt;/h4&gt;
&lt;p&gt;类似就是选取这个区域的平均值&lt;/p&gt;
&lt;h2&gt;Convolutional Networks&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-14%2021.21.00.png&quot; alt=&quot;alt text&quot;&gt;
如图，我们将卷积层，池化层，激活函数，全连接层全部连接起来，我们就得到大名鼎鼎的卷积神经网络，当然卷积神经网络不一定只有一种方式。
这里举杨立昆的LeNet为例子&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们将一张28*28的一张灰度图，所以通道数就是1，先输入进卷积层，用的是same pedding，然后卷积层有20层，然后是5*5的卷积核&lt;/li&gt;
&lt;li&gt;之后经过激活函数处理&lt;/li&gt;
&lt;li&gt;再经过池化层&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;值得注意的是后面有一个flatten的操作，将原本的50*70*70的图片转化成一个一维的向量，这样子就像之前的linear classifier一样，最终得到一个大小为10的一维向量，表示想要识别的标签&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里有趣的是我们在用max pooling的时候其实就已经引入非线性函数了，所以说其实ReLU并不是必要的，但是在现代的神经网络构建中仍然保留，表现你的神经网络的规范性。&lt;/p&gt;
&lt;h2&gt;Normalization&lt;/h2&gt;
&lt;p&gt;当我们真正用这个传统架构去训练的时候，我们会发现一个问题，就是他十分难收敛，主要原因有说法是internal covariate shift。在深度神经网络中，每一层的输入实际上是前一层的输出。随着网络训练的进行，前一层的参数在不断变化，因此，后一层的输入数据（即前一层的输出）也会随之变化。这个过程可能导致神经网络的每一层都在不断地接收&lt;strong&gt;不同分布的输入数据&lt;/strong&gt;。具体而言，训练中的每一层会接收到来自上一层的输入数据，而这些输入数据的分布是随着网络参数更新而不断变化的。
这种变化对训练过程产生了负面影响，因为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;网络的每一层都需要不断适应输入分布的变化&lt;/strong&gt;，这会使得梯度传播变得更加复杂和不稳定。&lt;/li&gt;
&lt;li&gt;网络的优化过程可能会变得非常缓慢，甚至会导致收敛性问题。&lt;/li&gt;
&lt;li&gt;神经网络的训练依赖于稳定的输入分布，而内部协变量偏移的存在导致这种稳定性丧失。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-15%2020.29.16.png&quot; alt=&quot;alt text&quot;&gt;
如图，这里的计算就是假设我们有N个图片组成一个batch，然后每个图片是H*W*$C_{in}$&lt;br&gt;
然后在Batch Normalization的时候，他就会把N个图片，每一个图片的通道 $j\in C_{in}$ 的图片拿出来，这时候，这个图片就是一个H*W的二维的图片，然后对这些图片求和时候求均值，求方差以及标准化。具体来说$x_{i,j}$ 就是第 i 张图片，第 j 个通道，这是一个二维的矩阵，大小为H*W。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后避免方差$\sigma^2$  为0导致标准化的时候除以0，引入一个很小的 $\epsilon$ 。这种的变化下能使得每一通道的输入数据具有零均值和单位方差。&lt;/p&gt;
&lt;p&gt;虽然标准化操作通过使每个通道的均值为 0 和方差为 1 来加速训练和提高稳定性，但这种标准化可能会限制网络的表达能力，特别是在需要特定数据分布的任务中。例如，假设网络需要某个通道的数据具有不同的方差和偏移量，这时标准化会让它失去灵活性。&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;/p&gt;
&lt;p&gt;具体来说，Batch Normalization 的输出是:&lt;/p&gt;
&lt;p&gt;$$
y_{i,j}=\gamma_j\cdot\hat{x}_{i,j}+\beta_j
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$γ_j$&lt;/strong&gt; 是第 j 个通道的&lt;strong&gt;缩放因子&lt;/strong&gt;，用来控制标准化后的数据的方差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$β_j$​&lt;/strong&gt; 是第 j 个通道的&lt;strong&gt;平移因子&lt;/strong&gt;，用来控制标准化后的数据的均值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过这两个参数，网络可以对每个通道的数据进行自由缩放和平移，恢复网络的学习能力，并根据具体的任务需求进行调整。&lt;/p&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;strong&gt;批次级别的均值和方差&lt;/strong&gt;，会导致不稳定的预测结果，因为这些均值和方差无法代表整个训练集的统计信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以为了确保 &lt;strong&gt;测试时的标准化稳定性&lt;/strong&gt;，在训练过程中，我们会计算并保存每个通道的 &lt;strong&gt;全局均值&lt;/strong&gt; 和 &lt;strong&gt;全局方差&lt;/strong&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;这样做还有一个好处就是当测试的时候这些均值和方差就是常数了，所以Normalization这一步就相当于一个线性变换，于是我们就可以将这一步和前面的卷积融合在一起，直接融合到卷积核里面，于是好处就是我们节省了Normalization这一步的开销。&lt;/p&gt;
&lt;p&gt;Normalization的好处有很多，比如他可以加快训练速度，我们可以因此加快学习率等等
但是这个理论仍然没有很好的被理解，到底是为什么，就像是一个实验性的结论，还有就是我们要在训练和测试的时候替换那个均值和方差，这可能是很多程序bug的来源。&lt;/p&gt;
&lt;h2&gt;The essence of Convolutional in deeplearning&lt;/h2&gt;
&lt;p&gt;当我们看待深度学习中的卷积的时候，我们的卷积核其实就是一层小的模版，当我们用这个卷积核扫过图片的每一部分的时候，实际上我们是想看图片的这一块区域和这个模版是不是相像。举个例子假如说我们现在有一个3*3的卷积核&lt;/p&gt;
&lt;p&gt;$$
\begin{pmatrix}
-1 &amp;#x26; 0 &amp;#x26; 1\
-1 &amp;#x26; 0 &amp;#x26; 1 \
-1 &amp;#x26; 0 &amp;#x26; 1
\end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;当我们用这个卷积核去扫图片中每个3*3大小的区域的时候，这个区域和这个卷积核做内积，也就是说，当这个区域符合这种第一列为负数第三列为正数的时候，他们之间的内积就会变得很大。如果说这个区域的颜色都是相同的时候，内积的结果就是0。这实际上相当于识别图像的竖向向的边界。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-06%2017.26.11.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;比如图中白色的部分会和第三列内积，黑色部分和左边两列内积&lt;/p&gt;
&lt;p&gt;如果我们将这个卷积核翻转一下，他就可以识别图像的横向边界&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-06%2017.23.47.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果我们将这两种卷积核操作过后的结果结合一下，我们就可以得到整个图像的轮廓。&lt;/p&gt;
&lt;p&gt;事实上，在神经网络的训练中我们并不设定这个卷积核应该提取什么特征，而是经过损失函数评判，反向梯度传播之后，我们不断调整卷积核里面的参数。卷积核的学习过程，就是在训练中“变形自己”，最终变成一个“对目标任务最有帮助的特征检测器”。每一层都在提取更抽象、更有用的图像特征。&lt;/p&gt;
&lt;h4&gt;卷积的含义&lt;/h4&gt;
&lt;p&gt;deeplearning中的filter为什么要叫卷积，要知道这可不是数学定义上的卷积，称之为卷积的原因是他在本质上和数学的卷积有点类似。在本质上，无论是概率论还是深度学习中，&lt;strong&gt;卷积都是在做一个“滑动+相乘+求和”的操作&lt;/strong&gt;。deeplearning中的卷积是一种离散型的卷积。&lt;/p&gt;
&lt;p&gt;在概率论中我们定义的卷积是想要知道两个随机变量加和之后的分布，假设现在有连续型随机变量X和Y，他们之间相互独立，他们的然后我想知道x+y=z, z的概率是什么。&lt;/p&gt;
&lt;p&gt;$$
f_Z(z) =(f * g)(z) = \int_{-\infty}^{\infty} f_X(x) f_Y(z - x) dx
$$&lt;/p&gt;
&lt;p&gt;我们就可以看作是g函数翻转后平移z个单位之后与f函数相乘再相加的结果，当我们改变x的取值的时候就相当于“扫/滑动”这个过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-06%2017.51.22.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-06%2017.51.58.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个卷积的含义还可以有别的方式理解，这里主要想讲一下deeplearning中的卷积和概率论中的卷积中的相似之处，更深刻的理解概率论中的卷积可以参考&lt;a href=&quot;https://www.bilibili.com/video/BV1Yk4y1K7Az/?spm_id_from=333.788.recommend_more_video.0&amp;#x26;vd_source=66c42cadac6a36ebf163afdb2b4417ba&quot;&gt;3Blue1Brown的视频&lt;/a&gt;，以及他的&lt;a href=&quot;https://www.bilibili.com/video/BV1Vd4y1e7pj/?spm_id_from=333.337.search-card.all.click&amp;#x26;vd_source=66c42cadac6a36ebf163afdb2b4417ba&quot;&gt;离散型卷积的理解&lt;/a&gt; 讲的非常生动。&lt;/p&gt;</content:encoded><img src="/_astro/CNN.DEmP4Oxi.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/CNN.DEmP4Oxi.png" length="0" type="image/jpg"/></item><item><title>Lecture6 BackPropagation</title><link>https://laurie-hxf.xyz/blog/deeplearning-l6</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/deeplearning-l6</guid><description>UMich EECS 498-007 Deep learning-BackPropagation</description><pubDate>Wed, 02 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/propagate-backwards.awNwJWt_.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;Computational Graphs&lt;/h2&gt;
&lt;p&gt;之前提到我们不断训练模型的时候，我们希望模型中的权重能够朝梯度下降的方向进行调整，我们通过损失函数来定义当前的这些权重好不好，然后我们希望损失函数计算出来的结果越来小，因此我们的权重希望能够朝着梯度下降的方向调整。&lt;/p&gt;
&lt;p&gt;这时候我们就想要获得损失函数上点的梯度，以便我们向着梯度减小的方向调整参数。那这时候就产生一个问题，我们怎么计算梯度，一个想法就是暴力，对这个损失函数硬算梯度，但是这种方法显而易见的很不计算机，对于每一个损失函数，我们都要计算一遍梯度，也并不是很模块化。所以我们就有了反向传播算法计算梯度。&lt;/p&gt;
&lt;p&gt;我们用Computational Graphs来表示我们模型的内部结构，包括输入的x，权重矩阵W，正则化R，最后的到我们的损失函数L
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-27%2021.06.46.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;举个例子说明一下反向传播算法是怎么实现的：&lt;/p&gt;
&lt;p&gt;假设现在我们有一个损失函数$f(x,y,z)=(x+y)z$ 然后现在我们想求$x=-2,y=5,z=-4$ 这个点的梯度是什么，然后我们先进行的是forward pass，就是计算出之后的结果，q等于什么，f等于什么
之后进行backward pass，算出相邻后一个对前一个的导数然后一步步往下推，本质上就是链式法则。这种做法的好处就是非常的模块化，每一个节点的计算不再看其他节点是什么，只用看传到这个节点的导数是什么并传递给下一个就可以。我们给每个点要处理的导数命名，downstream gradient，local gradient，upstream gradient。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-27%2021.17.55.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;下面看一个再复杂的例子：
对于图中框出来的那个节点，假设前面所有的节点统一为x，当x经过倒数运算的时候，他得到了最终结果f，简单来说就是$f=\frac{1}{x}$  然后我们想计算  $\frac{ \partial f }{ \partial x }$  然后当前x的值为1.37，所以最终结果就为$-\frac{1}{x^2}=-\frac{1}{{1.37}^2}=-0.53$&lt;br&gt;
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-27%2021.50.19.png&quot; alt=&quot;alt text&quot;&gt;
再看后面：
我们再次把框中节点以前看作x，他经过$e^x$ 操作之后得到结果p($p = e^x$ )，然后我们想知道 $\frac{ \partial f }{ \partial x}$ 但这时候我们的计算方法就是 $\frac{ \partial f }{ \partial x} =  \frac{ \partial p }{ \partial x}* \frac{ \partial f }{ \partial p}$ ,然后$\frac{ \partial f }{ \partial p}$ 已经一步一步传下来了-0.53，所以我们在当前节点中计算$\frac{ \partial p }{ \partial x}$ 因此得到$\frac{ \partial f }{ \partial x}$
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-27%2022.00.00.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h4&gt;改进&lt;/h4&gt;
&lt;p&gt;根据这种计算方法，我们发现一些函数普遍的出现在模型中，比如图中蓝色框出来的部分，s型函数，这个函数挺普遍的，然后他的梯度的公式也很简单，于是我们就可以将这种函数压缩成一个节点，直接用先前算好公式带入，而不是将这些节点拆分的非常原子。这种做法的好处就是我们可以不用再存这么多节点，也不用每次都计算一下这个函数的梯度，可以加快反向传播的速度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-28%2023.19.17.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在我们就可以将很多种这些运算封装成一个个函数，这样当我们需要进行某个运算的时候我们直接调用就可以。这样子我们就实现模块化设计，不用根据每种损失函数都些一遍代码。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-30%2019.47.43.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Vector&lt;/h2&gt;
&lt;p&gt;上面到目前为止都是标量的计算，我们求的是损失对标量$x$ 的导数，但是在实际的神经网络中我们的输入是一个一维的向量，这表示一个图片。然后我们要求的是损失L对W的导数，对X向量的导数，准确来说一般称之为梯度而不是导数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-02%2021.53.52.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;假设现在我们中间某个节点有 $y=xw$ 然后这些都是矩阵，我们从上游传递到这个节点的导数是$\frac{dL}{dy}$ 然后我们想计算一下L对x的导数和L对w的导数，然后这就是他们的公式，但是很奇怪x和y不应该都是向量吗，为什么这里都是矩阵。&lt;/p&gt;
&lt;p&gt;视频中还提到一点就是我们显示计算每个矩阵的雅可比矩阵然后来相乘，意思就是我们不会先计算出整个雅可比矩阵 J 再与 $v^T$ 相乘，而是直接逐步计算结果。&lt;/p&gt;
&lt;p&gt;感觉视频讲的有点云里雾里的，不如deepseek举个例子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-02%2022.29.17.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-02%2022.29.38.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-02%2022.29.53.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-02%2022.30.19.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;正向累积（Forward Accumulation）&amp;#x26;&amp;#x26; 反向累积（Reverse Accumulation）&lt;/h2&gt;
&lt;p&gt;对于计算梯度而言，我们的式子可能是这样
$$
\frac{dL}{dx_0}=\frac{dx_1}{dx_0}\frac{dx_2}{dx_1}\frac{dx_3}{dx_2}\frac{dL}{dx_3}
$$
这时候就有两种顺序来算这个链式法则式子，一个是从右边往左边算，一个是从左边往右算，前者就是反向累积，他的意思就是对于一个计算图，我们从计算图的右边每次逐渐计算到左边&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-04-02%2022.44.37.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后这种顺序所需要的就是一开始前向传播的时候我们要记住中间值，时间复杂度为O(n),空间复杂度也为O(n)。&lt;/p&gt;
&lt;p&gt;然后前向累积就是先计算式子左边，然后推到式子右边，这种方法相比之下就不需要存储中间值，从最左边开始不断向右求导，空间复杂度为O(1),但是这种做法的坏处就是对于每一个变量$x_0,y_0,z_0$ 而言，他们各自的梯度都要单独算一遍，也就是说假设有m个变量，他的时间复杂度就是O(mn).&lt;/p&gt;</content:encoded><img src="/_astro/propagate-backwards.awNwJWt_.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/propagate-backwards.awNwJWt_.png" length="0" type="image/png"/></item><item><title>跑代码记录</title><link>https://laurie-hxf.xyz/blog/%E8%B7%91%E4%BB%A3%E7%A0%81%E8%AE%B0%E5%BD%95</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%B7%91%E4%BB%A3%E7%A0%81%E8%AE%B0%E5%BD%95</guid><description>SurgicalGaussian - Deformable 3D Gaussians for High-Fidelity Surgical Scene Reconstruction</description><pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/论文.B0EKI9Vc.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;指路&lt;/h2&gt;
&lt;p&gt;这次跑的实验是&lt;a href=&quot;https://github.com/xwx0924/SurgicalGaussian?tab=readme-ov-file&quot;&gt;这个&lt;/a&gt;,这篇论文的笔记在&lt;a href=&quot;https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87%E7%AC%94%E8%AE%B0&quot;&gt;这&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;不得不吐槽一下跑实验原来这么麻烦，我大概在周日的时候就开始跑，一直配环境到今天，每一天的命令行都是满屏红色，看着都要红温。多亏了师兄的帮助，帮我这个菜鸡解决好多问题。&lt;/p&gt;
&lt;p&gt;当环境一直配不好的时候，我就纳闷了，为什么这些研究人员不可以把他们的这些环境封装成一个docker，这样就不用再让别人折腾配环境。后来问师兄，~~原来就是懒，他们开源这些代码就只是证明他们可以跑出来，并不是要商业化，把配环境的麻烦交给别人。我真服了。仔细一想，如果他们把这些麻烦留给别人，万一他们造假，别人跑不出的话他们就可以说是你不会配，或者别人看这环境这么难配，直接就放弃了，这不就不能很好的验证他们的结果。~~&lt;/p&gt;
&lt;p&gt;下面就讲一下我配这个环境的走过的路😭&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu 18.04.6 LTS (GNU/Linux 5.4.0-150-generic x86_64)&lt;/li&gt;
&lt;li&gt;pytorch 1.12.0&lt;/li&gt;
&lt;li&gt;cuda 11.6&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在配置之前安装conda是必要的，避免这些环境污染别的，然后就是mamba！！！这个巨重要，极大加速安装环境的速度，而且他能更快地找到合适的包版本。这个真的极大优化你的体验，之前一直用conda，安装包巨慢，关键是他还很容易报错，安了这么久，结果给我报一堆红色的错误，当时真的破防。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;然后就开始跟着github少的可怜的指示来安装&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git clone https://github.com/xwx0924/SurgicalGaussian.git
cd SurgicalGaussian

conda create -n SurgicalGaussian python=3.7 
conda activate SurgicalGaussian

# install pytorch and others.
pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 torchaudio==0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113
pip install -r requirements.txt
# You also need to install the pytorch3d library to compute Gaussian neighborhoods.

# You can follow 4DGS to download depth-diff-gaussian-rasterization and simple-knn.
pip install -e submodules/depth-diff-gaussian-rasterization  
pip install -e submodules/simple-knn
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Pytorch&lt;/h4&gt;
&lt;p&gt;这里我安装的是pytorch 1.12.0+cu11.6 可以上&lt;a href=&quot;https://pytorch.org/get-started/previous-versions/&quot;&gt;pytorch官网&lt;/a&gt;来找对应安装版本的指令,我这里用的指令就是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip install torch==1.12.0+cu116 torchvision==0.13.0+cu116 torchaudio==0.12.0 --extra-index-url https://download.pytorch.org/whl/cu116
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用了个python程序来检验安装的版本是不是想要安装的版本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import torch
print(torch.__version__)          # PyTorch 版本
print(torch.version.cuda)         # PyTorch 编译时使用的 CUDA 版本
print(torch.cuda.is_available())  # 检查 CUDA 是否可用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出就是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;1.12.0
11.6
True
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Pytorch3D&lt;/h4&gt;
&lt;p&gt;然后就根据指引安装requirements.txt，安完之后就到了另一个坑，指引只是轻描淡写的写了一下要安装pytorch3d，但这个库可一点都不好安装。&lt;/p&gt;
&lt;p&gt;指引没有提怎么能安好这个库，这种情况下就去搜索这个库的&lt;a href=&quot;https://github.com/facebookresearch/pytorch3d/blob/main/INSTALL.md&quot;&gt;github仓库&lt;/a&gt;，里面提到要先安装一些环境，再安装pytorch3d这个库，先安装iopath，然后如果你的cuda版本低于11.7，你就要安装另外一个库。这里也建议用manba安装
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-28%2021.45.40.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mamba install -c iopath iopath
mamba install -c bottler nvidiacub
# Anaconda Cloud
mamba install pytorch3d -c pytorch3d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反正遇到conda的指令都替换为mamba，真的太折磨了。&lt;/p&gt;
&lt;h4&gt;Submodels&lt;/h4&gt;
&lt;p&gt;安装好后开始安装这两个submodel，注意的是，这两个model并没有包含在原仓库的代码里你必须跑到对应的别的库里面安装这两个model，然后这里又一个坑，指引说他借鉴了3D和4D两个仓库，这两个仓库里面都有这两个submodel，关键是这两个库里面的model版本是不一样的。原仓库安装的其实是&lt;a href=&quot;https://github.com/hustvl/4DGaussians&quot;&gt;4D&lt;/a&gt;的submodel，这里就将这个仓库clone然后把submodel拷贝到原仓库中&lt;/p&gt;
&lt;p&gt;这下终于可以安装这两个model，结果又报错，他可能报错你的pytorch版本和cuda版本不匹配，应为我安装的pytorch版本是基于cuda11.6的，但我的系统用的是10.1&lt;/p&gt;
&lt;p&gt;可以用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nvcc --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来看当前使用的cuda版本到底是什么，然后其实才发现原来cuda版本都没有安装，然后跑到&lt;a href=&quot;https://developer.nvidia.com/cuda-11-6-0-download-archive&quot;&gt;nvidia官网&lt;/a&gt;安装
根据提示来选择，最后一个选择local&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;wget [https://developer.download.nvidia.com/compute/cuda/11.6.0/local_installers/cuda_11.6.0_510.39.01_linux.run](https://developer.download.nvidia.com/compute/cuda/11.6.0/local_installers/cuda_11.6.0_510.39.01_linux.run)

sh cuda_11.6.0_510.39.01_linux.run --silent --toolkit --toolkitpath=~/cuda-11.6 --defaultroot=~/cuda-11.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好像这里还要export一下，具体指令忘记了。这里值得注意的是原本我只在conda里面安装了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mamba install nvidia/label/cuda-11.6.1::cuda-toolkit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后后面就会报错缺少什么.h文件，于是就替换成了这个命令。
再用这个指令检查一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nvcc --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后后面就开始安装submodels，这里应该就没有问题。&lt;/p&gt;
&lt;h4&gt;Train&lt;/h4&gt;
&lt;p&gt;安装完成以后以为万事大吉，环境终于配好了，结果还有报错。但我满心欢喜的想要运行训练的指令，结果又报错。说是缺mmcv这个库，安装库真的很痛苦，这里又踩坑。看&lt;a href=&quot;https://mmcv.readthedocs.io/zh-cn/2.x/get_started/installation.html&quot;&gt;官网&lt;/a&gt;,然后注意的是安装的版本要是1.7，不要安装2.0之后的版本，可能原仓库用的就是之前的版本。推荐使用官方推荐的mim安装方式&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip install -U openmim
mim install mmcv-full==1.7.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装好这个库后，我又运行了训练的命令，结果又有库没有安装，tinycudann，这玩意真是个毒瘤，网上一堆人安装这个库报错，我安装了好久，结果等我安装好后又报错说什么&lt;code&gt;Could not find compatible tinycudann extension for compute capability 75.&lt;/code&gt;  md，这里的解决办法就是找到报错的那个程序，把这个库给注释掉，就是这么无语，这个库根本没有用到。&lt;/p&gt;
&lt;p&gt;到此应该就没有问题了，还要注意的是，数据集原仓库也没有给，要自己去给的链接那里下载，然后安装的文件排列要遵循指引中提到的，&lt;/p&gt;
&lt;p&gt;然后应该就可以~~顺利~~的运行训练指令，我真服了。跑完之后跑render指令，这里又要注意的是render里面还有一个库要安装，一看又是没有用上的，就注释掉了。然后就把剩下的指令跑完了就行了，跑出个结果。&lt;/p&gt;
&lt;h2&gt;结果&lt;/h2&gt;
&lt;p&gt;运行完这个命令之后就可以看到数据的结果，主要是SSIM[^1]，PSNR[^2]，LPIPS[^3]，将这些数据和论文里面的一比，感觉差不多但还是稍逊了一点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;python metrics.py -m output/pulling
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-28%2023.57.42.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-28%2023.55.54.png&quot; alt=&quot;alt text&quot;&gt;
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-29%2012.16.00.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;看看渲染之后的效果&lt;/p&gt;
&lt;p&gt;| 原图                       | 处理后                  |
| ------------------------ | -------------------- |
| &lt;img src=&quot;https://laurie-hxf.xyz/_image?href=%2F_astro%2Ftrain00019.B7ChbzRh.png&amp;#x26;w=640&amp;#x26;h=512&amp;#x26;f=webp&quot; alt=&quot;alt text&quot;&gt; | &lt;img src=&quot;https://laurie-hxf.xyz/_image?href=%2F_astro%2F00019.DntP8ZjG.png&amp;#x26;w=640&amp;#x26;h=512&amp;#x26;f=webp&quot; alt=&quot;alt text&quot;&gt; |&lt;/p&gt;
&lt;p&gt;| 原图                       | 处理后                  |
| ------------------------ | -------------------- |
| &lt;img src=&quot;https://laurie-hxf.xyz/_image?href=%2F_astro%2F00043.BS2MnmY-.png&amp;#x26;w=640&amp;#x26;h=512&amp;#x26;f=webp&quot; alt=&quot;alt text&quot;&gt; | &lt;img src=&quot;https://laurie-hxf.xyz/_image?href=%2F_astro%2F11100043.DkgRaxGA.png&amp;#x26;w=640&amp;#x26;h=512&amp;#x26;f=webp&quot; alt=&quot;alt text&quot;&gt; |&lt;/p&gt;
&lt;p&gt;看右下角这个图就可以明显看出SurgicalGaussian对于一些从头到尾被遮挡的区域只能糊着抹过去，不能够很好的处理这部分的细节，不过这应该也是3D Gaussians的通病。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;感悟&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;经过这个贼麻烦的配置环境，还有要提的，就是一旦遇到报错了，先看一下报错信息是什么，看自己能不能解决。解决不了，把报错信息google一下，然后就会发现，原来已经有这么多人踩过坑，一般都在github issue里面，一般都已经有了解决方法。下策就是直接把报错喂给ai，这种做法真的很痛苦，如果你按照ai的指令瞎跑一堆，跑到最后，你的问题没有解决，但是你的环境已经混乱了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;还有就是当你连接服务器跑代码的时候最好用tmux，这个可以直接在后台跑，不用怕跑着跑着电脑和服务器断开连接，结果你再也不能和那个进程连接上。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;[^1]:SSIM（Structural Similarity Index，结构相似性指数）衡量两幅图像的结构相似性，反映人眼的视觉感知。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^2]:PSNR（Peak Signal-to-Noise Ratio，峰值信噪比）衡量图像的峰值信号与噪声之间的比率，常用于评估图像压缩、去噪等任务的质量。&lt;/p&gt;
&lt;p&gt;[^3]:LPIPS（Learned Perceptual Image Patch Similarity，感知相似度）衡量深度学习模型感知下的图像相似性，更符合人眼视觉。&lt;/p&gt;</content:encoded><img src="/_astro/论文.B0EKI9Vc.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/论文.B0EKI9Vc.png" length="0" type="image/png"/></item><item><title>SurgicalGaussian MICCAI 2024 论文笔记</title><link>https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87surgicalgaussian</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/%E8%AE%BA%E6%96%87surgicalgaussian</guid><description>SurgicalGaussian - Deformable 3D Gaussians for High-Fidelity Surgical Scene Reconstruction</description><pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/论文.B0EKI9Vc.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;Authors: &quot;Weixing Xie, Junfeng Yao, Xianpeng Cao, Qiqin Lin, Zerui Tang, Xiao Dong, Xiaohu Guo&quot;&lt;/p&gt;
&lt;p&gt;Date: &apos;2024-01-01&apos;&lt;/p&gt;
&lt;p&gt;URL: &quot;https://link.springer.com/10.1007/978-3-031-72089-5_58&quot;&lt;/p&gt;
&lt;p&gt;对内窥镜视频的动态重建（Dynamic reconstruction）对于机器人手术是关键的。~~动态重建的目的比如能用2D的视频重建出3D场景，然后投影到2D，可以为医生提供别的视角而不只是摄像头的单一视角，以及将可以将手术仪器这些移除，以此看到被手术仪器遮挡的部分。~~&lt;/p&gt;
&lt;p&gt;之前的重建技术都是基于NeRFs，虽然取得不错的成绩但是这种技术不能处理一些细节并且不能实时渲染，不止如此，受限制的单视感感知和遮挡仪器还提出了手术现场重建的特殊挑战。所以本篇论文就介绍了他们的基于3D Gaussian的新技术叫SurgicalGaussian，最终的效果就是能够去除视频中的手术工具，并且能有一个高质量的渲染，以及更快的渲染速度和GPU利用率。&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;一开始介绍了一下3维重建的历史，手术场景的3维重建的重要性&lt;/p&gt;
&lt;p&gt;然后一开始人们基于 point clouds and surfels（点云和表面元素）对手术场景进行离散的建模，但这种技术不能够处理剧烈运动和拓扑变化引起的颜色改变问题，比如角度不同，导致光照不同，最终导致颜色发生变化。&lt;/p&gt;
&lt;p&gt;然后NeRF技术就产生，相比于点云，这个方法用的是连续表示场景，这种方法在生成高质量的外观和几何图形方面具有优势。然后EndoNeRF改进了NeRF，EndoNeRF相比于NeRF引入depth suervision(深度监督)。同时还有LerPlane这个技术，他对采样点的时空特征进行了有效编码，减少了动态组织建模的工作量。但是基于NeRF的这些方式需要消耗大量的计算资源，很难进行实时渲染。&lt;/p&gt;
&lt;p&gt;所以就有了3D Gaussian Splatting (3DGS)，他能产生逼真的渲染效果，同时训练速度也比NeRF快。具体来说，这个方法就是将场景用三维高斯来表示，并且用可变光栅管道来渲染图像。&lt;/p&gt;
&lt;p&gt;本篇的在3DGS的基础上进行了一些改进：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;他们提出了deformable 3D Gaussians framework （可形变三维高斯框架）&lt;/li&gt;
&lt;li&gt;有效的高斯初始化策略（GIDM）&lt;/li&gt;
&lt;li&gt;通过颜色正则化和形变正则化，分别解决了遮挡区域的颜色预测和高斯形变场的噪声问题。&lt;/li&gt;
&lt;li&gt;高质量的重建质量和实时渲染速度&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;p&gt;将视频作为输入, $V = {I_i,D_i,M_i:i∈[0,T]}$,  $I_i$ 表示第i帧的图像，$D_i$ 是 深度图（Depth Map），用于表示 图像中每个像素点到摄像机的距离。 $M_i$ 表示mask，用1来表示图中手术工具的部分，其他用0表示。给定这些输入，能构产生可以去除手术器械并以高质量恢复变形的软组织的场景&lt;/p&gt;
&lt;h4&gt;Preliminaries(前言)&lt;/h4&gt;
&lt;p&gt;本质上3DGS的方法就是输入一组图片（视频帧），根据这些图片，训练一个模型使之能够根据这些构建一个3D的场景，这个场景就是一个3D 高斯点云，然后根据这个3D场景，我们可以选择不同的视角，然后将这些点投影到对应视角的2D图像上。&lt;/p&gt;
&lt;p&gt;每一个3D高斯点我们就表示为Gaussian primitives $\mathcal{G}$（高斯基本单元）这不是一个无结构的单点，而是一个 &lt;strong&gt;椭球形的 3D 形状&lt;/strong&gt;，由协方差矩阵 Σ 控制其方向和大小。当我们从某个视角观察一个 3D 场景时，每个高斯分布会投影到 2D 屏幕上，形成一个 &lt;strong&gt;高斯形状的斑点（splat）&lt;/strong&gt;，类似于散景模糊的点。&lt;br&gt;
渲染时，所有这些 2D 高斯点会&lt;strong&gt;叠加&lt;/strong&gt;在一起，形成最终的 2D 图像。
$$G(x)=exp(−\frac{1}{2}​(x−μ)^TΣ^{−1}(x−μ))$$&lt;/p&gt;
&lt;p&gt;这里的 Σ 为协方差矩阵，他控制高斯的形状和方向，向量$x\in R^3$表示世界坐标系中的点，这里默认的高斯中心μ为0，其中其中 $Σ=RSS^TR^T$，S是缩放矩阵，R是旋转矩阵。&lt;/p&gt;
&lt;p&gt;然后文章中只用向量$s\in R^3$来记录S的对角线元素，这个向量表示沿着 x、y 和 z 轴的缩放因子。$q\in R^4$ 记录旋转矩阵的四元数,以此不用记录整个矩阵从而节省空间&lt;/p&gt;
&lt;p&gt;每一个高斯点还需要$\alpha$ 表示透明度，c表示球面调和函数系数（Spherical Harmonics Coefficients）用于存储和计算球面上的光照或颜色分布&lt;/p&gt;
&lt;p&gt;因此高斯基本单元就可以表示为 $\mathcal{G}$ = {(x,s,q,$\alpha$,c)}&lt;/p&gt;
&lt;p&gt;然后通过视图变换矩阵V：通过视图变换将三维物体的坐标转换到相机坐标系中，这样物体相对于相机的方向和位置就确定了。&lt;/p&gt;
&lt;p&gt;投影变换的仿射近似的雅可比矩阵 J来将该视角的3D场景投影到2D中。&lt;/p&gt;
&lt;p&gt;然后对于2D图片上每一像素 r 我们可以根据公式得出他的颜色和深度信息&lt;/p&gt;
&lt;p&gt;$$
\hat{C}(\mathbf{r}) = \sum_{i} (\alpha_i&apos; \prod_{j=1}^{i-1} (1 - \alpha_j&apos;)) c_i,\ \hat{D}(\mathbf{r}) = \sum_{i} (\alpha_i&apos; \prod_{j=1}^{i-1} (1 - \alpha_j&apos;)) d_i. \tag{1}
$$&lt;/p&gt;
&lt;h4&gt;GIDM Initialization Strategy(GIDM初始化策略)&lt;/h4&gt;
&lt;p&gt;当一开始我们训练将2D转化为3D高斯点云的模型的时候，我们需要初始化这些高斯点的参数，通过投影然后和真实场景的2D图像对比，来不断调整这些高斯点的参数。一个好的初始化策略可以加速模型的收敛。&lt;/p&gt;
&lt;p&gt;一开始的3DGS用SFM来初始化这些参数，但是对于内窥镜这种复杂多变的场景，他的准确率就不太高。因此提出GIDM初始化策略。&lt;/p&gt;
&lt;p&gt;文章中的初始化策略就是帧与帧之间相互补充信息，在别的帧中出现的组织信息就被补充进点云中，最终点云中有的就是所有帧中组织信息的并集，缺失的就是所有帧中mask(手术仪器)的交集，以此来初始化。&lt;/p&gt;
&lt;p&gt;公式就是这个&lt;/p&gt;
&lt;p&gt;$$
\mathbf{P}^* = { \mathbf{D}^* \mathbf{K}_e^{-1} \mathbf{K}&lt;em&gt;i^{-1} (\mathbf{I}^* \odot (\mathbf{1} - \mathbf{M}^&lt;em&gt;)) }, \mathbf{M}^&lt;/em&gt; = \bigcap&lt;/em&gt;{i=0}^T \mathbf{M}_i. \tag{2}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$P^∗$: 这是生成的点云的集合，每个点是空间中的一个三维点。&lt;/li&gt;
&lt;li&gt;$D^∗$: 这是深度图（depth map）中的深度信息，表示相机与场景中每个点之间的距离（深度值）。&lt;/li&gt;
&lt;li&gt;$K_e$: 这是相机的外参矩阵（extrinsic matrix），通常用于描述相机在世界坐标系中的位置和朝向。它描述了相机的旋转和平移变换。&lt;/li&gt;
&lt;li&gt;$K_i$: 这是相机的内参矩阵（intrinsic matrix），描述了相机的焦距、主点、以及像素坐标系与相机坐标系之间的关系。&lt;/li&gt;
&lt;li&gt;$I^∗$: 这是输入的图像或深度图像，表示场景中每个像素的颜色或灰度值。在此上下文中，可能代表场景图像的颜色通道或亮度。&lt;/li&gt;
&lt;li&gt;$M^∗$: 这是一个掩码（mask）图像，用于指示哪些区域有效。掩码通常用于处理图像中的背景或无效区域（例如，深度信息缺失的区域）。&lt;/li&gt;
&lt;li&gt;⊙: 这是哈达玛积（Hadamard product），表示逐元素乘法。在此上下文中，它是将图像信息（例如，颜色信息或灰度值）与掩码 $(1−M^∗)$ 相结合，剔除无效区域。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Deformable 3D Gaussian Representation&lt;/h4&gt;
&lt;p&gt;当我们处理处理动态场景时，我们将一开始获得的是组织的高斯点云，但是这个点云是静态的，这些点云一开始存储在Canonical Spac（标准空间）中，他不会随着组织发生的形变而产生变化，于是我们就用deformation network（形变场）来建模这些点随时间是怎么运动的，他的的核心作用是：&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;，可以建模简单的刚体运动（Rigid Motion，如平移、旋转），也可以建模复杂的非刚体形变（Non-Rigid Deformation，如人脸表情、衣物摆动等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相当于给定时间 t 和高斯点的初始坐标，我们就可以得到t时间对应高斯点的坐标。这种方法在处理多个高斯点的时候非常的灵活，同时还可以节省空间，不用记录下每个高斯点的每个时间的位置。&lt;/p&gt;
&lt;p&gt;文章中，他们的这个形变场用的就是MLP来进行建模，将一开始生成的高斯点云输入进去，然后训练的这个MLP，让他预测之后点云的形状，然后再把预测后的点云投影到2D中，和实际的视频帧进行比对，以此来训练。&lt;/p&gt;
&lt;p&gt;训练完成后，他就可以输入一个高斯点的坐标 $x_c$，和时间 t ，他就可以计算出这个高斯点的偏移。
$$(δx,δs,δq) = F_Θ((γ(x_c),γ(t)).$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;位置变化 δx&lt;/li&gt;
&lt;li&gt;缩放变化 δs&lt;/li&gt;
&lt;li&gt;旋转变化 δq&lt;/li&gt;
&lt;li&gt;形变场$F_Θ$&lt;/li&gt;
&lt;li&gt;位置编码函数 $γ()$, 位置编码的作用是：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;增强 MLP 的表达能力&lt;/strong&gt;：如果直接输入坐标 (x,y,z)，MLP 可能难以学习到高频变化的运动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通过特定频率的编码，让 MLP 学习复杂的运动模式&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后 t 时间的高斯点就变成 $\mathcal{G_0} = {(x_0,s_0,q_0,\alpha,c)}$
其中 $x_0 = x_c + δx,s_0 = s_c· exp(δs),q_0 = q_c· δq$&lt;/p&gt;
&lt;p&gt;$\alpha,c$ 不变，因为他们是高斯的固有属性。&lt;/p&gt;
&lt;h4&gt;Optimization&lt;/h4&gt;
&lt;p&gt;他们的这个框架同时优化MLP形变场的参数以及2D转成标准空间下的3D高斯点云质量，以及去除手术器械。
$$
\mathcal L_{\text{color}} = \left| (I_i - \hat{C}_i) \cdot (1 - M_i) \right|_1
$$
真实彩色图像 $I_i$ 和预测图像 $\hat{C}_i$ 相减，然后取出手术器械部分$(1-M_i)$ 然后求 L1 范数，就是矩阵的每个数的绝对值加起来，这个公式就是损失函数&lt;/p&gt;
&lt;p&gt;$$
\mathcal L_{\text{depth}} = \left| (D_i - \hat{D}_i) \cdot (1 - M_i) \right|_1. \tag{5}
$$
和上面式子意思类似，是深度的损失函数&lt;/p&gt;
&lt;h5&gt;Deformation Regularization.&lt;/h5&gt;
&lt;p&gt;在处理单视角时候，形变场会后一些限制，这会造成噪声。于是文章提出一种正则化方法，让一个高斯点附近的高斯点之间有相似的形变。&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}&lt;em&gt;{pos} = \sum&lt;/em&gt;{i=1}^{N} \sum_{k=1}^{K} \left| d\left(\mathbf{x}_c^{(i)}, \mathbf{x}_c^{(k)}\right) - d\left(\mathbf{x}_o^{(i)}, \mathbf{x}_o^{(k)}\right) \right|_1. \tag{6}
$$&lt;/p&gt;
&lt;p&gt;位置损失函数$\mathcal{L}_{pos}$，计算标准空间中每个高斯点 i 和他周围K(文章中K=5)个点的的距离，和观察空间中点i和他周围K个点的距离，然后求范数，就得到损失函数&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}&lt;em&gt;{cov} = \sum&lt;/em&gt;{i=1}^{N} \sum_{k=1}^{K} \left| d\left( \mathbf{Σ}_c^{(i)}, \mathbf{Σ}_c^{(k)} \right) - d\left( \mathbf{Σ}_o^{(i)}, \mathbf{Σ}_o^{(k)} \right) \right|_1. \tag{7}
$$&lt;/p&gt;
&lt;p&gt;这个是协方差矩阵的损失函数$\mathcal{L}_{cov}$ ，与上面的公式类似&lt;/p&gt;
&lt;h5&gt;Occlusion-Based Color Regularization（基于遮挡的色彩正则化）&lt;/h5&gt;
&lt;p&gt;因为NeRFs是用连续的办法表示场景，因此可以很好的弥补从头到尾被手术仪器遮挡的部分，但是3DGS是基于离散的高斯点，mask $M^*$ 让这部分的图像缺失，所以在从头到尾被手术仪器遮挡的部分他会使得渲染的图片中有个空洞，于是文章中引入a total variational loss，利用周围的点的颜色可以帮助弥补缺失的那部分颜色。&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}&lt;em&gt;{smooth} = \frac{1}{n} \sum&lt;/em&gt;{p,q} \left( | \mathbf{C}^{p,q} - \mathbf{C}^{p-1,q} |&lt;em&gt;{2}^{2} + | \mathbf{C}^{p,q} - \mathbf{C}^{p,q-1} |&lt;/em&gt;{2}^{2} \right), \mathbf{C} = \hat{\mathbf{C}}_{i} \odot \mathbf{M}^{*}, \tag{8}
$$&lt;/p&gt;
&lt;p&gt;这个公式的意思就是比较 (p,q) 位置的像素值和(p-1，q),(p,q-1)位置像素值的差异，用欧几里得范数的平方来度量，求和n个像素之后取平均。&lt;/p&gt;
&lt;h5&gt;Total Loss&lt;/h5&gt;
&lt;p&gt;最后他们再添加3DGS中的SSIM loss 得到最终的损失函数&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L} = (\mathcal{L}&lt;em&gt;{color} + \lambda_1 \mathcal{L}&lt;/em&gt;{ssim} + \lambda_2 \mathcal{L}&lt;em&gt;{depth}) + (\lambda_3 \mathcal{L}&lt;/em&gt;{pos} + \lambda_4 \mathcal{L}_{cov} + \lambda_5 \mathcal{L}_{smooth}). \tag{9}
$$&lt;/p&gt;
&lt;h2&gt;Experiments&lt;/h2&gt;
&lt;p&gt;这里就讲了一下他们实验的结果，交代数据集，和其他之前已有的模型进行比较,分析之前方法的缺陷，进行消融实验（Evaluation Metrics）&lt;a href=&quot;%E6%B6%88%E8%9E%8D%E5%AE%9E%E9%AA%8C%E7%9A%84%E7%9B%AE%E7%9A%84%E5%9C%A8%E4%BA%8E%E7%A7%BB%E9%99%A4%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%89%B9%E5%AE%9A%E7%9A%84%E9%83%A8%E5%88%86%EF%BC%8C%E6%9D%A5%E6%8E%A7%E5%88%B6%E5%8F%98%E9%87%8F%E5%BC%8F%E7%9A%84%E7%A0%94%E7%A9%B6%E8%BF%99%E4%B8%AA%E9%83%A8%E5%88%86%E5%AF%B9%E4%BA%8E%E7%B3%BB%E7%BB%9F%E6%95%B4%E4%BD%93%E7%9A%84%E5%BD%B1%E5%93%8D%E3%80%82%E5%A6%82%E6%9E%9C%E5%8E%BB%E9%99%A4%E8%BF%99%E4%B8%80%E9%83%A8%E5%88%86%E5%90%8E%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%80%A7%E8%83%BD%E6%B2%A1%E6%9C%89%E5%A4%AA%E5%A4%A7%E6%8D%9F%E5%A4%B1%EF%BC%8C%E9%82%A3%E4%B9%88%E8%AF%B4%E6%98%8E%E8%BF%99%E4%B8%80%E9%83%A8%E5%88%86%E5%AF%B9%E4%BA%8E%E6%95%B4%E4%B8%AA%E7%B3%BB%E7%BB%9F%E8%80%8C%E8%A8%80%E5%B9%B6%E4%B8%8D%E5%85%B7%E6%9C%89%E5%A4%AA%E5%A4%A7%E7%9A%84%E9%87%8D%E8%A6%81%E6%80%A7%EF%BC%9B%E5%A6%82%E6%9E%9C%E5%8E%BB%E9%99%A4%E4%B9%8B%E5%90%8E%E7%B3%BB%E7%BB%9F%E6%80%A7%E8%83%BD%E6%98%8E%E6%98%BE%E7%9A%84%E4%B8%8B%E9%99%8D%EF%BC%8C%E5%88%99%E8%AF%B4%E6%98%8E%E8%BF%99%E4%B8%80%E9%83%A8%E5%88%86%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%98%AF%E5%BF%85%E4%B8%8D%E5%8F%AF%E5%B0%91%E7%9A%84%E3%80%82%E5%BD%93%E7%84%B6%EF%BC%8C%E5%A6%82%E6%9E%9C%E5%87%BA%E7%8E%B0%E4%BA%86%E7%AC%AC%E4%B8%89%E7%A7%8D%E6%83%85%E5%86%B5%EF%BC%8C%E4%B9%9F%E5%B0%B1%E6%98%AF%E5%8E%BB%E9%99%A4%E4%B9%8B%E5%90%8E%E6%A8%A1%E5%9E%8B%E7%9A%84%E6%80%A7%E8%83%BD%E4%B8%8D%E9%99%8D%E5%8F%8D%E5%8D%87%EF%BC%8C%E9%82%A3%E4%B9%88%E5%BB%BA%E8%AE%AE%E6%89%BE%E4%B8%80%E4%B8%8Bbug%E6%88%96%E8%80%85%E4%BF%AE%E6%94%B9%E8%AE%BE%E8%AE%A1%E3%80%82&quot;&gt;^1&lt;/a&gt;，交代他们的实验细节。&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;概括了一下他们的创新点，成果，明显好于SOTA&lt;a href=&quot;**SOTA**%C2%A0%E6%98%AF%C2%A0**State-of-the-Art**%C2%A0%E7%9A%84%E7%BC%A9%E5%86%99%EF%BC%8C%E8%A1%A8%E7%A4%BA%E6%9F%90%E4%B8%80%E9%A2%86%E5%9F%9F%E5%BD%93%E5%89%8D%E6%9C%80%E5%85%88%E8%BF%9B%E7%9A%84%E6%96%B9%E6%B3%95%E3%80%81%E6%8A%80%E6%9C%AF%E6%88%96%E7%BB%93%E6%9E%9C%E3%80%82%E5%AE%83%E5%B8%B8%E7%94%A8%E4%BA%8E%E5%AD%A6%E6%9C%AF%E8%AE%BA%E6%96%87%E3%80%81%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E5%92%8C%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD%E7%A0%94%E7%A9%B6%E4%B8%AD%EF%BC%8C%E6%8C%87%C2%A0**%E5%9C%A8%E6%9F%90%E9%A1%B9%E4%BB%BB%E5%8A%A1%E4%B8%8A%E6%80%A7%E8%83%BD%E6%9C%80%E4%BC%98%E7%9A%84%E6%96%B9%E6%B3%95%E6%88%96%E6%A8%A1%E5%9E%8B**%E3%80%82&quot;&gt;^2&lt;/a&gt;，说本文为手术场景建模提供一种新思路。&lt;/p&gt;
&lt;hr&gt;</content:encoded><img src="/_astro/论文.B0EKI9Vc.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/论文.B0EKI9Vc.png" length="0" type="image/png"/></item><item><title>nnUNet practise</title><link>https://laurie-hxf.xyz/blog/nnunet-practise</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/nnunet-practise</guid><description>use nnUNet to train a model</description><pubDate>Sat, 08 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/nnUNet.DR4lBins.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;最近要用nnUNet来处理医学图像，准确来说叫图像分割。简单来说就是训练一个模型，使之给定他一个图片，他能够将图片的血管分割出来。此次使用的工具是&lt;a href=&quot;https://github.com/MIC-DKFZ/nnUNet?tab=readme-ov-file&quot;&gt;nnUNet&lt;/a&gt;，nnUNet是一个自适应的深度学习框架，专为医学图像分割任务设计。他的最大的特点就是自动化，他自动训练训练集，自动预训练，自动评判。简而言之，他就是一个功能多样且强大，封装好的医学图像分割工具。&lt;/p&gt;
&lt;p&gt;所以本次博客主要记录一下如何使用nnUNet进行训练。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/laurie-hxf/nnunet_practise&quot;&gt;训练仓库&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;mac m1 pro 16 + 512&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;配置环境&lt;/h2&gt;
&lt;p&gt;一开始我们要clone nnUNet的仓库到我们的本地，当然也可以不用，要是没有需求修改他们的模型就不用。
但这里我clone了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git clone https://github.com/MIC-DKFZ/nnUNet.git
cd nnUNet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后建议用conda或者其他的来管理python包这里要安装的还挺多的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip install -e .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是用于在当前目录下以“可编辑模式”（editable mode）安装 Python 包的命令。具体来说，这个命令会查找当前目录中的 &lt;code&gt;setup.py&lt;/code&gt;（或者 &lt;code&gt;pyproject.toml&lt;/code&gt;），并将包安装到 Python 环境中，但不是把文件复制过去，而是建立一个指向源代码的链接。这意味着当你修改源码时，无需重新安装，修改会立即生效，非常适合开发调试阶段。&lt;/p&gt;
&lt;h2&gt;数据处理&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/MIC-DKFZ/nnUNet/blob/master/documentation/dataset_format.md&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我一开始拿到的数据集其实并不是按照nnUNet标准要求的数据格式来存放的。以下是我一开始数据集的结构图。我一开始有两个数据集，每个数据集的格式都是如此&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/path/to/your_dataset_1/
 |——crop                          
 |——crop_test
 |——crop_train                    
 |   |——0
 |      |——image.png
 |      |——label.png
 |      |——mask.png
 |   |——1
 | …
 |——original                      
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;original：存放原始图片，用于以防万一后续检查&lt;/li&gt;
&lt;li&gt;crop：切割后的图片，就是将原始图片进行裁切，放大我们想要关注的区域&lt;/li&gt;
&lt;li&gt;crop_train：他和crop_test平分crop中的图片，一半用于训练，一半用来训练完之后的检验。然后这个数据有0，1，2, ....个文件夹，里面存的image.png, label.png, mask.png
&lt;ul&gt;
&lt;li&gt;image.png 切割后的图片&lt;/li&gt;
&lt;li&gt;label.png 标签，就是人工标注的分割结果，用于训练和检测&lt;/li&gt;
&lt;li&gt;mask.png 测试的时候防止预测区域外干扰使用的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;crop_test 结构和crop_train一样&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但可惜的是，nnUNet支持的格式并不是这种, 数据集必须位于 &lt;code&gt;nnUNet_raw&lt;/code&gt; 文件夹中（你可以在安装 nnU-Net 时定义该文件夹，或在每次运行 nnU-Net 命令时导出/设置该路径）。每个分割数据集都作为一个独立的“Dataset”存储，并关联一个数据集 ID（一个三位整数）和一个你自定义的数据集名称。例如，Dataset005_Prostate 的名称为 “Prostate”，其数据集 ID 为 5。数据集在 &lt;code&gt;nnUNet_raw&lt;/code&gt; 文件夹中的组织结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nnUNet_raw/
├── Dataset001_BrainTumour
├── Dataset002_Heart
├── Dataset003_Liver
├── Dataset004_Hippocampus
├── Dataset005_Prostate
├── ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在每个数据集文件夹内，期望的结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Dataset001_BrainTumour/
├── dataset.json
├── imagesTr
├── imagesTs  # 可选
└── labelsTr
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;imagesTr&lt;/strong&gt;：存放训练样本的图像。nnU-Net 会基于这些数据进行管道配置、交叉验证训练、后处理以及寻找最佳集成策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;imagesTs&lt;/strong&gt;（可选）：存放测试样本的图像。nnU-Net 不会使用它们，仅作为一个便于存储的目录，这是 MSD 文件夹结构的遗留部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;labelsTr&lt;/strong&gt;：存放训练样本的真实分割图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dataset.json&lt;/strong&gt;：包含数据集的元数据信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述方案将产生如下文件夹结构。以下以 MSD 的第一个数据集 BrainTumour 为例，该数据集有四个输入通道：FLAIR（0000）、T1w（0001）、T1gd（0002）和 T2w（0003）。注意，imagesTs 文件夹为可选。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nnUNet_raw/Dataset001_BrainTumour/
├── dataset.json
├── imagesTr
│   ├── BRATS_001_0000.nii.gz
│   ├── BRATS_001_0001.nii.gz
│   ├── BRATS_001_0002.nii.gz
│   ├── BRATS_001_0003.nii.gz
│   ├── BRATS_002_0000.nii.gz
│   ├── BRATS_002_0001.nii.gz
│   ├── BRATS_002_0002.nii.gz
│   ├── BRATS_002_0003.nii.gz
│   ├── ...
├── imagesTs
│   ├── BRATS_485_0000.nii.gz
│   ├── BRATS_485_0001.nii.gz
│   ├── BRATS_485_0002.nii.gz
│   ├── BRATS_485_0003.nii.gz
│   ├── BRATS_486_0000.nii.gz
│   ├── BRATS_486_0001.nii.gz
│   ├── BRATS_486_0002.nii.gz
│   ├── BRATS_486_0003.nii.gz
│   ├── ...
└── labelsTr
    ├── BRATS_001.nii.gz
    ├── BRATS_002.nii.gz
    ├── ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是 MSD 第二个数据集的另一个示例，该数据集只有一个输入通道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nnUNet_raw/Dataset002_Heart/
├── dataset.json
├── imagesTr
│   ├── la_003_0000.nii.gz
│   ├── la_004_0000.nii.gz
│   ├── ...
├── imagesTs
│   ├── la_001_0000.nii.gz
│   ├── la_002_0000.nii.gz
│   ├── ...
└── labelsTr
    ├── la_003.nii.gz
    ├── la_004.nii.gz
    ├── ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：nnUNet version 2 已经不止支持nii.gz格式，包括png等的很多格式都已经支持，具体可以看他们的&lt;a href=&quot;https://github.com/MIC-DKFZ/nnUNet/blob/master/documentation/dataset_format.md&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;所以我们要做的就是将数据集中的crop_train中的训练图片和label分出来，存到对应的文件夹中。此外我们还要根据我们的数据写dataset.json文件。&lt;/p&gt;
&lt;p&gt;脚本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os  
import shutil  
from batchgenerators.utilities.file_and_folder_operations import maybe_mkdir_p, save_json  
from PIL import Image  
import numpy as np  
  
  
def organize_dataset(input_dir, output_dir, is_label=False):  
    for case in os.listdir(input_dir):  
        case_dir = os.path.join(input_dir, case)  
        if os.path.isdir(case_dir):  
            image_file = os.path.join(case_dir, &apos;image.png&apos;)  
            label_file = os.path.join(case_dir, &apos;label.png&apos;) if is_label else None  
  
            # Copy image to output directory  
            if label_file is None:  
                img = Image.open(image_file).convert(&quot;L&quot;)  
                img.save(os.path.join(output_dir, f&apos;{case}_0000.png&apos;))  

  
            if is_label:  
                label_image = Image.open(label_file)  
                label_array = np.array(label_image)  
                label_array[label_array == 255] = 1  
                converted_label_image = Image.fromarray(label_array)  
                converted_label_image.save(os.path.join(output_dir, f&apos;{case}.png&apos;))  
  

def main():  
    # base_dir = &apos;/Path/to/your/dataset/crop_train&apos;  
    base_dir = &apos;/Path/to/your/dataset/crop_train&apos;  
    # test_dir = &apos;/Path/to/your/dataset/crop_test&apos;  
    test_dir = &apos;/Path/to/your/dataset/crop_test&apos;  
    # nnunet_raw_dir = &apos;/Path/to/nnUNet_raw/Dataset001_****&apos;  
    nnunet_raw_dir = &apos;/Path/to/nnUNet_raw/Dataset001_****&apos;  
    imagesTr_dir = os.path.join(nnunet_raw_dir, &apos;imagesTr&apos;)  
    labelsTr_dir = os.path.join(nnunet_raw_dir, &apos;labelsTr&apos;)  
    imagesTs_dir = os.path.join(nnunet_raw_dir, &apos;imagesTs&apos;)  
  
    maybe_mkdir_p(imagesTr_dir)  
    maybe_mkdir_p(labelsTr_dir)  
    maybe_mkdir_p(imagesTs_dir)  
  
    organize_dataset(base_dir, imagesTr_dir)  
    organize_dataset(base_dir, labelsTr_dir, is_label=True)  
    organize_dataset(test_dir, imagesTs_dir)  
if __name__ == &apos;__main__&apos;:  
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得注意的是，这里面还有一些细节。一开始我们的图片都是有色的，意味着我们的图片每个像素都有三个参数RGB，在nnUNet中就表示多通道，但是我训练的时候是按单通道来训练，所以我就将这个图片转化为黑白的。&lt;/p&gt;
&lt;p&gt;| 原图                     | 处理后                     |
| ---------------------- | ----------------------- |
| &lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot;&gt; | &lt;img src=&quot;0_0000.png&quot; alt=&quot;alt text&quot;&gt; |
|                        |                         |&lt;/p&gt;
&lt;p&gt;其次label的图像也要处理，我的数据集中，label是黑白的，意味着图片的数组就是[0,255],0表示黑色，255表示白色。在nnUNet中，0被强制表示为背景，也就是背景必须为黑，然后其他的参数必须要连续，所以我的图片的数组就必须是[0,1]，所以这个脚本将图片中的255全都改成0。反映出来的结构就是，每一个处理后的图片都几乎是全黑的，因为0，1这两个灰度很接近，所以整张图几乎就是全黑的。但是没有关系，我们人眼分辨出来计算机可以，这满足了nnUNet的格式。到最后我们只用再把1改成255就可以分辨了。&lt;/p&gt;
&lt;p&gt;| 原图                       | 处理后                  |
| ------------------------ | -------------------- |
| &lt;img src=&quot;label.png&quot; alt=&quot;alt text&quot;&gt; | &lt;img src=&quot;3.png&quot; alt=&quot;alt text&quot;&gt; |&lt;/p&gt;
&lt;p&gt;分类完这些图片之后，就是data.json文件,这个文件的作用就是告诉模型一些元数据。&lt;/p&gt;
&lt;p&gt;元数据（Metadata）就是描述数据的一组信息，它提供了关于数据本身的背景、结构、属性等为模型训练提供了“关于数据的指南”，使得整个训练流程——从预处理、数据增强、网络架构设计到最终的推理和后处理——都能基于具体数据集的特点自动调整，从而提高模型的适用性和性能。&lt;/p&gt;
&lt;p&gt;以下是我的json文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{  
  &quot;channel_names&quot;: {  
    &quot;0&quot;: &quot;CT&quot;  
  },  
  &quot;labels&quot;: {  
    &quot;background&quot;: 0,  
    &quot;lesion&quot;: 1  
  },  
  &quot;numTraining&quot;: 14,  
  &quot;file_ending&quot;: &quot;.png&quot;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;channel：就代表输入有几个通道，我只有一个就只有0&lt;/li&gt;
&lt;li&gt;labels：就代表label的结构，0表示背景，1表示白色的部分&lt;/li&gt;
&lt;li&gt;numTraining：表示训练图像有多少个&lt;/li&gt;
&lt;li&gt;file_ending：表示图片的格式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将json文件放到指定位置之后数据处理部分就完成&lt;/p&gt;
&lt;h2&gt;预训练&lt;/h2&gt;
&lt;p&gt;预训练通过在通用数据上建立良好的基础，使得在面对具体任务时，模型能够更快、更高效地学习并达到较好的表现。&lt;/p&gt;
&lt;p&gt;运行预处理的最简单方式为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nnUNetv2_plan_and_preprocess -d DATASET_ID --verify_dataset_integrity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 DATASET_ID 即数据集的编号。我们建议首次运行时总是加上 &lt;code&gt;--verify_dataset_integrity&lt;/code&gt; 选项，以检查一些最常见的错误来源！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DATASET_ID：就是一开数据命名的，要是Dataset001，就填1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一个过程很快，结束后就再 nnUNet_preprocessed中看到结果&lt;/p&gt;
&lt;h2&gt;训练&lt;/h2&gt;
&lt;p&gt;这一步开始我们就正式训练，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nnUNetv2_train DATASET_NAME_OR_ID UNET_CONFIGURATION FOLD [其他选项, 详见 -h]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;DATASET_NAME_OR_ID：就是数据集编号，1，2，3这些&lt;/li&gt;
&lt;li&gt;UNET_CONFIGURATION ：是标识所请求 U-Net 配置的字符串（默认包括 2d、3d_fullres、3d_lowres、3d_cascade_lowres）我训练的2d的我就选2d&lt;/li&gt;
&lt;li&gt;FOLD：选第几个fold当作训练时的验证集，默认他总共有5折&lt;/li&gt;
&lt;li&gt;-device （cpu,gpu,mps）:这里就是训练的设备，mac m1，2可以用mps&lt;/li&gt;
&lt;li&gt;-tr nUNetTrainer_XXepochs：这里就是总共训练几轮，XX是轮数，默认是1000，他可以有1，10，20，50，这些参数，可以去/nnUNet/nnunetv2/training/nnUNetTrainer/variants/training_length/nnUNetTrainer_Xepochs.py里面看有什么选项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些还挺重要的，别像我一开始mac用cpu加速，然后epochs默认1000次，算了一下大概要3个月，才能训完一个只有50张图片的模型，然后我的电脑还嘎嘎烫。后来我用了mps以及50 epochs，大概要5个小时。&lt;/p&gt;
&lt;p&gt;训练过程中他会有一些反馈，告诉你当前是第几轮了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-08%2013.03.00.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;比如图中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Epoch：就是第几轮的意思，我这是47轮&lt;/li&gt;
&lt;li&gt;current learning rate：就是当前的&lt;strong&gt;学习率&lt;/strong&gt;，一般来说刚开始训练的时候，这个会比较大，简单来说就是一开始像梯度下降的地方快速前进，随着训练到后面，你越接近极小值，你的步伐应该越来越小，否则会出现接近极小值的那部分仍然有很多的抖动。&lt;/li&gt;
&lt;li&gt;train_loss：这是 &lt;strong&gt;训练集上的损失（loss）&lt;/strong&gt;，表示模型在训练数据上的误差大小。&lt;/li&gt;
&lt;li&gt;val_loss：这是 &lt;strong&gt;验证集上的损失&lt;/strong&gt;，衡量模型在未见过的数据上的表现。&lt;/li&gt;
&lt;li&gt;Pseudo Dice：是 nnU-Net 计算的 Dice 分数,这具体含义我还不了解&lt;/li&gt;
&lt;li&gt;EMA： &lt;strong&gt;指数滑动平均（EMA, Exponential Moving Average）&lt;/strong&gt; Dice 分数的历史最佳值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当你训练完成之后就会是这样，这里我用fold 0 当作验证集，意思就是我50个数据被分为5 个fold
用其中的1个当作验证集，其他4个当作训练集。正常来说，我们应该要&lt;strong&gt;循环训练所有 fold&lt;/strong&gt;（通常 5 折交叉验证）意思就是让每一个fold当作一次验证集，最后平均这些结果。但是我的电脑训练一次都已经费劲了，所以我这里只选择了一个fold。后面的数据就是用验证集检验的结果，最后就是validation complets，以及&lt;strong&gt;Mean Validation Dice&lt;/strong&gt;（平均 Dice 系数）= &lt;strong&gt;0.8252&lt;/strong&gt;，另一个数据集是0.7089
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-03-08%2013.12.23.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;预测&lt;/h2&gt;
&lt;p&gt;训练完成之后就让模型处理一些图片&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nnUNetv2_predict -i INPUT_FOLDER -o OUTPUT_FOLDER -d DATASET_NAME_OR_ID -c CONFIGURATION --save_probabilities
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;INPUT_FOLDER： 输入文件路径&lt;/li&gt;
&lt;li&gt;OUTPUT_FOLDER：输出文件路径&lt;/li&gt;
&lt;li&gt;DATASET_NAME_OR_ID：跟之前一样，数据集名字&lt;/li&gt;
&lt;li&gt;CONFIGURATION：2d，3d还是别的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：我只用了fold 0作为数据集，但nnUNet的预测会想要用5 fold的数据来预测，所以-f 0 就是让他使用fold 0 来预测
-device mps 就是用mac的架构来预测，默认是cuda&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nnUNetv2_predict -i ./nnUNet_raw/Dataset002_chasedb1/imagesTs -o ./nnUNet_raw/Dataset002_chasedb1/output -d 2 -c 2d --save_probabilities -f 0 -device mps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行完之后就可以在对应文件夹中看到分割出来的图片。但注意，输出的图像还是[0,1]的，输出的就是全黑的图片，后面再用脚本将它转化回来。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os  
import numpy as np  
from PIL import Image  
  
# 设置你的文件夹路径  
folder = &quot;Path/to/your/result&quot;  
  
for filename in os.listdir(folder):  
    if filename.lower().endswith(&apos;.png&apos;):  
        filepath = os.path.join(folder, filename)  
        img = Image.open(filepath)  
        # 如果图片不是RGB模式，可先转换：img = img.convert(&quot;RGB&quot;)  
        img_array = np.array(img)  
        # 将所有像素值为1的元素替换为255  
        img_array[img_array == 1] = 255  
        # 转换回图片对象并保存覆盖原文件  
        new_img = Image.fromarray(img_array)  
        new_img.save(filepath)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以看到结果&lt;/p&gt;
&lt;p&gt;| 预测图像                      | 分割图像               |
| ------------------------- | ------------------ |
| &lt;img src=&quot;0_0000_1.png&quot; alt=&quot;alt text&quot;&gt; | &lt;img src=&quot;0.png&quot; alt=&quot;alt text&quot;&gt; |&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./nnUNet.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;感觉做这个也不容易，我的所有核都跑满了，训练结果不能说很好吧，但是受限于设备这些，我也不可能一直用我的电脑训练，想要更好的结果那就epochs再多点，5个fold全都跑完，在cuda上跑，应该会有不错的效果吧。&lt;/p&gt;</content:encoded><img src="/_astro/nnUNet.DR4lBins.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/nnUNet.DR4lBins.png" length="0" type="image/png"/></item><item><title>Lecture5 Neural Networks</title><link>https://laurie-hxf.xyz/blog/deeplearning-l5</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/deeplearning-l5</guid><description>UMich EECS 498-007 Deep learning-Neural Networks</description><pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-02-19 20.14.14.BvXzr8O6.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;Problem: Linear Classifiers aren’t that powerful&lt;/h2&gt;
&lt;h3&gt;#1 Feature Transforms&lt;/h3&gt;
&lt;p&gt;我们之前讲过linear classifier是线性的，所以他不能识别一些非线性的图案,但是我们可以通过一些方法将我们要识别的数据转化成线性的，这样我们就可以利用Linear Classifiers来识别
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-09%2015.51.34.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;#2 Color Histogram&lt;/h3&gt;
&lt;p&gt;我们将图片转化为颜色直方图，这样可以忽略物体在照片中的空间位置，根据颜色来识别物体，把图片中的颜色转化为向量然后训练。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-09%2015.57.34.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;#3 Histogram of Oriented Gradients (HoG)&lt;/h3&gt;
&lt;p&gt;HoG的核心思想是&lt;strong&gt;通过捕捉图像中物体轮廓和边缘的形状信息&lt;/strong&gt;来提取特征。图像的梯度方向和幅度可以反映出物体的边缘、纹理等结构信息，这些信息对物体的识别和分类非常重要。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-09%2015.59.11.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;#4 Bag of Words (Data-Driven!)&lt;/h3&gt;
&lt;p&gt;我们冲数据集中每个图片提取一些块，然后这些块组成一个 codebook，然后我们就可以将图片表示为这个图片有多少个codebook中这一个块的个数，以此类推
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-09%2016.13.30.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Neural Networks&lt;/h2&gt;
&lt;p&gt;上面说的这些都是图片的某些特征，我们不只用单独的特征来识别图片，我们用多个特征组合起来形成一个长特征向量来表示一张图片
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-09%2016.17.44.png&quot; alt=&quot;alt text&quot;&gt;
以前的想法就是将一个系统分为两部分，一部分就是特征的提取，一部分就是训练部分
神经网络的动机就是最大化提高图像分类的能力，最大的区别就是他用一整个系统共同来调整这两部分&lt;/p&gt;
&lt;p&gt;现在就看一下神经网络的简单例子&lt;/p&gt;
&lt;p&gt;$$
Linear\ Classifiers:f = Wx+b
$$
$$2
\ layer\ Neural\ Networks:f = W_2max(0,W_1x+b_1)+b_2
$$&lt;/p&gt;
&lt;h3&gt;Fully-connected neural network&lt;/h3&gt;
&lt;p&gt;由于x中的每个元素都会对h中的每个元素造成影响，h中的也会对s造成影响，神经网络的每一层都是相互连接的，所以将这种神经网络称为Fully-connected neural network，也叫多层感知机(MLP)
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-09%2016.41.24.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;max那部分被称为&lt;strong&gt;激活函数&lt;/strong&gt;，如果我们没有那部分，我们的函数变为$s=W_2W_1x$ 这时他仍然是一个Linear Classifiers，所以我们要在两个矩阵之间加一个非线性的函数。当然这种激活函数可以有很多种，不只是max这种，但max是用的最广泛的激活函数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-19%2020.52.15.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;激活函数最重要的作用就是将分类可以变的不再线性，比如原本没有激活函数的时候
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-09%2022.39.21.png&quot; alt=&quot;alt text&quot;&gt;
然后通过激活函数之后，B,C,D中的数据全部被投影到坐标轴中
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-19%2019.48.00.png&quot; alt=&quot;alt text&quot;&gt;
于是再通过线性分类(也就是第二个W)就可以将这些区分开来，从而这种做法就可以达到非线性的区分。这里的神经网络就是2层
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-19%2019.49.29.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;神经网络的由来&lt;/h3&gt;
&lt;p&gt;就因为这种结构启发与神经元，所以叫神经网络。又图中每一个hidden layer其实相当于每一层的权重矩阵以及激活函数，偏移量。中间的一个个圆圈在神经网络中也被称为神经元（Neuron），“节点”（Node）或“单元”（Unit），他本质上就是权重矩阵中的一行，偏移量中的一个数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-19%2020.14.14.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Regularize&lt;/h3&gt;
&lt;p&gt;前面我们说过正则化的目的之一是防止过拟合，在神经网络中我们可以看到层数越多他模拟的就越精细，他也就越容易过拟合。但是这时我们不选择减少层数来实现正则化，而是添加偏移量。从而使我们的决策边界变得平滑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-19%2020.13.08.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Universal Approximation(万能逼近定理)&lt;/h3&gt;
&lt;p&gt;这里想说的就是，适当的人工神经网络能够逼近任何连续函数，只要该网络具有足够的宽度（即足够多的神经元）和合适的激活函数。&lt;/p&gt;
&lt;p&gt;比如我们的ReLU激活函数，每四个神经元他就可以帮助形成 其中一种bump function。bump function具有强烈的局部性和光滑性，通常用来进行局部分析或构造光滑函数的近似。
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-19%2020.53.18.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;当我们把多个bump function连接在一起的时候他就可以模拟非线性的函数，这就是神经网络在理论上具备了近似任何函数的能力的原因&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-19%2020.56.33.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是这只是理论上，实际上Universal approximation 没有告诉我们，我们是否可以通过SGD来学习到任何函数以及我们需要多大的数据来训练一个函数。&lt;/p&gt;
&lt;p&gt;所以这些局限性促使了很多新的研究方向，包括更高效的训练算法、优化技术、正则化方法、以及小样本学习等&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-02-19 20.14.14.BvXzr8O6.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-02-19 20.14.14.BvXzr8O6.png" length="0" type="image/png"/></item><item><title>The details about this blog</title><link>https://laurie-hxf.xyz/blog/blog-detail</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/blog-detail</guid><description>The details about this blog</description><pubDate>Sat, 15 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/blog.CHf-bVlS.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;动机&lt;/h2&gt;
&lt;p&gt;一开始我的博客是github pages中的&lt;a href=&quot;https://github.com/alshedivat/al-folio&quot;&gt;al-folio&lt;/a&gt;主题，用了一个学期之后发现之前的这个配置维护很麻烦，每次部署文章的时候都要等半天才可以部署好。所以就有了迁移博客的念头，后来就发现现在这个更好看的这个&lt;a href=&quot;https://github.com/cworld1/astro-theme-pure&quot;&gt;主题&lt;/a&gt;。探索了一下，这主题功能又多又好玩，耐看，部署简单，还可以实时编辑，所以就选这个主题。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;h3&gt;克隆&lt;/h3&gt;
&lt;p&gt;一开始的话克隆这个仓库，在命令行输入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git clone https://github.com/cworld1/astro-theme-pure.git
cd astro-theme-pure
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我这里选择的是vercel来部署我的网站，这时建议fork这个原仓库，然后再克隆到本地，在vercel中就直接导入对应github仓库，接着就按照vercel的指示就可以。数据库我暂时还在用&lt;a href=&quot;https://console.leancloud.app/apps&quot;&gt;leancloud&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;然后mac用户就要下载一下bun&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl -fsSL https://bun.sh/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然homebrew也可以，不过有点慢&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;brew install bun
&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-shell&quot;&gt;bun install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完之后就开始运行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;bun dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时他就应该弹出local network
然后在浏览器打开连接就可以实时看自己的博客&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;h3&gt;基本&lt;/h3&gt;
&lt;p&gt;在/src/site.config.ts中可以配置基本的信息，包括头像，网站名字，favicon这些在注释里都有提到，包括备案信息，github账号这些。然后在这里管理子页面，可以省去一些不要的子页面。&lt;/p&gt;
&lt;h3&gt;主页&lt;/h3&gt;
&lt;p&gt;这个在/src/pages/index.astro文件里面更改，可以添加一些自己的功能。&lt;/p&gt;
&lt;h3&gt;About&lt;/h3&gt;
&lt;p&gt;这个在/src/pages/about/index.astro文件里面更改，最好玩的就是这个tool，不过这里的图片都是svg的图片，自己对应的工具要在网上找然后转化为svg图片。有一些免费的网站，比如这个&lt;a href=&quot;https://www.vectorizer.io&quot;&gt;网站&lt;/a&gt;就挺好用的，有一定的免费额度，但是不多。或者直接找某些软件的svg图片。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-15%2000.03.29.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Projects&lt;/h3&gt;
&lt;p&gt;这里就在/src/pages/projects/index.astro里面更改，添加自己的仓库。这个界面会展示你的GitHub贡献图，有的时候连接不上github他就会一直报错。这时候只用把那段代码暂时先注释掉该别的先。&lt;/p&gt;
&lt;h3&gt;links&lt;/h3&gt;
&lt;p&gt;这个就在/src/pages/index/index.astro里面更改&lt;/p&gt;
&lt;h3&gt;Blog&lt;/h3&gt;
&lt;p&gt;写博客在/src/content/blog里面添加文件夹，然后把图片放到文件夹里面。仿照现有的blog模仿就行。如果习惯在obsidian中写博客，可能格式不能兼容，可以看看我写的&lt;a href=&quot;https://github.com/laurie-hxf/obsidian-convert-format&quot;&gt;工具&lt;/a&gt;来转化格式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;值得注意的是，除了blog里的图片放到对应文件夹里面，其他额外的图片都要放到/public这个文件夹里面。&lt;/strong&gt;&lt;/p&gt;</content:encoded><img src="/_astro/blog.CHf-bVlS.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/blog.CHf-bVlS.png" length="0" type="image/png"/></item><item><title>Lecture4 Optimization</title><link>https://laurie-hxf.xyz/blog/deeplearing-l4</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/deeplearing-l4</guid><description>UMich EECS 498-007 Deep learning-Optimization</description><pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-02-06 18.32.26.CsW5A5bE.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;事实上，损失函数只是告诉你当前的W造成的损失是多少，判断这个W到底好不好，但是没有告诉你怎么找这个W。&lt;/p&gt;
&lt;p&gt;所以这时候就用到&lt;strong&gt;optimization&lt;/strong&gt;
$w^*$就是最优的w，$\arg \min_w​$表示对于 $w$ 的所有可能取值，找到使得 $L(w)$ 最小化的 $w$&lt;/p&gt;
&lt;p&gt;$$w^* = \arg \min_w L(w)$$&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;&lt;strong&gt;optimization&lt;/strong&gt; method&lt;/h2&gt;
&lt;h3&gt;random search&lt;/h3&gt;
&lt;p&gt;随便选W&lt;/p&gt;
&lt;h3&gt;follow the slope&lt;/h3&gt;
&lt;p&gt;找梯度，沿着梯度下降的地方&lt;/p&gt;
&lt;h5&gt;Numeric gradient(数值梯度)&lt;/h5&gt;
&lt;p&gt;一种是像这样对W的每一个元素都增加一个微小量，保持其他元素不变，然后计算斜率。
但是这种办法非常的慢对每个元素都要，实际中W可以非常大
同时这种做法只能得到近似，因为我们用的有限的差值来计算&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-06%2015.45.32.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h5&gt;Analytic gradient(解析梯度)&lt;/h5&gt;
&lt;p&gt;更有效的办法就是直接用损失函数来找到他对应的梯度
通过莱布尼茨，牛顿的那些数学办法找到梯度
具体实现中我们用的是反向传播算法&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;Gradient Descent(梯度下降)&lt;/h4&gt;
&lt;h5&gt;Vanilla gradient descent&lt;/h5&gt;
&lt;p&gt;$$损失函数：L(W) = \frac{1}{N} \sum_{i=1}^{N} L_i(x_i, y_i, W) + \lambda R(W)$$&lt;/p&gt;
&lt;p&gt;$$损失函数梯度：\nabla_W L(W) = \frac{1}{N} \sum_{i=1}^{N} \nabla_W L_i(x_i, y_i, W) + \lambda \nabla_W R(W)$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Vanilla gradient decsent
w = initialize_weights()   #初始化一个w
for t in range(num_steps): #进行num_steps之后结束
  dw = compute_gradient(loss_fn,data,w) #计算dw
  w -= learing_rate * dw   #每次通过dw对w进行调整，learing rate决定调整速度
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hyperparameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Weight initialization method&lt;/li&gt;
&lt;li&gt;Number of steps&lt;/li&gt;
&lt;li&gt;Learning rate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-06%2017.04.37.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h5&gt;Batch gradient descent--&gt;Stochastic Gradient Descent（SGD）&lt;/h5&gt;
&lt;p&gt;对于Vanilla gradient descent而言，我们需要每次对样本集中的每个样本进行计算，从而得到dw，然而当样本集数量很大的时候，计算的速度就会比较慢。于是，就有Stochastic Gradient Descent&lt;/p&gt;
&lt;p&gt;$$L(W) = \mathbb{E}&lt;em&gt;{(x, y) \sim p&lt;/em&gt;{\text{data}}} \left[ L(x, y, W)\right] + \lambda R(W) \approx \frac{1}{N} \sum_{i=1}^{N} L_i(x_i, y_i, W) + \lambda R(W)$$&lt;/p&gt;
&lt;p&gt;$$\nabla_W L(W) = \nabla_W \mathbb{E}&lt;em&gt;{(x, y) \sim p&lt;/em&gt;{\text{data}}} \left[ L(x, y, W) \right] + \lambda \nabla_W R(W) \approx \frac{1}{N} \sum_{i=1}^{N} \nabla_W L_i(x_i, y_i, W) + \lambda \nabla_W R(W)$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Stochastic Gradient Descent
w = initialize_weights()   #初始化一个w
for t in range(num_steps): #进行num_steps之后结束
  minibatch = sample_data(data,batch_size)  #选取一部份的样本
  dw = compute_gradient(loss_fn,minidata,w) #根据这一小部份样本计算dw
  w -= learing_rate * dw   #每次通过dw对w进行调整，learing rate决定调整速度
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hyperparameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Weight initialization method&lt;/li&gt;
&lt;li&gt;Number of steps&lt;/li&gt;
&lt;li&gt;Learning rate&lt;/li&gt;
&lt;li&gt;Batch size&lt;/li&gt;
&lt;li&gt;Data sampling&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h5&gt;Problems&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;如果我们调整的步子太大，我们可能会得到锯齿状的路线，如果过小，可能w收敛的速度会比较慢&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;%E6%88%AA%E5%B1%8F2025-02-06%2017.47.35.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们会掉入到局部极小值以及鞍点中，而不是全局最小值&lt;/li&gt;
&lt;li&gt;对于SGD而言他比较容易受到噪声干扰，因为他只是选取一部份的样本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;%E6%88%AA%E5%B1%8F2025-02-06%2017.54.29.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;SGD+Momentum&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;v = 0
for t in range(num_steps):
  dw = compute_gradient(w)
  v = rho * v + dw       #他引入了历史的梯度影响，而不只是受到当前梯度的影响
  w -= learing_rate * v 
&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;/li&gt;
&lt;li&gt;由于受到历史梯度的影响，他就不再会过于敏感与噪声&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-06%2018.32.26.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;这种想法就类似于下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%88%AA%E5%B1%8F2025-02-06%2018.37.17.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;AdaGrad &amp;#x26; RMSProp&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;grad_squared = 0
for t in range(num_steps):
  dw = compute_gradient(w)
  grad_squared += dw * dw
  w -= learing_rate * dw / (grad_squared.sqrt() + 1e-7)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;每个参数都有一个独立的学习率（learing_rate），这个学习率是通过该参数的梯度历史自动调整的。具体来说，&lt;strong&gt;历史上梯度较大的参数会有较小的学习率&lt;/strong&gt;，而&lt;strong&gt;梯度较小的参数会有较大的学习率&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;这种调整机制确保了在训练过程中，参数更新较多的方向会逐渐减小学习率，避免过度更新，而较少更新的参数会有较大的学习率，鼓励它们继续更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AdaGrad和前面的SGD最大的不同就是他的learing_rate会根据当前的梯度大小来调整，而SGD的learing_rate是不变的。&lt;/p&gt;
&lt;p&gt;但是这个方法也有问题就是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;学习率过早衰减&lt;/strong&gt;：由于 AdaGrad 是基于梯度的平方累积来调整学习率的，这意味着随着训练的进行，&lt;strong&gt;学习率会单调递减&lt;/strong&gt;，最终可能导致学习率变得非常小，从而停止更新。特别是在训练的后期，这可能会影响模型的收敛性，导致训练停滞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有长时间有效的学习率&lt;/strong&gt;：AdaGrad 的自适应机制通常会导致学习率在训练过程中迅速下降，尤其是在处理高频特征时，这可能会导致无法进一步优化模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以就有RMSProp&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;grad_squared = 0
for t in range(num_steps):
  dw = compute_gradient(w)
  grad_squared += decay_rate * grad_squared + (1 - decay_rate) * dw * dw
  w -= learing_rate * dw / (grad_squared.sqrt() + 1e-7)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种办法可以避免grad_squared在不断的增大，导致学习率的衰减过早
所以可以看到这种办法可以避免SGD+Momentum的过度更新&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-07%2013.26.18.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;Adam：RMSProp+Momentum&lt;/h5&gt;
&lt;p&gt;这种算法就将上面的两种算法结合&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;moment1 = 0
moment2 = 0
for t in range(1,num_steps + 1):
  dw = compute_gradient(w)
  moment1 = beta1 * moment1 + (1 - beta1) * dw
  moment2 = beta2 * moment2 + (1 - beta2) * dw * dw
  w -= learning_rate * moment1 / (moment2.sqrt() + 1e-7)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| | |
|-------|-------|
| &lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-07%2013.33.47.png&quot; alt=&quot;alt text&quot;&gt; | &lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-07%2013.34.12.png&quot; alt=&quot;alt text&quot;&gt; |&lt;/p&gt;
&lt;p&gt;但是这个算法又有个问题就是如果一开始 t = 0，然后beta2=0.999/一个趋近于1的数，那么在一开始的时候，learing_rate就会变得非常大，意味着我们在一开始就迈一个很大的步伐&lt;/p&gt;
&lt;p&gt;所以改进版就是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;moment1 = 0
moment2 = 0
for t in range(1,num_steps + 1):
  dw = compute_gradient(w)
  moment1 = beta1 * moment1 + (1 - beta1) * dw
  moment2 = beta2 * moment2 + (1 - beta2) * dw * dw
  moment1_unbias = moment1 / (1 - beta1 ** t)
  moment2_unbias = moment2 / (1 - beta2 ** t)
  w -= learning_rate * moment1_unbias / (moment2_unbias.sqrt() + 1e-7)
&lt;/code&gt;&lt;/pre&gt;</content:encoded><img src="/_astro/截屏2025-02-06 18.32.26.CsW5A5bE.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-02-06 18.32.26.CsW5A5bE.png" length="0" type="image/png"/></item><item><title>Lecture3 Linear Classifiers</title><link>https://laurie-hxf.xyz/blog/deeplearing-l3</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/deeplearing-l3</guid><description>UMich EECS 498-007 Deep learning-Linear Classifiers</description><pubDate>Wed, 05 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-01-23 22.33.47.CMVBD3ae.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;h2&gt;Parametric Approach(参数法)&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-23%2022.23.41.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;$$f(x,W)=Wx+b$$
这一个的方法就是先根据照片的长宽像素以及RGB 3 bit生成一个一维向量，如图就是$32\times32\times3=3072$ 的一维向量。然后去乘一个矩阵，这个矩阵的大小是一维向量的大小和想要区分的标签的大小，比如图中就是总共有10个我们想要进行分类的标签，所以矩阵的大小就是$10\times3072$ 这样的话我们乘出来的结果就是一个$10\times1$的矩阵，这就代表这张图片在这10中标签中的分数情况。我们还可能加一个偏移量矩阵b，来对结果进行调整。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-23%2022.33.47.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Algebraic Viewpoint(代数角度理解)&lt;/h3&gt;
&lt;h4&gt;Bias Trick&lt;/h4&gt;
&lt;p&gt;我们可以将b这个偏差合并到W矩阵中，并且在x中多加一个1，得到的结果不变&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-23%2022.40.30.png&quot; alt=&quot;alt text&quot;&gt;
这种做法从代数角度会很有帮助
转化后：
$$f(x′,W′)=W′⋅x′$$
偏置 b 现在就是扩展权重的一部分，代数形式统一为点积运算。&lt;/p&gt;
&lt;p&gt;每一个像素乘0.5&lt;/p&gt;
&lt;h3&gt;Visual Viewpoint&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-23%2023.08.18.png&quot; alt=&quot;alt text&quot;&gt;
我们不再将图片拆成一维向量，而是将矩阵W拆成图片的形状，这样子可以直接得出每一个标签相应的分数。这种做法有点像拿着不同标签的模版来比对（template matching），每一个标签都有一个模版，根据模版的矩阵内积来评价这张图片的分数&lt;/p&gt;
&lt;p&gt;但是从这样的角度我们就可以看到Linear Classifiers的局限性，比如我们要识别一张在森林里面的图片，更有可能的是Linear Classifiers会很大可能将这个识别为deer因为同样背景都有很多绿色，然后car因为中间都差不多有车的模样。以及他特别依赖训练集，比如我们可以看到我们用很多红色的车来训练，所以他的模版就是红色的。但是这样一来当我们有一辆绿色的车之类的他就识别不出来。&lt;/p&gt;
&lt;p&gt;还有就是就是A single template cannot capture multiple modes of the data，比如上图中的马看上去有两个头，这是因为当我们训练的时候，有朝向左边的马也有朝向右边的马，这些图片最终会合成一个模版（因为每一个标签只有一个模版），所以我们用一个模版来表示的话就会造成这种两头马。本质上来说就是Linear Classifiers试图用一张图片来涵盖训练集中所有的特征。&lt;/p&gt;
&lt;h3&gt;Geometric Viewpoint&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-30%2020.01.42.png&quot; alt=&quot;alt text&quot;&gt;
这个角度就是先从图上抽两个像素点，他们的值作为x，y轴，当其他像素点的值保持不动的时候改变他们的值，然后在三维空间中形成一个面，z轴就是对应的分数。&lt;/p&gt;
&lt;p&gt;W矩阵中的每一行就对应了一个面，也就对应一个模版，将整个图片和这一行相乘就得到这张图片对应的这个模版的分数。那么就是说W中有多少行（想要识别的标签），他就有多少个平面。一个好的W就应该一个图片在相关的标签的分数应该要比别的模版高。&lt;/p&gt;
&lt;p&gt;然后图中的这些线就是面与x，y平面的交线，表示对应的分数为0，然后数学上来看的话就是垂直这条线的话，分数就会增长，前面讲的模版的话就是这个正交线，~~但是分数不一定沿着图中的箭头方向增长吧。有可能延相反方向增长？起点也不一定在原点？~~&lt;/p&gt;
&lt;p&gt;然后如果我们将2个像素拓展到整个图像，那应该就是超维的一个平面。从这个观点看，超平面就会将空间切开。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-30%2020.11.38.png&quot; alt=&quot;alt text&quot;&gt;
这种方法就会暴露一个问题，linear classifier不能将这些图像通过一个线性的超平面将这些颜色分开来。比如说图中的蓝色部分经过蓝色模版的计算他的分数要比红色的要高，那么就意味着蓝色平面在蓝色部分要比红色平面要高，但在三维中无论平面怎么排列，都可能模拟出下图，因为这只是线性的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-30%2020.23.40.png&quot; alt=&quot;alt text&quot;&gt;
这就是一开始感知机不行的原因，他一开始就是Linear Classifiers，他就连XOR都不能识别&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-30%2020.31.51.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Choosing a good W&lt;/h2&gt;
&lt;p&gt;现在我们的目的就是怎么找到一个合适的W，方法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use a loss function to quantify how good a value of W is&lt;/li&gt;
&lt;li&gt;Find a W that minimizes the loss function(optimization)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Loss Function(损失函数)&lt;/h3&gt;
&lt;p&gt;A loss function tells how good our current classifier is
Low loss = good classifier
High loss = bad classifier&lt;/p&gt;
&lt;p&gt;一个数据集可以表示成这样：x表示一张图片，y表示对应标签的index
$$ {(x_i,y_i)}^N_{i=1} $$&lt;/p&gt;
&lt;p&gt;那么一个损失函数通常表示成这样，f表示对于x他经过和W运算之后得到的分数
$$L_i(f(x_i,W),y_i)$$&lt;/p&gt;
&lt;p&gt;平均之后就是
$$L=\frac{1}{N} \sum_i L_i(f(x_i,W),y_i)$$&lt;/p&gt;
&lt;h4&gt;Multiclass SVM Loss（多类别 SVM 损失）&lt;/h4&gt;
&lt;p&gt;属于一种损失函数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-31%2022.42.39.png&quot; alt=&quot;alt text&quot;&gt;
根据每一个图片用W算出他的分数，如果他正确的标签得分最高，那么他的损失就是0，反之就是算差值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-31%2022.41.56.png&quot; alt=&quot;alt text&quot;&gt;
假如我们得出了一个W，让他的L为0，意味着这个W可以很好的区分对应的标签，那这个W是否是唯一的呢？很明显不是，因为2W也是0，那么这时候我们就要有一个机制来判断哪一个W更好呢
这时就用到&lt;strong&gt;正则化&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;Regularization&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-31%2023.09.33.png&quot; alt=&quot;alt text&quot;&gt;
&lt;strong&gt;第二部分&lt;/strong&gt;（右侧）：
λR(W)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是&lt;strong&gt;正则化项（Regularization Term）&lt;/strong&gt;，用于防止模型过拟合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;λ&lt;/strong&gt; 是一个&lt;strong&gt;超参数（hyperparameter）&lt;/strong&gt;，用于控制正则化的强度。较大的 λ 会增加正则化的影响，使模型更简单，较小的 λ 则更关注拟合数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;R(W)&lt;/strong&gt; 是对模型参数 W 施加的限制（如 L1/L2 正则化）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;正则化的目的&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Express preferences in among models beyond &quot;minimize training error&quot;(增加一些偏好比如L1正则化注重某一个参数比重，L2注重所有参数的作用)&lt;/li&gt;
&lt;li&gt;Avoid overfitting: Prefer simple models that generalize better（避免过拟合化）&lt;/li&gt;
&lt;li&gt;Improve optimization by adding curvature&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Cross-Entropy Loss (Multinomial Logistic Regression)（交叉熵损失）&lt;/h4&gt;
&lt;p&gt;这一个办法就是将分数转化成概率&lt;/p&gt;
&lt;p&gt;$$分数：s=f(x_i,W)$$
softmax公式就是将分数进行用e进行指数化，然后再归一，这样可以避免负数的影响&lt;/p&gt;
&lt;p&gt;$$Softmax function 归一化：P(Y=k|X=x_i)=\frac{exp(s_k)}{\sum_jexp(s_j)}$$&lt;/p&gt;
&lt;p&gt;$$计算损失：L_i=-logP(Y=y_i|X=x_i)$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kullback–Leibler（KL）散度&lt;/strong&gt;是一种衡量&lt;strong&gt;两个概率分布之间的差异&lt;/strong&gt;的方法，常用于信息论和机器学习。它可以理解为“当我们用分布 Q 近似真实分布 P 时，Q 造成了多少信息损失”。
KL 散度（DKLDKL​）的计算公式如下：
$$D_{KL}(P∣∣Q)=∑_yP(y)log⁡\frac {P(y)}{Q(y)}$$​
其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;P(y)P(y) 是真实的概率分布（ground truth）。&lt;/li&gt;
&lt;li&gt;Q(y)Q(y) 是我们希望学习的概率分布（模型的预测概率）。&lt;/li&gt;
&lt;li&gt;该公式的核心思想是计算&lt;strong&gt;P 和 Q 之间的相对熵&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-02-01%2018.38.20.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-01-23 22.33.47.CMVBD3ae.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-01-23 22.33.47.CMVBD3ae.png" length="0" type="image/png"/></item><item><title>Lecture2 Image Classifier</title><link>https://laurie-hxf.xyz/blog/deeplearing-l2</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/deeplearing-l2</guid><description>UMich EECS 498-007 Deep learning-Image Classifier</description><pubDate>Mon, 03 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/截屏2025-01-1323.49.21.CJyYnJJB.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;一个机器学习的算法通常包括两部分：
训练部分&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def train(images,labels):
#machine learing
	return model
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预测部分：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def predict(model,test_images):
#Use model to predict labels
	return test_labels
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Nearest Neighbor算法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;train部分只是单纯的记住每个训练过的图片和相应的标签&lt;/li&gt;
&lt;li&gt;predict部分就是将输入的图片和记住的图片通过某些&lt;strong&gt;比较算法&lt;/strong&gt;来比较，找到最相似的图片，输出相应的标签&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那这个&lt;strong&gt;比较函数&lt;/strong&gt;可以是什么&lt;/p&gt;
&lt;h3&gt;L1 distance&lt;/h3&gt;
&lt;p&gt;$$d_1(I_1,I_2)=\sum_p |I_1^p-I_2^p|$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-1323.02.01.png&quot; alt=&quot;alt text&quot;&gt;
这种算法的训练复杂度是O(1)我们只是需要那个训练集就可以，但是测试复杂度是O(n)
&lt;em&gt;这样其实是非常不好的，这和机器学习的预期完全相反，我们希望能花更多的时间来训练，然后更快的时间来完成测试。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;同时这种算法并不能“真正”的理解图片的内容，因为他是根据图片时间的像素差距来判断，所以通常如果两张照片有相同的颜色，形态之类的他都会误以为是同一种，即使他们是不同的生物&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;进阶(KNN)&lt;/h2&gt;
&lt;p&gt;我们将训练集中的每一个图片根据某种特征提取出特征值出来，然后用相应的颜色表示对应的标签，然后放入坐标轴中。我们就得到一张具有离散的不同颜色的点，这些点形成区间。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;具体这些离散的点怎么形成呢，可能是当我们把训练集中的所有图片转化为点放到坐标轴上的时候，他就会通过比较函数计算坐标轴中剩余的点，然后给定他的标签，从而实现离散的点到连续的区间。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-1323.49.21.png&quot; alt=&quot;alt text&quot;&gt;
然后当测试的时候，我们同样将图片转化成点放入图中，直接判断他在那个区间，然后判断出他的标签。&lt;/p&gt;
&lt;p&gt;但是如果我们判断点的标签的时候只依据和他最近点的坐标的话，那这个图将不会有鲁棒性，很容易受到噪声的影响，从而让图变得有锯齿以及孤立的区间（图中绿色里面的黄色）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是KNN的核心就是每个点不依赖单独一个点，而是找到K个最近的点，然后和这些点中大多数的一样标签一样。这样的可以使图像变得平滑（从左图变到右图）&lt;/strong&gt;
&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-1400.04.29.png&quot; alt=&quot;alt text&quot;&gt;
这种做法会造成出现白色区域，因为这些区域的点到其他的区域都差不多近，这时就额外处理，比如遇到这种情况就选最近的就行了。&lt;/p&gt;
&lt;h2&gt;Distance Metric(距离度量)&lt;/h2&gt;
&lt;p&gt;所谓的比较函数，更科学叫做Distance Metric(距离度量)，用于测量两点之间的差异。除了上面提到的,我们还有L2(Euclidean distance)
$$d_2(I_1,I_2)=\sqrt{ \sum_p (I_1^p-I_2^p)^2}$$
有趣的是当我们分别用两种方式来画图的时候，L1的边缘更直角化，而L2则更圆滑。具体的数学原理暂时先留着。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-1400.14.42.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;那该怎么选择距离度量呢？引用chatgpt
&lt;em&gt;&lt;strong&gt;Distance Metric&lt;/strong&gt; 是用来衡量两点（或两对象）间差异的核心工具。选择合适的距离度量对于算法的性能至关重要，通常根据数据的特性和问题的背景来选择：&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连续数据：欧几里得、曼哈顿。&lt;/li&gt;
&lt;li&gt;离散数据：汉明距离。&lt;/li&gt;
&lt;li&gt;高维或方向数据：余弦距离。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Hyperparameters(超参数)&lt;/h2&gt;
&lt;p&gt;所以对于这种算法，我们该选多大的K，该选什么样的距离度量，这些都属于超参数。但是对于每一类问题，他们的超参数是不同的且没有有效的办法来预先知道这些超参数，所以一般都是尝试不同的超参数，然后判断哪种效果好。&lt;/p&gt;
&lt;h3&gt;设置超参数&lt;/h3&gt;
&lt;p&gt;那么我们怎么设置超参数和怎么评价效果的好坏&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-1400.41.31.png&quot; alt=&quot;alt text&quot;&gt;
我们需要做的就是将我们的数据集拆分成三份，一份用来训练，一份用来验证，根据这个来调整我们的超参数，最后一份当我们调整完成我们的算法的时候来测试我们算法的准确性，用完即弃。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%88%AA%E5%B1%8F2025-01-1400.41.43.png&quot; alt=&quot;alt text&quot;&gt;
最好的做法就是将算法拷贝多次，每次将不同的fold作为验证集，来得到不同的超参数，然后选准确率高的。但这种做法一般适用小模型，因为训练的成本会很贵。&lt;/p&gt;</content:encoded><img src="/_astro/截屏2025-01-1323.49.21.CJyYnJJB.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/截屏2025-01-1323.49.21.CJyYnJJB.png" length="0" type="image/png"/></item><item><title>How to use clash on linux </title><link>https://laurie-hxf.xyz/blog/clash-on-linux/clash-on-linux</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/clash-on-linux/clash-on-linux</guid><description>vpn on linux by using clash</description><pubDate>Thu, 07 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/linux.tm54jhbh.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;本博客基于这个&lt;a href=&quot;https://docs.gtk.pw/contents/linux/linux-clash-gui.html#%E4%B8%8B%E8%BD%BD&quot;&gt;博客&lt;/a&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;x86架构 ubuntun24&lt;/p&gt;
&lt;h2&gt;下载&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://app.box.com/s/my3vtk7cxefp7v69vsnzg3kebehqba1x/folder/211969947230&quot;&gt;box&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据电脑的架构下载对应的版本&lt;/p&gt;
&lt;p&gt;但是根据自己的经验似乎这个网址本身就是要vpn才能连接&lt;/p&gt;
&lt;p&gt;linux下好像是无法打开这个网址的&lt;/p&gt;
&lt;p&gt;如果是这样的话，建议用另外一台可以登上这个网址的电脑先下载这个文件，然后将文件传给linux&lt;/p&gt;
&lt;p&gt;这里我们用scp(基于ssh的安全传输方式)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;scp file.txt user@remote_host:/home/user/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;file.txt就是要传输的那个文件&lt;/p&gt;
&lt;p&gt;user@remote_host就是要传输的那台电脑&lt;/p&gt;
&lt;p&gt;对于小白而言，user其实就是主机名，可以打开linux终端，输入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;whoami
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来查看主机名&lt;/p&gt;
&lt;p&gt;对于remote_host就是要传输对象的ip地址&lt;/p&gt;
&lt;p&gt;可以用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ip addr show
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进行查看,一般就在wlo1那里查看，一般长10.xx...之类的就是&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;当你成功传输之后，打开tar文件，图形化界面直接就帮你解压，不行就tar命令&lt;/p&gt;
&lt;p&gt;进入文件中之后就直接点击cfw文件，然后他就有clash的图形化界面&lt;/p&gt;
&lt;p&gt;点击左边&lt;code&gt;profiles&lt;/code&gt;，在搜索栏输入购买流量的url（流量当然是购买来的），然后按个回车就能导入
&lt;img src=&quot;./vpn1.jpg&quot; alt=&quot;alt text&quot;&gt;
订阅成功之后就选中刚刚导入的内容&lt;/p&gt;
&lt;h2&gt;设置系统代理&lt;/h2&gt;
&lt;p&gt;由于 Clash for Windows 的系统代理功能只在 Windows 和 macOS 下生效，所以在 Linux 下需要手动设置系统代理。&lt;/p&gt;
&lt;p&gt;在系统设置中，找到网络设置。
&lt;img src=&quot;./vpn2.jpg&quot; alt=&quot;alt text&quot;&gt;
点开代理&lt;/p&gt;
&lt;p&gt;打开，然后选择手动&lt;/p&gt;
&lt;p&gt;按图中配置&lt;/p&gt;
&lt;p&gt;127.0.0.1是本地主机号，7890是clash选择的端口号
| | |
|-------|-------|
| &lt;img src=&quot;./vpn3.jpg&quot; alt=&quot;alt text&quot;&gt; | &lt;img src=&quot;./vpn4.jpg&quot; alt=&quot;alt text&quot;&gt; |&lt;/p&gt;
&lt;p&gt;自己可以在proxies选择不同的流量&lt;/p&gt;
&lt;p&gt;打开&lt;code&gt;Allow lan&lt;/code&gt;就可以使用&lt;/p&gt;</content:encoded><img src="/_astro/linux.tm54jhbh.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/linux.tm54jhbh.png" length="0" type="image/png"/></item><item><title>How to build NAS </title><link>https://laurie-hxf.xyz/blog/nas/nas</link><guid isPermaLink="true">https://laurie-hxf.xyz/blog/nas/nas</guid><description>build nas by a computer and HDD</description><pubDate>Sat, 19 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;/_astro/nas.BH1NOffE.png&quot; alt=&quot;&quot; style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;p&gt;该博客主要介绍如何利用一台电脑+机械硬盘来搭建一台可以随地访问的nas&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;配置：&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;mac ipad iphone作为接收，以此来随地接收文件
一台win11+ubuntu24的双系统电脑+西部数据4t紫盘机械硬盘+绿联硬盘盒&lt;/p&gt;
&lt;p&gt;机械硬盘在刚买回来的时候要进行初始化，我用的是mac的磁盘工具对他进行“抹掉”
然后为其分配一个文件系统这里我选择exFAT，因为这个格式对不同的操作系统兼容性更强&lt;/p&gt;
&lt;p&gt;一开时用了绿联的硬盘盒将机械硬盘装进去用usb3.0数据线连接到linux电脑上&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Samba&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;然后在linux上要安装Samba（用于文件共享）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update
sudo apt install samba
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用vim编辑Samba配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo vim /etc/samba/smb.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在文件末尾添加一下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;[MyNAS]
path = /path/to/your/harddrive//这里是你机械硬盘在你电脑的位置，可以在图形化界面打开这个机械硬盘然后再在终端中看他的路径，像我的就在/media/laurie/HDD
available = yes
valid users = laurie//这里可以自己填用户名
read only = no
browsable = yes
public = yes
writable = yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后保存退出
然后创建一个Samba用户，会要求你输入一个密码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo smbpasswd -a laurie//这里的用户名填上面配置的用户名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重启Samba服务&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo systemctl restart smbd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;ZeroTier&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;linux&lt;/h3&gt;
&lt;p&gt;然后进行内网穿透的话我使用ZeroTier，原理的话可以自行chatgpt
首先在linux上安装zerotier&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl -s https://install.zerotier.com | sudo bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动zerotier服务&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo systemctl enable zerotier-one
sudo systemctl start zerotier-one
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后访问&lt;a href=&quot;https://www.zerotier.com&quot;&gt;zerotier&lt;/a&gt;创建一个账号，然后创建一个新的虚拟网络
创建完之后会得到一个NEetwork ID
这里用哪一个设备进行创建都可以
然后在linux中加入ZeroTier网络&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo zerotier-cli join &amp;#x3C;Network ID&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入 ZeroTier Central，找到刚刚加入的设备，勾选旁边的复选框，批准该设备加入网络
然后可以在那个界面看到zerotier为你分配的managed ip&lt;/p&gt;
&lt;h3&gt;Mac&lt;/h3&gt;
&lt;p&gt;然后现在要保证你要连接的设备和你的nas在同一个网络中
在mac上用命令行安装ZeroTier（也可以下载ZeroTier app 我是用的命令行）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;brew install zerotier-one
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完后启动zerotier服务&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo zerotier-cli join &amp;#x3C;Network ID&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在zerotier网站上检查是否已经添加成功&lt;/p&gt;
&lt;h3&gt;ipad&amp;#x26;iphone&lt;/h3&gt;
&lt;p&gt;在这两个设备下要下载zerotier app外区的app store有的下，然后点击+号，输入network id来加入网络&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Finder&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;最后就是利用苹果的finder来对硬盘中的文件进行访问&lt;/p&gt;
&lt;h3&gt;MAC&lt;/h3&gt;
&lt;p&gt;command+k然后弹出连接服务器窗口
在搜索栏里填写smb://&amp;#x3C;虚拟ip&gt;/&amp;#x3C;Samba配置文件添加内容的那个头名字，在我的上面的例子中就是MyNas&gt;
然后填写nas的用户名和密码就可以连接&lt;/p&gt;
&lt;h3&gt;Win&lt;/h3&gt;
&lt;p&gt;可以打开文件资源管理器，直接在地址栏中输入以下格式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;\\&amp;#x3C;你的Managed IP&gt;\&amp;#x3C;共享文件夹&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没试过不保证成功率&lt;/p&gt;
&lt;h3&gt;Ipad&amp;#x26;iphone&lt;/h3&gt;
&lt;p&gt;在文件中找到添加服务器
然后也按照这个格式smb://&amp;#x3C;虚拟ip&gt;/&amp;#x3C;Samba配置文件添加内容的那个头名字，在我的上面的例子中就是MyNas&gt;填写&lt;/p&gt;
&lt;p&gt;但是iphone好像会不是很稳定，目前还没找到解决办法&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;shell&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;当然也可以用ssh远程连接&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;shell hostname@虚拟ip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就可以连接&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;至此我们就搭建了nas服务器，最后如果遇到什么问题请问chatgpt他才是世界上最好的老师&lt;/p&gt;</content:encoded><img src="/_astro/nas.BH1NOffE.png" alt="" style="max-width:100%;height:auto;"/><enclosure url="/_astro/nas.BH1NOffE.png" length="0" type="image/png"/></item></channel></rss>