fairseqのwmt19翻訳システムをtransformersに移植する

Move fairseq's wmt19 translation system to transformers.

Stas Bekmanさんによるゲストブログ記事

この記事は、fairseq wmt19翻訳システムがtransformersに移植された方法をドキュメント化する試みです。

私は興味深いプロジェクトを探していて、Sam Shleiferさんが高品質の翻訳者の移植に取り組んでみることを提案してくれました。

私はFacebook FAIRのWMT19ニュース翻訳タスクの提出に関する短い論文を読み、オリジナルのシステムを試してみることにしました。

最初はこの複雑なプロジェクトにどう取り組むか分からず、Samさんがそれを小さなタスクに分解するのを手伝ってくれました。これが非常に助けになりました。

私は、両方の言語を話すため、移植中に事前学習済みのen-ru / ru-enモデルを使用することを選びました。ドイツ語は話せないので、de-en / en-deのペアで作業するのははるかに難しくなります。移植プロセスの高度な段階で出力を読んで意味を理解することで翻訳の品質を評価できることは、多くの時間を節約することができました。

また、最初の移植をen-ru / ru-enモデルで行ったため、de-en / en-deモデルが統合されたボキャブラリを使用していることに全く気づいていませんでした。したがって、2つの異なるサイズのボキャブラリをサポートするより複雑な作業を行った後、統合されたボキャブラリを動作させるのは簡単でした。

手抜きしましょう

最初のステップは、もちろん手抜きです。大きな努力をするよりも小さな努力をする方が良いです。したがって、fairseqへのプロキシとして機能し、transformersのAPIをエミュレートする数行のコードで短いノートブックを作成しました。

もし基本的な翻訳以外のことが必要なければ、これで十分でした。しかし、もちろん、完全な移植を行いたかったので、この小さな勝利の後、より困難な作業に移りました。

準備

この記事では、~/portingの下で作業していると仮定し、したがってこのディレクトリを作成します:

mkdir ~/porting
cd ~/porting

この作業にはいくつかのツールのインストールが必要です:

# fairseqのインストール
git clone https://github.com/pytorch/fairseq
cd fairseq
pip install -e .
# fairseqの下にmosesdecoderのインストール
git clone https://github.com/moses-smt/mosesdecoder
# fairseqの下にfastBPEのインストール
git clone [email protected]:glample/fastBPE.git
cd fastBPE; g++ -std=c++11 -pthread -O3 fastBPE/main.cc -IfastBPE -o fast; cd -
cd -

# transformersのインストール
git clone https://github.com/huggingface/transformers/
pip install -e .[dev]

ファイル

概要として、以下のファイルを作成して書き込む必要があります:

  • src/transformers/configuration_fsmt.py – 短い設定クラス。
  • src/transformers/convert_fsmt_original_pytorch_checkpoint_to_pytorch.py – 複雑な変換スクリプト。
  • src/transformers/modeling_fsmt.py – モデルのアーキテクチャが実装されている場所です。
  • src/transformers/tokenization_fsmt.py – トークナイザのコード。
  • tests/test_modeling_fsmt.py – モデルのテスト。
  • tests/test_tokenization_fsmt.py – トークナイザのテスト。
  • docs/source/model_doc/fsmt.rst – ドキュメントファイル。

他にも変更する必要のあるファイルがありますが、最後にそれについて話します。

変換

移植プロセスの中でも最も重要な部分の1つは、元のモデルの開発者が提供する利用可能なソースデータ(事前学習済みの重みを持つチェックポイント、モデルとトレーニングの設定、辞書とトークナイザのサポートファイルなど)を受け取り、transformersでサポートされる新しいモデルファイルに変換するスクリプトを作成することです。最終的な変換スクリプトはこちらにあります: src/transformers/convert_fsmt_original_pytorch_checkpoint_to_pytorch.py

このプロセスは、既存の変換スクリプトの1つをコピーすることから始めました src/transformers/convert_bart_original_pytorch_checkpoint_to_pytorch.py 、それを大部分取り除いた後、ポーティングプロセスの進行に合わせて徐々に部分を追加しました。

開発中は、すべてのコードを変換されたモデルファイルのローカルコピーに対してテストしていました。すべてが準備できた最後の段階で、ファイルを 🤗 s3 にアップロードし、その後オンラインバージョンに対してテストを継続しました。

fairseqモデルとそのサポートファイル

まず、fairseqの事前学習済みモデルで得られるデータを見てみましょう。

これには、非常に簡単にハブに提出されたモデルを展開することができる便利なtorch.hub APIを使用します:

import torch
torch.hub.load('pytorch/fairseq', 'transformer.wmt19.en-ru', checkpoint_file='model4.pt', 
               tokenizer='moses', bpe='fastbpe')

このコードは、事前学習済みモデルとそのサポートファイルをダウンロードします。この情報は、pytorchハブ上のfairseqに対応するページで見つけました。

ダウンロードしたファイルの中身を確認するには、まず~/.cacheの正しいフォルダを見つける必要があります。

ls -1 ~/.cache/torch/hub/pytorch_fairseq/

上記のコマンドは次のように表示されます:

15bca559d0277eb5c17149cc7e808459c6e307e5dfbb296d0cf1cfe89bb665d7.ded47c1b3054e7b2d78c0b86297f36a170b7d2e7980d8c29003634eb58d973d9
15bca559d0277eb5c17149cc7e808459c6e307e5dfbb296d0cf1cfe89bb665d7.ded47c1b3054e7b2d78c0b86297f36a170b7d2e7980d8c29003634eb58d973d9.json

他のモデルを使用していた場合、複数のエントリが表示されることがあります。

将来的にその難解なキャッシュフォルダの名前を参照しやすくするため、シンボリックリンクを作成しましょう:

ln -s /code/data/cache/torch/hub/pytorch_fairseq/15bca559d0277eb5c17149cc7e808459c6e307e5dfbb296d0cf1cfe89bb665d7.ded47c1b3054e7b2d78c0b86297f36a170b7d2e7980d8c29003634eb58d973d9 \
~/porting/pytorch_fairseq_model

注意:自分で試すときには、パスは異なる場合があります。モデルのハッシュ値によって変更される可能性があるため、正しいパスは~/.cache/torch/hub/pytorch_fairseq/にあります。

フォルダの中を見てみましょう:

ls -l ~/porting/pytorch_fairseq_model/
total 13646584
-rw-rw-r-- 1 stas stas     532048 Sep  8 21:29 bpecodes
-rw-rw-r-- 1 stas stas     351706 Sep  8 21:29 dict.en.txt
-rw-rw-r-- 1 stas stas     515506 Sep  8 21:29 dict.ru.txt
-rw-rw-r-- 1 stas stas 3493170533 Sep  8 21:28 model1.pt
-rw-rw-r-- 1 stas stas 3493170532 Sep  8 21:28 model2.pt
-rw-rw-r-- 1 stas stas 3493170374 Sep  8 21:28 model3.pt
-rw-rw-r-- 1 stas stas 3493170386 Sep  8 21:29 model4.pt

上記のコマンドは次のように表示されます:

  1. model*.pt – 4つのチェックポイント(pytorchのstate_dictで学習済みの重みとその他の情報を含む)
  2. dict.*.txt – ソースとターゲットの辞書
  3. bpecodes – トークナイザーによって使用される特別なマップファイル

以下のセクションでこれらのファイルを調査します。

翻訳システムの動作方法

以下は、コンピュータがテキストを翻訳する方法の非常に簡単な紹介です。

コンピュータはテキストを読むことはできませんが、数字のみを扱うことができます。したがって、テキストを処理する場合、1つ以上の文字を数字に対応づけ、それらをコンピュータプログラムに渡す必要があります。プログラムが完了すると、数字が返されますが、これをテキストに変換する必要があります。

