人工智能,为我所用

内容简介

自2016年谷歌 AlphaGo 赢得了围棋大战后,人工智能近两年迎来了新一轮的爆发。人工智能在产业界和资本圈引起了高度关注,成为新的风口。

人工智能的三个核心要素是数据、算法和计算能力。相比前几次的热潮,目前 AI 在数据、算法和计算能力方面都有更加坚实的基础。人工智能并不是一个单独的存在,而必须要和其他产业结合起来才能提升效率,创造价值。本期我们为大家集结了2017年人工智能应用在各大领域的研究进展和技术成果,我们相信人工智能并不仅是一个风口,而是已经迎来真正属于自己的时代。

本书内容

深度学习在推荐领域的应用

文/吴岸城

2012年 Facebook 在广告领域开始应用定制化受众(Facebook Custom Audiences)功能后,“受众发现”这个概念真正得到大规模应用,什么叫“受众发现”?如果你的企业已经积累了一定的客户,无论这些客户是否关注你或者是否跟你在 Facebook 上有互动,都能通过 Facebook 的广告系统触达到。“受众发现”实现了什么功能?在没有这个系统之前,广告投放一般情况都是用标签去区分用户,再去给这部分用户发送广告,“受众发现”让你不用选择这些标签,包括用户基本信息、兴趣等。你需要做的只是上传一批你目前已有的用户或者你感兴趣的一批用户,剩下的工作就等着 Custom Audiences 帮你完成了。

Facebook 这种通过一群已有的用户发现并扩展出其他用户的推荐算法就叫 Lookalike,当然 Facebook 的算法细节笔者并不清楚,各个公司实现 Lookalike 也各有不同。这里也包括腾讯在微信端的广告推荐上的应用、Google 在 YouTube 上推荐感兴趣视频等。下面让我们结合前人的工作,实现自己的 Lookalike 算法,并尝试着在新浪微博上应用这一算法。

调研

首先要确定微博领域的数据,关于微博的数据可以这样分类:

用户基础数据:年龄、性别、公司、邮箱、地点、公司等。

关系图:根据人—人,人—微博的关注、评论、转发信息建立关系图。

内容数据:用户的微博内容,包含文字、图片、视频。

有了这些数据后,怎么做数据的整合分析?来看看现在应用最广的方式——协同过滤、或者叫关联推荐。协同过滤主要是利用某兴趣相投、拥有共同经验群体的喜好来推荐用户可能感兴趣的信息,协同过滤的发展有以下三个阶段:

第一阶段,基于用户喜好做推荐,用户 A 和用户 B 相似,用户 B 购买了物品 a、b、c,用户 A 只购买了物品 a,那就将物品 b、c 推荐给用户 A。这就是基于用户的协同过滤,其重点是如何找到相似的用户。因为只有准确的找到相似的用户才能给出正确的推荐。而找到相似用户的方法,一般是根据用户的基本属性贴标签分类,再高级点可以用上用户的行为数据。

第二阶段,某些商品光从用户的属性标签找不到联系,而根据商品本身的内容联系倒是能发现很多有趣的推荐目标,它在某些场景中比基于相似用户的推荐原则更加有效。比如在购书或者电影类网站上,当你看一本书或电影时,推荐引擎会根据内容给你推荐相关的书籍或电影。

第三阶段,如果只把内容推荐单独应用在社交网络上,准确率会比较低,因为社交网络的关键特性还是社交关系。如何将社交关系与用户属性一起融入整个推荐系统就是关键。在神经网络和深度学习算法出现后,提取特征任务就变得可以依靠机器完成,人们只要把相应的数据准备好就可以了,其他数据都可以提取成向量形式,而社交关系作为一种图结构,如何表示为深度学习可以接受的向量形式,而且这种结构还需要有效还原原结构中位置信息?这就需要一种可靠的向量化社交关系的表示方法。基于这一思路,在2016年的论文中出现了一个算法 node2vec,使社交关系也可以很好地适应神经网络。这意味着深度学习在推荐领域应用的关键技术点已被解决。

在实现算法前我们主要参考了如下三篇论文:

  • Audience Expansion for Online Social Network Advertising 2016
  • node2vec: Scalable Feature Learning for Networks Aditya Grover 2016
  • Deep Neural Networks for YouTube Recommen dations 2016

第一篇论文是 LinkedIn 给出的,主要谈了针对在线社交网络广告平台,如何根据已有的受众特征做受众群扩展。这涉及到如何定位目标受众和原始受众的相似属性。论文给出了两种方法来扩展受众:

  1. 与营销活动无关的受众扩展;
  2. 与营销活动有关的受众扩展。

在图1中,LinkedIn 给出了如何利用营销活动数据、目标受众基础数据去预测目标用户行为进而发现新的用户。今天的推荐系统或广告系统越来越多地利用了多维度信息。如何将这些信息有效加以利用,这篇论文给出了一条路径,而且在工程上这篇论文也论证得比较扎实,值得参考。

enter image description here

图1 LinkedIn 的 Lookalike 算法流程图

第二篇论文,主要讲的是 node2vec,这也是本文用到的主要算法之一。node2vec 主要用于处理网络结构中的多分类和链路预测任务,具体来说是对网络中的节点和边的特征向量表示方法。

简单来说就是将原有社交网络中的图结构,表达成特征向量矩阵,每一个 node(可以是人、物品、内容等)表示成一个特征向量,用向量与向量之间的矩阵运算来得到相互的关系。

下面来看看 node2vec 中的关键技术——随机游走算法,它定义了一种新的遍历网络中某个节点的邻域的方法,具体策略如图2所示。

enter image description here

图2 随机游走策略

假设我们刚刚从节点 t 走到节点 v,当前处于节点 v,现在要选择下一步该怎么走,方案如下:

enter image description here

其中 dtx 表示节点 t 到节点 x 之间的最短路径,dtx=0 表示会回到节点 t 本身,dtx=1 表示节点 t 和节点 x 直接相连,但是在上一步却选择了节点 v,dtx=2 表示节点 t 不与 x 直接相连,但节点 v 与 x 直接相连。其中 p 和 q 为模型中的参数,形成一个不均匀的概率分布,最终得到随机游走的路径。与传统的图结构搜索方法(如 BFS 和 DFS)相比,这里提出的随机游走算法具有更高的效率,因为本质上相当于对当前节点的邻域节点的采样,同时保留了该节点在网络中的位置信息。

node2vec 由斯坦福大学提出,并有开源代码,这里顺手列出,这一部分大家不用自己动手实现了。https://github.com/aditya-grover/node2vec

注:本文的方法需要在源码的基础上改动图结构。

第三篇论文讲的是 Google 如何做 YouTube 视频推荐,论文是在我做完结构设计和流程设计后看到的,其中模型架构的思想和我们不谋而合,还解释了为什么要引入 DNN(后面提到所有的 feature 将会合并经历几层全连接层):引入 DNN 的好处在于大多数类型的连续特征和离散特征可以直接添加到模型当中。此外我们还参考了这篇论文对于隐含层(FC)单元个数选择。图3是这篇论文提到的算法结构。

实现

  • 数据准备

  • 获得用户的属性(User Profile),如性别、年龄、学历、职业、地域、能力标签等;

  • 根据项目内容和活动内容制定一套受众标签(Audience Label);

  • 提取用户之间的关注关系,微博之间的转发关系;

  • 获取微博 message 中的文本内容;

  • 获得微博 message 中的图片内容。

  • 用户标签特征处理

  • 根据步骤 a 中用户属性信息和已有的部分受众标签系统。利用 GBDT 算法(可以直接用 xgboost)将没有标签的受众全部打上标签。这个分类问题中请注意处理连续值变量以及归一化。

  • 将标签进行向量化处理,这个问题转化成对中文单词进行向量化,这里用 word2vec 处理后得到用户标签的向量化信息 Label2vec。这一步也可以使用 word2vec 在中文的大数据样本下进行预训练,再用该模型对标签加以提取,对特征的提取有一定的提高,大约在0.5%左右。

enter image description here图3 YouTube 推荐结构图

  • 文本特征处理

将步骤 a 中提取到的所有微博 message 文本内容清洗整理,训练 Doc2Vec 模型,得到单个文本的向量化表示,对所得的文本作聚类(KMeans,在 30w 的微博用户的 message 上测试,K 取128对文本的区分度较强),最后提取每个 cluster 的中心向量,并根据每个用户所占有的 cluster 获得用户所发微博的文本信息的向量表示 Content2vec。

  • 图像特征(可选)

将步骤 a 中提取到的所有的 message 图片信息整理分类,使用预训练卷积网络模型(这里为了平衡效率选取 VGG16 作为卷积网络)提取图像信息,对每个用户 message 中的图片做向量化处理,形成 Image2vec,如果有多张图片将多张图片分别提取特征值再接一层 MaxPooling 提取重要信息后输出。

  • 社交关系建立(node2vec 向量化)

将步骤 a 中获得到的用户之间的关系和微博之间的转发评论关系转化成图结构,并提取用户关系 sub-graph,最后使用 node2Vec 算法得到每个用户的社交网络图向量化表示。图4为简历社交关系后的部分图示。

enter image description here

图4 用户社交关系

  • 将 bcde 步骤得到的向量做拼接,经过两层 FC,得到表示每个用户的多特征向量集(User Vector Set,UVS)。这里取的输出单元个数时可以根据性能和准确度做平衡,目前我们实现的是输出512个单元,最后的特征输出表达了用户的社交关系、用户属性、发出的内容、感兴趣的内容等的混合特征向量,这些特征向量将作为下一步比对相似性的输入值。

  • 分别计算种子用户和潜在目标用户的向量集,并比对相似性,我们使用的是余弦相似度计算相似性,将步骤 f 得到的用户特征向量集作为输入 x,y,代入下面公式计算相似性:

使用余弦相似度要注意:余弦相似度更多的是从方向上区分差异,而对绝对的数值不敏感。因此没法衡量每个维度值的差异,这里我们要在每个维度上减去一个均值或者乘以一个系数,或者在之前做好归一化。

  • 受众扩展
  • 获取种子受众名单,以及目标受众的数量 N;
  • 检查种子用户是否存在于 UVS 中,将存在的用户向量化;
  • 计算受众名单中用户和 UVS 中用户的相似度,提取最相似的前 N 个用户作为目标受众。

最后我们将以上步骤串联起来,形成如图5所示。

enter image description here

图5 Lookalike 算法示意图

在以上步骤中特征提取完成后,我们使用一个2层的神经网络做最后的特征提取,算法结构示意图如图6所示。

enter image description here

图6 Lookalike 算法结构图

其中 FC1 层也可以替换成 MaxPooling,MaxPooling 层具有强解释性,也就是在用户特征群上提取最重要的特征点作为下一层的输入,读者可以自行尝试,这里限于篇幅问题就不做展开了。

讲到这里,算法部分就已基本完结,其中还有些工程问题,并不属于本次主题探讨范围,这里也不做讨论了。

结果

我司算法团队根据 Lookalike 思想完整实现其算法,并在实际产品中投入试用。针对某客户(乳品领域世界排名前三的品牌主)计算出结果(部分):

表1 部分计算结果

enter image description here

可以观察到以上微博 ID 的主题基本都是西点企业或西点培训企业,和品牌主售卖的乳品有很高的关联性:乳品是非常重要的西点原料,除终端用户外,西点相关企业就是乳品企业主需要寻找的最重要的受众之一。

探讨

特征表达

除了以上提到的特征外,我们也对其他的重要特征表达做了处理和变换:根据我们的需求,需要抽取出人的兴趣特征,如何表达一个人的兴趣?除了他自己生成的有关内容外,还有比较关键的一点是比如“我”看了一些微博,但并没有转发,大多数情况下都不会转发,但有些“我”转发了,有些“我”评论了;“我”转发了哪些?评论了哪些?这次距上次的浏览该人的列表时间间隔多久?都代表“我”对微博的兴趣,而间接的反应“我”的兴趣特征。这些数据看来非常重要,又无法直接取得,怎么办?

下面来定义一个场景,试图描述出我们对看过的内容中哪些是感兴趣的,哪些不是感兴趣的:

  • 用户 A,以及用户 A 关注的用户 B;
  • 用户 A 的每天动作时间(比如他转发、评论、收藏、点赞)起始时间,我们定义为苏醒时间 A_wake(t);
  • 用户 B 每天发帖(转发、评论)时间:B_action(t);
  • 简单假设一下Awake(t)>Baction(t),也就是B_action(t) 的评论都能看到。这就能得到用户 A 对应了哪些帖子;
  • 同理,也可知用户 A 在 A_wake(t) 时间内转发了、评论了哪些帖子;
  • 结合上次浏览间隔时间,可以描述用户 A 对哪些微博感兴趣(positive),哪些不感兴趣(negative)。

全连接层的激活单元比对提升

在 Google 那篇论文中比对隐含层(也就是我们结构图中的 FC 层)各种单元组合产生的结果,Google 选择的是最后一种组合,如图7所示。

enter image description here

图7 YouTube 推荐模型隐含层单元选择对比

我们初期选用了 512tanh→56tanh 这种两层组合,后认为输入特征维度过大,512个单元无法完整的表达特征,故又对比了1024→512组合,发现效果确实有微小提升大概在0.7%。另外我们的 FC 层输入在(-1,1)区间,考虑到 relu 函数的特点没有使用它,而是使用 elu 激活函数。测试效果要比 tanh 函数提升0.3%-0.5%。

附:

node2vec 伪码:

enter image description here

Bandit 算法与推荐系统

文/陈开江

推荐系统里面有两个经典问题:EE 和冷启动。前者涉及到平衡准确和多样,后者涉及到产品算法运营等一系列。Bandit 算法是一种简单的在线学习算法,常常用于尝试解决这两个问题,本来为你介绍基础的 Bandit 算法及一系列升级版,以及对推荐系统这两个经典问题的思考。

什么是 Bandit 算法

为选择而生

我们会遇到很多选择的场景。上哪个大学,学什么专业,去哪家公司,中午吃什么等等。这些事情,都让选择困难症的我们头很大。那么,有算法能够很好地对付这些问题吗?

当然有!那就是 Bandit 算法。

一个赌徒,要去摇老虎机,走进赌场一看,一排老虎机,外表一模一样,但是每个老虎机吐钱的概率可不一样,他不知道每个老虎机吐钱的概率分布是什么,那么每次该选择哪个老虎机可以做到最大化收益呢?这就是多臂赌博机问题(Multi-armed bandit problem, K-armed bandit problem, MAB)。

图1  MAB问题

图1 MAB 问题

怎么解决这个问题呢?最好的办法是去试一试,不是盲目地试,而是有策略地快速试一试,这些策略就是 Bandit 算法。

