본문 바로가기

PROJECT

[에어] 'KcELECTRA' 로 악성댓글 분류 모델 만들기 (파이썬/Colab)

에어 프로젝트
#7 KcELECTRA 로 악성댓글 분류 모델 만들기

 

인터넷에 게시된 댓글을 읽다보면 우리는 수많은 악플을 마주하게 된다. 따라서 많은 기업들은 이를 자동으로 처리하기 위한 여러가지 방법들을 도입해 최대한 욕설이나 혐오표현들을 포함한 악성댓글이 게시되지 않도록 하고 있다. 예를 들어, 필터링을 거쳐 다른 언어로 바꾸거나, 욕설에 대해 ****와 같은 마스킹 처리를 하는 등 다양한 노력을 기울이고 있다.

 

하지만, 비속어를 사용하지 않고도 특정 성별, 인종을 비하하는 혐오표현을 사용하거나 강한 비난표현을 통해 상대를 모욕하는 등 방법은 다양하다. 때로는 욕설이 아님에도 필터링에 의해 욕설로 인식하는 문제가 발생하기도 한다. 그렇다면 어떤 방식으로 효율적이고 정확하게 악성댓글을 탐지할 수 있을까. 바로 욕설뿐만 아니라 혐오표현, 인신공격표현 등을 포함한 악성댓글을 라벨링한 데이터셋을 통해 학습된 딥러닝 모델을 활용하는 것이다.

 

지난 에어 프로젝트에서는 KoBERT 모델을 이용하여 짧은 대화 텍스트가 어떠한 감정(공포/놀람/분노/슬픔/중립/행복/혐오)에 속하는지 예측하는 다중분류 모델을 만들어보았다. 이번 에어 프로젝트에서는 KcELECTRA 모델을 파인튜닝하여 악성댓글을 분류할 수 있는 딥러닝 모델을 만들어 보고자 한다.

 

 

|| KcELECTRA 란?

모델 학습을 시작하기 전에 파인튜닝에 사용될 KcELECTRA 에 대해 간단하게 알아보자. KcELECTRA 는 이전 에어프로젝트에서 사용한 KoBERT 와 마찬가지로 한국어 데이터를 활용해 학습시킨 모델로 다중언어로 학습된 모델보다 한국어 처리에 대해 훨씬 뛰어난 성능을 보인다. 하지만 한국어 데이터셋으로 동일하게 학습된 두 모델에도 이름에서 알 수 있듯 두 가지의 차이점이 존재한다.

 

첫째, 각 모델이 학습한 데이터가 다르다.

모델 이름의 첫 부분에서 알 수 있듯 KcELECTRA 의 학습에 사용된 데이터는 한국어 댓글(Korean Comments) 데이터로 KoBERT가 학습한 데이터인 뉴스기사, 백과사전과 같이 잘 정제되어있는 데이터와는 확연한 차이가 있다. 때문에 KcELECTRA는 구어체의 댓글과 같이 잘 정제되어있지 않고 신조어와 오탈자가 많은 데이터 셋을 학습하는데 더욱 적합하다.

 

둘째, 학습의 효율성이 다르다.

두 모델 간의 가장 큰 차이는 바로 학습 효율성이 다르다는 것이다. BERT 의 특징 중 하나는 MLM(Masked Language Model)로 문장의 빈칸을 채우는 방식으로 학습되었다는 것이다. 조금 더 구체적으로 얘기하면 입력 문장에서 15%의 단어를 가리고(masking) 가려진 단어를 맞추는 방법으로 학습한다. 하지만 이러한 BERT의 학습방법에는 문제점이 있다. 바로 데이터의 15%만 학습을 진행하기 때문에 원하는 성능이 나오기 위해서는 많은 데이터가 필요하고 그에 따라 필요한 학습량과 컴퓨팅 자원도 크게 증가하게 된다는 것이다. 하지만 15%의 학습 데이터 비율을 100%까지 끌어올릴 수 있다면 적은 학습량과 컴퓨팅 자원으로도 더욱 효율적으로 학습할 수 있지 않을까. ELECTRA(Efficiently Learning an Encoder that Classifies Token Replacements Accurately) 는 바로 이러한 문제에 대한 해결책을 제시한 모델로 RTD(Replaced Token Detection)라는 학습 방법을 사용했다. RTD는 GAN(Generative Adversarial Network)에서 사용된 Generator, Discriminator 두 가지 모델을 활용해 학습하는 방식이다.

