열렬히.뛰기

5장 : 문장 쌍 분류 모델

머신러닝 & 딥러닝 > 자연어처리 (1) : BERT와 GPT > 5장 : 문장 쌍 분류 모델

1. 개요

문장 쌍 분류: 문장 2개가 주어질 때 해당 문장 사이 관계가 어떤 범주인지 분류하는 과제

자연어 추론은 2개의 문장이 참, 거짓, 중립, 판단불가인지 가려내는 것.

참(entailment)는 함의로, 거짓(contradiction)은 모순으로 보기도 한다.

사용하는 데이터셋 : 업스테이지에서 공개한 NLI 데이터셋

  • 레이블에 전제에 대한 가설이 참인지, 거짓인지, 중립인지 주어져 있음.

NLP 과제 수행 모델

  1. 전제 문장 & 가설 문장 입력
  2. 두 문장의 관계가 어떤 범주일지 확률을 출력
  3. 후처리 과정

(예시)

  1. 전제 : 100분간 잘 껄 그래도 xx 때문에 2점준다.

    가설1: 100분간 잤다.

    가설2: 소닉붐이 정말 멋있었다.

  2. 확률

    python
    A = [[0.02, 0.97, 0.01],  # 가설1
         [0.01, 0.01, 0.98]]	# 가설2
    
  3. 후처리

    가설1 = 거짓

    가설2 = 중립

모델 구조

  1. 전제와 가설 두 문장을 각각 토큰화한다.

  2. [CLS] + 전제 + [SEP] + 가설 + [SEP] 형태로 이어 붙인다.

    CLS : 문장의 시작을 알리는 토큰

    SEP : 전제와 가설을 서로 구분해 주는 토큰

  3. BERT 모델에 입력하고 문장 수준의 벡터(pooler_out)를 뽑는다.

  4. 추가 모듈을 붙여 결과가 [참일 확률, 거짓일 확률, 중립일 확률] 로 만든다.

추가 모듈은 뉴럴넷을 사용한다..

  • pooler_out 벡터에 드롭아웃 적용
  • 가중치 행렬을 곱해 pooler_out을 분류해야 할 범주 수만큼의 차원을 가지는 벡터로 변환
  • 소프트맥스 함수 사용 → 최종 출력 도출

최종 출력과 정답 레이블을 비교하며 정답 레이블에 같아지게끔 모델 업데이트.

4장에서의 문서 분류 모델과 거의 비슷.

pooler_output에 문장 1개의 의미가 응축되어 있다면 문서분류 (4장)

pooler_output에 문장 2개의 의미가 응축되어 있다면 문장 쌍 분류 과제 (5장)

2. 모델 학습하기

코랩 노트북 초기화

  • TPU 패키지
python
!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

설정

의존성 패키지

python
!pip install ratsnlp

구글 드라이브 패키지

python
from google.colab import drive
drive.mount('/gdrive', force_remount=True)

모델 환경설정

python
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으로 조절했다

랜덤 시드 고정

python
from ratsnlp import nlpbook
nlpbook.set_seed(args)

학습 재현을 위해 랜덤 시드를 고정

로거 설정

python
nlpbook.set_logger(args)

message 출력을 위한 logger 설정

말뭉치 내려받기

python
nlpbook.download_downstream_dataset(args)

토크나이저 준비

python
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(
    args.pretrained_model_name,
    do_lower_case=False,
)

데이터 전처리 & 학습 데이터 로더 구축

python
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]을 시행하면 다음과 같음

평가 데이터 로더 만들기

python
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,
)

모델 불러오기

python
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절에 썼던 것과 동일

모델 학습

python
from ratsnlp.nlpbook.classification import ClassificationTask
task = ClassificationTask(model, args)

트레이너 정의

python
trainer = nlpbook.get_trainer(args)

학습 개시

python
trainer.fit(
    task,
    train_dataloaders=train_dataloader,
    val_dataloaders=val_dataloader,
)

3. 모델 실전투입

환경설정

의존성 패키지

python
!pip install ratsnlp

구글 드라이브 설정

python
drive.mount('/gdrive', force_remount=True)

각종 인퍼런스 설정

python
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,
)

모델 학습시키기

토크나이저 로드

python
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(
args.pretrained_model_name,
do_lower_case=False,
)

체크포인트 로드

python
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 설정 및 초기화

python
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과 토큰
  • 인증 토큰 받고 입력하기
python
!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()

만약 웹서비스가 안된다면?

python
while 1:
  a = input("전제 : ")
  b = input("가정 : ")
  if a == "중지" or b == "중지":
    break
  print(inference_fn(a, b))