Sphinx で日本語改行が空白になる件について

Sphinx日本ユーザ会の「不自然な空白が表示される」にある通り、japanesesupport.py を使うと解消されるようです。ただしこれをそのまま使うと改行だけでなくリンクの前後の空白なども潰されてしまい、英単語が頻繁に混じる文章だとかなりギチギチした見た目になります。Issues に置いてあるパッチ版を使うといい感じになるようです(なぜか issues が放置されてるので、もしかしたら何か問題があってマージされてないのかもしれませんが…)。

この問題って Sphinx だけでなく markdown とか色々なところで(このブログでも)現れてますが、どこも大抵放置してるんですよね。多分皆ブラウザが対応しないのが悪いと思ってるということなんでしょうけど。

FC 『AD&D プールオブレイディアンス』を少し解析してみた

相当手強いのでとりあえず不完全ですが途中結果だけ置いておきます。

今までで一番読みにくいコードだと感じています。バンク切り替えが8K単位になっている上に、同じバンクが異なるアドレスにロードされたりしてややこしいんですよね…。ルーチンの先頭でいちいちレジスタをスタックに退避していることが多かったり、他のゲームではあまり見られない特徴があるので、もしかしたらアセンブラでなく何らかの高級言語で書かれているのかもしれません。

pyvenvをactivateすると"parameter not set"とか言われる場合の対処

zshからpyvenvをactivateしようとすると

$ source bin/activate
deactivate:2: _OLD_VIRTUAL_PATH: parameter not set

というエラーになることがあります。これはzshno_unsetオプションを設定していると起こります。no_unsetオプションを設定したままこれを回避するには、無名関数を使って

$ (){ setopt local_options unset; source bin/activate }

とすればいいようです。deactivate時も同様に

$ (){ setopt local_options unset; deactivate }

でOK。

OpenAI GymのPong-ram用資料

OpenAI GymのPong-ram環境用のメモ。

とりあえずメモリアドレスを軽く調べました(趣旨的には解析とか一切なしで解くことが求められているのでしょうけど)。なお、Atari2600は $80-$FF の128 バイトがRAMとなっています(参考: Atari 2600 Specifications)。

address type description
$88 U8 フレームカウンタ
$8D U8 COMスコア
$8E U8 PLAYERスコア
$95 U8 COM座標y
$B1 U8 ボール座標x
$B6 U8 ボール座標y
$B8 S8 ボール速度y?
$BA S8 -1 * ボール速度x
$BC U8 PLAYER座標y

ボールのy方向の動きについては完全には理解していません($B8 に疑問符がついてるのはそのため)。もしかしたらサブピクセルまで計算されていたり、場合により $B8 が2回加えられるかもしれません。ボールの動き計算コード自体は $F554 にあります。

自機の動きには多少の慣性がかかりますが、これをメモリ内容だけで把握する方法はよくわかっていません。多分 $84 を目的座標として前回述べたような処理が行われるのだと思いますが、$84 は1Fおきに全く関係ない値に変わるため、その他のアドレスも参照しないと自機の慣性は正しく把握できないと思います。

とりあえず自機座標とボール座標/速度がわかれば最低限足りるとは思いますが、慣性も考慮しないと得点率が悪くなるかもしれません。手でプレイするとわかりますが適切な角度で打ち返さないとCOMは失点してくれないので、結構繊細な操作が要求されるんですよね…。

ただ、Pong-ram環境では1ステップごとに2-4フレーム進むようなので、そこまで繊細なことを考えても仕方ないのかもしれません。この辺はやってみないとわからない感じですね。

環境に関する情報をまとめておきます:

attribute value
env.action_space Discrete(6)
env.observation_space Box(128,)
env.reward_range (-inf,inf)
env.spec.timestep_limit 10000

actionの意味は以下の通りと思われます(2と4, 3と5の移動量は多分同じだと思いますが確証はありません):

意味
0,1 何もしない
2,4 上へ移動
3,5 下へ移動

observationは128バイトのRAMをそのまま格納した配列です。なお、配列の要素は numpy.uint8 で、Pythonの int と振る舞いが異なるので注意が必要です。

1点取ったとき 1.0, 1点取られたとき -1.0 の報酬が得られるようです。