출처 : ELECTRA: Pre-training Text Encoders as Discriminators Rather Than Generators

RTD 학습 방식을 좀 더 쉽게 설명하면, 가장 먼저 BERT 에서와 같이 입력된 토큰을 [MASK] 토큰으로 교체하여 완전히 마스킹하는 것이 아니라 Generator를 통해 다소 그럴 듯한 가짜 토큰으로 대체하는 방식으로 입력된 문장을 변형시킨다. 예를 들어, 위의 그림에서 "cooked"라는 토큰은 "[MASK]" 토큰이 아닌 "ate"라는 토큰으로 대체되었는데 이는 Generator에 의해 대체된 것이다. 이후에는 입력된 문장의 모든 토큰에 대해 실제 토큰인지 아니면 Generator를 통해 생성된 가짜 토큰인지 맞히는 이진분류 방식으로 Discriminator의 학습이 진행된다. 그리고 이러한 이진분류는 일부 토큰이 아닌 모든 토큰에 대해 진행되므로 기존의 MLM 방식보다 훨씬 효율적이면서도 효과적인 방식으로 모델을 학습할 수 있게 된다. 결과적으로 ELECTRA 는 RTD 라는 새로운 학습방법을 도입해 적은 계산량으로도 이전의 다른 모델과 비슷한 수준의 성능을 가지게 될 수 있었다.

KcELECTRA에 대해 더 궁금하거나 KcELECTRA를 이용한 실습 코드가 궁금하다면 아래 프로젝트 글을 참조하길 바란다.

 

GitHub - Beomi/KcELECTRA: 🤗 Korean Comments ELECTRA: 한국어 댓글로 학습한 ELECTRA 모델

🤗 Korean Comments ELECTRA: 한국어 댓글로 학습한 ELECTRA 모델. Contribute to Beomi/KcELECTRA development by creating an account on GitHub.

github.com

 

 

|| KcELECTRA를 이용하여 악성댓글 분류모델 만들기

이제, KcELECTRA 를 활용한 악성댓글 분류모델을 만드려고 하는데, 학습 데이터는 ZIZUN님의 korean-malicious-comments-dataset 을 사용하려고 한다. 해당 데이터는 한국어 악성댓글 10,000개를 수집한 데이터로 욕설표현, 혐오표현 등을 포함한 악성댓글은 0으로, 그렇지 않은 경우는 1로 레이블링이 수행되어 있다. 아래 이미지는 데이터의 일부분을 캡처한 것이다.

 

코드는 Colab Pro에서 작성하였고, HuggingFace의 transformers 라이브러리를 활용해 보다 간편하게 학습을 진행했다.

 

 

 

1. Colab 환경 설정

먼저 아래 코드를 실행하여 필요한 라이브러리 및 데이터셋을 불러오고, Colab 환경을 설정한다.

!pip install transformers
!git clone https://github.com/ZIZUN/korean-malicious-comments-dataset.git
import pandas as pd
import matplotlib.pyplot as plt

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer
from sklearn.metrics import precision_recall_fscore_support, accuracy_score
# GPU 설정
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print("device:", device)

GPU 를 사용한다면 "device: cuda:0" 라고 출력될 것이다.

 

 

2. 데이터셋 제작

(1) 원본 데이터 불러오기

환경설정이 끝났다면 학습할 악성댓글 데이터셋을 pandas 를 활용하여 불러온다.

df = pd.read_csv("/content/korean-malicious-comments-dataset/Dataset.csv", sep="\t")
df.head()

