PyTorch DL for NLP -Bi-LSTM CRF-

前回はLSTMを使ってPOS tagging(品詞タグ付け)を行いました。

 

今回は応用編として、bidirectional LSTMとCRFを組み合わせたモデルを使ってNERを行います。

PyTorch公式チュートリアルの以下のページに沿って進めます。コードは以下のページからダウンロードしたものを一部改変して使っています。

 

環境

  • MacBook Pro (Retina, 15-inch, Mid 2015)
  • macOS High Sierra 10.13.5
  • Python 3.6.3
  • PyTorch 0.4.0

 

Named Entity Recognition: NER

固有表現抽出と訳されます。

固有表現とは人名や地名などの固有名詞、日時や数量などの数的表現のことです。

 

NERで使われるタグは2つの要素からなります。

IOBフォーマット: 始まり(Beginning)、中間(Inside)、外部(Outside)を表現

NEタイプ: 組織名(ORG)、人名(PER)、地名(LOC)など

 

例えば以下の文は、

  • John lives in New York

このようにタグづけされます。

  • B-PER, O, O, B-LOC, I-LOC

Johnは人名かつ固有表現の始まりなのでB-PER、

livesとinは固有表現ではないのでO、

Newは「New York」という固有表現の始まりなのでB-LOC、

Yorkは先のNewの続きなのでI-LOCです。

 

NERではハードルとなる点がいくつかあります。

  • 同音異義語: Onというベトナムの地名と前置詞を見分けなくてはいけない
  • NE同士の曖昧性: Mayは日付にも人名にもなり得る
  • 複数単語: Stanford Universityは単語の始まりと終わりを見つけなくてはいけない

 

NERやNEについて詳しくは以下の書籍などを参照ください。

入門 自然言語処理
Posted with Amakuri at 2018.6.8
Steven Bird, Ewan Klein, Edward Loper
オライリージャパン

 

英語版はウェブにて無料で公開されているのでこちらをご覧になるのも良いでしょう。

 

Long-Short Term Memory Netoworks: LSTM

LSTMについては前回触れたので深くは解説しません。

端的に表現すると、RNNの一種です。

それまでのRNNで課題だった長期記憶のメカニズムを実現したモデルです。

 

模式図は以下のようになります。

引用: Bidirectional LSTM-CRF Models for Sequence Tagging

編みかけの部分の内部でLSTMに特徴的な計算を行なっているのですが、ここでは紹介を省きます。

詳しくは前回の記事をご覧ください。

 

Bidirectional LSTM: bi-LSTM

bidirectional LSTMではその名の通り、双方向性のLSTMです。

上記のLSTMと合わせて、系列データを反対の順から流すLSTMを用います。

引用: Bidirectional LSTM-CRF Models for Sequence Tagging

 

これにより、LSTMを上回る精度の分析ができます。

例えば以下のような文で考えます。

  •  I sleep at 10 PM.

LSTMではatの後ろに数字が来ていることから、この数字は時間を表していると判断します。

bi-LSTMではそれに加えて、その数字の後ろにPMがあることからより確信を持って時間を表していると判断ができるのです。

単純に考えて(本当に単純に考えて)判断材料が2倍になるということです。

 

Conditional Random Field: CRF

条件付き確率場と訳します。

言語学習のための機械学習入門では「対数線形モデルを系列ラベリング問題に適用したもの」というように定義しています。

先述のLSTMやbi-LSTMはPOS taggingで優れた結果を出しました。

しかし、出力されるラベルに依存性があるような問題では難が残ります。

 

例えばNERではB-PERの後には以下の3通りしか現れません

  • I-PER: 人名が続く場合
  • B-XXX: 他の固有表現が始まる場合
  • O: 固有表現以外が出現する場合

B-PERの後にはI-LOCやI-ORGが来ては行けないという強い制約があるのです。

 

そこで、bi-LSTMの出力をCRFに入力し、CRFが最終的な出力(ラベル)を計算するという手法が考案されました。

これによってNERを高い精度で実行できるようになりました。

 

今回のLSTMに続くCRFでの処理を例に取ってCRFの働きを確認します。

CRFでは以下のような系列データX(今回の例ではxiは文章の各単語に対するLSTMの出力)が与えられます。

予測されるラベルを以下のようなyと定義します。

 

この時、スコアs(X, y)は以下のように定義されます。

ここでAはタグの遷移スコアです。はi番目のタグからj番目のタグへ遷移する時のスコアを表します。

