从房价预测到图像识别:用Python和NumPy手搓你的第一个神经网络(附代码)

在咖啡厅里第一次听说"神经网络"这个词时,我盯着朋友电脑屏幕上那些错综复杂的线条看了半天——它们看起来就像是一团被猫咪抓乱的毛线。当时我怎么也想不到,短短三个月后,我就能用不到100行Python代码实现一个能识别手写数字的神经网络。这就像突然发现原来魔术师的道具箱里装的不过是些镜子和小机关,那种"原来如此"的顿悟感,正是我想通过这篇文章带给你的体验。

我们将从最基础的房价预测模型开始,逐步构建一个能识别MNIST手写数字的神经网络。不用担心复杂的数学公式,我会用厨房里的调料瓶和乐高积木来比喻那些听起来高大上的概念。最重要的是,所有代码你都可以在Jupyter Notebook里逐行运行,看到每个变量是如何流动变化的——这才是真正理解神经网络的最佳方式。

1. 准备你的数字工具箱

1.1 最小化开发环境配置

你只需要以下三样工具就能开始我们的神经网络之旅:

# 基础环境检查清单
import sys
import numpy as np
print(f"Python版本: {sys.version.split()[0]}")
print(f"NumPy版本: {np.__version__}")

如果看到版本号输出(Python 3.6+,NumPy 1.19+),说明你的环境已经准备好了。如果没有安装NumPy,用这条命令快速安装:

pip install numpy matplotlib

提示:推荐使用Jupyter Notebook进行交互式实验,可以实时观察每个单元格的变量变化

1.2 理解神经网络的"乐高积木"

想象你在教一个三岁小朋友认识动物。你会先展示各种猫的图片,然后说"这是猫"——这就是监督学习的本质。神经网络的核心构件其实只有三种:

  1. 神经元:像一个小计算器,接收输入并产生输出
  2. 权重:决定每个输入的重要程度(就像选择听妈妈的话还是朋友的建议)
  3. 激活函数:给计算结果加点"个性",让网络能学习复杂模式

让我们用房价预测的例子具体化这些概念。假设决定房价的因素只有房屋面积(平方米)和卧室数量:

特征权重说明
房屋面积0.8每平米对价格的影响
卧室数量0.2每个卧室的附加价值

这个表格中的权重就是神经网络需要自动学习的核心参数。

2. 从零实现单神经元网络

2.1 房价预测的NumPy实现

我们先实现一个最简单的"神经元"——它实际上就是一个线性回归模型:

class SingleNeuron:
    def __init__(self, input_size):
        self.weights = np.random.randn(input_size)
        self.bias = np.random.randn()
    
    def relu(self, x):
        return np.maximum(0, x)
    
    def forward(self, x):
        return self.relu(np.dot(x, self.weights) + self.bias)

# 使用示例
neuron = SingleNeuron(2)  # 两个输入特征
house_features = np.array([120, 3])  # 120平米,3个卧室
predicted_price = neuron.forward(house_features)
print(f"预测房价: {predicted_price:.2f}万元")

这段代码中的relu函数就是著名的ReLU激活函数,它让我们的神经元具备了非线性处理能力。试想如果没有这个函数,无论多少层的神经网络都只能表示线性关系——就像无论用多少张透明纸叠加,你最终只能得到另一种颜色的直线。

2.2 训练这个小神经元

训练过程本质上是不断调整权重和偏差,让预测结果越来越接近真实值。这里有一个直观的类比:假设你在调节淋浴的水温:

  1. 先随便拧开热水和冷水龙头(随机初始化权重)
  2. 用手试水温(计算预测误差)
  3. 根据感觉调整两个龙头的角度(梯度下降)
  4. 重复直到水温刚好合适

用代码实现这个调节过程:

def train(neuron, X, y, epochs=100, lr=0.01):
    for _ in range(epochs):
        # 前向传播
        predictions = np.array([neuron.forward(x) for x in X])
        # 计算误差(均方误差)
        error = predictions - y
        # 反向传播(计算梯度)
        grad_w = np.dot(X.T, error) / len(X)
        grad_b = np.mean(error)
        # 更新参数
        neuron.weights -= lr * grad_w
        neuron.bias -= lr * grad_b

# 模拟数据:[[面积, 卧室数], ...]
X_train = np.array([[80,2], [95,3], [110,3], [150,4]])
y_train = np.array([320, 380, 420, 580])  # 真实价格(万元)
neuron = SingleNeuron(2)
train(neuron, X_train, y_train)

运行后你会发现这个简单神经元已经能根据房屋特征给出合理的价格预测了。虽然不如专业评估师准确,但核心逻辑与复杂神经网络完全一致。

3. 升级到多层神经网络

3.1 组装神经网络"乐高"

单个神经元能力有限,就像一个人无法完成交响乐演奏。我们需要将多个神经元分层组合:

  1. 输入层:接收原始数据(如图像像素、房屋特征)
  2. 隐藏层:中间处理层,通常有多层
  3. 输出层:生成最终结果(如分类概率、预测价格)

用Python实现一个两层的神经网络:

class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size):
        # 第一层参数
        self.w1 = np.random.randn(input_size, hidden_size)
        self.b1 = np.random.randn(hidden_size)
        # 输出层参数
        self.w2 = np.random.randn(hidden_size, output_size)
        self.b2 = np.random.randn(output_size)
    
    def relu(self, x):
        return np.maximum(0, x)
    
    def forward(self, x):
        # 第一层计算
        h = self.relu(np.dot(x, self.w1) + self.b1)
        # 输出层计算
        return np.dot(h, self.w2) + self.b2

3.2 训练技巧与调试

训练深层网络时最常见的问题是梯度消失或爆炸。想象你在教一群小朋友传话:

  • 如果每个人说话声音太小(梯度消失),传到后面就听不见了
  • 如果某个人突然大喊(梯度爆炸),后面的小朋友会被吓到

解决方法包括:

  1. 参数初始化:使用Xavier或He初始化,避免初始权重过大或过小
  2. 学习率调整:训练初期用较大学习率,后期逐步减小
  3. 批量归一化:对每层的输入进行标准化处理

改进后的训练代码:

def train(net, X, y, epochs=1000, batch_size=32, lr=0.01):
    for epoch in range(epochs):
        # 随机选择一批数据
        indices = np.random.choice(len(X), batch_size)
        X_batch, y_batch = X[indices], y[indices]
        
        # 前向传播
        h = net.relu(np.dot(X_batch, net.w1) + net.b1)
        output = np.dot(h, net.w2) + net.b2
        
        # 计算梯度
        error = output - y_batch
        grad_w2 = np.dot(h.T, error) / batch_size
        grad_b2 = np.mean(error, axis=0)
        
        hidden_error = np.dot(error, net.w2.T) * (h > 0)
        grad_w1 = np.dot(X_batch.T, hidden_error) / batch_size
        grad_b1 = np.mean(hidden_error, axis=0)
        
        # 更新参数
        net.w2 -= lr * grad_w2
        net.b2 -= lr * grad_b2
        net.w1 -= lr * grad_w1
        net.b1 -= lr * grad_b1

4. 挑战MNIST手写数字识别

4.1 准备图像数据

MNIST数据集包含70,000张28x28像素的手写数字图片。我们可以用以下代码加载并预处理数据:

from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split

# 加载数据
mnist = fetch_openml('mnist_784', version=1)
X, y = mnist["data"], mnist["target"].astype(np.uint8)

# 归一化到0-1范围
X = X / 255.0

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# 将标签转为one-hot编码
def to_one_hot(y, num_classes=10):
    return np.eye(num_classes)[y]

y_train_onehot = to_one_hot(y_train)
y_test_onehot = to_one_hot(y_test)

4.2 构建并训练网络

现在我们可以构建一个适合图像分类的神经网络:

# 网络结构:784输入 -> 128隐藏层 -> 10输出(对应0-9数字)
net = TwoLayerNet(input_size=784, hidden_size=128, output_size=10)

# 训练参数
def train_mnist(net, X_train, y_train, epochs=20, batch_size=64, lr=0.1):
    for epoch in range(epochs):
        # 随机打乱数据
        indices = np.random.permutation(len(X_train))
        X_shuffled, y_shuffled = X_train[indices], y_train[indices]
        
        # 分批训练
        for i in range(0, len(X_train), batch_size):
            X_batch = X_shuffled[i:i+batch_size]
            y_batch = y_shuffled[i:i+batch_size]
            
            # 前向传播
            h = net.relu(np.dot(X_batch, net.w1) + net.b1)
            output = np.dot(h, net.w2) + net.b2
            
            # 计算softmax和交叉熵损失
            exp_scores = np.exp(output - np.max(output, axis=1, keepdims=True))
            probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
            
            # 反向传播
            error = probs - y_batch
            grad_w2 = np.dot(h.T, error) / batch_size
            grad_b2 = np.mean(error, axis=0)
            
            hidden_error = np.dot(error, net.w2.T) * (h > 0)
            grad_w1 = np.dot(X_batch.T, hidden_error) / batch_size
            grad_b1 = np.mean(hidden_error, axis=0)
            
            # 更新参数
            net.w2 -= lr * grad_w2
            net.b2 -= lr * grad_b2
            net.w1 -= lr * grad_w1
            net.b1 -= lr * grad_b1
        
        # 每个epoch后评估准确率
        h = net.relu(np.dot(X_test, net.w1) + net.b1)
        test_output = np.dot(h, net.w2) + net.b2
        test_preds = np.argmax(test_output, axis=1)
        accuracy = np.mean(test_preds == y_test)
        print(f"Epoch {epoch+1}, 测试准确率: {accuracy:.3f}")

# 开始训练
train_mnist(net, X_train, y_train_onehot)

经过20轮训练后,你应该能看到测试准确率达到约85-90%。虽然不如现代深度学习模型的99%+准确率,但对于完全从零实现的神经网络来说已经相当不错了!

4.3 可视化网络决策

理解神经网络如何做出判断同样重要。我们可以可视化第一层权重,看看网络学到了什么:

import matplotlib.pyplot as plt

# 显示第一层权重
fig, axes = plt.subplots(8, 16, figsize=(16, 8))
for i, ax in enumerate(axes.flat):
    if i < 128:  # 我们的隐藏层有128个神经元
        ax.imshow(net.w1[:, i].reshape(28, 28), cmap='gray')
    ax.axis('off')
plt.show()

这些可视化结果看起来像是各种数字部件的"探测器",有的对曲线敏感,有的对直线敏感。这正是神经网络分层提取特征的直观体现——底层神经元检测边缘和笔画,高层神经元组合这些特征识别完整数字。

更多推荐