人気ブログランキング | 話題のタグを見る

パターン認識読書法

最近ニューロコンピュータの本を読み始めた。ニューロコンピュータとは、一口に言うと、人間のパターン認識能力をコンピュータでシミュレーションさせようという研究だ。

ニューロコンピュータでは、コンピュータのプログラミングは、アルゴリズムのプログラム言語への翻訳ではなく、パターンを学習させるという方法が取られる。

具体的には、たくさんの画像データとその解釈を機械に与えて、手書き文字のようなパターンを判別できるようにしたり、あるいは、多量のビッグデータからそのデータに内在する傾向を読み取らせたりする。このようにニューロコンピュータでは、プログラムの動作を決めるのが、プログラミングではなくて学習であることが特徴だ。

この学習によるパターン認識が、今までの手続き型のプログラミングをはるかに越えるパターン認識力をもたらしてくれることが注目されている。しかし、人間の脳は、すでにそのような高度なパターン認識を獲得する能力を有している。これを読書に活用できないだろうか。

パターン認識の観点から読書法を考えたとき、今までの文章を逐次的に追いかける普通の読書法とは全く異なってくる。

先ず、ページをパラパラと気ままに繰りながら、目についた所、興味のありそうな所をちょっとだけ読んでいく。スキミングと呼ばれる手法で、雑誌やグラビアを読むときに無意識にやっている方法だ。

パターン認識読書法では、このスキミングを行う際に内容をあまり詮索しない。チラチラと読みとった内容が自然にパターンを形づくるまで、ひたすら情報のインプットだけを続けるのだ。したがって、ページを開く順序はランダムでいいし、知らない用語が出てきても特に調べることもしなくて良い。意味の理解はパターンがひとりでに現れるまで待つことになる。

ただし、本に書かれていることの意味が自然に理解できるためには何回も本に目を通す必要がある。普通に読む場合より効率がいい訳ではない。脳の中でパターン認識の結合ができるためには、シナプス結合の効率がそれにあわせて変化する必要がある。ニューロコンピュータのアナロジーが脳でも働いているとすれば、刺激の提示に対して、シナプスの結合性の変化は徐々にしか起こらない。きちんとしたパターンが生成するためには、原則刺激の提示の十分な回数が必要だからだ。

それでは、なぜ、そのような効率の悪い読み方をするのかという話になるが、この方法は、あまり馴染みのない知識の本を読むときに便利なのだ。良く知らない分野の知識は、そもそもフレームワーク自体を持ち合わせていない。しかし、意味不明のまま、スキミングで眺めているうちに、何となくパターンが見えてくる。脳のパターン認識能力のおかげだ。

全てをパターン認識でやりとおすことはかえって効率が悪いだろう。ある時点で通常の読み方に切り替える必要がある。しかし、パターン認識で概要が掴めた後は、普通の読み方をする場合も随分楽に感じる。

脳のパターン認識の力は普段あまり意識することはない。パターン認識の作業が無意識に行われているからだ。しかし、ノイマン型のコンピュータが苦手なことをスイスイこなすスーパーコンピュータを自前で持っているのだから、意識的に活用することも考えて良いのではないだろうか。
# by tnomura9 | 2015-05-12 10:05 | 考えるということ | Comments(0)

System.Random (17) Random クラス

System.Random 解読シリーズ

Random 型クラスは、乱数ジェネレータを利用して、様々な型のデータを発生させるときのクラスメソッドを提供している。クラスメソッドは Int 型でも Double 型でも同様の動作を行うときに共通の名前を使うことができ、操作の抽象化ができる。クラスメソッドはこのように多相型関数だが、実装は instance 宣言の時にそれぞれの型に応じて行う必要がある。

以下に、System.Random.hs のソースファイルの Random 型クラスの class 宣言のコメントを日本語訳したものを示す。

Random クラスは、乱数ジェネレータを利用して、プログラマに様々なデータ型の乱数を発生する手段を提供する。

最小限実装を定義すべき Random クラスの関数は randomR と random だ。

class Random a where

randomR は乱数の範囲 (lo, hi) と乱数ジェネレータ g を引数に取り、範囲 [lo, hi] の間の均一に分布する乱数および新しい乱数ジェネレータを値として返す。lo > hi の時の動作については特に明示していない。連続なデータ型については、lo と hi の値についての規定はないが、それらは、実装と範囲の値による。

  randomR :: RandomGen g => (a,a) -> g -> (a,g)

random は randomR と同じように乱数を発生するが、乱数のデータ型のデフォールトの範囲を使う。

Bounded タイプ(例えば Char 型のような有界のデータで Bounded クラスのインスタンス)では範囲はそのデータ型の全体になる)

