エンジニアMのAI学習記録(2017年10月分)

最終更新:2019年01月11日 公開:2017年10月06日
こちらは、Webに関連するエンジニア向けの記事です。
当社のWeb関連技術の公開と採用活動のために掲載しています。

(2017年11月02日更新)

みなさんこんにちは、エンジニアのMです。
これは当社の技術やAIに興味のある方に向けた日誌になります。
今月も毎週更新していきます、よろしくお願いします。

10月2日

■ 読書
「ビジネスで使える超統計学」(ISBN 978-4-7980-4153-7)を読む。Gensimと格闘する前の頭の体操として統計の復習をする。
エクセルの解説部分は現状必要ないのでカット。例がわかりやすく、具体的な計算をバッサリと省いてエクセルに特化しているところが入門書として良いと思った。数字の意味合いさえわかっていれば、計算は丸投げでも扱えるという発想は時間のないビジネスマンにピッタリだ。

■ Gensimと格闘する
以前はGensimを使ってコードを書いてみた人の記事を日本語で探していたが、今日からは英語にも範囲を広げて情報を集める。

Gensimについてひとつ例を挙げると、

from gensim import models
(略)
model = models.Doc2Vec(documents, dm=1, size=300, window=5, alpha=.025,\
min_alpha=.0001, min_count=5, sample=1e-6, iter=600)

のようにdocumentsを渡してmodelを初期化すると、この時点で渡したパラメータに従って学習済みモデルが構築される。trainメソッドを呼び出す必要はない。

これは、

if documents is not None:
self.build_vocab(documents, trim_rule=trim_rule)
self.train(documents, total_examples=self.corpus_count, epochs=self.iter)

と、documentsが渡された時はbuild_vocabとtrainを呼び出すようにクラスDoc2Vec内に記述されているためである。

そういうことなので、

from gensim import models
(略)
model=models.Doc2Vec(dm=1, size=300, window=5, alpha=.025,min_alpha=.0001,\
min_count=5, sample=1e-6, iter=600)
model.build_vocab(documents) # trim_ruleは省略可
model.train(documents, total_examples=model.corpus_count, epochs=model.iter)

と分けて記述しても同じ実行結果になる。

■ GNU LGPLについて調べる
Gensimの調査にあまり進展が無いので、GNU LGPLについて調べることにする。
GensimはGNU LGPLに従うということなので、商用利用することになった場合
を想定してライセンスの詳細についてしっかり知る必要があると考えた。

■ GNU LGPLとは?
コピーレフトライセンスについて考察」と、「Wikipedia : GNU Lesser General Public License」を参考にした。

(Wikipediaより)
社内など私的組織内部や個人で(private)利用するにあたってのソースコード改変、再コンパイルには制限がない。

ということなので、例えば「WEBサービスに組み込みたいから適宜改変して利用します」という行為は問題ない。ライブラリを組み込んで頒布するわけでなく、自分で利用する分には自由という解釈で良さそう。
もちろん、大規模に改修した結果有益なものができたと判断すれば、GPLに基づいて頒布するということも視野に入ると思われる。

しばらくはオープンソースソフトウェア(OSS)を一方的に活用する側の人ではあると思うが、いずれは提供する側の人になれればと思う。

■ Gensim続き(オンライン学習)
build_vocabメソッドについてコードを調べてみると、内部でscan_vocab→scale_vocab→finalize_vocabを呼び出していることが分かった。
これは機能ごとに分解しているだけのようなので、基本的にbuild_vocabを使うだけで良いか。

ライブラリのソースコードからオンライン学習の仕方を見つけることはできなかったので、ここで自力での読解を断念。
ウェブで検索すると、すぐにオンライン学習のヒントになりそうなコードを発見。
参考:Online learning for Doc2Vec
11ヶ月前に作成されたページだが、エラー出力に従って手直しすることで動作。
build_vocabメソッドの引数として、update = Trueを設定することで単語辞書を更新することができるようだ。
Tagsの出力を見る限り、正常に追加が行われている様子。
検証として、自前のコードに追加して動作することを確認する。

■ 自前のコードとテキストデータに対してオンライン学習を試す
Livedoorニュースコーパスを適当に2分割して、モデルを更新できるかどうか検証する。
学習させるところまでは問題なかったが、何らかの更新をし忘れていたか。類似度解析をさせようとしたら、indexの範囲を超えてしまった。

mean.append(weight * self.doctag_syn0norm[self._int_index(doc)])
IndexError: index 7269 is out of bounds for axis 0 with size 3990

原因を探ってみたが、そもそもdoctag_syn0normについてよく分からなかった。明日はこの点について調べようと思う。

10月3日

■ Gensim続き
タグの更新はされていたが、類似度解析させようとしたらエラーを吐いたのが昨日の話。doctag_syn0normがどこで定義されて、どこで更新されているのかを探したいと思う。

一つ一つ追っていく。
まずmost_similarメソッドの中でinit_simが呼び出されてdoctag_syn0normが更新されている。
コードを見ていくと、init_simメソッドの中で、doctag_syn0normのshape(行列の形状)を定義するのに、doctag_syn0を使っている。
そしてdoctag_syn0を更新しているのはreset_weightsメソッドだということが分かったので、

(sentencesの取得や各種定義の部分は省略)

model=models.Doc2Vec.load('doc2vec_div.model')
model.build_vocab(sentences, update=True)
model.docvecs.reset_weights(model)
model.train(sentences, total_examples=model.corpus_count, start_alpha=.025,\
end_alpha=.0001, epochs=20)
model.save('doc2vec_div.model')

とすることで、オンライン学習に利用したテキストデータも既知のドキュメントとして扱い類似度解析させることが可能になった。

これの応用で、

  • 初期モデル用データ(※類似度解析の対象にしたくないデータ)を使って、学習させる
  • 解析対象のデータ(※類似度解析の対象にしたいデータ)を使ってモデルを更新
  • 解析対象のデータに対して(つまり clip_start = [初期モデル用データ個数] として)most_similarメソッドを呼ぶ

という流れで解析対象を指定して扱うことができる。
また初期モデルと追加の学習とで学習率を調整してやれば、学習の比重も変えられるはず。データを分けて扱うことで、モデルの使い回しができるようになることは計算効率上大きい。

しばらくDocvecsArrayなどについて理解できればと思いコードを眺めていたが、
特に収穫はなかったので先程の処理で良さそうだという結論に至った。
Gensimについての調べるのはここまでで一区切りとする。

■ 脆弱性の例について調べてみる
SQLインジェクション、クロスサイトスクリプティングなど聞いたことはあっても、詳しく知らない脆弱性に関わる用語について調べることにした。
Wikipediaで気になる用語を探して、適宜ウェブで検索してみる。

10月4日

■ ベクター画像について
ベクター画像について調べていると、svgというxmlで記述される画像形式を発見。Python用のライブラリも存在するらしいので、これまでに調べてきた種々のライブラリと併用ができそう。
そもそもなぜベクター画像を調べようと思ったかというと、機械学習の入力データ次元数を削減して画像分析を高速化できるのではないかと考えたためである。
これは後で検討してみたいと思う。

■ 前日に続いて脆弱性について調べる
引き続き脆弱性について調べる。基礎知識として用語と概要を知っておけば、後々どういうことに気をつけなければならないか見当がつくと考えられる。

■ Pythonで画像を扱うライブラリを探す
OpenCV、Pillow、Scikit-imageなどすぐにいくつか見つかった。画像処理、画像分析のできるライブラリが揃っているようなので、導入すればすぐにでも画像を扱えるPythonコードを書くことができそうだ。

■ Gensimでオンライン学習&類似度解析
Livedoorニュースコーパスを対象にベースとなる学習モデルを作成し、こちらで手入力したテキストデータを対象に類似度解析させてみる。
追加のテキストに対する学習率は高めに設定する。

近い意味の文章を用意したはずだが、短文であったせいか期待した結果は得られなかった。
そもそもニュースコーパスに対する類似度解析でも、類似度が高くて0.2程度で内容の類似性が認められないことが多い。記事ごとに内容が違いすぎてうまく学習できていないのか、それとも単にデータ量が不足しているからなのか。

調べていたら、JUMAN++はかなり処理に時間がかかるらしいという情報を偶然得たので、形態素解析済みのデータを保存、読み込みする方策について考えたほうが良さそうだ。
いい機会なので、雑多に散らかったコードの整理も行う。

