chainerで自然言語処理できるかマン

chainerで自然言語処理を勉強していくブログ

chainerを最新バージョンに入れなおす(1.19.0)

だいぶサボっていたら最終更新から半年ぐらい経っていて、chainerのバージョンも1.19(半年前に使っていたのは1.6)とかになってしまっていたので、最新版に入れなおします。

windowsで開発環境を整える - chainerで自然言語処理できるかマン

windowsで開発環境を整える(cudnn導入) - chainerで自然言語処理できるかマン

せっかくなので、新しい環境を作り直し。(状態は、上の記事で、anacondaとcudnnはインストールされている状態)

chainerダウンロード

Release v1.19.0 · pfnet/chainer · GitHub

環境構築

# anaconda promptを起動し、ディレクトリに移動

$ conda create -n chainer119 python=3.4 anaconda
$ set VS100COMNTOOLS=%VS120COMNTOOLS%
$ activate chainer119
$ python setup.py install

mnistで確認

$ python examples\mnist\train_mnist.py --gpu=0
GPU: 0
# unit: 1000
# Minibatch-size: 100
# epoch: 20

Downloading from http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz...
Downloading from http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz...
Downloading from http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz...
Downloading from http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz...
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
1           0.193373    0.0969053             0.94135        0.9703       68.3304
2           0.0744085   0.0989361             0.976782       0.968       72.226
3           0.0455612   0.0714431             0.986231       0.9786       76.1043
4           0.036945    0.0744536             0.988449       0.9792       80.0062
5           0.0282744   0.0757293             0.990615       0.9792       83.8103
6           0.0243317   0.0787942             0.992265       0.9805       87.3836
7           0.0194383   0.0895611             0.993815       0.9781       90.9687
8           0.018533    0.0836107             0.993931       0.9812       94.5583
9           0.0158314   0.0820245             0.994565       0.9823       98.1675
10          0.0141824   0.1011                0.995515       0.978       101.729
11          0.0136863   0.0793345             0.995799       0.9808       105.37
12          0.0136476   0.11199               0.995848       0.9779       108.967
13          0.0139166   0.0934333             0.995149       0.9808       112.566
14          0.0110531   0.113764              0.996999       0.9777       116.203
15          0.00998231  0.0998481             0.996915       0.9807       119.865
16          0.0102082   0.107755              0.996582       0.9814       123.578
17          0.00974214  0.0950405             0.996866       0.983       127.239
18          0.00615721  0.111511              0.998216       0.9821       130.843
19          0.0150763   0.0972736             0.996049       0.9815       134.471
20          0.0100696   0.0954892             0.997166       0.984       138.116

lossやaccuracyがちゃんと収束しているようなので、大丈夫そうです。

Andor et al., Globally Normalized Transition-Based Neural Networks, 2016メモ

googleのstate-of-the-artな係り受け解析器として公開された「SyntaxNet」の論文を眺めてみたメモ。
SyntaxNetの紹介記事は結構あるのに論文の内容に関する記事があまり見当たらないので有識者の解説待っています。
また、以下について、なにか間違いなどありましたらコメントいただけるとうれしいです。

models/syntaxnet at master · tensorflow/models · GitHub
[1603.06042] Globally Normalized Transition-Based Neural Networks

概要

  • transition-baseなニューラルネットワークモデルで、品詞タグ付け、係り受け解析、文圧縮(要約の一種で単語を削除することで短くする)でstate-of-the-artな結果が得られた
  • 誤りに「Label Bias問題」が含まれているようで、「大域的な正規化をするモデル」を作ることで解決している
  • 最近はrecurrentな構造が流行っているが、このモデルでは「Feed-forwardなニューラルネット」使っていて、これでも匹敵する結果が実現できている

モデル

  • Nivre(2006)の「incremental transition based parser」をベースにしている

Transition System

  • 入力xに対して、以下を定義
    • 状態sの集合S
    • 状態の初期状態s*
    • 各状態で行える決定行為の集合A(s)
    • 状態sで決定行為dをした時の次の状態s'を返す(推移)関数t(s,d)
  • 各状態sで、いくつか選べる決定行為dからどれを選ぶか?をスコア関数ρ(s,d;θ)を使って決める
    • ベクトルθは、適当なパラメータ
  • 各状態へは、初期状態s*から決定行為を繰り返すことでたどり着ける

「入力xと同じ数の決定を持つ完全構造transition system」を各タスク(品詞タグ付けや係り受け解析など)に適用する。
係り受けの場合、transition-basedな手法での「arc-standard」や「arc-eager」は適用できる。(が、「swap transition system」はダメっぽい)

スコア関数ρ

  • 先行研究に従って、FFNNを利用
  • ただし、「ρ(s,d;θ) := 状態sに対するベクトル関数 * 最後の層に関するパラメータベクトル」の形で、線形にスコアが計算される
  • これを次のようなsoftmax的な正規化をかけた学習を行う

大域的な正規化と局所的な正規化

局所的な正規化

  • Chen&Manning(2014)の方法では、j回目の決定行為の時の各決定行為d_jを選ぶ確率は、「決定行為d_jを選ぶスコア関数ρ()のexp()を取った値」を「すべてのjについてその値を計算した和」で割ったもの、を使っている
    • その状態における決定行為について正規化(足して1になるように)しているので「局所的」
    • 遷移確率的な感じ
  • この方法だと、最初から最後までの決定行為列の確率は、各状態における「決定行為の確率値の積」で表せる

大域的な正規化

  • 上記とは対照的に、最初から最後までの決定行為列の確率を、CRFのように「nまでのあらゆる決定行為列でのスコア合計のexp()を取った値の和」で割ったもの、を使う
    • 要するに、「考えられるすべての決定行為列」の確率値を足し合わせると1になるような正規化
    • パス全体を見て正規化しているので「大域的」
    • これだと、ある状態での決定行為の確率の合計は1にはならないので、「遷移確率」とはならない
  • 大域的な正規化を目的関数とする場合を「CRF objective」とよんでいるようです

学習

上記の局所的・大域的な正規化を行っている確率値pがあるので、損失関数は「L(最適な決定行為列d;θ)=-ln p(d;θ)」のように書ける。
損失関数が定義できれば、誤差を使って逆誤差伝播でNNを学習できる。
(微分はどうやって計算するのかコードをのぞいてみると、Tensorflowはgradients関数が使えて自動で計算できるっぽい)

