Tech

第一个神经网络-手写识别

by illlights, 2023-04-05


最近参加美赛可能要用到神经网络,所以稍微学了一下,姑且把我的学习过程写出来吧。本系列不会讲解深度学习中的数学原理,直接用“习惯用法”代替了研究的数学证明,需要了解其中的数学原理的推荐吴思达的公开课

我使用 Tensorflow 框架,直接在 Google Colab 上运行,在这里可以直接打开使用:AI Learning.ipynb,但是还是建议自己重新打一遍代码,否则很可能做完之后还是什么都不会。

基本原理

亚马逊云抄来的原理,算一个检测吧,先得看得懂这些东西再继续,不然很可能完全不知道在做什么。

神经网络的工作原理是什么?

神经网络架构的灵感源自人脑。人脑细胞称为神经元,构成了一个复杂、高度互联的网络,并能互相发送电信号,以帮助人类处理信息。同样,人工神经网络由人工神经元组成,它们共同合作以解决问题。人工神经元是软件模块,称为节点;人工神经网络是软件程序或算法,在其核心,使用计算系统求解数学计算。

简单神经网络架构

基本神经网络的相互连接的人工神经元分为三层:

神经元示意图

1. 输入层

来自外部世界的信息通过输入层进入人工神经网络。输入节点对数据进行处理、分析或分类,然后将其继续传递到下一层。

2. 隐藏层

隐藏层从输入层或其他隐藏层获取其输入。人工神经网络可以具有大量的隐藏层。每个隐藏层都会对来自上一层的输出进行分析和进一步处理,然后将其继续传递到下一层。

3. 输出层

输出层提供人工神经网络对所有数据进行处理的最终结果。它可以包含单个或多个节点。例如,如果我们要解决一个二元(是/否)分类问题,则输出层包含一个输出节点,它将提供 1 或 0 的结果。但是,如果我们要解决一个多类分类问题,则输出层可能会由一个以上输出节点组成。

深度神经网络架构

深度神经网络又名深度学习网络,拥有多个隐藏层,包含数百万个链接在一起的人工神经元。名为权重的数字代表节点之间的连接。如果节点之间相互激励,则该权重为正值,如果节点之间相互压制,则该权重为负值。节点的权重值越高,对其他节点的影响力就越大。
从理论上讲,深度神经网络可将任何输入类型映射到任何输出类型。但与其他机器学习方法相比,它们也需要更多大量的训练。它们需要数百万个训练数据示例,而不像较简单的网络那样,可能只需数百或数千个训练数据示例。

个人理解

简单来说神经网络可以看作一个整体的函数,里面可以有数以亿计的参数(参数数量是我们设定的)。通过不断的调整参数来调整结果,使输出达到能继续预测的准确度。

有一点像评价类模型,常规评价类模型的建模相当于人为调整参数,但是使用神经网络就是在利用数学原理自动修改参数。

自然而然,整个建模过程就分为两大部分:训练、预测。训练就是调整参数的过程,而预测是使用这些参数求得结果的过程。当然预测过程也需要求结果,注意区分。

数据准备

计算机只能处理二进制数据,十进制转二进制非常容易,而图片的本质就是每个像素点的 R 、G 、B 十进制数值。这就是计算机显示图像的原理。为了将图像输入 Python 中进行处理,我们得约定一种数据表示方法。

import numpy as np
from tensorflow.keras.preprocessing.image import load_img, img_to_array
from tensorflow.keras.utils import load_img, img_to_array
from PIL import ImageOps

img_path = 'xxx.jpg'             
img = load_img(img_path)       # 导入图片文件,我预处理了图片,每个图片分辨率均为 28*28
img = ImageOps.grayscale(img)  # 转换为灰度图像,将 RGB 图片转化为灰度图片。
x = np.array(img).astype('float32') # 将 28*28 图片数据展平成 784 维向量
x = np.expand_dims(x, axis=0)  # 符合数据网络数据的要求
x = x/255.0                    # 归一化数据,让所有的数据都在 0~1 这个区间内

现在只要人工写几万张手写图片,并且先人工识别出每张图片的意思,输入 Python 中就可以开始训练了!

是不是很麻烦?这就是神经网络的重点,要有庞大且优质的数据集。这个数据集才是神经网络算法的核心,得到优质的数据集甚至比选择好的模型还重要。

对于手写数字识别来说,已经有整理好的数据集,直接导入就行了。这里我们顺便把依赖导入了吧:

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, models
from tensorflow.keras.models import load_model
from tensorflow.keras.callbacks import ModelCheckpoint

mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

关键的数据导入就是最后两行代码,从库中读取数据。数据分为四个组:

  • train_images: 训练集图像的 numpy 数组,形状为 (60000, 28, 28)。
  • train_labels: 训练集标签的 numpy 数组,形状为 (60000,),每个标签是一个整数,表示对应图像的数字。
  • test_images: 测试集图像的 numpy 数组,形状为 (10000, 28, 28)。
  • test_labels: 测试集标签的 numpy 数组,形状为 (10000,),每个标签是一个整数,表示对应图像的数字。

搭建神经网络

先看代码

model = models.Sequential([
    layers.Flatten(input_shape=(28, 28)),  # 输入层,将 28x28 的图片矩阵展平成 784 维向量
    layers.Dense(128, activation='relu'),  # 隐藏层,128 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.2),
    layers.Dense(128, activation='relu'),  # 隐藏层,128 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.2),
    layers.Dense(10, activation='softmax')  # 输出层,10 个神经元,使用 Softmax 激活函数,用于多分类问题
])

很容易理解,models.Sequential中的一个参数就是一层,填入的数字是该层的节点数,activation则表示该层使用什么激活函数,是上一层到这一层的映射关系。

激活函数在这里不多做介绍,以后有机会再写文章详细说。

Dropout 层的主要参数是丢弃率(dropout rate),它是一个 0 到 1 之间的浮点数,表示每次训练迭代时随机丢弃的神经元比例。例如,Dropout(0.2) 表示每次训练迭代时随机丢弃 20% 的神经元输出,使得网络不会过于依赖于某些特定神经元。这可以提高模型的泛化能力,从而在测试集上获得更好的性能。

层数、节点数和丢弃率都需要不断尝试。对于手写识别这种简单的分类问题一两层就够了,层数过多反而容易出现过拟合的问题。

模型训练与验证

  1. 编译模型:指定优化器、损失函数、训练目标
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
  1. 训练模型:用 x_train 和 y_train 数据训练模型,epochs 用于指定训练的次数
model.fit(x_train, y_train, epochs=10)
  1. 验证模型:用 x_test 和 y_test 验证模型的效果。作为操作者,我们可以根据这个结果来调整模型。
test_loss, test_acc = model.evaluate(x_test, y_test)
print(f"Test accuracy: {test_acc}")

手写数字识别是个很简单的任务,不需要迭代几百次,用 Colab 几秒钟就能完成,所以也无需保存模型、重新训练,但是如果模型大了,保存、读取就显得越发重要:

保存模型:codemodel.save("mnist_model.h5")
加载模型:

from tensorflow.keras.models import load_model
loaded_model = load_model("mnist_model.h5")

使用加载的模型进行预测:

prediction = loaded_model.predict(input_image)
predicted_label = np.argmax(prediction)
print(f"Predicted label: {predicted_label}")

继续训练模型:

from tensorflow.keras.models import load_model

loaded_model = load_model("mnist_model.h5")

# 编译模型(可选):如果您在保存模型之前已经编译过模型,那么在加载模型时,编译信息也会被保留。但是,如果您希望改变优化器、损失函数或指标,您可以在加载模型后重新编译模型。
loaded_model.compile(optimizer='adam',
                 loss='sparse_categorical_crossentropy',
                 metrics=['accuracy'])
# 继续训练模型:使用fit()方法继续训练模型。在这个例子中,我用MNIST数据集的训练数据和2个额外的训练周期(epochs)。
loaded_model.fit(x_train, y_train, epochs=2)
# 评估模型性能
test_loss, test_acc = loaded_model.evaluate(x_test, y_test)
print(f"Test accuracy: {test_acc}")
# 保存更新后的模型 
loaded_model.save("mnist_model_updated.h5")

如果想要在中途保存权重 (weigh),参考:

# 计算每五个周期的批次数
save_freq_batches = len(x_train) // 5

