分别利用手写 BP 网络和卷积神经网络 (CNN) 完成汉字识别任务
一、源码地址
https://github.com/busyforest/CharClassify
二、文件结构
-
CharClassify_BP
目录:用手写 BP 网络实现对 12 个汉字分类。train_data
目录:训练数据,其中每个汉字的第 1-500 个.bmp
文件用作训练集,501-620用作测试集。weights
目录:保存训练出来的网络的权重参数。CharClassify_Cross_L2.py
:实现汉字分类模型的训练。Test.py
:将测试集中的图片依次取出并利用image_to_vector()
转换,进行识别,根据输出概率最大的index
进行选择,打印识别结果并统计正确率。
-
CharClassify_CNN
目录:实现 CNN 网络分类任务。Le_Net5_model.py
:根据 Yann LeCun 的论文 Gradient-Based Learning Applied to Document Recognition ,构造了一个 Le_Net5 卷积神经网络。train.py
:载入数据集,用 CPU 训练该网络。train_GPU.py
:载入数据集,用 GPU 训练该网络。test.py
:用测试集测试该网络的正确率。train_data_CNN
:用于 CNN 网络的训练集和测试集,和 BP 网有所不同,CNN 的数据集以文件夹的形式载入,所以我手动把测试集和训练集分成了test
和train
两个文件夹。model.pkl
:训练好的模型以.pkl
文件形式保存在这里。
三、原理简介
我们分别使用普通的 BP 网络和卷积神经网络进行 12 个手写汉字识别任务。数据集和测试集均为 128x128 的 .bmp
格式的灰度图像,可以自己准备数据集,并相应修改代码中处理文件的方法。
-
关于 BP 网络
BP 网络的原理已经在我上一篇文章中介绍过,参见 手写BP网络拟合一元非线性函数(附数学原理公式推导)。在本次任务中,我们采用一个简单的三层神经网络来实现。其中中间层激活函数使用 sigmoid,输出层激活函数使用 softmax。损失函数采用 Cross-Entrophy。
-
softmax函数将一个向量中的每个元素转换为一个[0,1]范围内的概率值,并且这些概率值的总和为1。这样,输出向量中的每个元素都可以解释为一个特定类别的概率。对于一个输入向量 ( x ) ,Softmax 函数的计算公式为:
softmax ( x i ) = e x i ∑ j e x j \text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}} softmax(xi)=∑jexjexi
在汉字分类任务中,神经网络会输出每个类别的得分(在这里表现为归一化之前的概率),Softmax 函数将这些得分转换为归一化之后的概率分布,从而确定汉字最有可能属于哪个类别。
-
Cross-entropy(交叉熵损失函数):交叉熵是用来评估当前训练得到的概率分布与真实分布的差异情况。 它刻画的是实际输出(概率)与期望输出(概率)的距离,也就是交叉熵的值越小, 两个概率分布就越接近。
-
交叉熵损失函数的优势在于当梯度小的时候权重更新的就相应更慢,梯度大的时候更新更快。 这解决了 sigmoid 在两端梯度很小,权重更新慢的问题。另外,sigmoid 在执行分类任务时很容易落入局部最优解,即使使用梯度下降也难以得到全局最优解。
-
-
关于卷积神经网络 (CNN)
- CNN模型的基本结构如下所示:
- 输入层:接受手写汉字图片的像素值作为输入。
- 卷积层:包含多个卷积核,用于提取特征。
- 池化层:进行下采样,减少参数数量,同时保留特征。
- 全连接层:将池化层输出展平,连接到一个或多个全连接层。 输出层:使用 softmax 函数将全连接层的输出转换为类别概率
- 源码中的 CNN 部分调用 pytorch 库实现了一个简单的 LeNet5,结构如下:
- 卷积层C1:6个5x5的卷积核,输出6个特征图,使用ReLU激活函数。
- 最大池化层S2:2x2的窗口进行下采样。
- 卷积层C3:16个5x5的卷积核,输出16个特征图,使用ReLU激活函数。
- 最大池化层S4:2x2的窗口进行下采样。
- 卷积层C5:120个5x5的卷积核。
- 全连接层F6:连接120个神经元的隐藏层。
- 输出层Output:输出10个类别的结果
更多关于 LeNet5 及卷积神经网络的内容可参见这篇论文 Gradient-based learning applied to document recognition
- CNN模型的基本结构如下所示:
四、代码简析
1、关于 BP 网络
import datetime
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
def cross_entropy(y_true, y_pred):
return -np.sum(y_true * np.log(y_pred))
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)
def softmax(x):
exp_x = np.exp(x - np.max(x))
return exp_x / np.sum(exp_x)
def recognize(index):
switcher = {
1: "博",
2: "学",
3: "笃",
4: "志",
5: "切",
6: "问",
7: "近",
8: "思",
9: "自",
10: "由",
11: "无",
12: "用"
}
return switcher.get(index, "nothing")
def image_to_vector(i, j):
image = Image.open("train_data/train/{}/{}.bmp".format(i, j))
# 获取图像的尺寸
width, height = image.size
# 创建一个空的NumPy数组来存储灰度值
gray_array = np.empty((height, width), dtype=np.uint8)
# 遍历图像的每个像素
for y in range(height):
for x in range(width):
# 获取像素的灰度值
gray = image.getpixel((x, y))
# 将灰度值存储在NumPy数组中
if gray == 255:
gray_array[y, x] = 1
else:
gray_array[y, x] = 0
return gray_array
class BPnet:
def __init__(self):
self.input_size = 784
self.hidden_size = 128
self.output_size = 12
self.learning_rate = 0.001
self.losses = []
self.test_losses = []
self.test_accuracies = []
self.w1 = np.random.uniform(-1, 1, size=(self.input_size, self.hidden_size))
self.b1 = np.random.uniform(-2, 0, size=(1, self.hidden_size))
self.w2 = np.random.uniform(-1, 1, size=(self.hidden_size, self.output_size))
self.b2 = np.random.uniform(-2, 0, size=(1, self.output_size))
self.lambda_reg = 0.001
def train(self):
start_time = datetime.datetime.now()
for epoch in range(1000):
for j in range(1, 500):
for i in range(1, 13):
flat_array = image_to_vector(i, j).flatten()
x = np.array(flat_array).T.reshape(1, self.input_size)
d = np.zeros((1, self.output_size))
d[0][i - 1] = 1
# Forward pass
h = x @ self.w1 + self.b1
s = sigmoid(h)
z = s @ self.w2 + self.b2
y = softmax(z)
# Backward pass
e = y - d
d_w2 = s.T @ e + self.lambda_reg * self.w2 # weights regularization term added
d_b2 = e + self.lambda_reg * self.b2
d_w1 = x.T @ (e @ self.w2.T * sigmoid_derivative(
s)) + self.lambda_reg * self.w1 # weights regularization term added
d_b = e @ self.w2.T * sigmoid_derivative(s)+ self.lambda_reg * self.b1
# Update parameters with regularization
self.b2 -= self.learning_rate * d_b2
self.w2 -= self.learning_rate * d_w2
self.b1 -= self.learning_rate * d_b
self.w1 -= self.learning_rate * d_w1
np.save("weights/w1.npy", self.w1)
np.save("weights/w2.npy", self.w2)
np.save("weights/b1.npy", self.b1)
np.save("weights/b2.npy", self.b2)
print("Epoch:", epoch, "train_loss", cross_entropy(d, y), end=" ")
self.losses.append(cross_entropy(d, y))
self.training_test()
end_time = datetime.datetime.now()
plt.plot(self.losses)
plt.plot(self.test_losses)
plt.xlabel('Epoch')
plt.ylabel('Cross Entropy Loss')
plt.legend(['Train', 'Test'])
plt.savefig("weights/loss.png")
plt.plot(self.test_accuracies)
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.savefig("accuracy.png")
print("训练开始时间:", start_time)
print("训练结束时间:", end_time)
def training_test(self):
total = 0
correct = 0
for a in range(510, 600):
for b in range(1, 13):
flat_array = image_to_vector(b, a).flatten()
x_test = np.array(flat_array).T.reshape(1, self.input_size)
d_test = np.zeros((1, self.output_size))
d_test[0][b - 1] = 1
h_test = x_test @ self.w1 + self.b1
s_test = sigmoid(h_test)
z_test = s_test @ self.w2 + self.b2
y_test = softmax(z_test)
index = np.argmax(y_test)
if index == b - 1:
correct += 1
total += 1
print("test loss:", cross_entropy(d_test, y_test), "test accuracy:", correct / total)
self.test_losses.append(cross_entropy(d_test, y_test))
self.test_accuracies.append(correct / total)
def SetWeight(self, w1, b1, w2, b2):
self.w1 = w1
self.b1 = b1
self.w2 = w2
self.b2 = b2
if __name__ == '__main__':
np.random.seed(52)
bp = BPnet()
bp.train()
class BPnet
: bp 网络类
init(self)
:初始化方法,可以在这里调节网络的 input_size
、 hidden_size
、output_size
、 lambda_reg
等参数( labmda_reg
用于 L2 正则化),同时完成了随机初始化参数的任务
train(self)
:训练网络,总共跑 1000 个 epoch,每个 epoch 将 12 个汉字逐个 取出,输入到网络中,并调整参数,以 .npy
文件形式记录在 目录下。 一个 epoch 跑完之后记录 train_loss
,并调用traing_test
函数。
training_test(self)
:计算测试集上的 loss 和 正确率,每个 epoch 都在最后调用一下,可以清楚的看到 loss 和 accuracy 随着 epoch 的变化。
SetWeight(self, w1, b1, w2, b2)
设置权重参数,方便验证已有的参数的准确率以及在已有参数基础上继续训练。
image_to_vector(i, j)
:将 train_data/train/i/j.bmp
图像转换成 28x28 的灰度值向量。
recognize(index)
:根据输入的 index
索引值返回十二个汉字中的一个,从一个数字转换成字符。
sigmoid(x)
、 softmax(x)
、 cross_entropy(y_true, y_pred)
等:相关损失函数和激活函数。
2、关于 CNN
-
LeNet_5_model.py: 模型的定义
import torch import torch.nn as nn class LeNet5(nn.Module): def __init__(self): super(LeNet5, self).__init__() self.C1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1, padding=2) self.relu1 = nn.ReLU() self.S2 = nn.MaxPool2d(kernel_size=2) self.C3 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1) self.relu2 = nn.ReLU() self.S4 = nn.MaxPool2d(kernel_size=2) self.C5 = nn.Conv2d(in_channels=16, out_channels=120, kernel_size=5, stride=1) self.relu3 = nn.ReLU() self.F6 = nn.Linear(in_features=120, out_features=84) self.relu4 = nn.ReLU() self.Output = nn.Linear(in_features=84, out_features=12) def forward(self, x): x = self.C1(x) x = self.relu1(x) x = self.S2(x) x = self.C3(x) x = self.relu2(x) x = self.S4(x) x = self.C5(x) x = self.relu3(x) x = x.view(x.size(0), -1) x = self.F6(x) x = self.relu4(x) x = self.Output(x) return x if __name__ == '__main__': x = torch.randn(1, 1, 28, 28) model = LeNet5() print(model)
-
train.py:模型的训练
import datetime import torch import torch.nn as nn import torch.utils.data as Data from torchvision import datasets from torchvision import transforms from Le_Net5_model import LeNet5 data_transform = transforms.Compose([ transforms.Grayscale(), # 将图像转换为灰度值 transforms.ToTensor() # 将图像转换为张量 ]) train_set_path = r"train_data_CNN/train" data_train = datasets.ImageFolder(train_set_path, transform=data_transform) data_loader = Data.DataLoader(data_train, batch_size=64, shuffle=True) model = LeNet5() Epoch = 25 batch_size = 64 lr = 0.001 loss_function = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=lr) torch.set_grad_enabled(True) model.train() start_time = datetime.datetime.now() for epoch in range(Epoch): running_loss = 0.0 acc = 0.0 for step, data in enumerate(data_loader): x, y = data optimizer.zero_grad() y_pred = model(x) loss = loss_function(y_pred, y) loss.backward() running_loss += float(loss.data.cpu()) pred = y_pred.argmax(dim=1) acc += (pred.data.cpu() == y.data).sum() optimizer.step() if step % 100 == 1: loss_avg = running_loss / (step + 1) acc_avg = float(acc / ((step + 1) * batch_size)) print('Epoch', epoch + 1, ',step', step + 1, '| Loss_avg: %.4f' % loss_avg, '|Acc_avg:%.4f' % acc_avg) end_time = datetime.datetime.now() print('训练时间:', end_time - start_time)
-
test.py:模型的测试
import torch import torchvision import torch.utils.data as Data from torchvision import datasets, transforms data_transform = transforms.Compose([ transforms.Grayscale(), # 将图像转换为灰度值 transforms.ToTensor() # 将图像转换为张量 ]) test_set_path = r"train_data_CNN/test" data_test = datasets.ImageFolder(test_set_path, transform=data_transform) data_loader = Data.DataLoader(data_test, batch_size=64, shuffle=False) device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") net = torch.load('./model.pkl', map_location=torch.device(device)) net.to(device) torch.set_grad_enabled(False) total = 0 correct = 0 for i, data in enumerate(data_loader): x, y = data y_pred = net(x.to(device, torch.float)) pred = y_pred.argmax(dim=1) correct += (pred == y.to(device, torch.float)).sum().item() total += y.size(0) print('Accuracy of the network on the test images: ', 100 * correct / total, "%")
-
train_GPU.py:用 GPU 加速训练(需要配置好 CUDA)
import datetime import torch import torch.nn as nn import torch.utils.data as Data from torchvision import datasets from torchvision import transforms from Le_Net5_model import LeNet5 data_transform = transforms.Compose([ transforms.Grayscale(), # 将图像转换为灰度值 transforms.ToTensor() # 将图像转换为张量 ]) train_set_path = r"train_data_CNN/train" data_train = datasets.ImageFolder(train_set_path, transform=data_transform) data_loader = Data.DataLoader(data_train, batch_size=64, shuffle=True) model = LeNet5() Epoch = 25 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) print("正在使用:", torch.cuda.get_device_name()) batch_size = 64 lr = 0.001 loss_function = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=lr) torch.set_grad_enabled(True) model.train() start_time = datetime.datetime.now() for epoch in range(Epoch): running_loss = 0.0 acc = 0.0 for step, data in enumerate(data_loader): x, y = data optimizer.zero_grad() y_pred = model(x.to(device, torch.float)) loss = loss_function(y_pred, y.to(device, torch.long)) loss.backward() running_loss += float(loss.data.cpu()) pred = y_pred.argmax(dim=1) acc += (pred.data.cpu() == y.data).sum() optimizer.step() if step % 100 == 0: loss_avg = running_loss / (step + 1) acc_avg = acc / ((step + 1) * batch_size) print('Epoch', epoch + 1, ',step', step + 1, '| Loss_avg: %.4f' % loss_avg, '|Acc_avg:%.4f' % acc_avg) # torch.save(model, './model.pkl') end_time = datetime.datetime.now() print("训练时间:", end_time - start_time)
五、测试结果
在 BP 网络上的 loss 曲线如下:
![](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fpic.imgdb.cn%2Fitem%2F669727f1d9c307b7e9605270.png&pos_id=img-5qw9rLQH-1721182558139%29)
最后在测试集上的正确率约为 85%。
CNN 收敛非常快,15 到 20 个 epoch 即可收敛,最终正确率在 96.75%左右。