AI入门教程03.01:全连接神经网络

先来个简单的,写一个全连接神经网络。模型如下图

fc

使用MNIST数据集,内容为手写数字,1通道灰度图。

模型流程为

  • 将1通道28x28图片转换为1通道宽1高784的图片
  • 将784图片缩小为1通道宽1高256图片
  • 将256图片限制为1通道宽1高10图片对应0-9这10个结果

模型开发

将神经网络模型转换为对应代码的基本流程。

硬件选择

根据硬件情况选择加速方式

1
2
3
4
if torch.cuda.is_available():
device = torch.device("cuda") # 如果GPU可用,则使用CUDA
else:
device = torch.device("cpu") # 如果GPU不可用,则使用CPU

加载数据集并进行预处理

缩放、裁剪、归一化数据集中的数据。

MNIST数据集图片大小相同,这一步跳过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 加载MNIST数据集的训练集
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=transforms.ToTensor(),
)
# 加载MNIST数据集的测试集
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=transforms.ToTensor(),
)

# batch大小
batch_size = 64

# 创建dataloader
train_dataloader = torch.utils.data.DataLoader(training_data, batch_size=batch_size)
test_dataloader = torch.utils.data.DataLoader(test_data, batch_size=batch_size)

如果指定位置没有数据集就自动下载。

定义模型

根据神经网络模型定义编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
# 定义神经网络模型
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(784, 256)
self.fc2 = nn.Linear(256, 10)

def forward(self, x):
x = x.view(-1, 784)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
return x

全连接使用Linear函数,只要是784->256->10

使用view函数将二维图片转换为一维数组。

全连接后使用激活函数获得数据。

激活函数

激活函数(Activation Function)是神经网络中非常重要的一部分,它主要用于在神经网络节点(也称为神经元)中引入非线性因素。没有激活函数,神经网络中的每一层都仅仅是上一层的线性变换,这会导致无论神经网络有多少层,输出都是输入的线性组合,无法学习和模拟复杂的非线性关系。

激活函数的主要作用包括:

  • 引入非线性:使得神经网络能够学习和表示几乎任何复杂的函数映射关系,包括非线性的输入输出关系。
  • 控制输出幅度:例如,将输出值限制在一个特定范围内(如0到1之间),这有助于模型训练的稳定性和效率。

常见的激活函数包括:

  • Sigmoid函数:输出值在(0, 1)之间,常用于二分类问题的输出层。但由于其梯度在两端趋于0(梯度消失问题),且计算量大,现在较少在深度学习中使用。
  • Tanh函数:输出值在(-1, 1)之间,是Sigmoid函数的改进版,解决了输出不以0为中心的问题,但梯度消失问题依然存在。
  • ReLU(Rectified Linear Unit)函数:当输入大于0时,输出等于输入;当输入小于等于0时,输出为0。ReLU函数计算简单,且在输入为正数时梯度不为0,缓解了梯度消失问题,是目前最常用的激活函数之一。但ReLU在输入小于等于0时,神经元不会被激活(称为“死亡ReLU”问题)。
  • Softmax函数:通常用于多分类问题的输出层,它将一个含任意实数的K维向量压缩到另一个K维实向量中,使得每一个元素的范围都在(0,1)之间,并且所有元素的和为1。这样可以将输出解释为概率。

初始化模型、损失函数和优化器

1
2
3
4
5
# 实例化模型并定义损失函数和优化器
net = Net().to(device)
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数适合分类任务
optimizer = torch.optim.SGD(net.parameters(), lr=0.01)
#optimizer = optim.Adam(model.parameters(), lr=learning_rate)

损失函数

损失函数(Loss Function)是在机器学习和深度学习中用来评估模型预测值与真实值之间差异的函数。它是优化算法的目标函数,优化算法通过最小化损失函数来更新模型的参数,从而提高模型的预测准确性。

损失函数的作用可以概括为以下几点:

  • 量化预测误差:损失函数提供了一个量化模型预测误差的方法,使得我们可以通过数值来评估模型的性能。
  • 指导模型优化:在训练过程中,损失函数的值被用来指导模型的优化。优化算法(如梯度下降)通过计算损失函数关于模型参数的梯度,并根据这些梯度来更新模型的参数,以减小损失函数的值。
  • 作为模型选择的依据:在模型选择阶段,我们可以比较不同模型在相同数据集上的损失函数值,从而选择性能更好的模型。

常见的损失函数包括:

  • 均方误差(Mean Squared Error, MSE):用于回归问题,计算预测值与真实值之间差的平方的平均值。MSE对较大的误差给予更大的惩罚。
  • 均方根误差(Root Mean Squared Error, RMSE):MSE的平方根,也是用于回归问题,与MSE具有相同的性质,但量纲与真实值相同,更易于理解。
  • 平均绝对误差(Mean Absolute Error, MAE):计算预测值与真实值之间差的绝对值的平均值。与MSE相比,MAE对异常值(即离群点)的敏感度较低。
  • 交叉熵损失(Cross-Entropy Loss):主要用于分类问题,特别是当输出层使用softmax函数时。交叉熵损失函数衡量的是模型预测的概率分布与真实标签的概率分布之间的差异。
  • 对数损失(Log Loss):也称为对数似然损失,是交叉熵损失的一种特殊情况,用于二分类问题。它衡量的是模型预测的概率分布与真实标签之间的负对数似然值。
  • 合页损失(Hinge Loss):主要用于支持向量机(SVM)的分类问题,特别是当输出是决策函数的原始分数时。合页损失鼓励分类器对正确的类别有更高的分数,同时对错误的类别有较低的分数,并且有一定的间隔。

优化器

优化器(Optimizer)是深度学习和机器学习中的一个核心概念,其主要作用是更新神经网络的权重,以减少或最小化损失函数(Loss Function)的值。损失函数衡量了模型的预测值与真实值之间的差异,而优化器的目标则是通过调整网络参数来最小化这个差异,从而提高模型的准确性和性能。

优化器的作用

  • 更新网络权重:在神经网络训练过程中,优化器利用损失函数相对于模型参数的梯度(即损失函数的导数)来更新模型的权重,使模型逐渐逼近最优解。
  • 提高模型准确性:通过不断优化权重,模型能够更好地拟合训练数据,从而提高在新数据上的预测准确性。
  • 改善学习速率:优化器能够根据损失函数的梯度动态调整学习速率,帮助模型在训练过程中避免陷入局部最小值或过度拟合。

优化器有多种类型,每种类型都有其独特的算法和适用场景。以下是一些常见的优化器类型:

  • 梯度下降(Gradient Descent, GD):最基本的优化器,它按照梯度的反方向更新参数。然而,传统的梯度下降算法在每次迭代时都使用全部训练数据来计算梯度,这在大规模数据集上可能会导致计算效率低下。
  • 随机梯度下降(Stochastic Gradient Descent, SGD):每次迭代只使用一个或少量训练样本来计算梯度并更新参数。这种方法显著提高了计算效率,但可能导致训练过程中的波动较大。
  • 小批量梯度下降(Mini-Batch Gradient Descent, MBGD):介于GD和SGD之间的一种折中方法,每次迭代使用一个小批量(mini-batch)的训练样本来计算梯度并更新参数。这种方法既保持了较高的计算效率,又在一定程度上减小了训练过程中的波动。
  • 动量(Momentum):在SGD的基础上加入了动量项,帮助加速SGD在相关方向上的收敛,并减小震荡。动量法通过引入一个累计梯度的指数加权平均,将过去的梯度信息考虑进当前的参数更新中,从而增加稳定性和提高训练效率。
  • 自适应梯度下降(Adaptive Gradient, Adagrad):对不同参数使用不同的学习率,对于更新频率较低的参数施以较大的学习率,对于更新频率较高的参数使用较小的学习率。这种方法特别适合处理稀疏数据。
  • RMSprop:解决了Adagrad学习率不断减小到极小的问题,通过引入衰减系数来限制历史信息的无限积累。RMSprop通过维护模型梯度平方的指数加权平均来调整学习率。
  • Adam(Adaptive Moment Estimation):结合了AdaGrad和Momentum两种优化算法的优点,能够快速收敛并且减少训练时间。Adam优化器计算出每个参数的独立自适应学习率,不需要手动调整学习率的大小。

优化器的选择对模型的训练速度和最终性能有很大影响。在选择优化器时,通常需要考虑模型的具体需求、数据的特性以及训练效率等因素。不同的优化器可能适合不同的任务和数据集。例如,对于大规模数据集和复杂模型,Adam优化器通常表现出色;而对于一些特定问题,简单的SGD加动量可能会带来更好的性能。

训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for epoch in range(10):
index = 0
for i,inputs, labels in train_dataloader:
inputs, labels = inputs.to(device), labels.to(device)

# 前向传播
outputs = net(inputs)
loss = criterion(outputs, labels)

# 反向传播和优化
optimizer.zero_grad() #梯度归零
loss.backward() #反向传播
optimizer.step() #优化
index+=1

print(f'Epoch [{epoch}], Setp [{index}/{len(train_dataloader)}], Loss: {loss.item():.4f}')

训练时终端会输出当前训练参数,mbox_loss/loss应该是整体下降的,如果是下面的输出是不正常的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
I060515:11:51.840446 34915 solver.Cpp:259] Train net output #0:mbox_loss =nan(*1 =nan loss)
I060515:11:51.840451 34915 sgd_solver.Cpp:138]Iteration 160,lr =0.001
I060515:11:52.567498 34915 solver.Cpp:243]Iteration 170,loss =nan
I060515:11:52.567518 34915 solver.Cpp:259] Train net output #0:mbox_loss =nan(*1 =nan loss)
I060515:11:52.567523 34915 sgd_solver.Cpp:138]Iteration 170,lr =0.001
I060515:11:53.282116 34915 solver.Cpp:243]Iteration 180,loss =nan
I060515:11:53.282138 34915 solver.Cpp:259] Train net output #0:mbox_loss =nan(*1 =nan loss)
I060515:11:53.282143 34915 sgd_solver.Cpp:138]Iteration 180,lr =0.001
I060515:11:54.002791 34915 solver.Cpp:243]Iteration 190,loss =nan
I060515:11:54.002841 34915 solver.Cpp:259] Train net output #0:mbox_loss =nan(*1 =nan loss)
I060515:11:54.002846 34915 sgd_solver.Cpp:138]Iteration 190,lr =0.001
I060515:11:54.732021 34915 solver.Cpp:243]Iteration 200,loss =nan
I060515:11:54.732043 34915 solver.Cpp:259] Train net output #0:mbox_loss =nan(*1 =nan loss)
I060515:11:54.732048 34915 sgd_solver.Cpp:138]Iteration 200,lr =0.001
I060515:11:55.460265 34915 solver.Cpp:243]Iteration 210,loss =nan
I060515:11:55.460289 34915 solver.Cpp:259] Train net output #0:mbox_loss =nan(*1 =nan loss)
I060515:11:55.460292 34915 sgd_solver.Cpp:138]Iteration 210,lr =0.001
I060515:11:56.187893 34915 solver.Cpp:243]Iteration 220,loss =nan
I060515:11:56.187916 34915 solver.Cpp:259] Train net output #0:mbox_loss =nan(*1 =nan loss)
I060515:11:56.187919 34915 sgd_solver.Cpp:138]Iteration 220,lr =0.001
I060515:11:56.907727 34915 solver.Cpp:243]Iteration 230,loss =nan
I060515:11:56.907747 34915 solver.Cpp:259] Train net output #0:mbox_loss =0(*1 =0 loss)
I060515:11:56.907752 34915 sgd_solver.Cpp:138]Iteration 230,lr =0.001

可以考虑缩小lr值或者batch值

预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 加载模型并测试
correct = 0
total = 0
net.eval()
with torch.no_grad():
for inputs, labels in test_dataloader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = net(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

# 结果评估
print('Accuracy: %d %%' % (100 * correct / total))

运行

迭代100次,运行输出为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Epoch [99], Setp [917/938],Loss: 0.1756
Epoch [99], Setp [918/938],Loss: 0.2033
Epoch [99], Setp [919/938],Loss: 0.3347
Epoch [99], Setp [920/938],Loss: 0.1615
Epoch [99], Setp [921/938],Loss: 0.1972
Epoch [99], Setp [922/938],Loss: 0.2536
Epoch [99], Setp [923/938],Loss: 0.1282
Epoch [99], Setp [924/938],Loss: 0.1388
Epoch [99], Setp [925/938],Loss: 0.3028
Epoch [99], Setp [926/938],Loss: 0.2694
Epoch [99], Setp [927/938],Loss: 0.4310
Epoch [99], Setp [928/938],Loss: 0.1672
Epoch [99], Setp [929/938],Loss: 0.2218
Epoch [99], Setp [930/938],Loss: 0.2361
Epoch [99], Setp [931/938],Loss: 0.3474
Epoch [99], Setp [932/938],Loss: 0.2533
Epoch [99], Setp [933/938],Loss: 0.4752
Epoch [99], Setp [934/938],Loss: 0.1587
Epoch [99], Setp [935/938],Loss: 0.0987
Epoch [99], Setp [936/938],Loss: 0.2242
Epoch [99], Setp [937/938],Loss: 0.2413
Epoch [99], Setp [938/938],Loss: 0.1720
Accuracy: 88 %

完整代码

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms

if torch.cuda.is_available():
device = torch.device("cuda") # 如果GPU可用,则使用CUDA
else:
device = torch.device("cpu") # 如果GPU不可用,则使用CPU

# 定义神经网络模型
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(784, 256)
self.fc2 = nn.Linear(256, 10)

def forward(self, x):
x = x.view(-1, 784)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x


# 加载MNIST数据集的训练集
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=transforms.ToTensor(),
)
# 加载MNIST数据集的测试集
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=transforms.ToTensor(),
)

# batch大小
batch_size = 64

# 创建dataloader
train_dataloader = torch.utils.data.DataLoader(training_data, batch_size=batch_size)
test_dataloader = torch.utils.data.DataLoader(test_data, batch_size=batch_size)

# 实例化模型并定义损失函数和优化器
net = Net().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.01)

# 加载数据并训练
for epoch in range(10):
index=0
for inputs, labels in train_dataloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
index+=1
print(f'Epoch [{epoch}], Setp [{index}/{len(train_dataloader)}],Loss: {loss.item():.4f}')

# 加载模型并测试
net.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in test_dataloader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = net(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

# 结果评估
print('Accuracy: %d %%' % (100 * correct / total))

AI入门教程03.01:全连接神经网络
https://blog.jackeylea.com/ai/how-to-build-a-fully-connected-nn/
作者
JackeyLea
发布于
2024年6月20日
许可协议