这个多臂问题,推荐系统里很多问题都与它类似:

  • 假设一个用户对不同类别的内容感兴趣程度不同,那么我们的推荐系统初次见到这个用户时,怎么快速地知道他对每类内容的感兴趣程度?这就是推荐系统的冷启动。
  • 假设我们有若干广告库存,怎么知道该给每个用户展示哪个广告,从而获得最大的点击收益?是每次都挑效果最好那个么?那么新广告如何才有出头之日?
  • 我们的算法工程师又想出了新的模型,有没有比 A/B test 更快的方法知道它和旧模型相比谁更靠谱?
  • 如果只是推荐已知的用户感兴趣的物品,如何才能科学地冒险给他推荐一些新鲜的物品?

Bandit 算法与推荐系统

在推荐系统领域里,有两个比较经典的问题常被人提起,一个是 EE 问题,另一个是用户冷启动问题。

什么是 EE 问题?又叫 exploit-explore 问题。exploit 就是:对用户比较确定的兴趣,当然要利用开采迎合,好比说已经挣到的钱,当然要花;explore 就是:光对着用户已知的兴趣使用,用户很快会腻,所以要不断探索用户新的兴趣才行,这就好比虽然有一点钱可以花了,但是还得继续搬砖挣钱,不然花完了就得喝西北风。

用户冷启动问题,也就是面对新用户时,如何能够通过若干次实验,猜出用户的大致兴趣。

我想,屏幕前的你已经想到了,推荐系统冷启动可以用 Bandit 算法来解决一部分。

这两个问题本质上都是如何选择用户感兴趣的主题进行推荐,比较符合 Bandit 算法背后的 MAB 问题。

比如,用 Bandit 算法解决冷启动的大致思路如下:用分类或者 Topic 来表示每个用户兴趣,也就是 MAB 问题中的臂(Arm),我们可以通过几次试验,来刻画出新用户心目中对每个 Topic 的感兴趣概率。这里,如果用户对某个 Topic 感兴趣(提供了显式反馈或隐式反馈),就表示我们得到了收益,如果推给了它不感兴趣的 Topic,推荐系统就表示很遗憾(regret)了。如此经历“选择-观察-更新-选择”的循环,理论上是越来越逼近用户真正感兴趣的 Topic 的。

怎么选择 Bandit 算法?

现在来介绍一下 Bandit 算法怎么解决这类问题的。Bandit 算法需要量化一个核心问题:错误的选择到底有多大的遗憾?能不能遗憾少一些?

王家卫在《一代宗师》里寄出一句台词:

人生要是无憾,那多无趣?

而我说:算法要是无憾,那应该是过拟合了。

所以说:怎么衡量不同 Bandit 算法在解决多臂问题上的效果?首先介绍一个概念,叫做累积遗憾(regret):

图2   积累遗憾

图2 积累遗憾

这个公式就是计算 Bandit 算法的累积遗憾,解释一下:

首先,这里我们讨论的每个臂的收益非0即1,也就是伯努利收益。

然后,每次选择后,计算和最佳的选择差了多少,然后把差距累加起来就是总的遗憾。

wB(i) 是第 i 次试验时被选中臂的期望收益, w* 是所有臂中的最佳那个,如果上帝提前告诉你,我们当然每次试验都选它,问题是上帝不告诉你,所以就有了 Bandit 算法,我们就有了这篇文章。

这个公式可以用来对比不同 Bandit 算法的效果:对同样的多臂问题,用不同的Bandit算法试验相同次数,看看谁的 regret 增长得慢。

那么到底不同的 Bandit 算法有哪些呢?

常用 Bandit 算法

Thompson sampling 算法

Thompson sampling 算法简单实用,因为它只有一行代码就可以实现。简单介绍一下它的原理,要点如下:

  1. 假设每个臂是否产生收益,其背后有一个概率分布,产生收益的概率为 p。
  2. 我们不断地试验,去估计出一个置信度较高的“概率p的概率分布”就能近似解决这个问题了。
  3. 怎么能估计“概率p的概率分布”呢? 答案是假设概率 p 的概率分布符合 beta (wins, lose) 分布,它有两个参数:wins, lose。
  4. 每个臂都维护一个 beta 分布的参数。每次试验后,选中一个臂,摇一下,有收益则该臂的 wins 增加1,否则该臂的 lose 增加1。
  5. 每次选择臂的方式是:用每个臂现有的 beta 分布产生一个随机数 b,选择所有臂产生的随机数中最大的那个臂去摇。
import  numpy as npimport  pymc#wins 和 trials 是一个N维向量,N是赌博机的臂的个数,每个元素记录了choice = np.argmax(pymc.rbeta(1 + wins, 1 + trials - wins)) wins[choice] += 1trials += 1

UCB 算法

UCB 算法全称是 Upper Confidence Bound(置信区间上界),它的算法步骤如下:

  • 初始化:先对每一个臂都试一遍;
  • 按照如下公式计算每个臂的分数,然后选择分数最大的臂作为选择:图3  UCB算法
  • 观察选择结果,更新 t 和 Tjt。其中加号前面是这个臂到目前的收益均值,后面的叫做 bonus,本质上是均值的标准差,t 是目前的试验次数,Tjt 是这个臂被试次数。

这个公式反映一个特点:均值越大,标准差越小,被选中的概率会越来越大,同时哪些被选次数较少的臂也会得到试验机会。

Epsilon-Greedy 算法

这是一个朴素的 Bandit 算法,有点类似模拟退火的思想:

  1. 选一个(0,1)之间较小的数作为 epsilon;
  2. 每次以概率 epsilon 做一件事:所有臂中随机选一个;
  3. 每次以概率 1-epsilon 选择截止到当前,平均收益最大的那个臂。

是不是简单粗暴?epsilon 的值可以控制对 Exploit 和 Explore 的偏好程度。越接近0,越保守,只想花钱不想挣钱。

朴素 Bandit 算法

最朴素的 Bandit 算法就是:先随机试若干次,计算每个臂的平均收益,一直选均值最大那个臂。这个算法是人类在实际中最常采用的,不可否认,它还是比随机乱猜要好。

以上五个算法,我们用10000次模拟试验的方式对比了其效果如图4,实验代码来源:

图4  五种Bandit算法模拟试验的效果图

图4 五种 Bandit 算法模拟试验的效果图

算法效果对比一目了然:UCB 算法和 Thompson 采样算法显著优秀一些。

至于你实际上要选哪一种 Bandit 算法,你可以选一种 Bandit 算法来选 Bandit 算法。

Bandit 算法与线性回归

UCB 算法

UCB 算法在做 EE(Exploit-Explore)的时候表现不错,但它是上下文无关(context free)的 Bandit 算法,它只管埋头干活,根本不观察一下面对的都是些什么特点的 arm,下次遇到相似特点但不一样的 arm 也帮不上什么忙。

UCB 解决 Multi-armed bandit 问题的思路是:用置信区间。置信区间可以简单地理解为不确定性的程度,区间越宽,越不确定,反之亦反之。

每个 Item 的回报均值都有个置信区间,随着试验次数增加,置信区间会变窄(逐渐确定了到底回报丰厚还是可怜)。每次选择前,都根据已经试验的结果重新估计每个 Item 的均值及置信区间。 选择置信区间上限最大的那个 Item。

“选择置信区间上界最大的那个 Item”这句话反映了几个意思:

  1. 如果 Item 置信区间很宽(被选次数很少,还不确定),那么它会倾向于被多次选择,这个是算法冒风险的部分;
  2. 如果 Item 置信区间很窄(备选次数很多,比较确定其好坏了),那么均值大的倾向于被多次选择,这个是算法保守稳妥的部分;
  3. UCB 是一种乐观的算法,选择置信区间上界排序,如果时悲观保守的做法,是选择置信区间下界排序。

