본문 바로가기
text/Python

뉴스 문서 군집화 하기 (document clustering with DBSCAN)

by hoonzii 2021. 4. 28.
반응형

문서 클러스터링 해보려고 한다.

 

순서는

1. 데이터 모으기

2. 데이터를 분류 가능하게 변환하기

3. 변환된 데이터 분류하기 

로 단순화 시킬 수 있다.

 

사용한 모듈의 경우 다음과 같다.

- python 3.7
- requests, BeatifulSoup => 데이터 모으기 위해 사용

- pandas => 모은 데이터를 좀더 보기 편하게 바꾸거나, 저장하기 위해 사용
- Konlpy(Okt) => 데이터 정제할때 사용
- sklearn => 클러스터링 작업때 사용

 

1. 데이터 모으기

 

저번엔 NAVER 에서 데이터를 뽑았으니, 이번엔 DAUM에서 데이터를 뽑아보자

 

네이버 영화평 가져오기

 

네이버 영화평 가져오기

네이버 영화평 가져오기 설명 들어가기 전 네이버의 robots.txt 에 대해 먼저 숙지하자. 사용 언어 및 모듈     - python 3.7     - request = request 요청을 보내 html 값을 가져오기  ..

hoonzi-text.tistory.com

네이버 기사 댓글 가져오기

 

네이버 기사 댓글 가져오기

네이버 기사 댓글 가져오기 들어가기 전 네이버의 robots.txt 에 대해 먼저 숙지하자. 사용 언어 및 모듈     - python 3.7     - request = request 요청을 보내 html 값을 가져오기     ..

hoonzi-text.tistory.com

위 두 게시물을 보면 대충 어떻게 뽑으면 되겠구나... 감이 오실거라 믿는다.

 

그래도 정리하는 겸, 글자수를 늘려보는 겸 다음 뉴스 가져오는 과정을 적어본다.

import requests
from bs4 import BeautifulSoup

url = 'https://news.daum.net/breakingnews/?page=1&regDate=20210421'
html = requests.get(url)
soup = BeautifulSoup(html.content, "html.parser")
news_section = soup.find('ul',{'class':'list_news2 list_allnews'})
news_list = news_section.find_all('a',{'class':'link_txt'})

 

news_list 에는 전체기사 목록의 제목들과 해당 기사의 url이 담긴 tag값이 담기게 된다

 

전체기사 목록의 제목들과 해당 기사의 url이 담긴 tag값이 담기게 된다.

 

news_list를 순회하며 값을 조회해 보자

제목은 전체기사에서 조회해 가져왔고, 본문의 경우는 제목과 같이 존재하는 url 링크를 타고 들어가야 한다.

 

코드로 보자면

for news in news_list: # 리스트에는 제목&url이 들어있는 tag 들이 들어있다.
    article_title = news.text # 제목을 가져오기
    article_url = news.get('href') # url을 가져오기
    article_html = requests.get(article_url) # 가져온 url을 통해 html을 가져오기
    soup = BeautifulSoup(article_html.content, "html.parser")
    # 본문 텍스트가 있는 영역 혹은 태그의 값을 가져온다.
    text_list = [text_tag.text for text_tag in soup.find_all('p',{'dmcf-ptype':"general"})]
    # 리스트 형태이기 때문에 띄어쓰기 단위로 하나의 텍스트로 변환
    article_content = " ".join(text_list)
    print(article_title, article_content)
    print()

잘가져왔는지 살펴보면

 

위 코드에 대한 결과 예시

잘 가져온것을 확인 할 수 있다.

하나의 케이스에 대해 잘 돌아가는 것을 확인했다면, 반복문을 통해 특정날짜의 모든 기사를 조회할 수 있다.

 

코드로 보자면

