MLTalks

Stay Hungry, Stay Foolish

1. 论文

1.1 基本使用

  • SentencePiece是用于NLP训练中对句子进行token化的工具,跟语言无关, SentencePiece中包含了byte-pairencoding(BPE)unigram language model两种切分subword的算法。

  • SentencePiece中包含有四个部分:

    • Normalizer: 规一化操作,类似Unicode把字符转为统一格式。
    • Trainer: 从规一化后的语料中学习subword的切分模型。
    • Encoder: 对应预处理时的tokenization操作,把句子转为对应的subword或id。
    • Decoder: 对应后处理时的detokenization操作,把subword或id还原为原有句子
阅读全文 »

什么是协程

协程(coroutine) 的概念根据Donald Knuth的说法早在1958年就由Melvin Conway提出了,对应wikipedia的定义如下: > Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes. 这里_子例程(subroutine)是一个概括性的术语,子例程可以是整个程序中的一个代码区块,当它被主程序调用的时候就会进入运行。例如函数就是子例程中的一种。

1
c = max(a,b);
从wikipedia定义可以看出协程相比子例程更加的灵活,允许执行过程中_被挂起_和_恢复,多个协程可以一起相互协作执行任务。从协程(co + routine)名字上来拆解为支持 协作(cooperate) 的例程。

协程与子例程的执行区别

图中左边是子例程的执行过程,右边是协程的执行过程,可以很明显的看出来执行过程中的区别。 * 先说左边,子例程可以看成是某个函数,子例程的执行过程中通常是嵌套顺序执行的过程,子例程1和子例程2的关系(调用和被调用)不是完全平等的,子例程1能调用子例程2,但子例程2不能反过来调用子例程1。 * 再说右边,协程1和协程2的关系是完全对等的,协程1执行过程中可以中断挂起执行另外一个协程2,反之也是可以的,直到最终两个协程都执行完以后再返回回到主程序中,即协程1和协程2相互协作完成了整个任务。 接下来举一个协程实现生产者和消费者的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var q := new queue

coroutine produce
loop
while q is not full
create some new items
add the items to q
yield to consume

coroutine consume
loop
while q is not empty
remove some items from q
use the items
yield to produce
这里有一个队列queue,一个生产者produce,一个消费者consume,yield代表中断挂起当前协程,并恢复其他协程的操作。生产者生产物品以后加入到队列以后,中断挂起自身并恢复消费者,消费者从队列中消费完物品以后中断挂起自身并恢复生产者,不断来回切换直到达到最终条件(比如所有原料都生产成物品并全都被消费完成),程序终止。

进程、线程、协程的关系和比较

通常会提到 进程是资源分配的最小单位,线程是CPU调度的最小单位, 一个进程里可以有多个线程 ,这里直接画了个图来说明三者关系。

  • 进程是资源分配的最小单位,会拥有独立的地址空间以及对应的内存空间,还有网络和文件资源等,不同进程之间资源都是独立的,可以通过进程间通信(管道、共享内存、信号量等方式)来进行交互。
  • 线程为CPU调度的基本单位,除了拥有运行中必不可少的信息(如程序计数器、一组寄存器和栈)以外,本身并不拥有系统资源,所有线程会共享进程的资源,比如会共享堆资源。
  • 协程可以认为是运行在线程上的代码块,协程提供的挂起操作会使协程暂停执行,而不会导致线程阻塞。一个线程内部可以创建几千个协程都没有任何问题。
  • 进程的切换和线程切换中都包含了对应上下文的切换,这块都涉及到了内核来完成,即一次用户态到内核态的切换和一次内核态到用户态的切换。因为进程上下文切换保存的信息更多,所以进程切换代价会比线程切换代价更大。
  • 协程是一个纯用户态的并发机制,同一时刻只会有一个协程在运行,其他协程挂起等待;不同协程之间的切换不涉及内核,只用在用户态切换即可,所以切换代价更小,更轻量级,适合IO密集型的场景。

coroutine的python实现

  1. Python最初的版本里是包含了yield/send关键字,通过yield/send可以方便的实现一个协程的例子,这里还是以为生产者和消费者为例,具体实现方式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-

    def consumer():
    """consumer定义"""
    ret = ''
    while True:
    # 挂起consumer,恢复producer, ret会传回给producer
    item_id = yield ret
    if not item_id:
    return
    print('consume item_id:{}'.format(item_id))
    ret = 'success'

    def producer(consumer):
    """producer定义"""
    # send一个None可以看成是启动consumer
    # consumer这里包含了yield关键字相当于是一个generator
    consumer.send(None)
    item_id = 0
    # 生产的item的总数
    item_total_count = 3
    while item_id < item_total_count:
    item_id = item_id + 1
    print('produce item:{}'.format(item_id))
    # 挂起producer,恢复consumer, item_id会传回给consumer
    ret = c.send(item_id)
    print('consumer return:{}'.format(ret))
    consumer.close()

    if __name__ == "__main__":
    c = consumer()
    producer(c)

    结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    produce item:1
    consume item_id:1
    consumer return:success
    produce item:2
    consume item_id:2
    consumer return:success
    produce item:3
    consume item_id:3
    consumer return:success

  2. python 3.5版本开始引入了async/await关键字给了我们另外一种实现的方法,还是以为生产者和消费者为例,具体实现方式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-

    import asyncio
    import random

    async def producer(queue, item_total_count):
    """producer 定义"""
    for item_id in range(0, item_total_count):
    # 生产一个新的item
    print('produce item_id:{}'.format(item_id))
    # 模拟IO操作, 挂起producer,恢复consumer
    #await asyncio.sleep(random.random())
    # 把item放入队列, 挂起producer,恢复consumer
    await queue.put(item_id)
    # 放入一个None到队列表示生产全部结束
    await queue.put(None)


    async def consumer(queue):
    """consumer 定义"""
    while True:
    # 等待从队列中获得一个新的item, 等待过程中会挂起consumer,恢复producer
    item_id = await queue.get()
    if item_id is None:
    # 表示生产者都生产完了,没有待消费的请求了
    break
    print('consume item_id:{}'.format(item_id))
    # 模拟IO操作, 挂起consumer,恢复producer
    #await asyncio.sleep(random.random())

    if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    queue = asyncio.Queue(loop=loop)
    producer_coro = producer(queue, 3)
    consumer_coro = consumer(queue)
    loop.run_until_complete(asyncio.gather(producer_coro, consumer_coro))
    loop.close()

    结果:

    1
    2
    3
    4
    5
    6
    produce item_id:0
    produce item_id:1
    produce item_id:2
    consume item_id:0
    consume item_id:1
    consume item_id:2

本文主要根据A Berkeley View of Systems Challenges for AI进行了汇总,以及一些材料上的收集,论文中对未来AI系统以及研究的方向做了非常广泛的讨论。

AI发展的趋势介绍

关键任务(Mission-critical) AI

Mission-critical的含义就是在AI在生活中起到的任务越来越重要,例如自动驾驶、机器人辅助医疗、智能家居等,这些AI的应用非常关键以至于可以影响到人的生死。AI在不断变化的环境中进行部署,必须不断的适应周围环境并学习新的知识。比如自动驾驶需要在处理之前没有遇到过的危险场景中,观察学习其他车辆如何行动从而恰当的处理。另外,关键任务AI也要能处理各种噪音数据以及恶意的攻击。挑战: 不断地与周围动态环境进行互动学习,从而做出及时、鲁棒、安全的决策。

个性化(Personalized) AI

基于用户的个性化决策要考虑到用户自己的习惯与偏好,例如虚拟助理的语音可以模仿用户的口音,自动驾驶可以学习用户的驾驶习惯等。这样的个性化应用需要收集到大量敏感的用户信息,这些信息的滥用可能会造成用户的经济以及精神上的伤害。

挑战: 设计AI系统必须支持个性化的应用,但是这些个性化应用不能损害用户的利益,不能泄露用户的隐私。

跨组织的AI

