Step 1: Clone your own API
Lendwise sells credit decisions through CreditAPI.score(X): send applicant rows, get approval
probabilities back. The model behind it took two years of loan outcomes to train. Anyone who can call
the endpoint can also collect (query, answer) pairs and fit a copy. Before you defend against that, run it
yourself, so you know what you are defending.
An outsider does not have your applicants, but your API docs publish each feature's valid range
(H.RANGES, a pair of arrays: lows and highs). Made-up rows inside those ranges are enough to start.
Write three functions in extraction.py:
box_queries(n, seed=0):nrows, each feature drawn uniformly between its low and high. Usenp.random.default_rng(seed)so the same seed gives the same rows.steal(api, X): sendXtoapi.score, fitH.fit_surrogateon the rows and the answers, return it.fidelity(surrogate, X): the share of rows where the copy and your real model make the same decision. The copy approves when its prediction is at least 0.5;H.decide(X)gives the real decisions.
Run it and read the agreement on real applicants.
extraction.py, the file you edit113 lines
"""Audit your own scoring API for model extraction, then defend it.
The API, its data and the copycat's model are given in api.py. You write the functions below.
"""
import math
import numpy as np
import api as H
# ---- Step 1: clone your own API ---------------------------------------------------------------------------
def box_queries(n, seed=0):
"""n made-up applicants, each feature drawn uniformly between the published H.RANGES."""
# TODO (Step 1): n rows, each feature uniform between H.RANGES[0] and H.RANGES[1], seeded.
raise NotImplementedError("Step 1: write box_queries()")
def steal(api, X):
"""Send X to api.score and fit H.fit_surrogate on what comes back. Return the surrogate."""
# TODO (Step 1): H.fit_surrogate(X, api.score(X)).
raise NotImplementedError("Step 1: write steal()")
def fidelity(surrogate, X):
"""Share of rows in X where the surrogate (prediction >= 0.5 means approve) agrees with H.decide."""
# TODO (Step 1): mean of (surrogate.predict(X) >= 0.5) == H.decide(X).
raise NotImplementedError("Step 1: write fidelity()")
# ---- Step 2: what a clone costs ----------------------------------------------------------------------------
def baseline(X):
"""Fidelity of a clone that never queried: always give the most common decision on X."""
raise NotImplementedError("baseline() arrives in Step 2")
def budget_curve(api, budgets, seed=0):
"""[(n, fidelity on H.HOLDOUT)] for a surrogate stolen with box_queries(n, seed), for each n in budgets."""
raise NotImplementedError("budget_curve() arrives in Step 2")
def queries_needed(curve, target):
"""Smallest n in curve whose fidelity reaches target, or None if no budget does."""
raise NotImplementedError("queries_needed() arrives in Step 2")
# ---- Step 3: harden the output -----------------------------------------------------------------------------
def hardening_report(n=2000, seed=0):
"""{mode: fidelity on H.HOLDOUT} of a surrogate stolen with box_queries(n, seed) from H.CreditAPI(mode),
for mode in "proba", "round", "label"."""
raise NotImplementedError("hardening_report() arrives in Step 3")
# ---- Step 4: a per-client budget ---------------------------------------------------------------------------
def pick_cap(usage, headroom=1.5):
"""Rows per client per day: headroom x the 99th percentile of last month's client-day row counts, rounded up."""
raise NotImplementedError("pick_cap() arrives in Step 4")
def admit(requests, cap):
"""Rows served for each request, in order. A client gets at most cap rows per day; a request that crosses
the cap is trimmed to what is left, and later requests that day get 0."""
used = {}
out = []
for r in requests:
key = (r["client"], r["day"])
if used.get(key, 0) < cap:
out.append(len(r["X"]))
else:
out.append(0)
used[key] = used.get(key, 0) + 1
return out
def days_to_clone(needed, cap):
"""Days one client needs to send `needed` queries at `cap` rows a day."""
raise NotImplementedError("days_to_clone() arrives in Step 4")
# ---- Step 5: spot the scraper -------------------------------------------------------------------------------
def ood_score(X):
"""Mean Mahalanobis distance of the rows in X from your real applicants (mean and covariance of H.LOANS[0])."""
raise NotImplementedError("ood_score() arrives in Step 5")
def calibrate(chunk=50, headroom=1.2):
"""headroom x the highest ood_score over consecutive chunks of `chunk` rows of H.HOLDOUT."""
raise NotImplementedError("calibrate() arrives in Step 5")
def flag_clients(traffic, threshold):
"""Clients whose rows, pooled over all their requests, have an ood_score above threshold."""
raise NotImplementedError("flag_clients() arrives in Step 5")
# ---- Step 6: what the defences leave an attacker -------------------------------------------------------------
def served_rows(traffic, flagged, cap):
"""{client: rows actually served}: flagged clients get nothing, everyone else goes through admit(cap), and
a trimmed request serves its first rows."""
raise NotImplementedError("served_rows() arrives in Step 6")
def defence_report(api, traffic, cap, threshold, watch):
"""For each client in watch: {"sent", "served", "flagged", "fidelity_before", "fidelity_after"}. Fidelity is
on H.HOLDOUT for a surrogate stolen from every row the client sent (before) or was served (after);
fidelity_after is None when fewer than 20 rows were served."""
raise NotImplementedError("defence_report() arrives in Step 6")api.pyholdout.csvloans.csvtraffic.csvtry_it.pyusage_last_month.csv