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行列」を作成する。
作成される行列はこのような形。
- その行列の要素の共有変数
embeddings
を初期化する。 embeddings
をparams
に追加する。
次に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_layer
のembeddings
の値を取得し、
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_layer
のembeddings
に値を再セットする。
これで事前学習済みのデータセットに存在する単語は、その重みで更新され、存在しない単語は初期値の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層は、「文字、単語を入力とする場合で処理が異なる」
- 文字(
with_batch=True
):単語数×入力系列の長さ×次元数(3階テンソル) - 単語(
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
ただし、デフォルトでは、忘却ゲート無しの構造
なので、前回はメモリセルの更新式は
※(忘却ゲート)の値で前回のメモリセルの値を調整
であったが、ここでは、
としてメモリセルを更新している。
あと、前回の入力ゲートの計算式は、
であり、LSTMオリジナルモデル
にが加わっている点が異なる。
同様に出力ゲートの計算式の
であり、LSTMオリジナルモデル
にが加わっている点が異なる。
(但し、この点については論文では特に言及されていない)
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_output
をinputs
に加算。
※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
含める場合、隠れ層を追加している。
(追加してもしなくても、出力の形を同じにするため)
これは、絵にするとこんな感じ。
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層についてまとめようと思う。