许多公司在利用第三方的数据去提升他们自己的AI服务能力,例如医院之间的数据共享可以更好的组织传染病的爆发、金融机构之间的数据共享可以更好的进行反欺诈检测。这种应用的扩展使得从一家公司进行数据收集、处理,变成了一个公司使用多方的数据资源来进行决策。

挑战: 设计AI系统需要支持多源数据的训练,这些数据都来自不同的公司或者组织,并在训练过程中保证各家原始数据的机密性,在保证数据机密性的基础上甚至可以提供AI的能力给它的竞争对手。

AI的需求超越了摩尔定律

海量数据的存储和处理是近些年AI系统成功的关键,由于下列两个原因使得后续的数据处理能力很难跟上数据扩张的速度。 * 数据增长是指数级的。IoE设备在2018年采集的数据大小是400ZB,是2015年的50倍,到2025年,需要有3到4个数量级的增长才能处理人类的基因组数据,需要要求接下来的每年计算能力都至少要翻倍。 * 硬件的发展到了瓶颈。需要10年DRAMs和磁盘大小才能翻倍,需要20年CPU的性能才能翻倍。

挑战: 发展领域(domain-specific)的架构和软件系统去满足后摩尔时代性能要求,例如定制的人工智能芯片、高效处理的数据的边缘云计算(edge-cloud)以及数据的采集与抽样方法的发展。

阅读全文 »

高维稀疏场景描述

机器学习中一个通常的预估任务是为了预估一个函数$ y: ^n \(, 预估过程中使用一组具有实数值的特征向量\) ^n $ 去预测目标 $ \((在回归预估中\) = $, 在分类预估中 $ = +, - \(). 高维稀疏场景下向量\) \(中大部分元素\) x_i \(的值都是0, 我们定义\) m(x) $为向量 $ $ 中非零元素的个数,$ _{D} \(为所有向量\) \(的\) m(x) \(值的平均值。实际中(例如推荐系统)经常会碰到超大规模稀疏的情况(\) _{D} n \()。 下面举一个例子,假设我们有一个电影的推荐系统,系统中每一条记录表示一个用户(\) u U \()在某个时间点(\) t \(}对一个电影(item)(\) i I \()的评分(\) r , 2, 3, 4, 5 \(). 用户\) U $和电影item $ I $的示例如下: > $ U = Alice(A), Bob(B), Charlie(C), $ > $ I = Titanic (TI), Notting Hill (NH), Star Wars (SW), Star Trek (ST), $ 系统可观测到的用户数据为$ S $示例如下: > $ S = (A, TI, 2010-1, 5), (A, NH, 2010-2, 3), (A, SW, 2010-4, 1), $ > $ (B, SW, 2009-5, 4), (B, ST, 2009-8, 5), (C, TI, 2009-9, 1), (C, SW, 2009-12, 5) $

如下图所示:

图中展示了如何从观测到的用户数据$ S \(去创建训练所需要的特征向量的过程,图中蓝色框住的地方表示系统中的用户,每一条记录都表示一个用户的一个行为对\) (u, i, t, r) S \(,上面\) S \(中的第一条记录\) (A, TI, 2010-1, 5) \(,Alice简写为A,在蓝框中\) (x_{A}^{(1)} = 1) \(,红色框表示看的电影的item,这里\) x_{TI}^{(1)} = 1 $. 黄色框中表示用户对历史看过的所有的电影的历史评分,这里历史评分的取值经过了归一化操作,归一化以后的值的和为1。绿色表示从09年1月开始计算经历的月份数。深红色的框表示用户上次评价的电影名, 评价过的电影对应的位置设置成1,未评价的设置成0。

阅读全文 »

这里先提前说下谱聚类的基本特征,然后后面按照论文的顺序做了讲解。 * 谱聚类算法与kmeans的区别,是把聚类的过程看成了一个图切割的过程,每一条样本都看成是N维空间内的一个节点,然后我们希望切割出来的子图,在子图内部点与点之前的距离(也就是相似度)会比较高,子图与子图之间的距离尽可能的小。 * 在谱聚类的过程中会把原始数据会做变换,从高维空间映射到低维空间,这样更容易进行聚类,可以看成向量化的过程。 * 矩阵的谱(spectrum)在数学中的定义表示是矩阵一系列的特征值。