ターン制限(10000)があるので、単に失点しないだけでなく得点率が求められると思われます。1ステップあたり2-4フレーム進むので、最悪の場合20000フレームで打ち切られます。なお、全く操作せずパーフェクト負けした場合は大体1000ステップ程度かかるようです。

メモリを見つつ1ステップずつ動かせるスクリプトを書いたので一応貼っておきます。

Atari2600 Pong 風にぬるっと動いてみるテスト

OpenAI GymのAtariゲーム環境の中にPong-ramというのがあって、ゲーム画面ではなくメモリ内容を直接見られるようなので、Stellaで動かしながらメモリアドレスなどを調べています。

幸いAtari 2600はNESと同じでCPUが6502(正確には機能削減版の6507)なので、少しコードを読んでみました。わりと癖があるというかNESゲームとは違う書き方をしているところが多くて読みにくいですが、自機の移動に関するコードがちょっと面白かったので紹介してみます。

自機の移動は擬似コードで書くとこんな感じになっています:

y = (y + dst_y) / 2

これはつまり現座標と最終的な移動先座標の中間点へ移動するということで、一気に移動するのではなく徐々に移動する感じになります。実際にこのゲームをプレイしてみると自機の移動がぬるっとした感じなのですが、それを実現するのがこのコードだったというわけですね(こういうのをeasingって言うのかな?)。

なんとなくjavascript同じようなものを作ってみました(玉がマウスカーソルを追いかけてくるだけ)。上のスライダーで係数を変えると動きの勢いが変わります。

多層パーセプトロンを書いてみる

前回は単純パーセプトロン(SLP)でAND, ORを学習し、XORは学習に失敗することを確認しました。今回は多層パーセプトロン(Multi Layer Perceptron, MLP)を使ってXORも含めて学習してみます。内容的には高卒でもわかる機械学習第3回以降に相当します。

相変わらず定義がいいかげんなまま記号を使ったりするのでお察しください^^;

今回は入力層、出力層に加えて1つの隠れ層を設けます。バイアス抜きで入力層のユニット数2, 隠れ層のユニット数2, 出力層のユニット数1とします*1。今回も出力側のみ値を {-1,1} の形式にします。

         in  hidden  out
bias      o     o

{0,1} --> o     o

{0,1} --> o     o     o --> {-1,1}

言い換えると、出力層の活性化関数をこのように定義します:

{ \displaystyle
f^{(2)}(u) = \begin{cases}
   1 & (u^{(2)} \ge 0) \\
  -1 & (u^{(2)} < 0)
\end{cases}
}

今回は隠れ層もあるのでここにも活性化関数を定義する必要があります。これを  f^{(1)}(u) と書くことにします。この関数の定義は任意ですが、XORを学習するためには非線形関数でなければなりません。線形関数だと出力層のユニット値は結局入力の線型結合にしかならないため、線形分離不能なXORは学習できないためです。このことは後に実際に試してみます。

今回も訓練データ1件ごとのオンライン学習とし、誤差関数を以下のように定義します(SLPのときと同様。微分ができるならこれで問題ないはず):

{ \displaystyle
E = max(0, -yu^{(2)})
}

u(2) は出力層のユニット値(w(2)*o(1)), y は教師データです。重みの更新は不正解時のみ行うので、実質的には

{ \displaystyle
E = -yu^{(2)}
}

と考えて問題ありません。これが全ての重みについて微分できるかどうか見ていきます。

まず重み行列の形を確認しておきます。入力層から隠れ層への重みを W(1), 隠れ層から出力層への重みを W(2) とすると、以下のようになります:

{ \displaystyle
W^{(1)} = \begin{bmatrix}
  w^{(1)}_{1,0} & w^{(1)}_{1,1} & w^{(1)}_{1,2} \\
  w^{(1)}_{2,0} & w^{(1)}_{2,1} & w^{(1)}_{2,2}
\end{bmatrix} \\
W^{(2)} = \begin{bmatrix}
  w^{(2)}_{1,0} & w^{(2)}_{1,1} & w^{(2)}_{1,2}
\end{bmatrix}
}

出力側の W(2) から考えてみます。簡単のため1要素についてのみ考えます:

{ \displaystyle
\frac{\partial E}{\partial w^{(2)}_{1,0}} =
\frac{\partial E}{\partial u^{(2)}} \frac{\partial u^{(2)}}{\partial w^{(2)}_{1,0}} =
-yo^{(1)}_0
}

よって出力層の重みについては微分可能です。次は隠れ層の W(1) について考えます。やはり1要素についてのみ考えると:

{ \displaystyle
\frac{\partial E}{\partial w^{(1)}_{1,0}} =
\frac{\partial E}{\partial o^{(1)}_1} \frac{\partial o^{(1)}_1}{\partial u^{(1)}_1} \frac{\partial u^{(1)}_1}{\partial w^{(1)}_{1,0}}
}

この3項を個別に求めると:

{ \displaystyle
\frac{\partial E}{\partial o^{(1)}_1} =
\frac{\partial E}{\partial u^{(2)}} \frac{\partial u^{(2)}}{\partial o^{(1)}_1} =
-yw^{(2)}_{1,1} \\
\frac{\partial o^{(1)}_1}{\partial u^{(1)}_1} = f'^{(1)}(u^{(1)}_1) \\
\frac{\partial u^{(1)}_1}{\partial w^{(1)}_{1,0}} = x_0
}

よって、全て代入して

{ \displaystyle
\frac{\partial E}{\partial w^{(1)}_{1,0}} = -yw^{(2)}_{1,1}f'^{(1)}(u^{(1)}_1)x_0
}

となり、隠れ層の重みについても微分可能です。これで勾配がわかるので、後は勾配降下法により学習可能です。

ということでPythonコードを書いてみました。今回は重みを乱数で初期化し、隠れ層の活性化関数と学習率をコマンドラインから与える方式にしています。シグモイド関数を指定し、学習率を 0.1 としたときの出力例を示します:

#------------------------------------------------------------
# Learning "AND" (eta=0.1)
w1 = [[-0.11631637 -0.32964966 -0.76916355]
 [ 0.05914025 -0.94074692  0.49658873]]
w2 = [ 0.4131751  -0.8584504  -0.27154784]

# Test "AND"
[1 0 0] -> -1
[1 0 1] -> -1
[1 1 0] -> -1
[1 1 1] -> 1

#------------------------------------------------------------
# Learning "OR" (eta=0.1)
w1 = [[-0.12387904  0.52193623 -0.35688955]
 [-0.25589149 -1.01165379 -0.52477719]]
w2 = [ 0.15433217  0.06677338 -0.56990398]

# Test "OR"
[1 0 0] -> -1
[1 0 1] -> 1
[1 1 0] -> 1
[1 1 1] -> 1

#------------------------------------------------------------
# Learning "XOR" (eta=0.1)
w1 = [[ 0.26898265  0.64685031 -1.14389473]
 [-2.09612791 -1.51971218  1.74802215]]
w2 = [-0.69779446  0.9496701   1.01464362]

# Test "XOR"
[1 0 0] -> -1
[1 0 1] -> 1
[1 1 0] -> 1
[1 1 1] -> -1

今回はXORも正しく学習できています。なお、隠れ層の活性化関数として恒等関数(identity)も選べますが、これは線形関数なのでXORは正しく学習できません。前述の記事の第3回でXORはOR, NAND, ANDの組み合わせで実現できると述べられていますが、これはORとNANDの出力がステップ関数などの非線形関数を通したものだからということです。

実は今回はSLPのときと違って色々ハマり所があります。まず、全ての重みを0初期化すると隠れ層の活性化関数が何であってもXORは正しく学習できなくなります。これは全ての重みが0だと実質SLPと変わらなくなるためだと思われます。隠れ層の重みが全て0なので隠れ層のユニット値は全て0になり、出力層の重みも全て0なので、隠れ層の2ユニットの重みが毎回同じように更新されてしまい、実質1ユニットと変わらなくなってしまいます。ということで今回は重みを乱数で初期化しています。

これに伴い、学習成功率が100%ではなくなっています。実験した結果シグモイド関数で学習率0.1なら安定して学習できるように見えますが、活性化関数や学習率を変えると結構学習失敗が起こります(AND, ORすら学習できないこともあります)。そもそも収束判定を行っていないのと、重みを初期化する際の乱数の範囲が -1.0 から 1.0 なので場合によっては非常に0に近い値で初期化されることなどが原因なのかなぁと思いますが、正直よくわかってません。ちゃんと理論を勉強しなさいというのが結論かなと思いますが、ひとまずハマり所はある程度実感できたということで。

