<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Coding - Yifeitao ｜ 次生進化</title><link>https://yifeitao.com/ja/categories/coding/</link><description>創造と学習と投資について</description><generator>Hugo -- gohugo.io</generator><language>ja</language><lastBuildDate>Tue, 23 Apr 2019 00:00:00 +0000</lastBuildDate><atom:link href="https://yifeitao.com/ja/categories/coding/index.xml" rel="self" type="application/rss+xml"/><item><title>蛙とキャッシュ：実用的に簡略化した動的計画法</title><link>https://yifeitao.com/ja/frog-and-cache/</link><pubDate>Tue, 23 Apr 2019 00:00:00 +0000</pubDate><guid>https://yifeitao.com/ja/frog-and-cache/</guid><description>&lt;h2 id="1-一匹の蛙から"&gt;1. 一匹の蛙から&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://i.imgur.com/zuhczaLm.jpg" alt=" " loading="lazy" decoding="async" referrerpolicy="no-referrer" /&gt;
&lt;/p&gt;
&lt;p&gt;ある蛙が、&lt;code&gt;n&lt;/code&gt;段の階段を跳び下りて池に入ろうとしている。1回で1段あるいは2段だけ跳べるとすると、池に降りる跳び方は全部で何通りあるだろうか（たとえば&lt;code&gt;n=30&lt;/code&gt;のとき）。&lt;/p&gt;
&lt;p&gt;数学の言葉で言えば、蛙の跳び方の関数&lt;code&gt;f(n)&lt;/code&gt;を求めることになる。非負整数を引数にとる関数なので、小さい場合から考えればよく、&lt;code&gt;f(1)=1, f(2)=2&lt;/code&gt;はすぐに導ける。問題は、その先の虜挙がどんどん面倒になることである。&lt;/p&gt;
&lt;p&gt;自分がその蛙だと考えてみよう。&lt;code&gt;n&lt;/code&gt;段の階段を前にして、1回目に1段跳べば残りは&lt;code&gt;n-1&lt;/code&gt;段で、&lt;code&gt;f(n-1)&lt;/code&gt;通りの跳び方がある。1回目に2段跳ぶこともでき、その場合は残りが&lt;code&gt;n-2&lt;/code&gt;段で&lt;code&gt;f(n-2)&lt;/code&gt;通りである。したがってこの問題の答えは見慣れたあれ、不思議なフィボナッチ数列になる：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned} F_{0} &amp;amp;=0 \ F_{1} &amp;amp;=1 \ F_{n} &amp;amp;=F_{n-1}+F_{n-2} \quad(\mathrm{n} \geq 2) \end{aligned}
$$&lt;/p&gt;
&lt;p&gt;この種の関数値の問題を解く第一歩は、漸化式を見つけることである。その漸化式をpythonのコードに翻訳する：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fib&lt;/span&gt;(n):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; n&lt;span style="color:#f92672"&gt;==&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; n&lt;span style="color:#f92672"&gt;==&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; fib(n&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)&lt;span style="color:#f92672"&gt;+&lt;/span&gt;fib(n&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;%%time
fib(30)
Wall time: 269 ms
832040&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;実行時間は&lt;code&gt;284ms&lt;/code&gt;で、かなり遅い。なぜ遅いのか。同じ計算の繰り返しがあまりに多いからである。&lt;code&gt;f(5)&lt;/code&gt;の計算を例にとると、呼び出し関係は次のようになる：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;f(5)==&amp;gt;f(4), f(3)
f(4)==&amp;gt;f(3), f(2), f(3)==&amp;gt;f(2), f(1)
f(3)==&amp;gt;f(2), f(1), f(2)==&amp;gt;f(1), f(0), f(2)==&amp;gt;f(1), f(0), f(1)
f(2)==&amp;gt;f(1), f(0), f(1), f(1), f(0), f(1), f(0), f(1)
f(1), f(0), f(1), f(1), f(0), f(1), f(0), f(1)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;そこで自然な発想は、中間の計算結果をすべてキャッシュしておくことである。幸いなことに、pythonにはこの「電池」が標準で備わっている。&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="1-一匹の蛙から">1. 一匹の蛙から</h2>
<p><img src="https://i.imgur.com/zuhczaLm.jpg" alt=" " loading="lazy" decoding="async" referrerpolicy="no-referrer" />
</p>
<p>ある蛙が、<code>n</code>段の階段を跳び下りて池に入ろうとしている。1回で1段あるいは2段だけ跳べるとすると、池に降りる跳び方は全部で何通りあるだろうか（たとえば<code>n=30</code>のとき）。</p>
<p>数学の言葉で言えば、蛙の跳び方の関数<code>f(n)</code>を求めることになる。非負整数を引数にとる関数なので、小さい場合から考えればよく、<code>f(1)=1, f(2)=2</code>はすぐに導ける。問題は、その先の虜挙がどんどん面倒になることである。</p>
<p>自分がその蛙だと考えてみよう。<code>n</code>段の階段を前にして、1回目に1段跳べば残りは<code>n-1</code>段で、<code>f(n-1)</code>通りの跳び方がある。1回目に2段跳ぶこともでき、その場合は残りが<code>n-2</code>段で<code>f(n-2)</code>通りである。したがってこの問題の答えは見慣れたあれ、不思議なフィボナッチ数列になる：</p>
<p>$$
\begin{aligned} F_{0} &amp;=0 \ F_{1} &amp;=1 \ F_{n} &amp;=F_{n-1}+F_{n-2} \quad(\mathrm{n} \geq 2) \end{aligned}
$$</p>
<p>この種の関数値の問題を解く第一歩は、漸化式を見つけることである。その漸化式をpythonのコードに翻訳する：</p>


<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fib</span>(n):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> n<span style="color:#f92672">==</span><span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> n<span style="color:#f92672">==</span><span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> fib(n<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)<span style="color:#f92672">+</span>fib(n<span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>)</span></span></code></pre></div>


<pre tabindex="0"><code>%%time
fib(30)

Wall time: 269 ms
832040</code></pre>
<p>実行時間は<code>284ms</code>で、かなり遅い。なぜ遅いのか。同じ計算の繰り返しがあまりに多いからである。<code>f(5)</code>の計算を例にとると、呼び出し関係は次のようになる：</p>


<pre tabindex="0"><code>f(5)==&gt;f(4), f(3)
f(4)==&gt;f(3), f(2), f(3)==&gt;f(2), f(1)
f(3)==&gt;f(2), f(1), f(2)==&gt;f(1), f(0), f(2)==&gt;f(1), f(0), f(1)
f(2)==&gt;f(1), f(0), f(1), f(1), f(0), f(1), f(0), f(1)
f(1), f(0), f(1), f(1), f(0), f(1), f(0), f(1)</code></pre>
<p>そこで自然な発想は、中間の計算結果をすべてキャッシュしておくことである。幸いなことに、pythonにはこの「電池」が標準で備わっている。</p>


<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> functools <span style="color:#f92672">import</span> lru_cache
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@lru_cache</span>()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fib</span>(n):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> n<span style="color:#f92672">==</span><span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> n<span style="color:#f92672">==</span><span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> fib(n<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)<span style="color:#f92672">+</span>fib(n<span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>)</span></span></code></pre></div>


<pre tabindex="0"><code>%%time
fib(30)
Wall time: 0 ns
832040</code></pre>
<p>速すぎて時間が計測できなかった。pythonにおける<code>lru_cache</code>の基本原理は辞書を構築することで、辞書の<code>key</code>が呼び出しの引数、<code>value</code>がその引数に対する計算結果である。おおよそ次のコードと等価である：</p>


<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fib</span>(n):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> n <span style="color:#f92672">in</span> fib<span style="color:#f92672">.</span>cache:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> fib<span style="color:#f92672">.</span>cache[n]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> n<span style="color:#f92672">==</span><span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        ans <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">elif</span> n<span style="color:#f92672">==</span><span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>        ans <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">elif</span>:
</span></span><span style="display:flex;"><span>        ans <span style="color:#f92672">=</span> fib(n<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)<span style="color:#f92672">+</span>fib(n<span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>    fib<span style="color:#f92672">.</span>cache[n] <span style="color:#f92672">=</span> ans
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> ans
</span></span><span style="display:flex;"><span>fib<span style="color:#f92672">.</span>cache <span style="color:#f92672">=</span> {}</span></span></code></pre></div>
<p>もちろん、この問題に限れば、より細かいキャッシュの方法を使うこともできるし、再帰をやめてループに書き換えることもできる（キャッシュを2つだけ保持するのに等しく、空間の使用量を大幅に減らせる。ただし、さまざまな<code>n</code>の値を繰り返し計算するのであれば、むしろ前の方法のほうが適しているかもしれない）：</p>


<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fib</span>(n):
</span></span><span style="display:flex;"><span>    a, b <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(n):
</span></span><span style="display:flex;"><span>        a, b <span style="color:#f92672">=</span> b, a<span style="color:#f92672">+</span>b
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> a</span></span></code></pre></div>
<p>この問題は <a href="https://leetcode-cn.com/problems/climbing-stairs"
   rel="noopener noreferrer" target="_blank" class="external">leetcode 70</a>
 と等しい。leetcode上でのpython3の解答は次のとおりである：</p>


<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> functools <span style="color:#f92672">import</span> lru_cache
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Solution</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@lru_cache</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">climbStairs</span>(self, n: int) <span style="color:#f92672">-&gt;</span> int:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> n<span style="color:#f92672">==</span><span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> n<span style="color:#f92672">==</span><span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>climbStairs(n<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)<span style="color:#f92672">+</span>self<span style="color:#f92672">.</span>climbStairs(n<span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>)</span></span></code></pre></div>
<p>実行時間52 ms、メモリ消費13.2MB。</p>
<h2 id="2-実用的に簡略化した動的計画法">2. 実用的に簡略化した動的計画法</h2>
<p>この蛙から比較的汎用な示唆を取り出し、漸化式を構成できる同種の問題を解こう：</p>
<ol>
<li>漸化式を見つけて再帰関数を立て、問題を複数の部分問題の求解へ帰着させる。</li>
<li>同じ部分問題を繰り返し計算しないよう、キャッシュを用い、空間と引き換えに時間を得る。</li>
</ol>
<p>一般のアルゴリズム教科書や面接問題の解説では、このキャッシュ構造の設計にかなりの時間を割いている。実際のエンジニアリングの問題では、キャッシュ空間を多少多く使うことにそれほど敏感でないことも多く、したがって再帰関数を書き、汎用的なキャッシュの仕組みを加えるだけでほぼ解決する。キャッシュ空間が問題になって初めて、問題に適したより小さなキャッシュを検討すればよい。</p>
<p>この方法を検証するため、さらにいくつかの問題を見てみよう。leetcodeから直接いくつか拾って解く。</p>
<h3 id="21-最大部分配列和">2.1 <a href="https://leetcode-cn.com/problems/maximum-subarray/"
   rel="noopener noreferrer" target="_blank" class="external">最大部分配列和</a>
</h3>
<p>整数配列<code>nums</code>が与えられたとき、和が最大となる連続部分配列（少なくとも1つの要素を含む）を見つけ、その最大和を返す。</p>
<p><strong>例:</strong></p>
<blockquote>
<p>入力: [-2,1,-3,4,-1,2,1,-5,4],
出力: 6
説明: 連続部分配列 [4,-1,2,1] の和が最大で、6 である。</p>
</blockquote>
<p>配列の各位置で終わる部分配列について得られる最大和の漸化式を考える。
$$
\begin{aligned}  f(0)&amp;=nums(0) \ f(k)&amp;=max(f(k-1), 0)+nums(k) \quad(k&gt;0) \end{aligned}
$$
これに基づけば、最終的な結果が次となることは容易に分かる。
$$
ans = max_{i=0}^n(f(i))
$$
leetcode上でpython3のコードに翻訳すると次のとおりである：</p>


<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> functools <span style="color:#f92672">import</span> lru_cache
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Solution</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">maxSubArray</span>(self, nums: List[int]) <span style="color:#f92672">-&gt;</span> int:
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>nums <span style="color:#f92672">=</span> nums
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> max(self<span style="color:#f92672">.</span>f(i) <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(len(nums)))
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@lru_cache</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">f</span>(self, k):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> k <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>nums[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> max(self<span style="color:#f92672">.</span>f(k<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>), <span style="color:#ae81ff">0</span>) <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>nums[k]</span></span></code></pre></div>
<p>実行時間76 ms、メモリ消費13.7 MB。</p>
<h3 id="22-最小経路和">2.2 <a href="https://leetcode-cn.com/problems/minimum-path-sum/"
   rel="noopener noreferrer" target="_blank" class="external">最小経路和</a>
</h3>
<p>非負整数を含む <em>m</em> x <em>n</em> のグリッドが与えられたとき、左上から右下への経路であって、経路上の数値の総和が最小となるものを見つけよ。</p>
<p><strong>例:</strong></p>
<blockquote>
<p>入力:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
出力: 7
説明: 経路 1→3→1→1→1 の総和が最小だからである。</p>
</blockquote>
<p>行列の各位置を右下角としたときの最小経路和を求めると、次の漸化式が容易に得られる：</p>
<p>$$
\begin{align}
f(0, 0) &amp;= grid(0, 0) \\
f(x, 0) &amp;= f(x-1, 0) + grid(x, 0) \quad(x&gt;0) \\
f(0, y) &amp;= f(0, y-1) + grid(0, y) \quad(y&gt;0) \\
f(x, y) &amp;= \min(f(x-1, y), f(x, y-1)) + grid(x, y) \quad(x&gt;0, y&gt;0)
\end{align}
$$</p>
<p>leetcode上でpython3のコードに翻訳すると次のとおりである：</p>


<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> functools <span style="color:#f92672">import</span> lru_cache
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Solution</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">minPathSum</span>(self, grid: List[List[int]]) <span style="color:#f92672">-&gt;</span> int:
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>grid <span style="color:#f92672">=</span> grid
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>f(len(grid)<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, len(grid[<span style="color:#ae81ff">0</span>])<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@lru_cache</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">f</span>(self, x, y):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> x <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">and</span> y <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>grid[<span style="color:#ae81ff">0</span>][<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> y <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>f(x<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0</span>) <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>grid[x][<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> x <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>f(<span style="color:#ae81ff">0</span>, y<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>grid[<span style="color:#ae81ff">0</span>][y]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> min(self<span style="color:#f92672">.</span>f(x<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>,y), self<span style="color:#f92672">.</span>f(x,y<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)) <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>grid[x][y]</span></span></code></pre></div>
<p>実行時間1052ms、メモリ消費13.9M。</p>
]]></content:encoded><category>coding</category><category domain="tags">アルゴリズム</category><category domain="tags">動的計画法</category></item><item><title>二値分類問題のスコア値と損失関数</title><link>https://yifeitao.com/ja/score-and-loss-function-of-binary-classification/</link><pubDate>Wed, 21 Feb 2018 00:00:00 +0000</pubDate><guid>https://yifeitao.com/ja/score-and-loss-function-of-binary-classification/</guid><description>&lt;p&gt;二値分類問題は教師あり学習における最も基本的な問題の一つである。本稿は、スコア値と損失関数の観点から、二値分類問題を解く数多くの機械学習アルゴリズムを整理することを目的とする。各アルゴリズムの詳細については、関連する教科書&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;を参照されたい。&lt;/p&gt;
&lt;p&gt;&lt;img src="loss.png" alt=" " loading="lazy" decoding="async" referrerpolicy="no-referrer" width="528" height="370" /&gt;
&lt;/p&gt;</description><content:encoded><![CDATA[<p>二値分類問題は教師あり学習における最も基本的な問題の一つである。本稿は、スコア値と損失関数の観点から、二値分類問題を解く数多くの機械学習アルゴリズムを整理することを目的とする。各アルゴリズムの詳細については、関連する教科書<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup><sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>を参照されたい。</p>
<p><img src="loss.png" alt=" " loading="lazy" decoding="async" referrerpolicy="no-referrer" width="528" height="370" />
</p>
<p>データ集合$D={(\mathbf x_1, y_1), (\mathbf x_2, y_2), &hellip;, (\mathbf x_m, y_m)}$ が与えられ、ここで$y_i\in {-1, 1} $、$\mathbf x_i=(x_{i1}, x_{i2}, &hellip;x_{id})$はd次元ベクトルである。なお、資料によっては二値分類に${0, 1}$を用いることを好むが、二値分類をどの数値で表すかに原理上の違いはなく、数学的な表現に若干の差異が生じるだけである。</p>
<p>スコア値は機械学習アルゴリズムにおける正式な用語ではないが、次のような見方を導入してよいだろう。サンプル$\mathbf x$についてのスコア関数$s(\mathbf x)$を構成したい。その基本的性質は単純で、目標値$y$と同符号であること、すなわち正サンプルのスコアは正の数、負サンプルのスコアは負の数であることを要求する。そうすれば最終的な二値分類関数は$sign(s(\mathbf x))$となる。</p>
<p>では、スコア関数をどのように構成すればよいか。直感的な考え方は、サンプルの$d$個の属性を重み付きで総和することであり、これによって各属性のスコアにおける重要度を反映できる。こうして構成したスコアが0のところで識別の働きをもつとは限らないため、さらにバイアス項を加える。したがってスコア関数は$s(\mathbf x)=w^T\mathbf x+b$となり、この基本的なスコア関数から多くのアルゴリズムが派生する。</p>
<p>二値分類問題を評価する基本尺度は分類誤り率である。各サンプルについて損失関数を計算するなら、分類が正しければ損失0、誤っていれば損失1となる。この種の損失関数は一般に0-1損失関数と呼ばれ、$l_{0-1}=\mathbb{I}[sign(s(\mathbf x))\neq y]$と書ける。</p>
<p>正負のサンプルを統一的に扱うために$z=s(\mathbf x)*y$ とおくと、分類が正しいとき$z$は必ず正の値となる。これを絶対スコア値と呼ぶことにしよう。$z$は分類が正しいかどうかの尺度である。このとき$l_{0-1}=\mathbb{I}[sign(z)= -1]$であるから、$l_{0-1}$は$z$に関する区分関数となる。最適化理論の観点から見ると、この関数は勾配も<a href="https://zh.wikipedia.org/wiki/%E6%AC%A1%E5%AF%BC%E6%95%B0"
   rel="noopener noreferrer" target="_blank" class="external">劣勾配</a>
も求められないため、その近似を見つけて最適化する必要がある。近似$\widetilde l$の基本要件は、常に非負の値であること、そしてそれが小さいとき$l_{0-1}$も小さいことである。</p>
<p>スコア値は属性$X$の線形結合であり、別の角度から見れば、スコア値は重み$w$についての線形結合でもある。一般には$X$を新しい特徴空間へ変換して$\phi (\mathbf x)$を得る。するとスコア値は$z=(w^T\mathbf \phi (\mathbf x)+b)y$となる。以上の分析はほぼそのまま成り立ち、モデルの複雑さが増すだけである。以下の分析は特に断らない限り元の空間で行う。</p>
<h2 id="1-線形回帰">1 線形回帰</h2>
<p>スコア値が目標値と同符号であることを要求するが、目標をもう少し狭く設定して、スコア値を直接${-1, 1}$としてもよい。これが線形回帰問題であり、損失関数には誤差の二乗を用いる（直感的に理解することもできるし、サンプルの誤差が正規分布に従うと考えて最尤の観点から導くこともできるが、本稿の主題ではない）。$l_{sqr}=(s(\mathbf x)-y)^2=(s(\mathbf x) * y-y^2)^2=(z-1)^2$に注意すれば、誤差関数は絶対スコア値$z$に関する二次関数である。図から分かるように、$l_{sqr}$は常に$l_{0-1}$より大きく、しかも$l_{sqr}$が小さいとき$l_{0-1}$も小さいので、$l_{0-1}$の近似として用いることができる。</p>
<p>線形回帰には閉形式解$w=(X^TX)^{-1}X^Ty$があるため、線形回帰で二値分類を行うのは非常に簡単である。しかし効果は一般にあまりよくない。なぜか。$l_{sqr}$の曲線から分かるように、絶対スコア値が大きいとき$l_{0-1}$は0であるべきであり、直感的には絶対スコア値が大きいことは識別度が高いという良いことなのに、$l_{sqr}$はかえってより大きな罰を与える。これは不合理である。線形回帰はそれなりの$w$を与えることができ、他のアルゴリズムの初期値として利用できる。</p>
<h2 id="2-パーセプトロン学習">2 パーセプトロン学習</h2>
<p>パーセプトロン学習は次のような直感的な考え方で理解できる。絶対スコア値について、それが正の数であれば分類は正しく、誤差は0とみなす。負の数であれば、0から遠いほど誤差が大きいとみなし、すなわち$l_p=max(0, -z)$とする。$l_p$の曲線から見ても、これも$l_{0-1}$の近似である。区分関数ではあるが劣勾配は求められ、$z\geqslant 0$のとき勾配を0とみなし、$z&lt;0$のとき$\frac{dl_p}{dw}=\frac {dl_p}{dz}\frac{dz}{dw}=-1*y\mathbf x=-y\mathbf x$となる。確率的勾配降下法を用いれば$w\leftarrow w+\eta y\mathbf x$、つまりスコアを誤ったときにのみ最適化を行う。これがまさにパーセプトロン学習アルゴリズムの行っていることである。</p>
<h2 id="3-サポートベクターマシン">3 サポートベクターマシン</h2>
<p>パーセプトロン学習の誤差関数はさらに改良できる。絶対スコア値が十分に大きければ、そのサンプルを正サンプルとみなすことにいっそう安心できる。$z$は$w,b$についてスケール可能であることを考慮し、十分に大きいとは1より大きいことと定義してよい。そこで$l_{hinge}=max(0, 1-z)$が得られ、この損失関数はヒンジ損失関数と呼ばれる。構造リスクを考慮すると、最適化目標は次となる。</p>
<p>$$
\underset{w,b}{min}=\frac{1}{2}\left |w  \right |^2+C\sum_{i=1}^{m}max(0, 1-z)
$$</p>
<p>スラック変数を用いることでソフトマージンのサポートベクターマシンになり、二次計画問題として解いてモデルを求めることができる。</p>
<p>スコア値の観点から理解すると、スコア値が十分に大きいことで、モデルは新しいサンプルに対して良好な頑健性をもつ。これがまさにサポートベクターマシンの優れている点である。特徴空間を導入すると、サポートベクターマシンの求解過程では内積を大量に計算する必要があるが、特徴空間の内積を元の空間のカーネル関数に簡約することでカーネル法を導入できる。</p>
<h2 id="4-ロジスティック回帰">4 ロジスティック回帰</h2>
<p>$z$の値をもとに、分類が正しい確率、すなわちその程度を測る確率を算出したい。つまり$z$の値域を$(-\infty , +\infty )$から$(0,1)$へ写像する。そのためにはsigmoid関数が良い選択であり、勾配が求められ、しかも0のところで確率がちょうど0.5になる。すなわち</p>
<p>$$
sigmoid(z)=\frac {1}{1+exp(-z)}
$$</p>
<p>次に、このsigmoid関数に基づいて損失関数$l_{0-1}(z)$を模擬する。損失関数は$z$について減少し、しかも常に正の数であることを要求する。確率の対数をとってから符号を反転することを考えれば、次のようになる。</p>
<p>$$
l_{log}(z)=-log(sigmoid(z))=log(1+exp(-z))
$$</p>
<p>図から分かるように、これも$l_{0-1}(z)$の一種の近似である。対数損失関数を最適化目標とすると、</p>
<p>$$
\underset{w,b}{min}=\sum_{i=1}^{m}log(1+exp(-z))+ \lambda\left |w  \right |^2
$$</p>
<p>これは正則化付きのロジスティック回帰であり、$l_{log}(z)$は至る所で微分可能であるため、勾配降下法などの最適化手法で解くことができる。</p>
<h2 id="5-adaboost">5 AdaBoost</h2>
<p>指数関数も同様に$l_{0-1}$を近似する基本条件を満たす。すなわち損失関数を$l_{exp}(z)=exp(-z)$と定義する。</p>
<p>指数損失関数と加法モデルを組み合わせることで、AdaBoostアルゴリズムを導出できる。</p>
<p>加法モデルとは次のように理解できる。得られた特徴空間におけるd次元ベクトル$\phi(\mathbf x)$がd個の弱分類器に対応すると仮定する。弱分類器とは、ランダムに推測する50%の正解率より少しだけ高い分類器のことである。Boost法とは、これらの弱分類器を有効に組み合わせて強分類器を構築する方法である。加法モデルは本質的に依然として線形モデルであり、スコア関数はやはり$s(\mathbf x)=w^T\mathbf \phi (\mathbf x)$となる。（弱分類器自体がバイアスをもちうることを考慮し、$b=0$とする。）ただしここでの$\phi(\mathbf x)$は単純な属性変換ではなく、1つの弱分類器を表す（本質的に見れば、これも一種の属性変換である）。</p>
<p>AdaBoostは段階的に反復する過程である。$m-1$回の反復を経てすでにスコア関数$s_{m-1}(\mathbf x)=\sum_{i=1}^{m-1}w_i\phi_i(\mathbf x)$が得られていると仮定し、第$m$回の反復後に$w_m, \phi_m(\mathbf x), s_m(\mathbf X)$が得られるとすると、$s_m(\mathbf X)=s_{m-1}(\mathbf X)+w_m\phi_m(\mathbf x)$となる。AdaBoostの最適化目標は、各段階で得られるスコア関数を指数損失関数について最小化することであり、すなわち</p>
<p>$$
w_m, \phi_m = arg\underset{w,\phi}{min}\sum_{i=1}^Nexp(-z_{mi})  \quad z_{mi}=y_is_m(\mathbf X_i)
$$</p>
<p>この式のさらなる導出についてはここでは詳述しない。</p>
<h2 id="まとめ">まとめ</h2>
<p>以上、6種類の損失関数を紹介した。いずれもスコア値に関する単調非増加関数であり、うち後半の5種類は0-1損失関数の近似的な代替である。代替関数を解くことで元の問題の解が得られるかどうかについては深い研究があり、代替損失の「一致性」問題と呼ばれる<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>。</p>
<p>さらに、すべてのサンプルの中から正例を見つけて順序付けしたい場合、スコア値は最適化に参加する時点で順序付けの性質を備えている必要がある。対数損失と指数損失は単調減少であり、これらを最適化目標とするモデルが算出するスコア値は順位付けに参加できる。他のいくつかの損失関数は単調減少ではないため、対応するスコア値は順位付けに適さない。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>統計的学習方法 李航&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>機械学習 周志华&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://projecteuclid.org/download/pdf_1/euclid.aos/1079120130"
   rel="noopener noreferrer" target="_blank" class="external">https://projecteuclid.org/download/pdf_1/euclid.aos/1079120130</a>
&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded><category>coding</category><category domain="tags">機械学習</category><category domain="tags">分類</category></item><item><title>高可配置性ソフトウェア開発</title><link>https://yifeitao.com/ja/development-of-highly-configurable-software/</link><pubDate>Tue, 17 Jan 2017 00:00:00 +0000</pubDate><guid>https://yifeitao.com/ja/development-of-highly-configurable-software/</guid><description>&lt;p&gt;可配置性はソフトウェア工学の実践における重要な問題の一つである。設定ファイルは実行ファイルとともに、ソフトウェアの実行時バージョン管理を構成する要素となる。Visual Basic、Python、Lua などのインタプリタ型言語は、いわゆる「スクリプト」の形で現れることが多く、オペレーティングシステムやゲームなどの大規模ソフトウェアの可配置性をさらに高めてきた。また、ドメイン駆動設計はドメインモデルを強調すると同時に、設定ドメインモデルを記述するドメイン固有言語の発展も促した。産業制御の分野では、ソフトウェアの可配置性は組態（コンフィギュレーション）という概念で語られる。ゲーム開発の分野では、一般にエンジンとスクリプトという概念で語られる。&lt;/p&gt;
&lt;h2 id="1-基本原理"&gt;1 基本原理&lt;/h2&gt;
&lt;p&gt;ソフトウェアの複雑さの根源は現実世界の複雑さにある。ソフトウェアの複雑さが人の理解能力を超えると、ソフトウェア開発の困難、さらには失敗をもたらす。優れたソフトウェア工学の方法論は現実世界の良好な抽象モデルを与え、合理的な抽象階層構造によってソフトウェアの複雑さを管理し、人がある全体から細部へと階層的にソフトウェアを把握できるようにする。現実世界に由来する複雑さは減少も消滅もしないが、制約を受けることになる。&lt;/p&gt;
&lt;p&gt;高可配置性は、急速な要求変更の問題をある程度解決できる。ソフトウェアの中で不変の部分をモデルとして捉え、変わりやすい部分を設定ファイルなどの方法で制約するのである。&lt;/p&gt;
&lt;pre class="mermaid"&gt;graph TB
subgraph ソフトウェア1
complexity2[複雑性]--&amp;gt;abstract[共通抽象]
end
subgraph ソフトウェア
complexity1[複雑性]--&amp;gt;complexity3[複雑性]
end&lt;/pre&gt;
&lt;h2 id="2-構築方法"&gt;2 構築方法&lt;/h2&gt;
&lt;p&gt;高可配置性ソフトウェアを構築するには、まずドメインの特徴に応じて汎用ソフトウェアモデルを確立し、次にそのモデルに対して設定技術を用いる。異なる設定とモデルの組み合わせが、最終的に異なるソフトウェアインスタンスを構築する。&lt;/p&gt;
&lt;pre class="mermaid"&gt;graph TB
subgraph ソフトウェア2
config2[ソフトウェア設定2]--&amp;gt;abstract2[汎用モデル]
end
subgraph ソフトウェア1
config1[ソフトウェア設定1]--&amp;gt;abstract1[汎用モデル]
end&lt;/pre&gt;
&lt;h3 id="21-モデリング"&gt;2.1 モデリング&lt;/h3&gt;
&lt;p&gt;モデリングの問題は非常に複雑な問題であり、設計時のシナリオによって異なる。ドメイン知識と設計経験を必要とし、ドメインの専門家と密接に協力する必要がある。この分野にはいくつかの古典がある：&lt;a href="https://book.douban.com/subject/26819666/"
rel="noopener noreferrer" target="_blank" class="external"&gt;『エリック・エヴァンスのドメイン駆動設計』&lt;/a&gt;
と&lt;a href="https://book.douban.com/subject/25844633/"
rel="noopener noreferrer" target="_blank" class="external"&gt;『実践ドメイン駆動設計』&lt;/a&gt;
。&lt;/p&gt;
&lt;h3 id="22-設定技術"&gt;2.2 設定技術&lt;/h3&gt;
&lt;h4 id="221-設定ファイル"&gt;2.2.1 設定ファイル&lt;/h4&gt;
&lt;p&gt;設定ファイルは最も広く使われ、最も基本的な設定技術である。設定ファイルは主にソフトウェアの起動および実行に必要なデータを記述するために用いられ、一般にソフトウェアの実行ロジックを直接記述するものではない。一般的な設定ファイル形式には ini、xml、json などがある。簡単な比較を下表に示す。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;形式&lt;/th&gt;
&lt;th&gt;表現力&lt;/th&gt;
&lt;th&gt;可読性&lt;/th&gt;
&lt;th&gt;典型的な用途&lt;/th&gt;
&lt;th&gt;例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ini&lt;/td&gt;
&lt;td&gt;弱い、ネスト不可&lt;/td&gt;
&lt;td&gt;最も良い&lt;/td&gt;
&lt;td&gt;ソフトウェアの起動パラメータ。&lt;/td&gt;
&lt;td&gt;起動ディレクトリ、IP アドレス。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xml&lt;/td&gt;
&lt;td&gt;最も強い&lt;/td&gt;
&lt;td&gt;最も悪い&lt;/td&gt;
&lt;td&gt;複雑なオブジェクトのシリアライズ。&lt;/td&gt;
&lt;td&gt;データフレームの構造と解析規則。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;json&lt;/td&gt;
&lt;td&gt;普通&lt;/td&gt;
&lt;td&gt;普通&lt;/td&gt;
&lt;td&gt;Web API インタフェース。&lt;/td&gt;
&lt;td&gt;天気予報サービスのインタフェース。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id="222-スクリプト言語"&gt;2.2.2 スクリプト言語&lt;/h4&gt;
&lt;p&gt;大規模なプログラムは、しばしば複数のプログラミング言語を組み合わせて実現される。一般に、コンパイル型言語でシステムの比較的固定された部分と実行効率が重要な部分を実装し、インタプリタ型言語でシステムの変わりやすい部分を実装する。たとえば Linux オペレーティングシステムは C で多くのツールを実装し、bash スクリプトでそれらのツールを総合的に組み合わせて応用する。大規模ゲームは C++ のゲームエンジン上に構築され、Lua スクリプトなどでゲームの動作ロジックを記述する。&lt;/p&gt;</description><content:encoded><![CDATA[<p>可配置性はソフトウェア工学の実践における重要な問題の一つである。設定ファイルは実行ファイルとともに、ソフトウェアの実行時バージョン管理を構成する要素となる。Visual Basic、Python、Lua などのインタプリタ型言語は、いわゆる「スクリプト」の形で現れることが多く、オペレーティングシステムやゲームなどの大規模ソフトウェアの可配置性をさらに高めてきた。また、ドメイン駆動設計はドメインモデルを強調すると同時に、設定ドメインモデルを記述するドメイン固有言語の発展も促した。産業制御の分野では、ソフトウェアの可配置性は組態（コンフィギュレーション）という概念で語られる。ゲーム開発の分野では、一般にエンジンとスクリプトという概念で語られる。</p>
<h2 id="1-基本原理">1 基本原理</h2>
<p>ソフトウェアの複雑さの根源は現実世界の複雑さにある。ソフトウェアの複雑さが人の理解能力を超えると、ソフトウェア開発の困難、さらには失敗をもたらす。優れたソフトウェア工学の方法論は現実世界の良好な抽象モデルを与え、合理的な抽象階層構造によってソフトウェアの複雑さを管理し、人がある全体から細部へと階層的にソフトウェアを把握できるようにする。現実世界に由来する複雑さは減少も消滅もしないが、制約を受けることになる。</p>
<p>高可配置性は、急速な要求変更の問題をある程度解決できる。ソフトウェアの中で不変の部分をモデルとして捉え、変わりやすい部分を設定ファイルなどの方法で制約するのである。</p>
<pre class="mermaid">graph TB
    subgraph ソフトウェア1
        complexity2[複雑性]--&gt;abstract[共通抽象]
    end
    subgraph ソフトウェア
        complexity1[複雑性]--&gt;complexity3[複雑性]
    end</pre>

<h2 id="2-構築方法">2 構築方法</h2>
<p>高可配置性ソフトウェアを構築するには、まずドメインの特徴に応じて汎用ソフトウェアモデルを確立し、次にそのモデルに対して設定技術を用いる。異なる設定とモデルの組み合わせが、最終的に異なるソフトウェアインスタンスを構築する。</p>
<pre class="mermaid">graph TB
    subgraph ソフトウェア2
        config2[ソフトウェア設定2]--&gt;abstract2[汎用モデル]
    end
    subgraph ソフトウェア1
        config1[ソフトウェア設定1]--&gt;abstract1[汎用モデル]
    end</pre>

<h3 id="21-モデリング">2.1 モデリング</h3>
<p>モデリングの問題は非常に複雑な問題であり、設計時のシナリオによって異なる。ドメイン知識と設計経験を必要とし、ドメインの専門家と密接に協力する必要がある。この分野にはいくつかの古典がある：<a href="https://book.douban.com/subject/26819666/"
   rel="noopener noreferrer" target="_blank" class="external">『エリック・エヴァンスのドメイン駆動設計』</a>
と<a href="https://book.douban.com/subject/25844633/"
   rel="noopener noreferrer" target="_blank" class="external">『実践ドメイン駆動設計』</a>
。</p>
<h3 id="22-設定技術">2.2 設定技術</h3>
<h4 id="221-設定ファイル">2.2.1 設定ファイル</h4>
<p>設定ファイルは最も広く使われ、最も基本的な設定技術である。設定ファイルは主にソフトウェアの起動および実行に必要なデータを記述するために用いられ、一般にソフトウェアの実行ロジックを直接記述するものではない。一般的な設定ファイル形式には ini、xml、json などがある。簡単な比較を下表に示す。</p>
<table>
  <thead>
      <tr>
          <th>形式</th>
          <th>表現力</th>
          <th>可読性</th>
          <th>典型的な用途</th>
          <th>例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ini</td>
          <td>弱い、ネスト不可</td>
          <td>最も良い</td>
          <td>ソフトウェアの起動パラメータ。</td>
          <td>起動ディレクトリ、IP アドレス。</td>
      </tr>
      <tr>
          <td>xml</td>
          <td>最も強い</td>
          <td>最も悪い</td>
          <td>複雑なオブジェクトのシリアライズ。</td>
          <td>データフレームの構造と解析規則。</td>
      </tr>
      <tr>
          <td>json</td>
          <td>普通</td>
          <td>普通</td>
          <td>Web API インタフェース。</td>
          <td>天気予報サービスのインタフェース。</td>
      </tr>
  </tbody>
</table>
<h4 id="222-スクリプト言語">2.2.2 スクリプト言語</h4>
<p>大規模なプログラムは、しばしば複数のプログラミング言語を組み合わせて実現される。一般に、コンパイル型言語でシステムの比較的固定された部分と実行効率が重要な部分を実装し、インタプリタ型言語でシステムの変わりやすい部分を実装する。たとえば Linux オペレーティングシステムは C で多くのツールを実装し、bash スクリプトでそれらのツールを総合的に組み合わせて応用する。大規模ゲームは C++ のゲームエンジン上に構築され、Lua スクリプトなどでゲームの動作ロジックを記述する。</p>
<h4 id="223-ドメイン固有言語">2.2.3 ドメイン固有言語</h4>
<p>ドメイン固有言語は一般的な設定ファイルとプログラムコードの中間に位置する。すなわち、ある特定のドメインに向けた、表現力が制限された計算機プログラミング言語である。</p>
<p>ドメイン固有言語はさらに内部ドメイン固有言語、外部ドメイン固有言語、言語ワークベンチなどに分かれる。内部ドメイン固有言語は主要言語のプログラムコードの特殊な書き方であり、設定とは見なせない。外部ドメイン固有言語は新しい言語を設計して表現するもので、依然としてテキスト設定の範囲に属する。言語ワークベンチは主に可視化された設定方式を提供するもので、可視化プログラミングとも呼ばれる。</p>
<p>ドメイン固有言語は設定ファイルとスクリプトプログラムの中間に位置し、ドメイン内で高い表現力を提供するとともに、ドメインの専門家にも理解しやすい。専門家の知識をソフトウェアへと生成することを加速できる。</p>
<p>モデルの必要に応じて、上記 3 つの設定技術はいずれも高可配置性ソフトウェア開発に適用できる。下表では 3 つの設定技術の主な特徴と典型的な用途を比較する。</p>
<table>
  <thead>
      <tr>
          <th>設定技術</th>
          <th>表現力</th>
          <th>可読性</th>
          <th>典型的な用途</th>
          <th>例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設定ファイル</td>
          <td>ソフトウェアの実行に必要なデータを記述する。一般にソフトウェアの実行ロジックを直接記述しない。</td>
          <td>具体的な設定ファイル形式による。</td>
          <td>ソフトウェアの起動パラメータ。</td>
          <td>起動ディレクトリ、ネットワーク通信アドレス。</td>
      </tr>
      <tr>
          <td>スクリプト言語</td>
          <td>ソフトウェアの実行ロジックを直接記述する。</td>
          <td>プログラマにしか適さず、ドメインの専門家には理解しにくい。</td>
          <td>基本インタフェースの形式が固定されている場合に、変わりやすいロジックを記述する。</td>
          <td>C++ で構築した機器監視インタフェースの上で、Python スクリプトで監視フローを記述する。</td>
      </tr>
      <tr>
          <td>ドメイン固有言語</td>
          <td>ドメインモデルの設定を記述し、それによって間接的にソフトウェアの実行ロジックを記述する。</td>
          <td>プログラマにもドメインの専門家にも適する。</td>
          <td>認容度の高いドメインモデルが確立され、モデルの設定が豊富で変わりやすい場合。</td>
          <td>「状態-反応」モデルで故障処理を表現し、ドメイン固有言語を確立して具体的な内容を記述する。</td>
      </tr>
  </tbody>
</table>
<h2 id="3-エンジニアリング実践">3 エンジニアリング実践</h2>
<p>図に示すように、高可配置性ソフトウェア開発は実際に従来のソフトウェア開発プロセスを変え、ソフトウェアのライフサイクルにおける設定の重要な位置づけを際立たせる。以下では、高可配置性が従来のソフトウェア開発の各段階に与える影響を述べる。</p>
<pre class="mermaid">graph TB
    subgraph 高可配置性ソフトウェアプロセス
    	requirements2[要件分析]--&gt;construct2[ソフトウェア本体構築]
    	construct2--&gt;config[ソフトウェア設定]
        config--&gt;test2[ソフトウェア本体テスト]
        config--&gt;config_test[ソフトウェア設定テスト]
		test2--&gt;maintain2[ソフトウェア本体保守] 
		config_test--&gt;config_maintain[ソフトウェア設定保守]
    end
    
    subgraph 一般的なソフトウェアプロセス
    	requirements1[要件分析]--&gt;construct1[ソフトウェア構築]
        construct1--&gt;test1[ソフトウェアテスト]
		test1--&gt;maintain1[ソフトウェア保守] 
    end</pre>

<h3 id="31-要件分析">3.1 要件分析</h3>
<p>要件分析の段階では、通常の開発と比べて、高可配置性ソフトウェア開発では以下の問題を重点的に分析する必要がある：</p>
<ol>
<li>高可配置性技術を採用する必要性。これは主にソフトウェアの適用範囲によって決まる。すなわち、ソフトウェアに展開可能性を持たせるかどうか、ソフトウェアが対応すべき要求が変わりやすいかどうかである。適応すべきシナリオが多いほど、要求が変わりやすいほど必要性は大きくなる。ソフトウェアがほぼ固定された一つのシナリオにしか使われないなら、設定を用いる必要はない。</li>
<li>モデル要求と設定要求を区別すること。要求収集の際には、ソフトウェアの各種の適用シナリオにおける要求を収集し、ソフトウェアの展開に伴って生じうる要求の拡張を分析し、できるだけ多くの事例を分析し、深く分析してシステムの不変性を抽出し、システムのモデルを抽象化する。そしてシステムの変わりやすい部分を設定要求として識別する。</li>
</ol>
<h3 id="32--ソフトウェア構築">3.2  ソフトウェア構築</h3>
<p>高可配置性ソフトウェア開発におけるソフトウェア構築は二つの段階に分かれる：</p>
<ol>
<li>ソフトウェア本体の構築：この段階はプログラマが行う。高可配置性はアーキテクチャ設計への要求が高く、オブジェクト指向パラダイムを用いる場合には、オブジェクト指向の設計原則を深く理解し適用する必要があり、各種の構造型設計パターン、およびリフレクションや依存性注入といった実行時関連技術を大量に応用する必要がある；</li>
<li>ソフトウェア設定の段階：この段階はプログラマが行うこともでき、ドメインの専門家が行うこともできる。設定がソフトウェアの重要な構成部分となるため、設定ファイルはソースコードと同様に厳格に管理し、バージョン管理システムに組み入れるべきである。手作業の設定は誤りを生じやすいため、できるだけ可視化された設定ツールを提供し、利用者が勝手に設定を変更することを制限すべきである。</li>
</ol>
<h3 id="33-ソフトウェアテスト">3.3 ソフトウェアテスト</h3>
<p>ソフトウェアの二段階の開発に対応して、高可配置性ソフトウェアのテストも二つの部分に分かれる：</p>
<ol>
<li>
<p>ソフトウェア本体のテスト。テストケースを構築する際には、各種のシナリオの設定を設計すべきである。これらのシナリオではモデルの境界状況を考慮し、テストを通じてソフトウェア本体の欠陥をできるだけ取り除き、ソフトウェアの展開・普及時に設定後に欠陥が発見されることを減らす。</p>
</li>
<li>
<p>ソフトウェア設定のテスト。設定テストを完了して初めて具体的なシナリオで応用できる。設定の過程はプログラマではなくドメインの専門家が行うことが多いため、設定の検証を行い、設定完了後のテストツールとテスト方法の細則を提供して、ドメインの専門家もソフトウェアのテストを完了できるようにするのがよい。</p>
</li>
</ol>
<h3 id="34--ソフトウェア保守">3.4  ソフトウェア保守</h3>
<p>ソフトウェアの二段階の開発に対応して、高可配置性ソフトウェアの保守も二つの部分に分かれる：</p>
<ol>
<li>
<p>ソフトウェア本体の保守。汎用的な機能のアップグレードはソフトウェア本体のアップグレードによって行う。ソフトウェア本体の機能の変更は設定との互換性を保つべきであり、設定の形式を変更せざるを得ない場合には、設定のアップグレード変換機能を提供すべきである；</p>
</li>
<li>
<p>ソフトウェア設定の保守。利用者の要求と適用シナリオが変化し、その変化がソフトウェア本体の想定の範囲内であれば、ドメインの専門家が設定を変更して完了できる。ソフトウェア設定は次第に利用者の重要な資産となる。</p>
</li>
</ol>
]]></content:encoded><category>coding</category><category domain="tags">ソフトウェア工学</category><category domain="tags">プログラミング</category></item><item><title>「デバッグの法則」（Debugging Rules!）——デバッグは技術である</title><link>https://yifeitao.com/ja/debug-is-technology/</link><pubDate>Sat, 24 Dec 2016 00:00:00 +0000</pubDate><guid>https://yifeitao.com/ja/debug-is-technology/</guid><description>&lt;p&gt;一般に、エンジニアがあるものを芸術、あるいは玄学と呼ぶときは、その難度が高く、従うべき規則があまりないことを意味する。デバッグはこの範疇に属する。ほとんどすべてのプログラマは、バグに翻弄されて七転八倒した経験を持つ。時にあれこれいじっているうちに問題は解決したように見えるが、なぜ解決したのかは分からない。それを説明不能な玄学現象として扱うしかない。最も厄介なバグは、たまにしか現れず再現が難しいもので、学名をハイゼンベルクバグという。&lt;/p&gt;
&lt;p&gt;しかし、ソフトウェアはやはり科学技術の範疇に属するものであり、デバッグもまた技術的な仕事であるべきだ。「デバッグの法則」（Debugging Rules!）は、デバッグ技術を扱った珍しい書物である。九つの普遍的な原則は、ソフトウェアとハードウェアの開発・保守に当てはまるだけでなく、日常生活にさえ応用できる。この本を見つけるのにも少し苦労したが、幸いなことに図霊コミュニティには&lt;a href="http://www.ituring.com.cn/book/84"
rel="noopener noreferrer" target="_blank" class="external"&gt;正規の電子書籍&lt;/a&gt;
が売られていた。&lt;/p&gt;
&lt;p&gt;正直に言えば、著者が挙げる例の多くはハードウェア寄りで、理解するのにやや隔たりを感じる。しかし九つの原則は実に実用的で、私の以前の開発・デバッグの経験の中でも無意識にいくつかはまとめていたかもしれない。それでもこの本を読むと、武術の心法を手に入れたような感覚がある。読んだ後、以下で九つの原則をもう一度たどり、理解を深める。&lt;/p&gt;
&lt;h2 id="1-システムを理解する"&gt;1 システムを理解する&lt;/h2&gt;
&lt;p&gt;これは最も重要な原則である。バグを分析するには、当然システムがどのように動作するかを理解する必要がある。そのためには基本原理をいくつか学び、身につける必要があり、具体的なクラスライブラリ、ツール、技術については関連するドキュメントを真剣に読む必要がある。私が公式ドキュメントを真剣に読む習慣を身につけたのはここ数年のことである。検索エンジンの発達によって、我々は google 指向プログラミングの習慣を身につけてしまった。問題に直面すると、いつも一気に解決しようとする。結果として多くの回り道をすることになる。道標を見る時間を惜しむからだ。本書は特に「一字一句」マニュアルを読むことを強調しており、これは浮ついた人間への頭からの一撃である。システムを理解せずにデバッグを始めることと、原理を理解せずにコードを書くことは一脈相通じるもので、俗に「偶然指向プログラミング」と呼ばれる。&lt;/p&gt;
&lt;h2 id="2-失敗を再現する"&gt;2 失敗を再現する&lt;/h2&gt;
&lt;p&gt;この原則はバグを再現することの重要性と方法を説く。バグは必ずある特定の条件下で発生する。その特定の条件を丹念に突き止めれば、半分は成功したも同然である。私のハードウェア保守の経験から言えば、バグを発見した人がその過程全体を詳細に記録できれば、非常に役立つ。多くのバグは発生条件が厳しく、だからプログラマの口癖の一つが「私のところでは問題ないです」なのである。こうしたバグに対しては、まずシミュレーションの方法を見つけるのがよい。たとえばソフトウェアの動作や相互作用の速度を上げる、ストレステストを行うなどである。次に、ソフトウェア内部に詳細で合理的なバグ記録の仕組みを持ち、内部から再現条件を見つけやすくする。&lt;/p&gt;
&lt;h2 id="3-考えず見る"&gt;3 考えず、見る&lt;/h2&gt;
&lt;p&gt;この原則は観測の重要性を強調する。空想するよりも現実に向き合うことが大切である。言語は思考に影響する。だからバグに遭遇したときの口癖は「ちょっと見てみます」であるべきで、「たぶん〜が原因だと思います」ではない。我々はあらゆる手段を尽くしてバグの細部をはっきりと見るべきであり、成熟したプログラムにはデバッグ用の基盤が備わっているべきである。私の経験から言えば、少なくともログは不可欠である。初級のプログラマはブレークポイントだけに頼ってデバッグする習慣があるが、問題はブレークポイント自体がプログラムの実行フローを変えてしまうことである（特にマルチスレッドの場合）。もちろん推測も依然として有用で、観測の範囲を絞り込む助けになる。あるいは少なくとも観測の優先順位を決めるのに役立ち、そこで経験が生きる。しかしどうであれ、経験は観測に代わりえない。&lt;/p&gt;
&lt;h2 id="4-分割統治"&gt;4 分割統治&lt;/h2&gt;
&lt;p&gt;二分探索法は探索の時間計算量を線形から対数に変えられる。これはプログラム中の探索アルゴリズムだけでなく、デバッグ時の方法論でもある。システムにこの意識があれば、鍵となるのはシステムをどう分割するかであり、ここで原則1に戻る。少なくともブレークポイントでデバッグするとき、バグの位置を見つけるのに二分法が使えるし、ログを挿入するときにもこの原則を応用できる。さらに本書ではバグ同士が互いに影響し合う現象にも触れている。私はむしろバグのシーソー現象と言ってもよいとさえ思う。解決の道は一つも見逃さないことである。バグをデバッグしているうちにコードの品質があまりに悪いことに気づき、リファクタリングが必要になることもある。そうでなければ砂の上に高台は築けない。効果的なリファクタリングを経て、バグが自然に発見され解消されることさえある。&lt;/p&gt;
&lt;h2 id="5-一度に一箇所だけ変える"&gt;5 一度に一箇所だけ変える&lt;/h2&gt;
&lt;p&gt;この原則は実験科学における対照の原則に似ている。一度に一つの変数だけを考察し、正常な状況と異常な状況を比較し、いかなるテスト条件の差異も見落としてはいけない。前の条ではバグを一つも見逃さないと言ったが、一つずつ片付ける必要もある。リファクタリングの際も、理解していないコードをやたらにいじらないよう注意すべきである。多くのコードの危険性は、一箇所を触ると全体に響くことにある。これがバグ修正時の最大のリスクだと思う。しばしば一つのバグを消すと同時にいくつかのバグを増やすことになる。やはり原則1に戻り、システムを理解することである。とはいえ、先人が書いたコードを理解するのは容易ではないこともある。これはまた、コードを書くときに徳を積むべきことを我々に示している。コードはコンパイラのためにあるのではなく、後に我々のコードを保守する人のためにあるのだ。&lt;/p&gt;
&lt;h2 id="6-監査証跡を残す"&gt;6 監査証跡を残す&lt;/h2&gt;
&lt;p&gt;この原則は記録の重要性と方法を説く。医者が診察するのと同じで、適切な質問をし、具体的で有効な答えを出す必要がある。私の経験では、バグのデバッグを始めると、人はすっかり深みにはまって抜け出せなくなることがある。記録を取ることで、思考の泥沼から自分を引き離し、デバッグの計画と方法を絶えず修正できる。専用のシステムソフトウェアでは、ソフトウェアの利用者に、バグの発生状況を有効な言葉で記録する方法を訓練する必要がある。記録がなければ、デバッグする側は適切な質問で利用者の記憶を呼び覚まさねばならない。また、深夜に解決できないバグは一眠りする必要があると思う。拡散思考が自動的に働き、たいてい朝には解決している。&lt;/p&gt;
&lt;h2 id="7-プラグを確認する"&gt;7 プラグを確認する&lt;/h2&gt;
&lt;p&gt;この原則は、いわゆる「初歩的なミス」について述べている。母が「パソコンのスピーカーから音が出ない」と言ってきたら、まず電源が入っているかを聞く、という類である。あるいは main 関数を mian 関数と書いてしまうとか。一度、ブレークポイントを置いてデバッグしてもブレークポイントで止まらず、後で分かったのは、似たコードが二箇所あり、ブレークポイントを間違った場所に置いていたことだった。シナリオと経験に照らして、そのシナリオでよくある初歩的なミスを犯していないか自問すれば、多くの場合すぐに効果が出る。多くの電気機器の取扱説明書の故障 FAQ でも、プラグの確認が強調されている。初歩的なミスが「初歩的」なのは難度であって、頻度ではない。&lt;/p&gt;
&lt;h2 id="8-新しい視点を得る"&gt;8 新しい視点を得る&lt;/h2&gt;
&lt;p&gt;この原則は助けを求めることと交流について述べている。ここで重要なのは、他者の視点によって自分の思考の固定観念を打ち破ることである。だから相手には現象だけを詳細に説明し、自分の推測は言わないほうがよい。相手の判断を汚染してしまうからだ。プログラマにとって、このときの google 指向プログラミングの力は驚異的である。おそらく九割の問題は、実際に質問する必要はなく、適切な検索語を見つけて検索すればよい。中国のプログラマにとって、英語で検索することは必須の技能である。もちろん、見つけたものを本当に理解することも非常に重要で、薬を見たらすぐ飲む、ではいけない。もし google で本当に検索できなければ、あなたのレベルが少し高い証拠である。そのときは stackoverflow などのコミュニティで質問すればよい。&lt;/p&gt;
&lt;h2 id="9-バグを修正しなければバグは依然として存在する"&gt;9 バグを修正しなければ、バグは依然として存在する&lt;/h2&gt;
&lt;p&gt;この原則は、甘い期待を抱くな、バグが一瞬顔を出しただけだからといってダチョウ政策を取るな、ということを述べている。来るべきものは必ず来る。報いがないのではない、時が来ていないだけである。もちろん実際の状況は複雑でありうる。ソフトウェア工学はもともと時間・コスト・品質の妥協の産物である。しかし、たとえ一部のバグを見逃すとしても、それによって生じうる結果を分析し、十分な防御機構を築くべきである。だから多くのソフトウェア認証体系にはソフトウェア安全水準の規格がある。ここにはバグの再現と一を聞いて十を知るという問題も関わる。本当にバグの原因を見つけ、有効な修正手段を提供したことを確認し、同様の問題をすべて洗い出す必要がある。たとえばメモリリークを一つ修正したら、そうしたリークを引き起こすすべてのコードを修正しなければならない。&lt;/p&gt;
&lt;h2 id="附記デバッグしやすいコードの書き方"&gt;附記：デバッグしやすいコードの書き方&lt;/h2&gt;
&lt;p&gt;デバッグの原則から逆に見ると、ソフトウェアの構築自体にも大いに役立つ。何しろデバッグは穴を塞ぐだけのものであり、書かれるコードのバグが少ないほうがよく、デバッグのときに便利なように手段を多く用意しておくのがよい。ここでも個人の経験を簡単にまとめておく。不断の改善が必要かもしれない。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;プログラムにはログの仕組みを備えるべきで、プログラムの debug 版ではデバッグ情報を出力しやすくする。release 版であっても、ソフトウェアに問題が発生したとき（たとえば C# で例外が投げられたとき）の詳細な状況を記録すべきである；&lt;/li&gt;
&lt;li&gt;人間が読むのに適したコードを書くべきである。そうでなければ後の保守をする人が読んでも分からない。どうデバッグせよというのか。どう書くかは『コードコンプリート』に最も詳しく書かれている。各種の慣習的な書き方のほかに、相手の身になって考えることも重要だと思う；&lt;/li&gt;
&lt;li&gt;各種のクラスライブラリやツールを使うときは、できるだけ原理を理解し、公式ドキュメントを丹念に読む。たとえばメモリリークをどう防ぐか、マルチスレッドの状況にどう対処するかを詳しく説明しているクラスライブラリもある。よく見てから使えば、禍根を植えることが少なくなる。少なくともデバッグのときに蠅のように当てもなく原因を推測する必要はなくなる；&lt;/li&gt;
&lt;li&gt;テストしやすいコードを書く。そうすればデバッグのときに問題を切り離しやすく、二分法も使いやすい。この点は TDD の実践が比較的十分に論じており、私自身も強化する必要がある。&lt;/li&gt;
&lt;/ol&gt;</description><content:encoded><![CDATA[<p>一般に、エンジニアがあるものを芸術、あるいは玄学と呼ぶときは、その難度が高く、従うべき規則があまりないことを意味する。デバッグはこの範疇に属する。ほとんどすべてのプログラマは、バグに翻弄されて七転八倒した経験を持つ。時にあれこれいじっているうちに問題は解決したように見えるが、なぜ解決したのかは分からない。それを説明不能な玄学現象として扱うしかない。最も厄介なバグは、たまにしか現れず再現が難しいもので、学名をハイゼンベルクバグという。</p>
<p>しかし、ソフトウェアはやはり科学技術の範疇に属するものであり、デバッグもまた技術的な仕事であるべきだ。「デバッグの法則」（Debugging Rules!）は、デバッグ技術を扱った珍しい書物である。九つの普遍的な原則は、ソフトウェアとハードウェアの開発・保守に当てはまるだけでなく、日常生活にさえ応用できる。この本を見つけるのにも少し苦労したが、幸いなことに図霊コミュニティには<a href="http://www.ituring.com.cn/book/84"
   rel="noopener noreferrer" target="_blank" class="external">正規の電子書籍</a>
が売られていた。</p>
<p>正直に言えば、著者が挙げる例の多くはハードウェア寄りで、理解するのにやや隔たりを感じる。しかし九つの原則は実に実用的で、私の以前の開発・デバッグの経験の中でも無意識にいくつかはまとめていたかもしれない。それでもこの本を読むと、武術の心法を手に入れたような感覚がある。読んだ後、以下で九つの原則をもう一度たどり、理解を深める。</p>
<h2 id="1-システムを理解する">1 システムを理解する</h2>
<p>これは最も重要な原則である。バグを分析するには、当然システムがどのように動作するかを理解する必要がある。そのためには基本原理をいくつか学び、身につける必要があり、具体的なクラスライブラリ、ツール、技術については関連するドキュメントを真剣に読む必要がある。私が公式ドキュメントを真剣に読む習慣を身につけたのはここ数年のことである。検索エンジンの発達によって、我々は google 指向プログラミングの習慣を身につけてしまった。問題に直面すると、いつも一気に解決しようとする。結果として多くの回り道をすることになる。道標を見る時間を惜しむからだ。本書は特に「一字一句」マニュアルを読むことを強調しており、これは浮ついた人間への頭からの一撃である。システムを理解せずにデバッグを始めることと、原理を理解せずにコードを書くことは一脈相通じるもので、俗に「偶然指向プログラミング」と呼ばれる。</p>
<h2 id="2-失敗を再現する">2 失敗を再現する</h2>
<p>この原則はバグを再現することの重要性と方法を説く。バグは必ずある特定の条件下で発生する。その特定の条件を丹念に突き止めれば、半分は成功したも同然である。私のハードウェア保守の経験から言えば、バグを発見した人がその過程全体を詳細に記録できれば、非常に役立つ。多くのバグは発生条件が厳しく、だからプログラマの口癖の一つが「私のところでは問題ないです」なのである。こうしたバグに対しては、まずシミュレーションの方法を見つけるのがよい。たとえばソフトウェアの動作や相互作用の速度を上げる、ストレステストを行うなどである。次に、ソフトウェア内部に詳細で合理的なバグ記録の仕組みを持ち、内部から再現条件を見つけやすくする。</p>
<h2 id="3-考えず見る">3 考えず、見る</h2>
<p>この原則は観測の重要性を強調する。空想するよりも現実に向き合うことが大切である。言語は思考に影響する。だからバグに遭遇したときの口癖は「ちょっと見てみます」であるべきで、「たぶん〜が原因だと思います」ではない。我々はあらゆる手段を尽くしてバグの細部をはっきりと見るべきであり、成熟したプログラムにはデバッグ用の基盤が備わっているべきである。私の経験から言えば、少なくともログは不可欠である。初級のプログラマはブレークポイントだけに頼ってデバッグする習慣があるが、問題はブレークポイント自体がプログラムの実行フローを変えてしまうことである（特にマルチスレッドの場合）。もちろん推測も依然として有用で、観測の範囲を絞り込む助けになる。あるいは少なくとも観測の優先順位を決めるのに役立ち、そこで経験が生きる。しかしどうであれ、経験は観測に代わりえない。</p>
<h2 id="4-分割統治">4 分割統治</h2>
<p>二分探索法は探索の時間計算量を線形から対数に変えられる。これはプログラム中の探索アルゴリズムだけでなく、デバッグ時の方法論でもある。システムにこの意識があれば、鍵となるのはシステムをどう分割するかであり、ここで原則1に戻る。少なくともブレークポイントでデバッグするとき、バグの位置を見つけるのに二分法が使えるし、ログを挿入するときにもこの原則を応用できる。さらに本書ではバグ同士が互いに影響し合う現象にも触れている。私はむしろバグのシーソー現象と言ってもよいとさえ思う。解決の道は一つも見逃さないことである。バグをデバッグしているうちにコードの品質があまりに悪いことに気づき、リファクタリングが必要になることもある。そうでなければ砂の上に高台は築けない。効果的なリファクタリングを経て、バグが自然に発見され解消されることさえある。</p>
<h2 id="5-一度に一箇所だけ変える">5 一度に一箇所だけ変える</h2>
<p>この原則は実験科学における対照の原則に似ている。一度に一つの変数だけを考察し、正常な状況と異常な状況を比較し、いかなるテスト条件の差異も見落としてはいけない。前の条ではバグを一つも見逃さないと言ったが、一つずつ片付ける必要もある。リファクタリングの際も、理解していないコードをやたらにいじらないよう注意すべきである。多くのコードの危険性は、一箇所を触ると全体に響くことにある。これがバグ修正時の最大のリスクだと思う。しばしば一つのバグを消すと同時にいくつかのバグを増やすことになる。やはり原則1に戻り、システムを理解することである。とはいえ、先人が書いたコードを理解するのは容易ではないこともある。これはまた、コードを書くときに徳を積むべきことを我々に示している。コードはコンパイラのためにあるのではなく、後に我々のコードを保守する人のためにあるのだ。</p>
<h2 id="6-監査証跡を残す">6 監査証跡を残す</h2>
<p>この原則は記録の重要性と方法を説く。医者が診察するのと同じで、適切な質問をし、具体的で有効な答えを出す必要がある。私の経験では、バグのデバッグを始めると、人はすっかり深みにはまって抜け出せなくなることがある。記録を取ることで、思考の泥沼から自分を引き離し、デバッグの計画と方法を絶えず修正できる。専用のシステムソフトウェアでは、ソフトウェアの利用者に、バグの発生状況を有効な言葉で記録する方法を訓練する必要がある。記録がなければ、デバッグする側は適切な質問で利用者の記憶を呼び覚まさねばならない。また、深夜に解決できないバグは一眠りする必要があると思う。拡散思考が自動的に働き、たいてい朝には解決している。</p>
<h2 id="7-プラグを確認する">7 プラグを確認する</h2>
<p>この原則は、いわゆる「初歩的なミス」について述べている。母が「パソコンのスピーカーから音が出ない」と言ってきたら、まず電源が入っているかを聞く、という類である。あるいは main 関数を mian 関数と書いてしまうとか。一度、ブレークポイントを置いてデバッグしてもブレークポイントで止まらず、後で分かったのは、似たコードが二箇所あり、ブレークポイントを間違った場所に置いていたことだった。シナリオと経験に照らして、そのシナリオでよくある初歩的なミスを犯していないか自問すれば、多くの場合すぐに効果が出る。多くの電気機器の取扱説明書の故障 FAQ でも、プラグの確認が強調されている。初歩的なミスが「初歩的」なのは難度であって、頻度ではない。</p>
<h2 id="8-新しい視点を得る">8 新しい視点を得る</h2>
<p>この原則は助けを求めることと交流について述べている。ここで重要なのは、他者の視点によって自分の思考の固定観念を打ち破ることである。だから相手には現象だけを詳細に説明し、自分の推測は言わないほうがよい。相手の判断を汚染してしまうからだ。プログラマにとって、このときの google 指向プログラミングの力は驚異的である。おそらく九割の問題は、実際に質問する必要はなく、適切な検索語を見つけて検索すればよい。中国のプログラマにとって、英語で検索することは必須の技能である。もちろん、見つけたものを本当に理解することも非常に重要で、薬を見たらすぐ飲む、ではいけない。もし google で本当に検索できなければ、あなたのレベルが少し高い証拠である。そのときは stackoverflow などのコミュニティで質問すればよい。</p>
<h2 id="9-バグを修正しなければバグは依然として存在する">9 バグを修正しなければ、バグは依然として存在する</h2>
<p>この原則は、甘い期待を抱くな、バグが一瞬顔を出しただけだからといってダチョウ政策を取るな、ということを述べている。来るべきものは必ず来る。報いがないのではない、時が来ていないだけである。もちろん実際の状況は複雑でありうる。ソフトウェア工学はもともと時間・コスト・品質の妥協の産物である。しかし、たとえ一部のバグを見逃すとしても、それによって生じうる結果を分析し、十分な防御機構を築くべきである。だから多くのソフトウェア認証体系にはソフトウェア安全水準の規格がある。ここにはバグの再現と一を聞いて十を知るという問題も関わる。本当にバグの原因を見つけ、有効な修正手段を提供したことを確認し、同様の問題をすべて洗い出す必要がある。たとえばメモリリークを一つ修正したら、そうしたリークを引き起こすすべてのコードを修正しなければならない。</p>
<h2 id="附記デバッグしやすいコードの書き方">附記：デバッグしやすいコードの書き方</h2>
<p>デバッグの原則から逆に見ると、ソフトウェアの構築自体にも大いに役立つ。何しろデバッグは穴を塞ぐだけのものであり、書かれるコードのバグが少ないほうがよく、デバッグのときに便利なように手段を多く用意しておくのがよい。ここでも個人の経験を簡単にまとめておく。不断の改善が必要かもしれない。</p>
<ol>
<li>プログラムにはログの仕組みを備えるべきで、プログラムの debug 版ではデバッグ情報を出力しやすくする。release 版であっても、ソフトウェアに問題が発生したとき（たとえば C# で例外が投げられたとき）の詳細な状況を記録すべきである；</li>
<li>人間が読むのに適したコードを書くべきである。そうでなければ後の保守をする人が読んでも分からない。どうデバッグせよというのか。どう書くかは『コードコンプリート』に最も詳しく書かれている。各種の慣習的な書き方のほかに、相手の身になって考えることも重要だと思う；</li>
<li>各種のクラスライブラリやツールを使うときは、できるだけ原理を理解し、公式ドキュメントを丹念に読む。たとえばメモリリークをどう防ぐか、マルチスレッドの状況にどう対処するかを詳しく説明しているクラスライブラリもある。よく見てから使えば、禍根を植えることが少なくなる。少なくともデバッグのときに蠅のように当てもなく原因を推測する必要はなくなる；</li>
<li>テストしやすいコードを書く。そうすればデバッグのときに問題を切り離しやすく、二分法も使いやすい。この点は TDD の実践が比較的十分に論じており、私自身も強化する必要がある。</li>
</ol>
]]></content:encoded><category>coding</category><category domain="tags">プログラミング</category><category domain="tags">デバッグ</category><category domain="tags">読書ノート</category></item></channel></rss>