用神经网络实现一个手写体识别

439 阅读6分钟

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。

逻辑回归

相信很多读者都听说过线性回归,线性回归是一个很常用的机器学习算法。我们通常使用线性回归来解决预测问题。像房价预测、股票预测等。这些例子的输出都是一个连续值,我们也把这类问题称作回归问题。

而机器学习另一个研究的问题就是分类问题了,这类问题的输出结果是离散的。像我们动物分类的问题,我们输出的结果是一种确定的动物,猫或者狗,而不会是一个介于猫狗之间的动物。

那逻辑回归又是什么呢?逻辑回归就是在线性回归的基础上,使用一个激活函数,对线性回归的结果进行二次处理,从而解决分类问题的一种算法。

我们可以看下面这个例子。

假如我们有一个模型用于计算学生的成绩:

y=w1x1+w2x2+w3x3+by = w_1x_1 + w_2x_2 + w_3x_3 + b

其中w1w_1w2w_2w3w_3 为 0.3、0.4、0.3,而x1x_1x2x_2x3x_3 分别为输入的语文、数学、英语成绩。b 则是另外的加分。

假设额外加分为 0,我们就可以把式子写成:

y=0.3x1+0.4x2+0.3x3y = 0.3x_1 + 0.4x_2 + 0.3x_3

我们可以尝试用在这个模型求一组数据:

[100, 100, 100]
[90, 50, 80]

输出结果分别是:

100
52

现在我们可以使用一个激活函数,对上面的结果进行处理。我们假设激活函数为:

y=x60x60y = \frac{x-60}{|x-60|}

在实际应用中我们都会选择使用 ReLU 和 Tanh 函数作为激活函数,这里只是为了举例子。

上面的函数可以将大于 60 的数字变成 1,将小于 60 的数字变成 -1,这样我们就把连续的结果映射成了离散的结果。

注意:上面的激活函数在 x=60 处没有定义,这是只作为学习例子,在实际使用中不可取。

因为激活函数的输入是回归方程的输出,因此我们可以把函数写成:

y=0.3x1+0.4x2+03.w30.3x1+0.4x2+03.w3y = \frac{0.3x_1 + 0.4x_2 + 03.w_3}{|0.3x_1 + 0.4x_2 + 03.w_3|}

这样我们就用回归的方式解决了分类问题。

神经网络

神经网络也是一种机器学习的算法,它的思想就是将多个逻辑回归函数排列在一层,给每一个节点初始化一组参数。

在这里插入图片描述

上面是单个节点,其中 X 为输入的数据,W 为我们的参数(这里省去了 b)然后经过激活函数,输出 -1。其实上面就是一个简单的逻辑回归,我们再添加几个节点:

在这里插入图片描述

我们还可以继续添加节点数,构造一个更加复杂的函数。这就是我们所说的神经网络。而如果我们再宽度(添加节点)的基础上,再添加其它层的节点,就是我们深度学习的由来了。下图就是一个全连接前馈神经网络的图。

在这里插入图片描述

图片摘自李宏毅《机器学习》课程 PPT

神经网络在解释性上比较弱,我们很难知道每个节点起了什么作用,但是在效果上神经网络非常出色。

特征提取

下面我们来使用神经网络来实现一个手写体数字识别的问题。首先我们需要有手写体数字的数据集,数据集可以在下面下载:

pan.baidu.com/s/1akzIgDs9…

提取码:wqas

需要先读取所有的图片,然后提取图片的特征,即像素信息。我们需要安装几个模块:

pip install opencv-python
pip install numpy
pip install scikit-learn

然后我们使用 OpenCV 读取图片数据:

import os
import re
import cv2
import random
import numpy as np
# 1、特征提取
def get_feature(img_path, img=None):
    """
    提取特征
    """
    if img_path:
        img = cv2.imread(img_path, 0)
    # 对图片进行阈值处理,将小于 200 的像素点处理为黑色
    
    # 将图片转换成一维
    img = img.reshape((img.size, ))
    return img

images = []

