当前位置:首页>python>一天一个Python知识点——Day 155:神经网络基础

一天一个Python知识点——Day 155:神经网络基础

  • 2026-10-11 17:06:47
一天一个Python知识点——Day 155:神经网络基础

一、开篇:你早已会用的“黑箱”,今天拆开看看

过去三天,你学会了用TensorFlow、PyTorch、Keras搭建模型。你写下:

model.add(Dense(128, activation='relu'))

然后模型就学会了。

但你是否曾在深夜问过自己:

  • 那一层 Dense(128) 究竟对数据做了什么?

  • activation='relu' 为什么非得是“非线性”?

  • 梯度到底是怎么“反向传播”的?

  • 为什么我的模型训练着训练着就“死了”?

今天,我们不写框架。 我们用最笨的办法——纯Python + 三层循环,手撕一个神经网络。

你会发现:神经网络不是魔法,是数学;不是黑箱,是四则运算。

从感知机到多层网络——历史的顿悟时刻

1957年:一个叫罗森布拉特的人点燃了引信

感知机(Perceptron)——人类历史上第一个有“学习”能力的机器。

它的数学表达式简单得令人敬畏:

output = sign(w·x + b)

其中:

  • w 是权重,x 是输入,b 是偏置

  • sign 是阶跃函数:大于0输出1,小于0输出-1 

它能做什么? 二分类。给定一组猫和狗的图片特征,它画一条直线,把两类分开。

它不能做什么? 任何“非线性”的事情。最著名的反例:XOR异或问题 。

(0,0)→0, (0,1)→1, (1,0)→1, (1,1)→0  —— 没有一条直线能分开这四个点!

1969年,明斯基在《感知机》一书中严厉指出这个缺陷。第一次AI寒冬,就此降临。

历史的讽刺在于:解决XOR的方法其实就在感知机里——堆两层。但那个年代,没人想到。

拯救者:多层感知机(MLP)

多层感知机 = 输入层 + 隐藏层 + 输出层

结构简单得令人难以置信:

输入 → 线性变换 → 激活函数 → 线性变换 → 激活函数 → 输出

但它解决了XOR :

  • 第一层:把二维输入映射到二维隐藏空间

  • 第二层:在隐藏空间里做线性分类

原来如此! 所谓“深度学习”,不过是在“线性变换 + 非线性挤压”这个配方上,重复足够多次。

神经元拆解——一台会学习的计算器

一个神经元在做什么?

z = w₁x₁ + w₂x₂ + ... + wₙxₙ + ba = σ(z)

四则运算,仅此而已。

  • 加权求和:输入乘权重,加偏置

  • 激活函数:把线性结果“拧”成非线性

这不是魔法,是你的Python代码里跑过的无数次 np.dot(w, x) + b。

2.2 矩阵乘法:神经网络的核心秘密

假设:

  • 输入批次:X,形状 (batch_size, n_features)

  • 权重矩阵:W,形状 (n_neurons, n_features)

  • 偏置:b,形状 (n_neurons,)

这一层计算的就是:

H = X @ W.T + b

为什么用矩阵乘法? ——因为快。

让我们亲手验证:三层循环 vs PyTorch

import torchimport numpy as npdef matmul_naive(A, B):    """三层循环的矩阵乘法——每个Python程序员第一次写出的版本"""    m, n = A.shape    n, p = B.shape    C = torch.zeros(m, p)    for i in range(m):        for j in range(p):            for k in range(n):                C[i, j] += A[i, k] * B[k, j]    return Cdef matmul_better(A, B):    """去掉最内层循环——让PyTorch用C语言帮你做点积"""    m, n = A.shape    n, p = B.shape    C = torch.zeros(m, p)    for i in range(m):        for j in range(p):            C[i, j] = (A[i, :] * B[:, j]).sum()    return Cdef matmul_broadcast(A, B):    """利用广播——同时计算整行与矩阵的乘积"""    m, n = A.shape    n, p = B.shape    C = torch.zeros(m, p)    for i in range(m):        C[i, :] = (A[i, :].unsqueeze(1) * B).sum(dim=0)    return C# 测试:5张MNIST图片,展平成784维,映射到10个类别X = torch.randn(5, 784)W = torch.randn(10, 784)%time C1 = matmul_naive(X, W.T)%time C2 = matmul_better(X, W.T)%time C3 = matmul_broadcast(X, W.T)%time C4 = X @ W.T  # PyTorch的矩阵乘法

实验结果(基于真实测试):

  • 三层循环:1.7秒

  • 去掉内层循环:3.4毫秒(快500倍)

  • 广播优化:0.6毫秒(快2800倍)

  • PyTorch原生:0.01毫秒(快170,000倍)

结论: 向量化不是技巧,是神经网络能在现代硬件上运行的唯一原因。

没有激活函数,堆再多层也是白堆

一个令人震惊的数学事实

假设我们堆叠两层线性层(无激活函数):

H = X @ W1 + b1O = H @ W2 + b2
展开它:
O = (X @ W1 + b1) @ W2 + b2  = X @ (W1 @ W2) + (b1 @ W2 + b2)

