Pytorch를 활용한 RNN


김성동님의 Pytorch를 활용한 딥러닝 입문 중 RNN 파트 정리입니다.

Language Modeling

철수와 영희는 식탁에 앉아 사과를 __(A)__
  • (A)에 들어올 단어는? 먹었다!

  • 이런 아이디어 기반해서 만들어진 것이 Language Model

\[P(x^{(t+1)}=w_j|x^{(t)}, ...,x^{(1)})\]
  • 키보드의 자동 완성 기능, 서치 엔진에서 쿼리 자동 완성 기능 모두 Language Model을 적용한 Application으로 볼 수 있음
  • 음성 인식을 할 때도 Language Model이 쓰임! 해당 단어만 잘못 들었을 경우(noise가 껴있다거나) 이전까지 인지한 단어를 기반으로 단어를 추론!
  • N-gram으로 모델링을 합니다. gram은 gramma의 줄임말
철수와 영희는 식탁에 앉아 사과를 ______
  • Unigram : 토큰 하나가 변수가 됨 : “철수”, “와”, “영희”, “는”, “식탁”, “에”
  • Bigram : 두개의 토큰이 하나의 변수가 됨 : “철수 와”, “와 영희”, “영희 는”, “는 식탁”, “식탁 에”, “에 앉아”
  • Trigram : 3개의 토큰이 하나의 변수 : “철수 와 영희”, “와 영희 는”, “영희 는 식탁”, “는 식탁 에”, “식탁 에 앉아”
  • 4-gram : 4개의 토큰 : “철수 와 영희 는”, “와 영희 는 식탁”, “영희 는 식탁 에”, “는 식탁 에 앉아”
  • N-gram : N개의 토큰이 하나의 변수가 됨

가정 : t+1의 확률은 이전 n-1개의 단어(토큰)에만 의존한다

문제점

  • 앞의 정보를 무시하고 있음. 가정 자체에 한계 존재
  • n-1 이전의 맥락을 모델링할 수 없음
  • 해당 n-gram이 Corpus에 없거나 희소한 경우 확률이 0이나 매우 낮게 나올 수도 있음
  • n이 커질수록 더욱 확률은 희박해짐
  • Corpus에 있는 n-gram을 모두 카운트해서 저장해야 하기 때문에 모델의 공간 복잡도가 O(exp(n))
  • 위 문제점 때문에 Neural Language Model로 접근하기 시작함

Window-based Language Model

  • 고정된 Window size(~n-1)를 인풋으로 받는 FFN(Feed Forward Neural Network)
  • 카운트할 필요가 없기 때문에 Sparsitiy 문제 없음
  • 모델의 사이즈도 작음
  • 여전히 고정된 window size에 의존하기 때문에 Long-term Context를 포착하지 못함
  • 토큰의 윈도우 내의 위치에 따라 다른 파라미터를 적용 받음(Parameter sharing이 없음)

Recurrent Neural Network

  • 모든 Timestamp에서 같은 Parameter를 공유!
  • Input의 길이가 가변적입니다

  • time t의 hidden state는 이전 모든 time step x를 인풋으로 받는 함수 g의 아웃풋으로 볼 수 있습니다(모두 연결되어 있으니까-!)

Notation

  • 인풋의 차원에 대한 감이 있어야 합니다!
  • x는 word vector
  • 모든 Time Step에서 Parameter를 Sharing!

RNN 참고 자료

예시

뭐 먹 을까?

D=3로 총 4개의 Timestamp가 있어서 4x3 매트릭스를 indexing했습니다

0.10.10.2
0.50.20.3
1.00.00.2
0.10.10.

첫 행이 \(h^{(0)}\)!

  • 마지막 step의 Hidden state는 “뭐 먹을까?”라는 문장을 인코딩한 벡터로 볼 수 있습니다
input_size = 10 # input dimension (word embedding) D
hidden_size = 30 # hidden dimension H
batch_size = 3
length = 4

rnn = nn.RNN(input_size, hidden_size,num_layers=1,bias=True,nonlinearity='tanh', batch_first=True, dropout=0, bidirectional=False)

input = Variable(torch.randn(batch_size,length,input_size)) # B,T,D  <= batch_first
hidden = Variable(torch.zeros(1,batch_size,hidden_size)) # 1,B,H    (num_layers * num_directions, batch, hidden_size)

output, hiddne = rnn(input, hidden) 
output.size() # B, T, H   
hidden.size() # 1, B, H
# (배치 사이즈, 시퀀스 길이, input 차원)을 가지는 Input 
# (1,배치 사이즈, hidden 차원)을 가지는 초기 hidden state

나는 너 좋아
오늘 뭐 먹지
  • Batch Size, 2
  • Time : 문장의 길이, 3
  • Dimension : 인풋의 차원, 10
  • Style마다 먼저 쓰는 것이 다른데, B, T, D로 많이 사용하곤 함 (batch_first=Ture)
  • hidden은 마지막 hidden state를 뜻합니다

하이퍼 파라미터 세팅

  • ?에 들어갈 것은 무엇일까요?
    \(E\) : VxD
    \(W_e\) : DxH
    \(W_h\) : HxH
    \(U\) : HxV

  • 모든 timestep에서 그 다음에 올 단어를 예측하고 그 오차를 Cross Entropy로 구하면 됩니다!

TorchText

  • 링크
  • Tokenize, Vocab 구축, Tensor로 감싸주는 프로세스등을 진행할 수 있습니다

  • Field
    • 데이터 전처리 파이프라인을 정의하는 클래스
    • Tokensize, Unkwown 태그, Vocab 구축, 문장에서 숫자는 Num이란 태그로 대체 등등의 과정을 파이프라인이라고 할 수 있는데, 이것을 정의하는 클래스

Code

# 1. Field 선언
tagger = Kkma()
tokenize = tagger.morphs

TEXT = Field(tokenize=tokenize,use_vocab=True,lower=True, include_lengths=True, batch_first=True) 
# tokenize는 함수!, lower는 대문자를 소문자로 바꿔줌, include_lengths는 input을 (input, length)로 쪼개줌
LABEL = Field(sequential=False,unk_token=None, use_vocab=True)
# sequential이 true면 토크나이즈를 함

# 2. 데이터셋 로드
train_data, test_data = TabularDataset.splits(
                                   path="data/", # 데이터가 있는 root 경로
                                   train='train.txt', validation="test.txt",
                                   format='tsv', # \t로 구분
                                   #skip_header=True, # 헤더가 있다면 스킵
                                   fields=[('TEXT',TEXT),('LABEL',LABEL)])
# TabularDataset은 csv, tsv 포맷을 갖는 데이터셋 

# 꺼내오고 싶다면
one_example = train_data.examples[0]
one_example.TEXT
one_example.LABEL

# 3. Vocabulary 구축
TEXT.build_vocab(train_data)
LABEL.build_vocab(train_data)

TEXT.vocab.itos
  
# 4. iterator 선언
train_iter, test_iter = Iterator.splits(
    (train_data, test_data), batch_size=3, device=-1, # device -1 : cpu, device 0 : 남는 gpu
    sort_key=lambda x: len(x.TEXT),sort_within_batch=True,repeat=False) # x.TEXT 길이 기준으로 정렬

TEXT.vocab.itos[1]

for batch in train_iter:
    print(batch.TEXT)
    print(batch.LABEL)
    break

# 5. 모델링
  
  • <pad> token
    • 길이를 맞춰주기 위한 padding 토큰

Backpropagation for RNN

  • 최대한 간단한 RNN. \(h^{(t)} = W_hh^{(t-1)}\)
  • 타입 스텝마다 \(J^{(t)}/W_h\) 미분한 것을 더하면 됩니다

LSTM(Long Short Term Memory)

  • 기본 RNN은 Timestamp이 엄청 길면 vanish gradient가 생기고 hidden size를 고정하기 때문에 많은 step을 거쳐오면 정보가 점점 희소해집니다
  • 이것을 극복하기 위해 만들어진 LSTM
  • 긴 Short term Memory
  • hidden state말고 cell state라는 정보도 time step 마다 recurrent!

Forget Gate

  • 이번 시점의 인풋 \(x_t\)와 이전까지의 hidden state \(h_{t-1}\)을 인풋으로 받아서 Cell state 중 잊어버릴 부분을 결정합니다

Input Gate

  • time step t의 새로운 정보 중 얼마나 Cell state에 반영할지 결정 하는 Input gate

  • 기존의 Cell state에 까먹을만큼 까먹고,새로운 정보를 받아들일만큼 받아들인다

Output Gate

  • Cell state에 tanh한 결과 정보 중 얼마만큼의 비율로 이번 hidden state로 만들지 정하는 Output gate

Shortcut connection

  • NN에서 하나 이상의 layer를 skip하는 구조
  • ResNet에서 실험한 여러 Shortcut connection 중 exclusive gating과 같은 아이디어
  • Cell state에 여러 gate function을 사용하여 해당 layer의 연산을 거치지 않은 Information이 계속 그 다음 step으로 전달 될 수 있습니다

  • 전부 곱셈으로 이루어져있는 RNN의 해당 부분(이전 Hidden state에서 다음 Hidden state로 Recurrent하는)을 gate function을 이용해 덧셈으로 대체합니다!

Code

input_size = 10
hidden_size = 30
output_size = 10
batch_size = 3
length = 4
num_layers = 3

rnn = nn.LSTM(input_size,hidden_size,num_layers=num_layers,bias=True,batch_first=True,bidirectional=True)

input = Variable(torch.randn(batch_size,length,input_size)) # B,T,D
hidden = Variable(torch.zeros(num_layers*2,batch_size,hidden_size)) # (num_layers * num_directions, batch, hidden_size)
cell = Variable(torch.zeros(num_layers*2,batch_size,hidden_size)) # (num_layers * num_directions, batch, hidden_size)

