翻訳プログラムの作り方

2020年10月14日

Transformerは現在、自然言語処理における業界標準になっています。

音声認識機械翻訳テキスト読み上げなど系列変換処理のために開発されたTransformerは、ニューラルネットワークとAttentionメカニズム(注意機構)を組み合わせて機能し、従来のアーキテクチャよりはるかに効率的です。さらに、自然言語処理だけでなく、コンピュータビジョンや音楽生成の分野でも実装されています。

このように様々な分野で活用されているにもかかわらず、Transformerは依然として非常にわかりづらいモデルです。本記事では、エンコーダおよびデコーダのアーキテクチャと、様々な層から構成されるニューラルネットワークの全体的なデータフローをご紹介します。英語をドイツ語に変換する独自の翻訳プログラムを実装することにより、Transformerについてさらに理解を深めたいと思います。

Transformerを利用して英語からドイツ語に変換する翻訳プログラムを作成します。英語の文章を入力とし、ドイツ語の文章を出力とするブラックボックスとして捉えることができます。

英語からドイツ語に変換する翻訳プログラムをトレーニングするためには、英語とドイツ語コーパスが必要です。

torchtext.datasetsを使ってIWSLT(音声翻訳に関する国際ワークショップ)のデータセットを利用することができます。この対訳コーパスは、翻訳タスクでの業界標準であり、多数の言語で様々なトピックをカバーするTEDおよびTEDxのトークが含まれています。

コーディングの説明に入る前に、学習の際、モデルの入力および出力として必要となるデータを見てみましょう。ネットワークへの入力として二種類の行列が必要です。

原文となる英語文(ソース文)バッチサイズ x ソース文の長さの形状の行列です。この行列の数字は、英語辞書(作成する必要があります)に基づく特定の単語に対応しています。例えば、英語辞書のインデックス番号234は「the」という単語に対応するという具合です。上の図では、いくつかの文がインデックス番号6で終わっています。 全ての文が同じ長さでないため、インデックス番号6でパディングされているのです。つまり、6は<blank>トークンを表しています。

シフトさせたドイツ語の翻訳文(ターゲット文)ソース文の場合と同様、この行列の数字は、ドイツ語辞書(作成する必要があります)に基づく特定の単語に対応しています。この行列には、一定のパターンがあるように見えます。全ての文がドイツ語のインデックス番号2で始まり、常に一定のパターンに従って終わります。[3が必ず含まれ、1が一つあるいは複数個含まれる場合があります。]これは意図的なものです。ターゲット文が開始トークン(つまり、2が<s>トークン)で始まり、終了トークン(3が</s>トークンです)とパディング用トークン(1が<blank>トークンに相当します)で終わるようにしたいからです。

データ前処理方法を理解したところで、前処理を実行するための実際のコードについて見ていきましょう。まず、ドイツ語と英語の文をトークン化するトークナイザを提供するspaCyモデルをロードします。

# Load the Spacy Models
spacy_de = spacy.load('de')
spacy_en = spacy.load('en')

def tokenize_de(text):
return [tok.text for tok in spacy_de.tokenizer(text)]

def tokenize_en(text):
return [tok.text for tok in spacy_en.tokenizer(text)]

空白埋め・パディングや文の開始および終了を示すために利用する特別なトークンを定めます。

# Special TokensBOS_WORD = '<s>'EOS_WORD = '</s>'BLANK_WORD = "<blank>"

これで、torchtextのdata.fieldを利用してソース文とターゲット文両方の前処理パイプラインを定義することができます。ソース文ではpad_tokenだけを指定しますが、ターゲット文では pad_tokeninit_token、そしてeos_tokenに言及しています。また、どのトークナイザを使用するかも定義します。

SRC = data.Field(tokenize=tokenize_en, pad_token=BLANK_WORD)TGT = data.Field(tokenize=tokenize_de, init_token = BOS_WORD,eos_token = EOS_WORD, pad_token=BLANK_WORD)

ここまで、データを一切使用していないので、torchtext.datasetsのIWSLTデータを使用してトレーニング用、検証用、テスト用のデータセットを作成してみましょう。MAX_LENパラメータを利用して文をフィルタリングすれば、コードを迅速に実行できます。.enおよび.deの拡張子を持つデータを取得し、fieldsパラメータを使用して前処理ステップを指定します。

