news 2026/5/22 11:16:02

手写多元线性回归:从代价函数到梯度下降的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写多元线性回归:从代价函数到梯度下降的完整实现

1. 项目概述:从零手写多元线性回归,不是调包,是真正理解它怎么“动”起来

你有没有过这种感觉:调用sklearn.linear_model.LinearRegression().fit(X, y)一行代码就出结果,但当面试官问“梯度下降里那个 θ 更新公式,为什么是减号?为什么是乘以学习率 α?如果 α 太大或太小会怎样?”——你脑子里突然一片空白,只能含糊地说“它是让损失变小的算法”?这说明你还没真正“握住”这个模型的脉搏。这篇博文,就是带你亲手把多元线性回归的每一块骨头都拆开、擦亮、再严丝合缝地装回去。我们不碰任何高级封装,只用最基础的NumPyPandasMatplotlib,从零开始构建一个能跑通、能调试、能解释的完整流程。核心关键词是Cost Function(代价函数),它不是教科书里一个冷冰冰的公式,而是整个训练过程的“心跳监测仪”。它实时告诉你:模型现在离真相有多远?每一步调整是让误差变小了,还是在原地打转甚至越走越偏?我试过不下二十次,每次改一个参数,都盯着它的数值跳动,就像看着心电图一样紧张又兴奋。这个项目最适合两类人:一类是刚学完理论、想亲手验证公式的初学者;另一类是已经用过很多次sklearn、但总感觉心里没底、想彻底搞懂底层逻辑的实践者。它解决的不是“能不能跑”,而是“为什么这样跑才对”。接下来,我会像带徒弟一样,把从数据加载、特征预处理、到模型训练、结果验证的每一个环节,连同那些藏在代码注释背后、老师不会讲、文档里找不到的“实操心法”,全部摊开给你看。

2. 整体设计与思路拆解:为什么必须放弃循环,拥抱向量化?

2.1 核心设计哲学:向量化不是炫技,是生存必需

很多人第一次写梯度下降,本能地会用 for 循环遍历每一个样本,计算预测值、误差、再更新权重。我最初也是这么干的,写完一运行,500个样本的训练集,跑了整整47秒。而当我把所有循环替换成NumPy的矩阵运算后,时间直接压到了0.03秒。这不是性能优化的“锦上添花”,而是工程实践的“生死线”。原因很简单:现代 CPU 和 GPU 的核心优势在于并行计算。一个X @ theta矩阵乘法,硬件可以在一个时钟周期内同时完成成百上千个乘加运算;而一个 Python for 循环,本质上是串行的,它得一个一个数着来,中间还要反复做类型检查、内存寻址,效率天差地别。更关键的是,向量化代码的可读性和可维护性更高。当你看到cost = (1/(2*m)) * np.sum((X @ theta - y)**2)这一行,它和数学公式J(θ) = 1/(2m) * Σ(h_θ(x^(i)) - y^(i))²是一一对应的,逻辑清晰,毫无歧义。而等价的 for 循环版本,光是嵌套的缩进和变量名就能让你头晕目眩。所以,我们的整个设计基石,就是“一切皆向量”。从数据输入X(m 行 n+1 列)、权重theta(n+1 行 1 列)、真实标签y(m 行 1 列),到中间所有的误差、梯度,全部用二维或一维数组表示。这不仅是提速,更是让代码逻辑和数学原理完全对齐,让你一眼就能看出哪里出了问题。

2.2 特征缩放:不是可选项,是梯度下降的“润滑剂”

想象一下,你在一个巨大的山谷里找最低点,但这个山谷的形状极其怪异:东西方向是一条平缓的长坡,南北方向却是一道陡峭的悬崖。如果你蒙着眼睛往下走,每一步都迈同样大小,那你在平缓的东-西方向上可能要走几千步才能挪动一点点,在陡峭的南-北方向上却可能一步就滑下悬崖,直接摔死。这就是没有特征缩放的梯度下降。在“Graduate Admission”数据集中,“GRE Scores”范围是 290-340(跨度约50),“Undergraduate GPA”是 6.8-9.9(跨度约3.1),“Research Experience”只有 0 或 1(跨度为1)。这三个特征的量纲和尺度天差地别。如果不做处理,梯度下降在GPA方向上的更新会非常“吝啬”,而在GRE方向上却可能“大步流星”,导致整个优化路径像醉汉一样左右摇摆,收敛速度极慢,甚至根本无法收敛。我实测过,不缩放时,要达到一个还过得去的精度,迭代次数轻松破万;而做了缩放后,20-30次迭代就能稳定下来。这就是为什么我们必须引入Feature Scaling。它不是为了“让数据看起来更漂亮”,而是为了给梯度下降铺一条平坦、均匀的跑道。我们最终选择了Min-Max Scaling(最小-最大缩放),公式是(x - x_min) / (x_max - x_min)。它的优点是结果被严格限制在 [0, 1] 区间内,物理意义直观,且对异常值的鲁棒性比标准化(Standardization)稍好一点。当然,选择它也意味着我们放弃了“均值为0、方差为1”的统计特性,但这对我们当前的纯优化任务来说,并非必需。

2.3 模块化分层:把大象切成几块肉,再一块一块吃

一个健壮的机器学习项目,绝不能是一大坨混在一起的代码。我的经验是,必须严格遵循“单一职责”原则,把整个流程切成四个独立、可测试的模块:

  1. 数据加载与预处理模块:只负责读取 CSV、清洗缺失值、划分训练/测试集、执行特征缩放。它输出的应该是干净、格式统一的X_train,X_test,y_train,y_test
  2. 核心算法模块:只包含三个纯函数:hypothesis(X, theta)cost_function(X, y, theta)gradient_descent(X, y, theta, alpha)。它们不依赖任何外部状态,输入什么,就输出什么,可以脱离主流程单独单元测试。
  3. 模型训练模块:这是“胶水”代码。它负责初始化theta,设置超参数(alpha,threshold),然后调用上面的核心函数,控制 while 循环,记录每次迭代的cost值,最后返回训练好的theta和历史costs
  4. 评估与可视化模块:负责用训练好的theta在测试集上做预测,计算测试集cost,绘制学习曲线(Learning Curve),并打印关键指标。

这种分层的好处是,当你发现模型效果不好时,你可以像排查电路一样,逐级定位:是数据没处理好?是代价函数算错了?是梯度更新有 bug?还是学习率设得太激进?而不是面对一整页代码,无从下手。我在调试过程中,就曾因为一个X矩阵忘记添加x₀=1的列,导致hypothesis函数永远返回 0,而这个错误在分层结构下,三分钟就定位到了。

3. 核心细节解析与实操要点:代价函数、假设函数与梯度下降的三位一体

3.1 假设函数(Hypothesis):从数学符号到代码的精确映射

多元线性回归的假设函数,其数学本质就是一个加权求和:h_θ(x) = θ₀ + θ₁x₁ + θ₂x₂ + ... + θₙxₙ。这里的x₀是一个永远等于 1 的“虚拟特征”,它把截距项θ₀也纳入了统一的向量点积框架中。所以,真正的向量化形式是h_θ(x) = θ^T * x,其中θx都是列向量。这个看似微小的x₀=1,是向量化实现的“阿喀琉斯之踵”,漏掉它,整个大厦就塌了。在代码里,这体现为一个关键操作:X = np.column_stack([np.ones(m), X])。这行代码的意思是,给原始的X(m 行 n 列)左边,硬生生“粘”上一列全是 1 的向量,从而得到一个mn+1列的新矩阵。我曾经犯过一个低级错误,把这行代码写在了特征缩放之后,结果x₀=1这一列也被缩放了,变成了1 -> (1-1)/(1-1),直接触发了除零错误。正确的顺序必须是:先添加x₀列,再对x₁xₙ这些“真”特征进行缩放。hypothesis函数本身极其简洁:return X @ theta。这里@NumPy的矩阵乘法操作符,它完美对应了数学中的θ^T * x。注意,Xm x (n+1)theta(n+1) x 1,所以结果是一个m x 1的列向量,每一行就是对应样本的预测值。这个函数的“无副作用”特性至关重要——它只读取输入,只返回输出,绝不修改任何外部变量。这保证了它的可预测性和可测试性。

3.2 代价函数(Cost Function):模型的“体检报告单”

代价函数J(θ)就是模型的“健康指数”。它用一个单一的数字,量化了当前所有权重θ下,模型在整个训练集上的平均预测误差有多大。我们选用的是最经典的均方误差(MSE),公式是J(θ) = 1/(2m) * Σ(h_θ(x^(i)) - y^(i))²。为什么是1/(2m)而不是1/m?纯粹是为了求导方便。后面梯度下降的更新公式里,会自然出现一个2,和这里的1/2抵消,让最终的公式更简洁。这个函数的代码实现,是向量化思想的绝佳范例:

def cost_function(X, y, theta): m = len(y) predictions = X @ theta error = predictions - y cost = (1/(2*m)) * np.sum(error**2) return cost

让我们逐行拆解它的精妙之处:

  • predictions = X @ theta: 一次性计算出所有m个样本的预测值,得到一个m x 1的向量。
  • error = predictions - y: 因为y也是一个m x 1的向量,所以这是向量间的逐元素减法,瞬间得到所有样本的误差向量。
  • error**2: 向量的逐元素平方,得到一个m x 1的误差平方向量。
  • np.sum(error**2): 对这个向量求和,得到一个标量,即所有误差平方的总和。
  • 最后乘以1/(2*m),得到平均代价。

提示:在实际调试中,我习惯在cost_function开头加一句assert predictions.shape == y.shape。这是一个简单的断言,用来确保X @ theta的结果维度和y完全一致。一旦维度不匹配,程序会立刻报错并停止,而不是默默给出一个荒谬的cost值。这种“防御性编程”习惯,能帮你省下无数个小时的排查时间。

3.3 梯度下降(Gradient Descent):让模型自己“学会走路”

如果说hypothesis是模型的“嘴”,cost_function是模型的“眼睛”,那么gradient_descent就是模型的“腿”和“大脑”。它根据“眼睛”看到的误差,决定下一步“腿”该往哪个方向、迈多大的步子。其核心更新规则是:θ := θ - α * ∇J(θ)。其中∇J(θ)就是代价函数J(θ)θ的偏导数向量,也就是“梯度”。对于 MSE 代价函数,这个梯度的向量化形式是∇J(θ) = (1/m) * X^T * (X @ theta - y)。这个公式是怎么来的?我们可以从单个参数θⱼ的偏导数推导:∂J/∂θⱼ = (1/m) * Σ(h_θ(x^(i)) - y^(i)) * xⱼ^(i)。你会发现,Σ(... * xⱼ^(i))这个求和,本质上就是向量(X @ theta - y)X的第j列的点积。而X^T的作用,就是把X的所有列都“竖起来”,这样X^T @ (X @ theta - y)就能一次性算出所有j个偏导数,组成一个(n+1) x 1的梯度向量。代码实现如下:

def gradient_descent(X, y, theta, alpha): m = len(y) predictions = X @ theta error = predictions - y # 计算梯度: (1/m) * X^T * error gradient = (1/m) * (X.T @ error) # 更新 theta: theta := theta - alpha * gradient theta = theta - alpha * gradient return theta

这里的关键洞察是:X.T @ error这一行,完成了传统 for 循环里需要n+1层嵌套才能完成的计算。它高效、简洁、且完全可验证。你可以手动用一个小的2x2矩阵X和一个2x1error向量,纸笔算一遍X.T @ error,然后和你用 for 循环算出的结果对比,确保完全一致。这是建立信心的最有效方法。

4. 实操过程与核心环节实现:从数据到模型的完整流水线

4.1 数据准备:加载、探索与特征工程

我们使用的数据集是 Kaggle 上的 “Graduate Admission 2”,它包含了 500 条申请者的记录。首先,我们需要加载数据并进行初步探索:

import pandas as pd import numpy as np import matplotlib.pyplot as plt # 加载数据 df = pd.read_csv('Admission_Predict_Ver1.1.csv') # 查看前几行和基本信息 print(df.head()) print(df.info()) print(df.describe())

df.describe()的输出会立刻揭示特征尺度的巨大差异:

GRE Score TOEFL Score University Rating SOP LOR CGPA Research count 500.000000 500.000000 500.000000 500 500 500 500 mean 316.472000 107.192000 3.118000 3.5 3.5 8.57 0.586 std 11.277000 6.062000 1.146000 1.0 1.0 0.60 0.493 min 290.000000 92.000000 1.000000 1.0 1.0 6.80 0.000 max 340.000000 120.000000 5.000000 5.0 5.0 9.92 1.000

