허깅페이스(Hugging Face)에서 제공하는 transformer 라이브러리를 활용하여 한국어 BERT로 네이버 영화 리뷰의 감정을 분류해보도록 하겠습니다.
!git clone https://github.com/e9t/nsmc.git
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()
df.info()로 데이터 프레임의 정보를 살펴보면 결측치가 존재합니다. 해당 정보들은 추후 토큰화 및 학습 과정에서 오류를 발생시킬 수 있으므로 제거합니다.
-> 네이버 영화 리뷰 감정분석이므로, 결측치를 제거 해도 큰 상관 없으니까
df.dropna(inplace = True) # inplace = True로 설정하면 데이터프레임에 즉시 적용됩니다.
한글 자연어처리를 지원하는 konlpy
라이브러리를 설치합니다. 해당 라이브러리는 다양한 한국어 토크나이저를 제공하는데 대표적으로 Okt
와 Mecab
이 있습니다. Mecab
은 학습 속도가 빠르고 사용자 사전 추가 등 활용도 높은 기능을 제공하지만 설치 과정이 복잡합니다. Okt
는 Mecab
에 비해 약간 느리지만 준수한 성능을 보이고 어간 추출(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%의 성능 개선이 있었습니다.
실습 환경 사양에 따라 20만 개의 데이터를 처리하기 어려울 수 있습니다. 또한 많은 데이터는 정확도를 높이는 데 도움이 되지만 학습 속도가 매우 느립니다. Colab의 경우에도 해당 데이터를 처리하기 어렵고 오래 걸리기 때문에 임의로 2만 개의 데이터만 설정합니다.
df = pd.concat([df.iloc[:10000], df.iloc[-10000:]])
사이킷런에서 제공하는 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()
다양한 딥러닝 / 머신러닝 모델을 지원하는 허깅페이스(HuggingFace)에서 올라온 모델을 쉽게 활용할 수 있는 trasnformers
라이브러리를 제공합니다. transformers
를 설치한 후 모델 이름만 지정하면 이미 내장된 값들을 불러올 수 있습니다. 원하는 모델은 아래에서 찾아볼 수 있습니다. 보통 한국어 모델은 ‘ko’를 입력하여 찾을 수 있습니다.
이번에 활용할 모델은 대표적인 한국어 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)
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개(0, 1)이므로 num_labels = 2로 지정합니다.
model = BertForSequenceClassification.from_pretrained('monologg/kobert', num_labels=2) # 0, 1로 분류하기 때문에 레이블은 2개로 지정합니다.
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}")
모델을 평가 모드로 지정하고 테스트 데이터에 대해 정확도를 파악합니다. 최종적으로 약 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
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 매거진]