Modul 6 Sains Data: Neural Network dengan Tensorflow dan Keras

Pengantar Neural Network dengan TensorFlow dan Keras”

Offline di Departemen Matematika
Published

April 28, 2025

Kembali ke Sains Data

Sekarang kita sudah masuk ke materi artificial neural network (ANN) atau biasa disebut neural network (NN), yang mendasari dunia deep learning.

Saat modul praktikum ini disusun (April 2024), ada dua framework utama untuk deep learning di Python, yaitu:

  1. TensorFlow: https://www.tensorflow.org/

    (dan Keras di dalamnya: https://keras.io/)

  2. PyTorch: https://pytorch.org/

Kedua framework ini bersaing. Umumnya, TensorFlow lebih sering digunakan di industri, sedangkan PyTorch lebih sering digunakan dalam riset/penelitian.

Di pertemuan kali ini, kita akan membahas TensorFlow, baik penggunaannya secara sendiri (pure TensorFlow, yaitu tanpa Keras) maupun dengan bantuan Keras. Kalau belum punya, instal terlebih dahulu:

pip install tensorflow

Keras terinstal bersama TensorFlow (karena Keras ada di dalamnya).

Lalu import:

import tensorflow as tf
from tensorflow import keras
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
tfversion = tf.__version__
print(tfversion)
2.18.0
kerasversion = keras.__version__
print(kerasversion)
3.8.0

Teori Neural Network

Overview:

  • Secara umum, suatu neural network terdiri dari sejumlah layer atau lapisan (minimal dua).

  • Layer pertama disebut input layer, dan layer terakhir disebut output layer.

  • Tiap layer terdiri dari sejumlah neuron, yang masing-masing bisa menyimpan suatu nilai.

  • Kecuali input layer, tiap neuron terhubung dengan sejumlah neuron di layer sebelumnya.

  • Tiap sambungan terdiri dari nilai weight (sebagai pengali), nilai bias (sebagai pergeseran), dan suatu “fungsi aktivasi” yang menghasilkan nilai untuk neuron tujuan.

  • Weight maupun bias disebut parameter dari neural network.

  • Proses training adalah terus-menerus memperbarui parameter hingga hasil prediksi neural network sudah cukup baik, dengan meminimumkan suatu loss function atau fungsi objektif (yang intinya menghitung error).

  • Suatu neural network bisa memiliki sejumlah layer, masing-masing dengan banyaknya neuron tertentu dan fungsi-fungsi aktivasi tertentu. Hal-hal itu disebut hyperparameter dari neural network. Suatu arsitektur adalah suatu pilihan/konfigurasi hyperparameter.

SLP: (Single-Layer) Perceptron

ANN paling pertama adalah perceptron (juga disebut SLP atau single-layer perceptron) yang dirancang oleh Frank Rosenblatt pada tahun 1957 (Géron, 2019). Ini adalah neural network yang paling sederhana, bahkan ini bisa disebut building block dari semua ANN (apabila diberi kebebasan untuk modifikasi). Konsep dasar neural network bisa kita pelajari di sini.

Sumber gambar: Aggarwal (2018) hal. 5

Perceptron hanya terdiri dari satu input layer dan satu output layer. Bahkan, aslinya hanya ada satu neuron di output layer.

Apabila dibutuhkan lebih dari satu neuron di output layer, itu bisa dianggap menggunakan lebih dari satu perceptron (yaitu menggunakan banyaknya perceptron sesuai banyaknya neuron di output layer), yang saling “ditumpuk”:

Sumber gambar: Goodfellow, et. al. (2016) hal. 337

Perhatikan bahwa, tiap neuron di layer asal terhubung dengan tiap neuron di layer tujuan. Layer tujuan seperti ini disebut dense (padat). Kebalikan dari dense adalah sparse.

Aslinya, fungsi aktivasi yang digunakan oleh perceptron adalah Heaviside step function \(H(v)\) yang mungkin kalian kenal dari mata kuliah PDB, atau juga disebut threshold activation function:

\[H(v) = \begin{cases} 1, & v \ge 0 \\ 0, & v < 0 \end{cases}\]

Sehingga, untuk output neuron ke-\(j\) yang disambung dari \(n\) input neuron, model perceptron bisa dirumuskan sebagai berikut:

\[y_j = H\left(\left(\sum_{i=1}^{n} w_{ij} x_i \right) + b_j\right)\]

dengan

  • \(x_i\) adalah nilai pada input neuron ke-\(i\)

  • \(y_j\) adalah nilai pada output neuron ke-\(j\)

  • \(w_{ij}\) adalah parameter weight untuk sambungan input neuron ke-\(i\) menuju output neuron ke-\(j\)

  • \(b_j\) adalah parameter bias untuk output neuron ke-\(j\)

Lebih umumnya,

\[y_j = \Phi\left(\left(\sum_{i=1}^{n} w_{ij} x_i \right) + b_j\right)\]

dengan \(\Phi(v)\) adalah sembarang fungsi aktivasi.

Note: seperti di gambar, sebenarnya bias juga bisa dianggap neuron istimewa yang nilai \(x_i\) nya selalu satu.

Biasanya, semua nilai di layer selanjutnya dihitung secara sekaligus menggunakan perkalian matriks, dengan perumusan:

\[\textbf{y} = \Phi\left(W^T \textbf{x} + \textbf{b}\right)\]

dengan \(\textbf{x} = [x_i]\), \(\textbf{y} = [y_j]\), dan \(\textbf{b} = [b_j]\) adalah vektor kolom, serta \(W = \left[w_{ij}\right]\) adalah matriks.

Itu untuk satu buah data training.

Bisa saja, beberapa data training diperhitungkan sekaligus. Caranya, vektor kolom \(\textbf{x}\) itu kita “lebarkan” ke samping sehingga menjadi matriks \(X = [x_{it}]\), sehingga data training ke-\(t\) ada di kolom ke-\(t\). Dengan demikian, output nya akan berupa matriks \(Y = [y_{jt}]\) dengan hasil untuk data training ke-\(t\) ada di kolom ke-\(t\). Selain itu, vektor \(\textbf{b}\) perlu diperluas menjadi matriks \(B\) dengan tiap kolom identik, dan fungsi aktivasi \(\Phi\) dihitung per kolom.

\[Y = \Phi\left(W^T X + B\right)\]

Kembali ke kasus satu buah data training. Biasanya, dataset disajikan dengan tiap fitur di kolom sendiri, tidak seperti perumusan kita sejauh ini dengan tiap fitur di baris tersendiri. Untuk menyesuaikan, kita bisa men-transpose semuanya:

\[\textbf{y} = \Phi\left(\textbf{x} W + \textbf{b}\right)\]

dengan \(\textbf{x} = [x_i]\), \(\textbf{y} = [y_j]\), dan \(\textbf{b} = [b_j]\) adalah vektor baris, serta \(W = \left[w_{ji}\right]\) adalah matriks berisi bobot untuk menyambung ke output neuron ke-\(j\) dari input neuron ke-\(i\).

MLP: Multilayer Perceptron

Konsep single-layer perceptron bisa diperumum menjadi multilayer perceptron atau neural network yang biasa kita kenal, dengan menambahkan beberapa layer di antara input layer dan output layer. Semua layer selain input layer dan output layer disebut hidden layer.

Sumber gambar: Aggarwal (2018) hal. 18

Konsep perhitungan antara tiap layer tetap sama, yaitu

\[\textbf{y} = \Phi\left(\textbf{w}^T \textbf{x} + \textbf{b}\right)\]

(versi vektor kolom), atau

\[\textbf{y} = \Phi\left(\textbf{x} W + \textbf{b}\right)\]

(versi vektor baris)

Fungsi Aktivasi

Sumber gambar: Aggarwal (2018) hal. 13

Beberapa fungsi aktivasi adalah (Aggarwal, 2018, hal. 12-13):

  1. “Linier” atau identitas

\[\Phi(v) = v\]

  1. Sign (fungsi tanda): \(\text{sign}(v)\) atau \(\text{sgn}(v)\)

\[ \Phi(v) = \text{sign}(v) = \begin{cases} 1, & v > 0 \\ 0, & v = 0 \\ -1, & v < 0 \end{cases} \]

  1. Sigmoid, terkadang dilambangkan \(\sigma(v)\) dan terkadang disebut fungsi aktivasi logistik

\[\Phi(v) = \frac{1}{1 + e^{-v}}\]

  1. (Soft) tanh: \(\tanh(v)\)

\[\Phi(v) = \frac{e^{2v} - 1}{e^{2v} + 1} = 2 * \text{sigmoid}(2v) - 1\]

  1. Rectified Linear Unit (ReLU)

\[\Phi(v) = \max\{v, 0\}\]

  1. Hard tanh

\[\Phi(v) = \max\{\min\{v, 1\}, -1\}\]

Fungsi aktivasi yang paling sering digunakan adalah ReLU, kecuali untuk output layer.

Untuk output layer, biasanya,

  • untuk regresi, banyaknya neuron sesuai banyaknya nilai prediksi (umumnya hanya satu), dan digunakan fungsi aktivasi linier

  • untuk klasifikasi multiclass (lebih dari dua kelas), biasanya banyaknya output neuron sesuai banyaknya kelas, dan digunakan fungsi aktivasi softmax sebagai berikut, agar output berupa peluang tiap kelas:

\[\Phi(\overline{v})_i = \frac{\exp(v_i)}{\sum_{j=1}^k \exp(v_j)}\]

  • untuk klasifikasi biner, hanya ada satu neuron di output layer, dan digunakan fungsi aktivasi sigmoid. (Keberadaan hanya satu output neuron lebih hemat daripada menggunakan dua output neuron)

Loss function

Misalkan \(y_i\) adalah nilai sebenarnya dan \(\hat{y}_i\) adalah hasil prediksi.

Untuk regresi, biasa digunakan MSE (mean squared error), juga disebut L2 loss:

\[\text{MSE}(y, \hat{y}) = \frac{1}{n} \sum_{i=1}^{n} \left( y_i - \hat{y}_i \right)^2\]

Untuk klasifikasi, biasa digunakan yang namanya cross-entropy loss, juga disebut logistic loss atau log loss:

\[L_{\text{log}}(y,\hat{y}) = -(y \ln (\hat{y}) + (1 - y) \ln (1 - \hat{y}))\]

Proses training

Proses training untuk neural network dilakukan secara iteratif, yaitu tiap iterasi akan memperbarui parameter sehingga nilai loss function menjadi lebih kecil.

Tiap iterasi melakukan langkah-langkah berikut untuk tiap data training:

  1. Forward pass: menghitung nilai output akhir, yaitu \(\hat{y}\) (hasil prediksi), berdasarkan input data training.

  2. Menghitung loss antara \(y\) (nilai asli) dan \(\hat{y}\)

  3. Backpropagation: menghitung gradien dari loss terhadap tiap parameter, secara “mundur”

  4. Update optimizer: menggunakan algoritma optimizer seperti gradient descent untuk memperbarui parameter-parameter (weights and biases) berdasarkan gradien dari loss

    Note: ada banyak optimizer, seperti gradient descent, SGD (stochastic gradient descent), dan Adam (adaptive moment estimation). Pilihan optimizer (serta parameter-parameter yang bisa diatur untuk optimizer, seperti learning rate) juga menjadi hyperparameter untuk neural network.

Note: istilah backward pass meliputi langkah backpropagation dan update optimizer.

Apabila data training sangat banyak, terkadang data training tersebut dibagi menjadi beberapa batch, dan tiap iterasi menggunakan batch yang berbeda. Apabila semua batch sudah diproses, sebutannya adalah satu epoch. Sehingga, satu epoch terdiri dari sejumlah iterasi sesuai banyaknya batch.

(Apabila data training tidak dibagi menjadi batch, maka satu epoch sama dengan satu iterasi.)

Contoh optimizer: metode gradient descent

Metode gradient descent mencari minimum lokal dari suatu fungsi \(g\) (dalam hal ini, loss function) dengan rumus iterasi seperti berikut:

\[\textbf{x}_{i+1} = \textbf{x}_i - \eta \nabla g\left(\textbf{x}_i\right)\]

dengan \(\eta\) adalah learning rate. Simbol nabla (\(\nabla\)) menandakan perhitungan gradien.

Perhatikan bahwa gradien menandakan arah tercepat untuk kenaikan fungsi, seringkali disebut direction of steepest ascent. Di sini, justru kita mengurangi; atau sama saja, menambah dengan kebalikannya, yaitu arah tercepat untuk penurunan fungsi. Sedangkan, learning rate melambangkan seberapa jauh kita melangkah ke arah penurunan tersebut. Harapannya, kita akan cepat konvergen menuju minimum fungsi, karena kita terus melangkah ke arah penurunan tercepat.

Variasi gradient descent adalah SGD (stochastic gradient descent). Bedanya sederhana saja:

  • Gradient descent selalu memanfaatkan keseluruhan data training yang diberikan (lebih tepatnya, keseluruhan batch) di tiap iterasi.

  • Sedangkan, SGD selalu memilih sebagian data training saja (lebih tepatnya, sebagian dari batch), dan cara memilihnya bersifat random atau disebut stokastik.

Keuntungan SGD dibandingkan gradient descent biasa:

Train-Validation-Test Split

Ketika menggunakan metode machine learning yang di-training secara iteratif, seperti neural network, biasanya ada juga yang namanya validation data. Sehingga, di awal, dataset dipisah menjadi data train, data validation, dan data test.

Gunanya, kita bisa menguji akurasi model di akhir tiap epoch, menggunakan data validation daripada data test.

Rasio yang paling sering digunakan adalah 80-10-10, yaitu 80% data train, 10% data validation, dan 10% data test.

Apabila menggunakan scikit-learn, untuk melakukan train-validation-test split, caranya adalah dengan split dua kali, yaitu

  1. Split menjadi data “train” dan data test

  2. Data “train” itu di-split lagi menjadi data train sesungguhnya dan data validation

atau bisa juga

  1. Split menjadi data train dan data “test”

  2. Data “test” itu di-split lagi menjadi data validation dan data test sesungguhnya

Mengenal TensorFlow

import tensorflow as tf

Tensor, Konstanta

Tensor adalah semacam perumuman dari array/vektor ataupun matriks.

  • Skalar (bilangan) adalah tensor berdimensi nol (atau rank nol).

  • Array atau vektor adalah tensor berdimensi satu (atau rank satu).

  • Matriks adalah tensor berdimensi dua (atau rank dua).

  • Istilah “tensor” biasanya merujuk pada tensor berdimensi tiga (atau rank tiga), yaitu semacam matriks tapi tiga dimensi, sehingga ada baris, kolom, dan satu dimensi lagi.

Fitur tensor di TensorFlow mirip dengan fitur array di numpy, yang memang juga bisa multidimensi.

x = tf.zeros(shape = (3,4))
print(x)
tf.Tensor(
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]], shape=(3, 4), dtype=float32)
x = tf.ones(shape = (3,4))
print(x)
tf.Tensor(
[[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]], shape=(3, 4), dtype=float32)