ただ、まともに計算すると(おそらく各決定行為列ごとに係り受けの状態を保持しながら素性を取り出す必要があって膨大な決定行為列候補を持ち続ける必要があって)結構処理が重いっぽいので、「ビームサーチ」と「early updates」という手法を使っている。

ビームサーチ

1回目の決定行為、2回目の決定行為、、、、のように何回目の決定行為か?で階層をわけたとする場合、各階層ごとに、スコアの合計値が高いd個の決定列のみ保持する方法。dのことを「ビーム幅」と呼び、これを大きくするほど処理は増えるが正確な値になっていく。
d=1の時は、各決定行為で一番良いスコアを選び続けることに相当。

transition-baseな係り受け解析でも取られる方法のようですが、ビームサーチ自体はより一般的な手法で、競プロのマラソンマッチとかだとよく見かける気がします。 Chokudai search

early updates

詳しく引用論文を見ていないけど、ビーム幅から最適な決定行為列のパスが外れてしまうような場合、本来ビーム幅内にいて、かつ、最後の決定行為後は一番スコア合計値が高くなるようになっていなければならないことから、外れてしまった時点までの部分的な決定行為列を使って更新してしまう方法、のことのようです。

ビームサーチとearly updatesを使った場合は、「考えられるすべての決定行為列」を求めることはできないので、「ビーム幅に残っている決定行為列」で代用しているようです。

ラベルバイアス問題

局所的な正規化の場合は、遷移確率的に、必ずある状態における決定行為の確率は全部合わせて1になるようにしていましたが、これだと表現力が弱く、同じ入力で後に続く入力によってラベルが決まるような(先読みが必要な)問題に対してうまく対応できないようです。
実用上は局所的な正規化でも、入力のある程度の先読みを行えれば解消できる(できない例もすぐ作れる)とありますが、大域的な正規化を用いるとちゃんと扱える点で有用のようです。

実装

SyntaxNetのgithubのReadmeを読んでみると、英文が入力された後、上記のシステムを使った「品詞付与」→「係り受け解析」の順で処理をしているようです。
また、学習は、「局所的な正規化で事前学習」した後にその学習済みモデルを初期値に「大域的な正規化で学習」しているようです。
取り出す素性については、引用文献で素性について検討している論文があるようで、それを参考にしているようです。

メモ

  • windowsでdocker動かしていじってみようと準備してみたら、デフォルトのメモリが1GB程度の割り当てのようでビルドに失敗してしまったので、確保メモリは多めにしてあげたほうが良いようです
  • 引用論文は一通り目を通した方がよい
  • 学習の実装の細かいところで不明確な部分が多いので、一通り実装を読みたい
  • 論文の5.1の1行目、been beenは誤字?

example\ptbがやっていることを確認

example\ptbが何をやっているかをもう少し見ていきたいと思います。
コードを読む:example\ptbを読む - chainerで自然言語処理できるかマン

RNNLM

example\ptbでは「RNNLM with LSTM」を学習しています。
RNNLMは正式にはRecurrent Neural Network Language Modelのことで、RNNを使って言語モデルを実現しています。

言語モデルは、「文の生起確率(出現のしやすさ)」を計算するモデルのことで、P(文)の具体的な確率値を求めます。
ここで用いられているのは「文は単語列で表される」と考え、以下のように表現できると仮定しています。
P(文)=P(単語1 単語2 ...)=P(単語1) \cdot P(単語2|単語1) \cdot P(単語3|単語1 単語2) \cdot ...
整理すると、
P(文)=\Pi P(単語i|単語1 ... 単語(i-1) )
としています。

RNNLMでは、ニューラルネットワークの出力層のsoftmaxによる値を各単語の生起確率P(単語i|単語1 ... 単語(i-1) )として、利用する事でRNNを言語モデルとして使います。
さらに、RNNの中間ノードとしてLSTMを使う事で、長期間の情報の保存が可能になり、さらなる改善がされており、このサンプルでもそれが実装されています。

chainerでのLSTMの実装

使っているのはchainer.links.LSTM(in_size, out_size)で、これは、in_size個の入力をうけとり、out_size個のLSTMおよび出力を持ちます。
内部変数として、入力からLSTMへのupward接続(links.Linear、下図の赤い線)と、(一つ前の時刻の)LSTM出力から(現時刻の)LSTM入力へのlateral接続(links.Linear、下図の青い線)、LSTMの状態c、一つ前の出力hを持っています。

links.LSTM(3,2)の場合を図にしてみると、
f:id:chainer_nlp_man:20160224020536p:plain
のような感じで、図のLSTMは活性関数としてのLSTM(chainer.functions.activation.lstm)を表し、fully-connectedなので、接続はlinks.LSTM内のすべてのfunctions.activation.LSTMについて張られるようです。
図ではlateral接続が出力から時間遅れで入力に行っているように書いていますが、実装上は内部変数hを使って入力しています。

net.pyで定義されているRNNLMでは、これを2層重ねたものを使っています。
また、Zaremba et al., Recurrent Neural Network Regularization, ICLR 2015メモ - chainerで自然言語処理できるかマンのとおり、recurrentしていない部分にdropoutを入れています。recurrentしている部分にいれるとしたらlateral接続の部分にいれることになるようです。

LSTM自体のバリエーションは複数あるようですが、chainerサンプルで実装されているのはchainer.functions.activation.lstm — Chainer 1.6.1 documentationで、forget gateを持ちますが、peepholeではない版のようです。

ptbで学習したモデルを使って文生成

example\ptbを読む - chainerで自然言語処理できるかマンの学習結果のrnnlm.modelファイルを使って、文生成をしてみます。

準備

下記ファイルを同じディレクトリ内に用意しておきます。

  • ptb.train.txt
  • ptb.test.txt
  • ptb.valid.txt
  • rnnlm.model
  • net.py

コード

#encoding: utf-8
#
# Copyright (c) 2016 chainer_nlp_man
#
# This software is released under the MIT License.
# http://opensource.org/licenses/mit-license.php
#
import argparse
import math
import sys
import itertools
import random
import bisect

import numpy as np

import chainer
import chainer.links as L
import chainer.functions as F
from chainer import serializers

import net

# 引数にモデルファイルを指定
parser = argparse.ArgumentParser()
parser.add_argument('--model', '-m', default='',
                    help='the model from given file')
args = parser.parse_args()

