PyTorch 1.3 Tutorials : テキスト : TorchText で言語翻訳 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 12/29/2019 (1.3.1)
* 本ページは、PyTorch 1.3 Tutorials の以下のページを翻訳した上で適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
テキスト : TorchText で言語翻訳
このチュートリアルは、英語とドイツ語の両者のセンテンスを含む良く知られたデータセットからのデータを前処理するためにtorchtext の幾つかの便利なクラスをどのように使用するか、そしてドイツ語センテンスを英語に翻訳できる attention つきの sequence-to-sequence モデルを訓練するためにどのように使用するかを示します。
It is based off of this tutorial from PyTorch community member Ben Trevett and was created by Seth Weidman with Ben’s permission.
このチュートリアルの最後までで、貴方は次ができるでしょう :
- 次の torchtext の便利なクラスを使用してセンテンスを NLP モデリングのために一般に使用される形式に前処理します :
- TranslationDataset
- Field
- BucketIterator
Field と TranslationDataset
torchtext は言語翻訳モデルを作成する目的のために容易に統合できるデータセットを作成するためのユティリティを持ちます。一つの主要なクラスは Field で、これは各センテンスが前処理されるべき方法を指定します、そしてもう一つは TranslationDataset です ; torchtext は幾つかのそのようなデータセットを持ちます ; このチュートリアルでは Multi30k データセット を使用します、これは英語とドイツ語で約 30,000 センテンス (平均して長さおよそ 13 単語) を含みます。
Note: このチュートリアルでのトークン化は Spacy を必要とします。Spacy を使用するのはそれが英語以外の言語でのトークン化のための強力なサポートを提供するからです。torchtext は basic_english tokenzier を提供し英語のための他の tokenzier (e.g. Moses (訳注: リンク切れ)) も提供しますが言語翻訳のためには – そこではマルチ言語が必要です – Spacy が最善の方法です。
このチュートリアルを実行するためには、最初に pip か conda を使用して spacy をインストールします。次に、英語とドイツ語 Spacy tokenizer のための生データをダウンロードします :
python -m spacy download en python -m spacy download de
Spacy がインストールされていれば、次のコードは Field で定義された tokenizer に基づいて TranslationDataset のセンテンスの各々をトークン化します :
from torchtext.datasets import Multi30k from torchtext.data import Field, BucketIterator SRC = Field(tokenize = "spacy", tokenizer_language="de", init_token = '', eos_token = ' ', lower = True) TRG = Field(tokenize = "spacy", tokenizer_language="en", init_token = ' ', eos_token = ' ', lower = True) train_data, valid_data, test_data = Multi30k.splits(exts = ('.de', '.en'), fields = (SRC, TRG))
downloading training.tar.gz downloading validation.tar.gz downloading mmt_task1_test2016.tar.gz
train_data を定義した今、torchtext の Field の非常に有用な特徴を見ることができます : 今では build_vocab は各言語に関連する語彙を作成することを可能にします。
SRC.build_vocab(train_data, min_freq = 2) TRG.build_vocab(train_data, min_freq = 2)
コードのこれらの行がひとたび実行されれば、SRC.vocab.stoi はキーとして語彙のトークンを持ち値として対応するインデックスを持つ辞書になります ;SRC.vocab.itos はキーと値が交換された同じ辞書になります。このチュートリアルではこの事実を広く利用はしませんが、これは貴方が出会う他の NLP タスクで有用でありがちでしょう。
BucketIterator
使用する最後の torchtext 固有の特徴は BucketIterator で、これは簡単に利用できます、それは最初の引数として TranslationDataset を取るからです。特に doc が言うように: 同様の長さのサンプルを一緒にバッチ化する iterator を定義します。各新しいエポックのために新たにシャッフルされたバッチを生成しながら必要なパディングの総量を最小化します。使用される bucketing 手続きについてはプールを見てください。
import torch device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') BATCH_SIZE = 128 train_iterator, valid_iterator, test_iterator = BucketIterator.splits( (train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device)
これらの iterator は単に DataLoader のように呼び出せます ; 下で、train と evaluate 関数で、それらは単純に以下で呼び出されます :
for i, batch in enumerate(iterator):
それから各バッチは src と trg 属性を持ちます :
src = batch.src trg = batch.trg
nn.Module と Optimizer を定義する
torchtext 視点からはそれが殆ど総てです : データセットが構築されて iterator が定義され、このチュートリアルの残りは Optimizer と一緒に、単純にモデルを nn.Module として定義してから、それを訓練します。
私達のモデルは特に、ここ で記述されているアーキテクチャに従います (ここ で著しく多くコメントされたバージョンを見つけられます)。
Note: このモデルは言語翻訳のために使用できる単なるサンプル・モデルです ; それを選択したのはそれがタスクのための標準的なモデルであるためで、翻訳のために使用する推奨モデルであるからではありません。貴方が気付いているかもしれないように、最先端モデルは現在は Transformer に基づいています ; Transformer 層を実装するための PyTorch の機能は ここ で見ることができます ; そして特に、下のモデルで使用される “attention” は transformer モデルで現れる multi-headed self-attention とは異なります。
import random from typing import Tuple import torch.nn as nn import torch.optim as optim import torch.nn.functional as F from torch import Tensor class Encoder(nn.Module): def __init__(self, input_dim: int, emb_dim: int, enc_hid_dim: int, dec_hid_dim: int, dropout: float): super().__init__() self.input_dim = input_dim self.emb_dim = emb_dim self.enc_hid_dim = enc_hid_dim self.dec_hid_dim = dec_hid_dim self.dropout = dropout self.embedding = nn.Embedding(input_dim, emb_dim) self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional = True) self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim) self.dropout = nn.Dropout(dropout) def forward(self, src: Tensor) -> Tuple[Tensor]: embedded = self.dropout(self.embedding(src)) outputs, hidden = self.rnn(embedded) hidden = torch.tanh(self.fc(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1))) return outputs, hidden class Attention(nn.Module): def __init__(self, enc_hid_dim: int, dec_hid_dim: int, attn_dim: int): super().__init__() self.enc_hid_dim = enc_hid_dim self.dec_hid_dim = dec_hid_dim self.attn_in = (enc_hid_dim * 2) + dec_hid_dim self.attn = nn.Linear(self.attn_in, attn_dim) def forward(self, decoder_hidden: Tensor, encoder_outputs: Tensor) -> Tensor: src_len = encoder_outputs.shape[0] repeated_decoder_hidden = decoder_hidden.unsqueeze(1).repeat(1, src_len, 1) encoder_outputs = encoder_outputs.permute(1, 0, 2) energy = torch.tanh(self.attn(torch.cat(( repeated_decoder_hidden, encoder_outputs), dim = 2))) attention = torch.sum(energy, dim=2) return F.softmax(attention, dim=1) class Decoder(nn.Module): def __init__(self, output_dim: int, emb_dim: int, enc_hid_dim: int, dec_hid_dim: int, dropout: int, attention: nn.Module): super().__init__() self.emb_dim = emb_dim self.enc_hid_dim = enc_hid_dim self.dec_hid_dim = dec_hid_dim self.output_dim = output_dim self.dropout = dropout self.attention = attention self.embedding = nn.Embedding(output_dim, emb_dim) self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim) self.out = nn.Linear(self.attention.attn_in + emb_dim, output_dim) self.dropout = nn.Dropout(dropout) def _weighted_encoder_rep(self, decoder_hidden: Tensor, encoder_outputs: Tensor) -> Tensor: a = self.attention(decoder_hidden, encoder_outputs) a = a.unsqueeze(1) encoder_outputs = encoder_outputs.permute(1, 0, 2) weighted_encoder_rep = torch.bmm(a, encoder_outputs) weighted_encoder_rep = weighted_encoder_rep.permute(1, 0, 2) return weighted_encoder_rep def forward(self, input: Tensor, decoder_hidden: Tensor, encoder_outputs: Tensor) -> Tuple[Tensor]: input = input.unsqueeze(0) embedded = self.dropout(self.embedding(input)) weighted_encoder_rep = self._weighted_encoder_rep(decoder_hidden, encoder_outputs) rnn_input = torch.cat((embedded, weighted_encoder_rep), dim = 2) output, decoder_hidden = self.rnn(rnn_input, decoder_hidden.unsqueeze(0)) embedded = embedded.squeeze(0) output = output.squeeze(0) weighted_encoder_rep = weighted_encoder_rep.squeeze(0) output = self.out(torch.cat((output, weighted_encoder_rep, embedded), dim = 1)) return output, decoder_hidden.squeeze(0) class Seq2Seq(nn.Module): def __init__(self, encoder: nn.Module, decoder: nn.Module, device: torch.device): super().__init__() self.encoder = encoder self.decoder = decoder self.device = device def forward(self, src: Tensor, trg: Tensor, teacher_forcing_ratio: float = 0.5) -> Tensor: batch_size = src.shape[1] max_len = trg.shape[0] trg_vocab_size = self.decoder.output_dim outputs = torch.zeros(max_len, batch_size, trg_vocab_size).to(self.device) encoder_outputs, hidden = self.encoder(src) # first input to the decoder is thetoken output = trg[0,:] for t in range(1, max_len): output, hidden = self.decoder(output, hidden, encoder_outputs) outputs[t] = output teacher_force = random.random() < teacher_forcing_ratio top1 = output.max(1)[1] output = (trg[t] if teacher_force else top1) return outputs INPUT_DIM = len(SRC.vocab) OUTPUT_DIM = len(TRG.vocab) # ENC_EMB_DIM = 256 # DEC_EMB_DIM = 256 # ENC_HID_DIM = 512 # DEC_HID_DIM = 512 # ATTN_DIM = 64 # ENC_DROPOUT = 0.5 # DEC_DROPOUT = 0.5 ENC_EMB_DIM = 32 DEC_EMB_DIM = 32 ENC_HID_DIM = 64 DEC_HID_DIM = 64 ATTN_DIM = 8 ENC_DROPOUT = 0.5 DEC_DROPOUT = 0.5 enc = Encoder(INPUT_DIM, ENC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, ENC_DROPOUT) attn = Attention(ENC_HID_DIM, DEC_HID_DIM, ATTN_DIM) dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, DEC_DROPOUT, attn) model = Seq2Seq(enc, dec, device).to(device) def init_weights(m: nn.Module): for name, param in m.named_parameters(): if 'weight' in name: nn.init.normal_(param.data, mean=0, std=0.01) else: nn.init.constant_(param.data, 0) model.apply(init_weights) optimizer = optim.Adam(model.parameters()) def count_parameters(model: nn.Module): return sum(p.numel() for p in model.parameters() if p.requires_grad) print(f'The model has {count_parameters(model):,} trainable parameters')
The model has 1,856,685 trainable parameters
Note: 特に言語翻訳モデルのパフォーマンスをスコアするとき、nn.CrossEntropyLoss 関数にターゲットが単純にパディングされているインデックスは無視するように知らせなければなりません。
PAD_IDX = TRG.vocab.stoi['<pad>'] criterion = nn.CrossEntropyLoss(ignore_index=PAD_IDX)
最後に、このモデルを訓練して評価できます :
import math import time def train(model: nn.Module, iterator: BucketIterator, optimizer: optim.Optimizer, criterion: nn.Module, clip: float): model.train() epoch_loss = 0 for _, batch in enumerate(iterator): src = batch.src trg = batch.trg optimizer.zero_grad() output = model(src, trg) output = output[1:].view(-1, output.shape[-1]) trg = trg[1:].view(-1) loss = criterion(output, trg) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), clip) optimizer.step() epoch_loss += loss.item() return epoch_loss / len(iterator) def evaluate(model: nn.Module, iterator: BucketIterator, criterion: nn.Module): model.eval() epoch_loss = 0 with torch.no_grad(): for _, batch in enumerate(iterator): src = batch.src trg = batch.trg output = model(src, trg, 0) #turn off teacher forcing output = output[1:].view(-1, output.shape[-1]) trg = trg[1:].view(-1) loss = criterion(output, trg) epoch_loss += loss.item() return epoch_loss / len(iterator) def epoch_time(start_time: int, end_time: int): elapsed_time = end_time - start_time elapsed_mins = int(elapsed_time / 60) elapsed_secs = int(elapsed_time - (elapsed_mins * 60)) return elapsed_mins, elapsed_secs N_EPOCHS = 10 CLIP = 1 best_valid_loss = float('inf') for epoch in range(N_EPOCHS): start_time = time.time() train_loss = train(model, train_iterator, optimizer, criterion, CLIP) valid_loss = evaluate(model, valid_iterator, criterion) end_time = time.time() epoch_mins, epoch_secs = epoch_time(start_time, end_time) print(f'Epoch: {epoch+1:02} | Time: {epoch_mins}m {epoch_secs}s') print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}') print(f'\t Val. Loss: {valid_loss:.3f} | Val. PPL: {math.exp(valid_loss):7.3f}') test_loss = evaluate(model, test_iterator, criterion) print(f'| Test Loss: {test_loss:.3f} | Test PPL: {math.exp(test_loss):7.3f} |')
Epoch: 01 | Time: 0m 37s Train Loss: 5.680 | Train PPL: 293.018 Val. Loss: 5.253 | Val. PPL: 191.191 Epoch: 02 | Time: 0m 36s Train Loss: 5.020 | Train PPL: 151.375 Val. Loss: 5.120 | Val. PPL: 167.352 Epoch: 03 | Time: 0m 36s Train Loss: 4.761 | Train PPL: 116.811 Val. Loss: 4.999 | Val. PPL: 148.239 Epoch: 04 | Time: 0m 36s Train Loss: 4.616 | Train PPL: 101.057 Val. Loss: 5.000 | Val. PPL: 148.418 Epoch: 05 | Time: 0m 35s Train Loss: 4.512 | Train PPL: 91.100 Val. Loss: 5.021 | Val. PPL: 151.506 Epoch: 06 | Time: 0m 38s Train Loss: 4.408 | Train PPL: 82.126 Val. Loss: 4.917 | Val. PPL: 136.566 Epoch: 07 | Time: 0m 38s Train Loss: 4.324 | Train PPL: 75.463 Val. Loss: 4.882 | Val. PPL: 131.843 Epoch: 08 | Time: 0m 36s Train Loss: 4.271 | Train PPL: 71.585 Val. Loss: 4.807 | Val. PPL: 122.374 Epoch: 09 | Time: 0m 36s Train Loss: 4.183 | Train PPL: 65.553 Val. Loss: 4.788 | Val. PPL: 120.005 Epoch: 10 | Time: 0m 36s Train Loss: 4.115 | Train PPL: 61.241 Val. Loss: 4.810 | Val. PPL: 122.776 | Test Loss: 4.865 | Test PPL: 129.624 |
Next steps
- ここ で torchtext を使用する Ben Trevett のチュートリアルの残りを確認してください。
- 次の単語予測を通した言語モデルのために他の torchtext 特徴を nn.Transformer と一緒に使用するチュートリアルに期待してください (= stay tuned)。
以上