Siri Belajar AI: Buat RAG dari Kosong (Bahagian 2)
Hi, salam semua. Selamat pulang dari kampung, selamat pulang dari berhari raya. Sudah tiba masanya untuk kita sambung semua siri belajar AI. Kita nak sambung semula buat RAG dari kosong.
Kepada yang terlepas, boleh tengok Bahagian 1.
Sedikit ulangkaji, apa yang kita belajar dalam Bahagian 1.
- Kita belajar apa itu maksud RAG. (Retrieval, Augmented, Generation)
- Kita belajar bahagian retrieval. Maksudnya kita belajar macam mana nak keluarkan/perah (extract) teks dari PDF.
- Kita juga belajar macam mana nak susun teks yang kita dah keluarkan itu kedalam satu jadual/dataset yang kemas.
- Kita juga belajar bagaimana untuk menanam (embed) teks itu tadi dalam embedding model. Kenapa perlu tanam? Sebab kita perlu tukarkan teks kepada nombor yang boleh diproses oleh LLM nanti.
Jadi secara asasnya, apa yang kita belajar dalam Bahagian 1, hanyalah bahagian retrieval sahaja. Dalam bahagian 2 ni nanti, kita akan belajar bagaimana untuk buat semantic search.
Apa benda tu? Semantic search ni macam search yang kita lebib pakai.Tapi dalam konteks ini, ia lebih terhad kepada dokumen yang kita dah tanamkan (embed) dalam bahagian 1. Ibaratkan seperti kita sedang mencari perkataan dalam dokumen tadi, dan kita mengaharapkan yang perkataan yang kita cari itu, tepatlah sepertimana yang kita harapkan. Untuk itu, kita perlu ambil kembali embedding yang kita dah simpan dalam file excel (boleh rujuk bahagian 1)
import random
import torch
import numpy as np
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#import the data from csv
df_embed_load = pd.read_csv("txtch_embeddings.csv")
#convert embedding column from string to array
df_embed_load["embedding"] = df_embed_load["embedding"].apply(lambda x: np.fromstring(x.strip("[]"), sep=' '))
#convert ke list
pages_and_chunks = df_embed_load.to_dict(orient="records")
#convert embeddings to torch tensor and send to device
embeddings = torch.tensor(np.array(df_embed_load["embedding"].tolist()), dtype=torch.float32).to(device)
Kemudian, kita bersedia dengan embedding model supaya kita dapat gunakan embedding model pada soalan ataupun query yang kita nak hantar.
from sentence_transformers import util,SentenceTransformer
embedding_model = SentenceTransformer(model_name_or_path="all-mpnet-base-v2", device=device)
Seperti dalam bahagian 1, kita akan menggunakan model “all-mpnet-base-v2”. Kalau kau orang ada model lain, boleh je pakai. Boleh tengok contoh-contoh model embedding yang ada di sini
Ok, kita dah muat naik semula embedding kita, kita dah pun sediakan embedding model, lepas ni nak buat apa? Lepas ni kita akan buat satu fungsi untuk cari balik perkataan yang kita dah embedkan dalam bahagian 1. Kita nak pastikan yang proses embedding itu berlaku dengan betul, jadi kalau kita cari balik perkataan yang kita nak dalam embedding tu, dia beluh keluarkan balik perkataan yang kita nak. Langkahnya seperti di bawah:
- Kita kena definisikan “perkataan” apa yang kita nak cari. Benda ni kita panggil sebagai query.
- Query ni kita akan tukarkan kepada nombor ataupun vector dengan menggunakan embedding model
- Kemudian kita akan menggunakan fungsi dot product ataupun cosine similarity untuk mencari perkataan yang paling dekat dengan query kita dalam dokumen yang kita dah tanamkan (rujuk bahagian 1).
- Kita paparkan hasil carian kita (3 yang teratas) dalam bentuk yang kemas.
import textwrap
def print_wrapped(text,wrap_length=100):
wrapped_text = textwrap.fill(text, wrap_length)
print(wrapped_text)
def retrieve_source(query: str,
embeddings: torch.tensor,
model: SentenceTransformer=embedding_model,
n_resources_return: int =5,
print_time: bool= True):
"""
Embed a query with model and returns top k scores and indices from embeddings
"""
#Embed the query
query_embedding = model.encode(query, convert_to_tensor=True)
#get cosine similarity
start_time = timer()
score = util.cos_sim(query_embedding, embeddings)[0]
end_time = timer()
if print_time:
print(f"Time to get dot scores on ({len(embeddings)}): {end_time - start_time:.5f} seconds")
scores, indices = torch.topk(score, k=n_resources_return)
return scores, indices
def print_top_results(query: str,
embeddings: torch.tensor,
pages_and_chunks: list[dict]=pages_and_chunks,
n_resources_return: int= 5):
"""
Finds relevant passages given a query and print them out along with their scores
"""
scores, indices = retrieve_source(query, embeddings,
n_resources_return=n_resources_return)
#loop through zipped together scores and indices from torch.topk
for score, idx in zip(scores, indices):
print(f"Score: {score:.4f}")
print("Text: ")
print_wrapped(pages_and_chunks[idx]["sentence_chunk"])
print(f"Page number: {pages_and_chunks[idx]['page_number']}")
print("\n")
query= "double jeopardy"
retrieve_source(query=query,embeddings=embeddings)
print_top_results(query=query,embeddings=embeddings)
Dalam kod di atas, kita menggunakan fungsi cosine similarity untuk mencari perkataan “double jeopardy” dalam teks yang kita dah embed di bahagian 1.
Hasilnya seperti di bawah
Ok. nampaknya semantic search kita berfungsi. Ia berfungsi kerana kita menggunakan fungsi matematik untuk mencari vektor yang paling terdekat pada query yang kita isikan tadi.
Matematik Fungsi Persamaan (similarity function)
Seperti yang ditulis dalam bahagian 1, komputer tak tahu bagaimana hendak memproses perkataan. Kerana itulah ianya semua ditukar kepada nombor. Tapi nombor ini bukan nombor kosong, ianya disimpan dalam bentuk vektor yang mempunyai nilai(magnitud) dan juga arah.
Ia dipersembahkan dalam bentuk tatasusunan (array), seperti di bawah:
Kita sebagai manusia bila tengok ni memang tak faham, tapi komputer faham, sebab tu mereka boleh memproses semua ni dengan baik. Jadi untuk kita mencari perkataan dalam nombor seperti, kita perlulah menggunakan fungsi matematik.
Ada dua fungsi yang biasa digunakan dalam mencari persamaan vektor. Yang pertama, adalah dot product. Secara ringkasnya dot product ni adalah satu proses matematik di mana kita akan mendarabkan dua vektor.
Masalah pada dot product pula adalah, ia hanya mementingkan magnitud pada vektor sahaja. Hasil dari dot product hanya akan menunjukkan nilai yang bergerak pada arah yang sama, tapi ianya bergantung tinggi pada magnitud vektor tersebut.
Berbeza pula dengan fungsi kedua, fungsi yang kita panggil sebagai cosine similarity. Fungsi ini mementingkan arah vektor dan kurang mengambil kira pada magnitud. Kita boleh tengok persamaannya di bawah:
Cakap-cakap camni sahaja tak nampak jelas kan? Mari kita buat sedikit demonstrasi. Cuba korang salin dan tampal kod di bawah:
import torch
def dot_product(vector1, vector2):
return torch.dot(vector1, vector2)
def cosine_similarity(vector1, vector2):
dot_product = torch.dot(vector1, vector2)
#get euclidean/l2 norm
norm1 = torch.sqrt(torch.sum(vector1 ** 2))
norm2 = torch.sqrt(torch.sum(vector2 ** 2))
return dot_product / (norm1 * norm2)
# Contohnya
vector1 = torch.tensor([1, 2, 3],dtype=torch.float32)
vector2 = torch.tensor([1, 2, 3],dtype=torch.float32)
vector3 = torch.tensor([4, 5, 6],dtype=torch.float32)
vector4 = torch.tensor([-1, -2, -3],dtype=torch.float32)
#calculate dot product
print("Dot product between vector 1 and vector2: ",dot_product(vector1,vector2))
print("Dot product between vector 1 and vector3: ",dot_product(vector1,vector3))
print("Dot product between vector 1 and vector4: ",dot_product(vector1,vector4))
#calculate cosine similarity
print("Cosine similarity between vector1 and vector2 ",cosine_similarity(vector1,vector2))
print("Cosine similarity between vector1 and vector3 ",cosine_similarity(vector1,vector3))
print("Cosine similarity between vector1 and vector4 ",cosine_similarity(vector1,vector4))
Dalam kod di atas, apa yang kita buat adalah kita memperkenalkan 4 jenis vector. Masing-masing dengan nilai yang ada di dalamnya. Sekali imbas, kita boleh nampak yang vector1, mempunyai nilai yang sama dengan vector2, dan nilai yang berbeza dengan vector 3 dan nilai yang bertentangan arah dengan vector 4.
Jadi bila kita runningkan kod di atas, kita boleh dapat hasil seperti di bawah:
Dari sini, kita boleh nampak yang jika kita menggunakan dot product, nilai vektor yang jauh akan menghasilkan nilai dot product yang berkali ganda tinggi (32). Tapi jika kita menggunakan cosine simiularity, nilai dia taklah begitu jauh, melambangkan apa yang kita nampak. Nilai 4,5,6 tak adalah begitu jauh dari 1,2,3. Jadi pakai cosine similiarity lebih cantik, cuma ianya banyak bergantung pada konteks dan kesesuaian.
Ok, setakat sini sahajalah untuk bahagian 2. Dalam bahagian 3 nanti, kita akan belajar pula bagaimana untuk memuat turun LLM ke dalam komputer kita sendiri, supaya kita boleh gunakan LLM tu dalam RAG pipeline kita.