Komputer menyimpan angka pecahan dalam format titik mengambang yang hampir selalu hanya mendekati nilai sebenarnya. Pada aplikasi biasa selisih kecil tersebut tidak terasa, tetapi deep learning menjumlahkan jutaan pecahan berulang kali sehingga pembulatan menumpuk, dan satu nilai NaN cukup untuk membuat seluruh pelatihan gagal. Chapter ini membuktikan dengan contoh nyata bahwa float32 dan float64 memberikan hasil berbeda, menunjukkan cara mendeteksi serta membersihkan NaN dan inf, lalu menstabilkan softmax — teknik wajib sebelum membangun model klasifikasi. Pemahaman ini menjadi bekal saat membaca kurva loss yang tiba-tiba tidak bergerak.

Presisi float32 vs float64 #

Angka 0.1 tidak memiliki representasi biner yang persis, sehingga komputer menyimpan nilai terdekat. Selisih miliaran dari sekian persen tersebut biasanya tidak terlihat, tetapi perhitungan ulang dapat mengungkapnya, seperti pada berkas presisi-float.py berikut.

python
import numpy as np

# 0.1 tidak punya representasi biner yang persis
total = 0.0
for _ in range(10):
    total += 0.1
print("0.1 ditambahkan 10 kali:", total)
print("nilai yang diharapkan  : 1.0")

# Penyerapan bilangan kecil oleh bilangan besar pada float32
besar32 = np.float32(1e9)
besar64 = np.float64(1e9)
print("1e9 + 1 == 1e9 pada float32:", besar32 + np.float32(1.0) == besar32)
print("1e9 + 1 == 1e9 pada float64:", besar64 + 1.0 == besar64)

# Akumulasi kesalahan pada perhitungan rata-rata
np.random.seed(42)
X = np.random.rand(100_000)
X32 = X.astype(np.float32)
print("mean float64 :", X.mean())
print("mean float32 :", X32.mean())
print("selisih      :", abs(X.mean() - float(X32.mean())))

Output yang Dihasilkan:

text
0.1 ditambahkan 10 kali: 0.9999999999999999
nilai yang diharapkan  : 1.0
1e9 + 1 == 1e9 pada float32: True
1e9 + 1 == 1e9 pada float64: False
mean float64 : 0.49948825007353703
mean float32 : 0.49948823
selisih      : 1.5553578536220414e-08

Mari kita telaah kode di atas baris per baris:

  1. Baris 5-8 (for _ in range(10)): 0.1 disimpan sebagai 0.1000000000000000055511151231257827..., dan penjumlahan beruntun sepuluh kali mengakumulasi selisih pembulatannya — hasilnya 0.9999999999999999, bukan 1.0.
  2. Baris 12-15 (besar32 + np.float32(1.0)): float32 hanya menyimpan sekitar 7 angka signifikan, sehingga pada besaran satu miliar jarak antar nilai yang bisa direpresentasikan lebih besar dari 1. Akibatnya 1e9 + 1 bernilai sama dengan 1e9 — angka 1 terserap. Pada float64 yang menyimpan sekitar 16 angka signifikan, penjumlahan tersebut tetap terlihat.
  3. Baris 19-21 (np.random.rand(100_000)): rata-rata dihitung dua kali dari data yang sama. Selisih hasil float32 terhadap float64 sebesar 1.56e-08 kecil, tetapi kesalahan sejenis ini menumpuk sepanjang ribuan langkah pelatihan — alasan utama mengapa verifikasi implementasi biasa dilakukan di float64.

Overflow, Underflow, dan Nilai NaN #

Titik mengambang punya rentang terbatas: hasil yang melampaui batas atas menjadi inf (overflow), dan hasil yang terlalu dekat nol menjadi 0.0 (underflow). Nilai inf dan NaN lalu menular ke seluruh perhitungan berikutnya. Berkas overflow-nan-inf.py memperlihatkan ketiganya beserta alat pemeriksa dan pembersihnya.

python
import numpy as np

# Overflow: hasil melampaui rentang float64 menjadi inf
with np.errstate(over="ignore"):
    besar = np.exp(1000.0)
print("np.exp(1000)  :", besar)

# Underflow: bilangan terlalu kecil menjadi nol
print("np.exp(-1000) :", np.exp(-1000.0))

# inf dan NaN menular lewat operasi berikutnya
print("inf - inf     :", besar - besar)
print("inf * 0       :", besar * 0.0)
print("nan + 1       :", np.nan + 1.0)

# Pendeteksi nilai bermasalah pada array
data = np.array([1.0, np.nan, np.inf, -np.inf, 3.0])
print("data          :", data)
print("isfinite      :", np.isfinite(data))
print("isnan         :", np.isnan(data))

# Pembersihan: ganti NaN dan inf dengan nilai pengganti
bersih = np.nan_to_num(data, nan=0.0, posinf=1e6, neginf=-1e6)
print("setelah dibersihkan:", bersih)

Output yang Dihasilkan:

text
np.exp(1000)  : inf
np.exp(-1000) : 0.0
inf - inf     : nan
inf * 0       : nan
nan + 1       : nan
data          : [  1.  nan  inf -inf   3.]
isfinite      : [ True False False False  True]
isnan         : [False  True False False False]
setelah dibersihkan: [ 1.e+00  0.e+00  1.e+06 -1.e+06  3.e+00]

Mari kita telaah kode di atas baris per baris:

  1. Baris 5-7 (np.errstate(over="ignore")): np.exp(1000.0) melampaui batas atas float64 (sekitar 1.8e308) sehingga hasilnya inf. Blok errstate mematikan peringatan agar keluaran bersih; tanpa blok tersebut NumPy mencetak RuntimeWarning: overflow encountered in exp ke keluaran galat standar.
  2. Baris 11 (np.exp(-1000.0)): hasil matematisnya sangat kecil sehingga pembulatan terdekat adalah 0.0 — underflow berlangsung senyap tanpa peringatan.
  3. Baris 14-16 (besar - besar, …): operasi yang melibatkan inf menghasilkan NaN, dan NaN menular — nan + 1 tetap nan. Satu elemen NaN pada loss membuat rata-rata batch, gradien, dan bobot model menjadi NaN seluruhnya.
  4. Baris 20-22 (np.isfinite, np.isnan): isfinite bernilai True hanya untuk bilangan biasa, sedangkan isnan melacak keberadaan NaN. Kedua fungsi ini menjadi pemeriksa rutin pada pipeline data: kolom yang mengandung nilai kosong sering dibaca sebagai NaN.
  5. Baris 26 (np.nan_to_num(...)): fungsi pembersih mengganti NaN dengan 0.0, inf dengan 1e6, dan -inf dengan -1e6 sesuai parameter yang kita tentukan. Nilai pengganti selalu dipilih sesuai konteks data, bukan asal terisi.

Stabilisasi Softmax #

Softmax mengubah skor bebas (logits) menjadi probabilitas dengan membagi eksponen setiap skor terhadap jumlahnya. Eksponen tumbuh sangat cepat — np.exp(1000) sudah meluap menjadi inf — sehingga versi naif gagal untuk logits besar. Trik stabilisasinya sederhana: kurangi setiap logit dengan nilai terbesar sebelum mengambil eksponen, karena hasil matematis softmax tidak berubah.

python
import numpy as np


def softmax_naif(z):
    eksponen = np.exp(z)
    return eksponen / eksponen.sum()


def softmax_stabil(z):
    z_geser = z - np.max(z)
    eksponen = np.exp(z_geser)
    return eksponen / eksponen.sum()


# Logits bernilai besar: versi naif meluap
logits = np.array([1000.0, 1001.0, 1002.0])
print("softmax naif   :", softmax_naif(logits))
print("softmax stabil :", softmax_stabil(logits))

# Logits bernilai kecil: kedua versi sepakat
kecil = np.array([1.0, 2.0, 3.0])
print("naif   (kecil) :", softmax_naif(kecil))
print("stabil (kecil) :", softmax_stabil(kecil))
print("jumlah prob.   :", softmax_stabil(kecil).sum())

Output yang Dihasilkan:

text
softmax naif   : [nan nan nan]
softmax stabil : [0.09003057 0.24472847 0.66524096]
naif   (kecil) : [0.09003057 0.24472847 0.66524096]
stabil (kecil) : [0.09003057 0.24472847 0.66524096]
jumlah prob.   : 0.9999999999999999

Mari kita telaah kode di atas baris per baris:

  1. Baris 5-7 (softmax_naif): fungsi menghitung np.exp(z) langsung. Untuk logits [1000, 1001, 1002], ketiga eksponen meluap menjadi inf, lalu pembagian inf / inf menghasilkan nan — prediksi model rusak total tanpa pesan galat dari Python.
  2. Baris 10-13 (softmax_stabil): np.max(z) bernilai 1002, sehingga z_geser menjadi [-2, -1, 0]. Eksponen terbesar kini np.exp(0) = 1, jauh dari batas overflow, dan hasil pembagian menghasilkan probabilitas valid: sekitar 0.09, 0.24, dan 0.67.
  3. Baris 22-23 (pencetakan logits besar): versi stabil mengurangi setiap elemen dengan konstanta yang sama. Menurut sifat eksponen, pembagian pembilang dan penyebut oleh exp(1002) saling menghilangkan, sehingga hasil matematisnya identik — hanya kestabilan numeriknya yang berubah.
  4. Baris 27-29 (logits kecil): untuk logits [1, 2, 3], kedua versi menghasilkan keluaran yang sama persis — bukti stabilisasi tidak mengubah hasil, hanya memperluas rentang nilai yang aman.
  5. Baris 30 (softmax_stabil(kecil).sum()): jumlah probabilitas tercetak 0.9999999999999999, bukan 1.0 — sekali lagi efek pembulatan titik mengambang.

Panduan Memilih dtype #

Pemilihan dtype adalah kompromi antara presisi, kecepatan, dan memori. Tabel berikut merangkum pemakaian umum di deep learning:

dtypeUkuranPemakaian umum di deep learning
float648 byteAnalisis data dan verifikasi implementasi; jarang untuk pelatihan
float324 byteStandar pelatihan dan inference pada CPU maupun GPU
float16 / bfloat162 bytePelatihan presisi campuran (mixed precision)
int648 byteLabel kelas dan indeks dataset
int32 / int164 / 2 byteIndeks dataset berukuran sangat besar
bool1 byteMask: pemfilteran data dan attention mask

Penjelasan resmi tentang perilaku galat NumPy tersedia di numpy.org/doc/stable/user/errors dan detail nan_to_num di numpy.org/doc/stable/reference/generated/numpy.nan_to_num . Semua framework besar — PyTorch, TensorFlow, JAX — mengikuti semantik IEEE 754 yang sama, sehingga kebiasaan membaca inf, NaN, dan toleransi pembandingan dari chapter ini berlaku juga di sana.

Kita kini memiliki seluruh bekal NumPy: array, broadcasting, matriks, pandas sebagai jembatan data, visualisasi, dan kesadaran numerik. Section berikutnya, Dasar Neural Network, menggabungkan semuanya — kita akan membangun neuron dan layer pertama dari nol menggunakan murni NumPy, lalu melatihnya dengan gradien yang dihitung sendiri.