媒介
书接上回,我们已经完成数据预处理部分的内容,后续仍需要对表格进行裁剪,此处略去该操作,接着我们需要建立模型,练习模型等操作,此处采用基于密集卷积网络的文本辨认模型,并结合CTC损失函数,下面是进一步的解释。
1.任务形貌
这里简单说一下我们的任务,以及各文件的作用,以及路径信息。
- src/train_imgs/samples_images/0_3827351228301812241814.jpg:表示练习集中的第一条数据,大小200*32,此中练习集共计3000张图片, 结果如下:
- src/data_3_train.txt存储相应的图片的类别标签,前半部分表示路径信息,后半部分表示标签
- src/labels.txt:用来存储标签对应的数字类别
注意:这里是有个错位关系的,由于第零行是空的,后续会做相干解释
同理,测试集也是如此原理,文件结构如下图所示:
2.模型搭建
密集卷积网络(DenseNet)是深度残差网络(ResNet)的特例,两者主要解决的是深度网络梯度消失和梯度爆炸的题目,随着网络层数的增长,网络回传过程会带来梯度弥散题目,经过几轮后的反传的梯度会彻底消失。
两者区别主要在于:
- ResNet:通过残差毗连(加法)实现特性复用,将输入直接传递到输出。
- DenseNet:通过密集毗连(拼接)实现更高效的特性复用,所有层的特性被重复利用.
详细网络结构如下:
- from tensorflow.keras.models import Model
- from tensorflow.keras.layers import Dense, Dropout, Activation, Reshape, Permute
- from tensorflow.keras.layers import Conv2D, Conv2DTranspose, ZeroPadding2D
- from tensorflow.keras.layers import AveragePooling2D, GlobalAveragePooling2D
- from tensorflow.keras.layers import Input, Flatten
- from tensorflow.keras.layers import concatenate
- from tensorflow.keras.layers import BatchNormalization
- from tensorflow.keras.regularizers import l2
- from tensorflow.keras.layers import TimeDistributed
- def conv_block(input, growth_rate, dropout_rate=None, weight_decay=1e-4):
- x = BatchNormalization(axis=-1, epsilon=1.1e-5)(input)
- x = Activation('relu')(x)
- x = Conv2D(growth_rate, (3,3), kernel_initializer='he_normal', padding='same')(x)
- if(dropout_rate):
- x = Dropout(dropout_rate)(x)
- return x
- def dense_block(x, nb_layers, nb_filter, growth_rate, droput_rate=0.4,
- weight_decay=1e-4):
- for i in range(nb_layers):
- cb = conv_block(x, growth_rate, droput_rate, weight_decay)
- x = concatenate([x, cb], axis=-1)
- nb_filter += growth_rate
- return x, nb_filter
- def transition_block(input, nb_filter, dropout_rate=None, pooltype=1, weight_decay=1e-4):
- x = BatchNormalization(axis=-1, epsilon=1.1e-5)(input)
- x = Activation('relu')(x)
- x = Conv2D(nb_filter, (1, 1), kernel_initializer='he_normal', padding='same', use_bias=False,
- kernel_regularizer=l2(weight_decay))(x)
- if(dropout_rate):
- x = Dropout(dropout_rate)(x)
- if(pooltype == 2):
- x = AveragePooling2D((2, 2), strides=(2, 2))(x)
- elif(pooltype == 1):
- x = ZeroPadding2D(padding = (0, 1))(x)
- x = AveragePooling2D((2, 2), strides=(2, 1))(x)
- elif(pooltype == 3):
- x = AveragePooling2D((2, 2), strides=(2, 1))(x)
- return x, nb_filter
- def dense_cnn(input, nclass):
- _dropout_rate = 0.4
- _weight_decay = 1e-4
- _nb_filter = 64
- # conv 64 5*5 s=2
- x = Conv2D(_nb_filter, (5, 5), strides=(2, 2), kernel_initializer='he_normal',
- padding='same', use_bias=False, kernel_regularizer=l2(_weight_decay))(input)
- # 64 + 8 * 8 = 128
- x, _nb_filter = dense_block(x, 8, _nb_filter, 8, None, _weight_decay)
- # 128
- x, _nb_filter = transition_block(x, 128, _dropout_rate, 2, _weight_decay)
- # 128 + 8 * 8 = 192
- x, _nb_filter = dense_block(x, 8, _nb_filter, 8, None, _weight_decay)
- # 192 -> 128
- x, _nb_filter = transition_block(x, 128, _dropout_rate, 2, _weight_decay)
- # 128 + 8 * 8 = 192
- x, _nb_filter = dense_block(x, 8, _nb_filter, 8, None, _weight_decay)
- x = BatchNormalization(axis=-1, epsilon=1.1e-5)(x)
- x = Activation('relu')(x)
- x = Permute((2, 1, 3), name='permute')(x)
- x = TimeDistributed(Flatten(), name='flatten')(x)
- y_pred = Dense(nclass, name='out', activation='softmax')(x)
- # model = Model(inputs=input, outputs=y_pred)
- return y_pred
复制代码 该网络主要由以下几个部分构成:
- 卷积块(Conv Block):执行卷积操作,增长特性图的数目。
- 密集块(Dense Block):由多个卷积块构成,每个卷积块的输出会与之前所有卷积块的输出毗连起来。
- 过渡块(Transition Block):用于减少特性图的数目,控制模型的复杂度。
- 全毗连层(Fully Connected Layer):将特性图转换为终极的分类结果。
该网络结构十分的复杂,这里我们将其生存在densenet.py后续假如利用该结构,直接import densenet即可。
3.代码解释
由于该网络结构十分复杂,同时个人对keras架构的网络并不是特殊了解,这里通过断点调试的方式对代码进行解读.根据每个断点的内容对代码进行理解,最后会附上完整的代码。
3.1模型加载
首先定义了图片的大小,和每次练习处理图片的数目即batch_size,char_set用于类别标签的对应关系,结果如上所示,下一步通过列表生成式对char_set做了更新,下述对列表推导式进行了解释。
得到的列表中,进行了切片操作,由于第一个值为空,又将结果加了一个特殊字符得到新的char_set,此处可以解释为什么第一行应该是空的,由于此处的切片下标从1开始。
这时通过结果可以看到char_set的值。reload_lib.reload(densenet) 这行代码的功能是重新加载 densenet 模块,个人以为没太大须要。后续又定义了一个get_model函数用来获取模型。只传入了两个参数。
首先定义了输入层的数据类型,形状为 (img_h, None, 1),None 表示该维度的长度可变,这里是图像的宽度维度。批量大小也可以进行调整,由于也是None,预测输出的结果,是一个40维的向量。后续又定义了不定长的标签输入层以及长度。损失函数采用自定义的函数ctc_lambda_func
采用的是CTC损失函数,此中labels表示目标标签,y_pred表示预测标签,input_length表示输入序列的长度,label_length表示目标标签的长度
CTC 损失常用于处理序列数据,特殊是在输入序列和目标序列长度不一致且没有明白的时间对齐关系时,比如语音辨认和手写文字辨认任务。
后续两步用来定义模型(输入包括图像输入、标签输入、输入长度输入和标签长度输入,输出为 CTC 损失)和编译模型(利用自定义的 CTC 损失函数(lambda y_true, y_pred: y_pred),优化器为 adam,评估指标为准确率),今后将模型进行返回。
这时可以查看模型结果和参数信息输出,结果如下:
通过结果可以看到,当前模型参数目很大,而且结构非常复杂
3.2加载数据
在练习前,需要将练习集和测试集进行加载,生存至迭代器中,在pytorch中就十分的很方便,直接Dataloader,而在keras中就很复杂。

