Skip to content

Instantly share code, notes, and snippets.

@srifqi
Created May 5, 2023 00:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save srifqi/64ab787999e16a0cb18eb8117e737dd4 to your computer and use it in GitHub Desktop.
Save srifqi/64ab787999e16a0cb18eb8117e737dd4 to your computer and use it in GitHub Desktop.
Contoh implementasi perceptron multilapis (MLP) dalam JavaScript ES6 murni tanpa pustaka pihak ketiga
/**
* Program untuk membuat, melatih, dan menggunakan MLP sederhana
*
* Berikut contoh implementasi perceptron multilapis (MLP) dalam JavaScript ES6
* murni tanpa pustaka pihak ketiga.
*
* @author srifqi
* @license MIT License
*/
/** ======= FUNGSI-FUNGSI KOMPONEN UTAMA ======= **/
/**
* @typedef MLP
* @type {object}
* @property {number[][]} lapisan Matriks bobot antarlapisan
* @property {number[][]} bias Matriks bobot bias tiap lapisan (setelah lapisan masukan)
* @property {function[]} aktivasi Daftar fungsi aktivasi tiap lapisan (setelah lapisan masukan)
* @property {function[]} derAktivasi Daftar turunan fungsi aktivasi tiap lapisan (setelah lapisan masukan)
*/
/**
* Fungsi utama untuk menyusun MLP
*
* @param {number[]} ukuran Daftar ukuran tiap lapisan (termasuk lapisan masukan)
* @param {number[]} aktivasi Daftar aktivasi tiap lapisan (setelah lapisan masukan)
* @returns {MLP} Model berupa MLP dari parameter yang diberikan
*/
function buatMLP(ukuran, aktivasi) {
const model = {
lapisan: [],
bias: [],
aktivasi: [],
derAktivasi: []
};
for (let i = 1; i < ukuran.length; i ++) {
const uA = ukuran[i - 1];
const uB = ukuran[i];
model.lapisan.push(buatMatriks2DAcak(uB, uA));
model.bias.push(buatMatriks2DAcak(uB, 1));
}
for (let i = 0; i < aktivasi.length; i ++) {
const ai = aktivasi[i];
model.aktivasi.push(daftarGenAktivasi[ai]);
model.derAktivasi.push(daftarDerAktivasi[ai]);
}
return model;
}
/**
* Fungsi utama untuk melatih MLP
*
* Pelatihan dilakukan secara stokastik, yaitu pembaruan parameter dilakukan
* tiap entri dan tidak diambil rata-rata.
*
* @param {MLP} model Model yang akan dilatih
* @param {number[][]} X Daftar masukan dari set latihan
* @param {number[][]} Y Daftar keluaran (hasil/target) dari set latihan
* @param {Function} fungsiLajuBelajar Fungsi untuk mengatur tetapan laju belajar
* @param {number} maksIter Jumlah iterasi maksimum
* @returns {MLP} Model hasil pelatihan
*/
function latih(model, X, Y, fungsiLajuBelajar, maksIter) {
if (X.length != Y.length) {
throw new Error(`Ukuran X dan Y tidak sama: ${X.length} dengan ${Y.length}.`);
}
let lajuBelajarSebelumnya = 0;
// tiap iterasi
for (let iter = 1; iter <= maksIter; iter ++) {
const lajuBelajar = fungsiLajuBelajar(iter - 1);
if (lajuBelajarSebelumnya != lajuBelajar) {
console.log(`Laju belajar diatur ke ${lajuBelajar}.`);
lajuBelajarSebelumnya = lajuBelajar;
}
let benar = 0;
let jumlahGalat = 0;
// tiap entri
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];
// untuk menghitung akurasi
if (maksArg(transposisi(keluaran)[0]) == maksArg(Y[k])) {
benar ++;
}
// rambat mundur
let galat = kurang(keluaran, transposisi([Y[k]]));
jumlahGalat += transposisi(galat)[0].reduce((a, v) => a + v);
// 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));
}
}
}
const akurasi = benar / X.length;
const rerataGalat = jumlahGalat / X.length;
console.log(`Iterasi ${iter}/${maksIter} | Galat: ${rerataGalat} | Akurasi: ${benar}/${X.length} / ${akurasi}`);
}
return model;
}
/**
* Fungsi utama untuk memprediksi dengan MLP yang diberikan
*
* @param {MLP} model Model yang akan melakukan prediksi
* @param {number[][]} X Daftar masukan
* @returns {Object.<string,*>}
*/
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;
}
/** ======= FUNGSI-FUNGSI BANTUAN MATEMATIKA ======= **/
/**
* Fungsi bantuan untuk membuat matriks 2 dimensi dengan isi tertentu (seragam)
*
* @param {number} baris Jumlah baris
* @param {number} kolom Jumlah kolom
* @param {number} [nilai=0] Nilai tiap elemen matriks
* @returns {number} Matriks baru sesuai parameter yang diberikan
*/
function buatMatriks2D(baris, kolom, nilai) {
if (nilai == undefined) {
nilai = 0;
}
return Array.from(Array(baris), () => Array.from(Array(kolom), () => nilai));
}
/**
* Fungsi bantuan untuk membuat matriks 2 dimensi dengan isi acak
*
* @param {number} baris Jumlah baris
* @param {number} kolom Jumlah kolom
* @returns {number} Matriks baru sesuai parameter yang diberikan
*/
function buatMatriks2DAcak(baris, kolom) {
return Array.from(Array(baris), () => Array.from(Array(kolom), () => Math.random()));
}
/**
* Fungsi bantuan untuk melakukan transposisi matriks
*
* @param {number[][]} matriks Matriks yang akan ditransposisi
* @returns Hasil transposisi matriks
*/
function transposisi(matriks) {
let hasil = buatMatriks2D(matriks[0].length, matriks.length);
for (let i = 0; i < matriks.length; i ++) {
for (let j = 0; j < matriks[0].length; j ++) {
hasil[j][i] = matriks[i][j];
}
}
return hasil;
}
/**
* Fungsi bantuan untuk mengalikan beberapa nilai
*
* Nilai yang diberikan bisa berupa matriks ataupun skalar.
*
* @param {...number|number[][]} elemen Daftar yang akan dikalikan
* @returns {number|number[][]} Hasil perkalian
*/
function kali(...elemen) {
if (elemen.length == undefined || elemen.length <= 1) {
throw new Error("Hanya ada satu yang akan dikalikan.");
}
let hasil = elemen[0];
for (let q = 1; q < elemen.length; q ++) {
let ki = hasil;
let ka = elemen[q];
// keduanya skalar
if (ki.length == undefined && ka.length == undefined) {
hasil = ki * ka;
// keduanya matriks
} else if (ki.length >= 1 && ka.length >= 1) {
if (ki[0].length != ka.length) {
let kiy = ki.length;
let kix = ki[0].length;
let kay = ka.length;
let kax = ka[0].length;
throw new Error(`Ukuran matriks tidak sesuai: ${kiy}x${kix} dengan ${kay}x${kax}.`);
}
hasil = buatMatriks2D(ki.length, ka[0].length);
for (let i = 0; i < ki.length; i ++) {
for (let j = 0; j < ka[0].length; j ++) {
let jumlah = 0;
for (let k = 0; k < ka.length; k ++) {
jumlah += ki[i][k] * ka[k][j];
}
hasil[i][j] = jumlah;
}
}
// salah satunya matriks
} else {
let skalar = ki;
let matriks = ka;
if (ka.length == undefined) { // cukup periksa salah satunya
skalar = ka;
matriks = ki;
}
for (let i = 0; i < matriks.length; i ++) {
for (let j = 0; j < matriks[0].length; j ++) {
matriks[i][j] *= skalar;
}
}
hasil = matriks;
}
}
return hasil;
}
/**
* Fungsi bantuan untuk menambahkan dua matriks
*
* Kedua matriks harus berukuran sama.
*
* @param {number[][]} A Matriks A
* @param {number[][]} B Matriks B
* @returns {number[][]} Hasil pertambahan kedua matriks: A + B.
*/
function tambah(A, B) {
if (A.length != B.length || A[0].length != B[0].length) {
let Ay = A.length;
let Ax = A[0].length;
let By = B.length;
let Bx = B[0].length;
throw new Error(`Ukuran matriks tidak sama: ${Ay}x${Ax} dengan ${By}x${Bx}.`);
}
for (let i = 0; i < A.length; i ++) {
for (let j = 0; j < A[0].length; j ++) {
A[i][j] += B[i][j];
}
}
return A;
}
/**
* Fungsi bantuan untuk mengurangkan dua matriks
*
* Kedua matriks harus berukuran sama.
*
* @param {number[][]} A Matriks A
* @param {number[][]} B Matriks B
* @returns {number[][]} Hasil pengurangan matriks A oleh matriks B: A - B.
*/
function kurang(A, B) {
if (A.length != B.length || A[0].length != B[0].length) {
let Ay = A.length;
let Ax = A[0].length;
let By = B.length;
let Bx = B[0].length;
throw new Error(`Ukuran matriks tidak sama: ${Ay}x${Ax} dengan ${By}x${Bx}.`);
}
for (let i = 0; i < A.length; i ++) {
for (let j = 0; j < A[0].length; j ++) {
A[i][j] -= B[i][j];
}
}
return A;
}
/**
* Fungsi bantuan untuk mencari indeks yang memiliki nilai maksimum
*
* Jika ada banyak nilai maksimum yang sama, indeks paling kecil yang dipilih.
*
* @param {number[]} daftar Daftar yang akan diambil indeks dari maksimumnya
* @returns {number} Indeks yang memiliki nilai maksimum
*/
function maksArg(daftar) {
if (daftar.length == undefined) {
throw new Error("Masukan tidak berupa daftar.");
}
if (daftar.length == 0) {
throw new Error("Daftar yang diberikan kosong.");
}
let indeksMaks = 0;
for (let q = 1; q < daftar.length; q ++) {
if (daftar[q] > daftar[indeksMaks]) {
indeksMaks = q;
}
}
return indeksMaks;
}
/**
* Fungsi bantuan untuk melakukan rambatan maju
*
* @param {MLP} model Model yang digunakan
* @param {number[][]} X Daftar masukan
* @returns {number[][]} Daftar keluaran tiap lapisan (setelah lapisan masukan)
*/
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;
}
/**
* Daftar fungsi aktivasi
*
* @type {Object<string,Function>}
*/
let daftarGenAktivasi = {};
/**
* Daftar turunan fungsi aktivasi
*
* @type {Object<string,Function>}
*/
let daftarDerAktivasi = {};
/**
* Fungsi bantuan untuk menerapkan ReLU terhadap tiap elemen matriks
*
* @param {number[][]} x Matriks masukan
* @returns {number[][]} Matriks hasil pemetaan ReLU
*/
function genReLU(x) {
for (let i = 0; i < x.length; i ++) {
for (let j = 0; j < x[i].length; j ++) {
x[i][j] = Math.max(x[i][j], 0);
}
}
return x;
}
daftarGenAktivasi["relu"] = genReLU;
/**
* Fungsi bantuan untuk menerapkan turunan ReLU terhadap tiap elemen matriks
*
* @param {number[][]} x Matriks masukan
* @returns {number[][]} Matriks hasil pemetaan turunan ReLU
*/
function derReLU(x) {
for (let i = 0; i < x.length; i ++) {
for (let j = 0; j < x[i].length; j ++) {
x[i][j] = x[i][j] >= 0 ? 1 : 0;
}
}
return x;
}
daftarDerAktivasi["relu"] = derReLU;
/** ======= PROGRAM UTAMA ======= **/
/**
* Fungsi utama yang akan dijalankan pertama (lihat di bagian akhir)
*/
function utama() {
const ukuran = [4, 5, 3];
const aktivasi = ["relu", "relu"]; // hanya ada ReLU
const model = buatMLP(ukuran, aktivasi);
const lajuBelajarTetap = () => 0.001;
latih(model, X, Y, lajuBelajarTetap, 4);
console.log(prediksi(model, [X[0]])); //contoh prediksi
}
/** ======= EKSEKUSI PROGRAM ======= **/
// Contoh untuk set data Iris (lihat di bawah)
const X = [ // daftar masukan (n, w, h)
[5.1, 3.5, 1.4, 0.2]
// ...
]; // daftar masukan (n, w, h)
const Y = [ // daftar label (n, j)
[1, 0, 0]
// ...
]; // daftar label (n, j)
utama();
const X = [ // daftar masukan (n, w, h)
[5.1,3.5,1.4,0.2],
[4.9,3.0,1.4,0.2],
[4.7,3.2,1.3,0.2],
[4.6,3.1,1.5,0.2],
[5.0,3.6,1.4,0.2],
[5.4,3.9,1.7,0.4],
[4.6,3.4,1.4,0.3],
[5.0,3.4,1.5,0.2],
[4.4,2.9,1.4,0.2],
[4.9,3.1,1.5,0.1],
[5.4,3.7,1.5,0.2],
[4.8,3.4,1.6,0.2],
[4.8,3.0,1.4,0.1],
[4.3,3.0,1.1,0.1],
[5.8,4.0,1.2,0.2],
[5.7,4.4,1.5,0.4],
[5.4,3.9,1.3,0.4],
[5.1,3.5,1.4,0.3],
[5.7,3.8,1.7,0.3],
[5.1,3.8,1.5,0.3],
[5.4,3.4,1.7,0.2],
[5.1,3.7,1.5,0.4],
[4.6,3.6,1.0,0.2],
[5.1,3.3,1.7,0.5],
[4.8,3.4,1.9,0.2],
[5.0,3.0,1.6,0.2],
[5.0,3.4,1.6,0.4],
[5.2,3.5,1.5,0.2],
[5.2,3.4,1.4,0.2],
[4.7,3.2,1.6,0.2],
[4.8,3.1,1.6,0.2],
[5.4,3.4,1.5,0.4],
[5.2,4.1,1.5,0.1],
[5.5,4.2,1.4,0.2],
[4.9,3.1,1.5,0.1],
[5.0,3.2,1.2,0.2],
[5.5,3.5,1.3,0.2],
[4.9,3.1,1.5,0.1],
[4.4,3.0,1.3,0.2],
[5.1,3.4,1.5,0.2],
[5.0,3.5,1.3,0.3],
[4.5,2.3,1.3,0.3],
[4.4,3.2,1.3,0.2],
[5.0,3.5,1.6,0.6],
[5.1,3.8,1.9,0.4],
[4.8,3.0,1.4,0.3],
[5.1,3.8,1.6,0.2],
[4.6,3.2,1.4,0.2],
[5.3,3.7,1.5,0.2],
[5.0,3.3,1.4,0.2],
[7.0,3.2,4.7,1.4],
[6.4,3.2,4.5,1.5],
[6.9,3.1,4.9,1.5],
[5.5,2.3,4.0,1.3],
[6.5,2.8,4.6,1.5],
[5.7,2.8,4.5,1.3],
[6.3,3.3,4.7,1.6],
[4.9,2.4,3.3,1.0],
[6.6,2.9,4.6,1.3],
[5.2,2.7,3.9,1.4],
[5.0,2.0,3.5,1.0],
[5.9,3.0,4.2,1.5],
[6.0,2.2,4.0,1.0],
[6.1,2.9,4.7,1.4],
[5.6,2.9,3.6,1.3],
[6.7,3.1,4.4,1.4],
[5.6,3.0,4.5,1.5],
[5.8,2.7,4.1,1.0],
[6.2,2.2,4.5,1.5],
[5.6,2.5,3.9,1.1],
[5.9,3.2,4.8,1.8],
[6.1,2.8,4.0,1.3],
[6.3,2.5,4.9,1.5],
[6.1,2.8,4.7,1.2],
[6.4,2.9,4.3,1.3],
[6.6,3.0,4.4,1.4],
[6.8,2.8,4.8,1.4],
[6.7,3.0,5.0,1.7],
[6.0,2.9,4.5,1.5],
[5.7,2.6,3.5,1.0],
[5.5,2.4,3.8,1.1],
[5.5,2.4,3.7,1.0],
[5.8,2.7,3.9,1.2],
[6.0,2.7,5.1,1.6],
[5.4,3.0,4.5,1.5],
[6.0,3.4,4.5,1.6],
[6.7,3.1,4.7,1.5],
[6.3,2.3,4.4,1.3],
[5.6,3.0,4.1,1.3],
[5.5,2.5,4.0,1.3],
[5.5,2.6,4.4,1.2],
[6.1,3.0,4.6,1.4],
[5.8,2.6,4.0,1.2],
[5.0,2.3,3.3,1.0],
[5.6,2.7,4.2,1.3],
[5.7,3.0,4.2,1.2],
[5.7,2.9,4.2,1.3],
[6.2,2.9,4.3,1.3],
[5.1,2.5,3.0,1.1],
[5.7,2.8,4.1,1.3],
[6.3,3.3,6.0,2.5],
[5.8,2.7,5.1,1.9],
[7.1,3.0,5.9,2.1],
[6.3,2.9,5.6,1.8],
[6.5,3.0,5.8,2.2],
[7.6,3.0,6.6,2.1],
[4.9,2.5,4.5,1.7],
[7.3,2.9,6.3,1.8],
[6.7,2.5,5.8,1.8],
[7.2,3.6,6.1,2.5],
[6.5,3.2,5.1,2.0],
[6.4,2.7,5.3,1.9],
[6.8,3.0,5.5,2.1],
[5.7,2.5,5.0,2.0],
[5.8,2.8,5.1,2.4],
[6.4,3.2,5.3,2.3],
[6.5,3.0,5.5,1.8],
[7.7,3.8,6.7,2.2],
[7.7,2.6,6.9,2.3],
[6.0,2.2,5.0,1.5],
[6.9,3.2,5.7,2.3],
[5.6,2.8,4.9,2.0],
[7.7,2.8,6.7,2.0],
[6.3,2.7,4.9,1.8],
[6.7,3.3,5.7,2.1],
[7.2,3.2,6.0,1.8],
[6.2,2.8,4.8,1.8],
[6.1,3.0,4.9,1.8],
[6.4,2.8,5.6,2.1],
[7.2,3.0,5.8,1.6],
[7.4,2.8,6.1,1.9],
[7.9,3.8,6.4,2.0],
[6.4,2.8,5.6,2.2],
[6.3,2.8,5.1,1.5],
[6.1,2.6,5.6,1.4],
[7.7,3.0,6.1,2.3],
[6.3,3.4,5.6,2.4],
[6.4,3.1,5.5,1.8],
[6.0,3.0,4.8,1.8],
[6.9,3.1,5.4,2.1],
[6.7,3.1,5.6,2.4],
[6.9,3.1,5.1,2.3],
[5.8,2.7,5.1,1.9],
[6.8,3.2,5.9,2.3],
[6.7,3.3,5.7,2.5],
[6.7,3.0,5.2,2.3],
[6.3,2.5,5.0,1.9],
[6.5,3.0,5.2,2.0],
[6.2,3.4,5.4,2.3],
[5.9,3.0,5.1,1.8]
]; // daftar masukan (n, w, h)
const Y = [ // daftar label (n, j)
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1]
]; // daftar label (n, j)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment