2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
In order to better understand neural networks, it is very appropriate to start from the small task of handwritten digit recognition to figure out the working principles and general processes of neural networks layer by layer.
This article will complete a handwritten digit recognition task to illustrate how to design, implement and train a standard feedforward neural network, in order to have a more concrete and intuitive understanding of neural networks.
Specifically, we need to design and train a 3-layer neural network. This neural network will take digital images as input. After the neural network calculates, it will recognize the number in the image, thereby realizing the classification of digital images:
In this process, three aspects are mainly explained: the design and implementation of neural networks, the preparation and processing of training data, and the training and testing process of the model.
In order to design a neural network to process image data, it is necessary to first clarify the size and format of the input image data.
The images we are going to process are gray channel images of size 28 × 28 pixels (the format of the MNIST dataset itself).
Such a gray image includes 2828 = 784 data points, we first need to flatten it into 1A vector of size 784:
Then we input this vector into the neural network. We will use a three-layer neural network to process the vector x corresponding to the image. The input layer needs to receive the 784-dimensional image vector x. Each dimension of data in x is received by a neuron, so the input layer must contain 784 neurons:
The hidden layer is used for feature extraction, processing the input feature vector into a higher-level feature vector.
Since the handwritten digit image is not complicated, the number of neurons in the hidden layer is set to 256, so that there will be a 784*256 linear layer between the input layer and the hidden layer:
It can transform a 784-dimensional input vector into a 256-dimensional output vector, which will continue to propagate forward to the output layer.
Since the digital image is ultimately to be recognized as ten possible numbers from 0 to 9, the output layer needs to define 10 neurons corresponding to these ten numbers:
After the 256-dimensional vector is calculated by the linear layer between the hidden layer and the output layer, a 10-dimensional output result is obtained. This 10-dimensional vector represents the predicted scores of 10 numbers:
In order to continue to get the predicted probabilities of the 10 numbers, we also need to input the output of the output layer into the softmax layer. The softmax layer will convert the 10-dimensional vector into 10 probability values P0 to P9. Each probability value corresponds to a number, that is, the possibility that the input image is a certain number. In addition, the sum of the 10 probability values P0 to P9 added together is 1, which is determined by the nature of the softmax function:
The above is the design idea of the neural network. Next, we will use the PyTorch framework to implement it.
First, let's implement our neural network:
code show as below:
import torch
from torch import nn
# 定义神经网络
class NetWork(nn.Module):
def __init__(self):
super().__init__()
# 线性层1,输入层和隐藏层之间的线性层
self.layer1 = nn.Linear(784, 256)
# 线性层2,隐藏层和输出层之间的线性层
self.layer2 = nn.Linear(256, 10)
# 这里没有直接定义 softmax 层
# 这是因为后面会使用 CrossEntropyLoss 损失函数
# 在这个损失函数中会实现 softmax 的计算
def forward(self, x):
x = x.view(-1, 28 * 28) # 使用view 函数将 x 展平
x = self.layer1(x) # 将 x 输入至 layer1
x = torch.relu(x) # 使用 ReLu 函数激活
return self.layer2(x) # 输入至 layer2 计算结果
Next, prepare the dataset:
To obtain the dataset, you can also download it from the PyTorch official website in the following way:
# 准备数据集
train_data = torchvision.datasets.MNIST(root='data', train=True, download=True)
test_data = torchvision.datasets.MNIST(root='data', train=False, download=True)
The data downloaded in this way will be automatically stored in train_data and test_data, and a corresponding toolkit will be downloaded in the directory we specified in the code:
However, this is not a universal method. In future work and study, there are many data sets that we need to operate and process by ourselves, which is not as convenient as the above. Therefore, here is a more universal method, which is to download the original data set from the official website. Yes, it is a lot of pictures!
We save the downloaded data into the following two folders:
We save the data into two directories, train and test. Train has 60,000 data and test has 10,000 data, which are used for model training and testing respectively.
There are ten subdirectories in both the train and test directories, and the names of the subdirectories correspond to the numbers in the images. For example, the image of the number 3 is saved in the folder named 3:
Where the name of the image is a random string signature.
After completing the data preparation, the data reading function is implemented. When learning this part, beginners only need to know the general data processing flow.
The code is implemented as follows:
# 首先实现图像的预处理pipeline
transform = torchvision.transforms.Compose([
torchvision.transforms.Grayscale(num_output_channels=1), # 转换为单通道灰度图像
torchvision.transforms.ToTensor() # 转换为PyTorch支持的张量
])
# 这是方式一准备数据集的方式嗷
train_data = torchvision.datasets.MNIST(root='data', train=True, download=True, transform=transform)
test_data = torchvision.datasets.MNIST(root='data', train=False, download=True, transform=transform)
# 下面这是方式二准备数据集的方式
# 使用 ImageFolder 函数读取数据文件夹,构建数据集 dataset
# 这个函数会将保存数据的文件夹的名字,作为数据的标签,组织数据
# 例如,对于名字为 3 的文件夹
# 就会将 3 作为文件夹中图像数据的标签,和图像配对用于后续的训练,使用起来非常方便
train_dataset = torchvision.datasets.ImageFolder(root='./mnist/train', transform=transform)
test_dataset = torchvision.datasets.ImageFolder(root='./mnist/test', transform=transform)
# 不管使用哪种准备数据集的方式,最后的效果都是一样的
# 打印它们的长度看一下
print(len(train_data))
print(len(test_data))
Take a look at the running results:
You can see that both the training data and the test data have been obtained, which is exactly what we said before.
Then we use train_loader to read small batches of data:
# 使用 train_loader 实现小批量的数据读取
# 这里设置小批量的大小,batch_size = 64。
# 也就是每个批次包括 64 个数据
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
# 打印 train_loader 的长度
print(" train loader length: ", len(train_loader))
# 60000 个训练数据,如果每个小批量读入 64 个样本,那么 60000 个数据会被分为 938 组
# 计算 938 * 64 = 60032,这说明最后一组会不够 64 个数据
The results are as follows:
We can then loop over the train_loader to get each mini-batch of data:
# 循环遍历 train_loader
# 每一次循环,都会取出 64 个图像数据,作为一个小批量 batch
for batch_idx, (data, label) in enumerate(train_loader):
if batch_idx == 3: # 打印前三个 batch 观察
break
print("batch_idx: ", batch_idx)
print("data.shape: ", data.shape) # 数据的尺寸
print("label: ", label.shape) # 图像中的数字
print(label)
Here is a simple explanation of the above loop statement to make it clearer:
enumerate(): This is a built-in function in Python that combines a traversable data object (such as a list, tuple, or string) into an index sequence, listing both the data and the data subscript, that is, getting both the index and the value. Here, it is used to traverse each batch in train_loader, where batch_idx is the index of the current batch (starting from 0), and (data, label) is the data and label of the current batch.
for batch_idx, (data, label) in enumerate(train_loader):: This loop statement means that for each batch in train_loader, the code in the loop body is executed.In each loop iteration, batch_idx is the index of the current batch, and (data, label) is the data and label of the current batch.. data is usually a tensor containing multiple data points, each of which is a sample; label is the label tensor corresponding to these data points, which is used as the target value in supervised learning.
The operation effect is as follows:
From the running results, we can see that:
1. batch_idx = 0 means it is the first batch of data
2. data.shape indicates that the size of the data is [64, 1, 28, 28]
The above size means that each batch of data includes 64 images, each image has 1 gray channel, and the image size is 28*28.
3. label.shape means that there are 64 numbers in the batch, and the total number of labels corresponding to them is 64. Each number has a label.
Note the difference. There are definitely only 9 real label categories, because there are only numbers from 1 to 9, but here it means that there are 64 label values.
4. The tensor array represents the label value corresponding to each of the 64 digital images.
After the previous preparation, we can start training and testing the model.
Here is the training code:
# 在使用 PyTorch 训练模型时,需要创建三个对象
model = NetWork() # 1、模型本身,它就是我们设计的神经网络
optimizer = torch.optim.Adam(model.parameters()) # 2、优化器,优化模型中的参数
criterion = nn.CrossEntropyLoss() # 3、损失函数,分类问题使用交叉熵损失误差
# 开始训练模型
for epoch in range(10): # 外层循环,代表了整个训练数据集的遍历次数
# 整个训练集要循环多少轮,是10次还是20次还是100次都是有可能的
# 内层循环使用train_loader 进行小批量的数据读取
for batch_idx, (data, label) in enumerate(train_loader):
# 内层每循环一次,就会进行一次梯度下降算法
# 包括五个步骤:
output = model(data) # 1、计算神经网络的前向传播结果
loss = criterion(output, label) # 2、计算 output 和标签 label 之间的误差损失 loss
loss.backward() # 3、使用 backward 计算梯度
optimizer.step() # 4、使用 optimizer.step 更新参数
optimizer.zero_grad() # 5、将梯度清零
# 这五个步骤是使用 PyTorch 框架训练模型的定式,初学的时候记住就可以了
# 每迭代 100 个小批量,就打印一次模型的损失,观察训练的过程
if batch_idx % 100 == 0:
print(f"Epoch {epoch + 1} / 10 "
f"| Batch {batch_idx} / {len(train_loader)}"
f"| Loss: {loss.item():.4f}")
torch.save(model.state_dict(), 'mnist.pth') # 最后保存训练好的模型
The operation effect is as follows:
Omitted in the middle...
You can see that the final loss value is very, very small, which is 0.0239.
Finally, the test process is basically the same as the training process. The code is as follows:
model = NetWork() # 定义神经网络模型
model.load_state_dict(torch.load('mnist.pth')) # 加载刚刚训练好的模型文件
right = 0 # 保存正确识别的数量
for i, (x, y) in enumerate(test_data):
output = model(x) # 将其中的数据 x 输入到模型中
predict = output.argmax(1).item() # 选择概率最大的标签作为预测结果
# 对比预测值 predict 和真实标签 y
if predict == y:
right += 1
else:
# 将识别错误的样例打印出来
print(f"wrong case: predict = {predict}, but y = {y}")
# 计算出测试结果
sample_num = len(test_data)
accuracy = right * 1.0 / sample_num
print("test accuracy = %d / %d = %.3lf" % (right, sample_num, accuracy))
The results are as follows:
It can be seen that the test accuracy is 98%, which is still very high.
The above is the process of designing and training a neural network from scratch.
The above are explained and described according to each functional part, which may be a bit confusing. We encapsulate the above code as follows.
import torch
import torchvision.datasets
from torch import nn
# ----------------1、定义神经网络-------------------
class NetWork(nn.Module):
def __init__(self):
super().__init__()
# 线性层1,输入层和隐藏层之间的线性层
self.layer1 = nn.Linear(784, 256)
# 线性层2,隐藏层和输出层之间的线性层
self.layer2 = nn.Linear(256, 10)
# 这里没有直接定义 softmax 层
# 这是因为后面会使用 CrossEntropyLoss 损失函数
# 在这个损失函数中会实现 softmax 的计算
def forward(self, x):
x = x.view(-1, 28 * 28) # 使用view 函数将 x 展平
x = self.layer1(x) # 将 x 输入至 layer1
x = torch.relu(x) # 使用 ReLu 函数激活
return self.layer2(x) # 输入至 layer2 计算结果
# ----------------2、图像预处理-------------------
# 实现图像的预处理的pipeline
transform = torchvision.transforms.Compose([
torchvision.transforms.Grayscale(num_output_channels=1), # 转换为单通道灰度图像
torchvision.transforms.ToTensor() # 转换为PyTorch支持的张量
])
# ----------------3、数据集准备-------------------
# 准备训练数据集
train_data = torchvision.datasets.MNIST(root='data', train=True, download=True, transform=transform)
# ----------------4、数据集加载-------------------
# 使用 train_loader 实现小批量的数据读取
# 这里设置小批量的大小,batch_size = 64。
# 也就是每个批次包括 64 个数据
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
# ----------------5、训练神经网络-------------------
# 在使用 PyTorch 训练模型时,需要创建三个对象
model = NetWork() # 1、模型本身,它就是我们设计的神经网络
optimizer = torch.optim.Adam(model.parameters()) # 2、优化器,优化模型中的参数
criterion = nn.CrossEntropyLoss() # 3、损失函数,分类问题使用交叉熵损失误差
# 开始训练模型
for epoch in range(10): # 外层循环,代表了整个训练数据集的遍历次数
# 整个训练集要循环多少轮,是10次还是20次还是100次都是有可能的
# 内层循环使用train_loader 进行小批量的数据读取
for batch_idx, (data, label) in enumerate(train_loader):
# 内层每循环一次,就会进行一次梯度下降算法
# 包括五个步骤:
output = model(data) # 1、计算神经网络的前向传播结果
loss = criterion(output, label) # 2、计算 output 和标签 label 之间的误差损失 loss
loss.backward() # 3、使用 backward 计算梯度
optimizer.step() # 4、使用 optimizer.step 更新参数
optimizer.zero_grad() # 5、将梯度清零
# 这五个步骤是使用 PyTorch 框架训练模型的定式,初学的时候记住就可以了
# 每迭代 100 个小批量,就打印一次模型的损失,观察训练的过程
if batch_idx % 100 == 0:
print(f"Epoch {epoch + 1} / 10 "
f"| Batch {batch_idx} / {len(train_loader)}"
f"| Loss: {loss.item():.4f}")
torch.save(model.state_dict(), 'mnist.pth') # 最后保存训练好的模型
import torch
import torchvision.datasets
from torch import nn
# ----------------1、定义神经网络-------------------
class NetWork(nn.Module):
def __init__(self):
super().__init__()
# 线性层1,输入层和隐藏层之间的线性层
self.layer1 = nn.Linear(784, 256)
# 线性层2,隐藏层和输出层之间的线性层
self.layer2 = nn.Linear(256, 10)
# 这里没有直接定义 softmax 层
# 这是因为后面会使用 CrossEntropyLoss 损失函数
# 在这个损失函数中会实现 softmax 的计算
def forward(self, x):
x = x.view(-1, 28 * 28) # 使用view 函数将 x 展平
x = self.layer1(x) # 将 x 输入至 layer1
x = torch.relu(x) # 使用 ReLu 函数激活
return self.layer2(x) # 输入至 layer2 计算结果
# ----------------2、图像预处理-------------------
# 实现图像的预处理的pipeline
transform = torchvision.transforms.Compose([
torchvision.transforms.Grayscale(num_output_channels=1), # 转换为单通道灰度图像
torchvision.transforms.ToTensor() # 转换为PyTorch支持的张量
])
# ----------------3、数据集准备-------------------
# 准备测试数据集
test_data = torchvision.datasets.MNIST(root='data', train=False, download=True, transform=transform)
# ----------------4、测试神经网络-------------------
model = NetWork() # 定义神经网络模型
model.load_state_dict(torch.load('mnist.pth')) # 加载刚刚训练好的模型文件
right = 0 # 保存正确识别的数量
for i, (x, y) in enumerate(test_data):
output = model(x) # 将其中的数据 x 输入到模型中
predict = output.argmax(1).item() # 选择概率最大的标签作为预测结果
# 对比预测值 predict 和真实标签 y
if predict == y:
right += 1
else:
# 将识别错误的样例打印出来
print(f"wrong case: predict = {predict}, but y = {y}")
# 计算出测试结果
sample_num = len(test_data)
accuracy = right * 1.0 / sample_num
print("test accuracy = %d / %d = %.3lf" % (right, sample_num, accuracy))