[인공지능사관학교: 자연어분석A반] 딥러닝 (10)

Suhyeon Lee·2025년 7월 24일

순환 신경망(RNN, Recurrent Neural Network)

  • 다음 문장을 완성해보자:
    • 나는 지각을 ___
    • 그래서 선생님께 혼이 ___
    • 내 짝은 우리 반에서 가장 ___
    • 담임 선생님은 우리 반에서 가장 ___
  • 인간은 사고를 할 때 앞의 문맥에 따라 mask를 채워 나가는 과정을 거침
  • 다음 단어를 쓰기 위해서는 "이전 단어를 기억"하고 있어야 한다.
  • 대화형 인공지능
    • 대화형 인공지능에게 질문: "너 남자친구 있니?"
    • 음성인식 스마트 스피커

RNN 탄생 이유

  • 문장을 듣고 무엇을 의미하는지 알아야 서비스 제공 가능
    • 문장을 듣고 이해한다 == 많은 문장을 이미 학습해 놓았다
  • 문장의 의미를 전달하려면 각 단어가 "정해진 순서대로" 입력되어야 함 → 과거에 입력된 데이터 사이를 고려해야 하는 문제
    • 문장의 의미를 이해하기 위해서는 순서가 중요함
      • BUT 이전에 사용한 BoW(Bag of Words)는 문장의 구조나 문법을 고려하지 않고 오직 출현 빈도(frequency)만 고려하여 단어를 표현
  • 시간적 개념이 들어간 데이터들을 해결하기 위해 순환신경망(RNN) 고안
    • Recurrent: 순환하는
    • Neural Networks: 신경망
  • 순환신경망(RNN)은 "기억" 가중치를 가지고 다음 데이터로 넘어감

일반 신경망과 순환 신경망의 차이

  • RNN은 여러 개의 데이터가 순서대로 입력되었을 때 앞서 입력 받은 데이터의 연산 결과를 잠시 기억해 놓는 방식
    • 기억된 데이터를 가지고 다음 데이터로 넘어가면서 함께 연산
  • 앞에서 나온 입력에 대한 결과가 뒤에서 나오는 입력 값에 영향을 주는 것을 알 수 있음
    • 비슷한 두 문장이 입력되어도(주가 알려줘) 앞에서 나온 입력값(오늘/어제)을 구별하여 출력 값에 반영 가능
  • 같은 층 안에서 맴도는 성질 때문에 '순환 신경망'이라고 부름
    • 모든 입력 값에 대해 위 작업을 순서대로 실행하므로 같은 층을 맴도는 것처럼 보임

transformer

  • 2017년 구글이 제안한 sequence-to-sequence 모델
    • sequence-to-sequence: 특성 속성을 지닌 시퀀스를 다른 속성의 시퀀스로 변환하는 작업
      • 예: 기계번역 (어떤 언어의 토큰 시퀀스를 다른 언어의 토큰 시퀀스로 변환)
        "어제 카페 갔었어 거기 사람 많더라" → "I went to a cafe yesterday. There were a lots of people there"
  • RNN 기반
  • BERT, GPT를 이해하려면 transformer를 알아야 함
    • transformer를 이해하려면 RNN을 알아야 함!

RNN 기본 구조

  1. one to many
    • image captionaing: 사진의 캡션을 만들 때
model = Sequential()
model.add(
	RepeatVector(
    	number_of_times # 출력 개수 설정(3개의 캡션을 원하면 3으로 설정)
        , input_shape=input_shape
    )
)
model.add(
	SimpleRNN(
    	units=output_size
        , return_sequences=True # SimpleRNN 신경망이 순환하며 단일 값을 계속 출력
    )
)
  1. many to one(다수 입력 단일 출력)
    • 감성분석: 다양한 단어들이 들어가서 긍정/부정 결과 나옴
    • 문장의 뜻 파악
model = Sequential()
model.add(
	SimpleRNN(
    	units=output_size
        , input_shape=(timesteps, features)
	)
)
  1. many to many
    • 번역
    • 동영상
model = Sequential()
model.add(
	SimpleRNN(
    	units=output_size
        , input_shape=(timesteps, features)
        , return_sequences=True # SimpleRNN 신경망이 순환하며 단일 값을 계속 출력
    )
)
  • 문제가 무엇인지에 따라 다양한 구조 설계
  • 기억 가중치:

RNN 활용 사례

  • Sequential Data(순차 기반 데이터)
    • 분석에 사용되는 특성들이 시간적, 순차적 특징을 지닌 데이터
      • 전력 데이터 예측
      • 기온 데이터 예슥
      • 특정 단어가 마스킹 된 데이터 예측

실습: RNN 알고리즘

개요

  • 문장, 소리, 동영상, 시계열 등 순서가 있는 데이터(Sequential data)를 학습할 때 사용
    • '순서대로' 구성 → 앞에 있는 내용에 대해 기억해서(과거의 내용을 기억해서) 학습
  • 순차적인 데이터를 입력 받아 과거에 입력된 데이터와 나중에 입력된 데이터 사이의 관계를 고려
  • DNN, CNN은 지역적인 특성을 추출하는 것이 목적이기 때문에 시간적인 선후 관계를 분석하기에는 어려움이 있음 → RNN(Recurrent Neural Network) 고안
    • CNN이 인간의 시각 인지 방식을 모방한 기술이라면 RNN은 인간의 기억 방식을 모방한 기술
    • 인간의 기억은 시간이 지남에 따라 점차 희미해지는 조건
      • 초반에 입력된 입력값은 현재 예측에 영향이 감소하는 방법

시간에 따른 순서를 '기억'하면서 학습한다!

SimpleRNN

  • 형태: SimpleRNN(units=3, input)_shape=(4,9))
    • units: 뉴런의 개수
    • input_shape: (timesteps, features) 형태의 튜플로 들어감
      • timesteps: 순환 횟수 설정
  • 가장 기본적인 RNN 모델
    • vanila RNN이라고도 함
  • 5개의 알파벳을 가진 단어
    • 앞의 4개 알파벳을 학습하여 다음에 등장할 알파벳을 예측하는 모델을 생성해보자
      • hell → o
      • appl → e
      • hello, apple, hobby, below, wheel
  • RNN 데이터 구조
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# hello, apple, hobby, below, wheel
# 단어 사전: 입력되는 하나의 단어 → h, e, l, o, a, p, b, y, w: 9개의 알파벳을 사용해 구축
# 단어 사전 만들기: 각 알파벳을 원핫 인코딩 → 수치화
h = [1,0,0,0,0,0,0,0,0]
e = [0,1,0,0,0,0,0,0,0]
l = [0,0,1,0,0,0,0,0,0]
o = [0,0,0,1,0,0,0,0,0]
a = [0,0,0,0,1,0,0,0,0]
p = [0,0,0,0,0,1,0,0,0]
b = [0,0,0,0,0,0,1,0,0]
y = [0,0,0,0,0,0,0,1,0]
w = [0,0,0,0,0,0,0,0,1]
  • 데이터 분석을 위해 토큰화 결과를 수치로 만드는 방법

    • 원핫 인코딩: 해당 데이터가 있으면 1, 없으면 0
    • BOW(Bag of Word): 단어 토큰으로 쪼개서 숫자로 바꿔주는 방식
      • CounterVectorize: 단순 카운트
      • TF-IDF: 한 문서에서만 많이 나오는 단어에 가중치 부여
  • 원핫 인코딩(One-Hot encoding)

    • 토큰에 고유 번호를 배정하고 모든 고유 번호 위치의 한 컬럼만 1, 나머지 컬럼은 0인 벡터로 표시하는 방법
    • 단어사전은 내가 가지고 있는 총 개수만큼 생성
  • 문제와 정답으로 변환 → 입력(문제), 출력(정답)
    • 입력: 앞의 4개 알파벳
    • 출력: 마지막 1개 알파벳
