2. NLP-LSTM, GRU (2) : 텍스트 분류기

안상훈·2024년 10월 9일

AI핵심기술

목록 보기
10/21
post-thumbnail

개요

본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 AI 핵심 기술 집중 클래스의 자연어처리(NLP) 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.


0. RNN의 장기의존성 문제

이전 포스트 2. NLP-RNN (1) : 텍스트 분류기에서 진행한
RNN모델로 스팸 문자 분류기를 설계한 뒤 이를 성능평가한 결과물을 확인하면 아래와 같다.

이게 결과를 보면 알 수 있듯이 FastText방법론으로 사전학습된 임베딩 레이어 파라미터를 적용시켜도 분류 정확도가 80% 미만으로 성능이 꽤나 처참한 것을 알 수 있다.

이는 데이터 및 텍스트 전처리 과정까지 분석해보면 이유를 어느정도 가늠해 볼 수 있다.

먼저 데이터셋의 구성을 보면
문서는 평균 87개의 토큰으로 구성되어있고, 문서 중 토큰 보유 개수가 많은 문서와 적은 문서 중 context_length(토큰 보유 개수)를 330으로 설정 했을 때 5% 문서만이 330개 보다 더 많은 토큰을 보유하고 그 이하 문서는 토큰 개수가 적은 것으로 확인할 수 있다.

그러면 대부분의 문서는 87개의 의미있는 데이터 토큰 + 243<PAD> : 0 제로 패딩 토큰이 채워져 있다고 볼 수 있다.

이를 RNN의 동작구조와 대응하여 어떤식으로 학습결과가 어떻게 형성되는지를 확인하자면 아래의 그림처럼 표현할 수 있다.

위를 수학적으로 풀이해보면 아래의 수식과 같아지며

입력데이터 xx가 제로 패딩 토큰이 입력되는 88번째 토큰부터는 이전토큰의 학습정보만 남게 되며
이 정보는 활성화함수(tanh)로 인해 계속해서 ±1\pm1로 정규화 되고 수식에 의해 이전 토큰의 학습정보가 0으로
수렴하는 기울기 소실문제가 발생하게 된다.

대략 zero pad 토큰이 10회 이상 반복되면 이전토큰의 학습정보는 거의 다 유실되는데
이것이 RNN의 치명적인 문제라 볼 수 있는 장기 의존성 문제 (Long-Term Dependency Problem)이다.



1. LSTM (Long Short-Term Memory) 개요

LSTMRNN의 장기의존성 문제를 해결하기 위해 제안된 모델로

기존에 RNN이 출력하는 hidden_state정보 외에도 새로운 상태정보값인 cell_state정보를 출력하며,장기적인 정보 흐름을 유지시켜주는 매우 중요한 상태정보이다.

cell_state를 통하여 의미가 있는 정보라 판단되면 오랜 시퀀스를 거치면서 모델이 순환Recurrent학습 하더라도 정보가 희미해지는 문제를 방지해 장기 의존성을 유지한다.

cell_state의 정보흐름을 본다면 지극히 간단한 행렬연산을 수행하고 활성화 함수를 거치지 않기에 기울기 소실 문제가 발생하지 않는다.
따라서 정보의 장기적 유지 및 업데이트가 가능하며, cell_state는 내부에 포함되어 있는 Forget Gate, Input Gate 2개의 게이트 결과물을 통해
기존정보는 어느정도 잊고 신규정보는 어느정도 기억할지?
를 결정하고(Update)
이를 반영하여 Output Gate에서 hidden_state
Output Feature를 출력한다.

3개의 게이트와 게이트로 인해 발생하는 중간 연산을 도식화 하면 위 사진과 같아지며, 각 게이트 및 업데이트 연산에 대한 수식은 생략하도록 하겠다.

요지는 '의미있는 정보는 순환학습이 지속되더라도 보존하려 함'
이것이다.

물론 코드를 작성하는 과정에서 가장 중요한 것은 이것이다

cell_state의 Feautre 차원정보는 어떻게 되는가?

hidden_state 차원 : (num_layers, Batch_size, hid_dim)
과 동일한 차원을 갖는다

