機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

TheanoでNER(モデル構築)

前回は前処理部分を簡単に理解した。
kento1109.hatenablog.com
前回も載せたがコードはここ。
github.com

今回はメインのモデル構築について整理していく。

モデル構築(model.py)



train.pyの

f_train, f_eval = model.build(**parameters)

でモデルを構築する。
**parametersは辞書の各要素を展開して渡す)

build関数の中身を見ていく。

Network variables

シンボル (変数) の宣言

Sentence length
s_len = (word_ids if word_dim else char_pos_ids).shape[0]

word_dimは、デフォルト100だったので、標準の場合、word_idsの長さとなる。
s_lenも、TensorVariable)
word_dimは、Word Embeddingの次元数

Word inputs
input_dim = 0
inputs = []

if word_dim:  # default="100"
    input_dim += word_dim
    word_layer = EmbeddingLayer(n_words, word_dim, name='word_layer')
    word_input = word_layer.link(word_ids)
    inputs.append(word_input)

EmbeddingLayerクラスの__init__では、
引数で指定した形のembedding行列」を作成する。
作成される行列はこのような形。
f:id:kento1109:20171202155147p:plain

  • その行列の要素の共有変数embeddingsを初期化する。
  • embeddingsparamsに追加する。

次にword_layerのlink関数を呼ぶ。
link関数は、embeddings行列のうち、word_idsに対応する部分を返す。
inputが、ミニバッチの場合、戻り値は「ミニバッチ数×入力系列の長さ×入力次元」三階テンソルとなる。
イメージはこんな感じ。

import numpy as np
np.set_printoptions(precision=3)

vocab = 10
dims = 5
emd = np.random.rand(vocab, dims)
[[ 0.214  0.891  0.836  0.201  0.412]
 [ 0.516  0.24   0.819  0.587  0.428]
 [ 0.245  0.06   0.075  0.433  0.318]
 [ 0.505  0.949  0.388  0.704  0.744]
 [ 0.839  0.285  0.553  0.113  0.327]
 [ 0.59   0.759  0.233  0.983  0.436]
 [ 0.919  0.009  0.885  0.882  0.987]
 [ 0.988  0.278  0.763  0.63   0.496]
 [ 0.76   0.871  0.465  0.652  0.789]
 [ 0.387  0.846  0.056  0.268  0.682]]

target = np.array([[1, 3], [2, 6], [0, 5]])

emd[target]
[[[ 0.516  0.24   0.819  0.587  0.428]
  [ 0.505  0.949  0.388  0.704  0.744]]

 [[ 0.245  0.06   0.075  0.433  0.318]
  [ 0.919  0.009  0.885  0.882  0.987]]

 [[ 0.214  0.891  0.836  0.201  0.412]
  [ 0.59   0.759  0.233  0.983  0.436]]]

事前学習済みのembeddingを使う場合、

new_weights = word_layer.embeddings.get_value()

word_layerembeddingsの値を取得し、

for i in xrange(n_words):
    word = self.id_to_word[i]
    if word in pretrained:
        new_weights[i] = pretrained[word]
        c_found += 1
    elif word.lower() in pretrained:
        new_weights[i] = pretrained[word.lower()]
        c_lower += 1
    elif re.sub('\d', '0', word.lower()) in pretrained:
        new_weights[i] = pretrained[
            re.sub('\d', '0', word.lower())
        ]
        c_zeros += 1

new_weights に事前学習済みの重みをセットし、

word_layer.embeddings.set_value(new_weights)

word_layerembeddingsに値を再セットする。
これで事前学習済みのデータセットに存在する単語は、その重みで更新され、存在しない単語は初期値のembeddings行列になる。

Chars inputs
if char_dim:  # default="25"
    input_dim += char_lstm_dim
    char_layer = EmbeddingLayer(n_chars, char_dim, name='char_layer')

    char_lstm_for = LSTM(char_dim, char_lstm_dim, with_batch=True,
                         name='char_lstm_for')
    # bi-directional
    char_lstm_rev = LSTM(char_dim, char_lstm_dim, with_batch=True,
                         name='char_lstm_rev')

    char_lstm_for.link(char_layer.link(char_for_ids))
    char_lstm_rev.link(char_layer.link(char_rev_ids))

    char_for_output = char_lstm_for.h.dimshuffle((1, 0, 2))[
        T.arange(s_len), char_pos_ids
    ]
    char_rev_output = char_lstm_rev.h.dimshuffle((1, 0, 2))[
        T.arange(s_len), char_pos_ids
    ]

    inputs.append(char_for_output)
    if char_bidirect:
        inputs.append(char_rev_output)
        input_dim += char_lstm_dim

input_dimであるが、word_dim(default 100)+char_dim(default 25)なので、125になる。
EmbeddingLayerは全文字数×25のEmbedding行列を作成する。
次にLSTM層(前向き+後ろ向き)を構築する。
char_lstm_dimは、default="25"
LSTMクラスの__init__では、
引数で指定した形の重み・バイアス」を作成する。
作成する重みは下記の通り(バイアスは省略)

  • Input gate weights
    • w_xi:入力層×隠れ層
    • w_hi:隠れ層×隠れ層
    • w_ci:隠れ層×隠れ層
  • Forget gate weights(※コメントアウトされている)
    • w_xf:入力層×隠れ層
    • w_hf:隠れ層×隠れ層
    • w_cf:隠れ層×隠れ層
  • Output gate weights
    • w_xo:入力層×隠れ層
    • w_ho:隠れ層×隠れ層
    • w_co:隠れ層×隠れ層
  • Cell weights
    • w_xc:入力層×隠れ層
    • w_hc:隠れ層×隠れ層

※LSTM層は、「文字、単語を入力とする場合で処理が異なる

  1. 文字(with_batch=True):単語数×入力系列の長さ×次元数(3階テンソル
  2. 単語(with_batch=False):入力系列の長さ×次元数(行列)

少しわかりにくかったので図でまとめる。
例えば、下記の文章があった場合、

Tom is my best friend.

word embedding の場合、
inputは下記のようになる。(ベクトル)

一方、char embedding の場合、
inputは下記のようになる。(行列)

char_lstm_revは後ろ向き用LSTMインスタンス
次にchar_lstm_forのlink関数を呼ぶ。
引数は、char_layerのlink関数。
embeddings行列のうち、char_for_idsに対応する部分を返す。
char_for_idsは、シンボル (変数)
char_lstm_forのlink関数は、embedding層の出力であることが分かる。
link関数は、LSTMの出力式を計算する関数。
忘却ゲート付きの標準形は下記
kento1109.hatenablog.com
ただし、デフォルトでは、忘却ゲート無しの構造
なので、前回はメモリセルの更新式は
c(t)=i(t)\odot \tilde { c }(t) +f(t)\odot c(t-1)
f(t)(忘却ゲート)の値で前回のメモリセルの値を調整
であったが、ここでは、
c(t)=i(t)\odot \tilde { c }(t) +(1-i(t))\odot c(t-1)
としてメモリセルを更新している。
あと、前回の入力ゲートの計算式は、
i(t)=\sigma(W_ix(t)+U_ih(t-1)+W_ic(t-1)+b_i)
であり、LSTMオリジナルモデル
i(t)=\sigma(W_ix(t)+U_ih(t-1)+b_i)
W_ic(t-1)が加わっている点が異なる。
同様に出力ゲートの計算式の
o(t)=\sigma(W_ox(t)+U_oh(t-1)+W_oc(t)+b_o)
であり、LSTMオリジナルモデル
o(t)=\sigma(W_ox(t)+U_oh(t-1)+b_o)
W_oc(t)が加わっている点が異なる。
(但し、この点については論文では特に言及されていない)

link関数の戻り値は、h[-1]なので、
値はミニバッチ(単語)数×次元数(行列)となる。
(系列データの最後の出力値のみを戻す)
*文字・単語と何れもの場合も1単語に1つの出力となる。

次でhの次元を入れ替え

char_for_output = char_lstm_for.h.dimshuffle((1, 0, 2))[
    T.arange(s_len), char_pos_ids
]

hは、入力系列の長さ×ミニバッチ(単語)数×次元数なので、これを「ミニバッチ(単語)数×入力系列の長さ×次元数」に変更する。

inputs.append(char_for_output)

char_for_outputinputsに加算。
inputsは入力データの全ての特徴量を含めたもの

if char_bidirect:  # default="1"
    inputs.append(char_rev_output)
    input_dim += char_lstm_dim

で後ろ向きのLSTMも入力に含める。
(加えた場合、input_dimは150になる。)

Capitalization feature
if cap_dim:  # default="0"
    input_dim += cap_dim
    cap_layer = EmbeddingLayer(n_cap, cap_dim, name='cap_layer')
    inputs.append(cap_layer.link(cap_ids))
Prepare final input

これまでのinputsを結合する。

if len(inputs) != 1:
    inputs = T.concatenate(inputs, axis=1)
Dropout on final input
if dropout:
    dropout_layer = DropoutLayer(p=dropout)
    input_train = dropout_layer.link(inputs)
    input_test = (1 - dropout) * inputs
    inputs = T.switch(T.neq(is_train, 0), input_train, input_test)

neq関数の復習
neq関数は、ベクトル同士の各要素の一致(不一致)を真偽値で返す。(不一致の場合、Trueを返す)

import numpy
import theano
import theano.tensor as T

y = numpy.array([1, 0, 1, 1, 1])
y_pred = numpy.array([1, 1, 1, 0, 1])
T.neq(y_pred, y).eval()
>> [False  True False  True False]
T.mean(T.neq(y_pred, y)).eval()
>> 0.4

switch関数の復習
switch関数は、第1引数が真の場合は第2引数、偽の場合は第3引数の値を返すで返す。

from theano import tensor as T
from theano.ifelse import ifelse

a,b = T.scalars('a', 'b')
x,y = T.matrices('x', 'y')

z_switch = T.switch(T.lt(a, b), T.mean(x), T.mean(y))

inputsの値は、 is_train

  • 0でない場合は、input_train,
  • 0の場合input_test

となる。

LSTM for words
word_lstm_for = LSTM(input_dim, word_lstm_dim, with_batch=False,
                     name='word_lstm_for')
word_lstm_rev = LSTM(input_dim, word_lstm_dim, with_batch=False,
                     name='word_lstm_rev')
word_lstm_for.link(inputs)
word_lstm_rev.link(inputs[::-1, :])
word_for_output = word_lstm_for.h
word_rev_output = word_lstm_rev.h[::-1, :]

Chars inputsのLSTM層と異なるのは、
with_batch=False」の部分。
また、出力系列は、

word_for_output = word_lstm_for.h

としており、入力系列に対する全ての出力を返している。
(文字の場合、最後の状態のみ)

(あとは同じ)
この論文の実装では、
訓練時においてもデータをバッチ化しておらず、訓練データの単位は1文書ずつとしている。
また、

word_lstm_rev.link(inputs[::-1, :])

で単語を末尾から入力し、その出力値は末尾から並んでいるので、

word_rev_output = word_lstm_rev.h[::-1, :]

で先頭からの順番に戻していることを押さえておく。
次に、後ろ向きLSTMを含めるかの判定

if word_bidirect:  # default="1"
    final_output = T.concatenate(
        [word_for_output, word_rev_output],
        axis=1
    )
    tanh_layer = HiddenLayer(2 * word_lstm_dim, word_lstm_dim,
                             name='tanh_layer', activation='tanh')
    final_output = tanh_layer.link(final_output)
else:
    final_output = word_for_output

含める場合、隠れ層を追加している。
(追加してもしなくても、出力の形を同じにするため)
これは、絵にするとこんな感じ。
f:id:kento1109:20171205165626p:plain:w300

Sentence to Named Entity tags - Score

出力層の部分。
最後にfinal_outputの値を出力層に渡す。

final_layer = HiddenLayer(word_lstm_dim, n_tags, name='final_layer',
                          activation=(None if crf else 'softmax'))  # default="1"
tags_scores = final_layer.link(final_output)

CRFを接続する場合、活性化関数は不要なので、None(この場合、恒等写像となる。)
その場合、この恒等写像結果をタグスコアとして使用する。
CRFを接続しない場合、softmax関数により、各タグの確率値を出力する。

長くなったので、ここで区切る。
とりあえず、LSTM層までは読めた。
次は、CRF層についてまとめようと思う。