# 创建 ModelCheckpoint 回调以在训练期间保存权重检查点
checkpoint_path = "checkpoints/weights_cp-{epoch:04d}.ckpt"
checkpoint_callback = ModelCheckpoint(filepath=checkpoint_path,
                                      save_weights_only=True,
                                      save_freq=save_freq_batches, # 每五个周期保存一次权重
                                      verbose=1)

# 使用 fit() 方法进行训练
loaded_model.fit(x_train, y_train, epochs=100, callbacks=[checkpoint_callback])
loaded_model.save("mnist_model_updated.h5")

使用模型进行预测

import numpy as np
from PIL import ImageOps
from tensorflow.keras.preprocessing.image import load_img, img_to_array

img_path = '3.jpg'
img = load_img(img_path, color_mode='grayscale', target_size=(28, 28))
img_array = img_to_array(img)
# img_array = 255 - img_array  # 如果输入图像的背景是白色,这会将其反转为黑色背景
img_array = img_array.astype('float32')
img_array = np.expand_dims(img_array, axis=0)
img_array /= 255
pred = model.predict(img_array)
class_idx = np.argmax(pred[0])
print(pred[0])
print(class_idx)

在使用自己的图片之前要进行数据处理,反而是预测的代码非常简单,model.predict(x)

完整代码

import tensorflow as tf
from tensorflow.keras import layers, models
from tensorflow.keras.models import load_model
from tensorflow.keras.callbacks import ModelCheckpoint

mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

model = models.Sequential([
    layers.Flatten(input_shape=(28, 28)),  # 输入层,将 28x28 的图片矩阵展平成 784 维向量
    layers.Dropout(0.1),
    layers.Dense(64, activation='relu'),  # 隐藏层,64 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.1),
    layers.Dense(128, activation='relu'),  # 隐藏层,128 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.1),
    layers.Dense(64, activation='relu'),  # 隐藏层,64 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.1),
    layers.Dense(256, activation='relu'),  # 隐藏层,256 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.1),
    layers.Dense(64, activation='relu'),  # 隐藏层,64 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.1),
    layers.Dense(128, activation='relu'),  # 隐藏层,128 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.1),
    layers.Dense(64, activation='relu'),  # 隐藏层,64 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.1),
    layers.Dense(32, activation='relu'),  # 隐藏层,32 个神经元,使用 ReLU 激活函数
    layers.Dropout(0.1),
    layers.Dense(10, activation='softmax')  # 输出层,10 个神经元,使用 Softmax 激活函数,用于多分类问题
])


# 创建 ModelCheckpoint 回调
checkpoint_path = "checkpoints/cp-{epoch:04d}.ckpt"
checkpoint_callback = ModelCheckpoint(filepath=checkpoint_path,
                                      save_weights_only=True,
                                      save_freq='epoch',
                                      verbose=1)

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
# model.fit(x_train, y_train, epochs=1, callbacks=[checkpoint_callback])
model.fit(x_train, y_train, epochs=20)
test_loss, test_acc = model.evaluate(x_test, y_test)
model.save("hello1.h5")
print(f"Test accuracy: {test_acc}")
# 预测
import numpy as np
from PIL import ImageOps
from tensorflow.keras.preprocessing.image import load_img, img_to_array

img_path = '3.jpg'
img = load_img(img_path, color_mode='grayscale', target_size=(28, 28))
img_array = img_to_array(img)
# img_array = 255 - img_array  # 如果输入图像的背景是白色,这会将其反转为黑色背景
img_array = img_array.astype('float32')
img_array = np.expand_dims(img_array, axis=0)
img_array = img_array / 255.0
pred = model.predict(img_array)
class_idx = np.argmax(pred[0])
print(pred[0])
print(class_idx)

自己调一调参数吧!

作者: illlights

2 条评论
    LemonPig 回复
    LemonPig2023-04-08 19:33

    请收下我的敬佩!我对AI向来有至高的兴趣,但是从未给自己机会真正实践。而今天有幸在这个满是宝藏的blog上看到了这篇实践记录,自己感到不胜惭愧与羡慕。以后我也想从事人工智能行业,看来我需要学习的还有很多!

      admin 回复
      admin2023-04-08 19:42

      我是门外汉——大佬不要被我误导了——

      @LemonPig
2024 © typecho & elise