机器学习趟水实战

目标

通过对机器学习实践过程中各个环节的小结,达到能够独立训练一个可用于生产环境的模型

jupyter

jupyter是基于ipython的交互式调试工具,优点包括

  • 代码实时修改实时生效执行,而且支持划分为多个模块
  • 可以直接看到程序输出,图片,表格等
  • 跨操作系统
jupyter notebook # 启动
jupyter

经验

  1. 要在浏览器直接输出图片需要添加代码 %matplotlib inline
  2. 浏览器执行的代码如果要本地文件IO则需要改变标准IO编码sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

特征工程

就目前经验来看遇到的一半的坑都是跟特征相关的,样本比例失调导致部分特征样本不足或过多、特征未归一化导致部分算法效果很差。算法可以都进行尝试,参数可以自动调优,只有特征工程环节需要投入大量精力进行准备,特征选择得当,针对算法预处理合适,结果将事半功倍。

特征选择

特征要尽量选择方差大的,所包含的信息量大,能够对样本能够有效划分。特征之间尽量保持独立。设计好特征后,利用自己熟悉的工具快速生成大量带标注的样本数据,比如

长度,数目,数字占比,标注
4,2,0.80,1
...

特征优化

得到标注的特征样本后其实就可以训练出一个差不多的模型了,不过为了模型的效果达到最优,还要对特征进行优化,比如特征数据的无量纲化、补全缺失值、无关特征精简等。

特征优化可以降低模型误差,加速最优解的迭代,减少计算量等。

分类变量

一些特征比如名称是否为高频词结尾,属于分类性质,在准备特征的时候可以输出‘是、否’,但是在训练模型的时候必须对其进行编码,比如用0和1。

但是当分类包含多个值,比如‘户籍’,可能的值为北京、上海、深圳等,此时虽然也可以用数字进行编码,比如用1、2、3来编码,但可能会误导算法,比如使用决策树算法的时候,该特征是否可以进行比较?

此时我们需要将分类特征泛化为0-1特征,比如‘户籍-北京’、‘户籍-上海’

demof_df = pd.DataFrame({
  'age': [21, 25, 23],
  'from': ['北京', '上海', '上海'],
})
pd.get_dummies(demo_df)

# 返回值为
# age from_北京 from_上海
# 21 1 0
# 25 0 1
# 23 0 1

特征离散化

特征的离散化也叫特征分箱(binning),有时候我们关注的是特征分档,而不希望具体的连续值导致模型模糊,此时需要对紧密度进行分箱

bins = numpy.linspace(0, 1, 4) 
# output: [0, 0.33, 0.66, 1]

which_bin = numpy.digitize(X, bins=bins)
# X: [0.2, 0.3, 0.5, 0.9]
# output: [1, 1, 2, 3]

缺失值补全

有时候样本特征是通过采集得来的,可能存在缺失值,此时需要对这些样本的缺失值进行补全。补全的策略需要根据具体的场景进行分析,常用的补全思想为

  • 平均值补全
  • 众数(出现频率最大)补全
  • 相邻前/后值补全

标准化 & 归一化

经验不足,理解的不深,标准化大概思想为将数据分布调整为标准正态分布,从而将特征矩阵压缩在同一量纲下。归一化的做法很简单,当前值与最小值求差再比上样本最大值与最小值的差,取值在0-1,是为了在模型计算向量距离的时候可比较。

我们以字符串长度特征为例

# 设置要调研的列名 和 分割数
ob_col = 'featureLen'
sp_num = 50

# 原始数据
X[ob_col].hist(bins = sp_num)
plt.show()
原始值

可以大概了解到长度分布类似长尾数据,主要集中在1到20之间,最长可达60多

col_name = X.columns.values

# 标准化
X_std = StandardScaler().fit_transform(X)
X_std = DataFrame(X_std, columns=col_name)
X_std[ob_col].hist(bins = sp_num)
plt.show()
标准化

可以看到特征分布大致以0轴为中心

# 归一化
X_min = MinMaxScaler().fit_transform(X)
X_min = DataFrame(X_min, columns=col_name)
X_min[ob_col].hist(bins = sp_num)
plt.show()
标准化后归一化

标准化话再进行归一化,特征分布被缩放到0-1之间,注意归一化为线性变换,分布形状不会有任何变化