Untuk menentukan array kita sendiri, di numpy digunakan numpy.array.

Untuk menentukan tensor kita sendiri, di TensorFlow digunakan tensorflow.constant (agar nilainya tidak bisa diubah) atau tensorflow.Variable (nilainya bisa diubah).

Pada umumnya (apabila tidak ada keterangan), tensor di TensorFlow berupa tensorflow.constant

const0 = tf.constant(1.5)
print(const0)
tf.Tensor(1.5, shape=(), dtype=float32)
print(tf.rank(const0))
tf.Tensor(0, shape=(), dtype=int32)
const1 = tf.constant([2.31, 4.567, 8.9])
print(const1)
tf.Tensor([2.31  4.567 8.9  ], shape=(3,), dtype=float32)
print(tf.rank(const1))
tf.Tensor(1, shape=(), dtype=int32)
const1[0] = 52.5
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-6babe8920968> in <cell line: 0>()
----> 1 const1[0] = 52.5

TypeError: 'tensorflow.python.framework.ops.EagerTensor' object does not support item assignment
const2 = tf.constant([
    [1, 2.718, 3.14],
    [4, 5, 6.28]
])
print(const2)
tf.Tensor(
[[1.    2.718 3.14 ]
 [4.    5.    6.28 ]], shape=(2, 3), dtype=float32)
print(tf.rank(const2))
tf.Tensor(2, shape=(), dtype=int32)

Variabel dan assignment untuk tensor

v = tf.Variable(initial_value = tf.zeros(shape = (2,3)))
print(v)
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[0., 0., 0.],
       [0., 0., 0.]], dtype=float32)>

Assignment untuk variabel di TensorFlow dilakukan dengan .assign

v.assign(tf.ones(shape = (2,3)))
print(v)
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[1., 1., 1.],
       [1., 1., 1.]], dtype=float32)>
v[0, 0].assign(9)
print(v)
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[9., 1., 1.],
       [1., 1., 1.]], dtype=float32)>

Ada juga .assign_add, sama saja dengan +=

v.assign_add(tf.ones(shape = (2,3)))
print(v)
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[10.,  2.,  2.],
       [ 2.,  2.,  2.]], dtype=float32)>

Serupa, ada .assign_sub yaitu -=

v.assign_sub(tf.ones(shape = (2,3)))
print(v)
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[9., 1., 1.],
       [1., 1., 1.]], dtype=float32)>

Tensor random

Kita bisa membuat tensor dengan nilai yang random, misalnya dari distribusi normal atau dari distribusi uniform

# dari distribusi normal
x = tf.random.normal(shape = (2,3), mean = 0, stddev = 1)
print(x)
tf.Tensor(
[[ 0.1701715  -1.5403947   0.5286183 ]
 [-0.6036802   0.27208146 -1.4331723 ]], shape=(2, 3), dtype=float32)
# dari distribusi uniform
x = tf.random.uniform(shape = (2,3), minval = 0, maxval = 1)
print(x)
tf.Tensor(
[[0.81854975 0.95575297 0.9605453 ]
 [0.36875296 0.45838344 0.62123   ]], shape=(2, 3), dtype=float32)

Operasi TensorFlow seperti numpy

Operasi di TensorFlow mirip dengan numpy

a = 4 * tf.ones((2, 2))
print(a)
tf.Tensor(
[[4. 4.]
 [4. 4.]], shape=(2, 2), dtype=float32)
b = tf.square(a)
print(b)
tf.Tensor(
[[16. 16.]
 [16. 16.]], shape=(2, 2), dtype=float32)
c = tf.sqrt(a)
print(c)
tf.Tensor(
[[2. 2.]
 [2. 2.]], shape=(2, 2), dtype=float32)
d = b + c
print(d)
tf.Tensor(
[[18. 18.]
 [18. 18.]], shape=(2, 2), dtype=float32)
# perkalian matriks
e = tf.matmul(a, c)
print(e)
tf.Tensor(
[[16. 16.]
 [16. 16.]], shape=(2, 2), dtype=float32)
# perkalian per elemen
e *= d
print(e)
tf.Tensor(
[[288. 288.]
 [288. 288.]], shape=(2, 2), dtype=float32)

Automatic differentiation dengan GradientTape

TensorFlow memiliki fitur yang bernama automatic differentiation, juga disebut autodiff atau autograd. Dengan fitur ini, TensorFlow bisa menghitung turunan/gradien secara otomatis. Fitur ini membedakan antara TensorFlow dengan numpy.

Caranya adalah menggunakan GradientTape seperti berikut. Semua operasi di dalam with statement dicatat oleh GradientTape, yang kemudian bisa menghitung gradiennya.

Contohnya, turunan \(x^3\) terhadap \(x\) di \(x=4\) adalah \(3(4)^2 = 48\).

x = tf.Variable(4.0)
with tf.GradientTape() as tape:
    y = x ** 3
dy_dx = tape.gradient(y, x)
print(dy_dx)
tf.Tensor(48.0, shape=(), dtype=float32)

Tidak harus dengan tensorflow.Variable, bahkan dengan tensorflow.constant juga bisa. Namun, kita harus secara eksplisit meminta TensorFlow untuk memperhatikan nilai x, yaitu dengan .watch

x = tf.constant(4.0)
with tf.GradientTape() as tape:
    tape.watch(x)
    y = x ** 3
dy_dx = tape.gradient(y, x)
print(dy_dx)
tf.Tensor(48.0, shape=(), dtype=float32)

Kita bisa menghitung turunan kedua dengan nested with statement seperti berikut, contohnya turunan kedua dari \(x^3\) terhadap \(x\) di \(x=4\) adalah \(6(4) = 24\)

x = tf.Variable(4.0)
with tf.GradientTape() as tape2:
    with tf.GradientTape() as tape1:
        y = x ** 3
    dy_dx = tape1.gradient(y, x)
dy2_dx2 = tape2.gradient(dy_dx, x)
print(dy2_dx2)
tf.Tensor(24.0, shape=(), dtype=float32)

(Pure) TensorFlow: klasifikasi biner dengan perceptron

Perceptron digunakan untuk klasifikasi biner. Mari kita coba buat model perceptron dengan pure TensorFlow, menggunakannya untuk memprediksi kelas dari titik-titik dua dimensi.

Generate dataset

Dataset titik-titik dua dimensi, dengan dua kelas (misalnya “negatif” dan “positif”), bisa kita generate:

num_samples_per_class, num_classes = 1000, 2
negative_samples = np.random.multivariate_normal(mean = [0,3], cov = [[1,0.5],[0.5,1]], size = num_samples_per_class)
positive_samples = np.random.multivariate_normal(mean = [3,0], cov = [[1,0.5],[0.5,1]], size = num_samples_per_class)

inputs = np.vstack((negative_samples, positive_samples)).astype(np.float32)
targets = np.vstack((
    np.zeros((num_samples_per_class, 1), dtype = 'float32'),
    np.ones((num_samples_per_class, 1), dtype = 'float32')
))
print(inputs.shape)
print(targets.shape)
(2000, 2)
(2000, 1)
plt.scatter(inputs[:, 0], inputs[:, 1], c=targets[:, 0])
plt.show()

Kalau mau, kita bisa susun data ini ke dalam bentuk pandas DataFrame, lalu export ke CSV:

titik_negatif_positif_df = pd.DataFrame(
    np.hstack([inputs, targets]),
    columns = ["x", "y", "kelas"]
)
titik_negatif_positif_df
x y kelas
0 -0.109873 2.904561 0.0
1 -0.070282 3.484931 0.0
2 -0.556087 3.335495 0.0
3 -0.314094 3.379053 0.0
4 -0.636380 2.201054 0.0
... ... ... ...
1995 3.338591 0.450919 1.0
1996 3.168565 0.507815 1.0
1997 1.506407 -0.884150 1.0
1998 1.901459 -0.520556 1.0
1999 1.771375 -0.421099 1.0

2000 rows × 3 columns

titik_negatif_positif_df.to_csv("./titik_negatif_positif.csv", index=False)

Import kembali dataset

Tentunya, karena titik-titiknya di-generate secara random, mungkin saja titik-titik yang kalian peroleh akan sedikit berbeda, bahkan tiap kali di-run ulang akan berbeda.

Kalau kalian mau menyamakan dengan modul ini, CSV nya bisa di-download dari GitHub Pages ini: titik_negatif_positif.csv

Kita bisa import kembali:

df = pd.read_csv("./titik_negatif_positif.csv", dtype="float32")

Kali ini, kita tambahkan keterangan dtype="float32". Ini penting, karena TensorFlow biasanya menangani float32 (yaitu tipe data float dengan penyimpanan 32-bit), bukan float64 yang biasa digunakan oleh pandas.

df
x y kelas
0 -0.109873 2.904561 0.0
1 -0.070282 3.484931 0.0
2 -0.556087 3.335495 0.0
3 -0.314094 3.379053 0.0
4 -0.636380 2.201054 0.0
... ... ... ...
1995 3.338591 0.450919 1.0
1996 3.168565 0.507815 1.0
1997 1.506407 -0.884150 1.0
1998 1.901459 -0.520556 1.0
1999 1.771375 -0.421099 1.0

2000 rows × 3 columns

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000 entries, 0 to 1999
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   x       2000 non-null   float32
 1   y       2000 non-null   float32
 2   kelas   2000 non-null   float32
dtypes: float32(3)
memory usage: 23.6 KB
inputs_df = df.drop(columns=["kelas"])
targets_df = df[["kelas"]]
inputs_df
x y
0 -0.109873 2.904561
1 -0.070282 3.484931
2 -0.556087 3.335495
3 -0.314094 3.379053
4 -0.636380 2.201054
... ... ...
1995 3.338591 0.450919
1996 3.168565 0.507815
1997 1.506407 -0.884150
1998 1.901459 -0.520556
1999 1.771375 -0.421099

2000 rows × 2 columns

targets_df
kelas
0 0.0
1 0.0
2 0.0
3 0.0
4 0.0
... ...
1995 1.0
1996 1.0
1997 1.0
1998 1.0
1999 1.0

2000 rows × 1 columns

plt.scatter(inputs_df["x"], inputs_df["y"], c=targets_df["kelas"])
plt.show()

TensorFlow kurang bisa menangani pandas DataFrame, sehingga harus kita ubah jadi array numpy:

inputs = inputs_df.to_numpy()
targets = targets_df.to_numpy()
print(inputs.shape)
print(targets.shape)
(2000, 2)
(2000, 1)

Menyusun model dan training

Untuk input dua dimensi dan klasifikasi biner, kita perlu perceptron dengan dua neuron di input layer dan satu neuron di output layer. Sebelum proses training dimulai, nilai matriks \(W\) dan vektor kolom \(b\) diisi secara random terlebih dahulu.

input_dim = 2
output_dim = 1
W = tf.Variable(tf.random.normal(shape = (input_dim, output_dim)))
b = tf.Variable(tf.random.normal(shape = (output_dim,)))
# forward pass
def model(inputs):
    return tf.sigmoid(
        tf.matmul(inputs, W) + b
    )
# cross entropy loss
def entropy_loss(y, yhat):
    per_sample_losses = - y * tf.math.log(yhat) - (1-y) * tf.math.log(1-yhat)
    return tf.reduce_mean(per_sample_losses)
# satu epoch di training loop
learning_rate = 0.1
def training_step(inputs, targets):
    with tf.GradientTape() as tape:
        predictions = model(inputs)
        loss = entropy_loss(targets, predictions)

        grad_loss_wrt_W, grad_loss_wrt_b = tape.gradient(loss, [W, b])

        # update menggunakan gradient descent
        W.assign_sub(learning_rate * grad_loss_wrt_W)
        b.assign_sub(learning_rate * grad_loss_wrt_b)

        return loss
# training loop
for epoch in range(100):
    loss = training_step(inputs, targets)
    print(f"Loss at epoch {epoch}: {loss}")
Loss at epoch 0: 0.37649667263031006
Loss at epoch 1: 0.33892661333084106
Loss at epoch 2: 0.3085114061832428
Loss at epoch 3: 0.28344419598579407
Loss at epoch 4: 0.2624468505382538
Loss at epoch 5: 0.24460428953170776
Loss at epoch 6: 0.2292504459619522
Loss at epoch 7: 0.21589185297489166
Loss at epoch 8: 0.2041565477848053
Loss at epoch 9: 0.19375956058502197
Loss at epoch 10: 0.1844790279865265
Loss at epoch 11: 0.1761399507522583
Loss at epoch 12: 0.1686023324728012
Loss at epoch 13: 0.16175292432308197
Loss at epoch 14: 0.15549910068511963
Loss at epoch 15: 0.14976432919502258
Loss at epoch 16: 0.14448483288288116
Loss at epoch 17: 0.13960705697536469
Loss at epoch 18: 0.13508561253547668
Loss at epoch 19: 0.13088177144527435
Loss at epoch 20: 0.1269623339176178
Loss at epoch 21: 0.12329864501953125
Loss at epoch 22: 0.119865782558918
Loss at epoch 23: 0.1166420504450798
Loss at epoch 24: 0.11360841244459152
Loss at epoch 25: 0.11074810475111008
Loss at epoch 26: 0.10804630815982819
Loss at epoch 27: 0.10548984259366989
Loss at epoch 28: 0.10306701809167862
Loss at epoch 29: 0.1007673367857933
Loss at epoch 30: 0.09858141094446182
Loss at epoch 31: 0.09650078415870667
Loss at epoch 32: 0.09451782703399658
Loss at epoch 33: 0.09262565523386002
Loss at epoch 34: 0.09081799536943436
Loss at epoch 35: 0.08908917009830475
Loss at epoch 36: 0.08743401616811752
Loss at epoch 37: 0.08584779500961304
Loss at epoch 38: 0.08432614803314209
Loss at epoch 39: 0.08286512643098831
Loss at epoch 40: 0.08146108686923981
Loss at epoch 41: 0.08011066168546677
Loss at epoch 42: 0.07881075888872147
Loss at epoch 43: 0.07755852490663528
Loss at epoch 44: 0.07635130733251572
Loss at epoch 45: 0.07518665492534637
Loss at epoch 46: 0.0740623027086258
Loss at epoch 47: 0.07297613471746445
Loss at epoch 48: 0.07192617654800415
Loss at epoch 49: 0.07091060280799866
Loss at epoch 50: 0.06992770731449127
Loss at epoch 51: 0.06897588819265366
Loss at epoch 52: 0.06805365532636642
Loss at epoch 53: 0.06715962290763855
Loss at epoch 54: 0.06629245728254318
Loss at epoch 55: 0.06545095145702362
Loss at epoch 56: 0.06463393568992615
Loss at epoch 57: 0.06384031474590302
Loss at epoch 58: 0.06306910514831543
Loss at epoch 59: 0.062319304794073105
Loss at epoch 60: 0.06159002706408501
Loss at epoch 61: 0.06088041514158249
Loss at epoch 62: 0.060189660638570786
Loss at epoch 63: 0.05951699614524841
Loss at epoch 64: 0.05886170268058777
Loss at epoch 65: 0.058223091065883636
Loss at epoch 66: 0.057600509375333786
Loss at epoch 67: 0.056993357837200165
Loss at epoch 68: 0.05640103667974472
Loss at epoch 69: 0.055823005735874176
Loss at epoch 70: 0.05525872856378555
Loss at epoch 71: 0.05470770224928856
Loss at epoch 72: 0.05416945740580559
Loss at epoch 73: 0.05364353582262993
Loss at epoch 74: 0.053129516541957855
Loss at epoch 75: 0.05262697488069534
Loss at epoch 76: 0.05213551968336105
Loss at epoch 77: 0.05165477842092514
Loss at epoch 78: 0.05118439346551895
Loss at epoch 79: 0.050724029541015625
Loss at epoch 80: 0.050273347645998
Loss at epoch 81: 0.049832046031951904
Loss at epoch 82: 0.049399811774492264
Loss at epoch 83: 0.04897637292742729
Loss at epoch 84: 0.048561446368694305
Loss at epoch 85: 0.04815478250384331
Loss at epoch 86: 0.04775610938668251
Loss at epoch 87: 0.047365207225084305
Loss at epoch 88: 0.0469818189740181
Loss at epoch 89: 0.04660574346780777
Loss at epoch 90: 0.04623674973845482
Loss at epoch 91: 0.04587464779615402
Loss at epoch 92: 0.045519232749938965
Loss at epoch 93: 0.045170318335294724
Loss at epoch 94: 0.04482771083712578
Loss at epoch 95: 0.044491250067949295
Loss at epoch 96: 0.04416074976325035
Loss at epoch 97: 0.043836068361997604
Loss at epoch 98: 0.04351703077554703
Loss at epoch 99: 0.04320349544286728

