Translate

2016年12月27日火曜日

一切コーディングしないでWatson NLCを使った質疑応答デモを作る

TesnorFlow で自前の分類器をつくろうと考えていたが
それと比較するものも必要かな..と思い
Watson の Natural Language Classifier を使ったデモを作った。

前提として
・有効なBluemixアカウントがあること
・Bluemixへアクセス可能なPC
・ブラウザ、Excel、ShiftJIS→UTF-8変換できるツール(以下では秀丸を使用)
があることとする。

ここでは、某DBスペシャリストテキストを参考に
イチから学習データを作り、
DBに関する質問を投げると、テキストの該当ページヘ
誘導できるように分類(XXXセクションのYYY章)を出す
という分類器をつくることとする。

学習対象は
それこそ、歴史の教科書だって、数学の教科書だってかまわないけど
対象の文章を読んで理解し、その文章が答えになるような質問文を作成できれば
なんだってかまわない。

これができるのなら、
コーディングを一切知らなくても、
デモレベルなら誰でも作れてしまう

しかも、
BluemixではよくあるNode-REDすら使っていないゼロコーディング

以下、その手順。

----
・クライアントPC上でブラウザを起動、Bluemixコンソールへアクセス
・ユーザ名を入力し、Login押下
・パスワードを入力し、Login押下

・「サービス」を選択し、「ダッシュボード」を選択

・サービスの作成押下


・Watsonを選択




・「Natural Language Classifier」を選択



・作成押下


・Access the beta toolkit 押下





 ・Login with Bluemix を選択



・確認押下


・Add training data リンク選択


・以下の画面が出たら閉じずにこのままにする


・クライアントPC上でExcelを開く
・1列目に質問、2列目に分類名を記述する

 ・分類の種類はMAX150くらいとする(それ以上だとサチる)
 ・分類名は以下のルールで作成
  「<セクション番号をゼロ詰め3桁化><少番号をゼロ詰め2桁化><該当章を表す単語を適当につける>」
 ・1つの分類に最低10個の質問をつくること








・CSV形式でファイル保存する
  ShiftJIS形式で保存される



・秀丸エディタでCSVファイルを開く

・UTF-8形式で保存


・ブラウザのWatson Toolkit画面に戻り、アップロードアイコン(Create classifier右横)を押下


・UTF-8形式のCSVファイルを選択



・Create classification押下



・分類器に名前を適当につけてCreate押下


・しばらくたら、Traning dataリンクを選択し、Classifiersリンクを選択する


・CSVファイルデータが多いと以下の画面の状態になる




・以下の画面になったら「→」押下


・質問を記入し、Classify押下


・正解の分類を選択肢、次の質問を入力、Classify押下




・次の分類結果が表示される


・正解を選択し、次の質問を入力、CLassify押下


・正解を選択し、画面右のAdd to training data押下


・しばらく右ペインが処理中になり


・完了すると、トレーニングデータに3件追加される





-----

と、Watson Toolkit を使えばこんなに簡単に一括学習、1件学習、学習済み分類器の保存が可能だ。
あとはこの学習済み分類器を、適当なWebアプリからREST APIで呼び出せばいい。

ここからあとはコーディングが必要だが、デモならここまでのレベルで十分だ。

IBMのWatson NLCは現時点で日本語も使える。

が、
Watson APIの日本語版はソフトバンクで一括利用させるため
ソフトバンクの営業曰く来年の何処かのタイミングでIBMには日本語機能は
止めてもらうことになるらしい。
いつになるかは言っていなかった。

このへんキチンとIBMと話ししてるのかよくわからないが..

まあ、なんにせよToolkitを使えば
コーパス作成に集中することになり
とっても機械学習の実装がつまらなく^H^H シンプルになっている。

..良いのか悪いのかわからないけど..










2016年12月21日水曜日

TensorFlowは機械学習ビジネスの"フレームワーク"ではないかと邪推する

TensorFlowを始める最初に読んだのが

有山圭ニ著「TensorFlowはじめました」


だったのだけど、
機械学習はおろかPythonの知識すらなかったので
正直書いていることがよくわからなかった。

特になんで変数やPlaceholderをわざわざ定義しなきゃならないのか
Python自体の持つ変数で十分じゃないのか
とか
なんでわざわざsessionをおこしてrunするのか
Pythonの関数として定義させて呼び出せば
Pythonコードの読み方がトリッキーにならずに済むのに
とか
おもっていた。




p8にかかれていたオーバヘッドの絵(上の図はそれを模写したもの)が
その理由であることもなんとなくしかわからなかった。

がTensorFlowのGuide>How-Tosを読んでいくと
なんとなく意味がわかってきた。

それに気づいたのが情報機構主催の研修
「Chainerを利用したDeepLearningプログラムの仕組みとその作成・実装方法」
で講師だった新納浩幸教授がChainerが「フレームワーク」であると説明したときだった。

Chainerがフレームワークなら
TensorFlowもフレームワークになっているんじゃないのか
とおもい、やや強引だけどMVCフレームワークの代表格Struts2と比較する絵を
書いてみたら、意外と整理できた。






StrutsをJSPなどを無視してざっくり説明すると
Actionを実装した具現クラスをたくさん作って、
XMLで書いた定義ファイルに従って画面遷移させる
MVCフレームワークだ。

TensorFlowはというと、
C++でOperationの具現クラスを作って独自Operrationが
つくれるようになっている。
機械学習素人やいっちょかみレベルの開発者は
TensorFlowが提供する既存のOperationを使うだけなので
C++開発はほとんどしないが、
TensorFlow自体はすべてC++コードがメインだ。
Pythonからそれを呼び出してつかっているだけなのだけど、
Pythonコード上のリテラルを使ってしまうと
最初の絵の左側のように毎回処理が終わったらC++のレジスタ領域から
Pyhonの変数にもどさないといけない。
だけどtf.Variableを用意してこれをC++上のオブジェクトとしてしまえるようにすれば
Pythonの世界に戻ってこなきて良いわけだ。

XMLで計算グラフを定義してもいいけど、
機械学習のモデル設計者の立場からすると
ランタイム実行を何度も繰り返す主戦場をあたかも
Pythonの上で開発しているように見せかければいいわけか。

ただプログラミングに重心を持つ開発者からすると
Chainerのほうが行数も少なく済む。
グラフ定義しておいてそこで処理せず
セッション実行で初めて動き出すところに違和感を感じる。

けど、主戦場がC++でPythonはそれを定義するために使っている部分と
それを実行する部分をしっかり理解してコードを読み書きすれば
違和感は払拭できる。

ChainerはGPUを使う場合はnumpyではなくCuPyを使うけど
TensorFlowはtf.Variableを使うのでそんな必要はない。
かわりにrun関数の引数でPythonの世界で扱っていたデータを
C++の世界に送り出してやればあとは"データフロー"グラフで処理してくれる。

 tf.Variable名前空間を使えば、訓練済みの重さとバイアスを保存・復元も簡単、