output, (hidden,cell) = rnn(input,(hidden,cell))

print(output.size())
print(hidden.size())
print(cell.size())


linear = nn.Linear(hidden_size*2,output_size)
output = F.softmax(linear(output),1)
output.size()


GRU(Gated Recurrent Unit)

  • 조경현 박사님이 제안한 구조
  • LSTM과 유사하게 생겼는데, LSTM을 더 간략화한 구조
  • hidden state만 흘러가고 cell state는 없음
  • Update gate는 이번 step에서 계산한 hidden을 얼마나 update할지 결정한다. (update 되는만큼 기존의 정보를 잊는다.)
    • LSTM의 forget, input gate를 하나의 Update gate로!
  • 만약 z가 0이라면 이번 step의 히든 스테이트는 이전 레이어의 히든 스테이트를 그대로 Copy합니다(identity mapping)

Code

input_size = 10 # input dimension (word embedding) D
hidden_size = 30 # hidden dimension H
batch_size = 3
length = 4

rnn = nn.GRU(input_size,hidden_size,num_layers=1,bias=True,batch_first=True,bidirectional=True)
input = Variable(torch.randn(batch_size,length,input_size)) # B,T,D
hidden = Variable(torch.zeros(2,batch_size,hidden_size)) # 2,B,H

output, hidden = rnn(input,hidden)

print(output.size())
print(hidden.size())

Bidirectional RNN

  • 인풋 시퀀스를 양방향(forward, backward)으로 연결하며 hidden state를 계산
  • 2개의 hidden state가 필요

  • RNN에선 레이어를 많이 쌓는다고 반드시 좋아지는 것은 아닙니다!

Standard From(RNN)

class RNN(nn.Module):
    def __init__(self,input_size,embed_size,hidden_size,output_size,num_layers=1,bidirec=False):
        super(RNN,self).__init__()
        
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        if bidirec:
            self.num_directions = 2
        else:
            self.num_directions = 1
            
        self.embed = nn.Embedding(input_size,embed_size)
        self.lstm = nn.LSTM(embed_size,hidden_size,num_layers,batch_first=True,bidirectional=bidirec)
        self.linear = nn.Linear(hidden_size*self.num_directions,output_size)
        
    def init_hidden(self,batch_size):
        # (num_layers * num_directions, batch_size, hidden_size)
        hidden = Variable(torch.zeros(self.num_layers*self.num_directions,batch_size,self.hidden_size))
        cell = Variable(torch.zeros(self.num_layers*self.num_directions,batch_size,self.hidden_size))
        return hidden, cell
    
    def forward(self,inputs):
        """
        inputs : B,T
        """
        embed = self.embed(inputs) # word vector indexing
        hidden, cell = self.init_hidden(inputs.size(0)) # initial hidden,cell
        
        output, (hidden,cell) = self.lstm(embed,(hidden,cell))
        
        # Many-to-Many
        output = self.linear(output) # B,T,H -> B,T,V
        
        # Many-to-One
        #hidden = hidden[-self.num_directions:] # (num_directions,B,H)
        #hidden = torch.cat([h for h in hidden],1)
        #output = self.linear(hidden) # last hidden
        
        return output

Dropout / Layer Normalization

  • Recurrent connection(가로 방향)에는 Dropout을 적용하지 않고, 나머지 connection(세로 방향)에만 Dropout을 적용합니다!!
  • Recurrent Connection에 Dropout을 적용하면 과거의 정보까지 잃어버리게 되기 때문입니다-

Layer Normalization

  • RNN에선 레이어 노말라이제이션이 표준이 되가고 있습니다
  • Batch와는 독립적으로 Layer의 Output 자체를 Normalization합니다. (Batch size의 의존성 X)
  • LSTM, GRU에 적용하는 것은 복잡할 수 있지만, RNN에 적용한다면!

  • Pytorch에서는 0.4 버전부터 정식으로 사용 가능할 것으로 예상됩니다(현재 0.3.1)

Sequence Tagging

  • 연속된 시퀀스에 태그를 다는 테스크
  • POS tagging, NER, SRL
  • Text 분류는 many to one
  • Language Model, Sequence Tagging은 many to many

Named Entity Recognition(NER)

  • 엔티티의 이름을 인지
  • 보통 2개 이상의 토큰이 하나의 Entity를 구성
    • B : Entity의 시작
    • I : B로 시작한 Entity에 속함
    • O : Entity가 아님

카일스쿨 유튜브 채널을 만들었습니다. 데이터 사이언스, 성장, 리더십, BigQuery 등을 이야기할 예정이니, 관심 있으시면 구독 부탁드립니다 :)

PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다

이 글이 도움이 되셨거나 다양한 의견이 있다면 댓글 부탁드립니다 :)

Buy me a coffeeBuy me a coffee





© 2017. by Seongyun Byeon

Powered by zzsza