# 単語<->ID変換用
vocab2id = {}
id2vocab = {}

# train_ptb.pyと同じ読み込み方にすることで単語とIDのペアが一致するようにする
def load_data(filename):
    global vocab2id, id2vocab, n_vocab
    words = open(filename).read().replace('\n', '<eos>').strip().split()
    for i, word in enumerate(words):
        if word not in vocab2id:
            vocab2id[word] = len(vocab2id)
            id2vocab[vocab2id[word]] = word

load_data('ptb.train.txt')
load_data('ptb.valid.txt')
load_data('ptb.test.txt')

# train_ptb.pyと同じ設定にする
n_units = 650

lm = net.RNNLM(len(vocab2id), n_units, False)
model = L.Classifier(lm)

# モデルデータの読み込み
serializers.load_hdf5(args.model, model)

# 文の適当な生成
for i in range(0,10):
    print(i+1, end=": ")
    # モデルの状態をいったんリセット
    model.predictor.reset_state()
    word = "<eos>"
    while True:
        # RNNLMへの入力を準備
        x = chainer.Variable(np.array([vocab2id[word]]))
        # RNNLMの出力のsoftmaxを取得
        y = F.softmax(model.predictor(x))
        # 各単語の確率値として、単語をサンプリングし、次の単語とする
        y_accum = np.add.accumulate(y.data[0])
        r = random.random()
        word = id2vocab[bisect.bisect(y_accum, r)]
        # もし文末だったら終了
        if word == "<eos>":
            print(".")
            break
        else:
            print(word, end=" ")

実行

$ python gen_sentence.py -m rnnlm.model

結果

1: mr. burton said certificates of annuity and acquisitions received by the government to be submitted on a <unk> basis by painewebber inc. will have just been involved in the forest business and private incentives but declined to disclose what full licensing only .
2: <unk> white house of duff & trecker said the new trading company 's cash flow has been reduced by $ N million for the sale of those shares under the agreement .
3: the fund called cholesterol .
4: gary l. <unk> head of the only reinsurance department 's office raised his <unk> account title to revise washington motor co. cleveland securities .
5: short interest in shares of high-yield high-risk junk bonds moved up N last week mostly as <unk> from boston 's N N N high over $ N billion .
6: the cut would focus on <unk> loans designed to distribute <unk> even to the ldp earlier this year .
7: but top trial activist david <unk> d. ore. cited the unprecedented factors of operating in damages of gifts from fashionable banks that be undervalued in <unk> division .
8: the fda 's successor will become married artistic efforts by a <unk> in the press as well as the quality of the reasons .
9: the house which will help lay <unk> out steppenwolf 's <unk> operations once became a direct <unk> to international environmental protection although it sold the $ N million procter & gamble co. buddy <unk> thompson operations and rjr  nabisco inc. in a fraudulent interview .
10: a number of agencies not <unk> legal corruption and lawyers at new york have <unk> the government 's <unk> plea into l. <unk> .

結構文っぽく生成されているように見えます。

XORの学習

chainerのバージョンを1.6.1へあげてみたので、TutorialをやりながらXORの学習を行うMulti-layer Perceptronを書いてみました。
初期値(L.LinearのWがランダム)に依って局所解に落っこちやすいみたいで、損失が十分に小さくなってくれないことが多いです。。。

コード

#encoding: utf-8
#
# Copyright (c) 2016 chainer_nlp_man
#
# This software is released under the MIT License.
# http://opensource.org/licenses/mit-license.php
#
import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, Variable, optimizers, serializers, utils
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L

# 2入力、2出力
# [nobias_flagがFalseの場合]
#  x h y
# -o-o-o-
#   x x
# -o-o-o-
# (Trueの場合は、xとhにバイアスノードが1つずつ加わる)
class MLP(Chain):
    def __init__(self, nobias_flag):
        super(MLP, self).__init__(
            l1 = L.Linear(2,2,nobias=nobias_flag),
            l2 = L.Linear(2,2,nobias=nobias_flag),
        )
        self.nobias_flag = nobias_flag

    def __call__(self, x):
        h = F.sigmoid(self.l1(x))
        y = self.l2(h)
        return y

    def dump(self):
        print(self.l1.W.data)
        if not self.nobias_flag:
            print(self.l1.b.data)
        print(self.l2.W.data)
        if not self.nobias_flag:
            print(self.l2.b.data)

class Classifier(Chain):
    def __init__(self,predictor):
        super(Classifier, self).__init__(
            predictor = predictor
        )

    def __call__(self, x, t):
        y = self.predictor(x)
        self.loss = F.softmax_cross_entropy(y,t)
        self.accuracy = F.accuracy(y,t)
        return self.loss


# モデルの準備
model = Classifier(MLP(False))
optimizer = optimizers.Adam()
optimizer.setup(model)

# 学習ループ
loss_value = 100000
cnt = 0
while loss_value > 1e-5:
    # 学習データ
    x = Variable(np.array([[0,0],[1,0],[0,1],[1,1]], dtype=np.float32))
    t = Variable(np.array([0,1,1,0], dtype=np.int32))

    # 学習
    model.zerograds()
    loss = model(x,t)
    loss_value = loss.data
    loss.backward()
    optimizer.update()

    cnt += 1
    if cnt%1000 == 0:
        # 途中結果の出力
        y = F.softmax(model.predictor(x))

        print("=====iter = {0}, loss = {1}=====".format(cnt, loss_value))
        print("---output value---")
        print(y.data)
        print("---result---")
        print(y.data.argmax(1))
        print("---dump---")
        model.predictor.dump()


# モデルファイルを保存
serializers.save_npz('my_xor.model', model)

実行

$ python xor.py

結果

L.Linearが持つWとbのうち、Wの初期値はデフォルトでランダムに設定されるので、何回か実行しています。

うまくいくケース

初期値がいいところにいると、最適解に向かってくれて損失(loss)が0に近づいてくれるようです。

=====iter = 24000, loss = 1.2278556823730469e-05=====
---output value---
[[  9.99987483e-01   1.25582164e-05]
 [  9.03956334e-06   9.99990940e-01]
 [  1.53962192e-05   9.99984622e-01]
 [  9.99987483e-01   1.25290881e-05]]
---result---
[0 1 1 0]
---dump---
[[ 9.62363434 -9.92488861]
 [-9.60831738  9.82478237]]
[-5.2305131  -5.20280075]
[[-11.67700863 -10.53997993]
 [ 11.75784016  12.29858112]]
[ 5.6855135  -5.84928703]

うまくいかないケース

局所解によく落ちてしまっています。(終わらない)

=====iter = 84000, loss = 0.3465735912322998=====
---output value---
[[  1.00000000e+00   3.97014652e-08]
 [  4.53840165e-08   1.00000000e+00]
 [  5.00000417e-01   4.99999613e-01]
 [  5.00000417e-01   4.99999613e-01]]
---result---
[0 1 0 0]
---dump---
[[ -9.91501236  21.46763802]
 [ 10.34690571  21.0509758 ]]
[ 5.1153326  -4.40958834]
[[ 8.80068302 -9.16878891]
 [-8.29266644  8.18357563]]
[ 0.0821298  -0.17688689]

Seq2Seqメモ

Sequence-to-Sequence(Seq2Seq)学習は、任意長の入力列から任意長の出力列を出力するような学習のことで、Neural Networkの枠組みで扱う方法が提案されて、いい結果が報告されています。雑なメモ。

入力・出力列の例

(自然)言語処理系

など。

メモリユニット

通常のRNNでは、中間の隠れ層にあたる部分は「直接的に入力から状態を計算するだけの単純なユニット」でしたが、過去の長期の情報を保持できるよう拡張した「GRU」や「LSTM」と呼ばれるユニットに置き換えたものが使われています。

GRUは、メモリの役割を持つ内部状態を持たせて、ユニットの入力として「update gate」「reset gate」を増やし、その2つの入力でメモリの状態や入力xの影響度を操作できるようにしているようです。

LSTMは、メモリの役割を持つ内部状態を持たせて、ユニットの入力として「input gate」「output gate」「forget gate」を増やし、その3つの入力でメモリの状態や入力、出力の調整を行えるようにしているようです。メモリ状態を各gateの制御に使えるようにつないでいる場合は「peephole connection」版で、つながない版もよく使われるみたいです。

Seq2Seqの種類

  • Simple Seq2Seq
    • 単純なSeq2Seqのモデルは、encoder(入力->隠れ状態)とdecoder(隠れ状態->出力)を連続的に行う構造
    • encoderは、入力から隠れ層ユニットを更新するだけ
    • 入力の終わりを入力すると、そこからdecoderが始まり、対応する出力を1つずつ出力していく
  • Peeky Seq2Seq
    • Simple Seq2Seqのencoderとdecoderの間に文脈ベクトルcに関する隠れ層を追加したもの
  • Attention Seq2Seq
    • 上記のSeq2Seqのモデルだと、入力はencoderの時だけで、decoderのときは入力を受け取れない
    • decoderの部分で入力時の隠れ層の状態が考慮できるように(見えるように?)したものっぽい

参考

example\ptbを読む

examples\ptbに用意してあるコード(LSTMを使ったRNN言語モデル with dropoutはrecurrentじゃない部分だけに適用)を読んでいきたいと思います。 一応、1.6.1でもコードの変更は無いみたいですが、結果は1.5.0.2のやつです。

サンプル実行

# GPUで学習実行
$ python examples\ptb\train_ptb.py --gpu=0
#vocab = 10000
going to train 1812681 iterations
kern.cu
iter 10000 training perplexity: 996.19 (119.44 iters/sec)
iter 20000 training perplexity: 417.39 (121.77 iters/sec)
iter 30000 training perplexity: 300.75 (122.24 iters/sec)
iter 40000 training perplexity: 255.47 (121.69 iters/sec)
evaluate
epoch 1 validation perplexity: 237.56
...
epoch 38 validation perplexity: 120.29
learning rate = 0.0024379168015552267
iter 1770000 training perplexity: 42.36 (120.08 iters/sec)
iter 1780000 training perplexity: 48.21 (120.22 iters/sec)
iter 1790000 training perplexity: 46.21 (120.53 iters/sec)
iter 1800000 training perplexity: 44.88 (120.05 iters/sec)
iter 1810000 training perplexity: 40.86 (119.79 iters/sec)
evaluate
epoch 39 validation perplexity: 119.67
learning rate = 0.002031597334629356
test
test perplexity: 114.81662731578731
save the model
save the optimizer

個人PCについてるGPUで計算しても6時間ぐらいかかってしまいました。。。

他の言語モデルのperplexityはどの程度なんでしょう、、 参考まで、(同じデータじゃないと思いますが)、deeplearning2015:schedule | CILVR Lab @ NYUのWeek10part1のp.18を見てみると、Kneser-Ney 5-gramで141程度、Random forestで132程度という値が見えます。

学習のコード(train_ptb.py)

import chainer
from chainer import cuda
import chainer.links as L
from chainer import optimizers
from chainer import serializers

import net

word2vecのコードと比べて「serializers」が増えています。
コード中では「load_hdf5()」と「save_hdf5()」でmodelの重みやoptimizerの状態をHDF5形式で読み込んだり保存したりするために使われています。
HDF5はHierarchical Data Format5のことらしく、階層構造を含めてデータを保存できる形式です。
HDF5フォーマットに関するメモ書き - たまに書きます

HDFViewというものを利用すると、中身がどうなっているか見ることができます。(以下は生成されたrnnlm.modelの中身)
f:id:chainer_nlp_man:20151208231436p:plain

netは同じディレクトリ内に置かれたnet.pyで、RNNLMを定義しています。

class RNNLM(chainer.Chain):
    def __init__(self, n_vocab, n_units, train=True):
        super(RNNLM, self).__init__(
            embed=L.EmbedID(n_vocab, n_units),
            l1=L.LSTM(n_units, n_units),
            l2=L.LSTM(n_units, n_units),
            l3=L.Linear(n_units, n_vocab),
        )
        self.train = train

単語埋め込み層(embed)と、LSTM層が2つ(l1,l2)と、全結合(出力)層(l3)で構成されています。
Standard Link implementations — Chainer 1.6.1 documentation
Standard Link implementations — Chainer 1.6.1 documentation
Standard Link implementations — Chainer 1.6.1 documentation
v1.5からChainでまとめて管理できるようになったらしく、こんな感じで継承クラスを作って管理するっぽいです。

    def reset_state(self):
        self.l1.reset_state()
        self.l2.reset_state()

LSTMは「状態」(メモリ的機能)を持つので、reset_state()では、LSTM層の状態をリセットできるようになっています。

    def __call__(self, x):
        h0 = self.embed(x)
        h1 = self.l1(F.dropout(h0, train=self.train))
        h2 = self.l2(F.dropout(h1, train=self.train))
        y = self.l3(F.dropout(h2, train=self.train))
        return y

__call__()は、順方向計算時に呼ばれる関数で、xを入力として、埋め込み層、LSTM層、LSTM層、全結合(出力)層の順に計算しています。
また、それぞれの層の間で、dropoutが入れてあり、確率pで0になっているようです。
Standard Function implementations — Chainer 1.6.1 documentation
前の論文の通り、non-recurrentな結合のところにだけdropoutが入れてあり、recurrentな結合(LSTMの前の出力を保持する内部変数h)にはdropoutは入れてありません。

n_epoch = 39   # number of epochs
n_units = 650  # number of units per layer
batchsize = 20   # minibatch size
bprop_len = 35   # length of truncated BPTT
grad_clip = 5    # gradient norm threshold to clip

各サイズなどの設定。
n_unitsは層ごとのユニット数で、650は先の論文だとmediumサイズらしいです。(largeは1500とか)
bprop_lenは、BPTTしたときtruncateする時間サイズを指定。
grad_clipは、後でGradientClipping()の引数にしてありますが、この関数は勾配のL2normの大きさがgrad_clipよりも大きい場合、この大きさに縮める処理を行うようです。
Optimizer — Chainer 1.6.1 documentation

# Prepare dataset (preliminary download dataset by ./download.py)
vocab = {}

全語彙を保管する用の辞書を準備。単語に対してIDを返すために利用しています。

def load_data(filename):
    global vocab, n_vocab
    words = open(filename).read().replace('\n', '<eos>').strip().split()
    dataset = np.ndarray((len(words),), dtype=np.int32)
    for i, word in enumerate(words):
        if word not in vocab:
            vocab[word] = len(vocab)   # 単語をIDに変換
        dataset[i] = vocab[word]       # datasetに単語IDを追加
    return dataset

train_data = load_data('ptb.train.txt')
valid_data = load_data('ptb.valid.txt')
test_data = load_data('ptb.test.txt')
print('#vocab =', len(vocab))

load_data()は、ファイルを読み込み、単語をIDにしつつdatasetに文を単語IDにしたものをいれていっています。
学習用、検証用、評価用を準備。

# Prepare RNNLM model, defined in net.py
lm = net.RNNLM(len(vocab), n_units)
model = L.Classifier(lm)
model.compute_accuracy = False  # we only want the perplexity
for param in model.params():
    data = param.data
    data[:] = np.random.uniform(-0.1, 0.1, data.shape)

net.pyのRNNLM定義を使ってインスタンスを生成。
L.Classifier()は、引数にpredictor(ここではRNNLM)を受け取って、入力値とラベルに対する損失(やAccuracy)を計算してくれる簡単な分類器クラスです。

Linkクラスはparams()を持っており、リンク階層のすべてのパラメータ(辺の重み)を返してくれています。
dataは配列を扱っているので、要素数分(data.shape)だけ-0.1~0.1の一様乱数で初期化しています。

# Setup optimizer
optimizer = optimizers.SGD(lr=1.)
optimizer.setup(model)
optimizer.add_hook(chainer.optimizer.GradientClipping(grad_clip))

SGDで最適化。lrは学習率(learning rate)。
GradientClipping()をフックに追加していますが、これは勾配のL2normの大きさが指定のサイズより大きい場合に小さくする関数。

# Init/Resume
if args.initmodel:
    print('Load model from', args.initmodel)
    serializers.load_hdf5(args.initmodel, model)
if args.resume:
    print('Load optimizer state from', args.resume)
    serializers.load_hdf5(args.resume, optimizer)

もし、初期値となるモデルや状態のファイルがあれば、それをロード。

def evaluate(dataset):
    # Evaluation routine
    evaluator = model.copy()  # to use different state
    evaluator.predictor.reset_state()  # initialize state

    sum_log_perp = 0
    for i in six.moves.range(dataset.size - 1):
        x = chainer.Variable(xp.asarray(dataset[i:i + 1]), volatile='on')
        t = chainer.Variable(xp.asarray(dataset[i + 1:i + 2]), volatile='on')
        loss = evaluator(x, t)
        sum_log_perp += loss.data
    return math.exp(float(sum_log_perp) / (dataset.size - 1))

evaluate()は、パープレキシティの計算を行っています。

# Learning loop
whole_len = train_data.shape[0]  # 学習データのサイズ
jump = whole_len // batchsize    # バッチサイズずつに分割した場合の幅
cur_log_perp = xp.zeros(())
epoch = 0
start_at = time.time()
cur_at = start_at
accum_loss = 0
batch_idxs = list(range(batchsize))
print('going to train {} iterations'.format(jump * n_epoch))

学習に使う各種変数を指定。

for i in six.moves.range(jump * n_epoch):
    x = chainer.Variable(xp.asarray( # バッチサイズ単位の単語を配列に入れる(入力用)
        [train_data[(jump * j + i) % whole_len] for j in batch_idxs]))
    t = chainer.Variable(xp.asarray( # xを1つずらした配列(出力用)
        [train_data[(jump * j + i + 1) % whole_len] for j in batch_idxs]))
    loss_i = model(x, t) # 損失を計算
    accum_loss += loss_i # 累計損失を計算
    cur_log_perp += loss_i.data

    if (i + 1) % bprop_len == 0:  # Run truncated BPTT  # bprop_len幅まで計算したら、まとめて勾配から最適化を行う
        model.zerograds()  # 勾配をゼロ初期化
        accum_loss.backward() # 累計損失を使って、誤差逆伝播(誤差の計算)
        accum_loss.unchain_backward()  # truncate # 誤差逆伝播した変数や関数へのreferenceを削除
        accum_loss = 0
        optimizer.update() # 最適化ルーチンの実行

    if (i + 1) % 10000 == 0: # 10000反復分終了したら、10000反復分でのパープレキシティを確認
        now = time.time()
        throuput = 10000. / (now - cur_at)
        perp = math.exp(float(cur_log_perp) / 10000)
        print('iter {} training perplexity: {:.2f} ({:.2f} iters/sec)'.format(
            i + 1, perp, throuput))
        cur_at = now
        cur_log_perp.fill(0)

    if (i + 1) % jump == 0: # 1エポック分終了したら、検証用データでパープレキシティを確認
        epoch += 1
        print('evaluate')
        now = time.time()
        perp = evaluate(valid_data)
        print('epoch {} validation perplexity: {:.2f}'.format(epoch, perp))
        cur_at += time.time() - now  # skip time of evaluation

        if epoch >= 6:  # 徐々に学習率を小さくしていっている
            optimizer.lr /= 1.2
            print('learning rate =', optimizer.lr)

    sys.stdout.flush()

