用Python和NumPy从零实现神经网络:吴恩达深度学习课实践指南

在咖啡厅里盯着吴恩达教授的《深度学习》课程视频,我反复拖动进度条试图理解那些矩阵运算的含义。直到有一天,我决定关掉视频,打开Jupyter笔记本,用最基础的Python和NumPy库亲手实现一个神经网络——这个决定让我真正理解了反向传播的优雅和向量化的威力。

1. 环境准备与基础概念

工欲善其事,必先利其器。我们需要准备最精简的工具链:

import numpy as np
import matplotlib.pyplot as plt

神经网络的核心在于用数学函数模拟生物神经元。一个最简单的神经网络单元可以表示为:

输出 = 激活函数(权重·输入 + 偏置)

这个公式看似简单,却蕴含着几个关键概念:

  • 权重(Weights): 决定每个输入特征的重要性
  • 偏置(Bias): 调整神经元的激活阈值
  • 激活函数: 引入非线性(常用Sigmoid、ReLU等)

初学者常犯的错误是直接使用线性函数作为激活——这会导致网络退化为线性回归,失去"深度"的意义。

2. 实现逻辑回归模型

逻辑回归实际上是单层神经网络,非常适合作为入门案例。我们需要实现三个核心组件:

2.1 Sigmoid激活函数

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

这个函数的输出范围是(0,1),非常适合二分类问题。它的导数有个优雅的性质:

def sigmoid_derivative(z):
    return sigmoid(z) * (1 - sigmoid(z))

2.2 前向传播

前向传播就是数据通过网络流动的过程:

def forward_prop(X, W, b):
    Z = np.dot(W.T, X) + b  # 线性部分
    A = sigmoid(Z)          # 激活部分
    return A

2.3 损失函数与梯度计算

二元交叉熵损失函数衡量预测与真实的差距:

def compute_cost(A, Y):
    m = Y.shape[1]
    cost = -np.mean(Y * np.log(A) + (1-Y) * np.log(1-A))
    return cost

反向传播计算梯度时,初学者常困惑于矩阵维度。记住这个黄金法则:

dW = (1/m) * X · (A-Y).T
db = (1/m) * np.sum(A-Y)

3. 构建单隐层神经网络

现在升级到真正的神经网络——包含一个隐藏层。这需要管理两组参数(W1,b1)和(W2,b2)。

3.1 参数初始化

随机初始化打破对称性很重要:

def initialize_parameters(n_x, n_h, n_y):
    W1 = np.random.randn(n_h, n_x) * 0.01
    b1 = np.zeros((n_h, 1))
    W2 = np.random.randn(n_y, n_h) * 0.01
    b2 = np.zeros((n_y, 1))
    return {"W1": W1, "b1": b1, "W2": W2, "b2": b2}

3.2 完整的前向传播

def forward_propagation(X, parameters):
    W1, b1, W2, b2 = parameters['W1'], parameters['b1'], parameters['W2'], parameters['b2']
    
    Z1 = np.dot(W1, X) + b1
    A1 = np.tanh(Z1)  # 隐藏层使用tanh激活
    Z2 = np.dot(W2, A1) + b2
    A2 = sigmoid(Z2)  # 输出层仍用sigmoid
    
    cache = {"Z1": Z1, "A1": A1, "Z2": Z2, "A2": A2}
    return A2, cache

3.3 反向传播的实现

这是最考验理解的部分,需要计算四个梯度:

def backward_propagation(parameters, cache, X, Y):
    m = X.shape[1]
    
    W2 = parameters['W2']
    A1 = cache['A1']
    A2 = cache['A2']
    
    dZ2 = A2 - Y
    dW2 = (1/m) * np.dot(dZ2, A1.T)
    db2 = (1/m) * np.sum(dZ2, axis=1, keepdims=True)
    
    dZ1 = np.dot(W2.T, dZ2) * (1 - np.power(A1, 2))  # tanh的导数是1-A^2
    dW1 = (1/m) * np.dot(dZ1, X.T)
    db1 = (1/m) * np.sum(dZ1, axis=1, keepdims=True)
    
    return {"dW1": dW1, "db1": db1, "dW2": dW2, "db2": db2}

4. 训练过程与可视化

4.1 参数更新

def update_parameters(parameters, grads, learning_rate):
    W1 = parameters['W1'] - learning_rate * grads['dW1']
    b1 = parameters['b1'] - learning_rate * grads['db1']
    W2 = parameters['W2'] - learning_rate * grads['dW2']
    b2 = parameters['b2'] - learning_rate * grads['db2']
    
    return {"W1": W1, "b1": b1, "W2": W2, "b2": b2}

4.2 训练循环

完整的训练过程需要迭代以下步骤:

  1. 初始化参数
  2. 前向传播计算预测值
  3. 计算损失函数
  4. 反向传播计算梯度
  5. 更新参数
  6. 重复直到收敛
def train(X, Y, n_h, learning_rate=0.01, num_iterations=10000):
    n_x = X.shape[0]
    n_y = Y.shape[0]
    
    parameters = initialize_parameters(n_x, n_h, n_y)
    costs = []
    
    for i in range(num_iterations):
        A2, cache = forward_propagation(X, parameters)
        cost = compute_cost(A2, Y)
        grads = backward_propagation(parameters, cache, X, Y)
        parameters = update_parameters(parameters, grads, learning_rate)
        
        if i % 1000 == 0:
            costs.append(cost)
            print(f"迭代次数 {i}: 损失值 {cost}")
    
    return parameters, costs

4.3 结果可视化

训练完成后,我们可以绘制学习曲线:

plt.plot(costs)
plt.ylabel('损失值')
plt.xlabel('迭代次数(每1000次)')
plt.title(f"学习率 = {learning_rate}")
plt.show()

以及决策边界:

def plot_decision_boundary(X, Y, parameters):
    # 创建网格点
    x_min, x_max = X[0, :].min() - 1, X[0, :].max() + 1
    y_min, y_max = X[1, :].min() - 1, X[1, :].max() + 1
    h = 0.01
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), 
                         np.arange(y_min, y_max, h))
    
    # 预测每个网格点
    Z, _ = forward_propagation(np.c_[xx.ravel(), yy.ravel()].T, parameters)
    Z = Z.reshape(xx.shape)
    
    # 绘制等高线和散点
    plt.contourf(xx, yy, Z, alpha=0.8)
    plt.scatter(X[0, :], X[1, :], c=Y.ravel(), edgecolors='k')
    plt.show()

5. 实战技巧与常见问题

5.1 调试技巧

当网络不收敛时,可以尝试:

  • 梯度检查:比较解析梯度与数值梯度
  • 学习率测试:尝试不同的学习率(0.001, 0.01, 0.1等)
  • 小数据集测试:先在少量数据上过拟合,确保代码正确

5.2 性能优化

  • 向量化:避免Python循环,使用NumPy矩阵运算
  • 内存管理:及时删除大中间变量
  • 提前停止:当验证集误差开始上升时停止训练

5.3 扩展思路

掌握了基础实现后,可以尝试:

  • 添加更多隐藏层实现深度网络
  • 实现不同的激活函数(ReLU, LeakyReLU等)
  • 加入正则化技术(L2, Dropout)
  • 实现更高级的优化器(Adam, RMSprop)

第一次看到自己手写的神经网络在简单的分类任务上达到90%准确率时,那种成就感是看十遍理论推导也无法替代的。建议读者在理解本文代码后,尝试用不同的数据集测试,比如经典的鸢尾花数据集或MNIST数字识别——这才是真正掌握神经网络的开始。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