GRE Score的标准差是 11.27,而Research的标准差是 0.493,相差超过 20 倍。这正是我们必须进行特征缩放的铁证。接下来,我们定义要用于建模的特征列和目标列:

# 定义特征列(去掉 'Serial No.' 和 'Chance of Admit') feature_columns = ['GRE Score', 'TOEFL Score', 'University Rating', 'SOP', 'LOR ', 'CGPA', 'Research'] X = df[feature_columns].values # 转换为 NumPy 数组 y = df['Chance of Admit'].values.reshape(-1, 1) # 确保是列向量

注意y.reshape(-1, 1),这是为了确保它是一个m x 1的二维数组,而不是一个m长度的一维数组。NumPy对这两种格式的广播规则不同,不统一格式会导致后续矩阵运算出错。

4.2 特征缩放:Min-Max Scaling 的手写实现

我们不使用sklearn.preprocessing.MinMaxScaler,而是亲手实现,以加深理解:

def min_max_scale(X): """ 对 X 的每一列(每个特征)进行 Min-Max 缩放 X: (m, n) 的二维数组 返回: 缩放后的 (m, n) 数组 """ X_scaled = np.zeros_like(X) for i in range(X.shape[1]): # 遍历每一列 col_min = np.min(X[:, i]) col_max = np.max(X[:, i]) # 防止分母为零(某列所有值都相同) if col_max != col_min: X_scaled[:, i] = (X[:, i] - col_min) / (col_max - col_min) else: # 如果所有值都一样,缩放后全为 0 X_scaled[:, i] = 0 return X_scaled # 应用缩放 X_scaled = min_max_scale(X)

这个函数虽然用了 for 循环,但它只循环了n(特征数,这里是 7)次,而不是m(样本数,这里是 500)次。所以它的开销微乎其微。更重要的是,它清晰地展示了缩放的逻辑:对每一列,找到它的最小值和最大值,然后将该列的每个元素除以这个范围。if col_max != col_min的判断,是为了防止数据中存在全为同一值的“坏”特征,这在真实世界的数据中并不罕见。

4.3 数据集划分与初始化:为训练做好准备

我们将数据集按 80% 训练、20% 测试的比例划分:

# 划分训练集和测试集 m = len(y) train_size = int(0.8 * m) X_train, X_test = X_scaled[:train_size], X_scaled[train_size:] y_train, y_test = y[:train_size], y[train_size:] # 添加 x₀ = 1 的列 X_train = np.column_stack([np.ones(train_size), X_train]) X_test = np.column_stack([np.ones(len(X_test)), X_test]) # 初始化 theta 为全零向量 n_features = X_train.shape[1] # 包含 x₀,所以是 8 theta = np.zeros((n_features, 1)) # 设置超参数 alpha = 0.01 # 学习率 threshold = 1e-6 # 收敛阈值

这里alpha = 0.01是一个经验值。学习率太大,theta会在最优值附近疯狂震荡,甚至发散;学习率太小,收敛速度会慢得令人绝望。我建议初学者从0.0010.010.1这三个值开始尝试,观察学习曲线的形状。threshold = 1e-6意味着,如果连续两次迭代的cost值之差小于百万分之一,我们就认为模型已经“走累了”,可以停下来了。

4.4 模型训练:见证梯度下降的“舞蹈”

现在,我们把所有模块组装起来,开始真正的训练:

def train_model(X, y, theta, alpha, threshold): m = len(y) costs = [] iterations = 0 # 计算初始代价 current_cost = cost_function(X, y, theta) costs.append(current_cost) print(f"Initial Cost: {current_cost:.6f}") # 主训练循环 while True: # 计算梯度并更新 theta theta = gradient_descent(X, y, theta, alpha) # 计算新的代价 new_cost = cost_function(X, y, theta) costs.append(new_cost) iterations += 1 # 检查收敛性 cost_diff = abs(current_cost - new_cost) if cost_diff < threshold: print(f"Converged after {iterations} iterations.") print(f"Final Cost: {new_cost:.6f}") break # 为了防止无限循环,设置一个最大迭代次数 if iterations > 10000: print("Warning: Maximum iterations reached.") break current_cost = new_cost # 每 100 次迭代打印一次进度,避免刷屏 if iterations % 100 == 0: print(f"Iteration {iterations}: Cost = {new_cost:.6f}") return theta, costs # 执行训练 theta_final, cost_history = train_model(X_train, y_train, theta, alpha, threshold)

