Rabu, 10 Mei 2023

MLP Sederhana dengan JavaScript Murni (Tanpa Pustaka Pihak Ketiga)

For English version from this entry, click here.

Halo!

Beberapa waktu lalu, aku menonton video tentang cara kerja MLP sederhana. Kemudian, video-video terkait mulai bermunculan. (Ah, sistem saran YouTube ini memang-memang, ya.) Salah satunya adalah cara membuat jaringan saraf tiruan sederhana dengan Python, tetapi tanpa pustaka pihak ketiga, antara lain, Tensorflow atau PyTorch.

Videonya dimulai dengan pembahasan secara perhitungan atau matematisnya. Setelah itu, kode program mulai disusun dengan membuat fungsi-fungsi kecil sebagai bantuan sekaligus membuat rapi kode secara keseluruhan. Pada akhirnya, jadilah sebuah skrip panjang yang melakukan MLP secara tunggal.

Apa itu MLP?

Perceptron multilapis (multilayer perceptron) adalah salah satu bentuk jaringan saraf tiruan yang tersusun dari beberapa lapis perceptron. Umumnya jaringan ini terdiri dari tiga jenis lapisan utama: (1) lapisan masukan, (2) lapisan tersembunyi, dan (3) lapisan keluaran. Semua lapisan dalam sebuah MLP punya struktur yang sama, yaitu tersusun dari beberapa perceptron.

Diagram jaringan MLP yang terdiri dari tiga kolom dengan label: lapisan masukan, lapisan tersembunyi, dan lapisan keluaran. Antara dua lapisan yang bersebelahan, terdapat garis yang menghubungkan tiap perceptron pada lapisan yang di kiri dengan tiap perceptron pada lapisan yang di kanan.
Diagram jaringan MLP dengan tiga lapisan utama

Perceptron pada dasarnya adalah komponen pengklasifikasi sederhana. Ia menerima masukan yang dibobot tertentu beserta biasnya dan menghasilkan sebuah nilai yang nantinya dianggap sebagai nilai keaktifan dari si perceptron. Secara matematis, perceptron bisa ditulis sebagai berikut:

o=fA(ixiwi+b)

  • o adalah hasil keluaran.
  • x adalah nilai masukan.
  • w adalah bobot tiap masukan.
  • b adalah nilai bias.
  • fA adalah fungsi aktivasi.

Diagram perceptron tunggal yang terdiri dari lingkaran di tengah, garis-garis di sebelah kiri, dan garis tunggal di sebelah kanan. Garis yang di kiri bertuliskan x dan w. Garis yang di kanan bertuliskan f-A dan o. Lingkaran di tengah bertuliskan tanda tambah di tengah dan huruf b di bagian bawah.
Diagram perceptron tunggal

Oke, caranya bagaimana, tuh?

Catatan: Penjelasan ini adalah versi ringkasnya. Lihat kode aslinya di bawah untuk versi lengkapnya.

Kita mulai dengan struktur data si model. Apa saja yang diperlukan dalam sebuah MLP? Untuk mempermudah, kita memerlukan empat hal sebagai berikut:

const model = {
	lapisan:     [], // matriks bobot antarlapisan
	bias:        [], // matriks bobot bias tiap lapisan (setelah lapisan masukan)
	aktivasi:    [], // daftar fungsi aktivasi tiap lapisan (setelah lapisan masukan)
	derAktivasi: []  // daftar turunan fungsi aktivasi tiap lapisan (setelah lapisan masukan)
};

Selanjutnya, diperlukan cara untuk melakukan perambatan maju/umpan ke depan (feed forward). Caranya adalah dengan melakukan perkalian matriks secara berulang dengan beberapa langkah tambahan untuk tiap lapisan. Ringkasnya, ini sama dengan rumus perceptron di atas.

function rambatMaju(model, X) {
	let nilai = X;
	let daftarHasil = [nilai];
	for (let i = 0; i < model.lapisan.length; i ++) {
		nilai = kali(model.lapisan[i], nilai);
		nilai = tambah(nilai, model.bias[i]);
		nilai = model.aktivasi[i](nilai);
		daftarHasil.push(nilai);
	}
	return daftarHasil;
}

Selanjutnya, kita mulai untuk melakukan pelatihan untuk si model. Untuk tiap iterasi, agar mudah dibuatnya, tiap sampel/entri dari set data dikenai rambat maju, hitung galat, dan rambat mundur sekaligus dalam sekali jalan.

Ini memang bukan cara yang efektif untuk memastikan bahwa pelatihan bersifat stabil karena cara ini sangat stokastik. Biasanya, tiap iterasi, rambat maju, hitung galat, dan rambat mundur dilakukan secara keseluruhan (atau per kloter/batch) agar pelatihannya bersifat stabil.

Kalau diperhatikan, tidak ada langkah untuk menghitung turunan fungsi aktivasi dari lapisan keluaran (terakhir). Ini dilakukan karena, berdasarkan percobaanku, si model gagal dilatih dan nilai galat pada lapisan-lapisan lain menjadi NaN dan lain-lain ketika galat lapisan terakhir dikali dengan turunan fungsi aktivasi lapisan terakhir.

for (let k = 0; k < X.length; k ++) {
	// rambat maju
	const masukan = transposisi([X[k]]);
	const daftarNilai = rambatMaju(model, masukan);
	const keluaran = daftarNilai[daftarNilai.length - 1];
	// rambat mundur
	let galat = kurang(keluaran, transposisi([Y[k]]));
	// tanpa perkalian dengan turunan pada lapisan keluaran
	for (let i = model.lapisan.length - 1; i >= 0; i --) {
		const galatLapisan = kali(
			galat,
			transposisi(daftarNilai[i]),
			lajuBelajar
		);
		const galatBiasLapisan = kali(
			galat,
			lajuBelajar
		)
		model.lapisan[i] = kurang(model.lapisan[i], galatLapisan);
		model.bias[i] = kurang(model.bias[i], galatBiasLapisan);
		if (i > 0) {
			galat = kali(transposisi(model.lapisan[i]), model.derAktivasi[i - 1](galat));
		}
	}
}

Setelah model terlatih, kita bisa coba untuk melakukan prediksi seperti di bawah. Fungsi ini tak hanya menghasilkan pilihan si model, tetapi juga keluaran mentahnya. Kita anggap si model dilatih untuk masalah klasifikasi.

function prediksi(model, X) {
	const hasil = {
		keluaran: [],
		terpilih: []
	}
	for (let k = 0; k < X.length; k ++) {
		const masukan = transposisi([X[k]]);
		const daftarNilai = rambatMaju(model, masukan);
		const keluaran = daftarNilai[daftarNilai.length - 1];
		const terpilih = maksArg(transposisi(keluaran)[0]);
		hasil.keluaran.push(keluaran);
		hasil.terpilih.push(terpilih);
	}
	return hasil;
}

... dan itu saja sepertinya secara umum. Masih ada banyak detail yang perlu dibuat, tetapi hal-hal di atas itu adalah gambaran umumnya. Aku pakai keseluruhan data set Iris sebagai data pelatihan dan ia mencapai akurasi 94% yang menurutku sudah bagus.

Versi lengkapnya bisa dilihat di GitHub Gist yang sudah kubuat.

Semoga membantu dan selamat mencoba!

Tidak ada komentar:

Posting Komentar