cell_state 차원 : (num_layers, Batch_size, hid_dim) 이다.


1.1 GRU(Gated Recurrent Unit)

GRULSTM를 경량화 시킨 버전으로 재귀 필터의 개념을 도입해
이전 정보와 현재 정보의 반영 비율을 결정하는 방식으로 모델이 동작한다.

즉, LSTM은 장기기억을 보존하기 위해 cell_state가 사용된다면

GRU는 어차피 재귀필터의 가중치만 조정하면 이전정보가 계속 보존되니 장기기억 보존용 cell_state가 없는 장점이 있는 것이다.

GRUUpdate, Reset Gate, Output Gate 이렇게 3개만 존재하기에 하나의 게이트가 없고
cell_state가 존재하지 않아 파라미터 및 연산이 단순화된 장점이 존재하며,
수식으로 살펴본다면 전체적으로 재귀 필터의 일반식

xˉk=αxˉk1+(1α)xk\huge \bar{x}_{k} = \alpha*\bar{x}_{k-1} + (1-\alpha)*x_k

과 유사한 형태를 띄고 있음을 확인 할 수 있다.

따라서 LSTM대비 학습 속도나 모델의 무게도 꽤 최적화된 모델이라 볼 수 있으나, 그만큼 Trade-off 개념으로 성능도 하락하는 측면이 있어서
산업계에서는 두 모델을 혼용하여 사용하는 편이다.

통상 GRULSTM대비 30~40% 정도 파라미터 수 감소가 되지만
정확도가 5~7% 정도 하락하기에 이같은 장/단점을 숙지해 적합한 모델을 선택해야 한다.



2. LSTM, GRU실습

LSTM, GRU에 대한 공부가 어느정도 되었으니 이제 실습을 진행하고자 한다

이전포스트 2. NLP-RNN (1) : 텍스트 분류기에서 실습한
실습 데이터셋 - 한국어 스팸 분류기용 데이터셋
https://github.com/tbvjvsladla/ASH_NLP_lacture/blob/main/spam_SNS.csv

을 기반으로 RNN 과의 텍스트 분류 비교성능을 확인해보자

데이터&텍스트 전처리에는 이전 포스트 1. NLP-Text 전처리 마침 : 모듈화에서 설명하고 있는

데이터&텍스트 전처리 함수 모음 NLP_pp.py를 사용한다.


데이터 및 텍스트 전처리

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 데이터 및 텍스트 전처리 함수를 모듈화 시킨 파일
from NLP_pp import *
import urllib.request

url = 'https://raw.githubusercontent.com/tbvjvsladla/ASH_NLP_lacture/main/spam_SNS.csv'
path = './data/spam_SNS.csv'

# 깃허브에 있는 파일 다운로드
urllib.request.urlretrieve(url=url, filename=path)
# 다운로드 받은 파일 불러오기
raw_data = pd.read_csv(path)
# 데이터셋읜 결측치 & 중복치 제거 함수 실행
raw_data = df_cleaning(raw_data, 'content')
import re

# 한글, 영어(소문자, 대문자), 숫자
p1 = re.compile(r'[^가-힣a-zA-Z0-9\s]')
# 한글 자모 데이터
p2 = re.compile(r'[ㄱ-ㅎㅏ-ㅣ]+')
# 개행문자 + 하나 이상의 공백문자
p3 = re.compile(r'\n|\s+')

def regex_sub(origin_sent):
    clean_text = p1.sub(repl=" ", string=origin_sent)
    clean_text = p2.sub("", clean_text)
    clean_text = p3.sub(" ", clean_text)

    return clean_text
# 설계한 정규표현식기반 특수문자 삭제 함수 적용
# apply함수는 inplace=True(덮어쓰기) 기능이 없음
raw_data['content'] = raw_data['content'].apply(regex_sub)

# '일상대화'는 0으로, '스팸문자'는 1로 변환
raw_data['class'] = raw_data['class'].map({'일상대화': 0, '스팸문자': 1})

데이터 전처리 중간결과 체크

