시계열 데이터 딥 러닝 모델
RNN 파라미터 설명
- batch size:
- 한 번의 순전파동안 처리되는 시퀀스 묶음의 개수
- 만약 batch_size=3 이라면, 한 번에 3 개의 시퀀스 묶음을 입력으로 처리
- seqence length:
- 각 시퀀스가 몇 개의 타임스텝으로 이루어져있는지 나타내는 길이. 즉, 한 시퀀스 안에 몇 개의 입력이 있는지 나타내는 길이
- 만약 seq_len=5 라면, 각 시퀀스는 5 개의 타임스텝으로 구성
- ex. 하나의 문장이 5 개의 단어로 이루어져 있다면, 그 문장의 시퀀스 길이는 5
- input size:
- 각 타임스텝에서 입력되는 데이터의 특성 (feature) 수
- 만약 input_size=4 라면, 각 타임스텝에서 4 개의 특성이 입력
- ex. 온도, 습도, 바람 속도, 기압 네 가지 특성을 포함한 날씨 데이터라면, input_size=4
- hidden size:
- RNN 의 hidden state 벡터의 크기. 즉, RNN 모듈의 출력 벡터의 크기
- output size:
- 출력층의 노드 수. 즉, 전체 모델이 예측하려는 클래스의 수
- ex. 이진 분류 문제라면 두 개의 클래스를 예측하므로 output_size=2
- number of layers:
- hidden layer 의 개수
- 만약 num_layers=3 이라면 input layer 와 output layer 를 제외하고 hidden layer 의 개수가 3
- number of directions:
- RNN 모듈의 방향의 개수로, 단방향 RNN 일 경우 1, 양방향 RNN 일 경우 2
- 만약 bidirectional=True (Default 는 False) 라면 num_directions=2
영화 리뷰 감정 분석 (긍정/부정)
- batch_size: 3 개의 리뷰를 동시에 처리한다고 가정 (batch_size=3)
- seq_len: 각 리뷰는 5 개의 단어로 구성 (seq_len=5)
- input_size: 각 단어는 10 차원의 임베딩 벡터로 표현 (input_size=10)
입력 텐서의 형태: (3, 5, 10)
Batch 1: [ [단어1의 임베딩], [단어2의 임베딩], [단어3의 임베딩], [단어4의 임베딩], [단어5의 임베딩] ]
Batch 2: [ [단어1의 임베딩], [단어2의 임베딩], [단어3의 임베딩], [단어4의 임베딩], [단어5의 임베딩] ]
Batch 3: [ [단어1의 임베딩], [단어2의 임베딩], [단어3의 임베딩], [단어4의 임베딩], [단어5의 임베딩] ]
각 배치는 5 개의 단어로 이루어져 있고, 각 단어는 10 차원의 벡터로 표현되므로 (3, 5, 10) 의 형태
torch.nn.RNN(input_size, hidden_size)
이 함수의 파라미터는 input_size 와 hidden_size 를 지정하며 seq_len, batch_size 는 입력 데이터 텐서의 차원에서 결정됩니다.
- 입력 데이터 텐서의 차원:
- (seq_len, batch_size, input_size) (batch_first=False 지정할 것, Default)
- (batch_size, seq_len, input_size) (batch_first=True 지정할 것)
# 입력 텐서 예시: (batch_size=5, seq_len=7, input_size=10)
x = torch.randn(5, 7, 10)
# nn.RNN 정의
rnn = nn.RNN(input_size=10, hidden_size=20, batch_first=True)
# RNN 통과
output, hidden = rnn(x)
- output:
- 최종 layer 의 모든 타임스텝, 모든 방향에서의 출력
- 출력 크기는 (batch_size, seq_len, num_directions * hidden_size) 로 (5, 7, 20) (batch_first=True)
output, hidden = rnn(x)
출력을 output 과 hidden 으로 나눈 이유는 RNN 의 복잡한 구조를 효율적으로 다루기 위한 설계적 선택 때문입니다.
- output:
- 최종 layer 의 모든 타임스텝, 모든 방향의 hidden state 를 저장한 텐서
- (batch_size, seq_len, num_directions * hidden_size) (batch_first=True 인 경우)
- (forward t1, backward t1), (forward t2, backward t2), (forward t3, backward t3), (forward t4, backward t4), ... 이런 순서로 concat 돼서 저장
Forward 는 t1 → t2 → t3 → t4, Backward 는 t4 → t3 → t2 → t1 방향으로 진행하지만 Output 텐서에는 (forward_t1, backward_t1), (forward_t2, backward_t2), ... 이런 순서로 값이 들어가게 됩니다.
- hidden:
- 모든 레이어의 모든 방향, 마지막 타임스텝의 hidden state 를 저장한 텐서
- (num_layers * num_directions, batch_size, hidden_size) (batch_first 여부에 상관 없이)
- 각 레이어마다 forward 와 backward 방향의 마지막 hidden state 를 따로 반환
- ex.:
- 0 번째: 첫 번째 레이어 (시작 층) 의 forward 방향 마지막 hidden state
- 1 번째: 첫 번째 레이어의 backward 방향 마지막 hidden state
- 2 번째: 두 번째 레이어의 forward 방향 마지막 hidden state
- 3 번째: 두 번째 레이어의 backward 방향 마지막 hidden state
torch.nn.RNN.forward(x, hx=None)
- 초기 hidden state (hx) 제공하지 않으면 함수 내부적으로 자동으로 torch.zeros(num_layers, batch_size, hidden_size) 로 초기화
- PyTorch 에서는 기본적으로 h0 를 0 으로 초기화하지만, 0 이 아닌 값을 사용하고 싶다면 직접 생성한 h0 값을 rnn(x, h_0) 처럼 전달하면 됨
- 초기 hidden state (hx) 의 모양은 (num_layers * num_directions, batch_size, hidden_size) 로 제공할 것
- batch_size=1 인 경우에도 shape 을 (num_layers * num_directions, 1, hidden_size) 로 맞춰야 함
- 입력 (x) 과 초기 hidden state (hx) 만 제공하면 (제공하지 않으면 자동으로 초기화) 이후 모든 연산은 자동으로 처리
RNN 의 출력
보통 RNN 의 출력을 활용하기 위해 추가적인 output layer 를 사용합니다. 예를 들어, FC layer 를 통해 최종 출력 크기를 조정하거나, Softmax 등을 사용해 분류 결과를 얻습니다.
- RNN 의 본질적인 출력은 각 타임스텝의 hidden state 입니다. 따라서 모든 타임스텝에 대해 hidden state 가 출력되고, 그 모양은 (batch_size, seq_len, hidden_size) 가 됩니다. (batch_first=True 인 경우)
- 추가적인 output layer 를 통해 원하는 출력 모양으로 변환합니다. 예를 들어, 다대일 (Many-to-One) 모델에서는 마지막 타임스텝의 hidden state 만 사용하고, 이를 FC layer 에 전달해 최종 출력 (batch_size, output_size) 를 생성합니다. (batch_first=True 인 경우)
일대다 (One-to-Many) vs 다대일 (Many-to-One) vs 다대다 (Many-to-Many)
일대다 (One-to-Many), 다대일 (Many-to-One), 다대다 (Many-to-Many) 모델의 차이는 어떤 타임스텝의 출력을 사용하는지에 달려 있습니다.
- 일대다 (One-to-Many)
- 목적: 하나의 입력으로 여러 개의 출력 생성 (ex. 입력 단어 하나로부터 다음 여러 단어 생성, 이미지 캡셔닝)
- 방식:
- 학습:
- 하나의 입력을 주고 해당 입력의 출력을 다음 타임 스텝의 입력으로 주는 방식으로 출력을 반복해서 사용. 하지만 해당 방식은 모델이 학습 초기에는 올바른 시퀀스를 생성하지 못하기 때문에, 학습이 더 어려워질 수 있어 선호되지 않으며, Teacher Forcing 방식을 사용하는것이 일반적
- 문장 생성 모델에서 첫 단어부터 무작위로 문장 생성을 원할 경우 <SOS> 와 같은 토큰을 학습 및 테스트 시에 사용하며, 그렇지 않은 경우 첫 입력부터 일반적인 단어를 입력으로 제공
- 이미지 캡셔닝에서 학습시에는 <SOS> 및 <EOS> 토큰을 사용하지만 테스트 시에는 이미지 임베딩 벡터만 입력으로 넣어줄 뿐 <SOS> 토큰은 넣지 않는것이 일반적. 학습시 계속해서 첫 생성 단어로 <SOS> 토큰이 나오도록 학습 되어있기 때문에 테스트의 결과물로는 첫 출력은 항상 <SOS> 토큰이 나올 확률이 높음
- 테스트:
- 하나의 입력을 제공하면 해당 입력의 출력을 다음 타임스텝의 입력으로 제공하여 새로운 출력을 만들고 해당 출력을 또 다시 다음 타임스텝의 입력으로 제공하여 새로운 출력을 만드는 완전히 Autoregressive 한 방식으로 출력 시퀀스를 생성
- 학습:
Teacher Forcing 은 모델이 이전 타임스텝의 예측값이 아닌 정답 레이블 (ground truth) 을 다음 입력으로 사용하는 학습 기법으로 모델이 빠르게 수렴하게 됩니다. 대부분의 일대다 모델 학습의 경우 Teacher Forcing 기법을 사용하지만 너무 많이 사용하게 될 경우 Exposure Bias 문제 (학습 시에는 정답을 입력으로 사용하지만, 테스트 시에는 모델의 예측을 입력으로 사용해야 하는 차이로 인해 발생하는 문제. 대표적인 문제의 예로 출력이 한 번 잘못되면 이후 입력이 점점 훈련 데이터와 달라지면서 오류가 누적되는 현상 발생) 가 발생할 수 있습니다. 이를 방지하기 위해 Scheduled Sampling (SS) (일부 step 에서만 Teacher Forcing 을 사용하고 나머지에서는 모델의 출력을 사용하는 방식. 처음에는 GT 를 주로 사용하고, 학습이 진행될수록 모델 출력을 점점 더 사용) 을 적용하기도 합니다. 출력 시퀀스가 입력 시퀀스보다 한 타임스텝 뒤로 밀려있는 형태의 데이터 셋은 Teacher Forcing 을 사용할 때의 학습 데이터셋에서 자주 보이는 형태입니다. (ex. 입력 ["A", "B", "C"] 와 출력 ["B", "C", "D"]) Teacher Forcing 은 원칙적으로 Autoregressive 모델에서만 사용되는 기법입니다. 왜냐하면 Teacher Forcing 의 핵심 개념 자체가 이전 출력값을 다음 입력으로 사용하는 Autoregressive 구조에서 발생하는 문제를 해결하기 위한 방법이기 때문입니다.
Autoregressive 모델은 이전 시점의 output 을 다음 시점의 input 으로 사용하는 모델입니다. 일반적인 시계열 모델은 이전 시점의 hidden state 을 다음 시점으로 전달합니다. 하지만 이러한 hidden state 의 전달과 Autoregressive 모델은 별개의 내용입니다. Autoregressive 모델은 hidden state 의 전달이 아닌 이전 시점의 output 을 다음 시점의 input 으로 사용하는것과 관련한 내용입니다. 따라서 모든 시계열 모델이 Autoregressive 모델인 것은 아닙니다. (품사 태깅 모델의 경우 이전 시점의 output 이 다음 시점의 input 으로 사용되는 형태가 아닌 각 시점의 입력이 처음부터 모두 주어져 있는 형태이기 때문에 non-autoregressive 모델) Autoregressive 모델은 훈련 시와 테스트 시 모두 같은 방식으로 작동해야하지만 훈련 시에는 Teacher Forcing 을 적용할 수 있습니다.
# 일대다: 하나의 입력으로 시퀀스 출력
outputs = []
hidden = initial_hidden # 초기 hidden state
input = first_input # 첫 입력
for t in range(seq_len):
output, hidden = rnn(input, hidden) # (1, hidden_size), (1, hidden_size)
output = self.fc(output) # (1, hidden_size) -> (1, output_size)
outputs.append(out)
outputs = torch.stack(outputs, dim=1) # 최종 출력: (1, seq_len, output_size)
- 다대일 (Many-to-One)
- 목적: 입력 시퀀스 전체에 대한 정보를 바탕으로 하나의 출력 생성 (ex. 문장 전체 감정 분류)
- 방식: 모든 타임스텝의 hidden state 이 나오지만 학습 및 테스트 모두 마지막 타임스텝의 hidden state 만 사용하여 최종 결과 예측
- 출력: output, hidden = model(x) 인 경우
- 다중 레이어 단방향: hidden[-1] 혹은 output[:, -1, :]
- 다중 레이어 양방향: torch.cat((h_forward, h_backward), dim=-1) 혹은 torch.cat((output_final_forward, output_final_backward), dim=-1)
- h_forward = hidden[-2]
- h_backward = hidden[-1]
- output_final_forward = output[:, -1, :hidden_size]
- output_final_backward = output[:, 0, hidden_size:]
- hidden 을 활용하는 게 더 일반적이고 깔끔하며, output 은 더 구체적인 시퀀스 정보를 다루거나 attention 메커니즘에서 많이 사용됨
# 다대일: 마지막 타임스텝의 출력만 사용
self.fc = nn.Linear(hidden_size, output_size) # FC layer 정의
output = self.fc(output[:, -1, :]) # (batch_size, hidden_size) -> (batch_size, output_size)
- 다대다 (Many-to-Many)
- 목적: 입력 시퀀스의 각 타임스텝에 대해 각각의 출력을 생성 (ex. 기계 번역)
- 방식: 모든 타임스텝의 hidden state 를 활용
# 다대다: 모든 타임스텝의 출력을 사용
self.fc = nn.Linear(hidden_size, output_size) # FC layer 정의
output = self.fc(output) # (batch_size, seq_len, hidden_size) -> (batch_size, seq_len, output_size)
시퀀스 길이에 따른 학습 및 테스트 시 성능 영향
- 시퀀스 길이가 길수록 학습 난이도 증가
- 기울기 소실 (Vanishing Gradient): 시퀀스 길이가 길어지면 기울기 소실 문제로 인해 RNN / LSTM / GRU 가 장기 의존성 (Long-term Dependency) 을 학습하기 어려워짐
- 기울기 폭주 (Exploding Gradient): 시퀀스 길이가 길어지면 기울기 폭주 문제로 인해 학습이 불안정해짐
- 메모리 문제: 시퀀스가 길어질수록 GPU 메모리 사용량이 증가하여 학습 속도가 느려짐
- 학습 시 사용한 시퀀스 길이보다 더 긴 문장을 생성하면 모델 성능이 하락할 가능성 높음
- 모델이 학습한 길이 범위를 넘어가면 언어 패턴을 잘 유지하지 못하고 엉뚱한 문장을 생성할 가능성이 커짐
- 해결책:
- Curriculum Learning (짧은 시퀀스부터 점진적으로 긴 시퀀스를 학습)
- Beam Search, Sampling 기법 활용
장기 의존성 (Long-term Dependency): 시퀀스 데이터에서 초반 타임스텝의 정보가 후반 타임스텝의 예측에 영향을 주는 현상을 의미합니다. 예를 들어, 긴 문장에서 앞쪽 단어가 뒤쪽 단어의 의미를 결정하는 경우가 있습니다. (ex. "나는 아침에 커피를 마셨다. 그래서 지금 카페인은..." → "카페인은" 을 예측하려면 앞 문장의 "커피" 정보가 필요함)
장기 의존성 문제 (Long-term Dependency Problem): 모델이 장기적인 관계, 즉, 장기 의존성을 잘 학습하지 못하는 문제를 이야기합니다. 주로 RNN 에서 발생하는 문제로, 학습시 기울기 소실 (Vanishing Gradient) 문제로 인해 (긴 시퀀스를 다룰때 역전파 과정에서 그래디언트가 점점 작아져서 이전 단계로 전달되지 않는 문제) 모델이 제대로 학습되지 않아 발생하는 문제입니다. 모델이 장기 의존성을 잘 학습하지 못할 경우 모델의 장기 의존성 결여로 이어져 텍스트 생성 시에도 시퀀스가 길어질수록 초반 정보를 잘 반영하지 못하여 텍스트 생성 성능이 떨어지며 이로 인해 문맥이 깨지고 의미가 일관되지 않는 문장을 생성하게 됩니다.
기울기 소실 (Vanishing Gradient) & 기울기 폭주 (Exploding Gradient): 시퀀스 모델 학습시 역전파 과정에서 그래디언트가 점점 작아져서 0 으로 수렴하여 이전 단계로 전달되지 못하여 장기 의존성 문제를 발생시키거나 그래디언트가 점점 커져 발산하여 학습이 불안정해지는 문제입니다. 이는 해당 모델이 매 시점마다 같은 가중치를 사용하여 그래디언트가 지수적으로 감소 혹은 증가함에 따라 발생하는 문제입니다.
Backpropagation Through Time (BPTT)
매 시점마다 동일한 가중치를 공유하는 시퀀스 모델에서 역전파 수행시 각 시점에 대한 파라미터 사용을 모두 추적하여 시점별로 loss 에 대한 그래디언트를 각각 계산해서 이들을 전부 누적하여 parameter 업데이트를 한 번에 진행합니다.
LSTM
셀 상태 (Cell State) 와 게이트 (Gate) 메커니즘을 도입하여 RNN 의 기울기 소실 (Vanishing Gradient) 로 인한 장기 의존성 (Long-term Dependency) 문제를 완화한 모델로, 더 긴 문맥을 유지할 수 있도록 합니다.
GRU
LSTM 의 복잡성을 줄이면서도 성능을 유지하기 위해 등장한 모델입니다. LSTM 은 게이트가 많아 계산량이 크고 학습 속도가 느린데, GRU 는 Forget Gate 와 Input Gate 를 Update Gate 하나로 단순화하여 연산량은 감소시키고 학습 속도는 증가시켰으며, 동시에 성능은 LSTM 과 비슷합니다.