雑記: 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化されて見えます。これにどんどん自分のノートを書いていけばよさそうな気がします。
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')

Keras で少ない画像データから学習したい

深層学習で画像分類をしてみたいときとかあると思います。でも画像を用意するのが面倒です。すると Keras ブログに「少ないデータから強力な画像分類」とあります。GitHubスクリプトもあります。これを使ってみます。
Building powerful image classification models using very little data
fchollet/classifier_from_little_data_script_1.py - GitHub

データの準備

Google 検索で出てきた順にカレーとラーメンの画像を保存しました。でも時間がなかったので訓練用とテスト用に各クラス10枚ずつしか保存しませんでした。Keras ブログに書いてあるように、「少ないデータ」とは少ないといえども "just a few hundred or thousand pictures" ということなので、真面目にやる場合はきちんと用意してください。これらの画像は Keras ブログの指示通り、data/train/curry/ のようにフォルダを切った下に置きます。

訓練

このカレーとラーメンの画像分類を、畳み込み層とプーリング層を積み上げたニューラルネットワークで訓練するわけですが、少ないデータを最大限に活用して学ぶために、画像を常にランダムに少しの回転や平行移動、シア変形(平行四辺形のように歪める変形)、拡大縮小、左右反転などをさせながらモデルに入力します(どれだけの変形まで許容するかは自分で指定します)。こうすることによって過学習が抑制されモデルの汎化性能が上がるそうです。

結果

訓練データが20枚というやる気のなさでもテストデータの9割を正解してくれました(下表)。何回か訓練を試行するとテスト正解率が100%になることも多かったです。

f:id:cookie-box:20170810000634p:plain:w480
なお「カレー」と誤判断されたラーメンは以下のサイトの「天日地鶏」というお店のもので、ニューラルネットはこのチャーシューがカレールーに、麺とネギがライスに見えたのかもしれません(?)。
キングオブ静岡ラーメン~人気ランキングTOP10|静岡新聞SBS-アットエス
「ラーメン」と誤判断されたカレーは以下でした。何がラーメン要素だったのかよくわかりません。
​新潟発。衝撃200円カレーが東京に初進出 - エキサイトニュース(1/2)
逆に以下のあまりカレーに見えない MOKUBAZA のチーズキーマカレーはカレーに判定されました。ラーメンにも見えませんが。
【保存版】2015年のカレーまとめ / もっとも印象に残ったお店20選 | ロケットニュース24

スクリプト

ほぼ GitHubスクリプトの通りですが、サンプル数を合わせてあるのと、最後に個別データの確率を csv 出力する部分を追加してあります。

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 pandas

if __name__ == '__main__':
  img_width, img_height = 150, 150 # 訓練時の画像サイズ
  
  train_data_dir = 'data/train'
  validation_data_dir = 'data/validation'
  nb_train_samples = 20
  nb_validation_samples = 20
  epochs = 50
  batch_size = 5
  
  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(Dropout(0.5))
  model.add(Dense(1, activation='sigmoid'))
  
  model.compile(loss='binary_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='binary')
  # テストデータ作成をディレクトリ内のデータから随時作成するジェネレータ
  validation_generator = test_datagen.flow_from_directory(validation_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='binary')
  
  # 訓練
  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)
  # 訓練済重みの保存
  model.save_weights('weight.h5')
  
  # 具体的に個々のデータに対してどのような確率分布になったかの出力
  train_generator = test_datagen.flow_from_directory(train_data_dir,
    target_size=(img_width, img_height), batch_size=1, class_mode='binary', shuffle=False)
  validation_generator = test_datagen.flow_from_directory(validation_data_dir,
    target_size=(img_width, img_height), batch_size=1, class_mode='binary', shuffle=False)
  
  predict = model.predict_generator(train_generator, steps=nb_train_samples)
  prob = pandas.DataFrame(predict, columns = ['curry'])
  prob['ramen'] = 1.0 - prob['curry']
  prob['predict'] = prob.idxmax(axis=1)
  prob.to_csv('train.csv', index=False, encoding='utf-8')
  
  predict = model.predict_generator(validation_generator, steps=nb_validation_samples)
  prob = pandas.DataFrame(predict, columns = ['curry'])
  prob['ramen'] = 1.0 - prob['curry']
  prob['predict'] = prob.idxmax(axis=1)
  prob.to_csv('test.csv', index=False, encoding='utf-8')