[NLP] BERT 실습 예제: 네이버 영화 리뷰 감정 분류

fragrance_0·2023년 11월 23일
0

NLP

목록 보기
5/5
post-custom-banner

💡 BERT를 활용한 네이버 영화 리뷰 감정 분류

허깅페이스(Hugging Face)에서 제공하는 transformer 라이브러리를 활용하여 한국어 BERT로 네이버 영화 리뷰의 감정을 분류해보도록 하겠습니다.

허깅페이스

🔗 1. 데이터 전처리

1.1. 데이터 불러오기

!git clone https://github.com/e9t/nsmc.git

깃헙

1.2. 데이터 살펴보기

3개의 컬럼 id document label에 총 20만 개의 데이터로 구성되어 있습니다. .txt로 제공되어도 pd.read_csv()로 불러올 수 있는데, 이때 구분자(delimiter)를 탭(\t)으로 불러옵니다.

import pandas as pd

df = pd.read_csv("./nsmc/ratings.txt", delimiter='\t', quoting=3); df.info()

1.3. 데이터 결측치 제거

df.info()로 데이터 프레임의 정보를 살펴보면 결측치가 존재합니다. 해당 정보들은 추후 토큰화 및 학습 과정에서 오류를 발생시킬 수 있으므로 제거합니다.

-> 네이버 영화 리뷰 감정분석이므로, 결측치를 제거 해도 큰 상관 없으니까

df.dropna(inplace = True) # inplace = True로 설정하면 데이터프레임에 즉시 적용됩니다.

1.4. Okt 라이브러리를 활용한 토큰화

한글 자연어처리를 지원하는 konlpy 라이브러리를 설치합니다. 해당 라이브러리는 다양한 한국어 토크나이저를 제공하는데 대표적으로 OktMecab이 있습니다. Mecab은 학습 속도가 빠르고 사용자 사전 추가 등 활용도 높은 기능을 제공하지만 설치 과정이 복잡합니다. OktMecab에 비해 약간 느리지만 준수한 성능을 보이고 어간 추출(Stemming) 기능을 제공합니다. 간단하게 여기서는 Okt를 활용합니다.

!pip install konlpy

from konlpy.tag import Okt
okt = Okt()
df['documnet'] = df['document'].map(lambda x: ' '.join(okt.morphs(x, stem = True)))

‘ ‘.join을 하여 다시 하나의 문자열로 반환하는 이유는 BERT를 위한 자체적인 토크나이저가 별도로 있기 때문입니다. 그럼에도 Okt로 토큰화를 하는 이유는 텍스트 정규화를 진행하기 위해서입니다. 위의 사례에서 볼 수 있듯이 띄어쓰기 등 문법이 많이 지켜지지 않았는데요. 온라인 텍스트 데이터 특성상 오탈자, 비문 등이 많이 존재합니다. 따라서 우선 토큰화 이후 띄어쓰기를 하면서 정규화 작업을 진행합니다. 해당 작업만으로도 1-2%의 성능 개선이 있었습니다.

1.5. 일부 데이터만 불러오기

실습 환경 사양에 따라 20만 개의 데이터를 처리하기 어려울 수 있습니다. 또한 많은 데이터는 정확도를 높이는 데 도움이 되지만 학습 속도가 매우 느립니다. Colab의 경우에도 해당 데이터를 처리하기 어렵고 오래 걸리기 때문에 임의로 2만 개의 데이터만 설정합니다.

df = pd.concat([df.iloc[:10000], df.iloc[-10000:]])

🔗 2. 모델 학습

2.1. 훈련-테스트 데이터 분할

사이킷런에서 제공하는 train_test_split 함수로 간편하게 훈련 - 테스트 데이터를 나눌 수 있습니다. 2만 개의 데이터도 적은 수는 아니나 더 다양한 케이스를 학습하기 위해 20%를 test 데이터로 구분했습니다.

from sklearn.model_selection import train_test_split

train_df, test_df = train_test_split(df, test_size=0.2, random_state=42)

train_texts = train_df['document'].astype(str).tolist() # 문자열 데이터로 명시 후 리스트 화
train_labels = train_df['label'].tolist()
test_texts = test_df['document'].astype(str).tolist()
test_labels = test_df['label'].tolist()

2.2. BERT 토크나이저 불러오기

다양한 딥러닝 / 머신러닝 모델을 지원하는 허깅페이스(HuggingFace)에서 올라온 모델을 쉽게 활용할 수 있는 trasnformers 라이브러리를 제공합니다. transformers를 설치한 후 모델 이름만 지정하면 이미 내장된 값들을 불러올 수 있습니다. 원하는 모델은 아래에서 찾아볼 수 있습니다. 보통 한국어 모델은 ‘ko’를 입력하여 찾을 수 있습니다.

hugging face | models

이번에 활용할 모델은 대표적인 한국어 BERT 모델인 KoBERT입니다. 모델의 이름은 위의 홈페이지에서 찾아볼 수 있고 복사 버튼을 통해 쉽게 가지고 올 수 있습니다.

!pip install transformers

from transformers import BertTokenizer, BertForSequenceClassification

