Co to jest Retrieval-Augmented Generation, czyli RAG?

Przykład kodu w Python który wykorzystuje RAG by przeszukiwać poemat Pan Tadeusz Adama Mickiewicza.

Generowanie wspomagane wyszukiwaniem (RAG) to technika zwiększania dokładności i wiarygodności generatywnych modeli sztucznej inteligencji na podstawie faktów pobieranych ze źródeł zewnętrznych.

Aby zrozumieć najnowsze osiągnięcia w dziedzinie generatywnej sztucznej inteligencji, wyobraź sobie salę sądową.

Sędziowie rozpatrują i rozstrzygają sprawy w oparciu o swoje ogólne rozumienie prawa. Czasami sprawa – na przykład pozew o błąd w sztuce lub spór pracowniczy – wymaga specjalnej wiedzy specjalistycznej, dlatego sędziowie wysyłają urzędników sądowych do biblioteki prawniczej w poszukiwaniu precedensów i konkretnych spraw, które mogliby przytoczyć.

Czym jest Generatywna Sztuczna Inteligencja?

Generatywna Sztuczna Inteligencja umożliwia użytkownikom szybkie generowanie nowej treści na podstawie różnorodnych danych wejściowych. Dane wejściowe i wyjściowe dla tych modeli mogą obejmować tekst, obrazy, dźwięki, animacje, modele 3D lub inne rodzaje danych.

Jak działa Generatywna Sztuczna Inteligencja?

Modele Generatywnej Sztucznej Inteligencji wykorzystują sieci neuronowe do identyfikowania wzorców i struktur w istniejących danych w celu generowania nowej i oryginalnej treści.

Jednym z przełomów w modelach generatywnej sztucznej inteligencji jest zdolność do wykorzystania różnych podejść do uczenia, w tym uczenia nienadzorowanego lub półnadzorowanego podczas szkolenia. Dzięki temu organizacje mogą łatwiej i szybciej wykorzystywać duże ilości danych nieoznaczonych do tworzenia modeli podstawowych. Jak sugeruje nazwa, modele podstawowe można wykorzystać jako podstawę dla systemów sztucznej inteligencji, które mogą wykonywać wiele zadań.

Przykłady modeli podstawowych obejmują GPT-3 i Stable Diffusion, które pozwalają użytkownikom wykorzystać moc języka. Na przykład popularne aplikacje, takie jak ChatGPT, korzystające z GPT-3, umożliwiają użytkownikom wygenerowanie eseju na podstawie krótkiego tekstu zapytania. Z kolei Stable Diffusion pozwala użytkownikom generować fotorealistyczne obrazy na podstawie danych tekstowych.

Dlaczego mielibyśmy używać RAG skoro mamy wszystkowiedzące modele LLM?

Jednym z powodów jest to że modele LMM nie mają dostępu do danych poufnych, korporacyjnych lub po prostu niedostępnych jako źródła publicznych modeli. RAG pozwala zastosować algorytmy LLM do generowania treści właśnie z tych źródeł.

Środowisko programu

Skrypt który realizuje to zadanie jest napisany w Python. Wykorzystuje LangChain - framework do tworzenia aplikacji opartych na modelach językowych. Sam Lanchain rozbity został na trzy pakiety: langchain-core, langchain-community i langchain. Przeczytaj więcej o każdym z pakietów.

W programie wykorzystywana jest także baza Chroma. Odczytany z poematu tekst dzielimy na kawałki, obliczamy wektory ('embeddings') dla każdego 'kawałka' tekstu i zapisujemy sam fragment oraz wektor do właśnie tej bazy.

Następnym komponentem jest właśnie LLM. Używamy modelu do obliczania wektorów (do czego one są nam potrzebne niżej) oraz generowania treści z naszego poematu (zapisanego w Chroma DB). Środowiskiem modeli jest Ollama.

Jak działa program - loader?

  • Odczytujemy plik, dzielimy na kawałki ("chunks"). Polecenie splittera 'CharacterTextSplitter' ma parametry 'chunk_size', 'chunk_overlap', 'separator'. Chunk_size mowi jakiej maksymalnej długości mają być kawałki tekstu. ALE jeśli tekst nie zmieści się w deklarowanej długości, chunk będzie dłuższy. Np. kiedy separator wypadnie dalej niż po 1000 znaków. Program wyświetli wtedy komunikat: 'Created a chunk .... longer than the specified'. Chunk_overlap to po prostu nakładanie się następujących po sobie fragmentów treści czasami wymagane by nie utracić kontekstu. Jeśli tekst konczyłby sie np słowami '... mały rowerek.' to przy 'chunk_overlap=8' to słowo 'rowerek' rozpocznie następne zdanie. Separator oznacza... separator który rozdziela kolejne fragmenty treści. Domyślnym separatorem jest nowa linia. Efektem zastosowania takiego separatora jest próba utrzymania wszystkich akapitów (a następnie zdań, a następnie słów) razem tak długo, jak to możliwe, ponieważ ogólnie wydają się one najsilniejszymi semantycznie powiązanymi fragmentami tekstu. Jednak Twój tekst może być inny, użyj więc innego separatora. Pana Tadeusza. Tekst możesz pobrać np. z https://wolnelektury.pl
  •  Obliczamy wektor dla tego fragmentu tekstu i zapisujemy sam tekst i wektor do bazy Chroma. Gdybyśmy nie użyli parametru 'persist_directory', fragmenty teksu i ich osadzenia ('embeddings') zapisane zostałyby w pamięci.

