简介面向机器学习初学者的期末大作业方案围绕对数几率回归模型使用西瓜与鸢尾花两个经典数据集完成识别分类覆盖特征处理、模型构建、参数训练、准确率评估与可视化等完整流程对应机器学习课程中常见的二分类实验场景可直接用于课程大作业或作为对率回归算法入门项目。压缩包约544KB共30个文件典型内容包含6个Python源码、4个Jupyter Notebook、2份Word算法实践报告以及项目说明、开发环境配置等辅助文件源码带有详细注释Notebook便于分步复现报告可对照梳理研究思路有助于理解对数几率回归的推导与梯度更新过程。目前已有295人学习下载适合计算机、数据科学、人工智能等专业学生参考随包附带的实验报告和分步Notebook可帮助快速完成结课材料也支持基于现有代码扩展新数据集或二次开发用于毕设或初期项目演示。1. 对数几率回归课程设计为什么手写一个分类器比调包更值得期末周最焦虑的时刻往往是代码还没跑通报告还一个字没写。这个「期末大作业基于对数几率回归模型实现西瓜鸢尾花识别分类 python 源码 作业报告 详细注释」的标题指向的正是这样一套完整交付物模型代码、实验报告、注释齐全的源码包。对数几率回归Logistic Regression是机器学习入门绕不开的模型它虽然逻辑简单但牵扯到的损失函数、梯度下降、特征归一化、多分类策略几乎覆盖了分类任务的全部核心知识点。这个作业的巧妙之处在于西瓜数据集和鸢尾花数据集一个样本少、一个类别多恰好逼着你把二分类和多分类都亲手做一遍。对正在选课程设计题目的学生来说这个方向投入产出比很高对想补基础的在职开发者来说它也是一份能快速建立分类模型直觉的可复现代码。本文就顺着「理论 → 手写实现 → 踩坑 → 可视化」这条线把这个作业做厚。2. 对数几率回归的数学内核损失函数、梯度推导与决策边界2.1 线性回归到对数几率回归概率是怎么从「分数」变出来的线性回归的输出是连续的实数值直接拿来做分类遇到离群点会非常敏感。比如用含糖率预测西瓜好坏训练集里最甜的瓜是 0.45测试集来了一个 0.60 的瓜线性模型给出的分数会冲到很高但实际它只是「好瓜」分数和概率之间没有天然映射关系。对数几率回归的思路是保持线性模型的形式z w^T x b再用 sigmoid 函数把 z 压缩到 0 到 1 之间得到类别概率。这一步压缩不只是为了让输出落在概率区间还保住了单调性——z 越大p 越大决策边界就是w^T x b 0这条线。所谓「对数几率」的命名也很直白把概率写成几率odds再取对数ln(p / (1-p)) w^T x b所以这个模型本质上是在对「几率的对数」做线性回归。为什么这个作业选它而不是 SVM 或决策树因为 SVM 直接找最大间隔决策树靠信息增益切分这两者都绕过了概率建模。而这里的任务——西瓜好坏、鸢尾花品种——需要你向老师解释「每个测试样本为什么被判成这一类」对数几率回归给出的是概率解释成本最低。贴一下阈值决策的示意逻辑import numpy as np def sigmoid(z): return 1 / (1 np.exp(-z)) def predict_probability(w, b, x): z np.dot(w, x) b return sigmoid(z)sigmoid函数接收线性得分z无论z是 -10 还是 10输出都落在 (0, 1)。predict_probability返回的是正类的概率最终分类时再套一层p 0.5的阈值。这段代码看起来简单但它是对数几率回归最内核的东西后面的梯度下降、损失函数、多分类扩展全部建立在这三行之上。2.2 损失函数与梯度更新为什么是交叉熵而不是均方误差一个常见的作业雷区是有人把线性回归的均方误差MSE直接拿过来用于分类结果训练损失死活不降。原因是 sigmoid 函数在两端饱和MSE 的梯度趋近于 0梯度下降更新变得极慢。对数几率回归正确的损失函数来自极大似然估计对单个样本来说预测概率为 p 时它的似然是p^y * (1-p)^(1-y)取负对数再做平均就得到交叉熵损失L -1/N * Σ[y * ln(p) (1-y) * ln(1-p)]这个损失函数的梯度形式非常漂亮。对权重向量 w 求偏导结果是(p - y) * x也就是说误差越大梯度越大更新越猛误差趋近于 0 时梯度也趋近于 0。这比 MSE 在饱和区的表现健康得多。手写梯度下降更新时最常用的风格是 batch 梯度下降也就是每个 epoch 拿全部训练样本计算一次平均梯度。留意图中正则化项作业里如果能加上 L2 正则报告会更有层次。更新公式是# 梯度下降更新一轮的伪代码 gradient (X.T (sigmoid(X w) - y)) / m alpha * w w w - learning_rate * gradient第一项来自交叉熵损失对 w 的偏导第二项alpha * w是 L2 正则的梯度alpha是正则化强度用来惩罚过大的权重抑制过拟合。learning_rate控制每一步走多远一般从 0.01 起步学习率太大会让损失来回震荡太小则几百轮都收敛不到理想位置。2.3 二分类扩展到三分类OvR 策略与软分配题目里鸢尾花有三个类别而对数几率回归天然是二分类器。常见做法是 OvROne-vs-Rest为每个类别训练一个二分类器当前类为正类其余都算负类。预测阶段把某个样本分别送入 3 个分类器得到 3 个原始得分哪个得分高就归哪一类。但这里有个细节经常被忽略每个分类器都是用不同的正负类组合训练出来的它们的得分尺度不完全一致。有的同学直接比较 3 个分类器的 sigmoid 输出结果发现所有样本都分到同一类——原因就是不同分类器的概率校准不在一个量纲上。更稳的做法是把 3 个得分向量拼在一起做 softmax得到一组合计为 1 的概率分布再取 argmax。这相当于给三个分类器的输出做一个软分配直接避免了分数尺度不一致的坑。如果你看过 sklearn 的LogisticRegression会知道它内部用的是 softmax 多项式回归而不是 OvR但课程作业通常要求你体现对二分类基础模型的理解OvR 的手写实现反而更能展示原理掌握程度。决策树在这个任务上也能跑但边界是一段段折线解释起来不如对数几率回归的线性边界直观。作业报告里如果能把 OvR 和 softmax 的取舍写出来老师会认为你是真的理解了这个模型。3. 用 Python 从零实现数据预处理、梯度下降与三分类训练3.1 西瓜与鸢尾花数据集读取、划分与特征归一化先把数据集准备好。鸢尾花数据集是 sklearn 内置的150 条样本、4 个特征、3 个类别数据质量高适合做主实验西瓜数据集是 UCI 的经典数据常见版本只有 17 条样本恰好用来展示小样本情况下模型的行为差异。加载和划分代码如下import numpy as np import pandas as pd from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 加载鸢尾花 iris load_iris() X_iris, y_iris iris.data, iris.target # 加载西瓜数据集假设本地 CSV 包含密度、含糖率两列连续特征 melon_df pd.read_csv(watermelon3.0.csv) X_melon melon_df[[density, sugar]].values y_melon (melon_df[label] 是).astype(int).values # 划分训练测试集 X_train, X_test, y_train, y_test train_test_split( X_iris, y_iris, test_size0.3, random_state42, stratifyy_iris )train_test_split里的stratifyy_iris是关键参数它保证划分后的训练集和测试集里三个类别的比例与原数据集一致避免某种花在测试集里恰好缺了。西瓜数据集样本太少通常不划分测试集而是把所有数据都参与训练用留一法Leave-One-Out评估报告中要说明这一点否则老师会质疑测试集只有 5 条样本的统计意义。特征归一化这一步不能偷懒。对数几率回归的梯度更新与特征尺度直接相关如果含糖率的取值范围是 0.1 到 0.5另一个特征取值范围是 10 到 100梯度下降会沿着数值大的特征方向震荡。用StandardScaler把每个特征变成均值为 0、方差为 1 的分布后训练会更平滑。注意标准化用的是训练集的均值和方差测试集直接沿用这套参数不能重新计算。3.2 手写 sigmoid、损失函数与训练循环接下来是最核心的部分手写一个不依赖 sklearn 的 LogisticRegression 训练器。这个阶段分三个函数一是 sigmoid二是带正则的交叉熵损失三是梯度下降训练循环。完整代码如下class LogisticRegressionManual: def __init__(self, learning_rate0.01, epochs500, alpha0.0): self.lr learning_rate self.epochs epochs self.alpha alpha self.w None def sigmoid(self, z): # 防止 z 过大导致 exp 溢出先做截断处理 z np.clip(z, -100, 100) return 1.0 / (1.0 np.exp(-z)) def fit(self, X, y): # 在 X 前拼接一列 1对应偏置项 b X np.column_stack([np.ones(X.shape[0]), X]) n_samples, n_features X.shape self.w np.zeros(n_features) for epoch in range(self.epochs): pred self.sigmoid(X self.w) error pred - y # 梯度平均 L2 正则项注意偏置 w[0] 不参与正则 gradient (X.T error) / n_samples gradient[1:] self.alpha * self.w[1:] self.w - self.lr * gradient if epoch % 100 0: loss self._loss(pred, y) print(fepoch {epoch}, loss {loss:.4f}) return self def _loss(self, pred, y): # 加 epsilon 防止 log(0) eps 1e-8 return -np.mean(y * np.log(pred eps) (1 - y) * np.log(1 - pred eps))这段代码里值得注意的有三处。第一np.clip(z, -100, 100)防止 sigmoid 内部np.exp(-z)溢出训练初期特征没归一化时这一步直接决定你会不会看到 nan。第二梯度里偏置w[0]不参与 L2 正则这是通用惯例因为偏置只负责平移决策边界不应该被惩罚。第三每 100 个 epoch 打印一次损失这个输出是作业报告里损失曲线的数据来源最好保留。正则强度alpha默认设为 0跑基线实验时不开正则遇到测试集准确率明显低于训练集时再从 0.01、0.1、1.0 里尝试。learning_rate如果设成 0.1损失容易在底部震荡设成 0.001收敛要好几千轮。对这两个数据集0.01 是一个比较稳的起步值。3.3 多分类扩展三行代码从二分类到三分类手写二分类器跑通后多分类就是在外面套一个 OvR 循环。对每个类别 i把 y i 的样本标记为正类1其余改成负类0训练一个二分类器。三个分类器训练完成后预测阶段把 3 个得分向量堆成一个矩阵做 softmax 后取概率最大的索引。代码如下def train_ovr(X_train, y_train, **kwargs): classes np.unique(y_train) classifiers {} for cls in classes: y_binary (y_train cls).astype(int) model LogisticRegressionManual(**kwargs) model.fit(X_train, y_binary) classifiers[cls] model return classifiers def predict_ovr(classifiers, X): # 收集每个模型对同一批样本的线性得分拼成矩阵 scores [] for cls in sorted(classifiers.keys()): clf classifiers[cls] X_with_bias np.column_stack([np.ones(X.shape[0]), X]) scores.append(clf.sigmoid(X_with_bias clf.w)) score_matrix np.vstack(scores).T # softmax 稳定版本防止分子分母都很大造成溢出 exp_scores np.exp(score_matrix - np.max(score_matrix, axis1, keepdimsTrue)) prob_matrix exp_scores / exp_scores.sum(axis1, keepdimsTrue) return np.argmax(prob_matrix, axis1)train_ovr里每个子模型的超参数通过**kwargs透传这样后面调学习率时不用改动模型代码。predict_ovr中的np.max(score_matrix, axis1, keepdimsTrue)是 softmax 的数值稳定技巧把所有得分减去最大值后再做指数运算不会破坏 argmax 结果但能避免 exp 溢出。最后argmax返回 0、1、2 三个索引中的最大值位置。跑完之后用accuracy (predict_ovr(classifiers, X_test) y_test).mean()算准确率鸢尾花通常能到 95% 左右少数测试样本会落在 setosa 和 versicolor 交界的概率附近。这不是代码 bug而是线性决策边界的固有限制报告中如实描述即可。4. 常见问题与避坑损失不下降、全预测成同一类、报告被批空洞4.1 损失函数打印出 nan 或干脆不下降先查归一化再看学习率现象训练循环里 loss 输出要么是 nan要么一开始是两位数几十个 epoch 之后纹丝不动。原因最常见的是特征没有归一化。对比一下鸢尾花的萼片宽度取值范围 2 到 4和花瓣长度取值范围 1 到 7差距看起来不大但西瓜数据集的密度是 0.1 到 0.9、含糖率可能跨几个数量级梯度会偏向数值大的特征方向导致损失在某个维度上来回震荡。另一个原因是学习率过大梯度直接跨越了损失函数的低谷甚至每一步都迈到损失更高的地方。还有小概率是np.exp(z)溢出z 超过 700 后exp直接变成 inf。解决先用StandardScaler做特征归一化再检查学习率。把学习率从 0.1 依次降到 0.05、0.01、0.001每换一个值就打印前 10 个 epoch 的损失看是否呈现单调下降趋势。如果特征归一化后仍然 nan去查数据里有没有缺失值或无穷大用np.isnan(X).any()验证。4.2 三分类全预测成同一类OvR 分数对比的正确姿势现象predict_ovr返回的预测值全是 0测试集准确率只有 33%——相当于瞎猜。原因这个坑经常出现在直接从三个二分类器里取 sigmoid 输出做比较的写法里。三个模型的训练正负类样本比例不同比如类别 0 有 50 条正样本、100 条负样本类别 2 有 50 条正样本、50 条负样本它们的概率输出天然不在同一尺度。于是模型 0 给所有样本的输出都偏高argmax永远选中它。解决不要直接比较原始 sigmoid 输出改成先堆叠分数、再做整体 softmax 的方法就是前面代码里predict_ovr的做法。这一步把每个样本的 3 个分数归一化成和为 1 的概率分布相当于在比较排序关系而不是绝对值。另一个备选方案是把三个模型输出的原始得分直接取最大不套 sigmoid也能得到与 softmax 相同的 argmax 结果但 softmax 可以让概率解释更自然。4.3 训练集准确率 100%、测试集只有 70%过拟合与 L2 正则化现象训练集上的准确率完全是满分但在测试集上明显缩水或者损失曲线表现为训练损失持续下降、测试损失在某个 epoch 后掉头向上。原因logistic 回归照样过拟合尤其当特征维度很高而样本很少时。西瓜数据集只有 17 条样本如果加入全部离散特征并做 one-hot 后仍然用线性模型硬拟合模型会把训练集里的噪声当成规律。epoch 过多也是因素梯度下降在训练集上反复滚动权重持续增长决策边界越来越贴合训练样本。解决LogisticRegressionManual里已经预留了alpha正则参数把alpha0.1传进去重跑观察测试集准确率变化。还有一个简单办法是早停训练到 300 个 epoch 时计算一次测试准确率存下当前权重如果后续 epoch 测试准确率不再提升就回退到最优位置。记住正则系数不是越大越好过大比如 10.0会把权重压到接近零模型退化成「只看偏置随机猜测」——要在 0.01 到 1.0 之间做小网格搜索。4.4 决策边界画出来是歪的特征选择与可视化维度现象用 matplotlib 画决策边界时出来的直线看起来扭曲或分层不像预期那样干净地切分两类样本。原因画图时通常只用两个特征作为坐标轴但训练时喂了四个特征。另一部分被忽略的特征所携带的判别信息无法在二维平面上体现散点和边界自然会出现错位。同样的问题也出现在「先标准化再训练、画图时却用原始特征值」的场景边界和散点根本对不上。解决分两种情况处理。为了可视化就只选两个特征重新训练一个模型绘图代码里用的数据要和训练数据完全一致为了拿到最佳分类准确率就用全部四个特征训练可视化时用一个网格覆盖二维平面对网格内每个点用完整四维特征向量其余维度取均值去预测再画等高线。后者的等高线才反映全特征模型的真实决策形状比直接画一条直线更有说服力。4.5 报告写作课程作业想要的是「过程」而不是「结果」现象作业报告交上去老师给的评语是「缺乏分析深度」或者答辩时被问「这个权重为什么是正的」答不上来。原因很多同学把报告写成了 sklearn 的 API 文档只贴代码、只写最终准确率没有任何中间过程的分析。准确率 95% 只是结果老师真正想看到的是你尝试过什么、哪里失败了、最终怎么解决的。数据探索部分缺失也是报告显得空洞的原因。解决报告按「问题定义 → 数据探索 → 模型推导 → 实验设置 → 结果分析 → 结论」六段来写。数据探索要写每个类别的样本数量、特征均值方差、散点图分布模型推导要手动写出损失函数对 w 的梯度推导过程这是加分项实验设置要写学习率、epoch、正则系数、数据集划分方式。结果分析要附上损失曲线、混淆矩阵、决策边界图三件套。最后一定要讨论「多分类为什么用 OvR 而不是直接 softmax」和「线性模型在鸢尾花数据上的边界在哪」这两个问题能体现出你踩过坑之后真的想明白了。5. 验收前最有价值的一个技巧把「模型状态」可视化出来准备验收材料和报告配图时最有价值的不是最终准确率那个数字而是训练过程中展示模型状态的三张图损失曲线、决策边界、权重柱状图。损失曲线证明模型在收敛而不是在乱跳决策边界图证明分类器学到的规律是合理的权重柱状图能直接回答「哪个特征对判别贡献最大」这种高频答辩问题。损失曲线可以这样画import matplotlib.pyplot as plt # 假设训练循环里用 history 列表记录了每个 epoch 的损失 plt.plot(range(len(history)), history) plt.xlabel(epoch) plt.ylabel(loss) plt.title(Training Loss Curve) plt.grid(True) plt.savefig(loss_curve.png, dpi150)图片存成 150 dpi 的 PNG插入报告后足够清晰。如果损失曲线是锯齿状上下跳动说明学习率偏大如果 500 轮后仍在稳步下降说明 epoch 不够可以提高到 2000 后再截图。决策边界图的绘制代码相对固定创建一个二维网格把每个网格点送进模型预测用等高线把分类区域涂上不同颜色再把原始散点画在上面。画完看一眼边界附近的重叠程度就能直接判断线性模型在这份数据上的天花板在哪里。权重柱状图则简单得多训练完成后把model.w[1:]和特征名一一对应画一张条形图。比如鸢尾花模型里花瓣长度和花瓣宽度的权重往往是最大的正值这说明这两个特征主导了类别判断和散点图里观察到的分布规律吻合。答辩时主动说出这一步比被动等老师提问效果好得多。我自己的习惯是任何模型调完参先看这三张图再决定是否继续而不是只看准确率。准确率会撒谎图不会。这篇期末大作业想要真正吃透建议你也按这个顺序跑一遍实验——先手写实现再对照 sklearn 版验证数值差异最后把可视化输出放进报告。希望帮到你。本文还有配套的精品资源点击获取