作者:byronhe,腾讯 WXG 开发工程师

一、问题背景

随着深度学习的广泛应用,在搜索引擎/推荐系统/机器视觉等业务系统中,越来越多的深度学习模型部署到线上服务。

机器学习模型在离线训练时,一般要将输入的数据做特征工程预处理,再输入模型在 TensorFlow PyTorch 等框架上做训练。

1.常见的特征工程逻辑

常见的特征工程逻辑有:

  1. 分箱/分桶 离散化

  2. log/exp 对数/幂等 math numpy 常见数学运算

  3. 特征缩放/归一化/截断

  4. 交叉特征生成

  5. 分词匹配程度计算

  6. 字符串分隔匹配判断 tong

  7. 缺省值填充等

  8. 数据平滑

  9. onehot 编码,hash 编码等

这些特征工程代码,当然一般使用深度学习最主要的语言 python 实现。

二、业务痛点

离线训练完成,模型上线部署后,同样要用 C++ 重新实现 这些 python 的特征工程逻辑代码。

我们发现,“用 C++ 重新实现” 这个步骤,给实际业务带来了大量的问题:

  1. 繁琐,费时费力,极容易出现 python 和 C++ 代码不一致

  2. 不一致会直接影响模型在线上的效果,导致大盘业务指标不如预期,产生各种 bad case

  3. 不一致难以发现,无法测试,无法监控,经常要靠用户投诉反馈,甚至大盘数据异常才能发现

1. 业界方案

针对这些问题,我调研了这些业界方案:

《推荐系统中模型训练及使用流程的标准化》
https://www.infoq.cn/article/2E6LCqb1GeqFRAjkkjX3

《自主研发、不断总结经验,美团搜索推荐机器学习平台》
https://cloud.tencent.com/developer/article/1357309

《京东电商推荐系统实践》
https://www.infoq.cn/article/1OkKmb_gEYNR3YqC9RcW

“模型线上线下一致性问题对于模型效果非常重要,我们使用特征日志来实时记录特征,保证特征的一致性。这样离线处理的时候会把实时的用户反馈,和特征日志做一个结合生成训练样本,然后更新到模型训练平台上,平台更新之后在推送到线上,这样整个排序形成了一个闭环。”

总结起来,有几种思路:

  1. 在线特征存储起来给离线用

  2. 在线 C++ 代码编译成 so 导出给离线用

  3. 根据一份配置生成离线和在线代码

  4. 提取公共代码,加强代码复用,等软件工程手段,减少不一致

2. 自动翻译方案

(1) .已有方案的缺点

但这些思路都有各种缺点:

  1. 所有在线请求的所有特征,这个存储量数据量很大

  2. 算法改代码需要等待后台开发,降低了算法同学的工作效率

  3. 特征处理代码的复杂度转移到配置文件中,不一定能充分表达,而且配置格式增加学习成本

  4. 就这边真实离线特征处理代码来看,大部分代码都无法抽取出公共代码做复用。

(2). 翻译器

回到问题出发点考虑,显而易见,这个问题归根结底就是需要一个 “ python 到 c++ 的翻译器 ” 。

那其实 “翻译器 Transpiler ” ,和编译器解释器类似,也是个古老的热门话题了,比如 WebAssembly, CoffeeScript ,Babel ,
Google Closure Compiler,f2c

于是一番搜索,发现 python 到 C++ 的翻译器也不少,其中 Pythran 是新兴比较热门的开源项目。

于是一番尝试后,借助 pythran,我们实现了:

  1. 一条命令 全自动把 Python 翻译成等价 C++

  2. 严格等价保证改写,彻底消除不一致

  3. 完全去掉重新实现 这块工作量,后台开发成本降到 0 ,彻底解放生产力

  4. 算法同学继续使用纯 python,开发效率无影响,** 无学习成本 **

  5. 并能推广到其他需要 python 改写成后台 C++ 代码 的业务场景,解放生产力

三、pythran 的使用流程

(1). 安装

一条命令安装:

pip3 install pythran

(2). 写 Python 代码

下面这个 python demo,是 pythran 官方 demo

import math
import numpy as np def zero(n, m):
return [[0]*n for col in range(m)] #pythran export matrix_multiply(float list list, float list list)
def matrix_multiply(m0, m1):
new_matrix = zero(len(m0),len(m1[0]))
for i in range(len(m0)):
for j in range(len(m1[0])):
for k in range(len(m1)):
new_matrix[i][j] += m0[i][k]*m1[k][j]
return new_matrix #pythran export arc_distance(float[], float[], float[], float[])
def arc_distance(theta_1, phi_1, theta_2, phi_2):
"""
Calculates the pairwise arc distance
between all points in vector a and b.
"""
temp = (np.sin((theta_2-theta_1)/2)**2
+ np.cos(theta_1)*np.cos(theta_2) * np.sin((phi_2-phi_1)/2)**2)
distance_matrix = 2 * np.arctan2(np.sqrt(temp), np.sqrt(1-temp))
return distance_matrix #pythran export dprod(int list, int list)
def dprod(l0,l1):
"""WoW, generator expression, zip and sum."""
return sum(x * y for x, y in zip(l0, l1)) #pythran export get_age(int )
def get_age(age):
if age <= 20:
age_x = '0_20'
elif age <= 25:
age_x = '21_25'
elif age <= 30:
age_x = '26_30'
elif age <= 35:
age_x = '31_35'
elif age <= 40:
age_x = '36_40'
elif age <= 45:
age_x = '41_45'
elif age <= 50:
age_x = '46_50'
else:
age_x = '50+'
return age_x

(3). Python 转成 C++

一条命令完成翻译

pythran -e demo.py -o  demo.hpp

(4). 写 C++ 代码调用

pythran/pythonic/ 目录下是 python 标准库的 C++ 等价实现,翻译出来的 C++ 代码需要 include 这些头文件

写个 C++ 代码调用

#include "demo.hpp"
#include "pythonic/numpy/random/rand.hpp"
#include <iostream> using std::cout;
using std::endl; int main() {
pythonic::types::list<pythonic::types::list<double>> m0 = {{2.0, 3.0},
{4.0, 5.0}},
m1 = {{1.0, 2.0},
{3.0, 4.0}};
cout << m0 << "*" << m1 << "\n=\n"
<< __pythran_demo::matrix_multiply()(m0, m1) << endl
<< endl; auto theta_1 = pythonic::numpy::random::rand(3),
phi_1 = pythonic::numpy::random::rand(3),
theta_2 = pythonic::numpy::random::rand(3),
phi_2 = pythonic::numpy::random::rand(3);
cout << "arc_distance " << theta_1 << "," << phi_1 << "," << theta_2 << ","
<< phi_2 << "\n=\n"
<< __pythran_demo::arc_distance()(theta_1, phi_1, theta_2, phi_2) << endl
<< endl; pythonic::types::list<int> l0 = {2, 3}, l1 = {4, 5};
cout << "dprod " << l0 << "," << l1 << "\n=\n"
<< __pythran_demo::dprod()(l0, l1) << endl
<< endl; cout << "get_age 30 = " << __pythran_demo::get_age()(30) << endl << endl; return 0;
}

(5). 编译运行

g++ -g -std=c++11 main.cpp -fopenmp -march=native -DUSE_XSIMD -I /usr/local/lib/python3.6/site-packages/pythran/ -o pythran_demo

./pythran_demo

四、pythran 的功能与特性

(1). 介绍

按官方定义,Pythran 是一个 AOT (Ahead-Of-Time - 预先编译) 编译器。给科学计算的 python 加注解后,pythran 可以把 python 代码变成接口相同的原生 python 模块,大幅度提升性能。

并且 pythran 也可以利用 OpenMP 多核和 SIMD 指令集。

支持 python 3 和 Python 2.7 。

pythran 的 manual 挺详细:
https://pythran.readthedocs.io/en/latest/MANUAL.html

(2). 功能

pythran 并不支持完整的 python, 只支持 python 语言特性的一个子集:

  • polymorphic functions 多态函数(翻译成 C++ 的泛型模板函数)

  • lambda

  • list comprehension 列表推导式

  • map, reduce 等函数

  • dictionary, set, list 等数据结构

  • exceptions 异常

  • file handling 文件处理

  • 部分 numpy

不支持的功能:

  • classes 类

  • polymorphic variables 可变类型变量

(3). 支持的数据类型和函数

pythran export 可以导出函数和全局变量。
支持导出的数据类型,BNF 定义是:

    argument_type = basic_type
                  | (argument_type+)    # this is a tuple
                  | argument_type list    # this is a list
                  | argument_type set    # this is a set
                  | argument_type []+    # this is a ndarray, C-style
                  | argument_type [::]+    # this is a strided ndarray
                  | argument_type [:,...,:]+ # this is a ndarray, Cython style
                  | argument_type [:,...,3]+ # this is a ndarray, some dimension fixed
                  | argument_type:argument_type dict    # this is a dictionary     basic_type = bool | byte | int | float | str | None | slice
               | uint8 | uint16 | uint32 | uint64 | uintp
               | int8 | int16 | int32 | int64 | intp
               | float32 | float64 | float128
               | complex64 | complex128 | complex256

可以看到基础类型相当全面,支持各种 整数,浮点数,字符串,复数

复合类型支持 tuple, list, set, dict, numpy.ndarray 等,

对应 C++ 代码的类型实现在 pythran/pythonic/include/types/ 下面,可以看到比如 dict 实际就是封装了一下 std::unordered_map
https://pythran.readthedocs.io/en/latest/SUPPORT.html
可以看到支持的 python 基础库,其中常用于机器学习的 numpy 支持算比较完善。

五、pythran 的基本原理

和常见的编译器/解释器类似, pythran 的架构是分成 3 层:

  1. python 代码解析成抽象语法树 AST 。用 python 标准库自带的的 ast 模块实现

  2. 代码优化。
    在 AST 上做优化,有多种 transformation pass,比如 deadcode_elimination 死代码消除,loop_full_unrolling 循环展开 等。还有 Function/Module/Node 级别的 Analysis,用来遍历 AST 供 transformation 利用。

  3. 后端,实现代码生成。目前有 2 个后端,Cxx / Python, Cxx 后端可以把 AST 转成 C++ 代码( Python 后端用来调试)。

目前看起来 ,pythran 还欠缺的:

  1. 字符串处理能力欠缺,缺少 str.encode()/str.decode() 对 utf8 的支持

  2. 缺少正则表达式 regex 支持

看文档要自己加也不麻烦,看业务需要可以加。