article_list = [] # 기사가 담길 리스트 선언
index = 1 # page 이동을 위한 index 설정
while True:
	# page 를 변경하며 한페이지의 전체기사 목록을 가져오기
    url = 'https://news.daum.net/breakingnews/?page={}&regDate=20210421'.format(index)
    index += 1 
    html = requests.get(url)
    soup = BeautifulSoup(html.content, "html.parser")
    news_section = soup.find('ul',{'class':'list_news2 list_allnews'})
    news_list = news_section.find_all('a',{'class':'link_txt'})
    if len(news_list) < 15: # page내 기사가 15개 미만이라면 마지막 page로 간주, 반복문 탈출!
        break
    
    for news in news_list:
        article_title = news.text
        article_url = news.get('href')
        try:
            article_html = requests.get(article_url)
            soup = BeautifulSoup(article_html.content, "html.parser")
            text_list = [text_tag.text for text_tag in soup.find_all('p',{'dmcf-ptype':"general"})]
            article_content = " ".join(text_list)
			# 기사 정보를 제목과 본문으로 저장하기 위한 dictionary 형태로 변환
            article = dict()
            article['title'] = article_title
            article['content'] = article_content
        except:
            continue
        
        article_list.append(article) # 기사 담기
    # 중간중간 잘 진행되는지 여부 확인을 위해 기사 개수가 10배수 일때마다 표시하기
    if len(article_list) % 10 == 0: 
        print(index, len(article_list))
        time.sleep(0.5) # 가져오는 작업이 서버의 부담을 줄수있기에 잠깐씩 쉬어준다.

진행 상황 예시

결과를 살펴보자면

잘 들어간 것을 확인할 수 있다.

 

해당 기사 데이터를 pandas의 DataFrame으로 구성해 다음에도 사용할 수 있게 바꾸어 보자.

import pandas as pd

df = pd.DataFrame(article_list, columns = ['title','content'])
df.to_pickle("daum_news.pkl")

 

위 과정을 통해 데이터 모으기 작업은 완료 되었다. 이제 다음 작업으로 넘어가보자

 

2. 데이터를 분류 가능하게 변환하기

 

저번처럼 문장을 단순 음절 단위나 어절 단위가 아닌 명사 단위로 변환하려고 한다.

 

음절의 경우, 문서를 음절단위로 분해시켜 해당 음절 벡터의 합 또는 평균을 통해 문서의 정보를 나타내 한계점이 있고,

어절의 경우, 전체 단어 사전의 크기가 어절의 개수만큼 커지기 때문에 시도하지 않았다. 

(하지만 문서의 개수가 적다면 딱히 상관없을 듯하다.)

 

이번에는 문서를 명사들의 집합 {명사1, 명사2, 명사3,...} 으로 보고 주제가 같거나 비슷할 경우, 명사 집합의 요소가 비슷하고, 빈도역시 비슷할 것으로 가정한다.

 

문장에서 명사만을 걸러내기 위해 한국어 형태소 분석기를 이용한다.

대표적인 python의 한국어 형태소 분석기인 Konlpy를 사용한다. 

