这篇文章使用 Python 标准库实现一个简单的机器学习模型:逻辑回归二分类。
项目不依赖numpy、scikit-learn等第三方库,适合刚开始学习机器学习、希望了解算法内部计算过程的人。完整流程包括数据生成、训练集与测试集划分、特征标准化、批量梯度下降、模型评估、参数保存和手动预测。
算法原理
逻辑回归虽然名字中有“回归”,但它主要用于分类任务。
对于二维输入样本,先计算:
再通过 Sigmoid 函数把 z 转换为 0 到 1 之间的概率:
当 p大于等于0.5时,预测为类别 1,否则预测为类别 0。
模型使用交叉熵作为损失函数,并通过批量梯度下降不断更新权重和偏置。为了让训练更加稳定,代码还使用训练集的均值和标准差对特征进行了标准化。
完整代码
# -*- coding: utf-8 -*- """ 一个从零实现的简单机器学习示例:逻辑回归二分类 只使用 Python 标准库,不需要安装 numpy / scikit-learn 等任何第三方库。 整体流程: 1. 生成数据(两个类别,二维特征) 2. 划分训练集 / 测试集 3. 特征标准化 4. 用梯度下降训练逻辑回归 5. 在测试集上评估准确率 6. 保存模型参数,并做一次预测示例 """ import json import math import os import random # ================================================================ 1. 数据 def make_data(n_per_class=200, seed=42): """生成两个线性可分(带噪声)的二维类别数据。""" rng = random.Random(seed) X, y = [], [] for _ in range(n_per_class): # 类别 0:中心在 (-1, -1) X.append([rng.gauss(-1.0, 0.8), rng.gauss(-1.0, 0.8)]) y.append(0) for _ in range(n_per_class): # 类别 1:中心在 (+1, +1) X.append([rng.gauss(1.0, 0.8), rng.gauss(1.0, 0.8)]) y.append(1) # 打乱顺序 idx = list(range(len(y))) rng.shuffle(idx) return [X[i] for i in idx], [y[i] for i in idx] def train_test_split(X, y, test_ratio=0.2, seed=42): """按比例划分训练集与测试集。""" rng = random.Random(seed) idx = list(range(len(y))) rng.shuffle(idx) n_test = int(len(y) * test_ratio) test_idx = idx[:n_test] train_idx = idx[n_test:] X_train = [X[i] for i in train_idx] y_train = [y[i] for i in train_idx] X_test = [X[i] for i in test_idx] y_test = [y[i] for i in test_idx] return X_train, X_test, y_train, y_test # ================================================================ 2. 预处理 def mean(values): return sum(values) / len(values) def std(values): mu = mean(values) var = sum((v - mu) ** 2 for v in values) / len(values) return math.sqrt(var) def standardize(X_train, X_test): """用训练集的均值和标准差做标准化,避免测试集信息泄漏。""" n_features = len(X_train[0]) mu, sigma = [], [] for j in range(n_features): col = [row[j] for row in X_train] m = mean(col) s = std(col) if s == 0: s = 1.0 mu.append(m) sigma.append(s) def transform(X): return [[(row[j] - mu[j]) / sigma[j] for j in range(n_features)] for row in X] return transform(X_train), transform(X_test), mu, sigma # ================================================================ 3. 模型 def sigmoid(z): """数值稳定的 sigmoid 函数。""" if z >= 0: return 1.0 / (1.0 + math.exp(-z)) ez = math.exp(z) return ez / (1.0 + ez) def dot(a, b): return sum(x * y for x, y in zip(a, b)) def compute_loss(w, b, X, y): """交叉熵损失。""" eps = 1e-12 total = 0.0 for xi, yi in zip(X, y): p = sigmoid(dot(w, xi) + b) total += yi * math.log(p + eps) + (1 - yi) * math.log(1 - p + eps) return -total / len(y) def train_logistic_regression(X, y, lr=0.1, epochs=2000, verbose=True): """用批量梯度下降训练逻辑回归。""" m = len(y) n = len(X[0]) w = [0.0] * n b = 0.0 loss_history = [] for epoch in range(1, epochs + 1): # 计算梯度 dw = [0.0] * n db = 0.0 for xi, yi in zip(X, y): err = sigmoid(dot(w, xi) + b) - yi for j in range(n): dw[j] += err * xi[j] db += err # 更新参数 for j in range(n): w[j] -= lr * dw[j] / m b -= lr * db / m # 记录损失 if epoch % 200 == 0 or epoch == 1: loss = compute_loss(w, b, X, y) loss_history.append(loss) if verbose: print(f"epoch {epoch:5d} | loss = {loss:.4f}") return w, b, loss_history def predict(w, b, X, threshold=0.5): """预测类别(0 或 1)。""" return [1 if sigmoid(dot(w, xi) + b) >= threshold else 0 for xi in X] def accuracy(y_true, y_pred): """计算准确率。""" correct = sum(1 for a, p in zip(y_true, y_pred) if a == p) return correct / len(y_true) # ================================================================ 4. 保存模型 def save_model(path, w, b, mu, sigma): """把模型参数保存成 JSON 文件。""" with open(path, "w", encoding="utf-8") as f: json.dump({"w": w, "b": b, "mu": mu, "sigma": sigma}, f, ensure_ascii=False, indent=2) print(f"[保存] 模型已保存到 {path}") def load_model(path): """加载模型参数。""" with open(path, "r", encoding="utf-8") as f: data = json.load(f) return data["w"], data["b"], data["mu"], data["sigma"] # ================================================================ 4.5 交互测试 def interactive(w, b, mu, sigma): """让用户手动输入特征值,实时查看预测结果。""" print("\n" + "-" * 50) print("手动测试:输入两个数字(用空格分开),回车查看预测结果") print("例如:0.5 0.5 输入 q 退出") print("-" * 50) while True: try: s = input("> ").strip() except EOFError: break if s.lower() in ("q", "quit", "exit"): print("已退出测试") break if s == "": continue parts = s.split() if len(parts) != 2: print("格式不对,请输入两个数字,用空格分开(如 0.5 0.5)") continue try: sample = [float(parts[0]), float(parts[1])] except ValueError: print("输入的不是数字,请重新输入") continue sample_std = [(sample[j] - mu[j]) / sigma[j] for j in range(2)] prob = sigmoid(dot(w, sample_std) + b) label = 1 if prob >= 0.5 else 0 print(f"输入 {sample} → 预测类别 {label}(概率 {prob:.4f})") # ================================================================ 5. 主程序 def main(): print("=" * 50) print("简单机器学习示例:逻辑回归二分类") print("=" * 50) # 1) 生成数据并划分 X, y = make_data() X_train, X_test, y_train, y_test = train_test_split(X, y) print(f"训练集 {len(X_train)} 条,测试集 {len(X_test)} 条\n") # 2) 标准化 X_train, X_test, mu, sigma = standardize(X_train, X_test) # 3) 训练 print("开始训练:") w, b, losses = train_logistic_regression(X_train, y_train) # 4) 评估 train_acc = accuracy(y_train, predict(w, b, X_train)) test_acc = accuracy(y_test, predict(w, b, X_test)) print(f"\n训练集准确率:{train_acc * 100:.2f}%") print(f"测试集准确率:{test_acc * 100:.2f}%") # 5) 打印参数 print(f"\n权重 w = {[round(v, 4) for v in w]}") print(f"偏置 b = {b:.4f}") # 6) 保存模型(保存在脚本所在目录) path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "model.json") try: save_model(path, w, b, mu, sigma) except OSError as e: print(f"[警告] 模型保存失败(不影响本示例运行):{e}") # 7) 单样本预测示例 sample = [0.5, 0.5] sample_std = [(sample[j] - mu[j]) / sigma[j] for j in range(len(sample))] prob = sigmoid(dot(w, sample_std) + b) print(f"\n对样本 {sample} 的预测:类别 {1 if prob >= 0.5 else 0}(概率 {prob:.4f})") # 8) 进入手动测试模式,可反复输入自己的数字 interactive(w, b, mu, sigma) if __name__ == "__main__": main()最终运行结果
结果分析
从运行结果可以看出,模型的损失值从第 1 轮的 0.6646 下降到第 2000 轮的 0.1073,说明梯度下降正在不断优化模型参数。
训练集准确率为 95.62%,测试集准确率为 97.50%。测试集准确率略高于训练集,并不意味着模型出现异常,而是因为测试集只有 80 条数据,样本数量较少,准确率会受到个别样本影响。
最终权重约为:
w = [3.6356, 3.2681]
两个权重都是正数,符合数据设计。因为类别 1 的数据中心在 (1, 1),两个特征值越大,样本越可能属于类别 1。偏置约为 0.0020,非常接近 0,也符合两个类别中心关于原点对称的特点。
对于输入样本 [0.5, 0.5],模型给出的类别 1 概率为 0.9394,所以最终预测结果为类别 1。这个结果与两类数据的分布规律一致,说明模型已经学到了有效的分类边界。
总结
这个示例完整展示了逻辑回归的基本训练过程:
- 生成二维二分类数据。
- 按 8:2 划分训练集和测试集。
- 使用训练集参数完成标准化。
- 使用 Sigmoid 和交叉熵构建分类模型。
- 使用批量梯度下降更新参数。
- 使用准确率评价模型。
- 将模型参数保存为 model.json。
- 支持手动输入数字进行实时预测。
整个程序不需要 numpy 或 scikit-learn,因此适合用于理解机器学习算法的底层计算过程。后续可以继续增加损失曲线绘制、更多特征、L2 正则化、学习率调整,以及使用真实数据集进行训练。