一、问题背景
随着深度学习的广泛应用,在搜索引擎/推荐系统/机器视觉等业务系统中,越来越多的深度学习模型部署到线上服务。
机器学习模型在离线训练时,一般要将输入的数据做特征工程预处理,再输入模型在 TensorFlow PyTorch 等框架上做训练。
1.常见的特征工程逻辑
常见的特征工程逻辑有:
- 分箱/分桶 离散化
- log/exp 对数/幂等 math numpy 常见数学运算
- 特征缩放/归一化/截断
- 交叉特征生成
- 分词匹配程度计算
- 字符串分隔匹配判断 tong
- 缺省值填充等
- 数据平滑
- onehot 编码,hash 编码等
这些特征工程代码,当然一般使用深度学习最主要的语言 python 实现。
二、业务痛点
离线训练完成,模型上线部署后,同样要用 C++ 重新实现 这些 python 的特征工程逻辑代码。
我们发现,“用 C++ 重新实现” 这个步骤,给实际业务带来了大量的问题:
- 繁琐,费时费力,极容易出现 python 和 C++ 代码不一致
- 不一致会直接影响模型在线上的效果,导致大盘业务指标不如预期,产生各种 bad case
- 不一致难以发现,无法测试,无法监控,经常要靠用户投诉反馈,甚至大盘数据异常才能发现
1. 业界方案
针对这些问题,我调研了这些业界方案:
《推荐系统中模型训练及使用流程的标准化》
https://www.infoq.cn/article/2E6LCqb1GeqFRAjkkjX3
《自主研发、不断总结经验,美团搜索推荐机器学习平台》
https://cloud.tencent.com/developer/article/1357309
《京东电商推荐系统实践》
https://www.infoq.cn/article/1OkKmb_gEYNR3YqC9RcW
“模型线上线下一致性问题对于模型效果非常重要,我们使用特征日志来实时记录特征,保证特征的一致性。这样离线处理的时候会把实时的用户反馈,和特征日志做一个结合生成训练样本,然后更新到模型训练平台上,平台更新之后在推送到线上,这样整个排序形成了一个闭环。”
总结起来,有几种思路:
- 在线特征存储起来给离线用
- 在线 C++ 代码编译成 so 导出给离线用
- 根据一份配置生成离线和在线代码
- 提取公共代码,加强代码复用,等软件工程手段,减少不一致
2. 自动翻译方案
(1). 已有方案的缺点
但这些思路都有各种缺点:
- 所有在线请求的所有特征,这个存储量数据量很大
- 算法改代码需要等待后台开发,降低了算法同学的工作效率
- 特征处理代码的复杂度转移到配置文件中,不一定能充分表达,而且配置格式增加学习成本
- 就这边真实离线特征处理代码来看,大部分代码都无法抽取出公共代码做复用。
(2). 翻译器
回到问题出发点考虑,显而易见,这个问题归根结底就是需要一个 “ python 到 c++ 的翻译器 ” 。
那其实 “翻译器 Transpiler ” ,和编译器解释器类似,也是个古老的热门话题了,比如 WebAssembly, CoffeeScript ,Babel ,
Google Closure Compiler,f2c
于是一番搜索,发现 python 到 C++ 的翻译器也不少,其中 Pythran 是新兴比较热门的开源项目。
于是一番尝试后,借助 pythran,我们实现了:
- 一条命令 全自动把 Python 翻译成等价 C++
- 严格等价保证改写,彻底消除不一致
- 完全去掉重新实现 这块工作量,后台开发成本降到 0 ,彻底解放生产力
- 算法同学继续使用纯 python,开发效率无影响,无学习成本
- 并能推广到其他需要 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 层:
- python 代码解析成抽象语法树 AST 。用 python 标准库自带的的 ast 模块实现
- 代码优化。
在 AST 上做优化,有多种 transformation pass,比如 deadcode_elimination 死代码消除,loop_full_unrolling 循环展开 等。还有 Function/Module/Node 级别的 Analysis,用来遍历 AST 供 transformation 利用。 - 后端,实现代码生成。目前有 2 个后端,Cxx / Python, Cxx 后端可以把 AST 转成 C++ 代码( Python 后端用来调试)。
目前看起来 ,pythran 还欠缺的:
- 字符串处理能力欠缺,缺少 str.encode()/str.decode() 对 utf8 的支持
- 缺少正则表达式 regex 支持
看文档要自己加也不麻烦,看业务需要可以加。
文章来源: 腾讯技术工程
你可能还喜欢下面这些文章
语法高亮是文本编辑器用来显示文本的,特别是源代码,根据不同的类别来用不同的颜色和字体显示。这个功能有助于编写结构化的语言,比如编程语言,标记语言,这些语言的语法错误显示是有区别的。语法高亮并不会影响文本自身的意义,而且能很好的符合人们的阅读习惯。语法高亮同时也能帮助开发者很快的找到他们程序中的错误。例如,大部分编辑器会用不同的颜色突出字符串常量。所以,非常容易发现是否遗漏了分隔符,因为相对于其他文本颜色不同。现 在有各种各样的语法高亮工具,可以格式化语言,并且根据不同的编程语言进行高亮显示。无论是个 HTML 页面还是 PHP,Ruby,Python 或者是 ASP。这篇文章中,我们会介绍 1
汉语词性对照表词性编码词性名称注 解Ag形语素形容词性语素。形容词代码为 a,语素代码g前面置以A。a形容词取英语形容词 adjective的第1个字母。ad副形词直接作状语的形容词。形容词代码 a和副词代码d并在一起。an名形词具有名词功能的形容词。形容词代码 a和名词代码n并在一起。b区别词取汉字“别”的声母。c连词取英语连词 conjunction的第1个字母。dg副语素副词性语素。副词代码为 d,语素代码g前面置以D。d副词取 adverb的第2个字母,因其第1个字母已用于形容词。e叹词取英语叹词 exclamation的第1个字母。f方位词取汉字“方”g语素绝大多数语素都能作为合成词
特征工程是数据分析中最耗时间和精力的一部分工作,它不像算法和模型那样是确定的步骤,更多是工程上的经验和权衡。因此没有统一的方法。这里只是对一些常用的方法做一个总结。本文关注于特征选择部分。后面还有两篇会关注于特征表达和特征预处理。1. 特征的来源在做数据分析的时候,特征的来源一般有两块,一块是业务已经整理好各种特征数据,我们需要去找出适合我们问题需要的特征;另一块是我们从业务特征中自己去寻找高级数据特征。我们就针对这两部分来分别讨论。2. 选择合适的特征我们首先看当业务已经整理好各种特征数据时,我们如何去找出适合我们问题需要的特征,此时特征数可能成百上千,哪些才是我们需要的呢?第一
一、any容器是什么?1、any“不是”模板类,any是一种很特殊的容器。2、any只能容纳一个元素,但这个元素可以是任意的类型,可以是基本数据类型(int、double、string、标准容器或者任何自定义类型)。3、一种动态(类型检查只发生在运行时)语言特性的数据结构。4、C++17引入,需要RIIT支持,VS默认是没有支持C++17的,需要自己修改设置,如果不能使用any,请修改标准。二、any类摘要C++typeid关键字详解:三、any类用法注意:any的析构函数删除内部holder对象。如果类型是指针,any并不会对指针执行delete操作,所有any保存原始指针对造成内存泄漏。完
C++中zlib的crc32和python zlib.crc32结果不一致的解决方案
背景python和c++的代码中均有使用crc32分流的操作,需要保证分流得到的结果一致,那么两个crc32的方法得到的结果需要一致才行。然而实际测试中发现python2中zlib.crc32和c++的zlib中crc32得到的结果却不一致。问题复现python版crc32结果为 -102031187。如下:C++ 版zlib crc32运行结果为:4192936109python版得出的结果是-102031187,而C++版本得出的结果是4192936109。资料查找首先从百度上查看有没有人和我遇到同样的问题,结果发现有。但是回答的结果乱七八糟,没什么参考价值。于是找了找python的文档,
项目做多了之后,可能会慢慢总结出自己的代码库出来,当在新的项目中使用的时候,总不能一直是复制粘贴。这个时候,composer就能派上用场了。一个项目开始,使用composer就能够加载所需要的依赖,非常方便。这个时候,来做一个自己的包吧!使用命名空间composer自动加载需要用到命名空间,因此所有的代码库都需要使用命名空间,如果没有,那就改吧!使用命名空间之后你可能会打开新世界的大门。创建composer.json我假设你已经安装了composer,并且已经会使用了。创建自己的包我们首先需要创建一个composer.json,示例文件如下上面的composer.json有一个比较重要的是au
作为代码中,第一个看到的,极有可能就是define这个东西,称为宏!(define是可以出现在任何地方的,但是我们一般把这个写到最开始)然而,很多时候,初学者有时候可能看不懂她,因此,我的c语言学习的第一篇就写这个啦。define基本用法,简单定义最浅显的,define能用一个有含义的字符来替代一些数字,比如#define PI 3.141592654这样,假如以后要计算圆的周长或者面积,就可以用PI这个字符而不用写3.141592654啦。比如#define PI 3.141592654#include "stdio.h"int main(){ int r = 3; float
一个将网页里面的(图片,链接地址)相对路径转化为绝对路径的php实现方法
抓取网页的时候有时候会需要下载里面的图片或者其他附件,但有的网页里面用的是相对路径,这时候就要转化为绝对路径。 /*** url补全,相对url转化为绝对url* 作用是补全url*/function url2abs($srcurl,$baseurl){ $srcinfo = parse_url($srcurl); //print_r($srcinfo); if(isset($srcinfo)) { return $srcurl; } $baseinfo = parse_url($baseurl); $url = $baseinfo.'://'.$basein
原本我想要写造福后人,请为你的程序编写单元测试吧。突然觉得这样写会不会太高尚了一些,想想还是不给以后的自己找麻烦,编写单元测试比较好。一直都在隐式的做着单元测试你可能没有听过单元测试,或者听过,但没有使用单元测试框架来做单元测试,又或者对单元测试框架不屑一顾等等...(等等,这不是在说我自己嘛)好了,在你没有使用单元测试框架之前,其实你一直在不知不觉中使用单元测试。比如,当你写完了一个方法的时候,你会在下面调用这个方法,然后看看运行的结果,哦,对了,要的就是这个结果。这就是一个单元测试啊!你对这个方法做了一个测试,传n个参数进入,输出一个结果,结果与预期一致,通过;结果与预期不一致,失败,de
来说说php的empty,isset,is_null 与!,这几个都是if语句中比较常见的判断逻辑。但是有时候用的很纠结,甚至看别人写的程序里面也很纠结。特地梳理梳理,避免踩坑先来定义一些东西<?php$a;$b=0;$c=array();$d='';$e=null;empty,用了会上瘾这是一个用了会上瘾的语言结构!多好,empty可接受的参数是一个变量,任意类型,哪怕是变量不存在,只要变量被boolean转换之后是false(参考:php的boolean都有哪些),那么empty返回的就是false,并且不会出现警告!等价于不过注意的是,empty里面不能使用表达式(在php<
赞赏微信赞赏支付宝赞赏