分数型のデータでは、範囲は一般に [0,1) のような半閉区間になる。

整数型のデータでは、範囲は Int 型の範囲だ。

  random  :: RandomGen g => g -> (a, g)

randomR の変種。randomR と同じように指定した範囲の乱数を発生させるが、単一の乱数ではなく、乱数の無限リストを生成する。新しいジェネレータは返さない。

  randomRs :: RandomGen g => (a,a) -> g -> [a]
  randomRs ival g = x : randomRs ival g' where (x,g') = randomR ival g

random の変種。random と同じようにデフォールトの範囲の乱数を発生させるが、単一の乱数ではなく、乱数の無限リストを生成する。新しいジェネレータは返さない。

  randoms  :: RandomGen g => g -> [a]
  randoms  g      = (\(x,g') -> x : randoms g') (random g)

randomR の変種でグローバルの乱数ジェネレータを利用する。"System.Random#globalrng" を参照。

  randomRIO :: (a,a) -> IO a
  randomRIO range  = getStdRandom (randomR range)

random の変種で、グローバルの乱数ジェネレータを利用する。"System.Random#globalrng"を参照。

  randomIO  :: IO a
  randomIO   = getStdRandom random

このように、コメントを除くと class 宣言による型クラスのプログラムは、単に多相型関数の型の宣言をしているもの (randomR, rondom) と、その型クラスの多相型関数を利用して関数を定義しているもの(rondomRs, romdoms) の2種類がある。

型クラスについては、Haskell の学習を始めた頃は説明を読んでものことかよくわからなかった。それは Int 型や String 型などの低レベルのデータ型からボトムアップ式に理解しようとしていたからだ。

しかしながら、トップダウンで型クラスを眺めるとその意味が良く分かる。初めに抽象的な関数を型クラスのクラスメソッドとして定義しておけは、パターンが同じでデータ型が違うような処理に対して、同じ意味の処理に対しては同じ関数で対応できる。

実際のプログラムでもいきなり個々のデータ型についてのプログラムを始めるよりは、最初に型クラスとそのクラスメソッドを定義しておいて、あとで実装を考えるという方向性のほうが全体的な見通しが良くなるだろう。

今回の解読では型クラスの便利さに目覚めることができた。
# by tnomura9 | 2015-05-07 18:35 | Haskell | Comments(0)

自然数の集合とラッセルのパラドックス

自然数の集合と「自分自身を要素としない集合の集合」というラッセルの集合はどちらも無限の要素を含むはずだ。しかし、自然数の集合は安定して集合として存在できるのに、ラッセルの集合はパラドックスになってしまう。一体どこが違うのだろうか。

無限集合は無限の要素を含むために、全てを列挙することはできない。無限集合の全ての要素を提示することはできないのだ。そこで、数学では無限集合を定義するのに内包公理を使う。無限集合のどの要素 x も述語 P(x) を充足していれば、述語 P(x) を充足する要素の集まりという内包的定義で集合が確定できると言ってもいいだろうということだ。問題はこの述語 P(x) をどのように定義するかということだ。

自然数は無限の要素を含んでいるが、ある数 x が自然数かどうかを判断するのにどのような述語 P(x) を定義したら良いのだろうか。どのような数を持ってきてもその数が自然数かどうかを反論しようもなく判断できる述語とはどのようなものなのだろうか。それには数学的帰納法が使われる。

たとえば、ある数が自然数かどうかを判断するには、その数を一定のルールで別の自然数に変換すればいい。その変換を有限回の手続きで繰り返していって、明らかに自然数である数に変換できれば元の数が自然数であると確定できるという考え方だ。ある数が自然数であるかどうかを判断する手続きは有限だが、どの数を持ってきても、この有限の手続きに帰着できるのなら、自然数の無限の要素を捕まえたと言っていいのではないかということだ。無限を有限の手続きで捉えるというこのトリッキーな方法はしかし、数学のあらゆる場面で活用されている。

ちなみに自然数を定義する帰納的定義は次のようになる。

1)1は自然数である。
2) n が自然数であれば n + 1 も自然数である。

このルールを使って 10 が自然数であることを確認するためには、2)のルールを使って、9 が自然数でなければならない。さらに、9 が自然数であるためには 8 が自然数でなければならない。この推論を繰り返すと 10, 9, 8, …, 2, 1 と1にたどり着く。1は明らかに自然数だから、2が自然数であることがわかる。2が自然数であれば、3も自然数だ。先ほどの推論を逆に繰り返すと最終的に 10 が自然数であることがわかる。