*1:前述の記事の第3回にある通り、XORはOR, NAND, ANDの組み合わせで実現できるので、隠れ層のユニット数は2で足りるはず。

単純パーセプトロンを書いてみる

今更感漂うネタですが簡単なパーセプトロンのコードを書いてみたいと思います。高卒でもわかる機械学習というシリーズが非常にわかりやすかったのですが、動くコードがなかったのでそこだけ補完してみようという感じです。

今回は単純パーセプトロン(Single Layer Perceptron, SLP)でAND, ORを学習してみます(ついでにXORが学習できないことも確かめます)。内容的には前述の記事の第2回, 第3回に相当します。これらの記事を前提とするので記号などはちゃんとした定義なしにいきなり使ったりします。ご了承ください。

SLPということで層は入力層と出力層の2つだけです。またAND, ORということは2つの論理値から1つの論理値を計算するので、入力層のユニット数は2(バイアス除く), 出力層のユニット数は1となります。

         in  out
          o

{0,1} --> o

{0,1} --> o   o --> {-1,1}

計算の都合上、出力層からの出力は {0,1} ではなく {-1,1} とし、訓練データの形式もこれに合わせます(False が -1 になるだけです)。入力側は普通に {0,1} とします。例えばANDの真理値表は以下のようになります:

a b a AND b
0 0 -1
0 1 -1
1 0 -1
1 1 1

言い換えると、出力層の活性化関数をこのように定義します:

{ \displaystyle
f(u) = \begin{cases}
   1 & (u \ge 0) \\
  -1 & (u < 0)
\end{cases}
}

学習は訓練データ1件ごとのオンライン学習とし、誤差関数を以下のように定義します*1:

{ \displaystyle
E = max(0, -yu)
}

u は出力層のユニット値(w*x), y は教師データです。前述の通り出力側の値は {-1,1} としているので、SLPが正解を出力したら u と y は同符号、不正解なら異符号となります。重み w の更新は不正解時のみ行うので、実質的には

{ \displaystyle
E = -yu
}

と考えて問題ありません。これは明らかに重み w の各要素について微分可能で、例えば

{ \displaystyle
\frac{\partial E}{\partial w_0} = -yx_0
}

となります。これで勾配がわかるので、後は勾配降下法により学習可能です。

ということでPythonコードを書いてみました。AND, OR, XORそれぞれについて教師データを与え、10000セット学習させています*2。一応学習率ηは 0.01 に設定していますが、重みを0初期化しているので学習率は結果に影響しないはずです*3。出力はこんな感じになります:

#------------------------------------------------------------
# [AND]
#------------------------------------------------------------
### Learning ###
w = [-0.03  0.02  0.01]

### Test ###
[1 0 0] -> -1
[1 0 1] -> -1
[1 1 0] -> -1
[1 1 1] -> 1

#------------------------------------------------------------
# [OR]
#------------------------------------------------------------
### Learning ###
w = [-0.01  0.01  0.01]

### Test ###
[1 0 0] -> -1
[1 0 1] -> 1
[1 1 0] -> 1
[1 1 1] -> 1

#------------------------------------------------------------
# [XOR]
#------------------------------------------------------------
### Learning ###
w = [ 0.   -0.01  0.  ]

### Test ###
[1 0 0] -> 1
[1 0 1] -> 1
[1 1 0] -> -1
[1 1 1] -> -1

予想通り、AND, ORは学習できていますがXORは正しく学習できていません(XORは線形分離不能)。次回は隠れ層を増設した多層パーセプトロン(Multi Layer Perceptron, MLP)でXORも含めて学習してみたいと思います。

*1:バッチ学習だとリンク先の式をそのまま使うことはできないと思われるので、リンク先にある通りオンライン学習としました。

*2:実は数回で学習は終わっていますが、収束判定とかはとりあえず考えないことにします。

*3:重み更新式より、初期重みが0なら最終的な重みはηの定数倍となり、η自体は出力結果に影響しない。ちゃんとした本にはこの辺の証明も書いてあるらしいです。