# 데이터프레임의 항목을 분리 후 리스트 타입으로 변경
raw_x_data = raw_data['content'].values.tolist()
raw_y_label = raw_data['class'].values.tolist()
from mecab import MeCab #한글 단어 토크나이저
from tqdm import tqdm

#mecab 형태소 분석기 인스턴스화
word_tokenizer = MeCab()
# 토큰화 수행
tokenized_x_data = tokenize(raw_x_data, word_tokenizer)
# 깃허브에 있는 stopwordlist.txt파일 다운
stop_url = 'https://raw.githubusercontent.com/tbvjvsladla/ASH_NLP_lacture/main/kr_stopword_list.txt'
# 불용어 데이터셋 다운로드
stopword_list = download_stopword_list(stop_url)
# 토큰화 처리한 '기사 본문' 데이터셋의 불용어 제거
r_t_x_data = remove_stopword(tokenized_x_data, stopword_list)

불용어 제거 중간결과 체크

# 불용어 제거된 데이터를 자모 데이터로 분리
jamo_x_data = decompose_jamo(r_t_x_data)
from sklearn.model_selection import train_test_split

# 훈련/검증/평가를 75%, 20%, 5%로 분할을 수행
# random_state -> 데이터셋을 내누는데 '재현성' 유지를 위해 넣음 -> 안넣어도 됨
# stratify -> Y_label의 클래스 비율을 유지하면서 데이터 나눌때 옵션
x_train, x_etc, y_train, y_etc = train_test_split(
    jamo_x_data, raw_y_label, test_size=0.25, stratify=raw_y_label
)

# 그 외 데이터셋을 반반으로 Val, Test로 나눔
x_val, x_test, y_val, y_test = train_test_split(
    x_etc, y_etc, test_size=0.2, stratify=y_etc
)

from collections import Counter

word_list = []
# train항목을 워드 리스트에 입력
for sent in x_train:
    for word in sent:
        word_list.append(word)
# val항목을 워드 리스트에 입력
for sent in x_val:
    for word in sent:
        word_list.append(word)

# 단어와 해당 단어의 출몰 빈도를 함께 저장하는
# Counter 타입의 변수 생성
word_counts = Counter(word_list)

rare_th = 3 #희소단어의 등장 빈도를 결정하는 파라미터
# 희소단어 등장 빈도를 바탕으로 희소 단어를 배제하기 위해 준비 함수
tot_vocab_cnt, rare_vocab_cnt = set_rare_vocab(word_counts, rare_th, 
                                               report=False)
#등장 빈도가 높은 단어 순으로 정렬하기
vocab = sorted(word_counts, key=word_counts.get, reverse=True)

#등장 빈도가 높은 단어만 인덱싱 하기
vocab_size = tot_vocab_cnt - rare_vocab_cnt
vocab = vocab[:vocab_size]
# 스페셜 토큰 선언
spec_token = ['<PAD>', '<UNK>']
# 스페셜 토큰을 포함한 {단어:단어idx}의 딕셔너리 생성
word_to_idx, idx_to_word = set_word_to_idx(spec_token, vocab, 
                                           report=True)

# 데이터셋의 정수 인코딩 수행
e_x_train = text_to_sequences(x_train, word_to_idx)
e_x_val = text_to_sequences(x_val, word_to_idx)
e_x_test = text_to_sequences(x_test, word_to_idx)

context_length = 330
set_sent_pad(e_x_train, context_length, report=False)

# 데이터셋의 문장 패딩(정수인코딩의 완료)
padded_x_train = pad_seq_x(e_x_train, context_length)
padded_x_val = pad_seq_x(e_x_val, context_length)
padded_x_test = pad_seq_x(e_x_test, context_length)

import torch
bs = 256 # Batch_size 하이퍼 파라미터

# 정수(원핫)인코딩 데이터를 데이터로더로 변환
trainloader = set_dataloader(padded_x_train, y_train, bs, '훈련')
valloader = set_dataloader(padded_x_val, y_val, bs, '검증')
testloader = set_dataloader(padded_x_test, y_test, bs, '평가')

FastText로 임베딩 레이어 학습시키기

