🦨

Label Studioによる固有表現抽出のデータアノテーション ~ spacy のファインチューニング

この記事では、Label Studioを使用して固有表現抽出(NER)のデータ作成を行う方法、作成したデータを用いてspacyのモデルをfine-tuningする方法についてまとめます。

Label Studioについて

Label StudioはLLMのfine-tuningや訓練に用いるデータを作成できるツールの1つです。
特徴として、

  1. インストール・立ち上げがとても簡単
  2. 物体検出やNER、話者分離など、多様なタスクに対応している

などがあげられます。また、この記事では触れませんが、

  1. 機械学習モデルを用いたpre-annotationやactive-learning
  2. 外部データベースとの連携

なども行えるようです。

インストール

pip だけでインストールできます。

pip install label-studio

ほかにもHomebrewやDockerなどでインストールすることもできます。詳細は公式ドキュメントをご参照ください。

インストールできた場合は、次のコマンドでLabel Studioを立ち上げられます。

label-studio

localhost:8080 に接続して以下のような画面が表示されたら成功です。

HumanSignal. Label Studioのログイン画面のスクショ

アノテーション

ユーザー作成

「SIGN UP」を選択し、適当なメールアドレス、パスワードを入力してアカウントを作成してください。(メールアドレスは実際に使用しているものでも、user@example.com のようなものでもよいと思います。)

無事にユーザー作成ができた場合は以下のような画面に移ります。

HumanSignal. Label Studioのプロジェクト一覧画面のスクショ

プロジェクト作成

  1. 「Create Project」からプロジェクト作成画面を開く。

  2. 必要な場合は「Project Name」、「Description」を設定する。

  3. 「Data Import」からデータをインポートする。

    ファイルの例
    マシンラーニング・ソリューションズは機械学習ベンチャー企業へ様々な支援を行う株式会社である。
    マシンラーニング・ソリューションズは2017年に設立された。
    世界レベルの機械学習研究者をマシンラーニング・ソリューションズは技術顧問として招聘している。
    

    .txtファイルをインポートすると「Treat CSV/TSV as ...」と選択肢が出てきますが、「List of tasks」を選ぶようにしてください。

    また.txt ファイルの場合、1つの行が1つのデータに分割されます。改行を含むデータを扱いたい場合などは他の形式を用いる必要があります

  4. 「Labeling Setup」から「Named Entity Recognition」を選択する。


    HumanSignal. Label Studioのテンプレート選択画面のスクショ

    新たなラベルを追加するなどの細かな設定はここで行うことができます。詳細は公式ドキュメントをご参照ください。

  5. 「Save」を押す。以下のようにデータが追加されていれば成功。


    HumanSignal. Label Studioのプロジェクト画面のスクショ

アノテーションの作成

「Label All Tasks」をクリックするとアノテーション画面に移ります。1つのデータにだけラベル付けしたい場合は、そのデータをクリックすればよいです。

つけたいラベルを選択し、文章の該当部分を範囲選択することでラベル付けできます。
以下は、“マシンラーニング・ソリューションズ”にラベルORGをつける際の画面です。


HumanSignal. Label Studioのアノテーション画面のスクショ

すべてのラベルを付け終えたら「Submit」で保存できます。「Label All Tasks」を押して始めた場合は次のデータのアノテーション画面に移り、そうでない場合はアノテーションが終了します。

fine-tuning

spacyのインストール

pip install -U pip setuptools
pip install -U spacy
python -m spacy download ja_core_news_sm
動作確認

spacyを用いてNERを行うコードの例です。

import spacy

nlp = spacy.load("ja_core_news_sm")

txt = "マシンラーニング・ソリューションズのオフィスは東京都にある。"

doc = nlp(txt)
for ent in doc.ents:
    print(ent.text, ent.label_)

以下のような出力が得られます。

東京都 GPE

作成したデータのダウンロード・変換

まず、アノテーションの作成 を参考に、データのラベル付けを行ってください。

プロジェクト画面右上の「Export」を押し、「JSON-MIN」を選択してから右下の「Export」を押すとデータをJSON形式でダウンロードできます。


HumanSignal. Label Studioのエクスポート画面のスクショ

出力されるファイルの例
[
  {
    "text": "マシンラーニング・ソリューションズは機械学習ベンチャー企業へ様々な支援を行う株式会社である。",
    "id": 1,
    "label": [
      {
        "start": 0,
        "end": 17,
        "text": "マシンラーニング・ソリューションズ",
        "labels": [
          "ORG"
        ]
      }
    ],
    "annotator": 2,
    "annotation_id": 159,
    "created_at": "2024-04-02T00:36:58.236828Z",
    "updated_at": "2024-04-02T00:36:58.236870Z",
    "lead_time": 126.952
  },
  {
    "text": "マシンラーニング・ソリューションズは2017年に設立された。",
    "id": 2,
    "label": [
      {
        "start": 0,
        "end": 17,
        "text": "マシンラーニング・ソリューションズ",
        "labels": [
          "ORG"
        ]
      }
    ],
    "annotator": 2,
    "annotation_id": 160,
    "created_at": "2024-04-02T00:37:06.698209Z",
    "updated_at": "2024-04-02T00:37:06.698239Z",
    "lead_time": 7.445
  },
  {
    "text": "世界レベルの機械学習研究者をマシンラーニング・ソリューションズは技術顧問として招聘している。",
    "id": 3,
    "label": [
      {
        "start": 14,
        "end": 31,
        "text": "マシンラーニング・ソリューションズ",
        "labels": [
          "ORG"
        ]
      }
    ],
    "annotator": 2,
    "annotation_id": 161,
    "created_at": "2024-04-02T00:37:42.870619Z",
    "updated_at": "2024-04-02T00:37:42.870645Z",
    "lead_time": 33.072
  }
]

そのままではfine-tuningに使用できないため、以下のようなコードによって.spacy ファイルに変換する必要があります。

コード例
import spacy
from spacy.tokens import DocBin
import json


nlp = spacy.blank("ja")
with open("/path/to/annotation_data.json", encoding="utf-8") as f:
    training_data = json.loads(f.read())

db = DocBin()
for annotation in training_data:
    doc = nlp(annotation["text"])
    ents = []
    for label in annotation["label"]:
        span = doc.char_span(label["start"], label["end"], label["labels"][0])
        if span is not None:
            ents.append(span)
    doc.ents = ents
    db.add(doc)

db.to_disk("./train.spacy")
db.to_disk("./dev.spacy")

 
train.spacydev.spacyは異なるべきですが、今回は簡単のためにどちらも同じにしました。

fine-tuning

  1. base_config.cfg ファイルを作成し、以下の内容を入力する。

    base_config.cfg の中身
    base_config.cfg
    # This is an auto-generated partial config. To use it with 'spacy train'
    # you can run spacy init fill-config to auto-fill all default settings:
    # python -m spacy init fill-config ./base_config.cfg ./config.cfg
    [paths]
    train = null
    dev = null
    vectors = null
    [system]
    gpu_allocator = null
    
    [nlp]
    lang = "ja"
    pipeline = ["tok2vec","ner"]
    batch_size = 1000
    
    [components]
    
    [components.tok2vec]
    factory = "tok2vec"
    
    [components.tok2vec.model]
    @architectures = "spacy.Tok2Vec.v2"
    
    [components.tok2vec.model.embed]
    @architectures = "spacy.MultiHashEmbed.v2"
    width = ${components.tok2vec.model.encode.width}
    attrs = ["NORM", "PREFIX", "SUFFIX", "SHAPE"]
    rows = [5000, 1000, 2500, 2500]
    include_static_vectors = false
    
    [components.tok2vec.model.encode]
    @architectures = "spacy.MaxoutWindowEncoder.v2"
    width = 96
    depth = 4
    window_size = 1
    maxout_pieces = 3
    
    [components.ner]
    factory = "ner"
    
    [components.ner.model]
    @architectures = "spacy.TransitionBasedParser.v2"
    state_type = "ner"
    extra_state_tokens = false
    hidden_width = 64
    maxout_pieces = 2
    use_upper = true
    nO = null
    
    [components.ner.model.tok2vec]
    @architectures = "spacy.Tok2VecListener.v1"
    width = ${components.tok2vec.model.encode.width}
    
    [corpora]
    
    [corpora.train]
    @readers = "spacy.Corpus.v1"
    path = ${paths.train}
    max_length = 0
    
    [corpora.dev]
    @readers = "spacy.Corpus.v1"
    path = ${paths.dev}
    max_length = 0
    
    [training]
    dev_corpus = "corpora.dev"
    train_corpus = "corpora.train"
    
    [training.optimizer]
    @optimizers = "Adam.v1"
    
    [training.batcher]
    @batchers = "spacy.batch_by_words.v1"
    discard_oversize = false
    tolerance = 0.2
    
    [training.batcher.size]
    @schedules = "compounding.v1"
    start = 100
    stop = 1000
    compound = 1.001
    
    [initialize]
    vectors = ${paths.vectors}
    
  2. python -m spacy init fill-config base_config.cfg config.cfg を実行する。

  3. 作成されたconfig.cfg の一部を以下のように書き替える。

    base_config.cfg
    [components.ner]
    - factory = "ner"
    + source = "ja_core_news_sm"
    incorrect_spans_key = null
    
    base_config.cfg
    [components.tok2vec]
    - factory = "tok2vec"
    + source = "ja_core_news_sm"
    
  4. python -m spacy train config.cfg --output ./output --paths.train ./train.spacy --paths.dev ./dev.spacy を実行する。モデルがoutput/model-last に出力される。

モデルの使用

作成したモデルは以下のように使用することができます。

このコードは使用するモデルを除き、spacyのインストールの動作確認で用いたものと同じです。

import spacy

# nlp = spacy.load("ja_core_news_sm")
nlp = spacy.load("./output/model-last")

txt = "マシンラーニング・ソリューションズのオフィスは東京都にある。"

doc = nlp(txt)
for ent in doc.ents:
    print(ent.text, ent.label_)

以下のような出力が得られます。

マシンラーニング・ソリューションズ ORG
東京都 GPE

モデルとしてja_core_news_sm を使用した際は“マシンラーニング・ソリューションズ”は検出されませんでしたが、fine-tuningしたことによりORGとして検出されていることがわかります。

おわりに

本記事ではLabel Studioによってデータアノテーションを行う方法、および作成したデータでspacyモデルのfine-tuningを行う方法についてまとめました。

本文中でも少しだけ言及しましたが、Label StudioにはNER以外のテンプレートが多数あり、幅広いタスクに対応しています。また、便利な機能もたくさんありますので、アノテーションを行う際にはぜひLabel Studioを試してみてください。

参考文献

Discussion