岩波データサイエンス Vol.6: ノート1

6月下旬に以下の本が出ましたので、この本を読んで時系列解析の復習ポイントだと思ったことと、本の記述に対して自分で考えたことを書きます。誤っている可能性があります。
岩波データサイエンス Vol.6岩波データサイエンス Vol.6
岩波データサイエンス刊行委員会

岩波書店 2017-06-23
売り上げランキング : 944

Amazonで詳しく見る
by G-Tools

  • そもそもなぜ時系列解析には状態空間モデルなのかというと、時系列を予測したい場合は、本質的に、「観測できない真のダイナミクス」と「ノイズ」を分離する必要があるからだよという話。ノイズまで学習してしまったら予測にとっては害だから(「しばしば悲惨な結果をもたらす」5ページ)。
  • 直接観測できない真の姿を状態という。状態を観測するときにノイズが加わるし(観測ノイズ)、その状態が時間発展するときにもノイズが加わる(システムノイズ)(7ページ)。
    • 線形・ガウス型状態空間モデルとは発生する観測ノイズ  v_n とシステムノイズ w_n が平均ゼロのガウシアン(分散は時変でよい)、一期前の状態を現在の状態にうつす写像 F_n が線形(時変でよい)、システムノイズが状態に加わるときの変換  G_n も線形(時変でよい)、観測も線形 H_n(時変でよい;観測ノイズは平均ゼロのガウシアンのまま観測に加わるとする)の状態空間モデルを特にそういう(7~8ページ)。
       x_n = F_n(x_{n-1})+G_n(v_n)
      y_n = H_n(x_n) + w_n
      この式をみるとなんでシステムモデルの方のノイズだけ  G_n が付いてるんだと思うけど、平均ゼロのガウシアンを G_n で線形変換するというのは平均ゼロとは限らないガウシアンにするということで、例えば海の上で漂流していて陸地までの距離を知りたいとき(下図;2015-12-29の日記からリサイクル)沖の方に流される風が吹いていたら「大気によるノイズ」はゼロでないドリフト成分をもつだろう。「それって状態の時間発展 F_n の方に入らないの?」って思うかもしれないけど、ケースバイケースだと思う。状態空間モデルを描くとき現実のシステムを思い浮かべて描くわけで、「うーんこの要素はシステムの範疇」だったら  F_n に含めるし、システム外と考えるのだったら  G_n に含めると思う。他方観測ノイズはドリフトしないのは「観測ノイズがドリフトすんな」という要請だと思う。仮に何らかのドリフト要素があるなら  H_n に含めましょうということなのだと思います。
      f:id:cookie-box:20151229173717p:plain:w400
  • 「パラメータ推定に使われる尤度も、予測値だけでは計算できない(11ページ)」
    • 観測データ  y_0 を説明するモデルを  p(y | \theta) と考えているとする。パラメータ  \theta の尤度関数は  f(\theta) = p(y_0 | \theta) となる。もし  p(x | \theta)デルタ関数だったら(分布を考えず点のみ予測するのだったら)  p(y_0 | \hat{\theta})=1 を与える  \hat{\theta} を除いて尤度がゼロになってしまいパラメータが尤もらしいかどうかの比較ができない。尤度関数が微分できないので最尤法もしづらい。たぶん。なお、時系列の場合の尤度関数は  f(\theta) = p(y_{0:n} | \theta) = \prod_{i=0}^{n} p(y_{i} | \theta)
  • 季節調整法で、トレンド成分、季節成分に加えて定常AR成分を導入すると予測がよくなることがある(20ページ)。
    • 定常過程とは: 弱定常過程 - クッキーの日記
      • 時系列から任意の10時点の値を取ってきたときにそれらがしたがう同時確率分布と、さらにそれぞれの30ステップ後の10時点の値を取ってきたときにそれらがしたがう同時確率分布が同じ。時系列をプロットしていったとき、プロットはずっと水平な帯の中におさまる。
    • 「定常モデルと非定常モデルでは長期予測においてまったく働きが異なる(20ページ)」: ここが何を言いたいのかと思ったんだけど、次の文章と合わせると、定常モデルは「繰り返されるパターンが何か」を表現しさえすればよいけど、非定常モデルは「繰り返されるパターン」「あと時系列全体の成長トレンド」を同時に表現することになるので働きが異なるということなんだろうと思います。
    • ここでいう「トレンドが波打っている」というのは、季節調整で排除できない、「商品が売れた月の翌月は一時的に売れ行きが鈍る」のような短期変動なのだろうと思います。
  • 拡張カルマンフィルタ(言葉と参考文献のみ;20ページ):
  • 混合ガウス分布近似で「正規分布の項数が時間の進行とともに爆発的に増大する(21ページ)」:
    • 何が爆発するのかわからないんですが、毎ステップか数ステップ毎に何か情報量基準を最小化するように混合正規分布をその個数も含めてフィッティングし直すということなんでしょうか。
  • R の dlm はつかったことがあります。

雑記

Anaconda 環境を利用した Windows への Keras (TensorFlow backend) 導入

以下の記事の手順でできました。
qiita.com今回は Keras を利用するので Keras も導入しました。Anaconda インストール後、コマンドプロンプトを立ち上げて以下のように「仮想環境作成 → 仮想環境立ち上げ → TensorFlow導入 → Keras導入」しました。仮想環境を作成するとき Python のバージョンを指定できますが、3.5 にしないといけないようです。

C:\work> conda create -n tensorflow python=3.5 anaconda
C:\work> activate tensorflow
(tensorflow) C:\work> pip install --ignore-installed --upgrade https://storage.googleapis.com/tensorflow/windows/cpu/tensorflow-1.0.1-cp35-cp35m-win_amd64.whl
(tensorflow) C:\work> pip install keras

これで Keras (TensorFlow backend) がつかえるようになったことを確認するために以下のスクリプトを実行してみます。中間層が2層の全結合ニューラルネットワークで iris を分類します。

# -*- coding: utf-8 -*-
import numpy
import random
from keras.models import Sequential
from keras.layers.core import Dense, Activation
from keras.utils import np_utils
from sklearn import datasets

if __name__ == '__main__':
  # irisデータのロード
  iris = datasets.load_iris()
  x = iris.data
  y = np_utils.to_categorical(iris.target, num_classes=3)

  # シャッフルして訓練データとテストデータに分割
  n = iris.target.shape[0]
  indices = random.sample(range(n), n)
  x = x[indices]
  y = y[indices]
  n_train = int(n / 2)
  x_train = x[range(n_train)]
  y_train = y[range(n_train)]
  x_test = x[range(n_train, n)]
  y_test = y[range(n_train, n)]

  # モデルの構築と学習
  model = Sequential()
  model.add(Dense(10, input_shape=(x.shape[1],), activation='relu'))
  model.add(Dense(10, activation='relu'))
  model.add(Dense(3, activation='softmax'))
  model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
  model.fit(x_train, y_train, epochs=100, batch_size=10, shuffle=True)

  # テスト結果表示
  scores = model.evaluate(x_train, y_train, verbose=0)
  print('Accuracy (train): %.2f%%' % (scores[1]*100))
  scores = model.evaluate(x_test,  y_test,  verbose=0)
  print('Accuracy (test) : %.2f%%' % (scores[1]*100))

以下のように学習できました。

(tensorflow) C:\work> python test.py
...
Epoch 99/100
75/75 [==============================] - 0s - loss: 0.3137 - acc: 0.9733
Epoch 100/100
75/75 [==============================] - 0s - loss: 0.3072 - acc: 0.9733
Accuracy (train): 97.33%
Accuracy (test) : 96.00%
さらに Anaconda 環境への OpenCV の導入

さらに画像処理ライブラリ OpenCV も導入します。
Anaconda で対象の仮想環境を立ち上げた状態で、以下のコマンドで導入できます。

(tensorflow) C:\work> conda install -c menpo opencv3

導入が上手くいったかどうか、以下のスクリプトで確認します。

# -*- coding: utf-8 -*-
import cv2
import numpy

