演習 Sequence-to-Sequence (Seq2Seq) モデル

Sequence-to-Sequence (Seq2Seq) モデルは、系列を入力として系列を出力するモデルです。

入力系列をRNNで固定長のベクトルに変換(= Encode)し、そのベクトルを用いて系列を出力(= Decode)することから、Encoder-Decoder モデルとも呼ばれます。

RNNの代わりにLSTMやGRUでも可能です。

機械翻訳のほか、文書要約や対話生成にも使われます。
今回は機械翻訳を例にとって解説していきます。

In [8]:
! wget https://www.dropbox.com/s/9narw5x4uizmehh/utils.py
! mkdir images data

# data取得
! wget https://www.dropbox.com/s/o4kyc52a8we25wy/dev.en -P data/
! wget https://www.dropbox.com/s/kdgskm5hzg6znuc/dev.ja -P data/
! wget https://www.dropbox.com/s/gyyx4gohv9v65uh/test.en -P data/
! wget https://www.dropbox.com/s/hotxwbgoe2n013k/test.ja -P data/
! wget https://www.dropbox.com/s/5lsftkmb20ay9e1/train.en -P data/
! wget https://www.dropbox.com/s/ak53qirssci6f1j/train.ja -P data/
--2020-12-17 06:17:04--  https://www.dropbox.com/s/9narw5x4uizmehh/utils.py
Resolving www.dropbox.com (www.dropbox.com)... 162.125.6.18, 2620:100:6019:18::a27d:412
Connecting to www.dropbox.com (www.dropbox.com)|162.125.6.18|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: /s/raw/9narw5x4uizmehh/utils.py [following]
--2020-12-17 06:17:04--  https://www.dropbox.com/s/raw/9narw5x4uizmehh/utils.py
Reusing existing connection to www.dropbox.com:443.
HTTP request sent, awaiting response... 302 Found
Location: https://uc69d0fdc8bee50f3d5f18d22c67.dl.dropboxusercontent.com/cd/0/inline/BFMkaNYPXfhDLb65C3InllFpSvfu9Lf6vNIsQWFDssYaAu45elv9hQJjF_W5n957QA_sfb-k34kFzv2TTGHuAc16O458IBBalR5dPp5CxKtZQA/file# [following]
--2020-12-17 06:17:04--  https://uc69d0fdc8bee50f3d5f18d22c67.dl.dropboxusercontent.com/cd/0/inline/BFMkaNYPXfhDLb65C3InllFpSvfu9Lf6vNIsQWFDssYaAu45elv9hQJjF_W5n957QA_sfb-k34kFzv2TTGHuAc16O458IBBalR5dPp5CxKtZQA/file
Resolving uc69d0fdc8bee50f3d5f18d22c67.dl.dropboxusercontent.com (uc69d0fdc8bee50f3d5f18d22c67.dl.dropboxusercontent.com)... 162.125.6.15, 2620:100:6019:15::a27d:40f
Connecting to uc69d0fdc8bee50f3d5f18d22c67.dl.dropboxusercontent.com (uc69d0fdc8bee50f3d5f18d22c67.dl.dropboxusercontent.com)|162.125.6.15|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 949 [text/plain]
Saving to: ‘utils.py.1’

utils.py.1          100%[===================>]     949  --.-KB/s    in 0s      

2020-12-17 06:17:04 (134 MB/s) - ‘utils.py.1’ saved [949/949]

mkdir: cannot create directory ‘images’: File exists
mkdir: cannot create directory ‘data’: File exists
--2020-12-17 06:17:05--  https://www.dropbox.com/s/o4kyc52a8we25wy/dev.en
Resolving www.dropbox.com (www.dropbox.com)... 162.125.6.18, 2620:100:6019:18::a27d:412
Connecting to www.dropbox.com (www.dropbox.com)|162.125.6.18|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: /s/raw/o4kyc52a8we25wy/dev.en [following]
--2020-12-17 06:17:05--  https://www.dropbox.com/s/raw/o4kyc52a8we25wy/dev.en
Reusing existing connection to www.dropbox.com:443.
HTTP request sent, awaiting response... 302 Found
Location: https://ucc7f7b6d7bba92a9259a0c51e3c.dl.dropboxusercontent.com/cd/0/inline/BFP0ZB0NP_YCGkVdMNahG2Azljv-5b0xKYXCLcywkTg-B7pRwQDgOve5BgnuWuUfFJ0gCeXZW7cB3xhQbklppb134Q5eSxag8Wb4tTwUrCQAOA/file# [following]
--2020-12-17 06:17:05--  https://ucc7f7b6d7bba92a9259a0c51e3c.dl.dropboxusercontent.com/cd/0/inline/BFP0ZB0NP_YCGkVdMNahG2Azljv-5b0xKYXCLcywkTg-B7pRwQDgOve5BgnuWuUfFJ0gCeXZW7cB3xhQbklppb134Q5eSxag8Wb4tTwUrCQAOA/file
Resolving ucc7f7b6d7bba92a9259a0c51e3c.dl.dropboxusercontent.com (ucc7f7b6d7bba92a9259a0c51e3c.dl.dropboxusercontent.com)... 162.125.6.15, 2620:100:6019:15::a27d:40f
Connecting to ucc7f7b6d7bba92a9259a0c51e3c.dl.dropboxusercontent.com (ucc7f7b6d7bba92a9259a0c51e3c.dl.dropboxusercontent.com)|162.125.6.15|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 17054 (17K) [text/plain]
Saving to: ‘data/dev.en.1’

dev.en.1            100%[===================>]  16.65K  --.-KB/s    in 0s      

2020-12-17 06:17:05 (253 MB/s) - ‘data/dev.en.1’ saved [17054/17054]

--2020-12-17 06:17:06--  https://www.dropbox.com/s/kdgskm5hzg6znuc/dev.ja
Resolving www.dropbox.com (www.dropbox.com)... 162.125.6.18, 2620:100:6019:18::a27d:412
Connecting to www.dropbox.com (www.dropbox.com)|162.125.6.18|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: /s/raw/kdgskm5hzg6znuc/dev.ja [following]
--2020-12-17 06:17:06--  https://www.dropbox.com/s/raw/kdgskm5hzg6znuc/dev.ja
Reusing existing connection to www.dropbox.com:443.
HTTP request sent, awaiting response... 302 Found
Location: https://uc737e78c13869601c281af97fff.dl.dropboxusercontent.com/cd/0/inline/BFOgY3xIl9nedkqXQHvFHwGkr2A--vSflRpTC3vycBB_cXZIbkb-HSBId2I_KuhPdW0qHn4O2s50kBCgpwhX-uaNHaIvWfHzi4xovtCaIVZeYA/file# [following]
--2020-12-17 06:17:06--  https://uc737e78c13869601c281af97fff.dl.dropboxusercontent.com/cd/0/inline/BFOgY3xIl9nedkqXQHvFHwGkr2A--vSflRpTC3vycBB_cXZIbkb-HSBId2I_KuhPdW0qHn4O2s50kBCgpwhX-uaNHaIvWfHzi4xovtCaIVZeYA/file
Resolving uc737e78c13869601c281af97fff.dl.dropboxusercontent.com (uc737e78c13869601c281af97fff.dl.dropboxusercontent.com)... 162.125.6.15, 2620:100:6019:15::a27d:40f
Connecting to uc737e78c13869601c281af97fff.dl.dropboxusercontent.com (uc737e78c13869601c281af97fff.dl.dropboxusercontent.com)|162.125.6.15|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 27781 (27K) [text/plain]
Saving to: ‘data/dev.ja.1’

dev.ja.1            100%[===================>]  27.13K  --.-KB/s    in 0.001s  

2020-12-17 06:17:06 (26.6 MB/s) - ‘data/dev.ja.1’ saved [27781/27781]

