函数计算部署机器学习遇到的问题和解法

随着 Serverless 的流行,将应用迁移到云上已经成了一种必然的趋势。

我们今天来看一下如何将机器学习应用迁移到函数计算上。

1. 本地开发

首先我们看一下本地开发机器学习应用的步骤。我们大概可以将本地开发概括为三个步骤,分别是代码编写,安装依赖,运行调试。我们分别来看一下。

1.1 代码编写

假定我们的项目结构为:

projectroot├──index.py├──model_data│  ├──checkpoint│  ├──model.data-00000-of-00001│  ├──model.index│  └──model.met└──pic└──e2.jpg

其中 index.py 存放了机器学习相关的代码,model_data 存放了数据模型,pic 中存放了要进行测试的图片。

index.py 的内容为(代码参考了这篇文章):

# -*- coding:utf-8 -*-  importosimportsysimportcv2importnumpyasnpimporttensorflowastf  saver =NonedefreversePic(src):foriinrange(src.shape[0]):forjinrange(src.shape[1]):            src[i, j] =255- src[i, j]returnsrcdefmain():sess = tf.Session()      saver = tf.train.import_meta_graph('model_data/model.meta')    saver.restore(sess,'model_data/model')    graph = tf.get_default_graph()        input_x = sess.graph.get_tensor_by_name("Mul:0")    y_conv2 = sess.graph.get_tensor_by_name("final_result:0")        path="pic/e2.jpg"im = cv2.imread(path, cv2.IMREAD_GRAYSCALE)    im = reversePic(im)    im = cv2.resize(im, (28,28), interpolation=cv2.INTER_CUBIC)      x_img = np.reshape(im , [-1,784])      output = sess.run(y_conv2 , feed_dict={input_x:x_img})print'the predict is %d'% (np.argmax(output))    sess.close()if__name__ =='__main__':      main()

1.2 安装依赖

在运行应用前,需要先安装应用依赖的模块,这里主要依赖了 opencv 以及 tensorflow,安装方法很简单:

pip install opencv-python

pip install tensorflow

执行完这两条命令后,opencv-python 以及 tensorflow 就被安装到系统目录里。Linux 下默认为 /usr/local/lib/pythonX.Y/site-packages。

1.3 运行

运行时,python 会自动在配置的路径下查找相关模块并进行加载。

$ python index.py

the predict is 8

经过这三个步骤,我们就完成了本地机器学习应用的开发,我们接下来看下如何迁移应用到函数计算。

2. 迁移函数计算

2.1 本地开发与函数计算开发对比

首先,我们需要做一些准备工作。让我们来思考下函数计算应用开发方式与本地应用应用的开发方式有什么不同呢?

代码入口。本地开发时,代码可以省略 main 函数,也可以提供 main 函数作为程序入口,但在函数计算中,函数入口是固定的,非 Http 触发器的函数入口必须是一个包含了两个参数的函数,比如:def handler(event, context)。

模块依赖。本地开发时,项目依赖的模块通常会被安装到系统的某个目录。比如我们上面执行的 pip install tensorflow。而对于函数计算,由于为了能够最大限度的对应用进行优化,开放给用户的操作空间通常是比较小的。因此,对于函数计算,目前还无法做到安装项目依赖到运行环境。我们只能通过将自定义模块一同打包的方式。参考

运行。本地开发时,需要使用 python 命令或者 IDE 来运行代码。而在函数计算,我们需要首先部署应用到函数计算,再通过触发器或者控制台手动触发执行。

接下来我们针对这三点开发方式的不同对代码进行改造。

2.2 改造代码

2.2.1 代码入口改造

这个比较简单,只需要将

defmain():

修改为

defhandler(event, context):

并删除下面代码:

if__name__ =='__main__':      main()

2.2.2. 模块依赖

这一块稍微复杂些。不同的语言因为模块加载机制的不同,这里的处理逻辑也会有差异。比如对于 java,无论是使用 maven,还是 gradle,都可以很容易的一键将代码以及依赖打包成 jar。但遗憾的是 python 目前没有这种机制。

我们先根据场景对 python 依赖的模块做个简单的分类。

应用依赖: 对于本例中使用 pip 安装的模块,比如 pip install tensorflow,我们暂且将其称为应用依赖。

