@vivounicorn
2021-09-08T16:20:52.000000Z
字数 17108
阅读 1634
第十二章
机器学习
机器学习框架
机器学习的应用场景几乎涵盖了生活中的各个领域,最典型的场景有:
一般的建模系统流程如下:
以广告和推荐系统为例:
数据准备和数据标注
样本生成分两部分:
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: zhanglei
experimentName: auto-catboost
# trial的最大并发数
trialConcurrency: 10
# 实验最多执行时间
maxExecDuration: 1h
# Trial的个数
maxTrialNum: 1000
# 训练平台,可选项有: local, remote, pai
trainingServicePlatform: 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.5
useAnnotation: false
tuner:
# 参数或结构搜索策略定义,可选项有:
# TPE, Random, Anneal, Evolution, BatchTuner, MetisTuner, GPTuner, SMAC等,有些需要单独安装
builtinTunerName: TPE
classArgs:
# 选择求解目标函数最大值还是最小值: maximize, minimize
optimize_mode: maximize
trial:
# Trial代码所在目录位置、可执行文件及GPU配置
command: python3 catboost_trainer.py
codeDir: .
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.py
className: CustomizedTuner
# 自定义Tuner的构造函数参数指定
classArgs:
arg1: value1
4、Trial,是一次模型学习的尝试,它使用Tuner生成的参数初始化模型,而后做模型训练,并返回最终训练效果,一个CatBoost做AutoML的例子如下:
1)、定义CatBoost类:
# coding=UTF-8
"""
class CatBoostModel
"""
import random
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import StratifiedShuffleSplit
from sklearn.metrics import roc_auc_score
import gc
import catboost as cb
from catboost import *
import numpy as np
import pandas as pd
from tools.feature_utils import cat_fea_cleaner
class 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_ = None
self.X = None
self.y = None
self.model = None
def fit(self, X, y, **kwargs):
"""
Fit the training data to FeatureSelector
Paramters
---------
X : array-like numpy matrix
The training input samples, which shape is [n_samples, n_features].
y : array-like numpy matrix
The target values (class labels in classification, real numbers in
regression). Which shape is [n_samples].
catboost_params : dict
Parameters of lightgbm
eval_ratio : float
The ratio of data size. It's used for split the eval data and train data from self.X.
early_stopping_rounds : int
The early stopping setting in lightgbm.
num_boost_round : int
num_boost_round in lightgbm.
"""
self.X = X
self.y = y
X_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 FeatureSelector
Returns
-------
list :
Return the index of imprtant feature.
"""
assert topk > 0
self.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_df
def 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 df
gc.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-8
import bz2
import urllib.request
import logging
import os
import os.path
from sklearn.datasets import load_svmlight_file
from sklearn.preprocessing import LabelEncoder
import nni
from sklearn.metrics import roc_auc_score
import gc
import pandas as pd
from models.auto_catboost.catboost_model import CatBoostModel
from tools.feature_utils import write_feature_importance
from feature_engineering.feature_data_processing.dataset_formater import read_columns2list
from tools.feature_utils import name2feature, get_default_parameters, cat_fea_cleaner
from 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 df
gc.collect()
logger.debug("The trainning process is ended.")
if len(test_file_name_list) == 0:
logger.debug("No testing file is found.")
return
av_auc = 0
for 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 0
logger.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 df
gc.collect()
logger.debug("The inference process is ended.")
return auc
except ValueError:
logger.error("inference error with file:{0}".format(test_file_name))
return 0
def 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 stop
nnictl create --config models/auto_catboost/config.yml -p 8070
1、控制台上会显示一次实验的概览以及webUI地址:
2、首页展示实验当前状态,包括参数、运行时长、当前最优模型、效果最好的Top 10 Trial情况。
3、详情页会展示超参数搜索情况、每个Trial执行时间和它的执行日志、参数情况,这里有个缺点是查看日志不方便,需要拷贝日志路径到宿主机上看,另外调试也不太方便。
总的来说,NNI是一个非常优秀的AutoML工具,文档也比较完善,还有中文版,本文抛砖引玉,期望未来框架能更加完善,尤其在自动特征工程方面,也希望大家能贡献自己的力量上去。