阅读全文 »

基本结构

Variable

定义在variable.h文件中,具体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Variable {
public:
template <typename T>
const T& Get() const {
PADDLE_ENFORCE(holder_ != nullptr, "Variable must hold some thing");
PADDLE_ENFORCE(IsType<T>(),
"Variable must be type %s, the holding type is %s",
typeid(T).name(), holder_->Type().name());
return *static_cast<const T*>(holder_->Ptr());
}
template <typename T>
T* GetMutable() {
if (!IsType<T>()) {
holder_.reset(new PlaceholderImpl<T>(new T()));
}
return static_cast<T*>(holder_->Ptr());
}
...
std::unique_ptr<Placeholder>
holder_; // pointers to a PlaceholderImpl object indeed.
friend class Scope;
const std::string* name_;
};
Placeholder用来真正保存分配的空间, 实现在PlaceholderImpl中。 Variable中的holder_保留了指向Placeholder实例的指针。每个variable都有一个string类型的名字保存在name_中,名字只在当前Scope空间中国年有效。

阅读全文 »

简介

在生产环境部署模型预测的时候,在负载较高的情况下也对实时性、准确性、鲁棒性有很高的要求。现有很多的机器学习框架和系统都是针对模型训练而不是预测。本文中Clipper是一个通用的低延迟预测服务系统,通过模块化的架构来简化模型预测的部署。

Clipper1 2系统是一个分层的架构, 通过分层简化了预测服务栈, 并实现了预测服务的三个目标:低延迟、高吞吐、提高准确度。整体被分为了两层, 第一个是模型抽象层(model abstraction layer), 第二个是模型选择层(model selection layer)。抽象层提供了统一的API来屏蔽ML框架和模型的差异,实现模型对应用的透明;模型选择层在模型抽象层之上,动态选择模型进行预测并融合不同模型预测结果,从而提供更加准确和鲁棒的模型预测服务。

为了实现低延迟和高吞吐,Clipper中采用了非常多优化手段。在抽象层中,Clipper针对不同模型的预测进行了缓存,并针对请求延迟的情况进行自适应调整batch大小,最大化提升请求吞吐量。在模型选择层,为了提高预测的精度,Clipper采用了bandit方法,对不同模型预测的结果进行了集成,评估模型预测的不确定性;在降低延迟方面,也采用了优化落后者(straggle3 mitigation)方法,预测过程中不等待最慢的模型的返回值。作者对Clipper用Rust进行了实现4,添加了多机器学习框架的支持, 包括Spark MLlib, Scikit-Learn, Caffe, TensorFlow, HTK等。

系统架构详细说明

Clipper架构被分为了模型选择层和模型抽象层,抽象层负责提供统一的预测接口,保证资源的隔离,针对批处理的(batch oriented)机器学习框架优化预测请求的负载。选择层负责将预测请求分发给不同的模型,基于反馈来组合预测结果,以此提高预测的准确度、评估不确定性、提供鲁棒的预测服务。

系统中预测请求先从请求方通过Clipper提供的REST或者RPC API发送过来,Clipper收到请求以后选择层根据请求的类型和最近的反馈,将请求通过模型抽象层分发给具体的一个或者多个模型进行预测操作。模型抽象层在将请求分配给自适应批计算队列(adaptive batching queue)以前,会先检查请求在预测缓存区中是否存在。Adaptive-batching-queue保存了针对不同机器学习框架和模型进行调整过的预测请求,然后请求会通过RPC发送给保存具体的模型的容器进行处理。这里为了方便处理,我们把模型容器部署到不同的Docker容器中。预测完成以后,结果会按照请求的反向顺序先发送给抽象层,然后是模型选择层。模型选择层会根据收到的多个不同模型预测的结果生成最终的预测结果以及估计置信度,最后把结果返回给请求的终端。终端请求方也会统计预测的效果,并将收集到这些信息通过RPC/REST请求发送给模型选择层。模型选择层会根据收到的反馈信息来对预测进行调整。接下来来介绍模型抽象层和模型选择层。

阅读全文 »