[翻訳] TransformersとTokenizersを用いてスクラッチで新規に言語モデルをトレーニングする方法

[How to train a new language model from scratch using Transformers and Tokenizers](https://huggingface.co/blog/how-to-train)の翻訳です。

:::note warn
本書は抄訳であり内容の正確性を保証するものではありません。正確な内容に関しては原文を参照ください。
:::

過去数ヶ月を通じて我々は、**スクラッチで新規言語モデルのトレーニング**をこれまで以上に簡単にする目的のもと、[transformers](https://github.com/huggingface/transformers)と[tokenizers](https://github.com/huggingface/tokenizers)ライブラリに対して幾つかの改善を加えました。

この記事では、**Esperanto**に対してどのようにDistilBERTと同じ数のlayerとheadを持っている"小規模な"モデル(84 M parameters = 6 layers, 768 hidden size, 12 attention heads)をトレーニングするのかをデモします。そして、POSタギングの後段タスクにおいてモデルをファインチューニングします。

エスペラントは、学習を容易にするというゴールを持っている*人工言語*です。いくつかの理由からこのデモではこの言語を選択しています:

- 比較的リソースが少ない言語(それでも200万人の話者がいます)であり、このデモはもう一つの英語モデルをトレーニングよりもつまならないものではありません😁
- 文法は高度に規則正しい(すべての共通的な名詞は-oで終わり、すべての形容詞は-aで終わります)ので、小規模データセットにおいても興味深い言語的な結果を得ることができます。
- 最後に、言語の基盤における非常に重要なゴールは、人々を結びつけることであり、これはNLPコミュニティのゴールともアラインしていると言えるでしょう💚

> 「注意 この記事を理解するためにエスペラントを理解する必要はありませんが、学びたいのであれば280kのアクティブな学習者がいる[Duolingo](https://www.duolingo.com/enroll/eo/en/Learn-Esperanto)が良い場所となるでしょう。」

我々のモデルはこう呼ばれるでしょう...お待ちください...**EsperBERTo**😂
![](https://huggingface.co/blog/assets/01_how-to-train/eo.svg)

# 1. データセットの特定

最初に、エスペラントのテキストコーパスを見つけましょう。ここでは、INRIAの[OSCAR corpus](https://traces1.inria.fr/oscar/)のエスペラントのポーションを使用します。OSCARは言語分類とWebの[Common Crawl](https://commoncrawl.org/)によるダンプのフィルタリングによって得られた膨大な多言語コーパスです。
![](https://huggingface.co/blog/assets/01_how-to-train/oscar.png)

データセットのエスペラントのポーションはたった299Mなので、ニュース、文献、Wikipediaのような様々なソースからのテキストから構成される[Leipzig Corpora Collection](https://wortschatz.uni-leipzig.de/en/download)のエスペラントのサブコーパスと結合します。

最終的なトレーニングコーパスのサイズは3GBで以前として小さいものですが、あなたのモデルにおいては、事前トレーニングするデータを増やせばより良い結果を得られることでしょう。

# 2. トークナイザーのトレーニング

ここでは、(GPT-2と同じ)バイトレベルのバイトペアエンコーディングトークナイザーとRoBERTaと同じ特殊トークンを使用します。サイズが52,000になるように任意にピックアップしましょう。

バイトレベルのBPEは単一バイトのアルファベットからボキャブラリを構築し始め、すべての単語がトークンに分解されるので、バイトレベルのBPEをトレーニングすることをお勧めします(もはや`<unk>`トークンは不要です！)。

```py:Python
#! pip install tokenizers

from pathlib import Path

from tokenizers import ByteLevelBPETokenizer

paths = [str(x) for x in Path("./eo_data/").glob("**/*.txt")]

# Initialize a tokenizer
tokenizer = ByteLevelBPETokenizer()

# Customize training
tokenizer.train(files=paths, vocab_size=52_000, min_frequency=2, special_tokens=[
    "
",
    "<pad>",
    "
",
    "<unk>",
    "<mask>",
])

# Save files to disk
tokenizer.save_model(".", "esperberto")
```
![](https://huggingface.co/blog/assets/01_how-to-train/tokenizers-fast.gif)
*我々のデータセットではトレーニングは~5分程度です。*

🔥🔥 ワオ、なんて速い！ ⚡️🔥

これで、頻度でランクされた高頻度トークンのリストである`vocab.json`とマージのリストである`merges.txt`を手に入れます。

```json
{
    "
": 0,
    "<pad>": 1,
    "
": 2,
    "<unk>": 3,
    "<mask>": 4,
    "!": 5,
    "\"": 6,
    "#": 7,
    "$": 8,
    "%": 9,
    "&": 10,
    "'": 11,
    "(": 12,
    ")": 13,
    # ...
}
```

```text
# merges.txt
l a
Ġ k
o n
Ġ la
t a
Ġ e
Ġ d
Ġ p
# ...
```

素晴らしいのは、我々のトークナイザーがエスペラントに最適化されていることです。英語からトレーニングされた一般的なトークナイザーと比べて、多くのネイティブの単語は単一の分割されないトークンで表現されています。発音区別符号、すなわちエスペラントで使用されるアクセントの文字ĉ, ĝ, ĥ, ĵ, ŝ, ŭはネイティブにエンコードされています。また、シーケンスをより効率的な方法で表現しています。このコーパスでは、エンコードされたシーケンスの平均長は事前トレーニングされたGPT-2トークナイザーと比較して~30%小さくなっています。

ここでは、`tokenizers`での活用方法、RoBERTaの特殊トークンの取り扱い方法を説明しますが、もちろん、これらを直接`transformers`から活用することができます。

```py:Python
from tokenizers.implementations import ByteLevelBPETokenizer
from tokenizers.processors import BertProcessing


tokenizer = ByteLevelBPETokenizer(
    "./models/EsperBERTo-small/vocab.json",
    "./models/EsperBERTo-small/merges.txt",
)
tokenizer._tokenizer.post_processor = BertProcessing(
    ("
", tokenizer.token_to_id("
")),
    ("
", tokenizer.token_to_id("
")),
)
tokenizer.enable_truncation(max_length=512)

print(
    tokenizer.encode("Mi estas Julien.")
)
# Encoding(num_tokens=7, ...)
# tokens: ['
', 'Mi', 'Ġestas', 'ĠJuli', 'en', '.', '
']
```

# 3. スクラッチで言語モデルをトレーニング

**アップデート:** 関連Colabノートブックでは、スクリプトを通じてではなく、新たな[Trainer](https://github.com/huggingface/transformers/blob/master/src/transformers/trainer.py)を直接使用しています。ベストと考えるアプローチを採用してください。

それでは、`transformers`の[run\_language\_modeling\.py](https://github.com/huggingface/transformers/blob/main/examples/legacy/run_language_modeling.py)スクリプト(よりシームレスにスクラッチからのトレーニングをサポートしているので`run_lm_finetuning.py`から名称変更されました)を用いて言語モデルをトレーニングしましょう。既存のモデルやチェックポイントからではなく、スクラッチからトレーニングするために`-model_name_or_path`を`None`のままにしておくことを覚えておいてください。

> 「BERTライクのモデルにいくつかの変更を加えたRoBERTaライクのモデルをトレーニングします(詳細は[ドキュメント](https://huggingface.co/transformers/model_doc/roberta.html)をご覧ください)。」

モデルはBERTライクなので、データセットをランダムにマスクした任意のトークンをどのように埋めるのかを予測するタスクである*Masked language modeling*のタスクでトレーニングを行います。これは、サンプルスクリプトによってケアされます。

以下の2つのことを行う必要があります:

- 我々のテキストファイルからデータをロードする`Dataset`のシンプルなサブクラスの実装
    - ご自身のユースケースに応じて、提供されているサンプルのいずれか(`TextDataset`や`LineByLineTextDataset`)が動作するのであれば、自分でDatasetのサブクラスを記述する必要すらないかもしれません。しかし、ご自身のコーパスの特性に応じて、様々なカスタム調整を追加したいと考えることでしょう。
- 様々なハイパーパラメーターのセットで実験を行って選択

こちらが、EsperantoDatasetのシンプルなバージョンです。

```py:Python
from torch.utils.data import Dataset

class EsperantoDataset(Dataset):
    def __init__(self, evaluate: bool = False):
        tokenizer = ByteLevelBPETokenizer(
            "./models/EsperBERTo-small/vocab.json",
            "./models/EsperBERTo-small/merges.txt",
        )
        tokenizer._tokenizer.post_processor = BertProcessing(
            ("
", tokenizer.token_to_id("
")),
            ("
", tokenizer.token_to_id("
")),
        )
        tokenizer.enable_truncation(max_length=512)
        # or use the RobertaTokenizer from `transformers` directly.

        self.examples = []

        src_files = Path("./data/").glob("*-eval.txt") if evaluate else Path("./data/").glob("*-train.txt")
        for src_file in src_files:
            print("🔥", src_file)
            lines = src_file.read_text(encoding="utf-8").splitlines()
            self.examples += [x.ids for x in tokenizer.encode_batch(lines)]

    def __len__(self):
        return len(self.examples)

    def __getitem__(self, i):
        # We’ll pad at the batch level.
        return torch.tensor(self.examples[i])
```

データセットが大きい場合には、前処理ステップとしてではなく、温座フライでサンプルをロードしてトークナイズすることにオプトインすることができます。

こちらが、スクリプトに引き渡す**ハイパーパラメーターと引数**の固有のセットです:

```
    --output_dir ./models/EsperBERTo-small-v1
    --model_type roberta
    --mlm
    --config_name ./models/EsperBERTo-small
    --tokenizer_name ./models/EsperBERTo-small
    --do_train
    --do_eval
    --learning_rate 1e-4
    --num_train_epochs 5
    --save_total_limit 2
    --save_steps 2000
    --per_gpu_train_batch_size 16
    --evaluate_during_training
    --seed 42
```

いつものように、ご自身のGPU(s)にフィットする最大のバッチサイズを選択します。

🔥🔥🔥 **トレーニングをスタートしましょう！！** 🔥🔥🔥

ここでは[特定のハイパーパラメータのセット](https://tensorboard.dev/experiment/8AjtzdgPR1qG6bDIe1eKfw/#scalars)をTensorboardでチェックすることができます。
![](https://huggingface.co/blog/assets/01_how-to-train/tensorboard.png)

> 「我々のサンプルスクリプトは、デフォルトで*runs/*配下にTensorboardフォーマットで記録します。ご自身のボードを参照するために`tensorboard dev upload --logdir runs`を実行すると、ご自身のMLエクスペリメントを誰でも共有できるGoodgeマネージドのホスティングバージョンである[tensorboard\.dev](https://tensorboard.dev/)をセットアップします」

# 4. 実際にLMがトレーニングされたことをチェック

トレーニングを参照し、評価ロスが削減していく様子を見ることに加え、我々の言語モデルが興味深い何かを学習しているのかどうかをチェックする最も簡単な方法は`FillMaskPipeline`を通じたものです。

パイプラインはトークナイザーとモデルに対するシンプルなラッパーであり、`fill-mask`することで、マスクされたトークン(ここでは`<mask>`)を含むシーケンスを入力することができ、確率とともに最も可能性の高いものが埋められたリストが返却されます。

```py:Python
from transformers import pipeline

fill_mask = pipeline(
    "fill-mask",
    model="./models/EsperBERTo-small",
    tokenizer="./models/EsperBERTo-small"
)

# The sun <mask>.
# =>

result = fill_mask("La suno <mask>.")

# {'score': 0.2526160776615143, 'sequence': '
 La suno brilis.
', 'token': 10820}
# {'score': 0.0999930202960968, 'sequence': '
 La suno lumis.
', 'token': 23833}
# {'score': 0.04382849484682083, 'sequence': '
 La suno brilas.
', 'token': 15006}
# {'score': 0.026011141017079353, 'sequence': '
 La suno falas.
', 'token': 7392}
# {'score': 0.016859788447618484, 'sequence': '
 La suno pasis.
', 'token': 4552}
```

OK、シンプルな構文/文法が動作しています。より面白いプロンプトにトライしてみましょう:

```py:Python
fill_mask("Jen la komenco de bela <mask>.")

# This is the beginning of a beautiful <mask>.
# =>

# {
#     'score':0.06502299010753632
#     'sequence':'
 Jen la komenco de bela vivo.
'
#     'token':1099
# }
# {
#     'score':0.0421181358397007
#     'sequence':'
 Jen la komenco de bela vespero.
'
#     'token':5100
# }
# {
#     'score':0.024884626269340515
#     'sequence':'
 Jen la komenco de bela laboro.
'
#     'token':1570
# }
# {
#     'score':0.02324388362467289
#     'sequence':'
 Jen la komenco de bela tago.
'
#     'token':1688
# }
# {
#     'score':0.020378097891807556
#     'sequence':'
 Jen la komenco de bela festo.
'
#     'token':4580
# }
```

> **“Jen la komenco de bela tago”,** 本当です！

より複雑なプロンプトによって、言語モデルがより意味のある知識をキャプチャしていることや、ある種の(統計的な)コモンセンスによる理由づけを行っているかどうかをプローブすることができます。

# 5. 後段のタスクでLMをファインチューニング

これで、後段タスクである**品詞のタグづけ**において我々の新たな言語モデルをファインチューニングすることができます。

上述したように、エスペラントは単語の末尾が通常文法の品詞の条件になる高度に規則正しい言語です。CoNLL-2003フォーマットのアノテーションされたエスペラントのPOSタグのデータセット(以下のサンプルをご覧ください)を用いて、`transformers`の[run\_ner\.py](https://github.com/huggingface/transformers/blob/main/examples/pytorch/token-classification/run_ner.py)スクリプトを活用することができます。

> 「POSのタグづけはNERと同じようにトークンの分類タスクなので、全く同じスクリプトを活用することができます。」

![](https://huggingface.co/blog/assets/01_how-to-train/conll-2003.png)

先ほどと同じように、こちらがこのファインチューニングの[Tensorboard](https://tensorboard.dev/experiment/lOZn2wOWQo6ixpwtWyyDfQ/#scalars)です。GPUごとにバッチサイズ54を用いて3エポックトレーニングしています。

このタスクは比較的簡単なので、トレーニングと評価ロスは少量の残差に収束していますが、エンドツーエンドでトレーニングできるので楽しいものとなっています😃。

今回は、`TokenClassificationPipeline`を使いましょう:

```py:Python
from transformers import TokenClassificationPipeline, pipeline


MODEL_PATH = "./models/EsperBERTo-small-pos/"

nlp = pipeline(
    "ner",
    model=MODEL_PATH,
    tokenizer=MODEL_PATH,
)
# or instantiate a TokenClassificationPipeline directly.

nlp("Mi estas viro kej estas tago varma.")

# {'entity': 'PRON', 'score': 0.9979867339134216, 'word': ' Mi'}
# {'entity': 'VERB', 'score': 0.9683094620704651, 'word': ' estas'}
# {'entity': 'VERB', 'score': 0.9797462821006775, 'word': ' estas'}
# {'entity': 'NOUN', 'score': 0.8509314060211182, 'word': ' tago'}
# {'entity': 'ADJ', 'score': 0.9996201395988464, 'word': ' varma'}
```

**動いているようです！ 🔥**

NERのよりチャレンジングなデータセットについては、WikiANNのシルバースタンダードデータセットでトレーニングすることができる[@stefan\-it](https://github.com/stefan-it)がお勧めです。

# 6. モデルを共有 🎉

最後に、素晴らしいモデルを手に入れたらコミュニティに共有することを検討してください:

- CLI: `transformers-cli upload`を用いてモデルをアップロード
- README.mdモデルカードを記述し、リポジトリの`model_cards/`配下に追加します。理想的にはモデルカードに以下を含めるべきです:
    - モデルの説明
    - トレーニングパラメーター(データセット、前処理、ハイパーパラメータ)
    - 評価結果
    - 意図される用途と制限
    - 他のなんでも助かります！🤓

**以上です！**

➡️あなたのモデルには https://huggingface.co/models 上のページがあり、`AutoModel.from_pretrained("username/model_name")`を用いて誰でもロードすることができます。
![](https://huggingface.co/blog/assets/01_how-to-train/model_page.png)

別の言語のモデルを見てみたいのであれば、https://huggingface.co/models をチェックしてください。
![](https://huggingface.co/front/thumbnails/models.png)

**ありがとうございます！**
![](https://huggingface.co/blog/assets/01_how-to-train/EsperBERTo-thumbnail-v2.png)