Prediksi

Sekarang training sudah selesai, kita bisa gunakan model kita untuk memprediksi kelas berdasarkan inputs (koordinat titik-titik)

predictions = model(inputs)

Akibat penggunaan fungsi aktivasi sigmoid, hasil prediksi cukup jelas, apakah kelas pertama (kelas 0) atau kelas kedua (kelas 1):

print(predictions)
tf.Tensor(
[[0.01171741]
 [0.00699979]
 [0.00357984]
 ...
 [0.8950965 ]
 [0.9203457 ]
 [0.8933534 ]], shape=(2000, 1), dtype=float32)

Kita bisa menampilkan hasil prediksi ini dengan aturan pemilihan warna (c) seperti berikut:

  • apabila nilai prediksinya lebih dari 0.5 (pernyataan “lebih besar dari 0.5” bernilai benar), ia tergolong kelas 1 (atau sama saja nilai True);

  • selain itu (pernyataan “lebih besar dari 0.5” bernilai salah), ia tergolong kelas 0 (atau sama saja nilai False).

plt.scatter(inputs[:, 0], inputs[:, 1], c=predictions[:, 0] > 0.5)
plt.show()

Kinerja perceptron cukup mirip regresi logistik, ataupun SVM dengan kernel linier. Perhatikan bahwa, di hasil prediksi ini, seolah-olah ada perbatasan atau garis pemisah antara kedua kelas. Kalau kita bandingkan dengan data aslinya, sebenarnya ada beberapa titik yang melewati perbatasan tersebut, dan akhirnya terjadi misklasifikasi.

Mengenal Keras dengan Sequential API

Dengan pure TensorFlow, banyak hal yang harus kita susun secara manual. Untuk neural network kecil seperti perceptron, mungkin tidak masalah. Namun, neural network pada umumnya sangat “dalam” atau deep, dengan puluhan hidden layer yang bervariasi.

Daripada benar-benar membuatnya semua secara manual, ada yang namanya Keras yang sangat menyederhanakan proses penyusunan neural network. Biasanya, daripada benar-benar membuat neural network secara manual dalam pure TensorFlow seperti tadi, pengguna TensorFlow memanfaatkan Keras.

Keras tersedia di dalam TensorFlow:

from tensorflow import keras

Perlu dicatat, ketika menggunakan Keras, sebaiknya semua fungsi/operasi yang kita gunakan juga dari dalam Keras daripada langsung dari TensorFlow. Misalnya, daripada tf.matmul, gunakan keras.ops.matmul

