인공지능/자연어 처리

자연어 처리 Python 실습 - 한국어 관계 추출

이게될까 2024. 4. 11. 03:03
728x90
728x90

Introduction

Chapter 7. 자연어 이해(NLU) Task 강의의 한국어 관계 추출 Task 실습 강의입니다.

이번 실습에서는 (1) KLUE-BERT base 모델을 KLUE 벤치마크 데이터셋의 KLUE-RE 데이터셋으로 파인튜닝하고, (2) 파인튜닝한 모델의 관계 추출 작업에 대한 성능과 결과를 분석해보겠습니다.

!pip install transformers

1. KLUE-BERT 모델 불러오기

오늘 실습에는 대표적인 한국어 BERT 모델인 KLUE-BERT 모델을 사용합니다!
KLUE-BERT 모델은 벤치마크 데이터인 KLUE에서 베이스라인으로 사용되었던 모델로,모두의 말뭉치, CC-100-Kor, 나무위키, 뉴스, 청원 등 문서에서 추출한 63GB의 데이터로 학습된 모델입니다.

Morpheme-based Subword Tokenizer를 사용하였으며, vocab size는 32,000이고 모델의 크기는 111M입니다.

KLUE Benchmark : https://klue-benchmark.com/

KLUE-BERT : https://huggingface.co/klue/bert-base

from transformers import AutoTokenizer, AutoConfig, AutoModelForSequenceClassification
MODEL_NAME = "klue/bert-base" # 이름을 통해 모델을 불러온다.

오늘 사용할 데이터셋의 관계 레이블 개수는 30개이므로, 미리 모델 config에 반영합니다

model_config =  AutoConfig.from_pretrained(MODEL_NAME) 
model_config.num_labels = 30 # 레이블 수
model_config 



model =  AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, config=model_config) # 30개의 레이블을 사용한다
model.parameters # 레이어를 다 볼 수 있다.


print(model.config)

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)

2. 데이터셋 로드하기

본 실습에서는 KLUE 벤치마크 데이터셋 중 하나인 KLUE-RE 데이터셋을 사용하여, 자연어 추론 실습을 진행합니다.

해당 데이터셋에 대한 라이센스는 여기서 확인할 수 있습니다.

2.1. 관계 추출(RE) 작업 설명

관계 추출(RE) 작업은 텍스트에서 엔티티 쌍 간의 의미론적 관계를 식별하는 작업입니다.
구체적으로, 관계는 subject entity와 object entity 쌍 간에 정의되는 값으로,

예를 들어서, 'Kierkegaard was born to an affluent family in Copenhagen’이라는 문장에서

subject_entityKierkegaard, object_entityCopenhagen일 때,

관계 추출 작업의 목표는 두 엔티티 간의 관계가 place_of_birth임을 예측하는 것입니다.

2.2. 데이터셋 특징

본 데이터셋은 관계 추출(RE) 작업을 단일 문장 분류 작업으로 공식화하며,

따라서, 관계 추출 모델은 주어진 문장 내 두 엔티티 간의 관계를 설명하는 사전 정의된 관계 유형 중 하나를 예측해야 합니다.
데이터셋은 총 30개의 사전 정의된 관계 레이블을 사용하며, 구체적으로 18개의 person_related 관계, 11개의 organization_related 관계, 그리고 no_relation이 있습니다.

스크린샷 2023-01-16 오후 10.06.56.png

데이터셋은 WIKIPEDIA, WIKITREE 및 정책 말뭉치에서 후보 문장을 샘플링한 뒤, 다양한 엔터티 및 관계형 사실들을 추출하여 구축되었습니다.

데이터셋 내 문장의 최대 길이는 최대 510단어로 제한되었고,

우리는 사회적으로 편향될 수 있는 일부 문장을 걸러내거나 맥락 없이 제시될 때 증오심 표현을 포함할 수 있습니다.

스크린샷 2023-01-16 오후 10.08.27.png

KLUE RE 벤치마크 데이터셋은 (1) micro-F1 score, (2) precision-recall curve (AUPRC)로 평가 됩니다.

2.3. 데이터셋 로드

KLUE 벤치마크 데이터셋과 KLUE-RE 데이터셋은 다음의 위치에서 다운로드할 수 있습니다.

!wget -O ./raw_train_dataset.json https://raw.githubusercontent.com/KLUE-benchmark/KLUE/main/klue_benchmark/klue-re-v1.1/klue-re-v1.1_train.json
!wget -O ./raw_test_dataset.json https://raw.githubusercontent.com/KLUE-benchmark/KLUE/main/klue_benchmark/klue-re-v1.1/klue-re-v1.1_dev.json
!wget -O ./relation_list.json https://raw.githubusercontent.com/KLUE-benchmark/KLUE/main/klue_benchmark/klue-re-v1.1/relation_list.json


import pandas as pd
import torch
import json
label_list = []

with open("relation_list.json", "r") as f:
  relation_list = json.load(f) # 제이슨을 리스트 형식으로 변화해준다.
  label_list = relation_list["relations"]

print(label_list[:10])
print(len(label_list))

['no_relation', 'org:dissolved', 'org:founded', 'org:place_of_headquarters', 'org:alternate_names', 'org:member_of', 'org:members', 'org:political/religious_affiliation', 'org:product', 'org:founded_by']
30

def tranform_to_dataset(file_path):
  dataset = []
  with open(file_path, "r") as f:
    raw_data = json.load(f)
    for data in raw_data:
      dataset.append([
          data["sentence"],
          data["subject_entity"],
          data["object_entity"],
          data["label"]
      ])
  return dataset

raw_train_dataset = tranform_to_dataset("raw_train_dataset.json")
raw_test_dataset = tranform_to_dataset("raw_test_dataset.json")

print('Length of Train : ',len(raw_train_dataset)) # 32470
print('Length of Test : ',len(raw_test_dataset)) #7745
raw_test_dataset[0]

["20대 남성 A(26)씨가 아버지 치료비를 위해 B(30)씨가 모아둔 돈을 훔쳐 인터넷 방송 BJ에게 '별풍선'으로 쏜 사실이 알려졌다.",
{'word': 'A', 'start_idx': 7, 'end_idx': 7, 'type': 'PER'},
{'word': '30', 'start_idx': 29, 'end_idx': 30, 'type': 'NOH'},
'no_relation']

이번에는 데이터셋 입력이 복잡하므로, tokenized_dataset 형식을 먼저 만든 뒤, 데이터셋 클래스에서 사용합니다.

[CLS] + subject_entity + [SEP] + object_entity + [SEP] + sentence + [SEP]

def tokenized_dataset(dataset, tokenizer):
  """ tokenizer에 따라 sentence를 tokenizing 합니다."""
  concat_entity = []
  sentences = []
  labels = []

  for sentence, subject_entity, object_entity, label in dataset:
    temp = ''
    temp = subject_entity['word'] + '[SEP]' + object_entity['word']
    concat_entity.append(temp)
    sentences.append(sentence)
    labels.append(label)

  tokenized_sentences = tokenizer(
    concat_entity,
    sentences,
    return_tensors="pt",
    padding=True,
    truncation=True,
    max_length=256,
    add_special_tokens=True,
  )

  return tokenized_sentences, labels

#이번 실습에서는 3000개의 학습 데이터셋과 1000개의 테스트 데이터셋만 사용해볼게요!

tokenized_train_dataset, train_labels = tokenized_dataset(raw_train_dataset[:3000], tokenizer)
tokenized_test_dataset, test_labels = tokenized_dataset(raw_test_dataset[:1000], tokenizer)

tokenized_train_dataset[0].ids

2.4. 데이터셋 클래스 선언

import torch
from torch import nn
import torch.nn.functional as F
import numpy as np

from torch.utils.data import Dataset, DataLoader

class RE_Dataset(Dataset):
  """ Dataset 구성을 위한 class."""
  def __init__(self, pair_dataset, labels, dict_label_to_num):
    self.pair_dataset = pair_dataset
    self.labels = labels

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

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

#label_dict 선언
idx_list = [i for i in range(len(label_list))]
dict_label_to_num = dict(zip(label_list, idx_list))
print(dict_label_to_num)

{'no_relation': 0, 'org:dissolved': 1, 'org:founded': 2, 'org:place_of_headquarters': 3, 'org:alternate_names': 4, 'org:member_of': 5, 'org:members': 6, 'org:political/religious_affiliation': 7, 'org:product': 8, 'org:founded_by': 9, 'org:top_members/employees': 10, 'org:number_of_employees/members': 11, 'per:date_of_birth': 12, 'per:date_of_death': 13, 'per:place_of_birth': 14, 'per:place_of_death': 15, 'per:place_of_residence': 16, 'per:origin': 17, 'per:employee_of': 18, 'per:schools_attended': 19, 'per:alternate_names': 20, 'per:parents': 21, 'per:children': 22, 'per:siblings': 23, 'per:spouse': 24, 'per:other_family': 25, 'per:colleagues': 26, 'per:product': 27, 'per:religion': 28, 'per:title': 29}

train_dataset = RE_Dataset(tokenized_train_dataset, train_labels, dict_label_to_num)
test_dataset = RE_Dataset(tokenized_test_dataset, test_labels, dict_label_to_num)

3. 학습 파라미터 설정

import torch
from transformers import Trainer, TrainingArguments

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

model.to(device)

이번 실습에서는 학습 코드를 직접 짜는 대신

Transformers의 Trainer API를 사용해볼게요!

