这里先简单介绍一下白屏监控实现方式,在进入webview后,由客户端对webview进行截屏随后上传图片到 OSS,并进行埋点。
在flink层消费埋点数据,获取图片,对图片判定结果(白屏,非白屏)进行落库。
最开始的判断方式是对图片像素点进行遍历,看是否有纯色区域占比大于90%,有的话就认为是白屏。
这种策略发布后我们发现了很多bad case, 最典型的当属任务完成倒计时、用户搜索页面,这种页面纯色区域都是大于90%的,但是我们不能认为他是白屏,针对各种复杂的情况,我们最终考虑由机器学习来自动识别我们的图片。
前后端整体流程
TensorFlow是一个端到端开源机器学习平台。
它拥有一个全面而灵活的生态系统,其中包含各种工具、库和社区资源,可助力研究人员推动先进机器学习技术的发展,并使开发者能够轻松地构建和部署由机器学习提供支持的应用。
本文使用的版本
Python 3.9
tensorflow 2.6.0
首先准备训练数据,将收集到的2200+张图片分类存放在不同的文件夹中,如下所示
train_data/
white/
white_loading/
white_error/
network_error/
not_white/
接下来我们使用这些数据, 先获取待训练数据所在路径,然后我们将数据集的80%用于训练,20%用于验证(在开发模型时使用验证拆分是一种很好的做法)。
import os
import pathlib
import tensorflow as tf
data_dir = pathlib.Path(os.path.dirname(__file__) + '/../train_data')
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="training",
seed=123,
image_size=(IMAGE_HEIGHT, IMAGE_WIDTH),
batch_size=batch_size)
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="validation",
seed=123,
image_size=(IMAGE_HEIGHT, IMAGE_WIDTH),
batch_size=batch_size)
随后我们就可以通过 train_ds.class_names方式来获取标签,为了在后续图片识别中使用我们需要把这些标签保存下来。
class_names = train_ds.class_names
save_data_to_file(list2LineData(class_names), 'white_screen_model/labels.txt')
Dataset.cache()在第一次从磁盘加载图像后,将图像保存在内存中。
这将确保数据集在训练模型时不会成为瓶颈。
如果数据集太大而无法放入内存,也可以使用此方法来创建高性能的磁盘缓存。
Dataset.shuffle() 会随机打乱我们的数据集。
Dataset.prefetch() 会创建一个从数据集中预取 buffer_size 大小的数据集。
AUTOTUNE = tf.data.AUTOTUNE
train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)
接下来,我们来展示一下待训练数据中的前9张图片。
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 10))
for images, labels in train_ds.take(1):
for i in range(9):
ax = plt.subplot(3, 3, i + 1)
plt.imshow(images[i].numpy().astype("uint8"))
plt.title(class_names[labels[i]])
plt.axis("off")
当我们的代码运行到这里时可能会遇到报错,笔者在这里就遇到坑了,报错信息如下:
看起来好像是图片损坏了,一开始我想把损坏的图片全部删掉,这样代码就可以继续往下跑了,但是经过检测后发现大部分图片都是损坏的,如果删掉的话会影响训练效果。
这个时候我们转变思路,图片明明可以正常预览,我们能不能通过某种手段把图片修复一下呢?
功夫不负有心人,经过不懈的努力(google)后,终于成功修复了图片,代码如下:
from PIL import Image, ImageFile
# 传入的图片已经超过了MAXBLOCK限制的大小,PIL处理不了,必须要把这个图片删除一部分
ImageFile.LOAD_TRUNCATED_IMAGES = True
def repairImg(imgPath) :
try:
img = Image.open(imgPath)
img = img.resize((IMAGE_WIDTH, IMAGE_HEIGHT), Image.ANTIALIAS)
img.save(imgPath)
return img
except:
print("repairImg error:", imgPath)
展示效果如下:
数据归一化
首先我们需要对数据进行归一化处理,当我们使用梯度下降法寻找最优解时,不归一化造成的后果就是我们很可能需要走“之字形”路线才能慢慢逼近正确值,从而导致需要更多的迭代次数。
如下图:左图未归一化,右图归一化
接下来我们将 RGB 通道值进行归一化处理,统一将通道值乘以 1/255,从而将数据控制在 0-1 范围内。
layers.experimental.preprocessing.Rescaling(
1./255, input_shape=(IMAGE_HEIGHT, IMAGE_WIDTH, 3)
)
构造卷积神经网络模型
layers.Conv2D
该层创建了一个卷积核, 该卷积核对层输入进行卷积, 以生成输出张量。接下来我们重点介绍一下本次使用到的这些参数
tf.keras.layers.Conv2D(
filters, kernel_size, strides=(1, 1), padding='valid'
)
filters 含义是过滤器个数,或者叫卷积核个数,这个与卷积后的输出通道数一样
kernel_size 卷积核尺寸,一般为3×3或者5×5,如果长宽一样,可以简化为3或者5
strides 滑动步长
padding “valid”意味着不填充,“same”是在输入的左/右或上/下均匀填充,这样输出与输入具有相同的高度/宽度维度。
activation 激活函数,如下左图,在神经元中,输入的 inputs 通过加权,求和后,还被作用了一个函数,这个函数就是激活函数。
layers.Conv2D
它的作用是对卷积层输出的空间数据进行池化(采样),采用的池化策略是最大值池化。它将输入的图像划分为若干个矩形区域,对每个子区域输出最大值。效果见下图:
layers.Conv2D(16, 3, padding='same', activation='relu')
layers.MaxPooling2D()
layers.Conv2D(32, 3, padding='same', activation='relu')
layers.MaxPooling2D()
layers.Conv2D(64, 3, padding='same', activation='relu')
layers.MaxPooling2D()
layers.Flatten 会把多维数据展开为一维数据, 随后添加一个全连接层输出空间维度为 128,激活函数为“relu”,最后一个全连接层输出的空间维度就是我们的分类数量,在本例中也就是 5,每个节点都包含一个得分,用来表示当前图像属于 5 个分类中的哪一个。
num_classes = len(class_names)
layers.Flatten()
layers.Dense(128, activation='relu')
layers.Dense(num_classes)
接下来我们用 keras.Sequential 模型将层串起来,Sequential 模型适用于一个普通的层堆栈,其中每一层恰好有一个输入张量和一个输出张量。
num_classes = len(class_names)
model = Sequential([
layers.experimental.preprocessing.Rescaling(1./255, input_shape=(IMAGE_HEIGHT, IMAGE_WIDTH, 3)),
layers.Conv2D(16, 3, padding='same', activation='relu'),
layers.MaxPooling2D(),
layers.Conv2D(32, 3, padding='same', activation='relu'),
layers.MaxPooling2D(),
layers.Conv2D(64, 3, padding='same', activation='relu'),
layers.MaxPooling2D(),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dense(num_classes)
])
损失函数
用来估量你模型的预测值f(x)与真实值Y的不一致程度,损失函数越小,模型的鲁棒性就越好。
SparseCategoricalCrossentropy损失函数 计算标签和预测之间的交叉熵损失。
当使用交叉熵处理具有大量标签的分类问题时会提前对标签进行热编码,如果标签数据较多的话会占用大量的内存,SparseCategoricalCrossentropy 通过执行相同的误差交叉熵计算来解决这个问题,不需要在训练之前对目标变量进行热编码。
优化器
调节神经元的权重和偏置量,使得损失函数的返回值尽可能的小,这就是优化器的作用。
Adagrad 专门针对各个特征调整学习率:这意味着数据集中的某些权重与其他权重具有不同的学习率。它总是在缺少大量输入的稀疏数据集中效果最佳
Adadelta 是另一种更加改进的优化算法,这里的 delta 指的是当前权重和新更新的权重之间的差异。Adadelta 完全取消了学习率参数的使用,取而代之的是平方增量的指数移动平均值。
RMSprop 它是由 Geoffrey Hinton 开发的Adagrad的独家版本,这个优化器背后的想法非常简单:不是让所有的梯度积累动量,它只在特定的修复窗口中积累梯度
Adam 是一种使用过去梯度计算当前梯度的方法,他的优点有:计算效率高,内存需求小。即使很少调整超参数,通常也能很好地工作。
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
在训练一开始所有的神经元都会被随机初始化,也就是全靠猜,然后我们要计算猜测值和正确结果之间的误差,然后使用误差来调整神经元之间的链接强度。随着训练的进行,我们就会慢慢的逼近正确答案,最终得到一个不错的预测模型。
epochs=15
history = model.fit(
train_ds,
validation_data=val_ds,
epochs=epochs
)
model.save('saved_model/white_screen_model')
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs_range = range(epochs)
plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
这个时候我们发现,随着训练的进行,训练数据的损失值在不断降低,但是校验数据的损失值却开始反方向上升,这是发生了过拟合现象,考试是王者,实战是青铜,那怎么样解决过拟合问题呢?
为数据集添加更多的训练数据,在 TensorFlow 官网中有介绍,我们对图片进行旋转、剪切、添加噪声从而增加数据集
添加权重正则化,您可能熟悉奥卡姆剃刀原理:给出对某事的两种解释,最有可能正确的解释是“最简单”的一种,即假设最少的一种。这也适用于神经网络学习的模型:给定一些训练数据和网络架构,有多组权重值(多个模型)可以解释数据,与复杂模型相比,更简单的模型不太可能过度拟合。
降低模型复杂度,减少标签数量
我们的模型对细节点学习的太多了,那我们就通过 layers.Dropout(0.2) 丢失一部分学习数据
左图过拟合、右图解决过拟合问题后
我们通过 keras.models.load_model 加载我们保存在本地磁盘的模型
从远程下载图片到本地,或者直接读取本地图片,并将图片缩放至训练时的大小
将图片转化我 Numpy 数组, 并调用 model.predict为输入样本生成预测值
class_names = read_file_line(pathlib.Path(
'./saved_model/white_screen_model/labels.txt'))
# 读取模型
model = tf.keras.models.load_model('./saved_model/white_screen_model')
# 从远程下载图片到本地并获取图片所在磁盘路径
img_path = tf.keras.utils.get_file(origin=img_url)
## 读取图片
img = keras.preprocessing.image.load_img(
img_path, target_size=(IMAGE_HEIGHT, IMAGE_WIDTH)
)
img_array = keras.preprocessing.image.img_to_array(img)
img_array = tf.expand_dims(img_array, 0)
predictions = model.predict(img_array)
score = tf.nn.softmax(predictions[0])
# np.argmax 返回最大值的下标
label = class_names[np.argmax(score)]
# np.max 返回最大值
score = 100 * np.max(score)
功能如下:
查看各业务域白屏率走势,指标异常告警,以及具体的白屏问题列表,根据页面的url进行聚合
进入详情页面,展示具体每次发生白屏的相关信息(网络、设备、App版本等基础信息、用户的id、加载的静态资源列表、页面性能数据)。
查看行为日志,用户是否切换到后台,是否有JS报错信息等。
首先还是很感谢大家看到这里,这次的文章就分享到这儿,接下来,你可以动手去训练属于自己的模型了。若有纰漏之处,欢迎各位大佬不吝赐教。
参考链接
1.TensorFlow官方教程:
https://www.tensorflow.org/tutorials?hl=zh_cn
2.维基百科-卷积神经网络:
https://zh.wikipedia.org/wiki/%E5%8D%B7%E7%A7%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C
3.如何选择损失函数:
*文/徐铭