解析Spark开源框架elephas之一

来源:互联网 发布:人工智能丛书 编辑:程序博客网 时间:2024/05/16 08:31

写在前面的话


elephas是一个把python深度学习框架keras衔接到Spark集群的第三方python包。由于这个版本并不稳定,并且没有什么资料,我打算剖析其源代码。

分析代码要从其主程序开始,就是spark_model.py,其网址在 https://github.com/maxpumperla/elephas/blob/master/elephas/spark_model.py。在这个博客里,我暂且把发现的一些问题先记下来。由于我并不是一个喜欢“调格式“的人,更多喜欢不求甚解,所以先散乱地分析,之后我会系统化逐行代码解析。


程序是从哪里开始的


根据官网上面的程序,https://github.com/maxpumperla/elephas

  • Create a local pyspark context
from pyspark import SparkContext, SparkConfconf = SparkConf().setAppName('Elephas_App').setMaster('local[8]')sc = SparkContext(conf=conf)
  • Define and compile a Keras model
model = Sequential()model.add(Dense(128, input_dim=784))model.add(Activation('relu'))model.add(Dropout(0.2))model.add(Dense(128))model.add(Activation('relu'))model.add(Dropout(0.2))model.add(Dense(10))model.add(Activation('softmax'))model.compile(loss='categorical_crossentropy', optimizer=SGD())
  • Create an RDD from numpy arrays
from elephas.utils.rdd_utils import to_simple_rddrdd = to_simple_rdd(sc, X_train, Y_train)
  • A SparkModel is defined by passing Spark context and Keras model. Additionally, one has choose an optimizer used for updating the elephas model, an update frequency, a parallelization mode and the degree of parallelism, i.e. the number of workers.

from elephas.spark_model import SparkModelfrom elephas import optimizers as elephas_optimizersadagrad = elephas_optimizers.Adagrad()spark_model = SparkModel(sc,model, optimizer=adagrad, frequency='epoch', mode='asynchronous', num_workers=2)spark_model.train(rdd, nb_epoch=20, batch_size=32, verbose=0, validation_split=0.1, num_workers=8)

  • Run your script using spark-submit
spark-submit --driver-memory 1G ./your_script.py

可以知道,程序是从spark_model 开始的,具体是从这个类的train方法开始的。



 模型的流程


train的方法是啥?从train的方法中应该可以抓住模型的执行流程。

先贴代码

    def train(self, rdd, nb_epoch=10, batch_size=32,              verbose=0, validation_split=0.1):        '''        Train an elephas model.        '''        rdd = rdd.repartition(self.num_workers)        master_url = self.determine_master()        if self.mode in ['asynchronous', 'synchronous', 'hogwild']:            self._train(rdd, nb_epoch, batch_size, verbose, validation_split, master_url)        else:            print("""Choose from one of the modes: asynchronous, synchronous or hogwild""")    def _train(self, rdd, nb_epoch=10, batch_size=32, verbose=0,               validation_split=0.1, master_url='localhost:5000'):        '''        Protected train method to make wrapping of modes easier        '''        self.master_network.compile(optimizer=self.master_optimizer, loss=self.master_loss, metrics=self.master_metrics)        if self.mode in ['asynchronous', 'hogwild']:            self.start_server()        yaml = self.master_network.to_yaml()        train_config = self.get_train_config(nb_epoch, batch_size,                                             verbose, validation_split)        if self.mode in ['asynchronous', 'hogwild']:            worker = AsynchronousSparkWorker(                yaml, train_config, self.frequency, master_url,                self.master_optimizer, self.master_loss, self.master_metrics, self.custom_objects            )            rdd.mapPartitions(worker.train).collect()            new_parameters = get_server_weights(master_url)        elif self.mode == 'synchronous':            init = self.master_network.get_weights()            parameters = self.spark_context.broadcast(init)            worker = SparkWorker(yaml, parameters, train_config)            deltas = rdd.mapPartitions(worker.train).collect()            new_parameters = self.master_network.get_weights()            for delta in deltas:                constraints = self.master_network.constraints                new_parameters = self.optimizer.get_updates(self.weights, constraints, delta)        self.master_network.set_weights(new_parameters)        if self.mode in ['asynchronous', 'hogwild']:            self.stop_server()
