기술 블로그

[deep learning] koBART를 사용한 비격식체-격식체 문장전환

young._.88 2022. 11. 25. 04:36

얼레벌레 학교를 다닌 지 벌써 어연 3년 차에 접어들었습니다. 어쩌다 보니 외부에 내보일 대단한 프로젝트 경험 한번 없이 컴퓨터공학 전공 수업의 꽃, 졸업 프로젝트를 수강하게 됐네요..ㅎ.ㅎ 아무것도 모르면서 대체 무슨 생각으로 인공지능 관련 주제를 선택했는지 모르겠습니다. (과거의 저한테 딱콩 한 대 때려주고 싶네요)


아무튼 오늘은 AI 노베이스 프로 독학러의 딥러닝 프로젝트 진행 과정을 보여드리겠습니다. 아직 프로젝트 진행 초기 단계라 대단한 과정을 담지는 못하였기에 일부 기능의 기술 검증을 진행한 toy project 수준으로 봐주시면 감사하겠습니다.



우선 제가 구현하고자 하는 서비스에 대해 간략히 소개해드리자면,

저희 팀의 프로젝트 주제는 [편안한 글쓰기를 도와주는 글쓰기 피드백 서비스]입니다. 영어권 서비스인 'Grammarly'와 유사한 서비스를 제공한다고 봐주시면 됩니다. 대부분의 사람들은 어떤 이유든 글을 쓰며 살아갑니다. 교수님이나 회사 상사에게 메일을 쓰기도 하고, 학교 과제로 에세이를 쓰기도 하고, 이렇게 개인 블로그를 작성하기도 하면서요. 이때 겪었던 여러 어려움들을 줄이고, 보다 편안한 글쓰기 환경을 제공하고자 이러한 서비스를 구상했습니다.

 

서비스가 제공하는 여러 기능이 있지만 오늘은 그중 일상 언어를 사용한 비격식체 문장을 예의를 갖춘 공적인 격식체 문장으로 변환해주는 서비스를 구현한 과정에 대해 글을 작성해보겠습니다.

 


먼저 딥러닝 모델은 크게 세 가지로 분류할 수 있습니다. 아래 표에 간략하게 설명해놓았으니 참고 부탁드립니다.

이름 모델 설명 주로 하는 일 사용 예시 예시 모델
인코더 읽어서 해석하는 모델 무언가를 해석하고
문장을 읽어서 분류함
분류: 입력에 대한 예측
태깅: 개발 단어별 예측
BERT, Electra,
RoBERTa
디코더 해석된걸 가져와서
생성하는 모델
새로운 문장을 생성함 10글자를 쓰면,
그 뒤 10글자를 써줌
GPT
인코더 디코더 읽고 해석한 후
다시 생성하는 모델
문장 A를 문장 B로
바꿔줌
Sequence to Sequence
번역, 요약, 질문생성
BART, T5

 

저희는 입력한 비격식체 문장을 격식체 문장으로 paraphrase 하여 새로운 문장으로 바꾸기 위해서 문장 A를 입력하면 문장 B를 출력하는 인코더-디코더 모델을 사용했습니다. 다양한 인코더-디코더 모델이 존재하지만 저희는 완성도 높은 한국어 기반 BART 모델인 koBART를 사용하기로 결정했습니다.


앞서 말했듯이 저는 딥러닝의 'ㄷ'자도 모르는 딥린이..로서 머신을 돌리기 위해서는 일단 딥러닝 모델의 작동 원리를 이해하고 딥러닝 관련 기초 지식을 습득해야 했습니다. 하지만 이번 학기는 대학교 3학년을 왜 사망년..이라고 부르는지 몸소 깨닫게 해 준 학기이기에 이리저리 치이며 약 2주 동안 속성으로 공부할 수밖에 없었습니다. ㅠ,ㅠ

 

저는 팀원들과 스터디를 진행하며 멘토님께서 추천해주신 wikidocs 자료를 통해 필요한 챕터만 따로 뽑아 독학했습니다. 꼭 필요한 내용을 간결하고 쉽게 정리해놓은 사이트라 큰 도움이 됐습니다. 저 같은 딥러닝 초보 분들께 추천드립니다.

 

그래도 기본적인 내용을 공부하고 나니 이후에는 실질적인 자료 참고해서 부딪혀가며 개발할 수준은 되었던 것 같습니다.


▶데이터셋 확보하기


그런데 혹시 딥러닝은 데이터셋 확보가 전부라는 말... 들어보셨나요? 저는 이번 프로젝트를 하며 몸소 느꼈습니다. 저희 프로젝트 목표를 달성하기 위해서는, 즉 높은 정확도를 필요로 하는 머신을 정상적으로 가동하기 위해서는 수만 개의 양질의 데이터셋이 필요합니다. 게다가 저희의 프로젝트에 쓰일 비격식-격식 말뭉치 쌍은 연구된 바가 적어서 다른 사람의 데이터셋을 구해오는 것만으로는 프로젝트 진행에 어려움이 있을 것이라고 판단했습니다.

 

그래서 저희는 koBART 모델이 정상적으로 작동하는지 알아보고자 자체적으로 데이터셋 생성에 돌입했습니다.(비극의 서막..입니다) 하지만 저희에게는 시간이 부족하였기 때문에 욕심 내지 않고 뉴스 기사나 개인 메세지함을 뒤져가며 각자 양질의 비격식-격식 말뭉치 데이터셋 1,000쌍 (2,000 문장)을 작성하였습니다. 이때 양질의 데이터셋이란, 한국어의 표준 어투를 지키고, 격식 문장과 비격식 문장이 서로 동일한 의미를 가지면서, 상황에 맞는 적절한 어휘가 사용된 문장들의 모음을 의미합니다.

 

아무튼 총 6,000 문장 정도의 데이터셋을 모은 후 머신을 돌려보기 시작했습니다. (이후에 다시 언급하겠지만 문장 6,000개는 턱도 없이 부족한 양이었습니다..^.ㅠ)

보이시나요 6,008개의 문장들이...


아 혹시.. 이 데이터라도 필요하신 분들이 계실까하여 비루한 Huggingface 링크 첨부합니다.

csv 파일들 다운 받아서 사용하시면 됩니다


▶koBART 작동시키기

 

일단 저희는 아직 간단하게 머신을 돌리는 수준이라 별도의 프로그램 설치 없이 google의 colab에서 작업을 진행하였습니다. 작성한 데이터셋은 test, train, validation 용으로 1:8:1 비율로 나누고 csv 파일 형태로 변환하여 Huggingface에 업로드하여 사용하였습니다.

 

머신을 실행시키는 과정은 코드를 보면서 설명하겠습니다. (실행을 위한 완벽한 코드 전체를 첨부한 것이 아닙니다. 이 점 유의해주세요!)

 

1. 우선 Huggingface에 업로드한 데이터셋 다운로드를 위해 필요한 것들을 설치하고 데이터셋을 불러와 줍니다.

!pip install datasets
!pip install transformers

# Huggingface page내의 'Use in dataset library' 버튼을 누르면 쉽게 코드를 복붙할 수 있음
# 직접 본인 dataset name을 "" 안에 넣어도 됨
from datasets import load_dataset
dataset = load_dataset("gayom/styletransfer-dataset")

2. print 함수로 다운로드된 데이터셋 구조를 확인해보면 다음과 같습니다.

print(dataset)

3. 데이터를 토큰화하여 저장하기 위해 먼저 kobart-base-v2tokenizer를 다운받습니다.

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("gogamza/kobart-base-v2")

4. 비격식체의 문장(informal)들을 input 값으로, 격식체의 문장(formal)들을 정답 값으로 하여 모델을 학습시키겠습니다.

input 데이터 값을 데이터셋의 informal 열로, label 데이터 값을 데이터셋의 formal 열로 지정한 후 전체 데이터셋을 토큰화하여 저장합니다.

max_input_length = 512
max_target_length = 512

def preprocess_function(examples):
    modi_informal = ['<s>' + sequence + '</s>' for sequence in examples["informal"]]
    model_inputs = tokenizer(modi_informal, max_length=max_input_length, truncation=True)

    # Setup the tokenizer for targets
    with tokenizer.as_target_tokenizer():   # kobart의 경우, 동일한 tokenizer를 사용합니다.
        modi_formal = ['<s>' + sequence + '</s>' for sequence in examples["formal"]]
        labels = tokenizer(modi_formal, max_length=max_target_length, truncation=True)

    model_inputs["labels"] = labels["input_ids"]
    return model_inputs
       
preprocessed_dataset_sample = preprocess_function(dataset['train'][0:1])

for keys in preprocessed_dataset_sample.keys():
    print(keys, ':', preprocessed_dataset_sample[keys])

5. tokenizer가 잘 작동하는지 확인 후 train 데이터를 토큰화하여 저장합니다. 

tokenized_datasets = dataset.map(preprocess_function, batched=True)

for keys in tokenized_datasets['train'][0].keys():
    print(keys, ':', tokenized_datasets['train'][0][keys])

