BIG_PY
home
Introduce
home

7장 시계열 분석 3, 4

시계열 분석

7.3 순환 신경망(RNN)

🥧
순환 신경망(RNN)
RNN은 시간적으로 연속성이 있는 데이터를 처리하려고 고안된 인공 신경망
RNN의 ‘Recurrent(반복되는)’는 이전 은닉층이 현재 은닉층의 입력이 되면서 반복되는 순환구조를 가진다는 의미임
RNN이 기존 네트워크와 다른 점은 ‘기억’을 갖는다는 것
기억은 현재까지 입력 데이터를 요약한 정보이며 새로운 입력이 네트워크로 들어올 때마다 기억은 조금씩 수정되며, 결국 최종적으로 남겨진 기억은 모든 입력 전체를 요약한 정보가 됨
→ 첫 번째 입력(x1)이 들어오면 첫 번째 기억(h1)이 만들어지고, 두 번째 입력(x2)이 들어오면 기존 기억(h1)과 새로운 입력을 참고하여 새 기억(h2)을 만듦
즉, RNN은 외부 입력과 자신의 이전 상태를 입력 받아 현재 상태를 갱신함
RNN 유형
1.
일대일 : 순환이 없기 때문에 RNN이라고 말하기 어려우며, 순방향 네트워크가 대표적 사례
2.
일대다 : 입력이 하나이고, 출력이 다수인 구조로 이미지를 입력해서 이미지에 대한 설명을 문장으로 출력하는 이미지 캡션이 대표적 사례
3.
다대일 : 입력이 다수이고 출력이 하나인 구조로 문장을 입력해서 긍정/부정을 출력하는 감성 분석기에서 사용됨
다대일 구조에 층을 쌓아 올리면 다음과 같은 적층된 구조를 가질 수 있음
4.
다대다 : 입력과 출력이 다수인 구조로, 언어를 번역하는 자동 번역기 등이 대표적 사례
5.
동기화 다대다 : 입력과 출력이 다수인 구조로 문장에서 다음에 나올 단어를 예측하는 언어 모델, 즉 프레임 수준의 비디오 분류가 대표적 사례
RNN 계층과 셀
RNN은 내장된 계층뿐만 아니라 셀 레벨의 API(운영체제가 제공하는 함수의 집합체)도 제공함
RNN 계층이 입력된 배치 순서대로 모두 처리하는 것과 다르게 RNN 셀은 오직 하나의 단계만 처리함
RNN 셀은 RNN 계층의 for loop 구문을 갖는 구조라고 할 수 있음
RNN 계층은 셀을 래핑하여 동일한 셀을 여러 단계에 적용함
셀은 실제 계산에 사용되는 RNN 계층의 구성 요소로, 단일 입력과 과거 상태를 가져와서 새로운 상태를 생성함
셀 유형
-nn.RNNCell : SimpleRNN 층에 대응되는 RNN 셀
-nn.GRUCell : GRU 계층에 대응되는 GRU 셀
-nn.LSTMCell : LSTM 계층에 대응되는 LSTM 셀
RNN 계층과 셀을 분리해서 설명하는 이유?
→ 둘을 분리해야 구현이 가능하기 때문
RNN의 활용 분야는 대표적으로 ‘자연어 처리’가 있음
연속적인 단어들의 나열인 자연어 처리는 음성 인식, 단어의 의미 판단 및 대화 등에 대한 처리가 가능함
이외에도 손글씨, 센서 데이터 등 시계열 데이터 처리에 활용됨

7.4 RNN 구조

