<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds/atom-style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://www.languisher.top/en/</id>
    <title>Retypeset</title>
    <updated>2026-04-11T09:03:37.909Z</updated>
    <generator>Astro-Theme-Retypeset with Feed for Node.js</generator>
    <author>
        <name>languisher</name>
        <uri>https://www.languisher.top/</uri>
    </author>
    <link rel="alternate" href="https://www.languisher.top/en/"/>
    <link rel="self" href="https://www.languisher.top/en/atom.xml"/>
    <subtitle>Retypeset is a static blog theme based on the Astro framework. Inspired by Typography, Retypeset establishes a new visual standard and reimagines the layout of all pages, creating a reading experience reminiscent of paper books, reviving the beauty of typography. Details in every sight, elegance in every space.</subtitle>
    <rights>Copyright © 2026 languisher</rights>
    <entry>
        <title type="html"><![CDATA[Git 基础（一）：Git 对象和引用]]></title>
        <id>https://www.languisher.top/en/posts/git-basics-1/</id>
        <link href="https://www.languisher.top/en/posts/git-basics-1/"/>
        <updated>2026-04-10T11:11:30.472Z</updated>
        <summary type="html"><![CDATA[在软件开发中，一个项目从最初的几行代码，到逐渐演化成复杂系统，整个开发过程本质上是一系列变化的积累。但问题在于，变化本身是很难管理的。我们如...]]></summary>
        <content type="html"><![CDATA[<p>在软件开发中，一个项目从最初的几行代码，到逐渐演化成复杂系统，整个开发过程本质上是一系列变化的积累。</p>
<p>但问题在于，<strong>变化本身是很难管理的。</strong> 我们如何描述项目状态的变化？如果没有工具，我们只能：</p>
<ul>
<li>手动复制文件做备份（例如 <code>project_final_v3_real_final</code>）</li>
<li>靠记忆或零散笔记记录修改原因</li>
<li>在多人协作时反复覆盖彼此的工作</li>
</ul>
<p>这在本质上是混乱的，因为我们没有一个结构化的方式来记录历史。</p>
<p><strong>版本控制系统（Version Control System, VCS）</strong> 正是为了解决这个问题而出现的。它将一个项目的演化过程抽象为一系列“快照”（snapshot）及其之间的关系。</p>
<p>从这个角度看，一个项目的开发过程可以类比为一个<strong>状态机</strong>：</p>
<ul>
<li>每一个快照是一个“状态”，而开发过程就是在这些状态之间不断演进。</li>
<li>每一个快照记录某一时刻项目的完整状态，同时附带元信息（作者、时间、修改说明等），从而让这些变化变得可追踪、可理解、可回溯。</li>
</ul>
<p>本文简单介绍 <strong>Git</strong>，作为现代版本控制系统的事实标准，是如何解决这些问题的。</p>
<h2>Git Repository and Data Types</h2>
<p><strong>Git</strong> 通常和一个文件夹关联，记录这个文件夹在不同时间点的状态。</p>
<p><strong>Git 对象</strong> 是对项目数据的分层建模：</p>
<ul>
<li>blob 表示文件内容</li>
<li>tree 表示目录结构</li>
<li>commit 表示某一时刻整个项目的状态。这里“状态”不仅是指项目的当前文件快照（内容），还包括基于哪一个上一个状态（时间关系），以及做出这些变化的作者、时间、message 等信息。</li>
</ul>
<pre><code>type object = blob | tree | commit
</code></pre>
<h3>文件系统相关：Blob, Tree 和 Snapshot</h3>
<p>文件树中的文件和目录对应 Git 的两种对象：</p>
<ul>
<li><strong>Blob</strong> - File：文件内容（纯数据）</li>
<li><strong>Tree</strong> - Directory：目录结构（名字 + 指针）</li>
</ul>
<p><img src="Attachments/TreeBlob3.png" alt="" /></p>
<p>因此：</p>
<pre><code>// a file is a bunch of bytes
type blob = array&lt;byte&gt;

// a directory contains named files and directories
type tree = map&lt;string, tree | blob&gt;
</code></pre>
<p>项目的文件<strong>快照（Snapshot）</strong>. 即某一时刻整个项目的完整结构 + 内容。Snapshot 可以用项目根目录的 tree 来表示，例如下图就展示了一个可能的文件快照。</p>
<p><img src="Attachments/TreeBlob.png" alt="" /></p>
<h3>历史追踪相关：History 和 Commit</h3>
<p><strong>History</strong>. 在 Git 中，历史可以表示为一个有向无环图（DAG），其中每个节点是项目的一个 snapshot.</p>
<ul>
<li>这意味着，除了根节点之外（即初始状态），每个 snapshot 都会指向一个或多个父节点</li>
<li>这意味着这个 snapshot 由之前项目的每个状态经过若干改变修正而来</li>
</ul>
<p><strong>Commit</strong> 记录了一次项目状态. Commit 是 Git 历史中的一个节点，它包含</p>
<ul>
<li>一个文件快照，即当前时刻项目文件的状态，也就是从项目根目录为根结点的 tree，描述该时刻文件结构和内容</li>
<li>指向其父 commit 的引用，记录是从哪一个快照而来的，从而将多个 snapshot 连接成一个有向无环图。</li>
<li>一些元信息，例如作者、提交时间和 message</li>
</ul>
<p><img src="Attachments/GitCommit.png" alt="" /></p>
<pre><code>// a commit has parents, metadata, and the top-level tree
type commit = struct {
    parents: array&lt;commit&gt;
    author: string
    message: string
    snapshot: tree
}
</code></pre>
<p>Git 中的 commit 是不可变的。这不意味着 commit 中的错误无法被修正，只是说，对提交历史的“修改”，实际上是创建新的 commit，然后更新引用（reference）去指向这些新的 commit。</p>
<h3>总结：Commit, Tree 和 Blob</h3>
<p>Commit, tree 和 blob 的概念可以从下图体现。Commit 通过存储项目根目录 tree 指针的方式记录项目的 snapshot.</p>
<p><img src="Attachments/TreeBlob2.png" alt="" /></p>
<h2>Git 中的引用</h2>
<p>因为 Git 对象是根据项目内容 hash 标识的，基于项目历史不可篡改的项目，所有 Git 对象都是不可变的。假如我们想回到项目之前历史的某一时刻，我们需要改变项目的状态。</p>
<p>这可以通过指针来实现，用以指向某个 commit，从而表示当前所在的位置。例如，</p>
<ul>
<li>HEAD 表示当前项目的位置，通过指向某个 commit（通常经由分支）来确定当前状态</li>
<li>Branch 表示一条开发分支，指向某个 commit，并随着新的提交不断向前移动，从而记录一条状态的演化路径。与此同时，不同的 branch 表示项目历史中不同的开发路径，例如并行进行修复代码逻辑 A 的 bug 和增加新功能代码逻辑 B. 一条 Branch 可以被命名为 <code>main</code>, <code>master</code>, <code>dev-XXX</code> 等.</li>
</ul>
<p><img src="Attachments/GitHEAD.png" alt="" /></p>
<p><strong>Git Reference</strong>. 即指向某一个 commit 的指针，并且可以被灵活移动。其中，</p>
<ul>
<li>一部分 reference（如 branch）是可移动的，会随着新的提交不断向前推进；</li>
<li>而另一部分（如 tag）则用于固定标记历史中的特定位置，通常不会改变。</li>
</ul>
<h2>Git Repository：可回溯的状态空间</h2>
<p>从上面的角度来看，Git 实际上构建了一个完整的“项目状态空间”：</p>
<ul>
<li><strong>objects（blob / tree / commit）</strong> 描述了“状态是什么”</li>
<li><strong>references（HEAD / branch / tag）</strong> 描述了“当前在哪个状态，以及如何移动”</li>
</ul>
<p>因此可以理解为 <strong>Git = 一个不可变的状态集合 + 一组可移动的指针</strong>. 这使得 Git 具备了几个关键能力：</p>
<ul>
<li>可以精确记录每一个历史状态（snapshot）</li>
<li>可以在不同状态之间自由切换（checkout）</li>
<li>可以并行演化多个开发路径（branch）</li>
<li>可以在不破坏历史的前提下进行修改（通过创建新的 commit）</li>
</ul>
<p>因此，Git 构建了一个可导航、可回溯、可分叉的状态空间，而开发过程本质上就是在这个状态空间中的不停移动。</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://missing.csail.mit.edu/2026/version-control/">Version Control and Git</a></li>
<li><a href="https://alexwlchan.net/a-plumbers-guide-to-git/2-blobs-and-trees/">Part 2: Blobs and trees</a></li>
<li><a href="https://medium.com/swlh/git-from-the-bits-and-pieces-beyond-the-basics-part-1-aca2d02d360b">Git from the bits and pieces, beyond the basics — Part 1</a></li>
</ul>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-24T19:37:40.472Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[强化学习基础（1）：马尔可夫决策过程]]></title>
        <id>https://www.languisher.top/en/posts/markov-chain/</id>
        <link href="https://www.languisher.top/en/posts/markov-chain/"/>
        <updated>2026-03-21T17:51:58.499Z</updated>
        <summary type="html"><![CDATA[马尔可夫决策过程（Markov decision process，MDP）是强化学习的重要概念。本文在动手学强化学习著作的基础上，对于马尔可...]]></summary>
        <content type="html"><![CDATA[<p><strong>马尔可夫决策过程</strong>（Markov decision process，MDP）是强化学习的重要概念。本文在<a href="https://hrl.boyuai.com/chapter/1/%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E5%86%B3%E7%AD%96%E8%BF%87%E7%A8%8B">动手学强化学习</a>著作的基础上，对于马尔可夫决策过程进行了总结。</p>
<h2>随机过程</h2>
<p><strong>随机过程 (stochastic process)</strong> 研究对象是随时间演变的随机现象，其中：</p>
<p><strong>状态 (state)</strong>：随机现象在某时刻 $t$ 的取值是一个向量随机变量 $S_{t} \in \mathcal{S}$，其中 $\mathcal{S} = { s_{1},\dots, s_{n} }$ 表示所有可能的状态集合，且 $|\mathcal{S}|=n$ 表示可能的状态数量.</p>
<ul>
<li>举例：$S_t=$ (机器人位置，机器人速度，机器人加速度)</li>
</ul>
<p><strong>状态转移</strong>：在某个时刻 $t$ 的状态 $S_{t}$ 取决于 $t$ 时刻之前的状态 ${S_{1},\dots, S_{t-1}}$.</p>
<p><strong>状态转移概率</strong>：已知历史信息 $(S_{1},\dots,S_{t})$ 时，下一个时刻状态为 $S_{t+1}$ 的概率是：
$$
\mathbb{P}(S_{t+1}|S_{1},\dots,S_{t})
$$</p>
<h2>马尔可夫过程</h2>
<blockquote>
<p>[!note]
马尔可夫过程可以看作一个带概率的状态机：其描述了一类随机系统，其中状态随时间演化，并满足：下一时刻的状态只依赖当前状态，而与过去无关。</p>
</blockquote>
<p><strong>马尔可夫性质 (Markov property)</strong>. 在某个时刻 $t+1$ 的状态只取决于上一时刻，即在时刻 $t$ 的状态。用公式表述就是：
$$
\mathbb{P}(S_{t+1}= s_{t+1}|S_{t}=s_{t}) = \mathbb{P}(S_{t+1} = s_{t+1}| S_{1} = s_{1},\dots,S_{t}=s_{t})
$$</p>
<ul>
<li>虽然马尔可夫性质使得下一时刻的状态只依赖当前状态，看似忽略了历史信息，但实际上可以通过将历史信息编码到当前状态中，从而不丢失对未来预测所需的信息。</li>
</ul>
<p><strong>状态转移矩阵 (state transition matrix)</strong>. 基于马尔可夫性质的状态转移概率可以用矩阵表述：
$$
\mathcal{P} = \begin{bmatrix}
\mathbb{P}(S_{t+1}|S_{t})
\end{bmatrix}<em>{|\mathcal{S}| \times |\mathcal{S}|} = \begin{bmatrix}
\mathbb{P}(s</em>{1}|s_{1})  &amp;  \dots &amp;  \mathbb{P}(s_{n}|s_{1}) \
\vdots  &amp; \ddots  &amp; \vdots \
\mathbb{P}(s_{1}|s_{n})  &amp; \dots  &amp;  \mathbb{P}(s_{n}|s_{n})
\end{bmatrix}
$$</p>
<ul>
<li>每一行表示一种初始状态</li>
<li>每一列表示一种转移后的状态</li>
<li>对于第 $i$ 行第 $j$ 列矩阵表示 $\mathbb{P}(S_{t+1}=s_{j}| S_{t} = s_{i})$，即 $s_{i}\to s_{j}$ 的状态转移</li>
</ul>
<p><img src="Attachments/MarkovProcess.png" alt="" /></p>
<p><strong>马尔可夫过程 (Markov process)</strong>. 指具有马尔可夫性质的随机过程，可以表示为
$$
\text{Markov process} = \langle \mathcal{S}, \mathcal{P}\rangle
$$</p>
<p><strong>状态序列 (episode)</strong>. 一串按时间顺序排列的状态</p>
<ul>
<li>例如 $s_{1}\to s_{1} \to s_{2}\to s_{3}\to s_{6}$</li>
</ul>
<p><strong>采样 (sampling)</strong>. 从某一个状态开始，根据它的状态转移矩阵，一步一步随机生成一个状态序列的过程。</p>
<h2>马尔可夫奖励过程</h2>
<blockquote>
<p>[!note]
马尔可夫奖励过程是在马尔可夫过程的基础上，引入奖励函数，用于评估从某个状态出发的长期回报，从而衡量状态的优劣。</p>
<p>重要概念：</p>
<ul>
<li>回报描述了从当前时刻开始直到终止状态时，<strong>所有</strong>奖励的衰减之和</li>
<li>价值函数描述了从状态出发的<strong>期望回报</strong>。</li>
</ul>
</blockquote>
<h3>问题建模</h3>
<p><strong>奖励 (reward)</strong>. 对于一个状态 $s$，其奖励 $r(s) \in \mathbb{R}$ 表示处于（或者理解为从任意状态转移到）该状态时所获得的期望奖励。</p>
<ul>
<li>这是单步的</li>
<li>其向量化表示： $\mathcal{R} = \begin{bmatrix} R(s_{1})  \ \vdots  \  R(s_{n})\end{bmatrix}$</li>
</ul>
<p><strong>回报 (return)</strong>. 对于一个状态序列，从第 $t$ 时刻开始直到终止状态时，所有奖励的衰减之和
$$
G_{t}= R_{t}+ \gamma R_{t+1} + \gamma^2 R_{t+2}+\dots = \sum_{k=0}^\infty \gamma^k R_{t+k}
$$</p>
<ul>
<li>这是多步的</li>
<li>$\gamma\in [0,1)$ 是折扣因子，表示我们倾向于尽快获得奖励、对远期利益打折扣的程度</li>
<li>$R_{t}$ 表示在时刻 $t$ 获得的奖励</li>
</ul>
<p><img src="Attachments/MRP.png" alt="" /></p>
<p><strong>马尔可夫奖励过程 (Markov reward process)</strong>. 在马尔可夫过程的基础上，加上每个状态的奖励函数和回报的折扣因子，可以表示为
$$
\text{Markov reward process} =\langle \mathcal{S}, \mathcal{P}, \mathcal{R}, \gamma \rangle
$$</p>
<ul>
<li>通过定义奖励和状态转移，使我们可以用“长期期望回报”来量化一个状态的好坏</li>
</ul>
<h3>状态价值函数</h3>
<p><strong>价值 (value)</strong> 和 <strong>价值函数 (value function)</strong>. 对于一个状态 $s$，其期望回报称之为该状态的价值。所有状态的价值组成了价值函数
$$
V(s) = \mathbb{E} [G_{t}|S_{t}=s]
$$</p>
<ul>
<li>期望回报是指在给定状态转移概率（以及策略）下，对所有可能的未来状态序列的回报取期望。</li>
<li>价值函数是指对于所有状态，建立一个从状态到其价值的映射</li>
</ul>
<p><strong>贝尔曼方程 (Bellman equation)</strong>. 求解每个状态的价值. 推导：</p>
<p>$$
\begin{align}
V(s) &amp;= \mathbb{E}[G_{t}|S_{t}= s]  \
&amp;= \mathbb{E}[R_{t}+ \gamma (R_{t+1}+ \gamma R_{t+2}+\gamma^2R_{t+3}+\dots)|S_{t}=s] \
&amp;= \mathbb{E}[R_{t}+\gamma G_{t+1}|S_{t}=s] \
&amp;= \mathbb{E}[R_{t}|S_{t}=s] + \gamma \sum_{s_{t+1}\in \mathcal{S}}\mathbb{E}[G_{t+1}|S_{t+1}=s_{t+1}] \mathbb{P}(S_{t+1}=s_{t+1}| S_{t}=s_{t}) \
&amp;= r(s) + \gamma \sum_{s' \in \mathcal{S}} V(s') p(s'|s)
\end{align}
$$
可以看到，任意状态 $s$ 的价值可以理解为以下几项之和：</p>
<ul>
<li>当前拿到的及时奖励 $r(s)$</li>
<li>未来价值的期望：所有在下一个时刻可能的状态 $s'$
<ul>
<li>计算各自的价值 $V(s')$</li>
<li>计算各自的概率，即转移函数 $p(s'|s)$</li>
<li>两者相乘，再乘以折扣因子 $\gamma$</li>
</ul>
</li>
</ul>
<p><img src="Attachments/BellmanEquation.png" alt="" /></p>
<p>将价值、转移函数和奖励向量化，则得到：
$$
\mathcal{V} = \mathcal{R} + \gamma \mathcal{P} \mathcal{V} \implies \mathcal{V} = (1 - \gamma \mathcal{P})^{-1} \mathcal{R}
$$</p>
<p>其中 $\mathcal{V} = \begin{bmatrix} V(s_{1})  \ \vdots  \  V(s_{n})\end{bmatrix}$</p>
<h2>马尔可夫决策过程</h2>
<blockquote>
<p>[!note]
现在考虑有智能体参与的环境，智能体通过做出动作改变了状态之间的转移概率以及获得奖励的方式。</p>
<p>重要概念：在本章节中，虽然 {状态/动作} 价值函数同样也是表达 {特定状态/基于现在状态做出特定动作} 的远期期望收益，但是现在其和智能体采取的策略 $\pi$，也就是智能体具体会做什么动作有关。</p>
</blockquote>
<h3>问题建模</h3>
<p><strong>智能体 (agent)</strong>. 之前介绍的过程是自发改变的随机过程，现在我们引入外界的“刺激”来改变这个随机过程，我们将这个刺激称为智能体。具体而言，在基础的马尔可夫过程中，环境会从 $s_{t} \to s_{t+1} \to s_{t+2} \to \dots$ 从状态到状态的变化。智能体在中间引入了动作的概念，即：</p>
<ul>
<li>智能体有一些可选的<strong>动作(action)</strong>，其集合为 $\mathcal{A}$</li>
<li>智能体的 <strong>策略 (policy)</strong>. 这些动作是基于输入状态 $s$ 采取的。基于状态 $s$ 的情况下，智能体采取动作 $a$ 的概率为：$\pi(a|s) = \mathbb{P}(A_{t}=a|S_{t}=s)$.</li>
<li>状态转移函数：现在下一个时刻的状态不仅取决于上一个时刻的状态，还取决于上一个时刻智能体所做出的动作：$\mathbb{P}(s'|s,a)$</li>
<li>奖励函数：现在奖励函数不只是由状态，还和智能体决策（动作）一起共同决定：$r(s,a)$
<ul>
<li>这意味着智能体不再是进入状态 $s$ 就获得奖励，而是基于状态 $s$ 做出动作 $a$ 再获得奖励</li>
</ul>
</li>
</ul>
<p><img src="Attachments/MRPvsMDP.png" alt="" /></p>
<p><strong>马尔可夫决策过程 (Markov decision process)</strong>. 在马尔可夫奖励过程的基础上，引入智能体的动作概念，并且修改状态转移函数和奖励函数，使其现在和智能体的动作有关：
$$
\text{Markov decision process}= \langle \mathcal{S}, \mathcal{A}, \mathbb{P}(s'|s,a), r(s,a), \gamma \rangle
$$</p>
<h3>状态价值函数和动作价值函数</h3>
<p><img src="Attachments/StateValueandActionValueFunction.png" alt="" /></p>
<p><strong>状态价值函数 (state-value function)</strong>. 和在马尔可夫奖励过程的状态的价值函数相似，描述从状态 $s$ 出发，遵守策略 $\pi$ 所能获得期望回报：
$$
V^\pi (s) = \mathbb{E}<em>{\pi}[G</em>{t}|S_{t}=s]
$$</p>
<p><strong>贝尔曼期望方程 (Bellman Expectation Equation)——状态价值函数</strong>：</p>
<ul>
<li>智能体会基于该状态 $s$ 做出动作 $a$，且概率为 $\pi(s,a)$</li>
<li>对于每个动作 $a$，会带来奖励 $r(s,a)$</li>
<li>这会导致智能体可能进入状态 $s'$，概率为 $p(s'|s,a)$</li>
<li>对应的状态 $s'$ 具有价值 $V^\pi(s')$</li>
<li>因为是远期收益，需要加上折扣因子 $\gamma$</li>
</ul>
<p>因此，可以写为：
$$
\begin{align}
V^\pi (s) &amp;= \mathbb{E}<em>{\pi}[G</em>{t}|S_{t}=s]  \
&amp;= \mathbb{E}<em>{\pi}[R</em>{t}+\gamma R_{t+1}+ \gamma^2R_{t+2}+ \dots |S_{t}=s] \
&amp;= \mathbb{E}<em>{\pi}[R</em>{t}|S_{t}=s] + \gamma \mathbb{E}<em>{\pi} [G</em>{t+1}|S_{t}=s] \
&amp;= \sum_{a \in \mathcal{A}}\pi(a|s)r(s,a) + \gamma\sum_{a\in \mathcal{A}} \sum_{s' \in \mathcal{S}} \underbrace{\pi(a|s),\mathbb{P}(s'|s,a)}<em>{\text{probability of transitioning from } s \text{ to } s' \text{ via } a}\mathbb{E}</em>{\pi}[G_{t+1}|S_{t+1}=s'] \
&amp;= \sum_{a\in\mathcal{A}} \pi(a|s) \left( r(s,a) + \gamma \sum_{s' \in \mathcal{S}}  \mathbb{P}(s'|s, a) V^\pi (s') \right)
\end{align}
$$</p>
<p><strong>动作价值函数 (action-value function)</strong>. 与此同时，我们需要衡量在当前策略下，对当前状态 $s$ 做出动作 $a$ 所带来的收益：
$$
Q^\pi (s, a) = \mathbb{E}<em>{\pi}[G</em>{t}|S_{t}=s, A_{t}=a]
$$</p>
<p>动作价值函数和状态价值函数的关系：</p>
<ul>
<li>状态 $s$ 的价值函数等于
<ul>
<li>策略 $\pi$ 基于状态 $s$ 采取动作 $a$ 的概率 $\pi(a|s)$</li>
<li>乘以这个动作的价值 $Q^\pi(s,a)$</li>
</ul>
</li>
<li>动作 $(s,a)$ 的价值函数等于
<ul>
<li>及时奖励</li>
<li>加上经过衰减后的未来奖励，即
<ul>
<li>下一个时刻进入状态 $s'$ 的概率 $\mathbb{P}(s'|s,a)$</li>
<li>对应状态 $s'$ 的价值 $V^\pi (s')$</li>
<li>的加权求和</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>用公式表达即：
$$
\begin{cases}
V^\pi(s) = \sum_{a \in \mathcal{A}} \pi(a|s) Q^\pi (s, a) \
Q^\pi(s,a) = r(s,a) + \gamma\sum_{s' \in \mathcal{S}} \mathbb{P}(s'|s,a) V^\pi (s')
\end{cases}
$$</p>
<p><strong>贝尔曼期望方程 (Bellman Expectation Equation)——动作价值函数</strong>：基于以上两个关系，可以消除 $V^\pi(s)$，得到 $Q^\pi(s,a)$ 的表达式：
$$
Q^\pi(s,a) = r(s, a) + \gamma \sum_{s' \in\mathcal{S}}  \mathbb{P}(s'|s,a)  \sum_{a' \in A}\pi(a'|s') Q^\pi (s',a')
$$</p>
<blockquote>
<p>这些公式可以直接从图中直观观察到，本质上是概率乘以对应奖励加权求和。</p>
</blockquote>
<h3>马尔可夫决策过程求解</h3>
<p>通过贝尔曼方程，我们可以得到马尔可夫奖励过程的解析解。基于此，我们通过将马尔可夫决策过程退化成马尔可夫奖励过程得到其解析解：</p>
<p>状态奖励：从 $r(s,a)$ 到 $r(s)$ 做动作奖励的加权求和：
$$
r(s) = \sum_{a\in \mathcal{A}} \pi(a|s) r(s, a)
$$</p>
<p>同理，从 $p(s'|s,a)$ 到 $p(s'|s)$ 转移概率也对所有动作做加权求和：
$$
\mathbb{P}(s'|s)= \sum_{a\in \mathcal{A}} \pi(a|s) \mathbb{P}(s'|s,a)
$$
这样就可以做 MDP 到 MRP 的转换：
$$
\langle \mathcal{S}, \mathcal{A}, \mathbb{P}(s'|s,a), r(s,a), \gamma \rangle \to \langle \mathcal{S}, \mathbb{P}'(s'|s), r'(s), \gamma \rangle
$$</p>
<h2>最优策略和贝尔曼最优方程</h2>
<blockquote>
<p>[!note]
<strong>最优策略</strong>: 最优策略的目标是找到一个策略，使得其对应的价值函数在所有策略中最大。</p>
</blockquote>
<p><strong>最优策略</strong>. 如何定义一个策略 $\pi(a|s)$ 是最优的？</p>
<ul>
<li>这意味着从任意状态 $s \in \mathcal{S}$ 出发，智能体所获得的预期远期收益都是最高的</li>
<li>这个期望收益可以用状态价值函数来描述</li>
</ul>
<p>因此，如果 $\pi^* \in \Pi$ 是最优策略，它满足：
$$
\forall s\in \mathcal{S}, \quad V^{<em>} (s) = \max_{\pi}V^\pi(s)
$$
其中 $V^</em>(s) = V^{\pi^*}(s)$，</p>
<p>换言之，当系统处于状态 $s$ 的时候，最优策略是所有策略中使其价值函数最大的那一个：</p>
<p>$$
\boxed{\pi^* = \arg \max_{\pi} V^\pi(s)}
$$</p>
<p>同样的，对于任意状态-动作对 $(s,a)\in \mathcal{S}\times\mathcal{A}$（意思是处于状态 $s$ 且智能体采取动作 $a$ 的情况下），策略$\pi^<em>$ 最优意味着：在之后继续采取策略 $\pi^</em>$ 为这个状态-动作对所带来的远期价值最高。
$$
\forall (s, a) \in \mathcal{S} \times \mathcal{A}, \quad Q^*(s,a) = \max_{\pi}Q^\pi (s, a)
$$</p>
<p><strong>最优策略在任意状态 $s$ 下，一定会在所有可能的状态-动作对 $(s,.)$ 中，采取动作 $a$ 使得状态-动作对 $(s,a)$ 使得其动作价值最高。</strong> 换言之：
$$
\pi^<em>(s) \in \arg \max_{a \in \mathcal{A}}Q^</em>(s, a), \quad V^<em>(s) = \max_{a \in \mathcal{A}} Q^</em>(s, a)
$$
如果 $\pi^<em>$ 是确定性策略，则 $V^</em>(s)= Q^<em>(s, \pi^</em>(s))$</p>
<p><strong>贝尔曼最优方程 (Bellman optimality equation)</strong>. 对于最优状态价值函数和最优动作价值函数进行推导。</p>
<p>首先最优策略意味着在状态-动作对 $(s,a)$ 下，无论在下一个时刻进入哪个状态 $s'$，都会带来该状态的最优状态价值。这意味着：
$$
\begin{align}
Q^<em>(s, a)
&amp;=   r(s,a) + \gamma \sum_{s\in \mathcal{S}} p(s'|s,a)  V^</em> (s')  \
&amp;=  r(s,a) + \gamma \sum_{s\in \mathcal{S}} p(s'|s,a) \max_{a\in \mathcal{A}} Q^*(s,a)
\end{align}
$$</p>
<p>将这个表达式代回最优状态价值函数，我们得到：
$$
V^<em>(s) = \max_{a\in \mathcal{A}} \left{ r(s,a) + \gamma \sum_{s' \in \mathcal{S}} p(s'|s,a) V^</em>(s') \right}
$$</p>
<h2>最优策略的解</h2>
<p>在本章节我们将会介绍两种算法，在已知转移函数 $p(s'|s,a)$ 和奖励函数 $r(s,a)$ 的马尔可夫决策过程下，如何解得最优策略。</p>
<h3>策略迭代算法</h3>
<p>现在我们希望得到最优策略。现在我们知道：</p>
<ul>
<li>对于任意策略 $\pi$，其对应一种状态价值函数 $V^\pi(s)$ 和动作价值函数 $Q^{\pi}(s, a)$</li>
<li>因此当策略发生变化的时候，需要重新评估这两个值</li>
<li>可不可以通过迭代的方式，持续优化策略使得其逐渐靠近最优策略？</li>
</ul>
<p>这等价于：
$$
\dots \to \pi_{i} \to^\text{Evaluation} V^{\pi_{i}} \to^\text{Optimization} \to \pi_{i+1} \to \dots \to \pi^*
$$</p>
<p>基于以上思想，<strong>策略迭代算法</strong></p>
<ul>
<li>从初始策略 $\pi_{0}$ 开始</li>
<li>反复进行：对于当前策略 $\pi_{i}$
<ul>
<li>对该策略进行评估，即计算其状态价值函数</li>
<li>对该策略进行提升，即找到一个不差于该策略的策略 $\pi_{i+1}$</li>
<li>直到状态价值函数优化程度提升幅度小于一定程度停止</li>
</ul>
</li>
</ul>
<p><strong>策略评估</strong>：得到当前策略 $\pi_{i}$ 的状态价值函数。这可以通过<em>贝尔曼期望方程</em>中的最优状态价值函数推导。这可以通过迭代+不动点的思想计算，即对于策略 $\pi_{i}$</p>
<ul>
<li>将状态价值函数随机初始化，得到 $V^0(s)$</li>
<li>当前轮的状态价值函数用上一轮的状态价值计算</li>
<li>最终状态价值函数一定会收敛到该策略的实际状态价值函数 $V^k \to_{k \to + \infty} V^{\pi_{i}}$</li>
</ul>
<p>$$
\forall s\in\mathcal{S}, \quad V^{k+1}(s) = \sum_{a\in \mathcal{A}} \pi(a|s) \left( r(s,a) + \sum_{s' \in \mathcal{S}} p(s'|s, a)V^k(s)\right)
$$</p>
<p><strong>策略优化</strong>：找到一个新策略 $\pi_{i+1}$ 使其不差于当前策略 $\pi_{i}$. 这意味着对于任意状态
$$
\forall s \in \mathcal{S}, \quad Q^{\pi_{i}}(s, \pi_{i+1}(s)) \geq V^{\pi_{i}}(s)
$$</p>
<p><strong>我们可以对于任意状态 $s$，贪心选取在目前策略 $\pi_{i}$ 下，对每一个状态 $s$ 选取动作价值最大的动作</strong>，因此</p>
<p>$$
\forall s \in\mathcal{S}, \quad \pi_{i+1}(s) = \arg\max_{a\in \mathcal{A}} Q^{\pi_{i} } (s,a) = \arg \max_{a\in\mathcal{A}} \left{r(s,a) + \gamma\sum_{s' \in \mathcal{S}} \mathbb{P}(s'|s,a) V^\pi (s')\right}
$$
当优化程度小于一定阀值 $\Delta$ 即停止。</p>
<p>具体算法表述：
<img src="Attachments/PolicyIteration.png" alt="" /></p>
<h2>价值迭代算法</h2>
<p>贝尔曼最优方程告诉我们在最优情况下价值是怎么样的。这意味着我们可以利用这个公式，在目前时刻做出决策之后，假设未来时刻都一直是最优的，反推现在应该如何做。</p>
<p>通过贝尔曼最优方程我们得到价值最优函数的表达式：</p>
<p>$$
V^<em>(s) = \max_{a\in \mathcal{A}} \left{ r(s,a) + \gamma \sum_{s' \in \mathcal{S}} p(s'|s,a) V^</em>(s') \right}
$$
这个公式可以通过动态规划做求解：</p>
<ul>
<li>迭代求解，等式左边的 $V^*(s)$ 为 $V_{k+1}$，等式右边的为 $V_{k}$</li>
<li>$V^<em>$ 是这个方程的不动点，因此当 $V_{k+1}=V_{k}$ 时我们就找到了想求的 $V^</em>$</li>
</ul>
<p>再求得价值最优函数后，我们直接定义策略 $\pi$ 对每一个状态 $s$ 采取动作价值最大的动作 $a$
$$
\forall s \in\mathcal{S}, \quad \pi(s) = \arg\max_{a\in \mathcal{A}} Q^{ <em>} (s,a) = \arg \max_{a\in\mathcal{A}} \left{r(s,a) + \gamma\sum_{s' \in \mathcal{S}} \mathbb{P}(s'|s,a) V^</em> (s')\right}
$$
容易得到 $\pi$ 就是我们想求的最优策略。</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://hrl.boyuai.com/chapter/1/%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E5%86%B3%E7%AD%96%E8%BF%87%E7%A8%8B">第 3 章 马尔可夫决策过程（动手学强化学习）</a></li>
</ul>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-21T17:51:58.499Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[并行计算基础通信原语]]></title>
        <id>https://www.languisher.top/en/posts/collective-communication/</id>
        <link href="https://www.languisher.top/en/posts/collective-communication/"/>
        <updated>2026-03-10T08:51:38.942Z</updated>
        <summary type="html"><![CDATA[本文会介绍在并行计算中常见的通信原语。我们定义每个设备为一个 rank. 在并行集合通信中， All 表示通信的 dst 是所有设备 Red...]]></summary>
        <content type="html"><![CDATA[<p>本文会介绍在并行计算中常见的通信原语。</p>
<p><img src="Attachments/collective_communication.png" alt="" /></p>
<h2>基础概念定义</h2>
<p>我们定义每个设备为一个 rank.</p>
<p>在并行集合通信中，</p>
<ul>
<li><strong>All</strong> 表示通信的 dst 是所有设备</li>
<li><strong>Reduce</strong> 表示对于数据执行 associative/commutative 计算（例如求和/求平均）</li>
<li><strong>Gather</strong> 表示将“分散”（在各个设备）的数据 shard 合并起来</li>
<li><strong>Scatter</strong> 则是 Gather 的反面，将完整的数据分块分发给多个设备</li>
</ul>
<h2>集合通信操作定义</h2>
<h3>局部和全局</h3>
<p>假设我们希望将一个设备上的数据复制给所有其他设备，让所有设备都拥有该数据的一份<em>完整</em>备份，这就是 <strong>广播（Broadcast）</strong>。</p>
<pre><code>|  D0  |  D1  |  D2  |  D3  |
|      |      | data |      |
--copy--&gt;
| data | data | data | data |
</code></pre>
<p>与广播相反的情形：假设我们所有设备都存储了相同形状的数据，<em>现在我们希望将这些数据进行 Reduce 操作（例如求和/求平均）之后</em>将计算结果只保留到一个设备上，这个操作是 <strong>归约（Reduce）</strong>.</p>
<pre><code>|  D0  |  D1  |  D2  |  D3  |
| data | data | data | data |
--reduce (e.g. sum/avg)--&gt;
|      |      | data |      | (Reduced)
</code></pre>
<h3>全局和全局</h3>
<p>前面介绍了 Reduce 操作，如果我们希望把 Reduce 操作之后的结果保留在所有设备上，这个操作是 <strong>All Reduce</strong>.</p>
<pre><code>|  D0  |  D1  |  D2  |  D3  |
| data | data | data | data |
--reduce (e.g. sum/avg)--&gt;
| data | data | data | data | (Reduced)
</code></pre>
<p>假设我们现在每个设备都持有数据的一部分，例如：</p>
<ul>
<li>完整的数据是一个 array：$x = [x_{0}, x_{1}, \dots, x_{1023}] \in \mathbb{R}^{1024}$</li>
<li>现在一共有 4 个设备，rank 0 持有 $x^{(0)}= [x_{0}, \dots, x_{255}] \in \mathbb{R}^{1024/4}$, rank 1 持有 $x^{(1)} = [x_{256}, \dots, x_{511}] \in \mathbb{R}^{1024/4}$, ...</li>
</ul>
<p>现在我们希望每个设备都有该数据的完整备份，这意味着：</p>
<ul>
<li>每个 rank 需要将自己的数据复制到所有其它 rank 上</li>
<li>每个 rank 需要接收除了自己所持有的数据部分之外的数据 shard</li>
</ul>
<p>这被称之为 <strong>All Gather</strong>.</p>
<pre><code>|    D0     |    D1     |    D2     |    D3     |
| data[0]   | data[1]   | data[2]   | data[3]   |
--concatenate--&gt;
| data[0:3] | data[0:3] | data[0:3] | data[0:3] |
</code></pre>
<p>与 All Gather 相反的情形：</p>
<ul>
<li>假设我们所有设备都存储了相同形状的数据，</li>
<li><em>现在我们希望将这些数据进行 Reduce 操作（例如求和/求平均）之后</em></li>
<li>将数据的每一部分分发给不同的设备</li>
<li>这意味着每个设备只用负责计算 其被分配到部分的 Reduce 操作计算，并且不同设备的 Reduce 操作计算可以同时进行</li>
</ul>
<p>这被称之为 <strong>Reduce Scatter</strong>.</p>
<pre><code>|    D0     |    D1     |    D2     |    D3     |
| data[0:3] | data[0:3] | data[0:3] | data[0:3] |
--reduce[0]-|-reduce[1]-|-reduce[2]-|-reduce[3]-|
| data[0]   | data[1]   | data[2]   | data[3]   | (Reduced)
</code></pre>
<h2>Ring 通信</h2>
<p>在上一个章节介绍了通信的逻辑语义，实际实现算法最常见的是 ring 通信，其核心思想是：</p>
<ul>
<li>将所有 device 连成一个环。假设我们有 4 个设备，这意味着 <code>0 -&gt; 1 -&gt; 2 -&gt; 3 -&gt; 0</code></li>
<li>系统形成一个 pipeline，在每一步中：</li>
<li>每个 GPU 发送给其 <em>右</em> 邻居，并且接受来自其 <em>左</em> 邻居的数据</li>
</ul>
<p>例子：</p>
<pre><code>|    D0     |    D1     |    D2     |    D3     |
| A[0]      | A[1]      | A[2]      | A[3]      |
    -           -           -           -
| ---A[0]--&gt;| ---A[1]--&gt;| ---A[2]--&gt;| ---A[3]--&gt;|
| A[0,3]    | A[1,0]    | A[2,1]    | A[3,2]    |
      -           -           -           -
| ---A[3]--&gt;| ---A[0]--&gt;| ---A[1]--&gt;| ---A[2]--&gt;|
| A[0,3,2]  | A[1,0,3]  | A[2,1,0]  | A[3,2,1]  |
        -           -           -           -
| ---A[1]--&gt;| ---A[2]--&gt;| ---A[0]--&gt;| ---A[3]--&gt;|
| A[0,3,2,1]| A[1,0,3,2]| A[2,1,0,3]| A[3,2,1,0]|
</code></pre>
<h2>All-Reduce 计算优化</h2>
<p>假设我们有 $p$ 个设备。回顾一下，All-Reduce 操作</p>
<ul>
<li>首先，所有设备上都有形状相同的数据 $x\in \mathbb{R}^N$</li>
<li>我们希望对于所有 $x_{i}, i\in {1, \dots, N}$，跨设备进行 Reduce 操作</li>
</ul>
<p>我们可以</p>
<ul>
<li>利用 Reduce-Scatter 的思想，让 device i 负责 $[x_{i/p \times N}, \dots, x_{i/p \times N+N}]$ 的 Reduce 计算，</li>
<li>再通过一次 All-Gather 操作将每个 device 得到的 reduced data shard 合并成完整的数据，完成同步</li>
</ul>
<p>因此，一个重要的结论是：
$$
\text{All-Reduce} = \text{Reduce-Scatter} + \text{All-Gather}
$$</p>
<p><img src="Attachments/all_reduce.png" alt="" /></p>
<p>通信计算：</p>
<ul>
<li>假设不同 device 之间传递的 tensor size = $N$，$N$ 是整个 tensor 的大小且一共有 $p$ 个 device，每个设备持有 $N/p$ 的数据</li>
<li>一共要进行 $p-1$ 个 step</li>
<li>因此每个设备在整个流程中要传输 $(p-1)N/p$ 的数据</li>
</ul>
<p>在 All-Reduce 的情景下：</p>
<ul>
<li>ReduceScatter 需要发送 $(p-1) \times N/p$ 数据（这是因为进行 Reduce 操作时，需要接收所有其他设备上的数）</li>
<li>AllGather 拼接，同理也需要发送 $(p-1) \times N/p$ 数据</li>
</ul>
<p>因此，一共是
$$
2 \frac{p-1}{p} N
$$
数据，当 $p$ 数量很大的时候近似于 $2N$.</p>
<h2>总结</h2>
<p>我们介绍了 Reduce, Broadcast, All {Gather/Reduce} 和 Reduce Scatter 操作以及如何优化 All-Reduce 操作。</p>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-10T08:51:38.942Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[CUDA 寻址]]></title>
        <id>https://www.languisher.top/en/posts/cuda-indexing/</id>
        <link href="https://www.languisher.top/en/posts/cuda-indexing/"/>
        <updated>2026-03-04T14:45:48.564Z</updated>
        <summary type="html"><![CDATA[本文简单介绍了在 CUDA 环境下 Grid, Block 和 Thread 的概念，以及如何寻找它们的地址。CUDA Kernel 是一个...]]></summary>
        <content type="html"><![CDATA[<p>本文简单介绍了在 CUDA 环境下 Grid, Block 和 Thread 的概念，以及如何寻找它们的地址。</p>
<h2>关键概念</h2>
<p><img src="Attachments/CUDAIndexing.png" alt="" /></p>
<p><strong>CUDA Kernel</strong> 是一个用 <code>__global__</code> 声明的函数，它在 GPU (device) 上执行，由 CPU (host) 发起调用。</p>
<pre><code>__global__ void myKernel() {
	...
}

// invoke
dim3 gridDim(2, 2);
dim3 blockDim(4, 3);
mykernel&lt;&lt;&lt;gridDim, blockDim&gt;&gt;&gt;();
</code></pre>
<p><strong>Grid</strong>. 当 Host 启动一个 kernel 时，CUDA runtime 会在 Device 上创建一个 <strong>grid</strong>。</p>
<ul>
<li>一个 grid 是由多个 thread blocks 组成的集合。</li>
<li>每一次 kernel launch 会生成一个 grid，因此 kernel launch 与 grid 之间是一一对应的关系。</li>
<li>不同的 kernel launch 可以使用不同的 grid 维度和 block 维度。</li>
<li><code>gridDim.{x,y,z}</code> 表示 Grid 的 shape</li>
</ul>
<p><strong>Block</strong>. 多个 thread 组成的集合。</p>
<ul>
<li>在同一个 grid 中，所有的 block 的 shape 都是一样的</li>
<li>在不同 kernel launch 对应的 grid 中，block 的 shape 可能不同</li>
<li><code>blockDim.{x,y,z}</code> 表示 Block 的 shape</li>
<li>使用 <code>blockIdx.{x,y,z}</code> 表示当前 block 在 grid 中的索引</li>
</ul>
<p><strong>Thread</strong>. 实际执行的最小 (software) execution unit.</p>
<ul>
<li>使用 <code>threadIdx.{x,y,z}</code> 表示当前 thread 在 block 中的索引</li>
</ul>
<p>Grid 和 Block 都可以是三维的，如下图所示：
<img src="Attachments/CUDAIndexing3D.png" alt="" /></p>
<h2>Thread indexing</h2>
<p>并行计算中，不同线程需要处理不同的数据。为了确定每个线程所负责的数据位置，我们需要计算每个线程的 <strong>global thread index</strong>。在这个章节我们会研究如何通过线程索引来确定每个线程处理的数据位置。</p>
<p><img src="Attachments/FlattenedIndexingIllustration.png" alt="定位 thread index 的例子" /></p>
<p>总体思路：目的是将多维的 thread 组织（grid/block/thread）映射为一维的 global thread index</p>
<ul>
<li>首先定位 block 位置，把 blockIdx flatten 成 1D，确认每个 block 有多少个 thread</li>
<li>再确定 thread 在 block 第几行，把 threadIdx flatten 成 1D，确认 block 中的每一行有多少个 thread</li>
<li>最后加上 threadIdx.x</li>
</ul>
<p>$$
\text{threadId} = \underbrace{\text{blockId}}<em>{\text{grid 内 block 的线性编号}} \times \underbrace{\text{threadsPerBlock}}</em>{\text{每个 block 的线程总数}} + \underbrace{\text{localThreadId}}_{\text{block 内 thread 的线性编号}}
$$</p>
<p><img src="Attachments/1DGrid1DBlock.png" alt="" /></p>
<p>1D Grid + 1D Block 情况：总共有 $\text{blockDim}.x \times \text{gridDim}.x$ 个 thread，
$$
\text{threadId} = (\text{blockIdx}.x \times \text{blockDim}.x) + \text{threadIdx}.x
$$</p>
<p><img src="Attachments/1DGrid2DBlock.png" alt="" /></p>
<p>1D Grid + 2D Block 情况：总共有 $\text{gridDim}.x \times (\text{blockDim}.x \times \text{blockDim}.y)$ 个 thread，
$$
\text{threadId} = (\text{blockIdx}.x \times \text{blockDim}.x \times \text{blockDim}.y) + (\text{threadIdx}.y \times \text{blockDim}.x) + \text{threadIdx}.x
$$</p>
<p><img src="Attachments/2DGrid1DBlock.png" alt="" /></p>
<p>2D Grid + 1D Block 情况：总共有 $(\text{gridDim}.x \times \text{gridDim}.y) \times \text{blockDim}.x$ 个 thread，
$$
\text{blockId} := (\text{blockIdx}.y \times \text{gridDim}.x) + \text{blockIdx}.x
$$
因此
$$
\text{threadId} = \text{blockId} \times \text{blockDim}.x + \text{threadIdx}.x
$$</p>
<p><img src="Attachments/2DGrid2DBlock.png" alt="" /></p>
<p>2D Grid + 2D Block 情况：总共有 $(\text{gridDim}.x \times \text{gridDim}.y) \times (\text{blockDim}.x \times \text{blockDim}.y)$ 个 thread,
$$
\text{blockId} = \text{gridDim}.x \times \text{blockIdx}.y + \text{blockIdx}.x
$$
因此
$$
\text{threadId} = \text{blockId} \times \text{blockDim}.x \times \text{blockDim}.y + \text{blockDim}.x \times \text{threadIdx}.y + \text{threadIdx}.x
$$</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://anuradha-15.medium.com/cuda-thread-indexing-fb9910cba084">CUDA Thread Indexing</a></li>
<li><a href="https://medium.com/@omkarpast/mastering-cuda-kernel-development-a-comprehensive-guide-1f3032666b94">Mastering CUDA Kernel Development: A Comprehensive Guide</a></li>
<li><a href="https://siboehm.com/articles/22/CUDA-MMM">How to Optimize a CUDA Matmul Kernel for cuBLAS-like Performance: a Worklog</a></li>
</ul>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-04T14:45:48.564Z</published>
    </entry>
</feed>