EDA란?
Exploratory Data Analysis의 약자로 데이터의 실질적인 분석 및 데이터를 활용한 작업 이전에 데이터의 분포 등 대략적인 정보를 파악하기 위한 작업. 이름에서도 알 수 있듯이 데이터를 탐색하는 과정이라 볼 수 있다. 주로 시각화와 함께 이뤄진다.
자연어 처리에서의 EDA
일반적인 수치 데이터에 대한 EDA가 가장 쉽게 찾아볼 수 있지만, 자연어 처리에 대한 EDA는 떠올리기 힘들었다. 따라서 이번 글에서는 자연어 처리에서의 EDA에 대해 직접 수행해본 결과를 공유해 보고자 한다. 주로 문자열의 길이 통계나품사, 토큰 등의 단위로 구분한 후 이에 대한 통계가 이용되는 듯하다.
본문은 다음 자료를 참고하였다. 영문 데이터에 대한 EDA에 대한 내용이어서 한국어 자연어처리에 맞게 몇몇 코드들을 수정하고, api를 이용하였다.
neptune.ai/blog/exploratory-data-analysis-natural-language-processing-tools
분석
EDA를 위한 데이터로는 '유튜버 보겸의 유튜브 제목들'을 사용하고자 한다. 생각보다 많은 영상들이 있었으며, 구독자 수도 많고, 제목 어그로가 댓글에서도 밈처럼 언급될 만큼 제목을 잘 짓는다고 생각하여 분석 대상으로 선정하였다.
크롤링 된 전체 영상 제목 수: 7868개
제목 길이 분포
코드
#제목의 글자 수 히스토그램
ax = df['title'].str.len().hist()
ax.set_xlabel('글자 수')
ax.set_ylabel('빈도 수')
결과
20~40자 사이의 제목이 주로 쓰이는 것을 알 수 있다.
토큰 수 분포
-띄어쓰기, 탭, 개행 등의 구분자로 구분된 단위를 토큰으로 보았다.
코드
#토큰 수
ax = df['title'].str.split().map(lambda x: len(x)).hist()
ax.set_xlabel('토큰 수')
ax.set_ylabel('빈도 수')
결과
대부분 10개 이하의 토큰(어절)이 쓰였음을 알 수 있다. 대부분의 구분자가 띄어쓰기라는 걸 고려한다면 10번 미만의 띄어쓰기로 구성된 제목이 대다수 임을 유추해 볼 수 있다.
토큰별 길이 평균 분포
코드
#토큰별 길이 평균
ax = df['title'].str.split().\
apply(lambda x : [len(i) for i in x]). \
map(lambda x: np.mean(x)).hist()
ax.set_xlabel('토큰 길이 평균')
ax.set_ylabel('빈도 수')
결과
5~6개 미만의 토큰(어절)들이 자주 쓰였다.
명사 통계
-mecab 형태소 분석기를 사용하였으며, 고유명사가 많이 쓰였으나, 실제로 명사 추출이 잘 수행되지는 않아서 데이터를 일부 살펴본 후 사용자 사전에서 추가해 주었다.
-제목 중 명사들의 분포를 조사하고자 한다. 1글자로만 추출된 대상 중 의존명사처럼 의미 없다고 생각되는 경우들도 있어서 1글자를 제외한 내용들도 따로 시각화해 보았다.
코드1
## 명사별 개수
# 명사 추출 > 전체 코퍼스로 구축 > 개수 세기 > 시각화
from eunjeon import Mecab
m = Mecab('C:/mecab/mecab-ko-dic')
import collections
import seaborn as sns
import matplotlib.pyplot as plt
titles = df['title'].to_list()
n_corpus = []
for t in titles:
n_corpus += m.nouns(t) #mecab에서 명사 추출
count = collections.Counter(n_corpus)
most = count.most_common() #빈도 수 순으로 추출
x, y= [], []
for word,count in most[:40]:
x.append(word)
y.append(count)
plt.rcParams['font.family'] = 'NanumGothic'
plt.figure(figsize=(10,10))
sns.barplot(x=y,y=x)
결과1
현재 유튜브의 유튜버 이름인 '보겸'이 가장 많이 등장했으며, '하이라이트', '롤', '아프리카', '오버워치', 그리고 롤의 챔피언 이름들도 등장하였다. 과거 영상 제목들을 살펴보면, '하이라이트' +'챔피언 이름' 형식의 제목들을 쉽게 찾아볼 수 있는 점과 연관시켜 볼 수 있겠다.
코드2
#2글자 이상의 명사
n_corpus2 = []
for t in titles:
for n in m.nouns(t):
if len(n)>1:
n_corpus2.append(n)
count = collections.Counter(n_corpus2)
most = count.most_common() #빈도 수 순으로 추출
x, y= [], []
for word,count in most[:40]:
x.append(word)
y.append(count)
plt.rcParams['font.family'] = 'NanumGothic'
plt.figure(figsize=(10,10))
ax = sns.barplot(x=y,y=x)
ax.set(xlabel = '빈도 수', ylabel = '명사')
결과2
'롤'이 제거된 것은 아쉽지만, 좀 더 깔끔한 명사들을 살펴볼 수 있다.
형태소 통계
-계속해서 mecab을 이용해 형태소 별 분포를 살펴보고자 한다.
코드
#형태소 분석 후 형태소별 통계, 일반 명사, 고유명사 순
tags = []
for t in titles:
temp = m.pos(t)
for p in temp:
tags.append(p[1])
counter=collections.Counter(tags)
x,y=list(map(list,zip(*counter.most_common(10))))
ax = sns.barplot(x=y,y=x)
ax.set(xlabel = '빈도 수', ylabel = '형태소')
결과
mecab-ko에 사용된 품사 태그 정보는 여기에서 찾아볼 수 있다. 일반 명사(NNG)가 가장 많이 잡혔으며, 고유 명사(NNP), 숫자(SN), 기타(SY)가 그 뒤를 이었다.
N-gram 통계
n-gram은 쉽게 말해서 n개의 단위이다. 단위는 형태소, 어절, 음절 등 다양하게 적용할 수 있다.
예를 들어 "한국 사람들은 정말 멋지다."라는 문장이 있으면, 여기서 2-gram(어절 단위로)으로 데이터를 뽑아내면, (한국, 사람들은), (사람들은, 정말), (정말, 멋지다)가 될 것이다.
n-gram 추출 코드
from sklearn.feature_extraction.text import CountVectorizer
m_corpus = []
for t in titles:
m_corpus.append(' '.join(m.morphs(t)))
def get_top_ngram(corpus, n=None):
vec = CountVectorizer(ngram_range=(n, n)).fit(corpus)
bag_of_words = vec.transform(corpus)
sum_words = bag_of_words.sum(axis=0)
words_freq = [(word, sum_words[0, idx]) for word, idx in vec.vocabulary_.items()]
words_freq =sorted(words_freq, key = lambda x: x[1], reverse=True)
return words_freq[:20]
2-gram 통계(형태소)
코드
top_n_bigrams=get_top_ngram(m_corpus,2)[:20]
x,y=map(list,zip(*top_n_bigrams))
ax = sns.barplot(x=y,y=x)
ax.set(xlabel = '빈도 수', ylabel = '2gram 어절')
결과
3-gram 통계(형태소)
코드
top_n_bigrams=get_top_ngram(m_corpus,3)[:20]
x,y=map(list,zip(*top_n_bigrams))
ax = sns.barplot(x=y,y=x)
ax.set(xlabel = '빈도 수', ylabel = '3gram 형태소')
결과
앞서 살펴본 5년 전 제목들 중 비슷한 패턴으로 나타나는 제목들이 주로 나타난 것을 알 수 있다. 하이라이트 + 날짜, 챔피언 이름 + 하이라이트 + highlight 등의 패턴이 자주 보인다.
그러나 실제로 년도와 조회수의 분포를 살펴보면, 4,5년 전의 데이터가 상대적으로 적게 나타나는 것을 볼 수 있다. (0.2 * $10^7$ = 200만 이하)
물론 채널의 크기와도 연관이 있겠지만, 단순히 조회수만 가지고 어그로성을 판단한다면, 3년 번부터의 데이터가 좀 더 의미있지 않을까 한다. 따라서 4, 5년 전의 데이터를 제거하고 2, 3-gram 분석을 진행해보았다.
바뀐 점은 롤 외에 다양한 게임들이 주로 등장했다. ps4의 라스트오브어스나, 워킹 데드, 모바일 게임인 클래시 로얄이나 pc게임 배그 등이 등장했다. 또 '봉탕'이라는 이름이 자주 등장했다. 그리고 위의 순위에선 아래쪽에 있지만, 죄송합니다, 만났습니다, 버렸습니다, 말씀 드리 (겠?) 습니다 처럼 '~ 습니다'의 패턴이 자주 등장함을 살펴볼 수 있었다. 여기서 조금만 더 나아가 '~습니다 패턴에 대해' 화용론적 측면까지 고려해 본다면, 제목과 다음 내용들을 연관지어 볼 수 있겠다.
- 죄송합니다: 사과하는 내용
- 만났습니다: 누군가와 같이 등장, 합방 정보 제공
- 버렸습니다: 어떤 물건, (또는 캐릭터..?) 를 버렸다는 정보 제공, 버리는 대상이 주로 중요한 대상이거나 사람들의 이목을 끌 대상임이 추측됨.
- 말씀드리겠습니다: 어떤 정보를 제공할 것임을 암시
개체명 분석 후 통계
개체명 분석은 etri에서 제공하는 api를 이용하여 진행하였다. 생각보다 성능이 좋아 깜짝 놀랐다. 형태소 분석도 가능하고, 개체명 분석 외 의존 구문 분석등 다양한 분석이 가능하다. api코드의 발급은 여기에 잘 나와있다.
1회 최대 1만 자, 1일 5000회 제한이 있어 전체 데이터를 여러 번들로 나누어 api를 호출하였다.
#1회 최대 글자수 1만개
#1제목당 100글자라 했을 때, 80개를 한 묶음으로
#전체 데이터 수 7898개 즉, 80개씩 100묶음, 100회 호출
#80개씩 100묶음 만들기
print(len(titles))
title_bundles = []
#bundle의 순서: 0~79, 80~159, ...
for i in range(100): #마지막은 나머지 알아서 처리?
if i == 99:
title_bundles.append('\n'.join(titles[i*80:]))
break
else:
title_bundles.append('\n'.join(titles[i*80:(i+1)*80]))
api호출하여 개체명 분석 결과 받기 및 json파일을 원하는 데이터 형식으로 변환
import urllib3
import json
from tqdm import tqdm
#api호출 후 ne로 바꿔줄 코드
#구어체가 더 잘 잡을 것 같다.
openApiURL = "http://aiopen.etri.re.kr:8000/WiseNLU_spoken"
accessKey = "신청 후 발급받은 api코드를 입력해 주세요"
analysisCode = "ner"
ne_list = []
err_list = []
for i, t in enumerate(tqdm(title_bundles)):
text = t
requestJson = {
"access_key": accessKey,
"argument": {
"text": text,
"analysis_code": analysisCode
}
}
http = urllib3.PoolManager()
response = http.request(
"POST",
openApiURL,
headers={"Content-Type": "application/json; charset=UTF-8"},
body=json.dumps(requestJson)
)
dict_temp = json.loads(str(response.data,"utf-8"))
try:
for d in dict_temp['return_object']['sentence']:
ne_list += d['NE']
except:
err_list += dict_temp
#json 파일 중 원하는 데이터만 추출하기
ne_dict = []
ne_names = []
ne_tags = []
ne_txt = ''
for n in ne_list:
ne_txt += n['text'] + '\t' + n['type'] + '\n'
ne_dict.append((n['text'], n['type']))
ne_names += [n['text']]
ne_tags += [n['type']]
단어 뿐만 아니라 구 단위의 개체명도 잡아내었다. 물론 '구'의 경우 정확도가 떨어지긴 한다.
개체명 분포
코드
#개체명 이름 개수
count = collections.Counter(ne_names)
most = count.most_common()
x, y= [], []
for word,count in most[:40]:
x.append(word)
y.append(count)
plt.rcParams['font.family'] = 'NanumGothic'
plt.figure(figsize=(10,10))
ax = sns.barplot(x=y,y=x)
t.figure(figsize=(10,10))
ax = sns.barplot(x=y,y=x)
ax.set(xlabel = '빈도 수', ylabel = '개체명')
결과
신기하게도 만화 '원피스'가 가장 많은 빈도 수를 차지하였다. '보겸'이 '보겸이', '보겸TV'등으로 빈도 수가 분산되었기 때문이라 보인다. 또 LOL, LoL , GTA5, 심지어 '보겸 + 야스오'의 합성어로 볼 수 있는 '보스오'까지 잡아낸 것을 보면 성능이 꽤 괜찮아 보인다.
개체명 태그 분포
태그셋(tagset)은 다음 링크에서 확인해볼 수 있다.
코드
#개체명 종류 개수
count = collections.Counter(ne_tags)
most = count.most_common()
x, y= [], []
for word,count in most[:40]:
x.append(word)
y.append(count)
plt.rcParams['font.family'] = 'NanumGothic'
plt.figure(figsize=(10,10))
ax = sns.barplot(x=y,y=x)
ax.set(xlabel = '빈도 수', ylabel = '개체명 태그')
결과
개체명 PS-NAME 분포
태그 중 가장 많은 빈도 수를 보인 'PS-NAME'에 대해 해당 태그를 갖는 개체명을 추가로 살펴보았다.
코드
#개체명 PS_NAME 개수 (가장 많은 종류)
ne_ps_names = []
for i, n in enumerate(ne_names):
if ne_tags[i] == 'PS_NAME':
ne_ps_names.append(n)
count = collections.Counter(ne_ps_names)
most = count.most_common()
x, y= [], []
for word,count in most[:40]:
x.append(word)
y.append(count)
plt.rcParams['font.family'] = 'NanumGothic'
plt.figure(figsize=(10,10))
ax = sns.barplot(x=y,y=x)
ax.set(xlabel = '빈도 수', ylabel = '개체명(PS_NAME)')
결과
워드 클라우드
코드
from wordcloud import WordCloud#, STOPWORDS
#stopwords = set(STOPWORDS)
#2글자 이상 명사
n_corpus2 = list(filter(lambda a: len(a) >= 2, n_corpus))
count = collections.Counter(n_corpus2)
most = count.most_common(40)
def show_wordcloud(data):
wordcloud = WordCloud(font_path = r'NanumFontSetup_TTF_GOTHIC\\NanumGothicBold.ttf', #글자가 깨져서 폰트 파일의 주소를 할당해 주었다.
background_color='white',
#stopwords=stopwords,
max_words=100,
max_font_size=30,
scale=3,
random_state=1)
wordcloud=wordcloud.generate(str(data))
fig = plt.figure(1, figsize=(12, 12))
plt.rcParams['font.family'] = 'NanumGothic'
plt.axis('off')
plt.imshow(wordcloud)
plt.show()
show_wordcloud(most)
결과
'컴퓨터' 카테고리의 다른 글
[알고리즘] 백준. 행렬 제곱 #10830 (0) | 2021.01.19 |
---|---|
블랙 서바이벌 영원회귀 리뷰글 분석(EDA) (feat. 도배글 처리) (0) | 2021.01.04 |
python pandas 기본 정리 (0) | 2020.12.11 |
pyinstaller FileNotFoundError: [Errno 2] No such file or directory: [16716] Failed to execute script 오류 (0) | 2020.12.06 |
[엑셀] 열 이동시키기. (0) | 2020.10.29 |
댓글