Translate

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をひたすら読んでいたときにリファレンスを読んだから..だったりします..


0 件のコメント:

TensorFlow 1.3.0 rc0 がリリースされたことを知る

今朝、Twitterのタイムラインに #TensorFlow 1.3.0rc0 has been released! Please see the release notes for details on added features and changes: htt...