2017-12-04 週の日記

最近雑記ばかりで雑すぎるので週次の日記にしようと思います。12/4(月)~12/10(日)の日記にしようと思いますがついでなのでその前の週のことも一緒に書きます。

読んだ記事

GitHub - NVIDIA/sentiment-discovery: Unsupervised Language Modeling at scale for robust sentiment classification
  • テキストを単語列ではなく文字列として学習してセンチメント分類しても上手くいくという話(辞書を作成しないから Unsupervised?)。
  • 単語列ではなく文字列として扱うことで、capitalized/uncaptilized の取り扱いを明示的に与えなくていいし、ミススペルにも対応できるし、未知語にも対応できる。
  • でも英語は26文字しかないけど日本語は平仮名、片仮名、漢字がたくさんあるけど上手くいくのだろうか。
Interpretable Machine Learning
  • 機械学習モデルを解釈可能にするという話。GitHub Pages で作成してあるようなので自分もいつかこういうのを書きたいなあ。
  • model-agnostics: モデルに依存しない(という意味でいいのですよね?)
[1712.02029] AdaBatch: Adaptive Batch Sizes for Training Deep Neural Networks
  • SGDによる深層ニューラルネットの最適化は学習率とバッチサイズを慎重に選ぶ必要がある。バッチサイズが小さい方が少ないエポック数で収束するけどバッチサイズが大きい方がたくさん並列計算できるので計算効率がいい。なのでエポックを経るごとにバッチサイズを大きくしていくことで、小さい固定バッチサイズ並のエポック数で大きい固定バッチサイズ並の計算効率を達成しましたという話(?)。

参加したイベント

第9回 強化学習アーキテクチャ勉強会 - connpass(11/28)
  • この勉強会の中で、紹介された高次行動を抽象化する解法だとベルマン最適方程式の解より高い報酬が得られるのかという質問があって、得られるのではないかというやり取りがあったのですが、それを聞いていて、報酬が高くなるのではなくてよりよい解にたどり着く確率とか収束の速さがよくなるのではないかなと思ったんですが、オンライン学習なら確かによくなるのかなと思いました(オンライン学習の解ってどこなんですけど)。よくわかりません。
  • オプションのサブゴールは所与ということだったのですが、サブゴールもタスクから決める論文もあるとかおっしゃっていたのですが、もっとちゃんと訊けばよかったです。
Gunosyデータマイニング研究会 #132 - connpass(11/29)
  • 分散未知の場合の共役事前分布がなんでウィシャート分布じゃなくて逆ガンマ分布なのかと思ったのですが、共分散行列を  \sigma^2 I に決め打っているからで、でも共分散行列が  \sigma^2 I って何だろう、予測対象変数が各要素が独立の確率値とかだったらそんな状況もあるのだろうかと思いました。
Speee もくもく会 #31 - connpass(12/2)
  • 朝食も昼食もご馳走になってしまいました…なんかすみません…。
朝もくもく会@神田橋 - connpass(12/7)
  • もくもく会を主催してみたのですが誰も来ませんでした。逆に来たらびっくりするんでそれはいいんですけど、1時間というのはどうにも短いと思いました。30分単位で借りられれば30分延ばしたいんですが。もっとアクセスがよさそうな神田駅前にも貸し会議室を見つけましたが、神田駅に寄るのが面倒なので次もここでやります。
mockmock.dev #141 - connpass(12/10)
  • Slack 上のもくもく会に参加させていただきました。実は冒頭50分昼食作って食べてました。すみません…。

その他

来週やりたいこと

  • GitHub Pages をせっかくつくったので、週の日記にはその週に読んだことを、GitHub Pages にはその蓄積を配置したいですが、フォルダ以下に記事群を置いたら PythonPerl で各記事にリンクを張った HTML を生成するようにしたい(そんなことをやっているので文献読みが進みません)。

雑記: GitHub Pages で自分のノート(HTML)をつくる

気になったウェブ上の記事や論文をまとめて自分用に知識を構造化したいですが、紙のノートは後からカットペーストして整理するのに不向きだし、ブログは構造がブログだし、Qiita は他の人に役立つことを意識したものだし記事をずっとインクリメントしていくものでもないと思います。あと自宅からも会社からも閲覧したいのと、Contributions Calendar が面白そうなので GitHub をつかってみたいと思います。

GitHub 上に自分のノートをつくる
  • 手元の Win 機に Git がインストールされていなかったのでまずインストールする。
  • GitHubリポジトリをつくる。
    • https://github.com/ にアカウント名とメールアドレスとパスワードを入れる。
    • メールがくるのでリンクをクリックしてメールアドレスを Verify する。
    • 遷移した画面の右上の + マークから New Repository をクリックする。
    • リポジトリ名だけ入れる。最初なので test にしました。無料版だと非公開にはできない。
  • ローカルリポジトリをつくって GitHub リポジトリと関連付ける。
    • 最初は空コミットでいいや。 空コミットは何故か失敗したので、test.md というファイルを add して commit した。いきなりコミットすると Git に「あなたはどこの誰?」と訊かれるので、画面の指示通りにメールアドレスとアカウント名を教えてあげる(でも push 時に確認ダイアログが出てまた訊かれた)。
      git init
      git add test.md
      git status -suno
      git config --global user.email xxxxx@xxx.com
      git config --global user.name CookieBox26
      git commit -m "first commit"
      git log
      git remote add origin https://github.com/CookieBox26/test.git
      git push -u origin master
      test.md
      # ほげ

      ## ふが

      ここに本文を書く。
    • そうすると test.md が GitHub の自分のページからでHTML化されて見えます。これにどんどん自分のノートを書いていけばよさそうな気がします。
      https://github.com/CookieBox26/test/blob/master/test.md
GitHub Pages をつかってみる
  • これで作業の土台はできましたが、push するまでマークダウンがプレビューできないのは不便です。Markdown Previewer で検索すると以下のサイトがあり、マークダウン記法はこれで確認できそうです。
    Markdown Editor
  • でもツールでマークアップさせながらの作業は面倒です。個人的には HTML タグ打ちも苦にならないというかタグ打ちの方が思い通りの表現ができるので、HTML ファイル(できればその階層構造も)を直接コミットしてそれをウェブ上から閲覧できないだろうかと思います。→ 試しに適当な test.html を作成してコミットしてみましたが、これだけではテキスト表示されるのみでした。
  • どうしようかと思っていると以下のページを見つけます。
    GitHub Pages を使った静的サイトの公開方法が、とても簡単になっていた | Tips Note by TAM
    このページにしたがってリポジトリを静的サイト化します。上の記事で設定する Setting は画面の一番右上の + メニューの Setting ではなくて、特定のリポジトリを表示したときの一番右側のタブです。ここで master branch を選択して Save を押すと、ページがここに公開されましたよという URL が表示されると思います。
    https://CookieBox26.github.io/test
    • このとき、先ほどコミットした test.html のファイル名を index.html に rename しました。
    • 余計なファイルがあると URL が表示される代わりに「余計なファイルがある」というエラーが表示されます。今回は test.md を削除しました。
    • また、HTML ファイルのエンコードUTF-8 にする必要があります。SJIS で作成したために日本語が文字化けしていたのでこれも直しました(コミットからサイトへの反映に少々時間がかかるようなので、反映されない場合はまず少し待ってみてください)。
これで安心してノートを蓄積できるね

蓄積しないと意味ないね。

雑記

Qiita に投稿しましたが、後半文字だらけで見づらいのと、肝心なHTMLソース部分の説明がないので後日絵や解説を入れるなどリファクタリングできたらいいなあと思います。qiita.com
後半文字だらけで見づらいですが、この分野を知らない人にもわかるようにどのような仮定を置いているかをきちんと書こうとしています。それで肝心の二項ツリーの説明までたどり着けていないんですが、二項ツリーで置いている仮定は既に説明している「無裁定条件」「無リスク金利で貸し借りができる相手の存在」に加えて、

  • 時間  \Delta t だけ先に株価は今より  u 倍に上がるか  d 倍に下がるかの2択とする(から二項ツリーになるのですが)。
  •  ud =1 とする。
  •  \Delta t の1次以上の項は無視できるとする。

