原创 Jeffrey 得物技术
AI在目前互联网可以说早已渗入各个业务。搜索、预测、图像识别、语音识别等等早已应用较为成功。
得物被广大年轻消费者熟知的一个原因是其强大的质量与正品保障体系,而为此背后需要的是得物供应链体系艰辛的付出。
算法推出模型中台解决方案,来辅助人工做瑕疵检测与一致性识别。同时需要将部分AI的功能赋予端侧,让其能辅助模型中台更加准确高效的实现高效的瑕疵检测与一致性识别。所以我们思考在端侧部署AI标准化引擎去对拍摄的照片做初步的筛选和部位分类。
Pink客户端是得物供应链体系中面向质检、鉴别等流程的入口,也是工作人员为每件商品质检、鉴别的入口,其承载了整个得物体系中供应链重要的一环,从得物平台购的每一笔订单都会通过Pink客户端。
标准化引擎则是天眼系统中的一部分,他集成在pink客户端目的是为了快速实时反馈响应现场人员,辅助工作人员帮助其完成质检、鉴别并留档。
天眼系统致力于颠覆传统人工质检、鉴别体系, 理论上达到自动化质检、鉴别。
系统的目标:
总体架构如下:
其中客户端的重点则是标准化引擎。
拆解开来看可分为 模型AI识别模块、动态化模型模块、图片质量识别模块。
其主要是结合具体的业务场景,比如根据商品类目加载模型、根据识别部位调用配置中的算法模型。本文会在模型AI识别模块中做一些举例。
我们先来回顾下基础知识,可能很多人对机器学习有一个误解,觉得是比较高深的东西。可能是从想去了解机器学习从一些比较系统的书籍入手,比如比较有名的的 Ian Goodfellow 的《Deep Learning》, 国内的周志华的《机器学习》。
当然这些都是非常有名的书籍,如果对机器学习的理论感兴趣的,也建议去读下。但就作为初学˙者来说,并且公司在公司也是需要尽快落地项目的角度,其实完全可以从使用者角度去理解一些机器学习的一些基础概念,如何交互入手,以可以尽快落地为目的。回过头等有时间或者有更深层次的需求再来深挖。
目前关于机器学习主要的开发流程是:
本文我主要介绍下跟上述开发流程中端侧相关的基础概念。
4.2.1.1 模型
一个简单的理解,可将算法模型理解为函数,我们也不用管举例深度学习中神经网络的概念,一个训练好的模型,就是一个实现好的函数。
它能接受参数,根据不同参数,在推理引擎的执行下,产生一个可预期的结果。
4.2.1.2 推理引擎
如果说把模型比作是是函数,那么推理引擎就是函数的执行者。当然特定引擎需要特定的模型才能识别,不同框架可能定义了不同的算子导致不能兼容。
具体的来说,以我们使用的MNN为例, 需要算法团队的小伙伴也给mnn框架引擎能执行的模型才行。
4.2.1.3 技术选型
调研了目前阶段较流行的框架,经过初步筛选主要有苹果的Core ML、腾讯的NCNN、Google的TensorFlow Lite、阿里巴巴的MNN。结合我们业务场景,设备的以及目前技术团队的资源,双端支持等特点,选择了阿里的MNN推理框架。
但是这个也不是代表未来就局限在这个框架上面。上面的动态化模型子系统目前处于规划阶段,后续完全可以在模型下发的时候完全可以增加框架这一选项,利用动态化来动态来指定框架来执行推理。
在选定合适的框架之后,我们开始接入得物,我这里简单介绍接入与封装。
4.2.2.1 推理引擎接入
接入的难点在于需要在macOS上针对Android、iOS的系统架构、以及CPU指令集架构做交叉编译。此工作较为繁琐与机械化,由于篇幅限制,就不再赘述,感兴趣的小伙伴可以参考MNN接入指南【1】来操作,具体碰到问题可以私聊,相互学习。
4.2.2.2 引擎的封装
模型本身只是训练好的各个特征向量的组合连接,如同人类神经元的组合连接。没法执行需要借助执行引擎去执行,下面是完整的流程:
结合上述流程,以Android为例:
interface DuFriday {
// 初始化标准化引擎
fun init();
// 加载特定模型
fun createInstance(model: String?, callback: Callback<Map<String, Any?>>?);;
// 创建会话
fun createSession(id: Long?, callback: Callback<Map<String, Any?>>?);
// 运行会话
fun runSession(imagePath: String, type: Int?, callback: Callback<Map<String, Any?>>?);
// 释放单个会话
fun releaseSession(sessiondId: Long?);
// 释放所有会话
fun releaseAllSession();
// 释放单个模型
fun releaseInstance(id: Long?);
// 释放所有模型
fun releaseAllInstance();
// 退出
fun quit();
}
上述接口,提供给业务使用,考虑到推理过程是个耗时过程,因此将其封装到一个单独的线程中去实现,以Android为例:
单独的线程中去实现,以Android为例:
private var mThread: HandlerThread? = null
private var mMainHandler: Handler? = null
runSession() {
mHandler?.post {
val response = runSession(); // 伪代码
mMainHandler?.post {
callback?.onSuccess(response)
}
}
封装好三方推理框架之后,自然而然就要集成到业务中来使用了。
4.2.2.3 集成业务,接入模型
(1)启动模型
以拍照业务为例:业务需要根据配置加载模型,业务跟模型需要有个映射关系。动态化配置模块将维护这组关系:
{
"1": {
"model": "shoes.mnn",
"label": "shoes.txt"
},
"2": {
"model": "clothes.mnn",
"label": "clothes.txt"
},
"3": {
"model": "clothes.mnn",
"label": "clothes.txt"
}
}
例如鞋子的id是1,其对应的模型以及标签则分别为shoes.mnn
、shoes.txt
根据动态化配置模块下发的关系,会启动鞋子对于的模型shoes.mnn
.载入内存。
(2)输入处理
模型需要的输入是一个224x224的三通道的图片,此处输入数据需要对齐(与算法模型要求的输入保持一致)处理。
buildInput() {
val bitmap = BitmapFactory.decodeFile(imagePath)
// 缩放至224
val matrix = Matrix()
matrix.postScale(224.0f / bitmap.width, 224.0f / bitmap.height)
matrix.invert(matrix)
// 4通道转为3通道 ARGB=> BGR
val config = MNNImageProcess.Config()
config.mean = floatArrayOf(103.94f, 116.78f, 123.68f)
config.normal = floatArrayOf(0.017f, 0.017f, 0.017f)
config.dest = MNNImageProcess.Format.BGR
// inputTensor为输入缓冲区,下面这个方法意为将bitmap通过matrix转化,以及 通道配置转化,放入inputTensor
MNNImageProcess.convertBitmap(bitmap, inputTensor, config, matrix)
}
(3)运行模型
上面引擎接入时调用封装好的API, 并为其加上耗时监控
run() {
val start = System.nanoTime()
// 调用api
targetSession.run()
val end = System.nanoTime()
val cost = (end - start) / 1000000.0f
}
(4)获取结果
模型的输出格式为一个约定好序列的概率数组:
// 第一个位置为鞋子主图、第二位鞋标、第三个为中低走线
[0.3, 0.8, 0.5]
我们需要将其最高概率取出 最高概率下标为1,并与动态化配置模块下发的标签文件shoes.txt 中各个字段需要对应的部位。
[
{
"code": 1,
"name": "主图"
},
{
"code": 2,
"name": "鞋标"
},
{
"code": 3,
"name": "中底"
}
]
也就是: "鞋标" ,以及对应的code为2,这些信息会回调给业务,业务做其后续的处理。
(5)总体流程
一图抵千言,标准化引擎关键流程,我梳理成流程图的形式。
(6)小结
通过动态配置模块、模型AI识别模块将实现,全类目、全部位的图片识别。如果AI足够“智能”,那方案设计工作变得简单化。
一个模型识别出各个类目的各个部位:
在现场拍摄任意的照片, AI会告知你照片中物体是AJ1 的xxx款限量版球鞋,球鞋正面可能有些污渍,识别出来货号是xxxx、批次号xxxx.
这样算法团队、对数据要求肯定也更加高。可能还会造成识别效率的降低。那目前工程结合算法模型的方式,将识别率、算法团队的数据要求降低到按类别为分的小集合上。这样组合的方式,不免让人眼前一亮。
全称是:Open Source Computer Vision Library
OpenCV是Intel开源计算机视觉库。它由一系列 C 函数和少量 C++ 类构成,实现了图像处理和计算机视觉方面的很多通用算法。
结合OpenGL,OpenGL是将数据渲染到视觉的库。那么我的简单理解:将视觉处理成数据的库。
该模块主要的目标是识别出拍出来的照片有模糊、过曝、过暗的照片。
4.3.2.1 模糊
我们利用OpenCV中的 Sobel算子来实现。sobel算子可以计算出图片的梯度,模糊照片其相邻像素点的灰度梯度是很接近的,而Sobel算子刚好可以计算这个一阶梯度。
为什么用Sobel算子而不用Laplacian算子呢?
对于模糊识别的场景起不需要Laplacian算子计算出的二阶梯度,如果用二阶梯度会导致丢失掉一些相似灰度值的点
完整的代码:
boolean hasBlur(String imagePath, double threshold) {
Mat img = Imgcodecs.imread(imagePath);
int height = img.height();
int width = img.width();
Mat cropImg = new Mat(img, new Range(height / 4, height / 4 * 3), new Range(width / 4, width / 4 * 3));
// 转为灰度图
Mat imageCropGray = new Mat();
Imgproc.cvtColor(cropImg, imageCropGray, Imgproc.COLOR_BGR2GRAY);
// 计算灰度图的梯度
Mat imageSobel = new Mat();
Imgproc.Sobel(imageCropGray, imageSobel, Core.CV_16U, 1, 1);
// 计算梯度的平均值
MatOfDouble meanStdValueImage = new MatOfDouble();
Core.meanStdDev(imageSobel, meanStdValueImage, meanStdValueImage);
double meanValue = meanStdValueImage.at(Double.class, 0, 0).getV();
// 当整张图片平均梯度小于某个值, 那就认定为模糊
return meanValue < threshold;
}
4.3.2.2 过曝、过暗
过曝跟过暗,目前统一都想图片转为灰度图,然后取灰度图的均值。判断均值超过某个阈值就过曝,低于某个均值就过暗。
完整代码:
boolean hasOverLight(String imagePath, double threshold) {
Mat img = Imgcodecs.imread(imagePath);
int height = img.height();
int width = img.width();
Mat cropImg = new Mat(img, new Range(height / 4, height / 4 * 3), new Range(width / 4, width / 4 * 3));
// 转为灰度图
Mat imageCropGray = new Mat();
Imgproc.cvtColor(cropImg, imageCropGray, Imgproc.COLOR_BGR2GRAY);
// 计算灰度图的平均值
MatOfDouble meanStdValueImage = new MatOfDouble();
Core.meanStdDev(imageCropGray, meanStdValueImage, meanStdValueImage);
double meanValue = meanStdValueImage.at(Double.class, 0, 0).getV();
// 当整张图片平均大于某个值, 那就认定为过亮
return meanValue > threshold;
}
boolean hasOverDark(String imagePath, double threshold) {
Mat img = Imgcodecs.imread(imagePath);
int height = img.height();
int width = img.width();
Mat cropImg = new Mat(img, new Range(height / 4, height / 4 * 3), new Range(width / 4, width / 4 * 3));
// 转为灰度图
Mat imageCropGray = new Mat();
Imgproc.cvtColor(cropImg, imageCropGray, Imgproc.COLOR_BGR2GRAY);
// 计算梯度的平均值
MatOfDouble meanStdValueImage = new MatOfDouble();
Core.meanStdDev(imageCropGray, meanStdValueImage, meanStdValueImage);
double meanValue = meanStdValueImage.at(Double.class, 0, 0).getV();
// 当整张图片平均小于某个值, 那就认定为过暗
return meanValue < threshold;
}
上述就是标准化引擎全局上的一个实践。实践过程中不免会踩坑,对于技术来说不仅仅要投入到实现,更重要的是在跟产品业务方不断的讨论中慢慢明确方向。因为我们也仅仅只是一直在实现目标的路上,小步快跑,基于做出来的事情再向前迭代,无疑不失为一种达成目标的手段。就如同目前一些存在问题一样。
用于规避某些模型是用特定ai框架训练出来,转化为别的框架模型就可能失真,甚至错误的判断。
目前明亮识别对于商品本身就是黑色的商品,识别准确度会有问题,后续针对这种场景对分情况处理,不能用一个均值一概而论
目前整体的AI参与都是模式都是线下将模型训练完成,再部署到线上,去使用模型来执行推理。没有负反馈机制,没有真正闭环。后续如果能将这个过程也迁移至线上,那么整个瑕疵检测、一致性识别的准确率将会大大提高。
随着AI技术的发展,越来越多的企业会将AI的应用接入到业务中。AI技术的可应用点目前有识别(图像识别、语音识别、情绪识别)、自然语言处理、自然语言生成、虚拟助理、预测(趋势预测、股票预测)等等。AI目前所处的阶段仍然是领域特定的,结合工程上面的设计可以达到辅助或者代替人工去执行任务,比如各个商品类目的部位识别、特定商品部位的特定瑕疵识别。有了这些辅助性AI,将会大大降低工人的门槛与工作的出错率。在建设过程中,也是在不断的激发自己思考拓宽技术栈,了解业务场景,不断预见问题,解决问题,对于我们来说是一个不断成长的过程。
当然还有其他方面的应用需要结合业务、技术去发现,在这方面有想法的同学欢迎一起交流。
参考链接:
【1】https://www.yuque.com/mnn/cn
|