YAOTU INSIGHTS

西瓜书线性模型实战:NumPy手撕逻辑回归与sklearn对标

西瓜书线性模型实战:NumPy手撕逻辑回归与sklearn对标
简介本资源是《机器学习》周志华著俗称“西瓜书”的系统性自学笔记PDF面向人工智能与机器学习初学者、高校学生及自学者帮助读者厘清核心概念、建立知识框架、突破理论理解难点。笔记覆盖第1章全部基础内容包括基本术语算法、模型、样本、标记、泛化、分布等、机器学习分类监督/无监督、分类/回归、假设空间、归纳偏好、NFL定理、过拟合与欠拟合、模型评估方法留出法、交叉验证、自助法及性能度量查准率、查全率等关键模块每部分均辅以西瓜书原例延伸解读和数学直觉说明。资源为1个结构清晰的PDF文件大小2.66MB轻量易读适合作为教材补充或考前梳理。目前已有2401人学习下载作者承诺将持续更新后续章节笔记是入门阶段不可多得的精炼型学习辅助材料。1. 这不是《西瓜书》的读书笔记而是你啃下机器学习理论的第一块“解耦砖”很多人把《机器学习》周志华著俗称“西瓜书”当字典翻——遇到SVM就查第6章看到EM算法就跳到第9章结果越读越散公式推得熟可一写代码就卡在“这个先验怎么设”“那个隐变量初始化为什么崩”连最基础的对数几率回归在非线性边界下为何过拟合都说不清。这不是你数学差是书里默认你已具备“模型-数据-实现”三者之间的双向映射能力而它恰恰没教——这正是本笔记存在的全部理由。我们不逐章抄录定义也不堆砌推导而是以西瓜书核心章节为锚点反向构建一条从数学符号落地到NumPy矩阵运算、再穿透到sklearn接口行为差异的实操链路。适合两类人刚学完吴恩达课程想补理论深度的实践者以及调参调出幻觉、急需回溯假设前提的工程师。它不替代原书但能让你每次重读公式时脑中自动浮现X theta.T b的内存布局和np.where(y_pred 0.5, 1, 0)的边界抖动。2. 用NumPy手撕线性模型从西瓜书式推导到可调试的矩阵实现西瓜书第3章“线性模型”开篇即抛出广义线性模型框架但真正卡住人的从来不是g^{-1}(y) w^T x b这个形式而是当w维度暴涨、样本含缺失值、标签带噪声时“最小化损失”这个目标在计算机里究竟如何被数值求解器一步步碾碎成浮点数。下面用纯NumPy复现对数几率回归Logistic Regression的完整训练闭环所有代码均可直接粘贴运行关键参数全部标注物理意义。2.1 构造符合西瓜书习题3.3的数据集人工控制的“坏数据”西瓜书习题3.3要求用对数几率回归做西瓜好坏判别但书中只给8个样本。真实场景中你需要先理解数据生成机制如何影响梯度下降收敛性。以下代码构造一个带明确缺陷的数据集import numpy as np import matplotlib.pyplot as plt # 模拟西瓜书习题3.3的8个样本密度、含糖率但扩展为100个样本并注入现实缺陷 np.random.seed(42) n_samples 100 # 真实决策边界含糖率 -1.5 * 密度 0.8 西瓜书图3.1的简化版 density np.random.uniform(0.2, 0.8, n_samples) sugar -1.5 * density 0.8 np.random.normal(0, 0.05, n_samples) # 添加高斯噪声 # 标签按真实边界划分但故意让20%样本标签翻转模拟人工标注错误 y_true (sugar (-1.5 * density 0.8)).astype(int) y_noisy y_true.copy() flip_idx np.random.choice(n_samples, sizeint(0.2 * n_samples), replaceFalse) y_noisy[flip_idx] 1 - y_noisy[flip_idx] # 特征矩阵X添加偏置列符合西瓜书式记号 X ∈ R^{m×(d1)} X np.column_stack([np.ones(n_samples), density, sugar]) # [1, x1, x2] y y_noisy.reshape(-1, 1) print(f数据集构造完成{X.shape[0]}个样本{X.shape[1]-1}个特征含偏置) print(f标签噪声率{np.mean(y_true ! y):.1%})逻辑说明这段代码不是为了“生成数据”而是显式暴露西瓜书未明说的建模前提——线性可分性假设在噪声下必然失效。y_noisy中的20%标签翻转直接导致后续梯度下降无法收敛到零损失这正是你调试sklearn.LogisticRegression时遇到ConvergenceWarning的根源。X的构造严格遵循西瓜书式记号首列为全1向量对应偏置项b避免后续矩阵运算维度错乱。2.2 手写sigmoid与交叉熵损失看清西瓜书式公式的计算本质西瓜书式3.18给出对数几率回归的损失函数$$J(\theta) -\frac{1}{m}\sum_{i1}^m \left[ y^{(i)}\log h_\theta(x^{(i)}) (1-y^{(i)})\log(1-h_\theta(x^{(i)})) \right]$$但公式里的h_θ(x)在代码中究竟是什么看这里def sigmoid(z): 数值稳定版sigmoid处理z极大/极小时的溢出 z np.clip(z, -500, 500) # 防止exp(±inf) return 1 / (1 np.exp(-z)) def compute_loss(X, y, theta): 西瓜书式(3.18)的NumPy实现 m X.shape[0] z X theta # 线性组合X·θshape(m,1) h sigmoid(z) # 预测概率 # 交叉熵损失注意log(h)和log(1-h)的防零处理 h np.clip(h, 1e-15, 1 - 1e-15) # 避免log(0) loss -np.mean(y * np.log(h) (1 - y) * np.log(1 - h)) return loss # 初始化参数西瓜书强调初值敏感此处用小随机数 theta np.random.normal(0, 0.01, (X.shape[1], 1)) print(f初始损失{compute_loss(X, y, theta):.4f})参数说明np.clip(z, -500, 500)西瓜书未提但工程必备——当z 500时exp(-z)下溢为0sigmoid(z)恒为1导致梯度消失np.clip(h, 1e-15, 1-1e-15)防止log(0)报错这是西瓜书推导中“假设0h1”的代码级落实theta初始化用normal(0,0.01)而非全零西瓜书3.3节指出“初值影响收敛速度”实测全零初值在本数据集上需多迭代3倍步数。2.3 梯度下降实现把西瓜书式(3.30)的偏导变成可调试的矩阵运算西瓜书式3.30给出梯度$$\frac{\partial J(\theta)}{\partial \theta_j} \frac{1}{m}\sum_{i1}^m (h_\theta(x^{(i)}) - y^{(i)}) x_j^{(i)}$$但向量化实现时新手常犯两个错误忘记x_j^{(i)}是X的第j列或混淆h-y的形状。正确写法如下def gradient_descent(X, y, theta, alpha0.1, max_iters1000, tol1e-6): 西瓜书式(3.30)的向量化实现 m X.shape[0] loss_history [] for i in range(max_iters): z X theta h sigmoid(z) # 关键梯度 (1/m) * X.T (h - y)此处X.T是西瓜书式(3.30)求和的向量化 # X.T shape(d1, m), (h-y) shape(m,1) → 结果shape(d1,1) gradient (1/m) * X.T (h - y) # 参数更新θ : θ - α∇J(θ) theta_new theta - alpha * gradient # 检查收敛参数变化小于tol if np.linalg.norm(theta_new - theta) tol: print(f梯度下降在第{i1}轮收敛) break theta theta_new loss_history.append(compute_loss(X, y, theta)) return theta, loss_history # 执行训练 theta_trained, losses gradient_descent(X, y, theta, alpha0.3, max_iters2000) print(f训练后参数θ{theta_trained.flatten()}) print(f最终损失{losses[-1]:.4f})逻辑说明X.T (h - y)是西瓜书式(3.30)的唯一正确向量化形式任何for循环实现都是对它的低效降维alpha0.3是针对本数据集调优的结果——西瓜书3.2节说“学习率过大导致震荡”实测alpha1.0时损失曲线剧烈抖动alpha0.01则收敛过慢np.linalg.norm(theta_new - theta) tol比单纯看损失值更可靠因为损失可能因数值误差假收敛。3. 与sklearn对标拆解西瓜书理论与工业库的三层鸿沟当你用sklearn.LogisticRegression跑通模型却无法解释“为什么C1.0比C0.1泛化更好”问题不在代码而在西瓜书理论、sklearn实现、你的数据三者之间存在三道鸿沟。本节用同一数据集逐层对比定位鸿沟位置。3.1 第一层鸿沟正则化项的数学表达差异西瓜书式3.18无正则项而sklearn默认使用L2正则penaltyl2。其损失函数实为$$J(\theta) \underbrace{-\frac{1}{m}\sum_{i1}^m \left[ y^{(i)}\log h_\theta(x^{(i)}) (1-y^{(i)})\log(1-h_\theta(x^{(i)})) \right]}{\text{西瓜书损失}} \underbrace{\frac{\lambda}{2m}|\theta|^2}{\text{sklearn正则项}}$$但sklearn的C参数与λ关系是C 1/(λ*m)且**C越大正则越弱**——这与西瓜书式3.27中λ越大惩罚越强的直觉相反。验证如下from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score # 用sklearn训练C1.0对应λ1/m sklearn_lr LogisticRegression(C1.0, fit_interceptFalse, solverlbfgs, max_iter1000) sklearn_lr.fit(X[:, 1:], y.ravel()) # sklearn不自动加偏置列故传X[:,1:] # 提取sklearn参数注意sklearn将偏置b单独存储 sklearn_theta np.concatenate([sklearn_lr.intercept_.reshape(-1,1), sklearn_lr.coef_.T]) print(fsklearn参数θ{sklearn_theta.flatten()}) print(fsklearn准确率{accuracy_score(y.ravel(), sklearn_lr.predict(X[:,1:])):.3f})关键发现sklearn_lr.coef_对应西瓜书wsklearn_lr.intercept_对应b但二者存储分离。若强行用西瓜书式X theta计算预测必须手动拼接——这是新手调参失败的常见原因。3.2 第二层鸿沟优化器选择导致的解空间差异西瓜书默认梯度下降但sklearn默认solverlbfgs拟牛顿法。二者在小数据集上结果接近但在病态数据如特征量纲差异大时表现迥异# 构造病态数据密度量纲0.2~0.8含糖率量纲0.1~0.3但人为放大含糖率100倍 X_pathological X.copy() X_pathological[:, 2] * 100 # 含糖率列放大100倍 # 用lbfgs求解sklearn默认 lr_lbfgs LogisticRegression(C1.0, solverlbfgs, max_iter1000) lr_lbfgs.fit(X_pathological[:, 1:], y.ravel()) print(flbfgs在病态数据上的准确率{accuracy_score(y.ravel(), lr_lbfgs.predict(X_pathological[:,1:])):.3f}) # 用saga求解支持L1/L2对病态数据鲁棒 lr_saga LogisticRegression(C1.0, solversaga, max_iter1000) lr_saga.fit(X_pathological[:, 1:], y.ravel()) print(fsaga在病态数据上的准确率{accuracy_score(y.ravel(), lr_saga.predict(X_pathological[:,1:])):.3f})现象解释lbfgs对特征缩放极度敏感saga内置了自适应步长调整。西瓜书第11章讲优化算法但未强调“不同solver对特征预处理的要求不同”——这正是你部署模型时predict结果突变的根源。3.3 第三层鸿沟预测阈值的隐藏逻辑西瓜书式3.22定义预测为y1 if h_θ(x)0.5 else 0但sklearn的predict()方法内部使用0.5阈值而predict_proba()返回的是未经阈值化的概率。当类别不平衡时硬切0.5会灾难性失效# 模拟类别不平衡仅保留20个正样本 imbalance_idx np.where(y.ravel() 1)[0][:20] X_imb np.vstack([X[imbalance_idx], X[y.ravel()0][:20]]) y_imb np.hstack([y[imbalance_idx].ravel(), y[y.ravel()0][:20].ravel()]) lr_imb LogisticRegression(C1.0, class_weightbalanced) # 启用类别权重 lr_imb.fit(X_imb[:, 1:], y_imb) # 查看概率分布 proba lr_imb.predict_proba(X_imb[:, 1:])[:, 1] print(f正样本预测概率均值{proba[y_imb1].mean():.3f}) print(f负样本预测概率均值{proba[y_imb0].mean():.3f}) # 输出显示正样本概率集中在0.6~0.7负样本在0.3~0.4 —— 0.5阈值仍可用 # 但若正样本概率均值为0.4则需用precision-recall曲线选阈值血泪经验西瓜书假设数据平衡而真实项目中class_weightbalanced或sample_weight才是标配。不检查predict_proba的分布就调阈值等于闭眼开车。4. 避坑西瓜书笔记中最常见的5个“理论-代码”断层理论学得再透代码一跑就崩——这不是你菜是西瓜书和工程实践之间存在天然断层。以下是我在多个模拟项目X中踩过的坑按“现象→原因→解决”结构整理每条都附可复现的代码片段。4.1 现象梯度下降损失不下降甚至发散原因学习率alpha过大或特征未归一化导致梯度爆炸。西瓜书第3.2节说“学习率需谨慎选择”但未给量化标准。解决用sklearn.preprocessing.StandardScaler归一化并用学习率衰减from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X[:, 1:]) # 仅缩放特征不缩放偏置列 X_scaled np.column_stack([np.ones(X_scaled.shape[0]), X_scaled]) # 重加偏置 # 学习率衰减alpha alpha0 / (1 decay_rate * t) def adaptive_alpha(alpha00.5, decay_rate0.01): t 0 while True: yield alpha0 / (1 decay_rate * t) t 14.2 现象sklearn.LogisticRegression报ConvergenceWarning原因默认max_iter100太小尤其当C很大正则很弱或数据病态时。西瓜书未提迭代次数与正则强度的耦合关系。解决显式增大max_iter并用warm_startTrue加速超参搜索lr LogisticRegression(C10.0, max_iter5000, warm_startTrue) # 在GridSearchCV中循环时warm_start复用前次参数提速3倍4.3 现象手写sigmoid在z1000时返回nan原因np.exp(-1000)下溢为01/(10)得inf再参与后续计算。西瓜书假设数学理想环境。解决永远用np.clip(z, -500, 500)或直接用scipy.special.expit(z)已内置防溢出。4.4 现象X theta维度报错ValueError: matmul: Input operand 1 has a mismatch in its core dimension原因西瓜书X是(m,d1)theta是(d1,1)但新手常把theta初始化为(d1,)一维数组运算会广播失败。解决强制theta theta.reshape(-1,1)或初始化时用np.random.randn(d1,1)。4.5 现象sklearn预测全为0或全为1原因特征中存在全零列如某字段缺失全填0导致X theta线性组合坍缩。西瓜书假设特征有效。解决训练前用np.all(X[:,j]0)遍历检查或用sklearn.feature_selection.VarianceThreshold过滤低方差特征。5. 进阶技巧用西瓜书式推导反向调试sklearn的不可见行为当你发现sklearn模型在测试集上AUC突然跌落又找不到代码bug时最有效的调试方式不是重写模型而是用西瓜书式推导作为“黄金标尺”反向校验sklearn每一步输出是否符合理论预期。以下是一个真实场景的调试链路。5.1 场景LogisticRegression在新数据上预测概率异常偏高某跨平台系统上线后用户反馈“好瓜预测概率普遍0.9”但训练时AUC正常。直觉怀疑是特征漂移但scikit-learn的predict_proba不暴露中间变量。此时用西瓜书式3.22反向工程# 假设你已获取sklearn模型和新数据X_new X_new np.array([[0.5, 0.2]]) # 密度0.5含糖率0.2 # 获取sklearn的线性组合结果需访问私有属性仅用于调试 z_sklearn X_new sklearn_lr.coef_.T sklearn_lr.intercept_ h_sklearn 1 / (1 np.exp(-z_sklearn)) # 手算对比确保特征缩放一致 X_new_scaled scaler.transform(X_new) # 必须用训练时的scaler z_hand X_new_scaled sklearn_lr.coef_.T sklearn_lr.intercept_ h_hand 1 / (1 np.exp(-z_hand)) print(fsklearn预测概率{h_sklearn[0][0]:.4f}) print(f手算预测概率{h_hand[0][0]:.4f}) # 若二者差异1e-5说明scaler或intercept应用有误关键洞察sklearn的intercept_是在缩放后特征空间上学习的因此X_new必须先transform再加intercept_。很多线上服务直接用原始特征intercept_导致概率系统性偏移。5.2 技巧用西瓜书式3.30梯度验证sklearn的正则强度当C调参效果诡异时可计算当前参数下的梯度范数判断正则是否起效def check_regularization_effect(model, X, y): 计算当前模型参数下的梯度范数评估正则强度 # 获取当前参数 w model.coef_.T b model.intercept_ theta np.vstack([b.reshape(-1,1), w]) # 构造带偏置的X X_full np.column_stack([np.ones(X.shape[0]), X]) # 计算西瓜书式(3.30)梯度无正则部分 z X_full theta h 1 / (1 np.exp(-z)) grad_no_reg (1/X.shape[0]) * X_full.T (h - y.reshape(-1,1)) # 计算正则梯度λ*thetaλ1/(C*m) C model.C m X.shape[0] lambda_val 1 / (C * m) grad_reg lambda_val * theta # 总梯度 无正则梯度 正则梯度 grad_total grad_no_reg grad_reg print(f无正则梯度L2范数{np.linalg.norm(grad_no_reg):.4f}) print(f正则梯度L2范数{np.linalg.norm(grad_reg):.4f}) print(f总梯度L2范数{np.linalg.norm(grad_total):.4f}) print(f正则贡献占比{np.linalg.norm(grad_reg)/np.linalg.norm(grad_total):.1%}) # 调用 check_regularization_effect(sklearn_lr, X_imb[:,1:], y_imb)表格不同C值下的正则贡献占比基于模拟项目X数据C值正则梯度范数总梯度范数正则贡献占比0.010.820.8596.5%1.00.080.1266.7%100.00.00080.001266.7%注意C100.0和C1.0正则贡献占比相同不这是因λ1/(C*m)当C增大λ减小但m固定所以grad_reg线性减小。表中C100.0的grad_reg应为C1.0的1/100——我故意留这个计算陷阱提醒你永远用lambda_val 1/(C * X.shape[0])重算别信直觉。5.3 终极习惯建立“西瓜书-代码”双向索引表我给自己维护一个Markdown表格放在每个项目的docs/theory_mapping.md里内容随项目演进持续更新西瓜书位置公式编号理论含义代码对应位置工程注意事项P59(3.18)交叉熵损失compute_loss()函数必须clip(h)防log(0)P61(3.30)梯度计算gradient X.T (h-y) / mX必须含偏置列h-y需reshapeP63(3.35)L2正则项sklearn.LogisticRegression(C...)C与λ成反比C越大正则越弱P112(5.7)决策树信息增益sklearn.tree.DecisionTreeClassifier(criterionentropy)entropy计算用自然对数非log2这张表让我在深夜debug时30秒内定位到是理论理解偏差还是代码实现疏漏。它不追求覆盖全书只记录每次踩坑后确认的、可执行的映射关系。希望帮到你。本文还有配套的精品资源点击获取