Sentiment Analisys with Recursive Neural Network

※このNotebookは、chainer/examples/sentimentを元に作成しています。scriptとして実行したい場合はそちらを参照してください。

このNotebookでは、Recursive Neural Networkを用いて文書の感情分析を行います。

まずは、以下のセルを実行して、ChainerとそのGPUバックエンドであるCuPyをインストールします。Colaboratoryの「ランタイムのタイプ」がGPUであれば、GPUをバックエンドとしてChainerを動かすことができます。

[1]:
!curl https://colab.chainer.org/install | sh -
Reading package lists... Done
Building dependency tree
Reading state information... Done
libcusparse8.0 is already the newest version (8.0.61-1).
libnvrtc8.0 is already the newest version (8.0.61-1).
libnvtoolsext1 is already the newest version (8.0.61-1).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

必要なモジュールをimportし、その後にChainerのバージョンやNumPy・CuPy、そしてCuda等の実行環境を確認してみましょう。

[2]:
import collections
import numpy as np

import chainer
from chainer import cuda
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions
from chainer import reporter


chainer.print_runtime_info()
Chainer: 4.0.0
NumPy: 1.14.3
CuPy:
  CuPy Version          : 4.0.0
  CUDA Root             : None
  CUDA Build Version    : 8000
  CUDA Driver Version   : 9000
  CUDA Runtime Version  : 8000
  cuDNN Build Version   : 7102
  cuDNN Version         : 7102
  NCCL Build Version    : 2104

1. 学習データの用意

このnotebookでは、chainer/examples/sentiment/download.pyで前処理された文書データを学習データに使用することを想定しています。以下のセルを実行して、必要な学習データをダウンロードし、解凍しましょう。

[ ]:
# download.py
import os.path
from six.moves.urllib import request
import zipfile


request.urlretrieve(
    'https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip',
    'trainDevTestTrees_PTB.zip')
zf = zipfile.ZipFile('trainDevTestTrees_PTB.zip')
for name in zf.namelist():
    (dirname, filename) = os.path.split(name)
    if not filename == '':
        zf.extract(name, '.')

以下のコマンドを実行して学習データが用意できたか確認してみましょう。

dev.txt  test.txt  train.txt

と表示されればダウンロードできています。

[4]:
!ls trees
dev.txt  test.txt  train.txt

test.txtの1行目を見て、各サンプルがどのように記述されているか見てみましょう。