MAX_LEN = 20train, val, test = datasets.IWSLT.splits(    exts=('.en''.de'), fields=(SRC, TGT),    filter_pred=lambda x: len(vars(x)['src']) <= MAX_LEN    and len(vars(x)['trg']) <= MAX_LEN)

学習データを取得できたので、どのようになったか見てみましょう。

for i, example in enumerate([(x.src,x.trg) for x in train[0:5]]):print(f"Example_{i}:{example}")

---------------------------------------------------------------

Example_0:(['David''Gallo'':''This''is''Bill''Lange''.''I'"'m"'Dave''Gallo''.']['David''Gallo'':''Das''ist''Bill''Lange''.''Ich''bin''Dave''Gallo''.'])

Example_1:(['And''we'"'re"'going''to''tell''you''some''stories''from''the''sea''here''in''video''.']['Wir''werden''Ihnen''einige''Geschichten''über''das''Meer''in''Videoform''erzählen''.'])

Example_2:(['And''the''problem'',''I''think'',''is''that''we''take''the''ocean''for''granted''.']['Ich''denke'',''das''Problem''ist'',''dass''wir''das''Meer''für''zu''selbstverständlich''halten''.'])

Example_3:(['When''you''think''about''it'',''the''oceans''are''75''percent''of''the''planet''.']['Wenn''man''darüber''nachdenkt'',''machen''die''Ozeane''75''%''des''Planeten''aus''.'])

Example_4:(['Most''of''the''planet''is''ocean''water''.']['Der''Großteil''der''Erde''ist''Meerwasser''.'])

data.fieldオブジェクトはトークン化を実行しましたが、開始、終了、パディング用トークンがまだ適用されていません。これは意図的なものです。パディング用トークンの数は本来、特定のバッチの文の最大長に依存するのですが、私たちはまだバッチを持っていないからです。また、最初に述べた通りdata.fieldオブジェクトに組み込まれた機能を利用してソース文とターゲット文の辞書を作成します。MIN_FREQを2に指定し、2回以上出現していない単語は辞書に含めないようにします。

MIN_FREQ = 2SRC.build_vocab(train.src, min_freq=MIN_FREQ)TGT.build_vocab(train.trg, min_freq=MIN_FREQ)

これが完了したらdata.Bucketiteratorを利用してtrain_iteratorとvalid_iteratorを取得します。data.Bucketiteratorを使用すると、同じような長さのバッチを取得できます。検証データではバッチサイズを1にしています。必ずしもこのようにする必要はありませんが、検証データのパフォーマンスをチェックする際、パディングを避けたり、パディングを最小限にしたりするために行っています。

BATCH_SIZE = 350

# Create iterators to process text in batches of approx. the same length by sorting on sentence lengths

train_iter = data.BucketIterator(train, batch_size=BATCH_SIZE, repeat=False, sort_key=lambda x: len(x.src))

val_iter = data.BucketIterator(val, batch_size=1, repeat=False, sort_key=lambda x: len(x.src))

先に進む前に、どのようなバッチなのか、つまりモデルが学習する際の入力データはどのようなものなのかを確認しておくとよいでしょう。

batch = next(iter(train_iter))src_matrix = batch.src.Tprint(src_matrix, src_matrix.size())

以下がソース文の行列です。

trg_matrix = batch.trg.Tprint(trg_matrix, trg_matrix.size())

そして、以下がターゲット文の行列です。

最初のバッチにはsrc_matrixに長さ20の文350個が含まれtrg_matrixに長さ22の文350個が含まれています。前処理がきちんと行われたことを確認するためにsrc_matrixtrg_matrixで数字が何を表しているのか、いくつかチェックしてみましょう。

print(SRC.vocab.itos[1])print(TGT.vocab.itos[2])print(TGT.vocab.itos[1])--------------------------------------------------------------------<blank><s><blank>

文字列からインデックスを確認する方法でも構いません。

print(TGT.vocab.stoi['</s>'])--------------------------------------------------------------------3

