1. 개요
문장 쌍 분류: 문장 2개가 주어질 때 해당 문장 사이 관계가 어떤 범주인지 분류하는 과제
자연어 추론은 2개의 문장이 참, 거짓, 중립, 판단불가인지 가려내는 것.
참(entailment)는 함의로, 거짓(contradiction)은 모순으로 보기도 한다.
사용하는 데이터셋 : 업스테이지에서 공개한 NLI 데이터셋
- 레이블에 전제에 대한 가설이 참인지, 거짓인지, 중립인지 주어져 있음.
NLP 과제 수행 모델
- 전제 문장 & 가설 문장 입력
- 두 문장의 관계가 어떤 범주일지 확률을 출력
- 후처리 과정
(예시)
-
전제 : 100분간 잘 껄 그래도 xx 때문에 2점준다.
가설1: 100분간 잤다.
가설2: 소닉붐이 정말 멋있었다.
-
확률
pythonA = [[0.02, 0.97, 0.01], # 가설1 [0.01, 0.01, 0.98]] # 가설2 -
후처리
가설1 = 거짓
가설2 = 중립
모델 구조
-
전제와 가설 두 문장을 각각 토큰화한다.
-
[CLS] + 전제 + [SEP] + 가설 + [SEP] 형태로 이어 붙인다.
CLS : 문장의 시작을 알리는 토큰
SEP : 전제와 가설을 서로 구분해 주는 토큰
-
BERT 모델에 입력하고 문장 수준의 벡터(pooler_out)를 뽑는다.
-
추가 모듈을 붙여 결과가 [참일 확률, 거짓일 확률, 중립일 확률] 로 만든다.
추가 모듈은 뉴럴넷을 사용한다..
- pooler_out 벡터에 드롭아웃 적용
- 가중치 행렬을 곱해 pooler_out을 분류해야 할 범주 수만큼의 차원을 가지는 벡터로 변환
- 소프트맥스 함수 사용 → 최종 출력 도출
최종 출력과 정답 레이블을 비교하며 정답 레이블에 같아지게끔 모델 업데이트.
4장에서의 문서 분류 모델과 거의 비슷.
pooler_output에 문장 1개의 의미가 응축되어 있다면 문서분류 (4장)
pooler_output에 문장 2개의 의미가 응축되어 있다면 문장 쌍 분류 과제 (5장)
2. 모델 학습하기
코랩 노트북 초기화
- TPU 패키지
!pip install cloud-tpu-client==0.10 torch==1.12.0 https://storage.googleapis.com/tpu-pytorch/wheels/colab/torch_xla-1.12-cp37-cp37m-linux_x86_64.whl
설정
의존성 패키지
!pip install ratsnlp
구글 드라이브 패키지
from google.colab import drive
drive.mount('/gdrive', force_remount=True)
모델 환경설정
import torch
from ratsnlp.nlpbook.classification import ClassificationTrainArguments
args = ClassificationTrainArguments(
pretrained_model_name="beomi/kcbert-base",
downstream_task_name="pair-classification",
downstream_corpus_name="klue-nli",
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=3,
tpu_cores=0 if torch.cuda.is_available() else 8,
seed=7,
)
상세설명 (참고 : 4.2장)
- 모델이름
- 다운스트림 태스크 이름배치 사이즈
- 에포크 : 3으로 조절했다
랜덤 시드 고정
from ratsnlp import nlpbook
nlpbook.set_seed(args)
학습 재현을 위해 랜덤 시드를 고정
로거 설정
nlpbook.set_logger(args)
message 출력을 위한 logger 설정
말뭉치 내려받기
nlpbook.download_downstream_dataset(args)
토크나이저 준비
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(
args.pretrained_model_name,
do_lower_case=False,
)
데이터 전처리 & 학습 데이터 로더 구축
from ratsnlp.nlpbook.paircls import KlueNLICorpus
from ratsnlp.nlpbook.classification import ClassificationDataset
from torch.utils.data import DataLoader, SequentialSampler, RandomSampler
corpus = KlueNLICorpus()
train_dataset = ClassificationDataset(
args=args,
corpus=corpus,
tokenizer=tokenizer,
mode="train",
)
train_dataloader = DataLoader(
train_dataset,
batch_size=args.batch_size,
sampler=RandomSampler(train_dataset, replacement=False),
collate_fn=nlpbook.data_collator,
drop_last=False,
num_workers=args.cpu_workers,
)
KlueNLICorpus 클래스
- JSON 파일 형식의 KLUE-NLI 데이터를 문장과 레이블 형식으로 읽음.
- 문장 : 전제 + 가설.
- 레이블 : 참, 거짓, 중립
ClassificationDataset 클래스
- KlueNUICorpus와 토크나이저를 가지고 있음.
- 이를 모델이 읽을 수 있는 형태로 가공
- 전제와 가설을 토큰화 및 인덱스로 변형
- 레이블 역시 정수로 변환
- max_seq_length 보다 길면 줄임.
이를 시행한 뒤 train_dataset[0]을 시행하면 다음과 같음
평가 데이터 로더 만들기
val_dataset = ClassificationDataset(
args=args,
corpus=corpus,
tokenizer=tokenizer,
mode="test",
)
val_dataloader = DataLoader(
val_dataset,
batch_size=args.batch_size,
sampler=SequentialSampler(val_dataset),
collate_fn=nlpbook.data_collator,
drop_last=False,
num_workers=args.cpu_workers,
)
모델 불러오기
from transformers import BertConfig, BertForSequenceClassification
pretrained_model_config = BertConfig.from_pretrained(
args.pretrained_model_name,
num_labels=corpus.num_labels,
)
model = BertForSequenceClassification.from_pretrained(
args.pretrained_model_name,
config=pretrained_model_config,
)
- 4-2절에 썼던 것과 동일
모델 학습
from ratsnlp.nlpbook.classification import ClassificationTask
task = ClassificationTask(model, args)
트레이너 정의
trainer = nlpbook.get_trainer(args)
학습 개시
trainer.fit(
task,
train_dataloaders=train_dataloader,
val_dataloaders=val_dataloader,
)
3. 모델 실전투입
환경설정
의존성 패키지
!pip install ratsnlp
구글 드라이브 설정
drive.mount('/gdrive', force_remount=True)
각종 인퍼런스 설정
from ratsnlp.nlpbook.classification import ClassificationDeployArguments
args = ClassificationDeployArguments(
pretrained_model_name="beomi/kcbert-base",
downstream_model_dir="/gdrive/My Drive/nlpbook/checkpoint-paircls",
max_seq_length=64,
)
모델 학습시키기
토크나이저 로드
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(
args.pretrained_model_name,
do_lower_case=False,
)
체크포인트 로드
import torch
from transformers import BertConfig, BertForSequenceClassification
fine_tuned_model_ckpt = torch.load(
args.downstream_model_checkpoint_fpath,
map_location=torch.device("cpu")
)
pretrained_model_config = BertConfig.from_pretrained(
args.pretrained_model_name,
num_labels=fine_tuned_model_ckpt['state_dict']['model.classifier.bias'].shape.numel(),
)
model = BertForSequenceClassification(pretrained_model_config)
model.load_state_dict({k.replace("model.", ""): v for k, v in fine_tuned_model_ckpt['state_dict'].items()})
model.eval()
출력값 만들고 후처리
인퍼런스 함수 선언, Bert 설정 및 초기화
def inference_fn(premise, hypothesis):
inputs = tokenizer(
[(premise, hypothesis)],
max_length=args.max_seq_length,
padding="max_length",
truncation=True,
)
with torch.no_grad():
outputs = model(**{k: torch.tensor(v) for k, v in inputs.items()})
prob = outputs.logits.softmax(dim=1)
entailment_prob = round(prob[0][0].item(), 2)
contradiction_prob = round(prob[0][1].item(), 2)
neutral_prob = round(prob[0][2].item(), 2)
if torch.argmax(prob) == 0:
pred = "참 (entailment)"
elif torch.argmax(prob) == 1:
pred = "거짓 (contradiction)"
else:
pred = "중립 (neutral)"
return {
'premise': premise,
'hypothesis': hypothesis,
'prediction': pred,
'entailment_data': f"참 {entailment_prob}",
'contradiction_data': f"거짓 {contradiction_prob}",
'neutral_data': f"중립 {neutral_prob}",
'entailment_width': f"{entailment_prob * 100}%",
'contradiction_width': f"{contradiction_prob * 100}%",
'neutral_width': f"{neutral_prob * 100}%",
}
웹 서비스 시작하기
- ngrok과 토큰
- 인증 토큰 받고 입력하기
!mkdir /root/.ngrok2 && echo "authtoken: {코드입력}" > /root/.ngrok2/ngrok.yml
- 웹 서비스 시작
from ratsnlp.nlpbook.paircls import get_web_service_app
app = get_web_service_app(inference_fn)
app.run()
만약 웹서비스가 안된다면?
while 1:
a = input("전제 : ")
b = input("가정 : ")
if a == "중지" or b == "중지":
break
print(inference_fn(a, b))