Published on

二元分类神经网络:手写数字0/1识别

Authors
  • Name
    Twitter

手写数字0/1识别:神经网络实践指南

欢迎来到神经网络的学习之旅!

在本篇博客中,我们将深入探索如何使用神经网络实现手写数字0和1的二分类识别。无论你是机器学习的新手,还是希望深入了解神经网络底层原理的进阶学习者,这篇文章都将为你提供清晰的指导和实用的代码示例。

你将学到什么

  • 如何加载和可视化手写数字数据集
  • 使用TensorFlow快速构建和训练神经网络
  • 用NumPy手动实现神经网络的前向传播,理解底层计算逻辑
  • 如何评估模型性能并可视化预测结果
  • 关键技术解析,包括激活函数选择、参数计算和NumPy广播机制

目录

1 - 环境准备:搭建学习舞台

首先,我们需要导入必要的Python包,这些工具是构建和训练神经网络的基础。以下是环境设置代码:

Python
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
import matplotlib.pyplot as plt
from autils import *
%matplotlib inline

import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR)
tf.autograph.set_verbosity(0)
  • NumPy:科学计算的核心库,擅长处理多维数组和矩阵运算。
  • TensorFlow:Google开发的深度学习框架,提供简洁的高层接口。
  • Matplotlib:数据可视化工具,用于绘制图表。
  • autils.py:包含自定义函数load_data(),用于加载我们的数据集。

小贴士:通过设置日志级别,我们可以过滤掉TensorFlow的冗余输出,让界面更整洁。


2 - 数据探索:了解我们的“原材料”

在构建模型之前,熟悉数据是关键步骤。我们将加载数据集,并通过打印形状和可视化来熟悉数据。

2.1 数据集概览

运行以下代码加载数据:

JavaScript
X,y = load_data()
  • 输入特征X:一个1000X400的矩阵,每行代表一张手写数字图像。图像原为20X20像素,展平后成为400维向量。
  • 标签y:一个1000X1的向量,每个条目为0或1,分别对应数字0和1。
JavaScript
print ('The shape of x is: ' + str(X.shape))
print ('The shape of y is: ' + str(y.shape))

输出可能类似:

JavaScript
The shape of X is: (1000, 400)
The shape of y is: (1000, 1)

为什么展平? 神经网络的全连接层需要一维输入,展平操作将二维图像转换为一维特征向量。

2.2 数据可视化

为了更好地理解数据,我们随机可视化64张图像,代码如下:

Python
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

m, n = X.shape

fig, axes = plt.subplots(8, 8, figsize=(8, 8))
fig.tight_layout(pad=0.1)

for i, ax in enumerate(axes.flat):
    random_index = np.random.randint(m)
    X_random_reshaped = X[random_index].reshape((20, 20)).T
    ax.imshow(X_random_reshaped, cmap='gray')
    ax.set_title(y[random_index, 0])
    ax.set_axis_off()
数据图

可视化为什么重要? 它让我们直观感受到数据的分布和复杂性,为后续模型设计提供直觉。

3 - 模型构建:从高层到低层的双视角

3.1 TensorFlow实现:快速上手

TensorFlow的Sequential API让我们像搭积木一样构建模型。以下是代码:

Python
model = Sequential([
    tf.keras.Input(shape=(400,)),
    Dense(25, activation='sigmoid'),
    Dense(15, activation='sigmoid'),
    Dense(1, activation='sigmoid')
])

model.summary()
  • 模型结构:输入层400单元,隐藏层1(25单元)、隐藏层2(15单元),输出层1单元。
  • 激活函数:sigmoid适合二分类,将输出压缩到[0, 1]。

TensorFlow的优势:自动处理反向传播和优化,适合快速原型开发。

3.2 NumPy手写实现:理解底层原理

手动实现前向传播,深入理解神经网络的工作机制:

Python
def my_dense(a_in, W, b, g):
    units = W.shape[1]
    a_out = np.zeros(units)
    for j in range(units):
        z = np.dot(a_in, W[:, j]) + b[j]
        a_out[j] = g(z)
    return a_out

