<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>中文博客 on Zeqiang Fang | 方泽强</title><link>https://zeqiang.fun/cn/</link><description>Recent content in 中文博客 on Zeqiang Fang | 方泽强</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 04 Dec 2017 00:00:00 +0000</lastBuildDate><atom:link href="https://zeqiang.fun/cn/" rel="self" type="application/rss+xml"/><item><title>你所应该知道的 A/B 测试 (A/B Test You Should Know)</title><link>https://zeqiang.fun/cn/2021/10/abtest-you-should-know/</link><pubDate>Sun, 17 Oct 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/10/abtest-you-should-know/</guid><description><![CDATA[
        <h2 id="什么是-a-b-测试">什么是 A/B 测试</h2>
<p><strong>A/B 测试</strong>是一种随机测试，将两个不同的东西（即 A 和 B）进行假设比较。A/B 测试可以用来测试某一个变量两个不同版本的差异，一般是让 A 和 B 只有该变量不同，再测试目标对于 A 和 B 的反应差异，再判断 A 和 B 的方式何者较佳 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。A/B 测试的前身为双盲测试 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>，在双盲测试中人员会被随机分为两组，受试验的对象及研究人员并不知道哪些对象属于对照组，哪些属于实验组，通过一段时间的实验后对比两组人员的结果是否有明显差异。在各种科学研究领域中，从医学、食品、心理到社会科学及法证都有使用双盲方法进行实验。</p>
<p>一个简单的 A/B 测试流程如下：</p>
<p><img src="/images/cn/2021-10-17-abtest-you-should-know/abtest.png" alt=""></p>
<ol>
<li>对目标人群进行随机划分，以进行有效的独立随机实验。</li>
<li>对不同分组应用不同的策略。</li>
<li>在确保实验有效的前提下，对不同分组的结果进行分析，以确定不同策略的优劣。</li>
</ol>
<p>A/B 测试的主要目的是帮助我们更加科学的判断不同策略的优劣性，避免拍脑门的决策，不给杠精们互相 BATTLE 的机会。同时我们也需要认识到 A/B 测试只是一个工具，它能够帮助我们对产品和策略进行不断优化，但对产品和策略的创新更多还是需要洞察力。它可以让我们在已达到的山上越来越高，却不能用它来发现一座新的山脉。一句话：A/B 测试不是万能的，但离开 A/B 测试是万万不能的。</p>
<h2 id="a-b-测试的科学性">A/B 测试的科学性</h2>
<h3 id="流量分配">流量分配</h3>
<p>进行 A/B 测试的第一个问题就是如何划分用户，如果采用上面简单五五开的方式我们一次只能做一个实验，当我们需要同时做多个实验时就无法满足了。如果对用户分成多个桶，当桶的数量过多时，每个桶中的用户数量就会过少，从而会导致实验的置信度下降。</p>
<p>为了保证可以使用相同的流量开展不同的实验，同时各个实验之间不能相关干扰，我们需要采用正交实验。正交实验的思想如下：</p>
<p><img src="/images/cn/2021-10-17-abtest-you-should-know/orthogonal-experiment.png" alt=""></p>
<p>每个独立实验为一层，层与层之间的流量是正交的，流量经过一层实验后会再次被随机打散。</p>
<p>有些情况下实验并不是独立的，例如同时对按钮和背景的颜色进行实验，按钮和背景颜色之间并不是独立的（即有些按钮和背景颜色搭配从设计角度是不可行的，没有必要进行实验），这种情况下我们需要采用互斥实验。互斥实验的思想如下：</p>
<p><img src="/images/cn/2021-10-17-abtest-you-should-know/mutual-experiment.png" alt=""></p>
<p>实验在同一层进行流量拆分，不同组之间的流量是没有重叠的。</p>
<p>在 A/B 测试中，当多个实验内容相互影响应选择互斥方法分配流量，当多个实验内容不会相互影响应选择正交方法分配流量。更加精细的流量分类和控制可以参考 Google 的论文 <em>Overlapping Experiment Infrastructure: More, Better, Faster Experimentation</em> <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>。</p>
<h3 id="评价指标选择">评价指标选择</h3>
<p>在设计实验之前我们需要明确实验的目标，根据目标才能确定合理的评价指标。更多情况下我们应该从业务的视角出发选择合适的评价指标，我们以风险策略模型实验为例，我们可以从技术和业务角度选择不同的评价指标：</p>
<ol>
<li>技术角度：准确率和召回率</li>
<li>业务角度：客诉率和追损金额</li>
</ol>
<p>单纯从技术角度出发我们会忽视很多现实问题，例如两个策略的准确率和召回率差不多，但识别的结果人群不一样，这些人造成的损失也可能不一样。因此能够帮助我们追回更多损失同时有更小的客诉率才是更优的策略。</p>
<p>在进行实验时结果指标至关重要，但有时我们也应该关注一些过程指标。以页面优化实验为例，可能的过程指标和结果指标有：</p>
<ol>
<li>过程指标：页面平均停留时间，页面跳出率等</li>
<li>结果指标：商品加购率，商品转化率等</li>
</ol>
<p>策略和模型最终都是要为业务服务的，因此我们应常关注业务指标，一些常用的业务指标有：点击率（CTR）、转化率（CVR）、千次展示收入（RPM）等。</p>
<h3 id="有效性检验">有效性检验</h3>
<p>当实验完成得到结果后，我们还需要判断实验结果是否有效，这部分主要依靠统计学中的假设检验进行分析。针对两个实验在确定合理的统计量后，需要构建如下两个假设：</p>
<ul>
<li><strong>原假设</strong> $H_0$：两个实验的统计量无差异</li>
<li><strong>备选假设</strong> $H_1$：两个实验的统计量有差异</li>
</ul>
<p>对于假设检验只可能有两种结果：一个是接受原假设，一个是拒绝原假设。在进行假设检验过程中，容易犯两类错误：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">是否接受原假设 \ 假设真伪</th>
          <th style="text-align: center">$H_0$ 为真</th>
          <th style="text-align: center">$H_1$ 为真</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">拒绝原假设</td>
          <td style="text-align: center">第一类错误<br/>$\alpha$：显著水平</td>
          <td style="text-align: center">正确决策<br/>$1 - \alpha$：置信度</td>
      </tr>
      <tr>
          <td style="text-align: center">接受原假设</td>
          <td style="text-align: center">正确决策<br/>$1 - \beta$：统计功效</td>
          <td style="text-align: center">第二类错误<br/>$\beta$</td>
      </tr>
  </tbody>
</table>
<p>第一类错误（弃真）即原假设为真时拒绝原假设，犯第一类错误的概率为 $\alpha$，即显著水平。第二类错误（取伪）即原假设为假时未拒绝原假设，犯第二类错误的概率为 $\beta$。</p>
<p>在进行有效性检验时我们有多个指标可以参考：</p>
<ol>
<li>P 值。P 值就是当原假设为真时，比所得到的样本观察结果更极端的结果出现的概率。如果 P 值很小，说明原假设情况的发生的概率很小，而如果出现了，根据小概率原理，我们就有理由拒绝原假设，P 值越小，我们拒绝原假设的理由越充分。</li>
<li>置信区间。置信区间就是分别以统计量的置信上限和置信下限为上下界构成的区间。置信水平是指包含总体平均值的概率是多大，例如：95% 的置信水平表示，如果有 100 个样本，可以构造出 100 个这样的区间，有 95% 的可能性包含总体平均值。在 A/B 测试时，如果置信区间上下限的值同为正或负，则认为存在有显著差异的可能性；如果同时有正值和负值，则认为不存在有显著差异的可能性。</li>
<li>统计功效。一般情况下我们希望拒绝原假设，得到新的结论，即在进行 A/B 测试时希望实验组的效果优于对照组。也就是我们希望不要出现在应该拒绝原假设时却没有拒绝的情况，即犯第二类错误。统计功效就是我们没有犯第二类错误的概率 $1 - \beta$，在进行 A/B 测试时表示当两个策略之间存在显著差异时，实验能正确做出存在差异判断的概率。</li>
</ol>
<p>综上，我们可以认为当 A/B 测试实验数据在 95% 的置信水平区间内，P 值小于0.05，功效大于 80% 的情况下，实验结果是可信赖的。</p>
<h2 id="a-a-测试">A/A 测试</h2>
<p>在做 A/B 测试的时候，有时尽管我们发现 A/B 两组有明显差异，但我们依旧无法确认这种差异是由于实验条件不同还是 A/B 两组用户本身的差异带来的。尽管 A/B 两组用户是随机抽样，但两组用户在空跑期（即实验条件一致）也会出现显著差异。因此为了避免这个问题，我们会选择进行 A/A 测试，即在正式开启实验之前，先进行一段时间的空跑，对 A/B 两组用户采用同样的实验条件，一段时间后，再看两组之间的差异。如果差异显著，数据弃之不用，重新选组。如果差异不显著，记录两组之间的均值差，然后在实验期结束时，用实验期的组间差异减去空跑期的组间差异得到最终实验结果。</p>
<p>A/A 测试也会存在一些局限，在实际情况中，组间差异是一定存在。因此在这个前提下，可以用统计方式来衡量差异大小，在计算实验效果的时候，把差异考虑在内即可。差异产生的主要原因就是“随机性”，我们同样可以利用置信度和置信区间来描述 A/A 实验的波动。在进行实验时直接进行 A/B 测试，不需要考虑 A/A 测试，在分析结果时，需要考虑 A/B 测试实验之间的差异要大于 A/A 测试实验之间的差异。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://zh.wikipedia.org/wiki/A/B%E6%B8%AC%E8%A9%A6">https://zh.wikipedia.org/wiki/A/B測試</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://zh.wikipedia.org/wiki/%E9%9B%99%E7%9B%B2">https://zh.wikipedia.org/wiki/雙盲</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Tang, Diane, et al. &ldquo;Overlapping experiment infrastructure: More, better, faster experimentation.&rdquo; <em>Proceedings of the 16th ACM SIGKDD international conference on Knowledge discovery and data mining</em>. 2010.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>一个人的摩旅 (Travel with My Motorcycle Alone)</title><link>https://zeqiang.fun/cn/2021/10/travel-with-my-motorcycle-alone/</link><pubDate>Wed, 06 Oct 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/10/travel-with-my-motorcycle-alone/</guid><description><![CDATA[
        <h2 id="之前">之前</h2>
<p>三十岁了，不小了，一些事情再不做就不知道会拖到什么时候了。自从之前看到朋友发的一段摩旅的视频，就一直念念不忘，有机会一定要来一次，虽然不一定如视频中那般潇洒。最后证明确实不一样，尽管谈不上是一场修行，但遇到的人事物都是独特且难以忘记的。</p>
<p>有了出发的勇气，但拖延症还是很严重，各种准备和规划直到出发前一晚才简单搞定，当然这也给后面的遇到的问题埋下了伏笔。本来规划了 5 天的行程，最后一天赶到大同见朋友一面再返回北京，但一场冷雨让我在第 4 天直接折返北京。</p>
<p><img src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/roadmap.jpg" alt=""></p>
<h2 id="第一天">第一天</h2>
<p>折腾了好久，费劲扒拉才把后座包绑好。出门还没上高速，碰见一个骑复古车的哥们，问了我去哪里，我说自己去内蒙古，一句注意安全，一句感谢。出发前才掉了一格油，就没有去加，距离丰宁还 20 多公里，油表灯就闪了，好在还是坚持到了丰宁，成功续满了油。路上断断续续发现成队的、独行的骑士，还有一个书包和后座包都贴了实习标志的哥们，应该都是追逐自由的人吧。</p>
<p>到丰宁前中途停下检查了后座包，发现有一侧绑的不合理，卡口会被后轮摩到，都快要磨断了，幸好检查了下，改造一番绑好继续上路。从丰宁出发没多久，隐约闻道一股烧焦的味道，没当回事儿，以为是路上其他汽车的尾气。中间休息的时候又检查了一下后座包，才发现从丰宁出发时有一侧绑的有点松，后座包倒向了排气那边。奈何我帅气的双排气在上面，就把包烫了一个大洞，一并倒霉的还有带了但一次还没来得及穿过的冬款骑行服和骑行裤。不过好在底下放的是衣服，如果我把相机、电脑、无人机、充电宝放在那边低下&hellip;..都不敢想会有什么事儿。这就是没有提前做好功课如何绑后座包的后果，当然闻道异味没有第一时间检查也很不好。</p>
<p>包和衣服被烫了之后，心情一下就不好了，虽然一再自我安慰，但还是缓了很久才勉强不伤心了。由于中途发生了不少事情，整体耽误了一个小时左右，赶到塞罕坝这边酒店的时候已经是六点半了，天已经完全黑下来了，后面一个多小时的路程真的是又累又冷。万幸的是我把我夏季骑行服里面的内胆加上了，不然估计到酒店就冻成狗了。</p>
<h2 id="第二天">第二天</h2>
<p>早起退房碰见一对儿摩旅的，男生骑了一辆 ADV，小姐姐骑了一辆 Ninja 400。一清早晴空万里，阳光明媚，昨日的不开心一下子全都消散了。虽然秋天已经来了好一阵儿了，不过路两旁的风景还是很漂亮的。</p>


<div class="gallery tiles-3 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  
<link rel="stylesheet" href="/css/photoswipe.css" />
<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/grassland-in-autumn.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/grassland-in-autumn.jpg" alt="秋日草原"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/grassland-in-autumn.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>秋日草原</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/saihanba-01.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/saihanba-01.jpg" alt="路两旁的树"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/saihanba-01.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>路两旁的树</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/saihanba-02.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/saihanba-02.jpg" alt="小行星即视感"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/saihanba-02.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>小行星即视感</p>
      </figcaption>
  </figure>
</div>


</div>

<p>骑进乌兰布统，越往后走人越少，路上碰见了好多次牛马拦路。从乌兰布统去到多伦的路上风景很好，选择下道骑行是明智的，一路上草原、小河、树林，遇到太多美景。</p>


<div class="gallery tiles-3 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/tree.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/tree.jpg" alt="树"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/tree.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>树</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/road.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/road.jpg" alt="路"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/road.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>路</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/river.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/river.jpg" alt="河"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/river.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>河</p>
      </figcaption>
  </figure>
</div>


</div>

<p>赶着中午骑到了多伦湖，简单吃过饭后就开始了环湖之旅，一路上走走停停。到了一处小高峰，把摩托骑了上去，太适合给我的小摩托来一张照片了。刚拍上就碰到了俩大哥，帮我和我的小摩托来了本次摩旅的第一张合照，聊了会儿道谢之后就开始往回返。</p>
<p>回到环湖起点时间还早，简单搜了下发现往北一点儿就是「滦河」源，小时候家乡人口中的滦河（唐山话：láng hé）原来是从这里流下去的。往滦河源的路不是很好走，骑到一半发现一条土路，骑过去是一个小平台，望向四周很美。后面跟过来好多越野车，也纷纷下来拍照，发现我骑了摩托过来就一个个站在旁边拍起了照片。我过去车上拿我的相机，说道慢慢拍不着急，小姐姐问我可不可以坐上去，我说没问题。后来我也拜托他们帮我和我的小摩托拍了第二张合照，我也成了几个阿姨的摄影师。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/motorcycle-and-me.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/motorcycle-and-me.jpg" alt="小摩托和我"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/motorcycle-and-me.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>小摩托和我</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/my-back.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/my-back.jpg" alt="背影"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/my-back.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>背影</p>
      </figcaption>
  </figure>
</div>


</div>

<p>快乐是什么？一路上美丽的风景，一路上美丽的人，还有多少能带给他人快乐的我和我的小摩托。果然不敢路的一天是轻松的，从滦河源下来之后就早早赶到多伦的酒店休息了。</p>
<h2 id="第三天">第三天</h2>
<p>早起的阳光不错，虽然空气有些凉，但是太阳照着还是暖洋洋的，耳机里恰巧响起了赵照的「在冬天和奶奶一起晒太阳」，有些惬意。走着走着云多了起来，中午赶到了太仆寺旗，吃上一口热乎的麻辣烫。</p>
<p>中午微信群里有哥们说北京下大雨了，我心底还在暗暗窃喜这几天出行的天气都还不错，然而这 Flag 还是立早了&hellip;&hellip;下午在去往张北的路上碰见了一波从北京来的摩旅队伍，什么车都有，有 ADV，有踏板，可见是真友谊，ADV 还能带着小踏板一起愉快的玩耍。前半程的风景很不错，路两旁的树叶开始泛黄飘落，一条直路望不见头。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/zhangbei-01.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/zhangbei-01.jpg" alt="敌方视角"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/zhangbei-01.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>敌方视角</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2021-10-06-travel-with-my-motorcycle-alone/zhangbei-02.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/zhangbei-02.jpg" alt="我方视角"/>
    </div>
    <a href="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/zhangbei-02.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>我方视角</p>
      </figcaption>
  </figure>
</div>


</div>

<p>天气渐渐阴沉了下来，肉眼可见前方有积雨云，但还是朝着前方骑了下去。突然天空飘起了小雨，但很快又没了，停在一个加油站打开雷达云图，发现云应该已经从要走的路上飘过去了，简单休息了一下又继续往前走。</p>
<p>路上淅淅沥沥的飘落几滴雨，但路面已经湿透了，之前应该是下了一阵。走着走着发现大腿有点凉，想着应该是雨水溅落到裤子上了。找了个地方停下一看，我去，车子、衣服、后座包已经被黒黑的泥水沾满，躲过了雨水，躲不过泥巴&hellip;&hellip;水越溅越多，身体也越来越冷，万幸还是撑到了张北。找了个肯德基，点了一大杯热拿铁，喝完天已经放晴，看看时间不早了，再不走估计到乌兰察布得很晚了，简单把车擦了下就又启程了。</p>
<p>前半程的国道一路都是风景，但从张北到乌兰察布的后半程除了大车和尘土啥都没有。天彻底放晴，一股暖意上来，还好老天没太折磨我，停下来特意给我脏了吧唧但不离不弃的小摩托照了张像，纪念有史以来最脏的一次。</p>
<p><img src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/ditry-motorcycle.jpg" alt=""></p>
<p>上了高速就快多了，大概 6 点半多赶到了酒店，卸下行囊第一件事就是去找了个洗车店把我的小摩托好好冲一冲，拿过老板的水枪就开始了自助洗车，洗完之后，我漂亮的小摩托又回来了！</p>
<h2 id="第四天">第四天</h2>
<p>一早起来天气有些阴冷，不过早有预期，昨天看天气，相比其他地方至少没有下雨。先走了国道去火山，路不太好，一路上身体到不是很冷，冷的是手，夏季的手套还真实漏风，走走停停，冷的受不了就带着手套放在发动机上「烤烤火」。</p>
<p>快到火山了发现一条岔路，还有警察叔叔在摆一个大牌子，「火山旅游路线」，问了后果断选择了指向的新路，新路好走的不要不要的。来到六号火山，人还不少，阴天更是给这里增添了一份外星感。摩托的好处是能骑上平台，转到北面发现人比较少，借来的大疆终于能派上用场了，虽然最后感觉拍的并不好。</p>
<p>往下走看到有几辆越野车爬上了小山丘，心里想来都来了，不在土路上骑一遭怎么对得起我这「越野版」的小狮子。</p>
<p><img src="/images/cn/2021-10-06-travel-with-my-motorcycle-alone/motorcycle-on-hill.jpg" alt=""></p>
<p>转完火山发现时间不是很晚，导航了下发现应该能在 7 点前赶回北京，想了想北京这几天都有雨，应该怎么都躲不过，一番权衡之下打算今天直接回北京。上了高速很快就到了集宁服务区，已经快 2 点了，正好吃午饭，这时天也下上了小雨。吃完之后再次权衡，还是决定继续往北京走，如果下大了不行就找最近的口下。一路上，雨不算大也不算小，这就让人很纠结，每到一个服务区就停下来看看云图，最终还是在晚上 10 点前回到了北京。陪了我五年的手机最终在进入地库前因为进水太多失灵然后宕机了，万幸坚持到了回家。</p>
<p>这段雨中经历感觉能回忆一辈子，不是因为勇敢，也不是因为幸运。敢这么走下来，还是评估了环境没有那么恶劣，对自己的驾驶技术也有一定把握，当然最重要的是量力，人永远不要同自然和生命开玩笑。所以每一个服务区都停下来，要么吃点东西，要么喝点热水，休整好后继续往前走。如果说知道 10 点才能到北京，让我再选择一次的话，我想我会选择在中途歇上一晚，虽然第二天也躲不开雨，但能骑得更从容些。</p>
<h2 id="之后">之后</h2>
<p>真的要感谢我靠谱的小摩托，一直不离不弃，也要感谢这一路上遇到的陌生人的祝福和帮助。服务区遇到的摩托情侣，问我衣服是不是穿得太少，我没好意思说烫坏了，就只说带少了。餐厅拼桌吃饭的一家，男主人在深圳上班，自己不玩摩托，但工作的地方挨着交管局，见到太多扣查的摩托（深圳全市禁摩），分别时一个握手，一句平安。高速路上，三车道，我在最右侧，中间车道的一辆大车超我时特意变到左侧，掀起的水花完全没溅到我，这一定是个可爱的司机大叔吧。</p>
<p>旅行的意义在哪里？形形色色的人，真实且普通，慢下来，你能看到自己的更多面，也能看到陌生人的一面，或好或坏，无所谓，世界不就是这样吗。</p>


  


<script src="/js/load-photoswipe.js"></script>


<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.css'/>
<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/default-skin/default-skin.min.css'/>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js'></script>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js'></script>


<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

<div class="pswp__bg"></div>

<div class="pswp__scroll-wrap">
    
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    </div>
    
    <div class="pswp__ui pswp__ui--hidden">
    <div class="pswp__top-bar">
      
      <div class="pswp__counter"></div>
      <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
      <button class="pswp__button pswp__button--share" title="Share"></button>
      <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
      <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
      
      
      <div class="pswp__preloader">
        <div class="pswp__preloader__icn">
          <div class="pswp__preloader__cut">
            <div class="pswp__preloader__donut"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
      <div class="pswp__share-tooltip"></div>
    </div>
    <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
    </button>
    <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
    </button>
    <div class="pswp__caption">
      <div class="pswp__caption__center"></div>
    </div>
    </div>
    </div>
</div>


        ]]></description></item><item><title>设计语言初探 (A Glimpse of Design Language)</title><link>https://zeqiang.fun/cn/2021/08/a-glimpse-of-design-language/</link><pubDate>Sun, 08 Aug 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/08/a-glimpse-of-design-language/</guid><description><![CDATA[
        <p>设计语言（Design Language）或设计语言系统（Design Language System, DLS）是一套用于指导产品设计的整体风格方案 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。设计语言把设计作为一种“沟通的方式”，用于在特定的场景内，做适当的表达，进行特定的信息传递。设计语言在建筑、工业设计和数字产品等领域都有广泛的应用，本文仅围绕数字产品进行初探。</p>
<h2 id="为什么构建设计语言">为什么构建设计语言？</h2>
<h3 id="统一">统一</h3>
<p>通过设计语言可以在整个平台中统一颜色、字体、组件、动效等各种规范，避免由于设计师的个人特点导致产品风格不一致。</p>
<h3 id="体验">体验</h3>
<p>优秀的设计语言符合大众审美，可以提高产品的可用性和易用性。设计语言可以使用户能够与具备一致性的应用进行交互，让用户在使用过程中获得愉悦，提升用户体验。</p>
<h3 id="效率">效率</h3>
<p>优秀的设计语言使得设计和开发团队能够快速、经济、高效地进行开发、重构和迭代产品。通过不断更新和完善的文档库，可以改善团队之间的协作，提高生产力。</p>
<h3 id="品牌">品牌</h3>
<p>设计语言的构建可以传达一个统一的公司品牌形象。设计语言让产品具有自己的身份，使其在市场上的众多产品中更容易被识别出来，加深用户对品牌的印象。</p>
<h2 id="设计语言构建">设计语言构建</h2>
<p>在此我们借助语言学的角度来讨论数字化产品的构建 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>。在语言应用中，我们通常会涉及语法、语素、语句、语义、语境、语气、语素和响度等维度，通过不同的组合达成应景的表达和适时的沟通。</p>
<h3 id="语法">语法</h3>
<p>设计语言中的语法即设计价值观和设计原则，这是构建设计语言系统的起点，用于传达品牌主张或设计理念，它将指引业务设计执行的方向。</p>
<p>制定设计原则时，首先研究用户特性，聚焦产品核心价值，然后通过脑暴等形式选择有特点的维度，结合用户体验与品牌属性将其视觉化，最后用简要的语言归纳出来。</p>
<h4 id="ant-design-设计价值观">Ant Design 设计价值观 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></h4>
<table>
  <tr>
    <td width="50%"><img src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*zx7LTI_ECSAAAAAAAAAAAABkARQnAQ"/></td>
    <td>
      <p><b>自然</b></p>
      <p>数字世界的光速迭代使得产品日益复杂，而人类意识和注意力资源有限。面对这种设计矛盾，追求「自然」交互将是 Ant Design 持之以恒的方向。</p>
    </td>
  </tr>
  <tr>
    <td width="50%"><img src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*yHjSQKAhF5kAAAAAAAAAAABkARQnAQ"/></td>
    <td>
      <p><b>确定性</b></p>
      <p>界面是用户与系统交互的媒介，是手段而非目的。在追求「自然」交互基础上，通过 Ant Design 创造的产品界面应是高确定性、低合作熵的状态。</p>
    </td>
  </tr>
  <tr>
    <td width="50%"><img src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*xOYlR4e8ihIAAAAAAAAAAABkARQnAQ"/></td>
    <td>
      <p><b>意义感</b></p>
      <p>一个产品或功能被设计者创造出来不只是用户的需要，而更多是承载用户的某个工作使命。产品设计应充分站在工作视角，促成用户使命的达成；同时，在「自然」、「确定」之上，兼顾用户的人性需求，为工作过程创造富有意义感的人机交互。</p>
    </td>
  </tr>
  <tr>
    <td width="50%"><img src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*pKz3TabovrEAAAAAAAAAAABkARQnAQ"/></td>
    <td>
      <p><b>生长性</b></p>
      <p>企业级产品功能的增长与用户系统角色的演变相生相伴。设计者应为自己创造的产品负责，提升功能、价值的可发现性。用发展的眼光做设计，充分考虑人、机两端的共同生长。</p>
    </td>
  </tr>
</table>
<h4 id="ant-design-设计原则">Ant Design 设计原则 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup></h4>
<h5 id="亲密性">亲密性</h5>
<p>如果信息之间关联性越高，它们之间的距离就应该越接近，也越像一个视觉单元；反之，则它们的距离就应该越远，也越像多个视觉单元。亲密性的根本目的是实现组织性，让用户对页面结构和信息层次一目了然。</p>
<h5 id="对齐">对齐</h5>
<p>正如「格式塔学派」中的连续律（Law of Continuity）所描述的，在知觉过程中人们往往倾向于使知觉对象的直线继续成为直线，使曲线继续成为曲线。在界面设计中，将元素进行对齐，既符合用户的认知特性，也能引导视觉流向，让用户更流畅地接收信息。</p>
<h5 id="对比">对比</h5>
<p>对比是增加视觉效果最有效方法之一，同时也能在不同元素之间建立一种有组织的层次结构，让用户快速识别关键信息。</p>
<h5 id="重复">重复</h5>
<p>相同的元素在整个界面中不断重复，不仅可以有效降低用户的学习成本，也可以帮助用户识别出这些元素之间的关联性。</p>
<h5 id="直截了当">直截了当</h5>
<p>正如 Alan Cooper 所言：「需要在哪里输出，就要允许在哪里输入」。这就是直接操作的原理。eg：不要为了编辑内容而打开另一个页面，应该直接在上下文中实现编辑。</p>
<h5 id="足不出户">足不出户</h5>
<p>能在这个页面解决的问题，就不要去其它页面解决，因为任何页面刷新和跳转都会引起变化盲视（Change Blindness），导致用户心流（Flow）被打断。频繁的页面刷新和跳转，就像在看戏时，演员说完一行台词就安排一次谢幕一样。</p>
<h5 id="简化交互">简化交互</h5>
<p>根据费茨法则（Fitts&rsquo;s Law）所描述的，如果用户鼠标移动距离越少、对象相对目标越大，那么用户越容易操作。通过运用上下文工具（即：放在内容中的操作工具），使内容和操作融合，从而简化交互。</p>
<h5 id="提供邀请">提供邀请</h5>
<p>很多富交互模式（eg：「拖放」、「行内编辑」、「上下文工具」）都有一个共同问题，就是缺少易发现性。所以「提供邀请」是成功完成人机交互的关键所在。</p>
<p>邀请就是引导用户进入下一个交互层次的提醒和暗示，通常包括意符（eg：实时的提示信息）和可供性，以表明在下一个界面可以做什么。当可供性中可感知的部分（Perceived Affordance）表现为意符时，人机交互的过程往往更加自然、顺畅。</p>
<h5 id="巧用过渡">巧用过渡</h5>
<p>人脑灰质（Gray Matter）会对动态的事物（eg：移动、形变、色变等）保持敏感。在界面中，适当的加入一些过渡效果，能让界面保持生动，同时也能增强用户和界面的沟通。</p>
<h5 id="即时反应">即时反应</h5>
<p>「提供邀请」的强大体现在 交互之前 给出反馈，解决易发现性问题；「巧用过渡」的有用体现在它能够在 交互期间 为用户提供视觉反馈；「即时反应」的重要性体现在 交互之后 立即给出反馈。</p>
<p>就像「牛顿第三定律」所描述作用力和反作用一样，用户进行了操作或者内部数据发生了变化，系统就应该立即有一个对应的反馈，同时输入量级越大、重要性越高，那么反馈量级越大、重要性越高。</p>
<p>虽然反馈太多（准确的说，错误的反馈太多）是一个问题，但是反馈太少甚至没有反馈的系统，则让人感觉迟钝和笨拙，用户体验更差。</p>
<h4 id="alibaba-fusion-设计价值观">Alibaba Fusion 设计价值观 <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup></h4>
<p><img src="https://fusion.alicdn.com/images/2o1CnXRJI4a_-ilKT92bcI0fs.png" alt=""></p>
<h5 id="化繁为简的交互模式">化繁为简的交互模式</h5>
<p>面对互联网产品高迭代节奏和复杂的中后台场景，将复杂的业务组件抽象为用户标准认知层的交互方式，这套组件库来自于阿里巴巴上百个中后台场景的抽象结果，试图建立中后台 web 设计标准。</p>
<h5 id="驾驭技术">驾驭技术</h5>
<p>你用的所有设计资料，小到 sketch 样式工具中的颜色、字体、字号、投影、边框、尺寸；再到组件，大到一套完整的中后台产品系统，均能找到其对应的代码，完整的释放整个团队的前端生产力。</p>
<h5 id="追求新鲜-潮流">追求新鲜，潮流</h5>
<p>设计风格每年都会更新换代，由于 Alibaba Fusion  设计系统中的颜色、字体、字号、投影等样式均可通过线上配置修改，这也决定了它可以快速（甚至 15 分钟内）完成整套设计系统的样式迭代。</p>
<h5 id="聚变-裂变">聚变/裂变</h5>
<p>通过在 Alibaba Fusion 设计系统原则下，变换样式、多维度定制组件交互形式，可瞬间获取属于自己业务属性的设计系统；我们期待有无数业务线能够通过 Alibaba Fusion 的设计系统原则聚变出符合各类业务场景的 Fusion 生态系统。</p>
<h5 id="效率-1">效率</h5>
<p>Alibaba Fusion Design 希望构建一套提升设计与开发之间 UI 构建效率的工作方式，让 UED 的工作能够尽可能多的投入在 UE（User Experience）的用户调研、用户体验、商业思考，而在 D（Design）的过程中更多的投入与创意而非日复一日的重复绘图。</p>
<h4 id="腾讯-q-语言理念">腾讯 Q 语言理念 <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup></h4>
<table>
  <tr>
    <td width="30%"><img src="https://qzonestyle.gtimg.cn/qzone/qzact/act/external/qdesign/Design/QLanguage/DesignConcept/tongyi.png"/></td>
    <td>
      <p><b>统一体验</b></p>
      <p>QQ 作为一个社交平台，会容纳多样性的功能与体验，为了降低用户在不同场景功能下的学习成本，并提升易用性，统一体验是提升平台易用性的关键基础。同时有助于提升各角色间的协作效率。</p>
    </td>
  </tr>
  <tr>
    <td width="30%"><img src="https://qzonestyle.gtimg.cn/qzone/qzact/act/external/qdesign/Design/QLanguage/DesignConcept/jiyin.png"/></td>
    <td>
      <p><b>基因体现</b></p>
      <p>当今同质化的社交应用越来越多，QQ 作为横跨多时期多平台的社交应用，一方面需紧贴时代趋势，在众多应用中脱颖而出，另一方面有足够的历史底蕴，应强化自身基因特征，提升整体品牌认知。</p>
    </td>
  </tr>
  <tr>
    <td width="30%"><img src="https://qzonestyle.gtimg.cn/qzone/qzact/act/external/qdesign/Design/QLanguage/DesignConcept/xiangshan.png"/></td>
    <td>
      <p><b>社交向善</b></p>
      <p>社交应用会融入琳琅满目的娱乐化规则与玩法，但吸引年轻人的不应只是单纯娱乐消费，需要考虑社交娱乐的本质初心，QQ 更倡导用户在一个积极健康，安全贴心，触动情感的环境进行社交，并最终导人向善。</p>
    </td>
  </tr>
  <tr>
    <td width="30%"><img src="https://qzonestyle.gtimg.cn/qzone/qzact/act/external/qdesign/Design/QLanguage/DesignConcept/gaoxiao.png"/></td>
    <td>
      <p><b>高效娱乐</b></p>
      <p>伴随信息传播便利性提升，用户需要更高浓度的信息和更快的娱乐方式。用户时间愈加宝贵，偏向消费耗时较短的短视频、信息流等内容，希望更快找到喜欢的内容，以及更高浓度的内容。</p>
    </td>
  </tr>
  <tr>
    <td width="30%"><img src="https://qzonestyle.gtimg.cn/qzone/qzact/act/external/qdesign/Design/QLanguage/DesignConcept/xingqu.png"/></td>
    <td>
      <p><b>兴趣细分</b></p>
      <p>互联网使用场景更细分，兴趣爱好更加细分深入。各种兴趣圈、游戏圈、粉丝圈等年轻用户基于互联网衍生出来的圈子，需要有更细分深入的功能场景去承载。线上和线下联动是细分圈子持续活跃的关键。</p>
    </td>
  </tr>
  <tr>
    <td width="30%"><img src="https://qzonestyle.gtimg.cn/qzone/qzact/act/external/qdesign/Design/QLanguage/DesignConcept/yali.png"/></td>
    <td>
      <p><b>社交压力</b></p>
      <p>互联网信息传播的扩散效应，以及社会的复杂性给用户带来更多社交压力，原创越来越少。可以通过丰富的形象建立和维护体系增强用户的社交动力，引导产生更多原创内容和互动。</p>
    </td>
  </tr>
</table>
<h4 id="腾讯-q-语言原则">腾讯 Q 语言原则 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup></h4>
<table>
  <tr>
    <td width="80%"><img src="https://qzonestyle.gtimg.cn/qzone/qzact/act/external/qdesign/Design/QLanguage/DesignPrinciples/yuanze-004.gif"/></td>
    <td>
      <p><b>活力灵动</b></p>
      <p>对年轻人有吸引力，传递积极乐观情感，有怦然心动的感觉。</p>
    </td>
  </tr>
  <tr>
    <td width="80%"><img src="https://qzonestyle.gtimg.cn/qzone/qzact/act/external/qdesign/Design/QLanguage/DesignPrinciples/yuanze005.gif"/></td>
    <td>
      <p><b>亲和自然</b></p>
      <p>体验过程犹如与朋友打交道，亲和自然，懂我所想。</p>
    </td>
  </tr>
  <tr>
    <td width="80%"><img src="https://qzonestyle.gtimg.cn/qzone/qzact/act/external/qdesign/Design/QLanguage/DesignPrinciples/yuanze006.gif"/></td>
    <td>
      <p><b>自我有范</b></p>
      <p>用户能无压力表达自我，满足不同人群个性诉求。</p>
    </td>
  </tr>
</table>
<h3 id="语素">语素</h3>
<p>视觉基础是构成设计语言的最小单位，就像语素是语言中最小的音义结合体。在原子设计理论中，它属于最小粒度的元素，通常包括：色彩、布局、字体、图标等。</p>
<h4 id="色彩">色彩</h4>
<p>无论 UI 还是平面，颜色是视觉传达的最核心也是最基本的语言，不同的主色，会给人不同的视觉感受，同样的主色不同的配色，视觉感受也会不同。通常一款产品的色彩体系包含：品牌色、功能色、中立色三个部分：</p>
<p><strong>品牌色</strong>：代表品牌形象及 VI 识别的色彩，品牌色的数量可以一个也可以多个，用于主按钮、主 icon 等需要突出品牌特征的地方。</p>
<p><img src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*1c74TKxuEW4AAAAAAAAAAABkARQnAQ" alt=""></p>
<p><strong>功能色</strong>：代表明确的信息以及状态，如成功、出错、失败、提醒等。功能色的选取需要遵守用户对色彩的基本认知，如绿色代表成功，红色代表警示或失败。</p>
<p><img src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*QY4JRa92gHQAAAAAAAAAAABkARQnAQ" alt=""></p>
<p><strong>中立色</strong>：灰或饱和度低的颜色，用于界面设计中的字体、背景、边框、分割线等，中立色通常是按照透明度的方式实现。</p>
<p><img src="https://gw.alipayobjects.com/zos/antfincdn/8yMmB1lcD%24/colors.jpg" alt=""></p>
<h4 id="布局">布局</h4>
<p>空间布局是体系化视觉设计的起点，和传统的平面设计的不同之处在于，UI 界面的布局空间要基于「动态、体系化」的角度出发展开。在中后台视觉体系中定义布局系统，可以从 5 个方面出发：统一的画板尺寸、适配方案、网格单位、栅格、常用模度。</p>
<p><strong>统一画板</strong>：为了尽可能减少沟通与理解的成本，有必要在组织内部统一设计板的尺寸。</p>
<p><strong>适配</strong>：在设计过程中还需要建立适配的概念，根据具体情况判断系统是否需要进行适配，以及哪些区块需要考虑动态布局。</p>
<p>左右布局的适配方案：常被用于左右布局的设计方案中，常见的做法是将左边的导航栏固定，对右边的工作区域进行动态缩放。</p>
<p><img src="https://gw.alipayobjects.com/zos/rmsportal/vSqMhPolCtINKLvVVdLt.png" alt=""></p>
<p>上下布局的适配方案：常被用于上下布局的设计方案中，做法是对两边留白区域进行最小值的定义，当留白区域到达限定值之后再对中间的主内容区域进行动态缩放。</p>
<p><img src="https://gw.alipayobjects.com/zos/rmsportal/VQEiJqtZfvvdyZSKcEsE.png" alt=""></p>
<p><strong>网格单位</strong>：通过网格体系可以实现视觉体系的秩序。网格的基数为 8，不仅符合偶数的思路同时能够匹配多数主流的显示设备。通过建立网格的思考方式，还能帮助设计者快速实现布局空间的设计决策同时也能简化设计到开发的沟通损耗。</p>
<p><strong>栅格</strong>：以上下布局的结构为例，对内容区域进行 24 栅格的划分设置，如下图所示。页面中栅格的 Gutter 设定了定值，即浏览器在一定范围扩大或缩小，栅格的 Column 宽度会随之扩大或缩小，但 Gutter 的宽度值固定不变。</p>
<p><img src="https://gw.alipayobjects.com/zos/rmsportal/YPUZpPCzFgQHVxXCIAzq.png" alt=""></p>
<p><strong>模度</strong>：模度是为了帮助不同设计能力的设计者们在界面布局上的一致性和韵律感，统一设计到开发的布局语言，减少还原损耗。</p>
<p><img src="https://gw.alipayobjects.com/zos/rmsportal/ZBeDQMLMHLRfmUlUaaII.png" alt=""></p>
<h4 id="字体">字体</h4>
<p>字体是界面设计中最基本的构成之一。通过定义字体在设计上的使用规则，从而在阅读的舒适性上达到平衡。确定字体主要从下面四个方面出发：字体家族、主字体、字阶与行高、字重。</p>
<p><strong>字体家族</strong>：优秀的字体系统首先是要选择合适的字体家族。提供一套利于屏显的备用字体库，来维护在不同平台以及浏览器的显示下，字体始终保持良好的易读性和可读性，体现了友好、稳定和专业的特性。在中后台系统中，数字经常需要进行纵向对比展示，将数字的字体 <code>font-variant-numeric</code> 设置为 <code>tabular-nums</code>，使其为等宽字体。</p>
<p><strong>主字体</strong>：基于电脑显示器阅读距离（50 cm）以及最佳阅读角度（0.3），将自私设置为 14，以保证在多数常用显示器上的用户阅读效率最佳。</p>
<p><img src="https://gw.alipayobjects.com/zos/rmsportal/yriUFbqOPtVniYYiikfb.png" alt=""></p>
<p><strong>字阶与行高</strong>：字阶和行高决定着一套字体系统的动态与秩序之美。字阶是指一系列有规律的不同尺寸的字体。行高可以理解为一个包裹在字体外面的无形的盒子。</p>
<p><img src="https://gw.alipayobjects.com/zos/rmsportal/xpykKKFJQorFJltdXkie.png" alt=""></p>
<p><strong>字重</strong>：字重的选择同样基于秩序、稳定、克制的原则。多数情况下，只出现 regular 以及 medium 的两种字体重量，分别对应代码中的 400 和 500。在英文字体加粗的情况下会采用 semibold 的字体重量，对应代码中的 600。</p>
<table>
  <thead>
      <tr>
          <th><img src="https://gw.alipayobjects.com/zos/rmsportal/orIVrEOZIpjMbqZGiXEi.png" alt=""></th>
          <th><img src="https://gw.alipayobjects.com/zos/rmsportal/sasWhUzTGjlZKftukraH.png" alt=""></th>
          <th><img src="https://gw.alipayobjects.com/zos/rmsportal/QqxifAZlISrSUwnlonyx.png" alt=""></th>
      </tr>
  </thead>
  <tbody>
  </tbody>
</table>
<h4 id="图标">图标</h4>
<p>图标是 UI 设计中必不可少的组成。通常我们理解图标设计的含义，是将某个概念转换成清晰易读的图形，从而降低用户的理解成本，提升界面的美观度。在我们的企业级应用设计范围中，图标在界面设计的诸多元素中往往只占了很小的比重，在调用时也会被缩到比设计稿小很多倍的尺寸，加上在图形素材极度丰富并且便于获取的今天，在产品设计体系中实现一套美观、一致、易用、便于延展的图标体系往往会被不小心忽略掉。</p>
<h3 id="语句">语句</h3>
<p>组件就像由若干个语素组成的语句，比如一个基础按钮，通常就是由颜色、字体、边距等元素组成。而我们平时所说的组件库，其实就是一部词典，其中包含了设计系统中所需的基础组件与用法，在界面设计中也具有较高的通用性。</p>
<p><img src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*wsXrT7yQH2MAAAAAAAAAAABkARQnAQ" alt=""></p>
<h3 id="语义">语义</h3>
<p>符号是语言的载体，但符号本身没有意义，只有被赋予含义的符号才能够被使用，这时候语言就转化为信息，而语言的含义就是语义。在视觉传达设计中也一样，使用的图标或图形，需具备正确的语义属性。如果商场导视设计中非要使用「裙子」图标来代表「男厕」入口，如此混淆语义挑战公众认知，那就等着被投诉吧。</p>
<h3 id="语境">语境</h3>
<p>语境包含 3 个维度：一是流程意义上的上下文，二是产品属性中的语境，三是用户当下所处的环境。</p>
<p>当设计需要对上下文进行特别处理时，有可能对话的层级次序是受限于屏幕稀缺性，通常可采用 z-depth 叠加（Material Design 属性）、步骤条、元素关联转场动效等方式。举个常见的例子，当用户发起一个删除数据的请求时，界面会弹出一个二次确认的模态会话，用户点击确认之后才会执行删除操作。</p>
<p>针对用户当下所处的环境来适配界面语境，常见通过界面换肤的手法来实现，比如微信读书等阅读应用为用户提供白天模式或黑夜模式的选择。用户所处的外部环境因素可以很大程度上决定界面语言的应用，就好像在菜市场买东西要靠吼，在图书馆借书仅需要用肢体语言便能达成。</p>
<p><img src="https://image.uisdc.com/wp-content/uploads/2019/06/uisdc-yy-20190624-8.jpg" alt=""></p>
<h3 id="语气">语气</h3>
<p>交互界面通常需要使用说明或提示文案来指导用户完成操作，大多数情况下都是使用第二人称，就像在与用户对话，从以用户为中心的角度上讲，建议保持谦逊、友善的语气，尽可能避免使用晦涩的专业术语，谨慎使用带有强烈情感属性的感叹号，或过于口语化的语言。另外，语气的拿捏也将直接影响到与用户的距离感，以及当下的应景度。</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'>正确示例：使用检索可以快速搜索任务。</div>
<div class="blockquote" style='border-left: 4px solid #F51744;'>不良示例：你一定会爱上方便快捷的检索功能！</div>
<h3 id="语速">语速</h3>
<p>语速在这里指的是界面的信息密度，在不同的场合对语速的控制能够提升接受者的体验，视觉设计也同样需要注意把握间距与留白，网格系统在这里可以起到「节拍器」的作用，借助节拍器可以让设计更具节奏感。而交互意义上的语速，更多体现在操作路径的长度，以及动效的速率。</p>
<p>下图分别展示了 QQ 音乐和富途牛牛两种不同场景的「语速」：</p>
<p><img src="/images/cn/2021-08-08-a-glimpse-of-design-language/qq-music.png" alt=""></p>
<p><img src="/images/cn/2021-08-08-a-glimpse-of-design-language/futu-nn.png" alt=""></p>
<h3 id="响度">响度</h3>
<p>其实就好像我们说话可以通过音量大小来控制信息的可感知程度，希望接受者听清楚的就说大声一点。汤姆奥斯本（Tom Osborne）的视觉响度指南（Visual Loudness Guide）是一个如何系统地处理按钮和链接的例子，它们不是单独列出，而是作为一个套件呈现，并且根据每个元素的视觉冲击力会相应的拥有一个「响度」值。我们在构建设计语言系统时，也同样需要设置梯级「响度」的按钮、字重等组件来满足不同场景的表达需求。</p>
<p><img src="/images/cn/2021-08-08-a-glimpse-of-design-language/visual-loudness.png" alt=""></p>
<h2 id="设计语言列表">设计语言列表</h2>
<table>
  <thead>
      <tr>
          <th>企业</th>
          <th>设计语言</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><i class="icon icon-apple">Apple</i></td>
          <td><a href="https://developer.apple.com/design/">Human Interface Guidelines</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-google">Google</i></td>
          <td><a href="https://material.io/design">Material Design</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-microsoft">Microsoft</i></td>
          <td><a href="https://www.microsoft.com/design/fluent">Fluent Design</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-facebook">Facebook</i></td>
          <td><a href="https://design.facebook.com/">Facebook Design</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-adobe">Adobe</i></td>
          <td><a href="https://spectrum.adobe.com/">Spectrum</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-firefox">Firefox</i></td>
          <td><a href="https://design.firefox.com/photon">Photon Design</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-ibm">IBM</i></td>
          <td><a href="https://www.carbondesignsystem.com/">Carbon Design System</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-airbnb">Airbnb</i></td>
          <td><a href="https://airbnb.design/lottie/">Lottie</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-salesforce">Salesforce</i></td>
          <td><a href="https://www.lightningdesignsystem.com/">Lightning Design System</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-ant-group">蚂蚁金服</i></td>
          <td><a href="https://ant.design/">Ant Design</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-alibaba">阿里巴巴</i></td>
          <td><a href="https://alibaba.fusion.design/">Fusion Design</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-wechat">腾讯</i></td>
          <td><a href="https://weui.io/">WeUI</a></td>
      </tr>
      <tr>
          <td><i class="icon icon-qq">腾讯</i></td>
          <td><a href="https://qq.design/">Q Design</a></td>
      </tr>
  </tbody>
</table>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://en.wikipedia.org/wiki/Design_language">https://en.wikipedia.org/wiki/Design_language</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://www.uisdc.com/design-language">https://www.uisdc.com/design-language</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://ant.design/docs/spec/values-cn">https://ant.design/docs/spec/values-cn</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://ant.design/docs/spec/overview-cn">https://ant.design/docs/spec/overview-cn</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p><a href="https://fusion.design/pc/doc/design/%E8%AE%BE%E8%AE%A1%E6%A6%82%E8%A7%88/12">https://fusion.design/pc/doc/design/设计概览/12</a>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p><a href="https://qq.design/design/QLanguage/Concept/">https://qq.design/design/QLanguage/Concept/</a>&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p><a href="https://qq.design/design/QLanguage/Principles/">https://qq.design/design/QLanguage/Principles/</a>&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>大数据 SQL 性能调优 (Big Data SQL Performance Tuning)</title><link>https://zeqiang.fun/cn/2021/05/big-data-sql-performance-tuning/</link><pubDate>Sun, 23 May 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/05/big-data-sql-performance-tuning/</guid><description><![CDATA[
        <p>在日常工作中，数据处理和分析在研发、产品和运营等多个领域起着重要的作用。在海量数据处理和分析中，SQL 是一项基础且重要的能力。一个优秀的 SQL Boy 和茶树姑的 SQL 代码除了保持简单、可读和易于维护的<a href="/cn/2021/05/sql-style-guide/">样式风格</a>外，还需要具备良好的执行性能，准确且高效的计算出结果才能让你在工作中决胜于千里之外。</p>
<p>影响 SQL 执行性能的主要因素可以总结为如下几项：</p>
<ol>
<li>计算资源量（CPU，内存，网络等）</li>
<li>计算数据量（输入和输出的记录数）</li>
<li>计算复杂度（业务逻辑复杂程度和对应的 SQL 实现和执行）</li>
</ol>
<p>计算资源量是一个前置制约因素，理论上更多的资源能够带来更快的计算效果。计算数据量也可以认为是一个前置制约因素，理论上更大的数据量会导致计算速度降低，但对于复杂的计算逻辑，通过合理的 SQL 可以更好的控制计算过程中的数据量，从而提升 SQL 性能。计算复杂度是影响 SQL 性能的关键因素，复杂的业务逻辑必然比简单的业务逻辑处理时间要长，相同业务逻辑的不同 SQL 实现也会影响运行效率，这就要求我们对业务逻辑进行全面的理解，对实现 SQL 进行合理优化，从而提升计算速度。</p>
<h2 id="执行引擎">执行引擎</h2>
<p>SQL 是用于一种用于数据定义和数据操纵的特定目的的编程语言 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。SQL 虽然有 ISO 标准 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>，但大部分 SQL 代码在不同的数据库系统中并不具有完全的跨平台性。不同的执行引擎也会对 SQL 的语法有相应的改动和扩展，同时对于 SQL 的执行也会进行不同的适配和优化。因此，脱离执行引擎的 SQL 性能优化是不可取的。</p>
<h3 id="hive">Hive</h3>
<p>Apache Hive 是一个建立在 Hadoop 架构之上的数据仓库。可以将结构化的数据文件映射为一张数据库表，并提供简单的 SQL 查询功能，可以将 SQL 语句转换为 MapReduce 任务进行运行。因此 MapReduce 是 Hive SQL 运行的核心和根基。</p>
<p>我们以 Word Count 为例简单介绍一下 MapReduce 的原理和过程，Word Count 的 MapReduce 处理过程如下图所示：</p>
<p><img src="/images/cn/2021-05-23-big-data-sql-performance-tuning/word-count-mapreduce.png" alt=""></p>
<ol>
<li><strong>Input</strong>：程序的输入数据。</li>
<li><strong>Splitting</strong>：讲输入数据分割为若干部分。</li>
<li><strong>Mapping</strong>：针对 Splitting 分割的每个部分，对应有一个 Map 程序处理。本例中将分割后的文本统计成 <code>&lt;K,V&gt;</code> 格式，其中 <code>K</code> 为单词，<code>V</code> 为该单词在这个 Map 中出现的次数。</li>
<li><strong>Shuffling</strong>：对 Mapping 的相关输出结果进行合并。本例中将具有相同 <code>K</code> 的统计结果合并到一起。</li>
<li><strong>Reducing</strong>：对 Shuffling 合并的结果进行汇总。本例中讲相同 <code>K</code> 的 <code>V</code> 值进行加和操作并返回单个统计结果。</li>
<li><strong>Merged</strong>：对 Reducing 的结果进行融合形成最终输出。</li>
</ol>
<h3 id="spark">Spark</h3>
<p>Apache Spark 是一个用于大规模数据处理的统一分析引擎，Spark SQL 则作为 Apache Spark 用于处理结构化数据的模块。</p>
<p>Spark 中常见的概念有：</p>
<ol>
<li><strong>RDD</strong>：Resilient Distributed Dataset，弹性分布式数据集，是分布式内存中一个抽象概念，提供了一种高度受限的共享内存模型。</li>
<li><strong>DAG</strong>：Directed Acyclic Graph，有向无环图，反应了 RDD 之间的依赖关系。</li>
<li><strong>Driver Program</strong>：控制程序，负责为 Application 创建 DAG，通常用 <code>SparkContext</code> 代表 Driver Program。</li>
<li><strong>Cluster Manager</strong>：集群管理器，负责分配计算资源。</li>
<li><strong>Worker Node</strong>：工作节点，负责具体计算。</li>
<li><strong>Executor</strong>：运行在 Worker Node 上的一个<a href="/cn/2021/04/process-thread-and-coroutine-theory/">进程</a>，负责运行 Task，并为 Application 存储数据。</li>
<li><strong>Application</strong>：Spark 应用程序，包含多个 Executor。</li>
<li><strong>Task</strong>：任务，运行在 Executor 上的工作单元，是 Executor 中的一个<a href="/cn/2021/04/process-thread-and-coroutine-theory/">线程</a>。</li>
<li><strong>Stage</strong>：一组并行的 Task，Spark 一般会根据 Shuffle 类算子（例如：<code>reduceByKey</code> 或 <code>join</code> 等）划分 Stage。</li>
<li><strong>Job</strong>：一组 Stage 的集合，一个 Job 包含多个 RDD 及作用于 RDD 上的操作。</li>
</ol>
<p>相关概念构成了 Spark 的整体架构，如下图所示：</p>
<p><img src="/images/cn/2021-05-23-big-data-sql-performance-tuning/spark-architecture.png" alt=""></p>
<p>在 Spark 中，一个任务的执行过程大致分为 4 个阶段，如下图所示：</p>
<p><img src="/images/cn/2021-05-23-big-data-sql-performance-tuning/spark-scheduling.jpeg" alt=""></p>
<ol>
<li>定义 RDD 的 Transformations 和 Actions 算子 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>，并根据这些算子形成 DAG。</li>
<li>根据形成的 DAG，DAGScheduler 将其划分为多个 Stage，每个 Stage 包含多个 Task。</li>
<li>DAGScheduler 将 TaskSet 交由 TaskScheduler 运行，并将执行完毕后的结果返回给 DAGScheduler。</li>
<li>TaskScheduler 将任务分发到每一个 Worker 去执行，并将执行完毕后的结果返回给 TaskScheduler。</li>
</ol>
<p>Spark 相比于 Hadoop 的主要改进有如下几点：</p>
<ol>
<li>Hadoop 的 MapReduce 的中间结果都会持久化到磁盘上，而 Spark 则采用基于内存的计算（内存不足时也可选持久化到磁盘上），从而减少 Shuffle 数据，进而提升计算速度。</li>
<li>Spark 采用的 DAG 相比于 Hadoop 的 MapReduce 具有更好的容错性和可恢复性，由于 Spark 预先计算出了整个任务的 DAG，相比于 MapReduce 中各个操作之间是独立的，这更有助于进行全局优化。</li>
</ol>
<h3 id="presto">Presto</h3>
<p>Presto 是一种用于大数据的高性能分布式 SQL 查询引擎。Presto 与 Hive 执行任务过程的差异如下图所示：</p>
<p><img src="/images/cn/2021-05-23-big-data-sql-performance-tuning/hive-vs-presto.png" alt=""></p>
<p>Presto 的优点主要有如下几点：</p>
<ol>
<li>基于内存计算，减少了磁盘 IO，从而计算速度更快。</li>
<li>能够连接多个数据源，跨数据源连表查询。</li>
</ol>
<p>虽然 Presto 能够处理 PB 级数据，但并不代表 Presto 会把 PB 级别数据都放在内存中计算。而是根据场景，例如 <code>COUNT</code> 和 <code>AVG</code> 等聚合操作，是边读数据边计算，再清理内存，再读取数据计算，这种情况消耗的内存并不高。但是连表查询，可能产生大量的临时数据，从而速度会变慢。</p>
<h2 id="性能调优">性能调优</h2>
<p>本节关于 SQL 性能调优的建议主要针对 Hive，Spark 和 Presto 这类大数据 OLAP 执行引擎设计，其他执行引擎不一定完全适用。</p>
<p>下文性能调优中均以如下两张表为例进行说明：</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS sku_order
(
  order_id STRING '订单 ID',
  sku_id STRING '商品 ID',
  sale_quantity BIGINT '销售数量' 
)
COMMENT '商品订单表'
PARTITIONED BY
(
  dt STRING COMMENT '日期分区'
)
;
</code></pre>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS sku_info
(
  sku_id STRING '商品 ID',
  sku_name STRING '商品名称',
  category_id STRING '品类 ID',
  category_name STRING '品类名称'
)
COMMENT '商品信息表'
</code></pre>
<h3 id="减少数据量">减少数据量</h3>
<ul>
<li>限定查询分区。对于包含分区的数据表（例如：日期分区），通过合理限定分区来减少数据量，避免全表扫描。</li>
<li>限定查询字段。避免使用 <code>SELECT *</code>，仅选择需要的字段。<code>SELECT *</code> 会通过查询元数据获取字段信息，同时查询所有字段会造成更大的网络开销。</li>
<li>在关联前过滤数据。应在进行数据表关联之前按照业务逻辑进行数据过滤，从而提升执行效率。</li>
</ul>
<h3 id="数据倾斜">数据倾斜</h3>
<p>在 Shuffle 阶段，需要将各节点上相同的 Key 拉取到某个节点（Task）上处理，如果某个 Key 对应的数据量特别大则会产生数据倾斜。结果就是该 Task 运行的时间要远远大于其他 Task 的运行时间，从而造成作业整体运行缓慢，数据量过大甚至可能导致某个 Task 出现 OOM。</p>
<p>在 SQL 中主要有如下几种情况会产生数据倾斜：</p>
<ul>
<li><code>JOIN</code> 导致的数据倾斜：两表关联，关联字段的无效值（例如：<code>NULL</code>）或有效值过多，可能会导致数据倾斜。</li>
<li><code>GROUP BY</code> 导致的数据倾斜：当 <code>GROUP BY</code> 的字段（或字段组合）中，Key 分布不均，可能会导致数据倾斜。</li>
<li><code>DISTINCT</code> 导致的数据倾斜：当 <code>DISTINCT</code> 的字段（或字段组合）中，Key 分布不均，可能会导致数据倾斜。</li>
</ul>
<p>对于不同的数据倾斜情况，解决方案如下：</p>
<ul>
<li>
<p>对于 <code>JOIN</code> 中的无效值进行过滤。</p>
<pre><code class="language-sql">SELECT
  category_name,
  SUM(sale_quantity) AS sale_quantity
FROM
  (
    SELECT
      sku_id,
      sale_quantity
    FROM
      sku_order
    WHERE
      dt = '20210523'
      AND sku_id IS NOT NULL
  ) AS sku_order_filtered
LEFT JOIN
  sku_info
ON
  sku_order_filtered.sku_id = sku_info.sku_id
GROUP BY
  category_name
;
</code></pre>
</li>
<li>
<p>对于 <code>JOIN</code> 开启 Map Join 或 Broadcast Join 策略，将小表广播到每个 Executor 上来避免产生 Shuffle，从而使得 <code>JOIN</code> 能够快速完成。</p>
</li>
</ul>
<pre><code class="language-shell">set spark.sql.autoBroadcastJoinThreshold=10485760;
</code></pre>
<ul>
<li>
<p>对于 <code>JOIN</code> 中存在数据倾斜的 KEY 进行打散处理。</p>
<pre><code class="language-sql">SELECT
  category_name,
  SUM(sale_quantity) AS sale_quantity
FROM
  (
    SELECT
      IF(sku_id IN (0000, 9999), CONCAT(sku_id, '_', CEIL(RAND() * 10)), sku_id) AS sku_id,
      sale_quantity
    FROM
      sku_order
    WHERE
      dt = '20210523'
  ) AS sku_order_modified
LEFT JOIN
  (
    SELECT
      sku_id,
      category_name,
    FROM
      sku_info
    WHERE
      sku_id NOT IN (0000, 9999)
    UNION ALL
    SELECT
      CONCAT(sku_id, '_', suffix) AS sku_id,
      category_name
    FROM
      (
        SELECT
          sku_id,
          SPLIT('1,2,3,4,5,6,7,8,9,10', ',') AS suffix_list,
          category_name
        FROM
          sku_info
        WHERE
          sku_id IN (0000, 9999)
      ) sku_info_tmp LATERAL VIEW EXPLODE(suffix_list) sku_info_suffix AS suffix
  ) sku_info_all
ON
  sku_order_modified.sku_id = sku_info_all.sku_id
GROUP BY
  category_name
;
</code></pre>
</li>
<li>
<p>对于 <code>GROUP BY</code> 导致的数据倾斜采用两步聚合。</p>
<pre><code class="language-sql">SELECT
  IF(sku_is_null = 1, NULL, sku_id) AS sku_id,
  SUM(sale_quantity) AS sale_quantity
FROM
  (
    SELECT
      sku_id,
      sku_is_null,
      SUM(sale_quantity) AS sale_quantity
    FROM
    (
      SELECT
        IF(sku_id IS NULL, CONCAT(sku_id, CEIL(RAND() * 10)), sku_id) AS sku_id,
        IF(sku_id IS NULL, 1, 0) AS sku_is_null,
        sale_quantity
      FROM
        sku_order
      WHERE
        dt = '20210523'
    ) sku_order_modified
    GROUP BY
      sku_id,
      sku_is_null
  ) sku_order_group
GROUP BY
  IF(sku_is_null = 1, NULL, sku_id)
;
</code></pre>
</li>
<li>
<p>对于 <code>DISTINCT</code> 导致的数据倾斜，可以改写为 <code>GROUP BY</code> 实现，从而通过多个 Task 计算避免数据倾斜。</p>
<pre><code class="language-sql">/* COUNT DISTINCT */
SELECT
  COUNT(DISTINCT sku_id) AS cnt
FROM
  sku_order
WHERE
  dt = '20210523'
;

/* GROUP BY */
SELECT
  COUNT(1) AS cnt
FROM
  (
    SELECT
      sku_id
    FROM
      sku_order
    WHERE
      dt = '20210523'
    GROUP BY
      sku_id
  ) AS sku_stats
;
</code></pre>
</li>
</ul>
<h3 id="其他建议">其他建议</h3>
<ul>
<li>
<p>使用 <a href="https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression">Common Table Expressions (CTEs)</a> 而非子查询。<code>WITH</code> 语句产生的结果类似临时表，可以重复使用，从而避免相同逻辑业务重复计算。</p>
</li>
<li>
<p>使用 <code>LEFT SEMI JOIN</code> 而非 <code>IN</code> 和子查询。Hive 在 0.13 后的版本中才在 <code>IN</code> 和 <code>NOT IN</code> 中支持子查询。</p>
<pre><code class="language-sql">/* BAD */
SELECT
  order_id,
  sku_id,
  sale_quantity
FROM
  sku_order
WHERE
  sku_id IN (SELECT sku_id FROM sku_info)
;

/* GOOD */
SELECT
  order_id,
  sku_id,
  sale_quantity
FROM
  sku_order
LEFT SEMI JOIN
  sku_info
ON
  sku_order.sku_id = sku_info.sku_id
;
</code></pre>
</li>
</ul>
<h2 id="参数调优">参数调优</h2>
<p>除了 SQL 本身逻辑的优化外，执行引擎的相关参数设置也会影响 SQL 的执行性能。本小节以 Spark 引擎为例，总结相关参数的设置及其影响。</p>
<h3 id="动态分区">动态分区</h3>
<pre><code class="language-shell">/* 以下 Hive 参数对 Spark 同样有效 */

/* 是否启用动态分区功能 */
set hive.exec.dynamic.partition=true;

/* strict 表示至少需要指定一个分区，nonstrict 表示可以全部动态指定分区 */
set hive.exec.dynamic.partition.mode=nonstrict;

/* 动态生成分区的最大数量 */
set hive.exec.max.dynamic.partitions=1000;
</code></pre>
<h3 id="资源申请">资源申请</h3>
<pre><code class="language-shell">/* 每个 Executor 中的核数 */
set spark.executor.cores=2;

/* Executor 的内存总量。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead &lt;= 16G。 */
set spark.executor.memory=4G;

/* Executor 的堆外内存大小，由 YARN 控制，单位为 MB。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead &lt;= 16G。 */
set spark.yarn.executor.memoryOverhead=1024;

/* Driver 的内存总量，主要用于存放任务执行过程中 Shuffle 元数据，以及任务中 Collect 的数据，Broadcast 的小表也会先存放在 Driver 中。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead &lt;= 16G。 */
set spark.driver.memory=8G;

/* Driver 的堆外内存，由 YARN 控制，单位为 MB。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead &lt;= 16G。 */
set spark.yarn.driver.memoryOverhead=1024;

/* storage memory + execution memory 占总内存（java heap-reserved memory）的比例。executor jvm 中内存分为 storage、execution 和 other 内存。storage 存放缓存 RDD 数据，execution 存放 Shuffle 过程的中间数据，other 存放用户定义的数据结构或 Spark 内部元数据。如果用户自定义数据结构较少，可以将该参数比例适当上调。 */
set spark.memory.fraction=0.7;
</code></pre>
<h3 id="动态分配">动态分配</h3>
<p>开启动态分配，Spark 可以根据当前作业负载动态申请和释放资源：</p>
<pre><code class="language-shell">set spark.dynamicAllocation.enabled=true;
</code></pre>
<p>同时需要设置同一时刻可以申请的最小和最大 Executor 数量：</p>
<pre><code class="language-shell">set spark.dynamicAllocation.minExecutors=10;
set spark.dynamicAllocation.maxExecutors=100;
</code></pre>
<h3 id="小文件合并">小文件合并</h3>
<pre><code class="language-shell">/* 小文件合并阈值，如果生成的文件平均大小低于阈值会额外启动一轮 Stage 进行小文件的合并，默认不合并小文件。 */
set spark.sql.mergeSmallFileSize=67108864;

/* 	设置额外的合并 Job 时的 Map 端输入大小 */
set spark.sql.targetBytesInPartitionWhenMerge=67108864;

/* 设置 Map 端输入的合并文件大小 */
set spark.hadoopRDD.targetBytesInPartition=67108864;
</code></pre>
<p>在决定一个目录是否需要合并小文件时，会统计目录下的平均大小，然后和 <code>spark.sql.mergeSmallFileSize</code> 比较。在合并文件时，一个 Map Task 读取的数据量取决于下面三者的较大值：<code>spark.sql.mergeSmallFileSize</code>，<code>spark.sql.targetBytesInPartitionWhenMerge</code>，<code>spark.hadoopRDD.targetBytesInPartition</code>。</p>
<h3 id="shuffle-相关">Shuffle 相关</h3>
<p>当大表 <code>JOIN</code> 小表时，如果小表足够小，可以将小表广播到所有 Executor 中，在 Map 阶段完成 <code>JOIN</code>。如果该值设置太大，容易导致 Executor 出现 OOM。</p>
<pre><code class="language-shell">/* 10 * 1024 * 1024, 10MB */
set spark.sql.autoBroadcastJoinThreshold=10485760;
</code></pre>
<p>设置 Reduce 阶段的分区数：</p>
<pre><code class="language-shell">set spark.sql.shuffle.partitions=1000;
</code></pre>
<p>设置过大可能导致很多 Reducer 同时向一个 Mapper 拉取数据，导致 Mapper 由于请求压力过大而挂掉或响应缓慢，从而 fetch failed。</p>
<p>一些其他 Shuffle 相关的配置如下：</p>
<pre><code class="language-shell">/* 同一时刻一个 Reducer 可以同时拉取的数据量大小 */
set spark.reducer.maxSizeInFlight=25165824;

/* 同一时刻一个 Reducer 可以同时产生的请求数 */
set spark.reducer.maxReqsInFlight=10;

/* 同一时刻一个 Reducer 向同一个上游 Executor 拉取的最多 Block 数 */
set spark.reducer.maxBlocksInFlightPerAddress=1;

/* Shufle 请求的 Block 超过该阈值就会强制落盘，防止一大堆并发请求将内存占满 */
set spark.reducer.maxReqSizeShuffleToMem=536870911;

/* Shuffle 中连接超时时间，超过该时间会 fetch failed */
set spark.shuffle.io.connectionTimeout=120;

/* Shuffle 中拉取数据的最大重试次数 */
set spark.shuffle.io.maxRetries=3;

/* Shuffle 重试的等待间隔 */
set spark.shuffle.io.retryWait=5;
</code></pre>
<h3 id="orc-相关">ORC 相关</h3>
<p>ORC 文件的格式如下图所示：</p>
<p><img src="/images/cn/2021-05-23-big-data-sql-performance-tuning/orc-file-layout.png" alt=""></p>
<p>其中，Postscript 为文件描述信息，包括 File Footer 和元数据长度、文件版本、压缩格式等；File Footer 是文件的元数据信息，包括数据量、每列的统计信息等；文件中的数据为 Stripe，每个 Stripe 包括索引数据、行数据和 Stripe Footer。更多有关 ORC 文件格式的信息请参见 <a href="https://orc.apache.org/specification/ORCv1/">ORC Specification v1
</a>。</p>
<p>在读取 ORC 压缩表时，可以控制生成 Split 的策略，包括：</p>
<ul>
<li><strong>BI</strong>：以文件为力度进行 Split 划分</li>
<li><strong>ETL</strong>：将文件进行切分，多个 Stripe 组成一个 Split</li>
<li><strong>HYBRID</strong>：当文件的平均大小大于 Hadoop 最大 Split 值时使用 ETL 策略，否则使用 BI 策略</li>
</ul>
<p>对于一些较大的 ORC 表，可能其 Footer 较大，ETL 策略可能会导致从 HDFS 拉取大量的数据来切分 Split，甚至会导致 Driver 端 OOM，因此这类表的读取建议采用 BI 策略。对于一些较小，尤其是有数据倾斜的表（即大量 Stripe 存储于少数文件中），建议使用 ETL 策略。</p>
<p>一些其他 ORC 相关的配置如下：</p>
<pre><code class="language-shell">/* ORC 谓词下推，默认是关闭 */
set spark.sql.orc.filterPushdown=true;

/* 	开启后，在 Split 划分时会使用 Footer 信息 */
set spark.sql.orc.splits.include.file.footer=true;

/* 设置每个 Stripe 可以缓存的大小 */
set spark.sql.orc.cache.stripe.details.size=10000;

/* 当为 true 时，Spark SQL 的谓语将被下推到 Hive Metastore 中，更早的消除不匹配的分区。 */
set spark.sql.hive.metastorePartitionPruning=true;

/* 读 ORC 表时，设置小文件合并的阈值，低于该值的 Split 会合并在一个 Task 中执行 */
set spark.hadoop.mapreduce.input.fileinputformat.split.minsize=67108864;

/* 读 ORC 表时，设置一个 Split 的最大阈值，大于该值的 Split 会切分成多个 Split。 */
set spark.hadoop.mapreduce.input.fileinputformat.split.maxsize=268435456;

/* 文件提交到HDFS上的算法：1. version=1 是按照文件提交。2. version=2 是批量按照目录进行提交，可以极大节约文件提交到 HDFS 的时间，减轻 NameNode 压力。 */
set spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version=2;
</code></pre>
<h3 id="自适应执行">自适应执行</h3>
<pre><code class="language-shell">/* 开启动态执行 */
set spark.sql.adaptive.enabled=true;
</code></pre>
<p>当自适应执行开启后，调整 <code>spark.sql.adaptive.shuffle.targetPostShuffleInputSize</code>，当 Mapper 端两个 Partition 的数据合并后小于该值时，Spark 会将两个 Partition 合并到一个 Reducer 进行处理。</p>
<pre><code class="language-shell">set spark.sql.adaptive.shuffle.targetPostShuffleInputSize=67108864;
</code></pre>
<p>当自适应执行开启后，有时会导致过多分区被合并，为了防止分区过少影响性能，可以设置如下参数：</p>
<pre><code class="language-shell">set spark.sql.adaptive.minNumPostShufflePartitions=10;
</code></pre>
<p>一些其他自适应执行相关的配置如下：</p>
<pre><code class="language-shell">/* 开启动态调整 Join */
set spark.sql.adaptive.join.enabled=true;

/* 设置 SortMergeJoin 转 BroadcastJoin 的阈值，如果不设置该参数，该阈值和 spark.sql.autoBroadcastJoinThreshold 值相等。 */
set spark.sql.adaptiveBroadcastJoinThreshold=33554432;

/* 是否允许为了优化 Join 而增加 Shuffle，默认是 false */
set spark.sql.adaptive.allowAddititionalShuffle=false;

/* 开启自动处理 Join 时的数据倾斜 */
set spark.sql.adaptive.skewedJoin.enabled=true;

/* 控制处理一个倾斜 Partition 的 Task 个数上限，默认值是 5 */
set spark.sql.adaptive.skewedPartitionMaxSplits=100;

/* 设置一个 Partition 被视为倾斜 Partition 的行数下限，行数低于该值的 Partition 不会被当做倾斜 Partition 处理。 */
set spark.sql.adaptive.skewedPartitionRowCountThreshold=10000000;

/* 设置一个 Partition 被视为倾斜 Partition 的大小下限，大小小于该值的 Partition 不会被当做倾斜 Partition 处理。 */
set spark.sql.adaptive.skewedPartitionSizeThreshold=536870912;

/* 设置倾斜因子，当一个 Partition 满足以下两个条件之一，就会被视为倾斜 Partition：1. 大小大于 spark.sql.adaptive.skewedPartitionSizeThreshold 的同时大于各 Partition 大小中位数与该因子的乘积。2. 行数大于 spark.sql.adaptive.skewedRowCountThreshold 的同时大于各 Partition 行数中位数与该因子的乘积。*/
set spark.sql.adaptive.skewedPartitionFactor=10;
</code></pre>
<h3 id="推测执行">推测执行</h3>
<pre><code class="language-shell">/* Spark 推测执行开关，默认是 true */
set spark.speculation=true;

/* 开启推测执行后，每隔该值时间会检测是否有需要推测执行的 Task */
set spark.speculation.interval=1000ms;

/* 当成功 Task 占总 Task 的比例超过 spark.speculation.quantile，统计成功 Task 运行时间中位数乘以 spark.speculation.multiplier 得到推测执行阈值，当在运行的任务超过这个阈值就会启动推测执行。当资源充足时，可以适当减小这两个值。 */
set spark.speculation.quantile=0.99;
set spark.speculation.multiplier=3;
</code></pre>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://zh.wikipedia.org/wiki/SQL">https://zh.wikipedia.org/wiki/SQL</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://www.iso.org/committee/45342.html">https://www.iso.org/committee/45342.html</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://spark.apache.org/docs/latest/rdd-programming-guide.html">https://spark.apache.org/docs/latest/rdd-programming-guide.html</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>SQL 样式指南 (SQL Style Guide)</title><link>https://zeqiang.fun/cn/2021/05/sql-style-guide/</link><pubDate>Tue, 04 May 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/05/sql-style-guide/</guid><description><![CDATA[
        <p>代码样式指南主要用于规范项目中代码的一致性，使得代码简单、可读和易于维护，从一定程度上也影响代码的质量。一句话概括如何评价代码的质量：</p>
<blockquote>
<p>衡量代码质量的唯一有效标准：WTF/min &ndash; <a href="https://en.wikipedia.org/wiki/Robert_C._Martin">Robert C. Martin</a></p>
</blockquote>
<p><img src="/images/cn/2021-05-04-sql-style-guide/wtfsm.jpg" alt=""></p>
<p>Google 针对大多数编程语言（例如：C/C++，Java，JavaScript，Python，R 等）都整理了相关的<a href="https://google.github.io/styleguide/">代码风格</a>，但对于 SQL 这种用于数据库查询特殊目的的编程语言并没有整理对应的风格。同其他编程语言代码风格一样，没有哪种风格是最好的，只要在项目中采用统一合理的风格即可。</p>
<p>本文参考的 SQL 样式指南有如下几种：</p>
<ol>
<li><a href="https://www.sqlstyle.guide/zh/">https://www.sqlstyle.guide/zh/</a></li>
<li><a href="https://about.gitlab.com/handbook/business-technology/data-team/platform/sql-style-guide/">https://about.gitlab.com/handbook/business-technology/data-team/platform/sql-style-guide/</a></li>
<li><a href="https://docs.telemetry.mozilla.org/concepts/sql_style.html">https://docs.telemetry.mozilla.org/concepts/sql_style.html</a></li>
<li><a href="https://github.com/mattm/sql-style-guide">https://github.com/mattm/sql-style-guide</a></li>
</ol>
<p>本文给出的 SQL 样式指南基于上述几种进行整理和修改。</p>
<h2 id="一般原则">一般原则</h2>
<ul>
<li>使用一致的、描述性名称。</li>
<li>使用空格（2 个或 4 个，项目中保持一致），避免使用 TAB 缩进。</li>
<li>在 SQL 中加入必要的注释，块注释使用 <code>/* */</code>，行注释使用 <code>--</code>，并在末尾换行。</li>
<li>使用单引号 <code>'</code> 作为被引号包裹的标识符。</li>
<li>运算符前后添加空格，逗号 <code>,</code> 后添加空格，避免行尾有空格。</li>
<li>每行代码不超过 80 个字符。</li>
</ul>
<h2 id="命名惯例">命名惯例</h2>
<ul>
<li>避免名称和保留字一样。</li>
<li>关键词、函数名称采用大写，字段名、表名采用小蛇式（lower snake case）命名。</li>
<li>名称要以字母开头，不能以下划线结尾，名称中仅可以使用字母、数字和下划线。</li>
<li>不要在名字中出现连续下划线 <code>__</code>，这样很难辨认。</li>
<li>尽量避免使用缩写词。使用时一定确定这个缩写简明易懂。</li>
<li>字段名总是使用单数。</li>
</ul>
<h2 id="对齐和换行">对齐和换行</h2>
<ul>
<li>
<p>避免<a href="https://zh.wikipedia.org/wiki/%E5%B7%9D%E6%B5%81_(%E5%AD%97%E4%BD%93%E6%8E%92%E5%8D%B0%E5%AD%A6)">川流</a>式对齐代码。</p>
<pre><code class="language-sql">/* Good */
SELECT id
FROM table_name
WHERE column = &quot;test&quot;
;
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT id
  FROM talbe_name
 WHERE column = &quot;test&quot;
;
</code></pre>
</li>
<li>
<p>多个元素组合无法呈现在一行中时，应将第一个元素另起一行。</p>
<pre><code class="language-sql">/* Good */
SELECT
  CASE postcode
    WHEN 'BN1' THEN 'Brighton'
    WHEN 'EH1' THEN 'Edinburgh'
  END AS city
FROM table_name
;
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT
  CASE postcode WHEN 'BN1' THEN 'Brighton'
                WHEN 'EH1' THEN 'Edinburgh'
  END AS city
FROM table_name
;
</code></pre>
</li>
<li>
<p>由括号构成的多行，结尾括号应单独一行。</p>
<pre><code class="language-sql">/* Good */
SELECT id
FROM table_name
WHERE postcode IN (
  'looooooooooooooooooooooooong_BN1',
  'loooooooooooooooooooooooooog_EH1'
)
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT id
FROM table_name
WHERE postcode IN ('looooooooong_BN1',
                   'looooooooong_EH1')
</code></pre>
</li>
<li>
<p>多行采用右侧逗号和左侧关键字连接。</p>
<pre><code class="language-sql">/* Good */
SELECT
  id,
  name
FROM
  talbe_name
WHERE
  id &gt; 1
  AND name LIKE &quot;%Tom%&quot;
;
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT
  id
  , name
FROM
  table_name
WHERE
  id &gt; 1 AND
  name LIKE &quot;%Tom%&quot;
;
</code></pre>
</li>
<li>
<p>根关键词建议单独一行，多个参数单独一行。</p>
<pre><code class="language-sql">/* Good */
SELECT
  id,
  name
FROM
  table_name
WHERE
  id &gt; 1
  AND name LIKE &quot;%Tom%&quot;
LIMIT
  10
;
</code></pre>
<pre><code class="language-sql">/* Acceptable */
SELECT
  id,
  name
FROM table_name
WHERE
  id &gt; 1
  AND name LIKE &quot;%Tom%&quot;
LIMIT 10
;
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT id, name
FROM table_name
WHERE
  id &gt; 1
  AND name LIKE &quot;%Tom%&quot;
LIMIT 10
;
</code></pre>
</li>
</ul>
<h2 id="明确指定">明确指定</h2>
<ul>
<li>
<p>使用 <code>AS</code> 明确指定别名，而非隐式。</p>
<pre><code class="language-sql">/* Good */
SELECT
  table_name_1.id AS user_id,
  table_name_2.name AS user_name
FROM
  looooooooong_table_name_1 AS table_name_1
LEFT JOIN
  looooooooong_table_name_2 AS table_name_2
ON
  table_name_1.id = table_name_2.id
;
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT
  table_name_1.id user_id,
  table_name_2.name user_name
FROM
  looooooooong_table_name_1 table_name_1
LEFT JOIN
  looooooooong_table_name_2 table_name_2
ON
  table_name_1.id = table_name_2.id
;
</code></pre>
</li>
<li>
<p>避免使用隐式关联。</p>
<pre><code class="language-sql">/* Good */
SELECT
  table_name_1.id,
  table_name_2.name
FROM
  table_name_1
INNER JOIN
  table_name_2
ON
  table_name_1.id = table_name_2.id
;
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT
  table_name_1.id,
  table_name_2.name
FROM
  table_name_1,
  table_name_2
ON
  table_name_1.id = table_name_2.id
;
</code></pre>
</li>
<li>
<p>明确关联类型。</p>
<pre><code class="language-sql">/* Good */
SELECT
  table_name_1.id,
  table_name_2.name
FROM
  table_name_1
INNER JOIN
  table_name_2
ON
  table_name_1.id = table_name_2.id
;
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT
  table_name_1.id,
  table_name_2.name
FROM
  table_name_1
JOIN
  table_name_2
ON
  table_name_1.id = table_name_2.id
;
</code></pre>
</li>
<li>
<p>明确指定分组列。</p>
<pre><code class="language-sql">/* Good */
SELECT
  submission_date,
  normalized_channel IN ('nightly', 'aurora', 'beta') AS is_prerelease,
  COUNT(*) AS count
FROM
  telemetry.clients_daily
WHERE
  submission_date &gt; '2019-07-01'
GROUP BY
  submission_date,
  is_prerelease
;
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT
  submission_date,
  normalized_channel IN ('nightly', 'aurora', 'beta') AS is_prerelease,
  COUNT(*) AS count
FROM
  telemetry.clients_daily
WHERE
  submission_date &gt; '2019-07-01'
GROUP BY
  1, 2
;
</code></pre>
</li>
</ul>
<h2 id="子查询">子查询</h2>
<ul>
<li>
<p>尽量使用 <a href="https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression">Common Table Expressions (CTEs)</a> 而非子查询。</p>
<pre><code class="language-sql">/* Good */
WITH sample AS (
  SELECT
    client_id,
    submission_date
  FROM
    main_summary
  WHERE
    sample_id = '42'
)

SELECT *
FROM sample
LIMIT 10
</code></pre>
<pre><code class="language-sql">/* Bad */
SELECT *
FROM (
  SELECT
    client_id,
    submission_date
  FROM
    main_summary
  WHERE
    sample_id = '42'
)
LIMIT 10
</code></pre>
</li>
<li>
<p>尽量在 CTEs 中处理查询而非主语句中。</p>
<pre><code class="language-sql">/* Good */
WITH backings_per_category AS (
  SELECT
    ...
), backers AS (
  SELECT
    backings_per_category.backer_id,
    COUNT(backings_per_category.id) AS projects_backed_per_category
  INNER JOIN ksr.users AS users ON users.id = backings_per_category.backer_id
  GROUP BY backings_per_category.backer_id
), backers_and_creators AS (
  ...
)
SELECT * FROM backers_and_creators;
</code></pre>
<pre><code class="language-sql">/* Bad */
WITH backings_per_category AS (
  SELECT
    ...
), backers AS (
  SELECT
    backer_id,
    COUNT(backings_per_category.id) AS projects_backed_per_category
), backers_and_creators AS (
  ...
)
SELECT *
FROM backers_and_creators
INNER JOIN backers
ON backers_and_creators
ON backers.backer_id = backers_and_creators.backer_id
</code></pre>
</li>
</ul>
<h2 id="其他">其他</h2>
<ul>
<li>尽量使用 <code>!=</code> 而不是 <code>&lt;&gt;</code> 表示不等于。</li>
<li>尽量使用 <code>BETWEEN</code> 而不是多个 <code>AND</code> 语句。</li>
<li>尽量使用 <code>IN()</code> 而不是多个 <code>OR</code> 语句。</li>
<li>尽量避免使用 <code>SELECT *</code>。</li>
<li>尽量避免使用无意义的别名，例如：<code>a, b, c</code>。</li>
</ul>

        ]]></description></item><item><title>进程，线程和协程 (Process, Thread and Coroutine)</title><link>https://zeqiang.fun/cn/2021/04/process-thread-and-coroutine-python-implementation/</link><pubDate>Sat, 03 Apr 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/04/process-thread-and-coroutine-python-implementation/</guid><description><![CDATA[
        <blockquote>
<p>理论篇请参见：<a href="/cn/2021/04/process-thread-and-coroutine-theory">进程，线程和协程 (Process, Thread and Coroutine) - 理论篇</a></p>
</blockquote>
<p>本文将介绍进程，线程和协程在 Python 中的实现，代码详见<a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2021-04-03-process-thread-and-coroutine-python-implementation">这里</a>，部分参考自「Python 并发编程」 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>:。</p>
<h2 id="进程和线程">进程和线程</h2>
<p>在 Python 中可以使用 <code>multiprocessing.Process</code> 和 <code>threading.Thread</code> 来实现进程和线程。我们采用<strong>CPU 密集型</strong>、<strong>磁盘 IO 密集型</strong>、<strong>网络 IO 密集型</strong>和<strong>模拟 IO 密集型</strong>任务类型来测试单线程，多线程和多进程之间的性能差异。</p>
<pre><code class="language-python">import requests

# CPU 密集型
def cpu_bound_task(x=1, y=1):
    c = 0

    while c &lt; 1500000:
        c += 1
        x += x
        y += y

# 磁盘 IO 密集型
def disk_io_bound_task():
    with open('tmp.log', 'w') as f:
        for idx in range(5000000):
            f.write('{}\n'.format(idx))

# 网络 IO 密集型
def web_io_bound_task():
    try:
        requests.get('https://www.baidu.com')
    except Exception as e:
        pass

# 模拟 IO 密集型
def simulation_io_bound_task():
    time.sleep(2)
</code></pre>
<p>为了方便统计运行时间，定义如下一个运行时间装饰器：</p>
<pre><code class="language-python">import time

def timer(task_mode):
    def wrapper(func):
        def decorator(*args, **kwargs):
            task_type = kwargs.setdefault('task_type', None)
            start_time = time.time()
            func(*args, **kwargs)
            end_time = time.time()
            print('耗时（{} - {}）: {}'.format(
                task_mode, task_type, end_time - start_time))
        return decorator
    return wrapper
</code></pre>
<p>单线程，多线程和多进程的测试代码如下：</p>
<pre><code class="language-python">from threading import Thread
from multiprocessing import Process

@timer('单线程')
def single_thread(func, task_type='', n=10):
    for idx in range(n):
        func()


@timer('多线程')
def multi_threads(func, task_type='', n=10):
    threads = {}

    for idx in range(n):
        t = Thread(target=func)
        threads[idx] = t
        t.start()

    for thread in threads.values():
        thread.join()


@timer('多进程')
def multi_processes(func, task_type='', n=10):
    processes = {}

    for idx in range(n):
        p = Process(target=func)
        processes[idx] = p
        p.start()

    for process in processes.values():
        process.join()
</code></pre>
<p>运行测试</p>
<pre><code class="language-python"># 单线程
single_thread(cpu_bound_task, task_type='CPU 密集型任务')
single_thread(disk_io_bound_task, task_type='磁盘 IO 密集型任务')
single_thread(web_io_bound_task, task_type='网络 IO 密集型任务')
single_thread(simulation_io_bound_task, task_type='模拟 IO 密集型任务')

# 多线程
multi_threads(cpu_bound_task, task_type='CPU 密集型任务')
multi_threads(disk_io_bound_task, task_type='磁盘 IO 密集型任务')
multi_threads(web_io_bound_task, task_type='网络 IO 密集型任务')
multi_threads(simulation_io_bound_task, task_type='模拟 IO 密集型任务'

# 多进程
multi_processes(cpu_bound_task, task_type='CPU 密集型任务')
multi_processes(disk_io_bound_task, task_type='磁盘 IO 密集型任务')
multi_processes(web_io_bound_task, task_type='网络 IO 密集型任务')
multi_processes(simulation_io_bound_task, task_type='模拟 IO 密集型任务')
</code></pre>
<p>可以得到类似如下的结果：</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>单线程</th>
          <th>多线程</th>
          <th>多进程</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CPU 密集型</td>
          <td>83.42</td>
          <td>93.82</td>
          <td>9.08</td>
      </tr>
      <tr>
          <td>磁盘 IO 密集型</td>
          <td>15.64</td>
          <td>13.27</td>
          <td>1.28</td>
      </tr>
      <tr>
          <td>网络 IO 密集型</td>
          <td>1.13</td>
          <td>0.18</td>
          <td>0.13</td>
      </tr>
      <tr>
          <td>模拟 IO 密集型</td>
          <td>20.02</td>
          <td>2.02</td>
          <td>2.01</td>
      </tr>
  </tbody>
</table>
<p>从测试结果来看，不难得出如下结论：</p>
<ol>
<li>多线程和多进程相比单线程速度整体上有很大提升。</li>
<li>对于 CPU 密集型任务，由于 GIL 加锁和释放问题，多线程相比单线程更慢。</li>
<li>多线程更适合在 IO 密集场景下使用，例如：爬虫等。</li>
<li>多进程更适合在 CPU 密集场景下使用，例如：大数据处理，机器学习等。</li>
</ol>
<p>创建线程有两种方式：</p>
<h3 id="利用函数创建线程">利用函数创建线程</h3>
<p>Python 中的 <code>threading.Thread()</code> 接受两个参数：<strong>线程函数</strong>，用于指定线程执行的函数；<strong>线程函数参数</strong>，以元组的形式传入执行函数所需的参数。</p>
<pre><code class="language-python">import time
from threading import Thread

# 自定义函数
def func(name='Python'):
    for idx in range(2):
        print('Hello, {}'.format(name))
        time.sleep(1)

# 创建线程
thread_1 = Thread(target=func)
thread_2 = Thread(target=func, args=('Leo', ))

# 启动线程
thread_1.start()
thread_2.start()
</code></pre>
<p>可以得到如下输出：</p>
<pre><code>Hello, Python
Hello, Leo
Hello, Python
Hello, Leo
</code></pre>
<h3 id="利用类创建线程">利用类创建线程</h3>
<p>利用类创建线程需要自定义一个类并继承 <code>threading.Thread</code> 这个父类，同时重写 <code>run</code> 方法。最后通过实例化该类，并运行 <code>start()</code> 方法执行该线程。</p>
<pre><code class="language-python"># 自定义类
class MyThread(Thread):
    def __init__(self, name='Python'):
        super(MyThread, self).__init__()
        self.name = name

    def run(self):
        for idx in range(2):
            print('Hello, {}'.format(self.name))
            time.sleep(1)

# 创建线程
thread_1 = MyThread()
thread_2 = MyThread('Leo')

# 启动线程
thread_1.start()
thread_2.start()
</code></pre>
<p>可以得到同上面一样的输出：</p>
<pre><code>Hello, Python
Hello, Leo
Hello, Python
Hello, Leo
</code></pre>
<p>线程的一些常用方法和属性如下所示：</p>
<pre><code class="language-python"># 创建线程
t = Thread(target=func)

# 启动线程
t.start()

# 阻塞线程
t.join()

# 判断线程是否处于执行状态
# True: 执行中，False: 其他
t.is_alive()

# 这是线程是否随主线程退出而退出
# 默认为 False
t.daemon = True

# 设置线程名称
t.name = 'My Thread'
</code></pre>
<h2 id="锁">锁</h2>
<p>在一段代码中加锁表示同一时间有且仅有一个线程可以执行这段代码。在 Python 中锁分为两种：<strong>互斥锁</strong>和<strong>可重入锁</strong>。利用 <code>threading.Lock()</code> 可以获取全局唯一的锁对象，使用 <code>acquire()</code> 和 <code>release()</code> 方法可以获取和释放锁，注意两个需成对出现，否则可能造成死锁。</p>
<h3 id="互斥锁">互斥锁</h3>
<p>例如定义两个函数，并在两个线程中执行，这两个函数共用一个变量 <code>C</code>：</p>
<pre><code class="language-python">import time
import random

from threading import Thread

# 共用变量
C = 0

def job1(n=10):
    global C

    for idx in range(n):
        C += 1
        print('Job1: {}'.format(C))

def job2(n=10):
    global C

    for idx in range(n):
        C += 10
        print('Job2: {}'.format(C))

t1 = Thread(target=job1)
t2 = Thread(target=job2)

t1.start()
t2.start()
</code></pre>
<p>运行结果如下：</p>
<pre><code>Job1: 1
Job2: 11
Job2: 21
Job1: 22
Job1: 23
Job2: 33
Job2: 43
Job1: 44
Job2: 54
Job1: 55
Job2: 65
Job1: 66
Job2: 76
Job2: 86
Job1: 87
Job1: 88
Job2: 98
Job1: 99
Job2: 109
Job1: 110
</code></pre>
<p>两个线程共用一个全局变量，两个线程根据自己执行的快慢对变量 <code>C</code> 进行修改。在增加锁后：</p>
<pre><code class="language-python">import time
import random

from threading import Lock

# 全局唯一锁
LOCK = Lock()

# 共用变量
C = 0

def job1_with_lock(n=10):
    global C, LOCK

    LOCK.acquire()

    for idx in range(n):
        C += 1
        print('Job1: {}'.format(C))
        time.sleep(random.random())

    LOCK.release()


def job2_with_lock(n=10):
    global C, LOCK

    LOCK.acquire()

    for idx in range(n):
        C += 10
        print('Job2: {}'.format(C))
        time.sleep(random.random())

    LOCK.release()

t1 = Thread(target=job1_with_lock)
t2 = Thread(target=job2_with_lock)

t1.start()
t2.start()
</code></pre>
<p>运行结果如下：</p>
<pre><code>Job1: 1
Job1: 2
Job1: 3
Job1: 4
Job1: 5
Job1: 6
Job1: 7
Job1: 8
Job1: 9
Job1: 10
Job2: 20
Job2: 30
Job2: 40
Job2: 50
Job2: 60
Job2: 70
Job2: 80
Job2: 90
Job2: 100
Job2: 110
</code></pre>
<p>此时，由于 <code>job1_with_lock</code> 先拿到了锁，所以当执行时 <code>job2_with_lock</code> 无法获取到锁，就无法对 <code>C</code> 进行修改。只有当 <code>job1_with_lock</code> 执行完毕释放锁后，<code>job2_with_lock</code> 才能执行对 <code>C</code> 的修改操作。为了避免忘记释放锁，可以使用 <code>with</code> 上下文管理器来加锁。</p>
<h3 id="可重入锁">可重入锁</h3>
<p>在同一个线程中，我们可能会多次请求同一个资源，这称为<strong>嵌套锁</strong>。如果使用常规的方式：</p>
<pre><code class="language-python">from threading import Lock

def lock_with_lock(n=10):
    c = 0
    lock = Lock()

    with lock:
        for idx in range(n):
            c += 1
            with lock:
                print(c)

t = Thread(target=lock_with_lock)
t.start()
</code></pre>
<p>则无法正常运行，因为第二次获取锁时，锁已经被同一线程获取，从而无法运行后续代码。由于后续代码无法运行则无法释放锁，从而上述的嵌套锁会造成<strong>死锁</strong>。</p>
<p>为了解决这个问题，<code>threading</code> 模块提供了<strong>可重入锁</strong> <code>RLock</code>：</p>
<pre><code class="language-python">from threading import RLock

def rlock_with_lock(n=10):
    c = 0
    lock = RLock()

    with lock:
        for idx in range(n):
            c += 1
            with lock:
                print(c)
  
t = Thread(target=rlock_with_lock)
t.start()
</code></pre>
<p>运行结果如下：</p>
<pre><code>1
2
3
4
5
6
7
8
9
10
</code></pre>
<h3 id="全局解释器锁">全局解释器锁</h3>
<p>全局解释器锁（Global Interpreter Lock，GIL），是计算机程序设计语言解释器用于同步线程的一种机制，它使得任何时刻仅有一个线程在执行。</p>
<blockquote>
<p>任何 Python 线程执行前，必须先获得 GIL 锁，然后，每执行 100 条字节码，解释器就自动释放 GIL 锁，让别的线程有机会执行。这个 GIL 全局锁实际上把所有线程的执行代码都给上了锁，所以，多线程在 Python 中只能交替执行，即使 100 个线程跑在 100 核 CPU 上，也只能用到 1 个核。</p>
</blockquote>
<h2 id="通信">通信</h2>
<p>Python 中实现线程中通信有如下 3 中方法：</p>
<h3 id="event-事件">Event 事件</h3>
<p><code>threading.Event</code> 可以创建一个事件变量，多个线程等待这个事件的发生，在事件发生后，所有线程继续运行。<code>threading.Event</code> 包含如下三个函数：</p>
<pre><code class="language-python">event = threading.Event()

# 重置 event，使得所有该 event 事件都处于待命状态
event.clear()

# 等待接收 event 的指令，决定是否阻塞程序执行
event.wait()

# 发送 event 指令，使所有设置该 event 事件的线程执行
event.set()
</code></pre>
<p>例如：</p>
<pre><code class="language-python">import time

from threading import Thread, Event

class EventThread(Thread):
    def __init__(self, name, event):
        super(EventThread, self).__init__()

        self.name = name
        self.event = event

    def run(self):
        print('线程 {} 启动于 {}'.format(self.name, time.ctime(time.time())))
        self.event.wait()
        print('线程 {} 结束于 {}'.format(self.name, time.ctime(time.time())))

threads = {}
event = Event()

for tid in range(3):
    threads[tid] = EventThread(str(tid), event)

event.clear()

for thread in threads.values():
    thread.start()

print('等待 3 秒钟 ...')
time.sleep(3)

print('唤醒所有线程 ...')
event.set() 
</code></pre>
<p>运行结果如下：</p>
<pre><code>线程 0 启动于 Thu Apr  1 23:12:32 2021
线程 1 启动于 Thu Apr  1 23:12:32 2021
线程 2 启动于 Thu Apr  1 23:12:32 2021
等待 3 秒钟 ...
唤醒所有线程 ...
线程 0 结束于 Thu Apr  1 23:12:35 2021
线程 1 结束于 Thu Apr  1 23:12:35 2021
线程 2 结束于 Thu Apr  1 23:12:35 2021
</code></pre>
<p>可见线程启动后并未执行完成，而是卡在了 <code>event.wait()</code> 处，直到通过 <code>event.set()</code> 发送指令后，所有线程才继续向下执行。</p>
<h3 id="condition">Condition</h3>
<p><code>threading.Condition</code> 与 <code>threading.Event</code> 类似，包含如下 4 个函数：</p>
<pre><code class="language-python">cond = threading.Condition()

# 类似 lock.acquire()
cond.acquire()

# 类似 lock.release()
cond.release()

# 等待指定触发，同时会释放对锁的获取,直到被 notify 才重新占有琐。
cond.wait()

# 发送指定，触发执行
cond.notify()
</code></pre>
<p>以一个捉迷藏的游戏为例：</p>
<pre><code class="language-python">import time

from threading import Thread, Condition

class Seeker(Thread):
    def __init__(self, condition, name):
        super(Seeker, self).__init__()

        self.condition = condition
        self.name = name

    def run(self):
        time.sleep(1) # 确保先运行 Hider 中的方法

        self.condition.acquire()

        print('{}: 我把眼睛蒙好了'.format(self.name))
        self.condition.notify()
        self.condition.wait()
        print('{}: 我找到你了'.format(self.name))
        self.condition.notify()

        self.condition.release()
        print('{}: 我赢了'.format(self.name))


class Hider(Thread):
    def __init__(self, condition, name):
        super(Hider, self).__init__()

        self.condition = condition
        self.name = name

    def run(self):
        self.condition.acquire()

        self.condition.wait()
        print('{}: 我藏好了'.format(self.name))
        self.condition.notify()
        self.condition.wait()
        self.condition.release()
        print('{}: 被你找到了'.format(self.name))

condition = Condition()

seeker = Seeker(condition, 'Seeker')
hider = Hider(condition, 'Hider')

seeker.start()
hider.start()
</code></pre>
<p>运行结果如下：</p>
<pre><code>Seeker: 我把眼睛蒙好了
Hider: 我藏好了
Seeker: 我找到你了
Seeker: 我赢了
Hider: 被你找到了
</code></pre>
<p>可见通过 <code>cond.wait()</code> 和 <code>cond.notify()</code> 进行阻塞和通知可以实现双方动作交替进行。</p>
<h3 id="queue-队列">Queue 队列</h3>
<p>从一个线程向另一个线程发送数据最安全的方式是使用 <code>queue</code> 库中的队列。创建一个被多个线程共享的队列对象，通过 <code>put()</code> 和 <code>get()</code> 方法向队列发送和获取元素。队列的常用方法如下：</p>
<pre><code class="language-python">from queue import Queue

# maxsize=0 表示不限大小
# maxsize&gt;0 且消息数达到限制时，put() 方法会阻塞
q = Queue(maxsize=0)

# 默认阻塞程序，等待队列消息，可设置超时时间
q.get(block=True, timeout=None)

# 发送消息，默认会阻塞程序至队列中有空闲位置放入数据
q.put(item, block=True, timeout=None)

# 等待所有的消息都被消费完
q.join()

# 通知队列任务处理已经完成，当所有任务都处理完成时，join() 阻塞将会解除
q.task_done()

# 查询当前队列的消息个数
q.qsize()

# 队列消息是否都被消费完，返回 True/False
q.empty()

# 检测队列里消息是否已满
q.full()
</code></pre>
<p>以老师点名为例：</p>
<pre><code class="language-python">import time

from queue import Queue
from threading import Thread

class Student(object):
    def __init__(self, name):
        super(Student, self).__init__()

        self.name = name

    def speak(self):
        print('{}: 到'.format(self.name))


class Teacher(object):
    def __init__(self, queue):
        super(Teacher, self).__init__()

        self.queue = queue

    def call(self, student_name):
        if student_name == 'exit':
            print('老师: 点名结束，开始上课')
        else:
            print('老师: {}'.format(student_name))

        self.queue.put(student_name)


class CallManager(Thread):
    def __init__(self, queue):
        super(CallManager, self).__init__()

        self.students = {}
        self.queue = queue

    def put(self, student):
        self.students.setdefault(student.name, student)

    def run(self):
        while True:
            student_name = self.queue.get()

            if student_name == 'exit':
                break
            elif student_name in self.students:
                self.students[student_name].speak()
            else:
                print('学生: 老师，没有 {} 这个人'.format(student_name))

queue = Queue()

teacher = Teacher(queue=queue)
s1 = Student(name='张三')
s2 = Student(name='李四')

cm = CallManager(queue)
cm.put(s1)
cm.put(s2)

cm.start()

print('开始点名')
teacher.call('张三')
time.sleep(1)
teacher.call('李四')
time.sleep(1)
teacher.call('王五')
time.sleep(1)
teacher.call('exit')
</code></pre>
<p>运行结果如下：</p>
<pre><code>开始点名
老师: 张三
张三: 到
老师: 李四
李四: 到
老师: 王五
学生: 老师，没有 王五 这个人
老师: 点名结束，开始上课
</code></pre>
<p>除了先进先出队列 <code>queue.Queue</code> 外，还有后进先出队列 <code>queue.LifoQueue</code> 和优先级队列 <code>queue.PriorityQueue</code>。</p>
<h2 id="进程池和线程池">进程池和线程池</h2>
<p><strong>池</strong>是一组资源的集合，这组资源在服务器启动之初就被完全创建好并初始化，这称为静态资源分配。当服务器进入正式运行阶段，即开始处理客户请求的时候，如果它需要相关的资源，就可以直接从池中获取，无需动态分配。很显然，直接从池中取得所需资源比动态分配资源的速度要快得多，因为分配系统资源的系统调用都是很耗时的。</p>
<p>池的概念主要目的是为了重用：让线程或进程在生命周期内可以多次使用。它减少了创建创建线程和进程的开销，以空间换时间来提高了程序性能。重用不是必须的规则，但它是程序员在应用中使用池的主要原因。</p>
<p>Python 中利用 <code>concurrent.futures</code> 库中的 <code>ThreadPoolExecutor</code> 和 <code>ProcessPoolExecutor</code> 创建<strong>线程池</strong>和<strong>进程池</strong>。示例如下：</p>
<pre><code class="language-python">import time
import threading

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed


def print_func(n=3):
    for idx in range(n):
        print('运行 {}-{}'.format(threading.get_ident(), idx))
        time.sleep(1)


def return_func(n=3):
    res = []

    for idx in range(n):
        res.append('{}-{}'.format(threading.get_ident(), idx))
        time.sleep(1)

    return res


def test_thread_pool_print(n=3, m=12):
    with ThreadPoolExecutor(max_workers=n) as executor:
        for _ in range(m):
            executor.submit(print_func)


def test_process_pool_print(n=3, m=12):
    with ProcessPoolExecutor(max_workers=n) as executor:
        for _ in range(m):
            executor.submit(print_func)


def test_thread_pool_return(n=3, m=12):
    with ThreadPoolExecutor(max_workers=n) as executor:
        futures = [executor.submit(return_func) for _ in range(m)]

        for future in as_completed(futures):
            print(future.result())


def test_process_pool_return(n=3, m=12):
    with ProcessPoolExecutor(max_workers=n) as executor:
        futures = [executor.submit(return_func) for _ in range(m)]

        for future in as_completed(futures):
            print(future.result())


line_sep = '-' * 60

print(line_sep)
print('测试线程池')
print(line_sep)
test_thread_pool_print()
print(line_sep)

print(line_sep)
print('测试进程池')
print(line_sep)
test_process_pool_print()
print(line_sep)

print(line_sep)
print('测试线程池')
print(line_sep)
test_thread_pool_return()
print(line_sep)

print(line_sep)
print('测试进程池')
print(line_sep)
test_process_pool_return()
print(line_sep)
</code></pre>
<p>运行结果如下：</p>
<pre><code>------------------------------------------------------------
测试线程池
------------------------------------------------------------
运行 123145462505472-0
运行 123145479294976-0
运行 123145496084480-0
运行 123145496084480-1
运行 123145462505472-1
运行 123145479294976-1
运行 123145496084480-2
运行 123145462505472-2
运行 123145479294976-2
运行 123145462505472-0
运行 123145479294976-0
运行 123145496084480-0
运行 123145479294976-1
运行 123145462505472-1
运行 123145496084480-1
运行 123145479294976-2
运行 123145462505472-2
运行 123145496084480-2
------------------------------------------------------------
------------------------------------------------------------
测试进程池
------------------------------------------------------------
运行 4545199616-0
运行 4545199616-1
运行 4545199616-2
运行 4545199616-0
运行 4545199616-1
运行 4545199616-2
运行 4663131648-0
运行 4663131648-1
运行 4663131648-2
运行 4663131648-0
运行 4663131648-1
运行 4663131648-2
运行 4633173504-0
运行 4633173504-1
运行 4633173504-2
运行 4633173504-0
运行 4633173504-1
运行 4633173504-2
------------------------------------------------------------
------------------------------------------------------------
测试线程池
------------------------------------------------------------
['123145496084480-0', '123145496084480-1', '123145496084480-2']
['123145479294976-0', '123145479294976-1', '123145479294976-2']
['123145462505472-0', '123145462505472-1', '123145462505472-2']
['123145479294976-0', '123145479294976-1', '123145479294976-2']
['123145496084480-0', '123145496084480-1', '123145496084480-2']
['123145462505472-0', '123145462505472-1', '123145462505472-2']
------------------------------------------------------------
------------------------------------------------------------
测试进程池
------------------------------------------------------------
['4791307776-0', '4791307776-1', '4791307776-2']
['4588228096-0', '4588228096-1', '4588228096-2']
['4654599680-0', '4654599680-1', '4654599680-2']
['4791307776-0', '4791307776-1', '4791307776-2']
['4588228096-0', '4588228096-1', '4588228096-2']
['4654599680-0', '4654599680-1', '4654599680-2']
------------------------------------------------------------
</code></pre>
<p>其中，<code>submit()</code> 方法用于提交要执行的任务到线程池或进程池中，并返回该任务的 <code>Future</code> 对象。<code>Future</code> 对象的 <code>done()</code> 方法用于判断任务是否执行完毕，通过 <code>result(timeout=None)</code> 方法获取返回结果。利用 <code>concurrent.futures.as_completed()</code> 方法可以返回一个包含指定 Future 实例的迭代器，这些实例在完成时生成 Future 对象。</p>
<h2 id="生成器和迭代器">生成器和迭代器</h2>
<figure>
  <img data-src="/images/cn/2021-04-03-process-thread-and-coroutine-python-implementation/iterators-and-generators.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：https://nvie.com/posts/iterators-vs-generators/</p></figcaption>
</figure>
<p><strong>容器</strong>是一种把多个元素组织在一起的数据结构，容器中的元素可以逐个迭代获取，可以用 <code>in</code> 或 <code>not in</code> 判断元素是否包含在容器中。常见的容器对象有：</p>
<pre><code>list, deque, ...
set, frozensets, ...
dict, defaultdict, OrderedDict, Counter, ...
tuple, namedtuple, ...
str
</code></pre>
<h3 id="可迭代对象">可迭代对象</h3>
<p>很多容器都是<strong>可迭代对象</strong>，此外还有更多的对象同样也可以是可迭代对象，比如处于打开状态的 <code>file</code> 和 <code>socket</code> 等。凡是可以返回一个迭代器的对象都可称之为可迭代对象，例如：</p>
<pre><code class="language-python">from collections import deque
from collections.abc import Iterable

print(isinstance('Leo Van', Iterable))
print(isinstance([1, 2, 3], Iterable))
print(isinstance({'k1': 'v1', 'k2': 'v2'}, Iterable))
print(isinstance(deque('abc'), Iterable))
</code></pre>
<p>运行结果如下：</p>
<pre><code>True
True
True
True
</code></pre>
<h3 id="迭代器">迭代器</h3>
<p><strong>迭代器</strong>是一个带有状态的对象，通过 <code>next()</code> 方法可以返回容器中的下一个值。任何实现 <code>__iter__()</code> 和 <code>__next__()</code> 方法的对象都是迭代器。其中，<code>__iter__()</code> 方法返回迭代器本身，<code>__next__()</code> 方法返回容器中的下一个值，如果容器中没有更多元素了，则抛出 <code>StopIteration</code> 异常。例如：</p>
<pre><code class="language-python">class MyList(object):
    def __init__(self, end):
        super(MyList, self).__init__()

        self.end = end

    def __iter__(self):
        return MyListIterator(self.end)

    def __repr__(self):
        return '[{}]'.format(', '.join([str(ele) for ele in self]))


class MyListIterator(object):
    def __init__(self, end):
        super(MyListIterator, self).__init__()

        self.data = end
        self.start = 0

    def __iter__(self):
        return self

    def __next__(self):
        while self.start &lt; self.data:
            self.start += 1
            return self.start - 1

        raise StopIteration

my_list = MyList(3)

print('MyList: {}'.format(my_list))
print(isinstance(my_list, Iterable))
print(isinstance(my_list, Iterator))

for ele in my_list:
    print(ele)

my_list_iterator = MyListIterator(3)

print('MyListIterator: {}'.format(my_list_iterator))
print(isinstance(my_list_iterator, Iterable))
print(isinstance(my_list_iterator, Iterator))

my_iterator = iter(my_list)

print('MyIterator: {}'.format(my_iterator))
print(isinstance(my_iterator, Iterable))
print(isinstance(my_iterator, Iterator))

while True:
    try:
        print(next(my_iterator))
    except StopIteration as e:
        return
</code></pre>
<p>运行结果如下：</p>
<pre><code>MyList: [0, 1, 2]
True
False
0
1
2
MyListIterator: &lt;__main__.MyListIterator object at 0x7fc9602b2100&gt;
True
True
0
1
2
MyIterator: &lt;__main__.MyListIterator object at 0x7fc9602b2fa0&gt;
True
True
0
1
2
Stop
</code></pre>
<h3 id="生成器">生成器</h3>
<p><strong>生成器</strong>非常类似于返回数组的函数，都是具有参数、可被调用、产生一系列的值。但是生成器并不是构造出数组包含所有的值并一次性返回，而是每次产生一个值，因此生成器看起来像函数，但行为像迭代器。</p>
<p>Python 中创建生成器有两种方法：使用类似列表方式或 <code>yield</code> 关键字：</p>
<pre><code class="language-python">from collections.abc import Generator
from inspect import getgeneratorstate

a_list = [x for x in range(10)]
print(a_list)
print(isinstance(a_list, Generator))

a_generator = (x for x in range(10))
print(a_generator)
print(isinstance(a_generator, Generator))

def my_yield(n):
    now = 0

    while now &lt; n:
        yield now
        now += 1

    raise StopIteration

gen = my_yield(4)
print(gen)
print(isinstance(gen, Generator))
</code></pre>
<p>运行结果如下：</p>
<pre><code>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
False
&lt;generator object &lt;genexpr&gt; at 0x7fdf84a8a430&gt;
True
&lt;generator object my_yield at 0x7fdf86a03f20&gt;
True
</code></pre>
<p>由于生成器并不是一次生成所有元素，而是每次执行后返回一个值，通过 <code>next()</code> 和 <code>generator.send(None)</code> 两个方法可以激活生成器，例如：</p>
<pre><code class="language-python">def my_yield(n):
    now = 0

    while now &lt; n:
        yield now
        now += 1

    raise StopIteration

gen = my_yield(4)
print(gen.send(None))
print(next(gen))
print(gen.send(None))
print(next(gen))
</code></pre>
<p>运行结果如下：</p>
<pre><code>0
1
2
3
</code></pre>
<p>生成器在其生命周期中共有 4 种状态：</p>
<ul>
<li><code>GEN_CREATED</code>：已创建</li>
<li><code>GEN_RUNNING</code>：正在执行（只在多线程应用中能看到该状态）</li>
<li><code>GEN_SUSPENDED</code>：暂停中</li>
<li><code>GEN_CLOSED</code>：已关闭</li>
</ul>
<p>例如：</p>
<pre><code class="language-python">from collections.abc import Generator
from inspect import getgeneratorstate

def my_yield(n):
    now = 0

    while now &lt; n:
        yield now
        now += 1

    raise StopIteration

gen = my_yield(4)
print(gen)
print(getgeneratorstate(gen))

print(gen.send(None))
print(next(gen))
print(getgeneratorstate(gen))

print(gen.send(None))
print(next(gen))
print(getgeneratorstate(gen))

gen.close()
print(getgeneratorstate(gen))
</code></pre>
<p>运行结果如下：</p>
<pre><code>GEN_CREATED
0
1
GEN_SUSPENDED
2
3
GEN_SUSPENDED
GEN_CLOSED
</code></pre>
<p>生成器在不满足生成元素的条件时，会抛出 <code>StopIteration</code> 异常，通过类似列表形式构建的生成器会自动实现该异常，自定的生成器则需要手动实现该异常。</p>
<h2 id="协程">协程</h2>
<h3 id="yield">yield</h3>
<p>协程通过 <code>yield</code> 暂停生成器，可以将程序的执行流程交给其他子程序，从而实现不同子程序之间的交替执行。例如：</p>
<pre><code class="language-python">def jump_range(n):
    idx = 0

    while idx &lt; n:
        jump = yield idx
        print('[idx: {}, jump: {}]'.format(idx, jump))

        if jump is None:
            jump = 1

        idx += jump

itr = jump_range(6)
print(next(itr))
print(itr.send(2))
print(next(itr))
print(itr.send(-1))
print(next(itr))
print(next(itr))
</code></pre>
<p>运行结果如下：</p>
<pre><code>0
[idx: 0, jump: 2]
2
[idx: 2, jump: None]
3
[idx: 3, jump: -1]
2
[idx: 2, jump: None]
3
[idx: 3, jump: None]
4
</code></pre>
<p><code>yield idx</code> 将 <code>idx</code> 返回给外部调用程序，<code>jump = yield</code> 可以接受外部程序通过 <code>send()</code> 发送的信息，并将其赋值给 <code>jump</code>。</p>
<p><code>yield from</code> 是 Python 3.3 之后出现的新语法，后面是可迭代对象，可以是普通的可迭代对象，也可以是迭代器，甚至是生成器。<code>yield</code> 和 <code>yield from</code> 的对比如下：</p>
<pre><code class="language-python">a_str = 'Leo'
a_list = [1, 2, 3]
a_dict = {'name': 'Leo', 'gender': 'Male'}
a_gen = (idx for idx in range(4, 8))

def gen(*args, **kwargs):
    for item in args:
        for ele in item:
            yield ele

new_gen = gen(a_str, a_list, a_dict, a_gen)
print(list(new_gen))

a_gen = (idx for idx in range(4, 8))

def gen_from(*args, **kwargs):
    for item in args:
        yield from item

new_gen = gen_from(a_str, a_list, a_dict, a_gen)
print(list(new_gen))
</code></pre>
<p>运行结果如下：</p>
<pre><code>['L', 'e', 'o', 1, 2, 3, 'name', 'gender', 4, 5, 6, 7]
['L', 'e', 'o', 1, 2, 3, 'name', 'gender', 4, 5, 6, 7]
</code></pre>
<p>在实现生成器的嵌套时，使用 <code>yield from</code> 可以比使用 <code>yield</code> 避免各种意想不到的异常。使用 <code>yield from</code> 时，需要关注如下几个概念：</p>
<ul>
<li>调用方：调用委托生成器的代码</li>
<li>委托生成器：包含 <code>yield from</code> 表达式的生成器函数</li>
<li>子生成器：<code>yield from</code> 后面的生成器函数</li>
</ul>
<p>如下是一个计算平均数的例子：</p>
<pre><code class="language-python"># 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0

    while True:
        num = yield average

        if num is None:
            break

        count += 1
        total += num
        average = total / count

    return total, count, average

# 委托生成器
def proxy_gen():
    while True:
        total, count, average = yield from average_gen()
        print('计算完毕，共输入 {} 个数值，总和 {}，平均值 {}'.format(
            count, total, average))

# 调用方
calc_average = proxy_gen()
next(calc_average)
print(calc_average.send(10))
print(calc_average.send(20))
print(calc_average.send(30))
calc_average.send(None)
</code></pre>
<p>运行结果如下：</p>
<pre><code>10.0
15.0
20.0
计算完毕，共输入 3 个数值，总和 60，平均值 20.0
</code></pre>
<p>委托生成器的作用是在调用方和子生成器之间建立一个<strong>双向通道</strong>，调用方通过 <code>send()</code> 将消息发送给子生成器，子生成器 <code>yield</code> 的值则返回给调用方。<code>yield from</code> 背后为整个过程做了很多操作，例如：捕获 <code>StopIteration</code> 异常等。</p>
<h3 id="asyncio">asyncio</h3>
<p><code>asyncio</code> 是 Python 3.4 引入的标准库，直接内置了对<strong>异步 IO</strong> 的支持。只要在一个函数前面加上 <code>async</code> 关键字就可以将一个函数变为一个协程。例如：</p>
<pre><code class="language-python">from collections.abc import Coroutine

async def async_func(name):
    print('Hello, ', name)

coroutine = async_func('World')
print(isinstance(coroutine, Coroutine))
</code></pre>
<p>运行结果如下：</p>
<pre><code>True
</code></pre>
<p>利用 <code>asyncio.coroutine</code> 装饰器可以将一个生成器当作协程使用，但其本质仍旧是一个生成器。例如：</p>
<pre><code class="language-python">import asyncio

from collections.abc import Generator, Coroutine

@asyncio.coroutine
def coroutine_func(name):
    print('Hello,', name)
    yield from asyncio.sleep(1)
    print('Bye,', name)


coroutine = coroutine_func('World')
print(isinstance(coroutine, Generator))
print(isinstance(coroutine, Coroutine))
</code></pre>
<p>运行结果如下：</p>
<pre><code>True
False
</code></pre>
<p><code>asyncio</code> 中包含如下几个重要概念：</p>
<ul>
<li><strong><code>event_loop</code> 事件循环</strong>：程序开启一个无限的循环，协程将注册到事件循环上，当满足事件发生时，调用相应的协程函数。</li>
<li><strong><code>coroutine</code> 协程</strong>：一个使用 <code>async</code> 定义的协程函数，它的调用不会立即执行，而是会返回一个协程对象。协程对象需要注册到事件循环上，由事件循环控制调用。</li>
<li><strong><code>future</code> 对象</strong>：代表将来执行或没有执行的对象。它和 <code>task</code> 对象没有本质上的区别。</li>
<li><strong><code>task</code> 对象</strong>：一个协程对象是一个原生可以挂起的函数，任务是对协程的进一步封装，其中包含任务的各种状态。<code>Task</code> 对象是 <code>Future</code> 的子类，它将 <code>coroutine</code> 和 <code>Future</code> 联系在一起，将 <code>coroutine</code> 封装成为一个 <code>Future</code> 对象。</li>
<li><strong><code>async / await</code> 关键字</strong>：<code>async</code> 定义一个协程，<code>await</code> 用于挂起阻塞的异步调用接口。</li>
</ul>
<p>协程完整的工作流程如下：</p>
<pre><code class="language-python">import asyncio

async def hello(name):
    print('Hello,', name)
    
# 定义协程
coroutine = hello('World')

# 定义事件循环
loop = asyncio.get_event_loop()

# 创建任务
task = loop.create_task(coroutine)

# 将任务交由时间循环并执行
loop.run_until_complete(task)
</code></pre>
<p>运行结果如下：</p>
<pre><code>Hello, World
</code></pre>
<p><code>await</code> 用于挂起阻塞的异步调用接口，其作用在一定程度上类似于 <code>yield</code>。<code>yield from</code> 后面可接可迭代对象，也可接 future 对象或协程对象；<code>await</code> 后面必须接 future 对象或协程对象。</p>
<pre><code class="language-python">import asyncio
from asyncio.futures import Future

async def hello(name):
    await asyncio.sleep(2)
    print('Hello, ', name)

coroutine = hello(&quot;World&quot;)

# 将协程转为 task 对象
task = asyncio.ensure_future(coroutine)

print(isinstance(task, Future))
</code></pre>
<p>运行结果如下：</p>
<pre><code>True
</code></pre>
<p>异步 IO 的实现原理就是在 IO 高的地方挂起，等 IO 结束后再继续执行。绝大部分情况下，后续代码的执行是需要依赖 IO 的返回值的，这就需要使用<strong>回调</strong>。</p>
<p>回调的实现有两种，一种是在同步编程中直接获取返回结果：</p>
<pre><code class="language-python">import asyncio
import time

async def _sleep(x):
    time.sleep(x)
    return '暂停了 {} 秒'.format(x)

coroutine = _sleep(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)

loop.run_until_complete(task)
print('返回结果：{}'.format(task.result()))
</code></pre>
<p>运行结果如下：</p>
<pre><code>返回结果：暂停了 2 秒
</code></pre>
<p>另一种是通过添加回调函数来实现：</p>
<pre><code class="language-python">import asyncio
import time

async def _sleep(x):
    time.sleep(x)
    return '暂停了 {} 秒'.format(x)

def callback(future):
    print('回调返回结果：{}'.format(future.result()))

coroutine = _sleep(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)

task.add_done_callback(callback)
loop.run_until_complete(task)
</code></pre>
<p>运行结果如下：</p>
<pre><code>回调返回结果：暂停了 2 秒
</code></pre>
<p><code>asyncio</code> 实现并发需要多个协程来完成，每当有任务阻塞时需要 <code>await</code>，然后让其他协程继续工作。</p>
<pre><code class="language-python">import asyncio

async def do_some_work(x):
    print('等待中 ...')
    await asyncio.sleep(x)
    print('{} 秒后结束'.format(x))
    return x

# 协程对象
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

# 任务列表
tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

for task in tasks:
    print('任务结果：{}'.format(task.result()))
</code></pre>
<p>运行结果如下：</p>
<pre><code>等待中 ...
等待中 ...
等待中 ...
1 秒后结束
2 秒后结束
4 秒后结束
任务结果：1
任务结果：2
任务结果：4
</code></pre>
<p>协程之间可以进行嵌套，即在一个协程中 <code>await</code> 另一个协程：</p>
<pre><code class="language-python">import asyncio

async def do_some_work(x):
    print('等待中 ...')
    await asyncio.sleep(x)
    print('{} 秒后结束'.format(x))
    return x

async def out_do_some_work():
    coroutine1 = do_some_work(1)
    coroutine2 = do_some_work(2)
    coroutine3 = do_some_work(4)

    tasks = [
        asyncio.ensure_future(coroutine1),
        asyncio.ensure_future(coroutine2),
        asyncio.ensure_future(coroutine3)
    ]

    dones, pendings = await asyncio.wait(tasks)

    for task in dones:
        print('任务结果：{}'.format(task.result()))

loop = asyncio.get_event_loop()
loop.run_until_complete(out_do_some_work())
</code></pre>
<p>如果使用 <code>asyncio.gather()</code> 来获取结果，则需要对获取结果部分做如下修改：</p>
<pre><code class="language-python">results = await asyncio.gather(*tasks)
for result in results:
    print('任务结果：{}'.format(result))
</code></pre>
<p><code>asyncio.wait()</code> 返回 <code>dones</code> 和 <code>pendings</code>，分别表示已完成和未完成的任务；<code>asyncio.gather()</code> 则会把结果直接返回。</p>
<p>运行结果如下：</p>
<pre><code>等待中 ...
等待中 ...
等待中 ...
1 秒后结束
2 秒后结束
4 秒后结束
任务结果：1
任务结果：4
任务结果：2
</code></pre>
<p>协程（准确的说是 <code>Future</code> 或 <code>Task</code> 对象）包含如下状态：</p>
<ul>
<li><code>Pending</code>：已创建，未执行</li>
<li><code>Running</code>：执行中</li>
<li><code>Done</code>：执行完毕</li>
<li><code>Cancelled</code>：被取消</li>
</ul>
<p>测试代码如下：</p>
<pre><code class="language-python">coroutine = _sleep(10)
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)

print('Pending')

try:
    t = Thread(target=loop.run_until_complete, args=(task, ))
    t.start()
    print('Running')
    t.join()
except KeyboardInterrupt as e:
    task.cancel()
    print('Cancel')
finally:
    print('Done')
</code></pre>
<p>执行顺利的话，运行结果如下：</p>
<pre><code>Pending
Running
Done
</code></pre>
<p>如果在启动后按下 <kbd>Ctrl</kbd> + <kbd>C</kbd> 则会触发 <code>task.cancel()</code>，运行结果如下：</p>
<pre><code>Pending
Running
Cancelled
Done
</code></pre>
<p><code>asyncio.wait()</code> 可以通过参数控制何时返回：</p>
<pre><code class="language-python">import random
import asyncio

async def random_sleep():
    await asyncio.sleep(random.uniform(0.5, 6))

loop = asyncio.get_event_loop()
tasks = [random_sleep() for _ in range(1, 10)]

dones, pendings = loop.run_until_complete(asyncio.wait(
    tasks, return_when=asyncio.FIRST_COMPLETED))
print('第一次完成的任务数：{}'.format(len(dones)))

dones, pendings = loop.run_until_complete(asyncio.wait(
    pendings, timeout=2))
print('第二次完成的任务数: {}'.format(len(dones)))

dones, pendings = loop.run_until_complete(asyncio.wait(pendings))
print('第三次完成的任务数：{}'.format(len(dones)))
</code></pre>
<p>运行结果如下：</p>
<pre><code>第一次完成的任务数：1
第二次完成的任务数: 4
第三次完成的任务数：4
</code></pre>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://iswbm.com/108.html">https://iswbm.com/108.html</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>进程，线程和协程 (Process, Thread and Coroutine)</title><link>https://zeqiang.fun/cn/2021/04/process-thread-and-coroutine-theory/</link><pubDate>Thu, 01 Apr 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/04/process-thread-and-coroutine-theory/</guid><description><![CDATA[
        <blockquote>
<p>Python 实现篇请参见：<a href="/cn/2021/04/process-thread-and-coroutine-python-implementation">进程，线程和协程 (Process, Thread and Coroutine) - 实现篇</a></p>
</blockquote>
<h2 id="进程-线程和协程">进程，线程和协程</h2>
<p>**进程（Process）**是计算机中已运行的程序 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。**线程（Thread）**是操作系统能够进行运算调度的最小单位。大部分情况下，线程被包含在进程之中，是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流，一个进程中可以并发多个线程，每条线程并行执行不同的任务 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>。</p>
<p>进程和线程之间的主要区别在于：</p>
<ol>
<li>线程共享创建其进程的地址空间，进程使用自己的地址。</li>
<li>线程可以直接访问进程的数据，进程使用其父进程数据的副本。</li>
<li>线程可以同其进程中其他线程直接通信，进程必须使用**进程间通讯（inter-process communicate, IPC）**与同级进程通信。</li>
<li>线程开销较小，进程开销较大。</li>
<li>线程的创建较为容易，进程需要复制其父进程。</li>
<li>线程可以控制相同进程的其他线程，进程只能控制其子进程。</li>
<li>对于主线程的修改（例如：取消、优先级修改等）可能会影响进程中的其他线程，对于父进程的修改不会影响其子进程。</li>
</ol>
<p>单线程进程和多线程进程之间的对比如下图所示：</p>
<p><img src="/images/cn/2021-04-01-process-thread-and-coroutine-theory/single-thread-process-vs-multiple-threads-process.png" alt=""></p>
<p>一个关于进程和线程的形象类比如下 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>：</p>
<ol>
<li>计算机的核心是 CPU，它承担了所有的计算任务。它就像一座工厂，时刻在运行。</li>
<li>假定工厂的电力有限，一次只能供给一个车间使用。也就是说，一个车间开工的时候，其他车间都必须停工。背后的含义就是，单个 CPU 一次只能运行一个任务。</li>
<li><strong>进程</strong>就好比工厂的车间，它代表 CPU 所能处理的单个任务。任一时刻，CPU 总是运行一个进程，其他进程处于非运行状态。</li>
<li>一个车间里，可以有很多工人。他们协同完成一个任务。</li>
<li><strong>线程</strong>就好比车间里的工人。一个进程可以包括多个线程。</li>
<li>车间的空间是工人们共享的，比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的，每个线程都可以使用这些共享内存。</li>
<li>可是，每间房间的大小不同，有些房间最多只能容纳一个人，比如厕所。里面有人的时候，其他人就不能进去了。这代表一个线程使用某些共享内存时，其他线程必须等它结束，才能使用这一块内存。</li>
<li>一个防止他人进入的简单方法，就是门口加一把锁。先到的人锁上门，后到的人看到上锁，就在门口排队，等锁打开再进去。这就叫“互斥锁”（Mutual Exclusion，Mutex），防止多个线程同时读写某一块内存区域。</li>
<li>还有些房间，可以同时容纳 <code>$n$</code> 个人，比如厨房。也就是说，如果人数大于 <code>$n$</code>，多出来的人只能在外面等着。这好比某些内存区域，只能供给固定数目的线程使用。</li>
<li>这时的解决方法，就是在门口挂 <code>$n$</code> 把钥匙。进去的人就取一把钥匙，出来时再把钥匙挂回原处。后到的人发现钥匙架空了，就知道必须在门口排队等着了。这种做法叫做“信号量”（Semaphore），用来保证多个线程不会互相冲突。不难看出，Mutex 是 Semaphore 的一种特殊情况（<code>$n = 1$</code> 时）。也就是说，完全可以用后者替代前者。但是，因为 Mutex 较为简单，且效率高，所以在必须保证资源独占的情况下，还是采用这种设计。</li>
<li>操作系统的设计，因此可以归结为三点：(1). 以多进程形式，允许多个任务同时运行；(2). 以多线程形式，允许单个任务分成不同的部分运行；(3). 提供协调机制，一方面防止进程之间和线程之间产生冲突，另一方面允许进程之间和线程之间共享资源。</li>
</ol>
<p><strong>协程</strong>（Coroutine）是计算机程序的一类组件，推广了协作式多任务的子例程，允许执行被挂起与被恢复。相对子例程而言，协程更为一般和灵活，但在实践中使用没有子例程那样广泛。协程更适合于用来实现彼此熟悉的程序组件，如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道。</p>
<p><strong>子例程</strong>（Subroutine），是一个大型程序中的某部分代码，由一个或多个语句块组成。它负责完成某项特定任务，而且相较于其他代码，具备相对的独立性。</p>
<p>协程和子例程的执行过程对比如下：</p>
<p><img src="/images/cn/2021-04-01-process-thread-and-coroutine-theory/subroutine-vs-coroutine.png" alt=""></p>
<ul>
<li>子例程可以调用其他子例程，调用者等待被调用者结束后继续执行，故而子例程的生命期遵循后进先出，即最后一个被调用的子例程最先结束返回。协程的生命期完全由对它们的使用需要来决定。</li>
<li>子例程的起始处是惟一的入口点，每当子例程被调用时，执行都从被调用子例程的起始处开始。协程可以有多个入口点，协程的起始处是第一个入口点，每个 <code>yield</code> 返回出口点都是再次被调用执行时的入口点。</li>
<li>子例程只在结束时一次性的返回全部结果值。协程可以在 <code>yield</code> 时不调用其他协程，而是每次返回一部分的结果值，这种协程常称为<strong>生成器</strong>或<strong>迭代器</strong>。</li>
</ul>
<p>协程类似于线程，但是协程是协作式多任务的，而线程是抢占式多任务的。这意味着协程提供<strong>并发性</strong>而非<strong>并行性</strong>。协程超过线程的好处是它们可以用于硬性实时的语境（在协程之间的切换不需要涉及任何系统调用或任何阻塞调用），这里不需要用来守卫<strong>临界区段</strong>的<strong>同步原语</strong>比如互斥锁、信号量等，并且不需要来自操作系统的支持。</p>
<h2 id="通信">通信</h2>
<h3 id="进程间通信">进程间通信</h3>
<h4 id="管道">管道</h4>
<p>管道（Pipeline）是一系列将标准输入输出链接起来的进程，其中每一个进程的输出被直接作为下一个进程的输入。 例如：</p>
<pre><code class="language-shell">ls -l | less
</code></pre>
<p><code>ls</code> 用于在 Unix 下列出目录内容，<code>less</code> 是一个有搜索功能的交互式的文本分页器。这个管道使得用户可以在列出的目录内容比屏幕长时目录上下翻页。</p>
<h4 id="命名管道">命名管道</h4>
<p>命名管道是计算机进程间的一种先进先出通信机制。是类 Unix 系统传统管道的扩展。传统管道属于匿名管道，其生存期不超过创建管道的进程的生存期。但命名管道的生存期可以与操作系统运行期一样长。</p>
<h4 id="信号">信号</h4>
<p>信号（Signals）是 Unix、类 Unix 以及其他 POSIX 兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制，用来提醒进程一个事件已经发生。当一个信号发送给一个进程，操作系统中断了进程正常的控制流程，此时，任何非原子操作都将被中断。如果进程定义了信号的处理函数，那么它将被执行，否则就执行默认的处理函数。</p>
<p>例如，在一个运行的程序的控制终端键入特定的组合键可以向它发送某些信号：<kbd>Ctrl</kbd> + <kbd>C</kbd> 发送 INT 信号（SIGINT），这会导致进程终止；<kbd>Ctrl</kbd> + <kbd>Z</kbd> 发送 TSTP 信号（SIGTSTP），这会导致进程挂起。</p>
<h4 id="消息队列">消息队列</h4>
<p>消息队列提供了异步的通信协议，每一个贮列中的纪录包含详细说明的资料，包含发生的时间，输入设备的种类，以及特定的输入参数，也就是说：消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中，直到接收者取回它。</p>
<p>消息队列本身是异步的，它允许接收者在消息发送很长时间后再取回消息。和信号相比，消息队列能够传递更多的信息。与管道相比，消息队列提供了有格式的数据，这可以减少开发人员的工作量。</p>
<h4 id="信号量">信号量</h4>
<p>信号量（Semaphore）又称为信号标，是一个同步对象，用于保持在 0 至指定最大值之间的一个计数值。当线程完成一次对该 Semaphore 对象的等待（wait）时，该计数值减一；当线程完成一次对 Semaphore 对象的释放（release）时，计数值加一。当计数值为 0，则线程等待该 Semaphore 对象不再能成功直至该 Semaphore 对象变成 signaled 状态。Semaphore 对象的计数值大于 0，为 signaled 状态；计数值等于 0，为 nonsignaled 状态.</p>
<h4 id="共享内存">共享内存</h4>
<p>共享内存指可被多个进程存取的内存，一个进程是一段程序的单个运行实例。在这种情况下，共享内存被用作进程间的通讯。</p>
<h4 id="伯克利套接字">伯克利套接字</h4>
<p>伯克利套接字（Internet Berkeley Sockets），又称为 BSD 套接字是一种应用程序接口，主要用于实现进程间通讯，在计算机网络通讯方面被广泛使用。</p>
<h3 id="线程间通信">线程间通信</h3>
<h4 id="锁机制">锁机制</h4>
<ul>
<li><strong>互斥锁</strong>：互斥锁（Mutual Exclusion，Mutex）是一种用于多线程编程中，防止两条线程同时对同一公共资源（比如全局变量）进行读写的机制。</li>
<li><strong>条件锁</strong>：读写锁是计算机程序的并发控制的一种同步机制，用于解决读写问题。读操作可并发重入，写操作是互斥的。</li>
<li><strong>条件变量</strong>：条件变量是利用线程间共享的全局变量进行同步的一种机制，主要包括两个动作：一个线程等待“条件变量的条件成立”而挂起；另一个线程使“条件成立”（给出条件成立信号）。为了防止竞争，条件变量的使用总是和一个互斥锁结合在一起。</li>
<li><strong>自旋锁</strong>：自旋锁是用于多线程同步的一种锁，线程反复检查锁变量是否可用。由于线程在这一过程中保持执行，因此是一种忙等待。一旦获取了自旋锁，线程会一直保持该锁，直至显式释放自旋锁。</li>
</ul>
<h4 id="信号-1">信号</h4>
<p>同上文。</p>
<h4 id="信号量-1">信号量</h4>
<p>同上文。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://zh.wikipedia.org/wiki/%E8%BF%9B%E7%A8%8B">https://zh.wikipedia.org/wiki/进程</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B">https://zh.wikipedia.org/wiki/线程</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html">https://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>胶囊网络 (Capsule Network)</title><link>https://zeqiang.fun/cn/2021/03/capsule-network/</link><pubDate>Sun, 14 Mar 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/03/capsule-network/</guid><description><![CDATA[
        <h2 id="cnn-的缺陷">CNN 的缺陷</h2>
<p><a href="/cn/2018/08/cnn/">卷积神经网络（CNN）</a>在图像领域取得了很大的成功，但同时也存在一定的缺陷。卷积层中的卷积核对输入图像利用卷积运算提取其中的特征。卷积核以一个较小的尺寸并以一定的步长在图像上移动得到特征图。步长越大，特征图的尺寸就越小，过大的步长会丢失部分图像中的特征。池化层作用于产生的特征图上，使得 CNN 可以在不同形式的图像中识别出相同的物体，为 CNN 引入了空间不变性。</p>
<p>CNN 最大的缺陷就是忽略了不同特征之间的相对位置，从而无法从图像中识别出姿势、纹理和变化。CNN 中的池化操作使得模型具有空间不变性，因此模型就不具备等变性。以下图为例，CNN 会把第一幅和第二幅识别为人脸，而将第三幅方向翻转的图识别为不是人脸。池化操作造成了部分信息的丢失，因此需要更多的训练数据来补偿这些损失。</p>
<figure>
  <img data-src="/images/cn/2021-03-14-capsule-network/it-is-a-face.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：https://www.spiria.com/en/blog/artificial-intelligence/deep-learning-capsule-network-revolution/</p></figcaption>
</figure>
<h3 id="等变和不变">等变和不变</h3>
<p>对于一个函数 <code>$f$</code> 和一个变换 <code>$g$</code>，如果有：</p>
<p><code>$$ f \left(g \left(x\right)\right) = g \left(f \left(x\right)\right) $$</code></p>
<p>则称 <code>$f$</code> 对变换 <code>$g$</code> 有<strong>等变性</strong>。</p>
<p>例如，变换 <code>$g$</code> 为将图像向左平移若干像素，函数 <code>$f$</code> 表示检测一个人脸的位置。则 <code>$f \left(g \left(x\right)\right)$</code> 表示先将图片左移，我们将在原图的左侧检测到人脸；<code>$g \left(f \left(x\right)\right)$</code> 表示先检测人脸位置，然后将人脸位置左移。这两者的输出结果是一样的，与我们施加变换的顺序无关。CNN 中的卷积操作使得它对平移操作具有等变性。</p>
<p>对于一个函数 <code>$f$</code> 和一个变换 <code>$g: g \left(x\right) = x'$</code>，如果有：</p>
<p><code>$$ f \left(x\right) = f \left(x'\right) = f \left(g \left(x\right)\right) $$</code></p>
<p>则称 <code>$f$</code> 对变换 <code>$g$</code> 有<strong>不变性</strong>。</p>
<p>例如，变换 <code>$g$</code> 为旋转或平移，函数 <code>$f$</code> 表示检测图中是否有黑色，那么这些变换不会对函数结果有任何影响，可以说函数对该变换具有不变性。CNN 中的池化操作对平移操作具有近似不变性。</p>
<h3 id="逆图形">逆图形</h3>
<p>计算机图形学根据几何数据的内部层次结构来构造可视图像，该表示的结构将对象的相对位置考虑在内。软件采用层次的表示方式将其渲染为屏幕上的图像。人类大脑的工作原理则与渲染过程相反，我们称其为逆图形。大脑中对象的表示并不依赖于视角。</p>
<p>例如下图，人眼可以很容易的分辨出是自由女神像，只是角度不同，但 CNN 却很难做到，因为它不理解 3D 空间的内在。</p>
<p><img src="/images/cn/2021-03-14-capsule-network/statue-of-liberty-different-poses.jpg" alt=""></p>
<h2 id="胶囊网络">胶囊网络</h2>
<h3 id="胶囊">胶囊</h3>
<p>在引入“<strong>胶囊</strong>”这个概念的第一篇文献 Transforming Auto-encoders <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 中，Hinton 等人对胶囊概念理解如下：</p>
<blockquote>
<p>人工神经网络不应当追求“神经元”活动中的视角不变性（使用单一的标量输出来总结一个局部池中的重复特征检测器的活动），而应当使用局部的“胶囊”，这些胶囊对其输入执行一些相当复杂的内部计算，然后将这些计算的结果封装成一个包含信息丰富的输出的小向量。每个胶囊学习辨识一个有限的观察条件和变形范围内隐式定义的视觉实体，并输出实体在有限范围内存在的概率及一组“实例参数”，实例参数可能包括相对这个视觉实体的隐式定义的典型版本的精确的位姿、照明条件和变形信息。当胶囊工作正常时，视觉实体存在的概率具有局部不变性——当实体在胶囊覆盖的有限范围内的外观流形上移动时，概率不会改变。实例参数却是“等变的”——随着观察条件的变化，实体在外观流形上移动时，实例参数也会相应地变化，因为实例参数表示实体在外观流形上的内在坐标。</p>
</blockquote>
<p>人造神经元输出单个标量。对于 CNN 卷积层中的每个卷积核，对整个输入图复制同一个内核的权重输出一个二维矩阵。矩阵中每个数字是该卷积核对输入图像一部分的卷积，这个二维矩阵看作是重复特征检测器的输出。所有卷积核的二维矩阵堆叠在一起得到卷积层的输出。CNN 利用最大池化实现不变性，但最大池化丢失了有价值的信息，也没有编码特征之间的相对空间关系。</p>
<p>胶囊将<strong>特征检测的概率作为其输出向量的长度进行编码，检测出的特征的状态被编码为该向量指向的方向</strong>。当检测出的特征在图像中移动或其状态发生变化时，概率仍然保持不变（向量的长度没有改变），但它的方向改变了。</p>
<p>下表总结了胶囊和神经元的不同：</p>
<table>
  <thead>
    <tr>
      <th align="center" colspan="2"></th>
      <th align="center">Capsule</th>
      <th align="center">Traditional Neuron</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid;">
      <td align="center" colspan="2">Input from low-level capsule/neuron</td>
      <td align="center">$\text{vector}\left(\mathbf{u}_i\right)$</td>
      <td align="center">$\text{scalar}\left(x_i\right)$</td>
    <tr/>
    <tr>
      <td align="center" rowspan="8">Operration</td>
      <td align="center">Affine Transform</td>
      <td align="center">$\widehat{\mathbf{u}}_{j \mid i}=\mathbf{W}_{i j} \mathbf{u}_{i}$</td>
      <td align="center">-</td>
    <tr/>
    <tr>
      <td align="center">Weighting</td>
      <td align="center" rowspan="4">$\mathbf{s}_{j}=\sum_{i} c_{i j} \widehat{\mathbf{u}}_{j \mid i}$</td>
      <td align="center" rowspan="4">$a_{j}=\sum_{i} w_{i} x_{i}+b$</td>
    <tr/>
    <tr>
      <td align="center">Sum</td>
    <tr/>
    <tr>
      <td align="center">Nonlinear Activation</td>
      <td align="center">$\mathbf{v}_{j}=\dfrac{\left\|\mathbf{s}_{j}\right\|^{2}}{1+\left\|\mathbf{s}_{j}\right\|^{2}} \dfrac{\mathbf{s}_{j}}{\left\|\mathbf{s}_{j}\right\|}$</td>
      <td align="center">$h_{j}=f\left(a_{j}\right)$</td>
    <tr/>
    <tr style="border-top: 1px solid; border-bottom: 1px solid;">
      <td align="center" colspan="2">Output</td>
      <td align="center">$\text{vector}\left(\mathbf{v}_j\right)$</td>
      <td align="center">$\text{scalar}\left(h_j\right)$</td>
    </tr>
    <tr>
      <td align="right" colspan="3"><img src="/images/cn/2021-03-14-capsule-network/capsule-vs-traditional-neuron-capsule.png"/></td>
      <td align="center"><img src="/images/cn/2021-03-14-capsule-network/capsule-vs-traditional-neuron-traditional-neuron.png"/></td>
    </tr>
  </tbody>
</table>
<p>人造神经元包含如下 3 个计算步骤：</p>
<ol>
<li>输入标量的加权</li>
<li>加权标量的求和</li>
<li>求和标量到输出标量的非线性变换</li>
</ol>
<p>胶囊可以理解为上述 3 个步骤的向量版，同时增加了对输入的仿射变换：</p>
<ol>
<li>输入向量的矩阵乘法：胶囊接受的输入向量编码了低层胶囊检测出的相应对象的概率，向量的方向编码了检测出的对象的一些内部状态。接着将这些向量乘以相应的权重矩阵 <code>$\mathbf{W}$</code>，<code>$\mathbf{W}$</code> 编码了低层特征（例如：眼睛、嘴巴和鼻子）和高层特征（例如：面部）之间的空间关系和其他重要关系。</li>
<li>向量的标量加权：这个步骤同人造神经元对应的步骤类似，但神经元的权重是通过反向传播学习的，而胶囊则使用动态路由。</li>
<li>加权向量的求和：这个步骤同人造神经元对应的步骤类似。</li>
<li>求和向量到输出向量的非线性变换：胶囊神经网络的非线性激活函数接受一个向量，然后在不改变方向的前提下，将其长度压缩到 1 以下。</li>
</ol>
<h3 id="动态路由">动态路由</h3>
<p>胶囊网络使用动态路由算法进行训练，算法过程如下 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>：</p>


<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/pseudocode@latest/build/pseudocode.min.css">


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Routing 算法}
\begin{algorithmic}
\PROCEDURE{Routing}{$\widehat{\mathbf{u}}_{j | i}, r, l$}
\STATE for all capsule $i$ in layer $l$ and capsule $j$ in layer $\left(l + 1\right)$: $b_{ij} \gets 0$
\FOR{$r$ iterations}
  \STATE for all capsule $i$ in layer $l$: $\mathbf{c}_i \gets \text{softmax} \left(\mathbf{b}_i\right)$
  \STATE for all capsule $j$ in layer $\left(l + 1\right)$: $\mathbf{s}_j \gets \sum_{i} c_{ij} \widehat{\mathbf{u}}_{j | i}$
  \STATE for all capsule $j$ in layer $\left(l + 1\right)$: $\mathbf{v}_j \gets \text{squash} \left(\mathbf{s}_j\right)$
  \STATE for all capsule $i$ in layer $l$ and capsule $j$ in layer $\left(l + 1\right)$: $b_{ij} \gets b_{ij} + \widehat{\mathbf{u}}_{j | i} \cdot \mathbf{v}_j$
\ENDFOR
\RETURN $\mathbf{v}_j$
\ENDPROCEDURE
\end{algorithmic}
\end{algorithm}
</pre></div>

<ol>
<li>第 1 行表示算法的输入为：低层 <code>$l$</code> 中所有胶囊的输出 <code>$\widehat{\mathbf{u}}$</code>，以及路由迭代计数 <code>$r$</code>。</li>
<li>第 2 行中的 <code>$b_{ij}$</code> 为一个临时变量，其值在迭代过程中更新，算法运行完毕后其值被保存在 <code>$c_{ij}$</code> 中。</li>
<li>第 3 行表示如下步骤将会被重复 <code>$r$</code> 次。</li>
<li>第 4 行利用 <code>$\mathbf{b}_i$</code> 计算低层胶囊的权重向量 <code>$\mathbf{c}_i$</code>。<code>$\text{softmax}$</code> 确保了所有权重为非负数，且和为一。第一次迭代后，所有系数 <code>$c_{ij}$</code> 的值相等，随着算法的继续，这些均匀分布将发生改变。</li>
<li>第 5 行计算经前一步确定的路由系数 <code>$c_{ij}$</code> 加权后的输入向量的线性组合。该步缩小输入向量并将他们相加，得到输出向量 <code>$\mathbf{s}_j$</code>。</li>
<li>第 6 行对前一步的输出向量应用 <code>$\text{squash}$</code> 非线性函数。这确保了向量的方向被保留下来，而长度被限制在 1 以下。</li>
<li>第 7 行通过观测低层和高层的胶囊，根据公式更新相应的权重 <code>$b_{ij}$</code>。胶囊 <code>$j$</code> 的当前输出与从低层胶囊 <code>$i$</code> 处接收到的输入进行点积，再加上旧的权重作为新的权重。点积检测胶囊输入和输出之间的相似性。</li>
<li>重复 <code>$r$</code> 次，计算出所有高层胶囊的输出，并确立路由权重。之后正向传导就可以推进到更高层的网络。</li>
</ol>
<p>点积运算接收两个向量，并输出一个标量。对于给定长度但方向不同的两个向量而言，点积有几种情况：<code>$a$</code> 最大正值；<code>$b$</code> 正值；<code>$c$</code> 零；<code>$d$</code> 负值；<code>$e$</code> 最小负值，如下图所示：</p>
<p><img src="/images/cn/2021-03-14-capsule-network/capsule-dot-product-1.png" alt=""></p>
<p>我们用紫色向量 <code>$\mathbf{v}_1$</code> 和 <code>$\mathbf{v}_2$</code> 表示高层胶囊，橙色向量表示低层胶囊的输入，其他黑色向量表示接收自其他低层胶囊的输入，如下图所示：</p>
<p><img src="/images/cn/2021-03-14-capsule-network/capsule-dot-product-2.png" alt=""></p>
<p>左侧的紫色输出 <code>$\mathbf{v}_1$</code> 和橙色输入 <code>$\widehat{\mathbf{u}}_{1|1}$</code> 指向相反的方向，这意味着他们的点积是一个负数，从而路由系数 <code>$c_{11}$</code> 减少；右侧的紫色输出 <code>$\mathbf{v}_2$</code> 和橙色输入 <code>$\widehat{\mathbf{u}}_{2|1}$</code> 指向相同的方向，从而路由系数 <code>$c_{12}$</code> 增加。在所有高层胶囊及其所有输入上重复该过程，得到一个路由系数集合，达成了来自低层胶囊的输出与高层胶囊的输出的最佳匹配。</p>
<h3 id="网络架构">网络架构</h3>
<p>胶囊网络由 6 层神经网络构成，前 3 层是编码器，后 3 层是解码器：</p>
<ol>
<li>卷积层</li>
<li>PrimaryCaps（主胶囊）层</li>
<li>DigitCaps（数字胶囊）层</li>
<li>第一全连接层</li>
<li>第二全连接层</li>
<li>第三全连接层</li>
</ol>
<h4 id="编码器">编码器</h4>
<p>编码器部分如下图所示：</p>
<p><img src="/images/cn/2021-03-14-capsule-network/capsule-network-encoder.png" alt=""></p>
<p>卷积层用于检测 2D 图像的基本特征。PrimaryCaps 层包含 32 个主胶囊，接收卷积层检测到的基本特征，生成特征的组合。DigitCaps 层包含 10 个数字胶囊，每个胶囊对应一个数字。</p>
<p>对于 <code>$k$</code> 个类别的数字，我们希望最高层的胶囊当且仅当一个数字出现在图像中时具有一个长的实例化向量。为了允许多个数字，对于每个 DigitCap 使用一个独立的损失函数：</p>
<p><code>$$ L_{k}=T_{k} \max \left(0, m^{+}-\left\|\mathbf{v}_{k}\right\|\right)^{2}+\lambda\left(1-T_{k}\right) \max \left(0,\left\|\mathbf{v}_{k}\right\|-m^{-}\right)^{2} $$</code></p>
<p>DigitCaps 层的输出为 10 个 16 维的向量，根据上面的公式计算每个向量的损失值，然后将 10 个损失值相加得到最终损失。</p>
<p>在损失函数中，当正确的标签与特定 DigitCap 的数字对应时 <code>$T_k$</code> 为 1，否则为 0。加号前一项用于计算正确 DigitCap 的损失，当概率值大于 <code>$m^{+} = 0.9$</code> 时为 0，当概率值小于 <code>$m^{+} = 0.9$</code> 时为非零值；加号后一项用于计算错误 DigitCap 的损失，当概率值小于 <code>$m^{-} = 0.1$</code> 时值为 0，当概率值大于 <code>$m^{-} = 0.1$</code> 时为非零值。公式中的 <code>$\lambda = 0.5$</code> 用于确保训练中数值的稳定性。</p>
<p>简单来说，低层的胶囊用于检测一些特定模式的出现概率和姿态，高层的胶囊用于检测更加复杂的图像，如下图所示：</p>
<p><img src="/images/cn/2021-03-14-capsule-network/two-layer-capsnet.png" alt=""></p>
<h4 id="解码器">解码器</h4>
<p>解码器部分如下图所示：</p>
<p><img src="/images/cn/2021-03-14-capsule-network/capsule-network-decoder.png" alt=""></p>
<p>解码器从正确的 DigitCap 中接收一个 16 维向量，并学习将其解码为数字图像。解码器接收正确的 DigitCap 的输出作为输入，并学习重建一张 <code>$28 \times 28$</code> 像素的图像，损失函数为重建图像与输入图像之间的欧式距离。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Hinton, G. E., Krizhevsky, A., &amp; Wang, S. D. (2011, June). Transforming auto-encoders. In <em>International conference on artificial neural networks</em> (pp. 44-51). Springer, Berlin, Heidelberg.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Sabour, S., Frosst, N., &amp; Hinton, G. E. (2017, December). Dynamic routing between capsules. In <em>Proceedings of the 31st International Conference on Neural Information Processing Systems</em> (pp. 3859-3869).&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>投票公平合理吗？</title><link>https://zeqiang.fun/cn/2021/01/is-voting-fair-and-reasonable/</link><pubDate>Sun, 17 Jan 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/01/is-voting-fair-and-reasonable/</guid><description><![CDATA[
        <blockquote>
<p>只有道德上的相对民主，没有制度上的绝对公平，求同存异才能长治久安。</p>
</blockquote>
<h2 id="无处不在的投票">无处不在的投票</h2>
<p>在古代雅典城邦有一项政治制度称之为<a href="https://zh.wikipedia.org/wiki/%E9%99%B6%E7%89%87%E6%94%BE%E9%80%90%E5%88%B6"><strong>陶片放逐制</strong></a>，是由雅典政治家<a href="https://zh.wikipedia.org/wiki/%E5%85%8B%E9%87%8C%E6%96%AF%E6%8F%90%E5%B0%BC">克里斯提尼</a>于公元前 510 年创立。雅典人民可以通过投票强制将某个人放逐，目的在于驱逐可能威胁雅典的民主制度的政治人物。投票者在选票——陶罐碎片较为平坦处，刻上他认为应该被放逐者的名字，投入本部落的投票箱。如果选票总数未达到 6000，此次投票即宣告无效；如果超过 6000，再按票上的名字将票分类，得票最多的人士即为当年放逐的人选。</p>
<p><img src="/images/cn/2021-01-17-is-voting-fair-and-reasonable/agma-ostrakon-aristide.png" alt=""></p>
<p>美国总统选举的方式称为<a href="https://zh.wikipedia.org/wiki/%E9%81%B8%E8%88%89%E4%BA%BA%E5%9C%98_(%E7%BE%8E%E5%9C%8B)"><strong>选举人团</strong></a>，是一种间接选举，旨在选出美国总统和副总统。根据《美国宪法》及其修正案，美国各州公民先选出该州的选举人，再由选举人代表该州投票。不采用普选制度的原因，主要是由于美国是联邦制国家，并考虑到各州的特定地理及历史条件，制宪元老决定采取选举人团制度，保障各州权益，所以美国没有公民直选的总统。</p>
<p><img src="/images/cn/2021-01-17-is-voting-fair-and-reasonable/usa-electoral-college-2020-with-results.png" alt=""></p>
<p>离我们最近的可能就是朋友聚会吃什么这个问题了，烧烤还是火锅，这是个问题。当然，只要你想要，还可以：蒸羊羔、蒸熊掌、蒸鹿尾儿、烧花鸭、烧雏鸡、烧子鹅 &hellip;&hellip;</p>
<p><img src="/images/cn/2021-01-17-is-voting-fair-and-reasonable/barbecue-or-hot-pot.png" alt=""></p>
<h2 id="投票制度">投票制度</h2>
<h3 id="多数制">多数制</h3>
<p><a href="https://zh.wikipedia.org/wiki/%E5%A4%9A%E6%95%B8%E5%88%B6"><strong>多数制</strong></a>（Plurality Voting System）的原则是“胜者全取”，又被称为<strong>多数代表制</strong>（Majoritarian Representation），分成<strong>相对多数</strong>（Relative Plurality）和<strong>绝对多数</strong>（Absolute Majority）。相对多数制即不论票数多少，得票最多的候选人便可当选。绝对多数制指候选人需要得到指定的票数方可当选，门槛在设定上须达有效票之过半数、三分之二、四分之三或五分之四等多数，亦可以是更高的比例或数字。</p>
<p>多数制的优点在于简单易行，缺点在于当选票分散的越平均，投票结果的争议性就会越大。例如：有 10 人参与投票，有 3 人选择吃火锅，有 3 人选择吃烧烤，有 4 人选择吃家常菜。根据多数制规则，最终的选择为吃家常菜，但会有 6/10 的人没有得到满意的结果。</p>
<h3 id="波达计数法">波达计数法</h3>
<p><a href="https://zh.wikipedia.org/wiki/%E6%B3%A2%E9%81%94%E8%A8%88%E6%95%B8%E6%B3%95"><strong>波达计数法</strong></a>（Borda Count）是通过投票人对候选者进行排序，如果候选者在选票的排第一位，它就得某个分数，排第二位得一个较小的分数，如此类推，分数累计下来最高分的候选者便取胜。</p>
<p>波达计数法相比于多数制不容易选出比较有争议的选项。假设有一场选举，共 4 位候选人，共收集到有效选票 100 张，选票统计结果（理论山可能出现的选票类型为 <code>$4! = 24$</code> 种可能，简单起见，示例中仅有 4 种可能）如下：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">#</th>
          <th style="text-align: center">51 票</th>
          <th style="text-align: center">5 票</th>
          <th style="text-align: center">23 票</th>
          <th style="text-align: center">21 票</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">1</td>
          <td style="text-align: center"><strong>张三</strong></td>
          <td style="text-align: center"><strong>王五</strong></td>
          <td style="text-align: center">李四</td>
          <td style="text-align: center">马六</td>
      </tr>
      <tr>
          <td style="text-align: center">2</td>
          <td style="text-align: center"><strong>王五</strong></td>
          <td style="text-align: center">李四</td>
          <td style="text-align: center"><strong>王五</strong></td>
          <td style="text-align: center"><strong>王五</strong></td>
      </tr>
      <tr>
          <td style="text-align: center">3</td>
          <td style="text-align: center">李四</td>
          <td style="text-align: center">马六</td>
          <td style="text-align: center">马六</td>
          <td style="text-align: center">李四</td>
      </tr>
      <tr>
          <td style="text-align: center">4</td>
          <td style="text-align: center">马六</td>
          <td style="text-align: center"><strong>张三</strong></td>
          <td style="text-align: center"><strong>张三</strong></td>
          <td style="text-align: center"><strong>张三</strong></td>
      </tr>
  </tbody>
</table>
<p>选举采用排名第 <code>$n$</code> 位得 <code>$4 - n$</code> 分的准则，每个人的分数如下：</p>
<ul>
<li>张三：<code>$51 \times 3 + 5 \times 0 + 23 \times 0 + 21 \times 0 = 153$</code></li>
<li>李四：<code>$51 \times 1 + 5 \times 2 + 23 \times 3 + 21 \times 1 = 151$</code></li>
<li>王五：<code>$51 \times 2 + 5 \times 3 + 23 \times 2 + 21 \times 2 = 205$</code></li>
<li>马六：<code>$51 \times 0 + 5 \times 1 + 23 \times 1 + 21 \times 3 = 91$</code></li>
</ul>
<p>按照多数制应该是张三获胜（得到第 1 名的票数最多，共计 51 票）。不过通过波达计数法可以发现大家对张三的喜好比较极端：只有特别喜欢（排名第 1）和特别不喜欢（排名第 4）两种情况，所以张三是一个<strong>具有争议的人</strong>。但王五不同，虽然其获得第 1 的选票并不多，但是获得第 2 的选票很多，说明<strong>大部分人都是比较能接受</strong>王五的。</p>
<p>波达计数法同时也存在几个问题：</p>
<ol>
<li>
<p>不同的权重的计分规则，可能得到不同结果。以下表为例：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">#</th>
          <th style="text-align: center">2 票</th>
          <th style="text-align: center">3 票</th>
          <th style="text-align: center">4 票</th>
          <th style="text-align: center">#</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">2 分</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">5 分</td>
      </tr>
      <tr>
          <td style="text-align: center">1 分</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">2 分</td>
      </tr>
      <tr>
          <td style="text-align: center">0 分</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">1 分</td>
      </tr>
  </tbody>
</table>
<p>采用左边权重的计分结果为：A：10 分，B：6 分，C：11 分，则 C 胜出。采用右边权重的计分结果为：A：29 分，B：15 分，C：28 分，则 A 胜出。不难看出，多数投票制是波达计数法的一个特例，即第 1 名的权重为非零值，其余名次权重均为零。</p>
</li>
<li>
<p>容易出现恶意投票局面。参与的人在投票时往往会掺杂个人情感，假设存在这样一次选举：张三、李四、王五三人竞选班长，张三和李四是最有希望竞争班长的人员，而王五则表现平庸。参与投票的同学几乎近一半的人支持张三，近一半的人支持李四，很少人支持王五。参与投票的同学自认为都很“聪明”，担心自己不支持的人（假设为李四）担任班长，同时认为王五没希望竞选成功，因此在排序时会把王五排在第 2 名，把李四排在第 3 名。此时，张三和李四都变成了具有争议的人，那么选举的结果就很有可能是王五获胜。</p>
</li>
</ol>
<h3 id="孔多赛投票制">孔多赛投票制</h3>
<p>波达计数法还存在一个严重的问题是，当其中一个选项退出后，投票的结果会发生变化。仍然以上文中的例子为例：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">#</th>
          <th style="text-align: center">2 分</th>
          <th style="text-align: center">1 分</th>
          <th style="text-align: center">0 分</th>
          <th style="text-align: center">#</th>
          <th style="text-align: center">1 分</th>
          <th style="text-align: center">0 分</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">2 票</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">2 票</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">C</td>
      </tr>
      <tr>
          <td style="text-align: center">3 票</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">3 票</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">C</td>
      </tr>
      <tr>
          <td style="text-align: center">4 票</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">4 票</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A</td>
      </tr>
  </tbody>
</table>
<p>在包含选项 B 时，投票结果为：A：10 分，B：6 分，C：11 分，C 获胜。当把选项 B 去掉后，投票结果为：A：5 分，C：4 分，A 获胜。去掉选项 B，规则没有发生变化（不同排名之间权重相差为 1），投票人的意愿也没有发生变化（候选人的相对名次没有发生变动），但投票结果却截然不同。</p>
<p>为了解决这个问题，<strong>孔多赛</strong>提供了采用两两对决的方式进行投票。投票人依旧按照类似波达计数法对候选人进行排序，但与波达计数法不同的是并不进行计分而是需要统计出所有两两对决的情况。上面这个例子中两两对决的情况有 6 种：<code>A &gt; B</code>，<code>B &gt; A</code>，<code>B &gt; C</code>，<code>C &gt; B</code>，<code>A &gt; C</code>，<code>C &gt; A</code>，对决的统计结果如下表所示：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">#</th>
          <th style="text-align: center">5 次</th>
          <th style="text-align: center">4 次</th>
          <th style="text-align: center">2 次</th>
          <th style="text-align: center">7 次</th>
          <th style="text-align: center">5 次</th>
          <th style="text-align: center">4 次</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">1 分</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">C</td>
      </tr>
      <tr>
          <td style="text-align: center">0 分</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A</td>
      </tr>
  </tbody>
</table>
<p>由于 <code>A &gt; B</code> 有 5 次，<code>B &gt; A</code> 有 4 次，因此在 A 和 B 的两两对决中获胜的是 A。同理可得，在 A 和 C 的两两对决中获胜的是 A，在 B 和 C 的两两对决中获胜的是 C。当存在很多选项时，假设选项 A 在与任何其它选项的两两比较中都能胜出，那 A 称为<strong>孔多塞胜利者</strong>。</p>
<p>如果你对 Facebook 历史有了解，早期扎克伯格在哈佛大学就读学士期间，写了一个名为 Facemash 的网站程序，根据哈佛校内报纸《The Harvard Crimson》，Facemash 会从校内的网络上收集照片，每次将两张照片并排后让用户选择“更火辣”的照片。Facemash 就采取了类似孔多塞投票的方法对女生进行投票，下图是电影<a href="https://movie.douban.com/subject/3205624/">社交网络</a>中这个原型的截图：</p>
<p><img src="/images/cn/2021-01-17-is-voting-fair-and-reasonable/facemash.png" alt=""></p>
<p>孔多塞投票制由于统计比较麻烦，现实生活中使用的情况不多。同时，孔多塞投票也存在一个问题，即<strong>孔多塞循环</strong>。假设 A 和 B 对决的优胜者为 A，B 和 C 对决的优胜者为 B，C 和 A 对决的优胜者为 C，则没有一个选项打败其他所有选项，那么这样就无法得到投票的结果。这称之为<a href="https://zh.wikipedia.org/wiki/%E6%8A%95%E7%A5%A8%E6%82%96%E8%AE%BA"><strong>孔多塞悖论</strong></a>（投票悖论），在这个假想情况中，集体倾向可以是循环性的，即使个人的倾向不是。</p>
<h2 id="所以呢">所以呢？</h2>
<p>所以什么样的投票才算是公平合理的投票呢？<a href="https://zh.wikipedia.org/wiki/%E8%82%AF%E5%B0%BC%E6%96%AF%C2%B7%E9%98%BF%E7%BD%97">肯尼斯·阿罗</a>给出了一个解答。</p>
<p>有 <code>$N$</code> 种选择，有 <code>$m$</code> 个决策者，他们每个人都对这 <code>$N$</code> 个选择有一个从优至劣的排序。我们要设计一种选举法则，使得将这 <code>$m$</code> 个排序的信息汇总成一个新的排序，称为投票结果。我们希望这种法则满足<a href="https://zh.wikipedia.org/wiki/%E9%98%BF%E7%BD%97%E6%82%96%E8%AE%BA">以下条件</a>：</p>
<ol>
<li><strong>一致性（Unanimity）</strong>。或称为<a href="https://zh.wikipedia.org/wiki/%E5%B8%95%E7%B4%AF%E6%89%98%E6%9C%80%E4%BC%98">帕累托最优</a>（Pareto Efficiency），即如果所有的 <code>$m$</code> 个决策者都认为选择 <code>$A$</code> 优于 <code>$B$</code>，那么在投票结果中，<code>$A$</code> 也优于 <code>$B$</code>。</li>
<li><strong>非独裁（Non-Dictatorship）</strong>。不存在一个决策者 <code>$X$</code>，使得投票结果总是等同于 <code>$X$</code> 的排序。</li>
<li><strong>独立于无关选项（Independence of Irrelevant Alternatives，IIA)）</strong>。如果现在一些决策者改了主意，但是在每个决策者的排序中，<code>$A$</code> 和 <code>$B$</code> 的相对位置不变，那么在投票结果中 <code>$A$</code> 和 <code>$B$</code> 的相对位置也不变。</li>
</ol>
<p>如果选项 <code>$N \geq 3$</code>，投票人数 <code>$m \geq 2$</code> 时，没有任何一种投票规则能够满足以上 3 点，这就是<a href="https://zh.wikipedia.org/wiki/%E9%98%BF%E7%BD%97%E6%82%96%E8%AE%BA">阿罗悖论</a>。所以只有道德上的相对民主，没有制度上的绝对公平，求同存异才能长治久安。</p>
<blockquote>
<p>本文参考自《<a href="https://www.youtube.com/watch?v=9Oisrp99L14">投票公平合理吗？为什么没有绝对的公平？阿罗不可能定理</a>》。</p>
</blockquote>

        ]]></description></item><item><title>图存储与计算（Network Storage &amp; Computing)</title><link>https://zeqiang.fun/cn/2021/01/network-storage-and-computing/</link><pubDate>Fri, 01 Jan 2021 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2021/01/network-storage-and-computing/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%A4%8D%E6%9D%82%E7%BD%91%E7%BB%9C/">《复杂网络系列》</a>文章</p>
</blockquote>
<h2 id="图存储">图存储</h2>
<h3 id="语义网络与-rdf-存储">语义网络与 RDF 存储</h3>
<p>1968 年 Ross Quillian 在其博士论文中最先提出<strong>语义网络</strong>（Semantic Web），把它作为人类联想记忆的一个显式心理学模型，并在他设计的可教式语言理解器 TLC（Teachable Language Comprehenden）中用作知识表示方法。</p>
<p>语义网络的基本思想是在网络中，用“节点”代替概念，用节点间的“连接弧”（称为联想弧）代替概念之间的关系，因此，语义网络又称联想网络。它在形式上是一个带标识的有向图。由于所有的概念节点均通过联想弧彼此相连知识推导。</p>
<p>一个语义网络的基本构成如下：</p>
<ul>
<li>语义网络中的节点：表示各种事物、概念、情况、属性、动作、状态等，每个节点可以带有若干属性，一般用框架或元组表示。此外，节点还可以是一个语义子网络，形成一个多层次的嵌套结构。</li>
<li>语义网络中的弧：表示各种语义联系，指明它所连接的节点间某种语义关系。</li>
<li>节点和弧都必须带有标识，以便区分各种不同对象以及对象间各种不同的语义联系。</li>
</ul>
<p>之后 Tim Berners-Lee 又提出了<strong>语义网堆栈</strong>（Semantic Web Stack）的概念。语义网堆栈利用图示解释是不同层面的语言所构成的层级结构，其中，每一层面都将利用下游层面的能力，语义网堆栈如下图所示：</p>
<img src="/images/cn/2021-01-01-network-storage-and-computing/sweb-stack-zh.png" width="60%" />
<p><strong>资源描述框架</strong>（Resource Description Framework，RDF）是用于描述网络资源的 W3C 标准，比如网页的标题、作者、修改日期、内容以及版权信息。</p>
<p>RDF 使用 Web 标识符来标识事物，并通过属性和属性值来描述资源。</p>
<p>对资源、属性和属性值的解释：</p>
<ul>
<li>资源是可拥有 URI 的任何事物，比如 <code>http://www.w3school.com.cn/rdf</code></li>
<li>属性是拥有名称的资源，比如 <code>author</code> 或 <code>homepage</code></li>
<li>属性值是某个属性的值，比如 <code>David</code> 或 <code>http://www.w3school.com.cn</code>（请注意一个属性值可以是另外一个资源)</li>
</ul>
<p>下面是一个 RDF 示例文档（这是一个简化的例子，命名空间被忽略了）：</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot;?&gt;

&lt;RDF&gt;
  &lt;Description about=&quot;http://www.w3school.com.cn/RDF&quot;&gt;
    &lt;author&gt;David&lt;/author&gt;
    &lt;homepage&gt;http://www.w3school.com.cn&lt;/homepage&gt;
  &lt;/Description&gt;
&lt;/RDF&gt;
</code></pre>
<p><strong>资源</strong>、<strong>属性</strong>和<strong>属性值</strong>的组合可形成一个陈述（被称为陈述的主体、谓语和客体)。上述的 RDF 文档包含了如下两个陈述：</p>
<ul>
<li>陈述：The <code>author</code> of <code>http://www.w3school.com.cn/rdf</code> is <code>David</code>
<ul>
<li>陈述的主体是：<code>http://www.w3school.com.cn/rdf</code></li>
<li>谓语是：<code>author</code></li>
<li>客体是：<code>David</code></li>
</ul>
</li>
<li>陈述：The <code>homepage</code> of <code>http://www.w3school.com.cn/rdf</code> is <code>http://www.w3school.com.cn</code>
<ul>
<li>陈述的主体是：<code>http://www.w3school.com.cn/rdf</code></li>
<li>谓语是：<code>homepage</code></li>
<li>客体是：<code>http://www.w3school.com.cn</code></li>
</ul>
</li>
</ul>
<p>更多 RDF 介绍请参见：https://www.w3school.com.cn/rdf/index.asp 。</p>
<p><a href="https://jena.apache.org/">Apache Jena</a> 是一个用于构建<strong>语义网络</strong>（Semantic Web）和<strong>链接数据</strong>（Linked Data）应用的开源 Java 框架。Jena 提供了 3 大部分功能：</p>
<ol>
<li>RDF
<ul>
<li>RDF API：提供构建和读取 RDF 图的核心 API，并利用 <a href="https://en.wikipedia.org/wiki/RDF/XML">RDF/XML</a> 或 <a href="https://en.wikipedia.org/wiki/Turtle_(syntax)">Turtle</a> 等数据类型序列化数据。</li>
<li>ARQ（SPARQL)：提供一种 SPARQL 1.1 的编译引擎 ARQ 用于查询 RDF。</li>
</ul>
</li>
<li>Triple store
<ul>
<li>TDB：提供一种原生高效的 Triple 存储 TDB，全面支持 Jena APIs。</li>
<li>Fuseki：提供 REST 风格的 RDF 数据交互方式。</li>
</ul>
</li>
<li>OWL
<ul>
<li>Ontology API：通过 RDFS，OWL 等为 RDF 数据添加更多语义信息。</li>
<li>Inference API：通过内置的 OWL 和 RDFS <a href="https://en.wikipedia.org/wiki/Semantic_reasoner">语义推理器</a> 构建个性化的推理规则。</li>
</ul>
</li>
</ol>
<p>下面以 <strong>Graph of The Gods</strong> 的关系图对 Jena 的基本功能进行说明。<strong>Graph of The Gods</strong> 是一张描述希腊神话相关事物之间关系的图，其中顶点的类型有：titan（泰坦，希腊神话中曾经统治师姐的古老神族)，god（神)，demigod（半神)，human（人)，monster（怪物)，location（地点)；关系的类型有：father（父亲)，brother（兄弟)，mother（母亲)，battled（战斗)，lives（居住)。</p>
<p><img src="/images/cn/2021-01-01-network-storage-and-computing/graph-of-the-gods.svg" alt=""></p>
<p>以 Apache Tomcat 作为容器来安装 Apache Jena Fuseki，下载最新版的 Apache Jena Fuseki 并解压，将其中的 fuseki.war 复制到已经安装并运行的 Apache Tomcat 的 webapps 路径下。安装完毕后，进入 http://127.0.0.1:8080/fuseki 即可使用 Apache Jena Fuseki。</p>
<p><img src="/images/cn/2021-01-01-network-storage-and-computing/apache-jena-fuseki.png" alt=""></p>
<p>在导入 Graph of The Gods 数据后，执行如下查询语句可以获得 <code>jupiter</code> 的所有兄弟：</p>
<pre><code class="language-sparql">PREFIX gods: &lt;http://leovan.me/gods/&gt;

SELECT DISTINCT ?god
WHERE {
  ?god gods:brother gods:jupiter
}
</code></pre>
<p>查询结果为：</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>god</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>gods:pluto</td>
      </tr>
      <tr>
          <td>2</td>
          <td>gods:neptune</td>
      </tr>
  </tbody>
</table>
<h3 id="图数据库">图数据库</h3>
<p><strong>图数据库</strong>是一个使用图结构进行语义查询的数据库，它使用节点、边和属性来表示和存储数据。不同于关系型数据库，图数据库为 NoSQL（Not Only SQL）的一种，属于联机事务处理（OLTP）的范畴，可以解决现有关系数据库的局限性。</p>
<p>下图展示了近年来不同类型数据库的流行度趋势，不难看出近年来越来越多的人开始关注图数据库。</p>
<figure>
  <img data-src="/images/cn/2021-01-01-network-storage-and-computing/db-engines-database-categories-trend.png" class="lazyload"/>
  <figcaption><p class="figcaption">数据库流行度趋势 <a href="https://db-engines.com/en/ranking_categories">https://db-engines.com/en/ranking_categories</a></p></figcaption>
</figure>
<p>截止到 2020 年 12 月，图数据库的排名如下图所示：</p>
<figure>
  <img data-src="/images/cn/2021-01-01-network-storage-and-computing/db-engines-graph-database-ranking.png" class="lazyload"/>
  <figcaption><p class="figcaption">图数据库排名 <a href="https://db-engines.com/en/ranking/graph+dbms">https://db-engines.com/en/ranking/graph+dbms</a></p></figcaption>
</figure>
<p>其中，<a href="https://neo4j.com/">Neo4j</a>、<a href="https://janusgraph.org/">JanusGraph</a>、<a href="https://dgraph.io/">Dgraph</a>、<a href="https://www.tigergraph.com/">TigerGraph</a>、<a href="https://nebula-graph.io/">Nebula Graph</a> 均为时下常用的图数据库。从下图的流行度趋势角度来看，JanusGraph、Dgraph、TigerGraph 和 Nebula Graph 等后起之秀发展迅速。</p>
<figure>
  <img data-src="/images/cn/2021-01-01-network-storage-and-computing/db-engines-graph-database-trend.png" class="lazyload"/>
  <figcaption><p class="figcaption">图数据库流行度趋势 <a href="https://db-engines.com/en/ranking_trend/graph+dbms">https://db-engines.com/en/ranking_trend/graph+dbms</a></p></figcaption>
</figure>
<p>不同的图数据库有着不同的优劣势，用户可以根据实际业务场景选择合适的图数据库。下面给到一些较新的图数据库对比和评测：</p>
<ol>
<li><a href="https://nebula-graph.com.cn/posts/benchmarking-mainstraim-graph-databases-dgraph-nebula-graph-janusgraph/">主流开源分布式图数据库 Benchmark</a></li>
<li><a href="https://nebula-graph.com.cn/posts/performance-comparison-neo4j-janusgraph-nebula-graph/">图数据库对比：Neo4j vs Nebula Graph vs HugeGraph</a></li>
<li><a href="https://www.tigergraph.com.cn/wp-content/uploads/2018/10/TigerGraph-Benchmark-Report-2018-1.pdf">图分析系统基准测试报告</a></li>
<li><a href="https://fma-ai.cn/pdf/FMA_benchmark.pdf">图数据平台产品测试报告</a></li>
</ol>
<h3 id="查询语言">查询语言</h3>
<p><strong>图查询语言</strong>（Graph Query Language，GQL）是一种用于图数据库的查询语言，类比于关系型数据库的查询语言 SQL。2019 年 9 月，GQL 被提议为一种新的数据库查询语言（<a href="https://www.iso.org/standard/76120.html">ISO/IEC WD 39075</a>），目前仍处于开发当中，因此市面上还没有统一的图查询语言标准。</p>
<h4 id="gremlin">Gremlin</h4>
<p><a href="https://tinkerpop.apache.org/gremlin.html">Gremlin</a> 是 <a href="https://tinkerpop.apache.org/">Apache TinkerPop</a> 框架下的图遍历语言。Gremlin 适用于基于 OLTP 的图数据库以及基于 OLAP 的图分析引擎，支持命令式和声明式查询。支持 Gremlin 的图数据库有：Neo4j、JanusGraph 等。</p>
<h4 id="cypher">Cypher</h4>
<p><a href="http://www.opencypher.org/">Cypher</a> 是一种声明式图查询语言，这使得在不必编写遍历逻辑的情况下可以实现高效的查询。支持 Cypher 的图数据库有：Neo4j、RedisGraph、Nebula Graph 等。</p>
<h4 id="ngql">nGQL</h4>
<p><a href="https://docs.nebula-graph.com.cn/manual-CN/1.overview/1.concepts/2.nGQL-overview/">nGQL</a> 是一种声明式的图查询语言，支持图遍历、模式匹配、聚合运算和图计算等特性。支持 nGQL 的图数据库有：Nebula Graph。</p>
<h4 id="比较">比较</h4>
<p>针对 3 种不同的查询语言，对于图中相关概念的表示也略有不同，如下表所示：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">术语</th>
          <th style="text-align: left">Gremlin</th>
          <th style="text-align: left">Cypher</th>
          <th style="text-align: left">nGQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">点</td>
          <td style="text-align: left">Vertex</td>
          <td style="text-align: left">Node</td>
          <td style="text-align: left">Vertex</td>
      </tr>
      <tr>
          <td style="text-align: left">边</td>
          <td style="text-align: left">Edge</td>
          <td style="text-align: left">Relationship</td>
          <td style="text-align: left">Edge</td>
      </tr>
      <tr>
          <td style="text-align: left">点类型</td>
          <td style="text-align: left">Label</td>
          <td style="text-align: left">Label</td>
          <td style="text-align: left">Tag</td>
      </tr>
      <tr>
          <td style="text-align: left">边类型</td>
          <td style="text-align: left">label</td>
          <td style="text-align: left">RelationshipType</td>
          <td style="text-align: left">edge type</td>
      </tr>
      <tr>
          <td style="text-align: left">点 ID</td>
          <td style="text-align: left">vid</td>
          <td style="text-align: left">id(n)</td>
          <td style="text-align: left">vid</td>
      </tr>
      <tr>
          <td style="text-align: left">边 ID</td>
          <td style="text-align: left">eid</td>
          <td style="text-align: left">id(r)</td>
          <td style="text-align: left">无</td>
      </tr>
      <tr>
          <td style="text-align: left">插入</td>
          <td style="text-align: left">add</td>
          <td style="text-align: left">create</td>
          <td style="text-align: left">insert</td>
      </tr>
      <tr>
          <td style="text-align: left">删除</td>
          <td style="text-align: left">drop</td>
          <td style="text-align: left">delete</td>
          <td style="text-align: left">delete / drop</td>
      </tr>
      <tr>
          <td style="text-align: left">更新属性</td>
          <td style="text-align: left">setProperty</td>
          <td style="text-align: left">set</td>
          <td style="text-align: left">update</td>
      </tr>
  </tbody>
</table>
<p>更多不同查询语言之间的详细对比可以参见如下资料：</p>
<ol>
<li><a href="https://nebula-graph.com.cn/posts/graph-query-language-comparison-cypher-gremlin-ngql/">一文了解各大图数据库查询语言 | 操作入门篇</a></li>
<li><a href="https://nebula-graph.com.cn/posts/sql-vs-ngql-comparison/">文档解读 ｜ SQL vs. nGQL</a></li>
</ol>
<h2 id="图计算">图计算</h2>
<h3 id="图计算框架">图计算框架</h3>
<h4 id="graphx">GraphX</h4>
<p><a href="https://spark.apache.org/graphx/">GraphX</a> 是一个基于 <a href="https://spark.apache.org/">Spark</a> 大规模图计算框架。GraphX 通过引入一个包含带有属性的顶点和变的有向图对 Spark 的 RDD 进行了扩展。通过 subgraph、joinVertices 和 aggregateMessages 等算子实现了 PageRank、连通子图、LPA 等图算法。</p>
<h4 id="plato">Plato</h4>
<p><a href="https://github.com/Tencent/plato">Plato</a> 是由腾讯开源的高性能图计算框架。Plato 主要提供两方面的能力：离线图计算和图表示学习，目前支持的图算法如下：</p>
<table>
  <thead>
      <tr>
          <th>算法分类</th>
          <th>算法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>图特征</td>
          <td>树深度/宽度；节点数/边数/密度/节点度分布；N-阶度；HyperANF</td>
      </tr>
      <tr>
          <td>节点中心性指标</td>
          <td>KCore；Pagerank；Closeness；Betweenness</td>
      </tr>
      <tr>
          <td>连通图 &amp; 社团识别</td>
          <td>Connected-Component；LPA；HANP</td>
      </tr>
      <tr>
          <td>图表示学习</td>
          <td>Node2Vec-Randomwalk；Metapath-Randomwalk</td>
      </tr>
      <tr>
          <td>聚类/分圈算法</td>
          <td>FastUnfolding</td>
      </tr>
      <tr>
          <td>其他图相关算法</td>
          <td>BFS；共同类计算</td>
      </tr>
      <tr>
          <td>待开源算法</td>
          <td>Word2Vec；Line；GraphVite；GCN</td>
      </tr>
  </tbody>
</table>
<p>在计算性能上，Plato 与 Spark GraphX 在 PageRank 和 LPA 两个算法上的计算耗时与内存消耗对比如下图所示：</p>
<p><img src="/images/cn/2021-01-01-network-storage-and-computing/plaot-spark-graphx-benchmark.png" alt="Plato &amp; Spark GraphX Benchmark"></p>
<h4 id="graphscope">GraphScope</h4>
<p><a href="https://github.com/alibaba/GraphScope">GraphScope</a> 由有阿里巴巴开源的一个统一的分布式图计算平台。GraphScope 提供了一个一站式环境，可以通过用户友好的 Python 接口在集群内对图进行操作。GraphScope 利用一系列开源技术使得集群上的大规模图数据的多阶段处理变得简单，这些技术包括：用于分析的 <a href="https://github.com/alibaba/libgrape-lite">GRAPE</a>、用于查询的 <a href="https://github.com/alibaba/GraphScope/blob/master/interactive_engine">MaxGraph</a> 、用于图神经网络计算的 <a href="https://github.com/alibaba/graph-learn">Graph-Learn</a> 和用于提供高效内存数据交换的 <a href="https://github.com/alibaba/libvineyard">vineyard</a>。GraphScope 的整体架构如下图所示：</p>
<p><img src="/images/cn/2021-01-01-network-storage-and-computing/architecture-of-graphscope.png" alt="Architecture of GraphScope"></p>
<p>GraphScope Interactive Engine（GIE）是一个用于探索性分析大规模复杂图结构数据的引擎，它通过 Gremlin 提供高级别的图查询语言，同时提供自动并行执行功能。</p>
<p>GraphScope Analytical Engine（GAE）是一个基于 GRAPE <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 提供并行图算法的分析引擎。除了提供基础的内置算法以外，GAE 允许用户利用 Python 基于 PIE <sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 编程模型编写自定义算法，PIE 编程模型的运行方式如下图所示：</p>
<p><img src="/images/cn/2021-01-01-network-storage-and-computing/execution-model-in-gae.png" alt="Execution Model in GAE"></p>
<p>GraphScope 还提供以顶点为中心的 Pregel 模型 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>，用户可以使用 Pregel 模型来实现自定义算法。</p>
<p>GraphScope Learning Engine（GLE）是一个用于开发和训练大规模图神经网络的分布式框架。GLE 提供基于全量图（用于 GCN、GAT 等算法）和采样子图（用于 GraphSAGE，FastGCN、GraphSAINT 等算法）两种不同方式训练图模型。整体架构如下图所示：</p>
<p><img src="/images/cn/2021-01-01-network-storage-and-computing/gle.png" alt="GLE"></p>
<h4 id="galileo">Galileo</h4>
<p>Galileo 是由京东零售研发的图计算平台，提供离线和在线图计算和图数据服务能力。目前 Galileo 暂未开源，待开源后补充相关信息。</p>
<h3 id="图神经网络">图神经网络</h3>
<p>关于图神经网络内容，请参见之前的博客 <a href="/cn/2020/04/graph-embedding-and-gnn/">图嵌入 (Graph Embedding) 和图神经网络 (Graph Neural Network)</a>。</p>
<h2>:tada::tada::tada: Happe New Year! :tada::tada::tada:</h2>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Fan, W., Yu, W., Xu, J., Zhou, J., Luo, X., Yin, Q., &hellip; &amp; Xu, R. (2018). Parallelizing sequential graph computations. <em>ACM Transactions on Database Systems (TODS)</em>, 43(4), 1-39.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Malewicz, G., Austern, M. H., Bik, A. J., Dehnert, J. C., Horn, I., Leiser, N., &amp; Czajkowski, G. (2010, June). Pregel: a system for large-scale graph processing. In <em>Proceedings of the 2010 ACM SIGMOD International Conference on Management of data</em> (pp. 135-146).&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>网络算法 (Network Algorithms)</title><link>https://zeqiang.fun/cn/2020/12/network-algorithms/</link><pubDate>Sat, 12 Dec 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/12/network-algorithms/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%A4%8D%E6%9D%82%E7%BD%91%E7%BB%9C/">《复杂网络系列》</a>文章<br>
本文内容主要参考自：《网络科学引论》<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
</blockquote>
<h2 id="网络基础算法">网络基础算法</h2>
<h3 id="最短路径">最短路径</h3>
<p><strong>最短路径</strong>（shortest path）算法是寻找两个顶点之间的最短路径，寻找网络中最短路径的标准算法称为<strong>广度优先搜索</strong>（breadth-first search）。算法的基本思想如下图所示：</p>
<p><img src="/images/cn/2020-12-12-network-algorithms/bfs.png" alt=""></p>
<p>根据广度优先搜索的基本思想，不难证明距 <code>$s$</code> 最短距离为 <code>$d$</code> 的每个顶点都有一个到 <code>$s$</code> 的最短距离为 <code>$d - 1$</code> 的邻居顶点。一个简单的实现方式是，创建一个有 <code>$n$</code> 个元素的数组存储从源顶点 <code>$s$</code> 到其他所有顶点的距离，同时创建一个距离变量 <code>$d$</code> 来记录当前在搜索过程中所处的层数，算法的具体流程如下：</p>
<ol>
<li>遍历距离数组，查找到 <code>$s$</code> 的距离为 <code>$d$</code> 的所有顶点。</li>
<li>查找上述顶点的所有邻居顶点，如果同 <code>$s$</code> 的距离未知，则距离置为 <code>$d + 1$</code>。</li>
<li>如果距离未知的邻居顶点数量为零，则停止算法，否则将 <code>$d$</code> 的值加一并重复上述过程。</li>
</ol>
<p>这种方法在最坏的情况下时间复杂度为 <code>$O \left(m + n^2\right)$</code>，考虑多数网络的直径只随 <code>$\log n$</code> 增长，算法运行的时间复杂度为 <code>$O \left(m + n \log n\right)$</code>。</p>
<p>上述算法中步骤 1 是最耗时的部分，通过使用<strong>队列</strong>的数据结构我们可以避免每次都遍历列表来找到距离源顶点 <code>$s$</code> 距离为 <code>$d$</code> 的顶点。构造一个队列，一个指针指向下一个要读取的元素，另一个指针指向要填充的空位，这样距离为 <code>$d + 1$</code> 的顶点就会紧跟在距离为 <code>$d$</code> 的顶点后面，队列如下图所示：</p>
<p><img src="/images/cn/2020-12-12-network-algorithms/bfs-queue.png" alt=""></p>
<p>通过队列可以将算法的时间复杂度降至 <code>$O \left(m + n\right)$</code>，对于 <code>$m \propto n$</code> 的稀疏网络而言，<code>$O \left(m + n\right)$</code> 相当于 <code>$O \left(n\right)$</code>，所以算法的时间复杂度同顶点数量成正比。</p>
<p>通过对算法进行进一步修改则可以得到源顶点 <code>$s$</code> 到其他任何顶点的最短路径。方法是在原来的网络上构建一个新的有向网络，该网络代表最短路径，称为<strong>最短路径树</strong>（shortest path tree），通常情况下，该网络是一个有向非循环网络，而不是树。</p>
<p>对于加权网络，利用广度优先搜索无法找到最短路径，这里需要用到 Dijkstra 算法 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 进行求解。算法将图中的顶点分成两组 <code>$S$</code> 和 <code>$U$</code>，整个算法过程如下：</p>
<ol>
<li>初始状态，<code>$S$</code> 仅包含源顶点，即 <code>$S = \left\{v\right\}$</code>，<code>$U$</code> 包含其余顶点。如果 <code>$v$</code> 与 <code>$U$</code> 中的顶点 <code>$u$</code> 为邻居，则距离为边的权重，否则为无穷大。</li>
<li>从 <code>$U$</code> 中选择一个距离 <code>$v$</code> 最短的顶点 <code>$k$</code>，并把 <code>$k$</code> 加入到 <code>$S$</code> 中。</li>
<li>若从源点 <code>$v$</code> 经过顶点 <code>$k$</code> 到达 <code>$u$</code> 的距离比之前 <code>$v$</code> 到 <code>$u$</code> 的距离短，则将距离修改为这个更短的距离。</li>
<li>重复步骤 2 和 3，直至所有顶点都包含在 <code>$S$</code> 中。</li>
</ol>
<p>整个算法过程的可视化效果如下图所示：</p>
<p><img src="/images/cn/2020-12-12-network-algorithms/dijkstras-progress.gif" alt=""></p>
<p>Dijkstra 算法的时间复杂度为 <code>$O \left(m + n^2\right)$</code>，通过二叉堆的数据结构可以将时间复杂度优化至 <code>$O \left(\left(m + n\right) \log n\right)$</code>。</p>
<p>Dijkstra 算法虽然能够处理加权网络，但不能处理存在负权重的网络，需要利用 Floyd-Warshall 算法 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 进行求解。更多 Floyd-Warshall 算法的细节请参见之前的博客<a href="/cn/2018/11/computational-complexity-and-dynamic-programming/">计算复杂性 (Computational Complexity) 与动态规划 (Dynamic Programming)</a>。</p>
<h3 id="最大流和最小割">最大流和最小割</h3>
<p>对于连接给定顶点 <code>$s$</code> 和 <code>$t$</code> 的两条路径，若没有共享边，则这两条路径是<strong>边独立</strong>的；若除 <code>$s$</code> 和 <code>$t$</code> 外不共享任何其他顶点，则这两条路径是<strong>顶点独立</strong>的。顶点之间的<strong>边连通度</strong>和<strong>顶点连通度</strong>分别是顶点之间边独立路径数和顶点独立路径数。连通度是度量顶点之间连通鲁棒性的简单参数。假设一个网络是一个管线网络，其中每个管线的容量均为单位流量，那么边连通度等于从 <code>$s$</code> 流向 <code>$t$</code> 的<strong>最大流</strong>。</p>
<p><strong>增广路径算法</strong>（Ford-Fulkerson Algorithm，FFA）是计算最大流最简单的算法。基本思想是：首先利用广度优先搜索算法找到一条从源 <code>$s$</code> 到目标 <code>$t$</code> 的路径。该步骤“消耗”了网络中的一些边，将这些边的容量填充满后，它们不再承载更多流量。之后在剩余边中找到从 <code>$s$</code> 到 <code>$t$</code> 的另一条路径，重复该过程直到找不到更多的路径为止。</p>
<p>但这还不是一个有效的算法，如下图中的 (a) 所示，如果在 <code>$s$</code> 和 <code>$t$</code> 之间运用广度优先搜索，可以发现黑色标记的路径。一旦这些边的容量被填充满，就不能在剩余边中找到从 <code>$s$</code> 到 <code>$t$</code> 的更多路径，但很明显，从 <code>$s$</code> 到 <code>$t$</code> 有两条边独立路径（上下各一条）。</p>
<p><img src="/images/cn/2020-12-12-network-algorithms/augmenting-path-algorithm.png" alt=""></p>
<p>解决该问题的一个简单修正方法是允许网络流量在一条边中能够同时在两个方向流动。更一般地，因为一条边容许承载的最大流是在任意方向的单位流量，那么一条边可以有多个单位流量，只要保证他们能够相互抵消，并且最终每条边承载不超过一个单位流量。</p>
<p>增广路径算法的实现利用了<strong>剩余图</strong>（residual graph），这是一个有向网络，该网络中的有向边连接原网络中相应的顶点对，并在指定方向承载一个或多个单位流量。例如上图中 (c) 和 (d) 就是对应 (a) 和 (b) 的流量状态的剩余图。算法的正确性在这里就不过多展开说明，该算法在计算两个顶点之间的最大流的平均时间复杂度为 <code>$O \left(\left(m + n\right) m / n\right)$</code>。</p>
<p>在图论中，去掉其中所有边使一张网络不再连通的边集为图的<strong>割</strong>，一张图上最小的割为<strong>最小割</strong>。通过对增广路径算法进行改动即可以寻找到边独立路径、最小边割集和顶点独立路径。</p>
<h2 id="图划分和社团发现">图划分和社团发现</h2>
<p><strong>图划分</strong>（graph partitioning）和<strong>社团发现</strong>（community detection）都是指根据网络中的边的连接模式，把网络顶点划分成群组、簇或社团。将网络顶点划分成群组后最常见的属性是，同一群组内部的顶点之间通过边紧密连接，而不同群组之间只有少数边。</p>
<h3 id="图划分">图划分</h3>
<p>最简单的图划分问题是把网络划分成两部分，有时也称其为<strong>图对分</strong>（graph bisection）。图对分是把一个网络中的顶点划分成为两个指定规模的非重叠群组，使得不同群组之间相互连接的边数最小。群组之间的边数称为<strong>割集规模</strong>（cut size）。 利用穷举搜索解决该问题是极为耗时的，通过启发式算法我们可以找到较好的网络划分。</p>
<h4 id="kernighan-lin-算法">Kernighan-Lin 算法</h4>
<p>Kernighan-Lin 算法是由 Brian Kernighan 和 Shen Lin 在 1970 年提出的 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>，是图对分问题中最简单、最知名的启发式算法之一，如下图所示。</p>
<p><img src="/images/cn/2020-12-12-network-algorithms/kernighan%E2%80%93lin-algorithm.png" alt=""></p>
<p>先以任意方式将网络顶点按指定规模划分成两个群组，对于任何由分属不同群组的顶点 <code>$i$</code> 和顶点 <code>$j$</code> 组成的顶点对 <code>$\left(i, j\right)$</code>，交换 <code>$i$</code> 和 <code>$j$</code> 的位置，并计算交换前后两个群组之间割集规模的变化量。在所有顶点对中找到使割集规模减小最多的顶点对，或者若没有使割集规模减小的顶点对，则找到使割集规模增加最小的顶点对，交换这两个顶点。重复这个过程，同时保证网络中的每个顶点只能移动一次。</p>
<p>继续算法，每一步都交换最大程度减少或最小程度增加群组之间边数的顶点对，直到没有可以变换的顶点对，此时本轮算法停止。在完成所有交换后，检查网络在此过程中经过的每一个状态，然后选择割集规模最小的状态。最后，重复执行上述整个过程，每次始于上次发现的最优网络划分，直到割集规模不在出现改善。</p>
<p>Kernighan-Lin 算法的主要缺点是运算速度缓慢，采用一些技巧来改善算法也只能使时间复杂度降至 <code>$O \left(n^3\right)$</code>，因此该算法仅适用于有几百或几千个顶点的网络，而不适用于更大规模的网络。</p>
<h4 id="谱划分">谱划分</h4>
<blockquote>
<p>请先了解<a href="#%E9%99%84%E5%BD%95">附录</a>中的拉普拉斯算子和拉普拉斯矩阵等相关概念。</p>
</blockquote>
<p>考虑具有 <code>$n$</code> 个顶点 <code>$m$</code> 条边的网络，将其划分为两个群组，称为群组 1 和群组 2。可以把该划分的割集规模，也就是两个群组之间的边数表示为：</p>
<p><code>$$ \label{eq:r_1} R = \dfrac{1}{2} \sum_{i, j \text{ 属于不同群组}} A_{ij} $$</code></p>
<p>对于每个网络划分，定义有参数 <code>$s_i$</code> 组成的集合，集合中每个元素对应于一个顶点 <code>$i$</code>，则有：</p>
<p><code>$$ s_i = \left\{\begin{array}{ll} +1 &amp; \text{顶点 } i \text{ 在群组 1 中} \\ -1 &amp; \text{顶点 } i \text{ 在群组 2 中} \end{array}\right. $$</code></p>
<p>那么：</p>
<p><code>$$ \dfrac{1}{2} \left(1 - s_i s_j\right) = \left\{\begin{array}{ll} 1 &amp; \text{顶点 } i \text{ 和 } j \text{ 在不同的群组中} \\ 0 &amp; \text{顶点 } i \text{ 和 } j \text{ 在相同的群组中} \end{array}\right. $$</code></p>
<p>则式 \ref{eq:r_1} 可以改写为：</p>
<p><code>$$ \begin{aligned} R &amp; = \dfrac{1}{4} \sum_{ij} A_{ij} \left(1 - s_i s_j\right) \\ &amp; = \dfrac{1}{4} \left(k_i \delta_{ij} - A_{ij}\right) s_i s_j \\ &amp; = \dfrac{1}{4} \sum_{ij} L_{ij} s_i s_j \end{aligned} $$</code></p>
<p>其中，<code>$\delta_{ij}$</code> 是克罗内克函数，<code>$L_{ij}$</code> 是图拉普拉斯矩阵的第 <code>$ij$</code> 个元素。写成矩阵的形式有：</p>
<p><code>$$ R = \dfrac{1}{4} \mathbf{s}^{\top} \mathbf{L} \mathbf{s} $$</code></p>
<p>由于每个 <code>$s_i$</code> 的取值只能是 <code>$\left\{+1, -1\right\}$</code>，所以在给定 <code>$\mathbf{L}$</code> 时求解 <code>$\mathbf{s}$</code> 使其割集规模最小时并不容易。具体求解方法的推导在此不再展开说明，最终谱划分算法的过程如下所示：</p>
<ol>
<li>计算图拉普拉斯矩阵的第二小特征值 <code>$\lambda_2$</code>，称为网络的<strong>代数连通度</strong>（algebraic connectivity），及其对应的特征向量 <code>$\mathbf{v}_2$</code>。</li>
<li>按从大到小的顺序对特征向量的元素进行排序。</li>
<li>把前 <code>$n_1$</code> 个最大元素对应的顶点放入群组 1，其余放入群组 2，计算割集规模。</li>
<li>把前 <code>$n_1$</code> 个最小（注意：中文译本中有错误）元素对应的顶点放入群组 2，其余放入群组 1，并重新计算割集规模。</li>
<li>在两种网络划分中，选择割集规模较小的那个划分。</li>
</ol>
<p>谱划分方法在稀疏网络上的时间复杂度为 <code>$O \left(n^2\right)$</code>，这比 Kernighan-Lin 算法时间复杂度少了一个因子 <code>$n$</code>，从而使该算法能应用于更大规模的网络。</p>
<h3 id="社团发现">社团发现</h3>
<p><strong>社团发现</strong>（社区发现，社群发现，Community Detection）的基本目的与图划分类似，即把网络分成几个节点点群组，并使节点群组之间的连接较少。主要的差别就是群组的数量和规模是不确定的。社团发现的算法分类和具体实现很多，本文仅介绍几个常用的算法，更多方法及其细节请参见如下开放资源：</p>
<ol>
<li>Community Detection in Graphs <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup></li>
<li>Deep Learning for Community Detection: Progress, Challenges and Opportunities <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup></li>
<li>复杂网络社团发现算法研究新进展 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup></li>
<li><a href="https://github.com/benedekrozemberczki/awesome-community-detection">benedekrozemberczki/awesome-community-detection</a></li>
</ol>
<h4 id="fast-unfolding-louvain">Fast Unfolding (Louvain)</h4>
<p><strong>Fast Unfolding (Louvain)</strong> <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> 是一种基于模块度的社团发现算法，通过模块度来衡量一个社团的紧密程度。算法包含两个阶段：</p>
<ol>
<li>历遍网络中所有的节点，通过比较将节点给每个邻居社团带来的模块度变化，将这个节点加入到使模块度增加最大的社团中。</li>
<li>对于步骤 1 的结果，将属于同一个社团的节点合并成为一个大的节点，进而重型构造网络。新的节点之间边的权重是所包含的之前所有节点之间相连的边权重之和，然后重复步骤 1。</li>
</ol>
<p>算法的两个步骤如下图所示：</p>
<p><img src="/images/cn/2020-12-12-network-algorithms/louvain.png" alt=""></p>
<h4 id="label-propagation-algorithm-lpa">Label Propagation Algorithm (LPA)</h4>
<p><strong>标签传播算法</strong>（Label Propagation Algorithm，LPA）是一种基于半监督学习的社团发现算法。对于每个节点都有对应的标签（即节点所隶属的社团），在算法迭代过程中，节点根据其邻居节点更新自身的标签。更新的规则是选择邻居节点中最多的标签作为自身的标签。</p>
<p>标签传播的过程中，节点的标签更新方式分为<strong>同步更新</strong>和<strong>异步更新</strong>两种方式。同步更新是指对于节点 <code>$x$</code>，在第 <code>$t$</code> 步时，根据其所有邻居节点在 <code>$t - 1$</code> 步时的标签对其进行更新，即：</p>
<p><code>$$ C_{x}(t)=f\left(C_{x_{1}}(t-1), C_{x_{2}}(t-1), \cdots, C_{x_{k}}(t-1)\right) $$</code></p>
<p>同步更新对于一个二分或者近似二分的网络来说可能会出现标签震荡的现象。对于异步更新方式，更新公式为：</p>
<p><code>$$ C_{x}(t)=f\left(C_{x_{i 1}}(t), \cdots, C_{x_{i m}}(t), C_{x_{i(m+1)}}(t-1), \cdots, C_{x_{i k}}(t-1)\right) $$</code></p>
<p>其中，邻居节点 <code>$x_{i1}, \cdots, x_{im}$</code> 的标签在第 <code>$t$</code> 步时已经更新过，而 <code>$x_{i(m+1)}, \cdots, x_{ik}$</code> 的标签还未更新。</p>
<h2 id="附录">附录</h2>
<p><strong>拉普拉斯算子</strong>（Laplace operator，Laplacian）是由欧式空间中的一个函数的梯度的散度给出的微分算子，通常写作 <code>$\Delta$</code>，<code>$\nabla^2$</code> 或 <code>$\nabla \cdot \nabla$</code>。</p>
<p><strong>梯度</strong>（gradient）是对多元导数的概括，函数沿着梯度的方向变化最快，变化率则为梯度的模。假设二元函数 <code>$f \left(x, y\right)$</code> 在区域 <code>$G$</code> 内具有一阶连续偏导数，点 <code>$P \left(x, y\right) \in G$</code>，则称向量：</p>
<p><code>$$ \nabla f = \left(\dfrac{\partial f}{\partial x}, \dfrac{\partial f}{\partial y} \right) = \dfrac{\partial f}{\partial x} \mathbf{i} + \dfrac{\partial f}{\partial y} \mathbf{j} $$</code></p>
<p>为函数 <code>$f$</code> 在点 <code>$P$</code> 处的梯度，其中 <code>$\mathbf{i}$</code> 和 <code>$\mathbf{j}$</code> 为单位向量，分别指向 <code>$x$</code> 和 <code>$y$</code> 坐标方向。</p>
<p><strong>散度</strong>（divergence）将向量空间上的一个向量场对应到一个标量场上，记为 <code>$\nabla \cdot$</code>。散度的意义是场的有源性，当 <code>$\nabla \cdot F &gt; 0$</code> 时，表示该点是发源点；当 <code>$\nabla \cdot F &lt; 0$</code> 时，表示该点是汇聚点；当 <code>$\nabla \cdot F = 0$</code> 时，表示该点无源，如下图所示。</p>
<p><img src="/images/cn/2020-12-12-network-algorithms/divergence.png" alt=""></p>
<p>拉普拉斯离散化后即为<strong>拉普拉斯矩阵</strong>（laplacian matrix），也称为<strong>调和矩阵</strong>（harmonic matrix）。离散化的拉普拉斯算子形式如下：</p>
<p><code>$$ \begin{aligned} \Delta f &amp; = \dfrac{\partial^2 f}{\partial x^2} + \dfrac{\partial^2 f}{\partial y^2} \\ &amp; = f \left(x + 1, y\right) + f \left(x - 1, y\right) - 2 f \left(x, y\right) + f \left(x, y + 1\right) + f \left(x, y - 1\right) - 2 f \left(x, y\right) \\ &amp; = f \left(x + 1, y\right) + f \left(x - 1, y\right) + f \left(x, y + 1\right) + f \left(x, y - 1\right) - 4 f \left(x, y\right) \end{aligned} $$</code></p>
<p>从上述离散化后的拉普拉斯算子形式可以看出，拉普拉斯矩阵表示的是对矩阵进行微小扰动后获得的收益。</p>
<p>设图 <code>$G$</code> 有 <code>$n$</code> 个节点，节点的邻域为 <code>$N$</code>，图上的函数 <code>$f = \left(f_1, f_2, \cdots, f_n\right)$</code>，其中 <code>$f_i$</code> 表示节点 <code>$i$</code> 处的函数值。对 <code>$i$</code> 进行扰动，其可能变为邻域内的任意一个节点 <code>$j \in N_i$</code>：</p>
<p><code>$$ \Delta f_{i}=\sum_{j \in N_{i}}\left(f_{i}-f_{j}\right) $$</code></p>
<p>设每一条边 <code>$e_{ij}$</code> 的权重为 <code>$w_{ij}$</code>，<code>$w_{ij} = 0$</code> 表示节点 <code>$i$</code> 和节点 <code>$j$</code> 不相邻，则有：</p>
<p><code>$$ \begin{aligned} \Delta f_i &amp; = \sum_{j \in N} w_{ij} \left(f_i - f_j\right) \\ &amp; = \sum_{j \in N} w_{ij} f_i - \sum_{j \in N} w_{ij} f_i \\ &amp; = d_i f_i - W_{i:} f  \end{aligned} $$</code></p>
<p>对于所有节点有：</p>
<p><code>$$ \begin{aligned} \Delta f &amp; = \left(\begin{array}{c} \Delta f_{1} \\ \vdots \\ \Delta f_{N} \end{array}\right)=\left(\begin{array}{c} d_{1} f_{1}-W_{1:} f \\ \vdots \\ d_{N} f_{N}-W_{N:} f \end{array}\right) \\ &amp; = \left(\begin{array}{ccc} d_{1} &amp; \cdots &amp; 0 \\ \vdots &amp; \ddots &amp; \vdots \\ 0 &amp; \cdots &amp; d_{N} \end{array}\right) f-\left(\begin{array}{c} W_{1:} \\ \vdots \\ W_{N:} \end{array}\right) f \\ &amp; = diag \left(d_i\right) f - W f \\ &amp; = \left(D - W\right) f \\ &amp; = L f \end{aligned} $$</code></p>
<p>令图 <code>$G$</code> 的邻接矩阵为 <code>$W$</code>，度矩阵为 <code>$D$</code>，从上式可知拉普拉斯矩阵 <code>$L = D - W$</code>，其中：</p>
<p><code>$$ L_{ij} = \left\{\begin{array}{ll} \deg \left(v_i\right) &amp; \text{如果 } i = j \\ -1 &amp; \text{如果 } i \neq j \text{ 且 } v_i \text{ 与 } v_j \text{ 相邻} \\ 0 &amp; \text{其他情况} \end{array}\right. $$</code></p>
<p>以下面的图为例：</p>
<p><img src="/images/cn/2020-12-12-network-algorithms/laplacian-matrix-demo-graph.png" alt=""></p>
<p>邻接矩阵为：</p>
<p><code>$$ \left(\begin{array}{llllll} 0 &amp; 1 &amp; 0 &amp; 0 &amp; 1 &amp; 0 \\ 1 &amp; 0 &amp; 1 &amp; 0 &amp; 1 &amp; 0 \\ 0 &amp; 1 &amp; 0 &amp; 1 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 1 &amp; 0 &amp; 1 &amp; 1 \\ 1 &amp; 1 &amp; 0 &amp; 1 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 1 &amp; 0 &amp; 0 \end{array}\right) $$</code></p>
<p>度矩阵为：</p>
<p><code>$$ \left(\begin{array}{cccccc} 2 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; 3 &amp; 0 &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 2 &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 3 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 0 &amp; 3 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 1 \end{array}\right) $$</code></p>
<p>拉普拉斯矩阵为：</p>
<p><code>$$ \left(\begin{array}{rrrrrr} 2 &amp; -1 &amp; 0 &amp; 0 &amp; -1 &amp; 0 \\ -1 &amp; 3 &amp; -1 &amp; 0 &amp; -1 &amp; 0 \\ 0 &amp; -1 &amp; 2 &amp; -1 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; -1 &amp; 3 &amp; -1 &amp; -1 \\ -1 &amp; -1 &amp; 0 &amp; -1 &amp; 3 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 1 &amp; 0 &amp; 1 \end{array}\right) $$</code></p>
<h2 id="开放资源">开放资源</h2>
<h3 id="常用网络算法包">常用网络算法包</h3>
<table>
  <thead>
      <tr>
          <th>名称</th>
          <th>语言</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://networkx.org/">NetworkX</a></td>
          <td><i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://graph-tool.skewed.de/">graph-tool</a></td>
          <td><i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://snap.stanford.edu/index.html">SNAP</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/networkit/networkit">NetworKit</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://igraph.org/">igraph</a></td>
          <td><i class="icon icon-c">C</i>, <i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i>, <i class="icon icon-r">R</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/JuliaGraphs/LightGraphs.jl">lightgraphs</a></td>
          <td><i class="icon icon-julia">Julia</i></td>
      </tr>
  </tbody>
</table>
<p>不同扩展包之间的性能比较如下表所示 <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup>：</p>
<table>
  <thead>
      <tr>
          <th>数据集</th>
          <th>算法</th>
          <th>graph-tool</th>
          <th>igraph</th>
          <th>LightGraphs</th>
          <th>NetworKit</th>
          <th>NetworkX</th>
          <th>SNAP</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Amazon</td>
          <td>CC</td>
          <td>0.08</td>
          <td>0.22</td>
          <td>0.07</td>
          <td>0.09</td>
          <td>2.22</td>
          <td>0.31</td>
      </tr>
      <tr>
          <td>Amazon</td>
          <td>k-core</td>
          <td>0.08</td>
          <td>0.15</td>
          <td>0.04</td>
          <td>0.15</td>
          <td>3.63</td>
          <td>0.37</td>
      </tr>
      <tr>
          <td>Amazon</td>
          <td>loading</td>
          <td>2.61</td>
          <td>0.57</td>
          <td>4.66</td>
          <td>0.98</td>
          <td>4.72</td>
          <td>1.61</td>
      </tr>
      <tr>
          <td>Amazon</td>
          <td>page rank</td>
          <td>0.04</td>
          <td>0.57</td>
          <td>0.02</td>
          <td>0.02</td>
          <td>8.59</td>
          <td>0.58</td>
      </tr>
      <tr>
          <td>Amazon</td>
          <td>shortest path</td>
          <td>0.03</td>
          <td>0.05</td>
          <td>0.01</td>
          <td>0.04</td>
          <td>1.37</td>
          <td>0.12</td>
      </tr>
      <tr>
          <td>Google</td>
          <td>CC</td>
          <td>0.28</td>
          <td>1.38</td>
          <td>0.29</td>
          <td>0.37</td>
          <td>7.77</td>
          <td>1.56</td>
      </tr>
      <tr>
          <td>Google</td>
          <td>k-core</td>
          <td>0.39</td>
          <td>0.92</td>
          <td>0.16</td>
          <td>0.83</td>
          <td>42.6</td>
          <td>1.31</td>
      </tr>
      <tr>
          <td>Google</td>
          <td>loading</td>
          <td>11.02</td>
          <td>3.87</td>
          <td>16.75</td>
          <td>4.38</td>
          <td>19.24</td>
          <td>7.56</td>
      </tr>
      <tr>
          <td>Google</td>
          <td>page rank</td>
          <td>0.36</td>
          <td>2.42</td>
          <td>0.06</td>
          <td>0.1</td>
          <td>33.5</td>
          <td>2.31</td>
      </tr>
      <tr>
          <td>Google</td>
          <td>shortest path</td>
          <td>0.08</td>
          <td>0.41</td>
          <td>0.01</td>
          <td>0.14</td>
          <td>3.41</td>
          <td>0.26</td>
      </tr>
      <tr>
          <td>Pokec</td>
          <td>CC</td>
          <td>1.83</td>
          <td>3.96</td>
          <td>1.5</td>
          <td>1.75</td>
          <td>61.74</td>
          <td>9.75</td>
      </tr>
      <tr>
          <td>Pokec</td>
          <td>k-core</td>
          <td>3.6</td>
          <td>5.99</td>
          <td>0.95</td>
          <td>5.05</td>
          <td>296.26</td>
          <td>6.91</td>
      </tr>
      <tr>
          <td>Pokec</td>
          <td>loading</td>
          <td>71.46</td>
          <td>25.75</td>
          <td>170.63</td>
          <td>26.77</td>
          <td>140.19</td>
          <td>52.73</td>
      </tr>
      <tr>
          <td>Pokec</td>
          <td>page rank</td>
          <td>1.1</td>
          <td>23.39</td>
          <td>0.21</td>
          <td>0.24</td>
          <td>239.75</td>
          <td>8.62</td>
      </tr>
      <tr>
          <td>Pokec</td>
          <td>shortest path</td>
          <td>0.48</td>
          <td>0.6</td>
          <td>0.05</td>
          <td>0.56</td>
          <td>5.65</td>
          <td>2.3</td>
      </tr>
  </tbody>
</table>
<h3 id="常用网络可视化软件">常用网络可视化软件</h3>
<table>
  <thead>
      <tr>
          <th>软件</th>
          <th>平台</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://cytoscape.org/">Cytoscape</a></td>
          <td><i class="icon icon-windows">Windows</i>, <i class="icon icon-macos">macOS</i>, <i class="icon icon-linux">Linux</i></td>
      </tr>
      <tr>
          <td><a href="https://gephi.org/">Gephi</a></td>
          <td><i class="icon icon-windows">Windows</i>, <i class="icon icon-macos">macOS</i>, <i class="icon icon-linux">Linux</i></td>
      </tr>
      <tr>
          <td><a href="https://tulip.labri.fr/TulipDrupal/">Tulip</a></td>
          <td><i class="icon icon-windows">Windows</i>, <i class="icon icon-macos">macOS</i>, <i class="icon icon-linux">Linux</i></td>
      </tr>
      <tr>
          <td><a href="http://mrvar.fdv.uni-lj.si/pajek/">Pajek</a></td>
          <td><i class="icon icon-windows">Windows</i></td>
      </tr>
  </tbody>
</table>
<p>不同可视化软件之间的比较如下表所示 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup>：</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Cytoscape</th>
          <th>Tulip</th>
          <th>Gephi</th>
          <th>Pajek</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scalability</td>
          <td>⭑⭑</td>
          <td>⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑⭑⭑⭑</td>
      </tr>
      <tr>
          <td>User friendliness</td>
          <td>⭑⭑</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑</td>
      </tr>
      <tr>
          <td>Visual styles</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑</td>
      </tr>
      <tr>
          <td>Edge bundling</td>
          <td>⭑⭑⭑</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑⭑</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Relevance to biology</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑</td>
      </tr>
      <tr>
          <td>Memory efficiency</td>
          <td>⭑</td>
          <td>⭑⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑⭑⭑⭑</td>
      </tr>
      <tr>
          <td>Clustering</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑</td>
          <td>⭑⭑</td>
      </tr>
      <tr>
          <td>Manual node/edge editing</td>
          <td>⭑⭑⭑</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑</td>
      </tr>
      <tr>
          <td>Layouts</td>
          <td>⭑⭑⭑</td>
          <td>⭑⭑</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑</td>
      </tr>
      <tr>
          <td>Network profiling</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑</td>
      </tr>
      <tr>
          <td>File formats</td>
          <td>⭑⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑</td>
      </tr>
      <tr>
          <td>Plugins</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑</td>
      </tr>
      <tr>
          <td>Stability</td>
          <td>⭑⭑⭑</td>
          <td>⭑</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑⭑⭑</td>
      </tr>
      <tr>
          <td>Speed</td>
          <td>⭑⭑</td>
          <td>⭑</td>
          <td>⭑⭑⭑</td>
          <td>⭑⭑⭑⭑</td>
      </tr>
      <tr>
          <td>Documentation</td>
          <td>⭑⭑⭑⭑</td>
          <td>⭑</td>
          <td>⭑⭑</td>
          <td>⭑⭑⭑</td>
      </tr>
  </tbody>
</table>
<p>其中，⭑ 表示较弱、⭑⭑ 表示中等、⭑⭑⭑ 表示较好、⭑⭑⭑⭑ 表示优秀。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Newman, M. E. J. (2014) <em>网络科学引论</em>. 电子工业出版社.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://zh.wikipedia.org/wiki/%E6%88%B4%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95">https://zh.wikipedia.org/wiki/戴克斯特拉算法</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://zh.wikipedia.org/zh-hans/Floyd-Warshall%E7%AE%97%E6%B3%95">https://zh.wikipedia.org/zh-hans/Floyd-Warshall算法</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Kernighan, B. W., &amp; Lin, S. (1970). An efficient heuristic procedure for partitioning graphs. <em>The Bell system technical journal</em>, 49(2), 291-307.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Fortunato, S. (2010). Community detection in graphs. <em>Physics reports</em>, 486(3-5), 75-174.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Liu, F., Xue, S., Wu, J., Zhou, C., Hu, W., Paris, C., &hellip; &amp; Yu, P. S. (2020). Deep Learning for Community Detection: Progress, Challenges and Opportunities. <em>arXiv preprint arXiv:2005.08225</em>.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>骆志刚, 丁凡, 蒋晓舟, &amp; 石金龙. (2011). 复杂网络社团发现算法研究新进展. <em>国防科技大学学报</em>, (1), 12.&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Blondel, V. D., Guillaume, J. L., Lambiotte, R., &amp; Lefebvre, E. (2008). Fast unfolding of communities in large networks. <em>Journal of statistical mechanics: theory and experiment</em>, 2008(10), P10008.&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p><a href="https://www.timlrx.com/2020/05/10/benchmark-of-popular-graph-network-packages-v2/">Benchmark of popular graph/network packages v2</a>&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Pavlopoulos, G. A., Paez-Espino, D., Kyrpides, N. C., &amp; Iliopoulos, I. (2017). Empirical comparison of visualization tools for larger-scale network analysis. <em>Advances in bioinformatics</em>, 2017.&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>真实世界网络结构 (Structure of Real-World Network)</title><link>https://zeqiang.fun/cn/2020/11/structure-of-real-world-network/</link><pubDate>Sat, 28 Nov 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/11/structure-of-real-world-network/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%A4%8D%E6%9D%82%E7%BD%91%E7%BB%9C/">《复杂网络系列》</a>文章<br>
本文内容主要参考自：《网络科学引论》<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
</blockquote>
<h2 id="分支">分支</h2>
<p>在无向网络中，一个典型的现象是很多网络都有一个分支，该分支占据了网络的绝大部分，而剩余部分则被划分为大量的小分支，这些小分支之间彼此并不相连。如下图所示：</p>
<p><img src="/images/cn/2020-11-28-structure-of-real-world-network/components-in-an-undirected-network.png" alt=""></p>
<p>一个网络通常不能有两个或更多占据网络大部分的大分支。如果将一个 <code>$n$</code> 个顶点的网络分解为两个分支，每个分支约为 <code>$\dfrac{1}{2} n$</code> 个顶点，则两个分支的顶点之间会有 <code>$\dfrac{1}{4} n^2$</code> 个顶点对，这些顶点对有可能一个顶点在一个大分支中，而另一个顶点在另外一个大分支中。如果在任何一个顶点对之间有一条边，那么这两个分支就会合并为一个分支。</p>
<p>有向图中分支分为两种：<strong>弱连通分支</strong>和<strong>强连通分支</strong>。弱连通分支的定义与无向网络的分支定义类似，强连通分支是指网络顶点的一个最大子集，该子集中的顶点能够通过有向路径到达其余所有顶点，同时也能够通过有向路径从其余所有顶点到达。</p>
<p>每个连通分支拥有<strong>外向分支</strong>（即从强连通分支中的任意顶点出发，沿着有向路径能够到达的所有顶点的集合）和<strong>内向分支</strong>（即沿着有向路径能够到达强连通分支的所有顶点的集合）。利用**“领结”图**可以很好地刻画有向网络的总体情况，万维网的“领结”图如下所示：</p>
<p><img src="/images/cn/2020-11-28-structure-of-real-world-network/components-in-a-directed-network.png" alt=""></p>
<h2 id="小世界效应">小世界效应</h2>
<p><strong>小世界效应</strong>（small-world effect）是指对于大多数网络而言，网络顶点之间的测地距离都惊人的小，例如：<a href="https://zh.wikipedia.org/wiki/%E5%85%AD%E5%BA%A6%E5%88%86%E9%9A%94%E7%90%86%E8%AE%BA">六度分隔理论</a>。网络的数学模型显示出网络测地路径长度的数量级通常与网络定点数 <code>$n$</code> 成对数关系 ，即 <code>$\log n$</code>。</p>
<h2 id="度分布">度分布</h2>
<p>顶点的度是指连接到它的边的数量。<strong>度分布</strong>（degree distribution）<code>$p_k$</code> 是指网络中节点度的概率分布，也可以理解为从网络中随机选择一个顶点，其度为 <code>$k$</code> 的概率。<strong>度序列</strong>（degree sequence）是指所有顶点度的集合。</p>
<p>根据度 <code>$k$</code> 描述出大型网络的度分布有着非常重要的作用，下图给出了 Internet 的度分布：</p>
<p><img src="/images/cn/2020-11-28-structure-of-real-world-network/degree-distribution-of-the-internet.png" alt=""></p>
<p>现实世界中，几乎所有网络的度分布都有类似的由度较大的核心顶点构成的尾部，统计上称为<strong>右偏</strong>（right-skewed）的。</p>
<h2 id="幂律和无标度网络">幂律和无标度网络</h2>
<p>以 Internet 为例，下图给出了度分布的一个有趣特征，下图使用了对数标度重新绘制了上图的直方图：</p>
<p><img src="/images/cn/2020-11-28-structure-of-real-world-network/power-law-degree-distribution-of-the-internet.png" alt=""></p>
<p>如上图所示，对数处理后，分布大致遵循一条直线。度分布 <code>$p_k$</code> 的对数与度 <code>$k$</code> 的对数之间具有线性函数关系：</p>
<p><code>$$ \ln p_k = - \alpha \ln k + c $$</code></p>
<p>对两侧同时做指数运算，有：</p>
<p><code>$$ p_k = C k^{- \alpha} $$</code></p>
<p>其中，<code>$C = e^c$</code> 是一个常数。这种形式的分布，即按照 <code>$k$</code> 的幂变化，称为<strong>幂律</strong>（power law）。在不同类型的网络中，幂律度分布是普遍存在的，常数 <code>$\alpha$</code> 是幂律的指数，该值的典型取值区间为 <code>$2 \leq \alpha \leq 3$</code>。通常，度分布并非在整个区间都遵循幂律分布，当 <code>$k$</code> 较小时，度分布并不是单调的。具有幂律度分布的网络也称为<strong>无标度网络</strong>（scale-free network）。</p>
<p>观察幂律分布的另外一种方式是构建<strong>累积分布函数</strong>，定义如下：</p>
<p><code>$$ P_k = \sum_{k' = k}^{\infty} p_{k'} $$</code></p>
<p>假设度分布 <code>$p_k$</code> 在尾部服从幂律，确切地讲，对于某个 <code>$k_{\min}$</code>，当 <code>$k \geq k_{\min}$</code> 时有 <code>$p_k = C k^{- \alpha}$</code>，则对于 <code>$k \geq k_{\min}$</code>，有：</p>
<p><code>$$ P_{k}=C \sum_{k^{\prime}=k}^{\infty} k^{\prime-\alpha} \simeq C \int_{k}^{\infty} k^{\prime-\alpha} \mathrm{d} k^{\prime}=\frac{C}{\alpha-1} k^{-(\alpha-1)} $$</code></p>
<p>这里通过积分来近似求和是合理的，因为当 <code>$k$</code> 值较大时，幂律函数的变化率较小。所以，如果度分布 <code>$p_k$</code> 服从幂律，那么 <code>$p_k$</code> 的累积分布函数也服从幂律。</p>
<h2 id="聚类系数">聚类系数</h2>
<p>聚类系数是度量某个顶点的两个邻居顶点也互为邻居的平均概率。该测度计算值与随机条件下得到的期望值之间有较大的差异，这种巨大差异可能也显示出了真正发挥作用的社会效应。在合作网络中，与随机选择合作者相比，实际的合作网络中包含更多的三角形结构。这种现象背后有很多原因，其中一个原因可能是人们会介绍其合作者认识，而这些合作者两两之间也开始进行合作。</p>
<p>随着度的增加，局部聚类系数不断减少，这种现象的一个可能的解释是顶点分成紧密的群组或社团，同一个群组内部的顶点之间连接较多。在表现出此类行为的网络中，属于小型群组的顶点的度较小，因为这种群组的成员也相对较少，但在较大的群组中的顶点的度较大。同时，小型群组中的顶点的局部聚类系数较高。出现这种情况是因为将每个群组与网络的其余部分隔离开之后，每个群组大体上相当于一个小型网络，较小的网络会有更大的聚类系数。当对不同规模的网络取平均之后，会发现度小的顶点具有较高的聚类系数，如下图所示：</p>
<p><img src="/images/cn/2020-11-28-structure-of-real-world-network/local-clustering-as-a-function-of-degree-on-the-internet.png" alt=""></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Newman, M. E. J. (2014) <em>网络科学引论</em>. 电子工业出版社.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>网络表示，测度和度量 (Network Representation, Measures &amp; Metrics)</title><link>https://zeqiang.fun/cn/2020/11/graph-theory/</link><pubDate>Sat, 21 Nov 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/11/graph-theory/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%A4%8D%E6%9D%82%E7%BD%91%E7%BB%9C/">《复杂网络系列》</a>文章<br>
本文内容主要参考自：《网络科学引论》<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
</blockquote>
<p><strong>网络</strong>（network）也称为<strong>图</strong>（graph），是一个由多个<strong>顶点</strong>（vertex）及连接顶点的<strong>边</strong>（edge）组成的集合。在网络中，我们通常用 <code>$n$</code> 表示顶点的数目，用 <code>$m$</code> 表示边的数目。在大多数网络中两个顶点之间都只有一条边，极少数情况下，两个顶点之间有多条边，称之为<strong>重边</strong>（multiedge）。在极特殊情况下，还会存在连接到顶点自身的边，称之为<strong>自边</strong>（self-edge）。既没有自边也没有重边的图称之为<strong>简单网络</strong>（simple network）或<strong>简单图</strong>（simple graph），存在重边的网络称之为<strong>重图</strong>（multigraph）。相关概念示例如下：</p>
<p><img src="/images/cn/2020-11-21-network-representation-measures-and-metrics/graph.png" alt=""></p>
<h2 id="网络表示">网络表示</h2>
<h3 id="无向网络">无向网络</h3>
<p>对于一个包含 <code>$n$</code> 个顶点的无向图，可以用整数 <code>$1$</code> 到 <code>$n$</code> 对各个顶点进行标注。如果用 <code>$\left(i, j\right)$</code> 表示顶点 <code>$i$</code> 和顶点 <code>$j$</code> 之间的边，那么通过给定 <code>$n$</code> 的值及所有边的列表就能表示一个完整的网络，这种表示方法称之为<strong>边列表</strong>（edge list）。</p>
<p>相比于边列表，<strong>邻接矩阵</strong>（adjacency matrix）可以更好地表示网络。一个简单图的邻接矩阵 <code>$\mathbf{A}$</code> 中元素 <code>$A_{ij}$</code> 的含义如下：</p>
<p><code>$$ A_{ij}=\left\{\begin{array}{ll} 1 &amp; \text{如果顶点 } i \text{ 和顶点 } j \text{ 之间存在一条边} \\ 0 &amp; \text{其他} \end{array}\right. $$</code></p>
<p>对于一个没有自边的网络，其邻接矩阵有两个特点：</p>
<ol>
<li>邻接矩阵对角线上的元素取值均为零。</li>
<li>邻接矩阵是对称的。</li>
</ol>
<h3 id="加权网络">加权网络</h3>
<p>对于<strong>加权网络</strong>（weighted network）和<strong>赋值网络</strong>（valued network）可以将邻接矩阵中对应元素的值设定为相应的权重的方式来进行表示。</p>
<h3 id="有向网络">有向网络</h3>
<p><strong>有向网络</strong>（directed network）或<strong>有向图</strong>（directed graph）有时简称为 digraph，在这类网络中，每条边都有方向，从一个顶点指向另一个顶点，称之为<strong>有向边</strong>（directed edge）。</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>注意：</strong> 有向网络的邻接矩阵中元素 <code>$A_{ij} = 1$</code> 时表示存在从顶点 <code>$j$</code> 到顶点 <code>$i$</code> 的边。虽然表示方法有些出人意料，但在数据计算上会带来极大的方便。</div>
<h3 id="超图">超图</h3>
<p>在某些类型的网络中，一些边会同时连接多个顶点。例如：创建一个社会网络，用来表示一个大规模社区中的各个家庭。每个家庭都可能会有两名或多名成员，因此表示这些家庭之间关系的做好方法就是使用一种广义边来同时连接多个顶点。这样的边称之为<strong>超边</strong>（hyperedge），含有超边的网络称之为<strong>超图</strong>（hypergraph）。下图 (a) 表示一个小型超图，其中超边用环的形式表示。</p>
<p><img src="/images/cn/2020-11-21-network-representation-measures-and-metrics/hypergraph.png" alt=""></p>
<p>当一个网络中的顶点因为某种群组之间的关系被连接在一起时，可以使用超图来表示这个网络，在社会学中，这样的网络称之为<strong>隶属网络</strong>。对于超图，可于采用<strong>二分图</strong>的方式进行表示，通过引入 4 个新的顶点代表 4 个群组，在顶点及其所属群组之间通过边连接，如上图 (b) 所示。</p>
<h3 id="二分网络">二分网络</h3>
<p>群组内成员之间的关系可以用超图中的超边表示，也可以等价地用更方便的<strong>二分图</strong>（bipartite network）表示。这种网络中有两类顶点，一类顶点代表原始顶点，另一类顶点则表示原始顶点所属的群组。</p>
<p>二分网络中，与邻接矩阵等价的是一个矩形矩阵，称之为<strong>关联矩阵</strong>（incidence matrix）。如果 <code>$n$</code> 代表人数或网络中的成员数目，<code>$g$</code> 是群组的数目，那么关联矩阵 <code>$\mathbf{B}$</code> 是一个 <code>$g \times n$</code> 的矩阵，其元素 <code>$B_{ij}$</code> 的取值含义如下：</p>
<p><code>$$ B_{ij}=\left\{\begin{array}{ll} 1 &amp; \text{如果顶点 } j  \text{ 属于群组 } i \\ 0 &amp; \text{其他} \end{array}\right. $$</code></p>
<p>研究统一类型顶点之间的直接联系可以通过对二分网络进行<strong>单模投影</strong>（one-mode projection），推导出同类顶点之间的直接联系，如下图所示。</p>
<p><img src="/images/cn/2020-11-21-network-representation-measures-and-metrics/one-mode-projection.png" alt=""></p>
<h3 id="树">树</h3>
<p>**树（tree）**是连通的、无向的且不包含闭合循环的网络，如下图所示。</p>
<p><img src="/images/cn/2020-11-21-network-representation-measures-and-metrics/tree.png" alt=""></p>
<p><strong>连通</strong>是指任意两个顶点之间都存在一条相互可达的路径。一个网络可能有两个或多个部分组成，每个部分相互之间不连通，如果任意单独的部分都为树，则称这个网络为<strong>森林</strong>（forest）。</p>
<p>由于树没有闭合循环，因此任意两个顶点之间有且只有一条相连的路径。如果一个树有 <code>$n$</code> 个顶点，那么它有且仅有 <code>$n - 1$</code> 条边。</p>
<h3 id="度">度</h3>
<p>图中顶点的<strong>度</strong>（degree）是指与其直接相连的边数目。将顶点 <code>$i$</code> 的度表示为 <code>$k_i$</code>，对于有 <code>$n$</code> 个顶点构成的无向图，可利用邻接矩阵将度表示为：</p>
<p><code>$$ k_i = \sum_{j=1}^{n} A_{ij} $$</code></p>
<p>在无向图中，每个边都有两端，如果一共有 <code>$m$</code> 条边，那么就有 <code>$2m$</code> 个边的端点。同时，边的端点数与所有顶点度的总和相等：</p>
<p><code>$$ 2m = \sum_{j=1}^{n} k_i $$</code></p>
<p>即</p>
<p><code>$$ m = \dfrac{1}{2} \sum_{i=1}^{n} k_i = \dfrac{1}{2} \sum_{ij} A_{ij} $$</code></p>
<p>无向图中顶点度的均值 <code>$c$</code> 为：</p>
<p><code>$$ c = \dfrac{1}{n} \sum_{i=1}^{n} k_i $$</code></p>
<p>综上可得：</p>
<p><code>$$ c = \dfrac{2m}{n} $$</code></p>
<p>在一个简单图中，可能的边数的最大值是 <code>$\dbinom{n}{2} = \dfrac{1}{2} n \left(n - 1\right)$</code> 个。图的<strong>连通度</strong>（connectance）或<strong>密度</strong>（density）<code>$\rho$</code> 是所有图中实际出现的边的数目与边数最大值之间的比值：</p>
<p><code>$$ \rho = \dfrac{m}{\dbinom{n}{2}} = \dfrac{2m}{n \left(n - 1\right)} = \dfrac{c}{n - 1} $$</code></p>
<p>在有向图中，每个顶点有两个度：<strong>入度</strong>（in-degree）是连接到该顶点的入边的数目，<strong>出度</strong>（out-degree）是出边数目。当从顶点 <code>$j$</code> 到 <code>$i$</code> 有一条边时，邻接矩阵中对应的元素 <code>$A_{ij} = 1$</code>，则入度和出度记为：</p>
<p><code>$$ k_i^{\text{in}} = \sum_{j=1}^{n} A_{ij}, k_j^{\text{out}} = \sum_{i=1}^{n} A_{ij} $$</code></p>
<p>在有向图中，边的数目 <code>$m$</code> 等于入边的端点数总和，也等于出边的端点数总和，有：</p>
<p><code>$$ m=\sum_{i=1}^{n} k_{i}^{\mathrm{in}}=\sum_{j=1}^{n} k_{j}^{\mathrm{out}}=\sum_{i j} A_{i j} $$</code></p>
<p>每个有向图的入度的均值 <code>$c_{\text{in}}$</code> 和出度的均值 <code>$c_{\text{out}}$</code> 是相等的：</p>
<p><code>$$ c_{\text {in }}=\frac{1}{n} \sum_{i=1}^{n} k_{i}^{\text {in }}=\frac{1}{n} \sum_{j=1}^{n} k_{j}^{\text {out }}=c_{\text {out }} $$</code></p>
<p>简化后有：</p>
<p><code>$$ c = \dfrac{m}{n} $$</code></p>
<h3 id="路径">路径</h3>
<p>网络中的<strong>路径</strong>是指由一组顶点构成的序列，序列中每两个连续顶点都通过网络中的边连接在一起，路径长度等于该路径经过的边的数目（而非顶点的数目）。从顶点 <code>$j$</code> 到顶点 <code>$i$</code> 存在长度为 <code>$r$</code> 的路径总数为：</p>
<p><code>$$ N_{ij}^{\left(r\right)} = \left[\mathbf{A}^r\right]_{ij} $$</code></p>
<p>其中，<code>$\left[\cdots\right]_{ij}$</code> 表示矩阵中的第 <code>$i$</code> 行、第 <code>$j$</code> 列的元素。</p>
<p><strong>测地路径</strong>（geodesic path），简称为<strong>最短路径</strong>（shortest path），即两个顶点间不存在更短路径的路径。图的<strong>直径</strong>（diameter）是指图中任意一对相互连接的顶点之间的最长测地路径长度。<strong>欧拉路径</strong>（Eulerian path）是经过网络中的所有边且每条边只经过一次的路径。<strong>哈密顿路径</strong>（Hamiltonian path）是访问网络的所有顶点且每个顶点只访问一次的路径。</p>
<h3 id="分支">分支</h3>
<p>如果一个网络中两个顶点之间不存在路径，则称这个网络是<strong>非连通</strong>（disconnected）的，如果网络中任意两个顶点之间都能找到一条路径，则称这个网络是<strong>连通</strong>（connected）的。</p>
<p>网络中的子群称为<strong>分支</strong>（component）。分支是网络中顶点的子集，该子集中任何两个顶点之间至少存在一条路径，在保证该性质的前提下，网络中其他顶点都不能被添加到这个子集中。在保证一个给定性质的前提下，不能再向它添加其他顶点，就称其为<strong>最大子集</strong>（maximal subset）。</p>
<h3 id="连通度">连通度</h3>
<p>如果两条路经除了起点和终点外，不共享其他任何顶点，那么这两条路径是<strong>顶点独立</strong>（vertex-independent）的。如果两条路径是顶点独立的，那么也是边独立的，反之则不成立。</p>
<p>两个顶点之间的独立路径数称为顶点之间的<strong>连通度</strong>（connectivity），如果明确考虑边还是顶点，则需利用<strong>边连通度</strong>（edge connectivity）及<strong>顶点连通度</strong>（vertex connectivity）的概念。</p>
<h3 id="子图">子图</h3>
<p>令原图表示为 <code>$G = \left(V, E\right)$</code>，其中，<code>$V$</code> 是图中所有顶点的集合，<code>$E$</code> 是图中所有边的集合，有：</p>
<ol>
<li><strong>子图</strong>（subgraph）：<code>$G'$</code> 中所有顶点和边均包含于原图 <code>$G$</code> 中，即 <code>$E' \in E, V' \in V$</code>。</li>
<li><strong>生成子图</strong>（spanning subgraph）：<code>$G'$</code> 中顶点同原图 <code>$G$</code> 相同，且 <code>$E' \in E$</code>。</li>
<li><strong>导出子图</strong>（induced subgraph）：<code>$G'$</code> 中，<code>$V' \in V$</code>，同时对于 <code>$V'$</code> 中任意一个顶点，只要在原图 <code>$G$</code> 中有对应的边，则也应包含在 <code>$E'$</code> 中。</li>
</ol>
<p><img src="/images/cn/2020-11-21-network-representation-measures-and-metrics/subgraph.png" alt=""></p>
<h3 id="motif">Motif</h3>
<p>Motif <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 被定义为反复出现的重要连接模式。这些模式在真实的网络中要比随机网络中出现的更加频繁，如下图所示：</p>
<p><img src="/images/cn/2020-11-21-network-representation-measures-and-metrics/motifs-real-randomized-networks.png" alt=""></p>
<p>Motif 的显著性定义为：</p>
<p><code>$$ Z_i = \dfrac{N_i^{\text{real}} - \bar{N}_i^{\text{rand}}}{\text{std} \left(N_i^{\text{rand}}\right)} $$</code></p>
<p>其中，<code>$N_i^{\text{real}}$</code> 为模式在真实图中出现的次数，<code>$N_i^{\text{rand}}$</code> 为模式在随机图中出现的次数。</p>
<h3 id="graphlets">Graphlets</h3>
<p>Graphlets 是对 Motif 的扩展，Motif 是从全局的角度发现模式，而 Graphlets 是从局部角度出发。Graphlets 是连接的非同构子图，这里要求子图为导出子图。下图展示了节点数为 2 至 5 的所有 Graphlets：</p>
<p><img src="/images/cn/2020-11-21-network-representation-measures-and-metrics/graphlets.png" alt=""></p>
<p>更多关于 Motif 和 Graphlets 的细节请参见 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 。</p>
<h2 id="测度和度量">测度和度量</h2>
<h3 id="中心性">中心性</h3>
<h4 id="度中心性">度中心性</h4>
<p><strong>中心性</strong>（centrality）是研究“网络中哪些顶点是最重要或最核心的？”这个问题的一个概念。网络中心性的最简单的测度是顶点的度，即与顶点相连的边的数量。有时为了强调度作为中心性测度的用途，在社会学中也称之为<strong>度中心性</strong>（degree centrality）。</p>
<h4 id="特征向量中心性">特征向量中心性</h4>
<p>度中心性可自然地扩展为<strong>特征向量中心性</strong>（eigenvector centrality）。可以将度中心性理解为给某顶点所有邻居顶点赋予一个“中心性值”，但并非所有连接顶点的值都是相同的。很多情况下，一个顶点会由于连接到一些本身很重要的点，而使自身的重要性得到提升，这就是特征向量中心性的本质。</p>
<p>对于每个顶点 <code>$i$</code>，假设其中心性为 <code>$x_i$</code>。对于所有 <code>$i$</code>，可以设其初始值 <code>$x_i = 1$</code>，利用该值可以计算出另一个更能体现中心性的值 <code>$x'_i$</code>，将 <code>$x'_i$</code> 定义为 <code>$i$</code> 所有邻居顶点的中心性之和：</p>
<p><code>$$ x'_i = \sum_{j} A_{ij} x_j $$</code></p>
<p>重复该过程可以得到更好的估计值，重复 <code>$t$</code> 步后，中心性 <code>$\mathbf{x} \left(t\right)$</code> 的计算公式如下：</p>
<p><code>$$ \mathbf{x} \left(t\right) = \mathbf{A}^t \mathbf{x} \left(0\right) $$</code></p>
<p>当 <code>$t \to \infty$</code> 时，中心性向量的极限与邻接矩阵中的主特征向量成正比。因此，可以等价地认为中心性 <code>$\mathbf{x}$</code> 满足：</p>
<p><code>$$ \mathbf{A} \mathbf{x} = \kappa_1 \mathbf{x} $$</code></p>
<p>其中，<code>$\kappa_1$</code> 为矩阵 <code>$\mathbf{A}$</code> 的特征值中的最大值。</p>
<p>特征向量中心性对于有向图和无向图都适用。在有向图中，邻接矩阵是非对称的，因此网络有两类特征向量，通常情况下我们选择右特征向量来定义中心性。因为在有向网络中，中心性主要是由指向顶点的顶点，而不是由顶点指向的顶点赋予的。</p>
<h4 id="katz-中心性">Katz 中心性</h4>
<p><strong>Katz 中心性</strong>解决了特征向量中心性中节点中心性可能为零的问题。通过为网络中每个顶点赋予少量的“免费”中心性，可以定义：</p>
<p><code>$$ x_i = \alpha \sum_{j} A_{ij} x_j + \beta $$</code></p>
<p>其中，<code>$\alpha$</code> 和 <code>$\beta$</code> 是正常数。使用矩阵表示可以写成：</p>
<p><code>$$ \mathbf{x} = \alpha \mathbf{A} \mathbf{x} + \beta \mathbf{1} $$</code></p>
<p>其中，<code>$\mathbf{1}$</code> 代表向量 <code>$\left(1, 1, 1, \cdots\right)$</code>。重新整理有 <code>$\mathbf{x} = \beta \left(\mathbf{I} - \alpha \mathbf{A}\right)^{-1} \mathbf{1}$</code>，由于只关心相对值，通常可以设置 <code>$\beta = 1$</code>，则有：</p>
<p><code>$$ \mathbf{x} = \left(\mathbf{I} - \alpha \mathbf{A}\right)^{-1} \mathbf{1} $$</code></p>
<h4 id="pagerank">PageRank</h4>
<p>Katz 中心性有一个不足，被一个 Katz 中心性较高的顶点指向的顶点具有较高的 Katz 中心性，但如果这个中心性较高的顶点指向大量顶点，那么这些大量被指向的顶点也会拥有较高的中心性，但这种估计并非总是恰当的。在新的中心性中，那些指向很多其他顶点的顶点，即使本身的中心性很高，但也只能传递给它指向的每个顶点少量的中心性，定义为：</p>
<p><code>$$ x_{i}=\alpha \sum_{j} A_{i j} \frac{x_{j}}{k_{j}^{\text {out }}}+\beta $$</code></p>
<p>其中，<code>$k_j^{\text{out}}$</code> 为顶点的出度，当 <code>$k_j^{\text{out}} = 0$</code> 时可以将其设定为任何一个非零值，都不会影响计算结果。利用矩阵的形式，可以表示为：</p>
<p><code>$$ \mathbf{x}=\alpha \mathbf{AD}^{-1} \mathbf{x}+\beta \mathbf{1} $$</code></p>
<p>其中，<code>$\mathbf{D}$</code> 为对角矩阵，<code>$D_{ii} = \max \left(k_j^{\text{out}}, 1\right)$</code>。同之前一样，<code>$\beta$</code> 只是整个公式的因子，设置 <code>$\beta = 1$</code>，有：</p>
<p><code>$$ \mathbf{x}=\left(\mathbf{I}-\alpha \mathbf{A} \mathbf{D}^{-1}\right)^{-1} \mathbf{1} $$</code></p>
<p>该中心性即为 <strong>PageRank</strong>。</p>
<p>上述 4 种中心性的区别和联系如下表所示：</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>带有常数项</th>
          <th>不带常数项</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>除以出度</td>
          <td><code>$\mathbf{x} = \left(\mathbf{I}-\alpha \mathbf{A} \mathbf{D}^{-1}\right)^{-1} \mathbf{1}$</code><br/>PageRank</td>
          <td><code>$\mathbf{x} = \mathbf{A} \mathbf{D}^{-1} \mathbf{x}$</code><br/>度中心性</td>
      </tr>
      <tr>
          <td>不除出度</td>
          <td><code>$\mathbf{x} = \left(\mathbf{I} - \alpha \mathbf{A}\right)^{-1} \mathbf{1}$</code><br/>Katz 中心性</td>
          <td><code>$\mathbf{x} = \kappa_1^{-1} \mathbf{A} \mathbf{x}$</code><br/>特征向量中心性</td>
      </tr>
  </tbody>
</table>
<h4 id="接近度中心性">接近度中心性</h4>
<p><strong>接近度中心性</strong>（closeness centrality）用于度量一个顶点到其他顶点的平均距离。</p>
<p><code>$$ C_{i}=\frac{1}{\ell_{i}}=\frac{n}{\sum_{j} d_{i j}} $$</code></p>
<p>其中，<code>$d_{i j}$</code> 表示从顶点 <code>$i$</code> 到 <code>$j$</code> 的测地路径长度，即路径中边的总数，<code>$\ell_{i}$</code> 表示从 <code>$i$</code> 到 <code>$j$</code> 的平均测地距离。在大多数网络中，顶点之间的测地距离一般都较小，并且随着网络规模的增长，该值只是以对数级别速度缓慢增长。</p>
<p>在不同分支中的两个顶点之间的测地距离定义为无穷大，则 <code>$C_i$</code> 为零。为了解决这个问题，最常见的方法是只计算同一分支内部的顶点的平均测地距离。新的定义使用顶点之间的调和平均测地距离：</p>
<p><code>$$ C_{i}^{\prime}=\frac{1}{n-1} \sum_{j(\neq i)} \frac{1}{d_{i j}} $$</code></p>
<p>公式中排除了 <code>$j = i$</code> 的情况，因为 <code>$d_{ii} = 0$</code>。结果也称之为<strong>调和中心性</strong>（harmonic centrality）。</p>
<h4 id="介数中心性">介数中心性</h4>
<p><strong>介数中心性</strong>（betweenness centrality）描述了一个顶点在其他顶点之间路径上的分布程度。假设在网络中每两个顶点之间，在每个单位时间内以相等的概率交换信息，信息总是沿着网络中最短测地路径传播，如果有多条最短测地路径则随机选择。由于消息是沿着最短路径以相同的速率传播，因此经过某个顶点的消息数与经过该顶点的测地路径数成正比。测地路径数就是所谓的介数中心性，简称<strong>介数</strong>。</p>
<p>定义 <code>$n_{st}^i$</code> 为从 <code>$s$</code> 到 <code>$t$</code> 经过 <code>$i$</code> 的测地路径数量，定义 <code>$g_{st}$</code> 为从 <code>$s$</code> 到 <code>$t$</code> 的测地路径总数，那么顶点 <code>$i$</code> 的介数中心性可以表示为：</p>
<p><code>$$ x_{i}=\sum_{s t} \frac{n_{s t}^{i}}{g_{s t}} $$</code></p>
<p>高介数中心性的顶点由于控制着其他顶点之间的消息传递，在网络中有着很强的影响力。删除介数最高的顶点，也最有可能破坏其他顶点之间的通信。</p>
<p>不同中心性的可视化如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-11-21-network-representation-measures-and-metrics/centrality-measures.png" class="lazyload"/>
  <figcaption><p class="figcaption">不同中心性可视化 <a href="https://commons.wikimedia.org/w/index.php?curid=39064835">By Tapiocozzo, CC BY-SA 4.0</a></p></figcaption>
</figure>
<p>其中，A：介数中心性；B：接近度中心性；C：特征向量中心性；D：度中心性；E：调和中心性；F：Katz 中心性。</p>
<h3 id="传递性">传递性</h3>
<p><strong>传递性</strong>（transitivity）在社会网络中的重要性要比其他网络中重要得多。在数学上，对于关系“<code>$\circ$</code>”，如果 <code>$a \circ b$</code> 和 <code>$b \circ c$</code>，若能推出 <code>$a \circ c$</code>，则称 <code>$\circ$</code> 具有传递性。</p>
<p>完全传递性值出现在每一个分支都是全连通的子图或团的网络中。<strong>团</strong>（clique）是指无向图网络中的一个最大顶点子集，在该子集中任何两个顶点之间都有一条边直接连接。完全传递性没有太多的实际意义，而部分传递性却很有用。在很多网络中，<code>$u$</code> 认识 <code>$v$</code> 且 <code>$v$</code> 认识 <code>$w$</code>，并不能保证 <code>$u$</code> 认识 <code>$w$</code>，但两者之间相互认识的概率很大。</p>
<p>如果 <code>$u$</code> 也认识 <code>$w$</code>，则称该路径是闭合的。在社会网络术语中，称 <code>$u, v, w$</code> 这 3 个顶点形成一个<strong>闭合三元组</strong>（closed triad）。我们将<strong>聚类系数</strong>（clustering coefficient）定义为网络中所有长度为 2 的路径中闭合路径所占的比例：</p>
<p><code>$$ C = \dfrac{\text{长度为 2 的路径中闭合路径数}}{\text{长度为 2 的路径数}} $$</code></p>
<p>其取值范围在 0 到 1 之间。社会网络的聚类系数比其他网络偏高。</p>
<p>对于顶点 <code>$i$</code>，定地单个顶点的聚类系数为：</p>
<p><code>$$ C_i = \dfrac{\text{顶点 i 的邻居顶点中直接相连的顶点对数}}{\text{顶点 i 的邻居顶点对总数}} $$</code></p>
<p><code>$C_i$</code> 也称为<strong>局部聚类系数</strong>（local clustering coefficient），该值代表了 <code>$i$</code> 的朋友之间互为朋友的平均概率。</p>
<h3 id="相互性">相互性</h3>
<p>聚类系数观察的是长度为 3 的循环，长度为 2 的循环的频率通过<strong>相互性</strong>（reciprocity）来度量，该频率描述了两个顶点之间相互指向的概率。</p>
<h3 id="相似性">相似性</h3>
<p>社会网络分析的另一个核心概念是顶点之间的相似性。构造网络相似性的测度有两种基本方法：<strong>结构等价</strong>（structural equivalence）和<strong>规则等价</strong>（regular equivalence），如下图所示：</p>
<p><img src="/images/cn/2020-11-21-network-representation-measures-and-metrics/structural-and-regular-equivalence.png" alt=""></p>
<h4 id="结构等价">结构等价</h4>
<p>针对无向网络中，最简单和最显而易见的结构等价测度就是计算两个顶点的共享邻居顶点数。在无向网络中，顶点 <code>$i$</code> 和 <code>$j$</code> 的共享邻居顶点数表示为 <code>$n_{ij}$</code>，有：</p>
<p><code>$$ n_{ij} = \sum_{k} A_{ik} A_{kj} $$</code></p>
<p>利用<strong>余弦相似度</strong>可以更好的对其进行度量。将邻接矩阵的第 <code>$i$</code> 和第 <code>$j$</code> 行分别看成两个向量，然后将这两个向量之间的夹角余弦值用于相似性度量，有：</p>
<p><code>$$ \sigma_{i j}=\cos \theta=\frac{\sum_{k} A_{i k} A_{k j}}{\sqrt{\sum_{k} A_{i k}^{2}} \sqrt{\Sigma_{k} A_{j k}^{2}}} $$</code></p>
<p>假设网络是不带权重的简单图，上式可以化简为：</p>
<p><code>$$ \sigma_{i j}=\frac{\sum_{k} A_{i k} A_{k j}}{\sqrt{k_{i}} \sqrt{k_{j}}}=\frac{n_{i j}}{\sqrt{k_{i} k_{j}}} $$</code></p>
<p>其中，<code>$k_i$</code> 是顶点 <code>$i$</code> 的度。余弦相似度的取值范围为从 0 到 1，1 表示两个顶点之间拥有完全相同的邻居节点。</p>
<p><strong>皮尔逊相关系数</strong>通过同随机选择邻居顶点条件下共享邻居顶点数的期望值进行比较的方式进行计算，得到的标准的皮尔逊相关系数为：</p>
<p><code>$$ r_{i j}=\frac{\sum_{k}\left(A_{i k}-\left\langle A_{i}\right\rangle\right)\left(A_{j k}-\left\langle A_{j}\right\rangle\right)}{\sqrt{\sum_{k}\left(A_{i k}-\left\langle A_{i}\right\rangle\right)^{2}} \sqrt{\sum_{k}\left(A_{j k}-\left\langle A_{j}\right\rangle\right)^{2}}} $$</code></p>
<p>上式的取值范围从 -1 到 1，数值越大表明两者之间越相似。</p>
<h4 id="规则等价">规则等价</h4>
<p>规则等价的顶点不必共享邻居顶点，但是两个顶点的邻居顶点本身要具有相似性。一些简单的代数测度思想如下：定义一个相似性值 <code>$\sigma_{ij}$</code>，若顶点 <code>$i$</code> 和 <code>$j$</code> 各自的邻居顶点 <code>$k$</code> 和 <code>$l$</code> 本身具有较高的相似性，则 <code>$i$</code> 和 <code>$j$</code> 的相似性也较高。对于无向网络，有以下公式：</p>
<p><code>$$ \sigma_{i j}=\alpha \sum_{k l} A_{i k} A_{j l} \sigma_{k l} $$</code></p>
<p>或者利用矩阵性质表示为 <code>$\mathbf{\sigma} = \alpha \mathbf{A \sigma A}$</code>。</p>
<h3 id="同质性">同质性</h3>
<p>在社会网络中，人们倾向于选择那些他们认为与其自身在某些方面相似的人作为朋友，这种倾向性称为<strong>同质性</strong>（homophily）或<strong>同配混合</strong>（assortative mixing）。</p>
<h4 id="依据枚举特征的同配混合">依据枚举特征的同配混合</h4>
<p>假设有一个网络，其顶点根据某个<strong>枚举特征</strong>（例如：国籍、种族、性别等）分类，且该特征的取值是一个有限集合。如果网络中连接相同类型顶点之间的边所占比例很大，那么该网络就是同配的。量化同配性简单的方法是观测这部分边占总边数的比例，但这并不是很好的度量方法，因为如果所有顶点都是同一个类型，那么测度值就是 1。</p>
<p>好的测度可以通过首先找出连接同类顶点的边所占的比例，然后减去在不考虑顶点类型时，随机连接的边中，连接两个同类顶点的边所占比例的期望值的方式得到。常用的测度为<strong>模块度</strong>（modularity）：</p>
<p><code>$$ Q=\frac{1}{2 m} \sum_{i j}\left(A_{i j}-\frac{k_{i} k_{j}}{2 m}\right) \delta_{g_{i} g_{i}} $$</code></p>
<p>其中，<code>$k_i$</code> 为顶点 <code>$i$</code> 的度，<code>$g_i$</code> 为顶点 <code>$i$</code> 的类型，<code>$m$</code> 为总边数，<code>$\delta_{ij}$</code> 为<a href="https://zh.wikipedia.org/wiki/%E5%85%8B%E7%BD%97%E5%86%85%E5%85%8B%CE%B4%E5%87%BD%E6%95%B0">克罗内克函数</a>。该值严格小于 1，如果同类顶点之间边数的实际值大于随机条件下的期望值，则该值为正数，否则为负数，值为正说明该网络是同配混合的。</p>
<h4 id="依据标量特征的同配混合">依据标量特征的同配混合</h4>
<p>如果根据<strong>标量特征</strong>（例如：年龄、收入等）来度量网络中的同质性。由于该类特征具有确定的顺序，因此根据标量的数值，不仅可以指出两个顶点在什么情况下是完全相同的，也可以指出它们在真么情况下是近似相同的。</p>
<p>令 <code>$x_i$</code> 为顶点 <code>$i$</code> 的标量值，<code>$\left(x_i, x_j\right)$</code> 为网络中每一条边 <code>$\left(i, j\right)$</code> 的两个端点的值，利用协方差可以得到<strong>同配系数</strong>：</p>
<p><code>$$ r=\frac{\sum_{i j}\left(A_{i j}-k_{i} k_{j} / 2 m\right) x_{i} x_{j}}{\sum_{i j}\left(k_{i} \delta_{i j}-k_{i} k_{j} / 2 m\right) x_{i} x_{j}} $$</code></p>
<p>该系数在全同配混合网络中取最大值 1，在全异配混合网络中取最小值 -1，值 0 意味着边两端的顶点值是非相关的。</p>
<h4 id="依据度的同配混合">依据度的同配混合</h4>
<p>依据度的同配混合是依据标量特征的同配混合的一个特例。依据度的同配混合网络中，高度数顶点倾向于与其他高度数顶点相连，而低度数顶点倾向于与其他低度数顶点相连。</p>
<p>在同配网络中，度大的顶点倾向于聚集在一起的网络中，我们希望得到网络中这些度大的顶点构成的顶点块或核，它们周围是一些度小的顶点构成的低密度<strong>边缘</strong>（periphery）。这种<strong>核心/边缘结构</strong>（core/periphery structure）是社会网络的普遍特征。</p>
<p><img src="/images/cn/2020-11-21-network-representation-measures-and-metrics/core-periphery-structure.png" alt=""></p>
<p>上图 (a) 给出了一个小型的同配混合网络，其核心/边缘结构明显，上图 (b) 给出了一个小型异配混合网络，通常不具备核心/边缘结构，但顶点的分布更加均匀。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Newman, M. E. J. (2014) <em>网络科学引论</em>. 电子工业出版社.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Milo, R., Shen-Orr, S., Itzkovitz, S., Kashtan, N., Chklovskii, D., &amp; Alon, U. (2002). Network motifs: simple building blocks of complex networks. <em>Science</em>, 298(5594), 824-827.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Jain, D., &amp; Patgiri, R. (2019, April). Network Motifs: A Survey. In <em>International Conference on Advances in Computing and Data Sciences</em> (pp. 80-91). Springer, Singapore.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Henderson, K., Gallagher, B., Eliassi-Rad, T., Tong, H., Basu, S., Akoglu, L., &hellip; &amp; Li, L. (2012, August). Rolx: structural role extraction &amp; mining in large graphs. In <em>Proceedings of the 18th ACM SIGKDD international conference on Knowledge discovery and data mining</em> (pp. 1231-1239).&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>文本相似度 (Text Similarity)</title><link>https://zeqiang.fun/cn/2020/10/text-similarity/</link><pubDate>Sat, 31 Oct 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/10/text-similarity/</guid><description><![CDATA[
        <p>文本相似度是指衡量两个文本的相似程度，相似程度的评价有很多角度：单纯的字面相似度（例如：我和他 v.s. 我和她），语义的相似度（例如：爸爸 v.s. 父亲）和风格的相似度（例如：我喜欢你 v.s. 我好喜欢你耶）等等。</p>
<h2 id="文本表示角度">文本表示角度</h2>
<h3 id="统计模型">统计模型</h3>
<h4 id="文本切分">文本切分</h4>
<p>在中文和拉丁语系中，文本的直观表示就存在一定的差异，拉丁语系中词与词之间存在天然的分隔符，而中文则没有。</p>
<blockquote>
<p>I can eat glass, it doesn&rsquo;t hurt me.<br>
我能吞下玻璃而不伤身体。</p>
</blockquote>
<p>因此针对拉丁语系的文本切分相对中文容易许多。</p>
<ul>
<li><strong>N 元语法</strong></li>
</ul>
<p>N-gram (N 元语法) 是一种文本表示方法，指文中连续出现的 $n$ 个词语。N-gram 模型是基于 $n-1$ 阶马尔科夫链的一种概率语言模型，可以通过前 $n-1$ 个词对第 $n$ 个词进行预测。以 <code>南京市长江大桥</code> 为例，N-gram 的表示如下：</p>
<pre><code>一元语法（unigram）：南/京/市/长/江/大/桥
二元语法（bigram）：南京/京市/市长/长江/江大/大桥
三元语法（trigram）：南京市/京市长/市长江/长江大/江大桥
</code></pre>
<pre><code class="language-python">import re
from nltk.util import ngrams

s = '南京市长江大桥'
tokens = re.sub(r'\s', '', s)

list(ngrams(tokens, 1))
# [('南',), ('京',), ('市',), ('长',), ('江',), ('大',), ('桥',)]

list(ngrams(tokens, 2))
# [('南', '京'), ('京', '市'), ('市', '长'),
#  ('长', '江'), ('江', '大'), ('大', '桥')]

list(ngrams(tokens, 3, pad_left=True, pad_right=True, left_pad_symbol='&lt;s&gt;', right_pad_symbol='&lt;/s&gt;'))
# [('&lt;s&gt;', '&lt;s&gt;', '南'),
#  ('&lt;s&gt;', '南', '京'),
#  ('南', '京', '市'),
#  ('京', '市', '长'),
#  ('市', '长', '江'),
#  ('长', '江', '大'),
#  ('江', '大', '桥'),
#  ('大', '桥', '&lt;/s&gt;'),
#  ('桥', '&lt;/s&gt;', '&lt;/s&gt;')]
</code></pre>
<ul>
<li><strong>分词</strong></li>
</ul>
<p>分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。在英文的行文中，单词之间是以空格作为自然分界符的，而中文只是字、句和段能通过明显的分界符来简单划界，唯独词没有一个形式上的分界符，虽然英文也同样存在短语的划分问题，不过在词这一层上，中文比之英文要复杂得多、困难得多。</p>
<pre><code class="language-python">s = '南京市长江大桥'

# jieba
# https://github.com/fxsjy/jieba
import jieba

list(jieba.cut(s, cut_all=False))
# ['南京市', '长江大桥']

list(jieba.cut(s, cut_all=True))
# ['南京', '南京市', '京市', '市长', '长江', '长江大桥', '大桥']

list(jieba.cut_for_search(s))
# ['南京', '京市', '南京市', '长江', '大桥', '长江大桥']

# THULAC
# https://github.com/thunlp/THULAC-Python
import thulac

thulac_ins = thulac.thulac()

thulac_ins.cut(s)
# [['南京市', 'ns'], ['长江', 'ns'], ['大桥', 'n']]

# PKUSEG
# https://github.com/lancopku/PKUSeg-python
import pkuseg

seg = pkuseg.pkuseg(postag=True)

seg.cut(s)
# [('南京市', 'ns'), ('长江', 'ns'), ('大桥', 'n')]

# HanLP
# https://github.com/hankcs/HanLP
import hanlp

tokenizer = hanlp.load('LARGE_ALBERT_BASE')

tokenizer(s)
# ['南京市', '长江', '大桥']
</code></pre>
<h4 id="主题模型">主题模型</h4>
<p>除了对文本进行切分将切分后结果全部用于表示文本外，还可以用部分字词表示一篇文档。主题模型（Topic Model）在机器学习和自然语言处理等领域是用来在一系列文档中发现抽象主题的一种统计模型。</p>
<p><img src="/images/cn/2020-10-31-text-similarity/what-is-topic-model.png" alt="What is Topic Model"></p>
<p>直观来讲，如果一篇文章有一个中心思想，那么一些特定词语会更频繁的出现。比方说，如果一篇文章是在讲狗的，那“狗”和“骨头”等词出现的频率会高些。如果一篇文章是在讲猫的，那“猫”和“鱼”等词出现的频率会高些。而有些词例如“这个”、“和”大概在两篇文章中出现的频率会大致相等。但真实的情况是，一篇文章通常包含多种主题，而且每个主题所占比例各不相同。因此，如果一篇文章 10% 和猫有关，90% 和狗有关，那么和狗相关的关键字出现的次数大概会是和猫相关的关键字出现次数的 9 倍。</p>
<p>一个主题模型试图用数学框架来体现文档的这种特点。主题模型自动分析每个文档，统计文档内的词语，根据统计的信息来断定当前文档含有哪些主题，以及每个主题所占的比例各为多少 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。</p>
<ul>
<li><strong>TF-IDF</strong></li>
</ul>
<p>TF-IDF 是 Term Frequency - Inverse Document Frequency 的缩写，即“词频-逆文本频率”。TF-IDF 可以用于评估一个字词在语料中的一篇文档中的重要程度，基本思想是如果某个字词在一篇文档中出现的频率较高，而在其他文档中出现频率较低，则认为这个字词更能够代表这篇文档。</p>
<p>形式化地，对于文档 $y$ 中的字词 $x$ 的 TF-IDF 重要程度可以表示为：</p>
<p><code>$$ w_{x, y} = tf_{x, y} \times \log \left(\dfrac{N}{df_{x}}\right) $$</code></p>
<p>其中，<code>$tf_{x, y}$</code> 表示字词 <code>$x$</code> 在文档 <code>$y$</code> 中出现的频率，<code>$df_x$</code> 为包含字词 <code>$x$</code> 的文档数量，<code>$N$</code> 为语料中文档的总数量。</p>
<p>以 <a href="https://github.com/liuhuanyong/MusicLyricChatbot">14 万歌词语料</a> 为例，通过 TF-IDF 计算周杰伦的《简单爱》中最重要的 3 个词为 <code>['睡着', '放开', '棒球']</code>。</p>
<ul>
<li><strong>BM25</strong></li>
</ul>
<p>BM25 算法的全称为 Okapi BM25，是一种搜索引擎用于评估查询和文档之间相关程度的排序算法，其中 BM 是 Best Match 的缩写。</p>
<p>对于一个给定的查询 <code>$Q$</code>，包含的关键词为 <code>$q_1, \cdots, q_n$</code>，一个文档 <code>$D$</code> 的 BM25 值定义为：</p>
<p><code>$$ \operatorname{score}(D, Q)=\sum_{i=1}^{n} \operatorname{IDF}\left(q_{i}\right) \cdot \frac{f\left(q_{i}, D\right) \cdot\left(k_{1}+1\right)}{f\left(q_{i}, D\right)+k_{1} \cdot\left(1-b+b \cdot \frac{|D|}{\text { avgdl }}\right)} $$</code></p>
<p>其中，<code>$f\left(q_{i}, D\right)$</code> 表示 <code>$q_i$</code> 在文档 <code>$D$</code> 中的词频，<code>$|D|$</code> 表示文档 <code>$D$</code> 中的词数，<code>$\text{avgdl}$</code> 表示语料中所有文档的平均长度。<code>$k_1$</code> 和 <code>$b$</code> 为自由参数，通常取值为 <code>$k_1 \in \left[1.2, 2.0\right], b = 0.75$</code> <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>。<code>$\operatorname{IDF} \left(q_i\right)$</code> 表示词 <code>$q_i$</code> 的逆文档频率，通常计算方式如下：</p>
<p><code>$$ \operatorname{IDF}\left(q_{i}\right)=\ln \left(\frac{N-n\left(q_{i}\right)+0.5}{n\left(q_{i}\right)+0.5}+1\right) $$</code></p>
<p>其中，<code>$N$</code> 为语料中文档的总数量，<code>$n \left(q_i\right)$</code> 表示包含 <code>$q_i$</code> 的文档数量。</p>
<p>BM25 算法是对 TF-IDF 算法的优化，在词频的计算上，BM25 限制了文档 <code>$D$</code> 中关键词 <code>$q_i$</code> 的词频对评分的影响。为了防止词频过大，BM25 将这个值的上限设置为 <code>$k_1 + 1$</code>。</p>
<p><img src="/images/cn/2020-10-31-text-similarity/bm25-tf-1.png" alt=""></p>
<p>同时，BM25 还引入了平均文档长度 <code>$\text{avgdl}$</code>，不同的平均文档长度 <code>$\text{avgdl}$</code> 对 TF 分值的影响如下图所示：</p>
<p><img src="/images/cn/2020-10-31-text-similarity/bm25-tf-2.png" alt=""></p>
<ul>
<li><strong>TextRank</strong></li>
</ul>
<p>TextRank <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 是基于 PageRank <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 算法的一种关键词提取算法。PageRank 最早是用于 Google 的网页排名，因此以公司创始人拉里·佩奇（Larry Page）的姓氏来命名。PageRank 的计算公式如下：</p>
<p><code>$$ S\left(V_{i}\right)=(1-d)+d * \sum_{V_{j} \in I n\left(V_{i}\right)} \frac{1}{\left|O u t\left(V_{j}\right)\right|} S\left(V_{j}\right) $$</code></p>
<p>其中，<code>$V_i$</code> 表示任意一个网页，<code>$V_j$</code> 表示链接到网页 <code>$V_i$</code> 的网页，<code>$S \left(V_i\right)$</code> 表示网页 <code>$V_i$</code> 的 PageRank 值，<code>$In \left(V_i\right)$</code> 表示网页 <code>$V_i$</code> 所有的入链集合，<code>$Out \left(V_j\right)$</code> 表示网页 <code>$V_j$</code> 所有的出链集合，<code>$|\cdot|$</code> 表示集合的大小，<code>$d$</code> 为阻尼系数，是为了确保每个网页的 PageRank 值都大于 0。</p>
<p>TextRank 由 PageRank 改进而来，计算公式如下：</p>
<p><code>$$ WS \left(V_{i}\right)=(1-d)+d * \sum_{V_{j} \in In\left(V_{i}\right)} \frac{w_{j i}}{\sum_{V_{k} \in Out\left(V_{j}\right)} w_{j k}} WS \left(V_{j}\right) $$</code></p>
<p>相比于 PageRank 公式增加了权重项 <code>$W_{ji}$</code>，用来表示两个节点之间的边的权重。TextRank 提取关键词的算法流程如下：</p>
<ol>
<li>将文本进行切分得到 <code>$S_i = \left[t_{i1}, t_{i2}, \cdots, t_{in}\right]$</code>。</li>
<li>将 <code>$S_i$</code> 中大小为 <code>$k$</code> 的滑动窗口中的词定义为共现关系，构建关键词图 <code>$G = \left(V, E\right)$</code>。</li>
<li>根据 TextRank 的计算公式对每个节点的值进行计算，直至收敛。</li>
<li>对节点的 TextRank 的值进行倒叙排序，获取前 <code>$n$</code> 个词作为关键词。</li>
</ol>
<ul>
<li><strong>LSA, PLSA, LDA &amp; HDP</strong></li>
</ul>
<p><strong>潜在语义分析（LSA, Latent Semantic Analysis）</strong><sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> 的核心思想是将文本的高维词空间映射到一个低维的向量空间，我们称之为隐含语义空间。降维可以通过<a href="/cn/2017/12/evd-svd-and-pca/">奇异值分解（SVD）</a>实现，令 <code>$X$</code> 表示语料矩阵，元素 <code>$\left(i, j\right)$</code> 表示词 <code>$i$</code> 和文档 <code>$j$</code> 的共现情况（例如：词频）：</p>
<p><code>$$ X = \mathbf{d}_{j} \cdot \mathbf{t}_{i}^{T} = \left[\begin{array}{c} x_{1, j} \\ \vdots \\ x_{i, j} \\ \vdots \\ x_{m, j} \end{array}\right] \cdot \left[\begin{array}{ccccc} x_{i, 1} &amp; \ldots &amp; x_{i, j} &amp; \ldots &amp; x_{i, n} \end{array}\right] = \left[\begin{array}{ccccc} x_{1,1} &amp; \ldots &amp; x_{1, j} &amp; \ldots &amp; x_{1, n} \\ \vdots &amp; \ddots &amp; \vdots &amp; \ddots &amp; \vdots \\ x_{i, 1} &amp; \ldots &amp; x_{i, j} &amp; \ldots &amp; x_{i, n} \\ \vdots &amp; \ddots &amp; \vdots &amp; \ddots &amp; \vdots \\ x_{m, 1} &amp; \ldots &amp; x_{m, j} &amp; \ldots &amp; x_{m, n} \end{array}\right] $$</code></p>
<p>利用奇异值分解：</p>
<p><code>$$ X = U \Sigma V^{T} $$</code></p>
<p>取最大的 <code>$K$</code> 个奇异值，则可以得到原始矩阵的近似矩阵：</p>
<p><code>$$ \widetilde{X} =U \widetilde{\Sigma} V^{T} $$</code></p>
<p>在处理一个新的文档时，可以利用下面的公式将原始的词空间映射到潜在语义空间：</p>
<p><code>$$ \tilde{x} =\tilde{\Sigma} ^{-1} V^{T} x_{test} $$</code></p>
<p>LSA 的优点：</p>
<ol>
<li>低维空间可以刻画同义词</li>
<li>无监督模型</li>
<li>降维可以减少噪声，使特征更加鲁棒</li>
</ol>
<p>LSA 的缺点：</p>
<ol>
<li>未解决多义词问题</li>
<li>计算复杂度高，增加新文档时需要重新训练</li>
<li>没有明确的物理解释</li>
<li>高斯分布假设不符合文本特征（词频不为负）</li>
<li>维度的确定是 Ad hoc 的</li>
</ol>
<p><strong>概率潜语义分析（Probabilistic Latent Semantic Analysis, PLSA）</strong><sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 相比于 LSA 增加了概率模型，每个变量以及相应的概率分布和条件概率分布都有明确的物理解释。</p>
<p>PLSA 认为一篇文档可以由多个主题混合而成，而每个主题都是词上的概率分布，文章中的每个词都是由一个固定的主题生成的，如下图所示：</p>
<p><img src="/images/cn/2020-10-31-text-similarity/plsa.png" alt=""></p>
<p>针对第 <code>$m$</code> 篇文档 <code>$d_m$</code> 中的每个词的生成概率为：</p>
<p><code>$$ p\left(w \mid d_{m}\right)=\sum_{z=1}^{K} p(w \mid z) p\left(z \mid d_{m}\right)=\sum_{z=1}^{K} \varphi_{z w} \theta_{m z} $$</code></p>
<p>因此整篇文档的生成概率为：</p>
<p><code>$$ p\left(\vec{w} \mid d_{m}\right)=\prod_{i=1}^{n} \sum_{z=1}^{K} p\left(w_{i} \mid z\right) p\left(z \mid d_{m}\right)=\prod_{i=1}^{n} \sum_{z=1}^{K} \varphi_{z w_{i}} \theta_{d z} $$</code></p>
<p>PLSA 可以利用 EM 算法求得局部最优解。</p>
<p>PLSA 优点：</p>
<ol>
<li>定义了概率模型，有明确的物理解释</li>
<li>多项式分布假设更加符合文本特征</li>
<li>可以通过模型选择和复杂度控制来确定主题的维度</li>
<li>解决了同义词和多义词的问题</li>
</ol>
<p>PLSA 缺点：</p>
<ol>
<li>随着文本和词的增加，PLSA 模型参数也随之线性增加</li>
<li>可以生成语料中的文档的模型，但不能生成新文档的模型</li>
<li>EM 算法求解的计算量较大</li>
</ol>
<p><strong>隐含狄利克雷分布（Latent Dirichlet Allocation, LDA）</strong><sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup> 在 PLSA 的基础上增加了参数的先验分布。在 PLSA 中，对于一个新文档，是无法获取 <code>$p \left(d\right)$</code> 的，因此这个概率模型是不完备的。LDA 对于 <code>$\vec{\theta}_m$</code> 和 <code>$\vec{\phi}_k$</code> 都增加了多项式分布的共轭分布狄利克雷分布作为先验，整个 LDA 模型如下图所示：</p>
<p><img src="/images/cn/2020-10-31-text-similarity/lda.png" alt=""></p>
<p>LDA 的参数估计可以通过<a href="/cn/2017/12/mcmc-and-gibbs-sampling/">吉布斯采样</a>实现。PLSA 和 LDA 的更多细节请参见《LDA 数学八卦》<sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup>。</p>
<p>LDA 在使用过程中仍需要指定主题的个数，而<strong>层次狄利克雷过程（Hierarchical Dirichlet Processes, HDP）</strong><sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup> 通过过程的构造可以自动训练出主题的个数，更多实现细节请参考论文。</p>
<p>LSA，PLSA，LDA 和 HDP 之间的演化关系如下图所示：</p>
<p><img src="/images/cn/2020-10-31-text-similarity/lsa-plsa-lda-hdp.png" alt=""></p>
<blockquote>
<p>本节相关代码详见 <a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2020-10-31-text-similarity/topic-model.py">这里</a>。</p>
</blockquote>
<h4 id="距离度量">距离度量</h4>
<blockquote>
<p>本节内容源自 <a href="/cn/2019/01/similarity-and-distance-measurement/">相似性和距离度量 (Similarity &amp; Distance Measurement)</a>。</p>
</blockquote>
<p>相似性度量 (Similarity Measurement) 用于衡量两个元素之间的相似性程度或两者之间的距离 (Distance)。距离衡量的是指元素之间的不相似性 (Dissimilarity)，通常情况下我们可以利用一个距离函数定义集合 <code>$X$</code> 上元素间的距离，即：</p>
<p><code>$$ d: X \times X \to \mathbb{R} $$</code></p>
<ul>
<li><strong>Jaccard 系数</strong></li>
</ul>
<p><code>$$ s = \dfrac{\left|X \cap Y\right|}{\left| X \cup Y \right|} = \dfrac{\left|X \cap Y\right|}{\left|X\right| + \left|Y\right| - \left|X \cap Y\right|} $$</code></p>
<p>Jaccard 系数的取值范围为：<code>$\left[0, 1\right]$</code>，0 表示两个集合没有重合，1 表示两个集合完全重合。</p>
<ul>
<li><strong>Dice 系数</strong></li>
</ul>
<p><code>$$ s = \dfrac{2 \left| X \cap Y \right|}{\left|X\right| + \left|Y\right|} $$</code></p>
<p>与 Jaccard 系数相同，Dice 系数的取值范围为：<code>$\left[0, 1\right]$</code>，两者之间可以相互转换 <code>$s_d = 2 s_j / \left(1 + s_j\right), s_j = s_d / \left(2 - s_d\right)$</code>。不同于 Jaccard 系数，Dice 系数的差异函数 <code>$d = 1 - s$</code> 并不是一个合适的距离度量，因为其并不满足距离函数的三角不等式。</p>
<ul>
<li><strong>Tversky 系数</strong></li>
</ul>
<p><code>$$ s = \dfrac{\left| X \cap Y \right|}{\left| X \cap Y \right| + \alpha \left| X \setminus Y \right| + \beta \left| Y \setminus X \right|} $$</code></p>
<p>其中，<code>$X \setminus Y$</code> 表示集合的相对补集。Tversky 系数可以理解为 Jaccard 系数和 Dice 系数的一般化，当 <code>$\alpha = \beta = 1$</code> 时为 Jaccard 系数，当 <code>$\alpha = \beta = 0.5$</code> 时为 Dice 系数。</p>
<ul>
<li><strong>Levenshtein 距离</strong></li>
</ul>
<p>Levenshtein 距离是 <strong>编辑距离 (Editor Distance)</strong> 的一种，指两个字串之间，由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括将一个字符替换成另一个字符，插入一个字符，删除一个字符。例如将 <strong>kitten</strong> 转成 <strong>sitting</strong>，转换过程如下：</p>
<p><code>$$ \begin{equation*} \begin{split} \text{kitten} \to \text{sitten} \left(k \to s\right) \\ \text{sitten} \to \text{sittin} \left(e \to i\right) \\ \text{sittin} \to \text{sitting} \left(\  \to g\right) \end{split} \end{equation*} $$</code></p>
<p>编辑距离的求解可以利用动态规划的思想优化计算的时间复杂度。</p>
<ul>
<li><strong>Jaro-Winkler 距离</strong></li>
</ul>
<p>对于给定的两个字符串 <code>$s_1$</code> 和 <code>$s_2$</code>，Jaro 相似度定义为：</p>
<p><code>$$ sim = \begin{cases} 0 &amp; \text{if} \  m = 0 \\ \dfrac{1}{3} \left(\dfrac{m}{\left|s_1\right|} + \dfrac{m}{\left|s_2\right|} + \dfrac{m-t}{m}\right) &amp; \text{otherwise} \end{cases} $$</code></p>
<p>其中，<code>$\left|s_i\right|$</code> 为字符串 <code>$s_i$</code> 的长度，<code>$m$</code> 为匹配的字符的个数，<code>$t$</code> 换位数目的一半。如果字符串 <code>$s_1$</code> 和 <code>$s_2$</code> 相差不超过 <code>$\lfloor \dfrac{\max \left(\left|s_1\right|, \left|s_2\right|\right)}{2} \rfloor - 1$</code>，我们则认为两个字符串是匹配的。例如，对于字符串 <strong>CRATE</strong> 和 <strong>TRACE</strong>，仅 <strong>R, A, E</strong> 三个字符是匹配的，因此 <code>$m = 3$</code>，尽管 <strong>C, T</strong> 均出现在两个字符串中，但是他们的距离超过了 1 (即，<code>$\lfloor \dfrac{5}{2} \rfloor - 1$</code>)，因此 <code>$t = 0$</code>。</p>
<p>Jaro-Winkler 相似度给予了起始部分相同的字符串更高的分数，其定义为：</p>
<p><code>$$ sim_w = sim_j + l p \left(1 - sim_j\right) $$</code></p>
<p>其中，<code>$sim_j$</code> 为字符串 <code>$s_1$</code> 和 <code>$s_2$</code> 的 Jaro 相似度，<code>$l$</code> 为共同前缀的长度 (规定不超过 <code>$4$</code>)，<code>$p$</code> 为调整系数 (规定不超过 <code>$0.25$</code>)，Winkler 将其设置为 <code>$p = 0.1$</code>。</p>
<ul>
<li><strong>汉明距离</strong></li>
</ul>
<p>汉明距离为两个<strong>等长字符串</strong>对应位置的不同字符的个数，也就是将一个字符串变换成另外一个字符串所需要<strong>替换</strong>的字符个数。例如：<strong>10<span style="color:#0000ff;">1</span>1<span style="color:#0000ff;">1</span>01</strong> 与 <strong>10<span style="color:#ff0000;">0</span>1<span style="color:#ff0000;">0</span>01</strong> 之间的汉明距离是 2，<strong>“<span style="color:#0000ff;">t</span>o<span style="color:#0000ff;">n</span>e<span style="color:#0000ff;">d</span>”</strong> 与 <strong>“<span style="color:#ff0000;">r</span>o<span style="color:#ff0000;">s</span>e<span style="color:#ff0000;">s</span>”</strong> 之间的汉明距离是 3。</p>
<pre><code class="language-python">import textdistance as td

s1 = '南京市长江大桥'
s2 = '北京市三元桥'

td.jaccard(s1, s2)
# 0.6666666666666666

td.sorensen_dice(s1, s2)
# 0.46153846153846156

td.tversky(s1, s2)
# 0.3

td.levenshtein(s1, s2)
# 4

td.jaro(s1, s2)
# 0.6428571428571429

td.hamming(s1, s2)
# 5
</code></pre>
<h3 id="表示学习">表示学习</h3>
<p>基于表示学习的文本相似度计算方法的思路如下：</p>
<ol>
<li>利用表示学习方法将不定长的文本表示为定长的实值向量。</li>
<li>计算转换后的实值向量相似度，用于表示两个文本的相似度。</li>
</ol>
<p>关于文本表示学习和实值向量相似度计算请参见之前博客：<a href="/cn/2018/10/word-embeddings/">词向量 (Word Embeddings)</a>，<a href="/cn/2019/01/similarity-and-distance-measurement/">相似性和距离度量 (Similarity &amp; Distance Measurement)</a>，<a href="/cn/2020/03/pre-trained-model-for-nlp/">预训练自然语言模型 (Pre-trained Models for NLP)</a>。</p>
<h2 id="文本词法-句法和语义角度">文本词法，句法和语义角度</h2>
<blockquote>
<p>本节主要参考自《基于词法、句法和语义的句子相似度计算方法》<sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup>。</p>
</blockquote>
<p>一段文本的内容分析由浅及深可以分为词法，句法和语义三个层次。</p>
<ol>
<li>词法，以词为对象，研究包括分词，词性和命名实体等。</li>
<li>句法，以句子为对象，研究包括句子成分和句子结构等。</li>
<li>语义，研究文字所表达的含义和蕴含的知识等。</li>
</ol>
<p>词法和句法可以统一成为语法，如下图所示：</p>
<p><img src="/images/cn/2020-10-31-text-similarity/lexical-syntax.png" alt=""></p>
<h3 id="词法">词法</h3>
<p>词法层以单个句子作为输入，其输出为已标记（词性，命名实体等）的词汇序列。</p>
<p><img src="/images/cn/2020-10-31-text-similarity/lexical-demo.png" alt=""></p>
<p>词汇序列的相似度计算可以采用上文中的距离度量等方式实现。</p>
<h3 id="句法">句法</h3>
<p>句法层用于研究句子各个组成部分及其排列顺序，将文本分解为句法单位，以理解句法元素的排列方式。句法层接收词法层分析后的将其转化为依存图。</p>
<p><img src="/images/cn/2020-10-31-text-similarity/syntax-demo.png" alt=""></p>
<p>对于依存图，我们可以利用三元组 <code>$S = \left(V_1, E, V_2\right)$</code> 表示任意一个依存关系，然后通过统计计算两个文本的依存图的三元组集合之间的相似度来评价句法层的相似度。此外，也可以从树结构的角度直接评价依存句法的相似度，更多细节可参考相关论文 <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup> <sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup>。</p>
<h3 id="语义">语义</h3>
<p>语义层用于研究文本所蕴含的意义。例如“父亲”和“爸爸”在词法层完全不同，但在语义层却具有相同的含义。针对语义相似度的两种深度学习范式如下：</p>
<p><img src="/images/cn/2020-10-31-text-similarity/deep-learning-paradigms-for-text-similarity.png" alt=""></p>
<p>第一种范式首先通过神经网络获取文本的向量表示，再通过向量之间的相似度来衡量文本的语义相似度。这种范式在提取特征时不考虑另一个文本的信息，更适合做大规模的语义相似召回，例如：DSSM <sup id="fnref:13"><a href="#fn:13" class="footnote-ref" role="doc-noteref">13</a></sup>，ARC-I <sup id="fnref:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup>，CNTN <sup id="fnref:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup>，LSTM-RNN <sup id="fnref:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup> 等。</p>
<p>第二种范式首先通过深度模型提取两个文本的交叉特征，得到匹配信号张量，再聚合为匹配分数。这种范式同时考虑两个文本的输入信息，更适合做小规模的语义相似精排，例如：ARC-II <sup id="fnref1:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup>，MatchPyramid <sup id="fnref:17"><a href="#fn:17" class="footnote-ref" role="doc-noteref">17</a></sup>，Match-SRNN <sup id="fnref:18"><a href="#fn:18" class="footnote-ref" role="doc-noteref">18</a></sup>，Duet <sup id="fnref:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup> 等。</p>
<h2 id="文本长度角度">文本长度角度</h2>
<p>从文本长度角度出发，我们可以粗略的将文本分类为<strong>短文本</strong>和<strong>长文本</strong>。<strong>短文本</strong>包括“字词”，“短语”，“句子”等相对比较短的文本形式，<strong>长文本</strong>包括“段落”，“篇章”等相对比较长的文本形式。</p>
<h3 id="短文本-v-s-短文本">短文本 v.s. 短文本</h3>
<p>短文本同短文本的常见比较形式有：关键词（字词）同文本标题（句子）的匹配，相似查询（句子）的匹配等。如果单纯的希望获取字符层面的差异，可以通过距离度量进行相似度比较。如果需要从语义的角度获取相似度，则可以利用表示学习对需要比对的文本进行表示，在通过语义向量之间的相似程度来衡量原始文本之间的相似度，详情可参见上文。</p>
<h3 id="短文本-v-s-长文本">短文本 v.s. 长文本</h3>
<p>短文本同长文本的比较多见于文档的搜索，即给定相关的查询（字词），给出最相关的文档（段落和篇章）。对于这类问题常见的解决方式是对长文本利用 TF-IDF，BM25等方法或进行主题建模后，再同查询的关键词进行匹配计算相似度度。</p>
<h3 id="长文本-v-s-长文本">长文本 v.s. 长文本</h3>
<p>长文本同长文本的比较多见于文档的匹配和去重，对于这类问题常见的解决方式是利用关键词提取获取长文本的特征向量，然后利用特征向量之间的相似度衡量对应文本的相似程度。在针对海量文本的去重，还以应用 <a href="/cn/2020/08/nearest-neighbor-search/">SimHash</a> 等技术对文本生成一个指纹，从而实现快速去重。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://zh.wikipedia.org/wiki/%E4%B8%BB%E9%A2%98%E6%A8%A1%E5%9E%8B">https://zh.wikipedia.org/wiki/主题模型</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Manning, C. D., Schütze, H., &amp; Raghavan, P. (2008). <em>Introduction to information retrieval</em>. Cambridge university press.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Mihalcea, R., &amp; Tarau, P. (2004, July). Textrank: Bringing order into text. In <em>Proceedings of the 2004 conference on empirical methods in natural language processing</em> (pp. 404-411).&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Page, L., Brin, S., Motwani, R., &amp; Winograd, T. (1999). <em>The PageRank citation ranking: Bringing order to the web</em>. Stanford InfoLab.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Deerwester, S., Dumais, S. T., Furnas, G. W., Landauer, T. K., &amp; Harshman, R. (1990). Indexing by latent semantic analysis. <em>Journal of the American society for information science</em>, 41(6), 391-407.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Hofmann, T. (1999, August). Probabilistic latent semantic indexing. In <em>Proceedings of the 22nd annual international ACM SIGIR conference on Research and development in information retrieval</em> (pp. 50-57).&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Blei, D. M., Ng, A. Y., &amp; Jordan, M. I. (2003). Latent dirichlet allocation. <em>Journal of machine Learning research</em>, 3(Jan), 993-1022.&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Rickjin(靳志辉). 2013. LDA数学八卦&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Teh, Y. W., Jordan, M. I., Beal, M. J., &amp; Blei, D. M. (2006). Hierarchical dirichlet processes. <em>Journal of the american statistical association</em>, 101(476), 1566-1581.&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>翟社平, 李兆兆, 段宏宇, 李婧, &amp; 董迪迪. (2019). 基于词法, 句法和语义的句子相似度计算方法. <em>东南大学学报: 自然科学版</em>, 49(6), 1094-1100.&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Zhang, K., &amp; Shasha, D. (1989). Simple fast algorithms for the editing distance between trees and related problems. <em>SIAM journal on computing</em>, 18(6), 1245-1262.&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p>Meila, M., &amp; Jordan, M. I. (2000). Learning with mixtures of trees. <em>Journal of Machine Learning Research</em>, 1(Oct), 1-48.&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:13">
<p>Huang, P. S., He, X., Gao, J., Deng, L., Acero, A., &amp; Heck, L. (2013, October). Learning deep structured semantic models for web search using clickthrough data. In <em>Proceedings of the 22nd ACM international conference on Information &amp; Knowledge Management</em> (pp. 2333-2338).&#160;<a href="#fnref:13" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:14">
<p>Hu, B., Lu, Z., Li, H., &amp; Chen, Q. (2014). Convolutional neural network architectures for matching natural language sentences. In <em>Advances in neural information processing systems</em> (pp. 2042-2050).&#160;<a href="#fnref:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:15">
<p>Qiu, X., &amp; Huang, X. (2015, June). Convolutional neural tensor network architecture for community-based question answering. In <em>Twenty-Fourth international joint conference on artificial intelligence</em>.&#160;<a href="#fnref:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:16">
<p>Palangi, H., Deng, L., Shen, Y., Gao, J., He, X., Chen, J., &hellip; &amp; Ward, R. (2016). Deep sentence embedding using long short-term memory networks: Analysis and application to information retrieval. <em>IEEE/ACM Transactions on Audio, Speech, and Language Processing</em>, 24(4), 694-707.&#160;<a href="#fnref:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:17">
<p>Pang, L., Lan, Y., Guo, J., Xu, J., Wan, S., &amp; Cheng, X. (2016). Text matching as image recognition. In <em>Proceedings of the Thirtieth AAAI Conference on Artificial Intelligence (AAAI'16)</em>. (pp. 2793–2799).&#160;<a href="#fnref:17" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:18">
<p>Wan, S., Lan, Y., Xu, J., Guo, J., Pang, L., &amp; Cheng, X. (2016, July). Match-SRNN: modeling the recursive matching structure with spatial RNN. In <em>Proceedings of the Twenty-Fifth International Joint Conference on Artificial Intelligence</em> (pp. 2922-2928).&#160;<a href="#fnref:18" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:19">
<p>Mitra, B., Diaz, F., &amp; Craswell, N. (2017, April). Learning to match using local and distributed representations of text for web search. In <em>Proceedings of the 26th International Conference on World Wide Web</em> (pp. 1291-1299).&#160;<a href="#fnref:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>而立之前 (Life before 30)</title><link>https://zeqiang.fun/cn/2020/08/life-before-30/</link><pubDate>Sat, 15 Aug 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/08/life-before-30/</guid><description><![CDATA[
        <p>没几个月就步入而立之年，古有云三十岁能自立于世，诚然不易。<strong>「立」</strong>，自当有成家立业之意，在当今社会，无论是客观因素还是主观因素，大家对成家立业的看法和行动步伐都略有差异，在此就不再发表愚见了。但我认为**「立身」**是我们都应该去思考和行动的事情，我们应该对于内在的自我和外部的社会有一个比较明确的认知，知道在社会中立足应该去做什么事、喜欢去做什么事、擅长去做什么事，然后不畏惧、不妥协、勇敢去做。</p>
<p>有时候会自嘲没有远大的志向和野心，但这样其实很真实，我认为平淡是人生的常态，脑海中记忆更深的才是平淡之外的苦难和幸福。虛泛的东西写多了容易变成鸡汤，简单整理几块内容省身克己，如于他人有益，实则万幸。</p>
<h2 id="三观">三观</h2>
<p>总是在说三观，其实容易泛泛而谈，我认为三观会从很大程度上决定应该去做什么、喜欢去做什么，甚至会影响到擅长去做什么。</p>
<h3 id="世界观">世界观</h3>
<p>世界观是我们对于宇宙的根本看法，我认为这是进行沟通的一个基础，如果两个人在这个层面上就处在对立的位置，感觉无论沟通什么都会失去意义。世界观的两个根本对立即唯物主义和唯心主义，我自己不在任意一个极端，非要占个队，我应该会略微偏向唯物主义。毕竟鸡不打鸣太阳照常会升起，但如果我看不到，于我又有什么意义。</p>
<p>在我认识的人中，没有遇见太过极端的，更多的可能是受到民族、信仰、地域等因素的长期影响吧，或多或少都会有些差异。我认为只要不处在完全的对立面，一切都还是可以去思考、去沟通、去辩解的。</p>
<h3 id="人生观">人生观</h3>
<p>生而为赢（Born to Win）是高中年代看过的一篇文章，很鸡血，也着实影响了自己的求学年代。但随着环境的改变和自己人生的实践，对于人生态度、目的和意义的思考也在不断转变。现在来看，生而为赢依旧没错，拼搏和奋斗才能创造平淡之中的幸福。当前的我想要追求的是平淡之中不乏波澜，或好或坏，同时在为之努力的过程中也不要给别人添堵，说白了不能损人利己，损人不利己就更要唾弃了。</p>
<p>「认命」是我最近比较喜欢的一个词，也是最近更新了对其理解的词。其实“认命”并不消极，尽人事，听天命，积极做事，结果往往会受到多方面的影响，成则已，不成换个路子继续尽人事。人生观相比世界观可能更容易发生变化，虽然没有世界观那么基础，但我认为对待人生观也应该更多的从“功利主义”而非“个人主义”角度出发，如果从这个层面上就背离社会大多数认可的正道，那么就会容易走上邪路，危害他人，这不就真给人添堵了吗。</p>
<h3 id="价值观">价值观</h3>
<p>孰好孰坏、孰优孰劣真的不是一个很容易回答的问题。面对比较基础的问题，只要不违背世界观和人生观，还是能够比较明确的给出一个结论的，但细到很小很具体的问题上，每个人的见解就不同了。所以，对待大是大非、大善大恶同上面一样，我更倾向于功利主义，对于小事，我认为要做到有理有据、不自欺欺人，那么就无伤大雅。</p>
<p>价值观包含物质性和精神性两大种类，我想两者也不必割裂而谈，不必不食人间烟火，能与朋友大快朵颐，亦能与朋友谈天论地，岂不快哉？</p>
<blockquote>
<p>做有意义的事，不给人添堵，什么有意义，需要自己去思考。</p>
</blockquote>
<h2 id="思考">思考</h2>
<p>要思考，但也别想太多，多思考做人做事自然是好的，但是天马行空的想太多，容易出现各种妄想症，我略受过其害。</p>
<h3 id="思辨胜于对错">思辨胜于对错</h3>
<p>做算法久了，虽然越来越需要更深入的了解业务，但同业务掰扯的时候还是不多的，更多的是围绕抽象出来的问题在做事。做这些事情需要更多的是专业力，是非对错就是一个量化评估的过程，但真遇到一些方向上的问题，或是与人更相关的问题，对错的判断就显得<a href="/cn/2018/06/play-safe-smart-choice-and-yuppie/">不是那么容易</a>。</p>
<p>即便将来一直沿着技术专家路线发展下去，我认为对上层问题的思考终将占据更重要的地位，而且也如实在这样发展。所以要有一套自己衡量评价事情的方法论，是非对错的评价标准因我们的世界观、人生观、价值观不同而有差异，但方法论还是能够有一定通用性的。方法论并不虚，就像我们做数据挖掘用的 <a href="https://en.wikipedia.org/wiki/Cross-industry_standard_process_for_data_mining">CRISP-DM</a> 一样，能够帮助我们更加客观、全面、系统的去做事。思考如何去评判，感觉比一味地争辩什么是对、什么是错更有意义。</p>
<h3 id="永不停止">永不停止</h3>
<p>无论处于顺势还是逆势，都不要停止思考，顺势思考如何锦上添花，逆势思考如何雪中送炭。浑浑噩噩、自欺欺人、得过且过是最不可取的。读万卷书、行万里路、交万名友，成本最低的就是读万卷书，思考和读书类似，除了时间没什么太大的花销，就看你想不想。</p>
<blockquote>
<p>生命不息，思考不止。</p>
</blockquote>
<h2 id="自由">自由</h2>
<p>自由有很多种，财务自由、意志自由，谈及大多数自由的时候都是我们所追求的。当然自由的首要前提正如《人权宣言》里面说的：「自由即有权做一切<strong>无害于他人</strong>的任何事情」。也就是自由并不是为所欲为，当然哪怕无害于他人，肆意挥霍、信口开河也是应该是被唾弃的。</p>
<p>读胡适的《容忍与自由》，感觉<strong>成事</strong>要「养成能够容忍谅解别人的见解的度量」，不要「以吾辈所主张这为绝对之是」。尤其进入到一个新的领域，要谦逊但不谦卑地去学习和理解，懂得尊重和敬畏即为容忍。</p>

<link rel="stylesheet" href="/css/douban-card.css" />


<div class='douban-card douban-card-book' id='douban-card-book-26674343' douban-id='26674343'></div>

<p>我是一个比较喜欢用数据说话的人，但有时也正是这种“所谓的有理有据”会让我“怼人”怼得理所当然。当想要让别人容忍谅解我们的见解，我们自己应该先做到这一点，凡事没有绝对，哪怕<a href="https://zh.wikipedia.org/wiki/%E5%87%A0%E4%B9%8E%E5%BF%85%E7%84%B6">概率是一也不一定必然发生</a>，数据说话是好事，只是不要被数字本身所蒙蔽就好。</p>
<blockquote>
<p>我年纪越大，越感觉到容忍比自由更重要</p>
</blockquote>

        ]]></description></item><item><title>最近邻搜索 (Nearest Neighbor Search)</title><link>https://zeqiang.fun/cn/2020/08/nearest-neighbor-search/</link><pubDate>Sat, 01 Aug 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/08/nearest-neighbor-search/</guid><description><![CDATA[
        <p>**最近邻搜索（Nearest Neighbor Search）**是指在一个确定的距离度量和一个搜索空间内寻找与给定查询项距离最小的元素。更精确地，对于一个包含 <code>$N$</code> 个元素的集合 <code>$\mathcal{X} = \left\{\mathbf{x}_1, \mathbf{x}_2, \cdots, \mathbf{x}_n\right\}$</code>，给定查询项 <code>$\mathbf{q}$</code> 的最近邻 <code>$NN \left(\mathbf{q}\right) = \arg\min_{\mathbf{x} \in \mathcal{X}} dist \left(\mathbf{q}, \mathbf{x}\right)$</code>，其中 <code>$dist \left(\mathbf{q}, \mathbf{x}\right)$</code> 为 <code>$\mathbf{q}$</code> 和 <code>$\mathbf{x}$</code> 之间的距离。由于<a href="/cn/2018/10/word-embeddings/#%E7%BB%B4%E6%95%B0%E7%81%BE%E9%9A%BE-the-curse-of-dimensionality">维数灾难</a>，我们很难在高维欧式空间中以较小的代价找到精确的最近邻。**近似最近邻搜索（Approximate Nearest Neighbor Search）**则是一种通过牺牲精度来换取时间和空间的方式从大量样本中获取最近邻的方法。</p>
<h2 id="精确搜索">精确搜索</h2>
<h3 id="暴力查找-brute-force-search">暴力查找（Brute-force Search）</h3>
<p>最简单的最邻近搜索便是遍历整个点集，计算它们和目标点之间的距离，同时记录目前的最近点。这样的算法较为初级，可以为较小规模的点集所用，但是对于点集的尺寸和空间的维数稍大的情况则不适用。对于 <code>$D$</code> 维的 <code>$N$</code> 个样本而言，暴力查找方法的复杂度为 <code>$O \left(DN\right)$</code>。</p>
<h3 id="k-d-树">k-D 树</h3>
<p>k-D 树（k-Dimesion Tree）<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 是一种可以高效处理 <code>$k$</code> 维空间信息的数据结构。k-D 树具有二叉搜索树的形态，二叉搜索树上的每个结点都对应 <code>$k$</code> 维空间内的一个点。其每个子树中的点都在一个 <code>$k$</code> 维的超长方体内，这个超长方体内的所有点也都在这个子树中。k-D 树的构建过程如下：</p>
<ol>
<li>若当前超长方体中只有一个点，返回这个点。</li>
<li>选择一个维度，将当前超长方体按照这个维度分割为两个超长方体。</li>
<li>选择一个切割点，将小于这个点的归入其中一个超长方体（左子树），其余归入另一个超长方体（右子树）。</li>
<li>递归地对分出的两个超长方体构建左右子树。</li>
</ol>
<p>一个 <code>$k = 2$</code> 的例子如下：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/k-d-tree.png" class="lazyload"/>
  
</figure>
<p>构建 k-D 树目前最优方法的时间复杂度为 <code>$O \left(n \log n\right)$</code>。对于单次查询，当 <code>$2$</code> 维时，查询时间复杂度最优为 <code>$O \left(\log n\right)$</code>，最坏为 <code>$O \left(\sqrt{n}\right)$</code>，扩展至 <code>$k$</code> 维，最坏为 <code>$O \left(n^{1 - \frac{1}{k}}\right)$</code>。k-D 树对于低维度最近邻搜索比较好，但当 <code>$k$</code> 增长到很大时，搜索的效率就变得很低，这也是“维数灾难”的一种体现。</p>
<h3 id="ball-树">Ball 树</h3>
<p>为了解决 k-D 树在高维数据上的问题，Ball 树 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 结构被提了出来。k-D 树是沿着笛卡尔积（坐标轴）方向迭代分割数据，而 Ball 树是通过一系列的超球体分割数据而非超长方体。Ball 树的构建过程如下：</p>
<ol>
<li>若当前超球体中只有一个点，返回这个点。</li>
<li>定义所有点的质心为 <code>$c$</code>，离质心 <code>$c$</code> 最远的点为 <code>$c_1$</code>，离 <code>$c_1$</code> 最远的点为 <code>$c_2$</code>。</li>
<li>将 <code>$c_1$</code> 和 <code>$c_2$</code> 作为聚类中心对数据点进行聚类得到两个簇 <code>$\left(c_1, r_1\right), \left(c_2, r_2\right)$</code>，将其归入左子树和右子树，其中 <code>$r$</code> 为超球的半径。</li>
<li>递归的对分出的两个超球体构建左右子树。</li>
</ol>
<p>一个二维的例子如下：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/ball-tree.png" class="lazyload"/>
  
</figure>
<p>每个点必须只能隶属于一个簇，但不同簇的超球体之间是可以相交的。在利用 Ball 树进行查询时，首先自上而下的找到包含查询点的叶子簇 <code>$\left(c, r\right)$</code>，在这个簇中找到距离查询点最近的观测点，这两个点的距离 <code>$d_{upper}$</code> 即为<strong>最近邻的距离上界</strong>。之后检查该叶子簇的所有兄弟簇是否包含比这个上界更小的观测点，在检查时，如果查询节点距离兄弟簇圆心的距离大于兄弟簇的半径与之前计算的上界 <code>$d_{upper}$</code> 之和，则这个兄弟节点不可能包含所需要的最近邻。</p>
<p>构建 Ball 树的时间复杂度为 <code>$O \left(n \left(\log n\right)^2\right)$</code>，查询时间复杂度为 <code>$O \left(\log \left(n\right)\right)$</code>。</p>
<h2 id="近似搜索">近似搜索</h2>
<h3 id="基于哈希的算法">基于哈希的算法</h3>
<p>基于哈希的算法的目标是将一个高维数据点转换为哈希编码的表示方式，主要包含两类方法：<strong>局部敏感哈希（Local Sensitive Hash, LSH）<strong>和</strong>哈希学习（Learning to Hash, L2H）</strong>。</p>
<h4 id="局部敏感哈希">局部敏感哈希</h4>
<p>局部敏感哈希采用的是与数据无关的哈希函数，也就是说整个学习处理过程不依赖于任何的数据内容信息。LSH 通过一个局部敏感哈希函数将相似的数据点以更高的概率映射到相同的哈希编码上去。这样我们在进行查询时就可以先找到查询样本落入那个哈希桶，然后再在这个哈希桶内进行遍历比较就可以找到最近邻了。</p>
<p>要使得相近的数据点通过哈希后落入相同的桶中，哈希函数需要满足如下条件：</p>
<ol>
<li>如果 <code>$d \left(x, y\right) \leq d_1$</code>，则 <code>$Pr \left[h \left(x\right), h \left(y\right)\right] \geq p_1$</code>。</li>
<li>如果 <code>$d \left(x, y\right) \geq d_2$</code>，则 <code>$Pr \left[h \left(x\right), h \left(y\right)\right] \leq p_2$</code>。</li>
</ol>
<p>其中，<code>$x, y \in \mathbb{R}^n$</code> 表示 <code>$n$</code> 维度数据点，<code>$d \left(x, y\right)$</code> 表示 <code>$x, y$</code> 之间的距离，<code>$h$</code> 为哈希函数。满足上述两个条件的哈希函数称为是 <code>$\left(d_1, d_2, p_1, p_2\right)$</code> 敏感的。</p>
<ul>
<li>MinHash（Jaccard 距离）</li>
</ul>
<p>MinHash 算法的思路是：采用一种哈希函数将元素的位置均匀打乱，然后在新顺序下每个集合的第一个元素作为该集合的特征值。我们以 <code>$s_1 = \left\{a, d\right\}$</code>，<code>$s_2 = \left\{c\right\}$</code>，<code>$s_3 = \left\{b, d, e\right\}$</code>，<code>$s_4 = \left\{a, c, d\right\}$</code> 为例，集合中可能的元素为 <code>$\left\{a, b, c, d, e\right\}$</code>，则这四个集合可以表示为：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">元素</th>
          <th style="text-align: center"><code>$s_1$</code></th>
          <th style="text-align: center"><code>$s_2$</code></th>
          <th style="text-align: center"><code>$s_3$</code></th>
          <th style="text-align: center"><code>$s_4$</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center"><code>$a$</code></td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: center"><code>$b$</code></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center"><code>$c$</code></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: center"><code>$d$</code></td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: center"><code>$e$</code></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
      </tr>
  </tbody>
</table>
<p>对矩阵进行随机打乱后有：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">元素</th>
          <th style="text-align: center"><code>$s_1$</code></th>
          <th style="text-align: center"><code>$s_2$</code></th>
          <th style="text-align: center"><code>$s_3$</code></th>
          <th style="text-align: center"><code>$s_4$</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center"><code>$b$</code></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center"><code>$e$</code></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center"><code>$a$</code></td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: center"><code>$d$</code></td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: center"><code>$c$</code></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1</td>
      </tr>
  </tbody>
</table>
<p>我们利用每个集合的第一个元素作为该集合的特征值，则有 <code>$h \left(s_1\right) = a$</code>，<code>$h \left(s_2\right) = c$</code>，<code>$h \left(s_3\right) = b$</code>，<code>$h \left(s_4\right) = a$</code>，可以看出 <code>$h \left(s_1\right) = h \left(s_4\right)$</code>。MinHash 能够保证在哈希函数均匀分布的情况下，哈希值相等的概率等于两个集合的 Jaccard 相似度，即：</p>
<p><code>$$ Pr \left(MinHash \left(s_1\right) = MinHash \left(s_2\right)\right) = Jaccard \left(s_1, s_2\right) $$</code></p>
<ul>
<li>SimHash（汉明距离）</li>
</ul>
<p>SimHash 是由 Manku 等人 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 提出的一种用于用于进行网页去重的哈希算法。SimHash 作为局部敏感哈希算法的一种其主要思想是将高维特征映射到低维特征，再通过两个向量的汉明距离来确定是否存在重复或相似。算法步骤如下：</p>
<ol>
<li>对文本进行特征抽取（例如：分词），并为每个特征赋予一定的权重（例如：词频）。</li>
<li>计算每个特征的二进制哈希值。</li>
<li>计算加权后的哈希值，当哈希值为 1 时，则对应位置为 <code>$w_i$</code>，否则为 <code>$-w_i$</code>，其中 <code>$w_i$</code> 为该特征对应的权重。</li>
<li>将所有特征加权后的哈希值按对应的位置进行累加合并。</li>
<li>如果累加位置大于 0 则置为 1，否则置为 0，最终得到哈希结果。</li>
</ol>
<p>算法流程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/simhash.png" class="lazyload"/>
  
</figure>
<p>在得到 SimHash 的值后，我们可以通过比较之间的汉明距离来判断相似性。为了提高海量数据的去重效率，以 64 位指纹为例，我们可以将其切分为 4 份 16 位的数据块，根据<a href="https://zh.wikipedia.org/wiki/%E9%B4%BF%E5%B7%A2%E5%8E%9F%E7%90%86">鸽巢原理</a>，汉明距离为 3 的两个文档必定有一个数据块是相等的。将这 4 分数据利用 KV 数据库和倒排索引进行存储，Key 为 16 位的截断指纹，Value  为剩余的指纹集合，从而提高查询的效率。同时可以选择 16，8 和 4 位进行索引，位数越小越精确，但所需的存储空间越大。</p>
<ul>
<li>p-stable 分布（欧式距离）</li>
</ul>
<p>当一个在 <code>$\Re$</code> 上的分布 <code>$\mathcal{D}$</code> 为 <code>$p\text{-stable}$</code> 时，存在 <code>$p \geq 0$</code> 使得对于任意 <code>$n$</code> 个实数 <code>$v_1, \cdots, v_n$</code> 和独立同分布 <code>$\mathcal{D}$</code> 下的变量 <code>$X_1, \cdots, X_n$</code>，有随机变量 <code>$\sum_{i}{v_i X_i}$</code> 和 <code>$\left(\sum_{i}{\left|v_i\right|^p}\right)^{1/p} X$</code> 具有相同的分布，其中 <code>$X$</code> 为分布 <code>$\mathcal{D}$</code> 下的随机变量 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>。常见的 p-stable 分布有：</p>
<ol>
<li>柯西分布：密度函数为 <code>$c \left(x\right) = \dfrac{1}{\pi} \dfrac{1}{1 + x^2}$</code>，为 <code>$1\text{-stable}$</code>。</li>
<li>正态分布：密度函数为 <code>$g \left(x\right) = \dfrac{1}{\sqrt{2 \pi}} e^{-x^2 / 2}$</code>，为 <code>$2\text{-stable}$</code>。</li>
</ol>
<p>p-stable 分布主要可以用于估计 <code>$\left\|v\right\|_p$</code>，对于两个相似的 <code>$v_1, v_2$</code>，它们应该具有更小的 <code>$\left\|v_1 - v_2\right\|_p$</code>，也就是对应的哈希值有更大的概率发生碰撞。对于 <code>$v_1, v_2$</code>，距离的映射 <code>$a \cdot v_1 - a \cdot v_2$</code> 和 <code>$\left\|v_1 - v_2\right\|_p \cdot X$</code> 具有相同的分布。<code>$a \cdot v$</code> 将向量 <code>$v$</code> 映射到实数集，如果将实轴以宽度 <code>$w$</code> 进行等分，<code>$a \cdot v$</code> 落在哪个区间中就将其编号赋予它，这样构造的哈希函数具有局部保持特性。构造的哈希函数族的形式为：</p>
<p><code>$$ h_{a, b} \left(v\right) = \left\lfloor \dfrac{a \cdot v + b}{w} \right\rfloor $$</code></p>
<p>其中，向量 <code>$a$</code> 的元素 <code>$a_i \sim N \left(0, 1\right)$</code>，<code>$b \sim U \left(0, w\right)$</code>。令 <code>$c = \left\|u - v \right\|_p$</code>，则两个向量在被分配到一个桶中的概率为：</p>
<p><code>$$ Pr \left[h_{a, b} \left(u\right) = h_{a, b} \left(v\right)\right] = \int_{0}^{w} \dfrac{1}{c} \cdot f_p \left(\dfrac{t}{u}\right) \left(1 - \dfrac{t}{w}\right) dt $$</code></p>
<p>其中，<code>$f_p$</code> 为概率密度函数。从上式中不难看出，随着距离 <code>$c$</code> 的减小，两个向量发生碰撞的概率增加。</p>
<ul>
<li>相关问题</li>
</ul>
<p>局部敏感哈希可以在次线性时间内完成搜索，但缺点在于需要比较长的比特哈希码和比较多的哈希表才能达到预期的性能。</p>
<p>在单表哈希中，当哈希编码位数 <code>$K$</code> 过小时，每个哈希桶中数据个数较多，从而会增加查询的响应时间。当哈希编码位数 <code>$K$</code> 较大时，查询样本同最近邻落入同一个桶中的概率会很小。针对这个问题，我们可以通过重复 <code>$L$</code> 次来增加最近邻的召回率。这个操作可以转化为构建 <code>$L$</code> 个哈希表，给定一个查询样本，我们可以找到 <code>$L$</code> 个哈希桶，然后再遍历这 <code>$L$</code> 个哈希桶中的数据。但这样会增加内存的消耗，因此需要选择合理的 <code>$K$</code> 和 <code>$L$</code> 来获得更好的性能。</p>
<p>Multi-probe LSH <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> 引入了一种新的策略解决召回的问题。Multi-probe LSH 不仅仅会遍历查询样本所在桶内的元素，同时还会查询一些其他有可能包含最近邻的桶，从而在避免构建多个哈希表的情况下增加召回率。</p>
<h4 id="哈希学习">哈希学习</h4>
<p>哈希学习（Learning to Hash）是由 Salakhutdinov 和 Hinton <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 引入到机器学习领域，通过机器学习机制将数据映射成二进制串的形式，能显著减少数据的存储和通信开销，从而有效提高学习系统的效率 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup>。从原空间中的特征表示直接学习得到二进制的哈希编码是一个 NP-Hard 问题。现在很多的哈希学习方法都采用两步学习策略：</p>
<ol>
<li>先对原空间的样本采用度量学习（Metric Learning）进行降维，得到 1 个低维空间的实数向量表示。</li>
<li>对得到的实数向量进行量化（即离散化）得到二进制哈希码。</li>
</ol>
<p>现有的方法对第二步的处理大多很简单，即通过某个阈值函数将实数转换成二进制位。通常使用的量化方法为 1 个阈值为 0 的符号函数，即如果向量中某个元素大于 0，则该元素被量化为 1，否则如果小于或等于 0，则该元素被量化为 0。</p>
<p>哈希学习相关的具体算法不再一一展开，更多细节请参见下文提供的相关 Survey。</p>
<h3 id="矢量量化算法">矢量量化算法</h3>
<p>**矢量量化（Vector Quantization）**是信息论中一种用于数据压缩的方法，其目的是减少表示空间的维度。一个量化器可以表示为由 <code>$D$</code> 维向量 <code>$x \in \mathbb{R}^D$</code> 到一个向量 <code>$q \left(x\right) \in \mathcal{C} = \left\{c_i; i \in \mathcal{I}\right\}$</code> 的映射 <code>$q$</code>，其中下标集合 <code>$\mathcal{I}$</code> 为有限集合，即 <code>$\mathcal{I} = 0, \cdots, k-1$</code>。<code>$c_i$</code> 称之为形心（centroids），<code>$\mathcal{C}$</code> 称之为大小为 <code>$k$</code> 的码本（codebook）。映射后的向量到一个给定下标 <code>$i$</code> 的集合 <code>$\mathcal{V}_i \triangleq \left\{x \in \mathbb{R}^D: q \left(x\right) = c_i\right\}$</code>（Voronoi），称之为一个单元（cell）。</p>
<p>以一个图像编码为例，我们通过 K-Means 算法得到 <code>$k$</code> 个 centroids，然后用这些 centroids 的像素值来替换对应簇中所有点的像素值。当 <code>$k = 2, 10, 100$</code> 时，压缩后的图像和原始图像的对比结果如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/lena-vq.png" class="lazyload"/>
  
</figure>
<p>当 <code>$k = 100$</code> 时，压缩后的图像和原始图像已经非常接近了，相关代码请参见<a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2020-08-01-nearest-neighbor-search/vector-quantization.py">这里</a>。</p>
<p>矢量量化以乘积量化（Product Quantization，PQ）最为典型，乘积量化的核心思想还是聚类，乘积量化生成码本和量化过程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/product-quantization.png" class="lazyload"/>
  
</figure>
<p>在训练阶段，以维度为 128 的 <code>$N$</code> 个样本为例，我们将其切分为 4 个子空间，则每个子空间的维度为 32 维。对每一个子空间利用 K-Means 对其进行聚类，令聚类个数为 256，这样每个子空间就能得到一个 256 大小的码本。样本的每个子段都可以用子空间的聚类中心来近似，对应的编码即为类中心的 ID。利用这种编码方式可以将样本用一个很短的编码进行表示，从而达到量化的目的。</p>
<p>在查询阶段，我们将查询样本分成相同的子段，然后在每个子空间中计算子段到该子空间中所有聚类中心的距离，这样我们就得到了 <code>$4 \times 256$</code> 个距离。在计算某个样本到查询样本的距离时，我们仅需要从计算得到的 4 组距离中将对应编码的距离取出相加即可，所有距离计算完毕排序后即可得到结果。</p>
<p>乘积量化有两种计算距离的方式 <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup>：<strong>对称距离</strong>和<strong>非对称距离</strong>，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/vq-symmetric-asymmetric-distance.png" class="lazyload"/>
  
</figure>
<p>对于 <code>$x$</code> 和 <code>$y$</code> 的距离 <code>$d \left(x, y\right)$</code>，对称距离利用 <code>$d \left(q \left(x\right), q \left(y\right)\right)$</code> 进行估计，非对称距离利用 <code>$d \left(x, q \left(y\right)\right)$</code> 进行估计。对称距离和非对称距离在不同阶段的时间复杂度如下表所示：</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>对称距离</th>
          <th>非对称距离</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>编码 <code>$x$</code></td>
          <td><code>$k^* D$</code></td>
          <td>0</td>
      </tr>
      <tr>
          <td>计算 <code>$d \left(u_j \left(x\right), c_{j, i}\right)$</code></td>
          <td>0</td>
          <td><code>$k^* D$</code></td>
      </tr>
      <tr>
          <td>对于 <code>$y \in \mathcal{Y}$</code>，计算 <code>$\hat{d} \left(x, y\right)$</code> 或 <code>$\tilde{d} \left(x, y\right)$</code></td>
          <td><code>$nm$</code></td>
          <td><code>$nm$</code></td>
      </tr>
      <tr>
          <td>查找最小 <code>$k$</code> 个距离</td>
          <td><code>$n + k$</code></td>
          <td><code>$\log k \log \log n$</code></td>
      </tr>
  </tbody>
</table>
<p>其中，<code>$k^*$</code> 为 centroids 个数，<code>$D$</code> 为向量维度，<code>$n$</code> 为样本个数，<code>$m$</code> 为分割个数。通常情况下我们采用非对称距离，其更接近真实距离。</p>
<p>IVFADC <sup id="fnref1:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> 是乘积量化的的加速版本，乘积量化在计算距离时仍需逐个遍历相加计算。倒排乘积量化首先对 <code>$N$</code> 个样本采用 K-Means 进行聚类，此处的聚类中心相比乘积量化应设置较小的数值。在得到聚类中心后，针对每一个样本 <code>$x_i$</code> 找到距离最近的类中心 <code>$c_i$</code>，两者相减后得到残差 <code>$x_i - c_i$</code>，然后对残差再进行乘积量化的全过程。在查询阶段，通过先前较粗力度的量化快速定位隶属于哪一个 <code>$c_i$</code>，然后在 <code>$c_i$</code> 区域利用乘积量化获取最终结果。整个流程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/ivfadc.png" class="lazyload"/>
  
</figure>
<p>Optimized Product Quantization (OPQ) <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup> 是乘积量化的一个优化方法。通常用于检索的原始特征维度较高，实践中利用乘积量化之前会对高维特征利用 PCA 等方法进行降维处理。这样在降低维度的时候还能够使得对向量进行子段切分的时候各个维度不相关。在利用 PCA 降维后，采用顺序切分子段仍存在一些问题，以 Iterative Quantization (ITQ) <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup> 中的一个二维平面例子来说明，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/itq.png" class="lazyload"/>
  
</figure>
<p>在利用乘积量化进行编码时，对于切分的各个子空间，应尽可能使得各个子空间的方差接近。上图中 <code>$(a)$</code> 图在 x 和 y 轴上的方差较大，而 <code>$(c)$</code> 图在两个方向上比较接近。OPQ 致力解决的问题就是对各个子空间方差上的均衡，OPQ 对于该问题的求解分为非参数求解方法和参数求解方法两种，更多算法细节请参见 ITQ 和 OPQ 原文。</p>
<h3 id="基于图的算法">基于图的算法</h3>
<p>NSW（Navigable Small World）<sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup> 算法是一种由 Malkov 等人提出的基于图的索引的方法。我们将 Navigable Small World 网络表示为一个图 <code>$G \left(V, E\right)$</code>，其中数据集合 <code>$X$</code> 的点被唯一映射到集合 <code>$V$</code> 中的一条边，边集 <code>$E$</code> 由构造算法确定。对于与一个节点 <code>$v_i$</code> 共享一条边的所有节点，我们称之为该节点的“友集”。</p>
<p>之后我们可以利用一个贪婪搜索的变种算法实现一个基本的 KNN 搜索算法。通过选择友集中未被访问过的距离查询样本最近的节点，可以在图中一个接一个的访问不同的节点，直到达到停止准则。整个过程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/nsw.png" class="lazyload"/>
  
</figure>
<p>上图中的边扮演着两种不同的角色：</p>
<ol>
<li>短距离边的子集作为 <a href="https://en.wikipedia.org/wiki/Delaunay_triangulation">Delaunay 图</a>的近似用于贪婪搜索算法。</li>
<li>长距离边的子集用于对数尺度的贪婪搜索，负责构造图的 Navigable Small World 属性。</li>
</ol>
<p>其中，黑色的边为短距离边，红色的边为长距离边，箭头为迭代查询路径。整个结构的构建可以通过元素的连续插入实现，对于新的元素，我们从当前结构中找到最接近的邻居集合与之相连。随着越来越多的元素插入到结构中，之前的短距离连接就变成了长距离连接。</p>
<p>NSW 的 KNN 查询过程如下所示：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{NSW 的 KNN 查询}
\begin{algorithmic}
\REQUIRE 查询样本 $q$，查询结果数量 $k$，最大迭代次数 $m$
\STATE $V_{cand} \gets \varnothing, V_{visited} \gets \varnothing, V_{res} \gets \varnothing$
\FOR{$i \gets 1$ \TO $m$}
  \STATE $V_{tmp} \gets \varnothing$
  \STATE $v_{rand} \gets $ 随机初始节点
  \STATE $V_{cand} \gets V_{cand} \cup v_{rand}$
  \WHILE{True}
    \STATE $v_c \gets V_{cand}$ 中距离 $q$ 最近的元素
    \STATE $V_{cand} \gets V_{cand} \setminus v_c$
    \IF{$v_c$ 比结果中的 $k$ 个元素距离 $q$ 还远}
      \BREAK
    \ENDIF
    \FOR{$v_e \in v_c$ 的“友集”}
      \IF{$v_e \notin V_{visited}$}
        \STATE $V_{visited} \gets V_{visited} \cup v_e, V_{cand} \gets V_{cand} \cup v_e, V_{tmp} \gets V_{tmp} \cup v_e$
      \ENDIF
    \ENDFOR
  \ENDWHILE
  \STATE $V_{res} \gets V_{res} \cup V_{tmp}$
\ENDFOR
\RETURN{$V_{res}$ 中与查询样本最近的 $k$ 个元素}
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>HNSW（Hierarchical Navigable Small World）<sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup> 是对 NSW 的一种改进。HNSW 的思想是根据连接的长度（距离）将连接划分为不同的层，然后就可以在多层图中进行搜索。在这种结构中，搜索从较长的连接（上层）开始，贪婪地遍历所有元素直到达到局部最小值，之后再切换到较短的连接（下层），然后重复该过程，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/hnsw.png" class="lazyload"/>
  
</figure>
<p>利用这种结构可以将原来 NSW 的多重对数（Polylogarithmic）计算复杂度降低至对数（Logarithmic）复杂度。更多关于数据插入和搜索的细节请参见原文。</p>
<p>NSG <sup id="fnref:13"><a href="#fn:13" class="footnote-ref" role="doc-noteref">13</a></sup> 提出了一种新的图结构 Monotonic Relative Neighborhood Graph (MRNG) 用于保证一个平均的低搜索时间复杂度（接近对数复杂度）。同时为了进一步降低索引复杂度，作者从确保连接性、降低平均出度、缩短搜索路径和降低索引大小 4 个方面考虑，提出了一个用于近似 MRNG 的 Spreading-out Graph (NSG)。</p>
<p>基于图的方法 HNSW 和基于乘积量化的方法 OPQ 之间的特性对比如下：</p>
<table>
  <thead>
      <tr>
          <th>特点</th>
          <th>OPQ</th>
          <th>HNSW</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>内存占用</td>
          <td>小</td>
          <td>大</td>
      </tr>
      <tr>
          <td>召回率</td>
          <td>较高</td>
          <td>高</td>
      </tr>
      <tr>
          <td>数据动态增删</td>
          <td>灵活</td>
          <td>不易</td>
      </tr>
  </tbody>
</table>
<p>本文部分内容参考自 <a href="https://yongyuan.name/blog/vector-ann-search.html">图像检索：向量索引</a>。</p>
<h3 id="算法对比">算法对比</h3>
<p>常用算法的开源实现的评测如下，更多评测结果请参见 <a href="https://github.com/erikbern/ann-benchmarks/">erikbern/ann-benchmarks</a>。</p>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/glove-100-k-10.png" class="lazyload"/>
  <figcaption><p class="figcaption">Glove-100-Angular (K=10)</p></figcaption>
</figure>
<figure>
  <img data-src="/images/cn/2020-08-01-nearest-neighbor-search/sift-128-k-10.png" class="lazyload"/>
  <figcaption><p class="figcaption">SIFT-128-Euclidean (K=10)</p></figcaption>
</figure>
<h2 id="开放资源">开放资源</h2>
<h3 id="survey">Survey</h3>
<ul>
<li>A Survey on Learning to Hash <sup id="fnref:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup></li>
<li>A Survey on Nearest Neighbor Search Methods <sup id="fnref:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup></li>
<li>An Investigation of Practical Approximate Nearest Neighbor Algorithms <sup id="fnref:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup></li>
<li>Approximate Nearest Neighbor Search on High Dimensional Data - Experiments, Analyses, and Improvement <sup id="fnref:17"><a href="#fn:17" class="footnote-ref" role="doc-noteref">17</a></sup></li>
<li>Binary Hashing for Approximate Nearest Neighbor Search on Big Data: A Survey <sup id="fnref:18"><a href="#fn:18" class="footnote-ref" role="doc-noteref">18</a></sup></li>
<li>Hashing for Similarity Search: A Survey <sup id="fnref:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup></li>
</ul>
<h3 id="开源库">开源库</h3>
<table>
  <thead>
      <tr>
          <th>库</th>
          <th>API</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://github.com/spotify/annoy">spotify/annoy</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i>, <i class="icon icon-go">Go</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/vioshyvo/mrpt">vioshyvo/mrpt</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i>, <i class="icon icon-go"><a href="https://github.com/rikonor/go-ann">Go</a></i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/pixelogik/NearPy">pixelogik/NearPy</a></td>
          <td><i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/aaalgo/kgraph">aaalgo/kgraph</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/nmslib/nmslib">nmslib/nmslib</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/nmslib/hnswlib">nmslib/hnswlib</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/lyst/rpforest">lyst/rpforest</a></td>
          <td><i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/facebookresearch/faiss">facebookresearch/faiss</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/ekzhu/datasketch">ekzhu/datasketch</a></td>
          <td><i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/lmcinnes/pynndescent">lmcinnes/pynndescent</a></td>
          <td><i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/yahoojapan/NGT">yahoojapan/NGT</a></td>
          <td><i class="icon icon-c">C</i>, <i class="icon icon-cpp">C++</i>, <i class="icon icon-python"><a href="https://github.com/yahoojapan/NGT/blob/master/python/README.md">Python</a></i>, <i class="icon icon-go"><a href="https://github.com/yahoojapan/gongt">Go</a></i>, <i class="icon icon-ruby"><a href="https://github.com/ankane/ngt">Ruby</a></i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/microsoft/SPTAG">microsoft/SPTAG</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/puffinn/puffinn">puffinn/puffinn</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/kakao/n2">kakao/n2</a></td>
          <td><i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i>, <i class="icon icon-go">Go</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/ZJULearning/nsg">ZJULearning/nsg</a></td>
          <td><i class="icon icon-cpp">C++</i></td>
      </tr>
  </tbody>
</table>
<h3 id="开源搜索引擎">开源搜索引擎</h3>
<table>
  <thead>
      <tr>
          <th>搜索引擎</th>
          <th>API</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://github.com/milvus-io/milvus">milvus-io/milvus</a></td>
          <td><i class="icon icon-c">C</i>, <i class="icon icon-cpp">C++</i>, <i class="icon icon-python">Python</i>, <i class="icon icon-java">Java</i><br/><i class="icon icon-go">Go</i>, <i class="icon icon-nodejs">Node.js</i>, <i class="icon icon-restful">RESTful API</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/vearch/vearch">vearch/vearch</a></td>
          <td><i class="icon icon-python">Python</i>, <i class="icon icon-go">Go</i></td>
      </tr>
  </tbody>
</table>
<h3 id="评测">评测</h3>
<ul>
<li><a href="https://github.com/erikbern/ann-benchmarks/">https://github.com/erikbern/ann-benchmarks/</a></li>
<li><a href="https://github.com/DBWangGroupUNSW/nns_benchmark">https://github.com/DBWangGroupUNSW/nns_benchmark</a></li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Bentley, J. L. (1975). Multidimensional binary search trees used for associative searching. <em>Communications of the ACM</em>, 18(9), 509-517.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Omohundro, S. M. (1989). <em>Five balltree construction algorithms</em> (pp. 1-22). Berkeley: International Computer Science Institute.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Manku, G. S., Jain, A., &amp; Das Sarma, A. (2007, May). Detecting near-duplicates for web crawling. In <em>Proceedings of the 16th international conference on World Wide Web</em> (pp. 141-150).&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Datar, M., Immorlica, N., Indyk, P., &amp; Mirrokni, V. S. (2004, June). Locality-sensitive hashing scheme based on p-stable distributions. In <em>Proceedings of the twentieth annual symposium on Computational geometry</em> (pp. 253-262).&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Lv, Q., Josephson, W., Wang, Z., Charikar, M., &amp; Li, K. (2007, September). Multi-probe LSH: efficient indexing for high-dimensional similarity search. In <em>Proceedings of the 33rd international conference on Very large data bases</em> (pp. 950-961).&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Salakhutdinov, Ruslan, and Geoffrey Hinton. &ldquo;Semantic hashing.&rdquo; <em>International Journal of Approximate Reasoning</em> 50.7 (2009): 969-978.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>李武军, &amp; 周志华. (2015). 大数据哈希学习: 现状与趋势. <em>科学通报, 60</em>(5-6), 485-490.&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Jegou, H., Douze, M., &amp; Schmid, C. (2010). Product quantization for nearest neighbor search. <em>IEEE transactions on pattern analysis and machine intelligence</em>, 33(1), 117-128.&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Ge, T., He, K., Ke, Q., &amp; Sun, J. (2013). Optimized product quantization. <em>IEEE transactions on pattern analysis and machine intelligence</em>, 36(4), 744-755.&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Gong, Y., Lazebnik, S., Gordo, A., &amp; Perronnin, F. (2012). Iterative quantization: A procrustean approach to learning binary codes for large-scale image retrieval. <em>IEEE transactions on pattern analysis and machine intelligence, 35</em>(12), 2916-2929.&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Malkov, Y., Ponomarenko, A., Logvinov, A., &amp; Krylov, V. (2014). Approximate nearest neighbor algorithm based on navigable small world graphs. <em>Information Systems</em>, 45, 61-68.&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p>Malkov, Y. A., &amp; Yashunin, D. A. (2018). Efficient and robust approximate nearest neighbor search using hierarchical navigable small world graphs. <em>IEEE transactions on pattern analysis and machine intelligence</em>.&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:13">
<p>Fu, C., Xiang, C., Wang, C., &amp; Cai, D. (2019). Fast approximate nearest neighbor search with the navigating spreading-out graph. <em>Proceedings of the VLDB Endowment, 12</em>(5), 461-474.&#160;<a href="#fnref:13" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:14">
<p>Wang, J., Zhang, T., Sebe, N., &amp; Shen, H. T. (2017). A survey on learning to hash. <em>IEEE transactions on pattern analysis and machine intelligence, 40</em>(4), 769-790.&#160;<a href="#fnref:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:15">
<p>Reza, M., Ghahremani, B., &amp; Naderi, H. (2014). A Survey on nearest neighbor search methods. <em>International Journal of Computer Applications, 95</em>(25), 39-52.&#160;<a href="#fnref:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:16">
<p>Liu, T., Moore, A. W., Yang, K., &amp; Gray, A. G. (2005). An investigation of practical approximate nearest neighbor algorithms. In <em>Advances in neural information processing systems</em> (pp. 825-832).&#160;<a href="#fnref:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:17">
<p>Li, W., Zhang, Y., Sun, Y., Wang, W., Li, M., Zhang, W., &amp; Lin, X. (2019). Approximate nearest neighbor search on high dimensional data-experiments, analyses, and improvement. <em>IEEE Transactions on Knowledge and Data Engineering</em>.&#160;<a href="#fnref:17" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:18">
<p>Cao, Y., Qi, H., Zhou, W., Kato, J., Li, K., Liu, X., &amp; Gui, J. (2017). Binary hashing for approximate nearest neighbor search on big data: A survey. <em>IEEE Access, 6</em>, 2039-2054.&#160;<a href="#fnref:18" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:19">
<p>Wang, J., Shen, H. T., Song, J., &amp; Ji, J. (2014). Hashing for similarity search: A survey. <em>arXiv preprint arXiv:1408.2927</em>.&#160;<a href="#fnref:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>无模型策略预测和控制 - 时序差分学习 (Model-Free Policy Prediction and Control - Temporal Difference Learning)</title><link>https://zeqiang.fun/cn/2020/07/model-free-policy-prediction-and-control-temporal-difference-learning/</link><pubDate>Sat, 11 Jul 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/07/model-free-policy-prediction-and-control-temporal-difference-learning/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">《强化学习系列》</a>文章<br>
本文内容主要参考自：<br>
1.《强化学习》<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup><br>
2. CS234: Reinforcement Learning <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup><br>
3. UCL Course on RL <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
</blockquote>
<h2 id="时序差分预测">时序差分预测</h2>
<p>**时序差分（Temporal Difference，TD）**和蒙特卡洛方法都利用经验来解决预测问题。给定策略 <code>$\pi$</code> 的一些经验，以及这些经验中的非终止状态 <code>$S_t$</code>，一个适用于非平稳环境的简单的每次访问型蒙特卡洛方法可以表示为：</p>
<p><code>$$ V\left(S_{t}\right) \gets V\left(S_{t}\right)+\alpha\left[G_{t}-V\left(S_{t}\right)\right] \label{eq:mc-update} $$</code></p>
<p>其中，<code>$G_t$</code> 是时刻 <code>$t$</code> 真实的回报，<code>$\alpha$</code> 是步长参数，称之为常量 <code>$\alpha$</code> MC。MC 需要等到一幕的结尾才能确定对 <code>$V \left(S_t\right)$</code> 的增量（此时才能获得 <code>$G_t$</code>），而 TD 则只需要等到下一个时刻即可。在 <code>$t+1$</code> 时刻，TD 使用观察到的收益 <code>$R_{t+1}$</code> 和估计值 <code>$V \left(S_{t+1}\right)$</code> 来进行一次有效更新：</p>
<p><code>$$ V\left(S_{t}\right) \gets V\left(S_{t}\right)+\alpha\left[R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right)\right] \label{eq:td-update} $$</code></p>
<p>这种 TD 方法称之为 TD(0)，算法的完整过程如下：</p>


<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/pseudocode@latest/build/pseudocode.min.css">


<div><pre class="pseudocode">
\begin{algorithm}
\caption{表格型 TD(0) 算法，用于估计 $V \approx v_{\pi}$}
\begin{algorithmic}
\REQUIRE 待评估策略 $\pi$
\STATE 对于所有 $s \in \mathcal{S}^+$，任意初始化 $V \left(s\right)$，其中 $V \left(\text{终止状态}\right) = 0$
\FOR{每一幕}
  \STATE 初始化 $S$
  \REPEAT
    \STATE $A \gets$ 策略 $\pi$ 在状态 $S$ 下做出的决策动作
    \STATE 执行动作 $A$，观测到 $R, S'$
    \STATE $V\left(S\right) \gets V\left(S\right)+\alpha\left[R+\gamma V\left(S^{\prime}\right)-V\left(S\right)\right]$
    \STATE $S \gets S'$
  \UNTIL{$S$ 为终止状态}
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>TD(0) 的更新在某种程度上基于已存在的估计，我们称之为一种<strong>自举法</strong>。</p>
<p>TD 和 MC 方法都能渐进地收敛于正确的预测，但两种方法谁收敛的更快，目前暂时未能证明。但在如下的随机任务上，TD 方法通常比常量 <code>$\alpha$</code> MC 方法收敛得更快。假设如下 MRP 所有阶段都同中心 C 开始，每个时刻以相同的概率向左或向右移动一个状态。幕终止于最左侧或最右侧，终止于最右侧时有 +1 的收益，除此之外收益均为零。</p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/random-walk-mrp.png" class="lazyload"/>
  
</figure>
<p>由于这个任务没有折扣，因此每个状态的真实价值是从这个状态开始并终止于最右侧的概率，即 A 到 E 的概率分别为 <code>$\frac{1}{6}, \frac{2}{6}, \frac{3}{6}, \frac{4}{6}, \frac{5}{6}$</code>。</p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/random-walk-mc-td.png" class="lazyload"/>
  
</figure>
<p>上图左侧显示了在经历了不同数量的幕采样序列之后，运行一次 TD(0) 所得到的价值估计。在 100 幕后，估计值已经非常接近真实值了。上图右侧显示了不同的 <code>$\alpha$</code> 情况下学习到的价值函数和真实价值函数的均方根（RMS）误差，对于所有的 <code>$s$</code>，近似价值函数都被初始化为中间值 <code>$V \left(s\right) = 0.5$</code>，显示的误差是 5 个状态上运行 100 次的平均误差。</p>
<p>给定近似价值函数 <code>$V$</code>，在访问非终止状态的每个时刻 <code>$t$</code>，使用式 <code>$\ref{eq:mc-update}$</code> 和 <code>$\ref{eq:td-update}$</code> 计算相应的增量经验，产生新的总增量，以此类推，直到价值函数收敛。我们称这种方法为<strong>批量更新</strong>，因为只有在处理了<strong>整批</strong>的训练数据后才进行更新。批量蒙特卡洛方法总是找出最小化训练集上均方误差的估计，而批量 TD(0) 总是找出完全符合马尔可夫过程模型的最大似然估计参数。因此，MC 在非马尔可夫环境中更加高效，而 TD 在马尔可夫环境中更加高效。</p>
<p>DP，MC 和 TD 的状态价值更新回溯过程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/dp-backup.png" class="lazyload"/>
  
</figure>
<p><code>$$ \textbf{DP} \quad V\left(S_{t}\right) \leftarrow \mathbb{E}_{\pi}\left[R_{t+1}+\gamma V\left(S_{t+1}\right)\right] $$</code></p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/mc-backup.png" class="lazyload"/>
  
</figure>
<p><code>$$ \textbf{MC} \quad V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(G_{t}-V\left(S_{t}\right)\right) $$</code></p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/td-backup.png" class="lazyload"/>
  
</figure>
<p><code>$$ \textbf{TD} \quad V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right)\right) $$</code></p>
<h2 id="时序差分控制">时序差分控制</h2>
<p>利用时序差分方法解决控制问题，我们依然采用广义策略迭代（GPI），只是在评估和预测部分采用时序差分方法。同蒙特卡洛方法一样，我们需要在试探和开发之间做出权衡，因此方法又划分为同轨策略和离轨策略。</p>
<h3 id="sarsa-同轨策略下的时序差分控制">Sarsa：同轨策略下的时序差分控制</h3>
<p>在同轨策略中，我们需要对所有状态 <code>$s$</code> 以及动作 <code>$a$</code> 估计出在当前的行动策略下所有对应的 <code>$q_{\pi} \left(s, a\right)$</code>。确保状态值在 TD(0) 下收敛的定理同样也适用于对应的动作值的算法上</p>
<p><code>$$ Q\left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma Q\left(S_{t+1}, A_{t+1}\right)-Q\left(S_{t}, A_{t}\right)\right] $$</code></p>
<p>每当从非终止状态的 <code>$S_t$</code> 出现一次转移之后，就进行上述的一次更新，如果 <code>$S_{t+1}$</code> 是终止状态，那么 <code>$Q \left(S_{t+1}, A_{t+1}\right)$</code> 则定义为 0。这个更新规则用到了描述这个事件的五元组 <code>$\left(S_t, A_t, R_{t+1}, S_{t+1}, A_{t+1}\right)$</code>，因此根据这五元组将这个算法命名为 <strong>Sarsa</strong>。Sarsa 控制算法的一般形式如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Sarsa（同轨策略下的 TD 控制）算法，用于估计 $Q \approx q_*$}
\begin{algorithmic}
\STATE 对于所有 $s \in \mathcal{S}^+, a \in \mathcal{A} \left(s\right)$，任意初始化 $Q \left(s, a\right)$，其中 $Q \left(\text{终止状态}, \cdot\right) = 0$
\FOR{每一幕}
  \STATE 初始化 $S$
  \STATE 使用从 $Q$ 得到的策略（例如 $\epsilon-$ 贪心），在 $S$ 处选择 $A$
  \REPEAT
    \STATE 执行动作 $A$，观测到 $R, S'$
    \STATE 使用从 $Q$ 得到的策略（例如 $\epsilon-$ 贪心），在 $S'$ 处选择 $A'$
    \STATE $Q\left(S, A\right) \gets Q\left(S, A\right)+\alpha\left[R+\gamma Q\left(S', A'\right)-Q\left(S, A\right)\right]$
    \STATE $S \gets S'$
    \STATE $A \gets A'$
  \UNTIL{$S$ 为终止状态}
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<h3 id="q-learning-离轨策略下的时序差分控制">Q-Learning：离轨策略下的时序差分控制</h3>
<p>离轨策略下的时序差分控制算法被称为 <strong>Q-Learning</strong>，其定义为：</p>
<p><code>$$ Q\left(S_{t}, A_{t}\right) \gets Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \max_{a} Q\left(S_{t+1}, a\right)-Q\left(S_{t}, A_{t}\right)\right] $$</code></p>
<p>在这里，待学习的动作价值函数 <code>$Q$</code> 采用了对最优动作价值函数 <code>$q_*$</code> 的直接近似作为学习目标，而与用于生成智能体决策序例轨迹的行动策略是什么无关。Q-Learning 算法的流程如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Q-Learning（离轨策略下的 TD 控制）算法，用于估计 $\pi \approx \pi_*$}
\begin{algorithmic}
\STATE 对于所有 $s \in \mathcal{S}^+, a \in \mathcal{A} \left(s\right)$，任意初始化 $Q \left(s, a\right)$，其中 $Q \left(\text{终止状态}, \cdot\right) = 0$
\FOR{每一幕}
  \STATE 初始化 $S$
  \REPEAT
    \STATE 使用从 $Q$ 得到的策略（例如 $\epsilon-$ 贪心），在 $S$ 处选择 $A$
    \STATE 执行动作 $A$，观测到 $R, S'$
    \STATE $Q\left(S, A\right) \gets Q\left(S, A\right)+\alpha\left[R+\gamma \max_{a} Q\left(S', a\right)-Q\left(S, A\right)\right]$
    \STATE $S \gets S'$
  \UNTIL{$S$ 为终止状态}
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<h3 id="期望-sarsa">期望 Sarsa</h3>
<p>如果将 Q-Learning 中对于下一个“状态-动作”二元组取最大值这一步换成取期望，即更新规则为：</p>
<p><code>$$ \begin{aligned} Q\left(S_{t}, A_{t}\right) &amp; \gets Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \mathbb{E}_{\pi}\left[Q\left(S_{t+1}, A_{t+1}\right) \mid S_{t+1}\right]-Q\left(S_{t}, A_{t}\right)\right] \\ &amp; \gets Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \sum_{a} \pi\left(a \mid S_{t+1}\right) Q\left(S_{t+1}, a\right)-Q\left(S_{t}, A_{t}\right)\right] \end{aligned} $$</code></p>
<p>给定下一个状态 <code>$S_{t+1}$</code>，这个算法确定地向期望意义上的 Sarsa 算法所决定的方向上移动，因此这个算法被称为<strong>期望 Sarsa</strong>。期望 Sarsa 在计算上比 Sarsa 更加复杂，但它消除了因为随机选择 <code>$A_{t+1}$</code> 而产生的方差。</p>
<h3 id="双学习">双学习</h3>
<p>在上述算法中，在估计值的基础上进行最大化也可以被看做隐式地对最大值进行估计，而这会产生一个显著的正偏差。假设在状态 <code>$s$</code> 下可以选择多个动作 <code>$a$</code>，这些动作在该状态下的真实价值 <code>$q \left(s, a\right)$</code> 全为零，但他们的估计值 <code>$Q \left(s, a\right)$</code> 是不确定的，可能大于零也可能小于零。真实值的最大值是零，但估计值的最大值是正数，因此就产生了正偏差，我们称其为<strong>最大化偏差</strong>。</p>
<p>我们用下例进行说明，在如下这个 MDP 中有两个非终止节点 A 和 B。每幕都从 A 开始并选择向左或向右的动作。向右则会立即转移到终止状态并得到值为 0 的收益和回报。向左则会是状态转移到 B，得到的收益也是 0。而在 B 这个状态下有很多种可能的动作，每种动作被选择后会立刻停止并得到一个从均值为 -0.1 方差为 1.0 的分布中采样得到的收益。</p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/double-learning-example.png" class="lazyload"/>
  
</figure>
<p>因此，任何一个以向左开始的轨迹的期望回报均为 -0.1，则在 A 这个状态中根本就不该选择向左。然而使用 <code>$\epsilon-$</code> 贪心策略来选择动作的 Q-Learning 算法会在开始阶段非常明显地选择向左这个动作。即使在算法收敛到稳定时，它选择向左这个动作的概率也比最优值高了大约 5%，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/double-learning-example-left-probability.png" class="lazyload"/>
  
</figure>
<p>解决该问题的一种方法为<strong>双学习</strong>。如果们将样本划分为两个集合，并用它们学习两个独立的对真实价值 <code>$q \left(a\right), \forall a \in A$</code> 的估计 <code>$Q_1 \left(a\right)$</code> 和 <code>$Q_2 \left(a\right)$</code>。则我们可以使用其中一个 <code>$Q_1$</code> 来确认最大的动作 <code>$A^* = \arg\max_a Q_1 \left(a\right)$</code>，用另一个 <code>$Q_2$</code> 来计算其价值的估计 <code>$Q_2 \left(A^*\right) = Q_2 \left(\arg\max_a Q_1 \left(a \right)\right)$</code>。由于 <code>$\mathbb{E} \left[Q_2 \left(A^*\right)\right] = q \left(A^*\right)$</code>，因此这个估计是无偏的。我们可以交换两个估计 <code>$Q_1 \left(a\right)$</code> 和 <code>$Q_2 \left(a\right)$</code> 的角色再执行一遍上面的过程，就可以得到另一个无偏的估计 <code>$Q_1 \left(\arg\max_a Q_2 \left(a\right)\right)$</code>。</p>
<p>双学习的 Q-Learning 版本为 Double Q-Learning。Double Q-Learning 在学习时会以 0.5 的概率进行如下更新：</p>
<p><code>$$ Q_{1}\left(S_{t}, A_{t}\right) \leftarrow Q_{1}\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma Q_{2}\left(S_{t+1}, \underset{a}{\arg \max } Q_{1}\left(S_{t+1}, a\right)\right)-Q_{1}\left(S_{t}, A_{t}\right)\right] $$</code></p>
<p>以 0.5 的概率交换 <code>$Q_1$</code> 和 <code>$Q_2$</code> 的角色进行同样的更新。使用 <code>$\epsilon-$</code> 贪心策略的 Double Q-Learning 的完整算法流程如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Double Q-Learning，用于估计 $Q_1 \approx Q_2 \approx q_*$}
\begin{algorithmic}
\REQUIRE 步长 $\alpha \in \left(0, 1\right]$，很小的 $\epsilon > 0$
\STATE 对于所有 $s \in \mathcal{S}^+, a \in \mathcal{A} \left(s\right)$，任意初始化 $Q_1 \left(s, a\right), Q_2 \left(s, a\right)$，其中 $Q \left(\text{终止状态}, \cdot\right) = 0$
\FOR{每一幕}
  \STATE 初始化 $S$
  \REPEAT
    \STATE 基于 $Q_1 + Q_2$ 使用 $\epsilon-$ 贪心策略在 $S$ 处选择 $A$
    \STATE 执行动作 $A$，观测到 $R, S'$
    \IF{$Random(0, 1] > 0.5$}
      \STATE $Q_{1}(S, A) \leftarrow Q_{1}(S, A)+\alpha\left(R+\gamma Q_{2}\left(S^{\prime}, \arg \max _{a} Q_{1}\left(S^{\prime}, a\right)\right)-Q_{1}(S, A)\right)$
    \ELSE
      \STATE $Q_{2}(S, A) \leftarrow Q_{2}(S, A)+\alpha\left(R+\gamma Q_{1}\left(S^{\prime}, \arg \max _{a} Q_{2}\left(S^{\prime}, a\right)\right)-Q_{2}(S, A)\right)$
    \ENDIF
    \STATE $S \gets S'$
  \UNTIL{$S$ 为终止状态}
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<h2 id="taxi-v3-示例">Taxi-v3 示例</h2>
<p>我们以 <a href="https://gym.openai.com/envs/Taxi-v3/">Taxi-v3</a> 为示例来测试 Sarsa，Q-Learning 和 期望 Sarsa 三种不同的算法。Taxi-v3 包含了一个 5x5 的网格，即 25 个可能的位置，我们需要驾驶一辆出租车分别在图中的 R、G、Y、B 四个位置接送乘客。客人共计存在 4 种可能的上车点，4 种可能的下车点，同时考虑出租车的位置，整个环境共有 <code>$5 \times 5 \times \left(4 + 1\right) \times 4 = 500$</code> 种可能的状态，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/taxi-v3-env.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：https://www.learndatasci.com/tutorials/reinforcement-q-learning-scratch-python-openai-gym/</p></figcaption>
</figure>
<p>出租车需要根据当前环境采取不同的动作，共计 6 种可能的动作：向南走，向北走，向东走，向西走，接上乘客，放下乘客。由于环境中存在墙，出租车每次撞墙不会发生任何移动。每一步动作默认 -1 的回报，当选择错误的地点接上或放下乘客时获得 -10 的回报，在成功运送一个客人后获得 +20 的回报。</p>
<p>分别利用 Sarsa，Q-Learning 和 期望 Sarsa 三种不同的算法训练模型，我们以 100 幕作为窗口计算平均回报，前 1000 个平均回报的对比结果如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/taxi-v3-rewards.png" class="lazyload"/>
  
</figure>
<p>Taxi-v3 的成绩排行榜可参见 <a href="https://github.com/openai/gym/wiki/Leaderboard#taxi-v3">这里</a>。利用训练好的模型执行预测的效果如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/taxi-v3-evaluation.gif" class="lazyload"/>
  
</figure>
<p>本文示例代码实现请参见<a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2020-07-11-model-free-policy-prediction-and-control-temporal-difference-learning/">这里</a>。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Sutton, R. S., &amp; Barto, A. G. (2018). <em>Reinforcement learning: An introduction</em>. MIT press.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>CS234: Reinforcement Learning <a href="http://web.stanford.edu/class/cs234/index.html">http://web.stanford.edu/class/cs234/index.html</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>UCL Course on RL <a href="https://www.davidsilver.uk/teaching">https://www.davidsilver.uk/teaching</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>无模型策略预测和控制 - 蒙特卡洛方法 (Model-Free Policy Prediction and Control - Monte-Carlo Learning)</title><link>https://zeqiang.fun/cn/2020/07/model-free-policy-prediction-and-control-monte-carlo-learning/</link><pubDate>Wed, 01 Jul 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/07/model-free-policy-prediction-and-control-monte-carlo-learning/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">《强化学习系列》</a>文章<br>
本文内容主要参考自：<br>
1.《强化学习》<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup><br>
2. CS234: Reinforcement Learning <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup><br>
3. UCL Course on RL <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
</blockquote>
<p>蒙特卡洛算法仅需要<strong>经验</strong>，即从真实或者模拟的环境交互中采样得到的状态、动作、收益的序例。从<strong>真实</strong>经验中学习不需要关于环境动态变化规律的先验知识，却依然能够达到最优的行为；从<strong>模拟</strong>经验中学习尽管需要一个模型，但这个模型只需要能够生成状态转移的一些样本，而不需要像动态规划那样生成所有可能的转移概率分布。</p>
<h2 id="蒙特卡洛预测">蒙特卡洛预测</h2>
<p>一个状态的价值是从该状态开始的期望回报，即未来的折扣收益累积值的期望。那么一个显而易见的方式是根据经验进行估计，即对所有经过这个状态之后产生的回报进行平均。随着越来越多的回报被观察到，平均值就会收敛到期望值，这就是蒙特卡洛算法的基本思想。</p>
<p>假设给定策略 <code>$\pi$</code> 下途径状态 <code>$s$</code> 的多幕数据，我们需要估计策略 <code>$\pi$</code> 下状态 <code>$s$</code> 的价值函数 <code>$v_{\pi} \left(s\right)$</code>。在同一幕中，<code>$s$</code> 可能多次被访问，因此蒙特卡洛方法分为<strong>首次访问型 MC 算法</strong>和<strong>每次访问型 MC 算法</strong>，两者的区别在于更新时是否校验 <code>$S_t$</code> 已经在当前幕中出现过。以首次访问型 MC 预测算法为例，算法流程如下：</p>


<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/pseudocode@latest/build/pseudocode.min.css">


<div><pre class="pseudocode">
\begin{algorithm}
\caption{首次访问型 MC 预测算法，用于估计 $V \approx v_{\pi}$}
\begin{algorithmic}
\REQUIRE 待评估策略 $\pi$
\STATE 对于所有 $s \in \mathcal{S}$，任意初始化 $V \left(s\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}$，$Returns \left(s\right) \gets \varnothing$
\WHILE{TRUE}
  \STATE 根据 $\pi$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \IF{$S_t$ 在 $S_0, S_1, \cdots, S_{t-1}$ 中出现过}
      \STATE $Returns \left(S_t\right) \gets Resurn \left(S_t\right) \cup G$
      \STATE $V \left(S_t\right) \gets avg \left(Returns \left(S_t\right)\right)$
    \ENDIF
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>以<a href="https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E4%B8%80%E9%BB%9E">二十一点</a>游戏为例，每一局可以看作一幕，胜、负、平分别获得收益 <code>$+1, -1, 0$</code>。每局游戏进行中的收益都为 <code>$0$</code>，并且不打折扣（<code>$\gamma = 1$</code>），最终的收益即为整个游戏的回报。玩家的动作为要牌（Hit）或停牌（Stand），状态则取决于玩家的牌和庄家显示的牌。假设所有牌来自无穷多的一组牌（即每次取出的牌都会再放回牌堆）。如果玩家手上有一张 A，可以视作 11 而不爆掉，那么称这张 A 为<strong>可用的</strong>，此时这张牌总会被视作 11。因此，玩家做出的选择只会依赖于三个变量：他手牌的总和（12-31），庄家显示的牌（A-10），以及他是否有可用的 A，共计 200 个状态。</p>
<p>考虑如下策略，玩家在手牌点数之和小于 20 时均要牌，否则停牌。通过该策略多次模型二十一点游戏，并且计算每一个状态的回报的平均值。模拟结果如下：</p>
<figure>
  <img data-src="/images/cn/2020-07-01-model-free-policy-prediction-and-control-monte-carlo-learning/blackjack-monte-carlo-on-policy.png" class="lazyload"/>
  
</figure>
<p>有可用 A 的状态的估计会更不确定、不规律，因为这样的状态更加罕见。无论哪种情况，在大于约 500000 局游戏后，价值函数都能很好地近似。</p>
<h2 id="蒙特卡洛控制">蒙特卡洛控制</h2>
<p>如果无法得到环境的模型，那么计算动作的价值（“状态-动作”二元组的价值）比计算状态的价值更加有用。动作价值函数的策略评估的目标是估计 <code>$q_{\pi} \left(s, a\right)$</code>，即在策略 <code>$\pi$</code> 下从状态 <code>$s$</code> 采取动作 <code>$a$</code> 的期望回报。只需将对状态的访问改为对“状态-动作”二元组的访问，蒙特卡洛算法就可以几乎和之前完全相同的方式解决该问题，唯一复杂之处在于一些“状态-动作”二元组可能永远不会被访问到。为了实现基于动作价值函数的策略评估，我们必须保证持续的试探。一种方式是将指定的“状态-动作”二元组作为起点开始一幕采样，同时保证所有“状态-动作”二元组都有非零的概率可以被选为起点。这样就保证了在采样的幕个数趋于无穷时，每一个“状态-动作”二元组都会被访问到无数次。我们把这种假设称为<strong>试探性出发</strong>。</p>
<p>策略改进的方法是在当前价值函数上贪心地选择动作。由于我们有动作价值函数，所以在贪心的时候完全不需要使用任何的模型信息。对于任意的一个动作价值函数 <code>$q$</code>，对应的贪心策略为：对于任意一个状态 <code>$s \in \mathcal{S}$</code>，必定选择对应动作价值函数最大的动作：</p>
<p><code>$$ \pi \left(s\right) = \arg\max_a q \left(s, a\right) $$</code></p>
<p>策略改进可以通过将 <code>$q_{\pi_k}$</code> 对应的贪心策略作为 <code>$\pi_{k+1}$</code> 来进行。这样的 <code>$\pi_k$</code> 和 <code>$\pi_{k+1}$</code> 满足策略改进定理，因为对于所有的状态 <code>$s \in \mathcal{S}$</code>：</p>
<p><code>$$ \begin{aligned} q_{\pi_{k}}\left(s, \pi_{k+1}(s)\right) &amp;=q_{\pi_{k}}\left(s, \underset{a}{\arg \max } q_{\pi_{k}}(s, a)\right) \\ &amp;=\max _{a} q_{\pi_{k}}(s, a) \\ &amp; \geqslant q_{\pi_{k}}\left(s, \pi_{k}(s)\right) \\ &amp; \geqslant v_{\pi_{k}}(s) \end{aligned} $$</code></p>
<p>对于蒙特卡洛策略迭代，可以逐幕交替进行评估与改进。每一幕结束后，使用观测到的回报进行策略评估，然后在该幕序列访问到的每一个状态上进行策略改进。使用这个思路的一个简单算法称为<strong>基于试探性出发的蒙特卡洛（蒙特卡洛 ES）</strong>，算法流程如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{蒙特卡洛 ES（试探性出发），用于估计 $\pi \approx \pi_*$}
\begin{algorithmic}
\STATE 对于所有 $s \in \mathcal{S}$，任意初始化 $\pi \left(s\right) \in \mathcal{A} \left(s\right)$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$，任意初始化 $Q \left(s, a\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$，$Returns \left(s, a\right) \gets \varnothing$
\WHILE{TRUE}
  \STATE 选择 $S_0 \in \mathcal{S}$ 和 $A_0 \in \mathcal{A} \left(S_0\right)$ 以使得所有“状态-动作”二元组的概率都 $> 0$
  \STATE 从 $S_0, A_0$ 开始根据 $\pi$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \IF{$S_t, A_t$ 在 $S_0, A_0, S_1, A_1, \cdots, S_{t-1}, A_{t-1}$ 中出现过}
      \STATE $Returns \left(S_t, A_t\right) \gets Resurn \left(S_t, A_t\right) \cup G$
      \STATE $Q \left(S_t, A_t\right) \gets avg \left(Returns \left(S_t, A_t\right)\right)$
      \STATE $\pi \left(S_t\right) \gets \arg\max_a Q \left(S_t, a\right)$
    \ENDIF
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>利用蒙特卡洛 ES 可以很直接地解决二十一点游戏，只需随机等概率选择庄家的扑克牌、玩家手牌的点数，以及确定是否有可用的 A 即可。令只在 20 或 21 点停牌为初始策略，初始动作价值函数全部为零，下图展示了蒙特卡洛 ES 得出的最优策略：</p>
<figure>
  <img data-src="/images/cn/2020-07-01-model-free-policy-prediction-and-control-monte-carlo-learning/blackjack-monte-carlo-exploring-starts.png" class="lazyload"/>
  
</figure>
<h2 id="同轨策略和离轨策略">同轨策略和离轨策略</h2>
<h3 id="同轨策略">同轨策略</h3>
<p>为了避免很难被满足的试探性出发假设，一般性的解法是智能体能够持续不断地选择所有可能的动作，有两种方法可以保证这一点，<strong>同轨策略（on-policy）<strong>和</strong>离轨策略（off-policy）</strong>。在同轨策略中，用于生成采样数据序列的策略和用于实际决策的待评估和改进的策略是相同的；而在离轨策略中，用于评估或改进的策略与生成采样数据的策略是不同的，即生成的数据“离开”了待优化的策略所决定的决策序列轨迹。</p>
<p>在同轨策略中，策略一般是“软性”的，即对于任意 <code>$s \in \mathcal{S}$</code> 以及 <code>$a \in \mathcal{A} \left(s\right)$</code>，都有 <code>$\pi \left(a | s\right) &gt; 0$</code>，但他们会逐渐地逼近一个确定性的策略。<code>$\epsilon-$</code> 贪心策略是指在绝大多数时候都采取获得最大估计值的动作价值函数对应的动作，但同时以一个较小的概率 <code>$\epsilon$</code> 随机选择一个动作。因此对于所有非贪心的动作都以 <code>$\frac{\epsilon}{|\mathcal{A} \left(s\right)|}$</code> 的概率被选中，贪心动作则以 <code>$1 - \epsilon + \frac{\epsilon}{|\mathcal{A} \left(s\right)|}$</code> 的概率被选中。同轨策略的蒙特卡洛控制</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{同轨策略的首次访问型 MC 控制算法（对于 $\epsilon-$ 软性策略），用于估计 $\pi \approx \pi_*$}
\begin{algorithmic}
\STATE $\pi \gets$ 一个任意的 $\epsilon-$ 软性策略
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$，任意初始化 $Q \left(s, a\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$，$Returns \left(s, a\right) \gets \varnothing$
\WHILE{TRUE}
  \STATE 根据 $\pi$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \IF{$S_t, A_t$ 在 $S_0, A_0, S_1, A_1, \cdots, S_{t-1}, A_{t-1}$ 中出现过}
      \STATE $Returns \left(S_t, A_t\right) \gets Resurn \left(S_t, A_t\right) \cup G$
      \STATE $Q \left(S_t, A_t\right) \gets avg \left(Returns \left(S_t, A_t\right)\right)$
      \STATE $A^* \gets \arg\max_a Q \left(S_t, a\right)$
      \STATE 对于所有 $a \in \mathcal{A} \left(S_t\right)$，$\pi\left(a \mid S_{t}\right) \leftarrow\left\{\begin{array}{ll}
1-\varepsilon+\varepsilon /\left|\mathcal{A}\left(S_{t}\right)\right| & \text { if } a=A^{*} \\
\varepsilon /\left|\mathcal{A}\left(S_{t}\right)\right| & \text { if } a \neq A^{*}
\end{array}\right.$
    \ENDIF
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}
</pre></div>

<h3 id="离轨策略">离轨策略</h3>
<p>所有的学习控制方法都面临一个困境：它们希望学到的动作可以使随后的智能体行为是最优的，但是为了搜索所有的动作（以保证找到最优动作），它们需要采取非最优的行动。同轨策略采用一种妥协的方法，它并不学习最优策略的动作值，而是学习一个接近最优而且仍能进行试探的策略的动作值。一个更加直接的方法是采用两个策略，一个用来学习并成为最优策略，另一个更加有试探性，用来产生智能体的行动样本。用来学习的策略被称为<strong>目标策略</strong>，用于生成行动样本的策略被称为<strong>行动策略</strong>。在这种情况下，我们认为学习所用的数据“离开”了待学习的目标策略，因此整个过程称为<strong>离轨策略学习</strong>。</p>
<p>几乎所有的离轨策略方法都采用了<strong>重要度采样</strong>，重要度采样是一种在给定来自其他分布的样本的条件下，估计某种分布的期望值的通用方法。在离轨策略学习中，对回报值根据其轨迹在目标策略与行动策略中出现的相对概率进行加权，这个相对概率称为<strong>重要度采样比</strong>。给定起始状态 <code>$S_t$</code>，后续的“状态-动作”轨迹 <code>$A_t, S_{t+1}, A_{t+1}, \cdots, S_T$</code> 在策略 <code>$\pi$</code> 下发生的概率为：</p>
<p><code>$$ \begin{aligned} \operatorname{Pr}\left\{A_{t},\right.&amp;\left.S_{t+1}, A_{t+1}, \ldots, S_{T} \mid S_{t}, A_{t: T-1} \sim \pi\right\} \\ &amp;=\pi\left(A_{t} \mid S_{t}\right) p\left(S_{t+1} \mid S_{t}, A_{t}\right) \pi\left(A_{t+1} \mid S_{t+1}\right) \cdots p\left(S_{T} \mid S_{T-1}, A_{T-1}\right) \\ &amp;=\prod_{k=t}^{T-1} \pi\left(A_{k} \mid S_{k}\right) p\left(S_{k+1} \mid S_{k}, A_{k}\right) \end{aligned} $$</code></p>
<p>其中，<code>$p$</code> 为状态转移概率函数。因此，在目标策略和行动策略轨迹下的相对概率（重要度采样比）为：</p>
<p><code>$$ \rho_{t: T-1} \doteq \frac{\prod_{k=t}^{T-1} \pi\left(A_{k} \mid S_{k}\right) p\left(S_{k+1} \mid S_{k}, A_{k}\right)}{\prod_{k=t}^{T-1} b\left(A_{k} \mid S_{k}\right) p\left(S_{k+1} \mid S_{k}, A_{k}\right)}=\prod_{k=t}^{T-1} \frac{\pi\left(A_{k} \mid S_{k}\right)}{b\left(A_{k} \mid S_{k}\right)} $$</code></p>
<p>化简后，重要度采样比只与两个策略和样本序列数据相关，而与 MDP 的动态特性（状态转移概率）无关。我们希望估计目标策略下的期望回报（价值），但我们只有行动策略中的回报 <code>$G_t$</code>。直接使用行动策略中的回报进行估计是不准的，因此需要使用重要度采样比调整回报从而得到正确的期望值：</p>
<p><code>$$ \mathbb{E}\left[\rho_{t: T-1} G_{t} \mid S_{t}=s\right]=v_{\pi}(s) $$</code></p>
<p>定义所有访问过状态 <code>$s$</code> 的时刻集合为 <code>$\mathcal{T} \left(s\right)$</code>，<code>$T \left(t\right)$</code> 表示时刻 <code>$t$</code> 后的首次终止，用 <code>$G_t$</code> 表示在 <code>$t$</code> 之后到达 <code>$T \left(t\right)$</code> 时的回报值。则 <code>$\left\{G_t\right\}_{t \in \mathcal{T} \left(s\right)}$</code> 就是状态 <code>$s$</code> 对应的回报值，<code>$\left\{\rho_{t:T \left(t\right) - 1}\right\}_{t \in \mathcal{T} \left(s\right)}$</code> 是相应的重要度采样比。则为了预测 <code>$v_{\pi} \left(s\right)$</code>，有：</p>
<p><code>$$ V(s) \doteq \frac{\sum_{t \in \mathcal{T}(s)} \rho_{t: T(t)-1} G_{t}}{|\mathcal{T}(s)|} \label{eq:ordinary-importance-sampling} $$</code></p>
<p>为一种简单平均实现的重要度采样，称之为<strong>普通重要度采样</strong>。</p>
<p><code>$$ V(s) \doteq \frac{\sum_{t \in \mathcal{T}(s)} \rho_{t: T(t)-1} G_{t}}{\sum_{t \in \mathcal{T}(s)} \rho_{t: T(t)-1}} \label{eq:weighted-importance-sampling} $$</code></p>
<p>为一种加权的重要度采样，称之为<strong>加权重要度采样</strong>，如果分母为零，则式 <code>$\ref{eq:weighted-importance-sampling}$</code> 的值也为零。式 <code>$\ref{eq:ordinary-importance-sampling}$</code> 得到的结果在期望上是 <code>$v_{\pi} \left(s\right)$</code> 的无偏估计，但其值可能变得很极端，式 <code>$\ref{eq:weighted-importance-sampling}$</code> 的估计是有偏的，但其估计的方差可以收敛到 0。</p>
<p>我们对二十一点游戏的状态值进行离轨策略估计。评估的状态为玩家有一张 A，一张 2（或者等价情况，有三张 A），从这个状态开始等概率选择要牌或停牌得到采样数据，目标策略只在和达到 20 或 21 时停牌。</p>
<figure>
  <img data-src="/images/cn/2020-07-01-model-free-policy-prediction-and-control-monte-carlo-learning/blackjack-monte-carlo-off-policy.png" class="lazyload"/>
  
</figure>
<p>在目标策略中，这个状态的值大概为 -0.27726（利用目标策略独立生成 1 亿幕数据后对回报进行平均得到）。两种离轨策略方法在采样随机策略经过 1000 幕离轨策略数据采样后都很好地逼近了这个值，但加权重要度采样在开始时错误率明显较低，这也是实践中的典型现象。</p>
<p>假设一个回报序列 <code>$G_1, G_2, \cdots, G_{n-1}$</code>，它们都从相同的状态开始，且每一个回报都对应一个随机权重 <code>$W_i$</code>，我们希望获得如下式子的估计：</p>
<p><code>$$ V_{n} \doteq \frac{\sum_{k=1}^{n-1} W_{k} G_{k}}{\sum_{k=1}^{n-1} W_{k}}, \quad n \geq 2 $$</code></p>
<p>同时在获得一个额外的回报 <code>$G_n$</code> 时能保持更新。为了能不断跟踪 <code>$V_n$</code> 的变化，我们必须为每一个状态维护前 <code>$n$</code> 个回报对应的权值的累加和 <code>$C_n$</code>。<code>$V_n$</code> 的更新方法如下：</p>
<p><code>$$ \begin{array}{l} V_{n+1} \doteq V_{n}+\dfrac{W_{n}}{C_{n}}\left[G_{n}-V_{n}\right], \quad n \geq 1 \\ C_{n+1} \doteq C_{n}+W_{n+1} \end{array} $$</code></p>
<p>其中，<code>$C_0 = 0$</code>，<code>$V_1$</code> 是任意值。一个完整的用于蒙特卡洛策略评估的逐幕增量算法如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{离轨策略 MC 预测算法（策略评估），用于估计 $Q \approx q_{\pi}$}
\begin{algorithmic}
\REQUIRE 一个任意的目标策略 $\pi$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$，任意初始化 $Q \left(s, a\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$，$C \left(s, a\right) \gets 0$
\WHILE{TRUE}
  \STATE $b \gets$ 任何能包括 $\pi$ 的策略
  \STATE 根据 $b$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \STATE $W \gets 1$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \STATE $C \left(S_t, A_t\right) \gets C \left(S_t, A_t\right) + W$
    \STATE $Q \left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\frac{W}{C\left(S_{t}, A_{t}\right)}\left[G-Q\left(S_{t}, A_{t}\right)\right]$
    \STATE $W \leftarrow W \frac{\pi\left(A_{t} \mid S_{t}\right)}{b\left(A_{t} \mid S_{t}\right)}$
    \IF{$W = 0$}
      \BREAK
    \ENDIF
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>在离轨策略中，策略的价值评估和策略的控制是分开的，用于生成行动数据的策略被称为<strong>行动策略</strong>，行动策略可能与实际上被评估和改善的策略无关，而被评估和改善的策略称为<strong>目标策略</strong>。这样分离的好处在于当行动策略能对所有可能的动作继续进行采样时，目标策略可以是确定的（贪心的）。</p>
<p>离轨策略蒙特卡洛控制方法要求行动策略对目标策略可能做出的所有动作都有非零的概率被选择。为了试探所有的可能性，要求行动策略是软性的。一个基于通用迭代策略（GPI）和重要度采样的离轨策略蒙特卡洛控制方法如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{离轨策略 MC 控制算法，用于估计 $\pi \approx \pi_*$}
\begin{algorithmic}
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$，任意初始化 $Q \left(s, a\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$，$C \left(s, a\right) \gets 0$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$，$\pi \left(s\right) \gets \arg\max_a Q \left(s, a\right)$
\STATE \COMMENT{出现平分情况选取方法应保持一致}
\WHILE{TRUE}
  \STATE $b \gets$ 任意软性策略
  \STATE 根据 $b$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \STATE $W \gets 1$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \STATE $C \left(S_t, A_t\right) \gets C \left(S_t, A_t\right) + W$
    \STATE $Q \left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\frac{W}{C\left(S_{t}, A_{t}\right)}\left[G-Q\left(S_{t}, A_{t}\right)\right]$
    \STATE $\pi\left(S_{t}\right) \leftarrow \arg \max _{a} Q\left(S_{t}, a\right)$
    \STATE \COMMENT{出现平分情况选取方法应保持一致}
    \IF{$A_t \neq \pi \left(S_t\right)$}
      \BREAK
    \ENDIF
    \STATE $W \leftarrow W \frac{1}{b\left(A_{t} \mid S_{t}\right)}$
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>本文示例代码实现请参见<a href="https://github.com/leovan/leovan.me/blob/master/scripts/cn/2020-07-01-model-free-policy-prediction-and-control-monte-carlo-learning/blackjack.py">这里</a>。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Sutton, R. S., &amp; Barto, A. G. (2018). <em>Reinforcement learning: An introduction</em>. MIT press.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>CS234: Reinforcement Learning <a href="http://web.stanford.edu/class/cs234/index.html">http://web.stanford.edu/class/cs234/index.html</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>UCL Course on RL <a href="https://www.davidsilver.uk/teaching">https://www.davidsilver.uk/teaching</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>利用动态规划求解马尔可夫决策过程 (Planning by Dynamic Programming)</title><link>https://zeqiang.fun/cn/2020/06/planning-by-dynamic-programming/</link><pubDate>Sat, 13 Jun 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/06/planning-by-dynamic-programming/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">《强化学习系列》</a>文章<br>
本文内容主要参考自：<br>
1.《强化学习》<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup><br>
2. CS234: Reinforcement Learning <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup><br>
3. UCL Course on RL <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
</blockquote>
<h2 id="动态规划">动态规划</h2>
<p><a href="/cn/2018/11/computational-complexity-and-dynamic-programming/"><strong>动态规划</strong></a>（Dynamic Programming，DP）是一种用于解决具有如下两个特性问题的通用算法：</p>
<ol>
<li>优化问题可以分解为子问题。</li>
<li>子问题出现多次并可以被缓存和复用。</li>
</ol>
<p>马尔可夫决策过程正符合这两个特性：</p>
<ol>
<li>贝尔曼方程给定了迭代过程的分解。</li>
<li>价值函数保存并复用了解决方案。</li>
</ol>
<p>在强化学习中，DP 的核心思想是使用价值函数来结构化地组织对最优策略的搜索。一旦得到了满足贝尔曼最优方程的价值函数 <code>$v_*$</code> 或 <code>$q_*$</code>，得到最优策略就容易了。对于任意 <code>$s \in \mathcal{S}$</code>（状态集合），<code>$a \in \mathcal{A} \left(s\right)$</code>（动作集合）和 <code>$s' \in \mathcal{S}^{+}$</code>（在分幕式任务下 <code>$\mathcal{S}$</code> 加上一个终止状态），有：</p>
<p><code>$$ \begin{aligned} v_{*}(s) &amp;=\max _{a} \mathbb{E}\left[R_{t+1}+\gamma v_{*}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &amp;=\max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{*}\left(s^{\prime}\right)\right] \end{aligned} $$</code></p>
<p><code>$$ \begin{aligned} q_{*}(s, a) &amp;=\mathbb{E}\left[R_{t+1}+\gamma \max _{a^{\prime}} q_{*}\left(S_{t+1}, a^{\prime}\right) | S_{t}=s, A_{t}=a\right] \\ &amp;\left.=\sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma \max _{a^{\prime}}\right] q_{*}\left(s^{\prime}, a^{\prime}\right)\right] \end{aligned} $$</code></p>
<p>将贝尔曼方程转化成为近似逼近理想价值函数的递归更新公式，我们就得到了 DP 算法。</p>
<h2 id="策略评估">策略评估</h2>
<p>对于一个策略 <code>$\pi$</code>，如何计算其状态价值函数 <code>$v_{\pi}$</code> 被称为<strong>策略评估</strong>。对于任意 <code>$s \in \mathcal{S}$</code>，有：</p>
<p><code>$$ \begin{aligned} v_{\pi}(s) &amp; \doteq \mathbb{E}_{\pi}\left[G_{t} | S_{t}=s\right] \\ &amp;=\mathbb{E}_{\pi}\left[R_{t+1}+\gamma G_{t+1} | S_{t}=s\right] \\ &amp;=\mathbb{E}_{\pi}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s\right] \\ &amp;=\sum_{a} \pi(a | s) \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi}\left(s^{\prime}\right)\right] \end{aligned} $$</code></p>
<p>其中 <code>$\pi \left(a | s\right)$</code> 表示在环境 <code>$s$</code> 中智能体在策略 <code>$\pi$</code> 下采取动作 <code>$a$</code> 的概率。只要 <code>$\gamma &lt; 1$</code> 或者任何状态在 <code>$\pi$</code> 下都能保证最后终止，则 <code>$v_{\pi}$</code> 唯一存在。</p>
<p>考虑一个近似的价值函数序列 <code>$v_0, v_1, \cdots$</code>，从 <code>$\mathcal{S}^{+}$</code> 映射到 <code>$\mathbb{R}$</code>，初始的近似值 <code>$v_0$</code> 可以任意选取（除了终止状态必须为 0 外）。下一轮迭代的近似可以使用 <code>$v_{\pi}$</code> 的贝尔曼方程进行更新，对于任意 <code>$s \in \mathcal{S}$</code> 有：</p>
<p><code>$$ \begin{aligned} v_{k+1}(s) &amp; \doteq \mathbb{E}_{\pi}\left[R_{t+1}+\gamma v_{k}\left(S_{t+1}\right) | S_{t}=s\right] \\ &amp;=\sum_{a} \pi(a | s) \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{k}\left(s^{\prime}\right)\right] \end{aligned} $$</code></p>
<p>显然，<code>$v_k = v_{\pi}$</code> 是这个更新规则的一个不动点。在保证 <code>$v_{\pi}$</code> 存在的条件下，序列 <code>$\left\{v_k\right\}$</code> 在 <code>$k \to \infty$</code> 时将会收敛到 <code>$v_{\pi}$</code>，这个算法称作 <strong>迭代策略评估</strong>。</p>
<h2 id="策略改进">策略改进</h2>
<p>对于任意一个确定的策略 <code>$\pi$</code>，我们已经确定了它的价值函数 <code>$v_{\pi}$</code>。对于某个状态 <code>$s$</code>，我们想知道是否应该选择一个不同于给定的策略的动作 <code>$a \neq \pi \left(s\right)$</code>。如果从状态 <code>$s$</code> 继续使用现有策略，则最后的结果就是 <code>$v \left(s\right)$</code>，但我们并不知道换成一个新策略后是得到更好的结果还是更坏的结果。一种解决方法是在状态 <code>$s$</code> 选择动作 <code>$a$</code> 后，继续遵循现有的策略 <code>$\pi$</code>，则这种方法的价值为：</p>
<p><code>$$ \begin{aligned} q_{\pi}(s, a) &amp; \doteq \mathbb{E}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &amp;=\sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi}\left(s^{\prime}\right)\right] \end{aligned} $$</code></p>
<p>一个关键的准则就是这个值是大于还是小于 <code>$v_{\pi} \left(s\right)$</code>。如果这个值更大，则说明在状态 <code>$s$</code> 选择动作 <code>$a$</code>，然后继续使用策略 <code>$\pi$</code> 会比使用始终使用策略 <code>$\pi$</code> 更优。</p>
<p>上述情况是<strong>策略改进定理</strong>的一个特例，一般来说，如果 <code>$\pi$</code> 和 <code>$\pi'$</code> 是任意两个确定的策略，对于任意 <code>$s \in \mathcal{S}$</code>：</p>
<p><code>$$ q_{\pi}\left(s, \pi^{\prime}(s)\right) \geq v_{\pi}(s) $$</code></p>
<p>则称策略 <code>$\pi'$</code> 相比于 <code>$\pi$</code> 一样好或更好。也就是说，对于任意状态 <code>$s \in \mathcal{S}$</code>，这样肯定能得到一样或更好的期望回报：</p>
<p><code>$$ v_{\pi^{\prime}}(s) \geq v_{\pi}(s) $$</code></p>
<p>延伸到所有状态和所有可能的动作，即在每个状态下根据 <code>$q_{\pi} \left(s, a\right)$</code> 选择一个最优的，换言之，考虑一个新的<strong>贪心</strong>策略 <code>$\pi'$</code>，满足：</p>
<p><code>$$ \begin{aligned} \pi^{\prime}(s) &amp; \doteq \underset{a}{\arg \max } q_{\pi}(s, a) \\ &amp;=\underset{a}{\arg \max } \mathbb{E}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &amp;=\underset{a}{\arg \max } \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi}\left(s^{\prime}\right)\right] \end{aligned} $$</code></p>
<p>这样构造出的贪心策略满足策略改进定理的条件，所以它和原策略相比一样好或更好。这种根据原策略的价值函数执行贪心算法，来构造一个更好策略的过程称之为<strong>策略改进</strong>。如果新的贪心策略 <code>$\pi'$</code> 和原策略 <code>$\pi$</code> 一样好而不是更好，则有 <code>$v_{\pi} = v_{\pi'}$</code>，对任意 <code>$s \in \mathcal{S}$</code>：</p>
<p><code>$$ \begin{aligned} v_{\pi^{\prime}}(s) &amp;=\max _{a} \mathbb{E}\left[R_{t+1}+\gamma v_{\pi^{\prime}}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &amp;=\max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi^{\prime}}\left(s^{\prime}\right)\right] \end{aligned} $$</code></p>
<p>这同贝尔曼方程完全相同，因此 <code>$v_{\pi}$</code> 一定与 <code>$v_*$</code> 相同，<code>$\pi$</code> 与 <code>$\pi'$</code> 均必须为最优策略。因此，在除了原策略即为最优策略的情况下，策略改进一定会给出一个更优的结果。</p>
<h2 id="策略迭代">策略迭代</h2>
<p>一个策略 <code>$\pi$</code> 根据 <code>$v_{\pi}$</code> 产生了一个更好的策略 <code>$\pi'$</code>，进而我们可以通过计算 <code>$v_{\pi'}$</code> 来得到一个更优的策略 <code>$\pi''$</code>。这样一个链式的方法可以得到一个不断改进的策略和价值函数序列：</p>
<p><code>$$ \pi_{0} \stackrel{E}{\longrightarrow} v_{\pi_{0}} \stackrel{I}{\longrightarrow} \pi_{1} \stackrel{E}{\longrightarrow} v_{\pi_{1}} \stackrel{I}{\longrightarrow} \pi_{2} \stackrel{E}{\longrightarrow} \cdots \stackrel{I}{\longrightarrow} \pi_{*} \stackrel{E}{\longrightarrow} v_{*} $$</code></p>
<p>其中 <code>$\stackrel{E}{\longrightarrow}$</code> 表示策略评估，<code>$\stackrel{I}{\longrightarrow}$</code> 表示策略改进。每一个策略都能保证同前一个一样或者更优，由于一个有限 MDP 必然只有有限种策略，所以在有限次的迭代后，这种方法一定收敛到一个最优的策略与最优价值函数。这种寻找最优策略的方法叫做<strong>策略迭代</strong>。整个策略迭代算法如下：</p>


<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/pseudocode@latest/build/pseudocode.min.css">


<div><pre class="pseudocode">
\begin{algorithm}
\caption{迭代策略算法}
\begin{algorithmic}
\FUNCTION{PolicyIteration}{}
\STATE \COMMENT{初始化}
\FOR{$s \in \mathcal{S}$}
  \STATE 初始化 $V \left(s\right) \in \mathbb{R}$
  \STATE 初始化 $\pi \left(s\right) \in \mathcal{A} \left(s\right)$
\ENDFOR
\WHILE{true}
  \STATE \COMMENT{策略评估}
  \REPEAT
    \STATE $\Delta \gets 0$
    \FOR{$s \in \mathcal{S}$}
      \STATE $v \gets V \left(s\right)$
      \STATE $V \left(s\right) \gets \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, \pi \left(s\right)\right)\left[r+\gamma V\left(s^{\prime}\right)\right]$
      \STATE $\Delta \gets \max\left(\Delta, \left|v - V \left(s\right)\right|\right)$
    \ENDFOR
  \UNTIL{$\Delta < \theta$}
  \STATE \COMMENT{策略改进}
  \STATE policy-stable $\gets$ true
  \FOR{$s \in \mathcal{S}$}
    \STATE $\pi' \left(s\right) \gets \pi \left(s\right)$
    \STATE $\pi \left(s\right) \gets \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma V\left(s^{\prime}\right)\right]$
    \IF{$\pi' \left(s\right) \neq \pi \left(s\right)$}
      \STATE policy-stable $\gets$ flase
    \ENDIF
  \ENDFOR
  \IF{policy-stable $=$ true}
    \BREAK
  \ENDIF
\ENDWHILE
\RETURN $V, \pi$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>以<strong>杰克租车（Jack&rsquo;s Car）问题</strong>为例：杰克在两地运营租车公司，每租出一辆车获得 10 元收益，为了保证每个地点有车可用，杰克需要夜间在两地之间移动车辆，每辆车的移动代价为 2 元。假设每个地点租车和还车的数量符合泊松分布 <code>$\dfrac{\lambda^n}{n!} e^{- \lambda}$</code>，其中 <code>$\lambda$</code> 为期望值，租车的 <code>$\lambda$</code> 在两地分别为 3 和 4，还车的 <code>$\lambda$</code> 在两地分别为 3 和 2。假设任何一个地点不超过 20 辆车，每天最多移动 5 辆车，折扣率 <code>$\gamma = 0.9$</code>，将问题描述为一个持续的有限 MPD，时刻按天计算，状态为每天结束时每个地点的车辆数，动作则为夜间在两个地点之间移动的车辆数。策略从不移动任何车辆开始，整个策略迭代过程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-06-13-planning-by-dynamic-programming/car-rental-policy-history.png" class="lazyload"/>
  
</figure>
<p>上例代码实现请参见<a href="https://github.com/leovan/leovan.me/blob/master/scripts/cn/2020-06-13-planning-by-dynamic-programming/car_rental.py">这里</a>。</p>
<h2 id="价值迭代">价值迭代</h2>
<p>策略迭代算法的一个缺点是每一次迭代都涉及了策略评估，这是一个需要多次遍历状态集合的迭代过程。如果策略评估是迭代进行的，那么收敛到 <code>$v_{\pi}$</code> 理论上在极限处才成立，实际中不必等到其完全收敛，可以提前截断策略评估过程。有多种方式可以截断策略迭代中的策略评估步骤，并且不影响其收敛，一种重要的特殊情况是在一次遍历后即刻停止策略评估，该算法称为<strong>价值迭代</strong>。可以将此表示为结合了策略改进与阶段策略评估的简单更新公式，对任意 <code>$s \in \mathcal{S}$</code>：</p>
<p><code>$$ \begin{aligned} v_{k+1}(s) &amp; \doteq \max _{a} \mathbb{E}\left[R_{t+1}+\gamma v_{k}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &amp;=\max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{k}\left(s^{\prime}\right)\right] \end{aligned} $$</code></p>
<p>可以证明，对任意 <code>$v_0$</code>，在 <code>$v_*$</code> 存在的条件下，序列 <code>$\left\{v_k\right\}$</code> 都可以收敛到 <code>$v_*$</code>。整个价值迭代算法如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{价值迭代算法}
\begin{algorithmic}
\FUNCTION{ValueIteration}{}
\STATE \COMMENT{初始化}
\FOR{$s \in \mathcal{S}^{+}$}
  \STATE 初始化 $V \left(s\right)$，其中 $V \left(\text{终止状态}\right) = 0$
\ENDFOR
\STATE \COMMENT{价值迭代}
\REPEAT
  \STATE $\Delta \gets 0$
  \FOR{$s \in \mathcal{S}$}
    \STATE $v \gets V \left(s\right)$
    \STATE $V \left(s\right) \gets\sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma V\left(s^{\prime}\right)\right]$
    \STATE $\Delta \gets \max\left(\Delta, \left|v - V \left(s\right)\right|\right)$
  \ENDFOR
\UNTIL{$\Delta < \theta$}
\STATE 输出一个确定的策略 $\pi \approx \pi_*$ 使得 $\pi(s)=\arg \max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma V\left(s^{\prime}\right)\right]$
\RETURN $V, \pi$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>以**赌徒问题（Gambler’s Problem）**为例：一个赌徒下注猜一系列抛硬币实验的结果，如果正面朝上则获得这一次下注的钱，如果背面朝上则失去这一次下注的钱，游戏在达到目标收益 100 元或全部输光时结束。每抛一次硬币，赌徒必须从他的赌资中选取一个整数来下注，这个问题可以表示为一个非折扣的分幕式有限 MDP。状态为赌徒的赌资 <code>$s \in \left\{1, 2, \cdots, 99\right\}$</code>，动作为赌徒下注的金额 <code>$a \in \left\{0, 1, \cdots, \min \left(s, 100 - s\right)\right\}$</code>，收益在一般情况下为 0，只有在赌徒达到获利 100 元的终止状态时为 1。</p>
<p>令 <code>$p_h$</code> 为抛硬币正面朝上的概率，如果 <code>$p_h$</code> 已知，那么整个问题可以由价值迭代或其他类似算法解决。下图为当 <code>$p_h = 0.4$</code> 时，价值迭代连续遍历得到的价值函数和最后的策略。</p>
<figure>
  <img data-src="/images/cn/2020-06-13-planning-by-dynamic-programming/gamblers-problem-value-iteration.png" class="lazyload"/>
  
</figure>
<figure>
  <img data-src="/images/cn/2020-06-13-planning-by-dynamic-programming/gamblers-problem-optimal-policy.png" class="lazyload"/>
  
</figure>
<p>上例代码实现请参见<a href="https://github.com/leovan/leovan.me/blob/master/scripts/cn/2020-06-13-planning-by-dynamic-programming/gamblers_problem.py">这里</a>。</p>
<h2 id="异步动态规划">异步动态规划</h2>
<p>之前讨论的 DP 方法的一个主要缺点是它们涉及对 MDP 的整个状态集的操作，如果状态集很大，即使单次遍历也会十分昂贵。<strong>异步动态规划</strong>算法是一类就地迭代的 DP 算法，其不以系统遍历状态集的形式来组织算法。这些算法使用任意可用的状态值，以任意顺序来更新状态值，在某些状态的值更新一次之前，另一些状态的值可能已经更新了好几次。然而为了正确收敛，异步算法必须要不断地更新所有状态的值：在某个计算节点后，它不能忽略任何一个状态。</p>
<h2 id="广义策略迭代">广义策略迭代</h2>
<p>策略迭代包含两个同时进行的相互作用的流程，一个使得价值函数与当前策略一致（策略评估），另一个根据当前价值函数贪心地更新策略（策略改进）。在策略迭代中，这两个流程交替进行，每个流程都在另一个开始前完成。然而这也不是必须的，在异步方法中，评估和改进流程则以更细的粒度交替进行。我们利用**广义策略迭代（GPI）**一词来指代策略评估和策略改进相互作用的一般思路，与这两个流程的力度和其他细节无关。</p>
<p>几乎所有的强化学习方法都可以被描述为 GPI，几乎所有方法都包含明确定义的策略和价值函数。策略总是基于特定的价值函数进行改进，价值函数也始终会向对应特定策略的真实价值函数收敛。</p>
<figure>
  <img data-src="/images/cn/2020-06-13-planning-by-dynamic-programming/generalized-policy-iteration.png" class="lazyload"/>
  
</figure>
<p>GPI 的评估和改进流程可以视为两个约束或目标之间的相互作用的流程。每个流程都把价值函数或策略推向其中的一条线，该线代表了对于两个目标中的某一个目标的解决方案，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-06-13-planning-by-dynamic-programming/policy-improvement.png" class="lazyload"/>
  
</figure>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Sutton, R. S., &amp; Barto, A. G. (2018). <em>Reinforcement learning: An introduction</em>. MIT press.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>CS234: Reinforcement Learning <a href="http://web.stanford.edu/class/cs234/index.html">http://web.stanford.edu/class/cs234/index.html</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>UCL Course on RL <a href="https://www.davidsilver.uk/teaching">https://www.davidsilver.uk/teaching</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>贝叶斯优化 (Bayesian Optimization)</title><link>https://zeqiang.fun/cn/2020/06/bayesian-optimization/</link><pubDate>Sat, 06 Jun 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/06/bayesian-optimization/</guid><description><![CDATA[
        <blockquote>
<p>本文内容主要参考自：</p>
<ol>
<li><a href="https://zhuanlan.zhihu.com/p/139478368">从高斯分布到高斯过程、高斯过程回归、贝叶斯优化</a></li>
<li><a href="https://distill.pub/2019/visual-exploration-gaussian-processes/">A Visual Exploration of Gaussian Processes</a></li>
<li><a href="https://www.aidanscannell.com/post/gaussian-process-regression/">Gaussian Process Regression</a></li>
<li><a href="https://distill.pub/2020/bayesian-optimization/">Exploring Bayesian Optimization</a></li>
</ol>
</blockquote>
<h2 id="高斯分布">高斯分布</h2>
<h3 id="一元高斯分布">一元高斯分布</h3>
<p>若随机变量 <code>$X$</code> 服从一个均值为 <code>$\mu$</code>，方差为 <code>$\sigma^2$</code> 的高斯分布，则记为：</p>
<p><code>$$ X \sim N \left(\mu, \sigma^2\right) $$</code></p>
<p>其概率密度函数为：</p>
<p><code>$$ f \left(x\right) = \dfrac{1}{\sigma \sqrt{2 \pi}} e^{- \dfrac{\left(x - \mu\right)^2}{2 \sigma^2}} $$</code></p>
<figure>
  <img data-src="/images/cn/2020-06-06-bayesian-optimization/univariate-gaussian-distribution.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：https://zh.wikipedia.org/wiki/正态分布</p></figcaption>
</figure>
<h3 id="二元高斯分布">二元高斯分布</h3>
<p>若随机变量 <code>$X, Y$</code> 服从均值为 <code>$\mu = \left(\mu_X, \mu_Y\right)^{\top}$</code>，方差为 <code>$\mu = \left(\sigma_X, \sigma_Y\right)^{\top}$</code> 的高斯分布，则记为：</p>
<p><code>$$ \left(X, Y\right) \sim \mathcal{N} \left(\mu, \sigma\right) $$</code></p>
<p>其概率密度函数为：</p>
<p><code>$$ f(x, y)=\frac{1}{2 \pi \sigma_{X} \sigma_{Y} \sqrt{1-\rho^{2}}} e^{-\dfrac{1}{2\left(1-\rho^{2}\right)}\left[\dfrac{\left(x-\mu_{X}\right)^{2}}{\sigma_{X}^{2}}+\dfrac{\left(y-\mu_{Y}\right)^{2}}{\sigma_{Y}^{2}}-\dfrac{2 \rho\left(x-\mu_{X}\right)\left(y-\mu_{X}\right)}{\sigma_{X} \sigma_{Y}}\right]} $$</code></p>
<p>其中，<code>$\rho$</code> 是 <code>$X$</code> 和 <code>$Y$</code> 之间的相关系数，<code>$\sigma_X &gt; 0$</code> 且 <code>$\sigma_Y &gt; 0$</code>。</p>
<figure>
  <img data-src="/images/cn/2020-06-06-bayesian-optimization/bivariate-gaussian-distribution.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：Bayesian tracking of multiple point targets using expectation maximization</p></figcaption>
</figure>
<h3 id="多元高斯分布">多元高斯分布</h3>
<p>若 <code>$K$</code> 维随机向量 <code>$X = \left[X_1, \cdots, X_K\right]^{\top}$</code> 服从多元高斯分布，则必须满足如下三个等价条件：</p>
<ol>
<li>任何线性组合 <code>$Y = a_1 X_1 + \cdots a_K X_K$</code> 均服从高斯分布。</li>
<li>存在随机向量 <code>$Z = \left[Z_1, \cdots, Z_L\right]^{\top}$</code>（每个元素服从独立标准高斯分布），向量 <code>$\mu = \left[\mu_1, \cdots, \mu_K\right]^{\top}$</code> 以及 <code>$K \times L$</code> 的矩阵 <code>$A$</code>，满足 <code>$X = A Z + \mu$</code>。</li>
<li>存在 <code>$\mu$</code> 和一个对称半正定矩阵 <code>$\Sigma$</code> 满足 <code>$X$</code> 的特征函数 <code>$\phi_X \left(u; \mu, \Sigma\right) = \exp \left(i \mu^{\top} u - \dfrac{1}{2} u^{\top} \Sigma u\right)$</code></li>
</ol>
<p>如果 <code>$\Sigma$</code> 是非奇异的，则概率密度函数为：</p>
<p><code>$$ f \left(x_1, \cdots, x_k\right) = \dfrac{1}{\sqrt{\left(2 \pi\right)^k \lvert\Sigma\rvert}} e^{- \dfrac{1}{2} \left(x - \mu\right)^{\top} \Sigma^{-1} \left(x - \mu\right)} $$</code></p>
<p>其中 <code>$\lvert\Sigma\rvert$</code> 表示协方差矩阵的行列式。</p>
<h3 id="边缘化和条件化">边缘化和条件化</h3>
<p>高斯分布具有一个优秀的代数性质，即在边缘化和条件化下是闭合的，也就是说从这些操作中获取的结果分布也是高斯的。**边缘化（Marginalization）<strong>和</strong>条件化（Conditioning）**都作用于原始分布的子集上：</p>
<p><code>$$ P_{X, Y}=\left[\begin{array}{l} X \\ Y \end{array}\right] \sim \mathcal{N}(\mu, \Sigma)=\mathcal{N}\left(\left[\begin{array}{l} \mu_{X} \\ \mu_{Y} \end{array}\right],\left[\begin{array}{l} \Sigma_{X X} \Sigma_{X Y} \\ \Sigma_{Y X} \Sigma_{Y Y} \end{array}\right]\right) $$</code></p>
<p>其中，<code>$X$</code> 和 <code>$Y$</code> 表示原始随机变量的子集。</p>
<p>对于随机向量 <code>$X$</code> 和 <code>$Y$</code> 的高斯概率分布 <code>$P \left(X, Y\right)$</code>，其边缘概率分布为：</p>
<p><code>$$ \begin{array}{l} X \sim \mathcal{N}\left(\mu_{X}, \Sigma_{X X}\right) \\ Y \sim \mathcal{N}\left(\mu_{Y}, \Sigma_{Y Y}\right) \end{array} $$</code></p>
<p><code>$X$</code> 和 <code>$Y$</code> 两个子集各自只依赖于 <code>$\mu$</code> 和 <code>$\Sigma$</code> 中它们对应的值。因此从高斯分布中边缘化一个随机变量仅需从 <code>$\mu$</code> 和 <code>$\Sigma$</code> 中舍弃相应的变量即可：</p>
<p><code>$$ p_{X}(x)=\int_{y} p_{X, Y}(x, y) d y=\int_{y} p_{X | Y}(x | y) p_{Y}(y) d y $$</code></p>
<p>条件化可以用于得到一个变量在另一个变量条件下的概率分布：</p>
<p><code>$$ \begin{array}{l} X | Y \sim \mathcal{N}\left(\mu_{X}+\Sigma_{X Y} \Sigma_{Y Y}^{-1}\left(Y-\mu_{Y}\right), \Sigma_{X X}-\Sigma_{X Y} \Sigma_{Y Y}^{-1} \Sigma_{Y X}\right) \\ Y | X \sim \mathcal{N}\left(\mu_{Y}+\Sigma_{Y X} \Sigma_{X X}^{-1}\left(X-\mu_{X}\right), \Sigma_{Y Y}-\Sigma_{Y X} \Sigma_{X X}^{-1} \Sigma_{X Y}\right) \end{array} $$</code></p>
<p>需要注意新的均值仅依赖于作为条件的变量，协方差矩阵和这个变量无关。</p>
<p>边缘化可以理解为在高斯分布的一个维度上的累加，条件化可以理解为在多元分布上切一刀从而获得一个维数更少的高斯分布，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-06-06-bayesian-optimization/marginalization-and-conditioning.png" class="lazyload"/>
  
</figure>
<h2 id="高斯过程">高斯过程</h2>
<p>**高斯过程（Gaussian Process）**是观测值出现在一个连续域（例如时间或空间）的随机过程。在高斯过程中，连续输入空间中每个点都是与一个正态分布的随机变量相关联。此外，这些随机变量的每个有限集合都有一个多元正态分布，换句话说它们的任意有限线性组合是一个正态分布。高斯过程的分布是所有那些（无限多个）随机变量的联合分布，正因如此，它是连续域（例如时间或空间）上函数的分布。</p>
<p>简单而言，高斯过程即为一系列随机变量，这些随机变量的任意有限集合均为一个多元高斯分布。从<strong>一元高斯分布</strong>到<strong>多元高斯分布</strong>相当于增加了空间维度，从<strong>高斯分布</strong>到<strong>高斯过程</strong>相当于引入了时间维度。一个高斯过程可以被均值函数 <code>$m \left(x\right)$</code> 和协方差函数 <code>$K \left(x, x'\right)$</code> 共同唯一确定：</p>
<p><code>$$ \begin{aligned} m(x) &amp;=\mathbb{E}[f(x)] \\ K\left(x, x'\right) &amp;=\mathbb{E}\left[(f(x)-m(x))\left(f\left(x^{\prime}\right)-m\left(x^{\prime}\right)\right)\right] \end{aligned} $$</code></p>
<p>则高斯过程可以表示为：</p>
<p><code>$$ f \left(x\right) \sim \mathcal{GP} \left(m \left(x\right), K \left(x, x'\right)\right) $$</code></p>
<p>均值函数决定了样本出现的整体位置，如果为零则表示以 <code>$y = 0$</code> 为基准线。协方差函数描述了不同点之间的关系，从而可以利用输入的训练数据预测未知点的值。常用的协方差函数有：</p>
<ul>
<li>常数：<code>$K_c \left(x, x'\right) = C$</code></li>
<li>线性：<code>$K_L \left(x, x'\right) = x^{\top} x'$</code></li>
<li>高斯噪声：<code>$K_{GN} \left(x, x'\right) = \sigma^2 \delta_{x, x'}$</code></li>
<li>指数平方：<code>$K_{\mathrm{SE}}\left(x, x^{\prime}\right)=\exp \left(-\dfrac{|d|^{2}}{2 \ell^{2}}\right)$</code></li>
<li>Ornstein-Uhlenbeck：<code>$K_{\mathrm{OU}}\left(x, x^{\prime}\right)=\exp \left(-\dfrac{|d|}{\ell}\right)$</code></li>
<li>Matérn：<code>$K_{\text {Matern }}\left(x, x^{\prime}\right)=\dfrac{2^{1-\nu}}{\Gamma(\nu)}\left(\dfrac{\sqrt{2 \nu}|d|}{\ell}\right)^{\nu} K_{\nu}\left(\dfrac{\sqrt{2 \nu}|d|}{\ell}\right)$</code></li>
<li>周期：<code>$K_{\mathrm{P}}\left(x, x^{\prime}\right)=\exp \left(-\dfrac{2 \sin ^{2}\left(\dfrac{d}{2}\right)}{\ell^{2}}\right)$</code></li>
<li>有理平方：<code>$K_{\mathrm{RQ}}\left(x, x^{\prime}\right)=\left(1+|d|^{2}\right)^{-\alpha}, \quad \alpha \geq 0$</code></li>
</ul>
<h2 id="高斯过程回归">高斯过程回归</h2>
<p>回归任务的目标是给定一个输入变量 <code>$x \in \mathbb{R}^D$</code> 预测一个或多个连续目标变量 <code>$y$</code> 的值。更确切的说，给定一个包含 <code>$N$</code> 个观测值的训练集 <code>$\mathbf{X} = \left\{x_n\right\}^N_1$</code> 和对应的目标值 <code>$\mathbf{Y} = \left\{y_n\right\}^N_1$</code>，回归的目标是对于一个新的 <code>$x$</code> 预测对应的 <code>$y$</code>。目标值和观测值之间通过一个映射进行关联：</p>
<p><code>$$ f: X \to Y $$</code></p>
<p>在贝叶斯模型中，我们通过观测数据 <code>$\mathcal{D} = \left\{\left(\mathbf{x}_n, \mathbf{y}_n\right)\right\}^N_{n=1}$</code> 更新先验分布 <code>$P \left(\mathbf{\Theta}\right)$</code>。通过贝叶斯公式我们可以利用先验概率 <code>$P \left(\mathbf{\Theta}\right)$</code> 和似然函数 <code>$P \left(\mathcal{D} | \mathbf{\Theta}\right)$</code> 推导出后验概率：</p>
<p><code>$$ p\left(\mathbf{\Theta} | \mathcal{D}\right)=\frac{p\left(\mathcal{D} | \mathbf{\Theta}\right) p\left(\mathbf{\Theta}\right)}{p\left(\mathcal{D}\right)} $$</code></p>
<p>其中 <code>$p\left(\mathcal{D}\right)$</code> 为边际似然。在贝叶斯回归中我们不仅希望获得未知输入对应的预测值 <code>$\mathbf{y}_*$</code> ，还希望知道预测的不确定性。因此我们需要利用联合分布和边缘化模型参数 <code>$\mathbf{\Theta}$</code> 来构造预测分布：</p>
<p><code>$$ p\left(\mathbf{y}_{*} | \mathbf{x}_{*}, \mathcal{D}\right)=\int p\left(\mathbf{y}_{*}, \mathbf{\Theta} | \mathbf{x}_{*}, \mathcal{D}\right) \mathrm{d} \Theta=\int p\left(\mathbf{y}_{*} | \mathbf{x}_{*}, \mathbf{\Theta}, \mathcal{D}\right) p(\mathbf{\Theta} | \mathcal{D}) \mathrm{d} \mathbf{\Theta} $$</code></p>
<p>通常情况下，由于积分形式 <code>$p \left(\Theta | \mathcal{D}\right)$</code> 不具有解析可解性（Analytically Tractable）：</p>
<p><code>$$ p\left(\mathcal{D}\right)=\int p\left(\mathcal{D} | \mathbf{\Theta}\right) p\left(\mathbf{\Theta}\right) d \Theta $$</code></p>
<p>但在高斯似然和高斯过程先验的前提下，后验采用函数的高斯过程的形式，同时是解析可解的。</p>
<p>对于高斯过程回归，我们构建一个贝叶斯模型，首先定义函数输出的先验为一个高斯过程：</p>
<p><code>$$ p \left(f | \mathbf{X}, \theta\right) = \mathcal{N} \left(\mathbf{0}, K \left(\mathbf{X}, \mathbf{X}\right)\right) $$</code></p>
<p>其中 <code>$K \left(\cdot, \cdot\right)$</code> 为协方差函数，<code>$\theta$</code> 为过程的超参数。假设数据已经变换为零均值，因此我们不需要在先验中设置均值函数，则令似然形式如下：</p>
<p><code>$$ p \left(\mathbf{Y} | f\right) \sim \mathcal{N} \left(f, \sigma^2_n \mathbf{I}\right) $$</code></p>
<p>假设观测值为独立同分布的高斯噪音的累加，则整个模型的联合分布为：</p>
<p><code>$$ p \left(\mathbf{Y} , f | \mathbf{X}, \theta\right) = p \left(\mathbf{Y} | f\right) p \left(f | \mathbf{X}, \theta\right) $$</code></p>
<p>虽然我们并不关心变量 <code>$f$</code>，但由于我们需要对不确定性进行建模，我们仍需考虑 <code>$\mathbf{Y}$</code> 和 <code>$f$</code> 以及 <code>$f$</code> 和 <code>$\mathbf{X}$</code> 之间的关系。高斯过程作为一个非参数模型，其先验分布构建于映射 <code>$f$</code> 之上，<code>$f$</code> 仅依赖于核函数的超参数 <code>$\theta$</code>，且这些超参数可以通过数据进行估计。我们可以将超参数作为先验，即：</p>
<p><code>$$ p \left(\mathbf{Y} , f | \mathbf{X}, \theta\right) = p \left(\mathbf{Y} | f\right) p \left(f | \mathbf{X}, \theta\right) p \left(\theta\right) $$</code></p>
<p>然后进行贝叶斯推断和模型选择，但是通常情况下这是不可解的。David MacKay 引入了一个利用最优化边际似然来估计贝叶斯平均的框架，即计算如下积分：</p>
<p><code>$$ p \left(\mathbf{Y} | \mathbf{X}, \theta\right) = \int p \left(\mathbf{Y} | f\right) p \left(f | \mathbf{X}, \theta\right) df $$</code></p>
<p>其中，高斯似然 <code>$p \left(\mathbf{Y} | f\right)$</code> 表示模型拟合数据的程度，<code>$p \left(f | \mathbf{X}, \theta\right)$</code> 为高斯过程先验。经过边缘化后，<code>$\mathbf{Y}$</code> 不在依赖于 <code>$f$</code> 而仅依赖于 <code>$\theta$</code>。</p>
<p>假设采用零均值函数，对于一个高斯过程先验，我们仅需指定一个协方差函数。以指数平方协方差函数为例，选择一系列测试输入点 <code>$X_*$</code>，利用协方差矩阵和测试输入点可以生成一个高斯向量：</p>
<p><code>$$ \mathbf{f}_* \sim \mathcal{N} \left(\mathbf{0}, K \left(X_*, X_*\right)\right) $$</code></p>
<p>从高斯先验中进行采样，我们首先需要利用标准正态来表示多元正态：</p>
<p><code>$$ \mathbf{f}_* \sim \mu + \mathbf{B} \mathcal{N} \left(0, \mathbf{I}\right) $$</code></p>
<p>其中，<code>$\mathbf{BB}^{\top} = K \left(X_*, X_*\right)$</code>，<code>$\mathbf{B}$</code> 本质上是协方差矩阵的平方根，可以通过 <a href="https://zh.wikipedia.org/wiki/Cholesky%E5%88%86%E8%A7%A3">Cholesky 分解</a>获得。</p>
<figure>
  <img data-src="/images/cn/2020-06-06-bayesian-optimization/gp-prior.png" class="lazyload"/>
  
</figure>
<p>上图（左）为从高斯先验中采样的 10 个序列，上图（右）为先验的协方差。如果输入点 <code>$x_n$</code> 和 <code>$x_m$</code> 接近，则对应的 <code>$f \left(x_n\right)$</code> 和 <code>$f \left(x_m\right)$</code> 相比于不接近的点是强相关的。</p>
<p>我们关注的并不是这些随机的函数，而是如何将训练数据中的信息同先验进行合并。假设观测数据为 <code>$\left\{\left(\mathbf{x}_{i}, f_{i}\right) | i=1, \ldots, n\right\}$</code>，则训练目标 <code>$\mathbf{f}$</code> 和测试目标 <code>$\mathbf{f}_*$</code> 之间的联合分布为：</p>
<p><code>$$ \left[\begin{array}{l} \mathbf{f} \\ \mathbf{f}_{*} \end{array}\right] \sim \mathcal{N}\left(\mathbf{0},\left[\begin{array}{ll} K(X, X) &amp; K\left(X, X_{*}\right) \\ K\left(X_{*}, X\right) &amp; K\left(X_{*}, X_{*}\right) \end{array}\right]\right) $$</code></p>
<p>根据观测值对联合高斯先验分布进行条件化处理可以得到高斯过程回归的关键预测方程：</p>
<p><code>$$ \mathbf{f}_{*} | X, X_{*}, \mathbf{f} \sim \mathcal{N}\left(\overline{\mathbf{f}}_{*}, \operatorname{cov}\left(\mathbf{f}_{*}\right)\right) $$</code></p>
<p>其中</p>
<p><code>$$ \begin{aligned} \overline{\mathbf{f}}_{*} &amp; \triangleq \mathbb{E}\left[\mathbf{f}_{*} | X, X_{*}, \mathbf{f}\right]=K\left(X_{*}, X\right) K(X, X)^{-1} \mathbf{f} \\ \operatorname{cov}\left(\mathbf{f}_{*}\right) &amp;=K\left(X_{*}, X_{*}\right)-K\left(X_{*}, X\right) K(X, X)^{-1} K\left(X, X_{*}\right) \end{aligned} $$</code></p>
<p>函数值可以通过对联合后验分布采样获得。</p>
<p>我们以三角函数作为给定的函数，并随机采样一些训练数据 <code>$\left\{\left(\mathbf{x}_{i}, f_{i}\right) | i=1, \ldots, n\right\}$</code>，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-06-06-bayesian-optimization/underlying-functions-and-training-points.png" class="lazyload"/>
  
</figure>
<p>我们希望将训练数据和高斯过程先验进行合并得到联合后验分布，我们可以通过在观测值上条件化联合高斯先验分布，预测的均值和协方差为：</p>
<p><code>$$ \begin{aligned} \overline{\mathbf{f}}_{*} &amp;=K\left(X_{*}, X\right) K(X, X)^{-1} \mathbf{f} \\ \operatorname{cov}\left(\mathbf{f}_{*}\right) &amp;=K\left(X_{*}, X_{*}\right)-K\left(X_{*}, X\right) K(X, X)^{-1} K\left(X, X_{*}\right) \end{aligned} $$</code></p>
<p><a href="http://www.gaussianprocess.org/gpml/">Rasmussen 和 Williams</a> 给出了一个实现高斯过程回归的实用方法：</p>


<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/pseudocode@latest/build/pseudocode.min.css">


<div><pre class="pseudocode">
\begin{algorithm}
\caption{高斯过程回归算法}
\begin{algorithmic}
\REQUIRE \\
    输入 $\mathbf{X}$ \\
    目标 $\mathbf{y}$ \\
    协方差函数 $k$ \\
    噪音水平 $\sigma^2_n$ \\
    测试输入 $\mathbf{x}_*$
\ENSURE \\
    均值 $\bar{f}_*$ \\
    方差 $\mathbb{V}\left[f_{*}\right]$
\FUNCTION{GaussianProcessRegression}{$\mathbf{X}, \mathbf{y}, k, \sigma^2_n, \mathbf{x}_*$}
\STATE $L \gets \text{cholesky} \left(K + \sigma^2_n I\right)$
\STATE $\alpha \gets L^{\top} \setminus \left(L \setminus \mathbf{y}\right)$
\STATE $\bar{f}_* \gets \mathbf{k}^{\top}_* \alpha$
\STATE $\mathbf{v} \gets L \setminus \mathbf{k}_*$
\STATE $\mathbb{V}\left[f_{*}\right] \gets k \left(\mathbf{x}_*, \mathbf{x}_*\right) - \mathbf{v}^{\top} \mathbf{v}$
\RETURN $\bar{f}_*, \mathbb{V}\left[f_{*}\right]$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>高斯过程后验和采样的序列如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-06-06-bayesian-optimization/gp-posterior.png" class="lazyload"/>
  
</figure>
<p>先验的协方差矩阵和后验的协方差矩阵可视化如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-06-06-bayesian-optimization/prior-posterior-convariance.png" class="lazyload"/>
  
</figure>
<p>本小结代码请参见<a href="https://github.com/leovan/leovan.me/blob/master/scripts/cn/2020-06-06-bayesian-optimization/gaussian-process-regression.py">这里</a>。</p>
<h2 id="贝叶斯优化">贝叶斯优化</h2>
<h3 id="主动学习">主动学习</h3>
<p>在很多机器学习问题中，数据标注往往需要耗费很大成本。**主动学习（Active Learning）**在最大化模型准确率时最小化标注成本，例如对不确定性最高的数据进行标注。由于我们仅知道少量数据点，因此我们需要一个代理模型（Surrogate Model）来建模真正的模型。高斯过程因其灵活性和具有估计不确定性估计的特性不失为一个常用的代理模型。</p>
<p>在估计 <code>$f \left(x\right)$</code> 的过程中，我们希望最小化评估的次数，因此我们可以通过主动学习来“智能”地选择下一个评估的数据点。通过不断的选择具有最高不确定性的数据点来获得 <code>$f \left(x\right)$</code> 更准确的估计，直至收敛或达到停止条件。下图展示了利用主动学习估计真实数据分布的过程：</p>

  
  <link rel="stylesheet" href="/css/figure-slider.css"/>


<div class="figure-slider">
  <div class="figure-slider-parameters">
    <span class="base-url">/images/cn/2020-06-06-bayesian-optimization/</span>
    <span class="image-filename-prefix">active-gp-</span>
    <span class="image-format">png</span>
    <span class="milliseconds">300</span>
  </div>
  <div class="figure-slider-image-container">
    <img class="figure-slider-image" src='/images/cn/2020-06-06-bayesian-optimization/active-gp-0.png'>
  </div>
  <div class="figure-slider-controls">
    <div class="figure-slider-buttons">
      <button class="figure-slider-button-previous">
        <span class="mdi mdi-skip-previous"></span>
      </button>
    </div>
    <div class="figure-slider-scroll-bar-container">
      <input class="figure-slider-scroll-bar" type="range" min='0' max='9' value='0'>
    </div>
    <div class="figure-slider-buttons">
      <button class="figure-slider-button-next">
        <span class="mdi mdi-skip-next"></span>
      </button>
    </div>
    <div class="figure-slider-buttons">
      <button class="figure-slider-button-play-pause figure-slider-button-play">
        <span class="mdi mdi-play"></span>
      </button>
    </div>
  </div>
</div>

<h3 id="贝叶斯优化问题">贝叶斯优化问题</h3>
<p>贝叶斯优化的核心问题是：基于现有的已知情况，如果选择下一步评估的数据点？在主动学习中我们选择不确定性最大的点，但在贝叶斯优化中我们需要在探索不确定性区域（探索）和关注已知具有较优目标值的区域之间进行权衡（开发）。这种评价的依据称之为<strong>采集函数（Acquisition Functions）</strong>，采集函数通过当前模型启发式的评估是否选择一个数据点。</p>
<p>贝叶斯优化的目标是找到一个函数 <code>$f: \mathbb{R}^d \mapsto \mathbb{R}$</code> 最大值（或最小值）对应的位置 <code>$x \in \mathbb{R}^d$</code>。为了解决这个问题，我们遵循如下算法：</p>
<ol>
<li>选择一个代理模型用于建模真实函数 <code>$f$</code> 和定义其先验。</li>
<li>给定观测集合，利用贝叶斯公式获取后验。</li>
<li>利用采集函数 <code>$\alpha \left(x\right)$</code> 确性下一个采样点 <code>$x_t = \arg\max_x \alpha \left(x\right)$</code>。</li>
<li>将采样的点加入观测集合，重复步骤 2 直至收敛或达到停止条件。</li>
</ol>
<h3 id="采集函数">采集函数</h3>
<ul>
<li>Probability of Improvement (PI)</li>
</ul>
<p>Probability of Improvement (PI) 采集函数会选择具有最大可能性提高当前最大的 <code>$f \left(x^{+}\right)$</code> 值的点作为下一个查询点，即：</p>
<p><code>$$ x_{t+1} = \arg\max \left(\alpha_{PI} \left(x\right)\right) = \arg\max \left(P \left(f \left(x\right)\right) \geq \left(f \left(x^{+}\right) + \epsilon\right)\right) $$</code></p>
<p>其中，<code>$P \left(\cdot\right)$</code> 表示概率，<code>$\epsilon$</code> 为一个较小的正数，<code>$x^{+} = \arg\max_{x_i \in x_{1:t}} f \left(x_i\right)$</code>，<code>$x_i$</code> 为第 <code>$i$</code> 步查询点的位置。如果采用高斯过程作为代理模型，上式则转变为：</p>
<p><code>$$ x_{t+1} = \arg\max_x \Phi \left(\dfrac{\mu_t \left(x\right) - f \left(x^{+}\right) - \epsilon}{\sigma_t \left(x\right)}\right) $$</code></p>
<p>其中，<code>$\Phi \left(\cdot\right)$</code> 表示标准正态分布累积分布函数。PI 利用 <code>$\epsilon$</code> 来权衡探索和开发，增加 <code>$\epsilon$</code> 的值会更加倾向进行探索。</p>
<ul>
<li>Expected Improvement (EI)</li>
</ul>
<p>PI 仅关注了有多大的可能性能够提高，而没有关注能够提高多少。Expected Improvement (EI) 则会选择具有最大期望提高的点作为下一个查询点，即：</p>
<p><code>$$ x_{t+1} = \arg\min_x \mathbb{E} \left(\left\|h_{t+1} \left(x\right) - f \left(x^*\right)\right\| | \mathcal{D}_t\right) $$</code></p>
<p>其中，<code>$f$</code> 为真实函数，<code>$h_{t+1}$</code> 为代理模型在 <code>$t+1$</code> 步的后验均值，<code>$\mathcal{D}_t = \left\{\left(x_i, f\left(x_i\right)\right)\right\}, \forall x \in x_{1:t}$</code> 为训练数据，<code>$x^*$</code> 为 <code>$f$</code> 取得最大值的真实位置。</p>
<p>上式中我们希望选择能够最小化与最大目标值之间距离的点，由于我们并不知道真实函数 <code>$f$</code>，Mockus <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 提出了一种解决办法：</p>
<p><code>$$ x_{t+1} = \arg\max_x \mathbb{E} \left(\max \left\{0, h_{t+1} \left(x\right) - f \left(x^{+}\right)\right\} | \mathcal{D}_t\right) $$</code></p>
<p>其中，<code>$f \left(x^{+}\right)$</code> 为到目前为止遇见的最大函数值，如果采用高斯过程作为代理模型，上式则转变为：</p>
<p><code>$$ \begin{aligned} EI(x) &amp;= \left\{\begin{array}{ll} \left(\mu_{t}(x)-f\left(x^{+}\right)-\epsilon\right) \Phi(Z)+\sigma_{t}(x) \phi(Z), &amp; \text { if } \sigma_{t}(x)&gt;0 \\ 0 &amp; \text { if } \sigma_{t}(x)=0 \end{array}\right. \\ Z &amp;= \frac{\mu_{t}(x)-f\left(x^{+}\right)-\epsilon}{\sigma_{t}(x)} \end{aligned} $$</code></p>
<p>其中 <code>$\Phi \left(\cdot\right)$</code> 表示标准正态分布累积分布函数，<code>$\phi \left(\cdot\right)$</code> 表示标准正态分布概率密度函数。类似 PI，EI 也可以利用 <code>$\epsilon$</code> 来权衡探索和开发，增加 <code>$\epsilon$</code> 的值会更加倾向进行探索。</p>
<ul>
<li>对比和其他采集函数</li>
</ul>
<figure>
  <img data-src="/images/cn/2020-06-06-bayesian-optimization/pi-vs-ei.svg" class="lazyload"/>
  
</figure>
<p>上图展示了在仅包含一个训练观测数据 <code>$\left(0.5, f \left(0.5\right)\right)$</code> 情况下不同点的采集函数值。可以看出 <code>$\alpha_{EI}$</code> 和 <code>$\alpha_{PI}$</code> 的最大值分别为 0.3 和 0.47。选择一个具有较小的 <code>$\alpha_{PI}$</code> 和一个较大的 <code>$\alpha_{EI}$</code> 的点可以理解为一个高的风险和高的回报。因此，当多个点具有相同的 <code>$\alpha_{EI}$</code> 时，我们应该优先选择具有较小风险（高 <code>$\alpha_{PI}$</code>）的点，类似的，当多个点具有相同的 <code>$\alpha_{PI}$</code> 时，我们应该优先选择具有较大回报（高 <code>$\alpha_{EI}$</code>）的点。</p>
<p>其他采集函数还有 Thompson Sampling <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>，Upper Confidence Bound (UCB)，Gaussian Process Upper Confidence Bound (GP-UCB) <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>，Entropy Search <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>，Predictive Entropy Search <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> 等，细节请参见原始论文或 A Tutorial on Bayesian Optimization <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>。</p>
<h2 id="开放资源">开放资源</h2>
<ul>
<li><a href="https://github.com/scikit-optimize/scikit-optimize">scikit-optimize/scikit-optimize</a></li>
<li><a href="https://github.com/hyperopt/hyperopt">hyperopt/hyperopt</a></li>
<li><a href="https://github.com/automl/SMAC3">automl/SMAC3</a></li>
<li><a href="https://github.com/fmfn/BayesianOptimization">fmfn/BayesianOptimization</a></li>
<li><a href="https://github.com/pytorch/botorch">pytorch/botorch</a></li>
<li><a href="https://github.com/GPflow/GPflowOpt">GPflow/GPflowOpt</a></li>
<li><a href="https://github.com/keras-team/keras-tuner">keras-team/keras-tuner</a></li>
<li><a href="https://github.com/tobegit3hub/advisor">tobegit3hub/advisor</a></li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Mockus, J. B., &amp; Mockus, L. J. (1991). Bayesian approach to global optimization and application to multiobjective and constrained problems. <em>Journal of Optimization Theory and Applications, 70</em>(1), 157-172.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Thompson, W. R. (1933). On the likelihood that one unknown probability exceeds another in view of the evidence of two samples. <em>Biometrika, 25</em>(3/4), 285-294.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Auer, P. (2002). Using confidence bounds for exploitation-exploration trade-offs. <em>Journal of Machine Learning Research, 3</em>(Nov), 397-422.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Hennig, P., &amp; Schuler, C. J. (2012). Entropy search for information-efficient global optimization. <em>Journal of Machine Learning Research, 13</em>(Jun), 1809-1837.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Hernández-Lobato, J. M., Hoffman, M. W., &amp; Ghahramani, Z. (2014). Predictive entropy search for efficient global optimization of black-box functions. <em>In Advances in neural information processing systems</em> (pp. 918-926).&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Frazier, P. I. (2018). A tutorial on bayesian optimization. <em>arXiv preprint arXiv:1807.02811</em>.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>马尔可夫决策过程 (Markov Decision Process)</title><link>https://zeqiang.fun/cn/2020/05/markov-decision-process/</link><pubDate>Sat, 23 May 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/05/markov-decision-process/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">《强化学习系列》</a>文章<br>
本文内容主要参考自：<br>
1.《强化学习》<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup><br>
2. CS234: Reinforcement Learning <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup><br>
3. UCL Course on RL <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
</blockquote>
<h2 id="马尔可夫模型">马尔可夫模型</h2>
<p>马尔可夫模型是一种用于序列数据建模的随机模型，其假设未来的状态仅取决于当前的状态，即：</p>
<p><code>$$ \mathbb{P} \left[S_{t+1} | S_t\right] = \mathbb{P} \left[S_{t+1} | S_1, \cdots, S_t\right] $$</code></p>
<p>也就是认为当前状态捕获了历史中所有相关的信息。根据系统状态是否完全可被观测以及系统是自动的还是受控的，可以将马尔可夫模型分为 4 种，如下表所示：</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>状态状态完全可被观测</th>
          <th>系统状态不是完全可被观测</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>状态是自动的</strong></td>
          <td>马尔可夫链（MC）</td>
          <td>隐马尔可夫模型（HMM）</td>
      </tr>
      <tr>
          <td><strong>系统是受控的</strong></td>
          <td>马尔可夫决策过程（MDP）</td>
          <td>部分可观测马尔可夫决策过程（POMDP）</td>
      </tr>
  </tbody>
</table>
<p>马尔可夫链（Markov Chain，MC）为从一个状态到另一个状态转换的随机过程，当马尔可夫链的状态只能部分被观测到时，即为<a href="/cn/2020/05/hmm-crf-and-sequence-labeling/">隐马尔可夫模型（Hidden Markov Model，HMM）</a>，也就是说观测值与系统状态有关，但通常不足以精确地确定状态。马尔可夫决策过程（Markov Decision Process，MDP）也是马尔可夫链，但其状态转移取决于当前状态和采取的动作，通常一个马尔可夫决策过程用于计算依据期望回报最大化某些效用的行动策略。部分可观测马尔可夫决策过程（Partially Observable Markov Decision Process，POMDP）即为系统状态仅部分可见情况下的马尔可夫决策过程。</p>
<h2 id="马尔可夫过程">马尔可夫过程</h2>
<p>对于一个马尔可夫状态 <code>$s$</code> 和一个后继状态 <code>$s'$</code>，状态转移概率定义为：</p>
<p><code>$$ \mathcal{P}_{ss'} = \mathbb{P} \left[S_t = s' | S_{t-1} = s\right] $$</code></p>
<p><strong>状态概率矩阵</strong> <code>$\mathcal{P}$</code> 定义了从所有状态 <code>$s$</code> 到后继状态 <code>$s'$</code> 的转移概率：</p>
<p><code>$$ \mathcal{P} = \left[\begin{array}{ccc} \mathcal{P}_{11} &amp; \cdots &amp; \mathcal{P}_{1 n} \\ \vdots &amp; &amp; \\ \mathcal{P}_{n 1} &amp; \cdots &amp; \mathcal{P}_{n n} \end{array}\right] $$</code></p>
<p>其中每一行的加和为 1。</p>
<p>**马尔可夫过程（马尔可夫链）**是一个无记忆的随机过程，一个马尔可夫过程可以定义为 <code>$\langle \mathcal{S}, \mathcal{P} \rangle$</code>，其中 <code>$\mathcal{S}$</code> 是一个有限状态集合，<code>$\mathcal{P}_{ss'} = \mathbb{P} \left[S_t = s' | S_{t-1} = s\right]$</code>，<code>$\mathcal{P}$</code> 为状态转移概率矩阵。以一个学生的日常生活为例，Class <code>$i$</code> 表示第 <code>$i$</code> 门课程，Facebook 表示在 Facebook 上进行社交，Pub 表示去酒吧，Pass 表示通过考试，Sleep 表示睡觉，这个马尔可夫过程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/student-markov-chain.png" class="lazyload"/>
  
</figure>
<p>从而可以产生多种不同的序列，例如：</p>
<pre><code>C1 -&gt; C2 -&gt; C3 -&gt; Pass -&gt; Sleep
C1 -&gt; FB -&gt; FB -&gt; C1 -&gt; C2 -&gt; Sleep
C1 -&gt; C2 -&gt; C3 -&gt; Pub -&gt; C2 -&gt; C3 -&gt; Pass -&gt; Sleep
</code></pre>
<p>状态转移概率矩阵如下所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/student-markov-chain-transition-matrix.png" class="lazyload"/>
  
</figure>
<p>据此我们可以定义<strong>马尔可夫奖励过程（Markov Reward Process，MRP）<strong>为 <code>$\langle \mathcal{S, P, R}, \gamma \rangle$</code>，其中 <code>$\mathcal{S}$</code> 和 <code>$\mathcal{P}$</code> 同马尔可夫过程定义中的参数相同，<code>$\mathcal{R}$</code> 为收益函数，<code>$\mathcal{R}_s = \mathbb{E} \left[R_t | S_{t-1} = s\right]$</code>，<code>$\gamma \in \left[0, 1\right]$</code> 为</strong>折扣率</strong>。如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/student-mrp.png" class="lazyload"/>
  
</figure>
<p><strong>期望回报</strong> <code>$G_t$</code> 定义为从时刻 <code>$t$</code> 之后的所有衰减的收益之和，即：</p>
<p><code>$$ G_t = R_{t+1} + \gamma R_{t+2} + \cdots = \sum_{k=0}^{\infty} \gamma^k R_{t+k+1} $$</code></p>
<p>当 <code>$\gamma$</code> 接近 <code>$0$</code> 时，智能体更倾向于近期收益，当 <code>$\gamma$</code> 接近 <code>$1$</code> 时，智能体更侧重考虑长远收益。邻接时刻的收益可以按如下递归方式表示：</p>
<p><code>$$ G_t = R_{t+1} + \gamma G_{t+1} $$</code></p>
<p>对于存在“最终时刻”的应用中，智能体和环境的交互能被自然地分成一个系列子序列，每个子序列称之为“<strong>幕（episodes）</strong>”，例如一盘游戏、一次走迷宫的过程，每幕都以一种特殊状态结束，称之为<strong>终结状态</strong>。这些幕可以被认为在同样的终结状态下结束，只是对不同的结果有不同的收益，具有这种<strong>分幕</strong>重复特性的任务称之为<strong>分幕式任务</strong>。</p>
<p>MRP 的状态价值函数 <code>$v \left(s\right)$</code> 给出了状态 <code>$s$</code> 的长期价值，定义为：</p>
<p><code>$$ \begin{aligned} v(s) &amp;=\mathbb{E}\left[G_{t} | S_{t}=s\right] \\ &amp;=\mathbb{E}\left[R_{t+1}+\gamma R_{t+2}+\gamma^{2} R_{t+3}+\ldots | S_{t}=s\right] \\ &amp;=\mathbb{E}\left[R_{t+1}+\gamma\left(R_{t+2}+\gamma R_{t+3}+\ldots\right) | S_{t}=s\right] \\ &amp;=\mathbb{E}\left[R_{t+1}+\gamma G_{t+1} | S_{t}=s\right] \\ &amp;=\mathbb{E}\left[R_{t+1}+\gamma v\left(S_{t+1}\right) | S_{t}=s\right] \end{aligned} $$</code></p>
<p>价值函数可以分解为两部分：即时收益 <code>$R_{t+1}$</code> 和后继状态的折扣价值 <code>$\gamma v \left(S_{t+1}\right)$</code>。上式我们称之为<strong>贝尔曼方程（Bellman Equation）</strong>，其衡量了状态价值和后继状态价值之间的关系。</p>
<h2 id="马尔可夫决策过程">马尔可夫决策过程</h2>
<p>一个**马尔可夫决策过程（Markov Decision Process，MDP）**定义为包含决策的马尔可夫奖励过程 <code>$\langle\mathcal{S}, \mathcal{A}, \mathcal{P}, \mathcal{R}, \gamma\rangle$</code>，在这个环境中所有的状态均具有马尔可夫性。其中，<code>$\mathcal{S}$</code> 为有限的状态集合，<code>$\mathcal{A}$</code> 为有限的动作集合，<code>$\mathcal{P}$</code> 为状态转移概率矩阵，<code>$\mathcal{P}_{s s^{\prime}}^{a}=\mathbb{P}\left[S_{t+1}=s^{\prime} | S_{t}=s, A_{t}=a\right]$</code>，<code>$\mathcal{R}$</code> 为奖励函数，<code>$\mathcal{R}_{s}^{a}=\mathbb{E}\left[R_{t+1} | S_{t}=s, A_{t}=a\right]$</code>，<code>$\gamma \in \left[0, 1\right]$</code> 为折扣率。上例中的马尔可夫决策过程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/student-mdp.png" class="lazyload"/>
  
</figure>
<p>**策略（Policy）**定义为给定状态下动作的概率分布：</p>
<p><code>$$ \pi \left(a | s\right) = \mathbb{P} \left[A_t = a | S_t = s\right] $$</code></p>
<p>一个策略完全确定了一个智能体的行为，同时 MDP 策略仅依赖于当前状态。给定一个 MDP <code>$\mathcal{M}=\langle\mathcal{S}, \mathcal{A}, \mathcal{P}, \mathcal{R}, \gamma\rangle$</code> 和一个策略 <code>$\pi$</code>，状态序列 <code>$S_1, S_2, \cdots$</code> 为一个马尔可夫过程 <code>$\langle \mathcal{S}, \mathcal{P}^{\pi} \rangle$</code>，状态和奖励序列 <code>$S_1, R_2, S_2, \cdots$</code> 为一个马尔可夫奖励过程 <code>$\left\langle\mathcal{S}, \mathcal{P}^{\pi}, \mathcal{R}^{\pi}, \gamma\right\rangle$</code>，其中</p>
<p><code>$$ \begin{aligned} \mathcal{P}_{s s^{\prime}}^{\pi} &amp;=\sum_{a \in \mathcal{A}} \pi(a | s) \mathcal{P}_{s s^{\prime}}^{a} \\ \mathcal{R}_{s}^{\pi} &amp;=\sum_{a \in \mathcal{A}} \pi(a | s) \mathcal{R}_{s}^{a} \end{aligned} $$</code></p>
<p>在策略 <code>$\pi$</code> 下，状态 <code>$s$</code> 的价值函数记为 <code>$v_{\pi} \left(s\right)$</code>，即从状态 <code>$s$</code> 开始，智能体按照策略进行决策所获得的回报的概率期望值，对于 MDP 其定义为：</p>
<p><code>$$ \begin{aligned} v_{\pi} \left(s\right) &amp;= \mathbb{E}_{\pi} \left[G_t | S_t = s\right] \\ &amp;= \mathbb{E}_{\pi} \left[\sum_{k=0}^{\infty} \gamma^k R_{t+k+1} | S_t = s\right] \end{aligned} $$</code></p>
<p>在策略 <code>$\pi$</code> 下，在状态 <code>$s$</code> 时采取动作 <code>$a$</code> 的价值记为 <code>$q_\pi \left(s, a\right)$</code>，即根据策略 <code>$\pi$</code>，从状态 <code>$s$</code> 开始，执行动作 <code>$a$</code> 之后，所有可能的决策序列的期望回报：</p>
<p><code>$$ \begin{aligned} q_\pi \left(s, a\right) &amp;= \mathbb{E}_{\pi} \left[G_t | S_t = s, A_t = a\right] \\ &amp;= \mathbb{E}_{\pi} \left[\sum_{k=0}^{\infty} \gamma^k R_{t+k+1} | S_t = s, A_t = a\right] \end{aligned} $$</code></p>
<p>状态价值函数 <code>$v_{\pi}$</code> 和动作价值函数 <code>$q_{\pi}$</code> 都能从经验中估计得到，两者都可以分解为当前和后继两个部分：</p>
<p><code>$$ \begin{aligned} v_{\pi}(s) &amp;= \mathbb{E}_{\pi}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s\right] \\ q_{\pi}(s, a) &amp;= \mathbb{E}_{\pi}\left[R_{t+1}+\gamma q_{\pi}\left(S_{t+1}, A_{t+1}\right) | S_{t}=s, A_{t}=a\right] \end{aligned} $$</code></p>
<p>从一个状态 <code>$s$</code> 出发，采取一个行动 <code>$a$</code>，状态价值函数为：</p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/bellman-expection-eq-state-value-1.png" class="lazyload"/>
  
</figure>
<p><code>$$ v_{\pi}(s)=\sum_{a \in \mathcal{A}} \pi(a | s) q_{\pi}(s, a) $$</code></p>
<p>从一个动作 <code>$s$</code> 出发，再采取一个行动 <code>$a$</code> 后，动作价值函数为：</p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/bellman-expection-eq-action-value-1.png" class="lazyload"/>
  
</figure>
<p><code>$$ q_{\pi}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{\pi}\left(s^{\prime}\right) $$</code></p>
<p>利用后继状态价值函数表示当前状态价值函数为：</p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/bellman-expection-eq-state-value-2.png" class="lazyload"/>
  
</figure>
<p><code>$$ v_{\pi}(s)=\sum_{a \in \mathcal{A}} \pi(a | s)\left(\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{\pi}\left(s^{\prime}\right)\right) $$</code></p>
<p>利用后继动作价值函数表示当前动作价值函数为：</p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/bellman-expection-eq-action-value-2.png" class="lazyload"/>
  
</figure>
<p><code>$$ q_{\pi}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} \sum_{a^{\prime} \in \mathcal{A}} \pi\left(a^{\prime} | s^{\prime}\right) q_{\pi}\left(s^{\prime}, a^{\prime}\right) $$</code></p>
<p><strong>最优状态价值函数</strong> <code>$v_* \left(s\right)$</code> 定义为所有策略上最大值的状态价值函数：</p>
<p><code>$$ v_* \left(s\right) = \mathop{\max}_{\pi} v_{\pi} \left(s\right) $$</code></p>
<p><strong>最优动作价值函数</strong> <code>$q_* \left(s, a\right)$</code> 定义为所有策略上最大值的动作价值函数：</p>
<p><code>$$ q_* \left(s, a\right) = \mathop{\max}_{\pi} q_{\pi} \left(s, a\right) $$</code></p>
<p>定义不同策略之间的大小关系为：</p>
<p><code>$$ \pi \geq \pi^{\prime} \text { if } v_{\pi}(s) \geq v_{\pi^{\prime}}(s), \forall s $$</code></p>
<p>对于任意一个马尔可夫决策过程有：</p>
<ul>
<li>存在一个比其他策略更优或相等的策略，<code>$\pi_* \geq \pi, \forall \pi$</code></li>
<li>所有的最优策略均能够获得最优的状态价值函数，<code>$v_{\pi_*} \left(s\right) = v_* \left(s\right)$</code></li>
<li>所有的最优策略均能够获得最优的动作价值函数，<code>$q_{\pi_*} \left(s, a\right) = q_* \left(s, a\right)$</code></li>
</ul>
<p>一个最优策略可以通过最大化 <code>$q_* \left(s, a\right)$</code> 获得：</p>
<p><code>$$ \pi_{*}(a | s)=\left\{\begin{array}{ll} 1 &amp; \text { if } a=\underset{a \in \mathcal{A}}{\operatorname{argmax}} q_{*}(s, a) \\ 0 &amp; \text { otherwise } \end{array}\right. $$</code></p>
<p>对于任意一个 MDP 均会有一个确定的最优策略，如果已知 <code>$q_* \left(s, a\right)$</code> 即可知晓最优策略。</p>
<p>最优状态价值函数循环依赖于贝尔曼最优方程：</p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/bellman-optimality-eq-state-value-1.png" class="lazyload"/>
  
</figure>
<p><code>$$ v_{*}(s)=\max _{a} q_{*}(s, a) $$</code></p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/bellman-optimality-eq-action-value-1.png" class="lazyload"/>
  
</figure>
<p><code>$$ q_{*}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{*}\left(s^{\prime}\right) $$</code></p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/bellman-optimality-eq-state-value-2.png" class="lazyload"/>
  
</figure>
<p><code>$$ v_{*}(s)=\max _{a} \mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{*}\left(s^{\prime}\right) $$</code></p>
<figure>
  <img data-src="/images/cn/2020-05-23-markov-decision-process/bellman-optimality-eq-action-value-2.png" class="lazyload"/>
  
</figure>
<p><code>$$ q_{*}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} \max _{a^{\prime}} q_{*}\left(s^{\prime}, a^{\prime}\right) $$</code></p>
<p>显式求解贝尔曼最优方程给出了找到一个最优策略的方法，但这种解法至少依赖于三条实际情况很难满足的假设：</p>
<ol>
<li>准确地知道环境的动态变化特性</li>
<li>有足够的计算资源来求解</li>
<li>马尔可夫性质</li>
</ol>
<p>尤其是假设 2 很难满足，现实问题中状态的数量一般很大，即使利用最快的计算机也需要花费难以接受的时间才能求解完成。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Sutton, R. S., &amp; Barto, A. G. (2018). <em>Reinforcement learning: An introduction</em>. MIT press.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>CS234: Reinforcement Learning <a href="http://web.stanford.edu/class/cs234/index.html">http://web.stanford.edu/class/cs234/index.html</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>UCL Course on RL <a href="https://www.davidsilver.uk/teaching">https://www.davidsilver.uk/teaching</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>多臂赌博机 (Multi-armed Bandit)</title><link>https://zeqiang.fun/cn/2020/05/multi-armed-bandit/</link><pubDate>Sat, 16 May 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/05/multi-armed-bandit/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">《强化学习系列》</a>文章<br>
本文内容主要参考自《强化学习》<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
</blockquote>
<h2 id="多臂赌博机问题">多臂赌博机问题</h2>
<p>一个赌徒，要去摇老虎机，走进赌场一看，一排老虎机，外表一模一样，但是每个老虎机吐钱的概率可不一样，他不知道每个老虎机吐钱的概率分布是什么，那么每次该选择哪个老虎机可以做到最大化收益呢？这就是<strong>多臂赌博机问题 (Multi-armed bandit problem, K- or N-armed bandit problem, MAB)</strong> <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>。</p>
<figure>
  <img data-src="/images/cn/2020-05-16-multi-armed-bandit/compulsive-gambling.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：http://hagencartoons.com/cartoons_166_170.html</p></figcaption>
</figure>
<p><code>$k$</code> 臂赌博机问题中，<code>$k$</code> 个动作的每一个在被选择时都有一个期望或者平均收益，称之为这个动作的**“价值”**。令 <code>$t$</code> 时刻选择的动作为 <code>$A_t$</code>，对应的收益为 <code>$R_t$</code>，任一动作 <code>$a$</code> 对应的价值为 <code>$q_* \left(a\right)$</code>，即给定动作 <code>$a$</code> 时收益的期望：</p>
<p><code>$$ q_* \left(a\right) = \mathbb{E} \left[R_t | A_t = a\right] $$</code></p>
<p>我们将对动作 <code>$a$</code> 在时刻 <code>$t$</code> 的价值的估计记做 <code>$Q_t \left(a\right)$</code>，我们希望它接近 <code>$q_* \left(a\right)$</code>。</p>
<p>如果持续对动作的价值进行估计，那么在任一时刻都会至少有一个动作的估计价值是最高的，将这些对应最高估计价值的动作成为<strong>贪心</strong>的动作。当从这些动作中选择时，称此为<strong>开发</strong>当前所知道的关于动作的价值的知识。如果不是如此，而是选择非贪心的动作，称此为<strong>试探</strong>，因为这可以让你改善对非贪心动作的价值的估计。“开发”对于最大化当前这一时刻的期望收益是正确的做法，但是“试探”从长远来看可能会带来总体收益的最大化。到底选择“试探”还是“开发”一种复杂的方式依赖于我们得到的函数估计、不确定性和剩余时刻的精确数值。</p>
<h2 id="动作价值估计方法">动作价值估计方法</h2>
<p>我们以一种自然的方式，就是通过计算实际收益的平均值来估计动作的价值：</p>
<p><code>$$ \begin{aligned} Q_t \left(a\right) &amp;= \dfrac{t \text{ 时刻前执行动作 } a \text{ 得到的收益总和 }}{t \text{ 时刻前执行动作 } a \text{ 的次数}} \\ &amp;= \dfrac{\sum_{i=1}^{t-1}{R_i \cdot \mathbb{1}_{A_i = a}}}{\sum_{i=1}^{t-1}{\mathbb{1}_{A_i = a}}} \end{aligned} $$</code></p>
<p>其中，<code>$\mathbb{1}_{\text{predicate}}$</code> 表示随机变量，当 predicate 为真时其值为 1，反之为 0。当分母为 0 时，<code>$Q_t \left(a\right) = 0$</code>，当分母趋向无穷大时，根据大数定律，<code>$Q_t \left(a\right)$</code> 会收敛到 <code>$q_* \left(a\right)$</code>。这种估计动作价值的方法称为<strong>采样平均方法</strong>，因为每一次估计都是对相关收益样本的平均。</p>
<p>当然，这只是估计动作价值的一种方法，而且不一定是最好的方法。例如，我们也可以利用累积遗憾（Regret）来评估动作的价值：</p>
<p><code>$$ \rho = T \mu^* - \sum_{t=1}^{T} \hat{r}_t $$</code></p>
<p>其中，<code>$\mu^* = \mathop{\max}_{k} \left\{\mu_k\right\}$</code> 为最大的回报，<code>$\hat{r}_t$</code> 为 <code>$t$</code> 时刻的回报。</p>
<h2 id="多臂赌博机算法">多臂赌博机算法</h2>
<p>以 10 臂赌博机为例，动作的收益分布如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-16-multi-armed-bandit/action-reward-distribution.png" class="lazyload"/>
  
</figure>
<p>动作的真实价值 <code>$q_* \left(a\right), a = 1, \cdots, 10$</code> 为从一个均值为 0 方差为 1 的标准正态分布中选择。当对于该问题的学习方法在 <code>$t$</code> 时刻选择 <code>$A_t$</code> 时，实际的收益 <code>$R_t$</code> 则由一个均值为 <code>$q_* \left(A_t\right)$</code> 方差为 1 的正态分布决定。</p>
<h3 id="epsilon-greedy"><code>$\epsilon$</code>-Greedy</h3>
<p><code>$\epsilon$</code>-Greedy 采用的动作选择逻辑如下：</p>
<ul>
<li>确定一个 <code>$\epsilon \in \left(0, 1\right)$</code>。</li>
<li>每次以 <code>$\epsilon$</code> 的概率随机选择一个臂，以 <code>$1 - \epsilon$</code> 选择平均收益最大的那个臂。</li>
</ul>
<p>下图分别展示了 <code>$\epsilon = 0$</code>（贪婪），<code>$\epsilon = 0.01$</code> 和 <code>$\epsilon = 0.1$</code> 三种情况下的平均收益和最优动作占比随训练步数的变化情况。</p>
<figure>
  <img data-src="/images/cn/2020-05-16-multi-armed-bandit/epsilon-greedy-step-average-reward.png" class="lazyload"/>
  
</figure>
<figure>
  <img data-src="/images/cn/2020-05-16-multi-armed-bandit/epsilon-greedy-step-best-action-ratio.png" class="lazyload"/>
  
</figure>
<p><code>$\epsilon$</code>-Greedy 相比于 <code>$\epsilon = 0$</code>（贪婪）算法的优势如下：</p>
<ul>
<li>对于更大方差的收益，找到最优的动作需要更多次的试探。</li>
<li>对于非平稳的任务，即动作的真实价值会随着时间而改变，这种情况下即使有确定性的情况下，也需要进行试探。</li>
</ul>
<p>令 <code>$R_i$</code> 表示一个动作被选择 <code>$i$</code> 次后获得的收益，<code>$Q_n$</code> 表示被选择 <code>$n - 1$</code> 次后它的估计的动作价值，其可以表示为增量计算的形式：</p>
<p><code>$$ \begin{aligned} Q_{n+1} &amp;= \dfrac{1}{n} \sum_{i=1}^{n}{R_i} \\ &amp;= \dfrac{1}{n} \left(R_n + \sum_{i=1}^{n-1}{R_i}\right) \\ &amp;= \dfrac{1}{n} \left(R_n + \left(n - 1\right) \dfrac{1}{n-1} \sum_{i=1}^{n-1}{R_i}\right) \\ &amp;= \dfrac{1}{n} \left(R_n + \left(n - 1\right) Q_n\right) \\ &amp;= \dfrac{1}{n} \left(R_n + n Q_n - Q_n\right) \\ &amp;= Q_n + \dfrac{1}{n} \left[R_n - Q_n\right] \end{aligned} $$</code></p>
<p>上述我们讨论的都是平稳的问题，即收益的概率分布不随着时间变化的赌博机问题。对于非平稳的问题，给近期的收益赋予比过去更高的权值是一个合理的处理方式。则收益均值 <code>$Q_n$</code> 的增量更新规则为：</p>
<p><code>$$ \begin{aligned} Q_{n+1} &amp;= Q_n + \alpha \left[R_n - Q_n\right] \\ &amp;= \left(1 - \alpha\right)^n Q_1 + \sum_{i=1}^{n} \alpha \left(1 - \alpha\right)^{n-i} R_i \end{aligned} $$</code></p>
<p>赋给收益 <code>$R_i$</code> 的权值 <code>$\alpha \left(1 - \alpha\right)^{n-i}$</code> 依赖于它被观测到的具体时刻和当前时刻的差，权值以指数形式递减，因此这个方法也称之为<strong>指数近因加权平均</strong>。</p>
<p>上述讨论中所有方法都在一定程度上依赖于初始动作值 <code>$Q_1 \left(a\right)$</code> 的选择。从统计学角度，初始估计值是有偏的，对于平均采样来说，当所有动作都至少被选择一次时，偏差会消失；对于步长为常数的情况，偏差会随时间而减小。</p>
<p>下图展示了不同初始动作估计值下最优动作占比随训练步数的变化情况：</p>
<figure>
  <img data-src="/images/cn/2020-05-16-multi-armed-bandit/epsilon-greedy-different-parameters-best-action-ratio.png" class="lazyload"/>
  
</figure>
<p>设置较大初始动作估计值会鼓励进行试探，这种方法称之为<strong>乐观初始价值</strong>，该方法在平稳问题中非常有效。</p>
<h3 id="ucb">UCB</h3>
<p><code>$\epsilon$</code>-Greedy 在进行尝试时是盲目地选择，因为它不大会选择接近贪心或者不确定性特别大的动作。在非贪心动作中，最好是根据它们的潜力来选择可能事实上是最优的动作，这要考虑它们的估计有多接近最大值，以及这些估计的不确定性。</p>
<p>一种基于<strong>置信度上界</strong>（Upper Confidence Bound，UCB）思想的选择动作依据如下：</p>
<p><code>$$ A_t = \mathop{\arg\max}_{a} \left[Q_t \left(a\right) + c \sqrt{\dfrac{\ln t}{N_t \left(a\right)}}\right] $$</code></p>
<p>其中，<code>$N_t \left(a\right)$</code> 表示在时刻 <code>$t$</code> 之前动作 <code>$a$</code> 被选择的次数，<code>$c &gt; 0$</code> 用于控制试探的程度。平方根项是对 <code>$a$</code> 动作值估计的不确定性或方差的度量，最大值的大小是动作 <code>$a$</code> 的可能真实值的上限，参数 <code>$c$</code> 决定了置信水平。</p>
<p>下图展示了 UCB 算法和 <code>$\epsilon$</code>-Greedy 算法平均收益随着训练步数的变化：</p>
<figure>
  <img data-src="/images/cn/2020-05-16-multi-armed-bandit/epsilon-greedy-ucb-step-average-reward.png" class="lazyload"/>
  
</figure>
<h3 id="梯度赌博机算法">梯度赌博机算法</h3>
<p>针对每个动作 <code>$a$</code>，考虑学习一个数值化的偏好函数 <code>$H_t \left(a\right)$</code>，偏好函数越大，动作就约频繁地被选择，但偏好函数的概念并不是从“收益”的意义上提出的。基于随机梯度上升的思想，在每个步骤中，在选择动作 <code>$A_t$</code> 并获得收益 <code>$R_t$</code> 之后，偏好函数会按如下方式更新：</p>
<p><code>$$ \begin{aligned} H_{t+1} \left(A_t\right) &amp;eq H_t \left(A_t\right) + \alpha \left(R_t - \bar{R}_t\right) \left(1 - \pi_t \left(A_t\right)\right) \\ H_{t+1} \left(a\right) &amp;eq H_t \left(a\right) - \alpha \left(R_t - \bar{R}_t\right) \pi_t \left(a\right) \end{aligned} $$</code></p>
<p>其中，<code>$\alpha &gt; 0$</code> 表示步长，<code>$\bar{R}_t \in \mathbb{R}$</code> 表示时刻 <code>$t$</code> 内所有收益的平均值。<code>$\bar{R}_t$</code> 项作为比较收益的一个基准项，如果收益高于它，那么在未来选择动作 <code>$A_t$</code> 的概率就会增加，反之概率就会降低，未选择的动作被选择的概率会上升。</p>
<p>下图展示了在 10 臂测试平台问题的变体上采用梯度赌博机算法的结果，在这个问题中，它们真实的期望收益是按照平均值为 4 而不是 0 的正态分布来选择的。所有收益的这种变化对梯度赌博机算法没有任何影响，因为收益基准项让它可以马上适应新的收益水平，如果没有基准项，那么性能将显著降低。</p>
<figure>
  <img data-src="/images/cn/2020-05-16-multi-armed-bandit/gradient-different-parameters-best-action-ratios.png" class="lazyload"/>
  
</figure>
<h2 id="算法性能比较">算法性能比较</h2>
<p><code>$\epsilon$</code>-Greedy 方法在一段时间内进行随机的动作选择；UCB 方法虽然采用确定的动作选择，但可以通过每个时刻对具有较少样本的动作进行优先选择来实现试探；梯度赌博机算法则不估计动作价值，而是利用偏好函数，使用 softmax 分布来以一种分级的、概率式的方式选择更优的动作；简单地将收益的初值进行乐观的设置，可以让贪心方法也能进行显示试探。</p>
<p>下图展示了上述算法在不同参数下的平均收益，每条算法性能曲线都看作一个自己参数的函数。<code>$x$</code> 轴上参数值的变化是 2 的倍数，并以对数坐标轴进行表示。</p>
<figure>
  <img data-src="/images/cn/2020-05-16-multi-armed-bandit/different-methods-performance.png" class="lazyload"/>
  
</figure>
<p>在评估方法时，不仅要关注它在最佳参数设置上的表现，还要注意它对参数值的敏感性。总的来说，在本文的问题上，UCB 表现最好。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Sutton, R. S., &amp; Barto, A. G. (2018). <em>Reinforcement learning: An introduction</em>. MIT press.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://cosx.org/2017/05/bandit-and-recommender-systems">https://cosx.org/2017/05/bandit-and-recommender-systems</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>强化学习简介 (Introduction of Reinforcement Learning)</title><link>https://zeqiang.fun/cn/2020/05/introduction-of-reinforcement-learning/</link><pubDate>Sat, 09 May 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/05/introduction-of-reinforcement-learning/</guid><description><![CDATA[
        <blockquote>
<p>本文为<a href="/categories/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">《强化学习系列》</a>文章</p>
</blockquote>
<h2 id="强化学习简介">强化学习简介</h2>
<p>**强化学习（Reinforcement Learning，RL）**是机器学习中的一个领域，是学习“做什么（即如何把当前的情景映射成动作）才能使得数值化的收益信号最大化”。学习者不会被告知应该采取什么动作，而是必须自己通过尝试去发现哪些动作会产生最丰厚的收益。</p>
<p>强化学习同机器学习领域中的<strong>有监督学习</strong>和<strong>无监督学习</strong>不同，有监督学习是从外部监督者提供的带标注训练集中进行学习（任务驱动型），无监督学习是一个典型的寻找未标注数据中隐含结构的过程（数据驱动型）。强化学习是与两者并列的第三种机器学习范式，强化学习带来了一个独有的挑战——**“试探”<strong>与</strong>“开发”**之间的折中权衡，智能体必须开发已有的经验来获取收益，同时也要进行试探，使得未来可以获得更好的动作选择空间（即从错误中学习）。</p>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/machine-learning-types.png" class="lazyload"/>
  
</figure>
<p>在强化学习中，有两个可以进行交互的对象：<strong>智能体（Agnet）<strong>和</strong>环境（Environment）</strong>：</p>
<ul>
<li>智能体：可以感知环境的<strong>状态（State）</strong>，并根据反馈的<strong>奖励（Reward）<strong>学习选择一个合适的</strong>动作（Action）</strong>，来最大化长期总收益。</li>
<li>环境：环境会接收智能体执行的一系列动作，对这一系列动作进行评价并转换为一种可量化的信号反馈给智能体。</li>
</ul>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/reinforcement-learning.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：https://en.wikipedia.org/wiki/Reinforcement_learning</p></figcaption>
</figure>
<p>除了智能体和环境之外，强化学习系统有四个核心要素：<strong>策略（Policy）</strong>、<strong>回报函数（收益信号，Reward Function）</strong>、<strong>价值函数（Value Function）<strong>和</strong>环境模型（Environment Model）</strong>，其中环境模型是可选的。</p>
<ul>
<li>策略：定义了智能体在特定时间的行为方式。策略是环境状态到动作的映射。</li>
<li>回报函数：定义了强化学习问题中的目标。在每一步中，环境向智能体发送一个称为收益的标量数值。</li>
<li>价值函数：表示了从长远的角度看什么是好的。一个状态的价值是一个智能体从这个状态开始，对将来累积的总收益的期望。</li>
<li>环境模型：是一种对环境的反应模式的模拟，它允许对外部环境的行为进行推断。</li>
</ul>
<p>强化学习是一种对目标导向的学习与决策问题进行理解和自动化处理的计算方法。它强调智能体通过与环境的直接互动来学习，而不需要可效仿的监督信号或对周围环境的完全建模，因而与其他的计算方法相比具有不同的范式。</p>
<p>强化学习使用马尔可夫决策过程的形式化框架，使用<strong>状态</strong>，<strong>动作</strong>和<strong>收益</strong>定义学习型智能体与环境的互动过程。这个框架力图简单地表示人工智能问题的若干重要特征，这些特征包含了对<strong>因果关系</strong>的认知，对<strong>不确定性</strong>的认知，以及对<strong>显式目标存在性</strong>的认知。</p>
<p>价值与价值函数是强化学习方法的重要特征，价值函数对于策略空间的有效搜索来说十分重要。相比于进化方法以对完整策略的反复评估为引导对策略空间进行直接搜索，使用价值函数是强化学习方法与进化方法的不同之处。</p>
<h2 id="示例和应用">示例和应用</h2>
<p>以经典的 Flappy Bird 游戏为例，智能体就是游戏中我们操作的小鸟，整个游戏中的天空和遮挡管道即为环境，动作为玩家单击屏幕使小鸟飞起的行为，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/flappy-bird-rl.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：https://easyai.tech/ai-definition/reinforcement-learning</p></figcaption>
</figure>
<p>目前，强化学习在包括<strong>游戏</strong>，<strong>广告和推荐</strong>，<strong>对话系统</strong>，<strong>机器人</strong>等多个领域均展开了广泛的应用。</p>
<h3 id="游戏">游戏</h3>
<p><strong>AlphaGo</strong> <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 是于 2014 年开始由英国伦敦 Google DeepMind 开发的人工智能围棋软件。AlphaGo 使用蒙特卡洛树搜索（Monte Carlo tree search），借助估值网络（value network）与走棋网络（policy network）这两种深度神经网络，通过估值网络来评估大量选点，并通过走棋网络选择落点。</p>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/alphago.png" class="lazyload"/>
  
</figure>
<p><strong>AlphaStar</strong> <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 是由 DeepMind 开发的玩 <a href="https://zh.wikipedia.org/wiki/%E6%98%9F%E6%B5%B7%E7%88%AD%E9%9C%B8II%EF%BC%9A%E8%87%AA%E7%94%B1%E4%B9%8B%E7%BF%BC">星际争霸 II</a> 游戏的人工智能程序。AlphaStar 是由一个深度神经网路生成的，它接收来自原始游戏界面的输入数据，并输出一系列指令，构成游戏中的一个动作。</p>
<p>更具体地说，神经网路体系结构将 Transformer 框架运用于模型单元（类似于关系深度强化学习），结合一个深度 LSTM 核心、一个带有 pointer network 的自回归策略前端和一个集中的值基线。这种先进的模型将有助于解决机器学习研究中涉及长期序列建模和大输出空间（如翻译、语言建模和视觉表示）的许多其他挑战。</p>
<p>AlphaStar 还使用了一种新的多智能体学习算法。该神经网路最初是通过在 Blizzard 发布的匿名人类游戏中进行监督学习来训练的。这使得 AlphaStar 能够通过模仿学习星际争霸上玩家所使用的基本微观和宏观策略。这个初级智能体在 95% 的游戏中击败了内置的「精英」AI 关卡（相当于人类玩家的黄金级别）。</p>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/alphastar.png" class="lazyload"/>
  
</figure>
<p><strong>OpenAI Five</strong> <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 是一个由 OpenAI 开发的用于多人视频游戏 <a href="https://zh.wikipedia.org/zh-hans/Dota_2">Dota 2</a> 的人工智能程序。OpenAI Five 通过与自己进行超过 10,000 年时长的游戏进行优化学习，最终获得了专家级别的表现。</p>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/openai-five.png" class="lazyload"/>
  
</figure>
<p><strong>Pluribus</strong> <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> 是由 Facebook 开发的第一个在六人无限注德州扑克中击败人类专家的 AI 智能程序，其首次在复杂游戏中击败两个人或两个团队。</p>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/facebook-pluribus.jpg" class="lazyload"/>
  
</figure>
<h3 id="广告和推荐">广告和推荐</h3>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/recommendation.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：A Reinforcement Learning Framework for Explainable Recommendation</p></figcaption>
</figure>
<h3 id="对话系统">对话系统</h3>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/dialogue-system.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：End-to-End Task-Completion Neural Dialogue Systems</p></figcaption>
</figure>
<h3 id="机器人">机器人</h3>
<figure>
  <img data-src="/images/cn/2020-05-09-introduction-of-reinforcement-learning/robot.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：Learning Synergies between Pushing and Grasping with Self-supervised Deep Reinforcement Learning</p></figcaption>
</figure>
<h2 id="开放资源">开放资源</h2>
<h3 id="开源实验平台">开源实验平台</h3>
<ul>
<li><a href="https://github.com/openai/gym">openai/gym</a></li>
<li><a href="http://mujoco.org/">MuJoCo</a></li>
<li><a href="https://github.com/openai/mujoco-py">openai/mujoco-py</a></li>
<li><a href="https://github.com/deepmind/lab">deepmind/lab</a></li>
</ul>
<h3 id="开源框架">开源框架</h3>
<ul>
<li><a href="https://github.com/deepmind/trfl/">deepmind/trfl/</a> <i class="icon icon-tensorflow"></i></li>
<li><a href="https://github.com/deepmind/open_spiel">deepmind/open_spiel</a> <i class="icon icon-tensorflow"></i></li>
<li><a href="https://github.com/google/dopamine">google/dopamine</a> <i class="icon icon-tensorflow"></i></li>
<li><a href="https://github.com/tensorflow/agents">tensorflow/agents</a> <i class="icon icon-tensorflow"></i></li>
<li><a href="https://github.com/keras-rl/keras-rl">keras-rl/keras-rl</a> <i class="icon icon-keras"></i></li>
<li><a href="https://github.com/tensorforce/tensorforce">tensorforce/tensorforce</a> <i class="icon icon-tensorflow"></i></li>
<li><a href="https://github.com/facebookresearch/ReAgent">facebookresearch/ReAgent</a> <i class="icon icon-pytorch"></i></li>
<li><a href="https://github.com/thu-ml/tianshou">thu-ml/tianshou</a> <i class="icon icon-pytorch"></i></li>
<li><a href="https://github.com/astooke/rlpyt">astooke/rlpyt</a> <i class="icon icon-pytorch"></i></li>
<li><a href="https://github.com/NervanaSystems/coach">NervanaSystems/coach</a> <i class="icon icon-tensorflow"></i></li>
<li><a href="https://github.com/PaddlePaddle/PARL">PaddlePaddle/PARL</a> <i class="icon icon-paddlepaddle"></i></li>
</ul>
<h3 id="开源模型">开源模型</h3>
<ul>
<li><a href="https://github.com/dennybritz/reinforcement-learning">dennybritz/reinforcement-learning</a> <i class="icon icon-tensorflow"></i></li>
<li><a href="https://github.com/openai/baselines">openai/baselines</a> <i class="icon icon-tensorflow"></i></li>
</ul>
<h3 id="其他资源">其他资源</h3>
<ul>
<li><a href="https://github.com/ShangtongZhang/reinforcement-learning-an-introduction">ShangtongZhang/reinforcement-learning-an-introduction</a></li>
<li><a href="https://github.com/aikorea/awesome-rl">aikorea/awesome-rl</a></li>
<li><a href="https://github.com/openai/spinningup">openai/spinningup</a></li>
<li><a href="https://github.com/udacity/deep-reinforcement-learning">udacity/deep-reinforcement-learning</a></li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://deepmind.com/research/case-studies/alphago-the-story-so-far">https://deepmind.com/research/case-studies/alphago-the-story-so-far</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://deepmind.com/blog/article/alphastar-mastering-real-time-strategy-game-starcraft-ii">https://deepmind.com/blog/article/alphastar-mastering-real-time-strategy-game-starcraft-ii</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://deepmind.com/blog/article/AlphaStar-Grandmaster-level-in-StarCraft-II-using-multi-agent-reinforcement-learning">https://deepmind.com/blog/article/AlphaStar-Grandmaster-level-in-StarCraft-II-using-multi-agent-reinforcement-learning</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://openai.com/projects/five/">https://openai.com/projects/five/</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p><a href="https://ai.facebook.com/blog/pluribus-first-ai-to-beat-pros-in-6-player-poker/">https://ai.facebook.com/blog/pluribus-first-ai-to-beat-pros-in-6-player-poker/</a>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>在群晖 NAS 上编译安装 tmux</title><link>https://zeqiang.fun/cn/2020/05/compile-and-install-tmux-on-synology-nas/</link><pubDate>Thu, 07 May 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/05/compile-and-install-tmux-on-synology-nas/</guid><description><![CDATA[
        <h2 id="工具链安装">工具链安装</h2>
<p>登录 NAS 控制台，在系统根目录创建 <code>toolkit</code> 目录：</p>
<pre><code class="language-shell">sudo mkdir /toolkit
sudo chown -R username:users /toolkit
</code></pre>
<p>其中 <code>username</code> 为使用的用户名，如果后续使用过程中出现磁盘空间不足的问题，可以在其他具有较大容量的分区建立 <code>toolkit</code>，再在根目录建立软链进行使用：</p>
<pre><code class="language-shell">mkdir /xxx/toolkit
sudo ln -s /xxx/toolkit /toolkit
sudo chown -R username:users /toolkit
</code></pre>
<p>之后下载相关工具脚本：</p>
<pre><code class="language-shell">cd /toolkit
git clone https://github.com/SynologyOpenSource/pkgscripts-ng.git
</code></pre>
<p>工具脚本使用 Python 3 实现，请确保 NAS 已经安装 Python 3，后续使用过程中如果提示相关 Python 扩展包未安装的情况请自行安装后重试。实验的 Synology NAS 为 DS418play，系统版本为 <strong>DSM 6.2.2</strong> 系统，处理器为 INTEL Celeron J3355 处理器（产品代号：<strong>Apollo Lake</strong>），首先利用 <code>EnvDeploy</code> 下载所需的编译环境：</p>
<pre><code class="language-shell">cd /toolkit/pkgscripts-ng
sudo ./EnvDeploy -v 6.2 -p apollolake
</code></pre>
<p>请根据自己机器的系统版本和处理器类型自行调整 <code>-v</code> 和 <code>-p</code> 参数。如果下载速度较慢可以手动从 <a href="https://sourceforge.net/projects/dsgpl/files/toolkit/DSM6.2/">https://sourceforge.net/projects/dsgpl/files/toolkit/DSM6.2/</a> 下载下列文件：</p>
<pre><code>base_env-6.2.txz
ds.apollolake-6.2.dev.txz
ds.apollolake-6.2.env.txz
</code></pre>
<p>将其放置到 <code>/toolkit/toolkit_tarballs</code> 目录中，然后通过如下命令进行部署安装：</p>
<pre><code class="language-shell">sudo ./EnvDeploy -v 6.2 -p apollolake -t /toolkit/toolkit_tarballs
</code></pre>
<h2 id="编译-tmux">编译 tmux</h2>
<p>在 <code>/toolkit</code> 目录下建立 <code>source</code> 文件夹，并将 tmux 源代码（本文以 3.1b 版本为例）下载到该文件夹中：</p>
<pre><code class="language-shell">cd /toolkit
mkdir source
cd source
wget https://github.com/tmux/tmux/releases/download/3.1b/tmux-3.1b.tar.gz
tar -zxvf tmux-3.1b.tar.gz
mv tmux-3.1b tmux
</code></pre>
<p>在 tmux 源代码根目录中建立 <code>SynoBuildConf</code> 文件夹，并在文件夹中创建如下文件：</p>
<pre><code class="language-shell">cd /toolkit/source/tmux
mkdir SynoBuildConf
</code></pre>
<p><code>build</code></p>
<pre><code class="language-bash">#!/bin/bash

case ${MakeClean} in
	[Yy][Ee][Ss])
		make distclean
		;;
esac

NCURSES_INCS=&quot;$(pkg-config ncurses --cflags)&quot;
NCURSES_LIBS=&quot;$(pkg-config ncurses --libs)&quot;

CFLAGS=&quot;${CFLAGS} ${NCURSES_INCS}&quot;
LDFLAGS=&quot;${LDFLAGS} ${NCURSES_LIBS}&quot;

env CC=&quot;${CC}&quot; AR=&quot;${AR}&quot; CFLAGS=&quot;${CFLAGS}&quot; LDFLAGS=&quot;${LDFLAGS}&quot; \
./configure ${ConfigOpt}

make ${MAKE_FLAGS}
</code></pre>
<p><code>depends</code></p>
<pre><code class="language-bash">[default]
all=&quot;6.2&quot;
</code></pre>
<p><code>install</code></p>
<pre><code class="language-bash">#!/bin/bash

PKG_NAME=&quot;tmux&quot;
TGZ_DIR=&quot;/tmp/_${PKG_NAME}_tgz&quot;
PKG_DIR=&quot;/tmp/_${PKG_NAME}_pkg&quot;
PKG_DEST=&quot;/image/packages&quot;

source /pkgscripts-ng/include/pkg_util.sh

create_package_tgz() {
	### clear destination directory
	for dir in $TGZ_DIR $PKG_DIR; do
		rm -rf &quot;$dir&quot;
	done
	for dir in $TGZ_DIR $PKG_DIR; do
		mkdir -p &quot;$dir&quot;
	done

	### install needed file into TGZ_DIR
	DESTDIR=&quot;${TGZ_DIR}&quot; make install

	### create package.tgz
	pkg_make_package $TGZ_DIR $PKG_DIR
}

create_package_spk(){
	### Copy package center scripts to PKG_DIR
	cp -r synology/scripts/ $PKG_DIR

	### Copy package icon
	cp -av synology/PACKAGE_ICON*.PNG $PKG_DIR

	### Generate INFO file
	synology/INFO.sh &gt; INFO
	cp INFO $PKG_DIR

	### Create the final spk.
	mkdir -p $PKG_DEST
	pkg_make_spk $PKG_DIR $PKG_DEST
}

main() {
	create_package_tgz
	create_package_spk
}

main &quot;$@&quot;
</code></pre>
<p>在 tmux 源代码根目录中建立 <code>synology</code> 文件夹，并在文件夹中创建如下文件：</p>
<pre><code class="language-shell">cd /toolkit/source/tmux
mkdir synology
</code></pre>
<p><code>INFO.sh</code></p>
<pre><code class="language-bash">#!/bin/sh

. /pkgscripts-ng/include/pkg_util.sh

package=&quot;tmux&quot;
version=&quot;3.1b&quot;
displayname=&quot;tmux&quot;
arch=&quot;$(pkg_get_platform) &quot;
maintainer=&quot;tmux&quot;
maintainer_url=&quot;https://github.com/tmux&quot;
distributor=&quot;Leo Van&quot;
distributor_url=&quot;https://leovan.me&quot;
description=&quot;tmux is a terminal multiplexer: it enables a number of terminals to be created, accessed, and controlled from a single screen. tmux may be detached from a screen and continue running in the background, then later reattached.&quot;
support_url=&quot;https://github.com/tmux/tmux&quot;
thirdparty=&quot;yes&quot;
startable=&quot;no&quot;
silent_install=&quot;yes&quot;
silent_upgrade=&quot;yes&quot;
silent_uninstall=&quot;yes&quot;

[ &quot;$(caller)&quot; != &quot;0 NULL&quot; ] &amp;&amp; return 0

pkg_dump_info
</code></pre>
<p>并为其添加运行权限：</p>
<pre><code class="language-shell">cd /toolkit/source/tmux/scripts
chmod u+x INFO.sh
</code></pre>
<p>下载 tmux 图标并将其重命名：</p>
<pre><code class="language-shell">cd /toolkit/source/tmux/synology
wget https://raw.githubusercontent.com/tmux/tmux/master/logo/tmux-logo-huge.png
convert tmux-logo-huge.png -crop 480x480+0+0 -resize 72x PACKAGE_ICON.PNG
convert tmux-logo-huge.png -crop 480x480+0+0 -resize 256x PACKAGE_ICON_256.PNG
</code></pre>
<p>此处需要使用 <a href="https://www.imagemagick.org/">ImageMagick</a> 对图标进行裁剪和缩放，请自行安装，或在本地对图片进行处理后上传到指定目录。在 <code>/toolkit/source/tmux/synology</code> 目录中建立 <code>scripts</code> 文件夹，并在文件夹中创建如下文件：</p>
<pre><code class="language-shell">cd /toolkit/source/tmux/synology
mkdir scripts
</code></pre>
<p><code>postinst</code></p>
<pre><code class="language-bash">#!/bin/sh

ln -sf &quot;$SYNOPKG_PKGDEST/usr/local/bin/tmux&quot; /usr/bin/
</code></pre>
<p><code>postuninst</code></p>
<pre><code class="language-bash">#!/bin/sh

rm -f /usr/local/bin/tmux
rm -f /usr/bin/tmux
</code></pre>
<p><code>postupgrade</code></p>
<pre><code class="language-bash">#!/bin/sh

exit 0
</code></pre>
<p><code>preinst</code></p>
<pre><code class="language-bash">#!/bin/sh

exit 0
</code></pre>
<p><code>preuninst</code></p>
<pre><code class="language-bash">#!/bin/sh

exit 0
</code></pre>
<p><code>preupgrade</code></p>
<pre><code class="language-bash">#!/bin/sh

exit 0
</code></pre>
<p><code>start-stop-status</code></p>
<pre><code class="language-bash">#!/bin/sh

case $1 in
	start)
		exit 0
	;;
	stop)
		exit 0
	;;
	status)
		if [ -h &quot;/usr/bin/tmux&quot; ]; then
			exit 0
		else
			exit 1
		fi
	;;
	killall)
        ;;
	log)
		exit 0
	;;
esac
</code></pre>
<p>为所有文件添加运行权限：</p>
<pre><code class="language-shell">cd /toolkit/source/tmux/synology/scripts
chmod u+x *
</code></pre>
<p>利用 <code>PkgCreate.py</code> 构建 <code>tmux</code> 扩展包：</p>
<pre><code class="language-shell">sudo ./PkgCreate.py -v 6.2 -p apollolake tmux
</code></pre>
<p>最终构建完毕的扩展包位于 <code>/toolkit/build_env/ds.apollolake-6.2/image/packages</code> 中。</p>
<h2 id="安装-tmux">安装 tmux</h2>
<p>在 <code>/toolkit/build_env/ds.apollolake-6.2/image/packages</code> 目录中有两个编译好的扩展包，分别是 <code>tmux-apollolake-3.1b_debug.spk</code> 和 <code>tmux-apollolake-3.1b.spk</code>。其中 <code>tmux-apollolake-3.1b.spk</code> 为 Release 版本，传输到本地，通过 NAS 的套件中心手动安装即可。安装完毕后，套件中心的“已安装”会出现 tmux，如下图所示：</p>
<p><img src="/images/cn/2020-05-07-compile-and-install-tmux-on-synology-nas/tmux-installed.png" alt=""></p>
<p>进入 NAS 控制台，运行 <code>tmux -V</code> 可以得到安装好的 tmux 版本信息：</p>
<pre><code class="language-shell">tmux 3.1b
</code></pre>
<p>在此放出编译好的 <a href="https://cdn.leovan.me/packages/synology/tmux-apollolake-3.1b.spk">tmux 扩展包</a>，方便和 DS418play 具有相同系统的 CPU 架构的小伙伴直接使用。</p>
<blockquote>
<p>本文主要参考了 Synology 官方的扩展包构建指南：https://help.synology.com/developer-guide/create_package/index.html</p>
</blockquote>

        ]]></description></item><item><title>隐马尔可夫 (Hidden Markov Model, HMM)，条件随机场 (Conditional Random Fields, CRF) 和序列标注 (Sequence Labeling)</title><link>https://zeqiang.fun/cn/2020/05/hmm-crf-and-sequence-labeling/</link><pubDate>Sat, 02 May 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/05/hmm-crf-and-sequence-labeling/</guid><description><![CDATA[
        <h2 id="隐马尔可夫">隐马尔可夫</h2>
<p>隐马尔可夫模型（Hidden Markov Model，HMM）是一个描述包含隐含未知参数的马尔可夫过程的统计模型。马尔可夫过程（Markov Process）是因俄国数学家安德雷·安德耶维齐·马尔可夫（Андрей Андреевич Марков）而得名一个随机过程，在该随机过程中，给定当前状态和过去所有状态的条件下，其下一个状态的条件概率分布仅依赖于当前状态，通常具备离散状态的马尔可夫过程称之为马尔可夫链（Markov Chain）。因此，马尔可夫链可以理解为一个有限状态机，给定了当前状态为 <code>$S_i$</code> 时，下一时刻状态为 <code>$S_j$</code> 的概率，不同状态之间变换的概率称之为转移概率。下图描述了 3 个状态 <code>$S_a, S_b, S_c$</code> 之间转换状态的马尔可夫链。</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/hmm-markov-chain-example.png" class="lazyload"/>
  
</figure>
<p>隐马尔可夫模型中包含两种序列：随机生成的状态构成的序列称之为状态序列（state sequence），状态序列是不可被观测到的；每个状态对应的观测值组成的序列称之为观测序列（observation sequence）。令 <code>$I = \left(i_1, i_2, \cdots, i_T\right)$</code> 为状态序列，其中 <code>$i_t$</code> 为第 <code>$t$</code> 时刻系统的状态值，对应的有 <code>$O = \left(o_1, o_2, \cdots, o_T\right)$</code> 为观测序列，其中 <code>$o_t$</code> 为第 <code>$t$</code> 时刻系统的观测值，系统的所有可能的状态集合为 <code>$Q = \{q_1, q_2, \cdots, q_N\}$</code>，所有可能的观测集合为 <code>$V= \{v_1, v_2, \cdots, v_M\}$</code>。</p>
<p>隐马尔可夫模型主要由三组参数构成：</p>
<ol>
<li>状态转移矩阵：
<code>$$ A = \left[a_{ij}\right]_{N \times N} $$</code>
其中，
<code>$$ a_{ij} = P \left(i_{t+1} = q_j | i_t = q_i\right), 1 \leq i, j \leq N $$</code>
表示 <code>$t$</code> 时刻状态为 <code>$q_i$</code> 的情况下，在 <code>$t+1$</code> 时刻状态转移到 <code>$q_j$</code> 的概率。</li>
<li>观测概率矩阵：
<code>$$ B = \left[b_j \left(k\right)\right]_{N \times M} $$</code>
其中，
<code>$$ b_j \left(k\right) = P \left(o_t = v_k | i_t = q_j\right), k = 1, 2, \cdots, M, j = 1, 2, \cdots, N $$</code>
表示 <code>$t$</code> 时刻状态为 <code>$q_i$</code> 的情况下，观测值为 <code>$v_k$</code> 的概率。</li>
<li>初始状态概率向量：
<code>$$ \pi = \left(\pi_i\right) $$</code>
其中，
<code>$$ \pi_i = P \left(i_1 = q_i\right), i = 1, 2, \cdots, N $$</code>
表示 <code>$t = 1$</code> 时刻，系统处于状态 <code>$q_i$</code> 的概率。</li>
</ol>
<p>初始状态概率向量 <code>$\pi$</code> 和状态转移矩阵 <code>$A$</code> 决定了状态序列，观测概率矩阵 <code>$B$ </code> 决定了状态序列对应的观测序列，因此马尔可夫模型可以表示为：</p>
<p><code>$$ \lambda = \left(A, B, \pi\right) $$</code></p>
<p>对于马尔可夫模型 <code>$\lambda = \left(A, B, \pi\right)$</code>，通过如下步骤生成观测序列 <code>$\{o_1, o_2, \cdots, o_T\}$</code>：</p>
<ol>
<li>按照初始状态分布 <code>$\pi$</code> 产生状态 <code>$i_1$</code>.</li>
<li>令 <code>$t = 1$</code>。</li>
<li>按照状态 <code>$i_t$</code> 的观测概率分布 <code>$b_{i_t} \left(k\right)$</code> 生成 <code>$o_t$</code>。</li>
<li>按照状态 <code>$i_t$</code> 的状态转移概率分布 <code>$\left\{a_{i_t i_{t+1}}\right\}$</code> 产生状态 <code>$i_{t+1}$</code>，<code>$i_{t+1} = 1, 2, \cdots, N$</code>。</li>
<li>令 <code>$t = t + 1$</code>，如果 <code>$t &lt; T$</code>，转步骤 3；否则，终止。</li>
</ol>
<p>马尔可夫模型在应用过程中有 3 个基本问题 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>：</p>
<ol>
<li>概率计算问题。给定模型 <code>$\lambda = \left(A, B, \pi\right)$</code> 和观测序列 <code>$O = \{o_1, o_2, \cdots, o_T\}$</code>，计算在模型 <code>$\lambda$</code> 下观测序列 <code>$O$</code> 出现的概率 <code>$P\left(O | \lambda \right)$</code>。</li>
<li>学习问题。已知观测序列 <code>$O = \{o_1, o_2, \cdots, o_T\}$</code>，估计模型 <code>$\lambda = \left(A, B, \pi\right)$</code> 参数，使得在该模型下观测序列概率 <code>$P\left(X | \lambda \right)$</code> 最大。即用极大似然估计的方法估计参数。</li>
<li>预测问题，也称为解码（decoding）问题。已知模型 <code>$\lambda = \left(A, B, \pi\right)$</code> 和观测序列 <code>$O = \{o_1, o_2, \cdots, o_T\}$</code>，求对给定观测序列条件概率 <code>$P \left(I | O\right)$</code> 最大的状态序列 <code>$I = \{i_1, i_2, \cdots, i_T\}$</code>。即给定观测序列，求最有可能的对应的状态序列。</li>
</ol>
<h3 id="概率计算">概率计算</h3>
<h4 id="直接计算法">直接计算法</h4>
<p>给定模型 <code>$\lambda = \left(A, B, \pi \right)$</code> 和观测序列 <code>$O = \{o_1, o_2, ..., o_T\}$</code>，计算在模型 <code>$\lambda$</code> 下观测序列 <code>$O$</code> 出现的概率 <code>$P\left(O | \lambda \right)$</code>。最简单的办法就是列举出左右可能的状态序列 <code>$I = \{i_1, i_2, ..., i_T\}$</code>，再根据观测概率矩阵 <code>$B$</code>，计算每种状态序列对应的联合概率 <code>$P \left(O, I | \lambda\right)$</code>，对其进行求和得到概率 <code>$P\left(O | \lambda \right)$</code>。</p>
<p>状态序列 <code>$I = \{i_1, i_2, ..., i_T\}$</code> 的概率是：</p>
<p><code>$$ P \left(I | \lambda \right) = \pi_{y_1} \prod_{t = 1}^{T - 1} a_{{i_t}{i_{t+1}}} $$</code></p>
<p>对于固定的状态序列 <code>$I = \{i_1, i_2, ..., i_T\}$</code>，观测序列 <code>$O = \{o_1, o_2, ..., o_T\}$</code> 的概率是：</p>
<p><code>$$ P \left(O | I, \lambda \right) = \prod_{t = 1}^{T} b_{i_t} \left(o_t\right) $$</code></p>
<p><code>$O$</code> 和 <code>$I$</code> 同时出现的联合概率为：</p>
<p><code>$$ \begin{split} P \left(O, I | \lambda \right) &amp;= P \left(O | I, \lambda \right) P \left(I | \lambda \right) \\ &amp;= \pi_{y_1} \prod_{t = 1}^{T - 1} a_{{i_t}{i_{t+1}}} \prod_{t = 1}^{T} b_{i_t} \left(o_t\right) \end{split} $$</code></p>
<p>然后，对于所有可能的状态序列 <code>$I$</code> 求和，得到观测序列 <code>$O$</code> 的概率 <code>$P \left(O | \lambda\right)$</code>，即：</p>
<p><code>$$ \begin{split} P\left(O | \lambda \right) &amp;= \sum_{I} P \left(O | I, \lambda \right) P \left(I | \lambda \right)  \\ &amp;= \sum_{i_1, i_2, \cdots, i_T} \pi_{y_1} \prod_{t = 1}^{T - 1} a_{{i_t}{i_{t+1}}} \prod_{t = 1}^{T} b_{i_t} \left(o_t\right) \end{split} $$</code></p>
<p>但利用上式的计算量很大，是 <code>$O \left(T N^T\right)$</code> 阶的，这种算法不可行。</p>
<h4 id="前向算法">前向算法</h4>
<p><strong>前向概率</strong>：给定马尔可夫模型 <code>$\lambda$</code>，给定到时刻 <code>$t$</code> 部分观测序列为 <code>$o_1, o_2, \cdots, o_t$</code> 且状态为 <code>$q_i$</code> 的概率为前向概率，记作：</p>
<p><code>$$ \alpha_t \left(i\right) = P \left(o_1, o_2, \cdots, o_t, i_t = q_i | \lambda\right) $$</code></p>
<p>可以递推地求得前向概率 <code>$\alpha_t \left(i\right)$</code> 及观测序列概率 <code>$P \left(O | \lambda\right)$</code>，前向算法如下：</p>
<ol>
<li>初值
<code>$$ \alpha_{1}(i)=\pi_{i} b_{i}\left(o_{1}\right), \quad i=1,2, \cdots, N $$</code></li>
<li>递推，对 <code>$t = 1, 2, \cdots, T-1$</code>
<code>$$ \alpha_{t+1}(i)=\left[\sum_{j=1}^{N} \alpha_{t}(j) a_{j i}\right] b_{i}\left(o_{t+1}\right), \quad i=1,2, \cdots, N $$</code></li>
<li>终止
<code>$$ P(O | \lambda)=\sum_{i=1}^{N} \alpha_{T}(i) $$</code></li>
</ol>
<h4 id="后向算法">后向算法</h4>
<p><strong>后向概率</strong>：给定隐马尔可夫模型 <code>$\lambda$</code>，给定在时刻 <code>$t$</code> 状态为 <code>$q_i$</code> 的条件下，从 <code>$t+1$</code> 到 <code>$T$</code> 的部分观测序列为 <code>$o_{t+1}, o_{t+2}, \cdots, o_T$</code> 的概率为后向概率，记作：</p>
<p><code>$$ \beta_{t}(i)=P\left(o_{t+1}, o_{t+2}, \cdots, o_{T} | i_{t}=q_{i}, \lambda\right) $$</code></p>
<p>可以递推地求得后向概率 <code>$\alpha_t \left(i\right)$</code> 及观测序列概率 <code>$P \left(O | \lambda\right)$</code>，后向算法如下：</p>
<ol>
<li>初值
<code>$$ \beta_{T}(i)=1, \quad i=1,2, \cdots, N $$</code></li>
<li>递推，对 <code>$t = T-1, T-2, \cdots, 1$</code>
<code>$$ \beta_{t}(i)=\sum_{j=1}^{N} a_{i j} b_{j}\left(o_{t+1}\right) \beta_{t+1}(j), \quad i=1,2, \cdots, N $$</code></li>
<li>终止
<code>$$ P(O | \lambda)=\sum_{i=1}^{N} \pi_{i} b_{i}\left(o_{1}\right) \beta_{1}(i) $$</code></li>
</ol>
<h3 id="学习算法">学习算法</h3>
<h4 id="监督学习算法">监督学习算法</h4>
<p>假设以给训练数据包含 <code>$S$</code> 个长度相同的观测序列和对应的状态序列 <code>$\left\{\left(O_1, I_1\right), \left(O_2, I_2\right), \cdots, \left(O_S, I_S\right)\right\}$</code>，那么可以利用极大似然估计法来估计隐马尔可夫模型的参数。</p>
<p>设样本中时刻 <code>$t$</code> 处于状态 <code>$i$</code> 时刻 <code>$t+1$</code> 转移到状态 <code>$j$</code> 的频数为 <code>$A_{ij}$</code>，那么转移概率 <code>$a_{ij}$</code> 的估计是：</p>
<p><code>$$ \hat{a}_{i j}=\frac{A_{i j}}{\sum_{j=1}^{N} A_{i j}}, \quad i=1,2, \cdots, N ; \quad j=1,2, \cdots, N $$</code></p>
<p>设样本中状态为 <code>$j$</code> 并观测为 <code>$k$</code> 的频数是 <code>$B_{jk}$</code>，那么状态为 <code>$j$</code> 观测为 <code>$k$</code> 的概率 <code>$b_j \left(k\right)$</code> 的估计是：</p>
<p><code>$$ \hat{b}_{j}(k)=\frac{B_{j k}}{\sum_{k=1}^{M} B_{j k}}, \quad j=1,2, \cdots, N ; \quad k=1,2, \cdots, M $$</code></p>
<p>初始状态概率 <code>$\pi_i$</code> 的估计 <code>$\hat{\pi}_i$</code> 为 <code>$S$</code> 个样本中初始状态为 <code>$q_i$</code> 的频率。</p>
<h4 id="无监督学习算法">无监督学习算法</h4>
<p>假设给定训练数据值包含 <code>$S$</code> 个长度为 <code>$T$</code> 的观测序列 <code>$\left\{O_1, O_2, \cdots, O_S\right\}$</code> 而没有对应的状态序例，目标是学习隐马尔可夫模型 <code>$\lambda = \left(A, B, \pi\right)$</code> 的参数。我们将观测序列数据看做观测数据 <code>$O$</code>，状态序列数据看作不可观测的隐数据 <code>$I$</code>，那么马尔可夫模型事实上是一个含有隐变量的概率模型：</p>
<p><code>$$ P(O | \lambda)=\sum_{I} P(O | I, \lambda) P(I | \lambda) $$</code></p>
<p>它的参数学习可以由 EM 算法实现。EM 算法在隐马尔可夫模型学习中的具体实现为 Baum-Welch 算法：</p>
<ol>
<li>初始化。对 <code>$n = 0$</code>，选取 <code>$a_{i j}^{(0)}, b_{j}(k)^{(0)}, \pi_{i}^{(0)}$</code>，得到模型 <code>$\lambda^{(0)}=\left(A^{(0)}, B^{(0)}, \pi^{(0)}\right)$</code>。</li>
<li>递推。对 <code>$n = 1, 2, \cdots$</code>：
<code>$$ \begin{aligned} a_{i j}^{(n+1)} &amp;= \frac{\sum_{t=1}^{T-1} \xi_{t}(i, j)}{\sum_{t=1}^{T-1} \gamma_{t}(i)} \\ b_{j}(k)^{(n+1)} &amp;= \frac{\sum_{t=1, o_{t}=v_{k}}^{T} \gamma_{t}(j)}{\sum_{t=1}^{T} \gamma_{t}(j)} \\ \pi_{i}^{(n+1)} &amp;= \gamma_{1}(i) \end{aligned} $$</code>
右端各按照观测 <code>$O=\left(o_{1}, o_{2}, \cdots, o_{T}\right)$</code> 和模型 <code>$\lambda^{(n)}=\left(A^{(n)}, B^{(n)}, \pi^{(n)}\right)$</code> 计算，
<code>$$ \begin{aligned} \gamma_{t}(i) &amp;= \frac{\alpha_{t}(i) \beta_{t}(i)}{P(O | \lambda)}=\frac{\alpha_{t}(i) \beta_{t}(i)}{\sum_{j=1}^{N} \alpha_{t}(j) \beta_{t}(j)} \\ \xi_{t}(i, j) &amp;= \frac{\alpha_{t}(i) a_{i j} b_{j}\left(o_{t+1}\right) \beta_{t+1}(j)}{\sum_{i=1}^{N} \sum_{j=1}^{N} \alpha_{t}(i) a_{i j} b_{j}\left(o_{t+1}\right) \beta_{t+1}(j)} \end{aligned} $$</code></li>
<li>终止。得到模型参数 <code>$\lambda^{(n+1)}=\left(A^{(n+1)}, B^{(n+1)}, \pi^{(n+1)}\right)$</code>。</li>
</ol>
<h3 id="预测算法">预测算法</h3>
<h4 id="近似算法">近似算法</h4>
<p>近似算法的思想是，在每个时刻 <code>$t$</code> 选择在该时刻最有可能出现的状态 <code>$i_t^*$</code>，从而得到一个状态序列 <code>$I^{*}=\left(i_{1}^{*}, i_{2}^{*}, \cdots, i_{T}^{*}\right)$</code>，将它作为预测的结果。给定隐马尔可夫模型 <code>$\lambda$</code> 和观测序列 <code>$O$</code>，在时刻 <code>$t$</code> 处于状态 <code>$q_i$</code> 的概率 <code>$\gamma_t \left(i\right)$</code> 是：</p>
<p><code>$$ \gamma_{t}(i)=\frac{\alpha_{t}(i) \beta_{t}(i)}{P(O | \lambda)}=\frac{\alpha_{t}(i) \beta_{t}(i)}{\sum_{j=1}^{N} \alpha_{t}(j) \beta_{t}(j)} $$</code></p>
<p>在每一时刻 <code>$t$</code> 最有可能的状态 <code>$i_t^*$</code> 是：</p>
<p><code>$$ i_{t}^{*}=\arg \max _{1 \leqslant i \leqslant N}\left[\gamma_{t}(i)\right], \quad t=1,2, \cdots, T $$</code></p>
<p>从而得到状态序列 <code>$I^{*}=\left(i_{1}^{*}, i_{2}^{*}, \cdots, i_{T}^{*}\right)$</code>。</p>
<p>近似算法的优点是计算简单，其缺点是不能保证预测的状态序列整体是最有可能的状态序列，因为预测的状态序列可能有实际不发生的部分。事实上，上述方法得到的状态序列中有可能存在转移概率为0的相邻状态，即对某些 <code>$i, j, a_{ij} = 0$</code> 。尽管如此，近似算法仍然是有用的。</p>
<h4 id="维特比算法">维特比算法</h4>
<p>维特比算法（Viterbi Algorithm）实际是用动态规划（Dynamic Programming）解隐马尔可夫模型预测问题，即用动态规划求概率最大路径（最优路径）。这时一条路径对应着一个状态序列。</p>
<p>首先导入两个变量 <code>$\sigma$</code> 和 <code>$\Psi$</code>。定义在时刻 <code>$t$</code> 状态为 <code>$i$</code> 的所有单个路径 <code>$\left(i_1, i_2, \cdots, i_t\right)$</code> 中概率最大值为：</p>
<p><code>$$ \delta_{t}(i)=\max _{i_{1}, i_{2}, \cdots, i_{t-1}} P\left(i_{t}=i, i_{t-1}, \cdots, i_{1}, o_{t}, \cdots, o_{1} | \lambda\right), \quad i=1,2, \cdots, N $$</code></p>
<p>由定义可得变量 <code>$\sigma$</code> 的递推公式：</p>
<p><code>$$ \begin{aligned} \delta_{t+1}(i) &amp;=\max _{i_{1}, i_{2}, \cdots, i_{t}} P\left(i_{t+1}=i, i_{t}, \cdots, i_{1}, o_{t+1}, \cdots, o_{1} | \lambda\right) \\ &amp;=\max _{1 \leqslant j \leqslant N}\left[\delta_{t}(j) a_{j i}\right] b_{i}\left(o_{t+1}\right), \quad i=1,2, \cdots, N ; \quad t=1,2, \cdots, T-1 \end{aligned} $$</code></p>
<p>定义在时刻 <code>$t$</code> 状态为 <code>$i$</code> 的所有单个路径 <code>$\left(i_1, i_2, \cdots, i_{t-1}, i\right)$</code> 中概率最大的路径的第 <code>$t - 1$</code> 个结点为：</p>
<p><code>$$ \Psi_{t}(i)=\arg \max _{1 \leqslant j \leqslant N}\left[\delta_{t-1}(j) a_{j i}\right], \quad i=1,2, \cdots, N $$</code></p>
<p>维特比算法流程如下：</p>
<ol>
<li>初始化
<code>$$ \begin{array}{c} \delta_{1}(i)=\pi_{i} b_{i}\left(o_{1}\right), \quad i=1,2, \cdots, N \\ \Psi_{1}(i)=0, \quad i=1,2, \cdots, N \end{array} $$</code></li>
<li>递推。对 <code>$t = 2, 3, \cdots, T$</code>
<code>$$ \begin{array}{c} \delta_{t}(i)=\max _{1 \leqslant j \leqslant N}\left[\delta_{t-1}(j) a_{j i}\right] b_{i}\left(o_{t}\right), \quad i=1,2, \cdots, N \\ \Psi_{t}(i)=\arg \max _{1 \leqslant j \leqslant N}\left[\delta_{t-1}(j) a_{j i}\right], \quad i=1,2, \cdots, N \end{array} $$</code></li>
<li>终止。
<code>$$ \begin{array}{c} P^{*}=\max _{1 \leqslant i \leqslant N} \delta_{T}(i) \\ i_{T}^{*}=\arg \max _{1 \leqslant i \leqslant N}\left[\delta_{T}(i)\right] \end{array} $$</code></li>
<li>最优路径回溯。对 <code>$t = T - 1, T - 2, \cdots, 1$</code>
<code>$$ i_{t}^{*}=\Psi_{t+1}\left(i_{t+1}^{*}\right) $$</code></li>
</ol>
<p>求的最优路径 <code>$I^{*}=\left(i_{1}^{*}, i_{2}^{*}, \cdots, i_{T}^{*}\right)$</code>。</p>
<h2 id="条件随机场">条件随机场</h2>
<p>概率无向图模型（Probabilistic Undirected Graphical Model）又称为马尔可夫随机场（Markov Random Field），是一个可以由无向图表示的联合概率分布。概率图模型（Probabilistic Graphical Model）是由图表示的概率分布，设有联合概率分布 <code>$P \left(Y\right), Y \in \mathcal{Y}$</code> 是一组随机变量。由无向图 <code>$G = \left(V, E\right)$</code> 表示概率分布 <code>$P \left(Y\right)$</code>，即在图 <code>$G$</code> 中，结点 <code>$v \in V$</code> 表示一个随机变量 <code>$Y_v, Y = \left(Y_v\right)_{v \in V}$</code>，边 <code>$e \in E$</code> 表示随机变量之间的概率依赖关系。</p>
<p><strong>成对马尔可夫性</strong>：设 <code>$u$</code> 和 <code>$v$</code> 是无向图 <code>$G$</code> 中任意两个没有边连接的结点，结点 <code>$u$</code> 和 <code>$v$</code> 分别对应随机变量 <code>$Y_u$</code> 和 <code>$Y_v$</code>。其他所有结点为 <code>$O$</code>，对应的随机变量组是 <code>$Y_O$</code>。成对马尔可夫是指给定随机变量组 <code>$Y_O$</code> 的条件下随机变量 <code>$Y_u$</code> 和 <code>$Y_v$</code> 是条件独立的，即：</p>
<p><code>$$ P\left(Y_{u}, Y_{v} | Y_{O}\right)=P\left(Y_{u} | Y_{O}\right) P\left(Y_{v} | Y_{O}\right) $$</code></p>
<p><strong>局部马尔可夫性</strong>：设 <code>$v \in V$</code> 是无向图 <code>$G$</code> 中任意一个结点，<code>$W$</code> 是与 <code>$v$</code> 有边连接的所有结点，<code>$O$</code> 是 <code>$v$</code> 和 <code>$W$</code> 以外的其他所有结点。<code>$v$</code> 表示的随机变量是 <code>$Y_v$</code>，<code>$W$</code> 表示的随机变量组是 <code>$Y_W$</code>，<code>$O$</code> 表示的随机变量组是 <code>$Y_O$</code>。局部马尔可夫性是指在给定随机变量组 <code>$Y_W$</code> 的条件下随机变量 <code>$Y_v$</code> 与随机变量组 <code>$Y_O$</code> 是独立的，即：</p>
<p><code>$$ P\left(Y_{v}, Y_{O} | Y_{W}\right)=P\left(Y_{v} | Y_{W}\right) P\left(Y_{O} | Y_{W}\right) $$</code></p>
<p>在 <code>$P \left(Y_O | Y_W\right) &gt; 0$</code> 时，等价地：</p>
<p><code>$$ P\left(Y_{v} | Y_{W}\right)=P\left(Y_{v} | Y_{W}, Y_{O}\right) $$</code></p>
<p>局部马尔可夫性如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/local-markov.png" class="lazyload"/>
  
</figure>
<p><strong>全局马尔可夫性</strong>：设结点结合 <code>$A, B$</code> 是在无向图 <code>$G$</code> 中被结点集合 <code>$C$</code> 分开的任意结点集合，如下图所示。结点集合 <code>$A, B$</code> 和 <code>$C$</code> 所对应的随机变量组分别是 <code>$Y_A, Y_B$</code> 和 <code>$Y_C$</code>。全局马尔可夫性是指给定随机变量组 <code>$Y_C$</code> 条件下随机变量组 <code>$Y_A$</code> 和 <code>$Y_B$</code> 是条件独立的，即：</p>
<p><code>$$ P\left(Y_{A}, Y_{B} | Y_{C}\right)=P\left(Y_{A} | Y_{C}\right) P\left(Y_{B} | Y_{C}\right) $$</code></p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/global-markov.png" class="lazyload"/>
  
</figure>
<p><strong>概率无向图模型</strong>定义为：设有联合概率分布 <code>$P \left(Y\right)$</code>，由无向图 <code>$G = \left(V, E\right)$</code> 表示，在图 <code>$G$</code> 中，结点表示随机变量，边表示随机变量之间的依赖关系。如果联合概率分布 <code>$P \left(Y\right)$</code> 满足成对、局部或全局马尔可夫性，就称此联合概率分布为概率无向图模型（Probabilistic Undirected Graphical Model），或马尔可夫随机场（Markov Random Field）。</p>
<p><strong>团与最大团</strong>：无向图 <code>$G$</code> 中任何两个结点均有边连接的结点子集称为团（Clique）。若 <code>$C$</code> 是无向图 <code>$G$</code> 的一个团，并且不能再加进任何一个 <code>$G$</code> 的结点时期成为一个更大的团，则称此 <code>$C$</code> 为最大团（Maximal Clique）。</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/clique.png" class="lazyload"/>
  <figcaption><p class="figcaption">无向图的团和最大团</p></figcaption>
</figure>
<p>上图表示 4 个结点组成的无向图。图中有 2 个结点组成的团有 5 个：<code>$\left\{Y_1, Y_2\right\}$</code>，<code>$\left\{Y_2, Y_3\right\}$</code>，<code>$\left\{Y_3, Y_4\right\}$</code>，<code>$\left\{Y_4, Y_2\right\}$</code> 和 <code>$\left\{Y_1, Y_3\right\}$</code>。有 2 个最大团：<code>$\left\{Y_1, Y_2, Y_3\right\}$</code> 和 <code>$\left\{Y_2, Y_3, Y_4\right\}$</code>。而 <code>$\left\{Y_1, Y_2, Y_3, Y_4\right\}$</code> 不是一个团，因为 <code>$Y_1$</code> 和 <code>$Y_4$</code> 没有边连接。</p>
<p>将概率无向图模型的联合概率分布表示为其最大团上的随机变量的函数的乘积形式的操作，称为概率无向图模型的因子分解。给定无向图模型，设其无向图为 <code>$G$</code>，<code>$C$</code> 为 <code>$G$</code> 上的最大团，<code>$Y_C$</code> 表示 <code>$C$</code> 对应的随机变量。那么概率无向图模型的联合概率分布 <code>$P \left(Y\right)$</code> 可以写作图中所有最大团 <code>$C$</code> 上的函数 <code>$\Psi_C \left(Y_C\right)$</code> 的乘积形式，即：</p>
<p><code>$$ P(Y)=\frac{1}{Z} \prod_{C} \Psi_{C}\left(Y_{C}\right) $$</code></p>
<p>其中，<code>$Z$</code> 是规范化因子：</p>
<p><code>$$ Z=\sum_{Y} \prod_{C} \Psi_{C}\left(Y_{C}\right) $$</code></p>
<p>规范化因子保证 <code>$P \left(Y\right)$</code> 构成一个概率分布。函数 <code>$\Psi_C \left(Y_C\right)$</code> 称为<strong>势函数</strong>，这里要求势函数 <code>$\Psi_C \left(Y_C\right)$</code> 是严格正的，通常定义为指数函数：</p>
<p><code>$$ \Psi_{C}\left(Y_{C}\right)=\exp \left\{-E\left(Y_{C}\right)\right\} $$</code></p>
<p>概率无向图模型的因子分解由这个 Hammersley-Clifford 定理来保证。</p>
<p><strong>条件随机场</strong>（Conditional Random Field）是给定随机变量 <code>$X$</code> 条件下，随机变量 <code>$Y$</code> 的马尔可夫随机场。设 <code>$X$</code> 与 <code>$Y$</code> 是随机变量，<code>$P \left(Y | X\right)$</code> 是给定 <code>$X$</code> 的条件下 <code>$Y$</code> 的条件概率分布。若随机变量 <code>$Y$</code> 构成一个有无向图 <code>$G = \left(V, E\right)$</code> 表示的马尔可夫随机场，即：</p>
<p><code>$$ P\left(Y_{v} | X, Y_{w}, w \neq v\right)=P\left(Y_{v} | X, Y_{w}, w \sim v\right) $$</code></p>
<p>对任意结点 <code>$v$</code> 成立，则称条件概率分布 <code>$P \left(Y | X\right)$</code> 为条件随机场。其中，<code>$w \sim v$</code> 表示在图 <code>$G = \left(V, E\right)$</code> 中与结点 <code>$v$</code> 有边连接的所有结点 <code>$w$</code>，<code>$w \neq v$</code> 表示结点 <code>$v$</code> 以外的所有结点，<code>$Y_v, Y_u$</code> 与 <code>$Y_w$</code> 为结点 <code>$v, u$</code> 和 <code>$w$</code> 对应的随机变量。</p>
<p>定义中并没有要求 <code>$X$</code> 和 <code>$Y$</code> 具有相同的结构，一般假设 <code>$X$</code> 和 <code>$Y$</code> 有相同的图结构，下图展示了无向图的线性链情况，即：</p>
<p><code>$$ G=(V=\{1,2, \cdots, n\}, E=\{(i, i+1)\}), \quad i=1,2, \cdots, n-1 $$</code></p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/linear-crf-1.png" class="lazyload"/>
  <figcaption><p class="figcaption">线性链条件随机场</p></figcaption>
</figure>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/linear-crf-2.png" class="lazyload"/>
  <figcaption><p class="figcaption">X 和 Y 有相同的图结构的线性链条件随机场</p></figcaption>
</figure>
<p>此情况下，<code>$X=\left(X_{1}, X_{2}, \cdots, X_{n}\right), Y=\left(Y_{1}, Y_{2}, \cdots, Y_{n}\right)$</code>，最大团是相邻两个结点的集合。</p>
<p><strong>线性链条件随机场</strong>：设 <code>$X=\left(X_{1}, X_{2}, \cdots, X_{n}\right), Y=\left(Y_{1}, Y_{2}, \cdots, Y_{n}\right)$</code> 均为线性链表示的随机变量序列，若在给定随机变量序列 <code>$X$</code> 的条件下，随机变量序列 <code>$Y$</code> 的条件概率分布 <code>$P \left(Y | X\right)$</code> 构成条件随机场，即满足马尔可夫性：</p>
<p><code>$$ \begin{array}{c} P\left(Y_{i} | X, Y_{1}, \cdots, Y_{i-1}, Y_{i+1}, \cdots, Y_{n}\right)=P\left(Y_{i} | X, Y_{i-1}, Y_{i+1}\right) \\ i=1,2, \cdots, n \quad (\text { 在 } i=1 \text { 和 } n \text { 时只考虑单边 }) \end{array} $$</code></p>
<p>则称 <code>$P \left(Y | X\right)$</code> 为线性链条件随机场。在标注问题中，<code>$X$</code> 表示输入观测序列，<code>$Y$</code> 表示对应的输出标记序列或状态序列。</p>
<p>根据 Hammersley-Clifford 定理，设 <code>$P \left(Y | X\right)$</code> 为线性链条件随机场，则在随机变量 <code>$X$</code> 取值为 <code>$x$</code> 的条件下，随机变量 <code>$Y$</code> 取值为 <code>$y$</code> 的条件概率有如下形式：</p>
<p><code>$$ P(y | x)=\frac{1}{Z(x)} \exp \left(\sum_{i, k} \lambda_{k} t_{k}\left(y_{i-1}, y_{i}, x, i\right)+\sum_{i, l} \mu_{l} s_{l}\left(y_{i}, x, i\right)\right) $$</code></p>
<p>其中，</p>
<p><code>$$ Z(x)=\sum_{y} \exp \left(\sum_{i, k} \lambda_{k} t_{k}\left(y_{i-1}, y_{i}, x, i\right)+\sum_{i, l} \mu_{l} s_{l}\left(y_{i}, x, i\right)\right) $$</code></p>
<p>其中，<code>$t_k$</code> 和 <code>$s_l$</code> 是特征函数，<code>$\lambda_k$</code> 和 <code>$\mu_l$</code> 是对应的权值。<code>$Z \left(x\right)$</code> 是规范化因子，求和是在所有可能的输出序列上进行的。</p>
<p>条件随机场的概率计算，学习算法和预测算法类似隐马尔可夫模型，在此不进行过多赘述，有兴趣的同学可以参见 <sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。</p>
<p>综上所述，隐马尔可夫模型和条件随机场的主要联系和区别如下：</p>
<ol>
<li>HMM 是概率有向图，CRF 是概率无向图</li>
<li>HMM 是生成模型，CRF 是判别模型</li>
</ol>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/relationship-between-nb-lr-hmm-lcrf-gdm-gcrf.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：An Introduction to Conditional Random Fields</p></figcaption>
</figure>
<p>如上图所示，上面部分为生成式模型，下面部分为判别式模型，生成式模型尝试构建联合分布 <code>$P \left(Y, X\right)$</code>，而判别模型则尝试构建条件分布 <code>$P \left(Y | X\right)$</code>。</p>
<h2 id="序列标注">序列标注</h2>
<p>序列标注（Sequence Labeling）是自然语言处理中的一项重要任务，对于给定的文本序列需要给出对应的标注序列。常见的序列标注任务包含：组块分析（Chunking），词性标注（Part-of-Speech，POS）和命名实体识别（Named Entity Recognition，NER）。</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/pos-ner-demo.png" class="lazyload"/>
  
</figure>
<p>上图为一段文本的词性标注和命名实体识别的结果。</p>
<h3 id="词性标注">词性标注</h3>
<p>词性标注是指为分词结果中的每个单词标注一个正确的词性，即确定每个词是名词、动词、形容词或其他词性的过程。</p>
<p>一些常用中文标注规范如下：</p>
<ol>
<li>北京大学现代汉语语料库基本加工规范 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></li>
<li>北大语料库加工规范：切分·词性标注·注音 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></li>
<li>计算所汉语词性标记集 3.0（ICTPOS 3.0）<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup></li>
<li>The Part-Of-Speech Tagging Guidelines for the Penn Chinese Treebank (3.0) <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup></li>
<li>中文文本标注规范（微软亚洲研究院）<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup></li>
</ol>
<h3 id="命名实体识别">命名实体识别</h3>
<p>命名实体识别，又称作“专名识别”，是指识别文本中具有特定意义的实体，主要包括人名、地名、机构名、专有名词等。简单的讲，就是识别自然文本中的实体指称的边界和类别。</p>
<p>常用的标注标准有 IO，BIO，BIOES，BMEWO 和 BMEWO+ 等。（参考自：<a href="https://lingpipe-blog.com/2009/10/14/coding-chunkers-as-taggers-io-bio-bmewo-and-bmewo/">Coding Chunkers as Taggers: IO, BIO, BMEWO, and BMEWO+</a>）</p>
<ol>
<li>IO 标注标准是最简单的标注方式，对于命名实体类别 X 标注为 <code>I_X</code>，其他则标注为 <code>O</code>。由于没有标签界线表示，这种方式无法表示两个相邻的同类命名实体。</li>
<li>BIO 标注标准将命名实体的起始部分标记为 <code>B_X</code>，其余部分标记为 <code>I_X</code>。</li>
<li>BIOES 标注标准将命名实体的起始部分标记为 <code>B_X</code>，中间部分标记为 <code>I_X</code>，结尾部分标记为 <code>E_X</code>，对于单个字符成为命名实体的情况标记为 <code>S_X</code>。</li>
<li>BMEWO 标注标准将命名实体的起始部分标记为 <code>B_X</code>，中间部分标记为 <code>M_X</code>，结尾部分标记为 <code>E_X</code>，对于单个字符成为命名实体的情况标记为 <code>W_X</code>。</li>
<li>BMEWO+ 标注标准在 BMEWO 的基础上针对不同情况的非命名实体标签的标注进行了扩展，同时增加了一个句外（out-of-sentence）标签 <code>W_OOS</code>，句子起始标签 <code>BB_O_OOS</code> 和句子结束标签 <code>WW_O_OOS</code>，如 <a href="http://www.alias-i.com/lingpipe/docs/api/com/aliasi/chunk/HmmChunker.html">下表</a> 所示：</li>
</ol>
<table>
  <thead>
      <tr>
          <th>标签</th>
          <th>描述</th>
          <th>可能上接的标签</th>
          <th>可能下接的标签</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>B_X</code></td>
          <td>命名实体类型 X 的起始</td>
          <td><code>E_Y, W_Y, EE_O_X, WW_O_X</code></td>
          <td><code>M_X, W_X</code></td>
      </tr>
      <tr>
          <td><code>M_X</code></td>
          <td>命名实体类型 X 的中间</td>
          <td><code>B_X, M_X</code></td>
          <td><code>M_X, W_X</code></td>
      </tr>
      <tr>
          <td><code>E_X</code></td>
          <td>命名实体类型 X 的结尾</td>
          <td><code>B_X, M_X</code></td>
          <td><code>B_Y, W_Y, BB_O_X, WW_O_X</code></td>
      </tr>
      <tr>
          <td><code>W_X</code></td>
          <td>命名实体类型 X 的单个字符</td>
          <td><code>E_Y, W_Y, EE_O_X, WW_O_X</code></td>
          <td><code>B_Y, W_Y, BB_O_X, WW_O_X</code></td>
      </tr>
      <tr>
          <td><code>BB_O_X</code></td>
          <td>非命名实体的起始，上接命名实体类型 X</td>
          <td><code>E_X, W_X</code></td>
          <td><code>MM_O, EE_O_Y</code></td>
      </tr>
      <tr>
          <td><code>MM_O</code></td>
          <td>非命名实体的中间</td>
          <td><code>BB_O_Y, MM_O</code></td>
          <td><code>MM_O, EE_O_Y</code></td>
      </tr>
      <tr>
          <td><code>EE_O_X</code></td>
          <td>非命名实体的结尾，下接命名实体类型 X</td>
          <td><code>BB_O_Y, MM_O</code></td>
          <td><code>B_X, W_X</code></td>
      </tr>
      <tr>
          <td><code>WW_O_X</code></td>
          <td>非命名实体，上接命名实体，下接命名实体类型 X</td>
          <td><code>E_X, W_X</code></td>
          <td><code>B_Y, W_Y</code></td>
      </tr>
  </tbody>
</table>
<p>不同标注标准的差别示例如下：</p>
<table>
  <thead>
      <tr>
          <th>字符</th>
          <th>IO</th>
          <th>BIO</th>
          <th>BIOES</th>
          <th>BMEWO</th>
          <th>BMEWO+</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td><code>W_OOS</code></td>
      </tr>
      <tr>
          <td>Yesterday</td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>BB_O_OOS</code></td>
      </tr>
      <tr>
          <td>afternoon</td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>MM_O</code></td>
      </tr>
      <tr>
          <td>,</td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>EE_O_PER</code></td>
      </tr>
      <tr>
          <td>John</td>
          <td><code>I_PER</code></td>
          <td><code>B_PER</code></td>
          <td><code>B_PER</code></td>
          <td><code>B_PER</code></td>
          <td><code>B_PER</code></td>
      </tr>
      <tr>
          <td>J</td>
          <td><code>I_PER</code></td>
          <td><code>I_PER</code></td>
          <td><code>I_PER</code></td>
          <td><code>M_PER</code></td>
          <td><code>M_PER</code></td>
      </tr>
      <tr>
          <td>.</td>
          <td><code>I_PER</code></td>
          <td><code>I_PER</code></td>
          <td><code>I_PER</code></td>
          <td><code>M_PER</code></td>
          <td><code>M_PER</code></td>
      </tr>
      <tr>
          <td>Smith</td>
          <td><code>I_PER</code></td>
          <td><code>I_PER</code></td>
          <td><code>E_PER</code></td>
          <td><code>E_PER</code></td>
          <td><code>E_PER</code></td>
      </tr>
      <tr>
          <td>traveled</td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>BB_O_PER</code></td>
      </tr>
      <tr>
          <td>to</td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>EE_O_LOC</code></td>
      </tr>
      <tr>
          <td>Washington</td>
          <td><code>I_LOC</code></td>
          <td><code>B_LOC</code></td>
          <td><code>S_LOC</code></td>
          <td><code>W_LOC</code></td>
          <td><code>W_LOC</code></td>
      </tr>
      <tr>
          <td>.</td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>O</code></td>
          <td><code>WW_O_OOS</code></td>
      </tr>
      <tr>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td><code>W_OOS</code></td>
      </tr>
  </tbody>
</table>
<p>不同标准的标签数量如下表所示：</p>
<table>
  <thead>
      <tr>
          <th>标注标准</th>
          <th>标签数量</th>
          <th>N=1</th>
          <th>N=3</th>
          <th>N=20</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>IO</td>
          <td>N+1</td>
          <td>2</td>
          <td>4</td>
          <td>21</td>
      </tr>
      <tr>
          <td>BIO</td>
          <td>2N+1</td>
          <td>3</td>
          <td>7</td>
          <td>41</td>
      </tr>
      <tr>
          <td>BIOES</td>
          <td>4N+1</td>
          <td>5</td>
          <td>13</td>
          <td>81</td>
      </tr>
      <tr>
          <td>BMEWO</td>
          <td>4N+1</td>
          <td>5</td>
          <td>13</td>
          <td>81</td>
      </tr>
      <tr>
          <td>BMEWO+</td>
          <td>7N+3</td>
          <td>10</td>
          <td>24</td>
          <td>143</td>
      </tr>
  </tbody>
</table>
<p>其中，N 为命名实体类型的数量。</p>
<h4 id="bilstm-crf">BiLSTM CRF <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup></h4>
<blockquote>
<p>本小节内容参考和修改自 <a href="https://github.com/createmomo/CRF-Layer-on-the-Top-of-BiLSTM">CRF-Layer-on-the-Top-of-BiLSTM</a>。</p>
</blockquote>
<p>Huang 等人提出了一种基于 BiLSTM 和 CRF 的神经网络模型用于序例标注。整个网络如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/bilstm-crf.png" class="lazyload"/>
  
</figure>
<p>关于模型中的 BiLSTM 部分在此不过多赘述，相关细节可以参见之前的博客：<a href="/cn/2018/09/rnn/">循环神经网络 (Recurrent Neural Network, RNN)</a> 和 <a href="/cn/2020/03/pre-trained-model-for-nlp/">预训练自然语言模型 (Pre-trained Models for NLP)</a>。BiLSTM-CRF 模型的输入是词嵌入向量，输出是对应的预测标注标签，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/bilstm-crf-1.png" class="lazyload"/>
  
</figure>
<p>BiLSTM 层的输出为每个标签的分数，对于 <code>$w_0$</code>，BiLSTM 的输出为 1.5 (<code>B_PER</code>)，0.9 (<code>I_PER</code>)，0.1 (<code>B_ORG</code>)，0.08 (<code>I_ORG</code>) 和 0.05 (<code>O</code>)，这些分数为 CRF 层的输入，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/bilstm-crf-2.png" class="lazyload"/>
  
</figure>
<p>经过 CRF 层后，具有最高分数的预测序列被选择为最优预测结果。如果没有 CRF 层，我们可以直接选择 BiLSTM 层输出分数的最大值对应的序列为预测结果。例如，对于 <code>$w_0$</code>，最高分数为 1.5，对应的预测标签则为 <code>B_PER</code>，类似的 <code>$w_1, w_2, w_3, w_4$</code> 对应的预测标签为 <code>I_PER, O, B_ORG, O</code>，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/bilstm-crf-3.png" class="lazyload"/>
  
</figure>
<p>虽然我们在上例中得到了正确的结果，但通常情况下并非如此。对于如下的示例，预测结果为 <code>I_ORG, I_PER, O, I_ORG, I_PER</code>，这显然是不正确的。</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/bilstm-crf-4.png" class="lazyload"/>
  
</figure>
<p>CRF 层在进行预测时可以添加一些约束，这些约束可以在训练时被 CRF 层学习得到。可能的约束有：</p>
<ul>
<li>句子的第一个词的标签可以是 <code>B_X</code> 或 <code>O</code>，而非 <code>I_X</code>。</li>
<li><code>B_X, I_X</code> 是有效的标签，而 <code>B_X, I_Y</code> 是无效的标签。</li>
<li>一个命名实体的起始标签应为 <code>B_X</code> 而非 <code>I_X</code>。</li>
</ul>
<p>CRF 层的损失包含两部分，这两部分构成了 CRF 层的关键：</p>
<ul>
<li>发射分数（Emission Score）</li>
</ul>
<p>发射分数即为 BiLSTM 层的输出分数，例如 <code>$w_0$</code> 对应的标签 <code>B_PER</code> 的分数为 1.5。为了方便起见，对于每类标签给定一个索引：</p>
<table>
  <thead>
      <tr>
          <th>标签</th>
          <th>索引</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>B_PER</code></td>
          <td>0</td>
      </tr>
      <tr>
          <td><code>I_PER</code></td>
          <td>1</td>
      </tr>
      <tr>
          <td><code>B_ORG</code></td>
          <td>2</td>
      </tr>
      <tr>
          <td><code>I_ORG</code></td>
          <td>3</td>
      </tr>
      <tr>
          <td><code>O</code></td>
          <td>4</td>
      </tr>
  </tbody>
</table>
<p>我们利用 <code>$x_{i y_{j}}$</code> 表示发射分数，<code>$i$</code> 为词的索引，<code>$y_i$</code> 为标注标签的索引。例如：<code>$x_{i=1, y_{j}=2} = x_{w_1, \text{B_ORG}} = 0.1$</code>，表示 <code>$w_1$</code> 为 <code>B_ORG</code> 的分数为 0.1。</p>
<ul>
<li>转移分数（Transition Score）</li>
</ul>
<p>我们利用 <code>$t_{y_i, y_j}$</code> 表示转移分数，例如 <code>$t_{\text{B_PER}, \text{I_PER}} = 0.9$</code> 表示由标签 <code>B_PER</code> 转移到 <code>I_PER</code> 的分数为 0.9。因此，需要一个转移分数矩阵用于存储所有标注标签之间的转移分数。为了使得转移分数矩阵更加鲁棒，需要添加两个标签 <code>START</code> 和 <code>END</code>，分别表示一个句子的开始和结束。下表为一个转移分数矩阵的示例：</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th><code>START</code></th>
          <th><code>B-PER</code></th>
          <th><code>I-PER</code></th>
          <th><code>B-ORG</code></th>
          <th><code>I-ORG</code></th>
          <th><code>O</code></th>
          <th><code>END</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>START</code></td>
          <td>0</td>
          <td>0.8</td>
          <td>0.007</td>
          <td>0.7</td>
          <td>0.0008</td>
          <td>0.9</td>
          <td>0.08</td>
      </tr>
      <tr>
          <td><code>B_PER</code></td>
          <td>0</td>
          <td>0.6</td>
          <td>0.9</td>
          <td>0.2</td>
          <td>0.0006</td>
          <td>0.6</td>
          <td>0.009</td>
      </tr>
      <tr>
          <td><code>I_PER</code></td>
          <td>-1</td>
          <td>0.5</td>
          <td>0.53</td>
          <td>0.55</td>
          <td>0.0003</td>
          <td>0.85</td>
          <td>0.008</td>
      </tr>
      <tr>
          <td><code>B_ORG</code></td>
          <td>0.9</td>
          <td>0.5</td>
          <td>0.0003</td>
          <td>0.25</td>
          <td>0.8</td>
          <td>0.77</td>
          <td>0.006</td>
      </tr>
      <tr>
          <td><code>I_ORG</code></td>
          <td>-0.9</td>
          <td>0.45</td>
          <td>0.007</td>
          <td>0.7</td>
          <td>0.65</td>
          <td>0.76</td>
          <td>0.2</td>
      </tr>
      <tr>
          <td><code>O</code></td>
          <td>0</td>
          <td>0.65</td>
          <td>0.0007</td>
          <td>0.7</td>
          <td>0.0008</td>
          <td>0.9</td>
          <td>0.08</td>
      </tr>
      <tr>
          <td><code>END</code></td>
          <td>0</td>
          <td>0</td>
          <td>0</td>
          <td>0</td>
          <td>0</td>
          <td>0</td>
          <td>0</td>
      </tr>
  </tbody>
</table>
<p>转移分数矩阵作为 BiLSTM-CRF 模型的一个参数，随机初始化并通过模型的训练不断更新，最终学习得到约束条件。</p>
<p>CRF 层的损失函数包含两个部分：真实路径分数和所有可能路径的总分数。假设每个可能的路径有一个分数 <code>$P_i$</code>，共 <code>$N$</code> 种可能的路径，所有路径的总分数为：</p>
<p><code>$$ P_{\text {total}}=P_{1}+P_{2}+\ldots+P_{N}=e^{S_{1}}+e^{S_{2}}+\ldots+e^{S_{N}} $$</code></p>
<p>则损失函数定义为：</p>
<p><code>$$ \text{Loss} = \dfrac{P_{\text{RealPath}}}{\sum_{i=1}^{N} P_i} $$</code></p>
<p>对于 <code>$S_i$</code>，共包含两部分：发射分数和转移分数。以路径 <code>START -&gt; B_PER -&gt; I_PER -&gt; O -&gt; B_ORG -&gt; O -&gt; END</code> 为例，发射分数为：</p>
<p><code>$$ \begin{aligned} \text{EmissionScore} = \ &amp;x_{0, \text{START}} + x_{1, \text{B_PER}} + x_{2, \text{I_PER}} \\ &amp;+ x_{3, \text{O}} + x_{4, \text{B_ORG}} + x_{5, \text{O}} + x_{6, \text{END}} \end{aligned} $$</code></p>
<p>其中 <code>$x_{i, y_j}$</code> 表示第 <code>$i$</code> 个词标签为 <code>$y_j$</code> 的分数，为 BiLSTM 的输出，<code>$x_{0, \text{START}}$</code> 和 <code>$x_{6, \text{END}}$</code> 可以设置为 0。转换分数为：</p>
<p><code>$$ \begin{aligned} \text{TransitionScore} = \ &amp;t_{\text{START}, \text{B_PER}} + t_{\text{B_PER}, \text{I_PER}} + t_{\text{I_PER}, \text{O}} \\ &amp;+ t_{\text{O}, \text{B_ORG}} + t_{\text{B_ORG}, \text{O}} + t_{\text{O}, \text{END}} \end{aligned} $$</code></p>
<p>其中 <code>$t_{y_i, y_j}$</code> 表示标注标签由 <code>$y_i$</code> 转移至 <code>$y_j$</code> 的分数。</p>
<p>对于所有路径的总分数的计算过程采用了类似 <a href="/cn/2018/11/computational-complexity-and-dynamic-programming/">动态规划</a> 的思想，整个过程计算比较复杂，在此不再详细展开，详细请参见参考文章。</p>
<p>利用训练好的 BiLSTM-CRF 模型进行预测时，首先我们可以得到序列的发射分数和转移分数，其次用维特比算法可以得到最终的预测标注序列。</p>
<h4 id="lattice-lstm">Lattice LSTM <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup></h4>
<p>Zhang 等人针对中文提出了一种基于 Lattice LSTM 的命名实体识别方法，Lattice LSTM 的结构如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-05-02-hmm-and-crf/lattice-lstm.png" class="lazyload"/>
  
</figure>
<p>模型的基本思想是将句子中的词汇（例如：南京，长江大桥等）信息融入到基于字符的 LSTM 模型中，从而可以显性地利用词汇信息。</p>
<p>模型的输入为一个字符序列 <code>$c_1, c_2, \cdots, c_m$</code> 和词汇表 <code>$\mathbb{D}$</code> 中所有匹配的字符子序列，其中词汇表 <code>$\mathbb{D}$</code> 利用大量的原始文本通过分词构建。令 <code>$w_{b, e}^d$</code> 表示有以第 <code>$b$</code> 个字符起始，以第 <code>$e$</code> 个字符结尾的子序列，例如：<code>$w_{1,2}^d$</code> 表示“南京
”，<code>$w_{7,8}^d$</code> 表示“大桥”。</p>
<p>不同于一般的字符级模型，LSTM 单元的状态考虑了句子中的子序列 <code>$w_{b,e}^d$</code>，每个子序列 <code>$w_{b,e}^d$</code> 表示为：</p>
<p><code>$$ \mathbf{x}_{b, e}^{w}=\mathbf{e}^{w}\left(w_{b, e}^{d}\right) $$</code></p>
<p>其中，<code>$\mathbf{e}^{w}$</code> 为词向量查询表。一个词单元 <code>$\mathbf{c}_{b,e}^w$</code> 用于表示 <code>$\mathbf{x}_{b,e}^w$</code> 的循环状态：</p>
<p><code>$$ \begin{aligned} \left[\begin{array}{c} \mathbf{i}_{b, e}^{w} \\ \mathbf{f}_{b, e}^{w} \\ \widetilde{c}_{b, e}^{w} \end{array}\right] &amp;=\left[\begin{array}{c} \sigma \\ \sigma \\ \tanh \end{array}\right]\left(\mathbf{W}^{w \top}\left[\begin{array}{c} \mathbf{x}_{b, e}^{w} \\ \mathbf{h}_{b}^{c} \end{array}\right]+\mathbf{b}^{w}\right) \\ \mathbf{c}_{b, e}^{w} &amp;=\mathbf{f}_{b, e}^{w} \odot \mathbf{c}_{b}^{c}+\mathbf{i}_{b, e}^{w} \odot \widetilde{c}_{b, e}^{w} \end{aligned} $$</code></p>
<p>其中，<code>$\mathbf{i}_{b, e}^{w}$</code> 和 <code>$\mathbf{f}_{b, e}^{w}$</code> 分别为输入门和遗忘门。由于仅在字符级别上进行标注，因此对于词单元来说没有输出门。</p>
<p>对于 <code>$\mathbf{c}_{j}^c$</code> 来说可能有多条信息流，例如 <code>$\mathbf{c}_7^c$</code> 的输入包括 <code>$\mathbf{x}_7^c$</code>（桥），<code>$\mathbf{c}_{6,7}^w$</code>（大桥）和 <code>$\mathbf{c}_{4,7}^w$</code>（长江大桥）。论文采用了一个新的门 <code>$\mathbf{i}_{b,e}^c$</code> 来控制所有子序列单元 <code>$\mathbf{c}_{b,e}^w$</code> 对 <code>$\mathbf{c}_{j}^c$</code> 的贡献：</p>
<p><code>$$ \mathbf{i}_{b, e}^{c}=\sigma\left(\mathbf{W}^{l \top}\left[\begin{array}{c} \mathbf{x}_{e}^{c} \\ \mathbf{c}_{b, e}^{w} \end{array}\right]+\mathbf{b}^{l}\right) $$</code></p>
<p>则单元状态 <code>$\mathbf{c}_j^c$</code> 的计算变为：</p>
<p><code>$$ \mathbf{c}_{j}^{c}=\sum_{b \in\left\{b^{\prime} | w_{b^{\prime}, j} \in \mathbb{D}\right\}} \boldsymbol{\alpha}_{b, j}^{c} \odot \boldsymbol{c}_{b, j}^{w}+\boldsymbol{\alpha}_{j}^{c} \odot \widetilde{\boldsymbol{c}}_{j}^{c} $$</code></p>
<p>在上式中，<code>$\mathbf{i}_{b,j}^c$</code> 和 <code>$\mathbf{i}_j^c$</code> 标准化为 <code>$\boldsymbol{\alpha}_{b, j}^{c}$</code> 和 <code>$\boldsymbol{\alpha}_{j}^{c}$</code>：</p>
<p><code>$$ \begin{aligned} \boldsymbol{\alpha}_{b, j}^{c} &amp;=\frac{\exp \left(\mathbf{i}_{b, j}^{c}\right)}{\exp \left(\mathbf{i}_{j}^{c}\right)+\sum_{b^{\prime} \in\left\{b^{\prime \prime} | w_{b^{\prime \prime}, j}^{d} \in \mathbb{D}\right\}} \exp \left(\mathbf{i}_{b^{\prime}, j}^{c}\right)} \\ \boldsymbol{\alpha}_{j}^{c} &amp;=\frac{\exp \left(\mathbf{i}_{j}^{c}\right)}{\exp \left(\mathbf{i}_{j}^{c}\right)+\sum_{b^{\prime} \in\left\{b^{\prime \prime} | w_{b^{\prime \prime}, j}^{d} \in \mathbb{D}\right\}} \exp \left(\mathbf{i}_{b^{\prime}, j}^{c}\right)} \end{aligned} $$</code></p>
<h2 id="开放资源">开放资源</h2>
<h3 id="标注工具">标注工具</h3>
<ol>
<li><a href="https://github.com/synyi/poplar">synyi/poplar</a></li>
<li><a href="https://github.com/nlplab/brat">nlplab/brat</a></li>
<li><a href="https://github.com/doccano/doccano">doccano/doccano</a></li>
<li><a href="https://github.com/heartexlabs/label-studio">heartexlabs/label-studio</a></li>
<li><a href="https://github.com/deepwel/Chinese-Annotator">deepwel/Chinese-Annotator</a></li>
<li><a href="https://github.com/jiesutd/YEDDA">jiesutd/YEDDA</a></li>
</ol>
<h3 id="开源模型-框架和代码">开源模型，框架和代码</h3>
<ol>
<li><a href="https://github.com/pytorch/text">pytorch/text</a></li>
<li><a href="https://github.com/flairNLP/flair">flairNLP/flair</a></li>
<li><a href="https://github.com/PetrochukM/PyTorch-NLP">PetrochukM/PyTorch-NLP</a></li>
<li><a href="https://github.com/allenai/allennlp">allenai/allennlp</a></li>
<li><a href="https://github.com/fastnlp/fastNLP">fastnlp/fastNLP</a></li>
<li><a href="https://stanfordnlp.github.io/CoreNLP/index.html">Stanford CoreNLP</a></li>
<li><a href="http://neuroner.com/">NeuroNER</a></li>
<li><a href="https://spacy.io/">spaCy</a></li>
<li><a href="https://www.nltk.org/">NLTK</a></li>
<li><a href="https://github.com/BrikerMan/Kashgari">BrikerMan/Kashgari</a></li>
<li><a href="https://github.com/Hironsan/anago">Hironsan/anago</a></li>
<li><a href="https://github.com/crownpku/Information-Extraction-Chinese">crownpku/Information-Extraction-Chinese</a></li>
<li><a href="https://github.com/thunlp/OpenNRE">thunlp/OpenNRE</a></li>
<li><a href="https://github.com/hankcs/HanLP">hankcs/HanLP</a></li>
<li><a href="https://github.com/jiesutd/NCRFpp">jiesutd/NCRFpp</a></li>
</ol>
<h3 id="其他资源">其他资源</h3>
<ol>
<li><a href="https://github.com/keon/awesome-nlp">keon/awesome-nlp</a></li>
<li><a href="https://github.com/crownpku/Awesome-Chinese-NLP">crownpku/Awesome-Chinese-NLP</a></li>
<li><a href="https://github.com/sebastianruder/NLP-progress">sebastianruder/NLP-progress</a></li>
<li><a href="https://github.com/thunlp/NREPapers">thunlp/NREPapers</a></li>
</ol>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>李航. (2019). <em>统计学习方法（第二版）</em>. 清华大学出版社.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>俞士汶, 段慧明, 朱学锋, &amp; 孙斌. (2002). 北京大学现代汉语语料库基本加工规范. <em>中文信息学报</em>, 16(5), 51-66.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>俞士汶, 段慧明, 朱学锋, 孙斌, &amp; 常宝宝. (2003). 北大语料库加工规范: 切分· 词性标注· 注音. <em>汉语语言与计算学报</em>, 13(2), 121-158.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="http://ictclas.nlpir.org/nlpir/html/readme.htm">http://ictclas.nlpir.org/nlpir/html/readme.htm</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Xia, F. (2000). The part-of-speech tagging guidelines for the Penn Chinese Treebank (3.0). <em>IRCS Technical Reports Series</em>, 38.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Huang, C. N., Li, Y., &amp; Zhu, X. (2006). Tokenization guidelines of Chinese text (v5.0, in Chinese). <em>Microsoft Research Asia</em>.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Huang, Z., Xu, W., &amp; Yu, K. (2015). Bidirectional LSTM-CRF models for sequence tagging. <em>arXiv preprint arXiv:1508.01991</em>.&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Zhang, Y., &amp; Yang, J. (2018). Chinese NER Using Lattice LSTM. In <em>Proceedings of the 56th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)</em> (pp. 1554-1564).&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>图嵌入 (Graph Embedding) 和图神经网络 (Graph Neural Network)</title><link>https://zeqiang.fun/cn/2020/04/graph-embedding-and-gnn/</link><pubDate>Sat, 11 Apr 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/04/graph-embedding-and-gnn/</guid><description><![CDATA[
        <p>图（Graph / Network）数据类型可以自然地表达物体和物体之间的联系，在我们的日常生活与工作中无处不在。例如：微信和新浪微博等构成了人与人之间的社交网络；互联网上成千上万个页面构成了网页链接网络；国家城市间的运输交通构成了物流网络。</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/graph.png" class="lazyload"/>
  <figcaption><p class="figcaption">图片来源：<a href="https://www.allthingsdistributed.com/2019/12/power-of-relationships.html">The power of relationships in data</a></p></figcaption>
</figure>
<p>通常定义一个图 <code>$G = \left(V, E\right)$</code>，其中 <code>$V$</code> 为<strong>顶点（Vertices）<strong>集合，<code>$E$</code> 为</strong>边（Edges）<strong>集合。对于一条边 <code>$e = u, v$</code> 包含两个</strong>端点（Endpoints）</strong> <code>$u$</code> 和 <code>$v$</code>，同时 <code>$u$</code> 可以称为 <code>$v$</code> 的<strong>邻居（Neighbor）</strong>。当所有的边为有向边时，图称之为<strong>有向（Directed）<strong>图，当所有边为无向边时，图称之为</strong>无向（Undirected）<strong>图。对于一个顶点 <code>$v$</code>，令 <code>$d \left(v\right)$</code> 表示连接的边的数量，称之为</strong>度（Degree）</strong>。对于一个图 <code>$G = \left(V, E\right)$</code>，其<strong>邻接矩阵（Adjacency Matrix）</strong> <code>$A \in \mathbb{A}^{|V| \times |V|}$</code> 定义为：</p>
<p><code>$$ A_{i j}=\left\{\begin{array}{ll} 1 &amp; \text { if }\left\{v_{i}, v_{j}\right\} \in E \text { and } i \neq j \\ 0 &amp; \text { otherwise } \end{array}\right. $$</code></p>
<p>作为一个典型的非欧式数据，对于图数据的分析主要集中在节点分类，链接预测和聚类等。对于图数据而言，**图嵌入（Graph / Network Embedding）<strong>和</strong>图神经网络（Graph Neural Networks, GNN）**是两个类似的研究领域。图嵌入旨在将图的节点表示成一个低维向量空间，同时保留网络的拓扑结构和节点信息，以便在后续的图分析任务中可以直接使用现有的机器学习算法。一些基于深度学习的图嵌入同时也属于图神经网络，例如一些基于图自编码器和利用无监督学习的图卷积神经网络等。下图描述了图嵌入和图神经网络之间的差异：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/graph-embedding-vs-graph-neural-networks.png" class="lazyload"/>
  
</figure>
<div class="blockquote" style='border-left: 4px solid #F66E40;'>本文中<strong>图嵌入</strong>和<strong>网络表示学习</strong>均表示 Graph / Network Embedding。</div>
<h2 id="图嵌入">图嵌入</h2>
<blockquote>
<p>本节内容主要参考自：<br>
A Comprehensive Survey of Graph Embedding: Problems, Techniques and Applications <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup><br>
Graph Embedding Techniques, Applications, and Performance: A Survey <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup><br>
Representation Learning on Graphs: Methods and Applications <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
</blockquote>
<p>使用邻接矩阵的网络表示存在计算效率的问题，邻接矩阵 <code>$A$</code> 使用 <code>$|V| \times |V|$</code> 的存储空间表示一个图，随着节点个数的增长，这种表示所需的空间成指数增长。同时，在邻接矩阵中绝大多数是 0，数据的稀疏性使得快速有效的学习方式很难被应用。</p>
<p>网路表示学习是指学习得到网络中节点的低维向量表示，形式化地，网络表示学习的目标是对每个节点 <code>$v \in V$</code> 学习一个实值向量 <code>$R_v \in \mathbb{R}^k$</code>，其中 <code>$k \ll |V|$</code> 表示向量的维度。经典的 Zachary&rsquo;s karate club 网络的嵌入可视化如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/karate-graph-embedding.png" class="lazyload"/>
  
</figure>
<h3 id="random-walk">Random Walk</h3>
<p>基于随机游走的图嵌入通过使得图上一个短距的随机游走中共现的节点具有更相似的表示的方式来优化节点的嵌入。</p>
<h4 id="deepwalk">DeepWalk</h4>
<p>DeepWalk <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 算法主要包含两个部分：一个随机游走序列生成器和一个更新过程。随机游走序列生成器首先在图 <code>$G$</code> 中均匀地随机抽样一个随机游走 <code>$\mathcal{W}_{v_i}$</code> 的根节点 <code>$v_i$</code>，接着从节点的邻居中均匀地随机抽样一个节点直到达到设定的最大长度 <code>$t$</code>。对于一个生成的以 <code>$v_i$</code> 为中心左右窗口为 <code>$w$</code> 的随机游走序列 <code>$v_{i-w}, \dotsc, v_{i-1}, v_i, v_{i+1}, \dotsc, v_{i+m}$</code>，DeepWalk 利用 SkipGram 算法通过最大化以 <code>$v_i$</code> 为中心，左右 <code>$w$</code> 为窗口的同其他节点共现概率来优化模型：</p>
<p><code>$$ \text{Pr} \left(\left\{v_{i-w}, \dotsc, v_{i+w}\right\} \setminus v_i \mid \Phi \left(v_i\right)\right) = \prod_{j=i-w, j \neq i}^{i+w} \text{Pr} \left(v_j \mid \Phi \left(v_i\right)\right) $$</code></p>
<p>DeepWalk 和 Word2Vec 的类比如下表所示：</p>
<table>
  <thead>
      <tr>
          <th>模型</th>
          <th>目标</th>
          <th>输入</th>
          <th>输出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Word2Vec</td>
          <td>词</td>
          <td>句子</td>
          <td>词嵌入</td>
      </tr>
      <tr>
          <td>DeepWalk</td>
          <td>节点</td>
          <td>节点序列</td>
          <td>节点嵌入</td>
      </tr>
  </tbody>
</table>
<h4 id="node2vec">node2vec</h4>
<p>node2vec <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> 通过改变随机游走序列生成的方式进一步扩展了 DeepWalk 算法。DeepWalk 选取随机游走序列中下一个节点的方式是均匀随机分布的，而 node2vec 通过引入两个参数 <code>$p$</code> 和 <code>$q$</code>，将<strong>宽度优先搜索</strong>和<strong>深度优先搜索</strong>引入了随机游走序列的生成过程。 宽度优先搜索注重邻近的节点并刻画了相对局部的一种网络表示， 宽度优先中的节点一般会出现很多次，从而降低刻画中心节点的邻居节点的方差， 深度优先搜索反映了更高层面上的节点之间的同质性。</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/node2vec.png" class="lazyload"/>
  
</figure>
<p>node2vec 中的两个参数 <code>$p$</code> 和 <code>$q$</code> 控制随机游走序列的跳转概率。假设上一步游走的边为 <code>$\left(t, v\right)$</code>， 那么对于节点 <code>$v$</code> 的不同邻居，node2vec 根据 <code>$p$</code> 和 <code>$q$</code> 定义了不同的邻居的跳转概率，<code>$p$</code> 控制跳向上一个节点的邻居的概率，<code>$q$</code> 控制跳向上一个节点的非邻居的概率，具体的未归一的跳转概率值 <code>$\pi_{vx} = \alpha_{pq} \left(t, x\right)$</code> 如下所示：</p>
<p><code>$$ \alpha_{p q}(t, x)=\left\{\begin{array}{cl} \dfrac{1}{p}, &amp; \text { if } d_{t x}=0 \\ 1, &amp; \text { if } d_{t x}=1 \\ \dfrac{1}{q}, &amp; \text { if } d_{t x}=2 \end{array}\right. $$</code></p>
<p>其中，<code>$d_{tx}$</code> 表示节点 <code>$t$</code> 和 <code>$x$</code> 之间的最短距离。为了获得最优的超参数 <code>$p$</code> 和 <code>$q$</code> 的取值，node2vec 通过半监督形式，利用网格搜索最合适的参数学习节点表示。</p>
<h4 id="app">APP</h4>
<p>之前的基于随机游走的图嵌入方法，例如：DeepWalk，node2vec 等，都无法保留图中的非对称信息。然而非对称性在很多问题，例如：社交网络中的链路预测、电商中的推荐等，中至关重要。在有向图和无向图中，非对称性如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/asymmetric-proximity.png" class="lazyload"/>
  
</figure>
<p>为了保留图的非对称性，对于每个节点 <code>$v$</code> 设置两个不同的角色：源和目标，分别用 <code>$\overrightarrow{s_{v}}$</code> 和 <code>$\overrightarrow{t_{v}}$</code> 表示。对于每个从 <code>$u$</code> 开始以 <code>$v$</code> 结尾的采样序列，利用 <code>$(u, v)$</code> 表示采样的节点对。则利用源节点 <code>$u$</code> 预测目标节点 <code>$v$</code> 的概率如下：</p>
<p><code>$$ p(v | u)=\frac{\exp (\overrightarrow{s_{u}} \cdot \overrightarrow{t_{v}})}{\sum_{n \in V} \exp (\overrightarrow{s_{u}} \cdot \overrightarrow{t_{n}})} $$</code></p>
<p>通过 Skip-Gram 和负采样对模型进行优化，损失函数如下：</p>
<p><code>$$ \begin{aligned} \ell &amp;= \log \sigma(\overrightarrow{s_{u}} \cdot \overrightarrow{t_{v}})+k \cdot E_{t_{n} \sim P_{D}}[\log \sigma(-\overrightarrow{s_{u}} \cdot \overrightarrow{t_{n}})] \\ &amp;= \sum_{u} \sum_{v} \# \text {Sampled}_{u}(v) \cdot \left(\log \sigma(\overrightarrow{s_{u}} \cdot \overrightarrow{t_{v}}) + k \cdot E_{t_{n} \sim P_{D}}[\log \sigma(-\overrightarrow{s_{u}} \cdot \overrightarrow{t_{n}})]\right) \end{aligned} $$</code></p>
<p>其中，我们根据分布 <code>$P_D \left(n\right) \sim \dfrac{1}{|V|}$</code> 随机负采样 <code>$k$</code> 个节点对，<code>$\# \text{Sampled}_{u}(v)$</code> 为采样的 <code>$\left(u, v\right)$</code> 对的个数，<code>$\sigma$</code> 为 sigmoid 函数。通常情况下，<code>$\# \text{Sampled}_{u}(v) \neq \# \text{Sampled}_{v}(u)$</code>，即 <code>$\left(u, v\right)$</code> 和 <code>$\left(v, u\right)$</code> 的观测数量是不同的。模型利用 Monte-Carlo End-Point 采样方法 <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 随机的以 <code>$v$</code> 为起点和 <code>$\alpha$</code> 为停止概率采样 <code>$p$</code> 条路径。这种采样方式可以用于估计任意一个节点对之间的 Rooted PageRank <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup> 值，模型利用这个值估计由 <code>$v$</code> 到达 <code>$u$</code> 的概率。</p>
<h3 id="matrix-fractorization">Matrix Fractorization</h3>
<h4 id="grarep">GraRep</h4>
<p>GraRep <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> 提出了一种基于矩阵分解的图嵌入方法。对于一个图 <code>$G$</code>，利用邻接矩阵 <code>$S$</code> 定义图的度矩阵：</p>
<p><code>$$ D_{i j}=\left\{\begin{array}{ll} \sum_{p} S_{i p}, &amp; \text { if } i=j \\ 0, &amp; \text { if } i \neq j \end{array}\right. $$</code></p>
<p>则一阶转移概率矩阵定义如下：</p>
<p><code>$$ A = D^{-1} S $$</code></p>
<p>其中，<code>$A_{i, j}$</code> 表示通过一步由 <code>$v_i$</code> 转移到 <code>$v_j$</code> 的概率。所谓的全局特征包含两个部分：</p>
<ol>
<li>捕获两个节点之间的长距离特征</li>
<li>分别考虑按照不同转移步数的连接</li>
</ol>
<p>下图展示了 <code>$k = 1, 2, 3, 4$</code> 情况下的强（上）弱（下）关系：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/grarep.png" class="lazyload"/>
  
</figure>
<p>利用 Skip-Gram 和 NCE（noise contrastive estimation）方法，对于一个 <code>$k$</code> 阶转移，可以将模型归结到一个矩阵 <code>$Y_{i, j}^k$</code> 的分解问题：</p>
<p><code>$$ Y_{i, j}^{k}=W_{i}^{k} \cdot C_{j}^{k}=\log \left(\frac{A_{i, j}^{k}}{\sum_{t} A_{t, j}^{k}}\right)-\log (\beta) $$</code></p>
<p>其中，<code>$W$</code> 和 <code>$C$</code> 的每一行分别为节点 <code>$w$</code> 和 <code>$c$</code> 的表示，<code>$\beta = \lambda / N$</code>，<code>$\lambda$</code> 为负采样的数量，<code>$N$</code> 为图中边的个数。</p>
<p>之后为了减少噪音，模型将 <code>$Y^k$</code> 中所有的负值替换为 0，通过 SVD（方法详情见参见<a href="/cn/2017/12/evd-svd-and-pca/">之前博客</a>）得到节点的 <code>$d$</code> 维表示：</p>
<p><code>$$ \begin{aligned} X_{i, j}^{k} &amp;= \max \left(Y_{i, j}^{k}, 0\right) \\ X^{k} &amp;= U^{k} \Sigma^{k}\left(V^{k}\right)^{T} \\ X^{k} \approx X_{d}^{k} &amp;= U_{d}^{k} \Sigma_{d}^{k}\left(V_{d}^{k}\right)^{T} \\ X^{k} \approx X_{d}^{k} &amp;= W^{k} C^{k} \\ W^{k} &amp;= U_{d}^{k}\left(\Sigma_{d}^{k}\right)^{\frac{1}{2}} \\ C^{k} &amp;= \left(\Sigma_{d}^{k}\right)^{\frac{1}{2}} V_{d}^{k T} \end{aligned} $$</code></p>
<p>最终，通过对不同 <code>$k$</code> 的表示进行拼接得到节点最终的表示。</p>
<h4 id="hope">HOPE</h4>
<p>HOPE <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup> 对于每个节点最终生成两个嵌入表示：一个是作为源节点的嵌入表示，另一个是作为目标节点的嵌入表示。模型通过近似高阶相似性来保留非对称传递性，其优化目标为：</p>
<p><code>$$ \min \left\|\mathbf{S}-\mathbf{U}^{s} \cdot \mathbf{U}^{t^{\top}}\right\|_{F}^{2} $$</code></p>
<p>其中，<code>$\mathbf{S}$</code> 为相似矩阵，<code>$\mathbf{U}^s$</code> 和 <code>$\mathbf{U}^t$</code> 分别为源节点和目标节点的向量表示。下图展示了嵌入向量可以很好的保留非对称传递性：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/hope.png" class="lazyload"/>
  
</figure>
<p>对于 <code>$\mathbf{S}$</code> 有多种可选近似度量方法：Katz Index，Rooted PageRank（RPR），Common Neighbors（CN），Adamic-Adar（AA）。这些度量方法可以分为两类：全局近似（Katz Index 和 RPR）和局部近似（CN 和 AA）。</p>
<p>算法采用了一个广义 SVD 算法（JDGSVD）来解决使用原始 SVD 算法计算复杂度为<code>$O \left(N^3\right)$</code> 过高的问题，从而使得算法可以应用在更大规模的图上。</p>
<h3 id="meta-paths">Meta Paths</h3>
<h4 id="matapath2vec">matapath2vec</h4>
<p>matapath2vec <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup> 提出了一种基于元路径的异构网络表示学习方法。在此我们引入 3 个定义：</p>
<ol>
<li>**异构网络（(Heterogeneous information network，HIN）**可以定义为一个有向图 <code>$G = \left(V, E\right)$</code>，一个节点类型映射 <code>$\tau: V \to A$</code> 和一个边类型映射 <code>$\phi: E \to R$</code>，其中对于 <code>$v \in V$</code> 有 <code>$\tau \left(v\right) \in A$</code>，<code>$e \in E$</code> 有 <code>$\phi \left(e\right) \in R$</code>，且 <code>$|A| + |R| &gt; 1$</code>。</li>
<li>**网络模式（Network schema）**定义为 <code>$T_G = \left(A, R\right)$</code>，为一个包含节点类型映射 <code>$\tau \left(v\right) \in A$</code> 和边映射 <code>$\phi \left(e\right) \in R$</code> 异构网络的 <code>$G = \left(V, E\right)$</code> 的元模板。</li>
<li>**元路径（Meta-path）**定义为网络模式 <code>$T_G = \left(A, R\right)$</code> 上的一条路径 <code>$P$</code>，形式为 <code>$A_{1} \stackrel{R_{1}}{\longrightarrow} A_{2} \stackrel{R_{2}}{\longrightarrow} \cdots \stackrel{R_{l}}{\longrightarrow} A_{l+1}$</code>。</li>
</ol>
<p>下图展示了一个学术网络和部分元路径：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/metapaths.png" class="lazyload"/>
  
</figure>
<p>其中，APA 表示一篇论文的共同作者，APVPA 表示两个作者在同一个地方发表过论文。</p>
<p>metapath2vec 采用了基于元路径的随机游走来生成采样序列，这样就可以保留原始网络中的语义信息。对于一个给定的元路径模板 <code>$P: A_{1} \stackrel{R_{1}}{\longrightarrow} A_{2} \stackrel{R_{2}}{\longrightarrow} \cdots A_{t} \stackrel{R_{t}}{\longrightarrow} A_{t+1} \cdots \stackrel{R_{l}}{\longrightarrow} A_{l}$</code>，第 <code>$i$</code> 步的转移概率为：</p>
<p><code>$$ p\left(v^{i+1} | v_{t}^{i}, P\right)=\left\{\begin{array}{ll} \dfrac{1}{\left|N_{t+1}\left(v_{t}^{i}\right)\right|} &amp; \left(v_{t}^{i}, v^{i+1}\right) \in E, \phi\left(v^{i+1}\right)=A_{t+1} \\ 0 &amp; \left(v_{t}^{i}, v^{i+1}\right) \in E, \phi\left(v^{i+1}\right) \neq A_{t+1} \\ 0 &amp; \left(v_{t}^{i}, v^{i+1}\right) \notin E \end{array}\right. $$</code></p>
<p>其中，<code>$v^i_t \in A_t$</code>，<code>$N_{t+1} \left(v^i_t\right)$</code> 表示节点 <code>$v^i_t$</code> 类型为 <code>$A_{t+1}$</code> 的邻居。之后，则采用了类似 DeepWalk 的方式进行训练得到节点表示。</p>
<h4 id="hin2vec">HIN2Vec</h4>
<p>HIN2Vec <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup> 提出了一种利用多任务学习通过多种关系进行节点和元路径表示学习的方法。模型最初是希望通过一个多分类模型来预测任意两个节点之间所有可能的关系。假设对于任意两个节点，所有可能的关系集合为 <code>$R = \{\text{P-P, P-A, A-P, P-P-P, P-P-A, P-A-P, A-P-P, A-P-A}\}$</code>。假设一个实例 <code>$P_1$</code> 和 <code>$A_1$</code> 包含两种关系：<code>$\text{P-A}$</code> 和 <code>$\text{P-P-A}$</code>，则对应的训练数据为 <code>$\langle x: P_1, y: A_1, output: \left[0, 1, 0, 0, 1, 0, 0, 0\right] \rangle$</code>。</p>
<p>但实际上，扫描整个网络寻找所有可能的关系是不现实的，因此 HIN2Vec 将问题简化为一个给定两个节点判断之间是否存在一个关系的二分类问题，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/hin2vec.png" class="lazyload"/>
  
</figure>
<p>模型的三个输入分别为节点 <code>$x$</code> 和 <code>$y$</code>，以及关系 <code>$r$</code>。在隐含层输入被转换为向量 <code>$W_{X}^{\prime} \vec{x}, W_{Y}^{\prime} \vec{y}$</code> 和 <code>$f_{01}\left(W_{R}^{\prime} \vec{r}\right)$</code>。需要注意对于关系 <code>$r$</code>，模型应用了一个正则化函数 <code>$f_{01} \left(\cdot\right)$</code> 使得 <code>$r$</code> 的向量介于 <code>$0$</code> 和 <code>$1$</code> 之间。之后采用逐元素相乘对三个向量进行汇总 <code>$W_{X}^{\prime} \vec{x} \odot W_{Y}^{\prime} \vec{y} \odot f_{01}\left(W_{R}^{\prime} \vec{r}\right)$</code>。在最后的输出层，通过计算 <code>$sigmoid \left(\sum W_{X}^{\prime} \vec{x} \odot W_{Y}^{\prime} \vec{y} \odot f_{01}\left(W_{R}^{\prime} \vec{r}\right)\right)$</code> 得到最终的预测值。</p>
<p>在生成训练数据时，HIN2Vec 采用了完全随机游走进行节点采样，而非 metapath2vec 中的按照给定的元路径的方式。通过随机替换 <code>$x, y, r$</code> 中的任何一个可以生成负样本，但当网络中的关系数量较少，节点数量远远大于关系数量时，这种方式很可能产生错误的负样本，因此 HIN2Vec 只随机替换 <code>$x, y$</code>，保持 <code>$r$</code> 不变。</p>
<h3 id="deep-learning">Deep Learning</h3>
<h4 id="sdne">SDNE</h4>
<p>SDNE <sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup> 提出了一种利用自编码器同时优化一阶和二阶相似度的图嵌入算法，学习得到的向量能够保留局部和全局的结构信息。SDNE 使用的网络结构如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/sdne.png" class="lazyload"/>
  
</figure>
<p>对于二阶相似度，自编码器的目标是最小化输入和输出的重构误差。SDNE 采用邻接矩阵作为自编码器的输入，<code>$\mathbf{x}_i = \mathbf{s}_i$</code>，每个 <code>$\mathbf{s}_i$</code> 包含了节点 <code>$v_i$</code> 的邻居结构信息。模型的损失函数如下：</p>
<p><code>$$ \mathcal{L}=\sum_{i=1}^{n}\left\|\hat{\mathbf{x}}_{i}-\mathbf{x}_{i}\right\|_{2}^{2} $$</code></p>
<p>由于网络的稀疏性，邻接矩阵中的非零元素远远少于零元素，因此模型采用了一个带权的损失函数：</p>
<p><code>$$ \begin{aligned} \mathcal{L}_{2nd} &amp;=\sum_{i=1}^{n}\left\|\left(\hat{\mathbf{x}}_{i}-\mathbf{x}_{i}\right) \odot \mathbf{b}_{i}\right\|_{2}^{2} \\ &amp;=\|(\hat{X}-X) \odot B\|_{F}^{2} \end{aligned} $$</code></p>
<p>其中，<code>$\odot$</code> 表示按位乘，<code>$\mathbf{b}_i = \left\{b_{i, j}\right\}_{j=1}^{n}$</code>，如果 <code>$s_{i, j} = 0$</code> 则 <code>$b_{i, j} = 1$</code> 否则 <code>$b_{i, j} = \beta &gt; 1$</code>。</p>
<p>对于一阶相似度，模型利用了一个监督学习模块最小化节点在隐含空间中距离。损失函数如下：</p>
<p><code>$$ \begin{aligned} \mathcal{L}_{1st} &amp;=\sum_{i, j=1}^{n} s_{i, j}\left\|\mathbf{y}_{i}^{(K)}-\mathbf{y}_{j}^{(K)}\right\|_{2}^{2} \\ &amp;=\sum_{i, j=1}^{n} s_{i, j}\left\|\mathbf{y}_{i}-\mathbf{y}_{j}\right\|_{2}^{2} \end{aligned} $$</code></p>
<p>最终，模型联合损失函数如下：</p>
<p><code>$$ \begin{aligned} \mathcal{L}_{mix} &amp;=\mathcal{L}_{2nd}+\alpha \mathcal{L}_{1st}+\nu \mathcal{L}_{reg} \\ &amp;=\|(\hat{X}-X) \odot B\|_{F}^{2}+\alpha \sum_{i, j=1}^{n} s_{i, j}\left\|\mathbf{y}_{i}-\mathbf{y}_{j}\right\|_{2}^{2}+\nu \mathcal{L}_{reg} \end{aligned} $$</code></p>
<p>其中，<code>$\mathcal{L}_{reg}$</code> 为 L2 正则项。</p>
<h4 id="dngr">DNGR</h4>
<p>DNGR <sup id="fnref:13"><a href="#fn:13" class="footnote-ref" role="doc-noteref">13</a></sup> 提出了一种利用基于 Stacked Denoising Autoencoder（SDAE）提取特征的网络表示学习算法。算法的流程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/dngr.png" class="lazyload"/>
  
</figure>
<p>模型首先利用 Random Surfing 得到一个概率共现（PCO）矩阵，之后利用其计算得到 PPMI 矩阵，最后利用 SDAE 进行特征提取得到节点的向量表示。</p>
<p>对于传统的将图结构转换为一个线性序列方法存在几点缺陷：</p>
<ol>
<li>采样序列边缘的节点的上下文信息很难被捕捉。</li>
<li>很难直接确定游走的长度和步数等超参数，尤其是对于大型网络来说。</li>
</ol>
<p>受 PageRank 思想影响，作者采用了 Random Surfing 模型。定义转移矩阵 <code>$A$</code>，引入行向量 <code>$p_k$</code>，第 <code>$j$</code> 个元素表示通过 <code>$k$</code> 步转移之后到达节点 <code>$j$</code> 的概率。<code>$p_0$</code> 为一个初始向量，其仅第 <code>$i$</code> 个元素为 1，其它均为 0。在考虑以 <code>$1 - \alpha$</code> 的概率返回初始节点的情况下有：</p>
<p><code>$$ p_{k}=\alpha \cdot p_{k-1} A+(1-\alpha) p_{0} $$</code></p>
<p>在不考虑返回初始节点的情况下有：</p>
<p><code>$$ p_{k}^{*}=p_{k-1}^{*} A=p_{0} A^{k} $$</code></p>
<p>直观而言，两个节点越近，两者的关系越亲密，因此通过同当前节点的相对距离来衡量上下文节点的重要性是合理的。基于此，第 <code>$i$</code> 个节点的表示可以用如下方式构造：</p>
<p><code>$$ r=\sum_{k=1}^{K} w(k) \cdot p_{k}^{*} $$</code></p>
<p>其中，<code>$w \left(\cdot\right)$</code> 是一个衰减函数。</p>
<p>利用 PCO 计算得到 PPMI 后，再利用一个 SDAE 进行特征提取。Stacking 策略可以通过不同的网络层学习得到不同层级的表示，Denoising 策略则通过去除数据中的噪声，增加结果的鲁棒性。同时，SNGR 相比基于 SVD 的方法效率更高。</p>
<h3 id="others">Others</h3>
<h4 id="line">LINE</h4>
<p>LINE <sup id="fnref:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup> 提出了一个用于大规模网络嵌入的方法，其满足如下 3 个要求：</p>
<ol>
<li>同时保留节点之间的一阶相似性（first-order proximity）和二阶相似性（second-order proximity）。</li>
<li>可以处理大规模网络，例如：百万级别的顶点和十亿级别的边。</li>
<li>可以处理有向，无向和带权的多种类型的图结构。</li>
</ol>
<p>给定一个无向边 <code>$\left(i, j\right)$</code>，点 <code>$v_i$</code> 和 <code>$v_j$</code> 的联合概率如下：</p>
<p><code>$$ p_{1}\left(v_{i}, v_{j}\right)=\frac{1}{1+\exp \left(-\vec{u}_{i}^{T} \cdot \vec{u}_{j}\right)} $$</code></p>
<p>其中，<code>$\vec{u}_{i} \in R^{d}$</code> 为节点 <code>$v_i$</code> 的低维向量表示。在空间 <code>$V \times V$</code> 上，分布 <code>$p \left(\cdot, \cdot\right)$</code> 的经验概率为 <code>$\hat{p}_1 \left(i, j\right) = \dfrac{w_{ij}}{V}$</code>，其中 <code>$W = \sum_{\left(i, j\right) \in E} w_{ij}$</code>。通过最小化两个分布的 KL 散度来优化模型，则目标函数定义如下：</p>
<p><code>$$ O_{1}=-\sum_{(i, j) \in E} w_{i j} \log p_{1}\left(v_{i}, v_{j}\right) $$</code></p>
<p>需要注意的是一阶相似度仅可用于无向图，通过最小化上述目标函数，我们可以将任意顶点映射到一个 <code>$d$</code> 维空间向量。</p>
<p>二阶相似度既可以用于无向图，也可以用于有向图。二阶相似度假设共享大量同其他节点连接的节点之间是相似的，每个节点被视为一个特定的上下文，则在上下文上具有类似分布的节点是相似的。在此，引入两个向量 <code>$\vec{u}_{i}$</code> 和 <code>$\vec{u}_{\prime i}$</code>，其中 <code>$\vec{u}_{i}$</code> 是 <code>$v_i$</code> 做为节点的表示，<code>$\vec{u}_{\prime i}$</code> 是 <code>$v_i$</code> 做为上下文的表示。对于一个有向边 <code>$\left(i, j\right)$</code>，由 <code>$v_i$</code> 生成上下文 <code>$v_j$</code> 的概率为：</p>
<p><code>$$ p_{2}\left(v_{j} | v_{i}\right)=\frac{\exp \left(\vec{u}_{j}^{\prime T} \cdot \vec{u}_{i}\right)}{\sum_{k=1}^{|V|} \exp \left(\vec{u}_{k}^{\prime T} \cdot \vec{u}_{i}\right)} $$</code></p>
<p>其中，<code>$|V|$</code> 为节点或上下文的数量。在此我们引入一个参数 <code>$\lambda_i$</code> 用于表示节点 <code>$v_i$</code> 的重要性程度，重要性程度可以利用度或者 PageRank 算法进行估计。经验分布 <code>$\hat{p}_{2}\left(\cdot \mid v_{i}\right)$</code> 定义为 <code>$\hat{p}_{2}\left(v_{j} \mid v_{i}\right)=\dfrac{w_{i j}}{d_{i}}$</code>，其中 <code>$w_{ij}$</code> 为边 <code>$\left(i, j\right)$</code> 的权重，<code>$d_i$</code> 为节点 <code>$v_i$</code> 的出度。LINE 中采用 <code>$d_i$</code> 作为节点的重要性 <code>$\lambda_i$</code>，利用 KL 散度同时忽略一些常量，目标函数定义如下：</p>
<p><code>$$ O_{2}=-\sum_{(i, j) \in E} w_{i j} \log p_{2}\left(v_{j} \mid v_{i}\right) $$</code></p>
<p>LINE 采用负采样的方式对模型进行优化，同时利用 Alias 方法 <sup id="fnref:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup> <sup id="fnref:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup> 加速采样过程。</p>
<h2 id="图神经网络">图神经网络</h2>
<blockquote>
<p>本节内容主要参考自：<br>
Deep Learning on Graphs: A Survey <sup id="fnref:17"><a href="#fn:17" class="footnote-ref" role="doc-noteref">17</a></sup><br>
A Comprehensive Survey on Graph Neural Networks <sup id="fnref:18"><a href="#fn:18" class="footnote-ref" role="doc-noteref">18</a></sup><br>
Graph Neural Networks: A Review of Methods and Applications <sup id="fnref:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup><br>
Introduction to Graph Neural Networks <sup id="fnref:20"><a href="#fn:20" class="footnote-ref" role="doc-noteref">20</a></sup></p>
</blockquote>
<p>图神经网络（Graph Neural Network，GNN）最早由 Scarselli 等人 <sup id="fnref:21"><a href="#fn:21" class="footnote-ref" role="doc-noteref">21</a></sup> 提出。图中的一个节点可以通过其特征和相关节点进行定义，GNN 的目标是学习一个状态嵌入 <code>$\mathbf{h}_v \in \mathbb{R}^s$</code> 用于表示每个节点的邻居信息。状态嵌入 <code>$\mathbf{h}_v$</code> 可以生成输出向量 <code>$\mathbf{o}_v$</code> 用于作为预测节点标签的分布等。</p>
<p>下面三张图分别从图的类型，训练方法和传播过程角度列举了不同 GNN 的变种 <sup id="fnref1:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup>。</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/gnn-graph-types.png" class="lazyload"/>
  
</figure>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/gnn-training-methods.png" class="lazyload"/>
  
</figure>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/gnn-propagation-steps.png" class="lazyload"/>
  
</figure>
<p>下面我们主要从模型的角度分别介绍不同种类的 GNN。</p>
<h3 id="graph-neural-networks">Graph Neural Networks</h3>
<p>为了根据邻居更新节点的状态，定义一个用于所有节点的函数 <code>$f$</code>，称之为 <em>local transition function</em>。定义一个函数 <code>$g$</code>，用于生成节点的输出，称之为 <em>local output function</em>。有：</p>
<p><code>$$ \begin{array}{c} \mathbf{h}_{v}=f\left(\mathbf{x}_{v}, \mathbf{x}_{co[v]}, \mathbf{h}_{ne[v]}, \mathbf{x}_{ne[v])}\right. \\ \mathbf{o}_{v}=g\left(\mathbf{h}_{v}, \mathbf{x}_{v}\right) \end{array} $$</code></p>
<p>其中，<code>$\mathbf{x}$</code> 表示输入特征，<code>$\mathbf{h}$</code> 表示隐含状态。<code>$co[v]$</code> 为连接到节点 <code>$v$</code> 的边集，<code>$ne[v]$</code> 为节点 <code>$v$</code> 的邻居。</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/graph-example.png" class="lazyload"/>
  
</figure>
<p>上图中，<code>$\mathbf{x}_1$</code> 表示 <code>$l_1$</code> 的输入特征，<code>$co[l_1]$</code> 包含了边 <code>$l_{(1, 4)}, l_{(6, 1)}, l_{(1, 2)}$</code> 和 <code>$l_{(3, 1)}$</code>，<code>$ne[l_1]$</code> 包含了节点 <code>$l_2, k_3, l_4$</code> 和 <code>$l_6$</code>。</p>
<p>令 <code>$\mathbf{H}, \mathbf{O}, \mathbf{X}$</code> 和 <code>$\mathbf{X}_N$</code> 分别表示状态、输出、特征和所有节点特征的向量，有：</p>
<p><code>$$ \begin{aligned} &amp;\mathbf{H}=F(\mathbf{H}, \mathbf{X})\\ &amp;\mathbf{O}=G\left(\mathbf{H}, \mathbf{X}_{N}\right) \end{aligned} $$</code></p>
<p>其中，<code>$F$</code> 为 <em>global transition function</em>，<code>$G$</code> 为 <em>global output function</em>，分别为图中所有节点的 local transition function <code>$f$</code> 和 local output function <code>$g$</code> 的堆叠版本。依据 Banach 的 Fixed Point Theorem <sup id="fnref:22"><a href="#fn:22" class="footnote-ref" role="doc-noteref">22</a></sup>，GNN 利用传统的迭代方式计算状态：</p>
<p><code>$$ \mathbf{H}^{t+1}=F\left(\mathbf{H}^{t}, \mathbf{X}\right) $$</code></p>
<p>其中，<code>$\mathbf{H}^t$</code> 表示第  <code>$t$</code> 论循环 <code>$\mathbf{H}$</code> 的值。</p>
<p>介绍完 GNN 的框架后，下一个问题就是如果学习得到 local transition function <code>$f$</code> 和 local output function <code>$g$</code>。在包含目标信息（<code>$\mathbf{t}_v$</code> 对于特定节点）的监督学习情况下，损失为：</p>
<p><code>$$ loss = \sum_{i=1}^{p} \left(\mathbf{t}_i - \mathbf{o}_i\right) $$</code></p>
<p>其中，<code>$p$</code> 为用于监督学习的节点数量。利用基于梯度下降的学习方法优化模型后，我们可以得到针对特定任务的训练模型和图中节点的隐含状态。</p>
<p>尽管实验结果表明 GNN 是一个用于建模结构数据的强大模型，但对于一般的 GNN 模型仍存在如下缺陷：</p>
<ol>
<li>对于固定点，隐含状态的更新是低效地。</li>
<li>GNN 在每一轮计算中共享参数，而常见的神经网络结构在不同层使用不同的参数。同时，隐含节点状态的更新可以进一步应用 RNN 的思想。</li>
<li>边上的一些信息特征并没有被有效的建模，同时如何学习边的隐含状态也是一个重要问题。</li>
<li>如果我们更关注节点的表示而非图的表示，当迭代轮数 <code>$T$</code> 很大时使用固定点是不合适的。这是因为固定点表示的分布在数值上会更加平滑，从而缺少用于区分不同节点的信息。</li>
</ol>
<h3 id="graph-convolutional-networks">Graph Convolutional Networks</h3>
<p>图卷积神经网络是将用于传统数据（例如：图像）的卷积操作应用到图结构的数据中。核心思想在于学习一个函数 <code>$f$</code>，通过聚合节点 <code>$v_i$</code> 自身的特征 <code>$\mathbf{X}_i$</code> 和邻居的特征 <code>$\mathbf{X}_j$</code> 获得节点的表示，其中 <code>$j \in N\left(v_i\right)$</code> 为节点的邻居。</p>
<p>下图展示了一个用于节点表示学习的 GCN 过程：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/gcn.png" class="lazyload"/>
  
</figure>
<p>GCN 在构建更复杂的图神经网路中扮演了一个核心角色：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/gcn-classification.png" class="lazyload"/>
  <figcaption><p class="figcaption">包含 Pooling 模块用于图分类的 GCN</p></figcaption>
</figure>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/gcn-auto-encoder.png" class="lazyload"/>
  <figcaption><p class="figcaption">包含 GCN 的图自编码器</p></figcaption>
</figure>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/gcn-graph-spatial-temporal-network.png" class="lazyload"/>
  <figcaption><p class="figcaption">包含 GCN 的图时空网络</p></figcaption>
</figure>
<p>GCN 方法可以分为两大类：基于频谱（Spectral Methods）和基于空间（Spatial Methods）的方法。</p>
<h4 id="基于频谱的方法-spectral-methods">基于频谱的方法（Spectral Methods）</h4>
<p>基于频谱的方法将图视为无向图进行处理，图的一种鲁棒的数学表示为标准化的图拉普拉斯矩阵：</p>
<p><code>$$ \mathbf{L}=\mathbf{I}_{\mathbf{n}}-\mathbf{D}^{-\frac{1}{2}} \mathbf{A} \mathbf{D}^{-\frac{1}{2}} $$</code></p>
<p>其中，<code>$\mathbf{A}$</code> 为图的邻接矩阵，<code>$\mathbf{D}$</code> 为节点度的对角矩阵，<code>$\mathbf{D}_{ii} = \sum_{j} \left(\mathbf{A}_{i, j}\right)$</code>。标准化的拉普拉斯矩阵具有实对称半正定的性质，因此可以分解为：</p>
<p><code>$$ \mathbf{L}=\mathbf{U} \mathbf{\Lambda} \mathbf{U}^{T} $$</code></p>
<p>其中，<code>$\mathbf{U}=\left[\mathbf{u}_{\mathbf{0}}, \mathbf{u}_{\mathbf{1}}, \cdots, \mathbf{u}_{\mathbf{n}-\mathbf{1}}\right] \in \mathbf{R}^{N \times N}$</code> 是由 <code>$\mathbf{L}$</code> 的特征向量构成的矩阵，<code>$\mathbf{\Lambda}$</code> 为特征值的对角矩阵，<code>$\mathbf{\Lambda}_{ii} = \lambda_i$</code>。在图信号处理过程中，一个图信号 <code>$\mathbf{x} \in \mathbb{R}^N$</code> 是一个由图的节点构成的特征向量，其中 <code>$\mathbf{x}_i$</code> 表示第 <code>$i$</code> 个节点的值。对于信号 <code>$\mathbf{x}$</code>，图上的傅里叶变换可以定义为：</p>
<p><code>$$ \mathscr{F}(\mathbf{x})=\mathbf{U}^{T} \mathbf{x} $$</code></p>
<p>傅里叶反变换定义为：</p>
<p><code>$$ \mathscr{F}^{-1}(\hat{\mathbf{x}})=\mathbf{U} \hat{\mathbf{x}} $$</code></p>
<p>其中，<code>$\hat{\mathbf{x}}$</code> 为傅里叶变换后的结果。</p>
<p>转变后信号 <code>$\hat{\mathbf{x}}$</code> 的元素为新空间图信号的坐标，因此输入信号可以表示为：</p>
<p><code>$$ \mathbf{x}=\sum_{i} \hat{\mathbf{x}}_{i} \mathbf{u}_{i} $$</code></p>
<p>这正是傅里叶反变换的结果。那么对于输入信号 <code>$\mathbf{x}$</code> 的图卷积可以定义为：</p>
<p><code>$$ \begin{aligned} \mathbf{x} *_{G} \mathbf{g} &amp;=\mathscr{F}^{-1}(\mathscr{F}(\mathbf{x}) \odot \mathscr{F}(\mathbf{g})) \\ &amp;=\mathbf{U}\left(\mathbf{U}^{T} \mathbf{x} \odot \mathbf{U}^{T} \mathbf{g}\right) \end{aligned} $$</code></p>
<p>其中，<code>$\mathbf{g} \in \mathbb{R}^N$</code> 为滤波器，<code>$\odot$</code> 表示逐元素乘。假设定义一个滤波器 <code>$\mathbf{g}_{\theta}=\operatorname{diag}\left(\mathbf{U}^{T} \mathbf{g}\right)$</code>，则图卷积可以简写为：</p>
<p><code>$$ \mathbf{x} *_{G} \mathbf{g}_{\theta}=\mathbf{U} \mathbf{g}_{\theta} \mathbf{U}^{T} \mathbf{x} $$</code></p>
<p>基于频谱的图卷积网络都遵循这样的定义，不同之处在于不同滤波器的选择。</p>
<p>一些代表模型及其聚合和更新方式如下表所示：</p>
<table>
  <thead>
      <tr>
          <th>模型</th>
          <th>聚合方式</th>
          <th>更新方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ChebNet <sup id="fnref:23"><a href="#fn:23" class="footnote-ref" role="doc-noteref">23</a></sup></td>
          <td><code>$\mathbf{N}_{k}=\mathbf{T}_{k}(\tilde{\mathbf{L}}) \mathbf{X}$</code></td>
          <td><code>$\mathbf{H}=\sum_{k=0}^{K} \mathbf{N}_{k} \mathbf{\Theta}_{k}$</code></td>
      </tr>
      <tr>
          <td>1st-order model</td>
          <td><code>$\begin{array}{l} \mathbf{N}_{0}=\mathbf{X} \\ \mathbf{N}_{1}=\mathbf{D}^{-\frac{1}{2}} \mathbf{A} \mathbf{D}^{-\frac{1}{2}} \mathbf{X} \end{array}$</code></td>
          <td><code>$\mathbf{H}=\mathbf{N}_{0} \mathbf{\Theta}_{0}+\mathbf{N}_{1} \mathbf{\Theta}_{1}$</code></td>
      </tr>
      <tr>
          <td>Single parameter</td>
          <td><code>$\mathbf{N}=\left(\mathbf{I}_{N}+\mathbf{D}^{-\frac{1}{2}} \mathbf{A} \mathbf{D}^{-\frac{1}{2}}\right) \mathbf{X}$</code></td>
          <td><code>$\mathbf{H}=\mathbf{N} \mathbf{\Theta}$</code></td>
      </tr>
      <tr>
          <td>GCN <sup id="fnref:24"><a href="#fn:24" class="footnote-ref" role="doc-noteref">24</a></sup></td>
          <td><code>$\mathbf{N}=\tilde{\mathbf{D}}^{-\frac{1}{2}} \tilde{\mathbf{A}} \tilde{\mathbf{D}}^{-\frac{1}{2}} \mathbf{X}$</code></td>
          <td><code>$\mathbf{H}=\mathbf{N} \mathbf{\Theta}$</code></td>
      </tr>
  </tbody>
</table>
<h4 id="基于空间的方法-spatial-methods">基于空间的方法（Spatial Methods）</h4>
<p>基于空间的方法通过节点的空间关系来定义图卷积操作。为了将图像和图关联起来，可以将图像视为一个特殊形式的图，每个像素点表示一个节点，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/spatial-based-gcn.png" class="lazyload"/>
  
</figure>
<p>每个像素同周围的像素相连，以 <code>$3 \times 3$</code> 为窗口，每个节点被 8 个邻居节点所包围。通过对中心节点和周围邻居节点的像素值进行加权平均来应用一个 <code>$3 \times 3$</code> 大小的滤波器。由于邻居节点的特定顺序，可以在不同位置共享权重。同样对于一般的图，基于空间的图卷积通过对中心和邻居节点的聚合得到节点新的表示。</p>
<p>为了使节点可以感知更深和更广的范围，通常的做法是将多个图卷积层堆叠在一起。根据堆叠方式的不同，基于空间的图卷积可以进一步分为两类：基于循环（Recurrent-based）和基于组合（Composition-based）的。基于循环的方法使用相同的图卷积层来更新隐含表示，基于组合的方式使用不同的图卷积层更新隐含表示，两者差异如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/recurrent-based-vs-composition-based.png" class="lazyload"/>
  
</figure>
<p>一些代表模型及其聚合和更新方式如下表所示：</p>
<table>
  <thead>
      <tr>
          <th>模型</th>
          <th>聚合方式</th>
          <th>更新方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Neural FPs <sup id="fnref:25"><a href="#fn:25" class="footnote-ref" role="doc-noteref">25</a></sup></td>
          <td><code>$\mathbf{h}_{\mathcal{N}_{v}}^{t}=\mathbf{h}_{v}^{t-1}+\sum_{k=1}^{\mathcal{N}_{v}} \mathbf{h}_{k}^{t-1}$</code></td>
          <td><code>$\mathbf{h}_{v}^{t}=\sigma\left(\mathbf{h}_{\mathcal{N}_{v}}^{t} \mathbf{W}_{L}^{\mathcal{N}_{v}}\right)$</code></td>
      </tr>
      <tr>
          <td>DCNN <sup id="fnref:26"><a href="#fn:26" class="footnote-ref" role="doc-noteref">26</a></sup></td>
          <td>Node classification:<br/><code>$\mathbf{N}=\mathbf{P}^{*} \mathbf{X}$</code><br/> Graph classification:<br/><code>$\mathbf{N}=1_{N}^{T} \mathbf{P}^{*} \mathbf{X} / N$</code></td>
          <td><code>$\mathbf{H}=f\left(\mathbf{W}^{c} \odot \mathbf{N}\right)$</code></td>
      </tr>
      <tr>
          <td>GraphSAGE <sup id="fnref:27"><a href="#fn:27" class="footnote-ref" role="doc-noteref">27</a></sup></td>
          <td><code>$\mathbf{h}_{\mathcal{N}_{v}}^{t}=\text{AGGREGATE}_{t}\left(\left\{\mathbf{h}_{u}^{t-1}, \forall u \in \mathcal{N}_{v}\right\}\right)$</code></td>
          <td><code>$\mathbf{h}_{v}^{t}=\sigma\left(\mathbf{W}^{t} \cdot\left[\mathbf{h}_{v}^{t-1} \Vert \mathbf{h}_{\mathcal{N}_{v}}^{t}\right]\right)$</code></td>
      </tr>
  </tbody>
</table>
<h3 id="graph-recurrent-networks">Graph Recurrent Networks</h3>
<p>一些研究尝试利用门控机制（例如：GRU 或 LSTM）用于减少之前 GNN 模型在传播过程中的限制，同时改善在图结构中信息的长距离传播。GGNN <sup id="fnref:28"><a href="#fn:28" class="footnote-ref" role="doc-noteref">28</a></sup> 提出了一种使用 GRU 进行传播的方法。它将 RNN 展开至一个固定 <code>$T$</code> 步，然后通过基于时间的传导计算梯度。传播模型的基础循环方式如下：</p>
<p><code>$$ \begin{aligned} &amp;\mathbf{a}_{v}^{t}=\mathbf{A}_{v}^{T}\left[\mathbf{h}_{1}^{t-1} \ldots \mathbf{h}_{N}^{t-1}\right]^{T}+\mathbf{b}\\ &amp;\mathbf{z}_{v}^{t}=\sigma\left(\mathbf{W}^{z} \mathbf{a}_{v}^{t}+\mathbf{U}^{z} \mathbf{h}_{v}^{t-1}\right)\\ &amp;\mathbf{r}_{v}^{t}=\sigma\left(\mathbf{W}^{r} \mathbf{a}_{v}^{t}+\mathbf{U}^{r} \mathbf{h}_{v}^{t-1}\right)\\ &amp;\begin{array}{l} \widetilde{\mathbf{h}}_{v}^{t}=\tanh \left(\mathbf{W} \mathbf{a}_{v}^{t}+\mathbf{U}\left(\mathbf{r}_{v}^{t} \odot \mathbf{h}_{v}^{t-1}\right)\right) \\ \mathbf{h}_{v}^{t}=\left(1-\mathbf{z}_{v}^{t}\right) \odot \mathbf{h}_{v}^{t-1}+\mathbf{z}_{v}^{t} \odot \widetilde{\mathbf{h}}_{v}^{t} \end{array} \end{aligned} $$</code></p>
<p>节点 <code>$v$</code> 首先从邻居汇总信息，其中 <code>$\mathbf{A}_v$</code> 为图邻接矩阵 <code>$\mathbf{A}$</code> 的子矩阵表示节点 <code>$v$</code> 及其邻居的连接。类似 GRU 的更新函数，通过结合其他节点和上一时间的信息更新节点的隐状态。<code>$\mathbf{a}$</code> 用于获取节点 <code>$v$</code> 邻居的信息，<code>$\mathbf{z}$</code> 和 <code>$\mathbf{r}$</code> 分别为更新和重置门。</p>
<p>GGNN 模型设计用于解决序列生成问题，而之前的模型主要关注单个输出，例如：节点级别或图级别的分类问题。研究进一步提出了 Gated Graph Sequence Neural Networks（GGS-NNs），使用多个 GGNN 产生一个输出序列 <code>$\mathbf{o}^{(1)}, \cdots, \mathbf{o}^{(K)}$</code>，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/ggs-nn.png" class="lazyload"/>
  
</figure>
<p>上图中使用了两个 GGNN，<code>$\mathcal{F}_o^{(k)}$</code> 用于从 <code>$\mathcal{\boldsymbol{X}}^{(k)}$</code> 预测 <code>$\mathbf{o}^{(k)}$</code>，<code>$\mathcal{F}_x^{(k)}$</code> 用于从 <code>$\mathcal{\boldsymbol{X}}^{(k)}$</code> 预测 <code>$\mathcal{\boldsymbol{X}}^{(k+1)}$</code>。令 <code>$\mathcal{\boldsymbol{H}}^{(k, t)}$</code> 表示第 <code>$k$</code> 步输出的第 <code>$t$</code> 步传播，<code>$\mathcal{\boldsymbol{H}}^{(k, 1)}$</code> 在任意 <code>$k$</code> 步初始化为 <code>$\mathcal{\boldsymbol{X}}^{(k)}$</code>，<code>$\mathcal{\boldsymbol{H}}^{(t, 1)}$</code> 在任意 <code>$t$</code> 步初始化为 <code>$\mathcal{\boldsymbol{X}}^{(t)}$</code>，<code>$\mathcal{F}_o^{(k)}$</code> 和 <code>$\mathcal{F}_x^{(k)}$</code> 可以为不同模型也可以共享权重。</p>
<p>一些代表模型及其聚合和更新方式如下表所示：</p>
<table>
  <thead>
      <tr>
          <th>模型</th>
          <th>聚合方式</th>
          <th>更新方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GGNN <sup id="fnref1:28"><a href="#fn:28" class="footnote-ref" role="doc-noteref">28</a></sup></td>
          <td><code>$\mathbf{h}_{\mathcal{N}_{v}}^{t}=\sum_{k \in \mathcal{N}_{v}} \mathbf{h}_{k}^{t-1}+\mathbf{b}$</code></td>
          <td><code>$\begin{aligned} &amp;\mathbf{z}_{v}^{t}=\sigma\left(\mathbf{W}^{z} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{U}^{z} \mathbf{h}_{v}^{t-1}\right)\\ &amp;\mathbf{r}_{v}^{t}=\sigma\left(\mathbf{W}^{r} \mathbf{h}_{\mathcal{N}_{v}}^{z}+\mathbf{U}^{r} \mathbf{h}_{v}^{t-1}\right)\\ &amp;\begin{array}{l} \widetilde{\mathbf{h}}_{v}^{t}=\tanh \left(\mathbf{W h}_{\mathcal{N}_{v}}^{t}+\mathbf{U}\left(\mathbf{r}_{v}^{t} \odot \mathbf{h}_{v}^{t-1}\right)\right) \\ \mathbf{h}_{v}^{t}=\left(1-\mathbf{z}_{v}^{t}\right) \odot \mathbf{h}_{v}^{t-1}+\mathbf{z}_{v}^{t} \odot \widetilde{\mathbf{h}}_{v}^{t} \end{array} \end{aligned}$</code></td>
      </tr>
      <tr>
          <td>Tree LSTM (Child sum) <sup id="fnref:29"><a href="#fn:29" class="footnote-ref" role="doc-noteref">29</a></sup></td>
          <td><code>$\mathbf{h}_{\mathcal{N}_{v}}^{t}=\sum_{k \in \mathcal{N}_{v}} \mathbf{h}_{k}^{t-1}$</code></td>
          <td><code>$\begin{aligned} &amp;\mathbf{i}_{v}^{t}=\sigma\left(\mathbf{W}^{i} \mathbf{x}_{v}^{t}+\mathbf{U}^{i} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{b}^{i}\right)\\ &amp;\mathbf{f}_{v k}^{t}=\sigma\left(\mathbf{W}^{f} \mathbf{x}_{v}^{t}+\mathbf{U}^{f} \mathbf{h}_{k}^{t-1}+\mathbf{b}^{f}\right)\\ &amp;\mathbf{o}_{v}^{t}=\sigma\left(\mathbf{W}^{o} \mathbf{x}_{v}^{t}+\mathbf{U}^{o} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{b}^{o}\right)\\ &amp;\mathbf{u}_{v}^{t}=\tanh \left(\mathbf{W}^{u} \mathbf{x}_{v}^{t}+\mathbf{U}^{u} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{b}^{u}\right)\\ &amp;\begin{array}{l} \mathbf{c}_{v}^{t}=\mathbf{i}_{v}^{t} \odot \mathbf{u}_{v}^{t}+\sum_{k \in \mathcal{N}_{v}} \mathbf{f}_{v k}^{t} \odot \mathbf{c}_{k}^{t-1} \\ \mathbf{h}_{v}^{t}=\mathbf{o}_{v}^{t} \odot \tanh \left(\mathbf{c}_{v}^{t}\right) \end{array} \end{aligned}$</code></td>
      </tr>
      <tr>
          <td>Tree LSTM (N-ary) <sup id="fnref1:29"><a href="#fn:29" class="footnote-ref" role="doc-noteref">29</a></sup></td>
          <td><code>$\begin{aligned} &amp;\mathbf{h}_{\mathcal{N}_{v}}^{t i}=\sum_{l=1}^{K} \mathbf{U}_{l}^{i} \mathbf{h}_{v l}^{t-1}\\ &amp;\mathbf{h}_{\mathcal{N}_{v} k}^{t f}=\sum_{l=1}^{K} \mathbf{U}_{k l}^{f} \mathbf{h}_{v l}^{t-1}\\ &amp;\mathbf{h}_{\mathcal{N}_{v}}^{t o}=\sum_{l=1}^{K} \mathbf{U}_{l}^{o} \mathbf{h}_{v l}^{t-1}\\ &amp;\mathbf{h}_{\mathcal{N}_{v}}^{t u}=\sum_{l=1}^{K} \mathbf{U}_{l}^{u} \mathbf{h}_{v l}^{t-1} \end{aligned}$</code></td>
          <td><code>$\begin{aligned} &amp;\mathbf{i}_{v}^{t}=\sigma\left(\mathbf{W}^{i} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v},}^{t i}+\mathbf{b}^{i}\right)\\ &amp;\mathbf{f}_{v k}^{t}=\sigma\left(\mathbf{W}^{f} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v} k}^{f f}+\mathbf{b}^{f}\right)\\ &amp;\mathbf{o}_{v}^{t}=\sigma\left(\mathbf{W}^{o} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t o}+\mathbf{b}^{o}\right)\\ &amp;\mathbf{u}_{v}^{t}=\tanh \left(\mathbf{W}^{u} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t u}+\mathbf{b}^{u}\right)\\ &amp;\mathbf{c}_{v}^{t}=\mathbf{i}_{v}^{t} \odot \mathbf{u}_{v}^{t}+\sum_{l=1}^{K} \mathbf{f}_{v l}^{t} \odot \mathbf{c}_{v l}^{t-1}\\ &amp;\mathbf{h}_{v}^{t}=\mathbf{o}_{v}^{t} \odot \tanh \left(\mathbf{c}_{v}^{t}\right) \end{aligned}$</code></td>
      </tr>
      <tr>
          <td>Graph LSTM <sup id="fnref:30"><a href="#fn:30" class="footnote-ref" role="doc-noteref">30</a></sup></td>
          <td><code>$\begin{aligned} \mathbf{h}_{\mathcal{N}_{v}}^{t i}=\sum_{k \in \mathcal{N}_{v}} \mathbf{U}_{m(v, k)}^{i} \mathbf{h}_{k}^{t-1} \\ \mathbf{h}_{\mathcal{N}_{v}}^{t o}=\sum_{k \in \mathcal{N}_{v}} \mathbf{U}_{m(v, k)}^{o} \mathbf{h}_{k}^{t-1} \\ \mathbf{h}_{\mathcal{N}_{v}}^{t u}=\sum_{k \in \mathcal{N}_{v}} \mathbf{U}_{m(v, k)}^{u} \mathbf{h}_{k}^{t-1} \end{aligned}$</code></td>
          <td><code>$\begin{aligned} &amp;\mathbf{i}_{v}^{t}=\sigma\left(\mathbf{W}^{i} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t i}+\mathbf{b}^{i}\right)\\ &amp;\mathbf{f}_{v k}^{t}=\sigma\left(\mathbf{W}^{f} \mathbf{x}_{v}^{t}+\mathbf{U}_{m(v, k)}^{f} \mathbf{h}_{k}^{t-1}+\mathbf{b}^{f}\right)\\ &amp;\mathbf{o}_{v}^{t}=\sigma\left(\mathbf{W}^{o} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t o}+\mathbf{b}^{o}\right)\\ &amp;\mathbf{u}_{v}^{t}=\tanh \left(\mathbf{W}^{u} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t u}+\mathbf{b}^{u}\right)\\ &amp;\begin{array}{l} \mathbf{c}_{v}^{t}=\mathbf{i}_{v}^{t} \odot \mathbf{u}_{v}^{t}+\sum_{k \in \mathcal{N}_{v}} \mathbf{f}_{v k}^{t} \odot \mathbf{c}_{k}^{t-1} \\ \mathbf{h}_{v}^{t}=\mathbf{o}_{v}^{t} \odot \tanh \left(\mathbf{c}_{v}^{t}\right) \end{array} \end{aligned}$</code></td>
      </tr>
  </tbody>
</table>
<h3 id="graph-attention-networks">Graph Attention Networks</h3>
<p>与 GCN 对于节点所有的邻居平等对待相比，注意力机制可以为每个邻居分配不同的注意力评分，从而识别更重要的邻居。</p>
<p>GAT <sup id="fnref:31"><a href="#fn:31" class="footnote-ref" role="doc-noteref">31</a></sup> 将注意力机制引入传播过程，其遵循自注意力机制，通过对每个节点邻居的不同关注更新隐含状态。GAT 定义了一个图注意力层（<em>graph attentional layer</em>），通过堆叠构建图注意力网络。对于节点对 <code>$\left(i, j\right)$</code>，基于注意力机制的系数计算方式如下：</p>
<p><code>$$ \alpha_{i j}=\frac{\exp \left(\text { LeakyReLU }\left(\overrightarrow{\mathbf{a}}^{T}\left[\mathbf{W} \vec{h}_{i} \| \mathbf{W} \vec{h}_{j}\right]\right)\right)}{\sum_{k \in N_{i}} \exp \left(\text { LeakyReLU }\left(\overrightarrow{\mathbf{a}}^{T}\left[\mathbf{W} \vec{h}_{i} \| \mathbf{W} \vec{h}_{k}\right]\right)\right)} $$</code></p>
<p>其中，<code>$\alpha_{i j}$</code> 表示节点 <code>$j$</code> 对 <code>$i$</code> 的注意力系数，<code>$N_i$</code> 表示节点 <code>$i$</code> 的邻居。令 <code>$\mathbf{h}=\left\{\vec{h}_{1}, \vec{h}_{2}, \ldots, \vec{h}_{N}\right\}, \vec{h}_{i} \in \mathbb{R}^{F}$</code> 表示输入节点特征，其中 <code>$N$</code> 为节点的数量，<code>$F$</code> 为特征维度，则节点的输出特征（可能为不同维度 <code>$F^{\prime}$</code>）为 <code>$\mathbf{h}^{\prime}=\left\{\vec{h}_{1}^{\prime}, \vec{h}_{2}^{\prime}, \ldots, \vec{h}_{N}^{\prime}\right\}, \vec{h}_{i}^{\prime} \in \mathbb{R}^{F^{\prime}}$</code>。<code>$\mathbf{W} \in \mathbb{R}^{F^{\prime} \times F}$</code> 为所有节点共享的线性变换的权重矩阵，<code>$a: \mathbb{R}^{F^{\prime}} \times \mathbb{R}^{F^{\prime}} \rightarrow \mathbb{R}$</code> 用于计算注意力系数。最后的输出特征计算方式如下：</p>
<p><code>$$ \vec{h}_{i}^{\prime}=\sigma\left(\sum_{j \in \mathcal{N}_{i}} \alpha_{i j} \mathbf{W} \vec{h}_{j}\right) $$</code></p>
<p>注意力层采用多头注意力机制来稳定学习过程，之后应用 <code>$K$</code> 个独立的注意力机制计算隐含状态，最后通过拼接或平均得到输出表示：</p>
<p><code>$$ \vec{h}_{i}^{\prime}=\Vert_{k=1}^{K} \sigma\left(\sum_{j \in \mathcal{N}_{i}} \alpha_{i j}^{k} \mathbf{W}^{k} \vec{h}_{j}\right) $$</code></p>
<p><code>$$ \vec{h}_{i}^{\prime}=\sigma\left(\frac{1}{K} \sum_{k=1}^{K} \sum_{j \in \mathcal{N}_{i}} \alpha_{i j}^{k} \mathbf{W}^{k} \vec{h}_{j}\right) $$</code></p>
<p>其中，<code>$\Vert$</code> 表示连接操作，<code>$\alpha_{ij}^k$</code> 表示第 <code>$k$</code> 个注意力机制计算得到的标准化的注意力系数。整个模型如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/gat.png" class="lazyload"/>
  
</figure>
<p>GAT 中的注意力架构有如下几个特点：</p>
<ol>
<li>针对节点对的计算是并行的，因此计算过程是高效的。</li>
<li>可以处理不同度的节点并对邻居分配对应的权重。</li>
<li>可以容易地应用到归纳学习问题中去。</li>
</ol>
<h3 id="应用">应用</h3>
<p>图神经网络已经被应用在监督、半监督、无监督和强化学习等多个领域。下图列举了 GNN 在不同领域内相关问题中的应用，具体模型论文请参考 Graph Neural Networks: A Review of Methods and Applications 原文 <sup id="fnref2:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup>。</p>
<figure>
  <img data-src="/images/cn/2020-04-11-graph-embedding-and-gnn/applications.png" class="lazyload"/>
  
</figure>
<h2 id="开放资源">开放资源</h2>
<h3 id="开源实现">开源实现</h3>
<table>
  <thead>
      <tr>
          <th>项目</th>
          <th>框架</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://github.com/rusty1s/pytorch_geometric">rusty1s/pytorch_geometric</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/dmlc/dgl">dmlc/dgl</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i>, <i class="icon icon-tensorflow">TF</i> &amp; <i class="icon icon-mxnet">MXNet</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/alibaba/euler">alibaba/euler</a></td>
          <td><i class="icon icon-tensorflow">TF</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/alibaba/graph-learn">alibaba/graph-learn</a></td>
          <td><i class="icon icon-tensorflow">TF</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/deepmind/graph_nets">deepmind/graph_nets</a></td>
          <td><i class="icon icon-tensorflow">TF</i> &amp; <i class="icon icon-sonnet">Sonnet</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/facebookresearch/PyTorch-BigGraph">facebookresearch/PyTorch-BigGraph</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/tencent/plato">tencent/plato</a></td>
          <td></td>
      </tr>
      <tr>
          <td><a href="https://github.com/PaddlePaddle/PGL">PaddlePaddle/PGL</a></td>
          <td><i class="icon icon-paddlepaddle"></i> PaddlePaddle</td>
      </tr>
      <tr>
          <td><a href="https://github.com/Accenture/AmpliGraph">Accenture/AmpliGraph</a></td>
          <td><i class="icon icon-tensorflow">TF</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/danielegrattarola/spektral">danielegrattarola/spektral</a></td>
          <td><i class="icon icon-tensorflow">TF</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/THUDM/cogdl/">THUDM/cogdl</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
      </tr>
      <tr>
          <td><a href="https://github.com/DeepGraphLearning/graphvite">DeepGraphLearning/graphvite</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
      </tr>
  </tbody>
</table>
<h3 id="论文列表和评测">论文列表和评测</h3>
<ul>
<li><a href="https://github.com/thunlp/NRLPapers">Must-read papers on network representation learning (NRL) / network embedding (NE)</a></li>
<li><a href="https://github.com/thunlp/GNNPapers">Must-read papers on graph neural networks (GNN)</a></li>
<li><a href="https://github.com/DeepGraphLearning/LiteratureDL4Graph">DeepGraphLearning/LiteratureDL4Graph</a></li>
<li><a href="https://github.com/nnzhan/Awesome-Graph-Neural-Networks">nnzhan/Awesome-Graph-Neural-Networks</a></li>
<li><a href="https://github.com/graphdeeplearning/benchmarking-gnns">graphdeeplearning/benchmarking-gnns</a></li>
<li><a href="https://ogb.stanford.edu/">Open Graph Benchmark</a></li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Cai, H., Zheng, V. W., &amp; Chang, K. C. C. (2018). A comprehensive survey of graph embedding: Problems, techniques, and applications. <em>IEEE Transactions on Knowledge and Data Engineering</em>, 30(9), 1616-1637.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Goyal, P., &amp; Ferrara, E. (2018). Graph embedding techniques, applications, and performance: A survey. <em>Knowledge-Based Systems</em>, 151, 78-94.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Hamilton, W. L., Ying, R., &amp; Leskovec, J. (2017). Representation learning on graphs: Methods and applications. <em>arXiv preprint arXiv:1709.05584</em>.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Perozzi, B., Al-Rfou, R., &amp; Skiena, S. (2014). Deepwalk: Online learning of social representations. In <em>Proceedings of the 20th ACM SIGKDD international conference on Knowledge discovery and data mining</em> (pp. 701-710).&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Grover, A., &amp; Leskovec, J. (2016). node2vec: Scalable feature learning for networks. In <em>Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining</em> (pp. 855-864).&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Fogaras, D., Rácz, B., Csalogány, K., &amp; Sarlós, T. (2005). Towards scaling fully personalized pagerank: Algorithms, lower bounds, and experiments. <em>Internet Mathematics</em>, 2(3), 333-358.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Haveliwala, T. H. (2002). Topic-sensitive PageRank. In <em>Proceedings of the 11th international conference on World Wide Web</em> (pp. 517-526).&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Cao, S., Lu, W., &amp; Xu, Q. (2015). Grarep: Learning graph representations with global structural information. In <em>Proceedings of the 24th ACM international on conference on information and knowledge management</em> (pp. 891-900).&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Ou, M., Cui, P., Pei, J., Zhang, Z., &amp; Zhu, W. (2016). Asymmetric transitivity preserving graph embedding. In <em>Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining</em> (pp. 1105-1114).&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Dong, Y., Chawla, N. V., &amp; Swami, A. (2017). metapath2vec: Scalable representation learning for heterogeneous networks. In <em>Proceedings of the 23rd ACM SIGKDD international conference on knowledge discovery and data mining</em> (pp. 135-144).&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Fu, T. Y., Lee, W. C., &amp; Lei, Z. (2017). Hin2vec: Explore meta-paths in heterogeneous information networks for representation learning. In <em>Proceedings of the 2017 ACM on Conference on Information and Knowledge Management</em> (pp. 1797-1806).&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p>Wang, D., Cui, P., &amp; Zhu, W. (2016). Structural deep network embedding. In <em>Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining</em> (pp. 1225-1234).&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:13">
<p>Cao, S., Lu, W., &amp; Xu, Q. (2016). Deep neural networks for learning graph representations. In <em>Thirtieth AAAI conference on artificial intelligence</em>.&#160;<a href="#fnref:13" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:14">
<p>Tang, J., Qu, M., Wang, M., Zhang, M., Yan, J., &amp; Mei, Q. (2015). Line: Large-scale information network embedding. In <em>Proceedings of the 24th international conference on world wide web</em> (pp. 1067-1077).&#160;<a href="#fnref:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:15">
<p>Walker, A. J. (1974). New fast method for generating discrete random numbers with arbitrary frequency distributions. <em>Electronics Letters</em>, 10(8), 127-128.&#160;<a href="#fnref:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:16">
<p>Walker, A. J. (1977). An efficient method for generating discrete random variables with general distributions. <em>ACM Transactions on Mathematical Software (TOMS)</em>, 3(3), 253-256.&#160;<a href="#fnref:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:17">
<p>Zhang, Z., Cui, P., &amp; Zhu, W. (2020). Deep learning on graphs: A survey. <em>IEEE Transactions on Knowledge and Data Engineering</em>.&#160;<a href="#fnref:17" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:18">
<p>Wu, Z., Pan, S., Chen, F., Long, G., Zhang, C., &amp; Philip, S. Y. (2020). A comprehensive survey on graph neural networks. <em>IEEE Transactions on Neural Networks and Learning Systems</em>.&#160;<a href="#fnref:18" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:19">
<p>Zhou, J., Cui, G., Zhang, Z., Yang, C., Liu, Z., Wang, L., &hellip; &amp; Sun, M. (2018). Graph neural networks: A review of methods and applications. <em>arXiv preprint arXiv:1812.08434</em>.&#160;<a href="#fnref:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref2:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:20">
<p>Liu, Z., &amp; Zhou, J. (2020). Introduction to Graph Neural Networks. <em>Synthesis Lectures on Artificial Intelligence and Machine Learning</em>, 14(2), 1–127.&#160;<a href="#fnref:20" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:21">
<p>Scarselli, F., Gori, M., Tsoi, A. C., Hagenbuchner, M., &amp; Monfardini, G. (2008). The graph neural network model. <em>IEEE Transactions on Neural Networks</em>, 20(1), 61-80.&#160;<a href="#fnref:21" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:22">
<p>Khamsi, M. A., &amp; Kirk, W. A. (2011). <em>An introduction to metric spaces and fixed point theory</em> (Vol. 53). John Wiley &amp; Sons.&#160;<a href="#fnref:22" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:23">
<p>Defferrard, M., Bresson, X., &amp; Vandergheynst, P. (2016). Convolutional neural networks on graphs with fast localized spectral filtering. In <em>Advances in neural information processing systems</em> (pp. 3844-3852).&#160;<a href="#fnref:23" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:24">
<p>Kipf, T. N., &amp; Welling, M. (2016). Semi-supervised classification with graph convolutional networks. <em>arXiv preprint arXiv:1609.02907</em>.&#160;<a href="#fnref:24" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:25">
<p>Duvenaud, D. K., Maclaurin, D., Iparraguirre, J., Bombarell, R., Hirzel, T., Aspuru-Guzik, A., &amp; Adams, R. P. (2015). Convolutional networks on graphs for learning molecular fingerprints. In <em>Advances in neural information processing systems</em> (pp. 2224-2232).&#160;<a href="#fnref:25" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:26">
<p>Atwood, J., &amp; Towsley, D. (2016). Diffusion-convolutional neural networks. In <em>Advances in neural information processing systems</em> (pp. 1993-2001).&#160;<a href="#fnref:26" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:27">
<p>Hamilton, W., Ying, Z., &amp; Leskovec, J. (2017). Inductive representation learning on large graphs. In <em>Advances in neural information processing systems</em> (pp. 1024-1034).&#160;<a href="#fnref:27" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:28">
<p>Li, Y., Tarlow, D., Brockschmidt, M., &amp; Zemel, R. (2015). Gated graph sequence neural networks. <em>arXiv preprint arXiv:1511.05493.</em>&#160;<a href="#fnref:28" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:28" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:29">
<p>Tai, K. S., Socher, R., &amp; Manning, C. D. (2015). Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks. In <em>Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing</em> (Volume 1: Long Papers) (pp. 1556-1566).&#160;<a href="#fnref:29" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:29" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:30">
<p>Peng, N., Poon, H., Quirk, C., Toutanova, K., &amp; Yih, W. T. (2017). Cross-sentence n-ary relation extraction with graph lstms. <em>Transactions of the Association for Computational Linguistics</em>, 5, 101-115.&#160;<a href="#fnref:30" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:31">
<p>Veličković, P., Cucurull, G., Casanova, A., Romero, A., Lio, P., &amp; Bengio, Y. (2017). Graph attention networks. <em>arXiv preprint arXiv:1710.10903.</em>&#160;<a href="#fnref:31" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>预训练自然语言模型 (Pre-trained Models for NLP)</title><link>https://zeqiang.fun/cn/2020/03/pre-trained-model-for-nlp/</link><pubDate>Sat, 28 Mar 2020 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2020/03/pre-trained-model-for-nlp/</guid><description><![CDATA[
        <blockquote>
<p>本文为 Pre-trained Models for Natural Language Processing: A Survey 和相关模型的读书笔记 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。</p>
</blockquote>
<p>在当下的 NLP 研究领域，随着计算机算力的不断增强，越来越多的通用语言表征的预训练模型（Pre-trained Models，PTMs）逐渐涌现出来。这对下游的 NLP 任务非常有帮助，可以避免大量从零开始训练新的模型。PTM 大致可以分为两代：</p>
<ul>
<li>第一代 PTM 旨在学习词嵌入。由于下游任务不在需要这些模型，因此为了计算效率，这些模型往往采用浅层模型，例如 Skip-Gram <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>，GloVe <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 等。尽管这些模型可以捕获词的语义，但由于未基于上下文环境，因此不能够捕捉到更深层次的概念，例如：句法结构，语义角色，指代等等。</li>
<li>第二代 PTM 专注于学习基于上下文的词嵌入，例如 CoVe <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>，ELMo <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>，OpenAI GPT <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 和 BERT <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup> 等。这些学习到的编码器在下游任务中仍会用于词在上下文中的语义表示。</li>
</ul>
<h2 id="预训练原理">预训练原理</h2>
<h3 id="语言表示学习">语言表示学习</h3>
<p>分布式表示的核心思想为用一个低维的实值向量表示一段文本，向量单独每个维度不具有任何实质含义，但整个向量表示了一个具体的概念。下图展示了一个 NLP 任务的一般神经网络架构：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/generic-neural-architecture-for-nlp.png" class="lazyload"/>
  <figcaption><p class="figcaption">NLP 任务的一般神经网络架构</p></figcaption>
</figure>
<p>词嵌入包含两种类型：<strong>上下文无关的词嵌入</strong>和<strong>基于上下文的词嵌入</strong>。两者的不同点在于一个词的嵌入是够会随着上下文的不同而随之改变。</p>
<ul>
<li>上下文无关的词嵌入</li>
</ul>
<p>为了表征语义，我们需要将离散的语言符号映射到一个分布式嵌入空间中。对于词典 <code>$\mathcal{V}$</code> 中的一个词 <code>$x$</code>，我们将其映射为查询表 <code>$\mathbf{E} \in \mathbb{R}^{D_e \times \|\mathcal{V}\|}$</code> 中的一个向量 <code>$\mathbf{e}_x \in \mathbb{R}^{D_e}$</code>，其中 <code>$D_e$</code> 为嵌入的维度。</p>
<p>这种类型的嵌入主要有两个缺陷：一是嵌入是静态的，词在不同的上下文中的嵌入表示是相同的，因此无法处理一词多义；二是未登录词（out-of-vocabulary，OOV）问题，通常可以采用字符级嵌入表示解决该问题。更多上下文无关的词嵌入模型，请参见之前的博客 <a href="/cn/2018/10/word-embeddings/">词向量</a>。</p>
<ul>
<li>基于上下文的词嵌入</li>
</ul>
<p>为了解决上述问题，我们需要区分在不同上下文下词的含义。给定一段文本 <code>$x_1, x_2, \dotsc, x_T$</code> 其中每段标记 <code>$x_t \in \mathcal{V}$</code> 为一个词或子词，<code>$x_t$</code> 基于上下文的表示依赖于整段文本。</p>
<p><code>$$ \left[\mathbf{h}_1, \mathbf{h}_2, \dotsc, \mathbf{h}_T\right] = f_{\text{enc}} \left(x_1, x_2, \dotsc, x_T\right) $$</code></p>
<p>其中，<code>$f_{\text{enc}} \left(\cdot\right)$</code> 为神经编码器，<code>$\mathbf{h}_t$</code> 为标记 <code>$x_t$</code> 的<strong>基于上下文的嵌入</strong>或<strong>动态嵌入</strong>。</p>
<h3 id="神经上下文编码器">神经上下文编码器</h3>
<p>神经上下文编码器大致可以分为 3 类：</p>
<ol>
<li><strong>基于卷积的模型</strong>：基于卷积的模型通过卷积操作从一个词的邻居中聚合局部信息来捕获这个词的含义 <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup>。
<figure>
    <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/convolutional-model.png" class="lazyload"/>
    <figcaption><p class="figcaption">Convolutional model</p></figcaption>
  </figure></li>
<li><strong>基于序列的模型</strong>：基于序列的模型采用 RNNs（LSTM <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup> 和 GRU <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup>） 来捕获词的上下文信息。实际中，我们采用双向的 RNNs 从词的两端收集信息，不过整体效果容易收到长期依赖问题的影响。
<figure>
    <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/sequential-model.png" class="lazyload"/>
    <figcaption><p class="figcaption">Sequential model</p></figcaption>
  </figure></li>
<li><strong>基于图的模型</strong>：基于图的模型将字作为图中的一个节点来学习上下文表示，这个图通常是一个词之间预定义的语言结构，例如：语法结构 <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup> <sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup> 或语义关系 <sup id="fnref:13"><a href="#fn:13" class="footnote-ref" role="doc-noteref">13</a></sup>。尽管基于语言学的图结构能提供有用的信息，但如何构建一个好的图结构则成为了难题。除此之外，基于语言学的图结构需要依赖专家知识和外部工具，例如：依存句法分析等。事实上，我们会采用一个更直接的方式去学习任意两个词之间的关系，通常连接的权重可以通过自注意力机制自动计算得出。Transformer <sup id="fnref:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup> 是一个采用了全链接自注意力架构的实现，同时也采用了位置嵌入（positional embedding），层标准化（layer normalization）和残差连接（residual connections）等网络设计理念。
<figure>
    <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/fully-connected-graph-based-model.png" class="lazyload"/>
    <figcaption><p class="figcaption">Fully-connected graph-based model</p></figcaption>
  </figure></li>
</ol>
<h3 id="为什么预训练">为什么预训练</h3>
<p>对于大多数的 NLP 任务，构建一个大规模的有标签的数据集是一项很大的挑战。相反，大规模的无标签语料是相对容易构建的，为了充分利用这些无标签数据，我们可以先利用它们获取一个好的语言表示，再将这些表示用于其他任务。预训练的好处如下：</p>
<ol>
<li>预训练可以从大规模语料中学习得到通用的语言表示，并用于下游任务。</li>
<li>预训练提供了更优的模型初始化方法，有助于提高模型的泛化能力和加速模型收敛。</li>
<li>预训练可以当作是在小数据集上一种避免过拟合的正则化方法。</li>
</ol>
<h3 id="预训练任务">预训练任务</h3>
<p>预训练任务对于学习语言的通用表示来说至关重要。通常情况下，预训练任务具有挑战性，同时需要大量训练数据。我们将预训练任务划分为 3 类：</p>
<ol>
<li><strong>监督学习</strong>，即从包含输入输出对的训练数据中学习一个由输入到输出的映射函数。</li>
<li><strong>非监督学习</strong>，即从无标签数据获取一些固有的知识，例如：聚类，密度，潜在表征等。</li>
<li><strong>自监督学习</strong>，是监督学习和非监督学习的混合体，核心思想是对于输入的一部分利用其他部分进行预测。</li>
</ol>
<h4 id="语言模型-language-modeling-lm">语言模型（Language Modeling，LM）</h4>
<p>NLP 中最常见的非监督任务为概率语言建模，这是一个经典的概率密度估计问题。给定一个文本序列 <code>$x_{1:T} = \left[x_1, x_2, \dotsc, x_T\right]$</code>，他的联合概率 <code>$p \left(x_{1:T}\right)$</code> 可以分解为：</p>
<p><code>$$ p \left(x_{1:T}\right) = \prod_{t=1}^{y}{p \left(x_t \mid x_{0:t-1}\right)} $$</code></p>
<p>其中 <code>$x_0$</code> 为序列开始的特殊标记。条件概率 <code>$p \left(x_t \mid x_{0:t-1}\right)$</code> 可以通过给定的语言上下文 <code>$x_{0:t-1}$</code> 词的概率分布进行建模估计。上下文 <code>$x_{0:t-1}$</code> 可以通过神经编码器 <code>$f_{\text{enc}} \left(\cdot\right)$</code> 进行建模，则条件概率可以表示为：</p>
<p><code>$$ p \left(x_t | x_{0:t-1}\right) = g_{\text{LM}} \left(f_{\text{enc}} \left(x_{0:t-1}\right)\right) $$</code></p>
<p>其中，<code>$g_{\text{LM}}$</code> 为预测层。</p>
<h4 id="遮罩语言模型-masked-language-modeling-mlm">遮罩语言模型（Masked Language Modeling，MLM）</h4>
<p>大致上来说，MLM 首先将输入句子的一些词条进行遮挡处理，其次再训练模型利用剩余的部分预测遮挡的部分。这种预训练方法会导致在预训练（pre-training）阶段和微调（fine-tuning）阶段的不一致，因为在微调阶段遮挡标记并未出现，BERT <sup id="fnref:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup> 通过一个特殊的符号 <code>[MASK]</code> 对其进行处理。</p>
<h5 id="sequence-to-sequence-mlm-seq2seq-mlm">Sequence-to-Sequence MLM (Seq2Seq MLM)</h5>
<p>MLM 通常以一个分类问题进行求解，我们将遮挡后的序列输入到一个神经编码器，再将输出向量传给一个 Softmax 分类器来预测遮挡的字符。我们可以采用 Encoder-Decoder（Seq2Seq）网络结构，将遮挡的序列输入到 Encoder，Decoder 则会循序的产生被遮挡的字符。MASS <sup id="fnref:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup> 和 T5 <sup id="fnref:17"><a href="#fn:17" class="footnote-ref" role="doc-noteref">17</a></sup> 均采用了这种序列到序列的 MLM 结构，这种结构对 Seq2Seq 风格的下游任务很有帮助，例如：问答，摘要和机器翻译。</p>
<h5 id="enhanced-masked-language-modeling-e-mlm">Enhanced Masked Language Modeling (E-MLM)</h5>
<p>同时，大量研究对于 BERT 所使用的遮罩处理进行了改进。RoBERTa <sup id="fnref:18"><a href="#fn:18" class="footnote-ref" role="doc-noteref">18</a></sup> 采用了一种动态的遮罩处理。UniLM 将遮罩任务拓展到 3 种不同的类型：单向的，双向的和 Seq2Seq 类型的。</p>
<h4 id="排列语言模型-permuted-language-modeling-plm">排列语言模型（Permuted Language Modeling，PLM）</h4>
<p>在 MLM 中一些特殊字符（例如：<code>[MASK]</code>）在下游任务中是无用的，为了解决这个问题，XLNet <sup id="fnref:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup> 提出了一种排列语言模型（Permuted Language Modeling，PLM）用于替代 MLM。简言之，PLM 是对输入序列的排列进行语言建模。给定一个序列，从所有可能的排列中随机抽样得到一个排列，将排列后的序列中的一些字符作为模型的预测目标，利用其他部分和目标的自然位置进行训练。需要注意的是这种排列并不会影响序列的自然位置，其仅用于定义字符预测的顺序。</p>
<h4 id="去噪自编码-denoising-autoencoder-dae">去噪自编码（Denoising Autoencoder，DAE）</h4>
<p>DAE 旨在利用部分有损的输入恢复原始无损的输入。对于语言模型，例如 Seq2Seq 模型，可以采用标准的 Transformer 来重构原始文本。有多种方式可以对文本进行破坏 <sup id="fnref:20"><a href="#fn:20" class="footnote-ref" role="doc-noteref">20</a></sup>：</p>
<ol>
<li>字符遮罩：随机采样字符并将其替换为 <code>[MASK]</code>。</li>
<li>字符删除：随机的从输入中删除字符，不同于字符遮罩，模型需要确定丢失字符的位置。</li>
<li>文本填充：采样一段文本并将其替换为一个 <code>[MASK]</code>，每段文本的长度服从泊松分布（$\lambda = 3$），模型需要确定这段文本中缺失的字符个数。</li>
<li>句子重排：将文档以终止标点进行分割，再进行随机排序。</li>
<li>文档旋转：随机均匀地选择一个字符，对文档进行旋转使得这个字符作为文档的起始字符，模型需要确定文档真实的起始位置。</li>
</ol>
<h4 id="对比学习-contrastive-learning-ctl">对比学习（Contrastive Learning，CTL）</h4>
<p>对比学习 <sup id="fnref:21"><a href="#fn:21" class="footnote-ref" role="doc-noteref">21</a></sup> 假设一些观测到的文本对比随机采样的文本具有更相似的语义。对于文本对 <code>$\left(x, y\right)$</code> 通过最小化如下目标函数来学习评分函数 <code>$s \left(x, y\right)$</code>：</p>
<p><code>$$ \mathbb{E}_{x, y^+, y^-} \left[- \log \dfrac{\exp \left(s \left(x, y^+\right)\right)}{\exp \left(s \left(x, y^+\right)\right) + \exp \left(s \left(x, y^-\right)\right)}\right] $$</code></p>
<p>其中，<code>$\left(x, y^+\right)$</code> 为一个相似对，<code>$y^-$</code> 对于 <code>$x$</code> 而言假定为不相似，<code>$y^+$</code> 和 <code>$y^-$</code> 通常称之为正样本和负样本。评分函数 <code>$s \left(x, y\right)$</code> 通过一个神经编码器计算可得，<code>$s \left(x, y\right) = f^{\top}_{\text{enc}} \left(x\right) f_{\text{enc}} \left(y\right)$</code> 或 <code>$s \left(x, y\right) = f_{\text{enc}} \left(x \oplus y\right)$</code>。CTL 的核心思想是“通过对比进行学习”。</p>
<p>下图展示了预训练模型的分类和部分代表模型：</p>

<div class="box fancy-figure caption-position-bottom caption-effect-fade" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img">
      <img itemprop="thumbnail" src="/images/cn/2020-03-28-pre-trained-model-for-nlp/ptms.png" alt="预训练模型分类及代表性模型"/>
    </div>
    <a href="/images/cn/2020-03-28-pre-trained-model-for-nlp/ptms.png" itemprop="contentUrl"></a>
      <figcaption>
          <p>预训练模型分类及代表性模型</p>
      </figcaption>
  </figure>
</div>

<h3 id="应用于下游任务">应用于下游任务</h3>
<h4 id="如何迁移">如何迁移</h4>
<h5 id="选择合适的预训练任务-模型架构和语料">选择合适的预训练任务，模型架构和语料</h5>
<p>不同的 PTMs 在相同的下游任务上有着不同的效果，这是因为 PTMs 有着不同的预训练任务，模型架构和语料。</p>
<ol>
<li>目前，语言模型是最流行的预训练任务，同时也可以有效地解决很多 NLP 问题。但是不同的预训练任务有着自己的侧重，在不同的任务上会有不同的效果。例如：NSP 任务使得 PTM 可以理解两句话之间的关系，因此 PTM 可以在例如问答（Question Answering，QA）和自然语言推理（Natural Language Inference，NLI）等下游任务上表现更好。</li>
<li>PTM 的网络架构对下游任务也至关重要。例如：尽管 BERT 可以处理大多数自然语言理解任务，对其很难生成语言。</li>
<li>下游任务的数据分布应该和 PTM 训练所用语料相似。目前，大量现成的 PTM 仅可以快速地用于特定领域或特定语言的下游任务上。</li>
</ol>
<h5 id="选择合适的网络层">选择合适的网络层</h5>
<p>给定一个预训练的模型，不同的网络层捕获了不同的信息，例如：词性标记（POS tagging），语法（parsing），长期依赖（long-term dependencies），语义角色（semantic roles），指代（coreference）等。Tenney <sup id="fnref:22"><a href="#fn:22" class="footnote-ref" role="doc-noteref">22</a></sup> 等人发现 BERT 表示方式类似传统的 NLP 流程：基础的句法信息出现在浅层的网络中，高级的语义信息出现在更高的层级中。</p>
<p>令 <code>$\mathbf{H}^{\left(l\right)} \left(1 \leq l \leq L\right)$</code> 表示共 <code>$L$</code> 层的预训练模型的第 <code>$l$</code> 层表示，<code>$g \left(\cdot\right)$</code> 表示用于特定任务的的模型。一般有 3 中情况选择表示：</p>
<ol>
<li>Embedding Only：一种情况是仅选用预训练模型的静态嵌入，模型的其他部分仍需作为一个任务从头训练。这种情况不能够获取到一些有用的深层信息，词嵌入仅能够捕获词的语义信息。</li>
<li>Top Layer：最简单有效的方式是将网络的顶层表示输入到模型中 <code>$g \left(\mathbf{H}^{\left(L\right)}\right)$</code>。</li>
<li>All Layers：另一种更灵活的方式是自动选择最合适的层，例如 ELMo：
<code>$$ \mathbf{r}_t = \gamma \sum_{l=1}^{L}{\alpha_l \mathbf{h}^{\left(l\right)}_t} $$</code>
其中 <code>$\alpha_l$</code> 是层 <code>$l$</code> 的 softmax 归一的权重，<code>$\gamma$</code> 是用于缩放预训练模型输出向量的一个标量值，再将不同层的混合输出输入到后续模型中 <code>$g \left(\mathbf{r}_t\right)$</code>。</li>
</ol>
<h5 id="是否微调">是否微调</h5>
<p>目前，主要有两种方式进行模型迁移：特征提取（预训练模型的参数是固定的）和模型微调（预训练模型的参数是经过微调的）。当采用特征提取时，预训练模型可以被看作是一个特征提取器。除此之外，我们应该采用内部层作为特征，因为他们通常是最适合迁移的特征。尽管两种不同方式都能对大多数 NLP 任务效果有显著提升，但以特征提取的方式需要更复杂的特定任务的架构。因此，微调是一种更加通用和方便的处理下游任务的方式。</p>
<h4 id="微调策略">微调策略</h4>
<p>随着 PTMs 网络层数的加深，其捕获的表示使得下游任务变得越来越简单，因此整个模型中用于特定任务的网络层一般比较简单，微调已经成为了采用 PTMs 的主要方式。但是微调的过程通常是比较不好预估的，即使采用相同的超参数，不同的随机数种子也可能导致差异较大的结果。除了标准的微调外，如下为一些有用的微调策略：</p>
<h5 id="两步骤微调">两步骤微调</h5>
<p>一种方式是两阶段的迁移，在预训练和微调之间引入了一个中间阶段。在第一个阶段，PTM 通过一个中间任务或语料转换为一个微调后的模型，在第二个阶段，再利用目标任务进行微调。</p>
<h5 id="多任务微调">多任务微调</h5>
<p>在多任务学习框架下对其进行微调。</p>
<h5 id="利用额外模块进行微调">利用额外模块进行微调</h5>
<p>微调的主要缺点就是其参数的低效性。每个下游模型都有其自己微调好的参数，因此一个更好的解决方案是将一些微调好的适配模块注入到 PTMs 中，同时固定原始参数。</p>
<h3 id="开放资源">开放资源</h3>
<h4 id="ptms-开源实现">PTMs 开源实现：</h4>
<table>
  <thead>
      <tr>
          <th>项目</th>
          <th>框架</th>
          <th>PTMs</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://github.com/tmikolov/word2vec">word2vec</a></td>
          <td>-</td>
          <td>CBOW, Skip-Gram</td>
      </tr>
      <tr>
          <td><a href="https://nlp.stanford.edu/projects/glove">GloVe</a></td>
          <td>-</td>
          <td>Pre-trained word vectors</td>
      </tr>
      <tr>
          <td><a href="https://github.com/facebookresearch/fastText">FastText</a></td>
          <td>-</td>
          <td>Pre-trained word vectors</td>
      </tr>
      <tr>
          <td><a href="https://github.com/huggingface/transformers">Transformers</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i> &amp; <i class="icon icon-tensorflow">TF</i></td>
          <td>BERT, GPT-2, RoBERTa, XLNet, etc.</td>
      </tr>
      <tr>
          <td><a href="https://github.com/pytorch/fairseq">Fairseq</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
          <td>English LM, German LM, RoBERTa, etc.</td>
      </tr>
      <tr>
          <td><a href="https://github.com/%EF%AC%82airNLP/%EF%AC%82air">Flair</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
          <td>BERT, ELMo, GPT, RoBERTa, XLNet, etc.</td>
      </tr>
      <tr>
          <td><a href="https://github.com/allenai/allennlp">AllenNLP</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
          <td>ELMo, BERT, GPT-2, etc.</td>
      </tr>
      <tr>
          <td><a href="https://github.com/fastnlp/fastNLP">FastNLP</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
          <td>BERT, RoBERTa, GPT, etc.</td>
      </tr>
      <tr>
          <td><a href="https://github.com/ymcui/Chinese-BERT-wwm">Chinese-BERT</a></td>
          <td>-</td>
          <td>BERT, RoBERTa, etc. (for Chinese)</td>
      </tr>
      <tr>
          <td><a href="https://github.com/google-research/bert">BERT</a></td>
          <td><i class="icon icon-tensorflow">TF</i></td>
          <td>BERT, BERT-wwm</td>
      </tr>
      <tr>
          <td><a href="https://github.com/pytorch/fairseq/tree/master/examples/roberta">RoBERTa</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
          <td></td>
      </tr>
      <tr>
          <td><a href="https://github.com/zihangdai/xlnet">XLNet</a></td>
          <td><i class="icon icon-tensorflow">TF</i></td>
          <td></td>
      </tr>
      <tr>
          <td><a href="https://github.com/google-research/ALBERT">ALBERT</a></td>
          <td><i class="icon icon-tensorflow">TF</i></td>
          <td></td>
      </tr>
      <tr>
          <td><a href="https://github.com/google-research/text-to-text-transfer-transformer">T5</a></td>
          <td><i class="icon icon-tensorflow">TF</i></td>
          <td></td>
      </tr>
      <tr>
          <td><a href="https://github.com/thunlp/ERNIE">ERNIE(THU)</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i></td>
          <td></td>
      </tr>
      <tr>
          <td><a href="https://github.com/PaddlePaddle/ERNIE">ERNIE(Baidu)</a></td>
          <td><i class="icon icon-paddlepaddle"></i> PaddlePaddle</td>
          <td></td>
      </tr>
      <tr>
          <td><a href="https://github.com/huggingface/transformers">Hugging Face</a></td>
          <td><i class="icon icon-pytorch">PyTorch</i> &amp; <i class="icon icon-tensorflow">TF</i></td>
          <td>很多&hellip;</td>
      </tr>
  </tbody>
</table>
<h4 id="论文列表和-ptms-相关资源">论文列表和 PTMs 相关资源：</h4>
<table>
  <thead>
      <tr>
          <th>资源</th>
          <th>URL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>论文列表</td>
          <td><a href="https://github.com/thunlp/PLMpapers">https://github.com/thunlp/PLMpapers</a></td>
      </tr>
      <tr>
          <td>论文列表</td>
          <td><a href="https://github.com/tomohideshibata/BERT-related-papers">https://github.com/tomohideshibata/BERT-related-papers</a></td>
      </tr>
      <tr>
          <td>论文列表</td>
          <td><a href="https://github.com/cedrickchee/awesome-bert-nlp">https://github.com/cedrickchee/awesome-bert-nlp</a></td>
      </tr>
      <tr>
          <td>Bert Lang Street</td>
          <td><a href="https://bertlang.unibocconi.it">https://bertlang.unibocconi.it</a></td>
      </tr>
      <tr>
          <td>BertViz</td>
          <td><a href="https://github.com/jessevig/bertviz">https://github.com/jessevig/bertviz</a></td>
      </tr>
  </tbody>
</table>
<h2 id="预训练模型">预训练模型</h2>
<h3 id="cove-2017">CoVe (2017) <sup id="fnref1:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup></h3>
<p>首先，给定一个源语言序列 <code>$w^x = \left[w^x_1, \dotsc, w^x_n\right]$</code> 和一个翻译目标语言序列 <code>$w^z = \left[w^z_1, \dotsc, w^z_n\right]$</code>。令 <code>$\text{GloVe} \left(w^x\right)$</code> 为词 <code>$w^x$</code> 对应的 GloVe 向量，<code>$z$</code> 为 <code>$w^z$</code> 中的词随机初始化的词向量。将 <code>$\text{GloVe} \left(w^x\right)$</code> 输入到一个标准的两层 biLSTM 网络中，称之为 MT-LSTM，MT-LSTM 用于计算序列的隐含状态如下：</p>
<p><code>$$ h = \text{MT-LSTM} \left(\text{GloVe} \left(w^x\right)\right) $$</code></p>
<p>对于机器翻译，MT-LSTM 的注意力机制的解码器可以对于输出的词在每一步产生一个分布 <code>$p \left(\hat{w}^z_t \mid H, w^z_1, \dotsc, w^z_{t-1}\right)$</code>。在 <code>$t$</code> 步，解码器利用一个两层的单向 LSTM 基于之前目标词嵌入 <code>$z_{t-1}$</code> 和一个基于上下文调整的隐含状态 <code>$\tilde{h}_{t-1}$</code> 生成一个隐含状态 <code>$h^{\text{dec}}_t$</code>：</p>
<p><code>$$ h^{\text{dec}}_t = \text{LSTM} \left(\left[z_{t-1}; \tilde{h}_{t-1}\right], h^{\text{dec}}_{t-1}\right) $$</code></p>
<p>之后解码器计算每一步编码到当前解码状态的注意力权重 <code>$\alpha$</code>：</p>
<p><code>$$ \alpha_t = \text{softmax} \left(H \left(W_1 h^{\text{dec}}_t + b_1\right)\right) $$</code></p>
<p>其中 <code>$H$</code> 表示 <code>$h$</code> 按照时间维度的堆叠。之后解码器将这些权重作为相关性用于计算基于上下文调整的隐含状态 <code>$\tilde{h}$</code>：</p>
<p><code>$$ \tilde{h}_t = \text{tanh} \left(W_2 \left[H^{\top} \alpha_t; h^{\text{dec}}_t\right] + b_2\right) $$</code></p>
<p>最后，输出词的分布通过基于上下文调整的隐含状态计算可得：</p>
<p><code>$$ p \left(\hat{w}^z_t \mid H, w^z_1, \dotsc, w^z_{t-1}\right) = \text{softmax} \left(W_{\text{out}} \tilde{h}_t + b_{\text{out}}\right) $$</code></p>
<p>CoVe 将 MT-LSTM 学习到的表示迁移到下游任务中，令 <code>$w$</code> 表示文字序列，<code>$\text{GloVe} \left(w\right)$</code> 表示对应的 GloVe 向量，则：</p>
<p><code>$$ \text{CoVe} \left(w\right) = \text{MT-LSTM} \left(\text{GloVe} \left(w\right)\right) $$</code></p>
<p>表示由 MT-LSTM 产生的上下文向量，对于分类和问答任务，有一个输入序列 <code>$w$</code>，我们可以将 GloVe 和 CoVe 向量进行拼接作为其嵌入表示：</p>
<p><code>$$ \tilde{w} = \left[\text{GloVe} \left(w\right); \text{CoVe} \left(w\right)\right] $$</code></p>
<p>CoVe 网络架构示意图如下：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/cove.png" class="lazyload"/>
  
</figure>
<h3 id="elmo-2018">ELMo (2018) <sup id="fnref1:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup></h3>
<p>在 ELMo 模型中，对于每个词条 <code>$t_k$</code>，一个 <code>$L$</code> 层的 biLM 可以计算得到 <code>$2L + 1$</code> 个表示：</p>
<p><code>$$ \begin{aligned} R_k &amp;= \left\{\mathbf{x}^{LM}_k, \overrightarrow{\mathbf{h}}^{LM}_{k, j}, \overleftarrow{\mathbf{h}}^{LM}_{k, j} \mid j = 1, \dotsc, L \right\} \\ &amp;= \left\{\mathbf{h}^{LM}_{k, j} \mid j = 0, \dotsc, L\right\} \end{aligned} $$</code></p>
<p>其中 <code>$\mathbf{h}^{LM}_{k, 0}$</code> 为词条的嵌入层，<code>$\mathbf{h}^{LM}_{k, j} = \left[\overrightarrow{\mathbf{h}}^{LM}_{k, j}; \overleftarrow{\mathbf{h}}^{LM}_{k, j}\right]$</code> 为每个 biLSTM 层。</p>
<p>对于下游任务，ELMo 将 <code>$R$</code> 中的所有层汇总成一个向量 <code>$\mathbf{ELMo}_k = E \left(R_k; \mathbf{\Theta}_e\right)$</code>。在一些简单的案例中，ELMo 仅选择顶层，即：<code>$E \left(R_k\right) = \mathbf{h}^{LM}_{k, L}$</code>。更通用的，对于一个特定的任务，我们可以计算一个所有 biLM 层的加权：</p>
<p><code>$$ \mathbf{ELMo}^{task}_k = E \left(R_k; \Theta^{task}\right) = \gamma^{task} \sum_{j=0}^{L}{s^{task}_j \mathbf{h}^{LM}_{k, j}} $$</code></p>
<p>其中，<code>$s^{task}$</code> 表示 softmax 归一化后的权重，<code>$\gamma^{task}$</code> 允许模型对整个 ELMo 向量进行缩放。<code>$\gamma$</code> 对整个优化过程具有重要意义，考虑每个 biLM 层的激活具有不同的分布，在一些情况下这相当于在进行加权之前对每一个 biLM 层增加了层标准化。</p>
<p>ELMo 网络架构示意图如下 <sup id="fnref:23"><a href="#fn:23" class="footnote-ref" role="doc-noteref">23</a></sup>：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/elmo.png" class="lazyload"/>
  
</figure>
<h3 id="gpt-2018">GPT (2018) <sup id="fnref1:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup></h3>
<p>给定一个语料 <code>$\mathcal{U} = \left\{u_1, \dotsc, u_n\right\}$</code>，使用标准的语言建模目标来最大化如下似然：</p>
<p><code>$$ L_1 \left(\mathcal{U}\right) = \sum_{i} \log P \left(u_i \mid u_{i-k}, \dotsc, u_{i-1}; \Theta\right) $$</code></p>
<p>其中，<code>$k$</code> 为上下文窗口的大小，条件概率 <code>$P$</code> 通过参数为 <code>$\Theta$</code> 的神经网络进行建模。GPT 中使用了一个多层的 Transformer Decoder 作为语言模型。模型首先对输入上下文词条应用多头自注意力机制，再通过按位置的前馈层产生目标词条的输出分布：</p>
<p><code>$$ \begin{aligned} h_0 &amp;= UW_e + W_p \\ h_l &amp;= \text{transformer_black} \left(h_{l-1}\right), \forall i \in \left[1, n\right] \\ P \left(u\right) &amp;= \text{softmax} \left(h_n W^{\top}_e\right) \end{aligned} $$</code></p>
<p>其中，<code>$U = \left(u_{-k}, \dotsc, u_{-1}\right)$</code> 为词条的上下文向量，<code>$n$</code> 为网络层数，<code>$W_e$</code> 为词条的嵌入矩阵，<code>$W_p$</code> 为位置嵌入矩阵。</p>
<p>给定一个有标签的数据集 <code>$\mathcal{C}$</code>，其中包含了输入词条序列 <code>$x^1, \dotsc, x^m$</code> 和对应的标签 <code>$y$</code>。利用上述预训练的模型获得输入对应的最后一个 Transformer 的激活输出 <code>$h^m_l$</code>，之后再将其输入到一个参数为 <code>$W_y$</code> 的线性输入层中预测 <code>$y$</code>：</p>
<p><code>$$ P \left(y \mid x^1, \dotsc, x^m\right) = \text{softmax} \left(h^m_l W_y\right) $$</code></p>
<p>模型通过最小化如下损失进行优化：</p>
<p><code>$$ L_2 \left(\mathcal{C}\right) = \sum_{\left(x, y\right)} \log P \left(y \mid x^1, \dotsc, x^m\right) $$</code></p>
<p>研究还发现将语言建模作为微调的附加目标可以帮助提高模型的泛化能力，同时可以加速模型收敛。GPT 中采用如下的优化目标：</p>
<p><code>$$ L_3 \left(\mathcal{C}\right) = L_2 \left(\mathcal{C}\right) + \lambda L_1 \left(\mathcal{C}\right) $$</code></p>
<p>GPT 网络架构示意图如下：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/gpt.png" class="lazyload"/>
  
</figure>
<h3 id="bert-2018">BERT (2018) <sup id="fnref1:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup></h3>
<p>BERT 采用了一中基于 Vaswani <sup id="fnref1:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup> 所提出模型的多层双向 Transformer 编码器。在 BERT 中，令 <code>$L$</code> 为 Transformer Block 的层数，<code>$H$</code> 为隐层大小，<code>$A$</code> 为自注意力头的数量。在所有情况中，设置前馈层的大小为 <code>$4H$</code>，BERT 提供了两种不同大小的预训练模型：</p>
<ul>
<li><code>$\text{BERT}_{\text{BASE}}$</code>：<code>$L=12, H=768, A=12$</code>，参数总量为 100 M。</li>
<li><code>$\text{BERT}_{\text{LARGE}}$</code>：<code>$L=24, H=1024, A=16$</code>，参数总量为 340 M。</li>
</ul>
<p><code>$\text{BERT}_{\text{BASE}}$</code> 采用了同 GPT 相同的模型大小用于比较，不同与 GPT，BERT 使用了双向的注意力机制。在文献中，双向 Transformer 通常称之为 Transformer 编码器，仅利用左边上下文信息的 Transformer 由于可以用于文本生成被称之为 Transformer 解码器。BERT，GPT 和 ELMo 之间的不同如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/bert-gpt-elmo-model-architectures.png" class="lazyload"/>
  
</figure>
<p>BERT 的输入表示既可以表示一个单独的文本序列，也可以表示一对文本序列（例如：问题和答案）。对于一个给定的词条，其输入表示由对应的词条嵌入，分割嵌入和位置嵌入三部分加和构成，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/bert-input-representation.png" class="lazyload"/>
  
</figure>
<p>具体的有：</p>
<ul>
<li>采用一个包含 30,000 个词条的 WordPiece 嵌入 <sup id="fnref:24"><a href="#fn:24" class="footnote-ref" role="doc-noteref">24</a></sup>。</li>
<li>位置嵌入最大支持 512 个词条。</li>
<li>序列的第一字符采用特殊的分类嵌入 <code>[CLS]</code>，其最终的隐含状态在分类任务中用于汇总整个序列的表示，对于非分类任务则忽视该向量。</li>
<li>句子对被整合成一个序列，首先利用一个特殊词条 <code>[SEP]</code> 对句子进行分割，其次对于第一个句子中的每个词条叠加一个学习到的 A 句子嵌入，对于第二个句子中的每个词条叠加一个学习到的 B 句子嵌入。</li>
<li>对于一个单独的句子，仅使用 A 句子嵌入。</li>
</ul>
<p>在预训练阶段，BERT 采用了两个无监督预测任务：</p>
<ol>
<li>遮罩的语言模型（Masked LM，MLM）<br>
不同于一般的仅利用 <code>[MASK]</code> 进行遮挡，BERT 选择采用 80% 的 <code>[MASK]</code>，10% 的随机词和 10% 保留原始词的方式对随机选择的 15% 的词条进行遮挡处理。由于编码器不知会预测哪个词或哪个词被随机替换了，这迫使其必须保留每个输入词条的分布式上下文表示。同时 1.5% 的随机替换也不会过多的损害模型的理解能力。</li>
<li>预测是否为下一个句子（Next Sentence Prediction）<br>
一些重要的下游任务，例如问答（Question Answering，QA）和自然语言推断（Natural Language Inference，NLI）是基于两个句子之间关系的理解，这是语言建模无法直接捕获的。BERT 通过训练一个预测是否为下一个句子的二分类任务来实现，对于一个句子对 A 和 B，50% 的 B 是句子 A 真实的下一句，剩余 50% 为随机抽取的。</li>
</ol>
<p>基于 BERT 的不同下游任务的实现形式如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/bert-task-specific-models.png" class="lazyload"/>
  
</figure>
<h3 id="unilm-2019">UniLM (2019) <sup id="fnref:25"><a href="#fn:25" class="footnote-ref" role="doc-noteref">25</a></sup></h3>
<p>给定一个输入序列 <code>$x = x_1 \cdots x_{|x|}$</code>，UniLM 通过下图的方式获取每个词条的基于上下文的向量表示。整个预训练过程利用单向的语言建模（unidirectional LM），双向的语言建模（bidirectional LM）和 Seq2Seq 语言建模（sequence-to-sequence LM）优化共享的 Transformer 网络。</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/unilm.png" class="lazyload"/>
  
</figure>
<p>输入序列 <code>$x$</code> 对于单向语言模型而言是一个分割的文本，对于双向语言模型和 Seq2Seq 语言模型而言是一对打包的分割文本。UniLM 在输入的起始位置添加特殊的 <code>[SOS]</code> （start-of-sequence），在结尾处添加 <code>[EOS]</code>（end-of-sequence）。<code>[EOS]</code> 对于自然语言理解（NLU）任务可以标记句子之间的界线，对于自然语言生成（NLG）任务可以确定解码过程停止的时间。输入的表示同 BERT 一样，文本利用 WordPiece 进行分割，对于每个输入词条，其向量表示为对应的词条嵌入，位置嵌入和分割嵌入的汇总。</p>
<p>对于输入向量 <code>$\left\{\mathbf{x}_i\right\}^{|x|}_{i=1}$</code> 首先将其输入到隐层 <code>$\mathbf{H}^0 = \left[\mathbf{x}_1, \dotsc, \mathbf{x}_{|x|}\right]$</code>，之后使用一个 <code>$L$</code> 层的 Transformer <code>$\mathbf{H}^l = \text{Transformer}_l \left(\mathbf{H}^{l-1}\right), l \in \left[1, L\right]$</code> 对每一层 <code>$\mathbf{H}^l = \left[\mathbf{h}^l_1, \dotsc, \mathbf{h}^l_{|x|}\right]$</code> 进行上下文表示编码。在每个 Tansformer 块中，使用多头自注意力机制对输出向量和上一层进行汇总，第 <code>$l$</code> 层 Transformer 自注意力头 <code>$\mathbf{A}_l$</code> 的输入通过如下方式计算：</p>
<p>`$$
\begin{aligned}
\mathbf{Q} &amp;= \mathbf{H}^{l-1} \mathbf{W}^Q_l, \mathbf{K} = \mathbf{H}^{l-1} \mathbf{W}^K_l, \mathbf{V} = \mathbf{H}^{l-1} \mathbf{W}^W_l \
\mathbf{M}_{ij} &amp;=
\begin{cases}
0, &amp; \text{allow to attend} \</p>
<ul>
<li>\infty, &amp; \text{prevent from attending}
\end{cases} \
\mathbf{A}_l &amp;= \text{softmax} \left(\dfrac{\mathbf{Q} \mathbf{K}^{\top}}{\sqrt{d_k}} + \mathbf{M}\right) \mathbf{V}_l
\end{aligned}
$$`</li>
</ul>
<p>其中，上一层的输出 <code>$\mathbf{H}^{l-1} \in \mathbb{R}^{|x| \times d_h}$</code> 通过参数矩阵 <code>$\mathbf{W}^Q_l, \mathbf{W}^K_l, \mathbf{W}^V_l \in \mathbb{R}^{d_h \times d_k}$</code> 线性地映射为相应的 Query，Key 和 Value，遮罩矩阵 <code>$\mathbf{M} \in \mathbb{R}^{|x| \times |x|}$</code> 用于确定一对词条是否可以被相互连接。</p>
<h3 id="transformer-xl-2019">Transformer-XL (2019) <sup id="fnref:26"><a href="#fn:26" class="footnote-ref" role="doc-noteref">26</a></sup></h3>
<p>将 Transformer 或注意力机制应用到语言建模中的核心问题是如何训练 Transformer 使其有效地将一个任意长文本编码为一个固定长度的表示。Transformer-XL 将整个语料拆分为较短的段落，仅利用每段进行训练并忽略之前段落的上下文信息。这种方式称之为 Vanilla Model <sup id="fnref:27"><a href="#fn:27" class="footnote-ref" role="doc-noteref">27</a></sup>，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/transformer-xl-vanilla-model.png" class="lazyload"/>
  
</figure>
<p>在这种训练模式下，无论是前向还是后向信息都不会跨越分割的段落进行传导。利用固定长度的上下文主要有两个弊端：</p>
<ol>
<li>这限制了最大依赖的长度，虽然自注意力机制不会像 RNN 一样受到梯度弥散的影响，但 Vanilla Model 也不能完全利用到这个优势。</li>
<li>虽然可以利用补全操作来实现句子或其他语义的分割，但实际上通常会简单的将一个长文本截断成一个固定长度的分割，这样会产生上下文分裂破碎的问题。</li>
</ol>
<p>为了解决这个问题，Transformer-XL 采用了一种循环机制的 Transformer。在训练阶段，在处理新的分割段落时，之前分割分部分的隐含状态序列将被**固定（fixed）<strong>和</strong>缓存（cached）**下来作为一个扩展的上下文被复用参与计算，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/transformer-xl-model.png" class="lazyload"/>
  
</figure>
<p>虽然梯度仍仅限于这个分割段落内部，但网络可以从历史中获取信息，从而实现对长期依赖的建模。令两个长度为 <code>$L$</code> 的连续分割段落为 <code>$\mathbf{s}_{\tau} = \left[x_{\tau, 1}, \dotsc, x_{\tau, L}\right]$</code> 和 <code>$\mathbf{s}_{\tau + 1} = \left[x_{\tau + 1, 1}, \dotsc, x_{\tau + 1, L}\right]$</code>，第 <code>$\tau$</code> 段分割 <code>$\mathbf{s}_{\tau}$</code> 的第 <code>$n$</code> 层隐含状态为 <code>$\mathbf{h}^n_{\tau} \in \mathbb{R}^{L \times d}$</code>，其中 <code>$d$</code> 为隐含维度。则对于分割段落 <code>$\mathbf{s}_{\tau + 1}$</code> 的第 <code>$n$</code> 层隐含状态通过如下方式进行计算：</p>
<p><code>$$ \begin{aligned} \tilde{\mathbf{h}}^{n-1}_{\tau + 1} &amp;= \left[\text{SG} \left(\mathbf{h}^{n-1}_{\tau}\right) \circ \mathbf{h}^{n-1}_{\tau + 1} \right] \\ \mathbf{q}^{n}_{\tau + 1}, \mathbf{k}^{n}_{\tau + 1}, \mathbf{v}^{n}_{\tau + 1} &amp;= \mathbf{h}^{n-1}_{\tau + 1} \mathbf{W}^{\top}_{q}, \tilde{\mathbf{h}}^{n-1}_{\tau + 1} \mathbf{W}^{\top}_{k}, \tilde{\mathbf{h}}^{n-1}_{\tau + 1} \mathbf{W}^{\top}_{v} \\ \mathbf{h}^{n}_{\tau + 1} &amp;= \text{Transformer-Layer} \left(\mathbf{q}^{n}_{\tau + 1}, \mathbf{k}^{n}_{\tau + 1}, \mathbf{v}^{n}_{\tau + 1}\right) \end{aligned} $$</code></p>
<p>其中，<code>$\text{SG} \left(\cdot\right)$</code> 表示停止梯度，<code>$\left[\mathbf{h}_u \circ \mathbf{h}_v\right]$</code> 表示将两个隐含序列按照长度维度进行拼接，<code>$\mathbf{W}$</code> 为模型的参数。与一般的 Transformer 相比，最大的不同在于 <code>$\mathbf{k}^n_{\tau + 1}$</code> 和 <code>$\mathbf{v}^n_{\tau + 1}$</code> 不仅依赖于 <code>$\tilde{\mathbf{h}}^{n-1}_{\tau - 1}$</code> 还依赖于之前分割段落的 <code>$\mathbf{h}^{n-1}_{\tau}$</code> 缓存。</p>
<p>在标准的 Transformer 中，序列的顺序信息通过位置嵌入 <code>$\mathbf{U} \in \mathbb{R}^{L_{\max} \times d}$</code> 提供，其中第 <code>$i$</code> 行 <code>$\mathbf{U}_i$</code> 对应一个分割文本内部的第 <code>$i$</code> 个<strong>绝对</strong>位置，<code>$L_{\max}$</code> 为最大可能长度。在 Transformer-XL 中则是通过一种<strong>相对</strong>位置信息对其进行编码，构建一个相对位置嵌入 <code>$\mathbf{R} \in \mathbb{R} ^{L_{\max} \times d}$</code>，其中第 <code>$i$</code> 行 <code>$\mathbf{R}_i$</code> 表示两个位置之间相对距离为 <code>$i$</code> 的嵌入表示。</p>
<p>对于一般的 Transformer，一个分割段落内部的 <code>$q_i$</code> 和 <code>$k_j$</code> 之间的注意力分数可以分解为：</p>
<p><code>$$ \begin{aligned} \mathbf{A}_{i, j}^{\mathrm{abs}} &amp;=\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{E}_{x_{j}}}_{(a)}+\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{U}_{j}}_{(b)} \\ &amp;+\underbrace{\mathbf{U}_{i}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{E}_{x_{j}}}_{(c)}+\underbrace{\mathbf{U}_{i}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{U}_{j}}_{(d)} \end{aligned} $$</code></p>
<p>利用相对位置思想，变化如下：</p>
<p><code>$$ \begin{aligned} \mathbf{A}_{i, j}^{\mathrm{rel}} &amp;=\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k, E} \mathbf{E}_{x_{j}}}_{(a)}+\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k, R} \textcolor{blue}{\mathbf{R}_{i-j}}}_{(b)} \\ &amp;+\underbrace{\textcolor{red}{u^{\top}} \mathbf{W}_{k, E} \mathbf{E}_{x_{j}}}_{(c)}+\underbrace{\textcolor{red}{v^{\top}} \mathbf{W}_{k, R} \textcolor{blue}{\mathbf{R}_{i-j}}}_{(d)} \end{aligned} $$</code></p>
<ol>
<li>首先，利用相对位置 <code>$\textcolor{blue}{\mathbf{R}_{i-j}}$</code> 替代绝对位置嵌入 <code>$\mathbf{U}_j$</code>，这里 <code>$\mathbf{R}$</code> 采用的是无需学习的 sinusoid 编码矩阵 <sup id="fnref2:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup>。</li>
<li>其次，引入了一个可训练的参数 <code>$\textcolor{red}{u} \in \mathbb{R}^d$</code> 用于替换 <code>$\mathbf{U}^{\top}_i \mathbf{W}^{\top}_q$</code>。类似的，对于 <code>$\mathbf{U}^{\top} \mathbf{W}^{\top}_q$</code> 使用一个可训练的 <code>$\textcolor{red}{v} \in \mathbb{R}^d$</code> 替换。</li>
<li>最后，有意地划分了两个权重矩阵 <code>$\mathbf{W}_{k, E}$</code> 和 <code>$\mathbf{W}_{k, R}$</code> 用于生成基于内容的 Key 向量和基于位置的 Key 向量。</li>
</ol>
<p>这样，<code>$\left(a\right)$</code> 代表了基于内容的位置信息，<code>$\left(b\right)$</code> 捕获了内容无关的位置偏置，<code>$\left(c\right)$</code> 表示了一个全局的内容偏置，<code>$\left(d\right)$</code> 捕获了一个全局的位置偏置。</p>
<p>利用一个自注意力头计算 <code>$N$</code> 层的 Transformer-XL 的过程如下，对于 <code>$n = 1, \dotsc, N$</code> 有：</p>
<p><code>$$ \begin{aligned} \widetilde{\mathbf{h}}_{\tau}^{n-1}=&amp;\left[\mathrm{SG}\left(\mathbf{m}_{\tau}^{n-1}\right) \circ \mathbf{h}_{\tau}^{n-1}\right] \\ \mathbf{q}_{\tau}^{n}, \mathbf{k}_{\tau}^{n}, \mathbf{v}_{\tau}^{n}=&amp; \mathbf{h}_{\tau}^{n-1} {\mathbf{W}_{q}^{n}}^{\top}, \widetilde{\mathbf{h}}_{\tau}^{n-1} {\mathbf{W}_{k, E}^{n}}^{\top}, \widetilde{\mathbf{h}}_{\tau}^{n-1} {\mathbf{W}_{v}^{n}}^{\top} \\ \mathbf{A}_{\tau, i, j}^{n}=&amp; {\mathbf{q}_{\tau, i}^{n}}^{\top} \mathbf{k}_{\tau, j}^{n} + {\mathbf{q}_{\tau, i}^{n}}^{\top} \mathbf{W}_{k, R}^{n} \mathbf{R}_{i-j} \\ &amp;+u^{\top} \mathbf{k}_{\tau, j}+v^{\top} \mathbf{W}_{k, R}^{n} \mathbf{R}_{i-j} \\ \mathbf{a}_{\tau}^{n}=&amp; \text { Masked-Softmax }\left(\mathbf{A}_{\tau}^{n}\right) \mathbf{v}_{\tau}^{n} \\ \mathbf{o}_{\tau}^{n}=&amp; \text { LayerNorm } \left(\text{Linear}\left(\mathbf{a}_{\tau}^{n}\right)+\mathbf{h}_{\tau}^{n-1}\right) \\ \mathbf{h}_{\tau}^{n}=&amp; \text { Positionwise-Feed-Forward }\left(\mathbf{o}_{\tau}^{n}\right) \end{aligned} $$</code></p>
<h3 id="xlnet-2019">XLNet (2019) <sup id="fnref1:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup></h3>
<p>给定一个序列 <code>$\mathbf{X} = \left[x_1, \dotsc, x_T\right]$</code>，AR 语言模型通过最大化如下似然进行预训练：</p>
<p><code>$$ \max_{\theta} \quad \log p_{\theta}(\mathbf{x})=\sum_{t=1}^{T} \log p_{\theta}\left(x_{t} | \mathbf{x}_{&lt;t}\right)=\sum_{t=1}^{T} \log \frac{\exp \left(h_{\theta}\left(\mathbf{x}_{1: t-1}\right)^{\top} e\left(x_{t}\right)\right)}{\sum_{x^{\prime}} \exp \left(h_{\theta}\left(\mathbf{x}_{1: t-1}\right)^{\top} e\left(x^{\prime}\right)\right)} $$</code></p>
<p>其中，<code>$h_{\theta}\left(\mathbf{x}_{1: t-1}\right)$</code> 是由 RNNs 或 Transformer 等神经网络网络模型生成的上下文表示，<code>$e \left(x\right)$</code> 为 <code>$x$</code> 的嵌入。对于一个文本序列 <code>$\mathbf{x}$</code>，BERT 首先构建了一个遮罩的数据集 <code>$\hat{\mathbf{x}}$</code>，令被遮挡的词条为 <code>$\overline{\mathbf{x}}$</code>，通过训练如下目标来利用 <code>$\hat{\mathbf{x}}$</code> 重构 <code>$\overline{\mathbf{x}}$</code>：</p>
<p><code>$$ \max_{\theta} \quad \log p_{\theta}(\overline{\mathbf{x}} | \hat{\mathbf{x}}) \approx \sum_{t=1}^{T} m_{t} \log p_{\theta}\left(x_{t} | \hat{\mathbf{x}}\right)=\sum_{t=1}^{T} m_{t} \log \frac{\exp \left(H_{\theta}(\hat{\mathbf{x}})_{t}^{\top} e\left(x_{t}\right)\right)}{\sum_{x^{\prime}} \exp \left(H_{\theta}(\hat{\mathbf{x}})_{t}^{\top} e\left(x^{\prime}\right)\right)} $$</code></p>
<p>其中 <code>$m_t = 1$</code> 表示 <code>$x_t$</code> 是被遮挡的，<code>$H_{\theta}$</code> 是一个 Transformer 将一个长度为 <code>$T$</code> 的文本序列映射到一个隐含向量序列 <code>$H_{\theta}(\mathbf{x})=\left[H_{\theta}(\mathbf{x})_{1}, H_{\theta}(\mathbf{x})_{2}, \cdots, H_{\theta}(\mathbf{x})_{T}\right]$</code>。两种不同的预训练目标的优劣势如下</p>
<ol>
<li><strong>独立假设</strong>：BERT 中联合条件概率 <code>$p(\overline{\mathbf{x}} | \hat{\mathbf{x}})$</code> 假设在给定的 <code>$\hat{\mathbf{x}}$</code> 下，遮挡的词条 <code>$\overline{\mathbf{x}}$</code> 是相关独立的，而 AR 语言模型则没有这样的假设。</li>
<li><strong>输入噪声</strong>：BERT 在预训练是使用了特殊标记 <code>[MASK]</code>，在下游任务微调时不会出现，而 AR 语言模型则不会存在这个问题。</li>
<li><strong>上下文依赖</strong>：AR 语言模型仅考虑了词条左侧的上下文，而 BERT 则可以捕获两个方向的上下文。</li>
</ol>
<p>为了利用 AR 语言模型和 BERT 的优点，XLNet 提出了排序语言模型。对于一个长度为 <code>$T$</code> 序列 <code>$\mathbf{x}$</code>，共有 <code>$T!$</code> 种不同的方式进行 AR 分解，如果模型共享不同分解顺序的参数，那么模型就能学习到两侧所有位置的信息。令 <code>$\mathcal{Z}_T$</code> 为长度为 <code>$T$</code> 的索引序列 <code>$\left[1, 2, \dotsc, T\right]$</code> 的所有可能排列，<code>$z_t$</code> 和 <code>$\mathbf{z}_{&lt;t}$</code> 分别表示一个排列 <code>$\mathbf{z} \in \mathcal{Z}_T$</code> 第 <code>$t$</code> 个和前 <code>$t-1$</code> 个元素。则排列语言模型的优化目标为：</p>
<p><code>$$ \max_{\theta} \quad \mathbb{E}_{\mathbf{z} \sim \mathcal{Z}_{T}}\left[\sum_{t=1}^{T} \log p_{\theta}\left(x_{z_{t}} | \mathbf{x}_{\mathbf{z}_{&lt;t}}\right)\right] $$</code></p>
<p>根据标准的 Transformer，下一个词条的分布 <code>$p_{\theta}\left(X_{z_{t}} | \mathbf{x}_{\mathbf{z}&lt;t}\right)$</code> 为：</p>
<p><code>$$ p_{\theta}\left(X_{z_{t}} = x | \mathbf{x}_{\mathbf{z}&lt;t}\right)=\frac{\exp \left(e(x)^{\top} h_{\theta}\left(\mathbf{x}_{\mathbf{z}&lt;t}\right)\right)}{\sum_{x^{\prime}} \exp \left(e\left(x^{\prime}\right)^{\top} h_{\theta}\left(\mathbf{x}_{\mathbf{z}&lt;t}\right)\right)} $$</code></p>
<p>其中，<code>$h_{\theta}\left(\mathbf{x}_{\mathbf{z}&lt;t}\right)$</code> 表示通过共享的 Transformer 产生的 <code>$\mathbf{X}_{\mathbf{Z}&lt;t}$</code> 的隐含表示。该表示并不依赖于所预测的位置，为了避免这个问题，我们将位置 <code>$z_t$</code> 加入到模型中：</p>
<p><code>$$ p_{\theta}\left(X_{z_{t}}=x | \mathbf{x}_{z_{&lt;t}}\right)=\frac{\exp \left(e(x)^{\top} g_{\theta}\left(\mathbf{x}_{\mathbf{z}&lt;t}, z_{t}\right)\right)}{\sum_{x^{\prime}} \exp \left(e\left(x^{\prime}\right)^{\top} g_{\theta}\left(\mathbf{x}_{\mathbf{z}&lt;t}, z_{t}\right)\right)} $$</code></p>
<p>对于 <code>$g_{\theta}\left(\mathbf{x}_{\mathbf{z}&lt;t}, z_{t}\right)$</code> 进行建模需要满足如下两个要求：</p>
<ol>
<li>预测 <code>$x_{z_t}$</code> 时，<code>$g_{\theta}\left(\mathbf{x}_{\mathbf{z}&lt;t}, z_{t}\right)$</code> 只能使用位置信息 <code>$z_t$</code> 而不能使用内容信息 <code>$x_{z_t}$</code>。</li>
<li>在预测 <code>$x_{z_t}$</code> 之后的词条时，<code>$g_{\theta}\left(\mathbf{x}_{\mathbf{z}&lt;t}, z_{t}\right)$</code> 又必须包含 <code>$x_{z_t}$</code> 的语义信息。</li>
</ol>
<p>为了解决这个问题，XLNet 提供了两种隐含表示：</p>
<ol>
<li>内容隐含表示 <code>$h_{\theta}\left(\mathbf{x}_{\mathbf{z} \leq t}\right)$</code>，简写为 <code>$h_{z_t}$</code>，它和标准的 Transformer 一样，既编码上下文也编码 <code>$x_{z_t}$</code> 的内容。</li>
<li>查询隐含表示 <code>$g_{\theta}\left(\mathbf{x}_{\mathbf{z}&lt;t}, z_{t}\right)$</code>，简写为 <code>$g_{z_t}$</code>，它仅编码上下文信息 <code>$\mathbf{X}_{\mathbf{Z}&lt;t}$</code> 和位置信息 <code>$z_t$</code>，不编码内容 <code>$x_{z_t}$</code>。</li>
</ol>
<p>模型的整个计算过程如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/xlnet.png" class="lazyload"/>
  
</figure>
<p>虽然排列语言模型有很多优点，但是由于计算量很大，模型很难进行优化，因此我们通过仅预测一个句子后面的一些词条解决这个问题。将 <code>$\mathbf{z}$</code> 分为两部分：非目标子序列 <code>$\mathbf{z}_{\leq c}$</code> 和目标子序列 <code>$\mathbf{z}_{&gt;c}$</code>，其中 <code>$c$</code> 为切分点。同时会设置一个超参数 <code>$K$</code>，表示仅 <code>$1 / K$</code> 的词条会被预测，有 <code>$|\mathbf{z}| /(|\mathbf{z}|-c) \approx K$</code>。对于未被选择的词条，其查询隐状态无需被计算，从而节省计算时间和资源。</p>
<h3 id="mass-2019">MASS (2019) <sup id="fnref1:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup></h3>
<p>MASS 是一个专门针对序列到序列的自然语言任务设计的预训练方法，对于一个给定的原始句子 <code>$x \in \mathcal{X}$</code>，令 <code>$x^{\setminus u:v}$</code> 表示将 <code>$x$</code> 从 <code>$u$</code> 到 <code>$v$</code> 位置进行遮挡处理，<code>$k = v - u + 1$</code> 为被遮挡词条的个数，<code>$x^{u:v}$</code> 为从 <code>$u$</code> 到 <code>$v$</code> 位置被遮挡的部分。MASS 利用被遮挡的序列 <code>$x^{\setminus u:v}$</code> 预测被遮挡的部分 <code>$x^{u:v}$</code>，目标函数的对数似然如下：</p>
<p><code>$$ \begin{aligned} L(\theta ; \mathcal{X}) &amp;=\frac{1}{|\mathcal{X}|} \Sigma_{x \in \mathcal{X}} \log P\left(x^{u: v} | x^{\setminus u: v} ; \theta\right) \\ &amp;=\frac{1}{|\mathcal{X}|} \Sigma_{x \in \mathcal{X}} \log \prod_{t=u}^{v} P\left(x_{t}^{u: v} | x_{&lt;t}^{u: v}, x^{\setminus u: v} ; \theta\right) \end{aligned} $$</code></p>
<p>对于一个具有 8 个词条的序列，<code>$x_3 x_4 x_5 x_6$</code> 被遮挡的示例如下：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/mass.png" class="lazyload"/>
  
</figure>
<p>模型仅预测遮挡的部分 <code>$x_3 x_4 x_5 x_6$</code>，对于解码器中位置 <code>$4-6$</code> 利用 <code>$x_3 x_4 x_5$</code> 作为输入，利用特殊遮挡符号 <code>$\left[\mathbb{M}\right]$</code> 作为其他位置的输入。对于不同长度 <code>$k$</code>，MASS 包含了上文中提到的两种预训练模型：</p>
<table>
  <thead>
      <tr>
          <th>长度</th>
          <th>概率</th>
          <th>模型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>$k=1$</code></td>
          <td><code>$P\left(x^{u} \mid x^{\setminus u} ; \theta\right)$</code></td>
          <td>masked LM in BERT</td>
      </tr>
      <tr>
          <td><code>$k=m$</code></td>
          <td><code>$P\left(x^{1:m} \mid x^{\setminus 1:m} ; \theta\right)$</code></td>
          <td>masked LM in GPT</td>
      </tr>
      <tr>
          <td><code>$k \in \left(1, m\right)$</code></td>
          <td><code>$P\left(x^{u:v} \mid x^{\setminus u:v} ; \theta\right)$</code></td>
          <td>两种之间</td>
      </tr>
  </tbody>
</table>
<p>对于不同 <code>$k$</code> 值，实验发现当 <code>$k$</code> 处于 <code>$m$</code> 的 <code>$50\%$</code> 至 <code>$70\%$</code> 之间时下游任务性能最优。</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/mass-k.png" class="lazyload"/>
  
</figure>
<p>当 <code>$k = 0.5 m$</code> 时，MASS 可以很好地平衡编码器和解码器的预训练。过度地偏向编码器（<code>$k=1$</code>，masked LM in BERT）和过度地偏向解码器（<code>$k=m$</code>，masked LM in GPT）均不能在下游的自然语言生成任务中取得很好的效果。</p>
<h3 id="roberta-2019">RoBERTa (2019) <sup id="fnref1:18"><a href="#fn:18" class="footnote-ref" role="doc-noteref">18</a></sup></h3>
<p>RoBERTa 主要围绕 BERT 进行了如下改进：</p>
<ol>
<li>模型采用了动态遮罩，不同于原始 BERT 中对语料预先进行遮罩处理，RoBERTa 在 40 轮训练过程中采用了 10 种不同的遮罩。</li>
<li>模型去掉了 NSP 任务，发现可以略微提升下游任务的性能。</li>
<li>模型采用了更大的训练数据和更大的 Batch 大小。</li>
<li>原始 BERT 采用一个 30K 的 BPE 词表，RoBERTa 采用了一个更大的 50K 的词表 <sup id="fnref:28"><a href="#fn:28" class="footnote-ref" role="doc-noteref">28</a></sup>。</li>
</ol>
<h3 id="bart-2019">BART (2019) <sup id="fnref1:20"><a href="#fn:20" class="footnote-ref" role="doc-noteref">20</a></sup></h3>
<p>BART 采用了一个标准的 Seq2Seq Transformer 结构，类似 GPT 将 ReLU 激活函数替换为 GeLUs。对于基线模型，采用了一个 6 层的编码和解码器，对于更大模型采用了 12 层的结构。相比于 BERT 的架构主要有以下两点不同：</p>
<ol>
<li>解码器的每一层叠加了对编码器最后一个隐含层的注意力。</li>
<li>BERT 在预测之前采用了一个前馈的网络，而 BART 没有。</li>
</ol>
<p>BART 采用了最小化破坏后的文档和原始文档之间的重构误差的方式进行预训练。不同于其他的一些去噪自编码器，BART 可以使用任意类型的文档破坏方式。极端情况下，当源文档的所有信息均丢失时，BART 就等价与一个语言模型。BART 中采用的文本破坏方式有：字符遮罩，字符删除，文本填充，句子重排，文档旋转，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/bart-transformations.png" class="lazyload"/>
  
</figure>
<h3 id="t5-2019">T5 (2019) <sup id="fnref1:17"><a href="#fn:17" class="footnote-ref" role="doc-noteref">17</a></sup></h3>
<p>T5（Text-to-Text Transfer Transformer） 提出了一种 text-to-text 的框架，旨在利用相同的模型，损失函数和超参数等对机器翻译，文档摘要，问答和分类（例如：情感分析）等任务进行统一建模。我们甚至可以利用 T5 通过预测一个数字的文本表示而不是数字本身来建模一个回归任务。模型及其输入输出如下图所示：</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/t5-text-to-text-framework.gif" class="lazyload"/>
  
</figure>
<p>Google 的这项研究并不是提出一种新的方法，而是从全面的视角来概述当前 NLP 领域迁移学习的发展现状。T5 还公开了一个名为 C4（Colossal Clean Crawled Corpus）的数据集，该数据集是一个比 Wikipedia 大两个数量级的 Common Crawl 的清洗后版本的数据。更多模型的细节请参见源论文和 Google 的 <a href="https://ai.googleblog.com/2020/02/exploring-transfer-learning-with-t5.html">官方博客</a>。</p>
<h3 id="ernie-baidu-2019">ERNIE (Baidu, 2019) <sup id="fnref:29"><a href="#fn:29" class="footnote-ref" role="doc-noteref">29</a></sup> <sup id="fnref:30"><a href="#fn:30" class="footnote-ref" role="doc-noteref">30</a></sup></h3>
<p>ERNIE 1.0 <sup id="fnref1:29"><a href="#fn:29" class="footnote-ref" role="doc-noteref">29</a></sup> 通过建模海量数据中的词、实体及实体关系，学习真实世界的语义知识。相较于 BERT 学习原始语言信号，ERNIE 直接对先验语义知识单元进行建模，增强了模型语义表示能力。例如：</p>
<p><code>BERT ：哈 [mask] 滨是 [mask] 龙江的省会，[mask] 际冰 [mask] 文化名城。</code><br>
<code>ERNIE：[mask] [mask] [mask] 是黑龙江的省会，国际 [mask] [mask] 文化名城。</code></p>
<p>在 BERT 模型中，我们通过『哈』与『滨』的局部共现，即可判断出『尔』字，模型没有学习与『哈尔滨』相关的任何知识。而 ERNIE 通过学习词与实体的表达，使模型能够建模出『哈尔滨』与『黑龙江』的关系，学到『哈尔滨』是 『黑龙江』的省会以及『哈尔滨』是个冰雪城市。</p>
<p>训练数据方面，除百科类、资讯类中文语料外，ERNIE 还引入了论坛对话类数据，利用 DLM（Dialogue Language Model）建模 Query-Response 对话结构，将对话 Pair 对作为输入，引入 Dialogue Embedding 标识对话的角色，利用 Dialogue Response Loss 学习对话的隐式关系，进一步提升模型的语义表示能力。</p>
<p>ERNIE 2.0 <sup id="fnref1:30"><a href="#fn:30" class="footnote-ref" role="doc-noteref">30</a></sup> 是基于持续学习的语义理解预训练框架，使用多任务学习增量式构建预训练任务。ERNIE 2.0 中，新构建的预训练任务类型可以无缝的加入训练框架，持续的进行语义理解学习。 通过新增的实体预测、句子因果关系判断、文章句子结构重建等语义任务，ERNIE 2.0 语义理解预训练模型从训练数据中获取了词法、句法、语义等多个维度的自然语言信息，极大地增强了通用语义表示能力。</p>
<figure>
  <img data-src="/images/cn/2020-03-28-pre-trained-model-for-nlp/ernie-2-framework.png" class="lazyload"/>
  
</figure>
<h3 id="state-of-art">State-of-Art</h3>
<p>NLP 任务的 State-of-Art 模型详见：</p>
<ul>
<li><a href="https://gluebenchmark.com/leaderboard">GLUE Leaderboard</a></li>
<li><a href="https://super.gluebenchmark.com/leaderboard">SuperGLUE Leaderboard</a></li>
<li><a href="https://rajpurkar.github.io/SQuAD-explorer/">SQuAD</a></li>
<li><a href="https://nlpprogress.com/">NLP-progress</a></li>
<li><a href="https://www.cluebenchmarks.com/">中文任务基准测评</a></li>
</ul>


<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Qiu, X., Sun, T., Xu, Y., Shao, Y., Dai, N., &amp; Huang, X. (2020). Pre-trained Models for Natural Language Processing: A Survey. <em>ArXiv:2003.08271 [Cs]</em>. <a href="http://arxiv.org/abs/2003.08271">http://arxiv.org/abs/2003.08271</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Mikolov, T., Sutskever, I., Chen, K., Corrado, G. S., &amp; Dean, J. (2013). Distributed representations of words and phrases and their compositionality. In <em>Advances in neural information processing systems</em> (pp. 3111-3119).&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Pennington, J., Socher, R., &amp; Manning, C. D. (2014, October). Glove: Global vectors for word representation. In <em>Proceedings of the 2014 conference on empirical methods in natural language processing (EMNLP)</em> (pp. 1532-1543).&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>McCann, B., Bradbury, J., Xiong, C., &amp; Socher, R. (2017). Learned in translation: Contextualized word vectors. In <em>Advances in Neural Information Processing Systems</em> (pp. 6294-6305).&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Peters, M. E., Neumann, M., Iyyer, M., Gardner, M., Clark, C., Lee, K., &amp; Zettlemoyer, L. (2018). Deep contextualized word representations. <em>arXiv preprint arXiv:1802.05365.</em>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Radford, A., Narasimhan, K., Salimans, T., &amp; Sutskever, I. (2018). Improving language understanding by generative pre-training. <em>URL <a href="https://openai.com/blog/language-unsupervised/">https://openai.com/blog/language-unsupervised/</a></em>.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Devlin, J., Chang, M. W., Lee, K., &amp; Toutanova, K. (2018). Bert: Pre-training of deep bidirectional transformers for language understanding. <em>arXiv preprint arXiv:1810.04805.</em>&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Kim, Y. (2014). Convolutional Neural Networks for Sentence Classification. In <em>Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP)</em> (pp. 1746-1751).&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Hochreiter, S., &amp; Schmidhuber, J. (1997). Long short-term memory. <em>Neural computation</em>, 9(8), 1735-1780.&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Chung, J., Gulcehre, C., Cho, K., &amp; Bengio, Y. (2014). Empirical evaluation of gated recurrent neural networks on sequence modeling. <em>arXiv preprint arXiv:1412.3555.</em>&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Socher, R., Perelygin, A., Wu, J., Chuang, J., Manning, C. D., Ng, A. Y., &amp; Potts, C. (2013). Recursive deep models for semantic compositionality over a sentiment treebank. In <em>Proceedings of the 2013 conference on empirical methods in natural language processing</em> (pp. 1631-1642).&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p>Tai, K. S., Socher, R., &amp; Manning, C. D. (2015). Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks. In <em>Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing (Volume 1: Long Papers)</em> (pp. 1556-1566).&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:13">
<p>Marcheggiani, D., Bastings, J., &amp; Titov, I. (2018). Exploiting Semantics in Neural Machine Translation with Graph Convolutional Networks. In <em>Proceedings of the 2018 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 2 (Short Papers)</em> (pp. 486-492).&#160;<a href="#fnref:13" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:14">
<p>Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., &hellip; &amp; Polosukhin, I. (2017). Attention is all you need. In <em>Advances in neural information processing systems</em> (pp. 5998-6008).&#160;<a href="#fnref:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref2:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:15">
<p>Devlin, J., Chang, M. W., Lee, K., &amp; Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. In <em>Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long and Short Papers)</em> (pp. 4171-4186).&#160;<a href="#fnref:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:16">
<p>Song, K., Tan, X., Qin, T., Lu, J., &amp; Liu, T. Y. (2019). MASS: Masked Sequence to Sequence Pre-training for Language Generation. In <em>International Conference on Machine Learning</em> (pp. 5926-5936).&#160;<a href="#fnref:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:17">
<p>Raffel, C., Shazeer, N., Roberts, A., Lee, K., Narang, S., Matena, M., &hellip; &amp; Liu, P. J. (2019). Exploring the limits of transfer learning with a unified text-to-text transformer. <em>arXiv preprint arXiv:1910.1068</em>&#160;<a href="#fnref:17" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:17" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:18">
<p>Liu, Y., Ott, M., Goyal, N., Du, J., Joshi, M., Chen, D., &hellip; &amp; Stoyanov, V. (2019). Roberta: A robustly optimized bert pretraining approach. <em>arXiv preprint arXiv:1907.11692.</em>&#160;<a href="#fnref:18" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:18" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:19">
<p>Yang, Z., Dai, Z., Yang, Y., Carbonell, J., Salakhutdinov, R. R., &amp; Le, Q. V. (2019). Xlnet: Generalized autoregressive pretraining for language understanding. In <em>Advances in neural information processing systems</em> (pp. 5754-5764).&#160;<a href="#fnref:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:20">
<p>Lewis, M., Liu, Y., Goyal, N., Ghazvininejad, M., Mohamed, A., Levy, O., &hellip; &amp; Zettlemoyer, L. (2019). Bart: Denoising sequence-to-sequence pre-training for natural language generation, translation, and comprehension. <em>arXiv preprint arXiv:1910.13461.</em>&#160;<a href="#fnref:20" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:20" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:21">
<p>Saunshi, N., Plevrakis, O., Arora, S., Khodak, M., &amp; Khandeparkar, H. (2019). A Theoretical Analysis of Contrastive Unsupervised Representation Learning. In <em>International Conference on Machine Learning</em> (pp. 5628-5637).&#160;<a href="#fnref:21" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:22">
<p>Tenney, I., Das, D., &amp; Pavlick, E. (2019). BERT Rediscovers the Classical NLP Pipeline. In <em>Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics</em> (pp. 4593-4601).&#160;<a href="#fnref:22" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:23">
<p>图片来源：http://www.realworldnlpbook.com/blog/improving-sentiment-analyzer-using-elmo.html&#160;<a href="#fnref:23" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:24">
<p>Wu, Y., Schuster, M., Chen, Z., Le, Q. V., Norouzi, M., Macherey, W., &hellip; &amp; Klingner, J. (2016). Google&rsquo;s neural machine translation system: Bridging the gap between human and machine translation. <em>arXiv preprint arXiv:1609.08144.</em>&#160;<a href="#fnref:24" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:25">
<p>Dong, L., Yang, N., Wang, W., Wei, F., Liu, X., Wang, Y., &hellip; &amp; Hon, H. W. (2019). Unified language model pre-training for natural language understanding and generation. In <em>Advances in Neural Information Processing Systems</em> (pp. 13042-13054).&#160;<a href="#fnref:25" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:26">
<p>Dai, Z., Yang, Z., Yang, Y., Carbonell, J. G., Le, Q., &amp; Salakhutdinov, R. (2019, July). Transformer-XL: Attentive Language Models beyond a Fixed-Length Context. In <em>Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics</em> (pp. 2978-2988).&#160;<a href="#fnref:26" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:27">
<p>Al-Rfou, R., Choe, D., Constant, N., Guo, M., &amp; Jones, L. (2019). Character-level language modeling with deeper self-attention. In <em>Proceedings of the AAAI Conference on Artificial Intelligence</em> (Vol. 33, pp. 3159-3166).&#160;<a href="#fnref:27" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:28">
<p>Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., &amp; Sutskever, I. (2019). Language models are unsupervised multitask learners. <em>URL <a href="https://openai.com/blog/better-language-models/">https://openai.com/blog/better-language-models/</a></em>.&#160;<a href="#fnref:28" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:29">
<p>Sun, Y., Wang, S., Li, Y., Feng, S., Chen, X., Zhang, H., &hellip; &amp; Wu, H. (2019). Ernie: Enhanced representation through knowledge integration. <em>arXiv preprint arXiv:1904.09223.</em>&#160;<a href="#fnref:29" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:29" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:30">
<p>Sun, Y., Wang, S., Li, Y., Feng, S., Tian, H., Wu, H., &amp; Wang, H. (2019). Ernie 2.0: A continual pre-training framework for language understanding. <em>arXiv preprint arXiv:1907.12412.</em>&#160;<a href="#fnref:30" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:30" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>ToB 产品用户权限 (User Privileges of ToB Products)</title><link>https://zeqiang.fun/cn/2019/11/user-privileges-of-2b-products/</link><pubDate>Sun, 24 Nov 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/11/user-privileges-of-2b-products/</guid><description><![CDATA[
        <p>用户权限在产品中是一个最基础的功能，它决定了用户在产品中能做些什么。虽然用户对权限的感知并不是很明显，但作为一个系统的基础功能，用户权限可以说会影响产品设计和实现的方方面面。对于不同类型的产品，用户权限的设计也略有差异，ToC 产品中用户之间相对独立，所需要考虑的问题会比 ToB 产品简单不少，本文仅针对 ToB 产品的用户权限给出一些思考。</p>
<h2 id="role-based-access-control">Role-Based Access Control</h2>
<p>RBAC (Role-Based Access Control，基于角色的访问控制) 是一种已经广泛应用于各种管理系统的权限管理模型 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。在 RBAC 中，操作权限与角色之间建立关联，再通过在角色与用户之间建立关联来完成对用户的授权，大大提高了权限控制的灵活性。在 RBAC 中包含几个关键的概念：</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>资源</strong>：所有可以访问的对象，从交互角度可以理解为：页面，按钮等一切可以操作的对象，从后台接口角度可以理解为：以 Rest 接口为例，资源即为每个 URI。</div>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>权限</strong>：获取资源的方式，简单理解即为资源的增删改查。</div>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>角色</strong>：一个授权等级的工作职位或职称。</div>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>用户</strong>：一名使用者或自动代理人。</div>
<p>资源，权限，角色和用户之间的关系如下图所示：</p>
<figure>
  <img data-src="/images/cn/2019-11-24-user-privileges-of-2b-products/r-p-r-u-relations.png" class="lazyload"/>
  <figcaption><p class="figcaption">资源-权限-角色-用户关系</p></figcaption>
</figure>
<p>「资源」和「权限」之间为多对多的关系，「权限」和「角色」之间为多对多的关系，「角色」和「用户」之间亦为多对多的关系。</p>
<h3 id="rbac0">RBAC0</h3>
<p>RBAC0 为 RBAC 中最简单最基础的权限模型，包含了用户，角色，权限之间的关系，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2019-11-24-user-privileges-of-2b-products/RBAC0.png" class="lazyload"/>
  <figcaption><p class="figcaption">RBAC0</p></figcaption>
</figure>
<p>用户，角色和权限之间都可以是多对多的关系，在系统分工简单权限清晰明确的情况下，用户和角色之间也可能是多对一的关系。</p>
<h3 id="rbac1">RBAC1</h3>
<p>RBAC1 在 RBAC0 的基础上引入了角色继承的概念，添加了「子角色」，上级角色可以继承下级角色所有的权限。</p>
<figure>
  <img data-src="/images/cn/2019-11-24-user-privileges-of-2b-products/RBAC1.png" class="lazyload"/>
  <figcaption><p class="figcaption">RBAC1</p></figcaption>
</figure>
<p>例如，一个公司的不同人力资源副总监分管不同的职能，因此具有不同的权限，而人力资源总监则应该具有所有人力资源副总监的权限。同时，人力资源总监也可能具有其他人力资源副总监都不具有的权限。</p>
<h3 id="rbac2">RBAC2</h3>
<p>RBAC2 在 RBAC0 的基础上增加了一些限制，引入了 SSD (Static Separation of Duty，静态职责分离) 和 DSD (Dynamic Separation of Duty，动态职责分离)。</p>
<p>SSD 主要应用于用户和角色之间，主要的约束包括：</p>
<ul>
<li>角色互斥约束：同一个用户仅能分配到互斥角色中的一个。例如：财务系统中一个用户不能同时分配会计和审计两种互斥的角色。</li>
<li>基数约束：一个用户拥有的角色是有限的，一个角色拥有的权限是有限的。</li>
<li>先决条件约束：用户在获取更高的角色之前需先拥有低级的角色。</li>
</ul>
<p>DSD 主要应用于会话和角色之间动态地限制用户及其拥有的角色和相应的权限。例如：一个用户在系统中拥有两个不同的角色，但在使用系统时只能激活其中一个角色。</p>
<h3 id="rbac3">RBAC3</h3>
<p>RBAC3 是 RBAC1 和 RBAC2 的合集，既包含了角色分层也包含了相关约束。</p>
<h2 id="权限扩展">权限扩展</h2>
<p>接下来我们探讨在 RBAC 的基础上在真实的 ToB 产品中又有哪些权限扩展，这里有时我会以「种植」业务的 SaaS 为例进行简略分析。我们将权限又分为「功能权限」和「数据权限」两个部分，如下图所示：</p>
<figure>
  <img data-src="/images/cn/2019-11-24-user-privileges-of-2b-products/function-and-data-privilege.png" class="lazyload"/>
  <figcaption><p class="figcaption">功能权限和数据权限</p></figcaption>
</figure>
<p>功能权限就是指我们具体能够干些什么，例如：育苗，除草，施肥等等。数据权限是指我们是对谁做这些事情，例如：是对北京的实验大棚除草，还是河北的生产大棚施肥。</p>
<p>ToB 产品的销售对象并非个人，而是一个组织，在此我们引入「组织」的概念：</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>组织</strong>：用户所隶属的单位，从产品销售角度限定了用户所能使用的功能。</div>
<p>对于一个组织，我们设置有「功能池」和「数据池」。功能池即组织购买的产品的功能集合，一个组织的用户所能够使用的功能受限与此，并由组织的系统管理员分配功能池中的功能。数据池即组织自己的相关数据集合，由组织的管理员对不同的员工进行分配。</p>
<figure>
  <img data-src="/images/cn/2019-11-24-user-privileges-of-2b-products/organization-functions-and-data.png" class="lazyload"/>
  <figcaption><p class="figcaption">组织功能池和数据池</p></figcaption>
</figure>
<p>对于功能权限和数据权限，均为一个树状的层级关系，以功能权限为例，层级如下图所示：</p>
<figure>
  <img data-src="/images/cn/2019-11-24-user-privileges-of-2b-products/function-tree.png" class="lazyload"/>
  <figcaption><p class="figcaption">功能权限层级关系树</p></figcaption>
</figure>
<p>功能的层级和菜单的层级有一定的对应关系，层级不易过深，这部分将在下文菜单部分详细介绍。数据的层级则需要根据产品涉及的具体业务做出相应的规范要求，例如：「根」-「种植场」-「种植区」-「种植大棚」-「种植地块」。同时，为了方便层级的扩充，在实现层面上不建议提前把所有层级固定死，可以采用父子层级的形式循环关联。</p>
<h2 id="权限分配">权限分配</h2>
<p>在介绍如何根据上述设计进行权限分配前，我们再引入一个概念，「用户组」：</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>用户组</strong>：具有相同权限的一组用户。</div>
<p>当一个组织中有大量用户需要具有相同的权限时，为了避免给每个用户分配权限导致的繁琐操作，可以将这些具有相同权限的用户归入一个用户组。对该用户组赋予一定的权限，则该用户组下面的所有用户自动具有相应权限，当需要取消其中某些用户权限时，仅需要将其从该用户组中移除即可。理论上，「用户」和「用户组」以及「用户组」和「角色」之间都可以是多对多的关系。</p>
<p>下图为一个最细力度的权限分配示意图：</p>
<figure>
  <img data-src="/images/cn/2019-11-24-user-privileges-of-2b-products/privileges-assignment.png" class="lazyload"/>
  <figcaption><p class="figcaption">权限分配</p></figcaption>
</figure>
<p>首先，我们暂时限定一个用户仅能够隶属于一个组织，因此该用户所能够具有的最多的权限即为该组织所购买的产品的功能权限和组织自己的全部数据权限。</p>
<p>其次，一个最基本的权限应该是由一个功能权限对应一个数据权限构成的。这里面的功能权限和数据权限可以是任意一个级别的，同时当具有高级别的功能权限或数据权限后，子级别的相应权限自动获得。</p>
<p>最后，权限、角色、用户组和用户之间均可以是多对多的关系。</p>
<p>虽然通过很多概念的抽象使得权限管理变得相对简单，但是由于理论上支持各种细粒度的操作，在一定程度上又会加重权限管理的运维成本。在一个 ToB 产品的前期，组织的管理员可能对整个系统不是很熟悉，这样权限管理的工作会落到内部管理员的头上。当随着产品的不断发展，组织和用户的量级不断变大，内部管理员仅需为每个组织开通一个组织管理员并赋予该组织的所有权限即可，该组织内部的权限管理则交由组织的管理员进行运维即可。</p>
<h2 id="菜单-页面和组件">菜单，页面和组件</h2>
<p>以上我们都是从业务逻辑的角度解释权限控制，接下来我们从用户交互的角度出发简单阐述一下权限控制在用户交互上的体现。下图为一个 ToB 产品的原型界面：</p>
<figure>
  <img data-src="/images/cn/2019-11-24-user-privileges-of-2b-products/menu-page-and-module.png" class="lazyload"/>
  <figcaption><p class="figcaption">菜单，页面和组件</p></figcaption>
</figure>
<p>在这个界面中，我们粗略的将其中的元素划分为三大块：「菜单」，「页面」和「组件」。这个划分并不是很准确，甚至可以说一定程度上概念之间有重叠，这样划分仅仅是为了更好的对应到上文涉及的相关概念上。</p>
<p>上文中我们提到一个角色的权限可以是任何一个级别的功能权限和数据权限的组合，同时权限、角色、用户组和用户之间都可以是多对多的关系，在此我们假设后台已经通过查询获得到了一个用户的所有权限。</p>
<h3 id="菜单">菜单</h3>
<p>菜单对应到主要是功能权限，但不是所有的功能权限都会以菜单的形式展现出来，因此我们可能还需要维护一套菜单的层级关系以及同功能权限的对应关系，或者我们直接利用功能权限的层级关系，但需要在里面补充上是否作为菜单显示的属性。具有不同权限的用户看到的菜单也会是不一样的。</p>
<p>在菜单设计时，建议的原则是层级不要过深，过深的话从交互上就会导致你的菜单像极了<a href="https://coolshell.cn/articles/17757.html">「箭头型代码」</a>。</p>
<figure>
  <img data-src="/images/cn/2019-11-24-user-privileges-of-2b-products/arrow-style-code.jpg" class="lazyload"/>
  <figcaption><p class="figcaption">箭头型代码</p></figcaption>
</figure>
<p>理论上，所有包含子功能的父功能都不代表一个具体的操作，一般仅仅是为了维护一个分组而创建的。</p>
<h3 id="页面">页面</h3>
<p>页面并不是浏览器里面的整个页面，这里我们特指不包含菜单的部分。当我们单击一个功能菜单时，对应的页面则会相应的显示出来。当然，我们知道单击按钮 (下文会提到的组件) 也可能会打开一个新的页面，这种情况我们在下文中再展开说明。当我们单击了一个叶子界别的菜单后，其实我们已经使用了一个权限，一般情况下对应的是对一个资源的查询。</p>
<h3 id="组件">组件</h3>
<p>页面中包含了操作的结果信息，一般情况下这些信息是不可交互的，同时页面中也包含了大量可以交互的地方，这里我们称其中的部分为组件，例如：按钮等。当然还有一些被我们在通常情况下也称之为的组件的元素，例如：输入框，下拉框等等，虽然他们也有交互功能，但是一般情况下他们并不会触发一个权限操作，因此这里我们就先将组件特指那些可以产生权限操作的元素。</p>
<p>一个页面可以包含多个组件，也就是说一个页面并不一定仅对应一个权限操作，可能是多个权限操作的集合。所以简单的说我们可以将功能权限设置的比菜单多一个级别，功能权限的倒数第二级对应叶子菜单，功能权限的最后一级对应页面上的组件。</p>
<h2 id="权限运维">权限运维</h2>
<p>权限的精细化管理和运维成本是相互矛盾的，想更加精细化的管理权限必然会增加运维成本。因此一个 ToB 的产品到底使用什么力度的权限管理更加合适呢？我想这没有一个准确的答案，从个人角度给出两点有参考意义的设计原则：</p>
<ol>
<li>客户需求。ToB 产品吗，最终是要拿来卖的，跪添甲方爸爸就好，甲方爸爸的需求就是合理的需求，无需质疑。说的有点过了，意思其实就是尽可能满足合理客户需求的前提下，制定合适的权限管理系统。</li>
<li>成本合算。有的时候客户也不是很清楚到底需不需要精细化的权限管理，那么问题就又抛回给了产品经理，那么我们需要考虑更多的是当下的实现成本和后期的改造成本。这是一个很现实的考虑，如果业务 KPI 压力比较大，有时候我们宁可承担更高的改造成本也会先实现一套简单的权限管理先用着。但是我个人认为，在研发资源相对充足的前提下还是尽量将权限系统设计完善，因为这是一个系统很底层的部分，甚至会影响后续所有的业务功能逻辑的设计和实现。</li>
</ol>
<p>权限运维是一个很耗费人力的工作，解决这个问题没有太好的途径。提高用户的交互体验，编写简单易懂的使用说明书，只有将一个组织的权限管理交到组织的管理员手里。才能够真正解放内部管理运维人员的工作时间。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Ferraiolo, David, Janet Cugini, and D. Richard Kuhn. &ldquo;Role-based access control (RBAC): Features and motivations.&rdquo; <em>Proceedings of 11th annual computer security application conference</em>. 1995.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>京东数科 HIPO 学习之旅 (JDD HIPO Learning Journey)</title><link>https://zeqiang.fun/cn/2019/10/jdd-hipo-learning-journey/</link><pubDate>Mon, 28 Oct 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/10/jdd-hipo-learning-journey/</guid><description><![CDATA[
        <p>今年有幸参加了京东数科的 HIPO 培训课程，整个学习之旅可谓收货满满。由于出差没能参加 8 月份的开营仪式，看到小伙伴们各种拓展，还能自己做饭，感觉错过了展示范小厨的大好机会。同样也错过了报名的「领导力」的课程，最终换到了「情商」和「沟通」两门课程学习。</p>
<h2 id="情商">情商</h2>
<p>自认为不是一个情商很高的人，果然开课前利用 6 秒钟情商 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 评测 (SEI) 评测之后的结果不甚理想。6 秒钟情商模型将情商划分为 3 个阶段：自我认知，包括：认识情绪、识别情绪模式；自我选择，包括：运用因果思维、驾驭情绪、运用内在动力、修炼乐观思维；自我超越，包括：增强同理心、追求超我目标。</p>
<p><img src="/images/cn/2019-10-28-jdd-hipo-learning-journey/six-seconds.png" alt="6 秒钟情商"></p>
<p>在自我认知的认识情绪和识别情绪模式上仍处于起步阶段，其他部分基本处于稳定阶段和熟练阶段。这完全符合我对自己情商的认知，不善于发现别人情绪的变化，一旦发现还是能够用自己擅长的理性思维解决问题的。不过认识情绪是整个环节的第一步，如果这一步都不能做得很好，就算后面能力不错，也很难成为一个高情商的人。</p>
<p>情绪是很容易变向极端，但比较难恢复平静的，下图就是我们测试自己当前的情绪状况，颜色越紫表示越紧张压力越大，颜色越蓝表示越平静。</p>
<p><img src="/images/cn/2019-10-28-jdd-hipo-learning-journey/emotion-test.jpg" alt="情绪测试"></p>
<p>课程上学习到了一个很好用的情绪管理的工具，TFA 卡片。TFA 卡片是一个实践情绪管理的工具，让我们尝试从T (Think，思考)，F (Feel，感受) 和 A (Act，行动) 三个角度对一个事件进行复盘实践。</p>
<p><img src="/images/cn/2019-10-28-jdd-hipo-learning-journey/tfa.png" alt="TFA 卡片"></p>
<p>情商课程让我认识到了情绪是应该去管理的，而不是去控制的，因为控制的情绪总会有爆发的一天。希望通过坚持不断地实践，努力成为一个高情商的人。</p>
<h2 id="沟通">沟通</h2>
<p>在我的认知范围内，只要将事情表达清楚就可以达成高效的沟通，然而现实并不是这样的，沟通课程告诉了我更多需要注意的地方。首先通过一个 DISC 模型 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 从两个维度将人的特质划分为四种，两个维度分别是：更关注事物本身还是人际关系和倾向快 (直接) 还是慢 (间接)。</p>
<p><img src="/images/cn/2019-10-28-jdd-hipo-learning-journey/disc.png" alt="DISC"></p>
<p>不同特质的人的性格也截然不同：</p>
<table>
  <thead>
      <tr>
          <th>特质</th>
          <th>特点</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>D - 掌控型</td>
          <td>爱冒险的、有竞争力的、大胆的、直接的、果断的、创新的、坚持不懈的、问题解决者、自我激励者</td>
      </tr>
      <tr>
          <td>I - 影响型</td>
          <td>有魅力的、自信的、有说服力的、热情的、鼓舞人心的、乐观的、令人信服的、受欢迎的、好交际的、可信赖的</td>
      </tr>
      <tr>
          <td>S - 沉稳型</td>
          <td>友善的、亲切的、好的倾听者、有耐心的、放松的、热诚的、稳定的、团队合作者、善解人意的、稳健的</td>
      </tr>
      <tr>
          <td>C - 严谨型</td>
          <td>准确的、有分析力的、谨慎的、谦恭的、圆滑的、善于发现事实、高标准、成熟的、有耐心的、严谨的</td>
      </tr>
  </tbody>
</table>
<p>通过测评分析，我是一个内在 C/S，外在正在向 D/S 转变的特征的人。评测结果也很符合当下的我，之前作为算法工程师更习惯用数据说话，但现在向一个产品经理的方向转变，需要更多的掌控和沟通。</p>
<p><img src="/images/cn/2019-10-28-jdd-hipo-learning-journey/communication-class.jpg" alt="沟通课程"></p>
<p>C 类特质的人有严谨的优势，但这个优势也需要根据不同的场景进行适当地调整，这样才能够避免让自己的优势变成劣势，反而影响与他人沟通的效率。在沟通的过程中，有时候大家的最终目标其实相差并不是很远，只是不同类型特质的人看待事情的角度不尽相同而已。如果在沟通过程中大家不考虑自己和沟通者的不同性格特质的话，就很容易触碰到大家的一些敏感区域，从而造成无效的沟通。因此无论是对上，对下还是平级沟通，我们都需要根据沟通者的性格特质采用相应的沟通技巧，进而高效地达成我们的沟通目的。</p>
<h2 id="库布其沙漠-50-公里挑战">库布其沙漠 50 公里挑战</h2>
<p>课程的最后安排了一个终极挑战「库布其沙漠 50 公里挑战」，对于我这个还比较喜欢出去浪的人是一定不能错过的。比赛之前我们做了些功课，我作为整个队伍的领航员走在最前面负责探路和制定路线，队长则负责在最后面压阵避免有同学掉队。组内的男生分担了两位女生的负重，当然最感动的是两位小姐姐帮我们买了鞋垫神奇 - 姨妈巾。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-01.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-01.jpg" alt="库布其沙漠"/>
    </div>
    <a href="/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-01.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>库布其沙漠</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-02.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-02.jpg" alt="库布其沙漠"/>
    </div>
    <a href="/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-02.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>库布其沙漠</p>
      </figcaption>
  </figure>
</div>


</div>

<p>我们一路穿越山丘，峡谷和小河，还有我们巨帅的越野保障车。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-03.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-03.jpg" alt="库布其沙漠 - 峡谷"/>
    </div>
    <a href="/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-03.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>库布其沙漠 - 峡谷</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-04.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-04.jpg" alt="库布其沙漠 - 越野车"/>
    </div>
    <a href="/images/cn/2019-10-28-jdd-hipo-learning-journey/kubuqi-04.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>库布其沙漠 - 越野车</p>
      </figcaption>
  </figure>
</div>


</div>

<p>挑战赛一共两天的行程，第一天路线比较固定，大家基本拼的就是体力和团队凝聚力了，队里的两位小姐姐超级给力，一路我们都保持了很好的队形，最终拿下了第一天赛程的冠军，领先第二名 20 多分钟。第二天赛程给定的建议路线有一个折线，前一晚我们计划选择走更近的直线，但中途被呼叫让我们走回预定线路上去。虽然我们已经提前确定规则并没有要求必须走预定线路，但是由于对讲机里不断呼叫，我们还是回到了预定路线上去。但比赛最后才发现，只有我们回到了预定路线上，其他队伍并没有回去，所以第二赛段我们只拿到了第二，被昨天的第二名领先了 15 分钟。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-28-jdd-hipo-learning-journey/award-ceremony.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-28-jdd-hipo-learning-journey/award-ceremony.jpg" alt="颁奖仪式"/>
    </div>
    <a href="/images/cn/2019-10-28-jdd-hipo-learning-journey/award-ceremony.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>颁奖仪式</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-28-jdd-hipo-learning-journey/prize.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-28-jdd-hipo-learning-journey/prize.jpg" alt="第一名"/>
    </div>
    <a href="/images/cn/2019-10-28-jdd-hipo-learning-journey/prize.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>第一名</p>
      </figcaption>
  </figure>
</div>


</div>

<p>庆幸的是两个赛段的总成绩我们还是第一名，当然第一名也还是要有付出的，颁奖仪式上我们用奖杯每人豪饮了一大碗啤酒，足足有一瓶多的一碗。整个比赛下来我感觉三点很重要：目标清晰，分工明确，齐心协力。我们在一开始就朝着冠军的奖杯出发的，我负责领航，队长负责断后，所有人相互帮助，一同朝着目标努力前进。当然，在规则允许的情况下，再多一分坚持己见就更好了。</p>
<h2 id="生活因你而火热">生活因你而火热</h2>
<p>最后放一下京东数科的 MV 吧，我有出境 6 秒哦 (02:27 - 02:32)，没错，打哈欠和撸猫的就是我，相信我，养猫一定比养猪容易，还有种菜 &#x1f603;。</p>


<video class="video-js vjs-big-play-centered vjs-fluid"
       data-setup='{
         "controls": true,
         "poster": "/images/cn/2019-10-28-jdd-hipo-learning-journey/生活因你而火热.png", 
         "autoplay": false,
         "preload": "auto",
         "loop": false
       }'>
  <source src="//dpv.videocc.net/c8f53742da/4/c8f53742dac09ea5574adfd949095164_3.mp4" type="video/mp4"></source>
  
  
  <p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
</video>



<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://cn.6seconds.org/about">https://cn.6seconds.org/about</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://en.wikipedia.org/wiki/DISC_assessment">https://en.wikipedia.org/wiki/DISC_assessment</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>杭州和东京之旅 (Tour of Hangzhou and Tokyo)</title><link>https://zeqiang.fun/cn/2019/10/tour-of-hangzhou-and-tokyo/</link><pubDate>Mon, 14 Oct 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/10/tour-of-hangzhou-and-tokyo/</guid><description><![CDATA[
        <p>换完工作方向后的第一个假期，决定好好调整一下，休个小长假，浪完这一圈好收收心，再全身心地投入到后面的工作中去。之前一直也没有陪家里人出去玩玩，父母年岁不小了，所以选择避开十一前面，四号错峰到了杭州，妥妥的人少了不少。</p>


<div class="gallery  caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  
<link rel="stylesheet" href="/css/photoswipe.css" />
<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/west-lake-1.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/west-lake-1.jpg" alt="西湖"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/west-lake-1.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>西湖</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/west-lake-2.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/west-lake-2.jpg" alt="西湖"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/west-lake-2.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>西湖</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/west-lake-3.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/west-lake-3.jpg" alt="西湖"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/west-lake-3.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>西湖</p>
      </figcaption>
  </figure>
</div>


</div>

<p>游西湖的时候下着零星的小雨，烟雨朦胧中的西湖别有一番韵味。想起有一日北京大雨，开完会坐在出租车上还哼起了“西湖的水，我的泪，我情愿和你化作一团火焰&hellip;..”。</p>
<p>陪着家人玩儿完，自己一个人跑去了日本，因为剩下的年假也不长了，就选择只停留在东京先。下了飞机，打车到酒店，我擦，出租车你敢不敢再贵点儿。好吧，我承认出来之前没做任何攻略，佛系浪吧 &#x1f602;。</p>
<p><img src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/taxi-fee.png" alt="死贵的出租车"></p>
<p>回程本想做地铁去机场的，结果各种原因又只能打车，就眼瞅着计价器越跳越多，越跳越多 &#x1f631;，后面再吐槽这一段经历。住在了新宿区歌舞伎町附近，夜晚的新宿很是热闹，不得不说东京的住宿也是贵的不要不要的。</p>
<p><img src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/shinjuku-night.jpg" alt="新宿区的夜晚"></p>
<p>没有攻略就前一天晚上简单的查了查，第一天到了必打卡的浅草寺，顺着上野公园，又去到了东京国立博物馆。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/sensoji-temple-1.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/sensoji-temple-1.jpg" alt="浅草寺"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/sensoji-temple-1.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>浅草寺</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/sensoji-temple-2.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/sensoji-temple-2.jpg" alt="浅草寺"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/sensoji-temple-2.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>浅草寺</p>
      </figcaption>
  </figure>
</div>


</div>



<div class="gallery  caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-national-museum.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-national-museum.jpg" alt="东京国立博物馆"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-national-museum.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>东京国立博物馆</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/shinobazu-pond.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/shinobazu-pond.jpg" alt="不忍池"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/shinobazu-pond.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>不忍池</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/ueno-toshogu-shrine.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/ueno-toshogu-shrine.jpg" alt="上野东照宫"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/ueno-toshogu-shrine.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>上野东照宫</p>
      </figcaption>
  </figure>
</div>


</div>

<p>天气还是很给力的，第二天依旧闲逛，在东京大学里面转了一圈愣是没转到安田讲堂那边，不过在三四郎池旁边抓拍到应该是一只小蜥蜴吧。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/the-university-of-tokyo-building.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/the-university-of-tokyo-building.jpg" alt="东京大学"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/the-university-of-tokyo-building.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>东京大学</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/the-university-of-tokyo-lizard.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/the-university-of-tokyo-lizard.jpg" alt="小蜥蜴"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/the-university-of-tokyo-lizard.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>小蜥蜴</p>
      </figcaption>
  </figure>
</div>


</div>

<p>没攻略果然还是有问题的，想去晴空塔却发现距离第一天的浅草寺很近，无奈就只能朝着那个方向又跑了一遍。之前有朋友晚上去过，风景不错，我选择了白天上去，稍微有些雾蒙蒙的，视野还是很开阔的。</p>


<div class="gallery  caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-skytree-1.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-skytree-1.jpg" alt="晴空塔"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-skytree-1.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>晴空塔</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-skytree-2.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-skytree-2.jpg" alt="晴空塔"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-skytree-2.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>晴空塔</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-skytree-3.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-skytree-3.jpg" alt="晴空塔"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-skytree-3.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>晴空塔</p>
      </figcaption>
  </figure>
</div>


</div>

<p>住的附近搜到了一家武士博物馆，下午回来早了就过去看了看，到的时间刚刚好，前面的一波英文讲解刚刚开始。小姐姐讲到刀柄上的文字时，说到日本的文字主要源自自己，英文和中文，她说她也看不懂刀柄上的文字，我过去一瞧发现这不就是繁体中文吗 &#x1f60f;，瞬间感觉到我中华语言和文化的优秀。最后还体验了一把武士对决的表演，感觉人家才是武士，我顶多也就是个浪人 &#x1f605;。</p>


<div class="gallery  caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/samurai-museum-1.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/samurai-museum-1.jpg" alt="武士博物馆"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/samurai-museum-1.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>武士博物馆</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/samurai-uniform.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/samurai-uniform.jpg" alt="武士服"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/samurai-uniform.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>武士服</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/samurai-museum-2.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/samurai-museum-2.jpg" alt="武士博物馆"/>
    </div>
    <a href="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/samurai-museum-2.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>武士博物馆</p>
      </figcaption>
  </figure>
</div>


</div>

<p>来了日本还有的就是买买买，给自己买，给家人买，给朋友带，总之各种买买买。计划预留了一天的时间去购物，谁知道玩着玩着就听说台风“海贝思”要来了，据说还是近期最大的一次台风，然后 12 号各种地方就都关门了，只能老老实实地呆在酒店里。傍晚还感觉到房子晃了一下，后面查了还真的是有地震，各种情况就这样都被赶上了。</p>
<p><img src="/images/cn/2019-10-14-tour-of-hangzhou-and-tokyo/tokyo-purple-sky.jpg" alt="台风前紫色的天空"></p>
<p>台风前天空都变成紫色的了，自己没照到，网上盗图一张。本来还很庆幸自己是 13 号下午的飞机回来，台风应该影响不大，谁知道 13 号退完房才发现航班被取消了，各种客服电话都打不进去，无奈我就只能赶紧打车到机场去看看。Anyway，经过各种买票，退票，改签终于还是赶上了 13 号的一班飞机回来。</p>
<p>第一次一个人出来浪还是很舒服的，自由，想去哪儿就去哪儿，唯一不爽的就是没人帮忙照相。</p>


  


<script src="/js/load-photoswipe.js"></script>


<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.css'/>
<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/default-skin/default-skin.min.css'/>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js'></script>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js'></script>


<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

<div class="pswp__bg"></div>

<div class="pswp__scroll-wrap">
    
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    </div>
    
    <div class="pswp__ui pswp__ui--hidden">
    <div class="pswp__top-bar">
      
      <div class="pswp__counter"></div>
      <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
      <button class="pswp__button pswp__button--share" title="Share"></button>
      <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
      <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
      
      
      <div class="pswp__preloader">
        <div class="pswp__preloader__icn">
          <div class="pswp__preloader__cut">
            <div class="pswp__preloader__donut"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
      <div class="pswp__share-tooltip"></div>
    </div>
    <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
    </button>
    <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
    </button>
    <div class="pswp__caption">
      <div class="pswp__caption__center"></div>
    </div>
    </div>
    </div>
</div>


        ]]></description></item><item><title>国际智慧温室种植挑战赛 (International Autonomous Greenhouse Challenge)</title><link>https://zeqiang.fun/cn/2019/09/international-autonomous-greenhouse-challenge/</link><pubDate>Sat, 21 Sep 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/09/international-autonomous-greenhouse-challenge/</guid><description><![CDATA[
        <p><a href="http://www.autonomousgreenhouses.com">国际智慧温室种植挑战赛</a> 是一个由 <a href="https://www.wur.nl">瓦赫宁根大学研究中心 (Wageningen University &amp; Research)</a> 主办的旨在利用自动化、信息技术和人工智能技术控制温室以实现增加产量、降低成本等目标的大赛。第一届赛事的种植作物为黄瓜，第二届赛事为樱桃西红柿。</p>


<div class="gallery  caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  
<link rel="stylesheet" href="/css/photoswipe.css" />
<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/cucumber.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/cucumber.jpg" alt="黄瓜"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/cucumber.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>黄瓜</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse.png');">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse.png" alt="温室"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse.png" itemprop="contentUrl"></a>
      <figcaption>
          <p>温室</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/tomatos.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/tomatos.jpg" alt="西红柿"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/tomatos.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>西红柿</p>
      </figcaption>
  </figure>
</div>


</div>

<p>很幸运能够在晚些时候加入到 CPlant 队伍中一同参与到这次赛事，虽然加入到队伍中比较晚，但工作之余也参与了大部分赛事的准备工作。</p>


<div class="gallery  caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse-1.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse-1.jpg" alt="温室"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse-1.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>温室</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse-2.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse-2.jpg" alt="温室"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse-2.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>温室</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse-3.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse-3.jpg" alt="温室"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/greenhouse-3.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>温室</p>
      </figcaption>
  </figure>
</div>


</div>

<p>整个赛事分为初赛和复赛两个部分，初赛采用 Hackathon 的形式通过仿真模拟进行，初赛晋级的队伍将会在后续 6 个月的时间内通过远程控制进行真实的作物种植比赛。本次赛事吸引了全球顶级的农业与 AI 领域的企业、大学和研究机构参与，组成来自 26 个国家的 21 支团队，超过 200 名专家与学生。</p>


<div class="gallery  caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/hacking.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/hacking.jpg" alt="Hacking"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/hacking.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>Hacking</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/me.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/me.jpg" alt="我"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/me.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>我</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/lunch.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/lunch.jpg" alt="午餐"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/lunch.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>午餐</p>
      </figcaption>
  </figure>
</div>


</div>

<p>初赛黑客马拉松评分主要由三部分组成：团队构成 （20％)、人工智能方法（30％)，以及虚拟西红柿种植净利润（50％)。仿真部分，采用了 Venlo 类型的温室，模拟时间从 2017/12/15 日至 2018/06/01，荷兰本地的外部天气，整个模拟过程并未考虑病虫害问题 (主要受到湿度影响)。仿真模型包含三个子模型：</p>
<ol>
<li>Kaspro 温室模型</li>
<li>Intkam 作物模型</li>
<li>经济模型</li>
</ol>
<p><strong>Kaspro 温室模型</strong>：主要通过温室的控制器 (例如：通风口，加热管道，CO<sub>2</sub> 补充器，遮阳帘，灌溉系统等) 控制温室内的环境变量 (例如：光照，温度，湿度，CO<sub>2</sub> 浓度，水量，水 EC 值等)，进而控制作物生长。环境控制模型是相对复杂的一个模型，因为控制器和环境变量之间并不是一对一的关系。</p>
<p><strong>Intkam 作物模型</strong>：主要通过设置茎的密度，叶片的去留策略，去顶时间，果实个数保留策略等控制作物的生长。</p>
<p><strong>经济模型</strong>：主要定义了不同时间、不同果重、不同糖分樱桃番茄的价格，不同时间段内光照、加热和 CO<sub>2</sub> 的成本，以及相关的人工成本。</p>
<p>最终经过 24 小时的 Hackathon，我们队伍的成绩如下，最后排名 <strong>9/21</strong>，很遗憾未能进入到决赛。</p>
<table>
  <thead>
    <tr style="font-weight: bold;">
      <th width=20% style="text-align: center;">Team Composition (20%)</th>
      <th width=20% style="text-align: center;">Strategy and AI Approach for the Growing Challenge (30%)</th>
      <th width=20% style="text-align: center;">Obtained Points Following Rankings in Hackathon (50%)</th>
      <th width=20% style="text-align: center;">Obtained  Final Results in Hackathon (Net Profit)</th>
      <th width=20% style="text-align: center;">Total Score</th>
    </tr>
  </thead>
    <tr>
      <td style="text-align: center;">15.6 <br/> (Ranking 6/21) <br/> (Max: 17.6) <br/> (Min: 7.6)</td>
      <td style="text-align: center;">21.6 <br/> (Ranking 4/21) <br/> (Max: 23.1) <br/> (Min: 4.8)</td>
      <td style="text-align: center;">21 <br/> (Ranking 9/21) <br/> (Max: 50) <br/> (Min: 1)</td>
      <td style="text-align: center;">92.0 <br/> (Ranking 9/21) <br/> (Max: 154.5) <br/> (Min: 0.7)</td>
      <td style="text-align: center;">58.2 <br/> (Ranking 9/21) <br/> (Max: 88.8) <br/> (Min: 13.4)</td>
    </tr>
  <tbody>
  </tbody>
</table>

<div class="box fancy-figure caption-position-bottom caption-effect-fade" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img">
      <img itemprop="thumbnail" src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/agc-2019-teams.png" alt="Autonomous Greenhouse Challenge 2019"/>
    </div>
    <a href="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/agc-2019-teams.png" itemprop="contentUrl"></a>
      <figcaption>
          <p>Autonomous Greenhouse Challenge 2019</p>
      </figcaption>
  </figure>
</div>

<p>所在的 CPlant 队伍是本次比赛中人数最多的一只队伍 (21 人，最少的队伍为 5 人，虽然人最多却未能进入决赛 &#x1f625;)，评审从国籍，研究和企业组成等多个角度对团队构成进行了评分，最终我们拿到了一个中等偏上的成绩。</p>
<p>人工智能方法方面是我们在准备过程中讨论比较多的内容，每个人根据自己的优势不同分别负责了 Plant Growth Model, Machine Learning, Deep Learning, Reinforcement Learning 和 Knowledge Graph 等不同部分的设计。答辩过程中多位评委对于我们的 Knowledge Graph 在整个人工智能中的应用很感兴趣，在最后点评中也提到我们是唯一一只提到 Knowledge Graph 及其在智慧农业中应用的队伍。我认为智慧农业不同于其他人工智能应用领域分支，其具有一定的特殊性，数据和实验并不像其他领域容易获取和实现，我们需要更多地结合农业科学本身的相关经验和知识。由于我之前从事过 NLP 和 Knowledge Graph 相关工作，我深信 Knowledge Graph 一定会是一个将农业和人工智能有机地结合起来的好工具，但至于如果结合和实现落地还需要进一步探索和研究。最终这部分我们拿到了一个相对不错的成绩。</p>
<p>分数占比最多的仿真部分我们做的有所欠缺，同时这也是我们最为陌生的一个部分。整个 Hackathon 从当地时间 12 日 13 时开始，至 13 日 13 时结束，我们通宵达旦，一整夜的 Coding 陪伴我们度过了中秋佳节。整个过程中我们几乎将全部的精力投入到了 Kaspro 温室模型参数的优化中来，Intkam 作物模型则是根据相关的农业经验进行了简单的优化配置，经济模型并没有直接的控制参数，而是通过相关投入和产出进行计算得到。通过不断的优化，净收益从 10 几分不断提高到 80 几分，后面则一直卡在了 80 几分未能进一步提高。整个 Hackathon 过程中，组委会不定时地公布一些不包含具体组名的成绩统计信息，在第一天白天就已经有队伍拿到了接近 100 分的成绩，在半夜的一次公布中有队伍已经拿到了接近 120 分的净收益。面对巨大的压力，我们仍不断地优化 Kaspro 温室模型参数，虽然成绩在稳步提高，但提高的幅度甚微。在临近比赛的时候，我们终于决定在 Intkam 作物模型做一些大胆的尝试，设置了一些现实中绝对不可能达到的参数，居然取得了很高的提升。在最后 10 几分钟内我们将成绩又提高了 10 分左右，但由于时间限制我们未能来得及进一步调整测试。</p>
<p>所有队伍的 Net Profit 和 Points 成绩从大到小排列结果如下：</p>
<p><img src="/images/cn/2019-09-21-international-autonomous-greenhouse-challenge/net-profit-and-points-results.png" alt="Net Profit &amp; Points Result"></p>
<p>一些与现实相差很远的参数设置却能够得到一个更好的结果，这个问题我们在最开始确实没有敢想。但其实开赛前的技术文件中有提及，整个模拟就是一个黑盒游戏，并没有任何规则可言，最终的评判准则只有净收益。虽然仿真模型与现实会有些差距，但对于这个单纯的游戏而言，先入为主的种植经验确实限制了我们的想象。而对于我这个正在朝着产品经理发展的野生程序猿而言，我正需要的就是这种想象和实践想象的能力，让我以胡适先生的一段话总结这次赛事的经验教训吧：</p>
<blockquote>
<p>大胆的假设，小心的求证。  &ndash; 胡适</p>
</blockquote>
<h2 id="muscle-壮志未酬-来年再战-muscle">&#x1f4aa; 壮志未酬，来年再战！&#x1f4aa;</h2>


  


<script src="/js/load-photoswipe.js"></script>


<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.css'/>
<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/default-skin/default-skin.min.css'/>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js'></script>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js'></script>


<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

<div class="pswp__bg"></div>

<div class="pswp__scroll-wrap">
    
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    </div>
    
    <div class="pswp__ui pswp__ui--hidden">
    <div class="pswp__top-bar">
      
      <div class="pswp__counter"></div>
      <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
      <button class="pswp__button pswp__button--share" title="Share"></button>
      <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
      <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
      
      
      <div class="pswp__preloader">
        <div class="pswp__preloader__icn">
          <div class="pswp__preloader__cut">
            <div class="pswp__preloader__donut"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
      <div class="pswp__share-tooltip"></div>
    </div>
    <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
    </button>
    <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
    </button>
    <div class="pswp__caption">
      <div class="pswp__caption__center"></div>
    </div>
    </div>
    </div>
</div>


        ]]></description></item><item><title>记忆中的儿时 (My Childhood in Memory)</title><link>https://zeqiang.fun/cn/2019/07/my-childhood-in-memory/</link><pubDate>Sun, 28 Jul 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/07/my-childhood-in-memory/</guid><description><![CDATA[
        <p>年中自己做出了一个重大的转变，担搁了不少时间，也没太多心情能静下心来读读写写。现在开始了新的征程，本想先总结总结前段的生活和这个我认为很重要的转变，但或许是这段时间想多了，前后也想得远了，回忆到很多小时候的事儿。开始读上《龍應台的香港筆記》，读着读着也很突然地就想起了童年，真的也很难说上有什么关系，但就是突然想起来了。</p>

<link rel="stylesheet" href="/css/douban-card.css" />


<div class='douban-card douban-card-book' id='douban-card-book-1919723' douban-id='1919723'></div>

<p>然后，也就这几天，突然又听到了刘昊霖的「儿时」，之前有听过但没怎么注意，偏偏这个时候再一听，感觉所有儿时的东西就一股脑都蹦出来了。</p>

  
  <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/aplayer/1.10.1/APlayer.min.css">
  <script src="//cdnjs.cloudflare.com/ajax/libs/aplayer/1.10.1/APlayer.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/meting@latest/dist/Meting.min.js"></script>


<meting-js
  id="001QvW0j49w3ZT"
  server="tencent"
  type="song"
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  >
  
</meting-js>

<p>能回忆起的最早的画面是小时候妈妈给买的小蛋糕，找了很久才找到这种记忆中蛋糕的照片，蛋糕里好像就只有一层奶油。</p>
<p><img src="/images/cn/2019-07-28-my-childhood-in-memory/cake.png" alt="蛋糕"></p>
<p>说实话记不起来是生日还是过年了，因为生日也在冬天，只是模糊地记得和妈妈冬天里赶集的时候好像也没有和妈妈开口要，但妈妈却给买了，当时应该真的是很开心。妈妈管自己比较严，从来都不太敢和妈妈要买什么，实在想要什么的时候我就会说如果我考了前几名能不能给我买个什么呀？</p>
<p>父母的文化水平不高，尤其是妈妈，就上了一段时间的小学，但妈妈却对我们姐弟俩在学业上要求很高。或许他们知道自己没有能够接受很好的知识教育，反而想着不能也让孩子和他们一样。爸爸不善言辞，但经常会带着我出去玩，暑假总能够在前几天就把作业写完，然后爸爸就骑着大梁自行车，我坐在大梁上，带着我去捞小鱼，去摘酸枣，真的很开心，无忧无虑。</p>
<p>小时候住在村里的瓦房中，一个前院，还有个后院，家里有好多的果树：梨树，葡萄树，李子树，樱桃树，柿子树，后院的黄李子真的很好吃，酸酸甜甜的。前院有一大株橙色的百合花，印象中还有一张穿着塑料凉鞋和妈妈的合照，妈妈的样子一直没怎么变，只是现在头发已经花白。整个童年都是在这个院子里面度过的，现在村里的老院子盖成了门市租了出去，爸妈现在也不在村子里住了，自己就更少回去看看了。都说物是人非，现在连老院子也变了，剩下的就只有印象中的东屋，西屋还有窗或地下<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。</p>
<p>姐姐大我九岁，没太多印象和姐姐一起玩，但还记得好几次和姐姐打架。有一次姐姐不小心把自己从炕上推到了地上，是真的疼了，记得自己哭得比较厉害，妈妈说了姐姐。长大后回家，一家人聚在一起回忆旧时光，聊到这里，小外甥女还问我说：我妈那天把你推到地上疼吗？现在想来感觉前脚还在和姐姐打闹，后脚姐姐的孩子都上初中了。姐姐一直对我都很好，小时候播的是四驱小子，姐姐在读师范，放假回来给我带了一辆绿色的「燃烧太阳」，超级开心。</p>
<p><img src="/images/cn/2019-07-28-my-childhood-in-memory/burning-sun.png" alt="燃烧太阳"></p>
<p>那时候的玩伴是斜对门奶奶家的外孙，当时村里的化肥厂还在，他读工厂的子弟学校，我读村里的小学。周末的时候他一般会回来，那会儿感觉最爱玩土，什么用沙子搭个城堡啦，用黄土做个小笔筒啦，还像模像样的用火烧一烧。家的旁边是村委会，里面有个大院子，那里就成了我们最大的根据地。院子里散落着一些不知道干什么用的设备器材，可以爬上爬下。当时我们还发明了一种叫做「闯关」的游戏，就是我俩假设出一关一关的剧情，然后就假装一关一关的完成任务。现在想想这不就是赤裸裸的意淫吗，不过那时候我们真的是玩得不亦乐乎。</p>
<p>回忆儿时的无忧无虑和欢声笑语，其实是对那种生活的向往。现在长大了，对自己的未来有了期许，也自然有了压力，遇见了更多的人和事，难免会有不对付和烦躁。停下来回味回味，感觉两个事很重要：「简单」和「感恩」。简单点，不会太累，很多时候很多事其实也没想的那么复杂，简单些反而迎刃而解，我想这就是童年快乐的源泉吧。多感恩，年轻气盛时老是记得别人的不好，现在反而更多地去发现人与事好的一面。一路走来，能相识相知都是很大的缘，感恩家人、朋友以及一路走来遇见的人和事，正是他们让自己的生活变得独一无二，也正是他们让我们有的回忆，值得回忆。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>唐山话，老家的房子一般就是两间卧室在两边，中间算是大厅，做饭，吃饭都在这，我们就叫这儿窗或地下。&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>启发式算法 (Heuristic Algorithms)</title><link>https://zeqiang.fun/cn/2019/04/heuristic-algorithms/</link><pubDate>Fri, 05 Apr 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/04/heuristic-algorithms/</guid><description><![CDATA[
        <h1 id="启发式算法-heuristic-algorithms">启发式算法 (Heuristic Algorithms)</h1>
<p><strong>启发式算法 (Heuristic Algorithms)</strong> 是相对于最优算法提出的。一个问题的最优算法是指求得该问题每个实例的最优解. 启发式算法可以这样定义 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>：一个基于直观或经验构造的算法，在可接受的花费 (指计算时间、占用空间等) 下给出待解决组合优化问题每一个实例的一个可行解，该可行解与最优解的偏离程度不一定事先可以预计。</p>
<p>在某些情况下，特别是实际问题中，最优算法的计算时间使人无法忍受或因问题的难度使其计算时间随问题规模的增加以指数速度增加，此时只能通过启发式算法求得问题的一个可行解。</p>
<p>利用启发式算法进行目标优化的一些优缺点如下：</p>
<table>
  <thead>
      <tr>
          <th>优点</th>
          <th>缺点</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 算法简单直观，易于修改 <br/>2. 算法能够在可接受的时间内给出一个较优解</td>
          <td>1. 不能保证为全局最优解 <br/>2. 算法不稳定，性能取决于具体问题和设计者经验</td>
      </tr>
  </tbody>
</table>
<p>启发式算法简单的划分为如下三类：<strong>简单启发式算法 (Simple Heuristic Algorithms)</strong>，<strong>元启发式算法 (Meta-Heuristic Algorithms)</strong> 和 <strong>超启发式算法 (Hyper-Heuristic Algorithms)</strong>。</p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/heuristic-algorithms.png" alt="Heuristic-Algorithms"></p>
<h1 id="简单启发式算法-simple-heuristic-algorithms">简单启发式算法 (Simple Heuristic Algorithms)</h1>
<h2 id="贪心算法-greedy-algorithm">贪心算法 (Greedy Algorithm)</h2>
<p>贪心算法是指一种在求解问题时总是采取当前状态下最优的选择从而得到最优解的算法。贪心算法的基本步骤定义如下：</p>
<ol>
<li>确定问题的最优子结构。</li>
<li>设计递归解，并保证在任一阶段，最优选择之一总是贪心选择。</li>
<li>实现基于贪心策略的递归算法，并转换成迭代算法。</li>
</ol>
<p>对于利用贪心算法求解的问题需要包含如下两个重要的性质：</p>
<ol>
<li>最优子结构性质。当一个问题具有最优子结构性质时，可用 <a href="/cn/2018/11/computational-complexity-and-dynamic-programming">动态规划</a> 法求解，但有时用贪心算法求解会更加的简单有效。同时并非所有具有最优子结构性质的问题都可以利用贪心算法求解。</li>
<li>贪心选择性质。所求问题的整体最优解可以通过一系列局部最优的选择 (即贪心选择) 来达到。这是贪心算法可行的基本要素，也是贪心算法与动态规划算法的主要区别。</li>
</ol>
<p>贪心算法和动态规划算法之间的差异如下表所示：</p>
<table>
  <thead>
      <tr>
          <th>贪心算法</th>
          <th>动态规划</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每个阶段可以根据选择当前状态最优解快速的做出决策</td>
          <td>每个阶段的选择建立在子问题的解之上</td>
      </tr>
      <tr>
          <td>可以在子问题求解之前贪婪的做出选择</td>
          <td>子问题需先进行求解</td>
      </tr>
      <tr>
          <td>自顶向下的求解</td>
          <td>自底向上的求解 (也可采用带备忘录的自顶向下方法)</td>
      </tr>
      <tr>
          <td>通常情况下简单高效</td>
          <td>效率可能比较低</td>
      </tr>
  </tbody>
</table>
<h2 id="局部搜索-local-search-和爬山算法-hill-climbing">局部搜索 (Local Search) 和爬山算法 (Hill Climbing)</h2>
<p>局部搜索算法基于贪婪思想，从一个候选解开始，持续地在其<strong>邻域</strong>中搜索，直至邻域中没有更好的解。对于一个优化问题：</p>
<p><code>$$ \min f \left(x\right), x \in \mathbb{R}^n $$</code></p>
<p>其中，<code>$f \left(x\right)$</code> 为目标函数。搜索可以理解为从一个解移动到另一个解的过程，令 <code>$s \left(x\right)$</code> 表示通过移动得到的一个解，<code>$S \left(x\right)$</code> 为从当前解出发所有可能的解的集合 (邻域)，则局部搜索算法的步骤描述如下：</p>
<ol>
<li>初始化一个可行解 <code>$x$</code>。</li>
<li>在当前解的邻域内选择一个移动后的解 <code>$s \left(x\right)$</code>，使得 <code>$f \left(s \left(x\right)\right) &lt; f \left(x\right), s \left(x\right) \in S \left(x\right)$</code>，如果不存在这样的解，则 <code>$x$</code> 为最优解，算法停止。</li>
<li>令 <code>$x = s \left(x\right)$</code>，重复步骤 2。</li>
</ol>
<p>当我们的优化目标为最大化目标函数 <code>$f \left(x\right)$</code> 时，这种局部搜索算法称之为爬山算法。</p>
<h1 id="元启发式算法-meta-heuristic-algorithms">元启发式算法 (Meta-Heuristic Algorithms)</h1>
<p>元启发式算法 (Meta-Heuristic Algorithms) 是启发式算法的改进，通常使用随机搜索技巧，可以应用在非常广泛的问题上，但不能保证效率。本节部分内容参考了《智能优化方法》<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 和《现代优化计算方法》<sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。</p>
<h2 id="禁忌搜索-tabu-search">禁忌搜索 (Tabu Search)</h2>
<p>禁忌搜索 (Tabu Search) 是由 Glover <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 提出的一种优化方法。禁忌搜索通过在解邻域内搜索更优的解的方式寻找目标的最优解，在搜索的过程中将搜索历史放入禁忌表 (Tabu List) 中从而避免重复搜索。禁忌表通过模仿人类的记忆功能，禁忌搜索因此得名。</p>
<p>在禁忌搜索算法中，禁忌表用于防止搜索过程出现循环，避免陷入局部最优。对于一个给定长度的禁忌表，随着新的禁忌对象的不断进入，旧的禁忌对象会逐步退出，从而可以重新被访问。禁忌表是禁忌搜索算法的核心，其功能同人类的短时记忆功能相似，因此又称之为“短期表”。</p>
<p>在某些特定的条件下，无论某个选择是否包含在禁忌表中，我们都接受这个选择并更新当前解和历史最优解，这个选择所满足的特定条件称之为渴望水平。</p>
<p>一个基本的禁忌搜索算法的步骤描述如下：</p>
<ol>
<li>给定一个初始可行解，将禁忌表设置为空。</li>
<li>选择候选集中的最优解，若其满足渴望水平，则更新渴望水平和当前解；否则选择未被禁忌的最优解。</li>
<li>更新禁忌表。</li>
<li>判断是否满足停止条件，如果满足，则停止算法；否则转至步骤 2。</li>
</ol>
<h2 id="模拟退火-simulated-annealing">模拟退火 (Simulated Annealing)</h2>
<p>模拟退火 (Simulated Annealing) 是一种通过在邻域中寻找目标值相对小的状态从而求解全局最优的算法，现代的模拟退火是由 Kirkpatrick 等人于 1983 年提出 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>。模拟退火算法源自于对热力学中退火过程的模拟，在给定一个初始温度下，通过不断降低温度，使得算法能够在多项式时间内得到一个近似最优解。</p>
<p>对于一个优化问题 <code>$\min f \left(x\right)$</code>，模拟退火算法的步骤描述如下：</p>
<ol>
<li>给定一个初始可行解 <code>$x_0$</code>，初始温度 <code>$T_0$</code> 和终止温度 <code>$T_f$</code>，令迭代计数为 <code>$k$</code>。</li>
<li>随机选取一个邻域解 <code>$x_k$</code>，计算目标函数增量 <code>$\Delta f = f \left(x_k\right) - f \left(x\right)$</code>。若 <code>$\Delta f &lt; 0$</code>，则令 <code>$x = x_k$</code>；否则生成随机数 <code>$\xi = U \left(0, 1\right)$</code>，若随机数小于转移概率 <code>$P \left(\Delta f, T\right)$</code>，则令 <code>$x = x_k$</code>。</li>
<li>降低温度 <code>$T$</code>。</li>
<li>若达到最大迭代次数 <code>$k_{max}$</code> 或最低温度 <code>$T_f$</code>，则停止算法；否则转至步骤 2。</li>
</ol>
<p>整个算法的伪代码如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{模拟退火算法}
\begin{algorithmic}
\STATE $x \gets x_0$
\STATE $T \gets T_0$
\STATE $k \gets 0$
\WHILE{$k \leq k_{max}$ \AND $T \geq T_f$}
    \STATE $x_k \gets $ \CALL{neighbor}{$s$}
    \STATE $\Delta f = f \left(x_k\right) - f \left(x\right)$
    \IF{$\Delta f < 0$ \OR \CALL{random}{$0, 1$} $ \leq P \left(\Delta f, T\right)$}
        \STATE $x \gets x_k$
    \ENDIF
    \STATE $T \gets $ \CALL{cooling}{$T, k, k_{max}$}
    \STATE $k \gets k + 1$
\ENDWHILE
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>在进行邻域搜索的过程中，当温度较高时，搜索的空间较大，反之搜索的空间较小。类似的，当 <code>$\Delta f &gt; 0$</code> 时，转移概率的设置也同当前温度的大小成正比。常用的降温函数有两种：</p>
<ol>
<li><code>$T_{k+1} = T_k * r$</code>，其中 <code>$r \in \left(0.95, 0.99\right)$</code>，<code>$r$</code> 设置的越大，温度下降越快。</li>
<li><code>$T_{k+1} = T_k - \Delta T$</code>，其中 <code>$\Delta T$</code> 为每一步温度的减少量。</li>
</ol>
<p>初始温度和终止温度对算法的影响较大，相关参数设置的细节请参见参考文献。</p>
<p>模拟退火算法是对局部搜索和爬山算法的改进，我们通过如下示例对比两者之间的差异。假设目标函数如下：</p>
<p><code>$$ f \left(x, y\right) = e^{- \left(x^2 + y^2\right)} + 2 e^{- \left(\left(x - 1.7\right)^2 + \left(y - 1.7\right)^2\right)} $$</code></p>
<p>优化问题定义为：</p>
<p><code>$$ \max f \left(x, y\right), x \in \left[-2, 4\right], y \in \left[-2, 4\right] $$</code></p>
<p>我们分别令初始解为 <code>$\left(1.5, -1.5\right)$</code> 和 <code>$\left(3.5, 0.5\right)$</code>，下图 (上) 为爬山算法的结果，下图 (下) 为模拟退火算法的结果。</p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/hill-climbing.png" alt="Hill Climbing"></p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/simulated-annealing.png" alt="Simulated Annealing"></p>
<p>其中，<span style="color:#FFF; background-color:#000;"><strong>白色</strong></span> 的大点为初始解位置，<span style="color:#F71893;"><strong>粉色</strong></span> 的大点为求解的最优解位置，颜色从白到粉描述了迭代次数。从图中不难看出，由于局部最大值的存在，从不同的初始解出发，爬山算法容易陷入局部最大值，而模拟退火算法则相对稳定。</p>
<h2 id="遗传算法-genetic-algorithm">遗传算法 (Genetic Algorithm)</h2>
<p>遗传算法 (Genetic Algorithm, GA) 是由 John Holland 提出，其学生 Goldberg 对整个算法进行了进一步完善 <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>。算法的整个思想来源于达尔文的进化论，其基本思想是根据问题的目标函数构造一个适应度函数 (Fitness Function)，对于种群中的每个个体 (即问题的一个解) 进行评估 (计算适应度)，选择，交叉和变异，通过多轮的繁殖选择适应度最好的个体作为问题的最优解。算法的整个流程如下所示：</p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/ga-process.png" alt="GA-Process"></p>
<h3 id="初始化种群">初始化种群</h3>
<p>在初始化种群时，我们首先需要对每一个个体进行编码，常用的编码方式有二进制编码，实值编码 <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>，矩阵编码 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup>，树形编码等。以二进制为例 (如下不做特殊说明时均以二进制编码为例)，对于 <code>$p \in \left\{0, 1, \dotsc, 100\right\}$</code> 中 <code>$p_i = 50$</code> 可以表示为：</p>
<p><code>$$ x_i = 50_{10} = 0110010_{2} $$</code></p>
<p>对于一个具体的问题，我们需要选择合适的编码方式对问题的解进行编码，编码后的个体可以称之为一个染色体。则一个染色体可以表示为：</p>
<p><code>$$ x = \left(p_1, p_2, \dotsc, p_m\right) $$</code></p>
<p>其中，<code>$m$</code> 为染色体的长度或编码的位数。初始化种群个体共 <code>$n$</code> 个，对于任意一个个体染色体的任意一位 <code>$i$</code>，随机生成一个随机数 <code>$\text{rand} \in U \left(0, 1\right)$</code>，若 <code>$\text{rand} &gt; 0.5$</code>，则 <code>$p_i = 1$</code>，否则 <code>$p_i = 0$</code>。</p>
<h3 id="计算适应度">计算适应度</h3>
<p>适应度为评价个体优劣程度的函数 <code>$f\left(x\right)$</code>，通常为问题的目标函数，对最小化优化问题 <code>$f\left(x\right) = - \min \sum{\mathcal{L} \left(\hat{y}, y\right)}$</code>，对最大化优化问题 <code>$f\left(x\right) = \max \sum{\mathcal{L} \left(\hat{y}, y\right)}$</code>，其中 <code>$\mathcal{L}$</code> 为损失函数。</p>
<h3 id="选择">选择</h3>
<p>对于种群中的每个个体，计算其适应度，记第 <code>$i$</code> 个个体的适应度为 <code>$F_i = f\left(x_i\right)$</code>。则个体在一次选择中被选中的概率为：</p>
<p><code>$$ P_i = \dfrac{F_i}{\sum_{i=1}^{n}{F_i}} $$</code></p>
<p>为了保证种群的数量不变，我们需要重复 <code>$n$</code> 次选择过程，单次选择采用轮盘赌的方法。利用计算得到的被选中的概率计算每个个体的累积概率：</p>
<p><code>$$ \begin{equation} \begin{split} CP_0 &amp;= 0 \\ CP_i &amp;= \sum_{j=1}^{i}{P_i} \end{split} \end{equation} $$</code></p>
<p>对于如下一个示例：</p>
<table>
  <thead>
      <tr>
          <th>指标 \ 个体</th>
          <th><code>$x_1$</code></th>
          <th><code>$x_2$</code></th>
          <th><code>$x_3$</code></th>
          <th><code>$x_4$</code></th>
          <th><code>$x_5$</code></th>
          <th><code>$x_6$</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>适应度 (F)</td>
          <td>100</td>
          <td>60</td>
          <td>60</td>
          <td>40</td>
          <td>30</td>
          <td>20</td>
      </tr>
      <tr>
          <td>概率 (P)</td>
          <td>0.322</td>
          <td>0.194</td>
          <td>0.194</td>
          <td>0.129</td>
          <td>0.097</td>
          <td>0.064</td>
      </tr>
      <tr>
          <td>累积概率 (CP)</td>
          <td>0.322</td>
          <td>0.516</td>
          <td>0.71</td>
          <td>0.839</td>
          <td>0.936</td>
          <td>1</td>
      </tr>
  </tbody>
</table>
<p>每次选择时，随机生成 <code>$\text{rand} \in U \left(0, 1\right)$</code>，当 <code>$CP_{i-1} \leq \text{rand} \leq CP_i$</code> 时，选择个体 <code>$x_i$</code>。选择的过程如同在下图的轮盘上安装一个指针并随机旋转，每次指针停止的位置的即为选择的个体。</p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/ga-roulette-wheel.png" alt="GA-Roulette-Wheel"></p>
<h3 id="交叉">交叉</h3>
<p>交叉运算类似于染色体之间的交叉，常用的方法有单点交叉，多点交叉和均匀交叉等。</p>
<ul>
<li>单点交叉：在染色体中选择一个切点，然后将其中一部分同另一个染色体的对应部分进行交换得到两个新的个体。交叉过程如下图所示：</li>
</ul>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/ga-crossover-one-point.png" alt="GA-Crossover-One-Point"></p>
<ul>
<li>多点交叉：在染色体中选择多个切点，对其任意两个切点之间部分以概率 <code>$P_c$</code> 进行交换，其中 <code>$P_c$</code> 为一个较大的值，例如 <code>$P_m = 0.9$</code>。两点交叉过程如下图所示：</li>
</ul>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/ga-crossover-two-points.png" alt="GA-Crossover-Two-Points"></p>
<ul>
<li>均匀交叉：染色体任意对应的位置以一定的概率进行交换得到新的个体。交叉过程如下图所示：</li>
</ul>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/ga-crossover-uniform.png" alt="GA-Crossover-Uniform"></p>
<h3 id="变异">变异</h3>
<p>变异即对于一个染色体的任意位置的值以一定的概率 <code>$P_m$</code> 发生变化，对于二进制编码来说即反转该位置的值。其中  <code>$P_m$</code> 为一个较小的值，例如 <code>$P_m = 0.05$</code>。</p>
<h3 id="小结">小结</h3>
<p>在整个遗传运算的过程中，不同的操作发挥着不同的作用：</p>
<ol>
<li>选择：优胜劣汰，适者生存。</li>
<li>交叉：丰富种群，持续优化。</li>
<li>变异：随机扰动，避免局部最优。</li>
</ol>
<p>除此之外，对于基本的遗传算法还有多种优化方法，例如：精英主义，即将每一代中的最优解原封不动的复制到下一代中，这保证了最优解可以存活到整个算法结束。</p>
<h3 id="示例-商旅问题">示例 - 商旅问题</h3>
<p>以 <a href="https://zh.wikipedia.org/zh/%E6%97%85%E8%A1%8C%E6%8E%A8%E9%94%80%E5%91%98%E9%97%AE%E9%A2%98">商旅问题</a> 为例，利用 GA 算法求解中国 34 个省会城市的商旅问题。求解代码利用了 <a href="https://deap.readthedocs.io/en/master/">Deap</a> 库，结果可视化如下图所示：</p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/ga-tsp.gif" alt="GA-TSP"></p>
<p>一个更有趣的例子是利用 GA 算法，使用不同颜色和透明度的多边形的叠加表示一张图片，在线体验详见 <a href="http://alteredqualia.com/visualization/evolve/">这里</a>，下图为不同参数下的蒙娜丽莎图片的表示情况：</p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/ga-mona-lisa.png" alt="GA-Mona-Lisa"></p>
<h2 id="蚁群算法-ant-colony-optimization-aco">蚁群算法 (Ant Colony Optimization, ACO)</h2>
<p>1991 年，意大利学者 Dorigo M. 等人在第一届欧洲人工生命会议 (ECAL) 上首次提出了蚁群算法。1996 年 Dorigo M. 等人发表的文章 “Ant system: optimization by a colony of cooperating agents” <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> 为蚁群算法奠定了基础。在自然界中，蚂蚁会分泌一种叫做信息素的化学物质，蚂蚁的许多行为受信息素的调控。蚂蚁在运动过程中能够感知其经过的路径上信息素的浓度，蚂蚁倾向朝着信息素浓度高的方向移动。以下图为例 <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup>：</p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/aco-shortest-path.png" alt="ACO Shortest Path"></p>
<p>蚂蚁从蚁巢 (N) 出发到达食物源所在地 (F)，取得食物后再折返回蚁巢。整个过程中蚂蚁有多种路径可以选择，单位时间内路径上通过蚂蚁的数量越多，则该路径上留下的信息素浓度越高。因此，最短路径上走过的蚂蚁数量越多，则后来的蚂蚁选择该路径的机率就越大，从而蚂蚁通过信息的交流实现了寻找食物和蚁巢之间最短路的目的。</p>
<h2 id="粒子群算法-particle-swarm-optimization-pso">粒子群算法 (Particle Swarm Optimization, PSO)</h2>
<p>Eberhart, R. 和 Kennedy, J. 于 1995 年提出了粒子群优化算法 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup> <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup>。粒子群算法模仿的是自然界中鸟群和鱼群等群体的行为，其基本原理描述如下：</p>
<p>一个由 <code>$m$</code> 个粒子 (Particle) 组成的群体 (Swarm) 在 <code>$D$</code> 维空间中飞行，每个粒子在搜索时，考虑自己历史搜索到的最优解和群体内 (或邻域内) 其他粒子历史搜索到的最优解，在此基础上进行位置 (状态，也就是解) 的变化。令第 <code>$i$</code> 个粒子的位置为 <code>$x_i$</code>，速度为 <code>$v_i$</code>，历史搜索的最优解对应的点为 <code>$p_i$</code>，群体内 (或邻域内) 所有粒子历史搜索到的最优解对应的点为 <code>$p_g$</code>，则粒子的位置和速度依据如下公式进行变化：</p>
<p><code>$$ \begin{equation} \begin{split} v^{k+1}_i &amp;= \omega v^k_i + c_1 \xi \left(p^k_i - x^k_i\right) + c_2 \eta \left(p^k_g - x^k_i\right) \\ x^{k+1}_i &amp;= x^k_i + v^{k+1}_i \end{split} \end{equation} $$</code></p>
<p>其中，<code>$\omega$</code> 为惯性参数；<code>$c_1$</code> 和 <code>$c_2$</code> 为学习因子，其一般为正数，通常情况下等于 2；<code>$\xi, \eta \in U \left[0, 1\right]$</code>。学习因子使得粒子具有自我总结和向群体中优秀个体学习的能力，从而向自己的历史最优点以及群体内或邻域内的最优点靠近。同时，粒子的速度被限制在一个最大速度 <code>$V_{max}$</code> 范围内。</p>
<p>对于 Rosenbrock 函数</p>
<p><code>$$ f \left(x, y\right) = \left(1 - x\right)^2 + 100 \left(y - x^2\right)^2 $$</code></p>
<p>当 <code>$x \in \left[-2, 2\right], y \in \left[-1, 3\right]$</code>，定义优化问题为最小化目标函数，最优解为 <code>$\left(0, 0\right)$</code>。利用 <a href="https://github.com/ljvmiranda921/pyswarms">PySwarms</a> 扩展包的优化过程可视化如下：</p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/rosenbrock-pso.gif" alt="Rosenbrock PSO"></p>
<p>其中，<code>$m = 50, \omega = 0.8, c_1 = 0.5, c_2 = 0.3$</code>，迭代次数为 200。</p>
<p>本节相关示例代码详见 <a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2019-04-05-heuristic-algorithms/">这里</a>。</p>
<h1 id="超启发式算法-hyper-heuristic-algorithms">超启发式算法 (Hyper-Heuristic Algorithms)</h1>
<p>超启发式算法 (Hyper-Heuristic Algorithms) 提供了一种高层次启发式方法，通过管理或操纵一系列低层次启发式算法 (Low-Level Heuristics，LLH)，以产生新的启发式算法。这些新启发式算法被用于求解各类组合优化问题 <sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup>。</p>
<p>下图给出了超启发式算法的概念模型。该模型分为两个层面：在问题域层面上，应用领域专家根据自己的背景知识，在智能计算专家协助下，提供一系列 LLH 和问题的定义、评估函数等信息；在高层次启发式方法层面上，智能计算专家设计高效的管理操纵机制，运用问题域所提供的 LLH 算法库和问题特征信息，构造出新的启发式算法。</p>
<p><img src="/images/cn/2019-04-05-heuristic-algorithms/hyper-heuristic-algorithms-model.png" alt="Hyper-Heuristic-Algorithms"></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>邢文训, &amp; 谢金星. (2005). <em>现代优化计算方法</em>. 清华大学出版社.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>汪定伟, 王俊伟, 王洪峰, 张瑞友, &amp; 郭哲. (2007). <em>智能优化方法</em>.  高等教育出版社.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Glover, F. W., &amp; Laguna, M. (1997). <em>Tabu Search</em>. Springer US.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Kirkpatrick, S., Gelatt, C. D., &amp; Vecchi, M. P. (1983). Optimization by Simulated Annealing. <em>Science</em>, 220(4598), 671–680.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p><a href="https://en.wikipedia.org/wiki/Genetic_algorithm">https://en.wikipedia.org/wiki/Genetic_algorithm</a>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Michalewicz, Z., Janikow, C. Z., &amp; Krawczyk, J. B. (1992). A modified genetic algorithm for optimal control problems. <em>Computers &amp; Mathematics with Applications</em>, 23(12), 83-94.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Gottlieb, J., &amp; Paulmann, L. (1998, May). Genetic algorithms for the fixed charge transportation problem. In <em>Evolutionary Computation Proceedings, 1998. IEEE World Congress on Computational Intelligence., The 1998 IEEE International Conference on</em> (pp. 330-335). IEEE.&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Dorigo, M., Maniezzo, V., &amp; Colorni, A. (1996). Ant system: optimization by a colony of cooperating agents. <em>IEEE Transactions on Systems, man, and cybernetics, Part B: Cybernetics</em>, 26(1), 29-41.&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Toksari, M. D. (2016). A hybrid algorithm of Ant Colony Optimization (ACO) and Iterated Local Search (ILS) for estimating electricity domestic consumption: Case of Turkey. <em>International Journal of Electrical Power &amp; Energy Systems</em>, 78, 776-782.&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Eberhart, R., &amp; Kennedy, J. (1995, November). Particle swarm optimization. In <em>Proceedings of the IEEE international conference on neural networks</em> (Vol. 4, pp. 1942-1948).&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Eberhart, R., &amp; Kennedy, J. (1995, October). A new optimizer using particle swarm theory. In <em>MHS'95. Proceedings of the Sixth International Symposium on Micro Machine and Human Science</em> (pp. 39-43). IEEE.&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p>江贺. (2011). 超启发式算法：跨领域的问题求解模式. <em>中国计算机学会通讯</em>, 7(2), 63-70&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>关不掉的浏览器标签页 (Browser Tabs You do not Close)</title><link>https://zeqiang.fun/cn/2019/03/browser-tabs-you-do-not-close/</link><pubDate>Sat, 09 Mar 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/03/browser-tabs-you-do-not-close/</guid><description><![CDATA[
        <blockquote>
<p>有舍，方有得。弱水三千，我只取一瓢饮。</p>
</blockquote>
<p>前不久买重了一本书《断舍离》，新版的装帧挺小清新的，就买了，到家盖完私印后才发现角落里躺着另一本。去年如实有些“买书如山倒，读书如抽丝”了，工作忙是客观原因，不过更多的还是要怪自己懒散了。书不厚，其实之前也没想着买这本书，因为总感觉书名透露着机场里面琳琅满目的成功学书籍的味道，发现买重了之后，用几天零散的时间也就翻完了。书里的道理自己都清楚，但又为什么没有明白地去做呢？我想这本书对于我最重要的就是激发了我去认真理解<strong>什么是“执念”</strong>，如何<strong>放下自己的“执念”</strong>。</p>
<p>或许从我妈那继承了“勤俭持家”的“优良传统”，我也不是很喜欢把东西丢掉，想着总还会有用上的时候，于是就慢慢的东西越堆越多。但毕竟还是年轻的一代，读完书，按照其中的建议整理了下租住的屋子，还真丢掉了不少东西，完事后多少有种轻松的感觉。其实我还是很愿意去收拾房间的，只不过之前更多的是“整理”，而现在则开始尝试“舍弃”。</p>
<p>屋子整理完了，第二天上班，打开电脑，才发现，更需要收拾的地方在这里，再具体写就是我的浏览器。标签页的数量已经多到连每个标签页的关闭按钮都显示不出来了，而且还是有 3 个这样的浏览器实例在那躺着，好在本子性能不错，不然 Chrome 这种吃内存大户早就把电脑搞瘫了。</p>
<h2 id="拖延症">拖延症</h2>
<p>主观之上说白了我就是“拖延症”犯了，没有什么好狡辩的。如果网页打开了，看完了，内容理解吸收了亦或开怀一笑，那么这个标签页也就自然被关闭了。但拖延症确实也不是很好能改掉，我想每个人或多或少都会有拖延症，只是程度不同而已。当然也会有很多客观因素，工作和生活的节奏都很快，真的来不及把所有事情都做完，更不要说都做好。</p>
<h2 id="知识管理">知识管理</h2>
<p>做不到完全摆脱拖延症，那就从技术的角度考虑考虑如何高效的获取获取信息 (知识) 吧。信息爆炸也已经不是一天两天的事了，如何在漫天遍地的信息中准确定位你想要的内容还是有些技巧可循。有些人习惯逛门户网站，这对于一些时效性信息不失为一个选择，但这类网站反而不会成为你关不掉的标签中的一员。对于知识性的内容，我更倾向于使用 RSS 进行订阅，并且仅订阅少量的优质汇总源和个人博客，对于离线知识则使用 <a href="https://www.zotero.org">Zotero</a> 进行管理。可以说在技巧和工具上我也算是走在前面了，但订阅中的一个个连接被打开，却没有一个个被关掉。</p>
<h2 id="断舍离">断舍离</h2>
<p>《断舍离》中提到的“执念”让我问了自己一个问题：你想要的真的是你想要的吗？所以你认为你想要的可能并不是你想要的 (或你需要的)，但清楚自己“真的”想要的，也如实不好做。所以我没有打算一下子就能把我的标签页控制在个位数，只能尝试着去做做看。《断舍离》中的一些方法是可取的，但要针对“信息”这种还比较特殊的东西进行一些变化可能才会适用。</p>
<h2 id="一些尝试">一些尝试</h2>
<ul>
<li>重新审视你到底想干什么，这样才会更清楚想要什么或需要什么？</li>
<li>当不清楚取舍与否，先尝试“舍”，如果有不好的影响，再“取”回来也还来得及。</li>
<li>完成一件事才是真正放下了执念，不要同时干两件类似的事情，干完一件再干一件。</li>
</ul>

        ]]></description></item><item><title>贝塞尔曲线 (Bézier Curve)</title><link>https://zeqiang.fun/cn/2019/02/bezier-curve/</link><pubDate>Tue, 19 Feb 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/02/bezier-curve/</guid><description><![CDATA[
        <p>知道<strong>贝塞尔曲线 (Bézier Curve)</strong> 这个名字已经有很长一段时间了，但一直没有去详细了解一番。直到最近想要绘制一个比较复杂的曲线，才发现很多工具都以贝塞尔曲线为基础的，这包括 Adobe 全家桶中的钢笔工具，还有 OmniGraffle 中的曲线。迫于仅靠猜其是如何工作的但一直没猜透的无奈，只能去详细了解一下其原理再使用了。</p>
<h1 id="数学表示">数学表示</h1>
<p>贝塞尔曲线 (Bézier Curve) 是由法国工程师<a href="https://zh.wikipedia.org/wiki/%E7%9A%AE%E5%9F%83%E5%B0%94%C2%B7%E8%B4%9D%E5%A1%9E%E5%B0%94">皮埃尔·贝兹 (Pierre Bézier)</a> 于 1962 年所广泛发表，他运用贝塞尔曲线来为汽车的主体进行设计 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。贝塞尔曲线最初由<a href="https://en.wikipedia.org/wiki/Paul_de_Casteljau">保尔·德·卡斯特里奥 (Paul de Casteljau)</a> 于 1959 年运用<a href="https://zh.wikipedia.org/wiki/%E5%BE%B7%E5%8D%A1%E6%96%AF%E7%89%B9%E9%87%8C%E5%A5%A5%E7%AE%97%E6%B3%95">德卡斯特里奥算法 (De Casteljau&rsquo;s Algorithm)</a> 开发，以稳定数值的方法求出贝塞尔曲线。</p>
<h2 id="线性贝塞尔曲线">线性贝塞尔曲线</h2>
<p>给定点 <code>$P_0, P_1$</code>，线性贝塞尔曲线定义为：</p>
<p><code>$$ B \left(t\right) = \left(1 - t\right) P_0 + t P_1, t \in \left[0, 1\right] $$</code></p>
<p>不难看出，线性贝塞尔曲线即为点 <code>$P_0$</code> 和 <code>$P_1$</code> 之间的线段。</p>
<p>对于 <code>$P_0 = \left(4, 6\right), P_1 = \left(10, 0\right)$</code>，当 <code>$t = 0.25$</code> 时，线性贝塞尔曲线如下图所示：</p>
<p><img src="/images/cn/2019-02-19-bezier-curve/1st-power-bezier-curve.png" alt="线性贝塞尔曲线"></p>
<p>整个线性贝塞尔曲线生成过程如下图所示：</p>
<p><img src="/images/cn/2019-02-19-bezier-curve/1st-power-bezier-curve.gif" alt="线性贝塞尔曲线生成过程"></p>
<h2 id="二次贝塞尔曲线">二次贝塞尔曲线</h2>
<p>给定点 <code>$P_0, P_1, P_2$</code>，二次贝塞尔曲线定义为：</p>
<p><code>$$ B \left(t\right) = \left(1 - t\right)^2 P_0 + 2 t \left(1 - t\right) P_1 + t^2 P_2, t \in \left[0, 1\right] $$</code></p>
<p>对于 <code>$P_0 = \left(0, 0\right), P_1 = \left(4, 6\right), P_2 = \left(10, 0\right)$</code>，当 <code>$t = 0.25$</code> 时，二次贝塞尔曲线如下图所示：</p>
<p><img src="/images/cn/2019-02-19-bezier-curve/2nd-power-bezier-curve.png" alt="二次贝塞尔曲线"></p>
<p>整个二次贝塞尔曲线生成过程如下图所示：</p>
<p><img src="/images/cn/2019-02-19-bezier-curve/2nd-power-bezier-curve.gif" alt="二次贝塞尔曲线生成过程"></p>
<h2 id="三次贝塞尔曲线">三次贝塞尔曲线</h2>
<p>给定点 <code>$P_0, P_1, P_2, P_3$</code>，三次贝塞尔曲线定义为：</p>
<p><code>$$ B \left(t\right) = \left(1 - t\right)^3 P_0 + 3 t \left(1 - t\right)^2 P_1 + 3 t^2 \left(1 - t\right) P_2 + t^3 P_3, t \in \left[0, 1\right] $$</code></p>
<p>对于 <code>$P_0 = \left(0, 0\right), P_1 = \left(-1, 6\right), P_2 = \left(6, 6\right), P_3 = \left(12, 0\right)$</code>，当 <code>$t = 0.25$</code> 时，三次贝塞尔曲线如下图所示：</p>
<p><img src="/images/cn/2019-02-19-bezier-curve/3rd-power-bezier-curve.png" alt="三次贝塞尔曲线"></p>
<p>整个三次贝塞尔曲线生成过程如下图所示：</p>
<p><img src="/images/cn/2019-02-19-bezier-curve/3rd-power-bezier-curve.gif" alt="三次贝塞尔曲线生成过程"></p>
<h2 id="一般化的贝塞尔曲线">一般化的贝塞尔曲线</h2>
<p>对于一般化的贝塞尔曲线，给定点 <code>$P_0, P_1, \cdots, P_n$</code>， <code>$n$</code> 次贝塞尔曲线定义为：</p>
<p><code>$$ B \left(t\right) =  \sum_{i=0}^{n}{\binom{n}{i} \left(1 - t\right)^{n - i} t^{i} P_i}, t \in \left[0, 1\right] $$</code></p>
<p>其中，</p>
<p><code>$$ b_{i, n} \left(t\right) = \binom{n}{i} \left(1 - t\right)^{n - i} t^{i} $$</code></p>
<p>称之为 <code>$n$</code> 阶 <a href="https://en.wikipedia.org/wiki/Bernstein_polynomial">Bernstein 多项式</a>，点 <code>$P_i$</code> 称为贝塞尔曲线的控制点。从生成过程来看，贝塞尔曲线是通过 <code>$n$</code> 次<strong>中介点</strong> (<code>$Q_j, R_k, S_l$</code>) 生成的，一个更加复杂的四次贝塞尔曲线 (<code>$t = 0.25$</code>) 如下图所示：</p>
<p><img src="/images/cn/2019-02-19-bezier-curve/4th-power-bezier-curve.png" alt="四次贝塞尔曲线"></p>
<p>整个四次贝塞尔曲线生成过程如下图所示：</p>
<p><img src="/images/cn/2019-02-19-bezier-curve/4th-power-bezier-curve.gif" alt="四次贝塞尔曲线生成过程"></p>
<p>其中，<code>$Q_0 = \left(1 - t\right) P_0 + t P_1$</code>，<code>$R_0 = \left(1 - t\right) Q_0 + t Q_1$</code>，<code>$S_0 = \left(1 - t\right) R_0 + t R_1$</code>，<code>$B = \left(1 - t\right) S_0 + t S_1$</code> 为构成贝塞尔曲线的点。</p>
<p>上述图形和动画的绘制代码请参见<a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2019-02-19-bezier-curve/bezier-curve-images.py">这里</a>。</p>
<h1 id="应用技巧">应用技巧</h1>
<p>在很多绘图软件中，钢笔工具使用的是三次贝塞尔曲线，其中<strong>起始点</strong>和<strong>结束点</strong>分别对应 <code>$P_0$</code> 和 <code>$P_1$</code>，起始点和结束点的<strong>控制点</strong>分别对应 <code>$P_2$</code> 和 <code>$P_3$</code>。</p>
<p>在利用钢笔工具绘图时，可以参考如下建议来快速高效地完成绘图 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>：</p>
<ol>
<li>控制点尽可能在曲线的最外侧或最内侧。</li>
<li>除了曲线的结束处外，控制点的控制柄尽可能水平或垂直。</li>
<li>合理安排控制点的密度。</li>
</ol>
<p>下面两张图分别展示了一个原始的字母图案，以及参考上述建议利用贝塞尔曲线勾勒出来的字母图案边框：</p>
<p><img src="/images/cn/2019-02-19-bezier-curve/letters.png" alt="Letters"></p>
<p><img src="/images/cn/2019-02-19-bezier-curve/letters-buzier-curve.png" alt="Letters Buzier Curve"></p>
<p>最后推荐一个网站 <a href="https://bezier.method.ac">The Bezier Game</a>，可以帮助更好的理解和掌握基于贝塞尔曲线的钢笔工具使用。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://zh.wikipedia.org/wiki/%E8%B4%9D%E5%A1%9E%E5%B0%94%E6%9B%B2%E7%BA%BF">https://zh.wikipedia.org/wiki/贝塞尔曲线</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="http://theagsc.com/blog/tutorials/so-whats-the-big-deal-with-horizontal-vertical-bezier-handles-anyway/">So What’s the Big Deal with Horizontal &amp; Vertical Bezier Handles Anyway?</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>如何阅读一本书 (How to Read a Book)</title><link>https://zeqiang.fun/cn/2019/02/how-to-read-a-book/</link><pubDate>Thu, 07 Feb 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/02/how-to-read-a-book/</guid><description><![CDATA[
        <blockquote>
<p>本文为《<a href="https://book.douban.com/subject/1013208/">如何阅读一本书</a>》的简要总结，附加部分个人理解，仅供参考和快速查阅。楷体引用部分多为书中原话，其他部分多为总结和个人理解。</p>
</blockquote>

<link rel="stylesheet" href="/css/douban-card.css" />


<div class='douban-card douban-card-book' id='douban-card-book-1013208' douban-id='1013208'></div>

<h1 id="阅读的层次">阅读的层次</h1>
<blockquote>
<p>第一提醒读者，阅读可以是一件多少主动的事。第二要指出的是，阅读越主动，效果越好。</p>
</blockquote>
<p>读书，不是非做不可的事，而是想要去做的事。——《<a href="https://movie.douban.com/subject/1465654/">女王的教室</a>》</p>
<blockquote>
<p>阅读的目标：为获得资讯而读，以及为求的理解而读。</p>
</blockquote>
<p>也就是说，阅读类似学习，知其然，知其所以然。</p>
<blockquote>
<p>阅读就是学习：指导型的学习，以及自我发现型的学习之间的差异。</p>
</blockquote>
<p>独立的思考与思辨。</p>
<p>阅读的层次分为：</p>
<ol>
<li>基础阅读 (Elementary Reading)</li>
<li>检视阅读 (Inspectional Reading)</li>
<li>分析阅读 (Analytical Reading)</li>
<li>主题阅读 (Syntopical Reading)</li>
</ol>
<h1 id="基础阅读">基础阅读</h1>
<p>基础阅读的四个阶段：</p>
<ol>
<li>阅读准备阶段，相当于学前教育或幼稚园的学习经验。</li>
<li>认字，相当于一年级学生典型的学习经验。</li>
<li>字汇的增长及对课文的运用，通常是四年级结束时学会的方法。</li>
<li>“成熟”的阅读者，小学或初中毕业时的读写能力。</li>
</ol>
<blockquote>
<p>无限制的受教育机会是一个社会能提供给人民最有价值的服务。</p>
</blockquote>
<h1 id="检视阅读">检视阅读</h1>
<h2 id="有系统的略读或粗读">有系统的略读或粗读</h2>
<p>略读 (Skimming) 和粗读 (Pre-reading) 是检视阅读的第一个层次，用不着花太多时间，如何去做，建议如下：</p>
<ol>
<li>先看书名页，然后如果有序就先看序。</li>
<li>研究目录页，对书的基本架构做概括性的理解。</li>
<li>如果书中附有索引，也要检阅一下。</li>
<li>读一下出版者的介绍。</li>
<li>挑几个看来跟主题息息相关的篇章来看。</li>
<li>东翻翻西翻翻，念一两段，连续读几页，不要太多。</li>
</ol>
<p>在最多不超过一个小时的时间内对书有个大概的了解其是否包含你还想继续挖掘下去的内容，是否值的你再继续投入时间与注意。</p>
<h2 id="粗浅的阅读">粗浅的阅读</h2>
<blockquote>
<p>头一次面对一本难读的书的时候，从头到尾读完一遍，碰到不懂的地方<strong>不要</strong>停下来查询或思索。</p>
</blockquote>
<blockquote>
<p>在阅读一本书的时候，慢不该慢的不值得，快不该快到有损于满足与理解。</p>
</blockquote>
<blockquote>
<p>略读或粗读一本书总是个好主意，尤其当你并不清楚手边的一本书是否值的细心阅读时。</p>
</blockquote>
<blockquote>
<p>在第一次阅读一本难读的书时，不要企图了解每一个子句。</p>
</blockquote>
<h2 id="做一个自我要求的读者">做一个自我要求的读者</h2>
<p>在阅读时要提出问题来，同时自己必须尝试去回答这些问题：</p>
<ol>
<li>整体来说，这本书到底在谈些什么？(主题)</li>
<li>作者细部说了什么，怎么说的？(细节)</li>
<li>这本书说得有道理吗？是全部有道理，还是部分有道理？(个人的评价)</li>
<li>这本书跟你有什么关系？(意义)</li>
</ol>
<p>这四个问题概括了一个阅读者的责任，<strong>读书要对书负责，更要对自己负责</strong>。</p>
<blockquote>
<p>你必须读出言外之意，才会有更大的收获，我们也鼓励你“写出言外之意”。</p>
</blockquote>
<p>对于阅读来说，在书上做笔记是不可或缺的事。</p>
<ol>
<li>可以让你保持清醒，不只是不昏睡，还是非常清醒。</li>
<li>主动的阅读是一种思考，而写出来是阅读者表达思考的好方法。</li>
<li>将自己的感想写下来，有助于记住作者的思想。</li>
</ol>
<blockquote>
<p>培养阅读的习惯，除了不断地运作练习之法，别无他法。</p>
</blockquote>
<blockquote>
<p>我们谈到一个有技术的人时，并不是在说他知道该如何去做那件事，而是他已经养成去做那件是的习惯了。</p>
</blockquote>
<h1 id="分析阅读">分析阅读</h1>
<h2 id="第一阶段">第一阶段</h2>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 1.</strong> 你一定要知道自己在读的是哪一类书，而且越早知道越好。最好早在你开始阅读之前就先知道。</div>
<blockquote>
<p>我们一定要超越“<strong>知道这是怎么回事</strong>”，进而明白“<strong>如果我们想做些什么，应该怎么利用它</strong>”。</p>
</blockquote>
<p>也就是我们需要做到<strong>知行合一</strong>。</p>
<blockquote>
<p>理论性作品是在教你<strong>这是什么</strong>，实用性的作品是在教你<strong>如何去做</strong>你想要做的事，或你认为应该做的事。</p>
</blockquote>
<blockquote>
<p>实用的书常会出现“<strong>应该</strong>”和“<strong>应当</strong>”，“<strong>好</strong>”和“<strong>坏</strong>”，“<strong>结果</strong>”和“<strong>意义</strong>”之类的字眼，相反的理论型作品却常常说“<strong>是</strong>”。</p>
</blockquote>
<p>理论性作品可以分为：</p>
<ol>
<li>历史，历史就是纪事，常以说故事的形态出现。</li>
<li>科学，以实验为基础，或依赖精确的观察研究，并不容易被证明。</li>
<li>哲学，是坐在摇椅上的思考，相对容易被观察和理解。</li>
</ol>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 2.</strong> 使用一个单一的句子，或最多几句话（一小段文字）来叙述整本书的内容。</div>
<p>也就是用你的话告诉别人这本书在讲什么。</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 3.</strong> 将书中重要的篇章列举出来，说明它们如何按照顺序组成一个整体的架构。</div>
<blockquote>
<p>规则 2 是在指导你注意一本书的整体性，规则 3 是在强调一本书的复杂度。</p>
</blockquote>
<blockquote>
<p>写作与阅读是一体两面的事，就像教书与被教一样。</p>
</blockquote>
<blockquote>
<p>一个作品应该<strong>有整体感，清楚明白，前后连贯</strong>。</p>
</blockquote>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 4.</strong> 找出作者要问的问题。</div>
<blockquote>
<p>第一阶段的目的就是掌握结构大纲。</p>
</blockquote>
<h2 id="第二阶段">第二阶段</h2>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 5.</strong> 找出重要的单字，透过他们与作者达成共识。</div>
<p>字词只是作者表达的工具，我们需要通过这些字词探索作者想表达的本意。</p>
<blockquote>
<p>一本书之所以能给你带来新的洞察力或启发，就是因为其中有一些你不能一读即懂的字句。</p>
</blockquote>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 6.</strong> 将一本书中最重要的句子圈出来，找出其中的主旨。</div>
<blockquote>
<p>主旨则是这些问题的答案。</p>
</blockquote>
<blockquote>
<p>阅读的一部分本质就是<strong>被困惑，而且知道自己被困惑</strong>。</p>
</blockquote>
<p>惑，知惑！</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 7.</strong> 从相关文句的关联中，设法架构出一本书的基本论述。</div>
<ol>
<li>要记住所有的论述都包含了一些声明。</li>
<li>要区别两种论述的不同之处。归纳法：以一个或多个特殊的事实证明某种共通的概念，演绎法：以连串的通则来证明更进一步的共通概念。</li>
<li>找出作者认为哪些事情是<strong>假设</strong>，哪些是能<strong>证实</strong>的或有根据的，以及哪些是不需要证实的<strong>自明之理</strong>。</li>
</ol>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 8.</strong> 找出作者的解答。</div>
<h2 id="第三阶段">第三阶段</h2>
<blockquote>
<p>遵守思维的礼节。</p>
</blockquote>
<blockquote>
<p>最能学习的读者，也就是最能批评的读者。</p>
</blockquote>
<p>受教是一种美德，但受教并非是被动的顺从，而是主动的思考。</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 9.</strong> 在你说出“我同意”，“我不同意”，或“我暂缓评论”之前，你一定要能肯定的说：“我了解了”。</div>
<blockquote>
<p>毫无理解便同意只是愚蠢，还不清楚便不同意也是无礼。</p>
</blockquote>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 10.</strong> 当你不同意作者的观点时，要理性地表达自己的意见，不要无礼地辩驳或争论。</div>
<blockquote>
<p>争议是教导与受教的一个过程。</p>
</blockquote>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 11.</strong> 尊重知识与个人观点的不同，在作任何评断之前，都要找出理论基础。</div>
<p>事无对错，需有理有据，避免口舌之争。</p>
<blockquote>
<p>当读者不只是盲目地跟从作者的论点，还能和作者的论点针锋相对时，他最后才能提出同意或反对的有意义的评论。</p>
</blockquote>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 12.</strong> 说一位作者知识不足，就是说他缺少某些与他想要解决的问题相关的知识。</div>
<blockquote>
<p>除非这些知识确实相关，否则就没有理由作这样的评论。</p>
</blockquote>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 13.</strong> 说一位作者的知识错误，就是说他的理念不正确。</div>
<blockquote>
<p>论点与事实相反。</p>
</blockquote>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 14.</strong> 说一位作者是不和逻辑的，就是说他的推论荒谬。</div>
<blockquote>
<p>荒谬有两种形态：一种是<strong>缺乏连贯</strong>，也就是结论冒出来了，却跟前面所说的理论连不起来。另一种是<strong>事件变化的前后不一致</strong>，也就是作者所说的两件事是前后矛盾的。</p>
</blockquote>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>规则 15.</strong> 说一位作者的分析是不完整的，就是说他并没有解决他一开始提出来的所有问题。</div>
<blockquote>
<p>严格来说，规则 15 并不能作为不同意一个作者的根据。我们只能就作者的成就是有限的这一点而站在对立面上。</p>
</blockquote>
<h2 id="cheat-sheet">CHEAT SHEET</h2>
<ul>
<li>分析阅读的第一阶段：找出一本书在谈些什么的规则
<ul>
<li>(1). 依照书本的种类与主题作分类。</li>
<li>(2). 用最简短的句子说出整本书在谈些什么。</li>
<li>(3). 按照顺序与关系，列出全书的重要部分。将全书的纲要拟出来之后，再将各个部分的纲要也一一列出。</li>
<li>(4). 找出作者在问的问题，或作者想要解决的问题。</li>
</ul>
</li>
<li>分析阅读的第二阶段：诠释一本书的内容规则
<ul>
<li>(5). 诠释作者使用的关键字，与作者达成共识。</li>
<li>(6). 从最重要的句子中抓出作者的重要主旨。</li>
<li>(7). 找出作者的论述，重新架构这些论述的前因后果，以明白作者的主张。</li>
<li>(8). 确定作者已经解决了哪些问题，还有哪些是未解决的。在未解决的问题中，确定哪些是作者认为自己无法解决的问题。</li>
</ul>
</li>
<li>分析阅读的第三阶段：像是沟通知识一样地评论一本书的规则
<ul>
<li>A. 智慧礼节的一般规则</li>
<li>(9). 除非你已经完成大纲架构，也能诠释整本书了，否则不要轻易批评。（在你说出：“我读懂了”之前，不要说你同意、不同意或暂缓评论。）</li>
<li>(10). 不要争强好胜，非辩到底不可。</li>
<li>(11). 在说出评论之前，你要能证明自己区别得出真正的知识与个人观点的不同。</li>
<li>B. 批评观点的特别标准</li>
<li>(12). 证明作者的知识不足。</li>
<li>(13). 证明作者的知识错误。</li>
<li>(14). 证明作者不合逻辑。</li>
<li>(15). 证明作者的分析与理由是不完整的。</li>
</ul>
</li>
</ul>
<h2 id="辅助阅读">辅助阅读</h2>
<blockquote>
<p><strong>内在阅读</strong>，是指阅读书籍本身，于所有其他的书都是不相关的。<strong>外在阅读</strong>，是指借助其他的一些书籍来阅读一本书。</p>
</blockquote>
<blockquote>
<p>外在的辅助来源可以分为四个部分：相关经验，其他的书，导论与摘要，工具书。</p>
</blockquote>
<blockquote>
<p>导读和摘要要尽量少用，因为：一本书的导读并不一定都是对的，就算他们写对了，可能也不完整。</p>
</blockquote>
<blockquote>
<p>如果你在阅读全书之前，先看了他的导读手册，你就隶属于他了。</p>
</blockquote>
<p>这也是为什么我不喜欢将自己<strong>看过</strong>的书借给其他人的原因，我不希望其他读者在第一次读这本书的时候就被我记录在书上的笔记所影响。</p>
<h1 id="阅读不同读物的方法">阅读不同读物的方法</h1>
<h2 id="阅读实用型的书">阅读实用型的书</h2>
<blockquote>
<p>分析阅读的规则，一般来说适用于<strong>论说性</strong>的作品，也就是说任何一种传达知识的书。</p>
</blockquote>
<div class="blockquote" style='border-left: 4px solid #F66E40;'>任何实用性的书<strong>都不能</strong>解决该书所关心的问题。</div>
<blockquote>
<p>实用性的书分为两类：其中一种，就像本书一样，或是烹饪书、驾驶指南，基本上都是在<strong>说明规则</strong>的。另一类的主要是在阐述<strong>形成规则的原理</strong>，许多伟大的经济、政治、道德巨著就属于这一类。</p>
</blockquote>
<p>在读实用性的书要提出的四个问题：</p>
<ol>
<li>这本书在谈些什么？</li>
<li>找出作者的共识、主旨和论述。</li>
<li>内容真实吗？（比前两点重要）</li>
<li>这本书于我何干？</li>
</ol>
<blockquote>
<p>赞同一本实用性的书，需要你采取行动。</p>
</blockquote>
<h2 id="阅读想象文学">阅读想象文学</h2>
<blockquote>
<p>想象文学的主要目的是<strong>娱乐</strong>，而非<strong>教育</strong>。</p>
</blockquote>
<p>关于阅读想象文学，建议的否定指令：</p>
<ol>
<li>不要抗拒想象文学带给你的影响力。（生活不只有眼前的苟且，还有诗和远方）</li>
<li>在想象文学中，不要去找共识、主旨和论述。</li>
<li>不要用适用于传递知识的，与真理一致的标准来批评小说。（一千个人眼中有一千个哈姆雷特）</li>
</ol>
<p>阅读小说的规则：</p>
<ol>
<li>架构性：</li>
</ol>
<ul>
<li>你必须将想象文学作品分类。</li>
<li>你要抓住整本书的大意，一篇故事的大意总是在情节之中。</li>
<li>你不仅能够将整本书简化为大意，还要能发现整本书各个部分是如何架构起来的。</li>
</ul>
<ol start="2">
<li>诠释性：</li>
</ol>
<ul>
<li>小说的要素是插曲、事件、角色与他们的思想、言语、感觉及行动。</li>
<li>共识与主旨有关。（尝试身临其境）</li>
<li>任何活动就是论述的发展。</li>
</ul>
<ol start="3">
<li>评论性</li>
</ol>
<ul>
<li>在你衷心感谢作者试着为你创造的经验之前，不要批评一本想象的作品。</li>
<li>不该反对或赞成，而是喜欢或不喜欢。</li>
</ul>
<h2 id="阅读故事-戏剧与诗">阅读故事、戏剧与诗</h2>
<blockquote>
<p>暴君并不怕唠叨的作家宣扬自由的思想，他害怕一个醉酒的诗人说了一个笑话，吸引了全民的注意力。</p>
</blockquote>
<blockquote>
<p>所谓“纯”艺术，并不是因为“精致”或“完美”，而是因为作品本身就是一个结束，不再与其他的影响有关。就如同爱默生所说的，<strong>美的本身就是存在的唯一标准</strong>。</p>
</blockquote>
<p>阅读故事书的规则：</p>
<ol>
<li>快读，并且全心全意地读。</li>
<li>整本书在谈些什么？一个故事的词义，存在于角色与事件之中。</li>
<li>批评小说时，要区分是满足<strong>个人特殊</strong>潜意识需求的小说还是<strong>大多数人</strong>潜意识的小说。</li>
</ol>
<p>阅读抒情诗的规则：</p>
<ol>
<li>不论你自己觉得懂不懂，都要一口气读完，不要停。</li>
<li>重读一遍，大声读出来。</li>
</ol>
<blockquote>
<p>对论说性作品所提出的问题是文法与逻辑上的问题。对抒情诗的问题却通常是修辞的问题，或是句法的问题。</p>
</blockquote>
<blockquote>
<p>要了解一首诗，一定要去<strong>读</strong>它，一遍又一遍地读。</p>
</blockquote>
<h2 id="阅读历史书">阅读历史书</h2>
<blockquote>
<p>就事实而言的历史 (history of fact) 与就书写记录而言的历史 (history as a written record of the facts) 是不同的。</p>
</blockquote>
<blockquote>
<p>历史的基本是叙事的。</p>
</blockquote>
<p>所以叙事应尽可能的公平，公正地描述所发生的事情。</p>
<blockquote>
<p>历史<strong>比较</strong>接近小说，而非科学。这并不是说历史学家在<strong>捏造</strong>事实，就像诗人或小说家那样。</p>
</blockquote>
<blockquote>
<p>历史根本就没有模式可循。</p>
</blockquote>
<p>在了解一个已经发生过的事情时，最好多听取几个不同的版本，哪怕每个人的陈述都已经尽可能的公平公正了，但也可能会存在信息的丢失。</p>
<blockquote>
<p>修昔底德说过，他写历史的原因是：希望经由他所观察到的错误，以及他个人受到的灾难与国家所受的苦楚，将来的人们不会重蹈覆辙。</p>
</blockquote>
<p>以铜为鉴，可以正衣冠，以人为鉴，可以知得失，以史为鉴，可以知兴替。</p>
<p>阅读历史书要提出的问题：</p>
<ol>
<li>每一本历史书都有一个特殊而且有限定范围的主题。</li>
<li>历史书在说一个故事，而这个故事当然是发生在一个特定的时间里。</li>
<li>这与我何干？历史会建议一些可行性，因为那是以前的人已经做过的事。</li>
</ol>
<p>传记包含很多类型：</p>
<ol>
<li>定案本 (definitive) 的传记是对一个人的一生作详尽完整的学术性报告，这个人重要到够得上写这种完结篇的传记。定案本的传记<strong>决不能</strong>用来写活着的人。</li>
<li>授权本 (authorized) 的传记通常是由继承人，或是某个重要人物的朋友来负责的。读这种书不能像读一般的历史书一样，读者必须了解作者可能会有<strong>偏见</strong>。</li>
<li>自传所写的都是还未完结的生活。对于任何自传都要有一点疑心，同时别忘了，在你还不了解一本书之前，不要妄下论断。</li>
</ol>
<h2 id="阅读科学与数学">阅读科学与数学</h2>
<blockquote>
<p>科学的客观不在于没有<strong>最初的偏见</strong>，而在于<strong>坦白承认</strong>。</p>
</blockquote>
<blockquote>
<p>科学基本上是<strong>归纳法</strong>，基本的论述也就是经由研究查证，建立出来的一个通则。</p>
</blockquote>
<blockquote>
<p>只要你记住，你的责任不是成为这个主题的专家，而是要去了解相关的问题，在阅读时就会轻松许多。</p>
</blockquote>
<h2 id="阅读哲学书">阅读哲学书</h2>
<p>我想在进一步阅读或学习如何阅读哲学书之前，最好针对这个相对特殊的类别有一个简要的科普。待我对其窥见一斑后再回来补充这一章节。</p>
<h2 id="阅读社会科学">阅读社会科学</h2>
<blockquote>
<p>社会科学不是一个完全独立的学科。诸如人类学、经济学、政治学、社会学的学科，都是组成社会科学的核心。大部分有关法律、教育、公共行政的作品，及一部分商业、社会服务的作品，再加上大量的心理学作品，也适合社会科学的定义。</p>
</blockquote>
<blockquote>
<p>阅读社会科学时，关于一个主题通常要读好几本书，而不会只读一本书。主要的着眼点在一个<strong>特殊的事件或问题</strong>上，而非一个<strong>特殊的作者或一本书</strong>。</p>
</blockquote>
<h1 id="主题阅读">主题阅读</h1>
<blockquote>
<p>在作主题阅读时，第一个要求就是知道：对一个特定的问题来说，所牵涉的绝对不是一本书而已。第二个要求则是：要知道就总的来说，应该读的是哪些书？第二个要求比第一个要求还难做到。</p>
</blockquote>
<blockquote>
<p>分析阅读的技巧只适用于单一的作品，主要的目标是要了解这本书。</p>
</blockquote>
<p>在主题阅读的准备阶段包含如下步骤：</p>
<ol>
<li>针对你要研究的主题，设计一份实验性的书目。你可以参考图书馆目录，专家的建议与书中的书目索引。</li>
<li>浏览这份书目上所有的书，确定哪些与你的主题相关，并就你的主题建立起清楚的概念。</li>
</ol>
<p>主题阅读一共有五个步骤，这些步骤不能称之为规则，因为只要漏掉其中一个步骤，主题阅读就会变得很困难。</p>
<ol>
<li>浏览所有在准备阶段被认定与你主题相关的书，找出最相关的章节。</li>
<li>根据主题创造出一套中立的词汇，带引作者与你达成共识，无论作者是否实际用到这些词汇，所有的作者，或至少绝大部分的作者都可以用这套词汇来诠释。</li>
<li>建立一个中立的主旨，列出一连串的问题，无论作者是否明白谈过这些问题，所有的作者，或者至少大多数的作者都要能解读为针对这些问题提供了他们的回答。</li>
<li>界定主要及次要的议题。然后将作者针对各个问题的不同意见整理陈列在各个议题之旁。你要记住，各个作者之间或之中，不见得一定存在着某个议题。有时候，你需要针对一些不是作者主要关心的范围的事情，把他的观点解读，才能建构出这种议题。</li>
<li>分析这些讨论。这得把问题和议题按照顺序排列，以求突显主题。比较有共通性的议题，要放在比较没有共通性的议题之前。各个议题之间的关系也要清楚得界定出来。</li>
</ol>
<h1 id="心智成长">心智成长</h1>
<blockquote>
<p>对你来说最重要的是，你不只要能读得好，还有有能力分辨出哪些书能够帮助你增进阅读能力。</p>
</blockquote>
<p>读一本好书，会让你的努力有所回报：</p>
<ol>
<li>当你成功地阅读了一本难读的好书之后，你的阅读技巧必然增进了。</li>
<li>一本好书能教你了解这个世界以及你自己。</li>
</ol>
<blockquote>
<p>一本书如果是可以让你学习的书，重读的时候，你会发现书中的内容好像比你记忆中的少了许多。如果这本书属于更高层次的书，你在重读的时候会发现这本书好像与你一起成长了。</p>
</blockquote>
<blockquote>
<p>好的阅读，也就是主动的阅读，不只是对阅读本身有用，也不只是对我们的工作或事业有帮助，更能帮助我们的心智保持活力与成长。</p>
</blockquote>

        ]]></description></item><item><title>相似性和距离度量 (Similarity &amp; Distance Measurement)</title><link>https://zeqiang.fun/cn/2019/01/similarity-and-distance-measurement/</link><pubDate>Tue, 01 Jan 2019 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2019/01/similarity-and-distance-measurement/</guid><description><![CDATA[
        <p>相似性度量 (Similarity Measurement) 用于衡量两个元素之间的相似性程度或两者之间的距离 (Distance)。距离衡量的是指元素之间的不相似性 (Dissimilarity)，通常情况下我们可以利用一个距离函数定义集合 <code>$X$</code> 上元素间的距离，即：</p>
<p><code>$$ d: X \times X \to \mathbb{R} $$</code></p>
<p>同时，对于集合 <code>$X$</code> 内的元素 <code>$x, y, z$</code>，距离函数一般满足如下条件：</p>
<ol>
<li><code>$d \left(x, y\right) \geq 0$</code> (非负性)</li>
<li><code>$d \left(x, y\right) = 0, \text{当且仅当} \ x = y$</code> (同一性)</li>
<li><code>$d \left(x, y\right) = d \left(y, x\right)$</code> (对称性)</li>
<li><code>$d \left(x, z\right) \leq d \left(x, y\right) + d \left(y, z\right)$</code> (三角不等式)</li>
</ol>
<h2 id="明可夫斯基距离-明氏距离-minkowski-distance">明可夫斯基距离 (明氏距离, Minkowski Distance)</h2>
<p>对于点 <code>$x = \left(x_1, x_2, ..., x_n\right)$</code> 和点 <code>$y = \left(y_1, y_2, ..., y_n\right)$</code>，<code>$p$</code> <strong>阶明可夫斯基距离</strong> 定义为：</p>
<p><code>$$ d \left(x, y\right) = \left(\sum_{i=1}^{n} |x_i - y_i|^p\right)^{\frac{1}{p}} $$</code></p>
<p>当 <code>$p = 1$</code> 时，称之为 <strong>曼哈顿距离 (Manhattan Distance)</strong> 或 <strong>出租车距离</strong>：</p>
<p><code>$$ d \left(x, y\right) = \sum_{i=1}^{n} |x_i - y_i| $$</code></p>
<p>当 <code>$p = 2$</code> 时，称之为 <strong>欧式距离 (Euclidean Distance)</strong> ：</p>
<p><code>$$ d \left(x, y\right) = \sqrt{\sum_{i=1}^{n} \left(x_i - y_i\right)^2} $$</code></p>
<p><img src="/images/cn/2019-01-01-similarity-and-distance-measurement/manhattan-distance.svg" alt="Manhattan Distance"></p>
<p>上图中 <span style="color:#00d100;"><strong>绿色</strong></span> 的直线为两点间的欧式距离，<span style="color:#ff0000;"><strong>红色</strong></span> <span style="color:#ffd600;"><strong>黄色</strong></span> <span style="color:#0000ff;"><strong>蓝色</strong></span> 的折线均为两点间的曼哈顿距离，不难看出 3 条折线的长度是相同的。</p>
<p>当 <code>$p \to \infty$</code> 时，称之为 <strong>切比雪夫距离 (Chebyshev Distance)</strong> ：</p>
<p><code>$$ d \left(x, y\right) = \lim_{p \to \infty} \left(\sum_{i=1}^{n} |x_i - y_i|^p\right)^{\frac{1}{p}} = \max_{i=1}^{n} |x_i - y_i| $$</code></p>
<p>下图展示了不同的 <code>$p$</code> 值下单位圆，即 <code>$x^p + y^p = 1$</code>，便于大家理解不同 <code>$p$</code> 值下的明可夫斯基距离：</p>
<p><img src="/images/cn/2019-01-01-similarity-and-distance-measurement/2D-unit-balls.png" alt="2D Unit Balls"></p>
<h2 id="马哈拉诺比斯距离-马氏距离-mahalanobis-distance">马哈拉诺比斯距离 (马氏距离, Mahalanobis Distance)</h2>
<p>马哈拉诺比斯距离表示数据的 <strong>协方差距离</strong>，与欧式距离不同其考虑到各种特性之间的联系是 <strong>尺度无关 (Scale Invariant)</strong> 的。对于一个协方差矩阵为 <code>$\sum$</code> 的变量 <code>$x$</code> 和 <code>$y$</code>，马氏距离定义为：</p>
<p><code>$$ d \left(x, y\right) = \sqrt{\left(x - y\right)^{\top} {\sum}^{-1} \left(x - y\right)} $$</code></p>
<p>马氏距离的最大优势就是其不受不同维度之间量纲的影响，同时引入的问题便是扩大了变化量较小的变量的影响。以下图为例 (源码详见 <a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2019-01-01-similarity-and-distance-measurement/mahalanobis-distance.R">这里</a>)：</p>
<p><img src="/images/cn/2019-01-01-similarity-and-distance-measurement/mahalanobis-distance.png" alt="Mahalanobis Distance"></p>
<p>左侧图中根据欧式距离计算，<span style="color:#F07769;"><strong>红色</strong></span> 的点距离 <span style="color:#34BA27;"><strong>绿色</strong></span> 的点更近一些，右侧图是根据马氏距离进行座标变换后的示意图，不难看出此时 <span style="color:#F07769;"><strong>红色</strong></span> 的点距离 <span style="color:#6C9BFF;"><strong>蓝色</strong></span> 的点更近一些。</p>
<h2 id="向量内积-inner-product-of-vectors">向量内积 (Inner Product of Vectors)</h2>
<p>在欧几里得几何中，两个笛卡尔坐标向量的点积常称为内积，向量内积是两个向量的长度与它们夹角余弦的积，定义为：</p>
<p><code>$$ x \cdot y = \sum_{i=1}^{n}{x_i y_i} $$</code></p>
<p>从代数角度看，先对两个数字序列中的每组对应元素求积，再对所有积求和，结果即为点积。从几何角度看，点积则是两个向量的长度与它们夹角余弦的积。在欧几里得空间中，点积可以直观地定义为：</p>
<p><code>$$ x \cdot y = \left| x \right| \left| y \right| \cos \theta $$</code></p>
<p><strong>余弦相似度 (Cosine Similarity)</strong> 可以利用两个向量夹角的 cos 值定义，即：</p>
<p><code>$$ s \left(x, y\right) = \cos \left(\theta\right) = \dfrac{x \cdot y}{\left| x \right| \left| y \right|} = \dfrac{\sum_{i=1}^{n}{x_i y_i}}{\sqrt{\sum_{i=1}^{n}{x_i^2}} \sqrt{\sum_{i=1}^{n}{y_i^2}}} $$</code></p>
<p>余弦相似度的取值范围为：<code>$\left[-1, 1\right]$</code>，1 表示两者完全正相关，-1 表示两者完全负相关，0 表示两者之间独立。余弦相似度与向量的长度无关，只与向量的方向有关，但余弦相似度会受到向量平移的影响。</p>
<p><strong>皮尔逊相关系数 (Pearson Correlation)</strong> 解决了余弦相似度会收到向量平移影响的问题，其定义为：</p>
<p><code>$$ \rho \left(x, y\right) = \dfrac{\text{cov} \left(x, y\right)}{\sigma_x \sigma_y} = \dfrac{E \left[\left(x - \mu_x\right) \left(y - \mu_y\right)\right]}{\sigma_x \sigma_y} $$</code></p>
<p>其中，<code>$\text{cov}$</code> 表示协方差，<code>$E$</code> 表示期望，<code>$\mu$</code> 表示均值，<code>$\sigma$</code> 表示标准差。对于样本的皮尔逊相关系数，可以通过如下方式计算：</p>
<p><code>$$ \begin{equation} \begin{split} r &amp;= \dfrac{\sum_{i=1}^{n}{\left(x_i - \bar{x}\right) \left(y_i - \bar{y}\right)}}{\sqrt{\sum_{i=1}^{n}{\left(x_i - \bar{x}\right)^2}} \sqrt{\sum_{i=1}^{n}{\left(y_i - \bar{y}\right)^2}}} \\ &amp;= \dfrac{1}{n-1} \sum_{i=1}^{n}{\left(\dfrac{x_i - \bar{x}}{\sigma_x}\right) \left(\dfrac{y_i - \bar{y}}{\sigma_y}\right)} \end{split} \end{equation} $$</code></p>
<p>皮尔逊相关系数的取值范围为：<code>$\left[-1, 1\right]$</code>，值的含义与余弦相似度相同。皮尔逊相关系数有一个重要的数学特性是：变量位置和尺度的变化并不会引起相关系数的改变。下图给出了不同的 <code>$\left(x, y\right)$</code> 之间的皮尔逊相关系数。</p>
<p><img src="/images/cn/2019-01-01-similarity-and-distance-measurement/correlation-examples.png" alt="Correlation Examples"></p>
<h2 id="集合距离-distance-of-sets">集合距离 (Distance of Sets)</h2>
<p>对于两个集合之间的相似性度量，主要有如下几种方法：</p>
<ul>
<li><strong>Jaccard 系数</strong></li>
</ul>
<p><code>$$ s = \dfrac{\left|X \cap Y\right|}{\left| X \cup Y \right|} = \dfrac{\left|X \cap Y\right|}{\left|X\right| + \left|Y\right| - \left|X \cap Y\right|} $$</code></p>
<p>Jaccard 系数的取值范围为：<code>$\left[0, 1\right]$</code>，0 表示两个集合没有重合，1 表示两个集合完全重合。</p>
<ul>
<li><strong>Dice 系数</strong></li>
</ul>
<p><code>$$ s = \dfrac{2 \left| X \cap Y \right|}{\left|X\right| + \left|Y\right|} $$</code></p>
<p>与 Jaccard 系数相同，Dice 系数的取值范围为：<code>$\left[0, 1\right]$</code>，两者之间可以相互转换 <code>$s_d = 2 s_j / \left(1 + s_j\right), s_j = s_d / \left(2 - s_d\right)$</code>。不同于 Jaccard 系数，Dice 系数的差异函数 <code>$d = 1 - s$</code> 并不是一个合适的距离度量，因为其并不满足距离函数的三角不等式。</p>
<ul>
<li><strong>Tversky 系数</strong></li>
</ul>
<p><code>$$ s = \dfrac{\left| X \cap Y \right|}{\left| X \cap Y \right| + \alpha \left| X \setminus Y \right| + \beta \left| Y \setminus X \right|} $$</code></p>
<p>其中，<code>$X \setminus Y$</code> 表示集合的相对补集。Tversky 系数可以理解为 Jaccard 系数和 Dice 系数的一般化，当 <code>$\alpha = \beta = 1$</code> 时为 Jaccard 系数，当 <code>$\alpha = \beta = 0.5$</code> 时为 Dice 系数。</p>
<h2 id="字符串距离-distance-of-strings">字符串距离 (Distance of Strings)</h2>
<p>对于两个字符串之间的相似性度量，主要有如下几种方法：</p>
<ul>
<li><strong>Levenshtein 距离</strong></li>
</ul>
<p>Levenshtein 距离是 <strong>编辑距离 (Editor Distance)</strong> 的一种，指两个字串之间，由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括将一个字符替换成另一个字符，插入一个字符，删除一个字符。例如将 <strong>kitten</strong> 转成 <strong>sitting</strong>，转换过程如下：</p>
<p><code>$$ \begin{equation*} \begin{split} \text{kitten} \to \text{sitten} \left(k \to s\right) \\ \text{sitten} \to \text{sittin} \left(e \to i\right) \\ \text{sittin} \to \text{sitting} \left(\  \to g\right) \end{split} \end{equation*} $$</code></p>
<p>编辑距离的求解可以利用动态规划的思想优化计算的时间复杂度。</p>
<ul>
<li><strong>Jaro-Winkler 距离</strong></li>
</ul>
<p>对于给定的两个字符串 <code>$s_1$</code> 和 <code>$s_2$</code>，Jaro 相似度定义为：</p>
<p><code>$$ sim = \begin{cases} 0 &amp; \text{if} \  m = 0 \\ \dfrac{1}{3} \left(\dfrac{m}{\left|s_1\right|} + \dfrac{m}{\left|s_2\right|} + \dfrac{m-t}{m}\right) &amp; \text{otherwise} \end{cases} $$</code></p>
<p>其中，<code>$\left|s_i\right|$</code> 为字符串 <code>$s_i$</code> 的长度，<code>$m$</code> 为匹配的字符的个数，<code>$t$</code> 换位数目的一半。如果字符串 <code>$s_1$</code> 和 <code>$s_2$</code> 相差不超过 <code>$\lfloor \dfrac{\max \left(\left|s_1\right|, \left|s_2\right|\right)}{2} \rfloor - 1$</code>，我们则认为两个字符串是匹配的。例如，对于字符串 <strong>CRATE</strong> 和 <strong>TRACE</strong>，仅 <strong>R, A, E</strong> 三个字符是匹配的，因此 <code>$m = 3$</code>，尽管 <strong>C, T</strong> 均出现在两个字符串中，但是他们的距离超过了 1 (即，<code>$\lfloor \dfrac{5}{2} \rfloor - 1$</code>)，因此 <code>$t = 0$</code>。</p>
<p>Jaro-Winkler 相似度给予了起始部分相同的字符串更高的分数，其定义为：</p>
<p><code>$$ sim_w = sim_j + l p \left(1 - sim_j\right) $$</code></p>
<p>其中，<code>$sim_j$</code> 为字符串 <code>$s_1$</code> 和 <code>$s_2$</code> 的 Jaro 相似度，<code>$l$</code> 为共同前缀的长度 (规定不超过 <code>$4$</code>)，<code>$p$</code> 为调整系数 (规定不超过 <code>$0.25$</code>)，Winkler 将其设置为 <code>$p = 0.1$</code>。</p>
<ul>
<li><strong>汉明距离</strong></li>
</ul>
<p>汉明距离为两个<strong>等长字符串</strong>对应位置的不同字符的个数，也就是将一个字符串变换成另外一个字符串所需要<strong>替换</strong>的字符个数。例如：<strong>10<span style="color:#0000ff;">1</span>1<span style="color:#0000ff;">1</span>01</strong> 与 <strong>10<span style="color:#ff0000;">0</span>1<span style="color:#ff0000;">0</span>01</strong> 之间的汉明距离是 2，<strong>“<span style="color:#0000ff;">t</span>o<span style="color:#0000ff;">n</span>e<span style="color:#0000ff;">d</span>”</strong> 与 <strong>“<span style="color:#ff0000;">r</span>o<span style="color:#ff0000;">s</span>e<span style="color:#ff0000;">s</span>”</strong> 之间的汉明距离是 3。</p>
<h2 id="信息论距离-information-theory-distance">信息论距离 (Information Theory Distance)</h2>
<p>首先我们需要理解什么是 <strong>熵 (Entropy)</strong>？熵最早是用来表示物理学中一个热力系统无序的程度，后来依据香农的信息论，熵用来衡量一个随机变量的不确定性程度。对于一个随机变量 <code>$X$</code>，其概率分布为：</p>
<p><code>$$ P \left(X = x_i\right) = p_i, \quad i = 1, 2, ..., n $$</code></p>
<p>则随机变量 <code>$X$</code> 的熵定义如下：</p>
<p><code>$$ H \left(X\right) = - \sum_{i=1}^{n} P \left(x_i\right) \log P \left(x_i\right) \label{eq:entropy} $$</code></p>
<p>例如抛一枚硬币，假设硬币正面向上 <code>$X = 1$</code> 的概率为 <code>$p$</code>，硬币反面向上 <code>$X = 0$</code> 的概率为 <code>$1 - p$</code>。则对于抛一枚硬币那个面朝上这个随机变量 <code>$X$</code> 的熵为：</p>
<p><code>$$ H \left(X\right) = - p \log p - \left(1-p\right) \log \left(1-p\right) $$</code></p>
<p>随概率 <code>$p$</code> 变化如下图所示：</p>
<p><img src="/images/cn/2019-01-01-similarity-and-distance-measurement/entropy-demo.png" alt="Entropy Demo"></p>
<p>从图可以看出，当 <code>$p = 0.5$</code> 时熵最大，也就是说抛一枚硬币，当正反两面朝上的概率相同时，熵最大，系统最复杂。对于公式 <code>$\ref{eq:entropy}$</code>，当取以 2 为底的对数时，熵的单位为比特 (bit)，当取自然对数时，熵的单位为纳特 (nat)，当取以 10 为底的对数时，熵的单位为哈特 (hart)。</p>
<p>对于随机变量 <code>$\left(X, Y\right)$</code>，其联合概率分布为：</p>
<p><code>$$ P \left(X = x_i, Y = y_i\right) = p_{i, j}, \quad i = 1,2,...,n; \quad j = 1,2,...,m $$</code></p>
<p><strong>条件熵 (Conditional Entropy)</strong> 表示在已知 <code>$X$</code> 的条件下 <code>$Y$</code> 的不确定性，定义为：</p>
<p><code>$$ \begin{equation} \begin{split} H \left(Y | X\right) &amp;= \sum_{i=i}^{n} P \left(x_i\right) H \left(Y | X = x_i\right) \\ &amp;= \sum_{i=1}^{n}{\sum_{j=1}^{m}{P \left(x_i, y_j\right) \log \dfrac{P \left(x_i\right)}{P \left(x_i, y_j\right)}}} \end{split} \end{equation} $$</code></p>
<p><strong>联合熵 (Joint Entropy)</strong> 用于衡量多个随机变量的随机系统的信息量，定义为：</p>
<p><code>$$ H \left(X, Y\right) = \sum_{i=1}^{n}{\sum_{j=1}^{m}{P \left(x_i, y_j\right) \log P \left(x_i, y_j\right)}} $$</code></p>
<ul>
<li><strong>互信息 (Mutual Information)</strong></li>
</ul>
<p>互信息用于衡量两个变量之间的关联程度，定义为：</p>
<p><code>$$ I \left(X; Y\right) = \sum_{i=1}^{n}{\sum_{j=1}^{m}{P \left(x_i, y_j\right) \log \dfrac{P \left(x_i, y_i\right)}{P \left(x_i\right) P \left(y_j\right)}}} $$</code></p>
<p>直观上，互信息度量 <code>$X$</code> 和 <code>$Y$</code> 共享的信息，它度量知道这两个变量其中一个，对另一个不确定度减少的程度。</p>
<ul>
<li><strong>相对熵 (Relative Entropy)</strong></li>
</ul>
<p>相对熵又称之为 <strong>KL 散度 (Kullback-Leibler Divergence)</strong>，用于衡量两个分布之间的差异，定义为：</p>
<p><code>$$ D_{KL} \left(P \| Q\right) = \sum_{i}{P \left(i\right) \ln \dfrac{P \left(i\right)}{Q \left(i\right)}} $$</code></p>
<p>KL 散度为非负数 <code>$D_{KL} \left(P \| Q\right) \geq 0$</code>，同时其不具有对称性 <code>$D_{KL} \left(P \| Q\right) \neq D_{KL} \left(Q \| P\right)$</code>，也不满足距离函数的三角不等式。</p>
<ul>
<li><strong>交叉熵 (Corss Entropy)</strong></li>
</ul>
<p>交叉熵定义为：</p>
<p><code>$$ \begin{equation} \begin{split} H \left(P, Q\right) &amp;= H \left(P\right) + D_{KL} \left(P \| Q\right) \\ &amp;= - \sum_{i}{P \left(i\right) \log Q \left(i\right)} \end{split} \end{equation} $$</code></p>
<p>交叉熵常作为机器学习中的损失函数，用于衡量模型分布和训练数据分布之间的差异性。</p>
<ul>
<li><strong>JS 散度 (Jensen-Shannon Divergence)</strong></li>
</ul>
<p>JS 散度解决了 KL 散度不对称的问题，定义为：</p>
<p><code>$$ D_{JS} \left(P \| Q\right) = \dfrac{1}{2} D_{KL} \left(P \| \dfrac{P + Q}{2}\right) + \dfrac{1}{2} D_{KL} \left(Q \| \dfrac{P + Q}{2}\right) $$</code></p>
<p>当取以 2 为底的对数时，JS 散度的取值范围为：<code>$\left[0, 1\right]$</code>。</p>
<ul>
<li><strong>推土机距离 (Earth Mover Distance, Wasserstein Distance)</strong></li>
</ul>
<p>推土机距离用于描述两个多维分布之间相似性，之所以称为推土机距离是因为我们将分布看做空间中的泥土，两个分布之间的距离则是通过泥土的搬运将一个分布改变到另一个分布所消耗的最小能量 (即运送距离和运送重量的乘积)。</p>
<p>对于给定的分布 <code>$P = \left\{\left(p_1, w_{p1}\right), \left(p_2, w_{p2}\right), \cdots, \left(p_m, w_{pm}\right)\right\}$</code> 和 <code>$Q = \left\{\left(q_1, w_{q1}\right), \left(q_2, w_{q2}\right), \cdots, \left(q_n, w_{qn}\right)\right\}$</code>，定义从 <code>$p_i$</code> 到 <code>$q_j$</code> 之间的距离为 <code>$d_{i, j}$</code>，所需运送的重量为 <code>$f_{i, j}$</code>。对于 <code>$f_{i, j}$</code> 有如下 4 个约束：</p>
<ol>
<li>运送需从 <code>$p_i$</code> 到 <code>$q_j$</code>，不能反向，即 <code>$f_{i, j} \geq 0, 1 \leq i \leq m, 1 \leq j \leq n$</code>。</li>
<li>从 <code>$p_i$</code> 运送出的总重量不超过原始的总重量 <code>$w_{pi}$</code>，即 <code>$\sum_{j=1}^{n}{f_{i, j}} \leq w_{pi}, 1 \leq i \leq m$</code>。</li>
<li>运送到 <code>$q_j$</code> 的总重量不超过其总容量 <code>$w_{qj}$</code>，即 <code>$\sum_{i=1}^{m}{f_{i, j}} \leq w_{qj}, 1 \leq j \leq n$</code>。</li>
<li><code>$\sum_{i=1}^{m}{\sum_{j=1}^{n}{f_{i, j}}} = \min \left\{\sum_{i=1}^{m}{w_{pi}}, \sum_{j=1}^{n}{w_{qj}}\right\}$</code>。</li>
</ol>
<p>在此约束下，通过最小化损失函数：</p>
<p><code>$$ \min \sum_{i=1}^{m}{\sum_{j=1}^{n}{d_{i, j} f_{i, j}}} $$</code></p>
<p>得到最优解 <code>$f_{i, j}^*$</code>，则推土机距离定义为：</p>
<p><code>$$ D_{W} \left(P, Q\right) = \dfrac{\sum_{i=1}^{m}{\sum_{j=1}^{n}{d_{i, j} f_{i, j}^*}}}{\sum_{i=1}^{m}{\sum_{j=1}^{n}{f_{i, j}^*}}} $$</code></p>
<h2 id="其他距离-other-distance">其他距离 (Other Distance)</h2>
<ul>
<li><strong>DTW (Dynamic Time Warping) 距离</strong></li>
</ul>
<p>DTW 距离用于衡量两个序列之间的相似性，序列的长度可能相等也可能不相等。对于两个给定的序列 <code>$X = \left(x_1, x_2, \cdots, x_m\right)$</code> 和 <code>$Y = \left(y_1, y_2, \cdots, y_n\right)$</code>，我们可以利用动态规划的方法求解 DTW 距离。首先我们构造一个 <code>$m \times n$</code> 的矩阵，矩阵中的元素 <code>$d_{i, j}$</code> 表示 <code>$x_i$</code> 和 <code>$y_j$</code> 之间的距离。我们需要找到一条通过该矩阵的路径 <code>$W = \left(w_1, w_2, \cdots, w_l\right)$</code>, <code>$\max\left(m, n\right) \leq l &lt; m + n + 1$</code>，假设 <code>$w_k$</code> 对应的矩阵元素为 <code>$\left(i, j\right)$</code>，对应的距离为 <code>$d_k$</code>，则 DTW 的优化目标为 <code>$\min \sum_{k=1}^{l}{d_k}$</code>。如下图右上角部分所示：</p>
<p><img src="/images/cn/2019-01-01-similarity-and-distance-measurement/dtw-threeway.png" alt="DTW Three-Way"></p>
<p>对于路径 <code>$W$</code>，需要满足如下 3 个条件：</p>
<ol>
<li><strong>边界条件</strong>：<code>$w_1 = \left(1, 1\right), w_k = \left(m, n\right)$</code>，即路径须从左下角出发，在右上角终止。</li>
<li><strong>连续性</strong>：对于 <code>$w_{l-1} = \left(i', j'\right), w_l = \left(i, j\right)$</code>，需满足 <code>$i - i' \leq 1, j - j' \leq 1$</code>，即路径不能跨过任何一点进行匹配。</li>
<li><strong>单调性</strong>：对于 <code>$w_{l-1} = \left(i', j'\right), w_l = \left(i, j\right)$</code>，需满足 <code>$0 \leq i - i', 0 \leq j - j'$</code>，即路径上的点需单调递增，不能回退进行匹配。</li>
</ol>
<p>利用动态规划求解 DTW 的状态转移方程为：</p>
<p><code>$$ dtw_{i, j} = \begin{cases} 0 &amp; \text{if} \  i = j = 0 \\ \infty &amp; \text{if} \  i = 0 \  \text{or} \  j = 0 \\ d_{i, j} + \min \left(dtw_{i-1, j}, dtw_{i-1, j-1}, dtw_{i, j-1}\right) &amp; \text{otherwise} \end{cases} $$</code></p>
<p><code>$dtw_{m, n}$</code> 则为最终的 DTW 距离。在 DTW 求解的过程中还可以使用不同的 Local Warping Step 和窗口类型，更多详细信息可看见 R 中 <a href="https://cran.r-project.org/web/packages/dtw/index.html">dtw 包</a>。下图展示了利用 DTW 求解后不同点之间的对应关系：</p>
<p><img src="/images/cn/2019-01-01-similarity-and-distance-measurement/dtw-twoway.png" alt="DTW Two-Way"></p>
<ul>
<li><strong>流形距离 (Distance of Manifold)</strong></li>
</ul>
<p>关于流形距离请参见之前的博客：<a href="/cn/2018/03/manifold-learning">流形学习 (Manifold Learning)</a>。</p>
<h2>:tada::tada::tada: Happe New Year! :tada::tada::tada:</h2>

        ]]></description></item><item><title>集成学习算法 (Ensemble Learning)</title><link>https://zeqiang.fun/cn/2018/12/ensemble-learning/</link><pubDate>Sat, 08 Dec 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/12/ensemble-learning/</guid><description><![CDATA[
        <p>传统机器学习算法 (例如：决策树，人工神经网络，支持向量机，朴素贝叶斯等) 的目标都是寻找一个最优分类器尽可能的将训练数据分开。集成学习 (Ensemble Learning) 算法的基本思想就是将多个分类器组合，从而实现一个预测效果更好的集成分类器。集成算法可以说从一方面验证了中国的一句老话：三个臭皮匠，赛过诸葛亮。</p>
<p>Thomas G. Dietterich <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 指出了集成算法在统计，计算和表示上的有效原因：</p>
<ul>
<li>统计上的原因</li>
</ul>
<p>一个学习算法可以理解为在一个假设空间 <code>$\mathcal{H}$</code> 中选找到一个最好的假设。但是，当训练样本的数据量小到不够用来精确的学习到目标假设时，学习算法可以找到很多满足训练样本的分类器。所以，学习算法选择任何一个分类器都会面临一定错误分类的风险，因此将多个假设集成起来可以降低选择错误分类器的风险。</p>
<ul>
<li>计算上的原因</li>
</ul>
<p>很多学习算法在进行最优化搜索时很有可能陷入局部最优的错误中，因此对于学习算法而言很难得到一个全局最优的假设。事实上人工神经网络和决策树已经被证实为是一 个NP 问题 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>。集成算法可以从多个起始点进行局部搜索，从而分散陷入局部最优的风险。</p>
<ul>
<li>表示上的原因</li>
</ul>
<p>在多数应用场景中，假设空间 <code>$\mathcal{H}$</code> 中的任意一个假设都无法表示 (或近似表示) 真正的分类函数 <code>$f$</code>。因此，对于不同的假设条件，通过加权的形式可以扩大假设空间，从而学习算法可以在一个无法表示或近似表示真正分类函数 <code>$f$</code> 的假设空间中找到一个逼近函数 <code>$f$</code> 的近似值。</p>
<p>集成算法大致可以分为：Bagging，Boosting 和 Stacking 等类型。</p>
<h2 id="bagging">Bagging</h2>
<p>Bagging (Boostrap Aggregating) 是由 Breiman 于 1996 年提出 <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>，基本思想如下：</p>
<ol>
<li>每次采用有放回的抽样从训练集中取出 <code>$n$</code> 个训练样本组成新的训练集。</li>
<li>利用新的训练集，训练得到 <code>$M$</code> 个子模型 <code>$\{h_1, h_2, ..., h_M\}$</code>。</li>
<li>对于分类问题，采用投票的方法，得票最多子模型的分类类别为最终的类别；对于回归问题，采用简单的平均方法得到预测值。</li>
</ol>
<p>Bagging 算法如下所示：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Bagging 算法}
\begin{algorithmic}
\REQUIRE \\
    学习算法 $L$ \\
    子模型个数 $M$ \\
    训练数据集 $T = \{(x_1, y_1), (x_2, y_2), ..., (x_N, y_N)\}$
\ENSURE \\
    Bagging 算法 $h_f\left(x\right)$
\FUNCTION{Bagging}{$L, M, T$}
\FOR{$m = 1$ \TO $M$}
    \STATE $T_m \gets $ boostrap sample from training set $T$
    \STATE $h_m \gets L\left(T_m\right)$
\ENDFOR
\STATE $h_f\left(x\right) \gets \text{sign} \left(\sum_{m=1}^{M} h_m\left(x\right)\right)$
\RETURN $h_f\left(x\right)$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>假设对于一个包含 <code>$M$</code> 个样本的数据集 <code>$T$</code>，利用自助采样，则一个样本始终不被采用的概率是 <code>$\left(1 - \frac{1}{M}\right)^M$</code>，取极限有：</p>
<p><code>$$ \lim_{x \to \infty} \left(1 - \dfrac{1}{M}\right)^M = \dfrac{1}{e} \approx 0.368 $$</code></p>
<p>即每个学习器仅用到了训练集中 <code>$63.2\%$</code> 的数据集，剩余的 <code>$36.8\%$</code> 的训练集样本可以用作验证集对于学习器的泛化能力进行包外估计 (out-of-bag estimate)。</p>
<h3 id="随机森林-random-forests">随机森林 (Random Forests)</h3>
<p>随机森林 (Random Forests) <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 是一种利用决策树作为基学习器的 Bagging 集成学习算法。随机森林模型的构建过程如下：</p>
<ul>
<li>数据采样</li>
</ul>
<p>作为一种 Bagging 集成算法，随机森林同样采用有放回的采样，对于总体训练集 <code>$T$</code>，抽样一个子集 <code>$T_{sub}$</code> 作为训练样本集。除此之外，假设训练集的特征个数为 <code>$d$</code>，每次仅选择 <code>$k\left(k &lt; d\right)$</code> 个构建决策树。因此，随机森林除了能够做到样本扰动外，还添加了特征扰动，对于特征的选择个数，推荐值为 <code>$k = \log_2 d$</code> <sup id="fnref1:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>。</p>
<ul>
<li>树的构建</li>
</ul>
<p>每次根据采样得到的数据和特征构建一棵决策树。在构建决策树的过程中，会让决策树生长完全而不进行剪枝。构建出的若干棵决策树则组成了最终的随机森林。</p>
<p>随机森林在众多分类算法中表现十分出众 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup>，其主要的优点包括：</p>
<ol>
<li>由于随机森林引入了样本扰动和特征扰动，从而很大程度上提高了模型的泛化能力，尽可能地避免了过拟合现象的出现。</li>
<li>随机森林可以处理高维数据，无需进行特征选择，在训练过程中可以得出不同特征对模型的重要性程度。</li>
<li>随机森林的每个基分类器采用决策树，方法简单且容易实现。同时每个基分类器之间没有相互依赖关系，整个算法易并行化。</li>
</ol>
<h2 id="boosting">Boosting</h2>
<p>Boosting 是一种提升算法，可以将弱的学习算法提升 (boost) 为强的学习算法。基本思路如下：</p>
<ol>
<li>利用初始训练样本集训练得到一个基学习器。</li>
<li>提高被基学习器误分的样本的权重，使得那些被错误分类的样本在下一轮训练中可以得到更大的关注，利用调整后的样本训练得到下一个基学习器。</li>
<li>重复上述步骤，直至得到 <code>$M$</code> 个学习器。</li>
<li>对于分类问题，采用有权重的投票方式；对于回归问题，采用加权平均得到预测值。</li>
</ol>
<h3 id="adaboost">Adaboost</h3>
<p>Adaboost <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> 是 Boosting 算法中有代表性的一个。原始的 Adaboost 算法用于解决二分类问题，因此对于一个训练集</p>
<p><code>$$ T = \{\left(x_1, y_1\right), \left(x_2, y_2\right), ..., \left(x_n, y_n\right)\} $$</code></p>
<p>其中 <code>$x_i \in \mathcal{X} \subseteq \mathbb{R}^n, y_i \in \mathcal{Y} = \{-1, +1\}$</code>，首先初始化训练集的权重</p>
<p><code>$$ \begin{equation} \begin{split} D_1 =&amp; \left(w_{11}, w_{12}, ..., w_{1n}\right) \\ w_{1i} =&amp; \dfrac{1}{n}, i = 1, 2, ..., n \end{split} \end{equation} $$</code></p>
<p>根据每一轮训练集的权重 <code>$D_m$</code>，对训练集数据进行抽样得到 <code>$T_m$</code>，再根据 <code>$T_m$</code> 训练得到每一轮的基学习器 <code>$h_m$</code>。通过计算可以得出基学习器 <code>$h_m$</code> 的误差为 <code>$\epsilon_m$</code>，根据基学习器的误差计算得出该基学习器在最终学习器中的权重系数</p>
<p><code>$$ \alpha_m = \dfrac{1}{2} \ln \dfrac{1 - \epsilon_m}{\epsilon_m} $$</code></p>
<p>更新训练集的权重</p>
<p><code>$$ \begin{equation} \begin{split} D_{m+1} =&amp; \left(w_{m+1, 1}, w_{m+1, 2}, ..., w_{m+1, n}\right) \\ w_{m+1, i} =&amp; \dfrac{w_{m, i}}{Z_m} \exp \left(-\alpha_m y_i h_m\left(x_i\right)\right) \end{split} \end{equation} $$</code></p>
<p>其中 <code>$Z_m$</code> 为规范化因子</p>
<p><code>$$ Z_m = \sum_{i = 1}^{n} w_{m, i} \exp \left(-\alpha_m y_i h_m \left(x_i\right)\right) $$</code></p>
<p>从而保证 <code>$D_{m+1}$</code> 为一个概率分布。最终根据构建的 <code>$M$</code> 个基学习器得到最终的学习器：</p>
<p><code>$$ h_f\left(x\right) = \text{sign} \left(\sum_{m=1}^{M} \alpha_m h_m\left(x\right)\right) $$</code></p>
<p>AdaBoost 算法过程如下所示：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{AdaBoost 算法}
\begin{algorithmic}
\REQUIRE \\
    学习算法 $L$ \\
    子模型个数 $M$ \\
    训练数据集 $T = \{(x_1, y_1), (x_2, y_2), ..., (x_N, y_N)\}$
\ENSURE \\
    AdaBoost 算法 $h_f\left(x\right)$
\FUNCTION{AdaBoost}{$L, M, T$}
\STATE $D_1\left(x\right) \gets 1 / n$
\FOR{$m = 1$ \TO $M$}
    \STATE $T_{sub} \gets $ sample from training set $T$ with weights
    \STATE $h_m \gets L\left(T_{sub}\right)$
    \STATE $\epsilon_m\gets Error\left(h_m\right)$
    \IF{$\epsilon_m > 0.5$}
        \BREAK
    \ENDIF
    \STATE $\alpha_m \gets \dfrac{1}{2} \ln \dfrac{1 - \epsilon_m}{\epsilon_m}$
    \STATE $D_{m+1} \gets \dfrac{D_m \exp \left(-\alpha_m y h_m\left(x\right)\right)}{Z_m}$
\ENDFOR
\STATE $h_f\left(x\right) \gets \text{sign} \left(\sum_{m=1}^{M} \alpha_m h_m\left(x\right)\right)$
\RETURN $h_f\left(x\right)$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<h3 id="gbdt-gbm-gbrt-mart">GBDT (GBM, GBRT, MART)</h3>
<p>GBDT (Gradient Boosting Decision Tree) 是另一种基于 Boosting 思想的集成算法，除此之外 GBDT 还有很多其他的叫法，例如：GBM (Gradient Boosting Machine)，GBRT (Gradient Boosting Regression Tree)，MART (Multiple Additive Regression Tree) 等等。GBDT 算法由 3 个主要概念构成：Gradient Boosting (GB)，Regression Decision Tree (DT 或 RT) 和 Shrinkage。</p>
<p>从 GBDT 的众多别名中可以看出，GBDT 中使用的决策树并非我们最常用的分类树，而是回归树。分类树主要用于处理响应变量为因子型的数据，例如天气 (可以为晴，阴或下雨等)。回归树主要用于处理响应变量为数值型的数据，例如商品的价格。当然回归树也可以用于二分类问题，对于回归树预测出的数值结果，通过设置一个阈值即可以将数值型的预测结果映射到二分类问题标签上，即 <code>$\mathcal{Y} = \{-1, +1\}$</code>。</p>
<p>对于 Gradient Boosting 而言，首先，Boosting 并不是 Adaboost 中 Boost 的概念，也不是 Random Forest 中的重抽样。在 Adaboost 中，Boost 是指在生成每个新的基学习器时，根据上一轮基学习器分类对错对训练集设置不同的权重，使得在上一轮中分类错误的样本在生成新的基学习器时更被重视。GBDT 中在应用 Boost 概念时，每一轮所使用的数据集没有经过重抽样，也没有更新样本的权重，而是每一轮选择了不用的回归目标，即上一轮计算得到的残差 (Residual)。其次，Gradient 是指在新一轮中在残差减少的梯度 (Gradient) 上建立新的基学习器。</p>
<p>下面我们通过一个年龄预测的 <a href="http://suanfazu.com/t/gbdt-die-dai-jue-ce-shu-ru-men-jiao-cheng/135">示例</a> (较之原示例有修改) 简单介绍 GBDT 的工作流程。</p>
<p>假设存在 4 个人 <code>$P = \{p_1, p_2, p_3, p_4\}$</code>，他们的年龄分别为 <code>$14, 16, 24, 26$</code>。其中 <code>$p_1, p_2$</code> 分别是高一和高三学生，<code>$p_3, p_4$</code> 分别是应届毕业生和工作两年的员工。利用原始的决策树模型进行训练可以得到如下图所示的结果：</p>
<p><img src="/images/cn/2018-12-08-ensemble-learning/gbdt-decision-tree-1.png" alt="GBDT-Descision-Tree-1"></p>
<p>利用 GBDT 训练模型，由于数据量少，在此我们限定每个基学习器中的叶子节点最多为 2 个，即树的深度最大为 1 层。训练得到的结果如下图所示：</p>
<p><img src="/images/cn/2018-12-08-ensemble-learning/gbdt-decision-tree-2.png" alt="GBDT-Descision-Tree-2"></p>
<p>在训练第一棵树过程中，利用年龄作为预测值，根据计算可得由于 <code>$p_1, p_2$</code> 年龄相近，<code>$p_3, p_4$</code> 年龄相近被划分为两组。通过计算两组中真实年龄和预测的年龄的差值，可以得到第一棵树的残差 <code>$R = \{-1, 1, -1, 1\}$</code>。因此在训练第二棵树的过程中，利用第一棵树的残差作为标签值，最终所有人的年龄均正确被预测，即最终的残差均为 <code>$0$</code>。</p>
<p>则对于训练集中的 4 个人，利用训练得到的 GBDT 模型进行预测，结果如下：</p>
<ol>
<li><code>$p_1$</code> ：14 岁高一学生。购物较少，经常问学长问题，预测年龄 <code>$Age = 15 - 1 = 14$</code>。</li>
<li><code>$p_2$</code> ：16 岁高三学生。购物较少，经常回答学弟问题，预测年龄 <code>$Age = 15 + 1 = 16$</code>。</li>
<li><code>$p_3$</code> ：24 岁应届毕业生。购物较多，经常问别人问题，预测年龄 <code>$Age = 25 - 1 = 24$</code>。</li>
<li><code>$p_4$</code> ：26 岁 2 年工作经验员工。购物较多，经常回答别人问题，预测年龄 <code>$Age = 25 + 1 = 26$</code>。</li>
</ol>
<p>整个 GBDT 算法流程如下所示：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{GBDT 算法}
\begin{algorithmic}
\REQUIRE \\
    子模型个数 $M$ \\
    训练数据集 $T = \{(x_1, y_1), (x_2, y_2), ..., (x_N, y_N)\}$
\ENSURE \\
    GBDT 算法 $h_f\left(x\right)$
\FUNCTION{GBDT}{$M, T$}
\STATE $F_1\left(x\right) \gets \sum_{i = 1}^{N} y_i / N$
\FOR{$m = 1$ \TO $M$}
\STATE $r_m \gets y - F_m \left(x\right)$
\STATE $T_m \gets \left(x, r_m\right)$
\STATE $h_m \gets RegressionTree \left(T_m\right)$
\STATE $\alpha_m \gets \dfrac{\sum_{i = 1}^{N} r_{im} h_m \left(x_i\right)}{\sum_{i = 1}^{N} h_m \left(x_i\right)^2}$
\STATE $F_m \left(x\right) = F_{m-1} \left(x\right) + \alpha_m h_m \left(x\right)$
\ENDFOR
\STATE $h_f\left(x\right) =  F_M \left(x\right)$
\RETURN $h_f\left(x\right)$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>GBDT 中也应用到了 Shrinkage 的思想，其基本思想可以理解为每一轮利用残差学习得到的回归树仅学习到了一部分知识，因此我们无法完全信任一棵树的结果。Shrinkage 思想认为在新的一轮学习中，不能利用全部残差训练模型，而是仅利用其中一部分，即：</p>
<p><code>$$ r_m = y - s F_m \left(x\right), 0 \leq s \leq 1 $$</code></p>
<p>注意，这里的 Shrinkage 和学习算法中 Gradient 的步长是两个不一样的概念。Shrinkage 设置小一些可以避免发生过拟合现象；而 Gradient 中的步长如果设置太小则会陷入局部最优，如果设置过大又容易结果不收敛。</p>
<h3 id="xgboost">XGBoost</h3>
<p>XGBoost 是由 Chen 等人 <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup> 提出的一种梯度提升树模型框架。XGBoost 的基本思想同 GBDT 一样，对于一个包含 <code>$n$</code> 个样本和 <code>$m$</code> 个特征的数据集 <code>$\mathcal{D} = \left\{\left(\mathbf{x}_i, y_i\right)\right\}$</code>，其中 <code>$\left|\mathcal{D}\right| = n, \mathbf{x}_i \in \mathbb{R}^m, y_i \in \mathbb{R}$</code>，一个集成树模型可以用 <code>$K$</code> 个加法函数预测输出：</p>
<p><code>$$ \hat{y}_i = \phi \left(\mathbf{x}_i\right) = \sum_{k=1}^{K}{f_k \left(\mathbf{x}_i\right)}, f_k \in \mathcal{F} $$</code></p>
<p>其中，<code>$\mathcal{F} = \left\{f \left(\mathbf{x}\right) = w_{q \left(\mathbf{x}\right)}\right\} \left(q: \mathbb{R}^m \to T, w \in \mathbb{R}^T\right)$</code> 为回归树 (CART)，<code>$q$</code> 表示每棵树的结构，其将一个样本映射到最终的叶子节点，<code>$T$</code> 为叶子节点的数量，每个 <code>$f_w$</code> 单独的对应一棵结构为 <code>$q$</code> 和权重为 <code>$w$</code> 的树。不同于决策树，每棵回归树的每个叶子节点上包含了一个连续的分值，我们用 <code>$w_i$</code> 表示第 <code>$i$</code> 个叶子节点上的分值。</p>
<p>XGBoost 首先对损失函数进行了改进，添加了 L2 正则项，同时进行了二阶泰勒展开。损失函数表示为：</p>
<p><code>$$ \begin{equation} \begin{split} \mathcal{L} \left(\phi\right) = \sum_{i}{l \left(\hat{y}_i, y_i\right)} + \sum_{k}{\Omega \left(f_k\right)} \\ \text{where} \ \Omega \left(f\right) = \gamma T + \dfrac{1}{2} \lambda \left\| w \right\|^2 \end{split} \end{equation} $$</code></p>
<p>其中，<code>$l$</code> 为衡量预测值 <code>$\hat{y}_i$</code> 和真实值 <code>$y_i$</code> 之间差异的函数，<code>$\Omega$</code> 为惩罚项，<code>$\gamma$</code> 和 <code>$\lambda$</code> 为惩罚项系数。</p>
<p>我们用 <code>$\hat{y}_i^{\left(t\right)}$</code> 表示第 <code>$t$</code> 次迭代的第 <code>$i$</code> 个实例，我们需要增加 <code>$f_t$</code> 来最小化如下的损失函数：</p>
<p><code>$$ \mathcal{L}^{\left(t\right)} = \sum_{i=1}^{n}{l \left(y_i, \hat{y}_i^{\left(t-1\right)} + f_t \left(\mathbf{x}_i\right)\right)} + \Omega \left(f_t\right) $$</code></p>
<p>对上式进行二阶泰勒展开有：</p>
<p><code>$$ \mathcal{L}^{\left(t\right)} \simeq \sum_{i=1}^{n}{\left[l \left(y_i, \hat{y}_i^{\left(t-1\right)}\right) + g_i f_t \left(\mathbf{x}_i\right) + \dfrac{1}{2} h_i f_t^2 \left(\mathbf{x}_i\right)\right]} + \Omega \left(f_t\right) $$</code></p>
<p>其中，<code>$g_i = \partial_{\hat{y}^{\left(t-1\right)}} l \left(y_i, \hat{y}^{\left(t-1\right)}\right), h_i = \partial_{\hat{y}^{\left(t-1\right)}}^{2} l \left(y_i, \hat{y}^{\left(t-1\right)}\right)$</code> 分别为损失函数的一阶梯度和二阶梯度。去掉常数项，第 <code>$t$</code> 步的损失函数可以简化为：</p>
<p><code>$$ \tilde{\mathcal{L}}^{\left(t\right)} = \sum_{i=1}^{n}{\left[ g_i f_t \left(\mathbf{x}_i\right) + \dfrac{1}{2} h_i f_t^2 \left(\mathbf{x}_i\right)\right]} + \Omega \left(f_t\right) $$</code></p>
<p>令 <code>$I_j = \left\{i \ | \ q \left(\mathbf{x}_i\right) = j\right\}$</code> 表示叶子节点 <code>$j$</code> 的实例集合，上式可重写为：</p>
<p><code>$$ \begin{equation} \begin{split} \tilde{\mathcal{L}}^{\left(t\right)} &amp;= \sum_{i=1}^{n}{\left[ g_i f_t \left(\mathbf{x}_i\right) + \dfrac{1}{2} h_i f_t^2 \left(\mathbf{x}_i\right)\right]} + \gamma T + \dfrac{1}{2} \lambda \sum_{j=1}^{T}{w_j^2} \\ &amp;= \sum_{j=1}^{T}{\left[\left(\sum_{i \in I_j}{g_i}\right) w_j + \dfrac{1}{2} \left(\sum_{i \in I_j}{h_i + \lambda}\right) w_j^2\right]} + \gamma T \end{split} \end{equation} $$</code></p>
<p>对于一个固定的结构 <code>$q \left(\mathbf{x}\right)$</code>，可以通过下式计算叶子节点 <code>$j$</code> 的最优权重 <code>$w_j^*$</code>：</p>
<p><code>$$ w_j^* = - \dfrac{\sum_{i \in I_j}{g_i}}{\sum_{i \in I_j}{h_i} + \lambda} $$</code></p>
<p>进而计算对应的最优值：</p>
<p><code>$$ \tilde{\mathcal{L}}^{\left(t\right)} \left(q\right) = - \dfrac{1}{2} \sum_{j=1}^{T}{\dfrac{\left(\sum_{i \in I_j}{g_i}\right)^2}{\sum_{i \in I_j}{h_i} + \lambda}} + \gamma T $$</code></p>
<p>上式可以作为评价树的结构 <code>$q$</code> 的评分函数。通常情况下很难枚举所有可能的树结构，一个贪心的算法是从一个节点出发，逐层的选择最佳的分裂节点。令 <code>$I_L$</code> 和 <code>$I_R$</code> 分别表示分裂后左侧和右侧的节点集合，令 <code>$I = I_L \cup I_R$</code>，则分裂后损失的减少量为：</p>
<p><code>$$ \mathcal{L}_{\text{split}} = \dfrac{1}{2} \left[\dfrac{\left(\sum_{i \in I_L}{g_i}\right)^2}{\sum_{i \in I_L}{h_i} + \lambda} + \dfrac{\left(\sum_{i \in I_R}{g_i}\right)^2}{\sum_{i \in I_R}{h_i} + \lambda} - \dfrac{\left(\sum_{i \in I}{g_i}\right)^2}{\sum_{i \in I}{h_i} + \lambda}\right] - \gamma $$</code></p>
<p>XGBoost 也采用了 Shrinkage 的思想减少每棵树的影响，为后续树模型留下更多的改进空间。同时 XGBoost 也采用了随机森林中的特征下采样 (列采样) 方法用于避免过拟合，同时 XGBoost 也支持样本下采样 (行采样)。XGBoost 在分裂点的查找上也进行了优化，使之能够处理无法将全部数据读入内存的情况，同时能够更好的应对一些由于数据缺失，大量零值和 One-Hot 编码导致的特征稀疏问题。除此之外，XGBoost 在系统实现，包括：并行化，Cache-Aware 加速和数据的核外计算 (Out-of-Core Computation) 等方面也进行了大量优化，相关具体实现请参见论文和 <a href="https://xgboost.readthedocs.io/en/latest/">文档</a>。</p>
<h3 id="lightgbm">LightGBM</h3>
<p>LightGBM 是由微软研究院的 Ke 等人 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup> 提出了一种梯度提升树模型框架。之前的 GBDT 模型在查找最优分裂点时需要扫描所有的样本计算信息增益，因此其计算复杂度与样本的数量和特征的数量成正比，这使得在处理大数据量的问题时非常耗时。LightGBM 针对这个问题提出了两个算法：</p>
<ol>
<li>Gradient-based One-Side Sampling (GOSS)</li>
<li>Exclusive Feature Bundling (EFB)</li>
</ol>
<h4 id="gradient-based-one-side-sampling">Gradient-based One-Side Sampling</h4>
<p>在 AdaBoost 中，样本的权重很好的诠释了数据的重要性，但在 GBDT 中并没有这样的权重，因此无法直接应用 AdaBoost 的采样方法。幸运的是 GBDT 中每个样本的梯度可以为我们的数据采样提供有用的信息。当一个样本具有较小的梯度时，其训练的误差也较小，表明其已经训练好了。一个直观的想法就是丢弃这些具有较小梯度的样本，但是这样操作会影响整个数据的分布，从而对模型的精度造成损失。</p>
<p>GOSS 的做法是保留具有较大梯度的样本，并从具有较小梯度的样本中随机采样。同时为了补偿对数据分布的影响，在计算信息增益的时候，GOSS 针对梯度较小的样本引入了一个常数乘子。这样就保证了模型更多的关注未得到较好训练的数据，同时又不会对原始数据分布改变过多。整个算法流程如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{GOSS 算法}
\begin{algorithmic}
\INPUT \\
    训练数据 $I$ \\
    迭代次数 $d$ \\
    具有较大梯度数据的采样比例 $a$ \\
    具有较小梯度数据的采样比例 $b$ \\
    损失函数 $loss$ \\
    基学习器 $L$
\FUNCTION{GOSS}{$I, d, a, b, loss, L$}
\STATE $\text{models} \gets \varnothing$
\STATE $\text{fact} \gets \dfrac{1-a}{b}$
\STATE $\text{topN} \gets a \times \text{len} \left(I\right)$
\STATE $\text{randN} \gets b \times \text{len} \left(I\right)$
\FOR{$i = 1$ \TO $d$}
    \STATE $\text{preds} \gets \text{models.predict} \left(I\right)$
    \STATE $\text{g} \gets loss \left(I, \text{preds}\right)$
    \STATE $\text{w} \gets \left\{1, 1, \dotsc\right\}$
    \STATE $\text{sorted} \gets \text{GetSortedIndices} \left(\text{abs} \left(\text{g}\right)\right)$
    \STATE $\text{topSet} \gets \text{sorted[1:topN]}$
    \STATE $\text{randSet} \gets \text{RandomPick} \left(\text{sorted[topN:len}\left(I\right)\text{]}, \text{randN}\right)$
    \STATE $\text{usedSet} \gets \text{topSet} \cup \text{randSet}$
    \STATE $\text{w[randSet]} \gets \text{w[randSet]} \times \text{fact}$
    \STATE $\text{newModel} \gets L \left(I \text{[usedSet]}, - \text{g[usedSet]}, \text{w[usedSet]}\right)$
    \STATE $\text{models} \gets \text{models} \cup \text{newModel}$
\ENDFOR
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<h4 id="exclusive-feature-bundling">Exclusive Feature Bundling</h4>
<p>高维数据往往是稀疏的，特征空间的稀疏性为我们提供了可能的近似无损的特征降维实现。进一步而言，在稀疏的特征空间中，很多特征之间是互斥的，也就是说它们不同时取非零值。因此，我们就可以将这些互斥的特征绑定成一个特征。由于 <code>$\#bundle \ll \#feature$</code>，因此构建直方图的复杂度就可以从 <code>$O \left(\#data \times \#features\right)$</code> 减小至 <code>$O \left(\#data \times \#bundle\right)$</code>，从而在不损失精度的情况下加速模型的训练。这样我们就需要解决如下两个问题：</p>
<ol>
<li>确定对哪些特征进行绑定。</li>
<li>如果对这些特征进行绑定。</li>
</ol>
<p>对哪些特征进行绑定可以利用 <a href="https://en.wikipedia.org/wiki/Graph_coloring">图着色问题</a> 进行解决。对于一个图 <code>$G = \left(V, E\right)$</code>，将 <code>$G$</code> 的 <a href="https://en.wikipedia.org/wiki/Incidence_matrix">关联矩阵</a> 中的每一行看成特征，得到 <code>$|V|$</code> 个特征，从而可以得出图中颜色相同的节点即为互斥的特征。算法如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Greedy Bundling}
\begin{algorithmic}
\INPUT \\
    特征 $F$ \\
    最大冲突数量 $K$
\OUTPUT \\
    需要绑定的特征 $bundles$
\FUNCTION{GreedyBundling}{$F, K$}
\STATE Construct graph $G$
\STATE $\text{searchOrder} \gets G.\text{sortByDegree}()$
\STATE $\text{bundles} \gets \varnothing$
\STATE $\text{bundlesConflict} \gets \varnothing$
\FOR{$i$ $\in$ searchOrder}
    \STATE $\text{needNew} \gets$ \TRUE
    \FOR{$j = 1$ \TO len(bundles)}
        \STATE $\text{cnt} \gets$ ConflictCnt(bundles[$j$],F[$i$])
        \IF{cnt $+$ bundlesConflict[$i$] $\leq K$}
            \STATE bundles[$j$].add($F[i]$)
            \STATE $\text{needNew} \gets$ \FALSE
            \BREAK
        \ENDIF
    \ENDFOR
    \IF{needNew}
        \STATE $bundles \gets bundles \cup F[i]$
    \ENDIF
\ENDFOR
\RETURN $bundles$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>上述算法的复杂度为 <code>$O \left(\#feature^2\right)$</code> ，并且仅在模型训练前运行一次。对于特征数不是很大的情况是可以接受的，但当特征数量很大时算法效率并不令人满意。进一步的优化是在不构造图的情况下进行高效的排序，即根据非零值的数量进行排序，更多的非零值意味着更高的冲突概率。</p>
<p>合并特征的关键在于确保原始特征的值能够从合并后的特征之中识别出来。由于基于直方图的算法保存的是原始特征的离散桶，而非连续的值，因此我们可以将互斥的特征置于不同的桶内。算法如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Merge Exclusive Features}
\begin{algorithmic}
\REQUIRE \\
    数据数量 $numData$ \\
    一组互斥特征 $F$
\ENSURE \\
    新的分箱 $newBin$ \\
    分箱范围 $binRanges$
\FUNCTION{MergeExclusiveFeatures}{$numData, F$}
\STATE $\text{binRages} \gets \left\{0\right\}$
\STATE $\text{totalBin} \gets 0$
\FOR{$f$ $\in$ $F$}
    \STATE $\text{totalBin} \gets \text{totalBin} + \text{f.numBin}$
    \STATE $\text{binRanges} \gets \text{binRanges} \cup \text{totalBin}$
\ENDFOR
\STATE $\text{newBin} \gets \text{Bin} \left(numData\right)$
\FOR{$i = 1$ \TO $numData$}
    \STATE $\text{newBin}[i] \gets 0$
    \FOR{$j = 1$ \TO $\text{len} \left(F\right)$}
        \IF{$F[j].\text{bin}[i] \neq 0$}
            \STATE $\text{newBin}[i] \gets F[j].\text{bin}[i] + \text{binRanges}[j]$
        \ENDIF
    \ENDFOR
\ENDFOR
\RETURN $newBin, binRanges$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>EFB 算法可以将大量的互斥特征合并为少量的稠密特征，从而通过避免对零值特征的计算提高算法的运行效率。</p>
<h4 id="tree-growth">Tree Growth</h4>
<p>大多的决策树算法通过逐层 (Level-wise / Depth-wise) 的方法生成树，如下图所示：</p>
<p><img src="/images/cn/2018-12-08-ensemble-learning/tree-growth-level-wise.png" alt="Level-Wise-Tree-Growth"></p>
<p>LightGBM 采用了另外一种 Leaf-wise (或称 Best-first) 的方式生成 <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup>，如下图所示：</p>
<p><img src="/images/cn/2018-12-08-ensemble-learning/tree-growth-leaf-wise.png" alt="Leaf-Wise-Tree-Growth"></p>
<p>该方法想选择具有最大 Delta Loss 值的叶子节点进行生长。在固定叶子节点数量的情况下，Leaf-wise 的生长方式比 Level-wise 的方式更容易获得较低的损失值。Leaf-wise 的生长方式在数据量较小时容易产生过拟合的现象，在 LightGBM 中可以通过限制树的最大深度减轻该问题。</p>
<p>更多有关 LightGBM 的优化请参见论文和 <a href="https://github.com/Microsoft/LightGBM/blob/master/docs/Features.rst">文档</a>。</p>
<h3 id="catboost">CatBoost</h3>
<p>CatBoost 是由俄罗斯 Yandex 公司 <sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup> <sup id="fnref:13"><a href="#fn:13" class="footnote-ref" role="doc-noteref">13</a></sup> 提出的一种梯度提升树模型框架。相比于之前的实现，CatBoost 的优化主要包括如下几点：</p>
<ol>
<li>提出了一种处理分类特征 (Categorical Features) 的算法。</li>
<li>提出了一种解决预测偏移 (Prediction Shift) 问题的算法。</li>
</ol>
<h4 id="分类特征">分类特征</h4>
<p>分类特征是由一些离散的值构成的集合，其无法直接应用在提升树模型中，一个常用的方法是利用 One-Hot 编码对分类特征进行预处理，将其转化成值特征。</p>
<p>另一种方法是根据样本的标签值计算分类特征的一些统计量 (Target Statistics, TS)。令 <code>$\mathcal{D} = \left\{\left(\mathbf{x}_k, y_k\right)\right\}_{k=1, \dotsc, n}$</code> 为一个数据集，其中 <code>$\mathbf{x}_k = \left(x_k^1, \dotsc, x_k^m\right)$</code> 为一个包含 <code>$m$</code> 个特征的向量 (包含值特征和分类特征)，<code>$y_k \in \mathbb{R}$</code> 为标签值。最简单的做法是将分类特征替换为全量训练数据上对应特征值相同的标签值的均值，即 <code>$\hat{x}_k^i \approx \mathbb{E} \left(y \ | \ x^i = x_k^i\right)$</code>。</p>
<ul>
<li>Greedy TS</li>
</ul>
<p>一个简单估计 <code>$\mathbb{E} \left(y \ | \ x^i = x_k^i\right)$</code> 的方法是对具有相同类型 <code>$x_k^i$</code> 的样本的标签值求均值。但这种估计对于低频的分类噪音较大，因此我们可以通过一个先验 <code>$P$</code> 来进行平滑：</p>
<p><code>$$ \hat{x}_k^i = \dfrac{\sum_{j=1}^{n}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}} \cdot y_j} + a P}{\sum_{j=1}^{n}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}}} + a} $$</code></p>
<p>其中，<code>$a &gt; 0$</code> 为先验系数，<code>$\boldsymbol{1}$</code> 为指示函数，通常 <code>$P$</code> 取整个数据集标签值的均值。</p>
<p>上述贪婪 (Greedy) 的做法的问题在于存在目标泄露 (Target Leakage)，即特征 <code>$\hat{x}_k^i$</code> 是通过 <code>$\mathbf{x}_k$</code> 的目标 <code>$y_k$</code> 计算所得。这会导致条件偏移 (Conditional Shift) 的问题 <sup id="fnref:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup>，即 <code>$\hat{x}^i \ | \ y$</code> 的分布在训练集和测试集上不同。因此在计算 TS 时需要满足如下特性：</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>特性 1.</strong> <code>$\mathbb{E} \left(\hat{x}^i \ | \ y = v\right) = \mathbb{E} \left(\hat{x}_k^i \ | \ y_k = v\right)$</code>，其中 <code>$\left(x_k, y_k\right)$</code> 为第 <code>$k$</code> 个训练样本。</div>
<p>一种修正方法是在计算 TS 时使用排除掉 <code>$\mathbf{x}_k$</code> 的一个子集，令 <code>$\mathcal{D}_k \subset \mathcal{D} \setminus \left\{\mathbf{x}_k\right\}$</code>，有：</p>
<p><code>$$ \hat{x}_k^i = \dfrac{\sum_{\mathbf{x}_j \in \mathcal{D}_k}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}} \cdot y_j} + a P}{\sum_{\mathbf{x}_j \in \mathcal{D}_k}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}}} + a} $$</code></p>
<ul>
<li>Holdout TS</li>
</ul>
<p>另一种方法是将训练集划分为两部分 <code>$\mathcal{D} = \hat{\mathcal{D}}_0 \sqcup \hat{\mathcal{D}_1}$</code>，利用 <code>$\mathcal{D}_k = \hat{\mathcal{D}}_0$</code> 计算 TS，利用 <code>$\hat{\mathcal{D}_1}$</code> 进行训练。虽然满足了 <strong>特性 1</strong>，但是这会导致计算 TS 和用于训练的数据均显著减少，因此还需要满足另一个特性：</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>特性 2.</strong> 有效地利用所有的训练数据计算 TS 和训练模型。</div>
<ul>
<li>Leave-one-out TS</li>
</ul>
<p>对于训练样本 <code>$\mathbf{x}_k$</code> 令 <code>$\mathcal{D}_k = \mathcal{D} \setminus \mathbf{x}_k$</code>，对于测试集，令 <code>$\mathcal{D}_k = \mathcal{D}$</code>，但这并没有解决 Target Leakage 问题。</p>
<ul>
<li>Ordered TS</li>
</ul>
<p>Catboost 采用了一种更有效的策略：首先对于训练样本进行随机排列，得到排列下标 <code>$\sigma$</code>，之后对于每个训练样本仅利用“历史”样本来计算 TS，即：<code>$\mathcal{D}_k = \left\{\mathbf{x}_j: \sigma \left(j\right) &lt; \sigma \left(k\right)\right\}$</code>，对于每个测试样本 <code>$\mathcal{D}_k = \mathcal{D}$</code>。</p>
<h4 id="prediction-shift-ordered-boosting">Prediction Shift &amp; Ordered Boosting</h4>
<p>类似计算 TS，Prediction Shift 是由一种特殊的 Target Leakage 所导致的。对于第 <code>$t$</code> 次迭代，我们优化的目标为：</p>
<p><code>$$ h^t = \mathop{\arg\min}_{h \in H} \mathbb{E} \left(-g^t \left(\mathbf{x}, y\right) - h \left(\mathbf{x}\right)\right)^2  \label{eq:catboost-obj} $$</code></p>
<p>其中，<code>$g^t \left(\mathbf{x}, y\right) := \dfrac{\partial L \left(y, s\right)}{\partial s} \bigg\vert_{s = F^{t-1} \left(\mathbf{x}\right)}$</code>。通常情况下会使用相同的数据集 <code>$\mathcal{D}$</code> 进行估计：</p>
<p><code>$$ h^t = \mathop{\arg\min}_{h \in H} \dfrac{1}{n} \sum_{k=1}^{n}{\left(-g^t \left(\mathbf{x}_k, y_k\right) - h \left(\mathbf{x}_k\right)\right)^2} \label{eq:catboost-obj-approx} $$</code></p>
<p>整个偏移的链条如下：</p>
<ol>
<li>梯度的条件分布 <code>$g^t \left(\mathbf{x}_k, y_k\right) \ | \ \mathbf{x}_k$</code> 同测试样本对应的分布 <code>$g^t \left(\mathbf{x}, y\right) \ | \ \mathbf{x}$</code> 存在偏移。</li>
<li>由式 <code>$\ref{eq:catboost-obj}$</code> 定义的基学习器 <code>$h^t$</code> 同由式 <code>$\ref{eq:catboost-obj-approx}$</code> 定义的估计方法存在偏移。</li>
<li>最终影响训练模型 <code>$F^t$</code> 的泛化能力。</li>
</ol>
<p>每一步梯度的估计所使用的标签值同构建当前模型 <code>$F^{t-1}$</code> 使用的相同。但是，对于一个训练样本 <code>$\mathbf{x}_k$</code> 而言，条件分布 <code>$F^{t-1} \left(\mathbf{x}_k \ | \ \mathbf{x}_k\right)$</code> 相对一个测试样本 <code>$\mathbf{x}$</code> 对应的分布 <code>$F^{t-1} \left(\mathbf{x}\right) \ | \ \mathbf{x}$</code> 发生了偏移，我们称这为预测偏移 (Prediction Shift)。</p>
<p>CatBoost 提出了一种解决 Prediction Shift 的算法：Ordered Boosting。假设对于训练数据进行随机排序得到 <code>$\sigma$</code>，并有 <code>$n$</code> 个不同的模型 <code>$M_1, \dotsc, M_n$</code>，每个模型 <code>$M_i$</code> 仅利用随机排序后的前 <code>$i$</code> 个样本训练得到。算法如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Ordered Boosting}
\begin{algorithmic}
\INPUT \\
    训练集 $\left\{\left(\mathbf{x}_k, y_k\right)\right\}_{k=1}^{n}$ \\
    树的个数 $I$
\OUTPUT \\
    模型 $M_n$
\FUNCTION{OrderedBoosting}{$numData, F$}
\STATE $\sigma \gets \text{random permutation of} \left[1, n\right]$
\STATE $M_i \gets 0$ for $i = 1, \dotsc, n$
\FOR{$t = 1$ \TO $I$}
    \FOR{$i = 1$ \TO $n$}
        \STATE $r_i \gets y_i - M_{\sigma \left(i\right) -1} \left(\mathbf{x}_i\right)$
    \ENDFOR
    \FOR{$i = 1$ \TO $n$}
        \STATE $\Delta M \gets \text{LearnModel} \left(\left(\mathbf{x}_j, r_j\right): \sigma \left(j\right) \leq i\right)$
        \STATE $M_i \gets M_i + \Delta M$
    \ENDFOR
\ENDFOR
\RETURN $M_n$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>在计算 TS 和进行 Ordered Boosting 时我们均使用了随机排列并得到 <code>$\sigma_{cat}$</code> 和 <code>$\sigma_{boost}$</code>。需要注意的是在将两部分合并为一个算法时，我们需要令 <code>$\sigma_{cat} = \sigma_{boost}$</code> 避免 Prediction Shift。这样可以保证目标 <code>$y_i$</code> 不用于训练模型 <code>$M_i$</code> (既不参与计算 TS，也不用于梯度估计)。</p>
<p>更多 CatBoost 的实现细节请参见论文和 <a href="https://tech.yandex.com/catboost/">文档</a>。</p>
<h3 id="不同实现的比较">不同实现的比较</h3>
<p>针对 <a href="https://github.com/scikit-learn/scikit-learn">scikit-learn</a>，<a href="https://github.com/dmlc/xgboost">XGBoost</a>，<a href="https://github.com/Microsoft/LightGBM">LightGBM</a> 和 <a href="https://github.com/catboost/catboost">CatBoost</a> 4 种 GBDT 的具体实现，下表汇总了各自的相关特性：</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>scikit-learn</th>
          <th>XGBoost</th>
          <th>LightGBM</th>
          <th>CatBoost</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>当前版本</strong></td>
          <td>0.20.1</td>
          <td>0.81</td>
          <td>2.2.2</td>
          <td>0.11.1</td>
      </tr>
      <tr>
          <td><strong>实现语言</strong></td>
          <td>C, C++, Python</td>
          <td>C, C++</td>
          <td>C, C++</td>
          <td>C++</td>
      </tr>
      <tr>
          <td><strong>API 语言</strong></td>
          <td>Python</td>
          <td>Python, R, Java, Scala, C++ and more</td>
          <td>Python, R</td>
          <td>Python, R</td>
      </tr>
      <tr>
          <td><strong>模型导出</strong></td>
          <td>JPMML <sup id="fnref:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup></td>
          <td>JPMML <sup id="fnref:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup>, ONNX <sup id="fnref:17"><a href="#fn:17" class="footnote-ref" role="doc-noteref">17</a></sup> <sup id="fnref:18"><a href="#fn:18" class="footnote-ref" role="doc-noteref">18</a></sup></td>
          <td>ONNX <sup id="fnref1:17"><a href="#fn:17" class="footnote-ref" role="doc-noteref">17</a></sup> <sup id="fnref:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup></td>
          <td>CoreML, Python, C++, JSON <sup id="fnref:20"><a href="#fn:20" class="footnote-ref" role="doc-noteref">20</a></sup></td>
      </tr>
      <tr>
          <td><strong>多线程</strong></td>
          <td>No</td>
          <td>Yes</td>
          <td>Yes</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td><strong>GPU</strong></td>
          <td>No</td>
          <td>Yes <sup id="fnref:21"><a href="#fn:21" class="footnote-ref" role="doc-noteref">21</a></sup></td>
          <td>Yes <sup id="fnref:22"><a href="#fn:22" class="footnote-ref" role="doc-noteref">22</a></sup></td>
          <td>Yes <sup id="fnref:23"><a href="#fn:23" class="footnote-ref" role="doc-noteref">23</a></sup></td>
      </tr>
      <tr>
          <td><strong>多 GPU</strong></td>
          <td>No</td>
          <td>Yes <sup id="fnref1:21"><a href="#fn:21" class="footnote-ref" role="doc-noteref">21</a></sup></td>
          <td>No</td>
          <td>Yes <sup id="fnref1:23"><a href="#fn:23" class="footnote-ref" role="doc-noteref">23</a></sup></td>
      </tr>
      <tr>
          <td><strong>Boosting 类型</strong></td>
          <td>Gradient Boosted Tree (<strong>GBDT</strong>)</td>
          <td><strong>GBDT</strong> (booster: gbtree) <br/>Generalized Linear Model, <strong>GLM</strong> (booster: gbliner) <br/>Dropout Additive Regression Tree, <strong>DART</strong> (booster: dart)</td>
          <td><strong>GBDT</strong> (boosting: gbdt) <br/><strong>Random Forest</strong> (boosting: rf) <br/><strong>DART</strong> (boosting: dart) <br/>Gradient-based One-Side Sampling, <strong>GOSS</strong> (bossting: goss)</td>
          <td><strong>Ordered</strong> (boosting_type: Ordered) <br/><strong>Plain</strong> (bossting_type: Plain)</td>
      </tr>
      <tr>
          <td><strong>Level-wise (Depth-wise) Split</strong></td>
          <td>Yes</td>
          <td>Yes <br/>(grow_policy: depthwise)</td>
          <td>No</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td><strong>Leaf-wise (Best-first) Split</strong></td>
          <td>No</td>
          <td>Yes <br/>(grow_policy: lossguide)</td>
          <td>Yes</td>
          <td>No</td>
      </tr>
      <tr>
          <td><strong>Histogram-based Split</strong></td>
          <td>No</td>
          <td>Yes <br/>(tree_method: hist / gpu_hist)</td>
          <td>Yes</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td><strong>过拟合控制</strong></td>
          <td>Yes <br>(max_depth, &hellip;)</td>
          <td>Yes <br/>(max_depth, max_leaves, gamma, reg_alpha, reg_lamda, &hellip;)</td>
          <td>Yes <br/>(max_depth, num_leaves, gamma, reg_alpha, reg_lamda, drop_rate, &hellip;)</td>
          <td>Yes <br/>(max_depth, reg_lambda, &hellip;)</td>
      </tr>
      <tr>
          <td><strong>分类特征</strong></td>
          <td>No</td>
          <td>No</td>
          <td>Yes <br/>(categorical_feature)</td>
          <td>Yes <br/>(cat_features)</td>
      </tr>
      <tr>
          <td><strong>缺失值处理</strong></td>
          <td>No</td>
          <td>Yes</td>
          <td>Yes <br/>(use_missing)</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td><strong>不均衡数据</strong></td>
          <td>No</td>
          <td>Yes <br/>(scale_pos_weight, max_delta_step)</td>
          <td>Yes <br/>(scale_pos_weight, poisson_max_delta_step)</td>
          <td>Yes <br/>(scale_pos_weight)</td>
      </tr>
  </tbody>
</table>
<p>不同实现的性能分析和比较可参见如下文章，括号中内容为分析的实现库：</p>
<ul>
<li><a href="https://xgboost.ai/2016/12/14/GPU-accelerated-xgboost.html">GPU Accelerated XGBoost</a> (XGBoost)</li>
<li><a href="https://xgboost.ai/2018/07/04/gpu-xgboost-update.html">Updates to the XGBoost GPU algorithms</a> (XGBoost)</li>
<li><a href="https://lightgbm.readthedocs.io/en/latest/Experiments.html">LightGBM Experiments</a> (XGBoost, LightGBM)，<a href="https://github.com/guolinke/boosting_tree_benchmarks">代码</a></li>
<li><a href="https://lightgbm.readthedocs.io/en/latest/GPU-Performance.html">GPU Tunning Guide and Performance Comparision</a> (LightGBM)</li>
<li><a href="https://blogs.technet.microsoft.com/machinelearning/2017/07/25/lessons-learned-benchmarking-fast-machine-learning-algorithms/">Lessons Learned From Benchmarking Fast Machine Learning Algorithms</a> (XGBoost, LightGBM), <a href="https://github.com/Azure/fast_retraining">代码</a></li>
<li><a href="https://github.com/catboost/benchmarks">CatBoost Benchmarks</a> (XGBoost, LightGBM, CatBoost, H2O)</li>
<li><a href="https://arxiv.org/abs/1809.04559">Benchmarking and Optimization of Gradient Boosted Decision Tree Algorithms</a> (XGBoost, LightGBM, CatBoost)</li>
<li><a href="https://sites.google.com/view/lauraepp/home">Laurae++: xgboost / LightGBM</a> (XGBoost, LightGBM), <a href="https://github.com/Laurae2/gbt_benchmarks">代码</a></li>
<li><a href="https://github.com/szilard/GBM-perf">GBM Performance</a> (XGBoost, LightGBM, H2O), <a href="https://github.com/szilard/GBM-perf">代码</a></li>
</ul>
<h2 id="stacking">Stacking</h2>
<p>Stacking 本身是一种集成学习方法，同时也是一种模型组合策略，我们首先介绍一些相对简单的模型组合策略：<strong>平均法</strong> 和 <strong>投票法</strong>。</p>
<p>对于 <strong>数值型的输出</strong> <code>$h_i \left(\mathbf{x}\right) \in \mathbb{R}$</code>，</p>
<ul>
<li>简单平均法 (Simple Averaging)</li>
</ul>
<p><code>$$ H \left(\mathbf{x}\right) = \dfrac{1}{M} \sum_{i=1}^{M}{h_i \left(\mathbf{x}\right)} $$</code></p>
<ul>
<li>加权平均法 (Weighted Averaging)</li>
</ul>
<p><code>$$ H \left(\mathbf{x}\right) = \sum_{i=1}^{M}{w_i h_i \left(\mathbf{x}\right)} $$</code></p>
<p>其中，<code>$w_i$</code> 为学习器 <code>$h_i$</code> 的权重，且 <code>$w_i \geq 0, \sum_{i=1}^{T}{w_i} = 1$</code>。</p>
<p>对于 <strong>分类型的任务</strong>，学习器 <code>$h_i$</code> 从类别集合 <code>$\left\{c_1, c_2, \dotsc, c_N\right\}$</code> 中预测一个标签。我们将 <code>$h_i$</code> 在样本 <code>$\mathbf{x}$</code> 上的预测输出表示为一个 <code>$N$</code> 维向量 <code>$\left(h_i^1 \left(\mathbf{x}\right); h_i^2 \left(\mathbf{x}\right); \dotsc, h_i^N \left(\mathbf{x}\right)\right)$</code>，其中 <code>$h_i^j \left(\mathbf{x}\right)$</code> 为 <code>$h_i$</code> 在类型标签 <code>$c_j$</code> 上的输出。</p>
<ul>
<li>绝对多数投票法 (Majority Voting)</li>
</ul>
<p><code>$$ H \left(\mathbf{x}\right) = \begin{cases} c_j, &amp; \displaystyle\sum_{i=1}^{M}{h_i^j \left(\mathbf{x}\right) &gt; 0.5 \displaystyle\sum_{k=1}^{N}{\displaystyle\sum_{i=1}^{M}{h_i^k \left(\mathbf{x}\right)}}} \\ \text{refuse}, &amp; \text{other wise} \end{cases} $$</code></p>
<p>即如果一个类型的标记得票数过半，则预测为该类型，否则拒绝预测。</p>
<ul>
<li>相对多数投票法 (Plurality Voting)</li>
</ul>
<p><code>$$ H \left(\mathbf{x}\right) = c_{\arg\max_j \sum_{i=1}^{M}{h_i^j \left(\mathbf{x}\right)}} $$</code></p>
<p>即预测为得票数最多的类型，如果同时有多个类型获得相同最高票数，则从中随机选取一个。</p>
<ul>
<li>加权投票法 （Weighted Voting)</li>
</ul>
<p><code>$$ H \left(\mathbf{x}\right) = c_{\arg\max_j \sum_{i=1}^{M}{w_i h_i^j \left(\mathbf{x}\right)}} $$</code></p>
<p>其中，<code>$w_i$</code> 为学习器 <code>$h_i$</code> 的权重，且 <code>$w_i \geq 0, \sum_{i=1}^{M}{w_i} = 1$</code>。</p>
<p>绝对多数投票提供了“拒绝预测”，这为可靠性要求较高的学习任务提供了一个很好的机制，但如果学习任务要求必须有预测结果时则只能选择相对多数投票法和加权投票法。在实际任务中，不同类型的学习器可能产生不同类型的 <code>$h_i^j \left(\boldsymbol{x}\right)$</code> 值，常见的有：</p>
<ul>
<li>类标记，<code>$h_i^j \left(\mathbf{x}\right) \in \left\{0, 1\right\}$</code>，若 <code>$h_i$</code> 将样本 <code>$\mathbf{x}$</code> 预测为类型 <code>$c_j$</code> 则取值为 1，否则取值为 0。使用类型标记的投票称之为 <strong>“硬投票” (Hard Voting)</strong>。</li>
<li>类概率，<code>$h_i^j \left(\mathbf{x}\right) \in \left[0, 1\right]$</code>，相当于对后验概率 <code>$P \left(c_j \ | \ \mathbf{x}\right)$</code> 的一个估计。使用类型概率的投票称之为 <strong>“软投票” (Soft Voting)</strong>。</li>
</ul>
<p>Stacking <sup id="fnref:24"><a href="#fn:24" class="footnote-ref" role="doc-noteref">24</a></sup> <sup id="fnref:25"><a href="#fn:25" class="footnote-ref" role="doc-noteref">25</a></sup> 方法又称为 Stacked Generalization，是一种基于分层模型组合的集成算法。Stacking 算法的基本思想如下：</p>
<ol>
<li>利用初级学习算法对原始数据集进行学习，同时生成一个新的数据集。</li>
<li>根据从初级学习算法生成的新数据集，利用次级学习算法学习并得到最终的输出。</li>
</ol>
<p>对于初级学习器，可以是相同类型也可以是不同类型的。在新的数据集中，初级学习器的输出被用作次级学习器的输入特征，初始样本的标记仍被用作次级学习器学习样本的标记。Stacking 算法的流程如下图所示：</p>
<p><img src="/images/cn/2018-12-08-ensemble-learning/stacking.png" alt="Stacking"></p>
<p>Stacking 算法过程如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Stacking 算法}
\begin{algorithmic}
\REQUIRE \\
    初级学习算法 $L = \{L_1, L_2, ... L_M\}$ \\
    次级学习算法 $L'$ \\
    训练数据集 $T = \{(\mathbf{x}_1, y_1), (\mathbf{x}_2, y_2), ..., (\mathbf{x}_N, y_N)\}$
\ENSURE \\
    Stacking 算法 $h_f\left(x\right)$
\FUNCTION{Stacking}{$L, L', T$}
\FOR{$m$ = $1$ to $M$}
  \STATE $h_t \gets L_m \left(T\right)$
\ENDFOR
\STATE $T' \gets \varnothing$
\FOR{$i$ = $1$ to $N$}
  \FOR{$m$ = $1$ to $M$}
    \STATE $z_{im} \gets h_m(\mathbf{x}_i)$
  \ENDFOR
  \STATE $T' \gets T' \cup \left(\left(z_{i1}, z_{i2}, ..., z_{iM}\right), y_i\right)$
\ENDFOR
\STATE $h' \gets L' \left(T'\right)$
\STATE $h_f\left(\mathbf{x}\right) \gets h' \left(h_1\left(\mathbf{x}\right), h_2\left(\mathbf{x}\right), ..., h_M\left(\mathbf{x}\right)\right)$
\RETURN $h_f\left(\mathbf{x}\right)$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>次级学习器的训练集是有初级学习器产生的，如果直接利用初级学习器的训练集生成次级学习器的训练集，过拟合风险会比较大 <sup id="fnref:26"><a href="#fn:26" class="footnote-ref" role="doc-noteref">26</a></sup>。因此，一般利用在训练初级学习器中未使用过的样本来生成次级学习器的训练样本。以 <code>$k$</code> 折交叉检验为例：初始的训练集 <code>$T$</code> 被随机划分为 <code>$k$</code> 个大小相近的集合 <code>$T_1, T_2, ..., T_k$</code>。令 <code>$T_j$</code> 和 <code>$\overline{T}_j = T \setminus T_j$</code> 表示第 <code>$j$</code> 折的测试集和训练集。则对于 <code>$M$</code> 个初级学习算法，学习器 <code>$h_m^{\left(j\right)}$</code> 是根据训练集 <code>$\overline{T}_j$</code> 生成的，对于测试集 <code>$T_j$</code> 中的每个样本 <code>$\mathbf{x}_i$</code>，得到 <code>$z_{im} = h_m^{\left(j\right)} \left(\mathbf{x}_i\right)$</code>。则根据 <code>$\mathbf{x}_i$</code> 所产生的次级学习器的训练样本为 <code>$\mathbf{z}_i = \left(\left(z_{i1}, z_{i2}, ..., z_{iM}\right), y_i\right)$</code>。最终利用 <code>$M$</code> 个初级学习器产生的训练集 <code>$T' = \{\left(\mathbf{z}_i, y_i\right)\}_{i=1}^N$</code> 训练次级学习器。</p>
<p>下图展示了一些基础分类器以及 Soft Voting 和 Stacking 两种融合策略的模型在 Iris 数据集分类任务上的决策区域。数据选取 Iris 数据集中的 Sepal Length 和 Petal Length 两个特征，Stacking 中的次级学习器选择 Logistic Regression，详细实现请参见 <a href="https://github.com/leovan/leovan.me/blob/master/scripts/cn/2018-12-08-ensemble-learning/clfs-decision-regions.py">这里</a>。</p>
<p><img src="/images/cn/2018-12-08-ensemble-learning/clfs-decision-regions.png" alt="Classifiers-Decision-Regions"></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Dietterich, T. G. (2000, June). Ensemble methods in machine learning. In <em>International workshop on multiple classifier systems</em> (pp. 1-15).&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Dietterich, T. G. (2002). <em>Ensemble Learning, The Handbook of Brain Theory and Neural Networks</em>, MA Arbib.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Laurent, H., &amp; Rivest, R. L. (1976). Constructing optimal binary decision trees is NP-complete. <em>Information processing letters</em>, 5(1), 15-17.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Blum, A., &amp; Rivest, R. L. (1989). Training a 3-node neural network is NP-complete. In <em>Advances in neural information processing systems</em> (pp. 494-501).&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Breiman, L. (1996). Bagging predictors. <em>Machine learning, 24</em>(2), 123-140.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Breiman, L. (2001). Random forests. <em>Machine learning, 45</em>(1), 5-32.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Fernández-Delgado, M., Cernadas, E., Barro, S., &amp; Amorim, D. (2014). Do we need hundreds of classifiers to solve real world classification problems?. <em>The Journal of Machine Learning Research, 15</em>(1), 3133-3181.&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Freund, Y., &amp; Schapire, R. E. (1997). A decision-theoretic generalization of on-line learning and an application to boosting. <em>Journal of computer and system sciences, 55</em>(1), 119-139.&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Chen, T., &amp; Guestrin, C. (2016). XGBoost: A Scalable Tree Boosting System. In <em>Proceedings of the 22Nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining</em> (pp. 785–794).&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Ke, G., Meng, Q., Finley, T., Wang, T., Chen, W., Ma, W., … Liu, T.-Y. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. In I. Guyon, U. V. Luxburg, S. Bengio, H. Wallach, R. Fergus, S. Vishwanathan, &amp; R. Garnett (Eds.), <em>Advances in Neural Information Processing Systems 30</em> (pp. 3146–3154).&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Shi, H. (2007). <em>Best-first Decision Tree Learning</em> (Thesis). The University of Waikato.&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p>Dorogush, A. V., Ershov, V., &amp; Gulin, A. (2018). CatBoost: gradient boosting with categorical features support. <em>arXiv preprint arXiv:1810.11363</em>&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:13">
<p>Prokhorenkova, L., Gusev, G., Vorobev, A., Dorogush, A. V., &amp; Gulin, A. (2018). CatBoost: unbiased boosting with categorical features. In S. Bengio, H. Wallach, H. Larochelle, K. Grauman, N. Cesa-Bianchi, &amp; R. Garnett (Eds.), <em>Advances in Neural Information Processing Systems 31</em> (pp. 6637–6647).&#160;<a href="#fnref:13" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:14">
<p>Zhang, K., Schölkopf, B., Muandet, K., &amp; Wang, Z. (2013, February). Domain adaptation under target and conditional shift. In <em>International Conference on Machine Learning</em> (pp. 819-827).&#160;<a href="#fnref:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:15">
<p><a href="https://github.com/jpmml/jpmml-sklearn">https://github.com/jpmml/jpmml-sklearn</a>&#160;<a href="#fnref:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:16">
<p><a href="https://github.com/jpmml/jpmml-xgboost">https://github.com/jpmml/jpmml-xgboost</a>&#160;<a href="#fnref:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:17">
<p><a href="https://github.com/onnx/onnx">https://github.com/onnx/onnx</a>&#160;<a href="#fnref:17" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:17" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:18">
<p><a href="https://pypi.org/project/winmltools">https://pypi.org/project/winmltools</a>&#160;<a href="#fnref:18" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:19">
<p><a href="https://github.com/onnx/onnxmltools">https://github.com/onnx/onnxmltools</a>&#160;<a href="#fnref:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:20">
<p><a href="https://tech.yandex.com/catboost/doc/dg/concepts/python-reference_catboost_save_model-docpage">https://tech.yandex.com/catboost/doc/dg/concepts/python-reference_catboost_save_model-docpage</a>&#160;<a href="#fnref:20" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:21">
<p><a href="https://xgboost.readthedocs.io/en/latest/gpu/index.html">https://xgboost.readthedocs.io/en/latest/gpu/index.html</a>&#160;<a href="#fnref:21" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:21" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:22">
<p><a href="https://lightgbm.readthedocs.io/en/latest/GPU-Tutorial.html">https://lightgbm.readthedocs.io/en/latest/GPU-Tutorial.html</a>&#160;<a href="#fnref:22" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:23">
<p><a href="https://tech.yandex.com/catboost/doc/dg/features/training-on-gpu-docpage">https://tech.yandex.com/catboost/doc/dg/features/training-on-gpu-docpage</a>&#160;<a href="#fnref:23" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:23" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:24">
<p>Wolpert, D. H. (1992). Stacked generalization. <em>Neural networks, 5</em>(2), 241-259.&#160;<a href="#fnref:24" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:25">
<p>Breiman, L. (1996). Stacked regressions. <em>Machine learning, 24</em>(1), 49-64.&#160;<a href="#fnref:25" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:26">
<p>周志华. (2016). <em>机器学习</em>. 清华大学出版社.&#160;<a href="#fnref:26" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>计算复杂性 (Computational Complexity) 与动态规划 (Dynamic Programming)</title><link>https://zeqiang.fun/cn/2018/11/computational-complexity-and-dynamic-programming/</link><pubDate>Sun, 18 Nov 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/11/computational-complexity-and-dynamic-programming/</guid><description><![CDATA[
        <h2 id="计算复杂性">计算复杂性</h2>
<p><strong>计算复杂性 (Computational Complexity)</strong> 是用于对一个问题求解所需的资源 (通常为 <strong>空间</strong> 和 <strong>时间</strong>) 的度量。在评估一个算法的时候，除了算法本身的准确性以外，同时需要关注算法运行的时间以及占用的内存，从而根据实际情况选择合适的算法。</p>
<h3 id="函数的增长">函数的增长</h3>
<p>计算复杂性中的空间和时间的评估方法类似，在此我们更多的以时间复杂度为例。算法的运行时间刻画了算法的效率，对于一个输入规模为 <code>$n$</code> 的问题，定义一个算法求解该问题 <strong>最坏情况</strong> 下的运行时间为 <code>$T \left(n\right)$</code>，我们可以使用一些 <strong>渐进记号</strong> 更加方便地对其进行描述。</p>
<ul>
<li><strong><code>$\Theta$</code> 记号</strong></li>
</ul>
<p>对于一个给定的函数 <code>$g \left(n\right)$</code>，<code>$\Theta \left(g \left(n\right)\right)$</code> 可以表示如下函数的集合：</p>
<p><code>$$ \Theta \left(g \left(n\right)\right) = \left\{f \left(n\right): \exists c_1 &gt; 0, c_2 &gt; 0, n_0 &gt; 0, s.t. \forall n \geq n_0, 0 \leq c_1 g \left(n\right) \leq f \left(n\right) \leq c_2 g \left(n\right) \right\} $$</code></p>
<p>也就是说当 <code>$n$</code> 足够大时，函数 <code>$f \left(n\right)$</code> 能够被 <code>$c_1 g \left(n\right)$</code> 和 <code>$c_2 g \left(n\right)$</code> 夹在中间，我们称 <code>$g \left(n\right)$</code> 为 <code>$f \left(n\right)$</code> 的一个 <strong>渐进紧确界 (Asymptotically Tight Bound)</strong>。</p>
<ul>
<li><strong><code>$O$</code> 记号</strong></li>
</ul>
<p><code>$\Theta$</code> 记号给出了一个函数的上界和下界，当只有一个 <strong>渐进上界</strong> 时，可使用 <code>$O$</code> 记号。<code>$O \left(g \left(n\right)\right)$</code> 表示的函数集合为：</p>
<p><code>$$ O \left(g \left(n\right)\right) = \left\{f \left(n\right): \exists c &gt; 0, n_0 &gt; 0, s.t. \forall n \geq n_0, 0 \leq f \left(n\right) \leq c g \left(n\right)\right\} $$</code></p>
<p><code>$O$</code> 记号描述的为函数的上界，因此可以用它来限制算法在最坏情况下的运行时间。</p>
<ul>
<li><strong><code>$\Omega$</code> 记号</strong></li>
</ul>
<p><code>$\Omega$</code> 记号提供了 <strong>渐进下界</strong>，其表示的函数集合为：</p>
<p><code>$$ \Omega \left(g \left(n\right)\right) = \left\{f \left(n\right): \exists c &gt; 0, n_0 &gt; 0, s.t. \forall n \geq n_0, 0 \leq c g \left(n\right) \leq f \left(n\right)\right\} $$</code></p>
<p>根据上面的三个渐进记号，不难证明如下定理：</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'><strong>定理 1</strong> 对于任意两个函数 <code>$f \left(n\right)$</code> 和 <code>$g \left(n\right)$</code>，有 <code>$f \left(n\right) = \Theta \left(g \left(n\right)\right)$</code>，当且仅当 <code>$f \left(n\right) = O \left(g \left(n\right)\right)$</code> 且 <code>$f \left(n\right) = \Omega \left(g \left(n\right)\right)$</code>。</div>
<ul>
<li><strong><code>$o$</code> 记号</strong></li>
</ul>
<p><code>$O$</code> 记号提供的渐进上界可能是也可能不是渐进紧确的，例如 <code>$2n^2 = O \left(n^2\right)$</code> 是渐进紧确的，但 <code>$2n = O \left(n^2\right)$</code> 是非渐进紧确的。我们使用 <code>$o$</code> 记号表示非渐进紧确的上界，其表示的函数集合为：</p>
<p><code>$$ o \left(g \left(n\right)\right) = \left\{f \left(n\right): \forall c &gt; 0, \exists n_0 &gt; 0, s.t. \forall n \geq n_0, 0 \leq f \left(n\right) &lt; c g \left(n\right)\right\} $$</code></p>
<ul>
<li><strong><code>$\omega$</code> 记号</strong></li>
</ul>
<p><code>$\omega$</code> 记号与 <code>$\Omega$</code> 记号的关系类似于 <code>$o$</code> 记号与 <code>$O$</code> 记号的关系，我们使用 <code>$\omega$</code> 记号表示一个非渐进紧确的下界，其表示的函数集合为：</p>
<p><code>$$ \omega \left(g \left(n\right)\right) = \left\{f \left(n\right): \forall c &gt; 0, \exists n_0 &gt; 0, s.t. \forall n \geq n_0, 0 \leq c g \left(n\right) &lt; f \left(n\right)\right\} $$</code></p>
<h3 id="np-完全性">NP 完全性</h3>
<p>计算问题可以按照在不同计算模型下所需资源的不同予以分类，从而得到一个对算法问题“难度”的类别，这就是复杂性理论中复杂性类概念的来源 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。对于输入规模为 <code>$n$</code> 的问题，一个算法在最坏情况下的运行时间为 <code>$O \left(n^k\right)$</code>，其中 <code>$k$</code> 为一个确定的常数，我们称这类算法为 <strong>多项式时间的算法</strong>。</p>
<p>本节我们将介绍四类问题：P 类问题，NP 类问题，NPC 类问题和 NPH 类问题。</p>
<ul>
<li><strong>P 类问题</strong></li>
</ul>
<p>P 类问题 (Polynomial Problem，多项式问题) 是指能在多项式时间内 <strong>解决</strong> 的问题。</p>
<ul>
<li><strong>NP 类问题</strong></li>
</ul>
<p>NP 类问题 (Non-Deteministic Polynomial Problem，非确定性多项式问题) 是指能在多项式时间内被 <strong>证明</strong> 的问题，也就是可以在多项式时间内对于一个给定的解验证其是否正确。所有的 P 类问题都是 NP 类问题，但目前 (截至 2018 年，下文如不做特殊说明均表示截至到该时间) 人类还未证明 <code>$P \neq NP$</code> 还是 <code>$P = NP$</code>。</p>
<ul>
<li><strong>NPC 类问题 (NP-Complete Problems)</strong></li>
</ul>
<p>在理解 NPC 类问题之前，我们需要引入如下几个概念：</p>
<ol>
<li><strong>最优化问题 (Optimization Problem)</strong> 与 <strong>判定问题 (Decision Problem)</strong>：最优化问题是指问题的每一个可行解都关联一个值，我们希望找到具有最佳值的可行解。判定问题是指问题的答案仅为“是”或“否”的问题。NP 完全性仅适用于判定问题，但通过对最优化问题强加一个界，可以将其转换为判定问题。</li>
<li><strong>归约 (Reduction)</strong>：假设存在一个判定问题 A，该问题的输入称之为实例，我们希望能够在多项式时间内解决该问题。假设存在另一个不同的判定问题 B，并且已知能够在多项式时间内解决该问题，同时假设存在一个过程，它可以将 A 的任何实例 <code>$\alpha$</code> 转换成 B 的某个实例 <code>$\beta$</code>，转换操作需要在多项式时间内完成，同时两个实例的解是相同的。则我们称这一过程为多项式 <strong>规约算法 (Reduction Algorithm)</strong>。通过这个过程，我们可以将问题 A 的求解“归约”为对问题 B 的求解，从而利用问题 B 的“易求解性”来证明 A 的“易求解性”。</li>
</ol>
<p>从而我们可以定义 NPC 类问题为：首先 NPC 类问题是一个 NP 类问题，其次所有的 NP 类问题都可以用多项式时间归约到这类问题。因此，只要找到 NPC 类问题的一个多项式时间的解，则所有的 NP 问题都可以通过多项式时间归约到该问题，并用多项式时间解决该问题，从而使得 <code>$NP = P$</code>，但目前，NPC 类问题并没有找到一个多项式时间的算法。</p>
<ul>
<li><strong>NPH 类问题 (NP-Hard Problems)</strong></li>
</ul>
<p>NPH 类问题定义为所有的 NP 类问题都可以通过多项式时间归约到这类问题，但 NPH 类问题不一定是 NP 类问题。NPH 类问题同样很难找到多项式时间的解，由于 NPH 类问题相比较 NPC 类问题放松了约束，因此即便 NPC 类问题找到了多项式时间的解，NPH 类问题仍可能无法在多项式时间内求解。</p>
<p>下图分别展示了 <code>$P \neq NP$</code> 和 <code>$P = NP$</code> 两种假设情况下四类问题之间的关系：</p>
<p><img src="/images/cn/2018-11-18-computational-complexity-and-dynamic-programming/p-np-np-complete-np-hard.svg" alt=""></p>
<h2 id="动态规划">动态规划</h2>
<p><strong>动态规划 (Dynamic Programming, DP)</strong> 算法通常基于一个递归公式和一个或多个初始状态，并且当前子问题的解可以通过之前的子问题构造出来。动态规划算法求解问题的时间复杂度仅为多项式复杂度，相比其他解法，例如：回溯法，暴利破解法所需的时间要少。动态规划中的 “Programming” 并非表示利用计算机编程，而是一种表格法。动态规划对于每个子问题只求解一次，将解保存在一个表格中，从而避免不必要的重复计算。</p>
<p>动态规划算法的适用情况如下 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>：</p>
<ol>
<li><strong>最优子结构性质</strong>，即问题的最优解由相关子问题的最优解组合而成，子问题可以独立求解。</li>
<li><strong>无后效性</strong>，即每个状态均不会影响之前的状态。</li>
<li><strong>子问题重叠性质</strong>，即在用递归算法自顶向下对问题进行求解时，每次产生的子问题并不总是新问题，有些子问题会被重复计算多次。</li>
</ol>
<p>一个动态规划算法的核心包含两个部分：<strong>状态</strong> 和 <strong>状态转移方程</strong>。状态即一个子问题的表示，同时这个表示需要具备 <strong>无后效性</strong>。状态转移方程用于描述状态之间的关系，也就是如何利用之前的状态构造出当前的状态进而求解。</p>
<p>动态规划有两种等价的实现方法：</p>
<ol>
<li><strong>带备忘的自顶向下法 (Top-Down with Memoization)</strong>，该方法采用自然的递归形式编写过程，但会保留每个子问题的解，当需要一个子问题的解时会先检查是否保存过，如果有则直接返回该结果。</li>
<li><strong>自底向上法 (Bottom-Up Method)</strong>，该方法需要恰当的定义子问题“规模”，任何子问题的求解都值依赖于“更小”的子问题的求解，从而可以按照子问题的规模从小到大求解。</li>
</ol>
<p>两种方法具有相同的渐进运行时间，在某些特殊的情况下，自顶向下的方法并未真正递归地考虑所有可能的子问题；自底向上的方法由于没有频繁的递归调用，时间复杂性函数通常具有更小的系数。</p>
<h3 id="背包问题">背包问题</h3>
<p><strong>背包问题 (Knapsack problem)</strong> 是一种组合优化的 NPC 类问题。问题可以描述为：给定一组物品，每种物品都有自己的重量和价值，在限定的总重量内，合理地选择物品使得总价值最高。</p>
<p>形式化的定义，我们有 <code>$n$</code> 种物品，物品 <code>$j$</code> 的重量为 <code>$w_j$</code>，价值为 <code>$p_j$</code>，假定所有物品的重量和价值都是非负的，背包所能承受的最大重量为 <code>$W$</code>。如果限定每种物品只能选择 0 个或 1 个，则该问题称为 <strong>0-1 背包问题</strong>；如果限定物品 <code>$j$</code> 最多只能选择 <code>$b_j$</code> 个，则该问题称为 <strong>有界背包问题</strong>；如果不限定每种物品的数量，则该问题称为 <strong>无界背包问题</strong>。最优化问题可以表示为：</p>
<p><code>$$ \begin{equation} \begin{split} \text{maximize} &amp; \sum_{j=1}^{n}{p_j x_j} \\ s.t. &amp; \sum_{j=1}^{n}{w_j x_j} \leq W, x_j \in \left\{0, 1, ..., b_j\right\} \end{split} \end{equation} $$</code></p>
<p>以 0-1 背包问题为例，用 <code>$d_{i, w}$</code> 表示取 <code>$i$</code> 件商品填充一个最大承重 <code>$w$</code> 的背包的最大价值，问题的最优解即为 <code>$d_{n, W}$</code>。不难写出 0-1 背包问题的状态转移方程如下：</p>
<p><code>$$ d_{i, w} =  \begin{cases} d_{i - 1, w}, &amp; w &lt; w_i \\ \max \left(d_{i - 1, w}, d_{i - 1, w - w_i} + p_i\right), &amp; w \geq w_i \\ 0, &amp; i w = 0 \end{cases} $$</code></p>
<p>一个 0-1 背包问题的具体示例如下：背包承受的最大重量 <code>$W = 10$</code>，共有 <code>$n = 5$</code> 种物品，编号分别为 <code>$A, B, C, D, E$</code>，重量分别为 <code>$2, 2, 6, 5, 4$</code>，价值分别为 <code>$6, 3, 5, 4, 6$</code>，利用 BP 求解该问题，不同 <code>$i, w$</code> 情况下的状态如下表所示 (计算过程详见 <a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2018-11-18-computational-complexity-and-dynamic-programming/0-1-knapsack-dp.py">这里</a>)：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">i \ w</th>
          <th style="text-align: center">1</th>
          <th style="text-align: center">2</th>
          <th style="text-align: center">3</th>
          <th style="text-align: center">4</th>
          <th style="text-align: center">5</th>
          <th style="text-align: center">6</th>
          <th style="text-align: center">7</th>
          <th style="text-align: center">8</th>
          <th style="text-align: center">9</th>
          <th style="text-align: center">10</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">1</td>
          <td style="text-align: center">NA</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
      </tr>
      <tr>
          <td style="text-align: center">2</td>
          <td style="text-align: center">NA</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
      </tr>
      <tr>
          <td style="text-align: center">3</td>
          <td style="text-align: center">NA</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, C) <br/> 8 - 11</td>
          <td style="text-align: center">(A, C) <br/> 8 - 11</td>
          <td style="text-align: center">(A, B, C) <br/> 10 - 14</td>
      </tr>
      <tr>
          <td style="text-align: center">4</td>
          <td style="text-align: center">NA</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, D) <br/> 7 - 10</td>
          <td style="text-align: center">(A, C) <br/> 8 - 11</td>
          <td style="text-align: center">(A, B, D) <br/> 9 - 13</td>
          <td style="text-align: center">(A, B, C) <br/> 10 - 14</td>
      </tr>
      <tr>
          <td style="text-align: center">5</td>
          <td style="text-align: center">NA</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A) <br/> 2 - 6</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, B) <br/> 4 - 9</td>
          <td style="text-align: center">(A, E) <br/> 6 - 12</td>
          <td style="text-align: center">(A, E) <br/> 6 - 12</td>
          <td style="text-align: center">(A, B, E) <br/> 8 - 15</td>
          <td style="text-align: center">(A, B, E) <br/> 8 - 15</td>
          <td style="text-align: center">(A, B, E) <br/> 8 - 15</td>
      </tr>
  </tbody>
</table>
<p>其中，NA 表示未选取任何物品，单元格上部括号中的为选取物品的编号，单元格下部分别为选取物品的总重量和总价值。</p>
<h3 id="最长公共子序列与最长公共子串">最长公共子序列与最长公共子串</h3>
<p>给定一个序列 <code>$X = \left\{x_1, x_2, \dotsc, x_m\right\}$</code>，另一个序列 <code>$Z = \left\{z_1, z_2, \dotsc, z_k\right\}$</code> 在满足如下条件时称其为 <code>$X$</code> 的一个 <strong>子序例 (Subsequence)</strong>，即存在一个严格递增的 <code>$X$</code> 的下标序列 <code>$\left\{i_1, i_2, \dotsc, i_k\right\}$</code>，对于所有的 <code>$j = 1, 2, \dotsc, k$</code>，满足 <code>$x_{i_j} = z_j$</code>。给定两个序例 <code>$X$</code> 和 <code>$Y$</code>，如果 <code>$Z$</code> 既是 <code>$X$</code> 的子序列，也是 <code>$Y$</code> 的子序列，则称它为 <code>$X$</code> 和 <code>$Y$</code> 的 <strong>公共子序列 (Common Subsequence)</strong>。<strong>最长公共子序列 (Longest Common Subsequence)</strong> 问题为给定两个序列 <code>$X = \left\{x_1, x_2, \dotsc, x_m\right\}$</code> 和 <code>$Y = \left\{y_1, y_2, \dotsc, y_n\right\}$</code>，求 <code>$X$</code> 和 <code>$Y$</code> 最长的公共子序列。</p>
<p>我们可以按如下递归的方式求解最长公共子序列问题：</p>
<ol>
<li>当 <code>$x_i = y_j$</code> 时，求解 <code>$X = \left\{x_1, x_2, \dotsc, x_{i-1}\right\}$</code> 和 <code>$Y = \left\{y_1, y_2, \dotsc, y_{j-1}\right\}$</code> 的最长公共子序列，在其尾部添加 <code>$x_i$</code> 和 <code>$y_j$</code> 即为当前状态下的最长公共子序列。</li>
<li>当 <code>$x_i \neq y_j$</code> 时，我们则需求解 <code>$X = \left\{x_1, x_2, \dotsc, x_{i-1}\right\}$</code> 和 <code>$Y = \left\{y_1, y_2, \dotsc, y_j\right\}$</code> 与 <code>$X = \left\{x_1, x_2, \dotsc, x_i\right\}$</code> 和 <code>$Y = \left\{y_1, y_2, \dotsc, y_{j-1}\right\}$</code> 两种情况下最长的公共子序列作为当前状态下的最长公共子序列。</li>
</ol>
<p>用 <code>$c_{i, j}$</code> 表示<code>$X = \left\{x_1, x_2, \dotsc, x_i\right\}$</code> 和 <code>$Y = \left\{y_1, y_2, \dotsc, y_j\right\}$</code> 情况下的最长公共子序列的长度，则状态转移方程如下：</p>
<p><code>$$ c_{i, w} =  \begin{cases} c_{i - 1, j - 1} + i, &amp; x_i = y_j \\ \max \left(c_{i, j - 1}, c_{i - 1, j}\right), &amp; x_i \neq y_j \\ 0, &amp; i j = 0 \end{cases} $$</code></p>
<p>例如：给定序列 <code>$X = \left\{A, B, C, B, D, A, B\right\}$</code> 和序列 <code>$Y = \left\{B, D, C, A, B, A\right\}$</code>，不同状态下最长公共子序列如下表所示 (计算过程详见 <a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2018-11-18-computational-complexity-and-dynamic-programming/longest-common-subsequence-dp.py">这里</a>)：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"></th>
          <th style="text-align: center"><code>$j$</code></th>
          <th style="text-align: center">0</th>
          <th style="text-align: center">1</th>
          <th style="text-align: center">2</th>
          <th style="text-align: center">3</th>
          <th style="text-align: center">4</th>
          <th style="text-align: center">5</th>
          <th style="text-align: center">6</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center"><code>$i$</code></td>
          <td style="text-align: center"></td>
          <td style="text-align: center"><code>$y_j$</code></td>
          <td style="text-align: center"><strong>B</strong></td>
          <td style="text-align: center">D</td>
          <td style="text-align: center"><strong>C</strong></td>
          <td style="text-align: center">A</td>
          <td style="text-align: center"><strong>B</strong></td>
          <td style="text-align: center"><strong>A</strong></td>
      </tr>
      <tr>
          <td style="text-align: center">0</td>
          <td style="text-align: center"><code>$x_i$</code></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center">1</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center"><strong>0</strong></td>
          <td style="text-align: center">0 (↑)</td>
          <td style="text-align: center">0 (↑)</td>
          <td style="text-align: center">0 (↑)</td>
          <td style="text-align: center">1 (↖)</td>
          <td style="text-align: center">1 (←)</td>
          <td style="text-align: center">1 (↖)</td>
      </tr>
      <tr>
          <td style="text-align: center">2</td>
          <td style="text-align: center"><strong>B</strong></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center"><strong>1 (↖)</strong></td>
          <td style="text-align: center"><strong>1 (←)</strong></td>
          <td style="text-align: center">1 (←)</td>
          <td style="text-align: center">1 (↑)</td>
          <td style="text-align: center">2 (↖)</td>
          <td style="text-align: center">2 (←)</td>
      </tr>
      <tr>
          <td style="text-align: center">3</td>
          <td style="text-align: center"><strong>C</strong></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1 (↑)</td>
          <td style="text-align: center">1 (↑)</td>
          <td style="text-align: center"><strong>2 (↖)</strong></td>
          <td style="text-align: center"><strong>2 (←)</strong></td>
          <td style="text-align: center">2 (↑)</td>
          <td style="text-align: center">2 (↑)</td>
      </tr>
      <tr>
          <td style="text-align: center">4</td>
          <td style="text-align: center"><strong>B</strong></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1 (↖)</td>
          <td style="text-align: center">1 (↑)</td>
          <td style="text-align: center">2 (↑)</td>
          <td style="text-align: center">2 (↑)</td>
          <td style="text-align: center"><strong>3 (↖)</strong></td>
          <td style="text-align: center">3 (←)</td>
      </tr>
      <tr>
          <td style="text-align: center">5</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1 (↑)</td>
          <td style="text-align: center">2 (↖)</td>
          <td style="text-align: center">2 (↑)</td>
          <td style="text-align: center">2 (↑)</td>
          <td style="text-align: center"><strong>3 (↑)</strong></td>
          <td style="text-align: center">3 (↑)</td>
      </tr>
      <tr>
          <td style="text-align: center">6</td>
          <td style="text-align: center"><strong>A</strong></td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1 (↑)</td>
          <td style="text-align: center">2 (↑)</td>
          <td style="text-align: center">2 (↑)</td>
          <td style="text-align: center">3 (↖)</td>
          <td style="text-align: center">3 (↑)</td>
          <td style="text-align: center"><strong>4 (↖)</strong></td>
      </tr>
      <tr>
          <td style="text-align: center">7</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">0</td>
          <td style="text-align: center">1 (↖)</td>
          <td style="text-align: center">2 (↑)</td>
          <td style="text-align: center">2 (↑)</td>
          <td style="text-align: center">3 (↑)</td>
          <td style="text-align: center">4 (↖)</td>
          <td style="text-align: center"><strong>4 (↑)</strong></td>
      </tr>
  </tbody>
</table>
<p>其中，每个单元格前面的数字为最长公共子序列的长度，后面的符号为还原最长公共子序列使用的备忘录符号。</p>
<p><strong>最长公共子串 (Longest Common Substring)</strong> 同最长公共子序列问题略有不同，子序列不要求字符是连续的，而子串要求字符必须是连续的。例如：给定序列 <code>$X = \left\{A, B, C, B, D, A, B\right\}$</code> 和序列 <code>$Y = \left\{B, D, C, A, B, A\right\}$</code>，最长公共子序列为 <code>$\left\{B, C, B, A\right\}$</code>，而最长公共子串为 <code>$\left\{A, B\right\}$</code> 或 <code>$\left\{B, D\right\}$</code>。用 <code>$c_{i, j}$</code> 表示<code>$X = \left\{x_1, x_2, \dotsc, x_i\right\}$</code> 和 <code>$Y = \left\{y_1, y_2, \dotsc, y_j\right\}$</code> 情况下的最长公共子串的长度，则状态转移方程如下：</p>
<p><code>$$ c_{i, w} =  \begin{cases} c_{i - 1, j - 1} + i, &amp; x_i = y_j \\ 0, &amp; x_i \neq y_j \\ 0, &amp; i j = 0 \end{cases} $$</code></p>
<p>利用动态规划可以在 <code>$\Theta \left(nm\right)$</code> 的时间复杂度内求解，利用广义后缀树 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 可以进一步降低问题求解的时间复杂度 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>。</p>
<h3 id="floyd-warshall-算法">Floyd-Warshall 算法</h3>
<p><strong>Floyd-Warshall 算法</strong> 是一种求解任意两点之间 <strong>最短路</strong> 的算法，相比 <strong>Dijkstra 算法</strong> <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>，Floyd-Warshall 算法可以处理有向图或负权图 (但不可以存在负权回路) 的情况 <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>。</p>
<p>用 <code>$d_{i, j}^{\left(k\right)}$</code> 表示从 <code>$i$</code> 到 <code>$j$</code> 路径上最大节点的标号为 <code>$k$</code> 的最短路径的长度。有：</p>
<ol>
<li><code>$d_{i, j}^{\left(k\right)} = d_{i, k}^{\left(k-1\right)} + d_{k, j}^{\left(k-1\right)}$</code>，若最短路径经过点 <code>$k$</code>。</li>
<li><code>$d_{i, j}^{\left(k\right)} = d_{i, j}^{\left(k-1\right)}$</code>，若最短路径不经过点 <code>$k$</code>。</li>
</ol>
<p>则状态转移方程如下：</p>
<p><code>$$ d_{i, j}^{\left(k\right)} =  \begin{cases} w_{i, j}, &amp; k = 0 \\ \min \left(d_{i, j}^{\left(k-1\right)}, d_{i, k}^{\left(k-1\right)} + d_{k, j}^{\left(k-1\right)}\right), &amp; k \leq 1 \end{cases} $$</code></p>
<p>以下图所示的最短路问题为例：</p>
<p><img src="/images/cn/2018-11-18-computational-complexity-and-dynamic-programming/shortest-path.png" alt=""></p>
<p>Floyd-Warshall 算法的求解伪代码如下所示：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Floyd-Warshall 算法}
\begin{algorithmic}
\REQUIRE \\
    边集合 $w$ \\
    顶点数量 $c$
\ENSURE \\
    距离矩阵 $d$ \\
    备忘录矩阵 $m$
\FUNCTION{Floyd-Warshall}{$w, c$}
\FOR{$i$ = $1$ to $c$}
    \FOR{$j$ = $1$ to $c$}
        \STATE $d_{i, j} \gets \infty$
    \ENDFOR
\ENDFOR
\FOR{$i$ = $1$ to $c$}
    \STATE $d_{i, i} \gets 0$
\ENDFOR
\FORALL{$w_{i, j}$}
    \STATE $d_{i, j} \gets w_{i, j}$
\ENDFOR
\FOR{$k$ = $1$ to $c$}
    \FOR{$i$ = $1$ to $c$}
        \FOR{$j$ = $1$ to $c$}
            \IF{$d_{i, j} > d_{i, k} + d_{k, j}$}
                \STATE $d_{i, j} \gets d_{i, k} + d_{k, j}$
                \STATE $m_{i, j} \gets k$
            \ENDIF
        \ENDFOR
    \ENDFOR
\ENDFOR
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>通过备忘录矩阵 <code>$m$</code>，恢复从点 <code>$i$</code> 到点 <code>$j$</code> 的过程如下所示：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Floyd-Warshall-Path 算法}
\begin{algorithmic}
\REQUIRE \\
    备忘录矩阵 $m$ \\
    起点 $i$ \\
    终点 $j$ \\
    路径 $p$
\FUNCTION{Floyd-Warshall-Path}{$m, i, j, p$}
\IF{$i == j$}
    \RETURN
\ENDIF
\IF{$m_{i, j} == 0$}
    \STATE $p \gets p \cup j$
\ELSE
    \STATE Floyd-Warshall-Path($m, i, m_{i, j}, p$)
    \STATE Floyd-Warshall-Path($m, m_{i, j}, j, p$)
\ENDIF
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
</pre></div>

<blockquote>
<p>文章部分内容参考了 Thomas H. Cormen 等人的《算法导论》</p>
</blockquote>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://zh.wikipedia.org/zh/%E8%AE%A1%E7%AE%97%E5%A4%8D%E6%9D%82%E6%80%A7%E7%90%86%E8%AE%BA">https://zh.wikipedia.org/zh/计算复杂性理论</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://zh.wikipedia.org/zh/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92">https://zh.wikipedia.org/zh/动态规划</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://zh.wikipedia.org/zh/%E5%90%8E%E7%BC%80%E6%A0%91">https://zh.wikipedia.org/zh/后缀树</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://zh.wikipedia.org/zh/%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E4%B8%B2">https://zh.wikipedia.org/zh/最长公共子串</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p><a href="https://zh.wikipedia.org/zh/%E6%88%B4%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95">https://zh.wikipedia.org/zh/戴克斯特拉算法</a>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p><a href="https://zh.wikipedia.org/zh/Floyd-Warshall%E7%AE%97%E6%B3%95">https://zh.wikipedia.org/zh/Floyd-Warshall算法</a>&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>利用 Flask 和 Google App Engine 部署模型服务</title><link>https://zeqiang.fun/cn/2018/10/serving-models-with-flask-and-gae/</link><pubDate>Fri, 19 Oct 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/10/serving-models-with-flask-and-gae/</guid><description><![CDATA[
        <div class="blockquote" style='border-left: 4px solid #369BE5;'>本文的配套代码请参见 <a href="https://github.com/leovan/model-serving-demo">这里</a>，建议配合代码阅读本文。</div>
<h2 id="模型部署和服务调用">模型部署和服务调用</h2>
<p>对于做算法的同学，大家或多或少的更关心模型的性能指标多些，对于一些工程性问题考虑的较少。模型的部署是这些工程性问题中重要的一个，它直接关系到模型在生产系统的使用。一些成熟的机器学习框架会提供自己的解决方案，例如 <a href="https://www.tensorflow.org">Tensorflow</a> 提供的 <a href="https://www.tensorflow.org/serving/">Serving</a> 服务等。但很多情况下我们构建的工程可能不只使用了一种框架，因此一个框架自身的部署工具可能就很难满足我们的需求了。</p>
<p>针对此类情况，本文介绍一个 <strong>简单</strong> 的 <strong>准生产</strong> 模型部署方案。简单是指除了模型相关代码之外的工程性代码量不大，这得益于将要使用的 <a href="http://flask.pocoo.org/">Flask</a> 框架。准生产是指这种部署方案应对一般的生产环境问题不大，对于高并发的场景可以通过横向扩容并进行负载均衡解决，但对于单次调用时效性要求较高的场景则需要另寻其他解决方案。</p>
<p>本文方案的模型部署和服务调用框架如下图所示：</p>
<p><img src="/images/cn/2018-10-19-serving-models-with-flask-and-gae/model-serving.png" alt="Model-Serving"></p>
<p>其主要特性如下：</p>
<ol>
<li>服务端采用 Python 的 Flask 框架构建，无需使用其他外部服务。Flask 框架的 <a href="https://zh.wikipedia.org/zh/%E5%BE%AE%E6%9C%8D%E5%8A%A1">微服务</a> (Microframework) 特性使得服务端代码简洁高效。</li>
<li>利用 <a href="https://gunicorn.org/">Gunicorn</a> 提供的高性能 Python WSGI HTTP UNIX Server，方便在服务端运行 Flask 应用。</li>
<li>客户端和服务端之间采用 <a href="https://zh.wikipedia.org/zh/%E8%A1%A8%E7%8E%B0%E5%B1%82%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2">RESTful API</a> 调用方式，尽管在性能上可能不及其他一些方案 (例如：基于 RPC 的解决方案等)，但其较好地解决了跨语言交互的问题，不同语言之间交互仅需使用 HTTP 协议和 JSON 数据格式即可。</li>
</ol>
<h2 id="flask-服务和-ajax-调用">Flask 服务和 AJAX 调用</h2>
<h3 id="flask-服务封装">Flask 服务封装</h3>
<p>为了将模型代码和 Flask 服务进行整合，首先假设你已经对模型部分代码做了完美的封装 &#x1f60e;，整个工程先叫做 <code>model-serving-demo</code> 吧。整理一下代码的目录结构，给一个我中意的 Python 目录结构风格：</p>
<pre><code>model-serving-demo/                # 工程根目录
├── bin/                           # 可执行命令目录
|   ├─ start.sh                    # 启动脚本
|   ├─ stop.sh                     # 停止脚本
|   └─ ...
├── conf/                          # 配置文件目录
|   ├─ logging.conf                # 日志配置文件
|   ├─ xxx_model.conf              # XXX Model 配置文件
|   └─ ...
├── data/                          # 数据文件目录
├── docs/                          # 文档目录
├── model_serving/                 # 模块根目录
|   ├─ models/                     # 模型代码目录
|   |   ├─ __init__.py
|   |   ├─ xxx_model.py            # XXX Model 代码
|   |   └─ ...
|   ├─ resources/                  # Flask RESTful Resources 代码目录
|   |   ├─ __init__.py
|   |   ├─ xxx_model_resource.py   # XXX Model Flask RESTful Resources 代码
|   |   └─ ...
|   ├─ tests/                      # 测试代码根目录
|   |   ├─ models                  # 模型测试代码目录
|   |   |   ├─ __init__.py
|   |   |   ├─ test_xxx_model.py   # XXX Model 测试代码
|   |   |   └─ ...
|   |   ├─ __init__.py
|   |   └─ ...
|   ├─ tmp/                        # 临时目录
|   └─ ...
├── .gitignore                     # Git Ignore 文件
├── app.yaml                       # Google App Engine 配置文件
├── LICENSE                        # 授权协议
├── main.py                        # 主程序代码
├── README.md                      # 说明文件
└── requirements.txt               # 依赖包列表
</code></pre>
<p>我们利用一个极简的示例介绍整个模型部署，相关的库依赖 <code>requirements.txt</code> 如下：</p>
<pre><code>Flask==1.0.2
Flask-RESTful==0.3.6
Flask-Cors==3.0.6
jsonschema==2.6.0
docopt==0.6.2

# 本地部署时需保留，GAE 部署时请删除
# gunicorn==19.9.0
</code></pre>
<p>其中：</p>
<ol>
<li><a href="http://flask.pocoo.org/">Flask</a> 用于构建 Flask 服务。</li>
<li><a href="https://flask-restful.readthedocs.io/">Flask-RESTful</a> 用于构建 Flask RESTful API。</li>
<li><a href="https://flask-cors.readthedocs.io/">Flask-Cors</a> 用于解决 AJAX 调用时的 <a href="https://zh.wikipedia.org/zh/%E8%B7%A8%E4%BE%86%E6%BA%90%E8%B3%87%E6%BA%90%E5%85%B1%E4%BA%AB">跨域问题</a>。</li>
<li><a href="https://python-jsonschema.readthedocs.io/">jsonschema</a> 用于对请求数据的 JSON 格式进行校验。</li>
<li><a href="http://docopt.org/">docopt</a> 用于从代码文档自动生成命令行参数解析器。</li>
<li><a href="https://gunicorn.org/">gunicorn</a> 用于提供的高性能 Python WSGI HTTP UNIX Server。</li>
</ol>
<p>XXX Model 的代码 <code>xxx_model.py</code> 如下：</p>
<pre><code class="language-python">from ..utils.log_utils import XXXModel_LOGGER


LOGGER = XXXModel_LOGGER


class XXXModel():
    def __init__(self):
        LOGGER.info('Initializing XXX Model ...')

        LOGGER.info('XXX Model Initialized.')

    def hello(self, name:str) -&gt; str:
        return 'Hello, {name}!'.format(name=name)
</code></pre>
<p>其中 <code>hello()</code> 为服务使用的方法，其接受一个类型为 <code>str</code> 的参数 <code>name</code>，并返回一个类型为 <code>str</code> 的结果。</p>
<p>XXX Model 的 Flask RESTful Resource 代码 <code>xxx_model_resource.py</code> 如下：</p>
<pre><code class="language-python">from flask_restful import Resource, request

from ..models.xxx_model import XXXModel
from ..utils.validation_utils import validate_json


xxx_model_instance = XXXModel()
xxx_model_schema = {
    'type': 'object',
    'properties': {
        'name': {'type': 'string'}
    },
    'required': ['name']
}


class XXXModelResource(Resource):
    @validate_json(xxx_model_schema)
    def post(self):
        json = request.json

        return {'result': xxx_model_instance.hello(json['name'])}
</code></pre>
<p>我们需要从 Flask RESTful 的 <code>Resource</code> 类继承一个新的类 <code>XXXModelResource</code> 用于处理 XXX Model 的服务请求。如上文介绍，我们在整个模型服务调用中使用 POST 请求方式和 JSON 数据格式，因此我们需要在类 <code>XXXModelResource</code> 中实现 <code>post()</code> 方法，同时对于传入数据的 JSON 格式进行校验。</p>
<p><code>post()</code> 方法用于处理整个模型的服务请求，<code>xxx_model_instance</code> 模型实例在类 <code>XXXModelResource</code> 外部进行实例化，避免每次处理请求时都进行初始化。<code>post()</code> 的返回结果无需处理成 JSON 格式的字符串，仅需返回词典数据即可，Flask RESTful 会自动对其进行转换。</p>
<p>为了方便对请求数据的 JSON 格式进行校验，我们将对 JSON 格式的校验封装成一个修饰器。使用时如上文代码中所示，在 <code>post()</code> 方法上方添加 <code>@validate_json(xxx_model_schema)</code> 即可，其中 <code>xxx_model_schema</code> 为一个符合 <a href="https://python-jsonschema.readthedocs.io/">jsonschema</a> 要求的 JSON Schema。示例代码中要求传入的 JSON 数据 <strong>必须</strong> 包含一个名为 <code>name</code> 类型为 <code>string</code> 的字段。</p>
<p><code>validate_json</code> 修饰器的代码 <code>validation_utils.py</code> 如下：</p>
<pre><code class="language-python">from functools import wraps
from jsonschema import validate, ValidationError
from flask_restful import request


def validate_json(schema, force=False):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            json_body = request.get_json(force=force)

            if json_body is None:
                return {'message': 'No JSON object'}, 400

            try:
                validate(json_body, schema)
            except ValidationError as e:
                return {'message': e.message}, 400

            return f(*args, **kwargs)
        return wrapper
    return decorator
</code></pre>
<p>首先我们需要验证请求包含一个 JSON 请求体，同时 JSON 请求体的内容需满足 <code>schema</code> 的要求。如果不满足这些条件，我们需要返回对应的错误信息 <code>message</code>，同时返回合理的 <a href="https://zh.wikipedia.org/zh/HTTP%E7%8A%B6%E6%80%81%E7%A0%81">HTTP 状态码</a> (例如：<code>400</code>) 用于表示无法处理错误的请求。对于正常的请求响应 (即 HTTP 状态码为 200 的情况)，状态码可以省略不写。</p>
<p>构建完 XXX Model 的 Flask RESTful Resource 后，我们就可以构建 Flask 的主服务了，主程序代码 <code>main.py</code> 如下：</p>
<pre><code class="language-python">&quot;&quot;&quot;
Model Serving Demo

Usage:
    main.py [--host &lt;host&gt;] [--port &lt;port&gt;] [--debug]
    main.py (-h | --help)
    main.py --version

Options:
    --host &lt;host&gt;                     绑定的 Host [default: 0.0.0.0]
    --port &lt;port&gt;                     绑定的 Port [default: 9999]
    --debug                           是否开启 Debug [default: False]
    -h --help                         显示帮助
    -v --version                      显示版本

&quot;&quot;&quot;

from docopt import docopt

from flask import Flask
from flask_cors import CORS
from flask_restful import Api

from model_serving.resources.xxx_model_resource import XXXModelResource


app = Flask(__name__)
CORS(app)

api = Api(app)
api.add_resource(XXXModelResource, '/v1/XXXModel')


if __name__ == '__main__':
    args = docopt(__doc__, version='Model Serving Demo v1.0.0')
    app.run(host=args['--host'], port=args['--port'], debug=args['--debug'])
</code></pre>
<p><code>docopt</code> 库用于从代码文档自动生成命令行参数解析器，具体使用方法请参见 <a href="http://docopt.org/">官方文档</a>。整个 Flask 主服务的构建比较简单，流程如下：</p>
<ol>
<li>构建 Flask 主程序，<code>app = Flask(__name__)</code>。</li>
<li>解决 AJAX 调用的跨域问题， <code>CORS(app)</code>。为了方便起见，我们不加任何参数，允许任意来源的请求，详细的使用方式请参见 <a href="https://flask-cors.readthedocs.io/">官方文档</a>。</li>
<li>构建 Flask RESTful API，<code>api = Api(app)</code>。</li>
<li>将构建好的 XXX Model 的 Flask RESTful Resource 添加到 API 中，<code>api.add_resource(XXXModelResource, '/v1/XXXModel')</code>。
其中第二个参数为请求的 URL，对于这个 URL 的建议将在后续小节中详细说明。</li>
</ol>
<p>Flask 主程序配置完毕后，我们通过 <code>app.run()</code> 在本地启动 Flask 服务，同时可以指定绑定的主机名，端口，以及是否开启调试模式等。通过 <code>python main.py</code> 启动 Flask 服务后，可以在命令行看到如下类似的日志：</p>
<pre><code>[2018/10/21 00:00:00] - [INFO] - [XXXModel] - Initializing XXX Model ...
[2018/10/21 00:00:00] - [INFO] - [XXXModel] - XXX Model Initialized.
 * Serving Flask app &quot;main&quot; (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
[2018/10/21 00:00:00] - [INFO] - [werkzeug] -  * Running on http://0.0.0.0:9999/ (Press CTRL+C to quit)
</code></pre>
<p>现在就可以测试调用服务了，我们用 <code>curl</code> 命令进行简单的测试，相关代码 <code>request-demo.sh</code> 如下：</p>
<pre><code class="language-bash">host=0.0.0.0
port=9999
url=https://zeqiang.fun/v1/XXXModel
curl_url=http://${host}:${port}${url}

invalid_json='{}'
valid_json='{&quot;name&quot;: &quot;Leo&quot;}'

# No JSON object
curl --request POST --url ${curl_url} --verbose

# Invalid JSON object
curl --header 'Content-Type: application/json; charset=UTF-8' \
    --request POST --data ${invalid_json} --url ${curl_url} --verbose

# Valid JSON object
curl --header 'Content-Type: application/json; charset=UTF-8' \
    --request POST --data ${valid_json} --url ${curl_url} --verbose
</code></pre>
<p>三种不同的请求返回的 HTTP 状态码和结果如下：</p>
<pre><code>HTTP/1.0 400 BAD REQUEST
{&quot;message&quot;: &quot;No JSON object&quot;}

HTTP/1.0 400 BAD REQUEST
{&quot;message&quot;: &quot;'name' is a required property&quot;}

HTTP/1.0 200 OK
{&quot;result&quot;: &quot;Hello, Leo!&quot;}
</code></pre>
<p>上文中，我们通过 <code>python main.py</code> 利用内置的 Server 启动了 Flask 服务，启动后日志中打印出来一条警告信息，告诉使用者不要在生产环境中使用内置的 Server。在生产环境中我们可以利用高性能 Python WSGI HTTP UNIX Server <a href="https://gunicorn.org/">gunicorn</a> 来启动 Flask 服务。</p>
<p>服务启动 (<code>start.sh</code>) 脚本代码如下：</p>
<pre><code class="language-bash">cd `dirname $0`
cd ..

base_dir=`pwd`
tmp_dir=${base_dir}/tmp
pid_file_path=${tmp_dir}/model-serving-demo.pid
log_file_path=${tmp_dir}/model-serving-demo.log

bind_host=0.0.0.0
bind_port=9999
workers=2

nohup gunicorn -b ${bind_host}:${bind_port} \
  -w ${workers} -p ${pid_file_path} \
  main:app &gt; ${log_file_path} 2&gt;&amp;1 &amp;
</code></pre>
<p>服务停止 (<code>stop.sh</code>) 脚本代码如下：</p>
<pre><code class="language-bash">cd `dirname $0`
cd ..

base_dir=`pwd`
tmp_dir=${base_dir}/tmp
pid_file_path=${tmp_dir}/model-serving-demo.pid

kill -TERM `echo ${pid_file_path}`
</code></pre>
<p>gunicorn 的详细参数配置和使用教程请参见 <a href="https://docs.gunicorn.org/en/stable/">官方文档</a>。</p>
<h3 id="restful-api-设计">RESTful API 设计</h3>
<p>RESTful API 是一种符合 REST(Representational State Transfer，表现层状态转换) 原则的框架，该框架是由 Fielding 在其博士论文 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 中提出。相关的核心概念如下：</p>
<ol>
<li><strong>资源 (Resources)</strong>，即网络中的一个实体 (文本，图片，服务等)，使用一个 URL 进行表示。</li>
<li><strong>表现层 (Representation)</strong>，资源具体的呈现形式即为表现层，例如图片可以表示为 PNG 文件，音乐可以表示为 MP3 文件，还有本文使用的数据格式 JSON 等。HTTP 请求的头信息中用 Accept 和 Content-Type 字段对表现层进行描述。</li>
<li><strong>状态转换 (State Transfer)</strong>，互联网通信协议 HTTP 协议是一个无状态协议，所有的状态都保存在服务端。因此如果客户端想要操作服务器，必须通过某种手段让服务器端发生 <strong>状态转换</strong>。客户端利用 HTTP 协议中的动作对服务器进行操作，例如：GET，POST，PUT，DELETE 等。</li>
</ol>
<p>利用 RESTful API 构建模型服务时，需要注意如下几点：</p>
<ol>
<li>为模型服务设置专用域名，例如：<code>https://api.example.com</code>，并配以负载均衡。</li>
<li>将 API 的版本号写入 URL 中，例如：<code>https://api.example.com/v1</code>。</li>
<li>RESTful 框架中每个 URL 表示一种资源，因此可以将模型的名称作为 URL 的终点 (Endpoint)，例如：<code>https://api.example.com/v1/XXXModel</code>。</li>
<li>对于操作资源的 HTTP 方式有多种，综合考虑建议选用 POST 方式，同时建议使用 JSON 数据格式。</li>
<li>为请求响应设置合理的状态码，例如：200 OK 表示正常返回，400 INVALID REQUEST 表示无法处理客户端的错误请求等。</li>
<li>对于错误码为 4xx 的情况，建议在返回中添加键名为 <code>message</code> 的错误信息。</li>
</ol>
<h3 id="ajax-调用">AJAX 调用</h3>
<p>对于动态网页，我们可以很容易的在后端服务中发起 POST 请求调用模型服务，然后将结果在前端进行渲染。对于静态网页，我们可以利用 AJAX 进行相关操作。首先我们需要一个交互界面，如下为利用 <a href="https://material.io/design/">Google Material Design</a> 风格的 <a href="https://github.com/material-components/material-components-web">Material Design Components Web</a> 组件设计一个交互界面，实现细节请参见 <a href="https://github.com/leovan/model-serving-demo/tree/master/client/xxx-model-ajax-client.html">示例代码</a>。</p>
<p>
  
<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/material-components-web@latest/dist/material-components-web.min.css">





















<style type="text/css">
.center {
  justify-content: center;
}

.text-field--fullwidth {
  width: 100%;
}

.loading {
  width: 32px;
  height: 32px;
  position: relative;
  margin: auto;
}

.loading-bounce-1, .loading-bounce-2 {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background-color: #333;
  opacity: 0.6;
  position: absolute;
  top: 0;
  left: 0;

  -webkit-animation: bounce 2.0s infinite ease-in-out;
  animation: bounce 2.0s infinite ease-in-out;
}

.loading-bounce-2 {
  -webkit-animation-delay: -1.0s;
  animation-delay: -1.0s;
}

@-webkit-keyframes bounce {
  0%, 100% { -webkit-transform: scale(0.0) }
  50% { -webkit-transform: scale(1.0) }
}

@keyframes bounce {
  0%, 100% {
    transform: scale(0.0);
    -webkit-transform: scale(0.0);
  } 50% {
    transform: scale(1.0);
    -webkit-transform: scale(1.0);
  }
}
</style>

<div class="mdc-card">
  <div class="mdc-card__actions center">
    <div class="mdc-typography--headline6"><span>XXX Model AJAX Client</span></div>
  </div>
  <div class="mdc-card__actions">
    <div class="mdc-text-field text-field--fullwidth" data-mdc-auto-init="MDCTextField">
      <input id="name" class="mdc-text-field__input" value="Leo">
      <div class="mdc-line-ripple"></div>
    </div>
  </div>
  <div class="mdc-card__actions center">
    <button id="submit" class="mdc-button mdc-button--outlined" data-mdc-auto-init="MDCRipple">Submit</button>
    <div class="loading" id="loading" style="display: none;">
      <div class="loading-bounce-1"></div>
      <div class="loading-bounce-2"></div>
    </div>
  </div>
  <div class="mdc-card__actions center">
    <div class="mdc-typography--body1">
      <p id="result">Result</p>
    </div>
  </div>
</div>

<script>
  $(document).ready(function() {
    $("#submit").click(function() {
      $("#submit").toggle();
      $("#loading").toggle();

      /*
      $.ajax({
        url: "http://0.0.0.0:9999/v1/XXXModel",
        method: "POST",
        contentType: "application/json; charset=UTF-8",
        data: JSON.stringify({"name": $("#name").val()}),
        timeout: 3000,

        success: function (data, textStatus, jqXHR) {
          $("#result").html(data.result);

          $("#loading").toggle();
          $("#submit").toggle();
        },
        error: function (jqXHR, textStatus, errorThrown) {
          $("#result").html(errorThrown);

          $("#loading").toggle();
          $("#submit").toggle();
        }
      });
      */

      setTimeout(function() {
        $("#result").html("Hello, " + $("#name").val() + "!");
        $("#submit").toggle();
        $("#loading").toggle();
      }, 1000);
    });
  });
</script>





</p>
<p>AJAX 服务请求代码的核心部分如下：</p>
<pre><code class="language-javascript">$(document).ready(function() {
    $(&quot;#submit&quot;).click(function() {
        $.ajax({
            url: &quot;http://0.0.0.0:9999/v1/XXXModel&quot;,
            method: &quot;POST&quot;,
            contentType: &quot;application/json; charset=UTF-8&quot;,
            data: JSON.stringify({&quot;name&quot;: $(&quot;#name&quot;).val()}),
            timeout: 3000,

            success: function (data, textStatus, jqXHR) {
                $(&quot;#result&quot;).html(data.result);
            },
            error: function (jqXHR, textStatus, errorThrown) {
                $(&quot;#result&quot;).html(errorThrown);
            }
        });
    });
});
</code></pre>
<p>代码使用了 <a href="https://jquery.com/">jQuery</a> 的相关函数。<code>JSON.stringify({&quot;name&quot;: $(&quot;#name&quot;).val()})</code> 获取 ID 为 <code>name</code> 的元素的值，并将其转换成符合服务端要求的 JSON 格式。通过 AJAX 向远程发出请求后，如果请求成功则将返回数据 <code>data</code> 中对应的结果 <code>result</code> 填充到 ID 为 <code>result</code> 的元素中，否则填入返回的错误信息。</p>
<h2 id="google-app-engine-部署">Google App Engine 部署</h2>
<p>上文中已经介绍了如何在本地利用 Flask 部署模型服务和相关调用方法，但如果希望在自己的网站中调用时，则利用 SaaS 来部署符合会是一个不二之选。国内外多个厂商均提供了相应的 SaaS 产品，例如 <a href="https://cloud.google.com/appengine/">Google</a>，<a href="https://aws.amazon.com/partners/saas-on-aws/">Amazon</a>，<a href="https://azure.microsoft.com/en-us/solutions/saas/">Microsoft</a> 等。Google App Engine (GAE) 提供了一个 <a href="https://cloud.google.com/free/docs/always-free-usage-limits">始终免费</a> 方案，虽然部署阶段会受到 GFW 的影响，但调用阶段测试影响并不是很大 (不同地区和服务提供商会有差异)。综合考虑，本文选择 GAE 作为 SaaS 平台部署服务，各位看官请自备梯子。</p>
<h3 id="环境准备">环境准备</h3>
<p>首先，在 <a href="https://console.cloud.google.com/projectcreate">Google Cloud Platform Console</a> 中建立一个新的 Project，假设项目名为 <code>YOUR_PROJECT_ID</code>。</p>
<p>其次，根据 <a href="https://cloud.google.com/sdk/docs/">Google Cloud SDK 文档</a> 在本地安装相应版本的 Google Cloud SDK。MacOS 下建议通过 <code>brew cask install google-cloud-sdk</code> 方式安装，安装完毕后确认在命令行中可以运行 <code>gcloud</code> 命令。</p>
<pre><code class="language-bash">$ gcloud version
Google Cloud SDK 221.0.0
bq 2.0.35
core 2018.10.12
gsutil 4.34
</code></pre>
<h3 id="构建-gae-工程">构建 GAE 工程</h3>
<p>模型服务仅作为后端应用，因此本节不介绍前端页面开发的相关部分，有兴趣的同学请参见 <a href="https://cloud.google.com/appengine/docs/standard/python3/quickstart">官方文档</a>。GAE 部署 Python Web 应用采用了 <a href="https://wsgi.readthedocs.io/en/latest/">WSGI 标准</a>，我们构建的本地部署版本完全满足这个要求，因此仅需为项目在根目录添加一个 GAE 配置文件 <code>app.yaml</code> 即可，内容如下：</p>
<pre><code class="language-yaml">runtime: python37

handlers:
  - url: /.*
    script: main.app

skip_files:
  - .idea/
  - .vscode/
  - __pycache__/
  - .hypothesis/
  - .pytest_cache/
  - bin/
  - ^(.*/)?.*\.py[cod]$
  - ^(.*/)?.*\$py\.class$
  - ^(.*/)?.*\.log$
</code></pre>
<p>其中，<code>runtime</code> 指定了服务运行的环境，<code>handlers</code> 指定了不同的 URL 对应的处理程序，在此所有的 URL 均由 <code>main.py</code> 中的 <code>app</code> 进行处理，<code>skip_files</code> 用于过滤不需要上传的文件。更多关于 <code>app.yaml</code> 的设置信息，请参见 <a href="https://cloud.google.com/appengine/docs/standard/python3/config/appref">官方文档</a>。</p>
<h3 id="部署-gae-工程">部署 GAE 工程</h3>
<p>在部署 GAE 工程之前我们可以利用本地的开发环境对其进行测试，测试无误后，即可运行如下命令将其部署到 GAE 上：</p>
<pre><code class="language-bash">gcloud app deploy --project [YOUR_PROJECT_ID]
</code></pre>
<p>然后根据命令行提示完成整个部署流程，部署完成的远程服务 URL 为 <code>https://YOUR_PROJECT_ID.appspot.com</code>，更多的测试和部署细节请参见 <a href="https://cloud.google.com/appengine/docs/standard/python3/testing-and-deploying-your-app">官方文档</a>。</p>
<p>部署后的 GAE 服务使用了其自带的域名 <code>appspot.com</code>。如果你拥有自己的域名，可以根据官方文档 <a href="https://cloud.google.com/appengine/docs/standard/python3/mapping-custom-domains">设置自己的域名</a> 并 <a href="https://cloud.google.com/appengine/docs/standard/python3/secURLng-custom-domains-with-ssl">开启 SSL</a>。</p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'>本文部分内容参考了 Genthial 的博客 <a href="https://guillaumegenthial.github.io/serving.html">Serving a model with Flask</a> 和阮一峰的博客 <a href="https://www.ruanyifeng.com/blog/2011/09/restful.html">理解RESTful架构</a> 和 <a href="https://www.ruanyifeng.com/blog/2014/05/restful_api.html">RESTful API 设计指南</a>。</div>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Fielding, Roy T., and Richard N. Taylor. <em>Architectural styles and the design of network-based software architectures.</em> Vol. 7. Doctoral dissertation: University of California, Irvine, 2000.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>序列到序列 (Seq2Seq) 和注意力机制 (Attention Machanism)</title><link>https://zeqiang.fun/cn/2018/10/seq2seq-and-attention-machanism/</link><pubDate>Fri, 12 Oct 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/10/seq2seq-and-attention-machanism/</guid><description><![CDATA[
        <h2 id="encoder-decoder-seq2seq">Encoder-Decoder &amp; Seq2Seq</h2>
<p>Encoder-Decoder 是一种包含两个神经网络的模型，两个网络分别扮演编码器和解码器的角色。Cho 等人 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 提出了一个基于 RNN 的 Encoder-Decoder 神经网络用于机器翻译。网络结构如下图所示：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/rnn-encoder-decoder.png" alt="RNN-Encoder-Decoder"></p>
<p>整个模型包含编码器 (Encoder) 和解码器 (Decoder) 两部分：Encoder 将一个可变长度的序列转换成为一个固定长度的向量表示，Decoder 再将这个固定长度的向量表示转换为一个可变长度的序列。这使得模型可以处理从一个可变长度序列到另一个可变长度序例的转换，即学习到对应的条件概率 <code>$p \left(y_1, \dotsc, y_{T'} | x_1, \dotsc, x_T\right)$</code>，其中 <code>$T$</code> 和 <code>$T'$</code> 可以为不同的值，也就是说输入和输出的序列的长度不一定相同。</p>
<p>在模型中，Encoder 为一个 RNN，逐次读入输入序列 <code>$\mathbf{x}$</code> 中的每个元素，其中 RNN 隐状态的更新方式如下：</p>
<p><code>$$ \mathbf{h}_{\langle t \rangle} = f \left(\mathbf{h}_{\langle t-1 \rangle}, x_t\right) $$</code></p>
<p>在读入序列的最后一个元素后 (通常为一个结束标记)，RNN 的隐状态则为整个输入序列的概括信息 <code>$\mathbf{c}$</code>。Decoder 为另一个 RNN，用于根据隐状态 <code>$\mathbf{h}'_{\langle t \rangle}$</code> 预测下一个元素 <code>$y_t$</code>，从而生成整个输出序列。不同于 Encoder 中的 RNN，Decoder 中 RNN 的隐状态 <code>$\mathbf{h}'_{\langle t \rangle}$</code> 除了依赖上一个隐含层的状态和之前的输出外，还依赖整个输入序列的概括信息 <code>$\mathbf{c}$</code>，即：</p>
<p><code>$$ \mathbf{h}'_{\langle t \rangle} = f \left(\mathbf{h}'_{\langle t-1 \rangle}, y_{t-1}, \mathbf{c}\right) $$</code></p>
<p>类似的，下一个输出元素的条件分布为：</p>
<p><code>$$ P \left(y_t | y_{t-1}, y_{t-2}, \dotsc, y_1, \mathbf{c}\right) = g \left(\mathbf{h}_{\langle t \rangle}, y_{t-1}, \mathbf{c}\right) $$</code></p>
<p>RNN Encoder-Decoder 的两部分通过最大化如下的对数似然函数的联合训练进行优化：</p>
<p><code>$$ \max_{\theta} \dfrac{1}{N} \sum_{n=1}^{N}{\log p_{\theta} \left(\mathbf{y}_n | \mathbf{x}_n\right)} $$</code></p>
<p>其中，<code>$\theta$</code> 为模型的参数，<code>$\mathbf{x}_n$</code> 和 <code>$\mathbf{y}_n$</code> 分别为输入和输出序列的成对样本。当模型训练完毕后，我们可以利用模型根据给定的输入序列生成相应的输出序列，或是根据给定的输入和输出序列对计算概率得分 <code>$p_{\theta} \left(\mathbf{y} | \mathbf{x}\right)$</code>。同时，作者还提出了一种新的 RNN 单元 GRU (Gated Recurrent Unit)，有关 GRU 的更多介绍请参见 <a href="/cn/2018/09/rnn">之前的博客</a>。</p>
<p>序列到序列 (Sequence to Sequence, Seq2Seq) 模型从名称中不难看出来是一种用于处理序列数据到序列数据转换问题 (例如：机器翻译等) 的方法。Sutskever 等人 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 提出了一种基于 Encoder-Decoder 网络结构的 Seq2Seq 模型用于机器翻译，网络结构细节同 RNN Encoder-Decoder 略有不同，如下图所示：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/seq2seq.png" alt="Seq2Seq"></p>
<p>模型的相关细节如下：</p>
<ol>
<li>对数据进行预处理，在每个句子的结尾添加特殊字符 <code>&lt;EOS&gt;</code>，如上图所示。首先计算 <code>A, B, C, &lt;EOS&gt;</code> 的表示，再利用该表示计算 <code>W, X, Y, Z, &lt;EOS&gt;</code> 的条件概率。</li>
<li>利用两个不同的 LSTM，一个用于输入序列，另一个用于输出序列。</li>
<li>选用一个较深的 LSTM 模型 (4 层) 提升模型效果。</li>
<li>对输入序列进行倒置处理，例如对于输入序列 <code>$a, b, c$</code> 和对应的输出序列 <code>$\alpha, \beta, \gamma$</code>，LSTM 需要学习的映射关系为 <code>$c, b, a \to \alpha, \beta, \gamma$</code>。</li>
</ol>
<p>在模型的解码阶段，模型采用简单的从左到右的 Beam Search，该方法维护一个大小为 <code>$B$</code> 的集合保存最好的结果。下图展示了 <code>$B = 2$</code> 情况下 Beam Search 的具体工作方式：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/beam-search.png" alt="Beam-Search"></p>
<p>其中，红色的虚线箭头表示每一步可能的搜索方向，绿色的实线箭头表示每一步概率为 Top <code>$B$</code> 的方向。例如，从 S 开始搜索：</p>
<ol>
<li>第一步搜索的可能结果为 SA 和 SB，保留 Top 2，结果为 SA 和 SB。</li>
<li>第二步搜索的可能结果为 SAC，SAD，SBE 和 SBF，保留 Top 2，结果为 SAC 和 SBE。</li>
<li>第三步搜索的可能结果为 SACG，SACH，SBEK 和 SBEL，保留 Top 2，结果为 SACH 和 SBEK。至此，整个搜索结束。</li>
</ol>
<p>Bahdanau 等人 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 提出了一种基于双向 RNN (Bidirectional RNN, BiRNN) 结合注意力机制 (Attention Mechanism) 的网络结构用于机器翻译。网络结构如下：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/seq2seq-birnn-attention.png" alt="Seq2Seq-BiRNN-Attention"></p>
<p>模型的编码器使用了一个双向的 RNN，前向的 RNN <code>$\overrightarrow{f}$</code> 以从 <code>$x_1$</code> 到 <code>$x_T$</code> 的顺序读取输入序列并计算前向隐状态 <code>$\left(\overrightarrow{h}_1, \dotsc, \overrightarrow{h}_T\right)$</code>，后向的 RNN <code>$\overleftarrow{f}$</code> 以从 <code>$x_T$</code> 到 <code>$x_1$</code> 的顺序读取输入序列并计算后向隐状态 <code>$\left(\overleftarrow{h}_1, \dotsc, \overleftarrow{h}_T\right)$</code>。对于一个词 <code>$x_j$</code>，通过将对应的前向隐状态 <code>$\overrightarrow{h}_j$</code> 和后向隐状态 <code>$\overleftarrow{h}_j$</code> 进行拼接得到最终的隐状态 <code>$h_j = \left[\overrightarrow{h}_j^{\top}; \overleftarrow{h}_j^{\top}\right]^{\top}$</code>。这样的操作使得隐状态 <code>$h_j$</code> 既包含了前面词的信息也包含了后面词的信息。</p>
<p>在模型的解码器中，对于一个给定的序例 <code>$\mathbf{x}$</code>，每一个输出的条件概率为：</p>
<p><code>$$ p \left(y_i | y_1, \dotsc, y_{i-1}, \mathbf{x}\right) = g \left(y_{i-1}, s_i, c_i\right) $$</code></p>
<p>其中，<code>$s_i$</code> 为 <code>$i$</code> 时刻 RNN 隐含层的状态，即：</p>
<p><code>$$ s_i = f \left(s_{i-1}, y_{i-1}, c_i\right) $$</code></p>
<p>这里需要注意的是不同于之前的 Encoder-Decoder 模型，此处每一个输出词 <code>$y_i$</code> 的条件概率均依赖于一个单独的上下文向量 <code>$c_i$</code>。该部分的改进即结合了注意力机制，有关注意力机制的详细内容将在下个小节中展开说明。</p>
<h2 id="注意力机制-attention-mechanism">注意力机制 (Attention Mechanism)</h2>
<p>Bahdanau 等人在文中 <sup id="fnref1:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 提出传统的 Encoder-Decoder 模型将输入序列压缩成一个固定长度的向量 <code>$c$</code>，但当输入的序例很长时，尤其是当比训练集中的语料还长时，模型的的效果会显著下降。针对这个问题，如上文所述，上下文向量 <code>$c_i$</code> 依赖于 <code>$\left(h_1, \dotsc, h_T\right)$</code>。其中，每个 <code>$h_i$</code> 都包含了整个序列的信息，同时又会更多地关注第 <code>$i$</code> 个词附近的信息。对于 <code>$c_i$</code>，计算方式如下：</p>
<p><code>$$ c_i = \sum_{j=1}^{T}{\alpha_{ij} h_j} $$</code></p>
<p>对于每个 <code>$h_j$</code> 的权重 <code>$\alpha_{ij}$</code>，计算方式如下：</p>
<p><code>$$ \alpha_{ij} = \dfrac{\exp \left(e_{ij}\right)}{\sum_{k=1}^{T}{\exp \left(e_{ik}\right)}} $$</code></p>
<p>其中，<code>$e_{ij} = a \left(s_{i-1}, h_j\right)$</code> 为一个 Alignment 模型，用于评价对于输入的位置 <code>$j$</code> 附近的信息与输出的位置 <code>$i$</code> 附近的信息的匹配程度。Alignment 模型 <code>$a$</code> 为一个用于评分的前馈神经网络，与整个模型进行联合训练，计算方式如下：</p>
<p><code>$$ a \left(s_{i-1}, h_j\right) = v_a^{\top} \tanh \left(W_a s_{i-1} + U_a h_j\right) $$</code></p>
<p>其中，<code>$W_a \in \mathbb{R}^{n \times n}, U_a \in \mathbb{R}^{n \times 2n}，v_a \in \mathbb{R}^n$</code> 为网络的参数。</p>
<h3 id="hard-soft-attention">Hard &amp; Soft Attention</h3>
<p>Xu 等人 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 在图像标题生成 (Image Caption Generation) 任务中引入了注意力机制。在文中作者提出了 Hard Attenttion 和 Soft Attention 两种不同的注意力机制。</p>
<p>对于 Hard Attention 而言，令 <code>$s_t$</code> 表示在生成第 <code>$t$</code> 个词时所关注的位置变量，<code>$s_{t, i} = 1$</code> 表示当第 <code>$i$</code> 个位置用于提取视觉特征。将注意力位置视为一个中间潜变量，可以以一个参数为 <code>$\left\{\alpha_i\right\}$</code> 的多项式分布表示，同时将上下文向量 <code>$\hat{\mathbf{z}}_t$</code> 视为一个随机变量：</p>
<p><code>$$ \begin{equation} \begin{split} &amp; p \left(s_{t, i} = 1 | s_{j &lt; t}, \mathbf{a}\right) = \alpha_{t, i} \\ &amp; \hat{\mathbf{z}}_t = \sum_{i}{s_{t, i} \mathbf{a}_i} \end{split} \end{equation} $$</code></p>
<p>因此 Hard Attention 可以依据概率值从隐状态中进行采样计算得到上下文向量，同时为了实现梯度的反向传播，需要利用蒙特卡罗采样的方法来估计梯度。</p>
<p>对于 Soft Attention 而言，则直接计算上下文向量 <code>$\hat{\mathbf{z}}_t$</code> 的期望，计算方式如下：</p>
<p><code>$$ \mathbb{E}_{p \left(s_t | a\right)} \left[\hat{\mathbf{z}}_t\right] = \sum_{i=1}^{L}{\alpha_{t, i} \mathbf{a}_i} $$</code></p>
<p>其余部分的计算方式同 Bahdanau 等人 <sup id="fnref2:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 的论文类似。Soft Attention 模型可以利用标准的反向传播算法进行求解，直接嵌入到整个模型中一同训练，相对更加简单。</p>
<p>下图展示了一些图片标题生成结果的可视化示例，其中图片内 <span style="background-color:#000; color:#FFF; font-style:bold;">白色</span> 为关注的区域，<span style="border-bottom:2px solid;">画线的文本</span> 即为生成的标题中对应的词。</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/image-caption-generation-visual-attention.png" alt="Image-Caption-Generation-Visual-Attention"></p>
<h3 id="global-local-attention">Global &amp; Local Attention</h3>
<p>Luong 等人 <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> 提出了 Global Attention 和 Local Attention 两种不同的注意力机制用于机器翻译。Global Attention 的思想是在计算上下文向量 <code>$c_t$</code> 时将编码器的所有隐状态均考虑在内。对于对齐向量 <code>$\boldsymbol{a}_t$</code>，通过比较当前目标的隐状态 <code>$\boldsymbol{h}_t$</code> 与每一个输入的隐状态 <code>$\bar{\boldsymbol{h}}_s$</code> 得到，即：</p>
<p><code>$$ \begin{equation} \begin{split} \boldsymbol{a}_t &amp;= \text{align} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right) \\ &amp;= \dfrac{\exp \left(\text{score} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right)\right)}{\sum_{s'}{\exp \left(\text{score} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_{s'}\right)\right)}} \end{split} \end{equation} $$</code></p>
<p>其中 <code>$\text{score}$</code> 为一个基于内容 (content-based) 的函数，可选的考虑如下三种形式：</p>
<p><code>$$ \text{score} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right) = \begin{cases} \boldsymbol{h}_t^{\top} \bar{\boldsymbol{h}}_s &amp; dot \\ \boldsymbol{h}_t^{\top} \boldsymbol{W}_a \bar{\boldsymbol{h}}_s &amp; general \\ \boldsymbol{W}_a \left[\boldsymbol{h}_t; \bar{\boldsymbol{h}}_s\right] &amp; concat \end{cases} $$</code></p>
<p>我们利用一个基于位置 (location-based) 的函数构建注意力模型，其中对齐分数通过目标的隐状态计算得到：</p>
<p><code>$$ \boldsymbol{a}_t = \text{softmax} \left(\boldsymbol{W}_a \boldsymbol{h}_t\right) $$</code></p>
<p>Global Attention 模型的网络结构如下所示：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/global-attention.png" alt="Global-Attention"></p>
<p>Global Attention 的一个问题在于任意一个输出都需要考虑输入端的所有隐状态，这对于很长的文本 (例如：一个段落或一篇文章) 计算量太大。Local Attention 为了解决这个问题，首先在 <code>$t$</code> 时刻对于每个目标词生成一个对齐位置 <code>$p_t$</code>，其次上下文向量 <code>$\boldsymbol{c}_t$</code> 则由以 <code>$p_t$</code> 为中心前后各 <code>$D$</code> 大小的窗口 <code>$\left[p_t - D, p_t + D\right]$</code> 内的输入的隐状态计算得到。不同于 Global Attention，Local Attention 的对齐向量 <code>$\boldsymbol{a}_t \in \mathbb{R}^{2D + 1}$</code> 为固定维度。</p>
<p>一个比较简单的做法是令 <code>$p_t = t$</code>，也就是假设输入和输出序列是差不多是单调对齐的，我们称这种做法为 <em>Monotonic</em> Alignment (<strong>local-m</strong>)。另一种做法是预测 <code>$p_t$</code>，即：</p>
<p><code>$$ p_t = S \cdot \text{sigmoid} \left(\boldsymbol{v}_p^{\top} \tanh \left(\boldsymbol{W}_p \boldsymbol{h}_t\right)\right) $$</code></p>
<p>其中，<code>$\boldsymbol{W}_p$</code> 和 <code>$\boldsymbol{h}_t$</code> 为预测位置模型的参数，<code>$S$</code> 为输入句子的长度。我们称这种做法为 <em>Predictive</em> Alignment (<strong>local-p</strong>)。作为 <code>$\text{sigmoid}$</code> 函数的结果，<code>$p_t \in \left[0, S\right]$</code>，则通过一个以 <code>$p_t$</code> 为中心的高斯分布定义对齐权重：</p>
<p><code>$$ \boldsymbol{a}_t \left(s\right) = \text{align} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right) \exp \left(- \dfrac{\left(s - p_t\right)^2}{2 \sigma^2}\right) $$</code></p>
<p>其中，根据经验设置 <code>$\sigma = \dfrac{D}{2}$</code>，<code>$s$</code> 为在窗口大小内的一个整数。</p>
<p>Local Attention 模型的网络结构如下所示：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/local-attention.png" alt="Local-Attention"></p>
<h3 id="self-attention">Self Attention</h3>
<p>Vaswani 等人 <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 提出了一种新的网络结构，称之为 Transformer，其中采用了自注意力 (Self-attention) 机制。自注意力是一种将同一个序列的不同位置进行自我关联从而计算一个句子表示的机制。Transformer 利用堆叠的 Self Attention 和全链接网络构建编码器 (下图左) 和解码器 (下图右)，整个网络架构如下图所示：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/self-attention.png" alt="Self-Attention"></p>
<h4 id="编码器和解码器">编码器和解码器</h4>
<p><strong>编码器</strong> 是由 <code>$N = 6$</code> 个相同的网络层构成，每层中包含两个子层。第一层为一个 Multi-Head Self-Attention 层，第二层为一个 Position-Wise 全链接的前馈神经网络。每一层再应用一个残差连接 (Residual Connection) <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup> 和一个层标准化 (Layer Normalization) <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup>。则每一层的输出为 <code>$\text{LayerNorm} \left(x + \text{Sublayer} \left(x\right)\right)$</code>，其中 <code>$\text{Sublayer} \left(x\right)$</code> 为子层本身的函数实现。为了实现残差连接，模型中所有的子层包括 Embedding 层的输出维度均为 <code>$d_{\text{model}} = 512$</code>。</p>
<p><strong>解码器</strong> 也是由 <code>$N = 6$</code> 个相同的网络层构成，但每层中包含三个子层，增加的第三层用于处理编码器的输出。同编码器一样，每一层应用一个残差连接和一个层标准化。除此之外，解码器对 Self-Attention 层进行了修改，确保对于位置 <code>$i$</code> 的预测仅依赖于位置在 <code>$i$</code> 之前的输出。</p>
<h4 id="scaled-dot-product-multi-head-attention">Scaled Dot-Product &amp; Multi-Head Attention</h4>
<p>一个 Attention 函数可以理解为从一个序列 (Query) 和一个键值对集合 (Key-Value Pairs Set) 到一个输出的映射。文中提出了一种名为 <strong>Scaled Dot-Product Attention</strong> (如下图所示)，其中输入包括 queries，维度为 <code>$d_k$</code> 的 keys 和维度为 <code>$d_v$</code> 的 values。通过计算 queries 和所有 keys 的点积，除以 <code>$\sqrt{d_k}$</code>，再应用一个 softmax 函数获取 values 的权重。</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/scaled-dot-product-attention.png" alt="Scaled-Dot-Product-Attention"></p>
<p>实际中，我们会同时计算一个 Queries 集合中的 Attention，并将其整合成一个矩阵 <code>$Q$</code>。Keys 和 Values 也相应的整合成矩阵 <code>$K$</code> 和 <code>$V$</code>，则有：</p>
<p><code>$$ \text{Attention} \left(Q, K, V\right) = \text{softmax} \left(\dfrac{Q K^{\top}}{\sqrt{d_k}}\right) V $$</code></p>
<p>其中，<code>$Q \in \mathbb{R}^{n \times d_k}$</code>，<code>$Q$</code> 中的每一行为一个 query，<code>$K \in \mathbb{R}^{n \times d_k}, V \in \mathbb{R}^{n \times d_v}$</code>。<code>$\dfrac{1}{\sqrt{d_k}}$</code> 为一个归一化因子，避免点积的值过大导致 softmax 之后的梯度过小。</p>
<p><strong>Multi-Head Attention</strong> 的做法并不直接对原始的 keys，values 和 queries 应用注意力函数，而是学习一个三者各自的映射再应用 Atteneion，同时将这个过程重复 <code>$h$</code> 次。Multi-Head Attention 的网路结构如下图所示：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/multi-head-attention.png" alt="Multi-Head-Attention"></p>
<p>Multi-Head Attention 的计算过程如下所示：</p>
<p><code>$$ \begin{equation} \begin{split} \text{MultiHead} \left(Q, K, V\right) &amp;= \text{Concat} \left(\text{head}_1, \dotsc, \text{head}_h\right) W^O \\ \textbf{where } \text{head}_i &amp;= \text{Attention} \left(QW_i^Q, KW_i^K, VW_i^V\right) \end{split} \end{equation} $$</code></p>
<p>其中，<code>$W_i^Q \in \mathbb{R}^{d_{\text{model}} \times d_k}, W_i^K \in \mathbb{R}^{d_{\text{model}} \times d_k}, W_i^V \in \mathbb{R}^{d_{\text{model}} \times d_v}, W_i^O \in \mathbb{R}^{h d_v \times d_{\text{model}}}, $</code> 为映射的参数，<code>$h = 8$</code> 为重复的次数，则有 <code>$d_k = d_v = d_{\text{model}} / h = 64$</code>。</p>
<p>整个 Transformer 模型在三处使用了 Multi-Head Attention，分别是：</p>
<ol>
<li>Encoder-Decoder Attention Layers，其中 queries 来自于之前的 Decoder 层，keys 和 values 来自于 Encoder 的输出，该部分同其他 Seq2Seq 模型的 Attention 机制类似。</li>
<li>Encoder Self-Attention Layers，其中 queries，keys 和 values 均来自之前的 Encoder 层的输出，同时 Encoder 层中的每个位置都能够从之前层的所有位置获取到信息。</li>
<li>Decoder Self-Attention Layers，其中 queries，keys 和 values 均来自之前的 Decoder 层的输出，但 Decoder 层中的每个位置仅可以从之前网络层的包含当前位置之前的位置获取信息。</li>
</ol>
<h4 id="position-wise-feed-forward-networks">Position-wise Feed-Forward Networks</h4>
<p>在 Encoder 和 Decoder 中的每一层均包含一个全链接的前馈神经网络，其使用两层线性变换和一个 ReLU 激活函数实现：</p>
<p><code>$$ \text{FFN} \left(x\right) = \max \left(0, x W_1 + b_1\right) W_2 + b_2 $$</code></p>
<p>全链接层的输入和输出的维度 <code>$d_{\text{model}} = 512$</code>，内层的维度 <code>$d_{ff} = 2048$</code>。</p>
<h4 id="positional-encoding">Positional Encoding</h4>
<p>Transformer 模型由于未使用任何循环和卷积组件，因此为了利用序列的位置信息则在模型的 Embedding 输入中添加了 <strong>Position Encoding</strong>。Position Encoding 的维度同 Embedding 的维度相同，从而可以与 Embedding 进行加和，文中使用了如下两种形式：</p>
<p><code>$$ \begin{equation} \begin{split} PE_{\left(pos, 2i\right)} &amp;= \sin \left(pos / 10000^{2i / d_{\text{model}}}\right) \\ PE_{\left(pos, 2i+1\right)} &amp;= \cos \left(pos / 10000^{2i / d_{\text{model}}}\right) \end{split} \end{equation} $$</code></p>
<p>其中，<code>$pos$</code> 为位置，<code>$i$</code> 为对应的维度，选用这种表示形式的原因是对于一个固定的偏移 <code>$k$</code>，<code>$PE_{pos + k}$</code> 都可以利用 <code>$PE_{pos}$</code> 线性表示。这是因为对于正弦和余弦函数有：</p>
<p><code>$$ \begin{equation} \begin{split} \sin \left(\alpha + \beta\right) &amp;= \sin \alpha \cos \beta + \cos \alpha \sin \beta \\ \cos \left(\alpha + \beta\right) &amp;= \cos \alpha \sin \beta - \sin \alpha \sin \beta \end{split} \end{equation} $$</code></p>
<h4 id="why-self-attention">Why Self-Attention</h4>
<p>相比于循环和卷积层，Transformer 模型利用 Self-Attention 层用于一个序列 <code>$\left(x_1, \dotsc, x_n\right)$</code> 到另一个等长序例 <code>$\left(z_1, \dotsc, z_n\right)$</code> 的映射，其中 <code>$x_i, z_i \in \mathbb{R}^d$</code>。Self-Attention 与循环和卷积的对比如下表所示：</p>
<table>
  <thead>
      <tr>
          <th>层类型</th>
          <th>每层的复杂度</th>
          <th>序列操作数</th>
          <th>长距离依赖路径长度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Self-Attention</td>
          <td><code>$O \left(n^2 \cdot d\right)$</code></td>
          <td><code>$O \left(1\right)$</code></td>
          <td><code>$O \left(1\right)$</code></td>
      </tr>
      <tr>
          <td>Recurrent</td>
          <td><code>$O \left(n \cdot d^2\right)$</code></td>
          <td><code>$O \left(n\right)$</code></td>
          <td><code>$O \left(n\right)$</code></td>
      </tr>
      <tr>
          <td>Convolutional</td>
          <td><code>$O \left(k \cdot n \cdot d^2\right)$</code></td>
          <td><code>$O \left(1\right)$</code></td>
          <td><code>$O \left(\log_k \left(n\right)\right)$</code></td>
      </tr>
      <tr>
          <td>Self-Attention (restricted)</td>
          <td><code>$O \left(r \cdot n \cdot d\right)$</code></td>
          <td><code>$O \left(1\right)$</code></td>
          <td><code>$O \left(n/r\right)$</code></td>
      </tr>
  </tbody>
</table>
<ol>
<li>对于每层的复杂度，当序例的长度 <code>$n$</code> 比表示的维度 <code>$d$</code> 小时，Self-Attention 要比循环结构计算复杂度小。为了改进在长序列上 Self-Attention 的计算性能，Self-Attention 可以被限制成仅考虑与输出位置对应的输入序列位置附近 <code>$r$</code> 窗口大小内的信息。</li>
<li>Recurrent 层的最小序列操作数为 <code>$O \left(n\right)$</code>，其他情况为 <code>$O \left(1\right)$</code>，这使得 Recurrent 的并行能力较差，即上表中的 Self-Attention (restricted)。</li>
<li>学习到长距离依赖是很多序列任务的关键，影响该能力的一个重要因素就是前向和后向信号穿越整个网络的路径长度，这个路径长度越短，越容易学习到长距离依赖。</li>
</ol>
<h4 id="attention-visualizations">Attention Visualizations</h4>
<p>第一张图展示了 Self-Attention 学到的句子内部的一个长距离依赖 <strong>“making &hellip; more diffcult”</strong>，图中不同的颜色表示不同 Head 的 Attention，颜色越深表示 Attention 的值越大。</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/self-attention-long-distance-dependencies.png" alt="Self-Attention-Long-Distance-Dependencies"></p>
<p>第二张图展示了 Self-Attention 学到的一个指代消解关系 (Anaphora Resolution)，its 指代的为上文中的 law。下图 (上) 为 Head 5 的所有 Attention，下图 (下) 为 Head 5 和 6 关于词 its 的 Attention，不难看出模型学习到了 its 和 law 之间的依赖关系 (指代消解关系)。</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/self-attention-anaphora-resolution.png" alt="Self-Attention-Long-Anaphora-Resolution"></p>
<h3 id="hierarchical-attention">Hierarchical Attention</h3>
<p>Yang 等人 <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup> 提出了一种层级的注意力 (Hierarchical Attention) 网络用于文档分类。Hierarchical Attention 共包含 4 层：一个词编码器 (Word Encoder)，一个词级别的注意力层 (Word Attention)，一个句子编码器 (Sentence Encoder) 和一个句子级别的注意力层 (Sentence Attention)。网络架构如下图所示：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/hierarchical-attention.png" alt="Hierarchical-Attention"></p>
<h4 id="word-encoder">Word Encoder</h4>
<p>对于一个给定的句子 <code>$w_{it}, t \in \left[0, T\right]$</code>，通过一个 Embedding 矩阵 <code>$W_e$</code> 得到每个词的向量表示，再应用一个双向的 GRU，即：</p>
<p><code>$$ \begin{equation} \begin{split} x_{it} &amp;= W_e w_{it}, t \in \left[1, T\right] \\ \overrightarrow{h}_{it} &amp;= \overrightarrow{\text{GRU}} \left(x_{it}\right), t \in \left[1, T\right] \\ \overleftarrow{h}_{it} &amp;= \overleftarrow{\text{GRU}} \left(x_{it}\right), t \in \left[T, 1\right] \end{split} \end{equation} $$</code></p>
<p>最后将前向的隐状态 <code>$\overrightarrow{h}_{it}$</code> 和后向的隐状态 <code>$\overleftarrow{h}_{it}$</code> 进行拼接，得到 <code>$h_{ij} = \left[\overrightarrow{h}_{it}, \overleftarrow{h}_{it}\right]$</code> 为整个句子在词 <code>$w_{ij}$</code> 附近的汇总信息。</p>
<h4 id="word-attention">Word Attention</h4>
<p>Word Attention 同一般的 Attention 机制类似，计算方式如下：</p>
<p><code>$$ \begin{equation} \begin{split} u_{it} &amp;= \tanh \left(W_w h_{it} + b_w\right) \\ a_{it} &amp;= \dfrac{\exp \left(u_{it}^{\top} u_w\right)}{\sum_{t}{\exp \left(u_{it}^{\top} u_w\right)}} \\ s_i &amp;= \sum_{t}{a_{it} h_{it}} \end{split} \end{equation} $$</code></p>
<h4 id="sentence-encoder">Sentence Encoder</h4>
<p>在 Word Attention 之后，我们得到了一个句子的表示 <code>$s_i$</code>，类似的我们利用一个双向的 GRU 编码文档中的 <code>$L$</code> 个句子：</p>
<p><code>$$ \begin{equation} \begin{split} \overrightarrow{h}_i &amp;= \overrightarrow{\text{GRU}} \left(s_i\right), i \in \left[1, L\right] \\ \overleftarrow{h}_i &amp;= \overleftarrow{\text{GRU}} \left(s_i\right), i \in \left[L, 1\right] \end{split} \end{equation} $$</code></p>
<p>最后将前向的隐状态 <code>$\overrightarrow{h}_i$</code> 和后向的隐状态 <code>$\overleftarrow{h}_i$</code> 进行拼接，得到 <code>$h_i = \left[\overrightarrow{h}_i, \overleftarrow{h}_i\right]$</code> 为整个文档关于句子 <code>$s_i$</code> 的注意力汇总信息。</p>
<h4 id="sentence-attention">Sentence Attention</h4>
<p>同理可得 Sentence Attention 的计算方式如下：</p>
<p><code>$$ \begin{equation} \begin{split} u_i &amp;= \tanh \left(W_s h_i + b_s\right) \\ a_i &amp;= \dfrac{\exp \left(u_i^{\top} u_s\right)}{\sum_{i}{\exp \left(u_i^{\top} u_s\right)}} \\ v &amp;= \sum_{i}{a_i h_i} \end{split} \end{equation} $$</code></p>
<p>最终得到整个文档的向量表示 <code>$v$</code>。</p>
<h3 id="attention-over-attention">Attention-over-Attention</h3>
<p>Cui 等人 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup> 提出了 Attention-over-Attention 的模型用于阅读理解 (Reading Comprehension)。网络结构如下图所示：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/attention-over-attention.png" alt="Attention-over-Attention"></p>
<p>对于一个给定的训练集 <code>$\langle \mathcal{D}, \mathcal{Q}, \mathcal{A} \rangle$</code>，模型包含两个输入，一个文档 (Document) 和一个问题序列 (Query)。网络的工作流程如下：</p>
<ol>
<li>先获取 Document 和 Query 的 Embedding 结果，再应用一个双向的 GRU 得到对应的隐状态 <code>$h_{doc}$</code> 和 <code>$h_{query}$</code>。</li>
<li>计算一个 Document 和 Query 的匹配程度矩阵 <code>$M \in \mathbb{R}^{\lvert \mathcal{D} \rvert \times \lvert \mathcal{Q} \rvert}$</code>，其中第 <code>$i$</code> 行第 <code>$j$</code> 列的值计算方式如下：
<code>$$ M \left(i, j\right) = h_{doc} \left(i\right)^{\top} \cdot h_{query} \left(j\right) $$</code></li>
<li>按照 <strong>列</strong> 的方向对矩阵 <code>$M$</code> 应用 softmax 函数，矩阵中的每一列为考虑一个 Query 中的词的 Document 级别的 Attention，因此定义 <code>$\alpha \left(t\right) \in \mathbb{R}^{\lvert \mathcal{D} \rvert}$</code> 为 <code>$t$</code> 时刻的 Document 级别 Attention (<strong><em>query-to-document</em> attention</strong>)。计算方式如下：
<code>$$ \begin{equation} \begin{split} \alpha \left(t\right) &amp;= \text{softmax} \left(M \left(1, t\right), \dotsc, M \left(\lvert \mathcal{D} \rvert, t\right)\right) \\ \alpha &amp;= \left[\alpha \left(1\right), \alpha \left(2\right), \dotsc, \alpha \left(\lvert \mathcal{Q} \rvert\right)\right] \end{split} \end{equation} $$</code></li>
<li>同理按照 <strong>行</strong> 的方向对矩阵 <code>$M$</code> 应用 softmax 函数，可以得到 <code>$\beta \left(t\right) \in \mathbb{R}^{\lvert \mathcal{Q} \rvert}$</code> 为 <code>$t$</code> 时刻的 Query 级别的 Attention (<strong><em>document-to-query</em> attention</strong>)。计算方式如下：
<code>$$ \beta \left(t\right) = \text{softmax} \left(M \left(t, 1\right), \dotsc, M \left(t, \lvert \mathcal{Q} \rvert\right)\right) $$</code></li>
<li>对于 document-to-query attention，我们对结果进行平均得到：
<code>$$ \beta = \dfrac{1}{n} \sum_{t=1}^{\lvert \mathcal{D} \rvert}{\beta \left(t\right)} $$</code></li>
<li>最终利用 <code>$\alpha$</code> 和 <code>$\beta$</code> 的点积 <code>$s = \alpha^{\top} \beta \in \mathbb{R}^{\lvert \mathcal{D} \rvert}$</code> 得到 attended document-level attention (即 <strong><em>attention-over-attention</em></strong>)。</li>
</ol>
<h3 id="multi-step-attention">Multi-step Attention</h3>
<p>Gehring 等人 <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup> 提出了基于 CNN 和 Multi-step Attention 的模型用于机器翻译。网络结构如下图所示：</p>
<p><img src="/images/cn/2018-10-12-seq2seq-and-attention-machanism/multi-step-attention.png" alt="Multi-step-Attention"></p>
<h4 id="position-embeddings">Position Embeddings</h4>
<p>模型首先得到序列 <code>$\mathbf{x} = \left(x_1, \dotsc, x_m\right)$</code> 的 Embedding <code>$\mathbf{w} = \left(w_1, \dotsc , w_m\right), w_j \in \mathbb{R}^f$</code>。除此之外还将输入序列的位置信息映射为 <code>$\mathbf{p} = \left(p_1, \dotsc, p_m\right), p_j \in \mathbb{R}^f$</code>，最终将两者进行合并得到最终的输入 <code>$\mathbf{e} = \left(w_1 + p_1, \dotsc, w_m + p_m\right)$</code>。同时在解码器部分也采用类似的操作，将其与解码器网络的输出表示合并之后再喂入解码器网络 <code>$\mathbf{g} = \left(g_1, \dotsc, g_n\right)$</code> 中。</p>
<h4 id="convolutional-block-structure">Convolutional Block Structure</h4>
<p>编码器和解码器均由多个 Convolutional Block 构成，每个 Block 包含一个卷积计算和一个非线性计算。令 <code>$\mathbf{h}^l = \left(h_1^l, \dotsc, h_n^l\right)$</code> 表示解码器第 <code>$l$</code> 个 Block 的输出，<code>$\mathbf{z}^l = \left(z_1^l, \dotsc, z_m^l\right)$</code> 表示编码器第 <code>$l$</code> 个 Block 的输出。对于一个大小 <code>$k = 5$</code> 的卷积核，其结果的隐状态包含了这 5 个输入，则对于一个 6 层的堆叠结构，结果的隐状态则包含了输入中的 25 个元素。</p>
<p>在每一个 Convolutional Block 中，卷积核的参数为 <code>$W \in \mathbb{R}^{2d \times kd}, b_w \in \mathbb{R}^{2d}$</code>，其中 <code>$k$</code> 为卷积核的大小，经过卷积后的输出为 <code>$Y \in \mathbb{R}^{2d}$</code>。之后的非线性层采用了 Dauphin 等人 <sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup> 提出的 Gated Linear Units (GLU)，对于卷积后的输出 <code>$Y = \left[A, B\right]$</code> 有：</p>
<p><code>$$ v \left(\left[A, B\right]\right) = A \otimes \sigma \left(B\right) $$</code></p>
<p>其中，<code>$A, B \in \mathbb{R}^d$</code> 为非线性单元的输入，<code>$\otimes$</code> 为逐元素相乘，<code>$\sigma \left(B\right)$</code> 为用于控制输入 <code>$A$</code> 与当前上下文相关度的门结构。</p>
<p>模型中还加入了残差连接，即：</p>
<p><code>$$ h_i^l = v \left(W^l \left[h_{i-k/2}^{l-1}, \dotsc, h_{i+k/2}^{l-1}\right] + b_w^l\right) + h_i^{l-1} $$</code></p>
<p>为了确保网络卷积层的输出同输入的长度相匹配，模型对输入数据的前后填补 <code>$k - 1$</code> 个零值，同时为了避免解码器使用当前预测位置之后的信息，模型删除了卷积输出尾部的 <code>$k$</code> 个元素。在将 Embedding 喂给编码器网络之前，在解码器输出应用 softmax 之前以及所有解码器层计算 Attention 分数之前，建立了一个从 Embedding 维度 <code>$f$</code> 到卷积输出大小 <code>$2d$</code> 的线性映射。最终，预测下一个词的概率计算方式如下：</p>
<p><code>$$ p \left(y_{i+1} | y_1, \dotsc, y_i, \mathbf{x}\right) = \text{softmax} \left(W_o h_i^L + b_o\right) \in \mathbb{R}^T $$</code></p>
<h4 id="multi-step-attention-1">Multi-step Attention</h4>
<p>模型的解码器网络中引入了一个分离的注意力机制，在计算 Attention 时，将解码器当前的隐状态 <code>$h_i^l$</code> 同之前输出元素的 Embedding 进行合并：</p>
<p><code>$$ d_i^l = W_d^l h_i^l + b_d^l + g_i $$</code></p>
<p>对于解码器网络层 <code>$l$</code> 中状态 <code>$i$</code> 和输入元素 <code>$j$</code> 之间的的 Attention <code>$a_{ij}^l$</code> 通过解码器汇总状态 <code>$d_i^l$</code> 和最后一个解码器 Block <code>$u$</code> 的输出 <code>$z_j^u$</code> 进行点积运算得到：</p>
<p><code>$$ a_{ij}^l = \dfrac{\exp \left(d_i^l \cdot z_j^u\right)}{\sum_{t=1}^{m}{\exp \left(d_i^l \cdot z_t^u\right)}} $$</code></p>
<p>条件输入 <code>$c_i^l$</code> 的计算方式如下：</p>
<p><code>$$ c_i^l = \sum_{j=1}^{m}{a_{ij}^l \left(z_j^u + e_j\right)} $$</code></p>
<p>其中，<code>$e_j$</code> 为输入元素的 Embedding。与传统的 Attention 不同，<code>$e_j$</code> 的加入提供了一个有助于预测的具体输入元素信息。</p>
<p>最终将 <code>$c_i^l$</code> 加到对应的解码器层的输出 <code>$h_i^l$</code>。这个过程与传统的单步 Attention 不同，被称之为 Multiple Hops <sup id="fnref:13"><a href="#fn:13" class="footnote-ref" role="doc-noteref">13</a></sup>。这种方式使得模型在计算 Attention 时会考虑之前已经注意过的输入信息。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., &amp; Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation. In <em>Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP)</em> (pp. 1724–1734).&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Sutskever, I., Vinyals, O., &amp; Le, Q. V. (2014). Sequence to Sequence Learning with Neural Networks. In Z. Ghahramani, M. Welling, C. Cortes, N. D. Lawrence, &amp; K. Q. Weinberger (Eds.), <em>Advances in Neural Information Processing Systems 27</em> (pp. 3104–3112).&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Bahdanau, D., Cho, K., &amp; Bengio, Y. (2014). Neural Machine Translation by Jointly Learning to Align and Translate. <em>arXiv preprint arXiv:1409.0473</em>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref2:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Xu, K., Ba, J., Kiros, R., Cho, K., Courville, A., Salakhudinov, R., … Bengio, Y. (2015). Show, Attend and Tell: Neural Image Caption Generation with Visual Attention. In <em>International Conference on Machine Learning</em> (pp. 2048–2057).&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Luong, T., Pham, H., &amp; Manning, C. D. (2015). Effective Approaches to Attention-based Neural Machine Translation. In <em>Proceedings of the 2015 Conference on Empirical Methods in Natural Language Processing</em> (pp. 1412–1421).&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., … Polosukhin, I. (2017). Attention is All you Need. In I. Guyon, U. V. Luxburg, S. Bengio, H. Wallach, R. Fergus, S. Vishwanathan, &amp; R. Garnett (Eds.), <em>Advances in Neural Information Processing Systems 30</em> (pp. 5998–6008).&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>He, K., Zhang, X., Ren, S., &amp; Sun, J. (2016). Deep Residual Learning for Image Recognition. In <em>2016 IEEE Conference on Computer Vision and Pattern Recognition (CVPR)</em> (pp. 770–778).&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Ba, J. L., Kiros, J. R., &amp; Hinton, G. E. (2016). Layer Normalization. <em>arXiv preprint arXiv:1607.06450</em>&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Yang, Z., Yang, D., Dyer, C., He, X., Smola, A., &amp; Hovy, E. (2016). Hierarchical Attention Networks for Document Classification. In <em>Proceedings of the 2016 Conference of the North American Chapter of the Association for Computational Linguistics</em>: Human Language Technologies (pp. 1480–1489).&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Cui, Y., Chen, Z., Wei, S., Wang, S., Liu, T., &amp; Hu, G. (2017). Attention-over-Attention Neural Networks for Reading Comprehension. In <em>Proceedings of the 55th Annual Meeting of the Association for Computational Linguistics</em> (Volume 1: Long Papers) (pp. 593–602).&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Gehring, J., Auli, M., Grangier, D., Yarats, D., &amp; Dauphin, Y. N. (2017). Convolutional Sequence to Sequence Learning. In <em>International Conference on Machine Learning</em> (pp. 1243–1252).&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p>Dauphin, Y. N., Fan, A., Auli, M., &amp; Grangier, D. (2016). Language Modeling with Gated Convolutional Networks. <em>arXiv preprint arXiv:1612.08083</em>&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:13">
<p>Sukhbaatar, S., szlam,  arthur, Weston, J., &amp; Fergus, R. (2015). End-To-End Memory Networks. In C. Cortes, N. D. Lawrence, D. D. Lee, M. Sugiyama, &amp; R. Garnett (Eds.), <em>Advances in Neural Information Processing Systems 28</em> (pp. 2440–2448).&#160;<a href="#fnref:13" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>词向量 (Word Embeddings)</title><link>https://zeqiang.fun/cn/2018/10/word-embeddings/</link><pubDate>Mon, 01 Oct 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/10/word-embeddings/</guid><description><![CDATA[
        <h2 id="文本表示">文本表示</h2>
<p>文本表示是计算机处理自然语言的核心，我们希望计算机能够同人类一样对自然语言能够实现语义层面的理解，但这并非易事。在中文和拉丁语系中，文本的直观表示就存在一定的差异，拉丁语系中词与词之间存在天然的分隔符，而中文则没有。</p>
<blockquote>
<p>I can eat glass, it doesn&rsquo;t hurt me.<br>
我能吞下玻璃而不伤身体。</p>
</blockquote>
<p>所以，在处理中文之前我们往往需要对原始文本进行分词，在此我们不谈这部分工作，假设我们已经得到了分词完的文本，即我们后续需要处理的“<strong>词</strong>”。早期的词表示方法多采用独热编码 (One-Hot Encoding)，对于每一个不同的词都使用一个单独的向量进行表示。对于一个包含 <code>$n$</code> 个词的语料而言，一个词的向量表示 <code>$\text{word}_i \in \left\{0, 1\right\}^n$</code> 仅在第 <code>$i$</code> 的位置值为 1，其他位置的值均为 0。例如，我们可以将“父亲”表示为：</p>
<p><code>$$ \left[1, 0, 0, 0, 0, 0, ...\right] \nonumber $$</code></p>
<p>One-Hot Encoding 的表示方法十分简洁，但也存在着一些问题。</p>
<h3 id="维数灾难-the-curse-of-dimensionality">维数灾难 (The Curse of Dimensionality)</h3>
<p>在很多现实问题中，我们仅用少数的特征是很难利用一个线性模型将数据区分开来的，也就是线性不可分问题。一个有效的方法是利用核函数实现一个非线性变换，将非线性问题转化成线性问题，通过求解变换后的线性问题进而求解原来的非线性问题。</p>
<p>假设 <code>$\mathcal{X}$</code> 是输入空间（欧式空间 <code>$\mathbb{R}^n$</code> 的子集或离散结合），<code>$\mathcal{H}$</code> 为特征空间（希尔伯特空间），若存在一个从 <code>$\mathcal{X}$</code> 到 <code>$ \mathcal{H}$</code> 的映射：</p>
<p><code>$$\phi \left(x\right): \mathcal{X} \rightarrow \mathcal{H}$$</code></p>
<p>使得对所有 <code>$x, z \in \mathcal{X}$</code> ，函数 <code>$K\left(x, z\right)$</code> 满足条件：</p>
<p><code>$$K\left(x, z\right) = \phi \left(x\right) \cdot \phi \left(z\right)$$</code></p>
<p>则 <code>$K\left(x, z\right)$</code> 为核函数， <code>$\phi \left(x\right)$</code> 为映射函数，其中 <code>$\phi \left(x\right) \cdot \phi \left(z\right)$</code> 为 <code>$\phi \left(x\right)$</code> 和 <code>$\phi \left(z\right)$</code> 的内积。</p>
<p>例如，对于一个下图所示的二维数据，显然是线性不可分的。</p>
<p><img src="/images/cn/2018-10-01-word-embeddings/2d-points.png" alt="2d-Points"></p>
<p>构建一个映射 <code>$\phi: \mathbb{R}^2 \rightarrow \mathbb{R}^3$</code> 经 <code>$X$</code> 映射为： <code>$x = x^2, y = y^2, z = y$</code> ，则通过变换后的数据通过可视化可以明显地看出，数据是可以通过一个超平面来分开的。</p>
<p><img src="/images/cn/2018-10-01-word-embeddings/3d-points.png" alt="3d-Points"></p>
<p>可以说随着维度的增加，我们更有可能找到一个超平面（线性模型）将数据划分开来。尽管看起来，随着维度的增加似乎有助于我们构建模型，但是同时数据在高维空间的分布变得越来越<strong>稀疏</strong>。因此，在构建机器学习模型时，当我们需要更好的覆盖数据的分布时，我们需要的数据量就更大，这也就会导致需要更多的时间去训练模型。例如，假设所有特征均为0到1之间连续分布的数据，针对1维的情况，当覆盖50%的数据时，仅需全体50%的样本即可；针对2维的情况，当覆盖50%的数据时，则需全体71% ( <code>$0.71^2 \approx 0.5$</code> ) 的样本；针对3维的情况，当覆盖50%的数据时，则需全体79% ( <code>$0.79^3 \approx 0.5$</code> )，这就是我们所说的维数灾难。</p>
<h3 id="分散式表示-distributed-representations">分散式表示 (Distributed Representations)</h3>
<p>分散式表示（Distributed Representations）<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 最早由 Hiton 提出，对比于传统的 One-Hot Representation ，Distributed Representations 可以将数据表示为低维，稠密，连续的向量，也就是说将原始空间中的潜在信息分散的表示在低维空间的不同维度上。</p>
<p>传统的 One-Hot Representation 会将数据表示成一个很长的向量，例如，在 NLP 中，利用 One-Hot Representation 表示一个单词：</p>
<pre><code>父亲: [1, 0, 0, 0, 0, 0, ...]
爸爸: [0, 1, 0, 0, 0, 0, ...]
母亲: [0, 0, 1, 0, 0, 0, ...]
妈妈: [0, 0, 0, 1, 0, 0, ...]
</code></pre>
<p>这种表示形式很简介，但也很稀疏，相当于语料库中有多少个词，则表示空间的维度就需要多少。那么，对于传统的聚类算法，高斯混合模型，最邻近算法，决策树或高斯 SVM 需要 <code>$O\left(N\right)$</code> 个参数 (或 <code>$O\left(N\right)$</code> 个样本) 将能够将 <code>$O\left(N\right)$</code> 的输入区分开来。而像 RBMs ，稀疏编码，Auto-Encoder 或多层神经网络则可以利用 <code>$O\left(N\right)$</code> 个参数表示 <code>$O\left(2^k\right)$</code> 的输入，其中 <code>$k \leq N$</code> 为稀疏表示中非零元素的个数 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>。</p>
<p>采用 Distributed Representation，则可以将单词表示为：</p>
<pre><code>父亲: [0.12, 0.34, 0.65, ...]
爸爸: [0.11, 0.33, 0.58, ...]
母亲: [0.34, 0.98, 0.67, ...]
妈妈: [0.29, 0.92, 0.66, ...]
</code></pre>
<p>利用这种表示，我们不仅可以将稀疏的高维空间转换为稠密的低维空间，同时我们还能学习出文本间的语义相似性来，例如实例中的 <code>父亲</code> 和 <code>爸爸</code>，从语义上看其均表示 <code>父亲</code> 的含义，但是如果利用 One-Hot Representation 编码则 <code>父亲</code> 与 <code>爸爸</code> 的距离同其与 <code>母亲</code> 或 <code>妈妈</code> 的距离时相同的，而利用 Distributed Representation 编码，则 <code>父亲</code> 同 <code>爸爸</code> 之间的距离要远小于其同 <code>母亲</code> 或 <code>妈妈</code> 之间的距离。</p>
<h2 id="word-embedding-之路">Word Embedding 之路</h2>
<h3 id="n-gram-模型">N-gram 模型</h3>
<p>N-gram (N 元语法) 是一种文本表示方法，指文中连续出现的 <code>$n$</code> 个词语。N-gram 模型是基于 <code>$n-1$</code> 阶马尔科夫链的一种概率语言模型，可以通过前 <code>$n-1$</code> 个词对第 <code>$n$</code> 个词进行预测。Bengio 等人 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 提出了一个三层的神经网络的概率语言模型，其网络结构如下图所示：</p>
<p><img src="/images/cn/2018-10-01-word-embeddings/nplm-network.png" alt="NPLM-Network"></p>
<p>模型的最下面为前 <code>$n-1$</code> 个词 <code>$w_{t-n+1}, ..., w_{t-2}, w_{t-1}$</code>，每个词 <code>$w_i$</code> 通过查表的方式同输入层对应的词向量 <code>$C \left(w_i\right)$</code> 相连。词表 <code>$C$</code> 为一个 <code>$\lvert V\rvert \times m$</code> 大小的矩阵，其中 <code>$\lvert V\rvert$</code> 表示语料中词的数量，<code>$m$</code> 表示词向量的维度。输入层则为前 <code>$n-1$</code> 个词向量拼接成的向量 <code>$x$</code>，其维度为 <code>$m \left(n-1\right) \times 1$</code>。隐含层直接利用 <code>$d + Hx$</code> 计算得到，其中 <code>$H$</code> 为隐含层的权重，<code>$d$</code> 为隐含层的偏置。输出层共包含 <code>$\lvert V\rvert$</code> 个神经元，每个神经元 <code>$y_i$</code> 表示下一个词为第 <code>$i$</code> 个词的未归一化的 log 概率，即：</p>
<p><code>$$ y = b + Wx + U \tanh \left(d + Hx\right) $$</code></p>
<p>对于该问题，我们的优化目标为最大化如下的 log 似然函数：</p>
<p><code>$$ L = \dfrac{1}{T} \sum_{t}{f \left(w_t, w_{t-1}, ..., w_{t-n+1}\right) + R \left(\theta\right)} $$</code></p>
<p>其中，<code>$f \left(w_t, w_{t-1}, ..., w_{t-n+1}\right)$</code> 为利用前 <code>$n-1$</code> 个词预测当前词 <code>$w_t$</code> 的条件概率，<code>$R \left(\theta\right)$</code> 为参数的正则项，<code>$\theta = \left(b, d, W, U, H, C\right)$</code>。<code>$C$</code> 作为模型的参数之一，随着模型的训练不断优化，在模型训练完毕后，<code>$C$</code> 中保存的即为词向量。</p>
<h3 id="continuous-bag-of-words-cbow-和-skip-gram-模型">Continuous Bag-of-Words (CBOW) 和 Skip-gram 模型</h3>
<p>CBOW 和 Skip-gram 均考虑一个词的上下文信息，两种模型的结构如下图所示：</p>
<p><img src="/images/cn/2018-10-01-word-embeddings/cbow-skipgram.png" alt="CBOW-Skipgram"></p>
<p>两者在给定的上下文信息中 (即前后各 <code>$m$</code> 个词) 忽略了上下文环境的序列信息，CBOW (上图左) 是利用上下文环境中的词预测当前的词，而 Skip-gram (上图右) 则是用当前词预测上下文中的词。</p>
<p>对于 CBOW，<code>$x_{1k}, x_{2k}, ..., x_{Ck}$</code> 为上下文词的 One-Hot 表示，<code>$\mathbf{W}_{V \times N}$</code> 为所有词向量构成的矩阵 (词汇表)，<code>$y_j$</code> 为利用上下文信息预测得到的当前词的 One-Hot 表示输出，其中 <code>$C$</code> 为上下文词汇的数量，<code>$V$</code> 为词汇表中词的总数量，<code>$N$</code> 为词向量的维度。从输入层到隐含层，我们对输入层词对应的词向量进行简单的加和，即：</p>
<p><code>$$ h_i = \sum_{c=1}^{C}{x_{ck} \mathbf{W}_{V \times N}} $$</code></p>
<p>对于 Skip-gram，<code>$x_k$</code> 为当前词的 One-Hot 表示，<code>$\mathbf{W}_{V \times N}$</code> 为所有词向量构成的矩阵 (词汇表)，<code>$y_{1j}, y_{2j}, ..., y_{Cj}$</code> 为预测的上次文词汇的 One-Hot 表示输出。从输入层到隐含层，直接将 One-Hot 的输入向量转换为词向量表示即可。</p>
<p>除此之外两者还有一些其他的区别：</p>
<ol>
<li>CBOW 要比 Skip-gram 模型训练快。从模型中我们不难发现：从隐含层到输出层，CBOW 仅需要计算一个损失，而 Skip-gram 则需要计算 <code>$C$</code> 个损失再进行平均进行参数优化。</li>
<li>Skip-gram 在小数量的数据集上效果更好，同时对于生僻词的表示效果更好。CBOW 在从输入层到隐含层时，对输入的词向量进行了平均 (可以理解为进行了平滑处理)，因此对于生僻词，平滑后则容易被模型所忽视。</li>
</ol>
<h3 id="word2vec">Word2Vec</h3>
<p>Mikolov 等人 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 利用上面介绍的 CBOW 和 Skip-gram 两种模型提出了经典的 Word2Vec 算法。Word2Vec 中针对 CBOW 和 Skip-gram 又提出了两种具体的实现方案 Hierarchical Softmax (层次 Softmax) 和 Negative Sampling (负采样)，因此共有 4 种不同的模型。</p>
<ul>
<li><strong>基于 Hierarchical Softmax 的模型</strong></li>
</ul>
<p><strong>基于 Hierarchical Softmax 的 CBOW 模型如下</strong>：</p>
<p><img src="/images/cn/2018-10-01-word-embeddings/hierarchical-softmax-cbow.png" alt="Hierarchical-Softmax-CBOW"></p>
<p>其中：</p>
<ol>
<li><strong>输入层</strong>：包含了 <code>$C$</code> 个词的词向量，<code>$\mathbf{v} \left(w_1\right), \mathbf{v} \left(w_2\right), ..., \mathbf{v} \left(w_C\right) \in \mathbb{R}^N$</code>，<code>$N$</code> 为词向量的维度。</li>
<li><strong>投影层</strong>：将输入层的向量进行加和，即：<code>$\mathbf{x}_w = \sum_{i=1}^{C}{\mathbf{v} \left(w_i\right)} \in \mathbb{R}^N$</code>。</li>
<li><strong>输出层</strong>：输出为一颗二叉树，是根据语料构建出来的 Huffman 树 <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>，其中每个叶子节点为词汇表中的一个词。</li>
</ol>
<p>Hierarchical Softmax 是解决概率语言模型中计算效率的关键，CBOW 模型去掉了隐含层，同时将输出层改为了 Huffman 树。对于该模型的优化求解，我们首先引入一些符号，对于 Huffman 树的一个叶子节点 (即词汇表中的词 <code>$w$</code>)，记：</p>
<ul>
<li><code>$p^w$</code>：从根节点出发到达 <code>$w$</code> 对应的叶子节点的路径。</li>
<li><code>$l^w$</code>：路径 <code>$p^w$</code> 包含的节点的个数。</li>
<li><code>$p_1^w, p_1^w, ..., p_{l^w}^w$</code>：路径 <code>$p^w$</code> 中的 <code>$l^w$</code> 个节点，其中 <code>$p_1^w$</code> 表示根节点，<code>$p_{l^w}^w$</code> 表示词 <code>$w$</code> 对应的叶子节点。</li>
<li><code>$d_2^w, d_3^w, ..., d_{l^w}^w \in \{0, 1\}$</code>：词 <code>$w$</code> 的 Huffman 编码，由 <code>$l^w - 1$</code> 位编码构成，<code>$d_j^w$</code> 表示路径 <code>$p^w$</code> 中第 <code>$j$</code> 个结点对应的编码。</li>
<li><code>$\theta_1^w, \theta_1^w, ..., \theta_{l^w - 1}^w \in \mathbb{R}^N$</code>：路径 <code>$p^w$</code> 中非叶子节点对应的向量，<code>$\theta_j^w$</code> 表示路径 <code>$p^w$</code> 中第 <code>$j$</code> 个非叶子节点对应的向量。</li>
</ul>
<p>首先我们需要根据向量 <code>$\mathbf{x}_w$</code> 和 Huffman 树定义条件概率 <code>$p \left(w | Context\left(w\right)\right)$</code>。我们可以将其视为一系列的二分类问题，在到达对应的叶子节点的过程中，经过的每一个非叶子节点均为对应一个取值为 0 或 1 的 Huffman 编码。因此，我们可以将编码为 1 的节点定义为负类，将编码为 0 的节点定义为正类 (即分到左边为负类，分到右边为正类)，则这条路径上对应的标签为：</p>
<p><code>$$ Label \left(p_i^w\right) = 1 - d_i^w, i = 2, 3, ..., l^w $$</code></p>
<p>则对于一个节点被分为正类的概率为 <code>$\sigma \left(\mathbf{x}_w^{\top} \theta\right)$</code>，被分为负类的概率为 <code>$1 - \sigma \left(\mathbf{x}_w^{\top} \theta\right)$</code>。则条件概率可以表示为：</p>
<p><code>$$ p \left(w | Context\left(w\right)\right) = \prod_{j=2}^{l^w}{p \left(d_j^w | \mathbf{x}_w, \theta_{j-1}^w\right)} $$</code></p>
<p>其中</p>
<p><code>$$ p \left(d_j^w | \mathbf{x}_w, \theta_{j-1}^w\right) = \begin{cases} \sigma \left(\mathbf{x}_w^{\top} \theta\right) &amp; d_j^w = 0 \\ 1 - \sigma \left(\mathbf{x}_w^{\top} \theta\right) &amp; d_j^w = 1 \end{cases} $$</code></p>
<p>或表示为：</p>
<p><code>$$ p \left(d_j^w | \mathbf{x}_w, \theta_{j-1}^w\right) = \left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{1 - d_j^w} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{d_j^w} $$</code></p>
<p>则对数似然函数为：</p>
<p><code>$$ \begin{equation} \begin{split} \mathcal{L} &amp;= \sum_{w \in \mathcal{C}}{\log \prod_{j=2}^{l^w}{\left\{\left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{1 - d_j^w} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{d_j^w}\right\}}} \\ &amp;= \sum_{w \in \mathcal{C}}{\sum_{j=2}^{l^w}{\left\{\left(1 - d_j^w\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] + d_j^w \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right]\right\}}} \end{split} \end{equation} $$</code></p>
<p>记上式花括号中的内容为 <code>$\mathcal{L} \left(w, j\right)$</code>，则 <code>$\mathcal{L} \left(w, j\right)$</code> 关于 <code>$\theta_{j-1}^w$</code> 的梯度为：</p>
<p><code>$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, j\right)}{\partial \theta_{j-1}^w} &amp;= \dfrac{\partial}{\partial \theta_{j-1}^w} \left\{\left(1 - d_j^w\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] + d_j^w \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right]\right\} \\ &amp;= \left(1 - d_j^w\right) \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \mathbf{x}_w - d_j^w \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right) \mathbf{x}_w \\ &amp;= \left\{\left(1 - d_j^w\right) \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] - d_j^w \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right\} \mathbf{x}_w \\ &amp;= \left[1 - d_j^w - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \mathbf{x}_w \end{split} \end{equation} $$</code></p>
<p>则 <code>$\theta_{j-1}^w$</code> 的更新方式为：</p>
<p><code>$$ \theta_{j-1}^w \gets \theta_{j-1}^w + \eta \left[1 - d_j^w - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \mathbf{x}_w $$</code></p>
<p>同理可得，<code>$\mathcal{L} \left(w, j\right)$</code> 关于 <code>$\mathbf{x}_w$</code> 的梯度为：</p>
<p><code>$$ \dfrac{\partial \mathcal{L} \left(w, j\right)}{\partial \mathbf{x}_w} = \left[1 - d_j^w - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \theta_{j-1}^w $$</code></p>
<p>但 <code>$\mathbf{x}_w$</code> 为上下文词汇向量的加和，Word2Vec 的做法是将梯度贡献到上下文中的每个词向量上，即：</p>
<p><code>$$ \mathbf{v} \left(u\right) \gets \mathbf{v} \left(u\right) + \eta \sum_{j=2}^{l^w}{\dfrac{\partial \mathcal{L} \left(w, j\right)}{\partial \mathbf{x}_w}}, u \in Context \left(w\right) $$</code></p>
<p>基于 Hierarchical Softmax 的 CBOW 模型的随机梯度上升算法伪代码如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{基于 Hierarchical Softmax 的 CBOW 随机梯度上升算法}
\begin{algorithmic}
\STATE $\mathbf{e} = 0$
\STATE $\mathbf{x}_w = \sum_{u \in Context \left(w\right)}{\mathbf{v} \left(u\right)}$
\FOR{$j = 2, 3, ..., l^w$}
    \STATE $q = \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)$
    \STATE $g = \eta \left(1 - d_j^w - q\right)$
    \STATE $\mathbf{e} \gets \mathbf{e} + g \theta_{j-1}^w$
    \STATE $\theta_{j-1}^w \gets \theta_{j-1}^w + g \mathbf{x}_w$
\ENDFOR
\FOR{$u \in Context \left(w\right)$}
    \STATE $\mathbf{v} \left(u\right) \gets \mathbf{v} \left(u\right) + \mathbf{e}$
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p><strong>基于 Hierarchical Softmax 的 Skip-gram 模型如下</strong>：</p>
<p><img src="/images/cn/2018-10-01-word-embeddings/hierarchical-softmax-skipgram.png" alt="Hierarchical-Softmax-Skipgram"></p>
<p>对于 Skip-gram 模型，是利用当前词 <code>$w$</code> 对上下文 <code>$Context \left(w\right)$</code> 中的词进行预测，则条件概率为：</p>
<p><code>$$ p \left(Context \left(w\right) | w\right) = \prod_{u \in Context \left(w\right)}{p \left(u | w\right)} $$</code></p>
<p>类似于 CBOW 模型的思想，有：</p>
<p><code>$$ p \left(u | w\right) = \prod_{j=2}^{l^u}{p \left(d_j^u | \mathbf{v} \left(w\right), \theta_{j-1}^u\right)} $$</code></p>
<p>其中</p>
<p><code>$$ p \left(d_j^u | \mathbf{v} \left(w\right), \theta_{j-1}^u\right) = \left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^u\right)\right]^{1 - d_j^u} \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^u\right)\right]^{d_j^u} $$</code></p>
<p>可得对数似然函数为：</p>
<p><code>$$ \begin{equation} \begin{split} \mathcal{L} &amp;= \sum_{w \in \mathcal{C}}{\log \prod_{u \in Context \left(w\right)}{\prod_{j=2}^{l^u}{\left\{\left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right]^{1 - d_j^u} \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^u\right)\right]^{d_j^u}\right\}}}} \\ &amp;= \sum_{w \in \mathcal{C}}{\sum_{u \in Context \left(w\right)}{\sum_{j=2}^{l^u}{\left\{\left(1 - d_j^u\right) \cdot \log \left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] + d_j^u \cdot \log \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right]\right\}}}} \end{split} \end{equation} $$</code></p>
<p>记上式花括号中的内容为 <code>$\mathcal{L} \left(w, u, j\right)$</code>，在 <code>$\mathcal{L} \left(w, u, j\right)$</code> 关于 <code>$\theta_{j-1}^u$</code> 的梯度为：</p>
<p><code>$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, u, j\right)}{\partial \theta_{j-1}^{u}} &amp;= \dfrac{\partial}{\partial \theta_{j-1}^{u}} \left\{\left(1 - d_j^u\right) \cdot \log \left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] + d_j^u \cdot \log \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right]\right\} \\ &amp;= \left(1 - d_j^u\right) \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \mathbf{v} \left(w\right) - d_j^u \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right) \mathbf{v} \left(w\right) \\ &amp;= \left\{\left(1 - d_j^u\right) \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] - d_j^u \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right\} \mathbf{v} \left(w\right) \\ &amp;= \left[1 - d_j^u - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \mathbf{v} \left(w\right) \end{split} \end{equation} $$</code></p>
<p>则 <code>$\theta_{j-1}^u$</code> 的更新方式为：</p>
<p><code>$$ \theta_{j-1}^u \gets \theta_{j-1}^u + \eta \left[1 - d_j^u - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \mathbf{v} \left(w\right) $$</code></p>
<p>同理可得，<code>$\mathcal{L} \left(w, u, j\right)$</code> 关于 <code>$\mathbf{v} \left(w\right)$</code> 的梯度为：</p>
<p><code>$$ \dfrac{\partial \mathcal{L} \left(w, u, j\right)}{\partial \mathbf{v} \left(w\right)} = \left[1 - d_j^u - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \theta_{j-1}^u $$</code></p>
<p>则 <code>$\mathbf{v} \left(w\right)$</code> 的更新方式为：</p>
<p><code>$$ \mathbf{v} \left(w\right) \gets \mathbf{v} \left(w\right) + \eta \sum_{u \in Context \left(w\right)}{\sum_{j=2}^{l^u}{\dfrac{\partial \mathcal{L} \left(w, u, j\right)}{\partial \mathbf{v} \left(w\right)}}} $$</code></p>
<p>基于 Hierarchical Softmax 的 Skip-gram 模型的随机梯度上升算法伪代码如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{基于 Hierarchical Softmax 的 Skig-gram 随机梯度上升算法}
\begin{algorithmic}
\STATE $\mathbf{e} = 0$
\FOR{$u \in Context \left(w\right)$}
    \FOR{$j = 2, 3, ..., l^u$}
        \STATE $q = \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^u\right)$
        \STATE $g = \eta \left(1 - d_j^u - q\right)$
        \STATE $\mathbf{e} \gets \mathbf{e} + g \theta_{j-1}^u$
        \STATE $\theta_{j-1}^u \gets \theta_{j-1}^u + g \mathbf{v} \left(w\right)$
    \ENDFOR
\ENDFOR
\STATE $\mathbf{v} \left(w\right) \gets \mathbf{v} \left(w\right) + \mathbf{e}$
\end{algorithmic}
\end{algorithm}
</pre></div>

<ul>
<li><strong>基于 Negative Sampling 的模型</strong></li>
</ul>
<p>基于 Negative Sampling (NEG) 的模型相比于基于 Hierarchical Softmax 的模型不再使用复杂的 Huffman 树，而是使用简单的<strong>随机负采样</strong>，从而大幅的提高了模型的性能。</p>
<p><strong>基于 Negative Sampling 的 CBOW 模型如下</strong>：</p>
<p>对于基于 Negative Sampling  CBOW 模型，已知词 <code>$w$</code> 的上下文 <code>$Context \left(w\right)$</code>，预测词 <code>$w$</code>，则词 <code>$w$</code> 即为一个<strong>正样本</strong>，其他词则为<strong>负样本</strong>。对于一个给定 <code>$Context \left(w\right)$</code> 的负样本集合 <code>$NEG \left(w\right) \neq \varnothing$</code>，词典中的任意词 <code>$\forall \tilde{w} \in \mathcal{D}$</code>，其样本的标签定义为：</p>
<p><code>$$ L^w \left(\tilde{w}\right) =  \begin{cases} 1, &amp; \tilde{w} = w \\ 0, &amp; \tilde{w} \neq w \end{cases} $$</code></p>
<p>则对于一个正样本 <code>$\left(Context, \left(w\right)\right)$</code>，我们希望最大化：</p>
<p><code>$$ g \left(w\right) = \prod_{u \in \left\{w\right\} \cup NEG \left(w\right)}{p \left(u | Context \left(w\right)\right)} $$</code></p>
<p>或表示为：</p>
<p><code>$$ p \left(u | Context \left(w\right)\right) = \left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{L^w \left(w\right)} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{1 - L^w \left(w\right)} $$</code></p>
<p>即增大正样本概率的同时减少负样本的概率。对于一个给定的语料库 <code>$\mathcal{C}$</code>，对数似然函数为：</p>
<p><code>$$ \begin{equation} \begin{split} \mathcal{L} &amp;= \sum_{w \in \mathcal{C}}{\log g \left(w\right)} \\ &amp;= \sum_{w \in \mathcal{C}}{\log \prod_{u \in \left\{w\right\} \cup NEG \left(w\right)}{\left\{\left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{L^w \left(u\right)} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{1 - L^w \left(u\right)}\right\}}} \\ &amp;= \sum_{w \in \mathcal{C}}{\sum_{u \in \left\{w\right\} \cup NEG \left(w\right)}{\left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]\right\}}} \end{split} \end{equation} $$</code></p>
<p>记上式花括号中的内容为 <code>$\mathcal{L} \left(w, u\right)$</code>，则 <code>$\mathcal{L} \left(w, u\right)$</code> 关于 <code>$\theta^u$</code> 的梯度为：</p>
<p><code>$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, u\right)}{\partial \theta^u} &amp;= \dfrac{\partial}{\partial \theta^u} \left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]\right\} \\ &amp;= L^w \left(u\right) \left[1 - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \mathbf{x}_w - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{x}_w^{\top} \theta^u\right) \mathbf{x}_w \\ &amp;= \left\{L^w \left(u\right) \left[1 - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right\} \mathbf{x}_w \\ &amp;= \left[L^w \left(u\right) - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \mathbf{x}_w \end{split} \end{equation} $$</code></p>
<p>则 <code>$\theta^u$</code> 的更新方式为：</p>
<p><code>$$ \theta^u \gets \theta^u + \eta \left[L^w \left(u\right) - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \mathbf{x}_w $$</code></p>
<p>同理可得，<code>$\mathcal{L} \left(w, u\right)$</code> 关于 <code>$\mathbf{x}_w$</code> 的梯度为：</p>
<p><code>$$ \dfrac{\partial \mathcal{L} \left(w, u\right)}{\partial \mathbf{x}_w} = \left[L^w \left(u\right) - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \theta^u $$</code></p>
<p>则 <code>$\mathbf{v} \left(\tilde{w}\right), \tilde{w} \in Context \left(w\right)$</code> 的更新方式为：</p>
<p><code>$$ \mathbf{v} \left(\tilde{w}\right) \gets \mathbf{v} \left(\tilde{w}\right) + \eta \sum_{u \in \left\{w\right\} \cup NEG \left(w\right)}{\dfrac{\partial \mathcal{L} \left(w, u\right)}{\partial \mathbf{x}_w}}, \tilde{w} \in Context \left(w\right) $$</code></p>
<p>基于 Negative Sampling 的 CBOW 模型的随机梯度上升算法伪代码如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{基于 Negative Sampling 的 CBOW 随机梯度上升算法}
\begin{algorithmic}
\STATE $\mathbf{e} = 0$
\STATE $\mathbf{x}_w = \sum_{u \in Context \left(w\right)}{\mathbf{v} \left(u\right)}$
\FOR{$u \in Context \left\{w\right\} \cup NEG \left(w\right)$}
    \STATE $q = \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)$
    \STATE $g = \eta \left(L^w \left(u\right) - q\right)$
    \STATE $\mathbf{e} \gets \mathbf{e} + g \theta^u$
    \STATE $\theta^u \gets \theta^u + g \mathbf{x}_w$
\ENDFOR
\FOR{$u \in Context \left(w\right)$}
    \STATE $\mathbf{v} \left(u\right) \gets \mathbf{v} \left(u\right) + \mathbf{e}$
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p><strong>基于 Negative Sampling 的 Skip-gram 模型如下</strong>：</p>
<p>对于 Skip-gram 模型，利用当前词 <code>$w$</code> 对上下文 <code>$Context \left(w\right)$</code> 中的词进行预测，则对于一个正样本 <code>$\left(Context, \left(w\right)\right)$</code>，我们希望最大化：</p>
<p><code>$$ g \left(w\right) = \prod_{\tilde{w} \in Context \left(w\right)}{\prod_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{p \left(u | \tilde{w}\right)}} $$</code></p>
<p>其中，<code>$NEG^{\tilde{w}} \left(w\right)$</code> 为处理词 <code>$\tilde{w}$</code> 时生成的负样本集合，且：</p>
<p><code>$$ p \left(u | \tilde{w}\right) =  \begin{cases} \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right) &amp; L^w \left(u\right) = 1 \\ 1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right) &amp; L^w \left(u\right) = 0 \end{cases} $$</code></p>
<p>或表示为：</p>
<p><code>$$ p \left(u | \tilde{w}\right) = \left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{L^w \left(u\right)} \cdot \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{1 - L^w \left(u\right)} $$</code></p>
<p>对于一个给定的语料库 <code>$\mathcal{C}$</code>，对数似然函数为：</p>
<p><code>$$ \begin{equation} \begin{split} \mathcal{L} &amp;= \sum_{w \in \mathcal{C}}{\log g \left(w\right)} \\ &amp;= \sum_{w \in \mathcal{C}}{\log \prod_{\tilde{w} \in Context \left(w\right)}{\prod_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{\left\{\left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{L^w \left(u\right)} \cdot \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{1 - L^w \left(u\right)}\right\}}}} \\ &amp;= \sum_{w \in \mathcal{C}}{\sum_{\tilde{w} \in Context \left(w\right)}{\sum_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{\left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]\right\}}}} \end{split} \end{equation} $$</code></p>
<p>记上式花括号中的内容为 <code>$\mathcal{L} \left(w, \tilde{w}, u\right)$</code>，则 <code>$\mathcal{L} \left(w, \tilde{w}, u\right)$</code> 关于 <code>$\theta^u$</code> 的梯度为：</p>
<p><code>$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, \tilde{w}, u\right)}{\partial \theta^u} &amp;= \dfrac{\partial}{\partial \theta^u} \left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]\right\} \\ &amp;= L^w \left(u\right) \left[1 - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \mathbf{v} \left(\tilde{w}\right) - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right) \mathbf{v} \left(\tilde{w}\right) \\ &amp;= \left\{L^w \left(u\right) \left[1 - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right\} \mathbf{v} \left(\tilde{w}\right) \\ &amp;= \left[L^w \left(u\right) - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \mathbf{v} \left(\tilde{w}\right) \end{split} \end{equation} $$</code></p>
<p>则 <code>$\theta^u$</code> 的更新方式为：</p>
<p><code>$$ \theta^u \gets \theta^u + \eta \left[L^w \left(u\right) - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \mathbf{v} \left(\tilde{w}\right) $$</code></p>
<p>同理可得，<code>$\mathcal{L} \left(w, \tilde{w}, u\right)$</code> 关于 <code>$\mathbf{v} \left(\tilde{w}\right)$</code> 的梯度为：</p>
<p><code>$$ \dfrac{\partial \mathcal{L} \left(w, \tilde{w}, u\right)}{\partial \mathbf{v} \left(\tilde{w}\right)} = \left[L^w \left(u\right) - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \theta^u $$</code></p>
<p>则 <code>$\mathbf{v} \left(\tilde{w}\right)$</code> 的更新方式为：</p>
<p><code>$$ \mathbf{v} \left(\tilde{w}\right) \gets \mathbf{v} \left(\tilde{w}\right) + \eta \sum_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{\dfrac{\partial \mathcal{L} \left(w, \tilde{w}, u\right)}{\partial \mathbf{v} \left(\tilde{w}\right)}} $$</code></p>
<p>基于 Negative Sampling 的 Skig-gram 模型的随机梯度上升算法伪代码如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{基于 Negative Sampling 的 Skig-gram 随机梯度上升算法}
\begin{algorithmic}
\STATE $\mathbf{e} = 0$
\FOR{$\tilde{w} \in Context \left(w\right)$}
    \FOR{$u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)$}
        \STATE $q = \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)$
        \STATE $g = \eta \left(L^w \left(u\right) - q\right)$
        \STATE $\mathbf{e} \gets \mathbf{e} + g \theta^u$
        \STATE $\theta^u \gets \theta^u + g \mathbf{v} \left(\tilde{w}\right)$
    \ENDFOR
\ENDFOR
\STATE $\mathbf{v} \left(\tilde{w}\right) \gets \mathbf{v} \left(\tilde{w}\right) + \mathbf{e}$
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>无论是基于 Negative Sampling 的 CBOW 模型还是 Skip-gram 模型，我们都需要对于给定的词 <code>$w$</code> 生成 <code>$NEG \left(w\right)$</code>，对于一个词典 <code>$\mathcal{D}$</code> 和给定的语料 <code>$\mathcal{C}$</code>，一个词被选择中的概率为：</p>
<p><code>$$ p_{NEG} \left(w\right) = \dfrac{\#w}{\sum_{u \in \mathcal{D}}{\#u}} $$</code></p>
<p>其中 <code>$\#w$</code> 和 <code>$\#u$</code> 表示词 <code>$w$</code> 和 <code>$u$</code> 在语料 <code>$\mathcal{C}$</code> 中出现的频次。在 Word2Vec 的 C 代码中 <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>，并没有使用词的原始频次，而是对其做了 0.75 次幂，即：</p>
<p><code>$$ p_{NEG} \left(w\right) = \dfrac{\left(\#w\right)^{0.75}}{\sum_{u \in \mathcal{D}}{\left(\#u\right)^{0.75}}} $$</code></p>
<div class="blockquote" style='border-left: 4px solid #369BE5;'>本节内容参考了 licstar 的 <a href="http://licstar.net/archives/328">博客</a> 和 peghoty 的 <a href="https://www.cnblogs.com/peghoty/p/3857839.html">博客</a>。</div>
<h2 id="其他-embedding-方法">其他 Embedding 方法</h2>
<h3 id="glove">GloVe</h3>
<p>GloVe (Global Vector 的简写) 是由 Pennington 等人 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup> 提出了一种词向量生成方法，该方法利用了语料的全局统计信息。</p>
<p>令 <code>$X$</code> 表示词与词之间的共现矩阵，<code>$X_{ij}$</code> 表示词 <code>$j$</code> 在词 <code>$i$</code> 为上下文的情况下出现的频次。则 <code>$X_i = \sum_{k}{X_{ik}}$</code> 表示在词<code>$i$</code> 为上下文的情况任意词出现的总次数。令 <code>$P_{ij} = P \left(j | i\right) = X_{ij} / X_i$</code> 表示词 <code>$j$</code> 在词 <code>$i$</code> 出现前提下出现的条件概率。</p>
<p>例如，我们令 <code>$i = ice, j = steam$</code>，则这两个词之间的关系可以利用同其他词 <code>$k$</code> 共现概率的比率学习得出。则有：</p>
<ol>
<li>与词 <code>ice</code> 相关，但与词 <code>steam</code> 不太相关，例如 <code>$k = solid$</code>，则比率 <code>$P_{ik} / P_{jk}$</code> 应该较大；类似的当词 <code>$k$</code> 与 <code>steam</code> 相关，但与词 <code>ice</code> 不太相关，则比率 <code>$P_{ik} / P_{jk}$</code> 应该较小。</li>
<li>当与词 <code>ice</code> 和词 <code>steam</code> 均相关或者均不太相关时，例如 <code>$k = water$</code> 或 <code>$k = fashion$</code>，则比率 <code>$P_{ik} / P_{jk}$</code> 应该和 1 接近。</li>
</ol>
<p>下表展示了在一个大量语料上的概率及其比率：</p>
<table>
  <thead>
      <tr>
          <th>概率和比例</th>
          <th><code>$k = solid$</code></th>
          <th><code>$k = gas$</code></th>
          <th><code>$k = water$</code></th>
          <th><code>$k = fashion$</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>$P \left(k \vert ice\right)$</code></td>
          <td><code>$1.9 \times 10^{-4}$</code></td>
          <td><code>$6.6 \times 10^{-5}$</code></td>
          <td><code>$3.0 \times 10^{-3}$</code></td>
          <td><code>$1.7 \times 10^{-5}$</code></td>
      </tr>
      <tr>
          <td><code>$P \left(k \vert steam\right)$</code></td>
          <td><code>$2.2 \times 10^{-5}$</code></td>
          <td><code>$7.8 \times 10^{-4}$</code></td>
          <td><code>$2.2 \times 10^{-3}$</code></td>
          <td><code>$1.8 \times 10^{-5}$</code></td>
      </tr>
      <tr>
          <td><code>$P \left(k \vert ice\right) / P \left(k \vert steam\right)$</code></td>
          <td><code>$8.9$</code></td>
          <td><code>$8.5 \times 10^{-2}$</code></td>
          <td><code>$1.36$</code></td>
          <td><code>$0.96$</code></td>
      </tr>
  </tbody>
</table>
<p>根据如上的假设，我们可以得到一个最基础的模型：</p>
<p><code>$$ F \left(w_i, w_j, \tilde{w}_k\right) = \dfrac{P_{ik}}{P_{jk}} $$</code></p>
<p>其中 <code>$w \in \mathbb{R}^d$</code> 为词向量，<code>$\tilde{w}_k \in \mathbb{R}^d$</code> 为单独的上下文词的词向量。假设向量空间是一个线性结构，因此 <code>$F$</code> 仅依赖于两个向量之间的差异，则模型可以改写为：</p>
<p><code>$$ F \left(w_i - w_j, \tilde{w}_k\right) = \dfrac{P_{ik}}{P_{jk}} $$</code></p>
<p>上式中右面是一个标量，如果左面的参数利用一个复杂的模型进行计算，例如神经网络，则会破坏我们希望保留的线性结构。因此，我们对参数采用点积运算，即：</p>
<p><code>$$ F \left(\left(w_i - w_j\right)^{\top} \tilde{w}_k\right) = \dfrac{P_{ik}}{P_{jk}} $$</code></p>
<p>在词之间的共现矩阵中，一个词和其上下文中的一个词之间应该是可以互换角色的。首先我们要保证 <code>$F$</code> 在 <code>$\left(\mathbb{R}, +\right)$</code> 和 <code>$\left(\mathbb{R}_{&gt;0}, \times\right)$</code> 上是同态的 (homomorphism)，例如：</p>
<p><code>$$ F \left(\left(w_i - w_j\right)^{\top} \tilde{w}_k\right) = \dfrac{F \left(w_i^{\top} \tilde{w}_k\right)}{F \left(w_j^{\top} \tilde{w}_k\right)} $$</code></p>
<p>其中 <code>$F \left(w_i^{\top} \tilde{w}_k\right) = P_{ik} = \dfrac{X_{ik}}{X_i}$</code>，则上式的一个解为 <code>$F = \exp$</code>，或：</p>
<p><code>$$ w_i^{\top} \tilde{w}_k = \log \left(P_{ik}\right) = \log \left(X_{ik}\right) - \log \left(X_i\right) $$</code></p>
<p>其中 <code>$\log \left(X_i\right)$</code> 与 <code>$k$</code> 无关记为 <code>$b_i$</code>，同时为了对称性添加 <code>$\tilde{b}_k$</code>，则上式改写为：</p>
<p><code>$$ w_i^{\top} \tilde{w}_k + b_i + \tilde{b}_k = \log \left(X_{ik}\right) $$</code></p>
<p>上式中，左侧为词向量的相关运算，右侧为共现矩阵的常量信息，则给出模型的损失函数如下：</p>
<p><code>$$ J = \sum_{i,j=1}^{V}{f \left(X_{ij}\right) \left(w_i^{\top} \tilde{w}_k + b_i + \tilde{b}_k - \log X_{ij}\right)^2} $$</code></p>
<p>其中，<code>$V$</code> 为词典中词的个数，<code>$f$</code> 为一个权重函数，其应具有如下特点：</p>
<ol>
<li><code>$f \left(0\right) = 0$</code>。如果 <code>$f$</code> 为一个连续函数，则当 <code>$x \to 0$</code> 时 <code>$\lim_{x \to 0}{f \left(x\right) \log^2 x}$</code> 应足够快地趋近于无穷。</li>
<li><code>$f \left(x\right)$</code> 应为非减函数，以确保稀少的共现不会权重过大。</li>
<li><code>$f \left(x\right)$</code> 对于较大的 <code>$x$</code> 应该相对较小，以确保过大的共现不会权重过大。</li>
</ol>
<p>文中给出了一个符合要求的函数如下：</p>
<p><code>$$ f \left(x\right) =  \begin{cases} \left(x / x_{\max}\right)^{\alpha} &amp; \text{if} \  x &lt; x_{\max} \\ 1 &amp; \text{otherwise} \end{cases} $$</code></p>
<p>其中两个超参数的值建议为 <code>$x_{\max} = 100, \alpha = 0.75$</code>。</p>
<h3 id="fasttext">fastText</h3>
<p>fastText 是由 Bojanowski 和 Grave 等人 <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> 提出的一种词向量表示方法。原始的 Skip-gram 模型忽略了词语内部的结构信息，fastText 利用 N-gram 方法将其考虑在内。</p>
<p>对于一个词 <code>$w$</code>，利用一系列的 N-gram 进行表示，同时在词的前后添加 <code>&lt;</code> 和 <code>&gt;</code> 边界符号以同其他文本序列进行区分。同时还将词语本身也包含在这个 N-gram 集合中，从而学习到词语的向量表示。例如，对于词 <code>$where$</code> 和 <code>$n = 3$</code>，则 N-gram 集合为：<code>&lt;wh, whe, her, ere, re&gt;</code>，同时包含词本身 <code>&lt;where&gt;</code>。需要注意的是，序列 <code>&lt;her&gt;</code> 与词 <code>$where$</code> 中的 tri-gram <code>her</code> 是两个不同的概念。模型提取所有 <code>$3 \leq n \leq 6$</code> 的 N-gram 序列。</p>
<p>假设 N-gram 词典的大小为 <code>$G$</code>，对于一个词 <code>$w$</code>，<code>$\mathcal{G}_w \subset \left\{1, ..., G\right\}$</code> 表示词中出现的 N-gram 的集合。针对任意一个 N-gram <code>$g$</code>，用向量 <code>$\mathbf{z}_g$</code> 表示，则我们利用一个词的所有 N-gram 的向量的加和表示该词。可以得到该模型的评分函数为：</p>
<p><code>$$ s \left(w, c\right) = \sum_{g \in \mathcal{G}_w}{\mathbf{z}_g^{\top} \mathbf{v}_c} $$</code></p>
<p>模型在学习不同词向量时可以共享权重 (不同词的可能包含相同的 N-gram)，使得在学习低频词时也可得到可靠的向量表示。</p>
<h3 id="wordrank">WordRank</h3>
<p>WordRank 是由 Ji 等人 <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup> 提出的一种词向量表示方法，其将词向量学习问题转换成一个排序问题。</p>
<p>我们令 <code>$\mathbf{u}_w$</code> 表示当前词 <code>$w$</code> 的 <code>$k$</code> 维词向量，<code>$\mathbf{v}_c$</code> 表示当前词上下文 <code>$c$</code> 的词向量。通过两者的内积 <code>$\langle \mathbf{u}_w, \mathbf{v}_c \rangle$</code> 来捕获词 <code>$w$</code> 和上下文 <code>$c$</code> 之间的关系，两者越相关则该内积越大。对于一个给定的词 <code>$w$</code>，利用上下文集合 <code>$\mathcal{C}$</code> 同词的内积分数进行排序，对于一个给定的上下文 <code>$c$</code>，排序为：</p>
<p><code>$$ \begin{equation} \begin{split} \text{rank} \left(w, c\right) &amp;= \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{I \left(\langle \mathbf{u}_w, \mathbf{v}_c \rangle - \langle \mathbf{u}_w, \mathbf{v}_{c'} \rangle \leq 0\right)} \\ &amp;= \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{I \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'}  \rangle \leq 0\right)} \end{split} \end{equation} $$</code></p>
<p>其中，<code>$I \left(x \leq 0\right)$</code> 为一个 0-1 损失函数，当 <code>$x \leq 0$</code> 时为 1 其他情况为 0。由于 <code>$I \left(x \leq 0\right)$</code> 为一个非连续函数，因此我们可以将其替换为一个凸上限函数 <code>$\ell \left(\cdot\right)$</code>，其可以为任意的二分类损失函数，构建排序的凸上限如下：</p>
<p><code>$$ \text{rank} \left(w, c\right) \leq \overline{\text{rank}} \left(w, c\right) = \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{\ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right)} $$</code></p>
<p>我们期望排序模型将更相关的上下文排在列表的顶部，基于此构建损失函数如下：</p>
<p><code>$$ J \left(\mathbf{U}, \mathbf{V}\right) := \sum_{w \in \mathcal{W}}{\sum_{c \in \Omega_w}{r_{w, c} \cdot \rho \left(\dfrac{\overline{\text{rank}} \left(w, c\right) + \beta}{\alpha}\right)}} $$</code></p>
<p>其中，<code>$\mathcal{W}$</code> 表示词典，<code>$\mathbf{U} := \left\{\mathbf{u}_w\right\}_{w \in \mathcal{W}}$</code> 和 <code>$\mathbf{V} := \left\{\mathbf{c}_w\right\}_{c \in \mathcal{C}}$</code> 分别表示词及其上下文词向量的参数，<code>$\Omega_w$</code> 表示与词 <code>$w$</code> 共现的上下文的集合，<code>$r_{w, c}$</code> 为衡量 <code>$w$</code> 和 <code>$c$</code> 之间关系的权重，<code>$\rho \left(\cdot\right)$</code> 为用于衡量排序好坏的单调递增的损失函数，<code>$\alpha \geq 0, \beta \geq 0$</code> 为超参数。可选的有：</p>
<p><code>$$ r_{w, c} = \begin{cases} \left(X_{w, c} / x_{\max}\right)^{\epsilon} &amp; \text{if} \ X_{w, c} &lt; x_{\max} \\ 1 &amp; \text{otherwise} \end{cases} $$</code></p>
<p>其中 <code>$x_{\max} = 100, \epsilon = 0.75$</code>。根据 <code>$\rho \left(\cdot\right)$</code> 的要求，损失函数在排序的顶部 (rank 值小) 的地方更加敏感，同时对于 rank 值较大的地方不敏感。这可以使得模型变得更加稳健 (避免语法错误和语言的非常规使用造成干扰)，因此可选的有：</p>
<p><code>$$ \begin{equation} \begin{split} \rho \left(x\right) &amp;:= \log_2 \left(1 + x\right) \\ \rho \left(x\right) &amp;:= 1 - \dfrac{1}{\log_2 \left(2 + x\right)} \\ \rho \left(x\right) &amp;:= \dfrac{x^{1 - t} - 1}{1 - t}, t \neq 1 \end{split} \end{equation} $$</code></p>
<p>损失函数可以等价的定义为：</p>
<p><code>$$ J \left(\mathbf{U}, \mathbf{V}\right) := \sum_{\left(w, c\right) \in \Omega}{r_{w, c} \cdot \rho \left(\dfrac{\overline{\text{rank}} \left(w, c\right) + \beta}{\alpha}\right)} $$</code></p>
<p>在训练过程中，外层的求和符号容易利用 SDG 算法解决，但对于内层的求和符号除非 <code>$\rho \left(\cdot\right)$</code> 是一个线性函数，否则难以求解。然而，<code>$\rho \left(\cdot\right)$</code> 函数的性质要求其不能是一个线性函数，但我们可以利用其凹函数的特性对其进行一阶泰勒分解，有：</p>
<p><code>$$ \rho \left(x\right) \leq \rho \left(\xi^{-1}\right) + \rho' \left(\xi^{-1}\right) \cdot \left(x - \xi^{-1}\right) $$</code></p>
<p>对于任意 <code>$x$</code> 和 <code>$\xi \neq 0$</code> 均成立，同时当且仅当 <code>$\xi = x^{-1}$</code> 时等号成立。因此，令 <code>$\Xi := \left\{\xi_{w, c}\right\}_{\left(w, c\right) \in \Sigma}$</code>，则可以得到 <code>$J \left(\mathbf{U}, \mathbf{V}\right)$</code> 的一个上界：</p>
<p><code>$$ \begin{equation} \begin{split} \overline{J} \left(\mathbf{U}, \mathbf{V}, \Xi\right) &amp;:= \sum_{\left(w, c\right)  \in \Omega}{r_{w, c} \cdot \left\{\rho \left(\xi_{wc}^{-1}\right) + \rho' \left(\xi_{wc}^{-1}\right) \cdot \left(\alpha^{-1} \beta + \alpha^{-1} \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{\ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) - \xi_{w, c}^{-1}}\right)\right\}} \\ &amp;= \sum_{\left(w, c, c'\right)}{r_{w, c} \cdot \left(\dfrac{\rho \left(\xi_{w, c}^{-1}\right) + \rho' \left(\xi_{w, c}^{-1}\right) \cdot \left(\alpha^{-1} \beta - \xi_{w, c}^{-1}\right)}{\lvert \mathcal{C} \rvert - 1} + \dfrac{1}{\alpha} \rho' \left(\xi_{w, c}^{-1}\right) \cdot \ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right)\right)} \end{split} \end{equation} $$</code></p>
<p>其中，<code>$\left(w, c, c'\right) \in \Omega \times \left(\mathcal{C} \setminus \left\{c\right\}\right)$</code>，至此我们可以通过均匀采样 <code>$\left(w, c\right) \in \Sigma$</code> 和 <code>$c' \in \mathcal{C} \setminus \left\{c\right\}$</code> 解决训练问题。</p>
<p>整个 WordRank 算法的伪代码如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{WordRank 算法}
\begin{algorithmic}
\STATE $\eta$ 为学习率
\WHILE{$\mathbf{U}$，$\mathbf{V}$ 和 $\Xi$ 未收敛}
    \STATE \COMMENT{阶段1：更新 $\mathbf{U}$ 和 $\mathbf{V}$}
    \WHILE{$\mathbf{U}$ 和 $\mathbf{V}$ 未收敛}
        \STATE 从 $\Omega$ 中均匀采样 $\left(w, c\right)$
        \STATE 从 $\mathcal{C} \setminus \left\{c\right\}$ 中均匀采样 $c'$
        \STATE \COMMENT{同时更新如下 3 个参数}
        \STATE $\mathbf{u}_w \gets \mathbf{u}_w - \eta \cdot r_{w, c} \cdot \rho' \left(\xi_{w, c}^{-1}\right) \cdot \ell' \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) \cdot \left(\mathbf{v}_c - \mathbf{v}_{c'}\right)$
        \STATE $\mathbf{v}_c \gets \mathbf{v}_c - \eta \cdot r_{w, c} \cdot \rho' \left(\xi_{w, c}^{-1}\right) \cdot \ell' \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) \cdot \mathbf{u}_w$
        \STATE $\mathbf{v}_{c'} \gets \mathbf{v}_{c'} - \eta \cdot r_{w, c} \cdot \rho' \left(\xi_{w, c}^{-1}\right) \cdot \ell' \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) \cdot \mathbf{u}_w$
    \ENDWHILE
    \STATE \COMMENT{阶段2：更新 $\Xi$}
    \FOR{$w \in \mathcal{W}$}
        \FOR{$c \in \mathcal{C}$}
            \STATE $\xi_{w, c} = \alpha / \left(\sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{\ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) + \beta}\right)$
        \ENDFOR
    \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}
</pre></div>

<h3 id="cw2vec">cw2vec</h3>
<p>cw2vec 是由 Cao 等人 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup> 提出的一种基于汉字笔画 N-gram 的中文词向量表示方法。该方法根据汉字作为象形文字具有笔画信息的特点，提出了笔画 N-gram 的概念。针对一个词的笔画 N-gram，其生成过程如下图所示：</p>
<p><img src="/images/cn/2018-10-01-word-embeddings/cw2vec-stroke-n-gram-generation.png" alt="cw2vec-Stroke-N-gram-Generation"></p>
<p>共包含 4 个步骤：</p>
<ol>
<li>将一个词拆解成单个的汉字，例如：“大人” 拆解为 “大” 和 “人”。</li>
<li>将每个汉字拆解成笔画，例如：“大” 和 “人” 拆解为 “一，丿，乀，丿，乀”。</li>
<li>将每个笔画映射到对应的编码序列，例如： “一，丿，乀，丿，乀” 映射为 13434。</li>
<li>利用编码序列生成笔画 N-gram，例如：134，343，434；1343，3434；13434。</li>
</ol>
<p>模型中定义一个词 <code>$w$</code> 及其上下文 <code>$c$</code> 的相似度如下：</p>
<p><code>$$ sim \left(w, c\right) = \sum_{q \in S\left(w\right)}{\vec{q} \cdot \vec{c}} $$</code></p>
<p>其中，<code>$S$</code> 为由笔画 N-gram 构成的词典，<code>$S \left(w\right)$</code> 为词 <code>$w$</code> 对应的笔画 N-gram 集合，<code>$q$</code> 为该集合中的一个笔画 N-gram，<code>$\vec{q}$</code> 为 <code>$q$</code> 对应的向量。</p>
<p>该模型的损失函数为：</p>
<p><code>$$ \mathcal{L} = \sum_{w \in D}{\sum_{c \in T \left(w\right)}{\log \sigma \left(sim \left(w, c\right)\right) + \lambda \mathbb{E}_{c' \sim P} \left[\log \sigma \left(- sim \left(w, c'\right)\right)\right]}} $$</code></p>
<p>其中，<code>$D$</code> 为语料中的全部词语，<code>$T \left(w\right)$</code> 为给定的词 <code>$w$</code> 和窗口内的所有上次文词，<code>$\sigma \left(x\right) = \left(1 + \exp \left(-x\right)\right)^{-1}$</code>，<code>$\lambda$</code> 为负采样的个数，<code>$\mathbb{E}_{c' \sim P} \left[\cdot\right]$</code> 表示负样本 <code>$c'$</code> 按照 <code>$D$</code> 中词的分布 <code>$P$</code> 进行采样，该分布可以为词的一元模型的分布 <code>$U$</code>，同时为了避免数据的稀疏性问题，类似 Word2Vec 中的做法采用 <code>$U^{0.75}$</code>。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Hinton, G. E. (1986, August). Learning distributed representations of concepts. In <em>Proceedings of the eighth annual conference of the cognitive science society</em> (Vol. 1, p. 12).&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Bengio, Y., Courville, A., &amp; Vincent, P. (2013). Representation learning: A review and new perspectives. <em>IEEE transactions on pattern analysis and machine intelligence</em>, 35(8), 1798-1828.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Bengio, Y., Ducharme, R., Vincent, P., &amp; Jauvin, C. (2003). A Neural Probabilistic Language Model. <em>Journal of Machine Learning Research</em>, 3(Feb), 1137–1155.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Mikolov, T., Chen, K., Corrado, G., &amp; Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. <em>arXiv preprint arXiv:1301.3781</em>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p><a href="https://zh.wikipedia.org/zh/%E9%9C%8D%E5%A4%AB%E6%9B%BC%E7%BC%96%E7%A0%81">https://zh.wikipedia.org/zh/霍夫曼编码</a>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p><a href="https://code.google.com/archive/p/word2vec/">https://code.google.com/archive/p/word2vec/</a>&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Pennington, J., Socher, R., &amp; Manning, C. (2014). Glove: Global Vectors for Word Representation. In <em>Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP)</em> (pp. 1532–1543).&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Bojanowski, P., Grave, E., Joulin, A., &amp; Mikolov, T. (2017). Enriching Word Vectors with Subword Information. <em>Transactions of the Association for Computational Linguistics</em>, 5, 135–146.&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Ji, S., Yun, H., Yanardag, P., Matsushima, S., &amp; Vishwanathan, S. V. N. (2016). WordRank: Learning Word Embeddings via Robust Ranking. In <em>Proceedings of the 2016 Conference on Empirical Methods in Natural Language Processing</em> (pp. 658–668).&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Cao, S., Lu, W., Zhou, J., &amp; Li, X. (2018). cw2vec: Learning Chinese Word Embeddings with Stroke n-gram Information. In <em>Thirty-Second AAAI Conference on Artificial Intelligence</em>.&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>循环神经网络 (Recurrent Neural Network, RNN)</title><link>https://zeqiang.fun/cn/2018/09/rnn/</link><pubDate>Fri, 21 Sep 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/09/rnn/</guid><description><![CDATA[
        <blockquote>
<p>文章部分内容参考了 Christopher 的博客 <a href="http://colah.github.io/posts/2015-08-Understanding-LSTMs/">Understanding LSTM Networks</a>，内容翻译和图片重绘已得到原作者同意，重绘后的图片源文件请参见 <a href="https://github.com/leovan/cdn.leovan.me/tree/master/images/blog/cn/2018-09-21-rnn/">这里</a>。</p>
</blockquote>
<h2 id="发展史">发展史</h2>
<p>循环神经网络 (Recurrent Neural Network, RNN) 一般是指时间递归神经网络而非结构递归神经网络 (Recursive Neural Network)，其主要用于对序列数据进行建模。Salehinejad 等人 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 的一篇综述文章列举了 RNN 发展过程中的一些重大改进，如下表所示：</p>
<table>
  <thead>
      <tr>
          <th>Year</th>
          <th>1st Author</th>
          <th>Contribution</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1990</td>
          <td>Elman</td>
          <td>Popularized simple RNNs (Elman network)</td>
      </tr>
      <tr>
          <td>1993</td>
          <td>Doya</td>
          <td>Teacher forcing for gradient descent (GD)</td>
      </tr>
      <tr>
          <td>1994</td>
          <td>Bengio</td>
          <td>Difficulty in learning long term dependencies with gradient descend</td>
      </tr>
      <tr>
          <td>1997</td>
          <td>Hochreiter</td>
          <td>LSTM: long-short term memory for vanishing gradients problem</td>
      </tr>
      <tr>
          <td>1997</td>
          <td>Schuster</td>
          <td>BRNN: Bidirectional recurrent neural networks</td>
      </tr>
      <tr>
          <td>1998</td>
          <td>LeCun</td>
          <td>Hessian matrix approach for vanishing gradients problem</td>
      </tr>
      <tr>
          <td>2000</td>
          <td>Gers</td>
          <td>Extended LSTM with forget gates</td>
      </tr>
      <tr>
          <td>2001</td>
          <td>Goodman</td>
          <td>Classes for fast Maximum entropy training</td>
      </tr>
      <tr>
          <td>2005</td>
          <td>Morin</td>
          <td>A hierarchical softmax function for language modeling using RNNs</td>
      </tr>
      <tr>
          <td>2005</td>
          <td>Graves</td>
          <td>BLSTM: Bidirectional LSTM</td>
      </tr>
      <tr>
          <td>2007</td>
          <td>Jaeger</td>
          <td>Leaky integration neurons</td>
      </tr>
      <tr>
          <td>2007</td>
          <td>Graves</td>
          <td>MDRNN: Multi-dimensional RNNs</td>
      </tr>
      <tr>
          <td>2009</td>
          <td>Graves</td>
          <td>LSTM for hand-writing recognition</td>
      </tr>
      <tr>
          <td>2010</td>
          <td>Mikolov</td>
          <td>RNN based language model</td>
      </tr>
      <tr>
          <td>2010</td>
          <td>Neir</td>
          <td>Rectified linear unit (ReLU) for vanishing gradient problem</td>
      </tr>
      <tr>
          <td>2011</td>
          <td>Martens</td>
          <td>Learning RNN with Hessian-free optimization</td>
      </tr>
      <tr>
          <td>2011</td>
          <td>Mikolov</td>
          <td>RNN by back-propagation through time (BPTT) for statistical language modeling</td>
      </tr>
      <tr>
          <td>2011</td>
          <td>Sutskever</td>
          <td>Hessian-free optimization with structural damping</td>
      </tr>
      <tr>
          <td>2011</td>
          <td>Duchi</td>
          <td>Adaptive learning rates for each weight</td>
      </tr>
      <tr>
          <td>2012</td>
          <td>Gutmann</td>
          <td>Noise-contrastive estimation (NCE)</td>
      </tr>
      <tr>
          <td>2012</td>
          <td>Mnih</td>
          <td>NCE for training neural probabilistic language models (NPLMs)</td>
      </tr>
      <tr>
          <td>2012</td>
          <td>Pascanu</td>
          <td>Avoiding exploding gradient problem by gradient clipping</td>
      </tr>
      <tr>
          <td>2013</td>
          <td>Mikolov</td>
          <td>Negative sampling instead of hierarchical softmax</td>
      </tr>
      <tr>
          <td>2013</td>
          <td>Sutskever</td>
          <td>Stochastic gradient descent (SGD) with momentum</td>
      </tr>
      <tr>
          <td>2013</td>
          <td>Graves</td>
          <td>Deep LSTM RNNs (Stacked LSTM)</td>
      </tr>
      <tr>
          <td>2014</td>
          <td>Cho</td>
          <td>Gated recurrent units</td>
      </tr>
      <tr>
          <td>2015</td>
          <td>Zaremba</td>
          <td>Dropout for reducing Overfitting</td>
      </tr>
      <tr>
          <td>2015</td>
          <td>Mikolov</td>
          <td>Structurally constrained recurrent network (SCRN) to enhance learning longer memory for vanishing gradient problem</td>
      </tr>
      <tr>
          <td>2015</td>
          <td>Visin</td>
          <td>ReNet: A RNN-based alternative to convolutional neural networks</td>
      </tr>
      <tr>
          <td>2015</td>
          <td>Gregor</td>
          <td>DRAW: Deep recurrent attentive writer</td>
      </tr>
      <tr>
          <td>2015</td>
          <td>Kalchbrenner</td>
          <td>Grid long-short term memory</td>
      </tr>
      <tr>
          <td>2015</td>
          <td>Srivastava</td>
          <td>Highway network</td>
      </tr>
      <tr>
          <td>2017</td>
          <td>Jing</td>
          <td>Gated orthogonal recurrent units</td>
      </tr>
  </tbody>
</table>
<h2 id="rnn">RNN</h2>
<h3 id="网络结构">网络结构</h3>
<p>不同于传统的前馈神经网络接受特定的输入得到输出，RNN 由人工神经元和一个或多个反馈循环构成，如下图所示：</p>
<p><img src="/images/cn/2018-09-21-rnn/rnn-loops.png" alt="RNN-Loops"></p>
<p>其中，<code>$\boldsymbol{x}_t$</code> 为输入层，<code>$\boldsymbol{h}_t$</code> 为带有循环的隐含层，<code>$\boldsymbol{y}_t$</code> 为输出层。其中隐含层包含一个循环，为了便于理解我们将循环进行展开，展开后的网络结构如下图所示：</p>
<p><img src="/images/cn/2018-09-21-rnn/rnn-loops-unrolled.png" alt="RNN-Loops-Unrolled"></p>
<p>对于展开后的网络结构，其输入为一个时间序列 <code>$\left\{\dotsc, \boldsymbol{x}_{t-1}, \boldsymbol{x}_t, \boldsymbol{x}_{t+1}, \dotsc\right\}$</code>，其中 <code>$\boldsymbol{x}_t \in \mathbb{R}^n$</code>，<code>$n$</code> 为输入层神经元个数。相应的隐含层为 <code>$\left\{\dotsc, \boldsymbol{h}_{t-1}, \boldsymbol{h}_t, \boldsymbol{h}_{t+1}, \dotsc\right\}$</code>，其中 <code>$\boldsymbol{h}_t \in \mathbb{R}^m$</code>，<code>$m$</code> 为隐含层神经元个数。隐含层节点使用较小的非零数据进行初始化可以提升整体的性能和网络的稳定性 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>。隐含层定义了整个系统的状态空间 (state space)，或称之为 memory <sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>：</p>
<p><code>$$ \boldsymbol{h}_t = f_H \left(\boldsymbol{o}_t\right) $$</code></p>
<p>其中</p>
<p><code>$$ \boldsymbol{o}_t = \boldsymbol{W}_{IH} \boldsymbol{x}_t + \boldsymbol{W}_{HH} \boldsymbol{h}_{t-1} + \boldsymbol{b}_h $$</code></p>
<p><code>$f_H \left(\cdot\right)$</code> 为隐含层的激活函数，<code>$\boldsymbol{b}_h$</code> 为隐含层的偏置向量。对应的输出层为 <code>$\left\{\dotsc, \boldsymbol{y}_{t-1}, \boldsymbol{y}_t, \boldsymbol{y}_{t+1}, \dotsc\right\}$</code>，其中 <code>$\boldsymbol{y}_t \in \mathbb{R}^p$</code>，<code>$p$</code> 为输出层神经元个数。则：</p>
<p><code>$$ \boldsymbol{y}_t = f_O \left(\boldsymbol{W}_{HO} \boldsymbol{h}_t + \boldsymbol{b}_o\right) $$</code></p>
<p>其中 <code>$f_O \left(\cdot\right)$</code> 为隐含层的激活函数，<code>$\boldsymbol{b}_o$</code> 为隐含层的偏置向量。</p>
<p>在 RNN 中常用的激活函数为双曲正切函数：</p>
<p><code>$$ \tanh \left(x\right) = \dfrac{e^{2x} - 1}{e^{2x} + 1} $$</code></p>
<p>Tanh 函数实际上是 Sigmoid 函数的缩放：</p>
<p><code>$$ \sigma \left(x\right) = \dfrac{1}{1 + e^{-x}} = \dfrac{\tanh \left(x / 2\right) + 1}{2} $$</code></p>
<h3 id="梯度弥散和梯度爆炸">梯度弥散和梯度爆炸</h3>
<p>原始 RNN 存在的严重的问题就是<strong>梯度弥散 (Vanishing Gradients)</strong> 和<strong>梯度爆炸 (Exploding Gradients)</strong>。我们以时间序列中的 3 个时间点 <code>$t = 1, 2, 3$</code> 为例进行说明，首先假设神经元在前向传导过程中没有激活函数，则有：</p>
<p><code>$$ \begin{equation} \begin{split} &amp;\boldsymbol{h}_1 = \boldsymbol{W}_{IH} \boldsymbol{x}_1 + \boldsymbol{W}_{HH} \boldsymbol{h}_0 + \boldsymbol{b}_h, &amp;\boldsymbol{y}_1 = \boldsymbol{W}_{HO} \boldsymbol{h}_1 + \boldsymbol{b}_o \\ &amp;\boldsymbol{h}_2 = \boldsymbol{W}_{IH} \boldsymbol{x}_2 + \boldsymbol{W}_{HH} \boldsymbol{h}_1 + \boldsymbol{b}_h, &amp;\boldsymbol{y}_2 = \boldsymbol{W}_{HO} \boldsymbol{h}_2 + \boldsymbol{b}_o \\ &amp;\boldsymbol{h}_3 = \boldsymbol{W}_{IH} \boldsymbol{x}_3 + \boldsymbol{W}_{HH} \boldsymbol{h}_2 + \boldsymbol{b}_h, &amp;\boldsymbol{y}_3 = \boldsymbol{W}_{HO} \boldsymbol{h}_3 + \boldsymbol{b}_o \end{split} \end{equation} $$</code></p>
<p>在对于一个序列训练的损失函数为：</p>
<p><code>$$ \mathcal{L} \left(\boldsymbol{y}, \boldsymbol{\hat{y}}\right) = \sum_{t=0}^{T}{\mathcal{L}_t \left(\boldsymbol{y_t}, \boldsymbol{\hat{y}_t}\right)} $$</code></p>
<p>其中 <code>$\mathcal{L}_t \left(\boldsymbol{y_t}, \boldsymbol{\hat{y}_t}\right)$</code> 为 <code>$t$</code> 时刻的损失。我们利用 <code>$t = 3$</code> 时刻的损失对 <code>$\boldsymbol{W}_{IH}, \boldsymbol{W}_{HH}, \boldsymbol{W}_{HO}$</code> 求偏导，有：</p>
<p><code>$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{W}_{HO}} &amp;= \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{W}_{HO}} \\ \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{W}_{IH}} &amp;= \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{W}_{IH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{W}_{IH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{h}_1} \dfrac{\partial \boldsymbol{h}_1}{\partial \boldsymbol{W}_{IH}} \\ \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{W}_{HH}} &amp;= \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{W}_{HH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{W}_{HH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{h}_1} \dfrac{\partial \boldsymbol{h}_1}{\partial \boldsymbol{W}_{HH}} \end{split} \end{equation} $$</code></p>
<p>因此，不难得出对于任意时刻 <code>$t$</code>，<code>$\boldsymbol{W}_{IH}, \boldsymbol{W}_{HH}$</code> 的偏导为：</p>
<p><code>$$ \dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{W}_{IH}} = \sum_{k=0}^{t}{\dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{y}_t} \dfrac{\partial \boldsymbol{y}_t}{\partial \boldsymbol{h}_t} \left(\prod_{j=k+1}^{t}{\dfrac{\partial \boldsymbol{h}_j}{\partial \boldsymbol{h}_{j-1}}}\right) \dfrac{\partial \boldsymbol{h}_k}{\partial \boldsymbol{W}_{IH}}} $$</code></p>
<p><code>$\dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{W}_{HH}}$</code> 同理可得。对于 <code>$\dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{W}_{HH}}$</code>，在存在激活函数的情况下，有：</p>
<p><code>$$ \prod_{j=k+1}^{t}{\dfrac{\partial \boldsymbol{h}_j}{\partial \boldsymbol{h}_{j-1}}} = \prod_{j=k+1}^{t}{f'_H \left(h_{j-1}\right) \boldsymbol{W}_{HH}} $$</code></p>
<p>假设激活函数为 <code>$\tanh$</code>，下图刻画了 <code>$\tanh$</code> 函数及其导数的函数取值范围：</p>
<p><img src="/images/cn/2018-09-21-rnn/tanh-function.png" alt="Tanh-Function"></p>
<p>可得，<code>$0 \leq \tanh' \leq 1$</code>，同时当且仅当 <code>$x = 0$</code> 时，<code>$\tanh' \left(x\right) = 1$</code>。因此：</p>
<ol>
<li>当 <code>$t$</code> 较大时，<code>$\prod_{j=k+1}^{t}{f'_H \left(h_{j-1}\right) \boldsymbol{W}_{HH}}$</code> 趋近于 0，则会产生<strong>梯度弥散</strong>问题。</li>
<li>当 <code>$\boldsymbol{W}_{HH}$</code> 较大时，<code>$\prod_{j=k+1}^{t}{f'_H \left(h_{j-1}\right) \boldsymbol{W}_{HH}}$</code> 趋近于无穷，则会产生<strong>梯度爆炸</strong>问题。</li>
</ol>
<h3 id="长期依赖问题">长期依赖问题</h3>
<p>RNN 隐藏节点以循环结构形成记忆，每一时刻的隐藏层的状态取决于它的过去状态，这种结构使得 RNN 可以保存、记住和处理长时期的过去复杂信号。但有的时候，我们仅需利用最近的信息来处理当前的任务。例如：考虑一个用于利用之前的文字预测后续文字的语言模型，如果我们想预测 “the clouds are in the <strong>sky</strong>” 中的最后一个词，我们不需要太远的上下信息，很显然这个词就应该是 <strong>sky</strong>。在这个情况下，待预测位置与相关的信息之间的间隔较小，RNN 可以有效的利用过去的信息。</p>
<p><img src="/images/cn/2018-09-21-rnn/rnn-long-term-dependencies-short.png" alt="RNN-Long-Term-Dependencies-Short"></p>
<p>但也有很多的情况需要更多的上下文信息，考虑需要预测的文本为 “I grew up in France &hellip; I speak fluent <strong>French</strong>”。较近的信息表明待预测的位置应该是一种语言，但想确定具体是哪种语言需要更远位置的“在法国长大”的背景信息。理论上 RNN 有能力处理这种<strong>长期依赖</strong>，但在实践中 RNN 却很难解决这个问题 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>。</p>
<p><img src="/images/cn/2018-09-21-rnn/rnn-long-term-dependencies-long.png" alt="RNN-Long-Term-Dependencies-Long"></p>
<h2 id="lstm">LSTM</h2>
<h3 id="lstm-网络结构">LSTM 网络结构</h3>
<p>长短时记忆网络 (Long Short Term Memroy, LSTM) 是由 Hochreiter 和 Schmidhuber <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 提出一种特殊的 RNN。LSTM 的目的就是为了解决长期依赖问题，记住长时间的信息是 LSTM 的基本功能。</p>
<p>所有的循环神经网络都是由重复的模块构成的一个链条。在标准的 RNN 中，这个重复的模块的结构比较简单，仅包含一个激活函数为 <code>$\tanh$</code> 的隐含层，如下图所示：</p>
<p><img src="/images/cn/2018-09-21-rnn/rnn.png" alt="RNN"></p>
<p>LSTM 也是类似的链条状结构，但其重复的模块的内部结构不同。模块内部并不是一个隐含层，而是四个，并且以一种特殊的方式进行交互，如下图所示：</p>
<p><img src="/images/cn/2018-09-21-rnn/lstm.png" alt="LSTM"></p>
<p>下面我们将一步一步的介绍 LSTM 单元 (cell) 的具体工作原理，在之前我们先对使用到的符号进行简单的说明，如下图所示：</p>
<p><img src="/images/cn/2018-09-21-rnn/lstm-operations-symbols.png" alt="LSTM-Operations-Symbols"></p>
<p>其中，每条线都包含一个从输出节点到其他节点的整个向量，粉红色的圆圈表示逐元素的操作，黄色的矩形为学习到的神经网络层，线条的合并表示连接，线条的分叉表示内容的复制并转移到不同位置。</p>
<h3 id="lstm-单元状态和门控机制">LSTM 单元状态和门控机制</h3>
<p>LSTM 的关键为单元的状态 (cell state)，即下图中顶部水平穿过单元的直线。单元的状态像是一条传送带，其直接运行在整个链条上，同时仅包含少量的线性操作。因此，信息可以很容易得传递下去并保持不变。</p>
<p><img src="/images/cn/2018-09-21-rnn/lstm-cell-state.png" alt="LSTM-Cell-State"></p>
<p>LSTM 具有向单元状态添加或删除信息的能力，这种能力被由一种称之为“门” (gates) 的结构所控制。门是一种可选择性的让信息通过的组件，其由一层以 Sigmoid 为激活函数的网络层和一个逐元素相乘操作构成的，如下图所示：</p>
<p><img src="/images/cn/2018-09-21-rnn/lstm-pointwise-operation.png" alt="LSTM-Pointwise-Operation"></p>
<p>Sigmoid 层的输出值介于 0 和 1 之间，代表了所允许通过的数据量。0 表示不允许任何数据通过，1 表示允许所有数据通过。一个 LSTM 单元包含 3 个门用于控制单元的状态。</p>
<h3 id="lstm-工作步骤">LSTM 工作步骤</h3>
<p>LSTM 的第一步是要决定从单元状态中所<strong>忘记</strong>的信息，这一步是通过一个称之为“<strong>遗忘门 (forget gate)</strong>”的 Sigmoid 网络层控制。该层以上一时刻隐含层的输出 <code>$h_{t-1}$</code> 和当前这个时刻的输入 <code>$x_t$</code> 作为输入，输出为一个介于 0 和 1 之间的值，1 代表全部保留，0 代表全部丢弃。回到之前的语言模型，单元状态需要包含主语的性别信息以便选择正确的代词。但当遇见一个新的主语后，则需要忘记之前主语的性别信息。</p>
<p><img src="/images/cn/2018-09-21-rnn/lstm-cell-forget-gate.png" alt="LSTM-Cell-Forget-Gate"></p>
<p><code>$$ f_t = \sigma \left(W_f \cdot \left[h_{t-1}, x_t\right] + b_f\right) $$</code></p>
<p>第二步我们需要决定要在单元状态中存储什么样的新信息，这包含两个部分。第一部分为一个称之为“<strong>输入门 (input gate)</strong>” 的 Sigmoid 网络层，其决定更新那些数据。第二部分为一个 Tanh 网络层，其将产生一个新的候选值向量 <code>$\tilde{C}_t$</code> 并用于添加到单元状态中。之后会将两者进行整合，并对单元状态进行更新。在我们的语言模型中，我们希望将新主语的性别信息添加到单元状态中并替代需要忘记的旧主语的性别信息。</p>
<p><img src="/images/cn/2018-09-21-rnn/lstm-cell-input-gate.png" alt="LSTM-Cell-Input-Gate"></p>
<p><code>$$ \begin{equation} \begin{split} i_t &amp;= \sigma \left(W_i \cdot \left[h_{t-1}, x_t\right] + b_i\right) \\ \tilde{C}_t &amp;= \tanh \left(W_C \cdot \left[h_{t-1}, x_t\right] + b_C\right) \end{split} \end{equation} $$</code></p>
<p>接下来需要将旧的单元状态 <code>$C_{t-1}$</code> 更新为 <code>$C_t$</code>。我们将旧的单元状态乘以 <code>$f_t$</code> 以控制需要忘记多少之前旧的信息，再加上 <code>$i_t \odot \tilde{C}_t$</code> 用于控制单元状态的更新。在我们的语言模型中，该操作真正实现了我们对与之前主语性别信息的遗忘和对新信息的增加。</p>
<p><img src="/images/cn/2018-09-21-rnn/lstm-cell-state-update.png" alt="LSTM-Cell-State-Update"></p>
<p><code>$$ C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t $$</code></p>
<p>最后我们需要确定单元的输出，该输出将基于单元的状态，但为一个过滤版本。首先我们利用一个 Sigmoid 网络层来确定单元状态的输出，其次我们对单元状态进行 <code>$\tanh$</code> 操作 (将其值缩放到 -1 和 1 之间) 并与之前 Sigmoid 层的输出相乘，最终得到需要输出的信息。</p>
<p><img src="/images/cn/2018-09-21-rnn/lstm-cell-output-gate.png" alt="LSTM-Cell-Output-Gate"></p>
<p><code>$$ \begin{equation} \begin{split} o_t &amp;= \sigma \left(W_o \cdot \left[h_{t-1}, x_t\right] + b_o\right) \\ h_t &amp;= o_t \odot \tanh \left(C_t\right) \end{split} \end{equation} $$</code></p>
<h3 id="lstm-变种">LSTM 变种</h3>
<p>上文中介绍的基础的 LSTM 模型，事实上不同学者对 LSTM 的结构进行了或多或少的改变，其中一个比较有名的变种是由 Gers 和 Schmidhuber 提出的 <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>。其添加了一种“窥视孔连接 (peephole connections)”，这使得每一个门结构都能够窥视到单元的状态。</p>
<p><img src="/images/cn/2018-09-21-rnn/peephole-cell.png" alt="Peephole-Cell"></p>
<p><code>$$ \begin{equation} \begin{split} f_t &amp;= \sigma \left(W_f \cdot \left[\boldsymbol{C_{t-1}}, h_{t-1}, x_t\right] + b_f\right) \\ i_t &amp;= \sigma \left(W_i \cdot \left[\boldsymbol{C_{t-1}}, h_{t-1}, x_t\right] + b_i\right) \\ o_t &amp;= \sigma \left(W_o \cdot \left[\boldsymbol{C_t}, h_{t-1}, x_t\right] + b_o\right) \end{split} \end{equation} $$</code></p>
<p>另一个变种是使用了成对的遗忘门和输入门。不同于一般的 LSTM 中分别确定需要遗忘和新添加的信息，成对的遗忘门和输入门仅在需要添加新输入是才会忘记部分信息，同理仅在需要忘记信息时才会添加新的输入。</p>
<p><img src="/images/cn/2018-09-21-rnn/cfig-cell.png" alt="CFIG-Cell"></p>
<p><code>$$ C_t = f_t \odot C_{t-1} + \boldsymbol{\left(1 - f_t\right)} \odot \tilde{C}_t $$</code></p>
<p>另外一个比较有名的变种为 Cho 等人提出的 Gated Recurrent Unit (GRU) <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>，单元结构如下：</p>
<p><img src="/images/cn/2018-09-21-rnn/gru-cell.png" alt="GRU-Cell"></p>
<p>GRU 将遗忘门和输入门整个成一层，称之为“<strong>更新门 (update gate)</strong>”，同时配以一个“<strong>重置门 (reset gate)</strong>”。具体的计算过程如下：</p>
<p>首先计算更新门 <code>$z_t$</code> 和重置门 <code>$r_t$</code>：</p>
<p><code>$$ \begin{equation} \begin{split} z_t &amp;= \sigma \left(W_z \cdot \left[h_{t-1}, x_t\right]\right) \\ r_t &amp;= \sigma \left(W_r \cdot \left[h_{t-1}, x_t\right]\right) \end{split} \end{equation} $$</code></p>
<p>其次计算候选隐含层 (candidate hidden layer) <code>$\tilde{h}_t$</code>，与 LSTM 中计算 <code>$\tilde{C}_t$</code> 类似，其中 <code>$r_t$</code> 用于控制保留多少之前的信息：</p>
<p><code>$$ \tilde{h}_t = \tanh \left(W \cdot \left[r_t \odot h_{t-1}, x_t\right]\right) $$</code></p>
<p>最后计算需要从之前的隐含层 <code>$h_{t-1}$</code> 遗忘多少信息，同时加入多少新的信息 <code>$\tilde{h}_t$</code>，<code>$z_t$</code> 用于控制这个比例：</p>
<p><code>$$ h_t = \left(1 - z_t\right) \odot h_{t-1} + z_t \odot \tilde{h}_t $$</code></p>
<p>因此，对于短距离依赖的单元重置门的值较大，对于长距离依赖的单元更新门的值较大。如果 <code>$r_t = 1$</code> 并且 <code>$z_t = 0$</code>，则 GRU 退化为一个标准的 RNN。</p>
<p>除此之外还有大量的 LSTM 变种，Greff 等人 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup> 对一些常见的变种进行了比较，Jozefowicz 等人 <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> 测试了大量的 RNN 结构在不同任务上的表现。</p>
<h2 id="扩展与应用">扩展与应用</h2>
<p>循环神经网络在序列建模上有着天然的优势，其在自然语言处理，包括：语言建模，语音识别，机器翻译，对话与QA，文本生成等；计算视觉，包括：目标识别，视觉追踪，图像生成等；以及一些综合场景，包括：图像标题生成，视频字幕生成等，多个领域均有不错的表现，有代表性的论文请参见 <a href="https://github.com/kjw0612/awesome-rnn">awesome-rnn</a>。</p>
<p>Google 的 <a href="https://magenta.tensorflow.org/">Magenta</a> 是一项利用机器学习创作艺术和音乐的研究，其中也包含了大量利用 RNN 相关模型构建的有趣项目。<a href="https://magenta.tensorflow.org/sketch-rnn-demo">SketchRNN</a> 是由 Ha 等人 <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup> 提出了一种能够根据用户描绘的一些简单图形自动完成后续绘画的 RNN 网络。</p>
<p><img src="/images/cn/2018-09-21-rnn/sketch-rnn-demo.gif" alt="SketchRNN-Demo"></p>
<p><a href="https://magenta.tensorflow.org/performance-rnn-browser">Performance RNN</a> 是由 Ian
等人 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup> 提出了一种基于时间和动态因素生成复合音乐的 LSTM 网络。</p>
<p><img src="/images/cn/2018-09-21-rnn/performance-rnn-demo.gif" alt="Performance-RNN-Demo"></p>
<p>更多有趣的作品请参见 Megenta 的 <a href="https://magenta.tensorflow.org/demos">Demos</a> 页面。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Salehinejad, H., Sankar, S., Barfett, J., Colak, E., &amp; Valaee, S. (2017). Recent Advances in Recurrent Neural Networks. <em>arXiv preprint arXiv:1801.01078.</em>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Sutskever, I., Martens, J., Dahl, G., &amp; Hinton, G. (2013). On the importance of initialization and momentum in deep learning. In <em>International Conference on Machine Learning</em> (pp. 1139–1147).&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Bengio, Y., Simard, P., &amp; Frasconi, P. (1994). Learning long-term dependencies with gradient descent is difficult. <em>IEEE Transactions on Neural Networks, 5(2)</em>, 157–166.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Hochreiter, S., &amp; Schmidhuber, J. (1997). Long short-term memory. Neural Computation, 9(8), 1735–1780.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Gers, F. A., &amp; Schmidhuber, J. (2000). Recurrent nets that time and count. In <em>Proceedings of the IEEE-INNS-ENNS International Joint Conference on Neural Networks. IJCNN 2000. Neural Computing: New Challenges and Perspectives for the New Millennium</em> (Vol. 3, pp. 189–194 vol.3).&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., &amp; Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation. In <em>Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP)</em> (pp. 1724–1734).&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Greff, K., Srivastava, R. K., Koutník, J., Steunebrink, B. R., &amp; Schmidhuber, J. (2017). LSTM: A Search Space Odyssey. <em>IEEE Transactions on Neural Networks and Learning Systems, 28(10)</em>, 2222–2232.&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Jozefowicz, R., Zaremba, W., &amp; Sutskever, I. (2015). An Empirical Exploration of Recurrent Network Architectures. In <em>Proceedings of the 32Nd International Conference on International Conference on Machine Learning</em> - Volume 37 (pp. 2342–2350).&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Ha, D., &amp; Eck, D. (2017). A Neural Representation of Sketch Drawings. <em>arXiv preprint arXiv:1704.03477</em>&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Ian S., &amp; Sageev O. Performance RNN: Generating Music with Expressive Timing and Dynamics. Magenta Blog, 2017. <a href="https://magenta.tensorflow.org/performance-rnn">https://magenta.tensorflow.org/performance-rnn</a>&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>泰国之行 (Tour of Thailand)</title><link>https://zeqiang.fun/cn/2018/09/tour-of-thailand/</link><pubDate>Sat, 15 Sep 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/09/tour-of-thailand/</guid><description><![CDATA[
        <p>近来这大半年的工作感觉活活像一场清宫剧，对于我们这种一心只想撸代码，两耳不闻窗外事的人来说，确实太累了。从年初换了工作方向后，交接了所有之前的线上任务和系统，所以这次旅行格外的清净，没有一封报警邮件。北京已经入秋，走之前还很热，回来已凉意浓浓，去的路上天气格外的好。</p>
<link rel="stylesheet" href="/css/photoswipe.css" />
<div class="box fancy-figure caption-position-bottom caption-effect-fade" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/blue-sky.jpg" alt="蓝蓝的天空"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/blue-sky.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>蓝蓝的天空</p>
      </figcaption>
  </figure>
</div>

<p>进去大皇宫不能衣冠不整，牛仔裤上有个洞，在门口买了条裤子套上，还挺配我的白衬衫，感觉满满的社会人气息 &#x1f60e;。发现中国人在外面很难 High 起来，油轮之上小姐姐唱着不同国家的歌曲，独独唱到中文歌的时候没有人上去跳。也许我们天生没有欧美人的开放，也不像中东朋友在国家内被束缚的太紧，不过感觉出来玩还是不要闷骚，把激情都释放出来才好。</p>


<div class="gallery tiles-3 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/grand-palace.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/grand-palace.jpg" alt="大皇宫"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/grand-palace.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>大皇宫</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/wat-arun.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/wat-arun.jpg" alt="夜晚的黎明寺"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/wat-arun.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>夜晚的黎明寺</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/tanker-in-night.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/tanker-in-night.jpg" alt="油轮之上"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/tanker-in-night.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>油轮之上</p>
      </figcaption>
  </figure>
</div>


</div>

<p>Pattaya 的水上市场虽然有商业景区的味道，但还是保留了很多当地的特色，水流两旁很多卖水果和小吃的小船。海边的日落很美，再来杯美酒，再来个佳人就更好了。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/pattaya-floating-market.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/pattaya-floating-market.jpg" alt="水上市场"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/pattaya-floating-market.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>水上市场</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/beach-sunset.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/beach-sunset.jpg" alt="海边日落"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/beach-sunset.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>海边日落</p>
      </figcaption>
  </figure>
</div>


</div>

<p>只在金沙岛上呆了小半天，天公还算作美，早起下着小雨，上岛了雨就停了。想想上次下水游泳还是上初中时候的事情了，虽然不怎么太会游，但至少还能扑腾两下。下次再有机会来，一定直奔普吉岛，舒舒服服的在岛上呆上几天，别的啥也不干，就游游泳，晒晒太阳。</p>

  
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/video.js/7.17.1/video-js.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/video.js/7.17.1/video.min.js"></script>


<video class="video-js vjs-big-play-centered vjs-fluid"
       data-setup='{
         "controls": true,
         "poster": "/images/cn/2018-09-15-tour-of-thailand/snorkeling.jpg", 
         "autoplay": false,
         "preload": "auto",
         "loop": false
       }'>
  <source src="//cdn.leovan.me/videos/blog/cn/2018-09-15-tour-of-thailand/snorkeling.mp4" type="video/mp4"></source>
  
  
  <p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
</video>

<p>这边最多的当然是各种各样的水果和超大只的海鲜，第一次吃到蛇皮果，据说这东西很壮阳。最鲜的还是螃蟹，越大只越好，泰国的酱料味道还是很特别的，不难吃但不是很习惯，不过海鲜什么都不沾，也很鲜甜。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/fruit.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/fruit.jpg" alt="好多好多水果"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/fruit.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>好多好多水果</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/seafood.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/seafood.jpg" alt="好多好多海鲜"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/seafood.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>好多好多海鲜</p>
      </figcaption>
  </figure>
</div>


</div>

<p>泰国的枪械管得比较松，打了几枪后座力比较强的 .45 手枪和霰弹枪，成绩还不错，有 10 环哦。回来想一想比国内打枪便宜不少，后悔没有把所有的枪型都试一试。</p>


<div class="gallery tiles-3 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/45mm-gun.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/45mm-gun.jpg" alt=".45 手枪"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/45mm-gun.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>.45 手枪</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/target-sheet.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/target-sheet.jpg" alt="还不赖的结果"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/target-sheet.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>还不赖的结果</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2018-09-15-tour-of-thailand/shotgun.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2018-09-15-tour-of-thailand/shotgun.jpg" alt="霰弹枪"/>
    </div>
    <a href="/images/cn/2018-09-15-tour-of-thailand/shotgun.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>霰弹枪</p>
      </figcaption>
  </figure>
</div>


</div>



  


<script src="/js/load-photoswipe.js"></script>


<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.css'/>
<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/default-skin/default-skin.min.css'/>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js'></script>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js'></script>


<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

<div class="pswp__bg"></div>

<div class="pswp__scroll-wrap">
    
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    </div>
    
    <div class="pswp__ui pswp__ui--hidden">
    <div class="pswp__top-bar">
      
      <div class="pswp__counter"></div>
      <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
      <button class="pswp__button pswp__button--share" title="Share"></button>
      <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
      <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
      
      
      <div class="pswp__preloader">
        <div class="pswp__preloader__icn">
          <div class="pswp__preloader__cut">
            <div class="pswp__preloader__donut"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
      <div class="pswp__share-tooltip"></div>
    </div>
    <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
    </button>
    <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
    </button>
    <div class="pswp__caption">
      <div class="pswp__caption__center"></div>
    </div>
    </div>
    </div>
</div>


        ]]></description></item><item><title>媒介之战 (War of Medias)</title><link>https://zeqiang.fun/cn/2018/09/war-of-medias/</link><pubDate>Sat, 01 Sep 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/09/war-of-medias/</guid><description><![CDATA[
        <blockquote>
<p>本文为《娱乐至死》(Amusing Ourselved To Death) 的读书随想。</p>
</blockquote>

<link rel="stylesheet" href="/css/douban-card.css" />


<div class='douban-card douban-card-book' id='douban-card-book-26319730' douban-id='26319730'></div>

<p>在这本书中，作者 <a href="https://en.wikipedia.org/wiki/Neil_Postman">Neil Postman</a> 的基本观点为推崇铅字文化，声讨电视文化。首先，我必须承认作者对电视文化的很多现象描述确实存在，我虽然也一直知道其存在，但却从未思考过其中的问题，这是这本书对我影响最多的地方。换言之，是书籍 (铅字文化) 能够让我更深入的思考问题，这也是作者所推崇的铅字文化的益处。同时，作者并没有否定电视文化作为娱乐本身的用途，这点我也是认同的。无论是电影，电视剧还是综艺节目，电视文化确实以一种五彩斑斓的形式丰富着我们的娱乐生活。但我认为作者对于两种不同文化的观点略微有些绝对和偏激，尤其是在书籍和电视 (不同的信息载体) 内容过剩的今天，我认为两者都存在精华和糟粕 (同时包括文化内容和娱乐内容)。我认为书籍和电视中文化和娱乐的界线也不是很明显，虽然我支持作者的不要将娱乐和一些严肃的事情 (例如：政治，宗教，教育等) 混在一起，但是我也不否能不能从电视文化中获取知识。我认为更重要的是对于信息的细粒度消化，在这个过程中比较重要的是 <strong>思考</strong> 和 <strong>实践</strong>。</p>
<p>作者在书的最前面提到：</p>
<blockquote>
<p>奥威尔担心我们憎恨的东西会毁掉我们，而赫胥黎担心的是，我们将毁于我们热爱的东西。</p>
</blockquote>
<p>在文末又再次呼应了这个观点：</p>
<blockquote>
<p>有两种方法可以让文化精神枯萎，一种是奥威尔式的 &ndash; 文化成为一个监狱，另一种是赫胥黎式的 &ndash; 文化成为一种滑稽戏。</p>
</blockquote>
<p>最后，又引用了赫胥黎在《美丽新世界》中的观点：</p>
<blockquote>
<p>人们感到痛苦的不是他们用笑声代替了思考，而是他们不知道自己为什么笑以及为什么不再思考</p>
</blockquote>
<p>最后这句最符合我的观点，下面从几个方面聊聊我对书中观点的一些看法和自己的一些补充。</p>
<h2 id="信息-文化和知识">信息、文化和知识</h2>
<p>作者用了两章的内容讨论 <strong>媒介</strong> 的作用，那么我就从媒介在信息的传递，文化的形成和知识的获取的三个角度来谈一谈我对媒介的认知。</p>
<h3 id="信息的传递">信息的传递</h3>
<p><strong>信息</strong>，我认为可以简单的理解为我们所能感知到的一切，这里的 <strong>感知</strong> 其实就可以理解为 <strong>媒介</strong>。对于信息本身，可以将其粗略的划分为两类：<strong>有用的信息</strong> 和 <strong>无用的信息</strong>。理论一点的解释就是：从信息论的角度而言，能够帮助降低系统不确定性 (信息熵) 的事物就认为是有用的信息 (与真实概念略有差异)；通俗一点的解释就是：你在过马路时，你能够感知到行人，车辆和建筑物等各种信息，此时你更关心的是那些会影响到你生命安全的车辆和行人，这些事物就是有用的信息。</p>
<p>无论是有用的信息还是无用的信息一定是通过某种方式 (嗅觉，听觉，视觉等) 和形态 (书籍，音乐，电视等) 传递到我们的大脑。所以我们对于事物的认知都不是透过其内涵本身，而是通过其在媒介中的具体表现形式。同一个事物本身透过不同的媒介，可能具有不同的具体形态，而我们直接去理解的正是这个具体的形态，这也就是麦克卢汉所说的 <strong>“媒介即信息”</strong>。</p>
<h3 id="文化的形成">文化的形成</h3>
<p><strong>文化</strong> 同 <strong>知识</strong> 有相似之处，我认为两者的主要区别在于：文化是一个比较中性的描述，其表示一段时间内形成的某些习惯 (例如：饮食，建筑等)，强调的是 <strong>习惯本身</strong>，而非 <strong>对习惯的评价 (好与坏)</strong>。而知识往往更像是一种具有 <strong>“好”的影响</strong> 的文化结晶，这个“好”可以对于集体而言也可以对于个人而言。</p>
<p>这些“习惯”又是如何一步步形成的呢？首先，我认为客观环境会是一个比较大的影响因素。从很底层出发，人首先需要做到同自然的抗争和共处，所以就会有人担忧干旱，有人担忧洪涝，自然会促使人们向不同方向发展。其次，就是我们人的主观因素了，我认为人的因素要更为复杂些。文化并不是一成不变的，而是在不断的演变，这种演变正是我们人自己主观选择的结果。</p>
<h3 id="知识的获取">知识的获取</h3>
<p>如上文中，我将 <strong>知识</strong> 定义为具有“好”的影响的文化，所以知识的获取就可以理解为对信息的选择。我认为对选择的结果产生影响的主要有两个方面：用于选择的 <strong>信息池</strong> 和 <strong>选择的方法</strong>。这个池子就像是作者所描述的不同媒介所包含的信息，铅字文化中包含更多的是知识类型的信息，而电视文化中包含的信息更多的是娱乐类型的信息。选择的方法则像一个甄别和抽象的工具，对你感知到的信息进行筛选，提取和抽象，得到最终有意义的知识。我认为两者都很重要，但在信息过剩的今天，后者对于我们自己 <strong>可控性</strong> 会更好些。</p>
<h2 id="严肃性与思考">严肃性与思考</h2>
<p>到这，我们就聊聊刚才说的可控性更好的选择方法，概括而言这个方法就是 <strong>思考</strong>。思考本身是具有 <strong>严肃性</strong> 的，在谈及思考的严肃性之前，我们先说一下信息的严肃性。在书的第六章和第七章中，对于电视文化的抨击主要有如下几点：</p>
<ol>
<li>电视不再是为我们展示具有娱乐性的内容，而是将所有内容都以娱乐方式表现出来。</li>
<li>电视呈现的事件都是独立存在的，剥夺了其与过去，未来和其他事件之间的关联。</li>
</ol>
<p>第一点其实就是在说一些严肃性的信息 (例如：宗教，教育，政治等) 不应该以电视这种形式进行展现。我很赞同这一点，从思考的角度，这些严肃的事情是需要不断思考的，也就是说思考是贯穿在这些信息的接受之中的，而电视往往没有在中途预留很多时间，很快便进入了预先设计好的后续环节。</p>
<p>第二点其实作者更多的描述的是 <strong>电视新闻</strong>，我也很赞同这一点，确实这些被剥离同其他事情关联的快速新闻，只能片面的向我们做出了事情本身的局部，长此以往只会让我们对其变得麻木不仁，因为他们关心的是给观众留下印象而非观点。</p>
<p>面对不同形式的信息，我们思考的方式也会有不同。例如读一本书，遇到不熟悉的名词，你可能需要停下来仔细调研思考之后才会继续阅读，避免影响对后续内容的理解。又例如观影，电影更擅长以视觉冲击让你对其中的某些场景留下深刻的印象，而对电影的思考往往是以事后对其内容进行反思的形式，进一步理解其深层含义。但无论是以何种形式去思考，最重要的一点是 <strong>独立思考</strong>，也就是我想表达的思考的严肃性。思考不必可避免的会涉及到对相同事物不同观点的发表和交流，独立思考让我们做到不人云亦云，同时我们也需要做到不固执己见。</p>
<h2 id="思考与实践">思考与实践</h2>
<p>思考不能停留在精神层面，思考可以让你不人云亦云，但是对事物的理解本身又有太多的主观性，其正确性却有待验证。<strong>实践</strong> 则可以帮助你检查你思考结果的正确性，否定自己错误的判断，避免固执己见。怎么去实践又是一门学问，但也大同小异，我感觉比较有效的几个方法如下：</p>
<ol>
<li>反复。是说对于一个事情在不同的时点可以重复思考，比如书可以再读一篇，电影可以再看一次，每一次都会有不同的收获。</li>
<li>笔记。这点对于读书和观影都有效，把想法写下来，不光会让你的思路变得更加清晰，有时你还会在总结的过程发现自相矛盾的地方，有助于自我改正。</li>
<li>做实验。好与不好，搞一下不就知道了？拿我们这群做模型的人来说，各种深度学习算法原理掌握的再好，不放在具体的问题上，不用真实的数据试一试，都很难说孰优孰劣。</li>
</ol>
<h2 id="媒介之战">媒介之战</h2>
<p>回到这本书的核心 &ndash; <strong>“媒介”</strong>。看起来媒介对我们像是一种 <strong>被动</strong> 的影响，而思考是一种 <strong>主动</strong> 的干预。但我认为媒介的演变其实就是我们主动选择的结果，一种对精神放纵的结果。现在不同媒介之间，甚至相同媒介内部都充满着过剩的信息，那些让你感觉获取方便，理解容易的信息所包含的知识应该不多。知识的获取一定是一件困难的事情，当你过于放纵，贪图舒适的信息获取，那必然仅能得到有限的知识。所以不要对于媒介的呈现形式所迷惑，对于铅字文化也好，对于电视文化也好，你都需要保持思辨的精神，不要让这场媒介之战影响我们对于知识的获取和未知的探索。</p>
<blockquote>
<p>What you see with your eyes may not be true, see it with your heart.</p>
</blockquote>
<p>一句本来之前用于讽刺自己的话，现在看来放在这也挺合适，不要浮于事物的表象，更重要的是你对事物的看法。</p>

        ]]></description></item><item><title>卷积神经网络 (Convolutional Neural Network, CNN)</title><link>https://zeqiang.fun/cn/2018/08/cnn/</link><pubDate>Sat, 25 Aug 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/08/cnn/</guid><description><![CDATA[
        <h2 id="发展史">发展史</h2>
<p>卷积神经网络 (Convolutional Neural Network, CNN) 是一种目前广泛用于图像，自然语言处理等领域的深度神经网络模型。1998 年，Lecun 等人 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 提出了一种基于梯度的反向传播算法用于文档的识别。在这个神经网络中，卷积层 (Convolutional Layer) 扮演着至关重要的角色。</p>
<p>随着运算能力的不断增强，一些大型的 CNN 网络开始在图像领域中展现出巨大的优势，2012 年，Krizhevsky 等人 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 提出了 AlexNet 网络结构，并在 ImageNet 图像分类竞赛 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 中以超过之前 11% 的优势取得了冠军。随后不同的学者提出了一系列的网络结构并不断刷新 ImageNet 的成绩，其中比较经典的网络包括：VGG (Visual Geometry  Group) <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>，GoogLeNet <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> 和 ResNet <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>。</p>
<p>CNN 在图像分类问题上取得了不凡的成绩，同时一些学者也尝试将其应用在图像的其他领域，例如：物体检测 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup><sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup><sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup>，语义分割 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup>，图像摘要 <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup>，行为识别 <sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup> 等。除此之外，在非图像领域 CNN 也取得了一定的成绩 <sup id="fnref:13"><a href="#fn:13" class="footnote-ref" role="doc-noteref">13</a></sup>。</p>
<h2 id="模型原理">模型原理</h2>
<p>下图为 Lecun 等人提出的 LeNet-5 的网络架构：</p>
<p><img src="/images/cn/2018-08-25-cnn/lenet-5.png" alt="LeNet-5"></p>
<p>下面我们针对 CNN 网络中的不同类型的网络层逐一进行介绍。</p>
<h3 id="输入层">输入层</h3>
<p>LeNet-5 解决的手写数字分类问题的输入为一张 32x32 像素的灰度图像 (Gray Scale)。日常生活中计算机常用的图像的表示方式为 RGB，即将一张图片分为红色通道 (Red Channel)，绿色通道 (Green Channel) 和蓝色通道 (Blue Channel)，其中每个通道的每个像素点的数值范围为 <code>$\left[0, 255\right]$</code>。灰度图像表示该图片仅包含一个通道，也就是不具备彩色信息，每个像素点的数值范围同 RGB 图像的取值范围相同。</p>
<p>因此，一张图片在计算机的眼里就是一个如下图所示的数字矩阵 (示例图片来自于 MNIST 数据集 <sup id="fnref:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup>)：</p>
<p><img src="/images/cn/2018-08-25-cnn/digit-pixels.png" alt="Digit-Pixels"></p>
<p>在将图像输入到 CNN 网络之前，通常我们会对其进行预处理，因为每个像素点的最大取值为 <code>$255$</code>，因此将每个像素点的值除以 <code>$255$</code> 则可以将其归一化到 <code>$\left[0, 1\right]$</code> 的范围。</p>
<h3 id="卷积层">卷积层</h3>
<p>在了解卷积层之前，让我们先来了解一下什么是卷积？设 <code>$f\left(x\right), g\left(x\right)$</code> 是 <code>$\mathbb{R}$</code> 上的两个可积函数，则卷积定义为：</p>
<p><code>$$ \left(f * g\right) \left(x\right) = \int_{- \infty}^{\infty}{f \left(\tau\right) g \left(x - \tau\right) d \tau} $$</code></p>
<p>离散形式定义为：</p>
<p><code>$$ \left(f * g\right) \left(x\right) = \sum_{\tau = - \infty}^{\infty}{f \left(\tau\right) g \left(x - \tau\right)} $$</code></p>
<p>我们用一个示例来形象的理解一下卷积的含义，以离散的形式为例，假设我们有两个骰子，<code>$f\left(x\right), g\left(x\right)$</code> 分别表示投两个骰子，<code>$x$</code> 面朝上的概率。</p>
<p><code>$$ f \left(x\right) = g \left(x\right) = \begin{cases} 1/6 &amp; x = 1, 2, 3, 4, 5, 6 \\ 0 &amp; \text{otherwise} \end{cases} $$</code></p>
<p>卷积 <code>$\left(f * g\right) \left(x\right)$</code> 表示投两个骰子，朝上数字之和为 <code>$x$</code> 的概率。则和为 <code>$4$</code> 的概率为：</p>
<p><code>$$ \begin{equation} \begin{split} \left(f * g\right) \left(4\right) &amp;= \sum_{\tau = 1}^{6}{f \left(\tau\right) g \left(4 - \tau\right)} \\ &amp;= f \left(1\right) g \left(4 - 1\right) + f \left(2\right) g \left(4 - 2\right) + f \left(3\right) g \left(4 - 3\right) \\ &amp;= 1/6 \times 1/6 + 1/6 \times 1/6 + 1/6 \times 1/6 \\ &amp;= 1/12 \end{split} \end{equation} $$</code></p>
<p>这是一维的情况，我们处理的图像为一个二维的矩阵，因此类似的有：</p>
<p><code>$$ \left(f * g\right) \left(x, y\right) = \sum_{v = - \infty}^{\infty}{\sum_{h = - \infty}^{\infty}{f \left(h, v\right) g \left(x - h, y - v\right)}} $$</code></p>
<p>这次我们用一个抽象的例子解释二维情况下卷积的计算，设 <code>$f, g$</code> 对应的概率矩阵如下：</p>
<p><code>$$ f = \left[ \begin{array}{ccc} \color{red}{a_{0, 0}} &amp; \color{orange}{a_{0, 1}} &amp; \color{yellow}{a_{0, 2}} \\ \color{green}{a_{1, 0}} &amp; \color{cyan}{a_{1, 1}} &amp; \color{blue}{a_{1, 2}} \\ \color{purple}{a_{2, 0}} &amp; \color{black}{a_{2, 1}} &amp; \color{gray}{a_{2, 2}} \end{array} \right] , g = \left[ \begin{array}{ccc} \color{gray}{b_{-1, -1}} &amp; \color{black}{b_{-1, 0}} &amp; \color{purple}{b_{-1, 1}} \\ \color{blue}{b_{0, -1}} &amp; \color{cyan}{b_{0, 0}} &amp; \color{green}{b_{0, 1}} \\ \color{yellow}{b_{1, -1}} &amp; \color{orange}{b_{1, 0}} &amp; \color{red}{b_{1, 1}} \end{array} \right] $$</code></p>
<p>则 <code>$\left(f * g\right) \left(1, 1\right)$</code> 计算方式如下：</p>
<p><code>$$ \left(f * g\right) \left(1, 1\right) = \sum_{v = 0}^{2}{\sum_{h = 0}^{2}{f \left(h, v\right) g \left(1 - h, 1 - v\right)}} $$</code></p>
<p>从这个计算公式中我们就不难看出为什么上面的 <code>$f, g$</code> 两个概率矩阵的角标会写成上述形式，即两个矩阵相同位置的角标之和均为 <code>$1$</code>。<code>$\left(f * g\right) \left(1, 1\right)$</code> 即为 <code>$f, g$</code> 两个矩阵中对应颜色的元素乘积之和。</p>
<p>在上例中，<code>$f, g$</code> 两个概率矩阵的大小相同，而在 CNN 中，<code>$f$</code> 为输入的图像，<code>$g$</code> 一般是一个相对较小的矩阵，我们称之为卷积核。这种情况下，卷积的计算方式是类似的，只是会将 <code>$g$</code> 矩阵旋转 <code>$180^{\circ}$</code> 使得相乘的元素的位置也相同，同时需要 <code>$g$</code> 在 <code>$f$</code> 上进行滑动并计算对应位置的卷积值。下图 <sup id="fnref:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup> 展示了一步计算的具体过程：</p>
<p><img src="/images/cn/2018-08-25-cnn/conv-example.png" alt="Conv-Example"></p>
<p>下图 <sup id="fnref1:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup> 形象的刻画了利用一个 3x3 大小的卷积核的整个卷积计算过程：</p>
<p><img src="/images/cn/2018-08-25-cnn/conv-sobel.gif" alt="Conv-Sobel"></p>
<p>一些预设的卷积核对于图片可以起到不同的滤波器效果，例如下面 4 个卷积核分别会对图像产生不同的效果：不改变，边缘检测，锐化和高斯模糊。</p>
<p><code>$$ \left[ \begin{array}{ccc} 0 &amp; 0 &amp; 0 \\ 0 &amp; 1 &amp; 0 \\ 0 &amp; 0 &amp; 0 \end{array} \right] , \left[ \begin{array}{ccc} -1 &amp; -1 &amp; -1 \\ -1 &amp;  8 &amp; -1 \\ -1 &amp; -1 &amp; -1 \end{array} \right] , \left[ \begin{array}{ccc} 0  &amp; -1 &amp; 0 \\ -1 &amp;  5 &amp; -1 \\ 0  &amp; -1 &amp; 0 \end{array} \right] , \dfrac{1}{16} \left[ \begin{array}{ccc} 1 &amp; 2 &amp; 1 \\ 2 &amp; 4 &amp; 2 \\ 1 &amp; 2 &amp; 1 \end{array} \right] $$</code></p>
<p>对 lena 图片应用这 4 个卷积核，变换后的效果如下 (从左到右，从上到下)：</p>
<p><img src="/images/cn/2018-08-25-cnn/lena-filters.png" alt="Lena-Filters"></p>
<p>在上面整个计算卷积的动图中，我们不难发现，利用 3x3 大小 (我们一般将这个参数称之为 <code>kernel_size</code>，即<strong>卷积核的大小</strong>，其可以为一个整数表示长宽大小相同，也可以为两个不同的整数) 的卷积核对 5x5 大小的原始矩阵进行卷积操作后，结果矩阵并没有保持原来的大小，而是变为了 (5-(3-1))x(5-(3-1)) (即 3x3) 大小的矩阵。这就需要引入 CNN 网络中卷积层的两个常用参数 <code>padding</code> 和 <code>strides</code>。</p>
<p><code>padding</code> 是指是否对图像的外侧进行<strong>补零操作</strong>，其取值一般为 <code>VALID</code> 和 <code>SAME</code> 两种。<code>VALID</code> 表示<strong>不进行补零</strong>操作，对于输入形状为 <code>$\left(x, y\right)$</code> 的矩阵，利用形状为 <code>$\left(m, n\right)$</code> 的卷积核进行卷积，得到的结果矩阵的形状则为 <code>$\left(x-m+1, y-n+1\right)$</code>。<code>SAME</code> 表示<strong>进行补零</strong>操作，在进行卷积操作前，会对图像的四个边缘分别向左右补充 <code>$\left(m \mid 2 \right) + 1$</code> 个零，向上下补充 <code>$\left(n \mid 2 \right) + 1$</code> 个零 (<code>$\mid$</code> 表示整除)，从而保证进行卷积操作后，结果的形状与原图像的形状保持相同，如下图 <sup id="fnref2:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup> 所示：</p>
<p><img src="/images/cn/2018-08-25-cnn/conv-zero-padding.png" alt="Conv2d-Zero-Padding"></p>
<p><code>strides</code> 是指进行卷积操作时，每次卷积核移动的步长。示例中，卷积核在横轴和纵轴方向上的移动步长均为 <code>$1$</code>，除此之外用于也可以指定不同的步长。移动的步长同样会对卷积后的结果的形状产生影响。</p>
<p>除此之外，还有另一个重要的参数 <code>filters</code>，其表示在一个卷积层中使用的<strong>卷积核的个数</strong>。在一个卷积层中，一个卷积核可以学习并提取图像的一种特征，但往往图片中包含多种不同的特征信息，因此我们需要多个不同的卷积核提取不同的特征。下图 <sup id="fnref3:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup> 是一个利用 4 个不同的卷积核对一张图像进行卷积操作的示意图：</p>
<p><img src="/images/cn/2018-08-25-cnn/conv2d-kernels.png" alt="Conv2d-Kernels"></p>
<p>上面我们都是以一个灰度图像 (仅包含 1 个通道) 为示例进行的讨论，那么对于一个 RGB 图像 (包含 3 个通道)，相应的，卷积核也是一个 3 维的形状，如下图 <sup id="fnref4:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup> 所示：</p>
<p><img src="/images/cn/2018-08-25-cnn/conv3d-kernels.png" alt="Conv3d-Kernels"></p>
<p>卷积层对于我们的神经网络的模型带来的改进主要包括如下三个方面：<strong>稀疏交互 (sparse interactions)</strong>，<strong>参数共享 (parameter sharing)</strong> 和<strong>等变表示 (equivariant representations)</strong>。</p>
<p>在全连接的神经网络中，隐含层中的每一个节点都和上一层的所有节点相连，同时有被连接到下一层的全部节点。而卷积层不同，节点之间的连接性受到卷积核大小的制约。下图 <sup id="fnref:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup> 分别以自下而上 (左) 和自上而下 (右) 两个角度对比了卷积层和全连接层节点之间连接性的差异。</p>
<p><img src="/images/cn/2018-08-25-cnn/sparse-interactions.png" alt="Sparse-Interactions"></p>
<p>在上图 (右) 中，我们可以看出节点 <code>$s_3$</code> 受到节点 <code>$x_2$</code>，<code>$x_3$</code> 和 <code>$x_4$</code> 的影响，这些节点被称之为 <code>$s_3$</code> 的<strong>接受域 (receptive field)</strong>。稀疏交互使得在 <code>$m$</code> 个输入和 <code>$n$</code> 个输出的情况下，参数的个数由 <code>$m \times n$</code> 个减少至 <code>$k \times n$</code> 个，其中 <code>$k$</code> 为卷积核的大小。尽管一个节点在一个层级之间仅与其接受域内的节点相关联，但是对于深层中的节点，其与绝大部分输入之间却存在这<strong>间接交互</strong>，如下图 <sup id="fnref1:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup> 所示：</p>
<p><img src="/images/cn/2018-08-25-cnn/indirect-interactions.png" alt="Indirect-Interactions"></p>
<p>节点 <code>$g_3$</code> 尽管<strong>直接</strong>的连接是稀疏的，但处于更深的层中可以<strong>间接</strong>的连接到全部或者大部分的输入节点。这就使得网络可以仅通过这种稀疏交互来高效的描述多个输入变量之间的复杂关系。</p>
<p>除了稀疏交互带来的参数个数减少外，<strong>参数共享</strong>也起到了类似的作用。所谓参数共享就是指在进行不同操作时使用相同的参数，具体而言也就是在我们利用卷积核在图像上滑动计算卷积时，每一步使用的卷积核都是相同的。同全连接网络的情况对比如下图 <sup id="fnref2:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup> 所示：</p>
<p><img src="/images/cn/2018-08-25-cnn/parameter-sharing.png" alt="Parameter-Sharing"></p>
<p>在全连接网络 (上图 - 下) 中，任意两个节点之间的连接 (权重) 仅用于这两个节点之间，而在卷积层中，如上图所示，其对卷积核中间节点 (黑色箭头) 的使用方式 (权重) 是相同的。参数共享虽然对于计算的时间复杂度没有带来改进，仍然是 <code>$O \left(k \times n\right)$</code>，但其却将参数个数降低至 <code>$k$</code> 个。</p>
<p>正是由于参数共享机制，使得卷积层具有平移 <strong>等变 (equivariance)</strong> 的性质。对于函数 <code>$f\left(x\right)$</code> 和 <code>$g\left(x\right)$</code>，如果满足 <code>$f\left(g\left(x\right)\right) = g\left(f\left(x\right)\right)$</code>，我们就称 <code>$f\left(x\right)$</code> 对于变换 <code>$g$</code> 具有等变性。简言之，对于图像如果我们将所有的像素点进行移动，则卷积后的输出表示也会移动同样的量。</p>
<h3 id="非线性层">非线性层</h3>
<p>非线性层并不是 CNN 特有的网络层，在此我们不再详细介绍，一般情况下我们会使用 ReLU 作为我们的激活函数。</p>
<h3 id="池化层">池化层</h3>
<p><strong>池化层</strong> 是一个利用 <strong>池化函数 (pooling function)</strong> 对网络输出进行进一步调整的网络层。池化函数使用某一位置的相邻输出的总体统计特征来代替网络在该位置的输出。常用的池化函数包括最大池化 (max pooling) 函数 (即给出邻域内的最大值) 和平均池化 (average pooling) 函数 (即给出邻域内的平均值) 等。但无论选择何种池化函数，当对输入做出少量平移时，池化对输入的表示都近似 <strong>不变 (invariant)</strong>。<strong>局部平移不变性</strong> 是一个很重要的性质，尤其是当我们关心某个特征是否出现而不关心它出现的位置时。</p>
<p>池化层同卷积层类似，具有三个比较重要的参数：<code>pool_size</code>，<code>strides</code> 和 <code>padding</code>，分别表示池化窗口的大小，步长以及是否对图像的外侧进行补零操作。下图 <sup id="fnref3:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup> 是一个 <code>pool_size=3</code>，<code>strides=3</code>，<code>padding='valid'</code> 的最大池化过程示例：</p>
<p><img src="/images/cn/2018-08-25-cnn/max-pooling.gif" alt="Max-Pooling"></p>
<p>池化层同时也能够提高网络的计算效率，例如上图中在横轴和纵轴的步长均为 <code>$3$</code>，经过池化后，下一层网络节点的个数降低至前一层的 <code>$\frac{1}{3 \times 3} = \frac{1}{9}$</code>。</p>
<h3 id="全连接层">全连接层</h3>
<p>全链接层 (Fully-connected or Dense Layer) 的目的就是将我们最后一个池化层的输出连接到最终的输出节点上。例如，最后一个池化层的输出大小为 <code>$\left[5 \times 5 \times 16\right]$</code>，也就是有 <code>$5 \times 5 \times 16 = 400$</code> 个节点，对于手写数字识别的问题，我们的输出为 0 至 9 共 10 个数字，采用 one-hot 编码的话，输出层共 10 个节点。例如在 LeNet 中有 2 个全连接层，每层的节点数分别为 120 和 84，在实际应用中，通常全连接层的节点数会逐层递减。需要注意的是，在进行编码的时候，第一个全连接层并不是直接与最后一个池化层相连，而是先对池化层进行 flatten 操作，使其变成一个一维向量后再与全连接层相连。</p>
<h3 id="输出层">输出层</h3>
<p>输出层根据具体问题的不同会略有不同，例如对于手写数字识别问题，采用 one-hot 编码的话，输出层则包含 10 个节点。对于回归或二分类问题，输出层则仅包含 1 个节点。当然对于二分类问题，我们也可以像多分类问题一样将其利用 one-hot 进行编码，例如 <code>$\left[1, 0\right]$</code> 表示类型 0，<code>$\left[0, 1\right]$</code> 表示类型 1。</p>
<h2 id="扩展与应用">扩展与应用</h2>
<p>本节我们将介绍一些经典的 CNN 网络架构及其相关的改进。</p>
<h3 id="alexnet">AlexNet <sup id="fnref1:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></h3>
<p><img src="/images/cn/2018-08-25-cnn/alexnet.png" alt="AlexNet"></p>
<p>AlexNet 在整体结构上同 LeNet-5 类似，其改进大致如下：</p>
<ul>
<li>网络包含了 5 个卷积层和 3 个全连接层，网络规模变大。</li>
<li>使用了 ReLU 非线性激活函数。</li>
<li>应用了 Data Augmentation，Dropout，Momentum，Weight Decay 等策略改进训练。</li>
<li>在算力有限的情况下，对模型进行划分为两部分并行计算。</li>
<li>增加局部响应归一化 (LRN, Local Response Normalization)。</li>
</ul>
<p>LRN 的思想来自与生物学中侧抑制 (Lateral Inhibition) 的概念，简单来说就是相近的神经元之间会发生抑制作用。在 AlexNet 中，给出的 LRN 计算公式如下：</p>
<p><code>$$ b_{x,y}^{i} = a_{x,y}^{i} / \left(k + \alpha \sum_{j = \max \left(0, i - n/2\right)}^{\min \left(N - 1, i + n/2\right)}{\left(a_{x,y}^{j}\right)^2}\right)^{\beta} $$</code></p>
<p>其中，<code>$a_{x,y}^{i}$</code> 表示第 <code>$i$</code> 个卷积核在位置 <code>$\left(x,y\right)$</code> 的输出，<code>$N$</code> 为卷积核的个数，<code>$k, n, \alpha, \beta$</code> 均为超参数，在原文中分别初值为：<code>$k=2, n=5, \alpha=10^{-4}, \beta=0.75$</code>。在上式中，分母为所有卷积核 (Feature Maps) 的加和，因此 LRN 可以简单理解为一个跨 Feature Maps 的像素级归一化。</p>
<p><strong>开源实现</strong>：</p>
<ul>
<li><i class="icon icon-tensorflow"></i> <a href="https://github.com/tensorflow/models/tree/master/research">tensorflow/models</a>,  <a href="https://github.com/tflearn/tflearn/blob/master/examples/images">tflearn/examples</a></li>
<li><i class="icon icon-pytorch"></i> <a href="https://pytorch.org/docs/stable/torchvision/models.html">pytorch/torchvision/models</a></li>
<li><i class="icon icon-caffe2"></i> <a href="https://github.com/caffe2/models">caffe2/models</a></li>
<li><i class="icon icon-mxnet"></i> <a href="https://github.com/apache/incubator-mxnet/blob/master/example/image-classification/symbols">incubator-mxnet/example</a></li>
</ul>
<h3 id="vgg-net">VGG Net <sup id="fnref1:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup></h3>
<img src="/images/cn/2018-08-25-cnn/vgg-16.png" width="180" style="float:left; margin-right:3em;"/>
<p>左图是 VGG-16 Net 的网络结构，原文中还有一个 VGG-19 Net，其差别在于后面三组卷积层中均多叠加了一个卷积层，使得网络层数由 16 增加至 19。</p>
<p>VGG Net 的主要改变如下：</p>
<ul>
<li>网络层级更深，从 AlexNet 的 8 层增加至 16 层和 19 层，更深的网络层级意味着更强的学习能力，但也需要更多的算力对模型进行优化。</li>
<li>仅使用 3x3 大小的卷积。在 AlexNet 中，浅层部分使用了较大的卷积核，而 VGG 使用了 3x3 的小卷积核进行串联叠加，减少了参数个数。</li>
<li>卷积采样的步长为 1x1，Max Pooling 的步长为 2x2。</li>
<li>去掉了效果提升不明显的但计算耗时的 LRN。</li>
<li>增加了 Feature Maps 的个数。</li>
</ul>
<p><strong>开源实现</strong>：</p>
<ul>
<li><i class="icon icon-tensorflow"></i> <a href="https://github.com/tensorflow/models/tree/master/research">tensorflow/models</a>,  <a href="https://github.com/tflearn/tflearn/tree/master/examples">tflearn/examples</a>, <a href="https://github.com/tensorlayer/awesome-tensorlayer">tensorlayer/awesome-tensorlayer</a></li>
<li><i class="icon icon-keras"></i> <a href="https://www.tensorflow.org/api_docs/python/tf/keras/applications">tf/keras/applications</a>, <a href="https://keras.io/applications">keras/applications</a></li>
<li><i class="icon icon-pytorch"></i> <a href="https://pytorch.org/docs/stable/torchvision/models.html">pytorch/torchvision/models</a></li>
<li><i class="icon icon-caffe2"></i> <a href="https://github.com/caffe2/models">caffe2/models</a></li>
<li><i class="icon icon-mxnet"></i> <a href="https://github.com/apache/incubator-mxnet/tree/master/example/image-classification/symbols">incubator-mxnet/example</a></li>
</ul>
<p style="clear:both;"></p>
<h3 id="network-in-network-nin">Network in Network (NIN) <sup id="fnref:17"><a href="#fn:17" class="footnote-ref" role="doc-noteref">17</a></sup></h3>
<p><img src="/images/cn/2018-08-25-cnn/network-in-network.png" alt="NIN"></p>
<p>NIN 网络的主要改变如下：</p>
<ul>
<li>利用多层的全连接网络替换线性的卷积，即 mlpconv (Conv + MLP) 层。其中卷积层为线性的操作，而 MLP 为非线性的操作，因此具有更高的抽象能力。</li>
<li>去掉了全连接层，使用 Global Average Pooling，也就是将每个 Feature Maps 上所有的值求平均，直接作为输出节点，如下图所示：</li>
</ul>
<p><img src="/images/cn/2018-08-25-cnn/global-average-pooling.png" alt="Global-Average-Pooling"></p>
<ul>
<li>相比 AlexNet 简化了网络结构，仅包含 4 个 NIN 单元和一个 Global Average Pooling，整个参数空间比 AlexNet 小了一个数量级。</li>
</ul>
<p>在 NIN 中，在跨通道的情况下，mlpconv 层又等价于传统的 Conv 层后接一个 1x1 大小的卷积层，因此 mlpconv 层有时也称为 cccp (cascaded cross channel parametric pooling) 层。1x1 大小的卷积核可以说实现了不同通道信息的交互和整合，同时对于输入通道为 <code>$m$</code> 和输出通道为 <code>$n$</code>，1x1 大小的卷积核在不改变分辨率的同时实现了降维 (<code>$m &gt; n$</code> 情况下) 或升维 (<code>$m &lt; n$</code> 情况下) 操作。</p>
<p><strong>开源实现</strong>：</p>
<ul>
<li><i class="icon icon-tensorflow"></i> <a href="https://github.com/tflearn/tflearn/tree/master/examples">tflearn/examples</a></li>
</ul>
<h3 id="googlenet-inception-v1-inception-v3-inception-v4">GoogLeNet (Inception V1) <sup id="fnref1:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>, Inception V3 <sup id="fnref:18"><a href="#fn:18" class="footnote-ref" role="doc-noteref">18</a></sup>, Inception V4 <sup id="fnref:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup></h3>
<p><img src="/images/cn/2018-08-25-cnn/googlenet.png" alt="GoogLeNet"></p>
<p>除了 VGG 这种从网络深度方向进行优化的策略以外，Google 还提出了在同一层利用不同大小的卷积核同时提取不同特征的思路，对于这样的结构我们称之为 Inception。</p>
<p><img src="/images/cn/2018-08-25-cnn/inception-v1-naive-dim-reduction.png" alt="Inception-V1"></p>
<p>上图 (左) 为原始的 Inception 结构，在这样一层中分别包括了 1x1 卷积，3x3 卷积，5x5 卷积和 3x3 Max Polling，使得网络在每一层都能学到不同尺度的特征。最后通过 Filter Concat 将其拼接为多个 Feature Maps。</p>
<p>这种方式虽然能够带来性能的提升，但同时也增加了计算量，因此为了进一步改善，其选择利用 1x1 大小的卷积进行降维操作，改进后的 Inception 模块如上图 (右) 所示。我们以 GoogLeNet 中的 inception (3a) 模块为例 (输入大小为 28x28x192)，解释 1x1 卷积的降维效果。</p>
<p>对于原始 Inception 模块，1x1 卷积的通道为 64，3x3 卷积的通道为 128，5x5 卷积的通道为 32，卷积层的参数个数为：</p>
<p><code>$$ \begin{equation} \begin{split} \# w_{\text{3a_conv_without_1x1}} =&amp; 1 \times 1 \times 192 \times 64 \\ &amp; + 3 \times 3 \times 192 \times 128 \\ &amp; + 5 \times 5 \times 192 \times 32 \\ =&amp; 387072 \end{split} \end{equation} $$</code></p>
<p>对于加上 1x1 卷积后的 Inception 模块 (通道数分别为 96 和 16) 后，卷积层的参数个数为：</p>
<p><code>$$ \begin{equation} \begin{split} \# w_{\text{3a_conv_with_1x1}} =&amp; 1 \times 1 \times 192 \times 64 \\ &amp; + 1 \times 1 \times 192 \times 96 + 3 \times 3 \times 96 \times 128 \\ &amp; + 1 \times 1 \times 192 \times 16 + 5 \times 5 \times 16 \times 32 \\ =&amp; 157184 \end{split} \end{equation} $$</code></p>
<p>可以看出，在添加 1x1 大小的卷积后，参数的个数减少了 2 倍多。通过 1x1 卷积对特征进行降维的层称之为 Bottleneck Layer 或 Bottleneck Block。</p>
<p>在 GoogLeNet 中，作者还提出了 Auxiliary Classifiers (AC)，用于辅助训练。AC 通过增加浅层的梯度来减轻深度梯度弥散的问题，从而加速整个网络的收敛。</p>
<p>随后 Google 在此对 Inception 进行了改进，同时提出了卷积神经网络的 4 项设计原则，概括如下：</p>
<ol>
<li>避免表示瓶颈，尤其是在网络的浅层部分。一般来说，在到达任务的最终表示之前，表示的大小应该从输入到输出缓慢减小。</li>
<li>高维特征在网络的局部更容易处理。在网络中增加更多的非线性有助于获得更多的解耦特征，同时网络训练也会加快。</li>
<li>空间聚合可以在低维嵌入中进行，同时也不会对表征能力带来太多影响。例如，再进行尺寸较大的卷积操作之前可以先对输入进行降维处理。</li>
<li>在网络的宽度和深度之间进行权衡。通过增加网络的深度和宽度均能够带来性能的提升，在同时增加其深度和宽度时，需要综合考虑算力的分配。</li>
</ol>
<p>Inception V3 的主要改进包括：</p>
<ul>
<li>增加了 Batch Normalized 层。</li>
<li>将一个 5x5 的卷积替换为两个串联的 3x3 的卷积 (基于原则 3)，减少了网络参数，如下图所示：</li>
</ul>
<p><img src="/images/cn/2018-08-25-cnn/inception-v3-v1-3x3.png" alt="Inception-V3-3x3"></p>
<ul>
<li>利用串联的 1xn 和 nx1 的非对称卷积 (Asymmetric Convolutions) 替代 nxn 的卷积 (基于原则 3)，减少了网络参数，如下图 (左) 所示：</li>
<li>增加带有滤波器组 (filter bank) 的 Inception 模块 (基于原则 2)，用于提升高维空间的表示能力，如下图 (右) 所示：</li>
</ul>
<p><img src="/images/cn/2018-08-25-cnn/inception-v3-1xn-nx1.png" alt="Inception-V3-1xn-nx1"></p>
<ul>
<li>重新探讨了 Auxiliary Classifiers 的作用，发现其在训练初期并没有有效的提高收敛速度，尽在训练快结束时会略微提高网络的精度。</li>
<li>新的下采样方案。在传统的做法中，如果先进行 Pooling，在利用 Inception 模块进行操作，如下图 (左) 所示，会造成表示瓶颈 (原则 1)；而先利用 Inception 模块进行操作，再进行 Pooling，则会增加参数数量。
<img src="/images/cn/2018-08-25-cnn/inception-v3-reducing-gird-size-old.png" alt="Inception-V3-reducing-grid-size-old"><br>
因此，借助 Inception 结构的思想，作者提出了一种新的下采样方案。下图 (左) 是利用 Inception 的思想进行下采样的内部逻辑，下图 (右) 为同时利用 Inception 思想和 Pooling 进行下采样的整体框架。</li>
</ul>
<p><img src="/images/cn/2018-08-25-cnn/inception-v3-reducing-gird-size-new.png" alt="Inception-V3-reducing-grid-size-new"></p>
<ul>
<li>Label Smoothing 机制。假设标签的真实分布为 <code>$q\left(k\right)$</code>，则对于一个真实标签 <code>$y$</code> 而言，有 <code>$q\left(y\right) = 1$</code>，对于 <code>$k \neq y$</code>，有 <code>$q\left(k\right) = 0$</code>。这种情况会导致两个问题：一个是当模型对于每个训练样本的真实标签赋予全部的概率时，模型将会发生过拟合；另一个是其鼓励拉大最大概率标签同其他标签之间的概率差距，从而降低网络的适应性。也就是说这种情况的发生是由于网络对于其预测结果过于自信。因此，对于一个真实标签 <code>$y$</code>，我们将其标签的分布 <code>$q\left(k | x\right) = \delta_{k, y}$</code> 替换为：
<code>$$ q' \left(k | x\right) = \left(1 - \epsilon\right) \delta_{k, y} + \epsilon u \left(k\right) $$</code>
其中，<code>$u \left(k\right)$</code> 是一个固定的分布，文中采用了均匀分布，即 <code>$u \left(k\right) = 1 / K$</code>；<code>$\epsilon$</code> 为权重项，试验中取为 <code>$0.1$</code>。</li>
</ul>
<p>Inception V4 对于 Inception 网络做了进一步细致的调整，其主要是将 Inception V3 中的前几层网络替换为了 stem 模块，具体的 stem 模块结构就不在此详细介绍了。</p>
<p><strong>开源实现</strong>：</p>
<ul>
<li><i class="icon icon-tensorflow"></i> <a href="https://github.com/tensorflow/models/tree/master/research">tensorflow/models</a>,  <a href="https://github.com/tflearn/tflearn/tree/master/examples">tflearn/examples</a>, <a href="https://github.com/tensorlayer/awesome-tensorlayer">tensorlayer/awesome-tensorlayer</a></li>
<li><i class="icon icon-keras"></i> <a href="https://www.tensorflow.org/api_docs/python/tf/keras/applications">tf/keras/applications</a>, <a href="https://keras.io/applications">keras/applications</a></li>
<li><i class="icon icon-pytorch"></i> <a href="https://pytorch.org/docs/stable/torchvision/models.html">pytorch/torchvision/models</a></li>
<li><i class="icon icon-caffe2"></i> <a href="https://github.com/caffe2/models">caffe2/models</a></li>
<li><i class="icon icon-mxnet"></i> <a href="https://github.com/apache/incubator-mxnet/tree/master/example/image-classification/symbols">incubator-mxnet/example</a></li>
</ul>
<h3 id="deep-residual-net-identity-mapping-residual-net-densenet">Deep Residual Net <sup id="fnref1:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>, Identity Mapping Residual Net <sup id="fnref:20"><a href="#fn:20" class="footnote-ref" role="doc-noteref">20</a></sup>, DenseNet <sup id="fnref:21"><a href="#fn:21" class="footnote-ref" role="doc-noteref">21</a></sup></h3>
<img src="/images/cn/2018-08-25-cnn/residual-network.png" width="300" style="float:left; margin-right:3em;"/>
<p>随着网络深度的不断增加啊，其效果并未如想象一般提升，甚至发生了退化，He 等人 <sup id="fnref2:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 发现在 CIFAR-10 数据集上，一个 56 层的神经网络的性能要比一个 20 层的神经网络要差。网络层级的不断增加，不仅导致参数的增加，同时也可能导致梯度弥散问题 (vanishing gradients)。</p>
<p>这对这些问题，He 等人提出了一种 Deep Residual Net，在这个网络结构中，残差 (residual) 的思想可以理解为：假设原始潜在的映射关系为 <code>$\mathcal{H} \left(\mathbf{x}\right)$</code>，对于新的网络层我们不再拟合原始的映射关系，而是拟合 <code>$\mathcal{F} \left(\mathbf{x}\right) = \mathcal{H} \left(\mathbf{x}\right) - \mathbf{x}$</code>，也就是说原始潜在的映射关系变为 <code>$\mathcal{F} \left(\mathbf{x}\right) + \mathbf{x}$</code>。新的映射关系可以理解为在网络前向传播中添加了一条捷径 (shortcut connections)，如下图所示：</p>
<p><img src="/images/cn/2018-08-25-cnn/residual-block.png" alt="Residual-Block"></p>
<p style="clear:both;"></p>
<p>增加 Short Connections 并没有增加参数个数，也没有增加计算量，与此同时模型依旧可以利用 SGD 等算法进行优化。</p>
<p><img src="/images/cn/2018-08-25-cnn/residual-results.png" alt="Residual-Results"></p>
<p>从 Deep Residual Net 的实验结果 (如上图) 可以看出，在没有加入残差模块的网络中 (上图 - 左) 出现了上文中描述的问题：更多层级的网络的效果反而较差；在加入了残差模块的网络中 (上图 - 右)，其整体性能均比未加入残差模块的网络要好，同时具有更多层级的网络的效果也更好。</p>
<p>随后 He 等人 <sup id="fnref1:20"><a href="#fn:20" class="footnote-ref" role="doc-noteref">20</a></sup> 又提出了 Identity Mapping Residual Net，在原始的 ResNet 中，一个残差单元可以表示为：</p>
<p><code>$$ \begin{equation} \begin{split} \mathbb{y}_{\ell} = &amp; h \left(\mathbb{x}_{\ell}\right) + \mathcal{F} \left(\mathbb{x}_{\ell}, \mathcal{W}_l\right) \\ \mathbb{x}_{\ell+1} = &amp; f \left(\mathbb{y}_{\ell}\right) \end{split} \end{equation} $$</code></p>
<p>其中 <code>$\mathbb{x}_{\ell}$</code> 和 <code>$\mathbb{x}_{\ell+1}$</code> 为第 <code>$\ell$</code> 个单元的输入和输出，<code>$\mathcal{F}$</code> 为残差函数，<code>$h \left(\mathbb{x}_{\ell}\right) = \mathbb{x}_{\ell}$</code> 为一个恒等映射，<code>$f$</code> 为 ReLU 函数。在 Identity Mapping Residual Net，作者将 <code>$f$</code> 由原来的 ReLU 函数也替换成一个恒定映射，即 <code>$\mathbb{x}_{\ell+1} \equiv \mathbb{y}_{\ell}$</code>，则上式可以改写为：</p>
<p><code>$$ \mathbb{x}_{\ell+1} = \mathbb{x}_{\ell} + \mathcal{F} \left(\mathbb{x}_{\ell}, \mathcal{W}_{\ell}\right) $$</code></p>
<p>则对于任意深度的单元 <code>$L$</code>，有如下表示：</p>
<p><code>$$ \mathbb{x}_L = \mathbb{x}_{\ell} + \sum_{i=\ell}^{L-1}{\mathcal{F} \left(\mathbb{x}_i, \mathcal{W}_i\right)} $$</code></p>
<p>上式形式的表示使得其在反向传播中具有一个很好的性质，假设损失函数为 <code>$\mathcal{E}$</code>，根据链式法则，对于单元 <code>$\ell$</code>，梯度为：</p>
<p><code>$$ \dfrac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}} = \dfrac{\partial \mathcal{E}}{\partial \mathbb{x}_L} \dfrac{\partial \mathbb{x}_L}{\partial \mathbb{x}_{\ell}} = \dfrac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}} \left(1 + \dfrac{\partial}{\partial \mathbb{x}_{\ell}} \sum_{i=\ell}^{L-1}{\mathcal{F} \left(\mathbb{x}_i, \mathcal{W}_i\right)}\right) $$</code></p>
<p>对于上式形式的梯度，我们可以将其拆解为两部分：<code>$\frac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}}$</code> 为不通过任何权重层的直接梯度传递，<code>$\frac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}} \left(\frac{\partial}{\partial \mathbb{x}_{\ell}} \sum_{i=\ell}^{L-1}{\mathcal{F} \left(\mathbb{x}_i, \mathcal{W}_i\right)}\right)$</code> 为通过权重层的梯度传递。前一项保证了梯度能够直接传回任意浅层 <code>$\ell$</code>，同时对于任意一个 mini-batch 的所有样本，<code>$\frac{\partial}{\partial \mathbb{x}_{\ell}} \sum_{i=\ell}^{L-1}\mathcal{F}$</code> 不可能永远为 <code>$-1$</code>，所以保证了即使权重很小的情况下也不会出现梯度弥散。下图展示了原始的 ResNet 和 Identity Mapping Residual Net 之间残差单元的区别和网络的性能差异：</p>
<p><img src="/images/cn/2018-08-25-cnn/identity-mapping-residual-network-unit.png" alt="Identity-Mapping-Residual-Net-Unit"></p>
<p>Huang 等人 <sup id="fnref1:21"><a href="#fn:21" class="footnote-ref" role="doc-noteref">21</a></sup> 在 ResNet 的基础上又提出了 DenseNet 网络，其网络结构如下所示：</p>
<p><img src="/images/cn/2018-08-25-cnn/densenet.png" alt="DenseNet"></p>
<p>DenseNet 的主要改进如下：</p>
<ul>
<li>Dense Connectivity：将网络中每一层都与其后续层进行直接连接。</li>
<li>Growth Rate：<code>$H_{\ell}$</code> 将产生 <code>$k$</code> 个 Feature Maps，因此第 <code>$\ell$</code> 层将包含 <code>$k_0 + k \times \left(\ell - 1\right)$</code> 个 Feature Maps，其中 <code>$k_0$</code> 为输入层的通道数。DenseNet 与现有框架的不同之处就是将网络限定的比较窄，例如：<code>$k = 12$</code>，并将该超参数称之为网络的增长率 (Growth Rate)。</li>
<li>Bottleneck Layers：在 3x3 的卷积之前增加 1x1 的卷积进行降维操作。</li>
<li>Compression：在两个 Dense Block 之间增加过渡层 (Transition Layer)，进一步减少 Feature Maps 个数。</li>
</ul>
<p><strong>开源实现</strong>：</p>
<ul>
<li><i class="icon icon-tensorflow"></i> <a href="https://github.com/tensorflow/models/tree/master/research">tensorflow/models</a>,  <a href="https://github.com/tflearn/tflearn/tree/master/examples">tflearn/examples</a>, <a href="https://github.com/tensorlayer/awesome-tensorlayer">tensorlayer/awesome-tensorlayer</a></li>
<li><i class="icon icon-keras"></i> <a href="https://www.tensorflow.org/api_docs/python/tf/keras/applications">tf/keras/applications</a>, <a href="https://keras.io/applications">keras/applications</a></li>
<li><i class="icon icon-pytorch"></i> <a href="https://pytorch.org/docs/stable/torchvision/models.html">pytorch/torchvision/models</a></li>
<li><i class="icon icon-caffe2"></i> <a href="https://github.com/caffe2/models">caffe2/models</a></li>
<li><i class="icon icon-mxnet"></i> <a href="https://github.com/apache/incubator-mxnet/tree/master/example/image-classification/symbols">incubator-mxnet/example</a></li>
</ul>
<h3 id="综合比较">综合比较</h3>
<p>Canziani 等人 <sup id="fnref:22"><a href="#fn:22" class="footnote-ref" role="doc-noteref">22</a></sup> 综合了模型的准确率，参数大小，内存占用，推理时间等多个角度对现有的 CNN 模型进行了对比分析。</p>
<p><img src="/images/cn/2018-08-25-cnn/cnn-accuracy-and-parameters.png" alt="CNN-Accuracy-and-Parameters"></p>
<p>上图 (左) 展示了在 ImageNet 挑战赛中不同 CNN 网络模型的 Top-1 的准确率。可以看出近期的 ResNet 和 Inception 架构以至少 7% 的显著优势超过了其他架构。上图 (右) 以另一种形式展现了除了准确率以外的更多信息，包括计算成本和网络的参数个数，其中横轴为计算成本，纵轴为 Top-1 的准确率，气泡的大小为网络的参数个数。可以看出 ResNet 和 Inception 架构相比 AlexNet 和 VGG 不仅有更高的准确率，其在计算成本和网络的参数个数 (模型大小) 方面也具有一定优势。</p>
<p>文章部分内容参考了 <strong>刘昕</strong> 的 <a href="http://valser.org/2016/dl/%E5%88%98%E6%98%95.pdf"><strong>CNN近期进展与实用技巧</strong></a>。CNN 除了在图像分类问题上取得很大的进展外，在例如：物体检测：R-CNN <sup id="fnref:23"><a href="#fn:23" class="footnote-ref" role="doc-noteref">23</a></sup>, SPP-Net <sup id="fnref:24"><a href="#fn:24" class="footnote-ref" role="doc-noteref">24</a></sup>, Fast R-CNN <sup id="fnref:25"><a href="#fn:25" class="footnote-ref" role="doc-noteref">25</a></sup>, Faster R-CNN <sup id="fnref:26"><a href="#fn:26" class="footnote-ref" role="doc-noteref">26</a></sup>，语义分割：FCN <sup id="fnref:27"><a href="#fn:27" class="footnote-ref" role="doc-noteref">27</a></sup> 等多个领域也取得了不俗的成绩。针对不同的应用场景，网络模型和处理方法均有一定的差异，本文就不再对其他场景一一展开说明，不同场景将在后续进行单独整理。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>LeCun, Y., Bottou, L., Bengio, Y., &amp; Haffner, P. (1998). Gradient-based learning applied to document recognition. <em>Proceedings of the IEEE, 86</em>(11), 2278-2324.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Krizhevsky, A., Sutskever, I., &amp; Hinton, G. E. (2012). Imagenet classification with deep convolutional neural networks. In <em>Advances in neural information processing systems</em> (pp. 1097-1105).&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="http://www.image-net.org/">http://www.image-net.org/</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Simonyan, K., &amp; Zisserman, A. (2014). Very deep convolutional networks for large-scale image recognition. <em>arXiv preprint arXiv:1409.1556.</em>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Szegedy, C., Liu, W., Jia, Y., Sermanet, P., Reed, S., Anguelov, D., &hellip; &amp; Rabinovich, A. (2015). Going deeper with convolutions. In <em>Proceedings of the IEEE conference on computer vision and pattern recognition</em> (pp. 1-9).&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>He, K., Zhang, X., Ren, S., &amp; Sun, J. (2016). Deep residual learning for image recognition. In <em>Proceedings of the IEEE conference on computer vision and pattern recognition</em> (pp. 770-778).&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref2:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Girshick, R., Donahue, J., Darrell, T., &amp; Malik, J. (2014). Rich feature hierarchies for accurate object detection and semantic segmentation. In <em>Proceedings of the IEEE conference on computer vision and pattern recognition</em> (pp. 580-587).&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Girshick, R. (2015). Fast r-cnn. In <em>Proceedings of the IEEE international conference on computer vision</em> (pp. 1440-1448).&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Ren, S., He, K., Girshick, R., &amp; Sun, J. (2015). Faster r-cnn: Towards real-time object detection with region proposal networks. In <em>Advances in neural information processing systems</em> (pp. 91-99).&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Long, J., Shelhamer, E., &amp; Darrell, T. (2015). Fully convolutional networks for semantic segmentation. In <em>Proceedings of the IEEE conference on computer vision and pattern recognition</em> (pp. 3431-3440).&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Vinyals, O., Toshev, A., Bengio, S., &amp; Erhan, D. (2015). Show and tell: A neural image caption generator. In <em>Proceedings of the IEEE conference on computer vision and pattern recognition</em> (pp. 3156-3164).&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p>Ji, S., Xu, W., Yang, M., &amp; Yu, K. (2013). 3D convolutional neural networks for human action recognition. <em>IEEE transactions on pattern analysis and machine intelligence, 35</em>(1), 221-231.&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:13">
<p>Kim, Y. (2014). Convolutional neural networks for sentence classification. <em>arXiv preprint arXiv:1408.5882.</em>&#160;<a href="#fnref:13" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:14">
<p><a href="http://yann.lecun.com/exdb/mnist">http://yann.lecun.com/exdb/mnist</a>&#160;<a href="#fnref:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:15">
<p><a href="https://mlnotebook.github.io/post/CNN1/">https://mlnotebook.github.io/post/CNN1/</a>&#160;<a href="#fnref:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref2:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref3:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref4:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:16">
<p>Goodfellow, I., Bengio, Y., Courville, A., &amp; Bengio, Y. (2016). <em>Deep learning</em> (Vol. 1). Cambridge: MIT press.&#160;<a href="#fnref:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref2:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref3:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:17">
<p>Lin, M., Chen, Q., &amp; Yan, S. (2013). Network In Network. <em>arXiv preprint arXiv:1312.4400.</em>&#160;<a href="#fnref:17" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:18">
<p>Szegedy, C., Vanhoucke, V., Ioffe, S., Shlens, J., &amp; Wojna, Z. (2016). Rethinking the Inception Architecture for Computer Vision. In <em>Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition</em> (pp. 2818–2826).&#160;<a href="#fnref:18" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:19">
<p>Szegedy, C., Ioffe, S., Vanhoucke, V., &amp; Alemi, A. (2016). Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning. <em>arXiv preprint arXiv:1602.07261.</em>&#160;<a href="#fnref:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:20">
<p>He, K., Zhang, X., Ren, S., &amp; Sun, J. (2016). Identity Mappings in Deep Residual Networks. <em>arXiv preprint arXiv:1603.05027.</em>&#160;<a href="#fnref:20" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:20" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:21">
<p>Huang, G., Liu, Z., van der Maaten, L., &amp; Weinberger, K. Q. (2016). Densely Connected Convolutional Networks. <em>arXiv preprint arXiv:1608.06993</em>&#160;<a href="#fnref:21" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:21" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:22">
<p>Canziani, A., Paszke, A., &amp; Culurciello, E. (2016). An Analysis of Deep Neural Network Models for Practical Applications. <em>arXiv preprint arXiv:1605.07678</em>&#160;<a href="#fnref:22" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:23">
<p>Girshick, R., Donahue, J., Darrell, T., &amp; Malik, J. (2014). Rich Feature Hierarchies for Accurate Object Detection and Semantic Segmentation. In <em>Proceedings of the 2014 IEEE Conference on Computer Vision and Pattern Recognition</em> (pp. 580–587).&#160;<a href="#fnref:23" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:24">
<p>He, K., Zhang, X., Ren, S., &amp; Sun, J. (2015). Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition. <em>IEEE Transactions on Pattern Analysis and Machine Intelligence, 37(9)</em>, 1904–1916.&#160;<a href="#fnref:24" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:25">
<p>Girshick, R. (2015). Fast R-CNN. <em>arXiv preprint arXiv:1504.08083.</em>&#160;<a href="#fnref:25" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:26">
<p>Ren, S., He, K., Girshick, R., &amp; Sun, J. (2017). Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks. <em>IEEE Transactions on Pattern Analysis and Machine Intelligence, 39(6),</em> 1137–1149.&#160;<a href="#fnref:26" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:27">
<p>Shelhamer, E., Long, J., &amp; Darrell, T. (2017). Fully Convolutional Networks for Semantic Segmentation. <em>IEEE Transactions on Pattern Analysis and Machine Intelligence, 39(4),</em> 640–651.&#160;<a href="#fnref:27" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>买书，囤书，看书 (Buy Books, Hoard Books and Read Books)</title><link>https://zeqiang.fun/cn/2018/07/buy-books-hoard-books-and-read-books/</link><pubDate>Tue, 10 Jul 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/07/buy-books-hoard-books-and-read-books/</guid><description><![CDATA[
        <p>写这么一篇主要是想自我分析一下和读书有关的毛病，写下来会让自己认识的深刻些。尤其是最近一年，书入库明显高于出库，导致未读完的书都快摆不下了。总结起来主要是两个问题：</p>
<ul>
<li><strong>书买了，不看。</strong></li>
<li><strong>书看了，看不完。</strong></li>
</ul>
<h1 id="买书">买书</h1>
<p>先聊聊为啥买书，简单通俗的解释就是“想看”。我认为，这么回答一点毛病都没有，因为这是我们去探索这个未知的世界最容易的方式。古人云：“读万卷书，行万里路”，都是探索这个世界的方式，后来细查得知这句话源自于<a href="https://zh.wikipedia.org/zh/%E8%91%A3%E5%85%B6%E6%98%8C"><strong>董其昌</strong></a>谈及绘画之道时所说 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>：</p>
<blockquote>
<p>画家六法，一曰气韵生动。气韵不可学，此生而知之，自然天授，然亦有学得处。**读万卷书，行万里路。**胸中脱去尘浊，自然丘壑内营，成立郛郭，随手写去，皆为山水传神。</p>
</blockquote>
<p>为什么说读书是其中最容易的方式呢？最主要就是经济实惠，你不需要太多的成本，包括财物和时间，当然是相对行万里路而言。这里谈及的书我不想把杂志和技术书籍囊括进来，于我而言，杂志是消磨零散时间的读物，例如：如厕，等车；而技术类的书籍有需要比较系统的整理和笔记，甚至需要反复去看和理解。其他书也可以反复看，有时候我会把喜欢的书再翻一遍，而且每一遍都会有新的发现。所以，想去买书至少能够说明你还有对知识的渴望和对未知探索的兴趣。这点我认为很重要，如果你连念想都没了，那就什么都没了。</p>
<h1 id="囤书">囤书</h1>
<p>囤书，就是买得多了，而且略微上瘾，想着可能买了就等于已经把其中的知识吃进脑子了吧。这点和我办健身卡很像，感觉卡办了，八块腹肌就有了似的，不过还好，至少我把跑步坚持下来了，关于这个类比的问题我们下面再详谈。其实，囤书本身也没有错，什么 618 啊，双 11 啊，书就是便宜吗，多买点实惠。错就错在了我们只做了“囤”的前半部，丢失了后半部。“囤”的后半部我认为是计划的制定，这其实直接决定着我们后续要说的执行。就和囤粮食一样，我们是做了详细的计划去应对未来的粮食短缺的，而不是今年收成好了就一定多囤一点，不过似乎也会有这样的趋势，和上面说书的价格类似吗。</p>
<p>这里说的意思就是计划的重要性，不要在脑子里想，我要干什么干什么，尤其是和我脑子一样不好用的同学，还是先写下来，坚持按照计划执行一段时间，等熟悉了，习惯养成了，再靠脑子才比较好。想想我读书最“舒服” (感觉写读的最快又不太合适，毕竟不应该以读多少，读多块论好坏) 的一段时间是我研究生期间的第一份实习，当时每天翻越 4 条地铁，从昌平线的沙河高教园到中关村。每天都很规律，早起听一路的歌，在 13 号线的换乘站看着七八辆车过去，然后被后面的人成功的推上列车；晚上也带上耳机，但不放歌，为的是减少噪音，上车找个角落，不坐，站着，用手机看一路的电子书。换乘点停一停，甚至还能简单的做些思考。就这样，不算长的实习期里看完了好几本书。这段时间虽然没有形成规范的计划，但是每天规律的上下班无形之中就促成了计划的按时执行。</p>
<h1 id="看书">看书</h1>
<p>回到最初的两个毛病，“<strong>不看</strong>”和“<strong>看不完</strong>”。好吧，先找些客观理由，时间真的不多。记得当时看《解忧杂货铺》的时候，应该是晚上十点多开始看到，一晚上看完了，结束应该已经两三点了。作为一个靠撸代码为生的人，明天还要准时上班工作，在中国这样一个竞争激烈市场中，报答还是和加班呈强正相关的。读《解忧杂货铺》的时候，其实是不知不觉就读完了，当然不知不觉也就两三点了。所以，我认为能够促使你读完的最重要的还是内容本身，是你喜欢的风格，就不会错过。尤其是书的前几章，前面写的不好，可能就会让你失去继续读下去的兴趣。还有就是书的厚度，之前买了几本很喜欢的书，喜欢是从其题材和评价先预支的结论，但碍于实在太厚，就迟迟未读，也怕看了没看完，还不如等做好准备再去读。</p>
<p>所以如上文提及的，读书之于健身，能把跑步坚持下来是因为时间不长，30 分钟能跑个不到 6 公里，跑完还很舒服，会感觉收益很高。而以增肌为目的的撸铁，对于我这种本来上肢力量就很弱的男生，短时间不见效果就放弃了。但无论是健身还是读书，这个想法也都是不对的，而且我是“一直”都知道是不对的。</p>
<p>对于我，简单思考之后，我想最主要的问题是缺乏一种强烈的约束力去推着你动起来。我算是一个自制力一般的人，有人鞭笞我几下，效果一下子就会上来，没人鞭笞我了，就容易出现不好不差的状态。不过与其说是缺乏外界的动力，不如说还是没有触及当下的痛点。我没有八块腹肌，但也没有肚子，做不到穿衣显瘦，脱衣有肉，但也还算看得过去。同时我知道，程序员不动是一定会长肚子的，身边的例子比比皆是。所以作为半个外貌协会的会员，我能够坚持跑步。同样，读书真的是我喜欢的事情，看电影也一样，两者各自有各自体验，但当下生活的压力促使我会投入更多的时间在工作相关的领域，自然留出来给读书的时间就少了。</p>
<p>总结起来，如何改变这个状态，还是要制定个读书计划的，各种情况难免无法严格执行，但尽量去做。还有就是当你不开心的时候，我认为是适合读书，这个想法来自于一次周末的阅读体验。五天的工作日结束，甚是不爽，为啥就不说了，总之不爽，周末来公司，本来想再敲几行代码，心情原因，效率如实不高。翻开了之前没有带回家的冯唐的《在宇宙间不易被风吹散》，冯唐的东西总是有点他特别的地方。一个下午，几杯咖啡，一本书，慢慢的就过了，然后不开心的东西就散了。还不忘发个朋友圈装下文艺青年：</p>
<blockquote>
<p>人燥的时候就多做些通灵的事，无需如鼓琴，对画，临帖一般高雅，一书，一茶足矣，静下来，腐朽之气全无。</p>
</blockquote>
<p>最后，再补一下我对读书这件事我们需要以何种态度去对待的看法，我认为：<strong>读书，不是非做不可的事，而是真正想要去做的事。</strong></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>《画旨》- 董其昌&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>Play Safe, Smart Choice &amp; Yuppie</title><link>https://zeqiang.fun/cn/2018/06/play-safe-smart-choice-and-yuppie/</link><pubDate>Sat, 30 Jun 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/06/play-safe-smart-choice-and-yuppie/</guid><description><![CDATA[
        <p>最近是看了 Youtube 上 <a href="https://www.youtube.com/user/WongFuProductions">Wong Fu Productions</a> 的一个系列视频 <a href="https://www.youtube.com/playlist?list=PLSHabwxChOtX1HLAKXle8-w7FvQ5Xg8gD">Yappie</a>，一下子把脑子中包括很久之前的一些思考就全都串联回忆起来了，所以就写了本文，把这些凌乱的思考拼凑起来。截止到本文写完，Yappie 已经出了两集，视频可能需要梯子，为了不影响理解，简单勾勒一下剧情。</p>
<p>主角是一个名叫 Andrew 的亚裔美国人，他和女友 (当然后面知道才没交往多久，然后就分手了) 去听了一个关于美国亚裔 (Aisan Americans) 的一些特殊的境遇问题的 Talk Show。但 Andrew 似乎对这场演讲并不感冒，中途手机中球赛的声音打断了演讲，并被演讲者问及了姓名。Andrew 告知了姓名，演讲者随即用个玩笑回应了他的不礼貌打断，说一个美国亚裔中有 1/13 的概率叫 Andrew，有 1/5 的概率会是一名工程师，会来自于 San Gabriel Valley，喜欢篮球，<a href="https://zh.wikipedia.org/zh/%E5%A4%A7%E4%BA%BA%E5%B0%8F%E5%AD%A9%E9%9B%99%E6%8B%8D%E6%AA%94">Boyz II Men</a> 和宝马 (然而这一系列看似玩笑的猜测，后面剧情验证，他就是这样的一个 Andrew)。演讲过后 Andrew 的女友和演讲者做着进一步的沟通，Andrew 过去为自己的不礼貌表示道歉，并想融入他们的谈话，但却完全和他们不在一个频道。然后，就没有然后了，他和女友就 OVER 了，并且女友说自己就不应该和一个 Yuppie 男约会。所以，什么是 Yuppie？其实他们的这段分手对白说的就很清楚了：</p>
<blockquote>
<p><strong>Girl</strong>: I knew I shouldn&rsquo;t have swiped on a Yuppie. <br/>
<strong>Andrew</strong>: A what? <br/>
<strong>Girl</strong>: You&rsquo;re a Yuppie, Andrew. <br/>
<strong>Andrew</strong>: Are we doing nicknames now or &hellip;? <br/>
<strong>Girl</strong>: A young Asian professional who acts like a Yuppie? Cause all you care about is earning a nice salary, and buying a nice car, and settling down in a nice suburb. <br/>
<strong>Andrew</strong>: That sounds like a nice, normal person. What&rsquo;s so wrong with that? <br/>
<strong>Girl</strong>: It&rsquo;s not wrong. It&rsquo;s just &hellip; safe. Listen, we&rsquo;re just &hellip; not the right fit. I need to be with someone who you know &hellip; cares, more about the world.</p>
</blockquote>
<p><img src="/images/cn/2018-06-30-play-safe-smart-choice-and-yuppie/yappie-ep1-screen-shot.png" alt="yappie-ep1-screen-shot"></p>
<p>后面，Andrew 回到公司后，貌似是人力的小姐姐要找他聊天，关于之前 Andrew 想进行岗位异动的事情。人力小姐姐说今年内是无法进行了，但 Andrew 也只是叹了叹气，说 that&rsquo;s fine。这一次，他又选择了一个 Safe 的方式，或许他在想至少还能保留现在的职位。但在和人力小姐姐沟通的后面，他已经开始走神思考着什么了。同时，在第 2 集中得知 Andrew 与前女友 Lana 分手貌似也是因为对他这种 Safe 的生活方式的不认同，后面的剧情中 Andrew 已经开始对着这种 Safe 的生活方式进行了改变，就不在详细介绍了。</p>
<h2 id="对与错-好与坏">对与错，好与坏</h2>
<p>Andrew 之所以很困惑为什么会叫他 Yuppie，是因为他认为他现在所做的事情 (赚钱，买好车，买豪宅) 是再正常不过的事情了，是对的 (Right)，是好的 (Good)，是明智的选择 (Smart Choice)。他不明白，为什么有人会对这些再正常不过的事情产生质疑。但是大家有没有想过：</p>
<blockquote>
<p>什么是对，什么是错，什么是好，什么又是坏？</p>
</blockquote>
<p>让我真正重视这些问题是源自哈佛大学的一门公开课 <a href="https://college.harvard.edu/justice-whats-right-thing-do"><strong>Justice: What&rsquo;s The Right Thing To Do?</strong></a>，现在 Michael 教授也出了这门课程对应的书 <a href="https://book.douban.com/subject/5975978/"><strong>《正義: 一場思辨之旅》</strong></a>。课程的一开始就抛出了经典的 <a href="https://zh.wikipedia.org/zh/%E6%9C%89%E8%BD%A8%E7%94%B5%E8%BD%A6%E9%9A%BE%E9%A2%98">电车难题 (Trolley Problem)</a>，问题如下图所示：</p>
<p><img src="/images/cn/2018-06-30-play-safe-smart-choice-and-yuppie/trolley-problem.png" alt="trolley-problem"></p>
<p>原始版本的两个问题都是有一辆快速行驶的电车，在电车当前行驶的轨道前面被绑了 5 个人，如果不采取任何措施的话，这 5 个人就会被电车轧死。假设你是唯一一个能够影响这件事情发生的人，在上图左面中的情况，你能够操纵一个轨道变换器，能够让电车变到另一条轨道上，但是这条轨道上也绑了一个人，电车会把这个人轧死；在上图右面中的情况，你站在一个桥上，你面前有个胖子，如果你把他退下去，靠他的身体就能逼停电车，但这个胖子会死掉，同时如果你跳下去是无法逼停电车的。问题就是，针对不同的情况你，你会如果选择？</p>
<p>在课堂上统计的结果是，左面的情况会有更多的人选择变换轨道，牺牲一个人，挽救 5 个人；而右面的情况则会有更少的人选择去推那个胖子。我会有同样的选择，理由是右面的情况另外一个人的死亡会和我“看似”有更直接的关系，因此导致我的负罪感也会更强，课程的后面又从功利主义和个人权利的角度对相关问题进行了讨论。</p>
<p>当然抛出这个问题的目的不是想让我们陷入这个哲学的问题无法自拔，而是说我们是不是应该想想是不是没有所谓的“真”的“对”，当然这有可能陷入“循环嵌套”的困境，什么又是“真”，不必纠结这个，理解这个意思就好。或许有人会说，“对”就是“对”啊，怎么还会有“真”的“对”，其实不难解释，学了这么多年数学，我们都清楚：结论是建立在一定的前提假设下的，如果假设变了，那么结果的“真”与“假”也可能会发生变化。除非，对，除非前提是公理或者公设，无需证明就是对的，好吧，那是不是又要陷进去了，“公理”就是“真”的“公理”吗？不要陷进去，就假设“公理”“真”的是“公理”，那么 Andrew 就是把太多东西当成“公理”了，我们很多人都是，也许亚裔，甚至是中国人这方面会更严重一些。那么，这个问题出在了哪儿呢？</p>
<h2 id="思辨精神与教育">思辨精神与教育</h2>
<p>问题就出在了我们的思辨精神，或者说我们的教育，当然这里我们并不会过多的去批判教育的问题，因为存不存在问题需要你去思考。我们从小接触的教育是这样的，至少我是这样的：最重要的是学习，或者说考试拿高分，这是“绝对”“正确”的，因此如果考少了，你就有可能被老师或者父母揍一顿。同样，在父母的眼里“棍棒底下出孝子”可能是“绝对”“正确”的，因此才会揍我们。所以，为了避免皮肉之苦，就只能好好学习，同时不允许一切有碍我们学习的东西出现。学习本身“似乎”是“绝对”“正确”的，尤其是在一定的社会条件下，好好学习可能是一个我们实现自我实现的一个很好的途径。我只是认为，这不能成为我们不做一些事情的理由，看动画片“似乎”会让我们学习的时间变少，所以可能很多家长的做法就是不让平时看动画片。然而，在这样的情况下，我的学业也还是一般般 &#x1f602;。</p>
<p>如果我们一昧的强调某些事情是“绝对”“正确”的，一些事情是“绝对”“错误”的，最终导致的就是我们会终将失去思辨的精神。对于个人的成长来说，在前期可能还不会发现有多大的影响，但当你脱离父母，学校等这些保护伞后，我认为思辨精神可以说是能够让我们理解生存的本质，生活的意义以及如何获得多彩人生的重要思想。</p>
<p>下图是我从 <a href="http://birdboxstudio.com/blog/">Birdbox Studio</a> 的 <a href="http://birdboxstudio.com/bird-box-shorts/wildebeest/">Wildbeest</a> 视频中截取的一段做成的动图。动画的内容比较简单，但却相当的讽刺：前面的角马坚持河里面的是鳄鱼，但后面的角马却“一再”地说是木头，无论是出于想验证自己的想法还是对自己想法的质疑，前面的角马最后被鳄鱼吃掉了，这时后面的角马认识到了这“真的”是鳄鱼，更讽刺的是再后面的角马却又说这是木头。</p>
<p><img src="/images/cn/2018-06-30-play-safe-smart-choice-and-yuppie/wildebeest-from-birdbox-studio.gif" alt="wildebeest"></p>
<p>我感觉用这个小视频来比喻我们的问题还是很恰当的，一昧的以家长的姿态强调什么是“对的”，什么是“错的”，最终会让孩子失去思辨的精神。而且这种问题是会传递的，可能将来你的孩子对于他的孩子也会存在这种“不当”的教育，又来了，我所说的这种“不当”是不是“真”的“不当”，取决于你的思考和理解。</p>
<p>当下，随着父母的知识水平的不断提升，开始认识到单纯的自然科学学习已经不够了，又开始给孩子们报各种兴趣班，而且是强制报名让孩子去学习。我认为这是同样的问题，只不过是父母角度的“绝对”“正确”又多了一项，就是还必须得有个特长。对于孩子而言，父母毕竟走过了更多的人生道路，也遇见过更多的问题，有远比孩子丰富的阅历，但我认为更加合适的方式是去“引导”，去让孩子尝试不同的东西，同时也给孩子一个思考和选择的机会。</p>
<p>其实，我认为思辨精神其实是我们人与生俱来的，最明显的表现就是小孩子永远爱问十万个为什么？所以，我认为对待孩子的问题要有耐心，要不厌其烦的去回答，这样才不会让孩子的这种性格消失。而且这是个正向反馈的过程，问的多了，得到的解释多了，思考多了，才会在下一次提问前更多的思考，提出少但更具意义的问题，同时又不会失去这种思辨的精神。</p>
<p>我还没有小孩，所以说了一大堆孩子教育的问题可能会有很多不当。除了孩子的思辨教育，作为一个成年人，对于我们自己又该如何去做呢？对于 Andrew，面对两任女友的不满，又该如何去改变呢？</p>
<p><strong>P.S.</strong> 关于思辨精神，推荐读过的一本不错的书，是一位台湾公民课教师 <strong>黃益中</strong> 的 <a href="https://book.douban.com/subject/26374212/"><strong>《思辨: 熱血教師的十堂公民課 》</strong></a>，我感觉于自己和于未来孩子的教育都很有帮助 。当然对于思辨，我认为《中庸》中的「博學之，審問之，<strong>慎思之，明辨之</strong>，篤行之」与其含义是相通的，所以我们老祖宗很早就认识到了思辨的重要性。</p>
<p><strong>P.S.</strong> 这段里面很多引号，引起来说明这些是我的观点，之于你，请慎思明辨。</p>
<h2 id="迈出改变的一步">迈出改变的一步</h2>
<p>该如何去做，答案就是迈出改变的一步。当然我们首先要认识到我们需要改变，这点其实不易，因为我们并没有接触到很好的思辨教育，至少我是这样的，在毕业后才意识到思辨的重要性。很难认识到自己需要改变，我认为有如下几个原因：</p>
<ul>
<li><strong>对于生活的还算满意</strong></li>
</ul>
<p>有时候我们会遇到不爽，但不爽也会很快过去，因为我们会说：好吧，其实整体来看，Life is not bad. 是的，就是这个 NOT BAD 会让我们习惯下去，不去改变。在 Andrew 参加的 Talk Show 中，如演讲者说的那样，作为 Model Minority (模范少数族裔)，我们过的还不错。</p>
<ul>
<li><strong>人际不没有想象的那么美好</strong></li>
</ul>
<p>我们有时候是很难发现自己的问题的，就像我们这群撸代码的一样，很难找到自己代码中所有的毛病，这也是测试工程师存在的理由。但在生活中，发现你的问题的人应该不少，但能够真诚的告诉你的我认为不会很多，这也就是为什么人生得一二知己足矣。Andrew 很幸运，两任女友都直白的和他说了真实的分手理由，没有搪塞。所以一个人说你有问题，你不一定真的有问题，但也需要反思，当说你有同一个问题的人多了，你就真的要反思了。炮弹落到同一个坑里面的概率很小，当真的都精准的打击到同一个地方时，那一定是这个地方的问题才让它成为众矢之的。</p>
<ul>
<li><strong>顾虑太多</strong></li>
</ul>
<p>认识到了问题，但还是不想改变，理由是如果我这样做了，那 XXX 怎么办 (比如家庭，工作等等)？会给自己找很多很多“理所当然”“对”的理由，我就经常这样，但我在努力改变中。太多的顾虑其实只是借口，可能当你走下去的时候，会发现 XXX 根本不是问题，甚至会比当下更好。当然，也可能会真的发生一些问题，如果真的发生了，要么想办法再解决它，要么再放弃之前的抉择，我认为也还来得及。所以，也不用把年龄当作借口，since it&rsquo;s nevery too late to do it.</p>
<p>在迈出这一步的路上，我做的并不好，但我已经认识到了，所以在努力改变中。简单而言，对于一个事情，我们可以怎么做？</p>
<blockquote>
<p><strong>THINK</strong> -&gt; <strong>CHANGE</strong> -&gt; IF &#x1f44d; THEN &#x1f60e; ELSE GOTO <strong>THINK</strong> AGAIN</p>
</blockquote>

        ]]></description></item><item><title>基于 PyQt5/PySide2 和 QML 的跨平台 GUI 程序开发</title><link>https://zeqiang.fun/cn/2018/05/cross-platform-gui-application-based-on-pyqt/</link><pubDate>Sun, 27 May 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/05/cross-platform-gui-application-based-on-pyqt/</guid><description><![CDATA[
        <p>先聊聊写界面化程序的目的，在 B/S 结构软件盛行的今天，C/S 结构的软件还有人用吗？答案是肯定的，至少你想用 B/S 结构的软件的时候你得有个 C/S 结构的浏览器，对吧？这样说显得有点抬杠，当然，我认为最重要的还是“简单”，或者说“用户友好”。再 Geek 的人应该也喜欢有的时候偷懒，虽然我称不上 Geek，但也经常在黑框框中不用鼠标敲着各种代码，但是还是希望能够有些小工具只要能够点个几下就能帮忙干些事情的。至于对于更普通的用户而言，就应该更加希望能够用最“简单，清晰，明了”的方式“快速”的完成一项任务，有点像 Windows 用户把桌面上的快捷方式拖到回收站，然后和我说：好了，程序卸载了，我只能回答说：或许你该换个 MAC。</p>
<h2 id="exclamation-更新-exclamation">&#x2757; 更新 &#x2757;</h2>
<p><a href="https://github.com/leovan/SciHubEVA">SciHubEVA</a> 最新版本已经采用 <a href="https://wiki.qt.io/Qt_for_Python">PySide2</a> 进行改写，Windows 版本安装包构建工作迁移至 <a href="http://www.jrsoftware.org/isinfo.php">Inno Setup 6</a>，更多变更请参见 <a href="https://github.com/leovan/SciHubEVA/blob/master/CHANGELOG.md">CHANGELOG</a>。</p>
<h2 id="跨平台-gui-程序开发方案选型">跨平台 GUI 程序开发方案选型</h2>
<p>所以，写个带界面的小工具就是把你的想法更好的服务自己和别人的一个好途径，那么问题来了，对于我这做算法的种业余编程选手，怎么搞定界面化应用呢？虽然是业余编程选手，也也一路从 Logo，Basic，VB，C/C++，Java，R，Python 等等走来，当然很多都是从入门到放弃，总之对于同时需要兼顾一定美感的我，总结了几种跨平台界面化的解决方案。</p>
<ol>
<li><a href="http://www.oracle.com/technetwork/java/javafx/overview/index.html">JavaFX</a>，基于 JVM，一次编译处处运行，配合 Material Design 风格的 <a href="https://github.com/jfoenixadmin/JFoenix">JFoenix</a>，应该是能写出很漂亮的界面的。</li>
<li><a href="https://www.qt.io/">Qt</a>，一次编写处处编译，配合 <a href="http://doc.qt.io/qt-5/qtquick-index.html">Qt Quick</a> 和 <a href="http://doc.qt.io/qt-5/qtqml-index.html">QML</a>，可以把前后端分离。原生 C++ 语言支持，同时有 Python 绑定，对于 Python 比较熟的同学相对友好。界面风格上在较新的 Qt Quick 中也支持了 <a href="http://doc.qt.io/Qt-5/qtquickcontrols2-material.html">Material Design 风格</a>。</li>
<li><a href="https://electronjs.org/">Electron</a>，使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架，很多优秀的应用都是用这个来搞的，例如：<a href="https://github.com/Microsoft/vscode">Visual Studio Code</a>，<a href="https://github.com/zeit/hyper">Hyper</a> 等。</li>
</ol>
<p>我不认为这 3 种方法孰优孰劣，因为毕竟我们的目的是快速的搞定一个漂亮的小工具，因此到底选哪个完全取决于个人对相关技术的熟悉程度。因此，对于我这个搞算法的，最终选择了 Qt 的 Python 绑定 <a href="https://riverbankcomputing.com/software/pyqt/intro">PyQt</a>。作为 R 的忠实用户，实在是没找到特别好的解决方案，只能找个借口说我们 R 就不是干这个用的&hellip;&hellip;</p>
<h2 id="环境配置">环境配置</h2>
<p>当然选择 PyQt 也是有些个人的倾向在里面的，写 C++ 的年代就用过 Qt，对于原理多少有些了解。不过针对 PyQt，以及其与 Qt Quick 和 QML 的结合使用在后面开发时发现相关文档比较少，只能一步一步地趟雷了。毕竟要做跨平台的 GUI 程序开发，因此本文会针对 macOS 和 Windows 两个系统做相关说明，Linux 系统由于发行版本太多就不做说明了，大部分情况应该和 macOS 类似。</p>
<ul>
<li>Python (开发语言)</li>
</ul>
<p>Python 的版本选择了 3.5，因为在后面选择 3.6 时发现编译打包的时候会有些错误，没有细究，简单 Google 了此类问题，发现回退到 3.5 版本就没问题了，可能需要相关打包工具的更新才能有对 3.6 更好的支持。如果使用 Conda 建立虚拟环境，建议新建一个干净的 Python 3.5 的环境。</p>
<ul>
<li>Qt 和 PyQt (界面化)</li>
</ul>
<p>Qt 和 PyQt 均采用比较新的版本，版本号需大于 5.10。Qt 直接从官网下载安装即可，理论上不需要安装 Qt，因为 PyQt 中包含了运行时环境，安装 Qt 的目的是为了使用其可视化的 Qt Creator，设计界面的时候会比较方便。如果使用 Conda 建立 Python 虚拟环境，请使用 pip 安装 PyQt 的对应版本，Conda 中的 PyQt 的版本相对较低，一些新的 Qt 特性不支持。</p>
<ul>
<li>PyInstaller (编译打包)</li>
</ul>
<p><a href="https://www.pyinstaller.org/">PyInstaller</a> 是一个用于打包 Python 代码到一个本地化可执行程序的工具，安装其最新版本即可：<code>pip install PyInstaller</code>。</p>
<ul>
<li>appdmg 和 NSIS (安装包制作)</li>
</ul>
<p><a href="https://github.com/LinusU/node-appdmg">appdmg</a> 是 macOS 下一个用于制作 DMG 镜像的工具，使用前先安装 <a href="https://nodejs.org">Node.js</a>，再通过 <code>npm install -g appdmg</code> 安装最新版即可。<a href="https://sourceforge.net/projects/nsis/">NSIS</a> 是 Windows 下一个用于制作安装包的工具，NSIS 的一个问题是不支持 Unicode，因此对于包含中文字符的脚本需要以 GBK 编码格式保存。Unicode 版本的 NSIS 为 <a href="http://www.scratchpaper.com">Unicode NSIS</a>，不过 Unicode NSIS 已经长时间未更新，因此本文依旧将 NSIS 作为安装包制作工具。</p>
<h2 id="界面设计">界面设计</h2>
<p>通过需求分析，整个工具最核心的两个界面为程序主界面和配置信息界面：</p>
<p><img src="/images/cn/2018-05-27-cross-platform-gui-application-based-on-pyqt/app.png" alt="APP"></p>
<p>程序主界面包含了待搜索的信息，保存的路径，相关的按钮和日志输出。</p>
<p><img src="/images/cn/2018-05-27-cross-platform-gui-application-based-on-pyqt/preferences.png" alt="PREFERENCES"></p>
<p>配置信息界面以配置项的分组不同分别包括通用，网络和代理等相关的配置信息更改。</p>
<p>整个界面设计采用了 Google 的 <a href="https://material.io/design/">Material Design</a> 风格，尤其是在没有 UI 支援的情况下，使用这个风格至少不会让你的应用太丑。在 PyQt 中，可以通过 <a href="http://doc.qt.io/Qt-5/qtquickcontrols2-styles.html">多种方式</a> 启用 Material Design 风格。</p>
<h2 id="程序开发">程序开发</h2>
<p>本文以 <a href="https://github.com/leovan/SciHubEVA">Sci-Hub EVA</a> 作为示例介绍 PyQt 的跨平台 GUI 程序开发。Sci-Hub EVA 是一个利用 Sci-Hub API 下载论文的界面化小工具，功能相对简单。首先介绍一下工程的目录：</p>
<pre><code class="language-txt">docs\
images\
translations\
ui\
BUILDING.md
Info.plist
LICENSE
README.md
SciHubEVA.conf
SciHubEVA.cpp
SciHubEVA.dmg.json
SciHubEVA.nsi
SciHubEVA.pro
SciHubEVA.qrc
SciHubEVA.win.version
requirements.txt
scihub_add_scihub_url.py
scihub_api.py
scihub_conf.py
scihub_eva.py
scihub_preferences.py
scihub_resources.py
scihub_utils.py
version_updater.py
</code></pre>
<p>其中，<code>docs</code> 目录为项目的一些文档，<code>images</code> 目录为项目的相关图片文件，<code>translations</code> 目录为项目的 i18n 翻译文件，<code>ui</code> 目录为相关的界面文件 (QML 文件)，<code>Info.plist</code> 为 macOS 程序信息文件，<code>SciHubEVA.conf</code> 为程序配置文件，<code>SciHubEVA.cpp</code> 为 Qt 生成的 C++ 主文件，<code>SciHubEVA.dmg.json</code> 为利用 appdmg 制作 DMG 镜像的配置文件，<code>SciHubEVA.nsi</code> 为利用 NSIS 制作 Windows 安装包的脚本文件，<code>SciHubEVA.pro</code> 为程序的 Qt 主项目文件，，<code>SciHubEVA.qrc</code> 为程序的资源文件，<code>SciHubEVA.win.version</code> 为打包 Windows 的版本信息文件，<code>requirements.txt</code> 为 Python 依赖包信息文件，<code>scihu_*.py</code> 为程序实现相关 Python 代码，<code>version_updater.py</code> 为版本更新的小工具。</p>
<p>下文中不会介绍具体的业务逻辑代码，而是对开发过程中的一些核心点和注意事项进行简单的介绍。</p>
<h3 id="python-与-qml-通信">Python 与 QML 通信</h3>
<p>首先，对于每一个界面 (QML 文件)，我们都有一个与之对应 Python 文件 (除非该页面没有具体的业务逻辑，例如：<code>ui\SciHubEVAAbout.qml</code> 为关于页面，<code>ui\SciHubEVAMenuBar.qml</code> 为菜单栏)，以主页面 (<code>ui\SciHubEVA.qml</code> 和 <code>scihub_eva.py</code>) 为例，我们为每个界面创建一个类，同时该类集成自 Qt 的一个基类：</p>
<pre><code class="language-python">class SciHubEVA(QObject):
    pass
</code></pre>
<p>Python 代码同界面交互的核心是通过 Qt 的 <a href="http://doc.qt.io/qt-5/signalsandslots.html"><strong>信号与槽</strong></a>，同样在 PyQt 中也是利用 <a href="http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html">相同的机制</a>。简单的理解 PyQt 与 QML 的信号与槽，可以认为<strong>信号</strong>就是<strong>函数的定义</strong>，<strong>槽</strong>就是<strong>函数的实现</strong>。同时，信号和槽往往会位于不同的地方，例如：信号定义在 Python 中，则对应的槽会在 QML 中，反之亦然，当然这并不是一定的。两者通过 <code>connect()</code> 函数连接起来，当触发一个信号时，槽就会接受到信号传递的参数，并执行槽里面相应的逻辑。</p>
<h3 id="i18n">i18n</h3>
<p>Qt 对于多语言支持比较完善，在 QML 中对于需要翻译的地方利用 <code>qsTr()</code> 函数处理待翻译的文本即可，例如：</p>
<pre><code class="language-qml">Label {
    id: labelQuery
    text: qsTr(&quot;Query: &quot;)
}
</code></pre>
<p>在 Python 代码中，对于继承自 <code>QObject</code> 的类，可以利用基类中的 <code>tr()</code> 函数处理待翻译的文本即可，例如：</p>
<pre><code class="language-python">self.tr('Saved PDF as: ')
</code></pre>
<p>同时将具有待翻译文本的文件加入到 <code>SciHubEVA.pro</code> 的主工程文件中，用于后续翻译处理：</p>
<pre><code class="language-text">lupdate_only {
SOURCES += \
    ui/SciHubEVA.qml \
    ui/SciHubEVAAbout.qml \
    ui/SciHubEVAMenuBar.qml \
    ui/SciHubEVAPreferences.qml \
    ui/SciHubEVAAddSciHubURL.qml \
    scihub_api.py
}

TRANSLATIONS += \
    translations/SciHubEVA_zh_CN.ts
</code></pre>
<p>因为 Python 代码中也有需要翻译的文件，因此我们需要运行如下命令生成翻译的源文件：</p>
<pre><code class="language-bash">lupdate SciHubEVA.pro
pylupdate5 SciHubEVA.pro
</code></pre>
<p>这样在 <code>translations</code> 目录即可生成待翻译的源文件 (ts 文件)，利用 Qt 自带的 Liguist 可以对其进行编辑，翻译并保存后，利用如下命令生成翻译的结果文件：</p>
<pre><code class="language-bash}">lrelease SciHubEVA.pro
</code></pre>
<p>在 <code>translations</code> 目录即可生成待翻译的结果文件 (qm 文件)。</p>
<h3 id="资源文件">资源文件</h3>
<p>在 GUI 编程中，我们不可避免的会使用到各种各样的资源，例如：图片，音频，字体等等。Qt 中提供了一种<a href="http://doc.qt.io/qt-5/resources.html">资源管理方案</a>，可以在不同场景下使用 (Python 和 QML 中均可)。<code>SciHubEVA.qrc</code> 定义了所有使用到的资源：</p>
<pre><code class="language-xml">&lt;RCC&gt;
    &lt;qresource prefix=&quot;/&quot;&gt;
        &lt;file&gt;ui/SciHubEVA.qml&lt;/file&gt;
        &lt;file&gt;ui/SciHubEVAMenuBar.qml&lt;/file&gt;
        &lt;file&gt;ui/SciHubEVAAbout.qml&lt;/file&gt;
        &lt;file&gt;ui/SciHubEVAPreferences.qml&lt;/file&gt;
        &lt;file&gt;ui/SciHubEVAAddSciHubURL.qml&lt;/file&gt;
        &lt;file&gt;images/about.png&lt;/file&gt;
    &lt;/qresource&gt;
&lt;/RCC&gt;
</code></pre>
<p>在 QML 中使用示例如下：</p>
<pre><code class="language-qml}">Image {
    id: imageAboutLogo
    source: &quot;qrc:/images/about.png&quot;
}
</code></pre>
<p>在 Python 中使用示例如下：</p>
<pre><code class="language-python">self._engine = QQmlApplicationEngine()
self._engine.load('qrc:/ui/SciHubEVA.qml')
</code></pre>
<p>使用 <code>qrc</code> 文件管理资源文件的一个好处就是不需要担心各种相对路径和绝对路径带来的找不到文件的错误，但同时一个缺点是当资源文件更新后，需要运行 <code>pyrcc5 SciHubEVA.qrc -o scihub_resources.py</code> 更新资源，同时还需要在主程序代码中引入生成的 Python 资源代码。</p>
<h3 id="界面线程分离">界面线程分离</h3>
<p>写 GUI 应用的一个重要问题就是界面线程的分离，需要把耗时的业务逻辑摘出来，单独作为一个线程运行，这样才不会造成界面的“假死”情况。<code>scihub_api.py</code> 中的 <code>SciHubAPI</code> 作为下载文章的主类，下载过程相对耗时。因为其既需要 Qt 中的 <code>tr()</code> 函数，也需要线程，通过 Python 的多继承，<code>SciHubAPI</code> 类构造如下：</p>
<pre><code class="language-python">class SciHubAPI(QObject, threading.Thread):
    pass
</code></pre>
<h2 id="编译打包">编译打包</h2>
<p>PyInstaller 是一个用于打包 Python 代码到一个本地化可执行程序的工具，详细的使用方法请参见<a href="https://www.pyinstaller.org/documentation.html">官方文档</a>。同样，我们在此仅说明打包过程中遇到的一些问题。</p>
<h3 id="macos">macOS</h3>
<p>macOS 下的编译打包命令如下：</p>
<pre><code class="language-bash"># 清理相关目录和文件
rm -rf build
rm -rf dist
rm -f SciHubEVA.spec

# 重新生成资源文件
rm -f scihub_resources.py
pyrcc5 SciHubEVA.qrc -o scihub_resources.py

# 编译打包
pyinstaller -w scihub_eva.py \
  --hidden-import &quot;PyQt5.Qt&quot; \
  --hidden-import &quot;PyQt5.QtQuick&quot; \
  --add-data &quot;LICENSE:.&quot; \
  --add-data &quot;SciHubEVA.conf:.&quot; \
  --add-data &quot;images/SciHubEVA.png:images&quot; \
  --add-data &quot;translations/SciHubEVA_zh_CN.qm:translations&quot; \
  --name &quot;SciHubEVA&quot; \
  --icon &quot;images/SciHubEVA.icns&quot;

# 拷贝程序信息
cp Info.plist dist/SciHubEVA.app/Contents
</code></pre>
<p>编译打包过程中的 <code>--hidden-import</code> 参数是因为我们使用了 Qt Quick 和 QML 相关框架，但是在 Python 代码中我们并没有显式的引入这两个包，因此我们需要告知 PyInstaller 我们使用了这两个包，这样 PyInstaller 才会把相关的动态链接库拷贝到打包的程序中。</p>
<p>打包好的程序 <code>SciEvaHub.app</code> 会保存在 <code>dist</code> 目录中。由于目前无论是 macOS 还是 Windows 系统，高分辨率已经比较常见，为了适应高分辨率，我们需要在代码中添加相应的支持，在入口 Python 文件中，我们需要在头部添加如下信息：</p>
<pre><code class="language-python">if hasattr(Qt, 'AA_EnableHighDpiScaling'):
    QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
    QGuiApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
</code></pre>
<p>同时针对 macOS 系统，我们需要在 <code>Info.plist</code> 中添加如下信息以支持高分辨率：</p>
<pre><code class="language-xml">&lt;key&gt;NSHighResolutionCapable&lt;/key&gt;
&lt;string&gt;True&lt;/string&gt;
&lt;key&gt;NSSupportsAutomaticGraphicsSwitching&lt;/key&gt;
&lt;string&gt;True&lt;/string&gt;
</code></pre>
<p><code>Info.plist</code> 中的其他信息针对性进行修改即可，最后将其拷贝到打包好的程序中。</p>
<h3 id="windows">Windows</h3>
<p>Windows 下的编译打包命令如下：</p>
<pre><code class="language-dos">rem 清理相关目录和文件
rd /s /Q build
rd /s /Q dist
del /Q SciHubEVA.spec

rem 重新生成资源文件
del /Q scihub_resources.py
pyrcc5 SciHubEVA.qrc -o scihub_resources.py

rem 编译打包
pyinstaller -w scihub_eva.py ^
  --hidden-import &quot;PyQt5.Qt&quot; ^
  --hidden-import &quot;PyQt5.QtQuick&quot; ^
  --add-data &quot;LICENSE;.&quot; ^
  --add-data &quot;SciHubEVA.conf;.&quot; ^
  --add-data &quot;images/SciHubEVA.png;images&quot; ^
  --add-data &quot;translations/SciHubEVA_zh_CN.qm;translations&quot; ^
  --name &quot;SciHubEVA&quot; ^
  --icon &quot;images/SciHubEVA.ico&quot; ^
  --version-file &quot;SciHubEVA.win.version&quot;
</code></pre>
<p>编译打包过程中的 <code>--version-file</code> 参数是 Windows 程序的相关版本信息，具体请参见微软的 <a href="http://msdn.microsoft.com/en-us/library/ff468916(v=vs.85).aspx">Version Information Structures</a>。</p>
<p>打包好的程序会在 <code>dist\SciHubEVA</code> 目录中，该目录还包含了所有运行时所需的文件。</p>
<h2 id="安装包制作">安装包制作</h2>
<h3 id="macos-1">macOS</h3>
<p>macOS 下我们使用 appdmg 工具将编译打包好的程序制作成 DMG 镜像文件。DMG 镜像文件可以对原始的程序进行压缩，便于分发。appdmg 通过一个 JSON 文件控制 DMG 镜像的制作，详细的 JSON 格式和相关参数请参见 <a href="https://github.com/LinusU/node-appdmg">官方文档</a>，Sci-Hub EVA 的 DMG 制作 JSON 文件如下：</p>
<pre><code class="language-json">{
    &quot;title&quot;: &quot;Sci-Hub EVA&quot;,
    &quot;icon&quot;: &quot;images/SciHubEVA.icns&quot;,
    &quot;icon-size&quot;: 100,
    &quot;background&quot;: &quot;images/SciHubEVA-dmg-backgroud.png&quot;,
    &quot;format&quot;: &quot;UDZO&quot;,
    &quot;window&quot;: {
        &quot;size&quot;: {
            &quot;width&quot;: 600,
            &quot;height&quot;: 400
        }
    },
    &quot;contents&quot;: [
        {
            &quot;x&quot;: 100,
            &quot;y&quot;: 150,
            &quot;type&quot;: &quot;file&quot;,
            &quot;path&quot;: &quot;dist/SciHubEVA.app&quot;
        },
        {
            &quot;x&quot;: 300,
            &quot;y&quot;: 150,
            &quot;type&quot;: &quot;link&quot;,
            &quot;path&quot;: &quot;/Applications&quot;
        }
    ]
}
</code></pre>
<p>打包好后的 DMG 镜像效果如下：</p>
<p><img src="/images/cn/2018-05-27-cross-platform-gui-application-based-on-pyqt/dmg.png" alt="DMG"></p>
<h3 id="windows-1">Windows</h3>
<p>Windows 下我们使用 NSIS 构建安装包，同样 NSIS 也支持多语言安装包构建，但请注意，NSIS 程序本身并不支持 Unicode，因此 NSIS 安装包的脚本需使用 GBK 编码保存。构建好的安装包的安装界面如下：</p>
<p><img src="/images/cn/2018-05-27-cross-platform-gui-application-based-on-pyqt/nsis.png" alt="NSIS"></p>
<p>整个 Sci-Hub EVA 的编译打包和安装包制作过程请参见 <a href="https://github.com/leovan/SciHubEVA/tree/master/building">构建说明文档</a>。</p>

        ]]></description></item><item><title>流形学习 (Manifold Learning)</title><link>https://zeqiang.fun/cn/2018/03/manifold-learning/</link><pubDate>Fri, 16 Mar 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/03/manifold-learning/</guid><description><![CDATA[
        <h1 id="降维">降维</h1>
<p>在之前的 <a href="/cn/2017/12/evd-svd-and-pca">博客</a> 中，我们曾经介绍过 PCA 方法及其降维的作用。在原始数据中各个特征之间存在着一定的信息冗余，随着特征的不断增加就容易出现“维数灾难”的问题，因此降维的目的就是在尽可能多的保留原始信息的同时减少数据的维度。一般情况下我们将降维方法分为：<strong>线性降维方法</strong>和<strong>非线性降维方法</strong>，线性降维方法的典型算法有：</p>
<ul>
<li>主成份分析 (PCA, Principal Component Analysis) <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li>
<li>线性判别分写 (LDA, Linear Discriminant Analysis) <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></li>
<li>多尺度变换 (MDS, Multi-Dimensional Scaling) <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></li>
</ul>
<p>非线性降维方法中在此我们仅列举一些基于流行学习的算法：</p>
<ul>
<li>保距特征映射 (ISOMAP) <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup></li>
<li>局部线性嵌入 (LLE, Locally Linear Embedding) <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup></li>
<li>拉普拉斯特征映射 (LE, Laplacian Eigenmap) <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup></li>
</ul>
<p>在现实数据中，很多情况数据是无法通过线性的方法进行降维表示的，因此就需要非线性的降维算法出马了。</p>
<h1 id="流形">流形</h1>
<p>在调研流形相关概念时，发现要想深一步的理解这些概念还是需要详细的了解微分几何相关的内容，鉴于本文的目的主要是介绍流形学习 (主要是降维角度) 的相关内容，因此我们对流形仅做一些粗略的介绍。</p>
<p>“<strong>流形</strong>”是英文单词 <strong>Manifold</strong> 的中文译名，它源于德文术语 Mannigfaltigkeit，最早出现在 Riemann 1851 年的博士论文中，用来表示某种属性所能取到的所有值 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup>。为了更好的理解流形，我们先引入几个概念：</p>
<p><strong>拓扑结构 (拓扑)</strong> 任意给定集合 <code>$X$</code> 上的一个<strong>拓扑结构 (拓扑)</strong> 是 <code>$X$</code> 的某些特定子集组成的集合 <code>$\tau \subset 2^X$</code>，其中那些特定子集称为 <code>$\tau$</code> 所声明的<strong>开集</strong>，同时满足如下性质：</p>
<ol>
<li>空集和全集是开集，即 <code>$\varnothing, X \in \tau$</code></li>
<li>任意多个开集的并集是开集</li>
<li>有限多个开集的交集是开集</li>
</ol>
<p><strong>拓扑空间</strong> 指定了拓扑结构的集合就称为一个<strong>拓扑空间</strong>。</p>
<p><img src="/images/cn/2018-03-16-manifold-learning/topological-space-sample.png" alt=""></p>
<p>上图中给出了一些拓扑空间的示例，其中左侧 4 个为正确示例，右侧 2 个为错误示例。右上角的缺少了 {2} 和 {3} 的并集 {2, 3}，右下角的缺少了 {1, 2} 和 {2, 3} 的交集 {2}。</p>
<p><strong>同胚</strong> 两个拓扑空间 <code>$\left(X, \tau_X\right)$</code> 和 <code>$\left(Y, \tau_Y\right)$</code> 之间的函数 <code>$f: X \to Y$</code> 称为<strong>同胚</strong>，如果它具有下列性质：</p>
<ol>
<li><code>$f$</code> 是双射 (单射和满射)</li>
<li><code>$f$</code> 是连续的</li>
<li>反函数 <code>$f^{−1}$</code> 也是连续的 (<code>$f$</code> 是开映射)</li>
</ol>
<p>如果拓扑空间是一个几何物体，同胚就是把物体连续延展和弯曲，使其成为一个新的物体。因此，正方形和圆是同胚的，但球面和环面就不是。用一幅图形象的理解同胚，例如下图所示的<strong>咖啡杯</strong>和<strong>甜甜圈</strong> <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup>：</p>
<p><img src="/images/cn/2018-03-16-manifold-learning/mug-and-torus-morph.gif" alt=""></p>
<p>最后我们回过头来解释到底什么是<strong>流形</strong>？流形并不是一个“形状”，而是一个“空间” <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup>。最容易定义的流形是<strong>拓扑流形</strong>，它局部看起来象一些“普通”的欧几里得空间 <code>$\mathbb{R}^n$</code>，一个拓扑流形是一个局部同胚于一个欧几里得空间的拓扑空间。根据 Whitney 嵌入理论 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup>，任何一个流形都可以嵌入到高维的欧氏空间中。例如，地球的表面可以理解为一个嵌入 3 维空间的 2 维流形，其局部同胚于 2 维的欧式空间，对于一个球体的表面，用极坐标的形式可以表示为</p>
<p><code>$$ \begin{equation} \begin{split} x &amp;= r \sin \theta \cos \phi \\ y &amp;= r \sin \theta \sin \phi \\ z &amp;= r \cos \theta \end{split} \end{equation} $$</code></p>
<p>也就是说其 3 个维度实际上是由 2 个变量控制的。</p>
<h1 id="流形学习">流形学习</h1>
<p>假设 <code>$Y$</code> 为一个欧式空间 <code>$\mathbb{R}^d$</code> 的一个 <code>$d$</code> 维流形，<code>$f: Y \to \mathbb{R}^D$</code> 为一个光滑嵌入，对于 <code>$D &gt; d$</code>，流形学习的目的就是根据空间 <code>$\mathbb{R}^D$</code> 中的观测数据 <code>$\{x_i\}$</code> 重构 <code>$Y$</code> 和 <code>$f$</code> 的过程。隐含数据 <code>$\{y_i\}$</code> 由 <code>$Y$</code> 随机生成，通过光滑嵌入 <code>$f$</code> 生成观测数据，即 <code>$\{x_i = f\left(y_i\right)\}$</code>，所以我们可以将流形学习的问题看做是对于一个给定的观测数据一个生成模型的反向过程 <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup>。</p>
<p>在介绍具体的流形学习算法前，我们先引入几个 3 维数据用于解释后续的具体算法</p>
<p><img src="/images/cn/2018-03-16-manifold-learning/manifold-examples.png" alt=""></p>
<p>第一个为<strong>瑞士卷 (Swiss Roll)</strong>，其形状和我们日常生活中的瑞士卷相似；第二个为 <strong>S 形曲线 (S Curve)</strong>；第三个为一个被<strong>切断的球面 (Severed Sphere)</strong>。</p>
<h2 id="mds">MDS</h2>
<p>多尺度变换 (MDS, Multi-Dimensional Scaling) <sup id="fnref1:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 是一种通过保留样本在高维空间中的不相似性 (Dissimilarity) 降低数据维度的方法，在这里不相似性可以理解为样本之间的距离。因此，根据距离的度量方式不同可以将其分为度量型 (metric) MDS 和 非度量型 (non-metric) MDS。度量型 MDS 通过计算不同样本之间距离的度量值进行降维，而非度量型则仅考虑距离的排序信息，在此我们仅对度量型 MDS 做简单介绍。</p>
<p>MDS 的目标是保留样本在高维空间中的不相似性，假设 <code>$x \in \mathbb{R}^D, x' \in \mathbb{R}^d, D &gt; d$</code>，则 MDS 的目标函数可以写为</p>
<p><code>$$ \min \sum_{i, j} \lvert dist \left(x_i, x_j\right) - dist \left(x'_i, x'_j\right) \rvert $$</code></p>
<p>则，度量型 MDS 的算法的步骤如下：</p>
<ol>
<li>计算样本的距离矩阵 <code>$\boldsymbol{D} = \left[d_{i, j}\right] = \left[dist \left(x_i, x_j\right)\right]$</code>。</li>
<li>构造矩阵 <code>$\boldsymbol{A} = \left[a_{i, j}\right] = \left[- \dfrac{1}{2} d_{i, j}^2\right]$</code>。</li>
<li>通过中心矫正的方法构造矩阵 <code>$\boldsymbol{B} = \boldsymbol{J} \boldsymbol{D} \boldsymbol{J}, \boldsymbol{J} = \boldsymbol{I} - \dfrac{1}{n} \boldsymbol{O}$</code>，其中 <code>$\boldsymbol{I}$</code> 为 <code>$n \times n$</code> 的单位阵，<code>$\boldsymbol{O}$</code> 为 <code>$n \times n$</code> 的值均为 <code>$1$</code> 的矩阵。</li>
<li>计算矩阵 <code>$\boldsymbol{B}$</code> 的特征向量 <code>$e_1, e_2, ..., e_m$</code> 及其对应的特征值 <code>$\lambda_1, \lambda_2, ..., \lambda_m$</code>。</li>
<li>确定维度 <code>$k$</code>，重构数据 <code>$\boldsymbol{X}' = \boldsymbol{E}_k \boldsymbol{\Lambda}_k^{1/2}$</code>，其中 <code>$\boldsymbol{\Lambda}_k$</code> 为前 <code>$k$</code> 个值最大的 <code>$k$</code> 个特征值构成的对角矩阵，<code>$\boldsymbol{E}_k$</code> 是对应的 <code>$k$</code> 个特征向量构成的矩阵。</li>
</ol>
<p>在《多元统计分析》<sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup>一书中证明了，<code>$\boldsymbol{X}$</code> 的 <code>$k$</code> 维主坐标正好是将 <code>$\boldsymbol{X}$</code> 中心化后 <code>$n$</code> 个样本的前 <code>$k$</code> 个主成份的值，由此可见 MDS 和 PCA 的作用是类似的。</p>
<p>我们利用中国省会的地理位置给出 MDS 的一个示例，首先我们获取中国省会共 34 个点的坐标，其次我们计算两两之间的距离，我们仅利用距离信息利用 MDS 还原出 2 维空间中的坐标，可视化结果如下所示</p>
<p><img src="/images/cn/2018-03-16-manifold-learning/cities-mds.svg" alt=""></p>
<p>其中，黑色的点为省会的真实位置，蓝色的点为利用距离矩阵和 MDS 还原出来的位置，为了绘制还原出的位置我们对 MDS 的结果做出了适当的翻转和变换。从结果中不难看出，尽管每个点的坐标相比真实坐标都有一定的偏离，但是其很好的保持了相对距离，这也正是 MDS 算法的要求。</p>
<h2 id="isomap">ISOMAP</h2>
<p>对于一些非线性的流形，如果使用线性的降维方法得到的效果就不尽人意了，例如上文中提到的瑞士卷。在 ISOMAP 中，我们首先引入一个测地线的概念，在距离度量定义时，测地线可以定义为空间中两点的局域最短路径。形象的，在一个球面上，两点之间的测地线就是过这两个点的大圆的弧线</p>
<p><img src="/images/cn/2018-03-16-manifold-learning/spherical-triangle.svg" alt=""></p>
<p>那么，对于非线性流形，ISOMAP 则是通过构建邻接图，利用图上的最短距离来近似测地线。在构造邻接图时，我们使用最近邻算法，对于一个点 <code>$x_i$</code> 连接距离其最近的 <code>$k$</code> 个点，两点之间的距离我们则一般使用传统的欧式距离。则任意两点之间的测地线距离则可以利用构建的邻接图上的最短路径进行估计，图上的最短路问题我们可以通过 Dijkstra 或 Floyd-Warshall 算法计算。得到样本的距离矩阵后，ISOMAP 算法则使用 MDS 方法计算得到低维空间的座标映射。</p>
<p><img src="/images/cn/2018-03-16-manifold-learning/swiss-roll-isomap.png" alt=""></p>
<p>上图中，我们给出了利用 ISOMAP 对瑞士卷降至 2 维的一个格式化过程。第一幅图中，我们标注了 2 个蓝色的点，其中蓝色的直线为这 2 个点在三维空间中的欧式距离。第二幅图中，同样是相同的两个点，我们首先利用最近邻算法 (<code>$k = 10$</code>) 将瑞士卷所有的点连接为一个邻接图，其中红色的路径为这 2 个点在邻接图上的最短路。第三幅图是通过 ISOMAP 算法降维至 2 维的结果，其中蓝色的直线是这两个点在 2 维空间中的欧式距离，红色的路径是 3 维最短路在 2 维结果中的连线，可以看出两者是很相近的。</p>
<h2 id="lle">LLE</h2>
<p>局部线性嵌入 (LLE, Locally Linear Embedding) <sup id="fnref1:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>，从这个名称上我们不难看出其不同与 ISOMAP 那种通过都建邻接图保留全局结构的，而是从局部结构出发对数据进行降维。在 LLE 方法中，主要有如下的基本假设：</p>
<ul>
<li>一个流形的局部可以近似于一个欧式空间</li>
<li>每个样本均可以利用其邻居进行线性重构</li>
</ul>
<p>基于上面的假设，LLE 算法的流程如下：</p>
<ol>
<li>对于点 <code>$X_i$</code>，计算距离其最近的 <code>$k$</code> 个点，<code>$X_j, j \in N_i$</code>。</li>
<li>计算权重 <code>$W_{ij}$</code> 是的能够通过点 <code>$X_i$</code> 的邻居节点最优的重构该点，即最小化
<code>$$ \epsilon \left(W\right) = \sum_i \left\lVert X_i - \sum_j W_{ij} X_j \right\rVert ^2 $$</code></li>
<li>通过权重 <code>$W_{ij}$</code> 计算 <code>$X$</code> 的低维最优重构 <code>$Y$</code>，即最小化
<code>$$ \phi \left(Y\right) = \sum_i \left\lVert Y_i - \sum_j W_{ij} Y_j \right\rVert ^2 $$</code></li>
</ol>
<p>具体上述问题的优化求解过程在此就不在详细描述。针对 LLE 算法，后续很多人从不同方面对其进行了改进：</p>
<ol>
<li>Hessian LLE <sup id="fnref:13"><a href="#fn:13" class="footnote-ref" role="doc-noteref">13</a></sup> 在局部中不再考虑局部的线性关系，而是保持局部的 Hessian 矩阵的二次型的关系。</li>
<li>Modified LLE <sup id="fnref:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup> 则是修改了寻找最临近的 <code>$k$</code> 个样本的方案，其在寻找 <code>$k$</code> 近邻时希望找到的近邻尽量分布在样本的各个方向，而不是集中在一侧。</li>
<li>LTSA (Local Tangent Space Alignment) <sup id="fnref:15"><a href="#fn:15" class="footnote-ref" role="doc-noteref">15</a></sup> 则是除了保留了局部的几何性质，同时使用的一个从局部几何到整体性质过渡的 alignment 方法，因此可以理解为是一个局部和整体的组合。</li>
</ol>
<h2 id="le">LE</h2>
<p>LE (Laplacian Eigenmap) <sup id="fnref1:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 的基本思想是认为在高维空间中距离近的点映射到低维空间中后其位置也相距很近。LE 从这个思想出发，最终将问题转化为求解图拉普拉斯算子的广义特征值问题，具体的一些证明不在这里详细展开说明，具体请参见原文，下面仅给出 LE 算法的流程：</p>
<ol>
<li>构建邻接图。</li>
<li>构建邻接矩阵 <code>$W$</code>，构建邻接矩阵有两种方法：对于点 <code>$i$</code> 和点 <code>$j$</code> 相连，如果利用 Hear Kernel (参数 <code>$t \in \mathbb{R}$</code>)，则令 <code>$W_{ij} = \exp \left(\dfrac{- \left\lVert x_i - x_j \right\rVert ^ 2}{t}\right)$</code>；如果使用简介方案，则令 <code>$W_{ij} = 1$</code>，对于不相连的点，则令 <code>$W_{ij} = 0$</code>。</li>
<li>进行特征映射，通过上面构造的图 <code>$G$</code>，计算如下广义特征值和特征向量
<code>$$ L f = \lambda D f $$</code>
其中 <code>$D$</code> 是一个对角矩阵，<code>$D_{ii} = \sum_{j} W_{ji}$</code>，<code>$L = D - W$</code> 即为拉普拉斯矩阵。对于上式的解 <code>$f_0, .., f_{k-1}$</code> 为根据特征值从小到大的排序，其中 <code>$0 = \lambda_0 \leq \lambda_1 \leq ... \leq \lambda_{k-1}$</code>，则降维至 <code>$d$</code> 维的后的特征即为 <code>$\left(f_1, f_2, ..., f_d\right)$</code>。</li>
</ol>
<h2 id="sne-和-t-sne">SNE 和 t-SNE</h2>
<h3 id="sne">SNE</h3>
<p>SNE (Stochastic Neighbor Embedding) <sup id="fnref:16"><a href="#fn:16" class="footnote-ref" role="doc-noteref">16</a></sup> 是由 Hinton 等人提出的一种降维算法，其方法的基本假设如下：</p>
<ol>
<li>对象之间的相似度可以用概率进行表示，即：相似的对象有更高的概率被同时选择，不相似的对象有较低的概率被同时选择。</li>
<li>在高维空间中构建的这种概率分布应该尽可能的同低维空间中的概率分布相似。</li>
</ol>
<p>对于两个点 <code>$x_i, x_j$</code>，假设 <code>$x_i$</code> 以条件概率 <code>$p_{j∣i}$</code> 选择 <code>$x_j$</code> 作为它的邻近点，因此如果两者距离更近 (更相似)，则概率值越大，反之概率值越小，则我们定义 <code>$p_{j∣i}$</code> 如下：</p>
<p><code>$$ p_{j∣i} = \dfrac{\exp \left(\dfrac{- \left\lVert x_i - x_j \right\rVert ^ 2}{2 \sigma_i^2}\right)}{\sum_{k \neq i} \exp \left(\dfrac{- \left\lVert x_i - x_k \right\rVert ^ 2}{2 \sigma_i^2}\right)} $$</code></p>
<p>其中，<code>$\sigma_i$</code> 为参数，同时我们设置 <code>$p_{i∣i} = 0$</code>，因为我们仅需衡量不同对象之间的相似度。</p>
<p>类似的，根据 SNE 的基本思想，当数据被映射到低维空间中后，其概率分布应同高维空间中的分布尽可能的相似，假设点 <code>$x_i, x_j$</code> 在低维空间中的映射点为 <code>$y_i, y_j$</code>，则在低维空间中的条件概率 <code>$q_{j∣i}$</code> 定义为：</p>
<p><code>$$ q_{j∣i} = \dfrac{\exp \left(- \left\lVert y_i - y_j \right\rVert ^ 2\right)}{\sum_{k \neq i} \exp \left(- \left\lVert y_i - y_k \right\rVert ^ 2\right)} $$</code></p>
<p>同样，我们设置 <code>$q_{i∣i} = 0$</code>。从 SNE 的基本假设出发，我们的目的是使得数据在高维空间中的条件概率尽可能的和其在低维空间中的条件概率相同，因此对于全部点样本点而言，就是保证高维空间的概率分布 <code>$P_i$</code> 和低维空间的概率分布 <code>$Q_i$</code> 尽量形同。在这里我们利用 KL 散度衡量这两个概率分布的差异，则 SNE 的损失函数可以写为：</p>
<p><code>$$ C = \sum_{i} KL \left(P_i \Vert Q_i\right) = \sum_{i} \sum_{j} p_{j∣i} \log \dfrac{p_{j∣i}}{q_{j∣i}} $$</code></p>
<p>因为 KL 散度具有不对称性可知，当在原始空间中两点距离较远而降维后的空间中距离较近 (即，<code>$q_{j|i} &lt; p_{j|i}$</code>) 时，会产生较大的 cost，相反则会产生较小的 cost。正是这种不对称性的损失函数导致了 SNE 算法更加关注局部结构，相比忽略了全局结构。</p>
<p>上文中，对于不同的点，<code>$\sigma_i$</code> 具有不同的值，SNE 算法利用困惑度 (Perplexity) 对其进行优化寻找一个最佳的 <code>$\sigma$</code>，对于一个随机变量 <code>$P_i$</code>，困惑度定义如下：</p>
<p><code>$$ Perp \left(P_i\right) = 2^{H \left(P_i\right)} $$</code></p>
<p>其中，<code>$H \left(P_i\right) = \sum_{j} p_{j|i} \log_2 p_{j|i}$</code> 表示 <code>$P_i$</code> 的熵。困惑度可以解释为一个点附近的有效近邻点个数。SNE 对困惑度的调整比较有鲁棒性，通常选择 5-50 之间，给定之后，使用二分搜索的方式寻找合适的 <code>$\sigma$</code>。</p>
<p>SNE 的损失函数对 <code>$y_i$</code> 求梯度后，可得：</p>
<p><code>$$ \dfrac{\delta C}{\delta y_i} = 2 \sum_j \left(p_{j|i} - q_{j|i} + p_{i|j} - q_{i|j}\right) \left(y_i - y_j\right) $$</code></p>
<h3 id="t-sne">t-SNE</h3>
<p>SNE 为我们提供了一种很好的降维方法，但是其本身也存在一定的问题，主要有如下两点：</p>
<ul>
<li><strong>不对称问题</strong>：损失函数中的 KL 散度具有不对称性，导致 SNE 更加关注局部结构，相比忽略了全局结构。</li>
<li><strong>拥挤问题</strong>：从高维空间映射到低维空间后，不同类别的簇容易挤在一起，无法较好地区分开。</li>
</ul>
<p>针对这两个问题，Maaten 等人又提出了 t-SNE 算法对其进行优化 <sup id="fnref:17"><a href="#fn:17" class="footnote-ref" role="doc-noteref">17</a></sup>。</p>
<p>针对不对称问题，Maaten 采用的方法是用联合概率分布来替代条件概率分布。高维控件中的联合概率分布为 <code>$P$</code>，低维空间中的联合概率分布为 <code>$Q$</code>，则对于任意的 <code>$i, j$</code>，有 <code>$p_{ij} = p_{ji}, q_{ij} = q_{ji}$</code>，联合概率定义为：</p>
<p><code>$$ \begin{align} p_{ij} &amp;= \dfrac{\exp \left(\dfrac{- \left\lVert x_i - x_j \right\rVert ^ 2}{2 \sigma^2}\right)}{\sum_{k \neq l} \exp \left(\dfrac{- \left\lVert x_k - x_l \right\rVert ^ 2}{2 \sigma^2}\right)} \\ q_{ij} &amp;= \dfrac{\exp \left(- \left\lVert y_i - y_j \right\rVert ^ 2\right)}{\sum_{k \neq l} \exp \left(- \left\lVert y_k - y_l \right\rVert ^ 2\right)} \end{align} $$</code></p>
<p>虽然这样保证了对称性，但是对于异常的情况，例如数据点 <code>$x_i$</code> 在距离群簇较远，则 <code>$\lVert x_i − x_j \rVert ^ 2$</code> 的值会很大，而 <code>$p_{ij}$</code> 会相应变得非常小，也就是说 <code>$x_i$</code> 的位置很远这件事情对损失函数影响很小 (惩罚过小)，那这个点在低维空间中将无法从其他点中区分出来。因此 Maaten 提出了对称的条件概率来重新定义上述联合概率 <code>$p_{ij}$</code> ，对于数量为 <code>$n$</code> 的数据点，新的概率公式是：</p>
<p><code>$$ p_{ij} = \dfrac{p_{j|i} + p_{i|j}}{2n} $$</code></p>
<p>则损失函数更新为：</p>
<p><code>$$ C = \sum_{i} KL \left(P_i \Vert Q_i\right) = \sum_{i} \sum_{j} p_{ij} \log \dfrac{p_{ij}}{q_{ij}} $$</code></p>
<p>梯度更新为：</p>
<p><code>$$ \dfrac{\delta C}{\delta y_i} = 4 \sum_j \left(p_{ij} - q_{ij}\right) \left(y_i - y_j\right) $$</code></p>
<p>拥挤问题 (Crowding) 就是从高维空间映射到低维空间后，不同类别的簇容易挤在一起，不能很好的地区分开。t-SNE 则是利用了 t 分布重新定义 <code>$q_{ij}$</code>，t 分布具有长尾特性，相比于高斯分布，其在尾部趋向于 0 的速度更慢，对比如图所示：</p>
<p><img src="/images/cn/2018-03-16-manifold-learning/gassion-t-comparison.png" alt=""></p>
<p>利用 t 分布重新定义的 <code>$q_{ij}$</code> 为：</p>
<p><code>$$ q_{ij} = \dfrac{\left(1 + \lVert y_i - y_j \rVert ^ 2\right) ^ {-1}}{\sum_{k \neq l} \left(1 + \lVert y_k - y_l \rVert ^ 2\right) ^ {-1}} $$</code></p>
<p>梯度更新为：</p>
<p><code>$$ \dfrac{\delta C}{\delta y_i} = 4 \sum_j \left(p_{ij} - q_{ij}\right) \left(y_i - y_j\right) \left(1 + \lVert y_i - y_j \rVert ^ 2\right) ^ {-1} $$</code></p>
<p>利用 t-SNE 对 MNIST 数据集进行降维可视化结果如下：</p>
<p><img src="/images/cn/2018-03-16-manifold-learning/mnist-t-sne.png" alt=""></p>
<h2 id="方法比较">方法比较</h2>
<p>针对上述的若干算法，我们简单列举一下每个算法的优缺点</p>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>优点</th>
          <th>缺点</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Isomap</td>
          <td>1. 保持流形的全局几何结构 <br/> 2. 适用于学习内部平坦的低维流形</td>
          <td>1. 对于数据量较大的情况，计算效率过低 <br/> 2. 不适于学习有较大内在曲率的流形</td>
      </tr>
      <tr>
          <td>LLE</td>
          <td>1. 可以学习任意维的局部线性的低维流形 <br/> 2. 归结为稀疏矩阵特征值计算，计算复杂度相对较小</td>
          <td>1. 所学习的流形只能是不闭合的 <br/> 2. 要求样本在流形上是稠密采样的 <br/> 3.对样本中的噪声和邻域参数比较敏感</td>
      </tr>
      <tr>
          <td>LE</td>
          <td>1. 是局部非线性方法，与谱图理论有很紧密的联系 <br/> 2. 通过求解稀疏矩阵的特征值问题解析地求出整体最优解，效率非常高 <br/> 3. 使原空间中离得很近的点在低维空间也离得很近，可以用于聚类</td>
          <td>1. 对算法参数和数据采样密度较敏感 <br/> 2. 不能有效保持流形的全局几何结构</td>
      </tr>
      <tr>
          <td>SNE, t-SNE</td>
          <td>1. 非线性降维效果相较上述方法较好</td>
          <td>1. 大规模高维数据时，效率显著降低 <br/> 2. 参数对不同数据集较为敏感</td>
      </tr>
  </tbody>
</table>
<p>对于<strong>瑞士卷 (Swiss Roll)</strong>，<strong>S 形曲线 (S Curve)</strong> 和<strong>切断的球面 (Severed Sphere)</strong>，我们利用不同的流形算法对其进行降维，可视化的对比结果如下面 3 张图所示，图中同时标注了算法的运行时间，实现主要参照了 scikit-learn 关于流形学习算法的比较 <sup id="fnref:18"><a href="#fn:18" class="footnote-ref" role="doc-noteref">18</a></sup>。</p>
<p><img src="/images/cn/2018-03-16-manifold-learning/s-curve.png" alt=""></p>
<p><img src="/images/cn/2018-03-16-manifold-learning/swiss-roll.png" alt=""></p>
<p><img src="/images/cn/2018-03-16-manifold-learning/severed-sphere.png" alt=""></p>
<p>文中相关图片绘制实现详见<a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2018-03-16-manifold-learning">代码</a>，本文部分内容参考了<strong>流形学习专题介绍</strong> <sup id="fnref:19"><a href="#fn:19" class="footnote-ref" role="doc-noteref">19</a></sup>， <strong>流形学习</strong> <sup id="fnref:20"><a href="#fn:20" class="footnote-ref" role="doc-noteref">20</a></sup>，<strong>Chrispher</strong> <sup id="fnref:21"><a href="#fn:21" class="footnote-ref" role="doc-noteref">21</a></sup> 的博客和 <strong>bingo</strong> <sup id="fnref:22"><a href="#fn:22" class="footnote-ref" role="doc-noteref">22</a></sup> 的博客。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Jolliffe, Ian T. &ldquo;Principal component analysis and factor analysis.&rdquo; <em>Principal component analysis.</em> Springer, New York, NY, 1986. 115-128.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Balakrishnama, Suresh, and Aravind Ganapathiraju. &ldquo;Linear discriminant analysis-a brief tutorial.&rdquo; <em>Institute for Signal and information Processing</em> 18 (1998): 1-8.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Cox, Trevor F., and Michael AA Cox. <em>Multidimensional scaling.</em> CRC press, 2000.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Tenenbaum, Joshua B., Vin De Silva, and John C. Langford. &ldquo;A global geometric framework for nonlinear dimensionality reduction.&rdquo; <em>Science</em> 290.5500 (2000): 2319-2323.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Roweis, Sam T., and Lawrence K. Saul. &ldquo;Nonlinear dimensionality reduction by locally linear embedding.&rdquo; <em>Science</em> 290.5500 (2000): 2323-2326.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Belkin, Mikhail, and Partha Niyogi. &ldquo;Laplacian eigenmaps for dimensionality reduction and data representation.&rdquo; <em>Neural computation</em> 15.6 (2003): 1373-1396.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>梅加强. 流形与几何初步&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p><a href="https://zh.wikipedia.org/zh-hans/%E6%B5%81%E5%BD%A2">https://zh.wikipedia.org/zh-hans/流形</a>&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>pluskid. <a href="http://blog.pluskid.org/?p=533">浅谈流形学习</a>&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p><a href="https://en.wikipedia.org/wiki/Whitney_embedding_theorem">https://en.wikipedia.org/wiki/Whitney_embedding_theorem</a>&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Silva, Vin D., and Joshua B. Tenenbaum. &ldquo;Global versus local methods in nonlinear dimensionality reduction.&rdquo; <em>Advances in neural information processing systems.</em> 2003.&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p>何晓群. 多元统计分析&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:13">
<p>Donoho, David L., and Carrie Grimes. &ldquo;Hessian eigenmaps: Locally linear embedding techniques for high-dimensional data.&rdquo; <em>Proceedings of the National Academy of Sciences</em> 100.10 (2003): 5591-5596.&#160;<a href="#fnref:13" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:14">
<p>Zhang, Zhenyue, and Jing Wang. &ldquo;MLLE: Modified locally linear embedding using multiple weights.&rdquo; <em>Advances in neural information processing systems.</em> 2007.&#160;<a href="#fnref:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:15">
<p>Zhang, Zhenyue, and Hongyuan Zha. &ldquo;Principal manifolds and nonlinear dimensionality reduction via tangent space alignment.&rdquo; <em>SIAM journal on scientific computing</em> 26.1 (2004): 313-338.&#160;<a href="#fnref:15" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:16">
<p>Hinton, Geoffrey E., and Sam T. Roweis. &ldquo;Stochastic neighbor embedding.&rdquo; <em>Advances in neural information processing systems.</em> 2003.&#160;<a href="#fnref:16" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:17">
<p>Maaten, Laurens van der, and Geoffrey Hinton. &ldquo;Visualizing data using t-SNE.&rdquo; <em>Journal of machine learning research</em> 9.Nov (2008): 2579-2605.&#160;<a href="#fnref:17" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:18">
<p><a href="http://scikit-learn.org/stable/auto_examples/manifold/plot_compare_methods.html">http://scikit-learn.org/stable/auto_examples/manifold/plot_compare_methods.html</a>&#160;<a href="#fnref:18" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:19">
<p>王瑞平. 流形学习专题介绍&#160;<a href="#fnref:19" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:20">
<p>何晓飞. 流形学习&#160;<a href="#fnref:20" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:21">
<p><a href="http://www.datakit.cn/blog/2017/02/05/t_sne_full.html">http://www.datakit.cn/blog/2017/02/05/t_sne_full.html</a>&#160;<a href="#fnref:21" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:22">
<p><a href="http://bindog.github.io/blog/2016/06/04/from-sne-to-tsne-to-largevis/">http://bindog.github.io/blog/2016/06/04/from-sne-to-tsne-to-largevis/</a>&#160;<a href="#fnref:22" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>深度学习优化算法 (Optimization Methods for Deeplearning)</title><link>https://zeqiang.fun/cn/2018/02/optimization-methods-for-deeplearning/</link><pubDate>Sat, 24 Feb 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/02/optimization-methods-for-deeplearning/</guid><description><![CDATA[
        <p>在构建神经网络模型的时候，除了网络结构设计以外，选取合适的优化算法也对网络起着至关重要的作用，本文将对神经网络中常用的优化算法进行简单的介绍和对比，本文部分参考了 Ruder 的关于梯度下降优化算法一文 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。首先，我们对下文中使用的符号进行同意说明：网络中的参数同一表示为 <code>$\theta$</code>，网络的假设函数为 <code>$h_{\boldsymbol{\theta}}\left(\boldsymbol{x}\right)$</code>，网络的损失函数为 <code>$J\left(\boldsymbol{\theta}\right)$</code>，学习率为 <code>$\alpha$</code>，假设训练数据中共包含 <code>$m$</code> 个样本，网络参数个数为 <code>$n$</code>。</p>
<h1 id="梯度下降">梯度下降</h1>
<p>在梯度下降算法中，常用的主要包含 3 种不同的形式，分别是批量梯度下降 (Batch Gradient Descent, BGD)，随机梯度下降 (Stochastic Gradient Descent, SGD) 和小批量梯度下降 (Mini-Batch Gradient Descent, MBGD)。一般情况下，我们在谈论梯度下降时，更多的是指小批量梯度下降。</p>
<h2 id="bgd">BGD</h2>
<p>BGD 为梯度下降算法中最基础的一个算法，其损失函数定义如下：</p>
<p><code>$$ J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2m} \sum_{i=1}^{m}{\left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right)} $$</code></p>
<p>针对任意参数 <code>$\theta_j$</code> 我们可以求得其梯度为：</p>
<p><code>$$ \nabla_{\theta_j} = \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j} = - \dfrac{1}{m} \sum_{i=1}^{m}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}} $$</code></p>
<p>之后，对于任意参数 <code>$\theta_j$</code> 我们按照其<strong>负梯度</strong>方向进行更新：</p>
<p><code>$$ \theta_j = \theta_j + \alpha \left[\dfrac{1}{m} \sum_{i=1}^{m}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}}\right] $$</code></p>
<p>整个算法流程可以表示如下：</p>


<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/pseudocode@latest/build/pseudocode.min.css">


<div><pre class="pseudocode">
\begin{algorithm}
\caption{BGD 算法}
\begin{algorithmic}
\FOR{$epoch = 1, 2, ...$}
    \FOR{$j = 1, 2, ..., n$}
        \STATE $J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2m} \sum_{i=1}^{m}{\left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right)}$
        \STATE $\theta_j = \theta_j - \alpha \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j}$
    \ENDFOR
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>从上述算法流程中我们可以看到，BGD 算法每次计算梯度都使用了整个训练集，也就是说对于给定的一个初始点，其每一步的更新都是沿着全局梯度最大的负方向。但这同样是其问题，当 <code>$m$</code> 太大时，整个算法的计算开销就很高了。</p>
<h2 id="sgd">SGD</h2>
<p>SGD 相比于 BGD，其最主要的区别就在于计算梯度时不再利用整个数据集，而是针对单个样本计算梯度并更新权重，因此，其损失函数定义如下：</p>
<p><code>$$ J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2} \left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right) $$</code></p>
<p>整个算法流程可以表示如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{SGD 算法}
\begin{algorithmic}
\FOR{$epoch = 1, 2, ...$}
    \STATE Randomly shuffle dataset
    \FOR{$i = 1, 2, ..., m$}
        \FOR{$j = 1, 2, ..., n$}
            \STATE $J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2} \left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right)$
            \STATE $\theta_j = \theta_j - \alpha \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j}$
        \ENDFOR
    \ENDFOR
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>SGD 相比于 BGD 具有训练速度快的优势，但同时由于权重改变的方向并不是全局梯度最大的负方向，甚至相反，因此不能够保证每次损失函数都会减小。</p>
<h2 id="mbgd">MBGD</h2>
<p>针对 BGD 和 SGD 的问题，MBGD 则是一个折中的方案，在每次更新参数时，MBGD 会选取 <code>$b$</code> 个样本计算的梯度，设第 <code>$k$</code> 批中数据的下标的集合为 <code>$B_k$</code>，则其损失函数定义如下：</p>
<p><code>$$ \nabla_{\theta_j} = \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j} = - \dfrac{1}{|B_k|} \sum_{i \in B_k}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}} $$</code></p>
<p>整个算法流程可以表示如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{MBGD 算法}
\begin{algorithmic}
\FOR{$epoch = 1, 2, ...$}
    \FOR{$k = 1, 2, ..., m / b$}
        \FOR{$j = 1, 2, ..., n$}
            \STATE $J \left(\boldsymbol{\theta}\right) = \dfrac{1}{|B_k|} \sum_{i \in B_k}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}}$
            \STATE $\theta_j = \theta_j - \alpha \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j}$
        \ENDFOR
    \ENDFOR
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<h1 id="momentum">Momentum</h1>
<p>当梯度沿着一个方向要明显比其他方向陡峭，我们可以形象的称之为峡谷形梯度，这种情况多位于局部最优点附近。在这种情况下，SGD 通常会摇摆着通过峡谷的斜坡，这就导致了其到达局部最优值的速度过慢。因此，针对这种情况，Momentum <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 方法提供了一种解决方案。针对原始的 SGD 算法，参数每 <code>$t$</code> 步的变化量可以表示为</p>
<p><code>$$ \boldsymbol{v}_t = - \alpha \nabla_{\boldsymbol{\theta}} J \left(\boldsymbol{\theta}_t\right) $$</code></p>
<p>Momentum 算法则在其变化量中添加了一个动量分量，即</p>
<p><code>$$ \begin{equation} \begin{split} \boldsymbol{v}_t &amp;= - \alpha \nabla_{\boldsymbol{\theta}} J \left(\boldsymbol{\theta}_t\right) + \gamma \boldsymbol{v}_{t-1} \\ \boldsymbol{\theta}_t &amp;= \boldsymbol{\theta}_{t-1} + \boldsymbol{v}_t \end{split} \end{equation} $$</code></p>
<p>对于添加的动量项，当第 <code>$t$</code> 步和第 <code>$t-1$</code> 步的梯度方向<strong>相同</strong>时，<code>$\boldsymbol{\theta}$</code> 则以更快的速度更新；当第 <code>$t$</code> 步和第 <code>$t-1$</code> 步的梯度方向<strong>相反</strong>时，<code>$\boldsymbol{\theta}$</code> 则以较慢的速度更新。利用 SGD 和 Momentum 两种方法，在峡谷行的二维梯度上更新参数的示意图如下所示</p>
<p><img src="/images/cn/2018-02-24-optimization-methods-for-deeplearning/sgd-and-momentum.png" alt=""></p>
<h1 id="nag">NAG</h1>
<p>NAG (Nesterov Accelerated Gradient) <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 是一种 Momentum 算法的变种，其核心思想会利用“下一步的梯度”确定“这一步的梯度”，当然这里“下一步的梯度”并非真正的下一步的梯度，而是指仅根据动量项更新后位置的梯度。Sutskever <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 给出了一种更新参数的方法：</p>
<p><code>$$ \begin{equation} \begin{split} \boldsymbol{v}_t &amp;= - \alpha \nabla_{\boldsymbol{\theta}} J \left(\boldsymbol{\theta}_t + \gamma \boldsymbol{v}_{t-1}\right) + \gamma \boldsymbol{v}_{t-1} \\ \boldsymbol{\theta}_t &amp;= \boldsymbol{\theta}_{t-1} + \boldsymbol{v}_t \end{split} \end{equation} $$</code></p>
<p>针对 Momentum 和 NAG 两种不同的方法，其更新权重的差异如下图所示：</p>
<p><img src="/images/cn/2018-02-24-optimization-methods-for-deeplearning/momentum-and-nag.png" alt=""></p>
<h1 id="adagrad">AdaGrad</h1>
<p>AdaGrad <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> 是一种具有自适应学习率的的方法，其对于低频特征的参数选择更大的更新量，对于高频特征的参数选择更小的更新量。因此，AdaGrad算法更加适用于处理稀疏数据。Pennington 等则利用该方法训练 GloVe <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 词向量，因为对于出现次数较少的词应当获得更大的参数更新。</p>
<p>因为每个参数的学习速率不再一样，则在 <code>$t$</code> 时刻第 <code>$i$</code> 个参数的变化为</p>
<p><code>$$ \theta_{t, i} = \theta_{t-1, i} - \alpha \nabla_{\theta} J \left(\theta_{t-1, i}\right) $$</code></p>
<p>根据 AdaGrad 方法的更新方式，我们对学习率做出如下变化</p>
<p><code>$$ \theta_{t, i} = \theta_{t-1, i} - \dfrac{\alpha}{\sqrt{G_{t, i}} + \epsilon} \nabla_{\theta} J \left(\theta_{t-1, i}\right) $$</code></p>
<p>其中，<code>$G_t$</code> 表示截止到 <code>$t$</code> 时刻梯度的平方和；<code>$\epsilon$</code> 为平滑项，防止除数为零，一般设置为 <code>$10^{-8}$</code>。AdaGrad 最大的优势就在于其能够自动调节每个参数的学习率。</p>
<h1 id="adadelta">Adadelta</h1>
<p>上文中 AdaGrad 算法存在一个缺点，即其用于调节学习率的分母中包含的是一个梯度的平方累加项，随着训练的不断进行，这个值将会越来越大，也就是说学习率将会越来越小，最终导致模型不会再学习到任何知识。Adadelta <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup> 方法针对 AdaGrad 的这个问题，做出了进一步改进，其不再计算历史所以梯度的平方和，而是使用一个固定长度 <code>$w$</code> 的滑动窗口内的梯度。</p>
<p>因为存储 <code>$w$</code> 的梯度平方并不高效，Adadelta 采用了一种递归的方式进行计算，定义 <code>$t$</code> 时刻梯度平方的均值为</p>
<p><code>$$ E \left[g^2\right]_t = \rho E \left[g^2\right]_{t-1} + \left(1 - \rho\right) g^2_{t} $$</code></p>
<p>其中，<code>$g_t$</code> 表示 <code>$t$</code> 时刻的梯度；<code>$\rho$</code> 为一个衰减项，类似于 Momentum 中的衰减项。在更新参数过程中我们需要其平方根，即</p>
<p><code>$$ \text{RMS} \left[g\right]_t = \sqrt{E \left[g^2\right]_t + \epsilon} $$</code></p>
<p>则参数的更新量为</p>
<p><code>$$ \Delta \theta_t = - \dfrac{\alpha}{\text{RMS} \left[g\right]_t} g_t $$</code></p>
<p>除此之外，作者还考虑到上述更新中更新量和参数的假设单位不一致的情况，在上述更新公式中添加了一个关于参数的衰减项</p>
<p><code>$$ \text{RMS} \left[\Delta \theta\right]_t = \sqrt{E \left[\Delta \theta^2\right]_t + \epsilon} $$</code></p>
<p>其中</p>
<p><code>$$ E \left[\Delta \theta^2\right]_t = \rho E \left[\Delta \theta^2\right]_{t-1} + \left(1 - \rho\right) \Delta \theta_t^2 $$</code></p>
<p>在原始的论文中，作者直接用 <code>$\text{RMS} \left[\Delta \theta^2\right]_t$</code> 替换了学习率，即</p>
<p><code>$$ \Delta \theta_t = - \dfrac{\text{RMS} \left[\Delta \theta\right]_{t-1}}{\text{RMS} \left[g\right]_t} g_t $$</code></p>
<p>而在 <code>Keras</code> 源码中，则保留了固定的学习率，即</p>
<p><code>$$ \Delta \theta_t = - \alpha \dfrac{\text{RMS} \left[\Delta \theta\right]_{t-1}}{\text{RMS} \left[g\right]_t} g_t $$</code></p>
<h1 id="rmsprop">RMSprop</h1>
<p>RMSprop <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> 是由 Hinton 提出的一种针对 AdaGrad 的改进算法。参数的更新量为</p>
<p><code>$$ \Delta \theta_t = - \dfrac{\alpha}{\text{RMS} \left[g\right]_t} g_t $$</code></p>
<h1 id="adam">Adam</h1>
<p>Adam (Adaptive Moment Estimation) <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup> 是另一种类型的自适应学习率方法，类似 Adadelta，Adam 对于每个参数都计算各自的学习率。Adam 方法中包含一个一阶梯度衰减项 <code>$m_t$</code> 和一个二阶梯度衰减项 <code>$v_t$</code></p>
<p><code>$$ \begin{equation} \begin{split} m_t &amp;= \beta_1 m_{t-1} + \left(1 - \beta_1\right) g_t \\ v_t &amp;= \beta_2 v_{t-1} + \left(1 - \beta_2\right) g_t^2 \end{split} \end{equation} $$</code></p>
<p>算法中，<code>$m_t$</code> 和 <code>$v_t$</code> 初始化为零向量，作者发现两者会更加偏向 <code>$0$</code>，尤其是在训练的初始阶段和衰减率很小的时候 (即 <code>$\beta_1$</code> 和 <code>$\beta_2$</code> 趋近于1的时候)。因此，对其偏差做如下校正</p>
<p><code>$$ \begin{equation} \begin{split} \hat{m}_t &amp;= \dfrac{m_t}{1 - \beta_1^t} \\ \hat{v}_t &amp;= \dfrac{v_t}{1 - \beta_2^t} \end{split} \end{equation} $$</code></p>
<p>最终得到 Adam 算法的参数更新量如下</p>
<p><code>$$ \Delta \theta = - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t $$</code></p>
<h1 id="adamax">Adamax</h1>
<p>在 Adam 中参数的更新方法利用了 <code>$L_2$</code> 正则形式的历史梯度 (<code>$v_{t-1}$</code>) 和当前梯度 (<code>$|g_t|^2$</code>)，因此，更一般的，我们可以使用 <code>$L_p$</code> 正则形式，即</p>
<p><code>$$ \begin{equation} \begin{split} v_t &amp;= \beta_2^p v_{t-1} + \left(1 - \beta_2^p\right) |g_t|^p \\ &amp;= \left(1 - \beta_2^p\right) \sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p \end{split} \end{equation} $$</code></p>
<p>这样的变换对于值较大的 <code>$p$</code> 而言是很不稳定的，但对于极端的情况，当 <code>$p$</code> 趋近于无穷的时候，则变为了一个简单并且稳定的算法。则在 <code>$t$</code> 时刻对应的我们需要计算 <code>$v_t^{1/p}$</code>，令 <code>$u_t = \lim_{p \to \infty} \left(v_t\right)^{1/p}$</code>，则有</p>
<p><code>$$ \begin{equation} \begin{split} u_t &amp;= \lim_{p \to \infty} \left(\left(1 - \beta_2^p\right) \sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p\right)^{1/p} \\ &amp;= \lim_{p \to \infty} \left(1 - \beta_2^p\right)^{1/p} \left(\sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p\right)^{1/p} \\ &amp;= \lim_{p \to \infty} \left(\sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p\right)^{1/p} \\ &amp;= \max \left(\beta_2^{t-1} |g_1|, \beta_2^{t-2} |g_2|, ..., \beta_{t-1} |g_t|\right) \end{split} \end{equation} $$</code></p>
<p>写成递归的形式，则有</p>
<p><code>$$ u_t = \max \left(\beta_2 \cdot u_{t-1}, |g_t|\right) $$</code></p>
<p>则 Adamax 算法的参数更新量为</p>
<p><code>$$ \Delta \theta = - \dfrac{\alpha}{u_t} \hat{m}_t $$</code></p>
<h1 id="nadam">Nadam</h1>
<p>Adam 算法可以看做是对 RMSprop 和 Momentum 的结合：历史平方梯度的衰减项 <code>$v_t$</code> (RMSprop) 和 历史梯度的衰减项 <code>$m_t$</code> (Momentum)。Nadam (Nesterov-accelerated Adaptive Moment Estimation) <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup> 则是将 Adam 同 NAG 进行了进一步结合。我们利用 Adam 中的符号重新回顾一下 NAG 算法</p>
<p><code>$$ \begin{equation} \begin{split} g_t &amp;= \nabla_{\theta} J \left(\theta_t - \gamma m_{t-1}\right) \\ m_t &amp;= \gamma m_{t-1} + \alpha g_t \\ \theta_t &amp;= \theta_{t-1} - m_t \end{split} \end{equation} $$</code></p>
<p>NAG 算法的核心思想会利用“下一步的梯度”确定“这一步的梯度”，在 Nadam 算法中，作者在考虑“下一步的梯度”时对 NAG 进行了改动，修改为</p>
<p><code>$$ \begin{equation} \begin{split} g_t &amp;= \nabla_{\theta} J \left(\theta_t\right) \\ m_t &amp;= \gamma m_{t-1} + \alpha g_t \\ \theta_t &amp;= \theta_{t-1} - \left(\gamma m_t + \alpha g_t\right) \end{split} \end{equation} $$</code></p>
<p>对于 Adam，根据</p>
<p><code>$$ \hat{m}_t = \dfrac{\beta_1 m_{t-1}}{1 - \beta_1^t} + \dfrac{\left(1 - \beta_1\right) g_t}{1 - \beta_1^t} $$</code></p>
<p>则有</p>
<p><code>$$ \begin{equation} \begin{split} \Delta \theta &amp;= - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t \\ &amp;= - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \left(\dfrac{\beta_1 m_{t-1}}{1 - \beta_1^t} + \dfrac{\left(1 - \beta_1\right) g_t}{1 - \beta_1^t}\right) \end{split} \end{equation} $$</code></p>
<p>上式中，仅 <code>$\dfrac{\beta_1 m_{t-1}}{1 - \beta_1^t}$</code> 和动量项相关，因此我们类似上文中对 NAG 的改动，通过简单的替换加入 Nesterov 动量项，最终得到 Nadam 方法的参数的更新量</p>
<p><code>$$ \Delta \theta = - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \left(\dfrac{\beta_1 m_{t-1}}{1 - \beta_1^{t+1}} + \dfrac{\left(1 - \beta_1\right) g_t}{1 - \beta_1^t}\right) $$</code></p>
<h1 id="amsgrad">AMSGrad</h1>
<p>对于前面提到的 Adadelta，RMSprop，Adam 和 Nadam 方法，他们均采用了平方梯度的指数平滑平均值迭代产生新的梯度，但根据观察，在一些情况下这些算法并不能收敛到最优解。Reddi 等提出了一种新的 Adam 变体算法 AMSGrad <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup>，在文中作者解释了为什么 RMSprop 和 Adam 算法无法收敛到一个最优解的问题。通过分析表明，为了保证得到一个收敛的最优解需要保留过去梯度的“长期记忆”，因此在 AMSGrad 算法中使用了历史平方梯度的最大值而非滑动平均进行更新参数，即</p>
<p><code>$$ \begin{equation} \begin{split} m_t &amp;= \beta_1 m_{t-1} + \left(1 - \beta_1\right) g_t \\ v_t &amp;= \beta_2 v_{t-1} + \left(1 - \beta_2\right) g_t^2 \\ \hat{v}_t &amp;= \max \left(\hat{v}_{t-1}, v_t\right) \\ \Delta \theta &amp;= - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} m_t \end{split} \end{equation} $$</code></p>
<p>作者在一些小数据集和 CIFAR-10 数据集上得到了相比于 Adam 更好的效果，但与此同时一些其他的 <a href="https://fdlm.github.io/post/amsgrad/">实验</a> 却得到了相比与 Adam 类似或更差的结果，因此对于 AMSGrad 算法的效果还有待进一步确定。</p>
<h1 id="算法可视化">算法可视化</h1>
<p>正所谓一图胜千言，<a href="https://imgur.com/a/Hqolp">Alec Radford</a> 提供了 2 张图形象了描述了不同优化算法之间的区别</p>
<img src="/images/cn/2018-02-24-optimization-methods-for-deeplearning/contours-evaluation-optimizers.gif" style="float: left; width: 50%;" />
<img src="/images/cn/2018-02-24-optimization-methods-for-deeplearning/saddle-point-evaluation-optimizers.gif" style="clear: right; width: 50%;" />
<p>左图为 <a href="https://en.wikipedia.org/wiki/Test_functions_for_optimization">Beale Function</a> 在二维平面上的等高线，从图中可以看出 AdaGrad，Adadelta 和 RMSprop 算法很快的找到正确的方向并迅速的收敛到最优解；Momentum 和 NAG 则在初期出现了偏离，但偏离之后调整了方向并收敛到最优解；而 SGD 尽管方向正确，但收敛速度过慢。</p>
<p>右图为包含鞍点的一个三维图像，图像函数为 <code>$z = x^2 - y^2$</code>，从图中可以看出 AdaGrad，Adadelta 和 RMSprop 算法能够相对很快的逃离鞍点，而 Momentum，NAG 和 SGD 则相对比较困难逃离鞍点。</p>
<p>很不幸没能找到 Alec Radford 绘图的原始代码，不过 Louis Tiao 在 <a href="http://louistiao.me/notes/visualizing-and-animating-optimization-algorithms-with-matplotlib/">博客</a> 中给出了绘制类似动图的方法。因此，本文参考该博客和 <code>Keras</code> 源码中对不同优化算法的实现重新绘制了 2 张类似图像，详细过程参见 <a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2018-02-24-optimization-methods-for-deeplearning">源代码</a>，动图如下所示：</p>
<img src="/images/cn/2018-02-24-optimization-methods-for-deeplearning/beales-2d-anim.gif" style="float: left; clear: both; width: 50%;" />
<img src="/images/cn/2018-02-24-optimization-methods-for-deeplearning/saddle-3d-anim.gif" style="clear: both; width: 50%;" />
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Ruder, Sebastian. &ldquo;An overview of gradient descent optimization algorithms.&rdquo; <em>arXiv preprint arXiv:1609.04747</em> (2016).&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Qian, Ning. &ldquo;On the momentum term in gradient descent learning algorithms.&rdquo; <em>Neural networks</em> 12.1 (1999): 145-151.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Nesterov, Yurii. &ldquo;A method for unconstrained convex minimization problem with the rate of convergence O (1/k^2).&rdquo; <em>Doklady AN USSR.</em> Vol. 269. 1983.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Sutskever, Ilya. &ldquo;Training recurrent neural networks.&rdquo; University of Toronto, Toronto, Ont., Canada (2013).&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Duchi, John, Elad Hazan, and Yoram Singer. &ldquo;Adaptive subgradient methods for online learning and stochastic optimization.&rdquo; <em>Journal of Machine Learning Research</em> 12.Jul (2011): 2121-2159.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Pennington, Jeffrey, Richard Socher, and Christopher Manning. &ldquo;Glove: Global vectors for word representation.&rdquo; <em>Proceedings of the 2014 conference on empirical methods in natural language processing (EMNLP).</em> 2014.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Zeiler, Matthew D. &ldquo;ADADELTA: an adaptive learning rate method.&rdquo; <em>arXiv preprint arXiv:1212.5701</em> (2012).&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Hinton, G., Nitish Srivastava, and Kevin Swersky. &ldquo;Rmsprop: Divide the gradient by a running average of its recent magnitude.&rdquo; <em>Neural networks for machine learning, Coursera lecture 6e</em> (2012).&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Kingma, Diederik P., and Jimmy Ba. &ldquo;Adam: A method for stochastic optimization.&rdquo; <em>arXiv preprint arXiv:1412.6980</em> (2014).&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Dozat, Timothy. &ldquo;Incorporating nesterov momentum into adam.&rdquo; (2016).&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Reddi, Sashank J., Satyen Kale, and Sanjiv Kumar. &ldquo;On the convergence of adam and beyond.&rdquo; International Conference on Learning Representations. 2018.&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>生成对抗网络简介 (GAN Introduction)</title><link>https://zeqiang.fun/cn/2018/02/gan-introduction/</link><pubDate>Sat, 03 Feb 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/02/gan-introduction/</guid><description><![CDATA[
        <h1 id="generative-adversarial-networks-gan">Generative Adversarial Networks (GAN)</h1>
<p><strong>生成对抗网络</strong> (<strong>Generative Adversarial Network, GAN</strong>) 是由 Goodfellow <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 于 2014 年提出的一种对抗网络。这个网络框架包含两个部分，一个生成模型 (generative model) 和一个判别模型 (discriminative model)。其中，生成模型可以理解为一个伪造者，试图通过构造假的数据骗过判别模型的甄别；判别模型可以理解为一个警察，尽可能甄别数据是来自于真实样本还是伪造者构造的假数据。两个模型都通过不断的学习提高自己的能力，即生成模型希望生成更真的假数据骗过判别模型，而判别模型希望能学习如何更准确的识别生成模型的假数据。</p>
<p><img src="/images/cn/2018-02-03-gan-introduction/zhoubotong.png" alt=""></p>
<h2 id="网络框架">网络框架</h2>
<p>GAN 由两部分构成，一个<strong>生成器</strong> (<strong>Generator</strong>) 和一个<strong>判别器</strong> (<strong>Discriminator</strong>)。对于生成器，我们需要学习关于数据 <code>$\boldsymbol{x}$</code> 的一个分布 <code>$p_g$</code>，首先定义一个输入数据的先验分布 <code>$p_{\boldsymbol{z}} \left(\boldsymbol{z}\right)$</code>，其次定义一个映射 <code>$G \left(\boldsymbol{z}; \theta_g\right): \boldsymbol{z} \to \boldsymbol{x}$</code>。对于判别器，我们则需要定义一个映射 <code>$D \left(\boldsymbol{x}; \theta_d\right)$</code> 用于表示数据 <code>$\boldsymbol{x}$</code> 是来自于真实数据，还是来自于 <code>$p_g$</code>。GAN 的网络框架如下图所示 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>：</p>
<p><img src="/images/cn/2018-02-03-gan-introduction/gan-framework.svg" alt=""></p>
<h2 id="模型训练">模型训练</h2>
<p>Goodfellow 在文献中给出了一个重要的公式用于求解最优的生成器</p>
<p><code>$$ \min_{G} \max_{D} V\left(D, G\right) = \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}\left(\boldsymbol{z}\right)}{\left[\log \left(1 - D\left(G\left(\boldsymbol{z}\right)\right)\right)\right]} $$</code></p>
<p>上式中，在给定的 <code>$G$</code> 的情况下，<code>$\max_{D} V\left(G, D\right)$</code>衡量的是 <code>$p_{data}$</code> 和 <code>$p_g$</code> 之间的“区别”，因此我们最终的优化目标就是找到最优的 <code>$G^*$</code> 使得 <code>$p_{data}$</code> 和 <code>$p_g$</code> 之间的“区别”最小。</p>
<p>首先，在给定 <code>$G$</code> 的时候，我们可以通过最大化 <code>$V \left(G, D\right)$</code> 得到最优 <code>$D^*$</code></p>
<p><code>$$ \begin{equation} \begin{split} V \left(G, D\right) &amp;= \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}\left(\boldsymbol{z}\right)}{\left[\log \left(1 - D\left(G\left(\boldsymbol{z}\right)\right)\right)\right]} \\ &amp;= \int_{\boldsymbol{x}}{p_{data}\left(\boldsymbol{x}\right) \log D\left(\boldsymbol{x}\right) dx} + \int_{\boldsymbol{z}}{p_{\boldsymbol{z}} \left(\boldsymbol{z}\right) \log \left(1 - D\left(g\left(\boldsymbol{z}\right)\right)\right) dz} \\ &amp;= \int_{\boldsymbol{x}}{p_{data}\left(\boldsymbol{x}\right) \log D\left(\boldsymbol{x}\right) + p_g\left(\boldsymbol{x}\right) \log \left(1 - D\left(\boldsymbol{x}\right)\right) dx} \end{split} \end{equation} $$</code></p>
<p>对于给定的任意 <code>$a, b \in \mathbb{R}^2 \setminus \{0, 0\}$</code>，<code>$a \log\left(x\right) + b \log\left(1 - x\right)$</code>在 <code>$x = \dfrac{a}{a+b}$</code> 处取得最大值，<code>$D$</code> 的最优值为</p>
<p><code>$$ D_{G}^{*} = \dfrac{p_{data} \left(\boldsymbol{x}\right)}{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)} $$</code></p>
<p>因此，<code>$\max_{D} V \left(G, D\right)$</code> 可重写为</p>
<p><code>$$ \begin{equation} \begin{split} &amp;C\left(G\right) \\ =&amp; \max_{D} V \left(G, D\right) = V \left(G, D^*\right) \\ =&amp; \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D_{G}^{*}\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}\left(\boldsymbol{z}\right)}{\left[\log \left(1 - D_{G}^{*}\left(G\left(\boldsymbol{z}\right)\right)\right)\right]} \\ =&amp; \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D_{G}^{*}\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{x} \sim p_g\left(\boldsymbol{x}\right)}{\left[\log \left(1 - D_{G}^{*}\left(\boldsymbol{x}\right)\right)\right]} \\ =&amp; \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log \dfrac{p_{data} \left(\boldsymbol{x}\right)}{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)} \right]} + \mathbb{E}_{\boldsymbol{x} \sim p_g\left(\boldsymbol{x}\right)}{\left[\log  \dfrac{p_g \left(\boldsymbol{x}\right)}{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}\right]} \\ =&amp; \int_{x}{p_{data} \left(\boldsymbol{x}\right) \log \dfrac{\dfrac{1}{2} p_{data} \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} + \int_{x}{p_g \left(\boldsymbol{x}\right) \log  \dfrac{\dfrac{1}{2} p_g \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} \\ =&amp; \int_{x}{p_{data} \left(\boldsymbol{x}\right) \log \dfrac{p_{data} \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} + \int_{x}{p_g \left(\boldsymbol{x}\right) \log  \dfrac{p_g \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} + 2 \log \dfrac{1}{2} \\ =&amp; KL \left(p_{data} \left(\boldsymbol{x}\right) \Vert \dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}\right) + KL \left(p_g \left(\boldsymbol{x}\right) \Vert \dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}\right) - 2 \log 2 \\ =&amp; 2 JS \left(p_{data} \left(\boldsymbol{x}\right) \Vert p_g \left(\boldsymbol{x}\right) \right) - 2 \log 2 \end{split} \end{equation} $$</code></p>
<p>其中 <code>$KL$</code> 表示 KL 散度 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>，<code>$JS$</code> 表示 JS 散度 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>，因此在全局最优情况下 <code>$p_g = p_{data}$</code>。</p>
<p>整个 GAN 的训练过程如下所示：</p>


<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/pseudocode@latest/build/pseudocode.min.css">


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Minibatch SGD for GAN 算法}
\begin{algorithmic}
\REQUIRE $iter, k, m$
\ENSURE $\theta_d, \theta_g$
\FOR{$i = 1, 2, ..., iter$}
    \FOR{$j = 1, 2, ..., k$}
        \STATE Sample minibatch of $m$ noise samples $\{z^{\left(1\right)}, ..., z^{\left(m\right)}\}$ from $p_g \left(\boldsymbol{z}\right)$
        \STATE Sample minibatch of $m$ examples $\{x^{\left(1\right)}, ..., x^{\left(m\right)}\}$ from $p_{data} \left(\boldsymbol{z}\right)$
        \STATE $\theta_d \gets \theta_d \textcolor{red}{+} \nabla_{\theta_d} \dfrac{1}{m} \sum_{i=1}^{m}{\left[\log D \left(x^{\left(i\right)}\right) + \log \left(1 - D \left(G \left(z^{\left(i\right)}\right)\right)\right)\right]}$
    \ENDFOR
    \STATE Sample minibatch of $m$ noise samples $\{z^{\left(1\right)}, ..., z^{\left(m\right)}\}$ from $p_g \left(\boldsymbol{z}\right)$
    \STATE $\theta_g \gets \theta_g \textcolor{red}{-} \nabla_{\theta_g} \dfrac{1}{m} \sum_{i=1}^{m}{\log \left(1 - D \left(G \left(z^{\left(i\right)}\right)\right)\right)}$
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>在实际的训练过程中，我们通常不会直接训练 <code>$G$</code> <strong>最小化</strong> <code>$\log \left(1 - D \left(G \left(\boldsymbol{z}\right)\right)\right)$</code>，因为其在学习过程中的早起处于饱和状态，因此我们通常会通过<strong>最大化</strong> <code>$\log \left(D \left(G \left(z\right)\right)\right)$</code>。</p>
<h2 id="存在的问题">存在的问题</h2>
<p>针对 GAN，包括 Goodfellow 自己在内也提出了其中包含的很多问题 <sup id="fnref1:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>，因此后人也提出了大量的改进，衍生出了大量的 GAN 变种。本章节仅对原始的 GAN 中存在的问题进行简略介绍，相关的改进请参见后续的具体改进算法。</p>
<h3 id="js-散度问题">JS 散度问题</h3>
<p>我们在训练判别器的时候，其目标是最大化 JS 散度，但 JS 散度真的能够很好的帮助我们训练判别器吗？ Wasserstein GAN 一文 <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>给出了不同生成器情况下 JS 散度的变化情况。</p>
<p><img src="/images/cn/2018-02-03-gan-introduction/different-generator-jsd.png" alt=""></p>
<p>上图中，左边为一个基于 MLP 的生成器，右边为一个 DCGAN <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 生成器，两者均有一个 DCGAN 的判别器。根据上文我们可以知道判别器的目标是最大化</p>
<p><code>$$ \begin{equation} \begin{split} L \left(D, \theta_g\right) &amp;= \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D_{G}^{*}\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{x} \sim p_g\left(\boldsymbol{x}\right)}{\left[\log \left(1 - D_{G}^{*}\left(\boldsymbol{x}\right)\right)\right]} \\ &amp;= 2 JS \left(p_{data} \left(\boldsymbol{x}\right) \Vert p_g \left(\boldsymbol{x}\right) \right) - 2 \log 2 \end{split} \end{equation} $$</code></p>
<p>上图中 Y 轴绘制的为 <code>$\dfrac{1}{2} L \left(D, \theta_g\right) + \log 2$</code>，因为 <code>$-2 \log 2 \leq L \left(D, \theta_g\right) \leq 0$</code>，因此我们可得 <code>$0 \leq \dfrac{1}{2} L \left(D, \theta_g\right) + \log 2 \leq \log 2$</code>。从图中我们可以看出，针对两种不同的情况，其值均很快的逼近最大值 <code>$\log 2 \approx 0.69$</code>，当接近最大值的时候，判别器将具有接近于零的损失，此时我们可以发现，尽管 JS 散度很快趋于饱和，但 DCGAN 生成器的效果却仍在不断的变好，因此，使用 JS 散度作为判别其的目标就显得不是很合适。</p>
<h3 id="多样性问题-mode-collapse">多样性问题 Mode Collapse</h3>
<p>Mode Collapse 问题是指生成器更多的是生成了大量相同模式的数据，导致的结果就是生成的数据缺乏多样性，如下图所示 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup>:</p>
<p><img src="/images/cn/2018-02-03-gan-introduction/mode-collapse-demo.png" alt=""></p>
<p>不难看出，其中红色方框圈出来的图像十分相似，这样的问题我们就称之为 Mode Collapse。Goolfellow 曾经从不同的 KL 散度的角度解释引起 Mode Collapse 的问题，但最后发现其并非由散度的不同所导致。对于 KL 散度，其并非是对称的，即 <code>$D_{KL} \left(p_{data} \Vert p_{model}\right)$</code> 与 <code>$D_{KL} \left(p_{model} \Vert p_{data}\right)$</code> 是不同的。在最大化似然估计的时候使用的是前者，而在最小化 JS 散度的时候使用的更类似于后者。如下图所示</p>
<p><img src="/images/cn/2018-02-03-gan-introduction/difference-of-kl-distributation.svg" alt=""></p>
<p>假设我们的模型 <code>$q$</code> 并没有足够能能力去拟合真实数据分布 <code>$p$</code>，假设真实数据由两个二维的高斯分布构成，而模型需要使用一个一维的高斯分布去拟合。在左图中，模型更倾向于覆盖两个高斯分布，也就是说其更倾向与在有真实数据的地方得到更大的概率。在右图中，模型更倾向于覆盖其中一个高斯分布，也就是说其更倾向于在没有真实数据的地方取得更小的概率。这样，如果我们用 JS 散度训练模型的时候就容易出现模式缺失的问题，但尽管我们利用前者去优化模型，但结果中仍然出现了 Mode Collapse 的问题，这也就说明并非 JS 散度问题导致的 Mode Collapse。</p>
<p>针对 Mode Collapse 的问题，出现了大量不同角度的优化</p>
<ul>
<li>基于正则化的优化 <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup></li>
<li>基于 Minibatch 的优化 <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup></li>
<li>基于 Unrolled Optimization 的优化 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup></li>
<li>基于集成算法的优化 <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup></li>
</ul>
<h2 id="mnist-示例">MNIST 示例</h2>
<p>我们利用 MNIST 数据集测试原始的 GAN 模型的效果，代码主要参考了 <a href="https://github.com/eriklindernoren/Keras-GAN"><code>Keras-GAN</code></a>，最终实现代码详见 <a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2018-02-03-gan-introduction/image_gan_keras.py"><code>image_gan_keras.py</code></a>，我们简单对其核心部分进行说明。</p>
<ul>
<li>
<p>生成器</p>
<pre><code class="language-python">def build_generator(self):
    model = Sequential()

    model.add(Dense(int(self._hidden_dim / 4),
                        input_shape=self._noise_shape))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(int(self._hidden_dim / 2)))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(self._hidden_dim))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(np.prod(self._input_shape), activation='tanh'))
    model.add(Reshape(self._input_shape))

    print('Generator Summary: ')
    model.summary()

    noise = Input(shape=self._noise_shape)
    image = model(noise)

    return Model(noise, image)
</code></pre>
</li>
</ul>
<p>在生成器中，我们使用了一个包含3个隐含层的全链接网络，其中 <code>self._hidden_dim</code> 是我们定义的隐含节点最多一层的节点数；<code>self._noise_shape</code> 为用于生成器的噪音数据的形状；<code>self._input_shape</code> 为输入数据形状，即图片数据的形状，中间层次采用的激活函数为 <code>LeakyReLU</code>，最后一层采用的激活函数为 <code>tanh</code>。</p>
<ul>
<li>
<p>判别器</p>
<pre><code class="language-python">def build_discriminator(self):
    model = Sequential()

    model.add(Flatten(input_shape=self._input_shape))
    model.add(Dense(int(self._hidden_dim / 2)))
    model.add(LeakyReLU(alpha=0.2))
    model.add(Dense(int(self._hidden_dim / 4)))
    model.add(LeakyReLU(alpha=0.2))
    model.add(Dense(1, activation='sigmoid'))

    print('Discriminator Summary: ')
    model.summary()

    image = Input(shape=self._input_shape)
    label = model(image)

    return Model(image, label)
</code></pre>
</li>
</ul>
<p>在判别器中，我们使用了一个包含2个隐含层的全链接网络，中间层次采用的激活函数为 <code>LeakyReLU</code>，最后一层采用的激活函数为 <code>sigmoid</code>。</p>
<ul>
<li>
<p>对抗网络</p>
<pre><code class="language-python">class ImageBasicGAN():
    def __init__(self, width, height, channels,
                 a_optimizer=Adam(1e-4, beta_1=0.5),
                 g_optimizer=Adam(1e-4, beta_1=0.5),
                 d_optimizer=Adam(1e-4, beta_1=0.5),
                 noise_dim=100, hidden_dim=1024):
        '''

        Args:
            width: 图像宽度
            height: 图像高度
            channels: 图像颜色通道数
            a_optimizer: 对抗网络优化器
            g_optimizer: 生成器优化器
            d_optimizer: 判别器优化器
            noise_dim: 噪音数据维度
            hidden_dim: 隐含层最大维度
        '''

        # 省略一大坨代码

        # 构建和编译判别器
        self._discriminator = self.build_discriminator()
        self._discriminator.compile(loss='binary_crossentropy',
                                    optimizer=d_optimizer,
                                    metrics=['accuracy'])

        # 构建和编译生成器
        self._generator = self.build_generator()
        self._generator.compile(loss='binary_crossentropy',
                                optimizer=g_optimizer)

        # 生成器利用噪声数据作为输入
        noise = Input(shape=self._noise_shape)
        generated_image = self._generator(noise)

        # 当训练整个对抗网络时，仅训练生成器
        self._discriminator.trainable = False

        # 判别器将生成的图像作为输入
        label = self._discriminator(generated_image)

        # 构建和编译整个对抗网络
        self._adversarial = Model(noise, label)
        self._adversarial.compile(loss='binary_crossentropy',
                                  optimizer=a_optimizer)
</code></pre>
</li>
</ul>
<p>在构造整个对抗网络的时候，需要注意我们训练完判别器后，通过训练整个对抗网络进而训练生成器的时候是固定住训练好的判别器的，因此在训练整个对抗网络的时候我们应该将判别器置为无需训练的状态。</p>
<ul>
<li>
<p>训练过程</p>
<pre><code class="language-python">def train(self, x_train, output_dir, iters,
          batch_size=32, k=1, save_interval=200):
    ''' 训练模型

    Args:
        x_train: 训练数据
        output_dir: 相关输出路径
        iters: 迭代次数
        batch_size: 批大小
        k: K
        save_interval: 结果保存间隔
    '''

    # 省略一大坨代码

    for iter in range(iters):
        # 训练判别器
        for _ in range(k):
            train_indices = np.random.randint(0, x_train.shape[0],
                                              batch_size)
            train_images = x_train[train_indices]

            noises = np.random.normal(0, 1, (batch_size, self._noise_dim))
            generated_images = self._generator.predict(noises)

            self._discriminator.train_on_batch(train_images,
                                               np.ones((batch_size, 1)))
            self._discriminator.train_on_batch(generated_images,
                                               np.zeros((batch_size, 1)))

        # 训练生成器
        noises = np.random.normal(0, 1, (batch_size, self._noise_dim))
        labels = np.ones(batch_size)

        self._adversarial.train_on_batch(noises, labels)

    # 再省略一大坨代码
</code></pre>
</li>
</ul>
<p>在训练整个对抗网络的时候，我们对于一个给定的生成器，我们将生成器生成的数据作为负样本，将从真实数据中采样的数据作为正样本训练判别器。Goodfellow 在描述 GAN 训练的过程中，对于给定的生成器，训练判别器 <code>$k$</code> 次，不过通常取 <code>$k = 1$</code>。训练好判别器后，再随机生成噪音数据用于训练生成器，周而复始直至达到最大迭代次数。</p>
<p>在整个训练过程中，我们分别记录了判别器和生成器的损失的变化，以及判别器的准确率的变化，如下图所示：</p>
<p><img src="/images/cn/2018-02-03-gan-introduction/mnist-gan-keras-train-history.png" alt=""></p>
<p>从上图中我们可以看出，在训练开始阶段，判别器能够相对容易的识别出哪些数据是来自于真实数据的采样，哪些数据是来自于生成器的伪造数据。随着训练的不断进行，判别器的准确率逐渐下降，并稳定在 60% 左右，也就是说生成器伪造的数据越来越像真实的数据，判别器越来越难进行甄别。</p>
<p>下图中我们展示了利用 MNIST 数据集，进行 30000 次的迭代，每 1000 次截取 100 张生成器利用相同噪音数据伪造的图像，最后合成的一张生成图片的变化动图。</p>
<p><img src="/images/cn/2018-02-03-gan-introduction/mnist-gan-generated-images.gif" alt=""></p>
<h1 id="deep-convolutional-gan">Deep Convolutional GAN</h1>
<p>DCGAN (Deep Convolutional GAN) 是由 Radford <sup id="fnref1:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> 等人提出的一种对原始 GAN 的变种，其基本的思想就是将原始 GAN 中的全链接层用卷积神经网络代替。在文中，Radford 等人给出构建一个稳定的 DCGAN 的建议，如下：</p>
<ul>
<li>在网络中不使用 pooling 层，而是使用多步长的卷积层 (判别器) 和多步长的反卷积层 (生成器)。</li>
<li>在生成器和判别器中均使用批标准化。</li>
<li>对于深层的框架，去掉全链接层。</li>
<li>在生成器中使用 ReLU 激活函数，最后一层使用 Tanh 激活函数。</li>
<li>在判别器中使用 LeakyReLU 激活函数。</li>
</ul>
<p>我们利用 MNIST 数据集测试 DCGAN 模型的效果，最终实现代码详见 <a href="https://github.com/leovan/leovan.me/tree/master/scripts/cn/2018-02-03-gan-introduction/image_dcgan_keras.py"><code>image_dcgan_keras.py</code></a>。训练过程中判别器和生成器的损失的变化，以及判别器的准确率的变化，如下图所示：</p>
<p><img src="/images/cn/2018-02-03-gan-introduction/mnist-dcgan-keras-train-history.png" alt=""></p>
<p>下图中我们展示了利用 MNIST 数据集，进行 30000 次的迭代，每 1000 次截取 100 张生成器利用相同噪音数据伪造的图像，最后合成的一张生成图片的变化动图。</p>
<p><img src="/images/cn/2018-02-03-gan-introduction/mnist-dcgan-generated-images.gif" alt=""></p>
<p>从生成的结果中可以看出，DCGAN 生成的图片的质量还是优于原始的 GAN 的，在原始的 GAN 中我们能够明显的看出其中仍旧包含大量的噪音点，而在 DCGAN 中这种情况几乎不存在了。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Goodfellow, Ian, et al. &ldquo;Generative adversarial nets.&rdquo; <em>Advances in neural information processing systems</em>. 2014.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Goodfellow, Ian. &ldquo;NIPS 2016 tutorial: Generative adversarial networks.&rdquo; <em>arXiv preprint arXiv:1701.00160</em> (2016).&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://en.wikipedia.org/wiki/Kullback">https://en.wikipedia.org/wiki/Kullback</a>–Leibler_divergence&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://en.wikipedia.org/wiki/Jensen">https://en.wikipedia.org/wiki/Jensen</a>–Shannon_divergence&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Arjovsky, Martin, Soumith Chintala, and Léon Bottou. &ldquo;Wasserstein gan.&rdquo; <em>arXiv preprint arXiv:1701.07875</em> (2017).&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Radford, Alec, Luke Metz, and Soumith Chintala. &ldquo;Unsupervised representation learning with deep convolutional generative adversarial networks.&rdquo; <em>arXiv preprint arXiv:1511.06434</em> (2015).&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p><a href="http://speech.ee.ntu.edu.tw/~tlkagk/courses/MLDS_2017/Lecture/GAN%20(v11).pdf">http://speech.ee.ntu.edu.tw/~tlkagk/courses/MLDS_2017/Lecture/GAN%20(v11).pdf</a>&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Che, Tong, et al. &ldquo;Mode regularized generative adversarial networks.&rdquo; <em>arXiv preprint arXiv:1612.02136</em> (2016).&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Salimans, Tim, et al. &ldquo;Improved techniques for training gans.&rdquo; <em>Advances in Neural Information Processing Systems.</em> 2016.&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>Metz, Luke, et al. &ldquo;Unrolled generative adversarial networks.&rdquo; <em>arXiv preprint arXiv:1611.02163</em> (2016).&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Tolstikhin, Ilya O., et al. &ldquo;Adagan: Boosting generative models.&rdquo; <em>Advances in Neural Information Processing Systems.</em> 2017.&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>Ising 模型，Hopfield 网络和受限的玻尔兹曼机 (Ising, Hopfield and RBM)</title><link>https://zeqiang.fun/cn/2018/01/ising-hopfield-and-rbm/</link><pubDate>Wed, 17 Jan 2018 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2018/01/ising-hopfield-and-rbm/</guid><description><![CDATA[
        <p><code>$\renewcommand{\sign}{\operatorname{sign}}$</code></p>
<h2 id="ising-模型">Ising 模型</h2>
<p><a href="https://zh.wikipedia.org/zh/%E6%98%93%E8%BE%9B%E6%A8%A1%E5%9E%8B">Ising 模型</a>最早是由物理学家威廉·冷次在 1920 年发明的，他把该模型当成是一个给他学生恩斯特·易辛的问题。易辛在他一篇 1924 年的论文 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 中求得了一维易辛模型的解析解，并且证明它不会产生相变。 二维方晶格易辛模型相对于一维的难出许多，因此其解析的描述在一段时间之后才在 1943 年由拉斯·昂萨格给出 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>。</p>
<p>Ising 模型假设铁磁物质是由一堆规则排列的小磁针构成，每个磁针只有上下两个方向。相邻的小磁针之间通过能量约束发生相互作用，同时受到环境热噪声的干扰而发生磁性的随机转变。涨落的大小由关键的温度参数决定，温度越高，随机涨落干扰越强，小磁针越容易发生无序而剧烈地状态转变，从而让上下两个方向的磁性相互抵消，整个系统消失磁性，如果温度很低，则小磁针相对宁静，系统处于能量约束高的状态，大量的小磁针方向一致，铁磁系统展现出磁性。而当系统处于临界温度 <code>$T_C$</code> 时，Ising 模型表现出一系列幂律行为和自相似现象 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>。</p>
<p>由于 Ising 模型的高度抽象，可以很容易地将它应用到其他领域之中。例如，将每个小磁针比喻为某个村落中的村民，而将小磁针上下的两种状态比喻成个体所具备的两种政治观点，相邻小磁针之间的相互作用比喻成村民之间观点的影响，环境的温度比喻成每个村民对自己意见不坚持的程度，这样 Ising 模型就可以建模该村落中不同政治见解的动态演化。在社会科学中，人们已经将 Ising 模型应用于股票市场、种族隔离、政治选择等不同的问题。另一方面，如果将小磁针比喻成神经元细胞，向上向下的状态比喻成神经元的激活与抑制，小磁针的相互作用比喻成神经元之间的信号传导，那么，Ising 模型的变种还可以用来建模神经网络系统，从而搭建可适应环境、不断学习的机器，例如 Hopfield 网络或 Boltzmann 机。</p>
<p>考虑一个二维的情况</p>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/ising-model.svg" alt=""></p>
<p>如图所示，每个节点都有两种状态 <code>$s_i \in \{+1, -1\}$</code>，则我们可以定义这个系统的能量为</p>
<p><code>$$ E = -H \sum_{i=1}^{N}{s_i} - J \sum_{&lt;i, j&gt;}{s_i s_j} $$</code></p>
<p>其中 <code>$H$</code> 为外界磁场的强度，<code>$J$</code> 为能量耦合常数，<code>$\sum_{&lt;i, j&gt;}$</code>表示对于相邻的两个节点的函数值求和。因此，可以得出</p>
<ol>
<li>当每个节点的方向同外部磁场一致时，系统能量越小；反之系统能量越大。</li>
<li>对于 <code>$J &gt; 0$</code>，当相邻的节点方向相同时，系统能量越小；反之系统能量越大。</li>
</ol>
<p>对于整个系统的演变，除了系统的总能量以外，还受到节点所处环境的热噪声影响。我们利用温度 <code>$T$</code> 表示环境对节点的影响，当 <code>$T$</code> 越高时，节点状态发生变化的可能性越大。此时，则有两种力量作用在每个节点上</p>
<ol>
<li>节点邻居和外部磁场的影响，这种影响使得当前节点尽可能的同其邻居和外部磁场保持一致，即尽可能是系统的总能量达到最小。</li>
<li>环境的影响，这种影响使得每个节点的状态以一定的概率发生随机变化。</li>
</ol>
<p>不难想像，当 <code>$T = 0$</code> 时，节点状态完全受其邻居和外部磁场影响，当 <code>$J = 0, H = 0$</code> 时，节点处于完全的随机状态。</p>
<p>对于 Ising 模型，我们利用蒙特卡罗方法进行模拟。初始化系统状态为 <code>$s_i^{\left(0\right)}$</code>，对于任意时刻 <code>$t$</code>，对其状态 <code>$s_i^{\left(t\right)}$</code>进行一个改变，将其中一个节点变为相反的状态，得到新的状态 <code>$s'_i$</code></p>
<p><code>$$ s_i^{\left(t+1\right)} = \begin{cases} s'_i &amp; \text{with probablity of } \mu \\ s_i^{\left(t\right)} &amp; \text{with probablity of } 1-\mu \end{cases} $$</code></p>
<p>其中 <code>$\mu = \min\left\lbrace\dfrac{e^{E\left(s_i^{\left(t\right)}\right) - E\left(s'_i\right)}}{kT}, 1\right\rbrace$</code> 表示接受转移的概率；<code>$k \approx 1.38 \times 10^{23}$</code> 为玻尔兹曼常数。我们利用蒙特卡罗方法对其进行模拟 <code>$T = 4J/k$</code>的情况，我们分别保留第 <code>$0, 1, 5, 50, 500, 5000$</code> 步的模拟结果</p>
<pre><code class="language-r"># 每一轮状态转移
each_round &lt;- function(current_matrix, ising_config) {
    n_row &lt;- nrow(current_matrix)
    n_col &lt;- ncol(current_matrix)
    
    for (i in 1:n_row) {
        for (j in 1:n_col) {
            current_row &lt;- sample(1:n_row, 1)
            current_col &lt;- sample(1:n_col, 1)
            s &lt;- current_matrix[current_row, current_col]
            e &lt;- -(current_matrix[(current_row-1-1)%%n_row+1, current_col] +
                current_matrix[current_row, (current_col-1-1)%%n_col+1] +
                current_matrix[(current_row+1)%%n_row, current_col] +
                current_matrix[current_row, (current_col+1)%%n_col]) *
                s * ising_config$j
            mu &lt;- min(exp((e + e) / (ising_config$k * ising_config$t)), 1)
            mu_random &lt;- runif(1)
            
            if (mu_random &lt; mu) {
                s &lt;- -1 * s
            }
            
            current_matrix[current_row, current_col] &lt;- s
        }
    }
    
    current_matrix
}

# Ising 模拟
ising_simulation &lt;- function(N, iter, ising_config, saved_steps) {
    set.seed(112358)
    current_matrix &lt;- matrix(sample(0:1, N^2, replace = T), N, N)*2-1
    saved_matrix &lt;- list()
    
    if (0 %in% saved_steps) {
        saved_matrix &lt;- c(saved_matrix, list(current_matrix))
    }
    
    for (i in 1:iter) {
        if (i %in% saved_steps) {
            saved_matrix &lt;- c(saved_matrix, list(current_matrix))
        }
        
        current_matrix &lt;- each_round(current_matrix, ising_config)
        
        if (i %% 1000 == 0) {
            cat(paste0(&quot;Steps: &quot;, i, '\n'))
        }
    }
    
    saved_matrix
}

# T = 4J/K，方便模拟取 j = 1, k = 1, t = 4
ising_config &lt;- list(j = 1, k = 1, t = 4)
diff_steps_matrix &lt;- ising_simulation(100, 5000, ising_config,
                                      c(0, 1, 5, 50, 500, 5000))
</code></pre>
<p>模拟结果可视化效果如图所示</p>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/ising-different-steps.png" alt=""></p>
<p>对于二维的 Ising 模型，存在一个相变点，在相变点上的温度 <code>$T_c$</code> 满足</p>
<p><code>$$ \sinh\left(\dfrac{2J_1}{kT_c}\right) \sinh\left(\dfrac{2J_2}{kT_c}\right) = 1 $$</code></p>
<p>若 <code>$J_1 = J_2$</code>，则</p>
<p><code>$$ T_c = \dfrac{2J}{k \ln\left(1 + \sqrt{2}\right)} \approx 2.27 \dfrac{J}{k} $$</code></p>
<p>称之为临界温度。当温度小于临界值的时候，Ising 模型中大多数节点状态相同，系统处于较为秩序的状态。当温度大于临界值的时候，大多数节点的状态较为混乱，系统处于随机的状态。而当温度接近临界的时候，系统的运行介于随机与秩序之间，也就是进入了混沌的边缘地带，这种状态称为临界状态。</p>
<p>我们模拟不同温度下，系统在运行 <code>$50$</code> 步时的状态</p>
<pre><code class="language-r">ising_config_t &lt;- c(1, 2, 2.27, 2.5, 3, 6)
diff_t_matrix &lt;- lapply(ising_config_t, function(t) {
    ising_config &lt;- list(j = 1, k = 1, t = t)
    ising_simulation(100, 50, ising_config, c(50))
})
</code></pre>
<p>模拟结果可视化效果如图所示</p>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/ising-different-t.png" alt=""></p>
<h2 id="hopfield-神经网络">Hopfield 神经网络</h2>
<p>Hopfield 神经网络 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 是一种基于能量的反馈人工神经网络。Hopfield 神经网络分为离散型 (Discrete Hopfield Neural Network, DHNN) 和 连续性 (Continues Hopfield Neural Network, CHNN)。</p>
<h3 id="离散型-hopfield-神经网络">离散型 Hopfield 神经网络</h3>
<h4 id="网络结构">网络结构</h4>
<p>对于离散型 Hopfield 神经网络，其网络结果如下</p>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/hopfield-network.png" alt=""></p>
<p>对于具有 <code>$n$</code> 个神经元的网络，我们设 <code>$t$</code> 时刻的网络状态为 <code>$\boldsymbol{X}^{\left(t\right)} = \left(x_1^{\left(t\right)}, x_2^{\left(t\right)}, ..., x_n^{\left(t\right)}\right)^T$</code>，对于 <code>$t+1$</code> 时刻网络的状态</p>
<p><code>$$ x_i^{\left(t+1\right)} = f \left(net_i\right) $$</code></p>
<p>其中，DHNN 中 <code>$f$</code> 多为符号函数，即</p>
<p><code>$$ x_i = \sign \left(net_i\right) = \begin{cases} 1, net_i \geq 0 \\ -1, net_i &lt; 0 \end{cases} $$</code></p>
<p><code>$net_i$</code> 为一个节点的输入，为</p>
<p><code>$$ net_i = \sum_{j=1}^{n}{\left(w_{ij}x_j - T_i\right)} $$</code></p>
<p>其中 <code>$T_i$</code> 为每个神经元的阈值，对于 DHNN，一般有 <code>$w_{ii} = 0, w_{ij} = w_{ji}$</code>，当反馈网络稳定后，稳定后的状态即为网络的输出。网络的更新主要有两种状态，<strong>异步方式</strong>和<strong>同步方式</strong>。</p>
<p>对于异步方式的更新方法，每一次仅改变一个神经元 <code>$j$</code> 的状态，即</p>
<p><code>$$ x_i^{\left(t+1\right)} = \begin{cases} \sign\left(net_i^{\left(t\right)}\right), i = j \\ x_i^{\left(t\right)}, i \neq j \end{cases} $$</code></p>
<p>对于同步方式的更新方法，每一次需改变所有神经元的状态，即</p>
<p><code>$$ x_i^{\left(t+1\right)} = \sign\left(net_i^{\left(t\right)}\right) $$</code></p>
<h4 id="网络稳定性">网络稳定性</h4>
<p>我们可以将反馈网络看做一个非线性动力学系统，因此这个系统最后可能会收敛到一个稳态，或在有限状态之间振荡，亦或是状态为无穷多个即混沌现象。对于 DHNN 因为其网络状态是有限的，因此不会出现混沌的现象。若一个反馈网络达到一个稳态状态 <code>$\boldsymbol{X}$</code> 时，即 <code>$\boldsymbol{X}^{\left(t+1\right)} = \boldsymbol{X}^{\left(t\right)}$</code> ，则称这个状态为一个吸引子。在 Hopfield 网络结构和权重确定的情况下，其具有 <code>$M$</code> 个吸引子，因此我们可以认为这个网络具有存储 <code>$M$</code> 个记忆的能力。</p>
<p>设 <code>$\boldsymbol{X}$</code> 为网络的一个吸引子，权重矩阵 <code>$\boldsymbol{W}$</code> 是一个对称阵，则定义 <code>$t$</code> 时刻网络的能量函数为</p>
<p><code>$$ E\left(t\right) = -\dfrac{1}{2} \boldsymbol{X}^{\left(t\right)T} \boldsymbol{W} \boldsymbol{X}^{\left(t\right)} + \boldsymbol{X}^{\left(t\right)T} \boldsymbol{T} $$</code></p>
<p>则定义网络能量的变化量</p>
<p><code>$$ \Delta E\left(t\right) = E\left(t+1\right) - E\left(t\right) $$</code></p>
<p>则以<strong>异步更新</strong>方式，不难推导得出</p>
<p><code>$$ \begin{equation} \begin{split} \Delta E\left(t\right) = -\Delta x_i^{\left(t\right)} \left(\sum_{j=1}^{n}{\left(w_{ij}x_j - T_j\right)}\right) - \dfrac{1}{2} \Delta x_i^{\left(t\right)2} w_{ii} \end{split} \end{equation} $$</code></p>
<p>由于网络中的神经元不存在自反馈，即 <code>$w_{ii} = 0$</code>，则上式可以化简为</p>
<p><code>$$ \Delta E\left(t\right) = -\Delta x_i^{\left(t\right)} net_i^{\left(t\right)} $$</code></p>
<p>因此，对于如上的能量变化，可分为 3 中情况：</p>
<ol>
<li>当 <code>$x_i^{\left(t\right)} = -1, x_i^{\left(t+1\right)} = 1$</code> 时，<code>$\Delta x_i^{\left(t\right)} = 2, net_i^{\left(t\right)} \geq 0$</code>，则可得 <code>$\Delta E \left(t\right) \leq 0$</code>。</li>
<li>当 <code>$x_i^{\left(t\right)} = 1, x_i^{\left(t+1\right)} = -1$</code> 时，<code>$\Delta x_i^{\left(t\right)} = -2, net_i^{\left(t\right)} &lt; 0$</code>，则可得 <code>$\Delta E \left(t\right) &lt; 0$</code>。</li>
<li>当 <code>$x_i^{\left(t\right)} = x_i^{\left(t+1\right)}$</code> 时，<code>$\Delta x_i^{\left(t\right)} = 0$</code>，则可得 <code>$\Delta E \left(t\right) = 0$</code>。</li>
</ol>
<p>则对于任何情况，<code>$\Delta E \left(t\right) \leq 0$</code>，也就是说在网络不断变化的过程中，网络的总能量是一直下降或保持不变的，因此网络的能量最终会收敛到一个常数。</p>
<p>设 <code>$\boldsymbol{X}'$</code> 为吸引子，对于异步更新方式，若<strong>存在</strong>一个变换顺序，使得网络可以从状态 <code>$\boldsymbol{X}$</code> 转移到 <code>$\boldsymbol{X}'$</code>，则称 <code>$\boldsymbol{X}$</code> 弱吸引到 <code>$\boldsymbol{X}'$</code>，这些 <code>$\boldsymbol{X}$</code> 的集合称之为 <code>$\boldsymbol{X}$</code> 的弱吸引域；若对于<strong>任意</strong>变换顺序，都能够使得网络可以从状态 <code>$\boldsymbol{X}$</code> 转移到 <code>$\boldsymbol{X}'$</code>，则称 <code>$\boldsymbol{X}$</code> 强吸引到 <code>$\boldsymbol{X}'$</code>，对于这些 <code>$\boldsymbol{X}$</code> 称之为 <code>$\boldsymbol{X}$</code> 的强吸引域。</p>
<p>对于 Hopfield 网络的权重，我们利用 Hebbian 规则进行设计。Hebbian 规则认为如果两个神经元同步激发，则它们之间的权重增加；如果单独激发，则权重减少。则对于给定的 <code>$p$</code> 个模式样本 <code>$\boldsymbol{X}^k, k = 1, 2, ..., p$</code>，其中 <code>$x \in \{-1, 1\}^n$</code> 且样本之间两两正交，则权重计算公式为</p>
<p><code>$$ w_{ij} = \dfrac{1}{n} \sum_{k=1}^{p}{x_i^k x_j^k} $$</code></p>
<p>则对于给定的样本 <code>$\boldsymbol{X}$</code> 确定为网络的吸引子，但对于有些非给定的样本也可能是网络的吸引子，这些吸引子称之为伪吸引子。以上权重的计算是基于两两正交的样本得到的，但真实情况下很难保证样本两两正交，对于非正交的模式，网络的存储能力则会大大下降。根据 Abu-Mostafa<sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> 的研究表明，当模式的数量 <code>$p$</code> 大于 <code>$0.15 n$</code> 时，网络的推断就很可能出错，也就是结果会收敛到伪吸引子上。</p>
<h4 id="示例">示例</h4>
<p>我们通过一个手写数字识别的例子介绍一些 Hopfield 网络的功能，我们存在如下 10 个数字的图片，每张为像素 16*16 的二值化图片，其中背景色为白色，前景色为黑色 (每个图片的名称为 <code>num.png</code>，图片位于 <code>/images/cn/2018-01-17-ising-hopfield-and-rbm</code> 目录)。</p>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/digits.png" alt=""></p>
<p>首先我们载入每张图片的数据</p>
<pre><code class="language-r">library(EBImage)

# 载入数据
digits &lt;- lapply(0:9, function(num) {
    readImage(paste0(num, '.png'))
})

# 转换图像为 16*16 的一维向量
# 将 (0, 1) 转换为 (-1, 1)
digits_patterns &lt;- lapply(digits, function(digit) {
    pixels &lt;- c(digit)
    pixels * 2 - 1
})
</code></pre>
<p>接下来利用这 10 个模式训练一个 Hopfield 网络</p>
<pre><code class="language-r">#' 训练 Hopfield 网络
#' 
#' @param n 网络节点个数
#' @param pattern_list 模式列表
#' @return 训练好的 Hopfield 网络
train_hopfield &lt;- function(n, pattern_list) {
    weights &lt;- matrix(rep(0, n*n), n, n)
    n_patterns &lt;- length(pattern_list)
    
    for (i in 1:n_patterns) {
        weights &lt;- weights + pattern_list[[i]] %o% pattern_list[[i]]
    }
    diag(weights) &lt;- 0
    weights &lt;- weights / n_patterns
    
    list(weights = weights, n = n)
}

# 训练 Hopfield 网络
digits_hopfield_network &lt;- train_hopfield(16*16, digits_patterns)
</code></pre>
<p>为了测试 Hopfiled 网络的记忆能力，我们利用 10 个模式生成一些测试数据，我们分别去掉图像的右边或下边的 5 个像素，生成新的 20 张测试图片</p>
<pre><code class="language-r"># 构造测试数据
digits_test_remove_right &lt;- lapply(0:9, function(num) {
    digit_test &lt;- digits[[num+1]]
    digit_test[12:16, ] &lt;- 1
    digit_test
})
digits_test_remove_bottom &lt;- lapply(0:9, function(num) {
    digit_test &lt;- digits[[num+1]]
    digit_test[, 12:16] &lt;- 1
    digit_test
})
digits_test &lt;- c(digits_test_remove_right, digits_test_remove_bottom)

# 转换图像为 16*16 的一维向量
# 将 (0, 1) 转换为 (-1, 1)
digits_test_patterns &lt;- lapply(digits_test, function(digit) {
    pixels &lt;- c(digit)
    pixels * 2 - 1
})
</code></pre>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/digits-test.png" alt=""></p>
<p>我们利用训练好的 Hopfield 网络运行测试数据，我们迭代 300 次并保存最后的网络输出</p>
<pre><code class="language-r">#' 运行 Hopfiled 网络
#' @param hopfield_network 训练好的 Hopfield 网络
#' @param pattern 输入的模式
#' @param max_iter 最大迭代次数
#' @param save_history 是否保存状态变化历史
#' @return 最终的模式 (以及历史模式)
run_hopfield &lt;- function(hopfield_network, pattern,
                         max_iter = 100, save_history = T) {
    last_pattern &lt;- pattern
    history_patterns &lt;- list()
    
    for (iter in 1:max_iter) {
        current_pattern &lt;- last_pattern
        
        i &lt;- round(runif(1, 1, hopfield_network$n))
        net_i &lt;- hopfield_network$weights[i, ] %*% current_pattern
        current_pattern[i] &lt;- ifelse(net_i &lt; 0, -1, 1)
        
        if (save_history) {
            history_patterns[[iter]] &lt;- last_pattern
        }
        
        last_pattern &lt;- current_pattern
    }
    
    list(history_patterns = history_patterns,
         final_pattern = last_pattern)
}

# 运行 Hopfield 网络，获取测试数据结果
digits_test_results_patterns &lt;- lapply(digits_test_patterns,
                                       function(pattern) {
    run_hopfield(digits_hopfield_network, pattern, max_iter = 300)
})

# 转换测试数据结果为图片
digits_test_results &lt;- lapply(digits_test_results_patterns,
                              function(result) {
    each_dim &lt;- sqrt(digits_hopfield_network$n)
    Image((result$final_pattern + 1) / 2,
          dim = c(each_dim, each_dim),
          colormode = 'Grayscale')
})
</code></pre>
<p>网络变换过程中，图像的变换如图所示</p>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/digits-test-results.gif" alt=""></p>
<p>最终网络的输出如图所示</p>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/digits-test-results.png" alt=""></p>
<p>从结果中可以看出，部分测试图片还是得到了比较好的恢复，但如上文所说，由于我们给定的模式之间并不是两两正交的，因此，网络的推断就很可能出错 (例如：数字 5 恢复的结果更像 9 多一些)，甚至结果会收敛到伪吸引子上。</p>
<h3 id="连续型-hopfield-神经网络">连续型 Hopfield 神经网络</h3>
<h4 id="网络结构-1">网络结构</h4>
<p>连续型 Hopfield 网络相比于离散型 Hopfield 网络的主要差别在于：</p>
<ol>
<li>网络中所有的神经元随时间 <code>$t$</code> 同时更新，网络状态随时间连续变化。</li>
<li>神经元的状态转移函数为一个 S 型函数，例如
<code>$$ v_i = f\left(u_i\right) = \dfrac{1}{1 + e^{\dfrac{-2 u_i}{\gamma}}} = \dfrac{1}{2} \left(1 + \tanh \dfrac{u_i}{\gamma}\right) $$</code>
其中，<code>$v_i$</code> 表示一个神经元的输出，<code>$u_i$</code> 表示一个神经元的输入。</li>
</ol>
<p>对于理想情况，网络的能量函数可以写为<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup></p>
<p><code>$$ E = -\dfrac{1}{2} \sum_{i=1}^{n}{\sum_{j=1}^{n}{w_{ij} v_i v_j}} - \sum_{i=1}^{n} v_i I_i $$</code></p>
<p>可以得出，随着网络的演变，网络的总能量是降低的，随着网络中节点的不断变化，网络最终收敛到一个稳定的状态。</p>
<h4 id="tsp-问题求解">TSP 问题求解</h4>
<p>旅行推销员问题 (Travelling salesman problem, TSP) 是指给定一系列城市和每对城市之间的距离，求解访问每一座城市一次并回到起始城市的最短路径 <sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup>。TSP 问题是一个 NP-hard 问题 <sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup>。</p>
<p>对于 TSP 问题，我们给定一个城市指之间的距离矩阵</p>
<p><code>$$ D = \left\lgroup \begin{array}{cccc} d_{11} &amp; d_{12} &amp; \cdots &amp; d_{1n} \\ d_{21} &amp; d_{22} &amp; \cdots &amp; d_{2n} \\ \vdots &amp; \vdots &amp;        &amp; \vdots \\ d_{n1} &amp; d_{n2} &amp; \cdots &amp; d_{nn} \end{array} \right\rgroup $$</code></p>
<p>其中 <code>$d_{ij} = d_{ji}, i \neq j$</code> 表示城市 <code>$i$</code> 和城市 <code>$j$</code> 之间的距离，<code>$d_{ij} = 0, i = j$</code>。TSP 问题的优化目标是找到一条路径访问每一座城市一次并回到起始城市，我们利用一个矩阵表示访问城市的路径</p>
<p><code>$$ V = \left\lgroup \begin{array}{cccc} v_{11} &amp; v_{12} &amp; \cdots &amp; v_{1n} \\ v_{21} &amp; v_{22} &amp; \cdots &amp; v_{2n} \\ \vdots &amp; \vdots &amp;        &amp; \vdots \\ v_{n1} &amp; v_{n2} &amp; \cdots &amp; v_{nn} \end{array} \right\rgroup $$</code></p>
<p>其中 <code>$v_{xi} = 1$</code> 表示第 <code>$i$</code> 次访问城市 <code>$x$</code>，因此对于矩阵 <code>$V$</code>，其每一行每一列仅有一个元素值为 <code>$1$</code>，其他元素值均为 <code>$0$</code>。</p>
<p>对于 TSP 问题，我们可以得到如下约束条件</p>
<ul>
<li>城市约束</li>
</ul>
<p>因为每个城市只能访问一次，因此对于第 <code>$x$</code> 行仅能有一个元素是 <code>$1$</code>，其他均为 <code>$0$</code>，即任意两个相邻元素的乘积为 <code>$0$</code></p>
<p><code>$$ \sum_{i=1}^{n-1}{\sum_{j=i+1}^{n}{v_{xi}v_{xj}}} = 0 $$</code></p>
<p>则对于城市约束，我们得到该约束对应的能量分量为</p>
<p><code>$$ E_1 = \dfrac{1}{2} A \sum_{x=1}^{n}{\sum_{i=1}^{n-1}{\sum_{j=i+1}^{n}{v_{xi}v_{xj}}}} $$</code></p>
<ul>
<li>时间约束</li>
</ul>
<p>因为每一时刻仅能够访问一个城市，因此对于第 <code>$i$</code> 行仅能有一个元素是 <code>$1$</code>，其他均为 <code>$0$</code>，即任意两个相邻元素的乘积为 <code>$0$</code></p>
<p><code>$$ \sum_{x=1}^{n-1}{\sum_{y=x+1}^{n}{v_{xi}v_{yi}}} = 0 $$</code></p>
<p>则对于时间约束，我们得到该约束对应的能量分量为</p>
<p><code>$$ E_2 = \dfrac{1}{2} B \sum_{i=1}^{n}{\sum_{x=1}^{n-1}{\sum_{y=x+1}^{n}{v_{xi}v_{yi}}}} $$</code></p>
<ul>
<li>有效性约束</li>
</ul>
<p>当矩阵 <code>$V$</code> 中所有的元素均为 <code>$0$</code> 的时候，可得 <code>$E_1 = 0, E_2 = 0$</code>，但显然这并不是一个有效的路径，因此我们需要保证矩阵 <code>$V$</code> 中元素值为 <code>$1$</code> 的个数为 <code>$n$</code>，即</p>
<p><code>$$ \sum_{x=1}^{n}{\sum_{i=1}^{n}{v_{xi}}} = n $$</code></p>
<p>则对于有效性约束，我们得到该约束对应的能量分量为</p>
<p><code>$$ E_3 = \dfrac{1}{2} C \left(\sum_{x=1}^{n}{\sum_{i=1}^{n}{v_{xi}}} - n\right)^2 $$</code></p>
<ul>
<li>路径长度约束</li>
</ul>
<p>如上三个约束仅能够保证我们的路径是有效的，但并不一定是最优的。根绝 TSP 问题的优化目标，我们需要引入一个反映路径长度的能量分量，并保证该能量分量随着路径长度的减小而减小。访问两个城市 <code>$x, y$</code> 有两种形式，<code>$x \to y$</code> 或 <code>$y \to x$</code>，如果城市 <code>$x$</code> 和城市 <code>$y$</code> 在旅行中顺序相邻，则 <code>$v_{xi}v_{y,i+1} = 1, v_{xi}v_{y,i-1} = 0$</code>，反之亦然。则反映路径长度的能量分量可以定义为</p>
<p><code>$$ E_4 = \dfrac{1}{2} D \sum_{x=1}^{n}{\sum_{y=1}^{n}{\sum_{i=1}^{n}{d_{xy}\left(v_{xi}v_{y,i+1} + v_{xi}v_{y,i-1}\right)}}} $$</code></p>
<p>综上所述，TSP 问题的能量函数定义为</p>
<p><code>$$ E = E_1 + E_2 + E_3 + E_4 $$</code></p>
<p>其中，<code>$A, B, C, D &gt; 0$</code> 分别为每个能量分量的权重。针对这样的能量函数，我们可得对应神经元 <code>$x_i$</code> 和 <code>$y_i$</code> 之间的权重为</p>
<p><code>$$ \begin{equation} \begin{split} w_{x_i, y_i} = &amp;-2A \delta_{xy} \left(1-\delta_{xy}\right) - 2B \delta_{ij} \left(1-\delta_{xy}\right) \\ &amp;- 2C -2D d_{xy} \left(\delta_{j, i+1} + \delta_{i, j+1}\right) \end{split} \end{equation} $$</code></p>
<p>其中</p>
<p><code>$$ \delta_{xy} = \begin{cases} 1, x = y \\ 0, x \neq y \end{cases} ,  \delta_{ij} = \begin{cases} 1, i = j \\ 0, i \neq j \end{cases} $$</code></p>
<p>因此可以得到网络关于时间的导数</p>
<p><code>$$ \begin{equation} \begin{split} \dfrac{d u_{xi}}{d t} = &amp;-2A \sum_{j \neq i}^{n}{v_{xj}} - 2B \sum_{y \neq x}^{n}{v_{yi}} - 2C \left(\sum_{x=1}^{n}{\sum_{j=1}^{n}{v_{xj}}} - n\right) \\ &amp;- 2D \sum_{y \neq x}^{n}{d_{xy}\left(v_{y, i+1} + v_{y, i-1}\right)} - \dfrac{u_{xi}}{\tau} \end{split} \end{equation} $$</code></p>
<p>据此，我们以一个 10 个城市的数据为例，利用 CHNN 求解 TSP 问题，其中 10 个城市的座标为</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">城市</th>
          <th style="text-align: center">横座标</th>
          <th style="text-align: center">纵座标</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">A</td>
          <td style="text-align: center">0.4000</td>
          <td style="text-align: center">0.4439</td>
      </tr>
      <tr>
          <td style="text-align: center">B</td>
          <td style="text-align: center">0.2439</td>
          <td style="text-align: center">0.1463</td>
      </tr>
      <tr>
          <td style="text-align: center">C</td>
          <td style="text-align: center">0.1707</td>
          <td style="text-align: center">0.2293</td>
      </tr>
      <tr>
          <td style="text-align: center">D</td>
          <td style="text-align: center">0.2293</td>
          <td style="text-align: center">0.7610</td>
      </tr>
      <tr>
          <td style="text-align: center">E</td>
          <td style="text-align: center">0.5171</td>
          <td style="text-align: center">0.9414</td>
      </tr>
      <tr>
          <td style="text-align: center">F</td>
          <td style="text-align: center">0.8732</td>
          <td style="text-align: center">0.6536</td>
      </tr>
      <tr>
          <td style="text-align: center">G</td>
          <td style="text-align: center">0.6878</td>
          <td style="text-align: center">0.5219</td>
      </tr>
      <tr>
          <td style="text-align: center">H</td>
          <td style="text-align: center">0.8488</td>
          <td style="text-align: center">0.3609</td>
      </tr>
      <tr>
          <td style="text-align: center">I</td>
          <td style="text-align: center">0.6683</td>
          <td style="text-align: center">0.2536</td>
      </tr>
      <tr>
          <td style="text-align: center">J</td>
          <td style="text-align: center">0.6195</td>
          <td style="text-align: center">0.2634</td>
      </tr>
  </tbody>
</table>
<p>已知的最优路线为 <code>$A \to D \to E \to F \to G \to H \to I \to J \to B \to C \to A$</code>，最优路线的路径长度为 <code>$2.6907$</code>。我们使用如下参数求解 TSP 问题，初始化 <code>$u_{init} = -\dfrac{\gamma}{2} \ln\left(n - 1\right)$</code>，<code>$\gamma = 0.02$</code>，学习率 <code>$\alpha = 0.0001$</code>，神经元激活阈值 <code>$\theta = 0.7$</code>，<code>$\tau = 1$</code>，能量分量权重参数 <code>$A = 500, B = 500, C = 1000, D = 500$</code>，单次迭代最大次数为 1000，共模拟 100 次。</p>
<pre><code class="language-r"># 城市座标
cities &lt;- data.frame(
    l = LETTERS[1:10],
    x = c(0.4000, 0.2439, 0.1707, 0.2293, 0.5171,
          0.8732, 0.6878, 0.8488, 0.6683, 0.6195),
    y = c(0.4439, 0.1463, 0.2293, 0.7610, 0.9414,
          0.6536, 0.5219, 0.3609, 0.2536, 0.2634)
)

# 通过城市座标构建距离矩阵
distance_matrix &lt;- function(points) {
    n &lt;- nrow(points)
    d &lt;- matrix(rep(0, n^2), n, n)
    
    for (i in 1:n) {
        for (j in i:n) {
            distance &lt;- sqrt((points[i, ]$x - points[j, ]$x)^2 +
                                 (points[i, ]$y - points[j, ]$y)^2)
            d[i, j] &lt;- distance
            d[j, i] &lt;- distance
        }
    }
    
    d
}

# 结果约束校验
check_path_valid &lt;- function(v, n) {
    # 城市约束
    c1 &lt;- 0
    for (x in 1:n) {
        for (i in 1:(n-1)) {
            for (j in (i+1):n) {
                c1 &lt;- c1 + v[x, i] * v[x, j]
            }
        }
    }
    
    # 时间约束
    c2 &lt;- 0
    for (i in 1:n) {
        for (x in 1:(n-1)) {
            for (y in (x+1):n) {
                c2 &lt;- c2 + v[x, i] * v[y, i]
            }
        }
    }
    
    # 有效性约束
    c3 &lt;- sum(v)
    
    ifelse(c1 == 0 &amp; c2 == 0 &amp; c3 == n, T, F)
}

# 根据结果矩阵获取路径
v_to_path &lt;- function(v, n) {
    p &lt;- c()
    
    for (i in 1:n) {
        for (x in 1:n) {
            if (v[x, i] == 1) {
                p &lt;- c(p, x)
                break
            }
        }
    }
    
    p
}

# 计算结果矩阵的路径长度
path_distance &lt;- function(v, n, d) {
    p &lt;- v_to_path(v, n)
    p &lt;- c(p, p[1])
    distance &lt;- 0 
    for (i in 1:(length(p)-1)) {
        distance &lt;- distance + d[p[i], p[i+1]]
    }
    
    distance
}

# 构建 Hopfield 网络
tsp_chnn &lt;- function(d, n, gamma = 0.02, alpha = 0.0001,
                     theta = 0.7, tau = 1,
                     A = 500, B = 500, C = 1000, D = 500,
                     max_iter = 1000) {
    v &lt;- matrix(runif(n^2), n, n)
    u &lt;- matrix(rep(1, n^2), n, n) * (-gamma * log(n-1) / 2)
    du &lt;- matrix(rep(0, n^2), n, n)
    
    for (iter in 1:max_iter) {
        for (x in 1:n) {
            for (i in 1:n) {
                # E1
                e1 &lt;- 0
                for (j in 1:n) {
                    if (j != i) {
                        e1 &lt;- e1 + v[x, j]
                    }
                }
                e1 &lt;- -A * e1
                
                # E2
                e2 &lt;- 0
                for (y in 1:n) {
                    if (y != x) {
                        e2 &lt;- e2 + v[y, i]
                    }
                }
                e2 &lt;- -B * e2
                
                # E3
                e3 &lt;- -C * (sum(v) - n)
                
                # E4
                e4 &lt;- 0
                for (y in 1:n) {
                    if (y != x) {
                        e4 &lt;- e4 + d[x, y] *
                            (v[y, (i+1-1)%%n+1] + v[y, (i-1-1)%%n+1])
                    }
                }
                e4 &lt;- -D * e4
                
                du[x, i] &lt;- e1 + e2 + e3 + e4 - u[x, i] / tau
            }
        }
        
        u &lt;- u + alpha * du
        v &lt;- (1 + tanh(u / gamma)) / 2
        v &lt;- ifelse(v &gt;= theta, 1, 0)
    }
    
    v
}

# 利用 Hopfiled 网络求解 TSP 问题
set.seed(112358)

n &lt;- 10
d &lt;- distance_matrix(cities)

# 模拟 100 次并获取最终结果
tsp_solutions &lt;- lapply(1:100, function(round) {
    v &lt;- tsp_chnn(d, n)
    valid &lt;- check_path_valid(v, n)
    distance &lt;- ifelse(valid, path_distance(v, n, d), NA)
    
    list(round = round, valid = valid,
         distance = distance, v = v)
})

# 获取最优结果
best_tsp_solution &lt;- NA
for (tsp_solution in tsp_solutions) {
    if (tsp_solution$valid) {
        if (!is.na(best_tsp_solution)) {
            if (tsp_solution$distance &lt; best_tsp_solution$distance) {
                best_tsp_solution &lt;- tsp_solution
            }
        } else {
            best_tsp_solution &lt;- tsp_solution
        }
    }
}

# 可视化最优结果
best_tsp_solution_path &lt;- v_to_path(best_tsp_solution$v, n)
ordered_cities &lt;- cities[best_tsp_solution_path, ] %&gt;%
    mutate(ord = seq(1:10))

best_tsp_solution_path_p &lt;- ggplot(ordered_cities) +
    geom_polygon(aes(x, y), color = 'black', fill = NA) +
    geom_point(aes(x, y)) +
    geom_text(aes(x, y, label = l), vjust = -1) +
    geom_text(aes(x, y, label = ord), vjust = 2) +
    coord_fixed() + ylim(c(0, 1)) + xlim(c(0, 1)) +
    theme(axis.title = element_blank())
print(best_tsp_solution_path_p)
</code></pre>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/tsp-best-solution-path.png" alt=""></p>
<h2 id="受限的玻尔兹曼机-rbm">受限的玻尔兹曼机 (RBM)</h2>
<h3 id="网络结构及其概率表示">网络结构及其概率表示</h3>
<p><strong>受限的玻尔兹曼机</strong> (Restricted Boltzmann Machine, RBM) 或<strong>簧风琴</strong> (harmonium) 是由 Smolensky 与 1986年在<strong>玻尔兹曼机</strong> (Boltzmann Machine, BM) 基础上提出的一种随机神经网络 (Stochastic Neural Networks) <sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup>。受限的玻尔兹曼机对于原始的玻尔兹曼机做了相应的限制，在其网络结构中包含<strong>可见节点</strong>和<strong>隐藏节点</strong>，并且<strong>可见节点</strong>和<strong>隐藏节点</strong>内部不允许存在连接，更加形象的可以将其理解为一个二分图。</p>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/rbm-network.svg" alt=""></p>
<p>对于二值版本的 RBM 而言，其中可见层 <code>$\mathbf{v} = \left(v_1, v_2, ..., v_{n_v}\right)^T$</code> 由 <code>$n_v$</code> 个二值随机变量构成；隐藏层 <code>$\mathbf{h} = \left(h_1, h_2, ..., h_{n_h}\right)^T$</code> 由 <code>$n_h$</code> 个二值随机变量构成。</p>
<p>RBM 同样作为一个基于能量的模型，其能量函数定义为：</p>
<p><code>$$ E \left(\boldsymbol{v}, \boldsymbol{h}\right) = -\sum_{i=1}^{n_v}{b_i v_i} -\sum_{j=1}^{n_h}{c_j h_j} - \sum_{i=1}^{n_v}{\sum_{j=1}^{n_h}{v_i w_{i,j} h_i}} $$</code></p>
<p>将其表示成矩阵向量的形式，可记为：</p>
<p><code>$$ E \left(\boldsymbol{v}, \boldsymbol{h}\right) = -\boldsymbol{b}^T \boldsymbol{v} - \boldsymbol{c}^T \boldsymbol{h} - \boldsymbol{v}^T \boldsymbol{W} \boldsymbol{h} $$</code></p>
<p>其中 <code>$\boldsymbol{b} \in \mathbb{R}^{n_v}$</code> 为可见层的偏置向量；<code>$\boldsymbol{c} \in \mathbb{R}^{n_h}$</code> 为隐含层的偏置向量；<code>$\boldsymbol{W} \in \mathbb{R}^{n_v \times n_h}$</code> 为可见层和隐含层之间的权重矩阵。根据能量函数，可得其联合概率分布为：</p>
<p><code>$$ P \left(\mathbf{v} = \boldsymbol{v}, \mathbf{h} = \boldsymbol{h}\right) = \dfrac{1}{Z} e^{-E \left(\boldsymbol{v}, \boldsymbol{h}\right)} $$</code></p>
<p>其中 <code>$Z$</code> 为归一化常数，成为配分函数：</p>
<p><code>$$ Z = \sum_{\boldsymbol{v}}{\sum_{\boldsymbol{h}}{e^{-E \left(\boldsymbol{v}, \boldsymbol{h}\right)}}} $$</code></p>
<p>对于 RBM 我们更加关注的的为边缘分布，即：</p>
<p><code>$$ P \left(\boldsymbol{v}\right) = \sum_{h}{P\left(\boldsymbol{v}, \boldsymbol{h}\right)} = \dfrac{1}{Z} \sum_{h}{e^{-E\left(\boldsymbol{v}, \boldsymbol{h}\right)}} $$</code></p>
<p>因为概率中包含归一化常数，我们需要计算 <code>$Z$</code>，从其定义可得，当穷举左右可能性的化，我们需要计算 <code>$2^{n_v + n_h}$</code> 个项，其计算复杂度很大。尽管 <code>$P\left(\boldsymbol{v}\right)$</code> 计算比较困难，但是其条件概率 <code>$P\left(\mathbf{h} | \mathbf{v}\right)$</code> 和 <code>$P\left(\mathbf{v} | \mathbf{h}\right)$</code> 计算和采样相对容易。为了便于推导，我们定义如下记号：</p>
<p><code>$$ \boldsymbol{h}_{-k} = \left(h_1, h_2, ..., h_{k-1}, h_{k+1}, ..., h_{n_h}\right)^T $$</code></p>
<p>则 <code>$P\left(h_k = 1 | \boldsymbol{v}\right)$</code> 定义如下：</p>
<p><code>$$ \begin{equation} \begin{split} &amp;P\left(h_k = 1 | \boldsymbol{v}\right) \\ = &amp;P\left(h_k = 1 | h_{-k}, \boldsymbol{v}\right) \\ = &amp;\dfrac{P\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}{P\left(h_{-k}, \boldsymbol{v}\right)} \\ = &amp;\dfrac{P\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}{P\left(h_k = 1 | h_{-k}, \boldsymbol{v}\right) + P\left(h_k = 0 | h_{-k}, \boldsymbol{v}\right)} \\ = &amp;\dfrac{\dfrac{1}{Z} e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}}{\dfrac{1}{Z} e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)} + \dfrac{1}{Z} e^{-E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right)}} \\ = &amp;\dfrac{e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}}{e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)} + e^{-E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right)}} \\ = &amp;\dfrac{1}{1 + e^{E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right) - E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right)}} \\ \end{split} \end{equation} $$</code></p>
<p>其中：</p>
<p><code>$$ \begin{equation} \begin{split} &amp;E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right) \\ = &amp;E\left(h_k = 1, \boldsymbol{v}\right) \\ = &amp;-\sum_{i=1}^{n_v}{b_i v_i} - \sum_{j=1, j \neq k}^{n_h}{c_j h_j} - \sum_{i=1}^{n_v}{\sum_{j=1, j \neq k}^{n_h}{v_i W_{i, j} h_i}} - c_k - \sum_{i=1}^{n_v}{v_i W_{i, k}} \\ &amp;E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right) \\ = &amp;E\left(h_k = 0, \boldsymbol{v}\right) \\ = &amp;-\sum_{i=1}^{n_v}{b_i v_i} - \sum_{j=1, j \neq k}^{n_h}{c_j h_j} - \sum_{i=1}^{n_v}{\sum_{j=1, j \neq k}^{n_h}{v_i W_{i, j} h_i}} \end{split} \end{equation} $$</code></p>
<p>因此，<code>$P\left(h_k = 1 | \boldsymbol{v}\right)$</code> 可以化简为：</p>
<p><code>$$ \begin{equation} \begin{split} &amp;P\left(h_k = 1 | \boldsymbol{v}\right) \\ = &amp;\dfrac{1}{1 + e^{-\left(c_k + \sum_{i=1}^{n_v}{v_i W_{i, k}}\right)}} \\ = &amp;\sigma\left(c_k + \sum_{i=1}^{n_v}{v_i W_{i, k}}\right) \\ = &amp;\sigma\left(c_k + \boldsymbol{v}^T \boldsymbol{W}_{:, k}\right) \end{split} \end{equation} $$</code></p>
<p>其中，<code>$\sigma$</code> 为 sigmoid 函数。因此，我们可以将条件分布表示为连乘的形式：</p>
<p><code>$$ \begin{equation} \begin{split} P\left(\boldsymbol{h} | \boldsymbol{v}\right) &amp;= \prod_{j=1}^{n_h}{P\left(h_j | \boldsymbol{v}\right)} \\ &amp;= \prod_{j=1}^{n_h}{\sigma\left(\left(2h - 1\right) \odot \left(\boldsymbol{c} + \boldsymbol{W}^T \boldsymbol{v}\right)\right)_j} \end{split} \end{equation} $$</code></p>
<p>同理可得：</p>
<p><code>$$ \begin{equation} \begin{split} P\left(\boldsymbol{v} | \boldsymbol{h}\right) &amp;= \prod_{i=1}^{n_v}{P\left(v_i | \boldsymbol{h}\right)} \\ &amp;= \prod_{i=1}^{n_v}{\sigma\left(\left(2v - 1\right) \odot \left(\boldsymbol{b} + \boldsymbol{W} \boldsymbol{h}\right)\right)_i} \end{split} \end{equation} $$</code></p>
<h3 id="模型训练">模型训练 <sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup></h3>
<p>对于 RBM 模型的训练，假设训练样本集合为 <code>$S = \left\lbrace{\boldsymbol{v^1}, \boldsymbol{v^2}, ..., \boldsymbol{v^{n_s}}}\right\rbrace$</code>，其中 <code>$\boldsymbol{v^i} = \left(v_{1}^{i}, v_{2}^{i}, ..., v_{n_v}^{i}\right), i = 1, 2, ..., n_s$</code>。则训练 RBM 的目标可以定义为最大化如下似然：</p>
<p><code>$$ \mathcal{L}_{\theta, S} = \prod_{i=1}^{n_s}{P\left(\boldsymbol{v}^i\right)} $$</code></p>
<p>其中 <code>$\theta$</code> 为待优化的参数，为了方便计算，等价目标为最大化其对数似然：</p>
<p><code>$$ \ln\mathcal{L}_{\theta, S} = \ln\prod_{i=1}^{n_s}{P\left(\boldsymbol{v}^i\right)} = \sum_{i=1}^{n_s}{\ln P\left(\boldsymbol{v}^i\right)} $$</code></p>
<p>我们将其对数似然简写为 <code>$\ln\mathcal{L}_S$</code> ，通过梯度上升方法，我们可以得到参数的更新公式：</p>
<p><code>$$ \theta = \theta + \eta \dfrac{\partial \ln\mathcal{L}_S}{\partial \theta} $$</code></p>
<p>对于单个样本 <code>$\boldsymbol{\color{red}{v'}}$</code> ，有：</p>
<p><code>$$ \begin{equation} \begin{split} \dfrac{\partial \ln\mathcal{L}_S}{\partial \theta} &amp;= \dfrac{\partial \ln P\left(\boldsymbol{\color{red}{v'}}\right)}{\partial \theta} = \dfrac{\partial \ln \left(\dfrac{1}{Z} \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}\right)}{\partial \theta} \\ &amp;= \dfrac{\partial \left(\ln \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}} - \ln Z\right)}{\partial \theta} = \dfrac{\partial \left(\ln \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}} - \ln \sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}\right)}{\partial \theta} \\ &amp;= \dfrac{\partial}{\partial \theta} \left(\ln \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}\right) - \dfrac{\partial}{\partial \theta} \left(\ln \sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}\right) \\ &amp;= -\dfrac{1}{\sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}} \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \dfrac{1}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &amp;= -\sum_{\boldsymbol{h}}{\dfrac{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}{\sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{\dfrac{e^{-E\left(\boldsymbol{v, h}\right)}}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &amp;= -\sum_{\boldsymbol{h}}{\dfrac{\dfrac{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}{Z}}{\dfrac{\sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}}{Z}} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{\dfrac{e^{-E\left(\boldsymbol{v, h}\right)}}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &amp;= -\sum_{\boldsymbol{h}}{\dfrac{P\left(\boldsymbol{\color{red}{v'}, h}\right)}{P\left(\boldsymbol{\color{red}{v'}}\right)} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{\dfrac{e^{-E\left(\boldsymbol{v, h}\right)}}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &amp;= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \end{split} \end{equation} $$</code></p>
<p>其中：</p>
<p><code>$$ \begin{equation} \begin{split} \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} &amp;= \sum_{\boldsymbol{v}}{\sum_{\boldsymbol{h}}{P\left(\boldsymbol{v}\right) P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}}} \\ &amp;= \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) \sum_{\boldsymbol{h}}{P \left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}}} \end{split} \end{equation} $$</code></p>
<p>则对于参数 <code>$w_{i, j}$</code> 可得：</p>
<p><code>$$ \begin{equation} \begin{split} &amp;\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial w_{i, j}}} \\ = &amp;-\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) h_i v_j} \\ = &amp;-\sum_{\boldsymbol{h}}{\prod_{k=1}^{n_h}{P\left(h_k | \boldsymbol{v}\right) h_i v_j}} \\ = &amp;-\sum_{\boldsymbol{h}}{P\left(h_i | \boldsymbol{v}\right) P\left(h_{-i} | \boldsymbol{v}\right) h_i v_j} \\ = &amp;-\sum_{\boldsymbol{h_i}}{\sum_{h_{-i}}{P\left(h_i | \boldsymbol{v}\right) P\left(\boldsymbol{h_{-i}} | \boldsymbol{v}\right) h_i v_j}} \\ = &amp;-\sum_{\boldsymbol{h_i}}{P\left(h_i | \boldsymbol{v}\right) h_i v_j} \sum_{\boldsymbol{h_{-i}}}{P\left(h_{-i} | \boldsymbol{v}\right)} \\ = &amp;-\sum_{\boldsymbol{h_i}}{P\left(h_i | \boldsymbol{v}\right) h_i v_j} \\ = &amp;-\left(P\left(h_i = 0 | \boldsymbol{v}\right) \cdot 0 \cdot v_j + P\left(h_i = 1 | \boldsymbol{v}\right) \cdot 1 \cdot v_j\right) \\ = &amp;-P\left(h_i = 1 | \boldsymbol{v}\right) v_j \end{split} \end{equation} $$</code></p>
<p>则对于参数 <code>$b_i$</code> 可得：</p>
<p><code>$$ \begin{equation} \begin{split} &amp;\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial b_i}} \\ = &amp;-\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) v_i} \\ = &amp;-v_i \sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right)} \\ = &amp;-v_i \end{split} \end{equation} $$</code></p>
<p>则对于参数 <code>$c_j$</code> 可得：</p>
<p><code>$$ \begin{equation} \begin{split} &amp;\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial c_j}} \\ = &amp;-\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) h_j} \\ = &amp;-\sum_{\boldsymbol{h}}{\prod_{k=1}^{n_h}{P\left(h_k | \boldsymbol{v}\right) h_j}} \\ = &amp;-\sum_{\boldsymbol{h}}{P\left(h_j | \boldsymbol{v}\right) P\left(h_{-j} | \boldsymbol{v}\right) h_j} \\ = &amp;-\sum_{h_j}{\sum_{h_{-j}}{P\left(h_i | \boldsymbol{v}\right) P\left(h_{-j} | \boldsymbol{v}\right) h_j}} \\ = &amp;-\sum_{h_j}{P\left(h_i | \boldsymbol{v}\right) h_j} \sum_{h_{-j}}{P\left(h_{-j} | \boldsymbol{v}\right)} \\ = &amp;-\sum_{h_j}{P\left(h_i | \boldsymbol{v}\right) h_j} \\ = &amp;-\left(P\left(h_j = 0 | \boldsymbol{v}\right) \cdot 0 + P\left(h_j = 1 | \boldsymbol{v}\right) \cdot 1\right) \\ = &amp;-P\left(h_j = 1 | \boldsymbol{v}\right) \end{split} \end{equation} $$</code></p>
<p>综上所述，可得：</p>
<p><code>$$ \begin{equation} \begin{split} \dfrac{\partial \ln P\left(\color{red}{\boldsymbol{v'}}\right)}{\partial w_{i, j}} &amp;= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial w_{i, j}}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial w_{i, j}}} \\ &amp;= P\left(h_i = 1 | \boldsymbol{\color{red}{v'}}\right) \color{red}{v'_j} - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_i = 1 | \boldsymbol{v}\right) v_j}\\ \dfrac{\partial \ln P\left(\color{red}{\boldsymbol{v'}}\right)}{\partial b_i} &amp;= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial b_i}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial b_i}} \\ &amp;= \color{red}{v'_i} - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) v_i} \\ \dfrac{\partial \ln P\left(\color{red}{\boldsymbol{v'}}\right)}{\partial c_j} &amp;= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial c_j}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial c_j}} \\ &amp;= P\left(h_j = 1 | \boldsymbol{\color{red}{v'}}\right) - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_j = 1 | \boldsymbol{v}\right)} \\ \end{split} \end{equation} $$</code></p>
<p>对于多个样本 <code>$S = \left\lbrace{\boldsymbol{v^1}, \boldsymbol{v^2}, ..., \boldsymbol{v^{n_s}}}\right\rbrace$</code>，有：</p>
<p><code>$$ \begin{equation} \begin{split} \dfrac{\partial \ln \mathcal{L}_S}{\partial w_{i, j}} &amp;= \sum_{m=1}^{n_S}{\left[P\left(h_i = 1 | \boldsymbol{v^m}\right) v_j^m - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_i = 1 | \boldsymbol{v} v_j\right)}\right]} \\ \dfrac{\partial \ln \mathcal{L}_S}{\partial b_i} &amp;= \sum_{m=1}^{n_S}{\left[v_i^m - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) v_i}\right]} \\ \dfrac{\partial \ln \mathcal{L}_S}{\partial c_j} &amp;= \sum_{m=1}^{n_S}{\left[P\left(h_j = 1 | \boldsymbol{v^m}\right) - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_j = 1 | \boldsymbol{v}\right)}\right]} \end{split} \end{equation} $$</code></p>
<p>针对如上方法，我们需要计算 <code>$\sum_{\boldsymbol{v}}$</code> 相关项，如上文所述，其计算复杂度为 <code>$O\left(2^{n_v + n_h}\right)$</code>，因为其条件概率计算比较容易，因此我们可以用 Gibbs 采样的方法进行估计，但由于 Gibbs 采样方法存在 burn-in period，因此需要足够次数的状态转移后才能够收敛到目标分布，因此这就增大了利用这种方法训练 RBM 模型的时间。</p>
<p>针对这个问题，Hinton 于 2002 年提出了对比散度 (Contrastive Divergence, CD) 算法 <sup id="fnref:11"><a href="#fn:11" class="footnote-ref" role="doc-noteref">11</a></sup>，基本思想为将训练样本作为采样的初始值，因为目标就是让 RBM 去拟合这些样本的分布，因此这样则可以通过更少的状态转移就收敛到平稳分布。<code>$k$</code> 步 CD 算法大致步骤为：</p>
<ol>
<li>对 <code>$\forall \boldsymbol{v} \in \boldsymbol{S}$</code>，初始化 <code>$\boldsymbol{v}^{\left(0\right)} = \boldsymbol{v}$</code>。</li>
<li>执行 <code>$k$</code> 步 Gibbs 采样，对于第 <code>$t$</code> 步，分别利用 <code>$P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(t-1\right)}\right)$</code> 和 <code>$P\left(\boldsymbol{v} | \boldsymbol{h}^{\left(t-1\right)}\right)$</code> 采样出 <code>$\boldsymbol{h}^{\left(t-1\right)}$</code> 和 <code>$\boldsymbol{v}^{\left(t\right)}$</code>。</li>
<li>利用采样得到的 <code>$\boldsymbol{v}^{\left(k\right)}$</code> <strong>近似估计</strong> <code>$\sum_{\boldsymbol{v}}$</code> 相关项：
<code>$$ \begin{equation} \begin{split} \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial w_{i, j}} &amp;\approx P\left(h_i=1|\boldsymbol{v}^{\left(0\right)}\right) v_j^{\left(0\right)} - P\left(h_i=1|\boldsymbol{v}^{\left(k\right)}\right) v_j^{\left(k\right)} \\ \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial b_i} &amp;\approx v_i^{\left(0\right)} - v_i^{\left(k\right)} \\ \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial c_j} &amp;\approx P\left(h_j=1|\boldsymbol{v}^{\left(0\right)}\right) - P\left(h_j=1|\boldsymbol{v}^{\left(k\right)}\right) \end{split} \end{equation} $$</code></li>
</ol>
<p><strong>近似估计</strong>可以看做是利用</p>
<p><code>$$ CDK\left(\theta, \boldsymbol{v}\right) = -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(0\right)}\right) \dfrac{\partial E\left(\boldsymbol{v}^{\left(0\right)}, h\right)}{\partial \theta}} + \sum_{\boldsymbol{h}}{P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(k\right)}\right) \dfrac{\partial E\left(\boldsymbol{v}^{\left(k\right)}, \boldsymbol{h}\right)}{\partial \theta}} $$</code></p>
<p>近似</p>
<p><code>$$ \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial \theta} = -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(0\right)}\right) \dfrac{\partial E\left(\boldsymbol{v}^{\left(0\right)}, h\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{v, h}\right) \dfrac{\partial E\left(\boldsymbol{v}, \boldsymbol{h}\right)}{\partial \theta}} $$</code></p>
<p>的过程。</p>
<p>基于对比散度的 RBM 训练算法可以描述为：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{CDK 算法}
\begin{algorithmic}
\REQUIRE $k, \boldsymbol{S}, \text{RBM}\left(\boldsymbol{W, b, c}\right)$
\ENSURE $\Delta \boldsymbol{W}, \Delta \boldsymbol{b}, \Delta \boldsymbol{c}$
\PROCEDURE{CDK}{$k, \boldsymbol{S}, \text{RBM}\left(\boldsymbol{W, b, c}\right)$}
    \STATE $\Delta \boldsymbol{W} \gets 0, \Delta \boldsymbol{b} \gets 0, \Delta \boldsymbol{c} \gets 0$
    \FORALL{$\boldsymbol{v \in S}$}
        \STATE $\boldsymbol{v}^{\left(0\right)} \gets \boldsymbol{v}$
        \FOR{$t = 0, 1, ..., k-1$}
            \STATE $\boldsymbol{h}^{\left(t\right)} \gets \text{sample_h_given_v} \left(\boldsymbol{v}^{\left(t\right)}, \text{RBM}\left(W, b, c\right)\right)$
            \STATE $\boldsymbol{v}^{\left(t+1\right)} \gets \text{sample_v_given_h} \left(\boldsymbol{h}^{\left(t\right)}, \text{RBM}\left(W, b, c\right)\right)$
        \ENDFOR
        \FOR{$i = 1, 2, ..., n_h; j = 1, 2, ..., n_v$}
            \STATE $\Delta w_{i, j} \gets \Delta w_{i, j} + \left[P\left(h_i=1|\boldsymbol{v}^{\left(0\right)}\right) v_j^{\left(0\right)} - P\left(h_i=1|\boldsymbol{v}^{\left(k\right)}\right) v_j^{\left(k\right)}\right]$
            \STATE $\Delta b_i \gets \Delta b_i = \left[v_i^{\left(0\right)} - v_i^{\left(k\right)}\right]$
            \STATE $\Delta c_j \gets \Delta c_j = \left[P\left(h_j=1|\boldsymbol{v}^{\left(0\right)}\right) - P\left(h_j=1|\boldsymbol{v}^{\left(k\right)}\right)\right]$
        \ENDFOR
    \ENDFOR
\ENDPROCEDURE
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>其中，<code>sample_h_given_v</code> 和 <code>sample_v_given_h</code> 分别表示在已知可见层时采样隐含层和在已知隐含层时采样可见层。对于 <code>sample_h_given_v</code> 其算法流程如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{sample\_h\_given\_v 算法}
\begin{algorithmic}
\FOR{$j = 1, 2, ..., n_h$}
    \STATE sample $r_j \sim Uniform[0, 1]$
    \IF{$r_j < P\left(h_j = 1 | \boldsymbol{v}\right)$}
        \STATE $h_j \gets 1$
    \ELSE
        \STATE $h_j \gets 0$
    \ENDIF
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>类似的，对于 <code>sample_v_given_h</code> 其算法流程如下：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{sample\_v\_given\_h 算法}
\begin{algorithmic}
\FOR{$j = 1, 2, ..., n_h$}
    \STATE sample $r_j \sim Uniform[0, 1]$
    \IF{$r_i < P\left(v_i = 1 | \boldsymbol{h}\right)$}
        \STATE $v_i \gets 1$
    \ELSE
        \STATE $v_i \gets 0$
    \ENDIF
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>至此，我们可以得到 RBM 模型训练的整个流程：</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{RBM 训练算法}
\begin{algorithmic}
\FOR{$iter = 1, 2, ..., \text{max\_iter}$}
    \STATE $\Delta \boldsymbol{W}, \Delta \boldsymbol{b}, \Delta \boldsymbol{c} \gets \text{CDK} \left(k, \boldsymbol{S}, \text{RBM}\left(\boldsymbol{W, b, c}\right)\right)$
    \STATE $\boldsymbol{W} \gets \boldsymbol{W} + \eta \left(\dfrac{1}{n_s} \Delta \boldsymbol{W}\right)$
    \STATE $\boldsymbol{b} \gets \boldsymbol{b} + \eta \left(\dfrac{1}{n_s} \Delta \boldsymbol{b}\right)$
    \STATE $\boldsymbol{c} \gets \boldsymbol{c} + \eta \left(\dfrac{1}{n_s} \Delta \boldsymbol{c}\right)$
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>其中，<code>$k$</code> 为 CDK 算法参数，<code>$\text{max_iter}$</code> 为最大迭代次数，<code>$\boldsymbol{S}$</code> 为训练样本，<code>$n_s = |\boldsymbol{S}|$</code>，<code>$\eta$</code> 为学习率。</p>
<p>对于模型的评估，最简单的是利用 RBM 模型的似然或对数似然，但由于涉及到归一化因子 <code>$Z$</code> 的计算，其复杂度太高。更常用的方式是利用<strong>重构误差</strong> (reconstruction error)，即输入数据和利用 RBM 模型计算得到隐含节点再重构回可见节点之间的误差。</p>
<h3 id="mnist-示例">MNIST 示例</h3>
<p>我们利用经典的 MNIST 数据作为示例，我们利用基于 tensorflow 的扩展包 tfrbm <sup id="fnref:12"><a href="#fn:12" class="footnote-ref" role="doc-noteref">12</a></sup>。tfrbm 实现了 Bernoulli-Bernoulli RBM 和 Gaussian-Bernoulli RBM 两种不同的 RBM，两者的比较详见 <sup id="fnref:13"><a href="#fn:13" class="footnote-ref" role="doc-noteref">13</a></sup> <sup id="fnref:14"><a href="#fn:14" class="footnote-ref" role="doc-noteref">14</a></sup>。</p>
<pre><code class="language-python">import numpy as np
from matplotlib import pyplot as plt, gridspec
from tfrbm import BBRBM, GBRBM
from tensorflow.examples.tutorials.mnist import input_data

# 读入训练数据和测试数据
mnist = input_data.read_data_sets('MNIST', one_hot=True)
mnist_train_images = mnist.train.images
mnist_test_images = mnist.test.images
mnist_test_labels = mnist.test.labels
</code></pre>
<p>MNIST 数据集中，训练集共包含 55000 个样本，每个样本的维度为 784，我们构建 Bernoulli-Bernoulli RBM，设置隐含节点个数为 64，学习率为 0.01，epoches 为 30，batch size 为 10。</p>
<pre><code class="language-python">bbrbm = BBRBM(n_visible=784,
              n_hidden=64,
              learning_rate=0.01,
              use_tqdm=True)

bbrbm_errs = bbrbm.fit(mnist_train_images, n_epoches=30, batch_size=10)

# Epoch: 0: 100%|##########| 5500/5500 [00:11&lt;00:00, 480.39it/s]
# Train error: 0.1267
# 
# ......
# 
# Epoch: 29: 100%|##########| 5500/5500 [00:11&lt;00:00, 482.15it/s]
# Train error: 0.0347
</code></pre>
<p>训练误差变化如下</p>
<pre><code class="language-python">plt.style.use('ggplot')
plt.plot(bbrbm_errs)
</code></pre>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/bbrbm-mnist-errs.png" alt=""></p>
<p>我们从 MNIST 的测试集中针对每个数字选取 10 个样本，共 100 个样本作为测试数据，利用训练好的 RBM 模型重构这 100 个样本</p>
<pre><code class="language-python">mnist_test_images_samples = np.zeros([10 * 10, 784])
mnist_test_images_samples_rec = np.zeros([10 * 10, 784])
mnist_test_images_samples_plt = np.zeros([10 * 10 * 2, 784])

digits_current_counts = np.zeros(10, dtype=np.int32)
digits_total_counts = np.ones(10, dtype=np.int32) * 10

for idx in range(mnist_test_images.shape[0]):
    image = mnist_test_images[idx, ]
    label = mnist_test_labels[idx, ]

    for digit in range(10):
        digit_label = np.zeros(10)
        digit_label[digit] = 1

        if (label == digit_label).all() and
               digits_current_counts[digit] &lt; 10:
            nrow = digits_current_counts[digit]
            sample_idx = nrow * 10 + digit
            mnist_test_images_samples[sample_idx, ] = image
            mnist_test_images_samples_rec[sample_idx, ] = \
                bbrbm.reconstruct(image.reshape([1, -1]))
            mnist_test_images_samples_plt[sample_idx * 2, ] = \
                mnist_test_images_samples[sample_idx, ]
            mnist_test_images_samples_plt[sample_idx * 2 + 1, ] = \
                mnist_test_images_samples_rec[sample_idx, ]
            digits_current_counts[digit] += 1

    if (digits_current_counts == digits_total_counts).all():
        break
</code></pre>
<p>对比测试输入数据和重构结果，奇数列为输入数据，偶数列为重构数据</p>
<pre><code class="language-python">def plot_mnist(mnist_images, nrows, ncols, cmap='gray'):
    fig = plt.figure(figsize=(ncols, nrows))
    gs = gridspec.GridSpec(nrows, ncols)
    gs.update(wspace=0.025, hspace=0.025)

    for nrow in range(nrows):
        for ncol in range(ncols):
            ax = plt.subplot(gs[nrow, ncol])
            idx = nrow * ncols + ncol
            minist_image = mnist_images[idx, ].reshape([28, 28])
            ax.imshow(minist_image, cmap=cmap)
            ax.axis('off')

    return fig
    
plot_mnist(mnist_test_images_samples_plt, 10, 20)
</code></pre>
<p><img src="/images/cn/2018-01-17-ising-hopfield-and-rbm/bbrbm-mnist.png" alt=""></p>
<p>测试集上的重构误差为</p>
<pre><code class="language-python">gbrbm.get_err(mnist_test_images_samples)

# 0.035245348
</code></pre>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Ernest Ising, Beitrag zur Theorie des Ferround Paramagnetismus (1924) Contribution to the Theory of Ferromagnetism (English translation of &ldquo;Beitrag zur Theorie des Ferromagnetismus&rdquo;, 1925) Goethe as a Physicist (1950)&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Onsager, L. &ldquo;A two-dimensional model with an order–disorder transition (crystal statistics I).&rdquo; <em>Phys. Rev</em> 65 (1944): 117-49.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="http://wiki.swarma.net/index.php?title=ISING%E6%A8%A1%E5%9E%8B">http://wiki.swarma.net/index.php?title=ISING模型</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Hopfield, John J. &ldquo;Neural networks and physical systems with emergent collective computational abilities.&rdquo; <em>Spin Glass Theory and Beyond: An Introduction to the Replica Method and Its Applications.</em> 1987. 411-415.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Abu-Mostafa, Y. A. S. E. R., and J. St Jacques. &ldquo;Information capacity of the Hopfield model.&rdquo; <em>IEEE Transactions on Information Theory</em> 31.4 (1985): 461-464.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>韩力群. 人工神经网络理论、设计及应用&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p><a href="https://zh.wikipedia.org/zh-hans/%E6%97%85%E8%A1%8C%E6%8E%A8%E9%94%80%E5%91%98%E9%97%AE%E9%A2%98">https://zh.wikipedia.org/zh-hans/旅行推销员问题</a>&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p><a href="https://zh.wikipedia.org/zh-hans/NP%E5%9B%B0%E9%9A%BE">https://zh.wikipedia.org/zh-hans/NP困难</a>&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p>Smolensky, Paul. <em>Information processing in dynamical systems: Foundations of harmony theory.</em> No. CU-CS-321-86. COLORADO UNIV AT BOULDER DEPT OF COMPUTER SCIENCE, 1986.&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p><a href="http://blog.csdn.net/itplus/article/details/19168937">http://blog.csdn.net/itplus/article/details/19168937</a>&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:11">
<p>Hinton, Geoffrey E. &ldquo;Training products of experts by minimizing contrastive divergence.&rdquo; <em>Neural computation</em> 14.8 (2002): 1771-1800.&#160;<a href="#fnref:11" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:12">
<p><a href="https://github.com/meownoid/tensorfow-rbm">https://github.com/meownoid/tensorfow-rbm</a>&#160;<a href="#fnref:12" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:13">
<p>Hinton, Geoffrey. &ldquo;A practical guide to training restricted Boltzmann machines.&rdquo; <em>Momentum</em> 9.1 (2010): 926.&#160;<a href="#fnref:13" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:14">
<p>Yamashita, Takayoshi, et al. &ldquo;To be Bernoulli or to be Gaussian, for a Restricted Boltzmann Machine.&rdquo; <em>Pattern Recognition (ICPR), 2014 22nd International Conference on. IEEE</em>, 2014.&#160;<a href="#fnref:14" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>马尔科夫链蒙特卡洛方法和吉布斯采样 (MCMC and Gibbs Sampling)</title><link>https://zeqiang.fun/cn/2017/12/mcmc-and-gibbs-sampling/</link><pubDate>Sun, 17 Dec 2017 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2017/12/mcmc-and-gibbs-sampling/</guid><description><![CDATA[
        <h2 id="蒙特卡罗方法-monte-carlo-mc">蒙特卡罗方法 (Monte Carlo, MC)</h2>
<p>蒙特卡罗方法 (Monte Carlo) 也称为统计模拟方法，是于 20 世纪 40 年代由冯·诺伊曼，斯塔尼斯拉夫·乌拉姆和尼古拉斯·梅特罗波利斯在洛斯阿拉莫斯国家实验室为核武器计划工作时 (曼哈顿计划) 发明。因为乌拉姆的叔叔经常在摩纳哥的蒙特卡罗赌场输钱，该方法被定名为蒙特卡罗方法。蒙特卡罗方法是以概率为基础的方法，与之对应的是确定性算法。</p>
<p>蒙特卡罗方法最早可以追述到 18 世纪的<a href="https://zh.wikipedia.org/zh-hans/%E5%B8%83%E4%B8%B0%E6%8A%95%E9%92%88%E9%97%AE%E9%A2%98">布丰投针问题</a>，该方法通过一个平行且等距木纹铺成的地板，随意抛一支长度比木纹之间距离小的针，求针和其中一条木纹相交的概率的方法得出了一个求 <code>$\pi$</code> 的蒙特卡罗方法。我们通过另一种方式使用蒙特卡罗方法计算圆周率 <code>$\pi$</code>，对于一个边长为 <code>$2r$</code> 的正方形，其内切圆的半径即为 <code>$r$</code>，因此圆形的面积 <code>$A_c$</code> 与正方形的面积 <code>$A_s$</code> 的比值为</p>
<p><code>$$ \dfrac{A_c}{A_s} = \dfrac{\pi r^2}{\left(2r\right)^2} = \dfrac{\pi}{4} $$</code></p>
<p><img src="/images/cn/2017-12-17-mcmc-and-gibbs-sampling/mc-pi.png" alt=""></p>
<p>如果我们在矩形内随机的生成均匀分布的点，则在圆内的点的个数的占比即为 <code>$\dfrac{\pi}{4}$</code>，因此通过模拟即可求出 <code>$\pi$</code> 的近似值</p>
<pre><code class="language-r">library(tidyverse)

# 圆的中心点和半径
r &lt;- 2
center_x &lt;- r
center_y &lt;- r

# 距离公式
distance &lt;- function(point_x, point_y, center_x, center_y) {
    sqrt((point_x - center_x)^2 + (point_y - center_y)^2)
}

# 点生成器
points_generator &lt;- function(size) {
    set.seed(112358)
    points_x &lt;- runif(size, min = 0, max = 2*r)
    points_y &lt;- runif(size, min = 0, max = 2*r)
    
    tibble(
        x = points_x,
        y = points_y,
        in_cycle = ifelse(
            distance(points_x, points_y, center_x, center_y) &gt; r, 0, 1)
    )
}

# 点的个数
sizes &lt;- c(1000, 10000, 100000, 1000000, 10000000)

# 估计的 PI 值
estimated_pi &lt;- sapply(sizes, function(size) {
    points &lt;- points_generator(size)
    sum(points$in_cycle) * 4 / size
})
print(estimated_pi)
# [1] 3.184000 3.146400 3.137880 3.143140 3.141889
</code></pre>
<p>模拟 <code>$1000$</code> 个随机点的结果如图所示</p>
<p><img src="/images/cn/2017-12-17-mcmc-and-gibbs-sampling/mc-pi-simulation.png" alt=""></p>
<p>对于简单的分布 <code>$p\left(x\right)$</code>，我们可以相对容易的生成其样本，但对于复杂的分布或高维的分布，样本的生成就比较困难了<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>，例如：</p>
<ol>
<li><code>$p\left(x\right) = \dfrac{\tilde{p}\left(x\right)}{\int\tilde{p}\left(x\right) dx}$</code>，其中 <code>$\tilde{p}\left(x\right)$</code> 是可以计算的，而分母中的积分是无法显式计算的。</li>
<li><code>$p\left(x, y\right)$</code> 是一个二维分布函数，函数本身计算很困难，但其条件分布 <code>$p\left(x | y\right)$</code> 和 <code>$p\left(y | x\right)$</code> 计算相对简单。对于高维情况 <code>$p\left(\boldsymbol{x}\right)$</code>，这种情况则更加明显。</li>
</ol>
<p>这时候则需要更加复杂的模拟方法来生成样本了。</p>
<h2 id="马尔科夫链-markov-chain-mc">马尔科夫链 (Markov Chain, MC)</h2>
<p>马尔可夫过程 (Markov Process) 是因俄国数学家安德雷·安德耶维齐·马尔可夫 (Андрей Андреевич Марков) 而得名一个随机过程，在该随机过程中，给定当前状态和过去所有状态的条件下，其下一个状态的条件概率分布仅依赖于当前状态，通常具备离散状态的马尔科夫过程称之为马尔科夫链 (Markov Chain)。因此，马尔科夫链可以理解为一个有限状态机，给定了当前状态为 <code>$s_i$</code> 时，下一时刻状态为 <code>$s_j$</code> 的概率，不同状态之间变换的概率称之为转移概率。下图描述了 3 个状态 <code>$S_a, S_b, S_c$</code> 之间转换状态的马尔科夫链。</p>
<p><img src="/images/cn/2017-12-17-mcmc-and-gibbs-sampling/markov-chain-demo.png" alt=""></p>
<p>对于马尔科夫链，我们设 <code>$X_t$</code> 表示 <code>$t$</code> 时刻随机变量 <code>$X$</code> 的取值，则马尔科夫链可以表示为</p>
<p><code>$$ P\left(X_{t+1} = s_j | X_0 = s_{i0}, X_1 = s_{i1}, ..., X_t = s_i\right) = P\left(X_{t+1} | X_t = s_i\right) $$</code></p>
<p>其中，<code>$s_{i0}, s_{i1}, ..., s_i, s_j$</code> 为随机变量 <code>$X$</code> 可能的状态。则定义从一个状态 <code>$s_i$</code> 到另一个状态 <code>$s_j$</code> 的转移概率为</p>
<p><code>$$ P\left(i \to j\right) = P_{ij} = P\left(X_{t+1} | X_t = s_i\right) $$</code></p>
<p>设 <code>$\pi_{k}^{\left(t\right)}$</code> 表示随机变量 <code>$X$</code> 在 <code>$t$</code> 时刻取值为 <code>$s_k$</code> 的概率，则 <code>$X$</code> 在 <code>$t+1$</code> 时刻取值为 <code>$s_i$</code> 的概率为</p>
<p><code>$$ \begin{equation} \begin{split} \pi_{i}^{\left(t+1\right)} &amp;= P\left(X_{t+1} = s_i\right) \\ &amp;= \sum_{k}{P\left(X_{t+1} = s_i | X_t = s_k\right) \cdot P\left(X_t = s_k\right)} \\ &amp;= \sum_{k}{P_{ki} \cdot \pi_{k}^{\left(t\right)}} \end{split} \end{equation} $$</code></p>
<p>我们通过一个例子来理解一下马尔科夫链，我们使用 LDA 数学八卦<sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>一文中的例子，对于人口，我们将其经济状况分为 3 类：下层，中层和上层，其父代到子代收入阶层的转移情况如表所示</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">父代阶层\子代阶层</th>
          <th style="text-align: center">下层</th>
          <th style="text-align: center">中层</th>
          <th style="text-align: center">下层</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">下层</td>
          <td style="text-align: center">0.65</td>
          <td style="text-align: center">0.28</td>
          <td style="text-align: center">0.07</td>
      </tr>
      <tr>
          <td style="text-align: center">中层</td>
          <td style="text-align: center">0.15</td>
          <td style="text-align: center">0.67</td>
          <td style="text-align: center">0.18</td>
      </tr>
      <tr>
          <td style="text-align: center">上层</td>
          <td style="text-align: center">0.12</td>
          <td style="text-align: center">0.36</td>
          <td style="text-align: center">0.52</td>
      </tr>
  </tbody>
</table>
<p>我们利用矩阵的形式表示转移概率</p>
<p><code>$$ P = \left\lgroup \begin{array}{cccc} P_{11} &amp; P_{12} &amp; \cdots &amp; P_{1n} \\ P_{21} &amp; P_{22} &amp; \cdots &amp; P_{2n} \\ \vdots &amp; \vdots &amp;        &amp; \vdots \\ P_{n1} &amp; P_{n2} &amp; \cdots &amp; P_{nn} \end{array} \right\rgroup $$</code></p>
<p>则</p>
<p><code>$$ \pi^{\left(t+1\right)} = \pi^{\left(t\right)} P $$</code></p>
<p>假设初始概率分布为 <code>$\pi_0 = \left(0.21, 0.68, 0.11\right)$</code>，则计算前 <code>$n$</code> 代人的阶层分布情况如下</p>
<pre><code class="language-r"># 转移矩阵
p &lt;- matrix(c(0.65, 0.28, 0.07,
              0.15, 0.67, 0.18,
              0.12, 0.36, 0.52),
            3, 3, byrow = T)
# 初始概率
pi &lt;- matrix(c(0.21, 0.68, 0.11), 1, 3, byrow = T)

# 迭代变化
for (i in 1:10) {
    pi_current &lt;- pi[i, ]
    pi_next &lt;- pi_current %*% p
    pi &lt;- rbind(pi, pi_next)
}

colnames(pi) &lt;- c('下层', '中层', '上层')
rownames(pi) &lt;- 0:10
print(pi)
#         下层      中层      上层
# 0  0.2100000 0.6800000 0.1100000
# 1  0.2517000 0.5540000 0.1943000
# 2  0.2700210 0.5116040 0.2183750
# 3  0.2784592 0.4969956 0.2245452
# 4  0.2824933 0.4917919 0.2257148
# 5  0.2844752 0.4898560 0.2256688
# 6  0.2854675 0.4890974 0.2254351
# 7  0.2859707 0.4887828 0.2252465
# 8  0.2862280 0.4886450 0.2251270
# 9  0.2863602 0.4885817 0.2250581
# 10 0.2864283 0.4885515 0.2250201
</code></pre>
<p>可以看出，从第 7 代人开始，分布就基本稳定下来了，如果将初值概率换成 <code>$\pi_0 = \left(0.75, 0.15, 0.1\right)$</code>，结果会是如何呢？</p>
<pre><code class="language-r">pi &lt;- matrix(c(0.75, 0.15, 0.1), 1, 3, byrow = T)

for (i in 1:10) {
    pi_current &lt;- pi[i, ]
    pi_next &lt;- pi_current %*% p
    pi &lt;- rbind(pi, pi_next)
}

colnames(pi) &lt;- c('下层', '中层', '上层')
rownames(pi) &lt;- 0:10
print(pi)
#         下层      中层      上层
# 0  0.7500000 0.1500000 0.1000000
# 1  0.5220000 0.3465000 0.1315000
# 2  0.4070550 0.4256550 0.1672900
# 3  0.3485088 0.4593887 0.1921025
# 4  0.3184913 0.4745298 0.2069789
# 5  0.3030363 0.4816249 0.2153388
# 6  0.2950580 0.4850608 0.2198812
# 7  0.2909326 0.4867642 0.2223032
# 8  0.2887972 0.4876223 0.2235805
# 9  0.2876912 0.4880591 0.2242497
# 10 0.2871181 0.4882830 0.2245989
</code></pre>
<p>可以看出从第 9 代人开始，分布又变得稳定了，这也就是说分布收敛情况是不随初始概率分布 <code>$\pi_0$</code> 的变化而改变的。则对于具有如下特征的马尔科夫链</p>
<ol>
<li>非周期性，可以简单理解为如果一个状态有自环，或者与一个非周期的状态互通，则是非周期的。</li>
<li>不可约性，即任意两个状态都是互通的。</li>
</ol>
<p>则这样的马尔科夫链，无论 <code>$\pi_0$</code> 取值如何，最终随机变量的分布都会收敛于 <code>$\pi^*$</code>，即</p>
<p><code>$$ \pi^* = \lim_{t \to \infty}{\pi^{\left(0\right)} \boldsymbol{P}^t} $$</code></p>
<p><code>$\pi^*$</code> 称之为这个马尔科夫链的平稳分布。</p>
<h2 id="马尔科夫链蒙特卡洛方法-mcmc">马尔科夫链蒙特卡洛方法 (MCMC)</h2>
<p>构造一个转移矩阵为 <code>$P$</code> 的马尔科夫链，如果其能收敛到平稳分布 <code>$p\left(x\right)$</code>，则可以从任意一个状态 <code>$x_0$</code> 出发，得到一个状态转移序列 <code>$x_0, x_1, ..., x_n, x_{n+1}, ...$</code>，如果马尔科夫链在第 <code>$n$</code> 部收敛，我们就可以得到服从分布 <code>$p\left(x\right)$</code> 的样本 <code>$x_n, x_{n+1}, ...$</code>。因此，利用马尔科夫链的平稳性生成数据的样本的关键就在于如何构造一个状态转移矩阵 <code>$P$</code>，使得其平稳分布为 <code>$p\left(x\right)$</code>。</p>
<p>如果对于任意的 <code>$i, j$</code>，马尔科夫链的转移矩阵 <code>$P$</code> 和分布 <code>$\pi\left(x\right)$</code> 满足</p>
<p><code>$$ \pi\left(i\right) P_{ij} = \pi\left(j\right) P_{ji} $$</code></p>
<p>则称 <code>$\pi\left(x\right)$</code> 为马尔科夫链的平稳分布，这称为<strong>细致平稳条件</strong>。对于一个马尔科夫链，通常情况下</p>
<p><code>$$ p\left(i\right) q\left(i, j\right) \neq p\left(j\right) q\left(j, i\right) $$</code></p>
<p>其中 <code>$p\left(i, j\right)$</code> 表示状态从 <code>$i$</code> 转移到 <code>$j$</code> 的概率。因此，为了构造满足细致平稳条件，我们引入一个<strong>接受概率</strong> <code>$\alpha\left(i, j\right)$</code>，使得</p>
<p><code>$$ p\left(i\right) q\left(i, j\right) \alpha\left(i, j\right) = p\left(j\right) q\left(j, i\right) \alpha\left(j, i\right) $$</code></p>
<p>最简单的，我们取</p>
<p><code>$$ \alpha\left(i, j\right) = p\left(j\right) q\left(j, i\right), \alpha\left(j, i\right) = p\left(i\right) q\left(i, j\right) $$</code></p>
<p>即可保证细致平稳性。通过引入接受概率，我们将原始的马尔科夫链改造为具有新的转移矩阵的马尔科夫链。在原始马尔科夫链上以概率 <code>$q\left(i, j\right)$</code> 从状态 <code>$i$</code> 转移到状态 <code>$j$</code> 时，我们以概率 <code>$\alpha\left(i, j\right)$</code> 接受这个转移，因此在新的马尔科夫链上的转移概率为 <code>$q\left(i, j\right) \alpha\left(i, j\right)$</code>。在新的马尔科夫链转移的过程中，如果接受概率 <code>$\alpha\left(i, j\right)$</code> 过小，则可能导致存在大量的拒绝转移，马尔科夫链则很难收敛到平稳分布 <code>$p\left(x\right)$</code>，因此我们对 <code>$\alpha\left(i, j\right), \alpha\left(j, i\right)$</code> 进行同比例放大，将其中较大的数放大至 <code>$1$</code>，则可以增加接受跳转的概率，从而更快的收敛到平稳分布。因此，我们可以取</p>
<p><code>$$ \alpha\left(i, j\right) = \min \left\lbrace\dfrac{p\left(j\right) q\left(j, i\right)}{p\left(i\right) q\left(i, j\right)}, 1\right\rbrace $$</code></p>
<p>这样我们就得到了 Metropolis-Hastings 算法</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Metropolis-Hastings 算法}
\begin{algorithmic}
\STATE $X_0 \gets x_0$
\FOR{$t = 0, 1, 2, ...$}
    \STATE $X_t = x_t$
    \STATE sample $y \sim q\left(x | x_t\right)$
    \STATE sample $u \sim Uniform[0, 1]$
    \IF{$u < \alpha\left(x_t, y\right) = \min\left\lbrace\dfrac{p\left(j\right) q\left(j, i\right)}{p\left(i\right) q\left(i, j\right)}, 1\right\rbrace$}
        \STATE $X_{t+1} \gets y$
    \ELSE
        \STATE $X_{t+1} \gets x_t$
    \ENDIF
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<h2 id="吉布斯采样-gibbs-sampling">吉布斯采样 (Gibbs Sampling)</h2>
<p>对于 Metropolis-Hastings 算法，由于存在接受跳转概率 <code>$\alpha &lt; 1$</code>，因此为了提高算法效率，我们尝试构建一个转移矩阵，使得 <code>$\alpha = 1$</code>。以二维情形为例，对于概率分布 <code>$p\left(x, y\right)$</code>，考虑两个点 <code>$A\left(x_1, y_1\right)$</code> 和 <code>$B\left(x_1, y_2\right)$</code></p>
<p><code>$$ \begin{equation} \begin{split} p\left(x_1, y_1\right) p\left(y_2 | x_1\right) &amp;= p\left(x_1\right) p\left(y_1 | x_1\right) p\left(y_2 | x_1\right) \\ p\left(x_1, y_2\right) p\left(y_1 | x_1\right) &amp;= p\left(x_1\right) p\left(y_2 | x_1\right) p\left(y_1 | x_1\right) \end{split} \end{equation} $$</code></p>
<p>可得</p>
<p><code>$$ \begin{equation} \begin{split} p\left(x_1, y_1\right) p\left(y_2 | x_1\right) &amp;= p\left(x_1, y_2\right) p\left(y_1 | x_1\right) \\ p\left(A\right) p\left(y_2 | x_1\right) &amp;= p\left(B\right) p\left(y_1 | x_1\right) \end{split} \end{equation} $$</code></p>
<p>可以得出在 <code>$x = x_1$</code> 上任意两点之间进行转移均满足细致平稳条件，同理可得在 <code>$y = y_1$</code>上也满足。因此，对于二维情况，我们构建满足如下调价的概率转移矩阵 <code>$Q$</code></p>
<p><code>$$ \begin{equation} \begin{split} &amp;Q\left(A \to B\right) = p\left(y_B | x_1\right), \text{for} \ x_A = x_B = x_1 \\ &amp;Q\left(A \to C\right) = p\left(x_C | y_1\right), \text{for} \ y_A = y_C = y_1 \\ &amp;Q\left(A \to D\right) = 0, \text{others} \end{split} \end{equation} $$</code></p>
<p>则对于平面上任意两点 <code>$X, Y$</code> 满足细致平稳条件</p>
<p><code>$$ p\left(X\right) Q\left(X \to Y\right) = p\left(Y\right) Q\left(Y \to X\right) $$</code></p>
<p>对于如上过程，我们不难推广到多维情况，将 <code>$x_1$</code> 变为多维情形 <code>$\boldsymbol{x_1}$</code>，容易验证细致平稳条件依旧成立。</p>
<p><code>$$ p\left(\boldsymbol{x_1}, y_1\right) p\left(y_2 | \boldsymbol{x_1}\right) = p\left(\boldsymbol{x_1}, y_2\right) p\left(y_1 | \boldsymbol{x_1}\right) $$</code></p>
<p>对于 <code>$n$</code> 维的情况，通过不断的转移得到样本 <code>$\left(x_1^{\left(1\right)}, x_2^{\left(1\right)}, ..., x_n^{\left(1\right)}\right)$</code>, <code>$\left(x_1^{\left(2\right)}, x_2^{\left(2\right)}, ..., x_n^{\left(2\right)}\right)$</code>, &hellip;，当马尔科夫链收敛后，后续得到的样本即为 <code>$p\left(x_1, x_2, ..., x_n\right)$</code> 的样本，收敛之前的这一阶段我们称之为 <strong>burn-in period</strong>。在进行转移的时候，坐标轴轮换的采样方法并不是必须的，可以在坐标轴轮换中引入随机性。至此，我们就得到了吉布斯采样算法</p>


<div><pre class="pseudocode">
\begin{algorithm}
\caption{Gibbs Sampling 算法}
\begin{algorithmic}
\STATE initialize $x^{\left(0\right)}, \text{for} \ i = 1, 2, ..., n$
\FOR{$t = 0, 1, 2, ...$}
    \STATE $x_1^{\left(t+1\right)} \sim p\left(x_1 | x_2^{\left(t\right)}, x_3^{\left(t\right)}, ..., x_n^{\left(t\right)}\right)$
    \STATE $x_2^{\left(t+1\right)} \sim p\left(x_2 | x_1^{\left(t\right)}, x_3^{\left(t\right)}, ..., x_n^{\left(t\right)}\right)$
    \STATE $...$
    \STATE $x_n^{\left(t+1\right)} \sim p\left(x_n | x_1^{\left(t\right)}, x_2^{\left(t\right)}, ..., x_{n-1}^{\left(t\right)}\right)$
\ENDFOR
\end{algorithmic}
\end{algorithm}
</pre></div>

<p>我们以二元高斯分布为例，演示如何用 Gibbs Sampling 方法进行采样，二元高斯分布定义为</p>
<p><code>$$ \left(X, Y\right) \sim \mathcal{N}\left(\boldsymbol{\mu}, \boldsymbol{\Sigma}\right) $$</code></p>
<p>其中</p>
<p><code>$$ \boldsymbol{\mu} = \left\lgroup \begin{array}{c} \mu_X \\ \mu_Y \end{array} \right\rgroup, \boldsymbol{\Sigma} = \left\lgroup \begin{array}{cc} \sigma_X^2 &amp; \rho \sigma_X \sigma_Y \\ \rho \sigma_X \sigma_Y &amp; \sigma_Y^2 \end{array} \right\rgroup $$</code></p>
<p>因此可得</p>
<p><code>$$ \begin{equation} \begin{split} \mu_{x|y} &amp;= \mu_x + \sigma_x \rho_x\left(\dfrac{y - \mu_y}{\sigma_y}\right), \sigma_{x|y}^2 = \sigma_x^2 \left(1 - \rho^2\right) \\ \mu_{y|x} &amp;= \mu_y + \sigma_y \rho_y\left(\dfrac{y - \mu_x}{\sigma_x}\right), \sigma_{y|x}^2 = \sigma_y^2 \left(1 - \rho^2\right) \end{split} \end{equation} $$</code></p>
<p>则</p>
<p><code>$$ \begin{equation} \begin{split} X|Y &amp;= \mu_{x|y} + \sigma_{x|y} \mathcal{N}\left(0, 1\right) \\ Y|X &amp;= \mu_{y|x} + \sigma_{y|x} \mathcal{N}\left(0, 1\right) \end{split} \end{equation} $$</code></p>
<p>对于 <code>$\mu_x = 0, \mu_y = 0, \sigma_x = 10, \sigma_y = 1, \rho = 0.8$</code>，采样过程如下</p>
<pre><code class="language-r">mu_x &lt;- 0
mu_y &lt;- 0
sigma_x &lt;- 10
sigma_y &lt;- 1
rho &lt;- 0.8

iter &lt;- 1000
samples &lt;- matrix(c(mu_x, mu_y), 1, 2, byrow = T)

set.seed(112358)
for (i in 1:iter) {
    sample_x &lt;- mu_x +
        sigma_x * rho * (samples[i, 2] - mu_y) / sigma_y +
        sigma_x * sqrt(1 - rho^2) * rnorm(1)
    sample_y &lt;- mu_y +
        sigma_y * rho * (sample_x - mu_x) / sigma_x +
        sigma_y * sqrt(1 - rho^2) * rnorm(1)
    samples &lt;- rbind(samples, c(sample_x, sample_y))
}
</code></pre>
<p>可视化结果如下</p>
<p><img src="/images/cn/2017-12-17-mcmc-and-gibbs-sampling/gibbs-sampling-bivariate-guassian-distribution.gif" alt=""></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>LDA 数学八卦，靳志辉，2013&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>特征值分解，奇异值分解和主成份分析 (EVD, SVD and PCA)</title><link>https://zeqiang.fun/cn/2017/12/evd-svd-and-pca/</link><pubDate>Mon, 11 Dec 2017 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2017/12/evd-svd-and-pca/</guid><description><![CDATA[
        <p><code>$\renewcommand{\diag}{\operatorname{diag}}\renewcommand{\cov}{\operatorname{cov}}$</code></p>
<h2 id="准备知识">准备知识</h2>
<h3 id="向量与基">向量与基</h3>
<p>首先，定义 <code>$\boldsymbol{\alpha}$</code> 为列向量，则维度相同的两个向量 <code>$\boldsymbol{\alpha}, \boldsymbol{\beta}$</code> 的内积可以表示为：</p>
<p><code>$$\boldsymbol{\alpha} \cdot \boldsymbol{\beta} = \boldsymbol{\alpha}^T \boldsymbol{\beta} = \sum_{i=1}^{n}{\alpha_i b_i}$$</code></p>
<p>后续为了便于理解，我们以二维向量为例，则 <code>$\boldsymbol{\alpha} = \left(x_1, y_1\right)^T, \boldsymbol{\beta} = \left(x_2, y_2\right)^T$</code>，在直角座标系中可以两个向量表示如下：</p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/vector-inner-product-and-projection.png" alt=""></p>
<p>我们从 <code>$A$</code> 点向向量 <code>$\boldsymbol{\beta}$</code> 的方向做一条垂线，交于点 <code>$C$</code>，则称 <code>$OC$</code> 为 <code>$OA$</code> 在 <code>$OB$</code> 方向上的投影。设向量 <code>$\boldsymbol{\alpha}$</code> 和向量 <code>$\boldsymbol{\beta}$</code> 的夹角为 <code>$\theta$</code>，则：</p>
<p><code>$$\cos \left(\theta\right) = \dfrac{\boldsymbol{\alpha} \cdot \boldsymbol{\beta}}{\lvert\boldsymbol{\alpha}\rvert \lvert\boldsymbol{\beta}\rvert}$$</code></p>
<p>其中，<code>$\lvert\boldsymbol{\alpha}\rvert = \sqrt{x_1^2 + y_1^2}$</code>，则 <code>$OC$</code> 的长度为 <code>$\lvert\boldsymbol{\alpha}\rvert \cos\left(\theta\right)$</code>。</p>
<p>在 <code>$n$</code> 维的线性空间 <code>$V$</code> 中，<code>$n$</code> 个线性无关的向量 <code>$\boldsymbol{\epsilon_1, \epsilon_2, ..., \epsilon_n}$</code> 称为 <code>$V$</code> 的一组<strong>基</strong>。则对于 <code>$V$</code> 中的任一向量 <code>$\boldsymbol{\alpha}$</code> 可以由这组基线性表示出来：</p>
<p><code>$$\boldsymbol{\alpha} = x_1 \boldsymbol{\epsilon_1} + x_2 \boldsymbol{\epsilon_2} + ... + x_n \boldsymbol{\epsilon_n}$$</code></p>
<p>则对于向量 <code>$\boldsymbol{\alpha} = \left(3, 2\right)^T$</code>，可以表示为：</p>
<p><code>$$\boldsymbol{\alpha} = 2 \left(1, 0\right)^T + 3 \left(0, 1\right)^T$$</code></p>
<p>其中 <code>$\left(1, 0\right)^T$</code> 和 <code>$\left(0, 1\right)^T$</code> 为二维空间中的一组基。</p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/vector-bases.png" alt=""></p>
<p>因此，当我们确定好一组基之后，我们仅需利用向量在基上的投影值即可表示对应的向量。一般情况下，我们会选择由坐标轴方向上的单位向量构成的基作为默认的基来表示向量，但我们仍可选择其他的基。例如，我们选择 <code>$\left(-\dfrac{1}{\sqrt{2}}, \dfrac{1}{\sqrt{2}}\right)$</code> 和 <code>$\left(\dfrac{1}{\sqrt{2}}, \dfrac{1}{\sqrt{2}}\right)$</code> 作为一组基，则向量在这组基上的坐标为 <code>$\left(-\dfrac{1}{\sqrt{2}}, \dfrac{5}{\sqrt{2}}\right)$</code>，示例如下：</p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/vector-change-of-bases.png" alt=""></p>
<h3 id="线性变换">线性变换</h3>
<p>以二维空间为例，定义一个如下矩阵</p>
<p><code>$$ A = \left\lgroup \begin{array}{cc} a_{11} &amp; a_{12} \\ a_{21} &amp; a_{22} \end{array} \right\rgroup $$</code></p>
<p>则对于二维空间中一个向量 <code>$\boldsymbol{\alpha} = \left(x, y\right)^T$</code> ，通过同上述矩阵进行乘法运算，可得</p>
<p><code>$$ \boldsymbol{\alpha'} = A \boldsymbol{\alpha} = \left\lgroup \begin{array}{cc} a_{11} &amp; a_{12} \\ a_{21} &amp; a_{22} \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup =  \left\lgroup \begin{array}{c} x' \\ y' \end{array} \right\rgroup $$</code></p>
<p>(1) 通过变换将任意一个点 <code>$x$</code> 变成它关于 <code>$x$</code> 轴对称的点 <code>$x'$</code></p>
<p><code>$$ x' = \left\lgroup \begin{array}{cc} 1 &amp; 0 \\ 0 &amp; -1 \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup =  \left\lgroup \begin{array}{c} x \\ -y \end{array} \right\rgroup $$</code></p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/vector-linear-transformation-1.png" alt=""></p>
<p>(2) 通过变换将任意一个点 <code>$x$</code> 变成它关于 <code>$y = x$</code> 对称的点 <code>$x'$</code></p>
<p><code>$$ x' = \left\lgroup \begin{array}{cc} 0 &amp; 1 \\ 1 &amp; 0 \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup =  \left\lgroup \begin{array}{c} y \\ x \end{array} \right\rgroup $$</code></p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/vector-linear-transformation-2.png" alt=""></p>
<p>(3) 变换将任意一个点 <code>$x$</code> 变成在它与原点连线上，与原点距离伸缩为 <code>$|\lambda|$</code> 倍的点 <code>$x'$</code></p>
<p><code>$$ x' = \left\lgroup \begin{array}{cc} \lambda &amp; 0 \\ 0 &amp; \lambda \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup =  \left\lgroup \begin{array}{c} \lambda x \\ \lambda y \end{array} \right\rgroup $$</code></p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/vector-linear-transformation-3.png" alt=""></p>
<p>(4) 通过变换将任意一个点 <code>$x$</code> 绕原点旋转了角度 <code>$\theta$</code> 的点 <code>$x'$</code></p>
<p><code>$$ \begin{equation} \begin{split} x'&amp; = \left\lgroup \begin{array}{cc} \cos \theta &amp; -\sin \theta \\ \sin \theta &amp; \cos \theta \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup \\ &amp; =  \left\lgroup \begin{array}{cc} \cos \theta &amp; -\sin \theta \\ \sin \theta &amp; \cos \theta \end{array} \right\rgroup \left\lgroup \begin{array}{c} r \cos \phi \\ r \sin \phi \end{array} \right\rgroup \\ &amp; =  \left\lgroup \begin{array}{c} r \cos \left(\phi + \theta\right) \\ r \sin \left(\phi + \theta\right) \end{array} \right\rgroup \end{split} \end{equation} $$</code></p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/vector-linear-transformation-4.png" alt=""></p>
<p>(5) 变换将任意一个点 <code>$x$</code> 变成它在 <code>$x$</code> 轴上 的投影点 <code>$x'$</code></p>
<p><code>$$ x' = \left\lgroup \begin{array}{cc} 1 &amp; 0 \\ 0 &amp; 0 \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup =  \left\lgroup \begin{array}{c} x \\ 0 \end{array} \right\rgroup $$</code></p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/vector-linear-transformation-5.png" alt=""></p>
<h2 id="特征值分解">特征值分解</h2>
<p>设 <code>$A$</code> 是线性空间 <code>$V$</code> 上的一个线性变换，对于一个非零向量 <code>$\boldsymbol{\alpha} = \left(x_1, x_2, ..., x_n\right)^T$</code> 使得</p>
<p><code>$$A \boldsymbol{\alpha} = \lambda \boldsymbol{\alpha}$$</code></p>
<p>则 <code>$\lambda$</code> 称为 <code>$A$</code> 的一个<strong>特征值</strong>，<code>$\boldsymbol{\alpha}$</code> 称为 <code>$A$</code> 的一个<strong>特征向量</strong>。通过</p>
<p><code>$$ \begin{equation} \begin{split} A \boldsymbol{\alpha} &amp;= \lambda \boldsymbol{\alpha} \\ A \boldsymbol{\alpha} - \lambda \boldsymbol{\alpha} &amp;= 0 \\ \left(A - \lambda E\right) \boldsymbol{\alpha} &amp;= 0 \\ A - \lambda E &amp;= 0 \end{split} \end{equation} $$</code></p>
<p>其中 <code>$E = \diag \left(1, 1, ..., 1\right)$</code> 为单位对角阵，即可求解其特征值，进而求解特征向量。若 <code>$A$</code> 是一个可逆矩阵，则上式可以改写为：</p>
<p><code>$$ A = Q \sum Q^{-1} $$</code></p>
<p>这样，一个方阵 <code>$A$</code> 就被一组特征值和特征向量表示了。例如，对于如下矩阵进行特征值分解</p>
<p><code>$$ A = \left\lgroup \begin{array}{cccc} 3 &amp; -2 &amp; -0.9 &amp; 0 \\ -2 &amp; 4 &amp; 1 &amp; 0 \\ 0 &amp; 0 &amp; -1 &amp; 0 \\ -0.5 &amp; -0.5 &amp; 0.1 &amp; 1 \end{array} \right\rgroup $$</code></p>
<pre><code class="language-r">A &lt;- matrix(c(3, -2, -0.9, 0,
              -2, 4, 1, 0,
              0, 0, -1, 0,
              -0.5, -0.5, 0.1, 1),
            4, 4, byrow = T)
A_eig &lt;- eigen(A)
print(A_eig)

# eigen() decomposition
# $values
# [1]  5.561553  1.438447  1.000000 -1.000000
# 
# $vectors
#             [,1]       [,2] [,3]        [,4]
# [1,] -0.61530186  0.4176225    0  0.15282144
# [2,]  0.78806410  0.3260698    0 -0.13448286
# [3,]  0.00000000  0.0000000    0  0.97805719
# [4,] -0.01893678 -0.8480979    1 -0.04431822
</code></pre>
<p>则利用特征值和特征向量，可以还原原矩阵</p>
<pre><code class="language-r">A_re &lt;- A_eig$vectors %*%
    diag(A_eig$values) %*%
    solve(A_eig$vectors)
print(A_re)

#      [,1] [,2] [,3] [,4]
# [1,]  3.0 -2.0 -0.9    0
# [2,] -2.0  4.0  1.0    0
# [3,]  0.0  0.0 -1.0    0
# [4,] -0.5 -0.5  0.1    1
</code></pre>
<h2 id="奇异值分解">奇异值分解</h2>
<p>特征值分解针对的是方阵，对于一个 <code>$m*n$</code> 的矩阵是无法进行特征值分解的，这时我们就需要使用奇异值分解来解决这个问题。对于 <code>$m*n$</code> 的矩阵 <code>$A$</code>，可得 <code>$A A^T$</code> 是一个 <code>$m*m$</code> 的方阵，则针对 <code>$A A^T$</code>，通过 <code>$\left(A A^T\right) \boldsymbol{\alpha} = \lambda \boldsymbol{\alpha}$</code>，即可求解这个方阵的特征值和特征向量。针对矩阵 <code>$A$</code>，奇异值分解是将原矩阵分解为三个部分</p>
<p><code>$$ A_{m*n} = U_{m*r} \sum\nolimits_{r*r} V_{r*n}^T $$</code></p>
<p>其中 <code>$U$</code> 称之为左奇异向量，即为 <code>$A A^T$</code> 单位化后的特征向量；<code>$V$</code> 称之为右奇异向量，即为 <code>$A^T A$</code> 单位化后的特征向量；<code>$\sum$</code>矩阵对角线上的值称之为奇异值，即为 <code>$A A^T$</code> 或 <code>$A^T A$</code> 特征值的平方根。</p>
<p>我们利用经典的 lena 图片展示一下 SVD 的作用，lena图片为一张 <code>$512*512$</code> 像素的彩色图片</p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/lena-std.png" alt=""></p>
<p>我们对原始图片进行灰度处理后，进行特征值分解，下图中从左到右，从上到下分别是原始的灰度图像，利用 20 个左奇异向量和 20 个右奇异向量重构图像，利用 50 个左奇异向量和 100 个右奇异向量重构图像，利用 200 个左奇异向量和 200 个右奇异向量重构图像。</p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/lena-reconstruction.png" alt=""></p>
<p>从图中可以看出，我们仅用了 200 个左奇异向量和 200 个右奇异向量重构图像与原始灰度图像已经基本看不出任何区别。因此，我们利用 SVD 可以通过仅保留较大的奇异值实现数据的压缩。</p>
<h2 id="主成份分析">主成份分析</h2>
<p>主成份分析<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>可以通俗的理解为一种降维方法。其目标可以理解为将一个 <code>$m$</code> 维的数据转换称一个 <code>$k$</code> 维的数据，其中 <code>$k &lt; m$</code>。对于具有 <code>$n$</code> 个样本的数据集，设 <code>$\boldsymbol{x_i}$</code> 表示 <code>$m$</code> 维的列向量，则</p>
<p><code>$$ X_{m*n} = \left(\boldsymbol{x_1}, \boldsymbol{x_2}, ..., \boldsymbol{x_n}\right) $$</code></p>
<p>对每一个维度进行零均值化，即减去这一维度的均值</p>
<p><code>$$ X'_{m*n} = X - \boldsymbol{u}\boldsymbol{h} $$</code></p>
<p>其中，<code>$\boldsymbol{u}$</code> 是一个 <code>$m$</code> 维的行向量，<code>$\boldsymbol{u}[m] = \dfrac{1}{n} \sum_{i=1}^{n} X[m, i]$</code>；<code>$h$</code> 是一个值全为 <code>$1$</code> 的 <code>$n$</code> 维行向量。</p>
<p>对于两个随机变量，我们可以利用协方差简单表示这两个变量之间的相关性</p>
<p><code>$$ \cov \left(x, y\right) = E \left(\left(x - \mu_x\right) \left(x - \mu_x\right)\right) $$</code></p>
<p>对于已经零均值化后的矩阵 <code>$X'$</code>，计算得出如下矩阵</p>
<p><code>$$ C = \dfrac{1}{n} X' X'^T = \left\lgroup \begin{array}{cccc} \dfrac{1}{n} \sum_{i=1}^{n} x_{1i}^2 &amp; \dfrac{1}{n} \sum_{i=1}^{n} x_{1i} x_{2i} &amp; \cdots &amp; \dfrac{1}{n} \sum_{}^{} x_{1i} x_{ni} \\ \dfrac{1}{n} \sum_{i=1}^{n} x_{2i} x_{1i} &amp; \dfrac{1}{n} \sum_{i=1}^{n} x_{2i}^2 &amp; \cdots &amp; \dfrac{1}{n} \sum_{}^{} x_{2i} x_{ni} \\ \vdots &amp; \vdots &amp; &amp; \vdots \\ \dfrac{1}{n} \sum_{i=1}^{n} x_{mi} x_{1i} &amp; \dfrac{1}{n} \sum_{i=1}^{n} x_{mi} x_{2i} &amp; \cdots &amp; \dfrac{1}{n} \sum_{}^{} x_{mi}^2 \\ \end{array} \right\rgroup $$</code></p>
<p>因为矩阵 <code>$X'$</code> 已经经过了零均值化处理，因此矩阵 <code>$C$</code> 中对角线上的元素为维度 <code>$m$</code> 的方差，其他元素则为两个维度之间的协方差。</p>
<p>从 PCA 的目标来看，我们则可以通过求解矩阵 <code>$C$</code> 的特征值和特征向量，将其特征值按照从大到小的顺序按行重排其对应的特征向量，则取前 <code>$k$</code> 个，则实现了数据从 <code>$m$</code> 维降至 <code>$k$</code> 维。</p>
<p>例如，我们将二维数据</p>
<p><code>$$ \left\lgroup \begin{array} -1 &amp; -1 &amp; 0 &amp; 0 &amp; 2 \\ -2 &amp; 0 &amp; 0 &amp; 1 &amp; 1 \end{array} \right\rgroup $$</code></p>
<p>降至一维</p>
<pre><code class="language-r">x &lt;- matrix(c(-1, -1, 0, 0, 2,
              -2, 0, 0, 1, 1),
            5, 2, byrow = F)
x_pca &lt;- prcomp(x)

print(pca)
# Standard deviations (1, .., p=2):
# [1] 1.5811388 0.7071068
# 
# Rotation (n x k) = (2 x 2):
#            PC1        PC2
# [1,] 0.7071068  0.7071068
# [2,] 0.7071068 -0.7071068

summary(pca)
# Importance of components:
#                           PC1    PC2
# Standard deviation     1.5811 0.7071
# Proportion of Variance 0.8333 0.1667
# Cumulative Proportion  0.8333 1.0000

x_ &lt;- predict(x_pca, x)
print(x_)
#             PC1        PC2
# [1,] -2.1213203  0.7071068
# [2,] -0.7071068 -0.7071068
# [3,]  0.0000000  0.0000000
# [4,]  0.7071068 -0.7071068
# [5,]  2.1213203  0.7071068
</code></pre>
<p>降维的投影结果如图所示</p>
<p><img src="/images/cn/2017-12-11-evd-svd-and-pca/pca-projection.png" alt=""></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Wold, Svante, Kim Esbensen, and Paul Geladi. &ldquo;Principal component analysis.&rdquo; <em>Chemometrics and intelligent laboratory systems</em> 2.1-3 (1987): 37-52.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item><item><title>墨尔本之行 (Trip to Melbourne)</title><link>https://zeqiang.fun/cn/2017/08/trip-to-melbourne/</link><pubDate>Sat, 26 Aug 2017 00:00:00 +0000</pubDate><guid>https://zeqiang.fun/cn/2017/08/trip-to-melbourne/</guid><description><![CDATA[
        <p>从下了飞机到酒店，一路上体验到了我国互联网对世界各地的影响，机场巴士可以用微信和支付宝，下了巴士发现这里也有共享单车，人家还配了头盔。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  
<link rel="stylesheet" href="/css/photoswipe.css" />
<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/airport-bus-chinese-online-payment.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/airport-bus-chinese-online-payment.jpg" alt="我天朝的移动支付"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/airport-bus-chinese-online-payment.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>我天朝的移动支付</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/shared-bikes.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/shared-bikes.jpg" alt="带头盔的共享单车"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/shared-bikes.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>带头盔的共享单车</p>
      </figcaption>
  </figure>
</div>


</div>

<p>晚上雅拉河还是很漂亮的，天气略凉，空气不错，淡淡的云，大冷天的人们也很愿意在外面吃饭。</p>

<div class="box fancy-figure caption-position-bottom caption-effect-fade" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/first-sight-of-melbourne-night.jpg" alt="First Sight of Melbourne&#39;s Night"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/first-sight-of-melbourne-night.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>First Sight of Melbourne&#39;s Night</p>
      </figcaption>
  </figure>
</div>

<p>谈到澳大利亚，最熟知的两种动物就是精壮的袋鼠和呆萌的考拉，晚上吃饭看了菜单发现居然有袋鼠肉，味道还是很不错的，没什么奇怪的味道。后来得知，在澳洲袋鼠的数量算得上略微的泛滥，所以法律是允许吃袋鼠的，如果你在路上开车不幸撞上了一只袋鼠，那么请快速的结束他的生命就好，免得痛苦，但如果你撞了一只考拉，那好吧，估计你出不去澳大利亚了&hellip;&hellip;</p>


<div class="gallery  caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/kangaroo.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/kangaroo.jpg" alt="袋鼠：“你瞅啥？”"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/kangaroo.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>袋鼠：“你瞅啥？”</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/kangaroo-meat.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/kangaroo-meat.jpg" alt="袋鼠 - 卒！"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/kangaroo-meat.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>袋鼠 - 卒！</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/koala.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/koala.jpg" alt="呆萌的考拉"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/koala.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>呆萌的考拉</p>
      </figcaption>
  </figure>
</div>


</div>

<p>当然，本次旅程最重要的还是 IJCAI 大会，让我这个半路出家搞 AI 的人感触最多的是：未知的还有很多，要学的也还有很多。这次大会期间也再次发表了呼吁禁止自主武器的公开信，我认为有时候我们更多的关注了 AI 技术的层面，而忽略了很多其他的事情，例如伦理和道德。例如现在医学图像识别技术在一定范围内已经超越了人类，那么是否我们就可以让机器直接做决定呢，如果出了问题，将如何处理，所以这个边界到底在哪里也是值的我们仔细思考的。</p>


<div class="gallery  caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/ijcai-opening.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/ijcai-opening.jpg" alt="Welcome to IJCAI"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/ijcai-opening.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>Welcome to IJCAI</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/ijcai-jd-me.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/ijcai-jd-me.jpg" alt="我司的展台"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/ijcai-jd-me.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>我司的展台</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/ijcai-closing.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/ijcai-closing.jpg" alt="Bye, IJCAI"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/ijcai-closing.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>Bye, IJCAI</p>
      </figcaption>
  </figure>
</div>


</div>

<p>这边的冬天是多雨的，来的时候推算这边应该是早春，没带太厚的衣服，和当地人聊到天气，我说现在是 Early spring 怎么怎么的，结果对方说 No, No, No, still winter。尽管天气还是很凉，不过白天甚至晚上外面还是有很多路演的艺人，下面这个大哥唱的特别好，没一会儿就围了一圈人，他说他在为了下一次旅行筹钱，想想我何时才能有这样的勇气和行动力。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/road-show-singer-1.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/road-show-singer-1.jpg" alt="路演女孩"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/road-show-singer-1.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>路演女孩</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/road-show-singer-2.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/road-show-singer-2.jpg" alt="路演大哥"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/road-show-singer-2.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>路演大哥</p>
      </figcaption>
  </figure>
</div>


</div>

<p>2017 年，墨尔本连续 7 年蝉联全球最适宜居住的城市冠军，很大一部分源自于其便利的交通。在墨尔本都是路上的有轨电车，还有一趟环城线路是免费的，晚上搭着它环城了一圈，最大的体会是，这儿真不大&hellip;&hellip; 路过市政厅，发现门口有人滑滑板，我也就敢在我们村委会门口玩一玩。环城电车有一个折返点，不像北京 10 号线那样无限循环，有点像带折返点的单程线，就在到头的时候奇迹发生了。我以为司机会下车到另一头再发动电车，不过他不是一个人离开的，还带走了方向盘，对是方向盘，劳动人民是智慧的，这车根本就不需要钥匙，拿走方向盘就行啦，不过话说有轨电车的方向盘又是干嘛用的。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/free-train.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/free-train.jpg" alt="免费的有轨电车"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/free-train.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>免费的有轨电车</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/city-hall.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/city-hall.jpg" alt="市政大厅"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/city-hall.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>市政大厅</p>
      </figcaption>
  </figure>
</div>


</div>

<p>作为不折不扣的伪军迷，自然也要了解一下这个城市的军事历史，说到这就不得不提大洋路 <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>。大洋路是位于澳洲东海岸维多利亚州南部的一条行车公路，全长约 276 公里，建于悬崖峭壁中间，起点自托尔坎 (Torquay)，终点于亚伦斯福特 (Allansford)。大洋路始建于 1920 年，在 1932 年竣工，澳洲政府借此纪念第一次世界大战中牺牲的人。</p>

<div class="box fancy-figure caption-position-bottom caption-effect-fade" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/the-great-ocean-road-beach.jpg" alt="大洋路上的海滩"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/the-great-ocean-road-beach.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>大洋路上的海滩</p>
      </figcaption>
  </figure>
</div>

<p>一路上有十二门徒石 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> 和阿德湖峡 <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> 等自然景点。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/loch-ard-gorge.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/loch-ard-gorge.jpg" alt="阿德湖峡"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/loch-ard-gorge.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>阿德湖峡</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/the-twelve-apostles.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/the-twelve-apostles.jpg" alt="十二门徒石"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/the-twelve-apostles.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>十二门徒石</p>
      </figcaption>
  </figure>
</div>


</div>

<p>战争永远是残酷的，伤痛是无法消除的，我们能做的就是尊重这些为了国家奉献过的人们，缅怀逝去的，照顾残留的，这条有一战老兵参与修建的大洋路就是我们最好的纪念。去参观战争纪念馆的时候，恰巧碰上了一场纪念会，到的时候差不多结束了，不是太清楚具体是在纪念哪场战役，只远远的看到年轻的士兵持枪肃立，年老的士兵时不时的留下泪水。一瞬间想到了两段话，一段是说人的死亡分为三次，断气时，下葬时和被遗忘时，庆幸的是我们并没有选择忘记，至少现在还没有；另一段是麦克阿瑟的经典演讲，老兵不死，只是慢慢凋零。</p>


<div class="gallery tiles-2 caption-position-bottom caption-effect-fade hover-effect-zoom hover-transition" itemscope itemtype="http://schema.org/ImageGallery">
	  

<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/shrine-of-remembrance-commemoration.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/shrine-of-remembrance-commemoration.jpg" alt="战争纪念馆前的纪念活动"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/shrine-of-remembrance-commemoration.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>战争纪念馆前的纪念活动</p>
      </figcaption>
  </figure>
</div>


<div class="box" >
  <figure class="photoswipe-figure" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
    <div class="img" style="background-image: url('/images/cn/2017-08-26-trip-to-melbourne/shrine-of-remembrance.jpg');">
      <img itemprop="thumbnail" src="/images/cn/2017-08-26-trip-to-melbourne/shrine-of-remembrance.jpg" alt="战争纪念馆"/>
    </div>
    <a href="/images/cn/2017-08-26-trip-to-melbourne/shrine-of-remembrance.jpg" itemprop="contentUrl"></a>
      <figcaption>
          <p>战争纪念馆</p>
      </figcaption>
  </figure>
</div>


</div>

<p>最近恰巧看了二十二 <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> 和三十二两部纪录片，战争受难最多的终究还是百姓，不过越来越多的历史开始慢慢淡出人们的视野，但也有着那么一群人再帮着我们更好的铭记。其实，我们铭记不是说为了让我们去记恨一些人，去记恨一个民族，我们所需要让人们铭记的是战争本身的残酷，愿天下之安宁，以活民命。</p>


  


<script src="/js/load-photoswipe.js"></script>


<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.css'/>
<link rel="stylesheet" href='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/default-skin/default-skin.min.css'/>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js'></script>
<script src='//cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js'></script>


<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

<div class="pswp__bg"></div>

<div class="pswp__scroll-wrap">
    
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    </div>
    
    <div class="pswp__ui pswp__ui--hidden">
    <div class="pswp__top-bar">
      
      <div class="pswp__counter"></div>
      <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
      <button class="pswp__button pswp__button--share" title="Share"></button>
      <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
      <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
      
      
      <div class="pswp__preloader">
        <div class="pswp__preloader__icn">
          <div class="pswp__preloader__cut">
            <div class="pswp__preloader__donut"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
      <div class="pswp__share-tooltip"></div>
    </div>
    <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
    </button>
    <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
    </button>
    <div class="pswp__caption">
      <div class="pswp__caption__center"></div>
    </div>
    </div>
    </div>
</div>

<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://zh.wikipedia.org/zh-hans/%E5%A4%A7%E6%B4%8B%E8%B7%AF">https://zh.wikipedia.org/zh-hans/大洋路</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://zh.wikipedia.org/zh-hans/%E5%8D%81%E4%BA%8C%E4%BD%BF%E5%BE%92%E5%B2%A9">https://zh.wikipedia.org/zh-hans/十二使徒岩</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://zh.wikipedia.org/zh-hans/%E9%98%BF%E5%BE%B7%E6%B9%96%E5%B3%A1">https://zh.wikipedia.org/zh-hans/阿德湖峡</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://zh.wikipedia.org/zh-hans/%E4%BA%8C%E5%8D%81%E4%BA%8C_(2017%E5%B9%B4%E7%94%B5%E5%BD%B1)">https://zh.wikipedia.org/zh-hans/二十二_(2017年电影)</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        ]]></description></item></channel></rss>