不过针对长尾数据我们也可以简单的进行取对数进行缩放

# log
X[ob_col + '_log'] = X[ob_col] + 1
X[ob_col + '_log'] = X[ob_col + '_log'].apply(np.log)
X[ob_col + '_log'].hist(bins = sp_num)

# 再标准、归一化
取log后再标准、归一化

特征筛选

特征筛选是指剔除掉关联性非常强的特征,简化模型,对应的就是下图中颜色最深和最浅的部分

特征相关性矩阵

踩坑

  1. 特征计算方法写错,全部返工,建议先小批量计算特征值,保证准确性
  2. 特征值计算结果包含INF NAN等异常值,需要剔除,建议读取特征文件时要进行过滤
  3. 特征计算的时候进行的转换,比如取对数,真实数据预测时特征也要进行相应的变换
  4. 归一化、标准化也是模型fit,模型要进行保留,真实数据预测时要使用相同的模型transform
  5. 用模型未接触过的真实数据测试时,注意样本是否有过期时间的因素
  6. pandas的read_csv方法读入样本文件后会对列进行排序,导致后续预测时特征错位,要通过指定列顺序,usecols=['featureAlphaPct', ‘featureChPct’ ...]来避免

模型训练

本小节为利用特征矩阵进行模型训练,使用常见的几种分类算法,会分别对算法思想进行极简介绍,以及核心代码、主要参数、需要注意的地方。

在开始介绍具体算法之前,先简单列举几个概念

  • 过拟合,指模型在训练数据中表现很好,但在测试数据中表现差,主要是因为模型太复杂,过度的拟合了样本集的中的每一个点。会导致遇到未出现样本时效果很差
  • 欠拟合,与过拟合相反,指模型在训练集和测试集表现都很差,一般因为模型太简单导致
  • 泛化性,泛化性好是指模型对于没遇到过的样本预测精度很高

KNN

K-邻近值分类器,通过计算样本之间的加权距离,找到目标值最近K个值的标注众数作为其标注

from sklearn.neighbors import KNeighborsClassifier

clf = KNeighborsClassifier(n_neighbors=3, weights='uniform').fit(self.X_train, self.y_train)

主要参数

  1. n_neighbors,即k的个数,太大话计算量大而且模型精度差,太小则导致模型过于复杂,泛化性差。默认为5,一般不大于20,经验为不会超过样本的平方根
  2. weights,距离权重计算方法,uniform为邻近点权重一样,distance为距离越近的点权重越大

注意

  1. 模型简单易于理解,对于异常值不敏感,对于简单的分类场景往往有较好表现
  2. 模型训练计算量大,而且在使用模型进行预测时也会比较慢
  3. knn对于特征矩阵的归一化敏感,需要注意特征调优

决策树

选取若干特征对样本集进行划分,使得新的样本集更加有序(相关概念为熵、基尼不纯度、信息量、信息增益),不断迭代得到一棵分类树

from sklearn.tree import DecisionTreeClassifier

clf = DecisionTreeClassifier().fit(self.X_train, self.y_train)

主要参数

  1. criterion,信息增益计算方法,gini为基尼不纯度,entropy为熵
  2. max_depth,树的最大高度
  3. max_features,划分样本时使用的特征最大数

注意

  1. 决策树思想简单,模型训练快,预测快
  2. 模型方便可视化,易于理解(其实多数情况树也不是那么直观好理解)
  3. 对特征值调优不敏感
  4. 比较容易过拟合

随机森林

生产环境一般不会简单的使用决策树,而是使用随机森林,因为即便决策树进行了预剪枝和后剪枝来防止过拟合,但实际使用中仍然很容易过拟合,随机森林的思想便是随机生成多棵树,他们对于样本集从不同的角度过拟合,用他们投票产生的结果作为预测结果,效果会更好,属于一种集成模型

from sklearn.ensemble import RandomForestClassifier

clf = RandomForestClassifier().fit(self.X_train, self.y_train)

主要参数

  1. n_estimators,即随机森林中决策树的个数,默认为100
  2. max_features,不要太大,会导致森林中的树区别太小。也不要太小,会导致欠拟合

注意

  1. 模型简单,易于理解,效果一般不错
  2. 模型预测速度快
  3. 模型训练速度比较慢
  4. 当森林中树数量偏小时,模型并不稳定

梯度提升树