这段代码的精髓在于while True循环和cost_diff的计算。它不依赖于预设的迭代次数,而是让模型自己“感觉”什么时候该停。cost_history列表记录了每一次迭代的代价,这是我们后续绘制学习曲线的全部数据。

4.5 结果可视化与模型评估:用图表说话

训练完成后,最重要的一步是“看”。我们绘制学习曲线,这是诊断模型健康状况的黄金标准:

plt.figure(figsize=(10, 6)) plt.plot(cost_history, label='Training Cost') plt.xlabel('Iterations') plt.ylabel('Cost J(θ)') plt.title('Learning Curve') plt.legend() plt.grid(True) plt.show()

一条健康的曲线,应该从左上角开始,随着迭代次数增加,平滑、单调地下降到右下角。如果曲线出现剧烈抖动,说明alpha太大;如果曲线下降得极其缓慢,说明alpha太小;如果曲线先降后升,那基本可以确定代码里有严重 bug。接着,我们在测试集上评估模型:

# 在测试集上计算代价 test_cost = cost_function(X_test, y_test, theta_final) print(f"Test Set Cost: {test_cost:.6f}") # 计算一些预测示例 sample_predictions = (X_test[:5] @ theta_final).flatten() sample_actual = y_test[:5].flatten() print("\nSample Predictions vs Actual:") for i in range(5): print(f"Sample {i+1}: Predicted = {sample_predictions[i]:.4f}, Actual = {sample_actual[i]:.4f}")

test_cost应该略高于train_cost,这是正常的,因为模型没见过测试数据。但如果test_cost远大于train_cost(比如差一个数量级),那就说明模型出现了严重的过拟合,需要考虑正则化等更高级的技术了。

5. 常见问题与排查技巧实录:那些让我熬夜到凌晨三点的坑

5.1 维度不匹配:最常见、最隐蔽的“幽灵 Bug”

这是新手踩得最多、也最让人抓狂的坑。NumPy的广播机制有时会“好心办坏事”,把一个本该报错的维度错误,悄悄地、错误地计算出来,然后给你一个完全错误的结果。例如,如果你忘了把yreshape 成列向量,y是一个(500,)的一维数组,而X @ theta是一个(500, 1)的二维数组,那么X @ theta - y不会报错,而是会触发广播,把y当作一个行向量来用,导致计算结果完全错误。排查方法只有一个:在每一个关键步骤后,立刻打印变量的shape

print(f"X shape: {X.shape}") # 应该是 (m, n+1) print(f"theta shape: {theta.shape}") # 应该是 (n+1, 1) print(f"y shape: {y.shape}") # 应该是 (m, 1) print(f"predictions shape: {(X @ theta).shape}") # 应该是 (m, 1)

我养成了一个习惯,在写完每一行涉及矩阵运算的代码后,都加一个print。虽然麻烦,但绝对值得。

5.2 学习率(alpha)调试:一场与“震荡”和“蜗牛”的赛跑

alpha的选择没有银弹,只能靠实验。我整理了一个快速调试指南:

alpha学习曲线表现你的感受应对策略
0.0001曲线几乎是一条直线,下降极其缓慢“这模型是不是死了?”增大alpha,尝试0.001
0.01曲线平滑下降,收敛迅速“稳了!”保持,这是理想区间
0.1曲线剧烈上下震荡,甚至不收敛“救命!它疯了!”减小alpha,尝试0.05
1.0曲线第一步就冲上天,然后变成 NaN“完了,全毁了。”大幅减小alpha,回到0.01

注意:NaN(Not a Number)是alpha过大的终极信号。一旦cost变成nan,就意味着计算中出现了0/0inf - inf这样的非法操作,整个训练就废了,必须重启。

5.3 特征缩放的“陷阱”:x₀=1列的生死时速

前面提到过,x₀=1列绝对不能参与缩放。但还有一个更隐蔽的陷阱:缩放器(Scaler)必须只在训练集上拟合(fit),然后用同一个缩放器去转换(transform)测试集。如果你分别对训练集和测试集做min_max_scale,那么两个数据集的缩放参数(minmax)就会不同,导致测试集的特征被映射到了一个和训练集完全不同的空间里,模型自然失效。我们的手写min_max_scale函数没有“fit/transform”分离,所以必须确保X_trainX_test是用同一个minmax(即X_trainminmax)来缩放的。因此,正确的做法是:

# 错误!不要这样做 X_train_scaled = min_max_scale(X_train) X_test_scaled = min_max_scale(X_test) # 这里用了 X_test 自己的 min/max! # 正确!这样做 X_train_scaled = min_max_scale(X_train) # 使用 X_train 的 min/max 来缩放 X_test X_test_scaled = np.zeros_like(X_test) for i in range(X_test.shape[1]): col_min = np.min(X_train[:, i]) # 关键!用训练集的 min col_max = np.max(X_train[:, i]) # 关键!用训练集的 max if col_max != col_min: X_test_scaled[:, i] = (X_test[:, i] - col_min) / (col_max - col_min) else: X_test_scaled[:, i] = 0

5.4 代价函数的“双面镜”:训练集与测试集的辩证关系

cost_function是一个强大的工具,但它也是一面“双面镜”。它在训练集上告诉你的,是模型对已知数据的记忆能力;它在测试集上告诉你的,是模型对未知数据的泛化能力。一个优秀的模型,应该在这两面镜子中都呈现出“健康”的影像。我见过太多人只盯着训练集cost,看到它降到了 0.001 就欢呼雀跃,结果一上测试集,cost直接飙到 10.0。这说明模型把训练数据的噪声都当成了规律,也就是过拟合。我的经验是,始终把train_costtest_cost放在同一个表格里对比:

迭代次数Train CostTest CostTrain/Test Ratio
00.2500000.2498761.0005
1000.0123450.0124560.9911
2000.0056780.0058900.9640
3000.0034560.0042340.8162
4000.0023450.0056780.4130

Train/Test Ratio开始急剧下降(比如从 0.96 掉到 0.41),这就是一个强烈的红色警报:模型正在过拟合。此时,你应该立即停止训练,并考虑引入正则化项(如 L2 正则化)来给模型“上点枷锁”。

6. 工具选型与性能对比:为什么是 NumPy,而不是纯 Python?

6.1 NumPy 的不可替代性:从“秒级”到“毫秒级”的飞跃

为了让你直观感受到NumPy的威力,我做了一个简单的性能对比实验。我用纯 Python 的 for 循环和NumPy的向量化,分别计算了 10,000 个样本的线性回归预测值:

import time # 纯 Python 循环版本 def hypothesis_loop(X, theta): m = X.shape[0] predictions = np.zeros((m, 1)) for i in range(m): pred = 0.0 for j in range(X.shape[1]): pred += X[i, j] * theta[j, 0] predictions[i, 0] = pred return predictions # NumPy 向量化版本 def hypothesis_vectorized(X, theta): return X @ theta # 性能测试 X_test_large = np.random.rand(10000, 8) theta_test = np.random.rand(8, 1) # 测试循环版本 start = time.time() pred_loop = hypothesis_loop(X_test_large, theta_test) time_loop = time.time() - start # 测试向量化版本 start = time.time() pred_vec = hypothesis_vectorized(X_test_large, theta_test) time_vec = time.time() - start print(f"Pure Python loop: {time_loop:.4f} seconds") print(f"NumPy vectorized: {time_vec:.4f} seconds") print(f"Speedup: {time_loop/time_vec:.1f}x")

在我的笔记本电脑上,结果是:

Pure Python loop: 0.2145 seconds NumPy vectorized: 0.0003 seconds Speedup: 715.0x

将近 700 倍的加速!这还不包括NumPy内部利用了高度优化的 C 和 Fortran 库(如 BLAS/LAPACK)所带来的额外红利。这意味着,如果你的项目需要处理百万级的数据,纯 Python 循环可能需要几个小时,而NumPy只需要几分钟。这种性能差距,已经不是“优化”,而是“能否落地”的分水岭。

6.2 Pandas 与 Matplotlib:数据处理与可视化的黄金搭档

Pandas的核心价值在于它提供了DataFrame这个超级灵活的数据容器。它让数据探索变得无比简单:

# 一行代码,就能看到所有特征与目标变量的相关性 correlation_matrix = df.corr() print(correlation_matrix['Chance of Admit'].sort_values(ascending=False))

