言語処理100本ノック2020 第10章 機械翻訳

はじめに

言語処理100本ノック2020

言語処理100本ノックは東北大学が公開している自然言語処理の問題集です。

とても良質なコンテンツで企業の研修や勉強会で使われています。

そんな言語処理100本ノックが2020年に改定されてました。昨今の状況を鑑みて、深層ニューラルネットワークに関する問題が追加されました。(その他にも細かい変更があります)

この記事では、言語処理100本ノック2020にPythonで取り組んでいきます。

他にも色々な解法があると思うので、一つの解答例としてご活用ください!

全100問の解説に戻る

90. データの準備

機械翻訳のデータセットをダウンロードせよ.訓練データ,開発データ,評価データを整形し,必要に応じてトークン化などの前処理を行うこと.ただし,この段階ではトークンの単位として形態素(日本語)および単語(英語)を採用せよ.

京都フリー翻訳タスク (KFTT)データセットからダウンロードし、展開する。

1
tar -zxvf kftt-data-1.0.tar.gz

ファイルは以下のような構成となっている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kftt-data-1.0/
kftt-data-1.0/data/
kftt-data-1.0/data/orig/
kftt-data-1.0/data/orig/kyoto-tune.en
kftt-data-1.0/data/orig/kyoto-dev.ja
kftt-data-1.0/data/orig/kyoto-dev.en
kftt-data-1.0/data/orig/kyoto-train.en
kftt-data-1.0/data/orig/kyoto-tune.ja
kftt-data-1.0/data/orig/kyoto-train.ja
kftt-data-1.0/data/orig/kyoto-test.ja
kftt-data-1.0/data/orig/kyoto-test.en
kftt-data-1.0/data/tok/
kftt-data-1.0/data/tok/kyoto-tune.en
kftt-data-1.0/data/tok/kyoto-dev.ja
kftt-data-1.0/data/tok/kyoto-train.cln.en
kftt-data-1.0/data/tok/kyoto-dev.en
kftt-data-1.0/data/tok/kyoto-train.en
kftt-data-1.0/data/tok/kyoto-tune.ja
kftt-data-1.0/data/tok/kyoto-train.cln.ja
kftt-data-1.0/data/tok/kyoto-train.ja
kftt-data-1.0/data/tok/kyoto-test.ja
kftt-data-1.0/data/tok/kyoto-test.en
kftt-data-1.0/README.txt
  • サブディレクトリtok/以下のファイルはトークン化されています。そのためこれをそのまま利用します
  • 重み学習(tune)は利用しません
  • train.clnは0単語の文や非常に長い(40単語以上)の文を除いたデータで、これを学習データとして利用します

つまり、以下のファイルを利用します。

1
2
3
4
5
6
kftt-data-1.0/data/tok/kyoto-train.cln.ja
kftt-data-1.0/data/tok/kyoto-train.cln.en
kftt-data-1.0/data/tok/kyoto-dev.ja
kftt-data-1.0/data/tok/kyoto-dev.en
kftt-data-1.0/data/tok/kyoto-test.ja
kftt-data-1.0/data/tok/kyoto-test.en

91. 機械翻訳モデルの訓練

90で準備したデータを用いて,ニューラル機械翻訳のモデルを学習せよ(ニューラルネットワークのモデルはTransformerやLSTMなど適当に選んでよい).
Attentionを導入したSeq2Seqモデルを学習させます。コードの大部分は、PyTorchの公式チュートリアルを利用しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
1
2
3
4
5
6
7
8
def normalizeString(s):
t = s
s = s.lower().strip()
s = re.sub(r"([.!?])", r" \1", s)
s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
if len(s.replace(' ','')): # ascii文字以外
return s
return t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def readLangs(lang1, lang2):
print("Reading lines...")
with open(lang1) as f:
lines1 = f.readlines()
with open(lang2) as f:
lines2 = f.readlines()
pairs = []
for l1,l2 in zip(lines1,lines2):
l1 = normalizeString(l1.rstrip('\n'))
l2 = normalizeString(l2.rstrip('\n'))
pairs.append([l1,l2])
input_lang = Lang(lang1)
output_lang = Lang(lang2)
return input_lang, output_lang, pairs
1
2
3
4
5
MAX_LENGTH = 10
def filterPair(p):
return len(p[0].split(' ')) < MAX_LENGTH and len(p[1].split(' ')) < MAX_LENGTH
def filterPairs(pairs):
return [pair for pair in pairs if filterPair(pair)]
1
2
train_ja = 'kftt-data-1.0/data/tok/kyoto-train.cln.ja'
train_en = 'kftt-data-1.0/data/tok/kyoto-train.cln.en'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