# 입력 특성 데이터 구성
# hell, appl, hobb, belo, whee
# 항상 numpy ndarray로 먼저 만들고 이후 tensor로 변환하기
X_data = np.array(
    [
        [h, e, l, l]
        , [a, p, p, l]
        , [h, o, b, b]
        , [b, e, l, o]
		, [w, h, e, e]
    ]
)
print(X_data)
[[[1 0 0 0 0 0 0 0 0]
  [0 1 0 0 0 0 0 0 0]
  [0 0 1 0 0 0 0 0 0]
  [0 0 1 0 0 0 0 0 0]]

 [[0 0 0 0 1 0 0 0 0]
  [0 0 0 0 0 1 0 0 0]
  [0 0 0 0 0 1 0 0 0]
  [0 0 1 0 0 0 0 0 0]]

 [[1 0 0 0 0 0 0 0 0]
  [0 0 0 1 0 0 0 0 0]
  [0 0 0 0 0 0 1 0 0]
  [0 0 0 0 0 0 1 0 0]]

 [[0 0 0 0 0 0 1 0 0]
  [0 1 0 0 0 0 0 0 0]
  [0 0 1 0 0 0 0 0 0]
  [0 0 0 1 0 0 0 0 0]]

 [[0 0 0 0 0 0 0 0 1]
  [1 0 0 0 0 0 0 0 0]
  [0 1 0 0 0 0 0 0 0]
  [0 1 0 0 0 0 0 0 0]]]

# 정답 (label): o, e, y, w, 1
y_data = np.array([o,e,y,w,l])
y_data = np.argmax(y_data, axis=1)
print(y_data)
[3 1 7 8 2]
# tensor로 변경
X_data = torch.tensor(X_data).float()
y_data = torch.tensor(y_data).long() # 다중분류의 출력(정답 데이터)은 long이어야 함!

# 데이터 크기 확인
print("입력 데이터:",X_data.shape) # (샘플 수, 순환 횟수: 몇 번을 돌 것인가, 단어 사전 단어 수 == 특성 수)
# (5,4,9): (samples, timesteps, feature)
print("출력 데이터:",y_data.shape)
입력 데이터: torch.Size([5, 4, 9])
출력 데이터: torch.Size([5])
  • Shape: [Samples, Timesteps, Features]
  • tensor 로 변경하는 키워드 정리
방법메모리 공유복사 발생 여부장점주의사항
torch.from_numpy()✅ 예❌ 없음매우 빠름, 메모리 공유dtype 호환 필요
torch.tensor()❌ 아님✅ 있음독립된 텐서, 안전느릴 수 있음, dtype 주의
torch.as_tensor()🔁 상황에 따라조건부유연함 (공유 or 복사)내부 처리 파악 어려움

모델 정의

  • units 수는 사용자가 결정
    • 우리는 2개 쓸 거임
  • tanh(hyperbolic tangent): 활성화 함수

  • RNN 학습에서 활성화 함수로 tanh 사용
    • sigmoid 보다 기울기 유지력이 높다 = 희석되는 정보가 적다 → 더 많은 시간의 정보를 유지
    • 희석 속도가 생기도록 하기 위해서 사용
      • ReLU는 시간이 지나도 영향력이 줄어들지 않음 → 시간이 지남에 따라 멀리 떨어진 층의 영향력은 줄어아 하는데 ReLU는 그렇지 못함

  • 📌 일반적인 hidden_size 선택 팁
데이터 규모추천 hidden_size
매우 단순한 문자 예제2~10
단어 기반 시퀀스64~256
문장/문서 처리128~512 이상 가능

class RNN(nn.Module):
    def __init__(
            self
            , input_size=9
            , hidden_size=2 # 사용할 units 수
            , output_size=9 # 알파벳 9개 중에 1개를 예측
    ):
        super().__init__()

        # RNN 층(위 그림 초록색 박스)
        self.rnn = nn.RNN(
            input_size=input_size
            , hidden_size=hidden_size
            , batch_first=True # batch_first=True를 통해서 입력 텐서의 첫 번째 차원이 배치 크기(samples)임을 알려줍니다
        )
        # 우리가 입력하는 데이터 형식: (samples, timestamps, features)
        # RNN에서 데이터를 입력받고자 하는 형식(데이터 형식): (timestamps, samples, features)

        # 분류층(위 그림 살구색 박스)
        self.fc = nn.Linear(hidden_size, output_size)
        # nn.RNN은 내부적으로 활성화 함수 tanh를 기본으로 사용함(별도 명시 X여도 알아서 씀)
        # 기본 설정은 nonlinearity="relu" → 변경하고 싶을 때 nonlinearity="relu" 등으로 파라미터 직접 입력

    def forward(self, x):
        # output: 모든 시점의 출력값, h_n: 마지막 스텝의 결과 → 보통의 분류 분제에서는 마지막 값만 사용
        output, h_n = self.rnn(x)
        out = self.fc(h_n.squeeze(0)) # RNN:[1, batch, hidden] 출력 → fc는 반드시 [batch, hidden]을 원한다! → sqeeze 사용해 형태 맞춰줌
        return out # RNN에서는 y보다 out이라고 많이 함
  • 활성화 함수 설정?

  • 중요 포인트

  • 모델 정의, 손실 함수, 최적화 함수