(설치 및 사용법에 대한 내용은 링크로 이동해 살펴보기 바란다. konlpy-ko.readthedocs.io/ko/v0.4.3/

 

KoNLPy: 파이썬 한국어 NLP — KoNLPy 0.4.3 documentation

KoNLPy: 파이썬 한국어 NLP KoNLPy(“코엔엘파이”라고 읽습니다)는 한국어 정보처리를 위한 파이썬 패키지입니다. 설치법은 이 곳을 참고해주세요. NLP를 처음 시작하시는 분들은 시작하기 에서 가

konlpy-ko.readthedocs.io

from konlpy.tag import Okt

okt = Okt() # 형태소 분석기 객체 생성
noun_list = []
for content in tqdm(df['content']): 
    nouns = okt.nouns(content) # 명사만 추출하기, 결과값은 명사 리스트
    noun_list.append(nouns)

10792 개의 기사 (다음 기사 가져오는 작업을 중간에 멈춰 약 1만개의 기사로 진행)

의 본문을 형태소 분석 후, 명사만 가져오는 작업을 진행 한 뒤, 이전 DataFrame에 결과를 합쳐준다.

 

중간에 명사 리스트가 비어있는 row가 보이는데, 한국어 형태소 분석기라 영어 문장의 경우, 분석이 불가능하다.

 

명사 리스트가 비어있으면 DataFrame에서 지워야 한다.

drop_index_list = [] # 지워버릴 index를 담는 리스트
for i, row in df.iterrows():
    temp_nouns = row['nouns']
    if len(temp_nouns) == 0: # 만약 명사리스트가 비어 있다면
        drop_index_list.append(i) # 지울 index 추가
        
df = df.drop(drop_index_list) # 해당 index를 지우기

# index를 지우면 순회시 index 값이 중간중간 비기 때문에 index를 다시 지정
df.index = range(len(df))

잘 지워진 것을 확인 할 수 있다.

 

이제 명사 리스트를 이용해 문서를 벡터로 바꿔야 한다.

이번엔 tfidf를 이용한 문서 벡터를 만들어 볼 예정이다.

 

# tfidf 알고리즘이 궁금하다면 여기 ko.wikipedia.org/wiki/Tf-idf

 

tf-idf - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. TF-IDF(Term Frequency - Inverse Document Frequency)는 정보 검색과 텍스트 마이닝에서 이용하는 가중치로, 여러 문서로 이루어진 문서군이 있을 때 어떤 단어가 특정 문서

ko.wikipedia.org

# tfidfVectorizer의 parameter 에 대한 값은 여기 chan-lab.tistory.com/27

 

[Python/Jupyter] TF-IDF 파라미터 알아보기 / min_idf, analyzer, sublinear_tf, ngram_range, max_features

안녕하세요. 은공지능 공작소의 파이찬입니다! 정말 오랜만에 포스팅을 올리네요. 9월달은 정말 바쁘게 지낸 것 같습니다. 오늘은 tf-idf vectorizer를 생성할 때 필요한 여러가지 파라미터들에 대해

chan-lab.tistory.com

 

쉽게 얘기하자면 각 문서의 중요단어에는 가중치를 줘서 다른 단어의 비해 높은 점수를 주는 방식이다.

 

나의 경우는

전체 문서중 단어 빈도가 5미만 일경우 단어 계산에서 제외,

ngram_range = (1,5) 로 설정했다. 

 

# ngram이란 단어 몇개까지를 하나의 단어로 볼건지에 대한 정보.

1~5까지라 함은 "A", "A B", "A B C", "A B C D", "A B C D E" 까지 포함 한다는 의미

 

# 자세한 설명은 여기 en.wikipedia.org/wiki/N-gram

 

n-gram - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Contiguous sequence of n items from a given sample of text or speech In the fields of computational linguistics and probability, an n-gram is a contiguous sequence of n items from a gi

en.wikipedia.org

코드로 보자면

from sklearn.feature_extraction.text import TfidfVectorizer

# 문서를 명사 집합으로 보고 문서 리스트로 치환 (tfidfVectorizer 인풋 형태를 맞추기 위해)
text = [" ".join(noun) for noun in df['nouns']]

tfidf_vectorizer = TfidfVectorizer(min_df = 5, ngram_range=(1,5))
tfidf_vectorizer.fit(text)
vector = tfidf_vectorizer.transform(text).toarray()

결과 값으로 문서에 해당하는 vector list 형태로 반환된다.

 

여기서 나는 sklearn 의 Normalizer를 사용했는데,

각 문서 vector의 단어 점수값이 일정 범위를 넘어가지 않았으면 했다. (0~1 사이)

이유는 벡터 공간상 가까운 거리의 벡터끼리 묶는 문서 클러스터링 방법에 있어서

단어 점수의 차가 일정하지 않다면 잘 안 묶일 것이라 생각했다.

 

소 뒷걸음 치다 벌레를 잡는다고 했던가

 

마침 Normalizer document에 나와있는걸 가져와 보자면

(scikit-learn.org/stable/modules/generated/sklearn.preprocessing.Normalizer.html)

예쓰~!

TFIDF vector -> Normalize -> Cosine similarity를 이용해 클러스터링을 진행한다고 하더라~ 라도 써있다.

 

이제 변환된 벡터를 이용해 클러스터링 하는 단계가 남아있다.

 

3. 변환된 데이터 분류하기 

 

사실 이부분 부터 고민하기 시작했다. "어떤 방법" 으로 기사를 묶을 건지 클러스터링 방법을 찾아보았는데...

 

- k-means

  • 클러스터링 이라고 검색시 가장 먼저 나오는 알고리즘
  • 각 군집내 평균 벡터와 해당 군집에 속한 벡터간의 거리 제곱의 합이 최소가 되는 군집을 찾는 방법
  • 많이 나오는 만큼 쉽고, 빠르게 적용이 가능
  • 클러스터 수를 지정해야 함 (얼마나 있을줄 알고,,,?)
  • 노이즈 데이터에 취약
  • 중심점을 임의로 잡기 때문에 군집 결과가 상이하거나 나쁠수 있다

 

- k-means++

  • kmeans 처럼 무작위로 중심점 잡는게 아니라 데이터 포인트 중에 하나를 선택하는 방법
  • k-means 보다 군집의 결과가 좋다고 하고, 알고리즘의 속도 역시 빠르다고 한다 (안써서 모름)
  • 고차원이고 sparse한 벡터(ex. 문서 tfidf 벡터) 간의 연산은 적합하지 않다고 한다(kmeans 역시 마찬가지)

 

- DBSCAN

  • 밀도 기반의 클러스터링
  • 어느 특정 벡터부터 시작해 반경내 기준치 만큼의 점들이 존재한다면 군집화 하는 방식
  • 일정 밀도 이상의 데이터를 기준으로 군집을 형성하기 때문에 노이즈 처리에 용이
  • 이미 형성된 군집 기준으로 기준점을 옮겨가며 처리하기 때문에 분포가 이상한 데이터에도 강건
  • kmeans에 비해 속도가 느리고, 파라미터 값인 epsilon, min-point 값에 영향을 많이 받는다.

 

일단 k-means는 클러스터의 개수를 지정한다는 점때문에 사용하지 않았다.

모은 데이터에 군집이 실제로 몇개 존재하는지는 모르기 때문에 임의로 몇개 정해서 묶고 싶지 않았다.

그래서 사용한 방법이 DBSCAN 이다.

여기를 통해 개념을 파악했고, bcho.tistory.com/1205

 

클러스터링 #3 - DBSCAN (밀도 기반 클러스터링)

DBSCAN (밀도 기반 클러스터링) 조대협(http://bcho.tistory.com) 기본 개념 이번에는 클러스터링 알고리즘중 밀도 방식의 클러스터링을 사용하는 DBSCAN(Density-based spatial clustering of applications with..

bcho.tistory.com

sklearn의 dbscan 모듈을 이용했다.

코드로 보자면

from sklearn.cluster import DBSCAN
import numpy as np

vector = np.array(vector) # Normalizer를 이용해 변환된 벡터
model = DBSCAN(eps=0.3,min_samples=6, metric = "cosine")
# 거리 계산 식으로는 Cosine distance를 이용
result = model.fit_predict(vector)

result 에는 각 vector 값이 속하는 cluster 숫자를 배열 형식으로 반환한다.

클러스터링 결과

 

결과값을 DataFrame에 덮어 씌워 준다.

result column 을 통해 cluster 숫자를 조회 할수 있다.

 

이제 같은 클러스터 끼리 표시해 군집화가 잘 되었는지 확인해 보자.

코드로 보자면

for cluster_num in set(result):
    # -1,0은 노이즈 판별이 났거나 클러스터링이 안된 경우
    if(cluster_num == -1 or cluster_num == 0): 
        continue
    else:
        print("cluster num : {}".format(cluster_num))
        temp_df = df[df['result'] == cluster_num] # cluster num 별로 조회
        for title in temp_df['title']:
            print(title) # 제목으로 살펴보자
        print()

클러스터링 결과

예시로 보여준 스크린샷을 보면 굉장히 잘 묶여 있는 것을 확인 할수 있다.

하지만 아닌 부분들도 많았는데....

연예, 스포츠, 주식(경제) 등 관련 없는 뉴스가 묶인것을 확인
내용이 없는 포토뉴스의 경우 [포토]라는 글자에 의해 묶인 것을 확인
같은 주제이지만 다른 클러스터로 묶인 경우 확인

위 스크린샷을 통해 보듯 여러 문제점이 발견된다.

 

이렇게 DBSCAN을 이용한 문서를 군집화 해보았고,

이를 이용해 각 시간별로 뉴스를 묶어 보여줄 수 있는 서비스를 만들면 좋지 않을까 생각이 든다.

 

위 사례들을 보았을때, 개선점으로는

1. 각 주제별로 수집을 따로해 (예를 들어 정치면, 사회면, 경제면 등) 군집화 해 보여주면 여러 주제가 한꺼번에 있는 경우보다 군집 품질이 올라갈 것 같다.

2. 사진, 포토, 공시, 부고, 부음 등의 단어들이 들어간 뉴스를 필터링하면 군집화 품질이 올라갈 것 같다.

3. tfidf-vectorizer 말고 다른 방법을 통해 vector embedding을 하면 문서 벡터의 품질이 올라가 군집화 품질이 올라갈 것 같다.

4. 다른 클러스터로 묶인 같은 주제의 문서들은 제목간의 유사도를 이용해 병합이 가능하지 않을까 싶다.

 

당장 생각나는 건 이정도,,,?

반응형

댓글