学習のメインループ。終了条件は十分に収束したらじゃなく、指定回数実行したらになっています。

入力テキストが「ab...cd...ef」(それぞれのアルファベットは単語に対応)みたいな場合、jump幅分飛ばしで学習の入力データを準備。
その時のRNNへの入力xは、仮に「a」「c」「e」だったとすると、出力は次の単語が正解データtになってほしいので「b」「d」「f」になります。 複数の単語をまとめてmodel(x,t)で求めてみて、損失と勾配を計算します。

model()実行後は、RNNの内部状態が変化した上で1単語ずらして同様に学習します。
L.LSTMが前のそのLSTMの出力を保持するhという変数を内部に持つので、連続で計算していけば、RNNのBPTT的な学習がされるようです。
truncated BPTTという、BPTTの過去の時間を打ち切ってしまう方法は、backword()で逆誤差伝播したあとに、unchain_backward()を呼び出す事で過去のつながりをいったん切ることができるようです。

下のif文2つは定期的にパープレキシティを計算。

# Evaluate on test dataset
print('test')
test_perp = evaluate(test_data)
print('test perplexity:', test_perp)

学習が終了したら、evaluate()でテストデータに対してパープレキシティを求めます。

# Save the model and the optimizer
print('save the model')
serializers.save_hdf5('rnnlm.model', model)
print('save the optimizer')
serializers.save_hdf5('rnnlm.state', optimizer)

最後にRNNLMのモデルと状態を(hdf5形式の)ファイルに出力して終了しています。 1.6.1だとnpz形式のようです。

参考

Recurrent Nets and their Computational Graph — Chainer 1.6.1 documentation
Chainerチュートリアル -v1.5向け- ViEW2015

Zaremba et al., Recurrent Neural Network Regularization, ICLR 2015メモ

examplesにあるRNNLMのコードのベースになっている論文がコメントにあるようなので、読んだメモ。

[1409.2329] Recurrent Neural Network Regularization

概要

  • LSTMを使ったRNNsの場合、単純なDropoutがうまくいかない
    • また、実用的な応用では、大きなRNNだと過学習してしまうため、しばしば(かなり)小さいモデルが使われる
  • うまく適用する方法を紹介する

RNNとLSTM

  • RNNを h^{l-1}_{t}, h^{l}_{t-1} \to h^{l}_{t}とする
    • 「時刻tのひとつ前のレイヤーl-1」と「時刻t-1のレイヤーl」の隠れ層から「時刻tのレイヤーl」の隠れ層の値が決まる
    • よくあるRNNsの場合は、 h^{l}_{t} = f(T_{n,n}h^{l-1}_{t}+T_{n,n}h^{l}_{t-1})で計算
  • LSTMはこの時間の遅れによる影響を「memorize」できるようにすることで拡張している
    • だいたいは明示的に「メモリセル」を明示的に持つ
    • ここで考えるLSTMは、Input gate, Input modulation gate, Forget gate, Output gateを持つ
    •  h^{l-1}_{t}, h^{l}_{t-1}, c^{l}_{t-1} \to h^{l}_{t},c^{l}_{t}

提案手法

  • main ideaは、「non-recurrentな接続に対してのみドロップアウト操作を適用する」こと
    • BPTT(Backpropagation through time)な感じに時間展開したネットワークで考えると、現時刻tでの遷移にのみ適用
  • 各gateへの入力 h^{l-1}_{t}, h^{l}_{t-1}のうち、 h^{l-1}_{t}に対してランダムに値を0にする(dropout)
  • 考え
    • ユニットが過去に起きたイベントを覚えておくことは重要
    • しかし、すべての接続関係にDropoutを適用してしまうと、recurrentしている接続をしているところを混乱させることになってしまう
    • これはLSTMに長期記憶を格納することを難しくさせてしまう
    • そこで、recurrentしている接続にはドロップアウトを適用しないことで、過去の記憶能力をそのまま発揮できるようになる

実験

ネットワークの作り方とかやってみないとわからなそうなので、実験は後から見る。

word2vecの学習が収束しているのかプロットして確認

word2vecのサンプルを実行する際、epoch毎に累積損失(accum_loss)が表示されていましたが、ちゃんと収束しているかがコマンドライン上で見るだけではわかりにくいので、グラフにプロットして確認したいと思います。
が、ちょっとハマったのでメモしておきたいと思います。

コードでは100,000単語ごとに処理時間やスループットを表示しているので、その間の累積損失をプロットします。

コード

matplotlibを使って、プログラムに以下を追加します。

モジュールのインポート。

import numpy as np
import matplotlib.pyplot as plt   # 追加

プロットするデータを保管する変数を準備。

next_count = 100000

py = []              # 追加
sub_accum_loss = 0   # 追加

for epoch in range(args.epoch):

スループットを表示している部分に累積損失を配列に入れる処理を追加します。

for epoch in range(args.epoch):
    ...
    for i in indexes:
        if word_count >= next_count:
            ...
            next_count += 100000
            cur_at = now

            py.append(float(sub_accum_loss))   # 追加(※)
            sub_accum_loss = 0                 # 追加

        ...
        accum_loss += loss.data
        sub_accum_loss += loss.data            # 追加
        word_count += args.batchsize

        ...

コードの最後に描画処理を入れます。

plt.plot(py)                      # 追加
plt.grid()                        # 追加
plt.title("sub_accum_loss")       # 追加
plt.savefig('word2vec_log.png')   # 追加
plt.show()                        # 追加

ハマったところは、配列に追加する(※)部分で、sub_accum_lossの型が「cupy.core.core.ndarray」という0次元配列になっているようで、プロットに失敗していしまっていました。floatに変換する必要があるようです。
numpy - 0-dimension array問題 - Qiita

結果

$ python examples\word2vec\train_word2vec_plot.py --gpu=0

