線形代数と確率過程の話
はじめに
理工系の学生なら、学部で線形代数を習うことでしょう。学部での線形代数は、主に行列やベクトルの性質を調べるものとして学ぶと思います。そこで面倒な計算をして対角化とか固有ベクトルとか固有値とかやって、「なんで行列を対角化しなきゃいけないのか?」「固有ベクトルとか求めて何がうれしいのか?」がいまいちわからないまま卒業し、その後の機械学習ブームで「時代は線形代数!」とか言われて教科書を引っ張り出したは良いけど、でも結局よくわからないままそっと閉じる、なんて人も多いのではと思います。
線形代数は、代数学の一分野です。代数学とはその名の通り「具体的な数」の「代」わりに「記号」を使って式を表現し、その性質を調べる学問です。小学校で「つるかめ算」や「過不足算」をやったと思いますが、中学に入るとそれがすべて方程式や連立方程式で解けてしまうことを習ったと思います。一度方程式に落としてしまえば、もとがリンゴであったか亀の足であったかを全く忘れて、機械的に式を変形していくと答えが出てしまう、そういう抽象性が代数学の強みです(数学は一般にそういう学問ですが……)。逆に言えば、一度式に落としてしまった抽象的な状態では、元が何であったかの情報が落ちています。大学で習う線形代数は、この「抽象化された状態」である行列やベクトルを扱うため、式変形は追うことができても、なんとなく腑に落ちない、という感じになりがちです。また、たまに幾何学的な意味、例えば「行列は回転と拡大縮小を表しており、固有値は拡大率を、固有ベクトルは拡大方向を表す」といった説明を受けても、「だからなに?」という感想で終わりがちです(例えば筆者)。
線形代数は、本当に様々なところに顔を出す学問で、行列や固有値の意味も文脈によって異なります。ここでは行列や固有値が出てくる一つの例として、確率過程との関わりを見てみようと思います。
すごろくゲーム
簡単なゲームを考えます。0から4までの数字がついたマスが並んでおり、2からスタートです。コインを投げ、表なら一つ上の数字へ、裏なら一つ下の数字へ移動し、4のマスに到達したら「勝ち」で賞金1万円がもらえますが、0のマスに到達してしまうと「負け」で1万円を払わなければいけません。コインには不正が無いものとしましょう。このゲームの獲得賞金の期待値は0であることはなんとなくわかると思います。
さて、このギャンブルの胴元から、「5千円払えば、3のマスからスタートさせてあげます」と言われたとしましょう。この提案に乗るべきでしょうか?それともこれは損な取引でしょうか?
まずは何も考えずにシミュレーションしてみましょう。知りたいのは、確率1/2で左もしくは右に行くすごろくで、iのマスからスタートした場合に「勝ち」に行く確率です。素直に実装するとこうなるでしょうか。
import random
N = 4
def one_trial(start):
position = start
while position in range(1, N):
position += random.choice([-1, 1])
return position == N
def win_probability(start):
n_trial = 100000
win = 0
for _ in range(n_trial):
if one_trial(start):
win += 1
win_rate = win / n_trial
print(f"{start} {win_rate}")
for s in range(1, N):
win_probability(s)
実行してみましょう。
$ python3 mc.py
1 0.25063
2 0.50053
3 0.74683
なんとなく、上から
$ python3 mc.py
1 0.20026
2 0.40096
3 0.60016
4 0.79874
どうやら、
マルコフ行列
さて、時刻
同様に、時刻
また、一度「勝ち」「負け」が確定したら、もう動きません。「勝ち」「負け」には、3や1のマスから確率1/2で行くので、
これをすべて並べて書くと
この式の右辺を見ると、それぞれの確率になにか係数を書けて足し合わせたものになっています。こういう形を線形結合と呼びます。何かが線形結合で書けている場合、行列とベクトルの形に書き直すことができます。
これを
と書きましょう。
例えば、時刻
これは、ある時刻に2のマスにいた場合、次の時刻で確率1/2で1のマスに、確率1/2で3のマスにいるよ、という意味です。この時、どのような履歴をたどって2のマスにたどり着いたかは、遷移確率に関係しません。このように「次の状態が履歴によらず、現在の状態だけに依存する」ような過程をマルコフ過程と呼びます。先程の遷移行列はマルコフ行列とも呼ばれます。
状態ベクトルと遷移行列の式をもう一度見てみましょう。
これは、
です。我々が知りたいのは、時間が十分に経過した後の振る舞い、つまり
です。つまり、この行列
数値対角化
numpy
やnumpy.linalg
もインポートしておきましょう。
import numpy as np
import numpy.linalg as LA
N = 4
M = np.zeros((N + 1, N + 1))
for i in range(N + 1):
if i in (0, N):
M[i][i] = 1.0
else:
M[i + 1][i] = 0.5
M[i - 1][i] = 0.5
まずは、Mを十分な回数かけた場合、たとえば100回かけた行列を表示してみましょう。
np.set_printoptions(precision=3)
Minf = M
for _ in range(100):
Minf = M@Minf
print(Minf)
$ python3 eigen.py
[[1.000e+00 7.500e-01 5.000e-01 2.500e-01 0.000e+00]
[0.000e+00 0.000e+00 4.441e-16 0.000e+00 0.000e+00]
[0.000e+00 4.441e-16 0.000e+00 4.441e-16 0.000e+00]
[0.000e+00 0.000e+00 4.441e-16 0.000e+00 0.000e+00]
[0.000e+00 2.500e-01 5.000e-01 7.500e-01 1.000e+00]]
どうやら、
になりそうな雰囲気ですね。例えば、最初に1のマスにいた場合、
となるので、確率
と、元のベクトルの定数倍になる時、このベクトル
です。これを
固有ベクトルを横に並べて作った行列を
と書けます。両辺の右から
つまり、固有ベクトルを並べた行列numpy.linalg.eig
で得ることができます。固有値は一次元配列で渡されるので、numpy.diag
で対角行列にしてやりましょう。
w, P = LA.eig(M)
Pinv = LA.inv(P)
D = np.diag(w)
print("D=")
print(D)
print("P^-1MP=")
print(Pinv@M@P)
実行結果はこんな感じです。
D=
[[ 1. 0. 0. 0. 0. ]
[ 0. 1. 0. 0. 0. ]
[ 0. 0. 0.707 0. 0. ]
[ 0. 0. 0. -0.707 0. ]
[ 0. 0. 0. 0. 0. ]]
P^-1MP=
[[ 1.000e+00 0.000e+00 6.054e-17 6.910e-17 4.531e-17]
[ 0.000e+00 1.000e+00 0.000e+00 1.943e-16 5.551e-17]
[ 0.000e+00 0.000e+00 7.071e-01 2.153e-16 6.673e-17]
[ 0.000e+00 0.000e+00 -2.539e-17 -7.071e-01 4.310e-17]
[ 0.000e+00 0.000e+00 -7.173e-17 -1.205e-16 5.324e-34]]
誤差がちょっと出ていますが、確かに数値的に一致していそうです。
行列のベキ乗
さて、行列を対角化してうれしいのは、行列のベキ乗が簡単に計算できるからでした。実際、
なので、
です。ここで
となります。これを
Dinf = np.diag([1, 1, 0, 0, 0])
Minf = P@Dinf@Pinv
print(Minf)
実行結果はこうなります。
[[1. 0.75 0.5 0.25 0. ]
[0. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. ]
[0. 0.25 0.5 0.75 1. ]]
確かに
になっていますね。
固有ベクトルによる展開
先程は時間無限大の極限でどうなるか調べるために遷移行列を無限回かけたものがどうなるかを見ました。しかし、時間無限大の極限を調べるには固有ベクトルによる展開の方が便利です。
5行5列の行列には、5つの固有ベクトル
これを固有ベクトルによる展開と呼びます。固有ベクトルに行列をかけると固有値が出てくるので、
さて、いま
となります。ここで、
この式の意味するところは、初期状態を固有ベクトルで展開した時、その展開係数
を行列表示すると、
固有ベクトルを横に並べた行列は
です。なので、
と、初期状態のベクトルに固有ベクトルを並べた行列の逆行列をかければ展開係数がわかることになります。やってみましょう。
例えば初期状態として1のマスにいた場合は
a = (np.array([0, 1, 0, 0, 0]))
print(Pinv@a)
実行結果はこうなります。
[ 0.75 0.25 0.784 -0.511 0.791]
それぞれ順番に
ついでに、2のマス、つまり真ん中のマスから始めた場合も見てみましょう。
a = (np.array([0, 0, 1, 0, 0]))
print(Pinv@a)
[5.000e-01 5.000e-01 1.108e+00 7.221e-01 1.065e-17]
勝ち負けの確率が1/2になってますね。
まとめ
行列や固有値が出てくる例として、確率過程を取り上げてみました。すごろくゲームでの勝率計算という、一見すると行列と関係なさそうな題材に、固有ベクトルや対角化といった線形代数で学んだことがわらわら出てきて、確率の時間発展が行列でかけること、その行列を無限回かけると収束先がわかること、初期状態を固有ベクトルで展開してやると最終的にどこに行くかの確率が行列を何度もかけなくてもわかることを見ました。
線形結合が現れるところには必ずといってよいほど行列が顔を出し、その系の性質は固有値や固有ベクトルが決めます。繰り返しになりますが、行列とはなにか抽象化されたものなので、その意味は文脈によって異なります。したがって、固有値や固有ベクトルが意味するものも場合によって変わってきます。「固有値とはどういうものか」のイメージを掴むには、結局のところ場数を踏んでいくしかないのかなと思います。しかし、こうやってプログラムを書いて、「あ、確かに行列が対角化されてるなぁ」とか確認してみると、紙と鉛筆だけで勉強するよりなんとなく腑に落ちる気がします。ここでは一般のNについての厳密解は求めず、数値解のみを調べましたが、興味のある人は厳密解の導出に挑戦してみると良いでしょう。
参考文献
-
線形代数を学ぶ理由
- 主に数値計算で離散化により現れる線形代数について
-
線形代数というものの見方
- 線形代数の幾何学的な意味、特に回転について
-
a/(b+c)+b/(c+a)+c/(a+b)=4の自然数解(a,b,c)を求める
- スクリプトを書いて数学を遊ぶ例
Discussion