🤗Transformers는 Trainer 클래스를 제공함으로써 준비된 데이터셋으로 사전 학습된 모델(pretrained models)을 미세 조정(fine-tuning)할 수 있도록 도와줍니다.

https://huggingface.co/docs/transformers/main_classes/trainer

num_epochs = 1
batch_size = 16
learning_rate = 5e-5
weight_decay = 0.01
warmup_steps = 100

logging_steps = 100
training_args = TrainingArguments(
  output_dir='./results',          # output directory
  save_total_limit=5,              # number of total save model.
  num_train_epochs=num_epochs,              # total number of training epochs
  learning_rate=learning_rate,               # learning_rate
  per_device_train_batch_size=batch_size,  # batch size per device during training
  per_device_eval_batch_size=batch_size,   # batch size for evaluation
  warmup_steps=warmup_steps,                # number of warmup steps for learning rate scheduler
  weight_decay=weight_decay,               # strength of weight decay
  logging_dir='./logs',            # directory for storing logs
  logging_steps=logging_steps,              # log saving step.
  evaluation_strategy='epoch', # evaluation strategy to adopt during training
  save_strategy='epoch', 
  load_best_model_at_end = True 
)

4. 모델 학습 및 분석

import os
import pandas as pd
import numpy as np
import sklearn
import matplotlib.pyplot as plt
import itertools

from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score, confusion_matrix

4.1. 평가 metric 선언

이번 실습에서는 KLUE-RE 벤치마크 데이터셋의 성능 평가 지표와 동일한 지표를 사용하여 성능을 평가해볼게요.

def klue_re_micro_f1(preds, labels, label_list):
    """KLUE-RE micro f1 (except no_relation)"""
    no_relation_label_idx = label_list.index("no_relation")

    label_indices = list(range(len(label_list)))
    label_indices.remove(no_relation_label_idx)

    return sklearn.metrics.f1_score(labels, preds, average="micro", labels=label_indices) * 100.0

def klue_re_auprc(probs, labels):
    """KLUE-RE AUPRC (with no_relation)"""
    labels = np.eye(30)[labels]
    score = np.zeros((30,))

    for c in range(30):
        targets_c = labels.take([c], axis=1).ravel()
        preds_c = probs.take([c], axis=1).ravel()
        precision, recall, _ = sklearn.metrics.precision_recall_curve(targets_c, preds_c)
        score[c] = sklearn.metrics.auc(recall, precision)

    return np.average(score) * 100.0

def plot_confusion_matrix(c_matrix, labels, title='Confusion Matrix', cmap=plt.cm.get_cmap('Blues')):
    plt.figure(figsize=(20,20))
    plt.imshow(c_matrix, interpolation='nearest', cmap=cmap)
    plt.title(title)
    plt.colorbar()
    marks = np.arange(len(labels))
    nlabels = []

    for k in range(len(c_matrix)):
        n = sum(c_matrix[k])
        nlabel = '{0}(n={1})'.format(labels[k],n)
        nlabels.append(nlabel)

    # plt.xticks(marks, labels)
    plt.yticks(marks, nlabels)

    thresh = c_matrix.max() / 2.

    for i, j in itertools.product(range(c_matrix.shape[0]), range(c_matrix.shape[1])):
      plt.text(j, i, c_matrix[i, j], horizontalalignment="center", color="white" if c_matrix[i, j] > thresh else "black") # 몇개씩 맞췄는지시

    plt.tight_layout()
    plt.ylabel('True label')
    plt.xlabel('Predicted label')
    plt.show()


#Trainer 모듈에서 사용할 compute_metrics 함수를 정의합니다.
def compute_metrics(pred):
  """ validation을 위한 metrics function """
  labels = pred.label_ids
  preds = pred.predictions.argmax(-1)
  probs = pred.predictions

  test_confusion_matrix = confusion_matrix(labels, preds)
  plot_confusion_matrix(test_confusion_matrix, labels=label_list)

  # calculate accuracy using sklearn's function
  f1 = klue_re_micro_f1(preds, labels, label_list)
  auprc = klue_re_auprc(probs, labels)
  acc = accuracy_score(labels, preds) # 리더보드 평가에는 포함되지 않습니다.

  return {
      'micro f1 score': f1,
      'auprc' : auprc,
      'accuracy': acc,
  }

4.2 모델 학습

trainer = Trainer(
  model=model,                         # the instantiated 🤗 Transformers model to be trained
  args=training_args,                  # training arguments, defined above
  train_dataset=train_dataset,         # training dataset
  eval_dataset=test_dataset,             # evaluation dataset
  compute_metrics=compute_metrics         # define metrics function
)

trainer.train()
model.save_pretrained('./best_model')

학습이 덜 되어서 패턴을 잘 못찾았다. 

728x90