訓練・自動評価・パラメータ更新系モジュールで完成したチェックポイントファイルを
ServingのリファレンスインプリメントであるServerCoreのようにしてやれば
最新の訓練済みパラメータの推論側のシステムへホットアップロードでき
CPU側でVariable管理、GPU側で学習や推論を動かす実装にして
推論側の高速化はQuantatizeでfloatの32bitを8bitに圧縮し処理で実現
Distributed TensorFlowKubernetes+docker
2017前半からGPUサポートするGoogle Cloud Platformで開発系本番系インフラを
つくることができる。
Deep Learningでレイヤが熱くなり混沌とするグラフ実装をスリム化する手段は
TF-Slimでなんとかして、
独自OperationはPython/C++両方でexposeするのでネーミングルールの注意まで
How-Tosに掲載されている..

TensorFlowのアーキテクチャを読んでいくと
ますますGoogleの機械学習/NN/DLに対する本気度がわかってくる..

ソフトウェアジャパン2016で人工知能学会会長の松原さんが
人工チノについては「日本は海外から1周以上周回遅れ」といっていたが
そのとおりかもしれない..

たしかに
国内企業産のChainerはバージョンアップも頻繁で
どんどん新しいモデルが表現できるようにして
先進技術をとりこんでいるのかもしれないが
フレームワークの目的範囲がTensorFlowよりも狭い気がする..


2016年12月13日火曜日

Dockerで上げていた Tensorflow コンテナの jupyter がいきなりパスワードを要求してきた

これまでDockerサーバとして使っていたPCのHDDを交換して再インストールし、
この上で gcr.io/tensorflow/tensorflow:latest-gpu

nvidia-docker run -it -p 8888:8888 -p 6006:6006 gcr.io/tensorflow/tensorflow:latest-gpu

で起動して、

http://xxx.xxx.xxx.xxx:8888/

をブラウザでひらいたら..



あれ?パスワード入力画面がでてきた..

コマンド一発起動なので、パスワードなんて何も指定してないんだけど..



-itで実行していたのでコンソール側を見ていると..

The Jupyter Notebook is running at: http://[all ip addresses on your system]:8888/?token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

ってメッセージが出ていた。


あれ?なんかGETパラメータのtokenがついてる..

で、このGETパラメータ付きのURLで入ってみると、パスワード画面が出てこなくなった..

でもこれだと -d オプションで起動したらtokenがわからなくなる..

とおもって、Dockerfile.gpuを見てたら
jupyter_notebook_config.pyCOPYしているところを発見。

で、このコードを見てみると
環境変数PASSWORDにパスワード文字列を指定できるようになっていた。

と、いうことで

nvidia-docker run -e PASSWORD=password -d -p 8888:8888 -p 6006:6006 gcr.io/tensorflow/tensorflow:latest-gpu

と、起動すればパスワードが「password」になるようになった。



..たのむからREADME.mdくらいには書いておいてほしかったなあ..




p.s.

タグ名に devel とついているイメージのDockerfileCMD /bin/bash となっているので、基本 -it オプションを付けてコマンドラインを有効にして使うことを前提にしています。このため -d オプションで起動するとattach/exec以外なにもできなくなります。

タグ名に devel がついていないイメージのDockerfileCMD /run_jupyter.sh となっており、Jupyterが起動します。このため -it を付けて実行すると、デフォルトトークン(パスワード代わりにつけるHTTP GETパラメータ付きURL)がコンソールに出ますが、シェル操作はできません。 -d オプションを代わりに付ければ、デーモン起動になります。

なお、コンテナ側ポート番号 8888 をホスト側ポート番号 9999 で使いたい場合は、 -p 9999:8888 とつけてください(1つのDockerサーバで複数上げる場合がある)。

ただ..Docker Hub上のイメージの仕様がまた変わることもあります...
(仕様がかわったら、コメント欄に教えていただけると、ありがたいです)

2016年12月12日月曜日

CUDA Toolkit 8.0 をUbuntu Server16.04LTSへインストールしようとすると、Nouveauカーネルドライバが邪魔と言われ、失敗する

CUDA対応の黒箱 GeForce GTX 750 Tiを刺したPCに
Ubuntu Server 16.04LTSを最小構成でインストールして、
apt update / apt -y upgrade した後、
openssh server, vuftpd, gcc, g++, make を個別にインストールし、
NVIDIAサイトから
cuda_8.0.44_linux.run
をダウンロードして、root権限で

sh cuda_8.0.44_linux.run


を実行したら


以下のようなメッセージが出てインストールに失敗した..

Installing the NVIDIA display driver...
A system reboot is required to continue installation. Please reboot then run the installer again. An attmept has been made to disable Nouveau. If this message persists after reboot, please see the display driver log file at /var/log/nvidia-installer.log for more information.

===========
= Summary =
===========

Driver:   Reboot required to continue
Toolkit:  Installation skipped
Samples:  Installation skipped

To uninstall the NVIDIA Driver, run nvidia-uninstall

Logfile is /tmp/cuda_install_13653.log


"Please reboot"とあるので、リブートして再度root権限で
sh cuda_8.0.44_linux.run
を実行してみたが、まったく同じメッセージが出てきた..

で同じくメッセージに書かれている/var/log/nvidia-installer.log を開いてみると、
以下のエラーメッセージが出ていた。

ERROR: The Nouveau kernel driver is currently in use by your system.  This driver is incompatible with the NVIDIA driver, and must be disabled before proceeding.  Please consult the NVIDIA driver README and your Linux distribution's documentation for details on how to correctly disable the Nouveau kernel driver.
WARNING: One or more modprobe configuration files to disable Nouveau are already present at: /etc/modprobe.d/nvidia-installer-disable-nouveau.conf.  Please be sure you have rebooted your system since these files were written.  If you have rebooted, then Nouveau may be enabled for other reasons, such as being included in the system initial ramdisk or in your X configuration file.  Please consult the NVIDIA driver README and your Linux distribution's documentation for details on how to correctly disable the Nouveau kernel driver.
-> For some distributions, Nouveau can be disabled by adding a file in the modprobe configuration directory.  Would you like nvidia-installer to attempt to create this modprobe file for you? (Answer: Yes)
-> One or more modprobe configuration files to disable Nouveau have been written.  For some distributions, this may be sufficient to disable Nouveau; other distributions may require modification of the initial ramdisk.  Please reboot your system and attempt NVIDIA driver installation again.  Note if you later wish to reenable Nouveau, you will need to delete these files: /etc/modprobe.d/nvidia-installer-disable-nouveau.conf
ERROR: Installation has failed.  Please see the file '/var/log/nvidia-installer.log' for details.  You may find suggestions on fixing installation problems in the README available on the Linux driver download page at www.nvidia.com.

