Python 监督机器学习(一)

原文:annas-archive.org/md5/f4a659650f8235d9d3140acaafda04a6

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

监督机器学习广泛应用于金融、在线广告和分析等各个领域,因为它能够训练系统进行定价预测、广告活动调整、客户推荐等,赋予系统自我调整和自主决策的能力。这种能力的优势使得了解机器如何在幕后学习变得至关重要。

本书将引导你实现和深入了解许多流行的监督机器学习算法。你将从快速概览开始,了解监督学习与无监督学习的区别。接下来,我们将探索线性回归和逻辑回归等参数模型,决策树等非参数方法,以及各种聚类技术,以促进决策和预测。随着进展,你还将接触到推荐系统,这是在线公司广泛使用的工具,用于提升用户互动和促进潜在销售。最后,我们将简要探讨神经网络和迁移学习。

本书结束时,你将掌握一些实用的技巧,获得将监督学习算法快速且有效地应用于新问题所需的实际操作能力。

本书的目标读者

本书适合那些希望开始使用监督学习的有志机器学习开发者。读者应具备一定的 Python 编程知识和一些监督学习的基础知识。

本书内容概览

第一章,迈向监督学习的第一步,介绍了监督机器学习的基础知识,帮助你为独立解决问题做好准备。本章包含四个重要部分。首先,我们将设置 Anaconda 环境,确保能够运行示例。在接下来的几节中,我们将进一步讲解机器学习的理论基础,最后在实施算法的部分中,我们将再次设置 Anaconda 环境并开始实现算法。

第二章,实现参数模型,深入探讨了几种流行的监督学习算法,这些算法都属于参数建模家族。我们将从正式介绍参数模型开始,接着重点介绍两种特别流行的参数模型:线性回归和逻辑回归。我们将花些时间了解其内部原理,然后进入 Python,真正从头开始编写代码。

第三章,使用非参数模型,探讨了非参数模型系列。我们将从讨论偏差-方差权衡开始,并解释参数模型和非参数模型在根本上的差异。然后,我们将介绍决策树和聚类方法。最后,我们将讨论非参数模型的一些优缺点。

第四章,监督学习的高级主题,涉及两个主题:推荐系统和神经网络。我们将从协同过滤开始,然后讨论如何将基于内容的相似性集成到协同过滤系统中。最后,我们将进入神经网络和迁移学习的讨论。

为了最大化本书的学习效果

您需要以下软件来顺利进行各章内容:

  • Jupyter Notebook

  • Anaconda

  • Python

下载示例代码文件

您可以从www.packt.com的账户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packt.com/support,注册后将文件直接发送给您。

您可以按照以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择“SUPPORT”标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书名并按照屏幕上的指示操作。

下载文件后,请确保使用以下最新版本的工具解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,链接为github.com/PacktPublishing/Supervised-Machine-Learning-with-Python。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。

