雑記: Windows10上AnacondaへのGPU版TensorFlow導入

GPU環境がほしいとかあると思います。ハードのことはよくわからないですが、さすがにGPU環境を手に入れるためにはそれに対応したGPUというものが必要そうな気がします。ちなみにGPUが何なのかはよくわかりません。画面を描画するための並列処理をする演算装置らしいですが、画面を描画するための情報ってそんなに後処理が必要な状態になっているんでしょうか。何もわかりません。

今回やったことは以下です。先に書いておくと大事なのは正しいバージョンの CUDA と cuDNN をインストールすることです。そこガバガバだと駄目です。というかちゃんとそう書いている先達がいらっしゃいました。
WindowsにTensorflowを入れてみる(成功編)

それっぽいGPUが入ったマシンを手に入れます。

手元のマシンにGPUを組み込むことができればGPUを調達すればいいと思うんですが、手元のノート2台はどちらも結構古いので今回はGPUが入っているノートを新調しました。ノートなのは自分には据え置き機を据え置いて作業できる場所がないので致し方ないです。OS は Windows 10 で、NVIDIA GeForce GTX 1060 というGPUが入っているらしく、それっぽさが感じられます。なおこれがどういうGPUなのかはよくわかりません。あとこういうGPUが入ったマシンがほしいときは自作するかBTOショップで購入するらしいです。インターネットに書いてありました。今回はBTOショップで購入しました。

  • それでよくわからないんですが、こういう単体GPUが入っているマシンにおいて単体GPUはいつどのように使われているんでしょうか。基本的に画面描画には単体GPUから使うんでしょうか。機械学習のために単体GPUを使いたい人は「画面描画を頑張っていただかなくてもよいです」とかならないんでしょうか。→ 設定から自分で細かく設定できるんですね。
    Windows10 - グラフィックの設定(アプリごとに使用するGPUを選択) - PC設定のカルマ
    ただグラフィックの設定という項目が見当たらないんですが、単体GPUを画面描画に使用するためのドライバをインストールしていないからかもしれません。あと普通にタスク マネージャーのパフォーマンス タブにGPUが表示されるんですね。みるとGPU 0=内臓GPUIntel HD Graphics 630)は使用されていましたがGPU 1=単体GPUNVIDIA GeForce GTX 1060)は使用率0%でした。
GPU版 TensorFlow を入れる前に用意しておくものを確認します。

どのように環境構築するか何も考えていなかったんですが、よくわからないので Anaconda を入れてそこに TensorFlow を入れることにします。TensorFlow 公式を参照します。
https://www.tensorflow.org/install/install_windows
これを参照すると、GPU対応の TensorFlow を入れるときは以下を入れておくこととありますが、それぞれ何なのかよくわからないので確認しておきます。

CUDA Toolkit 9.0 及び NVIDIA Driver

  • CUDAとはどうもGPU向けのコンパイラであるようです。ライブラリも含まれているようです。通常のCPU向けのコンパイラでは駄目なのかはよくわからないですが、GPUとCPUは構造が違うので、GPUが処理しやすいような命令にした方がよさそうな気はします。NVIDIA Driver というのは、なんかプリンタで印刷するときにもプリンタを動かすドライバがいるのでGPUを動かすドライバが要るんだと思います。
    CUDA - Wikipedia

cuDNN v7.0

CUDA Compute Capability 3.5 以上の GPU (TensorFlow をソースからビルドする場合は 3.0 以上)

  • 最後に GPU です。GPU が要るのは当たり前です。ただし、CUDA Compute Capability なるものが 3.5 以上ある GPU でなければならないとのことです。下のページで確認すると、GeForce GTX 1060 の CUDA Compute Capability は 6.1 なので要求を満たしています。CUDA Compute Capability が何なのかはさっぱりわかりませんが、GPU にもなんか女子力みたいなものがあるんだろうと思っておきます。
    CUDA GPUs | NVIDIA Developer
CUDA Toolkit 9.0 及び NVIDIA Driver をインストールします。

TensorFlow 公式からリンクがある以下のドキュメントの自分に必要な部分を日本語にしたにすぎません。
Installation Guide Windows :: CUDA Toolkit Documentation

  • そもそもGPUの存在を確認します。Windowsマークを右クリックしてデバイス マネージャーを起動、ディスプレイ アダプターを展開して、NVIDIA GeForce GTX 1060 が含まれていることを確認します。
  • Microsoft Visual Studio が必要なのでドキュメントに記載のあるバージョンをインストールします。以下のリンクから Visual Studio Community 2017 をインストールしました。インストール時にワークロードのうち Desktop development with C++ を選択しました。必要な Visual C++コンパイラが含まれていればいいと思うんですがどこまでインストールするのが最小限なのかわかりません。
    ダウンロード | IDE、Code、Team Foundation Server | Visual Studio
  • ドキュメント内のリンクから、CUDA Toolkit 9.0 のインストーラをダウンロードします。リンク先には最新版(この記事を書いている時点では 9.2)のダウンロードボタンがありますが、要求のバージョンはこれではないので、ちゃんと Legacy Releases から 9.0 を探してダウンロードします
    CUDA Toolkit 9.2 Download | NVIDIA Developer
    適切なインストーラをダウンロードしたら画面にしたがってインストールします。再起動の指示があれば再起動します。
    • ここで不用意な人(自分)は最新版 CUDA 9.2 をインストールしてしまったかもしれません。その場合は改めて CUDA 9.0 をインストールすればいいのですが、何も考えずに CUDA 9.0 をインストールすると、The installed version of Nsight Visual Studio Edition is newer than the one to be installed などというメッセージが出て失敗します。これはメッセージの続きの指示通りこれをアンインストールすればよく、コントロールパネルのプログラムのアンインストールから NVIDIA Nsight Visual Studio をアンインストールした上で CUDA 9.0 をインストールし直せば成功します(手元の環境では)。
  • ドキュメントには適切な Driver Model を使うこととあるんですがよくわかりません。Driver 自体は上の手順で一緒にインストールされたと思います。
  • コマンドプロンプトを起動し、
    C:\Users\Cookie>nvcc -V
    と入力してインストールされた CUDA Toolkit のバージョンが 9.0 であることを確認します。