エラー:Nouveauカーネルドライバが現在システムで使用されています。このドライバはNVIDIAドライバと互換性がありませんので、先に進む前に無効にする必要があります。 Nouveauカーネルドライバを正しく無効にする方法の詳細については、NVIDIAドライバのREADMEおよびLinuxディストリビューションのドキュメントを参照してください。
警告:Nouveauを無効にするための1つ以上のmodprobe設定ファイルは、すでに/etc/modprobe.d/nvidia-installer-disable-nouveau.confにあります。これらのファイルが書き込まれてからシステムを再起動してください。再起動した場合は、システム初期ラムディスクやX設定ファイルに含まれるなどの理由でNouveauが有効になることがあります。 Nouveauカーネルドライバを正しく無効にする方法の詳細については、NVIDIAドライバのREADMEおよびLinuxディストリビューションのドキュメントを参照してください。
- >いくつかのディストリビューションでは、modprobe設定ディレクトリにファイルを追加することでNouveauを無効にすることができます。 nvidia-installerがこのmodprobeファイルを作成しようとしますか? (回答:はい)
- > Nouveauを無効にするための1つ以上のmodprobe設定ファイルが記述されています。いくつかのディストリビューションでは、これはNouveauを無効にするのに十分かもしれません。他のディストリビューションでは初期のRAMディスクを変更する必要があります。システムを再起動し、NVIDIAドライバのインストールを再度試みてください。あとでNouveauを再度有効にしたい場合は、これらのファイルを削除する必要があります:/etc/modprobe.d/nvidia-installer-disable-nouveau.conf
エラー:インストールに失敗しました。詳細については、ファイル '/var/log/nvidia-installer.log'を参照してください。 Linuxドライバのダウンロードページ(www.nvidia.com)にあるREADMEに、インストールに関する問題を修正するための提案があります。


どうも「Nouveau カーネルドライバ」が邪魔らしく
これをアンインストールせよ
ということらしい..

..ったく、Xを入れてない最小構成なのに
なんでNouveauグラフィックスドライバが入るのか..
イランお世話やっちゅーの..

/etc/modprobe.d/nvidia-installer-disable-nouveau.conf にカーネルオプションができるので、これを再起動時反映してもらうためにroot権限で

update-initramfs -u


を実行して再起動すると..以下のメッセージがでて、
とりあえずうまくいった..


===========
= Summary =
===========

Driver:   Installed
Toolkit:  Installed in /usr/local/cuda-8.0
Samples:  Installed in /root, but missing recommended libraries

Please make sure that
 -   PATH includes /usr/local/cuda-8.0/bin
 -   LD_LIBRARY_PATH includes /usr/local/cuda-8.0/lib64, or, add /usr/local/cuda-8.0/lib64 to /etc/ld.so.conf and run ldconfig as root

To uninstall the CUDA Toolkit, run the uninstall script in /usr/local/cuda-8.0/bin
To uninstall the NVIDIA Driver, run nvidia-uninstall

Please see CUDA_Installation_Guide_Linux.pdf in /usr/local/cuda-8.0/doc/pdf for detailed information on setting up CUDA.

Logfile is /tmp/cuda_install_1590.log

..じゃ、cuDNNいれるかな..

2016年12月4日日曜日

サンプルコード word2vec_basic.py をガッツリ読んでみる

※本記事は、TensorFlow Advent Calendar 2016 参加記事(2016/12/04)です。


先日ふとTVを見ていたら
池上彰さんまで "AI" について語っておられました..

2016/11/23 TBS
池上彰のニュース2016総決算!  今そこにある7つの危機を考える!ニッポンが“危ない”


番組ではシンギュラリティについての話が出ていて
他国と比べ日本は仕事奪われる可能性が一番高いとでてました。

国は少子化対策で既婚家族の優遇ではなく
ひょっとしたらAI開発にもっと公金をかけてくるかも..

そうしたら、国内大手IT企業はウハウハ^H^H
いかんいかん..脱線しそうになった..


そうでなくて、
今回3度めと言われている人工知能ブームの波は
もしかしたらビッグ・ウェンズデー級かもしれない
..ってことを言いたかったんですよ..


なにより..
普段は企業向けシステムの開発をしている
AI初心者の私ですら
ニワカに人工知能をはじめるようになったわけですし..

..話を戻します。


今自分の働いている職場で人工知能をあつかうとすると..
いわゆる国内の企業内部で使うシステムが多いので
..やはり画像より社内文書やDBに入った定形データなどが
対象データになりそう..
AIをあつかうにしてもテキスト系の処理がメインになりそうかも..


ということで、
TensorFlowチュートリアルについて
Language and Sequence Processing (いわゆる自然言語処理系)の
最初にあるVector Representations of Words を中心に読んでみることにしました。

以下の引用は、このセクションの概要を翻訳したものです。

単語のベクトル表現 (Vector Representations of Words)
このチュートリアルでは、単語をベクトルとして表現する方法(ワード埋め込みと呼ばれる)を学ぶことがなぜ有用なのかを説明します。 埋め込み学習のための効率的な方法としてword2vecモデルを紹介します。 また、Noise-Contrasiveな訓練法の背後にある高度な詳細(訓練埋め込みにおける最近の最大の進歩)もカバーしています。
セクション内でword2vec という、
自然言語の文章をニューラルネットワークにかませるためによく使われる
単語をベクトル表現(浮動小数点の配列)化する手法の一つを
紹介しています。


..が、
チュートリアルのどこかの章を一つでも読んだ方は
わかっていると思いますが..
pythonだけでなく
ある程度AI用語だけでなく
数学の知識(微積、線形代数、行列計算、確率統計、計算機による近似系アルゴリズム)
が必要です。

知識のない人間が
字面だけを必死に読んでも..
ちっとも理解できない..


なかなか前に進まない..


このままでは、イカン..というか拉致あかんかも..
...と思っていたのですが、
冒頭のハイライトセクションに以下のように書かれてました。

..このチュートリアルの後半でコードを実行しますが、まっすぐにダイビングしたい場合は、tensorflow/examples/tutorials/word2vec/word2vec_basic.py の最小限の実装を自由に見てください。この基本的な例には、ダウンロードに必要なコードが含まれています いくつかのデータは、それを少し訓練して、結果を視覚化しています。 basicバージョンの読み込みと実行に慣れたら、 tensorflow/models/embeddings/word2vec.py にすすむことができます。これは..



ようは
Google側もそんなちんぷんかんぷんなやつがくることは
十分承知で、そんな人は
このチュートリアル本文を読み始める前に、
ある程度 word2vec_basic.pyよんでアタリつけとけよ
と冒頭にキチンと書いているのです。


なにわともあれコード word2vec_basic.py を頭から順番にまず読んでみることにしました。

以下のコードは
word2vec_basic.py に
私が読んだ際に日本語をコメントをありったけつけたものです。

なお、
以下のコードをコピー&ペーストしてJupyterとかで動かす場合は
文字コードとか全角空白とかに気をつけてください。


# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# 著作権 2015年、 TensorFlow 著者が著作権を所有しています。
#
# Apache ライセンス バージョン2.0 (以降、ライセンスと略す):
# ライセンスに従わずにこのファイルを使用してはいけません。
# ライセンスのコピーを以下のURLから取得してください。
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# 準拠法により要求される場合、もしくは同意の記述がある場合を除き、
# 基本的に、明示もしくは暗黙の、いかなる種類の保証もしくは条件なしで
# 「現状のまま」配布されるライセンス下において、ソフトウェアを配布してください。
# ライセンス下での支配下にある許可と制限については、特定の言語のライセンスを
# 参照のこと。
# ==============================================================================
# 実行方法
#    export HTTP_PROXY=http://[[proxy server]]:[[proxy port]] # proxy有りの場合
#    export HTTPS_PROXY=http://[[proxy server]]:[[proxy port]] # proxy有りの場合
#    python word2vec_basic.py
#    結果は、標準出力へ


