分别利用手写 BP 网络和卷积神经网络 (CNN) 完成汉字识别任务

分别利用手写 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 的数据集以文件夹的形式载入,所以我手动把测试集和训练集分成了 testtrain 两个文件夹。
    • 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

四、代码简析

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_sizehidden_sizeoutput_sizelambda_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 曲线如下:

最后在测试集上的正确率约为 85%。

CNN 收敛非常快,15 到 20 个 epoch 即可收敛,最终正确率在 96.75%左右。

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

busyforest

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值