transformer

以上でソース文とシフトさせたターゲット文をTransformerに入力する準備ができたので、自然言語処理用のTransformerの作成に移りましょう。

ここに記載した多くのブロックは、Pytorchのnn.moduleから引用したものです。実際、PytorchにはTransformer moduleもありますが、埋め込み層や位置エンコーディング層など、論文に記載されている多くの機能は含まれていません。そこで、Pytorchの実装からも多数引用して、完全なTransformerの実装に近づけています。

 

Pytorchのnn.moduleから引用したこれら様々なブロックを利用してTransformerを作成します。

各層で行われているのは、単に行列演算に過ぎません。特に、デコーダスタックがどのようにエンコーダからのメモリを入力として受け取るのかに注意してください。また、単語埋め込みに位置情報を追加するため、位置エンコーディング層も作成します。

以下のようにしてTransformerとオプティマイザを初期化できます。

source_vocab_length = len(SRC.vocab)target_vocab_length = len(TGT.vocab)

model = MyTransformer(source_vocab_length=source_vocab_length,target_vocab_length=target_vocab_length)

optim = torch.optim.Adam(model.parameters(), lr=0.0001, betas=(0.90.98), eps=1e-9)

model = model.cuda()

論文の著者は、学習率スケジューリングを用いたAdamオプティマイザを利用していますが、ここでは話を簡単にするため、通常のAdamオプティマイザを使用します。

 

翻訳プログラムのトレーニング

これで、以下のようにtrain関数を用いてTransformerをトレーニングすることができます。基本的にトレーニングで行うのは次のようなことです。

  • バッチからsrc_matrixとtrg_matrixを取得します。
  • src_maskを作成します。これは、src_matrixデータ内のパディングされた単語についてモデルに通知します。
  • trg_maskを作成します。どの時点においてもモデルがこれから出現するターゲット単語を見ることができないようにするためです。
  • モデルから予測を取得します。
  • 交差エントロピーを利用して損失を計算します。(論文では、KLダイバージェンスが使用されていますが、Transformerを理解するためには交差エントロピーでも十分です。)
  • 誤差逆伝播法を利用します。
  • 検証時の損失に基づいて最良のモデルを選択します。
  • デバッグの段階では、greedy_decode_sentence関数を利用し、全てのエポックで任意の文の出力を予測します。この関数については結果のセクションで詳しくご説明いたします。

では、以下のコードを利用してトレーニングを実行しましょう。

train_losses,valid_losses = train(train_iter, val_iter, model, optim, 35)

以下はトレーニング時の出力です。(一部のエポックのみ表示)

Epoch [1/35] completeTrain Loss: 86.092Val Loss: 64.514Original Sentence: This is an example to check how our model is performing.Translated Sentence: Und die der der der der der der der der der der der der der der der der der der der der der der der

Epoch [2/35] completeTrain Loss: 59.769Val Loss: 55.631Original Sentence: This is an example to check how our model is performing.Translated Sentence: Das ist ein paar paar paar sehr , die das ist ein paar sehr Jahre . </s>

....

Epoch [16/35] completeTrain Loss: 21.791Val Loss: 28.000Original Sentence: This is an example to check how our model is performing.Translated Sentence: Hier ist ein Beispiel , um zu prüfen , wie unser Modell aussieht . Das ist ein Modell . </s>

....

Epoch [34/35] completeTrain Loss: 9.492Val Loss: 31.005Original Sentence: This is an example to check how our model is performing.Translated Sentence: Hier ist ein Beispiel , um prüfen zu überprüfen , wie unser Modell ist . Wir spielen . </s>

Epoch [35/35] completeTrain Loss: 9.014Val Loss: 32.097Original Sentence: This is an example to check how our model is performing.Translated Sentence: Hier ist ein Beispiel , um prüfen wie unser Modell ist . Wir spielen . </s>

最初は「Und die der der der der der der der der der der der der der der der der der der der der der der der」といった翻訳ですとなってしまいましたが、何回か繰り返すうちにより理解可能なものになります。

 

翻訳プログラムの結果

Plotly Expressを用いてトレーニング時と検証時の損失を図示できます。

