作者:vivo 互联网客户端团队- Li Quanlong
本文介绍端智能技术在vivo官网APP的落地实践,通过抽象问题、提出端智能解决方案、方案落地这三大块内容逐步递进地展开端智能技术的应用过程。
一、前言
vivo官网APP首页是流量最大的页面,承载着新品、活动、商品、其他入口等流量分发的重任。在流量分发上,云端针对首页的主要场景建设了算法支撑。通过梳理首页的场景发现,智能硬件楼层场景的商品配置还是运营纯手工动态配置,而非算法推荐。为此,我们探索了端智能技术,将其运用在智能硬件楼层场景,用于提升商品分发效率,进而提升智能硬件楼层场景的点击率。
端智能广义上来说,是指将人工智能算法部署到端侧设备中,使端侧设备具备感知、理解和推理能力;狭义上来说,端智能就是将机器/深度学习算法集成到端侧设备中,通过算法模型处理端侧感知的数据从而实时得到推理结果。而所谓的"端"实际上是相对于"云"的概念,是一些带有计算能力的个体设备,如手机、家庭路由器、网络的边缘节点等。因此,可以看到端智能的应用离不开这几个关键点:数据、算法模型及计算能力。
二、抽象问题
端智能是如何提高vivo官网APP首页智能硬件楼层场景的商品分发效率的呢?在回答这个问题之前,我们先了解下智能硬件楼层场景,如下图所示:
图1:vivo官网APP首页-智能硬件楼层
智能硬件楼层场景,有4个商品展示资源位,由运营在众多的智能硬件商品中挑选出4个商品进行配置。所以,不同的用户群体进入到vivo官网APP首页看到该场景下的商品都是相同的。而引入端智能技术要解决的问题是:不同的用户群体看到的商品推荐是不一致的,是更加符合该用户群体的商品,做到推荐的精准匹配,如下图所示:
图2:智能硬件楼层商品分发
端智能推荐分发,就是在智能硬件资源池中推荐最适合的4个商品展示在智能硬件楼层场景中。我们抽象下问题,也就是在N个商品中选取前K个商品(K\<=N)进行展示,进一步思考下,如何选取前K个商品呢?其实本质是将N个商品按照推荐的概率值进行排序,选取概率值较大的前K个商品。因此,问题就可以进一步被抽象为设计一个算法模型,通过对用户群体的特征分析,输出该用户群体对N个商品感兴趣的概率值。
三、端智能方案
为什么是使用端智能技术,而不是使用人为约束规则或者云端模型来解决"针对某个用户群体,N个商品被推荐的概率值"的问题呢,是因为端智能技术的优势是:
- 推理计算是在端侧进行的,可以有效地节约云端计算资源及带宽;
- 因为在端侧进行,响应速度相较于网络请求会更快;
- 端侧的数据处理是本地的,数据隐私更安全;
- 算法模型是使用深度学习算法通过训练样本学习出来的,而非人工规则约束,因此可以应对复杂的场景,做精细化的推荐。
3.1 整体架构
vivo官网APP端智能整体架构设计如下:
图3:vivo官网APP端智能整体架构
端智能整体架构主要由模型离线训练、云端配置、APP端执行三大模块构成,离线训练主要负责算法模型的训练生成、模型转换以及模型发布;云端主要负责模型版本管理和模型运行及埋点监控;APP端在业务调用的时候主要负责计算推理,通过设备感知数据,将其通过特征工程处理后输入到算法模型中,模型在TensorFlow-Lite的基础上充分调用设备计算资源进行推理得到结果后反馈给业务使用。
**离线训练:**将原始数据进行数据清洗后,送入到设计的特征工程中处理为算法模型能够处理的特征。然后使用TensorFlow深度学习语言搭建网络模型,并使用处理后的数据进行训练得到后缀为.h5的模型文件,需要将该模型文件通过模型转换为后缀为.tflite的文件,因为.tflite的文件是可以在Android上通过TensorFlow-Lite工具库加载并执行推理。
**云端:**每次模型的训练都有相应的版本控制及监控能力,这样方便做模型的ABTest实验,也可以动态的升级和回退线上模型版本。
**APP端:**Google提供了TensorFlow-Lite机器学习工具库,在此库上可以加载后缀为.tflite的模型文件,并提供了执行前向计算推理能力。因此,在此基础上,APP端侧通过实时地感知数据,并通过特征工程处理后得到特征数据后,可以在端侧利用TensorFlow-Lite提供的能力进行加载模型并运行模型,进而实时得到计算推理结果。
3.2 原始数据
无论是离线的训练模型,还是APP端侧的计算推理都离不开"数据",因为数据是整个架构的灵魂所在。所以,在明确了要解决问题的前提下,就需要全面梳理埋点上报信息,查看下当前拥有的数据信息,如下图所示:
图4:原始数据特征
从上图可以看到能够获取到很多纬度的数据信息,但是在能够被利用之前,要先进行数据清洗工作。并不是每个信息都会包含这些纬度的信息、也并不是每个纬度的信息都同样重要,所以要结合场景来做数据的清洗。例如,在智能硬件楼层场景下,基础信息设备型号很重要,因为这个信息代表着当前用户使用的是哪款手机型号,所以在数据清洗时,若此信息采集时是为空,则需要设计默认值的方式进行处理。
在完成了数据清洗工作后,就可以进行下一步动作了,将设备型号、性别、所在城市等语义信息,通过特征工程处理为能够输入到算法模型处理的特征信息。在本文案例中,我们以基础信息为例子来展开介绍,基础信息如手机设备型号、当前用户所在城市、用户性别等。举例,若一个男性用户在使用x200手机访问vivo官网APP,并且授予vivo官网APP定位权限,获取的定位在南京,则当前获取一条原始数据为"vivo x200,南京,男"。
3.3 特征工程
获取的原始数据是自然语言的特征,需要将其处理为算法模型能够处理的数字化表示形式的离散特征。现在AI大模型层出不穷,可以很好地处理自然语言,输入的自然语言可以通过Embedding模型或者Word2Vec进行特征处理,将自然语言处理为模型能处理的"0.1 0.6 ... 0.4"形式,如下图所示:
图5:语义特征
通过Embedding模型处理的特征是具有语义相似性的,但回归本文,我们并不需要这种具有语义相似性的特征。输入的特征纬度越大,则模型的复杂度就越高,而需要的计算资源也就越高。在端侧做计算推理,在满足业务需求的前提下,算法模型要足够小,因此我们化繁为简,采用简单的方式来处理原始数据。这个时候,我们回顾下原始数据"vivo X200,南京,男",这几个特征实际上是并列关系,我们可以逐个地处理为One-Hot编码特征表示,然后再组装。
3.3.1 位置特征
我们以地理位置信息为例,用户可以分布在全国的各个地方,如在北京、上海、南京、重庆等,因此,我们需要找到一定的规律来处理地理位置信息。这个时候需要通过大数据的方式,大致了解到城市消费的平均价位信息的分布情况,像北京、上海等城市消费趋势相当;像武汉、合肥等城市消费相当。因此,我们可以根据此反映出来的现象,将消费趋势一致的城市归为一个聚类。例如,可以简单划分为三个聚类,分别是聚类A、聚类B、聚类C,通过这样的方式,就可以将不同的城市划分到这三个聚类中去。如"南京"属于聚类A,"扬州"属于聚类B等。再进行抽象下,可以使用三位数字(000)这种形式表示聚类。第一位数字0表示不是聚类A,数字是1则表示是聚类A,这样就可以通过三位数字来表示地理位置信息了。如,可以通过"100"来代表"南京"。
**默认值处理:**如果此时获取不到地理位置信息,可以按一定策略处理获取不到的信息进行填充,如,新增一位聚类D表示是没有获取到地理位置信息,亦或者将其都笼统归纳为聚类C。
3.3.2 设备型号
我们再看下,如何处理手机设备名称特征的。随着手机的更新迭代,市场上的手机名称也枚不胜举。但是,每款手机都是有其定位的,而这个定位实际上是可以通过手机发布的系列能够得知的。如,vivo X200手机是旗舰机型,属于vivo的X系列。参考地理位置信息通过聚类的分类方法,我们也可以简单地将设备机型名称通过系列来做区分。在vivo官网APP的选购页面,可以看到vivo品牌下有三个系列机型,分别是X系列、S系列和Y系列。因此,我们如法炮制,也可以使用三位数字(000)这种形式来表示设备机型名称。例如:第一位数字0表示不是X系列手机,1表示是X系列手机;同理第二位数字0表示不是S系列手机,1表示是S系列手机等。因为,vivo X200属于X系列,所以可以将其vivo X200原始数据处理为"100"来表示,同理如果此时获取手机设备是vivo S20 Pro,则被转换为抽象的特征"010"来表示。
**默认值处理:**参考位置特征的处理方式,可以新增一位One-Hot特征表示UnKnow,即未获取手机设备名称特征时,UnKnow这一列数字为1,反之为0。
3.3.3 性别特征
性别可以使用3位数字(000)表示,第一位表示女性,第二位表示男性,使用第三位表示未获取性别时的默认值。如,"男"可以通过"010"表示。
3.3.4 特征组合
因此,通过上述的处理规则,就可以将一条原始数据"vivo x200,南京,男"处理为"1001000010"来表示了,如下图所示:
图6:特征组合
特别需要注意的是,自然语言特征组合的顺序和处理后的数字化特征顺序一定是保持一致的。
3.4 算法模型
3.4.1 算法模型整体架构
算法模型整体架构如下图所示:
图7:算法模型整体架构
由三部分构成:分别是输入层、隐藏层及输出层。
- **输入层:**是经过特征工程处理后的特征,主要是用户基础信息和实时上下文信息。
- **隐藏层:**由多层全连接神经网络组成,使其拥有非线性变化能力,可以在更高纬度空间中逼近寻找拟合特征的最优解。
- **输出层:**第一个输出层是将每个分类商品打上标签,输出该标签概率集合;第二个输出层是在第一个输出层的基础上直接通过模型中存入的标签和商品SkuId映射关系,直接输出商品SkuId集合。
在设计算法模型的时候,要选择适合贴近要处理的场景,而非盲目地选择大模型做基座或者搭建很深的模型。不仅需要考虑模型的预测准确率,还需要考虑端侧的计算资源。
3.4.2 模型代码示例
算法模型的核心代码,如下示例:
# 输入层
input_data = keras.layers.Input(shape=(input_size,), name="input")
output = tf.keras.layers.Dense(64, activation="relu")(input_data)
output = tf.keras.layers.Dense(128, activation="relu")(output)
output = tf.keras.layers.Dense(output_size, activation="softmax")(output)
# 输出层:top k index
output = NewSkuAndFilterPredictEnvSkuIdLayer(index_groups=acc.index_groups, )(output)
# 输出层:index 映射 skuid 直接出
output = SkuTopKCategoricalLayer(acc.skuid_list, spu_count=len(acc.spuid_list))(output)
3.4.3 模型训练
**原始数据:**训练模型的原始数据是来源于大数据提供的埋点信息数据,例如获取当前日期前3个月的埋点信息数据。
**数据清洗:**并不是所有的原始数据都可以拿来直接使用,有部分数据是不符合约束条件的,称之为脏数据,即将不满足约束规则的脏数据清理后,则可以获得真正的用于训练模型的数据。
**2:8分割原则:**为了验证模型训练的Top5的准确率,将清洗后的数据,80%分为训练数据、20%分为测试数据;需要注意的是,训练数据在真正训练的时候,需要使用shuffle来打散,可以增强模型的鲁棒性。
代码示例:
train_dataset = tf.data.Dataset.from_tensor_slices((train_data, train_labels))test_dataset = tf.data.Dataset.from_tensor_slices((train_data, train_labels))# 使用shuffle方法对数据集进行随机化处理# 参数 buffer_size 指定了用于进行随机化处理的元素数量,通常设置为大于数据集大小的值以确保充分随机化、batch() 则指定了每个批次的数据量。train_dataset = train_dataset.shuffle(buffer_size=datasets.train_len).batch(config.BATCH_SIZE)test_dataset = test_dataset.shuffle(buffer_size=datasets.test_len).batch(config.BATCH_SIZE) # 获取模型输入、输出纬度大小acc_input_size = datasets.acc_columns - 1acc_out_size = datasets.acc_output # 构建模型model = build_parts_model(acc_input_size, acc_out_size)optimizer = tf.keras.optimizers.legacy.RMSprop(learning_rate=config.LEARN_RATIO)model.compile(optimizer=optimizer, loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=[tf.keras.metrics.SparseTopKCategoricalAccuracy(k=5, name="top5_acc_accuracy")])model.fit(train_dataset, epochs=config.TRAIN_EPOCH)
3.4.4 保存模型
通过13轮epoch的训练后,获得TensorFlow模型:
train_dataset = tf.data.Dataset.from_tensor_slices((train_data, train_labels))
test_dataset = tf.data.Dataset.from_tensor_slices((train_data, train_labels))
# 使用shuffle方法对数据集进行随机化处理
# 参数 buffer_size 指定了用于进行随机化处理的元素数量,通常设置为大于数据集大小的值以确保充分随机化、batch() 则指定了每个批次的数据量。
train_dataset = train_dataset.shuffle(buffer_size=datasets.train_len).batch(config.BATCH_SIZE)
test_dataset = test_dataset.shuffle(buffer_size=datasets.test_len).batch(config.BATCH_SIZE)
# 获取模型输入、输出纬度大小
acc_input_size = datasets.acc_columns - 1
acc_out_size = datasets.acc_output
# 构建模型
model = build_parts_model(acc_input_size, acc_out_size)
optimizer = tf.keras.optimizers.legacy.RMSprop(learning_rate=config.LEARN_RATIO)
model.compile(optimizer=optimizer, loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics=[tf.keras.metrics.SparseTopKCategoricalAccuracy(k=5, name="top5_acc_accuracy")])
model.fit(train_dataset, epochs=config.TRAIN_EPOCH)
3.4.5 模型转换
通过上面的步骤,可以得到在一个后缀名为.h5文件的模型文件,但我们需要将模型部署在APP端侧运行,端侧运行环境是依赖TensorFlow-Lite工具库,因此需要将.h5文件的模型转换为.tflite文件模型。
1.TensorFlow-Lite工具库
TensorFlow Lite学习指南中的官方介绍表明:"TensorFlow Lite 是一组工具,可帮助开发者在移动设备、嵌入式设备和 loT 设备上运行模型,以便实现设备端机器学习"。简而言之,端侧设备引入TensorFlow Lite工具库后,就可以加载机器学习模型,并且执行前向计算推理能力。若不使用该工具库,需要团队自行研发可以运行在端侧的机器学习模型的执行环境,如:裁剪python,保留其核心能力,像字节的pitaya框架。因此,引入该工具库,对我们团队来说就可以减少在端侧构建执行环境的巨大开发工作量。
2.算子兼容
使用TensorFlow-Lite工具库的时候,需要考虑算子兼容性问题。TensorFlow Lite内置算子 TensorFlow 核心库的算子的一部分,可以见下面图的包含关系:
图8:TensorFlow算子兼容
(图片来源:TensorFlow官网)
因此,在使用TensorFlow深度学习语言设计算法模型时,要考虑到算子的兼容性,避免出现在本地可以工作的模型,在端侧因为算子不兼容而导致部署失败。
3.模型转换
(1)非TensorFlow深度学习语言模型转换
使用其他深度学习框架训练的话,可以参考使用如下两个方式转换为.tflite文件模型,以Pytorch深度学习框架语言为例
根据Pytorch的代码,使用TensorFlow重写,得到TFLite文件
使用工具ONNX转换
若使用ONNX工具,则转换的链路是这样的:PyTorch -\> ONNX -\> TensorFlow -\> TFLite
(2)TensorFlow深度学习语言模型转换
使用TensorFlow深度学习语言训练的话,可以直接使用下述代码,转换为Android端侧可以加载的模型文件.tflite
图9:TensorFlow模型转换
(图片来源:TensorFlow官网)
模型转换代码示例如下:
1、Keras Model
训练过程中,直接将model转成tflite
converter = tf.lite.TFLiteConverter.from_keras_model(model)
# 转换模型
tflite_float_model = converter.convert()
# 保存模型
with open('mnist_lenet5.tflite', 'wb') as f:
f.write( tflite_float_model)
2、SavedModel
model.save('mnist_lenet5_save_model', save_format='tf')
# 转换模型
converter = tf.lite.TFLiteConverter.from_saved_model('mnist_lenet5_save_model') tflite_model = converter.convert()
# 保存模型
with open('mnist_lenet5_saved_model_dir.tflite', 'wb') as f:
f.write(tflite_model)
3.5 计算推理
Google提供了TensorFlow Lite库,可以加载TFLite文件,并在端侧利用本地计算资源完成推理,注意这里只是有推理能力,而不提供训练。如下所示:
dependencies {
implementation 'org.tensorflow:tensorflow-lite:2.14.0'
}
通过添加模块依赖项将TensorFlow Lite库引入到APP应用中。
3.5.1 加载模型
假设现在模型文件是放在assets文件目录下,那么首先先加载模型,构建Interpreter对象,如下代码:
private Interpreter mTfLite = new Interpreter(loadModelFileFromAssets("文件名"))
3.5.2 模型运行推理
加载好模型文件后,就可以直接调用推理API了,推理API提供了两个,分别是:
// 通过API的名称其实就能够看出区别了,run()是单个输入调用,runForMultipleInputsOutputs是多个输入调用,
// run方法实际执行时也是调用runForMultipleInputsOutputs
// 实际上mTfLite .run()底层
mTfLite .run()
mTfLite .runForMultipleInputsOutputs()
因此,只需要准备好输入和输出数组即可直接调用,如下代码所示:
// 输入数组
float[][] inputDataArray = new float[][]{inputDataArrayLen};
// 输出数组
float[][] outputDataArray = new float[1][outputDataLen];
// 运行推理
mTfLite .run( inputDataArray , outputDataArray);
四、方案落地
4.1 模型配置
在设计算法模型之初,需要考虑输入和输出纬度大小,尽量将输入和输出的纬度固定下来,这样的话当不断迭代算法模型时,就不会因为输入和输出纬度不一致导致不能兼顾到之前的APP版本,进而做到模型可动态升级。在模型配置中心管理模型版本、下发策略,在端侧下载模型时可通过模型版本号及文件MD5值校验模型文件的完整性。
4.2 运行监控
模型上线后,需要监控线上执行的效果。主要关注三个指标:
- **模型运行成功率:**加载模型、执行模型上报是否成功
- **模型版本分布:**模型版本升级情况,便于分析数据
- **模型各版本的平均运行时长:**关注运行时长,进而指导模型设计时考虑模型的复杂程度
图10:模型运行监控
如上图所示,通过建立完善的线上监控,一方面清楚了解模型运行情况,另一方面可以提供设计算法模型的方向,进而更好地迭代出一个模型,可以在端侧设备计算资源及收益效果达到一个各方都还不错的平衡点。
五、总结
要使用端智能能力,首先要知道解决什么问题。如,本文是解决的"重排序"问题,其实本质就是"多分类"问题。知道了具体解决的问题后,要进一步抽象出输入是什么、输出是什么。此时,还需要盘点当前能够获取哪些原始数据,并设计特征工程去处理好原始数据,将其转换为算法模型能够接受的特征。再紧接着,就是要结合应用场景设计好算法模型即可。
本文通过利用端智能重排序云端返回的商品信息,进而不同人群展示不同商品信息,实现千人千面效果。从获取原始数据开始,到特征工程的设计,设计符合该业务场景的算法模型,然后进行训练获取模型,再进行模型转换为TFLite文件格式,通过端侧加载该模型后,进行计算推理获取重排序后的结果。
在端智能的道路上,一方面我们继续探索更多的落地场景,另一方面继续挖掘丰富的端侧数据,更新迭代特征工程及算法模型,更好地为业务创造价值。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。