Skrypt loader.py

# import
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import CharacterTextSplitter

# mierzymy czas wykonania, start:
import time
start_time = time.time()

# ładujemy dokument
loader = TextLoader("F:/Ollama/RAG/pan_tadeusz.txt", encoding="utf8")
documents = loader.load()

#  dzielimy go na kawałki (chunks)
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0, separator=".")
docs = text_splitter.split_documents(documents)

# model SBERT - embeddings
#embeddings = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
# model Ollama - embeddings 
embeddings = OllamaEmbeddings(model="mistral")

# zapisujemy do Chroma ('vector store'), do pliku na dysku nie pamieci
db = Chroma.from_documents(docs, embeddings, persist_directory="F:/RAG/ChromaData")

#zapisujemy rezultat - baze z wektorami
db.persist()

# ile czasu zajelo wykonanie
end_time = time.time()

execution_time = end_time - start_time
print("Execution time:", execution_time, "seconds")

Jak działa program - reader?

  • Odczytujemy z bazy Chroma treść która jest skojarzona z naszym zapytaniem. Zauważ że nasze połączenie z bazą danych Chroma używa modelu 'mistral' - tego samego którego użyliśmy do utworzenia wektorów w poprzednim kroku (parametr 'embedding_function'). W rezultacie tego zapytania możemy otrzymać kilka dokumentów - używamy ich jako treści której użyje model. Parametr 'return_source_documents=True' pokaże nam które dokumenty zostały użyte do wygenerowania odpowiedzi.
  • Używamy modelu by wygenerował odpowiedź na nasze zapytaniu używając treści źródłowej jak wyżej i algorytmów modelu. Parametr 'search_kwargs' retrivera określa ile dokumentów ma być brane pod uwagę przy analizie materiału. Im wyższa liczba, tym dokładniejsza może być nasza odpowiedź (szczególnie jeśli zapytanie nie jest precyzyjne lub źródło 'rozproszone'). Ale będzie to mieć negatywny wpływ na szybkość odpowiedzi - trzeba przeanalizować wiekszą ilość informacji.

Skrypt reader.py

# import
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma

# mierzymy czas wykonania, start:
import time
start_time = time.time()

# reset zmiennej
db=None

embeddings = OllamaEmbeddings(model="mistral")

# odczytaj z bazy Chroma
db = Chroma(persist_directory="F:/Ollama/RAG/data", embedding_function=embeddings)
results = db.get()

llm = Ollama(base_url='http://localhost:11434', model="mistral", temperature=0)

qa_chain = RetrievalQA.from_chain_type(
    llm,
    retriever=db.as_retriever(search_type="similarity", search_kwargs={"k": 3}),
    return_source_documents=True
)

question = "O czym mowa w utworze?"
result = qa_chain.invoke({"query": question})
result["result"]
print(result)

#skasuj wszystko
#db.delete_collection()
#print(results)

# ile czasu zajelo wykonanie
end_time = time.time()

execution_time = end_time - start_time
print("Execution time:", execution_time, "seconds")

Rezultat

Jeśli wykonasz ten skrypt zobaczysz odpowiedz podobną do tej poniżej:

{'query': 'O czym mowa w utworze?', 
'result': " The poem appears to be a narrative about an event or scene involving various noblemen and their possessions.
There is mention of a judge, a seneschal (Rejent), an assessor, a bernardyn (a friar), Wilbik, Skoluba, Vojski, and Asesor.
They seem to be discussing the taking away of treasures from churches and the opposition to it.
There is also mention of Napoleon and his granting of titles and lands to his generals.
The poem contains descriptions of various objects such as a horse, a ring, golden armor, agun (a f), and a gun (fuzyjka).
There are mentions of Strapczyna, Wojski, Rejent, and Asesor. They seem to be opposing the taking away of treasures from churches.
The poem also contains descriptions of objects such as a horse, a judge, Spawnik, and a gun.
It is called Sagalas London à Bałabanów or Sagalas, which is a famous Polish English English English English poem.
It speaks of a sword, which was used to crush the animal's body. The poem also mentions a ring, a horse, and a judge.
They seem to be discussing the taking away of treasures from churches, opposition to it, and their opposition to it."}

Jeśli użyjesz parametru 'return_source_documents=True', w odpowiedzi otrzymasz wypisane 'source_documents' - dokumenty które zostały użyte do wygenerowania odpowiedzi.

Format odpowiedzi

W powyższym skrypcie 'result' ma format 'dictionary' (po polsku 'słownik').  Słowniki służą do przechowywania wartości danych w parach klucz : wartość. Słownik to zbiór uporządkowany*, podlegający zmianom i nie pozwalający na duplikowanie.