还能这样?把 Python 自动翻译成 C++的更多相关文章

  1. python打包成exe

    目前有三种方法可以实现python打包成exe,分别为 py2exe Pyinstaller cx_Freeze 其中没有一个是完美的 1.py2exe的话不支持egg类型的python库 2.Pyi ...

  2. 关于python打包成exe的一点经验之谈

    我经常用python写些脚本什么的,有时候脚本写完以后,每次运行都得在IDE打开在运行,很麻烦,所以经常将python编译成exe.SO...有了一点经验,在这和大家分享一下.      python ...

  3. Python打包成exe可执行文件

    Python打包成exe可执行文件 安装pyinstaller pyinstaller打包机制 Pyinstaller打包exe 总结命令 可能会碰到的一些常见问题 我们开发的脚本一般都会用到一些第三 ...

  4. Centos6.5 python升级成2.7版本出现的一些问题解决方法

    由于功能及程序依赖,需要将Centos上的python从2.6升级成2.7,把碰到的一些问题记录如下: 安装好2.7后将原来的/usr/bin/python改成/usr/bin/python26,并将 ...

  5. Python打包成exe,pyc

    D:\mypython\path\ C:\Python27\Scripts\pyinstaller.exe -w mypython.py # Python打包成exe D:\mypython\path ...

  6. python改成了python3的版本,那么这时候yum就出问题了

    既然把默认python改成了python3的版本,那么这时候yum就出问题了,因为yum貌似不支持python3,开发了这个命令的老哥也不打算继续写支持python3的版本了,所以,如果和python ...

  7. Python打包成exe,文件太大问题解决办法

    Python打包成exe,文件太大问题解决办法 原因 解决办法 具体步骤 情况一:初次打包 情况二:再次打包 原因 由于使用pyinstaller打包.py文件时,会把很多已安装的无关库同时打包进去, ...

  8. pyinstaller打包python文件成exe(原理.安装.问题)

    py文件打包成exe文件的方式一共有三种:py2exe.PyInstaller和cx_Freeze 本文分四个步骤来详讲如何用PyInstaller将py文件打包成exe文件 1. PyInstall ...

  9. python打包成exe,太大了该怎么解决?

    这是一个很长的故事,嫌长的直接看最后的结论 事情经过 上周接了个需求,写了个小工具给客户,他要求打包成exe文件,这当然不是什么难事.因为除了写Python的,绝大多数人电脑里都没有Python编译器 ...

随机推荐

  1. LeetCode-071-简化路径

    简化路径 题目描述:给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径. 在 Unix 风格的文件系统中,一个点(. ...

  2. ElasticSearch 分布式及容错机制

    1 ElasticSearch分布式基础 1.1 ES分布式机制 分布式机制:Elasticsearch是一套分布式的系统,分布式是为了应对大数据量.它的特性就是对复杂的分布式机制隐藏掉. 分片机制: ...

  3. TypeScript 2.0开启空值的严格检查

    摘要:在编程过程成空指针是最常见的bug之一,但是在TypeScript中我们无法使用具体的类型来表示特定的变量不能为空!幸运的是,TypeScript 2.0 解决了这个问题. 本文分享自华为云社区 ...

  4. 理解并手写 apply() 函数

    apply()函数,在功能上类似于call(),只是传递参数的格式有所不同. dog.eat.call(cat, '鱼', '肉'); dog.eat.apply(cat, ['鱼', '肉']); ...

  5. [] == ![] 返回 true

    对于==来说,如果数据类型不同,就会进行隐式类型转换. 首先判断是否在对比 null 和 undefined,是的话就会返回 true: 判断其中一方是否为 string ,在与 number进行比较 ...

  6. 万字长文---关于PKM收集与整理系统的思考和实践

    PKM闭环中有一个很重要的环节就是信息输入,包括各种信息来源,例如微信公众号.博客.知乎.RSS等等,因此也就诞生了一大堆稍后读软件,如何真正有效的获取输入而不是做一只仓鼠是需要思考的.最近看了< ...

  7. UOJ188题解

    我们先枚举一个最大质因子,然后设 \(dp[n][k]\) 为 \(n\) 以内使用了 \(pri[k]\) 以内的质数的数的最大质因子之和,答案就是: \[\sum_{k\leq n}dp[\lfl ...

  8. LGP4216题解

    这是一种题解没有的 \(O(m\log n)\) 做法. 首先第一步转化.设这是第 \(x\) 个任务,若 \(opt\) 为 \(1\),危险值大于 \(c\) 的只有可能在第 \(x-c-1\) ...

  9. tomcat manager status配置

    1. 确保tomcat下原来自带的几个项目未被删掉,tomcat启动时localhost:8080能直接访问tomcat主页 2. 修改tomcat下 conf/tomcat-users-xml文件, ...

  10. 后门及持久化访问2----进程注入之AppCertDlls 注册表项

    代码及原理介绍 如果有进程使用了CreateProcess.CreateProcessAsUser.CreateProcessWithLoginW.CreateProcessWithTokenW或Wi ...