尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零实现逻辑回归:NumPy手写梯度下降与反向传播

从零实现逻辑回归:NumPy手写梯度下降与反向传播 1. 项目概述从零实现逻辑回归如果你刚开始接触深度学习面对吴恩达老师课程里那些矩阵公式和梯度下降是不是感觉有点懵别担心我当年也一样。很多人学完理论一打开代码编辑器就卡壳了不知道从哪下手。这个“实现简单逻辑回归”的项目就是帮你打通从理论到实践这“最后一公里”的关键一步。它不是什么花哨的AI应用但却是你构建一切复杂神经网络模型的基石。简单来说逻辑回归就是一个二分类模型比如判断一张图片是不是猫、一封邮件是不是垃圾邮件。这个项目的核心就是不用任何现成的深度学习框架比如TensorFlow或PyTorch只靠NumPy亲手把前向传播、计算损失、反向传播、参数更新这一整套流程用代码实现出来。听起来基础但这里面每一步都藏着理解深度学习的“钥匙”。我会带你像搭积木一样一块一块把它拼起来并分享那些我踩过的坑和调试技巧让你不仅写出能跑的代码更真正理解代码背后的每一个数学原理和设计逻辑。2. 核心思路与数学原理拆解在动手写代码之前我们必须把脑子里的“数学图景”画清楚。逻辑回归虽然名字里有“回归”但它解决的是分类问题。它的核心思想是给出一组输入特征比如图片的像素值通过一个线性函数结合参数再经过一个非线性激活函数Sigmoid输出一个介于0和1之间的概率值代表属于正类比如“是猫”的可能性。2.1 前向传播从输入到预测概率前向传播就是数据从输入层流到输出层的过程。对于单个样本公式很简单z w^T * x ba σ(z) 1 / (1 e^(-z))这里x是特征向量w是权重参数b是偏置项σ就是Sigmoid函数。a就是我们预测的概率。但在实际中我们几乎永远不会只处理一个样本。为了计算高效我们采用向量化技术一次性处理整个训练集m个样本。这时X是一个(n_x, m)的矩阵n_x是特征数m是样本数W是(n_x, 1)的列向量b是一个标量通过Python的广播机制扩展到所有样本。向量化后的公式变为Z np.dot(W.T, X) bA sigmoid(Z)这个A就是一个(1, m)的矩阵包含了所有m个样本的预测概率。向量化是深度学习代码效率的灵魂它避免了低效的Python循环直接调用底层高度优化的数值计算库。注意这里有一个初学者极易混淆的点权重W的维度。在吴恩达的课程约定中W通常初始化为(n_x, 1)的列向量。因此计算Z时是W.T即W的转置形状(1, n_x)与X (n_x, m)做点积得到(1, m)的Z。如果你把W初始化成(1, n_x)的行向量那么公式就是Z np.dot(W, X) b。两种方式数学上等价但必须前后一致否则会在后续梯度计算中出错。2.2 损失与成本函数衡量预测的好坏得到预测值A后我们需要一个标准来衡量它和真实标签Y的差距。对于单个样本我们使用二元交叉熵损失L(a, y) -[y * log(a) (1-y) * log(1-a)]这个函数设计得很巧妙当真实标签y1时损失为-log(a)预测概率a越接近1损失越小当y0时损失为-log(1-a)预测概率a越接近0损失越小。如果预测完全错误y1但a→0损失会趋近于无穷大。同样我们需要计算整个训练集的平均损失即成本函数J -(1/m) * np.sum(Y * np.log(A) (1-Y) * np.log(1-A))我们的目标就是通过调整参数W和b最小化这个成本J。这里在实现时要特别注意数值稳定性。当A非常接近0或1时np.log(A)可能会计算得到负无穷-inf导致后续计算失败。一个实用的技巧是在计算log时给A加上一个极小的常数如1e-8进行裁剪。2.3 反向传播梯度下降的核心知道了成本有多高下一步就是要知道该往哪个方向、以多大的幅度调整参数才能让成本降下来。这就是反向传播要计算的——梯度。通过链式求导法则我们可以推导出损失函数对各个参数的偏导数。对于逻辑回归推导出的梯度公式非常简洁dZ A - YdW (1/m) * np.dot(X, dZ.T)db (1/m) * np.sum(dZ)dZ是成本函数对Z的导数它恰好等于预测值A减去真实值Y这个简洁的结果是Sigmoid函数导数形式与交叉熵损失结合后的美妙特性。dW和db就是成本函数J对参数W和b的梯度。它们指明了参数更新的方向为了让J减小W和b应该向梯度的反方向移动。2.4 参数更新沿着梯度方向前进拿到梯度dW和db后我们用最基础的梯度下降法来更新参数W W - learning_rate * dWb b - learning_rate * db这里的learning_rate学习率是一个超参数它控制着每次更新的步长。步长太小收敛速度慢步长太大可能会在最小值点附近震荡甚至发散。选择合适的学习率至关重要通常可以从一个较小的值如0.01开始尝试。3. 代码实现与逐行解析理论清晰后我们进入实战环节。我将把整个模型拆解成几个独立的函数并逐一实现。我们会使用一个简单的二维数据集来验证代码的正确性。3.1 工具准备与数据生成首先我们导入必要的库并人工构造一个数据集。使用真实数据集前先用一个简单数据验证流程是很好的调试习惯。import numpy as np import matplotlib.pyplot as plt # 生成一个简单的二分类数据集 np.random.seed(42) # 固定随机种子确保结果可复现 m 200 # 样本数量 X np.random.randn(2, m) * 1.5 # 特征形状(2, 200) # 根据一条直线 y 0.5*x 0.2 加上噪声来生成标签 Y_raw (0.5 * X[0, :] 0.2 np.random.randn(m) * 0.1) X[1, :] Y Y_raw.reshape(1, m).astype(float) # 将标签转换为(1, 200)的矩阵并转为浮点型 # 快速可视化一下数据分布 plt.scatter(X[0, Y[0]1], X[1, Y[0]1], cb, labelClass 1) plt.scatter(X[0, Y[0]0], X[1, Y[0]0], cr, labelClass 0) plt.xlabel(Feature 1) plt.ylabel(Feature 2) plt.legend() plt.title(Synthetic Binary Classification Data) plt.show()这段代码生成了200个样本每个样本有2个特征。标签Y是根据一个线性关系加上噪声生成的因此逻辑回归模型理论上可以找到一个较好的决策边界来区分它们。将数据可视化能让你直观感受任务的难度。3.2 核心函数实现接下来我们实现三个最核心的函数Sigmoid、前向传播、反向传播。def sigmoid(z): 计算Sigmoid函数值 参数: z -- 标量或numpy数组 返回: s -- sigmoid(z) s 1 / (1 np.exp(-z)) return s def initialize_parameters(dim): 初始化权重矩阵W和偏置b 参数: dim -- 特征数量即W的大小 返回: params -- 包含W和b的字典 # 这里将W初始化为很小的随机值而不是全零。 # 全零初始化会导致对称性问题在更复杂的网络中影响学习但逻辑回归中影响不大。 # 我们保持习惯为后续学习神经网络做准备。 W np.random.randn(dim, 1) * 0.01 b 0.0 # 偏置初始化为0是常见的做法 parameters {W: W, b: b} return parameters def forward_propagation(X, parameters): 执行前向传播计算预测值A和成本J 参数: X -- 输入数据形状(n_x, m) parameters -- 字典包含W和b 返回: A -- 预测值形状(1, m) cache -- 包含Z的字典用于反向传播 cost -- 交叉熵成本 W parameters[W] b parameters[b] m X.shape[1] # 样本数量 # 线性计算 Z np.dot(W.T, X) b # 注意是W.T # 激活函数 A sigmoid(Z) # 确保A的数值稳定性避免log(0) A np.clip(A, 1e-8, 1 - 1e-8) # 计算成本 cost -1/m * np.sum(Y * np.log(A) (1-Y) * np.log(1-A)) cost np.squeeze(cost) # 确保cost是标量例如将[[17]]变成17 cache {Z: Z, A: A} return A, cache, cost def backward_propagation(X, Y, cache, parameters): 执行反向传播计算梯度 参数: X -- 输入数据形状(n_x, m) Y -- 真实标签形状(1, m) cache -- 前向传播输出的字典包含A parameters -- 字典包含W和b虽然这里没用到W和b的值但保留接口一致性 返回: grads -- 包含dW和db的字典 m X.shape[1] A cache[A] # 梯度计算 dZ A - Y dW 1/m * np.dot(X, dZ.T) # 注意维度对齐 db 1/m * np.sum(dZ) grads {dW: dW, db: db} return grads def update_parameters(parameters, grads, learning_rate): 使用梯度下降更新参数 参数: parameters -- 字典包含W和b grads -- 字典包含dW和db learning_rate -- 学习率 返回: parameters -- 更新后的参数字典 W parameters[W] b parameters[b] dW grads[dW] db grads[db] # 参数更新 W W - learning_rate * dW b b - learning_rate * db parameters {W: W, b: b} return parameters实操心得在forward_propagation函数中对A进行np.clip操作是一个非常重要的技巧。由于计算机的浮点数精度限制当Z非常大或非常小时sigmoid(Z)的结果在数学上会无限接近0或1。直接对这样的值取对数会得到负无穷或零导致后续计算出现nan非数字。用np.clip将其限制在一个很小的安全区间内如[1e-8, 1-1e-8]能有效保证数值稳定性。这是工业级代码中常见的做法。3.3 模型整合与训练循环现在我们把上面的函数组装起来形成一个完整的训练流程。def model(X, Y, num_iterations2000, learning_rate0.1, print_costFalse): 整合逻辑回归模型 参数: X -- 训练数据形状(n_x, m) Y -- 训练标签形状(1, m) num_iterations -- 迭代次数 learning_rate -- 学习率 print_cost -- 是否每100次迭代打印一次成本 返回: params -- 学习后的参数字典 costs -- 记录每次迭代成本的列表用于绘图 np.random.seed(1) # 再次固定随机种子确保参数初始化可复现 n_x X.shape[0] costs [] # 1. 初始化参数 parameters initialize_parameters(n_x) # 2. 梯度下降循环 for i in range(num_iterations): # 前向传播 A, cache, cost forward_propagation(X, parameters) # 每100次迭代记录一次成本 if i % 100 0: costs.append(cost) if print_cost: print(f迭代次数 {i}: 成本 {cost}) # 反向传播 grads backward_propagation(X, Y, cache, parameters) # 更新参数 parameters update_parameters(parameters, grads, learning_rate) # 训练完成后绘制成本下降曲线 plt.plot(costs) plt.ylabel(Cost) plt.xlabel(Iterations (per hundreds)) plt.title(fLearning rate {learning_rate}) plt.show() return parameters, costs现在让我们在生成的数据上运行这个模型。# 训练模型 parameters, costs model(X, Y, num_iterations2000, learning_rate0.1, print_costTrue) # 查看学习到的参数 print(f学习到的权重 W: {parameters[W]}) print(f学习到的偏置 b: {parameters[b]})运行后你应该能看到成本随着迭代次数增加而稳定下降的曲线这是模型正在有效学习的直观证明。最终打印出的W和b就是你的逻辑回归模型学到的决策边界参数。决策边界是满足W.T * X b 0这条直线在二维特征空间里。3.4 预测与评估模型训练好后我们需要用它来做预测并评估其性能。def predict(X, parameters): 使用学习到的参数进行预测 参数: X -- 输入数据形状(n_x, m) parameters -- 字典包含W和b 返回: Y_prediction -- 模型预测的标签0或1形状(1, m) # 前向传播得到概率A A, _, _ forward_propagation(X, parameters) # 将概率转换为0/1标签 Y_prediction (A 0.5).astype(float) # 以0.5为阈值 return Y_prediction # 在训练集上进行预测 Y_prediction_train predict(X, parameters) # 计算准确率 train_accuracy 100 - np.mean(np.abs(Y_prediction_train - Y)) * 100 print(f训练集准确率: {train_accuracy:.2f}%)对于我们的合成数据准确率应该接近100%。你还可以尝试用matplotlib将决策边界画出来直观地看模型是如何划分两类数据的。4. 关键调试技巧与常见问题自己实现算法时一定会遇到各种bug。下面是我总结的几个关键调试点和常见问题。4.1 维度错误最频繁的“杀手”在向量化计算中维度不匹配是最常见的错误。请牢记以下检查清单初始化时W的维度是(n_x, 1)b是标量。前向传播Z np.dot(W.T, X) b确保W.T是(1, n_x)X是(n_x, m)点积结果是(1, m)。反向传播dZ A - YA和Y都必须是(1, m)。dW (1/m) * np.dot(X, dZ.T)X是(n_x, m)dZ.T是(m, 1)点积结果是(n_x, 1)与W维度一致。 一个快速调试方法是在每个函数开头用print(X.shape)之类的语句打印关键变量的维度。4.2 学习率选择成本曲线会说话学习率对训练效果有决定性影响。运行模型后一定要绘制成本J随迭代次数变化的曲线。理想情况曲线平滑、单调下降最终趋于平稳。这说明学习率设置得当。学习率太大曲线会上下剧烈震荡甚至成本不降反升发散。这时你需要显著减小学习率例如从0.1降到0.01或0.001。学习率太小曲线下降得非常缓慢可能需要成千上万次迭代才能收敛。虽然最终也能学好但效率太低。可以适当增大学习率。对于这个简单的逻辑回归学习率在0.01到0.5之间通常都能工作。可以从0.1开始尝试。4.3 梯度检查验证反向传播的正确性当你确信自己的代码没有语法错误但成本就是不下降或者准确率极低时很可能是梯度计算dW和db的公式写错了。这时可以使用梯度检查这个“金标准”来验证。 梯度检查的核心思想是利用导数的定义来近似计算梯度并与你反向传播计算出的梯度进行比较。def gradient_check(parameters, grads, X, Y, epsilon1e-7): 简单的梯度检查 parameters_values parameters # 参数字典 grad grads # 梯度字典 # 检查dW W parameters_values[W].copy() for i in range(W.shape[0]): for j in range(W.shape[1]): # 计算J_plus W_plus W.copy() W_plus[i][j] W_plus[i][j] epsilon parameters_plus {W: W_plus, b: parameters[b]} A_plus, _, J_plus forward_propagation(X, parameters_plus) # 计算J_minus W_minus W.copy() W_minus[i][j] W_minus[i][j] - epsilon parameters_minus {W: W_minus, b: parameters[b]} A_minus, _, J_minus forward_propagation(X, parameters_minus) # 近似梯度 grad_approx (J_plus - J_minus) / (2 * epsilon) # 与反向传播计算的梯度比较 grad_backprop grads[dW][i][j] # 计算相对误差 numerator np.abs(grad_backprop - grad_approx) denominator np.abs(grad_backprop) np.abs(grad_approx) difference numerator / denominator if difference 1e-7: print(f梯度检查未通过参数W[{i}][{j}]反向传播梯度: {grad_backprop}近似梯度: {grad_approx}) return print(梯度检查通过)注意梯度检查计算量很大O(n^2)只适用于调试小规模模型。一旦验证通过在正式训练时务必关闭它。4.4 成本不下降的排查清单如果训练时成本居高不下或波动异常请按以下顺序排查检查数据确认输入X和标签Y的维度是否正确Y的值是否只有0和1可以用print(X.shape, Y.shape)和np.unique(Y)查看。检查初始化参数W是否初始化为很小的随机数如果全零初始化在逻辑回归中虽然可以工作但养成好习惯很重要。b初始化为0。检查前向传播计算出的A预测概率是否在0到1之间cost是否为单个数值不是数组检查反向传播使用上述梯度检查验证dW和db的计算是否正确。这是最可能出错的地方。检查参数更新确认更新公式是W W - learning_rate * dW符号是否正确调整学习率尝试将学习率调小一个数量级如从0.1调到0.01看成本曲线是否开始下降。5. 从逻辑回归到神经网络的思考通过亲手实现这个简单的逻辑回归你已经掌握了深度学习模型最核心的训练范式初始化参数 - 前向传播计算预测和损失 - 反向传播计算梯度 - 更新参数。这个循环是训练一切神经网络的基础。逻辑回归本身可以看作一个没有隐藏层的神经网络只有输入层和输出层。你实现的sigmoid函数就是它的激活函数。当你未来学习多层神经网络时你会发现前向传播变成了Z[l] W[l] * A[l-1] b[l],A[l] g[l](Z[l])其中g可以是Sigmoid、ReLU等。反向传播的公式会更复杂但核心思想依然是链式法则你需要计算每一层的dZ[l],dW[l],db[l]。参数初始化变得更加重要比如需要使用He初始化来配合ReLU激活函数。所以不要小看这个基础项目。理解这里的每一个细节特别是向量化操作和梯度计算会让你在后续学习卷积神经网络CNN、循环神经网络RNN时事半功倍。你可以尝试的下一步是用这个纯NumPy实现的逻辑回归去跑一下经典的鸢尾花数据集或乳腺癌数据集看看它在真实数据上的表现。然后尝试增加一个隐藏层把它变成一个真正的、浅层的前馈神经网络。你会发现大部分代码只需要稍作扩展即可复用。
返回列表