{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "private_outputs": true, "provenance": [] }, "kernelspec": { "name": "python3", "display_name": "Python 3" }, "language_info": { "name": "python" } }, "cells": [ { "cell_type": "markdown", "source": [ "# 유사한 문서 검색: SBERT, FAISS 활용" ], "metadata": { "id": "AnAXz868UOUf" } }, { "cell_type": "markdown", "source": [ "### SBERT:\n", "- BERT를 기본 모델로 사용하지만, 문장 임베딩 생성을 위해 최적화\n", "- Mean Pooling 벡터 사용: 분류기 개발용 BERT 활용에서는 CLS(Classification) 토큰 벡터 사용\n", "- 유사성 측정에 적합: 코사인 유사도(cosine similarity)나 유클리드 거리(Euclidean distance) 등을 사용\n", "\n", "### FAISS(Facebook AI Similarity Search)\n", "- 대규모 벡터 검색 및 클러스터링을 위한 라이브러리. 주로 고차원 벡터의 빠른 유사성 검색을 수행" ], "metadata": { "id": "PUQ1irpLwCrw" } }, { "cell_type": "code", "source": [ "!pip install faiss-gpu\n", "!pip install transformers\n", "!pip install sentence-transformers\n" ], "metadata": { "id": "0XJvL3bbOYi8" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## 검색1: 전체 데이터에서 거리 가까운 문장 사례 찾기(유클리드거리, L2)" ], "metadata": { "id": "gkuuQhc854ib" } }, { "cell_type": "code", "source": [ "import numpy as np\n", "import faiss\n", "import pickle\n", "from sentence_transformers import SentenceTransformer\n", "import pandas as pd\n", "\n", "# 검색 대상 DataFrame 로드\n", "df = pd.read_excel('직장괴롭힘_071424.xlsx') #\n", "df.info()\n", "\n", "# SentenceTransformer 모델 로드\n", "model = SentenceTransformer('bongsoo/kpf-sbert-v1') # 768차원\n", "\n", "# 문장 임베딩\n", "embeddings = model.encode(df['사례'].tolist())\n", "embeddings.shape\n", "\n", "# (필요하면) embeddings 저장\n", "with open('embeddings_직장괴롭힘071424_kpfsbert.pkl', 'wb') as f:\n", " pickle.dump(embeddings, f)\n", "\n", "# (필요하면) embeddings 불러오기\n", "with open('embeddings_직장괴롭힘071424_kpfsbert.pkl', 'rb') as f:\n", " embeddings = pickle.load(f)\n", "\n", "# FAISS 인덱스 생성\n", "index = faiss.IndexFlatL2(embeddings.shape[1])\n", "index.add(embeddings)\n", "\n", "# 찾고자 하는 문장\n", "query_sentence = '''회사 과장님이 따로 불러서 \"일을 못한다\"며 야단치고 욕설을 했어요. '''\n", "\n", "# 찾고자 하는 문장의 임베딩\n", "query_embedding = model.encode([query_sentence])\n", "\n", "# 유사도 검색\n", "k = 5 # 상위 10개 문장\n", "distances, indices = index.search(query_embedding, k)\n", "\n", "# 유사한 문장을 데이터프레임으로 출력\n", "df_result=df[df['id'].isin(indices[0].tolist())]\n", "df_result['거리']=distances[0].tolist()\n", "df_result = df_result.sort_values(by='거리')\n", "print(df_result)\n" ], "metadata": { "id": "iUJOilG8P1el" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## 검색2: 전체 데이터에서 거리 가까운 문장 사례 찾기(코사인유사도)" ], "metadata": { "id": "EX46NJbGRbda" } }, { "cell_type": "markdown", "source": [ "\n", "\n", "![image.png](data:image/png;base64,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)\n", "\n", "코사인 유사도의 값은 -1에서 1 사이에 위치(1에 가까울수록 유사하고, -1에 가까울수록 다름)" ], "metadata": { "id": "8erIjqhm0LZg" } }, { "cell_type": "code", "source": [ "import numpy as np\n", "import faiss\n", "import pickle\n", "from sentence_transformers import SentenceTransformer\n", "import pandas as pd\n", "\n", "# 검색 대상 DataFrame 로드\n", "df = pd.read_excel('직장괴롭힘_071424.xlsx')\n", "df.info()\n", "\n", "# SentenceTransformer 모델 로드\n", "model = SentenceTransformer('bongsoo/kpf-sbert-v1') # 768차원\n", "\n", "# 문장 임베딩\n", "embeddings = model.encode(df['사례'].tolist())\n", "\n", "# 코사인 유사도 계산을 위한 임베딩 정규화(normalization): L2 정규화된 벡터의 코사인 유사도 = 두 정규화된 벡터 간의 내적\n", "embeddings_norm = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)\n", "embeddings_norm.shape\n", "\n", "# (필요하면) embeddings 저장\n", "with open('embeddingsNorm_직장괴롭힘071424_kpfsbert.pkl', 'wb') as f:\n", " pickle.dump(embeddings_norm, f)\n", "\n", "# (필요하면) embeddings 불러오기\n", "with open('embeddingsNorm_직장괴롭힘071424_kpfsbert.pkl', 'rb') as f:\n", " embeddings_norm = pickle.load(f)\n", "\n", "# FAISS index 생성: 내적(inner product)으로 검색 목적\n", "d = embeddings_norm.shape[1] # 임베딩의 차원 수를 저장: embeddings_norm은 (문장 수, 차원 수) 형태의 2차원 배열이므로, shape[1]은 임베딩의 차원 수를 나타냄\n", "index = faiss.IndexFlatIP(d) # 내적(IP, Inner Product) 검색을 위한 FAISS 인덱스 생성. IndexFlatIP는 내적 기반의 유사도 검색을 위한 인덱스를 의미\n", "index.add(embeddings_norm) # 정규화된 임베딩을 인덱스에 추가. 검색을 위해 미리 계산된 모든 문장 임베딩을 인덱스에 추가하여 검색이 가능하도록 함\n", "\n", "\n", "# 찾고자 하는 문장\n", "query_sentence = '''과장님이 \"일처리 잘해\"라고 야단치며 부족한 보고서 수정하라고 했다.'''\n", "query_sentence = '''과장님이 \"이것도 이해 못하냐 바보냐\"라고 욕을 하며 보고서를 던졌다.'''\n", "\n", "# 찾고자 하는 문장의 임베딩\n", "query_embedding = model.encode([query_sentence])\n", "\n", "# Normalize the query embedding\n", "query_embedding_norm = query_embedding / np.linalg.norm(query_embedding, axis=1, keepdims=True)\n", "\n", "# 유사도 검색\n", "k = 5 # 상위 5개 문장\n", "distances, indices = index.search(query_embedding_norm, k)\n", "cosine_similarities = distances[0]\n", "\n", "# 유사한 문장을 데이터프레임으로 출력\n", "df_result = df.iloc[indices[0]].copy()\n", "df_result['유사도'] = cosine_similarities\n", "# Sort the results by similarity in descending order\n", "df_result = df_result.sort_values(by='유사도', ascending=False)\n", "df_result" ], "metadata": { "id": "_CFk98LSPI0z" }, "execution_count": null, "outputs": [] } ] }