# Word2Vec 및 FastText 학습에 사용할 데이터: 
# 원본 데이터셋의 토큰화 후 불용어 제거를 수행한 데이터터
# 에다가 단어 -> 자모 분리를 수행한 데이터
word2vec_doc = jamo_x_data
from gensim.models import FastText

FT_model = FastText(
    sentences=word2vec_doc,
    vector_size = 100, # 임베딩 차원은 100으로 설정
    window = 5, # 논문의 최대 관심가질 주변단어 사이즈인 5~20
    min_count = rare_th, # (3) 단어장에서 배제할 희소단어 빈도 기준
    workers= -1, # 학습에 참여할 프로세스 개수 (최대로 설정)
    sg = 1, # Skip-gram 방식으로 학습 수행
    # FastText의 N-gram 범위 설정(3~6)
    min_n=3, max_n=6
)

# 단어장 크기 및 임베딩 차원 정보 추출
vocab_size = len(word_to_idx) 
embedding_dim = FT_model.wv.vector_size
def build_my_embed(word2idx, vocab_vector):
    vocab_size = len(word2idx)
    emb_dim = vocab_vector.vector_size

    embedding_matrix = np.zeros((vocab_size, emb_dim))

    for word, idx in word2idx.items():
        # word2idx의 단어를 학습된 임베딩레이어가 
        # 포함된 단어벡터에서 찾아냄
        if word in vocab_vector:
            embedding_vector = vocab_vector[word]
            embedding_matrix[idx] = embedding_vector
        # 스페셜 토큰별로 처리하기
        elif word == '<PAD>':
            # '<PAD>' 토큰의 임베딩 벡터는 0으로 유지
            embedding_matrix[idx] = np.zeros(emb_dim)
        
        else: # 단어벡터에 없는 단어 발생 -> '<UNK>' 처리
            # '<UNK>'는 랜덤 초기화 해버린다
            embedding_matrix[idx] = np.random.normal(size=(emb_dim,))

    return embedding_matrix
# FastText 방식으로 학습된 임베딩 레이어 조정
my_FT_embedding = build_my_embed(word_to_idx, FT_model.wv)

2.1 모델별 스팸분류기 설계

RNN, LSTM, GRU 모델기반 스팸분류기를 설계하기 전

주요 하이퍼 파라미터를 정리하자


RNN기반 스팸분류기 모델

import torch.nn as nn

class SimpleRNN(nn.Module):
    def __init__(self, vocab_size, embed_dim, num_classes,
                 hid_dim, emb_matirx=None):
        super(SimpleRNN, self).__init__()

        self.embed = nn.Embedding(vocab_size, embed_dim)

        if emb_matirx is not None:
            # 사전 훈련된 임베딩 매트릭스를 붙여넣음
            self.embed.weight = nn.Parameter(
                torch.tensor(emb_matirx, dtype=torch.float32))
            # 붙여넣은 Pretrained 임베드 레이어만 Freeze하고 싶을때는 False
            self.embed.weight.requires_grad = True

        # RNN은 반복횟수가 context_length으로 자동으로 지정됨
        self.rnn = nn.RNN(input_size=embed_dim, #RNN에 입력되는 차원
                          hidden_size=hid_dim, #RNN의 내부 cell의 차원
                          num_layers=1, #내부 은닉 셀이 몇층인지?
                          batch_first=True, #입력 텐서의 첫번째가 Batch임 
                          nonlinearity='tanh') #RNN활성화 함수 어떤것?
        
        # hidden_dim은 임베딩 차원 * context_length로 설정한다.
        self.classifier = nn.Sequential(
            nn.Linear(hid_dim, num_classes),
        )

    def forward(self, x):
        emb = self.embed(x)
        rnn_out, hidden = self.rnn(emb)
        out = hidden.squeeze(0)

        out = self.classifier(out)
        return out

LSTM 기반 스팸분류기 모델

import torch.nn as nn

class SimpleLSTM(nn.Module):
    def __init__(self, vocab_size, embed_dim, num_classes,
                 hid_dim, emb_matirx=None):
        super(SimpleLSTM, self).__init__()

        self.embed = nn.Embedding(vocab_size, embed_dim)

        if emb_matirx is not None:
            # 사전 훈련된 임베딩 매트릭스를 붙여넣음
            self.embed.weight = nn.Parameter(
                torch.tensor(emb_matirx, dtype=torch.float32))
            # 붙여넣은 Pretrained 임베드 레이어만 Freeze하고 싶을때는 False
            self.embed.weight.requires_grad = True

        # LSTM은 반복횟수가 context_length으로 자동으로 지정됨
        self.lstm = nn.LSTM(input_size=embed_dim, # LSTM에 입력차원
                            hidden_size=hid_dim, # LSTM 내부 cell 차원
                            num_layers=1, # 내부 Cell layer 개수=1
                            batch_first=True) # 입력 텐서는 batch가 첫번째로 옴
        
        # hidden_dim은 임베딩 차원 * context_length로 설정한다.
        self.classifier = nn.Sequential(
            nn.Linear(hid_dim, num_classes),
        )

    def forward(self, x):
        emb = self.embed(x)
        # LSTM의 출력은 out, hidden, cell 3가지가 나오는데
        # hidden, cell은 튜플로 묶어서 출력된다.
        # 이때 hidden은 입력데이터의 압축된 표현
        # cell은 장기적인 정보를 보존하기위한 메모리임을 숙지하자
        # lstm_out = (batch_size, seq_length, hidden_size)
        # hidden 및 cell = (num_layers, batch_size, hidden_size)
        lstm_out, (hidden, cell) = self.lstm(emb)
        # hidden의 첫번째 차원 num_layers=1 을 축소시킴
        out = hidden.squeeze(0)

        out = self.classifier(out)
        return out

GRU 기반 스팸분류기 모델

import torch.nn as nn

class SimpleGRU(nn.Module):
    def __init__(self, vocab_size, embed_dim, num_classes,
                 hid_dim, emb_matirx=None):
        super(SimpleGRU, self).__init__()

        self.embed = nn.Embedding(vocab_size, embed_dim)

        if emb_matirx is not None:
            # 사전 훈련된 임베딩 매트릭스를 붙여넣음
            self.embed.weight = nn.Parameter(
                torch.tensor(emb_matirx, dtype=torch.float32))
            # 붙여넣은 Pretrained 임베드 레이어만 Freeze하고 싶을때는 False
            self.embed.weight.requires_grad = True

        # RNN은 반복횟수가 context_length으로 자동으로 지정됨
        self.gru = nn.GRU(input_size=embed_dim, # GRU에 입력차원
                          hidden_size=hid_dim, # GRU 내부 cell 차원
                          num_layers=1, # 내부 Cell layer 개수=1
                          batch_first=True) # 입력 텐서는 batch가 첫번째로 옴
        
        # hidden_dim은 임베딩 차원 * context_length로 설정한다.
        self.classifier = nn.Sequential(
            nn.Linear(hid_dim, num_classes),
        )

    def forward(self, x):
        emb = self.embed(x)
        # GRU의 출력구조는 RNN과 동일하다
        # gur_out = (batch_size, seq_length, hidden_size)
        # hidden  = (num_layers, batch_size, hidden_size)
        gru_out, hidden = self.gru(emb)
        # hidden의 첫번째 차원 num_layers=1 을 축소시킴
        out = hidden.squeeze(0)

        out = self.classifier(out)
        return out

LSTM모델만 출력 형식이

lstm_out, (hidden, cell) = self.lstm(emb)

위와 같이 hidden_state, cell_state가 같이 튜플형식으로 묶여서 출력되는 것만 다르고
나머지는 RNN, GRU 모두 동일하다


2.2 스팸분류기 학습

학습을 위한 사전설정

# 학습 실험 조건을 구분하기 위한 키
model_key = ['RNN', 'LSTM', 'GRU']
cod_key = ['랜덤초기화', '사전훈련']
metrics_key = ['Loss', '정확도']

key_list = [f"{mk}_{ck}" for mk in model_key for ck in cod_key]
RNN_model_raninit = SimpleRNN(VOCAB_SIZE, EMB_DIM, 
                        NUM_CLASS, HIDE_DIM)
