1. 从零开始:为什么逻辑回归是深度学习的“第一块砖”
如果你刚开始接触吴恩达老师的深度学习课程,学完第一周的理论,面对第二周的编程作业“实现简单逻辑回归”时,心里可能会犯嘀咕:这听起来像是机器学习入门的内容,和“深度”有什么关系?我当年也有同样的困惑,但真正动手实现一遍后,才恍然大悟:逻辑回归,恰恰是理解神经网络一切奥秘的起点。
你可以把它想象成乐高积木中最基础的那块2x4的砖。单看它,功能简单,就是实现一个二分类(是/否,猫/非猫)。但它的内部结构——输入、权重、偏置、激活函数、损失计算、梯度下降——构成了神经网络单个神经元(或者说感知机)的完整工作流程。后续无论多么复杂的网络,无论是10层、100层的全连接网络,还是卷积神经网络(CNN)、循环神经网络(RNN),其最基本的计算单元,其前向传播和反向传播的核心思想,都脱胎于这个简单的逻辑回归模型。
所以,这个作业的目的绝不是让你“复习”机器学习。它的核心价值在于:强迫你脱离高级框架(如TensorFlow、PyTorch)的“黑箱”,用最原始的NumPy,从数学公式到代码,亲手搭建并训练一个“神经元”。这个过程会让你对“权重如何更新”、“损失如何计算并反向传播”、“梯度下降究竟在做什么”产生肌肉记忆般的理解。很多人在后续学习中出现“梯度消失”、“爆炸”等问题时感到抽象,根源往往就在于跳过了这个亲手搭建的环节,对数据流动和梯度计算缺乏直观感受。
接下来,我将带你一步步拆解这个作业,不仅给出能跑通的代码,更重要的是解释每一个变量为什么这样定义,每一步计算背后的数学原理是什么,以及我在初次实现时踩过的那些“坑”。我们会用识别猫的图片这个经典二分类任务作为贯穿始终的例子。你需要准备的是:基本的Python编程知识、对NumPy数组操作有初步了解,以及一份渴望理解底层原理的好奇心。
2. 环境搭建与数据初探:别在第一步就卡住
在动手写模型之前,一个干净、可复现的环境和清晰的数据认知是成功的基石。很多新手在这里就会遇到版本冲突、路径错误等问题,导致兴致勃勃开始,灰头土脸结束。
2.1 构建专属的Python环境
我强烈建议你使用conda或venv创建一个独立的虚拟环境。这能确保你使用的库版本与课程要求一致,避免和系统中其他项目冲突。
# 使用 conda 创建环境(假设环境名为 dl_lr) conda create -n dl_lr python=3.7 -y conda activate dl_lr # 或者使用 venv python -m venv dl_lr_env # Windows 激活 dl_lr_env\Scripts\activate # Linux/Mac 激活 source dl_lr_env/bin/activate接下来安装核心依赖。吴恩达的课程作业通常基于 Jupyter Notebook,并使用h5py来读取课程提供的.h5格式数据集。
pip install numpy matplotlib jupyter h5py注意:NumPy版本不宜过高。一些较新的NumPy版本在广播机制或函数接口上有细微变化,可能导致与课程示例代码不兼容。使用
pip install numpy==1.19.5是一个比较稳妥的选择。
2.2 理解你的数据:猫 vs 非猫
课程提供的数据集通常是一个名为train_catvnoncat.h5和test_catvnoncat.h5的文件。我们用h5py加载它,并看看里面到底有什么。
import h5py import numpy as np # 加载训练集 train_dataset = h5py.File('datasets/train_catvnoncat.h5', "r") train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # 训练集特征 train_set_y_orig = np.array(train_dataset["train_set_y"][:]) # 训练集标签 # 加载测试集 test_dataset = h5py.File('datasets/test_catvnoncat.h5', "r") test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # 测试集特征 test_set_y_orig = np.array(test_dataset["test_set_y"][:]) # 测试集标签 # 查看数据形状 print ("训练集特征形状: " + str(train_set_x_orig.shape)) print ("训练集标签形状: " + str(train_set_y_orig.shape)) print ("测试集特征形状: " + str(test_set_x_orig.shape)) print ("测试集标签形状: " + str(test_set_y_orig.shape))你可能会看到类似这样的输出:
训练集特征形状: (209, 64, 64, 3) 训练集标签形状: (209,) 测试集特征形状: (50, 64, 64, 3) 测试集标签形状: (50,)数据解读与第一个“坑”:
train_set_x_orig形状是(209, 64, 64, 3)。这代表有209张训练图片,每张图片是64像素高、64像素宽、3个颜色通道(RGB)。train_set_y_orig形状是(209,),这是一个一维数组,包含209个标签,1代表“是猫”,0代表“非猫”。- 这里的关键点:我们的逻辑回归模型期望的输入是一个二维矩阵,形状为
(特征数量, 样本数量)。每个样本应该是一个特征向量,而不是一个三维的图片块。所以,我们需要把每张64x64x3的图片“展平”成一个长度为 64 * 64 * 3 = 12288 的向量。这就是所谓的“预处理”。
2.3 数据预处理:标准化与维度变换
预处理包含两步:展平和标准化。
# 1. 展平数据 # 训练集样本数 m_train = train_set_x_orig.shape[0] m_test = test_set_x_orig.shape[0] # 每张图片展平后的特征数 num_px = train_set_x_orig.shape[1] # 64 num_py = train_set_x_orig.shape[2] # 64 num_channels = train_set_x_orig.shape[3] # 3 train_set_x_flatten = train_set_x_orig.reshape(m_train, -1).T test_set_x_flatten = test_set_x_orig.reshape(m_test, -1).T print ("训练集展平后形状: " + str(train_set_x_flatten.shape)) print ("测试集展平后形状: " + str(test_set_x_flatten.shape)) # 输出应为: (12288, 209) 和 (12288, 50)为什么是.T(转置)?这是本作业乃至后续神经网络实现中一个至关重要的约定,也是容易混淆的点。展平后,train_set_x_flatten的形状最初是(209, 12288),即每一行是一个样本的所有特征。但在后续的向量化计算中,为了计算效率,我们通常将权重w设计为(12288, 1)的列向量。那么w.T * x(其中x是一个样本)就需要x是列向量。更通用的做法是,让数据矩阵X的每一列代表一个样本。所以我们需要转置,得到(12288, 209),这样第i列就是第i个样本的12288维特征向量。这个约定在后续的Z = w.T X + b计算中会非常自然。
# 2. 标准化数据 # 图片RGB像素值范围是0-255,将其标准化到0-1区间,有助于梯度下降更快更稳定地收敛。 train_set_x = train_set_x_flatten / 255. test_set_x = test_set_x_flatten / 255.至此,我们的数据train_set_x和test_set_x已经准备好了,形状分别是(12288, 209)和(12288, 50)。标签train_set_y和test_set_y保持为(1, 209)和(1, 50)的行向量形式(后续需要调整)。
3. 逻辑回归模型的核心架构:前向传播的数学到代码
模型的核心就是两个函数:前向传播(计算预测值和损失)和反向传播(计算梯度并更新参数)。我们先从前向传播开始,把每一步的数学公式和Python代码对应起来。
3.1 模型参数初始化:小随机数的智慧
逻辑回归有两个参数:权重w(形状:(n_x, 1)) 和偏置b(一个标量)。n_x是特征数量,这里是12288。
为什么用随机初始化,而不是全零?对于逻辑回归,其实初始化成全零是可以的,因为只有一个神经元,对称性破坏的问题不严重。但在神经网络中,如果所有权重初始化为相同值(包括全零),那么每个神经元在反向传播时会获得完全相同的梯度,导致所有神经元学习到相同的特征,失去了网络的意义。因此,养成随机初始化的习惯至关重要。我们使用很小的随机数(如0.01),是为了避免在激活函数(如sigmoid)的饱和区(梯度接近0)开始训练,导致学习缓慢。
def initialize_with_zeros(dim): """ 此函数创建一个维度为(dim, 1)的权重向量w,并将b初始化为0。 参数: dim -- 权重向量w的大小(本例中为特征数量) 返回: w -- 初始化的权重向量 (dim, 1) b -- 初始化的偏置(标量) """ w = np.random.randn(dim, 1) * 0.01 # 使用小随机数 b = 0.0 # 为了确保w和b的数据类型正确,我们使用assert来检查 assert(w.shape == (dim, 1)) assert(isinstance(b, float) or isinstance(b, int)) return w, b3.2 Sigmoid函数:将线性输出映射为概率
线性计算z = w.T * x + b的结果是一个实数,范围是负无穷到正无穷。我们需要一个函数将其映射到(0, 1)区间,代表“是猫”的概率。这个函数就是Sigmoid。
公式:σ(z) = 1 / (1 + e^(-z))
它的输出特性完美符合概率要求:当z很大时,σ(z)接近1;当z很小时,σ(z)接近0;当z=0时,σ(z)=0.5。
def sigmoid(z): """ 计算z的sigmoid值。 参数: z -- 一个标量或numpy数组。 返回: s -- z的sigmoid结果。 """ s = 1 / (1 + np.exp(-z)) return s实操心得:
np.exp(-z)在z是一个非常小的负数时(比如 -1000),可能会因为数值下溢而得到0,但这通常不会导致问题,因为1/(1+0)=1。反之,如果z是一个非常大的正数(比如1000),np.exp(-z)会下溢为0,计算1/(1+0)=1也是正确的。NumPy能很好地处理这些极端情况。但在自己实现时,意识到这些数值稳定性问题是有益的。
3.3 前向传播:计算预测值与损失
前向传播包含两步:
计算线性输出和激活值(预测值):
Z = w.T X + b(向量化后,X形状为(n_x, m), 所以Z形状为(1, m))A = σ(Z), A就是我们对每个样本的预测概率ŷ,形状也是(1, m)。计算损失(代价):我们使用交叉熵损失函数。对于单个样本:
L(a, y) = -[y*log(a) + (1-y)*log(1-a)]。对所有样本取平均得到代价函数J。
为什么用交叉熵损失而不是均方误差(MSE)?这是一个关键选择。对于分类问题,特别是输出经过Sigmoid激活后,MSE损失函数会是非凸的,存在很多局部最小值,不利于梯度下降优化。而交叉熵损失是凸函数,能保证梯度下降找到全局最优解(对于逻辑回归)。从信息论角度看,它衡量了预测分布与真实分布的“距离”。
def propagate(w, b, X, Y): """ 实现前向传播和反向传播,计算代价和梯度。 参数: w -- 权重,形状为 (n_x, 1) b -- 偏置,一个标量 X -- 输入数据,形状为 (n_x, m) Y -- 真实标签,形状为 (1, m) 返回: cost -- 逻辑回归的交叉熵代价 dw -- w的损失梯度,形状与w相同 db -- b的损失梯度,形状与b相同 """ m = X.shape[1] # 样本数量 # 前向传播 Z = np.dot(w.T, X) + b # Z形状: (1, m) A = sigmoid(Z) # A形状: (1, m), 即预测值 ŷ # 计算代价(交叉熵损失) # 这里使用了向量化操作,避免for循环。注意np.log可能对0或1输入产生警告,但我们的A理论上不会精确等于0或1。 cost = - (1/m) * np.sum(Y * np.log(A) + (1 - Y) * np.log(1 - A)) # 反向传播(计算梯度) dZ = A - Y # 这是Sigmoid激活函数和交叉熵损失结合后推导出的优美结果,形状: (1, m) dw = (1/m) * np.dot(X, dZ.T) # 形状: (n_x, 1) db = (1/m) * np.sum(dZ) # 标量 # 使用断言确保梯度形状正确 assert(dw.shape == w.shape) assert(db.dtype == float) # 将代价从数组转换为标量(如果它是数组的话) cost = np.squeeze(cost) assert(cost.shape == ()) # 将梯度和代价存入字典 grads = {"dw": dw, "db": db} return grads, cost反向传播公式推导的直观理解:dZ = A - Y这个公式是核心。它表示预测概率A与真实标签Y之间的误差。对于第i个样本,如果y=1而a很小(预测错误),那么dz就是一个很大的负数,意味着需要大幅调整参数。dw和db就是这个误差dZ对参数w和b的“贡献”的平均值。np.dot(X, dZ.T)实现了对所有样本梯度的向量化求和,效率远高于for循环。
4. 优化器:梯度下降的迭代艺术
有了计算代价和梯度的函数,我们就可以用梯度下降法来迭代优化参数w和b,使代价J最小化。
4.1 梯度下降的更新规则
公式:w = w - α * dwb = b - α * db其中α是学习率,控制每次参数更新的步长。
4.2 实现优化循环
我们将前向传播、反向传播和参数更新封装到一个优化函数中。
def optimize(w, b, X, Y, num_iterations, learning_rate, print_cost=False): """ 此函数通过梯度下降算法优化w和b 参数: w -- 权重,形状为 (n_x, 1) b -- 偏置,一个标量 X -- 输入数据,形状为 (n_x, m) Y -- 真实标签,形状为 (1, m) num_iterations -- 优化迭代次数 learning_rate -- 学习率,梯度下降的步长 print_cost -- 每100次迭代打印一次损失值 返回: params -- 包含优化后的w和b的字典 grads -- 包含最后一次迭代的梯度的字典 costs -- 记录每次迭代损失值的列表,用于绘图 """ costs = [] for i in range(num_iterations): # 计算当前参数下的梯度和代价 grads, cost = propagate(w, b, X, Y) # 从grads字典中取出梯度 dw = grads["dw"] db = grads["db"] # 梯度下降更新规则 w = w - learning_rate * dw b = b - learning_rate * db # 每100次迭代记录一次代价 if i % 100 == 0: costs.append(cost) if print_cost: print ("第 %i 次迭代后的损失值: %f" %(i, cost)) # 将最终参数和梯度存入字典 params = {"w": w, "b": b} grads = {"dw": dw, "db": db} return params, grads, costs学习率的选择与第二个“坑”:学习率α是梯度下降中最重要的超参数之一。太大(如1.0),代价函数可能会震荡甚至发散;太小(如0.000001),收敛速度会极慢。对于这个猫分类数据集,经过实践,0.005或0.01是一个不错的起点。在optimize函数中,我们记录了每100次迭代的损失,并可以打印出来。务必观察损失曲线的变化:一个健康的训练过程,损失值应该随着迭代平稳下降,最终趋于平缓。如果损失值上下跳动或上升,很可能意味着学习率太大了。
4.3 预测与模型评估
训练完成后,我们需要用学到的参数w和b对新的数据进行预测,并评估模型在训练集和测试集上的准确率。
def predict(w, b, X): ''' 使用学习到的逻辑回归参数(w, b)预测X的标签 参数: w -- 权重,形状为 (n_x, 1) b -- 偏置,一个标量 X -- 输入数据,形状为 (n_x, m) 返回: Y_prediction -- 包含X中所有样本预测结果(0/1)的numpy数组,形状为(1, m) ''' m = X.shape[1] Y_prediction = np.zeros((1, m)) w = w.reshape(X.shape[0], 1) # 确保w形状正确 # 计算预测概率A A = sigmoid(np.dot(w.T, X) + b) # 将概率转换为0/1预测 # 方法1: 使用for循环(直观但慢) # for i in range(A.shape[1]): # if A[0, i] > 0.5: # Y_prediction[0, i] = 1 # else: # Y_prediction[0, i] = 0 # 方法2: 向量化操作(推荐,速度快) Y_prediction = (A > 0.5).astype(int) return Y_prediction评估准确率很简单,就是比较预测值Y_prediction和真实标签Y相等的比例。
def model(X_train, Y_train, X_test, Y_test, num_iterations=2000, learning_rate=0.005, print_cost=False): """ 构建完整的逻辑回归模型 参数: X_train -- 训练集,形状为 (n_x, m_train) Y_train -- 训练标签,形状为 (1, m_train) X_test -- 测试集,形状为 (n_x, m_test) Y_test -- 测试标签,形状为 (1, m_test) num_iterations -- 迭代次数 learning_rate -- 学习率 print_cost -- 是否打印训练过程中的损失 返回: d -- 包含模型信息的字典 """ # 1. 初始化参数 w, b = initialize_with_zeros(X_train.shape[0]) # 2. 梯度下降优化 params, grads, costs = optimize(w, b, X_train, Y_train, num_iterations, learning_rate, print_cost) # 3. 获取训练好的参数 w = params["w"] b = params["b"] # 4. 在训练集和测试集上进行预测 Y_prediction_train = predict(w, b, X_train) Y_prediction_test = predict(w, b, X_test) # 5. 计算准确率 train_accuracy = 100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100 test_accuracy = 100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100 # 打印结果 print("训练集准确率: {:.2f} %".format(train_accuracy)) print("测试集准确率: {:.2f} %".format(test_accuracy)) # 将结果存入字典 d = {"costs": costs, "Y_prediction_train": Y_prediction_train, "Y_prediction_test": Y_prediction_test, "w": w, "b": b, "learning_rate": learning_rate, "num_iterations": num_iterations} return d5. 模型训练、分析与可视化:读懂你的模型
现在,让我们把所有部分组合起来,训练模型并分析结果。
# 调用模型函数 d = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations=2000, learning_rate=0.005, print_cost=True)运行后,你可能会看到类似下面的输出:
第 0 次迭代后的损失值: 0.693147 第 100 次迭代后的损失值: 0.584508 第 200 次迭代后的损失值: 0.466949 ... 第 1800 次迭代后的损失值: 0.058389 第 1900 次迭代后的损失值: 0.053441 训练集准确率: 99.04 % 测试集准确率: 70.00 %5.1 解读训练结果:过拟合与欠拟合
这是一个非常典型的结果:
- 训练集准确率很高(99%):说明模型已经很好地记住了训练数据。
- 测试集准确率较低(70%):说明模型的泛化能力一般。模型从训练数据中学到的规律,不能完美地推广到没见过的测试数据上。
这指向了机器学习中的一个核心问题:过拟合。我们的模型(一个简单的逻辑回归)可能过于复杂(相对于这个小数据集),或者更常见的是,我们使用的特征(12288个像素值)对于区分猫和非猫来说,并不是最有效的。原始像素包含大量冗余和噪声信息。在深度学习中,我们会使用卷积神经网络(CNN)来自动学习更高级、更鲁棒的特征,从而大幅提升性能。但在这个入门作业中,70%左右的测试准确率是符合预期的,它清晰地展示了使用原始像素的线性分类器的局限性。
5.2 可视化学习过程
绘制损失曲线是诊断训练过程是否健康的重要手段。
import matplotlib.pyplot as plt # 绘制损失曲线 costs = np.squeeze(d['costs']) plt.plot(costs) plt.ylabel('cost') plt.xlabel('iterations (per hundreds)') plt.title("Learning rate =" + str(d["learning_rate"])) plt.show()你应该看到一条随着迭代次数增加而单调下降的曲线,最终趋于平缓。如果曲线震荡,说明学习率太大;如果下降极其缓慢,说明学习率太小。
5.3 学习率的影响实验
为了深刻理解学习率的作用,我们可以进行一个简单的实验:用不同的学习率训练模型,并观察损失曲线的变化。
learning_rates = [0.01, 0.005, 0.001, 0.0005] models = {} for lr in learning_rates: print ("使用学习率: " + str(lr)) models[str(lr)] = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations=1500, learning_rate=lr, print_cost=False) print ('\n' + "-------------------------------------------------------" + '\n') # 绘制不同学习率下的损失曲线 for lr in learning_rates: plt.plot(np.squeeze(models[str(lr)]["costs"]), label=str(lr)) plt.ylabel('cost') plt.xlabel('iterations (per hundreds)') plt.legend(loc='upper right', shadow=True) plt.title("不同学习率下的损失曲线") plt.show()通过这个对比图,你可以直观地看到:
- 学习率过大(如0.01):损失曲线可能震荡,甚至无法收敛(代价上升)。
- 学习率过小(如0.0005):损失下降非常缓慢,需要更多迭代才能达到相同的效果。
- 学习率适中(如0.005):损失平稳快速下降。
这个实验虽然简单,但它体现了深度学习调参的基本方法论:通过控制变量,观察模型性能(损失、准确率)的变化,从而找到合适的超参数。
6. 调试与常见问题排查:当你代码不工作时
第一次实现时,几乎一定会遇到各种错误。下面是我总结的几个最常见的“坑”及其解决方法。
6.1 维度不匹配错误
这是NumPy向量化操作中最常见的错误。错误信息通常是ValueError: shapes (a,b) and (c,d) not aligned。
排查清单:
检查
X,Y,w,b的初始形状。X_train应该是(n_x, m_train),例如(12288, 209)。检查你是否做了转置.T。Y_train应该是(1, m_train),例如(1, 209)。如果原始标签是(209,),需要用Y = Y.reshape(1, -1)进行重塑。w应该是(n_x, 1)。b应该是一个标量(float)。
在
propagate函数中,逐行检查矩阵运算的维度。Z = np.dot(w.T, X) + b:w.T是(1, n_x),X是(n_x, m),点积结果是(1, m)。b通过广播机制加到每个元素上。dZ = A - Y:A和Y都必须是(1, m)。dw = (1/m) * np.dot(X, dZ.T):X是(n_x, m),dZ.T是(m, 1),点积结果是(n_x, 1),与w形状一致。db = (1/m) * np.sum(dZ):对(1, m)的dZ求和,得到一个标量。
调试技巧:在函数的关键步骤后使用print(X.shape)打印数组形状,确保与你的预期一致。
6.2 损失值为 NaN 或无限大
这通常是由于数值计算问题引起的。
- 检查 Sigmoid 函数的输入
Z。如果Z的值非常大(正或负),np.exp(-z)可能会溢出(得到inf)或下溢(得到0)。虽然NumPy的sigmoid实现通常能处理,但如果你自己写的sigmoid没有做数值稳定处理,就可能出问题。确保使用1 / (1 + np.exp(-z))。 - 检查交叉熵损失计算中的
log。当预测概率A非常接近0或1时,np.log(A)或np.log(1-A)可能得到负无穷(-inf)。一个常见的稳定化技巧是“裁剪”:# 在计算损失前,将A限制在一个很小的范围内,如[1e-15, 1-1e-15] A = np.clip(A, 1e-15, 1 - 1e-15) cost = - (1/m) * np.sum(Y * np.log(A) + (1 - Y) * np.log(1 - A)) - 检查学习率。过大的学习率可能导致参数更新步伐太大,使得损失“爆炸”成NaN。尝试将学习率降低一个数量级(例如从0.1降到0.01)。
6.3 模型性能不佳(准确率接近50%)
如果训练集和测试集准确率都只在50%左右徘徊,说明模型没有学到任何东西,相当于随机猜测。
- 检查数据预处理。确保标签
Y是正确的(0或1)。确保数据标准化(/255.)已经完成。 - 检查参数初始化。确保权重
w是用小随机数初始化的(如np.random.randn(dim,1)*0.01)。如果初始化为全零,对于逻辑回归虽然可以工作,但养成好习惯很重要。如果初始值太大,可能导致Sigmoid饱和,梯度消失,学习停滞。 - 检查梯度计算是否正确。这是最可能的原因。实现一个梯度检查函数是深度学习入门阶段的必备技能。梯度检查通过数值方法(使用极限定义)近似计算梯度,并与你反向传播计算的解析梯度进行比较。如果两者差异很大,说明你的反向传播实现有bug。由于篇幅所限,梯度检查的实现细节较多,但其核心思想是:对于每个参数
θ,计算J(θ+ε) - J(θ-ε) / (2ε)作为数值梯度,并与你的dθ比较。相对误差应在1e-7量级。 - 检查学习率和迭代次数。学习率可能太小,或者迭代次数不够。尝试增加
num_iterations到5000或10000,观察损失是否还在下降。
7. 从逻辑回归到神经网络:思维的跃迁
完成这个简单的逻辑回归实现,其意义远不止于完成一次作业。它为你搭建了理解神经网络的完整思维框架。
单个神经元就是逻辑回归:你现在实现的,就是一个没有隐藏层的神经网络。输入层(12288个神经元)直接连接到输出层(1个神经元,使用Sigmoid激活)。
前向传播的通用模式:无论网络多复杂,前向传播都是Z[l] = W[l]A[l-1] + b[l],A[l] = g[l](Z[l])的重复。你刚刚实现了l=1的这一层。
反向传播的基石:你推导并实现了dZ = A - Y,以及dw和db的计算。在多层网络中,反向传播无非是从输出层开始,将这个误差一层层往回传递,利用链式法则计算每一层参数的梯度。你现在的理解,是解开反向传播黑盒的第一把钥匙。
向量化的重要性:你体验了用np.dot代替for循环带来的效率提升。在真正的深度网络中,面对百万级的数据,向量化是唯一可行的道路。
当你后续学习真正的神经网络时,你会看到:
- 初始化:从
initialize_with_zeros扩展到He或Xavier初始化。 - 激活函数:从
Sigmoid扩展到ReLU,Tanh。 - 损失函数:从二分类交叉熵扩展到多分类交叉熵、均方误差等。
- 优化器:从基础的梯度下降扩展到
Momentum,RMSprop,Adam。
但万变不离其宗,核心的数据流动(前向/反向)、梯度计算、参数更新的逻辑,你已经在这次“简单”的逻辑回归实现中亲手搭建并运行过了。这份从零开始构建的体验,是直接调用model.fit()所无法替代的。它赋予你一种“手感”和“直觉”,让你在后续面对更复杂的模型和更诡异的问题时,能有章法地进行分析和调试,而不是停留在盲目调参的层面。这就是这个作业,这块深度学习的“第一块砖”,真正想交给你的东西。