還能這樣?把Python自動翻譯成C++

一、問題背景

隨着深度學習的廣泛應用,在搜索引擎/推薦系統/機器視覺等業務系統中,越來越多的深度學習模型部署到線上服務。

機器學習模型在離線訓練時,一般要將輸入的數據做特徵工程預處理,再輸入模型在 TensorFlow PyTorch 等框架上做訓練。

1.常見的特徵工程邏輯

常見的特徵工程邏輯有:

  • 分箱/分桶 離散化
  • log/exp 對數/冪等 math numpy 常見數學運算
  • 特徵縮放/歸一化/截斷
  • 交叉特徵生成
  • 分詞匹配程度計算
  • 字符串分隔匹配判斷 tong
  • 缺省值填充等
  • 數據平滑
  • 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

“模型線上線下一致性問題對於模型效果非常重要,我們使用特徵日誌來實時記錄特徵,保證特徵的一致性。這樣離線處理的時候會把實時的用戶反饋,和特徵日誌做一個結合生成訓練樣本,然後更新到模型訓練平臺上,平臺更新之後在推送到線上,這樣整個排序形成了一個閉環。”

總結起來,有幾種思路:

  • 在線特徵存儲起來給離線用
  • 在線 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 層:

  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 支持

看文檔要自己加也不麻煩,看業務需要可以加。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章