nikkie-ftnextの日記

イベントレポートや読書メモを発信

「機械学習を使って30分で固有表現抽出器を作るチュートリアル」を写経しました

はじめに

わーい、ホッテントリ、わーい!🙌1 nikkieです。

固有表現抽出(NER)タスクをCRF(Conditional Random Fields2)で解く実装の理解を深めたく、チュートリアルで素振りしました。

目次

Hironsanによるチュートリアル

素振りに選んだチュートリアルはこちら。

Hironsan(中山光樹さん)は機械学習Python本の著者・訳者3であり、doccanoの非常に活動的なコントリビューターとも認識しています。

信頼できる方が過去に書いたチュートリアルであり、Qiita上でいいねやストックが多く(500超え)、CRFでNERを解くというドンピシャな内容だったので、最初に触るチュートリアルとして選びました。

素振り成果物

1つの巨大スクリプトにせずに、モジュール分割したのが工夫点です。

動作環境4

チュートリアルの概要

使うデータ

Hironsan作成のラベル付きデータを使います

README.mdより

hironsan.txtは、ウィキニュース日本語版をMeCab形態素解析してIOB2タグでタグ付けしたコーパスです。
全部で500文にタグ付けしています。

素性(特徴量)抽出

CRFはディープラーニングより前の機械学習モデルであり、人手で特徴量を作る必要があり、それが性能を左右するという認識です。
素性抽出パートで特徴量を作っていきます。

チュートリアルの「概要」より

今回は、前後2文字の単語、品詞細分類、文字種、固有表現タグを使います。

具体的なデータで見ていきましょう。
hironsan.txtの1文目です。

2005 名詞  数 *   *   *   *   *   B-DAT
年 名詞  接尾  助数詞   *   *   *   年 ネン  ネン  I-DAT
7   名詞  数 *   *   *   *   *   I-DAT

(省略)

た 助動詞   *   *   *   特殊・タ    基本形   た タ タ O
。 記号  句点  *   *   *   *   。 。 。 O

以下のように読み込まれています。

>>> train_sents[0][0]
['2005', '名詞', '数', '*', '*', '*', '*', '*', 'B-DAT']
>>> train_sents[0][1]
['年', '名詞', '接尾', '助数詞', '*', '*', '*', '年', 'ネン', 'ネン', 'I-DAT']
>>> train_sents[0][2]
['7', '名詞', '数', '*', '*', '*', '*', '*', 'I-DAT']
>>> train_sents[0][-1]
['。', '記号', '句点', '*', '*', '*', '*', '。', '。', '。', 'O']

作った素性はこちら。

1語目の「2005」は先行する文字がなく、後に続く「年」と「7」から素性が作られます。

>>> pprint(X_train[0][0])
['bias',
 'word=2005',
 'type=ZDIGIT',
 'postag=名詞-数',
 'BOS',
 'BOS',
 '+1:word=年',
 '+1:type=OTHER',
 '+1:postag=名詞-接尾-助数詞',
 '+2:word=7',
 '+2:type=ZDIGIT',
 '+2:postag=名詞-数']

3語目の「7」は、先行する「2005」「年」、後に続く「月」「14」から素性が作られます。

>>> pprint(X_train[0][2])
['bias',
 'word=7',
 'type=ZDIGIT',
 'postag=名詞-数',
 '-2:word=2005',
 '-2:type=ZDIGIT',
 '-2:postag=名詞-数',
 '-2:iobtag=B-DAT',
 '-1:word=年',
 '-1:type=OTHER',
 '-1:postag=名詞-接尾-助数詞',
 '-1:iobtag=I-DAT',
 '+1:word=月',
 '+1:type=OTHER',
 '+1:postag=名詞-一般',
 '+2:word=14',
 '+2:type=ZDIGIT',
 '+2:postag=名詞-数']

文の最後の語の「。」は、後に続く語がないので、先行する2語「れ」「た」から素性が作られます。

>>> pprint(X_train[0][-1])
['bias',
 'word=。',
 'type=OTHER',
 'postag=記号-句点',
 '-2:word=れ',
 '-2:type=HIRAG',
 '-2:postag=動詞-接尾',
 '-2:iobtag=O',
 '-1:word=た',
 '-1:type=HIRAG',
 '-1:postag=助動詞',
 '-1:iobtag=O',
 'EOS',
 'EOS']

写経した実装はfeature_engineering.pyにあります。
写経する中で私はほとんど同じコードをどうしても何度も書きたくなかったので、少し関数化するという工夫をしました(30分で動かすためにコピペするのも全然いいと思います)。

CRFsuiteを訓練

今回はpython-crfsuiteを使っています。

TrainerTaggerの扱いは(scikit-learnのインタフェースに慣れた身からすると)独特ですね

  • Trainer5
    • appendメソッドでデータをもたせる
    • set_paramsメソッドでハイパーパラメタ指定
    • trainで訓練(fit)。渡したパスに保存できる
  • Tagger6
    • 初期化してからopenでファイルから読み込む
    • tagメソッドで推論(predict)

モデルの評価、結果の再現!

チュートリアルの結果は再現しました!

              precision    recall  f1-score   support

       B-ART       1.00      0.89      0.94         9
       I-ART       0.92      1.00      0.96        12
       B-DAT       1.00      1.00      1.00        12
       I-DAT       1.00      1.00      1.00        22
       B-LOC       1.00      0.95      0.97        55
       I-LOC       0.94      0.94      0.94        17
       B-ORG       0.75      0.86      0.80        14
       I-ORG       1.00      0.90      0.95        10
       B-PSN       0.00      0.00      0.00         3
       B-TIM       1.00      0.71      0.83         7
       I-TIM       1.00      0.81      0.90        16

   micro avg       0.96      0.91      0.94       177
   macro avg       0.87      0.82      0.84       177
weighted avg       0.95      0.91      0.93       177
 samples avg       0.14      0.14      0.14       177

scikit-learnのバージョンが新しいため末尾のavgの行が多いのだと思います。
チュートリアルとは「weighted avg」の行が一致しますね。

終わりに

CRFで固有表現抽出するチュートリアルで素振りしました。
重複コードが減るよう実装を工夫しつつ、チュートリアルの結果が再現したので満足です。

一通り動くようになったコードが手元にあるので、理解を深めるために改造を試していこうと思います(どうか次回がありますように)