辺りになると思います。そうなると株価のボラティリティ  \sigma と矛盾しないように  u d が決まります。このとき二項ツリーによるオプション価格の求め方は以下の手順になります。

  • 満期日までの時間をじゅうぶん細かい  \Delta t ずつに区切る。
  • ツリーの根元のノードに現在の株価  S_0 を記入し、 u 倍や  d 倍しながら末端のノードまで株価を埋める。
  • 次に、株価の下にオプション価格を書いていく。ツリーの末端のノードについては容易に埋まる(例えばコール・オプションであれば行使価格  K として  \max (S_T-K, 0) を書き入れるだけ)。
  • 末端ではないノードのオプション価格については以下の手順で書き入れる。
    • 2つの子ノードを見て「どちらの未来になろうとも同じ価格になる、株とオプションからなるポートフォリオ」を構成する。
    • そのポートフォリオは無リスクなので、親ノードでのそのポートフォリオの価格は無裁定条件より決定する。
    • 親ノードの株価は既に埋まっているので、親ノードでのオプション価格を得る。

この要領で一番根元まで戻ってこれれば現在時点でのオプション価格が求まります。気分転換で数理ファイナンスの記事を書いてみましたが、次以降は統計や機械学習の記事を書きたいです。


それで、どのような仮定を置いているかをきちんとしたいと書いたのですが、少し前に twitter で話題になっていた(少し前というより定期的に話題になっているようですが)以下の確率の問題は仮定が足りない例です(文章は意訳です)。
Aさんには子どもが2人います。Aさんに「子どもたちに女の子はいますか?」と訊いたところAさんは「はい」と答えました。もう1人が女の子である確率はいくらでしょう。
「情報が足りない」「解釈による」といえばそれまでですが、どのように仮定を補うとどのように解けるのかを考えてみるのも面白いと思います。以下の1番下の図は答えが 1/3 となる考え方の1つの例です。ここで置いている仮定は、

  • Aさんに質問する前の子どもの性別の組合せの事前分布を P(男男)=P(男女)=P(女男)=P(女女)=0.25 とする。
  • 「もう1人」とは、女の子が少なくとも1人いると判明した後に、女の子を1人選び、残ったもう1人のこととする。

の2つになります。この問題でよく「こう解釈できるのでは?」争点になっているのは後者ですが、自分がどう解釈したかによって「このように仮定する」とすれば大丈夫です。それより前者について、「男女が生まれる確率が同じとする」のように表現しようとする回答が見受けられますが、その表現は以下の可能性を否定しないので同値ではないです。「独立な」とまでいうと1つ目の可能性のみ排除できますが、2~4つ目の可能性は残るのでやはり不十分です。

  • 第1子と第2子の性別に相関がある可能性。
  • 子どもが男でも女でもない性別である可能性。
  • 子どもの性別が出生後に変化する可能性。
  • Aさんに質問する前からAさんの子どもの性別の情報を(部分的に)知っている可能性。

この問題を「1/2 と 1/3 の2通りの解釈がある」と結論付ける回答もあるのですが、2通りどころか、仮定によって 0~1 のどの値にもなります。それどころか、Aさんの2人の子どもの性別の組合せの確率分布について事前に本当に全く何もわからないという立場を取るならば、1人が女の子だとわかったところでもう1人については「やはり何もわからない」という回答もあると思います。だから何なのかというと、「男女の出生率は等しく第n子にかかわらず独立」というのは、数理ファイナンスにおける「裁定機会は存在しない」「無リスク金利で貸し借りができる」などと似ていて、それなりには現実に近く、いつの間にかそれが当然のように受け入れがちな仮定なのですが(?)、厳密には成り立たないので、いつ仮定が利用されたかを把握していないと危ういんじゃないかな(まあ男女の出生率やその相関を厳密に考える場面などたぶん訪れないんですけど)という回りくどい話でした。

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

Keras で特定の軸方向に softmax したい

Keras で特定の軸方向に softmax したいときとかあると思います。

f:id:cookie-box:20170815221557p:plain:w720

Keras のドキュメントによるとどの軸に沿って正規化するかを指定する axis というのを渡せるようです。
Activations - Keras Documentation
ただ、渡し方がよくわかりません。以下の箇所に axis=2 と書いてもエラーになります。

from keras.models import Sequential
from keras.layers import Dense, Reshape, Activation

if __name__ == '__main__':
  model = Sequential()
  model.add(Dense(3*4*5, activation='relu', input_shape=(10,)))
  model.add(Reshape((3, 4, 5)))
  model.add(Activation('softmax', axis=2)) # ERROR
  model.compile(loss='mean_squared_error', optimizer='rmsprop')
  print(model.summary())

しょうがないので softmax をするだけの自作レイヤーをつくります。これはエラーになりません。

from keras.engine.topology import Layer
from keras.models import Sequential
from keras.layers import Dense, Reshape
from keras.activations import softmax

# いまやりたい softmax をするためだけのレイヤー
class MySoftmax(Layer):
  def __init__(self, **kwargs):
    super(MySoftmax, self).__init__(**kwargs)
  def call(self, x):
    return(softmax(x, axis=2))

if __name__ == '__main__':
  model = Sequential()
  model.add(Dense(3*4*5, activation='relu', input_shape=(10,)))
  model.add(Reshape((3, 4, 5)))
  model.add(MySoftmax())
  model.compile(loss='mean_squared_error', optimizer='rmsprop')
  print(model.summary())

本当に希望の軸方向に正規化されているか確認してみます。何でもよいので適当な入力を流し込めばよいです。

  x, y = [], []
  # 1だらけの適当なデータ
  for i in range(20):
    x.append(numpy.ones((10)))
    y.append(numpy.ones((3,4,5)))
  x = numpy.array(x)
  y = numpy.array(y)
  
  model.fit(x, y)
  y_test = model.predict(x)
  print(y_test[0])
[[[ 0.21292862  0.24584062  0.2997719   0.15851443  0.25      ]
  [ 0.30612475  0.24584062  0.22358862  0.32254457  0.25      ]
  [ 0.16930516  0.24584062  0.22358862  0.25293246  0.25      ]
  [ 0.31164148  0.26247811  0.25305092  0.26600859  0.25      ]]

 [[ 0.18996513  0.24999966  0.18364088  0.25715023  0.3020851 ]
  [ 0.23858465  0.25000107  0.27690381  0.21845251  0.27901751]
  [ 0.25863758  0.24999966  0.35581443  0.30594474  0.19078693]
  [ 0.31281266  0.24999966  0.18364088  0.21845251  0.22811046]]

 [[ 0.21095483  0.36942184  0.22160934  0.2253582   0.24394487]
  [ 0.26182058  0.22892946  0.22160934  0.32392535  0.24394487]
  [ 0.31626979  0.159567    0.335172    0.2253582   0.24394487]
  [ 0.21095483  0.24208161  0.22160934  0.2253582   0.26816541]]]

出力の shape が (3, 4, 5) で、axis=2 としたので、5つの要素の和を取る方向がこれで正規化されて…いません。
和が1になったのは4つの要素の和を取る方向でした。
axis=1, 2, 3 についてどの軸の方向が正規化されるのか調べると以下でした。5つの要素の和を取る方向を正規化したかったら axis=3 とすべきようです。

f:id:cookie-box:20170815225335p:plain:w750

axis=0, -1 のときは以下でした。axis=0 は 4x5 の面内の和が1になりました。axis=-1 はむしろ当初やりたかった softmax が達成されていました。-1 はデフォルトのはずで、じゃあわざわざカスタムレイヤつくる必要なかったのでしょうか。
f:id:cookie-box:20170815230442p:plain:w500

これでやりたい softmax ができたので、今度はこの softmax に合わせた cross entropy を用意しなくては、と思っているのですがもしかしてそれもわざわざやらなくてよいのでしょうか。

Keras で顔写真から年齢が学習できるのか

深層学習で回帰をしてみたいときとかあると思います。そこで、顔写真からその人の年齢を回帰してみます。ただ、何のデータをつかえばいいかわからなかったので、Berryz工房のジャケ写からキャプチャした画像をつかうことにします。結論を先にいうとこの記事の学習は全然駄目です。全然駄目ですがデータが決定的に足りないのとプレ処理が雑なのでその辺を改善すれば何とかなるのかもしれません。何とかならないかもしれません。

データの準備