終了時にGUIなグラフ画面が表示され、「word2vec_log.png」にその画像が出力されています。

f:id:chainer_nlp_man:20151205151141p:plain
十分に収束しているようです。

examples\word2vecを読む

examples\word2vecに用意してあるコードを読んでいきたいと思います。

word2vecとは

いろんな意味で使われているような気がしますが、正確には
word2vec - Tool for computing continuous distributed representations of words. - Google Project Hosting
のプログラムのことを指すと思います。
Mikolovら(2013)によって、単語の分散表現(複数の計算要素で表現するもの。ベクトルとか)を高速に得る手法が提案、ツールが公開され、得られた分散表現で単語の足し算・引き算ができたり、単語の類似性があったりで話題になっていました。
岡崎先生の分散表現に関する資料:単語の分散表現と構成性の計算モデルの発展

word2vecでは、ネットワークの形として「Continuous Bag of words(CBOW)」と「SkipGram」、高速化(?)に「Hierarchical softmax」と「Negative sampling」などの方法を取り入れており、このサンプルではこの4つが実装されているようです。
海野さんのword2vecの論文紹介資料:unnonouno: NIPS2013読み会でword2vec論文の紹介をしました

サンプル実行

まずはREADME.mdにある通りに実行してみます。

# ファイルダウンロード
$ python examples\ptb\download.py

# GPUで学習実行
$ python examples\word2vec\train_word2vec.py --gpu=0
GPU: 0
# unit: 100
Window: 5
Minibatch-size: 100
# epoch: 10
Training model: skipgram
Output type: hsm

n_vocab: 9999
data length: 887521
epoch: 0
kern.cu
100000 words, 27.32 sec, 3660.37 words/sec
200000 words, 6.55 sec, 15261.06 words/sec
300000 words, 6.77 sec, 14763.82 words/sec
400000 words, 7.06 sec, 14158.29 words/sec
500000 words, 7.02 sec, 14253.04 words/sec
600000 words, 6.83 sec, 14650.08 words/sec
700000 words, 6.66 sec, 15013.11 words/sec
800000 words, 6.73 sec, 14856.37 words/sec
41296432.0
epoch: 1
900000 words, 6.65 sec, 15039.67 words/sec
1000000 words, 6.64 sec, 15049.58 words/sec
1100000 words, 6.59 sec, 15182.98 words/sec
1200000 words, 6.64 sec, 15058.11 words/sec
1300000 words, 6.77 sec, 14773.31 words/sec
1400000 words, 6.64 sec, 15055.18 words/sec
1500000 words, 6.73 sec, 14858.73 words/sec
1600000 words, 6.62 sec, 15102.17 words/sec
1700000 words, 6.67 sec, 15003.61 words/sec
30504294.0
...
epoch: 9
8000000 words, 6.87 sec, 14558.01 words/sec
8100000 words, 6.65 sec, 15035.03 words/sec
8200000 words, 6.57 sec, 15209.29 words/sec
8300000 words, 6.53 sec, 15319.69 words/sec
8400000 words, 6.59 sec, 15174.44 words/sec
8500000 words, 6.69 sec, 14952.80 words/sec
8600000 words, 6.65 sec, 15048.00 words/sec
8700000 words, 6.57 sec, 15226.57 words/sec
8800000 words, 6.76 sec, 14793.76 words/sec
27516834.0

download.pyでは、「ptb.train.txt(学習用)」「ptb.valid.txt(検証用)」「ptb.test.txt(評価用)」の3ファイルをダウンロードしています。 このファイルは、LeCun先生のニューヨーク大学での2015年の講義で使っているデータのようです。

# ptb.train.txtの先頭3行
 aer banknote berlitz calloway centrust cluett fromstein gitano guterman hydro-quebec ipo kia memotec mlx nahb punts rake regatta rubens sim snack-food ssangyong swapo wachter 
 pierre <unk> N years old will join the board as a nonexecutive director nov. N 
 mr. <unk> is chairman of <unk> n.v. the dutch publishing group 

train.txtの最初の3行を見てみると、英語ではないものからスタートしているようですが、ほぼ英語っぽいように見えます。
スペース区切りで、大文字は小文字に変換、数字が「N」、未知語相当が「<unk>」に置き換えられているみたいです。
trainは42068行、validは3370行、testは3761行ありました。(ここではtrainのみ使用されます)

train_word2vec.pyでは、word2vecの学習が行われ、「word2vec.model」ファイルに単語の分散表現が出力されています。

# 類似単語の検索
$ python examples\word2vec\search.py
>> group
query: group
inc.: 0.46817001700401306
laurel: 0.44689810276031494
blackstone: 0.41681644320487976
wpp: 0.4130145311355591
giant: 0.4069499671459198
>> director
query: director
president: 0.5530474185943604
vice: 0.5511615872383118
chairman: 0.49361783266067505
j.: 0.4811353385448456
chief: 0.46405717730522156
>> year
query: year
week: 0.7283791899681091
month: 0.6891863942146301
summer: 0.5073532462120056
earlier: 0.5011518001556396
spring: 0.37471672892570496
>> four
query: four
five: 0.7502017021179199
three: 0.6845633387565613
two: 0.6798515319824219
six: 0.6025211215019226
several: 0.5378302335739136

search.pyで、上記のモデルファイル(学習された単語の分散表現)を読み込んで、入力された単語にベクトルが近い文字列を5件表示してくれています。
似た種類の単語が並んででうまく学習されているようです。

学習のコード(train_word2vec.py)

ダラダラと読んでいきます。わかっていないことが多いので、間違っている可能性が高いです。
また、Chainerの使い方と 自然言語処理への応用を参考にしながら見ていきます。

import numpy as np

import chainer
from chainer import cuda
import chainer.functions as F
import chainer.links as L
import chainer.optimizers as O

必要モジュールのimport。chainerの基本セットのようです。

index2word = {}
word2index = {}
counts = collections.Counter()
dataset = []

index2wordとword2indexは単語文字列とIDとの相互変換に使われます。
countsは、出現頻度を数えるために使われます。
datasetは、IDのリストとして使われます。

with open('ptb.train.txt') as f:
    for line in f:
        for word in line.split():
            if word not in word2index:
                ind = len(word2index)
                word2index[word] = ind
                index2word[ind] = word
            counts[word2index[word]] += 1
            dataset.append(word2index[word])