Tapi kalau error, tidak masalah masih menggunakan tf karena Keras masih dalam pengembangan (menuju Keras versi 3, bisa dibaca di sini: https://keras.io/guides/migrating_to_keras_3/). Mungkin, di versi yang akan datang, sudah tidak error lagi.

Dalam Keras, ada tiga “cara” atau API (application programming interface) yang bisa kita gunakan untuk menyusun neural network, yaitu

  1. Sequential API

  2. Functional API

  3. Subclassing API (yaitu dengan OOP)

Di pertemuan kali ini, kita akan mencoba cara yang paling sederhana, yaitu dengan Sequential API.

Datanya sudah siap dari yang tadi:

print(inputs.shape)
print(targets.shape)
(2000, 2)
(2000, 1)

Menyusun layer

Kita susun layer nya terlebih dahulu. Kali ini, kita akan membuat perceptron seperti yang cara manual / pure TensorFlow tadi. Untuk itu, kedua kode ini ekuivalen:

# langsung menentukan semua layer di awal, dengan memasukkan list
model2 = keras.Sequential(
    [
        keras.layers.InputLayer(input_shape = (2,)),
        keras.layers.Dense(units = 1, activation = 'sigmoid')
    ]
)
/usr/local/lib/python3.11/dist-packages/keras/src/layers/core/input_layer.py:27: UserWarning: Argument `input_shape` is deprecated. Use `shape` instead.
  warnings.warn(
# menambahkan layer secara berangsur-angsur
model2 = keras.Sequential()
model2.add(keras.layers.InputLayer(input_shape = (2,)))
model2.add(keras.layers.Dense(units = 1, activation = 'sigmoid'))

Daripada menggunakan string, untuk menentukan fungsi aktivasi di kedua cara di atas, kita juga bisa mengetik keras.activations.sigmoid seperti berikut:

# langsung menentukan semua layer di awal, dengan memasukkan list
model2 = keras.Sequential(
    [
        keras.layers.InputLayer(input_shape = (2,)),
        keras.layers.Dense(units = 1, activation = keras.activations.sigmoid)
    ]
)
# menambahkan layer secara berangsur-angsur
model2 = keras.Sequential()
model2.add(keras.layers.InputLayer(input_shape = (2,)))
model2.add(keras.layers.Dense(units = 1, activation = keras.activations.sigmoid))

Ringkasan dan diagram model

Kemudian, kita bisa melihat ringkasan bentuk model yang dihasilkan:

model2.summary()
Model: "sequential_2"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ dense_2 (Dense)                 │ (None, 1)              │             3 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 3 (12.00 B)
 Trainable params: 3 (12.00 B)
 Non-trainable params: 0 (0.00 B)

Kita juga bisa menampilkan semacam diagram, bahkan menyimpannya ke dalam file:

keras.utils.plot_model(
    model2,
    show_shapes = True,
    show_layer_activations = True,
    to_file = "keras_sequential_model2.png"
)

Memilih hyperparameter

Untuk memilih hyperparameter yaitu optimizer dan loss function (dan metrik evaluasi), kedua kode berikut ini ekuivalen:

# dengan string
model2.compile(
    optimizer = "sgd",
    loss = "binary_crossentropy",
    metrics = ["binary_accuracy"]
)
# dengan objek dari class
model2.compile(
    optimizer = keras.optimizers.SGD(),
    loss = keras.losses.BinaryCrossentropy(),
    metrics = [keras.metrics.BinaryAccuracy()]
)

Dengan cara yang kedua, kita juga bisa menentukan hyperparameter seperti learning rate:

# dengan objek dari class
model2.compile(
    optimizer = keras.optimizers.SGD(learning_rate = 0.01),
    loss = keras.losses.BinaryCrossentropy(),
    metrics = [keras.metrics.BinaryAccuracy()]
)

Training

Selanjutnya, tinggal training, menggunakan .fit seperti di scikit-learn. Bedanya, .fit di sini me-return suatu objek “history” yang berisi catatan loss di tiap epoch

x_train = inputs
y_train = targets
history2 = model2.fit(x_train, y_train, epochs=100, validation_split=0.2)
Epoch 1/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 1s 7ms/step - binary_accuracy: 0.9829 - loss: 0.1602 - val_binary_accuracy: 0.9775 - val_loss: 0.2491

Epoch 2/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9909 - loss: 0.1308 - val_binary_accuracy: 0.9875 - val_loss: 0.1955

Epoch 3/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9930 - loss: 0.1074 - val_binary_accuracy: 0.9925 - val_loss: 0.1616

Epoch 4/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9975 - loss: 0.0915 - val_binary_accuracy: 0.9950 - val_loss: 0.1385

Epoch 5/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9981 - loss: 0.0834 - val_binary_accuracy: 1.0000 - val_loss: 0.1218

Epoch 6/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9963 - loss: 0.0775 - val_binary_accuracy: 1.0000 - val_loss: 0.1092

Epoch 7/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9953 - loss: 0.0732 - val_binary_accuracy: 1.0000 - val_loss: 0.0993

Epoch 8/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9981 - loss: 0.0700 - val_binary_accuracy: 1.0000 - val_loss: 0.0914

Epoch 9/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9993 - loss: 0.0607 - val_binary_accuracy: 1.0000 - val_loss: 0.0847

Epoch 10/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9990 - loss: 0.0581 - val_binary_accuracy: 1.0000 - val_loss: 0.0792

Epoch 11/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9981 - loss: 0.0590 - val_binary_accuracy: 1.0000 - val_loss: 0.0744

Epoch 12/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9994 - loss: 0.0585 - val_binary_accuracy: 1.0000 - val_loss: 0.0703

Epoch 13/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9999 - loss: 0.0521 - val_binary_accuracy: 1.0000 - val_loss: 0.0667

Epoch 14/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9987 - loss: 0.0479 - val_binary_accuracy: 1.0000 - val_loss: 0.0635

Epoch 15/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9990 - loss: 0.0466 - val_binary_accuracy: 1.0000 - val_loss: 0.0607

Epoch 16/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9997 - loss: 0.0442 - val_binary_accuracy: 1.0000 - val_loss: 0.0582

Epoch 17/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9995 - loss: 0.0446 - val_binary_accuracy: 1.0000 - val_loss: 0.0559

Epoch 18/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9993 - loss: 0.0423 - val_binary_accuracy: 1.0000 - val_loss: 0.0538

Epoch 19/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9993 - loss: 0.0410 - val_binary_accuracy: 1.0000 - val_loss: 0.0519

Epoch 20/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9994 - loss: 0.0414 - val_binary_accuracy: 1.0000 - val_loss: 0.0501

Epoch 21/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9999 - loss: 0.0396 - val_binary_accuracy: 1.0000 - val_loss: 0.0485

Epoch 22/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9995 - loss: 0.0345 - val_binary_accuracy: 1.0000 - val_loss: 0.0470

Epoch 23/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9998 - loss: 0.0372 - val_binary_accuracy: 1.0000 - val_loss: 0.0456

Epoch 24/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9998 - loss: 0.0358 - val_binary_accuracy: 1.0000 - val_loss: 0.0443

Epoch 25/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9995 - loss: 0.0342 - val_binary_accuracy: 1.0000 - val_loss: 0.0431

Epoch 26/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - binary_accuracy: 0.9999 - loss: 0.0330 - val_binary_accuracy: 1.0000 - val_loss: 0.0420

Epoch 27/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - binary_accuracy: 0.9999 - loss: 0.0319 - val_binary_accuracy: 1.0000 - val_loss: 0.0409

Epoch 28/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - binary_accuracy: 0.9999 - loss: 0.0316 - val_binary_accuracy: 1.0000 - val_loss: 0.0399

Epoch 29/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - binary_accuracy: 0.9991 - loss: 0.0316 - val_binary_accuracy: 1.0000 - val_loss: 0.0390

Epoch 30/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - binary_accuracy: 0.9999 - loss: 0.0280 - val_binary_accuracy: 1.0000 - val_loss: 0.0381

Epoch 31/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 7ms/step - binary_accuracy: 0.9996 - loss: 0.0302 - val_binary_accuracy: 1.0000 - val_loss: 0.0372

Epoch 32/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9985 - loss: 0.0287 - val_binary_accuracy: 1.0000 - val_loss: 0.0364

Epoch 33/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9994 - loss: 0.0277 - val_binary_accuracy: 1.0000 - val_loss: 0.0357

Epoch 34/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9995 - loss: 0.0282 - val_binary_accuracy: 1.0000 - val_loss: 0.0349

Epoch 35/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9990 - loss: 0.0257 - val_binary_accuracy: 1.0000 - val_loss: 0.0342

Epoch 36/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9998 - loss: 0.0273 - val_binary_accuracy: 1.0000 - val_loss: 0.0336

Epoch 37/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9993 - loss: 0.0269 - val_binary_accuracy: 1.0000 - val_loss: 0.0330

Epoch 38/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9987 - loss: 0.0259 - val_binary_accuracy: 1.0000 - val_loss: 0.0324

Epoch 39/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9994 - loss: 0.0287 - val_binary_accuracy: 1.0000 - val_loss: 0.0318

Epoch 40/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9990 - loss: 0.0263 - val_binary_accuracy: 1.0000 - val_loss: 0.0312

Epoch 41/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9972 - loss: 0.0269 - val_binary_accuracy: 1.0000 - val_loss: 0.0307

Epoch 42/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9995 - loss: 0.0229 - val_binary_accuracy: 1.0000 - val_loss: 0.0302

Epoch 43/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9998 - loss: 0.0241 - val_binary_accuracy: 1.0000 - val_loss: 0.0297

Epoch 44/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9996 - loss: 0.0246 - val_binary_accuracy: 1.0000 - val_loss: 0.0292

Epoch 45/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9991 - loss: 0.0222 - val_binary_accuracy: 1.0000 - val_loss: 0.0288

Epoch 46/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9995 - loss: 0.0240 - val_binary_accuracy: 1.0000 - val_loss: 0.0283

Epoch 47/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9999 - loss: 0.0226 - val_binary_accuracy: 1.0000 - val_loss: 0.0279

Epoch 48/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9987 - loss: 0.0213 - val_binary_accuracy: 1.0000 - val_loss: 0.0275

Epoch 49/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9988 - loss: 0.0217 - val_binary_accuracy: 1.0000 - val_loss: 0.0271

Epoch 50/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9987 - loss: 0.0250 - val_binary_accuracy: 1.0000 - val_loss: 0.0267

Epoch 51/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9984 - loss: 0.0224 - val_binary_accuracy: 1.0000 - val_loss: 0.0264

Epoch 52/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9996 - loss: 0.0217 - val_binary_accuracy: 1.0000 - val_loss: 0.0260

Epoch 53/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9986 - loss: 0.0192 - val_binary_accuracy: 1.0000 - val_loss: 0.0256

Epoch 54/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9999 - loss: 0.0205 - val_binary_accuracy: 1.0000 - val_loss: 0.0253

Epoch 55/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9998 - loss: 0.0210 - val_binary_accuracy: 1.0000 - val_loss: 0.0250

Epoch 56/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9999 - loss: 0.0185 - val_binary_accuracy: 1.0000 - val_loss: 0.0247

Epoch 57/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9986 - loss: 0.0193 - val_binary_accuracy: 1.0000 - val_loss: 0.0244

Epoch 58/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9991 - loss: 0.0210 - val_binary_accuracy: 1.0000 - val_loss: 0.0241

Epoch 59/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 1.0000 - loss: 0.0216 - val_binary_accuracy: 1.0000 - val_loss: 0.0238

Epoch 60/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9999 - loss: 0.0200 - val_binary_accuracy: 1.0000 - val_loss: 0.0235

Epoch 61/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9989 - loss: 0.0207 - val_binary_accuracy: 1.0000 - val_loss: 0.0232

Epoch 62/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9991 - loss: 0.0184 - val_binary_accuracy: 1.0000 - val_loss: 0.0230

Epoch 63/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9990 - loss: 0.0198 - val_binary_accuracy: 1.0000 - val_loss: 0.0227

Epoch 64/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9997 - loss: 0.0180 - val_binary_accuracy: 1.0000 - val_loss: 0.0224

Epoch 65/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9992 - loss: 0.0187 - val_binary_accuracy: 1.0000 - val_loss: 0.0222

Epoch 66/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9999 - loss: 0.0181 - val_binary_accuracy: 1.0000 - val_loss: 0.0220

Epoch 67/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9996 - loss: 0.0166 - val_binary_accuracy: 1.0000 - val_loss: 0.0217

Epoch 68/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9989 - loss: 0.0183 - val_binary_accuracy: 1.0000 - val_loss: 0.0215

Epoch 69/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9998 - loss: 0.0176 - val_binary_accuracy: 1.0000 - val_loss: 0.0213

Epoch 70/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - binary_accuracy: 1.0000 - loss: 0.0161 - val_binary_accuracy: 1.0000 - val_loss: 0.0211

Epoch 71/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 1s 5ms/step - binary_accuracy: 0.9993 - loss: 0.0180 - val_binary_accuracy: 1.0000 - val_loss: 0.0208

Epoch 72/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - binary_accuracy: 0.9989 - loss: 0.0192 - val_binary_accuracy: 1.0000 - val_loss: 0.0206

Epoch 73/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 1s 5ms/step - binary_accuracy: 1.0000 - loss: 0.0165 - val_binary_accuracy: 1.0000 - val_loss: 0.0204

Epoch 74/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - binary_accuracy: 1.0000 - loss: 0.0179 - val_binary_accuracy: 1.0000 - val_loss: 0.0202

Epoch 75/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9988 - loss: 0.0169 - val_binary_accuracy: 1.0000 - val_loss: 0.0200

Epoch 76/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9996 - loss: 0.0157 - val_binary_accuracy: 1.0000 - val_loss: 0.0198

Epoch 77/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9991 - loss: 0.0186 - val_binary_accuracy: 1.0000 - val_loss: 0.0197

Epoch 78/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9996 - loss: 0.0164 - val_binary_accuracy: 1.0000 - val_loss: 0.0195

Epoch 79/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9998 - loss: 0.0157 - val_binary_accuracy: 1.0000 - val_loss: 0.0193

Epoch 80/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9992 - loss: 0.0143 - val_binary_accuracy: 1.0000 - val_loss: 0.0191

Epoch 81/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 1.0000 - loss: 0.0157 - val_binary_accuracy: 1.0000 - val_loss: 0.0190

Epoch 82/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9998 - loss: 0.0157 - val_binary_accuracy: 1.0000 - val_loss: 0.0188

Epoch 83/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9996 - loss: 0.0153 - val_binary_accuracy: 1.0000 - val_loss: 0.0186

Epoch 84/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 1.0000 - loss: 0.0149 - val_binary_accuracy: 1.0000 - val_loss: 0.0185

Epoch 85/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9989 - loss: 0.0161 - val_binary_accuracy: 1.0000 - val_loss: 0.0183

Epoch 86/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9978 - loss: 0.0161 - val_binary_accuracy: 1.0000 - val_loss: 0.0182

Epoch 87/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9998 - loss: 0.0156 - val_binary_accuracy: 1.0000 - val_loss: 0.0180

Epoch 88/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9997 - loss: 0.0140 - val_binary_accuracy: 1.0000 - val_loss: 0.0179

Epoch 89/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9996 - loss: 0.0151 - val_binary_accuracy: 1.0000 - val_loss: 0.0177

Epoch 90/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9999 - loss: 0.0154 - val_binary_accuracy: 1.0000 - val_loss: 0.0176

Epoch 91/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9999 - loss: 0.0136 - val_binary_accuracy: 1.0000 - val_loss: 0.0174

Epoch 92/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9993 - loss: 0.0138 - val_binary_accuracy: 1.0000 - val_loss: 0.0173

Epoch 93/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9991 - loss: 0.0152 - val_binary_accuracy: 1.0000 - val_loss: 0.0172

Epoch 94/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9999 - loss: 0.0134 - val_binary_accuracy: 1.0000 - val_loss: 0.0170

Epoch 95/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - binary_accuracy: 0.9991 - loss: 0.0143 - val_binary_accuracy: 1.0000 - val_loss: 0.0169

Epoch 96/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9987 - loss: 0.0169 - val_binary_accuracy: 1.0000 - val_loss: 0.0168

Epoch 97/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 1.0000 - loss: 0.0140 - val_binary_accuracy: 1.0000 - val_loss: 0.0166

Epoch 98/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9997 - loss: 0.0142 - val_binary_accuracy: 1.0000 - val_loss: 0.0165

Epoch 99/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9990 - loss: 0.0130 - val_binary_accuracy: 1.0000 - val_loss: 0.0164

Epoch 100/100

50/50 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - binary_accuracy: 0.9994 - loss: 0.0154 - val_binary_accuracy: 1.0000 - val_loss: 0.0163

Objek “history” tersebut memiliki dictionary .history. Kita bisa lihat, apa saja key yang ada:

print(history2.history.keys())
dict_keys(['binary_accuracy', 'loss', 'val_binary_accuracy', 'val_loss'])

Tiap key menyimpan data per epoch, sehingga ukurannya sama semua. Oleh karena itu, sebenarnya dictionary ini bisa diubah menjadi pandas DataFrame, yang kemudian bisa kita simpan ke CSV:

pd.DataFrame(history2.history).to_csv("./keras_sequential_history2.csv", index=False)

Kalau mau menyamakan, file nya bisa kalian download dari GitHub Pages ini: keras_sequential_history2.csv

Kemudian, kita bisa load kembali:

history2_df = pd.read_csv("./keras_sequential_history2.csv")
history2_df
binary_accuracy loss val_binary_accuracy val_loss
0 0.985000 0.150636 0.9775 0.249056
1 0.993125 0.122728 0.9875 0.195455
2 0.993750 0.105114 0.9925 0.161598
3 0.996250 0.092837 0.9950 0.138501
4 0.996875 0.083684 1.0000 0.121808
... ... ... ... ...
95 0.999375 0.014503 1.0000 0.016765
96 0.999375 0.014412 1.0000 0.016639
97 0.999375 0.014322 1.0000 0.016516
98 0.999375 0.014234 1.0000 0.016394
99 0.999375 0.014146 1.0000 0.016273

100 rows × 4 columns

Dua catatan yang paling sering diperhatikan adalah loss (training loss) dan juga val_loss (validation loss). Bahkan, seringkali kedua nilai ini dibuat gambar plotnya (terhadap epoch), untuk menganalisis bagaimana proses training model.

plt.plot(history2_df["loss"], label = "training loss")
plt.plot(history2_df["val_loss"], label = "validation loss")
plt.xlabel("epoch")
plt.legend()
plt.show()

Proses training tenryata berjalan dengan sangat baik! Kali ini, baik training loss maupun validation loss turun secara drastis dan terus menuju nol.

Biasanya, walaupun training loss tidak mungkin naik, terkadang validation loss naik turun, yang bisa jadi pertanda overfitting.

Menggunakan model

Seperti di scikit-learn, panggil .predict() untuk melakukan prediksi

predictions2 = model2.predict(inputs)
63/63 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step  

Ada sedikit progress bar, karena proses prediksi sebenarnya adalah forward pass. Kita bisa matikan progress bar dengan verbose=False

predictions2 = model2.predict(inputs, verbose=False)
print(predictions2)
[[2.0281430e-03]
 [6.6396926e-04]
 [3.5774685e-04]
 ...
 [9.9074566e-01]
 [9.9079442e-01]
 [9.8560268e-01]]
plt.scatter(inputs[:, 0], inputs[:, 1], c=predictions2[:, 0] > 0.5)
plt.show()

Menyimpan keseluruhan model

Perintahnya adalah .save(path_tempat_penyimpanan) dengan file format .keras

model2.save("./keras_sequential_model2.keras")

Kita bisa load kembali model tersebut:

model3 = keras.models.load_model("keras_sequential_model2.keras")

Hasil prediksinya akan sama (karena modelnya memang sama):

predictions3 = model3.predict(inputs)
63/63 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step
np.array_equal(predictions2, predictions3)
True

Menyimpan parameter model (saja)

Daripada menyimpan keseluruhan model, kita bisa menyimpan weights atau parameternya saja, dengan perintah .save_weights(path_tempat_penyimpanan) dan file format .weights.h5

model2.save_weights("keras_sequential_model2.weights.h5")

Untuk load kembali, kita perlu menyusun layer model terlebih dahulu, sama persis dengan susunan yang aslinya:

model4 = keras.Sequential(
    [
        keras.layers.InputLayer(input_shape = (2,)),
        keras.layers.Dense(units = 1, activation = keras.activations.sigmoid)
    ]
)
/usr/local/lib/python3.11/dist-packages/keras/src/layers/core/input_layer.py:27: UserWarning: Argument `input_shape` is deprecated. Use `shape` instead.
  warnings.warn(

Barulah kita gunakan perintah .load_weights(path_tempat_penyimpanan)

model4.load_weights("./keras_sequential_model2.weights.h5")

Lagi-lagi, hasil prediksinya akan sama:

predictions4 = model4.predict(inputs)
63/63 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step  
np.array_equal(predictions2, predictions4)
True

Perhatikan bahwa kita belum memanggil model4.compile, artinya kita belum memasang hyperparameter. Meskipun demikian, kita masih bisa melakukan prediksi, karena proses prediksi hanyalah forward pass, yang hanya membutuhkan parameter (weights and biases), yang memang sudah di-load.

Setelah melakukan model4.compile, dengan hyperparameter yang bahkan tidak harus sama dengan yang aslinya, kita bisa melanjutkan proses training kalau mau.

Mengapa tidak save keseluruhan model saja? Selain lebih hemat memori, contoh kasusnya, kita ingin menyimpan progress dari training model, yang sebenarnya susunan layer nya kita ketahui dengan pasti, seperti contoh model4 di atas.

(Pengayaan) Daftar pilihan hyperparameter di Keras

Pilihan fungsi aktivasi

Umum digunakan

  • Linier (identitas): keras.activations.linear

  • Sigmoid: keras.activations.sigmoid

  • ReLU: keras.activations.relu

  • (Soft) tanh: keras.activations.tanh

  • Softmax: keras.activations.softmax

Lainnya

  • Relu6: keras.activations.relu6

    \[\Phi(x) = \min \{ \text{ReLU}(x), 6 \}\]

  • Leaky ReLU: keras.activations.leaky_relu

    bisa dipasang hyperparameter \(\alpha \ge 0\): negative_slope

    \[\Phi(x) = \max \{x, \alpha x\}\]

  • ELU (Exponential Linear Unit): keras.activations.elu

    bisa dipasang hyperparameter \(\alpha \ge 0\): alpha

    \[ \Phi(x) = \begin{cases} x & x > 0 \\ \alpha (e^x - 1) & \text{otherwise} \end{cases} \]

  • Softplus: keras.activations.softplus

    \[\Phi(x) = \ln (e^x + 1)\]

  • Softsign: keras.activations.softsign

    \[\Phi(x) = \frac{x}{|x| + 1}\]

  • Mish: keras.activations.mish

    \[\Phi(x) = x \tanh (\text{softplus} (x))\]

  • Exponential: keras.activations.exponential

  • SELU (Scaled Exponential Linear Unit): keras.activations.selu

  • GELU (Gaussian error linear unit): keras.activations.gelu

  • Swish / Silu: keras.activatins.silu

  • Hard Silu: keras.activations.hard_silu

  • Hard sigmoid: keras.activations.hard_sigmoid

  • Log softmax: keras.activations.log_softmax

Sumber: https://keras.io/api/layers/activations/

Pilihan optimizer

Umum digunakan

  • SGD: keras.optimizers.SGD

  • Adam: keras.optimizers.Adam (saat ini dianggap optimizer terbaik)

  • RMSprop: keras.optimizers.RMSprop

  • Adagrad: keras.optimizers.Adagrad

Lainnya

  • AdamW: keras.optimizers.AdamW

  • Adadelta: keras.optimizers.Adadelta

  • Adamax: keras.optimizers.Adamax

  • Adafactor: keras.optimizers.Adafactor

  • Nadam: keras.optimizers.Nadam

  • Ftrl: keras.optimizers.Ftrl

  • Lion: keras.optimizers.Lion

  • Loss Scale Optimizer: keras.optimizers.LossScaleOptimizer

Kecuali Loss Scale Optimizer, semua optimizer bisa dipasang learning rate. Contohnya seperti berikut:

keras.optimizers.SGD(learning_rate=0.01)

Sumber: https://keras.io/api/optimizers/

Pilihan loss function

Umum digunakan

  • Binary cross-entropy (untuk klasifikasi biner)

    class: keras.losses.BinaryCrossentropy

    fungsi: keras.losses.binary_crossentropy

  • Categorial cross-entropy (untuk klasifikasi multiclass)

    class: keras.losses.CategoricalCrossentropy

    fungsi: keras.losses.categorical_crossentropy

  • MSE / mean squared error (untuk regresi)

    class: keras.losses.MeanSquaredError

    fungsi: keras.losses.mean_squared_error

Lainnya, untuk klasifikasi

  • Sparse categorical cross-entropy

    class: keras.losses.SparseCategoricalCrossentropy

    fungsi: keras.losses.spare_categorical_crossentropy

  • Poisson loss

    class: keras.losses.Poisson

    fungsi: keras.losses.poisson

  • Kullback-Leibler divergence loss

    class: keras.losses.KLDivergence

    fungsi: keras.losses.kl_divergence

Lainnya, untuk regresi

  • MAE / mean absolute error

    class: keras.losses.MeanAbsoluteError

    fungsi: keras.losses.mean_absolute_error

  • Mean absolute percentage error

    class: keras.losses.MeanAbsolutePercentageError

    fungsi: keras.losses.mean_absolute_percentage_error

  • Mean squared logarithmic error

    class: keras.losses.MeanSquaredLogarithmicError

    fungsi: keras.losses.mean_squared_logarithmic_error

  • Cosine similarity

    class: keras.losses.CosineSimilarity

    fungsi: keras.losses.cosine_similarity

  • Huber loss

    class: keras.losses.Huber

    fungsi: keras.losses.huber

  • Log Cosh loss

    class: keras.losses.LogCosh

    fungsi: keras.losses.log_cosh

Sumber: https://keras.io/api/losses/

Beberapa pilihan metrik evaluasi

Umum digunakan

  • Accuracy: keras.metrics.Accuracy

  • \(R^2\): keras.metrics.R2Score

  • Binary accuracy: keras.metrics.BinaryAccuracy

  • Categorical accuracy: keras.metrics.CategoricalAccuracy

Lainnya, untuk klasifikasi multiclass

  • Sparse categorical accuracy: keras.metrics.SpareCategoricalAccuracy

  • Top K categorical accuracy: keras.metrics.TopKCategoricalAccuracy

  • Spare top K categorical accuracy: keras.metrics.SpareTopKCategoricalAccuracy

Lainnya, untuk klasifikasi biner atau True/False

  • AUC: keras.metrics.AUC

  • Precision: keras.metrics.Precision

  • Recall: keras.metrics.Recall

  • True Positives: keras.metrics.TruePositives

  • True Negatives: keras.metrics.TrueNegatives

  • False Positives: keras.metrics.FalsePositives

  • False Negatives: keras.metrics.FalseNegatives

  • Precision at recall: keras.metrics.PrecisionAtRecall

  • Recall at precision: keras.metrics.RecallAtPrecision

  • Sensitivity at specificity: keras.metrics.SensitivityAtSpecificity

  • Specificity at sensitivity: keras.metrics.SpecificityAtSensitivity

  • F-1 score: keras.metrics.F1Score

  • F-Beta score: keras.metrics.FBetaScore

Semua pilihan loss function juga bisa digunakan sebagai metrik evaluasi.

Sumber: https://keras.io/api/metrics/

Referensi

Sumber gambar

  • Aggarwal, C. Charu. 2018. Neural Networks and Deep Learning: A Textbook. Edisi Pertama. Springer.

  • Goodfellow, Ian; Bengio, Yoshua; & Courville, Aaron. 2016. Deep Learning. MIT Press.

Buku lainnya

  • Géron, Aurélien. 2019. Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow: Concepts, Tools, and Techniques to Build Intelligent Systems. Edisi Kedua. O’Reilly Media.

Internet