본문 바로가기
컴퓨터

자연어 처리 EDA(Exploratory Data Analysis)

by skyjwoo 2020. 12. 17.
728x90
반응형

 

 

EDA란?

Exploratory Data Analysis의 약자로 데이터의 실질적인 분석 및 데이터를 활용한 작업 이전에 데이터의 분포 등 대략적인 정보를 파악하기 위한 작업. 이름에서도 알 수 있듯이 데이터를 탐색하는 과정이라 볼 수 있다. 주로 시각화와 함께 이뤄진다.

 

자연어 처리에서의 EDA

일반적인 수치 데이터에 대한 EDA가 가장 쉽게 찾아볼 수 있지만, 자연어 처리에 대한 EDA는 떠올리기 힘들었다. 따라서 이번 글에서는 자연어 처리에서의 EDA에 대해 직접 수행해본 결과를 공유해 보고자 한다. 주로 문자열의 길이 통계나품사, 토큰 등의 단위로 구분한 후 이에 대한 통계가 이용되는 듯하다.

 

본문은 다음 자료를 참고하였다. 영문 데이터에 대한 EDA에 대한 내용이어서 한국어 자연어처리에 맞게 몇몇 코드들을 수정하고, api를 이용하였다. 

neptune.ai/blog/exploratory-data-analysis-natural-language-processing-tools

 

Exploratory Data Analysis for Natural Language Processing: A Complete Guide to Python Tools - neptune.ai

Exploratory data analysis is one of the most important parts of any machine learning workflow and Natural Language Processing is no different....

neptune.ai

 

분석

 

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

 

 

현재 유튜브의 유튜버 이름인 '보겸'이 가장 많이 등장했으며, '하이라이트', '롤', '아프리카', '오버워치', 그리고 롤의 챔피언 이름들도 등장하였다. 과거 영상 제목들을 살펴보면, '하이라이트' +'챔피언 이름' 형식의 제목들을 쉽게 찾아볼 수 있는 점과 연관시켜 볼 수 있겠다. 

 

5년 전 제목들

 

 

코드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

 

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 분석을 진행해보았다. 

 

4,5 년 전 데이터 제거 후 2-gram 분석
4,5 년 전 데이터 제거 후 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']]

 

단어 뿐만 아니라 구 단위의 개체명도 잡아내었다. 물론 '구'의 경우 정확도가 떨어지긴 한다.

 

개체명 구(phrase) 예시들

 

 

개체명 분포

 

코드

#개체명 이름 개수
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)

 

결과

 

 

 

728x90
반응형

댓글