model = RNN()
loss_func = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)

모델 학습

n_epochs = 200
his = []

for i in range(n_epochs):
    optimizer.zero_grad()
    y_pred = model(X_data)
    loss = loss_func(y_pred, y_data)

    # 손실값 저장
    his.append(loss.item()) # float(loss)도 가능

    loss.backward()
    optimizer.step()

    if (i+1) % 10 == 0:
        print(f"epoch {i+1}, loss: {loss.item():.4f}")
epoch 10, loss: 1.9364
epoch 20, loss: 1.7201
epoch 30, loss: 1.5534
epoch 40, loss: 1.4093
epoch 50, loss: 1.2743
epoch 60, loss: 1.1322
epoch 70, loss: 0.9659
epoch 80, loss: 0.8073
epoch 90, loss: 0.6850
epoch 100, loss: 0.5967
epoch 110, loss: 0.5329
epoch 120, loss: 0.4851
epoch 130, loss: 0.4474
epoch 140, loss: 0.4165
epoch 150, loss: 0.3901
epoch 160, loss: 0.3671
epoch 170, loss: 0.3466
epoch 180, loss: 0.3283
epoch 190, loss: 0.3117
epoch 200, loss: 0.2966
plt.figure(figsize = (10,3))
plt.plot(his, label = 'loss')
plt.show()

실제 데이터를 넣어 결과 확인

# hell을 넣으면 어떤 값이 출력될까?
X_test = torch.tensor([[h,e,l,l]], dtype=torch.float)
alphabet = ['h', 'e', 'l', 'o', 'a', 'p', 'b', 'y', 'w']

with torch.no_grad():
    out = model(X_test)
    idx_pred = torch.argmax(out, dim=1)
    result = alphabet[idx_pred]

print(f"예측 결과: {result}")
예측 결과: o

실습: 월별 항공 승객 수 예측

✅ 데이터 개요:

  • AirPassengers.csv는 전통적으로 시계열 모델 실습에 자주 쓰이는 유명한 데이터셋
    • 1949년 ~ 1960년 월별 항공 승객 수
항목내용
데이터 내용1949년부터 1960년까지의 월별 항공 승객 수
샘플 수약 144개 (12년 × 12개월)
목적예측 모델링 (ex. 다음 달 수요 예측)
data=pd.read_csv("./data/AirPassengers.csv")

# 월 데이터 타입 변경 (datetime): 텍스트 데이터가 아닌 날짜 데이터로써 활용하기 위함
data["Month"] = pd.to_datetime(data["Month"])

# 승객 수 컬럼 데이터 타입 변경 (float)
data["#Passengers"] = data["#Passengers"].values.astype(float)
Month	#Passengers
0	1949-01-01	112.0
1	1949-02-01	118.0
2	1949-03-01	132.0
3	1949-04-01	129.0
4	1949-05-01	121.0
...	...	...
139	1960-08-01	606.0
140	1960-09-01	508.0
141	1960-10-01	461.0
142	1960-11-01	390.0
143	1960-12-01	432.0
144 rows × 2 columns

데이터 정규화

  • RNN 모델링을 위한 데이터는 0~1 사이로 변환해주면 좋다
  • 이미지 데이터, 센서 데이터도 0~1 사이로 변환
y = data[["#Passengers"]] # 2차원 구조로 추출해야 함(모델은 2차원만 받는다)

# MinMax스케일링
# RNN, 이미지 데이터에서 많이 활용
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
data_scale = scaler.fit_transform(y)




하루 돌아보기

👍 잘한 점

  • 미니 프로젝트 겲과물 기한 안에 제출 완료
  • 발표 완료

👎 아쉬웠던 점

  • 이력서 첨삭 녹음할 걸 그랬다
    • 최대한 집중해서 피드백 듣긴 했는데 금방 잊어버렸음…

🔬 개선점

  • 효율적인 메모 방법 고민
  • 다음 번 피드백은 녹음을 하자
profile
2 B R 0 2 B

0개의 댓글