NLP 튜토리얼: 라벨링 없이 트위터 유저들을 자동으로 나누어보기

NLP 튜토리얼: 라벨링 없이 트위터 유저들을 자동으로 나누어보기

트위터에는 굉장히 다양한 유저들이 있다.

그리고 트위터 유저들은 “BIO”라고 부르는 자기소개 페이지에 자신에 대한 정보를 적어둔다.

Github 트위터 공식 계정

위 스크린샷과 같이, 자신의 계정이 어떤 계정인지를 간단한 160자 내로 드러내는 것이 바로 BIO다.

그렇다면, 이런 계정들이 ‘어떤’ 계정인지 BIO를 이용해 분류해 볼 수 있지 않을까?

하지만 모든 유저를 우리가 손으로 라벨을 붙여 학습시키는 것은 힘들다.

그렇다면 알아서 분류하려면 어떻게 해야할까?

간단한 자연어 처리를 통해 라벨링 없이 유저를 클러스터링해보자.

🌟 바로바로 실행하면서 따라올 수 있는 Google Colab 👨🏻‍💻👩🏻‍💻👇

https://colab.research.google.com/drive/1bgv3CHZDp2smIWXQwAUD3j5z8LAYbLVz

미리보기: 어떤 결과물이 나오나?

결과물 미리보기 - 마우스를 올리면 텍스트가 보인다

이번 튜토리얼이 끝날때, 위와 같이 각 유저의 트위터 프로필들 클러스터링 된 결과물을 2차원 공간에 차원축소를 통해 각각 다른 색의 동그라미로 보여주고, 각 동그라미 위에 마우스를 올리면 어떤 트위터 BIO였는지 보여주는 Plot을 완성한다.

유저를 분류한다? 어떻게?

분류, 즉 Classification 문제는 주로 라벨링을 통한 Supervised Learning 방법을 사용한다.

하지만 앞서 언급한 것과 같이 라벨링이 필요하고, 더 높은 정확도를 얻기 위해서는 더 많은 데이터에 더 많은 라벨링(노가다)가 필요하다.

한편 이와 대조되는 방법으로 Unsupervised Learning을 사용하기도 하는데, 데이터만 넣어서 어떻게든 뭔가 만들어내는 방식이기도 하다.

이번 글에서는 NLP 사용의 기본적인 방법인 Word Vectorize인 word2vec 을 이용해본다.

Word2Vec

Word2Vec은 단어를 N차원의 벡터로 만들어준다. (보통 300차원 이내를 쓴다.)

Word2Vec 자체가 텍스트만 Tokenize해서 넣어주면 몇 가지 종류의 알고리즘을 통해 단어(토큰) 간의 상관관계를 찾아내고 서로 유사한 공간에 배치하도록 만들어진 Unsupervised Learning이다.

상세한 원리는 Word2Vec으로 문장 분류하기 - Ratsgo’s blog를 참고해보세요.

트위터 프로필로 Word2Vec을 학습시키자? NO!

최근 NLP 트렌드는 Transfer Learning을 통한 성능의 개선이었다. 한정된 데이터가 아니라 수많은 데이터가 있다면 임베딩 공간이 좀 더 의미를 잘 부여받은 공간을 갖게된다는 이야기.

Word2Vec같은 경우는 연산량이 많지 않아 데스크탑 CPU만으로도 몇만건의 데이터는 금방 학습이 끝나지만, 기존의 거대 데이터(위키 등)를 사용한 학습이 좀 더 높은 성능을 보인다고도 한다.

한편, 학습된 Word2Vec 모델에 없는 ‘새로운 단어’가 등장할 경우에는 Unknown 단어의 벡터로 모두 매핑되기 때문에 한계가 있다.

따라서 이번에는 네이버 댓글 약 100만개를 사용해 미리 학습시켜둔 Word2Vec 모델을 가져와 사용한다. (추가적인 학습은 진행하지 않는다!)

아래 내용은 2020년 1월 3일 Google Colab 환경 기준입니다.

데이터 준비하기

우선 어떤 데이터를 쓸지부터가 관건이다.

하지만 걱정하지 마시라! 이미 모아둔 데이터를 받아서 써보자.

어떤 데이터를 사용하나?

이번 튜토리얼에서는 트위터에서 네이버 뉴스를 링크한 트윗들을 크롤링한 데이터셋을 사용한다.

아래 명령어를 Google Colab에서 입력하면 곧바로 사용할 수 있다.

만약 일반 PC에서 진행중이라면 curl 앞의 ! 만 생략하고 입력하면 된다.

1
2
3
4
!curl gdrive.sh | bash -s 13fQZtkz4SSzVNcqh73SBqnUDw2mSDgzL
!curl gdrive.sh | bash -s 1W8u9ZuEhKCkTbaGLzUF4YcF2F_hYeE1B
!curl gdrive.sh | bash -s 1GffvAF3tBtSpjsblIdP1Tcv25XEyBewm
!curl gdrive.sh | bash -s 103jom7lxjiqQptIRjrIdSjXR8jRqrlEd

위 다운로드를 모두 진행하면 트위터 유저 데이터 tsv파일 하나, 그리고 네이버 댓글로 학습시킨 word2vec 모델 파일이 다운로드 된다.

데이터 내용은 다음과 같이 유저고유번호, 유저 프로필이름, 유저 로그인이름, 유저의 BIO가 들어있다.

1
2
df = pd.read_csv('twitter_users.tsv', sep='\t')
df.head(1)

트위터 유저 데이터

Word2Vec 모델 불러오기

Gensim에서 사용하는 Word2Vec 모델을 이용한다.

아래 두 줄의 명령어로 기존에 학습된 word2vec 모델을 불러올 수 있다.

1
2
3
from gensim.models import Word2Vec

model = Word2Vec.load('embedding.save')

추가 라이브러리 설치하기

앞서 언급했던 것 중 하나가 Word2Vec은 단어/토큰을 vector로 바꾼다는 것이었다.

이를 위해서는 문장으로 이루어진 트위터 프로필 정보를 단어/토큰단위로 쪼개야 한다는 뜻이다.

이를 위해 KoNLPy 라이브러리 중 Mecab을 사용한다.

KoNLPy 설치

KoNLPy는 pip를 통해 간단히 설치할 수 있다.

1
!pip install -q konlpy

주의: KoNLPy는 JPipe를 사용해 Python 패키지이지만 java를 호출한다.

따라서 Java 1.7 혹은 1.8이 설치되어있고 java 명령어로 실행 가능하도록 시스템이 설정되어있어야 한다.

Mecab 설치

KoNLPy중 가장 속도가 빠른 mecab은 아래 명령어를 통해 추가 설치를 진행해야 한다.

1
!bash <(curl -s https://raw.githubusercontent.com/konlpy/konlpy/master/scripts/mecab.sh)

트위터 프로필을 토큰화하기 (단어로 쪼개기)

KoNLPy의 mecab을 이용해 트위터 프로필 문장을 명사들로 쪼개보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
from konlpy.tag import Mecab

mecab = Mecab()

def get_tokens(x):
try:
return [i for i in mecab.nouns(x) if len(i) > 1] if x else []
except Exception as e:
if str(x) == 'nan':
return []
print(e)
print(str(x))
raise e

get_token 함수는 x 문자열을 받으면 문자열 목록을 반환하게 된다.

단, 이때 한 단어는 2글자 이상으로 제한을 걸었다. (한 단어는 무의미한 경우가 많다.)

토큰화 함수 DataFrame에 적용하기

앞서 읽은 DataFrame 중 user.description 컬럼에 위 함수를 적용해주자.

1
2
df['user_mecab'] = df['user.description'].map(get_tokens)
df['user_mecab_len'] = df['user_mecab'].map(len)

이후 토큰화된 문자의 개수로 필터링 하기 위해 길이 컬럼도 추가로 만들어주자.

명사수가 N개 이상인 프로필만 추출하기

빈 프로필, 혹은 엄청 짧은 프로필은 정보가 충분히 있다고 보기 어렵다.

따라서 N개 이상의 단어로 이루어진 프로필만으로 제한을 걸어주자.

앞서 만든 user_mecab_len 컬럼이 5 이상인 것만 남겨두자.

1
bio_exists_df = df[df['user_mecab_len'] >= 5]

위 Dataframe을 살펴보면 아래와 같이 단어와 해당 길이가 나타난다.

토큰화된 유저 프로필

문장의 벡터 = Mean(각 단어의 벡터)

단어의 벡터는 알지만, 문장의 벡터는 어떻게 표현할 수 있을까?

이러한 질문의 답은, “문장의 벡터는 해당 문장의 단어들의 벡터 평균”이라고 볼 수 있다.

다른/혹은 더 높은 성능을 위한 방법으로는, 문맥을 이해하는 BERT, 혹은 Doc2Vec와 같은 문장단위 임베딩, 혹은 Word2Vec의 Mean을 취한 뒤 TF-IDF를 취해주는 방법 등이 있다.

따라서 아래와 같이 sentence vector를 얻는 함수를 만들어줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
def get_sentence_mean_vector(morphs):
vector = []
for i in morphs:
try:
vector.append(model.wv[i])
except KeyError as e:
pass
try:
return np.mean(vector, axis=0)
except IndexError as e:
pass

마찬가지로 위에서 만들어온 DataFrame에 입혀주자.

1
bio_exists_df['wv'] = bio_exists_df['user_mecab'].map(get_sentence_mean_vector)

트위터 프로필 문장 벡터로 클러스터링하기

클러스터링에도 여러 방법이 있지만, 이번에는 엄청 간단하고 빠르고 기본적인 KMeans를 써보자.

이것 역시 sklearn에 구현된 것을 쓸 수 있다.

1
2
3
4
5
6
7
8
9
10
from sklearn.cluster import KMeans
import time

word_vectors = bio_exists_df.wv.to_list()
num_clusters = 10

# K means 를 정의하고 학습시킨다.
kmeans_clustering = KMeans( n_clusters = num_clusters )
idx = kmeans_clustering.fit_predict( word_vectors )
bio_exists_df['category'] = idx

위 코드를 사용하면 앞서 만든 Dataframe의 wv 컬럼을 이용한다.

KMeans는 사용하기 간단하고 다른 것을 건드릴 필요도 없다.

다만, 유일하게 n_clusters 하이퍼파라미터를 지정해주어야 하는데 이 클러스터 갯수를 어떻게 지정하느냐에 따라 클러스터링의 성능이 크게 달라지기 때문에 해당 숫자를 신중하게 결정하는 것이 좋다.

사실 이것도 굉장히 empirical한 접근방법이지만, 데이터셋을 보고 어떤 숫자가 적절할지를 보는 것이 중요하다. 무조건 모델을 돌리기보다 어떤 데이터인지 아는게 더 중요하다.

실제 유저 수는 아래와 같이 나타난다. 1번 클러스터가 제일 크지만, 이게 어떤 의미를 갖지는 않는다.

오히려 각 클러스터에 어떤 유저들이 나타났는지가 더 중요한 것이다.

10개로 나눠진 카테고리별 유저 수

차원 축소 & 시각화

앞서 트위터 프로필 문장을 300차원의 벡터로 만들었다.

하지만 300차원은 컴퓨터 화면으로는 볼 수 없는 어마어마한 고차원이기 때문에, 2차원의 화면에 맞도록 2차원으로 차원축소를 진행해야 한다.

TSNE 차원축소

t-SNE는 시각화를 위한 차원축소에 자주 사용하는 알고리즘이다.

이것 역시 sklearn에 구현되어있기 때문에, 보다 쉽게 사용할 수 있다.

아래와 같이 Dataframe의 wv 컬럼을 맞춰주도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sklearn.manifold import TSNE

X = bio_exists_df['wv'].to_list()
y = bio_exists_df['category'].to_list()

import os.path
import pickle

tsne_filepath = 'tsne3000.pkl'

# File Cache
if not os.path.exists(tsne_filepath):
tsne = TSNE(random_state=42)
tsne_points = tsne.fit_transform(X)
with open(tsne_filepath, 'wb+') as f:
pickle.dump(tsne_points, f)
else: # Cache Hits!
with open(tsne_filepath, 'rb') as f:
tsne_points = pickle.load(f)

tsne_df = pd.DataFrame(tsne_points, index=range(len(X)), columns=['x_coord', 'y_coord'])
tsne_df['user_bio'] = bio_exists_df['user.description'].to_list()
tsne_df['cluster_no'] = y

결과물에는 x, y 2차원의 좌표값이 나오게 된다. (tsne_df)

아래 샘플과 같이 300차원이 2차원으로 줄어든 것을 볼 수 있다.

2차원으로 축소된 트위터 프로필

Bokeh로 2차원 Plotting

단순히 Matplotlib을 이용한 이미지 결과가 아니라 실제 내용물(트위터 프로필)을 확인할 수 있도록 bokeh 를 이용해 Interactive Plot을 그려보자.

우선 bokeh가 Jupyter Notebook에서 동작할 수 있도록 여러 PY/JS들을 로딩해주자.

1
2
3
4
5
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import HoverTool, ColumnDataSource, value
from bokeh.palettes import brewer

output_notebook()

우리는 10개의 클러스터에 각각 다른 색을 입힐 것이기 때문에 아래와 같이 컬러를 만들어주자.

1
2
3
4
5
6
7
8
9
10
# Get the number of colors we'll need for the plot.
colors = brewer["Spectral"][len(tsne_df['cluster_no'].unique())]

# Create a map between factor and color.
colormap = {i: colors[i] for i in tsne_df['cluster_no'].unique()}

# Create a list of colors for each value that we will be looking at.
colors = [colormap[x] for x in tsne_df['cluster_no']]

tsne_df['color'] = colors

이후 Bokeh가 인식하는 DataSource 객체를 만들어준다.

이때, Pandas DataFrame을 .to_dict(orient='list') 를 사용하면 해당 Dataframe의 모든 컬럼정보가 들어갈 수 있다.

1
2
3
4
# Bokeh Datasouce 만들기
plot_data = ColumnDataSource(
data=tsne_df.to_dict(orient='list')
)

그리고 실제 Plot을 그리기 위한 배경으로 650x650 사이즈의 공간을 만들어준다.

1
2
3
4
5
6
7
8
# Plot 만들기(배경)
tsne_plot = figure(
title='TSNE Twitter BIO Embeddings',
plot_width = 650,
plot_height = 650,
active_scroll='wheel_zoom',
output_backend="webgl",
)

output_backend="webgl", 옵션을 사용하면 GPU가속을 통해 훨씬 부드러운 차트를 경험할 수 있다.

이후 각각 요소에 마우스를 올릴 때 무엇을 보여줄지 Tooltip으로 user_bio 컬럼을 사용하도록 지정한다.

1
2
3
4
5
6
# 해당 Hover 툴팁 만들기
tsne_plot.add_tools(
HoverTool(
tooltips='@user_bio'
)
)

해당 Plot에 데이터 정보들을 넣어준다.

Source를 입력하고 x,y 축을 넣어준 뒤, color정보를 컬럼명(color 컬럼)으로 넣어주면 된다.

1
2
3
4
5
6
7
8
9
10
tsne_plot.circle(
source=plot_data,
x='x_coord',
y='y_coord',
line_alpha=0.3,
fill_alpha=0.2,
size=10,
fill_color='color',
line_color='color',
)

그리고 귀찮은 선들을 지운뒤 화면에 보이도록 하면 결과가 나타난다.

1
2
3
4
5
6
7
8
9
# 각 값들 추가해주기 
tsne_plot.title.text_font_size = value('16pt')
tsne_plot.xaxis.visible = False
tsne_plot.yaxis.visible = False
tsne_plot.grid.grid_line_color = None
tsne_plot.outline_line_color = None

# 짠!
show(tsne_plot)

결과 보기

위 코드를 모두 실행하면 아래와 같이 색깔별로 모인 유저 프로필들을 볼 수 있다.

특히 뭉쳐있는 유저들을 살펴볼 경우, 굉장히 비슷한 말들이 적혀있고 주제도 비슷한 것을 볼 수 있다.

최종 결과물

그리고 각 클러스터별로 어떤 유저들이 모여있는지 살펴보면 아래와 같이 연예인 팬, 혹은 정치적 이야기, 혹은 자신의 관심사 등에 따른 집단이 그룹으로 분류되는 것을 볼 수 있다.

BTS Army

맺으며

트위터 유저들 모두가 자신의 정보를 빼곡히 프로필에 작성하는 것은 아니다.

하지만 자신의 관심사가 어떤 것인지에 대해, 그리고 이 계정이 어떤 목적을 위한 계정인지 드러나는 것을 간단하게 모아 살펴볼 수 있다.

물론, 성능이 우리가 바라는 것처럼 어마어마하게 멋지게 나오지는 않지만, 무작정 Labeling을 하기 이전게 간단하게 이런 방식으로도 데이터를 재미있게 구경해볼 수 있다는 점에서는 의미가 있다고 생각한다.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×