본 포스팅은 파이토치로 배우는 자연어 처리 (한빛미디어), 한국어 임베딩(에이콘), 머신러닝 교과서 with 파이썬, 사잇킷런, 텐서플로(길벗) 책 그리고 https://wikidocs.net/22886 참고하여 작성되었습니다.
이미지 자료https://github.com/gilbutITbook/080223/blob/master/ch16/
RNN에서 활성화함수로 tanh를 쓰는 이유 참고1 참고2
- 활성화 함수: 신경망 구조에서 입력값에 대해서 가중치를 곱한 후 적용하는 비선형 함수
- 사용 이유: 신경망 구조에서 비선형성을 추가해서 XOR 같은 비선형 문제를 해결하기 위해서(대다수의 문제가 비선형이어서 선형 함수로 해결할 수 없는 경우가 많다)
- 왜 tanh함수를 쓰는가? | “sigmoid에 비해 tanh 는 기울기가 0에서 1 사이이므로 Gradient Vanishing problem에 더 강하기 때문”
- sigmoid의 한계와 tanh의 등장
- sigmoid는 미분의 최대값은 0.25이기 때문에 깊어질 수록 vanishing 문제가 발생한다. 이를 해결하고자 Tanh를 만들었다. 이는 [-1,1]의 범위의 출력, 평균은 0으로 편향 이동 문제나 zig zag 현상을 해결하는데 쓰이는 방법이다. 더 나아가, 미분의 최대값이 1로 vanishing 문제를 해결한다.
- 그렇다면 Relu를 왜 안쓰는가?
- RNN은 이전 단계의 값을 가져와서 사용하므로, Relu를 쓰면 이전 값이 커짐에 따라 전체적인 출력이 발산할 수 있다. 따라서 RNN에서는 이를 normalizing하는 과정이 필요하며, sigmoid 보다 기울기의 역전파가 더 잘되는 tanh함수를 활성화 함수로 사용한다.
예시: 번역기
-> 입력: 번역하고자 하는 단어의 시퀀스인 문장
-> 출력: 해당되는 번역된 문장 또한 단어의 시퀀스
장점
단점
RNN의 그래디언트 배니싱 문제 참고
- RNN은 학습할 때, 가중치 매개변수의 기울기를 효율적으로 계산할 수 있는 오차 역전파법(Backpropagation)을 사용
- 역전파: 인공신경망을 학습하기 위한 알고리즘으로, 역방향으로 해당 함수의 국소적 미분을 곱해 나가는 방법(출력하고자 하는 값과 실제 모델이 계산한 값이 얼마나 차이가 나는지 계산한 후, 그 오차 값을 다시 전달하고, 각 노드가 가진 값을 업데이트 하기 위한 알고리즘)
- RNN의 경우 시간 방향으로 펼친 신경망의 역전파를 수행 => BPTT(BackPropagation Through Time)
- 문제점: 데이터의 시간 크기가 커지는 것에 비례하여 BPTT가 소비하는 컴퓨팅 자원이 증가해, 역전파 시의 기울기가 불안정해짐.
- 해결책: 큰 데이터를 다룰 때는 신경망 연결을 적당한 길이로 자름: 잘라낸 신경망에서 역전파를 수행하는 과정- Truncated-BPTT
참고하면 좋을 자료: https://blog.floydhub.com/a-beginners-guide-on-recurrent-neural-networks-with-pytorch/
class ElmanRNN(nn.Module):
""" RNNCell을 사용하여 만든 엘만 RNN """
def __init__(self, input_size, hidden_size, batch_first=False):
"""
매개변수:
input_size (int): 입력 벡터 크기
hidden_size (int): 은닉 상태 벡터 크기
batch_first (bool): 0번째 차원이 배치인지 여부(True로 설정시, 입력 텐서의 0번쨰와 1번째 차원을 바꿈)
"""
super(ElmanRNN, self).__init__()
self.rnn_cell = nn.RNNCell(input_size, hidden_size)
self.batch_first = batch_first
self.hidden_size = hidden_size
def _initial_hidden(self, batch_size):
return torch.zeros((batch_size, self.hidden_size))
def forward(self, x_in, initial_hidden=None):
""" ElmanRNN의 정방향 계산
매개변수:
x_in (torch.Tensor): 입력 데이터 텐서
If self.batch_first: x_in.shape = (batch_size, seq_size, feat_size)
Else: x_in.shape = (seq_size, batch_size, feat_size)
initial_hidden (torch.Tensor): RNN의 초기 은닉 상태
반환값:
hiddens (torch.Tensor): 각 타임 스텝에서 RNN 출력
If self.batch_first:
hiddens.shape = (batch_size, seq_size, hidden_size)
Else: hiddens.shape = (seq_size, batch_size, hidden_size)
"""
if self.batch_first:
batch_size, seq_size, feat_size = x_in.size()
x_in = x_in.permute(1, 0, 2)
else:
seq_size, batch_size, feat_size = x_in.size()
hiddens = []
if initial_hidden is None:
initial_hidden = self._initial_hidden(batch_size)
initial_hidden = initial_hidden.to(x_in.device)
hidden_t = initial_hidden
for t in range(seq_size):
hidden_t = self.rnn_cell(x_in[t], hidden_t)
hiddens.append(hidden_t)
hiddens = torch.stack(hiddens)
if self.batch_first:
hiddens = hiddens.permute(1, 0, 2)
return hiddens
class SurnameDataset(Dataset):
@classmethod
def load_dataset_and_make_vectorizer(cls, surname_csv):
"""데이터셋을 로드하고 새로운 Vectorizer 객체를 만듭니다
매개변수:
surname_csv (str): 데이터셋의 위치
반환값:
SurnameDataset의 객체
"""
surname_df = pd.read_csv(surname_csv)
train_surname_df = surname_df[surname_df.split=='train']
return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))
def __getitem__(self, index):
"""파이토치 데이터셋의 주요 진입 메서드
매개변수:
index (int): 데이터 포인트 인덱스
반환값:
다음 값을 담고 있는 딕셔너리:
특성 (x_data)
레이블 (y_target)
특성 길이 (x_length)
"""
row = self._target_df.iloc[index]
surname_vector, vec_length = \
self._vectorizer.vectorize(row.surname, self._max_seq_length)
nationality_index = \
self._vectorizer.nationality_vocab.lookup_token(row.nationality)
return {'x_data': surname_vector,
'y_target': nationality_index,
'x_length': vec_length}
class SurnameVectorizer(object): # 전체적인 벡터 변환 과정 수행
""" 어휘 사전을 생성하고 관리합니다 """
def vectorize(self, surname, vector_length=-1):
"""
매개변수:
title (str): 문자열
vector_length (int): 인덱스 벡터의 길이를 맞추기 위한 매개변수
"""
indices = [self.char_vocab.begin_seq_index]
indices.extend(self.char_vocab.lookup_token(token)
for token in surname)
indices.append(self.char_vocab.end_seq_index)
if vector_length < 0:
vector_length = len(indices)
out_vector = np.zeros(vector_length, dtype=np.int64)
out_vector[:len(indices)] = indices
out_vector[len(indices):] = self.char_vocab.mask_index
return out_vector, len(indices)
@classmethod
def from_dataframe(cls, surname_df):
"""데이터셋 데이터프레임으로 SurnameVectorizer 객체를 초기화합니다.
매개변수:
surname_df (pandas.DataFrame): 성씨 데이터셋
반환값:
SurnameVectorizer 객체
"""
char_vocab = SequenceVocabulary()
nationality_vocab = Vocabulary()
for index, row in surname_df.iterrows():
for char in row.surname:
char_vocab.add_token(char)
nationality_vocab.add_token(row.nationality)
return cls(char_vocab, nationality_vocab)
class SurnameClassifier(nn.Module):
""" RNN으로 특성을 추출하고 MLP로 분류하는 분류 모델 """
def __init__(self, embedding_size, num_embeddings, num_classes,
rnn_hidden_size, batch_first=True, padding_idx=0):
"""
매개변수:
embedding_size (int): 문자 임베딩의 크기
num_embeddings (int): 임베딩할 문자 개수
num_classes (int): 예측 벡터의 크기
노트: 국적 개수
rnn_hidden_size (int): RNN의 은닉 상태 크기
batch_first (bool): 입력 텐서의 0번째 차원이 배치인지 시퀀스인지 나타내는 플래그
padding_idx (int): 텐서 패딩을 위한 인덱스;
torch.nn.Embedding을 참고하세요
"""
super(SurnameClassifier, self).__init__()
self.emb = nn.Embedding(num_embeddings=num_embeddings,
embedding_dim=embedding_size,
padding_idx=padding_idx)
self.rnn = ElmanRNN(input_size=embedding_size,
hidden_size=rnn_hidden_size,
batch_first=batch_first)
self.fc1 = nn.Linear(in_features=rnn_hidden_size,
out_features=rnn_hidden_size)
self.fc2 = nn.Linear(in_features=rnn_hidden_size,
out_features=num_classes)
def forward(self, x_in, x_lengths=None, apply_softmax=False):
""" 분류기의 정방향 계산
매개변수:
x_in (torch.Tensor): 입력 데이터 텐서
x_in.shape는 (batch, input_dim)입니다
x_lengths (torch.Tensor): 배치에 있는 각 시퀀스의 길이
시퀀스의 마지막 벡터를 찾는데 사용합니다
apply_softmax (bool): 소프트맥스 활성화 함수를 위한 플래그
크로스-엔트로피 손실을 사용하려면 False로 지정합니다
반환값:
결과 텐서. tensor.shape는 (batch, output_dim)입니다.
"""
x_embedded = self.emb(x_in)
y_out = self.rnn(x_embedded)
if x_lengths is not None:
y_out = column_gather(y_out, x_lengths)
else:
y_out = y_out[:, -1, :]
y_out = F.relu(self.fc1(F.dropout(y_out, 0.5)))
y_out = self.fc2(F.dropout(y_out, 0.5))
if apply_softmax:
y_out = F.softmax(y_out, dim=1)
return y_out
def column_gather(y_out, x_lengths):
''' y_out에 있는 각 데이터 포인트에서 마지막 벡터 추출합니다
조금 더 구체적으로 말하면 배치 행 인덱스를 순회하면서
x_lengths에 있는 값에 해당하는 인덱스 위치의 벡터를 반환합니다.
매개변수:
y_out (torch.FloatTensor, torch.cuda.FloatTensor)
shape: (batch, sequence, feature)
x_lengths (torch.LongTensor, torch.cuda.LongTensor)
shape: (batch,)
반환값:
y_out (torch.FloatTensor, torch.cuda.FloatTensor)
shape: (batch, feature)
'''
x_lengths = x_lengths.long().detach().cpu().numpy() - 1
out = []
for batch_index, column_index in enumerate(x_lengths):
out.append(y_out[batch_index, column_index])
return torch.stack(out)