PyTorchによるSeq2Seq翻訳者の紹介
紹介
ニューラル機械翻訳は、深層学習を使用して、一つの言語から別の言語へのテキストの正確な翻訳を生成する実践です。これは、深層ニューラルネットワークを訓練して、単語の配列が正しい翻訳としての可能性を予測することを意味します。
このような技術の用途はほぼ無限です。今日では、他の言語で書かれたウェブページ全体をほぼ瞬時に、そして比較的正確に翻訳できる翻訳者が存在します。私たちはカメラをテキストに向けて、拡張現実を使ってそのテキストを翻訳に置き換えることができます。さらに、ライブスピーチを他の言語のテキストに動的に翻訳することも可能です。この能力は主に技術的グローバリゼーションを促進しており、シーケンスからシーケンスへのニューラル機械翻訳の概念なしには実現できなかったでしょう。
最初のSeq2Seq(シーケンス・ツー・シーケンス)翻訳器は2014年にGoogleの研究者によって導入されました。その発明は翻訳の状況を根本的に変え、Google翻訳のような人気サービスはインターネットのニーズに応えるために非常に高い精度とアクセシビリティを持つようになりました。
このブログ記事では、Seq2Seq翻訳の理論とデザインを分解します。その後、最初に元のフレームワークを改善し、その後新しいデータセットに適応させる方法を示す、公式のPyTorchガイドに基づいた拡張バージョンを通じて進めていきます。
前提条件
-
自然言語処理(NLP)に関する基本的な理解:トークン化、埋め込み、シーケンスデータなどの基本的な概念に慣れていること。
再帰神経ネットワーク(RNN)の知識:RNNおよびその変種(例:LSTMやGRU)の理解。これらはシーケンス間タスクで一般的に使用されます。
PyTorchの理解度: テンソル、自動微分(autograd)、モデルのトレーニングなど、PyTorchに関する基本的な経験。
Pythonプログラミングスキル: PyTorchでSeq2Seqモデルを実装するために主に使用される言語であるPythonに精通していること。
注意メカニズムの理解:翻訳の品質を向上させるためにSeq2Seqモデルにしばしば組み込まれる注意メカニズムの基本的な理解。
Seq2Seq翻訳者の動作
Seq2Seq翻訳器は、ディープラーニングにおいて比較的単純な方法で機能します。このクラスのモデルの目的は、固定長の文字列入力を、固定長のペア文字列出力にマッピングすることです。この2つの長さは異なる場合があります。入力言語の文字列が8語で、ターゲット言語の同じ文が4語であれば、高品質な翻訳者はそれを推測し、出力の文の長さを短縮する必要があります。
デザイン理論
ソース
Seq2Seq翻訳者は通常、共通のフレームワークを共有しています。すべてのSeq2Seq翻訳者の3つの主要な構成要素は、エンコーダおよびデコーダネットワークと、それらの間の中間ベクトルエンコーディングです。これらのネットワークは多くの場合、再帰的ニューラルネットワーク(RNN)で構成されていますが、特にゲーテッドリカレントユニット(GRU)や長短期記憶(LSTM)を使用して作成されることがよくあります。これは、潜在的な消失勾配が翻訳に影響を与えるのを制限するためです。
エンコーダーネットワークは、これらのRNNユニットの一連で構成されています。これを使用して、入力からエンコーダーベクトルへの要素を順次エンコードし、最終的な隠れ状態が中間ベクトルに書き込まれます。
多くのNMTモデルは、注意の概念を活用してこの文脈エンコードを改善しています。注意とは、デコーダがエンコーダの出力の特定の部分に焦点を合わせるように重みのセットを使って強制することです。これらの注意重みは、エンコーダの出力ベクトルに掛け合わされます。これにより、デコーダが生成している出力の文脈を理解する能力を高める結合ベクトルエンコードが生成され、したがって予測を改善します。これらの注意重みの計算は、デコーダの入力と隠れた状態を入力として使用するフィードフォワード注意レイヤーを通して行われます。
エンコーダーベクトルには、エンコーダーからの入力の数値表現が含まれています。うまくいけば、初期入力文からのすべての情報をキャプチャします。このエンコーディングベクトルは、その後、デコーダーネットワークの初期隠れ状態として機能します。
デコーダネットワークは本質的にエンコーダの逆です。エンコードされたベクトルの中間状態を隠れ状態として取り込み、逐次的に翻訳を生成します。出力の各要素は、次の要素のデコーダの予測に情報を提供します。
実際には
ソース
実際には、NMTは1つの言語の入力文字列を取り、文中の各要素、すなわち単語を表す埋め込みのシーケンスを生成します。エンコーダのRNNユニットは、前の隠れ状態と元の入力埋め込みの単一要素を入力として取り、各ステップは前のステップの隠れ状態にアクセスして予測された要素を通知することにより、前のステップを順次改善できます。また、文をエンコードすることに加えて、シーケンス内の要素として文末タグ表現が含まれていることも重要です。この文末タグ付けは、翻訳する言語で翻訳者がどの単語がデコーダを停止させ、翻訳された文を出力するかを知るのに役立ちます。
最終的な隠れ状態の埋め込みは、中間エンコーダベクトルにエンコードされます。このエンコーディングは、入力文に関する可能な限り多くの情報をキャプチャし、デコーダがそれを翻訳にデコードするのを容易にします。これは、デコーダネットワークの初期の隠れ状態として使用されることによって可能です。
エンコーダーのベクトルからの情報を使用して、デコーダーの各再帰ユニットは前のユニットから隠れ状態を受け取り、出力と自分の隠れ状態を生成します。デコーダーは隠れ状態によってシーケンスに対する予測を行い、各順次予測ごとに前の隠れ状態からの情報を用いてシーケンスの次のインスタンスを予測します。最終的な出力は、翻訳された文の各要素の段階的予測の結果です。この文の長さは、文末タグのおかげで入力文の長さとは無関係であり、これによりデコーダーは文に項を追加するのを止めるべき時を知ります。
次のセクションでは、カスタム関数とPyTorchを使用して、各ステップをどのように実装できるかを示します。
Seq2Seq翻訳者の実装
PyTorchのウェブサイトには、ゼロからSeq2Seq翻訳者を作成するための素晴らしいチュートリアルがあります。この次のセクションでは、そこにあるコードの多くを適応していますので、これらの更新を実装する前に、彼らのチュートリアルノートブックを実行してみる価値があるかもしれません。
私たちはチュートリアルを二つの方法で拡張します:全く新しいデータセットの追加と、翻訳能力を最適化するための調整を行います。最初に、WMT2014の英語 - フランス語翻訳データセットを入手して準備する方法を示します。このデータセットは、Notebook内のSeq2Seqモデルで使用されます。コードの多くはPyTorchチュートリアルと同じなので、私たちはエンコーダーネットワーク、アテンションデコーダーネットワーク、およびトレーニングコードに焦点を当てます。
データの準備
WMT2014 Europarl v7 英語 - フランス語データセットの取得と準備
WMT2014 Europarl v7英語-フランス語データセットは、欧州議会で行われた演説のコレクションであり、いくつかの異なる言語に翻訳されています。これはhttps://www.statmt.org/europarl/で自由にアクセスできます。
wget https://www.statmt.org/europarl/v7/fr-en.tgz
tar -xf fre-en.tgz
Torchが提供するチュートリアルデータセットもダウンロードしたいと思うでしょう。
wget https://download.pytorch.org/tutorial/data.zip
unzip data.zip
データセットを手に入れたら、Machine Learning Masteryのジェイソン・ブラウンリーが作成したコードを使用して、それらを迅速に準備し、NMTのために結合することができます。このコードはノートブックdata_processing.ipynbにあります。
load doc into memory
def load_doc(filename):
# open the file as read only
file = open(filename, mode='rt', encoding='utf-8')
# read all text
text = file.read()
# close the file
file.close()
return text
split a loaded document into sentences
def to_sentences(doc):
return doc.strip().split('\n')
clean a list of lines
def clean_lines(lines):
cleaned = list()
# prepare regex for char filtering
re_print = re.compile('[^%s]' % re.escape(string.printable))
# prepare translation table for removing punctuation
table = str.maketrans('', '', string.punctuation)
for line in lines:
# normalize unicode characters
line = normalize('NFD', line).encode('ascii', 'ignore')
line = line.decode('UTF-8')
# tokenize on white space
line = line.split()
# convert to lower case
line = [word.lower() for word in line]
# remove punctuation from each token
line = [word.translate(table) for word in line]
# remove non-printable chars form each token
line = [re_print.sub('', w) for w in line]
# remove tokens with numbers in them
line = [word for word in line if word.isalpha()]
# store as string
cleaned.append(' '.join(line))
return cleaned
save a list of clean sentences to file
def save_clean_sentences(sentences, filename):
dump(sentences, open(filename, 'wb'))
print('Saved: %s' % filename)
load English data
filename = 'europarl-v7.fr-en.en'
doc = load_doc(filename)
sentences = to_sentences(doc)
sentences = clean_lines(sentences)
save_clean_sentences(sentences, 'english.pkl')
spot check
for i in range(10):
print(sentences[i])
load French data
filename = 'europarl-v7.fr-en.fr'
doc = load_doc(filename)
sentences = to_sentences(doc)
sentences = clean_lines(sentences)
save_clean_sentences(sentences, 'french.pkl')
spot check
for i in range(1):
print(sentences[i])
これは私たちのWMT2014データセットを取得し、句読点、大文字、非印刷可能文字、数字を含むトークンを削除します。それから、後で使用するためにファイルをピクルします。
with open('french.pkl', 'rb') as f:
fr_voc = pickle.load(f)
with open('english.pkl', 'rb') as f:
eng_voc = pickle.load(f)
data = pd.DataFrame(zip(eng_voc, fr_voc), columns = ['English', 'French'])
data
私たちはpickle.load()を使用して保存されたファイルを読み込み、その後便利なPandas DataFrameを使用して2つのファイルを結合できます。
私たちの二つのデータセットを組み合わせる
翻訳者のためにより完全なデータセットを作成するために、今持っている2つを組み合わせましょう。
data2 = pd.read_csv('eng-fra.txt', '\t', names = ['English', 'French'])
私たちは、標準的なPyTorchチュートリアルからオリジナルのデータセットをロードする必要があります。2つのデータフレームを使って、これらを連結し、PyTorchのサンプルデータセットで使用されている元の形式に戻して保存することができます。
data = pd.concat([data,data2], ignore_index= True, axis = 0)
data.to_csv('eng-fra.txt')
さて、私たちのデータセットは、標準的なPyTorchのチュートリアルのように私たちのコードに適用できます!しかし、その前に、データセットを準備するためのステップを見て、どのような改善ができるかを見てみましょう。ノートブックseq2seq_translation_combo.ipynbを開いて最初のセルを実行し、matplotlibのインラインが機能していることとインポートが完了していることを確認してください。
from __future__ import unicode_literals, print_function, division
from io import open
import unicodedata
import string
import re
import random
import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F
import torchtext
from torchtext.data import get_tokenizer
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
データセット準備ヘルパー関数
SOS_token = 0
EOS_token = 1
class Lang:
def __init__(self, name):
self.name = name
self.word2index = {}
self.word2count = {}
self.index2word = {0: "SOS", 1: "EOS"}
self.n_words = 2 # Count SOS and EOS
def addSentence(self, sentence):
for word in sentence.split(' '):
self.addWord(word)
def addWord(self, word):
if word not in self.word2index:
self.word2index[word] = self.n_words
self.word2count[word] = 1
self.index2word[self.n_words] = word
self.n_words += 1
else:
self.word2count[word] += 1
翻訳者のために私たちのデータセットを処理するために、このLangクラスを使用して、言語クラスに役立つ機能を提供することができます。例えば、word2index、index2word、およびword2countです。次のセルには、元のデータセットをクリーンアップするための便利な関数が含まれています。
def readLangs(lang1, lang2, reverse=False):
print("Reading lines...")
# Read the file and split into lines
lines = open('%s-%s2.txt' % (lang1, lang2), encoding='utf-8').\
read().strip().split('\n')
# Split every line into pairs and normalize
pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]
# Reverse pairs, make Lang instances
if reverse:
pairs = [list(reversed(p)) for p in pairs]
input_lang = Lang(lang2)
output_lang = Lang(lang1)
else:
input_lang = Lang(lang1)
output_lang = Lang(lang2)
return input_lang, output_lang, pairs
次に、readLangs関数は私たちのcsvを取り込み、データセットを準備するために使用する
MAX_LENGTH = 12
eng_prefixes = [
"i am ", "i m ",
"he is", "he s ",
"she is", "she s ",
"you are", "you re ",
"we are", "we re ",
"they are", "they re ", "I don t", "Do you", "I want", "Are you", "I have", "I think",
"I can t", "I was", "He is", "I m not", "This is", "I just", "I didn t",
"I am", "I thought", "I know", "Tom is", "I had", "Did you", "Have you",
"Can you", "He was", "You don t", "I d like", "It was", "You should",
"Would you", "I like", "It is", "She is", "You can t", "He has",
"What do", "If you", "I need", "No one", "You are", "You have",
"I feel", "I really", "Why don t", "I hope", "I will", "We have",
"You re not", "You re very", "She was", "I love", "You must", "I can"]
eng_prefixes = (map(lambda x: x.lower(), eng_prefixes))
eng_prefixes = set(eng_prefixes)
def filterPair(p):
return len(p[0].split(' ')) < MAX_LENGTH and \
len(p[1].split(' ')) < MAX_LENGTH and \
p[1].startswith(eng_prefixes)
def filterPairs(pairs):
return [pair for pair in pairs if filterPair(pair)]
eng_prefixes
Torchチュートリアルからの別の変更として、現在統合されたデータセットの最も一般的な接頭辞を含むように英語の接頭辞のリストを拡張しました。また、データポイントのより堅牢なセットを作成するためにmax_lengthを12に拡張しましたが、これは有用な要因と同じくらいの混乱を引き起こす可能性があります。max_lengthを10に戻して、パフォーマンスがどのように変化するかを試してみてください。
def prepareData(lang1, lang2,reverse = False):
input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
print("Read %s sentence pairs" % len(pairs))
pairs = filterPairs(pairs)
print("Trimmed to %s sentence pairs" % len(pairs))
print("Counting words...")
for pair in pairs:
input_lang.addSentence(pair[0])
output_lang.addSentence(pair[1])
print("Counted words:")
print(input_lang.name, input_lang.n_words)
print(output_lang.name, output_lang.n_words)
return input_lang, output_lang, pairs
input_lang, output_lang, pairs = prepareData('eng', 'fra', True)
print(random.choice(pairs))
最後に、prepareData関数はすべてのヘルパー関数をまとめて、NMTトレーニング用の言語ペアをフィルタリングして最終化します。データセットの準備が整ったので、翻訳者自体のコードに進みましょう。
翻訳者
エンコーダー
class EncoderRNN(nn.Module):
def __init__(self, input_size, hidden_size):
super(EncoderRNN, self).__init__()
self.hidden_size = hidden_size
self.embedding = nn.Embedding(input_size, hidden_size)
self.gru = nn.GRU(hidden_size, hidden_size)
def forward(self, input, hidden):
embedded = self.embedding(input).view(1, 1, -1)
output = embedded
output, hidden = self.gru(output, hidden)
return output, hidden
def initHidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
私たちが使用しているエンコーダは本質的にチュートリアルと同じであり、この記事で解析するコードの中で最もシンプルな部分である可能性が高いです。フォワード関数から、各入力要素に対してエンコーダが出力ベクトルと隠れ状態を両方出力する様子がわかります。その隠れ状態はその後返され、出力とともに次のステップで使用できるようになります。
アテンションデコーダー
class AttnDecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
super(AttnDecoderRNN, self).__init__()
self.hidden_size = hidden_size
self.output_size = output_size
self.dropout_p = dropout_p
self.max_length = max_length
self.embedding = nn.Embedding(self.output_size, self.hidden_size)
self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
self.dropout = nn.Dropout(self.dropout_p)
self.gru = nn.GRU(self.hidden_size, self.hidden_size)
self.out = nn.Linear(self.hidden_size, self.output_size)
def forward(self, input, hidden, encoder_outputs):
embedded = self.embedding(input).view(1, 1, -1)
embedded = self.dropout(embedded)
attn_weights = F.softmax(
self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
attn_applied = torch.bmm(attn_weights.unsqueeze(0),
encoder_outputs.unsqueeze(0))
output = torch.cat((embedded[0], attn_applied[0]), 1)
output = self.attn_combine(output).unsqueeze(0)
output = F.relu(output)
output, hidden = self.gru(output, hidden)
output = F.log_softmax(self.out(output[0]), dim=1)
return output, hidden, attn_weights
def initHidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
この例ではアテンションを使用しているので、デコーダーでどのようにそれを実装できるかを見てみましょう。すぐに明らかなように、エンコーダーとデコーダーネットワークの間には、単純にその挙動の反転以上のいくつかの重要な違いがあります。
まず、init()関数には追加の2つのパラメータがあります:max_lengthとdropout_pです。max_lengthは、文が考慮されるために保持できる最大要素数です。この設定は、2つのペアデータセットの文の長さの大きな変動に基づいています。dropout_pは、正則化を助け、ニューロンの共同適応を防ぐために使用されます。
次に、注意層そのものがあります。各ステップで、注意層は注意の入力、デコーダーの状態、およびすべてのエンコーダーの状態を受け取ります。これを使用して注意スコアを計算します。各エンコーダーの状態に対して、注意はこのデコーダーの状態に対する「関連性」を計算します。注意関数を適用し、1つのデコーダーの状態と1つのエンコーダーの状態を受け取り、スカラー値を返します。注意スコアは注意重みを計算するために使用されます。これらの重みは、注意スコアにソフトマックスを適用して作成された確率分布です。最後に、注意出力は、注意重みで重み付けされたエンコーダーの状態の合計として計算されます。 (1)
これらの追加のパラメータと注意機構により、デコーダはシーケンス内のすべての単語の関係を理解するために、はるかに少ないトレーニングと全体的な情報を必要とすることができます。
トレーニング
teacher_forcing_ratio = 0.5
def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
encoder_hidden = encoder.initHidden()
encoder_optimizer.zero_grad()
decoder_optimizer.zero_grad()
input_length = input_tensor.size(0)
target_length = target_tensor.size(0)
encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
loss = 0
for ei in range(input_length):
encoder_output, encoder_hidden = encoder(
input_tensor[ei], encoder_hidden)
encoder_outputs[ei] = encoder_output[0, 0]
decoder_input = torch.tensor([[SOS_token]], device=device)
decoder_hidden = encoder_hidden
use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
if use_teacher_forcing:
# Teacher forcing: Feed the target as the next input
for di in range(target_length):
decoder_output, decoder_hidden, decoder_attention = decoder(
decoder_input, decoder_hidden, encoder_outputs)
loss += criterion(decoder_output, target_tensor[di])
decoder_input = target_tensor[di] # Teacher forcing
else:
# Without teacher forcing: use its own predictions as the next input
for di in range(target_length):
decoder_output, decoder_hidden, decoder_attention = decoder(
decoder_input, decoder_hidden, encoder_outputs)
topv, topi = decoder_output.topk(1)
decoder_input = topi.squeeze().detach() # detach from history as input
loss += criterion(decoder_output, target_tensor[di])
if decoder_input.item() == EOS_token:
break
loss.backward()
encoder_optimizer.step()
decoder_optimizer.step()
return loss.item() / target_length
私たちが使用しているトレイン関数は、いくつかのパラメータを受け取ります。input_tensor と target_tensor は、それぞれ文ペアの0番目と1番目のインデックスです。エンコーダは上で説明したエンコーダです。デコーダは上で説明した注意デコーダです。私たちは、エンコーダとデコーダのオプティマイザーを確率的勾配降下法(Stochastic Gradient Descent)からAdagradに変更しています。Adagradを使用した場合、翻訳の損失が低くなることがわかりました。最後に、基準としてクロスエントロピー損失(Cross Entropy Loss)を使用しており、これはnn.NLLLoss()を使用したチュートリアルとは異なります。
教師強制比率も確認すべきです。この値は0.5に設定されており、モデルの効果を向上させるために使用されます。0.5では、次の入力としてターゲットをデコーダにフィードするか、デコーダ自身の予測を使用するかをランダムに決定します。これにより翻訳の収束が早くなるかもしれませんが、不安定性を引き起こす可能性もあります。たとえば、教師強制を過度に使用すると、正確な文法を持つ出力を生成するモデルが作成される可能性がありますが、入力との翻訳的関係は無くなるかもしれません。
def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100):
start = time.time()
plot_losses = []
print_loss_total = 0 # Reset every print_every
plot_loss_total = 0 # Reset every plot_every
encoder_optimizer = optim.Adagrad(encoder.parameters())
decoder_optimizer = optim.Adagrad(decoder.parameters())
training_pairs = [tensorsFromPair(random.choice(pairs))
for i in range(n_iters)]
criterion = nn.CrossEntropyLoss()
for iter in range(1, n_iters + 1):
training_pair = training_pairs[iter - 1]
input_tensor = training_pair[0]
target_tensor = training_pair[1]
loss = train(input_tensor, target_tensor, encoder,
decoder, encoder_optimizer, decoder_optimizer, criterion)
print_loss_total += loss
plot_loss_total += loss
if iter % print_every == 0:
print_loss_avg = print_loss_total / print_every
print_loss_total = 0
print('%s (%d %d%%) %.4f' % (timeSince(start, iter / n_iters),
iter, iter / n_iters * 100, print_loss_avg))
if iter % plot_every == 0:
plot_loss_avg = plot_loss_total / plot_every
plot_losses.append(plot_loss_avg)
plot_loss_total = 0
showPlot(plot_losses)
TrainIters は実際にトレーニングプロセスを実装しています。指定された反復回数に対して、損失を計算します。この関数は損失値を保存し、トレーニングが完了した後に有用にプロットできるようにします。
翻訳
hidden_size = 256
encoder = EncoderRNN(input_lang.n_words, hidden_size).to(device)
attn_decoder = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1).to(device)
trainIters(encoder1, attn_decoder1, 75000, print_every=5000)
翻訳者を設定したので、必要なのはエンコーダーとアテンションデコーダーモデルをインスタンス化してトレーニングを行い、trainIters関数を実行することだけです。トレーニングセルの前にあるノートブック内のすべてのセルを実行して、ヘルパー関数を有効にすることを確認してください。
隠れ層のサイズを256に設定し、デバイスをdevice(type='cuda')に設定していることを確認します。これにより、RNNがGPUを使用してトレーニングされることが保証されます。
このセルを実行すると、あなたのモデルは75,000回のイテレーションでトレーニングされます。トレーニングが完了したら、提供された評価関数を使用して、新しい翻訳モデルの定性的なパフォーマンスを評価してください。ここには、デモ用にトレーニングしたモデルがランダムにサンプリングした翻訳でどのようにパフォーマンスを発揮したかの例があります。
締めくくりの考え
今は適切な翻訳データセットを取り、それをこの翻訳コードに組み込むことができるようになったはずです。他のWMT Europarlのペアリングのいずれかから始めることをお勧めしますが、無限の選択肢があります。