6. 이제 koBART 모델을 불러와 데이터를 학습시켜줍니다.

# koBART 모델 불러오기
from transformers import BartForConditionalGeneration
model = BartForConditionalGeneration.from_pretrained('gogamza/kobart-base-v2')

from transformers import DataCollatorForSeq2Seq, Seq2SeqTrainer, Seq2SeqTrainingArguments

batch_size = 16
args = Seq2SeqTrainingArguments(
    output_dir = "model_output",
    evaluation_strategy = "steps",
    learning_rate = 2e-5,
    per_device_train_batch_size = batch_size,
    per_device_eval_batch_size = batch_size,
    save_total_limit = 2,
    num_train_epochs = 5,
    predict_with_generate = True,
    save_steps = 1000,
    eval_steps = 500
)

data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)

7. 이때 evaluation을 위한 metric을 넣어줍니다. 이후 모델을 학습 시키고 저장합니다.

(다만, 이후 출력된 rouge 값을 확인해주면 머신의 상태가 좋지 않음을 직감할 수 있습니다. (^.ㅠ))

!pip install rouge_score

from datasets import load_metric
metric = load_metric("rouge")

import numpy as np

def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)
    # Replace -100 in the labels as we can't decode them.
    labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
    decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
    
    result = metric.compute(predictions=decoded_preds, references=decoded_labels, use_stemmer=True)
    # Extract a few results
    result = {key: value.mid.fmeasure * 100 for key, value in result.items()}
    
    # Add mean generated length
    prediction_lens = [np.count_nonzero(pred != tokenizer.pad_token_id) for pred in predictions]
    result["gen_len"] = np.mean(prediction_lens)
    
    return {k: round(v, 4) for k, v in result.items()}

trainer = Seq2SeqTrainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["test"],
    data_collator=data_collator,
    tokenizer=tokenizer,
    compute_metrics=compute_metrics
)

trainer.train()

trainer.save_model()

8. 마지막으로 학습된 모델로 inference를 확인해줍니다. (두근두근) 아쉽게도 여러 차례 test 해본 결과 정확도는 처참합니다.. 하하

import torch

def paraphrasing(text):
    tokenized_text = tokenizer(text, return_tensors='pt', truncation=True).to('cuda')
    with torch.no_grad():
        outputs = model.generate(
            tokenized_text['input_ids'],
            do_sample = True,
            eos_token_id = tokenizer.eos_token_id,
            max_length = 512,
            top_p = 0.7,
            top_k = 20,
            # num_beams=10,
            num_return_sequences = 1,
            no_repeat_ngram_size = 2,
            early_stopping = True
        )
        return tokenizer.decode(outputs[0], skip_special_tokens = True)
        
    paraphrasing("<s>문장</s>")

되다를 have가 아닌 be로 해석
성공!
'할 말'을 '드릴 말씀'으로 바꿔줬으면 더 좋았을텐데...
'살'을 '구매하실' 아니 그냥 '사실' 이라도 좋으니.. 바꿔줬으면 좋았을텐데...2


이로써 일단 저희의 koBART를 사용한 문장 형식 변환 프로젝트를 위한 간단한 toy project에 대해 튜토리얼을 작성해보았는데요, 어떠셨나요? 보여드리기 민망한 수준의 처참한 결과가 나온 탓에 속상하기도 하고 앞으로가 막막하기도 합니다.

 

하지만 여기서 무너질 수는 없죠! 결과를 분석해본 결과 낮은 정확도의 원인은 부족한 데이터셋의 으로 보입니다. 그렇기에 저희는 비슷한 연구에 사용된 30,000여 개의 문장을 프로젝트에 알맞게 수정하여 추가적인 학습 데이터로 사용할 예정입니다. 모델을 일부 수정하는 방법도 물색 중에 있습니다.

 

사실 이 글을 쓰면서도 사전과 맞춤법 검사기를 몇 번이나 들여다 보고, 얼마나 많은 문장을 썼다 지웠는지 모르겠습니다. 좋은 글을 쓰기란 정말 어려운 것 같네요. 꼭 좋은 서비스를 구현해내서 많은 사람들이 편하게 자신의 생각을 전달할 수 있도록 도움이 되고 싶습니다.

 

앞으로 비격식-격식 문장 전환 기능 이외에도 다양한 기능들을 구현하고 정리하여 올릴 예정이니 많은 관심 부탁드립니다 :D 이외에도 수정 사항이 생기면 때때로 업데이트하도록 하겠습니다. 감사합니다.