这里首先定义了相干路径信息,加载数据集,此处利用了自定义的gen()函数。

此中readfile()也是自定义的函数体。

该函数相对简单,创建一个字典,将文件路径作为键,标签作为值。通过结果可以发现该数据量并不是3000而是2906条数据,中间大概那边少了一部分文件。

结果如图所示,得到上述所示的字典。

_imagefile用于存储文件的路径信息,即将上述所得的字典的键存储成列心情势。后续初始化了一些融入批量大小的输入。反面又定义了一个random_uniform_num函数,用来打乱次序。

调用get()方法取出第一个批量,get()方法体内容很简单:

从属性index开始向后取一个批量大小,假如最后一部分不足一个批量,则取出最后的部分,并重新打乱,取出缺少的部分,将两者融合成一个批次大小。

shufimagefile得到对打乱获取的第一个批次图片的文件名,接着遍历一个批次的所有图片,首先通过os.path.join融合路径信息,得到图片的完整相对路径,并通过Image.open读取该图片,再通过convert('L')将图像转化为灰度图,接着通过一个简单的归一化处理,将图片像素值调整到[-0.5,0.5]之间,再将其生存至x中,并将真实标签长度生存至label_length中,真实的标签信息生存至label中,input_length大小是一个固定值,图片宽度大小除以8取整的结果。
遍历完成后将输入整合成一个字典input,outputs输出(即每个图片的损失)表示为0,最后通过yield (inputs, outputs) 这行代码将一个批次的输入数据和输出数据作为一个元组返回,使 gen 函数成为一个生成器。
注:由于 gen 是生成器函数,调用 gen(…) 时不会执行函数体里的所有代码,只是返回一个生成器对象,gen(…)函数内部的代码并不会在此时执行,这里只是在这里解释该段代码的功能
3.3模型权重的生存
这段代码创建的 ModelCheckpoint 回调函数会在每一轮练习结束后,检查验证集上的损失值。假如当前的验证集损失值是迄今为止最优的,就将模型的权重生存到指定路径的文件中(即./models/weights_densenet-{epoch:02d}-{val_loss:.2f}.h5)。这样可以确保在练习过程中,纵然出现过拟合或其他题目导致模型性能下降,也能生存验证集上表现最好的模型权重。
3.4学习率
这里的学习率并不是一个固定的值,而是随练习轮数不断减小的值。
该处定义了一个lambda函数,用来根据轮数计算学习率,得到如下的学习率列表。
接着通过LearningRateScheduler函数创建一个学习率调度器对象 changelr,用于在模型练习过程中根据练习轮数(epoch)动态调整学习率。
3.5过拟合
为了防止在练习中出现过拟合,创建一个EarlyStopping 回调函数实例,用于在模型练习过程中提前停止练习.
这段代码的意思是,在验证集上的损失值假如在连续 10 个练习轮次(epoch)中没有改善(即没有降低),就会停止练习,并告知用户练习已经提前停止。
3.6练习模型
在练习模型前,对练习集和测试集共有多少条数据进行了统计。
接着通过fit()对模型进行练习,下面临该函数的参数进行解释:
- train_loader:一个生成器对象,由 gen 函数生成。它会在练习过程中不断生成练习数据的批次,每个批次包含输入数据和对应的标签。
- steps_per_epoch:train_num_lines // batch_size 表示每个练习轮次(epoch)中需要执行的步数。train_num_lines 是练习集文件中的行数,即练习样本的总数;batch_size 是每个批次包含的样本数。通过整除运算得到每个轮次需要处理的批次数。
- epochs:8 表示模型练习的轮数,即模型会对整个练习集进行 8 次迭代练习。
- initial_epoch:0 表示练习开始的轮次编号。这里设置为 0,表示从第 0 轮开始练习。
- validation_data:test_loader 是一个生成器对象,由 gen 函数生成,用于提供验证数据。在每个练习轮次结束后,模型会利用验证数据进行评估,以监控模型在未见过的数据上的性能。
- validation_steps:test_num_lines // batch_size 表示在验证过程中需要执行的步数。test_num_lines 是测试集文件中的行数,即测试样本的总数;batch_size 是每个批次包含的样本数。通过整除运算得到验证过程中需要处理的批次数。
- callbacks:[checkpoint, earlystop, changelr] 是一个回调函数列表,包含了在练习过程中需要执行的额外操作。
- checkpoint 是 ModelCheckpoint 回调函数,用于在验证集损失值最优时生存模型的- 权重。
- earlystop 是 EarlyStopping 回调函数,用于在验证集损失值连续 10 个轮次没有改善时提前停止练习。
- changelr 是 LearningRateScheduler 回调函数,用于根据练习轮数动态调整学习率。
3.7调试检查
在练习过程中,假如想观察详细的值,会遇见下述环境:
由于 TensorFlow 默认利用图执行模式(Graph Execution),可以通过在 model.compile 里设置 run_eagerly=True,这样模型会以动态图(Eager Execution)模式运行.
此时,观察相干变量的结果。
这时就能看到结果了,由于默认值labels为1.0e+04,即不是该值的就是正常标签。
大概由于版本不兼容题目,即大概是tensorflow版本太老了,PyCharm 调试器出现了一些关于历程的报错信息,直接运行不影响结果,因此没须要太担心,直接更新tensorflow又会导致当前环境中其他包不兼容,因此就没有解决该题目。
4.结果分析
这里由于网络结构十分复杂,同时该网络的大部分参数是采用正态分布(高斯分布)来随机初始化权重
所以效果大概很差,比如如下结果:
假如你想提高该模型的效果,你可以选择一些与练习好的模型来对网络参数进行初始化,或者调整轮数,自行练习,上述代码的练习轮数最高只能8轮,由于下述代码动态调整学习率的缘由
可以将上述代码中的changelr做一下调整,可得到最高练习轮数89
- changelr = LearningRateScheduler(lambda epoch: float(learning_rate[epoch//10]))
复制代码 固然效果大概也不是很好,由于这种复杂的模型,练习轮数大概要成千上万,这里就不再展示,由于我的还是cpu版本的,练习速度慢,假如你的算力足够,可以将轮数调整到一个较大的值,由于检测到过拟合会自动停止练习。
5. 完整代码
可参考上述图片进行编写,调试过程对代码均已截图。假如无数据集,建议自行查找,即查找该册本对应章节的数据集即可。
本案例参考教材:《Python机器学习实战案例(第二版)》赵卫东、董亮
结语
至此,该项目已经完成,虽然终极的效果大概并不太好,但主要理解其练习过程,撒花撒花!!!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |