关闭

高并发网站架构与正态分布的前生今世

标签: 并发服务器架构
343人阅读 评论(0) 收藏 举报
分类:

高并发网站架构

什么是服务器?
不就是提供“付费”、“免费”服务的高档电脑嘛!

你提到服务?
存储一个图片,读取一篇文字,观看一个动作片,计算一个账户存款,…

什么是并发?
不如讲一讲什么是不并发。

我有一台服务器,1核CPU,连接到互联网提供服务。在09:00时刻,突然有100个用户同时要看服务器的数据,服务器怎么办?

          +-------+      09:00
          |       | 
          | 服务器 | 
          |       |
          +-------+
              |
              |
  ----------------------------

             互联网

  ----------------------------
    |    |    |   ......   |
  客户1 客户2 客户3        客户100

服务器:

–> 读取客户1的请求, 验证客户身份, 把数据发送给你, 用时1秒 [ 客户2到100等待中 ]
–> 读取客户2的请求, 验证客户身份, 把数据发送给你, 用时1秒 [ 客户3到100等待中 ]
–> 读取客户3的请求, 验证客户身份, 把数据发送给你, 用时1秒 [ 客户4到100等待中 ]
………………………………………………………………
–> 读取客户100的请求,验证客户身份, 把数据发送给你, 用时1秒 []
这就是“不并发”,即“迭代”,也就是“循环”的意思。

迭代 == 循环
既然来了100个客户,那么一个一个的处理,循环从客户1一直到客户100。处理完成客户1才去处理客户2,…。这样我们可以看出:

客户1从发出请求到收到响应,等待了1秒
客户2从发出请求到收到响应,等待了2秒
客户3从发出请求到收到响应,等待了3秒
…………………………….
客户100从发出请求到收到响应,等待了100秒
这就是“不并发”的问题,同时来100个客户,这些用户会排起长长的队伍,等待很长的时候,服务器才会去为他服务。客户可不喜欢这样的地方。

把服务器比喻成一个KFC,那么“不并发”就意味着只提供一个服务员,来了100个客户,当然要排个长长的队伍了。

          +-------+      09:00
          |       | 
          |  KFC  | 
          |       |
          +-- S --+
            客户1
            客户2
            客户3
            ......
            客户100

那么,如何并发?
这个问题太广泛了,需要先从 “CPU” “操作系统” “进程” 开始。

CPU 操作系统 进程
操作系统运行时,采用“抢占”的方式。当今绝大多数操作系统采用资源“抢占”。资源就是CPU计算,内存使用,磁盘读写,…基于此设计,在单核CPU的环境里,可以同时运行多个进程。

操作系统会划分时间片,并使用一个任务队列,把每个进程每个阶段的任务分配一个时间片,比如1ms(实际小的多)。1ms运行进程的任务,没有完成就挂起放入队列末端,下一次再运行。然后操作系统运行任务队列的下一个任务。

比如有个进程,他的任务是打开一个文件,然后读取100个字符,并把文件写入10个字符。

操作系统会运行进程,先打开文件,如果这时候时间片时间到了,挂起进程,放入队列后面,运行下一个进程。

当操作系统根据任务队列的前进,又一次到达这个进程,操作系统读取100个字符,时间到,挂起进程,放入队列后面,运行下一个进程。

… 如此重复 …

时间片?
事实上,操作系统被聪明的设计,即便是单核CPU,也可以同时运行多个进程。操作系统经常同时运行多个进程,比如 Photoshop, Firefox, Vim, … 他们是同时运行的,而且能“同时”工作。

对于进程,操作系统不会把 CPU 和内存一直放在某个进程中。如果这样,当有个进程耗费时间特别长时,其他的进程就罢工了,也就无法同时运行多进程了。

所以,操作系统会给每个进程一个时间片,即运行进程中任务的时间上限,到达时间后会挂起进程,放入任务队列后面,直到下一次任务队列取出这个进程任务。

任务队列?
操作系统使用一个线性的队列,管理ta自己的工作流程。操作系统不停地取出任务,运行,取出任务,运行,…

从编程的角度观看,就好比是一个数组。

IO的根本:内核缓冲区
对磁盘写需要花费大量时间,而对内存写则小的多。
一个高效的做法是:在内核区域开辟一块内存,用来放置读取和写入的内容。

比如

程序员在9:00写入100个字符,这些字符被复制到内核缓冲区中,这是属于内核的一块内存。

在9:10写入20个字符,这些字符也被复制到内核缓冲区中。

在某个时间,比如9:30,操作系统把内核缓冲区中写入的所有内容排序,然后一次性写入到磁盘。
从9:00到9:30之间可能写入了几百次内核缓冲区,但都是在内存区域,速度会很快,而在9:30只进行了一次磁盘写入。这样把数百次的写入操作集合成一次磁盘写入。从而减少磁盘写入次数。

———-+—————-+—————-+———-+—————> 时间线

 [100个字符]        [20个字符]           ...

 9:00 |写入         9:10 |写入       9:xx|写入
      v                 v              v 
+-----------------------------------------------------------------+
|                             内核缓冲区                            |  
+-----------------------------------------------------------------+       

                                              9:30 | 写入
                                                   v
+-----------------------------------------------------------------+
|                               磁盘                               |
+-----------------------------------------------------------------+

实际的计算机其运行速度非常高,9:00~9:30只不过是我们的人为假设,计算机这段时间间隔大概只有1分钟,或者更低,而在这1分钟内,可能已经运行了成百上千次不同的写入。

缓冲?
缓冲是一块内存,里面放着乱糟糟的东西。内存由小格子组成,每个小格子代表一位,可以放置一个0或者1。8个小格子称为1个字节,1024个小格子称为1千个字节,二进制都是用2的倍数表示,所以2进制的1千是1024。

计算机启动后,内存中的每个小格子都是有值的,我没有深入研究过初始是什么值。但是我们可以假定是0.

现在,需要一块缓冲,那就是从内存中拿出一块没有使用的区域,里边是很多小格子,每个小格子放置了0或者1。小格子中肯定有0或者1,不可能是空白的。

既然小格子都是0 1,那么这块内存中就有一些不确定的数值。这些数值在开始是无用的数据。这块内存可能刚才被某个进程使用过,存储了一些用户的账号密码,然后你的这块内存还放着这些数据。但是这些数据对你现在当前的进程是没用的。

使用赋值可以覆盖掉原有的格子中的值。小格子被新的0 1填充,获得一个新的数据。

看到这里你也应该明白了,如果我们申请了长度是64个小格子的内存,也就是可以放置64个0 1的内存,64个小格子是8个字节,可以放置8个ASCII字符,4个JavaScript字符(16位)。

如果我们在小格子里只填充了32个,那么剩下的32个是一些混乱的数据,我们不需要,所以我们需要精确定位要使用的小格子数。

也就是4个字节。我们填充4个字节的数据,然后操作这块缓冲的时候,也只操作4个字节的(读出到另一块内存,或者写入其他文件)。剩下的4个字节就不要去动,那些是混乱的数据。

Buffer 对象就是Node.js对缓冲的一个对象表示,通过提供的函数 API 我们可以操作缓冲。包括申请一块内存做缓冲,填充这块缓冲,操作这块缓冲的数据(里边的小格子)。

如何并发?
多进程 多线程

对于大量占用CPU的程序,如果要给1千个人同时提供CPU使用,最理想的状态就是提供1000个CPU,每个CPU占用一个线程。

不过现实还没有这么多核的服务器。如果我们没钱,那么我们只有4个核,充其量我们可以提供4个CPU服务。也就是同时并行4个线程。同时可以为4个客户提供CPU计算服务。

然而,大部分客户在使用服务的时间中,需要CPU计算的时间比例较少。

当你需要CPU密集的服务时,C语言是最好的选择,过去貌似更多的选择C++,但是现在的流行表示,以C++为代表的面向对象只会把程序搞得臃肿难以扩展。许多人在对C C++的反思后,仍然认为C才是最有价值的。比如版本管理系统Git,简洁有序。比如Redis,快速简洁。

C能做的是,用最简单的代码表达内容,获得最快速的CPU和内存操作。

多进程,为每一个客户启动一个进程提供服务。
多线程,在一个进程内为每一个客户提供一个线程服务。

多进程和多线程的过程是相似的,但是多线程的内存开销要比进程少,并且切换速度快一些,但是多线程编程会变复杂,并且会出现多个线程同时操作同一个数据,从而引入锁的问题。

当CPU密集时,显然需要多线程更好一些,每一个客户连接对应一个线程。因为在这样的系统里,每一个任务都没有等待的机会,所有的内容都一直在不停地运算,直到结束。

数据库就是很好的代表。

—> 连接进入 —> 领取一个线程 —> 计算 —> 返回 —> 收回线程
—> 连接进入 —> 领取一个线程 —> 计算 —> 返回 —> 收回线程
—> 连接进入 —> 领取一个线程 —> 计算 —> 返回 —> 收回线程
…………………………………………………..
理想情况下,进入 1万 个用户,我们希望有 1万 个线程在同时处理任务。显然,事实上硬件还达不到。这就需要一些操作系统排队。而一旦进入排队,后续进入的客户就会进入等待,他们会明显的感受到延迟的存在。

比如进入 1万 个用户,很有可能 10 个在运行计算,另外的在操作系统中排队。

这样的服务需要多核速度很快的 CPU,并且服务吞吐量并不特别高。

IO 多路复用

与操作系统达成协议,同时监测多个文件描述符(读写源头),操作系统提交程序控制权的时候,可以一次提交多个变动的描述符。从而可以一次控制多个源头读写。

最典型的就是Unix提供的 select, poll。

使用 select 模型的时候,工作过程是这样的:

首先打开多个文件描述符
交给操作系统处理数据读写
操作系统发现数据变动后,对这个标识符打上标签,停止阻塞,唤醒主程序
主程序遍历文件描述符,发现有变化的,就对其运行一个小任务
全部运行完任务,再次提供给操作系统
优化的 IO 多路复用 epoll kqueue

当操作系统发出通知后,我们使用一个小缓冲内存,读取数据中的一块,并运行任务,然后交回操作系统。因为处理的数据量很小,所以感觉上去像是没有阻塞。

交给操作系统后,操作系统就可以再次加入新变动的描述符用于下一次的任务。

文件描述符:是数据可以读写的源头表示,比如一个文件描述符,就是代表可以读写的文件。一个套接字描述符,也是可以读写的,网络进入出去的数据使用“套接字描述符”这个术语来表示。

config fds[1, 2, 3, …] // 配置文件描述符,他们关联了数据读写的源头

loop { // 循环运行
change_fds = epoll wait fds // 交给操作系统,并等待(睡眠)
forEach change_fds { // 当操作系统通知时,会把变动的描述符放入change_fds
if fd === socket in
read socket
if fd === file a
write file a
if fd === socket out
write socket

}
}
首先打开多个文件描述符,

交给操作系统处理数据读写

操作系统发现数据变动后,把变动的标识符放入一个变动描述符队列,停止阻塞,唤醒主程序

主程序遍历变动的文件描述符,对其运行一个小任务

全部运行完任务,再次提供给操作系统

我看不明白!
Apache 在以往的服务中提供多线程服务器模型,Nginx 提供IO多路复用的模型。

流行的数据库,像 Mysql,采用多线程模型,因为 ta 面对的是密集的数据操作。而应用服务器面对的是套接字的读取写入,等待,很多时候,客户都是没有数据可以收发的。

每个平台实现了不同的 IO 多路复用,Linux 采用 epoll,BSD 采用 kqueue,还有的没有采用,停留在多线程。libev是libevent的新版本,采用了统一封装,针对不同平台使用不同的IO吞吐。而在上层的编码中,采用统一的函数库。

Node.js,底层是 C 编写的 libev 框架,libev 在 Linux,BSD Unix上分别是用 epoll kqueue 多路复用模型,这在编程的抽象层常被叫做事件驱动。事实上,ta 是多路复用,同时监测多个文件描述符,采用非阻塞读写,从而在单线程进行并发。

非阻塞?IO…
所谓阻塞,是进程会进入睡眠,从而不再提供服务,直到读写的数据已经被放到内核缓冲区,操作系统内核会再次唤醒进程。

普通文件,也就是操作系统磁盘的文件,一般没有读和写的阻塞,一旦通过open打开后,会立刻有内存的映射。你可以读这个文件到内存,然后再写入别的地方。这些操作不需要等待数据准备,操作是直接运行的,时间花费在磁盘寻址和内存复制,没有数据准备的等待。

网络套接字数据被认为要到来时,会有一些等待期,被认为是阻塞。比如网络的数据要一条一条传过来,期间要经过漫长的光纤。

套接字是怎么利用多路复用无阻塞读写的?
首先套接字是个读写双工模式的。套接字的数据来源于网络,并从网络发布出去。因为此,每一阶段从网络来的数据量非常小。可以比作一个水龙头,虽然水(数据)确实一直不停地从水龙头中涌出,但是每一点的水量都是非常小的,CPU 内存处理这点流量几乎不费吹灰之力。

所以,程序可以不停地检测到数据流入,并且挤满内核缓冲区,然后wait完毕,操作系统内核通知进程(这个时间非常的短,CPU是很快的),进程读走内核缓冲区的内容,并返还给操作系统控制权。操作系统再次把内核缓冲区写满,然后通知进程,…,如此,周而复始,直到没有数据变动了,操作系统就一直wait,进程则一直睡眠,直到有新的数据变化。

每个循环阶段,每个读写占用的时间和读写的数据量都是小块的,几乎可以看做瞬时。完成后立刻交还给操作系统控制权,等待操作系统内核下一次的通知。

一个大文件如何在写入读取时不造成其他客户等待?
一个大型文件,可以使用一个游标记录每次读写的位置,每次只读写一小块,然后记录游标,停止读写,并返回到循环,进入等待。当操作系统下一次发出通知时,读写游标后面的一小块,并如此重复,直到完全读写。这样可以在最小的时间返还操作系统的控制权,以此达到无阻塞。

我如何搭建我的超级服务器?
你需要运行一个CPU极度密集的业务,并把ta投入到互联网上提供服务?

你需要一个服务器用来运行你的 CPU 密集型的业务,这台服务器是用 C 编写的,提供了高性能 CPU 和大量的内存用来提供可靠的快速的服务。

这个服务提供 TCP UDP 级别的服务,也就是说通过套接字与其他进程通信。

另外,这个服务应该使用多线程,对每一个进入的请求提供一个线程,并使用线程池提升反应质量。并要有一个良好的请求队列控制程序,以免请求过度,导致服务器崩溃。

这些服务可以是自己编写的,也可以是第三方提供的,比如 Mysql, Orcale, … 也可以是复杂的散点计算,或者大数据分析,… 他们通过套接字与下面的IO服务通信。

你需要一个服务器用来运行你的 IO 密集型的业务,一旦你的服务是面向网络,那么就意味着你需要验证成千上万的客户,这些业务内容不需要大量的 CPU 计算,就算是循环也可能只在100次量级之内。这时候应该使用编写更快速,更容易管理的语言,比如 Node.js,Python,Ruby。

这样的语言编写出来的程序,更容易扩展,和与他人合作。因为是IO服务,所以不存在计算的性能问题,存在的差别则是IO吞吐上。

基于 IO 服务器的演化,大致经历了多进程 -> 多线程 -> select poll多路复用 -> epoll kqueue 多路复用。现在最快速的 IO 服务器是采用 epoll kqueue 多路复用,比如 Nginx。Node.js 本身就是 epoll 的,其核心是基于 epoll 的 libev 事件驱动库。

如果你打算使用 Python,Ruby,选用他们的 epoll kqueue 事件驱动库,要比多线程库快速并稳定的多。

说明:epoll 只能在Linux服务器使用,在 BSD Unix 则是采用了kqueue,与 epoll 达到相似的目的。libev 在底层对多个平台进行了统一封装。

另外,基于事件驱动的服务,也更容易水平扩展,搭建集群可以将服务器拓展至几十个。

福利:甚至对于拓展的管理程序,Nginx 就可以提供现成的服务。

把你的 IO 密集服务器和你的 CPU 密集服务器,通过内部局域网进行连接通信(套接字通信),这样你就提供了一个 CPU 极度密集的互联网服务。

正态分布的前生今世

神说,要有正态分布,就有了正态分布。
神看正态分布是好的,就让随机误差就服从了正态分布。

创世纪-数理统计

一、正态分布,熟悉的陌生人

学过基础统计学的同学大都对正态分布非常熟悉。这个钟型的分布曲线不但形状优雅,其密度函数写成数学表达式

12π‾‾‾√σexp(−(x−μ)22σ2)12πσexp(−(x−μ)22σ2)
也非常具有数学的美感。其标准化后的概率密度函数
12π‾‾‾√exp(−x22)12πexp(−x22)
更加的简洁漂亮,两个最重要的数学常量π,eπ,e都出现在了公式之中。在我个人的审美之中,它也属于top-N的最美丽的数学公式之一,如果有人问我数理统计领域哪个公式最能让人感觉到上帝的存在,那我一定投正态分布的票。因为这个分布戴着神秘的面纱,在自然界中无处不在,让你在纷繁芜杂的数据背后看到隐隐的秩序。

【正态分布曲线】
正态分布又通常被称为高斯分布,在科学领域,冠名权那是一个很高的荣誉。早年去过德国的兄弟们还会发现,德国的钢镚和10马克的纸币上都留有高斯的头像和正态密度曲线。正态分布被冠名高斯分布,我们也容易认为是高斯发现了正态分布,其实不然,不过高斯对于正态分布的历史地位的确立是起到了决定性的作用。

【德国马克上的高斯头像和正态分布曲线】

正态曲线虽然看上去很美,却不是一拍脑袋就能想到的。我们在本科学习数理统计的时候,课本一上来介绍正态分布就给出密度分布函数,却从来不说明这个分布函数是通过什么原理推导出来的。所以我一直搞不明白数学家当年是怎么找到这个概率分布曲线的,又是怎么发现随机误差服从这个奇妙的分布的。我们在实践中大量的使用正态分布,却对这个分布的来龙去脉知之甚少,正态分布真是让人感觉既熟悉又陌生。直到我读研究生的时候,我的导师给我介绍了陈希儒院士的《数理统计学简史》这本书,看了之后才了解了正态分布曲线从发现到被人们重视进而广泛应用,也是经过了几百年的历史。

正态分布的这段历史是很精彩的,我们通过讲一系列的故事来揭开她的神秘面纱。

二、邂逅,正态曲线的首次发现

第一个故事和概率论的发展密切相关,主角是棣莫弗(De Moivre)和拉普拉斯(Laplace)。拉普拉斯是个大科学家,被称为法国的牛顿;棣莫弗名气可能不算很大,不过大家应该都熟悉这个名字,因为我们在高中数学学复数的时候我们都学过棣莫弗定理

(cosθ+isinθ)n=cos(nθ)+isin(nθ)(cos⁡θ+isin⁡θ)n=cos⁡(nθ)+isin⁡(nθ).
古典概率论发源于赌博,惠更斯、帕斯卡、费马、贝努利都是古典概率的奠基人,他们那会研究的概率问题大都来自赌桌上,最早的概率论问题是赌徒梅累在1654年向帕斯卡提出的如何分赌金的问题。统计学中的总体均值之所以被称为期望(Expectation),就是源自惠更斯、帕斯卡这些人研究平均情况下一个赌徒在赌桌上可以期望自己赢得多少钱。

棣莫弗(De Moivre) 拉普拉斯 (Laplace)
有一天一个哥们,也许是个赌徒,向棣莫弗提了一个和赌博相关的一个问题:A,B两人在赌场里赌博,A,B各自的获胜概率是pp和q=1−pq=1−p,赌nn局,若A赢的局数X>npX>np,则A付给赌场X−npX−np元,否则B付给赌场np−Xnp−X元。问赌场挣钱的期望值是多少?

问题并不复杂,本质上是一个二项分布,最后求出的理论结果是

2npqb(n,p,np),2npqb(n,p,np),
其中

b(n,p,i)=(ni)piqn−ib(n,p,i)=(ni)piqn−i
是常见的二项概率。但是对具体的nn,要把这个理论结果实际计算出数值结果可不是件容易的事,因为其中的二项公式中有组合数.这就驱动棣莫弗寻找近似计算的方法。

与此相关联的另一个问题,是遵从二项分布的随机变量X∼B(n,p)X∼B(n,p),问XX落在二项分布中心点一定范围的概率Pd=P(|X−np|≤d)Pd=P(|X−np|≤d)是多少?

对于p=12p=12的情形,棣莫弗做了一些计算并得到了一些近似结果,但是还不够漂亮,幸运的是棣莫弗和斯特林(Stirling)处在同一个时代,而且二人之间有联系,斯特林公式是在数学分析中必学的一个重要公式:(事实上斯特林公式的形式其实是棣莫弗最先发现的,但是斯特林改进了这个公式,改进的结果为棣莫弗所用)

n!∼2πn‾‾‾‾√(ne)nn!∼2πn(ne)n
1733年,棣莫弗很快利用斯特林公式进行计算并取得了重要的进展。考虑nn是偶数的情形,令二项概率

b(i)=b(n,12,i)=(ni)(12)nb(i)=b(n,12,i)=(ni)(12)n
通过斯特林公式做一些简单的计算容易得到,

b(n2)∼2πn‾‾‾√ ⇒ b(n2+d)b(n2)∼exp(−2d2n).b(n2)∼2πn ⇒ b(n2+d)b(n2)∼exp(−2d2n).
于是有

b(n2+d)∼22πn‾‾‾‾√exp(−2d2n).b(n2+d)∼22πnexp(−2d2n).
使用上式的结果,并在二项概率累加求和的过程中近似的使用定积分代替求和,很容易就能得到

P(∣∣∣Xn−12∣∣∣≤cn‾√)= ∼ = ∼ ∑−cn√≤i≤cn√b(n2+i)∑−cn√≤i≤cn√22πn‾‾‾‾√exp(−2i2n) (1)∑−2c≤2in√≤2c12π‾‾‾√exp(−12(2in‾√)2)2n‾√∫2c−2c12π‾‾‾√exp(−x22)dx.
P(|Xn−12|≤cn)=∑−cn≤i≤cnb(n2+i) ∼ ∑−cn≤i≤cn22πnexp(−2i2n) (1) = ∑−2c≤2in≤2c12πexp(−12(2in)2)2n ∼ ∫−2c2c12πexp(−x22)dx.

看,正态分布的密度函数的形式在积分公式中出现了!这也就是我们在数理统计课本上学到的二项分布的极限分布是正态分布。

以上只是讨论了p=12p=12的情形,棣莫弗也对p≠12p≠12做了一些计算,后来拉普拉斯对p≠12p≠12的情况做了更多的分析,并把二项分布的正态近似推广到了任意pp的情况。这是第一次正态密度函数被数学家勾画出来,而且是以二项分布的极限分布的形式被推导出来的。熟悉基础概率统计的同学们都知道这个结果其实叫棣莫弗-拉普拉斯中心极限定理。

[De Moivre-Laplace中心极限定理] 设随机变量Xn(n=1,2…)Xn(n=1,2…)服从参数为pp的二项分布,则对任意的xx,恒有

limn→∞P{Xn−Xpnp(1−p)‾‾‾‾‾‾‾‾‾√≤x}=∫x−∞12π‾‾‾√exp(−t22)dt.limn→∞P{Xn−Xpnp(1−p)≤x}=∫−∞x12πexp(−t22)dt.
我们在大学学习数理统计的时候,学习的过程都是先学习正态分布,然后才学习中心极限定理。而学习到正态分布的时候,直接就描述了其概率密度的数学形式,虽然数学上很漂亮,但是容易困惑数学家们是如何凭空就找到这个分布的。读了陈希孺的《数理统计学简史》之后,我才明白正态分布的密度形式首次发现是在棣莫弗-拉普拉斯的中心极限定理中。数学家研究数学问题的进程很少是按照我们数学课本的安排顺序推进的,现代的数学课本都是按照数学内在的逻辑进行组织编排的,虽然逻辑结构上严谨优美,却把数学问题研究的历史痕迹抹得一干二净。DNA双螺旋结构的发现者之一James Waston在他的名著《DNA双螺旋》序言中说:“科学的发现很少会像门外汉所想象的一样,按照直接了当合乎逻辑的方式进行的。”

