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
に保存されている隠れ状態ベクトルを取得します。中間ノードの場合、各子ノードから返された子ノードの隠れ状態ベクトルleft
、right
から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
を継承したThinStackSet
とThinStackGet
を定義します。
ThinStackSet
は文字通り、シンスタックに値をセットするための関数です。
forward
、backward
のinputs
は、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
は文字通り、シンスタックから値を取得するための関数です。
forward
、backward
のinputs
は、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)