[SSUDA] 자연어 처리 문장 쌍 분류 실습하기 with DACON
!pip install ratsnlp
from google.colab import drive
drive.mount('/content/drive')
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings("ignore")
path = '/content/drive/MyDrive/sentence/'
train = pd.read_csv(path + 'train_data.csv')
test = pd.read_csv(path + 'test_data.csv')
sample_submission = pd.read_csv(path + 'sample_submission.csv')
train.head()
필요한 패키지를 설치하고, 데이터를 불러옵니다.
from Korpora import Korpora
import os
nsmc = Korpora.load('nsmc', force_download=True)
def write_lines(path, lines):
with open(path, 'w', encoding = 'utf-8') as f:
for line in lines:
f.write(f'{line}\n')
write_lines('/root/train.txt', nsmc.train.get_all_texts())
write_lines('/root/test.txt', nsmc.test.get_all_texts())
NSMC는 네이버 영화 리뷰 자료입니다. 실습을 위해 자료를 다운 받고 텍스트 형태로 저장했습니다.
from tokenizers import BertWordPieceTokenizer
wordpiece_tokenizer = BertWordPieceTokenizer(lowercase = False)
wordpiece_tokenizer.train(
files=['/root/train.txt', '/root/test.txt'],
vocab_size = 10000,
)
os.makedirs('/gdrive/My Drive/nlpbook/wordpiece', exist_ok= True)
wordpiece_tokenizer.save_model('/gdrive/My Drive/nlpbook/wordpiece')
BERT 방식으로 토크나이저를 구축했습니다. 토크나이저란 문장을 토큰 시퀀스로 나눈것을 의미하는데요.
BERT 방식은 말뭉치에서 자주 등장하는 문자열을 토큰으로 인식한 뒤 문자열을 병합해 어휘 집합을 구축합니다.
이때 말뭉치의 우도를 가장 높이는 쌍을 먼저 병합하게 됩니다. 두 말뭉치가 동시에 자주 등장할 수록 병합 될 가능성이 높겠죠.
BERT 방식을 간단히 설명하긴 힘들기 때문에 댓글로 질문 남겨주시면 감사하겠습니다.
!head /gdrive/My\ Drive/nlpbook/wordpiece/vocab.txt
BERT로 구성된 어휘집합의 앞 내용들입니다. 여기서 PAD은 더미 토큰으로 길이를 맞춰주는 역할을 합니다.
from transformers import BertTokenizer
tokenizer_bert = BertTokenizer.from_pretrained(
'/gdrive/My Drive/nlpbook/wordpiece',
do_lower_case = False,
)
sentences = [
"아 더빙.. 진짜 짜증나네요 목소리",
"흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나",
"별루 였다..",
]
tokenized_sentences = [tokenizer_bert.tokenize(sentence) for sentence in sentences]
tokenized_sentences
BERT 토크나이저 모델로 예시 문장을 토큰화 시켰습니다. 이때 ## 기호는 문장의 시작이 아닌 것을 의미합니다.
batch_inputs = tokenizer_bert(
sentences,
padding = 'max_length',
max_length = 12,
truncation = True,
)
batch_inputs.keys()
앞선 코드는 시각적으로 살펴보기 위해 한 것이고 이 부분은 실제 모델 입력값입니다.
'input_ids', 'token_type_ids', 'attention_mask', 3가지 입력값이 나옵니다.
input_ids는 실제 문장을 의미하고, token_type_ids는 문장이 2개 입력됬을 경우 0과 1로 두 문장을 구분해줍니다.
마지막으로 attention_mask는 어디까지가 실제 문장인지, 공백 문장이 어딨는지를 알려줍니다.
batch_inputs['input_ids']
어휘 집합에 있는 토큰과 매칭되는 숫자가 실제로 출력됩니다.
from torch.cuda import is_available
import torch
from ratsnlp.nlpbook.classification import ClassificationTrainArguments
args = ClassificationTrainArguments(
pretrained_model_name='beomi/kcbert-base',
downstream_task_name = 'pair-classification', # 문장 쌍 분류를 할 예정이므로
downstream_model_dir='/gdrive/My Drive/nlpbook/checkpoint-paircls',
batch_size = 32 if torch.cuda.is_available() else 4,
learning_rate=5e-5,
max_seq_length=64,
epochs = 5,
tpu_cores=0 if torch.cuda.is_available() else 8,
seed = 7,
)
beomi/kcbert-base 라는 프리트레인을 마친 언어 모델을 사용하여 분석을 진행합니다.
여기서는 ClassificationTrainArguments 클래스를 활용해 사용하는 파라미터 값을 정해집니다.
from ratsnlp import nlpbook
nlpbook.set_seed(args)
nlpbook.set_logger(args)
같은 결과를 재현하기 위해 시드값을 고정해줍니다.
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(
'beomi/kcbert-base',
do_lower_case = False,
)
실제 kcbert-base 모델이 사용하는 토크나이저를 선언합니다. 이를 통해 입력문장을 토큰화 시킵니다.
np.random.seed(42)
tem = np.random.choice(train.shape[0], 5000, replace=False)
val = train.iloc[tem]
평가 데이터 셋을 트레인 데이터 셋중 일부를 사용해 추출합니다.
from ratsnlp.nlpbook.classification import ClassificationExample
from ratsnlp.nlpbook.classification import ClassificationFeatures
train_dataset = []
val_dataset = []
label = {"entailment" : 0, "contradiction" : 1, "neutral" : 2}
# for i in range(train.shape[0]):
# text_a = train['premise'].loc[i]
# text_b = train['hypothesis'].loc[i]
# label = train['label'].loc[i]
# examples.append(ClassificationExample(text_a=text_a, text_b=text_b, label=label))
for i in range(train.shape[0]):
token = tokenizer(
train['premise'].iloc[i], train['hypothesis'].iloc[i],
padding = 'max_length',
max_length = 64,
truncation = True,
)
train_dataset.append(ClassificationFeatures(token.input_ids, token.attention_mask,
token.token_type_ids, label = label[train['label'].iloc[i]]))
for i in range(val.shape[0]):
token = tokenizer(
val['premise'].iloc[i], val['hypothesis'].iloc[i],
padding = 'max_length',
max_length = 64,
truncation = True,
)
val_dataset.append(ClassificationFeatures(token.input_ids, token.attention_mask,
token.token_type_ids, label = label[val['label'].iloc[i]]))
입력된 문장을 앞서 정의한 토크나이저 모델을 이용해 토큰화를 합니다.
이때 ClassificationFeatures 클래스를 이용하면 분류를 위한 라벨까지 한 객체에 넣을 수 있습니다.
max_length = 64에 의미는 문장당 최대 토큰의 길이를 64로 설정한 것 입니다. truncation = True 은 64가 넘는 문장을 잘라낸다는 설정입니다.
from torch.utils.data import DataLoader, RandomSampler
train_dataloader = DataLoader(
train_dataset,
batch_size = 32,
sampler = RandomSampler(train_dataset, replacement = False),
collate_fn = nlpbook.data_collator, # 뽑은 인스턴스를 배치로 바꿔줌(텐서 형태로)
drop_last = False,
num_workers = 1,
)
val_dataloader = DataLoader(
val_dataset,
batch_size = 32,
sampler = RandomSampler(val_dataset, replacement = False),
collate_fn = nlpbook.data_collator, # 뽑은 인스턴스를 배치로 바꿔줌(텐서 형태로)
drop_last = False,
num_workers = 1,
)
토큰화한 데이터 셋을 DataLoader 함수를 이용해 배치화 시킵니다.
from transformers import BertConfig, BertForSequenceClassification
pretrained_model_config = BertConfig.from_pretrained(
'beomi/kcbert-base',
num_labels = 3, # 레이블이 3개이기 때문에
)
model = BertForSequenceClassification.from_pretrained(
'beomi/kcbert-base',
config = pretrained_model_config
)
사용할 모델은 kcbert-base 모델로 토크나이저를 수행했으며 두 문장의 관계를 모순/중립/참 세가지로 분류하는 것이 목적입니다.
from transformers import PreTrainedModel
from transformers.optimization import AdamW
from ratsnlp.nlpbook.metrics import accuracy
from pytorch_lightning import LightningModule
from torch.optim.lr_scheduler import ExponentialLR, CosineAnnealingWarmRestarts
from ratsnlp.nlpbook.classification.arguments import ClassificationTrainArguments
class ClassificationTask(LightningModule):
def __init__(self, model, learning_rate):
super().__init__()
self.model = model
self.learning_rate = learning_rate
def configure_optimizers(self):
optimizer = AdamW(self.parameters(), lr=self.learning_rate)
scheduler = ExponentialLR(optimizer, gamma=0.5)
return {
'optimizer': optimizer,
'scheduler': scheduler,
}
def training_step(self, inputs, batch_idx):
# outputs: SequenceClassifierOutput
outputs = self.model(**inputs)
preds = outputs.logits.argmax(dim=-1)
labels = inputs["labels"]
acc = accuracy(preds, labels)
self.log("loss", outputs.loss, prog_bar=False, logger=True, on_step=True, on_epoch=False)
self.log("acc", acc, prog_bar=True, logger=True, on_step=True, on_epoch=False)
return outputs.loss
def validation_step(self, inputs, batch_idx):
# outputs: SequenceClassifierOutput
outputs = self.model(**inputs)
preds = outputs.logits.argmax(dim=-1)
labels = inputs["labels"]
acc = accuracy(preds, labels)
self.log("val_loss", outputs.loss, prog_bar=True, logger=True, on_step=False, on_epoch=True)
self.log("val_acc", acc, prog_bar=True, logger=True, on_step=False, on_epoch=True)
return outputs.loss
task = ClassificationTask(model, 5e-5)
파이토치 내 LightningModule 클래스를 상속해 ClassificationTask 를 만들었습니다.
trainer = nlpbook.get_trainer(args)
trainer.fit(
task,
train_dataloader = train_dataloader,
val_dataloaders = val_dataloader
)
파이토치 라이트닝 라이브러리를 상속받은 get_trainer로 GPU 설정, 체크포인트 등을 자동으로 설정해줍니다.
그 후 학습을 진행하게 됩니다. 여기서 저는 모든 트레인 데이터를 학습에 사용하고 싶었습니다.
다만 이렇게 되면 val 값을 신용할수는 없겠죠. (val 데이터가 학습 데이터 내부 값이기 때문)
model.eval()
모델을 평가모드로 바꿔줍니다.
for i in range(test.shape[0]):
token = tokenizer(
test['premise'].iloc[i], test['hypothesis'].iloc[i],
padding = 'max_length',
max_length = 64,
truncation = True,
)
token['input_ids'] = [token['input_ids']]
token['attention_mask'] = [token['attention_mask']]
token['token_type_ids'] = [token['token_type_ids']]
outputs = model(**{k: torch.tensor(v) for k, v in token.items()})
prob = outputs.logits.softmax(dim = 1)
labels = torch.argmax(prob)
if labels == 0:
test['label'].iloc[i] = 'entailment'
elif labels == 1:
test['label'].iloc[i] = 'contradiction'
else:
test['label'].iloc[i] = 'neutral'
테스트 데이터를 토크나이저를 이용해 토큰화를 한 뒤 모델에 입력해줍니다.
모델의 출력값 outputs 은 길이가 3인 벡터를 출력해주는데, 이를 소프트맥스 함수에 통과시켜주면 각 범주의 확률값이 나옵니다.
확률값을 이용해 테스트 데이터가 어느 범주에 속해있는지 구해줍니다.
test['label'].value_counts()
train['label'].value_counts()
범주당 비슷한 비율로 구분한 것을 보아 결과가 터무니없지는 않은 것 같습니다.
sample_submission['label'] = test['label']
sample_submission.to_csv('sentence_3.csv',index=False)
나온 결과를 제출파일에 저장하여 제출했습니다. Public 결과는 약 0.624 정도로 앞선 모델이 다소 과적합되어 보입니다.
우선 딥러닝 관련 이론 위주로 항상 공부하다가 실전 데이터를 사용했는데, 확실히 흥미로웠습니다.
직접 구현했다기 보단 라이브러리를 불러온 형식이 많고, 코드 부분부분까지 정확하게 이해하진 못했습니다.
클래스나 함수 내 입력값 형식 관련해서 진행할 때 많은 오류를 겪었습니다. 어짜피 한번 겪을 과정이라 생각했지만 조금 힘들었네요.
다음에는 같은 데이터를 가지고 데이콘 운영자님이 작성하신 베이스라인 코드를 이해가 되는 한에서 리뷰를 해보겠습니다.