まず、ロシア語と英語の2つの文を取り上げ、各単語に一意の番号を割り当てましょう:

я  люблю следовательно я  существую
10 11    12            10 13

I  love therefore I  am
20 21   22        20 23

10から始まる数字はロシア語の単語を一意の番号にマッピングします。20から始まる数字は英語の単語に対して同じことを行います。ロシア語がわからなくても、単語я(「I」の意味)が文中で2回繰り返され、同じ番号10がそれに関連付けられていることが分かります。同様に、I(20)も2回繰り返されます。

翻訳システムは以下の段階で動作します:

1. [я люблю следовательно я существую] # 文を単語に分割する
2. [10 11 12 10 13]                    # 入力辞書で単語を検索し、IDに変換する
3. [ブラックボックス]                     # 機械学習システムのマジック
4. [20 21 22 20 23]                    # 出力辞書で数字を検索し、テキストに変換する
5. [I love therefore I am]             # トークンを文に戻して復元する

最初の2つと最後の2つのステップを組み合わせると、3つの段階が得られます:

  1. 入力のエンコード:入力テキストをトークンに分割し、これらのトークンの辞書(vocab)を作成し、その辞書内で各トークンを一意のIDに再マップします。
  2. 翻訳の生成:入力の数字を取り、事前にトレーニングされた機械学習モデルを実行して最適な翻訳を予測し、出力の数字を返します。
  3. 出力のデコード:出力の数字を取り、対象言語の辞書で検索し、テキストに変換し、最後に変換されたトークンを翻訳された文に結合します。

2番目の段階では、1つまたは複数の可能な翻訳が返される場合があります。後者の場合、呼び出し元は最適な結果を選択することができます。この記事では、複数の可能な結果を検索する方法の一つであるビームサーチアルゴリズムに言及します。また、ビームのサイズは返される結果の数を指します。

1つの結果のみが要求される場合、モデルは最も高い尤度確率で選択します。複数の結果が要求される場合、確率に基づいてソートされた結果が返されます。

この考え方は、翻訳に限らず、ほとんどのNLPタスクにも適用されることに注意してください。

トークン化

初期のシステムでは、文を単語と句読点にトークン化していました。しかし、多くの言語には数十万の単語があるため、巨大な語彙で作業することは非常に負荷がかかり、計算リソースの要件とタスクの完了時間が大幅に増加します。

2020年現在、いくつかの異なるトークン化メソッドがありますが、最近のもののほとんどはサブワードトークン化に基づいています。つまり、入力テキストを単語のセグメントと文字に分割するのではなく、これらの最新のトークナイザーは、最適なトークン化を取得するために何らかのトレーニングを使用して、入力テキストを単語のセグメントと文字に分割します。

このアプローチがメモリと計算要件を削減するのにどのように役立つか見てみましょう。例えば、go、going、speak、speaking、sleep、sleepingという6つの一般的な単語の入力語彙がある場合、単語レベルのトークン化では6つのトークンになります。しかし、go、go-ing、speak、speak-ingなどに分割すると、go、speak、sleep、ingの4つのトークンしかありません。この単純な変更により、33%の改善が実現されました!ただし、サブワードトークナイザーは文法ルールを使用しませんが、これらは大量のテキスト入力でトレーニングされ、そのような分割を見つけます。この例では、理解しやすいように単純な文法ルールを使用しました。

