{ "cells": [ { "cell_type": "markdown", "id": "ae1b13cb", "metadata": {}, "source": [ "\n", "" ] }, { "cell_type": "markdown", "id": "361f9db3", "metadata": {}, "source": [ "# Retrieval-Augmented Generation (RAG)\n", "\n", "## RAG på norsk: Gjenfinningsforsterket tekstgenerering\n", "\n", "Gjenfinningsforsterket tekstgenerering, Retrieval-Augmentes Generation, eller RAG er en måte å inkludere (deler av) dokumenter for å gi kontekst til spørsmål som man stiller en språkmodell. Dette kan redusere tendensen til hallusinering eller andre feil i svarene. Et system for RAG har to hoveddeler. For det første en dokumentdatabase med søkeindeks og for det andre en stor språkmodell. Fuguren under viser RAG programmets struktur.\n", "\n", "```{figure} ../rag_process.png\n", ":name: rag-process-figure\n", ":align: center\n", "\n", "Oversikt over RAG-prosessen.\n", "```\n", "\n", "Bilde fra [Retrieval-Augmented Generation](https://uio-library.github.io/LLM-course/4_RAG.html).\n", "\n", "Når brukeren stiller et spørsmål, vil det bli håndtert i to steg. Først blir det brukt til et søk i dokumentdatabasen. Søkeresultatene blir sendt sammen med spørsmålet til språkmodellen. Språkmodellen blir bedt om å svare på spørsmålene basert på konteksten i søkeresultatene.\n", "\n", "Vi vil bruke [LangChain](https://www.langchain.com/), et bibliotek med åpen kildekode, som brukes til å lage programmer med store språkmodeller. Dette kapittelet er inspirert av artikkelen [Retrieval-Augmented Generation (RAG) with open-source Hugging Face LLMs using LangChain](https://medium.com/@jiangan0808/retrieval-augmented-generation-rag-with-open-source-hugging-face-llms-using-langchain-bd618371be9d)." ] }, { "cell_type": "markdown", "id": "d2605047", "metadata": {}, "source": [ "```{admonition} Oppgave 6.1: Lage en ny notebook\n", ":class: tip\n", "\n", "Lag en ny Jupyter Notebook som du kaller `RAG` ved å velge Filmenyen i JupyterLab, deretter _New_ og _Notebook_. Hvis du blir spurt om å velge en kjerne, velg “Python 3”. Gi den nye notebooken et navn ved å velge Filmenyen i JupyterLab og deretter “Rename Notebook”. Bruk navnet `RAG`.\n", "```" ] }, { "cell_type": "markdown", "id": "e2b6fddc", "metadata": {}, "source": [ "```{admonition} Oppgave 6.2: Stopp gamle kjerner\n", ":class: tip\n", "\n", "JupyterLab bruker en Python kjerne til å kjøre koden i hver notebook. For å frigjøre GPU minne som ble brukt i forrige kapittel, bør du stoppe kjernen for den notebooken. I menyen på venstre side av JupyterLab, velg den mørke sirkelen med en hvit firkant i. Deretter velger du _KERNELS_ og _Shut Down All_.\n", "```" ] }, { "cell_type": "markdown", "id": "630a8383", "metadata": {}, "source": [ "## Språkmodellen\n", "\n", "Vi skal bruke modeller fra [HuggingFace](https://huggingface.co/), en nettside som har verktøy og modeller til maskinlæring. Vi kommer til å bruke språkmodellen med åpne vekter og parametere, [google/gemma-3-4b-it](https://huggingface.co/google/gemma-3-4b-it), fordi den er liten nok til at vi kan bruke den med de minste GPUene på Fox. Hvis du kjører på en GPU med mer minne, kan du få bedre resultater med en større modell, som for eksempel [mistralai/Ministral-8B-Instruct-2410](https://huggingface.co/mistralai/Ministral-8B-Instruct-2410)." ] }, { "cell_type": "markdown", "id": "29f02a2e", "metadata": {}, "source": [ "## Modellens plassering\n", "\n", "Vi må laste ned modellen som vi skal bruke. Grunnet forutsetningene over, kjører vi programmet på tungregningsklyngen [Fox ved UiO](https://www.uio.no/tjenester/it/forskning/beregning/fox/index.html). Vi må peke på stedet der vårt program skal lagre modellene som vi laster ned fra HuggingFace:" ] }, { "cell_type": "code", "execution_count": null, "id": "526b85a7", "metadata": { "hide-output": false }, "outputs": [], "source": [ "import os\n", "os.environ['HF_HOME'] = '/fp/projects01/ec443/huggingface/cache/'" ] }, { "cell_type": "markdown", "id": "ae46409e", "metadata": {}, "source": [ "```{note}\n", "\n", "Hvis du kjører programmene lokalt på din egen datamaskin, trenger du kanskje ikke sette `HF_HOME`.\n", "```" ] }, { "cell_type": "markdown", "id": "92de5d73", "metadata": {}, "source": [ "## Modellen\n", "\n", "Nå er vi klare til å laste opp og bruke modellen. For å gjøre dette, lager vi en _pipeline_. En pipeline kan bestå av flere steg, men i dette tilfellet trenger vi bare ett steg. Vi kan bruke metoden `HuggingFacePipeline.from_model_id()`, som automatisk laster den spesifiserte modellen fra HuggingFace.\n", "\n", "Som før, sjekker vi om vi har GPU tilgjengelig:" ] }, { "cell_type": "code", "execution_count": null, "id": "d31d6939", "metadata": { "hide-output": false }, "outputs": [], "source": [ "import torch\n", "device = 0 if torch.cuda.is_available() else -1" ] }, { "cell_type": "code", "execution_count": null, "id": "4ad0e341", "metadata": { "hide-output": false }, "outputs": [], "source": [ "from langchain_huggingface.llms import HuggingFacePipeline\n", "\n", "llm = HuggingFacePipeline.from_model_id(\n", " model_id='google/gemma-3-4b-it',\n", " task='text-generation',\n", " device=device,\n", " pipeline_kwargs={\n", " 'max_new_tokens': 1500,\n", " 'do_sample': True,\n", " 'temperature': 0.3,\n", " 'num_beams': 4\n", " }\n", ")" ] }, { "cell_type": "markdown", "id": "3963c82f", "metadata": {}, "source": [ "Vi kan gi noen argumenter til “pipelinen”:\n", "\n", "- `model_id`: modellens navn fra HuggingFace\n", "\n", "- `task`: oppgaven du planlegger å bruke modellen til\n", "\n", "- `device`: GPU maskinvaren som enheten bruker. Hvis vi ikke spesifiserer en enhet, vil GPU ikke brukes.\n", "\n", "- `pipeline_kwargs`: (keyword arguments) tilleggsparametere som gis til modellen\n", "\n", " - `max_new_tokens`: max lengde på teksten som genereres\n", "\n", " - `do_sample`: Hvis `False`vil det mest sannsynlige ordet bli valgt. Dette gjør outputten deterministisk. Vi kan sørge for en mer tilfeldig utvelging. Standardverdien later til å være `True`. \n", "\n", " - `temperature`: temperaturkontrollen er den _statistiske distribusjonen_ til neste ord. Vanligvis et tall mellom 0 and 1. Lav temperatur øker sannsynligheten for vanlige ord. Høy temperatur øker muligheten for sjeldnere ord i output. Utviklerne har ofte en anbefaling hva angår temperatur. Vi bruker anbefalingen som et startpunkt.\n", "\n", " - `num_beams`: som standard gir modellen en enkel sekvens av tokens/ord. Med beam search, vil programmet bygge flere samtidige sekvenser, og deretter velge den beste til slutt.\n", "\n", "\n", "```{admonition}\n", ":class: note\n", "\n", "Hvis du jobber på en maskin med mindre minne, kan du prøve en enda mindre modell. Du kan for eksempel prøve `google/gemma-3-1b-it`. Denne modellen har bare 1 milliard parametere, og er bare 2 GB i størrelse. Den er mulig å bruke på en bærbar maskin, avhengig av hvor mye minne den har.\n", "```" ] }, { "cell_type": "markdown", "id": "12f064af", "metadata": {}, "source": [ "## Språkmodellen i bruk\n", "\n", "Nå er språkmodellen klar til bruk. La oss forsøke å bruke den uten RAG. Vi kan sende en forespørsel:" ] }, { "cell_type": "code", "execution_count": null, "id": "0f23f233", "metadata": { "hide-output": false }, "outputs": [], "source": [ "query = 'What are the major contributions of the Trivandrum Observatory?'\n", "output = llm.invoke(query)\n", "print(output)" ] }, { "cell_type": "markdown", "id": "db20b39f", "metadata": {}, "source": [ "Svaret ble generert på grunnlag av informasjonen som befinner seg fra før av i språkmodellen. For å forbedre presisjonen i svaret, kan vi sørge for at språkmodellen får mer kontekst til spørsmålet. For å gjøre dette, må vi laste inn dokumentsamlingen." ] }, { "cell_type": "markdown", "id": "27e40cc8", "metadata": {}, "source": [ "## Vektorisering\n", "\n", "Tekst må vektoriseres før den kan bli bearbeidet. Vår HuggingFace pipeline vil gjøre det automatisk for språkmodellen, men vi må lage en vektorisator til søkeindeksen som vi skal bruke til dokumentdatabasen vår. Vi bruker en vektorisator som på engelsk kalles en _word embedding model_ fra HuggingFace. HuggingFace biblioteket vil automatisk laste ned modellen:" ] }, { "cell_type": "code", "execution_count": null, "id": "bb120495", "metadata": { "hide-output": false }, "outputs": [], "source": [ "from langchain_huggingface import HuggingFaceEmbeddings\n", "\n", "huggingface_embeddings = HuggingFaceEmbeddings(\n", " model_name='BAAI/bge-m3',\n", " model_kwargs = {'device': 'cuda:0'},\n", " #or: model_kwargs={'device':'cpu'},\n", " encode_kwargs={'normalize_embeddings': True}\n", ")" ] }, { "cell_type": "markdown", "id": "b659b474", "metadata": {}, "source": [ "```{admonition} Embeddingens argumenter\n", ":class: note\n", "\n", "Dette er argumentene til embedding modellen:\n", "\n", "- `model_name`: modellens navn fra HuggingFace \n", "- `device`: maskinvaren som skal brukes, enten GPU eller CPU \n", "- `normalize_embeddings`: embeddinger kan ha forskjellige størrelser. Når embeddingen normaliseres betyr det at man gjør størrelsen lik for alle. \n", "```" ] }, { "cell_type": "markdown", "id": "b07f40d2", "metadata": {}, "source": [ "## Dokumentets plassering\n", "\n", "Vi har samlet noen artikler som har Creative Commons lisens. Vi skal forsøke å laste opp alle dokumentene fra mappen som det vises til under. Hvis du vil, kan du endre stien til din egen mappe på hjemmeområdet:" ] }, { "cell_type": "code", "execution_count": null, "id": "9ff1cfc6", "metadata": { "hide-output": false }, "outputs": [], "source": [ "document_folder = '/fp/projects01/ec443/documents'" ] }, { "cell_type": "markdown", "id": "18ad728b", "metadata": {}, "source": [ "## Lasting av dokumentene\n", "\n", "Vi bruker `DirectoryLoader` fra LangChain til å laste alle filene fra `document_folder`. `document_folder` defineres over:" ] }, { "cell_type": "code", "execution_count": null, "id": "021c03ab", "metadata": { "hide-output": false }, "outputs": [], "source": [ "from langchain_community.document_loaders import DirectoryLoader\n", "\n", "loader = DirectoryLoader(document_folder)\n", "documents = loader.load()" ] }, { "cell_type": "markdown", "id": "5a33be3c", "metadata": {}, "source": [ "“Document loader” laster hver fil i et eget dokument. Vi kan undersøke størrelsen på dokumentene våre. Vi kan for eksempel bruke funksjonen `max()` for å finne lengden på det lengste dokumentet:" ] }, { "cell_type": "code", "execution_count": null, "id": "d4767215", "metadata": { "hide-output": false }, "outputs": [], "source": [ "print(f'Number of documents:', len(documents))\n", "print('Maximum document length: ', max([len(doc.page_content) for doc in documents]))" ] }, { "cell_type": "markdown", "id": "0a5b83d4", "metadata": {}, "source": [ "Vi kan se på ett av dokumentene:" ] }, { "cell_type": "code", "execution_count": null, "id": "05b60489", "metadata": { "hide-output": false }, "outputs": [], "source": [ "print(documents[0])" ] }, { "cell_type": "markdown", "id": "6c4e42bc", "metadata": {}, "source": [ "## Splitting av dokumentene\n", "\n", "Siden vi bare bruker PDFer med ganske korte sider, kan vi laste dem inn som de er. Andre og lengre dokumenter som for eksempel nettsider, bør deles inn i chunker. Vi kan bruke en text splitter fra LangChain til å dele dokumentene:" ] }, { "cell_type": "code", "execution_count": null, "id": "6394b935", "metadata": { "hide-output": false }, "outputs": [], "source": [ "from langchain_text_splitters import RecursiveCharacterTextSplitter\n", "\n", "text_splitter = RecursiveCharacterTextSplitter(\n", " chunk_size = 700, # Could be more, for larger models like mistralai/Ministral-8B-Instruct-2410\n", " chunk_overlap = 200,\n", ")\n", "documents = text_splitter.split_documents(documents)" ] }, { "cell_type": "markdown", "id": "fe1190c0", "metadata": {}, "source": [ "```{admonition} Text Splitterens Argumenter\n", ":class: note\n", "\n", "Her er tekst splitterens argumenter\n", "\n", "- `chunk_size`: antall tokens i hver chunk. Ikke nødvendigvis det samme som antall ord. \n", "- `chunk_overlap`: antall tokens som inkluderes i begge chunks der teksten deles. \n", "\n", "```\n", "Vi kan se etter om maks dokumentlengde har endret seg:" ] }, { "cell_type": "code", "execution_count": null, "id": "92a6401a", "metadata": { "hide-output": false }, "outputs": [], "source": [ "print(f'Number of documents:', len(documents))\n", "print('Maximum document length: ', max([len(doc.page_content) for doc in documents]))" ] }, { "cell_type": "markdown", "id": "521520b9", "metadata": {}, "source": [ "## Dokument indeksen\n", "\n", "Neste skritt er å lage en søkeindeks til dokumentene våre. Denne indeksen kommer vi til å bruke til gjenfinningsdelen i “Gjenfinningsforsterket tekstgenerering”. Vi bruker det åpne biblioteket [FAISS](https://github.com/facebookresearch/faiss) (Facebook AI Similarity Search) gjennom LangChain:" ] }, { "cell_type": "code", "execution_count": null, "id": "10a49b1c", "metadata": { "hide-output": false }, "outputs": [], "source": [ "from langchain_community.vectorstores import FAISS\n", "vectorstore = FAISS.from_documents(documents, huggingface_embeddings)" ] }, { "cell_type": "markdown", "id": "b48986cb", "metadata": {}, "source": [ "FAISS kan finne dokumenter som samsvarer med et søk:" ] }, { "cell_type": "code", "execution_count": null, "id": "102604bf", "metadata": { "hide-output": false }, "outputs": [], "source": [ "relevant_documents = vectorstore.similarity_search(query)\n", "print(f'Number of documents found: {len(relevant_documents)}')" ] }, { "cell_type": "markdown", "id": "1d33d38e", "metadata": {}, "source": [ "Vi kan vise det første dokumentet:" ] }, { "cell_type": "code", "execution_count": null, "id": "ef81fc0b", "metadata": { "hide-output": false }, "outputs": [], "source": [ "print(relevant_documents[0].page_content)" ] }, { "cell_type": "markdown", "id": "24277d3f", "metadata": {}, "source": [ "Til RAG programmet vårt trenger vi tilgang til en søkemotor fra et grensesnitt som kalles en retriever:" ] }, { "cell_type": "code", "execution_count": null, "id": "8ea9d612", "metadata": { "hide-output": false }, "outputs": [], "source": [ "retriever = vectorstore.as_retriever(search_kwargs={'k': 3})" ] }, { "cell_type": "markdown", "id": "2d49a7d8", "metadata": {}, "source": [ "```{admonition} Retriever argumenter\n", ":class: note\n", "\n", "Dette er retrieverens argumenter:\n", "\n", "- ‘k’: the number of documents to return (kNN search)\n", "```" ] }, { "cell_type": "markdown", "id": "23c6275c", "metadata": {}, "source": [ "## Lage en instruks/ prompt\n", "\n", "Vi kan bruke en instruks til å fortelle språkmodellen hvordan den skal svare. instruksen bør være kort og nyttig. I tillegg, skal vi ha plassbeholdere til spørsmålets kontekst. LangChain erstatter disse med den faktiske konteksten og spørsmålet når vi kjører forespørselen." ] }, { "cell_type": "code", "execution_count": null, "id": "5058e33f", "metadata": { "hide-output": false }, "outputs": [], "source": [ "from langchain_classic.prompts import PromptTemplate\n", "\n", "prompt_template = '''You are an assistant for question-answering tasks.\n", "Use the following pieces of retrieved context to answer the question.\n", "Context: {context}\n", "\n", "Question: {input}\n", "\n", "Answer:\n", "'''\n", "\n", "prompt = PromptTemplate(template=prompt_template,\n", " input_variables=['context', 'input'])" ] }, { "cell_type": "markdown", "id": "fed28ee8", "metadata": {}, "source": [ "## Vi lager Chatboten\n", "\n", "Nå kan vi bruke modulen `create_retrieval_chain` fra LangChain til å lage en agent som besvarer spørsmål, en «chatbot»:" ] }, { "cell_type": "code", "execution_count": null, "id": "a84f261c", "metadata": { "hide-output": false }, "outputs": [], "source": [ "from langchain_classic.chains import create_retrieval_chain\n", "from langchain_classic.chains.combine_documents import create_stuff_documents_chain\n", "\n", "combine_documents_chain = create_stuff_documents_chain(llm, prompt)\n", "rag_chain = create_retrieval_chain(retriever, combine_documents_chain)" ] }, { "cell_type": "markdown", "id": "72556707", "metadata": {}, "source": [ "## Spørsmål til «Chatboten»\n", "\n", "Nå kan vi sende instruksen til chatbotten:" ] }, { "cell_type": "code", "execution_count": null, "id": "497bee7c", "metadata": { "hide-output": false }, "outputs": [], "source": [ "result = rag_chain.invoke({'input': query})" ] }, { "cell_type": "code", "execution_count": null, "id": "87d83dbb", "metadata": { "hide-output": false }, "outputs": [], "source": [ "print(result['answer'])" ] }, { "cell_type": "markdown", "id": "ab419ab5", "metadata": {}, "source": [ "Forhåpentligvis vil svaret inneholde informasjon fra konteksten som ikke var en del av det forrige svaret, da vi kjørte instruksen uten RAG." ] }, { "cell_type": "markdown", "id": "61a0d1b5", "metadata": {}, "source": [ "## Oppgaver\n", "\n", "```{admonition} Oppgave 6.3: Bruk dine egne dokumenter\n", ":class: tip\n", "\n", "Endre dokumentenes plassering til din egen dokumentmappe. Du kan laste opp flere dokumenter, dersom du vil prøve å kjøre RAG på dem. Husk å endre instruksen til et spørsmål som kan besvares basert på dine egne dokumenter. Kjør instruksen og evaluere svaret.\n", "```\n", "\n", "```{admonition} Oppgave 6.4: Lagre dokumentindeksen\n", ":class: tip\n", "\n", "Dokumentindeksen som vi lagde med FAISS er bare lagret i minnet. For å unngå at vi må reindeksere dokumentene hver gang vi laster notebooken, kan vi lagre indeksen. Prøv å bruke funksjonen `vectorstore.save_local()` til å lagre indeksen. Du kan dermed laste indeksen fra en fil ved å bruke funksjonen `FAISS.load_local()`. Se dokumentasjon på [FAISS modulen i LangChain](https://python.langchain.com/docs/integrations/vectorstores/faiss/#saving-and-loading) dersom du vil ha flere detaljer.\n", "```\n", "\n", "````{admonition} Oppgave 6.5: Slurm jobber\n", ":class: tip\n", "\n", "Når du har laget et program som virker, er det mer effektivt å kjøre programmet som en batch jobb enn i JupyterLab. Dette fordi en økt i JupyterLab reserverer en GPU hele tiden, også når du ikke kjører beregninger. Dette er grunnen til at du bør lagre det ferdige programmet ditt som et vanlig Python program som kan [planlegges](https://training.pages.sigma2.no/tutorials/hpc-intro/episodes/13-scheduler.html) som en del av slurm køen ved UiO.\n", "\n", "Du kan lagre koden ved å velge filmenyen i JupyterLab, velg “Save and Export Notebook As…” og så “Executable Script”. Resultatet er Python filen `RAG.py` som lastes ned lokalt til din maskin. Du trenger også å laste ned slurmskriptet {download}`LLM.slurm<../LLM.slurm>`\n", "\n", "Last opp både Python filen `RAG.py` og slurm skriptet `LLM.slurm` til Fox. deretter starter du jobben med denne kommandoen:\n", "\n", "```{code} python\n", "! sbatch LLM.slurm RAG.py\n", "```\n", "\n", "Slurm lager en loggfil for hver jobb som lagres med et navn som for eksempel `slurm-1358473.out`. Som standard blir disse loggfilene lagret i den aktuelle arbeidskatalogen der du kjører `sbatch` kommandoen fra. Dersom du ønsker å lagre loggfilen et annet sted, kan du legge til en linje som vises under, i ditt slurm skript. Husk å endre brukernavnet:\n", "\n", "```{code} python\n", "#SBATCH --output=/fp/projects01/ec443//logs/slurm-%j.out\n", "````" ] }, { "cell_type": "markdown", "id": "c5cebb12", "metadata": {}, "source": [ "Får du feilmeldinger? Lant ned [Sublime text](https://www.sublimetext.com/download) slik at du lettere får oversikt over koden din. Output filen gir et linjenummer for der feilen ligger. Da kan du lese av rette linjenummeret i Sublime editoren." ] }, { "cell_type": "markdown", "id": "b2af2889", "metadata": {}, "source": [ "## Bonus Materiale\n", "\n", "```{admonition} Oppgave 6.6: Legge til siteringer\n", ":class: note\n", "\n", "Teksten som ble generert i dette kapittelet, har ikke sitater eller referanser. All tekst som bygger på kilder bør inkludere henvisninger til disse. Henvisninger gjør det mulig å finne kilden, og faktasjekke opplysningene. LangChain støtter siteringer i teksten når man bruker modeller som kan prosusere referanser. Se LangChain tutorial om [hvordan man kan få en RAG applikasjon til å legge til sitater](https://python.langchain.com/docs/how_to/qa_citations/).\n", "```" ] } ], "metadata": { "date": 1772447236.818845, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }