{ "cells": [ { "cell_type": "markdown", "id": "90b264de", "metadata": {}, "source": [ "---\n", "downloads: []\n", "---" ] }, { "cell_type": "markdown", "id": "34eb4d6a", "metadata": {}, "source": [ "# RAG\n", "\n", "검색 보강 생성 (Retrieval-Augmented Generation), 또는 RAG는 외부 문서를 검색하여 그 결과를 바탕으로 답변을 생성하는 방식입니다.\n", "기존의 생성형 언어 모델은 학습된 지식만을 기반으로 답변을 생성하지만, RAG는 질문과 관련된 정보를 문서에서 찾아 활용함으로써 더 정확하고 신뢰도 높은 응답을 생성할 수 있습니다.\n", "\n", "RAG는 크게 두 가지 구성 요소로 이루어져 있습니다:\n", "\n", "1. 검색기(retriever)는 사용자의 질문과 관련된 문서를 외부에서 검색하는 역할을 합니다.\n", "2. 생성기(generator)는 검색된 문서를 바탕으로 자연어 형태의 답변을 생성합니다.\n", "\n", "이 구조는 정보 검색과 텍스트 생성을 결합한 형태로, 단순한 질의응답을 넘어 다양한 분야에서 응용될 수 있습니다.\n", "예를 들어, 도메인 특화 질문응답 시스템, 문서 요약, 실시간 지식 기반 챗봇 등에 활용할 수 있습니다.\n", "\n", "RAG는 Facebook AI에서 제안한 방식이며, Hugging Face Transformers 라이브러리를 통해 쉽게 구현할 수 있습니다. \n", "\n", "한국어 환경에서는 KoSBERT, KoT5, KoGPT 등의 사전학습 언어 모델과 함께 사용할 수 있습니다." ] }, { "cell_type": "markdown", "id": "d2b99bc4", "metadata": {}, "source": [ "문서 검색기(retriever)는 사용자의 질문에 대해 관련 문서를 외부 데이터에서 찾아주는 구성 요소입니다. RAG에서의 첫 단계이며, 문서 기반 질문 응답 시스템의 핵심입니다.\n", "\n", "retriever는 주로 다음 단계를 포함합니다:\n", "\n", "1. 질문을 임베딩 벡터로 변환\n", "2. 미리 임베딩된 문서 벡터들과 비교\n", "3. 가장 유사한 문서 n개 반환" ] }, { "cell_type": "markdown", "id": "8cf10248", "metadata": {}, "source": [ "```sh\n", "pip install sentence-transformers faiss-cpu\n", "```" ] }, { "cell_type": "code", "execution_count": 4, "id": "3727757e", "metadata": {}, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_id = 'codebasic/aihub-koen-translation-integrated-base-1m'\n", "한영쌍 = load_dataset(dataset_id)" ] }, { "cell_type": "code", "execution_count": 6, "id": "0b1b04bb", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "\n", "특허데이터 = 한영쌍.filter(lambda x: x['source'] in [563]).remove_columns(['en', 'source'])\n", "특허데이터 = 특허데이터.rename_column('ko', 'text')\n", "pd.DataFrame(특허데이터['train'].shuffle().take(5))" ] }, { "cell_type": "markdown", "id": "cb4c05ed", "metadata": {}, "source": [ "## 문서 임베딩" ] }, { "cell_type": "code", "execution_count": 19, "id": "9f85d1a3", "metadata": {}, "outputs": [], "source": [ "from sentence_transformers import SentenceTransformer\n", "\n", "# 한국어 SBERT 모델\n", "문서인코더 = SentenceTransformer(\"snunlp/KR-SBERT-V40K-klueNLI-augSTS\")" ] }, { "cell_type": "code", "execution_count": 20, "id": "6623eccf", "metadata": {}, "outputs": [], "source": [ "문서임베딩 = 문서인코더.encode(특허데이터['train'][\"text\"], convert_to_numpy=True)" ] }, { "cell_type": "markdown", "id": "770a9826", "metadata": {}, "source": [ "## 검색용 색인 생성" ] }, { "cell_type": "code", "execution_count": 16, "id": "cfdb2c8c", "metadata": {}, "outputs": [], "source": [ "import faiss\n", "\n", "dimension = 문서임베딩.shape[1]\n", "index = faiss.IndexFlatL2(dimension)\n", "index.add(문서임베딩)\n", "\n", "faiss.write_index(index, \"patent_index.faiss\")" ] }, { "cell_type": "markdown", "id": "f3b90e17", "metadata": {}, "source": [ "## 검색 수행" ] }, { "cell_type": "code", "execution_count": 17, "id": "da54acc2", "metadata": {}, "outputs": [], "source": [ "# 사용자 질문\n", "query = \"컴퓨터 관련 특허\"\n", "query_embedding = 문서인코더.encode([query], convert_to_numpy=True)\n", "\n", "# 유사 문서 검색 (상위 2개)\n", "index = faiss.read_index(\"patent_index.faiss\")\n", "D, I = index.search(query_embedding, k=5)\n", "\n", "# 검색된 문서 출력\n", "for i in I[0]:\n", " print(특허데이터['train'][\"text\"][i])" ] }, { "cell_type": "markdown", "id": "cc4f4d8b", "metadata": {}, "source": [ "## 응용" ] }, { "cell_type": "code", "execution_count": 26, "id": "06210ed1", "metadata": {}, "outputs": [], "source": [ "from sentence_transformers import SentenceTransformer\n", "\n", "# 한국어 SBERT 모델\n", "문서인코더 = SentenceTransformer(\"snunlp/KR-SBERT-V40K-klueNLI-augSTS\")\n", "\n", "# 예시 질문\n", "question = \"컴퓨터 관련 특허를 획득하기 위해서는 어떤 절차가 필요한가요?\"\n", "\n", "# 질문 임베딩 및 검색\n", "question_embedding = 문서인코더.encode([question], convert_to_numpy=True)\n", "D, I = index.search(question_embedding, k=2)\n", "\n", "# 검색된 문서 추출\n", "retrieved_docs = [특허데이터['train']['text'][i] for i in I[0]]\n", "context = \"\\n\".join(retrieved_docs)\n", "print(f'검색된 문서:\\n{context}')" ] }, { "cell_type": "markdown", "id": "rag-context-optimization-md1", "metadata": {}, "source": [ "## 고급 콘텍스트 최적화 (Advanced Context Optimization)\n", "\n", "(rag-context-optimization)=\n", "\n", "검색기(Retriever)로부터 가져온 다량의 문서를 가공 없이 프롬프트에 그대로 결합하면, 불필요한 VRAM 소모가 늘어나고 정보 유실 현상인 **Lost in the Middle** 문제가 유발될 수 있습니다. 이를 방지하기 위한 콘텍스트 엔지니어링 기법 2가지를 알아봅니다.\n", "\n", "### 1. 맥락 재배치 (Long-Context Re-ordering)\n", "\n", "어텐션 연산 특성상 모델은 입력된 컨텍스트의 **처음**과 **끝** 부분에 강하게 주의를 기울이고, **중간** 영역의 정보는 쉽게 누락시킵니다. 따라서 검색 유사도가 가장 높은 핵심 문서 조각(Chunk)들을 프롬프트의 맨 앞과 맨 뒤에 배치하여 유실을 피하는 것이 정석입니다.\n", "\n", "아래는 상위 스코어 문서를 가장자리로 분산 배치해 주는 재배치 함수입니다." ] }, { "cell_type": "code", "execution_count": null, "id": "rag-context-optimization-code1", "metadata": {}, "outputs": [], "source": [ "def reorder_documents(docs):\n", " \"\"\"\n", " 유사도 점수가 높은 핵심 문서를 프롬프트의 양끝(처음과 끝)으로 재정렬하여\n", " Lost in the Middle 현상을 극복하도록 정렬합니다.\n", " \"\"\"\n", " sorted_docs = []\n", " # 홀수 인덱스는 왼쪽에 쌓고, 짝수 인덱스는 오른쪽에 쌓는 방식으로 분산 정렬\n", " left = True\n", " for doc in docs:\n", " if left:\n", " sorted_docs.insert(0, doc)\n", " else:\n", " sorted_docs.append(doc)\n", " left = not left\n", " return sorted_docs\n", "\n", "# 재배치 적용\n", "reordered_docs = reorder_documents(retrieved_docs)\n", "print(\"[원본 검색 순서 (상위 -> 하위)]\")\n", "for idx, doc in enumerate(retrieved_docs):\n", " print(f\"{idx+1}: {doc[:60]}...\")\n", "\n", "print(\"\\n[재배치 정렬 순서 (상위 문서를 처음과 끝으로 배치)]\")\n", "for idx, doc in enumerate(reordered_docs):\n", " print(f\"{idx+1}: {doc[:60]}...\")" ] }, { "cell_type": "markdown", "id": "rag-context-optimization-md2", "metadata": {}, "source": [ "### 2. 콘텍스트 압축 (Context Compression)\n", "\n", "필수적이지 않은 정보(조사, 중복 어휘 등)가 너무 많으면 프롬프트 공간을 낭비하게 됩니다. 소형 언어 모델이나 정보량 엔트로피 기반의 압축 기술(예: LLMLingua)을 사용하면 프롬프트의 핵심 의미를 보존하면서도 불필요한 단어를 지워낼 수 있습니다.\n", "\n", "아래는 텍스트 내에서 문법적으로 불필요한 중복 조사를 필터링하여 프롬프트 토큰을 절감하는 예시형 압축 헬퍼 함수입니다." ] }, { "cell_type": "code", "execution_count": null, "id": "rag-context-optimization-code2", "metadata": {}, "outputs": [], "source": [ "import re\n", "\n", "def simple_compress_context(text):\n", " \"\"\"\n", " 정규식을 이용해 중복 단어와 조사를 제거하는 가벼운 휴리스틱 콘텍스트 압축 헬퍼 함수입니다.\n", " 운영 환경에서는 LLMLingua 등 정보 엔트로피 기반의 소형 모델을 사용해 5~10배 수준까지 압축합니다.\n", " \"\"\"\n", " # 중복 공백 제거\n", " compressed = re.sub(r'\\s+', ' ', text)\n", " # 한글 조사 및 중복된 문맥적 특이점 정제\n", " compressed = compressed.replace(\"본 발명은\", \"[발명]\")\n", " compressed = compressed.replace(\"에 관한 것이다.\", \"\")\n", " return compressed.strip()\n", "\n", "raw_context = \"\\n\".join(reordered_docs)\n", "compressed_context = simple_compress_context(raw_context)\n", "\n", "print(f\"원본 콘텍스트 크기: {len(raw_context)} 자\")\n", "print(f\"압축된 콘텍스트 크기: {len(compressed_context)} 자 (절감률: {(1 - len(compressed_context)/len(raw_context))*100:.1f}%)\")\n", "\n", "# 다음 생성 셀에서 압축 및 재배치된 콘텍스트를 사용하도록 설정\n", "context = compressed_context" ] }, { "cell_type": "code", "execution_count": 27, "id": "c88f208e", "metadata": {}, "outputs": [], "source": [ "from transformers import PreTrainedTokenizerFast, GPT2LMHeadModel\n", "\n", "tokenizer = PreTrainedTokenizerFast.from_pretrained(\"skt/kogpt2-base-v2\")\n", "model = GPT2LMHeadModel.from_pretrained(\"skt/kogpt2-base-v2\")\n", "\n", "# 질문 + 검색 문서 기반 생성 입력\n", "prompt = f\"{context}\\n\\n질문: {question}\\n답변:\"\n", "\n", "input_ids = tokenizer.encode(prompt, return_tensors=\"pt\")\n", "output = model.generate(input_ids, max_new_tokens=256, do_sample=True, top_p=0.95, top_k=50)\n", "\n", "answer = tokenizer.decode(output[0], skip_special_tokens=True)\n", "print(\"생성된 답변:\\n\", answer)" ] } ], "metadata": { "kernelspec": { "display_name": "PyTorch 2", "language": "python", "name": "pytorch" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.18" }, "downloads": [] }, "nbformat": 4, "nbformat_minor": 5 }