"""第 7 章：逻辑回归、验证集选阈值与稳定的 Softmax；只用标准库。"""

from math import exp, log1p
from random import Random
from statistics import mean


def sigmoid(score):
    if score >= 0:
        return 1 / (1 + exp(-score))
    value = exp(score)
    return value / (1 + value)


def softmax(scores):
    largest = max(scores)
    values = [exp(score-largest) for score in scores]
    return [value/sum(values) for value in values]


def make_data(count, seed):
    rng = Random(seed)
    data = []
    for _ in range(count):
        pages, queue = rng.randint(5, 35), rng.randint(0, 10)
        features = [(pages-20)/10, (queue-5)/5]
        probability = sigmoid(0.9*features[0] + 0.8*features[1] - 0.8)
        label = int(rng.random() < probability)
        data.append((features, label))
    return data


def fit(data, steps=500, rate=0.2, regularization=0.05):
    weights, bias = [0.0, 0.0], 0.0
    for _ in range(steps):
        errors = [sigmoid(sum(w*x for w, x in zip(weights, features))+bias)-y
                  for features, y in data]
        gradients = [mean(e*row[0][j] for e, row in zip(errors, data))
                     + 2*regularization*weights[j] for j in range(2)]
        weights = [w-rate*g for w, g in zip(weights, gradients)]
        bias -= rate*mean(errors)
    return weights, bias


def probability(model, features):
    weights, bias = model
    return sigmoid(sum(w*x for w, x in zip(weights, features))+bias)


def binary_loss(score, label):
    # log(1+exp(score))-label*score 的稳定等价式。
    return max(score, 0) - label*score + log1p(exp(-abs(score)))


def evaluate(model, data, threshold):
    tp = fp = fn = tn = 0
    for features, label in data:
        predicted = probability(model, features) >= threshold
        tp += predicted and label == 1
        fp += predicted and label == 0
        fn += not predicted and label == 1
        tn += not predicted and label == 0
    precision = tp/(tp+fp) if tp+fp else 0.0
    recall = tp/(tp+fn) if tp+fn else 0.0
    return tp, fp, fn, tn, precision, recall, fp+3*fn


def main():
    train, validation, test = [make_data(n, seed) for n, seed in
                               [(120, 2026), (60, 2027), (60, 2028)]]
    model = fit(train)
    weights, bias = model
    loss = mean(binary_loss(sum(w*x for w, x in zip(weights, features))+bias, y)
                for features, y in train)
    print(f"训练交叉熵（不含正则项）：{loss:.3f}")
    candidates = []
    for threshold in [0.25, 0.5, 0.75]:
        tp, fp, fn, tn, precision, recall, cost = evaluate(model, validation, threshold)
        candidates.append((cost, threshold))
        print(f"阈值={threshold:.2f}：验证 FP={fp}，FN={fn}，代价={cost}")
    _, threshold = min(candidates)
    print(f"固定阈值：{threshold:.2f}")
    tp, fp, fn, tn, precision, recall, cost = evaluate(model, test, threshold)
    print(f"测试 TP={tp}，FP={fp}，FN={fn}，TN={tn}")
    print(f"测试精确率={precision:.3f}，召回率={recall:.3f}，代价={cost}")
    print("Softmax([2,1,0])：" + ", ".join(f"{p:.3f}" for p in softmax([2, 1, 0])))
    print(f"大分数的交叉熵：{binary_loss(1000, 0):.1f}")


if __name__ == "__main__":
    main()
