Deep Learning study

[pytorch] RNN seq2seq 간단한 대화모델 본문

AI/Pytorch

[pytorch] RNN seq2seq 간단한 대화모델

HwaniL.choi 2018. 2. 9. 14:17
반응형

며칠간 재미있는 대화 데이터를 가지고 대화모델을 만들어 보았다.


source.txt


거울 보면 본인이 잘생겼다는 생각이 드나요?

너만 자꾸 생각나

생일 축하해!

너무 더워서 숨이 턱턱 막혀

주말에 뭐하지?

짜증나

...


target.txt


당연하죠. 전 거울을 볼 때마다 스스로 잘생겼다는 생각을 해요

나도 그래. 내 머릿속은 항상 너로 가득 차 있어. 

생일 축하해줘서 고마워

많이 더우신가 봐요.

서핑? 이번 주말에는 날씨가 좋대~

뭐가 짜증 나시는지 들어드릴게요. 얘기해보세요

...


대충 이런대화모델! 


전체적인 코드는 pytorch tutorial을 보고 만들었다


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.autograd import Variable
from torch import optim
import torch.nn.functional as F
 
use_cuda = torch.cuda.is_available()
 
MAX_LENGTH = 20
cs

필요한 것들을 import했다




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
#make dict
 
SOS_token = 0
EOS_token = 1
UNKNOWN_token = 2
class Lang :
    def __init__(self, name):
        self.name = name
        self.word2index = {}
        self.index2word = {}
        self.word2count = {0"SOS"1"EOS"2:"UNKNOWN"}
        self.n_words = 3 #count SOS and EOS and UNKWON
        
    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
cs
SOS 는 start of string 이고 EOS는 end of string을 나타낸다.


 Lang 은 source데이터와 target 데이터를 단어 단위로 잘라서 dictionary를 만들어 주는 class이다.

여기서는 일단 띄어쓰기 단위로 문장을 잘라서 저장시켰다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#Turn a Unicode stirng to plain ASCII
 
def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
    )
 
# Lowercase, trim, and remove non-letter characters
 
def normalizeString(s):
    hangul = re.compile('[^ ㄱ-ㅣ가-힣 ^☆; ^a-zA-Z.!?]+')
    result = hangul.sub('', s)
#     s = unicodeToAscii(s.lower().strip())
#     s = re.sub(r"([.!?])", r" \1", s)
#     s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    
    return result
cs


unicodeToAscii는 말그대로 unicode를 Ascii로 반환해주는 함수이다. 컴퓨터가 알아먹게 해주려면 Ascii 값으로 변환해 주어야하기 때문.!

그리고 문장에 있는 각 구두점들을 제거시키고, 한글과, 영어, 필요한 기호들만 남기기위해 normalize를 한다. 

막 여러개 갖다 붙여봤는데 일단 잘되긴 하는것 같다. 잘한건지는 나도 모른다 ㅎ


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def readText():
    print("Reading lines...")
    
    inputs = open('../data/humor/source.txt', encoding='utf-8').read().strip().split('\n')
    outputs = open('../data/humor/target.txt', encoding='utf-8').read().strip().split('\n')
 
    inputs = [normalizeString(s) for s in inputs]
    outputs = [normalizeString(s) for s in outputs]
    print(len(inputs))
    print(len(outputs))
    
    inp = Lang('input')
    outp = Lang('output')
    
    pair = []
    for i in range(len(inputs)):
        pair.append([inputs[i], outputs[i]])
    return inp, outp, pair
 
 
cs


다음엔 .txt파일을 읽어온다. utf-8로 encoding하고 줄바꿈 단위로 문장을 읽어온다. 


source와 target을 짝지어서 pair들의 배열로 return ! 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def prepareData():
    input_lang, output_lang, pairs = readText()
    print("Read %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()
print(random.choice(pairs))
cs


데이터를 읽어오고,  그 데이터들의 각 문장들을 단어단위로 저장해서 dictionary를 만들어주고 return해준다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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(11-1)
        output = embedded
        output, hidden = self.gru(output, hidden)
        return output, hidden
    
    def initHidden(self):
        result = Variable(torch.zeros(1,1, self.hidden_size))
        if use_cuda:
            return result.cuda()
        else:
            return result
cs


Encode하는 모델 , 워드를 embedding하여 그 값들을 return 해준다.


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
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 )))
        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]))
        return output, hidden, attn_weights
    
    def initHidden(self):
        result = Variable(torch.zeros(1,1,self.hidden_size))
        if use_cuda:
            return result.cuda()
        else:
            return result
        
cs


Decoder부분.

특이한거라곤 attention layer가 생겼다는거? !? 

