机器学习
-
人工智能(Artificial Intelligence)属于计算机科学的分支,是让各类机器载体上模拟并拥有类似生物的智能,让机器可以进行感知、学习、识别、推理等行为的计算机科学技术。
-
机器学习(Machine Learning,ML)是实现人工智能的核心方法,传统的机器学习主要关注如何学习一个预测模型,将数据表示为特征后将特征输入到预测模型,并输出预测结果。现代机器学习则主要由神经网络来完成。
-
神经网络(Neural Network,NN)是机器学习的一个分支,主要关注如何使用神经网络来学习数据的表示,并使用这些表示来完成各种通用类型的任务。
-
深度学习(Deep Learning,DL)是神经网络的一个分支,主要关注如何使用层数较多的且参数较多的神经网络来学习数据。
下面是一个只用加减乘除实现求某数平方根的示例:
# 我们要求解的数
target_number = 17.0
# 初始化权重(我们的猜测值)
weight = 1
# 超参数
learning_rate = 0.01 # 学习率
epochs = 1000 # 训练轮数
# 训练过程
for epoch in range(epochs):
# 前向传播: 计算预测值 (weight * weight 应该等于 target_number)
prediction = weight * weight
# 计算损失: 均方误差 (MSE)
loss = (prediction - target_number) ** 2
# 反向传播: 计算损失(loss)对权重(weight)的梯度
gradient = 2 * (prediction - target_number) * 2 * weight
# 更新权重 (梯度下降)
weight = weight - learning_rate * gradient
# 最终结果
print(f"\n训练后的平方根估计值: {weight}")
print(f"误差: {target_number - prediction}")
上述代码展示了机器学习的核心概念:
- 学习率:控制每次参数更新的步长
- 前向传播:使用当前权重进行预测
- 损失计算:衡量预测值与目标值之间的差距
- 反向传播:计算损失对权重的梯度
- 梯度下降:沿梯度方向更新权重,最小化损失函数
这个简单的例子虽然不是典型的神经网络,但展示了机器学习的基本原理:通过不断调整参数,使模型输出逐渐接近目标值。
不同的接近方法即为不同的算法,要注意,没有一个算法是万能的,需要根据具体问题选择合适的算法。“All models are wrong, but some are useful.”?
如果数据较大,则必须要对数据做各种预处理,数据的质量决定了模型的上限。这也是一个非常耗费时间且需要经验的工作。
现代深度学习往往需要大量的算力,如何高效的利用算力,也是一个方向,例如分布式训练、混合精度训练、模型剪枝、知识蒸馏、高效微调等。
机器学习步骤
数据清洗
数据决定着模型预测的上界,模型则是尽可能达到这个上界。
特征工程
好的特征工程就像是衡量人的体重是否超标时,构造出BMI。这种数据直觉是AI较难替代的。
模型参数调节
至少从抽象层面上了解每个模型的工作原理,才能理解参数的含义。
模型融合
实在没有办法提升性能,可以试 试多模型融合,模型是可以互补的。
PyTorch
在我们这个时代有许多优秀的机器学习框架,其中pytorch明显优于其他框架。
许多著名的深度学习模型和框架都是基于 PyTorch(Torch)二次开发的。例如:YOLOv11、Transformers (Hugging Face)、Stable Diffusion等。还有一些开源项目使用pytorch构建出可用的多模态大模型。上限非常高。
PyTorch 可以利用计算加速设备(例如GPU、NPU),为了达成这一目的,PyTorch 的安装会绑定对应的cuda版本,PyTorch 使用 cuda 的接口来操作底层硬件。
CUDA:NVIDIA 专为自家 GPU 设计的 C++ 并行计算框架,其运行依赖于 NVIDIA 显卡驱动程序。它允许开发者利用 GPU 强大的并行计算能力加速各类计算密集型任务。能把复杂的计算任务(比如矩阵乘法、神经网络运算)翻译成GPU能理解的指令。没有CUDA,GPU只能处理简单的图形渲染,无法参与深度学习的计算。
cuDNN:cuDNN 是专门为深度学 习优化的“外挂包”,它基于 CUDA 开发,针对神经网络的关键操作(如卷积、池化层)做了极致优化,相当于给数学天才(GPU)配了一把趁手的“瑞士军刀”。如果用原始 CUDA 开发大模型,就像用菜刀切牛排——能切但效率低。cuDNN直接提供预制好的高效函数,比如把图像识别中的卷积运算速度提升2倍以上,还能减少内存占用,让大模型跑得更流畅。
CUDA Toolkit (NVIDIA 官方版):完整的 CUDA 开发环境,包含:
- NVIDIA 显卡驱动程序
- NVCC:NVIDIA CUDA 编译器,是 CUDA Toolkit 的核心组件,负责将 CUDA 代码编译为可在 NVIDIA GPU 上执行的二进制代码。
- 完整的 CUDA 开发工具链(编译器、IDE、调试器等)
- 各种 CUDA 加速库及其头文件
- 文档和示例代码
CUDA Toolkit (PyTorch 版):精简版 CUDA 工具包,主要包含:
- 运行 CUDA 功能所需的核心动态链接库
- 不包含驱动程序、开发工具及完整文档
- 专为支持 PyTorch 等框架的 CUDA 功能而设计
不过同样的测试代码,在WSL2中安装的cuda对显卡性能会存在一定的影响。
# windows原生环境
name: NVIDIA GeForce RTX 3090 Ti
write speed: 5063.91 MB/s
read speed: 5565.53 MB/s
# windows下的WSL2 Ubuntu-24.04 环境
name: NVIDIA GeForce RTX 3090 Ti
write speed: 2632.96 MB/s
read speed: 4429.29 MB/s
torch框架
torchvision
torchvision 是 PyTorch 项目的一部分。由流行的数据集、模型架构和计算机视觉的常见图像转换组成。
这些数据集包括了各种类型的数据,如图像、文本、音频等,可以用于各种任务,如分类、回归、聚类等。
安装:pip install torchvision
| 数据集名称 | 加载方法 | 模型类型 | 数据大小(样本数*特征数) |
|---|---|---|---|
| MNIST | torchvision.datasets.MNIST | 分类 | 70000*784 |
| CIFAR-10 | torchvision.datasets.CIFAR10 | 分类 | 60000*3072 |
| Fashion MNIST | torchvision.datasets.FashionMNIST | 分类 | 70000*784 |
import torchvision.datasets as datasets
# 加载数据集
train_dataset = datasets.MNIST(root='./data', train=True, download=True)
test_dataset = datasets.MNIST(root='./data', train=False, download=True)
torchtext
简化文本数据处理(分词/词嵌入/标记化),内置 NLP 数据集(IMDB 影评等)
安装:pip install torchtext
# 情感分析快速实现
from torchtext.data import Field, TabularDataset
TEXT = Field(tokenize='spacy')
LABEL = Field(sequential=False)
train_data = TabularDataset(path='train.csv', format='csv',
fields=[('text', TEXT), ('label', LABEL)])
Ignite
训练流程管理库,可以简化训练循环代码、内置进度条/早停/模型保存等实用功能、官方示例丰富
安装:pip install pytorch-ignite
from ignite.engine import Events, create_supervised_trainer
trainer = create_supervised_trainer(model, optimizer, loss_fn)
@trainer.on(Events.ITERATION_COMPLETED(every=100))
def log_loss(engine):
print(f"Epoch {engine.state.epoch}, Loss: {engine.state.output:.2f}")
神经网络
凡是模仿人的神经网络构建出来的数学模型,都叫神经网络,或者人工神经网络。
神经网络可以归为三块:
- BP 神经网络(Backpropagation Neural Network)
神经网络非常多,今天你根据生物神经网络构建 一个模型,明天我根据生物神经网络构建一个。1986 年,Romelhart 和 Mcclelland,提出了一个特殊的结构,使用了反向传播(Backpropagation)算法,并命名:BP 神经网络。
BP 神经网络提出后,瞬间崛起后当了主力军。在不特指时,往往说神经网络都是指 BP 神经网络。
- 全连接神经网络(Fully Connected Neural Network)
全连接神经网络,是一种最基础、最经典的神经网络结构。它的特点是:网络中相邻两层的所有神经元之间都有连接。也就是说,前一层的每个神经元都与后一层的每个神经元相连。信息只从输入层流向隐藏层,再流向输出层,没有"跳过"中间层的连接。这种网络是最早被广泛研究和使用的神经网络类型,也是理解深度学习的基础。
由多个全连接层组成的前馈神经网络,也称为多层感知机(Multi-Layer Perceptron, MLP)。
- 深度学习(Deep Learning)
BP 神经网络虽然很好用,但当要处理图象、音频、文字等问题时,却不行了,BP 神经网络的参数会随着输入个数指数增长。例如一个 50 * 50 像素的图象,就有 2500 个输入。假设有 100 个隐节点,则 2500 个输入在第一层的权重参数就有 2500 * 100 个,参数个数量级太爆炸,导致 BP 在求解时,很难找到优秀解就宕机了。这本来是个没办法的事,但偏偏图象、音频这些问题,它的输入存在很严重的相关性(例如相邻像素的值总是相近的)因此,可以根据这个业务特性,进行输入个数压缩,或者在求解时根据这个业务特性进行特殊讨巧(例如相邻输入对应的权重参数共享)使 BP 神经网络又可以解决这类问题了。问题解决了,BP 还是 BP,挂个名:深度学习!你可以把深度学习看作是 BP 神经网络的一种加强版,解决输入极极极极多的问题。
神 经元与神经元之间是以神经冲动的模式进行传值,信号到了神经元,都是以电信号的形式存在,当电信号在神经元积累到超过阈值时,就会触发神经冲动,将电信号传给其它神经元。正是根据这个思路,就构造出了以上的神经网络结构。
一张 28*28 的图片可以抽象为一个 784 维的向量,每个像素点的值就是向量的一个分量。我们可以将这个向量作为输入,通过神经网络,输出一个 10 维的向量,每个分量代表图片属于某个数字的概率。
一个有 784 个输入节点,16 个隐藏节点,10 个输出节点的神经网络结构,用线性代数的方法简洁表示层与层之间的权重:
矩阵表示
- pytorch
- numpy
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# 定义数据转换
# Compose 是一个函数,用于将多个变换组合在一起
transform = transforms.Compose([
# 将图像转换为张量
transforms.ToTensor(),
# 将图像的像素值转换为0.1307附近,标准差为0.3081
# 为什么是0.1307和0.3081?
# 因为MNIST数据集的像素值是0-255,转换为0-1之间的值
# 0.1307是MNIST数据集的均值,0.3081是MNIST数据集的标准差
# 所以需要将图像的像素值转换为0.1307附近,标准差为0.3081
transforms.Normalize((0.1307,), (0.3081,))
])
# 加载MNIST数据集
# root:数据集的根目录
# train:是否为训练集(获取带有train=True的数据集)
# download:是否下载数据集,如果没有,则下载
# transform:数据转换
train_dataset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
# 创建数据加载器
# batch_size:每个batch(批次)的大小
# 如果batch_size=1,则每次只取一个样本,则不需要打乱数据集
# 如果batch_size=1000,则每次取1000个样本,则需要打乱数据集
# 较大的batch_size可以提高训练速度,但可能会导致内存不足
# shuffle:是否打乱数据集
batch_size = 32
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# 定义神经网络模型
# 继承自nn.Module,所以需要实现forward方法
class NeuralNetwork(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
# 调用父类nn.Module的构造函数
super(NeuralNetwork, self).__init__()
# 初始化线性层1
self.layer1 = nn.Linear(input_size, hidden_size)
# 初始化ReLU激活函数
self.relu = nn.ReLU()
# 初始化线性层2
self.layer2 = nn.Linear(hidden_size, output_size)
# 在PyTorch中forward是一个特殊方法名,是模型正向传播的标准命名
# 由于nn.Module继承自nn.Module,所以需要实现forward方法,forward方法中的数据会被记录,用于下方loss.backward()自动反向传播
def forward(self, x):
# 将图像展平
x = x.reshape(-1, input_size)
# 执行线性层1的前向传播计算
x = self.layer1(x)
# 执行ReLU激活函数
x = self.relu(x)
# 执行线性层2的前向传播计算
x = self.layer2(x)
return x
# # 反向传播
# def backward(self, x):
# # 执行线性层1的反向传播计算
# x = self.layer1.backward(x)
# # 执行ReLU激活函数的反向传播计算
# x = self.relu.backward(x)
# # 执行线性层2的反向传播计算
# x = self.layer2.backward(x)
# 创建模型实例
# 输入层:28*28=784
input_size = 28 * 28
# 隐藏层:128(可以设置为其他值,通常设为2的幂次方,且不超过输入层的一半)
hidden_size = 128
# 输出层:10 (因为MNIST数据集有10个类别)
output_size = 10
model = NeuralNetwork(input_size, hidden_size, output_size)
# 定义损失函数和优化器
# 重点1:损失函数
# 常见的损失函数有:交叉熵、均方误差
# 交叉熵损失函数:用于分类问题,计算预测值和真实值之间的差异
# 公式:loss = -sum(y_true * log(y_pred))
# 均方误差:用于回归问题,计算预测值和真实值之间的差异
# 公式:loss = sum((y_true - y_pred) ** 2)
criterion = nn.CrossEntropyLoss()
# 重点2:优化器
# 常见的优化器有:随机梯度下降、Adam、RMSprop等
# SGD:随机梯度下降
# 公式:optimizer = optim.SGD(model.parameters(), lr=0.01)
# lr:学习率,用于控制每次更新参数的步长
# momentum:动量,用于加速梯度下降
# 梯度计算公式与momentum有关,最终更新的梯度为:g_v = momentum * g_v + lr * grad
# 其中grad是梯度,lr是学习率,momentum是动量,g_v是最终更新的梯度
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# 训练模型,epochs是训练的轮数,训练的轮数越多,训练的越充分,但训练时间越长
# 训练的前期梯度下降较多,后期梯度下降较少,因为后期梯度较小,所以需要较小的学习率
epochs = 5
for epoch in range(epochs):
running_loss = 0.0
for i, (images, labels) in enumerate(train_loader):
# 模型训练,把模型设置为训练模式
# 激活BatchNorm层的参数更新机制,使用当前批次的均值和方差进行归一化,并更新运行时统计值
# 激活Dropout层,随机丢弃一部分神经元,防止过拟合
# 确保梯度计算和参数更新正常进行
model.train()
# 前向传播
outputs = model(images)
# 计算损失
loss = criterion(outputs, labels)
# 梯度清零,如果不清零,梯度会累加
optimizer.zero_grad()
# 反向传播
'''
PyTorch的自动微分引擎很智能(你也可以在模型中像构建前向传播一样,手动设置backward方法),它能够自动处理梯度在计算图中的流动。
不需要我们明确指定梯度应该传播到哪个模型。
只要模型的参数参与了损失的计算,它们就会自动成为梯度计算的一部分。
1. 当你执行前向计算时,PyTorch会在后台构建一个动态计算图,记录所有操作及其依赖关系。每个张量都会存储信息,指向创建它的操作(存储在`grad_fn`属性中)。
2. 损失函数(如`loss`)是这个计算图的一个节点,通过`.backward()`方法,PyTorch会从这个节点开始沿着计算图向后追踪所有需要计算梯度的参数。
3. 当创建模型时,所有参数都会自动设置`requires_grad=True`,这些参数会被纳入计算图中。当计算损失时,这些参数通过一系列操作与损失函数相连接。
4. PyTorch会自动追踪从损失到各个参数的路径,不需要明确指定模型。这是因为计算图包含了所有操作的完整记录,包括哪些参数参与了计算。
'''
loss.backward()
# 更新参数,更新参数的公式为:weight = weight - lr * grad
optimizer.step()
# 累计损失
running_loss += loss.item()
# 每个epoch结束后计算准确率
correct = 0
total = 0
# 测试模型
# 测试模型时,不需要计算梯度,即关闭后台自动的梯度计算
with torch.no_grad():
# 加载测试集
for images, labels in test_loader:
# 测试模型,把模型设置为测试模式
# BatchNorm层会使用运行时统计数据而不是批次统计数据
# Dropout层会停止随机丢弃神经元
model.eval()
outputs = model(images)
# 获取预测值
# 区别与torch.maximum
# torch.max会返回两个值,一个是最大值,一个是最大值的索引
# 而torch.maximum只会返回最大值
# 点击进入这个方法,可以看到示例
_, predicted = torch.max(outputs.data, 1)
# 累计样本数,labels.size(0) 表示当前批次中样本的数量。
# 在测试过程中,当使用 DataLoader 加载数据时,每个批次可能包含多个样本(由 batch_size 决定)。
total += labels.size(0)
# 累计正确样本数
# 计算预测值与真实值相等的样本数
print(predicted) # 得到长度为batch_size的向量(预测值)
print(labels) # 得到长度为batch_size的向量(真实值)
# 计算预测值与真实值相等的样本数
test = (predicted == labels)
# [ True .... False ],Python计算布尔值的时候,把True当作1,False当作0
# 所以test.sum()就是预测值与真实值相等的样本数
# .item() 方法用于将张量转换为标量( 即一个数值)用于加法计算
correct += test.sum().item()
accuracy = correct / total
print(f'Epoch {epoch+1}/{epochs}, Loss: {running_loss/len(train_loader):.4f}, Accuracy: {accuracy:.4f}')
# 评估模型
correct = 0
total = 0
# 关闭梯度计算
with torch.no_grad():
for images, labels in test_loader:
# 测试模型,把模型设置为测试模式
"""
本代码不加model.train()和model.eval()原因有以下几点:
1. 模型中没有BatchNorm和Dropout等层,调用与否确实不会有区别
2. 创建模型后,PyTorch默认是训练模式,所以不调用model.train()也能正常训练,只有明确调用model.eval()后才会切换到评估模式
更复杂的模型如ResNet、BERT等都包含这些层。
所以需要养成习惯始终在相应阶段调用model.train()和model.eval()
这样代码更规范,也能避免在模型变复杂后出现意外问题。
"""
model.eval()
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
accuracy = correct / total
print(f'测试集准确率: {accuracy:.4f}')
import numpy
# 确保绘图在此nb中进行,而不是在外部窗口中
class NeuralNetwork:
# 初始化神经网络
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
# 设置每个输入、隐藏和输出层的节点数
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
# 连接权重矩阵,wih 和 who
# 数组中的权重是 w_i_j,其中链接从上一层的节点 i 到下一层的节点 j
# w11 w21
# w12 w22 等等
# 返回正态分布数据
self.wih = numpy.random.normal(0.0, pow(self.inodes, -0.5), (self.hnodes, self.inodes))
self.who = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.onodes, self.hnodes))
# 学习率
self.lr = learningrate
# 激活函数是 sigmoid 函数
self.activation_function = lambda x: 1 / (1 + numpy.exp(-x))
pass
# 训练神经网络
def train(self, 输入列表, 目标列表):
# 将输入列表转换为 2D 数组
inputs = numpy.array(输入列表, ndmin=2).T
targets = numpy.array(目标列表, ndmin=2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算从隐藏层出来的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入最终输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算从最终输出层出来的信号
final_outputs = self.activation_function(final_inputs)
# 输出层误差是 (目标 - 实际)
output_errors = targets - final_outputs
# 隐藏层误差是输出误差,按权重拆分,重新组合在隐藏节点处
hidden_errors = numpy.dot(self.who.T, output_errors)
# 更新隐藏层和输出层之间的链接权重
self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))
# 更新输入层和隐藏层之间的链接权重
self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
pass
# 查询神经网络
def query(self, 输入列表):
# 将输入列表转换为 2D 数组
inputs = numpy.array(输入列表, ndmin=2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算从隐藏层出来的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入最终输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算从最终输出层出来的信号
final_outputs = self.activation_function(final_inputs)
return final_outputs
input_nodes = 64
hidden_nodes = 32
output_nodes = 10
# 学习率
learning_rate = 0.2
# 创建神经网络实例
n = NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
from sklearn import datasets
from sklearn.model_selection import train_test_split
# 加载数据集
digits = datasets.load_digits()
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, test_size=0.1,random_state=0)
epochs = 2 # 回声增加样本量
for i in range(epochs):
for record in zip(y_train, X_train):
y, X = record
# 缩放和移动输入
inputs = (numpy.asfarray(X) / 16.0 * 0.99) + 0.01
# 创建目标输出值(全部为 0.01,除了所需标签为 0.99)
targets = numpy.zeros(output_nodes) + 0.01
# all_values[0] 是此记录的目标标签
targets[int(y)] = 0.99
n.train(inputs, targets)
# 我们自己的图像测试数据集
our_own_dataset = []
for i in zip(y_test, X_test):
label, img_array = i
# img_data = 16.0 - img_array
# 然后将数据缩放到范围从 0.01 到 1.0
img_data = (img_array / 16.0 * 0.99) + 0.01
# 将标签和图像数据附加到测试数据集
record = numpy.append(label, img_data)
our_own_dataset.append(record)
pass
right = 0
error = 0
for item in range(len(our_own_dataset)):
# 正确答案是第一个值
correct_label = our_own_dataset[item][0]
# print(correct_label)
# 数据是剩余的值
inputs = our_own_dataset[item][1:]
# 查询网络
outputs = n.query(inputs)
# 最高值的索引对应于标签
label = numpy.argmax(outputs)
if label != correct_label:
error += 1
else:
right += 1
print(right / (right + error))
图像识别
在现实生活中并不存在一个场景,给你单独一个数字让你进行识别。但是有一些类似的场景,譬如:车牌号、发票号码、网站的验证码等。针对这些场景,我提供一些思路:
- 我们需要对图像进行分割,下面是一个分割 4 个字的验证码的例子
- 分割完成后再将结果依次做标准化处理,譬如压缩数组大小
- 逐一识别,返回结果
下面代码以numpy版本的神经网络为例:
from PIL import ImageFont,Image,ImageDraw
# 生成一个验证码
c_chars = "0 1 2 3 4"
path = 'test.png'
size = (100,24) #图片大小
img = Image.new("RGB",size)
draw = ImageDraw.Draw(img) #draw一个
font = ImageFont.truetype("arial.ttf", 23) #字体
draw.text((5,0),c_chars,font=font,fill="white") #字颜色
# img.show()
img.save(path)
# 分割图片
def sliceImg(img_path, count = 5):
img = Image.open(img_path).convert("L")
w, h = img.size
eachWidth = int(w/count)
for i in range(count):
box = (i * eachWidth, 0, (i + 1) * eachWidth, h)
yield img.crop(box)
# 转化图片
def exchange(img):
target_size = (8, 8)
resized_image = img.resize(target_size)
# resized_image.show()
return resized_image
out = ""
for i in sliceImg(path):
# .flatten()方法将数组转化为列表
original_array = numpy.array(exchange(i)).flatten()
# 将数据0-1化
inputs = (original_array / 255.0 * 0.99) + 0.01
# 查询网络
outputs = n.query(inputs)
# print(outputs)
# 最高值的索引对应于标签
label = numpy.argmax(outputs)
out+=str(label)
print(out)
从结果上我们可以看到,0、1、2、4 被正确的识别了。但数字 3 没有被正确的识别。请思考这个案例中,最有效的优化方式。
答案A:优化图片裁切结构,使用其他膨胀、侵蚀、over padding等算法
答案B:增加回声,强化模型能力
答案C:增加训练次数
答案D:其他
# 这里我检查了转化后8*8的数字3的图片,对比了手写数字3和手写数字8,
# 我认为生成的数字3与手写8更接近相对于与手写3
# 所以我认为的解决的方案是:给模型再喂一些生成数字3的样本。
from PIL import ImageFont,Image,ImageDraw
# 生成一个验证码
c_chars = "3 3 3 3 3"
path = 'test.png'
size = (100,24) #图片大小
img = Image.new("RGB",size)
draw = ImageDraw.Draw(img) #draw一个
font = ImageFont.truetype("arial.ttf", 23) #字体
draw.text((5,0),c_chars,font=font,fill="white") #字颜色
# img.show()
img.save(path)
# 分割图片
def sliceImg(img_path, count = 5):
img = Image.open(img_path).convert("L")
w, h = img.size
eachWidth = int(w/count)
for i in range(count):
box = (i * eachWidth, 0, (i + 1) * eachWidth, h)
yield img.crop(box)
# 转化图片
def exchange(img):
target_size = (8, 8)
resized_image = img.resize(target_size)
# resized_image.show()
return resized_image
for i in sliceImg(path):
# .flatten()方法将数组转化为列表
original_array = numpy.array(exchange(i)).flatten()
# 将数据0-1化
inputs = (original_array / 255.0 * 0.99) + 0.01
targets = numpy.zeros(output_nodes) + 0.01
# all_values[0] 是此记录的目标标签
targets[int(3)] = 0.99
n.train(inputs, targets)
# 生成一个验证码
c_chars = "0 1 2 3 4"
path = 'test.png'
size = (100,24) #图片大小
img = Image.new("RGB",size)
draw = ImageDraw.Draw(img) #draw一个
font = ImageFont.truetype("arial.ttf", 23) #字体
draw.text((5,0),c_chars,font=font,fill="white") #字颜色
# img.show()
img.save(path)
out = ""
for i in sliceImg(path):
# .flatten()方法将数组转化为列表
original_array = numpy.array(exchange(i)).flatten()
# 将数据0-1化
inputs = (original_array / 255.0 * 0.99) + 0.01
# 查询网络
outputs = n.query(inputs)
# print(outputs)
# 最高值的索引对应于标签
label = numpy.argmax(outputs)
out+=str(label)
print(out)
# 移除生成的图片,保持文件夹的整洁
import os
os.remove(path)
可以看到输出的结果已经正确的显示为1、2、3、4。如此一来。模型就算训练完成了。
LeNet-5
LeNet-5被认为是卷积神经网络(CNN)的开山之作,主要用于手写数字识别。它包含了卷积层、池化层和全连接层,是现代CNN的雏形。
卷积
卷积神经网络(convolutional neural network)简称CNN。卷积神经网络的核心是卷积核,卷积核在图像处理领域可以用来提取图像的纵向和横向特征。
卷积核的大小一般为奇数,如3x3,5x5,7x7等,卷积核通常与图像处理(over padding)后的图像进行卷积操作,卷积核在图像上滑动,每次滑动一个像素,对应位置的像素值与卷积核对应位置的值相乘,然后求和,最后将求和的结果作为卷积核中心像素的值,这样就得到了一个新的图像。
新的图像可以用更少的数据反应出图像的特征。这个过程就是特征提取。
我们从一个6x6的矩阵开始:
我们的卷积核是一个3x3的矩阵:
我们假设卷积核位于原始矩阵的左上角 ,覆盖的区域如下:
此时,输出矩阵的第一个元素的计算为:
整个输出矩阵
卷积核在整个6x6矩阵上滑动(从左至右,从上至下),生成一个4x4的输出矩阵。输出矩阵的每个元素都按照上述方式计算。
点击查看卷积核动画
// 你可以尝试更改矩阵尺寸与卷积核的尺寸来感受卷积过程 function example(props) { // 使用 XPath 查询选择输出框 const xpathSelector = "/html/body/div/div[2]/div/div/main/div/div/div/div/article/div[2]/div[1]/div[4]"; const myElement = document.evaluate( xpathSelector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; // 矩阵尺寸 const matrixSize = 6; // 卷积核尺寸 const kernelSize = 3; const matrix = Array.from({ length: matrixSize }, (_, i) => Array.from({ length: matrixSize }, (_, j) => `a${i + 1}${j + 1}`) ); const [position, setPosition] = useState([0, 0]); useEffect(() => { const positions = []; for (let i = 0; i <= matrixSize - kernelSize; i++) { for (let j = 0; j <= matrixSize - kernelSize; j++) { positions.push([i, j]); } } let index = 0; const interval = setInterval(() => { setPosition(positions[index]); index = (index + 1) % positions.length; }, 1000); return () => clearInterval(interval); }, []); return ( <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', backgroundColor: '#f0f0f0' }}> <div style={{ display: 'grid', gridTemplateColumns: `repeat(${matrixSize}, 50px)`, gridGap: '5px', position: 'relative' }}> {matrix.map((row, i) => row.map((cell, j) => ( <div key={`${i}-${j}`} style={{ width: '50px', height: '50px', backgroundColor: '#fff', border: '1px solid #ccc', display: 'flex', justifyContent: 'center', alignItems: 'center', fontSize: '18px', backgroundColor: i >= position[0] && i < position[0] + kernelSize && j >= position[1] && j < position[1] + kernelSize ? 'yellow' : '#fff' }} > {cell} </div> )) )} </div> </div> ); }
最终输出矩阵为:
每个的具体计算方法如前所述,通过卷积核在原始矩阵上的滑动和计算得到。
通过这个例子,可以清晰地看到卷积核是如何对矩阵进行操作并生成输出的。
常见卷积核
-
水平边缘检测:
用途:检测水平边缘。
-
垂直边缘检测:
用途:检测垂直边缘。
-
Sobel算子 (水平):
用途:检测水平边缘和梯度。
-
Sobel算子(垂直):
用途:检测垂直边缘和梯度。
-
拉普拉斯算子:
用途:检测图像的二阶导数,强调边缘。
-
锐化:
用途:提高图像的清晰度。
-
高斯模糊(3x3):
用途:平滑图像,减少噪声。
-
高斯模糊(5x5):
用途:更强的平滑效果。
-
边缘增强:
用途:增强边缘,使图像轮廓更加明显。
-
均值滤波:
用途:均匀地平滑图像。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
import cv2
# 设置中文字体
# 替换为你系统中支持中文的字体路径(windows)
font_path = r'C:\Windows\Fonts\simhei.ttf'
# mac(如果有的话)
# font_path = '/System/Library/Fonts/STHeiti Light.ttc'
font_prop = FontProperties(fname=font_path)
# 读取灰度图像
image = np.array(cv2.imread('data/people.bmp',cv2.IMREAD_GRAYSCALE))
# 定义卷积核
kernels = {
'水平边缘': np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]]),
'垂直边缘': np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]),
'Sobel水平': np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]),
'Sobel垂直': np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]]),
'拉普拉斯': np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]]),
'锐化': np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]),
'高斯模糊3x3': np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]]) / 16,
'高斯模糊5x5': np.array([[1, 4, 6, 4, 1], [4, 16, 24, 16, 4], [6, 24, 36, 24, 6], [4, 16, 24, 16, 4], [1, 4, 6, 4, 1]]) / 256,
'边缘增强': np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]),
'均值滤波': np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]) / 9
}
# 使用NumPy实现卷积操作
def convolve2d(image, kernel):
# 获取图像和卷积核的尺寸
i_height, i_width = image.shape
k_height, k_width = kernel.shape
# 计算输出图像的尺寸
o_height = i_height - k_height + 1
o_width = i_width - k_width + 1
# 创建输出图像
output = np.zeros((o_height, o_width))
# 执行卷积操作
for y in range(o_height):
for x in range(o_width):
# 提取图像区域
region = image[y:y+k_height, x:x+k_width]
# 计算卷积值
output[y, x] = np.sum(region * kernel)
return output
# 应用卷积核
results = {}
for name, kernel in kernels.items():
# 为了处理边界,先对图像进行填充
if kernel.shape[0] == 5: # 对于5x5卷积核
pad_width = 2
else: # 对于3x3卷积核
pad_width = 1
padded_image = np.pad(image, pad_width, mode='constant')
filtered_image = convolve2d(padded_image, kernel)
# 归一化处理,确保像素值在有效范围内
filtered_image = np.clip(filtered_image, 0, 255).astype(np.uint8)
results[name] = filtered_image
# 显示结果
plt.figure(figsize=(15, 8))
for i, (name, result) in enumerate(results.items()):
plt.subplot(3, 4, i + 1)
plt.imshow(result, cmap='gray')
plt.title(name, fontproperties=font_prop)
plt.axis('off')
plt.tight_layout()
plt.show()
池化
池化(Pooling)是一种用于减少卷积神经网络(CNN)中特征图大小的操作。它通过将特征图上的局部区域进行聚合,得到一个更小的特征图。
池化操作类似卷积操作,使用的也是一个很小的矩阵,叫做池化核,但是池化核本身没有参数,只是通过对输入特征矩阵本身进行运算,它的大小通常是2x2、3x3、4x4等,然后将池化核在卷积得到的输出特征图中进行池化操作,需要注意的是,池化的过程中也有Padding方式以及步长的概念,与卷积不同的是,池化的步长往往等于池化核的大小。 最常见的池化操作为最大值池化(Max Pooling)和平均值池化(Average Pooling)两种。
import numpy as np
def pooling(input_array, pool_size=(2, 2), stride=None, mode='max'):
"""
池化操作函数
参数:
input_array: 输入数组,形状为[height, width]或[batch, height, width, channels]
pool_size: 池化窗口大小,默认为(2, 2)
stride: 步长,默认与pool_size相同
mode: 池化类型,'max'表示最大池化,'avg'表示平均池化
返回:
池化后的数组
"""
# 如果未指定stride,则默认与pool_size相同
if stride is None:
stride = pool_size
# 确保输入是numpy数组
input_array = np.asarray(input_array)
# 处理不同维度的输入
if input_array.ndim == 2: # 单通道2D输入
h, w = input_array.shape
d = 1
input_array = input_array.reshape(1, h, w, 1)
elif input_array.ndim == 3: # 带批次或通道的3D输入
raise ValueError("输入数组维度应为2D或4D")
elif input_array.ndim == 4: # 标准4D输入 [batch, height, width, channels]
pass
else:
raise ValueError("输入数组维度应为2D或4D")
# 获取输入尺寸
batch_size, height, width, channels = input_array.shape
# 计算输出尺寸
out_height = (height - pool_size[0]) // stride[0] + 1
out_width = (width - pool_size[1]) // stride[1] + 1
# 初始化输出数组
output = np.zeros((batch_size, out_height, out_width, channels))
# 执行池化操作
for b in range(batch_size):
for c in range(channels):
for i in range(out_height):
for j in range(out_width):
h_start = i * stride[0]
h_end = h_start + pool_size[0]
w_start = j * stride[1]
w_end = w_start + pool_size[1]
pool_region = input_array[b, h_start:h_end, w_start:w_end, c]
if mode == 'max':
output[b, i, j, c] = np.max(pool_region)
elif mode == 'avg':
output[b, i, j, c] = np.mean(pool_region)
else:
raise ValueError("支持的模式为'max'或'avg'")
# 如果输入是2D,则返回2D输出
if input_array.shape[0] == 1 and input_array.shape[3] == 1:
return output[0, :, :, 0]
return output
# 示例使用
if __name__ == "__main__":
# 创建测试数据
test_data = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
])
# 最大池化
max_pooled = pooling(test_data, pool_size=(2, 2), mode='max')
print("最大池化结果:")
print(max_pooled)
# 平均池化
avg_pooled = pooling(test_data, pool_size=(2, 2), mode='avg')
print("平均池化结果:")
print(avg_pooled)
'''
最大池化结果:
[[ 6. 8.]
[14. 16.]]
平均池化结果:
[[ 3.5 5.5]
[11.5 13.5]]
'''
- over padding(填充)
有时图像的特征在边缘上,例如
import numpy as np
import matplotlib.pyplot as plt
# 读取图像
inputs = np.array([
[255,1,2],
[255,1,2],
[255,1,2],]
)
# 用于提取纵向特征的卷积核
kernel = np.array([
[0,1,0],
[0,1,0],
[0,1,0]]
)
# 卷积操作结果,没能正确获取边缘的特征
'''
[[0. 2. 0.]
[0. 2. 0.]
[0. 2. 0.]]
'''
# 对输入图像进行填充
# array: 需要填充的数组
# pad_width: 填充的宽度(上下左右都填充)
# mode: 填充的方式,通常为'constant',
# 有0、空、最大、平均、中位等11种参数可以选,点击方法进入查看
# constant_values: 填充的值,通常为0
inputs = np.pad(
array=inputs,
pad_width=1,
mode='constant',
constant_values=0
)
# 卷积操作
out_put = np.zeros((inputs.shape[0] - kernel.shape[0] + 1, inputs.shape[1] - kernel.shape[1] + 1))
out_put_w = out_put.shape[0]
out_put_h = out_put.shape[1]
for i in range(out_put_w):
for j in range(out_put_h):
conv_result = np.sum(inputs[i:i+kernel.shape[0], j:j+kernel.shape[1]] * kernel)
out_put[i][j] = conv_result
# 卷积操作结果,正确的获取到了边缘的特征
print(out_put)
'''
[[510. 2. 4.]
[765. 3. 6.]
[510. 2. 4.]]
'''
步幅
步幅表示卷积核移动的步长,步幅越大,卷积核每次跳跃的距离就越多,卷积核的感受野越小。
感受野(Receptive Field)的定义:源自生物专业术语,在机器学习中表示卷积神经网络每一层输出的特征图(feature map)上的像素点映射回输入图像上的区域大小。通俗点的解释是,特征图上一点,相对于原图的大小,也是卷积神经网络特征所能看到输入图像的区域。
import numpy as np
def convolution_2d(input_array, kernel, stride=3):
"""
实现2D卷积操作
参数:
input_array: 输入数组,形状为 (height, width)
kernel: 卷积核,形状为 (kernel_size, kernel_size)
stride: 卷积步长,默认为3
返回:
卷积结果数组
"""
# 获取输入数组和卷积核的尺寸
input_height, input_width = input_array.shape
kernel_size = kernel.shape[0]
# 计算输出数组的尺寸
output_height = (input_height - kernel_size) // stride + 1
output_width = (input_width - kernel_size) // stride + 1
# 初始化输出数组
output = np.zeros((output_height, output_width))
# 执行卷积操作
for i in range(output_height):
for j in range(output_width):
# 计算当前窗口的位置
start_i = i * stride
start_j = j * stride
# 提取当前窗口
window = input_array[start_i:start_i+kernel_size, start_j:start_j+kernel_size]
# 计算卷积和
output[i, j] = np.sum(window * kernel)
return output
# 示例使用
if __name__ == "__main__":
# 创建10x10的示例输入数组
input_array = np.ones((8, 8))
# 即输入数组每行列数据下标为 0-7 0-7
# 创建5x5的卷积核
kernel = np.ones((5, 5))
# 执行卷积操作,步幅为3
# 第一次卷积的区域为 0-4 0-4
# 第二次卷积的区域为 3-7 3-7
result = convolution_2d(input_array, kernel, stride=3)
print("输入数组形状:", input_array.shape)
print("卷积核形状:", kernel.shape)
print("卷积结果形状:", result.shape)
print("\n输入数组:")
print(input_array)
print("\n卷积核:")
print(kernel)
print("\n卷积结果:")
print(result)
'''
输入数组形状: (10, 10)
卷积核形状: (5, 5)
卷积结果形状: (2, 2)
输入数组:
[[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]]
卷积核:
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
卷积结果:
[[25. 25.]
[25. 25.]]
'''
手写数字识别
- pytorch
- numpy
这是pytorch实现
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
import numpy as np
# 加载数据 & 预处理
digits = load_digits()
X = digits.images.astype(np.float32) / 16.0 # 归一化到0~1
y = digits.target
X = X[..., np.newaxis] # 添加通道维度 (n,8,8,1)
num_classes = 10
# 划分训练/验证集
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 转换为PyTorch张量,并调整为NCHW格式
X_train = torch.tensor(X_train).permute(0, 3, 1, 2) # NHWC -> NCHW
X_val = torch.tensor(X_val).permute(0, 3, 1, 2)
y_train = torch.tensor(y_train, dtype=torch.long)
y_val = torch.tensor(y_val, dtype=torch.long)
# 定义模型
class SimpleConvNet(nn.Module):
def __init__(self):
super(SimpleConvNet, self).__init__()
self.conv = nn.Conv2d(1, 8, kernel_size=3, padding=1) # 输入1通道,输出8通道
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(kernel_size=2) # 2x2池化
self.fc = nn.Linear(4 * 4 * 8, num_classes) # 全连接层
def forward(self, x):
x = self.conv(x) # 卷积层
x = self.relu(x) # ReLU激活
x = self.pool(x) # 最大池化
x = x.reshape(x.shape[0], -1)
x = self.fc(x) # 全连接层
return x
# 创建模型、损失函数和优化器
model = SimpleConvNet()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 训练参数
epochs = 10
batch_size = 64
# 训练循环
for epoch in range(epochs):
# 创建数据加载器进行批处理
indices = torch.randperm(len(X_train))
model.train() # 设置为训练模式
total_loss = 0
# 小批量训练
for i in range(0, len(X_train), batch_size):
# 获取批次数据
batch_indices = indices[i : i + batch_size]
x_batch = X_train[batch_indices]
y_batch = y_train[batch_indices]
# 前向传播
outputs = model(x_batch)
loss = criterion(outputs, y_batch)
total_loss += loss.item()
# 反向传播和优化
optimizer.zero_grad() # 清除之前的梯度
loss.backward() # 反向传播
optimizer.step() # 更新参数
# 验证
model.eval() # 设置为评估模式
with torch.no_grad(): # 不计算梯度
outputs = model(X_val)
_, predicted = torch.max(outputs, 1) # 获取最大值所在位置
accuracy = (predicted == y_val).float().mean()
print(
f"Epoch {epoch+1}/{epochs} loss={total_loss/len(indices)*batch_size:.4f} val_acc={accuracy:.4f}"
)
'''
Epoch 1/10 loss=2.2956 val_acc=0.4472
Epoch 2/10 loss=2.0604 val_acc=0.6167
Epoch 3/10 loss=1.5787 val_acc=0.7861
Epoch 4/10 loss=1.0126 val_acc=0.8000
Epoch 5/10 loss=0.6914 val_acc=0.7972
Epoch 6/10 loss=0.5458 val_acc=0.7917
Epoch 7/10 loss=0.4080 val_acc=0.8417
Epoch 8/10 loss=0.3853 val_acc=0.8778
Epoch 9/10 loss=0.3235 val_acc=0.9111
Epoch 10/10 loss=0.2700 val_acc=0.9250
'''
这是numpy实现
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
# 卷积层前向传播
def conv2d_forward(x, w, b):
# x: 输入数据,形状为(N,H,W,C)
# w: 卷积核权重,形状为(Kh,Kw,C,Cout)
# b: 偏置项,长度为Cout
N, H, W, C = x.shape # 获取输入数据的形状
Kh, Kw, _, Cout = w.shape # 获取卷积核的形状
padding = 1 # 固定使用padding=1
Ho = (H + 2 * padding - Kh) + 1 # 计算输出高度
Wo = (W + 2 * padding - Kw) + 1 # 计算输出宽度
# 对输入数据进行填充
x_pad = np.pad(
x, [(0, 0), (padding, padding), (padding, padding), (0, 0)], "constant"
)
# 初始化输出张量
y = np.zeros((N, Ho, Wo, Cout), dtype=x.dtype)
# 计算卷积
for n in range(N): # 遍历每个样本
for i in range(Ho): # 遍历输出高度
for j in range(Wo): # 遍历输出宽度
# 获取当前位置对应的输入数据块
patch = x_pad[n, i : i + Kh, j : j + Kw, :]
for cout in range(Cout): # 遍历每个输出通道
# 计算卷积结果
y[n, i, j, cout] = np.sum(patch * w[:, :, :, cout]) + b[cout]
cache = (x, w, b, x_pad) # 缓存用于反向传播
return y, cache
# 卷积层反向传播
def conv2d_backward(dy, cache):
# dy: 输出梯度,形状与卷积层输出相同
# cache: 前向传播保存的缓存数据
x, w, b, x_pad = cache # 解包缓存数据
N, H, W, C = x.shape # 获取输入数据的形状
Kh, Kw, _, Cout = w.shape # 获取卷积核的形状
_, Ho, Wo, _ = dy.shape # 获取输出梯度的形状
padding = 1 # 固定使用padding=1
# 初始化梯度
dx_pad = np.zeros_like(x_pad) # 填充后输入的梯度
dw = np.zeros_like(w) # 权重梯度
db = np.zeros_like(b) # 偏置梯度
# 计算梯度
for n in range(N): # 遍历每个样本
for i in range(Ho): # 遍历输出高度
for j in range(Wo): # 遍历输出宽度
# 获取当前位置对应的输入数据块
patch = x_pad[n, i : i + Kh, j : j + Kw, :]
for cout in range(Cout): # 遍历每个输出通道
# 累加权重梯度
dw[:, :, :, cout] += patch * dy[n, i, j, cout]
# 累加输入梯度
dx_pad[n, i : i + Kh, j : j + Kw, :] += (
w[:, :, :, cout] * dy[n, i, j, cout]
)
# 累加偏置梯度
db[cout] += dy[n, i, j, cout]
# 去除填充,得到原始输入梯度
dx = dx_pad[:, padding:-padding, padding:-padding, :]
return dx, dw, db
# ReLU激活函数前向传播
def relu_forward(x):
# x: 输入数据
y = np.maximum(0, x) # ReLU激活函数:max(0,x)
return y, x # 返回输出和缓存(输入x)
# ReLU激活函数反向传播
def relu_backward(dy, cache):
# dy: 输出梯度
# cache: 前向传播缓存的输入x
x = cache
# ReLU梯度:当x>0时为1,否则为0
return dy * (x > 0)
# 最大池化前向传播
def maxpool_forward(x):
# x: 输入数据,形状为(N,H,W,C)
N, H, W, C = x.shape # 获取输入形状
pool_size = (2, 2) # 固定池化窗口大小为2x2
ph, pw = pool_size
# 计算输出大小
Ho = (H - ph) // 2 + 1
Wo = (W - pw) // 2 + 1
# 初始化输出张量和掩码
y = np.zeros((N, Ho, Wo, C), dtype=x.dtype)
mask = {} # 记录最大值位置
# 计算池化
for n in range(N): # 遍历每个样本
for i in range(Ho): # 遍历输出高度
for j in range(Wo): # 遍历输出宽度
# 获取当前池化窗口
patch = x[n, i * 2 : i * 2 + ph, j * 2 : j * 2 + pw, :]
# 计算窗口内最大值
y[n, i, j, :] = patch.max(axis=(0, 1))
# 记录每个通道的最大值位置
for c in range(C):
idx = np.unravel_index(np.argmax(patch[:, :, c]), (ph, pw))
mask[(n, i, j, c)] = (i * 2 + idx[0], j * 2 + idx[1])
return y, (x, mask) # 返回输出和缓存
# 最大池化反向传播
def maxpool_backward(dy, cache):
# dy: 输出梯度
# cache: 前向传播保存的缓存
x, mask = cache # 解包缓存
# 初始化输入梯度
dx = np.zeros_like(x)
N, Ho, Wo, C = dy.shape # 获取输出梯度形状
# 计算梯度:仅在最大值位置传递梯度
for n in range(N): # 遍历每个样本
for i in range(Ho): # 遍历输出高度
for j in range(Wo): # 遍历输出宽度
for c in range(C): # 遍历每个通道
# 获取最大值位置并传递梯度
xi, xj = mask[(n, i, j, c)]
dx[n, xi, xj, c] += dy[n, i, j, c]
return dx
# 展平层前向传播
def flatten_forward(x):
# x: 输入数据,形状为(N,H,W,C)
# 将输入展平为(N, H*W*C)的二维张量
return x.reshape(x.shape[0], -1), x.shape
# 展平层反向传播
def flatten_backward(dy, cache):
# dy: 输出梯度,形状为(N, H*W*C)
# cache: 原始输入形状
# 将梯度重塑回原始输入形状
return dy.reshape(cache)
# 全连接层前向传播
def dense_forward(x, w, b):
# x: 输入数据,形状为(N, Din)
# w: 权重,形状为(Din, Dout)
# b: 偏置,长度为Dout
y = x.dot(w) + b # 线性变换:y = x·w + b
return y, (x, w, b) # 返回输出和缓存
# 全连接层反向传播
def dense_backward(dy, cache):
# dy: 输出梯度,形状为(N, Dout)
# cache: 前向传播缓存
x, w, b = cache # 解包缓存
# 计算各个参数的梯度
dx = dy.dot(w.T) # 输入梯度:dy·w^T
dw = x.T.dot(dy) # 权重梯度:x^T·dy
db = dy.sum(axis=0) # 偏置梯度:每个批次梯度的和
return dx, dw, db
# Softmax交叉熵损失前向传播
def softmax_crossentropy_forward(logits, labels):
# logits: 预测值,形状为(N, 类别数)
# labels: 真实标签,形状为(N, 类别数),one-hot编码
# 计算softmax概率,防止数 值溢出
ex = np.exp(logits - logits.max(axis=1, keepdims=True))
proba = ex / ex.sum(axis=1, keepdims=True)
N = logits.shape[0] # 样本数量
# 计算交叉熵损失
loss = -np.sum(labels * np.log(proba + 1e-12)) / N
return loss, (proba, labels, N) # 返回损失和缓存
# Softmax交叉熵损失反向传播
def softmax_crossentropy_backward(cache):
# cache: 前向传播缓存
proba, labels, N = cache # 解包缓存
# 计算梯度:(softmax概率 - 真实标签) / 样本数
return (proba - labels) / N
# 加载数据集
digits = load_digits()
X = digits.images.astype(np.float32) / 16.0 # 归一化到0~1范围
y = digits.target
X = X[..., np.newaxis] # 添加通道维度,变为(N,8,8,1)的形状
num_classes = 10
# 将标签转为one-hot编码
Y = np.eye(num_classes)[y]
# 划分训练集和验证集
X_train, X_val, Y_train, Y_val = train_test_split(X, Y, test_size=0.2, stratify=y)
y_val_labels = Y_val.argmax(axis=1) # 验证集标签(数字形式)
# 初始化网络参数(使用He初始化)
w1 = np.random.randn(3, 3, 1, 8).astype(np.float32) * np.sqrt(2 / 9) # 卷积层权重
b1 = np.zeros(8, dtype=np.float32) # 卷积层偏置
w2 = np.random.randn(4 * 4 * 8, num_classes).astype(np.float32) * np.sqrt(
2 / 128
) # 全连接层权重
b2 = np.zeros(num_classes, dtype=np.float32) # 全连接层偏置
# 超参数
epochs = 10 # 训练轮数
batch_size = 64 # 批次大小
lr = 0.1 # 学习率
# 训练过程
num_train = X_train.shape[0] # 训练样本数量
for ep in range(epochs):
# 打乱训练数据
perm = np.random.permutation(num_train)
X_train = X_train[perm]
Y_train = Y_train[perm]
# 小批量训练
for i in range(0, num_train, batch_size):
xb = X_train[i : i + batch_size] # 当前批次输入
yb = Y_train[i : i + batch_size] # 当前批次标签
# 前向传播
out1, c1 = conv2d_forward(xb, w1, b1) # 卷积层
out1r, c1r = relu_forward(out1) # ReLU激活
out2, c2 = maxpool_forward(out1r) # 最大池化
flat, cf = flatten_forward(out2) # 展平层
logits, c3 = dense_forward(flat, w2, b2) # 全连接层
loss, c4 = softmax_crossentropy_forward(logits, yb) # 损失计算
# 反向传播
dlogits = softmax_crossentropy_backward(c4) # 损失梯度
dflat, dw2, db2 = dense_backward(dlogits, c3) # 全连接层梯度
dout2 = flatten_backward(dflat, cf) # 展平层梯度
dout1r = maxpool_backward(dout2, c2) # 池化层梯度
dout1 = relu_backward(dout1r, c1r) # ReLU梯度
_, dw1, db1 = conv2d_backward(dout1, c1) # 卷积层梯度
# 参数更新(梯度下降)
w1 -= lr * dw1 # 更新卷积层权重
b1 -= lr * db1 # 更新卷积层偏置
w2 -= lr * dw2 # 更新全连接层权重
b2 -= lr * db2 # 更新全连接层偏置
# 在验证集上评估模型
out1, _ = conv2d_forward(X_val, w1, b1) # 卷积层前向传播
out1r, _ = relu_forward(out1) # ReLU激活
out2, _ = maxpool_forward(out1r) # 最大池化
flat, _ = flatten_forward(out2) # 展平层
logits, _ = dense_forward(flat, w2, b2) # 全连接层
preds = np.argmax(logits, axis=1) # 预测结果
acc = (preds == y_val_labels).mean() # 计算准确率
print(f"轮次 {ep+1}/{epochs} 损失={loss:.4f} 验证准确率={acc:.4f}")
AlexNet
在2012年ImageNet图像识别挑战赛中取得了巨大突破,是深度学习在计算机视觉领域爆发的标志。它比LeNet-5更深、更宽,使用了ReLU激活函数、Dropout等技术。
证明了GPU可以大幅提升神经网络的训练速度,证明了深度学习在计算机视觉领域的有效性。
《ImageNet Classification with Deep Convolutional Neural Networks》截止2025年,谷歌学术总引用次数排名第3。
ReLU激活函数
Dropout
ResNet
ResNet引入了“残差连接”(Residual Connection),解决了深度神经网络中梯度消失和网络退化的问题,使得可以训练非常深的网络(例如152层)。
是深度学习领域最重要的突破之一,使得训练超深网络成为可能,极大地提升了图像识别的准确率。
《Deep Residual Learning for Image Recognition》截止2025年,机器视觉标签下,谷歌学术总引用次数排名第1。
残差连接
Adam
提出了一种高效且自适应的随机优化算法,通过结合一阶矩(动量)和二阶矩(自适 应学习率)的估计,为每个模型参数独立调整学习率。
大幅简化了深度学习模型的训练过程,减少了手动调整学习率的需要,并保证了在稀疏梯度(尤其是在 NLP 中)下的稳定收敛。
成为深度学习,特别是自然语言处理(NLP)和 Transformer 架构的默认优化器之一。
《Adam: A Method for Stochastic Optimization》截止2025年,谷歌学术总引用次数排名第6。
Batch Normalizations
提出了一种规范化网络层输入的方法,解决了训练深度网络时“内部协变量偏移”(Internal Covariate Shift)的问题,即中间层输入的分布在训练过程中不断变化的现象。
使得研究人员能够使用更高的学习率和更深的(更复杂的)网络架构进行训练,同时极大地加速了模型的收敛速度。
有效充当正则化器,减少了对 Dropout 等其他正则化技术的依赖。
《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》截止2025年,谷歌学术总引用次数排名第8。
LSTM
LSTM(长短期记忆网络)是深度学习中最重要的架构之一。1997 年由 Hochreiter 和 Schmidhuber 提出,专门设计用于解决传统 RNN 的梯度消失问题,能够学习长期依赖关系。
LSTM 的核心创新:
- 门控机制:通过遗忘门、输入门、输出门控制信息流动
- 细胞状态:信息高速公路,让梯度能够长距离传播
- 长期记忆:有效捕获序列中的长期依赖关系
《Long Short-Term Memory》论文截止 2025 年,谷歌学术总引用次数排名第 5,是深度学习领域最具影响力的论文之一。
Vision Transformer
在现代深度学习中,文本、图片、音频、视频都是序列。
Vision Transformer(ViT)首次成功将 Transformer 架构(原用于 NLP)应用于图像分类任务,挑战了 CNN 在计算机视觉领域的主导地位。
ViT的成功是深度学习领域方法论的一次重大转变,标志着“大一统”架构(即 Transformer)开始统治 NLP 和 CV 两个领域。
《An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale》在 2020 年及之后发表的论文中,谷歌学术总引用次数排名第1。