def prepareData(lang1, lang2):
input_lang, output_lang, pairs = readLangs(lang1, lang2)
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(train_ja, train_en)
print(random.choice(pairs))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def indexesFromSentence(lang, sentence):
return [lang.word2index[word] for word in sentence.split(' ')]


def tensorFromSentence(lang, sentence):
indexes = indexesFromSentence(lang, sentence)
indexes.append(EOS_token)
return torch.tensor(indexes, dtype=torch.long, device=device).view(-1, 1)


def tensorsFromPair(pair):
input_tensor = tensorFromSentence(input_lang, pair[0])
target_tensor = tensorFromSentence(output_lang, pair[1])
return (input_tensor, target_tensor)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
import math

def asMinutes(s):
m = math.floor(s / 60)
s -= m * 60
return '%dm %ds' % (m, s)

def timeSince(since, percent):
now = time.time()
s = now - since
es = s / (percent)
rs = es - s
return '%s (- %s)' % (asMinutes(s), asMinutes(rs))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100, learning_rate=0.01):
start = time.time()
plot_losses = []
print_loss_total = 0 # Reset every print_every
plot_loss_total = 0 # Reset every plot_every

encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
training_pairs = [tensorsFromPair(random.choice(pairs))
for i in range(n_iters)]
criterion = nn.NLLLoss()

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)
1
2
3
4
5
6
7
8
9
10
11
12
13
import matplotlib.pyplot as plt
plt.switch_backend('agg')
import matplotlib.ticker as ticker
import numpy as np


def showPlot(points):
plt.figure()
fig, ax = plt.subplots()
# this locator puts ticks at regular intervals
loc = ticker.MultipleLocator(base=0.2)
ax.yaxis.set_major_locator(loc)
plt.plot(points)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
with torch.no_grad():
input_tensor = tensorFromSentence(input_lang, sentence)
input_length = input_tensor.size()[0]
encoder_hidden = encoder.initHidden()

encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)

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) # SOS

decoder_hidden = encoder_hidden

decoded_words = []
decoder_attentions = torch.zeros(max_length, max_length)

for di in range(max_length):
decoder_output, decoder_hidden, decoder_attention = decoder(
decoder_input, decoder_hidden, encoder_outputs)
decoder_attentions[di] = decoder_attention.data
topv, topi = decoder_output.data.topk(1)
if topi.item() == EOS_token:
decoded_words.append('<EOS>')
break
else:
decoded_words.append(output_lang.index2word[topi.item()])

decoder_input = topi.squeeze().detach()

return decoded_words, decoder_attentions[:di + 1]
1
2
3
4
5
6
7
8
9
def evaluateRandomly(encoder, decoder, n=10):
for i in range(n):
pair = random.choice(pairs)
print('>', pair[0])
print('=', pair[1])
output_words, attentions = evaluate(encoder, decoder, pair[0])
output_sentence = ' '.join(output_words)
print('<', output_sentence)
print('')
1
2
3
4
5
hidden_size = 256
encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)
attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1).to(device)

trainIters(encoder1, attn_decoder1, 75000, print_every=5000)
1
evaluateRandomly(encoder1,attn_decoder,10)

92. 機械翻訳モデルの適用

91で学習したニューラル機械翻訳モデルを用い,与えられた(任意の)日本語の文を英語に翻訳するプログラムを実装せよ
単語分割をする必要があるので、MeCabを利用しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
!sudo apt-get install mecab mecab-ipadic-utf8 libmecab-dev swig
!pip install mecab-python3
import MeCab
def translate(s):
wakati = MeCab.Tagger("-Owakati")
seq = wakati.parse(s).rstrip()
seq = seq.split()
seq = [s for s in seq if s in input_lang.word2index]
seq = ' '.join(seq)
output_words, attentions = evaluate(encoder1, attn_decoder1, seq)
return ' '.join(output_words)
s = 'テスト'
print (translate(s))

93. BLEUスコアの計測

91で学習したニューラル機械翻訳モデルの品質を調べるため,評価データにおけるBLEUスコアを測定せよ.

nltkを利用します。

別の記事でBLEUスコアのスクラッチ実装しました。BLEUスコアとは? Pythonでのスクラッチ実装

1
2
3
4
5
6
7
8
9
10
11
12
13
import nltk
test_ja = 'kftt-data-1.0/data/tok/kyoto-test.ja'
test_en = 'kftt-data-1.0/data/tok/kyoto-test.en'
_, _, test_pairs = prepareData(test_ja, test_en)
sm = []
for p in test_pairs:
ref = p[1]
hyp = translate(p[0])
ref = ref.replace('.','')
hyp = hyp.replace('.','')
score = nltk.translate.bleu_score.sentence_bleu([ref.split()],hyp.split())
sm.append(score)
print (np.mean(sm))

最後に

全100問の解説に戻る

記事情報

  • 投稿日:2020年5月28日
  • 最終更新日:2020年6月7日