棣莫弗给出他的发现后40年(大约是1770),拉普拉斯建立了中心极限定理较一般的形式,中心极限定理随后又被其它数学家们推广到了其它任意分布的情形,而不限于二项分布。后续的统计学家发现,一系列的重要统计量,在样本量N趋于无穷的时候,其极限分布都有正态的形式,这构成了数理统计学中大样本理论的基础。

棣莫弗在二项分布的计算中瞥见了正态曲线的模样,不过他并没有能展现这个曲线的美妙之处。棣莫弗的这个工作当时并没有引起人们足够的重视,原因在于棣莫弗不是个统计学家,从未从统计学的角度去考虑其工作的意义。正态分布(当时也没有被命名为正态分布)在当时也只是以极限分布的形式出现,并没有在统计学,尤其是误差分析中发挥作用。这也就是正态分布最终没有被冠名棣莫弗分布的重要原因。那高斯做了啥了不起的工作导致统计学家把正态分布的这顶桂冠戴在了他的头上呢?这先得从最小二乘法的发展说起。

三、最小二乘法,数据分析的瑞士军刀

第二个故事的主角是欧拉(Euler),拉普拉斯(Lapalace),勒让德(Legendre)和高斯(Gauss),故事发生的时间是十八世纪中到十九世纪初。十七、十八世纪是科学发展的黄金年代,微积分的发展和牛顿万有引力定律的建立,直接的推动了天文学和测地学的迅猛发展。当时的大科学家们都在考虑许多天文学上的问题。几个典型的问题如下:

土星和木星是太阳系中的大行星,由于相互吸引对各自的运动轨道产生了影响,许多大数学家,包括欧拉和拉普拉斯都在基于长期积累的天文观测数据计算土星和木星的运行轨道。
勒让德承担了一个政府给的重要任务,测量通过巴黎的子午线的长度。
海上航行经纬度的定位。主要是通过对恒星和月面上的一些定点的观测来确定经纬度。
这些天文学和测地学的问题,无不涉及到数据的多次测量、分析与计算;十七、十八世纪的天文观测,也积累了大量的数据需要进行分析和计算。很多年以前,学者们就已经经验性的认为,对于有误差的测量数据,多次测量取平均是比较好的处理方法。虽然缺乏理论上的论证,也不断的受到一些人的质疑,取平均作为一种异常直观的方式,已经被使用了千百年,在多年积累的数据的处理经验中也得到相当程度的验证,被认为是一种良好的数据处理方法。

【勒让德(Legendre)】

以上涉及的问题,我们直接关心的目标量往往无法直接观测,但是一些相关的量是可以观测到的,而通过建立数学模型,最终可以解出我们关心的量。这些问题都可以用如下数学模型描述:我们想估计的量是β0,⋯,βpβ0,⋯,βp,另有若干个可以测量的量x1,⋯,xp,yx1,⋯,xp,y,这些量之间有线性关系

y=β0+β1x1+⋯+βpxpy=β0+β1x1+⋯+βpxp
如何通过多组观测数据求解出参数β0,⋯,βpβ0,⋯,βp呢?欧拉和拉普拉斯采用的都是求解线性方程组的方法。

⎧⎩⎨⎪⎪⎪⎪y1=β0+β1x11+β2x21+⋯+βpxp1y2=β0+β1x12+β2x22+⋯+βpxp2 (2) ⋮
yn=β0+β1x1n+β2x2n+⋯+βpxpn{y1=β0+β1x11+β2x21+⋯+βpxp1y2=β0+β1x12+β2x22+⋯+βpxp2 (2) ⋮
yn=β0+β1x1n+β2x2n+⋯+βpxpn
但是面临的一个问题是,有nn组观测数据,p+1p+1个变量,如果n>p+1n>p+1,则得到的线性矛盾方程组,无法直接求解。所以欧拉和拉普拉斯采用的方法都是通过一定的对数据的观察,把nn个线性方程分为p+1p+1组,然后把每个组内的方程线性求和后归并为一个方程,从而就把nn个方程的方程组化为p+1p+1个方程的方程组,进一步解方程求解参数。这些方法初看有一些道理,但是都过于经验化,无法形成统一处理这一类问题的一个通用解决框架。
以上求解线性矛盾方程的问题在现在的本科生看来都不困难,就是统计学中的线性回归问题,直接用最小二乘法就解决了,可是即便如欧拉、拉普拉斯这些数学大牛,当时也未能对这些问题提出有效的解决方案。可见在科学研究中,要想在观念上有所突破并不容易。有效的最小二乘法是勒让德在1805年发表的,基本思想就是认为测量中有误差,所以所有方程的累积误差为

累积误差 =∑∑( 观测值 - 理论值 )22
我们求解出导致累积误差最小的参数即可。
β̂ ==argminβ∑i=1ne2iargminβ∑i=1n[yi−(β0+β1x1i+⋯+βpxpi)]2 (3)β^=argminβ∑i=1nei2=argminβ∑i=1n[yi−(β0+β1x1i+⋯+βpxpi)]2 (3)
勒让德在论文中对最小二乘法的优良性做了几点说明:
最小二乘使得误差平方和最小,并在各个方程的误差之间建立了一种平衡,从而防止某一个极端误差取得支配地位
计算中只要求偏导后求解线性方程组,计算过程明确便捷
最小二乘可以导出算术平均值作为估计值
对于最后一点,推理如下:假设真值为θ,x1,⋯,xnθ,x1,⋯,xn为n次测量值,每次测量的误差为ei=xi−θei=xi−θ,按最小二乘法,误差累积为

L(θ)=∑i=1ne2i=∑i=1n(xi−θ)2L(θ)=∑i=1nei2=∑i=1n(xi−θ)2
求解θθ使得L(θ)L(θ)达到最小,正好是算术平均
x¯=1n∑i=1nxix¯=1n∑i=1nxi.
由于算术平均是一个历经考验的方法,而以上的推理说明,算术平均是最小二乘的一个特例,所以从另一个角度说明了最小二乘方法的优良性,使我们对最小二乘法更加有信心。
最小二乘法发表之后很快得到了大家的认可接受,并迅速的在数据分析实践中被广泛使用。不过历史上又有人把最小二乘法的发明归功于高斯,这又是怎么一回事呢。高斯在1809年也发表了最小二乘法,并且声称自己已经使用这个方法多年。高斯发明了小行星定位的数学方法,并在数据分析中使用最小二乘方法进行计算,准确地预测了谷神星的位置。

扯了半天最小二乘法,没看出和正态分布有任何关系啊,离题了吧?单就最小二乘法本身,虽然很实用,不过看上去更多的算是一个代数方法,虽然可以推导出最优解,对于解的误差有多大,无法给出有效的分析,而这个就是正态分布粉墨登场发挥作用的地方。勒让德提出的最小二乘法,确实是一把在数据分析领域披荆斩棘的好刀,但是刀刃还是不够锋利;而这把刀的打造后来至少一半功劳被归到高斯,是因为高斯不但独自地给出了造刀的方法,而且把最小二乘这把刀的刀刃造得无比锋利,把最小二乘打造为了一把瑞士军刀。

高斯拓展了最小二乘法,把正态分布和最小二乘法联系在一起,并使得正态分布在统计误差分析中确立了自己的定位,否则正态分布就不会被称为高斯分布了。那高斯这位神人是如何把正态分布引入到误差分析之中,打造最小二乘这把瑞士军刀的呢?

四、众里寻她千百度,误差分布曲线的确立

第三个故事有点长,主角是高斯和拉普拉斯,故事的主要内容寻找随机误差分布的规律。

天文学是第一个被测量误差困扰的学科,从古代至十八世纪天文学一直是应用数学最发达的领域,到十八世纪,天文学的发展积累了大量的天文学数据需要分析计算,应该如何来处理数据中的观测误差成为一个很棘手的问题。我们在数据处理中经常使用平均的常识性法则,千百年来的数据使用经验说明算术平均能够消除误差,提高精度。平均有如此的魅力,道理何在,之前没有人做过理论上的证明。算术平均的合理性问题在天文学的数据分析工作中被提出来讨论:测量中的随机误差服应该服从怎样的概率分布?算术平均的优良性和误差的分布有怎样的密切联系?

伽利略在他著名的《关于两个主要世界系统的对话》中,对误差的分布做过一些定性的描述,主要包括:

误差是对称分布的;
大的误差出现频率低,小的误差出现频率高。
用数学的语言描述,也就是说误差分布函数f(x)f(x)关于0对称分布,概率密度随|x||x|增加而减小,这两个定性的描述都很符合常识。

许多天文学家和数学家开始了寻找误差分布曲线的尝试。托马斯•辛普森(Thomas Simpson,1710-1761)先走出了有意义的一步。设真值为θθ,而x1,⋯,xnx1,⋯,xn为nn次测量值,每次测量的误差为ei=xi−θei=xi−θ,若用算术平均x¯=(∑ni=1xi)/nx¯=(∑i=1nxi)/n去估计θθ,其误差为e¯=(∑ni=1ei)/ne¯=(∑i=1nei)/n。Simpson证明了,对于如下的一个概率分布,

【Simpson的误差态分布曲线】
有下面的估计:

P(|e¯|

推荐

提取三个特征(显示[评价]和隐示[浏览和点击])商品很多稀疏转为稠密。


分布式大数据高并发的web开发框架

一、引言
通常我们认为静态网页html的网站速度是最快的,但是自从有了动态网页之后,很多交互数据都从数据库查询而来,数据也是经常变化的,除了一些新闻资讯类的网站,使用html静态化来提高访问速度是不太现实的方案。我们不得不在代码与数据库之间寻求一种更合适的解决方案。
减少数据库访问次数,文件和数据库分离,大数据分布式存储,服务器的集群负载均衡,页面缓存的使用,nosql内存数据库代替关系型数据库,这些方案措施都是提高系统高并发性能的关键,下面一一分解。
二、分解
(1) 分布式服务器集群
A) 文件服务器集群
图片、视频、其他下载文件,它们的下载通常是占用网络带宽的罪恶魁首,这些资源一定要独立放在带宽好的文件服务器上,能提供http协议访问地址使用,不至于在下载文件时影响web服务器的cpu运算。
文件服务器最好使用磁盘阵列中心存储,比如阿里云提供的文件云服务器,这样使用简单,使用多大带宽多大存储空间选择就好了。
如果没有中心存储,也可以做文件服务器集群,如下图
分布式大数据高并发的web开发框架

说白了就是每个文件服务器都安装一个简单的web api作为文件传输和访问的接口,可以手动分配服务器地址给 web 程序使用,当然也可以做一层简单的负载均衡器供web程序统一接口调用。
需要注意的是:web api 接口上传文件结果一定要返回特定服务器完整的http文件下载地址,这个地址要存入数据库。
文件服务器的数量:文件服务器之间都相对独立,没有数据关联,因此数量的多少主要是看带宽的承载量和硬盘空间的大小,动态扩展服务器后,只需把服务器地址加入 web程序的调用列表即可达到无限扩展容量的机制。

B) Web服务器集群
不管是windows系统,还是linux系统,单台服务器的性能和资源都是有限的,支持的连接并发数都有上限,因此必须采用多服务器集群的方法才能提高连接并发数。连接并发数的容量计算也很容易:
连接并发数= 服务器1并发数+服务器2并发数+……+ 服务器n并发数
当然,我们不能都给每台web服务器分配一个域名地址访问,肯定是同一个域名同一个入口,例如百度后面有成百上千台web服务器,但是我们都是使用 www.baidu.com 一个入口,至于这个入口会自动给我们分配一台web服务器访问,我们不会在意这台web服务器的具体地址是多少,这就是负载均衡器的作用。
分布式大数据高并发的web开发框架

