본문 바로가기
TIL _Today I Learned/2024.09

[DAY 52] 텍스트 분류 모델 구축 (LSTM)

by gamdong2 2024. 9. 27.
[천재교육] 프로젝트 기반 빅데이터 서비스 개발자 양성 과정 9기
학습일 : 2024.09.27

📕 학습 목록

  • RNN
  • LSTM

 

📗 기억할 내용

1) LSTM 정의

  • LSTM(Long Short-Term Memory) : 순환 신경망(RNN, Recurrent Neural Network)의 일종으로, 긴 시퀀스 데이터를 처리하는 데 적합한 신경망
  • RNN은 시퀀스 데이터를 처리하기 위해 설계된 신경망으로, 텍스트, 음성, 시계열 데이터 등 순차적인 데이터를 학습할 때 사용됨
  • 하지만 RNN은 장기 의존성 문제(long-term dependency problem)를 가지고 있음. 이는 긴 시퀀스에서 먼 과거의 정보를 잘 기억하지 못하는 문제를 의미
  • LSTM은 이러한 문제를 해결하기 위해 고안되었으며, 정보를 장기적으로 기억할 수 있는 셀 상태(Cell State)와 이를 제어하는 게이트 구조를 도입함

 

2) LSTM의 주요 구성 요소

 

① 셀 상태(Cell State)

  • Cell State : LSTM의 핵심 구성 요소로, 정보가 흐르는 경로임. 셀 상태는 중요한 정보를 오랜 시간 동안 유지하거나 잊어버릴 수 있음
  • 셀 상태는 시점 간의 정보 흐름을 유지하며, 이전 시점에서 중요한 정보가 현재 시점에도 필요할 경우 이 정보를 계속 유지함
  • 셀 상태는 각 게이트(Input, Forget, Output)에 의해 제어됨

② 입력 게이트(Input Gate)

  • Input Gate : 새로운 입력 정보가 셀 상태에 얼마나 중요한지 판단하고, 그 정보를 셀 상태에 추가할지 여부를 결정함
  • 중요한 정보만 선택하여 셀 상태로 전달하며, 덜 중요한 정보는 입력되지 않도록 제어

③ 포겟 게이트(Forget Gate)

  • Forget Gate : 셀 상태에서 이전 시점의 정보 중 얼마나 잊을지를 결정함
  • 입력된 정보가 더 이상 유효하지 않거나 중요하지 않은 경우, 포겟 게이트가 이를 걸러냄
  • 예를 들어, 문장의 앞부분에서 중요한 정보가 나오지 않았을 경우, 포겟 게이트는 그 정보를 삭제함

④ 출력 게이트(Output Gate)

  • Output Gate : 셀 상태에서 중요한 정보를 선택하여 최종 출력값으로 변환함
  • 셀 상태와 입력 데이터를 바탕으로 현재 시점에서 어떤 정보를 출력할지 결정하며, 예측 결과를 도출하는 데 중요한 역할

 

3) LSTM의 작동 과정

 

① 입력 데이터 처리

  • 시퀀스의 각 시점에서 입력 게이트는 현재 입력된 정보가 얼마나 중요한지를 분석하고, 셀 상태에 저장할지 여부를 결정

② 포겟 게이트의 역할

  • 셀 상태에서 과거의 정보 중 불필요한 정보를 제거하여, 중요한 정보만 셀 상태에 남김. 이는 이전 시점에서 온 정보 중 현재 시점에 유효하지 않은 정보를 잊도록 함

③ 셀 상태 업데이트

  • 입력 게이트와 포겟 게이트의 결과에 따라 셀 상태는 중요한 정보를 유지하거나 업데이트함. 새로운 정보가 입력되면, 기존 셀 상태에 이 정보를 추가함

④ 최종 출력

  • 출력 게이트는 현재 상태에서 필요한 정보를 선택하여 최종 출력을 생성. 이는 주어진 작업에 맞는 예측 결과로 출력되며, 이를 통해 텍스트 분류, 시계열 예측 등 다양한 작업이 수행됨

 

4) LSTM의 장점

  • 장기 의존성 해결: LSTM은 셀 상태와 게이트 구조를 통해 긴 시퀀스에서도 중요한 정보를 기억할 수 있어, RNN의 장기 의존성 문제를 해결
  • 문맥 정보 반영: LSTM은 이전 시점의 정보를 유지하고 활용할 수 있어, 문맥을 반영한 예측이 가능
  • 다양한 응용 분야: LSTM은 텍스트 분류, 음성 인식, 시계열 예측, 번역 등의 다양한 분야에서 활용

 

📘 코드 실습

< LSTM 기반 텍스트 분류 모델 구축 >

1. 모델 구축 과정

 

1) 모델의 목적

  • 텍스트 분류: 주어진 텍스트 데이터를 긍정적 또는 부정적으로 분류하는 모델. 예를 들어, 사용자 리뷰나 피드백을 분석하여 긍정적인지 부정적인지를 판단