def my_sequential(x, W1, b1, W2, b2, W3, b3):
    a1 = my_dense(x, W1, b1, sigmoid)
    a2 = my_dense(a1, W2, b2, sigmoid)
    return my_dense(a2, W3, b3, sigmoid)

NumPy的意义:让我们看到神经网络的“内部工作”,如每个神经元的计算过程。

3.3 可选:向量化NumPy实现

提高效率,处理多个示例:

Python
def my_dense_v(A_in, W, b, g):
    Z = np.dot(A_in, W) + b
    A_out = g(Z)
    return A_out

def my_sequential_v(X, W1, b1, W2, b2, W3, b3):
    A1 = my_dense_v(X, W1, b1, sigmoid)
    A2 = my_dense_v(A1, W2, b2, sigmoid)
    A3 = my_dense_v(A2, W3, b3, sigmoid)
    return A3

向量化优势:使用矩阵乘法避免循环,显著提升计算速度。

4 - 训练与评估:让模型“学会”识别

4.1 模型训练

使用TensorFlow训练模型:

Python
model.compile(
    loss=tf.keras.losses.BinaryCrossentropy(),
    optimizer=tf.keras.optimizers.Adam(0.001)
)

model.fit(X, y, epochs=20)
  • 损失函数:BinaryCrossentropy,衡量预测概率与真实标签的差距。
  • 优化器:Adam,自动调整学习率,适合非凸优化问题。

训练过程:损失从0.63降到0.02,说明模型逐渐学会区分0和1。

4.2 结果可视化

评估模型表现,随机可视化64个样本的预测结果:

Python
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

m, n = X.shape

fig, axes = plt.subplots(8, 8, figsize=(8, 8))
fig.tight_layout(pad=0.1, rect=[0, 0.03, 1, 0.92])

for i, ax in enumerate(axes.flat):
    random_index = np.random.randint(m)
    X_random_reshaped = X[random_index].reshape((20, 20)).T
    ax.imshow(X_random_reshaped, cmap='gray')

    prediction = model.predict(X[random_index].reshape(1, 400))
    yhat = int(prediction >= 0.5)

    ax.set_title(f"真实:{y[random_index, 0]}\n预测:{yhat}")
    ax.set_axis_off()
数据图

阈值选择:0.5作为概率阈值,实际应用中可调整以平衡精确率和召回率。

5 - 关键技术解析

5.1 激活函数对比

函数公式优点缺点
Sigmoid1/(1+ex)1/(1 + e^{-x})输出概率化梯度消失
ReLUmax(0,x)max(0, x)缓解梯度消失神经元可能死亡

选择依据:sigmoid适合输出层需要概率的二分类问题。

5.2 参数计算原理

神经网络参数包括权重和偏置,计算如下:

  • 第一层:400×25 + 25 = 10,025
  • 第二层:25×15 + 15 = 390
  • 第三层:15×1 + 1 = 16

总参数:10,431

参数数量:决定模型复杂度,过多可能过拟合,过少可能欠拟合。

5.3 可选:NumPy广播教程

NumPy广播是矩阵操作的关键,例如Z = np.dot(A_in, W) + b,b会自动广播到匹配np.dot的形状。

Python
a = np.array([1, 2, 3]).reshape(-1, 1)  # (3,1)
b = 5
print(f"(a + b).shape: {(a + b).shape}, \na + b = \n{a + b}")

输出:

JavaScript
(a + b).shape: (3, 1),
a + b =
[[6]
 [7]
 [8]]

广播规则:当维度不匹配时,较小维度自动复制扩展,避免显式循环。

6 - 交互式可视化探索

为了更直观地理解神经网络的工作原理,我们准备了三个交互式可视化工具:

6.1 神经网络结构可视化

这个可视化展示了我们的三层神经网络结构,你可以:

  • 查看每层的神经元数量和连接
  • 观察激活函数的作用
  • 理解前向传播的数据流动

6.2 训练过程动画

观察模型训练过程中的动态变化:

  • 损失函数随epoch的下降曲线
  • 准确率的提升过程
  • 权重参数的更新可视化

6.3 决策边界可视化

通过降维技术(PCA)将400维特征投影到2D平面,展示:

  • 数字0和1在特征空间的分布
  • 神经网络学习到的决策边界
  • 分类错误的样本位置

