网站优化 代码优化wordpress企业站制作
网站优化 代码优化,wordpress企业站制作,潍坊 网站推广,电商怎么做新手入门怎么开店从零到一#xff1a;用Python与NumPy亲手构建线性回归与逻辑回归模型
最近几年#xff0c;机器学习的热度居高不下#xff0c;但很多学习者在看完了吴恩达教授等经典课程的理论讲解后#xff0c;面对空白的代码编辑器#xff0c;依然会感到无从下手。理论公式和实际可运行…从零到一用Python与NumPy亲手构建线性回归与逻辑回归模型最近几年机器学习的热度居高不下但很多学习者在看完了吴恩达教授等经典课程的理论讲解后面对空白的代码编辑器依然会感到无从下手。理论公式和实际可运行的代码之间似乎总隔着一道鸿沟。这篇文章就是为你——一位有一定Python基础渴望将机器学习理论付诸实践的学习者——准备的实战指南。我们将抛开现成的scikit-learn库仅使用最基础的NumPy从零开始一步步推导并实现线性回归和逻辑回归的核心算法。这不仅仅是“复现”更是一次深入理解模型内部运作机制、掌握向量化编程思维、并学会调试和优化模型的绝佳旅程。你会发现亲手搭建这些基础模型对理解更复杂的深度学习框架有着不可替代的价值。1. 环境搭建与数据准备为实战打下坚实基础在开始编写任何机器学习代码之前一个清晰、可复现的开发环境是必不可少的。我强烈建议使用conda或venv来创建独立的Python虚拟环境这能有效避免不同项目间的包版本冲突。对于本教程我们需要的核心库非常简单NumPy用于数值计算Matplotlib用于可视化或许再加上pandas来方便地加载数据。# 使用 conda 创建环境并安装依赖推荐 conda create -n ml_from_scratch python3.9 conda activate ml_from_scratch conda install numpy matplotlib pandas jupyter # 或者使用 pip 和 venv python -m venv ml_env source ml_env/bin/activate # Windows 下使用 ml_env\Scripts\activate pip install numpy matplotlib pandas数据是模型的燃料。为了聚焦于算法本身我们可以自己生成一些模拟数据。例如对于线性回归我们可以生成符合y 2x 1 噪声规律的数据点。这样做的好处是我们预先知道了“标准答案”可以清晰地验证模型学习的效果。import numpy as np import matplotlib.pyplot as plt # 设置随机种子以保证结果可复现 np.random.seed(42) # 生成线性回归模拟数据 def generate_linear_data(num_samples100): X 2 * np.random.rand(num_samples, 1) # 特征 X范围在[0, 2) # 真实关系: y 2x 1 高斯噪声 y 2 * X 1 np.random.randn(num_samples, 1) * 0.5 return X, y X_train, y_train generate_linear_data() plt.scatter(X_train, y_train) plt.xlabel(Feature X) plt.ylabel(Target y) plt.title(Generated Linear Regression Data) plt.show()提示在机器学习项目中养成设置随机种子如np.random.seed(42)的习惯至关重要。它能确保每次运行代码时随机生成的数据、初始化的参数都是一致的这对于调试和比较不同算法效果非常有帮助。对于逻辑回归我们需要生成可用于二分类的数据。scikit-learn的make_classification或make_moons函数是很好的选择但为了纯粹性我们也可以手动构造一个简单的二维数据集确保两类数据线性可分或具有清晰的决策边界。2. 线性回归的向量化实现与梯度下降优化线性回归是理解机器学习优化过程的绝佳起点。其核心思想是找到一组参数权重w和偏置b使得模型预测值ŷ wX b与真实值y之间的差距用损失函数衡量最小。2.1 定义模型与损失函数首先我们定义假设函数预测函数。这里的关键是使用向量化操作一次性对整个训练集进行计算这比使用循环效率高出几个数量级。def linear_hypothesis(X, w, b): 线性回归假设函数 (向量化实现) 参数: X: 特征矩阵形状 (m, n)m为样本数n为特征数 w: 权重向量形状 (n, 1) b: 偏置标量 返回: y_pred: 预测值形状 (m, 1) # 核心操作: y_pred X * w b # 这里使用点积 np.dot对于单特征w是标量多特征时w是向量 y_pred np.dot(X, w) b return y_pred接下来我们需要一个标尺来衡量预测的好坏这就是损失函数或成本函数。对于线性回归最常用的是均方误差MSE。def compute_mse_cost(y_true, y_pred): 计算均方误差损失 (Mean Squared Error) 参数: y_true: 真实标签形状 (m, 1) y_pred: 模型预测值形状 (m, 1) 返回: cost: 标量损失值 m y_true.shape[0] # 向量化计算每个样本的误差平方然后求和取平均 cost (1 / (2 * m)) * np.sum((y_pred - y_true) ** 2) return cost注意损失函数中的1/(2*m)而不是1/m是学术上的一个常见写法其目的是在后续求梯度时平方项求导产生的系数2能与分母的2约掉使得梯度表达式更简洁。这对最终优化结果没有影响。2.2 实现梯度下降算法梯度下降是优化损失函数的引擎。其原理是计算损失函数关于每个参数的梯度导数然后沿着梯度反方向即下降最快的方向更新参数。向量化实现能同时更新所有参数。def batch_gradient_descent(X, y, w, b, learning_rate, num_iterations): 批量梯度下降算法 参数: X: 特征矩阵 (m, n) y: 真实标签 (m, 1) w: 初始权重 (n, 1) b: 初始偏置 (标量) learning_rate: 学习率 α num_iterations: 迭代次数 返回: w, b: 优化后的参数 cost_history: 每次迭代的损失记录用于绘图 m X.shape[0] cost_history [] for i in range(num_iterations): # 1. 计算当前参数的预测值 y_pred linear_hypothesis(X, w, b) # 2. 计算梯度 (向量化形式) # 对w的梯度: dj_dw (1/m) * X^T * (y_pred - y) # 对b的梯度: dj_db (1/m) * sum(y_pred - y) error y_pred - y dj_dw (1 / m) * np.dot(X.T, error) dj_db (1 / m) * np.sum(error) # 3. 同时更新参数 w w - learning_rate * dj_dw b b - learning_rate * dj_db # 4. 记录当前损失 cost compute_mse_cost(y, y_pred) cost_history.append(cost) # 可选每1000次迭代打印一次损失 if i % 1000 0: print(fIteration {i:4d}: Cost {cost:.6f}) return w, b, cost_history现在让我们在一个简单的单变量数据集上运行这个算法并观察其学习过程。# 准备数据 (添加偏置列X01便于将b并入w进行向量化但这里我们分开处理以清晰理解) X X_train # 形状 (100, 1) y y_train # 初始化参数 w_initial np.random.randn(1, 1) # 随机初始化权重 b_initial 0.0 learning_rate 0.01 iterations 5000 # 运行梯度下降 w_final, b_final, costs batch_gradient_descent(X, y, w_initial, b_initial, learning_rate, iterations) print(f\n最终参数: w {w_final[0,0]:.4f}, b {b_final:.4f}) print(f真实关系近似为: y {w_final[0,0]:.4f} * x {b_final:.4f}) # 绘制学习曲线 plt.plot(range(iterations), costs) plt.xlabel(Iteration) plt.ylabel(Cost (MSE)) plt.title(Gradient Descent: Cost vs. Iterations) plt.grid(True) plt.show()通过绘制损失随迭代次数下降的曲线我们可以直观判断学习率是否合适、算法是否收敛。一条平滑下降并最终趋于平稳的曲线是健康的标志。2.3 特征缩放与学习率调优实战当特征尺度差异巨大时例如房屋面积x1范围是[500, 5000]而房间数量x2范围是[1, 5]梯度下降会变得非常缓慢路径曲折。特征缩放是解决这一问题的关键技术它能将不同特征的值映射到相近的范围内最常见的方法是Z-score标准化。缩放方法公式优点缺点Z-score标准化x (x - μ) / σ处理后数据均值为0标准差为1适用于大多数情况。需要计算均值和标准差。Min-Max归一化x (x - min) / (max - min)将值缩放到[0, 1]区间直观。对异常值敏感。def z_score_normalize(X): 对特征矩阵X进行Z-score标准化 (按列/特征进行) 参数: X: 原始特征矩阵 (m, n) 返回: X_norm: 标准化后的矩阵 mu: 每个特征的均值 (用于后续对新数据标准化) sigma: 每个特征的标准差 mu np.mean(X, axis0) # 计算每个特征的均值形状 (1, n) sigma np.std(X, axis0) # 计算每个特征的标准差 # 避免除零给标准差为0的特征一个很小的值 sigma np.where(sigma 0, 1, sigma) X_norm (X - mu) / sigma return X_norm, mu, sigma # 假设我们有一个多特征数据集 X_multi # X_norm, mu, sigma z_score_normalize(X_multi) # 在训练模型时使用 X_norm # 当有新数据需要预测时必须使用训练时计算的 mu 和 sigma 进行同样的变换学习率α是梯度下降中最重要的超参数之一。它控制着每次参数更新的步长。α太小收敛速度极慢需要大量迭代。α太大可能越过最低点导致损失震荡甚至发散。一个实用的调优策略是进行学习率扫描尝试一系列呈指数增长或衰减的学习率例如[0.001, 0.003, 0.01, 0.03, 0.1]运行少量迭代如1000次绘制每个学习率对应的损失下降曲线。选择那个使损失快速且平稳下降的学习率。def test_learning_rates(X, y, learning_rates): 测试不同学习率对梯度下降收敛的影响 plt.figure(figsize(10, 6)) for lr in learning_rates: w np.random.randn(X.shape[1], 1) b 0.0 _, _, costs batch_gradient_descent(X, y, w, b, lr, num_iterations1000) plt.plot(costs, labelflr{lr}) plt.xlabel(Iterations) plt.ylabel(Cost) plt.title(Cost vs. Iterations for Different Learning Rates) plt.legend() plt.grid(True) plt.yscale(log) # 使用对数坐标更易观察 plt.show() # 调用测试 # test_learning_rates(X_norm, y, [0.001, 0.01, 0.1, 0.5])3. 逻辑回归从概率视角理解分类线性回归预测连续值而逻辑回归解决的是二分类问题是/否0/1。其核心创新在于引入了Sigmoid函数或称逻辑函数将线性组合z wX b的输出映射到(0, 1)区间并将其解释为样本属于正类的概率。3.1 Sigmoid函数与决策边界Sigmoid函数的数学表达式为g(z) 1 / (1 e^{-z})。当z很大时g(z)接近1当z很小时g(z)接近0在z0时g(z)0.5。def sigmoid(z): Sigmoid激活函数 参数: z: 标量或NumPy数组 返回: 经过sigmoid变换的值范围在(0,1) # 使用 np.exp 进行向量化计算 return 1 / (1 np.exp(-z)) # 可视化Sigmoid函数 z np.linspace(-10, 10, 100) g sigmoid(z) plt.plot(z, g) plt.axvline(x0, colork, linestyle--, alpha0.3) plt.axhline(y0.5, colork, linestyle--, alpha0.3) plt.xlabel(z) plt.ylabel(g(z)) plt.title(Sigmoid Function) plt.grid(True) plt.show()逻辑回归的假设函数因此定义为h(x) g(wX b) sigmoid(wX b)。我们通常设定一个阈值默认为0.5来做出分类决策如果h(x) 0.5预测为类别1。如果h(x) 0.5预测为类别0。这个阈值0.5对应的z wX b 0在特征空间中就构成了一条直线对于二维特征或一个超平面称为决策边界。模型学习的目标就是找到参数w和b使得这个决策边界能最好地分开两类数据。3.2 交叉熵损失函数及其梯度线性回归使用的均方误差损失在逻辑回归中不再适用因为它会导致损失函数非凸存在多个局部极小值。逻辑回归使用交叉熵损失函数它源于最大似然估计的思想能很好地衡量预测概率分布与真实标签分布的差异。对于单个样本(x^(i), y^(i))其损失为L(f(x^(i)), y^(i)) -[y^(i) * log(f(x^(i))) (1 - y^(i)) * log(1 - f(x^(i)))]整个训练集的成本函数是所有样本损失的平均值。这个函数是凸的保证了梯度下降能找到全局最优解。def compute_logistic_cost(y_true, y_pred_prob): 计算逻辑回归的交叉熵损失 (向量化实现) 参数: y_true: 真实标签 (0或1)形状 (m, 1) y_pred_prob: 预测为正类的概率形状 (m, 1) 返回: cost: 标量损失值 m y_true.shape[0] # 避免log(0)导致数值问题对预测概率进行裁剪 epsilon 1e-15 y_pred_prob np.clip(y_pred_prob, epsilon, 1 - epsilon) # 交叉熵损失计算 cost - (1 / m) * np.sum(y_true * np.log(y_pred_prob) (1 - y_true) * np.log(1 - y_pred_prob)) return cost def logistic_gradient_descent(X, y, w, b, learning_rate, num_iterations): 逻辑回归的梯度下降算法 m X.shape[0] cost_history [] for i in range(num_iterations): # 1. 计算线性部分和概率预测 z np.dot(X, w) b y_pred_prob sigmoid(z) # f(x) # 2. 计算梯度 error y_pred_prob - y # 注意这个形式与线性回归惊人地相似 dj_dw (1 / m) * np.dot(X.T, error) dj_db (1 / m) * np.sum(error) # 3. 更新参数 w w - learning_rate * dj_dw b b - learning_rate * dj_db # 4. 记录损失 cost compute_logistic_cost(y, y_pred_prob) cost_history.append(cost) if i % 1000 0: print(fIteration {i:4d}: Cost {cost:.6f}) return w, b, cost_history注意在计算交叉熵损失时我们使用np.clip将预测概率限制在[epsilon, 1-epsilon]范围内。这是因为当预测概率非常接近0或1时log(0)会导致数值计算问题负无穷。这是一个非常重要的实战技巧。3.3 模型评估与决策边界可视化训练完逻辑回归模型后我们需要评估其性能。对于二分类最基本的评估指标是准确率。def predict(X, w, b, threshold0.5): 使用训练好的逻辑回归模型进行预测 参数: X: 特征矩阵 w, b: 模型参数 threshold: 分类阈值 返回: y_pred_class: 预测的类别 (0或1) y_pred_prob: 预测为正类的概率 z np.dot(X, w) b y_pred_prob sigmoid(z) y_pred_class (y_pred_prob threshold).astype(int) return y_pred_class, y_pred_prob def accuracy_score(y_true, y_pred): 计算分类准确率 return np.mean(y_true y_pred) # 假设 X_test, y_test 是测试集 # y_pred_class, _ predict(X_test, w_trained, b_trained) # test_accuracy accuracy_score(y_test, y_pred_class) # print(f测试集准确率: {test_accuracy:.4f})可视化是理解模型如何工作的强大工具。我们可以绘制出数据的散点图并叠加模型学习到的决策边界。def plot_decision_boundary(X, y, w, b): 绘制二维特征下逻辑回归的决策边界 # 创建网格点 x1_min, x1_max X[:, 0].min() - 0.5, X[:, 0].max() 0.5 x2_min, x2_max X[:, 1].min() - 0.5, X[:, 1].max() 0.5 xx1, xx2 np.meshgrid(np.linspace(x1_min, x1_max, 200), np.linspace(x2_min, x2_max, 200)) grid np.c_[xx1.ravel(), xx2.ravel()] # 预测网格上每个点的概率 _, Z_prob predict(grid, w, b) Z Z_prob.reshape(xx1.shape) # 绘制等高线和散点图 plt.figure(figsize(8, 6)) plt.contourf(xx1, xx2, Z, levels25, cmapRdBu, alpha0.6) plt.contour(xx1, xx2, Z, levels[0.5], colorsblack, linewidths2) # 决策边界 plt.scatter(X[y.ravel()0, 0], X[y.ravel()0, 1], cblue, labelClass 0, edgecolorsk) plt.scatter(X[y.ravel()1, 0], X[y.ravel()1, 1], cred, labelClass 1, edgecolorsk) plt.xlabel(Feature 1) plt.ylabel(Feature 2) plt.title(Logistic Regression Decision Boundary) plt.legend() plt.colorbar(labelPredicted Probability of Class 1) plt.show() # 使用一个简单的二维分类数据集进行演示 # from sklearn.datasets import make_classification # X, y make_classification(n_samples100, n_features2, n_informative2, n_redundant0, random_state42) # ... 训练模型 ... # plot_decision_boundary(X, y, w_final, b_final)4. 正则化对抗过拟合的利器当模型过于复杂例如特征过多或多项式次数过高它可能会完美“记忆”训练数据中的噪声和细节导致在训练集上表现极好但在未见过的测试集上表现糟糕。这种现象称为过拟合或高方差。正则化通过在损失函数中增加一个惩罚项来约束模型参数的大小从而降低模型复杂度提高泛化能力。4.1 L2正则化岭回归的原理与实现最常用的正则化是L2正则化它在原始损失函数的基础上加上所有权重w的平方和乘以一个正则化强度系数lambda通常写作λ。线性回归的L2正则化损失函数J(w, b) (1/(2m)) * Σ(y_pred - y)^2 (λ/(2m)) * Σ(w_j^2)逻辑回归的L2正则化损失函数J(w, b) -(1/m) * Σ[y*log(f) (1-y)*log(1-f)] (λ/(2m)) * Σ(w_j^2)注意正则化项通常不包含偏置b因为b只是影响决策边界的位置而不影响模型的弯曲复杂度。实现正则化梯度下降时只需要在权重w的梯度更新项中额外加上(λ/m) * w这一项。def compute_cost_linear_reg_with_regularization(X, y, w, b, lambda_): 带L2正则化的线性回归损失函数 m X.shape[0] y_pred linear_hypothesis(X, w, b) mse_cost compute_mse_cost(y, y_pred) # 正则化项注意不惩罚b reg_cost (lambda_ / (2 * m)) * np.sum(w ** 2) total_cost mse_cost reg_cost return total_cost def gradient_descent_with_regularization(X, y, w, b, learning_rate, num_iterations, lambda_, model_typelinear): 带L2正则化的梯度下降 (适用于线性和逻辑回归) model_type: linear 或 logistic m X.shape[0] cost_history [] for i in range(num_iterations): if model_type linear: y_pred linear_hypothesis(X, w, b) error y_pred - y elif model_type logistic: z np.dot(X, w) b y_pred_prob sigmoid(z) error y_pred_prob - y else: raise ValueError(model_type must be linear or logistic) # 计算梯度 dj_dw (1 / m) * np.dot(X.T, error) (lambda_ / m) * w # 添加正则化项 dj_db (1 / m) * np.sum(error) # b的梯度不变 # 更新参数 w w - learning_rate * dj_dw b b - learning_rate * dj_db # 计算并记录损失 if model_type linear: cost compute_cost_linear_reg_with_regularization(X, y, w, b, lambda_) else: # 逻辑回归的带正则化损失计算 z np.dot(X, w) b y_pred_prob sigmoid(z) logistic_cost compute_logistic_cost(y, y_pred_prob) reg_cost (lambda_ / (2 * m)) * np.sum(w ** 2) cost logistic_cost reg_cost cost_history.append(cost) return w, b, cost_history4.2 如何选择正则化参数 λλ是一个超参数需要手动调整。其选择是一个权衡λ 0没有正则化模型可能过拟合。λ 太大惩罚过重所有权重被压得非常接近0模型会变得过于简单欠拟合可能变成一条水平线对于线性回归或一个常概率对于逻辑回归。选择λ的黄金法则是使用验证集。将数据分为训练集、验证集和测试集。在训练集上用不同的λ值训练模型然后在验证集上评估性能例如用准确率或F1分数选择在验证集上表现最好的λ。最后用这个λ在完整的训练集上重新训练并在独立的测试集上做最终评估。def find_best_lambda(X_train, y_train, X_val, y_val, lambdas, model_typelogistic): 通过验证集选择最佳的正则化参数 lambda best_lambda None best_val_accuracy 0 accuracies [] for lambda_ in lambdas: # 初始化参数 w np.random.randn(X_train.shape[1], 1) * 0.01 b 0.0 # 使用当前lambda训练模型 w_trained, b_trained, _ gradient_descent_with_regularization( X_train, y_train, w, b, learning_rate0.1, num_iterations3000, lambda_lambda_, model_typemodel_type ) # 在验证集上评估 y_val_pred, _ predict(X_val, w_trained, b_trained) val_accuracy accuracy_score(y_val, y_val_pred) accuracies.append(val_accuracy) if val_accuracy best_val_accuracy: best_val_accuracy val_accuracy best_lambda lambda_ # 可视化不同lambda下的验证集准确率 plt.plot(lambdas, accuracies, bo-) plt.xscale(log) # lambda通常以对数尺度尝试 plt.xlabel(Lambda (Regularization Strength)) plt.ylabel(Validation Accuracy) plt.title(Selecting Lambda via Validation Set) plt.grid(True) plt.show() print(f最佳 lambda: {best_lambda}, 对应验证集准确率: {best_val_accuracy:.4f}) return best_lambda # 示例用法 # lambdas_to_try [0, 0.001, 0.01, 0.1, 1, 10, 100] # best_lambda find_best_lambda(X_train, y_train, X_val, y_val, lambdas_to_try, model_typelogistic)在实际项目中我经常发现λ在[0.01, 0.1]范围内效果不错但这强烈依赖于数据和特征尺度。务必在标准化特征后再应用正则化否则不同尺度的特征会受到不平等的惩罚。从理论公式到可运行的代码每一步都蕴含着对机器学习本质更深的理解。亲手实现这些基础算法遇到的每一个bug解决的每一个数值不稳定问题都会让你对后续使用高级框架如TensorFlow或PyTorch时遇到的“黑箱”操作有更清晰的认知。当你能自如地调整学习率、观察损失曲线、通过正则化控制模型复杂度时你才真正开始驾驭这些工具而不仅仅是调用它们。