--2020-12-17 06:17:06--  https://www.dropbox.com/s/gyyx4gohv9v65uh/test.en
Resolving www.dropbox.com (www.dropbox.com)... 162.125.6.18, 2620:100:6019:18::a27d:412
Connecting to www.dropbox.com (www.dropbox.com)|162.125.6.18|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: /s/raw/gyyx4gohv9v65uh/test.en [following]
--2020-12-17 06:17:07--  https://www.dropbox.com/s/raw/gyyx4gohv9v65uh/test.en
Reusing existing connection to www.dropbox.com:443.
HTTP request sent, awaiting response... 302 Found
Location: https://uc25e50314b204c1653bebe0c84d.dl.dropboxusercontent.com/cd/0/inline/BFPNNoyBZM0Ud6ayhLzpJqCSyOXa0q9paLldtcJzU9icogiMHYqcWwQWdBl50fe2Y7Mj7hQHb63tylh02hXEX833MN0EiIJO3G_NvnupZvFEFA/file# [following]
--2020-12-17 06:17:07--  https://uc25e50314b204c1653bebe0c84d.dl.dropboxusercontent.com/cd/0/inline/BFPNNoyBZM0Ud6ayhLzpJqCSyOXa0q9paLldtcJzU9icogiMHYqcWwQWdBl50fe2Y7Mj7hQHb63tylh02hXEX833MN0EiIJO3G_NvnupZvFEFA/file
Resolving uc25e50314b204c1653bebe0c84d.dl.dropboxusercontent.com (uc25e50314b204c1653bebe0c84d.dl.dropboxusercontent.com)... 162.125.6.15, 2620:100:6019:15::a27d:40f
Connecting to uc25e50314b204c1653bebe0c84d.dl.dropboxusercontent.com (uc25e50314b204c1653bebe0c84d.dl.dropboxusercontent.com)|162.125.6.15|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 17301 (17K) [text/plain]
Saving to: ‘data/test.en.1’

test.en.1           100%[===================>]  16.90K  --.-KB/s    in 0s      

2020-12-17 06:17:08 (255 MB/s) - ‘data/test.en.1’ saved [17301/17301]

--2020-12-17 06:17:08--  https://www.dropbox.com/s/hotxwbgoe2n013k/test.ja
Resolving www.dropbox.com (www.dropbox.com)... 162.125.6.18, 2620:100:6019:18::a27d:412
Connecting to www.dropbox.com (www.dropbox.com)|162.125.6.18|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: /s/raw/hotxwbgoe2n013k/test.ja [following]
--2020-12-17 06:17:08--  https://www.dropbox.com/s/raw/hotxwbgoe2n013k/test.ja
Reusing existing connection to www.dropbox.com:443.
HTTP request sent, awaiting response... 302 Found
Location: https://uc4934340719f2ae222a50f34fe2.dl.dropboxusercontent.com/cd/0/inline/BFOu2Z4FEmyHlqmblArfNvowZYzG7mpEJoVMGQDOeoa8sxwxocRouB26OLGMgB53Bm3AEHFFwd-bd5ZF5s3OAbj5o6IFcfXuiz0YcuIr7Zo_yQ/file# [following]
--2020-12-17 06:17:08--  https://uc4934340719f2ae222a50f34fe2.dl.dropboxusercontent.com/cd/0/inline/BFOu2Z4FEmyHlqmblArfNvowZYzG7mpEJoVMGQDOeoa8sxwxocRouB26OLGMgB53Bm3AEHFFwd-bd5ZF5s3OAbj5o6IFcfXuiz0YcuIr7Zo_yQ/file
Resolving uc4934340719f2ae222a50f34fe2.dl.dropboxusercontent.com (uc4934340719f2ae222a50f34fe2.dl.dropboxusercontent.com)... 162.125.6.15, 2620:100:6019:15::a27d:40f
Connecting to uc4934340719f2ae222a50f34fe2.dl.dropboxusercontent.com (uc4934340719f2ae222a50f34fe2.dl.dropboxusercontent.com)|162.125.6.15|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 27793 (27K) [text/plain]
Saving to: ‘data/test.ja.1’

test.ja.1           100%[===================>]  27.14K  --.-KB/s    in 0.001s  

2020-12-17 06:17:09 (20.5 MB/s) - ‘data/test.ja.1’ saved [27793/27793]

--2020-12-17 06:17:09--  https://www.dropbox.com/s/5lsftkmb20ay9e1/train.en
Resolving www.dropbox.com (www.dropbox.com)... 162.125.6.18, 2620:100:601a:18::a27d:712
Connecting to www.dropbox.com (www.dropbox.com)|162.125.6.18|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: /s/raw/5lsftkmb20ay9e1/train.en [following]
--2020-12-17 06:17:09--  https://www.dropbox.com/s/raw/5lsftkmb20ay9e1/train.en
Reusing existing connection to www.dropbox.com:443.
HTTP request sent, awaiting response... 302 Found
Location: https://ucaf83c1b7bc6a316c0eb4977241.dl.dropboxusercontent.com/cd/0/inline/BFM8oYSj3uk1thUNLPZqWZus1hE4z8gjPUTSjKNV1jMX2gAphowipqsf6qkl4Qu_ewbjxro8yqtRAMVDRITRrAg8PyLZPOVzrtN0vKsH_PuvVg/file# [following]
--2020-12-17 06:17:09--  https://ucaf83c1b7bc6a316c0eb4977241.dl.dropboxusercontent.com/cd/0/inline/BFM8oYSj3uk1thUNLPZqWZus1hE4z8gjPUTSjKNV1jMX2gAphowipqsf6qkl4Qu_ewbjxro8yqtRAMVDRITRrAg8PyLZPOVzrtN0vKsH_PuvVg/file
Resolving ucaf83c1b7bc6a316c0eb4977241.dl.dropboxusercontent.com (ucaf83c1b7bc6a316c0eb4977241.dl.dropboxusercontent.com)... 162.125.6.15, 2620:100:6019:15::a27d:40f
Connecting to ucaf83c1b7bc6a316c0eb4977241.dl.dropboxusercontent.com (ucaf83c1b7bc6a316c0eb4977241.dl.dropboxusercontent.com)|162.125.6.15|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1701356 (1.6M) [text/plain]
Saving to: ‘data/train.en.1’

train.en.1          100%[===================>]   1.62M  --.-KB/s    in 0.02s   

2020-12-17 06:17:10 (108 MB/s) - ‘data/train.en.1’ saved [1701356/1701356]

--2020-12-17 06:17:10--  https://www.dropbox.com/s/ak53qirssci6f1j/train.ja
Resolving www.dropbox.com (www.dropbox.com)... 162.125.6.18, 2620:100:601a:18::a27d:712
Connecting to www.dropbox.com (www.dropbox.com)|162.125.6.18|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: /s/raw/ak53qirssci6f1j/train.ja [following]
--2020-12-17 06:17:10--  https://www.dropbox.com/s/raw/ak53qirssci6f1j/train.ja
Reusing existing connection to www.dropbox.com:443.
HTTP request sent, awaiting response... 302 Found
Location: https://uc9499bc2cf3bcd2eed1d99a8e84.dl.dropboxusercontent.com/cd/0/inline/BFNUqEkzbjlyqUoogCwLATflT5CP2m_yTs7lo2K8zOoSTgn-swYL6KkbQ02ga9MRFklGXruxPnROqCPmEGkwLvNpW5Vnn6HTGPo4jgkyYFK9Bw/file# [following]
--2020-12-17 06:17:10--  https://uc9499bc2cf3bcd2eed1d99a8e84.dl.dropboxusercontent.com/cd/0/inline/BFNUqEkzbjlyqUoogCwLATflT5CP2m_yTs7lo2K8zOoSTgn-swYL6KkbQ02ga9MRFklGXruxPnROqCPmEGkwLvNpW5Vnn6HTGPo4jgkyYFK9Bw/file
Resolving uc9499bc2cf3bcd2eed1d99a8e84.dl.dropboxusercontent.com (uc9499bc2cf3bcd2eed1d99a8e84.dl.dropboxusercontent.com)... 162.125.6.15, 2620:100:6019:15::a27d:40f
Connecting to uc9499bc2cf3bcd2eed1d99a8e84.dl.dropboxusercontent.com (uc9499bc2cf3bcd2eed1d99a8e84.dl.dropboxusercontent.com)|162.125.6.15|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2784447 (2.7M) [text/plain]
Saving to: ‘data/train.ja.1’

train.ja.1          100%[===================>]   2.66M  --.-KB/s    in 0.03s   

2020-12-17 06:17:11 (103 MB/s) - ‘data/train.ja.1’ saved [2784447/2784447]

In [9]:
! ls data
dev.en	  dev.ja    test.en    test.ja	  train.en    train.ja
dev.en.1  dev.ja.1  test.en.1  test.ja.1  train.en.1  train.ja.1
In [10]:
import random
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from nltk import bleu_score

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.nn.utils.rnn import pad_packed_sequence, pack_padded_sequence
from utils import Vocab

# デバイスの設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

torch.manual_seed(1)
random_state = 42

print(torch.__version__)
1.7.0+cu101

1.データセットの準備

英語-日本語の対訳コーパスである、Tanaka Corpus ( http://www.edrdg.org/wiki/index.php/Tanaka_Corpus )を使います。
今回はそのうちの一部分を取り出したsmall_parallel_enja: 50k En/Ja Parallel Corpus for Testing SMT Methods ( https://github.com/odashi/small_parallel_enja )を使用します。

train.enとtrain.jaの中身を見てみましょう。

In [11]:
! head -10 data/train.en
i can 't tell who will arrive first .
many animals have been destroyed by men .
i 'm in the tennis club .
emi looks happy .
please bear this fact in mind .
she takes care of my children .
we want to be international .
you ought not to break your promise .
when you cross the street , watch out for cars .
i have nothing to live for .
In [12]:
! head -10 ./data/train.ja
誰 が 一番 に 着 く か 私 に は 分か り ま せ ん 。
多く の 動物 が 人間 に よ っ て 滅ぼ さ れ た 。
私 は テニス 部員 で す 。
エミ は 幸せ そう に 見え ま す 。
この 事実 を 心 に 留め て お い て 下さ い 。
彼女 は 私 たち の 世話 を し て くれ る 。
私 達 は 国際 人 に な り た い と 思 い ま す 。
約束 を 破 る べ き で は あ り ま せ ん 。
道路 を 横切 る とき は 車 に 注意 し なさ い 。
私 に は 生き 甲斐 が な い 。

それぞれの文章が英語-日本語で対応しているのがわかります。

1.1データの読み込みと単語の分割

In [13]:
def load_data(file_path):
    # テキストファイルからデータを読み込むメソッド
    data = []
    for line in open(file_path, encoding='utf-8'):
        words = line.strip().split()  # スペースで単語を分割
        data.append(words)
    return data
In [14]:
train_X = load_data('./data/train.en')
train_Y = load_data('./data/train.ja')
In [15]:
# 訓練データと検証データに分割
train_X, valid_X, train_Y, valid_Y = train_test_split(train_X, train_Y, test_size=0.2, random_state=random_state)

この時点で入力と教師データは以下のようになっています

In [16]:
print('train data', train_X[0])
print('valid data', valid_X[0])
train data ['where', 'shall', 'we', 'eat', 'tonight', '?']
valid data ['you', 'may', 'extend', 'your', 'stay', 'in', 'tokyo', '.']

1.2単語辞書の作成

データセットに登場する各単語にIDを割り振る

In [17]:
# まず特殊トークンを定義しておく
PAD_TOKEN = '<PAD>'  # バッチ処理の際に、短い系列の末尾を埋めるために使う (Padding)
BOS_TOKEN = '<S>'  # 系列の始まりを表す (Beggining of sentence)
EOS_TOKEN = '</S>'  # 系列の終わりを表す (End of sentence)
UNK_TOKEN = '<UNK>'  # 語彙に存在しない単語を表す (Unknown)
PAD = 0
BOS = 1
EOS = 2
UNK = 3
In [18]:
MIN_COUNT = 2  # 語彙に含める単語の最低出現回数 再提出現回数に満たない単語はUNKに置き換えられる

# 単語をIDに変換する辞書の初期値を設定
word2id = {
    PAD_TOKEN: PAD,
    BOS_TOKEN: BOS,
    EOS_TOKEN: EOS,
    UNK_TOKEN: UNK,
    }

# 単語辞書を作成
vocab_X = Vocab(word2id=word2id)
vocab_Y = Vocab(word2id=word2id)
vocab_X.build_vocab(train_X, min_count=MIN_COUNT)
vocab_Y.build_vocab(train_Y, min_count=MIN_COUNT)
In [19]:
vocab_size_X = len(vocab_X.id2word)
vocab_size_Y = len(vocab_Y.id2word)
print('入力言語の語彙数:', vocab_size_X)
print('出力言語の語彙数:', vocab_size_Y)
入力言語の語彙数: 3725
出力言語の語彙数: 4405
In [20]:
vocab_X.id2word
Out[20]:
{0: '<PAD>',
 1: '<S>',
 2: '</S>',
 3: '<UNK>',
 4: '.',
 5: 'the',
 6: 'i',
 7: 'to',
 8: 'you',
 9: 'is',
 10: 'he',
 11: 'a',
 12: '?',
 13: 'in',
 14: 'it',
 15: 'of',
 16: 'she',
 17: 'for',
 18: 'my',
 19: 'have',
 20: 'this',
 21: "'t",
 22: 'was',
 23: 'me',
 24: ',',
 25: 'that',
 26: 'at',
 27: 'his',
 28: 'we',
 29: 'do',
 30: 'will',
 31: 'on',
 32: 'her',
 33: "'s",
 34: 'with',
 35: 'not',
 36: 'your',
 37: 'are',
 38: 'what',
 39: 'be',
 40: 'as',
 41: 'can',
 42: 'him',
 43: 'there',
 44: 'don',
 45: 'has',
 46: 'go',
 47: 'like',
 48: 'up',
 49: 'all',
 50: 'time',
 51: 'out',
 52: 'by',
 53: 'very',
 54: 'come',
 55: 'they',
 56: 'how',
 57: 'from',
 58: 'please',
 59: 'no',
 60: 'and',
 61: 'had',
 62: 'good',
 63: "'m",
 64: 'am',
 65: 'here',
 66: 'did',
 67: 'get',
 68: 'about',
 69: 'been',
 70: "'ll",
 71: 'when',
 72: 'take',
 73: 'see',
 74: 'an',
 75: 'so',
 76: 'work',
 77: 'let',
 78: 'day',
 79: 'know',
 80: 'would',
 81: 'now',
 82: 'went',
 83: 'well',
 84: 'going',
 85: 'father',
 86: 'were',
 87: 'book',
 88: 'if',
 89: 'than',
 90: 'should',
 91: 'one',
 92: 'school',
 93: 'may',
 94: 'last',
 95: 'home',
 96: 'much',
 97: '!',
 98: 'want',
 99: 'got',
 100: 'money',
 101: 'make',
 102: 'made',
 103: 'more',
 104: 'long',
 105: 'man',
 106: 'english',
 107: 'any',
 108: 'car',
 109: 'us',
 110: 'tomorrow',
 111: 'too',
 112: 'help',
 113: 'but',
 114: 'must',
 115: 'room',
 116: 'came',
 117: 'house',
 118: 'our',
 119: 'could',
 120: 'yesterday',
 121: 'way',
 122: 'new',
 123: 'off',
 124: 'mother',
 125: 'after',
 126: 'back',
 127: 'think',
 128: 'some',
 129: 'better',
 130: 'today',
 131: 'soon',
 132: 'where',
 133: 'nothing',
 134: 'always',
 135: 'before',
 136: 'every',
 137: 'who',
 138: 'late',
 139: 'many',
 140: 'old',
 141: 'put',
 142: 'just',
 143: 'night',
 144: 'look',
 145: 'little',
 146: 'people',
 147: 'two',
 148: 'hard',
 149: 'down',
 150: 'next',
 151: 'over',
 152: 'give',
 153: 'morning',
 154: 'tell',
 155: 'years',
 156: "'ve",
 157: 'say',
 158: 'took',
 159: 'said',
 160: 'didn',
 161: 'once',
 162: 'never',
 163: 'station',
 164: 'does',
 165: 'keep',
 166: 'speak',
 167: 'right',
 168: 'train',
 169: 'plan',
 170: 'into',
 171: 'party',
 172: 'cannot',
 173: 'cold',
 174: 'ten',
 175: 'walk',
 176: 'three',
 177: 'boy',
 178: 'week',
 179: 'read',
 180: 'such',
 181: 'live',
 182: 'rain',
 183: 'teacher',
 184: "'d",
 185: 'lot',
 186: 'bus',
 187: 'left',
 188: 'children',
 189: 'away',
 190: 'tom',
 191: 'play',
 192: 'call',
 193: 'happy',
 194: 'leave',
 195: 'only',
 196: 'kind',
 197: 'name',
 198: 'accident',
 199: 'mind',
 200: 'letter',
 201: 'true',
 202: 'again',
 203: 'why',
 204: 'news',
 205: 'life',
 206: 'watch',
 207: 'feel',
 208: 'brother',
 209: 'dog',
 210: 'job',
 211: 'study',
 212: 'best',
 213: 'problem',
 214: 'anything',
 215: 'other',
 216: 'saw',
 217: 'days',
 218: 'use',
 219: 'something',
 220: 'books',
 221: 'japan',
 222: 'year',
 223: 'friends',
 224: 'or',
 225: 'far',
 226: 'meeting',
 227: 'gave',
 228: 'early',
 229: 'often',
 230: 'friend',
 231: 'lost',
 232: 'story',
 233: 'looked',
 234: 'great',
 235: 'yourself',
 236: 'stay',
 237: 'matter',
 238: 'first',
 239: 'busy',
 240: 'found',
 241: 'doctor',
 242: 'used',
 243: 'answer',
 244: 'met',
 245: 'seems',
 246: 'need',
 247: 'care',
 248: 'enough',
 249: 'them',
 250: 'beautiful',
 251: 'sister',
 252: 'thing',
 253: 'tennis',
 254: 'while',
 255: 'girl',
 256: 'business',
 257: 'child',
 258: 'which',
 259: 'heard',
 260: 'young',
 261: 'doesn',
 262: 'five',
 263: "'re",
 264: 'their',
 265: 'told',
 266: 'hear',
 267: 'door',
 268: 'students',
 269: 'question',
 270: 'these',
 271: 'finished',
 272: 'write',
 273: 'son',
 274: 'own',
 275: 'without',
 276: 'looking',
 277: 'bad',
 278: 'wrong',
 279: 'bed',
 280: 'looks',
 281: 'another',
 282: 'river',
 283: 'since',
 284: 'tokyo',
 285: 'tired',
 286: 'weather',
 287: 'family',
 288: 'homework',
 289: 'water',
 290: 'eat',
 291: 'comes',
 292: 'few',
 293: 'idea',
 294: 'coming',
 295: 'bought',
 296: 'health',
 297: 'quite',
 298: 'everything',
 299: 'run',
 300: 'along',
 301: 'love',
 302: 'town',
 303: 'asked',
 304: 'sure',
 305: 'difficult',
 306: 'seen',
 307: 'hand',
 308: 'turn',
 309: 'parents',
 310: 'being',
 311: 'hours',
 312: 'talk',
 313: 'called',
 314: 'nice',
 315: 'music',
 316: 'turned',
 317: 'easy',
 318: 'place',
 319: 'yet',
 320: 'getting',
 321: 'shall',
 322: 'hope',
 323: 'still',
 324: 'minutes',
 325: 'try',
 326: 'done',
 327: 'married',
 328: 'won',
 329: 'because',
 330: 'japanese',
 331: 'ever',
 332: 'reading',
 333: 'ran',
 334: 'coffee',
 335: 'really',
 336: 'john',
 337: 'open',
 338: 'able',
 339: 'park',
 340: 'success',
 341: 'meet',
 342: 'arrived',
 343: 'ask',
 344: 'swim',
 345: 'buy',
 346: 'believe',
 347: 'mary',
 348: 'picture',
 349: 'examination',
 350: 'afraid',
 351: 'isn',
 352: 'wait',
 353: 'wish',
 354: 'tv',
 355: 'waiting',
 356: 'fast',
 357: 'word',
 358: 'baby',
 359: 'dictionary',
 360: 'summer',
 361: 'kept',
 362: 'alone',
 363: 'same',
 364: 'dinner',
 365: 'ago',
 366: 'student',
 367: 'tonight',
 368: 'eyes',
 369: 'street',
 370: 'start',
 371: 'doing',
 372: 'gone',
 373: 'abroad',
 374: 'sorry',
 375: 'game',
 376: 'show',
 377: 'drink',
 378: 'class',
 379: 'most',
 380: 'playing',
 381: 'smoking',
 382: 'goes',
 383: 'food',
 384: 'around',
 385: 'interesting',
 386: 'through',
 387: 'things',
 388: 'world',
 389: 'big',
 390: 'ill',
 391: 'window',
 392: 'sick',
 393: 'agree',
 394: 'himself',
 395: 'lived',
 396: 'those',
 397: 'afternoon',
 398: 'snow',
 399: 'find',
 400: 'couldn',
 401: 'order',
 402: 'truth',
 403: 'month',
 404: 'broke',
 405: 'lives',
 406: 'set',
 407: "''",
 408: 'table',
 409: 'began',
 410: 'company',
 411: 'angry',
 412: 'each',
 413: 'country',
 414: 'remember',
 415: 'end',
 416: 'mine',
 417: 'everybody',
 418: 'advice',
 419: 'myself',
 420: 'present',
 421: 'visit',
 422: 'french',
 423: 'catch',
 424: 'dress',
 425: 'less',
 426: 'sunday',
 427: 'hour',
 428: 'free',
 429: 'carry',
 430: 'happened',
 431: 'known',
 432: 'stop',
 433: 'small',
 434: 'uncle',
 435: 'face',
 436: 'person',
 437: 'tall',
 438: 'six',
 439: 'succeed',
 440: 'hot',
 441: 'talking',
 442: 'fire',
 443: 'trouble',
 444: 'office',
 445: 'hotel',
 446: 'opinion',
 447: 'thank',
 448: 'times',
 449: 'then',
 450: 'pass',
 451: 'evening',
 452: 'likes',
 453: 'important',
 454: 'desk',
 455: 'large',
 456: 'mistake',
 457: 'wife',
 458: 'fine',
 459: 'mr',
 460: 'tried',
 461: 'caught',
 462: 'sight',
 463: 'london',
 464: 'until',
 465: 'change',
 466: 'makes',
 467: 'running',
 468: 'short',
 469: 'lunch',
 470: 'baseball',
 471: 'tea',
 472: 'rich',
 473: 'sleep',
 474: 'having',
 475: 'bicycle',
 476: 'o',
 477: "'clock",
 478: 'breakfast',
 479: 'bill',
 480: 'telephone',
 481: 'trip',
 482: 'against',
 483: 'worked',
 484: 'hurry',
 485: 'understand',
 486: 'phone',
 487: 'everyone',
 488: 'words',
 489: 'city',
 490: 'knows',
 491: 'america',
 492: 'camera',
 493: 'account',
 494: 'high',
 495: 'working',
 496: 'forget',
 497: 'failed',
 498: 'point',
 499: 'thought',
 500: 'ready',
 501: 'poor',
 502: 'takes',
 503: 'honest',
 504: 'born',
 505: 'across',
 506: 'studying',
 507: 'movie',
 508: 'piano',
 509: 'fail',
 510: 'walked',
 511: 'fell',
 512: 'shoes',
 513: 'age',
 514: 'box',
 515: 'others',
 516: 'says',
 517: 'full',
 518: 'pay',
 519: 'road',
 520: 'ken',
 521: 'learn',
 522: 'decided',
 523: 'building',
 524: 'near',
 525: 'haven',
 526: 'died',
 527: 'stand',
 528: 'already',
 529: 'fun',
 530: 'police',
 531: 'hat',
 532: 'ought',
 533: 'jane',
 534: 'cup',
 535: 'fish',
 536: 'bring',
 537: 'even',
 538: 'store',
 539: 'drive',
 540: 'making',
 541: 'become',
 542: 'speech',
 543: 'garden',
 544: 'kyoto',
 545: 'dream',
 546: 'part',
 547: 'vacation',
 548: 'light',
 549: 'strange',
 550: 'fact',
 551: 'wanted',
 552: 'flowers',
 553: 'paris',
 554: 'war',
 555: 'lake',
 556: 'traffic',
 557: 'longer',
 558: 'means',
 559: 'surprised',
 560: 'daughter',
 561: 'under',
 562: 'hair',
 563: 'wonder',
 564: 'wants',
 565: 'result',
 566: 'mistakes',
 567: 'herself',
 568: 'illness',
 569: 'possible',
 570: 'plane',
 571: 'dollars',
 572: 'speaks',
 573: 'birthday',
 574: 'york',
 575: 'absent',
 576: 'glad',
 577: 'solve',
 578: 'moment',
 579: 'bank',
 580: 'seeing',
 581: 'almost',
 582: 'promise',
 583: 'someone',
 584: 'number',
 585: 'airport',
 586: 'rather',
 587: 'heavy',
 588: 'till',
 589: 'living',
 590: 'tears',
 591: 'started',
 592: 'library',
 593: 'sit',
 594: 'cake',
 595: 'song',
 596: 'medicine',
 597: 'later',
 598: 'both',
 599: 'stayed',
 600: 'air',
 601: 'lend',
 602: 'television',
 603: 'exam',
 604: 'seven',
 605: 'yours',
 606: 'raining',
 607: 'sense',
 608: 'different',
 609: 'passed',
 610: 'usually',
 611: 'became',
 612: 'pen',
 613: 'reason',
 614: 'whether',
 615: 'anxious',
 616: 'front',
 617: 'brought',
 618: 'novel',
 619: 'certain',
 620: 'attend',
 621: 'sea',
 622: 'nobody',
 623: 'head',
 624: 'behind',
 625: 'case',
 626: 'felt',
 627: 'sat',
 628: 'clear',
 629: 'cut',
 630: 'cat',
 631: 'death',
 632: 'watching',
 633: 'mean',
 634: 'finish',
 635: 'future',
 636: 'winter',
 637: 'enjoyed',
 638: 'sun',
 639: 'quiet',
 640: 'rest',
 641: 'tree',
 642: 'during',
 643: 'gets',
 644: 'hospital',
 645: 'spring',
 646: 'kindness',
 647: 'danger',
 648: 'heart',
 649: 'close',
 650: 'visited',
 651: 'taking',
 652: 'together',
 653: 'above',
 654: 'stopped',
 655: 'happen',
 656: 'village',
 657: 'swimming',
 658: 'proposal',
 659: 'clean',
 660: 'necessary',
 661: 'bag',
 662: 'wrote',
 663: 'works',
 664: 'pretty',
 665: 'taken',
 666: 'touch',
 667: 'radio',
 668: 'miss',
 669: 'stood',
 670: 'chance',
 671: 'helped',
 672: 'writing',
 673: 'careful',
 674: 'sometimes',
 675: 'expensive',
 676: 'seat',
 677: 'jim',
 678: 'experience',
 679: 'secret',
 680: 'impossible',
 681: 'hands',
 682: 'sky',
 683: 'played',
 684: 'earth',
 685: 'boys',
 686: 'lie',
 687: 'shop',
 688: 'forward',
 689: 'fool',
 690: 'questions',
 691: 'address',
 692: 'college',
 693: 'famous',
 694: 'coat',
 695: 'send',
 696: 'dark',
 697: 'dead',
 698: 'paper',
 699: 'voice',
 700: 'travel',
 701: 'machine',
 702: 'four',
 703: 'paid',
 704: 'yen',
 705: 'clothes',
 706: 'easily',
 707: 'hold',
 708: 'mountain',
 709: 'bridge',
 710: 'succeeded',
 711: 'singer',
 712: 'line',
 713: 'noise',
 714: 'saying',
 715: 'studied',
 716: 'break',
 717: 'arrive',
 718: 'computer',
 719: 'spoke',
 720: 'whole',
 721: 'worth',
 722: 'men',
 723: 'bob',
 724: 'team',
 725: 'slowly',
 726: 'peace',
 727: 'eight',
 728: 'husband',
 729: 'concert',
 730: 'smoke',
 731: 'american',
 732: 'red',
 733: 'walking',
 734: 'interested',
 735: 'blue',
 736: 'quickly',
 737: 'side',
 738: 'rule',
 739: 'also',
 740: 'subject',
 741: 'strong',
 742: 'wind',
 743: 'ship',
 744: 'taxi',
 745: 'hardly',
 746: 'woman',
 747: 'moon',
 748: 'girls',
 749: 'between',
 750: 'pleased',
 751: 'failure',
 752: 'test',
 753: 'white',
 754: 'storm',
 755: 'several',
 756: 'fishing',
 757: 'post',
 758: "'",
 759: 'moved',
 760: 'price',
 761: 'might',
 762: 'spent',
 763: 'hit',
 764: 'eye',
 765: 'deal',
 766: 'milk',
 767: 'either',
 768: 'cost',
 769: 'its',
 770: 'smith',
 771: 'keeps',
 772: 'enjoy',
 773: 'twice',
 774: 'cry',
 775: 'check',
 776: 'glass',
 777: 'purpose',
 778: 'half',
 779: 'held',
 780: 'bit',
 781: 'asleep',
 782: 'view',
 783: 'closed',
 784: 'foreign',
 785: 'finally',
 786: 'report',
 787: 'osaka',
 788: 'pain',
 789: 'policeman',
 790: 'doubt',
 791: 'speaking',
 792: 'win',
 793: 'carried',
 794: 'given',
 795: 'upon',
 796: 'blame',
 797: 'killed',
 798: 'hurt',
 799: 'changed',
 800: 'broken',
 801: 'sing',
 802: 'anybody',
 803: 'crying',
 804: 'university',
 805: 'prevented',
 806: 'taller',
 807: 'sad',
 808: 'anyone',
 809: 'satisfied',
 810: 'sent',
 811: 'england',
 812: 'popular',
 813: 'cars',
 814: 'listen',
 815: 'driving',
 816: 'staying',
 817: 'invited',
 818: 'waited',
 819: 'cook',
 820: 'animals',
 821: 'beauty',
 822: 'ahead',
 823: 'language',
 824: 'proud',
 825: 'hill',
 826: 'thinking',
 827: 'suddenly',
 828: 'president',
 829: 'recently',
 830: 'shopping',
 831: 'movies',
 832: 'due',
 833: 'eating',
 834: 'ability',
 835: 'umbrella',
 836: 'worry',
 837: 'brown',
 838: 'laughed',
 839: 'newspaper',
 840: 'public',
 841: 'among',
 842: 'leaving',
 843: 'cooking',
 844: 'leaves',
 845: 'stolen',
 846: 'die',
 847: 'begin',
 848: 'fall',
 849: 'built',
 850: 'wall',
 851: 'singing',
 852: 'return',
 853: 'accustomed',
 854: 'afford',
 855: 'listening',
 856: 'pick',
 857: 'favor',
 858: 'concerned',
 859: 'ticket',
 860: 'wasn',
 861: 'trees',
 862: 'earlier',
 863: 'written',
 864: 'joke',
 865: 'missed',
 866: 'ride',
 867: 'sound',
 868: 'marry',
 869: 'floor',
 870: 'promised',
 871: 'flower',
 872: 'none',
 873: 'drop',
 874: 'telling',
 875: 'history',
 876: 'pleasant',
 877: 'expected',
 878: 'older',
 879: 'advised',
 880: 'else',
 881: 'harder',
 882: 'loves',
 883: 'mail',
 884: 'beyond',
 885: 'math',
 886: 'tony',
 887: 'birds',
 888: 'laugh',
 889: 'pale',
 890: 'whatever',
 891: 'aren',
 892: 'pictures',
 893: 'crowd',
 894: 'boat',
 895: 'borrow',
 896: 'knowledge',
 897: 'feeling',
 898: 'owe',
 899: 'enter',
 900: 'warm',
 901: 'sleeping',
 902: 'runs',
 903: 'thirty',
 904: 'alive',
 905: 'wash',
 906: 'dangerous',
 907: 'key',
 908: 'younger',
 909: 'usual',
 910: 'whose',
 911: 'holiday',
 912: 'noon',
 913: 'rules',
 914: 'fond',
 915: 'shower',
 916: 'top',
 917: 'horse',
 918: 'studies',
 919: 'anywhere',
 920: 'clock',
 921: 'offer',
 922: 'though',
 923: 'mouth',
 924: 'prefer',
 925: 'showed',
 926: 'healthy',
 927: 'information',
 928: 'boss',
 929: 'rained',
 930: 'happiness',
 931: 'arm',
 932: 'knew',
 933: 'foot',
 934: 'needs',
 935: 'wonderful',
 936: 'familiar',
 937: 'expect',
 938: 'seemed',
 939: 'efforts',
 940: 'fever',
 941: 'returned',
 942: 'golf',
 943: 'oh',
 944: 'sports',
 945: 'baggage',
 946: 'childhood',
 947: 'excuse',
 948: 'rains',
 949: 'hurried',
 950: 'seem',
 951: 'outside',
 952: 'pencil',
 953: 'cross',
 954: 'move',
 955: 'skiing',
 956: 'piece',
 957: 'meals',
 958: 'standing',
 959: 'past',
 960: 'join',
 961: 'responsible',
 962: 'talked',
 963: 'duty',
 964: 'invite',
 965: 'carefully',
 966: 'clever',
 967: 'date',
 968: 'explanation',
 969: 'least',
 970: 'i.',
 971: 'likely',
 972: 'somebody',
 973: 'carelessness',
 974: 'problems',
 975: 'suppose',
 976: 'yes',
 977: 'answered',
 978: 'supper',
 979: 'eaten',
 980: 'rumor',
 981: 'loved',
 982: 'sooner',
 983: 'appears',
 984: 'regret',
 985: 'forgot',
 986: 'scolded',
 987: 'notice',
 988: 'reached',
 989: 'rely',
 990: 'importance',
 991: 'guess',
 992: 'beginning',
 993: 'minute',
 994: 'france',
 995: 'picnic',
 996: 'nine',
 997: 'twenty',
 998: 'terrible',
 999: 'wake',
 ...}

2.テンソルへの変換

2.1 IDへの変換

まずはモデルが文章を認識できるように、文章を単語IDのリストに変換します

In [21]:
def sentence_to_ids(vocab, sentence):
    # 単語(str)のリストをID(int)のリストに変換する関数
    ids = [vocab.word2id.get(word, UNK) for word in sentence]
    ids += [EOS]  # EOSを加える
    return ids
In [22]:
train_X = [sentence_to_ids(vocab_X, sentence) for sentence in train_X]
train_Y = [sentence_to_ids(vocab_Y, sentence) for sentence in train_Y]
valid_X = [sentence_to_ids(vocab_X, sentence) for sentence in valid_X]
valid_Y = [sentence_to_ids(vocab_Y, sentence) for sentence in valid_Y]

この時点で入力と教師データは以下のようになっている

In [23]:
print('train data', train_X[0])
print('valid data', valid_X[0])
train data [132, 321, 28, 290, 367, 12, 2]
valid data [8, 93, 3532, 36, 236, 13, 284, 4, 2]

2.2 DataLoaderの定義

データセットからバッチを取得するデータローダーを定義します

  • この際、長さの異なる複数の系列をバッチで並列に扱えるように、短い系列の末尾を特定のシンボル(<PAD>など)でパディングし、バッチ内の系列の長さを最長のものに合わせる
  • (batch_size, max_length)のサイズの行列を得るが、実際にモデルを学習させるときには、バッチをまたいで各時刻ごとに進めていくので、転置して(max_length, batch_size)の形に変える
    (batch_first=Trueのオプションを使う場合は不要)
In [24]:
def pad_seq(seq, max_length):
    # 系列(seq)が指定の文長(max_length)になるように末尾をパディングする
    res = seq + [PAD for i in range(max_length - len(seq))]
    return res    


class DataLoader(object):

    def __init__(self, X, Y, batch_size, shuffle=False):
        """
        :param X: list, 入力言語の文章(単語IDのリスト)のリスト
        :param Y: list, 出力言語の文章(単語IDのリスト)のリスト
        :param batch_size: int, バッチサイズ
        :param shuffle: bool, サンプルの順番をシャッフルするか否か
        """
        self.data = list(zip(X, Y))
        self.batch_size = batch_size
        self.shuffle = shuffle
        self.start_index = 0
        
        self.reset()
    
    def reset(self):
        if self.shuffle:  # サンプルの順番をシャッフルする
            self.data = shuffle(self.data, random_state=random_state)
        self.start_index = 0  # ポインタの位置を初期化する
    
    def __iter__(self):
        return self

    def __next__(self):
        # ポインタが最後まで到達したら初期化する
        if self.start_index >= len(self.data):
            self.reset()
            raise StopIteration()

        # バッチを取得
        seqs_X, seqs_Y = zip(*self.data[self.start_index:self.start_index+self.batch_size])
        # 入力系列seqs_Xの文章の長さ順(降順)に系列ペアをソートする
        seq_pairs = sorted(zip(seqs_X, seqs_Y), key=lambda p: len(p[0]), reverse=True)
        seqs_X, seqs_Y = zip(*seq_pairs)
        # 短い系列の末尾をパディングする
        lengths_X = [len(s) for s in seqs_X]  # 後述のEncoderのpack_padded_sequenceでも用いる
        lengths_Y = [len(s) for s in seqs_Y]
        max_length_X = max(lengths_X)
        max_length_Y = max(lengths_Y)
        padded_X = [pad_seq(s, max_length_X) for s in seqs_X]
        padded_Y = [pad_seq(s, max_length_Y) for s in seqs_Y]
        # tensorに変換し、転置する
        batch_X = torch.tensor(padded_X, dtype=torch.long, device=device).transpose(0, 1)
        batch_Y = torch.tensor(padded_Y, dtype=torch.long, device=device).transpose(0, 1)

        # ポインタを更新する
        self.start_index += self.batch_size

        return batch_X, batch_Y, lengths_X

3.モデルの構築

EncoderとDecoderのRNNを定義します。

導入:PackedSequence

PyTorchのRNNでは、可変長の系列のバッチを効率よく計算できるように系列を表現するPackedSequenceというクラスを用いることができます。

入力バッチのテンソルをこのPackedSequenceのインスタンスに変換してからRNNに入力することで、パディング部分の計算を省略することができるため、効率的な計算が可能になります。

PackedSequenceを作成するには、まず、系列長の異なるバッチに対してパディングを行なってください。

ここで、パディングを行う前に各サンプルの系列長(lengths)を保存しておきます。

In [25]:
# 系列長がそれぞれ4,3,2の3つのサンプルからなるバッチを作成
batch = [[1,2,3,4], [5,6,7], [8,9]]
lengths = [len(sample) for sample in batch]
print('各サンプルの系列長:', lengths)
print()

# 最大系列長に合うように各サンプルをpadding
_max_length = max(lengths)
padded = torch.tensor([pad_seq(sample, _max_length) for sample in batch])
print('paddingされたテンソル:\n', padded)
padded = padded.transpose(0,1) # (max_length, batch_size)に転置
print('padding & 転置されたテンソル:\n', padded)
print('padding & 転置されたテンソルのサイズ:\n', padded.size())
print()
各サンプルの系列長: [4, 3, 2]

paddingされたテンソル:
 tensor([[1, 2, 3, 4],
        [5, 6, 7, 0],
        [8, 9, 0, 0]])
padding & 転置されたテンソル:
 tensor([[1, 5, 8],
        [2, 6, 9],
        [3, 7, 0],
        [4, 0, 0]])
padding & 転置されたテンソルのサイズ:
 torch.Size([4, 3])

次に、パディングを行ったテンソル(padded)と各サンプルの元々の系列長(lengths)をtorch.nn.utils.rnn.pack_padded_sequenceという関数に与えると、 databatch_sizesという要素を持ったPackedSequenceのインスタンス(packed)が作成できます。

  • data: テンソルのPAD以外の値のみを保有するベクトル
  • batch_sizes: 各時刻で計算が必要な(=PADに到達していない)バッチの数を表すベクトル
In [26]:
# PackedSequenceに変換(テンソルをRNNに入力する前に適用する)
packed = pack_padded_sequence(padded, lengths=lengths) # 各サンプルの系列長も与える
print('PackedSequenceのインスタンス:\n', packed) # テンソルのPAD以外の値(data)と各時刻で計算が必要な(=PADに到達していない)バッチの数(batch_sizes)を有するインスタンス
print()
PackedSequenceのインスタンス:
 PackedSequence(data=tensor([1, 5, 8, 2, 6, 9, 3, 7, 4]), batch_sizes=tensor([3, 3, 2, 1]), sorted_indices=None, unsorted_indices=None)

こうして得られたPackedSequenceのインスタンスをRNNに入力します。(ここでは省略)

RNNから出力されたテンソルはPackedSeauenceのインスタンスのままなので、後段の計算につなぐためにtorch.nn.utils.rnn.pad_packed_sequenceの関数によって通常のテンソルに戻します。

In [27]:
# PackedSequenceのインスタンスをRNNに入力する(ここでは省略)
output = packed

# テンソルに戻す(RNNの出力に対して適用する)
output, _length = pad_packed_sequence(output)  # PADを含む元のテンソルと各サンプルの系列長を返す
print('PADを含む元のテンソル:\n', output)
print('各サンプルの系列長:', _length)
PADを含む元のテンソル:
 tensor([[1, 5, 8],
        [2, 6, 9],
        [3, 7, 0],
        [4, 0, 0]])
各サンプルの系列長: tensor([4, 3, 2])

Encoder

今回はEncoder側でバッチを処理する際に、pack_padded_sequence関数によってtensorをPackedSequenceに変換し、処理を終えた後にpad_packed_sequence関数によってtensorに戻すという処理を行います。

In [28]:
class Encoder(nn.Module):
    def __init__(self, input_size, hidden_size):
        """
        :param input_size: int, 入力言語の語彙数
        :param hidden_size: int, 隠れ層のユニット数
        """
        super(Encoder, self).__init__()
        self.hidden_size = hidden_size

        self.embedding = nn.Embedding(input_size, hidden_size, padding_idx=PAD)
        self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, seqs, input_lengths, hidden=None):
        """
        :param seqs: tensor, 入力のバッチ, size=(max_length, batch_size)
        :param input_lengths: 入力のバッチの各サンプルの文長
        :param hidden: tensor, 隠れ状態の初期値, Noneの場合は0で初期化される
        :return output: tensor, Encoderの出力, size=(max_length, batch_size, hidden_size)
        :return hidden: tensor, Encoderの隠れ状態, size=(1, batch_size, hidden_size)
        """
        emb = self.embedding(seqs) # seqsはパディング済み
        packed = pack_padded_sequence(emb, input_lengths) # PackedSequenceオブジェクトに変換
        output, hidden = self.gru(packed, hidden)
        output, _ = pad_packed_sequence(output)
        return output, hidden

Decoder

今回はDecoder側ではパディング等行わないので、通常のtensorのままRNNに入力して問題ありません。

In [29]:
class Decoder(nn.Module):
    def __init__(self, hidden_size, output_size):
        """
        :param hidden_size: int, 隠れ層のユニット数
        :param output_size: int, 出力言語の語彙数
        :param dropout: float, ドロップアウト率
        """
        super(Decoder, self).__init__()
        self.hidden_size = hidden_size
        self.output_size = output_size

        self.embedding = nn.Embedding(output_size, hidden_size, padding_idx=PAD)
        self.gru = nn.GRU(hidden_size, hidden_size)
        self.out = nn.Linear(hidden_size, output_size)

    def forward(self, seqs, hidden):
        """
        :param seqs: tensor, 入力のバッチ, size=(1, batch_size)
        :param hidden: tensor, 隠れ状態の初期値, Noneの場合は0で初期化される
        :return output: tensor, Decoderの出力, size=(1, batch_size, output_size)
        :return hidden: tensor, Decoderの隠れ状態, size=(1, batch_size, hidden_size)
        """
        emb = self.embedding(seqs)
        output, hidden = self.gru(emb, hidden)
        output = self.out(output)
        return output, hidden

EncoderDecoder

上で定義したEncoderとDecoderを用いた、一連の処理をまとめるEncoderDecoderのクラスを定義します。

ここで、Decoder側の処理で注意する点があります。

RNNでは、時刻$t$の出力を時刻$t+1$の入力とすることができるが、この方法でDecoderを学習させると連鎖的に誤差が大きくなっていき、学習が不安定になったり収束が遅くなったりする問題が発生します。

この問題への対策としてTeacher Forcingというテクニックがあります。 これは、訓練時にはDecoder側の入力に、ターゲット系列(参照訳)をそのまま使うというものです。 これにより学習が安定し、収束が早くなるというメリットがありますが、逆に評価時は前の時刻にDecoderが生成したものが使われるため、学習時と分布が異なってしまうというデメリットもあります。

Teacher Forcingの拡張として、ターゲット系列を入力とするか生成された結果を入力とするかを確率的にサンプリングするScheduled Samplingという手法があります。

ここではScheduled Samplingを採用し、一定の確率に基づいてターゲット系列を入力とするか生成された結果を入力とするかを切り替えられるようにクラスを定義しておきます。

In [30]:
class EncoderDecoder(nn.Module):
    """EncoderとDecoderの処理をまとめる"""
    def __init__(self, input_size, output_size, hidden_size):
        """
        :param input_size: int, 入力言語の語彙数
        :param output_size: int, 出力言語の語彙数
        :param hidden_size: int, 隠れ層のユニット数
        """
        super(EncoderDecoder, self).__init__()
        self.encoder = Encoder(input_size, hidden_size)
        self.decoder = Decoder(hidden_size, output_size)

    def forward(self, batch_X, lengths_X, max_length, batch_Y=None, use_teacher_forcing=False):
        """
        :param batch_X: tensor, 入力系列のバッチ, size=(max_length, batch_size)
        :param lengths_X: list, 入力系列のバッチ内の各サンプルの文長
        :param max_length: int, Decoderの最大文長
        :param batch_Y: tensor, Decoderで用いるターゲット系列
        :param use_teacher_forcing: Decoderでターゲット系列を入力とするフラグ
        :return decoder_outputs: tensor, Decoderの出力, 
            size=(max_length, batch_size, self.decoder.output_size)
        """
        # encoderに系列を入力(複数時刻をまとめて処理)
        _, encoder_hidden = self.encoder(batch_X, lengths_X)
        
        _batch_size = batch_X.size(1)

        # decoderの入力と隠れ層の初期状態を定義
        decoder_input = torch.tensor([BOS] * _batch_size, dtype=torch.long, device=device) # 最初の入力にはBOSを使用する
        decoder_input = decoder_input.unsqueeze(0)  # (1, batch_size)
        decoder_hidden = encoder_hidden  # Encoderの最終隠れ状態を取得

        # decoderの出力のホルダーを定義
        decoder_outputs = torch.zeros(max_length, _batch_size, self.decoder.output_size, device=device) # max_length分の固定長

        # 各時刻ごとに処理
        for t in range(max_length):
            decoder_output, decoder_hidden = self.decoder(decoder_input, decoder_hidden)
            decoder_outputs[t] = decoder_output
            # 次の時刻のdecoderの入力を決定
            if use_teacher_forcing and batch_Y is not None:  # teacher forceの場合、ターゲット系列を用いる
                decoder_input = batch_Y[t].unsqueeze(0)
            else:  # teacher forceでない場合、自身の出力を用いる
                decoder_input = decoder_output.max(-1)[1]
                
        return decoder_outputs

4.訓練

4.1 損失関数の定義

基本的にはクロスエントロピーを損失関数として用いますが、パディングを行うと短い系列の末尾には<PAD>トークンが入るため、この部分の損失を計算しないように、マスクをかけます。

In [55]:
mce = nn.CrossEntropyLoss(size_average=False, ignore_index=PAD) # PADを無視する
def masked_cross_entropy(logits, target):
    logits_flat = logits.view(-1, logits.size(-1)) # (max_seq_len * batch_size, output_size)
    target_flat = target.view(-1) # (max_seq_len * batch_size, 1)
    return mce(logits_flat, target_flat)
/usr/local/lib/python3.6/dist-packages/torch/nn/_reduction.py:44: UserWarning: size_average and reduce args will be deprecated, please use reduction='sum' instead.
  warnings.warn(warning.format(ret))

4.2学習

In [56]:
# ハイパーパラメータの設定
num_epochs = 10
batch_size = 64
lr = 1e-3  # 学習率
teacher_forcing_rate = 0.2  # Teacher Forcingを行う確率
ckpt_path = 'model.pth'  # 学習済みのモデルを保存するパス

model_args = {
    'input_size': vocab_size_X,
    'output_size': vocab_size_Y,
    'hidden_size': 256,
}
In [57]:
# データローダを定義
train_dataloader = DataLoader(train_X, train_Y, batch_size=batch_size, shuffle=True)
valid_dataloader = DataLoader(valid_X, valid_Y, batch_size=batch_size, shuffle=False)

# モデルとOptimizerを定義
model = EncoderDecoder(**model_args).to(device)
optimizer = optim.Adam(model.parameters(), lr=lr)

実際に損失関数を計算する関数を定義します。

In [58]:
def compute_loss(batch_X, batch_Y, lengths_X, model, optimizer=None, is_train=True):
    # 損失を計算する関数
    model.train(is_train)  # train/evalモードの切替え
    
    # 一定確率でTeacher Forcingを行う
    use_teacher_forcing = is_train and (random.random() < teacher_forcing_rate)
    max_length = batch_Y.size(0)
    # 推論
    pred_Y = model(batch_X, lengths_X, max_length, batch_Y, use_teacher_forcing)
    
    # 損失関数を計算
    loss = masked_cross_entropy(pred_Y.contiguous(), batch_Y.contiguous())
    
    if is_train:  # 訓練時はパラメータを更新
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    batch_Y = batch_Y.transpose(0, 1).contiguous().data.cpu().tolist()
    pred = pred_Y.max(dim=-1)[1].data.cpu().numpy().T.tolist()

    return loss.item(), batch_Y, pred

ここで、Loss以外に、学習の進捗を確認するためにモデルの性能を評価する指標として、BLEUを計算します。

BLEUは機械翻訳の分野において最も一般的な自動評価基準の一つで、予め用意した複数の参照訳と、機械翻訳モデルが出力した訳のn-gramのマッチ率に基づく指標です。

NLTK (Natural Language Tool Kit) という自然言語処理で用いられるライブラリを用いて簡単に計算することができます。

In [59]:
def calc_bleu(refs, hyps):
    """
    BLEUスコアを計算する関数
    :param refs: list, 参照訳。単語のリストのリスト (例: [['I', 'have', 'a', 'pen'], ...])
    :param hyps: list, モデルの生成した訳。単語のリストのリスト (例: ['I', 'have', 'a', 'pen'])
    :return: float, BLEUスコア(0~100)
    """
    refs = [[ref[:ref.index(EOS)]] for ref in refs] # EOSは評価しないで良いので切り捨てる, refsのほうは複数なのでlistが一個多くかかっている
    hyps = [hyp[:hyp.index(EOS)] if EOS in hyp else hyp for hyp in hyps]
    return 100 * bleu_score.corpus_bleu(refs, hyps)

それではモデルの訓練を行います。

In [60]:
# 訓練
best_valid_bleu = 0.

for epoch in range(1, num_epochs+1):
    train_loss = 0.
    train_refs = []
    train_hyps = []
    valid_loss = 0.
    valid_refs = []
    valid_hyps = []
    # train
    for batch in train_dataloader:
        batch_X, batch_Y, lengths_X = batch
        loss, gold, pred = compute_loss(
            batch_X, batch_Y, lengths_X, model, optimizer, 
            is_train=True
            )
        train_loss += loss
        train_refs += gold
        train_hyps += pred
    # valid
    for batch in valid_dataloader:
        batch_X, batch_Y, lengths_X = batch
        loss, gold, pred = compute_loss(
            batch_X, batch_Y, lengths_X, model, 
            is_train=False
            )
        valid_loss += loss
        valid_refs += gold
        valid_hyps += pred
    # 損失をサンプル数で割って正規化
    train_loss = np.sum(train_loss) / len(train_dataloader.data)
    valid_loss = np.sum(valid_loss) / len(valid_dataloader.data)
    # BLEUを計算
    train_bleu = calc_bleu(train_refs, train_hyps)
    valid_bleu = calc_bleu(valid_refs, valid_hyps)

    # validationデータでBLEUが改善した場合にはモデルを保存
    if valid_bleu > best_valid_bleu:
        ckpt = model.state_dict()
        torch.save(ckpt, ckpt_path)
        best_valid_bleu = valid_bleu

    print('Epoch {}: train_loss: {:5.2f}  train_bleu: {:2.2f}  valid_loss: {:5.2f}  valid_bleu: {:2.2f}'.format(
            epoch, train_loss, train_bleu, valid_loss, valid_bleu))
        
    print('-'*80)
Epoch 1: train_loss: 52.29  train_bleu: 3.47  valid_loss: 48.50  valid_bleu: 5.12
--------------------------------------------------------------------------------
Epoch 2: train_loss: 44.00  train_bleu: 8.25  valid_loss: 44.43  valid_bleu: 7.81
--------------------------------------------------------------------------------
Epoch 3: train_loss: 39.86  train_bleu: 11.58  valid_loss: 42.09  valid_bleu: 10.97
--------------------------------------------------------------------------------
Epoch 4: train_loss: 36.97  train_bleu: 14.56  valid_loss: 40.96  valid_bleu: 14.47
--------------------------------------------------------------------------------
Epoch 5: train_loss: 34.48  train_bleu: 17.43  valid_loss: 39.71  valid_bleu: 12.13
--------------------------------------------------------------------------------
Epoch 6: train_loss: 32.65  train_bleu: 19.62  valid_loss: 39.87  valid_bleu: 15.31
--------------------------------------------------------------------------------
Epoch 7: train_loss: 31.24  train_bleu: 21.52  valid_loss: 39.69  valid_bleu: 16.10
--------------------------------------------------------------------------------
Epoch 8: train_loss: 30.04  train_bleu: 23.10  valid_loss: 39.60  valid_bleu: 15.74
--------------------------------------------------------------------------------
Epoch 9: train_loss: 29.03  train_bleu: 24.53  valid_loss: 40.09  valid_bleu: 16.55
--------------------------------------------------------------------------------
Epoch 10: train_loss: 27.91  train_bleu: 26.39  valid_loss: 40.52  valid_bleu: 16.39
--------------------------------------------------------------------------------
In [61]:
! ls
data  images  model.pth  __pycache__  sample_data  utils.py  utils.py.1

5.評価

In [70]:
# 学習済みモデルの読み込み
ckpt = torch.load(ckpt_path) # cpuで処理する場合はmap_locationで指定する必要があります。
model.load_state_dict(ckpt)
model.eval()
Out[70]:
EncoderDecoder(
  (encoder): Encoder(
    (embedding): Embedding(3725, 256, padding_idx=0)
    (gru): GRU(256, 256)
  )
  (decoder): Decoder(
    (embedding): Embedding(4405, 256, padding_idx=0)
    (gru): GRU(256, 256)
    (out): Linear(in_features=256, out_features=4405, bias=True)
  )
)
In [63]:
def ids_to_sentence(vocab, ids):
    # IDのリストを単語のリストに変換する
    return [vocab.id2word[_id] for _id in ids]

def trim_eos(ids):
    # IDのリストからEOS以降の単語を除外する
    if EOS in ids:
        return ids[:ids.index(EOS)]
    else:
        return ids
In [71]:
# テストデータの読み込み
test_X = load_data('./data/dev.en')
test_Y = load_data('./data/dev.ja')
In [72]:
test_X = [sentence_to_ids(vocab_X, sentence) for sentence in test_X]
test_Y = [sentence_to_ids(vocab_Y, sentence) for sentence in test_Y]
In [73]:
test_dataloader = DataLoader(test_X, test_Y, batch_size=1, shuffle=False)
In [91]:
# 生成
batch_X, batch_Y, lengths_X = next(test_dataloader)
sentence_X = ' '.join(ids_to_sentence(vocab_X, batch_X.data.cpu().numpy()[:-1, 0]))
sentence_Y = ' '.join(ids_to_sentence(vocab_Y, batch_Y.data.cpu().numpy()[:-1, 0]))
print('src: {}'.format(sentence_X))
print('tgt: {}'.format(sentence_Y))

output = model(batch_X, lengths_X, max_length=20)
output = output.max(dim=-1)[1].view(-1).data.cpu().tolist()
output_sentence = ' '.join(ids_to_sentence(vocab_Y, trim_eos(output)))
#output_sentence_without_trim = ' '.join(ids_to_sentence(vocab_Y, output))
print('out: {}'.format(output_sentence))
#print('without trim: {}'.format(output_sentence_without_trim))
src: is there any hope of his success ?
tgt: 彼 の 成功 の 見込み は あ り ま す か 。
out: 彼 の 成功 は は あ あ り ま す か 。
In [80]:
# BLEUの計算
test_dataloader = DataLoader(test_X, test_Y, batch_size=1, shuffle=False)
refs_list = []
hyp_list = []

for batch in test_dataloader:
    batch_X, batch_Y, lengths_X = batch
    pred_Y = model(batch_X, lengths_X, max_length=20)
    pred = pred_Y.max(dim=-1)[1].view(-1).data.cpu().tolist()
    refs = batch_Y.view(-1).data.cpu().tolist()
    refs_list.append(refs)
    hyp_list.append(pred)
bleu = calc_bleu(refs_list, hyp_list)
print(bleu)
16.541129307108786