但是,web服务器集群并不像文件服务器集群那么简单,因为web服务器的程序使用的数据是共享的,具有一致性和实时性的要求。
如果获取数据时都从数据库读取数据,可以保证数据的一致性,但数据量大时,影响性能而又不能保证实时性,这就是我们后面说到的使用缓存技术解决这个问题。
Web服务器类型:微软的.net iis ,阿帕奇 tomcat 都是出名的轻量级web服务器

C) 数据库服务器集群
通常大型的关系型数据库 oracle ,mysql , sqlserver 都已经提供了集群的功能,但是,由于磁盘读写速度的限制,难以胜任高并发实时性的要求,我们建议使用带有集群功能的内存数据库。
Mongodb内存数据库:
Mongodb是一个开源的nosql 内存数据库,基于json格式的文档存储,数据对象通过json的序列化与反序列化传输。在服务器内存足够大时,把所有的数据装载到内存中,便于快速访问;当内存不够大时,把不常使用的数据持久化到磁盘文件;最新版本的mongodb 3.0 已经提供了服务器集群的功能,并且引入了hadoop大数据的mapreduce并行运算模式,这让海量数据全部存储与多台服务器的内存上提供很好的解决方案。
现在服务器内存的硬件价格已经十分低廉,配置128G内存的服务器已经很普遍,但是我们还是建议一台服务器存储数据量不要超过1亿条记录,便于提高并行计算的效率。
分布式大数据高并发的web开发框架

在硬件充裕的情况下,mongodb集群建议采用多副节点的集群模式,不仅提高访问性能,也保证了数据安全完整性能。

Gemfire内存数据库
Gemfire是很多年前就已经商业化了的nosql内存数据库,经过了多家大型机构的使用考验,更幸运的是,在2015年4月发布其开源版本Geode,可能在不久的将来得到更广泛的使用。
除了开源nosql内存数据库,国内外的巨头都已经在研究自己的内存数据库,比如oracle, ibm , 阿里等等,nosql内存数据库大有代替关系型数据库的趋势。

朋友们可以去研究每个内存数据库的优劣势,不管使用那种产品,我们无非就是解决两个问题:1、内存数据库保证快速访问;2、服务器集群存储大数据。至于如何搭建环境大家可以查找相关资料。

(2) 负载均衡器
负载均衡服务器分为硬件均衡服务器和软件均衡服务器,目的就是统一提供一个访问入口来访问服务器集群,并且能够动态监控每个服务器的负载,把新的用户请求转给负载小的服务器。
硬件服务器就是直接购买独立的服务器作为负载均衡服务器,例如阿里云已经提供。
软件服务器使用具有代理功能的软件作为转发服务器,比如Nginx,HAProxy,LVS 等等,具体安装部署大家可以搜索相关资料。

(3) 缓存技术
从上面我们可以看出,web服务器与数据库服务器是分布在不同的服务器上的,也就是web程序从数据库获取数据时,通过网络tcp/ip协议进行网络数据传输,当查询的数据量过大时,网络带宽速率很可能变为瓶颈,高并发在线时,将会很大影响整体系统的效率。
我们使用缓存技术解决这个问题。这里就涉及到相关业务了,一般来说,我们根据具体的业务,把数据分为两类:
1、 一类是不经常变的数据,比如表结构,公共设置数据,省市县名录等等,这些数据我们将会作为永久缓存数据,我们将会一次性读取数据库全部记录缓存到web服务器本地长久保存,并且定时检测数据库是否有更新的数据,如果有更新,则再更新本地缓存数据;
2、 另一类是经常变的数据,对于这种数据,需要每次读取时都从数据库读取,往往这类数据也包含海量的数据,读取数据库时将不能一次性读取,只能分页读取,缓存数据保存的时间也不宜太长。

缓存的工具也很多,比如 .net里自带的MemoryCache,java 的有 ehcache ,都是比较出名的缓存。

DMLC深盟分布式深度机器学习开源平台解析

本段转载!由DMLC分布式深度机器学习开源项目(中文名深盟)开发者联合撰写,介绍DMLC已有的xgboost、cxxnet、Minerva、Parameter Server和Rabit等组件主要解决的问题、实现方式及其性能表现,并简要说明项目的近期规划。
【编者按】算法速度、系统性能以及易用性的瓶颈,制约着目前机器学习的普及应用,DMLC分布式深度机器学习开源项目(中文名深盟)的诞生,正是要降低分布式机器学习的门槛。本文由深盟项目开发者联合撰写,将深入介绍深盟项目当前已有的xgboost、cxxnet、Minerva、Parameter Server和Rabit等组件主要解决的问题、实现方式及其性能表现,并简要说明项目的近期规划。文章将被收录到《程序员》电子刊(2015.06A)人工智能实践专题,以下为全文内容:

机器学习能从数据中学习。通常数据越多,能学习到的模型就越好。在数据获得越来越便利的今天,机器学习应用无论在广度上还是在深度上都有了显著进步。虽然近年来计算能力得到了大幅提高,但它仍然远远不及数据的增长和机器学习模型的复杂化。因此,机器学习算法速度和系统性能是目前工业界和学术界共同关心的热点。

高性能和易用性的开源系统能对机器学习应用的其极大的推动作用。但我们发现目前兼具这两个特点的开源系统并不多,而且分散在各处。因此我们联合数个已有且被广泛使用的C++分布式机器学习系统的开发者,希望通过一个统一的组织来推动开源项目。我们为这个项目取名DMLC: Deep Machine Learning in Common,也可以认为是Distributed Machine Learning in C++。中文名为深盟。代码将统一发布在 https://github.com/dmlc

这个项目将来自工业界和学术界的几组开发人员拉到了一起,希望能提供更优质和更容易使用的分布式机器学习系统,同时也希望吸引更多的开发者参与进来。本文将介绍深盟项目目前已有的几个部件,并简要说明项目的近期规划。

xgboost: 速度快效果好的Boosting模型
在数据建模中,当我们有数个连续值特征时,Boosting分类器是最常用的非线性分类器。它将成百上千个分类准确率较低的树模型组合起来,成为一个准确率很高的模型。这个模型会不断地迭代,每次迭代就生成一颗新的树。然而,在数据集较大较复杂的时候,我们可能需要几千次迭代运算,这将造成巨大的计算瓶颈。

xgboost正是为了解决这个瓶颈而提出。单机它采用多线程来加速树的构建,并依赖深盟的另一个部件rabbit来进行分布式计算。为了方便使用,xgboost提供了 Python和R语言接口。例如在R中进行完整的训练和测试:

require(xgboost)
data(agaricus.train, package=’xgboost’)
data(agaricus.test, package=’xgboost’)
train<- agaricus.train
test<- agaricus.test
bst<- xgboost(data = traindata,label=trainlabel, max.depth = 2, eta = 1, nround = 100, objective = “binary:logistic”)
pred<- predict(bst, test$data)
由于其高效的C++实现,xgboost在性能上超过了最常用使用的R包gbm和Python包sklearn。例如在Kaggle的希格斯子竞赛数据上,单线程xgboost比其他两个包均要快出50%,在多线程上xgboost更是有接近线性的性能提升。由于其性能和使用便利性,xgboost已经在Kaggle竞赛中被广泛使用,并已经有队伍成功借助其拿到了第一名,如图1所示。

图1 xgboost和另外两个常用包的性能对比

CXXNET:极致的C++深度学习库
cxxnet是一个并行的深度神经网络计算库,它继承了xgboost的简洁和极速的基因,并开始被越来越多人使用。例如Happy Lantern Festival团队借助Cxxnet在近期的Kaggle数据科学竞赛中获得了第二名。在技术上,cxxnet有如下两个亮点。

灵活的公式支持和极致的C++模板编程
追求速度极致的开发者通常使用C++来实现深度神经网络。但往往需要给每个神经网络的层和更新公式编写独立的CUDA kernel。很多以C++为核心的代码之所以没有向matlab/numpy那样支持非常灵活的张量计算,是因为因为运算符重载和临时空间的分配会带来效率的降低。

然而,cxxnet利用深盟的mshadow提供了类似matlab/numpy的编程体验,但同时保留了C++性能的高效性。其背后的核心思想是expression template,它通过模板编程技术将开发者写的公式自动展开成优化过的代码,避免重载操作符等带来的额外数据拷贝和系统消耗。另外,mshadow通过模板使得非常方便的讲代码切换到CPU还是GPU运行。

通用的分布式解决方案
在分布式深度神经网络中,我们既要处理一台机器多GPU卡,和多台机器多GPU卡的情况。然而后者的延迟和带宽远差于前者,因此需要对这种两个情形做不同的技术考虑。cxxnet采用mshadow-ps这样一个统一的参数共享接口,并利用接下来将要介绍Parameter Server实现了一个异步的通讯接口。其通过单机多卡和多机多卡采用不同的数据一致性模型来达到算法速度和系统性能的最佳平衡。

我们在单机4块GTX 980显卡的环境下测试了流行的图片物体识别数据集ImageNet和神经网络配置AlexNet。在单卡上,cxxnet能够处理244张图片每秒,而在4卡上可以提供3.7倍的加速。性能超过另一个流行深度学习计算库Caffe (均使用CUDA 6.5,未使用cuDNN加速)。

在多机情况下,我们使用Amazon EC2的GPU实例来测试性能。由于优秀的异步通信,cxxnet打满了机器的物理带宽,并提供了几乎是线性的加速比,如图2所示。

图2 cxxnet在Amazon EC2上的加速比

cxxnet的另外一些特性:

轻量而齐全的框架:推荐环境下仅需要CUDA、OpenCV、MKL或BLAS即可编译。
cuDNN支持:Nvidia原生卷积支持,可加速计算30%。
及时更新的最新技术:及时跟进学术界的动态,例如现在已经支持MSRA的ParametricRelu和Google的Batch Normalization。
Caffe模型转换:支持将训练好的Caffe模型直接转化为cxxnet模型。
Minerva: 高效灵活的并行深度学习引擎
不同于cxxnet追求极致速度和易用性,Minerva则提供了一个高效灵活的平台让开发者快速实现一个高度定制化的深度神经网络。

Minerva在系统设计上使用分层的设计原则,将“算的快”这一对于系统底层的需求和“好用”这一对于系统接口的需求隔离开来,如图3所示。在接口上,我们提供类似numpy的用户接口,力图做到友好并且能充分利用Python和numpy社区已有的算法库。在底层上,我们采用数据流(Dataflow)计算引擎。其天然的并行性能够高效地同时地利用多GPU进行计算。Minerva通过惰性求值(Lazy Evaluation),将类numpy接口和数据流引擎结合起来,使得Minerva能够既“好用”又“算得快”。

图 3 Minerva的分层设计

惰性求值
Minerva通过自己实现的ndarray类型来支持常用的矩阵和多维向量操作。在命名和参数格式上都尽量和numpy保持一致。Minerva同时支持读取Caffe的配置文件并进行完整的训练。Minerva提供了两个函数与numpy进行对接。from_numpy函数和to_numpy函数能够在numpy的ndarray与Minerva的类型之间互相转换。因此,将Minerva和numpy混合使用将变得非常方便。

数据流引擎和多GPU计算
从Mapreduce到Spark到Naiad,数据流引擎一直是分布式系统领域研究的热点。数据流引擎的特点是记录任务和任务之间的依赖关系,然后根据依赖关系对任务进行调度。没有依赖的任务则可以并行执行,因此数据流引擎具有天然的并行性。在Minerva中,我们利用数据流的思想将深度学习算法分布到多GPU上进行计算。每一个ndarray运算在Minerva中就是一个任务,Minerva自身的调度器会根据依赖关系进行执行。用户可以指定每个任务在哪块卡上计算。因此如果两个任务之间没有依赖并且被分配到不同GPU上,那这两个任务将能够并行执行。同时,由于数据流调度是完全异步的,多卡间的数据通信也可以和其他任务并行执行。由于这样的设计,Minerva在多卡上能够做到接近线性加速比。此外,利用深盟的Parameter Server,Minerva可以轻松将数据流拓展到多机上,从而实现多卡多机的分布式训练。

图4 Minerva和Caffe在单卡和多卡上训练GoogLeNet的比较

表1 Minerva在不同网络模型和不同GPU数目上的训练速度

数据流引擎和多GPU计算
Minerva采用惰性求值的方式将类numpy接口和数据流引擎结合起来。每次用户调用Minerva的ndarray运算,系统并不立即执行这一运算,而是将这一运算作为任务,异步地交给底层数据流调度器进行调度。之后,用户的线程将继续进行执行,并不会阻塞。这一做法带来了许多好处:

在数据规模较大的机器学习任务中,文件I/O总是比较繁重的。而惰性求值使得用户线程进行I/O的同时,系统底层能同时进行计算。
由于用户线程非常轻量,因此能将更多的任务交给系统底层。其中相互没有依赖的任务则能并行运算。
用户能够在接口上非常轻松地指定每个GPU上的计算任务。Minerva提供了set_device接口,其作用是在下一次set_device调用前的运算都将会在指定的GPU上进行执行。由于所有的运算都是惰性求值的,因此两次set_device后的运算可以几乎同时进行调度,从而达到多卡的并行。
Parameter Server: 一小时训练600T数据
深盟的组件参数服务器(Parameter Server)对前述的应用提供分布式的系统支持。在大规模机器学习应用里,训练数据和模型参数均可大到单台机器无法处理。参数服务器的概念正是为解决此类问题而提出的。如图5所示,参数以分布式形式存储在一组服务节点中,训练数据则被划分到不同的计算节点上。这两组节点之间数据通信可归纳为发送(push)和获取(pull)两种。例如,一个计算节点既可以把自己计算得到的结果发送到所有服务节点上,也可以从服务节点上获取新模型参数。在实际部署时,通常有多组计算节点执行不同的任务,甚至是更新同样一组模型参数。

图5 参数服务器架构

在技术上,参数服务器主要解决如下两个分布式系统的技术难点。

降低网络通信开销
在分布式系统中,机器通过网络通信来共同完成任务。但不论是按照延时还是按照带宽,网络通信速度都是本地内存读写的数十或数百分之一。解决网络通信瓶颈是设计分布式系统的关键。

异步执行

在一般的机器学习算法中,计算节点的每一轮迭代可以划分成CPU繁忙和网络繁忙这两个阶段。前者通常是在计算梯度部分,后者则是在传输梯度数据和模型参数部分。串行执行这两个阶段将导致CPU和网络总有一个处于空闲状态。我们可以通过异步执行来提升资源利用率。例如,当前一轮迭代的CPU繁忙阶段完成时,可直接开始进行下一轮的CPU繁忙阶段,而不是等到前一轮的网络繁忙阶段完成。这里我们隐藏了网络通信开销,从而将CPU的使用率最大化。但由于没有等待前一轮更新的模型被取回,会导致这个计算节点的模型参数与服务节点处最新的参数不一致,由此可能会影响算法效率。

灵活的数据一致性模型

数据不一致性需要考虑提高算法效率和发挥系统性能之间的平衡。最好的平衡点取决于很多因素,例如CPU计算能力、网络带宽和算法的特性。我们发现很难有某个一致性模型能适合所有的机器学习问题。为此,参数服务器提供了一个灵活的方式用于表达一致性模型。

首先执行程序被划分为多个任务。一个任务类似于一个远程过程调用(Remote Procedure Call, RPC),可以是一个发送或一个获取,或者任意一个用户定义的函数,例如一轮迭代。任务之间可以并行执行,也可以加入依赖关系的控制逻辑,来串行执行,以确保数据的一致性。所有这些任务和依赖关系组成一个有向无环图,从而定义一个数据一致性模型,如图6所示。

图6 使用有向无环图来定义数据一致性模型

如图7所示,我们可以在相邻任务之间加入依赖关系的控制逻辑,得到顺序一致性模型,或者不引入任何依赖关系的逻辑控制,得到最终一致性模型。在这两个极端模型之间是受限延时模型。这里一个任务可以和最近的数个任务并行执行,但必须等待超过最大延时的未完成任务的完成。我们通过使用最大允许的延时来控制机器在此之前的数据不一致性。

图7 不同数据一致性下运行时间

图8展示了在广告点击预测中(细节描述见后文),不同的一致性模型下得到同样精度参数模型所花费的时间。当使用顺序一致性模型时(0延时),一半的运行时间花费在等待上。当我们逐渐放松数据一致性要求,可以看到计算时间随着最大允许的延时缓慢上升,这是由于数据一致性减慢了算法的收敛速度,但由于能有效地隐藏网络通信开销,从而明显降低了等待时间。在这个实验里,最佳平衡点是最大延时为8。

选择性通信

任务之间的依赖关系可以控制任务间的数据一致性。而在一个任务内,我们可以通过自定义过滤器来细粒度地控制数据一致性。这是因为一个节点通常在一个任务内有数百或者更多对的关键字和值(key, value)需要通信传输,过滤器对这些关键字和值进行选择性的通信。例如我们可以将较上次同步改变值小于某个特定阈值的关键字和值过滤掉。再如,我们设计了一个基于算法最优条件的KKT过滤器,它可过滤掉对参数影响弱的梯度。我们在实际中使用了这个过滤器,可以过滤掉至少95%的梯度值,从而节约了大量带宽。

缓冲与压缩

我们为参数服务器设计了基于区段的发送和获取通信接口,既能灵活地满足机器学习算法的通信需求,又尽可能地进行批量通信。在训练过程中,通常是值发生变化,而关键字不变。因此可以让发送和接收双方缓冲关键字,避免重复发送。此外,考虑到算法或者自定义过滤器的特性,这些通信所传输的数值里可能存在大量“0”,因此可以利用数据压缩有效减少通信量。

容灾
大规模机器学习任务通常需要大量机器且耗时长,运行过程中容易发生机器故障或被其他优先级高的任务抢占资源。为此,我们收集了一个数据中心中3个月内所有的机器学习任务。根据“机器数×用时”的值,我们将任务分成大中小三类,并发现小任务(100机器时)的平均失败率是6.5%;中任务(1000机器时)的失败率超过了13%;而对于大任务(1万机器时),每4个中至少有1个会执行失败。因此机器学习系统必须具备容灾功能。

参数服务器中服务节点和计算节点采用不同的容灾策略。对于计算节点,可以采用重启任务,丢弃失败节点,或者其他与算法相关的策略。而服务节点维护的是全局参数,若数据丢失和下线会严重影响应用的运行,因此对其数据一致性和恢复时效性要求更高。

参数服务器中服务节点的容灾采用的是一致性哈希和链备份。服务节点在存储模型参数时,通过一致性哈希协议维护一段或者数段参数。这个协议用于确保当有服务节点发生变化时,只有维护相邻参数段的服务节点会受到影响。每个服务节点维护的参数同时会在数个其他服务节点上备份。当一个服务节点收到来自计算节点的数据时,它会先将此数据备份到其备份节点上,然后再通知计算节点操作完成。中间的任何失败都会导致这次发送失败,但不会造成数据的不一致。

链备份适用于任何机器学习算法,但会使网络通信量成倍增长,从而可能形成性能瓶颈。对于某些算法,我们可以采用先聚合再备份的策略来减少通信。例如,在梯度下降算法里,每个服务节点先聚合来自所有计算节点的梯度,之后再更新模型参数,因此可以只备份聚合后的梯度而非来自每个计算节点的梯度。聚合可以有效减少备份所需通信量,但聚合会使得通信的延迟增加。不过这可以通过前面描述的异步执行来有效地隐藏。

在实现聚合链备份时,我们可以使用向量钟(vector clock)来记录收到了哪些节点的数据。向量钟允许我们准确定位未完成的节点,从而对节点变更带来的影响进行最小化。由于参数服务器的通信接口是基于区段发送的,所有区段内的关键字可以共享同一个向量钟来压缩其存储开销。

图8 三个系统在训练得到同样精度的模型时所各花费的时间

参数服务器不仅为深盟其他组件提供分布式支持,也可以直接在上面开发应用。例如,我们实现了一个分块的Proximal Gradient算法来解决稀疏的Logistic Regression,这是最常用的一个线性模型,被大量的使用在点击预测等分类问题中。

为了测试算法性能,我们采集了636TB真实广告点击数据,其中含有1700亿样本和650亿特征,并使用1000台机器共1.6万核来进行训练。我们使用两个服务产品的私有系统(均基于参数服务器架构)作为基线。图8展示的是这3个系统为了达到同样精度的模型所花费的时间。系统A使用了类梯度下降的算法(L-BFGS),但由于使用连续一致性模型,有30%的时间花费在等待上。系统B则使用了分块坐标下降算法,由于比系统A使用的算法更加有效,因此用时比系统A少。但系统B也使用连续一致性模型,并且所需全局同步次数要比系统A更多,所以系统B的等待时间增加到了50%以上。我们在参数服务器实现了与系统B同样的算法,但将一致性模型放松至受限延时一致性模型并应用了KKT过滤。与系统B相比,参数服务器需要略多的计算时间,但其等待时间大幅降低。由于网络开销是这个算法的主要瓶颈,放松的一致性模型使得参数服务器的总体用时只是系统B的一半。

Rabit:灵活可靠的同步通信
除了Parameter Server提供的异步通信之外,以GBDT和L-BFGS为代表的许多机器学习算法依然适合采用同步通信 (BSP)的方式进行交互。深盟的第二个通信框架Rabit提供了这一选择。

传统的同步通信机器学习程序往往采用MPI的Allreduce进行计算,但是因为MPI提供的接口过于复杂使得它并不容易提供容灾支持。Rabit简化了MPI的设计,抽取出机器学习最需要的Allreduce和Broadcast操作并加入了容灾的支持,使得基于分布式BSP的机器学习算法可以在部分节点出错或丢失的情况下快速恢复计算,完成剩下的任务。目前的GBDT算法xgboost就是基于Rabit提供的接口。同时,Rabit具有非常强的可移植性,目前支持在MPI、Hadoop Yarn和SunGrid Engine等各个平台下直接执行。异步的Parameter Server 接口加上同步的Rabit接口基本涵盖了各种分布式机器学习算法需要的通信需求,使得我们可以很快地实现各种高效的分布式机器学习算法。

未来规划
深盟目前已有的组件覆盖三类最常用的机器学习算法,包括被广泛用于排序的GBDT,用于点击预测的稀疏线性模型,以及目前的研究热点深度学习。未来深盟将致力于将实现和测试更多常用的机器学习算法,目前有数个算法正在开发中。另一方面,我们将更好的融合目前的组件,提供更加一致性的用户体验。例如我们将对cxxnet和Minerva结合使得其既满足对性能的苛刻要求,又能提供灵活的开发环境。

深盟另一个正在开发中的组件叫做虫洞,它将大幅降低安装和部署分布式机器学习应用的门槛。具体来说,虫洞将对所有组件提供一致的数据流支持,无论数据是以任何格式存在网络共享磁盘,无论HDFS还是Amazon S3。此外,它还提供统一脚本来编译和运行所有组件。使得用户既可以在方便的本地集群运行深盟的任何一个分布式组件,又可以将任务提交到任何一个包括Amazon EC2、Microsfot Azure和Google Compute Engine在内的云计算平台,并提供自动的容灾管理。

这个项目最大的愿望就是能将分布式机器学习的门槛降低,使得更多个人和机构能够享受大数据带来的便利。同时也希望能多的开发者能加入,联合大家的力量一起把这个事情做好。(责编/周建丁)

参考文献

[1] M. Li, D. G. Andersen, J. Park, A. J. Smola, A. Amhed, V. Josi- fovski, J. Long, E. Shekita, and B. Y. Su, Scaling distributed machine learning with the parameter server, in USENIX Symposium on Operating System Design and Implementation, 2014.

[2] M. Li, D. G. Andersen, and A. J. Smola.Communication Efficient DistributedMachine Learning with the Parameter Server.In Neural Information Processing Systems, 2014.

[3] M. Li, 大数据:系统遇上机器学习中国计算机学会通讯 2014 年 12 月

[4]Tianqichen, cxxnet和大规模深度学习http://www.weibo.com/p/1001603821399843149639

[5] Tianqi Chen, Tong He, Higgs Boson Discovery with Boosted Trees, Tech Report.

[6] 何通, xgboost: 速度快效果好的boosting模型, 统计之都http://cos.name/2015/03/xgboost/

[7] Minjie Wang, Tianjun Xiao, Jianpeng Li, Jiaxing Zhang, Chuntao Hong, Zheng Zhang, Minerva: A Scalable and Highly Efficient Training Platform for Deep Learning, Workshop, NIPS 14

推荐之用户画像

转至:https://www.zhihu.com/question/19853605/answer/51552384
因为之前为了做用户画像正好做过相关的研究,顺手答一发。
什么是用户画像( personas)?
Alan Cooper (交互设计之父)最早提出了 persona 的概念:“Personas are a concrete representation of target users.”Persona 是真实用户的虚拟代表,是 建立在一系列真实数据(Marketing data,Usability data)之上的目标用户模型。通过用户调研去了解用户,根据他们的目标、行为和观点的差 异,将他们区分为不同的类型,然后每种类型中抽取出典型特征,赋予名字、照片、一些人口统计学要素、场景等描述,就形成了一个人物原型 (personas)。

一些大公司还是很喜欢用personas做用研的,比如微软,腾讯blabla…

为何要建立用户画像 personas?
Cooper认为建立 Personas 的好处有
Creates a common language
Users are no longer elastic
Provides a target - no longer designing for everyone in the world
End debates about prioritization and implementation

简而言之,用户画像(persona )为了让团队成员在产品设计的过程中能够抛开个人喜好,将焦点关注在目标用户的动机和行为上进行产品设计。 因为,产品经理为具体的人物做产品设计要远远优于为脑中虚构的东西做设计,也更来得容易。

但是特别注意的是:
1、 用户画像要建立在真实的数据之上
2、 当有多个用户画像的时候,需要考虑用户画像的优先级,通常建议不能为超过三个以上的 persona 设计产品,这样容易产生需求冲突。
3、 用户画像是处在不断修正中的

Persona 一般需要具备哪些元素?
姓名 Name
照片 Photo
年龄Age
家庭状况 Personal details / family life
收入 Income/Salary
工作 Work / job details
用户场景/活动 Activities / use scenario
计算机技能/知识 Knowledge / skills / abilities
目标/动机 Goals / motives / concerns
喜好 Likes / dislikes
人生态度 Quotes 注意:对于手机浏览器的用户而言,使用手机浏览器的习惯和用户的生活方式(尤其是喜好、阶级)息息相关。
举几个栗子:
当年微软为office做的人物画像(包括了用户的主要活动、使用环境、使用工具等等等…):
这样的人物可以是多个的:
甚至具体到人物的日常作息:
使用设备:

既然写到了这里,再顺便说一下如何建立用户画像 personas?

建立 personas 可以分为以下几个步骤(参考):
Step1: 研究准备与数据收集

Step2: 亲和图
Step3: 人物原型框架
Step4: 优先级排序
Step5: 完善人物原型
参考阅读:CDC《创建定性用户画像》创建定性用户画像 (写的很具体~~推荐指数五颗星)

参考阅读:
Personas Personas | Usability.gov CDC《创建定性用户画像》创建定性用户画像
亲和图 KJ 法在用研中的应用 KJ法在用户研究中的应用
移动用户分类参考 Mobile Usage Segmentation System

gcc 的-lm选项是什么意思?

谢谢回复,原来gcc编译时还有这样的功能
我查了手册,有这样的描述:

“-llibrary
连接名为library的库文件.
连接器在标准搜索目录中寻找这个库文件,库文件的真正名字是`liblibrary.a’.连接器会 当做
文件名得到准确说明一样引用这个文件.”

是不是这个?

是的。

我目前简单理解就是,-l后面跟什么,就是链接什么库

是的。

楼上说还有-lpthread 是不是还有-llibc++?

libstdc++.so 也是有的,但用 g++ 編譯 C++ 代碼時,會自動鏈接這個庫,就如用 gcc 編譯 C 代碼時會自動鏈接 libc.so 一樣。

但我用的数学库是math.h啊,为什么只写-lm就知道是数学库?简写?

數學庫的實現位於 libm.so 中,鏈接它只需寫 -lm 即可。

还有上面说的那个’liblibrary.a’又是什么?

g++ -pthread

在 -llibrary 中,library 是庫的名子(這就是個例子),對應的庫文件是 liblibrary.so (動態庫)或 liblibrary.a (靜態庫)。
可见编译选项中指定 -pthread 会附加一个宏定义 -D_REENTRANT ,该宏会导致 libc 头文件选择那些thread-safe的实现;链接选项中指定 -pthread 则同 -lpthread 一样,只表示链接 POSIX thread 库。由于 libc 用于适应 thread-safe 的宏定义可能变化,因此在编译和链接时都使用 -pthread 选项而不是传统的 -lpthread 能够保持向后兼容,并提高命令行的一致性。

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:142145次
    • 积分:2246
    • 等级:
    • 排名:第16756名
    • 原创:70篇
    • 转载:33篇
    • 译文:5篇
    • 评论:13条
    博客专栏
    友情链接
    Checklist
    Love and strength, love then strength