奇迹发生了 :

两个线性层的复合,仍然是线性层!
无论你堆叠多少层,整个网络等价于单层感知机。

这就是为什么1957-1986年间,人们以为神经网络“只能做线性分类”。

激活函数:打破线性诅咒的关键

激活函数的作用只有一个:引入非线性。

它是压模机,把直线压成曲线,把平面压成曲面,把线性空间扭曲成可以任意分割的非线性流形。

四大激活函数:谁是谁的替身?

Sigmoid —— 历史的功臣

σ(x) = 1 / (1 + e^{-x})输出范围:(0, 1)

优点:平滑、可导、输出可解释为概率
缺点:梯度消失——两端导数趋近于0,深层网络无法学习 
今日角色:二分类输出层 

Tanh —— Sigmoid的升级版

tanh(x) = (e^x - e^{-x}) / (e^x + e^{-x})输出范围:(-1, 1)

改进:零中心化,梯度更强([-1,1]内导数0.42~1,Sigmoid仅0.2~0.25)
遗留问题:依然会梯度消失

ReLU —— 现代深度学习的基石

ReLU(x) = max(0, x)

革命性优势 :

  • 计算极快(没有指数运算)

  • 正区间梯度恒为1,无梯度消失

  • 稀疏激活(约50%神经元输出0)

代价:神经元坏死

Leaky ReLU / PReLU / ELU —— ReLU的救火队

LeakyReLU(x) = max(αx, x), α通常取0.01
设计思想:负区间不给0,给一点点坡度,让“死神经元”有机会复活 
激活函数可视化实验
import numpy as npimport matplotlib.pyplot as pltx = np.linspace(-5, 5, 200)activations = {    'Sigmoid': lambda x: 1/(1+np.exp(-x)),    'Tanh': lambda x: np.tanh(x),    'ReLU': lambda x: np.maximum(0, x),    'Leaky ReLU': lambda x: np.where(x>0, x, 0.01*x)}fig, axes = plt.subplots(2, 2, figsize=(10, 8))for ax, (name, func) in zip(axes.flat, activations.items()):    y = func(x)    ax.plot(x, y, linewidth=2)    ax.axhline(y=0, color='k', linestyle=':', alpha=0.5)    ax.axvline(x=0, color='k', linestyle=':', alpha=0.5)    ax.set_title(f'{name}', fontweight='bold')    ax.grid(alpha=0.3)plt.suptitle('激活函数家族:线性世界的扭曲器', fontsize=14, fontweight='bold')plt.tight_layout()plt.show()

反向传播——链式法则的艺术

核心思想:把“误差”传回去

神经网络训练 = 参数搜索
我们要找到一组(w, b),让损失函数最小。

梯度的含义:参数往哪个方向调,损失会下降。

反向传播的本质 :

从输出层开始,沿着计算图反向走,每经过一个节点,就用链式法则乘上这个节点的局部梯度。

一个具体例子:手动推导

假设这个最简单的网络:

输入 x → 线性层 z = wx + b → 激活 a = σ(z) → 损失 L = ½(a - y)²
前向传播(你已经很熟了):
z = w*x + ba = σ(z)L = 0.5*(a - y)^2

反向传播(从后往前推):

第1步:输出层的梯度

dL/da = a - y
第2步:激活函数的梯度
da/dz = σ(z) * (1 - σ(z))   # Sigmoid的导数
第3步:链式法则——dL/dz
dL/dz = (dL/da) * (da/dz) = (a - y) * σ(z)*(1-σ(z))
第4步:参数的梯度
dL/dw = dL/dz * dz/dw = dL/dz * xdL/db = dL/dz * dz/db = dL/dz * 1
这就是梯度下降的更新公式!
w ← w - η * dL/dwb ← b - η * dL/db

从零实现:一个神经元的反向传播

import numpy as npclass Neuron:    def __init__(self, n_inputs):        self.w = np.random.randn(n_inputs) * 0.01        self.b = 0.0    def sigmoid(self, z):        return 1 / (1 + np.exp(-z))    def forward(self, x):        """前向:记住中间结果用于反向"""        self.x = x  # 缓存输入        self.z = np.dot(self.w, x) + self.b        self.a = self.sigmoid(self.z)        return self.a    def backward(self, dL_da):        """反向:链式法则"""        da_dz = self.a * (1 - self.a)      # sigmoid导数        dL_dz = dL_da * da_dz        dL_dw = dL_dz * self.x             # 权重梯度        dL_db = dL_dz * 1                  # 偏置梯度        # 如果需要传播给前一层        dL_dx = dL_dz * self.w        return dL_dw, dL_db, dL_dx    def update(self, dL_dw, dL_db, lr=0.1):        self.w -= lr * dL_dw        self.b -= lr * dL_db# 训练一个神经元np.random.seed(42)neuron = Neuron(3)# 假数据x = np.array([0.5, -0.2, 0.8])y_true = 1.0# 前向y_pred = neuron.forward(x)loss = 0.5 * (y_pred - y_true) ** 2# 反向dL_da = y_pred - y_truedL_dw, dL_db, _ = neuron.backward(dL_da)# 更新neuron.update(dL_dw, dL_db, lr=0.5)print(f"更新后权重: {neuron.w}")print(f"更新后偏置: {neuron.b:.4f}")
这就是PyTorch的 .backward() 在你调用时,背后发生的全部真相。