from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import collections
import math
import os
import random
import zipfile

import numpy as np
from six.moves import urllib
from six.moves import xrange  # pylint: disable=redefined-builtin
import tensorflow as tf

# Step 1: Download the data.
# データをダウンロードする

# ダウンロード元 URL

url = 'http://mattmahoney.net/dc/'

# メソッド定義 maybe_download
#     ファイルを所定のサイトからtext8.zipをダウンロードする
#     未ダウンロードの場合、ダウンロードし、サイズが正しいか確認する
#     正しくない場合raiseされる
# 引数:
#    filename       ファイル名
#    expected_bytes 想定サイズ
# 戻り値:
#    filename ファイル名

def maybe_download(filename, expected_bytes):
  """Download a file if not present, and make sure it's the right size."""
  if not os.path.exists(filename):
    filename, _ = urllib.request.urlretrieve(url + filename, filename)
  statinfo = os.stat(filename)
  if statinfo.st_size == expected_bytes:
    print('Found and verified', filename)
  else:
    print(statinfo.st_size)
    raise Exception(
        'Failed to verify ' + filename + '. Can you get to it with a browser?')
  return filename
# メソッド定義 maybe_download:end

# データセットをダウンロードする
# 戻り値はダウンロードファイル text8.zip のパスとなる。
# 想定サイズ31,344,016バイトではない場合はraiseし終了する

filename = maybe_download('text8.zip', 31344016)


# メソッド定義 read_data
#    単語リスト(複数の文を1行にまとめたもの、カンマやピリオドやクォートなどなし、
#    単語間は空白1個のみとし、すべてが1行となっている)としてzipファイルに
#    格納されている最初のファイルを展開する
# 引数:
#    filename   ダウンロードファイル(text8.zip)パス
# 戻り値:
#    data       単語リストのデータ

def read_data(filename):
  """Extract the first file enclosed in a zip file as a list of words"""
  with zipfile.ZipFile(filename) as f:
    data = tf.compat.as_str(f.read(f.namelist()[0])).split()
  return data
# メソッド定義 read_data:end

# ダウンロードしたzipファイル内最初のファイルのみデータ取得

words = read_data(filename)
# 標準出力へファイルサイズを表示
print('Data size', len(words))

# Step 2: Build the dictionary and replace rare words with UNK token.
# 辞書を構築し、まれにしか登場しない単語をUNKトークンと入れ替える

# 基本語彙とは、対象データ上のすべての単語のうち最頻出順に並べて
# 先頭から(vocabulary_size - 1)番目までの単語と単語ごとに一意に割り当てられたID
# のペアをさす。

vocabulary_size = 50000

# メソッド定義 build_dataset
#    1行の文字列化された元データ(単語と空白の数珠つなぎ)から
#    基礎語彙(2次元配列)、UNK化済みdict形式元データ、その逆構成dict形式データ、
#    UNKカウント数を作成する
# 引数:
#    words              取り込んだ元データ(単語と空白の数珠つなぎ)
# 戻り値:
#    data               基礎語彙以外UNKトークン化された単語IDの1次元配列(リスト)
#                       id:   基礎語彙単語(1,2,..,vocabulary_size-1)、
#                             基礎語彙単語ではない場合-1
#    count              UNKへ差し替えた単語数(形式:2次元配列[0][1])
#    dictionary         基礎語彙(形式:dictクラス(key/value形式))
#                       key:   単語(文字列)
#                       value: ID(1,2,.. ,vocabulary_size-1)
#    reverse_dictionary 基礎語彙をkey/valueを逆にしたもの(形式:dictクラス(key/value形式))
#                       key:   ID(1,2,.. ,vocabulary_size-1)
#                       value: 単語(文字列)

def build_dataset(words):
  # 基本語彙の初期化(先頭のUNKトークン1個のみ)
  count = [['UNK', -1]]
  # 文字列words(単語と空白の数珠つなぎ)をCounterクラス化して登場頻度の多いものから順に
  # vocabulary_size - 1件とりだし、countへ

  count.extend(collections.Counter(words).most_common(vocabulary_size - 1))
  # 基本語彙格納用変数を初期化(空のdict)
  dictionary = dict()
  # 最頻度単語vocabulary_size - 1件ループ;開始
  for word, _ in count:
    # 基本語彙格納用変数に追加、値は直前の語彙数(1,2,...,(vocabulary_size - 1))
    # この値が単語のIDとなる

    dictionary[word] = len(dictionary)
  # 最頻度単語vocabulary_size - 1件ループ;終了

  # 基礎語彙以外UNK化した2次元配列格納用変数の初期化(空リスト)
  # 要素は単語ではなくID値(UNKの場合は0)

  data = list()
  # UNKトークン追加数カウンタ初期化(先頭の1件は除く)
  unk_count = 0
  # 元データ単語ループ:開始
  for word in words:
    # 基本語彙内に単語が存在する場合
    if word in dictionary:
      # 対象単語のIDをセット
      index = dictionary[word]
    # 基本語彙内に単語が存在しない場合
    else:
      # UNKトークン位置(0)をセット
      index = 0  # dictionary['UNK']
      # UNKカウンタ加算
      unk_count += 1
    # リスト要素としてIDもしくは0を格納
    data.append(index)
  # 元データ単語ループ:終了

  # UNKカウント数を2次元配列[0][1]化
  count[0][1] = unk_count
  # 基礎語彙のkey/valueを逆転させ(ID値、単語文字列)として格納
  # ここのzipは圧縮の意味ではなく複数リストのインデックス順に配列化する

  reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
  # 戻り値を返却
  return data, count, dictionary, reverse_dictionary
# メソッド定義 build_dataset: end

# メソッド build_datasetを呼び出しwordsから以下の値を取得
#   data       基礎語彙以外UNKトークン化された単語IDの1次元配列(リスト)
#   count      UNKへ差し替えた単語数(形式:2次元配列[0][1])
#   dictionary 基礎語彙(形式:dictクラス(key/value形式))
#   reverse_dictionary 基礎語彙をkey/valueを逆にしたもの(形式:dictクラス(key/value形式))

data, count, dictionary, reverse_dictionary = build_dataset(words)

# 変数wordsをコレクション処理対象に
del words  # メモリ削減するためのヒント

# 基本語彙の先頭5件をサンプル表示
# 1件目がUNKなので実質4件

print('Most common words (+UNK)', count[:5])

# 基本語彙の逆dictが正しいか確認するため以下のサンプルを表示
# ・元データwordsの先頭10件の単語のIDを列挙
# ・先の10件のIDをreverse_dictionaryを使って元の単語に復元して列挙

print('Sample data', data[:10], [reverse_dictionary[i] for i in data[:10]])

# 0に初期化
data_index = 0


# Step 3: Function to generate a training batch for the skip-gram model.
# skip-gram モデルのための訓練バッチを生成する関数を定義する


