자연어 처리(NLP) 개론 및 RNN

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


댓글 쓰기

다음 이전