UCB算法加入特征信息

Yahoo!的科学家们在2010年发表了一篇论文,给 UCB 引入了特征信息,同时还把改造后的 UCB 算法用在了 Yahoo!的新闻推荐中,算法名叫 LinUCB,刘鹏博士在《计算广告》一书中也有介绍 LinUCB 在计算广告中的应用。

单纯的老虎机回报情况就是老虎机自己内部决定的,而在广告推荐领域,一个选择的回报,是由 User 和 Item 一起决定的,如果我们能用 Feature来 刻画 User 和 Item 这一对 CP,在每次选择 Item 之前,通过 Feature 预估每一个 arm(item)的期望回报及置信区间,选择的收益就可以通过 Feature 泛化到不同的 Item 上。

为 UCB 算法插上了特征的翅膀,这就是 LinUCB 最大的特色。

图5  应用LinUCB算法的Yahoo!首页图5 应用 LinUCB 算法的 Yahoo! 首页

LinUCB 算法做了一个假设:一个 Item 被选择后推送给一个 User,其回报和相关 Feature 成线性关系,这里的“相关 Feature”就是 context,也是实际项目中发挥空间最大的部分。

于是试验过程就变成:用 User 和 Item 的特征预估回报及其置信区间,选择置信区间上界最大的 Item 推荐,观察回报后更新线性关系的参数,以此达到试验学习的目的。

LinUCB 基本算法描述如下:

图6   LinUCB算法描述

图6 LinUCB 算法描述

对照每一行解释一下(编号从1开始):

  1. 设定一个参数 \alpha,这个参数决定了我们Explore 的程度;
  2. 开始试验迭代;
  3. 获取每一个 arm 的特征向量 xa,t;
  4. 开始计算每一个 arm 的预估回报及其置信区间;
  5. 如果 arm 还从没有被试验过,那么:
  6. 用单位矩阵初始化 Aa;
  7. 用0向量初始化 ba;
  8. 处理完没被试验过的 arm;
  9. 计算线性参数 \theta;
  10. 用 \theta 和特征向量 xa,t 计算预估回报,同时加上置信区间宽度;
  11. 处理完每一个 arm;
  12. 选择第10步中最大值对应的 arm,观察真实的回报 rt;
  13. 更新 Aat;
  14. 更新 bat;
  15. 算法结束。

注意到上面的第4步,给特征矩阵加了一个单位矩阵,这就是岭回归(ridge regression),岭回归主要用于当样本数小于特征数时,对回归参数进行修正。

对于加了特征的 Bandit 问题,正符合这个特点:试验次数(样本)少于特征数。

每一次观察真实回报之后,要更新的不止是岭回归参数,还有每个 arm 的回报向量 ba。

详解 LinUCB 的实现

根据论文给出的算法描述,其实很好写出 LinUCB 的代码,麻烦的只是构建特征。

代码如下,一些必要的注释说明已经写在代码中。

class LinUCB:    def __init__(self):     self.alpha = 0.25      self.r1 = 1 # if worse -> 0.7, 0.8        self.r0 = 0 # if worse, -19, -21        # dimension of user features = d        self.d = 6        # Aa : collection of matrix to compute disjoint part for each article a, d*d        self.Aa = {}        # AaI : store the inverse of all Aa matrix        self.AaI = {}        # ba : collection of vectors to compute disjoin part, d*1        self.ba = {}        self.a_max = 0        self.theta = {}        self.x = None        self.xT = None        # linUCB    def set_articles(self, art):        # init collection of matrix/vector Aa, Ba, ba        for key in art:            self.Aa[key] = np.identity(self.d)            self.ba[key] = np.zeros((self.d, 1))            self.AaI[key] = np.identity(self.d)            self.theta[key] = np.zeros((self.d, 1))        # 这里更新参数时没有传入更新哪个arm,因为在上一次recommend的时候缓存了被选的那个arm,所以此处不用传入         # 另外,update操作不用阻塞recommend,可以异步执行            def update(self, reward):        if reward == -1:            pass        elif reward == 1 or reward == 0:            if reward == 1:                r = self.r1            else:                r = self.r0            self.Aa[self.a_max] += np.dot(self.x, self.xT)            self.ba[self.a_max] += r * self.x            self.AaI[self.a_max] = linalg.solve(self.Aa[self.a_max], np.identity(self.d))            self.theta[self.a_max] = np.dot(self.AaI[self.a_max], self.ba[self.a_max])        else:        # error            pass        # 预估每个arm的回报期望及置信区间    def recommend(self, timestamp, user_features, articles):        xaT = np.array([user_features])        xa = np.transpose(xaT)        art_max = -1        old_pa = 0        # 获取在update阶段已经更新过的AaI(求逆结果)        AaI_tmp = np.array([self.AaI[article] for article in articles])        theta_tmp = np.array([self.theta[article] for article in articles])        art_max = articles[np.argmax(np.dot(xaT, theta_tmp) + self.alpha * np.sqrt(np.dot(np.dot(xaT, AaI_tmp), xa)))]        # 缓存选择结果,用于update        self.x = xa        self.xT = xaT        # article index with largest UCB        self.a_max = art_max        return self.a_max

怎么构建特征

LinUCB 算法有一个很重要的步骤,就是给 User 和 Item 构建特征,也就是刻画 context。在原始论文里,Item 是文章,其中专门介绍了它们怎么构建特征的,也甚是精妙。容我慢慢表来。

原始用户特征

人口统计学:性别特征(2类),年龄特征(离散成10个区间)。

地域信息:遍布全球的大都市,美国各个州。

行为类别:代表用户历史行为的1000个类别取值。

原始文章特征

URL 类别:根据文章来源分成了几十个类别。

编辑打标签:编辑人工给内容从几十个话题标签中挑选出来的原始特征向量都要归一化成单位向量。

还要对原始特征降维,以及模型要能刻画一些非线性的关系。

用 Logistic Regression 去拟合用户对文章的点击历史,其中的线性回归部分为:

拟合得到参数矩阵 W,可以将原始用户特征(1000多维)投射到文章的原始特征空间(80多维),投射计算方式:

这是第一次降维,把原始1000多维降到80多维。

然后,用投射后的80多维特征对用户聚类,得到5个类簇,文章页同样聚类成5个簇,再加上常数1,用户和文章各自被表示成6维向量。

Yahoo! 的科学家们之所以选定为6维,因为数据表明它的效果最好,并且这大大降低了计算复杂度和存储空间。

我们实际上可以考虑三类特征:U(用户),A(广告或文章),C(所在页面的一些信息)。

前面说了,特征构建很有发挥空间,算法工程师们尽情去挥洒汗水吧。

总结一下 LinUCB 算法,有以下优点:

  1. 由于加入了特征,所以收敛比 UCB 更快(论文有证明);
  2. 特征构建是效果的关键,也是工程上最麻烦和值的发挥的地方;
  3. 由于参与计算的是特征,所以可以处理动态的推荐候选池,编辑可以增删文章;
  4. 特征降维很有必要,关系到计算效率。

Bandit 算法与协同过滤

协同过滤背后的哲学

推荐系统里面,传统经典的算法肯定离不开协同过滤。协同过滤背后的思想简单深刻,在万物互联的今天,协同过滤的威力更加强大。协同过滤看上去是一种算法,不如说是一种方法论,不是机器在给你推荐,而是“集体智慧”在给你推荐。

