NLP: Natural Language Processing
- NLU: ... understanding
- NLG: ... generation
사례
- Closed Domain Conversational Agent:
특정 분야에 국한된 챗봇 등
NLP의 역사
- 1985: RNN - Recurrent Neural Networks
- 1997: LSTM = RNN + Attention
- 2017: Transformer
- 2018: Pre-trained language models (BERT, GPT)
- 2020: GPT-3
- 2023: LLaMa, Bard, GPT-4, Claude
Recurrent Neural Networks (순환 신경망)
- 순환 구조로 시계열 파악
- RNN의 Vanishing gradient는 장기문맥 손실을 말한다.
Seq2Seq
- 문장을 입력받아 문장을 출력한다. Many to Many(여러 키워드)
Attention 알고리즘
- Seq2Seq with Attention (Seq2Seq에 어텐션 접목)
Transformer
- Attention is all you need ( 어텐션만 사용하자 )
RNN의 필요성
- 자연어 문장에 순서정보가 필요로 함
- 시계열 정보라는 것은 입력이 고정되지 않았다는 것을 의미한다.
- CNN은 고정된 크기의 입력 데이터만 처리한다.
- 입력과 출력의 갯수에 따라 일대일, 일대다, 다대일, 다대다 등이 존재한다.
RNN의 작동방식
- 입력 데이터가 고정적이지 않으므로 메모리가 필요하다.
- 시간이 지나도 정보가 지속될 수 있는 피드백 루프가 필요하다.
- 피드백 루프는 현재 단계의 출력이 다음 단계의 입력과 함께 하는 것을 말한다.
- 은닉 상태를 유지하며(은닉 상태로 돌아가서), 이전 입력의 정보를 저장한다. 장기 의존성을 처리하기 위해 LSTM(장기 단기 메모리)와 GPU(게이트 순환 유닛)와 같은 변형이 개발되었다.
- 토큰 단위로 한 개씩 완전연결 계층에 전달한다.
- 토큰: 문장을 의미 단위로 분절하는 단위
- 토큰1이 일차식이 되어 활성화 함수에 들어간다. (토큰1의 선형패턴화 값)
- 토큰1의 계산 결과와 토큰2의 값이 곱해져서,
일차식이 되어 활성화 함수에 들어간다. - Weight가 쌍으로 필요하다. 처음 들어올 때 사용하는 Weight1, 계산 결과가 다시 들어올 때 사용하는 Weight2
- 시계열 정보를 사용하여 입력과 출력 사이의 패턴을 식별한다.
- Context(맥락) Vector가 쌓이다 보면,
- 다음 단어를 추론할 수 있다.
- 분류할 수 있다.
RNN의 수식
- Ct = Wx * Xt + Wc * Ct-1 + bias
- input: 4, output: 5, 일때, 전체 파라미터는
WaXa + WbXb + WcXc + WdXd + WeCa + WfCb + WgCc +WhCd + WiCe + bias - tanh는 모든 x 값에 대해서 -1 ~ 1 사이의 y값을 도출한다.
즉 루프가 반복될 수록 최초 input x의 값은 점점 작아진다. 0에 가까워진다.
(기울기 소실문제) - 단, 1층임에도 불구하고 시퀀스의 길이(토큰의 갯수 = 루프의 횟수)에 따라 기울기가 소실된다. 그래서 이른 시기에 제안된 아이디어임에도 사용되기 힘들었다.
- tanh(x) 미분 = 1 - tanh^2(x)
LSTM (Long Short Term Memory)
- 매 루프마다 같은 weight를 사용하고 있지만, 매 루프마다 weight에 곱해지는 값은 다른 값을 곱한다. 다른 Gradient가 나오도록 유도한다.
- 이를 장기기억장치를 추가했다고 본다.
- Ct = Ct-1 * 시그모이드(x) + (시그모이드(x) * tanh(x))
- 시그모이드(x)는 값을 작게 만든다.
- (시그모이드(x) * tanh(x))는 부호에 따라 값을 작게 만들기도 하고 키우기도 한다.
- 장기기억은 때로는 유지될 필요가 있고 때로는 사라질 필요가 있다.
- 유지할 필요: 기존 맥락이 새 맥락과 연관성이 높을 때, 기존 맥락 유지
- 사라질 필요: 기존 맥락이 새 맥락과 연관성이 낮을 때, 기존 맥락 제거
- 시그모이드(x): 절대 크기를 결정
- tanh(x): 유지할지 사라질지 방향, 부호를 결정
- ft(Forget Gate)는 장기기억의 필요한 부분만 추려내는 역할을 한다.
- 2번의 tanh(x)는 장기기억과 단기기억의 방향을 일치시키는 역할을 한다.
- 장기기억에 tanh(x)가 더해지기 때문에 단기기억에도 tanh(x)를 곱해준다.
- 파라미터 갯수 확인
- ft = ( 5 + 4 + 1 ) * 5 = 50
- gt = ( 5 + 4 + 1 ) * 5 = 50
- it = ( 5 + 4 + 1 ) * 5 = 50
- ot = ( 5 + 4 + 1 ) * 5 = 50
- ft, gt, it -> ct
- ot * ct -> ht, zt ( 기존은 ot(활성화 함수는 tanh사용) -> ht, zt )
- 기존에는 루프의 결과만으로 추론했지만, LSTM은 마지막 장기기억 값을 곱해서 추론한다.
GRU (Gated Recurrent Unit)
- 2014년 조경현 교수가 제안
- 3개 게이트(Forget, Input, Output) -> 2개 게이트(Reset, Update)
- hidden state하나로 장기적 기억도 관리 사용
- 성능은 LSTM과 비슷하지만, 간소화되고 빠르다. 파라미터가 3/4로 줄어들었다.
- rt(Reset), zt(Update) 파라미터 갯수
- rt = ( 5 + 4 + 1 ) * 5 = 50
- zt = ( 5 + 4 + 1 ) * 5 = 50
- h^t = ( 5 + 4 + 1 ) * 5 = 50
- 상단의 (x) 와 (+)는 forget과 input의 비율을 결정한다.
- LSTM과 비교
- GRU의 rt는 LSTM의 Ct 값의 역할(장기기억장치)을 한다고 볼 수 있다.
- LSTM은 (x) 과 (+)를 거쳐 tanh(x)로 들어가고
- GRU도 (x)과 결합을 거쳐 tanh(x)로 들어간다.
- 데이터 양이 적을 때에는 모델 복잡도가 낮은 GPU가 더 좋다.
- 실제 사용할 때는 LSTM과 GPU 모두 사용하여 비교해본, 판단하는 것을 권장
실습
RNN
- 입력데이터
- inputs = torch.randn(batch_size, sequence_length, input_size)
- inputs.shape # 시퀀스 길이, 배치 크기, 입력(임베딩) 차원
- RNN 모델
- rnn = nn.RNN(input_size, hidden_size, batch_first=True)
- rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)
- CNN과 다르게 디테일하게 conv2d나 MaxPool2d 등을 쌓을 필요가 없고, num_layers 숫자만 넣어주면 한 줄로 모델을 만들 수 있다.
- RNN은 디테일한 설계들이 모델의 성능에 큰 영향을 더이상 주지 못한다. 현재가 최적화된 상태. 또한 트렌스포머의 등장으로 RNN에 더이상 신경을 쓰지 못함.
- RUN
- output, hidden = rnn(inputs)
- 두 shape 비교
- output: batchs, seqs, hidden
- seq 별로 출력되는 모든 결과 값
- hidden: layers, batchs, hidden
- 마지막 seq에 출력되는 결과 값
- layer당 1개씩 나오므로 layer 갯수와 동일하다.
- shape가 다르다면, 모델에 한 번에 입력되는 갯수가 다를까?
- output이 한 번에 들어가는 것이 아니라, 순차적으로 들어간다.
- RNN 분류 모델
- class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super(SimpleRNN, self).__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, num_classes)
def forward(self, x):
_, hidden = self.rnn(x)
out = self.fc(hidden[0]) # 마지막 타임스텝의 출력만 사용 - Many to one
return out # 만약, hidden이 아닌 output을 사용하는 경우 - Many to Many - 학습
- 기존 fc 와 동일
- for epoch in range(num_epochs):
outputs = model(inputs)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
양방향 RNN
- forward에 backward가 추가된 형태. -> 갔다가 <- 로 간다.
- forward로 가면서 물통의 절반을 채우고 backward로 가면서 물통의 나머지 절반을 채운다.
- forward -> 이동
- out_forward = outputs[ :, -1, :hidden_size ]
# 가장 우측(-1)에 도달한 것중 처음 채운 절반 - backwoard <- 이동
- out_backward = outputs[ :, 0, hidden_size: ]
# 우측을 거쳤다가 좌측(0)에 도달한 것중 뒤에 채운 절반 - 맨 마지막 결과 값 구하기
- torch.cat( (out_forward, out_backward), dim=1 )
- torch.cat( (hidden_forward, hidden_backward), dim=1 )
- hidden의 경우 한 곳에 적재 되는데,
forward, backward, forward, backward 순으로 엇갈려 적재된다. - hidden_forward = hidden[-2, : , : ]
- hidden_backward = hidden[-1, : , :]
시계열 데이터 생성
- 구조
- 1000개의 데이터를 사용
- 10개를 input, 1개를 output
- def create_sequences(data, seq_length):
xs, ys = [ ], [ ]
for i in range(len(data) - seq_length): # 11번째 부터 결과 값을 사용.
x = data[i:i+seq_length] # ex: 0 ~ 9
y = data[i+seq_length] # ex: 10
xs.append(x)
ys.append(y)
return np.array(xs), np.array(ys) - X, y = create_sequences(data, seq_length)
- (990, 10) (990,)
LSTM
- LSTM을 위한 전처리
- X_tensor = torch.FloatTensor(X).unsqueeze(2)
- torch.Size([990, 10, 1])
- y_tensor = torch.FloatTensor(y).unsqueeze(1)
- torch.Size([990, 1])
- LSTM 모델 생성
- lstm = nn.LSTM(input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True
# bidirectional=True # 양방향 RNN을 쓸 것인지 묻는다.
) - RUN
- h0 = torch.zeros(num_layers, batch_size, hidden_size) #초기값 (은닉)
- c0 = torch.zeros(num_layers, batch_size, hidden_size) #초기값 (셀=장기기억)
- lstm_out, (hn, cn) = lstm(X_tensor, (h0, c0)) #초기값은 안 넣어도 됨.
- lstm_out: 아웃풋
- (hn, cn): 은닉이 2개로 나옴 (cn: 장기기억)
LSTM 결과값
- 30 = seq수(한 문장의 토큰 수)
- 128 = hidden 사이즈
- 2 = 양방향 RNN 이므로, 우측방향과, 좌측방향 값 2개가 나온다.
- lstm_out...256은 hidden...2... 128과 같다.
- hidden = torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1)
- 다음 코드를 써도 된다. hidden = lstm_out[ : , -1 , : ]
GRU
- GRU 모델 생성
- gru = nn.GRU(input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True) - GRU 분류 모델
- class GRUModel(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size):
super(GRUModel, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.gru = nn.GRU(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
out, _ = self.gru(x) # GRU 통과
out = self.fc(out[:, -1, :]) # 마지막 시퀀스 출력값만 사용
return out
Tags:
AI개발_교육