0%

深度学习入门:神经网络中的数学基础

《Python深度学习》笔记

前言

说起大模型,我们都知道它是通过海量数据”训练”出来的,但很少有人了解”训练”二字背后究竟发生了什么。

所谓”训练”,本质上是一个深度学习的过程。而实现这一过程的载体,是一种称为神经网络的模型结构——它由若干层级层叠加而成,数据在各层之间逐步传递、转化,最终从数据中学习到数据的规律与特征表示。

我们先来看一个神经网络的具体实例:MNIST 手写数字识别,该例子使用Python的Keras库实现。

如何快速训练一个神经网络

下面是用 Keras 训练一个识别手写数字(0–9)的神经网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from tensorflow.keras.datasets import mnist
from tensorflow import keras
from tensorflow.keras import layers

# ── 1. 加载数据 ────────────────────────────────────────────────────
# MNIST 数据集包含 6 万张训练图像和 1 万张测试图像,每张图像是 28×28 像素的灰度图,表示手写数字 0–9。
# train_images.shape = (60000, 28, 28) # 6 万张 28×28 像素灰度图
# train_labels.shape = (60000,) # 每张图对应的数字 0–9
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

# ── 2. 定义神经网络架构 ────────────────────────────────────────────────
# 包含2个Dense层,都是密集连接(也叫全连接)的神经层
# 第2层是一个10路softmax分类层,它将返回一个由10个概率值(总和为1)组成的数组。每个概率值表示当前数字图像属于10个数字类别中某一个的概率。
model = keras.Sequential([
layers.Dense(512, activation='relu'),
layers.Dense(10, activation='softmax')
])

# ── 3. 编译(指定优化器、损失函数、评估指标)────────────────────────
model.compile(
optimizer='rmsprop',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)

# ── 4. 预处理 ─────────────────────────────────────────────────────
train_images = train_images.reshape((60000, 28 * 28)).astype('float32') / 255
test_images = test_images.reshape((10000, 28 * 28)).astype('float32') / 255

# ── 5. 训练 ───────────────────────────────────────────────────────
model.fit(train_images, train_labels, epochs=5, batch_size=128)

# ── 6. 评估 ───────────────────────────────────────────────────────
test_loss, test_acc = model.evaluate(test_images, test_labels)
print(f"测试准确率: {test_acc:.4f}") # 约 0.9780

寥寥数行代码,我们得到了一个训练好的数字识别模型,准确率约 97.8%。但是,模型训练的背后到底做了什么呢?下面我们来详细了解。

深度学习中的数据基础

神经网络的数据表示

神经网络处理的一切数据都是张量(Tensor),张量同时也可以理解为多维数组:

张量的阶(ndim)

阶 名称 形状示例 典型用途
0 标量 () 单个值
1 向量 (512,) 一个样本的特征
2 矩阵 (60000, 784) 向量数据:形状为(samples, features)的2阶张量,每个样本都是一个数值(​“特征”​)向量
3 3阶 张量 (60000, 28, 28) 时间序列数据:形状为(samples, timesteps, features)的3阶张量,每个样本都是特征向量组成的序列。
灰度图:形状为(samples, width, height)的3 阶张量,每个样本表示一个灰度图。
4 4阶 张量 (64, 3, 224, 224) 图像数据:形状为(samples, height, width, channels)的4阶张量,每个样本都是一个二维像素网格,每个像素则由一个“通道”​(channel)向量表示。
5 5阶 张量 (32, 10, 128, 128, 3) 视频数据:形状为(samples, frames, height, width, channels)的5阶张量,每个样本都是由图像组成的序列(序列长度为frames)​。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np
from tensorflow.keras.datasets import mnist


# 0阶 张量(标量)
x = np.array(12)
print(x.ndim) # 0

# 1阶 张量(向量)
x = np.array([12, 3, 6, 14, 7])
print(x.ndim) # 1

# 2阶 张量(矩阵)
x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1]])
print(x.ndim) # 2

# 查看 MNIST 数据的形状
(train_images, train_labels), _ = mnist.load_data()
print(train_images.shape) # (60000, 28, 28)
print(train_images.dtype) # uint8
print(train_labels.shape) # (60000,)
print(train_labels.dtype) # uint8

张量的三个关键属性

  1. ndim(阶数):维度数量

  2. shape(形状):每个维度的大小,如 (60000, 28, 28)

  3. dtype(数据类型):float32、uint8、int64 等

在开头的 mnist 数据例子中,输入数据是一个形状为 (60000, 28, 28) 的 3 阶张量,标签数据是形状为 (60000,) 的 1 阶张量。

神经网络的“齿轮”​:张量运算

神经网络中的所有变换,归根结底只有几种基本运算。

逐元素运算

relu运算和加法都是逐元素运算,即该运算分别应用于张量的每个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def naive_relu(x):
"""手动实现 relu,展示逐元素操作"""
assert x.ndim == 2
x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] = max(x[i, j], 0)
return x

def naive_add(x, y):
"""手动实现矩阵加法"""
assert x.ndim == 2 and x.shape == y.shape
x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] += y[i, j]
return x
广播

当两个张量形状不同时,在进行运算时会自动”广播”小张量以匹配大张量的形状。

1
2
3
4
5
x = np.random.random((64, 3, 32, 10))   # 4阶 张量
y = np.random.random((32, 10)) # 2阶 张量

z = np.maximum(x, y) # y 会被广播成 (64, 3, 32, 10),逐元素比较
print(z.shape) # (64, 3, 32, 10)
张量积