このアプローチのもう一つの重要な利点は、語彙に含まれていない入力テキストの単語を扱う場合です。たとえば、システムが語彙にない単語grokking (*)に遭遇したとしましょう。この単語を`grokk’-‘ing’に分割すると、機械学習モデルは単語の最初の部分に対して何をすべきかわからないかもしれませんが、’ing’は継続的な時制を示しているという有用な洞察を得ることができるため、より良い翻訳を生成することができます。このような状況では、トークナイザーは未知のセグメントを既知のセグメントに分割し、最悪の場合はそれらを個々の文字にまで減らします。

  • 注: 「grok」という言葉は1961年にロバート・A・ハインラインの「異邦人」で造語されました。直感的に理解する、または共感によって理解するという意味です。

現代のトークナイズ手法が単純な単語トークナイズよりも優れている理由には、他にも多くの微妙なニュアンスがありますが、この記事の範囲では扱いません。これらのシステムのほとんどは、単に「ing」のような接尾辞を分割するという単純な例と比較して、トークナイズの方法が非常に複雑ですが、原則は似ています。

トークナイザーの移植

最初のステップは、テキストがIDに変換されるトークナイザーのエンコーダー部分を移植することでした。デコーダー部分は最後まで必要ありません。

fairseqのトークナイザーの仕組み

まず、fairseqのトークナイザーがどのように機能するかを理解しましょう。

fairseq (*) は、トークナイズにByte Pair Encoding (BPE)アルゴリズムを使用しています。

  • 注: ここから先では、fairseqという言葉を使用する際には、この特定のモデル実装を指します – fairseqプロジェクト自体には多数の異なるモデルの実装があります。

BPEが行うことを見てみましょう:

import torch
sentence = "Machine Learning is great"
checkpoint_file='model4.pt'
model = torch.hub.load('pytorch/fairseq', 'transformer.wmt19.en-ru', checkpoint_file=checkpoint_file, tokenizer='moses', bpe='fastbpe')

# ステップごとにエンコードする
tokens = model.tokenize(sentence)
print("tokenize ", tokens)

bpe = model.apply_bpe(tokens)
print("apply_bpe: ", bpe)

bin = model.binarize(bpe)
print("binarize: ", len(bin), bin)

# model.encodeと比較 - 同じ出力を得るはず
expected = model.encode(sentence)
print("encode:   ", len(expected), expected)

出力は次のようになります:

('tokenize ', 'Machine Learning is great')
('apply_bpe: ', 'Mach@@ ine Lear@@ ning is great')
('binarize: ', 7, tensor([10217,  1419,     3,  2515,    21,  1054,     2]))
('encode:   ', 7, tensor([10217,  1419,     3,  2515,    21,  1054,     2]))

model.encodetokenize+apply_bpe+binarizeを行っていることが分かります – 同じ出力を得ることができます。

ステップは次のようになります:

  1. tokenize : 通常、アポストロフィをエスケープしたり他の前処理を行ったりしますが、この例では何も変更せずに入力文を返します
  2. apply_bpe : BPEは、トークナイザーによって提供されるbpecodesファイルに基づいて、入力を単語とサブワードに分割します – 6つのBPEチャンクを取得します
  3. binarize : これは、前のステップでのBPEチャンクを、語彙の対応するIDに単純に変換します(モデルと一緒にダウンロードされます)

詳細については、このノートブックを参照してください。

このタイミングでbpecodesファイルの中身を見てみるのも良いでしょう。以下はファイルの先頭部分です:

$ head -15 ~/porting/pytorch_fairseq_model/bpecodes
e n</w> 1423551864
e r 1300703664
e r</w> 1142368899
i n 1130674201
c h 933581741
a n 845658658
t h 811639783
e n 780050874
u n 661783167
s t 592856434
e i 579569900
a r 494774817
a l 444331573
o r 439176406
th e</w> 432025210
[...]

このファイルの上位のエントリには、非常に頻繁な1文字のシーケンスが含まれています。後ほど見るように、下部には最も一般的な複数文字のサブワードや完全な長い単語が含まれています。

特別なトークン </w> は単語の終わりを示します。したがって、上記のいくつかの引用行で次のように見つかります:

e n</w> 1423551864
e r</w> 1142368899
th e</w> 432025210

2番目の列に </w> が含まれていない場合、このセグメントは単語の中間にあることを意味します。

最後の列は、このBPEコードがトレーニング中に出現した回数を示しています。 bpecodes ファイルはこの列でソートされているため、最も一般的なBPEコードが上位にあります。

カウントを見ることで、このトークナイザーがトレーニング時に en で終わる単語が1,423,551,864回、er で終わる単語が1,142,368,899回、the で終わる単語が432,025,210回出現したことがわかります。後者は実際の単語 the を意味する可能性が高いですが、それに加えて latheloathetithe などの単語も含まれます。

これらの膨大な数値は、このトークナイザーが膨大なテキストでトレーニングされたことを示しています!

同じファイルの末尾を見ると:

$ tail -10 ~/porting/pytorch_fairseq_model/bpecodes
4 x 109019
F ische</w> 109018
sal aries</w> 109012
e kt 108978
ver gewal 108978
Sten cils</w> 108977
Freiwilli ge</w> 108969
doub les</w> 108965
po ckets</w> 108953
Gö tz</w> 108943

まだ頻繁に出現するサブワードの複雑な組み合わせが見られます。例えば、sal aries は109,012回出現します!そのため、bpecodes マップファイルには専用のエントリがあります。

apply_bpe はどのように動作するのでしょうか? bpecodes マップファイル内のさまざまな文字の組み合わせを検索し、最も長く一致するエントリを使用します。

先ほどの例に戻ると、MachineMach@@ + ine に分割されたことがわかりました。次に確認してみましょう:

$ grep -i ^mach  ~/porting/pytorch_fairseq_model/bpecodes
mach ine</w> 463985
Mach t 376252
Mach ines</w> 374223
mach ines</w> 214050
Mach th 119438

mach ine</w> が見つかります。 Mach ine は見つかりません – つまり、通常の大文字と一致しない場合は小文字での検索を処理しているということです。

では、Lear@@ + ning を確認してみましょう:

$ grep -i ^lear  ~/porting/pytorch_fairseq_model/bpecodes
lear n</w> 675290
lear ned</w> 505087
lear ning</w> 417623

lear ning</w> が見つかります(再び大文字と一致していません)。

これについてさらに考えると、トークン化においては大文字/小文字の区別は重要ではないと思われます。ただし、各場合をカバーすることが非常に重要な辞書には Mach / Lear および mach / lear の固有のエントリがある必要があります。

これで、この仕組みがわかると思います。

混乱する点の一つは、apply_bpe の出力が次のようだったことです:

('apply_bpe: ', 6, ['Mach@@', 'ine', 'Lear@@', 'ning', 'is', 'great'])

単語の終了を</w>でマークする代わりに、それらをそのままにして、代わりに終了でない単語を@@でマークする。おそらく、fastBPEの実装がfairseqで使用されているため、このようになっている。これをtransformersの実装に合わせるために変更する必要がありましたが、fastBPEは使用していないので、変更しました。

最後に、BPEコードを語彙IDにリマッピングすることを確認しましょう。繰り返しますが、次のようになりました:

('apply_bpe: ', 'Mach@@ ine Lear@@ ning is great')
('binarize: ', 7, tensor([10217,  1419,     3,  2515,    21,  1054,     2]))

2 – 最後のトークンIDはeos(ストリームの終わり)トークンです。これは、入力の終わりをモデルに示すために使用されます。

そして、Mach@@10217に、ine1419にリマッピングされます。

辞書ファイルが一致しているか確認しましょう:

$ grep ^Mach@@ ~/porting/pytorch_fairseq_model/dict.en.txt
Mach@@ 6410
$ grep "^ine " ~/porting/pytorch_fairseq_model/dict.en.txt
ine 88376

ちょっと待ってください – それらはbinarizeの後に得られたIDではありません。それは、それぞれ102171419であるはずです。

調査するために、語彙ファイルのIDがモデルで使用されていないこと、および内部的に語彙ファイルがロードされた後にそれらを新しいIDにリマップしていることがわかりました。幸運なことに、具体的にはどのように行われているのかを理解する必要はありませんでした。代わりに、リマッピングをすべて実行するためにfairseq.data.dictionary.Dictionary.loadを使用し、最終的な辞書を保存しました。私はデバッガーでfairseqのコードをステップスルーすることで、Dictionaryクラスについて知りました。

  • 注: モデルとデータセットの移植に取り組むほど、元のコードを再現しようとする代わりに、私自身のために元のコードを動かすことが、時間を節約するためにもっとも重要であり、さらに重要なのは、そのコードはすでにテストされているということに気づいています。何かを見落とし、後で大きな問題に直面する可能性があるので、最終的には、この変換コードは重要ではありません。なぜなら、transformersとそのエンドユーザーが使用するのは、生成されたデータだけだからです。

以下は変換スクリプトの関連部分です:

from fairseq.data.dictionary import Dictionary
def rewrite_dict_keys(d):
    # (1) 単語分割のシンボルを削除
    # (2) 単語が分割されていない場所に単語終了シンボルを追加
    # 例:d = {'le@@': 5, 'tt@@': 6, 'er': 7} => {'le': 5, 'tt': 6, 'er</w>': 7}
    d2 = dict((re.sub(r"@@$", "", k), v) if k.endswith("@@") else (re.sub(r"$", "</w>", k), v) for k, v in d.items())
    keep_keys = "<s> <pad> </s> <unk>".split()
    # 特殊トークンを復元
    for k in keep_keys:
        del d2[f"{k}</w>"]
        d2[k] = d[k]  # 復元
    return d2

src_dict_file = os.path.join(fsmt_folder_path, f"dict.{src_lang}.txt")
src_dict = Dictionary.load(src_dict_file)
src_vocab = rewrite_dict_keys(src_dict.indices)
src_vocab_size = len(src_vocab)
src_vocab_file = os.path.join(pytorch_dump_folder_path, "vocab-src.json")
print(f"Generating {src_vocab_file}")
with open(src_vocab_file, "w", encoding="utf-8") as f:
    f.write(json.dumps(src_vocab, ensure_ascii=False, indent=json_indent))
# ターゲット辞書についても同様に行いました - ここでは引用して省略しました
# また、`bpecodes`も保存する必要がありました。transformersの世界では`merges.txt`と呼ばれています

変換スクリプトを実行した後、変換された辞書を確認しましょう:

$ grep '"Mach"' /code/huggingface/transformers-fair-wmt/data/wmt19-en-ru/vocab-src.json
  "Mach": 10217,
$ grep '"ine</w>":' /code/huggingface/transformers-fair-wmt/data/wmt19-en-ru/vocab-src.json
  "ine</w>": 1419,

transformersバージョンの語彙ファイルでは正しいIDがあります。

ご覧の通り、transformersのBPE実装に合わせて語彙を書き直す必要がありました。次のように変更する必要があります:

['Mach@@', 'ine', 'Lear@@', 'ning', 'is', 'great']

を次のように変更します:

['Mach', 'ine</w>', 'Lear', 'ning</w>', 'is</w>', 'great</w>']

単語の一部であるチャンクをマークする代わりに、最後のセグメントを除いて、セグメントまたは単語をマークします。異なるエンコーディングスタイル間で簡単に切り替えることができます。

これにより、モデルファイルの最初の部分の移植が成功しました。最終的なコードのバージョンはこちらでご覧いただけます。

もし詳しく見たい場合、このノートブックにさらに微調整の部分があります。

トークナイザーのエンコーダーをtransformersに移植する

transformersfastBPEに依存できません。後者はCコンパイラが必要ですが、幸いなことに既に同じ機能を持つpythonバージョンがtokenization_xlm.pyで実装されています。

そのため、単純にそれをsrc/transformers/tokenization_fsmt.pyにコピーし、クラス名を変更しました:

cp tokenization_xlm.py tokenization_fsmt.py
perl -pi -e 's|XLM|FSMT|ig; s|xlm|fsmt|g;' tokenization_fsmt.py

そして、ほとんどの変更を加えるだけで、トークナイザーのエンコーダーの機能が実装されました。必要なサポートする言語に適用されない多くのコードがありましたので、それらのコードを削除しました。

2つの異なる語彙が必要だったため、トークナイザー内で1つではなく両方をサポートするようにコードを変更する必要がありました。そのため、スーパークラスのメソッドをオーバーライドする必要がありました:

    def get_vocab(self) -> Dict[str, int]:
        return self.get_src_vocab()

    @property
    def vocab_size(self) -> int:
        return self.src_vocab_size

fairseqbos(ストリームの開始)トークンを使用しなかったため、それらを含めないようにコードを変更する必要がありました(*):

-            return bos + token_ids_0 + sep
-        return bos + token_ids_0 + sep + token_ids_1 + sep
+            return token_ids_0 + sep
+        return token_ids_0 + sep + token_ids_1 + sep
  • 注:これはdiff(1)の出力で、2つのコードの差分を示しており、-で始まる行は削除された部分、+で始まる行は追加された部分を示しています。

fairseqは文字をエスケープし、ダッシュを分割するための処理を行っていましたので、以下のように変更しました:

-        [...].tokenize(text, return_str=False, escape=False)
+        [...].tokenize(text, return_str=False, escape=True, aggressive_dash_splits=True)

もし一緒に進んでいるのであれば、元のtokenization_xlm.pyに行った全ての変更を確認したい場合は、次のようにしてください:

cp tokenization_xlm.py tokenization_orig.py
perl -pi -e 's|XLM|FSMT|g; s|xlm|fsmt|g;' tokenization_orig.py
diff -u tokenization_orig.py tokenization_fsmt.py  | less

fsmtがリリースされた時点でリポジトリをチェックしていることを確認してください。その後、2つのファイルは分岐している可能性があるので、気をつけてください。

最終段階では、いくつかの入力を実行し、ポーティングされたトークナイザが元のトークナイザと同じIDを生成することを確認しました。これはこのノートブックで実行されているのがわかります。私は出力が一致するようになるまでこれを繰り返し実行し、出力を一致させるためにコードを調整し、異なる種類の入力を試し、同じ出力を生成することを確認しました。すべての入力が一致する出力を生成するまで、この作業を繰り返しました。

ほとんどのポーティングプロセスはこのように進行しました。私は小さな機能を取り上げ、fairseqの方法で実行し、出力を取得し、transformersのコードでも同じように行い、出力が一致するようにコードを調整しました。それから異なる種類の入力を試し、同じ出力を生成することを確認し、すべての入力が一致する出力を生成するまで繰り返しました。

コアの翻訳機能のポーティング

トークナイザのポーティングが比較的素早く成功したので(もちろん、既にほとんどのコードがあったおかげです)、次のステージははるかに複雑でした。これはgenerate()関数で、入力のIDを受け取り、モデルを実行して出力のIDを返すものです。

これを複数のサブタスクに分割する必要がありました。以下のことを行う必要がありました。

  1. モデルの重みをポーティングする。
  2. generate()を単一のビーム(つまり、単一の結果を返す)で動作させる。
  3. そして、複数のビーム(つまり、複数の結果を返す)で動作させる。

まず、既存のアーキテクチャの中で私のニーズに最も近いものは何か調査しました。最も近いのはBARTでしたので、次のようにしました:

cp modeling_bart.py modeling_fsmt.py
perl -pi -e 's|Bart|FSMT|ig; s|bart|fsmt|g;' modeling_fsmt.py

これが私の出発点で、fairseqが提供するモデルの重みと一緒に動作するように調整する必要がありました。

重みと設定のポーティング

最初に、公開されているチェックポイントの中身を見てみました。このノートブックでは、そこで行ったことを示しています。

そこには4つのチェックポイントがあることを発見しました。何をすべきかわからなかったので、まずは最初のチェックポイントだけを使用するというより簡単な仕事から始めました。後で、fairseqは4つのチェックポイントをアンサンブルで使用して最良の予測結果を得ていること、そしてtransformersは現在その機能をサポートしていないことを発見しました。ポーティングが完了し、パフォーマンススコアを計測できるようになったときに最良のスコアを持つmodel4.ptチェックポイントが提供されました。しかし、ポーティング中はパフォーマンスはあまり重要ではありませんでした。1つのチェックポイントのみを使用していたため、出力を比較する際には、fairseqも同じチェックポイントを使用することが重要でした。

それを実現するために、わずかに異なるfairseqのAPIを使用しました:

from fairseq import hub_utils
#checkpoint_file = 'model1.pt:model2.pt:model3.pt:model4.pt'
checkpoint_file = 'model1.pt'
model_name_or_path = 'transformer.wmt19.ru-en'
data_name_or_path = '.'
cls = fairseq.model_parallel.models.transformer.ModelParallelTransformerModel
models = cls.hub_models()
kwargs = {'bpe': 'fastbpe', 'tokenizer': 'moses'}
ru2en = hub_utils.from_pretrained(
            model_name_or_path,
            checkpoint_file,
            data_name_or_path,
            archive_map=models,
            **kwargs
        )

まず、モデルを見てみました:

print(ru2en["models"][0])

TransformerModel(
  (encoder): TransformerEncoder(
    (dropout_module): FairseqDropout()
    (embed_tokens): Embedding(31232, 1024, padding_idx=1)
    (embed_positions): SinusoidalPositionalEmbedding()
    (layers): ModuleList(
      (0): TransformerEncoderLayer(
        (self_attn): MultiheadAttention(
          (dropout_module): FairseqDropout()
          (k_proj): Linear(in_features=1024, out_features=1024, bias=True)
          (v_proj): Linear(in_features=1024, out_features=1024, bias=True)
          (q_proj): Linear(in_features=1024, out_features=1024, bias=True)
          (out_proj): Linear(in_features=1024, out_features=1024, bias=True)
        )
      [...]
# ノートブックには全ての出力があります

それはBARTのアーキテクチャに非常に似ていたが、いくつかのレイヤーにわずかな違いがあった。いくつかのレイヤーが追加され、他のレイヤーが削除された。これは素晴らしいニュースであり、車輪を再発明する必要はなく、うまく機能するデザインを微調整するだけで済むということでした。

上記のコードサンプルでは、torch.load()を使用してstate_dictをロードしていません。最初はこれをやっていましたが、結果は非常に困惑しました。私はself_attn.(k|q|v)_projの重みが欠落しており、代わりに単一のself_attn.in_projがあることに気づきました。私はfairseq APIを使用してモデルをロードしようとしたところ、問題が修正されました。おそらくそのモデルは古く、古いアーキテクチャを使用していて、k/q/v用の一組の重みを持っており、より新しいアーキテクチャはそれらを分離しています。fairseqがこの古いモデルをロードすると、重みが現代のアーキテクチャに合うように書き換えられます。

また、このノートブックを使用してstate_dictを視覚的に比較しました。そのノートブックでは、fairseqlast_optimizer_stateという2.2GBのデータを取得することがわかりますが、これは無視しても安全であり、最終的なモデルサイズは3倍小さくなります。

変換スクリプトでは、使用しないstate_dictキーも削除する必要がありました。たとえば、model.encoder.versionmodel.modelなどです。

次に、設定の引数を確認します:

args = dict(vars(ru2en["args"]))
pprint(args)

 'activation_dropout': 0.0,
 'activation_fn': 'relu',
 'adam_betas': '(0.9, 0.98)',
 'adam_eps': 1e-08,
 'adaptive_input': False,
 'adaptive_softmax_cutoff': None,
 'adaptive_softmax_dropout': 0,
 'arch': 'transformer_wmt_en_de_big',
 'attention_dropout': 0.1,
 'bpe': 'fastbpe',
 [...全体の出力はノートブックにあります...] 

OK、それらをモデルに設定するためにそれらをコピーします。対応する設定の名前がtransformersと異なる場合には、引数の名前をリネームする必要がありました。したがって、構成の再マッピングは次のようになります:

    model_conf = {
        "architectures": ["FSMTForConditionalGeneration"],
        "model_type": "fsmt",
        "activation_dropout": args["activation_dropout"],
        "activation_function": "relu",
        "attention_dropout": args["attention_dropout"],
        "d_model": args["decoder_embed_dim"],
        "dropout": args["dropout"],
        "init_std": 0.02,
        "max_position_embeddings": args["max_source_positions"],
        "num_hidden_layers": args["encoder_layers"],
        "src_vocab_size": src_vocab_size,
        "tgt_vocab_size": tgt_vocab_size,
        "langs": [src_lang, tgt_lang],
        [...]
        "bos_token_id": 0,
        "pad_token_id": 1,
        "eos_token_id": 2,
        "is_encoder_decoder": True,
        "scale_embedding": not args["no_scale_embedding"],
        "tie_word_embeddings": args["share_all_embeddings"],
    }

残っているのは、構成をconfig.jsonに保存し、pytorch.dumpに新しいstate_dictダンプを作成するだけです:

    print(f"{fsmt_tokenizer_config_file}を生成しています")
    with open(fsmt_tokenizer_config_file, "w", encoding="utf-8") as f:
        f.write(json.dumps(tokenizer_conf, ensure_ascii=False, indent=json_indent))
    [...]
    print(f"{pytorch_weights_dump_path}を生成しています")
    torch.save(model_state_dict, pytorch_weights_dump_path)

設定とモデルのstate_dictが移植されました – やったー!

最終的な変換コードはこちらにあります。

アーキテクチャコードの移植

モデルの重みとモデルの構成が移植されたので、modeling_bart.pyからコピーしたコードをfairseqの機能に合わせる必要があります。

最初のステップは、文をエンコードしてgenerate関数に入力することでした – fairseqtransformersのための。

いくつかの失敗した試みの後 (*) – 現在の複雑さのレベルでは、デバッグ方法としてprintを使用することは何も生じませんし、基本的なpdbデバッガーも同様です。効率的で複数の変数を監視し、コードの評価を行うウォッチを持つためには、本格的な視覚デバッガーが必要でした。私はさまざまなPythonデバッガーを試し、pycharmを試したときに初めて必要なツールだと気づきました。初めてpycharmを使用しましたが、直感的に使用方法を理解しました。

  • 注: モデルはロシア語で ‘nononono’ を生成しました – それは公正で楽しかったです!

時間が経つにつれて、pycharmの優れた機能を見つけました。これにより、機能ごとにブレークポイントをグループ化し、デバッグに応じてグループ全体をオンまたはオフにすることができました。たとえば、ここではビームサーチに関連するブレークポイントをオフにし、デコーダーに関連するブレークポイントをオンにしています:

FSMTを移植するためにこのデバッガーを使用したので、同じことをするためにpdbを使用すると何倍も時間がかかることがわかりました – おそらくそれを諦めるかもしれませんでした。

以下の2つのスクリプトから始めました:

  • fseq-translate
  • fsmt-translate

(最初はdecodeの部分はなしで)

両方のスクリプトを並行して実行し、それぞれのデバッガーでステップ実行し、関連する変数の値を比較しました – 最初の出現箇所を見つけるまで。その後、コードを学習し、modeling_fsmt.py内で調整を行い、デバッガーを再起動し、すばやく出現箇所にジャンプし、出力を再確認しました。このサイクルは、出力が一致するまで何度も繰り返されました。

最初に変更しなければならなかったのは、fairseqで使用されなかったいくつかのレイヤーを削除し、代わりに使用されている新しいレイヤーを追加することでした。そして、残りは主にsrc_vocab_sizeからtgt_vocab_sizeに切り替えるタイミングを見つけることでした – コアモジュールでは単にvocab_sizeですが、可能なモデルに2つの辞書があることを考慮していませんでした。最後に、いくつかのハイパーパラメータの設定が同じではなかったことがわかり、それらも変更しました。

最初に、より簡単なビームサーチを行った後、出力が100%一致するまでより複雑なビームサーチを行いました。ここでは、たとえば、fairseqearly_stopping=Trueの相当を使用していたのに対し、transformersではデフォルトでFalseになっていました。早期停止が有効になっている場合、ビームサイズと同じ数の候補があるとすぐに探索を停止し、無効になっている場合、アルゴリズムは既に持っているよりも高い確率の候補を見つけることができない場合にのみ探索を停止します。 fairseqの論文では、巨大なビームサイズの50が使用されており、早期停止の使用に対して補償されていると述べています。

Tokenizer decoder porting

移植されたgenerate関数がfairseqgenerateと非常に似た結果を生成するようになったら、次は出力を人が読めるテキストにデコードする最後のステージを完了する必要がありました。これにより、目視でクイックな比較と翻訳の品質を行うことができました – 出力IDではできませんでした。

エンコーディングプロセスと同様に、これは逆に行われました。

ステップは次のとおりです:

  1. 出力IDをテキスト文字列に変換する
  2. BPEエンコーディングを削除する
  3. 非トークン化 – エスケープ文字の処理など

ここでもさらにデバッグを行った後、tokenization_xlm.pyの元のアプローチからBPEの取り扱い方を変更する必要があり、また出力をmosesデトークナイザーを通して実行する必要がありました。

     def convert_tokens_to_string(self, tokens):
         """シーケンスのトークン(文字列)を単一の文字列に変換します。"""
-        out_string = "".join(tokens).replace("</w>", " ").strip()
-        return out_string
+        # BPEを削除
+        tokens = [t.replace(" ", "").replace("</w>", " ") for t in tokens]
+        tokens = "".join(tokens).split()
+        # デトークナイズ
+        text = self.moses_detokenize(tokens, self.tgt_lang)
+        return text

そして、すべてがうまくいきました。

モデルのs3へのアップロード

変換スクリプトがtransformersへの必要なすべてのファイルの移植を完了した後、モデルを私の🤗 s3アカウントにアップロードしました:

cd data
transformers-cli upload -y wmt19-ru-en
transformers-cli upload -y wmt19-en-ru
transformers-cli upload -y wmt19-de-en
transformers-cli upload -y wmt19-en-de
cd -

テストの間、私は🤗 s3アカウントを使用していました。そして、完全な変更がマージされる準備ができたときに、モデルをfacebookの組織アカウントに移動するようにPRで依頼しました。なぜなら、これらのモデルはそちらに所属するからです。

何度か、構成ファイルだけを更新する必要があり、大きなモデルを再度アップロードしたくなかったので、以下のスクリプトを作成しました。これにより、正しいアップロードコマンドが生成されます。入力するのは長すぎるため、間違いが生じる可能性がありました:

perl -le 'for $f (@ARGV) { print qq[transformers-cli upload -y $_/$f --filename $_/$f] \
for map { "wmt19-$_" } ("en-ru", "ru-en", "de-en", "en-de")}' \
vocab-src.json vocab-tgt.json tokenizer_config.json config.json
# 必要に応じてファイルを追加/削除してください

たとえば、config.jsonファイルのみを更新する必要がある場合、上記のスクリプトによって便利なコピー&ペーストが提供されます:

transformers-cli upload -y wmt19-en-ru/config.json --filename wmt19-en-ru/config.json
transformers-cli upload -y wmt19-ru-en/config.json --filename wmt19-ru-en/config.json
transformers-cli upload -y wmt19-de-en/config.json --filename wmt19-de-en/config.json
transformers-cli upload -y wmt19-en-de/config.json --filename wmt19-en-de/config.json

アップロードが完了したら、これらのモデルは次のようにアクセスできます(*):

tokenizer = FSMTTokenizer.from_pretrained("stas/wmt19-en-ru")
  • 脚注:stasはhttps://huggingface.coでのユーザー名です。

このアップロードを行う前に、モデルファイルがあるフォルダへのローカルパスを使用する必要がありました、例えば:

tokenizer = FSMTTokenizer.from_pretrained("/code/huggingface/transformers-fair-wmt/data/wmt19-en-ru")

重要:モデルファイルを更新して再度アップロードする場合、CDNキャッシュのためにアップロードされたモデルはアップロード後24時間まで利用できないことに注意してください。つまり、古いキャッシュされたモデルが配信されます。新しいモデルをすぐに使用する唯一の方法は、次のいずれかを行うことです:

  1. ローカルパスにダウンロードして、それをfrom_pretrained()に渡される引数として使用します。
  2. または、次の24時間の間、すべての場所でfrom_pretrained(..., use_cdn=False)を使用します – 1回だけでは十分ではありません。

AutoConfig、AutoTokenizerなど

私が行う必要があったもう一つの変更は、新しく移植されたモデルを自動化されたモデルtransformersシステムに組み込むことです。これは、主にモデルのウェブサイトでモデルの構成、トークナイザ、およびメインクラスを特定のクラス名を指定せずにロードするために使用されます。たとえば、FSMTの場合、次のようにできます:

from transformers import AutoTokenizer, AutoModelWithLMHead
mname = "facebook/wmt19-en-ru"
tokenizer = AutoTokenizer.from_pretrained(mname)
model = AutoModelWithLMHead.from_pretrained(mname)

これらのマッピングを有効にするための3つの*auto*ファイルがあります:

-rw-rw-r-- 1 stas stas 16K Sep 23 13:53 src/transformers/configuration_auto.py
-rw-rw-r-- 1 stas stas 65K Sep 23 13:53 src/transformers/modeling_auto.py
-rw-rw-r-- 1 stas stas 13K Sep 23 13:53 src/transformers/tokenization_auto.py

次に、パイプラインがあります。これらは、エンドユーザーからNLPの複雑さを完全に隠し、単にモデルを選択してタスクに使用するための非常にシンプルなAPIを提供します。たとえば、pipelineを使用して要約タスクを実行する方法は次のとおりです:

summarizer = pipeline("summarization", model="t5-base", tokenizer="t5-base")
summary = summarizer("Some long document here", min_length=5, max_length=20)
print(summary)

翻訳のパイプラインは、この文章の執筆時点では進行中の作業です。翻訳がサポートされるようになると、このドキュメントで更新情報を確認してください(現在、一部の特定のモデル/言語のみがサポートされています)。

最後に、src/transforers/__init__.pyを編集して、次のようにすることもできます:

from transformers import FSMTTokenizer, FSMTForConditionalGeneration

代わりに次のようにすることもできます:

from transformers.tokenization_fsmt import FSMTTokenizer
from transformers.modeling_fsmt import FSMTForConditionalGeneration

どちらの方法でも動作します。

FSMTを接続するために必要な場所をすべて見つけるために、BartConfigBartForConditionalGenerationBartTokenizerを模倣しました。それに対してgrepを使用し、それらのクラスを持つファイルを検索し、FSMTConfigFSMTForConditionalGenerationFSMTTokenizerの対応するエントリを挿入しました。

$ egrep -l "(BartConfig|BartForConditionalGeneration|BartTokenizer)" src/transformers/*.py \
| egrep -v "(marian|bart|pegasus|rag|fsmt)"
src/transformers/configuration_auto.py
src/transformers/generation_utils.py
src/transformers/__init__.py
src/transformers/modeling_auto.py
src/transformers/pipelines.py
src/transformers/tokenization_auto.py

grepの検索では、それらのクラスも含まれているファイルは除外しました。

手動テスト

これまでは、主に独自のスクリプトを使用してテストを行っていました。

翻訳機能が動作するようになった後、逆転させたru-enモデルを変換し、次に2つのパラフレーズスクリプトを作成しました:

  • fseq-paraphrase
  • fsmt-paraphrase

これらのスクリプトは、ソース言語の文を別の言語に翻訳し、その結果を元の言語に戻すという処理を行い、通常は言語間で類似の表現方法が異なるため、言い換えられた結果が得られます。

これらのスクリプトの助けを借りて、トークナイザの問題をいくつか見つけ、デバッガを使用して手順を踏んでfsmtスクリプトがfairseqバージョンと同じ結果を生成するようにしました。

この段階では、ビームサーチを使用しない場合はほとんど同じ結果が得られましたが、ビームサーチではまだいくつかの相違点がありました。特殊なケースを特定するために、sacrebleuのテストデータを使用して、fairseqおよびtransformersの翻訳を実行し、不一致のみを報告するfsmt-port-validate.pyスクリプトを作成しました。パターンを観察することで、いくつかの残りの問題を素早く特定し、それらの問題を修正することができました。

他のモデルの移植

次に、en-deおよびde-enモデルを移植しました。

これらが同じ方法で構築されていないことに驚きました。それぞれの辞書がマージされていましたので、しばらくはイライラしました。なぜなら、それをサポートするために別の大きな変更を行わなければならないと思っていたからです。しかし、実際には変更は必要ありませんでした。マージされた辞書を変更することなく、2つの同じ辞書を使用しました – 1つはソースとして、もう1つはそれをコピーしたものをターゲットとして。

すべての移植モデルの基本的な機能をテストする別のスクリプトを作成しました:fsmt-test-all.py.

テストカバレッジ

この次のステップは非常に重要でした – 移植モデルのための包括的なテストを準備する必要がありました。

transformersのテストスイートでは、大きなモデルに関連するほとんどのテストが@slowとしてマークされており、これらは通常CI(継続的インテグレーション)で実行されません。ですので、通常の事に遅いです。したがって、移植機能をテストするために、同じ構造を持つ非常に小さいモデルを作成する必要がありました。この小さなモデルはランダムな重みを持つことができます。ただし、品質テストには使用できません。重みがほんの数つだけであり、実際には何らかの実用的なトレーニングを行うことはできません。fsmt-make-tiny-model.pyではこのような小さなモデルを作成します。生成されたモデルとそのすべての辞書および設定ファイルは、わずか3MBのサイズでした。それをs3transformers-cli uploadを使ってアップロードし、テストスイートで使用することができました。

コードと同様に、tests/test_modeling_bart.pyをコピーしてFSMTを使用するように変換し、新しいモデルで動作するように調整しました。

いくつかの手動テストに使用していたスクリプトをユニットテストに変換しました – それは簡単でした。

transformersには、すべてのモデルが通過する多くの共通のテストがあります。これらのテストをFSMTで動作するように調整するためにさらにいくつかの調整を行う必要がありました(主に2つの辞書のセットアップに調整するため)そして、このモデルの一意性のために実行できなかったいくつかのテストをスキップするために、いくつかのテストをオーバーライドする必要がありました。結果はこちらでご覧いただけます。

さらに、軽いBLEU評価を行うためのテストを追加しました。4つのモデルごとに8つのテキスト入力を使用してBLEUスコアを測定しました。テストとデータを生成したスクリプトはこちらです。

SinusoidalPositionalEmbedding

fairseqは、transformersで使用されているSinusoidalPositionalEmbeddingとはやや異なる実装を使用していました。最初はfairseqの実装をコピーしました。ただし、テストスイートを動作させようとすると、torchscriptのテストが合格しませんでした。 SinusoidalPositionalEmbeddingstate_dictの一部にならず、モデルの重みとして保存されませんでした – このクラスによって生成されるすべての重みは決定論的であり、トレーニングされませんでした。 fairseqは、その重みをパラメータまたはバッファにしないというトリックを使用して、これを透過的に機能させるようにしました。そして、forward呼び出しの前に重みを正しいデバイスに切り替えました。torchscriptでは、これを受け入れることができませんでした。最初のforward呼び出しの前にすべての重みが正しいデバイス上にあることを求めていたためです。

この実装を書き換えて、通常のnn.Embeddingのサブクラスに変換し、save_pretrained()中にこれらの重みを保存しない機能を追加し、from_pretrained()state_dictのロード中にこれらの重みを見つけることができない場合にエラーを出さないようにしました。

評価

大量のテキストでの手動テストに基づいて、移植モデルは非常にうまく機能していることはわかっていましたが、元のモデルと比較して移植モデルの性能がどれほど良いのかはわかりませんでした。そのため、評価する時間が来ました。

翻訳のタスクでは、BLEUスコアが評価指標として使用されます。評価を実行するためのtransformersのスクリプトrun_eval.pyがあります。

ここでは、ru-enのペアの評価結果が表示されます。

export PAIR=ru-en
export MODEL=facebook/wmt19-$PAIR
export DATA_DIR=data/$PAIR
export SAVE_DIR=data/$PAIR
export BS=64
export NUM_BEAMS=5
export LENGTH_PENALTY=1.1
mkdir -p $DATA_DIR
sacrebleu -t wmt19 -l $PAIR --echo src > $DATA_DIR/val.source
sacrebleu -t wmt19 -l $PAIR --echo ref > $DATA_DIR/val.target
PYTHONPATH="src:examples/seq2seq" python examples/seq2seq/run_eval.py $MODEL \
$DATA_DIR/val.source $SAVE_DIR/test_translations.txt --reference_path $DATA_DIR/val.target \
--score_path $SAVE_DIR/test_bleu.json --bs $BS --task translation --num_beams $NUM_BEAMS \
--length_penalty $LENGTH_PENALTY --info $MODEL --dump-args

数分かかることで実行され、以下の結果が返ってきます:

{'bleu': 39.0498, 'n_obs': 2000, 'runtime': 184, 'seconds_per_sample': 0.092, 
'num_beams': 5, 'length_penalty': 1.1, 'info': 'ru-en'}

BLEU スコアは 39.0498 であり、sacrebleu を使用して wmt19 データセットで 2000 個のテスト入力を評価しました。

モデルアンサンブルを使用できなかったため、次に最もパフォーマンスの良いチェックポイントを見つける必要がありました。そのために、各チェックポイントを変換し、評価スクリプトを実行し、最も優れたものを報告するスクリプト fsmt-bleu-eval-each-chkpt.py を作成しました。結果として、4 つの利用可能なチェックポイントの中で model4.pt が最良のパフォーマンスを提供していることがわかりました。

元の論文で報告されているBLEUスコアとは異なるスコアが得られていないことを確認するために、同じデータとツールを使用しているかどうかを確認する必要がありました。 fairseq の問題で尋ねた結果、彼らがBLEUスコアを取得するために使用したコードを教えてもらいました(こちらで見つけることができます)。ただし、彼らの方法は非公開の再ランキング手法を使用していたため、我々は同じ方法でスコアをつけていなかったことがわかりました。さらに、彼らはデトークン化される前の出力で評価し、実際の出力ではないことが分かりましたが、これがより良いスコアをつけるようです。結論として、私たちは同じ方法でスコアをつけていませんでした(*)。

  • 脚注:論文「A Call for Clarity in Reporting BLEU Scores」は、開発者にメトリックの計算方法を統一するよう呼びかけています(要約: sacrebleu を使用してください)。

現在、この移植モデルは元のモデルよりもBLEUスコアがわずかに低くなっていますが、モデルアンサンブルが使用されていないため、正確な差を確定することはできません。

新しいモデルの移植

ここに4つの fairseq モデルをアップロードした後、3つの wmt16 および2つの wmt19 AllenAI モデル(Jungo Kasai ら)を移植することが提案されました。移植は簡単でしたが、関連のない複数のアーカイブに分散しているため、すべてのソースファイルをまとめる方法を見つける必要がありました。これが完了すると、変換は問題なく動作しました。

移植後に見つけた唯一の問題は、元のものよりも低いBLEUスコアが得られることでした。これらのモデルの作成者である Jungo Kasai は、カスタムのハイパーパラメータ length_penalty=0.6 を使用していることを提案し、これを適用すると大幅に改善された結果が得られました。

この発見により、最も優れたBLEUスコアを得るためのさまざまなハイパーパラメータを検索するための新しいスクリプト run_eval_search.py を作成しました。以下はその使用例です:

# search space
export PAIR=ru-en
export DATA_DIR=data/$PAIR
export SAVE_DIR=data/$PAIR
export BS=32
mkdir -p $DATA_DIR
sacrebleu -t wmt19 -l $PAIR --echo src > $DATA_DIR/val.source
sacrebleu -t wmt19 -l $PAIR --echo ref > $DATA_DIR/val.target
PYTHONPATH="src:examples/seq2seq" python examples/seq2seq/run_eval_search.py stas/wmt19-$PAIR \
$DATA_DIR/val.source $SAVE_DIR/test_translations.txt --reference_path $DATA_DIR/val.target \
--score_path $SAVE_DIR/test_bleu.json --bs $BS --task translation \
--search="num_beams=5:8:11:15 length_penalty=0.6:0.7:0.8:0.9:1.0:1.1 early_stopping=true:false"

ここでは、num_beamslength_penaltyearly_stopping のすべての可能な組み合わせを検索します。

実行が完了すると、以下のように報告されます:

bleu  | num_beams | length_penalty | early_stopping
----- | --------- | -------------- | --------------
39.20 |        15 |            1.1 |              0
39.13 |        11 |            1.1 |              0
39.05 |         5 |            1.1 |              0
39.05 |         8 |            1.1 |              0
39.03 |        15 |            1.0 |              0
39.00 |        11 |            1.0 |              0
38.93 |         8 |            1.0 |              0
38.92 |        15 |            1.1 |              1
[...]

transformers の場合、early_stopping=False の方が良い結果が得られます(fairseq では early_stopping=True 相当の設定を使用します)。

したがって、新しい5つのモデルに対して、このスクリプトを使用して最適なデフォルトパラメータを見つけ、モデル変換時にそれらを使用しました。ユーザーは generate() を呼び出す際にこれらのパラメータを上書きすることもできますが、なぜ最適なデフォルトを提供しないのでしょうか。

5つの移植済みのAllenAIモデルはこちらで見つけることができます。

その他のスクリプト

各移植グループのモデルには独自のニュアンスがありますので、将来的に再構築したり、新しいモデルを変換するための新しいスクリプトを作成するのが容易になるように、それぞれに専用のスクリプトを作成しました。変換、評価、その他のスクリプトはこちらで見つけることができます。

モデルカード

モデルを移植して他の人に利用できるようにするだけでは十分ではありません。利用方法、ハイパーパラメータのニュアンス、データセットのソース、評価メトリックなどの情報を提供する必要があります。これはモデルカードを作成することによって行われます。モデルカードは単なる README.md ファイルで、モデルのウェブサイトで使用されるいくつかのメタデータから始まり、共有できる有用な情報が続きます。

例えば、facebook/wmt19-en-ru のモデルカードを見てみましょう。以下はそのトップ部分です:

---
language: 
- en
- ru
thumbnail:
tags:
- translation
- wmt19
- facebook
license: apache-2.0
datasets:
- wmt19
metrics:
- bleu
---

# FSMT

## モデルの説明

これは以下のモデルの移植版です:
[...]

上記のように、言語、タグ、ライセンス、データセット、メトリックを定義しています。これらの記述方法については、モデルの共有とアップロードに関する完全なガイドがあります。その後は、モデルとそのニュアンスについて説明するマークダウンド文書が続きます。また、モデルページから直接モデルを試すこともできます。例えば、英語からロシア語への翻訳の場合は、次のリンクからお試しください: https://huggingface.co/facebook/wmt19-en-ru?text=My+name+is+Diego+and+I+live+in+Moscow 。

ドキュメンテーション

最後に、ドキュメンテーションの追加が必要でした。

幸いなことに、ほとんどのドキュメンテーションはモジュールファイルのドックストリングから自動生成されます。

前回と同様に、docs/source/model_doc/bart.rst をコピーし、FSMT に適応させました。準備ができたら、docs/source/index.rst 内に fsmt エントリを追加してリンクを作成しました。

次のコマンドを使用しました:

make docs

これにより、新しく追加されたドキュメントが正しくビルドされているかどうかをテストしました。実行後にチェックする必要があるファイルは docs/_build/html/model_doc/fsmt.html で、ブラウザで読み込んで正しく表示されていることを確認しました。

以下は最終的なソースドキュメントの docs/source/model_doc/fsmt.rst とその表示されたバージョンです。

PRの時間です

仕事がかなり完了したと感じた時、私はPRを提出する準備ができました。

この作業では多くのgitコミットが関わっていたため、クリーンなPRを作りたかったので、以下のテクニックを使用して新しいブランチですべてのコミットを一つにまとめました。これにより、後でそれらのいずれかにアクセスしたい場合に初期のコミットをすべて保持できました。

私が開発していたブランチはfair-wmtと呼ばれ、PRを提出するための新しいブランチはfair-wmt-cleanと名付けました。以下に私が行った手順を示します:

git checkout master
git checkout -b fair-wmt-clean
git merge --squash fair-wmt
git commit -m "Ready for PR"
git push origin fair-wmt-clean

それから私はGithubに行って、このfair-wmt-cleanブランチを基にPRを提出しました。

フィードバックのいくつかのサイクルと修正、そしてそのようなサイクルの数週間を経て、最終的にすべてが満足のいくものとなり、PRはマージされました。

このプロセスが進行している間、問題を見つけたり、新しいテストを追加したり、ドキュメントを改善したりなど、いくつかの課題に取り組みましたので、時間を有効に使うことができました。

その後、いくつかの機能を改善し再構築した後、さまざまなビルドスクリプトやモデルカードなどを追加し、さらにいくつかの変更を加えたPRをいくつか追加しました。

私が移植したモデルはfacebookallenaiの組織に所属していたため、それらのモデルファイルを私のs3アカウントから対応する組織に移動するようにSamに依頼しなければなりませんでした。

まとめ

  • transformersはモデルアンサンブルをサポートしていないため、モデルアンサンブルを移植することはできませんでしたが、プラスの面では、最終的なfacebook/wmt19-*モデルのダウンロードサイズは13GBではなく1.1GBです。何故なら、元のモデルにはモデルのオプティマイザの状態が含まれているため、テキストを翻訳するためにモデルをそのままダウンロードするだけの人々にとって、ほぼ9GB(4×2.2GB)の無駄な重さが追加されるからです。

  • ポーティングの仕事は、最初はtransformersfairseqの内部構造を知らなかったため、非常に困難に見えましたが、振り返ってみるとそれほど難しくありませんでした。これは主に、私が必要なコンポーネントのほとんどを既にtransformersのさまざまな部分で利用できていたためです。必要なパーツを見つけ、主に他のモデルから大いに借りながら、それらを私が必要とするように調整する必要がありました。これはコードとテストの両方に当てはまります。言い換えると、ポーティングは困難でしたが、すべてをゼロから書く必要があった場合、さらに困難でした。そして、適切なパーツを見つけることは簡単ではありませんでした。

感謝

  • このプロセスを通じて私を指導してくれたSam Shleiferは、彼の技術的なサポートと同様に、私が行き詰まった時にはインスピレーションを与えて励ましてくれたことで、非常に助けになりました。

  • PRのマージプロセスは受け入れられるまでに数週間かかりました。この段階では、Sam以外にもLysandre DebutとSylvain Guggerが、彼らの洞察と提案を通じて多くの貢献をしました。これらは私のコードベースに統合しました。

  • 私の作業のために道を開いてくれたtransformersコードベースに貢献してくれたすべての人に感謝しています。

ノート

Jupyter Notebookでのすべての自動印刷

私のJupyter Notebookは、すべての式を自動的に印刷するように設定されているため、明示的にprint()する必要はありません。デフォルトの動作は、各セルの最後の式のみを印刷することです。したがって、私のノートブックの出力を読む場合、同じセットアップを持っている場合を除いて、自分自身で実行する場合とは異なる場合があります。

次の内容を~/.ipython/profile_default/ipython_config.pyに追加することで、Jupyter Notebookの設定でプリントオール機能を有効にすることができます(存在しない場合は作成してください):

c = get_config()
# すべてのノードを対話的に実行
c.InteractiveShell.ast_node_interactivity = "all"
# 元の動作に戻す
# c.InteractiveShell.ast_node_interactivity = "last_expr"

お読みいただきありがとうございます。Jupyter Notebook サーバーを再起動する必要があります。

この記事を後日読む場合でも、すべてのリンクが機能するようにするために、リンクはコードの特定の SHA バージョンに作成されており、必ずしも最新バージョンではありません。これにより、ファイルの名前が変更されたり削除された場合でも、この記事が参照しているコードを見つけることができます。コードの最新バージョンを確認したい場合は、リンク内のハッシュコードを master に置き換えてください。たとえば、次のリンク:

https://github.com/huggingface/transformers/blob/129fdae04033fe4adfe013b734deaec6ec34ae2e/src/transformers/modeling_fsmt.py

は以下のようになります:

https://github.com/huggingface/transformers/blob/master/src/transformers/convert_fsmt_original_pytorch_checkpoint_to_pytorch.py

お読みいただきありがとうございました!

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

機械学習

3つの質問:大規模言語モデルについて、Jacob Andreasに聞く

CSAILの科学者は、最新の機械学習モデルを通じた自然言語処理の研究と、言語が他の種類の人工知能をどのように高めるかの調査...

人工知能

「マーシャンの共同創設者であるイータン・ギンスバーグについてのインタビューシリーズ」

エタン・ギンズバーグは、マーシャンの共同創業者であり、すべてのプロンプトを最適なLLMに動的にルーティングするプラットフ...

人工知能

ファイデムのチーフ・プロダクト・オフィサー、アルパー・テキン-インタビューシリーズ

アルパー・テキンは、FindemというAI人材の獲得と管理プラットフォームの最高製品責任者(CPO)ですFindemのTalent Data Clou...

人工知能

ベイリー・カクスマー、ウォータールー大学の博士課程候補 - インタビューシリーズ

カツマー・ベイリーは、ウォータールー大学のコンピュータ科学学部の博士課程の候補者であり、アルバータ大学の新入教員です...

AIテクノロジー

アンソニー・グーネティレケ氏は、Amdocsのグループ社長であり、テクノロジー部門および戦略部門の責任者です- インタビューシリーズ

アンソニー・グーネティレーケは、Amdocsでグループ社長、テクノロジーと戦略担当です彼と企業戦略チームは、会社の戦略を策...

人工知能

ジョシュ・フィースト、CogitoのCEO兼共同創業者 - インタビューシリーズ

ジョシュ・フィーストは、CogitoのCEO兼共同創業者であり、感情と会話AIを組み合わせた革新的なプラットフォームを提供するエ...