PyTorch:Bi-LSTM+CRF①(学習)
はじめに
今回は、Bi-LSTM+CRFに関して整理する。
最近の深層学習を用いた系列ラベリングに関する論文は、この手法でSOTAを達成していることが多い。
尚、Bi-LSTM+CRFの基本的なことに関しては、以前のTheano
での記事で述べた。
kento1109.hatenablog.com
Theano
では、遷移素性の計算をscan
を用いて実装していた。(正直、理解するのが難しかった・・)今回は、Pytorch
の場合、そこをどう実装しているのかに着目して読んでいきたい。
以下のチュートリアルでBi-LSTM+CRFまで実装してくれているという親切さである。
Advanced: Making Dynamic Decisions and the Bi-LSTM CRF — PyTorch Tutorials 0.3.0.post4 documentation
では、さっそくみていく。
LSTMに関しては
前回まとめたので重複する部分は割愛する。
モデル構築・学習
以下の通り
class BiLSTM_CRF(nn.Module): def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim): super(BiLSTM_CRF, self).__init__() self.embedding_dim = embedding_dim self.hidden_dim = hidden_dim self.vocab_size = vocab_size self.tag_to_ix = tag_to_ix self.tagset_size = len(tag_to_ix) self.word_embeds = nn.Embedding(vocab_size, embedding_dim) self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, num_layers=1, bidirectional=True) # Maps the output of the LSTM into tag space. self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size) # Matrix of transition parameters. Entry i,j is the score of # transitioning *to* i *from* j. self.transitions = nn.Parameter( torch.randn(self.tagset_size, self.tagset_size)) # These two statements enforce the constraint that we never transfer # to the start tag and we never transfer from the stop tag self.transitions.data[tag_to_ix[START_TAG], :] = -10000 self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000 self.hidden = self.init_hidden()
LSTM層の出力を線形変換した結果をself.hidden2tag
に保存するまでは前回と同じ。
ただしラベルには、「開始・終了ラベル」を追加しておく。
self.hidden2tag
は以下のような「単語系列×ラベル」の行列(観測素性)。
B | I | O | <START> | <STOP> | |
---|---|---|---|---|---|
Mark | 3.7 | 0.4 | 0.2 | 0.1 | 0.2 |
Watney | 1.6 | 3.1 | 0.3 | 0.7 | 0.1 |
visited | 0.2 | 0.5 | 1.7 | 0.4 | 0.8 |
Mars | 3.6 | 0.4 | 1.1 | 0.2 | 0.5 |
次に以下のような遷移素性行列パラメータを作成する。
self.transitions = nn.Parameter( torch.randn(self.tagset_size, self.tagset_size))
B | I | O | <START> | <STOP> | |
---|---|---|---|---|---|
B | 0.1 | 1.3 | 0.8 | -0.2 | 0.4 |
I | 0.1 | 1.0 | 1.3 | 0.4 | 0.5 |
O | 0.7 | 0.2 | 0.8 | -0.1 | 1.1 |
<START> | 0.5 | 0.1 | 0.9 | 1.1 | -0.4 |
<STOP> | -0.2 | -0.3 | -0.1 | 0.4 | 0.2 |
※列j
から行i
への遷移
ランダムで初期化した後、以下のあり得ない遷移「-10000」の重みをセットする。
<START>
への遷移<STOP>
からの遷移
self.transitions.data[tag_to_ix[START_TAG], :] = -10000 self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000
B | I | O | <START> | <STOP> | |
---|---|---|---|---|---|
B | 0.1 | 1.3 | 0.8 | -0.2 | -10000 |
I | 0.1 | 1.0 | 1.3 | 0.4 | -10000 |
O | 0.7 | 0.2 | 0.8 | -0.1 | -10000 |
<START> | -10000 | -10000 | -10000 | -10000 | -10000 |
<STOP> | -0.2 | -0.3 | -0.1 | 0.4 | -10000 |
損失値の計算
CRFの学習の目的は、系列 に対応するラベルの確率最大化、すなわち
さて、確率 であるがsoftmax関数を用いて以下のように定義される。
- は、系列 に対応するラベル のスコアに対数を取った値。
- は、系列 に対応するラベル のスコアに対数を取った値の総和。
尚、 は、素性関数 の対数を取った下式で定義される。
計算を簡単にするため確率 の対数を取る。
上式が最大となるよう学習を行う。
つまり、 が最小となるよう学習を行う。
実際、neg_log_likelihood
の戻り値は、
forward_score - gold_score
と書かれており、これを損失値として扱っている。
このことを頭に入れてコードを読んでいく。
学習時は
neg_log_likelihood = model.neg_log_likelihood(sentence_in, targets)
でネットワークを実行して損失値を得る。
def neg_log_likelihood(self, sentence, tags): feats = self._get_lstm_features(sentence) forward_score = self._forward_alg(feats) gold_score = self._score_sentence(feats, tags) return forward_score - gold_score
get_lstm_features
では、観測素性行列を得る。
それを引数にして_forward_alg
を呼ぶ。
読んだ感じ_forward_alg
がCRFの学習におけるメインとなる処理の1つと思われる。
_forward_alg
の計算を行う。
# Iterate through the sentence for feat in feats: alphas_t = [] # The forward variables at this timestep for next_tag in range(self.tagset_size): # broadcast the emission score: it is the same regardless of # the previous tag emit_score = feat[next_tag].view( 1, -1).expand(1, self.tagset_size)
外側のループで系列から単語毎の素性ベクトルを取り出す。
B | I | O | <START> | <STOP> | |
---|---|---|---|---|---|
Mark | 3.7 | 0.4 | 0.2 | 0.1 | 0.2 |
Watney | 1.6 | 3.1 | 0.3 | 0.7 | 0.1 |
visited | 0.2 | 0.5 | 1.7 | 0.4 | 0.8 |
Mars | 3.6 | 0.4 | 1.1 | 0.2 | 0.5 |
B | I | O | <START> | <STOP> | |
---|---|---|---|---|---|
Mark | 3.7 | 0.4 | 0.2 | 0.1 | 0.2 |
Watney | 1.6 | 3.1 | 0.3 | 0.7 | 0.1 |
visited | 0.2 | 0.5 | 1.7 | 0.4 | 0.8 |
Mars | 3.6 | 0.4 | 1.1 | 0.2 | 0.5 |
emit_score
に格納する。
emit_score = feat[next_tag].view( 1, -1).expand(1, self.tagset_size)
emit_score
Mark | 3.7 | 3.7 | 3.7 | 3.7 | 3.7 |
---|
次に遷移素性ベクトルを以下で得る。
trans_score = self.transitions[next_tag].view(1, -1)
B | I | O | <START> | <STOP> | |
---|---|---|---|---|---|
B | 0.1 | 1.3 | 0.8 | -0.2 | -10000 |
I | 0.1 | 1.0 | 1.3 | 0.4 | -10000 |
O | 0.7 | 0.2 | 0.8 | -0.1 | -10000 |
<START> | -10000 | -10000 | -10000 | -10000 | -10000 |
<STOP> | -0.2 | -0.3 | -0.1 | 0.4 | -10000 |
- 観測素性ベクトル:Markのラベル「B」の素性ベクトル
- 遷移素性ベクトル:先頭ラベル~Markのラベル「B」への遷移素性ベクトル
スコアは観測素性と遷移素性の加算であるが、ここで1つ問題が生じる。この計算の場合、任意の先頭ラベルからのラベル「B」への素性を計算しているが、実際には先頭ラベルは<START>
以外あり得ない。この制約を課すために最初に下記のようなベクトルを作っていた。これをベクトルの足し算に加えることで、先頭ラベルが<START>
以外のスコアを低くしている。
init_alphas = torch.Tensor(1, self.tagset_size).fill_(-10000.) init_alphas[0][self.tag_to_ix[START_TAG]] = 0. forward_var = autograd.Variable(init_alphas)
これのベクトルをlog_sum_exp
により、指数計算した総和の対数を取ることで、スカラー値を得る。
系列の最後まで計算したら単語毎の素性ベクトルに指数計算した総和の対数を取ったスカラー値をneg_log_likelihood
に返す。
これが、 に相当する。
_score_sentence
の計算を行う関数。
これは、先ほどの _forward_alg
に比べると計算が簡単。
計算は系列の正解ラベルの値のみを単語ごとに加算していく。
上述したが、計算後の
forward_score - gold_score
を損失値として返す。
メインの訓練部分では、これを最小化するように最適化を行う。
長くなったので一旦、区切る。
次は、予測に関するコードを見ていく。