输出会清晰地告诉你,CGPAGRE Score是与录取概率最相关的两个特征,这为后续的特征选择提供了直接依据。而Matplotlib则是将抽象的数字转化为直观洞见的桥梁。除了学习曲线,我还经常画yX的散点图矩阵(Scatter Matrix),来肉眼观察特征与目标之间是否存在线性关系:

from pandas.plotting import scatter_matrix scatter_matrix(df[feature_columns + ['Chance of Admit']], figsize=(12, 12)) plt.show()

这些图表,是任何数学公式都无法替代的“直觉培养皿”。它们能让你在敲下第一行代码之前,就对数据有一个感性的、全局的认识。

7. 从“能跑”到“精通”:超越基础的思考与延伸

7.1 正则化:为模型装上“刹车片”

我们目前的模型,是一个“裸奔”的线性回归。它追求在训练集上把cost降到最低,但对模型本身的复杂度没有任何约束。这就像一辆没有刹车的赛车,虽然起步快,但随时可能失控。正则化(Regularization)就是给这辆赛车装上刹车片。最常见的 L2 正则化(Ridge Regression),就是在代价函数后面,加上一个λ * Σθⱼ²的惩罚项,其中λ是正则化强度。这个项的作用是,当某个θⱼ的值变得很大时,它会受到严厉的惩罚,从而迫使所有θⱼ都保持在一个相对较小

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/22 11:12:12

DLSS Swapper:5分钟掌握游戏性能优化的终极指南

DLSS Swapper&#xff1a;5分钟掌握游戏性能优化的终极指南 【免费下载链接】dlss-swapper 项目地址: https://gitcode.com/GitHub_Trending/dl/dlss-swapper 你是否曾因游戏DLSS版本过旧而错失性能提升&#xff1f;或者新版本DLSS导致游戏频繁崩溃&#xff1f;DLSS Sw…

作者头像 李华
网站建设 2026/5/22 11:11:19

别再手动调参了!用这个R包5分钟搞定Seurat单细胞差异基因火山图

单细胞差异基因火山图自动化绘制全攻略&#xff1a;从Seurat到一键出图 在单细胞转录组数据分析中&#xff0c;差异表达基因(DEG)的识别与可视化是揭示细胞异质性的关键步骤。Seurat作为行业标准工具链的核心组件&#xff0c;其FindAllMarkers函数能够高效识别不同细胞亚群间的…

作者头像 李华
网站建设 2026/5/22 11:10:24

三步解锁网盘全速下载:新一代直链解析工具完全指南

三步解锁网盘全速下载&#xff1a;新一代直链解析工具完全指南 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 &#xff0c;支持 百度网盘 / 阿里云盘 / 中国移动云盘 / 天翼云盘…

作者头像 李华
网站建设 2026/5/22 11:05:34

Icepi Zero:口袋里的FPGA实验室,开源硬件开发板深度解析

1. 项目概述&#xff1a;为什么我们需要Icepi Zero&#xff1f;在嵌入式开发和数字电路设计的圈子里&#xff0c;FPGA&#xff08;现场可编程门阵列&#xff09;一直是个让人又爱又恨的家伙。爱的是它那无与伦比的灵活性&#xff0c;你可以用硬件描述语言&#xff08;HDL&#…

作者头像 李华
网站建设 2026/5/22 11:05:33

重新定义法线贴图:基于浏览器的实时生成革命

重新定义法线贴图&#xff1a;基于浏览器的实时生成革命 【免费下载链接】NormalMap-Online NormalMap Generator Online 项目地址: https://gitcode.com/gh_mirrors/no/NormalMap-Online 法线贴图生成技术正在经历一场静默革命。传统上&#xff0c;创建高质量法线贴图需…

作者头像 李华
网站建设 2026/5/22 11:03:19

UPS、EPS蓄电池更换周期及更换判定标准详解

在机房后备供电、工业不间断供电、消防应急供电体系中&#xff0c;UPS不间断电源与EPS应急电源的核心储能载体均为蓄电池。蓄电池的健康状态&#xff0c;直接决定整套应急供电系统的可靠性&#xff0c;是电气运维、机房维保、消防设施巡检的重点工作内容。在实际运维工作中&…

作者头像 李华