给你讲讲机器学习数据预处理中,归一化(normalization)的重要性。
前情回顾
Previously, on 玉树芝兰 ……
我给你写了一篇《如何用 Python 和 Tensorflow 2.0 神经网络分类表格数据?》,为你讲解了 Tensorflow 2.0 处理结构化数据的分类。
结尾处,我给你留了一个问题。
把测试集输入模型中,检验效果。结果是这样的:
model.evaluate(test_ds)
准确率接近80%,看起来很棒,对吗?
但是,有一个疑问:
注意这张截图。训练的过程中,除了第一个轮次外,其余4个轮次的这几项重要指标居然都没变!
它们包括:
- 训练集损失
- 训练集准确率
- 验证集损失
- 验证集准确率
所谓机器学习,就是不断迭代改进。
如果每一轮下来,结果都一模一样,这里八成有鬼。
我给了你提示:
看一个分类模型的好坏,不能只看准确率(accuracy)。对于二元分类问题,你可以关注一下 f1 score,以及混淆矩阵(confusion matrix)。
这段时间,你通过思考,发现问题产生原因,以及解决方案了吗?
从留言的反馈来看,有读者能够正确指出了问题。
但很遗憾,我没有能见到有人提出正确和完整的解决方案。
这篇文章,咱们就来谈谈,机器为什么“不肯学习”?以及怎么做,才能让它“学得进去”。
环境
本文的配套源代码,我放在了这个 Github 项目中。请你点击这个链接(http://t.cn/ESJmj4h)访问。
如果你对我的教程满意,欢迎在页面右上方的 Star 上点击一下,帮我加一颗星。谢谢!
注意这个页面的中央,有个按钮,写着“在 Colab 打开”(Open in Colab)。请你点击它。
然后,Google Colab 就会自动开启。
我建议你点一下上图中红色圈出的 “COPY TO DRIVE” 按钮。这样就可以先把它在你自己的 Google Drive 中存好,以便使用和回顾。
Colab 为你提供了全套的运行环境。你只需要依次执行代码,就可以复现本教程的运行结果了。
如果你对 Google Colab 不熟悉,没关系。我这里有一篇教程,专门讲解 Google Colab 的特点与使用方式。
为了你能够更为深入地学习与了解代码,我建议你在 Google Colab 中开启一个全新的 Notebook ,并且根据下文,依次输入代码并运行。在此过程中,充分理解代码的含义。
这种看似笨拙的方式,其实是学习的有效路径。
代码
请你在 Colab Notebook 里,找到这一条分割线:
用鼠标点击它,然后从菜单里面选择 Runtime
-> Run Before
:
运行结束后,你会获得如下图的结果:
《如何用 Python 和 Tensorflow 2.0 神经网络分类表格数据?》一文的结果已经成功复现。
下面我们依次来解读后面的语句。
首先,我们利用 Keras API 中提供的 predict
函数,来获得测试集上的预测结果。
pred = model.predict(test_ds)
但是请注意,由于我们的模型最后一层,用的激活函数是 sigmoid
, 因此 pred
的预测结果,会是从0到1区间内的小数。
而我们实际需要输出的,是整数0或者1,代表客户“流失”(1)或者“未流失”(0)。
幸好, numpy 软件包里面,有一个非常方便的函数 rint
,可以帮助我们四舍五入,把小数变成整数。
pred = np.rint(pred)
我们来看看输出结果:
pred
有了预测输出结果,下面我们就可以用更多的方法,检验分类效果了。
根据前文的提示,这里我们主要用到两项统计功能:
- 分类报告
- 混淆矩阵
我们先从 Scikit-learn 软件包导入对应的功能。
from sklearn.metrics import classification_report, confusion_matrix
然后,我们对比测试集实际标记,即 test['Exited']
,和我们的预测结果。
print(classification_report(test['Exited'], pred))
这里,你立刻就能意识到出问题了——有一个分类,即“客户流失”(1)里,三项重要指标(precision, recall 和 f1-score)居然都是0!
我们用同样的数据查看混淆矩阵,看看到底发生了什么。
print(confusion_matrix(test['Exited'], pred))
混淆矩阵的读法是,行代表实际分类,列代表预测分类,分别从0到1排列。
上图中,矩阵的含义就是:模型预测,所有测试集数据对应的输出都是0;其中预测成功了1585个(实际分类就是0),预测错误415个(实际分类其实是1)。
也就是说,咱们费了半天劲,训练出来的模型只会傻乎乎地,把所有分类结果都设置成0.
在机器学习里,这是一个典型的笨模型(dummy model)。
如果咱们的测试集里面,标签分类0和1的个数是均衡的(一样一半),那这种笨模型,应该获得 50% 的准确率。
然而,我们实际看看,测试集里面,分类0(客户未流失)到底占多大比例:
len(test[test['Exited'] == 0])/len(test)
结果是:
0.7925
这个数值,恰恰就是《如何用 Python 和 Tensorflow 2.0 神经网络分类表格数据?》一文里面,我们在测试集上获得了准确率。
一开始我们还认为,将近80%的准确率,是好事儿。
实际上,这模型着实很傻,只有一根筋。
设想我们拿另外一个测试集,里面只有 1% 的标注是类别0,那么测试准确率也就只有 1% 。
为了不冤枉模型,咱们再次确认一下。
使用 numpy 中的 unique
函数,查看一下预测结果 pred
中,到底有几种不同的取值。
np.unique(pred)
结果是:
array([0.], dtype=float32)
果不其然,全都是0.
果真是“人工不智能”啊!
分析
问题出在哪里呢?
模型根本就没有学到东西。
每一轮下来,结果都一样,毫无进步。
说到这里,你可能会有疑惑:
老师,是不是你讲解出错了?
两周前,我在 UNT 给学生上课的时候,他们就提出来了这疑问。
我早有准备,立即布置了一个课堂练习。
让他们用这套流程,处理另外的一个数据集。
这个数据集你也见过,就是我在《贷还是不贷:如何用Python和机器学习帮你决策?》里面用过的贷款审批数据。
我把数据放在了这个链接(http://t.cn/ESJ3x3o),你如果感兴趣的话,不妨也试着用前文介绍的流程,自己跑一遍。
学生们有些无奈地做了这个练习。
他们的心理活动大概是这样的:
你教的这套流程,连演示数据都搞不定,更别说练习数据了。做了也是错的。是不是先纠正了错误,再让我们练啊?
然后,当运行结果出来的时候,我在一旁,静静看着他们惊诧、沉思,以至于抓狂的表情。
同一套流程,在另外的数据上使用,机器确实学习到了规律。
数据集的细节里面,藏着什么魔鬼?
归一
直接说答案:
流程上确实有问题。数值型数据没有做归一化(normalization)。
归一化是什么?
就是让不同特征列上的数值,拥有类似的分布区间。
最简单的方法,是根据训练集上的对应特征,求 Z 分数。
Z 分数的定义是:
- Mean 是均值
- Standard Deviation 是标准差
为什么一定要做这一步?
回顾一下咱们的数据。
我这里用红色标出来了所有数值特征列。
看看有什么特点?
对,它们的分布迥异。
NumOfProducts
的波动范围,比起 Balance
或者 EstimatedSalary
,要小得多。
机器学习,并不是什么黑科技。
它的背后,是非常简单的数学原理。
最常用的迭代方法,是梯度下降(Gradient descent)。如下图所示:
其实就是奔跑着下降,找局部最优解。
如果没跑到,继续跑。
如果跑过辙了,再跑回来。
但问题在于,你看到的这张图,是只有1维自变量的情况。
咱们观察的数据集,仅数值型数据,就有6个。因此至少是要考察这6个维度。
不好意思,我无法给你绘制一个六维图形,自己脑补吧。
但是注意,对这六个维度,咱们用的,却是同一个学习速率(learning rate)。
就好像同一个老师,同时给6个学生上数学课。
如果这六个维度分布一致,这样是有意义的。
这也是为什么大多数学校里面,都要分年级授课。要保证授课对象的理解能力,尽量相似。
但假如这“6个学生”里,有一个是爱因斯坦,一个是阿甘。
你说老师该怎么讲课?
爱因斯坦听得舒服的进度,阿甘早就跟不上了。
阿甘能接受的进度,爱因斯坦听了可能无聊到想撞墙。
最后老师决定——太难了,我不教了!
于是谁都学不到东西了。
对应到我们的例子,就是因为数据分布差异过大,导致不论往哪个方向尝试改变参数,都按下葫芦浮起瓢,越来越糟。
于是模型判定,呆在原地不动,是最好的策略。
所以,它干脆不学了。
怎么办?
这个时候,就需要归一化了。
对应咱们这个不恰当的举例,就是在课堂上,老师要求每个人都保持每天一单位(unit)的学习进度。
只不过,爱因斯坦的一单位,是100页书。
阿甘同学……两行,还能接受吧?
新代码
请你点击这个链接(http://t.cn/ESJBJHW)访问更新后的代码。
按照之前的方式,点击“在 Colab 打开”(Open in Colab)。使用 “COPY TO DRIVE” 按钮,存放在你自己的 Google Drive 中。
对比观察后,你会发现,改动只有1个代码段落。
就是把原先的数值型特征采集从这样:
for header in numeric_columns:
feature_columns.append(feature_column.numeric_column(header))
变成这样:
for header in numeric_columns:
feature_columns.append(
feature_column.numeric_column(
header,
normalizer_fn=lambda x: (tf.cast(x, dtype=float)-train[header].mean())/train[header].std()))
尤其要注意,我们要保证平均值和标准差来自于训练集。只有这样,才能保证模型对验证集和测试集的分布一无所知,结果的检验才有意义。否则,就如同考试作弊一样。
这就是为了归一化,你所需做的全部工作。
这里我们依然保持原先的随机种子设定。也就是凡是使用了随机函数的功能(训练集、验证集和测试集的划分等),都与更新代码之前完全一致。
这样做,改变代码前后的结果才有可对比性。
下面我们使用菜单栏里面的 "Run All" 运行一下代码。
之后查看输出。
首先我们可以注意到,这次的训练过程,数值终于有变化了。
因为其他变量全都保持一致。所以这种变化,没有别的解释,只能是因为使用了归一化(normalization)。
我们更加关心的,是这次的分类报告,以及混淆矩阵。
分类报告是这样的:
注意这一次,类别1上面的几项指标,终于不再是0了。
混淆矩阵中,类别1里,也有36个预测正确的样本了。
成功了!
……
别急着欢呼。
虽然机器在学习和改进,但是效果好像也不是很好嘛。例如类别1的 Recall 简直惨不忍睹。
有没有什么办法改进呢?
这个问题,就需要你了解如何微调模型,以及超参数的设定了。
我把推荐的学习资料,放在了公众号的对应文章里,欢迎查看。
小结
这篇文章里,我为你介绍了以下知识点:
- 分类模型性能验证(尤其是 Accuracy 之外的)评测指标;
- 预处理过程中数值数据归一化(Normalization)的重要性;
- 如何在 Tensorflow 2.0 的数据预处理和特征抽取中使用归一化;
- 如何利用模型预测分类结果,并且使用第三方软件包功能快速统计汇报。
希望上述内容,能对你使用深度神经网络进行机器学习有帮助。
祝深度学习愉快!
延伸阅读
你可能也会对以下话题感兴趣。点击链接就可以查看。
- 如何高效学 Python ?
- 《文科生数据科学上手指南》分享
- 如何用 Python 和 fast.ai 做图像深度迁移学习?
- 如何用 Python 和深度迁移学习做文本分类?
- 如何用 Python 和 BERT 做中文文本二元分类?
喜欢请点赞和打赏。还可以微信关注和置顶我的公众号“玉树芝兰”(nkwangshuyi)。
如果你对 Python 与数据科学感兴趣,不妨阅读我的系列教程索引贴《如何高效入门数据科学?》,里面还有更多的有趣问题及解法。
题图:来自于 freepixels