基于pytorch的深度学习的MNIST手写数字图像分类
基于pytorch的深度学习图像分类
深度学习图像分类的原理基于神经网络和反向传播算法。神经网络包括输入层、隐藏层和输出层,通过前向传播将输入数据逐层传递,经过权重和偏差的处理以及激活函数的作用,最终得到分类预测结果。损失函数衡量了预测结果与实际标签的差异,而反向传播算法利用梯度下降等优化算法更新网络参数,使得损失函数逐渐减小,实现模型的优化。通过不断迭代训练过程,深度学习模型能够自动地从数据中学习到图像的特征,从而实现对图像的准确分类。
PyTorch是一个开源的深度学习框架,它以动态计算图和简洁的API设计为特点,使得构建、训练和部署神经网络模型变得高效而灵活。其自动求导功能简化了梯度计算过程,同时支持动态计算,适用于处理变长输入数据的场景。
以下是使用pytorch来实现一个简单的手写数字集的深度学习图像分类。
任务定义
使用pytorch构建神经网络使用深度学习的方法实现对手写数字集MNIST的图像分类。
MNIST数据集
MNIST数据集是一个广泛用于机器学习和深度学习领域的经典数据集,它包含了手写数字图像及其对应的标签。以下是MNIST数据集的一些特点:
- 图像内容:MNIST数据集包含了0到9的手写数字图像,每个数字都是黑白的,分辨率为28x28像素。
- 训练集和测试集:MNIST数据集被分为训练集和测试集两部分,分别包含60,000和10,000张图像。这样的划分使得可以用训练集来训练模型,用测试集来评估模型的性能。
- 标签信息:每张图像都有一个对应的标签,表示图像所代表的数字(0到9之间的一个数字)。
- 可视化数据集:
深度学习图像分类任务
使用深度学习实现对手写数字集的图像分类的原理如下:
- 特征学习能力:深度学习模型拥有强大的特征学习能力,能够从原始数据中学习到复杂的抽象特征,例如边缘、纹理等。对于手写数字图像分类,这种特征学习能力非常重要,因为人工定义特征提取规则在这种情况下往往会非常困难。
- 层级特征提取:深度学习模型通过多层次的神经网络结构,可以逐层地提取图像中的特征,从简单的特征(如边缘)到复杂的特征(如图案和形状)。这种层级特征提取使得模型能够理解图像的更高层次的语义信息。
- 非线性映射:深度学习模型中的激活函数引入了非线性变换,使得模型可以学习到复杂的非线性关系。这对于解决手写数字分类这种非线性可分问题非常关键。
- 端到端学习:深度学习模型可以通过端到端的方式直接从原始数据中学习到最终的分类结果,而无需手动设计复杂的特征提取过程。这极大地简化了图像分类任务的流程。
- 大规模数据集支持:深度学习模型通常需要大量的数据来进行训练,而手写数字集通常拥有足够的样本以供模型学习。
定义网络
# 定义神经网络
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__init__()
self.fc1 = nn.Linear(28*28, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
定义了一个名为NeuralNetwork
的类用于表示该神经网络,由3给全连接层组成。输入是28x28的图像数据,总共784个节点(对应于28x28的像素)。第一层的输出特征数量是256,有256个神经元,对输入的特征进行非线性变换,从而提取更高级别的特征。第二层有128个神经元,与第一个隐藏层类似,它进一步对特征进行变换和提取。输出层包含10个神经元,对应于10个手写数字的类别,每个神经元的输出表示属于相应类别的概率。(其中,选择256和128作为隐藏层的神经元数量,是基于经验和实验调优的结果。这个选择并非唯一,可以根据具体问题和数据集的特点进行调整。)
forward
方法展示了特征的传播过程。首先,将输入x
传递给第一个全连接层"fc1"。这里的torch.relu
是激活函数,它对fc1(x)
的输出进行非线性变换,得到了一个新的张量x
。接着,将上一层的输出x
传递给第二个全连接层fc2
,再次经过激活函数的处理。最后,将上一层的输出x
传递给输出层fc3
。在多分类问题中,通常会使用softmax
激活函数来得到类别的概率分布。在PyTorch中,CrossEntropyLoss
函数会自动对模型的输出进行Softmax
操作,因此在传入该损失函数时,通常不需要在模型的最后一层加入Softmax
激活函数。
准备数据
# MNIST数据集
# 数据变换
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
# 训练集
train_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
# 验证集
val_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
val_loader = torch.utils.data.DataLoader(dataset=val_dataset, batch_size=batch_size, shuffle=False)
# 测试集
test_dataset = torchvision.datasets.MNIST(root='./data', train=False, transform=transform, download=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)
首先定义数据预处理,在本例中,这行代码的作用是将输入的图像数据预处理成在训练神经网络时更适合使用的格式:将图像转换成张量,然后进行归一化处理。这样做可以帮助提升模型的训练效果。
随后,导入数据集,将数据划分为训练集、验证集和测试集。
实例化网络并定义损失函数和优化器
# 超参数
num_epochs = 20
batch_size = 600
learning_rate = 0.0001
# 初始化网络实例
net = NeuralNetwork()
net.cuda()
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate)
设置训练所需要的超参数,实例化网络并将网络放到GPU中,定义损失函数和优化器。
CrossEntropyLoss
在许多分类任务中被广泛使用,特别是在多类别分类问题中。在训练神经网络时,通常会将这个损失函数作为优化器的目标,通过反向传播算法来调整网络的参数,使得损失函数降低,从而提升模型的性能。CrossEntropyLoss
结合了Softmax
激活函数和负对数似然损失(Negative Log Likelihood,NLL Loss),用于衡量模型输出与真实标签之间的差异。假设模型输出的类别概率分布为
P
P
P,对应真实标签的概率分布为
Q
Q
Q,则交叉熵损失可以表示为:
H
(
P
,
Q
)
=
−
∑
i
Q
i
log
(
P
i
)
H(P, Q) = - \sum_{i} Q_i \log(P_i)
H(P,Q)=−i∑Qilog(Pi)
其中:
- Q i Q_i Qi 表示真实标签的第 i i i个类别的概率(取值为0或1,表示该类别是否是真实标签)。
- P i P_i Pi表示模型预测的第 i i i个类别的概率。
Adam(Adaptive Moment Estimation)是一种自适应学习率的优化算法,用于调整神经网络模型中每个参数的学习率,以便在训练过程中更有效地更新参数。它结合了Momentum和RMSprop两种优化算法的思想,并在此基础上进行了改进。Adam算法的参数更新公式如下:
m
t
=
β
1
⋅
m
t
−
1
+
(
1
−
β
1
)
⋅
g
t
v
t
=
β
2
⋅
v
t
−
1
+
(
1
−
β
2
)
⋅
g
t
2
m
^
t
=
m
t
1
−
β
1
t
v
^
t
=
v
t
1
−
β
2
t
p
t
+
1
=
p
t
−
η
v
^
t
+
ϵ
⋅
m
^
t
m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t \\ v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g_t^2 \\ \hat{m}_t = \frac{m_t}{1 - \beta_1^t} \\ \hat{v}_t = \frac{v_t}{1 - \beta_2^t} \\ p_{t+1} = p_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \cdot \hat{m}_t
mt=β1⋅mt−1+(1−β1)⋅gtvt=β2⋅vt−1+(1−β2)⋅gt2m^t=1−β1tmtv^t=1−β2tvtpt+1=pt−v^t+ϵη⋅m^t
其中:
- m t m_t mt 和 v t v_t vt 分别代表一阶矩估计和二阶矩估计。
- β 1 \beta_1 β1和 β 2 \beta_2 β2是用于控制一阶矩和二阶矩的衰减率,通常取0.9和0.999。
- g t g_t gt 是当前时刻的梯度。
- m ^ t \hat{m}_t m^t和 v ^ t \hat{v}_t v^t是偏差修正后的估计值。
- η \eta η是学习率。
- ϵ \epsilon ϵ是一个很小的常数,用于防止除零错误。
训练
# 训练网络
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
# 将图像数据展平为一维向量
images = images.reshape(-1, 28*28).cuda()
labels = labels.cuda()
# 前向传播
outputs = net(images)
loss = criterion(outputs, labels)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 打印训练信息
if (i+1) % 100 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {loss.item():.4f}')
if (epoch+1) % 5 == 0:
# 测试准确率
accuracy = test_accuracy()
accuracies.append(accuracy)
print(f'Epoch [{epoch+1}/{num_epochs}],\n Test Accuracy: {accuracy:.2f}%')
print('训练完成')
以上就是最基础的训练过程。首先设置外循环和内循环,外循环用于控制训练的轮次,内循环用于遍历数据集。随后将图像数据和标签输入到网络中进行学习。每100个批次打印一次训练信息,其中包含当前epoch、当前batch、当前loss。并设置每5个轮次,进行一次模型在验证集上的准确率评估。
测试
测试的程序为:
def test_accuracy():
net.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in val_loader:
images = images.reshape(-1, 28*28).cuda()
labels = labels.cuda()
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
accuracy = 100*correct/total
print(f'Test Accuracy: {accuracy:.2f}%')
return accuracy
计算正确预测的个数与总数的占比,即为准确率。除此之外,也可以使用精确度(Precision)、召回率(Recall)和F1分数(F1 Score)来评估模型的性能。它们的定义和计算公式如下:
- 精确度(Precision):
- 精确度是指在所有被模型预测为正类别的样本中,真正属于正类别的样本所占的比例。
- 公式:
P
r
e
c
i
s
i
o
n
=
T
P
T
P
+
F
P
Precision = \frac{TP}{TP + FP}
Precision=TP+FPTP
- T P TP TP:真正例 (True Positives) - 被模型正确地预测为正类别的样本数。
- F P FP FP:假正例 (False Positives) - 被模型错误地预测为正类别的负样本数。
- 精确度的取值范围在0到1之间,越高越好。高精确度表示模型在正类别的预测中有很少错误。
- 召回率(Recall):
- 召回率是指在所有实际正类别的样本中,被模型正确地预测为正类别的样本所占的比例。
- 公式:
R
e
c
a
l
l
=
T
P
T
P
+
F
N
Recall = \frac{TP}{TP + FN}
Recall=TP+FNTP
- T P TP TP:真正例 (True Positives) - 被模型正确地预测为正类别的样本数。
- F N FN FN:假反例 (False Negatives) - 被模型错误地预测为负类别的正样本数。
- 召回率的取值范围也在0到1之间,越高越好。高召回率表示模型能够捕获更多实际正类别的样本。
- F1分数(F1 Score):
- F1分数是精确度和召回率的调和平均,它综合了这两个指标的信息。
- 公式: F 1 S c o r e = 2 ⋅ P r e c i s i o n ⋅ R e c a l l P r e c i s i o n + R e c a l l F1 Score = \frac{2 \cdot Precision \cdot Recall}{Precision + Recall} F1Score=Precision+Recall2⋅Precision⋅Recall
- F1分数的取值范围也在0到1之间,越高越好。高F1分数表示模型在精确度和召回率之间取得了一个良好的平衡。
所以可以在训练结束后加上精确度(Precision)、召回率(Recall)和F1分数(F1 Score)的测试代码:
# 计算精确度、召回率、F1分数
y_true = []
y_pred = []
with torch.no_grad():
for images, labels in test_loader:
images = images.reshape(-1, 28*28).cuda()
labels = labels.cuda()
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
y_true.extend(labels.tolist())
y_pred.extend(predicted.tolist())
precision = precision_score(y_true, y_pred, average='weighted')
recall = recall_score(y_true, y_pred, average='weighted')
f1 = f1_score(y_true, y_pred, average='weighted')
print(f'Precision: {precision:.4f},\n Recall: {recall:.4f},\n F1 Score: {f1:.4f}')
例子
训练
设置超参数
# 超参数
num_epochs = 60
batch_size = 600
learning_rate = 0.0001
然后开始训练,训练完成后获得准确率为:97.63%
训练loss曲线和准确度曲线如下:
测试
Precision: 0.9763,
Recall: 0.9763,
F1 Score: 0.9763
从测试集中随机选取20张图像,预测其结果并可视化
其中Predicted
为预测结果,True Label
为真实标签。
关于我
更多内容请关注我的公众号:AI小火车