它的基本假设就是“物以类聚,人以群分”,你的圈子决定了你能见到的物品。这个假设很靠谱,却隐藏了一些重要的问题:作为用户的我们还可能看到新的东西吗?还可能有惊喜吗?还可能有圈子之间的更迭流动吗?这些问题的背后其实就是在前面提到过的 EE 问题(Exploit & Explore)。我们关注推荐的准确率,但是我们也应该关注推荐系统的演进发展,因为“推荐系统不止眼前的 Exploit,还有远方的 Explore”。

做 Explore 的方法有很多,Bandit 算法是其中的一种流派。前面也介绍过几种 Bandit 算法,基本上就是估计置信区间的做法,然后按照置信区间的上界来进行推荐,以 UCB、LinUCB 为代表。

作为要寻找诗和远方的 Bandit 浪漫派算法,能不能和协同过滤这种正统算法结合起来呢?事实上已经有人这么尝试过了,叫做 COFIBA 算法,具体在题目为 Collaborative Filtering Bandits 和 Online Clustering of Bandits 的两篇文章中有详细的描述,它就是 Bandit 和协同过滤的结合算法,两篇文章的区别是后者只对用户聚类(即只考虑了 User-based 的协同过滤),而前者采用了协同聚类(co-clustering,可以理解为 item-based 和 user-based 两种协同方式在同时进行),后者是前者的一个特殊情况。下面详细介绍一下这种结合算法。

Bandit 结合协同过滤

很多推荐场景中都有这两个规律:

  • 相似的用户对同一个物品的反馈可能是一样的。也就是对一个聚类用户群体推荐同一个 Item,他们可能都喜欢,也可能都不喜欢,同样地,同一个用户会对相似的物品反馈相同。这是属于协同过滤可以解决的问题;
  • 在使用推荐系统过程中,用户的决策是动态进行的,尤其是新用户。这就导致无法提前为用户准备好推荐候选,只能“走一步看一步”,是一个动态的推荐过程。

每一个推荐候选 Item,都可以根据用户对其偏好不同(payoff 不同)将用户聚类成不同的群体,一个群体来集体预测这个 Item 的可能的收益,这就有了协同的效果,然后再实时观察真实反馈回来更新用户的个人参数,这就有了 Bandit 的思想在里面。

举个例子,如果你父母给你安排了很多相亲对象,要不要见面去相一下?那需要提前看看每一个相亲对象的资料,每次大家都分成好几派,有说好的,有说再看看的,也有说不行的;你自己也会是其中一派的一员,每次都是你所属的那一派给你集体打分,因为他们是和你“三观一致的人”,“诚不欺我”;这样从一堆资料中挑出分数最高的那个人,你出去见 TA,回来后把实际感觉说给大家听,同时自己心里的标准也有些调整,重新给剩下的其它对象打分,打完分再去见,周而复始……

以上就是协同过滤和 Bandit 结合的思想。

另外,如果要推荐的候选 Item 较多,还需要对 Item 进行聚类,这样就不用按照每一个 Item 对 User 聚类,而是按照每一个 Item 的类簇对 User 聚类,如此以来,Item 的类簇数相对于 Item 数要大大减少。

COFIBA 算法

基于这些思想,有人提出了算法 COFIBA(读作 coffee bar),简要描述如下:

在时刻 t,用户来访问推荐系统,推荐系统需要从已有的候选池子中挑一个最佳的物品推荐给他,然后观察他的反馈,用观察到的反馈来更新挑选策略。 这里的每个物品都有一个特征向量,所以这里的 Bandit 算法是 context 相关的。 这里依然是用岭回归去拟合用户的权重向量,用于预测用户对每个物品的可能反馈(payoff),这一点和 linUCB 算法是一样的。

图7   COFIBA算法描述

图7 COFIBA 算法描述

对比 LinUCB 算法,COFIBA 算法的不同有两个:

  • 基于用户聚类挑选最佳的 Item(相似用户集体决策的 Bandit)。
  • 基于用户的反馈情况调整 User 和 Item 的聚类(协同过滤部分)。
  • 整体算法过程如下:

核心步骤是,针对某个用户 i,在每一轮试验时做以下事情:

首先计算该用户的 Bandit 参数 W(和 LinUCB 相同),但是这个参数并不直接参与到 Bandit 的选择决策中(和 LinUCB 不同),而是用来更新用户聚类的;遍历候选 Item,每一个 Item 表示成一个 context 向量了。

每一个 Item 都对应一套用户聚类结果,所以遍历到每一个 Item 时判断当前用户在当前 Item 下属于哪个类簇,然后把对应类簇中每个用户的 M 矩阵(对应 LinUCB 里面的 A 矩阵),b 向量(payoff 向量,对应 linUCB 里面的 b 向量)聚合起来,从而针对这个类簇求解一个岭回归参数(类似 LinUCB 里面单独针对每个用户所做),同时计算其 payoff 预测值和置信上边界。每个 Item 都得到一个 payoff 预测值及置信区间上界,挑出那个上边界最大的 Item 推出去(和 LinUCB 相同)。

观察用户的真实反馈,然后更新用户自己的 M 矩阵和 b 向量(更新个人的,对应类簇里其他的不更新)。

以上是 COFIBA 算法的一次决策过程。在收到用户真实反馈之后,还有两个计算过程:

  1. 更新 User 聚类
  2. 更新 Item 聚类

如何更新 User 和 Item 的聚类呢?见图8:图8  User和Item聚类更新描述

图8 User 和 Item 聚类更新描述

解释一下图8。a. 这里有6个 User,8个 Item,初始化时,User 和 Item 的类簇个数都是1。

b1. 在某一轮试验时,推荐系统面对的用户是4。推荐过程就是遍历1~8每个 Item,然后看看对应每个 Item 时,User4 在哪个类簇中,把对应类簇中的用户聚合起来为这个 Item 预测 payoff 和 CB。这里假设最终 Item5 胜出,被推荐出去了。

b2. 在时刻 t,Item 有3个类簇,需要更新的用户聚类是 Item5 对应的 User4 所在类簇。更新方式:看看该类簇里面除了 User4 之外的用户,对 Item5 的 payoff 是不是和user4相近,如果是,则保持原来的连接边,否则删除原来的连接边。删除边之后重新构建聚类结果。这里假设重新构建后原来 User4 所在的类簇分裂成了两个类簇:{4,5}和{6}。

C. 更新完用户类簇后,Item5 对应的类簇也要更新。更新方式是:对于每一个和 Item5(被推荐出的那个 Item)还存在连接边的 Item j,都去构造一个 User 的近邻集合 N,这个集合的用户对 Item j 有相近的 payoff,然后看看 N 是不是和刚刚更新后的 User4 所在的类簇相同,是的话,保留 Item5 和 Item j 之间的连接边,否则删除。这里假设 Item 3 和 Item 5 之间的连接边被删除。Item3 独立后给他初始化了一个聚类结果:所有用户还是一个类簇。

简单来说就是这样:

  1. User-based 协同过滤来选择要推荐的 Item,选择时用了 LinUCB 的思想;
  2. 根据用户的反馈,调整 User-based 和 Item-based 的聚类结果;
  3. Item-based 的聚类变化又改变了 User 的聚类;
  4. 不断根据用户实时动态的反馈来划分 User-Item 矩阵。

总结

Exploit-Explore 这一对矛盾一直客观存在,Bandit 算法是公认的一种比较好的解决 EE 问题的方案。除了 Bandit 算法之外,还有一些其他的 explore 的办法,比如:在推荐时,随机地去掉一些用户历史行为(特征)。

