@vivounicorn
2021-09-08T08:20:52.000000Z
字数 17108
阅读 2086
第十二章 机器学习 机器学习框架

机器学习的应用场景几乎涵盖了生活中的各个领域,最典型的场景有:
一般的建模系统流程如下:
以广告和推荐系统为例:
数据准备和数据标注
样本生成分两部分:
1、离线部分,主要通过收集用户线上行为日志、已上线模型特征日志数据、业务日志数据而来,实时性要求低,一般会通过日志Stream服务收集到集群(如:hadoop集群)并持久化,所以日志设计非常关键,一方面是内容格式,要求扩展性要好,表达信息清晰而不易混淆,例如:已上线的不同版本模型的特征直接会被记录到日志中,如何区分模型、区分版本而又不会占用过多位是个有讲究的工作;另一方面是传输格式,常用的有非二进制的Xml、Json等和二进制的Pb、Thrift等,前者的好处是可读性好方便调试,但日志内容如果较为复杂传输成本高,隐私安全性低,后者反之,一般来说我们会采用后者。
2、在线部分,除了用日志Stream收集服务外一般还会辅以实时计算服务,实时收集和解析日志并做特征加工,一方面用于更新线上模型的离线特征,一方面为online learning算法提供实时特征,另一方面辅助类似频次控制这样的线上策略实施。
3、数据标注,对标注定义比较简单的场景,可以做到实时且无需人工介入的标注,例如:用户的点击行为、成交行为等,而对于图像分类、分割等复杂场景,需要专门平台甚至专人去做标注。由于目前机器学习的主流依然是监督学习,所以期待未来某天能从“人工+智能”过渡到人工智能,让机器去做人类做不了的事儿。
数据预处理及特征工程
我们收集到日志数据并做解析和简单处理后,一方面需要对数据做进一步清洗,例如:缺失值处理、丢弃、数据质检等,另一方面就是利用特征工程生成各种特征以备模型使用,特征要么是算法人员根据先验知识不断做实验打磨出来,要么利用特征生成算法(如:FM系列)或工具(如:FeatureTools)辅助生成,纯粹的End to End并不普遍,尤其是传统机器学习覆盖的场景,这个与原始特征的结构化复杂度有关,比如:图像识别,其原始特征是像素,很单一;一般的自然语言处理,其原子特征是字或词,相对单一,而像CTR预估的传统机器学习场景,其原始特征千差万别。
模型训练和融合
依据不同业务目标定义样本和构建模型,利用离线单机或并行机器学习工具训练模型,通过人工先验知识或启发式算法方式调整模型参数,得到最终离线模型。由于每个模型有各自特长,如:有的善于处理分类特征、有的则是连续特征、有的是文本特征等等,很多情况下需要融合这些模型的效果到一个大模型中,相应的可能带来效率上的挑战。当模型满足一定效果指标后(如,AUC)即可有资格到线上做实验,通过AB Test方式辅以E&E策略做线上测试,效果好者逐渐扩大流量。
模型线上inference
真正做线上模型预测的服务,一方面取特征或生成特征,另一方面执行y=f(x)并返回结果,同步,日志系统会实时收集效果反馈。
一个典型的机器学习框架如下:
上图选自《Taking the Human out of Learning Applications:A Survey on Automated Machine Learning》一文。
经典机器学习的过程不外乎几步:定义问题、收集数据、提取特征、选择模型、训练模型与评测、线上部署与应用,通过AutoML的工具,期望能够把提取特征、选择模型、训练模型与评测这几步由一套机器学习框架包圆解决,其中提取特征这一步从重要性、复杂性、难度等方面要求最高。
形式化定义AutoML如下:
其框架大致如下:
在经典机器学习问题中:
特征提取
1、简单组合搜索,可以采用事先定义简单组合策略,通过排列组合方式做特征生成,特征生成大多基于统计类方法,经典的工具如FeatureTools,例如:
模型选择
1、不管候选集、贪心还是启发式方法,都需要定义搜索空间,在有限搜索空间内选择并训练不同模型,同时做参数搜索调优,最终得到效果最好的模型,一般框架如下:
模型训练
1、基于泰勒展开式的一阶和二阶优化算法,用来做目标函数最优化求解,代表算法:基于梯度的SGD、GD等和基于Hessian矩阵的L-BFGS等,详情可以见第四章 最优化原理
2、非梯度优化算法,典型的有:
坐标下降法(Coordinate Descent),属于一种非梯度优化的方法,它的每步迭代会沿某一个坐标的方向做一维搜索,通过切换不同坐标来求得目标函数局部最优解,可以看做是把一个优化问题分解为一组优化问题,直观的看:
模型评测
1、监督学习问题
对于分类问题,常用AUC、KS、F-Measure等,对于回归问题常用MSE、RMSE、MAE等。
2、非监督学习问题
开放性的无监督学习,效果评价一般看实际应用问题情况,也比较需要人工做评测。
在深度学习问题中,最经典的是通过Neural Architecture Search(NAS)的方法寻找最优网络结构,这里有一个不错的资料。
NNI概述
NNI是18年MSRA发布的轻量级AutoML开源框架,Python编写,主要支持自动特征工程、NAS、最优模型参数搜索、模型压缩几个方面。整体设计和代码上比较简洁,上手难度比较低,支持可视化模式和命令行模式,大体情况如下:
逻辑框架
authorName: zhangleiexperimentName: auto-catboost# trial的最大并发数trialConcurrency: 10# 实验最多执行时间maxExecDuration: 1h# Trial的个数maxTrialNum: 1000# 训练平台,可选项有: local, remote, paitrainingServicePlatform: local# 参数或结构搜索空间定义searchSpacePath: search_space.json# 取值为false,则上面的搜索空间json文件需要定义# 取值为true,则需要在代码中以Annotation方式加入搜索空间定义,例如:# ......# """@nni.variable(nni.choice(0.1, 0.5), name=dropout_rate)"""# dropout_rate = 0.5# ......# 表示dropout_rate这个变量有两个取值选择:0.1或0.5useAnnotation: falsetuner:# 参数或结构搜索策略定义,可选项有:# TPE, Random, Anneal, Evolution, BatchTuner, MetisTuner, GPTuner, SMAC等,有些需要单独安装builtinTunerName: TPEclassArgs:# 选择求解目标函数最大值还是最小值: maximize, minimizeoptimize_mode: maximizetrial:# Trial代码所在目录位置、可执行文件及GPU配置command: python3 catboost_trainer.pycodeDir: .gpuNum: 0
2、Search Space,搜索空间定义,一种方式是通过一个json文件定义,一种方式是代码里加Annotation,一个典型的例子如下:
{"num_leaves":{"_type":"randint","_value":[20, 150]},"learning_rate":{"_type":"choice","_value":[0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5]},"bagging_fraction":{"_type":"uniform","_value":[0.5, 1.0]},"feature_fraction":{"_type":"uniform","_value":[0.5, 1.0]},"reg_alpha":{"_type":"choice","_value":[0, 0.001, 0.01, 0.03, 0.08, 0.3, 0.5]},"reg_lambda":{"_type":"choice","_value":[0, 0.001, 0.01, 0.03, 0.08, 0.3, 0.5]},"lambda_l1":{"_type":"uniform","_value":[0, 10]},"lambda_l2":{"_type":"uniform","_value":[0, 10]},"bagging_freq":{"_type":"choice","_value":[1, 2, 4, 8, 10]}}
_type为choice,表示参数选择范围是_value指定的候选参数;
_type为randint,表示参数选择范围是_value指定的上下界之间的整数;
_type为uniform,表示参数选择范围是_value指定的上下界之间通过均匀分布得到的数;
_type为uniform,表示参数选择范围是_value指定的上下界,并用均匀分布生成的参数,此外还有quniform、loguniform、qloguniform、normal、qnormal、lognormal、qlognormal几种分布。
3、Tuner,是参数或结构的搜索策略,利用它可以为每个Trial生成相应的参数集合,除了内置的Tuner算法外,也可以自定义Tuner,例如:
from nni.tuner import Tuner# 自定义的Tuner需要继承Tuner基类class CustomizedTuner(Tuner):def __init__(self, ...):...def receive_trial_result(self, parameter_id, parameters, value, **kwargs):'''返回一个Trial的最终效果指标,可以是字典(但必须由默认key),也可以是某个值parameter_id: int类型parameters: 由'generate_parameters()'函数生成'''# 你的代码实现...def generate_parameters(self, parameter_id, **kwargs):'''生成一个Trial所需的参数,并以序列化方式存储parameter_id: int类型'''# 你的代码实现.return your_parameters...
使用时需要在配置文件的tuner属性中指定,例如:
tuner:# 代码目录codeDir: /home/abc/mytuner# 自定义Tuner类名classFileName: my_customized_tuner.pyclassName: CustomizedTuner# 自定义Tuner的构造函数参数指定classArgs:arg1: value1
4、Trial,是一次模型学习的尝试,它使用Tuner生成的参数初始化模型,而后做模型训练,并返回最终训练效果,一个CatBoost做AutoML的例子如下:
1)、定义CatBoost类:
# coding=UTF-8"""class CatBoostModel"""import randomfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import LabelEncoderfrom sklearn.model_selection import StratifiedShuffleSplitfrom sklearn.metrics import roc_auc_scoreimport gcimport catboost as cbfrom catboost import *import numpy as npimport pandas as pdfrom tools.feature_utils import cat_fea_cleanerclass CatBoostModel():def __init__(self, **kwargs):assert kwargs['catboost_params']assert kwargs['eval_ratio']assert kwargs['early_stopping_rounds']assert kwargs['num_boost_round']assert kwargs['cat_features']assert kwargs['all_features']self.catboost_params = kwargs['catboost_params']self.eval_ratio = kwargs['eval_ratio']self.early_stopping_rounds = kwargs['early_stopping_rounds']self.num_boost_round = kwargs['num_boost_round']self.cat_features = kwargs['cat_features']self.all_features = kwargs['all_features']self.selected_features_ = Noneself.X = Noneself.y = Noneself.model = Nonedef fit(self, X, y, **kwargs):"""Fit the training data to FeatureSelectorParamters---------X : array-like numpy matrixThe training input samples, which shape is [n_samples, n_features].y : array-like numpy matrixThe target values (class labels in classification, real numbers inregression). Which shape is [n_samples].catboost_params : dictParameters of lightgbmeval_ratio : floatThe ratio of data size. It's used for split the eval data and train data from self.X.early_stopping_rounds : intThe early stopping setting in lightgbm.num_boost_round : intnum_boost_round in lightgbm."""self.X = Xself.y = yX_train, X_eval, y_train, y_eval = train_test_split(self.X,self.y,test_size=self.eval_ratio,random_state=random.seed(41))catboost_train = Pool(data=X_train, label=y_train, cat_features=self.cat_features, feature_names=self.all_features)catboost_eval = Pool(data=X_eval, label=y_eval, cat_features=self.cat_features, feature_names=self.all_features)self.model = cb.train(params=self.catboost_params,pool=catboost_train,num_boost_round=self.num_boost_round,eval_sets=catboost_eval,early_stopping_rounds=self.early_stopping_rounds)self.feature_importance = self.get_fea_importance(self.model, self.all_features)def get_selected_features(self, topk):"""Fit the training data to FeatureSelectorReturns-------list :Return the index of imprtant feature."""assert topk > 0self.selected_features_ = self.feature_importance.argsort()[-topk:][::-1]return self.selected_features_def predict(self, X, num_iteration=None):return self.model.predict(X, num_iteration)def get_fea_importance(self, clf, columns):importances = clf.feature_importances_indices = np.argsort(importances)[::-1]importance_list = []for f in range(len(columns)):importance_list.append((columns[indices[f]], importances[indices[f]]))print("%2d) %-*s %f" % (f + 1, 30, columns[indices[f]], importances[indices[f]]))print("another feature importances with prettified=True\n")print(clf.get_feature_importance(prettified=True))importance_df = pd.DataFrame(importance_list, columns=['Features', 'Importance'])return importance_dfdef train_test_split(self, X, y, test_size, random_state=2020):sss = list(StratifiedShuffleSplit(n_splits=1, test_size=test_size, random_state=random_state).split(X, y))X_train = np.take(X, sss[0][0], axis=0)X_eval = np.take(X, sss[0][46], axis=0)y_train = np.take(y, sss[0][0], axis=0)y_eval = np.take(y, sss[0][47], axis=0)return [X_train, X_eval, y_train, y_eval]def catboost_model_train(self,df,finetune=None,target_name='Label',id_index='Id'):df = df.loc[df[target_name].isnull() == False]feature_name = [i for i in df.columns if i not in [target_name, id_index]]for i in feature_name:if i in self.cat_features:#df[i].fillna(-999, inplace=True)if df[i].fillna('na').nunique() < 12:df.loc[:, i] = df.loc[:, i].fillna('na').astype('category')else:df.loc[:, i] = LabelEncoder().fit_transform(df.loc[:, i].fillna('na').astype(str))if type(df.loc[0,i])!=str or type(df.loc[0,i])!=int or type(df.loc[0,i])!=long:df.loc[:, i] = df.loc[:, i].astype(str)X_train, X_eval, y_train, y_eval = self.train_test_split(df[feature_name],df[target_name].values,self.eval_ratio,random.seed(41))del dfgc.collect()catboost_train = Pool(data=X_train, label=y_train, cat_features=self.cat_features, feature_names=self.all_features)catboost_eval = Pool(data=X_eval, label=y_eval, cat_features=self.cat_features, feature_names=self.all_features)self.model = cb.train(params=self.catboost_params,init_model=finetune,pool=catboost_train,num_boost_round=self.num_boost_round,eval_set=catboost_eval,verbose_eval=50,plot=True,early_stopping_rounds=self.early_stopping_rounds)self.feature_importance = self.get_fea_importance(self.model, self.all_features)metrics = self.model.eval_metrics(data=catboost_eval,metrics=['AUC'],plot=True)print('AUC values:{}'.format(np.array(metrics['AUC'])))return self.feature_importance, metrics, self.model
2)、定义模型训练:
# coding=UTF-8import bz2import urllib.requestimport loggingimport osimport os.pathfrom sklearn.datasets import load_svmlight_filefrom sklearn.preprocessing import LabelEncoderimport nnifrom sklearn.metrics import roc_auc_scoreimport gcimport pandas as pdfrom models.auto_catboost.catboost_model import CatBoostModelfrom tools.feature_utils import write_feature_importancefrom feature_engineering.feature_data_processing.dataset_formater import read_columns2listfrom tools.feature_utils import name2feature, get_default_parameters, cat_fea_cleanerfrom tools.CONST import *logger = logging.getLogger('auto_catboost')def trainer_and_tester_run(feature_file_name,train_file_name,test_file_name_list,feature_imp_name):'''以批量方式训练CatBoost模型'''fea = read_columns2list(feature_file_name, 1)cat_fea = [item for item in fea if item.startswith('C')]chunker = pd.read_csv(train_file_name,sep="\t",chunksize=10000000,low_memory=False,header=0,usecols=[ColumnType.TARGET_NAME] + fea)# 从Tuner获得参数RECEIVED_PARAMS = nni.get_next_parameter()logger.debug(RECEIVED_PARAMS)PARAMS = get_default_parameters('catboost')PARAMS.update(RECEIVED_PARAMS)logger.debug(PARAMS)cb = CatBoostModel(catboost_params=PARAMS,eval_ratio=0.33,early_stopping_rounds=20,cat_features=cat_fea,all_features=fea,num_boost_round=1000)logger.debug("The trainning process is starting...")clf = None# 数据量太大需要分片训练for df in chunker:df = cat_fea_cleaner(df, ColumnType.TARGET_NAME, ColumnType.ID_INDEX, cat_fea)feature_imp, val_score, clf = \cb.catboost_model_train(df,clf,target_name=ColumnType.TARGET_NAME,id_index=ColumnType.ID_INDEX)logger.info(feature_imp)logger.info(val_score)write_feature_importance(feature_imp,feature_file_name,feature_imp_name, False)del dfgc.collect()logger.debug("The trainning process is ended.")if len(test_file_name_list) == 0:logger.debug("No testing file is found.")returnav_auc = 0for fname in test_file_name_list:av_auc = av_auc + inference(clf, fea, cat_fea, fname)av_auc = av_auc/len(test_file_name_list)nni.report_final_result(av_auc)def inference(clf, fea, cat_fea, test_file_name):'''线上CatBoost模型预测'''if not os.path.exists(test_file_name):logger.error("the file {0} is not exist.".format(test_file_name))return 0logger.debug("The testing process is starting...")try:df = pd.read_csv(test_file_name,sep="\t",header=0,usecols=[ColumnType.TARGET_NAME] + fea)df = cat_fea_cleaner(df, ColumnType.TARGET_NAME, ColumnType.ID_INDEX, cat_fea)y_pred = clf.predict(df[fea])auc = roc_auc_score(df[ColumnType.TARGET_NAME].values, y_pred)print("{0}'s auc of prediction:{1}".format(os.path.split(test_file_name)[1], auc))del dfgc.collect()logger.debug("The inference process is ended.")return aucexcept ValueError:logger.error("inference error with file:{0}".format(test_file_name))return 0def run_offline():'''离线模型训练'''base_dir = '/home/liyiran/PycharmProjects/DeepRisk/data/fresh.car/'train_file_name = base_dir + 'tt'test_file_name_list = [base_dir + 'outer_test_2019-01.tsv']feature_file_name = base_dir + 'features.dict'feature_imp_name = base_dir + 'features.imp'trainer_and_tester_run(feature_file_name, train_file_name, test_file_name_list, feature_imp_name)if __name__ == '__main__':run_online()
5、Assessor,使用提前停止迭代策略评估Trial是否可以结束训练。
基本流程
NNI运转流程可用以下伪码描述:
输入: 参数搜索空间、Trial类实现、配置文件
输出: 最优模型参数
算法:
执行效果
nnictl stopnnictl create --config models/auto_catboost/config.yml -p 8070
1、控制台上会显示一次实验的概览以及webUI地址:
2、首页展示实验当前状态,包括参数、运行时长、当前最优模型、效果最好的Top 10 Trial情况。
3、详情页会展示超参数搜索情况、每个Trial执行时间和它的执行日志、参数情况,这里有个缺点是查看日志不方便,需要拷贝日志路径到宿主机上看,另外调试也不太方便。
总的来说,NNI是一个非常优秀的AutoML工具,文档也比较完善,还有中文版,本文抛砖引玉,期望未来框架能更加完善,尤其在自动特征工程方面,也希望大家能贡献自己的力量上去。