# メソッド定義 geterate_batch
#     与えられた引数に従って、
#     batch_size個の訓練用インプットデータ(batch)とそれらの正解データ(labels)を作成する。
#   引数:
#     batch_size  訓練データの要素数
#     num_skips   訓練データ要素の分割数
#     skip_window 訓練対象となるdata要素位置
#   戻り値:
#     batch       訓練用インプットデータ(要素数batch_sizeの配列)
#                 値は、以下のようにbatch_size個の要素をnum_skip個に区分けして、
#                 data[skip_window],..  (num_skips*2+1個続く)..,data[skip_window],
#                 data[skip_window+1],..(num_skips*2+1個続く)..,data[skip_window+1],
#                           :
#                 data[dkip_window+(batch_size/num_skips -1)],..(num_skips*2+1個続く)
#                                    ..,data[dkip_window+(batch_size/num_skips -1)]
#     labels      訓練用インプットデータの正解データ([batch_size, 1]形式の2次元配列)
#                 値は以下の通り
#                 labels[0][0]=
#                               {data[skip_window-1],data[skip_window],  data[skip_window+1]}
#                               の中からランダム1個抽出
#                 labels[1][0]=
#                               {data[skip_window],  data[skip_window+1],data[skip_window+2]}
#                               の中からランダム1個抽出
#                      :
#                 labels[batch_size-1][0]=
#                              {data[dkip_window+(batch_size/num_skips -1)-1],
#                               data[dkip_window+(batch_size/num_skips -1)],
#                               data[dkip_window+(batch_size/num_skips -1)+1]}
#                               の中からランダム1個抽出