댓글과 레이블은 탭(\t)으로 구분되어 있으므로 sep 파라미터로 "\t" 를 사용해 데이터를 불러온다.

 

 

(2) 데이터 전처리

데이터를 정확하게 로드했는지 확인해보자.

df.info()

댓글 텍스트를 나타내는 content 는 10,000개의 데이터가 정확하게 로드되었지만, 악성댓글 여부를 나타내는 lable은 25개의 null 데이터가 존재하는 것을 확인할 수 있다.

 

null_idx = df[df.lable.isnull()].index
df.loc[null_idx, "content"]

데이터를 불러올때 "\t" 를 sep 파라미터로 설정했지만 "\t" 앞에 특수문자가 있는 경우 데이터가 정확하게 분리되지 않는 경우가 발생한 것 같다. 해당 데이터를 정확하게 분리하기 위해 추가적인 전처리 작업을 실행한다.

# lable 은 content의 가장 끝 문자열로 설정
df.loc[null_idx, "lable"] = df.loc[null_idx, "content"].apply(lambda x: x[-1])

# content는 "\t" 앞부분까지의 문자열로 설정
df.loc[null_idx, "content"] = df.loc[null_idx, "content"].apply(lambda x: x[:-2])

 

추가적으로, 학습을 위해 lable의 데이터 타입을 float 가 아닌 int 로 변환한다.

df = df.astype({"lable":"int"})
df.info()

이제 결측치가 없고 lable은 정수형인 원하는 데이터가 되었다.

 

(3) Train set / Test set으로 나누기

데이터셋이 준비가 되었다면 이제 모델을 학습할 Train data와 모델을 평가할 Test data로 나누어야 한다. 데이터프레임의 sample 메서드를 활용해 간단하게 train_set, test_set을 4:1의 비율로 분리한다.

train_data = df.sample(frac=0.8, random_state=42)
test_data = df.drop(train_data.index)

 

추가적으로 각 데이터셋에 존재하는 중복된 데이터를 제거한다.

# 데이터셋 갯수 확인
print('중복 제거 전 학습 데이터셋 : {}'.format(len(train_data)))
print('중복 제거 전 테스트 데이터셋 : {}'.format(len(test_data)))

# 중복 데이터 제거
train_data.drop_duplicates(subset=["content"], inplace= True)
test_data.drop_duplicates(subset=["content"], inplace= True)

# 데이터셋 갯수 확인
print('중복 제거 후 학습 데이터셋 : {}'.format(len(train_data)))
print('중복 제거 후 테스트 데이터셋 : {}'.format(len(test_data)))

중복 제거 후 학습 데이터셋의 개수가 줄어들었다.

 

 

(4) 토크나이징

다음으로 텍스트 형태의 데이터를 모델이 학습할 수 있는 토큰 형태로 분리하고 이를 토큰 id 값으로 변환한다.

이는 transformers 라이브러리의 AutoTokenizer 모듈을 활용해 손쉽게 진행할 수 있다.

 

huggingface의 model 로 등록된 모델의 경우 모델뿐만 아니라 학습에 사용된 tokenizer까지 업로드되어 있다. AutoTokenizer는 이를 활용하는 모듈로 사용하고자 하는 모델명만 입력하면 자동으로 해당 토크나이저를 불러올 수 있다.

MODEL_NAME = "beomi/KcELECTRA-base"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)

 

이제 불러온 토크나이저를 활용하여 데이터를 토크나이징한다.

tokenized_train_sentences = tokenizer(
    list(train_data["content"]),
    return_tensors="pt",                # pytorch의 tensor 형태로 return
    max_length=128,                     # 최대 토큰길이 설정
    padding=True,                       # 제로패딩 설정
    truncation=True,                    # max_length 초과 토큰 truncate
    add_special_tokens=True,            # special token 추가
    )

tokenizer에 데이터를 입력하면 자동으로 토크나이징하는 과정을 수행하는데 return_tensor, max_length 등 다양한 파라미터를 설정할 수 있다.

 