训练陷阱——为什么模型会“死”

梯度消失(Vanishing Gradients)

症状:靠近输入层的参数几乎不更新,深层网络训练不动。

原因:Sigmoid/Tanh在两端导数趋近0。链式法则连乘,层数越多,梯度指数级衰减 。

解药:ReLU。它在正区间梯度恒为1,梯度消失被终结。

神经元坏死(Dead Neurons)

症状:某些神经元永远输出0,梯度永远0,权重永不更新 。

数学本质:

  • 若对所有训练样本,z = w·x + b ≤ 0

  • 则ReLU输出恒为0

  • 反向时,dL/dw = dL/dz * x,其中 dL/dz = 0

  • 该神经元从网络中被物理移除

解药:

  • 使用Leaky ReLU / PReLU / ELU

  • 更小的学习率

  • 更谨慎的权重初始化

对称性破缺:为什么权重不能全零初始化

错误做法:

# 这是自杀w = np.zeros((n_inputs, n_neurons))b = np.zeros(n_neurons)

后果:同一层的所有神经元完全相同,前向输出相同,反向梯度相同,多个神经元等于一个神经元 。

正确做法:

  • 权重随机小值初始化(打破对称性)

  • 偏置可以初始化为0(它不乘输入,不影响对称性)

  • 现代推荐:He初始化(ReLU专用)或Xavier初始化

# He初始化(ReLU家族推荐)w = np.random.randn(n_inputs, n_neurons) * np.sqrt(2.0 / n_inputs)# Xavier初始化(Sigmoid/Tanh推荐)w = np.random.randn(n_inputs, n_neurons) * np.sqrt(1.0 / n_inputs)

从神经元到神经网络——全连接层代码实战

import numpy as npclass Layer_Dense:    """全连接层:神经网络的基本积木"""    def __init__(self, n_inputs, n_neurons, activation='relu'):        # He初始化(ReLU专用)        self.weights = np.random.randn(n_inputs, n_neurons) * np.sqrt(2.0 / n_inputs)        self.biases = np.zeros(n_neurons)        self.activation_name = activation    def forward(self, inputs):        self.inputs = inputs  # 缓存用于反向传播        self.z = np.dot(inputs, self.weights) + self.biases        # 激活函数        if self.activation_name == 'relu':            self.output = np.maximum(0, self.z)        elif self.activation_name == 'sigmoid':            self.output = 1 / (1 + np.exp(-self.z))        elif self.activation_name == 'linear':            self.output = self.z        return self.output    def backward(self, dvalues):        """反向传播:计算梯度"""        self.dz = dvalues.copy()        # 激活函数的梯度        if self.activation_name == 'relu':            self.dz[self.z <= 0] = 0        elif self.activation_name == 'sigmoid':            sig = self.output            self.dz = self.dz * (sig * (1 - sig))        # 参数梯度        self.dweights = np.dot(self.inputs.T, self.dz)        self.dbiases = np.sum(self.dz, axis=0, keepdims=True).flatten()        # 传播给前一层的梯度        self.dinputs = np.dot(self.dz, self.weights.T)        return self.dinputs# 构建一个两层的神经网络class TwoLayerNet:    def __init__(self, input_size, hidden_size, output_size):        self.layer1 = Layer_Dense(input_size, hidden_size, 'relu')        self.layer2 = Layer_Dense(hidden_size, output_size, 'sigmoid')    def forward(self, X):        self.hidden = self.layer1.forward(X)        self.output = self.layer2.forward(self.hidden)        return self.output    def backward(self, dloss):        d2 = self.layer2.backward(dloss)        d1 = self.layer1.backward(d2)    def update(self, lr=0.01):        self.layer2.weights -= lr * self.layer2.dweights        self.layer2.biases -= lr * self.layer2.dbiases        self.layer1.weights -= lr * self.layer1.dweights        self.layer1.biases -= lr * self.layer1.dbiasesprint("全连接层实现完成!")print("你现在拥有了一个可以解决XOR问题的神经网络")

各位学习者,你还记得第149天,我们在做特征工程;第150天,训练第一个模型;第151-154天,驾驭三大框架。

今天,你走完了“神经网络”认知的最后一步。以后你写 model.add(Dense(512, activation='relu')) 时,你会看到:

  • 那不是魔法,是 X @ W.T + b

  • 那不是线性,是 max(0, z)

  • 那不是黑箱,是链式法则的千万次优雅传递

你知道梯度从何而来,去向何方。
你知道神经元何时会死,如何让它复活。
你知道为什么堆叠层数能逼近任意函数,也知道为什么这只是“可能”而非“必然”。

你已经不是框架的使用者,而是懂原理的工程师。

最新文章

随机文章