def generate_batch(batch_size, num_skips, skip_window):
  # グr-バル変数参照宣言
  global data_index
  # batch_sizeがnum_skipsで割り切れなければAssertionErrorをスロー
  assert batch_size % num_skips == 0
  # num_skips が skip_windowの2倍を超える場合、AssertionErrorをスロー
  assert num_skips <= 2 * skip_window
  # (batch_size)形式で、各要素がnp.int32形式の配列を初期化
  # 実行すると、ここではint32の8要素1次元配列となる

  batch = np.ndarray(shape=(batch_size), dtype=np.int32)
  # (batch_size, 1)形式で、各要素がint32形式の配列を初期化
  # 実行すると、ここではint32の(8, 1)形式の2次元配列となる

  labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
  # ランダム値を出す
  span = 2 * skip_window + 1 # [ skip_window target skip_window ]
  # 最大長 span の deque クラスを生成
  # deque の動きについては以下のURLを参照
  # http://docs.python.jp/2/library/collections.html

  buffer = collections.deque(maxlen=span)

  # span回繰り返しループ:開始
  # buffer(deque形式)へ全単語のIDを順番に挿入する

  for _ in range(span):
    # dataのdata_index番目の要素を buffer(deque形式) へ追加
    # data:基礎語彙以外UNKトークン化された単語IDの1次元配列(リスト)

    buffer.append(data[data_index])
    # data_index 加算
    # len(data): 全単語数

    data_index = (data_index + 1) % len(data)
  # span回繰り返しループ:終了

  # ループ終了時点
  # ・data_index には data_index + span が格納
  # ・bufferには、data内の単語をdata_index番目からspan件切り出し格納


  print('start')
  # i を 0 から( batch_sizeをnum_skipsで切り捨て除算した数 - 1) までループ:開始
  for i in range(batch_size // num_skips):
    # bufferの真ん中のターゲットラベル
    target = skip_window  # target label at the center of the buffer
    # ターゲット回避対象配列の初期化:skip_windowのみ
    targets_to_avoid = [ skip_window ]
    # j を 0 から (num_skips - 1) までループ:開始
    for j in range(num_skips):
      # ターゲットがターゲット回避対象である場合
      while target in targets_to_avoid:
        # ターゲットを再設定:0~(span-1)の間のint値をランダムセット
        target = random.randint(0, span - 1)
      # 現ターゲットをターゲット回避対象に追加
      targets_to_avoid.append(target)
      # bufferにはdata[i*num_skip-1]..data[i*num_skip+1]が格納されている
      # batch[i*nuk_skips+j]にbufferのskip_window番目を格納
      # → iループの1周内は常に同じ値になる

      batch[i * num_skips + j] = buffer[skip_window]
      # labels[i*nuk_skips+j,0]にbufferから1件無作為抽出し格納
      # → iループの1周内は常に同一範囲のランダム選択値になる

      labels[i * num_skips + j, 0] = buffer[target]
   
    # j を 0 から (num_skips - 1) までループ:終了
   
    # bufferの最後にdata[data_index]を加え、先頭(buffer[0]相当)を削除し、
    # 常に maxlength = 3 (span) 状態にする
    # → bufferに入っているdata要素値をdataを右に1つづらす

    buffer.append(data[data_index])
    # data_index加算
    data_index = (data_index + 1) % len(data)
  # i を 0 から( batch_sizeをnum_skipsで切り捨て除算した数) までループ:終了
  return batch, labels
# メソッド定義 geterate_batch: end

# 試しに、訓練用データ、訓練用正解データを取得して、表示する
# →ここでの訓練バッチは、動作を確認してもらうためだけのものである

batch, labels = generate_batch(batch_size=8, num_skips=2, skip_window=1)
for i in range(8):
  print(batch[i], reverse_dictionary[batch[i]],
      '->', labels[i, 0], reverse_dictionary[labels[i, 0]])

# Step 4: Build and train a skip-gram model.
# skip-gram モデルをビルドし訓練する

# バッチサイズ(要素数)

batch_size = 128

# 埋め込みベクトルの次元
embedding_size = 128

# 左右どのくらいの単語数を考慮に入れるか
skip_window = 1

# ラベル生成のためのインプットデータを何回再利用するか
num_skips = 2

# 最近の近傍をサンプリングするランダムな確認セットを取り出します。
# ここでは、構造的に最頻出となる、ID値の小さい単語への確認サンプルに制限します。

# 類似性評価のためのランダム単語セット

valid_size = 16     # Random set of words to evaluate similarity on.

# 配布の先頭にあるdev サンプルのみ取り出す
valid_window = 100  # Only pick dev samples in the head of the distribution.

# 評価用インプット
# リプレースなしで、 0 ~ valid_window (100) までの範囲のランダム値で
# valid_size個の要素を持つ1次元配列を生成

valid_examples = np.random.choice(valid_window, valid_size, replace=False)

# サンプリング時のネガティブサンプル数
num_sampled = 64    # Number of negative examples to sample.

# TensorFlow データフローグラフを生成
graph = tf.Graph()

# TensorFlow グラフ定義: start
# デフォルトグラフをオーバライドして定義を記述する
with graph.as_default():
   
  # 入力層

  # インプットデータ
  # 訓練用インプットデータ用 placeholder ノードを生成

  train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
  # 訓練用インプットデータの正解データ用 placeholder ノードを生成
  train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
  # valid_examples: 範囲0~100内のランダムなint値の1次元配列(要素数:16)
  # 評価用インプット valid_examples を TensorFlow 定数化
  valid_dataset = tf.constant(valid_examples, dtype=tf.int32)

  # Ops and variables pinned to the CPU because of missing GPU implementation
  # GPU実装割り当て失敗時のために、CPUへピン留めされたopsとvariables

  # CPUへのピン留め: start

  with tf.device('/cpu:0'):
       
    # 隠れ層(埋め込み層)

    # Look up embeddings for inputs.
    # インプットの埋め込み表現を学習する
   
    # [vocabulary_size(基本語彙数), embedding_size(埋め込みベクトルの次元数)] 形式の配列に
    # -1.0から1.0までの範囲のランダム浮動小数点値(一様分布)を格納し、
    # TensorFlow Variable化

    embeddings = tf.Variable(
        tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
    # tf.nn.embedding_lookupは埋め込み表現を学習させるためのTensorFlowが提供する関数
    # https://www.tensorflow.org/versions/r0.11/api_docs/python/nn.html#embedding_lookup
    # train_inputsに従ってembeddingsを並べ替える

    embed = tf.nn.embedding_lookup(embeddings, train_inputs)

   
   
   
    # Construct the variables for the NCE loss
    # NCE(Noise Contrasive Estimation:ノイズ対照評価)損失による変数の構築

    # Negative Sampling(負例サンプリング)
    # 基本語彙から{P_n(w)}という確率分布で生成されるサンプルを集めたもので学習させる
    # 計算量が爆発してしまわないように、計算機的に効率よく処理できる
   
    # ここでは Negative Sampling(負例サンプリング)に非常によく似た「NCE損失」を使用する
    # TensorFlowにはこのNCE損失を関数として提供(tf.nn.nce_loss)している
    # NCE損失関数を処理するに、重み (nce_weights) とバイアス (nce_biases) を定義する
   
    # nce_weightsを
    # [vocabulary_size(基本語彙数), embedding_size(埋め込みベクトルの次元数)] 形式の
    # 配列に構成し、
    # 値を標準偏差が1.0/(埋め込みベクトルの次元数の平方根)までのランダム値で初期化
    nce_weights = tf.Variable(
        tf.truncated_normal([vocabulary_size, embedding_size],
                            stddev=1.0 / math.sqrt(embedding_size)))
    # nce_biases を
    # [vocabulary_size(基本語彙数)]形式の配列に構成し、値をすべて0として初期化

    nce_biases = tf.Variable(tf.zeros([vocabulary_size]))
  # CPUへのピン留め: end

  # Compute the average NCE loss for the batch.
  # tf.nce_loss automatically draws a new sample of the negative labels each
  # time we evaluate the loss.
  # batch におけるNCE損失平均を計算
  # NCE損失を計算(tf.nn.nce_loss())し、平均(tf.reduce_mean())をとる
  # 最適化例;
  # tf.nn.nce_loss() → tf.nn.sampled_softmax_loss()

  loss = tf.reduce_mean(
      tf.nn.nce_loss(nce_weights, nce_biases, embed, train_labels,
                     num_sampled, vocabulary_size))

  # Construct the SGD optimizer using a learning rate of 1.0.
  # NCE損失平均(loss)最小化するようにが確率的勾配降下法(SGD)を使って最適化
  # 学習率を1.0として実行
  # 他のオプティマイザ:
  #   AdaGrad法:        tf.train.AdagradOptimizer()
  #   モメンタム法:     tf.train.MomentumOptimizer()
  #   Adam法:           tf.train.AdamOptimizer()
  #   Follow the Regularized Leader:
  #                     tf.train.FtrlOptimizer()
  #   学習率の調整を自動化したアルゴリズム:
  #                     tf.train.RMSPropOptiomiser()

  optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)

  # Compute the cosine similarity between minibatch examples and all embeddings.
  # ミニバッチサンプリングとすべてのembeddingsとの間のcos類似度を計算

  # cos類似度とは、単語ベクトルを比較する際に使用する類似度計算手法
  # 1に近いと類似し、0に近いと似ていないことになる
  # 正規化されたベクトル間の計算は積算で済むので、ここでは
  # embeddingsをノルム平均で割って正規化してから掛け算(tf.matmul())している

  # embeddings の 2次平均ノルムを計算
  # embeddingsを1階化して各要素を2乗し合計をとり平方根を計算する

  norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
  # 2-ノルムでembeddingsを割り、embeddingsを正規化
  normalized_embeddings = embeddings / norm

  # valid_dataset のid順に従って、normalized_embeddingsを並べ替える
  valid_embeddings = tf.nn.embedding_lookup(
      normalized_embeddings, valid_dataset)

  # 行列の掛け算を実行
  # 結果は類似度(1に近いと類似し、0に近いと似ていない)となる

  similarity = tf.matmul(
      valid_embeddings, normalized_embeddings, transpose_b=True)

  # Add variable initializer.
  # TensorFlow 変数のイニシャライザの追加

  init = tf.initialize_all_variables()

# TensorFlow グラフ定義: end


# Step 5: Begin training.
# 訓練の開始

# 訓練ステップ数

num_steps = 100001

# TensorFlow 計算グラフの実行: start
# 先に定義し生成した計算グラフを使うTensorFlow セッションを定義

with tf.Session(graph=graph) as session:
  # We must initialize all variables before we use them.
  # 使用前にすべての変数を初期化する

  init.run()
  print("Initialized")

  # 損失平均を計算するためのカウンタ変数を定義
  average_loss = 0

  # stepループ(0~num_steps-1): start
  for step in xrange(num_steps):
    # 本当に使用する訓練用インプット (batch_inputs) およびラベル (batch_labels) を生成する
    # ランダムを内部で使用しているので、毎回異なるデータで訓練される

    batch_inputs, batch_labels = generate_batch(
        batch_size, num_skips, skip_window)
    # 訓練用インプットおよびラベルをまとめる
    feed_dict = {train_inputs : batch_inputs, train_labels : batch_labels}

    # We perform one update step by evaluating the optimizer op (including it
    # in the list of returned values for session.run()
    # optimizerオペレーションを評価することにより一つの更新ステップを実行
    # (session.run()の戻り値リストも含め)
   
    # まとめた訓練データを渡してセッション実行
    # ループ内なので100001回実行される
    # optimizerを使いたい場合は _ を適当な変数に変えて操作する

    _, loss_val = session.run([optimizer, loss], feed_dict=feed_dict)
    # batch におけるNCE損失を加算
    average_loss += loss_val

    # ループが2000回展ごとに処理:start
    # NCE損失平均を2000回毎に出力する

    if step % 2000 == 0:
      # stepが初回でない場合: start
      if step > 0:
        # average_loss = average_loss / 20000
        average_loss /= 2000
      # stepが初回でない場合: end

      # The average loss is an estimate of the loss over the last 2000 batches.
      # 2000回毎にNCE損失平均を出力
      print("Average loss at step ", step, ": ", average_loss)
      # 損失平均カウンタを初期化

      average_loss = 0
    # ループが2000回展ごとに処理:end

    # Note that this is expensive (~20% slowdown if computed every 500 steps)
    # ループが10000回展ごとに処理:start
    # 評価のために類似語を8つ出力する

    if step % 10000 == 0:
      # 類似度(1に近いと類似し、0に近いと似ていない)を取得
      sim = similarity.eval()
      # iループ(0~valid_size-1(15)):start
      for i in xrange(valid_size):
        # サンプリングから先頭16個の単語を取り出す
        valid_word = reverse_dictionary[valid_examples[i]]
        # 類似語抽出数し出力
        top_k = 8 # number of nearest neighbors
        nearest = (-sim[i, :]).argsort()[1:top_k+1]
        log_str = "Nearest to %s:" % valid_word
        for k in xrange(top_k):
          close_word = reverse_dictionary[nearest[k]]
          log_str = "%s %s," % (log_str, close_word)
        print(log_str)
      # iループ(0~valid_size-1(15)):end
    # ループが10000回展ごとに処理:end


  # stepループ(0~num_steps-1): end

  # 正規化されたembeddingsを最後に1回だけ取得
  # 可視化セクションで使用する

  final_embeddings = normalized_embeddings.eval()

# TensorFlow 計算グラフの実行: end

# Step 6: Visualize the embeddings.
# 埋め込み (embeddings) の可視化

# メソッド定義 plot_with_labels
#    引数で与えられた座標位置に単語名をプロットしたPNGファイルを作成する
# 引数
#    low_dim_embs プロットする座標(x,y)群
#    labels       プロットする座標があらわしている単語群
#    filename     プロット結果として出力するファイル名
# 戻り値
#    なし

def plot_with_labels(low_dim_embs, labels, filename='tsne.png'):
  # ラベル数より座標数のほうが祝ない場合、エラーメッセージを表示し終了
  assert low_dim_embs.shape[0] >= len(labels), "More labels than embeddings"
  # 18インチ×18インチで作成する
  plt.figure(figsize=(18, 18))  #in inches
  # ラベル要素が存在する間ループ: start
  for i, label in enumerate(labels):
    # 座標位置へプロットし、右下へラベルを貼る
    x, y = low_dim_embs[i,:]
    plt.scatter(x, y)
    plt.annotate(label,
                 xy=(x, y),
                 xytext=(5, 2),
                 textcoords='offset points',
                 ha='right',
                 va='bottom')
  # ラベル要素が存在する間ループ: end

  # PNGファイル化する

  plt.savefig(filename)
# メソッド定義 plot_with_labels: end


# try句内で例外ImportErrorが発生したらexceptionへ

try:
  # t-SNE のインポート
  from sklearn.manifold import TSNE
  import matplotlib.pyplot as plt

  # t-SNE(次元削除)は、高次元データの次元を圧縮するアルゴリズムで、
  # 高次元データを可視化する際に使用される
  # ここでは final_embeddingsをt-SNEをもちいて圧縮する

  # sklearn.manifold.TSNE(次元削除)生成
  #  perplexity:    難しさを5~50の値で指定、
  #                 大きなデータセットの場合大きくする
  #  n_components:  埋め込み空間の次元数、ここでは2次元
  #  n_iter:        最適化繰り返し数、少なくとも200をセットする

  tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000)
  # プロットする要素位置(0~499)
  plot_only = 500
  # TSNE生成時に指定したパラメータに従って、引数で与えられた
  # final_embeddingsの一部を次元削除し座標化(x, y)する

  low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only,:])
  # プロット対象の単語セットを取得
  labels = [reverse_dictionary[i] for i in xrange(plot_only)]
  # プロッチした図をPNGファイルとして出力する
  plot_with_labels(low_dim_embs, labels)

