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 |