본문 바로가기

공부방/프로젝트

[AI_Antenna] 중복된 뉴스 기사 제거하기, tf-idf

뉴스를 크롤링해서 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의 주요 매개변수

  1. min_df: 단어를 포함하기 위한 최소 문서 수
    • min_df=1: 단 한 번만 등장하는 단어도 포함
    • min_df=2: 최소 2개 문서에서 등장하는 단어만 포함
    • min_df=0.1: 전체 문서의 10% 이상에서 등장하는 단어만 포함
  2. max_df: 단어를 제외하기 위한 최대 문서 비율
    • max_df=0.9: 전체 문서의 90% 이상에서 등장하는 단어는 제외
    • 너무 흔한 단어(예: '있다', '하다' 등)를 제거하는데 유용
  3. 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분께 엑스 공식 계정을 통해 &quot;챗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개