あるいは最初から 1 から始めても良い。1が自然数だから2も自然数だ。2が自然数ならば3も自然数だ。これを繰り返して 10 に到達すれば 10 が自然数であることがわかる。

数学的帰納法を使う場合、論証が有限回の推論で完了するということが大切だ。自然数は無限集合であるが。そのどの要素も有限界の推論で自然数の要素であることが論証できる。

しかし、数学的帰納法の特徴について特筆すべきはこのルールが生成的なルールであるということである。n + 1 が自然数であることを論証するためには 1 … n が自然数であることが既知である必要がある。既知の 1 … n から未知の自然数の要素である n + 1 が自然数の要素であることを論証できる。自然数の集合と「自分を要素としない集合の集合」というラッセルの集合との違いは、この再帰的定義の生成的な側面に注目するとわかる。

自然数の帰納的定義が生成的なルールであると同じように、「自分を要素として含まない集合の集合」というラッセルの集合の定義も生成的な定義である。そのことは次のように考えることでわかる。

まず、「犬の集合」は自分自身を要素として含まないから、明らかにラッセルの集合の要素である。当面このような明らかにラッセルの集合の要素である集合を集めてみる。これは有限の要素の集まりだ。この既知の要素から新しい自分自身を要素として含まない集合というラッセルの集合の要素を作り出すことができれば、これは生成的なルールである。その生成的なルールは何かと言うと、要素の集まりから集合を作ることができるという集合の定義である。

「犬の集合」のようなラッセルの集合の要素の有限個の集まりから、新しい「自分自身を要素として含まない集合」を作り出すことができる。たとえば、「犬の集合」のみを要素とする集合を考えると、これは、新しい「自分自身を要素として含まない集合」だ。同様に最初に集めた有限の要素の任意の組み合わせからなる集合も新しい「自分自身を要素として含まない集合」になる。しかし、元となる要素の個数が有限なので、新しく生成される要素の数も有限だ。

有限の要素から、新しい有限の要素を生成し、新しい要素を含む有限の要素が生成される。ラッセルの集合にはこのような生成的なルールが適用されているのがわかる。この生成ルールで作り出される集合はどれも「自分自身を要素として含まない集合」である。したがって、これらを集めた「自分自身を要素として含まない集合の集合」も存在するはずだと思える。

しかし既存の「自分自身を要素として含まない集合」の集まりから、集合とはものの集まりであるという集合の定義を使って新しい「自分自身を要素として含まない集合」を作り出すことができるというラッセルの集合の生成的ルールは、どのような「自分自身を要素として含まない集合」の集合があったとしても、生成的ルールによって、その集合の要素とは異なる「自分自身を要素として含まない集合」を作り出すことができてしまう。この性質こそがラッセルのパラドックスを生み出す原因となるのだ。

それは次のように考えるとわかる。いま、「自分自身を要素としない集合」全てを集めた集合 R を考えてみよう。ラッセルの集合の生成的ルールから、この集合 R の要素を使って R の要素には含まれない「自分自身を要素として含まない集合」を作り出すことができる。最低限 R 自身が R の要素には含まれないが、「自分自身を要素として含まない集合」だ。しかし、これは R の要素が「自分自身を要素として含まない集合」の全てであるという仮定に反する。したがって、「自分自身を要素としない集合」全てを集めた集合 R は存在できないことがわかる。

自然数の生成的定義は既知の自然数にない新しい n+1 という自然数が生成されてもそれは自然数の集合のメンバーであると考えることができた。しかし、ラッセルの集合の生成的定義は既知のラッセル集合の要素から新しい「自分自身を要素として含まない集合」を生成したときには、その集合は既知の要素の集合には含まれることがない。このことが、「自分自身を要素として含まない集合」全てを集めた集合を作れなくしてしまっている。

ものの集まりを集合というものと考えるという集合の定義は、つねに既知の要素から新しい「もの」を作り出すという生成的な性質を持っている。この生成的な集合の定義の性質こそが、ラッセルのパラドックスを発生させる原因だったのだ。
# by tnomura9 | 2015-05-03 07:26 | 考えるということ | Comments(0)

System.Random (16) StdGen -- mkStdGen

System.Random 解読シリーズ

System.Random の RandomeGen クラスの解読も mkStdGen 関数を解読すれば終わりだ。mkStdGen 関数のコードは次のようになる。

{- |
The function 'mkStdGen' provides an alternative way of producing an initial
generator, by mapping an 'Int' into a generator. Again, distinct arguments
should be likely to produce distinct generators.
-}
mkStdGen :: Int -> StdGen -- why not Integer ?
mkStdGen s = mkStdGen32 $ fromIntegral s

mkStdGen32 :: Int32 -> StdGen
mkStdGen32 s
| s < 0     = mkStdGen32 (-s)
| otherwise = StdGen (s1+1) (s2+1)
      where
        (q, s1) = s `divMod` 2147483562
        s2      = q `mod` 2147483398

createStdGen :: Integer -> StdGen
createStdGen s = mkStdGen32 $ fromIntegral s

mkStdGen 関数は 整数 s に mkStdGen32 関数を関数適用しているだけだ。mkStdGen32 関数は引数の s を 32 ビットの整数に整形し、StdGen 型の2つのフィールドに 32 ビットの整数を納めて返す。引数 S はまず 2^31 で除算され、商が q に余りが s1 にバインドされる。

divMod 関数は整数の除算の商と余りのペアを返す関数だ。

Prelude> 5 `divMod` 3
(1,2)

また、divMod の除数には 2147483562 がつかわれているが、これは 2^31 とは違う。素数か何かの関係なのだろうか。理由はわからなかった。

Prelude> 2^31
2147483648

q は 32 ビットを超える場合もあるので `mod` 2147483398 で 32 ビットに整形される。divMod のときとは違う除数が使われている。最後に フィールドの値が 0 にならないように +1 されて、StdGen 型のコンテナに入れられて返される。

createStdGen 関数はコードを見ると、mkStdGen 関数と同じだ。おそらく互換性のための関数だろう。

StdGen 型の解読でわかったのは、StdGen 型の乱数ジェネレータといっても、単にペアの 32 ビットの整数値だということだ。これを利用して線形合同法で乱数を発生させているだけだ。シードに二つの整数が用いられているのは、シードが一つだけだと、乱数列が一種類しか発生しないからだ。シードの値を変えてもそれは乱数列のどの位置から始めるかを変えているだけなので、2次元配列に利用しても分布に規則性が発生してしまう。乱数ジェネレータに2つの整数を利用するのは、おそらく、それを解消するための工夫なのだろう。

次の記事からは Random クラスの解読になる。これは、StdGen を用いて Int や Double 型の乱数を発生させるための関数を定義している。乱数には整数もあるし実数もある。乱数の発生という操作は抽象的な概念だ。乱数の発生のような抽象的な操作をプログラムする場合は、Random のような型クラスを定義しておくと整数の乱数も実数の乱数も同じ名前の関数で取り扱うことができる。

RandomGen クラスのインスタンスは StdGen 型しかないが、将来の拡張性も考えて RandomGen クラスを定義したのだろう。型クラスの考え方は、Haskell の入門時には意味がよくわからないが、System.Random のような実際のコードを調べてみると、まず、型クラスを考えて、それからそのインスタンスになるデータ型の実装を考えるという順番でプログラムを行う方が便利であることが納得出来る。
# by tnomura9 | 2015-04-24 03:37 | Haskell | Comments(0)

ベクトルの内積

ベクトル x, y の内積の計算は次のようになる。

x .y = Σ xiyi

この内積はニューロコンピュータではよく出てくる計算なので、Haskell ではどう計算するのか考えてみた。

Prelude> let ip x y = sum $ zipWith (*) x y
Prelude> ip [1,2,3] [4,5,6]
32

これを眺めているうちに zipWith のコードがどうなっているのか気になったので調べてみた

-- | 'zipWith' generalises 'zip' by zipping with the function given
-- as the first argument, instead of a tupling function.
-- For example, @'zipWith' (+)@ is applied to two lists to produce the
-- list of corresponding sums.
--
-- 'zipWith' is right-lazy:
--
-- > zipWith f [] _|_ = []
{-# NOINLINE [1] zipWith #-}
zipWith :: (a->b->c) -> [a]->[b]->[c]
zipWith _f []     _bs    = []
zipWith _f _as    []     = []
zipWith f  (a:as) (b:bs) = f a b : zipWith f as bs

非常に基本的なリストの演算の再帰的定義だった。ややこしい呪文はなしだった。ついでに zip も調べてみた。基本的なコードは zipWith と全く同じ基本的な再帰的定義だった。

-- | 'zip' takes two lists and returns a list of corresponding pairs.
-- If one input list is short, excess elements of the longer list are
-- discarded.
--
-- 'zip' is right-lazy:
--
-- > zip [] _|_ = []
{-# NOINLINE [1] zip #-}
zip :: [a] -> [b] -> [(a,b)]
zip []     _bs    = []
zip _as    []     = []
zip (a:as) (b:bs) = (a,b) : zip as bs

Haskell は zipWith のような基本的な関数もブラックボックスではないのがうれしい。
# by tnomura9 | 2015-04-22 19:00 | Haskell | Comments(0)