评估深度学习架构在Spark集群的应用:从theano,keras到elephas
最终目标
最终目标:为了提高机器学习特别是深度学习的计算速度。提供的速度有三个方法:让算法的变得更加聪明(数据结构);让单个机器的计算能力增强(更好的CPU/GPU);让计算并行化(多线程;Hadoop/Spark)。本文仅仅探索第三种方式:评估不同的深度学习框架(是否支持GPU,易于实现,速度快)以及其如何并行化于分布式的集群之上。theano是一个python的包,用数组向量来定义和计算数学表达式。它使得在Python环境下编写深度学习算法变得简单。在它基础之上还搭建了许多类库。个人看来,其有两个优点,第一是符号计算,总所周知,模型搭建还以后,我们就需要计算梯度公司,进行训练,特别是神经网络,theano引入了符号计算,让我们不必在担心由于不断增加的模型复杂度而导致的梯度公司计算的复杂。第二是可以支持GPU,因此我们可以买GPU,增加硬件的计算能力。要知道GPU在计算上要比CPU快得多。不管怎样,做深度学习我们就选了theano。下面我们就在选了theano的基础之上,如何进行计算的并行化,在这里我们将研究如何把theano部署到Spark集群上面。目前有三台的8G的LinuxPC机,已经搭建了Hadoop,Spark集群,每台机器都安装了python theano
运行theano在Spark集群的初步探索
为了最终的目标,需要解决的问题:
(1)带着这个问题,我先找到了spark examples中自带的logistic_regression.py,随便捏造了一个数据,放在了分布式的文件系统中test文件夹下。放到集群了,跑了一下:
spark-submit --master spark://master:7077 logistic_regression.py hdfs:///test 20
结果是OK的。
从这个结果中,我看到了这个代码中有import numpy,所以spark应该在调用python编译的时候,是在其本地调用python,所以只有每一个work上装numpy,应该就是ok的。另外,其中在这个文件中,其依靠的spark的是对于LR训练中的每一次的迭代中,要计算所有数据的梯度,然后将其加起来,这是在这个层面上进行了分布式处理。“map( gradient_compute_f(lines ) ).reduce(add) ”。因此我感觉这个分布式的其实并不多吧。如果采取的算法是SGD,那么应该就不行了吧。(补充:后来找到了http://deepdist.com/ ,其论文中介绍了如何基于Spark实现了SGD,论文参见 J Dean, GS Corrado, R Monga, K Chen, M Devin, QV Le, MZ Mao, M’A Ranzato, A Senior, P Tucker, K Yang, and AY Ng. Large Scale Distributed Deep Networks. NIPS 2012: Neural Information Processing Systems, Lake Tahoe, Nevada, 2012.)
(2)然后,我打算把其中计算梯度部分,替换成theano之中的计算梯度函数,这样不就可以狸猫换太子了吗?于是我到了theano的官方主页,找到了LR的代码,http://www.deeplearning.net/software/theano/tutorial/examples.html#a-real-example-logistic-regression;然后在本地测试了一下,是OK的。然后当我准备把其放入了(1)中代码框架中,发现问题来了,theano都是符号计算,而且训练都是封装好的,因此我没有办法践行了狸猫换太子的想法了。
(3)于是,我就google spark+theano,百度不到,只能google。搜索之下,发现了
https://github.com/maxpumperla/elephas
http://blog.csdn.net/cyh_24/article/details/49683221
于是我发现了两个基于theano封装的包keras,elephas。
先说 keras,它是基于 theano 的深度学习库,用过 theano 的可能会知道,theano 程序不是特别好些。keras 是对theano的一个高层封装,使得代码写起来更加方便。
elephas 使得keras程序能够运行在Spark上面。使得基本不改变keras,就能够将程序运行到spark上面了。 至于其原理https://arxiv.org/pdf/1605.08325v1.pdf,github主页在https://github.com/maxpumperla/elephas
看样子,我的这个问题,也算法是一个比较困难并且重要的问题,而且有人已经解决了。于是我开始安装这两个包(安装好theano,pip就好了,安装并没有坑),并且看其tutorial。
-----------我是分割线 更新 2016 11 17
关于keras,elephas
keras相对来说是比较成熟的,最新的版本已经是1.1.1,并且通过包装的theano,tensorflow让深度学习的构建变得极其简单。其github上的exmaples例子运行都是非常流畅。
吐槽一下elephas
elephas说明性的论文2016年五月才发布,其可用的版本只有verison0.3.并且其依赖于keras0.3.对于最新的1.1.1,是不支持的。如何安装不注意就会报”
from keras.models import model_from_yaml, slice_X
ImportError: cannot import name slice_X
之类的bug。之前好不容易把用elephas,在Spark集群跑了mnist_mlp.py 的例子,想换一种输入数据,结果就报了weight不兼容的错误。
Caused by: org.apache.spark.api.python.PythonException: Traceback (most recent call last):
File "/home/hadoop/spark-1.6.2-bin-hadoop2.6/python/lib/pyspark.zip/pyspark/worker.py", line 111, in main
process()
File "/home/hadoop/spark-1.6.2-bin-hadoop2.6/python/lib/pyspark.zip/pyspark/worker.py", line 106, in process
serializer.dump_stream(func(split_index, iterator), outfile)
File "/home/hadoop/spark-1.6.2-bin-hadoop2.6/python/lib/pyspark.zip/pyspark/serializers.py", line 263, in dump_stream
vs = list(itertools.islice(iterator, batch))
File "/home/hadoop/anaconda2/lib/python2.7/site-packages/elephas/spark_model.py", line 262, in train
model.set_weights(weights_before_training)
File "/home/hadoop/anaconda2/lib/python2.7/site-packages/keras/layers/containers.py", line 104, in set_weights
self.layers[i].set_weights(weights[:nb_param])
File "/home/hadoop/anaconda2/lib/python2.7/site-packages/keras/layers/core.py", line 136, in set_weights
raise Exception("Layer shape %s not compatible with weight shape %s." % (K.get_value(p).shape, w.shape))
Exception: Layer shape (91, 512) not compatible with weight shape (784, 128).
分析其报错的内容,可以找到,其在spark_model.py 文件中,weight_before_training.
260 for epoch in range(nb_epoch):
261 weights_before_training = get_server_weights(self.master_url)
262 model.set_weights(weights_before_training)
263 self.train_config['nb_epoch'] = 1
264 if x_train.shape[0] > batch_size:
27 def get_server_weights(master_url='localhost:5000'):
28 '''
29 Retrieve master weights from parameter server
30 '''
31 request = urllib2.Request('http://{0}/parameters'.format(master_url),
32 headers={'Content-Type': 'application/elephas'})
33 return pickle.loads(urllib2.urlopen(request).read())
可见其应该是在权重初始化的时候,从master节点上把上次运行成功的weights发送过来作为初始化权重。以下在ipython,测试证实了这个想法。
In [24]:master_url ="localhost:5000"
In [25]:request = urllib2.Request('http://{0}/parameters'.format(master_url),headers={'Content-Type':'application/eleph'
...: })
In [26]:t = pickle.loads(urllib2.urlopen(request).read())
In [27]: for iin t:
...: print i.shape
...:
(784, 128)
(128,)
(128, 128)
(128,)
(128, 10)
(10,)
至于进一步的如何解决这个bug,网络上面的参考文献寥寥无几,目前还在寻找。也希望相关人士研究一下elephas的架构,共同分析一下这个问题。