张量积,也叫点积。它是一种将两个不同维度的信息“展开”,”组合”成一个更大空间的方式。它必须满足以下前提条件:

运算类型 必须满足的条件 示例
向量点积 两个向量长度相等 (n,) · (n,)
矩阵乘法 第一个矩阵的列数 = 第二个矩阵的行数 (m, k) @ (k, n)
高维 指定收缩的轴的尺寸必须对应相等 a.shape[-1] == b.shape[0](默认情况)

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def naive_vector_dot(x, y):
"""两个向量的点积(标量)"""
assert x.ndim == 1 and y.ndim == 1
assert x.shape[0] == y.shape[0]
return sum(x[i] * y[i] for i in range(x.shape[0]))

def naive_matrix_vector_dot(x, y):
"""矩阵与向量的点积(向量)"""
assert x.ndim == 2 and y.ndim == 1
assert x.shape[1] == y.shape[0]
return np.array([naive_vector_dot(x[i, :], y) for i in range(x.shape[0])])

def naive_matrix_dot(x, y):
"""矩阵与矩阵的点积(矩阵乘法)"""
assert x.ndim == 2 and y.ndim == 2
assert x.shape[1] == y.shape[0] # 左矩阵的列数 = 右矩阵的行数
z = np.zeros((x.shape[0], y.shape[1]))
for i in range(x.shape[0]):
for j in range(y.shape[1]):
z[i, j] = naive_vector_dot(x[i, :], y[:, j])
return z
张量变形

张量变形是指重新排列张量的行和列,以得到想要的形状。变形后,张量的元素个数与初始张量相同。

代码示例:

1
2
3
4
5
6
7
8
9
10
x = np.array([[0., 1.],
[2., 3.],
[4., 5.]])
print(x.shape) # (3, 2)

x = x.reshape((6, 1))
print(x.shape) # (6, 1)

x = x.reshape((2, 3))
print(x.shape) # (2, 3)

回到开头的数字识别的示例中:

1
keras.layers.Dense(512, activation="relu")

这个层叫“全连接”神经层,实际上可以理解为一个函数,其输入是一个张量,返回是另一个新的张量:

1
output = relu(dot(input, W) + b)

该函数涉及了2 种张量运算:点积运算、逐元素运算(relu 和 加法都是逐元素运算,relu 的作用是为了引入非线性)。

神经网络的“引擎”​:基于梯度的优化

先来复习下几个基本概念:

导数

导数最经典的几何意义就是:曲线在某一点的切线斜率。

  • 对于函数y = f(x),它的图像是一条曲线。

  • 在曲线上取一点x_0,过这一点画一条切线。

  • 这条切线的斜率,就是函数在x_0处的导数f'(x_0)。

直观理解:

  • 斜率 > 0:x 的微增将导致 y 变大;
  • 斜率 < 0:x 的微增将导致 y 变小;
  • 斜率 = 0:处于水平状态(可能是极值点)
  • 斜率的绝对值越大:变化越陡峭
梯度

张量运算的导数叫作梯度(gradient)​。在深度学习中,它可以表示为,当输入参数发生变化时,张量函数的输出如何变化。

梯度下降法

回到“全连接”神经层的这个函数:

1
output = relu(dot(input, W) + b)

在这个表达式中,W和b是张量,均为该层的属性。它们被称为该层的权重(weight)或可训练参数(trainableparameter)​。这些权重就是模型从训练数据中学到的信息,这些信息是如何学习得到的呢?

  1. 一开始,这些权重会随机初始化(random initialization)​;

  2. 抽取训练(input)样本 x 和对应的目标值 y_true (预期 output)组成的数据批量;

  3. 在 x 上运行“全连接”神经层中的函数,得到预测 y_pred 值(实际 output),这一步也叫前向传播;

  4. 计算 y_pred 和 y_true 的损失值 loss_value,用于衡量 y_pred 和 y_true 的差距;

  5. 更新模型的权重,减小 y_pred 和 y_true 的损失值 loss_value;

  6. 当得到的 y_pred 与 y_true 之间的差距(loss_value)足够小时,这个时候的模型便已经学会了如何将将输入映射到正确的输出。

而这个过程,就是“深度学习”的“训练”过程。

问题在于第 5 步,对于模型的某个权重,如何能知道这个系数该增大还是该减小,以及变化多少呢?

梯度下降法就是做这个事情的方法。

实际上整个“训练”过程,我们只有一个目的,就是降低损失值 loss_value。回顾“训练”的计算过程,其涉及的函数变换,大致可以表示如下:

1
2
y_pred = relu(dot(input, W) + b)
loss_value = loss(y_pred, y_true)

如果输入数据 input 和 y_true 保持不变,那么可以将前面的运算看作一个将模型权重 W 和 b 的值映射到损失值 loss_value 的函数:

1
loss_value = f(W, b)

此时该函数描述的是,W 和 b 变化时,loss_value 如何变化形成的曲线。但如何能知道怎么才能让 loss_value 往小的方向变化呢?答案也呼之欲出了,那就是先求 f(W, b) 函数的梯度,只要知道了梯度,就知道 loss_value 变化的方向(也叫反向传播),此时就可以沿着变化相反的方向调整 (W, b),从而达到让 loss_value 减小的目的,而这个整个过程也叫梯度下降。

模型训练的本质

现在我们可以完整地描述”训练一个神经网络”这件事了:模型由许多层链接在一起组成,并将输入数据映射为预测值。随后,损失函数将这些预测值与目标值进行比较,得到一个损失值,用于衡量模型预测值与预期值之间的差距。优化器将利用这个损失梯度来更新模型的权重。