Siri Belajar AI: Asas Model Bahasa (Bahagian 2)
Mari kenal sedikit tentang kehilangan (loss function)

Dalam siri yang lepas, kita sudahpun belajar tentang asas model bahasa. Di mana bila kita memberikan satu badan teks, kita cuma perlu mencari nilai kebarangkalian berdasarkan teks tersebut, dan menggunakannya untuk menghasilkan (generate) teks yang lain. Kita nampak yang hasilnya bolehlah, tapi tak adalah gempak sangat. Biasa-biasa je. Tapi sebenarnya kita boleh memperbaiki model kita ini. Tapi sebelum nak memperbaiki, kita kena tahu dulu macam mana nak nilai model kita ni secara kuantitatif. Kita nak tengok pada fungsi kehilangan (loss function). Susah betul nak terjemah ke bahasa melayu kan?
Ada banyak loss function yang digunakan dalam AI dan machine learning. Kita ada MSE (mean-squared error), kita ada MAE (mean-absolute error) dan banyak lagi. Tapi dalam hal model bahasa, loss function yang sebaiknya digunakan adalah negative-log likelihood. Dan hari ini kita akan belajar macam mana nak buat negative-log likelihood ni dari kosong (dari asas) dengan mempelajari matematik di sebaliknya. Formula untuk negative-log likelihood adalah seperti di bawah:
Ada dua istilah yang kita kenal dulu di sini, istilah kebarangkalian dan kebolehjadian. Kebarangkalian kita dah kenal, tapi apa tu kebolehjadian? Mari kita kenal dulu tentang kebolehjadian, dan apakah perbezaannya dengan kebarangkalian.
Kebarangkalian (Probability) dan Kebolehjadian (Likelihood), dua konsep yang berbeza
Dua-dua konsep ini kita dah belajar dulu masa sekolah kan? Saya bagi satu jalan cerita yang mudah. Katakanlah kau ada 3 keping duit syiling. Kau tengah bosan, duduk kat kedai kopi, sambil merenung nasib kau. Datang kawan kau tanya, awat kau termenung? Kau pun ceritalah, kau tengah bingung. Ada dua orang awek ni tengah syok dekat kau. Tapi kau tak tahu nak pilih mana satu.
“Macam lagu di sana menanti, di sini menunggu ke?” tanya kawan kau balik.
Kau mengangguk sayu, tapi dengan muka yang sangat poyo. Aku tak tahu buat apa ni, kata kau. Aku ingat macam nak lontar syiling je. Naik kepala, aku pilih yang disana. Naik ekor, aku pilih yang di sini.
Kawan aku menggelengkan kepala. “Kau tak boleh pakai syiling nak tentukan nasib orang. Bahaya.”
Kenapa? Kau tanya balik.
“Kau lupa pasal kebarangkalian dengan kebolehjadian? Kita belajar kat sekolah dulu?”
Kau garu kepala. Dah kenapa kawan kau ni, nak jadi cikgu matematik pulak?
“Katakanlah kau baling syiling tu, ada 50–50 kemungkinan yang ia akan jadi kepala ataupun ekor, betul?”
Kau mengangguk, betul jugak.
“Jadi itulah kebarangkalian. Tapi, katakanlah, bila kau lontar, 3, 3 kali jadi kepala. Kau boleh pakai ke duit syiling tu?”
Kau gelengkan kepala. Mana boleh.
“Ha, itulah dia kebolehjadian. Kalau 3,3 kali jadi kepala. Maka ia bukanlah satu perkara yang baik. Tak boleh jadi, tak boleh pakai benda ni. Jadi kau tak boleh gunakan cara ni untuk tentukan nasib awek-awek yang minat kau”
Kau mengangguk tanda faham. Jadi itulah sebenarnya beza kebarangkalian dan kebolehjadian. Kebarangkalian adalah nilai sesuatu peristiwa untuk berlaku. Kebolehjadian adalah kualiti, ataupun kebolehan kita untk menggunakan model tersebut. Jadi dah faham kan? Mari kita cari nilai kebarangkalian dan juga nilai kebolehjadian dalam model kita.
#mari kita gunakan balik kod yang kita guna dalam bahagian lepas
#untuk keluarkan senarai bigram
for ak in cleaned_nama[:3]: #buat 3 contoh jelah nama, tak nak panjang sangat
na = ['.'] + list(ak) + ['.']
for ak1,ak2 in zip(na,na[1:]):
ix1 = num_c[ak1]
ix2 = num_c[ak2]
prob = P[ix1,ix2] #kita letakkan senarai kebarangkalian bagi setiap bigram
print(f'Kebarangkalian untuk bigram {ak1}{ak2} adalah {prob:.4f}')Sekarang, macam mana kita nak kira nilai kebolehjadian? Cara paling mudah adalah dengan mendarabkan sahaja setiap nilai kebarangkalian tersebut. Tapi ini akan menjadikan nilai kebolehjadian sangatlah kecil, sampai tak boleh nampak. Boleh tengok pada kod di bawah:
likelihood_list = [] # Untuk menyimpan nilai kebolehjadian bagi setiap bigram
for ak in cleaned_nama[:3]:
na = ['.'] + list(ak) + ['.']
likelihood = 1 # Inisialisasi kebolehjadian sebagai 1 kerana kita
# akan darabkan kebarangkalian
for ak1, ak2 in zip(na, na[1:]):
ix1 = num_c[ak1]
ix2 = num_c[ak2]
prob = P[ix1, ix2] # Kebarangkalian bagi setiap bigram
likelihood *= prob # Darabkan kebarangkalian bigram ini ke
# dalam kebolehjadian
print(f'Kebarangkalian untuk bigram {ak1}{ak2} adalah
{prob:.4f} dan nilai kebolehjadian adalah {likelihood:.4f}')Boleh nampak sendiri yang nilai kebolehjadian semakin lama semakin rendah. Jadi bila diminta untuk mengira nilai kebolehjadian model ini, silap-silap boleh tak de keluar nombor lansung.Jadi, saintis data mencadangkan penggunaan nilai log. Sebab kalau kita dah tukarkan dia kepada log, kita dah tak perlu nak darab-darab lagi, kita hanya perlu tambah je. Ini dah masuk bahagian matematik asas, jadi saya tak naklah menyelam dasar sangat. Tapi, apa yang kita buat adalah, kita kira nilai kebolehjadian menggunakan nilai log dari nilai darab sahaja. Boleh tengok kod di bawah
#kita setkan dulu nilai awal log likelihood sebelum masukkan dalam
#kitaran (loop)
log_likelihood = 0.0
for ak in cleaned_nama[:3]:
na = ['.'] + list(ak) + ['.']
for ak1,ak2 in zip(na,na[1:]):
ix1 = num_c[ak1]
ix2 = num_c[ak2]
prob = P[ix1,ix2] #kita letakkan senarai kebarangkalian bagi
# setiap bigram
logprob = torch.log(prob) #kita tukarkan semua nilai
# kebarangkalian kepada nilai log
log_likelihood += logprob #dah ada nilai log, kita tambah
# jelah untuk dapatkan nilai kebolehjadian
print(f'Kebarangkalian untuk bigram {ak1}{ak2} adalah
{prob:.4f} dan nilai log kebarangkalian ialah {logprob:.4f}')
print(f'Nilai kebolehjadian keseluruhan model adalah {log_likelihood:.4f}')Tapi bila tengok balik, nilai kebolehjadian yang kita ada ni sangatlah sukar untuk kita faham. Susah nak buat kerja dengan nilai ni. Jadi sebaiknya, mereka mencadangkan kita gunakan nilai negative log likelihood. Nilai ini kita boleh normalisasikan dia, sehingga menjadi satu nilai pengukur yang boleh memberi satu nilai kuantitatif kepada performance model bahasa kita. Cara nak buat adalah seperti di bawah:
log_likelihood = 0.0
n = 0 #kita setkan nilai awal n, yang akan digunakan untuk
# proses normalisasi nanti
for ak in cleaned_nama[:3]:
na = ['.'] + list(ak) + ['.']
for ak1,ak2 in zip(na,na[1:]):
ix1 = num_c[ak1]
ix2 = num_c[ak2]
prob = P[ix1,ix2] #kita letakkan senarai kebarangkalian
# bagi setiap bigram
logprob = torch.log(prob) #kita tukarkan semua nilai
# kebarangkalian kepada nilai log
log_likelihood += logprob #dah ada nilai log, kita tambah
#jelah untuk dapatkan nilai kebolehjadian
n += 1 #kita tambahkan nilai n bagi setiap kitaran
print(f'Kebarangkalian untuk bigram {ak1}{ak2} adalah
{prob:.4f} dan nilai log kebarangkalian ialah {logprob:.4f}')
negative_log_likelihood = - log_likelihood #letak negatif je
fungsi_loss = negative_log_likelihood/n # bahagi dengan n untuk normalize
print(f'Nilai loss function model kita adalah {fungsi_loss:.4f}')Nilai 2.602 itu adalah nilai kebolehjadian yang kita perlukan. Ia lebih tepat dalam menggambarkan nilai model kita. Semakin kecil model ini, semakin baik prestasi model kita. Jadi objektif kita dalam menghasilkan model bahasa, adalah mencari nilai kebolehjadian yang paling rendah yang kita boleh capai. Untuk itu, dalam bahagian yang akan datang, kita akan cuba untk menggunakan jaringan neural untuk menambahbaik model kita.





