Translate

2016年12月30日金曜日

[書評]プリンシプルオブプログラミング

大学3年生になってようやく本を読むことの大切さに気づき始めたので,最近は積極的に読書をするようにしています.ただ読みっぱなしにするのも勿体無いので,書評という形でブログに書き残していきたいと思います.

書評を書くのは初めてなので何をどう書けば良いのかいまいちわからないのですが,精一杯頑張ります.統一感を出すために見出しなどは全ての本で共通にするつもりですが,まだ試行錯誤の段階なので変わるかもしれません.

さて,第1冊目は「プリンシプルオブプログラミング」です.


この本を買った理由

大学に入ってはじめてプログラミングにふれてから、C、Java、Pythonなど、数種類の言語を使ってきました。そしてプログラミングというものがどういうものなのか分かり始めてきた今、各言語の文法だけではなく、すべての言語に共通している「抽象的な概念」を知りたいと思い始めました。

どの言語を学ぶ時も、最初は初心者向けの書籍(あるいはウェブページ)をみて勉強すると思います。しかし初心者向けのものの多くは文法の説明がほとんどであり、各言語の特徴的な機能を理解するためには中級者向け以上のものを読まなければなりません。

しかし、中級者向けのものを読んでみても、プログラムコードが複雑で何をしようとしているのか理解するのは容易ではありません。さらに、なぜそのようなコードを書くに至ったのかを理解するのは至難の業でしょう。

そこで、いったん具体的な言語仕様に依存しない抽象的な概念をまなぶことで、プログラム全体を俯瞰的にみる力を手に入れたいと思いました。この力を手に入れれば、難しいコードに出会っても「道筋」がわかるので理解しやすくなるはずだと考えたのです。

そして、このような目的に適した本を探した結果、「プリンシプルオブプログラミング」に出会いました。似たような本で「リーダブルコード」という有名な本がありますが、この本では「リーダブルコード」とは異なり、プログラムコードが一切出てきません。なので、よりさらに抽象度が高い知識を手に入れることができます。

本の概要

この本は「プリンシプル」という短い標語と、その解説という構成になっています。解説は What, Why, How に分類されており、わかりやすいです。特に Why を解説する本はなかなかないので貴重です。

本の帯に書かれた「脱・初心者」という言葉の通り、いままで小規模な開発しか行ったことのない初心者が意識すべきことが書かれています。現役プログラマの方から見ると当たり前のことしか書かれていないらしいのですが、僕のようなプログラミング初心者には「常識」を手っ取り早く身に着けられる良い本だと思います。

この本から学んだこと

この本の内容を一言でいうと

シンプルなコードを書け!

です。本全体を通して「シンプル」とはなにか、そしてなぜ重要なのかを説明しています。

たとえば「シンプル」という言葉には、コードが短い、余計な機能を実装していない、テクニカルではなくだれでもわかる、プログラムを構成している各モジュールの依存関係がほとんどない、など、いろいろな意味を持っています。

そして、なぜこの「シンプルさ」が必要かというと、プログラムコードは書いたらそれで終わりではなく、ほかの人が書いたコードと組み合わせ、完成した後はそれを保守管理する必要があるからです。「シンプルさ」を維持することで、いつだれがみてもわかりやすいコードになり、また機能の拡張や置き換えが容易になります。

考察

この本はシンプルということを様々な角度からとらえて解説しています。読み進めていくと繰り返しが多いように感じますが、そのぶん読む側にとっては復習になり理解の助けになります。大規模な開発を始める前に、一度は読んでおいたほうがよい本だと思います。

2016年12月24日土曜日

万能なアルゴリズムは存在しない 〜ノーフリーランチ定理〜

大学の講義「人工知能」の課題でノーフリーランチ定理についての課題が出たので、ついでにブログにも載せておきます。

ノーフリーランチ定理とその解釈

題名に「万能なアルゴリズムは存在しない」と書きましたが、もう少し厳密に言うとこのようになります。
 コスト関数の極値を探索するあらゆるアルゴリズムは、全ての可能なコスト関数に適用した結果を平均すると同じ性能となる。( Wolpert and Macready、1995年、Wikipediaより又引用)
少し分かりにくいですね。そこで一般性を捨てて具体例を考えることで、分かりやすさを手に入れましょう。(といっても以下で考える具体例はほぼもとの命題に言い換えに過ぎません。)

 次のようなアルゴリズムを考えます。

関数$f(x)$を最小化するアルゴリズム

ただし、定義域の要素は有限個だとします。このときノーフリーランチは次のように言い換えられます。 

すべての関数に関して性能の平均を取ると、どのアルゴリズムも同じである。

このことから、次のようにもいえます。

関数$f(x)$が未知の場合、どのアルゴリズムが最適かはやってみないと分からない。

つまり、山登り法、最急降下法、焼きなまし法などすべてのアルゴリズムは問題によって得手不得手があり、どの問題にも万能な「最強のアルゴリズム」は存在しないのです。逆に言えば、最適なアルゴリズムを見つけてきたいのならば、解きたい問題についてよく理解し、その情報をアルゴリズムに反映せよ、といえます。

定理の略証

言いたいことはすべて書きましたが、「証明がないと信じられない」というかわいそうな素晴らしい人のために略証を与えておきます。この略証は、伊庭先生の研究室が発表した論文に沿って書かれており、記号などもなるべく同じものを使うようにしています。

 定義

集合$X,Y$: それぞれ関数$f$の定義域、値域をあらわす集合
ステップ数$k$: アルゴリズムを回した回数
集合$D_k^X,D_k^Y$: それぞれステップ数$k$で探索された$x,f(x)$の直積集合(の集合)
集合$D_k$: $=D_k^X\times D_k^Y$
アルゴリズム$a$: $d_k\in D_k$に対して,次に探索する$x_{k+1}=a(d_k)$を返す関数
集合$A$: アルゴリズム$a$の集合

ただし$X$の要素数は有限します。また同じ箇所を探索しても意味がないため、そのような探索はしないことにします。

定義を理解しやすいように$f(x,y)=x^2+2xy+3y^2+4y$とし$a$は山登り法(近傍をみて最良の点を採用する)アルゴリズムを考えます。

はじめに$a$は初期値を与えます($(x_1,y_1)=(0,0)$)。次に関数$f$によって$z_1=f(0,0)=0$が計算されます。このとき$d_1^X=((0,0)),\,d_1^Y=(0)$となります。

・・・・・・・・・・・・・・・・・・・・・
・・・(-2,2)(-1,2)(0,2)(1,2)(2,2)・・・
・・・?????・・・
・・・(-2,1)(-1,1)(0,1)(1,1)(2,1)・・・
・・・?????・・・
・・・(-2,0)(-1,0)(0,0)(1,0)(2,0)・・・
・・・??0??・・・
・・・(-2,-1)(-1,-1)(0,-1)(1,-1)(2,-1)・・・
・・・?????・・・
・・・(-2,-2)(-1,-2)(0,-2)(1,-2)(2,-2)・・・
・・・?????・・・
・・・・・・・・・・・・・・・・・・・・・

次にアルゴリズム$a$は$(0,0)$の近傍$(-1,-1),\cdots ,(1,1)$のうちひとつ$(-1,-1)$を返します。次に関数$f$によって$z_2=f(-1,-1)=2$が計算されます。このとき$d_2^X=((0,0),(-1,-1)),\,d_2^Y=(0,2)$となります。

・・・・・・・・・・・・・・・・・・・・・
・・・(-2,2)(-1,2)(0,2)(1,2)(2,2)・・・
・・・?????・・・
・・・(-2,1)(-1,1)(0,1)(1,1)(2,1)・・・
・・・?????・・・
・・・(-2,0)(-1,0)(0,0)(1,0)(2,0)・・・
・・・??0??・・・
・・・(-2,-1)(-1,-1)(0,-1)(1,-1)(2,-1)・・・
・・・?2???・・・
・・・(-2,-2)(-1,-2)(0,-2)(1,-2)(2,-2)・・・
・・・?????・・・
・・・・・・・・・・・・・・・・・・・・・

$k=3,\cdots ,9$まで$a$は$(0,0)$の残りの近傍を返します。9ステップ後には$d_9^X=((0,0),(-1,-1),\cdots ,(1,1)),\, d_9^Y=(0,2,\cdots ,10)$となります。

