nikkie-ftnextの日記

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

transformersのToken Classificationチュートリアルに沿ってconll2003でdistilbert-base-uncasedをファインチューニング

はじめに

ファインチューニング、ばーっといってみよー! nikkieです

えぬえるぴーやな素振り記事です。
チュートリアルに沿ってtransformersを使ってコーディングしました

目次

transformersのToken Classificationチュートリアル

こちらです。

Colabで開くリンクはこちら1
https://colab.research.google.com/github/huggingface/notebooks/blob/main/examples/token_classification.ipynb

固有表現認識(NER)のタスクのデータセットであるconll2003を使って

distilbert-base-uncasedをfine tuneします。

このノートブックはNER以外のトークン分類タスクにも適用できる実装となっていました(冒頭のtask変数で指定)。

動作環境

ローカル環境で写経した後、Colabで再実行しています。

ローカル環境は、Python 3.10.9。
仮想環境にpip install datasets 'transformers[torch]' seqeval evaluateして以下が入りました

  • datasets==2.14.5
  • evaluate==0.4.0
  • seqeval==1.2.2
  • tokenizers==0.13.3
  • torch==2.0.1
  • transformers==4.33.1

GPUが使いたかったのでColabで動かしました(Python 3.10.12)。
出力を抑えたのでライブラリのバージョンは確認できませんが、同様のバージョンと思います。

動かしたノートブックはこちら!

チュートリアルでの学び

チュートリアルは3部構成です

  • datasetsを使ったconll2003の取り扱い
  • トークナイザを使った前処理
  • Trainerをインスタンス化して訓練(fine tune)

このチュートリアルに取り組むまで知らなかった点をアウトプットします

単語で分割済みのデータセットトークナイズ

>>> from datasets import load_dataset
>>> datasets = load_dataset("conll2003")
>>> example = datasets["train"][4]

conll2003データセット単語に分割済みです。

>>> from pprint import pprint
>>> pprint(example)
{'chunk_tags': [11,
                11,
                12,
                13,
                11,
                12,
                12,
                11,
                12,
                12,
                12,
                12,
                21,
                13,
                11,
                12,
                21,
                22,
                11,
                13,
                11,
                1,
                13,
                11,
                17,
                11,
                12,
                12,
                21,
                1,
                0],
 'id': '4',
 'ner_tags': [5,
              0,
              0,
              0,
              0,
              3,
              4,
              0,
              0,
              0,
              1,
              2,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              5,
              0,
              0,
              0,
              0,
              0,
              0,
              0],
 'pos_tags': [22,
              27,
              21,
              35,
              12,
              22,
              22,
              27,
              16,
              21,
              22,
              22,
              38,
              15,
              22,
              24,
              20,
              37,
              21,
              15,
              24,
              16,
              15,
              22,
              15,
              12,
              16,
              21,
              38,
              17,
              7],
 'tokens': ['Germany',
            "'s",
            'representative',
            'to',
            'the',
            'European',
            'Union',
            "'s",
            'veterinary',
            'committee',
            'Werner',
            'Zwingmann',
            'said',
            'on',
            'Wednesday',
            'consumers',
            'should',
            'buy',
            'sheepmeat',
            'from',
            'countries',
            'other',
            'than',
            'Britain',
            'until',
            'the',
            'scientific',
            'advice',
            'was',
            'clearer',
            '.']}

トークナイザの__call__メソッドでは、is_split_into_wordsを使って、単語に分割されていてもトークナイズできることを知りました

>>> model_checkpoint = "distilbert-base-uncased"
>>> tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
>>> tokenizer("Hello, this is example") == tokenizer(["Hello", ",", "this", "is", "example"], is_split_into_words=True)
True

単語単位の正解ラベルとのアラインメント

トークナイザは単語(word)より小さい単位のsubwordでトークナイズします。
そのため、データセットの単語数よりも多くのトークンに分かれます。

ここでデータセット単語1つ1つにラベル(今回はner_tags)が付与されています。
そのため、subwordに分割すると、ラベルの数よりもトークンの数が多くなります。

>>> tokenized_input = tokenizer(example["tokens"], is_split_into_words=True)
>>> len(example["tokens"]), len(example["ner_tags"]), len(tokenized_input["input_ids"])
(31, 31, 39)
>>> # tokenized_input["input_ids"]はsubword単位で分割され、元のトークン数=ラベル数より増えている

そこで、元が同じ単語のsubwordは同じラベルとするようにアラインメントをとります2
その実装にトークナイザの出力のword_idsメソッドが使えることを知りました!

>>> tokenized_input.word_ids()  # 1,1や18,18,18が元は同じ単語
[None, 0, 1, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9, 10, 11, 11, 11, 12, 13, 14, 15, 16, 17, 18, 18, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, None]
>>> aligned_labels = [-100 if i is None else example["ner_tags"][i] for i in tokenized_input.word_ids()]
>>> len(aligned_labels)
39

Trainerインスタンスの初期化

fine tuneにはtransformersのTrainerインスタンスを初期化して使います。
初期化するのに必要なのは3点

  • TrainingArguments
    • バッチサイズやエポック数など
  • data collator
    • tokenizerを渡してつくる
  • 評価指標の算出に使う関数
    • evaluateのインターフェースでseqevalを使った
trainer = Trainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
    compute_metrics=compute_metrics,
)

終わりに

transformersのToken Classificationチュートリアルを写経しました。
conll2003でfine tuneして、trainの3エポック目、evaluate、validationデータのpredictで評価指標の値が一致しているので、チュートリアルのとおりに動かせたと思います🙌

transformersはNLP関連の書籍で触っていましたが、このチュートリアルはかなりかゆいところに手が届く内容でした。
is_split_into_words引数やword_idsメソッドを知られた3ので、conll2003以外のデータセットでも手を動かしてみたいと思っています!


  1. https://github.com/huggingface/transformers/blob/v4.33.0/examples/pytorch/README.md より
  2. チュートリアルでは別のアラインメント戦略も解説されました。チュートリアルのノートブックはフラグ変数で2つのアラインメント戦略を切り替えられます
  3. 知らなかったばっかりに、これまで車輪の再発明を強いられてきた可能性があります