Setiap hasil numerik membawa galat — selisih antara hasil komputer dan nilai matematis yang sesungguhnya. Galat ini tidak selalu berasal dari bug; sebagian lahir dari cara komputer menyimpan angka, sebagian lagi dari rumus hampiran yang sengaja kita pilih. Masalahnya, galat jenis berbeda menuntut penanganan berbeda: memperkecil step size justru memperbesar galat satu jenis, dan rumus yang tampak wajar bisa menghancurkan presisi secara diam-diam. Chapter ini memetakan jenis galat, mendemonstrasikan catastrophic cancellation, dan merumuskan aturan praktis menjaga akurasi.

Dua Keluarga Galat: Round-off dan Truncation #

Aturan dasarnya: round-off lahir dari pembulatan representasi 52 bit mantissa (chapter Floating Point), sedangkan truncation lahir dari rumus yang dipotong — misalnya deret Taylor yang diambil beberapa suku pertamanya.

Jenis galatSumberUkuran tipikalCara mengecilkan
Round-offpembulatan ke representasi 64 bitsekitar eps per operasisusun ulang rumus, kurangi jumlah operasi
Truncationrumus hampiran dipotong (deret, h)bergantung suku/h yang dibuangtambah suku atau perkecil h
Cancellationpengurangan bilangan nyaris samabisa 100% galat relatifpakai identitas matematis
Overflow/underflowkeluar rentang realmin sampai realmaxInf atau 0skala ulang variabel

Mekanisme truncation mudah didemonstrasikan dengan hampiran sin(x) ≈ x - x^3/6 (deret Taylor dipotong dua suku):

matlab
x = 0.5;
hampiran = x - x^3/6;
eksak = sin(x);
fprintf('hampiran = %.10f\n', hampiran);
fprintf('sin(0.5) = %.10f\n', eksak);
fprintf('galat    = %.3e\n', abs(hampiran - eksak));

Output yang Dihasilkan:

text
hampiran = 0.4791666667
sin(0.5) = 0.4794255386
galat    = 2.589e-04

Mari kita bedah baris-baris kode di atas:

  1. Baris 2 (hampiran = x - x^3/6): rumus memotong deret setelah suku x^3 — suku x^5/120 dan seterusnya dibuang, dan suku pertama yang dibuang itulah yang menentukan besar galat.
  2. Baris 3 (eksak = sin(x)): sin() internal MATLAB jauh lebih akurat daripada dua suku, sehingga layak menjadi pembanding.
  3. Baris 4-5: galat 2.6e-04 murni truncation; menambah satu suku deret akan memperkecilnya, bukan mengganti double dengan single.

Catastrophic Cancellation #

Masalah nyatanya: mengurangkan dua bilangan nyaris sama membuang hampir semua digit signifikan. Contoh klasiknya adalah menghitung (1 - cos(x)) / x^2 untuk x kecil — nilainya menuju 0.5, tetapi 1 - cos(x) mengurangkan 1 dengan bilangan yang nyaris 1. Solusinya: identitas trigonometri 1 - cos(x) = 2 * sin(x/2)^2 yang sama sekali tidak membutuhkan pengurangan:

matlab
fprintf('%8s %14s %14s %12s\n', 'x', 'buruk', 'baik', 'galat rel');
for x = [1e-2 1e-4 1e-6 1e-8 1e-10]
    buruk = (1 - cos(x)) / x^2;
    baik  = 2 * sin(x/2)^2 / x^2;   % identitas 1 - cos(x) = 2 sin^2(x/2)
    rel   = abs(buruk - baik) / baik;
    fprintf('%8.0e %14.8f %14.8f %12.2e\n', x, buruk, baik, rel);
end

Output yang Dihasilkan:

text
       x          buruk           baik    galat rel
   1e-02     0.49999583     0.49999583     2.89e-13
   1e-04     0.50000000     0.50000000     5.24e-09
   1e-06     0.50004445     0.50000000     8.89e-05
   1e-08     0.00000000     0.50000000     1.00e+00
   1e-10     0.00000000     0.50000000     1.00e+00

Mari kita bedah baris-baris kode di atas:

  1. Baris 2 (for x = [1e-2 ... 1e-10]): loop menguji x yang makin kecil; semakin kecil x, semakin dekat cos(x) ke 1, semakin parah pembatalannya.
  2. Baris 3 (buruk = ...): untuk x = 1e-8, nilai cos(x) dibulatkan menjadi tepat 1, sehingga 1 - cos(x) menghasilkan 0 dan hasil akhirnya 0 — galat relatif 100%.
  3. Baris 4 (baik = 2 * sin(x/2)^2 / x^2): identitas menghitung bilangan kecil secara langsung, tanpa pengurangan bilangan besar; hasilnya stabil 0.5 untuk semua x.

Fungsi Buruk vs Fungsi Baik: Rumus Kuadrat #

Rumus kuadrat (-b ± sqrt(b^2 - 4ac)) / (2a) menyimpan jebakan yang sama. Untuk persamaan x^2 - 1e8*x + 1 = 0, akar kecilnya sekitar 1e-8. Menghitungnya lewat cabang -b - sqrt(D) berarti mengurangkan dua bilangan nyaris sama; cabang yang baik memakai teorema Vieta — hasil kali akar sama dengan c/a, jadi akar kecil = c / akarBesar:

matlab
a = 1; b = -1e8; c = 1;
D = b^2 - 4*a*c;
akarBesar = (-b + sqrt(D)) / (2*a);      % aman: penjumlahan sebangun
akarKecilBuruk = (-b - sqrt(D)) / (2*a); % pembatalan: bilangan nyaris sama
akarKecilBaik = c / (a * akarBesar);     % Vieta: x1 * x2 = c / a
fprintf('akar besar      = %.4f\n', akarBesar);
fprintf('akar kecil buruk = %.6e\n', akarKecilBuruk);
fprintf('akar kecil baik  = %.6e\n', akarKecilBaik);
fprintf('galat rel buruk  = %.2e\n', abs(akarKecilBuruk - akarKecilBaik) / akarKecilBaik);

Output yang Dihasilkan:

text
akar besar      = 100000000.0000
akar kecil buruk = 7.450581e-09
akar kecil baik  = 1.000000e-08
galat rel buruk  = 2.55e-01

Mari kita bedah baris-baris kode di atas:

  1. Baris 4 (akarBesar): cabang plus aman — 1e8 dijumlahkan dengan sqrt(D) yang nyaris sama besarnya; penjumlahan sebangun tidak membuang digit.
  2. Baris 5 (akarKecilBuruk): cabang minus mengurangkan bilangan nyaris sama; dua puluh lima persen digit hasilnya salah.
  3. Baris 6 (akarKecilBaik): rumus Vieta menghitung akar kecil lewat pembagian, bukan pengurangan — akurat penuh, dan cocok dengan keluaran roots([1 -1e8 1]).

Galat Absolut vs Galat Relatif #

Dua ukuran standar: galat absolut = abs(x - xhampiran), dan galat relatif = abs(x - xhampiran) / abs(x). Galat absolut 1e-3 fatal bila nilai sebenarnya 0.01 (galat relatif 10%), tetapi sepele bila nilainya 10000 (galat relatif 1e-7). Perbandingan floating point yang sehat menggabungkan keduanya: abs(x - y) <= tolAbs + tolRel * max(abs(x), abs(y)). Versi ringkasnya untuk nilai berskala normal:

matlab
tolAbs = 1e-9;
tolRel = 1e-9;
x = 0.1 + 0.2;
y = 0.3;
fprintf('x == y            -> %d\n', x == y);
fprintf('abs(x - y)        = %.6e\n', abs(x - y));
fprintf('tolAbs + tolRel*x = %.6e\n', tolAbs + tolRel * x);
fprintf('dekat?            -> %d\n', abs(x - y) <= tolAbs + tolRel * x);

Output yang Dihasilkan:

text
x == y            -> 0
abs(x - y)        = 5.551115e-17
tolAbs + tolRel*x = 1.300000e-09
dekat?            -> 1

Mari kita bedah baris-baris kode di atas:

  1. Baris 1-2 (tolAbs, tolRel): toleransi absolut menjaga kasus nilai dekat nol; toleransi relatif menskalakan ambang mengikuti besarnya nilai.
  2. Baris 4-5 (x, y): x dan y secara matematis sama-sama 0.3, tetapi x membawa galat representasi dari 0.1 + 0.2.
  3. Baris 7 (x == y): perbandingan eksak bernilai 0 (false) — bukti operator == terlalu kaku untuk floating point.
  4. Baris 10 (dekat?): uji toleransi bernilai 1 (true) — galat 5.55e-17 jauh di bawah ambang 1.3e-9, sehingga kedua nilai layak dianggap sama secara numerik.

Aturan Praktis Mengurangi Galat #

AturanAlasanContoh di chapter ini
Hindari pengurangan bilangan nyaris samamemicu cancellation1 - cos(x)
Tulis ulang dengan identitas matematismenghitung nilai kecil secara langsung2 * sin(x/2)^2
Pilih cabang rumus yang amanpenjumlahan sebangun tidak membuang digitVieta pada rumus kuadrat
Bandingkan dengan toleransi, bukan ==galat pembulatan kecil pasti adatolAbs + tolRel * x
Jumlahkan dari terkecil ke terbesargalat pembulatan menumpuk pada akumulator besarloop total = total + 0.1

Berkas contoh.m pada bagian bawah halaman menjalankan seluruh demonstrasi di atas — truncation deret, tabel cancellation, rumus kuadrat, dan uji toleransi.

Ringkasan #

Galat numerik terbagi menjadi round-off (pembulatan representasi, tak terhindarkan) dan truncation (rumus dipotong, bisa diperhalus), ditambah cancellation yang menghancurkan digit saat dua bilangan nyaris sama dikurangkan. Pola bahayanya terlihat jelas pada (1 - cos(x)) / x^2 dan cabang minus rumus kuadrat — hasil runtuh justru saat input mengecil. Penawarnya adalah identitas matematis dan pemilihan cabang hitung yang menghindari pengurangan berbahaya, serta perbandingan berbasis toleransi abs(x - y) <= tolAbs + tolRel * abs(x). Dua aturan ini akan kita pakai terus di chapter metode numerik berikutnya.