<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>小生很忙</title>
    <link>https://chaoge123456.github.io/</link>
    <atom:link href="/rss2.xml" rel="self" type="application/rss+xml"/>
    
    <description>网络安全和机器学习相关技术分享</description>
    <pubDate>Sat, 28 Aug 2021 11:53:24 GMT</pubDate>
    <generator>http://hexo.io/</generator>
    
    <item>
      <title>区块链隐私与门罗币</title>
      <link>https://chaoge123456.github.io/%E5%8C%BA%E5%9D%97%E9%93%BE%E9%9A%90%E7%A7%81%E4%B8%8E%E9%97%A8%E7%BD%97%E5%B8%81.html/</link>
      <guid>https://chaoge123456.github.io/%E5%8C%BA%E5%9D%97%E9%93%BE%E9%9A%90%E7%A7%81%E4%B8%8E%E9%97%A8%E7%BD%97%E5%B8%81.html/</guid>
      <pubDate>Sat, 28 Aug 2021 11:32:49 GMT</pubDate>
      <description>
      
        
        
          &lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt;最近接触了一些与区块链相关的知识，主要与区块链隐私相关，感觉挺有趣，在此记录一下。本文主要梳理了区块链系统的隐私问题，并以门罗币为例，阐述了交易匿名化的解决方案。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://githubpage
        
      
      </description>
      
      <content:encoded><![CDATA[<p><strong>摘要：</strong>最近接触了一些与区块链相关的知识，主要与区块链隐私相关，感觉挺有趣，在此记录一下。本文主要梳理了区块链系统的隐私问题，并以门罗币为例，阐述了交易匿名化的解决方案。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/blockchain/monero.png" style="zoom:150%;"></p><hr><h3 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h3><ul><li>区块链系统概述</li><li>区块链隐私问题</li><li>门罗币隐私保护机制</li></ul><hr><h4 id="区块链系统概述"><a href="#区块链系统概述" class="headerlink" title="区块链系统概述"></a>区块链系统概述</h4><p>&emsp;&emsp;区块链本质上相当于一个去中心化的账本数据库，以P2P网络作为通信载体，依赖密码学确定所有权和保障隐私。相对于传统的数据库，其核心特征是不可篡改。实现区块链需要解决的两个关键问题是：</p><ul><li><strong>如何组织数据以确保不被篡改，即数据结构。</strong>这里的数据结构有两层含义，一层含义是指区块内部的交易数据的组织形式，常见的组织形式包括Merkle树、Merkle Patircia树等；另一层含义是指区块与区块之间的组织形式，常见的组织形式包括链式结构（比特币）、树状结构（以太坊）、DAG图结构（IOTA）。</li><li><strong>如何在分布式环境中对账本状态的更新达成共识，即共识机制。</strong>共识机制使得参与区块链系统的各个节点都可以参与到记账权的竞争当中，获得记账权的节点将会获得相应的奖励，同时系统中的节点都可以对新加入链中的区块进行验证。常见的共识机制包括Proof of work、Proof of stake等。</li></ul><p>&emsp;&emsp;以比特币为例，整个区块链系统的运行机制可以简述为：首先客户端发起一笔交易，将该交易广播至比特币网络中的任意节点，节点在收到交易后需要验证交易是否合法。如果验证不通过，节点将拒绝该交易，并向发送者返回交易被拒绝的消息；如果验证通过，节点会将该交易放入自己的交易池中，并向网络中继续传播。各节点从各自的交易池中打包交易，并通过加入随机数进行计算。最先计算出符合要求哈希值的节点打包的区块有效，即获得记账权。然后，该节点将计算得到的区块广播到区块链网络中，其他节点接收到新的区块后会进行验证。验证成功后将新区块连接到自己的链中，同时删除自己交易池中已经被打包的交易记录，重新开始新一轮的生产区块过程。</p><hr><h4 id="区块链隐私问题"><a href="#区块链隐私问题" class="headerlink" title="区块链隐私问题"></a>区块链隐私问题</h4><p>&emsp;&emsp;区块链系统中的隐私一般是指用户的身份隐私、位置隐私以及交易内容隐私，我认为造成区块链系统中的隐私问题的原因主要有三个：</p><ul><li>为了鼓励更多用户参与共识，早期的区块链系统不会对加入的节点进行认证，这就为攻击者窃取隐私数据创造了机会。攻击者可以共享系统中的交易数据、嗅探网络流量，从而挖掘出关于用户的敏感信息。</li><li>为了增强区块链系统的拓展性，通常将其部署在开放网络环境当中。这就使得攻击者可以任意部署节点,监听网络中各节点隐私信息以及网络通信信息。相关研究表明，攻击者可以对区块链地址及 IP 地址对应关系进行分析，揭露用户与实际物理位置的对应关系。</li><li>为了更加高效的对交易记录进行验证，区块链系统中的交易记录通常是完全公开的，没有采取额外的保护措施。交易记录通常能够反映一些敏感信息，有可能泄露用户的隐私。例如用户购物的交易记录能够反映用户的消费水平、生活状态等。</li></ul><p>&emsp;&emsp;为了解决区块链系统中的隐私问题，以下从三个角度来对当前的防御机制进行总结：</p><ul><li><p>对应于导致隐私问题的第一个原因，我们可以通过在区块链系统中加入节点认证机制限制恶意节点的加入。基于这一想法，研究人员提出了私有链和联盟链的架构，这些架构可以适用于对隐私需求更高的一些场景。（应用案例：Hyperledger Fabric）</p></li><li><p>对应于导致隐私问题的第二个原因，我们可以将区块链系统运行在具有隐私保护特性的网络上，例如Tor网络以及I2P网络。这些网络通过多层加密以及特殊的路由转发技术可以能够隐藏用户IP，防止通过网络层信息实现交易溯源。（应用案例：门罗币）</p></li><li><p>对应于导致隐私问题的第三个原因，我们可以在满足区块链正常运行的基础上 ，防止恶意节点获得准确的交易数据。这类技术一般包括以下三类：</p><ul><li>基于数据失真的技术：通过将交易内容的部分数据进行混淆 ，使攻击者无法获得准确的数据 ，增加分析难度。这种方案的难点在于不破坏交易结果的条件下 ，防止攻击者发现不同地址之间的交易关系，一般称为混币机制。（去中心化的混币方法CoinJoin）</li><li>基于数据加密的技术：通过将交易信息加密，使攻击者无法获得具体的交易信息，从而无法开展分析，这种方案的难点在于实现加密的同时，必须保证原有的验证机制不受影响。（门罗币，Zcash）</li><li>基于限制发布的技术：通过发布少量或者不发布交易数据，减少攻击者能够获得的交易数据 ，增加分析难度。（闪电网络）</li></ul></li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/blockchain/13.JPG" alt=""></p><hr><h4 id="门罗币隐私保护机制"><a href="#门罗币隐私保护机制" class="headerlink" title="门罗币隐私保护机制"></a>门罗币隐私保护机制</h4><p>&emsp;&emsp;以下将以比特币为例，探讨区块链中交易内容隐私保护方法和策略。比特币采用UTXO的方式来对交易内容进行记录，交易记录如下图所示。在交易记录中包含发送方引用的UTXO、发送方的公钥、交易金额、接收方的地址以及发送方对交易内容的签名。发送发生成交易记录之后向其他节点广播，节点收到交易记录之后对其进行验证，主要验证以下内容：</p><ul><li>每个被引用的输入必须有效，且未被使用过；</li><li>交易的签名必须与每笔输入的所有者签名匹配；</li><li>输入的总值必须等于或大于输出的总值。</li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/blockchain/7.JPG" alt=""></p><p>&emsp;&emsp;在比特币系统中，由于所有的交易记录都是公开的，所以节点可以高效的对交易内容进行验证，从而保证系统的正常运转。如果我们希望提高比特币系统的隐私性，势必要隐藏交易内容中的相关信息，首当其冲面临的问题就是如何实现隐私场景下的交易验证。接下来将以门罗币为例，讲解如何实现交易内容隐私保护以及如何实现隐私场景下的交易验证。</p><p>&emsp;&emsp;门罗币（Monero）是一种领先的密码学货币，聚焦交易隐私安全。在一笔交易中主要涉及的隐私信息包括发送方、接收方、交易金额，门罗币使用了一系列密码学方法来隐藏所有隐私细节并阻止任何区块链跟踪。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/blockchain/ringCT.JPG" style="zoom:50%;"></p><ul><li><strong>隐藏交易金额：</strong>对于大多数密码学货币，交易数额以明文形式发送，任何人都可以看到。门罗币利用RingCT技术来隐藏任何交易中发送的金额。RingCT通过允许发送者证明自己有足够的金额来进行交易，而不会透露该金额的值，从而使得此敏感信息保密。这都要归功于密码学中的承诺和范围证明。当发送方发起交易时，将以一种秘密方式“承诺”该金额，仅向网络披露足够的信息以确认交易的合法性，但不会公开揭露金额本身。一个有效的承诺保证了交易不会欺骗性地创建或超支已有的金额。</li><li><strong>隐藏发送方：</strong>在比特币交易记录中，发送者需要利用自己的私钥对交易进行签名，同时给出自己的公钥方便其他节点来对交易进行验证。因为交易记录中包含发送者的公钥，所以能够很方便的对发送者的交易行为进行追踪。如果我们希望隐藏发送者的身份，那就不能在交易记录中给出公钥。门罗币使用<strong>环签名</strong>来解决这一问题。在门罗币交易过程中，发送者随机选择几个用户的公钥，并将自己的公钥混入其中，构成一个集合（签名者在签名时无需集合中其他成员的协作，甚至于可以不让其他成员知晓）。发送者利用自己的私钥进行签名，同时给出集合中的公钥。当节点对该交易进行验证时，唯一知道的是该签名来自集合成员，但是无法区分是某个具体成员，从而实现了发送方的匿名性。</li><li><strong>隐藏接收方：</strong>在比特币交易记录中，需要指定接收方的地址。通过对接收方的地址进行追踪，可以获得关于接收方的隐私信息。为了实现对接收方的隐私保护，门罗币使用隐形地址进行交易。每笔交易都被发送到唯一的一次性地址上，接收方可以访问发送到隐形地址的资金，而不暴露其钱包公开地址或其他交易的任何关联。</li></ul><p>&emsp;&emsp;由于在交易记录中隐藏了交易金额、发送方、接收方，这就导致节点无法对交易内容进行验证。针对上述提到的三个验证问题，门罗币提供了以下解决方案：</p><ul><li>在交易过程中，发送方利用环签名技术来隐藏发送方的身份。发送方利用自身的私钥对交易进行签名，验证节点可以通过集合中的公钥对其进行验证。验证通过即证实了发送方对UTXO的所有权，因此引用的UTXO是有效的。那如何证明该UTXO未被使用过呢？门罗币通过利用每次交易生成和记录的<strong>密钥镜像</strong>来解决这一问题，这些密钥镜像是唯一地从所花费的实际输出中得出的。网络无法确定哪个环成员连接到密钥镜像，但是其他参与者只需要检查密钥镜像是否已被使用过。如果恶意用户尝试花费两次相同的输出，它将两次生成相同的密钥镜像，并且网络会立即拒绝欺诈性的第二笔交易。</li><li>环签名技术可以确保交易的签名必须与每笔输入的所有者签名匹配。</li><li>RingCT技术利用密码学中的承诺和范围证明，将以一种秘密方式“承诺”该金额，仅向网络披露足够的信息以确认交易的合法性，但不会公开揭露金额本身。</li></ul><p>&emsp;&emsp;除此之外，门罗币鼓励用户通过Tor、I2P等匿名网络接入到门罗币系统中，从而隐藏用户的IP地址，进一步增强其隐私性。</p>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/%E5%8C%BA%E5%9D%97%E9%93%BE%E9%9A%90%E7%A7%81%E4%B8%8E%E9%97%A8%E7%BD%97%E5%B8%81.html/#disqus_thread</comments>
    </item>
    
    <item>
      <title>开发环境配置</title>
      <link>https://chaoge123456.github.io/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.html/</link>
      <guid>https://chaoge123456.github.io/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.html/</guid>
      <pubDate>Fri, 13 Aug 2021 15:00:03 GMT</pubDate>
      <description>
      
        
        
          &lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt;最近实验室给配了一台新设备，这几天一直在忙着装系统、搭环境。虽然这装系统、搭环境也不是啥难事，但是零零碎碎的细节很是恼人，在此记录一下我在这个过程中遇到的一些问题，供大家参考。同时，也给大家分享一些有趣的应用和工具。&lt;/p&gt;
&lt;p&gt;&lt;
        
      
      </description>
      
      <content:encoded><![CDATA[<p><strong>摘要：</strong>最近实验室给配了一台新设备，这几天一直在忙着装系统、搭环境。虽然这装系统、搭环境也不是啥难事，但是零零碎碎的细节很是恼人，在此记录一下我在这个过程中遇到的一些问题，供大家参考。同时，也给大家分享一些有趣的应用和工具。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/images/ubuntu.png" alt="i3wm"></p><hr><h2 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h2><ul><li>系统选择</li><li>应用分享</li></ul><hr><h3 id="系统选择"><a href="#系统选择" class="headerlink" title="系统选择"></a>系统选择</h3><p>&emsp;&emsp;对于开发者而言，Linux系统拥有众多优秀的性能，我也一直尝试着将Linux作为主力使用的操作系统。但是，目前很多痛点问题在Linux场景下没有很好的解决方案，比如很多常用的软件没有Linux版本，因此之前我的电脑使用的是win10和Arch Linux双系统，在做不同的任务时可能需要在两个系统之间进行切换。之所以选择Arch Linux是因为Arch Linux拥有非常丰富的软件库，结合wine，能够很好的解决部分场景的痛点问题。但是Arch Linux的更新策略很激进，滚动更新的模式可能会让你的系统出现各种类型的bug。在平稳运行半年之后，我的Arch还是挂了，推测应该是显卡驱动的原因，一直没能修复好，因此只得作罢。之后，我又回归了windows系统。在windows场景下搭建Linux开发环境一般有三种方式：虚拟机、docker、WSL，前两种方式之前都尝试过，也不太符合我的需求，因此这次选择了WSL。我将所有的开发环境部署在WSL中，IDE安装在windows宿主机，IDE可以识别WSL中的编译环境。使用一段时间后，体验非常好，WSL能够满足我在开发场景中所有的需求，在性能方面也接近原生Linux。</p><p>&emsp;&emsp;因此，我准备在新机器上继续沿用windows+WSL的环境。由于新配的机器有一块nvidia的显卡，所以在装系统之前，我查阅了以下相关资料，WSL2已经支持显卡接口调用。但是，我忽略了一个关键细节：<strong>只有Build 20145或更高预览版的win10下的WSL2才支持显卡调用</strong>。装好系统之后，我才发现这一问题，所以装完之后又将系统升级到最新的预览版。接着需要在windows宿主机上安装显卡驱动程序，根据<a href="https://docs.nvidia.com/cuda/wsl-user-guide/index.html" target="_blank" rel="noopener">官网</a>的操作一路安装下去，遇到了很多bug，虽然最后也算是装好了，但是总感觉不够稳定。这些功能目前还在测试阶段，所以不推荐作为主力开发环境。win11已经支持上述功能，等正式发布之后，可以再尝试上述功能。</p><p>&emsp;&emsp;以防万一，我还是安装了一套Linux环境，这次使用的是ubuntu。ubuntu的社区很强大，各种资料文档也十分丰富，一旦出现问题，可以找到很多解决方案进行参考。双系统的安装过程这里就不赘述了，网上都能找到很多参考资料。接下来，介绍几个我在Linux系统中经常使用的软件工具。</p><hr><h3 id="应用分享"><a href="#应用分享" class="headerlink" title="应用分享"></a>应用分享</h3><p><strong>i3wm：</strong>linux 平铺式窗口管理器，强力推荐。</p><p><strong>zsh：</strong>功能强大且美观的Linux终端，安装过程可以参考这篇<a href="https://segmentfault.com/a/1190000015283092" target="_blank" rel="noopener">博客</a>。</p><p><strong>ranger：</strong>是一款终端下的文件管理器，具有vim式的操作方式。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install ranger # ubuntu下安装ranger</span><br><span class="line"><span class="meta">$</span><span class="bash"> sudo apt-get install highlight  <span class="comment"># 代码高亮</span></span></span><br><span class="line"><span class="meta">$</span><span class="bash"> sudo apt-get install w3m        <span class="comment"># html页面预览</span></span></span><br><span class="line">ranger --copy-config=all # 将在~/.config/ranger中生成一系列配置文件</span><br><span class="line"><span class="meta">#</span><span class="bash"> rc.conf 常用于设置选项和绑定快捷键.</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> scope.sh 常用于设置文件的预览方式.</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> rifle.conf 常用于设置使用那个软件来打开文件.</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> commands.py python文件,增强和改进ranger的各种功能.</span></span><br></pre></td></tr></table></figure><p>通过对ranger的配置文件进行修改，可以实现很多方便快捷的功能，以下介绍如何对pdf和image进行预览。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 在scope.sh文件中取消这段内容的注释，即可在ranger中预览pdf</span></span><br><span class="line">application/pdf)</span><br><span class="line">    pdftoppm -f 1 -l 1 \</span><br><span class="line">       -scale-to-x "$&#123;DEFAULT_SIZE%x*&#125;" \</span><br><span class="line">       -scale-to-y -1 \</span><br><span class="line">       -singlefile \</span><br><span class="line">       -jpeg -tiffcompression jpeg \</span><br><span class="line">       -- "$&#123;FILE_PATH&#125;" "$&#123;IMAGE_CACHE_PATH%.*&#125;" \</span><br><span class="line">    &amp;&amp; exit 6 || exit 1;;</span><br></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 在rc.conf文件中添加以下内容，即可在ranger中预览图片</span></span><br><span class="line">set preview_images true</span><br><span class="line">set preview_images_method w3m</span><br></pre></td></tr></table></figure><p><strong>flameshot：</strong>好用的截图工具</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install flameshot</span><br></pre></td></tr></table></figure><p><strong>proxychains：</strong>强大的终端代理工具</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install proxychains</span><br><span class="line"><span class="meta">#</span><span class="bash"> 配置文件的路径为/etc/proxychains.conf，注释原本的socks4代理配置，添加socks5代理</span></span><br><span class="line">socks5         127.0.0.1 1080</span><br></pre></td></tr></table></figure><p><strong>frp：</strong>有一台云服务器，为了随时对主机进行访问，需要配置内网穿透。frp是一款优秀的内网穿透工具，以下给出ssh服务的配置。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 客户端frpc.ini文件配置</span></span><br><span class="line">[common]</span><br><span class="line">server_addr = 82.58.156.32 # 自己的服务器端IP</span><br><span class="line">server_port = 7000 # 服务器端frp端口</span><br><span class="line">token = 12345678 # 连接口令</span><br><span class="line"></span><br><span class="line">[ssh]</span><br><span class="line">type = tcp</span><br><span class="line">local_ip = 127.0.0.1</span><br><span class="line">local_port = 22</span><br><span class="line">remote_port = 2222</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 服务器端frps.ini文件配置</span></span><br><span class="line">[common]</span><br><span class="line">bind_port = 7000</span><br><span class="line">dashboard_port = 7500 # 监控面板</span><br><span class="line">token = 12345678</span><br><span class="line">dashboard_user = admin # 监控面板用户名</span><br><span class="line">dashboard_pwd = admin # 监控面板密码</span><br></pre></td></tr></table></figure><p><strong>Filegator：</strong>轻量级的开源网盘，功能比较单一（nextcloud的功能更加复杂），能够满足我的日常需求，推荐使用docker方式进行安装。</p><p><strong>bashtop：</strong>终端系统监控软件</p><p><strong>nvtop：</strong>终端显卡监控软件</p>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.html/#disqus_thread</comments>
    </item>
    
    <item>
      <title>口令安全问题研究</title>
      <link>https://chaoge123456.github.io/%E5%8F%A3%E4%BB%A4%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98%E7%A0%94%E7%A9%B6.html/</link>
      <guid>https://chaoge123456.github.io/%E5%8F%A3%E4%BB%A4%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98%E7%A0%94%E7%A9%B6.html/</guid>
      <pubDate>Sun, 04 Jul 2021 12:58:49 GMT</pubDate>
      <description>
      
        
        
          &lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt;身份认证是确保信息系统安全的第一道防线，口令是应用最为广泛的身份认证方法。尽管口令存在众多的安全性和可用性缺陷，大量的新型认证技术陆续被提出，但由于口令具有简单易用、成本低廉、容易更改等特性，在可预见的未来仍将是最主要的认证方法。之前
        
      
      </description>
      
      <content:encoded><![CDATA[<p><strong>摘要：</strong>身份认证是确保信息系统安全的第一道防线，口令是应用最为广泛的身份认证方法。尽管口令存在众多的安全性和可用性缺陷，大量的新型认证技术陆续被提出，但由于口令具有简单易用、成本低廉、容易更改等特性，在可预见的未来仍将是最主要的认证方法。之前阅读了一些该领域的文章，也复现了一些常见的口令猜测算法，该博文进行一些总结。</p><h2 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h2><ul><li>口令安全问题分析</li><li>PCFG口令猜测算法实现</li><li>Markov口令猜测算法实现</li></ul><hr><h3 id="口令安全问题分析"><a href="#口令安全问题分析" class="headerlink" title="口令安全问题分析"></a>口令安全问题分析</h3><p>&emsp;&emsp;对于一个安全问题，往往可以从攻击者和防御者两个角度来考虑，以下分别从这两个角度对口令安全问题进行分析和总结。</p><h4 id="攻击者"><a href="#攻击者" class="headerlink" title="攻击者"></a>攻击者</h4><p>&emsp;&emsp;对于口令安全问题而言，攻击者的目的是利用各种手段快速方便的获取用户的口令信息，从而获得进入相关系统的权限。首先，攻击者需要对该问题进行分析，找到突破口。对口令安全造成威胁的因素主要有以下两种：</p><ul><li>系统（或网络应用）的口令安全机制，主要体现在以下几个方面。<ul><li><strong>口令生成策略：</strong>目前很多网站的口令生成策略一般要求字母+数字+特殊字符的组合，口令长度8位以上。如果网站的口令生成策略要求很低，会导致用户倾向于选择更为简单、便于记忆的口令组合，会加剧口令被攻击者成功猜测的风险。</li><li><strong>口令强度评估：</strong>良好的口令强度评估机制可以帮助用户选择更为安全的口令，增加攻击者猜测的成本。但是出于技术难度或者用户体验等方面的考虑，目前的口令强度评估机制仍然存在很大的问题。</li><li><strong>口令认证机制：</strong>由于口令传输阶段不涉及人的因素，研究的问题主要是基于口令的身份认证协议，问题容易刻画，目前这方面的研究较为成熟，攻击者很难从这个角度来突破。</li><li><strong>口令存储机制：</strong>如果系统采用明文方式存储用户口令，一旦口令文件被泄漏，会造成很大的安全威胁。一般情况下，采用较为先进的hash函数来生成口令文件。hash函数的单向性和碰撞约束特性能够保证即使口令文件被泄漏，攻击者也难以恢复用户的口令。</li></ul></li><li>用户脆弱口令行为，主要体现在以下几个方面。<ul><li><strong>用户的倾向性口令构造模式：</strong>用户倾向于用特定的模式来构造口令，这种构造模式可能与其所处文化环境、语言特点有关，并且具有群体特征。这些线索可以帮助攻击者缩小口令猜测范围，提高攻击效率。</li><li><strong>口令重用：</strong>面对大量需要管理的帐号，口令重用是很多用户常见的做法。口令重用可以使得攻击者在获得已知用户口令的情况下，提取用户口令构造特征，从而推测用户未知口令。</li><li><strong>基于个人信息构造口令：</strong>为了便于记忆，用户在构造口令时往往会掺入个人相关信息。攻击者可以结合已知的用户信息和相关口令构造模型来猜测用户口令。</li></ul></li></ul><p>&emsp;&emsp;通过以上分析可知，用户脆弱口令行为是造成口令安全风险的主要原因，同时系统（或网络应用）中脆弱的口令生成策略和口令强度评估机制也为攻击者成功攻击创造了条件。其实，口令猜测攻击的思路很简单，即提交不同的口令直到认证成功为止。但是，其难点在于如何提高攻击的成功率和效率。因此，口令猜测算法的研究专注于深入挖掘用户脆弱口令行为，针对不同场景构建高效的口令猜测算法。根据攻击过程中是否利用用户个人信息，口令猜测算法可分为漫步攻击和定向攻击。</p><ul><li>漫步攻击是指攻击者不关心具体攻击对象是谁，其唯一目标是在允许的猜测次数下，猜出越多口令越好。<ul><li><strong>启发式方法：</strong>没有严密的理论体系，很大程度依靠零散的“奇思妙想”，比如构造独特的猜测字典，采用精心设计的猜测顺序等。</li><li><strong>概率统计方法：</strong>基于概率统计思想，对口令的组成结构进行分析，从而得到概率模型，比如概率上下文无关方法（PCFG）、Markov、NLP。</li><li><strong>深度学习方法：</strong>核心思想是利用深度学习模型强大的端到端的学习能力，自动提取口令集合潜在的分布特征，从而指导猜测口令的生成，比如PassGAN、FLA。</li></ul></li><li>定向攻击是指攻击者利用与攻击相关的个人信息以增强口令猜测的针对性。<ul><li><strong>口令重用信息：</strong>利用用户在其他网站中泄漏的口令来推测用户口令构造特征，这类攻击方法包括TarGuess、pass2path。</li><li><strong>人口学相关信息：</strong>这类信息包括用户的姓名、年龄、生日、性别、邮箱、手机号以及家庭成员信息，这类攻击方法包括Targeted-Markov、Personal-PCFG、TarGues</li></ul></li></ul><p>&emsp;&emsp;总的来说，口令猜测算法的发展趋势是由漫步攻击慢慢过度到定向攻击，因为定向攻击的更有针对性且效率更高。除此之外，利用深度学习方法来进行口令猜测也是未来的发展趋势之一。因为传统的口令猜测算法大多建立在特定规则和概率统计的基础上，可能无法全面的体现口令的分布特征，深度学习模型强大的表示能力为解决这一问题提供了可能性。</p><h4 id="防御者"><a href="#防御者" class="headerlink" title="防御者"></a>防御者</h4><p>&emsp;&emsp;从攻击者的分析来看，造成口令安全风险的主要原因是用户的安全意识不足，其次是由于系统（或网络应用）的口令安全策略不严格造成的。由于用户的安全意识等人为因素是无法控制的，所以防御者构建口令安全防御机制主要是立足于口令强度评估等安全策略。口令强度评估机制需要准确的告诉用户，当前构造的口令是否安全，所以这就需要口令强度评估机制足够的健壮。目前的口令强度评估机制主要有以下三类：</p><ul><li><strong>基于规则的口令强度评价方法：</strong>口令强度依据长度和所包含的字符类型而定，比如NIST PSM。</li><li><strong>基于模式检测的口令强度评估方法：</strong>检测口令各个子段所属的构造模式，然后对各个模式赋予相应的分数，得到该口令的总分数，即为强度值，比如zxcvbn。</li><li><strong>基于攻击算法的口令强度评估方法：</strong>使用攻击算法对给定口令进行攻击，根据攻击的难易程度进行强度判定，比如PCFG-based PSM、Markovbased PSM。</li><li><strong>基于相似度检测的口令强度评估方法：</strong>vec-PPSM（口令重用）。</li></ul><p>&emsp;&emsp;目前的口令强度评估的研究相对来说不是很成熟，提出的很多评估方法都没能得到大规模的应用。目前大部分的口令强度评估机制都是基于漫步猜测攻击，未考虑到用户个人信息对口令安全性的影响，这一领域值得探索的方向是设计基于定向攻击者模型的口令强度评估机制。</p><hr><h3 id="PCFG口令猜测算法实现"><a href="#PCFG口令猜测算法实现" class="headerlink" title="PCFG口令猜测算法实现"></a>PCFG口令猜测算法实现</h3><p>&emsp;&emsp;PCFG是一种完全自动的、建立在严密的概率上下文无关法基础之上的漫步口令猜测算法，该算法的核心思想是将每一条口令看作是由字母段L、数字段D、特殊字符段S根据一定的模式互相组合而成的。我们可以通过对大量的口令数据进行分析，统计出这些口令可能的组合模式，从而利用该模式生成更多的口令，进行口令猜测。</p><h4 id="实现细节"><a href="#实现细节" class="headerlink" title="实现细节"></a>实现细节</h4><p>&emsp;&emsp;该算法的实现主要包括三个部分：口令集预处理、口令集训练、口令猜测，使用的编程语言为python3.7，具体细节如下所示：</p><ul><li><strong>口令集预处理：</strong>实验中所使用的口令集为MySpace，去掉了包含非ASCLL或者空格的口令，剩余口令总数为41251。然后将这些口令随机拆分为训练集和测试集，分别存放在trainword.txt和testword.txt文件中。代码中设置了参数eps可以对训练集和测试集的分配比例进行调节，本文将eps设置为0.4，即测试集占口令总数的40%（16500）。除此之外，在随机拆分训练集和测试集时，设置了随机种子seed，方便实验结果的复现。</li><li><strong>口令集训练：</strong>口令集训练的目的是统计出口令模式频率和字符组件频率。对于字符组件频率的统计，论文中提到的方法只统计了数字段和D和特殊S。在实现过程中，我也统计了字母段L的频率，同时字母段L将作为字典参与口令猜测过程。（根据测试集生成的口令模式一共有1134种）</li><li><strong>口令猜测：</strong>口令猜测的过程类似于树的遍历，这里采用类似于深度优先遍历的方法。分别对所有的口令模式频率表和字符组件频率表由大到小进行排序，从频率最高的口令模式开始进行口令猜测，根据字符组件频率表依次生成该模式下所有可能的口令，然后在进行下一个口令模式的猜测。对于每个生成的口令，需要计算其可能出现的概率，如果该概率值大于预设的阈值（比如0.000000001），则可以将其输出并与测试集中的口令进行比对。反之，则将该口令丢弃。实验中所有的猜测口令都存放在guess文件夹下对应的口令模式文件中。</li></ul><h4 id="实验结果"><a href="#实验结果" class="headerlink" title="实验结果"></a>实验结果</h4><p>&emsp;&emsp;按照上述实验设置进行了实验，具体代码参考我的<a href="https://github.com/chaoge123456/passwordguess" target="_blank" rel="noopener">GitHub</a>。在代码刚开始运行阶段，正确猜解口令的速度很快。两分钟的时间内大概能够正确猜解6000多个口令，之后正确猜解的速度逐渐降低，下图展示了口令破解速度的变化趋势。由于⽣成口令时我们设置了阈值，只有超过该阈值的口令才会进⾏匹配，所以真正参与口令猜测的 口令数量要远远小于实际⽣成的口令数量，上图横坐标展⽰的就是参与口令猜测的口令数量。这⾥展⽰ 的结果是利⽤口令模式表中前400种左右的模式⽣成的口令，正确猜解的口令数量为6529（总共 16500）</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/passwordguess/1.JPG" alt="PCFG" style="zoom:80%;"></p><hr><h3 id="Markov口令猜测算法实现"><a href="#Markov口令猜测算法实现" class="headerlink" title="Markov口令猜测算法实现"></a>Markov口令猜测算法实现</h3><p>&emsp;&emsp;⼀般情况下，⽤⼾构造口令的顺序是从前向后依次进⾏的。根据这⼀特点，Narayanan等⼈在 2005年⾸次将Markov链技术引⼊口令猜测中来。与PCFG算法不同，基于Markov模型的口令猜测算法 对整个口令进⾏训练，通过从左到右的字符之间的联系来计算口令的概率。该算法也分为训练和猜测集 ⽣成两个阶段，以下介绍实现细节。（针对此前代码存在的问题，本次代码进⾏了相应的修改。对于 start symbol问题，已经充分理解并实现；对于阈值问题，优化了阈值的分配⽅式。综合来看，实验效果得到了很⼤的提升）</p><h4 id="实现细节-1"><a href="#实现细节-1" class="headerlink" title="实现细节"></a>实现细节</h4><p>&emsp;&emsp;该算法的实现主要包括三个部分：口令集预处理、口令集训练、口令猜测，使⽤的编程语⾔为 python3.7，具体细节如下所⽰：</p><ul><li>口令集预处理：实验中所使⽤的口令集为Rockyou，去掉了包含⾮ASCLL或者空格的口令，剩余口 令总数为32460357。由于口令总数很⼤，这⾥我没有使⽤全部的口令进⾏训练和测试。我从数据 集中随机选择了2000000条口令，然后将这些口令拆分为训练集和测试集（各占50%）。代码中设 置了number参数，可以选择使⽤数据的量。除此之外，在随机选择数据时，设置了随机种⼦seed，⽅便实验结果的复现。</li><li>口令集训练：口令集训练的⽬的是统计出各个字串在训练集中出现的频数。统计频数时，对于口令 开头字⺟出现的频率单独进⾏统计，其余字串的频数均存放在⼀个字典中。我使⽤了End-Symbol 正规化技术，频数统计时也会对口令结束标志符号进⾏统计。频数统计完成之后，利⽤Laplace平 滑技术来计算概率，然后对每个字串后⾯出现的字⺟依据概率值⼤小进⾏排序。</li><li>口令集猜测：这⾥使⽤优先队列的思想来对猜测口令进⾏存储和遍历，如果当前队列前端的字符串 最后⼀个字符为口令结束标识符，则将其输出进⾏口令猜解，否则根据其字串在概率表中的统计情 况，在该字符串后继续添加字符并计算概率，然后插⼊队列。为了减少队列对内存的损耗，再将每 ⼀个字符串插⼊队列之前，要对其概率值进⾏判断。只有当其概率值⼤于预设阈值时，才准许插⼊队列。</li></ul><h4 id="实验结果-1"><a href="#实验结果-1" class="headerlink" title="实验结果"></a>实验结果</h4><p>&emsp;&emsp;按照上述实验设置进⾏了实验，具体代码参考我的<a href="https://github.com/chaoge123456/passwordguess" target="_blank" rel="noopener">GitHub</a>。由于实验机器性能有限，这⾥使⽤的测试集口令总数为 1000000，猜测次数为1000000，order=3,4,5，下图展⽰了口令破解速度的变化趋势。order=3时，猜测出的口令数⽬为279517；order=4时，猜测出的口令数⽬为378980；order=5时，猜测出的口令数 ⽬为414806。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/passwordguess/3.JPG" alt="3" style="zoom:100%;"></p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/passwordguess/4.JPG" alt="4"></p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/passwordguess/5.JPG" alt="5"></p>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/%E5%8F%A3%E4%BB%A4%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98%E7%A0%94%E7%A9%B6.html/#disqus_thread</comments>
    </item>
    
    <item>
      <title>生成对抗网络解读</title>
      <link>https://chaoge123456.github.io/%E7%94%9F%E6%88%90%E5%AF%B9%E6%8A%97%E7%BD%91%E7%BB%9C.html/</link>
      <guid>https://chaoge123456.github.io/%E7%94%9F%E6%88%90%E5%AF%B9%E6%8A%97%E7%BD%91%E7%BB%9C.html/</guid>
      <pubDate>Mon, 08 Feb 2021 06:13:28 GMT</pubDate>
      <description>
      
        
        
          &lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt;生成对抗网络（ Generative Adversarial Networks， GAN）是通过对抗训练的方式来使得生成网络产生的样本服从真实数据分布。在生成对抗网络中，有两个网络进行对抗训练。一个是判别器，目标是尽量准确地判断一个样
        
      
      </description>
      
      <content:encoded><![CDATA[<p><strong>摘要：</strong>生成对抗网络（ Generative Adversarial Networks， GAN）是通过对抗训练的方式来使得生成网络产生的样本服从真实数据分布。在生成对抗网络中，有两个网络进行对抗训练。一个是判别器，目标是尽量准确地判断一个样本是来自于真实数据还是由生成器产生；另一个是生成器，目标是尽量生成判别网络无法区分来源的样本  。两者交替训练，当判别器无法判断一个样本是真实数据还是生成数据时，生成器即达到收敛状态。以上是对生成对抗网络的简单描述，本文将对生成对抗网络的内在原理以及相应的优化机制进行介绍。</p><h2 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h2><ul><li>概率生成模型</li><li>生成对抗网络<ul><li>生成对抗网络的理论解释</li><li>生成对抗网络的求解过程</li></ul></li><li>生成对抗网络的优化<ul><li>fGAN</li><li>WGAN</li></ul></li><li>生成对抗网络的实现</li></ul><h3 id="概率生成模型"><a href="#概率生成模型" class="headerlink" title="概率生成模型"></a>概率生成模型</h3><p>&emsp;&emsp;概率生成模型，简称生成模型，是指一系列用于随机生成可观测数据的模型。假设在一个连续或离散的空间$\chi$中，存在一个随机向量$X$服从一个未知的数据分布$P_{data}(x)$，$x \in \chi$。生成模型是根据一些可观测的样本$x^1, x^2, …, x^m$来学习m一个参数化模型$P_G(\theta;x)$来近似未知分布，并可以用这$P_{data}(x)$个模型来生成一些样本，使得生成的样本和真实的样本尽可能的相似。对于一个低维空间中的简单分布而言，我们可以采用最大似然估计的方法来对$p_\theta(x)$进行求解。假设我们要统计全国人民的年均收入的分布情况，如果我们对每个$P_{data}(x)$样本都进行统计，这将消耗大量的人力物力。为了得到近似准确的收入分布情况，我们可以先假设其服从高斯分布，我们比如选取某个$P_G(x;\theta)$城市的人口收入$x^1, x^2, …, x^m$，作为我们的观察样本结果，然后通过最大似然估计来计算上述假设中的高斯分布的参数。<br>$$<br>L=\prod_{i=1}^{m} P_{G}\left(x^{i} ; \theta\right)<br>$$</p><p>$$<br>{\theta}^{*}=arg \max_{\theta} \sum_{i=1}^{m} \log P_{G}(x^{i} ; \theta)<br>$$</p><p>由于$P_G(x;\theta)$服从高斯分布，我们将其带入即可求得最终的近似的分布情况。下面我们对上述过程进行一些拓展，我们从$P_{data}(x)$尽可能采样更多的数据，此时可以得到<br>$$<br>{\theta}^{*}=arg \max_{\theta} \sum_{i=1}^{m} \log P_{G}(x^{i} ;\theta)\approx arg \max_{\theta} E_{x \sim P_{\text {data }}}[\log P_{G}(x ; \theta)]<br>$$</p><p>对该式进行一些变换，可以得到<br>$$<br>{\theta}^{*}=arg \min_{\theta} K L\left(P_{\text {data }} | P_{G}\right)<br>$$</p><p>&emsp;&emsp;由此可以看出，最大似然估计的过程其实就是最小化$P_{data}(x)$分布和$P_G $分布之间$KL$散度的过程。从本质上讲，所有的生成模型的问题都可以转换成最小化$P_{data}(x)$分布和$P_G $分布之间距离的问题，$KL$散度只是其中一种度量方式。</p><p>&emsp;&emsp;如上所述，对于低维空间的简单分布而言，我们可以显式的假设样本服从某种类型的分布，然后通过极大似然估计来进行求解。但是对于高维空间的复杂分布而言，我们无法假设样本的分布类型，因此无法采用极大似然估计来进行求解，生成对抗网络即属于这样一类生成模型。</p><h3 id="生成对抗网络"><a href="#生成对抗网络" class="headerlink" title="生成对抗网络"></a>生成对抗网络</h3><h4 id="生成对抗网络的理论解释"><a href="#生成对抗网络的理论解释" class="headerlink" title="生成对抗网络的理论解释"></a>生成对抗网络的理论解释</h4><p>&emsp;&emsp;在生成对抗网络中，我们假设低维空间中样本$z$服从标准类型分布，利用神经网络可以构造一个映射函数$G$（即生成器）将$z$映射到真实样本空间。我们希望映射函数$G$能够使得$P_G(x)$分布尽可能接近$P_{data}(x)$分布，即$P_G$与$P_{data}$之间的距离越小越好：<br>$$<br>G^{*}=arg \min_{G} {\operatorname{Div}}\left(P_{G}, P_{\text {data }}\right)<br>$$</p><p>由于$P_G$与$P_{data}$的分布都是未知的，所以无法直接求解$P_G$与$P_{data}$之间的距离。生成对抗网络借助判别器来解决这一问题。首先我们分别从$P_G$与$P_{data}$中取样，利用取出的样本训练一个判别器：我们希望当输入样本为$P_{data}$时，判别器会给出一个较高的分数；当输入样本为$P_G$时，判别器会给出一个较低的分数。例如，我们可以将判别器的目标函数定义成以下形式（与二分类的目标函数一致，即交叉熵）：<br>$$<br>V(G, D)=E_{x \sim P_{\text {data }}}[\log D(x)]+E_{x \sim P_{G}}[\log (1-D(x))]<br>$$</p><p>我们希望得到这样一个判别器（$G$固定）：<br>$$<br>D^{*}=arg \max_{D} V(D, G)<br>$$</p><p>从本质上来看，$\max _{D} V(D, G)$即表示$P_G$与$P_{data}$之间的$JS$散度（具体推导参见李宏毅老师的课程），即：<br>$$<br>\max_{D} V(G, D)=V\left(G, D^{*}\right)=-2 \log 2+2 J S D\left(P_{\text {data }} | P_{G}\right)<br>$$</p><p>$$<br>D^{*}(x)=\frac{P_{\text {data }}(x)}{P_{\text {data }}(x)+P_{G}(x)}<br>$$</p><p>因此通过构建判别器可以度量$P_G$与$P_{data}$之间的距离，所以$G^{*}$可以表示为：<br>$$<br>G^{*}=arg \min_{G} \max_{D} V(G, D)<br>$$</p><h4 id="生成对抗网络的求解过程"><a href="#生成对抗网络的求解过程" class="headerlink" title="生成对抗网络的求解过程"></a>生成对抗网络的求解过程</h4><p>$G^{*}$的求解过程大致如下：</p><ul><li>初始化生成器$G$和判别器$D$</li><li>迭代训练<ul><li>固定生成器$G$，更新判别器$D$的参数</li><li>固定生成器$D$，更新判别器$G$的参数</li></ul></li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/GAN/gan.png" alt="gan" style="zoom: 80%;"></p><p>对上述算法过程进行几点说明：</p><ul><li>在之前的描述中，$V(D,G)$表示的是目标函数的期望，但在实际计算过程中是通过采样平均的方式来逼近其期望值。</li><li>判别器的训练需要重复$k$次的原因是希望能尽可能使得$V(D,G)$接近最大值，这样才能满足$\max _{D} V(D, G)$即表示$P_G$与$P_{data}$之间的$JS$散度”这一假设。</li><li>在更新生成器参数时，$\frac{1}{m} \sum_{i=1}^{m} \log D\left(x^{i}\right)$这一项可以忽略，因为$D$固定，其相当于一个常数项。</li><li>在更新生成器参数时，我们使用$\tilde{V}=\frac{1}{m} \sum_{i=1}^{m} -\log \left(D\left(G\left(z^{i}\right)\right)\right)$代替$\tilde{V}=\frac{1}{m} \sum_{i=1}^{m} \log \left(1-D\left(G\left(z^{i}\right)\right)\right)$，这样做的目的是加速训练过程。</li></ul><h3 id="生成对抗网络的优化"><a href="#生成对抗网络的优化" class="headerlink" title="生成对抗网络的优化"></a>生成对抗网络的优化</h3><h4 id="fGAN"><a href="#fGAN" class="headerlink" title="fGAN"></a>fGAN</h4><p>&emsp;&emsp;通过上面的分析我们可以知道，构建生成模型需要解决的关键问题是最小化$P_G$和$P_{data}$之间的距离，这就涉及到如何对$P_G$和$P_{data}$之间的距离进行度量。在上述GAN的分析中，我们通过构建一个判别器来对$P_G$和$P_{data}$之间的距离进行度量，其中采用的目标函数为：<br>$$<br>V(G, D)=E_{x \sim P_{\text {data }}}[\log D(x)]+E_{x \sim P_{G}}[\log (1-D(x))]<br>$$</p><p>通过证明可知，$V(G, D)$其实度量的是$P_G$和$P_{data}$之间的$JS$散度。如果我们希望采用其他方式来衡量两个分布之间的距离，则需要对判别器的目标函数进行修改。根据论文<a href="https://arxiv.org/abs/1606.00709" target="_blank" rel="noopener">fGAN</a>，可以将判别器的目标函数定义成如下形式：<br>$$<br>D_{f^{*}}(P_{\text {data }} | P_{G})=\max_{\mathrm{D}}{E_{x \sim P_{\text {data }}}[D(x)]-E_{x \sim P_{G}}[f^{*}(D(x)]}<br>$$</p><p>则$G^{*}$可以表示为：<br>$$<br>G^{*}=arg \min_{G} D_{f^{*}}\left(P_{\text {data }} | P_{G}\right)<br>$$</p><p>$f^{*}$取不同表达式时，即表示不同的距离度量方式。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/GAN/f.png" alt="f"></p><p>令$f^{*}(t)=-log(1-exp(t))$，$D(x)$取$log$，代入$D_{f^{*}}\left(P_{\text {data }} | P_{G}\right)$即可得到$V(G,D)$。</p><h4 id="WGAN"><a href="#WGAN" class="headerlink" title="WGAN"></a>WGAN</h4><p>&emsp;&emsp;自2014年Goodfellow提出以来，GAN就存在着训练困难、生成器和判别器的loss无法指示训练进程、生成样本缺乏多样性等问题。针对这些问题，Martin Arjovsky进行了严密的理论分析，并提出了解决方案，即WGAN（WGAN的详细解读可参考<a href="https://zhuanlan.zhihu.com/p/25071913" target="_blank" rel="noopener">这篇博客</a>)。</p><ul><li><p>判别器越好，生成器梯度消失越严重。根据上面的分析可知，当判别器训练到最优时，$\max _{D} V(D, G)$衡量的是$P_G$与$P_{data}$之间的$JS$散度。问题就出在这个JS散度上，我们希望如果两个分布之间越接近它们的JS散度越小，通过优化JS散度就能将$P_G$拉向$P_{adta}$。这个希望在两个分布有所重叠的时候是成立的，但是如果两个分布完全没有重叠的部分，或者它们重叠的部分可忽略，$J S D\left(P_{\text {data }} | P_{G}\right)=log2$。在训练过程中，$P_G$与$P_{data}$都是通过采样得到的，在高维空间中两者之间几乎不存在交集，从而导致$\max _{D} V(D, G)$接近于0，生成器因此也无法得到有效训练。</p></li><li><p>最小化生成器loss函数$E_{x \sim P_{G}}[\log (1-D(x))$，会等价于最小化一个不合理的距离衡量，导致两个问题，一是梯度不稳定，二是collapse mode即多样性不足。假设当前的判别器最优，经过推导可以得到下面等式：<br>$$<br>E_{x \sim P_{G}}[\log (1-D(x))=KL\left(P_{\text {G }} | P_{data}\right)-2 J S D\left(P_{\text {data }} | P_{G}\right)<br>$$<br>这个等价最小化目标存在两个严重的问题。第一是它同时要最小化生成分布与真实分布的KL散度，却又要最大化两者的JS散度，一个要拉近，一个却要推远！这在直观上非常荒谬，在数值上则会导致梯度不稳定，这是后面那个JS散度项的毛病。第二，即便是前面那个正常的KL散度项也有毛病，因为KL散度不是一个对称的衡量$KL\left(P_{\text {G }} | P_{data}\right)$和$KL\left(P_{\text {data}} | P_{G}\right)$是有差别的。</p></li><li><p>原始GAN的主要问题就出在距离度量方式上面，Martin Arjovsky提出利用Wasserstein距离来进行衡量。Wasserstein距离相比KL散度、JS散度的优越性在于，即便两个分布没有重叠，Wasserstein距离仍然能够反映它们的远近。<br>$$<br>W(G, D)=E_{x \sim P_{\text {data }}}[D(x)]-E_{x \sim P_{G}}[D(x)]\<br>$$</p></li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/GAN/wgan.JPG" alt="wgan" style="zoom:80%;"></p><p>&emsp;&emsp;由以上算法可以看出，WGAN与原始的GAN在算法实现方面只有四处不同：（1）判别器最后一层去掉sigmoid；（2）生成器和判别器的loss不取log；（3）每次更新判别器的参数之后把它们的绝对值截断到不超过一个固定常数c；（4）不要用基于动量的优化算法（包括momentum和Adam），推荐RMSProp，SGD也行。</p><h3 id="生成对抗网络的实现"><a href="#生成对抗网络的实现" class="headerlink" title="生成对抗网络的实现"></a>生成对抗网络的实现</h3><p>&emsp;&emsp;本文实现了几种常见的生成对抗网络模型，包括原始GAN、CGAN、WGAN、DCGAN。开发环境为jupyter lab，所使用的深度学习框架为pytorch，并结合tensorboard动态观测生成器的训练效果，具体代码请参考<a href="https://github.com/chaoge123456/workspace/tree/main/GAN/gan" target="_blank" rel="noopener">我的github</a>。</p><h4 id="GAN"><a href="#GAN" class="headerlink" title="GAN"></a>GAN</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">real_label = torch.ones(batch_size, <span class="number">1</span>)</span><br><span class="line">fake_label = torch.zeros(batch_size, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 训练判别器</span></span><br><span class="line">d_real = D(real_img)</span><br><span class="line">d_real_loss = criterion(d_real, real_label)</span><br><span class="line"></span><br><span class="line">z = torch.normal(<span class="number">0</span>, <span class="number">1</span>, (batch_size, latent))</span><br><span class="line">fake_img = G(z)</span><br><span class="line">d_fake = D(fake_img)</span><br><span class="line">d_fake_loss = criterion(d_fake, fake_label)</span><br><span class="line"></span><br><span class="line">optimizer_D.zero_grad()</span><br><span class="line">d_loss = d_real_loss + d_fake_loss</span><br><span class="line">d_loss.backward()</span><br><span class="line">optimizer_D.step()</span><br><span class="line"></span><br><span class="line"><span class="comment"># 训练生成器</span></span><br><span class="line">fake_img = G(z)</span><br><span class="line">d_fake = D(fake_img)</span><br><span class="line">g_loss = criterion(d_fake, real_label)</span><br><span class="line"></span><br><span class="line">optimizer_G.zero_grad()</span><br><span class="line">g_loss.backward()</span><br><span class="line">optimizer_G.step()</span><br></pre></td></tr></table></figure><h4 id="CGAN"><a href="#CGAN" class="headerlink" title="CGAN"></a>CGAN</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">real_label = torch.ones(batch_size, <span class="number">1</span>)</span><br><span class="line">fake_label = torch.zeros(batch_size, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">z = torch.normal(<span class="number">0</span>, <span class="number">1</span>, (batch_size, latent))</span><br><span class="line"></span><br><span class="line"><span class="comment"># 训练判别器</span></span><br><span class="line">d_real = D(real_img, label)</span><br><span class="line">d_real_loss = criterion(d_real, real_label)</span><br><span class="line"></span><br><span class="line">fake_img = G(z, label)</span><br><span class="line">d_fake = D(fake_img, label)</span><br><span class="line">d_fake_loss = criterion(d_fake, fake_label)</span><br><span class="line"></span><br><span class="line">optimizer_D.zero_grad()</span><br><span class="line">d_loss = (d_real_loss + d_fake_loss)</span><br><span class="line">d_loss.backward()</span><br><span class="line">optimizer_D.step()</span><br><span class="line"></span><br><span class="line"><span class="comment"># 训练生成器</span></span><br><span class="line">fake_img = G(z, label)</span><br><span class="line">d_fake = D(fake_img, label)</span><br><span class="line">g_loss = criterion(d_fake, real_label)</span><br><span class="line"></span><br><span class="line">optimizer_G.zero_grad()</span><br><span class="line">g_loss.backward()</span><br><span class="line">optimizer_G.step()</span><br></pre></td></tr></table></figure><h4 id="WGAN-1"><a href="#WGAN-1" class="headerlink" title="WGAN"></a>WGAN</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 训练判别器</span></span><br><span class="line">d_real = D(real_img)</span><br><span class="line"><span class="comment">#d_real_loss = criterion(d_real, real_label)</span></span><br><span class="line">d_real_loss = d_real</span><br><span class="line"></span><br><span class="line">z = torch.normal(<span class="number">0</span>, <span class="number">1</span>, (batch_size, latent))</span><br><span class="line">fake_img = G(z)</span><br><span class="line">d_fake = D(fake_img)</span><br><span class="line"><span class="comment">#d_fake_loss = criterion(d_fake, fake_label)</span></span><br><span class="line">d_fake_loss = d_fake</span><br><span class="line"></span><br><span class="line">optimizer_D.zero_grad()</span><br><span class="line"><span class="comment">#d_loss = d_real_loss + d_fake_loss</span></span><br><span class="line">d_loss = torch.mean(d_fake_loss) - torch.mean(d_real_loss)</span><br><span class="line">d_loss.backward()</span><br><span class="line">optimizer_D.step()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> p <span class="keyword">in</span> D.parameters():</span><br><span class="line">    p.data.clamp_(-clip_value, clip_value)</span><br><span class="line"><span class="comment"># 训练生成器</span></span><br><span class="line">fake_img = G(z)</span><br><span class="line">d_fake = D(fake_img)</span><br><span class="line"><span class="comment">#g_loss = criterion(d_fake, real_label)</span></span><br><span class="line">g_loss = - torch.mean(d_fake)</span><br><span class="line"></span><br><span class="line">optimizer_G.zero_grad()</span><br><span class="line">g_loss.backward()</span><br><span class="line">optimizer_G.step()</span><br></pre></td></tr></table></figure>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/%E7%94%9F%E6%88%90%E5%AF%B9%E6%8A%97%E7%BD%91%E7%BB%9C.html/#disqus_thread</comments>
    </item>
    
    <item>
      <title>卷积神经网络实现解析</title>
      <link>https://chaoge123456.github.io/%E5%8D%B7%E7%A7%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%AE%9E%E7%8E%B0%E8%A7%A3%E6%9E%90.html/</link>
      <guid>https://chaoge123456.github.io/%E5%8D%B7%E7%A7%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%AE%9E%E7%8E%B0%E8%A7%A3%E6%9E%90.html/</guid>
      <pubDate>Sun, 17 May 2020 06:41:28 GMT</pubDate>
      <description>
      
        
        
          &lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt;卷积神经网络是受生物学中感受野机制的启发而提出的，是一种具有局部连接、权重共享等特性的深层前馈神经网络，其主要应用于图像和视频分析等领域。2012年，卷积神经网络在ImageNet大规模视觉识别挑战竞赛中大放异彩，一定程度上引领了深度
        
      
      </description>
      
      <content:encoded><![CDATA[<p><strong>摘要：</strong>卷积神经网络是受生物学中感受野机制的启发而提出的，是一种具有局部连接、权重共享等特性的深层前馈神经网络，其主要应用于图像和视频分析等领域。2012年，卷积神经网络在ImageNet大规模视觉识别挑战竞赛中大放异彩，一定程度上引领了深度学习袭卷全球的潮流。这篇文章聚焦于卷积神经网络的实现细节，本文我们将使用pytorch中的tensor实现一个简单的卷积神经网络框架（很多文章采用的是numpy实现），并在FashionMnist数据集中进行测试。文中给出实现过程中的部分代码，完整的代码可以在<a href="https://github.com/chaoge123456/ml-torch/blob/master/CNN/complex.ipynb" target="_blank" rel="noopener">我的github</a>中找到。</p><h2 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h2><ul><li>卷积神经网络简介</li><li>卷积层的实现细节<ul><li>前向计算</li><li>反向传播</li></ul></li><li>池化层的实现细节<ul><li>前向计算</li><li>反向传播</li></ul></li><li>测试结果</li></ul><h3 id="卷积神经网络简介"><a href="#卷积神经网络简介" class="headerlink" title="卷积神经网络简介"></a>卷积神经网络简介</h3><p>&emsp;&emsp;在前面的文章中我们讲到了全连接前馈网络，并且在FashionMnist数据集上对我们构建的网络框架进行了测试。简单回顾一下当时的数据处理过程，FashionMnist数据集中的图片通道数为1、图片尺寸为28×28，原始数据集表示为$ X=(N, 1, 28 ,28) $。在训练之前，我们需要将训练数据转化为 $ X=(N, 1*28*28) $，相当于将数据集中每一张图片的像素点展开成向量的形式。显而易见，将图片展开为向量会丢失空间信息，这会对模型的泛化性能产生很大的影响。除此之外，利用全连接前馈网络处理图像数据往往会需要很多的参数。举例来说，假设现在数据集中的图片为100×100的彩色图片，此时每张图片中包含的像素点为$ 3*100*100 $，即$ X=(N, 3*100*100) $。如果第一个隐藏层有1000个神经元，那么仅该层包含的参数个数为$ 30000*1000 + 1000 $。过多的参数会给模型的训练过程造成很大的负担，同时也会导致过拟合等问题。综上所述，一般情况下我们不采用全连接前馈网络来处理图像数据。</p><p>&emsp;&emsp;卷积神经网络最早主要是用来处理图像信息，是受生物学中<strong>感受野</strong>机制的启发而提出的（感受野是指卷积神经网络每一层输出的特征图上的像素点在输入图片上映射的区域大小。通俗点的解释是，特征图上的一个点对应输入图上的区域）。1998年，LeCun提出了经典的卷积网络模型LeNet-5，第一次较为完整的阐述了卷积神经网络的框架和结构。卷积神经网络由输入层、卷积层、激活层、池化层、全连接层及输出层构成。卷积层和池化层一般会取若干个，采用卷积层和池化层交替设置，即一个卷积层连接一个池化层，池化层后再连接一个卷积层，依此类推。与全连接前馈网络相比，卷积神经网络在结构上具有局部连接、 权重共享、降采样等特点，并且在训练过程中会完整保留数据的空间信息。这些特性使得卷积神经网络图像处理领域表现更加出色，并且使用的参数更少。</p><table><thead><tr><th style="text-align:center">CNN层次结构</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:center">输入层</td><td style="text-align:left">卷积网络的原始输入，可以是原始或预处理后的像素矩阵</td></tr><tr><td style="text-align:center">卷积层</td><td style="text-align:left">参数共享、局部连接，利用平移不变性从全局特征图提取局部特征</td></tr><tr><td style="text-align:center">激活层</td><td style="text-align:left">将卷积层的输出结果进行非线性映射</td></tr><tr><td style="text-align:center">池化层</td><td style="text-align:left">进一步筛选特征，可以有效减少后续网络层次所需的参数量</td></tr><tr><td style="text-align:center">全连接层</td><td style="text-align:left">将多维特征展平为2维特征，通常低维度特征对应任务的学习目标（类别或回归值）</td></tr></tbody></table><p>以下主要介绍卷积神经网络中卷积层和池化层的实现细节，激活层和全连接层的实现与全连接前馈网络基本一致，这里不再赘述，具体代码参考<a href="https://github.com/chaoge123456/ml-torch/blob/master/CNN/complex.ipynb" target="_blank" rel="noopener">我的github</a>（需要注意的是，本文实现代码中，默认输入图片高度和宽度相同）。</p><h3 id="卷积层实现细节"><a href="#卷积层实现细节" class="headerlink" title="卷积层实现细节"></a>卷积层实现细节</h3><p>&emsp;&emsp;开始介绍之前，简要说明参数设置情况。输入数据尺寸为$s=5$，通道数为$in_channel=3$，即$ input=(n, 3, 5, 5)$；单个卷积核尺寸为$k=2$，卷积层输出通道数为$out_channel=10$，即$kernel=(10, 3, 2, 2)$；假设卷积步幅$stride=1$，$pad=0$，根据特征图大小计算公式<br>$$<br>p=\frac{s-k+2*pad}{stride}+1<br>$$<br>则卷积层输出为$output=(N, 10, 4, 4)$。</p><h4 id="前向计算"><a href="#前向计算" class="headerlink" title="前向计算"></a>前向计算</h4><p>&emsp;&emsp;在卷积神经网络中所使用的卷积，一般是指互相关操作，其本质上就是利用卷积核在输入数据上进行滑动，并在滑动窗口内计算点积，计算过程如如下图所示。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/5.png" style="zoom:67%;"></p><p>&emsp;&emsp;如果将该操作拓展到多通道的情况，我们需要将每一个通道数据与其对应的卷积核分别进行互相关操作，每个通道都会得到一个输出，然后将所有输出相同位置相加，得到最终的特征图，计算过程如下所示。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/1.png" style="zoom: 50%;"></p><p>&emsp;&emsp;这一过程最简单的思路是通过多重循环来实现，但是这种实现方式会极大降低训练过程的效率。im2col算法通过将这一过程转化为矩阵乘法的形式可以极大程度提升计算效率，该算法的主要思想如下图所示。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/2.png" style="zoom: 50%;"></p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/3.png" style="zoom:50%;"></p><p>&emsp;&emsp;im2col的核心思想是将每个卷积核在其对应通道上滑动过程中滑动窗口位置上的数据重新排列，组合成一维行向量的形式，而卷积核自身则排列成列向量的形式。卷积层输出的特征图中的每一个值都能表示成上述行向量和列向量相乘的形式。如果含有多层卷积核，则如下图方式排列。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/4.png" style="zoom:50%;"></p><p>通过这种方式，我们的输入数据可以成$input=(n*p*p, k*k*in_channel)$，卷积核可表示为$kernel=(k*k*in_channel, out_channel)$。该过程的代码如下所示：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">im2col</span><span class="params">(self, input)</span>:</span></span><br><span class="line">    n, in_channel, s, _ = input.shape</span><br><span class="line">    p = (s - self.k_size) // self.stride + <span class="number">1</span></span><br><span class="line">    im = torch.zeros((n*p*p, k*k*in_channel))</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> range(p):</span><br><span class="line">        i_start = i*self.stride</span><br><span class="line">        i_end = i_start + self.k_size</span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> range(p):</span><br><span class="line">            j_start = j*self.stride</span><br><span class="line">            j_end = j_start + self.k_size</span><br><span class="line">            im[i*p+j::p*p, :] = input[:,:,i_start:i_end,j_start:j_end].reshape(n, <span class="number">-1</span>)</span><br><span class="line"></span><br><span class="line">    w = self.w.reshape(self.k_size**<span class="number">2</span>*c, self.out_channel)</span><br><span class="line">    output = torch.matmul(im, w) + self.b</span><br><span class="line">    <span class="keyword">return</span> output.reshape(n, self.out_channel, p, p)</span><br></pre></td></tr></table></figure><h4 id="反向传播"><a href="#反向传播" class="headerlink" title="反向传播"></a>反向传播</h4><p>&emsp;&emsp;在反向传播过程中，卷积层主要做两件事：（1）利用卷积操作的输出层的误差项$\delta^l$来求卷积操作输入层的误差项$\delta^{l-1}$，并将结果继续反向传播；（2）求解卷积核的梯度，并对其进行更新（反向传播的过程可以参考<a href="https://www.cnblogs.com/pinard/p/6494810.html" target="_blank" rel="noopener">这篇博客</a>）。首先我们来求解输入层的误差项$\delta^{l-1}$，在将具体实现之前，我们先看下面这幅图</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/6.png" style="zoom: 67%;"></p><p>图的左侧表示输入，右侧表示输出。在输入数据中，数字0所标识的位置分别经历了四次卷积过程，分别对输出层的四个位置产生影响，卷积核中的4、3、2、1标识的是其每次参与运算的权重。如果我们现在需要计算0所在位置的误差项，只需要将卷积核中4、3、2、1四个权重与输出层对应位置的误差项进行点积运算即可。这种做法与互相关相同，但是需要注意的是这里的卷积核与之前的不同，是前向传播中的卷积核旋转180度的结果。我们给出输入层的误差项$\delta^{l-1}$的公式<br>$$<br>\delta^{l-1}=\delta^{l} * \operatorname{rot} 180\left(W^{l}\right)<br>$$<br>除此之外，由于输出层的尺寸要小于输入层，所以在进行卷积之前需要对输出层进行padding，如下图所示。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/7.png" style="zoom:67%;"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">nexteta</span><span class="params">(self, grad)</span>:</span></span><br><span class="line">    <span class="comment"># 计算卷积输入层的误差项</span></span><br><span class="line">    n, c, s, _ = grad.shape</span><br><span class="line">    ln, lc, ls, _ = self.param[<span class="string">'inputshape'</span>]</span><br><span class="line">    pad = math.ceil((self.stride*(ls<span class="number">-1</span>) + self.k_size - s) / <span class="number">2</span>) </span><br><span class="line">    grad = self.paddings(grad, pad)</span><br><span class="line">    w = torch.flip(self.w, dims=[<span class="number">2</span>,<span class="number">3</span>]) <span class="comment"># 卷积核翻转</span></span><br><span class="line"></span><br><span class="line">    p = (s - self.k_size) // self.stride + <span class="number">1</span></span><br><span class="line">    w = w.reshape(lc, self.k_size**<span class="number">2</span>*c)</span><br><span class="line">    im = self.im2col(grad, n, c, s, p)</span><br><span class="line">    eta = torch.matmul(im, w.T).reshape(n, lc, p, p)</span><br><span class="line">    <span class="keyword">return</span> eta   </span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">paddings</span><span class="params">(self, input, pad)</span>:</span></span><br><span class="line">    <span class="comment"># padding实现接口</span></span><br><span class="line">    n, c, s, _ = input.shape</span><br><span class="line">    outshape = pad*<span class="number">2</span> + s</span><br><span class="line">    out = torch.zeros((n, c, outshape, outshape))</span><br><span class="line">    out[:,:,pad:outshape-pad,pad:outshape-pad] = input</span><br><span class="line">    <span class="keyword">return</span> out</span><br></pre></td></tr></table></figure><p>接下来，我们要对卷积核进行更新。首先我们给出计算公式，<br>$$<br>\frac{\partial J(W, b)}{\partial W^{l}}=a^{l-1} * \delta^{l}<br>$$<br>其中$a^{l-1}$表示卷积层的输入， $\delta^{l}$表示卷积操作的输出层的误差项。根据前面的描述我们可以知道，$a^{l-1}$是一个$(n, 3, 5, 5)$的矩阵，$\delta^{l}$是$(n, 10, 4, 4)$的矩阵，而我们要求解的卷积核的梯度是$(10, 3, 2, 2)$的矩阵。大致的计算过程是这样的：将$\delta^{l}$视为卷积核，每次取第$i$通道$(n, i, 4, 4)$分别与对应数据的$(n, 3, 5, 5)$的三个通道进行卷积操作，由此可以得到$(n, 3, 2, 2)$，然后对$n$维数据进行求和取平均即得到$(1, 3, 2, 2)$。将此过程进行10次，即可得到卷积核的梯度值，具体代码如下所示。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">backward</span><span class="params">(self, input, grad)</span>:</span></span><br><span class="line">    dw = torch.zeros(self.w.shape)</span><br><span class="line">    db = torch.zeros(self.b.shape)</span><br><span class="line">    n, c, s, _ = grad.shape</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> range(self.k_size):</span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> range(self.k_size):</span><br><span class="line">            cons = input[:,:,i*self.stride:i*self.stride+s,\n </span><br><span class="line">                         i*self.stride:i*self.stride+s]</span><br><span class="line">            <span class="keyword">for</span> k <span class="keyword">in</span> range(c):</span><br><span class="line">                dw[k,:,i,j] = torch.sum(cons * (grad[:, \n</span><br><span class="line">                         k, :, :])[:, <span class="keyword">None</span>], axis=(<span class="number">0</span>,<span class="number">2</span>,<span class="number">3</span>)) / n</span><br><span class="line">                db = torch.sum(grad, axis=(<span class="number">0</span>,<span class="number">2</span>,<span class="number">3</span>)) / n</span><br><span class="line">    self.w -= self.learning_rate*dw <span class="comment"># 更新权重</span></span><br><span class="line">    self.b -= self.learning_rate*db <span class="comment"># 更新偏置</span></span><br><span class="line">    eta = self.nexteta(grad)</span><br><span class="line">    <span class="keyword">return</span> eta</span><br></pre></td></tr></table></figure><h3 id="池化层"><a href="#池化层" class="headerlink" title="池化层"></a>池化层</h3><p>&emsp;&emsp;池化层又称为降采样层，作用是对感受域内的特征进行筛选，提取区域内最具代表性的特征，能够有效地降低输出特征尺度，进而减少模型所需要的参数量。按操作类型通常分为最大池化、平均池化和求和池化，它们分别提取感受域内最大、平均与总和的特征值作为输出，最常用的是最大池化（这里主要介绍最大池化）。</p><p>&emsp;&emsp;开始介绍之前，简要说明参数设置情况。输入数据尺寸为$s=4$，通道数为$in_channel=10$，即$ input=(n, 10, 4, 4)$；池化窗口尺寸为$k=2$，池化步幅$stride=2$，根据特征图大小计算公式<br>$$<br>p=\frac{s-k}{stride}+1<br>$$<br>则池化层输出为$output=(N, 10, 2, 2)$。</p><h4 id="前向计算-1"><a href="#前向计算-1" class="headerlink" title="前向计算"></a>前向计算</h4><p>&emsp;&emsp;与卷积操作相比，池化层的计算较为简单，且池化层不改变通道数量，不包含训练参数。最大池化提取感受域内最大的值作为输出，计算过程如下图所示</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/8.png" style="zoom:80%;"></p><p>需要注意的是，进行池化操作时需要记录感受域内最大值的位置，在反向传播时会用到，具体代码如下所示（因为需要记录位置，暂时没想到好的解决办法，所以直接用的循环）。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">pool</span><span class="params">(self, input)</span>:</span></span><br><span class="line">    self.param[<span class="string">'shape'</span>] = input.shape</span><br><span class="line">    n, c, s, _ = input.shape</span><br><span class="line">    p = (s - self.k_size) // self.stride + <span class="number">1</span></span><br><span class="line">    pl = torch.zeros((n, c, p, p))</span><br><span class="line">    index = []</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> range(n):</span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> range(c):</span><br><span class="line">            <span class="keyword">for</span> k <span class="keyword">in</span> range(p):</span><br><span class="line">                <span class="keyword">for</span> z <span class="keyword">in</span> range(p):</span><br><span class="line">                    inp = input[i,j,k*self.stride:k*self.stride+self.k_size,</span><br><span class="line">                                z*self.stride:z*self.stride+self.k_size]</span><br><span class="line">                    pl[i,j,k,z] = torch.max(inp)</span><br><span class="line">                    ix = torch.where(inp==pl[i,j,k,z])</span><br><span class="line">                    index.append((i,j,k*self.stride+ix[<span class="number">0</span>],z*self.stride+ix[<span class="number">1</span>])) </span><br><span class="line">                    self.param[<span class="string">'index'</span>] = index</span><br><span class="line">    <span class="keyword">return</span> pl</span><br></pre></td></tr></table></figure><h4 id="反向传播-1"><a href="#反向传播-1" class="headerlink" title="反向传播"></a>反向传播</h4><p>&emsp;&emsp;池化操作的反向传播主要是计算其输入层的误差项。首先我们定义一个全0矩阵，其大小与池化操作输入相同，对于最大池化而言，将池化操作输出层的误差项的值放在之前做前向传播算法得到最大值的位置，如下图所示。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/9.png" style="zoom:80%;"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">backward</span><span class="params">(self, input, grad)</span>:</span></span><br><span class="line">    <span class="comment"># 反向传播</span></span><br><span class="line">    grad = grad.reshape(<span class="number">-1</span>)</span><br><span class="line">    eta = torch.zeros(self.param[<span class="string">'shape'</span>])</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> range(len(self.param[<span class="string">'index'</span>])):</span><br><span class="line">    eta[self.param[<span class="string">'index'</span>][i]] = grad[i]</span><br><span class="line">    <span class="keyword">return</span> eta</span><br></pre></td></tr></table></figure><h3 id="测试结果"><a href="#测试结果" class="headerlink" title="测试结果"></a>测试结果</h3><p>&emsp;&emsp;我们定义了一个简单的卷积神经网络模型，并在FashionMnist做了一组测试。由于在池化层等操作上使用了循环操作，再加上笔记本性能不是特别好，跑了一个epoch，模型在测试集上的预测准确率达到了71.3%，但是花费的时间较长，大概40分钟，整体的代码确实需要做一些优化。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">learning_rate = <span class="number">0.1</span></span><br><span class="line">network = []</span><br><span class="line">network.append(Convolutional(<span class="number">1</span>,<span class="number">16</span>,<span class="number">5</span>,<span class="number">2</span>,learning_rate=learning_rate))</span><br><span class="line">network.append(ReLU())</span><br><span class="line">network.append(MaxPooling(<span class="number">16</span>,<span class="number">2</span>,<span class="number">2</span>))</span><br><span class="line">network.append(Dense(<span class="number">576</span>,<span class="number">20</span>,learning_rate=learning_rate))</span><br><span class="line">network.append(ReLU())</span><br><span class="line">network.append(Dense(<span class="number">20</span>,<span class="number">10</span>,learning_rate=learning_rate))</span><br><span class="line">network.append(Softmax())</span><br></pre></td></tr></table></figure><p>&emsp;&emsp;总的来说，完整实现一个卷积神经网络还是有难度的。整个过程我觉得最难的是理解数据在卷积网络中的流动过程，因为这些数据都是以张量的形式呈现，所以理解起来有些困难。除此之外就是代码的调试，因为其中存在很多细节，一个地方出问题就会对整个结果产生影响。但是，经历这个过程之后，确实对卷积网络有了更多的认识。</p>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/%E5%8D%B7%E7%A7%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%AE%9E%E7%8E%B0%E8%A7%A3%E6%9E%90.html/#disqus_thread</comments>
    </item>
    
    <item>
      <title>深度神经网络实现解析</title>
      <link>https://chaoge123456.github.io/%E6%B7%B1%E5%BA%A6%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%AE%9E%E7%8E%B0%E8%A7%A3%E6%9E%90.html/</link>
      <guid>https://chaoge123456.github.io/%E6%B7%B1%E5%BA%A6%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%AE%9E%E7%8E%B0%E8%A7%A3%E6%9E%90.html/</guid>
      <pubDate>Sat, 18 Apr 2020 19:09:28 GMT</pubDate>
      <description>
      
        
        
          &lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt;最近一段时间在回顾深度学习的一些基本知识，感觉对有些内容的理解比较模糊，于是萌生了手动来实现的想法。其实类似的工作之前也做过，写过决策树、支持向量机、神经网络等，不过当时是用numpy写的。因为现在一直在使用pytorch，pytor
        
      
      </description>
      
      <content:encoded><![CDATA[<p><strong>摘要：</strong>最近一段时间在回顾深度学习的一些基本知识，感觉对有些内容的理解比较模糊，于是萌生了手动来实现的想法。其实类似的工作之前也做过，写过决策树、支持向量机、神经网络等，不过当时是用numpy写的。因为现在一直在使用pytorch，pytorch中的tensor与numpy中的array很相似，所以这次的代码主要使用tensor来实现。目前实现的代码包括逻辑回归、softmax回归、深度神经网络和卷积神经网络，所有的代码都可以在<a href="https://github.com/chaoge123456/ml-torch" target="_blank" rel="noopener">我的github</a>中找到。这篇博客主要来记录在实现深度神经网络过程中的一些思路，以及遇到的问题。</p><h2 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h2><ul><li>深度神经网络简介</li><li>深度神经网络框架实现<ul><li>整体思路</li><li>前向计算</li><li>反向传播</li></ul></li><li>交叉熵损失函数与softmax激活函数</li></ul><h3 id="深度神经网络简介"><a href="#深度神经网络简介" class="headerlink" title="深度神经网络简介"></a>深度神经网络简介</h3><p>&emsp;&emsp;网上对深度神经网络（DNN）介绍的文章很多，这里不再赘述。推荐一些资料：<a href="https://www.cnblogs.com/pinard/p/6418668.html" target="_blank" rel="noopener">刘建平博客</a> 、复旦大学邱锡鹏教授<a href="https://nndl.github.io/" target="_blank" rel="noopener">神经网络与深度学习</a>。</p><h3 id="深度神经网络框架实现"><a href="#深度神经网络框架实现" class="headerlink" title="深度神经网络框架实现"></a>深度神经网络框架实现</h3><h4 id="整体思路"><a href="#整体思路" class="headerlink" title="整体思路"></a>整体思路</h4><p>&emsp;&emsp;在使用pytorch、tensorflow等框架来构建一个深度神经网络模型的时候，通常全连接层和激活函数层分开进行定义的。通过这种模块化的方式有利于自由的设计模型，在本文的代码中依然沿用这种方式。首先，我们需要定义一个父类，所有的全连接层以及激活函数层均继承自该父类。神经网络中的每一层都实现统一的方法接口，包括前向计算和反向传播，这样我们可以实现数据在神经网络模型中流动时的一致性。每一层可以根据自身的处理逻辑来重写继承自父类的方法，并且可以根据需要来增加成员方法和变量，例如全连层需要定义权重和偏值，而激活函数层则不需要。除此之外，在反向传播过程中，我们使用SGD算法来对参数进行更新。由于我们采用模块化的方式构建模型，所以神经网络的不同层之间相对独立，对于不同的层我们可以设置不同的学习率。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 全连接层以及激活函数层均继承自该父类</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Layer</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="keyword">pass</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">forward</span><span class="params">(self, input)</span>:</span></span><br><span class="line">        <span class="keyword">return</span> input</span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">backward</span><span class="params">(self, input, grad)</span>:</span></span><br><span class="line">        <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><h4 id="前向计算"><a href="#前向计算" class="headerlink" title="前向计算"></a>前向计算</h4><p>&emsp;&emsp;前向计算的过程较为简单，网络中每一层所需要做的就是将该层输入传入self.forward函数，根据内部逻辑返回输出，该输出又将作为下一层的输入。对于全连接层，假设输入为$a^{l-1}$，计算$z^l=(W^l)^T*a^{l-1}+b^l$，再将$z^l$传递到下一层；对于激活函数层，假设输入为$z^l$，计算$a^l=\sigma(z^l)$，再将$a^l$传递到下一层。需要注意的是，我们需要记录下神经网络中的每一层的输入值$a^i$，在反向传播更新权重时会用到。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/11.png" style="zoom: 80%;"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">forward</span><span class="params">(network, x)</span>:</span></span><br><span class="line">    <span class="comment"># 前向传播</span></span><br><span class="line">    activations = []</span><br><span class="line">    input = x</span><br><span class="line">    <span class="keyword">for</span> layer <span class="keyword">in</span> network:</span><br><span class="line">        activations.append(layer.forward(input))</span><br><span class="line">        input = activations[<span class="number">-1</span>]      </span><br><span class="line">    <span class="keyword">return</span> activations</span><br></pre></td></tr></table></figure><h4 id="反向传播"><a href="#反向传播" class="headerlink" title="反向传播"></a>反向传播</h4><p>&emsp;&emsp;对于反向传播而言，每一层的处理逻辑大致相同，即将该层输出值的误差项作为self.backward函数的输入，经过计算得到该层输入值的误差项，继续反向传播。需要注意的是，由于全连接层含有偏置和权重，在反向传播时除了需要计算误差项之外，还需要更新偏置和权重。反向传播算法的推导过程可以参考<a href="https://www.cnblogs.com/pinard/p/6422831.html" target="_blank" rel="noopener">这篇博客</a>，这里给出以下结论：<br>$$<br>\delta^{l}=(W^{l+1})^{T} \delta^{l+1} \odot \sigma^{\prime}(z^{l})<br>$$<br><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/cnn/cnn/12.png" style="zoom:80%;"></p><p>&emsp;&emsp;在很多介绍神经网络的书中，通常将一个全连接层和一个激活函数构成的整体当作神经网络的一层。但是，在我们代码中是将二者分开的，所以我们需要对上式进行改写。上式中$\delta^{l+1}$表示神经网络第$l+1$层输入值的误差项，即$a^l$的误差项；$\delta^{l}$表示神经网络第$l$层输入值的误差项，即$a^{l-1}$的误差项。由于我们将第$l$层拆分为两层，所以我们先要计算激活函数层输入值的误差项（记做$\delta^l_*$)，再计算全连接层输入值的误差项$\delta^{l}$。所以我们得到下面的公式：<br>$$<br>\delta^{l}_{*}= \delta^{l+1} \odot \sigma^{\prime}(z^{l})<br>$$</p><p>$$<br>\delta^{l}=(W^{l+1})^{T} \delta^{l}_{*}<br>$$</p><p>&emsp;&emsp;通过这种转换，实现了不同层之间数据流动的一致性，即反向传播时，不论是全连接层还是激活函数层都是接受其输出值的误差项，返回其输入值的误差项。除此之外，由于在全连接层需要对权重和偏置进行更新，需要$a^{l-1}$作为参数，所以在self.backward的参数列表中加入该项。虽然激活函数层不需要更新参数，但是为了统一写法，也会加入这一参数。以下给出全连接层的backward函数。<br>$$<br>\frac{\partial J(W, b, x, y)}{\partial W^{l}}=\delta^{l}\left(a^{l-1}\right)^{T}<br>$$</p><p>$$<br>\frac{\partial J(W, b, x, y)}{\partial b^{l}}=\delta^{l}<br>$$</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">backward</span><span class="params">(self, input, grad)</span>:</span></span><br><span class="line">    grad_input = torch.mm(grad, self.w.T)</span><br><span class="line">    dw = torch.mm(input.T, grad) / input.shape[<span class="number">0</span>]</span><br><span class="line">    db = torch.sum(grad, axis=<span class="number">0</span>) / input.shape[<span class="number">0</span>]</span><br><span class="line">    self.w -= self.learning_rate*dw</span><br><span class="line">    self.b -= self.learning_rate*db</span><br><span class="line">    <span class="keyword">return</span> grad_input</span><br></pre></td></tr></table></figure><h3 id="交叉熵损失函数与softmax激活函数"><a href="#交叉熵损失函数与softmax激活函数" class="headerlink" title="交叉熵损失函数与softmax激活函数"></a>交叉熵损失函数与softmax激活函数</h3><p>&emsp;&emsp;使用神经网络处理多分类问题的时候，我们往往会使用交叉熵损失函数与softmax激活函数组合的形式，即将神经网络最后一层的激活函数设置为softmax，模型整体的损失采用交叉熵来进行计算。在这之前，我对二者的理解停留在比较浅显的层面：softmax函数将多个神经元的输出，映射到（0,1）区间内，可以看成概率来理解；交叉熵损失函数可以衡量两个概率分布之间的相似性，即softmax的输出和训练数据标签onehot编码之间的相似性。除此之外，交叉熵损失函数与softmax激活函数组合更深层次的原因体现在计算层面，它能够简化反向传播的计算过程。以下，我们对该问题进行分析。</p><p>&emsp;&emsp;假设我们现在要计算softmax激活函数层输入值的误差项$\delta^{l}_{*}$，根据我们上面的得到的公式，我们需要分别计算$\delta^{l+1}$和$\sigma^{\prime}(z^{l})$。因为softmax是神经网络的最后一层，所以这里的$\delta^{l+1}$等于交叉熵损失函数对softmax输出值的导数$\frac{\partial J}{\partial a^{l}}$。而$\sigma^{\prime}(z^{l})$则表示softmax函数对其输入进行求导$\frac{\partial a^{l}}{\partial z^{l}}$。</p><p>&emsp;&emsp;按照常规思路，我们需要单独计算这两个过程。但是对于交叉熵损失函数与softmax激活函数组合情况而言，可以将这两个过程进行合并，可以直接推导出$\frac{\partial J}{\partial z^{l}}$的值。具体的推导过程，可以参考<a href="https://blog.csdn.net/qian99/article/details/78046329" target="_blank" rel="noopener">这篇博客</a>，这篇博客是我目前看过最简单易懂的，这里给出结论：<br>$$<br>\delta^{l}_{*}=\frac{\partial J}{\partial z^{l}}=a^l-y<br>$$<br>可以看出这个结论十分简洁优美，所以当我们使用交叉熵损失函数与softmax激活函数组合形式的时候，softmax层的backwrad函数只需要将$a^l-y$返回即可，无需进行任何操作。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># softmax激活函数作为神经网络最后一层</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Softmax</span><span class="params">(Layer)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="keyword">pass</span></span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">forward</span><span class="params">(self,input)</span>:</span></span><br><span class="line">        exp = torch.exp(input)</span><br><span class="line">        exp_sum = torch.sum(exp, axis=<span class="number">1</span>, keepdims=<span class="keyword">True</span>)</span><br><span class="line">        <span class="keyword">return</span> exp / exp_sum</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">backward</span><span class="params">(self,input,grad)</span>:</span></span><br><span class="line">        <span class="keyword">return</span> grad</span><br></pre></td></tr></table></figure><p>&emsp;&emsp;值得注意的一点是，在使用pytorch框架构建神经网络来实现多分类任务时，如果我们的代价函数使用torch.nn.CrossEntropyLoss()（交叉熵），网络的最后一层无需再定义softmax层，并且数据标签也不需要修改为onehot编码，这些逻辑在其内部应该都会实现，具体操作请参考<a href="https://github.com/chaoge123456/ml-torch/blob/master/DNN/simple.ipynb" target="_blank" rel="noopener">我的代码</a>。</p>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/%E6%B7%B1%E5%BA%A6%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%AE%9E%E7%8E%B0%E8%A7%A3%E6%9E%90.html/#disqus_thread</comments>
    </item>
    
    <item>
      <title>对抗样本生成系列：JSMA目标扰动</title>
      <link>https://chaoge123456.github.io/%E5%AF%B9%E6%8A%97%E6%A0%B7%E6%9C%AC%E7%94%9F%E6%88%90%E7%B3%BB%E5%88%97%EF%BC%9AJSMA%E7%9B%AE%E6%A0%87%E6%89%B0%E5%8A%A8.html/</link>
      <guid>https://chaoge123456.github.io/%E5%AF%B9%E6%8A%97%E6%A0%B7%E6%9C%AC%E7%94%9F%E6%88%90%E7%B3%BB%E5%88%97%EF%BC%9AJSMA%E7%9B%AE%E6%A0%87%E6%89%B0%E5%8A%A8.html/</guid>
      <pubDate>Sat, 31 Aug 2019 12:07:56 GMT</pubDate>
      <description>
      
        
        
          &lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt; 在之前的博客中介绍了三种对抗样本的生成算法，分别是FGSM、DeepFool和Universal Perturbation。这三种算法生成的对抗样本样本有一个共同的特点：其对抗性样例没有具体的目标，即我们无法控制目标模型对对抗性样例
        
      
      </description>
      
      <content:encoded><![CDATA[<p><strong>摘要：</strong> 在之前的博客中介绍了三种对抗样本的生成算法，分别是FGSM、DeepFool和Universal Perturbation。这三种算法生成的对抗样本样本有一个共同的特点：其对抗性样例没有具体的目标，即我们无法控制目标模型对对抗性样例的分类结果。举例来说，如果我们构建了一个识别小动物的分类模型，现在我们需要对一张狗的照片生成其对抗性样例。先前的算法生成的对抗性样例只能达到让分类器分类错误的目的，比如说将其识别为一只鸡或者一只猫，分类结果是随机的、不可控的。更进一步，我们希望构建一种针对性更强的对抗性样例，比如说我们希望分类器对对抗性样例的分类结果只能是一只猫或者是由我们预先指定的一种小动物。本文介绍的<a href="https://arxiv.org/abs/1511.07528" target="_blank" rel="noopener">JSMA</a>算法可以达到这样一种目的。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/adversarial/aisecurity.png" alt="avatar"></p><h2 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h2><ul><li>论文解读<ul><li>攻击模型</li><li>JSMA算法</li></ul></li><li>JSMA代码实现</li></ul><h3 id="论文解读"><a href="#论文解读" class="headerlink" title="论文解读"></a>论文解读</h3><p>&emsp;&emsp;JSMA由Papernot等人提出，其论文发表于2016年S&amp;P。论文的主要工作包括：从攻击者的目标和背景知识（或能力）两个方面来构建攻击模型、提出JSMA对抗样本生成算法、对对抗性扰动进行度量并构建防御机制。</p><h4 id="攻击模型"><a href="#攻击模型" class="headerlink" title="攻击模型"></a>攻击模型</h4><p>&emsp;&emsp;分析攻击模型，主要从两个角度进行考虑：攻击者的目标和攻击者的背景知识。对于攻击者的目标而言，主要可以分为以下四类：</p><ul><li><strong>Confidence reduction：</strong>减小输入分类的置信度，从而引入歧义</li><li><strong>Misclassification：</strong>将输出分类更改为与原始类不同的任何类</li><li><strong>Targeted misclassification：</strong>生成输入，强制输出分类为特定目标类</li><li><strong>Source/target misclassification：</strong>强制将特定输入的输出分类强制为特定目标类</li></ul><p>对于攻击者的背景知识而言，主要可以分为以下五类：</p><ul><li><strong>Training data and network architecture：</strong>最强大的背景知识，包含训练数据以及模型的结构和详细的参数信息</li><li><strong>Network architecture：</strong>了解目标模型的网络架构</li><li><strong>Training data：</strong>了解生成目标模型的训练数据</li><li><strong>Oracle：</strong>攻击者可以访问模型提供的接口，输入数据并获得反馈，并且可以观察到输入和输出的变化之间的关系</li><li><strong>Sample：</strong>攻击者可以访问模型提供的接口，输入数据并获得反馈，但是不能观察到输入和输出的变化之间的关系</li></ul><p>针对不同的场景，攻击者将采用不同的攻击手法，这也是对抗样本领域研究的重点。</p><h4 id="JSMA算法"><a href="#JSMA算法" class="headerlink" title="JSMA算法"></a>JSMA算法</h4><p>&emsp;&emsp;JSMA算法的灵感来自于计算机视觉领域的显著图。简单来说，就是不同输入特征对分类器产生不同输出的影响程度不同。如果我们发现某些特征对应着分类器中某个特定的输出，我们可以通过在输入样本中增强这些特征来使得分类器产生指定类型的输出。JSMA算法主要包括三个过程：计算前向导数，计算对抗性显著图，添加扰动，以下给出具体解释。</p><p>&emsp;&emsp;所谓前向导数，其实是计算神经网络最后一层的每一个输出对输入的每个特征的偏导。以MNIST分类任务为例，输入的图片的特征数（即像素点）为784，神经网络的最后一层一般为10个输出（分别对应0-9分类权重），那对于每一个输出我们都要分别计算对784个输入特征的偏导，所以计算结束得到的前向导数的矩阵为（10，784）。前向导数标识了每个输入特征对于每个输出分类的影响程度，其计算过程也是采用链式法则。这里需要说明一下，前面讨论过的FGSM和DeepFool不同在计算梯度时，是通过对损失函数求导得到的，而JSMA中前向导数是通过对神经网络最后一层输出求导得到的。前向导数$\nabla \mathbf{F}(\mathbf{X})$具体计算过程如下所示，$j$表示对应的输出分类，$i$表示对应的输入特征。<br>$$<br>\nabla \mathbf{F}(\mathbf{X})=\frac{\partial \mathbf{F}(\mathbf{X})}{\partial \mathbf{X}}=\left[\frac{\partial \mathbf{F_j}(\mathbf{X})}{\partial x_i}\right]_{i \in 1 \ldots M, j \in 1 . . N}<br>$$</p><p>$$<br>\begin{aligned} \frac{\partial \mathbf{F_j}(\mathbf{X})}{\partial x_i}=&amp;\left(\mathbf{W}_{n+1, j} \cdot \frac{\partial \mathbf{H_n}}{\partial x_i}\right) \times \frac{\partial f_{n+1, j}}{\partial x_i}\left(\mathbf{W}_{n+1, j} \cdot \mathbf{H_n}+b_{n+1, j}\right) \end{aligned}<br>$$</p><p>&emsp;&emsp;通过得到的前向导数，我们可以计算其对抗性显著图，即对分类器特定输出影响程度最大的输入。首先，根据扰动方式的不同（正向扰动和反向扰动），作者提出了两种计算对抗性显著图的方式，即：</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/adversarial/Saliency_maps.PNG" alt="avatar"></p><p>但是在文章中第四部分的应用中作者发现，找到单个满足要求的特征很困难，所以作者提出了另一种解决方案，通过对抗性显著图寻找对分类器特定输出影响程度最大的输入特征对，即每次计算得到两个特征<br>$$<br>\arg \max _{\left(p_1, p_2\right)}\left(\sum_{i=p_1, p_2} \frac{\partial \mathbf{F_t}(\mathbf{X})}{\partial \mathbf{X_i}}\right) \times\left|\sum_{i=p_1, p_2} \sum_{j \neq t} \frac{\partial \mathbf{F_j}(\mathbf{X})}{\partial \mathbf{X_i}}\right|<br>$$<br>具体的计算过程可以参考文章中的算法3。（注：根据扰动方式的不同，这种算法也有两种计算对抗性显著图的方式）</p><p>&emsp;&emsp;根据对抗性显著图所得到的特征，可以对其添加扰动。扰动方式包括正向扰动和反向扰动（$+\theta$或$-\theta$）。如果添加的扰动不足以使分类结果发生转变，我们利用扰动后的样本可以重复上述过程（计算前向导数-&gt;计算对抗性显著图-&gt;添加扰动)。这个过程需要注意两点</p><ul><li>扰动过程的重复次数需要被约束，即修改的特征数有限</li><li>一旦添加扰动后，该特征达到临界值，那么该特征不再参与扰动过程</li></ul><h3 id="JSMA算法实现"><a href="#JSMA算法实现" class="headerlink" title="JSMA算法实现"></a>JSMA算法实现</h3><p>&emsp;&emsp;同样，为了便于理解，这里给出相关代码段（与之前的数据集和网络模型一致）。所有的代码基于python实现，使用的深度学习框架为pytorch，更加完整的算法实现参见<a href="https://github.com/chaoge123456/MLsecurity/tree/master/blog/JSMA" target="_blank" rel="noopener">我的github</a>。（注：本文JSMA算法实现部分代码参考了<a href="https://github.com/kleincup/DEEPSEC" target="_blank" rel="noopener">DEEPSEC</a>)</p><ul><li><p>网络模型</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Net</span><span class="params">(nn.Module)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self)</span>:</span></span><br><span class="line">        super(Net, self).__init__()</span><br><span class="line">        self.fc1 = nn.Linear(<span class="number">28</span>*<span class="number">28</span>, <span class="number">300</span>)</span><br><span class="line">        self.fc2 = nn.Linear(<span class="number">300</span>, <span class="number">100</span>)</span><br><span class="line">        self.fc3 = nn.Linear(<span class="number">100</span>, <span class="number">10</span>)</span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">forward</span><span class="params">(self, x)</span>:</span></span><br><span class="line">        x = F.relu(self.fc1(x))</span><br><span class="line">        x = F.relu(self.fc2(x))</span><br><span class="line">        x = self.fc3(x)</span><br><span class="line">        <span class="keyword">return</span> x</span><br></pre></td></tr></table></figure></li><li><p>数据集（这里只给出测试集的数据定义）</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 定义数据转换格式</span></span><br><span class="line">mnist_transform = transforms.Compose([transforms.ToTensor(), transforms.Lambda(<span class="keyword">lambda</span> x : x.resize_(<span class="number">28</span>*<span class="number">28</span>))])</span><br><span class="line"><span class="comment"># 导入数据，定义数据接口</span></span><br><span class="line">testdata  = torchvision.datasets.MNIST(root=<span class="string">"./mnist"</span>, train=<span class="keyword">False</span>, download=<span class="keyword">True</span>, transform=mnist_transform)</span><br><span class="line">testloader = torch.utils.data.DataLoader(testdata, batch_size=<span class="number">256</span>, shuffle=<span class="keyword">True</span>, num_workers=<span class="number">0</span>)</span><br></pre></td></tr></table></figure></li></ul><p>&emsp;&emsp;对于前向导数，如果根据链式法则一层层的推导，计算过程十分复杂。但是在深度学习框架中，对于反向传播过程的梯度的计算都已经封装好，我们只需要简单的调用即可，所以前向导数的计算过程十分简单，如下所示</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">compute_jacobian</span><span class="params">(model, input)</span>:</span></span><br><span class="line"></span><br><span class="line">    output = model(input)</span><br><span class="line">    num_features = int(np.prod(input.shape[<span class="number">1</span>:]))</span><br><span class="line">    jacobian = torch.zeros([output.size()[<span class="number">1</span>], num_features])</span><br><span class="line">    mask = torch.zeros(output.size())  <span class="comment"># chooses the derivative to be calculated</span></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> range(output.size()[<span class="number">1</span>]):</span><br><span class="line">        mask[:, i] = <span class="number">1</span></span><br><span class="line">        zero_gradients(input)</span><br><span class="line">        output.backward(mask, retain_graph=<span class="keyword">True</span>)</span><br><span class="line">        <span class="comment"># copy the derivative to the target place</span></span><br><span class="line">        jacobian[i] = input._grad.squeeze().view(<span class="number">-1</span>, num_features).clone()</span><br><span class="line">        mask[:, i] = <span class="number">0</span>  <span class="comment"># reset</span></span><br><span class="line">    <span class="keyword">return</span> jacobian</span><br></pre></td></tr></table></figure><p>&emsp;&emsp;计算对抗性显著图的过程也较为简单，一般根据算法的描述一步步实现并不难。但是在该过程中存在一个问题，我们每次需要找到满足要求的一对特征，那组合的方式一共有784*783种，如果采用普通的循环来实现计算代价很大。参考了DEEPSEC的代码，我发现他们将这个问题巧妙的转化为矩阵求解的问题，大大缩短了计算时间。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">saliency_map</span><span class="params">(jacobian, target_index, increasing, search_space, nb_features)</span>:</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    domain = torch.eq(search_space, <span class="number">1</span>).float()  <span class="comment"># The search domain</span></span><br><span class="line">    <span class="comment"># the sum of all features' derivative with respect to each class</span></span><br><span class="line">    all_sum = torch.sum(jacobian, dim=<span class="number">0</span>, keepdim=<span class="keyword">True</span>)</span><br><span class="line">    target_grad = jacobian[target_index]  <span class="comment"># The forward derivative of the target class</span></span><br><span class="line">    others_grad = all_sum - target_grad  <span class="comment"># The sum of forward derivative of other classes</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># this list blanks out those that are not in the search domain</span></span><br><span class="line">    <span class="keyword">if</span> increasing:</span><br><span class="line">        increase_coef = <span class="number">2</span> * (torch.eq(domain, <span class="number">0</span>)).float()</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        increase_coef = <span class="number">-1</span> * <span class="number">2</span> * (torch.eq(domain, <span class="number">0</span>)).float()</span><br><span class="line">    increase_coef = increase_coef.view(<span class="number">-1</span>, nb_features)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># calculate sum of target forward derivative of any 2 features.</span></span><br><span class="line">    target_tmp = target_grad.clone()</span><br><span class="line">    target_tmp -= increase_coef * torch.max(torch.abs(target_grad))</span><br><span class="line">    alpha = target_tmp.view(<span class="number">-1</span>, <span class="number">1</span>, nb_features) + target_tmp.view(<span class="number">-1</span>, nb_features, <span class="number">1</span>)  </span><br><span class="line">    <span class="comment"># calculate sum of other forward derivative of any 2 features.</span></span><br><span class="line">    others_tmp = others_grad.clone()</span><br><span class="line">    others_tmp += increase_coef * torch.max(torch.abs(others_grad))</span><br><span class="line">    beta = others_tmp.view(<span class="number">-1</span>, <span class="number">1</span>, nb_features) + others_tmp.view(<span class="number">-1</span>, nb_features, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># zero out the situation where a feature sums with itself</span></span><br><span class="line">    tmp = np.ones((nb_features, nb_features), int)</span><br><span class="line">    np.fill_diagonal(tmp, <span class="number">0</span>)</span><br><span class="line">    zero_diagonal = torch.from_numpy(tmp).byte()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># According to the definition of saliency map in the paper (formulas 8 and 9),</span></span><br><span class="line">    <span class="comment"># those elements in the saliency map that doesn't satisfy the requirement will be blanked out.</span></span><br><span class="line">    <span class="keyword">if</span> increasing:</span><br><span class="line">        mask1 = torch.gt(alpha, <span class="number">0.0</span>)</span><br><span class="line">        mask2 = torch.lt(beta, <span class="number">0.0</span>)</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        mask1 = torch.lt(alpha, <span class="number">0.0</span>)</span><br><span class="line">        mask2 = torch.gt(beta, <span class="number">0.0</span>)</span><br><span class="line">    <span class="comment"># apply the mask to the saliency map</span></span><br><span class="line">    mask = torch.mul(torch.mul(mask1, mask2), zero_diagonal.view_as(mask1))</span><br><span class="line">    <span class="comment"># do the multiplication according to formula 10 in the paper</span></span><br><span class="line">    saliency_map = torch.mul(torch.mul(alpha, torch.abs(beta)), mask.float())</span><br><span class="line">    <span class="comment"># get the most significant two pixels</span></span><br><span class="line">    max_value, max_idx = torch.max(saliency_map.view(<span class="number">-1</span>, nb_features * nb_features), dim=<span class="number">1</span>)</span><br><span class="line">    p = max_idx // nb_features</span><br><span class="line">    q = max_idx % nb_features</span><br><span class="line">    <span class="keyword">return</span> p, q</span><br></pre></td></tr></table></figure><p>&emsp;&emsp;算法其余的实现可以参考<a href="https://github.com/chaoge123456/MLsecurity/tree/master/blog/JSMA" target="_blank" rel="noopener">我的gihub</a>。代码完成后进行了一些测试，选取原始样本0，目标分类结果分别为0-9。我们采用JSMA算法对其添加噪声，得到的结果如下所示。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/adversarial/adv_flag1.png" alt="avatar"></p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/adversarial/adv_flag2.png" alt="avatar"></p><p>我们将这些图片分别丢入原始分类器中，分类结果与我们的目标全都一致。由此看来，一般情况下，JSMA算法的效果还是很不错的。</p>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/%E5%AF%B9%E6%8A%97%E6%A0%B7%E6%9C%AC%E7%94%9F%E6%88%90%E7%B3%BB%E5%88%97%EF%BC%9AJSMA%E7%9B%AE%E6%A0%87%E6%89%B0%E5%8A%A8.html/#disqus_thread</comments>
    </item>
    
    <item>
      <title>对抗样本生成系列：FGSM和DeepFool</title>
      <link>https://chaoge123456.github.io/%E5%AF%B9%E6%8A%97%E6%A0%B7%E6%9C%AC%E7%94%9F%E6%88%90%E7%B3%BB%E5%88%97%EF%BC%9AFGSM%E5%92%8CDeepfool.html/</link>
      <guid>https://chaoge123456.github.io/%E5%AF%B9%E6%8A%97%E6%A0%B7%E6%9C%AC%E7%94%9F%E6%88%90%E7%B3%BB%E5%88%97%EF%BC%9AFGSM%E5%92%8CDeepfool.html/</guid>
      <pubDate>Tue, 20 Aug 2019 06:11:01 GMT</pubDate>
      <description>
      
        
        
          &lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt;近些年来，深度学习技术在海量数据以及强大计算能力的驱动下取得了长足的发展，特别是在语音识别、计算机视觉、自然语言处理等领域，深度学习以其强大的网络表达能力刷新了一项又一项记录，各种各样基于深度学习的产品和服务也逐渐在产业界落地应用。正
        
      
      </description>
      
      <content:encoded><![CDATA[<p><strong>摘要：</strong>近些年来，深度学习技术在海量数据以及强大计算能力的驱动下取得了长足的发展，特别是在语音识别、计算机视觉、自然语言处理等领域，深度学习以其强大的网络表达能力刷新了一项又一项记录，各种各样基于深度学习的产品和服务也逐渐在产业界落地应用。正因为深度学习技术蕴含着巨大的商业价值，其背后潜在的安全问题更值得我们去深究。最近的研究表明，深度学习面临安全和隐私等多方面的威胁。该系列主要讨论深度学习领域最为热门的安全问题–对抗样本。本文讨论的FGSM和DeepFool是较为的早期对抗样本的生成算法，除此之外还会对DeepFool衍生出的Universal Perturbation进行解读。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/adversarial/panda.jpg" alt="avatar"></p><h2 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h2><ul><li>FGSM<ul><li>算法概述</li><li>代码实现</li></ul></li><li>DeepFool<ul><li>算法概述</li><li>代码实现</li></ul></li><li>Universal Perturbation </li></ul><h3 id="FGSM"><a href="#FGSM" class="headerlink" title="FGSM"></a>FGSM</h3><h4 id="算法概述"><a href="#算法概述" class="headerlink" title="算法概述"></a>算法概述</h4><p>&emsp;&emsp;在机器学习领域，对抗样本的问题始终存在。特别是在入侵检测、垃圾邮件识别等传统的安全应用场景，对抗样本的生成和识别一直是攻击者和防御者博弈的战场。2013年，Szegedy等人首次提出针对深度学习场景下的对抗样本生成算法–<a href="https://arxiv.org/abs/1312.6199" target="_blank" rel="noopener">BFGS</a>，作者认为，深度神经网络所具有的强大的非线性表达能力和模型的过拟合是可能产生对抗性样本原因之一。2014年，Goodfellow等人（包括Szegedy）对该问题进行了更深层次的研究，它们认为高维空间下深度神经网络的线性线性行为是导致该问题的根本原因。并根据该解释，设计出一种快速有效的生成对抗性样例的算法–<a href="https://arxiv.org/abs/1412.6572" target="_blank" rel="noopener">FGSM</a>。以下是对论文的解读。</p><p>&emsp;&emsp;对于线性模型$f(x)=w^Tx+b$来说，如果我们对输入$ x $添加一些扰动，使得$ \tilde{x}=x+\eta $。为了确保添加的扰动是极小的或者说是无法感知的，我们要求$ \left|\eta\right|_\infty&lt;\epsilon $。所以我们可以得到加噪后的模型输出为<br>$$<br>f(\tilde{x})=w^Tx+w^T\eta+b<br>$$<br>为了尽可能增大添加的噪声对输出结果的影响，令$\eta=\epsilon sign(w)$（<a href="https://baike.baidu.com/item/sign%E5%87%BD%E6%95%B0/1343199" target="_blank" rel="noopener">sign()函数的定义</a>)。如果$w$是一个$n$维的向量，每一维的均值为$m$，则$w^T\eta=\epsilon nm$。虽然$\epsilon$的值很小，但是当$w$的维度$n$很大时，$\epsilon nm$将是一个很大的值，这将会给模型的预测带来很大的影响。因此，高维特征和线性行为可以成为对抗性样本存在的一种解释。</p><p>&emsp;&emsp;上面的解释是基于线性模型而言的，而深度神经网络作为一种高度非线性模型，为什么会存在对抗性样例呢？深度神经网络的非线性单元赋予了其强大的表达能力，但是非线性单元的存在会降低学习的效率。为了提高学习效率，需要对非线性单元进行改进，通常的做法是通过降低其非线性来实现。从早期的sigmoid到tanh再到ReLU，我们会发现这些非线性单元的线性行为在不断增强，这也导致了深度神经网络中的线性能力的增强，这在一定程度上解释了深度神经网络中对抗性样例存在的原因。</p><p>&emsp;&emsp;根据这种解释，作者提出了生成对抗性样例的算法FGSM。我们将深度神经网络模型看作是一个线性模型，即类似于$f(x)=w^Tx+b$。在线性模型中，$w$为$f(x)$关于$x$的导数，而在深度神经网络模型中我们可以将$w$视为代价函数关于输入$x$的导数，即<br>$$<br>w=\nabla_{x}J(\theta,x,y)<br>$$</p><p>$$<br>\eta=\epsilon sign(\nabla_{x}J(\theta,x,y))<br>$$</p><p>&emsp;&emsp;在文章的后半部分主要介绍了针对对抗性样本的防御机制，即通过对抗性训练来提高模型的鲁棒性，这些内容会在之后的工作中讨论。</p><h4 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h4><p>&emsp;&emsp;为了更好的理解算法的实现过程，以下给出本文相关的代码段。这些代码段给出的定义，对于后面的DeepFool和Universal Perturbation算法的解读依赖有效。所有的代码基于python实现，使用的深度学习框架为pytorch，更加完整的算法实现参见<a href="https://github.com/chaoge123456/MLsecurity/tree/master/blog/FGSM%20and%20DeepFool" target="_blank" rel="noopener">我的github</a>.</p><ul><li><p>网络模型</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Net</span><span class="params">(nn.Module)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self)</span>:</span></span><br><span class="line">        super(Net, self).__init__()</span><br><span class="line">        self.fc1 = nn.Linear(<span class="number">28</span>*<span class="number">28</span>, <span class="number">300</span>)</span><br><span class="line">        self.fc2 = nn.Linear(<span class="number">300</span>, <span class="number">100</span>)</span><br><span class="line">        self.fc3 = nn.Linear(<span class="number">100</span>, <span class="number">10</span>)</span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">forward</span><span class="params">(self, x)</span>:</span></span><br><span class="line">        x = F.relu(self.fc1(x))</span><br><span class="line">        x = F.relu(self.fc2(x))</span><br><span class="line">        x = self.fc3(x)</span><br><span class="line">        <span class="keyword">return</span> x</span><br></pre></td></tr></table></figure></li><li><p>数据集（这里只给出测试集的数据定义）</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 定义数据转换格式</span></span><br><span class="line">mnist_transform = transforms.Compose([transforms.ToTensor(), transforms.Lambda(<span class="keyword">lambda</span> x : x.resize_(<span class="number">28</span>*<span class="number">28</span>))])</span><br><span class="line"><span class="comment"># 导入数据，定义数据接口</span></span><br><span class="line">testdata  = torchvision.datasets.MNIST(root=<span class="string">"./mnist"</span>, train=<span class="keyword">False</span>, download=<span class="keyword">True</span>, transform=mnist_transform)</span><br><span class="line">testloader = torch.utils.data.DataLoader(testdata, batch_size=<span class="number">256</span>, shuffle=<span class="keyword">True</span>, num_workers=<span class="number">0</span>)</span><br></pre></td></tr></table></figure></li><li><p>代价函数</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">loss_function = nn.CrossEntropyLoss()</span><br></pre></td></tr></table></figure></li></ul><p>&emsp;&emsp;FGSM算法的实现较为简单，其核心在于利用代价函数求解已知样本的梯度值。我们假设模型已经训练完成，首先我们加载已训练完成的深度网络模型</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net = torch.load(<span class="string">'mnist_net_all.pkl'</span>) <span class="comment"># 加载模型</span></span><br></pre></td></tr></table></figure><p>然后我们选择一个测试样本，针对该测试样本生成其对应的对抗性样例。其原始图片格式如图所示。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">index = <span class="number">100</span> <span class="comment"># 选择测试样本</span></span><br><span class="line">image = Variable(testdata[index][<span class="number">0</span>].resize_(<span class="number">1</span>,<span class="number">784</span>), requires_grad=<span class="keyword">True</span>) <span class="comment"># requires_grad存储梯度值</span></span><br><span class="line">label = torch.tensor([testdata[index][<span class="number">1</span>]])</span><br></pre></td></tr></table></figure><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/adversarial/origin.PNG" alt="avatar"></p><p>接着将测试样本作为网络模型的输入，通过前向传播的过程计算其损失，然后利用其损失进行反向传播（即求导）。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">outputs = net(image) <span class="comment"># 前向传播</span></span><br><span class="line">loss = loss_function(outputs, label) <span class="comment"># 计算损失</span></span><br><span class="line">loss.backward() <span class="comment"># 反向传播</span></span><br></pre></td></tr></table></figure><p>在深度学习框架中，反向传播的过程对用户来说是透明的。计算结束后，其梯度值存储在image.data.grad中，添加扰动的过程如下</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># FGSM添加扰动</span></span><br><span class="line">epsilon = <span class="number">0.1</span> <span class="comment"># 扰动程度</span></span><br><span class="line">x_grad = torch.sign(image.grad.data)</span><br><span class="line">x_adversarial = torch.clamp(image.data + epsilon * x_grad, <span class="number">0</span>, <span class="number">1</span>)</span><br></pre></td></tr></table></figure><p>添加扰动后，得到对抗性样本如下所示</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/adversarial/adversary_1.PNG" alt="avatar"></p><p>实验过程中我们发现，深度网络模型对于原始的图片能够正确的分类，而对于扰动之后的的样本不能正确分类（分类结果为2）。</p><h3 id="DeepFool"><a href="#DeepFool" class="headerlink" title="DeepFool"></a>DeepFool</h3><h4 id="算法概述-1"><a href="#算法概述-1" class="headerlink" title="算法概述"></a>算法概述</h4><p>&emsp;&emsp;FGSM算法能够快速简单的生成对抗性样例，但是它没有对原始样本扰动的范围进行界定（扰动程度$\epsilon$是人为指定的），我们希望通过最小程度的扰动来获得良好性能的对抗性样例。2016年，Seyed等人提出的<a href="https://www.cv-foundation.org/openaccess/content_cvpr_2016/html/Moosavi-Dezfooli_DeepFool_A_Simple_CVPR_2016_paper.html" target="_blank" rel="noopener">DeepFool</a>算法很好的解决了这一问题。文章的核心思想是希望找到一种对抗性扰动的方法来作为对不同分类器对对抗性扰动鲁棒性评估的标准。简单来说就是，现在我需要两个相同任务的分类器A、B针对同一个样本生成各自的对抗性样例。对于分类器A而言，其生成对抗性样例所需要添加的最小扰动为$a$；对于分类器B而言，其生成对抗性样例所需要添加的最小扰动为$b$；通过对$a$、$b$的大小进行比较，我们就可以对这两个分类器对对抗性样例的鲁棒性进行评估。由于FGSM产生扰动是人为界定的，所以它不能作为评估的依据。DeepFool可以生成十分接近最小扰动的对抗性样例，因此它可以作为衡量分类器鲁棒性的标准。</p><p>&emsp;&emsp;DeepFool源于对分类问题的思考。对于如图所示的线性二分类问题，令$f(x)=w^Tx+b$，其中$\mathscr{F}={x : f(x)=0}$。此时$x_0$位于直线的下方，即$f(x_0)&gt;0$。现在我们希望对$x_0$添加扰动$r$，使得分类器$f(x_0+r)&lt;0$。那么如何添加扰动才能使得扰动的程度最小呢？这个问题可以转化为求点到直线之间的距离，我们通过$x_0$做直线$\mathscr{F}$的垂线，与$\mathscr{F}$相交于$p$（投影点），则$p$与$x_0$之间的距离即为$x_0$到分类边界$\mathscr{F}$的最短距离。所以当我们沿着分类边界法线方向对$x_0$进行扰动，可以保证扰动的程度最小<br>$$<br>r(x_0):={\arg\min_k}{|{r}|_{2}}=-{\frac{f(x_0)}{|{w}|_{2}^{2}}{w}}<br>$$<br>添加扰动之后将$x_0$映射到分类边界的投影点$p$，即$p=x_0+r(x_0)$。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/adversarial/linear.PNG" alt="avatar"></p><p>&emsp;&emsp;同样，对于非线性的二分类问题（分类边界为曲线或者曲面），我们也需要计算从目标样本点到分类边界的最短距离。这个计算过程较为复杂，一般采用垂直逼近法来逐步的逼近$x_0$在分类边界上的投影点，所以在论文中算法1会有一个迭代过程。值得注意的是，我们得到最终的$\sum_i{r_i}$表示的是从$x_0$到分类边界投影点的距离向量，满足$f(x_0+\sum_i{r_i})=0$。如果要使分类结果改变，需要再添加一些极小的扰动，如$f(x_0+(1+\eta)\sum_i{r_i})&lt;0$（文中$\eta$取0.02）。</p><p>&emsp;&emsp;对于多分类任务，思路也大致相同。在线性多分类任务中，需要注意的大致有两点：首先，对于多分类任务的分类边界要重新进行定义。我们令$f(x)$为分类器，$\hat{k}(x)=\mathop{\arg\max}_{k}{f_k(x)}$表示其对应的分类结果（一共有$k$个类别）。分类边界定义为<br>$$<br>\mathscr{F}_k={x : f_k(x)-f_{\hat{k}(x_0)}(x)=0}<br>$$<br>其次，由于分类边界有多个，我们需要以此求出$x_0$到每个分类边界的距离（类似于进行多次二分类的计算过程），然后进行比较，选择其中最短距离向量作为最终的扰动。<br>$$<br>\hat{l}(x_0)=\mathop{\arg\min_{k \neq \hat{k}_{x_0} }}{\frac{|f_k(x_0)-f_{\hat{k}(x_0)}(x_0)|}{|{w_k -w_{\hat{k}_{x_0}}}|_2}}<br>$$<br>之后通过计算$\boldsymbol{r}(x_0)$得到$x_0$在分类边界上的投影。</p><p>&emsp;&emsp;在非线性多分类任务中，与线性多分类的区别在于其分类边界是不确定的，所以我们需要采用类似于非线性二分类任务中的方法来逼近分类边界。获得分类边界之后的计算与线性多分类的过程类似。此后，重复该过程多次，即可获得最终$x_0$在分类边界的投影。</p><h4 id="代码实现-1"><a href="#代码实现-1" class="headerlink" title="代码实现"></a>代码实现</h4><p>&emsp;&emsp;以下讨论多分类情况下DeepFool算法的代码实现，模型结构以及数据集等与上述一致，不再重复。首先，我们选择一个测试样本，生成该样本的对抗性样例。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">net = torch.load(<span class="string">'mnist_net_all.pkl'</span>) <span class="comment"># 加载模型</span></span><br><span class="line">index = <span class="number">100</span> <span class="comment"># 选择测试样本</span></span><br><span class="line">image = Variable(testdata[index][<span class="number">0</span>].resize_(<span class="number">1</span>,<span class="number">784</span>), requires_grad=<span class="keyword">True</span>)</span><br><span class="line">label = torch.tensor([testdata[index][<span class="number">1</span>]])</span><br></pre></td></tr></table></figure><p>接下来，通过前向传播的过程，我们获得该样本对每一类别可能的取值情况，并将其从大到小排列起来，列表$I$对应其索引值，$label$即$\hat{k}_{x_0}$。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">f_image = net.forward(image).data.numpy().flatten() </span><br><span class="line"><span class="comment"># f_image: [-1.1256416 , -1.0344085 ,  2.0596995 , -2.0181773 , -0.24274658, -0.53373957,  6.6361637 , -2.250309  ,  0.06580263, -3.0854702 ]</span></span><br><span class="line"></span><br><span class="line">I = (np.array(f_image)).flatten().argsort()[::<span class="number">-1</span>]</span><br><span class="line"><span class="comment"># I: [6, 2, 8, 4, 5, 1, 0, 3, 7, 9]</span></span><br><span class="line"></span><br><span class="line">label = I[<span class="number">0</span>] <span class="comment"># 该样本的标签为6</span></span><br></pre></td></tr></table></figure><p>然后，我们定义一些需要用到的变量</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">input_shape = image.data.numpy().shape <span class="comment"># 获取原始样本的维度</span></span><br><span class="line">pert_image = copy.deepcopy(image) <span class="comment"># 深度复制原始样本</span></span><br><span class="line">w = np.zeros(input_shape) </span><br><span class="line">r_tot = np.zeros(input_shape)</span><br><span class="line"></span><br><span class="line">loop_i = <span class="number">0</span> </span><br><span class="line">max_iter = <span class="number">50</span> <span class="comment"># 最多迭代次数</span></span><br><span class="line">overshoot = <span class="number">0.02</span>  </span><br><span class="line">x = Variable(pert_image, requires_grad=<span class="keyword">True</span>)</span><br><span class="line">fs = net.forward(x)</span><br><span class="line">fs_list = [fs[<span class="number">0</span>][I[k]] <span class="keyword">for</span> k <span class="keyword">in</span> range(len(I))] <span class="comment"># 每个类别的取值情况，及其对应的梯度值</span></span><br><span class="line">k_i = label</span><br></pre></td></tr></table></figure><p>下面是算法实现的核心部分，参考论文中的伪代码，其中orig_grad表示$\nabla f_{\hat{k}_{x_0}}(x_i)$，cur_grad表示$\nabla f_k(x_i)$，fs[0][I[k]]表示$f_k(x_i)$，fs[0][I[0]]表示$f_{\hat{k}_{x_0}}(x_i)$。通过内部的for循环可以获得x到各分类边界的距离；在外部的while循环中，我们利用内部循环获得的所有边界距离中的最小值对x进行更新。重复这一过程，直到$x$的分类标签发生变化。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> k_i == label <span class="keyword">and</span> loop_i &lt; max_iter:</span><br><span class="line">    pert = np.inf</span><br><span class="line">    fs[<span class="number">0</span>][I[<span class="number">0</span>]].backward(retain_graph=<span class="keyword">True</span>)</span><br><span class="line">    orig_grad = x.grad.data.numpy().copy() </span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> k <span class="keyword">in</span> range(len(I)):</span><br><span class="line">        zero_gradients(x)</span><br><span class="line">        fs[<span class="number">0</span>][I[k]].backward(retain_graph=<span class="keyword">True</span>)</span><br><span class="line">        cur_grad = x.grad.data.numpy().copy()</span><br><span class="line">        </span><br><span class="line">        w_k = cur_grad - orig_grad</span><br><span class="line">        f_k = (fs[<span class="number">0</span>][I[k]] - fs[<span class="number">0</span>][I[<span class="number">0</span>]]).data.numpy()</span><br><span class="line">        </span><br><span class="line">        pert_k = abs(f_k) / np.linalg.norm(w_k.flatten())</span><br><span class="line">        <span class="keyword">if</span> pert_k &lt; pert: <span class="comment"># 获得最小的分类边界距离向量</span></span><br><span class="line">            pert = pert_k</span><br><span class="line">            w = w_k</span><br><span class="line">    r_i = (pert + <span class="number">1e-4</span>) * w / np.linalg.norm(w) </span><br><span class="line">    r_tot = np.float32(r_tot + r_i) <span class="comment"># 累积扰动</span></span><br><span class="line">    </span><br><span class="line">    pert_image = image + (<span class="number">1</span>+overshoot)*torch.from_numpy(r_tot) <span class="comment"># 添加扰动</span></span><br><span class="line">    x = Variable(pert_image, requires_grad=<span class="keyword">True</span>)</span><br><span class="line">    fs = net.forward(x)</span><br><span class="line">    k_i = np.argmax(fs.data.numpy().flatten()) <span class="comment"># 扰动后的分类标签</span></span><br><span class="line">    loop_i += <span class="number">1</span></span><br><span class="line">r_tot = (<span class="number">1</span>+overshoot)*r_tot <span class="comment"># 最终累积的扰动</span></span><br></pre></td></tr></table></figure><p>对原始图片，添加扰动获得如下图片（分类器将其错误分类为2，有些奇怪的地方在于其扰动的程度要大FGSM，算法应该没有问题，一直没找到原因）。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/adversarial/deepfool.PNG" alt="avatar"></p><h3 id="Universal-Perturbation"><a href="#Universal-Perturbation" class="headerlink" title="Universal Perturbation"></a>Universal Perturbation</h3><p>&emsp;&emsp;前面介绍的FGSM和DeepFool算法，它们都是针对单个样本生成其对抗性样例，也就是说每个对抗性样例的扰动程度都不同。那么是否能找到一种通用性的扰动边界，能够为不同的样本生成对抗性样例。在DeepFool工作的基础上，Seyed 等人提出了Universal Perturbation，该算法为寻找通用性扰动边界提供了可能。以下简单介绍一下论文的核心思想：</p><ul><li>从数据集中随机选取部分测试样本作为生成通用性扰动边界的范例，通过这些测试样本生成的通用性扰动适用于整个数据集。</li><li>算法的计算过程：输入第一个样本后，通过DeepFool算法找到该样本的最小扰动距离向量，将其累积到通用扰动向量$v$中（对$v$的扰动程度会有限制和调整，${|v|_p&lt;\xi}$）；当输入第二个样本之后，对其添加$v$的扰动之后，然后再通过DeepFool计算扰动后的样本的最小扰动距离向量，将其累积到通用扰动向量$v$中（对$v$的扰动程度会有限制和调整,${|v|_p&lt;\xi}$）。重复这一过程，直到最后一个测试样本。然后，我们使用通用扰动向量$v$，对原始的测试样本进行扰动，测试其生成对抗性样例的成功率。如果小于预先设置的阈值$1-\delta$，则跳出循环返回结果。否则，重复上述过程。</li><li>通用性扰动对不同的网络模型依然有效。也就是说，利用网络模型A生成的通用性扰动，同样适用于生成网络模型B的对抗性样例（A、B是不同类型的网络架构）。</li></ul>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/%E5%AF%B9%E6%8A%97%E6%A0%B7%E6%9C%AC%E7%94%9F%E6%88%90%E7%B3%BB%E5%88%97%EF%BC%9AFGSM%E5%92%8CDeepfool.html/#disqus_thread</comments>
    </item>
    
    <item>
      <title>校园网环境下服务器双网卡配置</title>
      <link>https://chaoge123456.github.io/%E6%A0%A1%E5%9B%AD%E7%BD%91%E7%8E%AF%E5%A2%83%E4%B8%8B%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8F%8C%E7%BD%91%E5%8D%A1%E9%85%8D%E7%BD%AE.html/</link>
      <guid>https://chaoge123456.github.io/%E6%A0%A1%E5%9B%AD%E7%BD%91%E7%8E%AF%E5%A2%83%E4%B8%8B%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8F%8C%E7%BD%91%E5%8D%A1%E9%85%8D%E7%BD%AE.html/</guid>
      <pubDate>Sun, 21 Apr 2019 06:02:31 GMT</pubDate>
      <description>
      
        
        
          &lt;hr&gt;
&lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt;最近一段时间在忙着写论文、看论文，博客一直没有更新了。这几天实验室添了两台交换机和五台服务器，这对于我这个爱折腾的人来说，确实是个大喜事，老师也把实验室设备的管理工作全权交予我来负责。忙活了几天时间，装好了系统，建立了一个小
        
      
      </description>
      
      <content:encoded><![CDATA[<hr><p><strong>摘要：</strong>最近一段时间在忙着写论文、看论文，博客一直没有更新了。这几天实验室添了两台交换机和五台服务器，这对于我这个爱折腾的人来说，确实是个大喜事，老师也把实验室设备的管理工作全权交予我来负责。忙活了几天时间，装好了系统，建立了一个小型的Hadoop集群和私有云。这其中我觉得最有意思的是关于实验室网络环境的配置，所以在这里做一些分享。</p><hr><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/network/tcp.jpg" alt="avatar"></p><h2 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h2><ul><li><p>网络环境简介</p></li><li><p>双网卡配置</p></li><li>Linux密码</li></ul><h3 id="网络环境简介"><a href="#网络环境简介" class="headerlink" title="网络环境简介"></a>网络环境简介</h3><p>&emsp;&emsp;一开始，实验室的服务器都没有分配校园网固定IP，为了对服务器进行安装配置，我用了一台闲置的交换机和一个二手路由器组建了一个简单的网络环境（服务器—&gt;交换机—&gt;路由器）。但是存在的问题是，服务器只能在实验室的网络环境才能访问，无法通过校园网访问。一些内网映射的工具又不太稳定，使用起来很不方便，最关键的是路由器成为了整个网络的瓶颈（几十块钱的路由器）。后来通过学校网络中心分配了几个固定的校园网IP，但是校园网IP不能直接连接外网，需要拨号才能联网。五台服务器也就意味着需要五个账号才能使所有的服务器同时连接外网，我也没有这么多账号。仔细考虑了一下我们当前的需求和配置：</p><ul><li>所有的服务器能通过校园网直接访问</li><li>所有的服务器能同时连接外网</li><li>宽带账号只有一个，路由器一个，交换机一个</li></ul><p>综合这些因素，配置服务器双网卡可能是一个好的选择。每台服务器都有多个网口，将每台服务器同时连接在校园网和路由器两个网络中就能满足这些需求。通过给连接在校园网的网口配置校园网固定IP，可以保证校园网对服务器的访问；通过路由器拨号上网，将服务器的另一个网口连接到路由器上，即可保证服务器对外网的访问。</p><h3 id="双网卡配置"><a href="#双网卡配置" class="headerlink" title="双网卡配置"></a>双网卡配置</h3><p>&emsp;&emsp;配置过程需要注意以下几点：</p><ul><li>再给两个网卡配置静态IP的时候，不需要GATEWAY字段</li><li>删除系统默认的网关（在 /etc/sysconfig/network文件中修改）</li><li>使用ip route命令添加默认网关 （ip route add default via 网关地址 dev 网卡1）。一般我们将路由器的网关地址作为服务器的默认网关，这样能保证服务器可以访问外网。</li><li>添加默认路由（ip route add 校园网网段 via 校园网静态ip的网关地址 dev 网卡2）。为了在校园网内部可以对服务器进行访问，我们需要添加一条默认的路由。对于校园网网段的访问，不通过路由器，而是通过校园网静态ip的网关地址。</li></ul><h3 id="Linux密码"><a href="#Linux密码" class="headerlink" title="Linux密码"></a>Linux密码</h3><p>&emsp;&emsp;之前一个同学在实验室主机上装过centos，可能太长时间没用，所以root密码忘了。找回LInux密码的过程挺有意思，一般情况下，通过单用户模式可以对root密码进行修改，但是系统设置了grub密码（防止修改用户密码）。至于grub密码就更不记得了，于是我们又想通过救援模式去修改grub密码，然后再修改root密码。参考了很多教程，我们发现修改不了grub密码。就在准备放弃的时候，我发现救援模式下可以直接访问/etc/shadow文件，我试着将文件中root用户记录删除，竟然修改成功了。重新开机之后，奇迹发生了，可以使用root用户直接登录了。这里给出<a href="https://www.tecmint.com/recover-or-rescue-corrupted-grub-boot-loader-in-centos-7/" target="_blank" rel="noopener">参考的博客链接</a>，希望对大家有所帮助。</p>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/%E6%A0%A1%E5%9B%AD%E7%BD%91%E7%8E%AF%E5%A2%83%E4%B8%8B%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8F%8C%E7%BD%91%E5%8D%A1%E9%85%8D%E7%BD%AE.html/#disqus_thread</comments>
    </item>
    
    <item>
      <title>tor匿名网络</title>
      <link>https://chaoge123456.github.io/Tor%E5%8C%BF%E5%90%8D%E7%BD%91%E7%BB%9C.html/</link>
      <guid>https://chaoge123456.github.io/Tor%E5%8C%BF%E5%90%8D%E7%BD%91%E7%BB%9C.html/</guid>
      <pubDate>Wed, 28 Nov 2018 19:14:32 GMT</pubDate>
      <description>
      
        
        
          &lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt; 前段时间看到一篇报道，暗网最大的托管商遭到黑客攻击，6500+网站被删。根据知道创宇平台的暗网雷达显示，一夜之间活跃在暗网中的网站从12000+下降到5000+，几乎遭到团灭，这一事件让暗网再次进入人们的视野。一直以来，暗网被赋予了
        
      
      </description>
      
      <content:encoded><![CDATA[<p><strong>摘要：</strong> 前段时间看到一篇报道，暗网最大的托管商遭到黑客攻击，6500+网站被删。根据知道创宇平台的暗网雷达显示，一夜之间活跃在暗网中的网站从12000+下降到5000+，几乎遭到团灭，这一事件让暗网再次进入人们的视野。一直以来，暗网被赋予了很多传奇的色彩，本文将带你一步步揭开暗网的神秘面纱。</p><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/tor.jpg" alt="avatar"></p><hr><h2 id="文章概览"><a href="#文章概览" class="headerlink" title="文章概览"></a>文章概览</h2><ul><li>基本概念<ul><li>深网</li><li>暗网</li><li>黑暗网络</li></ul></li><li>tor匿名网络 <ul><li>tor基本概述</li><li>tor网络结构</li><li>tor路由技术</li><li>tor匿名服务</li></ul></li><li>tor客户端代理</li></ul><h3 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h3><p>&emsp;&emsp;在介绍暗网之前，我们先来了解一下经常会混淆的三个概念“深网”(Deep web)、“暗网”(Dark web) 和“黑暗网络”(Darknet) 。</p><h4 id="深网"><a href="#深网" class="headerlink" title="深网"></a>深网</h4><p>&emsp;&emsp;深网是指服务器上可通过标准的网络浏览器和连接方法访问的页面和服务，但主流搜索引擎不会收录这些页面和服务。搜索引擎之所以不会收录深网，通常是因为网站或服务的配置错误、拒绝爬虫爬取信息、需要付费查看、需要注册查看或其他内容访问限制。据不完全统计，互联网世界中只有4％是对公众开放，剩下的96％的网站和数据则隐藏在深网中。</p><h4 id="暗网"><a href="#暗网" class="headerlink" title="暗网"></a>暗网</h4><p>&emsp;&emsp;暗网是深网中相对较小的一部分，与被故意隐藏的 Web 服务和页面有关。仅使用标准浏览器无法直接访问这些服务和页面，必须依靠使用覆盖网络 (Overlay Network)；而这种网络需要特定访问权限、代理配置、专用软件或特殊网络协议。</p><h4 id="黑暗网络"><a href="#黑暗网络" class="headerlink" title="黑暗网络"></a>黑暗网络</h4><p>&emsp;&emsp;黑暗网络是在网络层访问受限的框架，例如 tor 或 I2P，私有 VPN 也属于这个类别。通过这些框架的网络流量会被屏蔽。当进行数据传输时，系统只会显示您连接的黑暗网络以及您传输了多少数据，而不一定会显示您访问的网站或所涉及数据的内容。与之相反的是，直接与明网（Clean Net）或与未加密的表网服务和深网服务交互。在这种情况下，您与所请求资源之间的互联网服务提供商和网络运营商可以看到您传输的流量内容。</p><h3 id="tor匿名网络"><a href="#tor匿名网络" class="headerlink" title="tor匿名网络"></a>tor匿名网络</h3><h4 id="基本概述"><a href="#基本概述" class="headerlink" title="基本概述"></a>基本概述</h4><p>&emsp;&emsp;经过上面的描述，我们对暗网整体的框架有个大致的了解。暗网不同于普通的互联网络，它拥有特殊的运作方式和网络协议，tor 是目前世界范围内是最大的暗网。tor又名洋葱网络，最初是由美国军方的情报机构开发并且提供财务支持的。洋葱网络使用特殊的路由转发技术，即洋葱路由技术。洋葱路由技术利用 P2P 网络,把网络流量随机地通过 P2P 的节点进行转发，这样可以掩盖源地址与目标地址的路径，使得在 Internet 上难以确定使用者的身份和地址。这就类似于你给某人送一封匿名信，你不是自己去送或者通过邮局的邮差去送，而是在大街上随便找几个不认识的人让他帮你送，这样收信人就很难往回追踪找到你。</p><p>&emsp;&emsp;洋葱路由项目最初进展缓慢,废弃了其中的几个版本。直到 2002 年才由麻省理工的两位毕业生 Roger Dingledine 和 Nick Mathewson 以及原项目组成员 PaulSyverson 一起开发出一个新版的洋葱路由，也就是Tor。“洋葱路由”的最初目的并不是保护大众的隐私，它的目的是帮助政府情报人员隐藏身份，网上活动不被敌对国家监控。后来为了混淆系统的流量，而不是让系统仅仅拥有来自美国安全部门网络的流量，他们又让系统中加入来自其它网络的流量。于是，tor 的普通版本被推广给了普通大众，让普通大众也能使用 tor 来保护自己的隐私。这样就可以把政府情报人员的流量与社会上各行各业用户的复杂流量混在一起，能让流量分析更加困难，提供更强地隐私保护。</p><p>&emsp;&emsp;目前全球范围内，tor网络由超过一万七千个中继节点组成，每个中继节点都是由全球志愿者免费提供，这些中继节点大部分分布在欧洲和北美。暗网除了能给我们提供匿名的网络服务之外，在按暗网中也存在各种类型的网站，这些网站只有通过暗网才能够访问。暗网中的网站大部分都是提供一些“特殊的服务”，包括枪支、毒品、网络攻击等等。</p><h4 id="网络结构"><a href="#网络结构" class="headerlink" title="网络结构"></a>网络结构</h4><p>Tor 匿名网络结构主要由目录服务器、洋葱代理、洋葱路由器这三部分组成。</p><ul><li><strong>目录服务器：</strong>它负责收集和更新网络中所有可运行的中继节点信息，为客户端提供节点公钥等建立链路所需的必要信息。目前，绝大多数的目录服务器在欧美地区,只有极少数分布在亚洲。</li><li><strong>洋葱代理：</strong>它是 tor 用户的客户端代理程序，负责下载目录服务器中的路由信息，选择节点和建立路径，并对通信信息进行加解密。</li><li><strong>洋葱路由器（又称中继节点）：</strong>它是构建匿名通信链路的基础，负责转发 tor 客户端和网络服务器的通信信息，是实现匿名通信的关键。目前整个 tor 网络中有几千个路由节点分布在世界各地，这些中继节点又分为三种类型:<ul><li>Entry/Guard 中继节点──这是 tor 网络的入口节点。这些中继节点运行一段时间后，如果被证明是稳定的，并具有高带宽，就会被选来作为 Guard 中继节点。</li><li>Middle 中继节点──Middle 中继节点是位于中间节点位置上的洋葱路由器，充当流量从 Guard 中继节点传输到 Exit 中继节点的桥梁，这可以避免 Guard 中继节点和 Exit 中继节点探查到彼此的位置。</li><li>Exit 中继节点──位于出口节点位置上的洋葱路由器，负责将 Tor 网络内的流量转发到网络外部的互联网中去。每个出口节点都有一个相关的出口政策，该政策规定该节点能通过哪个端口转发何种协议的流量来防止滥用 tor 网络。</li></ul></li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/tor_structure.jpeg" alt="avatar"></p><p>&emsp;&emsp;tor 网络主要依赖于这些中继节点转发用户流量，tor 通过随机选取遍布于全球由志愿者运行的三个中继节点，然后分别与选择的入口节点、中间节点、出口节点协商会话密钥。用这些协商的密钥将通信数据先进行多层加密，然后再将加密的数据在三个洋葱路由器组成的通信链路上传送。数据每经过一个洋葱路由器就像是剥去一层洋葱皮一样解密去掉一个加密层，以此得到下一跳路由信息，然后将数据继续发往下一个洋葱路由器，不断重复此过程,直到数据送达目的地。这种转发方式能防止那些知道数据发送端以及接收端的中间人窃听数据内容。</p><h4 id="tor路由技术"><a href="#tor路由技术" class="headerlink" title="tor路由技术"></a>tor路由技术</h4><p>以下步骤讲述了Alice在使用tor与Bob的服务器进行通讯时，tor是如何工作的。</p><ul><li>Alice开启tor客户端代理，获取来自tor 目录服务器（Dave）中的tor节点（或中继的列表）以及它们的公钥。</li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/tor_work1.png" alt="avatar"></p><ul><li>Alice 选择三个节点建立通信链路。Alice 得到入口中继节点的 IP 地址和身份摘要，和入口节点协商一个只用于两者之间通信的短暂会话密钥。成功建立一跳的链路以后，Alice 使用同样地方法要求入口节点拓展链路到中间节点，得到了 Alice与中间节点的短暂会话密钥。Alice 重复此过程直至建立一条含有三跳的通信链路并且获得三个她与三个节点独一无二的短暂会话密钥。整个链路建立过程中的所有连接都是加密的。然后Alice 向 Bob 发送服务请求，并且将请求内容使用三个会话密钥按由远到近的顺序依次加密。入口节点接收到解密消息后会使用会话密钥将其解密一层，并将仍然加密的信息转发给中间节点。直至到达出口节点后，才将信息解密为明文。图中的虚线表示出口节点与 Bob 之间的连接是未加密的，出口节点将原始数据发送给 Bob。Bob 回复请求内容，并且每个节点会以相反的加密顺序加密一层,最终送还给 Alice。</li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/tor_work2.png" alt="avatar"></p><ul><li>如果 Alice 与 Bob 通信时间较长，Alice 每隔几分钟会重新选择三个节点建立新的通信链路以防攻击者窃听。如果 Alice 想要访问另一服务器 Jane，她也会重新选择中继节点建立新的通信链路。</li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/tor_work3.png" alt="avatar"></p><h4 id="tor匿名服务"><a href="#tor匿名服务" class="headerlink" title="tor匿名服务"></a>tor匿名服务</h4><p>&emsp;&emsp;之前提到暗网中也存在很多网站，这些网站在提供相应的服务时（暗网中大部分网络服务都是违法的）也希望是匿名的，即用户无法追踪到关于该网站的相关信息。下面主要讲解用户Alice和匿名服务器Bob交互的具体过程。</p><ul><li>服务器Bob与tor网络中的一些中继节点连接，请求这些中继节点作为匿名服务的接入点，并将Bob的公钥发送给这些中继节点。注意Bob和中继节点之间的连接也是匿名的，这些中继节点无法获取关于Bob的相关位置信息。</li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/THS-1.png" alt="avatar"></p><ul><li>Bob将之前建立的接入点的相关信息和自己的公钥组装成描述符，并用自己的私钥该描述符进行签名，然后将其发送到目录服务器。通过Bob的公钥可以生成一个16位的字符串，记为XYZ。当客户端请求XYZ.onion时就可以找到对应Bob的描述符。</li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/THS-2.png" alt="avatar"></p><ul><li>Alice通过某些渠道获得了tor域名XYZ.onion，Alice想要访问该服务器。她通过tor客户端访问XYZ.onion，此时可以从目录服务器中获取对应服务器的描述符，通过描述符可以知道Bob服务器的接入点和公钥。与此同时，Alice会提前建立另外一条私密临时会话点，用于下一步与Bob交互。</li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/THS-3.png" alt="avatar"></p><ul><li>当确认描述符存在且临时通道准备好之后，Alice用描述符中的公钥加密一条信息，包括临时会话点和会话秘钥，将加密后的信息发送给描述符中的接入点，之后接入点会将加密信息发送到对应的服务器（即Bob的服务器）。</li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/THS-4.png" alt="avatar"></p><ul><li>Bob收到加密信息后将其解密，获取临时会话点和会话秘钥。然后和临时会话点建立匿名连接，连接成功后，临时会话点会通知Alice。之后Alice和Bob可以通过临时会话点进行通信，注意通信过程中建立的连接都是匿名的。</li></ul><p><img src="https://githubpage-1255710107.cos.ap-shanghai.myqcloud.com/static/images/tor/THS-5.png" alt="avatar"></p><h3 id="tor客户端代理"><a href="#tor客户端代理" class="headerlink" title="tor客户端代理"></a>tor客户端代理</h3><p>&emsp;&emsp;如果只是希望匿名浏览web网页，我们可以通过<a href="https://www.torproject.org/projects/torbrowser.html.en" target="_blank" rel="noopener">tor浏览器</a>来实现。tor浏览器是基于火狐浏览器改造而来，可以方便的帮助我们连接到tor，实现网络匿名。如果想要在更多应用中实现网络匿名，我们可以安装tor客户端代理。以下配置过程基于ubuntu，且默认已安装shadowsocks。</p><h4 id="安装polipo"><a href="#安装polipo" class="headerlink" title="安装polipo"></a>安装polipo</h4><p>&emsp;&emsp;polipo是轻量级的跨平台代理服务器，可以实现http和socks代理，polipo本地服务端口为8123。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt-get install polipo</span><br></pre></td></tr></table></figure><p>安装完成后，修改配置文件/etc/polipo/config</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">socksParentProcy = "localhost:9050" #tor服务本地端口为9050</span><br><span class="line">socksProxyType = socks5</span><br></pre></td></tr></table></figure><h4 id="安装tor"><a href="#安装tor" class="headerlink" title="安装tor"></a>安装tor</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt-get install tor</span><br></pre></td></tr></table></figure><p>安装完成后，修改配置文件/etc/tor/torrc，添加以下内容。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SOCKS5Proxy 127.0.0.1:1080 #shadowsocks本地服务端口为1080</span><br></pre></td></tr></table></figure><h4 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h4><p>&emsp;&emsp;完成上述操作后，开启polipo、tor和shadowsocks。如果我们希望在chrome浏览器中实现网络匿名，可以通过添加tor代理来实现。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">代理协议：socks5    代理服务器：127.0.0.1    代理端口：9050</span><br></pre></td></tr></table></figure><p>如果我们希望在命令行中实现网络匿名，可以通过polipo代理来</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http_proxy=http://localhost:8123 "需要执行的操作“</span><br></pre></td></tr></table></figure>]]></content:encoded>
      
      <comments>https://chaoge123456.github.io/Tor%E5%8C%BF%E5%90%8D%E7%BD%91%E7%BB%9C.html/#disqus_thread</comments>
    </item>
    
  </channel>
</rss>
