"""G12: from-scratch binary logistic regression; no external dependencies."""
import math
def validate_xy(X, y):
n, d = len(X), len(X[0]) if X else 0
if not 1 <= n <= 10000 or not 1 <= d <= 64 or len(y) != n:
raise ValueError("bounded nonempty samples/features and matching labels required")
if any(len(row) != d for row in X):
raise ValueError("ragged feature matrix")
if any(type(v) is not int or v not in (0,1) for v in y):
raise ValueError("strict 0/1 labels required")
if any(not math.isfinite(v) for row in X for v in row):
raise ValueError("finite features required")
return n, d
def sigmoid(z):
if not math.isfinite(z):
raise ValueError("finite logit required")
if z >= 0:
return 1/(1+math.exp(-z))
e = math.exp(z)
return e/(1+e)
def logit_loss(z, y):
if not math.isfinite(z) or type(y) is not int or y not in (0,1):
raise ValueError("finite logit and strict binary target required")
return max(z, 0.) - y*z + math.log1p(math.exp(-abs(z)))
def loss_gradient(w, b, X, y, lam=0.):
n, d = validate_xy(X, y)
if len(w) != d or not all(math.isfinite(v) for v in [*w,b,lam]) or lam < 0:
raise ValueError("valid parameters and nonnegative regularization required")
loss, grad, db = 0., [0.]*d, 0.
for row, target in zip(X,y):
z = math.fsum(a*v for a,v in zip(w,row)) + b
loss += logit_loss(z,target)/n
error = (sigmoid(z)-target)/n
db += error
for j in range(d):
grad[j] += error*row[j]
loss += .5*lam*math.fsum(v*v for v in w)
grad = [g+lam*v for g,v in zip(grad,w)]
if not all(math.isfinite(v) for v in [loss,*grad,db]):
raise ValueError("nonfinite loss/gradient: check scale")
return loss, grad, db
def fit(X, y, lam=.01, rate=.1, steps=400):
_, d = validate_xy(X,y)
if type(steps) is not int or not 1 <= steps <= 10000 or not math.isfinite(rate) or rate <= 0:
raise ValueError("bounded positive step count and rate required")
w,b = [0.]*d,0.
for _ in range(steps):
_,gw,gb = loss_gradient(w,b,X,y,lam)
w = [a-rate*g for a,g in zip(w,gw)]
b -= rate*gb
final,_,_ = loss_gradient(w,b,X,y,lam)
return {"weights":w, "bias":b, "objective":final}
def predict(model, X):
w,b = model["weights"],model["bias"]
if not w or not all(math.isfinite(v) for v in [*w,b]):
raise ValueError("invalid model")
if any(len(row) != len(w) or any(not math.isfinite(x) for x in row) for row in X):
raise ValueError("invalid feature shape or value")
return [sigmoid(math.fsum(a*x for a,x in zip(w,row))+b) for row in X]
if __name__ == "__main__":
X,y = [[-2.],[-1.],[1.],[2.]],[0,0,1,1]
model = fit(X,y)
print(model)
print(predict(model,X))