2) 모델의 구조

  • 임베딩 레이어 (Embedding Layer)
    • 역할: 단어를 고차원 벡터로 변환하여 모델이 텍스트 데이터를 수치적으로 처리할 수 있도록 함
    • 설명: 각 단어는 고유한 인덱스에 매핑되고, 임베딩 레이어는 이 인덱스를 고정된 크기의 벡터로 변환함. 이를 통해 단어 간의 유사성을 반영할 수 있음
  • LSTM 레이어 (Long Short-Term Memory Layer)
    • 역할: 시퀀스 데이터(문장)를 처리하고, 단어 간의 순서와 문맥 정보를 학습
    • 설명: LSTM은 RNN의 한 종류로, 긴 시퀀스에서도 중요한 정보를 기억할 수 있는 셀 상태와 게이트 구조를 가지고 있어, 장기 의존성 문제를 해결
  • 완전 연결 레이어 (Fully Connected Layer)
    • 역할: LSTM 레이어의 출력을 받아 최종 분류 결과(긍정/부정)를 출력
    • 설명: 마지막 LSTM의 출력값을 선형 변환하여 각 클래스(예: 긍정, 부정)에 대한 확률을 계산

3) 모델의 전체 흐름

① 데이터 로드 및 전처리

  • 학습 데이터(ratings_train.txt)와 테스트 데이터(ratings_test.txt)를 pandas를 이용해 불러옴
  • 데이터의 중복 제거, 특수 문자 제거, 불용어 처리 등을 통해 텍스트를 정제

② 토큰화 및 단어 인덱싱

  • Okt 형태소 분석기를 사용하여 문장을 단어 단위로 분해하고, 불용어를 제거
  • 단어들을 고유 인덱스로 변환하여 숫자형 벡터로 만듦

③ 모델 설계

  • 임베딩 레이어: 단어 인덱스를 벡터로 변환
  • LSTM 레이어: 시퀀스 데이터를 처리하여 문맥 정보를 학습
  • 완전 연결 레이어: 최종 분류 결과를 출력

④ 모델 학습

  • 학습 데이터셋을 통해 모델을 훈련시키고, 각 에포크(epoch)마다 손실과 정확도를 계산
  • 검증 데이터셋을 사용하여 모델의 성능을 평가하고, 가장 좋은 성능을 보이는 모델을 저장

⑤ 모델 평가 및 테스트

  • 저장된 최적의 모델을 로드하여 테스트 데이터셋에 대해 성능을 평가
  • 학습되지 않은 새로운 데이터에 대해 긍정/부정 예측을 수행할 수 있는 함수를 작성

 

2. 모델 생성 코드

1) 데이터 로드 및 전처리

import pandas as pd

# 학습 데이터 및 테스트 데이터 로드 (TSV 파일)
train = pd.read_csv('./data/ratings_train.txt', sep='\t')
test = pd.read_csv('./data/ratings_test.txt', sep='\t')

# 각 데이터셋에 대한 정보 확인 (크기, 결측치, 중복 등)
for table in (train, test):
    print('데이터 개수')
    print(table.shape)

    print('결측치 여부')
    print(table.isnull()['label'].unique())

    print('중복 데이터 개수')
    print(table.shape[0] - table['document'].nunique())

    print('클래스별 데이터 개수')
    print(table.groupby('label').size())
    print()

# 전처리: 중복 제거, 특수 문자 제거, 불필요한 공백 제거, 결측치 처리
for table in (train, test):
    table.drop_duplicates(subset=['document'], inplace=True)  # 중복된 문장 제거
    table['document'] = table['document'].str.replace('[^가-힣ㄱ-ㅎㅏ-ㅣ ]', '', regex=True)  # 한글 이외의 문자는 모두 제거
    table['document'] = table['document'].str.replace('[ +]', '', regex=True)  # 여러 공백을 하나로
    table['document'].replace('', float('nan'), inplace=True)  # 빈 문자열을 결측치로 변환
    table.dropna(how='any', inplace=True)  # 결측치가 있는 행 제거

    print('전처리 후 데이터 개수:', table.shape)



2) 불용어 처리 및 토큰화

from konlpy.tag import Okt
from tqdm import tqdm
import numpy as np

okt = Okt()  # 한국어 텍스트를 위한 형태소 분석기 Okt 초기화

# 불용어 리스트 불러오기
with open('../stopwords-ko.txt', encoding='utf8') as f:
    stopwords = f.readlines()
stopwords = [s.strip() for s in stopwords]  # 각 불용어의 끝에 있는 줄바꿈 문자 제거

# 데이터 초기화
train.reset_index(drop=True, inplace=True)
test.reset_index(drop=True, inplace=True)

