背景
深度学习场景使用函数计算典型案例
阿里云 函数计算 客户 码隆科技 是一家专注于深度学习与计算机视觉技术创新的公司。当码隆的客户上传大量图像数据后,需要尽快把图像按照客户指定的方式处理,包括商品识别,纺织面料等柔性材质识别分析,内容审查,以图搜图等等。图像处理基于码隆预先训练好的深度学习模型,要求在短时间内准备大量的计算资源进行大规模并行处理。客户将深度学习推理逻辑实现为函数,在函数中加载模型后对图像数据进行处理。通过函数计算提供的大规模计算能力,客户能够短时间处理大量图像,平稳应对峰值压力。更多详细案例请见 函数计算客户案例。
深度学习场景的客户在使用函数计算服务中更希望平台做哪些改进?
深度学习场景下加载模型是主要的应用层冷启动开销,模型的规格多为 500MB+,应用层冷启动开销往往会导致毛刺的产生,为归避这类问题,函数计算引入了 initializer 接口来解决应用层冷启动开销带来的毛刺问题。
功能简介
Initializer 编程模式为用户提供了 initializer 入口定义,便于用户将业务逻辑分为initializer函数和请求处理函数两部分。函数计算使用容器执行用户函数代码,这样的执行环境我们称之为函数实例。函数实例会在启动的时候能够自动执行 initializer 函数,进行业务层冷启动,成功之后,该实例收到用户的 Invoke 请求,就能够执行用户的请求处理函数了。
引入 initializer 接口的优势:
- 分离初始化逻辑和请求处理逻辑,程序逻辑更清晰,让用户更易写出结构良好,性能更优的代码;
- 用户函数代码更新时,系统能够保证用户函数的平滑升级,规避应用层初始化冷启动带来的性能损耗。新的函数实例启动后能够自动执行用户的初始化逻辑,在初始化完成后再处理请求;
- 在应用负载上升,需要增加更多函数实例时,系统能够识别函数应用层初始化的开销,更精准的计算资源伸缩的时机和所需的资源量,让请求延时更加平稳;
- 即使在用户有持续的请求且不更新函数的情况下,FC系统仍然有可能将已有容器回收或更新,这时没有平台方(FC)的冷启动,但是会有业务方冷启动,Initializer 的引入可以最大限度减少这种情况;
案例实践
本实践以 函数计算部署机器学习遇到的问题和解法 这篇文章为基础,做了进一步改造和优化。下文将按照以下几个步骤讲解如何利用函数计算以高性能、低延时玩转深度学习场景下的识别手写数字案例:
安装依赖
训练模型
首先需要训练预期的模型,模型的训练可参考 这篇文章。按照文章中的步骤下载 MINIST 数据库和相关代码并开始训练模型,训练时长持续半小时左右,训练成功后的结构目录如下,其中 model_data 目录下的文件便是通过训练得到的模型。
project root
├── main.py
├── grf.pb
└── model_data
├── checkpoint
├── model.data-00000-of-00001
├── model.index
└── model.meta
应用依赖的安装
本案例需要安装的应用依赖有 tensorflow
和 opencv-python
,模型的训练和函数的处理逻辑都强依赖这两个库,训练模型可在本地直接操作,通过 pip 在本地安装两个依赖库即可,版本不限。由于函数运行在函数计算(FC)系统同样依赖这两个库,需要提前下载好依赖并打包上传到 OSS。推荐使用 fcli 工具的 sbox 命令,下面以 runtime 为 python2.7 进行操作:
目前 Pypi 上 tensorflow 最新版本为 1.11.0,为避免因版本问题影响您的实践,建议安装 1.8.0 版本。
cd <'此项目的根目录中'>
mkdir applib // 创建存储所有应用依赖的目录
fcli shell // fcli version >= 0.24
sbox -d applib -t python2.7
pip install -t $(pwd) tensorflow==1.8.0
pip install -t $(pwd) opencv-python
完成之后 exit 退出沙盒环境,并执行 exit 退出fcli。
上传依赖
依赖和模型下载成功后需要进行压缩并上传到 OSS 中,以便后面函数可以直接从 OSS 下载即可,在项目的根目录执行下面两条命令可以得到 applib.zip
和 model_data.zip
两个 zip 压缩包。
cd applib && zip -r applib.zip * && mv applib.zip ../ ; cd ..
cd model_data && zip -r model_data.zip * && mv model_data.zip ../ ; cd ..
下面提供了一个简单的上传 zip 包到 OSS 的模版,上传成功后删除本地的依赖和模型目录即可。
# -*- coding: utf-8 -*-
import oss2
auth = oss2.Auth(<'Your access_key_id'>, <'Your access_key_secret'>)
bucket = oss2.Bucket(auth, <'Your endpoint'>, <'Your bucket'>)
bucket.put_object_from_file('applib.zip', <'Your applib.zip path'>)
bucket.put_object_from_file('model_data.zip', <'Your model_data.zip path'>)
将机器学习应用迁移至函数计算
如何将本地机器学习应用进行改造并迁移到函数计算的流程在 将机器学习应用迁移至函数计算 中有详细的步骤,这篇文章中的改造并没有 initializer 的概念,您只需要关注 index.py 和 loader.py 是如何产生的,详细代码链接,改造后的目录结构如下,其中 index.py 存放了机器学习相关逻辑的代码,loader.py 存放了函数入口和加载依赖逻辑的代码。
project root
└─── code
├── loader.py
└── index.py
└── pic
└── e2.jpg
e2.jpg 只是文章中提供的一个简单的数字 2 图片,如做验证性测试需要更多的素材可以通过 keras.js 平台手动绘制生成。
引入 initializer 接口
经过应用迁移处理后得到了一个可以运行在函数计算服务上的函数,很明显可以看到函数入口 loader.handler
中首先需要从 OSS 加载应用依赖(tensorflow、opencv)和资源依赖(模型),加载的过程都属于应用层冷启动,冷启动所耗费的时间在一定程度上和所需依赖的大小规格成正比。为避免后续处理逻辑受到应用层冷启动延时的影响,这里将加载依赖逻辑放入 initializer 函数中。
其中 index.py 文件保持不变,loader.py 文件需要进行如下改造:
- 添加 initializer 函数,initializer 入口便为 loader.initializer。
- 将对 download_and_unzip_if_not_exist 的调用从 handler 中更换到 initializer 函数中。
loader.py 经过改造后的代码如下:
# -*- coding:utf-8 -*-
import sys
import zipfile
import os
import oss2
import imp
import time
app_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)
def download_and_unzip_if_not_exist(objectKey, path, context):
creds = context.credentials
if (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: ' + objectKey
print 'path: ' + path
print 'endpoint: ' + endpoint
print '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)
if not os.path.exists(path):
os.mkdir(path)
print 'before unzipping ' + objectKey + ' ...'
start_unzip_time = time.time()
with zipfile.ZipFile(zipName, "r") as z:
z.extractall(path)
print 'unzipping done, used %s seconds...' % (time.time() - start_unzip_time)
def initializer(context):
if not local:
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)
def handler(event, context):
desc = None
fn, modulePath, desc = imp.find_module('index')
mod = imp.load_module('index', fn, modulePath, desc)
request_handler = getattr(mod, 'handler')
return request_handler(event, context)
部署
本地开发已经完成,下面借助阿里云函数计算的工具 fun 可以进行一键部署,Fun 是一个用于支持 Serverless 应用部署的工具,它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作,步骤如下:
- 去 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:
- AliyunOSSReadOnlyAccess
initializer: # 函数名
Type: 'Aliyun::Serverless::Function'
Properties:
Handler: loader.handler # 处理函数入口
Initializer: loader.initializer # initializer 入口
CodeUri: ./code/
Description: 'tensorflow application!'
Runtime: python2.7
MemorySize: 1024
Timeout: 300
InitializationTimeout: 60
EnvironmentVariables:
Bucket: test-bucket # 替换为自己的 oss bucket
Endpoint: 'https://oss-cn-hangzhou.aliyuncs.com' # 替换掉 OSS Endpoint
AppLibObject: applib.zip
AppLibDir: /tmp/applib
ModelObject: model_data.zip
ModelDir: /tmp/model
- 执行
fun deploy
会显示如下信息,部署成功后可到对应 region 下查看部署是否生效。
测试
功能测试
登陆函数计算 控制台 对应 region 下找到所创建的函数,连续执行两次查看执行结果如下。
- 首次执行
- 第二次执行
从以上图片可以看到首次函数执行时间为 3793ms,第二次函数执行时间为 810ms,执行结果都为 the predict is 2
,从执行结果可以确认函数执行正确,但性能真的提高了吗?下面会有简单的性能测试做对比。
性能测试
这里对改造前的函数做同样的测试:
- 首次执行
- 首次执行日志
- 第二次执行
从以上图片可以看到首次函数执行时间为 17506ms,第二次函数执行时间为 815ms,通过日志可以发现首次触发函数执行大约 13s 花费在加载模型和依赖库上,函数的执行时间会随着模型和依赖库规格的增大而增大。由此可见,initializer 函数的引入会使得函数实例在首次启动时规避冷启动开销,降低函数执行时间,提高函数性能,并且不会对后续的请求产生任何影响。
总结
通过将深度学习场景下规格较大的模型、依赖库的加载等初始化逻辑进行提取放到 initializer 函数中可以极大的提升函数性能,规避用户系统/函数升级带来的冷启动开销,帮助用户实现业务系统的热升级。
最后欢迎大家通过扫码加入我们用户群中,使用过程中有问题或者有其他问题可以在群里提出来。函数计算官网客户群(11721331)。
参考文章:
1 :Tensorflow MINIST数据模型的训练,保存,恢复和手写字体识别
2:函数计算部署机器学习遇到的问题和解法