解决 Explore,势必就是要冒险,势必要走向未知,而这显然就是会伤害用户体验的:明知道用户肯定喜欢 A,你还偏偏以某个小概率给推荐非 A。

实际上,很少有公司会采用这些理性的办法做 Explore,反而更愿意用一些盲目主观的方式。究其原因,可能是因为:

  1. 互联网产品生命周期短,而 Explore 又是为了提升长期利益的,所以没有动力做;
  2. 用户使用互联网产品时间越来越碎片化,Explore 的时间长,难以体现出 Explore 的价值;
  3. 同质化互联网产品多,用户选择多,稍有不慎,用户用脚投票,分分钟弃你于不顾;
  4. 已经成规模的平台,红利杠杠的,其实是没有动力做 Explore 的。
  5. 基于这些,我们如果想在自己的推荐系统中引入 Explore 机制,需要注意以下几点:
  6. 用于 Explore 的 Item 要保证其本身质量,纵使用户不感兴趣,也不至于引起其反感;
  7. Explore 本身的产品需要精心设计,让用户有耐心陪你玩儿;
  8. 深度思考,这样才不会做出脑残的产品,产品不会早早夭折,才有可能让 Explore 机制有用武之地。
打造企业级云深度学习平台

文/陈迪豪

深度学习服务介绍

机器学习与人工智能,相信大家已经耳熟能详,随着大规模标记数据的积累、神经网络算法的成熟以及高性能通用 GPU 的推广,深度学习逐渐成为计算机专家以及大数据科学家的研究重点。近年来,无论是图像的分类、识别和检测,还是语音生成、自然语言处理,甚至是 AI 下围棋或者打游戏都基于深度学习有了很大的突破。而随着 TensorFlow、Caffe 等开源框架的发展,深度学习的门槛变得越来越低,甚至初中生都可以轻易实现一个图像分类或者自动驾驶的神经网络模型,但目前最前沿的成果主要还是出自 Google、微软等巨头企业。

Google 不仅拥有优秀的人才储备和大数据资源,其得天独厚的基础架构也极大推动了 AI 业务的发展,得益于内部的大规模集群调度系统 Borg,开发者可以快速申请大量GPU资源进行模型训练和上线模型服务,并且通过资源共享和自动调度保证整体资源利用率也很高。Google 开源了 TensorFlow 深度学习框架,让开发者可以在本地轻易地组合 MLP、CNN 和 RNN 等模块实现复杂的神经网络模型,但 TensorFlow 只是一个数值计算库,并不能解决资源隔离、任务调度等问题,将深度学习框架集成到基于云计算的基础架构上将是下一个关键任务。

除了 Google、微软,国内的百度也开源了 PaddlePaddle 分布式计算框架,并且官方集成了 Kubernetes 等容器调度系统,用户可以基于 PaddlePaddle 框架实现神经网络模型,同时利用容器的隔离性和 Kubernetes 的资源共享、自动调度、故障恢复等特性,但平台不能支持更多深度学习框架接口。而亚马逊和腾讯云相继推出了面向开发者的公有云服务,可以同时支持多种主流的开源深度学习框架,阿里、金山和小米也即将推出基于 GPU 的云深度学习服务,还有无数企业在默默地研发内部的机器学习平台和大数据服务。

面对如此眼花缭乱的云服务和开源技术,架构师该如何考虑其中的技术细节,从用户的角度又该如何选择这些平台或者服务呢。我将介绍小米云深度学习平台的架构设计与实现细节,希望能给 AI 领域的研发人员提供一些思考和启示。

云深度学习平台设计

云深度学习平台,我定义为 Cloud Machine Learning,就是基于云计算的机器学习和深度学习平台。首先 TensorFlow、MXNet 是深度学习框架或者深度学习平台,但并不是云深度学习平台,它们虽然可以组成一个分布式计算集群进行模型训练,但需要用户在计算服务器上手动启动和管理进程,并没有云计算中任务隔离、资源共享、自动调度、故障恢复以及按需计费等功能。因此我们需要区分深度学习类库以及深度学习平台之间的关系,而这些类库实现的随机梯度下降和反向传播等算法却是深度学习应用所必须的,这是一种全新的编程范式,需要我们已有的基础架构去支持。

云计算和大数据发展超过了整整十年,在业界催生非常多优秀的开源工具,如实现了类似 AWS IaaS 功能的 OpenStack 项目,还有 Hadoop、Spark、Hive 等大数据存储和处理框架,以及近年很火的 Docker、Kubernetes 等容器项目,这些都是构建现代云计算服务的基石。这些云服务有共同的特点,例如我们使用 HDFS 进行数据存储,用户不需要手动申请物理资源就可以做到开箱即用,用户数据保存在几乎无限制的公共资源池中,并且通过租户隔离保证数据安全,集群在节点故障或者水平扩容时自动触发 Failover 且不会影响用户业务。虽然 Spark 通过 MLib 接口提供部分机器学习算法功能,但绝不能替代 TensorFlow、Caffe 等深度学习框架的作用,因此我们仍需要实现 Cloud Machine Learning 服务,并且确保实现云服务的基本特性——我将其总结为下面几条:

  • 屏蔽硬件资源保证开箱即用
  • 缩短业务环境部署和启动时间
  • 提供“无限”的存储和计算能力
  • 实现多租户隔离保证数据安全
  • 实现错误容忍和自动故障迁移
  • 提高集群利用率和降低性能损耗

相比于 MapReduce 或者 Spark 任务,深度学习的模型训练时间周期长,而且需要调优的超参数更多,平台设计还需要考虑以下几点:

  • 支持通用 GPU 等异构化硬件
  • 支持主流的深度学习框架接口
  • 支持无人值守的超参数自动调优
  • 支持从模型训练到上线的工作流

这是我个人对云深度学习平台的需求理解,也是小米在实现 cloud-ml 服务时的基本设计原则。虽然涉及到高可用、分布式等颇具实现难度的问题,但借助目前比较成熟的云计算框架和开源技术,我们的架构和实现基本满足了前面所有的需求,当然如果有更多需求和想法欢迎随时交流。

云深度学习平台架构

遵循前面的平台设计原则,我们的系统架构也愈加清晰明了,为了满足小米内部的所有深度学习和机器学习需求,需要有一个多租户、任务隔离、资源共享、支持多框架和 GPU 的通用服务平台。通过实现经典的 MLP、CNN 或 RNN 算法并不能满足业务快速发展的需求,因此我们需要支持 TensorFlow 等用户自定义的模型结构,并且支持高性能 GPU 和分布式训练是这个云深度学习平台的必须功能,不仅仅是模型训练,我们还希望集成模型服务等功能来最大化用户的使用效益。

计算机领域有句名言“任何计算机问题都可以通过增加一个中间层来解决”。无论是 AWS、OpenStack、Hadoop、Spark 还是 TCP/IP 都是这样做的,通过增加一个抽象层来屏蔽底层资源,对上层提供更易用或者更可靠的访问接口。小米的 cloud-ml 平台也需要实现对底层物理资源的屏蔽,尤其是对 GPU 资源的抽象和调度,但我们不需要重新实现,因为社区已经有了很多成熟的分布式解决方案,如 OpenStack、Yarn和Kubernetes。目前 OpenStack 和 Yarn 对 GPU 调度支持有所欠缺,虚拟机也存在启动速度慢、性能 overhead 较大等问题,而容器方案中的 Kubernetes 和 Mesos 发展迅速,支持 GPU 调度等功能,是目前最值得推荐的架构选型之一。