10月5日

■ 類似度解析を試行錯誤するための下地を作る
昨日はコードをどう整理するかを考えるところで終わったので、実装を進めたいと思う。
とはいっても、元がほぼサンプルコードそのままだったものに多少機能を追加するだけ。サクッと終わらせたい。

■ そもそもなぜ短文に対して精度が低いのか
理由はおそらく正規化しているからだ。数十単語で構成される文章中の一単語と、数単語だけで構成される文章中の一単語とでは影響の大きさが全く違う。
単純なコサイン類似度だけの評価では短文を処理できない。この問題に対するアイデアについても考えを巡らせながら、コーディングを進めていこうと思う。

■ バグ?
途中経過を出力させていると、どうも怪しい部分が多い。ほとんどサンプルコードから借りているので、一度ちゃんと検証したほうが良さそうだ。
pickle化する直前のテキストデータがおかしいので、pickle化したデータも正確ではない気がする。考えるほどに沼にはまりそうな気がしてきたので、一回別のことについて作業を進めたいと思う。

■ OpenCVで画像処理を試す
OpenCVを使って画像処理をさせてみることにした。
yumでpython-opencvをインストールし、GUI環境を立ち上げてチュートリアルを一通り触れてみる。
画像処理入門講座 : OpenCVとPythonで始める画像処理
OpenCVはなかなかに高機能そうなので、いろいろと応用が利くのではないか。

画像処理に機械学習を持ち込むのは人間の手に負えないときのほうがよい。
ある程度処理の内容や手順が思い浮かぶうちは、こういう画像処理ライブラリが便利だと考えられる。

10月6日

■ AngularJSの本を読む
今日はOpenCVを触る予定だったが、昨日Angularの話になって終わったこともあり、少しAngularについて勉強してみることにした。

  • AngularJS アプリケーションプログラミング(ISBN 978-4-7741-7568-3)

バージョンが古い内容なので、仕組みやどういったことができるのか、を中心に学習する。
HTMLやJavascriptに馴染みが薄いので時間はかかったが、基本編をざっくりと読んで終わり。ここ最近はPythonのコードしか読んでいなかったが、Pythonの読みやすさを再確認する時間だった。
HTMLはタグが多くて見づらい…。

■ OpenCVとTesseractを組み合わせてみる
Tesseractで文字を処理した後、OpenCVで画像を処理させてみたいと思う。
Tesseractには文字と認識したブロックの座標を取得する機能があるので、その座標に基づいて文字を除去すれば、文字の部分とそれ以外を別々に処理することができるはず。

■ 結果
pyocrではTesseractのオプションを与えてやれないので、日本語のbox(文字の存在するエリア)を取得する精度すら低いのが痛い。実用には程遠いか。
pyocr(Tesseract)+OpenCV+Pillow+Matplotlibというライブラリの組み合わせで解析させることができた、ということを今回の成果とする。

■ 今週の総括
Gensimを始めとするライブラリの実験に費やした週だった。コードの読み書きが多かった分、Pythonにだいぶ慣れることができたように感じる。
次週はまたGensimをなんとかしたいと思う。

10月10日

■ Vagrantの修復
前々からたまに見かけていたエラーが直らなくなったので、修復するところから。