model_name = 'monologg/kobert'
tokenizer = BertTokenizer.from_pretrained(model_name)

train_encodings = tokenizer(train_texts, truncation=True, padding=True)
test_encodings = tokenizer(test_texts, truncation=True, padding=True)

2.3. DataLoader 구성

PyTorch에서 제공하는 DataLoader를 활용하여 훈련 데이터를 섞고(Shuffle) 배치(Batch) 단위로 불러올 수 있습니다. 파라미터가 많은 모델의 경우 많은 양을 한번에 처리하기 어렵습니다. 배치는 한 번에 처리할 데이터 집합을 의미하고, batch_size는 몇 개의 데이터를 포함할지 배치의 크기(양)를 의미합니다.

import torch
from torch.utils.data import DataLoader, Dataset

class CustomDataset(torch.utils.data.Dataset):
    def __init__(self, encodings, labels):
        self.encodings = encodings
        self.labels = labels

    def __getitem__(self, idx):
        item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
        item['labels'] = torch.tensor(self.labels[idx])
        return item

    def __len__(self):
        return len(self.labels)

train_dataset = CustomDataset(train_encodings, train_labels)
test_dataset = CustomDataset(test_encodings, test_labels)
batch_size = 64  # 배치 사이즈는 직접 지정해야 합니다.
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

2.4. BERT 모델 불러오기

분류를 위한 모델로 앞서 토크나이저와 동일한 모델 이름을 지정합니다. 해당 데이터의 경우 레이블이 2개(0, 1)이므로 num_labels = 2로 지정합니다.

model = BertForSequenceClassification.from_pretrained('monologg/kobert', num_labels=2) # 0, 1로 분류하기 때문에 레이블은 2개로 지정합니다.

2.5. 모델 훈련

Epoch이나 학습률 등은 직접 지정해주어야 하는 하이퍼파라미터입니다. 이진 분류 문제를 위해 손실 함수Cross Entropy를 사용합니다.

BERT 모델은 동일한 길이의 문장 벡터를 입력으로 받습니다. 하지만 주어진 문장의 토큰 길이는 모두 다르기 때문에 Attention Mask를 별도로 지정하여 필요한 값만 학습될 수 있도록 합니다.

from tqdm.auto import tqdm # 반복문이 얼마나 진행되었는지 알 수 있도록 프로그레스바를 표시합니다.

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # GPU 사용이 가능한 경우 설정

num_epochs = 10 
learning_rate = 2e-5 #2e-5는 0.00002
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
criterion = torch.nn.CrossEntropyLoss()
model.to(device) # GPU 사용이 가능한 경우

for epoch in range(num_epochs):
    model.train() # 훈련 모드 지정
    total_loss = 0

    for batch in tqdm(train_loader):
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        labels = batch['labels'].to(device)

        optimizer.zero_grad()
        outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
        loss = outputs.loss
        total_loss += loss.item()

        loss.backward()
        optimizer.step()

    average_loss = total_loss / len(train_loader)
    print(f"Epoch {epoch+1}/{num_epochs} - Average Loss: {average_loss:.4f}")

2.6. 모델 테스트

모델을 평가 모드로 지정하고 테스트 데이터에 대해 정확도를 파악합니다. 최종적으로 약 62%의 성능을 기록했습니다. 높은 수치는 아닙니다. 다만, 해당 데이터의 문장 길이가 대체로 짧고 텍스트에 긍부정을 판단할 만한 내용이 많지 않기 때문에 어려운 작업일 것으로 예상합니다.

model.eval()
correct_predictions = 0
total_predictions = 0

with torch.no_grad():
    for batch in test_loader:
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        labels = batch['labels'].to(device)

        outputs = model(input_ids, attention_mask=attention_mask)
        _, predicted_labels = torch.max(outputs.logits, dim=1)

        correct_predictions += torch.sum(predicted_labels == labels).item()
        total_predictions += labels.size(0)

accuracy = correct_predictions / total_predictions
print(f"Test Accuracy: {accuracy:.4f}")

>>> Test Accuracy: 0.6218

2.7. 추론

input_text를 통해 임의의 프롬프트를 입력하고 추론(Inference) 결과를 내봅니다. 학습된 모델에 대해 입력값을 넣어 결과를 예측하는 과정입니다. 이를 위해 입력 문장을 토큰화하고 모델에 입력하여 레이블을 예측합니다.

input_text = '이 영화 진짜 재밌다'
input_encoding = tokenizer.encode_plus(
    input_text,
    truncation=True,
    padding=True,
    return_tensors='pt'
)

input_ids = input_encoding['input_ids'].to(device)
attention_mask = input_encoding['attention_mask'].to(device)

model.eval()
with torch.no_grad():
    outputs = model(input_ids, attention_mask=attention_mask)
    _, predicted_labels = torch.max(outputs.logits, dim=1)
predicted_labels = predicted_labels.item()

print(predicted_labels)

>>> 1 # 긍정

🌈 전체 흐름 정리

◾️ 데이터 전처리

◾️ 모델 학습

[출처 | 딥다이브 Code.zip 매거진]

profile
@fragrance_0의 개발로그
post-custom-banner

0개의 댓글