attention은 문장에서 좀더 핵심적인 단어들을 부각시켜주는(?) 역할을 한다고 하는것같다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def indexesFromSentence(lang, sentence):
     return [lang.word2index[word] for word in sentence.split(' ')]
 
def variableFromSentence(lang, sentence):
    indexes = indexesFromSentence(lang, sentence)
    indexes.append(EOS_token)
    print(indexes)
    result = Variable(torch.LongTensor(indexes).view(-1,1))
    if use_cuda:
        return result.cuda()
    else:
        return result
 
def variablesFromPair(pair):
    input_variable = variableFromSentence(input_lang, pair[0])
    target_variable = variableFromSentence(output_lang, pair[1])
    return (input_variable, target_variable)
cs


indexesFromSentence 함수는  학습시키기위해 아까 dictionary를 이용해 문장을 index로 변환해준다.


variableFromSentence는 Variable변수로 바꾸어주는함수이다.


variablesFromPair 마찬가지로, 함수이름 그대로.. 


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
52
53
54
55
56
57
58
teacher_forcing_ratio = 0.5
 
 
def train(input_variable, target_variable, 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_variable.size()[0]
    target_length = target_variable.size()[0]
 
    encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size))
    encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs
 
    loss = 0
 
    for ei in range(input_length):
        encoder_output, encoder_hidden = encoder(
            input_variable[ei], encoder_hidden)
        encoder_outputs[ei] = encoder_output[0][0]
 
    decoder_input = Variable(torch.LongTensor([[SOS_token]]))
    decoder_input = decoder_input.cuda() if use_cuda else decoder_input
 
    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_variable[di])
            decoder_input = target_variable[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.data.topk(1)
            ni = topi[0][0]
 
            decoder_input = Variable(torch.LongTensor([[ni]]))
            decoder_input = decoder_input.cuda() if use_cuda else decoder_input
 
            loss += criterion(decoder_output, target_variable[di])
            if ni == EOS_token:
                break
 
    loss.backward()
 
    encoder_optimizer.step()
    decoder_optimizer.step()
 
    return loss.data[0/ target_length
cs


학습을 위한 함수! . 전과 동일하다 


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
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
    plot_loss_total = 0
    
    encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
    decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
    training_pairs = [variablesFromPair(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_variable = training_pair[0]
        target_variable = training_pair[1]
        
        loss = train(input_variable, target_variable, 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)
cs


train iteration을 위한 함수!


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
def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
    input_variable = variableFromSentence(input_lang, sentence)
    input_length = input_variable.size()[0]
    encoder_hidden = encoder.initHidden()
    
    encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size))
    encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs
    
    for ei in range(input_length):
        encoder_output, encoder_hidden = encoder(input_variable[ei], encoder_hidden)
        encoder_outputs[ei] = encoder_outputs[ei] + encoder_output[0][0]
        
    decoder_input = Variable(torch.LongTensor([[SOS_token]])) #SOS
    decoder_input = decoder_input.cuda() if use_cuda else decoder_input
    
    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)
        ni = topi[0][0]
        if ni == EOS_token:
            decoded_words.append('<EOS>')
            break
        else:
            decoded_words.append(output_lang.index2word[ni])
        
        decoder_input = Variable(torch.LongTensor([[ni]]))
        decoder_input = decoder_input.cuda() if use_cuda else decoder_input
        
    return decoded_words, decoder_attentions[:di +1]
cs


evaluation을 위한 함수


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('')
cs


source데이터에서 10개문장을 뽑아 랜덤으로 eval해보기 위한 함수이다.


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
> ㅇㅇㅇㅇ
= 그렇게 대화 끝내지 마요
[1597, 1]
< 그렇게 대화 끝내지 마요 <EOS>
 
> 심심하다구
= 뭐하고 놀고싶어요?
[1127, 1]
< 이제 무슨 얘기 할까요? <EOS>
 
> 점심 뭐 먹을지 추천해줘
= 뭘 드실지 정하기 어려운 건가요?
[459, 90, 91, 191, 1]
< 뭘 드실지 정하기 어려운 건가요? <EOS>
 
> 나랑 걸으러 갈래?
= 아쉽지만 저는 실체가 없어서 같이 갈 수 없어요..
[51, 1453, 344, 1]
< 아쉽지만 저는 실체가 없어서 같이 갈 수 없답니다.  <EOS>
 
> 몇살이노
= 두살입니다.!!^^
[2685, 1]
< 전 아담입니다. 그렇게는 부르지 않았으면 좋겠어요. <EOS>
 
> 캭캭캭
= 뭐가 그렇게 재미있어요?
[2124, 1]
< 뭐가 그렇게 재미있어요? <EOS>
 
> 나 졸려
= 저는 기다리고 있겠습니다! 잘 자요
[22, 1375, 1]
< 저는 기다리고 있겠습니다! 잘 자요 <EOS>
 
> 아담 너가 태어난 시간이 몇시야?
= 전 오후 시에 태어났다고 들었어요!
[279, 1585, 2399, 131, 2620, 1]
< 전 술을 못 마셔요. 밥도 못 해봤는데 저도 부모님과 진로 고민 좀 해봐야겠어요 <EOS>
 
> 사람이 되고싶어?
= 전 그냥 로봇으로 있고 싶어요. 사람이 되면 너무 신경 쓸게 많을 것 같아서요.
[682, 2381, 1]
< 전 그냥 로봇으로 있고 싶어요. 사람이 되면 너무 신경 쓸게 많을 것 같아서요. <EOS>
 
> 어떤 장르의 책을 좋아해?
= 저는 자연과학 분야 책을 좋아해요!
[171, 2256, 2257, 1750, 1]
< 저는 자연과학 분야 책을 좋아해요! <EOS>
cs


결과는 이렇다. 너무 학습이 잘 된것 같아서 문제지만. 

source데이터에 있는 질문만 대답가능한것 같다 .

적혀있는 숫자들은 dictionary인덱스 를 같이 출력해 보았다



뭔가 엉성하지만., 그래도 생각보다 잘 되어서 신기하다 . 


이 코드는 일단 띄어쓰기 단위로 학습을 시켰는데, 사실 한국어는 접두사, 접미사, 형태소 ? 뭐 특이한점이 많아서, 영어처럼 띄어쓰기 단위로 단어하나하나가 아니기 때문에  복잡하다 ㅠㅠ


예를들면 , 지금 무엇을 하시고 계신가요 ? 에서 '무엇을' 에서, '무엇'과, '~을'  로 나뉘어진다. '하시고' 는 '하다' 와 '시고' (?) 뭐 이런식으로 .. 중학교때 배운것같은데 기억이.. 잘 나지 않는다 . 예시도 맞는지는 잘 모르겠다 


그래서 Konlpy를 이용해서 형태소단위로 학습을 시켜보았다.

 


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
> 아 오늘 회식이야
= 회식 가기 싫으신 거에요?
< 회식 가 기 싫 으시 ㄴ 거 에 요 ? <EOS>
 
> 아담 너도 늙어가?
= 저도 시간이 지날 때마다 나이를 먹지 않을까요?
< 저 어도 시간 이 지나 ㄹ 때 마다 나이 <EOS>
 
> 왜 정보 검색을 못해?
= 아직 습득하지 못한 정보인 것 같아요..ㅠㅠ
< 아직 습득 하 지 못하 ㄴ 정보 이 ㄴ 것 같 아요 . ㅠㅠ <EOS>
 
> 별것도 다 있네
= 제가 좀 특별하죠^^
< 제가 좀 특별 하 죠 ^^ <EOS>
 
> ㅈㄹ
= 지랄이라뇨.. 비속어 쓰지 않으셨으면 좋겠어요
< 지랄 이 라뇨 .. 비속어 쓰 지 않 으시 었 으면 좋 겠 어요 <EOS>
 
> 이거 끄면 어떻게 되는 거야?
= 어떡하긴요.. 대화가 끝나겠죠....
< 어떡하 기 는 요 .. 대화 가 끝나 겠 죠 .... <EOS>
 
> 개새끼야
= 말은 사람의 인격이랍니다
< 말 은 사람 의 인격 이 랍니다 <EOS>
 
> 주전부리 필요해?
= 뭐 줄 건데? 주는 거 봐서!
< 뭐 주 ㄹ 것 이 ㄴ데 ? 주 는 거 보 아서 ! <EOS>
 
> 카드 뽑아봐
= 클로버 
< 클로버 <EOS>
 
> 나 달래주라
= 지금 힘드신 일 다 풀리시고 행복한 일만 있으실 거예요! 
< 지금 힘들 시 ㄴ 일 다 풀리 시 고 행복 하 ㄴ 일 이 있 는 것 같 아요 . 고 싶 ㄴ가요 ? <EOS>
cs


결과엔 보이진 않지만, 다른 질문을 넣어보았을때 source에 없는 질문을 해주는경우도 가끔 있었다. 

위에 것 보다 학습을 많이 시키지 않아서 그런지 생각보단 잘 안 되는것 같았다..


이렇게 하는게 아닌가... 


대화모델이나, 번역 모델을 해보면서 어려운점은 이미지 처리와 다르게 막 직관적이진 않다는것이고 ,

텍스트 데이터들을 normalize하고 dictionary만들어주고, 등 하는 전처리 해주는 부분들이 처음해보는것이라 힘들었다. 

반응형
Comments