・・・・・・・・・・・・・・・・・・・・・
・・・(-2,2)(-1,2)(0,2)(1,2)(2,2)・・・
・・・?????・・・
・・・(-2,1)(-1,1)(0,1)(1,1)(2,1)・・・
・・・?6710?・・・
・・・(-2,0)(-1,0)(0,0)(1,0)(2,0)・・・
・・・?101?・・・
・・・(-2,-1)(-1,-1)(0,-1)(1,-1)(2,-1)・・・
・・・?2-1-2?・・・
・・・(-2,-2)(-1,-2)(0,-2)(1,-2)(2,-2)・・・
・・・?????・・・
・・・・・・・・・・・・・・・・・・・・・

次にアルゴリズム$a$は$(0,0)$の近傍のうち最良な$(1,-1)$の近傍のうちひとつ$(0,-2)$を返します。次に関数$f$によって$z_{10}=f(0,2)=4$が計算されます。このとき$d_{10}^X=((0,0),(-1,-1),\cdots ,(1,1),(0,-2)),\, d_{10}^Y=(0,2,\cdots ,10,4)$となります。残りの近傍についても同様にします。(ただし探索済みの箇所はもう探索しないことに注意)

・・・・・・・・・・・・・・・・・・・・・
・・・(-2,2)(-1,2)(0,2)(1,2)(2,2)・・・
・・・?????・・・
・・・(-2,1)(-1,1)(0,1)(1,1)(2,1)・・・
・・・?6710?・・・
・・・(-2,0)(-1,0)(0,0)(1,0)(2,0)・・・
・・・?1014・・・
・・・(-2,-1)(-1,-1)(0,-1)(1,-1)(2,-1)・・・
・・・?2-1-2-1・・・
・・・(-2,-2)(-1,-2)(0,-2)(1,-2)(2,-2)・・・
・・・??410・・・
・・・・・・・・・・・・・・・・・・・・・

中心$(1,-1)$が全近傍で最良なことがわかったのでアルゴリズム$a$は終了します。

証明

ここでは証明の方針を書くだけにとどめて、残りは伊庭先生の研究室が発表した論文に任せることにします。あと、ここだけ「ですます調」をやめます。ですます調の証明は気持ち悪いので。

性能は$d_k^Y\in D_k^Y$で評価できる。なぜなら求めるべき最小値が$d_k^Y$の成分、すなわち探索を行った全ての$f(x)$の最小値であるからである。そこでアルゴリズム$a$において以下の式を評価することにする。
\[
\sum_{f\in F}P(\mathcal{D}_k^Y=d_k^Y|\mathcal{A}=a,\mathcal{F}=f)
\]
ただしカリグラフィー書体は確率変数を意味する。ノーフリーランチ定理の主張は、上の式がアルゴリズム$a$に寄らないというものである。ここでこの確率を$d_k^Y$に関して分割する。
\[
\sum_{t\in D_k^Y}\sum_{f\in F_{a,t}}P(\mathcal{D}_k^Y=d_k^Y|\mathcal{A}=a,\mathcal{F}=f)
\]
$F_{a,t}$は$a,t$を満たす関数$f$の集合$F_{a,t}\subset F$である(元の論文とは記号を変えた)。ここで分割した和が元の式の和をダブりなく全て網羅していることを確かめる。全て網羅していることはほぼ自明のためここでは証明は省略する。

まずアルゴリズム$a$は初項を与える。そして関数$f$とアルゴリズム$a$が与えられていれば、関数全般の性質から$d_k^Y$が一意に定まる(アルゴリズム$a$は一種の関数であることに注意)。つまり、$a$が与えられているとき、$d_k^Y$が違えば$f$も違うことがわかるので、ダブりがないことが示された。

$f\in F_{a,t}$である限り、$t\neq d_k^Y$ならば$f$をいくら動かしても$d_k^Y$は取り得ない。また逆に$t=d_k^Y$ならば$f$をいくら動かしても必ず$d_k^Y$を取る。故に上の式の値は$f\in F_{a,t}$の個数に等しい。つまり$k$ステップを終えた後で残っている$X$の要素(個数:$|X|-k$)から、$Y$の要素(個数:$|Y|$)への関数の全個数を数えればよく、これは$|Y|^{|X|-k}$である。以上をまとめると
\[
\sum_{f\in F}P(\mathcal{D}_k^Y=d_k^Y|\mathcal{A}=a,\mathcal{F}=f)=|Y|^{|X|-k}
\]
となり、アルゴリズム$a$に依存しない。故にノーフリーランチ定理は示された。