print(tokenized_train_sentences[0])
print(tokenized_train_sentences[0].tokens)
print(tokenized_train_sentences[0].ids)
print(tokenized_train_sentences[0].attention_mask)

반환된 데이터를 보면 ids, type_ids, tokens, offsets, attention_mask, special_tokens_mask, overflowing 값들을 포함하고 있는 것을 확인할 수 있으며, tokens 값에는 [CLS], [SEP],[PAD] 과 같은 special_token을 포함하여 텍스트가 분리된 것을 확인할 수 있다. 또한 ids 값을 통해 각 token의 id를 확인할 수 있으며, attention_mask 값을 통해 패딩된 값을 구분할 수 있다.

 

이제 학습 데이터셋뿐만 아니라 테스트 데이터셋에 대해서도 동일하게 토크나이징 과정을 수행한다.

tokenized_test_sentences = tokenizer(
    list(test_data["content"]),
    return_tensors="pt",
    max_length=128,
    padding=True,
    truncation=True,
    add_special_tokens=True,
    )

 

(5) 데이터셋  생성

마지막으로 PyTorch 프레임워크에서 모델이 학습할 수 있는 형태의 데이터셋을 생성한다.

먼저, 학습을 위한 데이터셋 클래스를 만든다.

class CurseDataset(torch.utils.data.Dataset):
    def __init__(self, encodings, labels):
        self.encodings = encodings
        self.labels = labels

    def __getitem__(self, idx):
        item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
        item["labels"] = torch.tensor(self.labels[idx])
        return item

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

 

이후 train_set, test_set 에 대한 데이터셋을 각각 생성한다.

train_label = train_data["lable"].values
test_label = test_data["lable"].values

train_dataset = CurseDataset(tokenized_train_sentences, train_label)
test_dataset = CurseDataset(tokenized_test_sentences, test_label)

 

 

3. 모델 학습

이제 학습을 위한 데이터셋이 준비되었으니 사전학습된 모델을 불러와 파인튜닝만 진행하면 된다.

 

(1) 모델 불러오기

먼저, 사전학습된 KcELECTRA 모델을 불러온다. 이 프로젝트에서는 분류 task를 위한 모델을 사용하므로 AutoModelForSequenceClassification 을 통해 손쉽게 사전학습모델을 불러올 수 있다.

model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=2)
model.to(device)

 

 

 

(2) 학습 파라미터 설정

transformers 라이브러리는 손쉬운 학습을 위한 TrainingArguments, Trainer 라는 모듈을 지원한다.

먼저 TrainingArguments 를 통해 학습에 사용되는 파라미터들을 설정한다.

training_args = TrainingArguments(
    output_dir='./',                    # 학습결과 저장경로
    num_train_epochs=10,                # 학습 epoch 설정
    per_device_train_batch_size=8,      # train batch_size 설정
    per_device_eval_batch_size=64,      # test batch_size 설정
    logging_dir='./logs',               # 학습log 저장경로
    logging_steps=500,                  # 학습log 기록 단위
    save_total_limit=2,                 # 학습결과 저장 최대갯수 
)

이외에도 다양한 파라미터들을 설정할 수 있으며 아래 공식 documentation에서 확인할 수 있다.

https://huggingface.co/docs/transformers/master/main_classes/trainer#transformers.TrainingArguments

 

Trainer

When using gradient accumulation, one step is counted as one step with backward pass. Therefore, logging, evaluation, save will be conducted every gradient_accumulation_steps * xxx_step training examples.

huggingface.co

 

다음으로는 학습과정에서 사용할 평가지표를 위한 함수를 설정한다.

def compute_metrics(pred):
    labels = pred.label_ids
    preds = pred.predictions.argmax(-1)
    precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average='binary')
    acc = accuracy_score(labels, preds)
    return {
        'accuracy': acc,
        'f1': f1,
        'precision': precision,
        'recall': recall
    }

정확도(accuracy), 정밀도(precision), 재현율(recall), F1스코어로 모델의 성능을 평가할 수 있다.

 

 

마지막으로, Trainer 모듈을 사용해 모델의 학습을 컨트롤하는 trainer를 생성한다.

trainer = Trainer(
    model=model,                         # 학습하고자하는 🤗 Transformers model
    args=training_args,                  # 위에서 정의한 Training Arguments
    train_dataset=train_dataset,         # 학습 데이터셋
    eval_dataset=test_dataset,           # 평가 데이터셋
    compute_metrics=compute_metrics,     # 평가지표
)

 

(3) 학습

이제 학습을 위한 모든 준비과정을 완료했으니 학습을 진행해보자.

trainer를 활용하면 단 한줄의 코드로 학습을 시작할 수 있다.

trainer.train()

TrainingArguments에 입력한 조건에 맞춰 손쉽게 모델을 학습하고 학습결과를 저장하는 것을 확인할 수 있다.

 

 

4. 모델 평가

테스트셋에 대한 성능평가 역시 단 한줄로 실행할 수 있다.

trainer.evaluate(eval_dataset=test_dataset)

test set에 대해서는 약 92%의 정확도를 보이는 것을 확인할 수 있다.

 

 

이미 test set으로 성능을 측정해보았지만, 새로운 문장을 입력하여 분류를 잘 하는지 살펴보기 위해 예측함수를 만든다.

# 0: curse, 1: non_curse
def sentence_predict(sent):
    # 평가모드로 변경
    model.eval()

    # 입력된 문장 토크나이징
    tokenized_sent = tokenizer(
        sent,
        return_tensors="pt",
        truncation=True,
        add_special_tokens=True,
        max_length=128
    )
    
    # 모델이 위치한 GPU로 이동 
    tokenized_sent.to(device)

    # 예측
    with torch.no_grad():
        outputs = model(
            input_ids=tokenized_sent["input_ids"],
            attention_mask=tokenized_sent["attention_mask"],
            token_type_ids=tokenized_sent["token_type_ids"]
            )

    # 결과 return
    logits = outputs[0]
    logits = logits.detach().cpu()
    result = logits.argmax(-1)
    if result == 0:
        result = " >> 악성댓글 👿"
    elif result == 1:
        result = " >> 정상댓글 😀"
    return result
#0 입력시 종료
while True:
    sentence = input("댓글을 입력해주세요: ")
    if sentence == "0":
        break
    print(sentence_predict(sentence))
    print("\n")

이제 위 코드를 실행하고, 댓글을 입력하면 된다.

여러가지 욕설 혹은 혐오표현을 포함한 댓글을 입력하였더니 아래와 같은 결과가 출력되었다.

 

 

|| 학습 결과 및 보완점

학습에 사용되지 않은 새로운 데이터에 대해서도 90%가 넘는 정확도를 보여주고, 임의로 입력한 댓글에 대해서도 잘 분류하는 것을 확인할 수 있었다. 하지만 댓글 분류를 실제 서비스에 적용하기 위해서는 더 높은 정확도를 요구한다. 높은 수준의 정확도를 보이지 않는다면 실제 악성댓글이 아닌데도 필터링되거나, 반대로 악성댓글이 필터링되지 않고 그대로 보여지는 경우가 자주 발생하기 때문이다. 따라서, 더욱 다양한 데이터셋을 통해 모델을 학습하거나 모델을 구조를 바꿔 더 높은 성능의 분류모델을 만들 수 있도록 해야한다.

 

또한 현재 모델은 악성댓글로 분류된 경우 댓글 전체를 마스킹하는 방식으로 적용할 수 있지만, attention 구조를 활용한다면 악성댓글로 분류된 댓글에서 부정적인 단어만을 마스킹하는 방식으로 출력되도록 구현하여 더욱 쓸모있는 악성댓글 분류모델을 만들 수 있을 것이다.

 

 

|| Reference