从上面的代码中,我们可以知道

              (1)train 的方法,其实直接调用了_train(),当然这样做的原因,也是一种程序的模块化的思想。

              (2)master_network 从这个类的初始化中,其实就是model,也就是keras中定义的model。self.master_network.compile 是把model编译一下,这在一般的keras流程中就是这样的,然后就是对model的fit了。

              (3)start_server() 其实是在主节点开始了Flask的app流程,该程序可以使得不同节点可以进行参数之间的通信,实际上就是在master节点建立一个服务器。从而使的 slave节点可以通过 访问url的方式 和master节点进行参数的交流。

def get_server_weights(master_url='localhost:5000'):    '''    Retrieve master weights from parameter server    '''    request = urllib2.Request('http://{0}/parameters'.format(master_url),                              headers={'Content-Type': 'application/elephas'})    ret = urllib2.urlopen(request).read()    weights = pickle.loads(ret)    return weightsdef put_deltas_to_server(delta, master_url='localhost:5000'):    '''    Update master parameters with deltas from training process    '''    request = urllib2.Request('http://{0}/update'.format(master_url),                              pickle.dumps(delta, -1), headers={'Content-Type': 'application/elephas'})    return urllib2.urlopen(request).read()

这两个写在文件前面的函数,就是后面slave节点调用url 去访问master节点进行参数传递。然后我们还可以看看到底服务器端的代码是怎么写的,也就是那个开启了flask的app的主节点。

 def start_service(self):        ''' Define service and run flask app'''        app = Flask(__name__)        self.app = app        @app.route('/')        def home():            return 'Elephas'        @app.route('/parameters', methods=['GET'])        def get_parameters():            if self.mode == 'asynchronous':                self.lock.acquire_read()            self.pickled_weights = pickle.dumps(self.weights, -1)            pickled_weights = self.pickled_weights            if self.mode == 'asynchronous':                self.lock.release()            return pickled_weights        @app.route('/update', methods=['POST'])        def update_parameters():            delta = pickle.loads(request.data)            if self.mode == 'asynchronous':                self.lock.acquire_write()            constraints = self.master_network.constraints            if len(constraints) == 0:                def empty(a): return a                constraints = [empty for x in self.weights]            self.weights = self.optimizer.get_updates(self.weights, constraints, delta)            if self.mode == 'asynchronous':                self.lock.release()            return 'Update done'        self.app.run(host='0.0.0.0', debug=True,                     threaded=True, use_reloader=False)
在这里,有一个问题,便是对其参数的改变时候,有一些lock之类的东东,这个是其其他的文件里写的,后面在分析,在这里我感觉应该是某种保护参数的机制。

            (4)接下俩一行,是yaml = self.master_network.to_yaml(). 为什么要反复的地to yaml 然后有read from yaml?

               一句话,为了序列化的方便。首先这句话,是keras里的知识,是把keras model 序列化为字符串,除了to yaml,还可以to json。反正你知道yaml是一个字符串就好了。本人本着科学研究的精神,试了一把,请看下图

