Pertanyaan paling tidak nyaman dalam operasional model datang dari luar tim teknis: “keputusan pengajuan kredit pada bulan Maret itu dibuat oleh model versi berapa, dilatih dari data mana, dan bagaimana kita tahu catatannya tidak diubah setelah kejadian?” Tanpa persiapan, pertanyaan ini tidak punya jawaban — dan jawaban yang direkonstruksi dari ingatan tidak bernilai sebagai bukti. Audit trail menyelesaikannya dengan pencatatan yang tidak bergantung pada ingatan siapa pun, dan lineage yang menautkan seluruh rantai dari data mentah sampai prediksi. Chapter ini membangun keduanya dalam bentuk mini namun mekanismenya sama dengan sistem sungguhan.

Audit Trail: Siapa, Apa, Kapan, Versi Mana #

Setiap peristiwa penting dalam hidup model layak menjadi satu rekaman audit. Artifak yang wajib tertangkap di dalamnya:

ArtefakContoh isiMenjawab
identitas pelakuuser/service yang menjalankansiapa
peristiwapelatihan, evaluasi, promosi, deployapa
waktu kejadianstempel waktukapan
versi modelnomor versi dari registrymodel mana
hash artefaksha256 atas config + data + bobotisinya masih asli?
hash rekaman sebelumnyatautan berantai ke rekaman lamaurutan bisa dipalsukan?

Dua baris terakhir adalah pembeda audit trail dari log biasa. Hash artefak membuktikan isi tidak berubah; hash berantai membuktikan urutan dan keberadaan rekaman tidak bisa disusun ulang — menyisipkan atau menghapus satu rekaman membatalkan seluruh hash di hilirnya. Mekanisme ini sekeluarga dengan yang dipakai blockchain, tetapi untuk kebutuhan audit cukup implementasi hashlib standar Python (docs.python.org/3/library/hashlib.html ).

Lineage Penuh: Data ke Kode ke Model ke Prediksi #

Reproducibility per run — seed, versi environment, hash dataset — sudah kita bangun di chapter Reproducible Experiments . Audit menaikkan cakupannya satu tingkat: bukan hanya “run ini bisa diulang”, melainkan “prediksi ini bisa ditelusuri sampai ke data dan kode yang menjadi asalnya”. Lineage penuh berjalan satu arah:

text
data (versi + hash) -> kode (commit) -> training run -> artifact model
-> versi registry -> deployment -> prediksi

Setiap panah adalah satu rekaman audit. Ketika semua panah utuh, pertanyaan apapun tentang satu prediksi bisa dijawab dengan berjalan mundur dari ujung rantai — tanpa menebak.

Mengapa Pihak Luar Peduli #

Kebutuhan audit biasanya datang dari pihak di luar tim: regulator sektor keuangan yang meminta penjelasan atas keputusan otomatis, tim hukum yang menyiapkan pembelaan, auditor internal yang menguji kontrol, atau klien perusahaan yang menuntut klausul ketertelusuran dalam kontrak. Semua pihak ini bertanya hal yang sama dalam bahasa berbeda: apakah keputusan sistem bisa dijelaskan dan diverifikasi setelah kejadian. Menyiapkan rekaman sebelum diminta jauh lebih murah daripada merekonstruksi setelah ditanya — rekonstruksi mundur hampir selalu tidak lengkap.

Contoh Teruji: Rantai Hash Audit #

Berkas audit_rantai.py mencatat tiga peristiwa — pelatihan v1, pelatihan v2, deploy v2 — sebagai rekaman berantai. Verifikasi pertama menghitung ulang seluruh hash dari artefak yang masih asli; verifikasi kedua mengubah satu bobot v2 sebesar 0.5 setelah fakta:

python
"""Audit trail mini dengan rantai hash sha256.

Tiga peristiwa dicatat: pelatihan v1, pelatihan v2, dan deployment
v2 ke produksi. Setiap rekaman menyimpan hash artefak (config, data,
model) dan hash rekaman sebelumnya — membentuk rantai. Verifikasi
menghitung ulang seluruh hash dari artefak yang ada sekarang, lalu
skenario manipulasi membuktikan bahwa perubahan sekecil apa pun pada
artefak membuat rekaman terkait tidak sahih.
"""

import hashlib
import json

import numpy as np

PANJANG_HASH = 16
GENESIS = "0" * PANJANG_HASH


def sha256_bytes(data):
    return hashlib.sha256(data).hexdigest()[:PANJANG_HASH]


def sha256_str(teks):
    return hashlib.sha256(teks.encode()).hexdigest()[:PANJANG_HASH]


def buat_dataset(seed):
    rng = np.random.default_rng(seed)
    kelas0 = rng.normal(loc=[-1.0, 0.0], scale=0.7, size=(120, 2))
    kelas1 = rng.normal(loc=[1.0, 0.4], scale=0.7, size=(120, 2))
    X = np.vstack([kelas0, kelas1])
    y = np.array([0] * 120 + [1] * 120)
    urut = rng.permutation(len(y))
    return X[urut], y[urut]


def latih(X, y, epoch):
    w = np.zeros(3)
    for _ in range(epoch):
        skor = np.clip(X @ w[1:] + w[0], -30, 30)
        p = 1.0 / (1.0 + np.exp(-skor))
        galat = p - y
        w[1:] -= (X.T @ galat) / len(y)
        w[0] -= galat.mean()
    return w


def hash_artefak(config, X, y, w):
    """Satu hash untuk satu set artefak: config + data + model."""
    bagian = [
        json.dumps(config, sort_keys=True).encode(),
        X.tobytes(), y.tobytes(), w.tobytes(),
    ]
    return sha256_bytes(b"|".join(bagian))


def buat_record(peristiwa, hash_art, prev_hash):
    """Rekaman berantai: hash rekaman bergantung pada rekaman sebelumnya."""
    payload = json.dumps({"peristiwa": peristiwa, "artefak": hash_art},
                         sort_keys=True)
    return {
        "peristiwa": peristiwa,
        "artefak": hash_art,
        "prev": prev_hash,
        "hash_rekam": sha256_str(prev_hash + payload),
    }


def verifikasi(rantai, hash_artefak_sekarang):
    """Hitung ulang hash tiap rekaman dan tautan rantainya."""
    prev = GENESIS
    hasil = []
    for rek in rantai:
        payload = json.dumps({"peristiwa": rek["peristiwa"],
                              "artefak": rek["artefak"]}, sort_keys=True)
        hash_hitung = sha256_str(rek["prev"] + payload)
        sahih = (hash_hitung == rek["hash_rekam"]
                 and rek["prev"] == prev
                 and rek["artefak"] == hash_artefak_sekarang[rek["peristiwa"]])
        hasil.append((rek["peristiwa"], sahih))
        prev = rek["hash_rekam"]
    return hasil


def main():
    X, y = buat_dataset(seed=42)
    config_v1 = {"model": "logreg", "epoch": 150, "seed": 42}
    config_v2 = {"model": "logreg", "epoch": 300, "seed": 42}
    w1 = latih(X, y, config_v1["epoch"])
    w2 = latih(X, y, config_v2["epoch"])

    hash_v1 = hash_artefak(config_v1, X, y, w1)
    hash_v2 = hash_artefak(config_v2, X, y, w2)
    hash_deploy = hash_artefak({"serving": "v2", "ambang": 0.90},
                               X, y, w2)

    print("== membangun audit trail (3 rekaman berantai) ==")
    rantai = []
    prev = GENESIS
    for peristiwa, hash_art in [("pelatihan v1", hash_v1),
                                ("pelatihan v2", hash_v2),
                                ("deploy v2 ke produksi", hash_deploy)]:
        rek = buat_record(peristiwa, hash_art, prev)
        rantai.append(rek)
        prev = rek["hash_rekam"]
        print(f"  {rek['peristiwa']:<22} artefak={hash_art} "
              f"prev={rek['prev'][:6]}.. rekam={rek['hash_rekam']}")

    artefak_asli = {"pelatihan v1": hash_v1, "pelatihan v2": hash_v2,
                    "deploy v2 ke produksi": hash_deploy}
    print("\n== verifikasi 1: artefak belum disentuh ==")
    for peristiwa, sahih in verifikasi(rantai, artefak_asli):
        print(f"  {peristiwa:<22} -> {'SAHIH' if sahih else 'GAGAL'}")

    print("\n== verifikasi 2: bobot v2 dimodifikasi setelah fakta ==")
    w2_dirusak = w2.copy()
    w2_dirusak[0] += 0.5  # satu nilai saja diubah
    hash_v2_dirusak = hash_artefak(config_v2, X, y, w2_dirusak)
    hash_deploy_dirusak = hash_artefak({"serving": "v2", "ambang": 0.90},
                                       X, y, w2_dirusak)
    artefak_dirusak = {"pelatihan v1": hash_v1,
                       "pelatihan v2": hash_v2_dirusak,
                       "deploy v2 ke produksi": hash_deploy_dirusak}
    for peristiwa, sahih in verifikasi(rantai, artefak_dirusak):
        print(f"  {peristiwa:<22} -> {'SAHIH' if sahih else 'GAGAL'}")
    print("rekaman v1 tetap sahih; rekaman yang menyinggung bobot v2 "
          "terdeteksi berubah")