if __name__ == '__main__':
  image = numpy.zeros((256, 256, 3), numpy.uint8)
  for i in range(256):
    for j in range(256):
      image[i, j, 0] = i
      image[i, j, 1] = j
  cv2.imwrite('hoge.png', image)

以下のような画像ファイル hoge.png が生成されます。

f:id:cookie-box:20170806220129p:plain

OpenCV で動画作成

OpenCV で動画を作成することができます。以下のスクリプトで movie.avi が生成されます。以下では各フレームを画素の行列から生成していますが、画像ファイルを読み込んでつなげて動画にすることもできます。

# -*- coding: utf-8 -*-
import cv2
import numpy

if __name__ == '__main__':
  vvw = cv2.VideoWriter('movie.avi', cv2.VideoWriter_fourcc('X','V','I','D'), 10, (200, 200))
  for i in range(10):
    for j in range(10):
      j_ = (9 - j) if (i % 2 == 1) else j
      image = numpy.zeros((10, 10, 3), numpy.uint8)
      image[i, j_, 0] = i  * 25
      image[i, j_, 1] = j_ * 25
      image = cv2.resize(image, (200, 200), interpolation=cv2.INTER_AREA)
      vvw.write(image)

以下のような動画ができます。.avi ははてなブログに貼り付けられないようなので、以下のページで .gif に変換しました。
AVI GIF 変換。オンライン フリー — Convertio

f:id:cookie-box:20170806222204g:plain

なお、cv2.VideoWriter の第2引数の4文字はコーデック指定です( Video Codecs by FOURCC - fourcc.org )。最適かどうかはわかりませんがとりあえず ('X','V','I','D') で生成できました。第3引数は1秒あたりのフレーム数、第4引数は画像サイズです。

雑記

データを分析するとき、取り込んだままのデータの列をそのままつかうのではなくて、元々あった列をもとに新しい列をつくることとかあると思います。でも Python とかよくわからないので Google 検索してどうやって加工するか調べると思います。

例えば、元々あった列のうち a, b, c 列の中央値をとり、新しい列 d に追加したいと思います。
以下の (1) (2) はどちらも望みを満たすようであることがわかります。でも (1) は遅いのでやってはいけないとわかります。
というか (2) のように一気にできるとわかれば (1) のように行ごとの処理にはしないと思いますが調べ方が難しいと思います。

# -*- coding: utf-8 -*-
import pandas
import numpy

if __name__ == "__main__":
  data = pandas.DataFrame({'name': numpy.repeat('Alice', 100000),
                           'a': range(0, 100000, 1),
                           'b': range(0, 200000, 2),
                           'c': range(0, 400000, 4)})

  # (1) 44.674 sec
  data['d'] = data.apply(lambda x: numpy.median(x[['a', 'b', 'c']]), axis=1) 
  # (2) 0.010 sec
  data['d'] = numpy.median(data[['a', 'b', 'c']].values, axis=1)

ともあれ、numpy.mean や numpy.median などで各行への処理を一気にできるのだったらそうすればいいことはわかります。
ただ、もっと自分で定義した処理をしたいときはベクトル演算が定義されているとは限らないので困ると思います。
そのような場合は numpy.vectorize で関数をベクトル化することにより処理速度を改善できると以下の記事にあります。
sinhrks.hatenablog.com

例えば、データに元々ある time 列をもとに、米国サマータイム期間かどうかを is_dst 列として付加したいとします。
apply をつかうと (1) のようにかけます。でも上の記事のアドバイス通りこの処理をベクトル化した (2) の方が速いです。

# -*- coding: utf-8 -*-
import pandas
import numpy

def is_dst(dt):
  return(pandas.Timestamp(dt).tz_localize('Asia/Tokyo').tz_convert('US/Eastern').dst().seconds > 0)