In [4]: print model.to_yaml()class_name: Modelconfig:  input_layers:  - [input_1, 0, 0]  layers:  - class_name: InputLayer    config:      batch_input_shape: !!python/tuple [null, 784]      input_dtype: float32      name: input_1      sparse: false    inbound_nodes: []    name: input_1  - class_name: Dense    config: {W_constraint: null, W_regularizer: null, activation: relu, activity_regularizer: null,      b_constraint: null, b_regularizer: null, bias: true, init: glorot_uniform, input_dim: null,      name: dense_1, output_dim: 64, trainable: true}    inbound_nodes:    - - [input_1, 0, 0]    name: dense_1  - class_name: Dense    config: {W_constraint: null, W_regularizer: null, activation: relu, activity_regularizer: null,      b_constraint: null, b_regularizer: null, bias: true, init: glorot_uniform, input_dim: null,      name: dense_2, output_dim: 64, trainable: true}    inbound_nodes:    - - [dense_1, 0, 0]    name: dense_2  - class_name: Dense    config: {W_constraint: null, W_regularizer: null, activation: softmax, activity_regularizer: null,      b_constraint: null, b_regularizer: null, bias: true, init: glorot_uniform, input_dim: null,      name: dense_3, output_dim: 10, trainable: true}    inbound_nodes:    - - [dense_2, 0, 0]    name: dense_3  name: model_1  output_layers:  - [dense_3, 0, 0]keras_version: 1.1.1

至于,为什么yaml 这样做是为了序列化的方便在下面一点解释。

        (5)接下来,我们分析这个分支:if self.mode in ['asynchronous', 'hogwild']: 精彩内容来了:

              

        if self.mode in ['asynchronous', 'hogwild']:            worker = AsynchronousSparkWorker(                yaml, train_config, self.frequency, master_url,                self.master_optimizer, self.master_loss, self.master_metrics, self.custom_objects            )            rdd.mapPartitions(worker.train).collect()            new_parameters = get_server_weights(master_url)

          如果选的是asyn 就是同步更新的话,下面就是定义AsynchronousSparkWorker 这个是系统定义了另外一个仅次于spark model的第二个重要的类。写到这个,读者可能会问,到底分布式在哪里啊,好像目前都是在主节点一个人YY,没有slave节点什么事情。通过rdd mapPartitions,这个函数,其中对于每一个节点执行AsynchronousSparkWorker 的train方法。因为这个设计到这个类在Spark集群中的传输,所以上面的model 要to yaml以方便传输。

      (6)最后,便是通过get_server_weights,获得新的参数,

并将其给self.master_network.set_weights(new_parameters)。


-----------updated 2016.11.22 高铁上

这两天去北京参加了一个IBM在北京举办的机器学习峰会,今年IBM主推了智慧计算,和Spark。今天还见到了Spark的commiter之一 Nick Pentreath。感觉Spark应该在未来几年都会是一个比较持续热的分析框架。而在深度学习的火爆也将直接反映在机器学习框架keras,tensorflow的hot。而据我所知,目前而言,python仍然是数据分析者使用的主要语言。因此如何把一些python的第三包部署在spark集群上面,将会是一个非常hot的话题。当然也就是我分析elephas的意义所在。

上面主要分析的是elephas的主文件spark_model.py中的master节点的主类,是所有代码的入口。因此这个类的分析我放在这系列的第一篇。接下来,将继续分析这个主类,主要是抠一些细节。

(1)为什么要import python 默认multiporocess?

我目前查到的信息,这是一个多线程的问题,在这个类中,通过把flask作为multiporocess的一个进程函数进行管理。进一步查资料,发现这个multiporocess实现的 多线程和thread其实是不一样的。不同的在于thread的多线程实际上用的还是一个核,而multiporocess可以在多核上运行。这个问题,我目前只有这些浅显的理解,继续完善吧。

(2)上面的类中,有两条通信方式?
 一个是Spark自身ssh通信,一个是基于flask,http通信协议,这个是用于解决训练过程中,不同节点的参数管理。

(3)为什么import socket?

因为,为了确认

        master_url = socket.gethostbyname(socket.gethostname()) + ':5000'
只有这里用哟。
(4)update_parameters函数之中,为什么会有lock?
这个是作者在多线程对于同一个公共变量,也就是模型的参数,进行修改时,为了解决多个线程对同一变量内存的冲突,所以使用了RWlock,
这个锁的代码放在了utils之中。

结束

好了,暂时第一篇就先到这里了。我将分析这个文件中的第二类AsynchronousSparkWorker。这个类是处理slave节点的主类。

现在在高铁上,睡一会,THE Night Is So Black!!!



0 0
原创粉丝点击