Notice
Recent Posts
Recent Comments
Link
나의 개발일지
[네이버 뉴스 요약 프로젝트] 전처리(Preprocessing) + 군집화(Clustering) 본문
목차
프로젝트 소개 : https://study-yoon.tistory.com/224
1. 크롤링 : https://study-yoon.tistory.com/225
2. 군집화 : https://study-yoon.tistory.com/226
3. 요약 : https://study-yoon.tistory.com/227
Github : https://github.com/Yoon-juhan/naverNewsCrawling
크롤링 마친 데이터 프레임 (news_df) 사용
작업 순서 : 필요없는 기사 삭제 → 명사 추출 → 명사 벡터화 → 군집화 → 상위 군집 추출
1. 필요 없는 기사 삭제
- 네이버 요약봇이 요약을 지원하지 않는 유형을 참고해 기사를 삭제
- 3문장 이하 or 300자 이하 기사 삭제
def shortNews(news_df):
news_df.drop(news_df[news_df['content'].apply(lambda x : len(x.split("다."))) <= 4].index, inplace=True)
news_df.drop(news_df[news_df['content'].apply(len) <= 300].index, inplace=True)
- 포토, 영상, 인터뷰 기사 등 삭제
def EtcNews(news_df):
news_df.drop(news_df[news_df['title'].str.contains('사진|포토|영상|움짤|헤드라인|라이브|정치쇼')].index, inplace=True)
news_df.drop(news_df[news_df['content'].str.contains('방송 :|방송:|진행 :|진행:|출연 :|출연:|앵커|[앵커]')].index, inplace=True)
- 영어 기사 삭제
- 영어 기사 구분을 위해 파이썬 언어감지 라이브러리 langdetect를 사용
- 라이브러리 설치 : pip install langdetect
from langdetect import detect
# 언어 감지
def isEnglish(text):
try:
lang = detect(text)
return lang == 'en'
except:
return False
# 영어 기사 삭제
def englishNews(news_df):
news_df = news_df[~news_df['content'].apply(isEnglish)]
- 전체 코드
더보기
from langdetect import detect
# 3문장 이하 or 300자 이하 기사 삭제
def shortNews(news_df):
news_df.drop(news_df[news_df['content'].apply(lambda x : len(x.split("다."))) <= 4].index, inplace=True)
news_df.drop(news_df[news_df['content'].apply(len) <= 300].index, inplace=True)
# 언어 감지
def isEnglish(text):
try:
lang = detect(text)
return lang == 'en'
except:
return False
# 영어 기사 삭제
def englishNews(news_df):
news_df = news_df[~news_df['content'].apply(isEnglish)]
# 포토, 영상, 인터뷰 기사 등 삭제
def EtcNews(news_df):
news_df.drop(news_df[news_df['title'].str.contains('사진|포토|영상|움짤|헤드라인|라이브|정치쇼')].index, inplace=True)
news_df.drop(news_df[news_df['content'].str.contains('방송 :|방송:|진행 :|진행:|출연 :|출연:|앵커|[앵커]')].index, inplace=True)
def startRemove(news_df):
EtcNews(news_df) # 포토, 영상, 인터뷰 기사 등 삭제
shortNews(news_df) # 3문장 or 300자 이하 기사 삭제
englishNews(news_df) # 영어 기사 삭제
news_df.drop_duplicates(subset=["url"], inplace=True)
2. 명사 추출
- 한국어 형태소 분석 : https://konlpy.org/ko/latest/
- 자바를 필요로 하기 때문에 위 사이트에서 설치 방법 참고
- 명사 추출은 konlpy의 Okt 클래스와 Komoran 클래스를 비교해서 Komoran으로 결정
# Okt, Komoran 비교
from konlpy.tag import Okt, Komoran
content = """미국의 민간 달 착륙선 '오디세우스'가 우주에서 촬영한 사진을 지구로 보냈다.미국 우주 기업 인튜이티브 머신스는 지난 17일 X 계정을 통해 오디세우스가 우주에서 촬영한 사진 4장을 공개했다. 인튜이티브 머신스는 "16일 IM-1 프로젝트의 첫 사진을 성공적으로 전송받았다"며 "이 사진들은 스페이스X의 2단 추진체와 분리된 직후 촬영된 것"이라고 했다.오디세우스가 보낸 4장의 사진은 외부에 있는 카메라를 통해 오디세우스와 지구가 함께 나오게 촬영했다. 지구를 배경으로 일종의 '셀카'를 찍은 셈이다.오디세우스는 지난 15일 발사됐다. 공중전화 부스 크기의 오디세우스는 미 항공우주국 관측장비 6개와 달 조형물 등 민간 물품 6개를 탑재하고 있다. NASA의 달 유인 기지 건설 프로젝트인 '아르테미스 미션'의 일부로 '민간 달 탑재체 수송 서비스'다.오디세우스는 오는 22일 달 착륙을 목표로 하고 있다. 달 착륙에 성공하면 민간 탐사선 최초로 달에 착륙하게 된다."""
okt = Okt()
komoran = Komoran()
print(okt.nouns(content))
print(komoran.nouns(content))
- Okt
- ['미국', '민간', '달', '착륙선', '오디세우스', '우주', '촬영', '사진', '지구', '미국', '우주', '기업', '인튜이티브', '머', '신스', '지난', '계정', '통해', '오디세우스', '우주', '촬영', '사진', '장', '공개', '인튜이티브', '머', '신스', '프로젝트', '첫', '사진', '성공', '전송', '며', '이', '사진', '스페이스', '의', '단', '추진', '체', '분리', '직후', '촬영', '것', '오디세우스', '장의', '사진', '외부', '카메라', '통해', '오디세우스', '지구', '촬영', '지구', '배경', '일종', '셀카', '를', '셈', '오디세우스', '지난', '발사', '공중전화', '부스', '크기', '오디세우스', '항공우주', '국', '관측', '장비', '개', '달', '조형', '물', '등', '민간', '물품', '개', '탑재', '의', '달', '유인', '기지', '건설', '프로젝트', '아르테미스', '미션', '의', '일부', '민간', '달', '탑재', '체', '수송', '서비스', '오디세우스', '달', '착륙', '목표', '달', '착륙', '민간', '탐사선', '최초', '달', '착륙']
- Komoran
- ['미국', '민간', '달', '착륙선', '오디세우스', '우주', '촬영', '사진', '지구', '미국', '우주', '기업', '머신', '일', '계정', '오디세우스', '우주', '촬영', '사진', '장', '공개', '머신', '일', '-1', '프로젝트', '사진', '성공', '전송', '사진', '스페이스X', '단', '추진체', '분리', '직후', '촬영', '것', '오디세우스', '장의', '사진', '외부', '카메라', '오디세우스', '지구', '촬영', '지구', '배경', '일종', '카', '셈', '오디세우스', '일', '발사', '공중전화', '부스', '크기', '오디세우스', '미', '항공우주국', '관측', '장비', '개', '달', '조형물', '등', '민간', '물품', '개', '탑재', '달', '유인', '기지', '건설', '프로젝트', '아르테미스', '미션', '일부', '민간', '달', '탑재체', '수송', '서비스', '오디세우스', '일', '달 착륙', '목표', '달 착륙', '성공', '민간', '탐사선', '최초', '달', '착륙']
- Komoran을 선택한 이유
- Okt의 결과를 보면 "추진체"가 "추진", "체"로 분리가 되고 "항공우주국"은 "항공우주", "국"으로 분리되는 것처럼 하나의 단어로 봐야 할 것을 분리하는 경향이 있다.
- Komoran은 "추친체", "스페이스X", "항공우주국"을 하나의 단어로 잘 판단하기 때문에 Komoran을 선택했다.
from konlpy.tag import Komoran
def getNouns(news_df):
komoran = Komoran()
nouns_list = []
for content in news_df["content"]:
nouns_list.append(komoran.nouns(content))
news_df["nouns"] = nouns_list
- Komoran의 nouns메서드로 명사를 추출 후 "nouns"열에 저장
3. 명사 벡터화
- 같은 카테고리 기사들의 명사를 하나의 리스트로 합침
- ['명사1 명사2 명사3 명사4 ...', '명사1 명사2 명사3 명사4 ...', ...]
- 하나의 요소는 하나의 문서이다. 즉 위 리스트는 여러 문서를 가진다.
- 합친 리스트를 TF-IDF 방식을 사용해 벡터화
- TF-IDF : 문서 집합에서 한 단어가 얼마나 중요한지를 수치적으로 나타낸 가중치 (TF 와 IDF의 곱)
- TF : 특정 단어가 하나의 문서 안에서 등장하는 빈도
- DF : 특정 단어가 등장하는 문서의 빈도
- IDF : DF에 역수를 취한 값
- 간단하게 하나의 문서에는 많이 등장하면서 여러 문서에는 적게 등장하는 단어가 큰 값을 가짐
from sklearn.feature_extraction.text import TfidfVectorizer
def getVector(news_df):
category_names = ["정치", "경제", "사회", "생활/문화", "세계", "IT/과학", "연예", "스포츠"]
vector_list = []
for i in range(8):
text = [" ".join(noun) for noun in news_df['nouns'][news_df['category'] == category_names[i]]] # 명사 열을 하나의 리스트에 담는다.
tfidf_vectorizer = TfidfVectorizer(min_df = 2, ngram_range=(1, 5))
tfidf_vectorizer.fit(text)
vector = tfidf_vectorizer.transform(text).toarray() # vector list 반환
vector = np.array(vector)
vector_list.append(vector)
return vector_list
- min_df : 특정 단어가 나타나는 최소 문서의 수 (특정 단어가 2개 이상의 문서에서 등장하지 않으면 제외)
- ngram_range : 단어의 묶음 ((1, 5)는 단어를 1개 ~ 5개의 묶음)
- ex) "a b c d e"라는 문서가 있으면 a, a b, a b c, a b c d, a b c d e 를 각각의 단어로 본다.
- vector_list에 8개의 벡터를 저장하고 반환
4. 군집화
- 카테고리마다 100개 정도의 기사를 크롤링 하기 때문에 비슷한 내용의 기사들이 다수 있음
- 내용이 비슷한 기사들을 하나의 요약 기사로 만들어내기 위해 그룹을 생성
- 군집화 알고리즘으로 DBSCAN을 선택
- DBSCAN은 군집의 개수를 정하지 않고 밀도를 기반으로 군집을 생성
- 불규칙한 데이터 또는 데이터에 대한 사전 예측이 어려울 때 활용성이 좋음
- 뉴스 기사는 위 조건에 적합하다 생각해서 DBSCAN 알고리즘을 선택했다.
from sklearn.cluster import DBSCAN
def addClusterNumber(news_df, vector_list):
cluster_number_list = []
for vector in vector_list:
model = DBSCAN(eps=0.2, min_samples=1, metric='cosine')
result = model.fit_predict(vector)
cluster_number_list.extend(result)
news_df['cluster_number'] = cluster_number_list # 군집 번호 칼럼 추가
- eps : 군집을 구성하는 최소의 거리 (이 거리 내의 포인트는 같은 군집으로 판단)
- min_samples : 군집을 구성하기 위한 최소 포인트 수
- metric : 거리(유사도) 측정 방법으로는 코사인 유사도를 사용
- eps는 낮추고 min_samples를 올릴수록 군집 내 데이터의 유사도가 높아질 것
5. 상위 군집 추출
- 군집 내 포인트가 많은 기사일수록 현재 이슈가 되는 기사라고 판단하고 카테고리마다 상위 10개 군집을 추출
def getClusteredNews(news_df):
category_names = ["정치", "경제", "사회", "생활/문화", "세계", "IT/과학", "연예", "스포츠"]
cluster_counts_df = pd.DataFrame(columns=["category", "cluster_number", "cluster_count"])
for i in range(8):
tmp = news_df[news_df['category'] == category_names[i]]['cluster_number'].value_counts().reset_index()
tmp.columns = ['cluster_number', 'cluster_count']
tmp['category'] = [category_names[i]] * len(tmp)
cluster_counts_df = pd.concat([cluster_counts_df, tmp])
# 상위 군집 10개씩만 추출
cluster_counts_df = cluster_counts_df[cluster_counts_df.index < 10]
return cluster_counts_df
- 군집이 잘 됐는지 확인
import pandas as pd
df = pd.DataFrame()
for i in range(len(cluster_counts_df)):
t = news_df[["category", "title", "cluster_number"]][(news_df["category"] == cluster_counts_df["category"].iloc[i]) & (news_df["cluster_number"] == cluster_counts_df["cluster_number"].iloc[i])]
df = df.append(t)
print(df)
- 같은 군집 번호를 가지는 기사끼리는 거의 같은 제목을 가지는 걸 볼 수 있다.
전체 코드
더보기
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import DBSCAN
import numpy as np
import pandas as pd
from konlpy.tag import Komoran
# 본문 명사 추출
def getNouns(news_df):
komoran = Komoran()
nouns_list = []
for content in news_df["content"]:
nouns_list.append(komoran.nouns(content))
news_df["nouns"] = nouns_list
# 명사 벡터화 (군집화에 사용)
def getVector(news_df):
category_names = ["정치", "경제", "사회", "생활/문화", "세계", "IT/과학", "연예", "스포츠"]
vector_list = []
for i in range(8):
text = [" ".join(noun) for noun in news_df['nouns'][news_df['category'] == category_names[i]]] # 명사 열을 하나의 리스트에 담는다.
tfidf_vectorizer = TfidfVectorizer(min_df = 2, ngram_range=(1, 5))
tfidf_vectorizer.fit(text)
vector = tfidf_vectorizer.transform(text).toarray() # vector list 반환
vector = np.array(vector)
vector_list.append(vector)
return vector_list
# 카테고리 별로 군집화, cluster_number 열에 군집 번호 생성
def addClusterNumber(news_df, vector_list):
cluster_number_list = []
for vector in vector_list:
model = DBSCAN(eps=0.2, min_samples=1, metric='cosine') # eps = Cluster를 구성하는 최소의 거리, min_samples = Cluster를 구성 시, 필요한 최소 데이터 포인트 수
result = model.fit_predict(vector)
cluster_number_list.extend(result)
news_df['cluster_number'] = cluster_number_list # 군집 번호 칼럼 추가
# 카테고리 별로 군집의 개수를 센다.
def getClusteredNews(news_df):
category_names = ["정치", "경제", "사회", "생활/문화", "세계", "IT/과학", "연예", "스포츠"]
cluster_counts_df = pd.DataFrame(columns=["category", "cluster_number", "cluster_count"])
for i in range(8):
tmp = news_df[news_df['category'] == category_names[i]]['cluster_number'].value_counts().reset_index()
tmp.columns = ['cluster_number', 'cluster_count']
tmp['category'] = [category_names[i]] * len(tmp)
cluster_counts_df = pd.concat([cluster_counts_df, tmp])
# 상위 군집 10개씩만 추출
cluster_counts_df = cluster_counts_df[cluster_counts_df.index < 10]
return cluster_counts_df
def startClustering(news_df):
getNouns(news_df)
vector_list = getVector(news_df)
addClusterNumber(news_df, vector_list)
cluster_counts_df = getClusteredNews(news_df)
return cluster_counts_df
'네이버 뉴스 요약 프로젝트' 카테고리의 다른 글
[네이버 뉴스 요약 프로젝트] 요약 (Summary) (0) | 2023.12.22 |
---|---|
[네이버 뉴스 요약 프로젝트] 크롤링 (Crawling) (0) | 2023.12.22 |
[네이버 뉴스 요약 프로젝트] (1) | 2023.12.22 |
Comments