[5]:
!head trees/dev.txt -n1
(3 (2 It) (4 (4 (2 's) (4 (3 (2 a) (4 (3 lovely) (2 film))) (3 (2 with) (4 (3 (3 lovely) (2 performances)) (2 (2 by) (2 (2 (2 Buy) (2 and)) (2 Accorsi))))))) (2 .)))

上記のように、各サンプルは木構造によって定義されています。

木構造を再帰的に(value, node)として定義していると思いますが、この時nodeに対するクラスラベルがvalueになります。

クラスラベルはそれぞれ、1(really negative)、2(negative)、3(neutral)、4(positive)、5(really positive)を表現しています。

試しにあるサンプルを図で表現したものが下記になります。

2. パラメータの設定

学習を行う際のパラメータをここで設定します。 * n_epoch:エポック数。学習時にtrainデータを何周させるか。 * n_units:ユニット数。Recursive Neural Networkの各ノードが何次元の隠れ状態ベクトルを持つか。 * batchsize:バッチサイズ。パラメータの更新をする際にいくつのtrainデータを一塊として学習させるか。 * n_label:ラベル数。識別するクラス数。今回は5ラベルあるので5。 * epoch_per_eval:何epochごとにvalidationを行うか。 * is_test:動作確認のために小さいデータセットを使うか。Trueなら小さいデータセットを使う。 * gpu_id:GPU ID。使用するGPUのID。Colaboratoryの場合0で良い。

[ ]:
# parameters
n_epoch = 100  # number of epochs
n_units = 30  # number of units per layer
batchsize = 25  # minibatch size
n_label = 5  # number of labels
epoch_per_eval = 5  # number of epochs per evaluation
is_test = True
gpu_id = 0

if is_test:
    max_size = 10
else:
    max_size = None

3. イテレータの準備

training、validation、testに使用するデータセットを読みこみ、Iteratorを作成しましょう。

まずは、str型で表現されている各サンプルをdictionary型で表現される木構造データに変換します。

パーサSexpParserによって実装されたread_corpusにより、文字列をtokenizeします。その後、tokenizeされた各サンプルをconvert_treeにより木構造データにします。このようにすることで、ラベルはint、nodeは2要素のtuple、木構造をdictionaryで表現することができ、元の文字列よりも扱いやすいデータ構造になります。

[ ]:
# data.py
import codecs
import re


class SexpParser(object):

    def __init__(self, line):
        self.tokens = re.findall(r'\(|\)|[^\(\) ]+', line)
        self.pos = 0

    def parse(self):
        assert self.pos < len(self.tokens)
        token = self.tokens[self.pos]
        assert token != ')'
        self.pos += 1

        if token == '(':
            children = []
            while True:
                assert self.pos < len(self.tokens)
                if self.tokens[self.pos] == ')':
                    self.pos += 1
                    break
                else:
                    children.append(self.parse())
            return children
        else:
            return token


def read_corpus(path, max_size):
    with codecs.open(path, encoding='utf-8') as f:
        trees = []
        for line in f:
            line = line.strip()
            tree = SexpParser(line).parse()
            trees.append(tree)
            if max_size and len(trees) >= max_size:
                break

    return trees


def convert_tree(vocab, exp):
    assert isinstance(exp, list) and (len(exp) == 2 or len(exp) == 3)

    if len(exp) == 2:
        label, leaf = exp
        if leaf not in vocab:
            vocab[leaf] = len(vocab)
        return {'label': int(label), 'node': vocab[leaf]}
    elif len(exp) == 3:
        label, left, right = exp
        node = (convert_tree(vocab, left), convert_tree(vocab, right))
        return {'label': int(label), 'node': node}

read_corpus()convert_tree() を使って、イテレーターを作成します。

[ ]:
vocab = {}

train_data = [convert_tree(vocab, tree)
                        for tree in read_corpus('trees/train.txt', max_size)]
train_iter = chainer.iterators.SerialIterator(train_data, batchsize)

validation_data = [convert_tree(vocab, tree)
                                 for tree in read_corpus('trees/dev.txt', max_size)]
validation_iter = chainer.iterators.SerialIterator(validation_data, batchsize,
                                                                                   repeat=False, shuffle=False)

test_data = [convert_tree(vocab, tree)
                        for tree in read_corpus('trees/test.txt', max_size)]

試しに、1つ目のtest_dataを表示してみましょう。以下のような木構造で表現されており、lableはそのnode以下のscoreを表現しており、葉nodeの数値は辞書vocab内の単語のidです。

[9]:
print(test_data[0])
{'label': 2, 'node': ({'label': 3, 'node': ({'label': 3, 'node': 252}, {'label': 2, 'node': 71})}, {'label': 1, 'node': ({'label': 1, 'node': 253}, {'label': 2, 'node': 254})})}

4. モデルの準備

使用するネットワークを定義しましょう。

traverseにより木構造データの各ノードを辿り、木全体での損失lossを計算します。traverseは再帰呼出しになっており、順々に子ノードをたどるような実装になっています。(木構造データを扱う時によくある実装ですね!)

まず、隠れ状態ベクトルvを計算します。リーフノードの場合、単語id wordからmodel.leaf(word)によってembedに保存されている隠れ状態ベクトルを取得します。中間ノードの場合、各子ノードから返された子ノードの隠れ状態ベクトルleftrightからv = model.node(left, right)により計算します。

loss += F.softmax_cross_entropy(y, t)で現在のノードのlossを子ノードのlossに足し合わせ、最後にreturn loss, vで親ノードにlossを返しています。

loss += F.softmax_cross_entropy(y, t)以降の行に正答率などをロギングするためのコードがありますが、modelの定義自体には不必要です。

[ ]:
class RecursiveNet(chainer.Chain):

    def traverse(self, node, evaluate=None, root=True):
        if isinstance(node['node'], int):
            # leaf node
            word = self.xp.array([node['node']], np.int32)
            loss = 0
            v = model.leaf(word)
        else:
            # internal node
            left_node, right_node = node['node']
            left_loss, left = self.traverse(left_node, evaluate=evaluate, root=False)
            right_loss, right = self.traverse(right_node, evaluate=evaluate, root=False)
            v = model.node(left, right)
            loss = left_loss + right_loss

        y = model.label(v)

        label = self.xp.array([node['label']], np.int32)
        t = chainer.Variable(label)
        loss += F.softmax_cross_entropy(y, t)

        predict = cuda.to_cpu(y.data.argmax(1))
        if predict[0] == node['label']:
            evaluate['correct_node'] += 1
        evaluate['total_node'] += 1

        if root:
            if predict[0] == node['label']:
                evaluate['correct_root'] += 1
            evaluate['total_root'] += 1

        return loss, v

    def __init__(self, n_vocab, n_units):
        super(RecursiveNet, self).__init__()
        with self.init_scope():
            self.embed = L.EmbedID(n_vocab, n_units)
            self.l = L.Linear(n_units * 2, n_units)
            self.w = L.Linear(n_units, n_label)

    def leaf(self, x):
        return self.embed(x)

    def node(self, left, right):
        return F.tanh(self.l(F.concat((left, right))))

    def label(self, v):
        return self.w(v)

    def __call__(self, x):
        accum_loss = 0.0
        result = collections.defaultdict(lambda: 0)
        for tree in x:
            loss, _ = self.traverse(tree, evaluate=result)
            accum_loss += loss

        reporter.report({'loss': accum_loss}, self)
        reporter.report({'total': result['total_node']}, self)
        reporter.report({'correct': result['correct_node']}, self)
        return accum_loss

ここで、__call__の実装に1つ注意があります。

__call__に渡されるxはミニバッチ化された入力データであり、[s_1, s_2, ..., s_N]のように各サンプルs_nが入っています。

画像認識で使うConvolutional Networkなどのネットワークの場合、ミニバッチxに対して一括で並列計算を行うことができます。しかし、今回のような木構造のネットワークの場合、以下の点で並列計算することが難しく、1つ1つのサンプルに対して計算を行い、最後に結果を集約するような実装になっています。

  • データ長がサンプルによって異なる
  • 各サンプルに対する計算順序が異なる

※実は、スタックを利用してRecursive Neural Networkでもミニバッチの並列計算を行うことができます。(発展)として後半で掲載しているので参照ください。

[ ]:
model = RecursiveNet(len(vocab), n_units)

if gpu_id >= 0:
    model.to_gpu()

# Setup optimizer
optimizer = chainer.optimizers.AdaGrad(lr=0.1)
optimizer.setup(model)
optimizer.add_hook(chainer.optimizer_hooks.WeightDecay(0.0001))

5. Updater・Trainerの準備と学習の実行

いつものように、UpdaterとTrainerを定義して、modelを学習させます。 今回、L.Classifierを使用せず、自分で精度accuracyを計算しています。extensions.MicroAverageを使用すると簡単に実装することができます。詳しくは、chainer.training.extensions.MicroAverageを参照ください。

[12]:
def _convert(batch, device):
  return batch

updater = chainer.training.StandardUpdater(
    train_iter, optimizer, device=gpu_id, converter=_convert)

trainer = chainer.training.Trainer(updater, (n_epoch, 'epoch'))
trainer.extend(
        extensions.Evaluator(validation_iter, model, device=gpu_id, converter=_convert),
        trigger=(epoch_per_eval, 'epoch'))
trainer.extend(extensions.LogReport())

trainer.extend(extensions.MicroAverage(
        'main/correct', 'main/total', 'main/accuracy'))
trainer.extend(extensions.MicroAverage(
        'validation/main/correct', 'validation/main/total',
        'validation/main/accuracy'))

trainer.extend(extensions.PrintReport(
        ['epoch', 'main/loss', 'validation/main/loss',
          'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))
trainer.run()
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
1           1620.74                           0.245495                                 5.10428
2           545.169     551.23                0.587838       0.442623                  7.81332
3           395.948                           0.72973                                  9.70386
4           374.31      546.901               0.731982       0.459016                  12.3539
5           345.539                           0.768018                                 14.2971
6           227.336     551.377               0.896396       0.494536                  16.9247
7           167.934                           0.936937                                 18.8253
8           126.277     570.017               0.954955       0.497268                  21.5186
9           98.0478                           0.972973                                 23.4103
10          77.475      594.685               0.984234       0.502732                  26.1162
11          62.3243                           0.986486                                 27.9949
12          50.843      617.205               0.990991       0.513661                  30.6482
13          42.1133                           0.990991                                 32.6093
14          35.4103     635.552               0.995495       0.513661                  35.2373
15          30.0894                           0.997748                                 37.1427
16          25.6345     650.352               0.997748       0.516393                  39.8406
17          22.1484                           0.997748                                 41.7452
18          19.4407     662.344               0.997748       0.516393                  44.3749
19          17.1996                           1                                        46.3293
20          15.3029     672.085               1              0.521858                  48.974
21          13.701                            1                                        50.8809
22          12.3821     680.558               1              0.521858                  53.5715
23          11.2896                           1                                        55.4855
24          10.3658     687.382               1              0.52459                   58.1776
25          9.57276                           1                                        60.0696
26          8.88406     692.939               1              0.527322                  62.705
27          8.28042                           1                                        64.6225
28          7.74716     697.521               1              0.532787                  67.2185
29          7.27279                           1                                        69.1315
30          6.84818     701.334               1              0.530055                  71.8325
31          6.46594                           1                                        73.7415
32          6.12004     704.532               1              0.530055                  76.3853
33          5.80552                           1                                        78.3144
34          5.51829     707.233               1              0.527322                  80.8981
35          5.25492                           1                                        82.809
36          5.01261     709.529               1              0.527322                  85.5038
37          4.78903                           1                                        87.3981
38          4.5823      711.501               1              0.527322                  90.0571
39          4.39086                           1                                        91.9491
40          4.21337     713.221               1              0.527322                  94.6118

6. テストデータでの性能の確認

[13]:
def evaluate(model, test_trees):
    result = collections.defaultdict(lambda: 0)
    with chainer.using_config('train', False), chainer.no_backprop_mode():
        for tree in test_trees:
            model.traverse(tree, evaluate=result)
    acc_node = 100.0 * result['correct_node'] / result['total_node']
    acc_root = 100.0 * result['correct_root'] / result['total_root']
    print(' Node accuracy: {0:.2f} %% ({1:,d}/{2:,d})'.format(
        acc_node, result['correct_node'], result['total_node']))
    print(' Root accuracy: {0:.2f} %% ({1:,d}/{2:,d})'.format(
        acc_root, result['correct_root'], result['total_root']))

print('Test evaluation')
evaluate(model, test_data)
Test evaluation
 Node accuracy: 50.00 %% (156/312)
 Root accuracy: 40.00 %% (4/10)

(発展) Recursive Neural Networkにおけるミニバッチ化[1]

Recursive Neural Networkは、以下の点からミニバッチ化されたデータを並列計算することが難しいです。

  • データ長がサンプルによって異なる
  • 各サンプルに対する計算順序が異なる

しかし、スタックを利用してRecursive Neural Networkでもミニバッチの並列計算を行うことができます。

Dataset, Iteratorの用意

まず、Recursive Neural Networkの再帰的な伝播計算をスタックを用いる直列的な計算に変換するために、データセットを直列的なデータセットに変換します。

木構造データセットの各ノードに対して、以下のように木に対して帰りがけ順に番号を振ります。

帰りがけ順とは、木構造のノードに番号をつける手順の一つで、全ての子ノードに親ノードよりも小さい番号をつける手順です。この手順で割り当てられたノードを、番号の小さい順にだどりながら処理を行うと、必ず親ノードの前に子ノードをたどることができます。

[ ]:
def linearize_tree(vocab, root, xp=np):
    # Left node indexes for all parent nodes
    lefts = []
    # Right node indexes for all parent nodes
    rights = []
    # Parent node indexes
    dests = []
    # All labels to predict for all parent nodes
    labels = []

    # All words of leaf nodes
    words = []
    # Leaf labels
    leaf_labels = []

    # Current leaf node index
    leaf_index = [0]

    def traverse_leaf(exp):
        if len(exp) == 2:
            label, leaf = exp
            if leaf not in vocab:
                vocab[leaf] = len(vocab)
            words.append(vocab[leaf])
            leaf_labels.append(int(label))
            leaf_index[0] += 1
        elif len(exp) == 3:
            _, left, right = exp
            traverse_leaf(left)
            traverse_leaf(right)

    traverse_leaf(root)

    # Current internal node index
    node_index = leaf_index
    leaf_index = [0]

    def traverse_node(exp):
        if len(exp) == 2:
            leaf_index[0] += 1
            return leaf_index[0] - 1
        elif len(exp) == 3:
            label, left, right = exp
            l = traverse_node(left)
            r = traverse_node(right)

            lefts.append(l)
            rights.append(r)
            dests.append(node_index[0])
            labels.append(int(label))

            node_index[0] += 1
            return node_index[0] - 1

    traverse_node(root)
    assert len(lefts) == len(words) - 1

    return {
        'lefts': xp.array(lefts, 'i'),
        'rights': xp.array(rights, 'i'),
        'dests': xp.array(dests, 'i'),
        'words': xp.array(words, 'i'),
        'labels': xp.array(labels, 'i'),
        'leaf_labels': xp.array(leaf_labels, 'i'),
    }
[ ]:
xp = cuda.cupy if gpu_id >= 0 else np

vocab = {}

train_data = [linearize_tree(vocab, t, xp)
                        for t in read_corpus('trees/train.txt', max_size)]
train_iter = chainer.iterators.SerialIterator(train_data, batchsize)

validation_data = [linearize_tree(vocab, t, xp)
                       for t in read_corpus('trees/dev.txt', max_size)]
validation_iter = chainer.iterators.SerialIterator(
    validation_data, batchsize, repeat=False, shuffle=False)

test_data = [linearize_tree(vocab, t, xp)
                       for t in read_corpus('trees/test.txt', max_size)]

試しに、1つ目のtest_dataを表示してみましょう。

leftsには親ノードdestsに対する左ノードのindex、rightsには親ノードdestsに対する右ノードのindex、destsには親ノードのindex、wordsには葉ノードの単語id、labelsには親ノードのラベル、leaf_labelsには葉ノードのラベルが入った辞書が生成されています。

[16]:
print(test_data[0])
{'lefts': array([0, 2, 4], dtype=int32), 'rights': array([1, 3, 5], dtype=int32), 'dests': array([4, 5, 6], dtype=int32), 'words': array([252,  71, 253, 254], dtype=int32), 'labels': array([3, 1, 2], dtype=int32), 'leaf_labels': array([3, 2, 1, 2], dtype=int32)}

ミニバッチ化可能なモデルの定義

Recursive Neural Networkでは、葉ノードに対して埋め込みベクトルを計算する操作Aと、2つの子ノードの隠れ状態ベクトルから親ノードの隠れ状態ベクトルを計算する操作Bの2つがあります。

各サンプルに対して、帰りがけ順にノードにindexをふりました。帰りがけ順にノードをたどると、葉ノードでは操作Aを、それ以外のノードでは操作Bを行えばよいことがわかります。

この操作はスタックを利用して、木構造を走査しているとみなすこともできます。スタックとは後入れ先出しのデータ構造で、データを追加するプッシュ操作と、最後にプッシュされたデータを取得するポップ操作の2つを行えます。

操作Aのときは計算結果をスタックにプッシュする操作を、操作Bのときは2つのデータをポップし、その計算結果をプッシュする操作を行います。

上記操作を並列化しするとき、各サンプルごとに木構造は違うので、うまく分岐して操作Aと操作Bを行う必要があります。この時、スタックを使うことによって、異なる木構造のデータに対しても同様な処理の単純な繰り返しを行うことでRecursive Neural Networkの計算を行うことができます。そのため、並列化可能です。

[ ]:
from chainer import cuda
from chainer.utils import type_check


class ThinStackSet(chainer.Function):
    """Set values to a thin stack."""

    def check_type_forward(self, in_types):
        type_check.expect(in_types.size() == 3)
        s_type, i_type, v_type = in_types
        type_check.expect(
            s_type.dtype.kind == 'f',
            i_type.dtype.kind == 'i',
            s_type.dtype == v_type.dtype,
            s_type.ndim == 3,
            i_type.ndim == 1,
            v_type.ndim == 2,
            s_type.shape[0] >= i_type.shape[0],
            i_type.shape[0] == v_type.shape[0],
            s_type.shape[2] == v_type.shape[1],
        )

    def forward(self, inputs):
        xp = cuda.get_array_module(*inputs)
        stack, indices, values = inputs
        stack[xp.arange(len(indices)), indices] = values
        return stack,

    def backward(self, inputs, grads):
        xp = cuda.get_array_module(*inputs)
        _, indices, _ = inputs
        g = grads[0]
        gv = g[xp.arange(len(indices)), indices]
        g[xp.arange(len(indices)), indices] = 0
        return g, None, gv


def thin_stack_set(s, i, x):
    return ThinStackSet()(s, i, x)

さらに、ここでは単純なスタックではなく、シンスタック[2]を使います。

文長を\(I\)、隠れベクトルの次元数を\(D\)とすると、シンスタックは\((2I-1) \times D\)の行列を使いまわすことでメモリ領域を効率的に利用することができます。

通常のスタックでは\(O(I^2 D)\)の空間計算量を必要とするところ、シンスタックは\(O(ID)\)で済みます。

プッシュ操作thin_stack_setとポップ操作thin_stack_getによって実現しています。

まずは、chainer.Functionを継承したThinStackSetThinStackGetを定義します。

ThinStackSetは文字通り、シンスタックに値をセットするための関数です。

forwardbackwardinputsは、stack, indices, values = inputsのように分解できます。

stackは名前の通り、シンスタック自身で関数の引数にすることで、関数間で受け渡すようにしています。

というのも、chainer.Functionは内部に状態を持たない構造になっており、関数の引数で受け渡すことで、外部でstackを管理するようになっています。

[ ]:
class ThinStackGet(chainer.Function):

    def check_type_forward(self, in_types):
        type_check.expect(in_types.size() == 2)
        s_type, i_type = in_types
        type_check.expect(
            s_type.dtype.kind == 'f',
            i_type.dtype.kind == 'i',
            s_type.ndim == 3,
            i_type.ndim == 1,
            s_type.shape[0] >= i_type.shape[0],
        )

    def forward(self, inputs):
        xp = cuda.get_array_module(*inputs)
        stack, indices = inputs
        return stack[xp.arange(len(indices)), indices], stack

    def backward(self, inputs, grads):
        xp = cuda.get_array_module(*inputs)
        stack, indices = inputs
        g, gs = grads
        if gs is None:
            gs = xp.zeros_like(stack)
        if g is not None:
            gs[xp.arange(len(indices)), indices] += g
        return gs, None


def thin_stack_get(s, i):
    return ThinStackGet()(s, i)

ThinStackGetは文字通り、シンスタックから値を取得するための関数です。

forwardbackwardinputsは、stack, indices = inputsのように分解できます。

[ ]:
class ThinStackRecursiveNet(chainer.Chain):

    def __init__(self, n_vocab, n_units, n_label):
        super(ThinStackRecursiveNet, self).__init__(
            embed=L.EmbedID(n_vocab, n_units),
            l=L.Linear(n_units * 2, n_units),
            w=L.Linear(n_units, n_label))
        self.n_units = n_units

    def leaf(self, x):
        return self.embed(x)

    def node(self, left, right):
        return F.tanh(self.l(F.concat((left, right))))

    def label(self, v):
        return self.w(v)

    def __call__(self, *inputs):
        batch = len(inputs) // 6
        lefts = inputs[0: batch]
        rights = inputs[batch: batch * 2]
        dests = inputs[batch * 2: batch * 3]
        labels = inputs[batch * 3: batch * 4]
        sequences = inputs[batch * 4: batch * 5]
        leaf_labels = inputs[batch * 5: batch * 6]

        inds = np.argsort([-len(l) for l in lefts])
        # Sort all arrays in descending order and transpose them
        lefts = F.transpose_sequence([lefts[i] for i in inds])
        rights = F.transpose_sequence([rights[i] for i in inds])
        dests = F.transpose_sequence([dests[i] for i in inds])
        labels = F.transpose_sequence([labels[i] for i in inds])
        sequences = F.transpose_sequence([sequences[i] for i in inds])
        leaf_labels = F.transpose_sequence([leaf_labels[i] for i in inds])

        batch = len(inds)
        maxlen = len(sequences)

        loss = 0
        count = 0
        correct = 0

        # thin stack
        stack = self.xp.zeros((batch, maxlen * 2, self.n_units), 'f')

        # 葉ノードの隠れ状態ベクトルとlossを計算
        for i, (word, label) in enumerate(zip(sequences, leaf_labels)):
            batch = word.shape[0]
            es = self.leaf(word)
            ds = self.xp.full((batch,), i, 'i')
            y = self.label(es)
            loss += F.softmax_cross_entropy(y, label, normalize=False) * batch
            count += batch
            predict = self.xp.argmax(y.data, axis=1)
            correct += (predict == label.data).sum()

            stack = thin_stack_set(stack, ds, es)

        # 中間ノードの隠れ状態ベクトルとlossを計算
        for left, right, dest, label in zip(lefts, rights, dests, labels):
            l, stack = thin_stack_get(stack, left)
            r, stack = thin_stack_get(stack, right)
            o = self.node(l, r)
            y = self.label(o)
            batch = l.shape[0]
            loss += F.softmax_cross_entropy(y, label, normalize=False) * batch
            count += batch
            predict = self.xp.argmax(y.data, axis=1)
            correct += (predict == label.data).sum()

            stack = thin_stack_set(stack, dest, o)

        loss /= count
        reporter.report({'loss': loss}, self)
        reporter.report({'total': count}, self)
        reporter.report({'correct': correct}, self)
        return loss
[20]:
model = ThinStackRecursiveNet(len(vocab), n_units, n_label)

if gpu_id >= 0:
    model.to_gpu()

optimizer = chainer.optimizers.AdaGrad(0.1)
optimizer.setup(model)
[20]:
<chainer.optimizers.ada_grad.AdaGrad at 0x7f8a3c453710>

Updater・Trainerの準備と学習の実行

では、さっそく新しく定義したThinStackRecursiveNetをモデルにして学習させてみましょう。ミニバッチを並列計算することができるようになったので、学習が高速になっていることがわかると思います。

[21]:
def convert(batch, device):
    if device is None:
        def to_device(x):
            return x
    elif device < 0:
        to_device = cuda.to_cpu
    else:
        def to_device(x):
            return cuda.to_gpu(x, device, cuda.Stream.null)

    return tuple(
        [to_device(d['lefts']) for d in batch] +
        [to_device(d['rights']) for d in batch] +
        [to_device(d['dests']) for d in batch] +
        [to_device(d['labels']) for d in batch] +
        [to_device(d['words']) for d in batch] +
        [to_device(d['leaf_labels']) for d in batch]
    )


updater = chainer.training.StandardUpdater(
    train_iter, optimizer, device=None, converter=convert)
trainer = chainer.training.Trainer(updater, (n_epoch, 'epoch'))
trainer.extend(
    extensions.Evaluator(validation_iter, model, converter=convert, device=None),
    trigger=(epoch_per_eval, 'epoch'))
trainer.extend(extensions.LogReport())

trainer.extend(extensions.MicroAverage(
    'main/correct', 'main/total', 'main/accuracy'))
trainer.extend(extensions.MicroAverage(
    'validation/main/correct', 'validation/main/total',
    'validation/main/accuracy'))

trainer.extend(extensions.PrintReport(
   ['epoch', 'main/loss', 'validation/main/loss',
     'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))

trainer.run()
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
1           1.75582                           0.268018                                 0.772637
2           1.0503      1.52234               0.63964        0.448087                  1.74078
3           0.752925                          0.743243                                 2.52495
4           1.21727     1.46956               0.745495       0.456284                  3.49669
5           0.681582                          0.817568                                 4.24974
6           0.477964    1.5514                0.880631       0.480874                  5.22265
7           0.38437                           0.916667                                 5.98324
8           0.30405     1.68066               0.923423       0.469945                  6.94833
9           0.222884                          0.959459                                 7.69772
10          0.175159    1.79104               0.977477       0.478142                  8.67923
11          0.142888                          0.97973                                  9.43108
12          0.118272    1.87948               0.986486       0.47541                   10.4046
13          0.0991659                         0.997748                                 11.1994
14          0.0841932   1.95415               0.997748       0.478142                  12.1657
15          0.0723124                         0.997748                                 12.9141
16          0.0627568   2.01682               0.997748       0.480874                  13.8787
17          0.0549726                         1                                        14.6336
18          0.04857     2.07107               1              0.478142                  15.6061
19          0.0432675                         1                                        16.3584
20          0.0388425   2.1181                1              0.480874                  17.3297
21          0.035117                          1                                        18.0761
22          0.0319522   2.15905               1              0.478142                  19.0487
23          0.0292416                         1                                        19.8416
24          0.0269031   2.1951                1              0.480874                  20.8083
25          0.0248729                         1                                        21.5566
26          0.0231      2.22721               1              0.483607                  22.5304
27          0.0215427                         1                                        23.2878
28          0.0201669   2.25614               1              0.486339                  24.2565
29          0.018944                          1                                        25.0171
30          0.017851    2.28247               1              0.480874                  26.0063
31          0.0168687                         1                                        26.7633
32          0.0159814   2.30664               1              0.483607                  27.7331
33          0.0151763                         1                                        28.5342
34          0.0144427   2.32898               1              0.483607                  29.5039
35          0.0137716                         1                                        30.257
36          0.0131555   2.34976               1              0.483607                  31.2306
37          0.0125881                         1                                        31.9842
38          0.0120638   2.3692                1              0.483607                  32.9617
39          0.0115783                         1                                        33.7175
40          0.0111272   2.38747               1              0.483607                  34.6946

だいぶ速くなりましたね!

Reference

[1] 深層学習による自然言語処理 (機械学習プロフェッショナルシリーズ)

[2] [A Fast Unified Model for Parsing and Sentence Understanding](http://nlp.stanford.edu/pubs/bowman2016spinn.pdf)