뉴스를 크롤링해서 csv파일로 저장해놓았을 때, 중복된 기사들을 제거해보자!
어떻게?
일단 가장 간단하게(?) 구현해 볼 수 있는 TF-IDF와 코사인 유사도를 사용하여 기사들 사이의 유사도를 계산하는 방법.
1. 제목과 요약문을 합친 새로운 칼럼을 만든다.
뉴스 1000개를 크롤링 한 csv 파일을 불러와서 일단 샘플로 나눈다.
sample_df = df.iloc[0:10]
sample_df['combined_text'] = sample_df['title'] + ' ' + sample_df['description']
sample_df['combined_text'][0]을 출력하면 다음과 같이 나온다.
오픈AI 챗GPT·소라, 먹통 6시간 만에 정상화(종합) 오픈AI 생성형 인공지능(AI) 서비스 '챗GPT', '소라' 등이 접속 장애 발생 약 6시간 만에 정상화됐다. 오픈AI는 한국시각으로 12일 오후 2시2분께 엑스 공식 계정을 통해 "챗GPT, API, 소라가 오늘 다운됐지만 현재 복구됐다...
2. 새로 만든 칼럼의 각 행을 TF-IDF 벡터로 변환한다. = 이 행이 하나의 뉴스 기사이므로, 이 기사에 들어있는 단어가 그 기사에서 얼마나 중요한지에 따라 숫자가 매겨짐. 자주 등장하지만 다른 기사에도 많이 나오는 단어(예: "기자", "뉴스")는 낮은 점수 / 자주 등장하면서 다른 기사에는 별로 없는 단어(예: 특정 회사명)는 높은 점수
# TF-IDF 벡터화
vectorizer = TfidfVectorizer(min_df=1)
tfidf_matrix = vectorizer.fit_transform(sample_df['combined_text'])
TfidfVectorizer의 주요 매개변수
- min_df: 단어를 포함하기 위한 최소 문서 수
- min_df=1: 단 한 번만 등장하는 단어도 포함
- min_df=2: 최소 2개 문서에서 등장하는 단어만 포함
- min_df=0.1: 전체 문서의 10% 이상에서 등장하는 단어만 포함
- max_df: 단어를 제외하기 위한 최대 문서 비율
- max_df=0.9: 전체 문서의 90% 이상에서 등장하는 단어는 제외
- 너무 흔한 단어(예: '있다', '하다' 등)를 제거하는데 유용
- ngram_range: 단어 조합의 범위
- (1,1): 단일 단어만 사용
- (1,2): 단일 단어와 두 단어 조합 모두 사용
- 예: "삼성전자" + "스마트폰" -> "삼성전자 스마트폰"도 하나의 특성으로 포함
여기서 min_df = n 의 의미를 최소 "n개의 문서에서 등장하는 단어"를 포함시킨다는 뜻이다. 예를 들어서, 다음과 같은 뉴스들이 있다고 했을 때,
뉴스1: "삼성전자 신제품 스마트폰 공개"
뉴스2: "삼성전자 반도체 실적 발표"
뉴스3: "LG전자 로봇청소기 출시"
# min_df=2로 설정 (최소 2개 문서에서 등장하는 단어만 포함)
vectorizer2 = TfidfVectorizer(min_df=2)
"""
"삼성전자": 2개 문서에 등장 → 포함됨
"신제품": 1개 문서에만 등장 → 제외됨
"로봇청소기": 1개 문서에만 등장 → 제외됨
"""
다시 돌아와서, sample_df['combined_text'][0]가 어떻게 TF-IDF 벡터로 변환됐는지 확인해보자. tfidf matrix를 출력해보면, 이런 식으로 나온다. 여기서 괄호 안에 숫자는 (0, 90) 0은 첫 번째 문서(기사)를 의미하고, 90은 단어 사전에서 해당 단어의 인덱스를 뜻한다. 그리고 그 옆에 있는 0.14553... 가 단어의 TF-IDF 점수를 말한다. (점수가 높을수록 그 단어가 문서에서 중요하다는 의미...?)

