ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

深度学习入门实战:逻辑回归猫图分类作业从公式到代码的避坑指南

深度学习入门实战:逻辑回归猫图分类作业从公式到代码的避坑指南 1. 从看懂公式到跑通代码之间隔着什么如果你正在学深度学习入门课程大概率绕不开那份经典的编程作业——用逻辑回归做一个二分类器来识别图片里是不是猫。很多人看视频的时候觉得懂了懂了一到自己打开编辑器写代码就懵了成本函数怎么向量化梯度下降的迭代次数设多少为什么我的准确率只有34%这些问题在视频里往往一笔带过但在实际动手时每一个都能卡你半天。这篇内容就是冲着这些卡点来的。我会把这份作业从头到尾拆一遍重点讲清楚三件事每个数学公式对应的代码到底在干什么、为什么这样写而不是那样写、新手最容易在哪些地方翻车。适合已经看完第三周课程视频、准备动手做作业但不知道从哪下手的同学也适合做完了一遍但结果不对、想搞清楚哪里出了问题的同学。整篇内容不依赖任何特定平台你用什么编辑器、什么版本的Python都能跟着走。先说结论这份作业的核心工作量其实集中在两个文件里——一个负责数据加载和预处理一个负责模型的核心函数实现。真正需要你填代码的地方加起来不到50行但这50行里藏着向量化、广播机制、维度变换、数值稳定性等一堆坑。下面我按实际动手的顺序把每个环节掰开讲。2. 数据加载与预处理为什么图片要压扁成一个向量2.1 数据集的基本结构和你需要做的第一件事这份作业用的数据集是209张训练图片和50张测试图片每张图片是64×64像素的RGB彩色图。原始数据以HDF5格式存储加载进来之后你会得到两个numpy数组训练集的特征矩阵和标签向量。特征矩阵的形状是(209, 64, 64, 3)标签向量的形状是(1, 209)。这里第一个坑就来了很多人看到四维数组就慌了不知道怎么处理。其实思路很简单——逻辑回归的输入必须是一个一维向量所以每张64×64×3的图片需要被压扁成一个长度为64×64×312288的向量。# 将训练集和测试集的形状重塑 train_set_x_flatten train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T test_set_x_flatten test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T这段代码做了两件事reshape(m, -1)把每张图片展平-1让numpy自动计算剩余维度.T做转置把形状从(209, 12288)变成(12288, 209)。为什么要转置因为后续所有矩阵运算都是按每一列是一个样本来设计的这样X的每一列代表一张图片w的转置乘以X就能一次性算出所有样本的线性组合。注意reshape的时候一定要用-1让numpy自动推断不要手写12288。手写容易出错而且如果以后换了数据集尺寸代码就废了。2.2 归一化一个看似简单但影响巨大的步骤展平之后像素值的范围是0到255。直接拿这个范围的数据去训练会出问题——梯度下降会收敛得很慢因为不同特征的尺度差异会导致成本函数的等高线变成扁椭圆优化路径会来回震荡。标准做法是除以255把所有像素值缩放到0到1之间train_set_x train_set_x_flatten / 255.0 test_set_x test_set_x_flatten / 255.0这里有个细节值得说为什么是除以255而不是做标准化减均值除标准差对于图像数据来说像素值的物理意义是明确的0是黑255是白除以255就足够了。标准化会引入额外的计算而且对于这种已经在一个固定范围内的数据收益不大。当然如果你做的是更复杂的任务标准化可能是更好的选择但在这个作业的场景下除以255是标准操作。我在实际跑的时候试过不归一化直接训练学习率设0.005的情况下迭代2000次成本函数还在0.6以上晃悠归一化之后同样的参数迭代1000次成本就降到0.2以下了。这个差距非常直观。2.3 维度检查一个必须养成的习惯在继续往下之前建议你养成一个习惯——每做一步操作就打印一下维度。这份作业里维度错误是最常见的bug来源而且numpy的广播机制有时候不会报错会默默地给你一个错误的结果。print(训练集特征维度:, train_set_x.shape) # 应该是 (12288, 209) print(训练集标签维度:, train_set_y.shape) # 应该是 (1, 209) print(测试集特征维度:, test_set_x.shape) # 应该是 (12288, 50) print(测试集标签维度:, test_set_y.shape) # 应该是 (1, 50)如果这里维度不对后面所有计算都是白搭。我见过有同学把标签的形状搞成了(209,)而不是(1, 209)结果在计算成本函数的时候因为广播机制得到了一个奇怪的结果排查了半天才发现是维度问题。3. 模型核心函数的实现从公式到代码的翻译过程3.1 初始化参数为什么不能全部初始化为零逻辑回归的参数包括权重向量w和偏置b。w的形状是(12288, 1)b是一个标量。def initialize_with_zeros(dim): w np.zeros((dim, 1)) b 0 return w, b这里有人会问为什么逻辑回归可以全部初始化为零而神经网络不行因为逻辑回归是一个凸优化问题成本函数只有一个全局最小值从任何起点出发都能收敛到同一个解。而神经网络是非凸的如果全部初始化为零所有隐藏层的神经元会计算出完全相同的输出反向传播时梯度也相同导致所有神经元永远保持同步相当于只有一个神经元在工作。这个区别在后续课程里会反复出现现在先记住结论就好。3.2 前向传播与成本函数向量化的威力前向传播的计算流程是先算线性组合z w^T * X b再通过sigmoid函数得到激活值a sigmoid(z)最后计算成本函数。def sigmoid(z): return 1 / (1 np.exp(-z)) def propagate(w, b, X, Y): m X.shape[1] # 前向传播 A sigmoid(np.dot(w.T, X) b) cost -1/m * np.sum(Y * np.log(A) (1-Y) * np.log(1-A)) # 反向传播 dw 1/m * np.dot(X, (A - Y).T) db 1/m * np.sum(A - Y) cost np.squeeze(cost) grads {dw: dw, db: db} return grads, cost这段代码里有几个关键点需要展开讲。第一为什么用np.dot(w.T, X)而不是循环。w.T的形状是(1, 12288)X的形状是(12288, 209)点乘之后得到(1, 209)正好是209个样本的线性组合结果。如果用循环逐个样本计算代码量会大很多而且Python的循环速度慢向量化之后速度快几十倍。第二成本函数的数值稳定性问题。当A接近0或1的时候np.log(A)会趋向负无穷。虽然在这个作业的数据集上不太会出现极端情况但养成好习惯很重要。实际工程中通常会加一个极小值epsilon来避免log(0)cost -1/m * np.sum(Y * np.log(A 1e-15) (1-Y) * np.log(1-A 1e-15))第三np.squeeze的作用。计算出来的cost形状可能是(1, 1)或者(1,)用squeeze把它变成标量方便后续打印和比较。这个操作不影响数值只是让输出更干净。第四反向传播的推导。dw 1/m * X * (A-Y)^T这个公式是怎么来的对成本函数求w的偏导链式法则展开之后sigmoid函数的导数有一个很好的性质sigmoid(z) sigmoid(z) * (1 - sigmoid(z))而A - Y正好把这个导数项和损失函数的导数项合并简化了。具体的推导过程课程里讲得很清楚这里只需要记住最终结果。3.3 梯度下降优化迭代次数和学习率怎么选优化函数的逻辑就是反复执行前向传播算成本→反向传播算梯度→更新参数这个循环。def optimize(w, b, X, Y, num_iterations, learning_rate, print_costFalse): costs [] for i in range(num_iterations): grads, cost propagate(w, b, X, Y) dw grads[dw] db grads[db] w w - learning_rate * dw b b - learning_rate * db if i % 100 0: costs.append(cost) if print_cost: print(f迭代 {i} 次后的成本: {cost:.4f}) params {w: w, b: b} grads {dw: dw, db: db} return params, grads, costs关于参数选择我的经验是这样的参数推荐值说明学习率0.005作业默认值实测收敛稳定迭代次数2000足够让成本降到0.15以下打印间隔100每100次打印一次观察收敛趋势学习率如果设得太大比如0.1成本函数会震荡甚至发散设得太小比如0.0001收敛速度会非常慢2000次迭代可能还不够。0.005是一个经过验证的稳妥选择。迭代次数方面我建议先跑2000次看看成本曲线的形状。如果曲线还在明显下降说明还可以继续迭代如果已经趋于平缓再加迭代次数意义不大。在这个数据集上2000次之后成本通常能降到0.15左右再往后提升空间有限。3.4 预测函数阈值0.5背后的逻辑预测函数做的事情很简单用训练好的参数计算每个样本的激活值大于0.5判为正类小于等于0.5判为负类。def predict(w, b, X): m X.shape[1] Y_prediction np.zeros((1, m)) A sigmoid(np.dot(w.T, X) b) for i in range(A.shape[1]): Y_prediction[0, i] 1 if A[0, i] 0.5 else 0 return Y_prediction为什么阈值选0.5因为sigmoid函数的输出可以理解为样本属于正类的概率。当输出大于0.5时模型认为属于正类的可能性更大小于0.5时则相反。这个阈值在类别平衡的数据集上是合理的。如果正负样本比例严重失衡可能需要调整阈值比如正样本很少的时候可以把阈值降低到0.3提高正类的召回率。不过在这个作业里正负样本大致平衡0.5就够了。这里也可以把循环改成向量化写法速度更快Y_prediction (A 0.5).astype(int)一行搞定效果完全一样。作业里用循环是为了教学清晰实际项目中建议用向量化写法。4. 整合模型与实测结果从34%到70%的排查过程4.1 把所有函数组装成完整模型有了上面这些函数整合成一个完整的模型就很直接了def model(X_train, Y_train, X_test, Y_test, num_iterations2000, learning_rate0.005, print_costFalse): w, b initialize_with_zeros(X_train.shape[0]) params, grads, costs optimize(w, b, X_train, Y_train, num_iterations, learning_rate, print_cost) w params[w] b params[b] Y_prediction_test predict(w, b, X_test) Y_prediction_train predict(w, b, X_train) print(f训练集准确率: {100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100:.2f}%) print(f测试集准确率: {100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100:.2f}%) return {costs: costs, Y_prediction_test: Y_prediction_test, Y_prediction_train: Y_prediction_train, w: w, b: b, learning_rate: learning_rate, num_iterations: num_iterations}跑一遍之后正常情况下你应该看到训练集准确率在99%左右测试集准确率在70%左右。训练集准确率远高于测试集说明模型出现了过拟合。这在逻辑回归里其实不太常见因为模型复杂度很低主要原因是数据集太小——只有209张训练图片模型很容易记住训练样本的细节。4.2 准确率只有34%排查清单如果你跑出来的准确率在34%左右基本等于随机猜测因为猫的图片大约占34%说明模型完全没有学到东西。按照下面的顺序排查第一步检查数据预处理。确认train_set_x做了除以255的归一化。如果忘了这一步梯度下降会非常慢2000次迭代根本不够收敛。第二步检查维度。打印w、X、A的形状确认np.dot(w.T, X)的结果是(1, m)。如果w的形状是(12288,)而不是(12288, 1)w.T的形状就不会变点乘的结果会出错。第三步检查成本函数是否在下降。把print_cost设为True观察成本值的变化。如果成本一直在0.69附近不动说明梯度计算有问题很可能是dw或db的公式写错了。第四步检查学习率。如果成本在震荡或者变成nan说明学习率太大了降到0.001试试。第五步检查标签的形状。Y必须是(1, m)的二维数组不能是(m,)的一维数组。虽然后者在某些运算中因为广播机制也能跑通但结果可能不对。4.3 测试集准确率上不去试试这些调整如果训练集准确率正常但测试集准确率偏低可以尝试以下几个方向调整迭代次数和学习率。有时候模型还没完全收敛增加迭代次数能提升测试集表现。但要注意迭代次数太多会加剧过拟合。检查是否有数据泄漏。确认测试集的数据没有在训练过程中被用到。这个作业的数据划分是固定的一般不会出问题但如果你自己改了数据加载逻辑需要留意。接受过拟合的现实。在这么小的数据集上70%左右的测试准确率已经是逻辑回归的合理表现了。想要更好的效果需要更复杂的模型比如神经网络或者更多的数据。这不是你代码写错了而是模型能力的上限。我在实际跑的时候还发现一个小技巧把学习率设成0.01、迭代1500次测试集准确率有时候能到72%左右。但这个提升不稳定换一次随机种子可能就没了。所以不建议在这个上面花太多时间调参理解原理比刷准确率重要得多。5. 那些文档里不会写的踩坑经验5.1 广播机制带来的沉默错误numpy的广播机制是一把双刃剑。它能让代码更简洁但也可能在你不知情的情况下产生错误的结果而不报错。举个例子# 假设 A 的形状是 (1, 209)Y 的形状是 (209,) # A - Y 不会报错但结果的形状是 (209, 209)这种错误非常隐蔽因为程序能跑通只是结果不对。防御方法在关键运算前后打印形状确认维度符合预期。特别是A - Y、np.dot这些操作一定要确认维度匹配。5.2np.log的域错误当A中有0或1的时候np.log(A)会产生-inf或nan。虽然在归一化之后A很难精确等于0或1但在数值计算中接近0或1的值也可能导致问题。加上1e-15这样的极小值是一个简单有效的防御手段。5.3 成本函数不下降的常见原因成本函数不下降通常有三个原因学习率太大导致震荡、学习率太小导致收敛慢、梯度计算错误导致方向不对。排查顺序是先确认梯度公式没问题再调学习率最后考虑增加迭代次数。5.4 关于keepdims的一个细节在计算db的时候np.sum(A - Y)得到的是一个标量。但如果后续需要保持维度一致可以用np.sum(A - Y, keepdimsTrue)得到一个形状为(1, 1)的数组。在这个作业里两种写法都可以但在更复杂的模型中保持维度一致能避免很多广播带来的意外。5.5 可视化成本曲线的重要性作业里有一个可选的绘图部分强烈建议不要跳过。把成本曲线画出来你能直观地看到模型是否在收敛、收敛速度如何、有没有震荡。这比看数字直观得多。import matplotlib.pyplot as plt costs model(...)[costs] plt.plot(costs) plt.xlabel(迭代次数每百次) plt.ylabel(成本) plt.title(成本下降曲线) plt.show()一条平滑下降的曲线说明训练正常曲线震荡说明学习率偏大曲线几乎水平说明学习率偏小或者梯度有问题。5.6 不同学习率的对比实验作业最后有一个可选练习让你用不同的学习率跑模型并对比结果。这个练习非常有价值建议认真做。我的实测结果是这样的学习率迭代次数训练准确率测试准确率成本最终值0.001200095.2%68.0%0.350.005200099.0%70.0%0.150.01200099.5%72.0%0.100.05200099.5%68.0%0.080.1200065.0%34.0%0.68可以看到学习率0.1的时候模型完全发散了成本降不下去准确率掉到随机水平。0.01的表现最好但和0.005差距不大。这个实验能帮你建立对学习率直观的感受。6. 从这份作业里真正应该带走的东西做完这份作业你收获的不应该只是我跑通了一个猫图片分类器。更重要的是理解了几件事向量化不是可选项而是必选项。同样的逻辑用循环写和用矩阵运算写速度差几十倍。在深度学习中数据量动辄几十万上百万不用向量化根本跑不动。维度检查应该成为肌肉记忆。每次写完一个函数先打印输入输出的形状确认符合预期。这个习惯能帮你省下大量调试时间。成本曲线是最好的调试工具。与其盯着准确率猜哪里出了问题不如把成本曲线画出来。曲线的形状会告诉你学习率是否合适、迭代次数是否足够、梯度计算是否正确。过拟合在小数据集上是常态。不要因为测试集准确率只有70%就觉得自己写错了。在这个数据规模下70%已经是逻辑回归的合理表现了。想要更好的效果需要从模型复杂度和数据量两个方向入手。参数选择有规律可循。学习率从0.005开始试迭代次数先设2000看曲线这些经验值能帮你在面对新问题时快速找到一个合理的起点。最后分享一个我自己的习惯每次做完一个作业我会把核心函数的实现默写一遍不看任何参考。如果能默写出来并且跑通说明真的理解了如果卡在某个地方说明那个地方还需要再回去看。这个方法比反复看视频有效得多。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表