<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://cv.hqzhou.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://cv.hqzhou.com/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-08-12T12:32:49+00:00</updated><id>https://cv.hqzhou.com/feed.xml</id><title type="html">Houquan Zhou</title><subtitle>PhD student [@SUDA-HLT](http://hlt.suda.edu.cn), Soochow University</subtitle><entry xml:lang="zh-CN"><title type="html">Triton 学习手记 （一）：基本概念</title><link href="https://cv.hqzhou.com/2025/08/19/learning-trition-0.html" rel="alternate" type="text/html" title="Triton 学习手记 （一）：基本概念" /><published>2025-08-19T11:00:00+00:00</published><updated>2025-08-19T11:00:00+00:00</updated><id>https://cv.hqzhou.com/2025/08/19/learning-trition-0</id><content type="html" xml:base="https://cv.hqzhou.com/2025/08/19/learning-trition-0.html"><![CDATA[<h2 id="前言">前言</h2>
<p>什么是 Triton？
Triton 的<a href="https://triton-lang.org/main/index.html">官网文档</a> 中是这样介绍的：</p>
<blockquote>
  <p>Triton is a language and compiler for parallel programming. It aims to provide a Python-based programming environment for productively writing custom DNN compute kernels capable of running at maximal throughput on modern GPU hardware.</p>
</blockquote>

<p>简单来说 Triton 是一个允许你在 Python 中以 Python 风格的代码来写 GPU 并行计算程序的包。</p>

<p>我们知道在神经网络经常涉及大量的运算，其中有非常多的运算是可以并行处理的。例如，对一个 $1024 \times 1024$ 的矩阵进行 ReLU 运算。由于 ReLU 运算对每个元素都是独立的，因此我们可以将这个矩阵切分为数个小的矩阵，然后并行地对这些小矩阵进行 ReLU 运算，从而加速整个运算。
事实上 pytorch、tensorflow 等深度学习框架所提供的函数的背后都或多或少使用了 CUDA 及其类似的技术来实现并行加速。</p>

<p>然而这些函数并行化细节对用户隐藏，用户只能使用这些函数，而不能控制并行化细节。
假设我们现在构思出了一个全新的函数，我们希望并行优化这个函数，但是这个函数又不是 pytorch 或 tensorflow 中的函数，我们该怎么办？
这时候 Triton 就派上用场了。他提供了一个相对简单的语言（Python + 类 numpy 的语法），让用户可以在这个语言中控制 GPU 的并行化和运算细节，充分挖掘 GPU 的潜力。</p>

<p>这篇 Blog 是我学习 Triton 的笔记。作为初学者，我不可避免地会犯一些错误，欢迎大家指正。</p>

<h2 id="单程序多数据">单程序多数据</h2>
<p>第一个概念是<strong>单程序多数据</strong>。</p>

<p>Triton 使用单程序多数据（Single Program Multiple Data, SPMD）的编程模型。
也就是说在并发的时候，在同一时间运行的是<strong>同一份程序</strong>。这份代码在运行时通过某些系统提供的标识符来区分自己要<strong>处理哪份数据</strong>。</p>

<p>在 Triton 中，这份程序通常被称作 <strong>kernel</strong>。下面是一个简单的例子：</p>

<p><span id="zero-kernel"></span></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">triton.language</span> <span class="k">as</span> <span class="n">tl</span> <span class="c1"># 简单起见我们利用 `tl` 来指代 `triton.language` 模块
</span><span class="kn">import</span> <span class="n">triton</span>
<span class="kn">import</span> <span class="n">torch</span>

<span class="c1">## 修饰符，表示这是一个 Triton Kernel
</span><span class="nd">@triton.jit</span>
<span class="k">def</span> <span class="nf">zero_kernel</span><span class="p">(</span><span class="n">x_ptr</span><span class="p">):</span>
    <span class="c1"># 获取当前程序的编号
</span>    <span class="n">pid</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">program_id</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="c1"># 将当前程序的编号对应的元素设置为 0
</span>    <span class="n">offset</span> <span class="o">=</span> <span class="n">pid</span>
    <span class="n">tl</span><span class="p">.</span><span class="nf">store</span><span class="p">(</span><span class="n">x_ptr</span> <span class="o">+</span> <span class="n">offset</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="n">n</span> <span class="o">=</span> <span class="mi">128</span>
    <span class="c1"># 创建一个长度为 n 的空 tensor，并将其移动到 GPU 上
</span>    <span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">empty</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="sh">"</span><span class="s">cuda</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="c1"># 打印未初始化前的 x
</span>    <span class="n">grid</span> <span class="o">=</span> <span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="p">)</span>
    <span class="n">zero_kernel</span><span class="p">[</span><span class="n">grid</span><span class="p">](</span><span class="n">x</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="c1"># 打印初始化后的 x
</span></code></pre></div></div>

<p>这个例子展示了 SPMD 的核心思想：在运行时，调度程序会根据 <code class="language-plaintext highlighter-rouge">grid</code> 启动 $n$ 个 <code class="language-plaintext highlighter-rouge">zero_kernel</code>，第 $i$ 个实例通过 <code class="language-plaintext highlighter-rouge">tl.program_id(0)</code> 知道自己的编号是 $i$，然后去设置第 $i$ 个元素。</p>

<p>如果觉得还是很难理解，我们不妨换个思路：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@triton.jit</span>                         <span class="c1">#
</span><span class="k">def</span> <span class="nf">zero_kernel</span><span class="p">(</span><span class="n">x_ptr</span><span class="p">):</span>             <span class="c1"># def zero(x, pid):
</span>    <span class="n">pid</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">program_id</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>          <span class="c1">#
</span>    <span class="k">pass</span>                            <span class="c1">#     pass
</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>          <span class="c1"># if __name__ == "__main__":
</span>    <span class="n">n</span> <span class="o">=</span> <span class="mi">128</span>                         <span class="c1">#     n = 128
</span>    <span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">empty</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="sh">"</span><span class="s">cuda</span><span class="sh">"</span><span class="p">)</span>   <span class="c1">#     x = torch.empty(n).to("cuda")
</span>    <span class="n">grid</span> <span class="o">=</span> <span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="p">)</span>                    <span class="c1">#     for pid in range(n):
</span>    <span class="n">zero_kernel</span><span class="p">[</span><span class="n">grid</span><span class="p">](</span><span class="n">x</span><span class="p">)</span>            <span class="c1">#         zero(x, pid)
</span></code></pre></div></div>

<p>我们还是依照单线程循环的思路来写代码，只不过在 Triton 中，编译器自动根据 <code class="language-plaintext highlighter-rouge">grid</code> 来帮我们执行了“循环”。</p>

<p>Triton 允许我们设置最多三维的 <code class="language-plaintext highlighter-rouge">grid</code>，例如：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">grid</span> <span class="o">=</span> <span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">m</span><span class="p">,</span> <span class="n">l</span><span class="p">)</span>                    <span class="c1"># for pid_n in range(n):
</span>                                    <span class="c1">#     for pid_m in range(m):
</span>                                    <span class="c1">#         for pid_l in range(l):
</span><span class="n">kernel</span><span class="p">[</span><span class="n">grid</span><span class="p">](</span><span class="o">*</span><span class="n">args</span><span class="p">)</span>                 <span class="c1">#             func(*args, pid_n, pid_m, pid_l)
</span></code></pre></div></div>

<p><strong>注</strong>：调研了一些资料，目前暂时没有找到 <code class="language-plaintext highlighter-rouge">grid</code> 维度设置的最佳实践，似乎高维只是为了方便用户理解。</p>

<h2 id="tensor-的读取和存储">Tensor 的读取和存储</h2>
<p>在阅读前面的<a href="#zero-kernel">zero-kernel</a>时，你可能已经注意到，在<code class="language-plaintext highlighter-rouge">zero</code>函数中，我们向 <code class="language-plaintext highlighter-rouge">zero_kernel</code> 传递了一个 tensor，但是为什么 <code class="language-plaintext highlighter-rouge">zero_kernel</code> 的参数是 <code class="language-plaintext highlighter-rouge">x_ptr</code>？
为什么 <code class="language-plaintext highlighter-rouge">x_ptr + offset</code> 表示第 <code class="language-plaintext highlighter-rouge">offset</code> 个元素？
为什么不是像 Pytorch 那样直接使用 <code class="language-plaintext highlighter-rouge">x[offset]</code>？</p>

<h3 id="指针">指针</h3>
<p>和 Pytorch 编程不太一样，Tensor 是以<strong>指针</strong>的形式传递给 Triton Kernel 的。这一设计的初衷是为了<strong>方便用户可以更精确地控制读取哪些元素</strong>。</p>

<p>我们可以把显存理解为一个巨大的一维数组，而<strong>指针</strong>是这个数组中的一个索引。
类似索引，我们可以通过对指针加上或者减去一个整数来访问显存中的不同位置。</p>

<p>在正式介绍 Triton 中的 Tensor 的读取和存储之前，我们先来介绍一些前置的知识。
如果读者对 Tensor 是如何在显存中布局的已经很熟悉，同时也了解 Strides 的含义，可以直接跳过下面两个小节。</p>

<h3 id="tensor-在显存中的布局">Tensor 在显存中的布局</h3>

<p>当我们新建一个 Tensor 时，无论这个 Tensor 是几维的，它都会被连续地被存储在显存中。</p>

<p><img src="/assets/img/learning-trition-0/memory_space-3.png" alt="memory_space.png" title="一个新建的 (2 * 3) 矩阵在显存中的布局" /></p>

<p>当我们使用 <code class="language-plaintext highlighter-rouge">x = torch.empty((2, 3))</code> 新建一个 Tensor 时，如上图所示，这个新建的矩阵会在内存中申请一块连续的空间，然后按照行优先(也即，先排右侧维度)的顺序，将元素依次存储在显存中。</p>

<h3 id="sizes-和-strides">Sizes 和 Strides</h3>

<p><strong>Size</strong> 相信大家都比较熟悉，它表示 Tensor 的形状。
<strong>Stride</strong> 这个词在英文中有步伐的意思，它定义了在每一维度，当前维度的索引加一后 (例如 <code class="language-plaintext highlighter-rouge">x[0, 0]</code> -&gt; <code class="language-plaintext highlighter-rouge">x[1, 0]</code>) 对应的元素在显存中的地址需要相应地向前移动多少步。</p>

<p><img src="/assets/img/learning-trition-0/sizes_and_strides.png" alt="sizes_and_strides.png" title="Sizes 和 Strides 的示意图" /></p>

<p>上图中给出了几种不同 size 和 stride 所描述的 Tensor。</p>

<p>一个矩阵按照行优先的顺序读取时，若显存地址是连续的，那么我们就认为该矩阵是连续的 (contiguous)。即该矩阵是按照一个<strong>约定俗成</strong>的顺序连续地存储在显存中的。
例如上图中的 a) 和 b) 都是连续的，而 c) 和 d) 则不是连续的。</p>

<p><strong>注意</strong>: 传入 kernel 的 Tensor 我们需要确保它是连续的。否则在 kernel 内读取时，可能会出现意想不到的错误。后续的 <a href="#注意事项">注意事项</a> 中会介绍如何确保 Tensor 是连续的方法。</p>

<p>下面我们简单介绍如何通过设置 stride 来实现一些常见的功能。</p>

<h4 id="view">View</h4>

<p>在利用 <code class="language-plaintext highlighter-rouge">view</code> 时，我们的目的是将 Tensor 转为目标形状。
假设我们希望目标 Tensor 的形状为 $(D_n, D_{n-1}, \cdots, D_1)$。
这时候我们只需要将 <code class="language-plaintext highlighter-rouge">stride</code> 设置为 $(\prod_{i=1}^{n-1} D_i, \prod_{i=1}^{n-2} D_i, \cdots, D_1, 1)$ 即可。
例如，对于一个维度为 $(2, 3, 4)$ 的张量 <code class="language-plaintext highlighter-rouge">stride</code> 应该设置为 $(3 \times 4, 4, 1)$。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>

<span class="n">m</span> <span class="o">=</span> <span class="mi">2</span>
<span class="n">n</span> <span class="o">=</span> <span class="mi">3</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">m</span> <span class="o">*</span> <span class="n">n</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">.</span><span class="nf">view</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">))</span>
<span class="nf">print</span><span class="p">(</span>
    <span class="n">x</span><span class="p">.</span><span class="nf">as_strided</span><span class="p">(</span>
        <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">),</span>
        <span class="n">stride</span><span class="o">=</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
    <span class="p">)</span>
<span class="p">)</span>

<span class="n">k</span> <span class="o">=</span> <span class="mi">4</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">m</span> <span class="o">*</span> <span class="n">n</span> <span class="o">*</span> <span class="n">k</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">.</span><span class="nf">view</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">k</span><span class="p">))</span>
<span class="nf">print</span><span class="p">(</span>
    <span class="n">x</span><span class="p">.</span><span class="nf">as_strided</span><span class="p">(</span>
        <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">k</span><span class="p">),</span>
        <span class="n">stride</span><span class="o">=</span><span class="p">(</span><span class="n">n</span> <span class="o">*</span> <span class="n">k</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
    <span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<h4 id="expand">Expand</h4>

<p>在深度学习中，我们经常需要扩展某个维度并重复该维度上的元素形成新的形状。
例如，对于一个维度为 $(2, 3, 4)$ 的张量 <code class="language-plaintext highlighter-rouge">x</code>，我们希望将其扩展为 $(10, 2, 3, 4)$。
即我们希望新的矩阵 <code class="language-plaintext highlighter-rouge">y</code> 中 <code class="language-plaintext highlighter-rouge">y[0]</code>, <code class="language-plaintext highlighter-rouge">y[1]</code>, … , <code class="language-plaintext highlighter-rouge">y[9]</code> 都等于 <code class="language-plaintext highlighter-rouge">x</code>。
这一操作就可以通过将第 0 维度上的 stride 设置为 0 来实现。</p>

<p>下面是一个简单的维度扩展例子:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>

<span class="n">m</span> <span class="o">=</span> <span class="mi">3</span>
<span class="n">n</span> <span class="o">=</span> <span class="mi">4</span>
<span class="n">k</span> <span class="o">=</span> <span class="mi">10</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">m</span> <span class="o">*</span> <span class="n">n</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">[...,</span> <span class="bp">None</span><span class="p">].</span><span class="nf">expand</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">k</span><span class="p">))</span>
<span class="nf">print</span><span class="p">(</span>
    <span class="n">x</span><span class="p">.</span><span class="nf">as_strided</span><span class="p">(</span>
        <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">k</span><span class="p">),</span>
        <span class="n">stride</span><span class="o">=</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span>
    <span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<h4 id="transpose">Transpose</h4>

<p>转置，也就是交换行列，是一个非常常见的操作，通过修改 stride 可以很方便地实现。
如果我们需要将一个矩阵转置，我们只需要交换我们需要 transposed 的两个维度所对应的 stride 即可。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>

<span class="n">m</span> <span class="o">=</span> <span class="mi">3</span>
<span class="n">n</span> <span class="o">=</span> <span class="mi">4</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">m</span> <span class="o">*</span> <span class="n">n</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">.</span><span class="n">T</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span>
    <span class="n">x</span><span class="p">.</span><span class="nf">as_strided</span><span class="p">(</span>
        <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">m</span><span class="p">),</span>
        <span class="n">stride</span><span class="o">=</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">m</span><span class="p">),</span>
    <span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<h4 id="diagonal">Diagonal</h4>

<p>最后一个例子展示如何通过设置 stride 来获取矩阵的对角元素。
对于在 $n \times n$ 矩阵中的一个对角元素 $(i, i)$，它的下一个元素应该在 $(i + 1, i + 1)$ 的位置。即在矩阵中下移一步 ($+n$) 后再右移一步 ($+1$)。因此我们只需要将 <code class="language-plaintext highlighter-rouge">stride</code> 设置为 $(n + 1,)$ 即可。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>

<span class="n">n</span> <span class="o">=</span> <span class="mi">4</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">n</span> <span class="o">*</span> <span class="n">n</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">.</span><span class="nf">diagonal</span><span class="p">())</span>
<span class="nf">print</span><span class="p">(</span>
    <span class="n">x</span><span class="p">.</span><span class="nf">as_strided</span><span class="p">(</span>
        <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">n</span><span class="p">,),</span>
        <span class="n">stride</span><span class="o">=</span><span class="p">(</span><span class="n">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,),</span>
    <span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="triton-中的读与写">Triton 中的读与写</h3>

<p>在 <code class="language-plaintext highlighter-rouge">triton.language</code> 中提供了 <code class="language-plaintext highlighter-rouge">load</code> 和 <code class="language-plaintext highlighter-rouge">store</code> 两个函数，用于从显存中读取和写入数据。</p>

<p>最为基础的用法是读取或写入指针指向的元素。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">x</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">x_ptr</span><span class="p">)</span>     <span class="c1"># 读取指针指向的元素
</span><span class="n">tl</span><span class="p">.</span><span class="nf">store</span><span class="p">(</span><span class="n">x_ptr</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="c1"># 写入指针指向的元素
</span></code></pre></div></div>

<p><strong>需要注意的是，与我们平常写 Python 代码不一样，我们并不是使用 <code class="language-plaintext highlighter-rouge">return</code> 来返回结果，而是使用 <code class="language-plaintext highlighter-rouge">tl.store</code> 来将结果写入到结果指针指向的元素。</strong>
这个结果指针也应该是我们在调用 Kernel 时传入的参数之一。
下面是一个简单的例子，它将一个 Tensor <code class="language-plaintext highlighter-rouge">x</code> 复制到另一个 Tensor <code class="language-plaintext highlighter-rouge">y</code> 中：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">triton.language</span> <span class="k">as</span> <span class="n">tl</span> <span class="c1"># 简单起见我们利用 `tl` 来指代 `triton.language` 模块
</span><span class="kn">import</span> <span class="n">triton</span>
<span class="kn">import</span> <span class="n">torch</span>

<span class="nd">@triton.jit</span>
<span class="k">def</span> <span class="nf">copy_kernel</span><span class="p">(</span>
    <span class="n">x_ptr</span><span class="p">,</span>
    <span class="n">y_ptr</span>
<span class="p">):</span>
    <span class="n">pid</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">program_id</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">value</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">x_ptr</span> <span class="o">+</span> <span class="n">pid</span><span class="p">)</span>
    <span class="n">tl</span><span class="p">.</span><span class="nf">store</span><span class="p">(</span><span class="n">y_ptr</span> <span class="o">+</span> <span class="n">pid</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="n">n</span> <span class="o">=</span> <span class="mi">10</span>
    <span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">randn</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="sh">"</span><span class="s">cuda</span><span class="sh">"</span><span class="p">)</span>      <span class="c1"># 在 GPU 上分配内存
</span>    <span class="n">y</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">empty</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="sh">"</span><span class="s">cuda</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">copy_kernel</span><span class="p">[(</span><span class="n">n</span><span class="p">,</span> <span class="p">)](</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>           <span class="c1"># y = x.clone()
</span></code></pre></div></div>
<p>这里的 <code class="language-plaintext highlighter-rouge">y</code> 实际上是操作的<strong>输出</strong>，但是我们需要在执行 Kernel 之前为它分配空间（<strong>在 GPU 上</strong>），并作为参数传递给 Kernel。</p>

<h4 id="批量读写">批量读写</h4>

<p>然而在大部分情况下，我们并不仅仅希望只对单个元素进行操作，而是希望像在 Pytorch 中那样对一个 Tensor 进行操作。</p>

<p>下面将介绍三种在 Triton 中通过指针来访问 Tensor 的方法。</p>

<p><strong>多维指针</strong></p>

<p>第一种方式是通过多维指针的形式来描述 Tensor 的形状。
简单来说我们算出 Tensor 中每个元素的地址，组成一个地址矩阵，然后通过这个地址矩阵来访问 Tensor 中的元素。
值得注意的是，为了正确还原 Tensor 的形状，我们需要将 Tensor 的形状信息传递给 Kernel，例如在下面例子中，我们通过 <code class="language-plaintext highlighter-rouge">N</code> 和 <code class="language-plaintext highlighter-rouge">M</code> 来传递一个形状为 $(N, M)$ 的 Tensor 的形状信息。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@triton.jit</span>
<span class="k">def</span> <span class="nf">plus_one_kernel</span><span class="p">(</span>
        <span class="n">x_ptr</span><span class="p">,</span>
        <span class="n">N</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
        <span class="n">M</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
        <span class="n">BLOCK_N</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
        <span class="n">BLOCK_M</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
    <span class="p">):</span>
    <span class="n">n_id</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">program_id</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">m_id</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">program_id</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

    <span class="n">row_offset</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">BLOCK_N</span><span class="p">)</span> <span class="o">+</span> <span class="n">n_id</span> <span class="o">*</span> <span class="n">BLOCK_N</span>
    <span class="n">col_offset</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">BLOCK_M</span><span class="p">)</span> <span class="o">+</span> <span class="n">m_id</span> <span class="o">*</span> <span class="n">BLOCK_M</span>

    <span class="n">index</span> <span class="o">=</span> <span class="n">row_offset</span><span class="p">[:,</span> <span class="bp">None</span><span class="p">]</span> <span class="o">*</span> <span class="n">M</span> <span class="o">+</span> <span class="n">col_offset</span><span class="p">[</span><span class="bp">None</span><span class="p">,</span> <span class="p">:]</span>
    <span class="n">block_ptr</span> <span class="o">=</span> <span class="n">x_ptr</span> <span class="o">+</span> <span class="n">index</span>
    <span class="n">mask</span> <span class="o">=</span> <span class="p">(</span><span class="n">row_offset</span><span class="p">[:,</span> <span class="bp">None</span><span class="p">]</span> <span class="o">&lt;</span> <span class="n">N</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">col_offset</span><span class="p">[</span><span class="bp">None</span><span class="p">,</span> <span class="p">:]</span> <span class="o">&lt;</span> <span class="n">M</span><span class="p">)</span>

    <span class="n">x</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">block_ptr</span><span class="p">,</span> <span class="n">mask</span><span class="o">=</span><span class="n">mask</span><span class="p">)</span> <span class="c1"># 读取 mask 为 True 的元素
</span>    <span class="n">tl</span><span class="p">.</span><span class="nf">store</span><span class="p">(</span><span class="n">block_ptr</span><span class="p">,</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">mask</span><span class="o">=</span><span class="n">mask</span><span class="p">)</span> <span class="c1"># 写入 mask 为 True 的元素
</span></code></pre></div></div>

<p>此外在这个代码中，我们还额外传递了 <code class="language-plaintext highlighter-rouge">BLOCK_N</code> 和 <code class="language-plaintext highlighter-rouge">BLOCK_M</code> 两个参数。这是因为我们在代码使用了 <code class="language-plaintext highlighter-rouge">tl.arange</code> 这一创建 Tensor 的函数。
而在 Triton 中，所有的创建 Tensor 的函数都需要 <strong>确保每一维度的大小都是 2 的幂</strong>。
为了避免 <code class="language-plaintext highlighter-rouge">N</code> 和 <code class="language-plaintext highlighter-rouge">M</code> 不是 2 的幂而出现错误，我们需要利用 <code class="language-plaintext highlighter-rouge">triton.next_power_of_2</code> 来对 <code class="language-plaintext highlighter-rouge">N</code> 和 <code class="language-plaintext highlighter-rouge">M</code> 进行向上取整。此外在 <code class="language-plaintext highlighter-rouge">N</code> 和 <code class="language-plaintext highlighter-rouge">M</code> 太大的时候，我们可以通过设置一个较小的 <code class="language-plaintext highlighter-rouge">BLOCK_N</code> 和 <code class="language-plaintext highlighter-rouge">BLOCK_M</code> 将大 Tensor 分为多个小矩阵并行处理。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">BLOCK_N</span> <span class="o">=</span> <span class="n">triton</span><span class="p">.</span><span class="nf">next_power_of_2</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>
<span class="n">BLOCK_M</span> <span class="o">=</span> <span class="n">triton</span><span class="p">.</span><span class="nf">next_power_of_2</span><span class="p">(</span><span class="n">M</span><span class="p">)</span>
</code></pre></div></div>

<p>由于 <code class="language-plaintext highlighter-rouge">BLOCK_N</code> 和 <code class="language-plaintext highlighter-rouge">BLOCK_M</code> 和 <code class="language-plaintext highlighter-rouge">N</code> 与 <code class="language-plaintext highlighter-rouge">M</code> 的值可能不一致，在计算 offset 时，我们需要利用引入一个掩码 <code class="language-plaintext highlighter-rouge">musk</code> 来确保我们只读写 <code class="language-plaintext highlighter-rouge">N</code> 和 <code class="language-plaintext highlighter-rouge">M</code> 范围内的元素。</p>

<p><img src="/assets/img/learning-trition-0/load_mask.png" alt="load_mask.png" title="mask 的示意图" /></p>

<p>上图是我们加载 $(2, 3)$ 矩阵的示意图。由于 <code class="language-plaintext highlighter-rouge">BLOCK_M</code> 需要设置为 $2$ 的幂 ($4$), 因此在计算 offset 时，每一行的末尾会多出 $1$ 个元素。我们需要通过 <code class="language-plaintext highlighter-rouge">mask</code> 来确保屏蔽掉这些多出的元素。</p>

<p><strong>块指针 (Block Pointer)</strong></p>

<p>我们可以看到上面一种方法写起来是非常麻烦的。
第二种方式是利用块指针 (Block Pointer) 来描述 Tensor 的形状。
它为我们提供了更简洁的写法，我们不再需要手动地构建指针矩阵和手动设置 mask。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@triton.jit</span>
<span class="k">def</span> <span class="nf">plus_one_kernel</span><span class="p">(</span>
        <span class="n">x_ptr</span><span class="p">,</span>
        <span class="n">N</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
        <span class="n">M</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
        <span class="n">BLOCK_N</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
        <span class="n">BLOCK_M</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
    <span class="p">):</span>
    <span class="n">n_id</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">program_id</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">m_id</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">program_id</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

    <span class="n">block_ptr</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">make_block_ptr</span><span class="p">(</span>
        <span class="n">x_ptr</span><span class="p">,</span> <span class="c1"># 父 Tensor 的指针，指向第一个元素
</span>        <span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="n">M</span><span class="p">),</span> <span class="c1"># 描述父 Tensor 的形状
</span>        <span class="n">strides</span><span class="o">=</span><span class="p">(</span><span class="n">M</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="c1"># 描述父 Tensor 的 strides
</span>        <span class="n">offsets</span><span class="o">=</span><span class="p">(</span><span class="n">n_id</span> <span class="o">*</span> <span class="n">BLOCK_N</span><span class="p">,</span> <span class="n">m_id</span> <span class="o">*</span> <span class="n">BLOCK_M</span><span class="p">),</span> <span class="c1"># 描述每个维度上的偏移量
</span>        <span class="n">block_shape</span><span class="o">=</span><span class="p">(</span><span class="n">BLOCK_N</span><span class="p">,</span> <span class="n">BLOCK_M</span><span class="p">),</span> <span class="c1"># 描述块的大小
</span>        <span class="n">order</span><span class="o">=</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="c1"># 描述在原始 Tensor 中每一维度的顺序。例如如果 strides 为 (1, M), 既转置后的 `x`，这时候 order 应该设置为 (0, 1)
</span>    <span class="p">)</span>

    <span class="n">x</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">block_ptr</span><span class="p">,</span> <span class="n">boundary_check</span><span class="o">=</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span>
    <span class="n">tl</span><span class="p">.</span><span class="nf">store</span><span class="p">(</span><span class="n">block_ptr</span><span class="p">,</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">boundary_check</span><span class="o">=</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span>
</code></pre></div></div>

<p><strong>张量描述符 (Tensor Descriptor)</strong></p>

<p>最后一种方法是利用张量描述符 (Tensor Descriptor) 来描述 Tensor 的形状并进行加载。
从用法来看它和块指针非常相似，但是它利用了 <a href="https://pytorch.org/blog/hopper-tma-unit/">TMA 技术</a> 来进一步压榨 GPU 的性能。</p>

<p>该方法在 <code class="language-plaintext highlighter-rouge">3.3.0</code> 版本中作为实验 API (<code class="language-plaintext highlighter-rouge">_experimental_make_tensor_descriptor</code>) 被加入，并在 <code class="language-plaintext highlighter-rouge">3.4.0</code> 中成为正式 API (<code class="language-plaintext highlighter-rouge">make_tensor_descriptor</code>)
值得注意的是，只有在 Hopper 之后的 GPU，即 H 系列和 B 系列以后的 GPU，才支持 TMA 技术。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@triton.jit</span>
<span class="k">def</span> <span class="nf">plus_one_kernel</span><span class="p">(</span>
        <span class="n">x_ptr</span><span class="p">,</span>
        <span class="n">N</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
        <span class="n">M</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
        <span class="n">BLOCK_N</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
        <span class="n">BLOCK_M</span><span class="p">:</span> <span class="n">tl</span><span class="p">.</span><span class="n">constexpr</span><span class="p">,</span>
    <span class="p">):</span>
    <span class="n">n_id</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">program_id</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">m_id</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">program_id</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

    <span class="n">tensor_desc</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="nf">make_tensor_descriptor</span><span class="p">(</span>
        <span class="n">x_ptr</span><span class="p">,</span> <span class="c1"># Tensor 的指针，指向第一个元素
</span>        <span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="n">M</span><span class="p">),</span> <span class="c1"># Tensor 的形状
</span>        <span class="n">strides</span><span class="o">=</span><span class="p">(</span><span class="n">M</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="c1"># Tensor 的 strides
</span>        <span class="n">block_shape</span><span class="o">=</span><span class="p">(</span><span class="n">BLOCK_N</span><span class="p">,</span> <span class="n">BLOCK_M</span><span class="p">)</span> <span class="c1"># 要处理的块的大小
</span>    <span class="p">)</span>

    <span class="n">x</span> <span class="o">=</span> <span class="n">tensor_desc</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">n_id</span> <span class="o">*</span> <span class="n">BLOCK_N</span><span class="p">,</span> <span class="n">m_id</span> <span class="o">*</span> <span class="n">BLOCK_M</span><span class="p">)</span> <span class="c1"># 根据 offset 读取指定的块
</span>    <span class="n">tensor_desc</span><span class="p">.</span><span class="nf">store</span><span class="p">(</span><span class="n">n_id</span> <span class="o">*</span> <span class="n">BLOCK_N</span><span class="p">,</span> <span class="n">m_id</span> <span class="o">*</span> <span class="n">BLOCK_M</span><span class="p">,</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="注意事项">注意事项</h3>

<p>在 Triton 中进行读写操作时，我们需要注意以下两点 <strong>输入 Tensor 是否连续</strong> 和 <strong>边界检查</strong>。</p>

<h4 id="注意输入-tensor-是否连续">注意输入 Tensor 是否连续</h4>
<p>如前文所描述的，传入 Kernel 的 Tensor 是一个指针。
若我们希望在 Kernel 中以 Tensor 的形式访问数据，我们需要利用 <code class="language-plaintext highlighter-rouge">shape</code> 和 <code class="language-plaintext highlighter-rouge">stride</code> 等形象来重构 Tensor。
在重构的时候，为了方便起见，我们一般约定 Tensor 是按照行优先的顺序存储的。即，这个 Tensor 是连续的 (contiguous)。</p>

<p>然而，如<a href="#sizes-和-strides">前文</a>中所提到的，有些操作会改变 Tensor 的连续性。因此我们在将 Tensor 传入 Kernel 之前，需要确保 Tensor 是连续的。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="ow">not</span> <span class="n">tensor</span><span class="p">.</span><span class="nf">is_contiguous</span><span class="p">():</span>
    <span class="n">tensor</span> <span class="o">=</span> <span class="n">tensor</span><span class="p">.</span><span class="nf">contiguous</span><span class="p">()</span>
</code></pre></div></div>

<p>事实上手动检查输入 Tensor 是否连续是十分烦人的，也很容易忘记。这时候使用装饰器来确保输入 Tensor 是连续的会是一个很好的选择。
这里十分推荐参考 Flash-linear-attention 中的 <a href="https://github.com/fla-org/flash-linear-attention/blob/b1d766994c7ac53c4d0a53a1b6e8f94de363abe1/fla/utils.py#L131"><code class="language-plaintext highlighter-rouge">input_guard</code></a> 装饰器。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">input_guard</span><span class="p">(</span>
    <span class="n">fn</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[...,</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">]</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Callable</span><span class="p">[...,</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">]:</span>
    <span class="sh">"""</span><span class="s">
    A decorator to make sure all input tensors are contiguous and set the device based on input tensors.
    </span><span class="sh">"""</span>

    <span class="nd">@functools.wraps</span><span class="p">(</span><span class="n">fn</span><span class="p">)</span>
    <span class="k">def</span> <span class="nf">wrapper</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
        <span class="n">contiguous_args</span> <span class="o">=</span> <span class="p">(</span><span class="n">i</span> <span class="k">if</span> <span class="ow">not</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">)</span> <span class="k">else</span> <span class="n">i</span><span class="p">.</span><span class="nf">contiguous</span><span class="p">()</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">args</span><span class="p">)</span>
        <span class="n">contiguous_kwargs</span> <span class="o">=</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="p">(</span><span class="n">v</span> <span class="k">if</span> <span class="ow">not</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">)</span> <span class="k">else</span> <span class="n">v</span><span class="p">.</span><span class="nf">contiguous</span><span class="p">())</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">kwargs</span><span class="p">.</span><span class="nf">items</span><span class="p">()}</span>

        <span class="n">tensor</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="k">for</span> <span class="n">arg</span> <span class="ow">in</span> <span class="n">args</span><span class="p">:</span>
            <span class="k">if</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">arg</span><span class="p">,</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">):</span>
                <span class="n">tensor</span> <span class="o">=</span> <span class="n">arg</span>
                <span class="k">break</span>
        <span class="k">if</span> <span class="n">tensor</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">kwargs</span><span class="p">.</span><span class="nf">values</span><span class="p">():</span>
                <span class="k">if</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">):</span>
                    <span class="n">tensor</span> <span class="o">=</span> <span class="n">value</span>
                    <span class="k">break</span>

        <span class="k">if</span> <span class="n">tensor</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
            <span class="n">ctx</span> <span class="o">=</span> <span class="nf">custom_device_ctx</span><span class="p">(</span><span class="n">tensor</span><span class="p">.</span><span class="n">device</span><span class="p">.</span><span class="n">index</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">ctx</span> <span class="o">=</span> <span class="n">contextlib</span><span class="p">.</span><span class="nf">nullcontext</span><span class="p">()</span>

        <span class="k">with</span> <span class="n">ctx</span><span class="p">:</span>
            <span class="k">return</span> <span class="nf">fn</span><span class="p">(</span><span class="o">*</span><span class="n">contiguous_args</span><span class="p">,</span> <span class="o">**</span><span class="n">contiguous_kwargs</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">wrapper</span>
</code></pre></div></div>

<h4 id="边界检查">边界检查</h4>
<p>在 Kernel 中，我们可以通过指针访问到整个显存空间。如果不多加小心，例如没有检查边界时，计算错误的偏移量，可能会导致 Kernel 访问到不属于它的数据，甚至修改其他数据。因此在读写，尤其是写的时候，需要格外注意检查边界。</p>

<p><strong>不进行边界检查会如何？</strong>
为了探讨这一问题我们看下面的代码。下面的代码实现一个非常简单的功能：为矩阵中第 offset 个元素赋值。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">triton.language</span> <span class="k">as</span> <span class="n">tl</span>
<span class="kn">import</span> <span class="n">triton</span>
<span class="kn">import</span> <span class="n">torch</span>

<span class="nd">@triton.jit</span>
<span class="k">def</span> <span class="nf">set_value_kernel</span><span class="p">(</span><span class="n">x_ptr</span><span class="p">,</span> <span class="n">offset</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
    <span class="c1"># 将 offset 对应的元素设置为 value
</span>    <span class="n">tl</span><span class="p">.</span><span class="nf">store</span><span class="p">(</span><span class="n">x_ptr</span> <span class="o">+</span> <span class="n">offset</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="c1"># 初始化
</span>    <span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">zeros</span><span class="p">((</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)).</span><span class="nf">to</span><span class="p">(</span><span class="sh">"</span><span class="s">cuda</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">x</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">a.shape: </span><span class="si">{</span><span class="n">a</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="s">; b.shape: </span><span class="si">{</span><span class="n">b</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">a: </span><span class="si">{</span><span class="n">a</span><span class="si">}</span><span class="s">; b: </span><span class="si">{</span><span class="n">b</span><span class="si">}</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>

    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">对 a 进行越界赋值</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">a</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="mf">1.0</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">a: </span><span class="si">{</span><span class="n">a</span><span class="si">}</span><span class="s">; b: </span><span class="si">{</span><span class="n">b</span><span class="si">}</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>

    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">使用 triton 对 a 进行越界赋值</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">set_value_kernel</span><span class="p">[(</span><span class="mi">1</span><span class="p">,)](</span><span class="n">a</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">a: </span><span class="si">{</span><span class="n">a</span><span class="si">}</span><span class="s">; b: </span><span class="si">{</span><span class="n">b</span><span class="si">}</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>运行上面代码，我们会得到如下输出：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>a.shape: torch.Size([3]); b.shape: torch.Size([3])
a: tensor([0., 0., 0.], device='cuda:0'); b: tensor([0., 0., 0.], device='cuda:0')

对 a 进行越界赋值
index 3 is out of bounds for dimension 0 with size 3
a: tensor([0., 0., 0.], device='cuda:0'); b: tensor([0., 0., 0.], device='cuda:0')

使用 triton 对 a 进行越界赋值
a: tensor([0., 0., 0.], device='cuda:0'); b: tensor([1., 0., 0.], device='cuda:0')
</code></pre></div></div>

<p>我们可以看到，尽管我们只向 <code class="language-plaintext highlighter-rouge">set_value_kernel</code> 传递了 Tensor <code class="language-plaintext highlighter-rouge">a</code> 的指针，但是最终 <code class="language-plaintext highlighter-rouge">b</code> 也被修改了。</p>

<p>因此，在 Triton 中，我们在进行读写操作时<strong>需要格外注意</strong>，避免意外修改其他数据。</p>

<p><strong>如何避免越界？</strong></p>

<ul>
  <li>若使用<strong>多维指针</strong>的方式来进行读写，我们可以设置 mask，在 mask 中将需要读写的元素设置为 <code class="language-plaintext highlighter-rouge">True</code>，其余元素设置为 <code class="language-plaintext highlighter-rouge">False</code>，即可避免越界访问。在默认情况下，padding 位置的元素会被设置为 $0$。但是也可通过 <code class="language-plaintext highlighter-rouge">other</code> 参数来设置 padding 元素的值。</li>
  <li>若使用<strong>块指针</strong>的方式来进行读写，我们可以设置 <code class="language-plaintext highlighter-rouge">boundary_check</code> 参数来指定对哪些维度进行边界检查。在默认情况下，会使用 $0$ 来填充边界外的元素。</li>
</ul>]]></content><author><name>Houquan Zhou</name></author><category term="Triton" /><category term="手记" /><summary type="html"><![CDATA[从零入门 Triton：理解 SPMD 编程模型、kernel 概念、Tensor 在显存中的布局与 stride（view/expand/transpose），附 Python 示例与注意事项。]]></summary></entry><entry xml:lang="zh-CN"><title type="html">词法标记中的HMM和CRF</title><link href="https://cv.hqzhou.com/2018/11/28/hmm-and-crf.html" rel="alternate" type="text/html" title="词法标记中的HMM和CRF" /><published>2018-11-28T02:15:00+00:00</published><updated>2018-11-28T02:15:00+00:00</updated><id>https://cv.hqzhou.com/2018/11/28/hmm-and-crf</id><content type="html" xml:base="https://cv.hqzhou.com/2018/11/28/hmm-and-crf.html"><![CDATA[<h2 id="前言">前言</h2>
<p>第一次与HMM和CRF的接触，体验不是很好。公式中每个符号的意思都懂，但和在一起后就完全看不懂，于是虽然程序是编出来了，但是对于模型是完全没有一点理解，出了问题也是在盲目地trial and error。22号到今天，我尝试着将HMM和CRF中一些常用的公式，如向前、向后算法，推了一遍，并试图理解推导过程中产生的语义。</p>

<h2 id="隐马尔可夫模型">隐马尔可夫模型</h2>
<h4 id="定义">定义</h4>
<blockquote>
  <p>隐马尔可夫模型是关于时态的概率模型，描述有一个隐藏的马尔可夫链随机生成不可观测的状态随机序列，再由各个状态生成一个观测状态的过程。<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>
</blockquote>

<p>首先什么是马尔可夫链？马尔可夫链就是一个状态机，状态机由当前状态 $Y_t$ 根据给定概率 $\mathcal{T}$ 随机地转移到下一个状态 $Y_{t+1}$，从而产生一条态随机序列。</p>

<p>下面给出一个包含5个状态 $Y \in \lbrace y_1,y_2,y_3,y_4,y_5\rbrace$ 的状态机，其中 $\mathcal{T}(y_4 \to y_5)$ 表示马尔可夫链中由状态 $y_4$ 转移到状态 $y_5$ 的概率</p>

<p><img src="/assets/img/hmm-and-crf/markov-chains.png" alt="markov-chains" /></p>

<p>下面是该马尔可夫链可能生成的一条状态随机序列</p>

<p><img src="/assets/img/hmm-and-crf/state-sequence.png" alt="state-sequence" /></p>

<p>在隐马尔可夫模型中，我们假设由马尔可夫链生成的隐藏状态我们是无法直接观测到的，我们只能够观察到由这些隐藏状态随机生成的观测量。</p>

<p><img src="/assets/img/hmm-and-crf/hmm-model.svg" alt="hmm-model" /></p>

<p>在日常生活中我们也能够找到这样的例子，我们用水壶煮水，水的温度我们往往是无法观测的<em>(在这里我们使用的水壶，是最简单的那种纯粹的不锈钢水壶没有任何的附加功能)</em>，但是有些状态我们是可以观察到的，比如：水壶发出的声响、壶口冒出的水汽。通过隐马尔可夫模型我们对水温随着时间的变化和水温和声响之间进行建模，我们就可以通过发出的响声来估算水温。另外，在某些领域，就算状态量可以被直接测量，但是因为仪器原因，或者由于线路干扰，最终测量人员看到的数值和原始数值会存在出入，通过隐马尔可夫模型我们也能在一定程度上消除这样的误差或干扰。<br />
在词性标注任务中，我们可以将词性作为无法被直接观测到的隐藏状态，而将词语作为观测值。下面我们都以词性标注为例子讲解隐马尔可夫模型中的三个问题。</p>

<h4 id="三个基本问题">三个基本问题</h4>
<p>隐马尔可夫模型有三个基本问题：概率计算问题、预测问题和学习问题。<br />
为了方便计算，我对隐马尔可夫模型进行一些拓展，其中 $\text{Start}$ 表示开始状态，$\text{Stop}$ 表示结束状态，隐藏状态 $\lbrace Y_1,\dotsc,Y_n\rbrace$ 为词性标注，观测状态 $\lbrace S_1,\dotsc,S_n\rbrace$ 为句子 $S$ 位于位置 $i$ 的词语，我们规定 $P(\text{Start}) = P(\text{Stop}) = 1$</p>

<p><img src="/assets/img/hmm-and-crf/hmm-pos-model.png" alt="hmm-pos-model" /></p>

<h4 id="概率计算问题">概率计算问题</h4>
<p>概率计算问题是给定句子 $S=(s_1,s_2,\dotsc,s_n)$ 计算 $P(S)$，其实这在词性标注中用得不多， 但是会出现在使用隐马尔可夫模型解决其他问题中。<br />
根据概率图模型，我们可很容易地求出整个模型的联合分布，而 $P(S)$ 就是在这个联合分布上求边缘分布了</p>

<p>$$P(S) = P(S_1,S_2,\dotsc,S_n) = \sum_{Y_1,\dotsc,Y_n}{P(S_1,\dotsc,S_n,Y_1,\dotsc,Y_n)}$$</p>

<p>其中</p>

<table>
  <tbody>
    <tr>
      <td>$$P(S_1,\dotsc,S_n,Y_1,\dotsc,Y_n) = P(Y_1</td>
      <td>\text{Start})P(S_1</td>
      <td>Y_1) \dotsm P(Y_n</td>
      <td>Y_{n-1})P(S_n</td>
      <td>Y_n)P(\text{Stop}</td>
      <td>Y_n)$$</td>
    </tr>
  </tbody>
</table>

<p>我们使用<strong>和-积变量消除</strong>可以将 $\sum_{Y_1,\dotsc,Y_n}$ 分解到乘积中如</p>

<p>$$ \begin{split}
    P(S) &amp;= \sum_{Y_1,\dotsc,Y_n}{P(S_1,\dotsc,S_n,Y_1,\dotsc,Y_n)}<br />
         &amp;= \sum_{Y_n}P(\text{Stop}|Y_n)P(S_n|Y_n)\dotsm\sum_{Y_2}P(Y_3|Y_2)P(S_2|Y_2)\sum_{Y_1}P(Y_2|Y_1)P(S_1|Y_1)P(Y_1|\text{Start})
\end{split} $$</p>

<p>我们可以看到最底层的 $\sum_{Y_1}P(S_1|Y_1)P(Y_1|\text{Start})$ 会被重复计算，因此我们可以将它的值用 $\alpha$ 存储起来，而避免重复计算，这就是动态规划的思想。</p>

<p>于是我们有</p>
<ol>
  <li>
    <p>初始值</p>

    <p>$$ \begin{split}
     \alpha_1 &amp;= P(Y_1|\text{Start})P(S_1|Y_1)<br />
             &amp;= P(\text{Start})P(Y_1|\text{Start})P(S_1|Y_1)<br />
             &amp;= P(Y_1)P(S_1|Y_1)<br />
             &amp;= P(S_1, Y_1)
 \end{split} $$</p>
  </li>
  <li>
    <p>递推 对 $i = 2,3,\dotsc,n$</p>

    <p>$$ \begin{split}
     \alpha_i &amp;= P(S_i|Y_i)\sum_{Y_{i-1}}\left(P(Y_i|Y_{i-1})\alpha_{i-1}\right)<br />
             &amp;= P(S_i|Y_i)\sum_{Y_{i-1}}\left(P(Y_i|Y_{i-1})P(S_1,\dotsc, S_{i-1}, Y_{i-1})\right)<br />
             &amp;= P(S_i|Y_i)\sum_{Y_{i-1}}P(S_1,\dotsc, S_{i-1}, Y_i, Y_{i-1})<br />
             &amp;= P(S_i|Y_i)P(S_1,\dotsc, S_{i-1}, Y_i)<br />
             &amp;= P(S_1,\dotsc, S_i, Y_i)
 \end{split} $$</p>
  </li>
  <li>
    <p>结束</p>

    <p>$$ \begin{split}
     P(S) &amp;= \sum_{Y_n}\left(P(\text{Stop}|Y_n)\alpha_{n}\right)<br />
         &amp;= \sum_{Y_n}P(S_1,\dotsc, S_n, Y_i)
         &amp;= P(S_1,\dotsc, S_n)
 \end{split} $$</p>
  </li>
</ol>

<p>从上面三个过程中我们可以很清楚地看到 $\alpha_i$ 所代表的含义是什么：位置 $i$ 的标记 $Y_i$ 与从开始到 $i$ 的词语 ${S_1,S_2,\dotsc,S_i}$ 的联合概率</p>

<p>$$ P(S_1,\dotsc, S_i, Y_i) $$</p>

<p>上述过程称为向前算法，<strong>和-积变量消除</strong>与随机变量直接的顺序无关因此 $P(S)$ 使用<strong>和-积变量消除</strong>还能 $\sum_{Y_1,\dotsc,Y_n}$ 分解成如下形式</p>

<p>$$ \begin{split}
    P(S) &amp;= \sum_{Y_1,\dotsc,Y_n}{P(S_1,\dotsc,S_n,Y_1,\dotsc,Y_n)}<br />
         &amp;= \sum_{Y_1}P(Y_1|\text{Start})P(S_1|Y_1)\sum_{Y_2}P(Y_1|Y_2)P(S_2|Y_2) \dotsm \sum_{Y_n}P(Y_n|Y_{n-1})P(S_n|Y_n)P(\text{Stop}|Y_n)
\end{split} $$</p>

<p>对这个公式我们有</p>

<ol>
  <li>
    <p>初始值</p>

    <table>
      <tbody>
        <tr>
          <td>$$ \beta_n = P(\text{Stop}</td>
          <td>Y_n) $$</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>
    <p>递推 对 $i = n-1,n-2,\dotsc,1$</p>

    <p>$$ \begin{split}
     \beta_i &amp;= \sum_{Y_{i+1}}\left(P(Y_{i+1}|Y_i)P(S_{i+1}|Y_{i+1})\beta_{i+1}\right)<br />
             &amp;= \sum_{Y_{i+1}}\left(P(Y_{i+1}|Y_i)P(S_{i+1}|Y_{i+1})P(S_{i+2},\dotsc,S_n|Y_{i+1})\right)<br />
             &amp;= \sum_{Y_{i+1}}\left(\frac{1}{P(Y_i)}P(Y_i|Y_{i+1})P(S_{i+1}|Y_{i+1})P(S_{i+2},\dotsc,S_n, Y_{i+1})\right)<br />
             &amp;= \sum_{Y_{i+1}}\left(\frac{1}{P(Y_i)}P(Y_i|Y_{i+1})P(S_{i+1},\dotsc,S_n, Y_{i+1})\right)<br />
             &amp;= \sum_{Y_{i+1}}\left(\frac{1}{P(Y_i)}P(S_{i+1},\dotsc,S_n, Y_i, Y_{i+1})\right)<br />
             &amp;= \sum_{Y_{i+1}}P(S_{i+1},\dotsc,S_n, Y_{i+1}|Y_i)<br />
             &amp;= P(S_{i+1},\dotsc,S_n|Y_i)
 \end{split} $$</p>
  </li>
  <li>
    <p>结束</p>

    <p>$$ \begin{split}
     P(S) &amp;= \sum_{Y_1}\left(P(Y_1|\text{Start})P(S_1|Y_1)\beta_1\right)<br />
          &amp;= \sum_{Y_1}P(S_1,\dotsc,S_n|Y_1)
          &amp;= P(S_1,\dotsc, S_n)
 \end{split} $$</p>
  </li>
</ol>

<p>其中 $\beta_i$ 表达的物理内涵为：从 $i+1$ 到结束的词语 ${S_1,S_2,\dotsc,S_i}$ 在给定位置 $i$ 的标记 $Y_i$ 的情况下的联合条件概率</p>

<table>
  <tbody>
    <tr>
      <td>$$ P(S_{i+1},\dotsc,S_n</td>
      <td>Y_i) $$</td>
    </tr>
  </tbody>
</table>

<p>值得我们注意的是，向前和向后算法的中间变量所表达的物理含义是不同的，$\alpha$ 表达的是一个联合分布，而 $\beta$ 则表示了一个条件分布</p>

<p>$$\begin{split} 
\beta_i &amp;= P(S_{i+1},\dotsc,S_n|Y_i) <br />
\alpha_i &amp;= P(S_1,\dotsc, S_i, Y_i)
\end{split}$$</p>

<p>在隐马尔可夫模型中有一个很好的性质，方便我们的计算，那就是给定 $Y_j$ 的条件下会阻断所有途径 $Y_j$ 的概率影响，即存在着很多有关 $Y_j$ 的条件独立</p>

<p><img src="/assets/img/hmm-and-crf/condition-block.png" alt="condition-block" /></p>

<p>因为存在 $S_i$ 与 $\mathbf S_{-i}$在给定 $Y_i$ 的条件下独立，即 $(S_i\bot \mathbf S_{-i} |Y_i)$，其中 $S_{-i}$ 代表模型 $S$ 中除位置 $i$ 外的其他观察变量的集合,我们有如下公式</p>

<p>$$ \begin{split}
    &amp;P(S_i|Y_i)P(\mathbf{S}_{-i}, Y_i)<br />
    = &amp;P(S_i|Y_i)P(\mathbf{S}_{-i}|Y_i)P(Y_i)<br />
    = &amp;P(\mathbf{S}_{-i}, S_i|Y_i)P(Y_i)<br />
    = &amp;P(\mathbf{S}_{-i}, S_i, Y_i)<br />
\end{split} $$</p>

<p>同理对任意 $i &lt; j &lt; k$ 有 任意 $Y_i$ 在给定 $Y_j$ 的条件下与 $S_j$ 和  $S_k$ 独立，即 $(S_k, S_j\bot Y_i |Y_j)$ ，对应的也有 $(S_i, S_j\bot Y_k |Y_j)$，所以我们有公式</p>

<p>$$ \begin{equation}
\begin{split}
    &amp;P(Y_i|Y_j)P(S_k, S_{k+1}, \dotsc, S_n, Y_j)<br />
    = &amp;P(Y_i|Y_j)P(S_k, S_{k+1}, \dotsc, S_n|Y_j)P(Y_j)<br />
    = &amp;P(S_k, S_{k+1}, \dotsc, S_n, Y_i|Y_j)P(Y_j)<br />
    = &amp;P(S_k, S_{k+1}, \dotsc, S_n, Y_i, Y_j)<br />
\end{split}
\end{equation} $$</p>

<p>$$ \begin{equation}
\begin{split}
    &amp;P(Y_k|Y_j)P(S_1, S_i, \dotsc, S_k, Y_j)<br />
    = &amp;P(Y_k|Y_j)P(S_1, S_i, \dotsc, S_k|Y_j)P(Y_j)<br />
    = &amp;P(S_1, S_i, \dotsc, S_k, Y_i|Y_j)P(Y_j)<br />
    = &amp;P(S_1, S_i, \dotsc, S_k, Y_i, Y_j)<br />
\end{split}
\end{equation} $$</p>

<h4 id="预测问题">预测问题</h4>
<p>预测问题也叫做解码问题，实际上是一个最大后验查询，即给定条件变量，查询其他变量的最有可能取值<br />
在词性标注任务中，这就表现为，给定一个句子 $S$ 寻找最有可能的标记序列 $Y^\star$</p>

<p>$$\begin{split} 
    Y^\star&amp;=\mathop{\text{argmax}}_{Y}P(Y|S)<br />
    &amp;=\mathop{\text{argmax}}<em>{Y}\frac{P(Y,S)}{P(S)}<br />
    &amp;=\mathop{\text{argmax}}</em>{Y}P(Y,S)
\end{split}$$</p>

<p>在计算 $\mathop{\text{argmax}}_{Y}P(Y,S)$ 的时候我们同样也能使用<strong>最大-积变量消除</strong></p>

<p>$$ \begin{split}
    \mathop{\text{argmax}}<em>{Y}P(Y,S) &amp;= \mathop{\text{argmax}}</em>{Y_1,\dotsc,Y_n}{P(Y_1,\dotsc,Y_n,S)}<br />
         &amp;= \mathop{\text{argmax}}_{Y_n}P(\text{Stop}|Y_n)P(S_n|Y_n)\dotsm\mathop{\text{argmax}}_{Y_2}P(Y_3|Y_2)P(S_2|Y_2)\mathop{\text{argmax}}_{Y_1}P(Y_2|Y_1)P(S_1|Y_1)P(Y_1|\text{Start})
\end{split} $$</p>

<p><em>注：max是求最大值，argmax是最大值时自变量的取值</em></p>

<p>同样地我们可以使用动态规划来减少重复的计算，这个算法又叫做<strong>维比特算法</strong></p>

<ol>
  <li>
    <p>初始值</p>

    <p>$$
     \delta_1 = P(Y_1|\text{Start})P(S_1|Y_1)<br />
     \varPsi_1 = 0
 $$</p>
  </li>
  <li>
    <p>递推 对 $i = 2,3,\dotsc,n$</p>

    <p>$$ \delta_i = P(S_i|Y_i)\max_{Y_{i-1}}\left(P(Y_i|Y_{i-1})\cdot\delta_{i-1}\right) <br />
 \begin{split}
     \varPsi_i &amp;= P(S_i|Y_i)\mathop{\text{argmax}}_{Y_{i-1}}\left(P(Y_i|Y_{i-1})\cdot\delta_{i-1}\right)<br />
     &amp;= \mathop{\text{argmax}}_{Y_{i-1}}\left(P(Y_i|Y_{i-1})\cdot\delta_{i-1}\right)
 \end{split}
 $$</p>
  </li>
  <li>
    <p>结束</p>

    <p>$$ 
     \max_{Y}P(Y|S) = \max_{Y_n}\delta_{n}<br />
     Y_n^\star=\mathop{\text{argmax}}<em>{Y_n}\delta</em>{n}
 $$</p>
  </li>
  <li>
    <p>最优路径回溯 对 $i = n-1,n-2,\dotsc,1$</p>

    <p>$$
     Y_i^\star=\varPsi_{i+1}(Y_{i+1}^*)
 $$</p>
  </li>
</ol>

<p>对此我们可以得到最优的词性标注序列 $Y^\star={Y_1^\star,Y_2^\star,\dotsc,Y_n^\star}$</p>

<h4 id="学习问题">学习问题</h4>

<p>有向图模型的学习十分简单。我们利用极大似然估计就可以完成学习，也就是说，我们只需要在训练集中统计得出，转移概率 $P(Y_i|Y_{i-1})$ 和发射概率 $P(S_i|Y_i)$ 即可。</p>

<h2 id="条件随机场">条件随机场</h2>
<h4 id="定义-1">定义</h4>
<blockquote>
  <p>条件随机场是给定随机变量 $S$ 条件下，随机变量 $Y$ 的马尔可夫随机场。<sup id="fnref:1:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>
</blockquote>

<p>其实我觉得条件随机场与马尔可夫网模型，在结构上没有什么太大的区别，只不过最终马尔可夫随机场使用其联合分布，而条件随机场使用其条件分布罢了</p>

<p>马尔可夫网中的联合分布</p>

<p>$$ 
P(S,Y)=\frac{1}{Z}\prod_{\mathbf{D}}\phi(\mathbf{D})<br />
Z=\sum_{S,Y}\prod_{\mathbf{D}}\phi(\mathbf{D})<br />
$$</p>

<p>条件随机场中的条件分布</p>

<p>$$
P(Y|S)=\frac{1}{Z(X)}\prod_{\mathbf{D}}\phi(\mathbf{D})<br />
Z(X)=\sum_{Y}\prod_{\mathbf{D}}\phi(\mathbf{D})
$$</p>

<p>我们可以看到这两个分布是可以相互转换的</p>

<p>$$\begin{split}
P(Y|S)&amp;=\frac{1}{Z(X)}\prod_{\mathbf{D}}\phi(\mathbf{D})<br />
&amp;=\frac{\prod_{\mathbf{D}}\phi(\mathbf{D})}{\sum_{Y}\prod_{\mathbf{D}}\phi(\mathbf{D})}<br />
&amp;=\frac{\frac{1}{Z}\prod_{\mathbf{D}}\phi(\mathbf{D})}{\frac{1}{Z}\sum_{Y}\prod_{\mathbf{D}}\phi(\mathbf{D})}<br />
&amp;=\frac{P(S,Y)}{P(S)}=\frac{P(S,Y)}{\sum_{Y}P(S,Y)}<br />
\end{split}
$$</p>

<p>在词性标注中我们常用的是线性链条件随机场，在线性链随机场中句子 $S$ 不再像在隐马尔可夫模型那样展开到每一个位置，而是作为一个整体出现，将句子的信息尽可能地保留</p>

<p><img src="/assets/img/hmm-and-crf/crf-model.png" alt="crf-model" /></p>

<h4 id="线性链条件随机场的分布">线性链条件随机场的分布</h4>
<p>由于条件随机场是无向图模型，因此它的分布不能像隐马尔可夫模型那样，计算条件分布的乘积即可。而是需要计算团上的因子的乘积。在线性链条件随机场中，由于模型的形状特别好——图可以被分为许多个以 ${Y_{i-1},Y_i,S}$ 构成的三角形团。因此我们可以将线性链条件随机场中的分布表示为</p>

<p>$$
    P(Y|S)=\frac{1}{Z(S)}\tilde{P}(Y,S)<br />
    \tilde{P}(Y,S)=\prod_{i=1}^{n+1}\phi(Y_i,Y_{i-1},S)<br />
    Z(S)=\sum_Y\tilde{P}(Y,S)
$$</p>

<p>其中 $\tilde{P}(Y,S)$ 称为未归一化概率，同时 $Y_0=\text{Start}$ 类似的 $Y_{n+1}=\text{Stop}$ ，但我们观察上式，我们还是不能像隐马尔可夫模型中那样清晰地理解因子的具体内涵。于是乎，我们可以更进一步地改造因子，即将其转换到对数空间中去</p>

<p>$$ \phi(Y_i,Y_{i-1},S)=\exp(\mathop{\mathcal{Score}}(S,i,Y_{i-1},Y_i)) $$</p>

<p>其中函数 $\mathop{\mathcal{Score}}(S,i,Y_{i-1},Y_i)$ 是得分函数，可以被写为权重向量 $\boldsymbol{w}$ 与0-1特征向量 $\boldsymbol{f}(S,i,Y_{i-1},Y_i)$ 的点乘，即</p>

<p>$$ \mathop{\mathcal{Score}}(S,i,Y_{i-1},Y_i)=\boldsymbol{w}\cdot \boldsymbol{f}(S,i,Y_{i-1},Y_i) $$</p>

<p>特征向量 $\boldsymbol{f}$ 在词性标注任务中往往通过如下模板来生成</p>

<p>$$
\begin{array}{l}
\hline
\begin{array}{ll}
 01: y_{i} \circ y_{i-1} &amp; 02: y_{i} \circ s_{i}<br />
 03: y_{i} \circ s_{i-1} &amp; 04: y_{i} \circ s_{i+1}<br />
 05: y_{i} \circ s_{i} \circ c_{i-1,-1} &amp; 06: y_{i} \circ s_{i} \circ c_{i+1,1}<br />
 07: y_{i} \circ c_{i,1} &amp; 08: y_{i} \circ c_{i,-1}<br />
 \end{array}<br />
 \begin{array}{l}
 09: y_{i} \circ c_{i,k} , 1 &lt; k &lt; #c_{i}<br />
 10: y_{i} \circ c_{i,1} \circ c_{i,k}, 1 &lt; k &lt; #c_{i}<br />
 11: y_{i} \circ c_{i,-1} \circ c_{i,k}, 1 &lt; k &lt; #c_{i}<br />
 12: \mathbf{if}\; #c_{i} = 1 \;\mathbf{then}\; y_{i} \circ w_{i} \circ c_{i-1, -1} \circ c_{t+1,1}<br />
 13: \mathbf{if}\; c_{i, k} = c_{i, k+1} \;\mathbf{then}\; y_{i} \circ c_{i,k} \circ \mathit{“Consecutive”}<br />
 14: y_{i} \circ \mathsf{prefix}! \left(w_{i} , k\right) , 1 \leq k \leq 4 , k \leq #c_{i}<br />
 15: y_{i} \circ \mathsf{suffix}! \left(w_{i} , k\right) , 1 \leq k \leq 4 , k \leq #c_{i}<br />
 \end{array}<br />
\hline
\end{array}
$$</p>

<p>其中 $\circ$ 代表字符串串联； $s_{i}$ 表示第 $i$ 个位置上的词性标注； $y_i$ 表示位置 $i$ 上的词性标注; $c_{i, k}$ 表示第 $i$ 个词 $s_{i}$ 上第 $k$ 个位置的字符，特别地有 $c_{i, -1}$ 是 $s_{i}$ 上最后一个字符; $\#c_{i}$ 表示 $s_{i}$ 中字符的个数; $\mathsf{prefix}\! \left(w_{i} , k\right)$ 和 $\mathsf{suffix}\! \left(w_{i} , k\right)$ 分别表示着 $s_{i}$ 中长度为 $k$ 的前缀和后缀。<br />
除了使用特征模板来生成特征向量，我们也可以使用神经网络，例如LSTM和GRU等来生成。</p>

<h4 id="三个基本问题-1">三个基本问题</h4>
<p>条件随机场同样有三个基本问题：概率计算问题、预测问题和学习问题。<br />
由于在概率计算上与隐马尔可夫模型存在着差异，因此着三个问题的在解法上也存在着不同，其中概率计算问题、预测问题在思路上与隐马尔可夫模型是一致的，但在学习问题上就有着天壤之别了，这主要是因为</p>

<blockquote>
  <p>造成这种差异的最主要原因或许是马尔可夫网与贝叶斯网之间存在的关键差别——使用全局归一化常数(即配分函数)，而不是使用每个CPD中的局部归一化。这种全局化因子使得网络中的所有参数耦合，进而导致无法分解的问题以及无法分别估计局部参数。<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>
</blockquote>

<p>但幸运的时候我们有其他算法可以解决这个问题。</p>

<h4 id="概率计算问题-1">概率计算问题</h4>
<p>在条件随机场中有两个我们特别关系的概率 $P(Y_i|S)$ ，即在给定句子 $S$ 时，位置 $i$ 的标记的概率。和 $P(Y_{i-1},Y_i|S)$</p>

<p>$$\begin{split}
    P(Y_i|S) &amp;= \sum_{Y_{\neq i}}P(Y_1,Y_2,\dotsc,Y_n|S)<br />
             &amp;= \frac{1}{Z(S)}\sum_{Y_{\neq i}}\tilde{P}(Y_1,Y_2,\dotsc,Y_n,S)<br />
             &amp;= \frac{1}{Z(S)}\sum_{Y_{\neq i}}\phi(\text{Start},Y_1,S)\phi(Y_1,Y_2,S)\dotsm\phi(Y_n,\text{Stop},S)\<br />
    P(Y_{i-1},Y_i|S) &amp;= \sum_{Y_{\neq {i,i-1}}}P(Y_1,Y_2,\dotsc,Y_n,S)<br />
             &amp;= \frac{1}{Z(S)}\sum_{Y_{\neq {i,i-1}}}\tilde{P}(Y_1,Y_2,\dotsc,Y_n,S)<br />
             &amp;= \frac{1}{Z(S)}\sum_{Y_{\neq {i,i-1}}}\phi(\text{Start},Y_1,S)\phi(Y_1,Y_2,S)\dotsm\phi(Y_n,\text{Stop},S)\<br />
    Z(S) &amp;= \sum_{Y_1,Y_2,\dotsc,Y_n}\tilde{P}(Y_1,Y_2,\dotsc,Y_n,S)<br />
         &amp;= \sum_{Y_1,Y_2,\dotsc,Y_n}\phi(\text{Start},Y_1,S)\phi(Y_1,Y_2,S)\dotsm\phi(Y_n,\text{Stop},S)<br />
\end{split}$$</p>

<p>对未归一化概率 $\tilde{P}(Y_1,Y_2,\dotsc,Y_n,S)$ 的求和进行了多次，并且有着共同点</p>

<p>$$\begin{split}
     &amp;\sum_{Y_{\neq i}}\tilde{P}(Y_1,Y_2,\dotsc,Y_n,S)<br />
    =&amp;\sum_{Y_{\neq i}}\phi(\text{Start},Y_1,S)\phi(Y_1,Y_2,S)\dotsm\phi(Y_n,\text{Stop},S)<br />
    =&amp;\sum_{Y_1,\dotsc,Y_{i-1}}\phi(\text{Start},Y_1,S)\dotsm\phi(Y_{i-1},Y_i,S)\sum_{Y_{i+1},\dotsc,Y_n}\phi(Y_i,Y_i+1,S)\dotsm\phi(Y_n,\text{Stop},S)<br />
    <br />
     &amp;\sum_{Y_{\neq {i,i-1}}}\tilde{P}(Y_1,Y_2,\dotsc,Y_n,S)<br />
    =&amp;\sum_{Y_{\neq {i,i-1}}}\phi(\text{Start},Y_1,S)\phi(Y_1,Y_2,S)\dotsm\phi(Y_n,\text{Stop},S)<br />
    =&amp;\sum_{Y_1,\dotsc,Y_{i-2}}\phi(\text{Start},Y_1,S)\dotsm\phi(Y_{i-2},Y_{i-1},S)\cdot\phi(Y_{i-1},Y_i,S)\cdot\sum_{Y_{i+1},\dotsc,Y_n}\phi(Y_i,Y_i+1,S)\dotsm\phi(Y_n,\text{Stop},S)<br />
    <br />
    &amp;\sum_{Y_1,Y_2,\dotsc,Y_n}\tilde{P}(Y_1,Y_2,\dotsc,Y_n,S)<br />
    =&amp;\sum_{Y_1,Y_2,\dotsc,Y_n}\phi(\text{Start},Y_1,S)\phi(Y_1,Y_2,S)\dotsm\phi(Y_n,\text{Stop},S)<br />
    =&amp;\sum_{Y_1}\phi(\text{Start},Y_1,S)\dotsm\sum_{Y_i}\phi(Y_{i-1},Y_i,S)\sum_{Y_{i+1},\dotsc,Y_n}\phi(Y_i,Y_i+1,S)\dotsm\phi(Y_n,\text{Stop},S)<br />
    =&amp;\sum_{Y_n}\phi(Y_n,\text{Stop},S)\dotsm\sum_{Y_i}\phi(Y_i,Y_{i+1},S)\sum_{Y_1,\dotsc,Y_{i-1}}\phi(\text{Start},Y_1,S)\dotsm\phi(Y_{i-1},Y_i,S)<br />
    =&amp;\sum_{Y_1,\dotsc,Y_i}\phi(\text{Start},Y_1,S)\dotsm\phi(Y_{i-1},Y_i,S)\sum_{Y_{i+1},\dotsc,Y_n}\phi(Y_i,Y_i+1,S)\dotsm\phi(Y_n,\text{Stop},S)<br />
\end{split}$$</p>

<p>我们可以看到这三个问题都可以使用<strong>和-积变量消除</strong>分解为</p>
<ul>
  <li>向前求和 $\sum_{Y_1,\dotsc,Y_{i-1}}\phi(\text{Start},Y_1,S)\dotsm\phi(Y_{i-1},Y_i,S)$</li>
  <li>向后求和 $\sum_{Y_{i+1},\dotsc,Y_n}\phi(Y_i,Y_i+1,S)\dotsm\phi(Y_n,\text{Stop},S)$</li>
</ul>

<p>这两部分。</p>

<p>于是我们就得到了条件随机场中的<strong>向前-向后算法</strong></p>

<ol>
  <li>
    <p>初始值</p>

    <p>$$\begin{split}
 \alpha_0 &amp;= \phi(\text{Start},Y_1,S)\<br />
 \beta_{n+1} &amp;= \phi(Y_n,\text{Stop},S)<br />
 \end{split}$$</p>
  </li>
  <li>
    <p>递推 对 $i = 2,3,\dotsc,n$</p>

    <p>$$\begin{split}
 \alpha_i &amp;= \sum_{Y_{i-1}}\left(\phi(Y_{i-1},Y_i,S)\alpha_{i-1}\right)\<br />
 \beta_i &amp;= \sum_{Y_{i+1}}\left(\phi(Y_i,Y_{i+1},S)\beta_{i+1}\right)<br />
 \end{split}$$</p>
  </li>
  <li>
    <p>结束</p>

    <p>最后我们有</p>

    <p>$$\begin{split}
 \alpha_i &amp;= \sum_{Y_1,\dotsc,Y_{i-1}}\phi(\text{Start},Y_1,S)\dotsm\phi(Y_{i-1},Y_i,S)\<br />
 \beta_i &amp;= \sum_{Y_{i+1},\dotsc,Y_n}\phi(Y_i,Y_i+1,S)\dotsm\phi(Y_n,\text{Stop},S)<br />
 \end{split}$$</p>

    <p>于是我们可以利用 $\alpha$ 和 $\beta$ 很快地将 $P(Y_i|S)$ 、 $P(Y_{i-1},Y_i|S)$ 以及 $Z(S)$ 快速地求出</p>

    <p>$$\begin{split}
 P(Y_i|S) &amp;= \alpha_i\beta_i<br />
 P(Y_{i-1},Y_i|S) &amp;= \alpha_{i-1}\phi(Y_{i-1},Y_i,S)\beta_i<br />
 Z(S) &amp;= \sum_{Y_n}\alpha_n<br />
      &amp;= \sum_{Y_1}\beta_1<br />
      &amp;= \sum_{Y_i}\left(\alpha_i\beta_i\right)
 \end{split}$$</p>
  </li>
</ol>

<p>现在我们需要思考一个问题， $\alpha_i$ 与 $\beta_i$ 所代表的物理内涵是什么？观察他们的式子我们可以说</p>

<blockquote>
  <ul>
    <li>$\alpha_i$ 表示在位置 $i$ 的标记是 $Y_i$ 并且到位置 $i$ 的前部分标记序列的非规范化概率；</li>
    <li>$\beta_i$ 表示在位置 $i$ 的标记是 $Y_i$ 并且从 $i+1$ 到 $n$ 的后部分标记序列的非规范化概率；<sup id="fnref:1:2"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></li>
  </ul>
</blockquote>

<p>对于一个条件随机场 $\mathcal{H}$ 我们考虑两个子图</p>

<p><img src="/assets/img/hmm-and-crf/meanning-of-alpha-beta.png" alt="meanning-of-alpha-beta" /></p>

<p>我们可以看到 $\alpha_i$ 是在前部分子图上求未归一化概率 $\tilde{P}(Y_i,S)$，而 $\beta_i$ 是在后部分子图上求未归一化概率 $\tilde{P}(Y_i,S)$</p>

<h4 id="预测问题-1">预测问题</h4>
<p>条件随机场中的解码问题的解决方法与隐马尔可夫模型中的解法基本上是一致的，都是使用了<strong>维比特算法</strong></p>

<p>$$\begin{split} 
    Y^\star&amp;=\mathop{\text{argmax}}_{Y}P(Y|S)<br />
    &amp;=\mathop{\text{argmax}}<em>{Y}\frac{1}{Z(S)}\tilde{P}(Y,S)<br />
    &amp;=\mathop{\text{argmax}}</em>{Y}\tilde{P}(Y,S)<br />
    &amp;=\mathop{\text{argmax}}<em>{Y}\prod</em>{i=1}^{n+1}\phi(Y_i,Y_{i-1},S)<br />
    &amp;=\mathop{\text{argmax}}<em>{Y}\prod</em>{i=1}^{n+1}\exp(\mathop{\mathcal{Score}}(S,i,Y_{i-1},Y_i))<br />
    &amp;=\mathop{\text{argmax}}<em>{Y}\exp(\sum</em>{i=1}^{n+1}\mathop{\mathcal{Score}}(S,i,Y_{i-1},Y_i))<br />
    &amp;=\mathop{\text{argmax}}<em>{Y}\sum</em>{i=1}^{n+1}\mathop{\mathcal{Score}}(S,i,Y_{i-1},Y_i)<br />
    &amp;=\mathop{\text{argmax}}<em>{Y}\sum</em>{i=1}^{n+1}\boldsymbol{w}\cdot \boldsymbol{f}(S,i,Y_{i-1},Y_i)<br />
\end{split}$$</p>

<ol>
  <li>
    <p>初始值</p>

    <p>$$
     \delta_1 = \boldsymbol{w}\cdot \boldsymbol{f}(S,1,Y_1,\text{Start})<br />
 $$</p>
  </li>
  <li>
    <p>递推 对 $i = 2,3,\dotsc,n$</p>

    <p>$$ 
 \delta_i = \max_{Y_{i-1}}\left(\delta_{i-1} + \boldsymbol{w}\cdot \boldsymbol{f}(S,i,Y_{i-1},Y_i)\right) <br />
 \varPsi_i = \mathop{\text{argmax}}<em>{Y</em>{i-1}}\left(\delta_{i-1} + \boldsymbol{w}\cdot \boldsymbol{f}(S,i,Y_{i-1},Y_i)\right)<br />
 $$</p>
  </li>
  <li>
    <p>结束</p>

    <p>$$ 
     \max_{Y}P(Y|S) = \max_{Y_n}\delta_{n}<br />
     Y_n^\star=\mathop{\text{argmax}}<em>{Y_n}\delta</em>{n}
 $$</p>
  </li>
  <li>
    <p>最优路径回溯 对 $i = n-1,n-2,\dotsc,1$</p>

    <p>$$
     Y_i^\star=\varPsi_{i+1}(Y_{i+1}^*)
 $$</p>
  </li>
</ol>

<p>对此我们可以得到最优的词性标注序列 $Y^\star={Y_1^\star,Y_2^\star,\dotsc,Y_n^\star}$</p>

<h4 id="学习问题-1">学习问题</h4>
<p>由于在条件随机场中使用了全局归一化常数 $Z(S)$ 导致参数高度耦合，因此我们没法得到解析解，但是因此目标函数是凹的<sup id="fnref:2:1"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>，我们可以使用迭代方法来获取最优解。<br />
学习过程中，我们希望有 $N$ 个训练数据的训练数据集 $\mathcal{D}$ 中所有数据对 $(S^j,Y^j)$ 的条件概率 $P(S=S^j|Y=Y^j)$ 最大化，于是我们给出条件随机场的最大似然函数</p>

<p>$$\begin{split}
    \mathcal{LL}(\mathcal{D};\boldsymbol{w})&amp;=\sum_{j=1}^N\log P(Y=Y^j|S=S^j)<br />
    &amp;=\sum_{j=1}^N\log \frac{\tilde{P}(Y=Y^j,S=S^j)}{Z(S=S^j)}<br />
    &amp;=\sum_{j=1}^N\log \frac{\prod_{i=1}^{n+1}\phi(Y_i=y_i^j,Y_{i-1}=y_{i-1}^j,S=S^j)}{Z(S=S^j)}<br />
    &amp;=\sum_{j=1}^N\log \frac{\prod_{i=1}^{n+1}\exp(\mathop{\mathcal{Score}}(S=S^j,i,Y_{i-1}=y_{i-1}^j,Y_i=y_i^j))}{Z(S=S^j)}<br />
    &amp;=\sum_{j=1}^N\log \frac{\exp(\sum_{i=1}^{n+1}\mathop{\mathcal{Score}}(S=S^j,i,Y_{i-1}=y_{i-1}^j,Y_i=y_i^j))}{Z(S=S^j)}<br />
    &amp;=\sum_{j=1}^N\left(\sum_{i=1}^{n+1}\mathop{\mathcal{Score}}(S=S^j,i,Y_{i-1}=y_{i-1}^j,Y_i=y_i^j) - \log Z(S=S^j)\right)<br />
    &amp;=\sum_{j=1}^N\left(\sum_{i=1}^{n+1}\boldsymbol{w}\cdot \boldsymbol{f}(S=S^j,i,Y_{i-1}=y_{i-1}^j,Y_i=y_i^j) - \log Z(S=S^j)\right)<br />
\end{split}$$</p>

<p>接下来我们对最大似然函数对权重 $\boldsymbol{w}$ 进行求导</p>

<p>$$\begin{split}
    \frac{\partial \mathcal{LL}(\mathcal{D};\boldsymbol{w})}{\partial \boldsymbol{w}} &amp;= \sum_{j=1}^N\left(\frac{\partial\left(\sum_{i=1}^{n+1}\boldsymbol{w}\cdot \boldsymbol{f}\left(S=S^j,i,Y_{i-1}=y_{i-1}^j,Y_i=y_i^j\right)\right)}{\partial \boldsymbol{w}}-\frac{\partial\log Z(S=S^j)}{\partial \boldsymbol{w}}\right)<br />
    &amp;=\sum_{j=1}^N\left(\sum_{i=1}^{n+1}\boldsymbol{f}\left(S=S^j,i,Y_{i-1}=y_{i-1}^j,Y_i=y_i^j\right) - \frac{\partial\log Z(S=S^j)}{\partial \boldsymbol{w}}\right)<br />
\end{split}$$</p>

<p>其中 $\frac{\partial\log Z(S)}{\partial \boldsymbol{w}}$ 为</p>

<p>$$\begin{split}
    \frac{\partial\log Z(S)}{\partial \boldsymbol{w}} &amp;= \frac{Z’(S)}{Z(S)}<br />
    &amp;=\frac{1}{Z(S)}\sum_{Y_1,Y_2,\dotsc,Y_n}\left(\prod_{i=1}^{n+1}\exp\left(\boldsymbol{w}\cdot \boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)\right)\right)’<br />
    &amp;=\frac{1}{Z(S)}\sum_{Y_1,Y_2,\dotsc,Y_n}\left(\exp\left(\sum_{i=1}^{n+1}\boldsymbol{w}\cdot \boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)\right)\right)’<br />
    &amp;=\frac{1}{Z(S)}\sum_{Y_1,Y_2,\dotsc,Y_n}\left(\exp\left(\sum_{i=1}^{n+1}\boldsymbol{w}\cdot \boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)\right)\sum_{i=1}^{n+1}\boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)\right)<br />
    &amp;=\sum_{Y_1,Y_2,\dotsc,Y_n}\left(\frac{P(Y,S)}{Z(S)}\sum_{i=1}^{n+1}\boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)\right)<br />
    &amp;=\sum_{Y_1,Y_2,\dotsc,Y_n}\left(P(Y|S)\sum_{i=1}^{n+1}\boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)\right)<br />
    &amp;=\sum_{i=1}^{n+1}\sum_{Y_1,Y_2,\dotsc,Y_n}P(Y|S)\boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)<br />
    &amp;=\sum_{i=1}^{n+1}\sum_{Y_{i-1},Y_i}\left(\boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)\sum_{Y_{\neq{i,i-1}}}P(Y|S)\right)<br />
    &amp;=\sum_{i=1}^{n+1}\sum_{Y_{i-1},Y_i}\boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)P(Y_{i-1},Y_i|S)<br />
\end{split}$$</p>

<p>最后我们有</p>

<p>$$\begin{split}
\frac{\partial \mathcal{LL}(\mathcal{D};\boldsymbol{w})}{\partial \boldsymbol{w}} &amp;=\sum_{j=1}^N\left(\sum_{i=1}^{n+1}\boldsymbol{f}\left(S=S^j,i,Y_{i-1}=y_{i-1}^j,Y_i=y_i^j\right) - \sum_{i=1}^{n+1}\sum_{Y_{i-1},Y_i}\boldsymbol{f}\left(S,i,Y_{i-1},Y_i\right)P\left(Y_{i-1},Y_i|S\right)\right)<br />
    &amp;=\sum_{j=1}^N\sum_{i=1}^{n+1}\left(\boldsymbol{f}\left(S=S^j,i,Y_{i-1}=y_{i-1}^j,Y_i=y_i^j\right) - \sum_{Y_{i-1},Y_i}\boldsymbol{f}\left(S=S^j,i,Y_{i-1},Y_i\right)P\left(Y_{i-1},Y_i|S=S^j\right)\right)
\end{split}$$</p>

<p>我们根据梯度使用优化算法就可以很快，迭代出最优的权重 $\boldsymbol{w}$ 了。</p>

<h2 id="最后">最后</h2>
<p>在写这篇文章的时候，再次将涉及到的公式推导了一遍，算是重新温习了一下HMM和CRF，其中发现了许多第一次学习这两个模型时候忽略的东西，比如HMM向前和向后算法中所表达的物理含义是不同的。还有写博客真的好累，尤其是画图的时候。</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>李航. 统计学习方法[J]. 2012. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:1:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a> <a href="#fnref:1:2" class="reversefootnote" role="doc-backlink">&#8617;<sup>3</sup></a></p>
    </li>
    <li id="fn:2">
      <p>Koller D, Friedman N, Bach F. Probabilistic graphical models: principles and techniques[M]. MIT press, 2009. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:2:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
  </ol>
</div>]]></content><author><name>Houquan Zhou</name></author><category term="概率图" /><category term="HMM" /><category term="CRF" /><category term="NLP" /><summary type="html"><![CDATA[系统梳理词法标注中的 HMM 与 CRF：前向后向算法、维特比解码、条件独立性与概率图直观理解，含推导与示意图。]]></summary></entry><entry xml:lang="zh-CN"><title type="html">概率图模型</title><link href="https://cv.hqzhou.com/2018/11/26/probabilistic-graphical-models.html" rel="alternate" type="text/html" title="概率图模型" /><published>2018-11-26T02:32:00+00:00</published><updated>2018-11-26T02:32:00+00:00</updated><id>https://cv.hqzhou.com/2018/11/26/probabilistic-graphical-models</id><content type="html" xml:base="https://cv.hqzhou.com/2018/11/26/probabilistic-graphical-models.html"><![CDATA[<h2 id="前言">前言</h2>
<p>11月22号的时候鼓起勇气来将HMM和CRF里面重要的几个公式推了一下，似乎明白了些什么。最近发现有些东西光是读和看，会以为自己已经懂了，但实际上这只是假象。要真自己动手就才会发现自己其实什么都不懂。所以试着用这种方法巩固知识和分享自己的一点见解。</p>

<h2 id="概率图模型">概率图模型</h2>
<h4 id="概率模型的意义">概率模型的意义</h4>
<p>概率图模型是</p>
<blockquote>
  <p>a general-purpose framework for constructing and using probabilistic models of complex systems.<br />
一种能够构建和利用复杂系统概率模型的通用框架。<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>
</blockquote>

<p>在概率模型中我们往往要处理数量巨大的随机变量，并对他们的联合分布进行建模<br />
<em>知道了联合分布我们就可以通过计算得到任意的边缘分布与条件分布</em></p>

<p>$$\begin{equation}P(A_1, A_2, A_3, \dotsc, A_i, \dotsc,A_n)\end{equation}$$</p>

<p>我们假设 $k_i=|Val(A_i)|$ 为每个随机变量可能的取值个数，那么包含 $n$ 个随机变量的联合分布就可能有 $\prod_i^n k_i$ 种可能取值。我们取最极端的情况，所有的随机变量都只有 $2$ 种类取值，那么 $n$ 个随机变量的联合分布的取值也会是 $2^n$ 种，这对我们训练概率模型和使用概率模型来推理是十分不利的。<br />
既然我们无法一次性处理那么多随机变量的联合分布，那么我们有没有什么办法将其中的随机变量进行分割，将这个大问题化成许多个小问题呢？或者说，我们如何描述这些随机变量之间的（独立）关系呢？答案肯定是有的，两种概率图模型为我们提供了这种可能。</p>
<h5 id="图论与概率论的几个重要概念">图论与概率论的几个重要概念</h5>
<p><strong id="graphic-relatives">图中的亲属关系</strong></p>

<p><img src="/assets/img/probabilistic-graphical-models/graphic-relatives.png" alt="graphic-relatives" /></p>

<p><strong id="chain-rule">链式法则</strong></p>

<table>
  <tbody>
    <tr>
      <td>$$\begin{equation}P(A_1, \dotsc, A_n)=P(A_1)P(A_2</td>
      <td>A_1)P(A_3</td>
      <td>A_2,A_1)\dotsm P(A_n</td>
      <td>A_{n-1},\dotsc,A_1)\end{equation}$$</td>
    </tr>
  </tbody>
</table>

<h5 id="概率的图化">概率的图化</h5>
<p>首先我们需要了解如何使用图来表示概率。我们通过一个例子来说明：<strong>T公司</strong>打算招应届生，他们希望招到的员工有很强的<strong>代码能力</strong> $C$，所以他们想到了通过在线笔试来获得<strong>笔试成绩</strong> $G$，以此来确定学生的代码能力。
于是他们首先构造了一个只有两个随机变量 $C,G$ 的模型。首先我们将每个随机变量作为一个结点。接着我们要确定随机变量间的关系。根据直觉，我们知道是<strong>代码能力</strong>在很大程度上决定了<strong>笔试成绩</strong>。我们将这样一个模型 $P(C,G)$ 用概率图模型表现出来如下</p>

<p><img src="/assets/img/probabilistic-graphical-models/code-to-grade.png" alt="code-to-grade" /></p>

<p>这个简单的有向图为我们给出了概率图模型的第一个好处：让我们可以将一个关于 $n$ 个随机变量的联合分布可以分解为多个紧凑的因子</p>

<table>
  <tbody>
    <tr>
      <td>$$ \begin{equation}P(C,G)=P(C)P(G</td>
      <td>C)\end{equation} $$</td>
    </tr>
  </tbody>
</table>

<p>在前面的例子中，<strong>T公司</strong>发现单纯使用<strong>笔试成绩</strong>就来判断<strong>代码能力</strong>的高低有点过于武断。他们在应聘者的简历上给出了他们技术博客的地址，在博客中可以看到<strong>点赞数量</strong> $L$，我们假设<strong>点赞数量</strong>只和<strong>博客质量</strong> $Q$ 有关，而要写出一篇高质量的博客不但要求<strong>代码能力</strong>，还要求他们的<strong>表达能力</strong> $E$。为此面试官对模型进行升级改造：</p>

<p><img src="/assets/img/probabilistic-graphical-models/recruit-model.svg" alt="recruit-model" id="recruit-model" /></p>

<p>我们使用<a href="#chain-rule">链式法则</a>可以轻易地将模型的联合分布变成多个条件概率的乘积的形式</p>

<table>
  <tbody>
    <tr>
      <td>$$ \begin{equation}P(C,G,E,Q,L)=P(C)P(G</td>
      <td>C)P(E</td>
      <td>C,G)P(Q</td>
      <td>C,G,E)P(L</td>
      <td>C,G,E,Q)\end{equation} $$</td>
    </tr>
  </tbody>
</table>

<p>但是我们更具概率图可以写出更加简单的式子</p>

<p>$$ \require{cancel}
\begin{equation}\begin{split}
P(C,G,E,Q,L)&amp;=P(C)P(G|C)P(E|\cancel{C,G})P(Q|C,E\cancel{,G})P(L|\cancel{C,E,G,}Q) <br />
&amp;=P(C)P(G|C)P(E)P(Q|C,E)P(L|Q)
\end{split}\end{equation}
$$</p>

<p>我们很容易可以在概率图中找到每一项乘积的对应</p>

<p><img src="/assets/img/probabilistic-graphical-models/factor-of-model.png" alt="factor-of-model" /></p>

<p>我们可以看到一般地，我们可以得到这样的公式</p>

<table>
  <tbody>
    <tr>
      <td>$$ \begin{equation}P(A_1,\dotsc,A_n)=\prod_{i=1}^n P(X_i</td>
      <td>\mathbf{Pa}_{X_i}^\mathcal{G})\end{equation} $$</td>
    </tr>
  </tbody>
</table>

<p>其中因子 $P(X_i|\mathbf{Pa}_{X_i}^\mathcal{G})$ 就是我们常说的<em>条件概率分布</em>，我们通过模型将链式法则以一种更加清晰、可理解的方式呈现出来。这正是因为概率图模型为我们提供的第二个好处：明确地给出了模型中所蕴含的独立关系假设。
我们把形如<a href="#recruit-model">上图</a>的有向无圈图（DAG）$\mathcal{G}$，叫做贝叶斯网，结点为随机变量，边表示一个结点对另外一个结点的直观影响。</p>

<blockquote>
  <p>可以以两种截，然不同的方式看待图 $\mathcal{G}$:</p>
  <ol>
    <li>它是提供了以因子分解的方式紧凑表示联合分布骨架的数据结构</li>
    <li>它是关于一个分布的一系列条件独立性假设的紧凑表示</li>
  </ol>

  <p>在严格意义上，这两种观点是等价的。<sup id="fnref:1:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>
</blockquote>

<p>前面介绍了有向图下面我们介绍如何使用无向图来描述概率。<br />
并不是所有的独立性描述都可以使用贝叶斯网来描述，一方面是因为贝叶斯网中不允许环的存在，从直观上来说是因为贝叶斯网为每一种影响都指定了因果方向，因此当我们在描述两个随机变量相互影响的时候就会出现问题</p>

<p>如下面这个例子</p>

<p><img src="/assets/img/probabilistic-graphical-models/markov-model-exmple.svg" alt="markov-model-exmple" /></p>

<p>一个矩形水缸被划分为 $A, B, C, D$ 四个区域，区域被玻璃板隔开。但相邻两个区域间的玻璃板上有缺口，允许分子自由地穿过。这就意味着如果发现一个区域被污染了，那么因为分子的自由扩散，它相邻的区域也有可能会受到污染。<br />
在这样的例子中存在着这样的独立假设，在知道 $B,D$ 区域是否受污染的时候， $A,C$ 之间不会有影响；同理在知道 $A,C$ 区域是否受污染的时候， $B,D$ 之间不会有影响，除了这两个独立假设外不再存在别的独立假设即</p>

<p>$$ \begin{equation}\begin{cases}
    &amp; (A\bot C|{B,D})<br />
    &amp; (B\bot D|{A,C})
\end{cases}\end{equation} $$ {: #markov-independence}</p>

<p>但是使用贝叶斯网是无法表示这样的独立关系而不带入额外的独立关系。因此我们需要使用无向图来表示这种独立假设</p>

<p><img src="/assets/img/probabilistic-graphical-models/markov-model.svg" alt="markov-model" id="markov-model" /></p>

<p>我们把形如<a href="#markov-model">上图</a>的无向图$\mathcal{H}$，叫做马尔可夫网，结点为随机变量，边表示两个结点间具有亲密关系。</p>

<p>与贝叶斯网类似，通过马尔可夫网可以将联合分布转换为紧凑的因子 $\phi$ 的分解，同时提供一系列马尔可夫独立性假设。<br />
但与贝叶斯网不同的是，他们因子之间的含义。在有向图中因子的物理含义为条件概率分布；但在无向图中，由于因子之间的连接是没有方向的，因此我们无法将因子表达为条件概率。在无向图中我们使用因子来表达随机变量之间的兼容度，或者说是随机变量之间的<strong>密切程度</strong>。此外，在因子的取值上两种模型也不相同，在有向图中因子的定义为条件概率分布因此它的取值要符合概率分布的所有要求，例如取值范围是 $[0,1]$ 、$P(\Omega)=1$ 和 $P(A + B) = P(A) + P(B)$ 等。但在无向图中因子不再具有这种要求，因子的取值范围甚至可以为负值，正因为缺少这种约束，无向图模型的联合分布并不能之间表示为，所有因子的乘积形式，应为它并没有被归一化，因此我们引入一个全局归一化常数 $Z$ 来将其变成一个合法的分布。</p>

<p>这样，我们可以为求<a href="#markov-model">例子</a>中的 $P(A, B, C, D)$ 了</p>

<p>$$ \begin{equation}
    P(A, B, C, D)=\frac{1}{Z}\phi_1(A, B)\phi_2(B, C)\phi_3(C, D)\phi_4(D, A)<br />
    Z=\sum_{A, B, C, D}\phi_1(A, B)\phi_2(B, C)\phi_3(C, D)\phi_4(D, A)
\end{equation} $$</p>

<p>更一般地我们有计算无向图模型联合分布的公式</p>

<p>$$ \begin{equation}
    P_\Phi(A_1,\dotsc,A_n)=\frac{1}{Z}\tilde{P}<em>\Phi(A_1,\dotsc,A_n)<br />
    \tilde{P}</em>\Phi(A_1,\dotsc,A_n)=\prod_{i=1}^K\phi_i(\mathbf{D}<em>i)<br />
    Z = \sum</em>{A_1,\dotsc,A_n}\tilde{P}_\Phi(A_1,\dotsc,A_n)
\end{equation} $$</p>

<p>其中 $\Phi$ 为因子集 $\Phi = {\phi_1(\mathbf{D}_1),\dotsc,\phi_K(\mathbf{D}_K)}$ ，公式中每个 $\mathbf{D}$ 都是无向图 $\mathcal{H}$ 中的一个完全子图即<strong>团</strong><br />
那么描述<strong>密切程度</strong>的 $\phi(\mathbf{D})$ 应该如何求呢？ 我们通常将因子改写为对数线性模型的形式，即</p>

<p>$$ \begin{equation}\phi(\mathbf{D})=\mathrm{exp}(\omega f(\mathbf{D}))\end{equation} $$</p>

<p>其中 $f(\mathbf{D})$ 为从子图 $\mathbf{D}$ 中提取的特征，很多时候我们会让特征的取值为 $0$ 或 $1$ ；即，取值为 $0$ 表示子图中没有对应的特征存在，反之取 $1$，$\omega $ 为特征对应的权重，这样一来联合分布就可以改写为对数线性模型的形式了</p>

<p>$$ \begin{equation}
    P(A_1,\dotsc,A_n)=\frac{1}{Z}\mathrm{exp}\left[\sum_{i=1}^k\omega_i f_i(\mathbf{D}_i)\right]
\end{equation} $$</p>

<p>我们知道，使用贝叶斯网网络无法表示<a href="#markov-independence">前面例子</a>所描述的独立性；那么有没有什么独立性是贝叶斯网可以描述的而马尔可夫网是不能描述的呢？<br />
我们考虑贝叶斯网中一个经典的结构：v-结构。在这个图 $\mathcal{G}$ 中，存在着边缘独立 $(I\bot D)$ 并且不存在着条件独立 $(I\bot D|G)$ ，但我们试图构建满足这样独立性的马尔可夫网的时候，我们会发现并不能找到满足这样独立性的图 $\mathcal{H}$</p>

<p><img src="/assets/img/probabilistic-graphical-models/markov-v.png" alt="markov-v" /></p>

<h2 id="最后">最后</h2>
<p>本来打算在这篇文章中就吧HMM和CRF都讲了的，结果发现还是高估自己了。光是概率图的部分就写了3天，所以果断将概率图模型的部分截出来，单独作为一篇文章。</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Koller D, Friedman N, Bach F. Probabilistic graphical models: principles and techniques[M]. MIT press, 2009. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:1:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
  </ol>
</div>]]></content><author><name>Houquan Zhou</name></author><category term="概率图" /><summary type="html"><![CDATA[通俗介绍概率图模型（贝叶斯网与马尔可夫网）：因子分解、条件独立性、团与配分函数、对数线性模型等核心概念与示例。]]></summary></entry><entry xml:lang="zh-CN"><title type="html">Brave new Blog</title><link href="https://cv.hqzhou.com/2018/11/23/brave-new-blog.html" rel="alternate" type="text/html" title="Brave new Blog" /><published>2018-11-23T07:28:00+00:00</published><updated>2018-11-23T07:28:00+00:00</updated><id>https://cv.hqzhou.com/2018/11/23/brave-new-blog</id><content type="html" xml:base="https://cv.hqzhou.com/2018/11/23/brave-new-blog.html"><![CDATA[<h2 id="why-blog">Why Blog？</h2>
<p>昨天重新看了一下HMM和CRF，试着推了一下公式。感觉有点明白了，但还是懵懵懂懂。害怕过个几天不留神把演算的稿纸丢了然后又忘记公式是怎么来的了。试着写写Blog吧，同时看看能不能改善一下自己的表达能力~</p>

<h2 id="配置">配置</h2>
<p>用了Github+Jekyll, 安装已经变得很方便了，尽管是在Windows系统下，用了大佬<a href="https://huangxuan.me/about/">黄玄</a>的<a href="https://github.com/Huxpro/huxpro.github.io">博客模板</a>，过段时间再试着自己折腾一下。
关于Timezone的问题Windows下找了很多教程都是无解。放弃了还是在时间后面加+0800吧。</p>]]></content><author><name>Houquan Zhou</name></author><category term="博客" /><summary type="html"><![CDATA[开启博客之旅：记录学习与表达的起点，搭建 GitHub+Jekyll 环境与主题选择的一些笔记。]]></summary></entry></feed>