RNN_model_pre_emb = SimpleRNN(VOCAB_SIZE, EMB_DIM,
                        NUM_CLASS, HIDE_DIM, my_FT_embedding)

LSTM_model_raninit = SimpleLSTM(VOCAB_SIZE, EMB_DIM, 
                        NUM_CLASS, HIDE_DIM)
LSTM_model_pre_emb = SimpleLSTM(VOCAB_SIZE, EMB_DIM,
                        NUM_CLASS, HIDE_DIM, my_FT_embedding)

GRU_model_raninit = SimpleGRU(VOCAB_SIZE, EMB_DIM, 
                        NUM_CLASS, HIDE_DIM)
GRU_model_pre_emb = SimpleGRU(VOCAB_SIZE, EMB_DIM,
                        NUM_CLASS, HIDE_DIM, my_FT_embedding)
# GPU사용 가능 유/무 확인
device = torch.device("cuda" if torch.cuda.is_available() else 'cpu')
models = {} # 딕셔너리

models[key_list[0]] = RNN_model_raninit.to(device)
models[key_list[1]] = RNN_model_pre_emb.to(device)
models[key_list[2]] = LSTM_model_raninit.to(device)
models[key_list[3]] = LSTM_model_pre_emb.to(device)
models[key_list[4]] = GRU_model_raninit.to(device)
models[key_list[5]] = GRU_model_pre_emb.to(device)
import torch.optim as optim
# 로스함수 및 옵티마이저 설계
criterion = nn.CrossEntropyLoss()

LR = 0.001 # 러닝레이트는 통일
optimizers = {}

optimizers[key_list[0]] = optim.Adam(RNN_model_raninit.parameters(), lr=LR)
optimizers[key_list[1]] = optim.Adam(RNN_model_pre_emb.parameters(), lr=LR)
optimizers[key_list[2]] = optim.Adam(LSTM_model_raninit.parameters(), lr=LR)
optimizers[key_list[3]] = optim.Adam(LSTM_model_pre_emb.parameters(), lr=LR)
optimizers[key_list[4]] = optim.Adam(GRU_model_raninit.parameters(), lr=LR)
optimizers[key_list[5]] = optim.Adam(GRU_model_pre_emb.parameters(), lr=LR)
# 사전에 모듈화 한 학습/검증용 라이브러리 import
from C_ModelTrainer import ModelTrainer

num_epoch = 8 #총 훈련/검증 epoch값

ES = 2 # 디스플레이용 에포크 스텝
# BC_mode = True(이진), False(다중)
# aux = 보조분류기 유/무
# wandb = 완디비에 연결 안하면 None
# iter = 훈련시 iteration의 acc및 loss 정보 추출
trainer = ModelTrainer(epoch_step=ES, device=device, 
                       BC_mode=False, aux=False, iter=False)
# 학습/검증 정보 저장
history = {key: {metric: [] 
                for metric in metrics_key} 
           for key in key_list}

학습 실행

#실험조건 : 모델 + 임베딩레이어 pretrain 유/무
for key in key_list: 
    # 모델 훈련/검증 코드
    for epoch in range(num_epoch):
        # 훈련모드의 손실&성과 지표
        train_loss, train_acc = trainer.model_train(
            models[key], trainloader,
            criterion, optimizers[key], epoch)
        # 검증모드의 손실&성과 지표
        val_loss, val_acc = trainer.model_evaluate(
            models[key], valloader,
            criterion, epoch)
        
        # 손실 및 성과 지표를 history에 저장
        history[key]['Loss'].append((train_loss, val_loss))
        history[key]['정확도'].append((train_acc, val_acc))

        # Epoch_step(ES)일 때마다 print수행
        if (epoch+1) % ES == 0 or epoch == 0:
            if epoch == 0:
                print(f"현재 훈련중인 조건: [{[key]}]")
            print(f"epoch {epoch+1:03d}," + "\t" + 
                f"훈련 [Loss: {train_loss:.3f}, " +
                f"Acc: {train_acc*100:.2f}%]")
            print(f"epoch {epoch+1:03d}," + "\t" + 
                f"검증 [Loss: {val_loss:.3f}, " +
                f"Acc: {val_acc*100:.2f}%]")
    print(f"\n----조건[{[key]}] 훈련 종료----\n")

위 사진같이 결과가 각 모델-조건별로 연속해서 출력되면 정상적으로 학습이 되고 있는 것이다.


학습 결과 분석

import matplotlib.pyplot as plt
# 한글 사용을 위한 폰트 포함
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# 학습/검증 결과 데이터를 재배치
res_data = {}

for key in key_list:
    res_data[key] = {}
    for metric in metrics_key:
        # 각 모델의 메트릭 데이터 추출
        metric_data = history[key][metric]
        # 훈련 및 검증 값 분리
        train_values = [tup[0] for tup in metric_data]
        val_values = [tup[1] for tup in metric_data]
        res_data[key][f'훈련_{metric}'] = train_values
        res_data[key][f'검증_{metric}'] = val_values
# 손실 및 정확도 그래프 그리기 그래프 생성
fig, axes = plt.subplots(3, 4, figsize=(12, 12))
axes = axes.flatten()  # 2차원 배열을 1차원으로 변환하여 인덱싱 쉽게 함

# 손실 그래프 그리기
for idx, key in enumerate(key_list):
    ax = axes[idx*2] #손실 그래프는 0, 2번째에 위치
    ax.plot(res_data[key]['훈련_Loss'], label='훈련 로스')
    ax.plot(res_data[key]['검증_Loss'], label='검증 로스')

    ax.set_title(f'{key} 텍스트 분류기 Loss')
    ax.set_xlabel('Epoch')
    ax.set_ylabel('Loss')
    ax.legend()

# 정확도 그래프 그리기
for idx, key in enumerate(key_list):
    ax = axes[idx*2 + 1] #정확도 그래프는 1, 3번째에 위치
    ax.plot(res_data[key]['훈련_정확도'], label='훈련 정확도')
    ax.plot(res_data[key]['검증_정확도'], label='검증 정확도')
    ax.set_title(f'{key} 텍스트 분류기 정확도')
    ax.set_xlabel('Epoch')
    ax.set_ylabel('정확도')
    ax.legend()


plt.tight_layout()
plt.show()

# 모든 모델의 손실과 정확도를 비교하는 그래프 생성
fig, axes = plt.subplots(2, 1, figsize=(10, 18))

# 모든 모델의 손실 그래프
ax = axes[0]
for key in key_list:
    ax.plot(res_data[key]['훈련_Loss'], label=f'{key} 훈련 로스')
    ax.plot(res_data[key]['검증_Loss'], label=f'{key} 검증 로스', linestyle='--')
ax.set_title('모든 조건별 모델의 훈련/검증 Loss')
ax.set_xlabel('Epoch')
ax.set_ylabel('Loss')
ax.legend()

# 모든 모델의 정확도 그래프
ax = axes[1]
for key in key_list:
    ax.plot(res_data[key]['훈련_정확도'], label=f'{key} 훈련 정확도')
    ax.plot(res_data[key]['검증_정확도'], label=f'{key} 검증 정확도', linestyle='--')
ax.set_title('모든 조건별 모델의 훈련/검증 정확도')
ax.set_xlabel('Epoch')
ax.set_ylabel('정확도')
ax.legend()

plt.tight_layout()
plt.show()

실험 결과를 본다면

이전 포스트처럼 RNN은 개념만 숙지하고 실 사용으로는 적합하지 않은 모델이라는 것을 재확인 할 수 있으며

LSTM이랑 GRU는 거의 성능이 유사하다 볼 수 있으나
현재 데이터셋의 크기가 그렇게 큰 편은 아니어서
두 모델간의 우열을 정확히 가르기에는 좀 부적절하다.

또 스팸 분류기나 감정분석기 같은 classification은
자연어 처리에서는 기본적인 자연어 이해, NLU(Natural Language Understanding) Task에 속하는거라

이것보다는 좀 더 난이도가 높은
Many-to-Many 방법론 : 개채명 인식(Named Entity Recognition)
실습을 통해 LSTMGRU의 성능을 비교분석하고자 한다.

profile
자율차 공부중

0개의 댓글