ファイルから1行ずつ読み込んで、単語のID化と頻度数え上げ、データセットの構築を行っています。
これを見る限りだと、改行が無視されるので、入力テキストは長い1文として扱われるようです。

n_vocab = len(word2index)

語彙の種類数がn_vocabに保存されます。

if args.out_type == 'hsm':
    HSM = L.BinaryHierarchicalSoftmax
    tree = HSM.create_huffman_tree(counts)
    loss_func = HSM(args.unit, tree)
elif args.out_type == 'ns':
    cs = [counts[w] for w in range(len(counts))]
    loss_func = L.NegativeSampling(args.unit, cs, 20)
elif args.out_type == 'original':
    loss_func = SoftmaxCrossEntropyLoss(args.unit, n_vocab)
else:
    raise Exception('Unknown output type: {}'.format(args.out_type))

コマンドライン引数に応じて、損失関数の種類を変えているようです。
HierarchicalSoftmaxとNegativeSamplingはchainer.linksモジュールにあるようで、単純なSoftmaxCrossEntropyLossのみこのコード内に定義されています。

class SoftmaxCrossEntropyLoss(chainer.Chain):
    def __init__(self, n_in, n_out):
        super(SoftmaxCrossEntropyLoss, self).__init__(
            W=L.Linear(n_in, n_out),
        )

    def __call__(self, x, t):
        return F.softmax_cross_entropy(self.W(x), t)

といっても、chainer.Chainを継承したクラスとして定義され、chainer.functionsで定義済みのsoftmax_cross_entropy()を利用して実装されているようです。
functionオブジェクトは、「演算ノード」に相当し、上の場合は損失関数の計算に使用されています。(ここではネットワークの最終的な出力と正解データとの差分的なもの)

if args.model == 'skipgram':
    model = SkipGram(n_vocab, args.unit, loss_func)
elif args.model == 'cbow':
    model = ContinuousBoW(n_vocab, args.unit, loss_func)
else:
    raise Exception('Unknown model type: {}'.format(args.model))

コマンドライン引数に応じて、ネットワークの形をmodel変数に定義しています。

class ContinuousBoW(chainer.Chain):

    def __init__(self, n_vocab, n_units, loss_func):
        super(ContinuousBoW, self).__init__(
            embed=F.EmbedID(n_vocab, args.unit),
            loss_func=loss_func,
        )

    def __call__(self, x, context):
        h = None
        for c in context:
            e = self.embed(c)
            h = h + e if h is not None else e

        return self.loss_func(h, x)

F.EmbedID関数は、単語をIDで表現した密ベクトルを実装したものらしいです。
(イメージ的には、1-of-KのIDから内部ノードへのエッジすべて、を作っているようです)
call関数では、context情報を受け取ったらそのIDに対応する密ベクトル情報eを内部ノードの状態hに加算し、その合計値とxとの損失を返しています。

class SkipGram(chainer.Chain):

    def __init__(self, n_vocab, n_units, loss_func):
        super(SkipGram, self).__init__(
            embed=L.EmbedID(n_vocab, n_units),
            loss_func=loss_func,
        )

    def __call__(self, x, context):
        loss = None
        for c in context:
            e = self.embed(c)

            loss_i = self.loss_func(e, x)
            loss = loss_i if loss is None else loss + loss_i

        return loss

一方、SkipGramでは、call関数で、xからcontextを予測することになるので、contextの各単語cの密ベクトル情報eとの損失を計算し、合計したものを返しています。

dataset = np.array(dataset, dtype=np.int32)

データセットをnumpyの配列に直しています。内部処理の都合のようです。

optimizer = O.Adam()
optimizer.setup(model)

最適化の手法を設定しています。
Adamは勾配の更新幅を自動調整する手法の一つで、echizen_tmさんの30minutes Adamで30分でわかります。

skip = (len(dataset) - args.window * 2) // args.batchsize
for epoch in range(args.epoch):
    indexes = np.random.permutation(skip)       # 0~skip-1までの順列をランダムに入れ替えたもの
    for i in indexes:
        position = np.array(range(0, args.batchsize)) * skip + (args.window + i)    # 適当な位置から均等にbatchsize個分の位置情報
        loss = calculate_loss(model, dataset, position)

        model.zerograds()    # 勾配をゼロに初期化
        loss.backward()      # 逆伝播を実行
        optimizer.update()   # 最適化ルーチンを実行

処理のメイン部分。(いくつか省略)
datasetの中の単語を複数まとめて処理するために、positionリストにランダム位置から均等にbatchsize個の位置情報をいれておいているようです。
まとめてcalculate_loss関数で損失を計算し、後半のOptimizer更新処理(お約束処理らしい)を実行しています。

def calculate_loss(model, dataset, position):
    # use random window size in the same way as the original word2vec
    # implementation.
    w = np.random.randint(args.window - 1) + 1
    context = []
    for offset in range(-w, w + 1):    # -wからwまで
        if offset == 0:   # 注目している単語自体は文脈には含めない
            continue
        c_data = xp.asarray(dataset[position + offset])   # positionのoffsetだけずらしたものを
        c = chainer.Variable(c_data)                      # Variableオブジェクトに変換。
        context.append(c)                                 # contextリストに入れておく。
    x_data = xp.asarray(dataset[position])                # 注目している単語もリスト化し、
    x = chainer.Variable(x_data)                          # Variableオブジェクトに変換。
    return model(x, context)                              # modelの実行結果(損失合計)を返す。

word2vecの元のプログラムではwindow幅が固定でなくランダムのようでした。
ここでも、それに合わせるため、wをランダムに変えているみたいです。 Variableはデータノードに相当し、実際の「値」を保持するオブジェクトのようです。
chainerの内部処理をするために、Variableオブジェクトに変換していますが、リストのままでまとめて処理できるようですね。

with open('word2vec.model', 'w') as f:
    f.write('%d %d\n' % (len(index2word), args.unit))
    w = model.embed.W.data
    for i in range(w.shape[0]):
        v = ' '.join(['%f' % v for v in w[i]])
        f.write('%s %s\n' % (index2word[i], v))

最後に、word2vec.modelファイルへ学習した密ベクトル情報を出力して終了。

だいーーーーぶ、見やすいですね。(word2vecのプログラムと比較して・・・)
基本の処理は分かった気になりましたが、やはりchainerのチュートリアルから進めたほうがよさそうでした。。

メモ

typoぽいの。処理的には一応問題なさそう。