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: '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