またy0とynはそれぞれSTARTタグ、STOPタグと特別なタグが割り振られます。

AはNEタグ(k個)にこの2つのタグを含めた(k+2)×(k+2)の正方行列になります。

一方PはLSTMの出力をまとめたものになります。

ある単語の分散表現がLSTMに入力されると、k個のタグに対するスコアすなわち要素k個のベクトルが出力されます。

文章にはn個の単語が含まれているので、すべての出力をまとめたPはn×kの行列になります。

 

改めてs(X, y)を見てみます。

第一項はLSTMの出力したyの遷移に関するスコア(CRFが担保するタグのつながりの正しさ)、

第二項はLSTMによるyのスコア(LSTMが担保する各単語のタグの正しさ)であることがわかります。

つまり、LSTMにとって確信度の高い予測yであっても、CRFにとってあり得ないタグの遷移であればスコアは下がるというわけです。

 

CRFは対数線形モデルなので、条件付き確率p(y|X)は前述のスコアs(X, y)を用いて以下のように表現できます。

Yxは出力yが取り得るすべてのパターン(IOBフォーマットにそぐわないものも含めて)です。従って、分母はp(y|X)を0から1の範囲に収めるためのものです。

 

学習過程では上記確率の対数、以下のlog(p(y|X))を最大化させるようネットワークのパラメータを更新します。

 

そして学習したネットワークを用いて予測をするときは、スコアを最大化するyを選択します。

なお、このときyの組み合わせはn個の単語とk個のラベルがあれば(k+2)^nと計算量が大きいのですが、後述のヴィタビアルゴリズムを導入することで計算の効率化を実現しています。

 

bi-LSTM CRF実装

模式図は以下のようになります。bi-LSTMの出力をCRFに投入して、文章内の単語に対するNERを行います。

引用: Neural Architectures for Named Entity Recognition

これによって高い精度でNERができる、とのことです。

詳しくは画像の引用元の論文を参照してみてください。

 

では実際に公式チュートリアルのコードを紐解いてみます。

 

まずは諸々インポート。

 

後々使う便利関数を設定しておきます。

まずはベクトルの最大要素のindexを返すargmax()

 

系列データをインデックのリストに変換するprepare_sequence()

 

log_sum_exp()

 

モデルの実装部分です。ひとまずメソッド名だけ見ます。

かなり長いですが、これは損失関数をクラスの中で定義していることが理由の一つです。

一つずつ確認します。

 

まずはコンストラクタ__init__()です。

 

次に_forward_alg()です。これは損失関数の計算に使われます。

小さい方が好ましいです。

ここで使われてる関数については以下に補足します。

  • torch.full(): 第一引数の形のtensorのすべての要素に第二引数の値を代入
  • torch.Tensor.expand(): 引数の形式に引き伸ばす
  • torch.cat(): 与えられたシーケンスを繋げる、第二引数は軸の指定

 

_get_lstm_features()forward()でLSTMの部分の計算として実行されます。

前回実装したLSTMの出力を取得します。

 

_score_sentence()はs(X, y)を計算します。

 

_viterbi_decode()はLSTMの出力を受け入れ、それから入力文章に対応するNEタグのリストを予測します。

これと合わせて出力NEタグのスコアを算出します。

n個の単語がそれぞれk個のタグをとり得るので場合の数はk^nとなりますが、ヴィタビアルゴリズムで計算することにより効率的に計算しています。

 

 

neg_log_likelihood()_forward_alg()_score_sentence()の返り値の差で表されます。

損失関数なので当然小さい方が好ましいです。

 

最後にforward()について見ます。

まず_get_lstm_features()で入力した系列データに対するLSTMの出力を得ます。

これを_viterbi_decode()に渡し、予測した出力tag_seq(論文のy)とそのスコアを得ます(論文のs(X, y))。

 

いよいよ学習の部分です。

 

以上でbidirectional LSTMとCRFを組み合わせたNERが実装できました。

ちゃんとしたデータセットで検証してみます。

 

Exercise: A new loss function for discriminative tagging

追加課題、望み薄ですが興味がわいたらやってみます。

 

元の論文

Bidirectional LSTM CRFを提案した元の論文を読んでみて、公開されているソースコードを使ってNERを試してみました。

 

参考

 

 

入門 自然言語処理
Posted with Amakuri at 2018.6.8
Steven Bird, Ewan Klein, Edward Loper
オライリージャパン

 

コメントを残す