另外一个树的集成思想是梯度提升,具体算法不是很了解,不多说误导了,大意是每次训练一棵决策树,然后将其预测错的样本再进行迭代训练,树不断叠加,最终得到的是一棵树

from sklearn.ensemble import GradientBoostingClassifier

clf = GradientBoostingClassifier().fit(self.X_train, self.y_train)

主要参数

  1. n_estimators,迭代次数
  2. learning_rate,学习强度,即后树的纠错强度
  3. max_depth,树的深度,由于迭代纠错,一般比较小,不超过5

注意

  1. 模型训练要慢一些,但是预测精度高,速度快
  2. 学习强度不易调参,微小变化也可能导致模型有很大不同,需要借助自动调参方法

最大朴素贝叶斯分类器

朴素贝叶斯分类器介绍起来需要引入挺多概念,但其实其本身并不复杂,我直接贴上sklearn官网关于朴素贝叶斯的推导

sklearn官网文档

朴素贝叶斯的所谓朴素,主要体现在假设各个特征之间是独立的(这种情况真实环境很难发生)从而简化计算过程。根据样本分布的不同,对于某维特征的先验概率计算方法也不同,朴素贝叶斯分类器有三个

  1. 特征为正态分布(高斯分布)时使用GaussianNB
  2. 特征为某个对象的统计,比如特征为常见汉字的数量,特征会比较多,矩阵稀疏,使用MultionmialNB
  3. 特征为0-1分布,比如抛硬币,使用BernoulliNB
from sklearn.naive_bayes import GaussianNB
from sklearn.naive_bayes import MultinomialNB
from sklearn.naive_bayes import BernoulliNB

clf = GaussianNB().fit(self.X_train, self.y_train)
clf = MultinomialNB().fit(self.X_train, self.y_train)
clf = BernoulliNB().fit(self.X_train, self.y_train)

主要参数

  1. fit_prior,是否需要模型从样本中自己学习先验概率
  2. class_prior,是否指定标注的分布,None或者[0.1, 0.9]表示分类1概率为10%(可能样本中不是10%)

注意

  1. 朴素贝叶斯模型训练速度快,预测也快

SVM支持向量机

据说是深度学习诞生之前最强大的分类算法。大致思想为在样本空间中寻找一条线、一个面或者一个超平面(多维空间),使得其两侧的样本尽可能多的为同一分类,而且样本要距离这个面越远表明模型泛化性越好。所以SVM一般适用于二分类问题

不难理解,决定这个面的是在分类之间的那些样本,被称为支持向量。

而且SVM的强大之处还在于当样本是线性不可分时,SVM可以将特征升维变换来寻找分割面,这叫做径向基核技巧

image
from sklearn.svm import SVC

clf = SVC().fit(self.X_train[:10000], self.y_train[:10000])

主要参数

  1. kernel,核函数选择,一般使用‘rbf’,即高斯核,无限维特征空间
  2. gamma,对于分界面的约束度,可近似理解为振幅,rbf时才有,一般取值为0.001, 0.0001。越大平面波动越大
  3. C,支持向量的个数,一般取值为1、10、100、1000,约大平面考虑的支持向量越多,平面越不平滑

注意

  1. SVC模型训练速度慢,官网建议样本不要超过10000
  2. 特征需要预处理

神经网络

现在叫做深度学习,理解不深,不误人子弟了

from sklearn.neural_network import MLPClassifier

clf = MLPClassifier().fit(self.X_train[:10000], self.y_train[:10000])

注意

  1. MLP模型训练速度慢,建议样本不要太多

参数调优

大部分模型具备多个参数,而且参数还可能是个连续值,人工调参的话工作量很大,一般使用超网格参数调优 ,即GridSearchCV

交叉验证

为了防止模型过拟合,一般做法是会将样本集划分为测试集和训练集,训练集用以模型训练,测试集用以模型评分。对于测试集和训练集的划分,有几种做法

简单交叉
即只是简单的把样本划分为一个训练集和一个测试集

from sklearn.model_selection import train_test_split

self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(data['X'], data['y'], test_size=0.3)

K-折交叉
把整体样本集划分为K份,每次拿其中一份作为测试集,其他全部作为训练集,这样可以保证模型能够充分利用数据,本文便采用的该方法

关于K的取值,一般可以从10开始尝试,越大越好,经验值为log(样本数),但越大计算量越大,本文取K为5,模型最慢的需要调优十多个小时