系统依赖: 在某些场景下,python 安装的库仅仅是对底层 c、c++ 库调用的封装,例如使用 zbar 时,除了使用 pip install zbar,还要在系统中安装相应的库:apt-get install -y libzbar-dev。我们暂且把像 libzbar-dev 一样需要使用系统软件包管理器安装的库称为系统依赖。

资源依赖: 对于一些应用,比如机器学习,启动后还需要加载数据模型,数据模型需要在程序启动时准备好,我们暂且将这种依赖称为资源依赖。资源依赖比较特殊,它是我们的应用逻辑所需要的,通常体积比较大。

对于应用依赖,我们可以通过 pip 的 -t 参数改变其安装位置,比如 pip install -t $(pwd) tensorflow。并且可以通过 sys.path 改变加载行为,使得可以从指定目录加载模块。

对于系统依赖,我们可以通过 apt-get 下载 deb 包,再利用 deb 包安装到指定目录。

apt-get install -y-d-o=dir::cache=$(pwd) libzbar-devforfin$(ls $(pwd)/archives/*deb);dodpkg -x$f$(pwd);donerm -r archives

对于系统依赖包含的链接库,可以通过 LD_LIBRARY_PATH 变量改变其加载行为。

对于资源依赖,因为控制权在我们的代码里,因此只需要改变代码的处理逻辑就可以了。

根据上面的描述,我们可以整理成下面的表格:

类别定义安装方法举例指定位置安装方法举例影响加载的因素

应用依赖pip 安装的模块pip install tensorflowpip install -t $(pwd) tensorflowsys.path

系统依赖系统软件包管理器安装的依赖apt-get install -y libzbar-devapt-get install -y -d -o=dir::cache=$(pwd) libzbar-dev

for f in $(ls $(pwd)/archives/*deb); do dpkg -x $f $(pwd); done

rm -r archives

LD_LIBRARY_PATH

资源依赖代码依赖的资源,比如数据模型\\由应用代码控制

2.2.3 下载依赖的逻辑

对于我们的 demo 应用,存在两种依赖,一种是应用依赖,另一种是资源依赖。而需要我们特别处理的只有应用依赖。我们需要在项目目录下创建一个名为 applib 的目录,并下载应用依赖到该目录。这里需要注意的是如果引用的模块使用 C / C++ / go 编译出来的可执行文件或者库文件,那么推荐使用 fcli 的 sbox 进行下载,使用方法为:

mkdir applib fcli shellsbox-dapplib -t python2.7pip install -t $(pwd) tensorflowpip install -t $(pwd) opencv-python

执行完毕后,就会发现 applib 中就包含了项目所需要的应用依赖。

2.2.4 打包依赖上传到 OSS

机器学习的应用依赖、资源依赖通常比较大,会很容易超过函数计算对代码包的限制(50M)。为了避开这个问题,我们需要将这些依赖上传到 OSS:

cdapplib && zip -r applib.zip * && mv applib.zip ../ ;cd..

执行完毕后,项目会多出一个名为 applib.zip 的压缩包,上传到 oss 即可。

同样的,对资源依赖进行相同的操作:

cdmodel_data && zip -r model_data.zip * && mv model_data.zip ../ ;cd..

2.2.5 初始化依赖

这里我们提供一个模板代码,负责在函数第一次启动时,从 OSS 下载资源到本地、解压,并配置好相应的环境变量。我们可以在项目中创建一个名为 loader.py 文件,内容为:

# -*- coding:utf-8 -*-  importsysimportzipfileimportosimportoss2importimpimporttimeapp_lib_object = os.environ['AppLibObject']app_lib_dir = os.environ['AppLibDir']model_object = os.environ['ModelObject']model_dir = os.environ['ModelDir']local = bool(os.getenv('local',""))print'local running: '+ str(local)inilized =Falsedefdownload_and_unzip_if_not_exist(objectKey, path, context):creds = context.credentialsif(local):print'thank you for running function in local!!!!!!'auth = oss2.Auth(creds.access_key_id,                        creds.access_key_secret)else:        auth = oss2.StsAuth(creds.access_key_id,                            creds.access_key_secret,                            creds.security_token)    endpoint = os.environ['Endpoint']    bucket = os.environ['Bucket']print'objectKey: '+ objectKeyprint'path: '+ pathprint'endpoint: '+ endpointprint'bucket: '+ bucket    bucket = oss2.Bucket(auth, endpoint, bucket)        zipName ='/tmp/tmp.zip'print'before downloading '+ objectKey +' ...'start_download_time = time.time()    bucket.get_object_to_file(objectKey, zipName)print'after downloading, used %s seconds...'% (time.time() - start_download_time)ifnotos.path.exists(path):        os.mkdir(path)print'before unzipping '+ objectKey +' ...'start_unzip_time = time.time()withzipfile.ZipFile(zipName,"r")asz:        z.extractall(path)print'unzipping done, used %s seconds...'% (time.time() - start_unzip_time)defhandler(event, context):globalinilizedifnotinilized:if(notlocal ):            download_and_unzip_if_not_exist(app_lib_object, app_lib_dir, context)            download_and_unzip_if_not_exist(model_object, model_dir, context)        sys.path.insert(1, app_lib_dir)printsys.path        inilized =Truefile_handle, desc =None,Nonefn, modulePath, desc = imp.find_module('index')    mod = imp.load_module('index', fn, modulePath, desc)    request_handler = getattr(mod,'handler')returnrequest_handler(event, context)

这段代码会首先读取 AppLibObject 环境变量,用于从 OSS 下载应用依赖,并解压到 AppLibDir 这个环境变量所代表的目录。

其次会读取 ModelObject 环境变量,用于从 OSS 下载资源依赖,并解压到 ModelDir 这个环境变量所代表的目录。

最后,当依赖准备妥当后,会调用 index.py 中的 handler 函数。

理论上,这个代码可以用于其它任何需要下载应用依赖、资源依赖的场景。而我们的 index.py 需要修改的,只有将原先的获取模型依赖的固定的路径,修改为利用 ModelDir 获取路径即可。

2.3 本地运行调试

代码编写完成后,我们的目录结构调整为:

projectroot├──code│  ├──index.py│  ├──loader.py│  └──pic│      └──e2.jpg├──applib│.  └── *└──model_data├──checkpoint├──model.data-00000-of-00001├──model.index└──model.met

我们本地运行看下效果,这里我们借助于函数计算推出的 fc-dcoker 工具。

为了避免本地每次调试做无谓的下载,我们取下巧,将应用依赖、资源依赖挂载到 fc-docker 中,并开启 local 的标识:

docker run --rm \-elocal=true\-eAppLibObject=applib.zip \-eAppLibDir=/tmp/applib \-eModelObject=model_data.zip \-eModelDir=/tmp/model \    -v $(pwd)/code:/code \    -v $(pwd)/applib:/tmp/applib \    -v $(pwd)/model_data:/tmp/model \    aliyunfc/runtime-python2.7 \  loader.handler

得到结果:

2018-08-28 17:44:16.043564: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supportsinstructions that this TensorFlow binary was not compiled to use: AVX2 FMAFunctionCompute python runtime inited.FC Invoke Start RequestId: f3ea930e-d7e2-4173-9726-453cdd89f18clocalrunning: True['/code','/tmp/applib','/var/fc/runtime/python2.7/src','/usr/local/lib/python27.zip','/usr/local/lib/python2.7','/usr/local/lib/python2.7/plat-linux2','/usr/local/lib/python2.7/lib-tk','/usr/local/lib/python2.7/lib-old','/usr/local/lib/python2.7/lib-dynload','/usr/local/lib/python2.7/site-packages']2018-08-28T17:44:16.154Z f3ea930e-d7e2-4173-9726-453cdd89f18c [INFO] Restoring parameters from /tmp/model/modelthe predict is 8RequestId: f3ea930e-d7e2-4173-9726-453cdd89f18c          Billed Duration: 9954 ms        Memory Size: 1998 MB      Max Memory Used: 239 MB

2.4 部署

本地开发完成,接下来,我们就需要部署应用到线上了。这里我们借助函数计算推出的 Fun 工具。

Fun 工具使用步骤如下:

去 release 页面对应平台的 binary 版本,解压就可以使用。或者使用 npm install @alicloud/fun -g 也可以直接使用。

使用 fun config 配置 ak、region 等信息。

编写 template.yml

fun deploy 部署

是的,不需要登录控制台进行繁琐的配置,仅仅在项目下提供一个 template.yml 即可:

ROSTemplateFormatVersion:'2015-09-01'Transform:'Aliyun::Serverless-2018-04-03'Resources:  tensorflow:# 服务名Type:'Aliyun::Serverless::Service'Properties:      Description:'tensorflow demo'Policies:        - AliyunOSSReadOnlyAccesstest:# 函数名Type:'Aliyun::Serverless::Function'Properties:        Handler: utils.handler        CodeUri: ./code/        Description:'tensorflow application!'Runtime: python2.7        MemorySize: 1024        Timeout: 300        EnvironmentVariables:          Bucket: just-fc-test# 替换为自己的 oss bucketEndpoint:'https://oss-cn-shanghai-internal.aliyuncs.com'# 替换掉 OSS EndpointAppLibObject: applib.zip          AppLibDir: /tmp/applib          ModelObject: model_data.zip          ModelDir: /tmp/model

至此,我们的项目中又多了一个 template.yml,结构为

projectroot├──code│  ├──index.py│  ├──loader.py│  └──pic│      └──e2.jpg├──applib│  └── *├──template.yml└──model_data├──checkpoint├──model.data-00000-of-00001├──model.index└──model.met

通过这一个 template.yml,执行 fun deploy 后即可创建好相应的服务、函数,并配置好函数的环境变量。

$ fun deployWaitingforservice tensorflow to be deployed...        Waitingforfunctiontestto be deployed...                Waitingforpackagingfunctiontestcode...                packagefunctiontestcodedonefunctiontestdeploy successservice tensorflow deploy success

即使修改了代码,只要重复执行 fun deploy 即可。

接下来,打开 https://fc.console.aliyun.com/ 控制台,依次找到创建好的服务、函数,点击执行,即可得到与本地一致的输出:


2.5 补充

在上面的例子中,我们只列出了应用依赖、资源依赖的情况。对于系统依赖的处理逻辑是比较简单的,比如我们拿 zbar 举例。除了需要在 applib 中通过 pip 安装 zbar,还要在 code 目录下新建一个 lib 目录,并通过 sandbox 在这个目录中执行:

apt-get install -y-d-o=dir::cache=$(pwd) libzbar-devforfin$(ls $(pwd)/archives/*deb);dodpkg -x$f$(pwd);donerm -r archives

执行完成后,目录结构变化为:

projectroot├── code│  ├── lib│  │  └── usr/lib│  │      └── *

就像上面提到的,我们需要修改 LD_LIBRARY_PATH 来改变系统依赖的加载行为,因此我们需要在 template.yaml 中的 EnvironmentVariables 下添加一行:

LD_LIBRARY_PATH: /code/lib/usr/lib:/code:/code/lib:/usr/local/lib

至此,就可以直接在 index.py 等文件中直接使用 zbar 了。

3. 总结

结果一番努力,我们终于将机器学习应用上线到函数计算了。回顾上面的所有操作可以发现,其实大部分的改造工作都是可以通过工具解决的。无论是 template.yml,还是 loader.py 都是直接拿来就能用的。而真正需要开发者操作的也就只有下载依赖、修改对资源依赖的引用路径了。


附:阿里云代金券1000元免费领取!领取地址:http://aliyun.jinre.com 

新老阿里云账户均可领取!可用于购买阿里云服务器ECS、云数据库RDS、虚拟主机、安骑士、DDoS高防IP等100多云计算产品。

    代金券自领取之日起,有效期是30天,请及时使用,过30天后还可以重新领取。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 认真的说,如果不是遇到了《得到》,我可能现在还在人生的路口不断的徘徊,长时间处于的低谷期--痛苦、彷徨、抱怨和自责...
    恋恋成长路阅读 377评论 0 1
  • 最近歌单总是暗搓搓的给我推荐离别的歌曲,就像张震岳的《再见》。 这是一首接触时间很短,却掺杂着浓烈情绪的歌。 大学...
    苏洛baby阅读 368评论 0 0
  • 不知不觉间生命已然走过一小半,回想起来以前的岁月始终奔跑在路上,甚至来不及停下来观赏沿途美丽的风景,为家庭...
    静儿2018阅读 358评论 0 2
  • 别人的想法 是飘忽不定的 他们想着和恋人幽会 想走大运或出大名 我总是想着麻烦 我的想法是稳重的 所以当麻烦来临时...
    五星连珠阅读 3,048评论 1 4
  • 生活给予我们的,无法改变。 我们能给予生活的,由我做主。 认清自己,慢慢的,一切都会好。 不抱怨,不懈怠,只愿能肆...
    逝少阅读 750评论 0 1