cuDNN v7.0 をインストールします。

TensorFlow 公式からリンクがある以下からインストールすればいいんですが NVIDIA アカウントが必要なので作成します。色々なアンケートに答えるとようやく zip をダウンロードできます。ここでも最新版をダウンロードするのではなく、Archived cuDNN Releases から CUDA 9.0 向けの cuDNN 7.0 系を選択します。ダウンロードした zip は展開しておきます。
NVIDIA cuDNN | NVIDIA Developer
以下の 4.3. Installing cuDNN on Windows にしたがい、ファイルを所定の場所にコピーします。
cuDNN Installation Guide :: Deep Learning SDK Documentation

cuDNN を置いた場所に環境変数を通します(しかし、手元では既に通っていました)。

  • コマンドプロンプトを起動し、
    C:\Users\Cookie>control sysdm.cpl
    と入力すると「システムのプロパティ」ウィンドウが出てくるので「詳細設定」タブの「環境変数」をクリックします。システム環境変数に CUDA_PATH がなければ「新規」から変数名 CUDA_PATH に変数値 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0 をセットします。既にあればいいです。
Anaconda をインストールし、GPU版 TensorFlow をインストールします。

ここからは TensorFlow 公式の Installing with Anaconda の通りです。
https://www.tensorflow.org/install/install_windows

  • 以下のサイトから Anaconda 5.2 For Windows Installer の Python 3.6 version をダウンロードします。
    Downloads - Anaconda
    基本的に Next をクリックすればインストールできるはずです。
  • Anaconda Prompt を起動し、tensorflow という名前の環境をつくります。別の名前でもいいんですが。今回は Python3.6 なので引数もそうします。
    (base) C:\Users\Cookie>conda create -n tensorflow pip python=3.6
  • そのままつくった環境に入り、GPU版 TensorFlow をインストールします。
    (base) C:\Users\Cookie>activate tensorflow
    (tensorflow) C:\Users\Cookie>pip install --ignore-installed --upgrade tensorflow-gpu
  • インストールできたか検証します。過去に TensorFlow をインストールしたことがあることがある人にはいつものです。sess = tf.Session() を入力した後でぶおおんとなるので早くしろといった気持ちになると思います(手元では)。画面に GeForce GTX 1060 と表示されているので GPU を使っていそうです。
    (tensorflow) C:\Users\Cookie>python
    >>> import tensorflow as tf
    >>> hello = tf.constant('Hello, TensorFlow!')
    >>> sess = tf.Session()
    >>> print(sess.run(hello))
  • GPU とひもづいているかには以下のような確認方法もあるようで、これで確認してもOKでした。
    TensorFlowからGPUが認識できているかを2行コードで確認する - 動かざることバグの如し
Keras もインストールして画像分類タスクで動作確認します(GPU版 vs CPU版)。
  • 先の環境に Keras もインストールします。
    (tensorflow) C:\Users\Cookie>pip install keras
  • 試しにCPU版環境もつくってみます。別の名前にします。こちらの環境で GPU とのひもづけを確認すると当然ひもづいていません。
    (tensorflow) C:\Users\Cookie>deactivate
    (base) C:\Users\Cookie>conda create -n tensorflow-cpu pip python=3.6
    (base) C:\Users\Cookie>activate tensorflow-cpu
    (tensorflow-cpu) C:> pip install --ignore-installed --upgrade tensorflow
    (tensorflow-cpu) C:> pip install keras
  • 動作確認用のタスクを準備します。以下の記事で Augmentation 付きの画像分類をしたことがあったのでこれをもっとたくさんの画像でやってみます。ネットワーク構造その他は適当です。このタスクが GPU と CPU の比較に適しているかは知らないです。
    Keras で少ない画像データから学習したい - クッキーの日記
    • 以下のページから花の画像のデータを拾ってきます(Dataset images)。解凍すると jpg\ 以下に 1360枚の jpg が入っていますが、ファイル名順に80枚ずつ17種類の花の画像になっているので、以下のように振り分けておきます。それぞれの花は Iris とか Pansy とかきちんと名前がありますが、面倒なのでフォルダ名は flower0~flower16 とします。なお自動で振り分けるスクリプトは下の方にあります(Python でやることなのかは知りません)。
      Visual Geometry Group Home Page
      • data/train/flower0/image_0001.jpg(各花の画像80枚のうち60枚は訓練用)
      • data/validation/flower0/image_0061.jpg(各花の画像80枚のうち20枚は訓練用)
    • この花の画像を17クラス分類する畳込みニューラルネットワークを学習します。スクリプトは下の方にあります。PIL というモジュールがないと怒られたら以下の記事を参照して対処します(普通に conda install PIL しようとすると Python を 2.7 系にしようとしてくるので止めてください)。
      Kerasに「PILが無い」と怒られた場合の対策
    • GPU環境(上)とCPU環境(下)でそれぞれ実行した結果は以下です。エポック数は20です。
      Elapsed Time: 296.8[sec]
      Accuracy (train): 95.4%
      Accuracy (test): 54.7%
      Elapsed Time: 1116.6[sec]
      Accuracy (train): 93.2%
      Accuracy (test): 50.6%
      乱数固定できていないので正解率はぶれぶれですがとりあえずGPU版の方がだいぶ早かったです。
動作確認に使用したスクリプトは以下です。

画像データのフォルダ振り分け
jpg フォルダがある場所でこれを実行します。data/train フォルダと data/validation フォルダだけ作ってから実行しましたが作っておかなくても os.makedirs が作ってくれる気がします。なお、jpg フォルダの下から画像ファイルを移動してしまうので注意してください。

import os
import glob
import shutil
imgs = glob.glob('jpg\\*.jpg')
for img in imgs:
    img_num = int(img.replace('jpg\\image_', '').replace('.jpg', '')) - 1
    flower_num = int(img_num / 80)
    is_train = ((img_num - flower_num * 80) < 60)
    dir_name = 'data\\' + ('train\\' if is_train else 'validation\\') + 'flower' + str(flower_num) + '\\'
    if not os.path.exists(dir_name):
        os.makedirs(dir_name)
    shutil.move(img, dir_name + img.replace('jpg\\', ''))