RNN은 은닉층 노드들이 연결되어 이전 단계 정보를 은닉층 노드에 저장할 수 있도록 구성한 신경망
xt-1에서 ht-1을 얻고 다음 단계에서 ht-1과 xt를 사용하여 과거 정보와 현재 정보를 모두 반영함
또한, ht와 xt+1의 정보를 이용하여 과거와 현재 정보를 반복해서 반영하는데, 이러한 구조를 요약한 것이 아래의 그림과 같음
RNN에서는 입력층, 은닉층, 출력층 외에 가중치 세 개를 가짐
RNN의 가중치는 Wxh, Whh, Why로 분류됨
Wxh는 입력층에서 은닉층으로 전달되는 가중치
Whh는 t 시점의 은닉층에서 t+1 시점의 은닉층으로 전달되는 가중치
Why는 은닉층에서 출력층으로 전달되는 가중치
가중치 Wxh, Whh, Why는 모든 시점에 동일하다는 것에 주의할 필요가 있음
t 단계에서의 RNN 계산
1.
은닉층 계산을 위해 xt, ht-1이 필요함
RNN에서 은닉층은 일반적으로 하이퍼볼릭 탄젠트 활성화 함수를 사용함
2.
출력층은 심층 신경망과 계산 방법이 동일함
즉, 소프트맥스 함수를 적용함
3.
RNN의 오차는 심층 신경망에서 전방향 학습과 달리 각 단계(t)마다 오차를 측정함
즉, 단계마다 실제 값과 예측 값으로 오차(평균 제곱 오차)를 이용하여 측정함
4.
RNN에서 역전파는 BPTT를 이용하여 모든 단계마다 처음부터 끝까지 역전파함
오차는 각 단계(t)마다 오차를 측정하고 이전 단계로 전달되는데, 이것을 BPTT(오류 역전파)라고 함
오차를 이용하여 Wxh, Whh, Why 및 바이어스를 업데이트하는데 BPTT는 오차가 멀리 전파될 때 계산량이 많아지고 전파되는 양이 점차 적어지는 문제점(기울기 소멸 문제)이 발생함
기울기 소멸 문제를 보완하기 위해 오차를 몇 단계까지만 전파시키는 생략된-BPTT를 사용할 수도 있고, 근본적으로 LSTM 및 GRU를 많이 사용함
IMDB 데이터셋을 사용하여 RNN 계층과 셀 구현
RNN 셀 구현
# 라이브러리 호출 import torch import torchtext import numpy as np import torch.nn as nn import torch.nn.functional as F import time
Python
복사
# 데이터 전처리 start=time.time() TEXT = torchtext.legacy.data.Field(lower=True, fix_length=200, batch_first=False) LABEL = torchtext.legacy.data.Field(sequential=False) # 레이블에 대한 전처리
Python
복사
# 데이터셋 준비 from torchtext.legacy import datasets train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
Python
복사
데이터셋을 분리하면 훈련 데이터셋 2만 5000개, 테스트 데이터셋이 2만 5000개가 됨
# 훈련 데이터셋 내용 확인 print(vars(train_data.examples[0])) # 데이터셋의 내용을 보고자 할 때는 examples 사용
Python
복사
# 데이터셋 전처리 적용 import string for example in train_data.examples: text = [x.lower() for x in vars(example)['text']] # 소문자로 변경 text = [x.replace("<br","") for x in text] # "<br"을 ""(공백)으로 변경 text = [''.join(c for c in s if c not in string.punctuation) for s in text] # 구두점 제거 text = [s for s in text if s] # 공백 제거 vars(example)['text'] = text for example in test_data.examples: text = [x.lower() for x in vars(example)['text']] text = [x.replace("<br","") for x in text] text = [''.join(c for c in s if c not in string.punctuation) for s in text] text = [s for s in text if s] vars(example)['text'] = text
Python
복사
# 훈련과 검증 데이터셋 분리 import random train_data, valid_data = train_data.split(random_state = random.seed(0), split_ratio=0.8)
Python
복사
# 데이터셋 개수 확인 print(f'Number of training examples: {len(train_data)}') print(f'Number of validation examples: {len(valid_data)}') print(f'Number of testing examples: {len(test_data)}') Number of training examples: 20000 Number of validation examples: 5000 Number of testing examples: 25000
Python
복사
훈련 데이터셋 2만 5000개를 훈련과 검증 용도로 분리했기 때문에 훈련 데이터셋은 2만 개, 검증 데이터셋은 5000개, 테스트 데이터셋은 2만 5000개가 됨
단어 집합은 IMDB 데이터셋에 포함된 단어들을 이용하여 하나의 딕셔너리와 같은 집합을 만드는 것
단어들의 중복은 제거된 상태에서 진행함
# 단어 집합 만들기 TEXT.build_vocab(train_data, max_size=10000, min_freq=10, vectors=None) LABEL.build_vocab(train_data) print(f"Unique tokens in TEXT vocabulary: {len(TEXT.vocab)}") print(f"Unique tokens in LABEL vocabulary: {len(LABEL.vocab)}") Unique tokens in TEXT vocabulary: 10002 Unique tokens in LABEL vocabulary: 3
Python
복사
TEXT는 10002개, LABEL은 세 개의 단어로 구성됨
LABEL은 긍정과 부정 두 개의 값만 있어야 할 것 같은 세 개가 있다고 출력됨
→ 현재 단어 집합의 단어와 그것에 부여된 고유 정수(인덱스)를 확인
# 테스트 데이터셋의 단어 집합 확인 print(LABEL.vocab.stoi)
Python
복사
확인 결과 긍정, 부정 외에 <unk>가 있음
<unk>는 사전에 없는 단어를 의미함 따라서 예제에서 사용하는 것은 pos, neg가 될 것임
# 데이터셋 메모리로 가져오기 BATCH_SIZE = 64 device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') embeding_dim = 100 # 각 단어를 100차원으로 조정(임베딩 계층을 통과한 후 각 벡터의 크기) hidden_size = 300 # 은닉층의 유닛 개수를 300개로 지정함 train_iterator, valid_iterator, test_iterator = torchtext.legacy.data.BucketIterator.splits( (train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device) # BucketIterator는 데이터로와 쓰임새가 같고 # 배치 크기 단위로 값을 차례대로 꺼내어 메모리로 가져오고 싶을 때 사용함
Python
복사
임베딩 처리를 진행함
# 워드 임베딩 처리 및 RNN 셀 정의 class RNNCell_Encoder(nn.Module): def __init__(self, input_dim, hidden_size): # RNN 셀 구현을 위한 구문 super(RNNCell_Encoder, self).__init__() self.rnn = nn.RNNCell(input_dim, hidden_size) # inputs는 입력 시퀀스로(시퀀스 길이, 배치, 임베딩)의 형태를 가짐 def forward(self, inputs): bz = inputs.shape[1] # 배치를 가져옴 ht = torch.zeros((bz, hidden_size)).to(device) # 배치와 은닉층 뉴런의 크기를 0으로 초기화 for word in inputs: ht = self.rnn(word, ht) # 재귀적으로 발생하는 상태 값을 처리하기 위한 구문 return ht class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.em = nn.Embedding(len(TEXT.vocab.stoi), embeding_dim)# 임베팅 처리를 위한 구문 self.rnn = RNNCell_Encoder(embeding_dim, hidden_size) self.fc1 = nn.Linear(hidden_size, 256) self.fc2 = nn.Linear(256, 3) def forward(self, x): x = self.em(x) x = self.rnn(x) x = F.relu(self.fc1(x)) x = self.fc2(x) return x
Python
복사
옵티마이저와 손실 함수 정의
# 옵티마이저와 손실 함수 정의 model = Net() # model이라는 이름으로 모델을 객체화 model.to(device) loss_fn = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)
Python
복사
모델 학습을 위한 함수 생성
# 모델 학습을 위한 함수 정의 def training(epoch, model, trainloader, validloader): correct = 0 total = 0 running_loss = 0 model.train() for b in trainloader: x, y = b.text, b.label # trainloader에서 text와 label을 꺼내 옴 x, y = x.to(device), y.to(device) # CPU 사용할 수 있도록 장치 지정 y_pred = model(x) loss = loss_fn(y_pred, y) # CrossEntropyLoss 손실 함수를 이용하여 오차 계산 optimizer.zero_grad() loss.backward() optimizer.step() with torch.no_grad(): y_pred = torch.argmax(y_pred, dim=1) correct += (y_pred == y).sum().item() total += y.size(0) running_loss += loss.item() # 누적된 오차를 전체 데이터셋으로 나누어서 에포크 단계마다 오차를 구함 epoch_loss = running_loss / len(trainloader.dataset) epoch_acc = correct / total valid_correct = 0 valid_total = 0 valid_running_loss = 0 model.eval() with torch.no_grad(): for b in validloader: x, y = b.text, b.label x, y = x.to(device), y.to(device) y_pred = model(x) loss = loss_fn(y_pred, y) y_pred = torch.argmax(y_pred, dim=1) valid_correct += (y_pred == y).sum().item() valid_total += y.size(0) valid_running_loss += loss.item() epoch_valid_loss = valid_running_loss / len(validloader.dataset) epoch_valid_acc = valid_correct / valid_total print('epoch: ', epoch, 'loss: ', round(epoch_loss, 3), 'accuracy:', round(epoch_acc, 3), 'valid_loss: ', round(epoch_valid_loss, 3), 'valid_accuracy:', round(epoch_valid_acc, 3) ) # 훈련이 진행될 때 에포크마다 정확도와 오차를 출력 return epoch_loss, epoch_acc, epoch_valid_loss, epoch_valid_acc
Python
복사
zero_grad()의 의미가 뭔지, 왜 사용하는지 알아보자
보통 딥러닝에서는 미니 배치+루프 조합을 사용해서 parameter들을 업데이트하는데,
한 루프에서 업데이트를 위해 loss.backward()를 호출하면 각 파라미터들의 .grad 값에 변화도가 저장이 된다.
이후 다음 루프에서 zero_grad()를 하지 않고 역전파를 시키면 이전 루프에서 .grad에 저장된 값이 다음 루프의 업데이트에도 간섭을 해서 원하는 방향으로 학습이 안된다고 한다.
따라서 루프가 한번 돌고 나서 역전파를 하기 전에 반드시 zero_grad()로 .grad 값들을 0으로 초기화시킨 후 학습을 진행해야 한다.
훈련과 검증 데이터셋을 이용한 모델 학습 진행함
# 모델 학습 epochs = 5 train_loss = [] train_acc = [] valid_loss = [] valid_acc = [] for epoch in range(epochs): epoch_loss, epoch_acc, epoch_valid_loss, epoch_valid_acc = training(epoch, model, train_iterator, valid_iterator) train_loss.append(epoch_loss) # 훈련 데이터셋을 모델에 적용했을 때의 오차 train_acc.append(epoch_acc) # 훈련 데이터셋을 모델에 적용했을 때의 정확도 valid_loss.append(epoch_valid_loss) # 검증 데이터셋을 모델에 적용했을 때의 오차 valid_acc.append(epoch_valid_acc) # 검증 데이터셋을 모델에 적용했을 때의 정확도 end = time.time() print(end-start) epoch: 0 loss: 0.011 accuracy: 0.495 valid_loss: 0.011 valid_accuracy: 0.507 epoch: 1 loss: 0.011 accuracy: 0.503 valid_loss: 0.011 valid_accuracy: 0.495 epoch: 2 loss: 0.011 accuracy: 0.508 valid_loss: 0.011 valid_accuracy: 0.494 epoch: 3 loss: 0.011 accuracy: 0.513 valid_loss: 0.011 valid_accuracy: 0.499 epoch: 4 loss: 0.011 accuracy: 0.521 valid_loss: 0.011 valid_accuracy: 0.5 212.57630896568298
Python
복사
에포크를 5로 지정해서 정확도가 낮지만 학습과 검증 데이터셋에 대한 오차가 유사하므로 과적합을 발생하지 않고 있음
테스트 데이터셋에 대해서는 어떤 결과가 나타날지 알아보자
# 모델 예측 함수 정의 def evaluate(epoch, model, testloader): test_correct = 0 test_total = 0 test_running_loss = 0 model.eval() with torch.no_grad(): for b in testloader: x, y = b.text, b.label x, y = x.to(device), y.to(device) y_pred = model(x) loss = loss_fn(y_pred, y) y_pred = torch.argmax(y_pred, dim=1) test_correct += (y_pred == y).sum().item() test_total += y.size(0) test_running_loss += loss.item() epoch_test_loss = test_running_loss / len(testloader.dataset) epoch_test_acc = test_correct / test_total print('epoch: ', epoch, 'test_loss: ', round(epoch_test_loss, 3), 'test_accuracy:', round(epoch_test_acc, 3) ) return epoch_test_loss, epoch_test_acc
Python
복사
# 모델 예측 결과 확인 epochs = 5 test_loss = [] test_acc = [] for epoch in range(epochs): epoch_test_loss, epoch_test_acc = evaluate(epoch, model, test_iterator) test_loss.append(epoch_test_loss) test_acc.append(epoch_test_acc) end = time.time() print(end-start) epoch: 0 test_loss: 0.011 test_accuracy: 0.5 epoch: 1 test_loss: 0.011 test_accuracy: 0.5 epoch: 2 test_loss: 0.011 test_accuracy: 0.5 epoch: 3 test_loss: 0.011 test_accuracy: 0.5 epoch: 4 test_loss: 0.011 test_accuracy: 0.5 353.44589710235596
Python
복사
테스트 데이터셋도 검증 데이터셋의 평가 결과와 크게 다르지 않음
RNN 계층 구현
# 라이브러리 호출 import torch import torchtext import numpy as np import torch.nn as nn import torch.nn.functional as F import time
Python
복사
# 데이터셋 내려받기 및 전처리 start=time.time() TEXT = torchtext.legacy.data.Field(sequential = True, batch_first = True, lower = True) LABEL = torchtext.legacy.data.Field(sequential = False, batch_first = True) from torchtext.legacy import datasets train_data, test_data = datasets.IMDB.splits(TEXT, LABEL) # IMDB 사용 train_data, valid_data = train_data.split(split_ratio = 0.8) TEXT.build_vocab(train_data, max_size=10000, min_freq=10, vectors=None) LABEL.build_vocab(train_data) BATCH_SIZE = 100 device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
Python
복사
BucketIterator()를 이용하여 훈련, 검증, 테스트 데이터셋으로 분리
# 데이터셋 분리 train_iterator, valid_iterator, test_iterator = torchtext.legacy.data.BucketIterator.splits( (train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device)
Python
복사
# 변수 값 지정 vocab_size = len(TEXT.vocab) # 텍스트 길이 지정 n_classes = 2 # pos(긍정), neg(부정) 지정
Python
복사
# RNN 계층 네트워크 class BasicRNN(nn.Module): def __init__(self, n_layers, hidden_dim, n_vocab, embed_dim, n_classes, dropout_p = 0.2): super(BasicRNN, self).__init__() self.n_layers = n_layers # RNN 계층에 대한 개수 self.embed = nn.Embedding(n_vocab, embed_dim) # 워드 임베딩 적용 self.hidden_dim = hidden_dim self.dropout = nn.Dropout(dropout_p) # 드롭아웃 적용 # RNN 계층에 대한 구문 self.rnn = nn.RNN(embed_dim, self.hidden_dim, num_layers = self.n_layers, batch_first = True) self.out = nn.Linear(self.hidden_dim, n_classes) def forward(self, x): x = self.embed(x) # 문자를 숫자/벡터로 변환 h_0 = self._init_state(batch_size = x.size(0)) # 최초 은닉 상태의 값을 0으로 초기화 x, _ = self.rnn(x, h_0) # RNN 계층을 의미하며, 파라미터로 입력과 이전 은닉 상태의 값을 받음 h_t = x[:, -1, :] # 모든 네트워크를 거쳐서 가장 마지막에 나온 단어의 임베딩 값 self.dropout(h_t) logit = torch.sigmoid(self.out(h_t)) return logit def _init_state(self, batch_size = 1): weight = next(self.parameters()).data # 모델의 파라미터 값을 가져와서 weigh 변수에 저장 return weight.new(self.n_layers, batch_size, self.hidden_dim).zero_() # 크기가 (계층의 개수, 배치 크기, 은닉층의 뉴런/유닛 개수)인 은닉 상태를 생성하여 0으로 초기화한 후 반환
Python
복사
모델에서 사용할 손실 함수와 옵티마이저 설정
# 손실 함수와 옵티마이저 설정 model = BasicRNN(n_layers = 1, hidden_dim = 256, n_vocab = vocab_size, embed_dim = 128, n_classes = n_classes, dropout_p = 0.5) model.to(device) loss_fn = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)
Python
복사
훈련 데이터셋을 이용하여 모델을 학습시키기 위한 함수
# 모델 학습 함수 def train(model, optimizer, train_iter): model.train() for b, batch in enumerate(train_iter): x, y = batch.text.to(device), batch.label.to(device) y.data.sub_(1) # 뺄셈에 대한 함수로 _은 inplace 연산을 의미함 # IMDB 레이블의 경우 긍정은 2, 부정은 1의 값을 가짐 optimizer.zero_grad() logit = model(x) loss = F.cross_entropy(logit, y) loss.backward() optimizer.step() if b % 50 == 0: # 훈련 데이터셋의 개수를 50으로 나누어서 나머지가 0이면 출력 print("Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}".format(e, b * len(x), len(train_iter.dataset), 100. * b / len(train_iter), loss.item()))
Python
복사
검증과 테스트 데이터셋을 모델에 적용하여 결과를 예측하기 위한 함수
# 모델 평가 함수 def evaluate(model, val_iter): model.eval() corrects, total, total_loss = 0, 0, 0 for batch in val_iter: x, y = batch.text.to(device), batch.label.to(device) y.data.sub_(1) logit = model(x) loss = F.cross_entropy(logit, y, reduction = "sum") total += y.size(0) total_loss += loss.item() corrects += (logit.max(1)[1].view(y.size()).data == y.data).sum() # 모델의 정확도 avg_loss = total_loss / len(val_iter.dataset) avg_accuracy = corrects / total return avg_loss, avg_accuracy
Python
복사
훈련 데이터셋을 이용하여 모델을 학습시키고 검증 데이터셋을 이용하여 성능(정확도)를 확인함
# 모델 학습 및 평가 BATCH_SIZE = 100 LR = 0.001 EPOCHS = 5 for e in range(1, EPOCHS + 1): train(model, optimizer, train_iterator) val_loss, val_accuracy = evaluate(model, valid_iterator) print("[EPOCH: %d], Validation Loss: %5.2f | Validation Accuracy: %5.2f" % (e, val_loss, val_accuracy))
Python
복사
→ 검증 데이터셋을 모델에 적용한 결과 50%의 정확도를 보이고 있음
높은 예측력이라고 볼 수 없음
테스트 데이터셋을 모델에 적용하여 성능 확인
# 테스트 데이터셋을 이용한 모델 예측 test_loss, test_acc = evaluate(model,test_iterator) print("Test Loss: %5.2f | Test Accuracy: %5.2f" % (test_loss, test_acc)) Test Loss: 0.69 | Test Accuracy: 0.54
Python
복사
→ 테스트 데이터셋에 대해 56%의 정확도를 보이고 있음
여전히 모델 성능이 좋다고 할 수 없고 이런 경우 다른 모델로 변경하여 테스트를 진행해 보아야 함