# 例外ImportError発生時の処理
except ImportError:
  # エラーメッセージ表示
  print("Please install sklearn, matplotlib, and scipy to visualize embeddings.")

このコードは以下の処理を行っています。
・http://mattmahoney.net/dc/text8.zip からデータを取得
・データを単語ごとに分けIDを振る
・主要語彙50000個の辞書(ID←→単語)を作成、少しだけ表示
・バッチデータ作成用関数定義
・訓練用のバッチデータサンプルを作成、すこしだけ表示
・計算グラフ(Word2Vec本体)定義
・訓練用バッチデータ作成
・セッション実行(計算グラフ実行)
 ・2000回ごとに損失平均表示
 ・10000回ごとに類似後サンプル表示
・結果をグラフ化(PNGファイル出力)

本体は計算グラフにあるのはまちがいありません。
その前の訓練用バッチデータの正解データの作り方から想像すると
単語をベクトル(1次元配列)で表現するのですがそのベクトルに近い
単語は類似性が高いと判断されることを求めていることがわかります。

なのでWord2VecをXYZ分析風に書くと、Word2Vec とはどうも、

(目的)コンピュータが人の書いた文章を把握するために、
(手段)主要な語彙をもとに整数のベクトル空間化し、
(活動)ニューラルネットワークモデルベースの人工知能などの計算のインプットとして活用可能にする

システムらしいことがあらためてよくわかりました。


このWord2Vecにおけるベクトル空間化を行う方法として、
CBOWとSkip-Gramモデルの2種類の方法があるのですが、
大規模文書の場合はSkip-Gramモデルが有効であることがわかってきており、
現在では主にSkip-Gramモデルベースでの実装が多くなってきているそうです。

Skip-Gramモデルでは、
各単語の類似語が似た単語ベクトルとなるように調整して作成しています。

作成結果となるベクトル空間をWord Embeddings(単語埋め込み)とよびます。

どうもCBOWはコレとは異なるベクトル空間表現なのでアウトプット自体が全然別のものになるようです。


word2vec_basic.py は、
Skip-GramベースのWord2Vec実装例の一つで、
処理の流れを把握しやすいように、
コードを上から下へ読むことで流れを把握できるようになっています。

word2vec_basic.py 上の実装では、
次の図のように
3層のニューラルネットワーク(ディープラーニングではないらしい)
として実装されています。

word2vec_basic.py では、
特に隠れ層から出力層の前半までを
TensorFlow グラフとしてコード化しています。







ここから
入力層、隠れ層、出力層の順番に
処理の流れをおおまかに記述するのだけど、
実装はこの送別に綺麗に分割できないといころがあるため、
一部次の層の説明が混じったりしますが、ご容赦ください。

入力層


ダウンロードした文書テキストを処理して、以下のデータを加工します。




  • 辞書
    文章を数値化する際、word2vec_basic.pyでは、単語に分けそれぞれ一意なIDを割り振っている。Word Embeddingsを使う場合、出力層の後処理として元の単語に復元する必要が出てくるときに使用する辞書を作成する。辞書は最頻出単語のみに制限して作成する。
    • 正引き辞書
      IDから単語を取得できるデータ。出力層より後で使用する。
    • 逆引き辞書
      単語からIDを取得できるデータ。モデルのインプットを作成する際に使用する。




  • バッチデータ(訓練データセット)
    Word2Vecモデルを学習させるための学習データ。十分な類似度が得られるには、相当数のデータが必要となる。
    • 訓練インプット
      Word2Vecを訓練する際のインプットデータ。語彙の範囲内で作成する。学習させるために大量のデータが必要となる。
    • ラベルデータ
      訓練データに対する正解データ。ここでは文章の前後1文字づつ合計3文字の中からランダム抽出して作成する。


     


隠れ層

word2vec_basic.py では、
隠れ層をTensorFlowの計算グラフをもちいて実装しています。

word2vec_basic.py は
基本的に上から下へコードを読めば処理の流れが把握できるように実装しているのですが、
この計算モデル部分はそうではありません。



上図のように計算グラフの定義部と実行部分があります。

前者は、
空実装のTensorFlowグラフをオーバライドして定義しています。

