KoBERT 모델을 사용해 뉴스 댓글을 분류하는 작은 프로젝트를 진행해보았다.
*프로젝트는 코랩 환경에서 진행했다
1. 주제 선정 이유🙋♂️
프로젝트의 주제는 KoBERT를 사용한 온라인 뉴스 악성 댓글 데이터 이진 분류이다. 주제 선정 이유는 다음과 같다.
- 유튜브, 개인방송, SNS, 게임 채팅 등 온라인 공간 확장
- 특정 집단뿐만 아니라 일반인을 대상으로 한 악성 댓글 증가
- 피해자의 경우 외상 후 스트레스 장애 및 대인관계 기피, 공포로의 발전 가능성 존재
큰 사회적 문제인 악성 댓글 문제를 기업 차원에서 어떻게 대응할 수 있을지에 대해 고민한 결과 딥러닝을 사용해 이를 분류해보기로 했다.
2. 데이터 선정🙋♂️
나는 데이터셋 중에서도 Gender-related bias를 가진 데이터를 사용했다. 이 데이터는 성별에 따른 역할이나 능력에 대한 편견, 성별과 나이에 대한 편견, 그 외 특정 성별, 성적 지향성, 성 정체성, 성 관련 사상을 가진 집단에 대한 편견이 있는 경우 TRUE로 라벨링 된 데이터이다. 데이터는 train/dev/test set으로 나누어져 있었고, 학습에는 train set과 dev set을 사용했다.
3. 데이터 살펴보기 및 전처리🙋♂️
3-1) 데이터 살펴보기
- 데이터 형태 및 중복값 유무 확인
# 데이터 가져오기
dataset_train = pd.read_csv('/content/drive/MyDrive/project4/gender_bias_data/train.gender_bias.binary.csv')
dataset_dev = pd.read_csv('/content/drive/MyDrive/project4/gender_bias_data/dev.gender_bias.binary.csv')
# 개수 확인
print('train dataset 갯수:', len(dataset_train)) # 7896개
print('dev dataset 갯수:', len(dataset_dev)) # 471개
# 형태 확인
dataset_train.loc[dataset_train['label'] == False].head(20)
dataset_train.loc[dataset_train['label'] == True].head(20)
# 중복값 확인
assert len(dataset_train) == int(dataset_train['comments'].duplicated().value_counts())
assert len(dataset_dev) == int(dataset_dev['comments'].duplicated().value_counts())
# 결과) 중복값 없음
- 타겟 비율 확인
plt.subplot(1,2,1)
ax = dataset_train['label'].value_counts(normalize=True).plot.bar()
plt.title('Train')
plt.xlabel('lable')
plt.subplot(1,2,2)
ax = dataset_dev['label'].value_counts(normalize=True).plot.bar()
plt.title('Dev')
plt.xlabel('lable')
plt.show()
- 길이 분포 확인
comments_len = [len(s.split()) for s in dataset_train['comments']]
print('댓글의 최소 길이 : {}'.format(np.min(comments_len)))
print('댓글의 최대 길이 : {}'.format(np.max(comments_len)))
print('댓글의 평균 길이 : {}'.format(np.mean(comments_len)))
plt.subplot(1,2,1)
plt.boxplot(comments_len)
plt.title('Comments')
plt.subplot(1,2,2)
plt.title('Comments')
plt.hist(comments_len, bins=40)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()
- 형태소분석기 Mecab과 word cloud를 활용한 시각화
import konlpy
from konlpy.tag import Mecab
# 데이터 합치기
dataset_concat = pd.concat([dataset_train, dataset_dev], names=['comments', 'label'])
# 데이터에서 명사만 추출
def getNouns(s):
if type(s) == str:
mecab = Mecab()
nouns_list = mecab.nouns(s)
nouns = ' '.join(nouns_list)
return nouns
else:
return str(s)
dataset_concat['nouns'] = dataset_concat['comments'].apply(getNouns)
# TRUE와 FALSE 데이터로 분리
true_data = dataset_concat[dataset_concat['label'] == True]
false_data = dataset_concat[dataset_concat['label'] == False]
from wordcloud import WordCloud
from PIL import Image
bubble_mask = np.array(Image.open('/content/drive/MyDrive/project4/speech-bubble.PNG'))
wordcloud = WordCloud(font_path='NanumBarunGothic',
mask=bubble_mask,
background_color='white')
# true label에 대한 word cloud 생성
wc_true = wordcloud.generate(' '.join(true_data['nouns']))
plt.figure(figsize=(10, 10))
plt.imshow(wc_true, cmap=plt.cm.gray, interpolation='bilinear')
plt.axis("off")
plt.show()
시각화 결과 성별에 관한 데이터이다 보니 '여자', '남자'라는 단어가 가장 크게 나타나고, 사이사이 차별·혐오와 관련된 단어들을 찾을 수 있었다.
👇더 자세한 word cloud 사용법은 아래에서 확인
3-2) 데이터 전처리
전처리는 간단하게 정규표현식을 사용해 특수문자를 제거하고, 라벨 값을 0과 1로 변경해주었다.
# 댓글 내 특수문자 제거
import re
def textCleaning(s):
res = re.sub(r'[^ A-Za-z0-9가-힣]','',s)
return res
dataset_train['comments'] = dataset_train['comments'].apply(textCleaning)
dataset_dev['comments'] = dataset_dev['comments'].apply(textCleaning)
# label 값 0또는 1로 변경
dataset_dev["label"] = dataset_dev["label"].astype(int)
dataset_train["label"] = dataset_train["label"].astype(int)
4. 딥러닝 방식 적용🙋♂️
* 사전 학습 모델을 기반으로 목적에 따라 학습된 weight나 bias를 조정하는 과정을 파인 튜닝(fine tuning)이라고 한다
* 사전 학습 모델로 파인튜닝을 하는 과정을 전이 학습(transfer learning)이라고 한다.
!pip install git+https://git@github.com/SKTBrain/KoBERT.git@master
import torch
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import gluonnlp as nlp
import numpy as np
from tqdm.notebook import tqdm
from kobert import get_tokenizer
from kobert import get_pytorch_kobert_model
from transformers import AdamW
from transformers.optimization import get_cosine_schedule_with_warmup
# GPU 설정
if torch.cuda.is_available():
device = torch.device("cuda:0")
print('There are %d GPU(s) available.' % torch.cuda.device_count())
print('We will use the GPU:', torch.cuda.get_device_name(0))
else:
device = torch.device('cpu')
print('No GPU available, using the CPU instead')
# KoBERT 및 Vocabulary 불러오기
bertmodel, vocab = get_pytorch_kobert_model(cachedir=".cache")
!wget -O .cache/ratings_train.txt http://skt-lsl-nlp-model.s3.amazonaws.com/KoBERT/datasets/nsmc/ratings_train.txt
!wget -O .cache/ratings_test.txt http://skt-lsl-nlp-model.s3.amazonaws.com/KoBERT/datasets/nsmc/ratings_test.txt
# 토크나이저
tokenizer = get_tokenizer()
tok = nlp.data.BERTSPTokenizer(tokenizer, vocab, lower=False)
koBERT에서는 *서브워드 토크 나이저(Subword Tokenizer)로 SentencePiece를 사용한다. 예시는 아래와 같다.
train_list[0][0]
# 출력) '현재 호텔주인 심정 아18 난 마른하늘에 날벼락맞고 호텔망하게생겼는데 누군 계속 추모받네'
from gluonnlp.data import SentencepieceTokenizer
sp = SentencepieceTokenizer(tok_path)
print(sp(train_list[0][0]))
# 출력) ['▁현재', '▁호텔', '주', '인', '▁심', '정', '▁아', '18', '▁난', '▁마', '른', '하늘', '에', '▁날', '벼', '락', '맞', '고', '▁호텔', '망', '하게', '생', '겼', '는데', '▁누', '군', '▁계속', '▁추', '모', '받', '네']
* 서브워드 분리 작업은 단어를 더 작은 단위인 서브 워드로 분리해 인코딩 및 임베딩 하는 전처리 작업이다. 그리고 이 작업을 하는 토크 나이저를 서브 워드 토크 나이저라고 한다.
KoBERT 입력 데이터로 만드는 함수는 다음과 같다. 토큰화, 정수 인코딩, 패딩 등이 이루어지는 함수이다.
class BERTDataset(Dataset):
def __init__(self, dataset, sent_idx, label_idx, bert_tokenizer, max_len,
pad, pair):
transform = nlp.data.BERTSentenceTransform(
bert_tokenizer, max_seq_length=max_len, pad=pad, pair=pair)
self.sentences = [transform([i[sent_idx]]) for i in dataset]
self.labels = [np.int32(i[label_idx]) for i in dataset]
def __getitem__(self, i):
return (self.sentences[i] + (self.labels[i], ))
def __len__(self):
return (len(self.labels))
## Setting parameters
max_len = 64
batch_size = 64
warmup_ratio = 0.1
num_epochs = 5
max_grad_norm = 1
log_interval = 200
learning_rate = 5e-5
데이터를 ['comments', 'label'] 형태로 변경해준 후 KoBERT 입력 데이터로 만들어주었다.
def makeDatalist(df):
data_list = []
for comment, label in zip(df['comments'], df['label']):
temp = []
temp.append(comment)
temp.append(label)
data_list.append(temp)
return data_list
train_list = makeDatalist(dataset_train)
dev_list = makeDatalist(dataset_dev)
data_train = BERTDataset(train_list, 0, 1, tok, max_len, True, False)
data_dev = BERTDataset(dev_list, 0, 1, tok, max_len, True, False)
# PyTorch Dataloader를 사용해 배치 데이터 생성
train_dataloader = torch.utils.data.DataLoader(data_train, batch_size=batch_size, num_workers=5)
dev_dataloader = torch.utils.data.DataLoader(data_dev, batch_size=batch_size, num_workers=5)
모델은 다음과 같다. 다중 분류 시 num_classes를 조절해주면 된다.
class BERTClassifier(nn.Module):
def __init__(self,
bert,
hidden_size = 768,
num_classes=2,
dr_rate=None,
params=None):
super(BERTClassifier, self).__init__()
self.bert = bert
self.dr_rate = dr_rate
self.classifier = nn.Linear(hidden_size , num_classes)
if dr_rate:
self.dropout = nn.Dropout(p=dr_rate)
def gen_attention_mask(self, token_ids, valid_length):
attention_mask = torch.zeros_like(token_ids)
for i, v in enumerate(valid_length):
attention_mask[i][:v] = 1
return attention_mask.float()
def forward(self, token_ids, valid_length, segment_ids):
attention_mask = self.gen_attention_mask(token_ids, valid_length)
_, pooler = self.bert(input_ids = token_ids, token_type_ids = segment_ids.long(), attention_mask = attention_mask.float().to(token_ids.device))
if self.dr_rate:
out = self.dropout(pooler)
return self.classifier(out)
odel = BERTClassifier(bertmodel, dr_rate=0.5).to(device)
다음은 optimizer와 learning rate scheduler에 대한 코드이다. optimizers는 Adam보다 일반화 성능이 높다고 평가되는 AdamW를 사용했다. scheduler는 get_cosine_schedule_with_warmup()를 사용했는데, 이는 0과 (optimizer에서 설정한) lr사이에서 코사인 함수를 이용하여 학습률을 스케줄링하는 방법이다.
# Prepare optimizer and schedule (linear warmup and decay)
no_decay = ['bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
{'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], 'weight_decay': 0.01},
{'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
]
optimizer = AdamW(optimizer_grouped_parameters, lr=learning_rate)
loss_fn = nn.CrossEntropyLoss()
t_total = len(train_dataloader) * num_epochs
warmup_step = int(t_total * warmup_ratio)
scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=warmup_step, num_training_steps=t_total)
다음은 accuracy를 구하기 위한 함수이다.
def calc_accuracy(X,Y):
max_vals, max_indices = torch.max(X, 1)
train_acc = (max_indices == Y).sum().data.cpu().numpy()/max_indices.size()[0]
return train_acc
🌈모델 학습 및 평가
for e in range(num_epochs):
train_acc = 0.0
test_acc = 0.0
model.train()
for batch_id, (token_ids, valid_length, segment_ids, label) in tqdm(enumerate(train_dataloader), total=len(train_dataloader)):
optimizer.zero_grad()
token_ids = token_ids.long().to(device)
segment_ids = segment_ids.long().to(device)
valid_length= valid_length
label = label.long().to(device)
out = model(token_ids, valid_length, segment_ids)
loss = loss_fn(out, label)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm) # gradient clipping
optimizer.step()
scheduler.step() # Update learning rate schedule
train_acc += calc_accuracy(out, label)
if batch_id % log_interval == 0:
print("epoch {} batch id {} loss {} train acc {}".format(e+1, batch_id+1, loss.data.cpu().numpy(), train_acc / (batch_id+1)))
print("epoch {} train acc {}".format(e+1, train_acc / (batch_id+1)))
model.eval()
for batch_id, (token_ids, valid_length, segment_ids, label) in tqdm(enumerate(dev_dataloader), total=len(dev_dataloader)):
token_ids = token_ids.long().to(device)
segment_ids = segment_ids.long().to(device)
valid_length= valid_length
label = label.long().to(device)
out = model(token_ids, valid_length, segment_ids)
test_acc += calc_accuracy(out, label)
print("epoch {} test acc {}".format(e+1, test_acc / (batch_id+1)))
epoch 5로 학습했을 때, 코랩 GPU 환경에서 30분내로 학습이 완료되었다. 학습결과 train set에서는 정확도가 0.98로 높게 나왔고, test set에서는(데이터에서는 dev set) 0.93이 나왔다.
test set의 경우 라벨링이 되어 있지 않았기 때문에, Korean HateSpeech Dataset 제작 측에서 오픈한 캐글 대회 Korean Gender Bias Detection에서 평가했다.
대회의 평가지표는 *F1 Score였고, BASELINE은 BERT모델을 사용했을 때의 점수인 0.68138이었다.
* F1 Score는 정밀도와 재현율의 조화평균, 즉 어느 한쪽으로 치우치지 않을 때 높은 값을 가지는 평가지표이다.
* 재현율(Recall)은 데이터가 실제로 TRUE일 때 모델이 TRUE라고 예측한 경우, 정밀도(Precision)는 모델이 TRUE라고 예측했을 때 데이터가 실제 TRUE인 경우를 말한다(둘은 서로 trade-off 관계이다).
에포크를 변경하며 제출해보았는데 최종적으로 5일 때 가장 높은 점수인 0.70833을 얻을 수 있었다.
5. 결과 및 느낀점🙋♂️
Korean HateSpeech Dataset 데이터와 koBERT 모델을 사용해서 댓글 내 성 편견 유무를 분류하는 모델을 만들어 보았다. 이틀간 정신없이 진행했는데, 다행히 좋은 데이터와 모델을 찾아내 기간 내 완성할 수 있었다.
프로젝트를 하며 느낀점은 크게 세 가지로 나눌 수 있다.
✔ 악성 댓글 문제와 근본적인 해결책
첫째는 악성 댓글 문제에 관한 고민이다. 이 프로젝트는 악성 댓글 삭제, 댓글 작성자 제재 및 신고에 활용되기를 기대하며 기획했다. 하지만 여러 기사와 자료를 보면서 댓글 삭제나 처벌과 같은 방법만이 해결책인지에 대한 의문이 들었고, 장기적인 관점에서는 딥러닝과 같은 기술적 방법뿐만 아니라 제도적 방법 등 다른 노력도 필요하겠다고 느꼈다.
✔ 데이터와 인공지능, 그리고 사회 문제
두번째는 사회적 문제를 해결하는 것에 대한 관심이다. Korean HateSpeech Dataset 데이터 셋 깃허브와 태깅 가이드라인, 발표 자료 등을 읽으며 하나의 데이터셋을 만드는 과정이 얼마나 복잡한 일인지 알 수 있었다. 하지만 그 과정 속에서도 '혐오'와 '편견'이라는 주제에 대해서 깊게 고민하고, 문제를 해결하려는 사람들이 멋있게 느껴졌고 나 또한 인공지능으로 그러한 일을 하고 싶다 생각했다.
✔ 추가 학습에 대한 필요성
마지막은 역시 추가 학습에 대한 필요성이다. 모델의 일반화 성능을 높이기 위한 방법, 성 편견 댓글뿐만 아니라 다양한 편견 및 혐오를 분류하는 다중 분류 모델 구축에 대한 것도 추가적으로 알아보고 싶다.
비록 모델 관련해서는 예제를 거의 따라 했지만, 주제 선정이나 데이터 수집, 코드 분석을 하면서 지난주보다는 조금 더 성장하지 않았나 생각한다. 실력이 한참 부족하지만 계속 부딪혀봐야지.
프로젝트 정리 글은 여기서 끝끝🖐
Korean HateSpeech Dataset
KoBERT
Kaggle 대회
'Code States AI' 카테고리의 다른 글
[코드스테이츠/개인프로젝트] 알라딘 베스트셀러 데이터를 사용한 콘텐츠 기반 책 추천 웹 애플리케이션 제작📚 (6) | 2022.03.01 |
---|---|
[코드스테이츠/개인프로젝트] 2주 프로젝트를 앞둔 심정과 계획들😇 (2) | 2022.02.14 |
[PYTHON/파이썬] 워드 클라우드(Word Cloud)로 한글 데이터 시각화하기 (0) | 2022.01.14 |