예전에 친구와 얘기를 나누던 중, 다음 기사와 네이버 기사의 댓글 온도차(?) 가 크다는 걸 발견했었다.
문제가 됐던 기사 인데, 네이버와 다음의 댓글은 각각 이랬다.
https://news.naver.com/main/read.naver?m_view=1&mode=LSD&mid=sec&sid1=100&oid=032&aid=0003083419
https://news.v.daum.net/v/20210703121619046
이 댓글의 온도차를 보면서 친구와 웃고 나서 한참 뒤, 나는 네이버와 다음 댓글 수집 방법에 대해 글을 썼었고,
그 뒤로 또 한참 뒤, 위와 같은 상황이 기억이 나서 실험을 하게 됐다.
궁금증은 네이버와 다음 댓글로 이진 분류를 하면 극명하게 갈릴 것인가?
컴퓨터로 이진분류하는 방법은 무수히 많겠지만 오늘은 머신러닝의 한 방법인 logistic regression 을 사용해보려고 한다.
데이터 모으기
데이터 준비는 이전 블로그 글을 참고!
데이터 수집일은 이 글을 적은 시점과 다르다.
(2021-11-26 날자에 2021-11-25 날짜 기사 (네이버, 다음) 의 기사 댓글 을 수집하였음!)
daum 댓글의 경우, api 가 건네준 정보가 많기 때문에 한번 확인을 해야했다
(수집한지 오래되서 기억이 가물가물)
우리가 필요한건 “contents”(댓글) 이다.
네이버, 다음 댓글 간의 개수차이가 존재했는데, (네이버 약 13만개, 다음 약 20만개) 이때 다음 댓글을 약 7만개 정도 덜어내기 위해 기준이 필요했다.
잘보면 중간에 sympathyCount, antipathyCount 값이 존재한다. 공감, 비공감 수를 의미하고
나는 공감 많은 순으로 내림차순 정렬한 뒤, 네이버 댓글 수만큼 개수를 맞춰주려 한다.
daum = daum[daum['contents'] != ""] # 댓글이 공란으로 존재하는 경우가 있어 제거
# 공감순으로 내림차순 정렬
daum = daum.sort_values(by=['sympathyCount'], axis=0, ascending=False)
# 댓글 , 공감수, 비공감수 만 남기고 다른 column 제거
daum = daum[['contents','sympathyCount','antipathyCount']]
네이버 댓글 개수만큼 다음 댓글 개수를 맞춰주기 위해
daum = daum[:len(naver)]
다음은 두 datafram을 하나의 데이터로 병합하는 작업이다. 댓글과 사이트별로 라벨링을 해준다.
네이버의 경우 1, 다음의 경우 0으로 라벨링해 차후 logistic regression 할때 분류 기준으로 삼는다.
new_comment_list = []
for i, row in tqdm(naver.iterrows(), total=len(naver)):
temp_dict = {}
temp_dict['comment'] = row['content']
temp_dict['label'] = 1 #naver의 경우, labeling을 1로
new_comment_list.append(temp_dict)
for i, row in tqdm(daum.iterrows(), total=len(daum)):
temp_dict = {}
temp_dict['comment'] = row['contents']
temp_dict['label'] = 0 #daum의 경우, labeling을 0으로
new_comment_list.append(temp_dict)
comment_df = pd.DataFrame(new_comment_list)
임베딩하기
기본적인 데이터는 준비되었으니, 해당 데이터를 컴퓨터가 알아듣게 숫자 데이터로 바꿔줄 필요가 있다.
내 데스트탑으로는 머신러닝을 돌릴 수 없으니, 구글 Colab을 이용한다.
데이터를 불러오기
import pandas as pd
from tqdm.notebook import tqdm
from gensim.models import Word2Vec
import numpy as np
from google.colab import drive
drive.mount('/content/drive') # 구글 드라이브 연결
comment_df = pd.read_csv('경로')
문장→토큰→임베딩 벡터 로 바꾸는데, 토큰의 단위는 음절, 어절, 형태소, 다른 규칙 등 성능이 잘나올 것 같은걸로 진행하면 된다. 내 경우에는 음절 단위로 토큰을 생성했다.
(그전에 다음, 네이버 댓글 dataFrame을 row 단위로 섞어준다.)
comment_df = comment_df.sample(frac=1) # row 전체 shuffle
comment_df = comment_df[['comment', 'label']]
comment_df.index = range(len(comment_df))
train / test set으로 나눈뒤, train set으로 word vector를 구성한다. word vector 의 경우, word2vec 이용!
from sklearn.model_selection import train_test_split
train, test = train_test_split(comment_df, test_size=0.2) # 8:2 로 나누기!
train_token = []
for i, row in tqdm(train.iterrows(), total=len(train)):
comment = row['comment']
token_list = comment.split()
syllable_list = []
for token in token_list:
syllable_list.extend([t for t in token]) # 음절 단위로 나눈걸로 문장 구성
train_token.append(syllable_list)
#벡터 크기는 300, 단어 양옆으로 최대 5개씩, 1개 나온 것도 무시하지 않고 보기, 모델 구성시 thread 4개사용
model = Word2Vec(sentences=train_token, size=300, window=5, min_count=1, workers=4)
model.save("~경로/word2vec_comment_20220122.model")
train set으로 구성한 모델을 이용해 train / test 댓글 → train / test vector로 구성한다.
# comment -> syllable -> vector
def comment2vec(model = None, comment_list = None):
if model == None:
return []
else:
if comment_list == None:
return []
else:
# train 에 등장하지 않았던 음절이 존재할 수 있기 때문에 모르는 음절은 [0,0,0...]
unk_vec = np.zeros(model.trainables.layer1_size, dtype="float32")
comment_vec_list = []
for comment in tqdm(comment_list, total=len(comment_list)):
token_list = comment.split()
comment_vec = []
for token in token_list:
syllable_list = [t for t in token]
for syllable in syllable_list:
vec = []
if syllable in model.wv:
vec = model.wv[syllable]
else:
vec = unk_vec
comment_vec.append(vec)
comment_vec = np.average(comment_vec, axis=0)
comment_vec_list.append(comment_vec)
return comment_vec_list
벡터 구성
train_vec = comment2vec(model= model, comment_list = [comment for comment in train['comment']])
test_vec = comment2vec(model = model, comment_list = [comment for comment in test['comment']])
train_label = [ [label] for label in train['label']]
test_label = [ [label] for label in test['label']]
분류하기
이번에는 TensorFlow 가 아니라 PyTorch를 이용했고, 코드는 여기를 참고했다.
필요한 모듈 임포트
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
벡터를 torch Tensor 로 변경
torch.manual_seed(1)
train_vec_list = np.array(train_vec)
x_train = torch.FloatTensor(train_vec_list)
train_label = np.array(train_label)
y_train = torch.FloatTensor(train_label)
test_vec_list = np.array(test_vec)
x_test = torch.FloatTensor(test_vec_list)
test_label = np.array(test_label)
y_test = torch.FloatTensor(test_label)
기본적인 logistic regression 학습
# 모델 초기화
W = torch.zeros((model.trainables.layer1_size, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# optimizer 설정
optimizer = optim.SGD([W, b], lr=1)
nb_epochs = 10000
for epoch in range(nb_epochs + 1):
# Cost 계산
hypothesis = torch.sigmoid(x_train.matmul(W) + b)
cost = - ((y_train * torch.log(hypothesis) + (1 - y_train) * torch.log(1 - hypothesis)).mean())
# cost로 H(x) 개선
optimizer.zero_grad()
cost.backward()
optimizer.step()
# 10번마다 로그 출력
if epoch % 1000 == 0:
print('Epoch {:4d}/{} Cost: {:.6f}'.format(
epoch, nb_epochs, cost.item()
))
결과를 살펴보자
hypothesis = torch.sigmoid(x_train.matmul(W) + b)
prediction = hypothesis >= torch.FloatTensor([0.5])
true_count = 0
false_count = 0
for label, pred in zip(train_label, prediction):
if (label == 0 and pred.item() == False) or (label == 1 and pred.item() == True):
true_count += 1
else:
false_count += 1
test set 결과
역시 60%... 랜덤으로 고르는 결과를 50%라고 보니, 랜덤으로 고른것과 다를 바 없는 것이다.
그래도 마지막으로 내가 쓴 댓글이 다음 댓글인지, 네이버 댓글인지 구별해보자.
def commentClassification(txt):
txt_vec = comment2vec(model, [txt]) # 문장 -> 토큰 -> 벡터로 변경해주는 함수
input = torch.FloatTensor(txt_vec[0]) # 해당 벡터를 텐서로 변경
hypothesis = torch.sigmoid(input.matmul(W) + b) # 학습한 W,b로 결과 반환
prediction = hypothesis >= torch.FloatTensor([0.5])
if prediction.item() == False:
print("daum comment")
else:
print("naver comment")
흠... 처음가정과 다른 결과가 나왔다.
문제는 이럴 것 같다. 토큰 단위를 단순히 음절 단위로 나눈것 (보통 형태소, 어절 단위로 진행)
예를 들어 [ 문, 재, 인] 과 [ 문, 재, 앙] 과 같은 단어가 음절로 나눈걸 word2vec에 돌렸을 경우,
‘인’ 단어와 ‘앙’ 단어는 벡터 공간상 비슷한 곳에 맵핑 될 가능성이 높다. (주변 단어들이 비슷하므로)
나는 인, 앙의 미묘한 차이를 통해 네이버/ 다음 댓글을 구별하고 싶었으니
음절단위로 진행하는 것 잘못된 선택이였다는 것이다!
아무튼 이렇게 첫번째 댓글 구별을 진행했고, 이후 방법을 달리해 진행할 요소는
- 토큰 단위를 다르게 하기 (어절, 형태소, sentencepiece 등)
- 임베딩 방법을 다르게 하기 (pretrain 방법이 아니라 nn.Module 임베딩을 해보기?)
- 분류 모델 다르게 하기(svm 등?)
하지만 언제 할지는 미지수~
이번에도 재밌었다!
'text > Python' 카테고리의 다른 글
ㅇㅎ 게시물 수집하기 (fastapi, APScheduler, MySql) (1) | 2022.05.05 |
---|---|
다문서 요약 하기 (multi-document summarization) (0) | 2022.03.01 |
다음 뉴스 댓글 가져오기 (0) | 2021.11.26 |
문서 요약 하기 (with textrank) (0) | 2021.10.23 |
뉴스 문서 군집화 하기.ver2 ( document clustering using Minhash & LSH) (1) | 2021.10.15 |
댓글