# 학습 데이터와 테스트 데이터를 토큰화하고 불용어 제거
X_train, y_train = [], []
for i in tqdm(range(len(train))):  # tqdm으로 진행 상황을 보여줌
    sentence = train['document'][i]
    y_train.append(train['label'][i])

    # 형태소 분석 및 불용어 제거
    tokenized = okt.morphs(sentence, stem=True)
    stopwords_removed_sentence = [word for word in tokenized if word not in stopwords and len(word) >= 2]  # 2글자 이상만 사용
    if stopwords_removed_sentence:
        X_train.append(stopwords_removed_sentence)

X_test, y_test = [], []
for i in tqdm(range(len(test))):
    sentence = test['document'][i]
    y_test.append(test['label'][i])

    tokenized = okt.morphs(sentence, stem=True)
    stopwords_removed_sentence = [word for word in tokenized if word not in stopwords and len(word) >= 2]
    if stopwords_removed_sentence:
        X_test.append(stopwords_removed_sentence)



3) LSTM 모델 학습

import torch
import torch.nn as nn
import torch.optim as optim

# LSTM 기반의 텍스트 분류 모델 설계
class LSTMModel(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_size, num_classes):
        super(LSTMModel, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)  # 임베딩 레이어
        self.lstm = nn.LSTM(embedding_dim, hidden_size, batch_first=True)  # LSTM 레이어
        self.fc = nn.Linear(hidden_size, num_classes)  # 출력 레이어

    def forward(self, x):
        x = self.embedding(x)  # 임베딩
        _, (hn, _) = self.lstm(x)  # LSTM 연산
        out = self.fc(hn[-1])  # 출력값
        return out

# 모델, 손실 함수, 옵티마이저 정의
vocab_size = 10000  # 예시
embedding_dim = 128
hidden_size = 256
num_classes = 2
model = LSTMModel(vocab_size, embedding_dim, hidden_size, num_classes)

criterion = nn.CrossEntropyLoss()  # 교차 엔트로피 손실 함수
optimizer = optim.Adam(model.parameters(), lr=0.001)  # Adam 옵티마이저



4) 모델 학습 과정

# 학습과 검증 함수 정의
def train_model(model, train_dataloader, valid_dataloader, num_epochs):
    best_val_loss = float('inf')  # 초기값을 무한대로 설정

    for epoch in range(num_epochs):
        model.train()  # 학습 모드로 전환
        train_loss, train_correct, train_total = 0, 0, 0

        for batch_X, batch_y in train_dataloader:
            batch_X, batch_y = batch_X.to(device), batch_y.to(device)
            logits = model(batch_X)  # 모델에 데이터를 입력하여 예측값 계산
            loss = criterion(logits, batch_y)  # 손실 계산

            optimizer.zero_grad()  # 이전의 기울기 초기화
            loss.backward()  # 역전파
            optimizer.step()  # 옵티마이저로 파라미터 업데이트

            train_loss += loss.item()
            train_correct += (logits.argmax(1) == batch_y).sum().item()  # 정확도 계산
            train_total += batch_y.size(0)

        train_acc = train_correct / train_total
        train_loss /= len(train_dataloader)

        val_loss, val_acc = evaluate(model, valid_dataloader)  # 검증 데이터로 평가

        print(f'Epoch {epoch+1}/{num_epochs}, Train Loss: {train_loss:.3f}, Train Acc: {train_acc:.3f}')
        print(f'Validation Loss: {val_loss:.3f}, Validation Acc: {val_acc:.3f}')

        # 최상의 검증 성능을 기록
        if val_loss < best_val_loss:
            best_val_loss = val_loss
            torch.save(model.state_dict(), './best_model.pth')  # 모델 저장



5) 테스트 및 리뷰 예측

# 학습된 모델을 로드하고 테스트 데이터로 평가
model.load_state_dict(torch.load('./best_model.pth'))  # 저장된 최상의 모델 불러오기
model.to(device)

test_loss, test_acc = evaluate(model, test_dataloader)  # 테스트 데이터로 평가
print(f'Test Loss: {test_loss:.3f}, Test Acc: {test_acc:.3f}')

# 새로운 리뷰에 대한 긍정/부정 예측 함수 정의
def predict(text, model, word_to_index):
    model.eval()  # 평가 모드로 전환
    tokens = okt.morphs(text, stem=True)  # 문장을 형태소 분석하여 토큰화
    token_indices = [word_to_index.get(token, 1) for token in tokens]  # 단어 인덱스 변환
    input_tensor = torch.tensor([token_indices], dtype=torch.long).to(device)

    with torch.no_grad():
        logits = model(input_tensor)  # 예측값 계산
    pred_ind = torch.argmax(logits, dim=1)  # 가장 높은 확률을 가진 클래스로 예측

    return pred_ind

# 예시: '싫어요'라는 문장이 입력된 경우
if int(predict('싫어요', model)) == 0:
    print('부정적 리뷰')
else:
    print('긍정적 리뷰')

 

 

 

📙 내일 일정

  • SQL 개론