Python Lasagne でニューラルネットするチュートリアル その 2

昨日の記事の続き。

まずは、Lasagne のハマリポイントを紹介しながらコードの説明。
そのあと、ディープラーニング? ディープニューラルネットワーク? どちらの呼び名が正しいのかビミョウに自信ないが、要は畳み込み& max-pooling &ドロップアウトを織り交ぜたモデルを学習させようとするとハマリポイントが増えるので、そのあたりの注意点とサンプルコード。

def digits_dataset(test_N = 400):
    import sklearn.datasets
    data = sklearn.datasets.load_digits()

    numpy.random.seed(0)
    z = numpy.arange(data.data.shape[0])
    numpy.random.shuffle(z)
    X = data.data[z>=test_N, :]
    y = numpy.array(data.target[z>=test_N], dtype=numpy.int32)
    test_X = data.data[z<test_N, :]
    test_y = numpy.array(data.target[z<test_N], dtype=numpy.int32)
    return X, y, test_X, test_y

データセットづくり。
ありきたりなコードだが、ここにすでに一つハマリポイントが隠されている。

特徴量 X の方は特筆することはなく、レコード数×次元のシンプルな ndarray でいいのだが。
Lasagne の出力層の非線形関数に softmax を指定した場合、出力層に与える正解ラベル y の型は暗黙に int32 が要求される。
うっかりここに普通の int の ndarray とか渡したりすると、

TypeError: ('Bad input argument to theano function with name "****.py:**" at index 1(0-based)', 'TensorType(int32, vector) cannot store a value of dtype int64 without risking loss of precision. If you do not mind this loss, you can: 1) explicitly cast your data to int32, or 2) set "allow_input_downcast=True" when calling "function".', array([...

と怒られて、どこのことを指しているのか見当つかず悩むハメになる。
このエラーが出たときは、正解ラベル y を numpy.array(*, dtype=numpy.int32) でくるむといい。

#### model
n_classes = 10
batch_size=100

l_in = lasagne.layers.InputLayer(
    shape=(batch_size, input_dim),
)
l_hidden1 = lasagne.layers.DenseLayer(
    l_in,
    num_units=512,
    nonlinearity=lasagne.nonlinearities.rectify,
)
l_hidden2 = lasagne.layers.DenseLayer(
    l_hidden1,
    num_units=64,
    nonlinearity=lasagne.nonlinearities.rectify,
)
model = lasagne.layers.DenseLayer(
    l_hidden2,
    num_units=n_classes,
    nonlinearity=lasagne.nonlinearities.softmax,
)

モデルの定義は入力層 lasagne.layers.InputLayer から始めて、lasagne.layers.* の第一引数に前の層を指定しながらつないでいき、最後に適当な出力層を宣言すると、それがそのままモデルの参照となる。


入力層は shape で入力層の次元を指定するのは自然だが、一度に入力するデータのレコード数もこのモデルを定義する段階で指定する必要がある。これがハマリポイントその……いくつ目だっけ?
おそらく実装上の都合だろうが、適当な batch_size を決めて、入力データを batch_size 件ずつに分けて学習を回すのが Lasagne 流になる。ここで shape の値に、入力データの全データ件数を指定すると、学習のところでデータを分割して回す必要がなくなってコードの一部がすっきりするのだが、(おそらく)最急降下法相当になり、足がめちゃめちゃ遅くなる。
というわけで適当な batch_size を指定する必要がある。あまり小さいと速度が落ちるし、学習が落ち着かずに loss や accuracy が跳ねまわる。大きいと足が遅くなるし、余りのデータが無駄になる。
十分大きいデータなら 500 前後がほどよい印象( mnist.py は batch_size=600 になっている)。このサンプルコードでは、データが 2000件にも満たないので batch_size=100 にしている。


出力層は、ニューラルネットワークで何が作りたいかによって変わるだろうが、いちばん一般的な他クラス分類器なら、サンプルにある通り lasagne.layers.DenseLayer を使って、 num_units にクラス数を、 nonlinearity に lasagne.nonlinearities.softmax を指定すればいい。
クラス数は y.max()+1 とかしてもよかったけど、わかりきってるのでリテラル書いちゃった。

#### loss function
objective = lasagne.objectives.Objective(model,
    loss_function=lasagne.objectives.categorical_crossentropy)

X_batch = T.matrix('x')
y_batch = T.ivector('y')
loss_train = objective.get_loss(X_batch, target=y_batch)

定義したニューラルネットワークモデルから目的関数を生成してくれるところが Lasagne の真骨頂である。
ああ、あとおそらくこの Objective をインスタンス化するまでのどこかのタイミングに、パラメータを格納する SharedVariable も用意してくれている。


loss_train は Theano の expression になっているので、theano.function に食わせれば実行コードにコンパイル済みの関数が得られる。Theano すげー。
あとは、この loss_train を使って、必要な関数や処理を作っていくことになる。

#### update function
learning_rate = 0.01
momentum = 0.9
all_params = lasagne.layers.get_all_params(model)
updates = lasagne.updates.nesterov_momentum(
    loss_train, all_params, learning_rate, momentum)

#### training
train = theano.function(
    [X_batch, y_batch], loss_train,
    updates=updates
)

Lasagne における学習は、Theano の updates の仕組みを使ってパラメータを更新する。
updates に渡す更新関数も、適切な lasagne.updates.* を呼べば Lasagne が作ってくれる。
Lasagne には単純な SGD も用意されているが、lasagne.updates.nesterov_momentum は Nesterov Moment を使った SGD になる。これは、「パラメータを前回動かした方向にしばらく動かす & 勾配を取る点も前回動かした方向に少しずらす」というもの。DNN 向けに SGD を改良したもので、収束が速くなる……のかな? mnist.py がこれを使っていたので、ここのサンプルもそれに倣った。


lasagne.layers.get_all_params は、前回の最後にもチラッと出ていたが、パラメータを格納した SharedVariable のリストを返すものである。
更新関数はもちろん対象となるそのパラメータたちを知らなければならないので必要性はわかるのだが、それをユーザが書かなければならないところには納得いかない(苦笑)。


学習率や Nesterov Moment の moment パラメータが固定で与えられているのは違和感があるかもしれない。
これを T.dscalar にして、theano.function の引数で与えるようにすれば可変にもできることは確認した。が、生 SGD より制御が難しく、結果をうまく改善できなかったので、mnist.py と同じように固定で与えている。


mnist.py では、この train 関数はデータを SharedVariable に置いて givens で渡す形で書かれている。そして引数はそのデータの中でその呼出で対象となる範囲を指すインデックスのみを指定する。特に GPU を用いる場合はデータの転送コストが一番高いわけで、そうすると mnist.py と同じ方式のほうが確実に効率いいだろう。
このサンプルでは簡素なコードを優先したことと、Lasagne を試している環境が GPGPU を利用できないものだったので(笑)、データを引数で渡すシンプルな形にした。

#### prediction
loss_eval = objective.get_loss(X_batch, target=y_batch,
                               deterministic=True)
pred = T.argmax(
    lasagne.layers.get_output(model, X_batch, deterministic=True),
    axis=1)
accuracy = T.mean(T.eq(pred, y_batch), dtype=theano.config.floatX)
test = theano.function([X_batch, y_batch], [loss_eval, accuracy])

予測のための関数を定義しているところ。
deterministic というパラメータは、おそらく、Dropout などのノイズ層にスルーで渡されて、True のときにはランダムに捨てるのをやめる(学習時のみドロップアウトする)という制御のためだと思われる。
それ以外には特に疑問はないだろう。

#### inference
numpy.random.seed()
nlist = numpy.arange(N)
for i in xrange(100):
    numpy.random.shuffle(nlist)
    for j in xrange(N / batch_size):
        ns = nlist[batch_size*j:batch_size*(j+1)]
        train_loss = train(X[ns], y[ns])
    loss, acc = test(test_X, test_y)
    print("%d: train_loss=%.4f, test_loss=%.4f, test_accuracy=%.4f" % (i+1, train_loss, loss, acc))

ようやく準備が全て終わって推論である。が、batch_size ずつしか訓練に与えることができないので、ここでも自前でコードをちょいと書く必要がある。といっても、読めばわかるレベルなので大丈夫だろう。
せっかく引数でデータを渡すのだから、渡すデータの順序がランダムになるようにした。batch_size に対して余る分はそのイテレーションでは使われないわけだが、ランダムにしておくことで使われないデータの偏りをなくすことも期待している。
テストデータの評価の方は batch_size に分ける必要がないので、一発呼び出しで済んでいる(このときは……)。


以上、これが一番シンプルな Lasagne の使い方。
でも、Lasagne でもっとディープラーニングっぽい画像処理したいという場合にはもう二手間くらい必要になる。
サンプルで使っているデータセット digits は 8x8 の画像なので、これを入力とした畳み込み& max-pooling &ドロップアウトを織り交ぜたモデルのサンプルコードがこちら。

import numpy
import lasagne
import theano
import theano.tensor as T

# dataset
def digits_dataset(input_width, input_height, test_N = 400):
    import sklearn.datasets
    data = sklearn.datasets.load_digits()
    N = data.data.shape[0]
    X = data.data.reshape(N, 1, input_width, input_height)
    y = numpy.array(data.target, dtype=numpy.int32)

    numpy.random.seed(0)
    z = numpy.arange(data.data.shape[0])
    numpy.random.shuffle(z)
    test_X = X[z<test_N]
    test_y = y[z<test_N]
    X = X[z>=test_N]
    y = y[z>=test_N]
    return X, y, test_X, test_y

n_classes = 10
input_width = input_height = 8
X, y, test_X, test_y = digits_dataset(input_width, input_height)
N = X.shape[0]
test_N = test_X.shape[0]
print(X.shape, test_X.shape)


#### model
batch_size=100

l_in = lasagne.layers.InputLayer(
    shape=(batch_size, 1, input_width, input_height),
)
l_conv1 = lasagne.layers.Conv2DLayer(
    l_in,
    num_filters=8,
    filter_size=(3, 3),
    nonlinearity=lasagne.nonlinearities.rectify,
    W=lasagne.init.GlorotUniform(),
    )
l_pool1 = lasagne.layers.MaxPool2DLayer(l_conv1, pool_size=(2, 2))

l_hidden1 = lasagne.layers.DenseLayer(
    l_pool1,
    num_units=256,
    nonlinearity=lasagne.nonlinearities.rectify,
)
l_hidden1_dropout = lasagne.layers.DropoutLayer(l_hidden1, p=0.2)
l_hidden2 = lasagne.layers.DenseLayer(
    l_hidden1_dropout,
    num_units=64,
    nonlinearity=lasagne.nonlinearities.rectify,
)
model = lasagne.layers.DenseLayer(
    l_hidden2,
    num_units=n_classes,
    nonlinearity=lasagne.nonlinearities.softmax,
)

#### loss function
objective = lasagne.objectives.Objective(model,
    loss_function=lasagne.objectives.categorical_crossentropy)

X_batch = T.tensor4('x')
y_batch = T.ivector('y')
loss_train = objective.get_loss(X_batch, target=y_batch)

#### update function
learning_rate = 0.01
momentum = 0.9
all_params = lasagne.layers.get_all_params(model)
updates = lasagne.updates.nesterov_momentum(
    loss_train, all_params, learning_rate, momentum)

#### training
train = theano.function(
    [X_batch, y_batch], loss_train,
    updates=updates
)

#### prediction
loss_eval = objective.get_loss(X_batch, target=y_batch,
                               deterministic=True)
pred = T.argmax(
    lasagne.layers.get_output(model, X_batch, deterministic=True),
    axis=1)
accuracy = T.mean(T.eq(pred, y_batch), dtype=theano.config.floatX)
test = theano.function([X_batch, y_batch], [loss_eval, accuracy])


#### inference
numpy.random.seed()
nlist = numpy.arange(N)
for i in xrange(100):
    numpy.random.shuffle(nlist)
    for j in xrange(N / batch_size):
        ns = nlist[batch_size*j:batch_size*(j+1)]
        train_loss = train(X[ns], y[ns])
    result = []
    for j in xrange(test_N / batch_size):
        j1, j2 = batch_size*j, batch_size*(j+1)
        result.append(test(test_X[j1:j2], test_y[j1:j2]))
    loss, acc = numpy.mean(result, axis=0)
    print("%d: train_loss=%.4f, test_loss=%.4f, test_accuracy=%.4f" % (i+1, train_loss, loss, acc))

元のサンプルコードとよく似ているが、細かいところが結構違うので、そのあたりを中心に説明して Lasagne チュートリアルを終わろう。

  • 2次元データを入力に使うときは、4次元テンソルで渡す必要がある。 digits_dataset() で X を reshape している行を見てもらえば手っ取り早いが、その形も (データ件数, 1, 横次元, 縦次元) と、2次元目がなぜか 1 でないといけない(理由は調べてない。 Theano の制限?)
  • モデルの定義で Conv2DLayer や MaxPool2DLayer や DropoutLayer で畳み込みや max-pooling やドロップアウトを記述できるが、これはコードを見ればわかると思うので説明略。
  • loss function の定義で、入力データを表す変数 X_batch が、1次元のときは T.matrix だったが、2次元では T.tensor4 とする。
  • 1次元のときは updates 関数だけが batch_size のしばりがあったのだが、2次元では loss function そのものにも batch_size のしばりが及ぶようになる。つまり test も一発呼び出しができなくなるので、こちらでも分割してループして結果を平均、といった処理を行う必要がある。テストは訓練と違ってランダムサンプリングするわけにはいかないので、テストデータのサイズは batch_size の整数倍であることが望ましい。

Python Lasagne でニューラルネットするチュートリアル その 1

@nishio さんに教えてもらったのだが、Lasagne というニューラルネットワークPython ライブラリが Kaggle でけっこうよく使われているらしい。
イタリア語読みすると「ラザーニェ」、Lasagna(ラザニア) の複数形なので、まあ日本人が呼ぶ分には「ラザニア」でいい気がする。


2015年6月現在でバージョンが 0.1.dev と、今手を出すのは人柱感満載。
実際、自分の思ったとおりのモデルを動かすのはなかなかに大変だったので、そのメモを残しておく。


インストールは別に難しいところはない。
ただ Theano 前提なので、Python 2.7 でないと動かないし、Windows で動かすのは茨の道だろう。


また、ドキュメントには "Install from PyPI" とあるくせに、pip ではインストールできない(ワナその1)。
ぐぐると、

Lasagne が PyPI からインストールできないんですけど
git clone でインストールできるよ
そりゃそうだろうけど、ドキュメントには "Install from PyPI" って書いてあるよ?
そんなこと言ってもできないもんはできないんだから、ガタガタぬかさず git から入れとけ

みたいなやりとりが引っかかって、ウケる。
というわけで、おとなしく git clone & python setup.py しよう。


インストール後、git clone した場所に examples というディレクトリがあって、かの MNIST を使ったサンプルコードが置いてある。
GPGPU が叩けない環境でも mnist.py と mnist_conv.py というサンプルは問題なく動くので、まずはそれで遊んでみるといい。
標準のサンプルなのにいきなり

The uniform initializer no longer uses Glorot et al.'s approach to determine the bounds, but defaults to the range (-0.01, 0.01) instead. Please use the new GlorotUniform initializer to get the old behavior. GlorotUniform is now the default for all layers.

みたいなワーニングが出るのだが、多分気にしたら負け。


mnist.py は 512 個ずつのユニットを持つ2段の隠れ層からなる古き良きニューラルネットワークで、環境にもよるだろうが2時間半くらい学習して 98.5% くらいの精度が出る。
mnist_conv.py は 5x5 の畳込みと 2x2 の max-pooling を2回重ねたあと、256 ユニットの隠れ層、そしてドロップアウトという今風のディープなニューラルネット。さすがに重く、それでも 27時間ほどで学習を終えて、99.4% の精度を叩き出す。
これが Python のコードをちょちょっと書くだけで動く(ウソ)んだから、楽しそうでしょう?


mnist.py のコードを見るとモデルを定義するのは簡単そうなので、簡単に使えるのかと思って、mnist.py を改造して自前のデータを僕の考えた最強のモデルに食わせようとしたら、図ったように動かない。
まず mnist.py のコードが無駄に複雑で、汎用化しているつもりなんだろうけど、明示していない仕様があれこれあるようで、謎の型エラーがバンバン出る。


よし、改造はあきらめて一からコードを書こう。ドキュメントにはちゃんと TUTORIAL の文字がある(ワナその2)。
開くと、

Understand the MNIST example
TODO:

良かった、紙のマニュアルだったら壁に叩きつけているところだった。電子化バンザイ。


しかたない、MNIST サンプルコードを理解してやろうじゃないか。
と、勢い込んで読み始めるが、学習や予測のためのコードが 100行以上あって、わずか数行で機械学習できる scikit-learn(ぬるま湯) に慣らされた ゆとり には大層ツライ。


ともあれ、そうして一応理解したつもりで、必要最小限にしぼった Lasagne のスモールサンプルコードがこちら。

import numpy
import lasagne
import theano
import theano.tensor as T

#### dataset
def digits_dataset(test_N = 400):
    import sklearn.datasets
    data = sklearn.datasets.load_digits()

    numpy.random.seed(0)
    z = numpy.arange(data.data.shape[0])
    numpy.random.shuffle(z)
    X = data.data[z>=test_N, :]
    y = numpy.array(data.target[z>=test_N], dtype=numpy.int32)
    test_X = data.data[z<test_N, :]
    test_y = numpy.array(data.target[z<test_N], dtype=numpy.int32)
    return X, y, test_X, test_y

X, y, test_X, test_y = digits_dataset()
N, input_dim = X.shape
n_classes = 10
print(X.shape, test_X.shape)


#### model
batch_size=100

l_in = lasagne.layers.InputLayer(
    shape=(batch_size, input_dim),
)
l_hidden1 = lasagne.layers.DenseLayer(
    l_in,
    num_units=512,
    nonlinearity=lasagne.nonlinearities.rectify,
)
l_hidden2 = lasagne.layers.DenseLayer(
    l_hidden1,
    num_units=64,
    nonlinearity=lasagne.nonlinearities.rectify,
)
model = lasagne.layers.DenseLayer(
    l_hidden2,
    num_units=n_classes,
    nonlinearity=lasagne.nonlinearities.softmax,
)

#### loss function
objective = lasagne.objectives.Objective(model,
    loss_function=lasagne.objectives.categorical_crossentropy)

X_batch = T.matrix('x')
y_batch = T.ivector('y')
loss_train = objective.get_loss(X_batch, target=y_batch)

#### update function
learning_rate = 0.01
momentum = 0.9
all_params = lasagne.layers.get_all_params(model)
updates = lasagne.updates.nesterov_momentum(
    loss_train, all_params, learning_rate, momentum)

#### training
train = theano.function(
    [X_batch, y_batch], loss_train,
    updates=updates
)

#### prediction
loss_eval = objective.get_loss(X_batch, target=y_batch,
                               deterministic=True)
pred = T.argmax(
    lasagne.layers.get_output(model, X_batch, deterministic=True),
    axis=1)
accuracy = T.mean(T.eq(pred, y_batch), dtype=theano.config.floatX)
test = theano.function([X_batch, y_batch], [loss_eval, accuracy])


#### inference
numpy.random.seed()
nlist = numpy.arange(N)
for i in xrange(100):
    numpy.random.shuffle(nlist)
    for j in xrange(N / batch_size):
        ns = nlist[batch_size*j:batch_size*(j+1)]
        train_loss = train(X[ns], y[ns])
    loss, acc = test(test_X, test_y)
    print("%d: train_loss=%.4f, test_loss=%.4f, test_accuracy=%.4f" % (i+1, train_loss, loss, acc))


このコードは何をやっているか。

  • データは scikit-learn の datasets に含まれる digits 。0 から 9 までの数字画像(16階調 8x8 ピクセル)が 1797 件。今回 scikit-learn はこのためだけw*1
  • 400 件をテストデータに、残り 1397 件を訓練データに回している。テストデータを切りの良い数字にしているのは次回への振り
  • モデルは隠れ層2層(1層目 512ユニット、2層目 64ユニット)。100周の学習で 97% くらいの精度になる。


細かい解説は次回に回すが、とりあえず Lasagne の守備範囲は、内部 DSL 的に記述されたモデルから、目的関数を生成するところだけということを念頭に置けば、このコードは特に苦もなく読めると思う。
学習におけるパラメータ更新とか、テストデータの評価とかはほぼ Theano 頼みで、現状はそこのつなぎを利用者が書く必要がある(だから書かないといけないコードが多い)。まあ 0.1.dev なんで。


またこのコードでは学習後のモデルを保存していないが(このサンプルデータの規模なら保存する必要もないだろうし)、まじめにやるなら当然その要望は出てくるだろう。
そのときは lasagne.layers.get_all_params(model) がパラメータを格納した Theano の SharedVariable のリストを返すので、こいつらを何らかの方法で永続化するといい。

続き。

*1:なので、わざわざ def してスコープを分けたところで sklearn を import している

「続・わかりやすいパターン認識」の8章「隠れマルコフモデル」の問題点 2つ #ぞくパタ

【追記】
本記事の内容は公式の正誤表ですでに修正済みです。第1版第4刷以降が出ることがあれば、そちらに反映されていることが期待されます。

【/追記】


昨日は ぞくパタ読書会 にのこのこ行ってきた。主催者、発表者、参加者の皆さん、会場を提供してくださったドワンゴさんに感謝。

続・わかりやすいパターン認識―教師なし学習入門―

続・わかりやすいパターン認識―教師なし学習入門―

「続・わかりやすいパターン認識」(以降「ぞくパタ」)の8章「隠れマルコフモデル」を読んだわけだが、この章には理解のさまたげになりうる問題点が大きく2つあると感じた。

  • 自明ではない条件付き独立性を、言及なく使っている
  • ビタービアルゴリズムで求める ψ_t(j) の定義が明記されていない


前者は読書会でも突っ込んだのだが、残念ながらピンときている人はいない様子だった? 式変形を追えばそこで詰まると思うのだが……いや、全員まったく問題なく式変形できた可能性もまだ残っている。

まあそれはともかく。簡単に問題点を整理・フォローしてみる。

自明ではない条件付き独立性を、言及なく使っている

書籍の中で対象となるのは次の番号の式。


見ればわかるが「隠れマルコフで重要な式のほぼ全て」である。
それもそのはず、そもそも隠れマルコフの仮定はまさにその条件付き独立性を得るため導入されており、その条件付き独立性のおかげでこのモデルは計算できるようになっているのだから。


ぞくパタはグラフィカルモデルをやってなく、グラフィカルモデルなしでモデルに含まれる条件付き独立性を導出するのは面倒なので、ネグったのだろう。そこまでは理解できるのだが、条件付き独立性を使っていることを言いもしないのはさすがにまずい。

「この式変形では、隠れマルコフの仮定から導かれる 〜 という条件付き独立性を用いている」と一言あるだけでも、無条件で成立するものではないと知れるし、ちゃんと式の導出を追っかけたいと思った人は条件付き独立性について調べ考える機会が与えられる。
今の状態では、隠れマルコフの仮定がいかにして強力な枠組みを生み出しているのか、その様子を目の当たりにすることもできない。


もったいない。


確かに何もかも理解するのは難しいし、そんな必要なんてないという意見もあるだろう。
が、捨てる順番的には、確率モデルで一番大切な条件付き独立性は最後ちゃうかなあ、と個人的には強く思う。


というわけで、使われている条件付き独立性を紹介し、それを使って実際に式変形する。
式変形は全部ではなくあえて 1つだけにしておくので、興味があれば残りはぜひ自力で。


まずは条件付き独立性。
つか一応「条件付き独立性」って何ってところからやっとくか。
ぞくパタ 7ページに書いてあるのをそのまま引くと、「 S が与えられた下で事象 X, Y は条件付き独立である」とは次のいずれかが成り立つこと。

  • P(X|Y,S)=P(X|S)
  • P(Y|X,S)=P(Y|S)
  • P(X,Y|S)=P(X|S)P(Y|S)


ぞくパタ では「事象」になっているが「確率変数」に対しても同様に定義する。


そして「S が与えられた下で X, Y は条件付き独立である」ことを X\perp Y | S と書くことにする。
本来なら条件付き独立性の ⊥ の縦棒は2本なのだが、はてダの環境では出ないので1本にさせてもらってる。手抜きですまん。


さて、隠れマルコフモデルでは以下のような条件付き独立性が成立する。記号はぞくパタのままなので、notation は省略。

  • (1) x_1,\cdots,x_t\;\perp\;x_{t+1},\cdots,x_n\;|\;s_t
  • (2) x_1,\cdots,x_t \;\perp\;s_{t+1}\;|\;s_t
  • (3) x_1,\cdots,x_{t-1},s_{t-1} \;\perp\; x_t \;|\;s_t


他にもあるのだが、とりあえず。
導出は……省略(苦笑)。上にも書いた通り、隠れマルコフの仮定から導出できなくないのだが、めんどくさすぎる。
ここでの主眼は「条件付き独立性を示す」ことではなく「条件付き独立性を使っていることを言う」なので、いいのだ(開き直り)。


ちなみにグラフィカルモデルは、そのめんどくさすぎる条件付き独立性の導出を「見ただけでわかる」ようにしてくれる強力なツールである。
興味があれば PRML 8章などをどうぞ。


さて条件付き独立性を使って上の式の 1つをやっつけよう。前向きアルゴリズム更新式 (8.10) あたりでいいか。

  • \alpha_t(j)\;=\;\left\{\sum_{i=1}^c\alpha_{t-1}(i)\;a_{ij}\right\}\;b(\omega_j,x_t)   (8.10)

a とかαとかをもとの確率になおす。

  • P(x_1,x_2,\cdots,x_t,s_t=\omega_j)\\ \;=\;\left\{\sum_{i=1}^c P(x_1,x_2,\cdots,x_{t-1},s_{t-1}=\omega_i) \;P(s_t=\omega_j|s_{t-1}=\omega_i) \right\}\;P(x_t|s_t=\omega_j)

長い。


x_1,x_2,\cdots,x_{t-1} はセットでしか動かさないので、それを {\bf x}_1^{t-1} と書くことにする。
また s_t=\omega_js_{t-1}=\omega_i は常に値が指定されているものとして、それぞれ単に s_t, s_{t-1} と記す。
それだけだと右辺の sum のインデックス i の指すものが行方不明になるので、sum は s_{t-1} についてとる形で表す。

  • P({\bf x}_1^{t-1},x_t,s_t)\;=\;\left\{\sum_{s_{t-1}} P({\bf x}_1^{t-1},s_{t-1}) \;P(s_t|s_{t-1}) \right\}\;P(x_t|s_t)

見通しが良くなった。これを示す。
確率の加法定理から

  • P({\bf x}_1^{t-1},x_t,s_t)\;=\;\sum_{s_{t-1}} P({\bf x}_1^{t-1},x_t,s_t,s_{t-1})

である。s_{t-1} を増やして消しただけ。
右辺の sum の中身を、乗法定理を2回使って変形する。

  • P({\bf x}_1^{t-1},x_t,s_t,s_{t-1})
  • =P(x_t|{\bf x}_1^{t-1},s_t,s_{t-1})\;P({\bf x}_1^{t-1},s_t,s_{t-1})
  • =P(x_t|{\bf x}_1^{t-1},s_t,s_{t-1})\;P(s_t|{\bf x}_1^{t-1},s_{t-1})\;P({\bf x}_1^{t-1},s_{t-1})

\alpha_{t-1}(i)=P({\bf x}_1^{t-1},s_{t-1}) なので、分解の3項目は片付いた。
(ここまでは乗法・加法定理しか使っていないので、隠れマルコフでなくても成立する)


1項目は上の条件付き独立性の (3) x_1,\cdots,x_{t-1},s_{t-1}\;\perp\;x_t\;|\;s_t を使うと、

  • P(x_t|{\bf x}_1^{t-1},s_t,s_{t-1})=P(x_t|,s_t)

となり、これは b(\omega_j,x_t) である。

2項目は同じく条件付き独立性 (2) x_1,\cdots,x_t\;\perp\;s_{t+1}\;|\;s_t から

  • P(s_t|{\bf x}_1^{t-1},s_{t-1})=P(s_t|s_{t-1})

となり、a_{ij} である。これで (8.10) が示された。


ここで一番大事なことは、「前向きアルゴリズムやビタービを導出するには隠れマルコフの仮定がちゃんと必要だった」とわかること。
これらのアルゴリズムは隠れマルコフ以外でも出てくるので、そのときになんで使えるかもこの辺りを理解していれば納得しやすい。

ビタービアルゴリズムで求める ψ_t(j) の定義が明記されていない

ビタービは、ψ_t(j) という値を再帰的に求めることで状態の系列を推定するアルゴリズムだが、その ψ_t(j) について、ぞくパタでは次のように説明している。


「ある時点 t で状態 ω_j に到達し、かつ x_t が観測される確率を考える。その確率は、1時点前の状態が ω_1〜ω_c のいずれであるかによって異なり、c 種存在する。その中で最大となる確率を ψ_t(j) で表す」


正直、この説明では ψ_t(j) がなんなのかわからなかった。
ψ_t(j) が何かわからなくても、(8.24) 式では漸化式が与えられるので、それを認めれば計算はできる。
が、その ψ_t(j) で最適な状態の系列が推定できることはなぜわかるのだろう?


ψ_t(j) が定義されていれば、漸化式はそこから導ける(ここでも条件付き独立性を使う)し、それを求められれば最適系列を与えることもわかる。問題解決。
というわけで定義をしよう。*1

  • \psi_t(j)\;:=\;\max_{s_1,\cdots,s_{t-1}}\;P(x_1,\cdots,x_t,s_1,\cdots,s_{t-1},s_t=\omega_j)


ちなみに Ψ_t(j) の方は、この最大値を与えるときの s_{t-1}=\omega_i のインデックス i である。このことをふまえると、Ψ を逆向きにたどると最適系列を得られることも間違いなく理解できる。


条件付き独立性についてはグラフィカルモデルをやってないという同情点があったが、こちらは単なる定義の明記漏れである。
次版があるならぜひ改善してほしい……と思うが、話を聞くとそういったフィードバックは受け付けられにくそうな雰囲気なので、期待はしない。


次回読書会参加は……ツッコミの反応イマイチだったし、11章のディリクレ過程と、あともしかして行くとしたら12章かなあ。
teramonagi さんと sfchaos さんはガチツッコミしてもいいと聞いているのでw

*1:この定義を見ると、本の「説明」が実はまずいということもわかる

プチコン3号 ショートサンプル&ドリル / 十字キー編

プチコン3号の短いサンプルプログラムと、かんたんな演習問題。ドリルを解くと、だんだんプチコンプログラミングを覚える、みたいな。
「新しい命令」は プチコンで 3DS のゲームを作ろう #petitcom - 木曜不足 に出てきてないもの。

■サンプル 1. 十字キーで移動(斜め移動できない)

X=200:Y=120
SPSET 0,600  ' 600 は好きなキャラクタの番号に変えていいよ
WHILE 1
  B=BUTTON()
  IF B==1 THEN Y=Y-1
  IF B==2 THEN Y=Y+1
  IF B==4 THEN X=X-1
  IF B==8 THEN X=X+1
  SPOFS 0,X,Y
  VSYNC
WEND

キャラクタ(スプライト)の番号はスマイルツールか プチコンの標準 BG とスプライトの一覧を作ってみた #petitcom - 木曜不足 を見てね。


▼新しい命令:

BUTTON(ボタン)
どのボタンが押されているかを返す関数。引数の意味は SmileBasic – 『プチコン3号 SmileBASIC』公式サイト 参照。押されているボタンの数値の足し算を返す。A と B の両方が押されていたら(それ以外が押されてなかったら) 16+32=48 が返る。
十字ボタン上 1
十字ボタン下 2
十字ボタン左 4
十字ボタン右 8
A 16
B 32
X 64
Y 128
L 256
R 512
ZR(拡張パッド) 2048
ZL(拡張パッド) 4096


▼問題:

  • [問題 1-1] 作ったけど、移動がものすごく遅い。速さを3倍にするには?
    • (ヒント) IF B==1 THEN Y=Y-1 は「上を押されたら、上に 1 動く」。動くのを3倍にするには? 新しい命令: なし
  • [問題 1-2] ずっと左を押してると画面から出て行ってしまう。画面のはしっこにきたらそれ以上左に行かないようにするには?
    • (ヒント) 今は「左が押されたら X=X-1」だけなので画面の外に行けてしまう。「左が押されていて、まだ左に行っても大丈夫なら X=X-1」に書き換える。「まだ左に行っても大丈夫」を X の式で書くと? 新しい命令: &&
&&(アンド)
IF 文で「〜と〜の両方が成り立ったら」という、2つ(以上)の条件を書きたいときに使う。「 IF A>0 && B>0 THEN 」と書くと「 A>0 かつ B>0 なら〜」という意味になる
    • (さらにヒント) X や Y がどうなったら「画面のはしっこ」なのかわからないってときは、 X と Y の値を画面に表示してみるといい。 VSYNC の前に次の1行を入れてみよう。
  CLS:PRINT X,Y
PRINT
画面に文字や数を表示する
CLS
画面の文字を消す。 ACLS はスプライトなども全部(オール)消すけど、 CLS が消すのは文字だけ。

■サンプル 2. 十字キーで移動(斜め移動できる)


サンプル 1 では、上ボタンと右ボタンを同時に押したら動かない。
斜めに動いて欲しいとか、「右に移動しながら A ボタンで撃ちたい」とか、そういうのは AND を使って、 IF の条件 B==1 を (B AND 1)==1 のように書き直すと思った通りの動きになる。

X=200:Y=120
SPSET 0,600
WHILE 1
  B=BUTTON()
  IF (B AND 1)==1 THEN Y=Y-1
  IF (B AND 2)==2 THEN Y=Y+1
  IF (B AND 4)==4 THEN X=X-1
  IF (B AND 8)==8 THEN X=X+1
  SPOFS 0,X,Y
  VSYNC
WEND


▼新しい命令:

AND(アンド)
くわしく説明するには「二進数」が必要。学研のまんが「算数頭をつくるひみつ」は「二進数」をやさしくおもしろく説明してくれている。ここではかんたんな説明をする。
たとえば「下ボタンだけ」を押したら B=BUTTON() は 2 だけど、左を押しながらだと 2+4=6 、B と同時押しだと 2+32=34 になる。つまり IF B==2 THEN は「ほかのボタンはどれも押さず、下ボタンだけが押されたら〜」ということ。だから斜めに行けないし、「移動しながら弾をうつ」とかもできない。
IF B==6 THEN, IF B==34 THEN って押されるかもしれない全部のパターンを並べる……なんて無理だから、ここはさっきのような足し算の中にどんな数が入っているのかわかる AND を使う(なんでもわかるわけじゃなくて、この場合はわかるってこと。ここが「二進数」のポイント)。2 AND 2 も、 6 AND 2 も、34 AND 2 も全部、下ボタンが押されていれば B AND 2 は 2 になるけど、下ボタンが押されてなかったら、ほかにどんなボタンが押されていても B AND 2 は 0 になる。
だから、IF (B AND 2)==2 THEN って書けば、「ほかに押されたボタンがあってもなくても、下ボタンが押されたら〜」って命令になる。(B AND 2) にカッコが付くのは、AND より == の方が優先順位が高く、カッコを忘れたら B AND (2==2) になってしまうから。
AND も && も「アンド」なのはたまたまじゃあなくて関係があるんだけど、「二進数」について知らないあいだは別のもの( && は複数の条件を並べるとき用、AND は BUTTON 用)と思っておいた方が間違いがない。プチコン3号のよく使う命令で、「二進数」のことを知ってた方がうれしいのは BUTTON くらいしかないから、急がなくても大丈夫。*1


▼問題:

  • [問題 2-1] 移動がやっぱり遅い。速さを3倍にするには?
    • (ヒント) 問題 1-1 といっしょ。
  • [問題 2-2] いつでも速いと操作しにくい。 B ダッシュ、つまり B ボタンを押しているときだけ速さを3倍にするには?
    • (ヒント) 移動の大きさを V=1 のように変数を使って表せば、B ボタンが押されたときに V に別の値を入れるだけでスピードを変えることできる! でも IF B==32 THEN だと「 B ボタンだけが押されたら」になってしまうので……。新しい命令: なし
  • [問題 2-3] どっちに動かしてもキャラクタの向きが変わらないのはつまらない。上に動かしたときは上に、右に動かしたときは右に向かせたい。
    • (ヒント) お姫様のスプライトの番号は、右=596、下=600、左=604、上=608 である。今は SPSET 0,600 ってなっているからずっと下(正面)向きなわけ。移動の大きさを変えたいときは V=1 と新しい変数を作った。向きに合わせてスプライトの番号を変えたいときは……。新しい命令:IF THEN のあとの :(コロン)
IF(イフ) 〜 THEN(ゼン)
THEN の後ろの命令は条件が成立したら実行されるけど、そこに2つ以上の命令を書きたいとき、命令の後に :(コロン) で区切れば、2つ目・3つ目の命令を続けることができる。
  • [問題 2-4](ちょい難) 向きは変わるようになったけど、動きがないからスケートで滑ってるみたい。歩いてるみたいに足を動かしたい。
    • (ヒント) スプライトの番号を 596,597,598,599 の順番に変えて、599 の次は 596 に戻せば右に歩いているように見える。同じように下は 600,601,602,603、左は 604,605,606,607、上は 608,609,610,611。向きごとにプログラムを書いたら大変そうだけど、よく見ると全部、(最初の数), (最初の数)+1, (最初の数)+2, (最初の数)+3 ってなっている。つまり、[2-3] で用意した変数と、ループを回るたびに 0, 1, 2, 3 の順番に変わる ( 3 のあとは 0 に戻る) 変数があれば……。

*1:次の機会は SPCOL のマスクを使いたいときだろう。

プチコン3の標準 BG とスプライトの一覧を作ってみた #petitcom

3DS でゲームが作れる プチコン3号。結構本格的なゲームも作れるくらい性能も自由度も十分ありつつ、ちょっとしたゲームをひょいと作れる手軽さもあって、久しぶりに1画面プログラムなゲームとかちまちま作って楽しんでたりする。
でも、いわゆるベーマガ世代をターゲッティングしすぎていて、プチコンで初めてのプログラミング、みたいなことは厳しめ。確かにあの頃の放り出し感は忠実に再現出来ているが、今のお子様はチュートリアルのあるゲームに慣れているわけで。
せめて入門本があったら良かったのだけど。2月に書籍が出るっぽいが、「公式ガイドブック」という名称には正直あまり期待感がそそられない……。ターゲットのおっちゃん層にはハマるんだろう。


プチコンはスプライトや BG の絵のパーツ、効果音、BGM などを全部自分で作ることもできるが、デフォルトでけっこういっぱい登録されてもいるので、絵心を発揮しなくても手軽にゲームを作ることができる。が、標準のツールは残念ながら使い勝手があまり良くなく、「どのキャラクタにしよっかな〜♪」という楽しいはずの気分がたちまち曇る。
やりたいことは単純に「全部のキャラクタを見て、定義番号が知りたい」だけのことなんだけど、3DS の画面は狭くてデフォルト BG やスプライトを全部表示することができない。まあ、そんだけ十分多くの絵のバリエーションが用意されているという意味では朗報だけどね。


というわけで、デフォルトの BG とスプライト画像の一覧を作ってみた。ただスプライトは 16x16 より大きいものは縮小しており見にくい。このへんはどうすればコンパクトに見やすくできるか考え中。
キャラクタの定義番号は縦横を足した16進数の数字になる。例えばスプライトで方位磁石は &H63(10進数で 99) である。
横は 32個並んでいるので、右半分はさらに &H10(10進数で16) 足す必要がある。例えばスプライトでサイコロの1は &H119 となる。作っちゃったあとで失敗したーと思ったが、これ作りなおすの結構面倒なのでパス。


こいつを PC の大きい画面で見ると、「ああ、あんなゲームができそう……こんなゲームも作ってみたい……」とうっとりしてしまうこと請け合いである。*1
つか、これくらいスマイルブームさんが作って公式で公開してくれればいいのにね!

プチコン標準BG

プチコン標準スプライト

*1:かつて電脳倶楽部というディスク雑誌ではだれでも利用可能なゲーム素材を募集・配布しており、それを使ったゲーム作りの記事を書かせてもらったなあ。「冷蔵庫の中を見て作る料理を決める楽しさ」とかなんとか言って

プチコンで 3DS のゲームを作ろう #petitcom

3DS になんか見たことないゲームあるけど、おっちゃんこれなに?」

「これはゲームやない。プチコン3号や。プログラミングってわかるか?」
「学校で『スクラッチ』ってのちょっとだけやったことあるよ。自分で 3DS のゲームが作れるってこと?」
「 Scratch(スクラッチ) 知っとるんやったら話早いわ。そうや、好きなゲーム作れんで。性能も機能もそこそこちゃんとしてるし、Scratch よりゲームっぽいゲーム作れるわ。ちょこっとめっちゃいっぱいがんばったら」
「めっちゃいっぱい……じゃあなんかゲーム作って、おっちゃん」
「あほう! 『自分で作れる』って自分で言うたやろ。遊びたかったら自分で作るんや!」
「えー。教えてくれるんだったらがんばってみる」
「その意気や。ほれ、やってみ」
プチコン始めるっと。メニューみたいの出てきたけど」

「『Smile BASIC(スマイルベーシック)でプログラムを作る』やな」
「『作品を見る』とか『作品公開とダウンロード』とか、これってもしかして自分で作らなくても……」
「はよ『作る』押せや」
「おっちゃんこわい(涙目)。うわー黒い画面とボタンいっぱい出たーむりー」


「これは古き良き BASIC の起動画面やな。ベーマガ世代なら懐かしいて涙ちょちょぎれるとこやけど、子供らはそんなん知らんのやから、もうちょい今風にしてもええと思うねんけどなあ。言語仕様もなにかと……」
「おっちゃん、誰に向かって話してるの? ここからどうやってゲーム作るの?」
「お、おう。どんなゲームが作りたいんや?」
「うーん。ポケモンとか妖怪ウォッチみたいなやつ?」
「フィールドタイプの RPG か。いきなしハードル高いなあ。ま、ええか。何事も経験や。下画面の真ん中の下にある "SMILE"(スマイル) っての押してみ」
「このニコニコマーク? はい。なにこれ?」


「スマイルツールっつってゲームの音や絵を用意したり作ったりするツールやねんけど、『いちげんさんお断り』感ありありやわなあ。まあグチっててもしゃーないから、今は下の "SPDEF" ってやつ押したって」

「ちっちゃい絵がいっぱい出た。イチゴ? ミカン? サクランボ? あ、もしかしてキャラクタどれにするか選べるの?」
「そうや。これはスプライトや。自分で好きな絵ぇ描くこともできるけど、今回はもうあるやつから主人公を選ぼうか」
「スプライトってスクラッチにもあったよ! ゲームのキャラクタだね。でもサクランボで RPG 作れるかなあ……」
「それもシュールでおもろそうやけど、十字キーで下へ行ったら他にもいろいろあるで」
「探しにくい」
「言うたんな……」
「あ! これ! これにする」

「お姫さんが主人公か。そういう RPG 無いわけやないけど、珍しめやな」
「どうやって選ぶの? OK ボタンとかないけど」
「甘ったれんな。そのキャラクタの上に書いたある数字覚えて、プログラムの中にその数字を書くんや」
「マジ?」
「まじまじ。はい、数字写したったから」

姫
596,597,598,599	右
600,601,602,603 下(正面)
604,605,606,607 左
608,609,610,611	上
612,613 右(伏せ)
614,615 左(伏せ)

「なんかめんどくさい……」
「次はお姫さんを表示するプログラムを書こうか。まず右の "EXIT"(イグジット) か X ボタンを押してツール終了や。またキーボードの画面に戻るから、今度は左下の "EDIT"(エディット)っての押してみ」
「押したけど……なんか変わった?」
「上の画面にプログラムを作るモードに変わったんや。下画面のキーボードを押したら、上画面に文字出てくるから、とりあえず今はこの通りプログラム書いてみ。行の終わりは右下の ENTER(エンター) 押すねんで 」

ACLS
SPSET 0,600
SPOFS 0,200,120

「全然わかんない」
「後で説明したるし。でけたか? そしたら START(スタート) ボタンを押して、初のプチコンプログラム実行や」
「START っと……。あ! 真ん中にちっちゃいお姫様が出た!」

「出たやろ? じゃあプログラムの説明すんで。プログラムっつうのは……」
「ねえねえ、十字でもスライドパッドでもお姫様動かないんだけど。バグ?」
「動かすプログラム書いてないねんから動くかいな。なんで『バグ』とかそんな言葉だけ知ってるねん」
『都会(まち)トム』で言ってたから」
「なんやそれ? ゲーム作る小説? まあええか。説明すんで。プログラムっつうのはコンピュータに渡す命令を並べたもんや。3DS もコンピュータの一種やからな。コンピュータはプログラムの順番通りに命令を実行するんや」
「ふーん。じゃあさっきのプログラムは『お姫様を表示して』っていうプログラムだったんだ。でも『お姫様を表示して』って命令にはなんとなく見えないけど……。3行ってことは命令3つ? お姫様表示するだけにそんなにいるの?」
「お姫さん表示する専用の命令みたいんまであったら、命令の数どんだけいるねん。お約束の多い今どきのプログラムからしたら3行でも短い方やで。表示する前の準備とかもあるんやし」
「プログラムってめんどくさいね……」
「ほんなら1行ずつ見ていこか。1行目の ACLS は画面をきれいにする命令や。見えてる画面はもちろん、見えてへんところにも画面がいっぱいあったりするんや。プログラムを走らしたり終わらしたりしても、全部の画面を勝手にはきれいにしてくれへん。誰か別のプログラムがお姫さんの絵を書き換えとったら、全然違う絵になってバグなんか何なんかわからんハメになったりする。ACLS はそれを全部きれいさっぱり一番最初の状態に戻してくれるんや。とりあえずプログラムの最初には ACLS って書いとくもんやと思っといたらええ」
「エーシーエルエス……そんな読めない命令覚えられない……」
「そこらへんはしゃーないなあ。一応、オール・クリヤー・スクリーン(All CLear Screen) で ACLS やねんけど……そんなん言われても、みたいな顔せんでええから。要るもんはがんばって覚えるしかないけど、せいぜい数十いうところやからなんとかなる。そん中でもよう使う命令はさらに限られとるし。ほんまもんの英語やと何万語も覚えんならんこと考えたら余裕やで」
「……はーい」
プチコン3号のエディタは途中まで入力したらキーボードの上に命令の候補が出るんで、うろ覚えでもまあまあなんとかなるし。まあ実際にプログラミングしてみたら案外大丈夫なもんやで」

「プログラムの説明に戻んで。2行目と3行目は『スプライト』の命令や。SP で始まる命令はほぼ全部スプライトをどうこうするためのもんと思っといてええ」
「『スプライト』って、スクラッチでもあったよ。ゲームのキャラクタでしょ」
「そや。大ざっぱに言うたら、『スプライト』は絵があるキャラクタをうまいこと動かす仕組みや。敵とか味方とか弾とか魔法とか爆発とか、ゲームの中で動くものはだいたいスプライトとして作るんは Scratch(スクラッチ) でもプチコンでも全く一緒やな」
「じゃあさっきのプログラムはお姫様スプライトに書いてあったんだね」
「ん? あーちゃうちゃう。そこは Scratch とプチコンの決定的にちゃうとこやな。Scratch は基本的にスプライトにスクリプトを書く、つまりゲームのキャラクタそれぞれがプログラム持っとって、それぞれ勝手に動くっちゅう今どきのスタイルやねん。けど、プチコンは1個のプログラムが全部のスプライトを動かすっていう古いスタイルやねん」
「そう言われれば、スクラッチは猫のスプライトに『10歩動かす』って書いたら猫が動いて、犬に『90度回す』って書いたら犬が回るんだった。んー、でもプチコンはプログラム1個しかないんだったら、お姫様に『動け!』って言ったのに王様が動いちゃったりしない?」
「そこはスプライトに番号つけて区別するんや。番号は 0 から 511 までで好きに決めてええねんけど、まあ小さい方から使うといたらええ。今のプログラムではお姫さんは 0 番になってる。2行目の SPSET 0,600 は、0 番のスプライトに 600番の絵を SET(セット)するって命令や。さっきメモしたお姫さんの正面向いてる絵が 600 やったやろ?」
「じゃあ 600 を他の数字にしたら絵が変わるってことだね。じゃあ……プログラム書くには EDIT(エディット) だったっけ……で、600 を 123 に変えてみて、START(スタート) を押したら……わぁ、ピコピコハンマーになったー」
「3行目の SPOFS 0,200,120 は、0 番のスプライトを画面の左上から数えて右に 200、下に 120 行ったところに動かせって命令や。画面は横 400、縦 240 の大きさやから、これで画面のちょうど真ん中あたりにお姫さんが表示されるって寸法や」
「じゃあ 0番はお姫様で、1番は王様で、みたいに最初に決めとかないといけないんだ。忘れそう……」
「スプライトの番号割り振ったら、さっきお姫さんのキャラクタの数字メモったみたいに紙に控えとくんや。人間すぐ忘れるからな」
「プログラムって大変だね……。いちいちお姫様を 0 番とかしないで、600 番のお姫様を真ん中に表示、ってできないの? それなら命令1個でお姫様表示できそうだけど」
「お姫さんが1人しかおらんかったらそれでもええかもしれんけど、同じお姫さんが2人3人いたら困るやん。全部 600 番なって区別つかへん」
「お姫様が2人ってお姉ちゃんと妹とか、ほかの国のお姫様とか? そういうときは色とか変わるでしょ。ピーチとデイジーみたいに*1
「えーとじゃあ村人A、村人B、村人Cとか?」
「あー、おんなじ顔でおんなじ服で。それならいそう。シューティングゲームでおんなじ敵がいっぱい出てくるのとかもだね」
「あとお姫さんもいつも 600番の絵やのうて、右向いたら別の絵に変わるわけや。キャラクタとしてはおんなじお姫さんでもな。そこらへんも考えたら、キャラクタごとに『スプライトの番号』があって、SPSET でその番号がどんな絵になるかを決めるんが賢いんや」
「そして SPOFS は 0番のスプライトを真ん中に表示するんだね」
「ちょい違う。『左上から数えて右 200、下 120 に動かす』や。試しにこの3行目だけ削ってみ」
「あ。お姫様が一番左上に表示された。そっか、SPOFS がキャラクタを表示するんじゃなくて、SPSET のときにもう表示されてて、 SPOFS は移動なんだ。それなら名前を SPMOVE にしてくれればいいのに」
「OFS は OFFSET(オフセット) の略で、基準の位置からどれくらいずれてるかって意味やねん。MOVE(ムーブ)、つまり移動だと、今いる場所からの移動みたいに解釈できてまうんがイヤやったんちゃうか」
「SPOFS はスクラッチの『10歩動かす』じゃなくて『 x座標を 200にする』の方ってことかあ」
「おう、そうやそうや。プチコンでも画面上の左右の位置は x 座標、上下の位置は y 座標っていうんで、Scratch といっしょやな」
「右 200、下 120……つまり x 座標が 200 で y 座標が 120 は真ん中あたりってのはどうやってわかるの?」
3DS の上画面は横 400、縦 240 の大きさやねん」
「真ん中ならその半分の 200, 120 にすればいいんだね。じゃあ SPOFS 0,400,240 ってすれば右下に……あれー何にも表示されない」
「スプライトの左上を 400,240 に持っていってるから画面の外に行ってしまうんや」
「えーとどういうこと? ……ああ、ちょうどスプライトの大きさの分戻してあげればいいのか。ってスプライトって大きさどれくらい?」
プチコン3号のスプライトは大きさ変えれるから、ものによってちゃうけど、だいたいは縦横 16 や。お姫さんもな」
「それじゃあ 400 と 240 から 16 引いて…… SPOFS 0,384,224 ってすれば……右下に出た! なんとなく好きな場所に表示できそうにはなったけど、全然ゲームじゃないよ? スライドパッドでお姫様動かしたいんだけど」
「スライドパッドは STICK(スティック) って命令を使うんや」
「また新しい命令かー。さっきの SPSET とか SPOFS とかといっしょで、1番目の数字がナニナニで、2番目がコレコレみたいなことがまたあるんでしょ。そんなのいちいち覚えてられるかなあ」
「大丈夫。STICK って打って、キーボードの右上の『?』押してみ」

「なんか説明出てきた。ふーん、こうやって命令の説明見れるんだ。へー」
「スライドパッドで下にずらしてったら、使い方の例も出てるし、まあ完璧とは言わんけど、そこそこなんとかなるで」
「その説明がよくわからないんだけど……。"STICK [端末ID] OUT X,Y" ってどういう事?」
「『引数』のとこに説明あるやつは、好きな値とか変数とか式とか置き換えれるやつや。これやと『端末ID』と『X』と『Y』やな。[ と ] で囲まれとるんは省略できるいう印や」
「省略できるって言われても、省略した時としなかった時でどう違うのかわかんないよ」
「親切やと省略したらどうなるんか書いたあるんやけどな……。『ワイヤレス通信で他の端末の情報を取得する際に指定』か。つまり、指定せんで省略したら自分の本体のスライドパッドのんが取れるっちゅうこっちゃ。わかるやろ?」
「わ・か・り・ま・せ・ん」
「そやろうなあ。わかるやつしかわからんのんは説明としてはビミョウわなあ」
「わかんないけど、端末IDってのは省略しちゃっていいってこと?」
「まあ今はそうや」
「OUTは引数のところに無いんだけど」
「そういうんは命令の一部やからそのまま OUT(アウト) って書くんや。最初っから STICK OUT って命令やと思うといたらええわ」
「 X, Y は『変化量を受け取る変数』……変数ってスクラッチにもあった気がする。数字を入れて持っておける箱だよね。スクラッチだと変数のパーツを置いたら使えたけど、プチコンは?」
「初めて変数使うたときに勝手に用意されるんやが、それやと逆にわかりづらいやろうから、VAR(バー) って命令で箱を用意するもんやって覚えとく方がええかもな。X と Y って変数使いたいなら VAR X, Y ってどっか最初の方に書いといたらええ」
「じゃあ、お姫様の表示位置が 200 と 120 になっていたのを変数 X と Y にして、STICK OUT の受け取る変数にそれを使ったらいいってことかな」
「悪うない。やってみ」

VAR X,Y         ' 変数 X と Y を用意(無くても動く)
ACLS
SPSET 0,600
STICK OUT X,Y   ' スライドパッドの値を X と Y に入れて
SPOFS 0,X,Y     ' お姫様を X, Y の場所に表示

( ' から後ろはコメントだから入れなくていいよ)


「できた! START! ……できてない……どうして……?」
「いろいろ惜しいなあ。どんなふうになった?」
「えー。動かない」
「プログラミングで一番大事なんは、そこでただ『動かない』って言うんやなくて、『どんなふうに動かないか』をちゃんと見ることなんや」
「うーん。でも本当にただ動かないだけだよ」
「何が?」
「お姫様が」
「どこから?」
「左上?」
「どこへ?」
「え? えーと、どこへって言うか、スライドパッドで動かした通りに動かない」
「画面を見て他に何か気づかへん?」
「うーん……お姫様のところになんかちかちかしてて、文字も重なって出てるような…… OK かな、これ。ってあれ? OK って、プログラム動いてない時に出てなかったっけ?」
「つまり、お姫さんがスライドパッドを右にずらしたら右、下にずらしたら下に動いてほしいのに、左上から動かへんし、よう見たらそもそもプログラム終わっとる」
「そうそう。って、そらうごかへんわー」
「おう、ノリ良うなってきたやないか。そしたらまず『プログラム終わっとる』をどうにかせなな」
「スクラッチだったらプログラム勝手に終わったりしないのに……」
「そこは Scratch とプチコンの大きい違いやなあ。Scratch は『すべてを止める』パーツを置くか、自分で停止をクリックせん限り動き続けるんや*2。なんでか言うと Scratch はそれぞれのスプライトにプログラムが付いてて、メインのプログラムっちゅうもんがないねん*3。けど、プチコンはメインのプログラムが最後まで走ったら終わってまうんや」
「でもプログラムって上から順番に実行するんだから、いつかは最後まで行っちゃうよね?」
「ループしたら最後まで行かへんよ」
「ループ? 同じ命令を繰り返すやつがスクラッチにあったけど、それ?」
「そうや。今やりたいんは『スライドパッドの通りにお姫様を動かす』をずっと続けることやから、それをループしたらええ。そしたらプログラムも終わらへんでずっと動いてくれるっちゅうわけや」*4
「スクラッチだと『ずっと繰り返す』みたいなコの字のパーツでループにしたいところを挟むんだったけど、プチコンだとどうするの? コの字とか書けないのに」
「方法はいくつかある。BASIC らしいループ言うたらやっぱし GOTO(ゴウトゥ) 使うたやつやねんけど、Scratch のループ知ってるんやったら WHILE(ホワイル) と WEND で挟む方がええやろうな。WHILE の後にはどこまで繰り返すかの条件を書くんやけど、1 書いたら『ずっと繰り返す』んや」
「そうしたら、ええと、こう?」

WHILE 1      ' ループ開始
VAR X,Y
ACLS
SPSET 0,600
STICK OUT X,Y
SPOFS 0,X,Y
WEND         ' ループ終了

「うわお。やりすぎや。変数用意するのとか、画面消すのとかは繰り返さんでええから」
「そっか繰り返したいところだけ WHILE と WEND で囲えばいいんだね」
「あとは、そやな、ループが一目でわかるよう、先頭に空白を入れて少し下げると見やすなる。Scratch でコの字になっとるんもそのイメージやな」

VAR X,Y
ACLS
SPSET 0,600
WHILE 1      ' ループ開始
  STICK OUT X,Y
  SPOFS 0,X,Y
WEND         ' ループ終了

「こう? うーんでもやっぱり動かない……」
「全然変わらへんか?」
「うん、一緒……いや、OK って出ないから、プログラム終わらなくなった」
「一歩前進やな」
「でも結局動いてないし」
「実は全然動いてないんやなくて、見てわかるほど動いてないだけなんやけどな……」
「えーなにそれ」
「どないするんがええかな。試しにSPOFS に渡す X と Y を 100倍とかしてみるか。プログラミングで掛け算は × やのうて * や」
「プログラム書き替えたくても、 EDIT を押しても何にもならないんだけど……」
「え? ああ、そうか。まだプログラム実行中やから、START 押していっぺん止めて EDIT や」

  SPOFS 0,X*100,Y*100

「こう? じゃあまた START で実行っと。……動いた! お姫様が動いた!」
「完成か?」
「違う! なんか変! これじゃない! でも動いた!」
「そやなあ、たとえ期待通りでなくても動くと嬉しいねん。おっちゃんにもそんな時代が……」
「おっちゃんはどうでもいいから、これどうしたらちゃんと動くの?」
「プログラミングで大事なんはなんやった?」
「『どんなふうに動かないか』。えーと動いてはいるんだけど……スライドパッドを動かしたら、そっちの方にすーっと動いて欲しかったのに、今はスライドパッドを動かした分だけ左上から離れて、スライドパッドを離したらまたもとの左上に戻っちゃう」
「他には?」
「なんか動きが変。思った方向に行かないことがあるっていうか……わかった! 横はいいんだけど、縦が逆だ。スライドパッドを上に入れたらお姫様が下に動く。左右は普通なのに……」
「動きの方はスライドパッドの仕様やろうな。3D のゲームで考えたら、上は『上に進む』やなくて『前に進む』なんやろ」
「そっか。じゃあ 2D のゲームの時は、受け取った Y のプラスマイナスが逆だと思えばいいんだね。離すと元に戻っちゃうのは?」
「STICK OUT でもらえるんは『変化量』やで。そん時のスライドパッドが真ん中からどれくらいずらされてるんかが X と Y に入ってて、その場所にお姫さんを表示してるんやから、そら離したら元に戻るわいな」
「うーん…… X と Y はお姫様の今いる場所のつもりなんだけど……」
「そしたら別の変数を使わなな。STICK OUT で受ける方は DX と DY ってしてみたらどうや」

  STICK OUT DX,DY
  SPOFS 0,X,Y

「あー。そういうこと。いやでもこれ START しなくても動かないってわかるよ!?」
「スライドパッドを右入れたらどうなって欲しい?」
「右に動いてほしい」
「変数で言うたら?」
「変数……横は X か。DX が大きくなったら X が増えてほしい。そっか X に DX を足せばいいんだ」

  X+DX

「惜しい! それやと X と DX を足し算しただけで、足した数がどっかいってまうんや*5。もっぺん X に『代入』せな」
「だいにゅう?」
「とりあえず先に正解を見とくか。X に DX を足すってプログラムはこう書くねん」

  X=X+DX

「XイコールX足すDX? どういうこと? DXがゼロじゃないとダメだよね、これだと」
「このイコールは等しいって意味やなくて、さっきも言った『代入』やねん。 X+DX の結果を変数 X に入れるっちゅうことや。イコールやなくて矢印やったら直観的にわかりやすかってんけどな*6。こんな感じに」

  X <- X+DX    ' ※注:プチコンでこう書いたらエラーになります

「変数イコールがあったら右の計算を左の変数に入れるってことなんだ。あ! スクラッチで『変数を○にする』ってのがあったけど、あれといっしょ?」
「そや。言葉やと勘違いしようがなくてええな。代入って言葉は中学になったら数学で習うんやけど、それはプログラミングの代入とは意味違うねん。んで、かなわんことに高校ではプログラミングは数学なんや。センター試験でプログラミングが狙い目やとかどっかで吹き込まれた生徒がのこのこ来たりするんやけど、そうゆうんはたいてい数学の代入とプログラミングの代入の区別が付かんでえろう難儀したわ……*7
「おっちゃん。ねえ、おっちゃん! これでいいの?」
「ん? お、おう。んーいや、STICK OUT の縦はプラスマイナスが逆やったこと忘れてへん?」
「あ、そうか。じゃあ Y=Y+DY は Y=Y-DY に変えて……」

VAR X,Y,DX,DY
ACLS
SPSET 0,600
WHILE 1
  STICK OUT DX,DY
  X=X+DX
  Y=Y-DY
  SPOFS 0,X,Y
WEND

「スタート! ……消えた? パッドを入れたらお姫様が……」
「あー……。ええとな、プログラムはほぼ合うてんねんけど、速すぎて見えてへんねん」*8
「ああ、知ってる。動体視力ってやつだよね?」
「なんぼ目ぇ良うても見えへんわ。そのくらい速すぎるから、ちょい遅うする命令があるねん。プチコンではループに VSYNC(ブイシンク) って命令を入れとくと、60 分の 1 秒に 1 回ループが回るよう調整してくれる。60 分の 1 秒いうんはハードウェアの都合っちゅうのもあるんやけど、ゲームにちょうどいいスピードなんで、基本いっつも VSYNC って入れるもんやと覚えといたらええ」
「VSYNC ってどこに入れたらいいの?」
「描画のタイミングとか『処理落ち』*9とか、細かいこと言い出したらいろいろあんねんけど、とりあえずループの最後に入れとき。難しいことは難しいことができるようなってから覚えたらええから」

VAR X,Y,DX,DY
ACLS
SPSET 0,600
WHILE 1
  STICK OUT DX,DY
  X=X+DX
  Y=Y-DY
  SPOFS 0,X,Y
  VSYNC     ' 1/60秒に1回ループ
WEND

「動いた! 動くよ! パッドでお姫様が思ったとおりに動く!」
「いつもゲームしとるだけの 3DS で、短いとは言え自分で作ったプログラムでキャラクタ動かせるんは、楽しなるよなあ」
「……でも全然ゲームじゃない」
「そらしゃーない。ちゃんとゲームにしよう思ったらこっからまだまだまだまだかかるわ」
「わかるけど……」
「よっしゃ、そしたらおっちゃんがこれをゲームっぽくしたろ。まだ知らん命令はできるだけ使わんで……。……。でけた」
「はや!」

VAR X,Y,DX,DY
ACLS
SPSET 0,600:SPCOL 0
SPSET 1,269:SPCOL 1:SPOFS 1,100,160 ' 宝箱 その1
SPSET 2,269:SPCOL 2:SPOFS 2,200,160 ' 宝箱 その2
SPSET 3,269:SPCOL 3:SPOFS 3,300,160 ' 宝箱 その3
WHILE 1
  STICK OUT DX,DY
  X=X+DX
  Y=Y-DY
  SPOFS 0,X,Y
  IF SPHITSP(0)>=0 THEN BREAK  ' 宝箱に当たったらループを抜ける
  VSYNC
WEND
SPSET 1,268 ' ふたが開いた宝箱に変える
SPSET 2,268
SPSET 3,268
SPSET 4,24  ' ダイヤモンドを表示
SPOFS 4,100,140

「中身の説明は後でしたるし、ちょい遊んでみ」

「宝箱?」
「そのうちの1つに近づくと……」
「開いた! ダイヤ出た! わーい」

「プログラムのどこが変わっとるかわかる?」
「んー、最初と最後に SPSET とかが増えてる?」
「スプライトの1番から3番が宝箱で、4番が宝石に使うてる。最初の SPSET のかたまりで宝箱(269)を置いて、最後の SPSET らへんで開いてる宝箱の絵(268)に変えたり、宝石(24)を置いたりしとるわけや。まだ知らん命令はできるだけ使わんようにはしたけど……」
「最初の SPSET のとこ、一行が結構長いね。んーこれってもしかして命令つなげてる?」

SPSET 0,600:SPCOL 0
SPSET 1,269:SPCOL 1:SPOFS 1,100,160 ' 宝箱 その1
SPSET 2,269:SPCOL 2:SPOFS 2,200,160 ' 宝箱 その2
SPSET 3,269:SPCOL 3:SPOFS 3,300,160 ' 宝箱 その3

「お、ようわかったな。プチコンは複数の命令を : (コロン) で区切って1行に書けるんや。コロンで分けたら、例えば宝箱1つ分の表示は 3つの命令でできてる」

SPSET 1,269
SPCOL 1
SPOFS 1,100,160

「これが3つ分だとだらーっと長くなりそう。だからつなげたんだね」
「まあそういう同じことの繰り返しを書く命令もちゃんとあるんやけど、今回は知らん命令を使わんようにしたかったんで、やめといてん」
「あれ? SPSET と SPOFS はお姫様を動かす時にもう使ったけど、SPCOL って初めて見る気がする」
「そやねん、こいつはまだ説明しとらん命令やねんけど、スプライトの衝突(しょうとつ)検知のために使わんわけにいかんかってん」
「スプライトのしょうとつ?」
「ループの中で VSYNC の前に入れた 1 行がそれや」

  IF SPHITSP(0)>=0 THEN BREAK

「なんかまだ見たことない命令ばっかり……」
「この一行は『お姫さんが宝箱にぶつかったらループを抜ける』って命令や」
「えーと、お姫様スプライトが宝箱スプライトに触ったらループを抜けて……ああ、最後の宝箱を開ける命令のところに行くんだ。なるほどー」
「 IF(イフ) と THEN(ゼン) は2つセットの命令で、IF の後ろの条件が成立したときだけ、THEN の後ろの命令を実行してくれるんや」
「スクラッチで『もし〜なら〜』ってパーツあったけど、あれと一緒だね。じゃあ IF の後ろの SPHITSP(0)>=0 ってのが、お姫様が宝箱に触ったっていう条件になるんだ」
「 SPHITSP はプチコンのスプライトの命令で、書き方で使い方が変わってちょい複雑やねんけど、 SPHITSP(0) って書いた場合は、0 番のスプライト、つまりお姫さんがぶつかったスプライトの番号が返ってくるねん。なんもぶつかってへんかったら -1 になるから、IF の条件のとこを SPHITSP(0)>=0 ってしといたら、『もしお姫さんが何かにぶつかったら〜』ちゅう命令になるわけや」
「 >= は ≧(以上)って意味?」
「ああ、そうそう。そうや。それも説明せなあかんな。プログラムでは等号・不等号や大小の条件はコンピュータで使いやすい記号になっとって、人間の記号とはちょっとちゃうねん。特に等号は == とイコール2個になるから注意や」

条件 プログラム
= ==
!=
> >
<=
>=

「えーなんでイコール2個……わかった! 代入とごっちゃになるから!?」
「おー賢いなあ。その通り。昔の BASIC(ベーシック) は条件の等号もイコール1個で書いとったもんやけど、それがようバグの原因になってなあ。ノスタルジーあふれるプチコンも、さすがにそこは真似せんかったちゅうわけや」*10
「スプライトがぶつかったらわかるってスクラッチにもあったかも……。でもあっちは『○○に触れた』の○○のところに、ぶつかったか調べる相手を選ばないといけなかったよ。プチコンは何とぶつかったか勝手に調べてくれるんだね」
プチコンも調べる相手を指定できるで。どっちもできる。便利やろ?」
「スクラッチシューティングゲームを作ると、敵いっぱい、弾いっぱいでスプライトがぶつかったかチェックできなくなって、弾の色でがんばってやったりしてたもん。プチコンのほうがたしかに便利かなあ」
「Scratch でシューティング作ったんか。たいしたもんやなあ。それやったらわかるやろうけど、ぶつかったか調べたい相手って、いつも全部のスプライトってわけやないねん」
「自分の弾と自分ってぶつからないよね」
「スプライトごとに衝突チェックするグループをつけたりできるんが、実は SPCOL って命令やねん」
「あー後で説明するって言ってたやつ」
「細かい使い方はややこいからやらんけど、とりあえず SPCOL 0 ってしたら、0番のスプライトは衝突チェックするようになるねん」
「だからお姫様と宝箱の番号で全部 SPCOL ってしてあるんだ」
「これで全部説明したんかな」
「THEN の後ろのBR……ってのがループを抜ける命令?」
「おっ、忘れるとこやった。そや。BREAK(ブレイク) って読むんや。ループを作る命令は今使うてる WHILE 以外にもあるんやけど、そのどれでも BREAK で抜けられるで。あ、GOTO で書いたループはあかんけどな。あと、ループの中にループがある入れ子の場合は一番内側のループから抜けるとかあるけど、まあそこらへんはぼちぼち」
「ねーねー、さっきから遊んでるんだけど、これ、ダイヤが入ってる箱決まってない? いつも左端なんだけど」
「おかしい思うたらプログラムを見てみーや」
「むー」

SPSET 4,24  ' ダイヤモンドを表示
SPOFS 4,100,140

「ダイヤを x 座標 100, y 座標 140 に表示……って、そりゃあ毎回おんなじに決まってるやん!」
「ときどきツッコミ良うなるなあ」
「おっちゃん、これじゃあさすがにつまんない〜」
「じゃあ今日は最後にそこだけなんとかしよか。プログラムで毎回違うことをさせたかったら『乱数』を使うんや。実際にプログラムを書くと……」

SPSET 4,24  ' ダイヤモンドを表示
R=RND(3)+1
SPOFS 4,R*100,140

「アールエヌディーってのが『らんすう』?」
「英語の RANDOMIZE(ランダマイズ) の略だから RND(ランド) って読むかな。こいつは……コンピュータ用のサイコロ? 呼ばれると中でサイコロ振って出た数字が返ってくるみたいなイメージや。普通のサイコロは 6 面やけど、RND は好きな面数選べんで。RND(3) のときは 0 か 1 か 2 が出る『3面サイコロ』や」
「 RND(3) なのに 3 は無いの?」
「 0 か 1 か 2 の『3通り』いうことやな。コンピュータは 0 から始まるほうが都合ええから。1 から 3 の乱数が欲しかったら RND(3)+1 ってするんや。『1から1000までのなにか適当な数』とかほんまのサイコロやと難しいけど、乱数やったら RND(1000)+1 でしまいや」
「 R=RND(3)+1 で、R に 1 か 2 か 3 の乱数が入って、x座標が R かける100, y座標 140 のところにダイヤが表示されるから、毎回違う宝箱のところにダイヤが出てくるようになるんだね」
「どうや、初めてのプチコンプログラミングは?」
「…………10回連続でダイヤ外した……」
「いやいや、ゲームやのうてプログラミングや」
「おもしろかったよ。全然違うのにスクラッチと結構いっしょで、今のところそんなに難しくないし……って、なんか終わったみたいに言ってるけど! 妖怪ウォッチみたいなの作りたいって言ったのに全然できてない!」
「いやいや、ゲーム作るの大変やってことはようわかったやろ。そんなんほいほいゲームできるんやったら、おっちゃん今頃大金持ちや」
「そうかー。おっちゃん、ゲーム買うお金なくて自分で作ってるくらいだもんね」
「そうそう。自分で作ったら金かからへんからなんぼでも遊べんで、ってなんでやねん! 今は大人やから欲しいゲームくらい買えるわ!」
「へー、おばちゃんに許してもらわなくても買えるようになったんだー。よかったねー」
「……お姫さんの右向きの番号とかメモったのに使ってなかったっけ。そこらへんはまた今度な」
「ほーい」



今年のお年玉駄文もちょっと遅れましたが、うちの子供たちのために書いたプチコン3号・チュートリアル。Scratch を知ってる設定は、実際うちの子たちがそうだから。
やっぱり間口の広さは Scratch が圧倒的。Scratch にある程度慣れて本格的なゲームを作るのは難しいとわかったあたりで、プチコンに移ってくる流れができたりすると楽しそうかな。

*1:マリオシリーズの姫。ピーチはピンク、デイジーは黄色のドレスをだいたい着てる

*2:さらに Scratch はキー入力や一部のイベントには止まってても反応します

*3:オブジェクト指向とかイベントドリブンとか、そういうキーワードが出てきてややこしいので説明しません

*4:Scratch も自分でループ書く必要はないけど、実は奥の見えないところでぐるぐる回ってます

*5:さらにプチコンは計算式だけの文を認めてないのでエラーになる

*6:実際、代入をイコールではなく矢印とかで書けるプログラミング言語もあります

*7:ちなみに英語では、数学の代入は substitution、プログラミングの代入は assignment と違う単語だったりします

*8:正確には、プチコンの描画のタイミングより速すぎて、描かれる前に画面の外まで移動してしまっているので、『速すぎて見えない』というより『速すぎて描くのが間に合わない』って感じ。そら見えへんわー。昔のコンピュータの BASIC だと、ウェイト無しで速すぎることはあっても見えないなんてことはなかったんだけどね……。技術の進歩はすごい。

*9:処理が多すぎてそもそも60分の1秒に間に合わないときにガクッとゲームのスピードが落ちること

*10:ノスタルジー的には不等号も <> が良かったかもしれない(笑)

「調査観察データの統計科学」3.1章 傾向スコアの数式メモ(後半)


前回に続いて、「調査観察データの統計科学」の 3.1章の後半を読む。
バランシングスコア b(x) を定義し、b(x) がバランシングスコアであることと、ある関数 g があって p(z=1|\boldsymbol{x})=g(b(\boldsymbol{x})) が成り立つことが同値である(特に十分である)ことを示した(p60)。


続いて本では、b(x) がバランシングスコアであり、かつ 2.5章の「強く無視できる割り当て」条件、つまり (y1,y0)⊥z|x が成立している時、(y1,y0)⊥z|b(x) が成り立つと言っている(p61)。
命題として書くと、


\boldsymbol{x}\bot z\;|\;b(\boldsymbol{x}) かつ (y_1,y_0)\bot z\;|\;\boldsymbol{x} ならば (y_1,y_0)\bot z\;|\;b(\boldsymbol{x})


となる。
これは、共変量 x を条件付けると割り当て z と潜在的結果変数 (y1,y0) が独立であるという「強く無視できる割り当て」条件について、バランシングスコアのもとでは「b(x) を条件付けると」に置き換えることができるということである。
共変量 x は一般に多次元であるのに対して、バランシングスコアはスカラーなので、「b(x) を縛ると」という条件は「 x を縛ると」よりもかなり緩いものになってくれていて、これがバランシングスコア(引いては傾向スコア)の嬉しさにつながる重要な性質だったりする。


さて本では、p61 の一番上の積分(期待値)を用いた複雑な式でこの命題を示そうとしている。式(3.2)と同様の計算ではあるのだが、こちらもそちらも「 x で期待値を取っているのに x が残る」という一見間違っているようにみえる式変形になっている。
実は一度 g(b(x)) に置き換えてから戻しているのだが、そのステップが省略されてしまっている。
式(3.2) の方は積分は必須なので避けられないが、p61 の方の命題は確率の乗法定理だけで示せる。丁寧にやるとちょっと数式長くなるんで、社内勉強会のスライドから抜粋。



長いが、一個一個は簡単な変形なので大丈夫だろう。
数式だけだとピンと来ないかもしれないけど、この関係式はグラフィカルモデルを描いたら明らかだったりする。



このグラフィカルモデルで、強く無視できる割り当ては (y1,y0) から z への矢印を切り、バランシングスコアは x から z への矢印を切る。すると、PRML 8章で言うところの tail-to-tail となり、b(x) で縛れば (y1,y0) と z は条件付き独立。一目瞭然。
というかこの絵を描いて、「なーんだ。じゃあ乗法定理だけで示せるはず〜」と上の証明を見つけた。


さて、こういう嬉しい性質のあるバランシングスコアの実例が「傾向スコア」である。


e_i:=p(z_i=1|\boldsymbol{x}_i) を第 i 対象者の「傾向スコア」と言う。
特に \boldsymbol{e}=(e_i) を単に「傾向スコア」と呼ぶ。


傾向スコア ei に対し、b(xi):=ei はバランシングスコアである。実際、関数 g を g(b(x)):=b(x) と置けば、g(b(\boldsymbol{x}_i))=e_i=p(z_i=1|\boldsymbol{x}_i) であり、前半で証明した「十分性」により b(x) がバランシングスコアであることがわかる。
実は、ここまで「嬉しい性質を持つバランシングスコア」なるものが存在するとは一言も言っていなかった。傾向スコアはバランシングスコアを構成することでその存在も証明したわけだ。他にもあるのだろうが、とりあえず知らない。

傾向スコア e_i:=p(z_i=1|\boldsymbol{x}_i) の真値はどう見ても分かりそうにないので、x_i と z_i から推定する必要がある。推定にはプロビット回帰やロジスティック回帰が使われることが多いということで、ロジスティック回帰の場合の説明が続く。
ロジスティック回帰のところは一般的な話なので大丈夫かな。強いて言えば exp の中に定数項が無いのがちょっと心配なくらいか。必要なら x_i がバイアス用の定数成分を持つことにすればいいんだろう。


というわけで、懸念があるとすれば「そもそもロジスティック回帰でいいの?」という部分だろう。
でも大丈夫。3.5章で、ロジスティック回帰モデルが傾向スコアの正しいモデルでない場合にロバストな推論を行うには、という話が出てくる。
えーとじゃあ「ロジスティック回帰モデルが傾向スコアの正しいモデルである」ってどういう状態? それがわかんなかったら、「正しくないからロバストな推定したい!」とすら思えないよね……。
というわけで、一番簡単な「ロジスティック回帰モデルが正しいモデルである例」でも見ておこう。


共変量 x は処置群 z=1 と対照群 z=0 とできっと異なる分布をしているはず(同じ分布なら無作為抽出と同等だから、めんどくさいことする意味ない!)。できるだけ単純にするため、x は一次元にしてしまい、処置群も対照群も同じ正規分布で、ただ平均がずれてるだけということにする。

  • p(x|z=1) = N(1,1)
  • p(x|z=0) = N(-1,1)


このとき、傾向スコア p(z=1|x) は


p(z=1|x)=\frac{p(x|z=1)p(z=1)}{p(x|z=1)p(z=1)+p(x|z=0)p(z=0)}


p(z=1) と p(z=0) がいるなあ。これも単純に p(z=1) = p(z=0) = 1/2 ってことにしよう。すると、


p(z=1|x)=\frac{p(x|z=1)}{p(x|z=1)+p(x|z=0)}=\frac{\exp(-(x-1)^2/2)}{\exp(-(x-1)^2/2)+\exp(-(x+1)^2/2)}
=\frac{1}{1+\exp(-(x+1)^2/2+(x-1)^2/2)}=\frac{1}{1+\exp(-2x)}


とロジスティック関数が登場。というわけでこの単純な例では、ちゃんとロジスティック回帰が真のモデルだとわかる。
一般的には、ロジスティック回帰は線形分類器であり、したがって完全に線形分離可能とまでは言わないものの、そこそこ分離できてないと当てはまり度はどんどん下がっていく。例えば上の簡単な例でも、p(x|z=1) = N(1,100) のように片方が大きい分散を持つだけで線形分離ではなくなる。
そうなってくると3.5章で説明されるような「二重にロバストな推定量」などが必要になってくるのだろう。