if __name__ == "__main__":
    main()

Output yang Dihasilkan:

text
== membangun audit trail (3 rekaman berantai) ==
  pelatihan v1           artefak=2d2608e56d0ddb18 prev=000000.. rekam=2df11f807a502aa9
  pelatihan v2           artefak=f7c44f29c2347a98 prev=2df11f.. rekam=5ade8ab8a8529c2a
  deploy v2 ke produksi  artefak=abcd1f65d739e2a3 prev=5ade8a.. rekam=b23f466823f90d5d

== verifikasi 1: artefak belum disentuh ==
  pelatihan v1           -> SAHIH
  pelatihan v2           -> SAHIH
  deploy v2 ke produksi  -> SAHIH

== verifikasi 2: bobot v2 dimodifikasi setelah fakta ==
  pelatihan v1           -> SAHIH
  pelatihan v2           -> GAGAL
  deploy v2 ke produksi  -> GAGAL
rekaman v1 tetap sahih; rekaman yang menyinggung bobot v2 terdeteksi berubah

Mari kita telaah kode di atas baris per baris:

  1. Baris 49-55 (hash_artefak): satu hash untuk satu set artefak lengkap — config di-serialize dengan sort_keys=True agar urutan kunci tidak memengaruhi hash, data dan bobot diambil dari tobytes(). Rekaman deployment memakai set artefaknya sendiri (config serving + bobot v2), menegaskan bahwa tiap peristiwa punya definisi “isinya” yang eksplisit.
  2. Baris 58-67 (buat_record): payload rekaman di-JSON-kan lalu di-hash bersama hash rekaman sebelumnya. Tautan inilah yang membuat rantai: hash rekaman 3 bergantung pada rekaman 2, yang bergantung pada rekaman 1, yang bertumpu pada GENESIS.
  3. Baris 70-83 (verifikasi): tiga pemeriksaan per rekaman — hash rekaman dihitung ulang, tautan prev cocok dengan hash rekaman sebelumnya, dan hash artefak yang dikirim pemanggil cocok dengan yang tercatat. Parameter hash_artefak_sekarang sengaja dipisahkan dari rantai: verifikasi membandingkan rekaman dengan keadaan dunia saat ini, bukan dengan dirinya sendiri.
  4. Baris 98-108 (membangun rantai): tiga rekaman dibuat berurutan dengan prev yang bergulir. Pada keluaran, baris prev=5ade8a.. pada rekaman ketiga terlihat sama dengan rekam=5ade8ab8... rekaman kedua — tautan antar rekaman tercetak telanjang.
  5. Baris 116-127 (manipulasi): w2_dirusak[0] += 0.5 mengubah satu nilai bobot. Hash artefak v2 dihitung ulang dari bobot yang sudah diubah, lalu verifikasi dijalankan dengan hash-hash baru tersebut. Hasilnya: rekaman v1 tetap SAHIH, sedangkan pelatihan v2 dan deploy v2 ke produksi menjadi GAGAL — hanya rekaman yang menyinggung artefak yang diubah yang tertangkap, dan manipulasi sekecil satu angka pun tidak lolos.

Pola keluaran verifikasi kedua layak direnungkan: sistem tidak mengatakan “ada yang mengubah bobot” — sistem mengatakan “rekaman ini tidak lagi cocok dengan artefak yang ada sekarang”. Deteksi integritas bersifat matematis; kesimpulan bahwa terjadi manipulasi, kelalaian, atau sekadar kesalahan proses tetap pekerjaan penyelidikan manusia.

Dengan audit trail, setiap keputusan memiliki jejak yang utuh dan bisa diverifikasi. Satu skenario masih tersisa untuk menutup siklus operasional: ketika rekaman dan monitoring bersama-sama menunjukkan model baru yang buruk sudah berjalan di produksi — seberapa cepat kita bisa membatalkan semuanya? Chapter penutup section ini, Rollback dan Model Update , menjawabnya dengan simulasi lengkap dari deploy sampai pemulihan.