인덱스에 해당하는 단어가 뭔지 알고 싶다면 이렇게 확인할 수 있다.
print(vectorizer.get_feature_names_out()[41])
#계정을
좀더 보기 좋게 해보면, sample_df['combined_text'][0]에 있던 모든 단어와 그 단어가 해당 문서에서 얼마나 중요한지 숫자로 나타나있다. 점수가 높은 단어는, 해당 문서에서 자주 등장하면서 다른 문서들에서는 덜 등장하는 단어들을 의미한다.
단어: 챗gpt | 인덱스: 184 | TF-IDF 점수: 0.3712 이 단어가 점수가 가장 높다.
vocabulary = vectorizer.vocabulary_
reverse_vocab = {v: k for k, v in vocabulary.items()}
first_doc = tfidf_matrix[0]
print(sample_df['combined_text'][0],"\n")
print("첫 번째 문서의 단어별 TF-IDF 점수:\n",)
for idx, score in zip(first_doc.indices, first_doc.data):
word = reverse_vocab[idx]
print(f"단어: {word:<10} | 인덱스: {idx:>3} | TF-IDF 점수: {score:.4f}")
"""
오픈AI 챗GPT·소라, 먹통 6시간 만에 정상화(종합) 오픈AI 생성형 인공지능(AI) 서비스 '챗GPT', '소라' 등이 접속 장애 발생 약 6시간 만에 정상화됐다. 오픈AI는 한국시각으로 12일 오후 2시2분께 엑스 공식 계정을 통해 "챗GPT, API, 소라가 오늘 다운됐지만 현재 복구됐다...
첫 번째 문서의 단어별 TF-IDF 점수:
단어: 복구됐다 | 인덱스: 90 | TF-IDF 점수: 0.1455
단어: 현재 | 인덱스: 220 | TF-IDF 점수: 0.1455
단어: 다운됐지만 | 인덱스: 63 | TF-IDF 점수: 0.1455
단어: 오늘 | 인덱스: 147 | TF-IDF 점수: 0.1455
단어: 소라가 | 인덱스: 117 | TF-IDF 점수: 0.1455
단어: api | 인덱스: 18 | TF-IDF 점수: 0.1455
단어: quot | 인덱스: 30 | TF-IDF 점수: 0.1237
단어: 통해 | 인덱스: 194 | TF-IDF 점수: 0.1455
단어: 계정을 | 인덱스: 41 | TF-IDF 점수: 0.1455
단어: 공식 | 인덱스: 48 | TF-IDF 점수: 0.1455
단어: 엑스 | 인덱스: 140 | TF-IDF 점수: 0.1455
단어: 2시2분께 | 인덱스: 9 | TF-IDF 점수: 0.1455
단어: 오후 | 인덱스: 151 | TF-IDF 점수: 0.1237
단어: 12일 | 인덱스: 3 | TF-IDF 점수: 0.0709
단어: 한국시각으로 | 인덱스: 211 | TF-IDF 점수: 0.1455
단어: 오픈ai는 | 인덱스: 150 | TF-IDF 점수: 0.1455
단어: 정상화됐다 | 인덱스: 175 | TF-IDF 점수: 0.1455
단어: 발생 | 인덱스: 86 | TF-IDF 점수: 0.1455
단어: 장애 | 인덱스: 162 | TF-IDF 점수: 0.1455
단어: 접속 | 인덱스: 171 | TF-IDF 점수: 0.1455
단어: 등이 | 인덱스: 74 | TF-IDF 점수: 0.1455
단어: 서비스 | 인덱스: 109 | TF-IDF 점수: 0.1237
단어: ai | 인덱스: 16 | TF-IDF 점수: 0.0589
단어: 인공지능 | 인덱스: 157 | TF-IDF 점수: 0.0538
단어: 생성형 | 인덱스: 107 | TF-IDF 점수: 0.1455
단어: 종합 | 인덱스: 179 | TF-IDF 점수: 0.1455
단어: 정상화 | 인덱스: 174 | TF-IDF 점수: 0.1455
단어: 만에 | 인덱스: 77 | TF-IDF 점수: 0.2911
단어: 6시간 | 인덱스: 15 | TF-IDF 점수: 0.2911
단어: 먹통 | 인덱스: 79 | TF-IDF 점수: 0.1455
단어: 소라 | 인덱스: 116 | TF-IDF 점수: 0.2911
단어: 챗gpt | 인덱스: 184 | TF-IDF 점수: 0.3712
단어: 오픈ai | 인덱스: 149 | TF-IDF 점수: 0.2911
"""
3. 기사들의 코사인 유사도를 계산한다. = 모든 기사들 사이의 쌍을 지었을 때 얼마나 유사한지를 계산한다. 비슷한 단어를 비슷한 비중으로 포함하는 기사들은 높은 유사도를 갖게 된다.
similarity_matrix = cosine_similarity(tfidf_matrix)
tfidf 매트릭스를 가지고 모든 기사 쌍 사이의 코사인 유사도를 계산한다. 아까 그 문장을 가지고 코사인 유사도를 계산하면, (지금 샘플에는 총 10개의 기사가 있기 때문에 10개의 기사와의 유사도가 숫자로 나온다.
similarity_matrix[0]
"""
array([1. , 0.08848655, 0.01109348, 0.03947293, 0.01429879,
0.00751456, 0.01734967, 0.01551088, 0.04917625, 0.01652935])
"""
코사인 유사도는 두 벡터 간의 각도를 기반으로 계산되고(??)
비슷한 단어를 비슷한 비중으로 포함하는 기사들은 높은 유사도를 갖는다.
이 유사도를 0.8 이상을 가지면 유사한 걸로 판단하려고 했는데, 0.8은 무슨... 제일 높은 게 0.08이네..
그래서 임계치를 0.03으로 지정하고 걸러내는 걸로 해봤더니, 10개 안에서 중복됐던 기사들 제거가 많이 됐다!
similarity_threshold = 0.03
duplicate_groups = []
processed = set()
for i in range(len(df)):
if i not in processed:
similar_indices = set([i])
# 유사한 기사 찾기
for j in range(i + 1, len(sample_df)):
if j not in processed and similarity_matrix[i, j] > similarity_threshold:
similar_indices.add(j)
if len(similar_indices) > 1:
duplicate_groups.append(similar_indices)
processed.update(similar_indices)
keep_indices = []
duplicate_counts = np.zeros(len(sample_df))
for group in duplicate_groups:
keep_idx = min(group)
keep_indices.append(keep_idx)
duplicate_counts[keep_idx] = len(group) - 1
for i in range(len(sample_df)):
if i not in processed:
keep_indices.append(i)
result_df = sample_df.iloc[keep_indices].copy()
result_df['duplicate_count'] = duplicate_counts[keep_indices]
1000개의 뉴스 기사를 가지고 임계값 0.5를 줬을때, 679개의 기사가 추려졌고,
0.3 -> 576개,
0.1 -> 373개
0.05 -> 164개가 추려졌다. 0.05정도면 대충 추려지는 듯!
0.03 -> 76개
0.01 -> 28개
'공부방 > 프로젝트' 카테고리의 다른 글
[AI안테나] LLM 뉴스 요약, solar pro와 gpt-4o-mini 비교 (1) | 2025.01.08 |
---|---|
[AI안테나] 두 번째 샘플 (0) | 2025.01.08 |
[AI안테나] 첫 번째 샘플 (0) | 2025.01.07 |
[AI_Antenna] 네이버 뉴스 크롤링 (1) | 2024.10.26 |
[AI_Antenna] 아이디어 정리 (3) | 2024.10.25 |