本文介绍飞猪互动双十一彩蛋“飞猪找一找”, 一个基于 AI 图像识别的纯客户端游戏
以这个游戏只是抛砖引玉,试图讨论:
- 前端同学如何使用 tensorflow 迅速地训练一个符合个性化需求的图像识别模型
- 如何使用 tensorflow.js 在前端载入和使用模型,以及如何整合所有体验到 React.js 框架下
- 踩过的一些需要注意的坑
- 前端同学使用 tensorflow.js 可以走多远?落地业务的可能性有多大?
- 和哪些技术可以结合?有哪些可以展望的未来
可以通过支付宝或者飞猪扫码此二维码试玩:
机器学习 101
机器学习说到底就是一种算法,并没有什么神秘性。业界甚至避免使用人工智能(AI)的称呼,因为人工智能暗示了智能,但现在的机器学习还称不上智能。婴儿能在没有规则和目标的情况下学习,而且要识别什么是“猫”大概看到 5-10 只猫就够了。相比之下机器学习非常笨拙,识别猫必须有上百万张图片数据灌入,且结果对数据源的取材也高敏感。
AI 是如何识别的呢
- 一个简化的模型是中学常解的三元一次联立方程,只不过他要解的是几百万元方程。
- 神经网络:每个元就是“参数”,而联立方程式称为一层“神经网络”,常用的简单模型可能有 3-6 层,复杂模型甚至有 50+层。
- 训练:即解这些方程式的过程。计算机通过趋近的算法逐步逼近正确的 “参数”,经过几千次训练,这些参数足够逼近正确答案,一个 AI 模型就训练完成了。此时再喂一张图片的像素数据,它会回答答案是 0.99,即 99%是猫。
端侧机器学习(client side machine learning) 的意义
1. 端侧机器学习有两类
- 在端侧使用训练完成的模型
- 在端侧进行模型训练
后者现在还不成熟,训练对机型要求高,比如需要通过 webgl 调用 gpu 加速训练。
2. 端侧机器学习的利弊
优点
- 开发成本,从训练、开发到发布一切前端搞定,迭代效率有大幅提高
- 无需网络本地可玩,这也确保了某些涉及隐私的应用
- 一次加载资源后,不需反复请求服务端传递数据量以及获取 AI 分析结果,非常适合图像识别等高频 AI 分析场景
缺点:模型大小受限,精细的非定制 mobile 的模型体积常有上百 M,并不适合直接投放 h5
如何使用 tensorflow.js 简单的跑起一个已有的模型
tensorflow.js
整合了 4 个 api:
从用途上来说,无外乎 a. 使用已有的模型。包括如何加载以及每个模型提供的高层的 api b. 重新训练已有模型 c. 完全定制和创建崭新的模型
本文会涉及到 a 和 b,重点放在 a,关于 b 也只会在原理层分析。
1.通过摄像获取图片
摄像头调用(webar)(新的 webar 模板,使用新版 webar) 如下的普通的 h5 页面调用摄像头受到安全限制无法使用
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
const stream = await navigator.mediaDevices.getUserMedia({
'audio': false,
'video': {facingMode: 'environment'}
});
我们使用支付宝的 webar.js来强行打开摄像头。webar 提供了getWebCameraAsync
方法获取摄像头
...
<canvas ref={node => (this.glCanvas = node)} />
...
// 开启相机
initCamera = async () => {
// 获取 webgl 上下文
const cvs = this.glCanvas;
const gl = cvs.getContext('webgl');
// 获取摄像头
this.camera = await getWebCameraAsync({ facing: CAMERA_FACING_BACK });
// 打开摄像头
await this.camera.openAsync();
// 在canvas上绘制相机投影
this.displayTarget = this.camera.createDisplayTarget('ar-container', { autoResize: 0, gl });
this.displayTarget.loop();
};
webar 的相机提供了 pause、resume、snapshot、close 等一系列方法用于开始/暂停/截图和销毁。详见文档
2. AI 处理和预测图片内容:
前端载入模型:
使用 tfjs 的 converter。0.x 和 1.x 版本从模型保存格式到调用 api 都不相同,新版参考文档 Save and load models, 旧版如下:
import { loadFrozenModel } from '@tensorflow/tfjs-converter';
const WEIGHT_MANIFEST_FILE_URL =
'https://gw.alipayobjects.com/os/fliggy-play/181301-3/weights_manifest.json';
const MODEL_FILE_URL =
'https://gw.alipayobjects.com/os/fliggy-play/181301-3/tensorflowjs_model.pb';
this.model = await loadFrozenModel(MODEL_FILE_URL, WEIGHT_MANIFEST_FILE_URL);
而预测非常简单:
// 运行模型获得预测结果
this.model.execute(data, 'final_result');
// 丢弃模型释放内存,在didMount中使用
this.model.dispose();
旧版 tfjs 需加载的模型文件三个:
- tensorflowjs_model.pb (模型文件)
- weights_manifest.json (配重 manifest)
- group1-shard1of1 (二进制配重表)
其中 group1-shard1of1 不被直接引用,但必须放在同一路径上,否则会报错!
- 使用模型处理和预测 版本 0.x 和 1.x 之间的 api 也有少许区别,例如从图片上获取像素信息,在 0.x 中是
fromPixels
在 1.x 中是browser.fromPixels
从截图上获取和加工像素数据:
import * as tfc from '@tensorflow/tfjs-core';
...
// tidy方法用于在tensorflow调用结束后清理内存
const result = tfc.tidy(() => {
// 使用webar相机截图
const img = await this.displayTarget.snapshotImageDataURLAsync({type: 'imageData'});
// 获取图片信息
const pixels = tfc.browser.fromPixels(img);
// “截图”,确保模型接收的所有素材数据格式统一到224*224
const centerHeight = pixels.shape[0] / 2;
const beginHeight = centerHeight - 112;
const centerWidth = pixels.shape[1] / 2;
const beginWidth = centerWidth - 112;
const pixelsCropped = pixels.slice(
[beginHeight, beginWidth, 0],
[224, 224, 3]
);
// 将规范化的224*224像素信息传入模型,获取模型预测结果
return predict(pixelsCropped);
});
预测的 predict 方法具体详见 gitlab,这里不展开。至此,你可以成功载入已有模型,从摄像机获取截图,并通过分析返回预测结果(是一个数组,包含预测结果和预测确定性)。
3. 优化注意
- 使用
tfc.tidy
包裹模型预测,用于清理内存 - 模型先跑一组空数据预热
tfc.zeros([VIDEO_PIXELS, VIDEO_PIXELS, 3])
- 注意在
willUnmount
里销毁模型和相机,清理内存 - 加载优化
利用 React 的 Suspense 和 lazy 语法,我们 code splitting 主体 Game 组件(乃至所有 AI 模型加载预热和相机调用),并确保 Game 组件在 Menu 加载完成后立刻 preload,达到满意的游戏加载体验
const Menu = lazy(() => import('./Menu'));
const Playground = lazy(() => import('./Playground'));
function Game() {
const [isMenu, set] = useState(true);
useEffect(() => {
import('./Playground');
}, []);
...
return (
<div>
{isMenu ? (
<Suspense fallback={<Loading show />}>
<Menu toGame={toGame} />
</Suspense>
) : (
<Suspense fallback={<Loading show />}>
<div className="game-page">
<Playground backToMenu={backToMenu} />
</div>
</Suspense>
)}
</div>
);
};
如何 retrain 一个模型
1. 什么是 retrain
图像识别模型有上百万个参数,从头开始训练一个图像识别模型需要海量的数据和上百个 gpu 训练小时。作为希望能低成本的使用的前端同学,从头训练模型不可取。但现有模型很可能无法满足你需要识别 “飞猪” 的定制要求。这时候最佳途径是 retrain,说具体点是 transfer learning,即利用已有的训练模型的结果,再添加一层神经网络,将其转换为我们定制的训练结果。
结果肯定不如从头开始,但这个简单手段的精确度已经足够高,是个投入产出比优异的方案。“飞猪找一找”的 retrain 在我的 mac pro 上,无 gpu 加速,一次训练 3000 轮,只需 30 分钟左右(首次训练会比较耗时)。
2. 如何 retrain
我们使用了 hub 上现成的 图像 retrain 模型, 具体训练细节可以参考 google 的 emoji-scavenger-hunt 仓库 README, 将收集的图片按照归类如下方式放入训练文件夹 train/data, 安装 python 等环境和依赖,然后运行 hub 的 retrain 模型。
data
└── images
├── cat
│ ├── cat1.jpg
│ ├── cat2.jpg
│ └── ...
└── dog
├── dog1.jpg
├── dog2.jpg
└── ...
emoji-scavenger-hunt 甚至写好了包含整个流程的 dockerfile,只需要安装 docker 运行
$ cd training
$ docker build -t model-builder .
$ docker run -v /path/to/data:/data -it model-builder
经过 4000 次训练,最终会生成 data/saved_model_web 文件夹, tensorflow.js 加载需要使用的 3 个文件,以及一个包含所有训练的物体的名称集合的.ts。
3. 如何优化训练结果
除了必要物体的类,多一个 unknown 类,用于训练所有的“杂音”,例如各种背景图。这能降低“过度训练”的可能性,例如模型识别了图片上一类的背景而把其当做区别物体的参照。
选材注意,照片的曝光和角度都会影响到最终训练结果,尽量接近真实场景
使用 imageOptim 压缩图片时,照片朝向信息会丢失,导致用于识别的图片都是横着的,这个极小且不易察觉的问题极大的影响了识别的精度。详细可参考文献计算机视觉模型效果不佳,你可能是被相机的 Exif 信息坑了。可以使用批量处理工具在压缩前将图片转向正确朝向。
结合业务的展望
图像识别只是抛砖引玉,在 hub 上有无数开发者已经训练完成的模型等待你的直接使用或者 retrain。目前常用的模型归类为:
- 图像识别(图像是什么东西)
- 物体识别(图像里有没有某样东西)
- 姿势识别(图像中人的姿势)
- 语音识别(高精度)
- 文字归类(从意义解析到情感判断)
事实上 AI 和 AR 是一个非常自然的结合。图像识别、物体识别、姿势识别将现实世界通过相机截图解读成有意义数据,AR 通过数据来增强现实世界的体验。这里能做的有趣事情就很多了。
彩蛋的原理如下
可以看到,AI 很好的完成了从现实获取数据的一步,即给前端提供与现实世界的一个接点,那么再进一步使用这个接点 + AR 技术去“增强现实”,想像空间就非常大了
例如淘宝小程序团队和欧莱雅集团深度合作的 modiface 口红试妆就是一个独特的例子。而在飞猪这个旅游主题下,如何和各种目的地的玩法结合是笔者之后思考的方向。
参考文献: