简介:面向机器学习课程期末大作业与实验环节的资料包,围绕多项式拟合正弦函数、GMM(高斯混合模型)与逻辑回归三个经典主题,打包了期末考试题、实验报告与可运行源码。逻辑回归部分覆盖两种损失函数的参数估计——无惩罚项与加入参数惩罚,支持梯度下降、共轭梯度或牛顿法实现,既可用高斯分布手工生成两类数据验证算法,也可到UCI数据集上测试广告预测等真实场景。压缩包大小约4.45MB,内容以Python源码、实验报告文档和试题文件为主,并附有Windows64、PyCharm、Python3、Anaconda环境下的调试说明;源码经严格调试可直接运行,适合计科、人工智能、通信工程、自动化、电子信息等专业的学生用于课程设计、期末大作业或项目初期演示,也便于在现有代码基础上修改扩展。当前已有170余人学习过该资源,内含完整的实验目的、实验要求、算法设计思想与可复用代码,能帮助读者快速掌握逻辑回归参数估计的完整流程,并理解类条件分布不满足朴素贝叶斯假设时模型表现会受到何种影响。
1. 从一次机器学习期末大作业说起:逻辑回归为什么值得自己动手实现
期末复习周最耗时间的不是背公式,而是把一类算法的完整链路自己跑通。逻辑回归看似简单,可实验要求里写明要实现无惩罚项和带惩罚项两种损失函数的参数估计,并验证类条件分布不满足朴素贝叶斯假设时的表现,这时候没有源码和实验报告参考,很多细节要从头摸。这个压缩包把多项式拟合正弦函数、GMM 聚类和逻辑回归三块打包在一起:前两者提供特征构造与数据生成的直觉,逻辑回归承担主实验,最后附期末考试题和完整实验报告。适合赶期末大作业的计算机、人工智能相关专业学生,也适合想快速复习参数估计算法的从业者。Windows 64 位环境配 Anaconda 和 PyCharm,跑 NumPy 与 scikit-learn 就能完整复现,上手成本很低。
2. 逻辑回归的两类损失函数:无惩罚项与L2惩罚项的理论边界
2.1 从线性判别到 sigmoid:逻辑回归的模型假设
先明确模型形式。给定特征向量 (x) 和参数 (w),逻辑回归先计算线性分数 (z = w^Tx + b),再通过 sigmoid 函数把 (z) 压缩到 (0,1) 区间,作为 (P(y=1|x)) 的估计。sigmoid 在 (z) 接近 0 时近似线性,在两端趋于饱和,这个非线性变换让线性模型也能输出概率,而不是直接输出无界分数。实现时有个容易被忽略的细节:当 (z) 很小或很大时,np.exp(-z)会溢出,产生nan或inf,所以代码里要对输入做 clip 限制。
import numpy as np def sigmoid(z): # 限制 z 的范围,避免 exp 溢出 return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))clip 的边界取 ±500 已经足够,超过这个范围后 sigmoid 的输出要么趋近 0 要么趋近 1,继续增大数值只会让 exp 计算不稳定,对梯度更新没有实际帮助。教学实验里很多人第一次跑出nan就是因为少了这一步,尤其是在没有特征归一化的情况下,w 的某个分量可能快速增大。
提示:如果数据没有做标准化,w 的不同分量尺度差异很大,clip 边界可能还需要提高到 ±1000,否则梯度更新前期仍可能溢出。
逻辑回归属于判别模型,它只建模条件概率 (P(y|x)),不需要像朴素贝叶斯那样先估计类条件分布 (P(x|y))。这一点在实验要求的第 1 条里非常关键:手工生成两组高斯分布数据时,如果两组数据具有相同的协方差矩阵,朴素贝叶斯的决策边界会趋于线性;可一旦两个类别的协方差矩阵不同,或者某个类本身是多模态的,朴素贝叶斯对 (P(x|y)) 的独立假设就会引入偏差。逻辑回归直接拟合边界,不关心 x 内部的结构,因此在这种场景下通常更稳健。GMM 在这个实验中的角色就是生成那种“不满足朴素贝叶斯假设”的数据,比如让同一个类别由两个相距较远的高斯簇组成。
2.2 交叉熵损失与梯度的闭式推导
把截距项 b 并进 w,在 X 最左边补一列 1,模型输出就是 σ(Xw)。损失函数采用交叉熵,单样本损失为-[y_i log p_i + (1-y_i) log(1-p_i)],全体样本取平均得到 J。这个形式来自伯努利分布的极大似然估计,取负对数后就是交叉熵。为什么选择交叉熵而不是均方误差?因为均方误差加上 sigmoid 以后,目标函数关于 w 非凸,梯度下降容易停在平坦区域;而交叉熵的梯度形式上非常干净,便于手工实现和验证。
推导过程可以浓缩成三步:先写出 σ(z) 的导数等于 σ(z)(1-σ(z)),再对 log 项逐个求导,最后利用分子分母互相抵消得到梯度表达式,最终结果是:
grad = (1/N) * X^T (σ(Xw) - y)这个形式意味着梯度的每一维都等于当前预测误差 (p - y) 在对应特征方向上的加权平均,误差越大,梯度越陡,参数更新越快。落到代码里:
def compute_loss_and_grad(X, y, w): N = X.shape[0] z = np.dot(X, w) p = sigmoid(z) # 加 1e-12 防止 log(0) 导致 nan loss = -np.mean(y * np.log(p + 1e-12) + (1 - y) * np.log(1 - p + 1e-12)) grad = np.dot(X.T, p - y) / N return loss, grad函数返回两个值,损失用于记录收敛过程,梯度用于后续的梯度下降、共轭梯度或牛顿法。注意 p - y 的维度是 (N,),X.T 与它做矩阵乘法后得到 (D,) 的梯度,与 w 的形状完全对应。如果你需要在实验报告里展示手写推导,损失和梯度的闭式表达式可以不写矩阵形式,改成累加形式,更利于老师核对过程。
2.3 加入L2惩罚项之后,目标函数从单峰变成强凸
实验要求的第 1 类损失是无惩罚项,第 2 类是加入对参数的惩罚。最常见的惩罚是 L2 正则化,目标函数变为:
J_reg(w) = J(w) + (λ / 2N) * Σ_{j=1}^{D-1} w_j^2这里需要细看两件事。第一,惩罚项通常只作用于特征权重,不作用于截距项,因为调整 w_0 只会平移决策边界,不会改变边界复杂度,sklearn 里的实现也是这么处理的。第二,λ 除以 N 是为了让正则项与损失项的尺度保持一致,因为你用的是平均交叉熵,如果不除以样本量,测试集大小变化时最优 λ 的取值会漂移。实现时把惩罚梯度和普通梯度分开计算,不容易出错。
def compute_loss_grad_l2(X, y, w, lmbda): N, D = X.shape loss, grad = compute_loss_and_grad(X, y, w) # 截距在 w[0],不惩罚 penalty = lmbda / (2 * N) * np.sum(w[1:] ** 2) grad_pen = np.zeros_like(w) grad_pen[1:] = lmbda / N * w[1:] return loss + penalty, grad + grad_penlmbda就是正则化强度,取 0 时退化为无惩罚项。加入 L2 后,逻辑回归原本就凸的目标函数变成强凸,意味着两点:一是梯度下降的收敛速度从可能很慢变成确定收敛;二是当特征维度高于样本数时,Hessian 矩阵原本可能不可逆,加惩罚后变成正定矩阵,牛顿法可以直接求解。从模型角度理解,L2 惩罚把每个权重向 0 收缩,等价于对参数引入均值为 0 的高斯先验,这在广告点击率预测这类高维稀疏特征场景中能有效抑制过拟合。
| 目标函数 | 凸性 | 梯度下降收敛 | 牛顿法可用性 | 适用场景 |
|---|---|---|---|---|
| 无惩罚项 | 凸,可能平坦 | 慢且依赖学习率 | 需样本量大于特征数 | 低维、特征独立 |
| L2 惩罚项 | 强凸 | 稳定 | 始终可逆 | 高维、特征相关 |
对期末实验来说,低维合成数据上无惩罚项够用,UCI 实际数据往往特征共线性强,加 L2 后你能明显看到测试集准确率更平稳。实验报告中把这两行对比结论写清楚,比贴大段推导更有说服力。
2.4 从多项式拟合正弦函数到逻辑回归的特征扩展
打包资源里单独有一个多项式拟合正弦函数的实验,很多人在做逻辑回归时觉得这两个实验毫不相关,但特征扩展的思想是相通的。正弦函数本身不是线性函数,用 (y = w_0 + w_1x) 去拟合必然是欠拟合,换成 (y = w_0 + w_1x + w_2x^2 + ... + w_nx^n),阶数越高,拟合能力越强。逻辑回归处理非线性分类边界时也做同样的事,将原始特征转成多项式特征后,原本线性不可分的点在新的多项式空间里可能就线性可分。
from sklearn.preprocessing import PolynomialFeatures # degree=3 会生成 [x1, x2, x1^2, x1*x2, x2^2, x1^3, ...] poly = PolynomialFeatures(degree=3, include_bias=False) X_poly = poly.fit_transform(X)degree是多项式阶数,include_bias=False表示不自动生成常数列,因为逻辑回归里 w[0] 已经承担截距角色。做多项式拟合正弦函数实验时,阶数从 3 开始逐步升到 15,会看到训练误差一路下降而测试误差先降后升,这就是过拟合的完整过程。回到逻辑回归上,多项式特征同样带来过拟合风险,配合 2.3 节的 L2 惩罚,可以在高次项上施加收缩,保留高次特征的同时限制它的影响。
3. 手写参数估计算法:梯度下降、共轭梯度与牛顿法的工程实现
3.1 梯度下降:学习率与收敛判据
先用最直观的梯度下降把参数估计跑通。参数更新规则是w <- w - lr * grad,lr是学习率。实现上只需要关心三点:初始化、停止条件、学习率调整。初始化直接取全零向量即可,因为逻辑回归目标函数是凸的,不同初始化会收敛到同一位置,不会像神经网络那样陷入局部最优。
def gd_fit(X, y, lr=0.5, epochs=500, lmbda=0.0): N, D = X.shape w = np.zeros(D) for epoch in range(epochs): loss, grad = compute_loss_grad_l2(X, y, w, lmbda) w -= lr * grad if epoch % 50 == 0: print(f"epoch {epoch}, loss {loss:.4f}") return w学习率是关键参数。lr 太大,w 会在最优点附近来回震荡,损失曲线像锯齿;lr 太小,500 轮可能还没走到低损失区域。我一般先用 0.5 跑一轮观察损失是否下降,出现振荡就除以 10,前 100 轮下降缓慢就乘 3。数据特征做标准化后,学习率在 0.1 到 1 之间普遍有效。收敛判据除了固定轮数,更常用的做法是看相邻两轮损失差值的绝对值小于某个阈值,比如 1e-6,同时限制最大轮数防止死循环。
3.2 牛顿法:用 Hessian 矩阵加速收敛
梯度下降只用一阶信息,每一步朝负梯度方向走,收敛速度是线性的;牛顿法则用 Hessian 矩阵刻画局部曲率,参数更新变成w <- w - H^{-1} grad,收敛速度接近二阶。逻辑回归的 Hessian 矩阵可以直接写出闭式解:H = (1/N) X^T S X,其中 S 是 N×N 对角矩阵,对角线元素为p_i(1-p_i)。这个形式很优美,因为 p_i 就是当前模型输出,每一轮迭代时重新计算即可。
def newton_fit(X, y, epochs=20, lmbda=0.0): N, D = X.shape w = np.zeros(D) for _ in range(epochs): p = sigmoid(np.dot(X, w)) S = np.diag((p * (1 - p)).flatten()) H = np.dot(X.T, np.dot(S, X)) / N grad = np.dot(X.T, p - y) / N # 加 L2 惩罚到 Hessian 和梯度 if lmbda > 0: penalty_mat = np.eye(D) * (lmbda / N) penalty_mat[0, 0] = 0.0 H += penalty_mat grad_pen = np.zeros_like(w) grad_pen[1:] = lmbda / N * w[1:] grad += grad_pen w -= np.linalg.solve(H, grad) return wnp.linalg.solve比手动计算 H 的逆矩阵更稳定,它通过矩阵分解求解线性方程组,避免了显式求逆带来的数值误差。
注意:当样本数 N 小于特征数 D 时,
X^T S X是奇异矩阵,np.linalg.solve会抛出LinAlgError。这是正常现象,说明需要加 L2 惩罚让 Hessian 变成正定矩阵,而不是代码写错。
牛顿法每轮代价远高于梯度下降,因为要构造并分解 D×D 矩阵,所以迭代轮数不需要多,20 轮以内通常就收敛到很高精度。
3.3 共轭梯度法:只给梯度也能处理大规模参数
共轭梯度法在逻辑回归里处于中间位置:它不需要显式构造 Hessian 矩阵,却保留了共轭方向带来的超线性收敛速度。手写完整 CG 逻辑稍长,教学实验里更常见的做法是使用scipy.optimize.minimize的 CG 或 BFGS 方法,只需要提供损失函数和梯度函数,算法内部会管理搜索方向和步长。
from scipy.optimize import minimize def cg_fit(X, y, lmbda=0.0): N, D = X.shape w0 = np.zeros(D) def fun(w): loss, grad = compute_loss_grad_l2(X, y, w, lmbda) return loss, grad res = minimize(fun, w0, method='CG', jac=True, options={'maxiter': 200}) return res.xjac=True告诉minimize传入的函数同时返回目标值和梯度,这样才符合接口要求。maxiter设 200 是保险值,逻辑回归的凸优化问题通常 50 次迭代以内就稳定。当数据量到几万、特征到几百时,牛顿法需要反复分解矩阵,内存和时间都吃紧,共轭梯度法只需要梯度计算和向量乘法,更适合这个量级。
3.4 三种算法在合成数据上的横评
在课程设计报告里,把三种算法放到同一组人工数据上比较是一个稳妥的增分写法。数据用前面生成的二维高斯分类数据,固定 500 个样本,三种算法都收敛后,比较三个指标:达到相同训练损失所需的迭代轮数、每轮消耗的时间、最终在测试集上的准确率。运行时间依赖机器配置,报告里建议写相对关系而不是具体秒数。
| 算法 | 每轮计算量 | 需要二阶信息 | 收敛速度 | 适合的数据规模 |
|---|---|---|---|---|
| 梯度下降 | O(ND) | 否 | 慢,线性 | 特征少、先跑通流程 |
| 共轭梯度 | O(ND) + 少量向量运算 | 否 | 中 | 特征中等到高 |
| 牛顿法 | O(ND + D^3) | 是 | 快,二阶 | 样本和特征都不大 |
写报告时注意一个常见误用:很多人把牛顿法每轮迭代少当成“总时间少”,在特征维度超过几千时,牛顿法每轮求 Hessian 和矩阵分解的代价远高于梯度下降,整体速度未必占优。横评的结论不应该是“牛顿法最优”,而应该结合数据规模说明选型逻辑,这样才显得你是自己调过参数而不是贴了一份编译通过就交付的代码。
4. 用高斯分布人造数据验证:当类条件不满足朴素贝叶斯假设时会怎样
4.1 手工生成两类高斯数据与训练集划分
实验要求第一项提到可以用高斯分布手工生成两类数据,最省事的方式是用sklearn.datasets.make_classification。它会生成带有指定特征数、类别数和簇结构的合成数据,每个簇内部呈高斯分布。参数里的n_clusters_per_class可以直接造出“一个类别包含多个高斯簇”的场景,这正是考察朴素贝叶斯假设是否被破坏的实验变量。
from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split # 2 个特征,2 类,每个类 1 个高斯簇 X, y = make_classification( n_samples=500, n_features=2, n_redundant=0, n_clusters_per_class=1, flip_y=0.05, random_state=42 ) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0 )n_redundant=0表示不生成冗余特征,保留原始两个有效维度,方便最后在二维平面上画决策边界。flip_y设置 5% 的标签噪声,更接近真实数据;如果设为 0,数据严格可分,两类算法都能拿满分,看不出差异。random_state固定后,实验报告里的图和数字可以复现,这一点在机器学习课程评分时很加分。Windows 64 位上的 Anaconda 已经自带这些库,PyCharm 里新建一个环境直接逐段运行即可。
4.2 朴素贝叶斯与逻辑回归的决策边界对比
在同样的训练集上分别拟合 GaussianNB 和 LogisticRegression,然后比较测试集准确率和决策边界形状。GaussianNB 假设每个类内的特征相互独立且服从高斯分布,如果生成数据时两个类的协方差矩阵相同,它的决策边界会接近一条直线;但如果类间协方差不同,边界会变成二次曲线,而 GaussianNB 由于独立假设可能产生明显偏移。
from sklearn.naive_bayes import GaussianNB from sklearn.linear_model import LogisticRegression gnb = GaussianNB().fit(X_train, y_train) lr = LogisticRegression(penalty='l2', C=1.0, solver='lbfgs').fit(X_train, y_train) print("GaussianNB acc:", gnb.score(X_test, y_test)) print("LogisticRegression acc:", lr.score(X_test, y_test))sklearn 里的 LogisticRegression 默认带 L2 惩罚,C=1.0是惩罚强度的倒数,C 越小正则越强,C 越大越接近无惩罚项。这里用默认参数主要是先拿到一个 baseline。准确率数字因数据而异,但现象是一致的:当两个类都只有一个高斯簇且协方差接近时,两者测试准确率都在 0.95 左右;当你把某类改成两个相距较远的簇时,朴素贝叶斯准确率下降幅度明显超过逻辑回归,因为特征在簇内和簇间呈现出的相关结构同时被独立假设抹掉了。
4.3 用 GMM 叠加多模态分布,彻底破坏独立性假设
如果想让实验证据更硬,就用 GMM 生成类内多模态数据。GaussianMixture 本身是生成模型,先用n_components=2拟合类别 0 的样本,再用它采样出一批新样本,类别 1 保持单高斯,这样制造出的数据分布中,类别 0 有两个相距较远的质心,决策边界在特征平面上呈现出明显的非线性。
from sklearn.mixture import GaussianMixture # 分别拟合两类数据的类条件分布 gmm0 = GaussianMixture(n_components=2, covariance_type='full', random_state=0).fit(X[y == 0]) gmm1 = GaussianMixture(n_components=1, random_state=0).fit(X[y == 1]) X0_new = gmm0.sample(250)[0] X1_new = gmm1.sample(250)[0]covariance_type='full'允许每个高斯分量拥有完整的协方差矩阵,意味着特征之间存在相关性,这正是朴素贝叶斯的独立性假设无法覆盖的情况。GMM 在这里有两重角色:一、作为数据生成器构造复杂类条件分布;二、它本身就是期末题里常考的无监督聚类模型。资源里单独有一个 GMM 实验,做完这个对比后,试着把逻辑回归的输入换成原始特征加 GMM 输出的簇标签,这种特征拼接思路在很多竞赛 baseline 里仍然有效。
| 数据生成方式 | 朴素贝叶斯现象 | 逻辑回归现象 |
|---|---|---|
| 同类单高斯,协方差相同 | 边界接近线性,准确率高 | 边界接近线性,准确率高 |
| 同类单高斯,协方差不同 | 独立假设导致边界偏移 | 判别式边界更贴近真实 |
| GMM 多模态混合 | 准确率明显下降 | 仍能保持较好判别能力 |
如果实验报告只放一张表,放上面这张就足够说明问题。
4.4 实验报告里体现“惩罚项效果”的曲线
逻辑回归的第二个验证点是用实际数据测试,最常见的路径是去 UCI 找一个二分类数据集,比如成人收入或银行营销数据。但 UCI 数据字段较多,处理起来要写编码和缺失值填充,很多人在这一步被卡住。更稳妥的做法是在合成数据上先做多项式特征扩展,再对比无惩罚和有惩罚两种设置的测试准确率。
from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import make_pipeline # 带 L2 的逻辑回归 pipeline = make_pipeline( PolynomialFeatures(degree=5, include_bias=False), LogisticRegression(penalty='l2', C=0.1, solver='lbfgs') ) pipeline.fit(X_train, y_train) print("L2 acc:", pipeline.score(X_test, y_test))PolynomialFeatures 将 2 个特征扩展到 5 阶后变成 20 维左右,特征复杂了,无惩罚模型会开始过拟合,训练准确率接近 1 而测试准确率下降,加 L2 后测试准确率能回升几个点。报告中画出“degree-准确率”曲线,横轴取 1 到 9,纵轴取测试准确率,两条曲线分别对应开惩罚和不开惩罚,这是最能说明正则化价值的图。注意 C 和 λ 是倒数关系,C 越小惩罚越强,在 sklearn 的网格搜索里通常设置C = 1/λ。
5. 考前自查与报告写作技巧:如何把实验报告变成加分项
5.1 实验报告必须有的四张图
一份逻辑回归实验报告,老师通常不会逐行读代码,而是看图表是否能支撑结论。第一张是损失下降曲线,横轴迭代轮数,纵轴训练损失,无惩罚和 L2 惩罚各一条;第二张是合成数据上的决策边界可视化,把背景网格上的预测概率用等高线画出来;第三张是多项式拟合正弦函数的阶数对比,3 阶、7 阶、15 阶各画一条,直观展示欠拟合、拟合和过拟合;第四张是朴素贝叶斯和逻辑回归在同一 GMM 数据上的准确率对比柱状图。四张图对应的代码都来自前面章节,整理顺序后直接嵌入报告即可。
5.2 参数调节:学习率、λ 和多项式阶数的联动
参数之间不是独立的。多项式阶数升高,特征尺度急剧变大,如果不对特征做标准化,梯度下降的学习率必须调得很小才能稳定,否则损失直接爆掉。L2 惩罚可以缓解高阶特征带来的方差,但 λ 也不能盲目加大:λ 太大,决策边界退化成直线;λ 太小,过拟合仍在。一个可复现的自检流程是先把 degree 固定在 5,跑通代码;再对学习率做 0.5、0.1、0.01 三档衰减;最后把 C 在 1e-3 到 1e2 之间取对数网格,画出验证集准确率,选择峰值对应的 C。这段逻辑可以直接写进报告的“参数讨论”小节。
5.3 从课程设计到广告点击率预测的实际迁移
如果还想多拿一点工程分,把实验里的人造数据换成 UCI 的实际二分类数据集,训练完成后用分类报告收尾:
from sklearn.metrics import classification_report, confusion_matrix pred = pipeline.predict(X_test) print(confusion_matrix(y_test, pred)) print(classification_report(y_test, pred))混淆矩阵比准确率更细,能看出模型在少数类上的表现;classification_report里的 precision、recall、f1-score 三个字段与广告点击率预测这类不平衡场景直接对应,点击率通常只有百分之几,准确率没有参考价值,这时重点看 recall 和 F1。压缩包里附的期末题也覆盖了逻辑回归推导、GMM 与朴素贝叶斯关系、多项式拟合过拟合分析三类题目,考前一天把这些图和结论对照着过一遍,比重新翻课件更接近考点。用上面这段代码跑一遍实验,把曲线和混淆矩阵整理进报告,分数往往比只贴源码高一个档位。
本文还有配套的精品资源,点击获取