Siri Belajar AI: Buat RAG dari Kosong (Bahagian 2)

Abu Huzaifah Bin Haji Bidin
6 min readApr 21, 2024

--

Gambar diambil dari https://www.stardog.com/blog/similarity-search/

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.

  1. Kita belajar apa itu maksud RAG. (Retrieval, Augmented, Generation)
  2. Kita belajar bahagian retrieval. Maksudnya kita belajar macam mana nak keluarkan/perah (extract) teks dari PDF.
  3. Kita juga belajar macam mana nak susun teks yang kita dah keluarkan itu kedalam satu jadual/dataset yang kemas.
  4. 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:

  1. Kita kena definisikan “perkataan” apa yang kita nak cari. Benda ni kita panggil sebagai query.
  2. Query ni kita akan tukarkan kepada nombor ataupun vector dengan menggunakan embedding model
  3. 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).
  4. 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

hasil carian perkataan “double jeopardy”

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:

Tatasununan (array) yang kita dah embedkan dalam bahagian 1

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.

Bentuk ringkas persamaan dot product

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:

Bentuk ringkas persamaan cosine similarity

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:

Hasil kod di atas

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.

--

--

Abu Huzaifah Bin Haji Bidin

Process Engineer with passion in anything data, analytics and AI. And I also write! Visit my website for more info (https://maercaestro.github.io)