はじめに
最近、固有表現抽出(Named Entity Recognition: NER)の学習をspaCyを用いて行う機会があったため、そのやり方について簡単にまとめたいと思います。
Ref
Version
- python: 3.11.3
- spaCy: 3.6.0
使用したNotebook
全体の流れ
- 学習データの用意
- spaCyのconfigファイルの用意
- 学習
- 評価
- 推論
学習データの用意
今回は、ストックマーク株式会社が公開しているWikipediaを用いた日本語の固有表現抽出データセットを利用します。
まずはデータセットを読み込みます。
with open("../ner-wikipedia-dataset/ner.json") as f: stockmark_data = json.load(f)
次にデータセットを、train, dev, testに分割します。(StockmarkData
は自分で定義したデータ型です。気になる方はNotebookを見てみてください。)
def random_split_to_train_dev_test(data: StockmarkData) -> tuple[StockmarkData, StockmarkData, StockmarkData]: all_len = len(data) train_len = int(all_len * 0.6) dev_len = train_len + int(all_len * 0.2) random.shuffle(data) train = data[:train_len] dev = data[train_len:dev_len] test = data[dev_len:] return train, dev, test train, dev, test = random_split_to_train_dev_test(stockmark_data)
次に、データセットをspaCyの学習データの形式(.spacyファイル)へと変換します。
def make_spacy(data: StockmarkData, name: str) -> None: nlp = spacy.blank("ja") db = DocBin() for training_example in tqdm(data): text = training_example['text'] annotations = training_example['entities'] doc = nlp(text) ents = [] for annotation in annotations: start = annotation["span"][0] end = annotation["span"][1] label = annotation["type"] span = doc.char_span(start, end, label=label) if span is None: print("Skipping entity") else: ents.append(span) doc.ents = ents db.add(doc) db.to_disk(f"../data/{name}.spacy") make_spacy(data=train, name="train") make_spacy(data=dev, name="dev") make_spacy(data=test, name="test")
こちらのコードについて、補足説明をします。
doc = nlp(text) ents = [] for data in annotations: start = data["span"][0] end = data["span"][1] label = data["type"] span = doc.char_span(start, end, label=label)
まず、上記コードにてannotation
の情報を、spacy.tokens.span.Span
に変換しています。
例えば、
- start: 0
- end: 10
- text:
Sansan株式会社は、「出会いからイノベーションを生み出す」をミッションとして掲げています
とした時、span
は
- 出力:
Sansan株式会社
- データ型:
spacy.tokens.span.Span
となります。
下記はコード例です。
text = "Sansan株式会社は、「出会いからイノベーションを生み出す」をミッションとして掲げています" start = 0 end = 10 label = "法人名" nlp = spacy.blank("ja") doc = nlp(text) span = doc.char_span(start, end, label=label) print(span) print(type(span))
出力は下記となります。
Sansan株式会社 <class 'spacy.tokens.span.Span'>
このspan
について、startとendが正しく与えられていてもNoneになってしまうパターンが存在します。例えば、下記のようなパターンです。
text = "また、草戸稲荷神社前には遊女町を造ったといわれる。" start = 3 end = 9 label = "施設名" nlp = spacy.blank("ja") doc = nlp(text) span = doc.char_span(start, end, label=label) print(f"期待出力: {text[start:end]}") print(f"実際の出力: {span}") print(f"データ型: {type(span)}")
出力は下記となります。
期待出力: 草戸稲荷神社 実際の出力: None データ型: <class 'NoneType'>
原因は、トークナイザーで区切られる単位と、span
の単位が一致していないためです。こちらのissueにて記載があります。
実際に確認してみましょう。今回のモデルで利用しているtokenizerで、また、草戸稲荷神社前には遊女町を造ったといわれる。
を分かち書きしてみます。
nlp = spacy.blank("ja") doc = nlp("また、草戸稲荷神社前には遊女町を造ったといわれる。") print([token for token in doc])
出力は下記となります。
[また, 、, 草戸, 稲荷, 神社前, に, は, 遊女, 町, を, 造っ, た, と, いわ, れる, 。]
理想としては、草戸稲荷神社
が1tokenとして区切られて欲しいですが、草戸, 稲荷, 神社前
と区切られてしまいました。
このような場合、span
はNone
を返します。
良い対処法が思いつかないため、もったいないですが、今回はNone
を返すspan
を無視して進めることにします。下記コードにて、span
がNone
の場合にはSkipping entity
の出力のみを行い、span
の追加は行わないコードにしています。
if span is None: print("Skipping entity") else: ents.append(span)
全固有表現数が13185なのに対し、None
のspan
は58だったため、学習データが大幅に減ってしまうこともありません。
spaCyのconfigファイルの用意
spaCyの学習方法として推奨されているのは、configファイルに全ての設定を記載し、spacy train
コマンドにて学習をすることです。
そのため、Training Pipelines & Models上にて、configファイルを取得する機能が提供されています。
まず、基本的な設定を行ったconfigファイルを、spaCyのサイトから取得します。
その後、下記のコマンドにて、defaultのsettingを自動補完することができます。
!python -m spacy init fill-config ../config/base_config.cfg ../config/config.cfg
どのような補完がされたか気になったためdiffを見てみると、先頭部分は下記のようになっていました。
学習
下記コマンドにて学習を行います。--output
には、学習したモデルを保存するパスを、--paths.train
, --paths.dev
には、作成した.spacyファイルを指定します。
!python -m spacy train ../config/config.cfg --output ./ --paths.train ../data/train.spacy --paths.dev ../data/dev.spacy
学習を開始すると、下記のようなメトリクスが出力されます。
============================= Training pipeline ============================= ℹ Pipeline: ['tok2vec', 'ner'] ℹ Initial learn rate: 0.001 E # LOSS TOK2VEC LOSS NER ENTS_F ENTS_P ENTS_R SCORE --- ------ ------------ -------- ------ ------ ------ ------ 0 0 0.00 51.59 0.00 0.00 0.00 0.00 0 200 241.81 3057.86 3.64 6.87 2.48 0.04 0 400 1078.83 4190.00 26.86 30.05 24.28 0.27 0 600 1349.77 4286.86 39.50 40.39 38.65 0.40 0 800 1011.61 4476.62 41.54 45.30 38.35 0.42 1 1000 777.36 4225.80 48.91 50.56 47.35 0.49 1 1200 495.57 4679.23 51.48 53.36 49.72 0.51 2 1400 673.15 5070.43 51.82 51.44 52.20 0.52 3 1600 709.94 5342.73 60.23 58.76 61.76 0.60 4 1800 1003.89 5872.42 65.90 67.48 64.39 0.66 5 2000 1082.21 5468.31 64.72 64.94 64.50 0.65 7 2200 1442.92 5723.62 66.89 69.06 64.84 0.67 8 2400 1394.84 5288.18 67.29 70.11 64.69 0.67 10 2600 1279.43 4287.84 67.86 70.37 65.52 0.68 12 2800 1198.31 3641.89 68.14 68.67 67.62 0.68 14 3000 1144.16 2891.50 67.43 68.76 66.15 0.67 16 3200 1105.80 2452.79 69.87 71.60 68.22 0.70 17 3400 1026.97 2153.10 68.54 72.55 64.95 0.69 19 3600 865.50 1604.89 69.96 71.33 68.63 0.70 21 3800 1082.88 1617.34 69.45 71.02 67.95 0.69 23 4000 874.30 1329.86 69.70 71.66 67.84 0.70 25 4200 1089.45 1332.97 69.24 72.10 66.60 0.69 27 4400 736.01 963.30 70.68 73.19 68.33 0.71 28 4600 894.48 1005.92 70.06 72.69 67.62 0.70 30 4800 670.78 769.95 69.16 69.89 68.44 0.69 32 5000 678.29 727.29 70.85 70.79 70.92 0.71 34 5200 659.66 590.80 70.98 72.40 69.61 0.71 36 5400 708.78 641.77 69.84 71.88 67.92 0.70 37 5600 823.40 590.72 70.17 72.12 68.33 0.70 39 5800 659.13 492.57 70.63 70.60 70.66 0.71 41 6000 755.01 528.94 69.45 70.57 68.37 0.69 43 6200 1380.70 704.92 70.39 74.31 66.87 0.70 45 6400 864.96 453.74 69.56 70.83 68.33 0.70 47 6600 908.95 383.52 69.67 69.22 70.13 0.70 48 6800 947.74 391.76 69.36 71.87 67.02 0.69 ✔ Saved pipeline to output directory model-last
メトリクスの説明はspaCyのUnderstanding the training output and score typesにあります。
FはF-Score, PはPrecision, RはRecallです。
また、TOK2VECはtokenからvectorを抽出するcomponentを意味します。
評価
下記コマンドにて評価を行います。--output
にて、評価を行ったファイルを出力するパスを指定し、--displacy-path
にて、test.spacyに対しタグ付けしたファイルを出力するパスを指定します。
!python -m spacy benchmark accuracy model-best ../data/test.spacy --output ../evaluate_result/test_metrics.json --displacy-path ../evaluate_result
評価が終わると、下記のようなログが表示されます。
================================== Results ================================== TOK 100.00 NER P 70.85 NER R 68.16 NER F 69.48 SPEED 1604 =============================== NER (per type) =============================== P R F 人名 74.68 78.90 76.73 法人名 71.00 66.60 68.73 イベント名 77.99 62.31 69.27 地名 70.90 85.49 77.51 施設名 59.05 53.68 56.24 製品名 56.70 51.84 54.16 政治的組織名 80.33 73.28 76.65 その他の組織名 69.09 41.53 51.88
この結果は、../evaluate_result/test_metrics.json
にも出力されます。
次に、../evaluate_result/entities.html
を確認します。
本ファイルは、test.spacy
にタグ付けしたhtmlファイルです。どのようにタグ付けされたかが一目で分かりますね。
推論
学習したモデルを呼び出して推論を行いたい場合は、下記のようにします。出力されたモデルはmodel-last
とmodel-best
がありますが、今回はmodel-best
を利用します。
model = spacy.load("model-best") text = """ 働き方を変えるDXサービスを提供するSansan株式会社は、契約DXサービス「Contract One」がサービス価値向上を目的に、マイクロソフト社が提供するAzure OpenAI Serviceを活用した「Contract One AI」を搭載したことを発表します。 今回は第一弾として文章内検索機能を追加します。契約書の内容について、定型質問から選択または質問内容を直接問いかけると、「Contract One AI」が適切な情報を抽出し質問に回答します。本機能の追加によって、法務担当者に限らず誰もが早く、簡単に契約情報を把握することが可能となります。「Contract One AI」は順次アップデートしていく予定です。 """ colors = {"法人名": "#F67DE3", "製品名": "#7DF6D9"} options = {"colors": colors} doc = model(text) spacy.displacy.render(doc, style="ent", options=options, jupyter=True)
textはこちらから引用しました。
結果は下記のようになりました。このテキストについてはうまく抽出できていることが分かります。
終わりに
spaCyを用いると、データの形式を合わせるだけで、NERの学習・評価・推論までが簡単なコード・コマンドで実行出来て非常に便利です。また、configファイルに設定を全て残しておけるため、実験の再現性も担保できそうです。
疑問点やおかしいところがあったらコメントやTwitterで教えてください。
Twitterアカウントはこちら twitter.com