畳込みニューラルネットワークによるクラス分類
昔の記事を2クラス分類から17クラス分類にして微調整したつもりなんですが、突貫で改造したので何か間違っていたらご連絡ください。

# -*- coding: utf-8 -*-
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras import backend as K
import time
#import pandas as pd

if __name__ == '__main__':
  img_width, img_height = 256, 256 # 訓練時の画像サイズ
  
  train_data_dir = 'data/train'
  validation_data_dir = 'data/validation'
  nb_train_samples = 60 * 17
  nb_validation_samples = 20 * 17
  epochs = 20
  batch_size = 32
  
  if K.image_data_format() == 'channels_first':
    input_shape = (3, img_width, img_height)
  else:
    input_shape = (img_width, img_height, 3)
  
  # モデル構築
  model = Sequential()
  model.add(Conv2D(32, (3, 3), activation='relu', input_shape=input_shape))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Conv2D(32, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Conv2D(64, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Flatten())
  model.add(Dense(128, activation='relu'))
  model.add(Dropout(0.5))
  model.add(Dense(17, activation='softmax'))
  
  model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])

  # 訓練データのプレ処理の設定: RGB値のスケーリングに加え、シア変形や拡大縮小によって訓練増強
  train_datagen = ImageDataGenerator(rescale=1. / 255, shear_range=0.2, zoom_range=0.2, horizontal_flip=True)
  # テストデータのプレ処理の設定: こちらはRGB値のスケーリングのみ
  test_datagen = ImageDataGenerator(rescale=1. / 255)
  
  # 訓練データをディレクトリ内のデータから随時作成するジェネレータ
  train_generator = train_datagen.flow_from_directory(train_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical', shuffle=True)
  # テストデータ作成をディレクトリ内のデータから随時作成するジェネレータ
  validation_generator = test_datagen.flow_from_directory(validation_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical', shuffle=True)
  
  # 訓練
  time_start = time.time()
  model.fit_generator(train_generator, steps_per_epoch=nb_train_samples//batch_size,
    epochs=epochs, validation_data=validation_generator, validation_steps=nb_validation_samples//batch_size)
  time_end = time.time()

  # 結果確認(のときは増強しない)
  train_generator = test_datagen.flow_from_directory(train_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical', shuffle=False)
  validation_generator = test_datagen.flow_from_directory(validation_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical', shuffle=False)

  print ('Elapsed Time: %.1f[sec]' % (time_end - time_start))
  result = model.evaluate_generator(train_generator)
  print('Accuracy (train): %.1f%%' % (result[1]*100))
  result = model.evaluate_generator(validation_generator)
  print('Accuracy (test): %.1f%%' % (result[1]*100))

直感 Deep Learning: 5章メモ(単語分散表現)(その1)

以下の本を読みます。

直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ
Antonio Gulli Sujit Pal 大串 正矢

オライリージャパン 2018-08-11
売り上げランキング : 1992

Amazonで詳しく見る
by G-Tools
キャラクターの原作とは関係ありません。本読みメモですがよく脱線します。誤りがありましたらご指摘いただけますと幸いです。
次回:まだ
f:id:cookie-box:20180305231302p:plain:w60

ジュン、これプロデューサーが買ってきた本なんだけど、5章を読むの手伝ってくれない?

f:id:cookie-box:20180305232608p:plain:w60

「直感 Deep Learning」? プロデューサーさんは深層学習の本なんてもう何冊も持ってるでしょう? しかもなんで5章?

f:id:cookie-box:20180305231302p:plain:w60

これ先月出た本で、評判がいいから買ったらしいんだけど、5章以外はもう知ってる話も結構ある感じだったから、まず5章から読みたいんだって。プロデューサーは自然言語処理の勉強したことないからって。

f:id:cookie-box:20180305232608p:plain:w60

じゃあ自然言語処理の本買えばいいだろ。

f:id:cookie-box:20180305231302p:plain:w60

それで、5章(137ページ~)の「単語分散表現」なんだけど、このタイトルからもう割と意味わかんないんだけど、2段落目まで読むと「単語分散表現」っていうのは「テキスト中の単語を数値ベクトルに変換する方法のひとつ」らしいんだ。でも分散表現って何かの説明がないのに「最も基本的な分散表現の手法である」って one-hot エンコーディングが引き合いに出てきて、one-hot エンコーディングには問題点があってとかなっててよくわかんない! いまの目的は何なの!?

f:id:cookie-box:20180305232608p:plain:w60

…ハヤトは、one-hot エンコーディングは理解しているんですか?

f:id:cookie-box:20180305231302p:plain:w60

それはこの本の10ページに書いてあって、6行しかないから読むのすぐだったよ。10個の手書き数字をそれぞれ以下のように表現するのが one-hot エンコーディングなんだろ?
  x手書き数字の"0" = (1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
  x手書き数字の"1" = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0)
  x手書き数字の"2" = (0, 0, 1, 0, 0, 0, 0, 0, 0, 0)
  \cdots
でも、そもそもなんで one-hot エンコーディングするんだろ? 「どの数字か」って情報はせっかく1次元だったのになんで10次元にしちゃったのって感じなんだけど。

f:id:cookie-box:20180305232608p:plain:w60

常に正解が確実にわかるのであれば確かに「どの数字か」という情報は1次元ですね。0~9のいずれかの整数などで表現できます。しかし今は正解に絶対の自信があるわけではありません。例えば数字の1にも数字の7にも見える字を書く人はたまにいますね。1らしいが7らしくもある。そんなときに出力が1次元だったらどうするんです? 1と7の間を取って4を返すわけにもいかないでしょう? 出力が10次元あれば、2番目と8番目の要素を 0.5 にすることでこの状況が表現できます。このように出力を柔軟に10次元とするなら、目指すべき正解も10次元にしておくべきですね。

f:id:cookie-box:20180305231302p:plain:w60

じゃあ、one-hot エンコーディングは、絶対の自信がないときの出力が目指すべき正解になるような表現ってこと…? そういうときに用いるのが分散表現なの?

f:id:cookie-box:20180305232608p:plain:w60

いえ、分散表現の正確な語義は知りませんが、少なくとも狭義には違うと思います。

f:id:cookie-box:20180305231302p:plain:w60

違うの!?

f:id:cookie-box:20180305232608p:plain:w60

おそらく137ページ3段落目の「最も基本的な分散表現」の「分散表現」は「データを数値ベクトルとして表現すること」程度の意味合いと思いますが、一般的に分散表現(distributed representation)といったとき、 局所表現(local representation)と対になる概念としてそういわれていることが多いようにみえます。one-hot エンコーディングはむしろ局所表現ですから、分散表現ではないと思います。以下などを参照させていただきました。

しかし、分散表現の英語 distributed representation は「分布表現」とも読めます。デルタ関数も特殊な確率分布だというような気持ちなら、局所表現も分散表現の特殊なケースといえるのかもしれません。であれば、one-hot エンコーディングも分散表現ですが。

f:id:cookie-box:20180305231302p:plain:w60

ごめんよくわかんない。

f:id:cookie-box:20180305232608p:plain:w60

そうですね、例えば…ハヤトは「ハヤトである」という特徴をもっていますね?

f:id:cookie-box:20180305231302p:plain:w60

え? いやまあ俺は俺だけど…特徴っていうかトートロジーだなそれは。

f:id:cookie-box:20180305232608p:plain:w60

はい、トートロジーです。何であれ、僕たち軽音部5人をそのように特徴付けるなら、僕たちはそれぞれ以下のような5次元の特徴ベクトルで表現できます。
  x隼人 = (1, 0, 0, 0, 0)
  x = (0, 1, 0, 0, 0)
  x夏来 = (0, 0, 1, 0, 0)
  x四季 = (0, 0, 0, 1, 0)
  x春名 = (0, 0, 0, 0, 1)

f:id:cookie-box:20180305231302p:plain:w60

できるって言われても…これ、one-hot エンコーディング

f:id:cookie-box:20180305232608p:plain:w60

ええ。そして、この one-hot エンコーディングは「分散」していないですね。

f:id:cookie-box:20180305231302p:plain:w60

何が?

f:id:cookie-box:20180305232608p:plain:w60

例えば、僕たち5人を先のような5次元ベクトルで表現して、「任意の2人を入力してその2人の仲のよさを出力するニューラルネットワーク」を学習するとします。

f:id:cookie-box:20180305231302p:plain:w60

なんでだよ、俺たちはみんな仲いいだろ! 不仲がいるみたいな感じやめろ! ちゃんとバンド内で仲良くして!?

f:id:cookie-box:20180305232608p:plain:w60

物の喩えです。しかし、先の表現では意味のある学習などできません。いえ、訓練データを暗記することはできるでしょう。しかし、訓練データを暗記することしかできません。なぜなら、ニューラルネットワークはハヤトについて「ハヤトである」以上の情報を知り得ませんので。そして当然、未知の新しいメンバーが入力された場合にも対応できません。そこで、表現を変えてみます。
  x隼人 = (1, 1, 1)
  x = (0, 0, 1)
  x夏来 = (1, 0, 1)
  x四季 = (0, 1, 1)
  x春名 = (0, 0, 0)

f:id:cookie-box:20180305231302p:plain:w60

何これ、3次元になった?

f:id:cookie-box:20180305232608p:plain:w60

ベクトルの1番目の要素は「担当が弦楽器である」、2番目の要素は「上にきょうだいがいる」、3番目の要素は「留年せず進級している」としてみました。

f:id:cookie-box:20180305231302p:plain:w60

3番目の要素が個人を特定しにかかってるな。

f:id:cookie-box:20180305232608p:plain:w60

こちらが(おそらく狭義の)分散表現です。ハヤトの特徴を「ハヤトである」から「弦楽器担当で、上にきょうだいがいて、留年していない」に「分散」したんです。こうすると、ニューラルネットワークは単なる暗記以上の学習をできる見込みがあります。例えば、「弦楽器担当の人と、上にきょうだいがいない人は特に仲がいいようだ」という情報が抽出できるかもしれません。そして、新メンバーにも対応できる可能性があります。このエンコーディングは未知のメンバーにも適用できますから。もちろん、これは例示的なヒューリスティックエンコーディングなので、実際の分散表現は未知データに対応できるとは限りません。というか、おそらく標準的な単語分散表現では対応できないですね。例えばこの章で紹介されている word2vec や GloVe では、膨大な数の文章(学習用コーパス)を与えて、そこに含まれる膨大な単語を教師なしで所与の次元数にエンコーディングします。なので、得られた表現の各次元にヒューリスティックな意味はありませんから、訓練に用いた文章に含まれていなかった単語のベクトル表現を知ることはできませんね。

f:id:cookie-box:20180305231302p:plain:w60

…なあジュン、この3次元の表現だと、俺とハルナは真逆ってこと?

f:id:cookie-box:20180305232608p:plain:w60

「弦楽器かどうか」「上にきょうだいがいるか」「留年していないか」のみが意味をもつ世界では最も遠くに配置されるというだけです。モデルの学習の上でこれらのみが意味をもつのでは足りないなら、4番目の要素、5番目の要素…をベクトルに追加するべきですね。もっとも、先にも言及したように、少なくともテキスト中の単語の分散表現では、ベクトルの要素の意味を人間的に与えて単語間の距離を決めるのではなく、逆に単語間の距離の手がかりの方を与えて「単語間の距離を適切に表すように空間内に単語を配置するやり方を何らかのアルゴリズムで学習する」と言った方が正しいです。また、いまの例示ではベクトルの各要素が  \{0,1\} のどちらかの値しかとっていませんが、もちろん実際には(計算機の範囲で)自由に実数値をとって構いません。

f:id:cookie-box:20180305231302p:plain:w60

「単語間の距離」って?

f:id:cookie-box:20180305232608p:plain:w60

どんなコーパスを用いて、どんな関係をもつ単語どうしが近いと定義するかによりますが、例えば「ギター」と「ベース」という単語は「近い」んじゃないか、と考えられます。同じ楽器ですし、「ギター」と「カレー」よりは「ギター」と「ベース」の方が近そうでしょう?

f:id:cookie-box:20180305231302p:plain:w60

…でもさ、ハルナの中では「ドラム」と「ドーナツ」が近いかもしれないじゃん。どっちも好きな物?みたいな感じで。

f:id:cookie-box:20180305232608p:plain:w60

そうですね、「春名さんのこれまでの発言」のようなコーパスを使って単語分散表現を学習したら本当にそうなるかもしれません。それはそれでいいです。単語分散表現は用いたコーパスによりますから。

f:id:cookie-box:20180305231302p:plain:w60

ふーん…あ、あとさ、単語って同じ単語でも違う意味があったりするじゃん。例えば、「翼」っていったら、うちの事務所の翼さんかもしれないし、765プロの翼さんかもしれないし、普通に鳥とかの翼の意味かもしれないし。

f:id:cookie-box:20180305232608p:plain:w60

いきなり単語分散表現の弱点を突くのはやめてください。

f:id:cookie-box:20180305231302p:plain:w60

ごめん…。

f:id:cookie-box:20180305232608p:plain:w60

整理しますね。

  • one-hot エンコーディング データを数値ベクトルにする手法であって、現れるデータが N 種類なら one-hot な N 次元のベクトルにするだけというもの。
    • なので、エンコーディングする対象のデータ集合さえあればベクトル表現が得られる。
    • ベクトルの各次元の意味は、「i 種類目のデータである」でしかない。
    • このエンコーディングをする目的は、多クラス分類問題においてモデルが出力すべきお手本をつくるためといった向きが大きい(はず)。
  • 分散表現: データを数値ベクトルにする手法であって、データ間の距離(類似性)を考慮した上でベクトル化する=空間内に配置するもの(具体的なアルゴリズムは色々ある)。
    • なので、エンコーディングする対象のデータ集合だけでなく、データ間の距離を示唆する情報も必要である(現実の場面では、エンコーディングする対象の単語たちが文章セットとして与えられるのでこれが達成される=例えば、文章の中で5単語以内の距離に出てくる頻度が多い単語どうしは距離が近いと定義するなど)。
    • ふつうベクトルの各次元にはヒューリスティックな意味はない(アルゴリズムによる)。
    • このエンコーディングをする目的は、データ(多くの場合、単語)を「その意味上の距離を保ったまま」モデルが扱える数値ベクトルにするためである(はず)。そうして得られた数値ベクトル表現は機械学習モデルの入力にも出力にもなりうるが、入力になることが多いと思われる。例えば以下のような使用シナリオが考えられる。
      • 文章セット全体から、登場する各単語を適当な分散表現手法で数値ベクトルにする。
      • 「各文章(=数値ベクトル列)→ その文章がポジティブかネガティブか」のようなRNNを教師あり学習する。
僕たちが機械学習で扱うデータの中で、「画像」や「音声」などは(何らかのセンサーを通して)直ちに実数の多次元配列です。しかし文章は違います。もちろん各文字に文字コードなどはあるでしょうが、文字コードの羅列は単語の意味を反映しません。それを無視した学習は(できなくはないですが)有効ではないでしょう。単語の意味を考慮した数値表現を得ることが必要です。それがいまの目的ですね。

f:id:cookie-box:20180305231302p:plain:w60

目的はわかったよ。それで、137ページの下の方に、単語分散表現以前の単語のベクトル化手法みたいなのが出てきて、「これらの手法は文章中の単語に着目しており、単語それ自体の意味を捉えようとする単語分散表現とは異なる手法になります」「単語分散表現は、文章を前提としそれを文脈とする従来の(中略)手法とは異なり、単語自体を文脈として使用します」ってあるんだけど、この辺も抽象的でよくわかんなくない? 単語を文脈として使用って何?

f:id:cookie-box:20180305232608p:plain:w60

本のタイトルが「直感」だから深く考えなくても。きちんと捉えたいなら、列挙されている単語分散表現以前の手法を追うべきでしょう。リンクも書いてありますし、調べてみましょう。

  • TF-IDF: 各単語を「全文書中でその単語が頻度ベースで占める割合」と「全文書中でその単語が出てくる文書の割合の逆数の対数」の積であるスカラーで表現する手法のようですね。
  • 潜在意味解析(LSA): (i,j) 成分が単語 i の文書 j における出現頻度であるような行列を特異値分解して、特異値が大きい方から k 個までに対応する左特異ベクトルと右特異ベクトルだけ残すという操作をして先の行列のランクを  k に落として、i 番目の単語を「k 本の左特異ベクトルの i 番目の要素をつなげたベクトル」、j 番目の文書を「k 本の右特異ベクトルの j 番目の要素をつなげたベクトル」にエンコードするようです。もっとも、そのような数値ベクトル表現を得て、単語や文書をクラスタリングすることが目的のようですが。
  • トピックモデル: 「トピック」という隠れ変数を考えるようですね。各文書はいくつかのトピックの重ね合わせを生成して、各トピックは単語分布を生成すると仮定して、「各文書のトピックの混合比」と「各トピックの単語分布」を推定するようです。なので、各単語について得られる数値表現は「各トピックでどれだけの出現確率をもつか」のようなベクトルになりますね。トピックを軸に各単語や各文書を解析するようです。具体的なアルゴリズムは色々ありうるようですが。Wikipedia にあまり具体的なことが書いていなかったので2つ目のページも参考にしました。LSA において、各右特異ベクトルが確率的に生成された各文書がそのトピックをどれだけ強くもっているかのベクトル、各特異値がトピック、各左特異ベクトルが確率的に生成されたトピック毎の単語分布、になるようにやり直そうとするとトピックモデルになりますね。Wikipedia のアニメーションは、これはトピックを見出す1つの方法といった感じなのですかね。
ただこれらが単語分散表現に比して何を欠いているのかというのは結局単語分散表現の方も追わないとわかりませんね。ただ word2vec と GloVe を流し読みした範囲ですぐわかるのは、上の従来の手法は「この単語とこの単語は文書内で隣り合って出現することが多い」という情報を使っていませんね(LSA やトピックモデルでは「同じ文書に現れやすい」というレベルでは考慮していますが)。…言い換えに近くなってしまいますが、137~138ページの「単語それ自体の意味を捉える」「単語自体を文脈として使用する」とは、「コーパス内での出現頻度以上の単語間の関係を織り込んだ単語空間を構成する」ということと考えます。TF-IDF は単語の数値表現ですが出現頻度に寄りすぎですし、LSAとトピックモデルは特異値やトピックを通した相関は表現しているんですが、結局各文書における出現頻度に基づくクラスタリングのようなものなので、文書の構造などには立ち入れないと思います。

f:id:cookie-box:20180305231302p:plain:w60

ジュンごめん、あのさ、まだ5章の序文なんだけど…。

f:id:cookie-box:20180305232608p:plain:w60

いいじゃないですか、気付きがあったなら全部回収しておけば。それに、5.1節はもういいでしょう。単語間の関係を保った数値ベクトル表現がほしいって話だし。5.2節も word2vec の具体的なアルゴリズムの解説なので、適当なコーパスで実際にやってみればいいですよ。その後も GloVe の紹介と実用場面での分散表現の得方で、1つ目は完全に自前で分散表現を学習する、2つ目は学習済みの重みで初期化した上で学習する(ファインチューニング)、3つ目は学習済みの重みをそのまま利用するやり方ですね。

f:id:cookie-box:20180305231302p:plain:w60

かなり内容あるじゃん! あと、word2vec を試してみるのに使えるデータってある? 本では1つの文章しか出てきてないっぽいし。

f:id:cookie-box:20180305232608p:plain:w60

ハヤトのソロ曲の歌詞とかでいいじゃないですか。

f:id:cookie-box:20180305231302p:plain:w60

なんで!?

(次回があれば)つづく

異常検知と変化検知: 10章メモ(疎構造学習による変化検知)

以下の赤い本の10章を読みます。キャラクターは適当です。誤りがありましたらご指摘いただけますと幸いです。

異常検知と変化検知 (機械学習プロフェッショナルシリーズ)異常検知と変化検知 (機械学習プロフェッショナルシリーズ)
井手 剛 杉山 将

講談社 2015-08-08
売り上げランキング : 56236

Amazonで詳しく見る
by G-Tools
f:id:cookie-box:20180513082851p:plain:w60

先の9章では「『時系列が変化した』とはどのように判定するのか」という決してトリビアルではない問題について、基本的な発想の例として「正常というより異常らしい度合い(対数尤度)のランプ関数の累積和が閾値に達したら変化したと判定する(累積和法)」という解法と、「部分時系列の、予め定めた部分時系列群のうち最近傍標本までの対数距離が大きい場合に異常部位と判定する(近傍法)」という解法を与えました。また、「少し前の状況と今の状況が違うか否か」という形で変化検知問題を抽象的に定式化し、その最も重要な実装例の1つである特異スペクトル変換法を扱いましたね。

f:id:cookie-box:20180513082908p:plain:w60

特異スペクトル変換法は式 (9.8) の過去側の分布/現在側の分布どちらの p にもフォンミーゼス・フィッシャー分布(vMF分布)を仮定したものだけど、vMF分布は長さ1のベクトルの分布であるのに対して、部分時系列はそれ自体は長さ1のベクトルじゃない。部分時系列を並べた行列を特異値分解して左特異ベクトルをとる、という操作をすることで長さ1のベクトルにするんだったね。時系列の変化度とは抽象的には「少し前の状況(分布)と今の状況(分布)のKL情報量」と定義されたけど、特異スペクトル変換法は「少し前の状況」及び「今の状況」を「1本(ないしは数本)の単位ベクトル」で表現するという点で、仮定する分布というよりこの変数変換自体が独特であるように感じるね。でもこの「元データ → 元データを特異値分解して出てきた特異値の大きい数本の左特異ベクトル」という変換がノイズ除去に相当するみたいだね。

f:id:cookie-box:20180513082851p:plain:w60

続く10章は、多変量時系列の異常検知において各変数の寄与度を知るにはどうすればいいのか、という話のようですね…察するに、ブラスバンドのハーモニーが乱れたときに、トランペットが悪いのかクラリネットが悪いのかチューバが悪いのか、各パートの異常への寄与度を知りたいといった感じですね。

f:id:cookie-box:20180513082908p:plain:w60

なんでそんな吹奏楽部がぎすぎすしそうな例を考えたの…。

f:id:cookie-box:20180513082851p:plain:w60

出力値が常に変動するような時系列から異常を検知するには、変数間の依存関係に着目するとよいみたいですね。トランペットとクラリネットはどちらも高音のメロディ楽器なので、両者の振幅の相関は高いだろうとか、相関が乱れたらどちらかが演奏を誤っているだろうとかそんな感じでしょうか。というかトランペットとクラリネットって実際同じようなタイミングで鳴るんでしょうか。

f:id:cookie-box:20180513082908p:plain:w60

知らないよ…。

f:id:cookie-box:20180513082851p:plain:w60

ただ変数間の依存関係に着目するには「直接相関」と「間接相関」の区別が重要ということで…130~131ページにある「都市ごとの教会の数と殺人件数の相関(どちらも都市の人口に比例しているだけ)」のような例って岩波DS3でも見ましたね。

f:id:cookie-box:20180513082908p:plain:w60

小学生の握力と算数ドリルの点数の疑似相関だったっけ。学年が交絡因子で。

f:id:cookie-box:20180513082851p:plain:w60

ああそういうのでしたね。でも、算数ってノートにねばり強く書き続ける力だと思うんですよね。つまり、握力が高まるほど長時間ノートに力強く数式を書き続けられるようになって、算数力も向上するのではないでしょうか。

f:id:cookie-box:20180513082908p:plain:w60

話ややこしくしなくていいよ…。

f:id:cookie-box:20180513082851p:plain:w60

132ページからもしばらく時系列からは離れて(?)、確率ベクトルの対マルコフグラフをデータからどう描けばいいのか、という話が続きますね。それでもうこの本では多変量正規分布を仮定したガウス型グラフィカルモデルのみ取り扱うようですね。132ページの「多変量正規分布というめがねをかけて」という表現がわかるようでわかりませんが置いておきますね。…あれ、データに多変量正規分布を仮定したときの共分散行列の推定値ってこの (2.4) 式でいいんでしたっけ。最近別件でアンサンブルカルマンフィルタを実装したんですけど、そちらではフィルタを実行するときアンサンブル粒子から以下の下側の式のように分母が N-1 の共分散行列を計算して、これを用いてカルマンゲインを計算したと記憶しているんですが…。

 \displaystyle \hat{\Sigma} \equiv \frac{1}{N} \sum_{n=1}^N (x^{(n)} - \hat{\mu})(x^{(n)} - \hat{\mu})^{\top} \tag{2.4}
 \displaystyle \hat{\Sigma} \equiv \frac{1}{N - 1} \sum_{n=1}^N (x^{(n)} - \hat{\mu})(x^{(n)} - \hat{\mu})^{\top}

f:id:cookie-box:20180513082908p:plain:w60

前者が最尤推定量で後者が不偏推定量だね。これらの違いは、雑に言うと以下のような感じかな…。

  • 手元にデータがある。色々な多変量正規分布の中から、手元のデータを生成する確率が最も高いものを選ぶ。これは対数尤度を最大化すればよい(計算略)。
  • 手元にデータがある。このデータがある多変量正規分布から生成されたということを踏まえる=つまり、手元のデータはたまたま手元にあるように生成されただけで、別の世界線では同じ多変量正規分布から生成された別のデータになっていたかもしれない。これを踏まえた上で、色々な多変量正規分布の中から、手元のデータを生成する確率が最も高いものを選ぶ。手元のデータを母集団と見做すならば共分散行列は式 (2.4) の標本共分散行列でいいのだが、「標本平均がたまたま観測された量である」という仮定の下では、この「標本平均(データに対して最も都合がよい場所に仮定した平均)の周りのばらつき」でばらつきを見積もるのはやや過小評価になる(証明略)。
最尤推定量の方は、データが fix された上でそれを多変量正規分布にぼかして考えたらどうなる、って感じで、不偏推定量の方は、データは真の分布からたまたまこのようにサンプリングされただけって感じがするんだよね。
  • 異常検知におけるガウス型グラフィカルモデルの推定では、手元のデータを生成した最も尤もらしい共分散行列を知りたいので最尤推定量を求める。それを通して手元のデータの変数間の相関構造を知りたいだけなので、サンプリングが違ったらどうだっただろう、という考慮はそぐわない。
  • アンサンブルカルマンフィルタでは状態分布を代替的にアンサンブル粒子で表現していて、フィルタのステップではアンサンブル粒子を共分散行列に翻訳したい。このとき、現在のアンサンブル粒子はたまたま今回の計算のために生成しただけなので、現在のアンサンブル粒子を生成した最も尤もらしい共分散行列を知りたいのではない。色々なアンサンブル粒子の選び方があることを踏まえて、不偏推定量を求める。
長々と書いてみたけど N が大きかったら N と N-1 の違いなんて大したことないし、そもそも10章通して N-1 を N に読み換えても話変わらない気がするんだけどね…。

f:id:cookie-box:20180513082851p:plain:w60

お疲れさまです。ところで、133ページを読むと、結局  x_i x_j の間に直接相関がないことと、精度行列の  (i,j) 成分がゼロであることが同値なんですね? ということは、「教会の数」「殺人件数」「人口」のデータの場合、「教会の数」と「殺人件数」の組合せに対応する精度行列の要素がゼロになるということですが、本当にちゃんとそうなるんでしょうか…。疑似相関とはいえ相関はあるんですよね? あとこれって一意に定まらなさそうな気もするんですよね。だって「教会の数」「殺人件数」「人口」のどれがニワトリでタマゴかなんてデータは語らないですよね?

f:id:cookie-box:20180513082908p:plain:w60

今回のケースでは、132ページのグラフみたいに、「人口」軸で輪切りにしたときに「教会の数」と「殺人件数」は無相関だ、というのはわかるよね。「教会の数」軸での輪切りや「殺人件数」軸での輪切りではそうならないんじゃないかな。雑な例だけど実験してみるね。まず、0番目( Python だから便宜上 0 から始めるね)の変数から1 番目の変数と2番目の変数それぞれにグラフの辺があって、1番目の変数と2番目の変数の間にはグラフの辺がない例。

# -*- coding: utf-8 -*- 
import numpy as np

# 標本共分散行列
def calc_cov_mat(z_en):
    z_hat = z_en - np.average(z_en, axis=0)
    v = np.outer(np.zeros(z_en.shape[1]), np.zeros(z_en.shape[1]))
    for i_en in range(z_en.shape[0]):
        v = v + np.outer(z_hat[i_en], z_hat[i_en])
    v = v / z_en.shape[0]
    return v

n_en = 100
x0_en = np.random.randn(n_en, 1)
x1_en = x0_en + 0.01 * np.random.randn(n_en, 1)
x2_en = -1.0 * x0_en + 0.01 * np.random.randn(n_en, 1)
x_en = np.concatenate([x0_en, x1_en, x2_en], axis=1)

Sigma = calc_cov_mat(x_en)
Lambda = np.linalg.inv(Sigma)

print(Sigma / Sigma[0, 0])
print(Lambda / Lambda[0, 0])
[[ 1.          0.99916607 -1.00078985]
 [ 0.99916607  0.99843717 -0.9999501 ]
 [-1.00078985 -0.9999501   1.00167383]]
[[ 1.         -0.4735883   0.52634417]
 [-0.4735883   0.44918519 -0.02475815]
 [ 0.52634417 -0.02475815  0.50121079]]

精度行列 Lambda の (1, 2) 要素=(2, 1) 要素は小さいね。試しに変数の役割を入れ替えて、x2­―x0、x2―x1 にグラフの辺がある場合もやってみるね。

x2_en = np.random.randn(n_en, 1)
x0_en = x2_en + 0.01 * np.random.randn(n_en, 1)
x1_en = -1.0 * x2_en + 0.01 * np.random.randn(n_en, 1)
x_en = np.concatenate([x0_en, x1_en, x2_en], axis=1)

Sigma = calc_cov_mat(x_en)
Lambda = np.linalg.inv(Sigma)

print(Sigma / Sigma[0, 0])
print(Lambda / Lambda[0, 0])
[[ 1.         -0.99804501  0.99911337]
 [-0.99804501  0.99624298 -0.99724056]
 [ 0.99911337 -0.99724056  0.99831405]]
[[ 1.          0.08005913 -0.92082764]
 [ 0.08005913  1.15942445  1.07805449]
 [-0.92082764  1.07805449  1.99854651]]

f:id:cookie-box:20180513082851p:plain:w60

確かに今度は (0, 1) 要素=(1, 0) 要素が小さいですね。偏相関係数をきちんととれるものなんですね。それで、134ページからは、この非対角要素に偏相関係数(に比例する量)をもつ精度行列をなるべく疎にしたいという話になるんですね。精度行列において閾値未満の非対角要素をゼロに切り捨てるというアイデアは駄目で、そもそも共分散行列が正則でなく精度行列が求まらなかったり、精度行列が求まっても閾値に敏感だったり切り捨てた結果の精度行列が正定値でなくなったりして多変量正規分布の精度行列ではなくなってしまうので駄目だとありますね。じゃあなんでそのアイデアを出してみたんですかね。

f:id:cookie-box:20180513082908p:plain:w60

いや、素朴な発想が上手くいかないと断っておくことも大事だからね? 何にせよ、そういう要望なら目的関数に正則化項を入れて多変量正規分布をフィッティングするんだろうね。

f:id:cookie-box:20180513082851p:plain:w60

なぜネタバレするんです…。おっしゃる通り、尤度関数に精度行列の要素の和のラプラス分布を乗じた上でこれを最大化するんですね。対数をとると単純に対数尤度関数から  \rho || \Lambda||_1 をマイナスすることになります( \rhoラプラス分布の尺度の逆数で、 || \Lambda||_1 は精度行列の各要素の絶対値の和ですね)。この項のことをしばしばL1正則化項と呼称すると。しかし、このL1正則化項の導入によって、精度行列を推定するのに単に標本共分散行列の逆行列を取ればよいというわけにはいかなくなってしまいました。どうやって精度行列を求めればいいんでしょう…。

f:id:cookie-box:20180513082908p:plain:w60

精度行列に関する(L1正則化項付き)対数尤度の偏微分をとればいいんじゃない。

f:id:cookie-box:20180513082851p:plain:w60

しかし137ページに  \rho>0 の場合は「偏微分=0」が解析的に解けないとありますね。 \rho<0 だったら解けるんでしょうか…そもそも今回は精度行列を疎にしたいという目的より必ず  \rho>0 なので、 \rho<0 の場合に興味はありませんが…。それで、解析的に解けないのでどうするかというと…  j 次元目以外固定してしまう?  j 次元目を一番後ろに寄せて、精度行列、精度行列の逆行列、標本共分散行列を137ページのようにブロック表記して、そうすると各行列が4つの部分に分解されて、うち2つは転置の関係なので、元の「偏微分=0」は (10.15) (10.16) (10.17) の3式になるわけですね…それで138~139ページの手続きにしたがえば元の精度行列の  j 次元目の行・列以外を固定したもとで目的関数を極大値にできますが…それを全ての次元について、ぐるぐると繰り返せば目的の精度行列が求まると…このように、元データを多変量正規分布のめがねでみてかつ疎な精度行列を得ようとする手法を「グラフィカルラッソ」というんですね。うーん、手順はわかるんですが、これ最適解に収束するんですか? ある次元以外固定してしまおう、というのがなんか心配なんですが…。

f:id:cookie-box:20180513082908p:plain:w60

その辺の議論はこの本にはなさそうだね。参考文献を参照かな。

f:id:cookie-box:20180513082851p:plain:w60

…まあいいです。何らかの参照データから疎な精度行列が求まり、変数間の依存関係がわかったとしましょう。これを利用する異常検知手法が 10.5 節に2例紹介されています。1例目は、ある多変量の観測値が得られたときに、その  j 次元目の要素の異常度は、  j 次元目以外の要素を given とした条件付き確率密度の対数のマイナス1倍とする(外れ値解析)。

f:id:cookie-box:20180513082908p:plain:w60

つまり、時系列の j 次元目だけに着目してよく観測される量だったとしても、j 次元目以外がこの状況のときには観測されない量だ、という場合は異常と判定されるようなイメージだね。

f:id:cookie-box:20180513082851p:plain:w60

2つ目の例は1点の観測値だけでなくまとまった観測値に対する異常判定ですね。このときは、参照データと観測データのそれぞれの分布を求めて、両者間のKL情報量を j 次元目以外について積分したものが異常度ということです。どちらの例も特にグラフィカルラッソや、何なら多変量正規分布でなくてもよさそうですね。ただ相関構造をシンプルにしておかなければ有効な解析ができないということなのですかね。多変量正規分布の場合の (10.28) → (10.29)、(10.30) → (10.31) の式変形は後で追っておきましょう。…143ページに「 \mathcal{D} \mathcal{D'} の立場を入れ替えた定義も可能です」とありますが、確かにKL情報量は非対称ですが、実際過去側の分布と現在側の分布のどちらをどちらにすべきなんでしょう?

f:id:cookie-box:20180513082908p:plain:w60

9章を読んだときにも似た話をしたね。KL情報量は対数密度比の期待値だから、過去側の分布で期待値をとるのか現在側の分布で期待値をとるのかって話だと思うけど…過去側の分布が基準と考えれば (10.30) 式の向きが自然なんだろうけど。異常時に分布がどう変化するかにもよるだろうね。

(他の章のメモがあれば)つづく