from sklearn.model_selection import GridSearchCV

grid = GridSearchCV(MLPClassifier(), cv=5)
grid.fit(self.X_train[:10000], self.y_train[:10000])

留一法
极端情况,每次只取一个样本作为测试集,其他全为训练集。计算量最大,只适合小样本。

模型评估

自动调参过程中会产生大量模型,对模型进行评估选优就很重要,一般常用混淆矩阵、ROC曲线、P-R曲线,本文采用的是混淆矩阵

混淆矩阵

混淆矩阵简单易懂,经常用在分类问题上,假如分类结果有N个(本文为2个,有效、无效),则混淆矩阵是个N*N的矩阵,矩阵中的点(Ci, Cj)的意思为属于分类i的样本被模型预测为分类j的个数,比如本文这个模型可能得出这样一个混淆矩阵

     有效  无效
有效   10    3
无效   2     8
  • 模型精度 Accuracy,为对角线上的点占样本比率
  • 某个分类的准确率 precision,为对角线上该类的点占其所在列的比率
  • 某个分类的召回率 recall,为对角线上该类的点占其所在行的比率
  • f1-score,为准召的一个综合评估

本文决策树模型的报告大概这样

clf = DecisionTreeClassifier().fit(self.X_train, self.y_train)
y_pred = clf.predict(X)
report = classification_report(y, y_pred)
print(reprot)
混淆矩阵评估报告

GridSearchCV

常用的调参工具为GridSearchCV,即网格搜索,也叫做超参数调优器。思想很简单,即提前设置好每个参数的取值范围,然后自动进行多重循环来训练模型,并记录每个模型的评分,最终得出一个最好的模型。

网格搜索的优势为

  • 支持多种参数形势,可以多重循环,也可以按照字典分类
  • 支持K-折交叉验证
  • 可以自定义模型评优方法
  • 保存了搜索过程中的各种参数,比如最优模型、所有模型、最优评分等

以比较需要调参的SVM为例,可以实现kernel参数取不同值时需要调参的组合不同

param_grid = [
    {
      'kernel': ['rbf'], 
      'gamma': [1e-3, 1e-4], 
      'C': [1, 10, 100, 1000]
    },
    {
      'kernel': ['linear'], 
      'C': [1, 10, 100, 1000]
    }
]
grid = GridSearchCV(SVC(), param_grid, cv=self.cv)
grid.fit(self.X_train[:5000], self.y_train[:5000]) # svm数据量超过10000计算量过大

经验

  • 由于自动调参大部分需要的时间很长,所以最好在每次调参结束后,利用joblib保存模型、pickle来持久化grid对象、以及模型最优参数
  • 参数最好不要一开始就设置取值范围特别大,导致自动调参花费大量时间,最好先设置范围小一些,再根据最优参数的结果逐步调整范围,快速迭代。注意此时参数random_state要一样

下图为本文的多次调参结果,.m为模型,.para为参数,.grid为grid对象,数值为评分

本文调参结果

模型集成

模型集成的思想类似随机森林,即将多个模型的结果进行组合,将他们的预测结果的众数作为预测结果,实践中可以有效提高模型性能。本文选择表现最好的几个模型进行集成

tree = joblib.load('grid/tree/tree.85.m')
knn = joblib.load('grid/knn/knn.86.m')
mlp = joblib.load('grid/mlp/mlp.83.m')
rf = joblib.load('grid/random_forest/random_forest.85.m')
gb = joblib.load('grid/gradient_boosting/gradient_boosting.88.m')
         
df = pd.DataFrame({
    'knn': knn.predict(X_test),
    'tree': tree.predict(X_test), 
    'rf': rf.predict(X_test),
    'gb': gb.predict(X_test),
    'mlp': mlp.predict(X_test),
})
ensemble_pred = df.mode(axis=1)[0]
report = classification_report(y, ensemble_pred)
print(reprot)
集成模型报告

可以看到集成模型的效果要优于任意一个单独模型

改进思考

  1. 细分为多分类问题,可能更加精准
  2. 实际使用中发现标注数据还是存在一定量的谬误,需要精细化准备样本集
  3. 特征工程可以更加细化,没有进行过滤和太多转换
  4. 尝试无监督算法,诸如LOF异常值检测

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容