简介:基于对数几率回归模型实现西瓜与鸢尾花分类识别的期末大作业资料包,面向计算机、数据科学、人工智能等专业学生及教师,覆盖课程设计、期末大作业与毕设参考场景。压缩包共30个文件、约544KB,主要包含Python源码(.py)、Jupyter Notebook交互脚本(.ipynb)、作业报告(.doc)和项目说明(.md),并附有工程配置文件,便于还原项目结构。代码已经过验证,稳定可运行,现有295人参与学习浏览。通过源码中的详细注释和报告,可以快速理解对数几率回归原理、数据预处理、训练评估全流程,同时掌握针对西瓜与鸢尾花数据集的分类实现及运用sklearn或手写模型完成任务的完整思路;项目框架也方便迁移至其他分类数据集,适合入门进阶和二次开发。整体目录组织清晰,按模块区分源码、脚本、报告和文档,能有效辅助撰写实验报告或深化对回归模型的认识。
1. 用对数几率回归同时做西瓜和鸢尾花,先解决二分类到多分类的问题
周志华《机器学习》西瓜书的课后题和期末大作业里,出现频率最高的组合就是“西瓜数据集 + 鸢尾花数据集”。这两个数据集看着简单,但如果写代码前没想清楚一件事,后面全乱套:对数几率回归天然输出的是二分类概率,而鸢尾花有 3 个类别。直接调用 sklearn 的LogisticRegression当然能跑出结果,但你写不出报告里最重要的一段“为什么这样扩展”,也会在答辩时被问到卡壳。本文按我完成同类作业的顺序拆解:先讲原理,再处理两个数据集的差异,最后给出可运行源码、代码注释规范和报告写法,适合正在写期末大作业、想拿高分而不是只求过查重的读者。
2. 对数几率回归的核心原理:logit 变换、损失函数与梯度的推导
做大作业最容易犯的错误,是一上来就写代码,把原理抄到报告里充字数。答辩老师通常只问三个问题:为什么用对率回归而不是线性回归、损失函数怎么来的、梯度下降朝哪个方向走。这三个问题都在这章里。
2.1 为什么对率回归不是“回归”,而是分类
线性回归的输出值是整个实数域上的连续值,它假设 y 和特征 x 是线性关系。但西瓜“好瓜/坏瓜”这个标签只有 0 和 1 两种取值,直接用线性回归拟合,输出的可能是 -0.3 或 1.4,没有概率含义。核心技术动机是要在实数域和 (0, 1) 区间之间建立一个可导映射。
常见做法是引入 logit 变换:
z = w^T x + b p = 1 / (1 + exp(-z))也就是把线性组合的得分 z 输入 sigmoid 函数。反过来看,当 p 被解释为“好瓜”的概率时,对 p 做 logit 变换得到:
log(p / (1 - p)) = w^T x + b这就是“对数几率”三个字的来源:logit(p) 等于线性回归的表达式,所以它在本质上仍然是线性模型,只是在输出端加了非线性映射。对分类边界而言,p = 0.5 对应 z = 0,决策边界永远是平面,这是它比 SVM、决策树好讲清楚的地方。
2.2 交叉熵损失函数和梯度方向
如果沿用线性回归的均方误差,得到的损失函数关于参数是非凸的,梯度下降容易停在局部最优。对率回归的约定是用最大似然估计推导交叉熵损失:
L(w, b) = -sum[y * log(p) + (1 - y) * log(1 - p)]整体除以样本数 m 得到平均损失。对参数求导后梯度是:
gw = (1/m) * X^T (p - y) gb = (1/m) * sum(p - y)这个式子漂亮的地方在于:梯度等于“预测概率与真实标签的误差”乘特征。预测越不准、特征值越大,参数更新幅度越大;预测完全准确时梯度为 0。下面这个表是课堂上通常会列出的对比:
| 损失函数 | 对正确分类样本的惩罚 | 梯度是否含有 p-y 因子 | 是否建议用于对率回归 |
|---|---|---|---|
| 均方误差 | 乐观样本也可能被大改 | 否 | 不建议 |
| 交叉熵 | 概率接近正确标签时惩罚趋近 0 | 是 | 建议 |
2.3 从二分类到多分类:OvR 与 softmax 的取舍
鸢尾花数据集的 3 个类别是 setosa、versicolor、virginica,二分类的对率回归不能直接用。作业里最常见的扩展是 OvR(One vs Rest):训练 3 个二分类器,每个分类器负责把“某一类”和“其余所有类”区分开。预测时把样本分别送进 3 个分类器,取输出概率最大的类别。
sklearn 里直接在LogisticRegression里设置multi_class='ovr'或'multinomial'都能跑。区别在于:后者是 softmax 回归,一次训练学出 K 组权重,原理上等价于 K 个二分类器的组合,但梯度更新同时考虑所有类别。期末作业如果只要求实现对数几率回归,OvR 更容易在报告中解释,因为可以给出每个二分类器的准确率。
3. 西瓜与鸢尾花识别分类的数据预处理:编码、缩放与训练集划分
很多人拿到.zip源码后,第一步就跑去跑模型,完全不看数据长什么样。实际上两个数据集的属性构成差异很大,不处理直接喂给对率回归,结果会难看且不稳定。
3.1 西瓜数据集的特征编码:连续值和离散值怎么并存
西瓜数据集(周志华书中表 4.1 对应版本)包含两类属性:连续属性和离散属性。典型的离散属性是色泽、根蒂、敲声、纹理、脐部、触感,典型连续属性是密度、含糖率。直接给离散属性标 0/1/2 对线性模型是一种误导,因为“青绿=0、乌黑=1、浅白=2”会隐含顺序关系。
我一般先把离散属性做 one-hot,连续属性原样保留。处理后每条样本变成一个固定长度的数值向量:
| 原始属性 | 类型 | 处理方式 | 示例 |
|---|---|---|---|
| 色泽 | 离散 | One-Hot | 青绿 -> [1, 0, 0] |
| 根蒂 | 离散 | One-Hot | 蜷缩 -> [1, 0, 0] |
| 纹理 | 离散 | One-Hot | 清晰 -> [1, 0] |
| 密度 | 连续 | 原样或缩放 | 0.697 |
| 含糖率 | 连续 | 原样或缩放 | 0.460 |
| 好瓜 | 目标 | 映射为 0/1 | 是 -> 1 |
代码里最省事的方式是直接用 pandas 的get_dummies,或者手工建立一个字典做映射。注意 one-hot 之后特征维度会膨胀到接近 20 维,而西瓜数据集只有 17 条左右,训练集更小,对率回归很容易被稀疏特征带着走,这是后面调正则化参数时要留意的。
3.2 鸢尾花数据集的读取与数据划分
鸢尾花数据集在 sklearn 里自带,共 150 条样本、4 个连续特征、3 个类别,每个类别 50 条。相比西瓜数据集,它干净得不需要做太多编码工作,重点在于训练集与测试集的划分策略。下面的代码直接读取并查看基本信息:
from sklearn.datasets import load_iris import pandas as pd iris = load_iris() df = pd.DataFrame(iris.data, columns=iris.feature_names) df['target'] = iris.target print(df.head()) print(df.groupby('target').size())这段代码先加载数据,再用DataFrame包装并打印前 5 行和每个类别的样本数量。注意鸢尾花的 4 个特征量纲不同:花萼长宽大致在 3.0~7.9 范围,花瓣长宽大致在 1.0~6.9 范围。看起来差不多,但对梯度下降的影响不一样。特征值更大的维度在计算梯度时会获得更大的更新量,最终让模型偏向某个特征。
所以标准流程是划分训练集和测试集,再在训练集上做标准化:
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test = train_test_split( iris.data, iris.target, test_size=0.2, random_state=42, stratify=iris.target ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)stratify=iris.target保证三个类别在训练集和测试集中的比例与原始数据一致。标准化用fit_transform只在训练集上计算均值和方差,测试集仅用transform,防止信息泄漏。西瓜数据集如果做 one-hot,离散列不需要标准化,连续列密度和含糖率可以做同样的处理。
3.3 特征缩放的必要性:一个可观察的小实验
如果不做标准化就把数据直接丢进手写的梯度下降,常见现象是损失下降非常慢,或者来回震荡。原因是梯度方向被大数值特征主导,等值线变成狭长的椭圆形,步长稍大就越过最优点,步长太小则收敛极慢。你可以做个对照组:把n_iter设为 500,分别打印标准化前后前 20 轮的损失值,报告的“实验分析”部分立刻有内容可写。
标准化还有一个额外好处:正则化项对每个特征的惩罚一致。L2 正则化默认所有特征权重共享一个惩罚系数,如果特征量纲不同,等价于在不重要的特征上惩罚过度,在重要特征上惩罚不足。这一点在西瓜数据集上尤其明显,因为 one-hot 产生的大量 0/1 特征和连续特征并存。
4. 对数几率回归的 Python 源码实现:从 numpy 手写到 sklearn 对照
期末作业的源码通常要包含两个层次:一是自己实现的模型,证明你理解原理;二是与成熟库的对照实验,证明你的实现正确。这一章给出两者,并解释每个参数的意义。
4.1 用 numpy 手写一个对数几率回归类
先实现最核心的 sigmoid、交叉熵损失、梯度下降三件套。这个版本只处理二分类,接口模仿 sklearn,方便后面替换:
import numpy as np def sigmoid(z): return 1.0 / (1.0 + np.exp(-z)) class LogisticRegressionDIY: """二分类对数几率回归,梯度下降法求解""" def __init__(self, lr=0.1, n_iter=1000, tol=1e-6): self.lr = lr # 学习率 self.n_iter = n_iter # 最大迭代次数 self.tol = tol # 损失变化阈值 def fit(self, X, y): n_samples, n_features = X.shape self.w = np.zeros(n_features) self.b = 0.0 self.losses = [] for i in range(self.n_iter): z = X @ self.w + self.b p = sigmoid(z) loss = -np.mean(y * np.log(p + 1e-12) + (1 - y) * np.log(1 - p + 1e-12)) self.losses.append(loss) dw = (X.T @ (p - y)) / n_samples db = np.mean(p - y) self.w -= self.lr * dw self.b -= self.lr * db if i > 0 and abs(self.losses[-2] - loss) < self.tol: break return self def predict_proba(self, X): return sigmoid(X @ self.w + self.b) def predict(self, X, threshold=0.5): return (self.predict_proba(X) >= threshold).astype(int)代码逻辑不复杂:z = X @ self.w + self.b一次性算出所有样本的线性得分,p是对应的概率;dw和db就是上一章推导的梯度;提前停止条件用相邻两轮损失的绝对差实现。注意np.log(p + 1e-12)是数值保护,防止p取到 0 时出现无穷大。
使用这个类时有两个常见错误。第一个是忘记把 y 转为 0/1 整数,西瓜数据集里如果标签是“是/否”,直接送进去会报错或得到错误梯度。第二个是学习率设得太大,比如lr=1.0,损失在前几轮会飘到几千,之后np.exp溢出。对标准化后的数据集,lr=0.1通常是安全的起点。
4.2 多分类改造:OvR 手写循环
处理鸢尾花三分类时,我在作业里采用 OvR 策略:对每个类别训练一个二分类器,预测时比较三个分类器的正类概率。实现不需要改上面的类,循环即可:
class OvRLogistic: """用多个二分类对数几率回归实现多分类""" def __init__(self, lr=0.1, n_iter=1000): self.lr = lr self.n_iter = n_iter self.classifiers = {} def fit(self, X, y): self.classes = np.unique(y) for c in self.classes: y_binary = (y == c).astype(int) clf = LogisticRegressionDIY(lr=self.lr, n_iter=self.n_iter) clf.fit(X, y_binary) self.classifiers[c] = clf return self def predict(self, X): scores = np.column_stack([ clf.predict_proba(X) for clf in self.classifiers.values() ]) return np.array(list(self.classifiers.keys()))[np.argmax(scores, axis=1)]这里column_stack把每个分类器输出的概率按列拼接,argmax选最大的下标。注意:直接用概率做比较在理论上可行,但如果三个分类器在不同样本上的概率尺度差异较大,会更稳的做法是输出每个类的得分后再做 softmax 归一化。作业中解释 OvR 时画一张“类别 0 与其余类”的示意图就够了。
4.3 sklearn 对照版:参数怎么设才不算瞎调
对照组的代码很短,但要写清楚参数含义,不然报告中“参数分析”一段无话可说:
from sklearn.linear_model import LogisticRegression # 对西瓜二分类 clf_watermelon = LogisticRegression( C=1.0, # 正则化强度的倒数,越小正则化越强 solver='lbfgs', # 适合中小数据集,默认即此 max_iter=500 ) # 对鸢尾花三分类,显式指定 OvR clf_iris = LogisticRegression( C=1.0, solver='liblinear', multi_class='ovr', max_iter=500 )C的常见取值范围是 0.01、0.1、1、10。对西瓜数据集这种高维度小样本,one-hot 之后特征数比样本数还多,我建议从C=0.1开始试,看测试集准确率变化。solver的选择不需要过度纠结:小数据集上用lbfgs或liblinear区别不大,但手写梯度下降属于sgd思路,如果想和三组结果立体比较,可以在 sklearn 里用solver='sag',它也是梯度法家族。
两个模型的对比结果,可以整理成类似下面的表:
| 数据集 | 模型 | 测试集准确率 | F1(宏平均) |
|---|---|---|---|
| 西瓜 | 手写实现 | 0.786 | 0.799 |
| 西瓜 | sklearn | 0.800 | 0.812 |
| 鸢尾花 | 手写 OvR | 0.900 | 0.899 |
| 鸢尾花 | sklearn OvR | 0.933 | 0.932 |
数值会因随机种子和数据划分而变,但只要手写版和 sklearn 版在同一个数量级,就说明实现基本正确。如果差距超过 10 个百分点,优先怀疑是否忘了标准化,其次是检查梯度更新公式里的括号和除法。
5. 期末作业提交前的自查:报告、注释与模型验证
最后一步不是写新功能,而是把现有的东西打磨成可提交状态。作业报告和注释的质量,往往决定了答辩时老师是追问还是放你走。
5.1 作业报告的结构设计
一份够用的报告不需要长篇大论,建议按下述模块组织,每个模块控制在一页以内:问题定义(两个数据集各是什么分类任务)、模型原理(logit 变换、损失函数、梯度推导)、数据集与预处理(表格展示编码前后维度变化)、实验结果(准确率表、训练损失曲线)、结论与改进。
报告里最容易出彩的地方是画训练损失曲线。在手写实现中记录self.losses,用 matplotlib 画出来,并对比两种学习率下的曲线。这个图能同时说明“梯度下降能收敛”和“学习率参数敏感”两个点,比大段文字有效。
5.2 代码注释规范和提交前检查清单
注释不需要每一行都写,但要在类和函数头部写明输入输出,在关键公式处标注对应哪一行数学表达式。比如dw = (X.T @ (p - y)) / n_samples上一行写“公式:梯度 = X^T(p-y)/m”,老师扫一眼就能对上。
提交前的自查我按这个顺序过:
- 代码里是否有绝对路径,如
C:/Users/xxx/,有则改成相对路径 - 模型训练是否固定
random_state,保证结果可复现 - 两个数据集的预处理链路是否分离,西瓜有没有误用鸢尾花的标准化器
- 测试集准确率和报告中的数字是否一致,避免报告写 0.9 但代码跑出 0.7
最后一个自查技巧:在zip重新解压到一个新目录,只保留说明文档里列出的依赖包,用python main.py全流程跑一遍。能在裸环境跑通,才是真正的高分交付。
本文还有配套的精品资源,点击获取