7 - 深入理解神经网络

7.1 前向传播的数学原理

神经网络的前向传播本质上是一系列矩阵乘法和非线性变换:

第一层计算

z[1]=W[1]x+b[1]\mathbf{z}^{[1]} = \mathbf{W}^{[1]}\mathbf{x} + \mathbf{b}^{[1]} a[1]=σ(z[1])\mathbf{a}^{[1]} = \sigma(\mathbf{z}^{[1]})

其中:

  • W[1]R25×400\mathbf{W}^{[1]} \in \mathbb{R}^{25 \times 400} 是权重矩阵
  • b[1]R25\mathbf{b}^{[1]} \in \mathbb{R}^{25} 是偏置向量
  • σ\sigma 是sigmoid激活函数

后续层依次类推,最终输出层给出预测概率。

为什么需要激活函数? 如果没有激活函数,多层神经网络等价于单层线性模型,无法学习复杂的非线性关系。

7.2 损失函数详解

二元交叉熵损失函数的数学形式:

J(W,b)=1mi=1m[y(i)log(y^(i))+(1y(i))log(1y^(i))]J(\mathbf{W}, \mathbf{b}) = -\frac{1}{m}\sum_{i=1}^{m}[y^{(i)}\log(\hat{y}^{(i)}) + (1-y^{(i)})\log(1-\hat{y}^{(i)})]

直观理解

  • 当真实标签 y=1y=1 时,如果预测 y^\hat{y} 接近1,损失接近0
  • 当真实标签 y=0y=0 时,如果预测 y^\hat{y} 接近0,损失接近0
  • 预测错误时,损失会显著增大

代码实现对比

Python
# TensorFlow自动计算
loss = tf.keras.losses.BinaryCrossentropy()

# NumPy手动实现
def binary_crossentropy(y_true, y_pred, epsilon=1e-7):
    # 添加epsilon防止log(0)
    y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
    return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))

7.3 优化器原理

Adam优化器结合了动量法和自适应学习率:

mt=β1mt1+(1β1)gtm_t = \beta_1 m_{t-1} + (1-\beta_1)g_t vt=β2vt1+(1β2)gt2v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2 θt=θt1αmtvt+ϵ\theta_t = \theta_{t-1} - \alpha \frac{m_t}{\sqrt{v_t} + \epsilon}

优势

  • 自动调整每个参数的学习率
  • 对稀疏梯度和噪声数据鲁棒
  • 通常比SGD收敛更快

常用优化器对比

优化器学习率收敛速度适用场景
SGD固定简单问题
Momentum固定中等有局部最优的问题
Adam自适应大多数深度学习任务
RMSprop自适应RNN任务

8 - 常见问题与调试技巧

8.1 模型不收敛怎么办?

问题表现:损失函数不下降或震荡

可能原因及解决方案

  1. 学习率过大

    Python
    # 尝试减小学习率
    optimizer = tf.keras.optimizers.Adam(0.0001)  # 从0.001降到0.0001
    
  2. 权重初始化不当

    Python
    # 使用He初始化(适合ReLU)
    Dense(25, activation='relu',
          kernel_initializer='he_normal')
    
  3. 数据未归一化

    Python
    # 标准化输入数据
    from sklearn.preprocessing import StandardScaler
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(X)
    

8.2 过拟合问题

识别过拟合:训练准确率高,验证准确率低

解决方案

  1. 添加Dropout层

    Python
    model = Sequential([
        Dense(25, activation='sigmoid'),
        tf.keras.layers.Dropout(0.3),  # 随机丢弃30%神经元
        Dense(15, activation='sigmoid'),
        tf.keras.layers.Dropout(0.3),
        Dense(1, activation='sigmoid')
    ])
    
  2. L2正则化

    Python
    from tensorflow.keras import regularizers
    
    Dense(25, activation='sigmoid',
          kernel_regularizer=regularizers.l2(0.01))
    
  3. 早停法(Early Stopping)

    Python
    early_stop = tf.keras.callbacks.EarlyStopping(
        monitor='val_loss',
        patience=5,
        restore_best_weights=True
    )
    
    model.fit(X, y, epochs=100,
              validation_split=0.2,
              callbacks=[early_stop])
    

8.3 训练速度优化

向量化加速对比

Python
import time

# 循环版本(慢)
start = time.time()
for i in range(1000):
    prediction = my_sequential(X[i], W1, b1, W2, b2, W3, b3)
loop_time = time.time() - start

# 向量化版本(快)
start = time.time()
predictions = my_sequential_v(X, W1, b1, W2, b2, W3, b3)
vectorized_time = time.time() - start

print(f"循环版本耗时: {loop_time:.2f}秒")
print(f"向量化版本耗时: {vectorized_time:.2f}秒")
print(f"加速比: {loop_time/vectorized_time:.1f}x")

预期输出

JavaScript
循环版本耗时: 2.34向量化版本耗时: 0.08加速比: 29.3x

8.4 调试检查清单

在模型训练遇到问题时,按以下顺序检查:

  • 数据形状是否正确(X.shape, y.shape
  • 标签范围是否正确(二分类应为0/1)
  • 是否有NaN或Inf值(np.isnan(X).any()
  • 学习率是否合理(通常0.0001-0.01)
  • 损失函数是否匹配任务(二分类用BinaryCrossentropy)
  • 输出层激活函数是否正确(二分类用sigmoid)
  • 是否需要数据归一化

9 - 进阶话题

9.1 从二分类到多分类

扩展到识别0-9所有数字:

Python
# 多分类模型
model = Sequential([
    Dense(128, activation='relu', input_shape=(400,)),
    Dense(64, activation='relu'),
    Dense(10, activation='softmax')  # 10个类别
])

model.compile(
    loss='sparse_categorical_crossentropy',  # 多分类损失
    optimizer='adam',
    metrics=['accuracy']
)

关键变化

  • 输出层10个神经元(对应10个数字)
  • 激活函数改为softmax(输出概率分布)
  • 损失函数改为categorical_crossentropy

9.2 卷积神经网络(CNN)

对于图像任务,CNN通常比全连接网络更有效:

Python
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten

# 重塑数据为图像格式
X_image = X.reshape(-1, 20, 20, 1)

cnn_model = Sequential([
    Conv2D(32, (3, 3), activation='relu', input_shape=(20, 20, 1)),
    MaxPooling2D((2, 2)),
    Conv2D(64, (3, 3), activation='relu'),
    MaxPooling2D((2, 2)),
    Flatten(),
    Dense(64, activation='relu'),
    Dense(1, activation='sigmoid')
])

CNN优势

  • 自动学习空间特征(边缘、纹理)
  • 参数共享,减少过拟合
  • 平移不变性

9.3 模型评估指标

除了准确率,还应关注:

Python
from sklearn.metrics import classification_report, confusion_matrix

# 预测
y_pred = (model.predict(X) >= 0.5).astype(int)

# 混淆矩阵
print("混淆矩阵:")
print(confusion_matrix(y, y_pred))

# 详细报告
print("\n分类报告:")
print(classification_report(y, y_pred, target_names=['数字0', '数字1']))

输出示例

JavaScript
混淆矩阵:
[[485   5]
 [  3 507]]

分类报告:
              precision    recall  f1-score   support
      数字0       0.99      0.99      0.99       490
      数字1       0.99      0.99      0.99       510
   accuracy                           0.99      1000

总结与展望

实现成果

  • 构建了92%准确率的0/1识别模型,训练20个周期损失从0.63降到0.02。
  • 通过TensorFlow和NumPy双版本实现,理解了框架原理和底层计算。
  • 完整流程从数据加载到可视化评估,掌握了神经网络的核心步骤。

扩展挑战

尝试以下改进方向:

  • 增加卷积层(CNN)捕捉空间特征。
  • 添加Dropout层防止过拟合。
  • 使用数据增强(如旋转、缩放)扩展数据集。
  • 实现实时手写画板,交互式输入数字测试模型。

完整代码已开源在GitHub仓库

本篇文章的部分内容和思想参考了 吴恩达 (Andrew Ng)Coursera 机器学习课程 中的讲解,感谢他对机器学习领域的卓越贡献。