데이터 분석/머신러닝, 딥러닝

Cosine 유사도

fullfish 2025. 11. 28. 14:04

1. 코사인 유사도 정의

코사인 유사도는 두 벡터의 **크기(길이)**가 아닌, 두 벡터가 이루는 각도의 코사인 값을 측정하여 유사도를 판단하는 지표입니다.

A. 텍스트에서의 활용

  • 텍스트가 TF-IDF와 같은 방식으로 벡터화되면, 각 문서는 여러 단어를 축으로 가지는 고차원 벡터가 됩니다.
  • 이때 코사인 유사도는 두 문서 벡터의 방향이 얼마나 일치하는지를 측정합니다.

B. 각도와 유사도의 관계

각도 (방향) 코사인 값 유사도 의미
작을수록 ($0^\circ$에 가까울수록) 1에 가까움 높음 ($\uparrow$) 두 문서의 내용과 주제가 매우 비슷함
$90^\circ$ (직각) 0 거의 없음 두 문서가 서로 독립적이거나 관련이 거의 없음
$180^\circ$ (반대 방향) -1 반대 관계 (텍스트에서는 흔치 않음)

C. 값의 범위

일반적으로 텍스트 데이터에서는 벡터의 모든 요소가 0 또는 양수이기 때문에 코사인 유사도의 범위는 0과 1 사이입니다.

  • 1에 가까울수록: 두 문서의 내용과 주제가 매우 비슷합니다.
  • 0에 가까울수록: 두 문서가 거의 관련이 없습니다.

 

import numpy as np
import pandas as pd
import re  # 정규 표현식 모듈 추가
from konlpy.tag import Okt  # 한국어 형태소 분석기 Okt 추가
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.model_selection import train_test_split  # 데이터 분할 모듈 추가
import random  # random 모듈 추가

# ----------------------------------------------------------------------
# 1. 한국어 텍스트 전처리 환경 설정
# ----------------------------------------------------------------------

# Okt 객체 초기화
okt = Okt()

# 전처리 함수에서 사용할 불용어 및 품사 정의 (예시)
# 만선님의 요청에 따라 'basic_stopwords' 대신 'stopwords' 변수명을 사용합니다.
stopwords = ["하다", "있다", "되다", "이다", "것", "그", "이", "가다", "오다"]
possible_pos = ["Noun", "Verb", "Adjective"]


# ----------------------------------------------------------------------
# 2. 사용자 정의 전처리 함수 (새로운 버전 - 토큰 리스트 반환)
# ----------------------------------------------------------------------


def preprocess(text):
    # NaN 값이 들어올 경우 처리 (string으로 변환)
    if pd.isna(text):
        text = ""

    # 1. 정규식 처리 및 소문자 변환 (TfidfVectorizer의 전처리 기능을 대체)
    # [주의] TfidfVectorizer의 preprocessor를 None으로 설정할 경우, 이 함수가 문자열을 처리해야 함
    str_reg = re.sub(r"[^가-힣0-9a-zA-Z\s]", "", text).lower()

    # 2. 형태소 분석 및 정규화/어간 추출 후 '명사/Noun' 형태의 문자열 리스트로 반환 (join=True)
    pos = okt.pos(str_reg, norm=True, stem=True, join=True)

    # 3. '단어/품사' 형태를 ['단어', '품사'] 형태로 분리
    pos = [word.split("/") for word in pos]

    # 4. 품사 필터링 및 불용어 필터링
    filtered_pos = [
        word
        for word, tag in pos
        # word가 비어있지 않고, 불용어가 아니며, 가능한 품사 목록에 포함될 때만 선택
        if word
        and word not in stopwords
        and tag in possible_pos  # basic_stopwords -> stopwords로 변경
    ]

    # 5. 최종 토큰들을 리스트 형태로 반환 (TfidfVectorizer의 tokenizer 입력 형태)
    return filtered_pos


# ----------------------------------------------------------------------
# 3. 데이터 로드 및 샘플링
# ----------------------------------------------------------------------

# movie_reviews.csv 읽어서 DataFrame (결측치 삭제)
df = pd.read_csv(
    "stat_nlp/movie_reviews.csv",
    header=0,
    names=["id", "review", "label"],
)

df = df.dropna()
df["label"] = df["label"].astype(int)

print(df.head())
print(df["label"].value_counts(normalize=True))

# train_test_split() 함수로 층화추출로 샘플 1000개 추출
_, df_sample = train_test_split(
    df,
    test_size=1000,
    stratify=df["label"],
    shuffle=True,
    random_state=42,
)

# docs_raw는 원본 리뷰 텍스트 리스트
# TfidfVectorizer의 입력은 가공되지 않은 텍스트 리스트로 변경
docs_raw = df_sample["review"].tolist()

# ----------------------------------------------------------------------
# 4. TF-IDF 벡터화 객체 설정 (tokenizer 사용)
# ----------------------------------------------------------------------

print("\n--- 한국어 텍스트 전처리 중 (TfidfVectorizer 내부 tokenizer 사용) ---")

# TfidfVectorizer의 입력은 전처리가 되지 않은 docs_raw (문자열 리스트)를 사용합니다.
# 내부적으로 TfidfVectorizer가 preprocess 함수를 호출하여 토큰화합니다.
tfidf = TfidfVectorizer(
    max_df=0.8,
    min_df=1,
    # 1. 토크나이저로 사용자 정의 preprocess 함수 지정 (토큰 리스트 반환)
    tokenizer=preprocess,
    # 2. TfidfVectorizer의 기본 전처리(소문자화, 구두점 제거 등) 비활성화
    #    -> 이 작업은 이제 preprocess 함수 내부에서 처리됩니다.
    preprocessor=None,
    # 3. 기본 토큰 패턴(공백 기준) 비활성화 (tokenizer가 이 역할을 수행)
    token_pattern=None,
)

# 5. 문서-단어 행렬 X 생성
# X의 입력은 전처리가 안 된 docs_raw (문자열 리스트)입니다.
X = tfidf.fit_transform(docs_raw)

print("\nTF-IDF 행렬 크기:", X.shape)

# 6. 전체 문서 간 코사인 유사도 계산
cos_sim = cosine_similarity(X, X)

# 7. 결과를 DataFrame으로 만들어 보기 쉽게 출력 (상위 5x5 샘플)
cos_df = pd.DataFrame(
    cos_sim[:5, :5],
    columns=[f"d{i}" for i in range(5)],
    index=[f"d{i}" for i in range(5)],
)

print("\n코사인 유사도 행렬 (상위 5x5 샘플):")
print(cos_df.round(2))

# ----------------------------------------------------------------------
# 8. 랜덤 10개 문서에 대해 각각 Top K 유사 문서 추출 (수정된 로직)
# ----------------------------------------------------------------------

N_SAMPLES = 10  # 랜덤하게 뽑을 기준 문서의 개수
TOP_K = 5  # 각 기준 문서별로 뽑을 유사 문서의 개수

# 1000개 문서 중 랜덤하게 10개의 인덱스를 추출
random_indices = random.sample(range(len(docs_raw)), N_SAMPLES)

print(
    f"\n--- 랜덤 추출된 {N_SAMPLES}개 문서에 대해 각각 Top {TOP_K} 유사 문서 검색 ---"
)

# 추출된 랜덤 인덱스를 순회하며 출력
for k, i in enumerate(random_indices):

    # 1. 기준 문서 i의 유사도 벡터를 가져옵니다.
    similarities = cos_sim[i]

    # 2. 자기 자신과의 유사도(1.0)를 -1.0으로 설정하여 제외합니다.
    #    이렇게 해야 정렬 시 자기 자신이 Top에 오지 않습니다.
    similarities[i] = -1.0

    # 3. 유사도 배열을 내림차순으로 정렬한 후, TOP_K개에 해당하는 인덱스를 추출합니다.
    #    np.argsort(-similarities)를 통해 내림차순 정렬 인덱스를 얻습니다.
    top_indices = np.argsort(-similarities)[:TOP_K]

    print(f"\n\n================ [기준 문서 {k+1} (인덱스 {i})] ================")
    print(docs_raw[i])
    print("----------------------------------------------------------------")

    # 4. 추출된 TOP_K 인덱스를 순회하며 결과 출력
    for rank, sim_idx in enumerate(top_indices):
        sim_score = similarities[sim_idx]

        # 원본 docs_raw[i]를 직접 비교에 사용
        print(f"  [Top {rank+1}] 유사도: {sim_score:.4f} (인덱스 {sim_idx})")
        print(f"  {docs_raw[sim_idx]}")

'데이터 분석 > 머신러닝, 딥러닝' 카테고리의 다른 글

딥러닝 텍스트 전처리  (0) 2025.12.01
K-means Document Clustering  (0) 2025.11.28
RNN(Recurrent Neural Networks)  (0) 2025.11.27
CNN(Convolutional neural network)  (0) 2025.11.26
LDA (Latent Dirichlet Allocation)  (0) 2025.11.26