データはWebの画像検索から調達します。ベリメンの年齢は4学年にわたるので、だいたい3, 4年置きのシングルのジャケ写から顔写真をキャプチャすればだいたいの年齢がカバーできると思います。なので、適当に「あなたなしでは生きてゆけない」「告白の噴水広場」「本気ボンバー!!」「ロマンスを語って」を訓練データにチョイスします。テストデータは「ライバル」にします。適当です。テストデータ数をちょうど30にしたかったので、「ライバル」の梨沙子のデータだけ訓練データに含めます。なお、キャプチャはだいたい正方形になるように手動で切り取っています。訓練時に 128x128 にリサイズします。

訓練

面倒なので以下の記事のネットワーク構造を使いまわします。変更点は、今回は年齢そのものの出力を目指すので最終層を活性化しないというところです。損失関数と最適化アルゴリズムも適当に変更します。
 Keras で少ない画像データから学習したい - クッキーの日記

結果

何かを学習したような雰囲気はありつつも概して駄目でした。データが足りません。
全体的に幼い方に誤っているので最終層のバイアスを大きくするだけでもっと最小2乗誤差が小さくなりそうです(そうなっていないのは学習を早々に止めすぎました)。

f:id:cookie-box:20170814235401p:plain:w560

スクリプト

画像は最初に全て読み込まず、ミニバッチ学習のたびにミニバッチの分だけ読み込むという方法にしています(というか今回の目的がそのような処理の練習でした)。keras.preprocessing.image.ImageDataGenerator の flow_from_directory や flow を活用したかったのですが上手くできなかったので愚直な書き方になりました(flow_from_directory はクラス分類学習の際に cat や dog などというサブフォルダを切って猫画像データや犬画像データを配置しておけば勝手にミニバッチ毎の読み込みを正解データ生成含めてやってくれますが、今回はクラス分類ではなく回帰であり、個々の画像に固有の数値の正解データを生成したいため利用できませんでした)。

from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Dropout, Flatten, Dense
from keras import backend as K
import os
import numpy
import cv2
import pandas

# 顔画像ファイル名から年齢を抽出
def filename2age(filename):
  tempstr = filename.replace('.PNG', '')
  tempstrs = tempstr.split('_')
  return(int(tempstrs[2]))

# ファイル名のリストからデータをロード
def load_data(dirname, filenames, img_width, img_height):
  x = []
  y = []
  for filename in filenames:
    image = cv2.imread(dirname + filename)
    image = cv2.resize(image, (img_width, img_height))
    age = filename2age(filename)
    x.append(image)
    y.append(age)
  x = numpy.array(x)
  x = x.astype('float32') / 255.
  y = numpy.array(y)
  return(x, y)

# メイン処理
if __name__ == '__main__':
  img_width, img_height = 128, 128
  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(64, activation='relu'))
  model.add(Dense(1))
  model.compile(loss='mean_squared_error', optimizer='rmsprop')
  
  # 学習
  filenames = numpy.array(os.listdir('data/')) # 30 files
  batch_size = 10
  n_batch_loop = 3 # 30 / 10
  epochs = 50
  for e in range(epochs):
    print('Epoch', e)
    indices = numpy.repeat(range(n_batch_loop), batch_size)
    numpy.random.shuffle(indices)
    print(indices)
    for i_batch_loop in range(n_batch_loop):
      filenames_temp = filenames[indices == i_batch_loop]
      x, y = load_data('data/', filenames_temp, img_width, img_height)
      model.fit(x, y, epochs=1, batch_size=batch_size)
  
  # 学習結果の確認 (訓練データ)
  x, y = load_data('data/', filenames, img_width, img_height)
  result = pandas.DataFrame(filenames, columns = ['filename'])
  result['actual'] = y
  result = pandas.concat([result,
    pandas.DataFrame(model.predict(x), columns = ['predict'])], axis=1)
  result.to_csv('train.csv', index=False, encoding='utf-8')
  
  # 学習結果の確認 (テストデータ)
  filenames = numpy.array(os.listdir('test_data/'))
  x, y = load_data('test_data/', filenames, img_width, img_height)
  result = pandas.DataFrame(filenames, columns = ['filename'])
  result['actual'] = y
  result = pandas.concat([result,
    pandas.DataFrame(model.predict(x), columns = ['predict'])], axis=1)
  result.to_csv('test.csv', index=False, encoding='utf-8')