if __name__ == "__main__":
  data = pandas.DataFrame({
    'name': numpy.repeat('Alice', 100000),
    'time': numpy.repeat(['2017/03/12 15:59', '2017/03/12 16:00'], [50000, 50000])})
  data.time = pandas.to_datetime(data.time)

  # (1) 28.834 sec
  data['is_dst'] = data.apply(lambda x:
    x.time.tz_localize('Asia/Tokyo').tz_convert('US/Eastern').dst().seconds > 0, axis=1)
  # (2) 9.768 sec
  data['is_dst'] = numpy.vectorize(is_dst)(data.time)

ところで、上の実装で自作の is_dst 関数は受け取った日付時刻を pandas.Timestamp 型に変換していますが、time 列は元々 pandas.Timestamp 型です。なのになぜこの変換をしているのかというと、ベクトル化した関数を time 列に適用したときに time 列が勝手に numpy.datetime64 型になってしまったからです。なんでそうなるのかは Python とかよくわからないのでわかりません。

雑記: sklearn.cluster.KMeans で k-means 法

sklearn.cluster.KMeans をつかってみたいときとかあると思います。iris データにつかうと以下のようになります。

# -*- coding: utf-8 -*-
import pandas
from sklearn import datasets
from sklearn.cluster import KMeans
from sklearn.metrics import confusion_matrix

if __name__ == "__main__":
  iris = datasets.load_iris()
  predict = KMeans(n_clusters=3).fit(iris.data)

  mat = pandas.DataFrame(confusion_matrix(iris.target, predict.labels_))
  mat.index = iris.target_names
  print(mat)
             0   1   2
setosa       0  50   0
versicolor  48   0   2
virginica   14   0  36

setosa はすべてクラスタ1に分類され(かつ、クラスタ1に他の品種は混ざっていない)、一方、virginica はクラスタ0とクラスタ2に分裂してしまったことがわかります。各品種が分類された最大クラスタが対角線上に並んでいないと見にくいという人は、以下のように列を入れ替えてください。

  mat = mat.loc[:,mat.idxmax(axis=1)]
  mat.columns = range(iris.target_names.shape[0])
  print(mat)
             0   1   2
setosa      50   0   0
versicolor   0  48   2
virginica    0  14  36

クラスタリングした結果、各クラスタの重心はどこになったかも確認できます(以下)。

  print(predict.cluster_centers_)
[[ 6.85        3.07368421  5.74210526  2.07105263]
 [ 5.006       3.418       1.464       0.244     ]
 [ 5.9016129   2.7483871   4.39354839  1.43387097]]

初期クラスタ重心を与えることもできます。
以下のようにすれば元データの 0 番目、50 番目、100 番目を初期クラスタ重心にできます。

# -*- coding: utf-8 -*-
import numpy
import pandas
from sklearn import datasets
from sklearn.cluster import KMeans
from sklearn.metrics import confusion_matrix

if __name__ == "__main__":
  iris = datasets.load_iris()
  initial_centers = numpy.array([iris.data[0], iris.data[50], iris.data[100]])
  predict = KMeans(n_clusters=3, init=initial_centers).fit(iris.data)

  mat = pandas.DataFrame(confusion_matrix(iris.target, predict.labels_))
  mat.index = iris.target_names
  print(mat)
  print(predict.n_iter_)

ただし、これを実行すると以下のように警告文が出てきます。これの意味は、KMeans はクラスタ初期化方法が k-means++(デフォルト)や random の場合はデフォルトで n_init=10 通りの初期値からのクラスタリングを試み、最もよいクラスタリング結果を出力します(モーメントがよく更新され続けた回をもっともよいクラスタリング結果とする)。しかし、初期クラスタ重心が明示的に与えられたときにはその他の初期化を試みるのはおかしいので、デフォルトの n_init=10 は無視して1回しかクラスタリングを試みませんよという意味です。その代わりにというか、クラスタリングイテレーション回数 n_iter_ が返却されます。クラスタ重心は3回更新されるのみだったようです。

/usr/local/lib/python2.7/site-packages/sklearn/cluster/k_means_.py:889: RuntimeWarning:
 Explicit initial center position passed: performing only one init in k-means
 instead of n_init=10
  return_n_iter=True)
             0   1   2
setosa      50   0   0
versicolor   0  48   2
virginica    0  14  36
3

雑記: Keras で自作レイヤー(Dense)

Keras でオリジナルの自作レイヤーを追加したいときとかあると思います。
自作レイヤー自体は以下の記事でつかったことがありますが、これはウェイトをもつレイヤーではなく、最後にかぶせて損失関数のみをカスタマイズするためのレイヤーでした。
Keras で変分自己符号化器(VAE)を学習したい - クッキーの日記
ウェイトをもつレイヤーはどう追加するのか知りたいのですが、以下のドキュメントの通りにすればできるのか確認します。
Writing your own Keras layers - Keras Documentation
上のページの MyLayer は単純な Dense のようなので、実際に Dense に置き換えられるか確認します。

keras.layers.Dense をつかった場合

まず、 普通に Keras の Dense をつかって MNIST の分類器をつくると例えば以下になると思います。モデルは適当です。
→ 下記スクリプトの設定で 95% 程度のテスト精度になりました。

# -*- coding: utf-8 -*-
import numpy
from keras.layers import Input, Dense, Dropout
from keras.models import Model
from keras.datasets import mnist
from keras.utils import np_utils

if __name__ == "__main__":
  (x_train, y_train), (x_test, y_test) = mnist.load_data()
  x_train = x_train.astype('float32') / 255.
  x_test = x_test.astype('float32') / 255.
  x_train = x_train.reshape((len(x_train), numpy.prod(x_train.shape[1:])))
  x_test = x_test.reshape((len(x_test), numpy.prod(x_test.shape[1:])))
  y_train = np_utils.to_categorical(y_train, num_classes=10)
  y_test = np_utils.to_categorical(y_test, num_classes=10)

  input = Input(shape=(x_train.shape[1],))
  converted = Dense(20, activation='relu')(input)
  converted = Dropout(0.2)(converted)
  converted = Dense(20, activation='relu')(converted)
  converted = Dropout(0.2)(converted)
  converted = Dense(10, activation='softmax')(converted)
  classifier = Model(input, converted)
  classifier.compile(optimizer='adam', loss='categorical_crossentropy',
                     metrics=['accuracy'])
  print(classifier.summary())

  classifier.fit(x_train, y_train, epochs=20, batch_size=100, shuffle=True)

  scores = classifier.evaluate(x_train, y_train, verbose=0)
  print('Accuracy (train): %.2f%%' % (scores[1]*100))
  scores = classifier.evaluate(x_test, y_test, verbose=0)
  print('Accuracy (test) : %.2f%%' % (scores[1]*100))
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 784)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 20)                15700     
_________________________________________________________________
dropout_1 (Dropout)          (None, 20)                0         
_________________________________________________________________
dense_2 (Dense)              (None, 20)                420       
_________________________________________________________________
dropout_2 (Dropout)          (None, 20)                0         
_________________________________________________________________
dense_3 (Dense)              (None, 10)                210       
=================================================================
Total params: 16,330
Trainable params: 16,330
Non-trainable params: 0
_________________________________________________________________
Accuracy (train): 95.69%
Accuracy (test) : 94.92%
自作 Dense をつかった場合

次に、import Dense をつかわずに自作 Dense ( class MyDense ) に置き換えたのが以下です。
→ これも 95% 程度のテスト精度になったので、ちゃんと Dense になっていると思います。
  パラメータ数も上と同じ 16,330 です。

Keras ドキュメント(Writing your own Keras layers - Keras Documentation)にあるとおり、自作レイヤーのクラスでは build メソッドでウェイトを定義し、call メソッドにウェイトを用いたこのレイヤーの計算処理を実装すればよいはずです。compute_output_shape メソッドは次のレイヤーのために出力 shape を返すようにしておきます。

# -*- coding: utf-8 -*-
import numpy
from keras import backend as K
from keras.engine.topology import Layer
from keras.layers import Input, Dropout
from keras.models import Model
from keras.datasets import mnist
from keras.utils import np_utils

class MyDense(Layer):
  def __init__(self, output_dim, activation, **kwargs):
    self.output_dim = output_dim
    self.activation = activation
    super(MyDense, self).__init__(**kwargs)

  def build(self, input_shape):
    self.kernel = self.add_weight(name='kernel',
                                  shape=(input_shape[1], self.output_dim),
                                  initializer='glorot_uniform')
    self.bias   = self.add_weight(name='bias',
                                  shape=(1, self.output_dim),
                                  initializer='zeros')
    super(MyDense, self).build(input_shape)

  def call(self, x):
    if self.activation == 'relu':
      return(K.relu(K.dot(x, self.kernel) + self.bias))
    elif self.activation == 'softmax':
      return(K.softmax(K.dot(x, self.kernel) + self.bias))
    else:
      return(K.dot(x, self.kernel) + self.bias)

  def compute_output_shape(self, input_shape):
    return(input_shape[0], self.output_dim)

if __name__ == "__main__":
  (x_train, y_train), (x_test, y_test) = mnist.load_data()
  x_train = x_train.astype('float32') / 255.
  x_test = x_test.astype('float32') / 255.
  x_train = x_train.reshape((len(x_train), numpy.prod(x_train.shape[1:])))
  x_test = x_test.reshape((len(x_test), numpy.prod(x_test.shape[1:])))
  y_train = np_utils.to_categorical(y_train, num_classes=10)
  y_test = np_utils.to_categorical(y_test, num_classes=10)

  input = Input(shape=(x_train.shape[1],))
  converted = MyDense(20, activation='relu')(input)
  converted = Dropout(0.2)(converted)
  converted = MyDense(20, activation='relu')(converted)
  converted = Dropout(0.2)(converted)
  converted = MyDense(10, activation='softmax')(converted)
  classifier = Model(input, converted)
  classifier.compile(optimizer='adam', loss='categorical_crossentropy',
                     metrics=['accuracy'])
  print(classifier.summary())

  classifier.fit(x_train, y_train, epochs=20, batch_size=100, shuffle=True)

  scores = classifier.evaluate(x_train, y_train, verbose=0)
  print('Accuracy (train): %.2f%%' % (scores[1]*100))
  scores = classifier.evaluate(x_test, y_test, verbose=0)
  print('Accuracy (test) : %.2f%%' % (scores[1]*100))
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 784)               0         
_________________________________________________________________
my_dense_1 (MyDense)         (None, 20)                15700     
_________________________________________________________________
dropout_1 (Dropout)          (None, 20)                0         
_________________________________________________________________
my_dense_2 (MyDense)         (None, 20)                420       
_________________________________________________________________
dropout_2 (Dropout)          (None, 20)                0         
_________________________________________________________________
my_dense_3 (MyDense)         (None, 10)                210       
=================================================================
Total params: 16,330
Trainable params: 16,330
Non-trainable params: 0
_________________________________________________________________
Accuracy (train): 95.83%
Accuracy (test) : 95.14%

class MyDense の実装は Keras ドキュメント(Writing your own Keras layers - Keras Documentation)の MyLayer を参考にしましたが、必要に応じて keras.engine.topology.Layer のソースを参照して修正しました。
keras/topology.py at master · fchollet/keras · GitHub

  • 当初 Keras ドキュメントの MyLayer のとおりに実装したところ、パラメータ数からバイアス項がないことに気付いたので、バイアス項を追加しました。
  • add_weight の引数に name が必要だったので追加しました。name をつけて何につかうのかわかりません。→ 後から自作 MyDense にはバイアス項がないことに気付いたとき、ユニークな名前でいくつでも weight 行列を追加できることに気付きました。
  • kernel と bias の初期化方法は本家 Dense に合わせました(初期化が uniform だといまいち到達精度が 94% 程度にしかなりませんでした)(誤差もあるかもしれませんが)。
  • 今回のモデルでは Dense を ReLU または softmax で活性化するので、Dense よろしく MyDense インスタンス生成時に活性化関数も指定するようにしました(ReLU と softmax 以外未対応)(活性化関数自体を引数に取ればいいんだろうとは思います)。



これで Dense が自作できることは確認できたのですが、自分がつくりたいのは Dense ではないのでこれからもっと調べていきます。