import pandas as pdimport plotly.express as pxlosses = pd.DataFrame({'train_loss':train_losses,'val_loss':valid_losses})px.line(losses,y = ['train_loss','val_loss'])

デプロイするには、以下のコードを使います。

model.load_state_dict(torch.load(fcheckpoint_best_epoch.pt”))

greeedy_decode_sentence関数を用いれば、どのようなソース文に対する予測も取得できます。

この関数は、区分的予測を行います。貪欲法は次のように実行されます。

  • 英語文全体をエンコーダの入力とし、開始トークン<s>だけを「シフトさせた出力(デコーダへの入力)」にしてフォワードパスを行います。
  • モデルが次の単語を予測します。—der
  • 次に、英語文全体をエンコーダの入力とし、すぐ前に予測された単語を加えて「シフトさせた出力(デコーダへの入力=<s> der)」にしてフォワードパスを行います。
  • モデルが次の単語を予測します。—schnelle
  • 英語文全体をエンコーダの入力とし、<s> der schnelleを「シフトさせた出力(デコーダへの入力)」にしてフォワードパスを行います。
  • モデルが終了トークン</s>を予測するか、最大数のトークン(定義可能なもの)を生成するまでこれを続けます。モデルが故障した場合に翻訳が無限に繰り返されないようにするためです。

これを利用すればどのような文でも翻訳できます。

sentence = "Isn't Natural language processing just awesome? Please do let me know in the comments."

print(greeedy_decode_sentence(model,sentence))

------------------------------------------------------------------

Ist es nicht einfach toll ? Bitte lassen Sie mich gerne in den Kommentare kennen . </s>

ドイツ語の翻訳機が手元にないので、自然言語処理のTransformerモデルのパフォーマンスを確認するため、次善の策を利用します。Googleの翻訳サービスを使用して、ドイツ語の意味を理解してみましょう。

「自然言語処理」が抜けているなど、明らかなミスがいくつかあるものの、たった1時間のトレーニングでニューラルネットワークが両言語の構造を理解できるようになったことを考えると、まずまずの成果であると言えそうです。

論文と同じ方法で全てを実行した場合、より良い結果が得られた可能性があります。

  • 全てのデータに基づいてトレーニングを行う
  • バイト対符号化
  • 学習率のスケジューリング
  • KLダイバージェンス
  • ビームサーチ
  • チェックポイント・アンサンブル手法

 

これら全ては以前の記事でご説明したものであり、実装も簡単です。ここでこれらを含めなかったのは、この記事の目的はTransformerの仕組みを理解することなので、あまり複雑にしたくなかったからです。とはいえ、Transformerモデルはさらに進化し、翻訳精度も向上しています。これについては、Transformerを中核にした人気の高い自然言語処理モデル、BERTに関する次の記事でご紹介したいと思います。

本記事では、英語からドイツ語への翻訳プログラムを作成することにより、自然言語処理におけるTransformerのアーキテクチャを見てきました。本記事のコードの詳細については、GitHubリポジトリでご確認ください。

 

※ 本記事は2020年9月25日、弊社英語ブログに掲載された寄稿記事に基づいたものです

ライオンブリッジのAI教師データサービス

当社は20年以上に渡るAIプロジェクトの実績を持ち、データ作成・アノテーションサービスを提供しております。データサイエンティストや言語学者を含み、100万人のアノテーターが登録されているので、大規模なAIプロジェクトも迅速且つ正確に仕上げます。アノテーターは秘密保持契約に署名することが義務付けられており、データ保護のためにオンサイトスタッフやリモートスタッフを派遣し、アノテーターにお客様ご指定のツールを利用してもらうこともできます。必要に応じて案件に特化した秘密保持契約も作成できるので、データの安全性も保証しております。ご相談・無料トライアルはこちらから。

AI向け教師データを提供し、研究開発をサポートいたします。

メディア掲載結果

    AI・機械学習の最新情報をお届けします!

    Lionbridge AIのブログで紹介している事例記事やトレンドニュースといったビジネスに役立つ情報はもちろん、オープンデータセット集なども合わせてメール配信しております。