目前小米 cloud-ml 平台的任务调度和物理机管理基于多节点的分布式 Kubernetes 集群,对于 OpenStack、Yarn 和 Mesos 我们也保留了实现接口,可以通过实现 Mesos 后端让用户的任务调度到 Mesos 集群进行训练,最终返回给用户一致的使用接口。目前 Kubernetes 最新稳定版是1.6,已经支持 Nvidia GPU 的调度和访问,对于其他厂商 GPU 暂不支持但基本能满足企业内部的需求,而且 Pod、Deployment、Job、StatefulSet 等功能日趋稳定,加上 Docker、Prometheus、Harbor 等生态项目的成熟,已经在大量生产环境验证过,可以满足通用 PaaS 或者 Cloud Machine learning 等定制服务平台的需求。

使用 Kubernetes 管理用户的 Docker 容器,还解决了资源隔离的问题,保证不同深度学习训练任务间的环境不会冲突,并且可以针对训练任务和模型服务使用 Job 和 Deployment 等不同的接口,充分利用分布式容器编排系统的重调度和负载均衡功能。但是,Kubernetes 并没有完善的多租户和 Quota 管理功能,难以与企业内部的权限管理系统对接,这要求我们对 Kubernetes API 进行再一次“抽象”。我们通过 API Server 实现了内部的 AKSK 签名和认证授权机制,在处理用户请求时加入多租户和 Quota 配额功能,并且对外提供简单易用的 RESTful API,进一步简化了整个云深度学习平台的使用流程,整体架构设计如图1。

图1  云深度学习平台整体架构

图1 云深度学习平台整体架构

通过实现 API Server,我们对外提供了 API、SDK、命令行以及 Web 控制台多种访问方式,最大程度上满足了用户复杂多变的使用环境。集群内置了 Docker 镜像仓库服务,托管了我们支持的17个深度学习框架的容器镜像,让用户不需要任何初始化命令就可以一键创建各框架的开发环境、训练任务以及模型服务。多副本的 API Server 和 Etcd 集群,保证了整个集群所有组件的高可用,和 Hadoop 或者 Spark 一样,我们的 cloud-ml 服务在任意一台服务器经历断网、宕机、磁盘故障等暴力测试下都能自动 Failover 保证业务不受任何影响。

前面提到,我们通过抽象层定义了云深度学习平台的接口,无论后端使用 Kubernetes、Mesos、Yarn 甚至是 OpenStack、AWS 都可以支持。通过容器的抽象可以定义任务的运行环境,目前已经支持17个主流的深度学习框架,用户甚至可以在不改任何一行代码的情况下定义自己的运行环境或者使用自己实现的深度学习框架。在灵活的架构下,我们还实现了分布式训练、超参数自动调优、前置命令、NodeSelector、Bring Your Own Image 和 FUSE 集成等功能,将在下面逐一介绍。

云深度学习平台实现

前面提到我们后端使用 Kubernetes 编排系统,通过 API Server 实现授权认证和 Quota 配额功能。由于云深度学习服务是一个计算服务,和我以前做过的分布式存储服务有着本质的区别,计算服务离线运算时间较长,客户端请求延时要求较低而且吞吐很小,因此我们的 API 服务在易用性和高性能上可以选择前者,目前主流的 Web 服务器都可以满足需求。基于 Web 服务器我们可以实现集成内部权限管理系统的业务逻辑,小米生态云提供了类似 AWS 的 AKSK 签名认证机制,用户注册登录后可以自行创建 Access key 和 Secret key,请求时在客户端进行 AKSK 的签名后发送,这样用户不需要把账号密码或密钥加到请求中,即使密钥泄露也可以由用户来禁用,请求时即使签名被嗅探也只能重放当前的请求内容,是非常可靠的安全机制。除此之外,我们参考 OpenStack 项目的体系架构,实现了多租户和 Quota 功能,通过认证和授权的请求需要经过 Quota 配额检查,在高可用数据库中持久化相应的数据,这样平台管理员就可以动态修改每个租户的 Quota,而且用户可以随时查看自身的审计信息。

小米 cloud-ml 服务实现了深度学习模型的开发、训练、调优、测试、部署和预测等完整功能,都是通过提交到后端的 Kubernetes 集群来实现,完整的功能介绍可以查看官方文档。Kubernetes 对外提供了 RESTful API 访问接口,通过 YAML 或者 JSON 来描述不同的任务类型,不同编程语言实现的系统也可以使用社区开发的 SDK 来访问。对于我们支持的多个深度学习框架,还有开发环境、训练任务、模型服务等功能,都需要定制 Docker 镜像,提交到 Kubernetes 时指定使用的容器镜像、启动命令等参数。通过对 Kubernetes API 的封装,我们可以简化 Kubernetes 的使用细节,保证了对 Mesos、Yarn 等后端支持的兼容性,同时避免了直接暴露 Kubernetes API 带来的授权问题以及安全隐患。

除了可以启动单个容器执行用户的训练代码,小米 cloud-ml 平台也支持 TensorFlow 的分布式训练,使用时只需要传入 ps 和 worker 个数即可。考虑到对 TensorFlow 原生 API 的兼容性,我们并没有定制修改 TensorFlow 代码,用户甚至可以在本地安装开源的 TensorFlow 测试后再提交,同样可以运行在云平台上。但本地运行分布式 TensorFlow 需要在多台服务器上手动起进程,同时要避免进程使用的端口与其他服务冲突,而且要考虑系统环境、内存不足、磁盘空间等问题,代码更新和运维压力成倍增加,Cloud Machine Learning 下的分布式 TensorFlow 只需要在提交任务时多加两个参数即可。有人觉得手动启动分布式 TensorFlow 非常繁琐,在云端实现逻辑是否更加复杂?其实并不是,通过云服务的控制节点,我们在启动任务前就可以分配不会冲突的端口资源,启动时通过容器隔离环境资源,而用户不需要传入 Cluster spec 等繁琐的参数,我们遵循 Google CloudML 标准,会自动生成 Cluster spec 等信息通过环境变量加入到容器的启动任务中。这样无论是单机版训练任务,还是几个节点的分布式任务,甚至是上百节点的分布式训练任务,cloud-ml 平台都可以通过相同的镜像和代码来运行,只是启动时传入的环境变量不同,在不改变任何外部依赖的情况下优雅地实现了看似复杂的分布式训练功能。

图2  云深度学习平台分布式训练

图2 云深度学习平台分布式训练

看到这里大家可能认为,小米的 cloud-ml 平台和 Google 的 CloudML 服务,都有点类似之前很火的 PaaS(Platform as a Service)或者 CaaS(Container as a Service)服务。确实如此,基于 Kubernetes 或者 Mesos 我们可以很容易实现一个通用的 CaaS,用户上传应用代码和 Docker 镜像,由平台调度和运行,但不同的是 Cloud Machine Learning 简化了与机器学习无关的功能。我们不需要用户了解 PaaS 的所有功能,也不需要支持所有编程语言的运行环境,暴露提交任务、查看任务、删除任务等更简单的使用接口即可,而要支持不同规模的 TensorFlow 应用代码,用户需要以标准的 Python 打包方式上传代码。 Python 的标准打包方式独立于 TensorFlow 或者小米 cloud-ml 平台,幸运的是目前 Google CloudML 也支持 Python 的标准打包方式,通过这种标准接口,我们甚至发现 Google CloudML 打包好的 samples 代码甚至可以直接提交到小米 cloud-ml 平台上训练。这是非常有意思的尝试,意味着用户可以使用原生的 TensorFlow 接口来实现自己的模型,在本地计算资源不充足的情况下可以提交到 Google CloudML 服务上训练,同时可以一行代码不用改直接提交到小米或者其他云服务厂商中的云平台上训练。如果大家在实现内部的云深度学习平台,不妨也参考下标准的 Python 打包方式,这样用户同一份代码就可以兼容所有云平台,避免厂商绑定。

除了训练任务,Cloud Machine Learning 平台最好也能集成模型服务、开发环境等功能。对于模型服务,TensorFlow 社区开源了 TensorFlow Serving 项目,可以加载任意 TensorFlow 模型并且提供统一的访问接口,而 Caffe 社区也提供了 Web demo 项目方便用户使用。目前 Kubernetes 和 Mesos 都实现了类似 Deployment 的功能,通过制作 TensorFlow Serving 等服务的容器镜像,我们可以很方便地为用户快速启动对应的模型服务。通过对 Kubernetes API 的封装,我们在暴露给用户 API 时也提供了 replicas 等参数,这样用户就可以直接通过 Kubernetes API 来创建多副本的 Deployment 实例,并且由 Kubernetes 来实现负载均衡等功能。除此之外,TensorFlow Serving 本身还支持在线模型升级和同时加载多个模型版本等功能,我们在保证 TensorFlow Serving 容器正常运行的情况下,允许用户更新分布式对象存储中的模型文件就可以轻易地支持在线模型升级的功能。

对于比较小众但有特定使用场景的深度学习框架,Cloud Macine Learning 的开发环境、训练任务和模型服务都支持 Bring Your Own Image 功能,也就是说用户可以定制自己的 Docker 镜像并在提交任务时指定使用。这种灵活的设置极大地降低了平台管理者的维护成本,我们不需要根据每个用户的需求定制通用的 Golden image,事实上也不可能有完美的镜像可以满足所有需求,用户不同的模型可能有任意的 Python 或者非 Python 依赖,甚至是自己实现的私有深度学习框架也可以直接提交到 Cloud Machine Learning 平台上训练。内测 BYOI 功能时,我们还惊喜地发现这个功能对于我们开发新的深度学习框架支持,以及提前测试镜像升级有非常大的帮助,同时用户自己实现的 Caffe 模型服务和 XGBoost 模型服务也可以完美支持。

当然 Cloud Machine Learning 平台还可以实现很多有意思的功能,例如通过对线上不同 GPU 机型打 label,通过 NodeSelector 功能可以允许用户选择具体的 GPU 型号进行更细粒度的调度,这需要我们暴露更底层 Kubernetes API 实现,这在集群测试中也是非常有用的功能。而无论是基于 GPU 的训练任务还是模型服务,我们都制作了对应的 CUDA 容器镜像,通过 Kubernetes 调度到对应的 GPU 计算节点就可以访问本地图像处理硬件进行高性能运算了。小米 cloud-ml 还开放了前置命令和后置命令功能,允许用户在启动训练任务前和训练任务结束后执行自定义命令,对于不支持分布式存储的深度学习框架,可以在前置命令中挂载 S3 fuse和 FDS fuse 到本地目录,或者初始化 HDFS 的 Kerberos 账号,灵活的接口可以实现更多用户自定义的功能。还有超参数自动调优功能,与 Google CloudML 类似,用户可以提交时指定多组超参数配置,云平台可以自动分配资源起多实例并行计算,为了支持读取用户自定义的指标数据,我们实现了类似 TensorBoard 的 Python 接口直接访问 TensorFlow event file 数据,并通过命令行返回给用户最优的超参数组合。最后还有 TensorFlow Application Template 功能,在 Cloud Machine Learning 平台上用户可以将自己的模型代码公开或者使用官方维护的开源 TensorFlow 应用,用户提交任务时可以直接指定这些开源模板进行训练,模板已经实现了 MLP、CNN、RNN 和 LR 等经典神经网络结构,还可以通过超参数来配置神经网络每一层的节点数和层数,而且可以支持任意稠密和稀疏的数据集,这样不需要编写代码就可以在云平台上训练自己的数据快速生成 AI 模型了。

在前面的平台设计和平台架构后,要实现完整的云深度学习服务并不困难,尤其是集成了Docker、Etcd、Kubernetes、TensorFlow 等优秀开源项目,组件间通过 API 松耦合地交互,需要的重复工作主要是打通企业内部权限系统和将用户请求转化成 Kubernetes 等后端请求而已,而支持标准的打包方式还可以让业务代码在任意云平台上无缝迁移。

云深度学习平台实践

目前小米云深度学习平台已经在内部各业务部门推广使用,相比于直接使用物理机,云服务拥有超高的资源利用率、快速的启动时间、近乎“无限”的计算资源、自动的故障迁移、支持分布式训练和超参数自动调优等优点,相信可以得到更好的推广和应用。

除了完成上述的功能,我们在实践时也听取了用户反馈进行改进。例如有内部用户反馈,在云端训练的 TensorFlow 应用把 event file 也导出到分布式存储中,使用 TensorBoard 需要先下载文件再从本地起服务相对麻烦,因此我们在原有基础架构实现了 TensorboardService 功能,可以一键启动 TensorBoard 服务,用户只需要用浏览器就可以打开使用。

管理 GPU 资源和排查 GPU 调度问题也是相当繁琐的,尤其是需要管理不同 GPU 设备和不同 CUDA 版本的异构集群,我们统一规范了 CUDA 的安装方式,保证 Kubernetes 调度的容器可以正常访问宿主机的 GPU 设备。当然对于 GPU 资源的调度和释放,我们有完善的测试文档可以保证每一个 GPU 都可以正常使用,根据测试需求实现的 NodeSelector 功能也帮忙我们更快地定位问题。

由于已经支持几十个功能和十几个深度学习框架,每次升级都可能影响已有服务的功能,因此我们会在多节点的分布式 staging 集群进行上线演习和测试,并且实现 smoke test 脚本进行完整的功能性测试。服务升级需要更新代码,但是为了保证不影响线上业务,无论是 Kubernetes 还是我们实现的 API Server 都有多副本提供服务,通过高可用技术先迁移服务进行滚动升级,对于一些单机运行的脚本也通过 Etcd 实现了高可用的抢主机制,保证所有组件没有单点故障。

大家可以通过前面提到的文档地址和 cloud-ml-sdk 项目了解到更多细节,或者关注我微博(@tobe-陈迪豪)与我交流。

总结

本文介绍了实现企业级云深度学习平台需要的概念和知识,基于小米 cloud-ml 服务探讨了云平台的设计、架构、实现以及实践这四方面的内容,希望大家看完有所收获。

机器学习平台 JDLP 长成记
Weiflow——微博机器学习框架
微博深度学习平台架构和实践
机器学习在热门微博推荐系统的应用
特征选择在新浪微博的演进
美丽联合业务升级下的机器学习应用
浅析强化学习及使用 Policy Network 实现自动化控制
强化学习解析与实践
基于容器的AI系统开发
看得“深”、看得“清”
基于深度学习的计算机视觉技术发展
面向图像分析应用的海量样本过滤方案
人脸识别技术发展及实用方案设计
从“连接”到“交互”
SLAM 刚刚开始的未来之“工程细节”
TensorFlow 下构建高性能神经网络模型的最佳实践
在物联网设备上实现深度学习
无人驾驶系统安全
无人驾驶硬件平台
无人驾驶刚刚开始的未来

阅读全文: http://gitbook.cn/gitchat/geekbook/5a5c27a92be8c36114822a3a

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页

打赏作者

蔚1

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值