def get_features(imgs_path):
    
    global images
    # 遍历数据集
    for root, dirs, files in os.walk(r'C:\Users\Administrator\Desktop\dataset'):
        for file in files:
            current_image_path = os.path.join(root, file)
            # 获取所有图片路径
            images.append(current_image_path)
            
    # 创建一个可以装所有数据的多维数组
    feature_size = get_feature(images[0]).size
    dataset = np.ones((len(images), feature_size+1), dtype=np.uint8)
    for index in range(len(images)):
        # 提取目标值
        target = re.findall(r"dataset\\(.*?)\\", images[index])
        feature = get_feature(images[index])

        # 将特征值和目标值合并    
        data = np.hstack((feature, target))

        # 合并所有数据集
        dataset[index, :] = data
    
    return dataset
    
dataset = get_features(None)

其中 get_feature 函数的作用是获取单张图片的特征,并将特征转换成一维的形式。这样我们才能将它作为参数放入神经网络中。但是此时我们还少了一个东西,就是目标值。

我们监督学习的数据通常需要特征和目标两个部分,而在数据集中,我们为每个数字的图片设置了一个目录。0 目录中全是数字 0,1 目录中全是数字 1。因此我们可以为每个数据添加标签。

在 get_features 函数中读取了数据集中所有图片,并为每个数字添加标签。最后我们得到了一个新的 ndarray 数组,这个数组我们就可以放入神经网络中进行训练。现在特征提取了,我们可以开始训练模型了。

训练模型

我们这里使用的是机器学习模块 scikit-learn,里面集成了神经网络的实现。我们先来创建一个模型:

from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split

# 1、数据集拆分
X_train, X_test, y_trian, y_test = train_test_split(dataset[:, :-1], dataset[:, -1:], test_size=0.25, random_state=21)

# 2、训练神经网络
mlp = MLPClassifier(solver='lbfgs', random_state=1, hidden_layer_sizes=[700, 700, 700, 700])
mlp.fit(X_train, y_trian)

我们使用 train_test_split 将数据集拆分成训练集和测试集,这样我们就能验证模型的好坏。

然后使用 MLPClassifier 类创建一个神经网络,我们只需要关注 hidden_layer_sizes 参数即可,这个参数就是我们各层网络的节点个数。这里我使用了一个非常大的网络,然后调用 fit 方法填充数据并训练,这样我们就训练好了一个模型。

我们可以调用 score 方法对模型进行评估:

# 3、模型评估
score = mlp.score(X_test, y_test)
print(0.9902985074626866)

输出结果如下:

0.9902985074626866

可以看到模型的准确率达到了 99%。但是实际应用中,会发现并没有这么理想。我们来测试一下。

手写体识别

我们使用 OpenCV 写一个写字板,然后再来进行测试。

import random
img = np.ones((100, 100), dtype=np.uint8)*255
drawing = False
def draw(event, x, y, flags, param):
    global drawing
    if event == cv2.EVENT_LBUTTONDOWN:
        drawing = True
    elif event == cv2.EVENT_LBUTTONUP:
        drawing = False
    if drawing and event == cv2.EVENT_MOUSEMOVE:
        img[y][x] = 0 + random.randint(0, 20)

cv2.namedWindow('image')
cv2.setMouseCallback('image', draw)
while True:
    cv2.imshow('image', img)
    key = cv2.waitKey(1)
    if key == ord('q'):
        kernel = np.ones((5, 5), np.uint8)
        img = cv2.erode(img, kernel)
        img = cv2.resize(img, (28, 28))
        cv2.imwrite('1.jpg', img)
        break
cv2.destroyAllWindows()
feature = get_feature('1.jpg')
print(feature.shape)
y_pre = mlp.predict(np.array([feature]))
plt.imshow(img)
plt.show()
print("预测结果为", y_pre)

上面我们用 OpenCV 创建了一个写字板,我们只需要在上面写一个数字,然后按 q 键即可获得图片的特征数据。

我们调用 mlp.predict 来对数字进行预测,在实际测试中发现准确率要远远低于 99%。大家可以在自己的电脑上尝试一下。