[default] GuestAdditions 5.1.26 running --- OK.
==> default: Checking for guest additions in VM...
==> default: Configuring and enabling network interfaces...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
==> default: Rsyncing folder: /cygdrive/d/Vagrant/centos7/ => /vagrant
C:/HashiCorp/Vagrant/embedded/gems/gems/vagrant-1.9.8/lib/vagrant/util/io.rb:32:in `encode': "\xA0" on Windows-31J (Encoding::InvalidByteSequenceError)

GuestAdditionsがうまく動いていないようだ。実際にCentOSから確認すると、共有フォルダが機能していないことがわかった。

いろいろ検索してたどり着いたこの記事の変更を適用して無事修復。
そもそもこのエラーを吐いたり吐かなかったりしていたのが不思議でならない。

■ Tesseract 4.00.00devを動かすための作業
ホームディレクトリで依存するソフトのインストールから一通り作業したのだが、どうにもうまくいかず。課題はまたしても持ち越しとなった。
ほぼ丸一日触っていたおかげでCUIの操作は早くなったが、できれば今日中に解決したかったのが正直なところ。

10月11日

■ Java入門
Javaを使うことはしばらく無いと思うが、教養として触っておく。最近は知識を掘り下げる作業が多かったので、薄くなぞるような勉強も頭の体操には良い。

  • 「本格学習Java入門」(ISBN 4-7741-2002-2)

構文はほとんどCなので、リファレンスを読みながらであればすぐにコーディングを始められそうだ。オブジェクト指向もPythonで触れてある。

■ インストール作業続き
必要とされているソフトを最新版に更新してみる。
エラー内容から判断して怪しいのは、

  • autoconf (→2.69)
  • automake (→1.15.1)
  • libtool (→2.4.6)
  • autoconf-archive (→2017.09.28)

以上4点。pkg-configは更新済み(0.29)。
それぞれ相互に関連しているツールなので、全部カッコ内のバージョンに更新する。
その他必要と言われたものもyum経由、あるいはソースからビルドして追加でインストールした。

  • libattr-devel
  • attr
  • help2man

これで準備ができたはずなので、

  • Leptonica (1.74.4)
  • Tesseract (4.00.00dev)

のインストールに取り掛かる。
しかしLeptonicaが見つからないとTesseractを入れようとしたときに怒られる。

ここで、Leptonicaに忘れていたmake checkをかけたら9割Failしていることが分かった。
確認してみると、

  • libpng12-devel
  • libjpeg-turbo
  • libtiff-devel

が入っていなかったり、バージョンが古かったりした。インストールを済ませることでLeptonicaに対するmake checkを1つを除いてパスできた。
ソースからLeptonicaを入れた場合はleptonica-devも不要のはずだが、Tesseractがleptonica-devを入れるよう指示してくる。
何かしら見落としがある可能性があったので、
GitHub:Compilation guide for various platforms」と、「Leptonica:README」を熟読した。

どうやら、

(GitHub)
Note that if building Leptonica from source, you may need to ensure that /usr/local/lib is in your library path. This is a standard Linux bug, and the information at Stackoverflow is very helpful.

(README)
Configure supports installing in a local directory (e.g., one that doesn’t require root access). For example, to install in $HOME/local,

   ./configure --prefix=$HOME/local/
      make install

For different ways to build and link leptonica with tesseract, see
https://github.com/tesseract-ocr/tesseract/wiki/Compiling
In brief, using autotools to build tesseract and then install it
in $HOME/local (after installing leptonica there), do the
following from your tesseract root source directory:

       ./autogen.sh
       LIBLEPT_HEADERSDIR=$HOME/local/include ./configure \
          --prefix=$HOME/local/ --with-extra-libraries=$HOME/local/lib
       make install

このあたりを見落としていたのがまずかったらしい。

export LD_LIBRARY_PATH=/usr/local/lib/

と/etc/profileに記述して、他にも色々更新しているので一旦ゲストOSを再起動。
Tesseractのconfigureに対して、

 LIBLEPT_HEADERSDIR=/usr/local/include ./configure

と指定することによってconfigureが通った。
どうやらヘッダーの場所を教える必要があったらしい。
leptonica-devを入れる方法の場合はこのあたりを自動でやってくれるのだろう。

丸2日近く使う羽目になったが、

[root@localhost tesseract]# tesseract --version
tesseract 4.00.00alpha
 leptonica-1.74.4
  libjpeg 6b (libjpeg-turbo 1.2.90) : libpng 1.5.13 : libtiff 4.0.3 : zlib 1.2.7

 Found AVX
 Found SSE

とTesseract最新版のインストールが無事に終わった。
英語のマニュアルやフォーラムを読み、延々とCUIを叩き続けたことは良い経験となったのではと思う。

教訓としては、

  • 英語でも面倒がらずにマニュアルを熟読すること
  • 依存するソフトのバージョンは、指定がなくても出来る限り最新を使う
  • 自分が使っているOSの環境を把握すること(パスなど)

の3点。次に同じような事態に陥っても、今度はもっと早く問題を解決できるはず。

10月12日

■ Tesseract続き
インストールは無事通ったので、コマンドラインから使えるようにしたい。
しかし、指示に従ってTESSDATA_PREFIXの値を設定してもtraineddataが読み込めていないようだ。

Tesseractを入れ直したりしてみたが、結局、

export TESSDATA_PREFIX=/usr/local/share/tessdata/tessdata/

としてeng.traineddataやosd.traineddataを/usr/local/share/tessdataに置くことで読み込まれるようになった。

[root@localhost tessdata]# tesseract --list-langs
List of available languages (3):
jpn
eng
osd

毎回exportするのは面倒なので/etc/profile内に書き加えてある。
コマンドラインからOCRの実行も確認できたので、これにてTesseractのインストールは完了。

■ pyocrから確認
Tesseractのテストとしてpyocrを使ったコードを動かしてみたが、うまく動いていない様子。安定版ではない4.00.00にはまだ対応していないようなので、とりあえず保留とする。

■ Juman++で入力テキストを分かち書きして保存する
先週触っていた、分かち書きして保存するコードを修正する。
print文で内容を確認した限りでは、1つのテキストファイル全体のうち一部分しか読み込めていないようだった。コードをよく調べて原因を突き止めたい。

一つ一つ確認したところ、Juman++は改行を認識するとその手前までで解析を行って結果を返すことがわかった(またしてもマニュアルの読み抜けである)。
これは、

(import re が必要)
def split_into_words(text):
    replaced=re.sub('\n', '', text)  # 改行(\n)を削除
    result=Jumanpp().analysis(replaced)
    return [mrph.midasi for mrph in result.mrph_list()]

としてすぐに対処できた。しかし今度は、

File "/home/vagrant/.pyenv/versions/3.6.2/lib/python3.6/site-packages/pyknp-0.3-py3.6.egg/pyknp/juman/morpheme.py", line 93, in _parse_spec
ValueError: invalid literal for int() with base 10: '\\'

とエラーを吐くようになってしまった。
returnの内容をprintで表示させると、’\u3000’という文字が含まれていることがわかった。これは全角スペースのことらしい。また半角スペースが含まれていてもエラーが出るようなので、まとめて削除する。

replaced=re.sub('\n|\u3000| ', '', text)

これでJuman++による形態素解析が正常に動作するようになった。
借りてきたコードが正しく動いているように見えても、油断できない例となった。

Juman++は高性能な分、解析にとても時間がかかる。実用には学習用データの選定が必須だ。4~5KBのテキストファイル1つにつき約8秒だと、Livedoorコーパス全体では12時間程かかってしまう計算である。

10月13日

■ Gensimで類似度解析
バグ取りの済んだプログラムで類似度解析をさせる。
以前の結果はなかったことにして、Doc2Vecの性能を確かめたいと思う。
類似性を人間が判断しやすそうという理由で、「家電チャンネル」の記事を使ってモデルを作成する。

ここで仕様について一度考え直してみる。

分かち書きテキスト&ファイルパス → TaggedDocument化 → それをリストにまとめる → Pickle化 → (モデル作成プログラム側で非Pickle化)

という手順を想定していたが、TaggedDocumentにするのは学習させる直前のほうが後々便利なのではと感じる。
TaggedDocument化前の段階なら、Python標準のデータ構造だけで扱っているのでデータ加工しやすいと考えた。

つまり、

分かち書きテキスト&ファイルパス → リストにまとめる → Pickle化 → (モデル作成プログラム側で非Pickle化 → TaggedDocument化)

という手順にする。
後者の手順で10個のテキストファイルに対して動作確認は済んだので、これで実装していく。
分かち書きした状態のテキストデータを保存するというのも少し考えたが、ファイル数が多くなりすぎるので却下した。

作成したモデルに対して、適当なテキストのパスを与えて類似度解析をしてみた。どことなく近い内容のものは出てくるが、類似度が0.2程度ではこんなものだろうか。
関連記事の部分やURL、更新日時を削除すればもう少し精度が上がるかもしれない。
次にエスマックスの記事を形態素解析させる際には、URLと更新日時が書かれた最初の2行を削除するよう変更した。
これでエスマックスの記事内の類似度がどう出るかを見て判断していきたい。

待ち時間に他の形態素解析器について調べた。
処理時間についてこんな情報があった。
参考:形態素解析の速度比較
Juman++がWikipediaのデータ5MB分で3時間というのは、こちらの環境での数字とほぼ変わらない。
kuromojiが高性能そうなのだが、Javaバージョンしかないのが残念。形態素解析だけkuromojiに任せて、分かち書きしたテキストデータをPythonから読ませる形ならなんとかなるかもしれない。

そうこうしているうちに形態素解析が終わっていた。
エスマックス記事に関しても類似度解析させてみたが、少なくとも学習パラメータmin_countが1より5の時のほうが良さそうなことは分かった。
(min_count:これより出現回数が少ない単語を破棄)
あらゆる単語について考慮しても、ノイズが多くなるだけということか。

例えばmin_count=5のとき、あるスマートフォン新モデルの紹介記事に対して、他のスマートフォン新モデルのレビュー記事が上位に来たりと一定の類似性が認められる。
Androidの話をしている記事に対しては高頻度でiOS、iPhoneの記事が上位に来る。
類似度では0.15~0.19程度。類似度0.20を上回るのは主に短文の記事で、類似性があまり無いことが多い。
またそもそも類似の記事が無さそうな記事については、類似度が全くアテにならないように見える。

結果はさておき、分かち書きを済ませたデータを用意しておくことによって、モデルの作成で試行錯誤しやすくなったのは非常に便利。自分なりの学習環境を整備する重要性を感じた。

以下にここまでの作業で使ったコードを掲載する(Python3用、ファイル3つ)。

import sys
import re
from os import listdir, path
from pyknp import Jumanpp
from gensim.models.doc2vec import LabeledSentence
import pickle

# 解析対象のディレクトリ
tdir = str(sys.argv[1])
# TaggedDocuments保存先
save_td = str(sys.argv[2])

# 記事ファイルをディレクトリから取得する
def corpus_files():
    # フォルダ探索
    dirs=[path.join(tdir, x) for x in listdir(tdir) if not x.endswith('.txt')]
    # dirsで見つかったフォルダ内のファイル探索
    docs=[path.join(x, y) for x in dirs for y in listdir(x)]

    # 指定ディレクトリのファイルも探索
    for x in listdir(tdir):
        if path.isfile(tdir + x):
            docs.extend(tdir + x)

    # 記事ファイルのリストをreturn & パスのリストを保存
    with open( save_td + '.txt', 'wt') as f:
        f.write('\n'.join(docs))
    return docs

# 記事の内容をパスから取得する
def read_document(path):
    with open(path, 'r') as f:
        for x in range(1,3):
            f.readline() # 2行読み飛ばす
        return f.read()

# JUMAN++を使って記事を単語リストに変換する
def split_into_words(text):
    replaced=re.sub('\n|\u3000| ', '', text)
    result=Jumanpp().analysis(replaced)
    return [mrph.midasi for mrph in result.mrph_list()]

# 記事を単語に分割して、リストを返す
def doc_to_sentence(doc, name):
    words=split_into_words(doc)
    return words, name

# 記事のパスリストから記事コンテンツに変換し、単語分割してセンテンスのジェネレータを返す
def corpus_to_sentences(corpus):
    docs=[read_document(x) for x in corpus]
    for idx, (doc, name) in enumerate(zip(docs, corpus)):
        sys.stdout.write('\r前処理中 {}/{}'.format(idx, len(corpus)))
        yield doc_to_sentence(doc, name)

#処理メイン
corpus=corpus_files()
sentences=corpus_to_sentences(corpus)

#TaggedDocument保存
tdall = []
for words, name in sentences:
    tdall.append([words, name])
with open(save_td, 'wb') as f:
    pickle.dump(tdall, f)
print('\n処理終了')
import sys
from gensim import models
from gensim.models.doc2vec import LabeledSentence
import pickle

# TaggedDocumentファイル
tdir=str(sys.argv[1])
# モデル保存先
save_md=str(sys.argv[2])

# 読み出したリストをTaggedDocumentに変換
def make_td(list):
    for words, name in list:
        yield LabeledSentence(words=words, tags=[name])

with open(tdir, 'rb') as f:
    lists = pickle.load(f)

sentences=make_td(lists)
model=models.Doc2Vec(sentences, dm=1, size=300, window=5, alpha=.025, min_alpha=.0001, min_count=5, sample=1e-6, iter=1000)
model.save(save_md)
from gensim import models
import sys 

# sys.argv[1]=ドキュメントのタグ(jumanpp.py実行時にtxtファイルとして一覧を保存済)
# sys.argv[2]=学習対象のモデル(d2v_init.pyで作成したもの)
model = models.Doc2Vec.load(sys.argv[2])
sim = model.docvecs.most_similar(sys.argv[1], topn=10)
for x in sim:
    print(x)

もっとデータ量を増やして検証したいところではある。kuromojiのためにJavaをもう少し勉強するかどうか、検討したい。
何気なく使っていたが、Juman++のメモリ消費量はかなり大きい。一度に大量に分析させるとメモリをかなり消費するので、メモリ搭載量が少ない(おそらく4GB以下の)システムでは注意が必要である。

10月16日

■ kuromojiを試すための下準備

kuromojiを使ってみるために、Javaの開発環境を用意する。
手早く理解するために日本語環境も用意する。

  • Eclipse(Oxygen) Windows 64bit [リンク]
  • Pleiades Eclipse日本語化プラグイン [リンク]

注意書きに従ってインストール。
プラグイン更新時には、Eclipseをクリーン起動するのを忘れずに。
Windows版なら”eclipse.exe -clean.cmd”を使って -clean オプションで起動することができる。

プロジェクト作成については以下を参考に。

  • Qiita:Eclipse+Maven という便利な開発環境をインストールからプロジェクト作成まで [リンク]
  • kuromoji公式ページ [リンク]

以上でkuromojiを使う準備が整った。

■ kuromojiを試してみる

プロジェクトに新規クラスを作成して、サンプルコードを試してみる。

package kuromoji.hello;

import org.atilika.kuromoji.*;

public class Kuromoji {
    public static void main(String[] args) {
        // TODO 自動生成されたメソッド・スタブ
        Tokenizer tokenizer = Tokenizer.builder().build();
        for (Token token : tokenizer.tokenize("形態素解析のテストをします。")) {
            System.out.println(token.getSurfaceForm() + "\t" + token.getAllFeatures());
        }
    }
}
形態素 名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
解析  名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ
の   助詞,連体化,*,*,*,*,の,ノ,ノ
テスト 名詞,サ変接続,*,*,*,*,テスト,テスト,テスト
を   助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
し   動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
ます  助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。   記号,句点,*,*,*,*,。,。,。

問題なさそうなので、テキストファイルからデータを読み込んで処理できるようにコーディングしていきたいと思う。

■ 単一テキストファイルの読み込みテスト
入門書とWebの情報を頼りに短時間で実装できたので、例のごとくLivedoorコーパスを対象にテストを行った。
空白、記号が含まれていても問題なく解析できている様子。処理は早く、精度も不満のないレベル。

Gensimとの連携のためには分かち書きしたテキストファイルを、元データからまとめて生成できる必要がある。

■ ディレクトリ指定して一括処理するコード

引数は対象のファイルが中にあるディレクトリを想定した。
(ex. C:\example\texts)
テキスト以外のファイルが存在すると何らかのエラーを吐くと思われる。

package kuromoji.hello;

import org.atilika.kuromoji.*;
import java.io.*;

public class Kuromoji { 
    public static void main(String[] args) {
        File fl1 = new File(args[0]);

        if(fl1.exists()) {
            //書き込み用ディレクトリを作成
            File fl2 = new File(args[0] + "_sp");
            if( !fl2.exists()) {
                fl2.mkdir();
            }

            String[] array = fl1.list();
            for(int i=0; i<array.length; i++) {

                System.out.println("処理中:"+(i+1)+"/"+array.length);

                FileReader fr;
                FileWriter fw;

                String fin;
                String fout;

                // 入力ファイル(引数から対象ファイルパスを生成)
                fin = args[0]+"\\"+array[i];

                // 出力ファイル(対象のファイルが存在する場合上書き)
                fout = args[0]+"_sp\\"+array[i];

                try {
                    fr = new FileReader(fin);
                } catch (IOException e) {
                    System.out.println("Read File can not open!");
                    fr = null;
                }
                try {
                    fw = new FileWriter(fout);
                } catch (IOException e) {
                    System.out.println("Write File can not open!");
                    fw = null;
                }

                try {
                    BufferedReader in = new BufferedReader(fr);
                    PrintWriter out = new PrintWriter(fw);

                    // 2行読み込んで捨てる(Livedoorコーパスのヘッダー部分)
                    for(int j=0; j<2; j++) {
                        in.readLine();
                    }

                    // Tokenizer初期化
                    Tokenizer tokenizer = Tokenizer.builder().build();

                    String s;
                    while ((s = in.readLine()) != null) {
                        for (Token token : tokenizer.tokenize(s)) {
                            // 記号を除外する
                            if(!token.getPartOfSpeech().substring(0,2).contentEquals("記号")) {
                                out.print(token.getSurfaceForm()+" ");
                            }
                        }
                    }
                    fw.close();
                    fr.close();

                } catch (Exception e) {
                    System.out.println("Detected Unknown Error!");
                }
            }
        } else {
            System.out.println("No Such a Directory!");
        }
    }
}

これでkuromojiを使って分かち書き処理をさせることができるようになった。試しに3MB程度のテキストを一気に処理させたが、数秒で終了した。
とりあえずkuromojiを使うためだけに書いたコードなので、全く再利用性などは考慮していない。

■ Livedoorコーパス全件に対するモデルでDoc2Vecを検証する

空白区切りのテキストはsplitメソッドで簡単にリスト化できるので、元のコードを少し変更するだけでGensimに渡すことができた。
テキストファイル約7000件(約25MB)を対象にモデルを作成し、適当に選んだ記事に対する類似度を表示させてみた。
しかし以前の検証よりジャンルが多岐にわたるようになったからか、全く関係のない記事が上位に来る率が高くなった。
Livedoorコーパスは一つの記事内に複数の内容が入っていたりするので、類似性の検証には向かない可能性はある。

■ せっかくなのでJavaについてもう少し勉強する

正確に類似の文章を見つけ出すのはなかなか難しいということがより分かってきた。
すぐに解決するようなものではなさそうなので、こちらは焦らずに進めていきたいと思う。

せっかくJavaについて勉強して簡単なコードも書いたので、今のうちにJava(オブジェクト指向)への理解を深めておく。

  • 「ずばりわかる!Java」ISBN (4-8222-2834-7)

10月17日

■ Javaの勉強

昨日の本を読み進める。Javaは使う予定はほとんどないが、オブジェクト指向の考え方について学ぶにはちょうど良い内容。
Javaについて色々と読んだ後で、Pythonでのオブジェクトの扱い方を復習する。
Pythonは言語側で色々と複雑さを吸収してくれているのがよく分かる。

■ PEP8コーディング規約

Pythonの読みやすいコードを書く上での規約。今まで特別に意識したことはなかったが、ここで過去に書いたコードについて調べてみた。

pip install pep8
pep8 --first d2v_init.py
import sys
from gensim import models
from gensim.models.doc2vec import LabeledSentence

import pickle

# TaggedDocumentファイル
tdir=str(sys.argv[1])

# モデル保存先
save_md=str(sys.argv[2])

with open(tdir, 'rb') as f:
    lists = pickle.load(f)

# print(lists)

# 読み出したリストをTaggedDocumentに変換
def make_td(list):
    for words, name in list:
        yield LabeledSentence(words=words, tags=[name])

sentences=make_td(lists)

model=models.Doc2Vec(sentences, dm=1, size=300, window=5, alpha=.025, min_alpha=.0001, min_count=5, sample=1e-6, iter=1000)

model.save(save_md)

としてTaggedDocumentからモデルを作成するコード(27行)を対象にしたところ、

d2v_init.py:8:5: E225 missing whitespace around operator
d2v_init.py:19:1: E302 expected 2 blank lines, found 1
d2v_init.py:25:80: E501 line too long (123 > 79 characters)
d2v_init.py:28:1: W391 blank line at end of file

と4種類警告された。
E225はわかりやすいところ。引数では逆に空けてはいけないらしい。アノテーションに関してはまた別の規則があってややこしい。
E302がピンとこないが、関数orクラス定義は2行空けることになっているらしい。クラス内のメソッドについても1行ずつ空ける決まり。
E501は単純に1行が長すぎると怒っている。長いときは複数行に渡る記述を使おう、というわけだ。それはまあ1行だけ200文字とかあるようなコードは私も読みたくはない。
W391はエラーではないが、ファイルに不要な空行があると怒られている。厳しい。
というわけで他にも(違反ではないが)気になる点を直し、コードが出来上がった。

import pickle
import sys
from gensim import models
from gensim.models.doc2vec import LabeledSentence

# TaggedDocumentファイル
tdir = str(sys.argv[1])

# モデル保存先
save_md = str(sys.argv[2])


# 読み出したリストをTaggedDocumentに変換
def make_tagged_docs(list):
    for words, name in list:
        yield LabeledSentence(words=words, tags=[name])


with open(tdir, 'rb') as f:
    lists = pickle.load(f)
sentences = make_tagged_docs(lists)

model = models.Doc2Vec(sentences, dm=1, size=300, window=5,
                       alpha=.025, min_alpha=.0001, min_count=5,
                       sample=1e-6, iter=1000)
model.save(save_md)

かなり読みやすくなったような気がする。コメントの付け方はさておき。
いい機会なので、「PEP 8 — Style Guide for Python Code」を一通り読む。
英語で理解できなかった部分や怪しい部分は「日本語版」で。

■ SQLについて学ぶ

いずれは知っておく必要があると思われるSQLについて触れておく。応用情報技術者試験のために多少は勉強したが、ほとんど理解できていなかったと言っていい。
SQL初心者向けの入門ページから入る。

  • 初心者のためのSQLガイド : SQLを直感的に理解しよう [リンク]
  • SQL実践講座 [リンク]
    プログラミングとは違った難しさがある。明日も勉強して概要をしっかり理解しておきたい。

10月18日

■ 引き続きSQLについて学ぶ

リファレンスを見ながらでも多少複雑な操作ができる、くらいまで行ければいいと思うが、なかなか難しい。

■ WPA2が破られたということで情報を集めた

つい先日WPA2の脆弱性が見つかったと話題になっていた。そろそろ情報が出揃ってきた頃だろうということで調べると、見つかったのはプロトコルそのものの脆弱性とのことだった。
ソフトウェア・アップデートで対応可能ということなので、パッチが公開され次第更新していけば問題はないとのこと。WPA2が全く使えなくなるほどの問題ではなかった、ということでひとまず安心できる。

■ 最近のセキュリティ関連トピックを見る

数日前にAndroid向けの新たなランサムウェアが見つかったという記事が出てきた。
この記事では、非公式の「Adobe Flash Player」アプリのインストールに注意喚起をしている。
かといって信頼できるウェブサイトからのダウンロードであれば大丈夫かというと、サイトやアプリが改竄されている可能性はゼロではないのが実情。[参考記事]
こうなると個人レベルで最も有効な対策は、感染に備えてオフラインのバックアップを常備しておくことなのかもしれない。

■ Gensimで高精度なモデルの作成を目指して

類似度を適宜確認しながら手作業で調整していくのではいつ終わるのか分からないので、パラメータ調整に役立つ情報は得られないかと試行錯誤してみる。

単語のベクトル値を保存して「Embedding Projector」を使うことによって、データを可視化して検討できそうだという結論に至った。t-SNEやPCAといった手法で次元削減させることもできる。
データをTSVに加工しないと読み込ませることができないようなので、実践するのは明日にする。

10月19日

■ マーケティング戦略について調べる

ウェブサービスを提供する会社に属している人間として、マーケティングについて知っておく必要があるだろう。ということで、本で多少知識を仕入れたことはあるが、復習も兼ねて調べて回ることにする。

■ Embedding Projectorを試してみる

データ整形用のコードを書いて、単語ベクトルの可視化を試してみた。
しかし、どうも単語同士の関係が怪しい。全く関係が無いはずの単語のコサイン類似度が高く出てしまう。
学習のさせ方に問題があるのか、入力データに問題があるのか、現段階では判断できない。

簡単のために、電機関連のITライフハック、家電チャンネル、エスマックスの3つを対象としてモデルを作成してみた。
名詞のみを抽出したTaggedDocumentに対して、min_count、sampleそしてwindowを大きめにしたところ、ある程度納得のできるコサイン類似度が出力されるようになった。
一つの文章が長いので、windowをある程度大きくするのは確かに有効かもしれない。名詞の抽出で除去しきれていない記号や数字の削除もしたら、まだ精度を上げられるかもしれない。

想像以上に単語ベクトルが怪しかったせいで、Embedding Projectorをほとんどいじれていない。明日もう少し検討を進めたいと思う。

10月20日

■ 自動機械学習について調べる

自動機械学習(AML:Automated Machine Learning)というものがあるらしい。
知識のある人間が手作業でパラメータを調節するより、コンピュータが自動で調節してくれれば遥かに楽である。

  • 機械学習を自動化する機械学習プロジェクト6選(前)[リンク]

機械学習フレームワークが流行ってから何年も経っていないと思うのだが、もう次の段階に進もうとしている。
さらに数年したら学習手法すら自己学習するようになっているのかもしれない。

■ auto-sklearnを試してみる

scikit-learnは以前に試しているので、auto-sklearnを試してみようと思う。
まずはDocumentationにあった論文を読むところから。
読んでいる間にサンプルデータを用意して回しておいたほうがいいのではということに気がついたので、とりあえず回してみることにした。
Exampleを読んでみると、

This will run for one hour should result in an accuracy above 0.98.

と書いてある。全部コンピュータ任せだとどうしても時間はかかるか。
とりあえずインタプリタにサンプルコードを打ち込んで学習を始めると、ゲストOSが4GB以上メモリを消費し始めた。機械学習はリソース&コストとの戦いである。

先行研究と比較しての改良点などについての記述が中心で、その研究について調べていないのでコメントしづらい。
とりあえず、効率・精度の両方で改善が見られ、かつ時間に対する値の安定性が高い、と解釈した。先行研究についてはまた別の機会に。

勉強中の身なので実際に活用することはしばらくないとは思うが、どうしても急ぎで最適化したパラメータが欲しい、のような状況なら使えるかもしれない。
急ぎと言っても丸1日はかかるが、それでも人力よりはマシか。

計算が終了してAccuracy scoreを確認すると約0.987と出たので、正しく実行されたようだ。アルゴリズムを全く指定せずに、データを投げただけで結果が出てくるというのは少し感動を覚える。

■ 数字と記号を除去して学習させてみる(昨日の続き)

Javaコードに正規表現でフィルタリングする部分を足して、学習時のノイズを低減できるように改良した。
word2vec_formatで出力したvocabファイル(出現単語と回数が記録されている)を使って、概ね除去できていることを確認。

以前に単語ベクトルの類似度を調べたときの記憶と照らし合わせると、だいぶ精度が上がっているように感じる。
今までは敢えて除去せずにやっていたわけだが、入力データに含まれるノイズがいかに影響していたかを実感している。それでも記事データという性質上まだまだノイズは含まれていると言えるが。

10月23日

■ AnacondaをWindows環境にインストールする

Anadondaを使ってPythonパッケージをWindowsに一括でインストールする。TensorFlowの勉強に本腰を入れるためには、仮想環境では力不足だと感じた。
ついでにPyCharm(Community Edition)もインストールする。

■ AlphaGoのアルゴリズムについて調べる

AlphaGoがどういうアルゴリズムを用いているのかについて調べ、どういったアイデアで最強のAIを作り上げたのかを知る。
本来は原文を読むべきだが、こちらの翻訳を読んで参考にした。

■ TensorFlowを試す

何かオリジナルのモデルについて考えようと思いAPIリファレンスやサンプルコードと格闘してみたが、なかなか動作やコードのイメージができない。
このままでは埒が明かないので、色んな人が書いたTensorFlowのコードを読んできっかけをつかみたいと思う。

  • 【本当の初心者向け】ニューラルネットとTensorFlow入門のためのオリジナルチュートリアル1 [リンク]
  • 深層学習とTensorFlow入門 [リンク]

機械学習から少し離れていたので、必要な数学の知識について書かれた前者のリンク先が非常に助かる。短期間に詰め込みすぎたせいなのか、内容が曖昧になっている部分が意外と多い。

10月24日

■ 昨日の続き

機械学習と必要な知識の復習をし、具体的な計算、学習手順について検討する。
題材は程よくシンプルなものとして、四目並べ(Connect Four)とした。
最善手を続けたら必ず引き分けになると予想する。

■ 実装

ゲーム部分の仕様を、TensorFlowとの連携まで考えて記述する。
肝心の機械学習を回せるまでが長いが、numpyの扱いまで含めた練習として捉えておく。

学習時のプレースホルダとして、

  • 棋譜:game_flow (手数 x 盤面の n x 43行列、バイアス項を含む)
  • 手の点数:pts(要素数が手数のベクトル、勾配の更新に使う)

を用意する。
棋譜は盤上のコマについて、(無し, 先手, 後手)= (0, 1, -1)とする。
手の点数は、勝敗が決していれば勝者の手を1、敗者の手を-1として勾配を更新する。
引き分けの場合は一律で先手-0.2、後手0.2とする(点数の付け方については暫定)。
初期盤面は全て0の行列となるので、バイアス項を加える必要がある。
動作の確認ということで、ひとまず隠れ層は1層にした。

■ 動かしてみる

とりあえず動く状態になったので学習させてみる。
重みの初期値は完全にランダムにしてある。
置けないところに置こうとしたら反則負けとしているので、途中の出力ではほとんど反則負けしているのが見える。
最終的に左端にしか置かなくなってしまったので、これではいけない。
今日のところはうまく学習させることはできなかったが、ひとまず動くものを用意することはできた。明日以降もTensorFlowを扱っていく。

10月25日

■ 昨日の続き

隠れ層を増やしてどのような結果になるか検証する。

  • 参考:第8回 TensorFlow で○×ゲームの AI を作ってみよう[リンク]

単純に増やしただけでは、最終的に左端に置き続ける現象は変わらず。学習がストップしてしまっている。
先手と後手で別々に学習させるという案も試したが、これもうまくいかず。
そこで、隠れ層にドロップアウトを適用することにした。過学習の回避に有効な方法である。

隠れ層を3層とし、各ノード数を128とし、1層目をp=0.5、2層目をp=0.2としてドロップアウトを適用する。
エポック数を100,000として学習させたところ、30,000付近で反則手で決着する割合が半数以下になった。ここまで劇的に効果が表れるとは思わなかったので、非常に驚いた。

loop = 40000
[[ 0.  0.  0.  0.  0.  0.  0.]
 [ 0. -1.  1.  1. -1.  0.  0.]
 [-1.  1. -1. -1.  1.  0.  0.]
 [ 1. -1.  1.  1. -1.  0.  0.]
 [-1.  1. -1. -1.  1.  1.  1.]
 [ 1. -1.  1. -1. -1. -1.  1.]]
winner = -1

# 直近の2000対戦分のログ
wF = 385    wS = 707   # wF:先手勝ち  wS:後手勝ち
iF = 325    iS = 583   # iF:先手反則負け  iS:後手反則負け

多少怪しい盤面だが、全部ランダムの初期値からそれらしい勝負をできるまでになっているのはニューラルネットワークの力を感じる。

当初は引き分けが多くなると予想していたが、反則手負けを許容しているせいか全く引き分けにならない。
隠れ層を増やして回してみるなどしたが、最初に適用したドロップアウトの設定より良くなりそうな兆候は見られなかった。

とにかく、しっかり動作することが確認できたので、十分学習させた上でモデルを保存する。
ちなみに私の環境で、学習速度は100回/秒程度だった。

今回学習に使ったコードは以下の通り。

# -*- coding: utf-8 -*-

import os
import shutil
import tensorflow as tf
import numpy as np

# デバッグ出力の集計用
infringeF = 0
infringeS = 0
winF = 0
winS = 0


def game_train_sim(w, num, sess):
    global infringeF
    global infringeS
    global winF
    global winS

    g_flow = np.zeros([42, 43], dtype=np.float32)  # 棋譜の一時保管
    g_flow[:, 42] = 1  # 棋譜にバイアス項付与
    pts = np.zeros([42, 7], dtype=np.float32)  # 点数の一時保管
    field = np.zeros([42 + 1], dtype=np.float32)  # バイアス項付き盤面
    field[42] = 1  # バイアス項
    field_rec = field[0:42]  # スライス参照の作成をして
    field_rec = field_rec.reshape(6, 7)  # 6 x 7 行列に
    place_pts = np.zeros([7])  # 着手場所
    turn = 0  # 手数
    winner = 0  # 勝者(0=引き分け,1=先手,-1=後手)
    DRAW_RATE = 0.1  # 引き分け時の点数

    while (turn < 42 and winner == 0):
        F = field.copy().reshape(1, 43)
        place_pts = sess.run(w, feed_dict={Game_flow: F})  # 着手場所の算出
        place = place_pts.argmax()

        g_flow[turn, :] = field.copy()  # 棋譜に盤面をコピー
        pts[turn, place] = 1.0  # 着手場所を記録

        if field_rec[0, place] != 0:  # 着手不可能な場所を選択
            if turn % 2:
                winner = -1
                infringeF += 1
            else:
                winner = 1
                infringeS += 1
            break

        for i in range(5, -1, -1):  # 着手して盤面を更新
            if field_rec[i, place] == 0:
                if turn % 2:
                    field_rec[i, place] = -1.0
                else:
                    field_rec[i, place] = 1.0
                break

        turn = turn + 1

        # 4つ並んでいるか判定
        winner = judge(field_rec)

    # 勝敗に応じたptsへ更新
    if winner > 0:
        pts[1::2, :] *= -1
        winF += 1
    elif winner < 0:
        pts[0::2, :] *= -1
        winS += 1
    else:
        pts[:, :] *= DRAW_RATE
        pts[1::2, :] *= -1

    # Debug Output
    if num % 500 == 0:
        print("loop = {}".format(num))
        if num % 2000 == 0:
            print(field_rec)
            print("winner = {}".format(winner))
            print("wF = {} \twS = {}".format(winF-infringeS, winS-infringeF))
            print("iF = {} \tiS = {}".format(infringeF, infringeS))
            print(place_pts)
            winF = 0
            winS = 0
            infringeF = 0
            infringeS = 0

    return g_flow[0:turn, :], pts[0:turn, :]


# 盤面を走査
def judge(field):
    win = 0
    for i in range(0, 3):
        for j in field[i:i + 4, :].sum(0):
            if j == 4:
                win = 1
                break
            elif j == -4:
                win = -1
                break
    if win != 0:
        return win

    for i in range(0, 4):
        for j in field[:, i:i + 4].sum(1):
            if j == 4:
                win = 1
                break
            elif j == -4:
                win = -1
                break
    if win != 0:
        return win

    for i in range(0, 4):
        for j in range(0, 3):
            k = field[j, i] + field[j + 1, i + 1] \
                + field[j + 2, i + 2] + field[j + 3, i + 3]
            if k == 4:
                win = 1
                break
            elif k == -4:
                win = -1
                break

            k = field[j + 3, i] + field[j + 2, i + 1] \
                + field[j + 1, i + 2] + field[j, i + 3]
            if k == 4:
                win = 1
                break
            elif k == -4:
                win = -1
                break

    return win


# 隠れ層の設定
def inference(x_ph):
    hidden1 = tf.layers.dense(x_ph, 128, activation=tf.nn.relu)
    drop1 = tf.layers.dropout(hidden1, rate=0.5)
    hidden2 = tf.layers.dense(drop1, 128, activation=tf.nn.relu)
    drop2 = tf.layers.dropout(hidden2, rate=0.2)
    logits = tf.layers.dense(drop2, 7)
    return logits


with tf.Graph().as_default() as graph:
    tf.set_random_seed(1000)
    Game_flow = tf.placeholder(tf.float32, shape=(None, 43))
    Pts = tf.placeholder(tf.float32, shape=(None, 7))

    logits = inference(Game_flow)
    y_ = tf.nn.softmax(logits)
    loss = -tf.reduce_sum(Pts * tf.log(y_))
    train = tf.train.AdamOptimizer(0.0002).minimize(loss)
    saver = tf.train.Saver()

    with tf.Session() as sess:
        sess.run(tf.initialize_all_variables())
        for i in range(200001):
            gf, p = game_train_sim(logits, i, sess)
            sess.run(train, feed_dict={Game_flow: gf, Pts: p})

        # 以下モデル保存部
        if not os.path.isdir("checkpoints"):
            os.mkdir("checkpoints")
        saver.save(sess, "checkpoints/connect_four")
        # Remove old model
        if os.path.exists("model"):
            shutil.rmtree("model")
        # Save model for deployment on ML Engine
        input_key = tf.placeholder(tf.int64, [None, ], name="key")
        output_key = tf.identity(input_key)
        input_signatures = {
            "key": tf.saved_model.utils.build_tensor_info(input_key),
            "x": tf.saved_model.utils.build_tensor_info(Game_flow)
        }
        output_signatures = {
            "key": tf.saved_model.utils.build_tensor_info(output_key),
            "y": tf.saved_model.utils.build_tensor_info(Pts)
        }
        predict_signature_def = tf.saved_model.signature_def_utils.build_signature_def(
            input_signatures,
            output_signatures,
            tf.saved_model.signature_constants.PREDICT_METHOD_NAME
        )
        builder = tf.saved_model.builder.SavedModelBuilder(os.path.join("model"))
        builder.add_meta_graph_and_variables(
            sess,
            [tf.saved_model.tag_constants.SERVING],
            signature_def_map={
                tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: predict_signature_def
            },
            assets_collection=tf.get_collection(tf.GraphKeys.ASSET_FILEPATHS)
        )
        builder.save()

別な乱数種に対しても回してみたが、学習は問題なく進んでいるようだ。
出力されるログの評価値と盤面を眺めていると、最下段に置く手はたとえ勝敗に直結する手であっても、評価されにくい傾向があることが分かった。
勝敗に応じて全部の手に一律で評価を与えているので、最終的に勝ちやすい真ん中付近を埋める手が重視されているのだろう。

10月26日

■ パラメータをいじってみる

隠れ層のノード数を減らしてみたり、誤差関数を変えてみたり、色々と試してみる。
出力を見る限り、reduce_sumよりはreduce_meanのほうが良さそう。
4目並べ程度の複雑さでは、隠れ層、ノード数は最初の状態が一番良さそうだ。
とりあえずTensorFlowで学習させることができたということにして、次の課題について考えることにする。

■ CUDAに対応させる

そうこうしているうちにGeForce GTX 1050 Ti が届いたので、TensorFlowをGPU対応で動かせるようにする。

あとはコマンドプロンプトで、

pip3 uninstall tensorflow
pip3 install tensorflow-gpu

としてGPU版に入れ替え。これでTensorFlowがGPUを使えるようになった。
気をつけたいのが、

  • TensorFlow 1.3
  • CUDA Toolkit 8.0
  • cuDNN v6.0 for CUDA 8.0
  • Python 3.5.x or 3.6.x

と、TensorFlowはしっかりバージョンを揃える必要がある。
今後バージョンが上がったら、それに応じて他もインストールガイドに従って揃えなければならない。

■ とりあえず昨日のコードを動かしてみる

計算速度が落ちている。GPU向けの処理に書き換えていく必要があるようだ。

ある程度データをまとめた上で計算させてみたらとも思ったが、1回ごとに勾配を更新しなければならないので今回の例では使えない。
シミュレーション部分の改善をするしかない。

■ TensorFlowのサンプルコードで威力を確認

GPUで機械学習がどれくらい早くなったかをいち早く体感するために、サンプルコードを実行してみる。
TensorFlowチュートリアルのMNISTとDeep MNISTを回してみた。
結果MNISTのほうは一瞬で、Deep MNISTも数分で終わってしまった。
これならコストの重い画像認識などの実験をする程度であれば活躍してくれるだろう。

余談:今回導入したのは、

このカードで、補助電源不要で動作音は静か。
同じく補助電源不要のGeForce GT 1030なら6割くらいの値段で買えるが、CUDAコア数も性能も半分。それならコストパフォーマンス重視、ということで1050 Ti を選択した。AWSなどに手を出す前の練習として、これからバリバリ働いてもらうことにする。

■ 高速化の方策

if,for文を可能な限り避け、行列演算にできれば良いということは知識としてある。今まで書いたコードのif,for文をどうやったら削ることができるか、検討してみる。

勝利条件の走査やコマの着手をできるだけnumpyで計算するように改良したところ、多少高速化することができた。

10月27日

■ 変更後のコードと変更前のコードで結果が一致するはず

ということで計算させていたのだが、どうにも結果が合わない。
ずっと変更後のコードを見ていたのだが、実は変更前のコードにバグがあったことが判明する。変更前のコードが正しいという前提が間違っていたとは。
改良後のコードは、バグが直って引き分けに持ち込まれるケースが現れるようになった。

# -*- coding: utf-8 -*-

import os
import shutil
import tensorflow as tf
import numpy as np

# デバッグ出力の集計用
infringe = [0, 0]
win = [0, 0]
draw = 0


def game_train_sim(w, num, sess):
    global infringe
    global win
    global draw

    place_list = [1, -1]

    g_flow = np.zeros([42, 43], dtype=np.float32)  # 棋譜の一時保管
    g_flow[:, 42] = 1  # 棋譜にバイアス項付与
    pts = np.zeros([42, 7], dtype=np.float32)  # 点数の一時保管
    field = np.zeros([42 + 1], dtype=np.float32)  # バイアス項付き盤面
    field[42] = 1  # バイアス項
    field_rec = field[0:42]  # スライス参照の作成をして
    field_rec = field_rec.reshape(6, 7)  # 6 x 7 行列に
    place_pts = np.zeros([7])  # 着手場所
    turn = 0  # 手数
    winner = 0  # 勝者(0=引き分け,1=先手,-1=後手)
    DRAW_RATE = 0.1  # 引き分け時の点数

    while (turn < 42 and winner == 0):
        F = field.copy().reshape(1, 43)
        place_pts = sess.run(w, feed_dict={Game_flow: F})  # 着手場所の算出
        place = place_pts.argmax()

        g_flow[turn, :] = field.copy()  # 棋譜に盤面をコピー
        pts[turn, place] = 1  # 着手場所を記録
        side = place_list[turn % 2]
        rev_side = place_list[(turn+1) % 2]

        if field_rec[0, place] != 0:  # 着手不可能な場所を選択
            winner = rev_side
            infringe[turn % 2] += 1
            turn += 1
            break

        for i in range(5, -1, -1):  # 着手して盤面を更新
            if field_rec[i, place] == 0:
                field_rec[i, place] = side
                break

        turn = turn + 1

        # 4つ並んでいるか判定
        if judge(field_rec) > 0:
            winner = side

    # 勝敗に応じたptsへ更新
    if winner > 0:
        pts[1::2, :] *= -1
        win[0] += 1
    elif winner < 0:
        pts[0::2, :] *= -1
        win[1] += 1
    else:
        pts[:, :] *= DRAW_RATE
        pts[1::2, :] *= -1
        draw += 1

    # Debug Output
    if num % 500 == 0:
        print("loop = {}".format(num))
        if num % 2000 == 0:
            print(field_rec)
            if winner > 0:
                winner = 1
            elif winner < 0:
                winner = -1
            print("winner = {}".format(winner))
            print("wF = {} \twS = {}".format(win[0]-infringe[1], win[1]-infringe[0]))
            print("iF = {} \tiS = {}".format(infringe[0], infringe[1]))
            print("Draw = {}".format(draw))
            print(place_pts)
            win = [0, 0]
            infringe = [0, 0]
            draw = 0

    return g_flow[0:turn, :], pts[0:turn, :]


# 盤面を走査
def judge(field):
    win = 0
    f_int = field.copy().astype(np.int8)  # 盤面を複製
    # 縦方向
    for i in range(0, 3):
        v_sum = f_int[i:i+4, :].sum(0)  # 符号付き和
        v_abs = np.abs(v_sum)  # 絶対値
        win += np.floor_divide(v_abs, 4).sum()  # 4つ並びができていればwin > 0

    if win != 0:
        return win

    for i in range(0, 4):
        h_sum = f_int[:, i:i + 4].sum(1)  # 符号付き和
        h_abs = np.abs(h_sum)  # 絶対値
        win += np.floor_divide(h_abs, 4).sum()  # 4つ並びができていればwin > 0

    if win != 0:
        return win

    slash = np.zeros([6, 4], dtype=np.int8)
    for i in range(0, 4):
        for j in range(0, 3):
            slash[j, i] = f_int[j, i] + f_int[j + 1, i + 1] \
                + f_int[j + 2, i + 2] + f_int[j + 3, i + 3]
            slash[j+3, i] = f_int[j + 3, i] + f_int[j + 2, i + 1] \
                + f_int[j + 1, i + 2] + f_int[j, i + 3]

    s_abs = np.abs(slash)  # 絶対値
    win += np.floor_divide(s_abs, 4).sum()  # 4つ並びができていればwin > 0
    return win


# 隠れ層の設定
def inference(x_ph):
    # 略

with tf.Graph().as_default() as graph:
    # 略

これでもまだ遅いが、当初に比べると多少マシにはなった。
バグ修正で挙動が多少変わっているので、隠れ層や学習率についてはまだ検討の余地があると思うが、4目並べについてはここまでとする。
想像以上にコードの書き換えとバグ取りで時間を使ってしまった。

■ 文章の要約について調べる

文章の要約をできるライブラリについて検索をかけてみたところ、

  • リクルートテクノロジーズ:自動要約APIを作ったので公開します [リンク]

というものが引っかかった。
ソースコード内に、

Reference:
      Günes Erkan and Dragomir R. Radev.
      LexRank: graph-based lexical centrality as salience in text
      summarization. (section 3)
      http://www.cs.cmu.edu/afs/cs/project/jair/pub/volume22/erkan04a-html/erkan04a.html

という記述を見つけたので、論文を読んでみることにする。
よく分からない単語が多いと思ったら、マルコフ連鎖に関連した用語のようだ。
マルコフ連鎖についても調べておかないと理解できそうにない。

  • Wikipedia:マルコフ連鎖 [リンク]
  • LexRank: Graph-based Lexical Centrality as Salience in Text Summarization [リンク]

読み終わる前に時間になってしまったので、今日はここまで。

10月30日

■ 論文を読む

先週読み終わらなかった論文の続きを読むところから始める。
マルコフ連鎖について調べたついでということで、聞き覚えのある統計学に関連した用語についても検索し、概要をつかむ。
論文中に出てきた手法についても軽く調べた。

  • Maximal Marginal Relevance (MMR)
  • Cross-Sentence Informational Subsumption (CSIS)

MMRについては、「情報利得比に基づく語の重要度と MMRの統合による
複数文書要約 [論文PDF]」を、
またCSISに関する引用元論文には、

CST is essential for the analysis of contradiction, redundancy, and complementarity in related documents and for multi-document summarization (MDS)

CSTは文書間の矛盾、冗長性、相補性の分析や、複数文書要約に不可欠なものである

と記述されている。

■ 試してみようとしたが…

とりあえずファイルからテキストを読ませてAPIを呼び出すも、

UnicodeDecodeError: 'ascii' codec can't decode byte 0xef in position 0: ordinal not in range(128)

とエラーが出る。そういえば今使っているのはPython2.7だった。
デコードして回避できたと思いきや、

  File "summpy_test.py", line 10, in <module>
    text, sent_limit=5, continuous=True, debug=True
  File "/usr/lib/python2.7/site-packages/summpy/lexrank.py", line 105, in summarize
    scores, sim_mat = lexrank(sentences, **lexrank_params)
  File "/usr/lib/python2.7/site-packages/summpy/lexrank.py", line 85, in lexrank
    graph.add_edge(i, j, {'weight': weight})
TypeError: add_edge() takes exactly 3 arguments (4 given)

というエラーが出る。どういう理由でargumentsが多くなっているのか見当がつかない。
せっかくなので少し試してみたかったのだが、また次の機会に。

10月31日

■ TensorFlowの練習

練習用の課題として、よりGPUを活かした計算のできるものを考える。
色々と考えてみたが、ある程度まとまったデータを容易に収集できなくてはならないのが難しい。
ありきたりだが、「CIFAR-10」をやってみることにした。GPUがある今なら、それなりのスピードで計算することができるはず。

■ CIFAR-10(力業)

色々考える前に、まずは贅沢なリソースの使い方で実験する。

  • 全結合層:3層の各2048ノード
  • 活性化関数:LeRU
  • ドロップアウト:1層目0.5、2層目0.2
  • 最適化関数:Adam Optimizer (学習率0.0001)

プーリングや畳込みは行わない。過学習防止のために、一回の学習毎にランダムな学習用データを1セット無視している。
以上の設定で50000×3072という大きなサイズのデータに対して力業で処理させてみる。
結果は、500回目あたりで

  • 学習用データ:Accuracy = 0.970、Loss = 0.115
  • テストデータ:Accuracy = 0.438、Loss = 4.813

となりほぼ収束した。
学習用データはdata_batch_1を代表としている。

そこでドロップアウトを2層目についても0.5としてみるも、ほぼ結果は変わらず。

  • Epoch = 1000
  • 学習用データ:Accuracy = 0.998、Loss = 0.030
  • テストデータ:Accuracy = 0.454、Loss = 4.643

さすがに無茶だったので、プーリング層や畳込み層を追加することにする。
1ドット単位での学習だけでは、どうしても過学習になってしまうようだ。

■ CIFAR-10(正攻法)

TensorFlow公式の「Deep MNIST for Experts」を参考にして実装する。
参考にとは言っても実装の仕方からして違うので、サンプルをコピーして読みながら書き換えていく作業になる。

11月1日

■ 昨日の続き

公式チュートリアル「Deep MNIST for Experts」のコードを読んで、CIFAR-10に対応したコードを書く。
入力データがMNIST→CIFAR-10になるので

  • MNIST:28x28x1(color channel : Grayscale)
  • CIFAR-10:32x32x3(color channels : RGB)

と読み替える。コード内のコメントで説明してくれていて助かった。
データ読み込み、バッチ処理回りもデータ規格に合わせて変更が必要。
全結合層(Fully Connected layer)についても1024ノードでは足りないかもしれないので、学習させてみてから各層のパラメータは適宜変更する。

■ とりあえず実行

data_batchファイル一つずつ(データ数10,000)学習させるという前回と同じ方法を選択したが、メモリ不足で学習に失敗した。プーリング層や畳込み層を追加するには、3.3GBでは足りないようだ。
そこでバッチ学習に変更してみたらどうかと思ったが、そもそもメモリの確保に失敗しているようなので変更後も結果は変わらず。
空き領域が8GBくらいあれば実行できたかもしれないが、GTX1060(6GB)2枚とか、GTX1080Ti(11GB)とかのレベルが必要と考えると今は現実的ではない。

■ やむなくCIFAR-10チュートリアルを見る

公式にCIFAR-10のチュートリアルがあるようなので、そちらを見て勉強する。
学習にはすごく時間がかかるようなので、とりあえず動かしておく。

ソースコードを一通り眺めたが、distorted_inputsの処理をCPUに固定したり、uint8と他の型を使い分けるなどの工夫でGPUの負荷が軽減されていた。

11月2日

■ クラスやモジュールを活用したPythonプログラムを作成する

CIFAR-10チュートリアルのソースコードを見ていて、読めるには読めるけれどもクラスやモジュールを使ったプログラムは書いたことが無いことに気がついた。
機能的には簡単なもので、いくつかのファイルに分かれたプログラムを書いてみる。

■ Python辞書を操作するプログラム

コマンドで指示をして、キーや値を与えるとPythonの辞書を操作するプログラムを書いてみた。

  • セーブ:上書き、新規(同名のファイルがあればリネーム)
  • ロード:新規、(現在の辞書に)追加
  • キー:追加(すでに存在していれば更新しない)、削除、検索
  • 値:更新
  • 辞書:クリア、表示

SQLっぽいイメージで最低限機能を揃えた。
今回の例では1つの辞書しか扱わない設定だったので、モジュールで十分なケースだった。クラスを活用すれば、辞書データを持ち、それを操作するメソッドを持つオブジェクト単位での操作となったか。

今回は単なるPythonの復習をしただけなのでコードは載せないが、正直あまりよいコードではないと思う。次のコーディングに反省を活かしたい。


アバター画像
フォーム作成クラウドサービス「Formzu(フォームズ)」を運営しているフォームズ株式会社です。
オフィスで働く方、ホームページを運営されている皆様へ
仕事の効率化、ビジネススキル、ITノウハウなど役立つ情報をお届けします。
  • 【初めての方へ】Formzuで仕事の効率化
  • 【初めての方へ】メールフォームについて