我们还提供了来自我们丰富图书和视频目录的其他代码包,您可以访问**github.com/PacktPublishing/**进行查看!

下载彩色图像

我们还提供了一份 PDF 文件,包含本书中使用的截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781838825669_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入以及 Twitter 用户名。示例:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

一段代码块的格式如下:

from urllib.request import urlretrieve, ProxyHandler, build_opener, install_opener
import requests
import os
pfx = "https://archive.ics.uci.edu/ml/machine-learning databases/spambase/"
data_dir = "data"

任何命令行输入或输出都如下所示:

jupyter notebook

粗体:指示一个新术语、重要词汇或屏幕上显示的词语。例如,菜单或对话框中的字词会像这样出现在文本中。以下是一个例子:“从管理面板中选择系统信息。”

警告或重要提示会以这种形式出现。

提示和技巧会以这种形式出现。

联系方式

我们始终欢迎读者的反馈。

常规反馈:如对本书的任何方面有疑问,请在消息主题中提及书名,并发送邮件至 customercare@packtpub.com

勘误:尽管我们已尽一切努力确保内容的准确性,但错误不可避免。如果您在本书中发现错误,请向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们任何形式的作品的非法副本,我们将不胜感激您提供地址或网站名称。请联系我们,链接至 copyright@packt.com

如果您有意成为作者:如果您在某个专题上有专业知识,并且有兴趣撰写或贡献书籍,请访问 authors.packtpub.com

评论

请留下您的评论。阅读并使用本书后,为什么不在购买书籍的网站上留下您的评论呢?潜在的读者可以看到并使用您的客观意见来做出购买决策,Packt 可以了解您对我们产品的看法,而我们的作者可以看到您对他们书籍的反馈。谢谢!

欲了解更多 Packt 相关信息,请访问 packt.com

第一章:向监督学习迈出的第一步

在本书中,我们将学习实现许多你在日常生活中接触到的常见机器学习算法。这里将有大量的数学、理论和具体的代码示例,足以满足任何机器学习爱好者的需求,并且希望你在此过程中能学到一些有用的 Python 技巧和实践。我们将从简要介绍监督学习开始,展示一个真实的机器学习示范;完成 Anaconda 环境的设置;学习如何衡量曲线的斜率、Nd-曲线和多个函数;最后,我们将讨论如何判断一个模型是否优秀。在本章中,我们将涵盖以下主题:

  • 监督学习应用示例

  • 设置环境

  • 监督学习

  • 爬山法与损失函数

  • 模型评估与数据分割

技术要求

本章中,如果你还没有安装以下软件,你需要进行安装:

  • Jupyter Notebook

  • Anaconda

  • Python

本章的代码文件可以在https:/​/​github.​com/​PacktPublishing/找到。

Supervised-Machine-Learning-with-Python

监督学习应用示例

首先,我们将看看可以通过监督机器学习做些什么。通过以下终端提示符,我们将启动一个新的 Jupyter Notebook:

jupyter notebook

一旦我们进入这个顶级目录Hands-on-Supervised-Machine-Learning-with-Python-master,我们将直接进入examples目录:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/6b307221-119e-4772-a9d0-44d3186979f3.png

你可以看到我们这里唯一的 Notebook 是1.1 Supervised Learning Demo.ipynb

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/c853b1ee-ae6e-4447-b516-ffc646c710d0.png

我们有一个监督学习示范的 Jupyter Notebook。我们将使用一个名为Spam的 UCI 数据集。这个数据集包含了不同的电子邮件,并且每封邮件都有不同的特征,这些特征对应于是否是垃圾邮件。我们希望构建一个机器学习算法,能够预测即将收到的电子邮件是否是垃圾邮件。如果你正在运行自己的电子邮件服务器,这将对你非常有帮助。

所以下面的代码中的第一个函数只是一个请求的 get 函数。你应该已经拥有数据集,它已经在examples目录中了。但如果没有,你可以继续运行以下代码。你可以看到我们已经有了spam.csv,所以我们不需要重新下载:

from urllib.request import urlretrieve, ProxyHandler, build_opener, install_opener
import requests
import os
pfx = "https://archive.ics.uci.edu/ml/machine-learning databases/spambase/"
data_dir = "data"
# We might need to set a proxy handler...
try:
    proxies = {"http": os.environ['http_proxy'],
               "https": os.environ['https_proxy']}
    print("Found proxy settings")
    #create the proxy object, assign it to a variable
    proxy = ProxyHandler(proxies)
    # construct a new opener using your proxy settings
    opener = build_opener(proxy)
    # install the opener on the module-level
    install_opener(opener)

except KeyError:
    pass
# The following will download the data if you don't already have it...
def get_data(link, where):
    # Append the prefix
    link = pfx + link

接下来,我们将使用pandas库。它是一个来自 Python 的数据分析库。在我们进行下一阶段的环境设置时,你可以安装它。这个库提供了数据框结构,是一种原生的 Python 数据结构,我们将如下使用它:

import pandas as pd
names = ["word_freq_make", "word_freq_address", "word_freq_all", 
         "word_freq_3d", "word_freq_our", "word_freq_over", 
         "word_freq_remove", "word_freq_internet", "word_freq_order",
         "word_freq_mail", "word_freq_receive", "word_freq_will", 
         "word_freq_people", "word_freq_report", "word_freq_addresses", 
         "word_freq_free", "word_freq_business", "word_freq_email", 
         "word_freq_you", "word_freq_credit", "word_freq_your", 
         "word_freq_font", "word_freq_000", "word_freq_money", 
         "word_freq_hp", "word_freq_hpl", "word_freq_george", 
         "word_freq_650", "word_freq_lab", "word_freq_labs", 
         "word_freq_telnet", "word_freq_857", "word_freq_data", 
         "word_freq_415", "word_freq_85", "word_freq_technology", 
         "word_freq_1999", "word_freq_parts", "word_freq_pm", 
         "word_freq_direct", "word_freq_cs", "word_freq_meeting", 
         "word_freq_original", "word_freq_project", "word_freq_re", 
         "word_freq_edu", "word_freq_table", "word_freq_conference", 
         "char_freq_;", "char_freq_(", "char_freq_[", "char_freq_!", 
         "char_freq_$", "char_freq_#", "capital_run_length_average", 
         "capital_run_length_longest", "capital_run_length_total",
         "is_spam"]
df = pd.read_csv(os.path.join("data", "spam.csv"), header=None, names=names)
# pop off the target
y = df.pop("is_spam")
df.head()

这允许我们将数据以如下格式呈现。我们可以使用各种统计函数,这些函数在进行机器学习时非常实用:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/9205d451-daa7-42ca-ae8f-075ae077fe9e.png

如果一些术语你不太熟悉,别着急——我们将在本书中详细学习这些术语。

对于train_test_split,我们将使用df数据集并将其分为两个部分:训练集和测试集。此外,我们还会有目标变量,它是一个01变量,表示是否为垃圾邮件的真假。我们也会分割这个目标变量,包括相应的真假标签向量。通过划分标签,我们得到3680个训练样本和921个测试样本,文件如以下代码片段所示:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(df, y, test_size=0.2, random_state=42, stratify=y)
print("Num training samples: %i" % X_train.shape[0])
print("Num test samples: %i" % X_test.shape[0])

前述代码的输出如下:

Num training samples: 3680
Num test samples: 921

请注意,我们的训练样本比测试样本多,这对于调整我们的模型非常重要。我们将在本书后面学习到这一点。所以,暂时不用太担心这里发生了什么,这些内容仅仅是为了演示目的。

在以下代码中,我们使用了packtml库。这是我们正在构建的实际包,是一个分类和回归树分类器。CARTClassifier实际上是决策树的一个泛化版本,用于回归和分类目的。我们在这里拟合的所有内容都是我们从头开始构建的监督学习算法。这是我们将在本书中构建的分类器之一。我们还提供了这个用于绘制学习曲线的实用函数。这个函数将接收我们的训练集并将其分成不同的折叠进行交叉验证。我们将在不同阶段拟合训练集中的样本数量,以便我们可以看到学习曲线在训练集和验证集折叠之间的收敛情况,这本质上决定了我们的算法如何学习:

from packtml.utils.plotting import plot_learning_curve
from packtml.decision_tree import CARTClassifier
from sklearn.metrics import accuracy_score
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

# very basic decision tree
plot_learning_curve(
        CARTClassifier, metric=accuracy_score,
        X=X_train, y=y_train, n_folds=3, seed=21, trace=True,
        train_sizes=(np.linspace(.25, .75, 4) * X_train.shape[0]).astype(int),
        max_depth=8, random_state=42)\
    .show()

我们将继续运行前面的代码,并绘制算法在不同训练集大小下的学习情况。你可以看到,我们将为四种不同的训练集大小进行拟合,并使用三折交叉验证。

所以,实际上我们是在拟合 12 个独立的模型,这需要几秒钟的时间:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/1923903f-00ea-423c-985e-9142be846bd2.png

在前面的输出中,我们可以看到我们的训练得分验证得分训练得分在学习如何泛化时会减少,而我们的验证得分随着算法从训练集泛化到验证集而提高。所以,在我们的验证集中,准确率大约徘徊在 92%到 93%之间。

我们将在这里使用来自最优模型的超参数:

decision_tree = CARTClassifier(X_train, y_train, random_state=42, max_depth=8)

逻辑回归

在本节中,我们将学习逻辑回归,这是我们将从头开始构建的另一个分类模型。我们将继续拟合以下代码:

from packtml.regression import SimpleLogisticRegression
# simple logistic regression classifier
plot_learning_curve(
        SimpleLogisticRegression, metric=accuracy_score,
        X=X_train, y=y_train, n_folds=3, seed=21, trace=True,
        train_sizes=(np.linspace(.25, .8, 4) *     X_train.shape[0]).astype(int),
        n_steps=250, learning_rate=0.0025, loglik_interval=100)\
    .show()

这比决策树要快得多。在下面的输出中,你可以看到我们在 92.5%区间附近聚集得更多。这看起来比我们的决策树更一致,但在验证集上的表现还是不够好:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/fa022926-38db-42f3-8dc2-f205f5120b4f.png

在下方的截图中,有垃圾邮件的编码记录。我们将看到这种编码在我们可以读取并验证的邮件上的表现。所以,如果你访问了顶部 Jupyter Notebook 中包含的 UCI 链接,它将提供数据集中所有特征的描述。这里有很多不同的特征,它们计算特定词汇与整个邮件中的词汇总数的比例。其中一些词汇可能是免费的,一些是受信的。我们还有其他几个特征,计算字符频率、感叹号的数量以及连续大写字母序列的数量。

所以,如果你有一组高度资本化的词汇,我们有这些特征:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/278f0298-97cf-4a1e-abd2-79fc4470b12d.png

在下方的截图中,我们将创建两封电子邮件。第一封邮件显然是垃圾邮件。即使有人收到这封邮件,也不会有人回应:

spam_email = """
Dear small business owner,

This email is to inform you that for $0 down, you can receive a 
FREE CREDIT REPORT!!! Your money is important; PROTECT YOUR CREDIT and 
reply direct to us for assistance!
"""

print(spam_email)

上述代码片段的输出结果如下:

Dear small business owner,

This email is to inform you that for $0 down, you can receive a 
FREE CREDIT REPORT!!! Your money is important; PROTECT YOUR CREDIT and 
reply direct to us for assistance!

第二封邮件看起来不像垃圾邮件:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/239df941-2895-4c5e-b9ab-d7e4d5304eca.png

我们刚刚拟合的模型将查看这两封邮件并编码这些特征,然后会分类哪些是垃圾邮件,哪些不是垃圾邮件。

以下函数将把这些电子邮件编码成我们讨论过的特征。最初,我们将使用一个Counter函数作为对象,并对电子邮件进行分词。我们所做的就是将电子邮件拆分成一个词汇列表,然后这些词汇可以被拆分成一个字符列表。稍后,我们将计算这些字符和单词的数量,以便生成我们的特征:

from collections import Counter
import numpy as np
def encode_email(email):
    # tokenize the email
    tokens = email.split()

    # easiest way to count characters will be to join everything
    # up and split them into chars, then use a counter to count them
    # all ONE time.
    chars = list("".join(tokens))
    char_counts = Counter(chars)
    n_chars = len(chars)

    # we can do the same thing with "tokens" to get counts of words
    # (but we want them to be lowercase!)
    word_counts = Counter([t.lower() for t in tokens])

    # Of the names above, the ones that start with "word" are
    # percentages of frequencies of words. Let's get the words
    # in question
    freq_words = [ 
        name.split("_")[-1]
        for name in names 
        if name.startswith("word")
    ]

    # compile the first 48 values using the words in question
    word_freq_encodings = [100\. * (word_counts.get(t, 0) / len(tokens))
                           for t in freq_words]

所以,开始时我们提到的所有特征告诉我们我们感兴趣的词汇是什么。我们可以看到,原始数据集关注的词汇包括地址、电子邮件、商业和信用,而对于我们的字符,我们寻找的是打开和关闭的括号以及美元符号(这些与垃圾邮件非常相关)。所以,我们将统计所有这些特征,具体如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/defb48de-1917-450d-8b29-3bfd7edc6f46.png

应用比例并跟踪capital_runs的总数,计算平均值、最大值和最小值:

 # make a np array to compute the next few stats quickly
capital_runs = np.asarray(capital_runs)
    capital_stats = [capital_runs.mean(), 
                     capital_runs.max(), 
                     capital_runs.sum()]

当我们运行之前的代码时,我们得到以下输出。这将编码我们的电子邮件。这只是一个包含所有不同特征的向量。它大约应该有 50 个字符长:

# get the email vectors
fake_email = encode_email(spam_email)
real_email = encode_email(not_spam)
# this is what they look like:
print("Spam email:")
print(fake_email)
print("\nReal email:")
print(real_email)

上述代码的输出结果如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/49f17f3e-ff3a-4a64-9765-05e471651690.png

当我们将前面的值输入到我们的模型中时,我们将看到模型的效果如何。理想情况下,我们会看到实际的假邮件被预测为假,实际的真实邮件被预测为真实。因此,如果邮件被预测为假,我们的垃圾邮件预测确实为垃圾邮件,无论是在决策树还是逻辑回归模型中。我们的真实邮件不是垃圾邮件,这一点可能更为重要,因为我们不想将真实邮件误筛入垃圾邮件文件夹。所以,你可以看到我们在这里拟合了一些非常不错的模型,这些模型可以应用于我们直观判断是否为垃圾邮件的情景:

predict = (lambda rec, mod: "SPAM!" if mod.predict([rec])[0] == 1 else "Not spam")

print("Decision tree predictions:")
print("Spam email prediction: %r" % predict(fake_email, decision_tree))
print("Real email prediction: %r" % predict(real_email, decision_tree))

print("\nLogistic regression predictions:")
print("Spam email prediction: %r" % predict(fake_email, logistic_regression))
print("Real email prediction: %r" % predict(real_email, logistic_regression))

上述代码的输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/8aa61090-9474-4640-bf82-5c88c68fb8c6.png

这是我们将在本书中从零开始构建的实际算法的演示,可以应用于真实世界的问题。

设置环境

我们将继续进行环境设置。现在我们已经完成了前面的示例,让我们开始设置我们的 Anaconda 环境。Anaconda 除了其他功能外,还是一个依赖管理工具,它能帮助我们控制我们想要使用的每个包的特定版本。我们将通过这个链接,www.anaconda.com/download/,访问 Anaconda 官网,并点击“Download”标签页。

我们正在构建的这个包将无法与 Python 2.7 兼容。所以,一旦你有了 Anaconda,我们将进行一个实际的编码示例,展示如何设置包以及.yml文件中的环境设置。

一旦你在主目录中设置好了 Anaconda,我们将使用environment.yml文件。你可以看到我们将要创建的环境名称是packt-sml,用于有监督的机器学习。我们将需要 NumPy、SciPy、scikit-learn 和 pandas。这些都是科学计算和数据分析的库。Matplotlib 是我们在 Jupyter Notebook 中用于绘制图表的工具,因此你需要这些图表。conda包使得构建这个环境变得非常简单。我们只需输入conda env create,然后使用-f参数指定文件,进入Hands-on-Supervised-Machine-Learning-with-Python-master目录,我们将使用如下命令中的environment.yml

cat environment.yml conda env create -f environment.yml

由于这是你第一次创建这个环境,它会创建一个大型脚本,下载你所需的所有内容。一旦创建了环境,你需要激活它。因此,在 macOS 或 Linux 机器上,我们将输入source activate packt-sml

如果你使用的是 Windows 系统,只需输入activate packt-sml,这将激活该环境:

 source activate packt-sml

输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/205cbcc3-10ea-48a9-a83b-76fcf196cffc.png

为了构建这个包,我们将输入cat setup.py命令。我们可以快速检查一下:

cat setup.py

看看这个 setup.py。基本上,这是使用 setup 工具安装包的方式。在下面的截图中,我们可以看到所有不同的子模块:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/c57903bf-a407-461b-93d6-f2798ddf73a0.png

我们将通过键入 python setup.py install 命令来构建这个包。现在,当我们进入 Python 并尝试导入 packtml 时,我们将看到以下输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/273e6b37-dfe5-4dbd-ae08-a34b4635bd8b.png

在这一部分中,我们已经安装了环境并构建了包。在下一部分中,我们将开始讨论监督学习背后的一些理论。

监督学习

在这一部分,我们将正式定义什么是机器学习,特别是什么是监督学习。

在 AI 的早期,所有的东西都是规则引擎。程序员编写函数和规则,计算机简单地遵循这些规则。现代 AI 更符合机器学习的理念,它教计算机自己编写函数。有人可能会认为这是一种过于简单化的说法,但从本质上讲,这正是机器学习的核心内容。

我们将通过一个简单的例子来展示什么是机器学习,什么不是机器学习。在这里,我们使用 scikit-learn 的 datasets 子模块创建两个对象和变量,这些对象和变量也叫做协方差或特征,沿着列轴排列。y 是一个向量,它的值的数量与 X 中的行数相同。在这个例子中,y 是一个类别标签。为了举例说明,y 这里可能是一个二分类标签,表示一个实际的情况,比如肿瘤的恶性程度。接着,X 是一个描述 y 的属性矩阵。一个特征可能是肿瘤的直径,另一个特征可能表示肿瘤的密度。前面的解释可以通过以下代码来体现:

import numpy as np
from sklearn.datasets import make_classification

rs = np.random.RandomState(42)
X,y = make_classification(n_samples=10, random_state=rs)

根据我们的定义,规则引擎只是业务逻辑。它可以简单也可以复杂,完全取决于你的需求,但规则是由程序员定义的。在这个函数中,我们将通过返回 1true 来评估我们的 X 矩阵,当行的和大于 0 时。尽管这里涉及了一些数学运算,但仍然可以看作是一个规则引擎,因为我们(程序员)定义了规则。因此,我们理论上可以进入一个灰色区域,其中规则本身是通过机器学习发现的。但为了便于讨论,假设主刀医生随意选择 0 作为阈值,任何超过该值的都被认为是癌症:

def make_life_alterning_decision(X):
    """Determine whether something big happens"""
    row_sums = X.sum(axis=1)
    return (row_sums > 0).astype(int)
make_life_alterning_decision(X)

前面代码片段的输出如下:

array([0, 1, 0, 0, 1, 1, 1, 0, 1, 0])

如前所述,我们的规则引擎可以简单也可以复杂。在这里,我们不仅仅关注 row_sums,我们还有多个标准来判断某样东西是否为癌症。行中的最小值必须小于 -1.5,并且还需要满足以下三个标准之一或多个:

  • 行和超过 0

  • 行和能被 0.5 整除

  • 行的最大值大于1.5

因此,尽管我们的数学这里有些复杂,但我们仍然只是在构建一个规则引擎:

def make_more_complex_life_alterning_decision(X):
    """Make a more complicated decision about something big"""   
    row_sums = X.sum(axis=1)
      return ((X.min(axis=1) < -1.5) &
              ((row_sums >= 0.) |
               (row_sums % 0.5 == 0) |
               (X.max(axis=1) > 1.5))).astype(int)

make_more_complex_life_alterning_decision(X) 

前面代码的输出如下:

array([0, 1, 1, 1, 1, 1, 0, 1, 1, 0])

现在,假设我们的外科医生意识到他们并不是自己认为的那种数学或编程天才。于是,他们雇佣程序员为他们构建机器学习模型。该模型本身是一个函数,发现补充决策函数的参数,而决策函数本质上就是机器自己学习到的函数。所以,参数是我们将在接下来的第二章《实现参数化模型》中讨论的内容,它们是参数化模型。因此,当我们调用fit方法时,背后发生的事情是模型学习数据的特征和模式,以及X矩阵如何描述y向量。然后,当我们调用predict函数时,它会将学习到的决策函数应用于输入数据,从而做出合理的猜测:

from sklearn.linear_model import LogisticRegression

def learn_life_lession(X, y):
    """Learn a lesson abd apply it in a future situation"""
    model = LogisticRegression().fit(X, y)
    return (lambda X: model.predict(X))
educated_decision = learn_life_lession(X, y)(X)
educated_decision

前面代码的输出如下:

array([1, 1, 0, 0, 0, 1, 1, 0, 1, 0])

所以,现在我们到了需要明确界定有监督学习到底是什么的时刻。有监督学习正是我们刚才描述的那个例子。给定我们的样本矩阵X,以及对应标签的向量y,它学习一个函数,近似y的值或!

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/8bae7a15-da5a-4a4b-ab78-6e5a9587ff51.png

还有一些其他形式的机器学习不是有监督的,称为无监督学习。这些没有标签,更侧重于模式识别任务。所以,决定是否有监督的标志就是数据是否有标签。

回到我们之前的例子,当我们调用fit方法时,我们学习了新的决策函数,然后当我们调用predict时,我们正在近似新的y值。所以,输出就是我们刚才看到的这个!

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/4430bb58-e5a2-4710-8fb3-a957fc2e1684.png

有监督学习从标记样本中学习一个函数,该函数近似未来的y值。在这一点上,你应该能够清楚地解释这个抽象概念——也就是有监督机器学习的高层次概念。

攀升法和损失函数

在上一节中,我们已经熟悉了有监督机器学习的概念。现在,我们将学习机器如何在幕后进行学习。本节将探讨许多机器学习算法使用的常见优化技术——爬山法。它基于这样一个事实:每个问题都有一个理想状态,并且有一种方法来衡量我们离这个理想状态有多近或多远。需要注意的是,并不是所有的机器学习算法都使用这种方法。

损失函数

首先,我们将介绍损失函数,然后,在深入了解爬山法和下降法之前,我们将进行一个简短的数学复习。

这节课会涉及一些数学内容,虽然我们尽量避免纯理论的概念,但这正是我们必须要了解的内容,以便理解大多数这些算法的核心。课程结束时会有一个简短的应用部分。如果你无法记住某些微积分内容,不必恐慌;只要尽量理解黑盒背后发生的事情。

如前所述,机器学习算法必须衡量它与某个目标的接近程度。我们将此定义为成本函数或损失函数。有时,我们也听到它被称为目标函数。虽然并不是所有机器学习算法都旨在直接最小化损失函数,但我们将在这里学习规则而不是例外。损失函数的目的是确定模型拟合的好坏。它通常会在模型的学习过程中进行评估,并在模型最大化其学习能力时收敛。

一个典型的损失函数计算一个标量值,该值由真实标签和预测标签给出。也就是说,给定我们的实际y和预测的y,即https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/6adc46e5-4e64-4050-b55c-5c1d8e0e8e55.png。这个符号可能有点难懂,但它的意思是,某个函数L,我们将其称为损失函数,将接受真实值y和预测值https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/6fa4fd62-405e-4353-b7bf-35e9022c2f5a.png,并返回一个标量值。损失函数的典型公式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/18aa419c-7dd3-45df-8bcf-d7724f238d73.png

所以,我列出了几个常见的损失函数,它们可能看起来熟悉,也可能不熟悉。平方误差和SSE)是我们将在回归模型中使用的度量:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/fc07628b-c403-4b88-8337-300ee6bcf651.png

交叉熵是一个非常常用的分类度量:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/29b4930c-d8a3-4fd2-b805-b7a4466b5e58.png

在下面的图示中,左侧的L函数仅仅表示它是关于y的损失函数,并且是给定参数 theta 的https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/ba99764f-61f6-4266-806b-7acbb95ed809.png。因此,对于任何算法,我们都希望找到一组 theta 参数,使得损失最小化。也就是说,如果我们在预测房价时,我们可能希望尽可能准确地估算每平方英尺的价格,以最小化我们的预测误差。

参数通常存在于一个比视觉上能够表示的更高维的空间中。因此,我们关注的一个大问题是:如何最小化成本?通常情况下,我们不可能尝试所有可能的值来确定问题的真实最小值。所以,我们必须找到一种方法来下降这个模糊的损失山丘。困难的部分在于,在任何给定的点上,我们都不知道曲线是上升还是下降,除非进行某种评估。这正是我们要避免的,因为这样做非常昂贵:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/8dfdcaa5-bf54-47ff-a8af-dbfc885a2b29.png

我们可以将这个问题描述为在一个漆黑的房间里醒来,房间的地板不平,试图找到房间的最低点。你不知道房间有多大,也不知道它有多深或多高。你首先该在哪里踩?我们可以做的一件事是检查我们站立的位置,确定周围哪个方向是向下倾斜的。为此,我们需要测量曲线的坡度。

测量曲线的坡度

以下是标量导数的快速复习。为了计算任何给定点的坡度,标准的方式通常是测量我们感兴趣的点与某个割线点之间的坡度,我们称这个点为 delta x

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/2ec22951-f7ed-4b45-bc08-48fba2dd67ce.png

x与其邻近值 delta x之间的距离趋近于0,或者当我们的极限趋近于0时,我们就能得到曲线的坡度。这个坡度由以下公式给出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/30cef7ab-b444-4823-aacf-64bdda6d6016.png

有几种你可能熟悉的符号。其中一个是fx的导数。常数的坡度是0。因此,如果f(x)9,换句话说,如果y仅仅是9,它永远不变。没有坡度。所以,坡度是0,如图所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/7dcce59e-5617-411b-921e-e5497a64a9b4.png

我们也可以在第二个例子中看到幂律的作用。这在之后会非常有用。如果我们将变量乘以幂,并将幂减去一,我们得到以下结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/22f03da9-d95c-4c95-9857-020d6fe1b9f3.png

测量 Nd 曲线的坡度

为了测量一个向量或多维曲面的坡度,我们将引入偏导数的概念,偏导数就是对某一变量求导数,而将所有其他变量视为常数。因此,我们的解是一个维度为k的向量,其中k是我们函数所涉及的变量的数量。在这种情况下,我们有xy。我们求解的向量中每个相应的位置都是对相应函数的定位变量求导的结果。

从概念层面来看,我们所做的是保持一个变量不变,同时改变周围的其他变量,看看坡度如何变化。我们分母的符号表示我们正在测量哪个变量的坡度,并且是相对于该点进行的。因此,在这种情况下,第一个位置,d(x),表示我们正在对函数f进行关于x的偏导数,保持y不变。同样,第二个位置,我们对函数f进行关于y的导数,保持x不变。因此,最终得到的是一个梯度,它是一个非常重要的关键词。它只是一个偏导数的向量:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/62ac4d23-5f7d-460a-858a-bdd93f6f3646.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/e86981ff-c747-49ba-a53d-8ee22588f525.png

测量多个函数的坡度

然而,我们想要变得更复杂,计算多个函数在同一时间的斜率。我们最终得到的只是一个沿着行的梯度矩阵。在下面的公式中,我们可以看到之前示例中我们刚刚解出的结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/38704fda-a85a-4fec-9e1c-4fbbcc29ff32.png

在下一个公式中,我们引入了这个新的函数,称为g。我们看到了函数g的梯度,每个位置对应于相对于变量xy的偏导数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/0a984ad0-25f6-4b2f-8613-03876048985a.png

当我们将这些组合成一个矩阵时,得到的就是雅可比矩阵。你不需要解这个矩阵,但你应该理解我们所做的是计算一个多维表面的斜率。只要理解这一点,你可以把它当作一个黑盒。我们就是这样计算梯度和雅可比矩阵的:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/f5f178d0-8f97-4450-8043-e9fb391a1bfe.png

爬山与下坡

我们将回到之前的例子——我们观察过的失落的山丘。我们想找到一组能够最小化我们的损失函数L的 theta 参数。正如我们已经确定的那样,我们需要爬山或者下山,并且了解自己在相对于邻近点的位置,而不必计算所有的内容。为了做到这一点,我们需要能够测量曲线相对于 theta 参数的斜率。所以,回到之前的房屋例子,我们希望知道每平方英尺成本增量的正确值是多少。一旦知道了这个,我们就可以开始朝着找到最佳估计的方向迈出步伐。如果你做出了一个错误的猜测,你可以转身并朝着完全相反的方向前进。所以,我们可以根据我们的度量标准爬山或下山,这使得我们能够优化我们想要学习的函数的参数,而不管该函数本身的表现如何。这是一层抽象。这个优化过程称为梯度下降,它支持我们在本书中将讨论的许多机器学习算法。

以下代码展示了一个如何计算矩阵相对于 theta 的梯度的简单示例。这个示例实际上是逻辑回归学习部分的简化代码:

import numpy as np

seed = (42)

X = np.random.RandomState(seed).rand(5, 3).round(4)

y = np.array([1, 1, 0, 1, 0])

h = (lambda X: 1\. / (1\. + np.exp(-X)))

theta = np.zeros(3)

lam = 0.05

def iteration(theta):

    y_hat = h(X.dot(theta))

    residuals = y - y_hat

    gradient = X.T.dot(residuals)
    theta += gradient * lam
    print("y hat: %r" % y_hat.round(3).tolist())
    print("Gradient: %r" % gradient.round(3).tolist())
    print("New theta: %r\n" % theta.round(3).tolist())

iteration(theta)
iteration(theta)

在最开始,我们随机初始化了Xy,这并不是算法的一部分。所以,这里的x是 sigmoid 函数,也叫做逻辑函数。逻辑这个词来源于逻辑进展。这是逻辑回归中应用的必要变换。只需要理解我们必须应用它;这是函数的一部分。所以,我们初始化我们的theta向量,用来计算梯度,初始值是零。再强调一次,所有的值都是零。它们是我们的参数。现在,对于每次迭代,我们会得到我们的 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/c700ee8d-f92f-44b3-9ac0-54c1cb781e40.png,也就是我们估计的y,如果你还记得的话。我们通过将X矩阵与我们的 theta 参数做点积,并通过那个逻辑函数h,也就是我们的 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/9fa6e3b4-1d08-487f-b97c-17fbef8dfa08.png,来得到这个值。

现在,我们想计算残差和预测器输入矩阵X之间的点积的梯度。我们计算残差的方式是将y减去 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/038cd708-4e73-4af7-a532-e5f7a70612f5.png,这就得到了残差。现在,我们得到了我们的 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/e627b377-2a40-4d23-9e9d-409a0db456e6.png。我们怎么得到梯度呢?梯度就是输入矩阵X和这些残差之间的点积。我们将利用这个梯度来确定我们需要朝哪个方向迈步。我们做的方式是将梯度加到我们的 theta 向量中。Lambda 调节我们在梯度上向上或向下迈步的速度。所以,它是我们的学习率。如果你把它当作步长——回到那个黑暗房间的例子——如果步长太大,很容易越过最低点。但如果步长太小,你会在房间里不停地绕圈子。所以,这是一种平衡,但它使我们能够调节更新 theta 值和下降梯度的速度。再次强调,这个算法将在下一章中讲解。

我们得到前面代码的输出如下:

y hat: [0.5, 0.5, 0.5, 0.5, 0.5]
Gradient: [0.395, 0.024, 0.538]
New theta: [0.02, 0.001, 0.027]

y hat: [0.507, 0.504, 0.505, 0.51, 0.505]
Gradient: [0.378, 0.012, 0.518]
New theta: [0.039, 0.002, 0.053]

这个例子展示了当我们调整系数时,梯度或斜率是如何变化的,反之亦然。

在下一节中,我们将学习如何评估我们的模型,并了解神秘的train_test_split

模型评估与数据划分

在这一章中,我们将定义评估模型的意义、评估模型效果的最佳实践、如何划分数据,以及在准备数据划分时你需要考虑的若干事项。

理解一些机器学习的核心最佳实践非常重要。作为机器学习从业者,我们的主要任务之一是创建一个能够有效预测新数据的模型。那么我们如何知道一个模型是否优秀呢?如果你回想一下上一部分,我们将监督学习定义为从标注数据中学习一个函数,以便能够近似新数据的目标。因此,我们可以测试模型的有效性。我们可以评估它在从未见过的数据上的表现——就像它在参加考试一样。

样本外评估与样本内评估

假设我们正在训练一台小型机器,这是一个简单的分类任务。这里有一些你需要了解的术语:样本内数据是模型从中学习的数据,而样本外数据是模型以前从未见过的数据。许多新手数据科学家的一个常见错误是,他们在模型学习的同一数据上评估模型的效果。这样做的结果是奖励模型的记忆能力,而不是其概括能力,这两者有着巨大的区别。

如果你看这里的两个例子,第一个展示了模型从中学习的样本,我们可以合理地相信它将预测出一个正确的结果。第二个例子展示了一个新的样本,看起来更像是零类。当然,模型并不知道这一点。但一个好的模型应该能够识别并概括这种模式,如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/2fbc7355-5703-4c20-86a5-84d65ef157cb.png

所以,现在的问题是如何确保模型的样本内数据样本外数据能够证明其有效性。更精确地说,我们的样本外数据需要被标注。新的或未标注的数据是不够的,因为我们必须知道实际答案,以便确定模型的准确性。因此,机器学习中应对这种情况的一种方式是将数据分为两部分:训练集和测试集。训练集是模型将从中学习的数据;测试集是模型将接受评估的数据。你拥有的数据量非常重要。实际上,在接下来的部分中,当我们讨论偏差-方差权衡时,你会看到一些模型需要比其他模型更多的数据来进行学习。

另一点需要记住的是,如果某些变量的分布高度偏斜,或者你的数据中嵌入了稀有的类别水平,甚至你的y向量中存在类别不平衡,你可能会得到一个不理想的划分。举个例子,假设在你的X矩阵中有一个二元特征,表示某个事件的传感器是否存在,而该事件每 10,000 次发生才会触发一次。如果你随机划分数据,所有正向的传感器事件都在测试集里,那么你的模型会从训练数据中学到传感器从未被触发,可能会认为它是一个不重要的变量,而实际上,它可能是一个非常重要、且具有很强预测能力的变量。所以,你可以通过分层抽样来控制这些类型的问题。

划分变得简单

这里有一段简单的代码示例,演示了如何使用 scikit-learn 库将数据划分为训练集和测试集。我们从 datasets 模块加载数据,并将Xy传递给划分函数。我们应该熟悉数据的加载。我们使用了sklearn中的model_selection子模块的train_test_split函数。这个函数可以接受任意数量的数组。所以,20%将作为test_size,剩余的 80%作为训练集。我们定义了random_state,以便如果需要证明我们是如何得到这个划分的,它可以被重现。这里还有一个stratify关键词,虽然我们没有使用它,它可以用于针对稀有特征或不平衡的y向量进行分层抽样。

from sklearn.datasets import load_boston

from sklearn.model_selection import train_test_split

boston_housing = load_boston() # load data

X, y = boston_housing.data, boston_housing.target # get X, y

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,

                                                                                                              random_state=42)

# show num samples (there are no duplicates in either set!)
print("Num train samples: %i" % X_train.shape[0])

print("Num test samples: %i" % X_test.shape[0])

上述代码的输出如下:

Num train samples: 404
Num test samples: 102

总结

在本章中,我们介绍了监督学习,搭建了我们的环境,并学习了爬山法和模型评估。到目前为止,你应该理解了机器学习背后的抽象概念基础。这一切都与优化多个损失函数有关。在下一章,我们将深入探讨参数化模型,甚至从零开始编写一些流行算法的代码。

第二章:实现参数化模型

在上一章中,我们学习了监督式机器学习的基础知识。在本章中,我们将深入探讨几个流行的监督学习算法,这些算法属于参数化建模范畴。我们将从正式介绍参数化模型开始。然后,我们将介绍两个非常流行的参数化模型:线性回归和逻辑回归。我们将花些时间了解它们的内部工作原理,然后跳进 Python,实际编写这些工作原理的代码,从零开始实现。

在本章中,我们将涵盖以下主题:

  • 参数化模型

  • 从零开始实现线性回归

  • 逻辑回归模型

  • 从零开始实现逻辑回归

  • 参数化模型的优缺点

技术要求

对于本章内容,如果你还没有安装以下软件,请安装:

  • Jupyter Notebook

  • Anaconda

  • Python

本章的代码文件可以在 https:/​/​github.​com/​PacktPublishing/找到。

Supervised-Machine-Learning-with-Python.

参数化模型

在监督学习中,有两类学习算法:参数化非参数化。这一领域也恰好是关于哪种方法更好的门槛和基于观点的猜测的热议话题。基本上,参数化模型是有限维度的,这意味着它们只能学习有限数量的模型参数。它们的学习阶段通常是通过学习一些向量θ来进行的,这个向量也叫做系数。最后,学习函数通常是已知的形式,我们将在本节稍后做出解释。

有限维度模型

如果我们回顾一下监督学习的定义,记住我们需要学习某个函数,f。一个参数化模型将通过有限数量的总结点来概括X(我们的矩阵)与y(我们的目标)之间的映射关系。总结点的数量通常与输入数据中的特征数量相关。因此,如果有三个变量,f将尝试在θ中给定三个值的情况下,总结Xy之间的关系。这些被称为模型参数 f: y=f(X)

让我们回顾一下并解释一些参数化模型的特性。

参数化学习算法的特性

现在我们将介绍参数化学习算法的不同特点:

  • 模型参数通常被限制在与输入空间相同的维度

  • 你可以指向一个变量及其相应的参数值,通常可以了解该变量的重要性或它与y(我们的目标)的关系

  • 最后,传统上它们运行速度较快,并且不需要太多数据

参数化模型示例

假设我们被要求根据房屋的面积和浴室数量来估算房屋的价格。我们需要学习多少个参数?在我们的例子中,我们将需要学习多少个参数?

好吧,给定面积和浴室数量,我们将需要学习两个参数。因此,我们的函数可以表示为给定两个变量——面积和浴室数量——的估计价格——P1P2P1表示面积与价格之间的关系。P2表示浴室数量与价格之间的关系。

以下代码展示了在 Python 中设置的问题。x1是我们的第一个变量——房屋的面积。你可以看到它的范围从最小的1200到最高的4000,我们的价格范围从200000500000。在x2中,我们有浴室的数量。这个范围从最少的1到最多的4

import numpy as np
from numpy import linalg

x1 = np.array([2500, 2250, 3500, 4000, 1890, 1200, 2630])
x2 = np.array([3, 2, 4, 3, 2, 1, 2])
y = np.array([260000, 285000, 425000, 482500, 205000, 220000, 320000])

现在,你可以从我们的图表中看到,这里似乎有一个正相关的趋势,这很有道理。但我们会随着深入这个例子而发现更多:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/3082ec42-423e-4e6f-a435-fd878d94150c.png

现在,我们要学习一个估算价格的函数,这个函数实际上就是我们数据中每一行向量的估计参数的内积。因此,我们在这里执行的是线性回归。线性回归可以通过最小二乘法方程方便地求解。由于我们技术上有无数个可能的解,最小二乘法将找到一个解,该解最小化平方误差的和。

在这里,我们对X完成了最小二乘法,并在第一个单元格中学习了我们的参数。然后,在下一个单元格中,我们将X与我们刚学习的 theta 参数相乘以得到预测结果。因此,如果你深入研究,你会发现只有一个房子我们严重低估了它的价值:倒数第二个房子,它的面积是1200平方英尺,只有一个浴室。所以,它可能是一个公寓,而且可能位于市区的一个非常热门的地方,这就是为什么它最初被定价这么高的原因:

# solve for the values of theta
from numpy import linalg
X = np.hstack((x1.reshape(x1.shape[0], 1), x2.reshape(x2.shape[0], 1)))
linalg.lstsq(X, y)[0]

# get the estimated y values
X.dot(linalg.lstsq(X,y)[0])

上述代码片段的输出结果如下:

array([   142.58050018, -23629.43252307])

array([285562.95287566, 273547.26035425, 404514.02053054, 499433.70314259,
       222218.28029018, 147467.16769047, 327727.85042187])

现在,来分析我们的参数。每增加一平方英尺,估算价格将上涨 142.58 美元,这在直观上是有道理的。然而,每增加一个浴室,我们的房子价值将减少 24,000 美元:Price = 142.58 sqft + -23629.43bathrooms

这里有另一个难题。根据这个逻辑,如果我们有一座 3000 平方英尺且没有浴室的房子,它的价格应该和一座 4000 平方英尺、拥有四个浴室的房子差不多。所以显然,我们的模型存在一些局限性。当我们尝试用少量特征和数据来总结映射关系时,肯定会出现一些不合逻辑的情况。但是,还有一些我们未曾考虑的因素可以在拟合线性回归时帮助我们。首先,我们没有拟合截距,也没有对特征进行中心化。所以,如果你回忆起初中的代数,或者甚至早期高中的代数,你会记得,当你在笛卡尔坐标图上拟合最优直线时,截距是线与y轴的交点,而我们没有拟合这样的截距。

在下面的代码中,我们在求解最小二乘法之前已对数据进行了中心化,并估计了截距,该截距实际上是y的平均值,实际价格减去X变量的内积,X的列均值与估计的参数的内积:

X_means = X.mean(axis=0) # compute column (variable) name
X_center = X - X_means  # center our data
theta = linalg.lstsq(X_center, y)[0]  # solve lstsq
print ("Theta: %r" % theta)

intercept = y.mean() -np.dot(X_means, theta.T) # find the intercept
print("Intercept: %.2f" % intercept)
print("Preds: %r" % (X.dot(theta.T) + intercept))

上述代码片段的输出如下:

Theta: array([ 128.90596161, -28362.07260241])
Intercept: 51887.87
Preds: array([289066.55823365, 285202.14043457, 389610.44723722, 482425.50064261,
 238795.99425642, 178212.9533507 , 334186.40584484])

这就总结了线性回归背后的数学和概念介绍,以及参数化学习的概念。在线性回归中,我们只是在一组数据点上拟合一条最佳直线,试图最小化平方误差的和。在接下来的部分,我们将了解 PyCharm,并逐步演示如何从头开始编写一个线性回归类。

从头实现线性回归

线性回归通过求解最小二乘方程来发现参数向量 theta。在本节中,我们将逐步讲解packtml Python 库中线性回归类的源代码,并简要介绍examples目录中的图形示例。

在我们查看代码之前,我们将介绍书中所有估计器背后的接口。它被称为BaseSimpleEstimator,是一个抽象类。它将强制执行唯一的一个方法,即predict。不同的子类层将强制执行其他方法,以适应不同的模型家族。但这个层次支持我们将构建的所有模型,因为我们正在构建的所有内容都是监督学习的,所以它们都需要具备预测能力。你会注意到,这个签名在dock字符串中有明确规定。每个模型的签名都会接受Xy,以及其他任何模型超参数:

class BaseSimpleEstimator(six.with_metaclass(ABCMeta)):
 """Base class for packt estimators.
 The estimators in the Packt package do not behave exactly like 
 scikit-learn
 estimators (by design). They are made to perform the model fit 
 immediately upon class instantiation. Moreover, many of the hyper
 parameter options are limited to promote readability and avoid
 confusion.
 The constructor of every Packt estimator should resemble the 
 following::
 def __init__(self, X, y, *args, **kwargs):
 ...
 where ``X`` is the training matrix, ``y`` is the training target
 variable,
 and ``*args`` and ``**kwargs`` are varargs that will differ for each
 estimator.
 """
 @abstractmethod
 def predict(self, X):
 """Form predictions based on new data.
 This function must be implemented by subclasses to generate
 predictions given the model fit.
 Parameters
 ----------
 X : array-like, shape=(n_samples, n_features)
 The test array. Should be only finite values.
 """

BaseSimpleEstimator 接口

第一次刷洗(flush)与 scikit-learn 的基础估计器接口类似。但也有几个不同之处。首先,当我们构建模型时,我们不会允许太多选项。此外,模型在实例化时立即开始训练。这与 scikit-learn 不同,因为我们没有fit方法。scikit-learn 有一个fit方法来允许网格搜索和超参数调优。因此,这也是我们与它们的签名不同的另一个原因。好了,接下来让我们看一下线性回归:

  1. 如果你有 PyCharm,赶快打开它。我们将位于packtmlHands-on-Supervised-Machine-Learning-with-Python库中,如下所示的代码所示。你可以看到这是在 PyCharm 中。这只是项目的根目录,而我们将要使用的包是packtml。我们将逐步讲解simple_regression.py文件代码的工作原理。如果你没有使用 PyCharm,Sublime 是一个替代选择,或者你可以使用任何你喜欢的文本编辑器:
from __future__ import absolute_import

from sklearn.utils.validation import check_X_y, check_array

import numpy as np
from numpy.linalg import lstsq

from packtml.base import BaseSimpleEstimator

__all__ = [
 'SimpleLinearRegression'
]

class SimpleLinearRegression(BaseSimpleEstimator):
 """Simple linear regression.

 This class provides a very simple example of straight forward OLS
 regression with an intercept. There are no tunable parameters, and
 the model fit happens directly on class instantiation.

 Parameters
 ----------
 X : array-like, shape=(n_samples, n_features)
 The array of predictor variables. This is the array we will use
 to regress on ``y``.

base.py,它位于我们的包级别内,将提供BaseSimpleEstimator的接口,我们将在整个包中使用。唯一将在抽象级别强制执行的方法是predict函数。该函数将接受一个参数,即X。我们已经提到过,监督学习意味着我们将学习一个函数,f,给定Xy,从而能够近似!,给定!,或者在这种情况下的X测试。由于每个子类将实现不同的predict方法,我们将使用抽象方法,即base,如下所示的代码片段:

@abstractmethod
   def predict(self, X):
       """Form predictions based on new data.
       This function must be implemented by subclasses to generate
       predictions given the model fit.
       Parameters
       ----------
       X : array-like, shape=(n_samples, n_features)
       The test array. Should be only finite values.
      """
  1. 接下来,在regression子模块内,我们将打开simple_regression.py文件。这个文件将实现一个名为SimpleLinearRegression的类。我们称它为“简单”,是为了避免与 scikit-learn 的线性回归混淆:
from __future__ import absolute_import

from sklearn.utils.validation import check_X_y, check_array

import numpy as np
from numpy.linalg import lstsq

from ..base import BaseSimpleEstimator

__all__ = [
    'SimpleLinearRegression'
]

class SimpleLinearRegression(BaseSimpleEstimator):
    """Simple linear regression.

    This class provides a very simple example of straight forward OLS
    regression with an intercept. There are no tunable parameters, and
    the model fit happens directly on class instantiation.

    Parameters
    ----------
    X : array-like, shape=(n_samples, n_features)
        The array of predictor variables. This is the array we will use
        to regress on ``y``.

SimpleLinearRegression将接受两个参数。X,即我们的协方差矩阵,和y,即训练目标,具体解释如下:

Parameters
    ----------
    X : array-like, shape=(n_samples, n_features)
        The array of predictor variables. This is the array we will use
        to regress on ``y``.

    y : array-like, shape=(n_samples,)
        This is the target array on which we will regress to build
        our model.
    Attributes
    ----------
    theta : array-like, shape=(n_features,)
        The least-squares solution (the coefficients)

    rank : int
        The rank of the predictor matrix, ``X``

    singular_values : array-like, shape=(n_features,)
        The singular values of ``X``

    X_means : array-like, shape=(n_features,)
        The column means of the predictor matrix, ``X``

    y_mean : float
        The mean of the target variable, ``y``

    intercept : float
        The intercept term
    """
    def __init__(self, X, y):
        # First check X, y and make sure they are of equal length, no
        NaNs
        # and that they are numeric
        X, y = check_X_y(X, y, y_numeric=True,
                         accept_sparse=False) # keep it simple
  1. 现在,在我们的签名中,我们将在init函数内做的第一件事是通过 scikit-learn 的check_X_y运行它。我们将确保Xy之间的维度匹配,因为如果传递的训练目标向量小于X中的样本数或反之,将无法正常工作。我们还强制要求y中的所有内容都是数值型的。

  2. 接下来我们需要做的是计算X中每列的均值,以便我们可以对所有数据进行中心化,同时计算y中值的均值,以便我们也可以对其进行中心化。在这个整个函数中,我们提取自 NumPy 库的最小二乘优化函数。因此,我们只需要输入现在已经中心化的Xy,并传递给lstsq。我们将返回三样东西,其中第一项是 theta,这是学习得到的参数。所以,X.theta将是y的最佳近似值。接下来我们将得到秩,这就是matrix的秩,以及singular_values,如果你想深入研究实际解的分解过程。正如上一节所讨论的关于房屋平均成本的问题,如果我们计算一栋房子价值减去X_means的内积,列均值是一个向量乘以 theta,另一个向量。因此,在这里我们将得到一个标量值作为截距,并将分配一些self属性:


# We will do the same with our target variable, y
X_means = np.average(X, axis=0)
y_mean = y.mean(axis=0)

# don't do in place, so we get a copy
X = X - X_means
y = y - y_mean

# Let's compute the least squares on X wrt y
# Least squares solves the equation `a x = b` by computing a
# vector `x` that minimizes the Euclidean 2-norm `|| b - a x ||²`.
theta, _, rank, singular_values = lstsq(X, y, rcond=None)

# finally, we compute the intercept values as the mean of the target
# variable MINUS the inner product of the X_means and the coefficients
intercept = y_mean - np.dot(X_means, theta.T)

# ... and set everything as an instance attribute
self.theta = theta
self.rank = rank
self.singular_values = singular_values

# we have to retain some of the statistics around the data too
self.X_means = X_means
self.y_mean = y_mean
self.intercept = intercept

一旦你实例化了一个类,你就完成了线性回归的拟合。然而,我们必须重写BaseSimpleEstimator超类中的predict函数。为了进行预测,所要做的就是计算Xtheta的内积,以及我们已经学习到的参数,再加上截距。与构造函数中看到的不同之处在于,我们不需要重新中心化X。如果一个X测试数据进来,唯一需要中心化数据的时刻是在学习参数时,而不是在应用它们时。然后,我们将用X乘以参数,计算内积,再加上截距。现在我们得到了一个预测值的向量!

def predict(self, X):
        """Compute new predictions for X"""
        # copy, make sure numeric, etc...
        X = check_array(X, accept_sparse=False, copy=False) # type: np.ndarray

        # make sure dims match
        theta = self.theta
        if theta.shape[0] != X.shape[1]:
            raise ValueError("Dim mismatch in predictors!")

        # creates a copy
        return np.dot(X, theta.T) + self.intercept

  1. 现在,我们可以继续查看我们的一个示例。在项目级别打开examples目录,然后打开regression目录。我们将查看example_linear_regression.py文件,如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/fe4735c6-9f3c-4fbc-9e3f-f88a0b11ded3.png

让我们逐步走过这里发生的过程,向你展示如何将它应用于真实数据。我们将加载刚才创建的线性回归,并导入 scikit-learn 的线性回归,以便进行结果比较。我们要做的第一件事是创建一个包含500个样本和2个维度的随机值X矩阵。然后我们将创建y矩阵,它将是第一个X变量和0的线性组合,即第一个列的2倍加上第二列的1.5倍。我们这么做是为了展示我们的线性回归类将学习到这些确切的参数,21.5,如以下代码片段所示:

from packtml.regression import SimpleLinearRegression
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from matplotlib import pyplot as plt
import numpy as np
import sys

# #############################################################################
# Create a data-set that perfectly models the linear relationship:
# y = 2a + 1.5b + 0
random_state = np.random.RandomState(42)
X = random_state.rand(500, 2)
y = 2\. * X[:, 0] + 1.5 * X[:, 1]

正如我们之前讨论过的,我们需要分割数据。你绝不应该仅仅对样本数据进行评估和拟合;否则,你容易发生过拟合:

# split the data
X_train, X_test, y_train, y_test = train_test_split(X, y,

                                                   random_state=random_state)
  1. 接下来,我们将拟合我们的线性回归并计算我们的预测结果。因此,我们还可以通过我们的断言来显示我们学习到的θ与我们期望的实际θ非常接近,即21.5。因此,我们的预测应该与y训练输入相似:
# Fit a simple linear regression, produce predictions
lm = SimpleLinearRegression(X_train, y_train)
predictions = lm.predict(X_test)
print("Test sum of residuals: %.3f" % (y_test - predictions).sum())
assert np.allclose(lm.theta, [2., 1.5])

  1. 接下来,我们将拟合一个 scikit-learn 回归模型,展示我们获得的结果与之前的相似,如果不是完全相同的话。我们将展示我们刚刚创建的类中的θ与 scikit-learn 生成的系数相匹配。scikit-learn 是一个经过充分测试和广泛使用的库。因此,它们匹配的事实表明我们走在正确的道路上。最后,我们可以展示我们的预测结果非常接近 scikit-learn 的解决方案:
# Show that our solution is similar to scikit-learn's

lr = LinearRegression(fit_intercept=True)
lr.fit(X_train, y_train)
assert np.allclose(lm.theta, lr.coef_)
assert np.allclose(predictions, lr.predict(X_test))
  1. 接下来,我们将在一个类上进行线性回归拟合,这样我们就可以查看图表。为此,请运行以下示例:
# Fit another on ONE feature so we can show the plot
X_train = X_train[:, np.newaxis, 0]
X_test = X_test[:, np.newaxis, 0]
lm = SimpleLinearRegression(X_train, y_train)

# create the predictions & plot them as the line
preds = lm.predict(X_test)
plt.scatter(X_test[:, 0], y_test, color='black')
plt.plot(X_test[:, 0], preds, linewidth=3)

# if we're supposed to save it, do so INSTEAD OF showing it
if len(sys.argv) > 1:
    plt.savefig(sys.argv[1])
else:
    plt.show()
  1. 进入Hands-on-Supervised-Machine-Learning-with-Python-master的顶部项目目录下的终端:即项目级别。记得激活环境。如果还没有激活环境,Unix 用户需要输入source activate,或者直接通过输入以下命令激活:
source activate packt-sml
  1. 通过输入文件名运行此示例,即examples/regression/example_linear_regression.py

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/adf40228-6a5b-486e-b22c-ed1b546c7a9c.png

当我们运行前面的代码时,我们应该会看到我们的图表,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/23f870bd-3a57-4f1a-99ac-4803aa1e57f8.png

我们可以看到残差的总和基本为零,这意味着我们的预测非常准确。这个情况比较简单,因为我们创建了一个可以学习到确切θ值的场景。你可以看到这里我们正在对一个变量进行拟合。由于我们仅在一个变量上进行学习,所以这是一个更为近似的结果。从定性和定量上看,这与我们通过 scikit-learn 的预测和系数所期望的结果相匹配。

在下一节中,我们将学习逻辑回归模型。

逻辑回归模型

在本节中,我们将介绍逻辑回归,它是我们将要覆盖的第一个爬坡算法,并简要回顾线性回归。我们还将看看逻辑回归在数学上和概念上的不同之处。最后,我们将学习核心算法并解释它是如何进行预测的。

概念

逻辑回归在概念上是线性回归的逆。如果我们想要的是离散值或类别,而不是一个真实值呢?我们已经看到过这种类型的问题的一个例子,早期我们曾想预测一封电子邮件是否是垃圾邮件。因此,使用逻辑回归时,我们可以预测类别成员的概率,也就是所谓的分类。

数学原理

在数学上,逻辑回归与线性回归非常相似。我们参数和X的内积表示类别成员的对数几率,这实际上是概率除以 1 减去概率的自然对数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/1c378b72-3456-40b9-a380-9b0bcecbc30c.png

我们真正想要的是类别成员的概率。我们可以从对数几率中反推出概率,并使用 sigmoid 或逻辑函数来确定这些概率。

逻辑(sigmoid)变换

在接下来的代码中,我们将创建一个X向量,其值介于-1010之间,然后应用逻辑变换得到y,然后我们可以将其绘制出来:

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

x = np.linspace(-10, 10, 10000)
y = 1\. / (1 + np.exp(-x)) # sigmoid transformation
plt.plot(x, y)

如你所见,我们得到一个 S 形曲线,原始的X值在x轴上,y值在y轴上。请注意,所有的值都被映射到y轴上的 0 到 1 之间。现在,这些可以被解释为概率:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/aaaab465-81eb-4407-ad05-b3699b28a07d.png

算法

现在,我们已经简要介绍了前面章节中的逻辑回归算法。这里是我们如何学习参数的回顾:

  1. 我们首先将 theta 初始化为零向量:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/d7b1d136-7920-4d85-8c0c-b2a8fbb157cb.png

  1. 由于这是一个爬坡算法,它是迭代的。因此,对于每次迭代,我们计算对数几率,即 theta 转置乘以X,然后通过逻辑变换进行转换:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/11564b99-d4fb-4fd5-9fcf-81418826430c.png

  1. 接下来,我们计算梯度,这是我们函数斜率的偏导数向量,这在上一节中已经介绍过。我们只需要计算这个值为X转置乘以残差,y - https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/e35e83e5-16a8-41b6-b237-2a443870a922.png. 请记住,*https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/26c98c1f-1dc1-478e-8a0b-2b25162bb75b.png*现在是经过逻辑变换后的概率:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/9d1b6e26-2d31-468d-8c23-c0a2e1ad025a.png

  1. 最后,我们可以通过将 theta 加上梯度来更新我们的系数。你还可以看到一个!参数,这仅仅是一个学习率参数。它控制我们在每一步中允许系数增长的幅度:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/b40eca30-20d9-43e9-869d-cf52a42fc3f2.png

创建预测

我们最终使梯度收敛,梯度不再更新我们的系数,最终留下的是一堆类别概率。那么,我们该如何生成预测呢?我们只需要超越一个给定的阈值,就能得到类别。因此,在本节中,我们将使用一个二分类问题。但是,对于多分类问题,我们只需对每个类别使用 argmax 函数。现在,我们将生成离散预测,如以下代码所示:

sigmoid = (lambda x: 1\. / (1 + np.exp(-x)))
log_odds = np.array([-5.6, 8.9, 3.7, 0.6, 0.])
probas = sigmoid(log_odds)
classes = np.round(probas).astype(int)
print("Log odds: %r\nProbas: %r\nClasses: %r"
      % (log_odds, probas, classes))

前面代码的输出如下:

Log odds: array([-5.6, 8.9, 3.7, 0.6, 0\. ])
Probas: array([0.00368424, 0.99986363, 0.97587298, 0.64565631, 0.5 ])
Classes: array([0, 1, 1, 1, 0])

在下一节中,我们将展示如何从头开始在packtml包中实现逻辑回归。

从头开始实现逻辑回归

在本节中,我们将逐步讲解在packtml包中使用 Python 实现逻辑回归的过程。我们将简要回顾一下逻辑回归的目标,然后讲解源代码并查看一个示例。

回想一下,逻辑回归旨在将一个样本分类到一个离散的类别中,也称为分类。逻辑转换使我们能够将从参数和X的内积中得到的对数赔率转换过来。

注意,我们打开了三个 Python 文件。一个是extmath.py,位于packtml中的utils目录下;另一个是simple_logistic.py,位于packtml中的regression库下;最后一个是example_logistic_regression.py文件,位于examples目录和regression文件夹中。

我们将通过以下步骤直接进入代码库:

  1. 我们将从extmath.py文件开始。这里有两个函数我们将使用。第一个是log_likelihood,这是我们希望在逻辑回归中最大化的目标函数:
def log_likelihood(X, y, w):
    """Compute the log-likelihood function.

    Computes the log-likelihood function over the training data.
    The key to the log-likelihood is that the log of the product of
    likelihoods becomes the sum of logs. That is (in pseudo-code),

        np.log(np.product([f(i) for i in range(N)]))

    is equivalent to:

        np.sum([np.log(f(i)) for i in range(N)])

    The log-likelihood function is used in computing the gradient for
    our loss function since the derivative of the sum (of logs) is equivalent
    to the sum of derivatives, which simplifies all of our math.
  1. log_likelihood函数的具体实现并不是理解逻辑回归工作原理的关键。但本质上,你可以看到我们将对y与对数赔率的乘积求和,再减去1加上对数赔率指数的对数。这里加权的实际上是对数赔率,即X.dot(w),其中w是我们正在学习的θ。这就是目标函数。所以,我们在对这些对数进行求和:
 weighted = X.dot(w)
 return (y * weighted - np.log(1\. + np.exp(weighted))).sum()
  1. 第二个是logistic_sigmoid函数,我们将深入学习它。这是我们如何从对数赔率中反推得到类别概率的方法,它的计算方式是1除以1加上对数赔率的指数,其中x在这里是对数赔率:
def logistic_sigmoid(x):
    """The logistic function.

    Compute the logistic (sigmoid) function over a vector, ``x``.

    Parameters
    ----------
    x : np.ndarray, shape=(n_samples,)
        A vector to transform.
    """
    return 1\. / (1\. + np.exp(-x))
  1. 我们将在逻辑回归类中使用这两个函数。因此,在simple_logistic.py中,你将看到一个类似于我们在上一节中使用的线性回归类的类:
# -*- coding: utf-8 -*-

from __future__ import absolute_import

from sklearn.utils.validation import check_X_y, check_array

import numpy as np
from packtml.utils.extmath import log_likelihood, logistic_sigmoid
from packtml.utils.validation import assert_is_binary
from packtml.base import BaseSimpleEstimator

__all__ = [
    'SimpleLogisticRegression'
]

try:
    xrange
except NameError: # py 3 doesn't have an xrange
    xrange = range

class SimpleLogisticRegression(BaseSimpleEstimator):
    """Simple logistic regression.

    This class provides a very simple example of straight forward logistic
    regression with an intercept. There are few tunable parameters aside from
    the number of iterations, & learning rate, and the model is fit upon
    class initialization.
  1. 现在,这个函数或类扩展了BaseSimpleEstimator。我们将稍后重写predict函数,构造函数将拟合模型并学习参数。因此,这个类有四个超参数。第一个是X,即我们的训练数据;然后是y,作为我们的训练标签;n_steps回忆一下,逻辑回归是一个迭代模型。所以,n_steps是我们将执行的迭代次数,learning_rate是我们的λ。如果你回顾一下算法本身,这控制着我们如何根据梯度快速更新θ;最后是loglik_interval,这是一个辅助参数。计算对数似然可能非常昂贵。我们可以在下面的代码片段中看到这个解释:
Parameters
    ----------
X : array-like, shape=(n_samples, n_features)
        The array of predictor variables. This is the array we will use
        to regress on ``y``.

y : array-like, shape=(n_samples,)
        This is the target array on which we will regress to build
        our model. It should be binary (0, 1).

n_steps : int, optional (default=100)
        The number of iterations to perform.

learning_rate : float, optional (default=0.001)
        The learning rate.

loglik_interval : int, optional (default=5)
        How frequently to compute the log likelihood. This is an expensive
        operation--computing too frequently will be very expensive.
  1. 最后,我们得到theta、参数、intercept以及log_likelihood,后者只是每个区间计算出的对数似然值的列表。我们将首先检查我们的Xy是否符合预期,即0, 1。我们不会做接近 scikit-learn 能够完成的事情。我们也不会允许不同的字符串类别:
def __init__(self, X, y, n_steps=100, learning_rate=0.001,
                 loglik_interval=5):
        X, y = check_X_y(X, y, accept_sparse=False, # keep dense for example
                         y_numeric=True)

        # we want to make sure y is binary since that's all our example covers
        assert_is_binary(y)

        # X should be centered/scaled for logistic regression, much like
        # with linear regression
        means, stds = X.mean(axis=0), X.std(axis=0)
        X = (X - means) / stds
  1. 接下来,我们想要确保它实际上是二值的。这是因为我们正在执行逻辑回归,这是一个在01之间离散的过程。回归有一个广义的形式,叫做softmax 回归,它允许我们使用多个类别进行分类。这是一个多分类问题。当我们深入神经网络时,会讨论到这个。现在,我们将这个问题限制为二分类问题。

  2. 接下来,我们想要对X矩阵进行中心化和标准化。这意味着我们将从X中减去列的均值并将其除以标准差。所以,我们有均值为0,标准差为1

# since we're going to learn an intercept, we can cheat and set the
# intercept to be a new feature that we'll learn with everything else
X_w_intercept = np.hstack((np.ones((X.shape[0], 1)), X))
  1. 现在,我们可以在学习线性回归参数或逻辑回归参数时做一些巧妙的事情,这是线性回归中无法做到的。我们可以在学习时将截距添加到矩阵中,而不是事后计算它。我们将在X矩阵上创建一个全为 1 的向量,作为新的特征,如下所示:
 # initialize the coefficients as zeros
 theta = np.zeros(X_w_intercept.shape[1])
  1. 正如我们在算法中定义的那样,我们从定义θ等于零开始。参数的数量等于X中的列数。对于每次迭代,我们将在这里计算对数几率。然后,我们使用逻辑 sigmoid 对其进行转换。我们将计算我们的残差为y - preds。所以,在这一点上,preds是概率。y可以视为二分类问题中的类别概率,其中1表示某事属于类别1的概率为 100%,而0表示某事属于类别1的概率为 0%:
 # now for each step, we compute the inner product of X and the
 # coefficients, transform the predictions with the sigmoid function,
 # and adjust the weights by the gradient
 ll = []
 for iteration in xrange(n_steps):
     preds = logistic_sigmoid(X_w_intercept.dot(theta))
     residuals = y - preds # The error term
     gradient = X_w_intercept.T.dot(residuals)

所以,我们可以从y中减去概率来得到我们的残差。为了得到我们的梯度,我们将进行X与残差的乘法,即那里的内积。请记住,梯度是我们函数的斜率的偏导数向量。

  1. 我们将通过将梯度乘以学习率来更新theta和参数。学习率是一个控制学习速度的 lambda 函数。你可能记得,如果我们学习得太快,可能会越过全局最优解,最终得到一个非最优解。如果我们学习得太慢,那我们将需要很长时间来拟合。逻辑回归是一个有趣的案例;因为它实际上是一个凸优化问题,我们将有足够的迭代次数来达到全局最优解。所以,这里的learning_rate有点讽刺意味,但一般来说,爬坡函数是通过使用learning_rate来工作的:
# update the coefficients
theta += learning_rate * gradient

# you may not always want to do this, since it's expensive. Tune
# the error_interval to increase/reduce this
if (iteration + 1) % loglik_interval == 0:
    ll.append(log_likelihood(X_w_intercept, y, theta))
  1. 最后一步是,如果我们处于适当的间隔,我们将计算log_likelihood。现在,虽然你可以在每次迭代时都计算这个函数,但那样会花费很长时间。我们可以选择每 5 分钟或 10 分钟计算一次,这样可以让我们看到我们正在优化这个函数。但同时,这也意味着我们不需要在每次迭代时都计算它。

  2. 最后,我们将把所有这些作为类的实例参数保存。注意,我们正在去掉截距,并从1开始保留参数。这些是我们将在内积中计算的非截距参数,用于预测:

# recall that our theta includes the intercept, so we need to  pop
# that off and store it
self.intercept = theta[0]
self.theta = theta[1:]
self.log_likelihood = ll
self.column_means = means
self.column_std = stds

所以,我们对X乘以theta.T进行逻辑变换,然后在居中和标准化输入X后加上intercept,这将给我们带来概率:

# scale the data appropriately
X = (X - self.column_means) / self.column_std

# creates a copy
return logistic_sigmoid(np.dot(X, theta.T) + self.intercept)

但为了获得实际的预测,我们只需将概率四舍五入。所以,在predict函数中,我们将使用predict_proba并将其四舍五入为 0 或 1,返回类型为int,这将给我们类别 0 和 1:

 def predict(self, X):
     return np.round(self.predict_proba(X)).astype(int)

逻辑回归示例

现在,作为一个例子,我们将查看我们的example_logistic_regression.py脚本。我们将比较我们simple_logistic_regression.py文件的输出与 scikit-learn 的输出,并证明我们得到的参数是相似的,如果不是完全相等的话。我们使用 scikit-learn 的make_classification函数创建100个样本和两个特征,并进行train_test_split。首先,我们将用我们刚刚讲解过的模型拟合我们自己的SimpleLogisticRegression并进行50步训练,这是一个50次迭代,如以下代码所示:

# -*- coding: utf-8 -*-

from __future__ import absolute_import

from packtml.regression import SimpleLogisticRegression
from packtml.utils.plotting import add_decision_boundary_to_axis
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt
import sys

# #############################################################################
# Create an almost perfectly linearly-separable classification set
X, y = make_classification(n_samples=100, n_features=2, random_state=42,
                           n_redundant=0, n_repeated=0, n_classes=2,
                           class_sep=1.0)

# split data
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# #############################################################################
# Fit a simple logistic regression, produce predictions
lm = SimpleLogisticRegression(X_train, y_train, n_steps=50)

predictions = lm.predict(X_test)
acc = accuracy_score(y_test, predictions)
print("Test accuracy: %.3f" % acc)

接下来,我们将计算 scikit-learn 的LogisticRegression,几乎没有正则化,并按照如下方式进行拟合:

# Show that our solution is similar to scikit-learn's
lr = LogisticRegression(fit_intercept=True, C=1e16) # almost no regularization
lr.fit(X_train, y_train)
print("Sklearn test accuracy: %.3f" % accuracy_score(y_test, 
                                                   lr.predict(X_test)))

我们将运行这段代码。确保你已经通过输入source activate packt-sml激活了你的 Anaconda 环境。

如果你使用的是 Windows 系统,这只需要输入activate packt-sml

我们看到我们的测试准确率为 96%,这与Sklearn的 100%非常接近。Scikit-learn 进行更多的迭代,因此它获得了更好的准确率。如果我们进行更多的迭代,理论上我们可以得到完美的准确率。在以下输出中,你可以看到一个完美的线性可分界限。但由于我们没有进行那么多迭代,我们还没有达到这个结果。所以,在这个图中你可以看到我们有一个线性边界,这就是我们学习到的决策函数,分隔了这两个类别。左边是一个类别,右边是另一个类别,如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/b26fd4cd-0ca3-4d1f-bdb5-732d5062ffbc.png

上述代码的输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/e3eddc15-ee9b-4acb-ab29-e3407fb81f18.png

假设我们对这段代码进行一百次甚至更多次的迭代,我们可以得到一个完美的线性可分平面,从而保证一个线性可分的类别,因为逻辑回归总是能够达到全局最优。我们也知道,我们的公式与 scikit-learn 的完全相同。因此,这只取决于我们在这里进行了多少次迭代。

在下一节中,我们将探讨参数模型的优缺点。

参数模型的优缺点

参数模型具有一些非常方便的特点。即,它们拟合速度快、不需要太多数据,并且可以很容易地解释。在线性和逻辑回归的情况下,我们可以轻松查看系数并直接解释单个变量变化的影响。在金融或保险等受监管行业中,参数模型往往占据主导地位,因为它们可以很容易地向监管机构解释。业务合作伙伴通常非常依赖系数所提供的见解。然而,正如我们迄今所看到的那样,它们往往过于简化。因此,作为一个例子,我们在上一节中看到的逻辑回归决策边界假设了两个类别之间有一个完美的线性边界。

现实世界很少能够被限制为线性关系。话虽如此,模型仍然非常简单。它们并不总能捕捉到变量之间关系的真正细微差别,这有点像双刃剑。此外,它们受异常值和数据规模的影响很大。所以,你必须非常小心地处理数据。这也是我们在拟合之前必须确保对数据进行中心化和标准化的原因之一。最后,如果你向模型中添加数据,模型的表现也不太可能会大幅提升。这引入了一个新的概念,我们称之为偏差。

偏差导致的误差是我们将在后续章节中讨论的一个概念。这是一个模型过于简化的结果。在下面的图示中,我们的模型通过将 logit 函数视为线性,过度简化了该函数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/c3f57086-32f9-43e4-9c09-66a912959d67.png

这也被称为欠拟合,这是参数模型家族中常见的问题。应对高偏差有几种方法,我们将在下一章介绍其中的大部分。但在探讨参数模型的缺点时,在这里指出其中一些是值得的。如前所述,在高偏差的情况下,我们无法通过增加更多数据来学习更好的函数。如果我们以之前的例子为例,假设你沿着 logit 线增加更多样本,我们学习到的或者蓝色的那条线不会比已经得到的更接近真实函数,因为它是线性的。它不够复杂,无法建模真实的基础函数,这是许多参数模型简化性的一个不幸后果。更多的模型复杂性和复杂的非线性特征通常有助于纠正高偏差。

总结

在这一章中,我们介绍了参数化模型。接着,我们详细讲解了线性逻辑回归的基础数学内容,然后转向了 Python 中的实现。现在我们已经讨论了一些参数化模型的优缺点,在下一章中,我们将看看一些非参数化模型。

第三章:使用非参数模型

在上一章中,我们介绍了参数模型并探讨了如何实现线性回归和逻辑回归。在本章中,我们将讨论非参数模型系列。我们将从讨论偏差-方差权衡开始,并解释参数模型和非参数模型在基本层面上的不同。稍后,我们将深入探讨决策树和聚类方法。最后,我们将讨论非参数模型的一些优缺点。

在本章中,我们将涵盖以下主题:

  • 偏差/方差权衡

  • 非参数模型和决策树简介

  • 决策树

  • 从零开始实现决策树

  • 各种聚类方法

  • 从零开始实现K-最近邻KNN

  • 非参数模型——优缺点

技术要求

本章中,如果你尚未安装以下软件,你需要先安装:

  • Jupyter Notebook

  • Anaconda

  • Python

本章的代码文件可以在 https:/​/​github.​com/​PacktPublishing/找到。

监督式机器学习与 Python

偏差/方差权衡

在本节中,我们将继续讨论由偏差引起的误差,并介绍一种新的误差来源——方差。我们将首先澄清误差项的含义,然后剖析建模误差的各种来源。

误差项

模型构建的核心主题之一是减少误差。然而,误差有多种类型,其中有两种是我们在某种程度上可以控制的。这两种被称为偏差方差。模型在减少偏差或方差的能力上存在权衡,这被称为偏差-方差权衡偏差-方差困境

一些模型在一定程度上能很好地控制这两者。然而,这个困境在大多数情况下总会出现在你的建模过程中。

由偏差引起的误差

高偏差也可以称为欠拟合或过度泛化。高偏差通常会导致模型缺乏灵活性,错过我们正在建模的目标函数中特征与特征之间的真实关系。在下图中,xy之间的真实关系被过于简化,错过了真实的* f(x) *函数,它本质上是一个逻辑函数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/52bed5c5-28a1-4bc7-8087-1ed23193d643.png

参数模型往往比非参数模型更容易受到高偏差问题的影响。线性回归和逻辑回归就是这类问题的例子,我们将在本章的最后一部分更详细地探讨这些内容。

由方差引起的误差

相比之下,对于你现在已经熟悉的高偏差,方差引起的误差可以看作是模型对于给定样本预测的变化性。想象你多次重复建模过程;方差就是不同模型推导下,对于给定样本的预测结果的波动。高方差模型通常被称为过拟合,且正好与高偏差相反。也就是说,它们的泛化能力不足。高方差通常源于模型对信号的敏感性不足,而对噪声的过度敏感。一般而言,随着模型复杂度的增加,方差成为我们主要关注的问题。请注意,在图表中,多项式项导致了一个过度拟合的模型,而一个简单的logit函数就足够了:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/caf73d07-a9f2-4b3d-9759-7957874afafd.png

与高偏差问题不同,高方差问题可以通过更多的训练数据来解决,这有助于模型更好地学习和泛化。所以,尚未覆盖的高方差模型的例子有决策树和 KNN。我们将在本章中介绍这两者。

学习曲线

在本节中,我们将研究一种方便的方式来诊断高偏差或高方差,称为学习曲线。在这个 Python 示例代码中,我们将利用packtml.utils子模块中的plot_learning_curve函数,如下所示:

from sklearn.datasets import load_boston
from sklearn.metrics import mean_squared_error
from packtml.utils.plotting import plot_learning_curve
from packtml.regression import SimpleLinearRegression
%matplotlib inline

boston = load_boston()
plot_learning_curve(
        model=SimpleLinearRegression, X=boston.data, y=boston.target,
        metric=mean_squared_error, n_folds=3,
        train_sizes=(50, 150, 250, 300),
        seed=42, y_lim=(0, 45))\
    .show

这个函数将接受一个估计器,并将其在train_sizes参数定义的不同训练数据集大小上进行拟合。显示的是每次增量模型拟合后的训练集和对应验证集的模型性能。因此,这个示例使用我们的线性回归类来建模波士顿房价数据,这是一个回归问题,且显示出高偏差的症状。注意到我们的误差在训练集和验证集之间非常相似。它迅速达到了某个值,但仍然相对较高。随着训练集的增长,它们并没有得到改善。我们得到的输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/5bb09654-b826-464c-981a-9d322d1a7c55.png

或者,如果我们使用决策树回归器来建模相同的数据,我们会注意到高方差或过拟合的症状:

from sklearn.datasets import load_boston
from sklearn.metrics import mean_squared_error
from packtml.utils.plotting import plot_learning_curve
from packtml.decision_tree import CARTRegressor
%matplotlib inline

boston = load_boston()
plot_learning_curve(
        model=CARTRegressor, X=boston.data, y=boston.target,
        metric=mean_squared_error, n_folds=3,
        train_sizes=(25, 150, 225, 350),
        seed=42, random_state=21, max_depth=50)\
    .show

训练得分验证得分之间存在巨大的差异,尽管随着数据量的增加,得分有所改善,但始终没有完全收敛。我们得到如下输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/e0e0dc71-135b-4802-8a4a-996d3e6db219.png

处理高偏差的策略

如果你确定自己正在遭遇高偏差问题,你可以通过构造更多富有信息的信号特征来使模型变得更复杂。例如,在这里,你可以尝试创建新的特征,这些特征是 x1 的多项式组合,这样,你就可以创建 x1 的对数几率函数,从而完美地建模我们的函数。你还可以尝试调整一些超参数,例如 KNN,尽管它是一个高方差模型,但随着 k 超参数的增加,它可能会迅速变得高度偏差,反之亦然:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/8d8f5203-9b9c-4b73-8e96-2f0c952b665a.png

处理高方差的策略

如果你面临的是高方差问题,我们已经看到更多的训练数据在一定程度上能有所帮助。你还可以进行一些特征选择,以减少模型的复杂性。最稳健的解决方案是袋装法或集成方法,它将多个小模型的输出结合起来,所有这些模型都会对每个样本的标签或回归分数进行投票:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/477ce929-ccc1-4fd0-8f69-882886f9d56f.png

在下一节中,我们将更加正式地定义非参数学习算法,并介绍决策树。

非参数模型和决策树简介

在本节中,我们将正式定义什么是非参数学习算法,并介绍我们第一个算法——决策树背后的某些概念和数学原理。

非参数学习

非参数模型不学习参数。它们确实学习数据的特征或属性,但不是在正式意义上学习参数。我们最终不会提取一个系数向量。最简单的例子是决策树。决策树会学习如何递归地划分数据,以便它的叶子尽可能纯净。因此,从这个意义上来说,决策函数是每个叶子的划分点,而不是参数。

非参数学习算法的特点

非参数模型通常更加灵活,并且不会对数据的底层结构做出过多假设。例如,许多线性模型或参数化模型假设每个特征都需要满足正态分布,并且它们相互独立。然而,大多数非参数模型并不这样假设。正如我们在上一节中讨论的,偏差-方差权衡也表明,非参数模型需要更多的数据来训练,以避免高方差问题的困扰。

一个模型是参数化的还是非参数化的?

如果你在想,是否一个模型是参数化的,这可能不是最重要的问题。你应该选择最适合你数据的建模技术。不过,一个好的经验法则是,模型学习了多少特征或参数。如果它与特征空间或维度有关,那么它很可能是参数化的,例如,学习线性回归中的系数θ。如果它与样本数有关,那么它很可能是非参数化的,例如,决策树的深度或聚类中的邻居数。

一个直观的例子——决策树

决策树将从所有数据开始,迭代地进行分裂,直到每个叶子节点的纯度最大化或满足其他停止标准。在这个例子中,我们从三个样本开始。树学习到,在颜色特征上进行分裂将是我们最有信息量的一步,有助于最大化叶子节点的纯度。所以,第一个需要注意的点是,第一次分裂是最有信息量的分裂,它能最好地将数据分成两部分。如下面的图所示,土豆类别通过颜色分裂被隔离在左边。我们已经完美地分类了土豆。然而,另外两个样本仍然需要进一步分裂。于是,决策树学到,如果是橙色且圆形的,它就是一个甘薯;否则,如果它只是橙色且不圆,它就是胡萝卜,然后再向左分裂一次。在这里,我们可以看到所有类别的完美分裂:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/f35a17a3-c73c-4c27-8952-478a124f2999.png

决策树——简介

我们对决策树感兴趣的,是定义一个灵活且可扩展的算法,能够实现决策树。这里,分类与回归树CART)算法发挥了作用。CART 算法可以广泛应用于这两类任务,基本上是通过向数据提问来学习。在每一个分裂点,CART 会扫描整个特征空间,从每个特征中抽取值,以确定最佳的特征和分裂值。它通过评估信息增益公式来实现这一点,该公式旨在最大化分裂中纯度的增益,这一点是相当直观的。基尼不纯度在叶子节点级别进行计算,它是衡量叶子节点纯度或不纯度的一种方式;其公式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/d6268b54-06ba-49be-8da4-7dc894aee979.png

底部的IG是我们的信息增益,它是根节点的基尼指数,如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/fce97018-e381-4527-9384-4960853433e3.png

决策树是如何做出决策的?

我们将首先处理目标,然后再看数学部分。我们将计算一个拆分的信息增益,以确定最佳拆分点。如果信息增益为正,这意味着我们从该拆分中学到了东西,这可能是最佳点。如果信息增益为负,这意味着我们实际上走错了方向。我们所做的就是创建了一个没有信息的拆分。树中的每个拆分将选择最大化信息增益的点。

那么,下面是设置:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/a2152363-2f8b-4a2f-8327-3bf389e2ece8.png

基尼不纯度为 0 时表示特别纯净。较高的不纯度本质上意味着在该叶子节点中找到了更多随机的类别。所以,我们的根节点相当不纯。现在我们的树将扫描整个特征空间,从每个特征中采样值。它将评估如果在这里进行拆分,信息增益会如何。假设我们的树选择了x12,我们将沿着样本值拆分该变量。我们想知道的是,如果通过这个拆分我们得到更纯净的叶子节点,我们将计算信息增益。为此,我们必须计算刚刚创建的每个叶子节点的基尼不纯度。

我们将通过使用packtml库来查看这个问题的示例。我们有example_information_gain.py文件,它位于examples/decision_tree目录下:

# -*- coding: utf-8 -*-

from __future__ import absolute_import

from packtml.decision_tree.metrics import gini_impurity, InformationGain
import numpy as np

# #############################################################################
# Build the example from the slides
y = np.array([0, 0, 0, 1, 1, 1, 1])
uncertainty = gini_impurity(y)
print("Initial gini impurity: %.4f" % uncertainty)

# now get the information gain of the split from the slides
directions = np.array(["right", "left", "left", "left",
                       "right", "right", "right"])
mask = directions == "left"
print("Information gain from the split we created: %.4f"
      % InformationGain("gini")(target=y, mask=mask, uncertainty=uncertainty))

接下来,我们将使用packtml.decision_tree.metrics中的InformationGain类来计算信息增益:

from packtml.decision_tree.metrics import gini_impurity, InformationGain
import numpy as np

当我们运行example_information_gain.py时,将得到以下输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/064c2ef2-3f84-4a24-8500-8b100b2fd139.png

在接下来的部分,我们将更深入地了解并学习决策树是如何生成候选拆分供我们评估的。

决策树

在前一部分中,我们计算了给定拆分的信息增益。回想一下,它是通过计算每个LeafNode中的父节点的基尼不纯度来计算的。较高的信息增益更好,这意味着我们通过拆分成功地减少了子节点的杂质。然而,我们需要知道如何生成候选拆分,以便进行评估。

对于每个拆分,从根节点开始,算法将扫描数据中的所有特征,为每个特征选择一个随机值。选择这些值的策略有很多种。对于一般的使用案例,我们将描述并选择一个k随机方法:

  • 对于每个特征中的每个样本值,我们模拟一个候选拆分。

  • 高于采样值的值会朝一个方向,比如左侧,超过该值的会朝另一个方向,即右侧。

  • 现在,对于每个候选拆分,我们将计算信息增益,并选择产生最高信息增益的特征值组合,即最佳拆分。

  • 从最佳拆分开始,我们将递归向下遍历每个拆分,直到满足停止标准。

现在,关于在哪里以及何时停止划分标准,我们可以使用多种方法来实现。一个常见的方法是最大树深度。如果树深度过大,我们会开始过拟合。例如,当树生长到五层深时,我们可能会进行剪枝。另一个方法是每个叶子的最小样本数。如果我们有 100 万个训练样本,我们会继续生长树,直到每个叶子只有一个样本;这也可能会过拟合。因此,最小样本数叶子参数可以让我们在划分后,如果每个叶子剩余的样本数,比如说 50 个,便停止划分。这是一个可以调节的超参数,你可以在交叉验证过程中进行调整。

手动划分决策树

现在我们来进行一个练习。假设我们有如下的训练集:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/cbadade4-c934-4312-b1d4-61859101f0cb.png

从前面的数据来看,最佳的划分点在哪里?我们应该使用什么特征或值的组合来定义我们的规则?

如果我们在 x1 上划分

首先,我们将计算根节点的基尼不纯度,这就是未划分的状态。我们得到0.444,如图所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/6ace640a-6b82-49f9-938a-94cd878bf06f.png

算法的下一步是遍历每个特征。下面是三种情况。使用我们的IG公式,我们可以计算出哪个是这个特征的最佳划分点。第一种情况恰好是最佳的:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/cbf032eb-f9f3-498e-9bbe-a0a42ca5c3ea.png

在第二种情况下,x1大于等于4时进行划分并不是一个好主意,因为结果与根节点的状态没有不同。因此,我们的信息增益是0

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/72157416-b405-492b-a5ae-d6668cac95a4.png

在最后一种情况下,当x1大于等于37时,的确产生了正的信息增益,因为我们成功地将一个正类样本与其他样本分开:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/cadc0ea0-fc63-4d29-9e00-94f92d172e50.png

如果我们在 x2 上划分

然而,我们还不确定是否已经完成。因此,我们将迭代到x2,看看是否有更好的划分点:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/5f99b20b-6803-4822-aee8-4094b8f563c3.png

候选划分点显示,当我们与当前在x1中识别到的最佳划分点进行比较时,两个潜在的划分都不是最佳划分。

因此,最佳的划分是x1大于等于21,这将完美地分离我们的类别标签。你可以在这个决策树中看到,当我们进行这个划分时,确实能够得到完全分离的类别:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/2aee3630-4c83-49b8-9d60-fc674c593e8b.png

然而,在更大的例子中,我们可能无法完全分离我们的类别,如果我们有数百万个样本的话。例如,在这种情况下,我们会在此时递归,找到每个节点的新划分点,直到满足停止标准。此时,我们可以使用我们的packtml库来运行这个例子,并展示我们确实能够识别出相同的最佳划分点,从而证明这不是手段上的巧妙操作。

在 PyCharm 中,example_classification_split.py 文件已经打开。这个文件位于你的 examples 目录下,并且在 decision_tree 示例目录内。你可以看到我们将从 packtml 中导入两个东西,它们恰好都在 decision_tree 子模块内,你会找到 RandomSplitter

from __future__ import absolute_import

from packtml.decision_tree.cart import RandomSplitter
from packtml.decision_tree.metrics import InformationGain
import numpy as np

我们在上一节中已经稍微看过 InformationGain,用来计算我们的信息增益候选拆分。这里,我们将看到我们如何实际创建候选拆分。我们得到如下数据和相应的类别标签:

# Build the example from the slides (3.3)
X = np.array([[21, 3], [ 4, 2], [37, 2]])
y = np.array([1, 0, 1])

RandomSplitter 会评估前面提到的每个值,因为 n_val_sample3。所以,它将为每个特征计算三个候选拆分点,我们将找出其中哪个是最好的:

# this is the splitting class; we'll use gini as the criteria
random_state = np.random.RandomState(42)
splitter = RandomSplitter(random_state=random_state,
                          criterion=InformationGain('gini'),
                          n_val_sample=3)
# find the best:
best_feature, best_value, best_gain = splitter.find_best(X, y)
print("Best feature=%i, best value=%r, information gain: %.3f"
      % (best_feature, best_value, best_gain))

当我们运行前面的代码时,我们看到 best_feature0best_value21,这意味着在特征 0 中,任何大于或等于 21 的值将走左侧,而其他值则走右侧。我们得到的 InformationGain0.444,这确实是我们手动计算时得到的结果,正是我们预期的:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/5b3a8a2e-2d07-4764-9e59-4ebfdc49d163.png

在下一节中,我们将讨论如何在 packtml 库中从零开始实现决策树。

从头开始实现决策树

我们将从查看拆分度量的实现开始。然后我们会覆盖一些拆分逻辑,最后,我们将看到如何包装树,以便能够从分类和回归任务中进行泛化。

分类树

让我们继续看一个分类树的例子。我们将使用信息增益准则。在 PyCharm 中打开了三个脚本,其中两个是 metrics.pycart.py,它们都位于 packtml/decision_tree 子模块内。然后我们还有一个 example_classification_decision_tree.py 文件,它在 examples/decision_tree 目录下。让我们从度量开始。

如果你打开 cart.py 文件,我们有一个顺序步骤,以帮助你理解决策树类如何工作:

# 1\. metrics.InformationGain & metrics.VarianceReduction
# 2\. RandomSplitter
# 3\. LeafNode
# 4\. BaseCART

metrics.py 文件的顶部开始,你可以看到 _all_ 将包括四个不同的度量:

__all__ = [
    'entropy',
    'gini_impurity',
    'InformationGain',
    'VarianceReduction'
]

entropygini_impurity 都是分类度量。我们已经讨论过 gini_impurity。你可以看到这里它们都在调用 clf_metric 私有函数,如下所示:

def entropy(y):
    """Compute the entropy of class labels.

    This computes the entropy of training samples. A high entropy means
    a relatively uniform distribution, while low entropy indicates a
    varying distribution (many peaks and valleys).

    References
    ----------
    .. [1] http://www.cs.csi.cuny.edu/~imberman/ai/Entropy%20and%20Information%20Gain.htm
    """
    return _clf_metric(y, 'entropy')

def gini_impurity(y):
    """Compute the Gini index on a target variable.

    The Gini index gives an idea of how mixed two classes are within a leaf
    node. A perfect class separation will result in a Gini impurity of 0 (that is,
    "perfectly pure").
    """
    return _clf_metric(y, 'gini')

现在,ginientropy 基本上是以相同的方式起作用,除了最后,gini 基本上是计算其自身的范数,而 entropy 是计算 log2

def _clf_metric(y, metric):
    """Internal helper. Since this is internal, so no validation performed"""
    # get unique classes in y
    y = np.asarray(y)
    C, cts = np.unique(y, return_counts=True)

    # a base case is that there is only one class label
    if C.shape[0] == 1:
        return 0.

    pr_C = cts.astype(float) / y.shape[0] # P(Ci)

    # 1 - sum(P(Ci)²)
    if metric == 'gini':
        return 1\. - pr_C.dot(pr_C) # np.sum(pr_C ** 2)
    elif metric == 'entropy':
        return np.sum(-pr_C * np.log2(pr_C))

    # shouldn't ever get to this point since it is internal
    else:
        raise ValueError("metric should be one of ('gini', 'entropy'), "
                         "but encountered %s" % metric)

这里需要注意的一点是,熵和基尼指数将对决策树的表现产生巨大的影响。基尼指数实际上是 CART 算法的标准,但我们在这里加入了熵,以便你能看到这是你在需要时可以使用的内容。

BaseCriterion是我们的分裂标准的基类。我们有两个分裂标准,InformationGainVarianceReduction。它们都会实现compute_uncertainty

class BaseCriterion(object):
    """Splitting criterion.

    Base class for InformationGain and VarianceReduction. WARNING - do
    not invoke this class directly. Use derived classes only! This is a
    loosely-defined abstract class used to prescribe a common interface
    for sub-classes.
    """
    def compute_uncertainty(self, y):
        """Compute the uncertainty for a vector.

        A subclass should override this function to compute the uncertainty
        (that is, entropy or gini) of a vector.
        """

class InformationGain(BaseCriterion):
    """Compute the information gain after a split.

    The information gain metric is used by CART trees in a classification
    context. It measures the difference in the gini or entropy before and
    after a split to determine whether the split "taught" us anything.

如果你还记得上一节,uncertainty本质上是由分裂引起的不纯度或熵。当我们使用ginientropy计算InformationGain时,我们的uncertainty将是分裂前的metric

def __init__(self, metric):
        # let fail out with a KeyError if an improper metric
        self.crit = {'gini': gini_impurity,
                     'entropy': entropy}[metric]

如果我们计算uncertainty,我们会传入一个节点,并在分割之前,计算节点内所有样本的基尼系数(Gini),然后当我们实际调用计算InformationGain时,我们传入mask,用来表示某个样本是去left还是去right。我们将在左右两侧分别计算基尼系数,并返回InformationGain

def __call__(self, target, mask, uncertainty):
        """Compute the information gain of a split.

        Parameters
        ----------
        target : np.ndarray
            The target feature

        mask : np.ndarray
            The value mask

        uncertainty : float
            The gini or entropy of rows pre-split
        """
        left, right = target[mask], target[~mask]
        p = float(left.shape[0]) / float(target.shape[0])

        crit = self.crit # type: callable
        return uncertainty - p * crit(left) - (1 - p) * crit(right)

这就是我们计算InformationGain的方法,而这只是我们构建的包装类。VarianceReduction非常相似,不同的是compute_uncertainty函数将简单地返回y的方差。当我们调用时,我们将前分裂节点的不确定性减去左右分裂后的不确定性之和。我们在这里所做的就是最大化每次分裂之间方差的减少。这样,我们就可以知道一个分裂是否是好的。它沿着相对直观的线进行分割,如下所示:

class VarianceReduction(BaseCriterion):
    """Compute the variance reduction after a split.

    Variance reduction is a splitting criterion used by CART trees in the
    context of regression. It examines the variance in a target before and
    after a split to determine whether we've reduced the variability in the
    target.
    """
    def compute_uncertainty(self, y):
        """Compute the variance of a target."""
        return np.var(y)

    def __call__(self, target, mask, uncertainty):
        left, right = target[mask], target[~mask]
        return uncertainty - (self.compute_uncertainty(left) +
                              self.compute_uncertainty(right))

这是我们的两个分裂标准:InformationGainVarianceReduction。我们将使用InformationGain进行分类,使用VarianceReduction进行回归。既然我们现在讨论的是分类,让我们集中讨论InformationGain。接下来在cart.py文件中,我们看到下一个要讨论的内容是RandomSplitter

在之前的一个部分中,我们了解了一种生成候选分裂的策略。这本质上就是RandomSplitter。这里有很多不同的策略可以使用。我们将使用一些熵(entropy),这样我们可以相对快速地完成这个类和算法,而无需深入细节。

RandomSplitter将接受几个参数。我们需要random_state,以便以后可以重复此分裂。标准(criterion)是InformationGainVarianceReduction的实例,以及我们希望从每个特征中采样的值的数量:

 def __init__(self, random_state, criterion, n_val_sample=25):
        self.random_state = random_state
        self.criterion = criterion # BaseCriterion from metrics
        self.n_val_sample = n_val_sample

所以,我们的find_best函数将扫描整个特征空间,按每个分裂或每个特征采样值的数量,并确定best_valuebest_feature作为分裂的依据。这将产生我们在当时的最佳分裂。所以,best_gain将从0开始。如果是负值,则表示不好,我们不想进行分裂。如果是正值,则意味着它比当前的最佳分裂更好,我们就采用它并继续寻找最佳分裂。我们要找到我们的best_featurebest_value

def find_best(self, X, y):
        criterion = self.criterion
        rs = self.random_state

        # keep track of the best info gain
        best_gain = 0.

        # keep track of best feature and best value on which to split
        best_feature = None
        best_value = None

        # get the current state of the uncertainty (gini or entropy)
        uncertainty = criterion.compute_uncertainty(y)

现在,对于我们数据集的每一列,我们将提取出特征。这只是一个 NumPy 数组,一个一维的 NumPy 数组:

# iterate over each feature
for col in xrange(X.shape[1]):
    feature = X[:, col]

我们将创建一个集合,以便跟踪我们已经看到的值,以防我们不断重复抽样相同的值。我们会对该特征进行排列,以便可以将其打乱并扫描特征中的每个值。你会注意到的一点是,我们本可以仅收集特征的唯一值。但首先,获取唯一值的成本相对较高。其次,这样会丢失关于该特征的所有分布信息。通过这种方式,我们实际上会有更多某个值,或者有更多值更加紧密地聚集在一起。这将使我们能够得到该特征更真实的样本:

 # For each of n_val_sample iterations, select a random value
 # from the feature and create a split. We store whether we've seen
 # the value before; if we have, continue. Continue until we've seen
 # n_vals unique values. This allows us to more likely select values
 # that are high frequency (retains distributional data implicitly)
 for v in rs.permutation(feature):

如果我们集合中的seen_values的数量等于我们想要抽样的值的数量,那么我们将退出循环。所以,如果我们说有100个唯一值,但我们已经看到了25个,那么我们将退出。否则,如果我们在该集合中已经看到了这个值,我们将继续。我们不想对已经计算过的值再做重复计算。因此,在这里,我们会将该值添加到集合中,并为是否划分为左或右创建我们的掩码:

# if we've hit the limit of the number of values we wanted to
# examine, break out
if len(seen_values) == n_vals:
   break
# if we've already tried this value, continue
elif v in seen_values: # O(1) lookup
     continue
# otherwise, it's a new value we've never tried splitting on.
# add it to the set.
seen_values.add(v)

# create the mask (these values "go left")
mask = feature >= v # type: np.ndarray

现在,还有一个特殊情况。如果我们已经抓取了最小值,那么我们的掩码将把所有东西都划分到一个方向,这正是我们正在检查的内容。我们不希望这样,因为否则我们就没有创建一个真实的划分。所以,如果是这种情况,我们就continue,重新进行抽样:

# skip this step if this doesn't divide the dataset
if np.unique(mask).shape[0] == 1: # all True or all False
    continue

现在让我们计算增益,计算InformationGainVarianceReduction,它会计算左侧和右侧的基尼指数,并从原始不确定性中减去它。如果gain很好,也就是说,如果它比我们看到的当前最佳值更好,那么我们就有了一个新的best_feature和新的best_value,并且我们会存储它。所以,我们会循环遍历这个过程,检查每个特征内随机抽样的值,并确定要划分的best_feature和在该特征中要划分的best_value。如果我们没有找到,这意味着我们没有找到一个有效的划分,这种情况发生在少数几种情况下:

# compute how good this split was
gain = criterion(y, mask, uncertainty=uncertainty)

# if the gain is better, we keep this feature & value &
# update the best gain we've seen so far
if gain > best_gain:
    best_feature = col
    best_value = v
    best_gain = gain

# if best feature is None, it means we never found a viable split...
# this is likely because all of our labels were perfect. In this case,
# we could select any feature and the first value and define that as
# our left split and nothing will go right.
if best_feature is None:
    best_feature = 0
    best_value = np.squeeze(X[:, best_feature])[0]
    best_gain = 0.

# we need to know the best feature, the best value, and the best gain
return best_feature, best_value, best_gain

接下来,我们将查看LeafNode。如果你之前构建过二叉树,那么你应该熟悉LeafNode的概念。LeafNode将存储一个左指针和一个右指针,这两个指针通常都初始化为 null,表示那里没有内容。因此,在这种情况下,叶节点将是我们决策树的核心部分。它提供了一个骨架,而树本身仅仅是一个外壳:

class LeafNode(object):
    """A tree node class.

    Tree node that store the column on which to split and the value above
    which to go left vs. right. Additionally, it stores the target statistic
    related to this node. For instance, in a classification scenario:

        >>> X = np.array([[ 1, 1.5 ],
        ...               [ 2, 0.5 ],
        ...               [ 3, 0.75]])
        >>> y = np.array([0, 1, 1])
        >>> node = LeafNode(split_col=0, split_val=2,
        ...                 class_statistic=_most_common(y))

LeafNode将存储split_col,即我们正在分割的特征,split_valsplit_gain,以及class_statistic。因此,分类问题中的class_statistic将是节点,在这里我们投票选出最常见的值;而在回归问题中,它将是均值。如果你想要做得更复杂,可以使用中位数或其他回归策略。不过,我们在这里保持简单,使用均值即可。所以,构造函数将存储这些值,并再次将左节点和右节点初始化为 null:

   def __init__(self, split_col, split_val, split_gain, class_statistic):

        self.split_col = split_col
        self.split_val = split_val
        self.split_gain = split_gain

        # the class statistic is the mode or the mean of the targets for
        # this split
        self.class_statistic = class_statistic

        # if these remain None, it's a terminal node
        self.left = None
        self.right = None

    def create_split(self, X, y):
        """Split the next X, y.

现在在create_split函数中,我们实际上开始处理树的结构。但这实际上是将节点进行分割,并创建新的左节点和右节点。因此,它从终端节点开始,向下进行下一个分割,我们可以对其进行递归操作。我们将从Xy的分割中获取当前数据集的当前集合。由于我们已经初始化的特征值将创建用于左侧和右侧的掩码,如果我们是全左或全右,它就会存储在这里。否则,它将进行分割,将左侧的行和右侧的行区分开来,或者如果没有行,它就不进行分割。如果左侧/右侧没有分割,我们只使用none,然后返回X_leftX_righty_lefty_right

# If values in the split column are greater than or equal to the
# split value, we go left.
left_mask = X[:, self.split_col] >= self.split_val

# Otherwise we go to the right
right_mask = ~left_mask # type: np.ndarray

# If the left mask is all False or all True, it means we've achieved
# a perfect split.
all_left = left_mask.all()
all_right = right_mask.all()

# create the left split. If it's all right side, we'll return None
X_left = X[left_mask, :] if not all_right else None
y_left = y[left_mask] if not all_right else None

# create the right split. If it's all left side, we'll return None.
X_right = X[right_mask, :] if not all_left else None
y_right = y[right_mask] if not all_left else None

return X_left, X_right, y_left, y_right

终端节点只是左节点和右节点的快捷方式。如果我们有任一节点,那么它就不是终端节点。但如果左右节点都为 null,那它就是一个终端节点:

def is_terminal(self):
     """Determine whether the node is terminal.

     If there is no left node and no right node, it's a terminal node.
     If either is non-None, it is a parent to something.
     """
     return self.left is None and self.right is None

我们将使用predict_record函数在LeafNode内部进行预测。这个函数将使用我们已有的class_statistic函数。class_statistic对于分类问题是众数,对于回归问题是均值。为了预测一个记录是向左还是向右,我们将递归操作,而这正是predict函数所做的,我们稍后会讲解,并查看如何生成预测:

   def predict_record(self, record):
        """Find the terminal node in the tree and return the class statistic"""
        # First base case, this is a terminal node:
        has_left = self.left is not None
        has_right = self.right is not None
        if not has_left and not has_right:
            return self.class_statistic

        # Otherwise, determine whether the record goes right or left
        go_left = record[self.split_col] >= self.split_val

        # if we go left and there is a left node, delegate the recursion to the
        # left side
        if go_left and has_left:
            return self.left.predict_record(record)

        # if we go right, delegate to the right
        if not go_left and has_right:
            return self.right.predict_record(record)

        # if we get here, it means one of two things:
        # 1\. we were supposed to go left and didn't have a left
        # 2\. we were supposed to go right and didn't have a right
        # for both of these, we return THIS class statistic
        return self.class_statistic

现在,树本身有两个类别。我们有CARTRegressorCARTClassifier。这两者都将封装BaseCART类,我们现在就来讲解这个类。BaseCART,和我们之前讲解过的大多数基本估计器一样,必定会接受两个参数,分别是Xy——我们的训练数据和训练标签。它还会接受我们的标准(criterion),我们会在后面传入它。标准可以是用于分类的InformationGain,用于回归的VarianceReductionmin_samples_split,以及我们已经讨论过的其他超参数。我们首先要做的,和往常一样,是检查我们的Xy,确保它们都是连续值,并且没有缺失数据。这只是为超参数分配self属性,我们将创建一个splitter,它是RandomSplitter,我们将在这个过程中使用它。这就是我们生长树的方式,所有操作都发生在find_next_split中。这个函数将接受三个参数:Xy,和样本计数:

class _BaseCART(BaseSimpleEstimator):
    def __init__(self, X, y, criterion, min_samples_split, max_depth,
                 n_val_sample, random_state):
        # make sure max_depth > 1
        if max_depth < 2:
            raise ValueError("max depth must be > 1")

        # check the input arrays, and if it's classification validate the
        # target values in y
        X, y = check_X_y(X, y, accept_sparse=False, dtype=None, copy=True)
        if is_classifier(self):
            check_classification_targets(y)

        # hyper parameters so we can later inspect attributes of the model
        self.min_samples_split = min_samples_split
        self.max_depth = max_depth
        self.n_val_sample = n_val_sample
        self.random_state = random_state

        # create the splitting class
        random_state = check_random_state(random_state)
        self.splitter = RandomSplitter(random_state, criterion, n_val_sample)

        # grow the tree depth first
        self.tree = self._find_next_split(X, y, 0)

本质上,我们将递归地调用find_next_split函数,直到树完全生长或修剪完毕。由于我们在递归,我们始终首先设置我们的基本情况。如果current_depth等于我们希望树生长的最大深度,或者X中样本的大小小于或等于min_samples_split,这两个条件都是我们的终止标准,这时我们将返回None

    def _find_next_split(self, X, y, current_depth):
        # base case 1: current depth is the limit, the parent node should
        # be a terminal node (child = None)
        # base case 2: n samples in X <= min_samples_split
        if current_depth == self.max_depth or \
                X.shape[0] <= self.min_samples_split:
            return None

否则,我们将获取我们的splitter,并在Xy之间找到最佳分裂,这将给我们带来best_featurebest_value和增益gain,增益可以是VarianceReductionInformationGain。接下来,我们刚刚找到了第一个分裂。那么,现在我们将创建一个对应于该分裂的节点。该节点将接受所有这些相同的参数,外加目标统计信息。当我们为节点生成预测时,如果它是终端节点,我们将返回该标签的节点;否则,我们将返回训练标签的均值。这就是我们在此处分配预测的方式。现在我们有了节点,并且希望创建分裂。因此,我们获得X_rightX_left,我们可以递归地处理树的两侧。我们将使用该节点来在XY上创建分裂。所以,如果XNoneX_leftNone,意味着我们将不再向左递归。如果它不是None,那么我们可以将一个节点分配给左侧,这样它就会递归调用find_next_split。如果X_rightNone,那么意味着我们不再继续在右侧生长。如果它不是None,我们可以做同样的事情。因此,我们将通过递归调用find_next_split来为右侧分配节点。我们会继续递归这个过程,不断地增加current_depth + 1,直到一侧达到了最大深度。否则,分裂的大小不足以满足min_sample_split,我们就停止生长。因此,我们到达了停止生长的临界点:

# create the next split
split_feature, split_value, gain = \
     self.splitter.find_best(X, y)

# create the next node based on the best split feature and value
# that we just found. Also compute the "target stat" (mode of y for
# classification problems or mean of y for regression problems) and
# pass that to the node in case it is the terminal node (that is, the
# decision maker)
node = LeafNode(split_feature, split_value, gain, self._target_stat(y))
# Create the splits based on the criteria we just determined, and then
# recurse down left, right sides
X_left, X_right, y_left, y_right = node.create_split(X, y)

# if either the left or right is None, it means we've achieved a
# perfect split. It is then a terminal node and will remain None.
if X_left is not None:
node.left = self._find_next_split(X_left, y_left,
                                  current_depth + 1)

现在,对于预测,我们将沿着树遍历,直到找到记录所属的节点。所以,对于X中的每一行,我们将预测一行,这在LeafNode类中已经看过,通过左右遍历,直到找到该行所属的节点。然后我们将返回class_statistics。因此,如果该行到达某个节点,它会说这个行属于这里。如果该类的节点,分类时为1,那么我们返回1。否则,如果均值是,例如,5.6,那么我们返回相同的值。这就是我们如何生成这些预测,我们将其打包成一个 NumPy 数组:

def predict(self, X):
    # Check the array
    X = check_array(X, dtype=np.float32) # type: np.ndarray

    # For each record in X, find its leaf node in the tree (O(log N))
    # to get the predictions. This makes the prediction operation
    # O(N log N) runtime complexity
    predictions = [self.tree.predict_record(row) for row in X]
    return np.asarray(predictions)

那么,让我们看看分类决策树如何在一些真实数据上表现。在下面的示例脚本中,我们将导入CARTClassifier

from packtml.decision_tree import CARTClassifier
from packtml.utils.plotting import add_decision_boundary_to_axis
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import numpy as np
import sys

我们将在multivariate_normal的 2D 空间中创建两个不同的气泡。使用RandomState中的multivariate_normal,我们将它们全部堆叠在一起,并像往常一样生成train_test_split

# Create a classification dataset
rs = np.random.RandomState(42)
covariance = [[1, .75], [.75, 1]]
n_obs = 500
x1 = rs.multivariate_normal(mean=[0, 0], cov=covariance, size=n_obs)
x2 = rs.multivariate_normal(mean=[1, 3], cov=covariance, size=n_obs)

X = np.vstack((x1, x2)).astype(np.float32)
y = np.hstack((np.zeros(n_obs), np.ones(n_obs)))

# split the data
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

我们将拟合CARTClassifier并执行两种不同的分类器。我们将首先进行第一个。现在你已经了解了方差和偏差,你知道分类器或非参数模型,特别是决策树,具有很高的方差:它们很容易发生过拟合。所以,如果我们使用非常浅的深度,那么我们更有可能不会过拟合。在第二个中,我们将尽可能地进行过拟合,最大深度设为25。由于我们拥有一个相对较小的数据集,我们可以合理地确定这可能会发生过拟合。我们将在查看这个示例的实际输出时看到这一点:

# Fit a simple decision tree classifier and get predictions
shallow_depth = 2
clf = CARTClassifier(X_train, y_train, max_depth=shallow_depth, criterion='gini',
                     random_state=42)
pred = clf.predict(X_test)
clf_accuracy = accuracy_score(y_test, pred)
print("Test accuracy (depth=%i): %.3f" % (shallow_depth, clf_accuracy))

# Fit a deeper tree and show accuracy increases
clf2 = CARTClassifier(X_train, y_train, max_depth=25, criterion='gini',
                      random_state=42)
pred2 = clf2.predict(X_test)
clf2_accuracy = accuracy_score(y_test, pred2)
print("Test accuracy (depth=25): %.3f" % clf2_accuracy)

所以,我们拟合这两棵树,查看准确性并绘制它们。让我们继续运行代码,看看结果如何:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/b83c0c9e-95c3-4083-986c-9d3ad10eb106.png

如果你运行前面的代码,我们得到的是在我们欠拟合的树上的 95%测试准确率,如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/b9a554fc-c31a-41a8-82cb-c0070e92a9a6.png

回归树

现在让我们看看回归树如何表现。我们走过了回归树的完全相同的实现,只不过我们这次将使用方差减少。与其使用模式投票来生成预测,我们将使用均值。

examples目录下,我们有example_regression_decision_tree.py文件。因此,在这里我们将导入CARTRegressor并使用mean_squared_error作为我们的损失函数,以确定我们的表现如何:

from packtml.decision_tree import CARTRegressor
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import numpy as np
import sys

我们将在这里生成一个正弦波的随机值。这就是我们希望作为输出的函数:

# Create a classification dataset
rs = np.random.RandomState(42)
X = np.sort(5 * rs.rand(80, 1), axis=0)
y = np.sin(X).ravel()

# split the data
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

我们将做与分类树中相同的事情。我们将拟合一个简单的max_depth=3的回归树,然后拟合一个max_depth=10的树作为第二个。它不会过拟合那么多,但它会展示随着我们加深模型,预测能力如何增加:

# Fit a simple decision tree regressor and get predictions
clf = CARTRegressor(X_train, y_train, max_depth=3, random_state=42)
pred = clf.predict(X_test)
clf_mse = mean_squared_error(y_test, pred)
print("Test MSE (depth=3): %.3f" % clf_mse)

# Fit a deeper tree and show accuracy increases
clf2 = CARTRegressor(X_train, y_train, max_depth=10, random_state=42)
pred2 = clf2.predict(X_test)
clf2_mse = mean_squared_error(y_test, pred2)
print("Test MSE (depth=10): %.3f" % clf2_mse)

这里,我们只是绘制输出:

x = X_train.ravel()
xte = X_test.ravel()

fig, axes = plt.subplots(1, 2, figsize=(12, 8))
axes[0].scatter(x, y_train, alpha=0.25, c='r')
axes[0].scatter(xte, pred, alpha=1.)
axes[0].set_title("Shallow tree (depth=3) test MSE: %.3f" % clf_mse)

axes[1].scatter(x, y_train, alpha=0.4, c='r')
axes[1].scatter(xte, pred2, alpha=1.)
axes[1].set_title("Deeper tree (depth=10) test MSE: %.3f" % clf2_mse)

# if we're supposed to save it, do so INSTEAD OF showing it
if len(sys.argv) > 1:
    plt.savefig(sys.argv[1])
else:
    plt.show()

让我们开始运行这个。与其运行example_classification_decision_tree.py,我们将运行example_regression_decision_tree.py

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/6040481b-c400-4601-89a7-2bbfa8b7a408.png

所以,首先,你可以看到我们的均方误差随着max_depth的增加而减小,这是一个好现象。你还可以看到,随着深度的增加,我们的结果开始很好地模拟这个正弦波,而且我们能够很好地学习这个非线性函数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/1d363e0b-682a-4d90-a01e-4099456ded17.png

在下一部分,我们将讨论聚类方法并从决策树开始。

各种聚类方法

在这一部分中,我们将介绍不同的聚类方法。首先,我们来看看什么是聚类。然后,我们将解释一些在聚类中可以使用的数学技巧。最后,我们将介绍我们最新的非参数算法 KNN。

什么是聚类?

聚类是机器学习模型中最直观的一个。其思想是,我们可以根据样本之间的接近程度将其分组。假设前提是,距离较近的样本在某些方面更为相似。因此,我们可能想进行聚类的原因有两个。第一个是为了发现目的,通常在我们对数据的基础结构没有假设,或没有标签时,我们会这么做。因此,这通常是完全无监督的做法。但由于这是一本显然是监督学习的书籍,我们将关注第二个用途,即将聚类作为分类技术。

距离度量

所以,在进入算法之前,我想谈一谈一些数学上的深奥概念。当你有两个点,或者任意数量的点,在二维空间中时,它相当容易理解。基本上,这是计算直角三角形中的斜边长度,用来测量距离。然而,当你有一个非常高维的空间时,会发生什么呢?这就是我们将要讨论的内容,我们有很多聚类问题。

所以,最常见的距离度量是欧几里得距离。它本质上是二维方法的推广。它是两个向量之间平方差和的平方根,可以用于任何维度的空间。还有很多其他方法我们暂时不涉及,其中有两种是曼哈顿距离马氏距离。曼哈顿距离与欧几里得距离相似,但它不是平方差,而是取差值的绝对值。

KNN – 介绍

KNN 是一种非常简单直观的聚类分类方法。其思想是,给定一组标记样本,当一个新样本被引入时,我们会查看其 k 个最近的点,并根据周围点的多数决定其类别。所以,在下图中,我们会将这个新的问号标记为正样本,因为它的大多数邻居是正样本:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/a81eab9b-1197-4342-acde-ce1ae5e74b3e.png

KNN – 考虑因素

在这里,你应该考虑一些因素。KNN 算法比较有趣,可能会因为超参数K的不同而在高偏差和高方差之间波动。如果K太大,而你将新样本与整个训练集进行比较,那么它就会偏向多数类。实际上,不管哪个类别多,我们就投票支持那个类别。这会导致模型严重欠拟合。如果K太小,它会更倾向于给紧邻的样本更高的优先级,这样模型就会极度过拟合。除了K的考虑外,你还可能需要对数据进行中心化和标准化。否则,你的距离度量对小规模特征的敏感性会较低。例如,如果一个特征是房屋价格(几千美元),另一个特征是浴室数量(比如1.53.5),你就会在隐式上更关注价格而忽略浴室数量。因此,你可能需要对数据进行中心化和标准化。

经典的 KNN 算法

经典的 KNN 算法会计算训练样本之间的距离,并将其存储在一个距离分区堆结构中,如KDTree或球树,它们本质上是排序堆的二叉树。然后我们查询树来获取测试样本。在本课程中,我们将采用稍微不同的方法,以便更直观、更易读。

在下一节中,我们将讨论如何从零实现 KNN。

从零实现 KNN

在本节中,我们将深入探讨packtml代码库,并了解如何从零开始实现它。我们将从回顾上一节中讨论的经典算法开始,然后查看实际的 Python 代码,看看它有哪些实现上的变化。

回想一下典型的 KNN 算法。高效的实现方法是预先计算距离并将其存储在一个特殊的堆中。当然,计算机科学中总有两种方式,一种是巧妙的方式,另一种是容易阅读的方式。我们会稍微不同一点,努力提高代码的可读性,但它本质上是相同的基本算法。

KNN 聚类

我们有两个文件需要查看。第一个是packtml Python 包中的源代码。第二个是一个将 KNN 应用于iris数据集的示例。现在,让我们跳转到 PyCharm,在那里打开了两个文件。在clustering子模块中,我们打开了knn.py文件。在这个文件中,我们可以找到 KNN 类和所有的实现细节。然后,在examples目录下的clustering子目录中,我们也打开了example_knn_classifier.py文件。在我们完成实现细节的讨论后,再一起走一遍这个文件。

现在,关于其他库,我们将使用 scikit-learn 的 utils 来验证Xy和分类目标。然而,我们还将使用metrics.pairwise子模块来使用euclidean_distances

from __future__ import absolute_import

from sklearn.metrics.pairwise import euclidean_distances
from sklearn.utils.validation import check_X_y
from sklearn.utils.multiclass import check_classification_targets

如果我们想使用不同的距离度量,我们还可以导入曼哈顿距离,正如之前部分所提到的。但是对于这个例子,我们将使用欧几里得距离。所以,如果以后你想调整这个,可以随时更改。我们的 KNN 类将接受三个参数。和BaseSimpleEstimator一样,我们将获取Xy,它们分别是我们的训练向量和训练标签,然后是k,它是我们希望计算的邻居数的调优参数:

 Parameters
 ----------
 X : array-like, shape=(n_samples, n_features)
     The training array. Should be a numpy array or array-like structure
     with only finite values.

y : array-like, shape=(n_samples,)
    The target vector.
k : int, optional (default=10)
    The number of neighbors to identify. The higher the ``k`` parameter,
    the more likely you are to *under*-fit your data. The lower the ``k``
    parameter, the more likely you are to *over*-fit your model.

所以,我们的构造函数非常简单。我们将检查我们的Xy并基本存储它们。然后我们将k分配给self属性。在其他实现中,我们可能会继续计算我们的 KDTree 或我们的球树:

    def __init__(self, X, y, k=10):
        # check the input array
        X, y = check_X_y(X, y, accept_sparse=False, dtype=np.float32,
                         copy=True)

        # make sure we're performing classification here
        check_classification_targets(y)

        # Save the K hyper-parameter so we can use it later
        self.k = k

        # kNN is a special case where we have to save the training data in
        # order to make predictions in the future
        self.X = X
        self.y = y

所以,我们将使用一种暴力法,在预测之前不计算距离。这是对距离的懒惰计算。在我们的预测函数中,我们将获取我们的X,它是我们的测试数组。X是在构造函数中分配的。我们将计算训练数组和测试数组之间的euclidean_distances。在这里,我们得到一个M乘以M的矩阵,其中M是测试数组中样本的数量:

def predict(self, X):
    # Compute the pairwise distances between each observation in
    # the dataset and the training data. This can be relatively expensive
    # for very large datasets!!
    train = self.X
    dists = euclidean_distances(X, train)

为了找到最近的距离,我们将通过列对距离进行argsort,以显示哪些样本最接近。接下来是距离数组,我们将沿着axis列对它进行argsort,这样我们就能基于距离找到最接近的样本:

# Arg sort to find the shortest distance for each row. This sorts
# elements in each row (independent of other rows) to determine the
# order required to sort the rows.
# that is:
# >>> P = np.array([[4, 5, 1], [3, 1, 6]])
# >>> np.argsort(P, axis=1)
# array([[2, 0, 1],
# [1, 0, 2]])
nearest = np.argsort(dists, axis=1)

我们将根据top_ky上切片标签。这些基本上是类标签:


 # We only care about the top K, really, so get sorted and then truncate
 # that is:
 # array([[1, 2, 1],
 # ...
 # [0, 0, 0]])
 predicted_labels = self.y[nearest][:, :self.k]

由于这是一个分类问题,我们关注的是mode。使用mode函数沿着该axis获取众数,并将其转换为 NumPy 数组:

 # We want the most common along the rows as the predictions
 # that is:
 # array([1, ..., 0])
 return mode(predicted_labels, axis=1)[0].ravel()

因此,我们只是为预测函数计算距离,进行argsort以找出最接近的距离,然后找到相应的标签,并获取众数。现在,在examples/clustering目录下,进入example_knn_classifier.py。我们将使用 scikit-learn 中的load_iris函数:

from __future__ import absolute_import

from packtml.clustering import KNNClassifier
from packtml.utils.plotting import add_decision_boundary_to_axis
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_iris
from matplotlib import pyplot as plt
from matplotlib.colors import ListedColormap
import sys

我们只使用前两个维度,这样我们就可以以相对直观的方式进行可视化。执行训练集划分,然后使用StandardScaler进行标准化和缩放:

# Create a classification sub-dataset using iris
iris = load_iris()
X = iris.data[:, :2] # just use the first two dimensions
y = iris.target

# split data
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# scale the data
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

使用k=10来拟合KNNClassifier

# Fit a k-nearest neighbor model and get predictions
k=10
clf = KNNClassifier(X_train, y_train, k=k)
pred = clf.predict(X_test)
clf_accuracy = accuracy_score(y_test, pred)
print("Test accuracy: %.3f" % clf_accuracy)

最后,我们将通过输入以下命令来绘制图形。确保你的环境已经激活,和往常一样:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/eed9a1bd-cf77-4838-801e-e3921dbe7935.png

对于k = 10,输出结果为大约 73-74%的测试准确度。请注意,我们只使用了两个维度:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-ml-py/img/d072a43a-558f-42b2-983c-feb58cb1ba79.png

所以,现在你已经是 KNN 专家了,你可以从零开始构建一个。接下来的部分,我们将比较非参数模型与参数模型。

非参数模型 – 优缺点

在本节中,我们将讨论每个统计学家最喜欢的哲学辩论,即非参数模型与参数模型的优缺点。

非参数模型的优点

非参数模型能够学习预测变量与输出变量之间一些非常复杂的关系,这使得它们在处理复杂建模问题时非常强大。就像我们在决策树中建模的回归正弦波一样,许多非参数模型对数据规模也相当宽容。唯一的主要例外是聚类技术,但这些技术对诸如决策树之类的模型可能具有显著优势,因为这些模型不需要像参数模型那样进行大量的预处理。最后,如果你发现自己面临高方差问题,你总是可以通过增加更多的训练数据来改善模型性能。

非参数模型的缺点

非参数模型也有一些不太好的地方。我们已经讨论过其中的一些。正如你所知道的,它们可能在拟合或预测时更慢,并且在许多情况下比许多参数模型更不直观。如果速度比准确性更不重要,非参数模型可能是你模型的一个好选择。同样,在可解释性方面,这些模型可能过于复杂,难以理解。最后,非参数模型的一个优势是随着数据增多会变得更好,这在数据难以获取时可能成为弱点。它们通常需要比参数模型更多的数据才能有效训练。

使用哪个模型?

我们已经讨论过的参数模型具有一些非常出色且便捷的特点。你可能会选择参数模型而非非参数模型的原因有很多。尤其是在受监管的行业中,我们需要更容易地解释模型。另一方面,非参数模型可能会创建更好、更复杂的模型。但如果你没有足够的数据,它的表现可能不太理想。最好不要过于哲学化地考虑该使用哪个模型。只需使用最适合你数据并满足业务需求的模型即可。

总结

在这一章中,我们首先介绍了非参数模型,然后我们走过了决策树。在接下来的部分中,我们学习了分裂标准及其如何生成分裂。我们还了解了偏差-方差权衡,以及非参数模型通常倾向于偏好更高方差的误差集,而参数模型则偏好高偏差。接着,我们研究了聚类方法,甚至从零开始编写了一个 KNN 类。最后,我们总结了非参数方法的优缺点。

在下一章中,我们将深入探讨监督学习中的一些更高级话题,包括推荐系统和神经网络。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值