後者は、
TensorFlowセッションを呼び出す実装になっています。
セッションを使って実行する際に、
計算グラフのノード名(もしくはオペレーション名)を指定します。
実行時には
そのセッション内において該当するノードを処理するために必要な
すべてのノード(入力となっているノードすべて)を
枝から根本へ向かって処理していきます。




上図の
青線内が訓練処理で、
訓練処理で完成したembeddingsの学習度合いを評価する処理が赤線部分となります。



出力層


先に実行結果を見法が早いので、
以下に標準出力の内容から切り出したモノを示しておきます。

word2vec_basic.py のセッション実行セクション内で
訓練と一緒に評価もループで繰り返しています。

そこで、訓練がどのように進んでいるかを
見えるようにするために標準出力に10000件実行するたびに
サンプルの単語に対して、
Word2Vecモデルがどのような単語を類似していると
判断しているかを見えるようにしてくれています。

以下のリストは、
サンプル単語"three" の類似語がどのように学習されていくかのみ
切り出したものです。

Nearest to three: conforming, bandwidth, sloths, recalls, solemnly, individual, euphemistic, computability,
Nearest to three: four, six, vs, nine, agave, aberdeen, gollancz, iupac,
Nearest to three: four, two, zero, six, five, nine, seven, eight,
Nearest to three: six, four, five, eight, seven, two, nine, zero,
Nearest to three: four, five, six, two, seven, eight, zero, one,
Nearest to three: four, five, six, two, seven, eight, nine, one,
Nearest to three: four, five, six, two, eight, seven, nine, one,
Nearest to three: four, five, six, two, seven, eight, nine, dasyprocta,
Nearest to three: six, four, five, seven, two, eight, one, dasyprocta,
Nearest to three: four, five, two, seven, six, eight, one, dasyprocta,
Nearest to three: four, five, two, six, seven, eight, nine, zero,

1行目はほぼ無学習状態なので、
"conforming" (適合性)とか、
"bandwidth" (帯域幅)とか
全く頓珍漢な類似語になっていますが、
最後はすべて数字を表す英単語になっており、
"three"という単語の理解はかなり正確であることがわかります。


ちなみに、
途中出てくる "dasyprocta" という単語は、
パカというげっ歯類動物の総称らしいです。

..なんでこんな単語が類似性有りと判断したんだか..

セッションによる処理(訓練と評価)が完了した後のコードは、
結果をわかりやすく表現するために、
Word Embeddings(ベクトル空間)を
(x, y)の2次元座標に次元削減して図示しています。

この部分エラーとなって実行されなかった方は、
Pythonパッケージsklearn matplotlib が不足しているという
メッセージがコンソールに出ていると思うので、
pip install sklearn
pip install matplotlib

してから再実行してください。
Python2系TensorFlow実行環境だと結構発生しているかも..
word2vec_basic.py でもこれらのパッケージがない人向けに
終盤にinport文はさんでるのは、
それを見越してのことだと思います。
word2vec_basic.pyの最後のtry句部分は、
embeddingsというベクトル空間を可視化するために
PNGファイルを作成しています。

必要なライブラリがインストール済みであれば、
実行したカレントディレクトリに以下のような tsne.png が作成されます。



意味として近い単語があつまり、
意味が異なるものは離れている状態であれば、
正しく学習されている状態といえる。



埋め込みベクトルembeddings は 128 次元あるので、
この次元をt-SNE(次元削減)をもちいて
2次元に変換しています。

いわばむりやり2次元グラフ上にプロットしているので
必ずしも学習度合いを正確に図にしているわけではないのですが
ある程度の指標にはなりうるのでしょうね。






..以上が、私が勝手にword2vec_basic.pyを解釈した内容..です。

実は、TensorFlowはおろか、Pythonコードを読むのも今回始めてだし、
途中で出てきた関数をいっこいっこ引き直したり調べたりして読んだので
解釈誤りはあるかもしれません
というかあると思います。

ので、もし誤りを見つけた人は
ぜひコメントに書いて教えてください



で、終わっても良いのですが、せっかくのAdvent Calendar参加記事ということで
蛇足になるかもしれませんが、word2vec_basic.pyを動かして、処理時間の計測結果をだしてみました。

使ったPCは、マウスコンピュータ製Corei7 860@2.80GHz、16GBメモリ、HDD500GBに
玄人志向 グラフィックボード NVIDIA GeForce GTX750Ti PCI-Ex16 LowProfile 2GB 補助電源なし
を載せて、Cuda8.0.22とDocker/NVidia-Docker1.12.1をインストールしたPCにて

  • nvidia-docker gpuありTensorFlowコンテナ(Python2.7.4)環境
  • docker cpuのみTensorFlowコンテナ(Python2.7.4)環境
  • Ubuntu Server 16.04LTSパッケージ上のPython3.5.1にGPU対応TensorFlowを導入した環境
  • コード修正:nympy(CPU) →CuPy(GPU)、"/cpu:0"(CPU) →"/gpu:0"(GPU)

で、word2vec_basic.pyをStepごとに経過時間を計測してみました。
結果は以下のグラフです。



..正直早くなることを期待していたのですが..

ほとんどかわらん..

しかもガッカリなのが最速が「素のDockerでCPUのみコンテナ」の場合ということ..

なんで物理マシンよりコンテナのほうが早いのよ..
ここらへんはDockerの仕組みに詳しくならないとわからんのかもしれんが..




一応GPU処理している部分が、かすかに分かるくらいの変化はグラフにでてきてくれて
ホッとしてますが..



..すみません、しょせんAmazonで1万円程度はらえば買えるGPUボードでは
これくらいなのかもしれません..


ただ..word2vec_basic.pyの次に使う(?) word2vec.py のコード自体を見ると
/cpu:0 が埋め込まれていて、
word2vec処理自体はどうもCPUのみで処理する
ことが多分正解なのだと思います。




なお、Chainerの研修を受けたことがあるのですが、
その際先生が
自然言語処理系は新技術が(LSTMとアノテーションくらいで)止まっていて
新たなブレークスルーが出てこない
とおっしゃってました。


もしかしたら、自然言語処理より画像処理が人工知能のメインストリームなのかも..

そういやChainer研修の先生の資料でもCPU/GPU比較はMNISTでやってたし..
そもそも自然言語処理で計測することが誤りで、
浮動小数点数の行列処理をガリガリやる画像系でないと効果あんまりないのかも..


..しょっぱいAdvent Calendar参加になってしまいました...







p.s.

Word2Vec モデルの核は実は
word2vec_basic.py上のコードではなく


tf.nn.embedding_lookup()



の中の処理自体だということです..



記事「TensorFlow の tf.nn.embedding_lookup() を調べる
を先に書いていたのは、
実はword2vec_basic.pyをひたすら読んでいたときにリファレンスを読んだから..だったりします..


既存アプリケーションをK8s上でコンテナ化して動かす場合の設計注意事項メモ

既存アプリをK8sなどのコンテナにして動かすには、どこを注意すればいいか..ちょっと調べたときの注意事項をメモにした。   1. The Twelve Factors (日本語訳からの転記) コードベース   バージョン管理されている1つのコードベースと複数のデプロイ 依存関係 ...