Python手写机器学习算法:从逻辑回归到多分类实战指南
发布时间:2026/9/27 2:32:13来源:尧图网络
简介这份PDF面向零基础到入门的机器学习学习者系统讲解如何用Python落地经典算法帮助读者从理论认知过渡到动手实践。内容围绕scikit-learn与Keras展开覆盖KNN、朴素贝叶斯、逻辑回归、SVM、决策树、随机森林、感知机、多层前向网络及CNN等模型并延伸至问题定义、评价指标选择、数据清洗、缺失值填充、标准化、PCA降维与特征工程等关键环节同时涉及Numpy数值计算与PIL图像处理。资源为单个PDF文件压缩包约35KB轻量便于随时查阅。目前已有2424人学习适合希望快速建立机器学习实践框架的初学者也可作为课程实验与项目练手的参考材料帮助读者理解模型训练、参数调优、交叉验证与性能优化的完整流程。1. 从一份 PDF 标题说起Python 手写机器学习算法到底值不值得做很多人第一次看到「手把手教你使用Python实现机器学习算法.pdf」这类标题第一反应是收藏第二反应是再也没打开过。我特别理解因为我自己硬盘里就躺着七八个类似名字的 PDF直到某次面试被问到「逻辑回归的梯度你自己推一遍」才发现光会sklearn.fit()根本救不了场。这份标题真正指向的不是又一份调包教程而是用 Python 从零把算法写一遍的动手路线自己实现假设函数、损失函数、梯度更新再拿真实数据跑通。它适合两类人——刚学完 Python 基础语法、想搞懂机器学习算法内部到底在算什么的新手以及用惯了框架、想回头补底层直觉的从业者。下面我按自己复现过的顺序把这条路拆成能直接抄作业的步骤顺带把踩过的坑一次讲清。2. 动手前的环境与数据底座别让配置问题劝退你2.1 Python 环境怎么选本地、VS Code 还是 PyCharm标题里写的是 Python 实现那第一步就是把 Python 装对。我一般推荐直接用官方安装包或 Anaconda版本锁在 3.10 或 3.11别追最新很多科学计算包对新版本的支持会滞后一两个月。装完之后编辑器二选一VS Code 轻、插件生态好适合边写边调PyCharm 对虚拟环境和调试的支持更省心适合项目稍微大一点之后。如果你搜过「vscode python环境配置」或者「pycharm配置python环境」会发现教程一大堆但核心就三件事解释器路径选对、虚拟环境建在项目目录下、终端能直接python起来。# 建一个独立虚拟环境避免污染全局包 python -m venv ml_from_scratch # 激活Windows 用 ml_from_scratch\Scripts\activate source ml_from_scratch/bin/activate # 装本次要用的核心包 pip install numpy pandas matplotlib scikit-learn jupyter这段命令的逻辑很直白venv建隔离环境激活后所有pip install只装进这个目录删项目时直接删文件夹就行。参数上唯一要注意的是 Python 版本python -m venv里的python必须是你想用的那个版本如果系统里有多个 Python建议写全路径比如/usr/bin/python3.11 -m venv ml_from_scratch。装完numpy负责矩阵运算pandas读数据matplotlib画图看收敛scikit-learn只用来做结果对照不参与我们自己的实现。提示如果你在 Linux 上遇到ensurepip is not available先装python3-venv系统包再建环境这是新手最常见的翻车点之一。2.2 数据集从哪来先跑通再谈规模算法要落地就得有数据。入门阶段我不建议一上来就啃大数据集用 scikit-learn 自带的几个小数据集足够验证逻辑分类用鸢尾花或乳腺癌数据集回归用波士顿房价的替代版原版因伦理问题已在新版本移除可用fetch_california_housing。如果你想练「我们已经有了一些电影的数据和分类」这类文本场景可以自己造一份带标签的电影评论小表或者用公开的中文情感数据集。关键是数据要小到你能肉眼检查大到能看出过拟合。from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split import numpy as np # 加载鸢尾花数据150 条4 个特征3 分类 X, y load_iris(return_X_yTrue) # 只取前两个类别和前两个特征方便二维可视化 mask y 2 X, y X[mask][:, :2], y[mask] # 按 7:3 切分随机种子固定保证可复现 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) print(X_train.shape, y_train.shape) # (70, 2) (70,)这里return_X_yTrue直接返回特征和标签省去字典取值mask y 2把三分类降成二分类是因为我们第一个要手写的逻辑回归本质是二分类器多分类后面用一对多扩展。random_state42是血泪经验不固定种子每次切分结果不同调参时你根本分不清是模型变了还是数据变了。特征只取两个是为了能在平面上画出决策边界等逻辑跑通了再换全特征。3. 手写第一个算法逻辑回归的假设、损失与梯度3.1 假设函数和 Sigmoid为什么不能直接用线性回归逻辑回归的名字有迷惑性它做的是分类不是回归。核心是把线性组合z w·x b塞进 Sigmoid 函数压到 0 到 1 之间当概率用。为什么不能直接拿线性输出比阈值因为线性输出无界损失函数会非凸梯度下降容易卡住。Sigmoid 把输出限制在 (0,1)配合交叉熵损失整个优化问题变成凸的这是选它的根本理由。def sigmoid(z): # 防止 exp 溢出z 太大时直接返回 1太小返回 0 z np.clip(z, -500, 500) return 1.0 / (1.0 np.exp(-z)) def predict_proba(X, w, b): # X: (n_samples, n_features), w: (n_features,), b: scalar return sigmoid(X w b)sigmoid里那句np.clip是必须的不加的话当z到 -1000 时np.exp(1000)直接溢出成inf训练中途报 warning 还找不到原因。X w是矩阵乘法比循环快几个数量级这是用 numpy 而不是纯 Python 列表的原因。参数w是权重向量长度等于特征数b是偏置标量别小看它没有b决策边界必须过原点很多数据根本拟合不了。3.2 交叉熵损失和梯度推导手算一遍再写代码损失函数用交叉熵公式是L -1/n * Σ[y·log(p) (1-y)·log(1-p)]。它对w的梯度有个非常漂亮的结果∂L/∂w 1/n * Xᵀ·(p - y)对b的梯度是1/n * Σ(p - y)。这个形式和对率回归的极大似然估计一致也是它好优化的原因。我建议你拿纸推一遍推完再写代码否则调参时梯度爆炸你都不知道从哪查。def compute_loss_and_grad(X, y, w, b): n X.shape[0] p predict_proba(X, w, b) # 加 1e-9 防止 log(0) loss -np.mean(y * np.log(p 1e-9) (1 - y) * np.log(1 - p 1e-9)) # 梯度注意是 (p - y) 不是 (y - p)符号错了会越训越差 dw X.T (p - y) / n db np.sum(p - y) / n return loss, dw, db1e-9是数值稳定项当p恰好等于 0 或 1 时log会变成-inf加个小量绕过去。X.T (p - y)里X.T是转置形状从(n, d)变(d, n)乘上(n,)的误差向量得到(d,)的梯度维度和w对齐。符号上p - y表示预测比真实大时梯度为正更新时w往负方向走这是梯度下降的定义决定的写反了损失会不降反升。3.3 训练循环与学习率三个必调参数训练就是反复算梯度、更新参数。学习率、迭代次数、是否加正则是这套流程里最影响结果的三个旋钮。学习率太大损失震荡甚至发散太小收敛慢到你以为代码卡死。我一般先用 0.1 试看损失曲线再决定调大还是调小。def train(X, y, lr0.1, epochs1000, verbose_every100): n, d X.shape w np.zeros(d) b 0.0 losses [] for epoch in range(epochs): loss, dw, db compute_loss_and_grad(X, y, w, b) w - lr * dw b - lr * db losses.append(loss) if epoch % verbose_every 0: print(fepoch {epoch}, loss {loss:.4f}) return w, b, lossesw np.zeros(d)是标准初始化逻辑回归是凸问题从零起步没问题神经网络才需要随机初始化。lr0.1对标准化后的数据通常合适如果你的特征量纲差异大先做标准化再训否则梯度会被大数值特征主导。epochs1000是保险值实际看损失曲线连续几百轮不降就可以停。losses存下来是为了画收敛图这是判断训练是否正常的黑匣子。4. 从二分类到多分类与正则化让模型真正能用4.1 一对多策略三个分类器拼出多分类逻辑回归天生二分类多分类靠一对多One-vs-Rest有几个类别就训几个二分类器每个把当前类当正例、其余当负例预测时取概率最大的那个。鸢尾花三分类就是训三个分类器。这个策略的代价是类别多时训练成本线性增长好处是实现简单、每个分类器可解释。def train_ovr(X, y, lr0.1, epochs1000): classes np.unique(y) models {} for c in classes: # 当前类标 1其余标 0 y_bin (y c).astype(float) w, b, _ train(X, y_bin, lr, epochs, verbose_everyepochs) models[c] (w, b) return models def predict_ovr(X, models): # 堆叠每个分类器的概率取最大列 probs np.column_stack([predict_proba(X, w, b) for w, b in models.values()]) return np.array(list(models.keys()))[np.argmax(probs, axis1)](y c).astype(float)把布尔数组转成 0/1 浮点直接喂给二分类训练函数。np.column_stack把每个分类器的概率拼成(n_samples, n_classes)矩阵argmax(axis1)按行取最大概率的列索引再映射回真实类别标签。注意models.values()的顺序和models.keys()必须一致Python 3.7 后字典有序这里没问题但如果你中途改过字典结构要小心。4.2 L2 正则化过拟合的后悔药当特征多、样本少时模型会把训练集背下来测试集一塌糊涂。L2 正则化在损失里加λ/2 * ||w||²梯度里加λ * w让权重不敢太大。λ 是正则强度越大惩罚越狠。我一般从 0.01 起调看训练集和测试集准确率的差距。def compute_loss_and_grad_l2(X, y, w, b, lam0.01): n X.shape[0] p predict_proba(X, w, b) loss -np.mean(y * np.log(p 1e-9) (1 - y) * np.log(1 - p 1e-9)) loss lam / 2 * np.sum(w ** 2) # 正则项注意不含 b dw X.T (p - y) / n lam * w db np.sum(p - y) / n return loss, dw, db正则项只加在w上不加b因为偏置控制整体平移惩罚它没意义还影响拟合。lam * w让每次更新时权重额外缩小一点这就是 weight decay 的来源。λ 设太大所有权重趋近零模型退化成常数预测损失曲线会卡在一个较高值不动看到这种现象先查 λ。4.3 用 scikit-learn 对照验证别自己骗自己手写实现最怕自我感觉良好一定要拿成熟库对照。同样的数据、同样的划分sklearn的逻辑回归准确率应该和你手写的接近差太多说明实现有 bug。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline # 标准化后训练逻辑回归对量纲敏感 clf make_pipeline(StandardScaler(), LogisticRegression(max_iter1000)) clf.fit(X_train, y_train) print(sklearn 准确率:, clf.score(X_test, y_test)) # 手写版预测 models train_ovr(X_train, y_train) pred predict_ovr(X_test, models) print(手写版准确率:, np.mean(pred y_test))make_pipeline把标准化和分类器串起来避免手动fit_transform训练集、transform测试集时搞混导致数据泄漏。max_iter1000是因为 sklearn 默认迭代次数对未标准化数据常常不够会报收敛警告。两个准确率差距在 2 个百分点内算正常差距大就回去查梯度符号、学习率、是否标准化。5. 避坑与排查手写算法最容易翻车的五个地方5.1 损失变成 nan 或 inf现象训练几轮后 loss 打印出nan后续全废。原因通常是学习率太大导致z爆炸exp溢出或者log(0)。解决先把学习率降到 0.01 试确认sigmoid里有clip损失里log加了1e-9。如果还不行检查数据里有没有极端异常值标准化能缓解大部分问题。5.2 准确率卡在 50% 不动现象二分类任务准确率一直在 0.5 附近损失不降。原因八成是梯度符号写反了w - lr * dw写成了或者dw里用了(y - p)。解决拿一条样本手算梯度和更新方向确认预测变大时权重应该往哪走。另一个可能是特征没标准化某个特征数值范围是几千梯度被它主导其他特征等于没学。5.3 训练集准测试集差一大截现象训练集 99%测试集 70%。原因过拟合或者切分时数据泄漏比如先标准化全量数据再切分。解决加 L2 正则、减少特征、增加样本切分务必先切再标准化用Pipeline最稳。如果数据本身有重复样本去重后再切。5.4 多分类预测结果全是一个类现象一对多训练完所有测试样本都预测成同一类。原因某个分类器的概率输出普遍偏高argmax被它垄断常见于类别不平衡或某个分类器没收敛。解决检查每个类别的样本数必要时对少数类加权确认每个分类器都训够了轮数打印各自的损失曲线看是否收敛。5.5 换了数据集就报维度错误现象ValueError: matmul维度不匹配。原因w的长度是按旧数据特征数初始化的换数据后特征数变了但没重新初始化。解决训练函数里永远用n, d X.shape动态取维度别把d写死。另外注意y的形状有的数据集返回(n,1)有的返回(n,)用y.ravel()统一压平。6. 进阶技巧把算法封装成可复用模块并验证收敛写到这一步你已经有了能跑的代码但散在 notebook 里下次用还得复制粘贴。我的习惯是把它收成一个类接口对齐 sklearn 的fit/predict这样以后换模型、做交叉验证都能直接套。下面这个封装把标准化、训练、预测串起来还带一个收敛检查。class MyLogisticRegression: def __init__(self, lr0.1, epochs1000, lam0.01): self.lr, self.epochs, self.lam lr, epochs, lam self.w, self.b None, None self.losses_ [] def fit(self, X, y): # 内部标准化避免外部忘记 self.mu_ X.mean(axis0) self.sigma_ X.std(axis0) 1e-8 X (X - self.mu_) / self.sigma_ n, d X.shape self.w, self.b np.zeros(d), 0.0 for _ in range(self.epochs): loss, dw, db compute_loss_and_grad_l2(X, y, self.w, self.b, self.lam) self.w - self.lr * dw self.b - self.lr * db self.losses_.append(loss) return self def predict_proba(self, X): X (X - self.mu_) / self.sigma_ return predict_proba(X, self.w, self.b) def predict(self, X, threshold0.5): return (self.predict_proba(X) threshold).astype(int)self.sigma_ 1e-8防止某列方差为零时除零。标准化参数存在实例里预测时用同一套mu_和sigma_这是保证训练测试一致的关键。losses_带下划线是 sklearn 的约定表示训练后产生的属性。threshold可调类别不平衡时可以往 0.3 或 0.7 挪比重新训练省事。验证收敛我一般看两件事损失曲线是否单调下降并趋于平缓以及最后 100 轮损失的标准差是否小于 1e-4。如果损失还在明显下降说明epochs不够如果上下震荡学习率偏大。下面这段画图代码我几乎每个项目都会跑一遍。import matplotlib.pyplot as plt model MyLogisticRegression(lr0.1, epochs500, lam0.01).fit(X_train, y_train) plt.plot(model.losses_) plt.xlabel(epoch) plt.ylabel(loss) plt.title(convergence check) plt.show() # 收敛判断最后 100 轮损失波动 tail np.array(model.losses_[-100:]) print(尾部损失标准差:, tail.std())tail.std()小于 1e-4 基本可以认为收敛。如果曲线前期下降后期翘尾多半是学习率偏大在最优解附近跳调小lr或加学习率衰减。这套封装我后来直接搬去做量化策略里的信号分类换数据只改fit的输入其他不动省了大量重复劳动。最后说个我自己的教训手写算法的价值不在于替代 sklearn而在于当模型表现异常时你能凭对梯度和损失的理解快速定位是数据问题、参数问题还是实现问题。我早期调一个检测模型准确率死活上不去最后发现是标签里混了脏数据如果只会调包可能一周都查不出来。把这份 PDF 标题背后的东西真正写一遍收获的是排查问题的底气。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网