​ 总算是开始了自己的第一篇技术博客。

​ 也总算是大概弄懂了一些深度学习、神经网络、卷积神经网络的知识,能靠自己实现识别字符的卷积神经网络了。

​ 今天时间所剩不多,可能写不完,写到哪算哪吧。

​ 在大二下的《人工智能导论》这门课开始接触深度学习,学得一知半解都算不上,啥都没弄明白,甚至还要做神经网络的实验,本是作为启蒙课的目的也没有达到,反而起到了揠苗助长的效果,让人觉得深度学习、神经网络特别难。就我目前的理解而言,我绝不敢说深度学习、神经网络简单,毕竟现在科技前沿都还没将之完全征服。但是我可以说,理解深度学习、神经网络的基本概念绝对不难。实际上我到目前为止也只是看了吴恩达老师不到半小时的讲解就入门了神经网络,实训课李伟老师一上午左右的讲解也让我掌握了数字图像基本概念和卷积神经网络的思路。不得不抨击一下这门课的设立,我被老师和实验弄迷糊的时间完全可以入门深度学习了,况且还让我很长一段时间对深度学习、神经网络有些畏惧,如今看来,似乎”不过如此“(bushi)。

​ 好吧,啰嗦了半天,好像都没时间写技术性的东西了。

​ 这次的任务是把老师已经用tensorflow1.x写好的识别字符的卷积神经网络改成用tensorflow2.x实现**(封装的特性)**。关于深度学习和神经网络的知识梳理有空再写吧。

​ 先到这里。

​ ————————————————————2022/7/15 23:28

​ ————————————————————2022/7/17 9:41接着写

​ 拖欠了两天都没写完哈哈。不过下一阶段的任务还没开始,也不急。字符识别卷积神经网络搭好了,接下来就是web的搭建了,Python的flask是完全没学过的内容。

​ 直接按照搭建网络的顺序逐个说明吧。

一.数据读取

现在很多CV领域的数据读取都是直接读取现成的数据集,但是这里需要从本地文件当中读取,并进行处理,这一部分tf1和tf2没区别,不用改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def load_data(dir_path):
data = []
labels = []
for item in os.listdir(dir_path):
item_path = os.path.join(dir_path, item)
if os.path.isdir(item_path):
for subitem in os.listdir(item_path):
subitem_path = os.path.join(item_path, subitem)
gray_image = cv.imread(subitem_path, cv.IMREAD_GRAYSCALE)
resized_image = cv.resize(gray_image, (IMAGE_WIDTH, IMAGE_HEIGHT))
data.append(resized_image.ravel())
labels.append(LABEL_DICT[item])

return np.array(data), np.array(labels)
'''
data文件夹里面有0~1,A~Z的所有字符,每个字符有一个文件夹存储对应图片,这部分代码的功能就是把每个图片读出来,转换成20*20的大小,label就是文件夹名称
'''

正式进行数据读取的步骤开始有了变化。原本会对训练集和测试集都进行正则化,然后再把标签独热编码,这两步都是自定义的函数。tf2提供相关的API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 获取训练集的特征矩阵、标签向量
train_data, train_labels = load_data(TRAIN_DIR)
# 对训练集的标签向量执行独热编码,tf2提供API
train_labels = tf.one_hot(train_labels, 34)

# 获取训练集的总样本数
train_samples_count = len(train_data)
train_indicies = np.arange(train_samples_count)
# 获得打乱的索引序列
np.random.shuffle(train_indicies)

# 获取测试集的特征矩阵、标签向量
test_data, test_labels = load_data(TEST_DIR)
# 对测试集的标签向量执行独热编码
test_labels = tf.one_hot(test_labels, 34)

# 进入tf2搭建的网络之后还要reshape,变成[N, width, height, 1(灰度图)]的形式
train_data = tf.reshape(train_data, (train_data.shape[0], IMAGE_WIDTH, IMAGE_HEIGHT, 1))
test_data = tf.reshape(test_data, (test_data.shape[0], IMAGE_WIDTH, IMAGE_HEIGHT, 1))
'''
可能会问,原先的正则化哪去了,答案是:直接在神经网络入口加一层正则化层就行
这样不用分别处理训练集和测试集,它们进入网络就会正则化了
'''

二.搭建网络(封装的特性)

tf2搭建网络最明显的特点就是封装的特性,直接在一个对象当中设置好每一层的参数就可以,不需要手动实现底层代码,有了非常明显的改进。

直接上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
network = tf.keras.models.Sequential([
# 正式操作之前先进行正则化,专门的正则化层
tf.keras.layers.BatchNormalization(),
# 第一层卷积层,32个卷积核,大小5*5,自动初始化,relu激活,输入形状固定
tf.keras.layers.Conv2D(filters=32, kernel_size=5, activation='relu', input_shape=(IMAGE_WIDTH, IMAGE_HEIGHT, 1), padding='same'),
# 第一层池化层
tf.keras.layers.MaxPool2D(pool_size=2, strides=2), # 20=>10,步长应该取2
# 第二层卷积层
tf.keras.layers.Conv2D(filters=64, kernel_size=5, activation='relu', input_shape=(10, 10, 32), padding='same'),
# 第二层池化层
tf.keras.layers.MaxPool2D(pool_size=2, strides=2), # 10=>5
# 把二维矩阵展平成向量
tf.keras.layers.Flatten(),
# 全连接层
tf.keras.layers.Dense(1024, activation='relu'),
# 全连接之后会导致参数爆炸,需要dropout一些数据
tf.keras.layers.Dropout(0.3),
# 仿射函数
tf.keras.layers.Dense(CLASSIFICATION_COUNT, activation='softmax')
])

这只是前向传播的代码,交叉熵损失函数和反向传播算法如下。

1
2
3
network.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),	# 反向传播算法&学习率设置
loss=tf.keras.losses.BinaryCrossentropy(), # 交叉熵损失函数
metrics=['accuracy'])

三.训练模型、评估准确率、保存模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 训练模型
network.fit(train_data, train_labels, batch_size=50, epochs=30, validation_split=0.02)
'''
直接指定batch_size和epochs即可,很方便
validation_split这个参数就是每次训练时,把训练集中一定比例的数据不参与训练,用来测算正确率等参数,测出来也不改变参数
最开始validation_split=0.1的时候,训练当中算出来的正确率很高,98/99%甚至100%。但是测试集效果不好,正确率89%左右。我估计是因为直接削减了10%的数据,泛化能力不强。
但是如果设置validation_split=0.0,训练和测试效果很好,98~99%,但是实际检测效果反而不如前者,说明可能过拟合了
所以设置validation_split=0.02,这样中庸的策略既保证了正确率,泛化能力又不错
'''

# 评估准确率
test_loss, test_acc = network.evaluate(test_data, test_labels)
print('\n测试准确率:', test_acc)

# 保存模型
network.save_weights(MODEL_PATH)
'''
保存模型可以直接保存模型。也可以保存模型的权重(参数),预测的时候,构造一个同架构的网络,直接加载参数即可。
前者比较友好,很简单,但是队友用了后者就用后者吧,我觉得后者会更快,因为直接加载参数显然比直接加载模型要更简单
'''

四.实战预测

1.自定义一个加载图片的函数。

1
2
3
4
5
6
def load_image(image_path, width, height):
gray_image = cv.imread(image_path, cv.IMREAD_GRAYSCALE)
resized_image = cv.resize(gray_image, (width, height))
data = []
data.append(resized_image.ravel())
return np.array(data)

2.搭建一个网络,并加载权重|参数,这主要是由之前保存只是保存了参数决定的,如果保存的是模型,就直接加载好了。

1
2
3
4
5
6
7
# 加载训练好的模型
model = tf.keras.models.Sequential([
# 网络的具体架构不重复,和前文完全一致
])
# 加载参数
model.load_weights("model/my_cnn_enu")
# 这样,model就是之前训练好的模型了

3.读取图片,reshape成可以进入网络的样子

1
2
3
digit_image_path = "images/english.jpg"
digit_image = load_image(digit_image_path, IMAGE_WIDTH, IMAGE_HEIGHT)
english_image = tf.reshape(digit_image, [-1, IMAGE_WIDTH, IMAGE_HEIGHT, 1])

4.预测

1
2
3
4
5
6
7
results = model.predict(english_image)
# 选概率最大的
num = np.argmax(results[0])
# 通过索引转成对应的字符
new_dict = {v : k for k, v in LABEL_DICT.items()}#反转字典
print("预测结果是:", new_dict[num])
# 预测结束

以上就是全部的内容了,是不是挺简单的呢,反正我觉得挺简单的,哈哈。

让大家看看效果叭。

image1

image2

image3

image4

image5

image6

最后一张预测错惹,没办法,这个A真的很像4。

第一篇技术博客正式完结,实际用时也不久,挺好的。

谢谢观看~