原文:annas-archive.org/md5/1b2ecfd03b995ad3ca86b0a07ad56e70

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

机器学习在计算机科学中有广泛的应用。使用机器学习技术的软件系统往往能为用户提供更好的用户体验。随着云数据的日益相关,开发者最终将构建更多智能系统,为用户简化并优化任何常规任务。

本书将介绍几种机器学习技术,并描述我们如何在 Clojure 编程语言中利用这些技术。

Clojure 是一种基于 Java 虚拟机(JVM)的动态和函数式编程语言。需要注意的是,Clojure 是 Lisp 语言家族的一员。Lisp 在 70 年代和 80 年代的人工智能革命中发挥了关键作用。不幸的是,人工智能在 80 年代末失去了其活力。然而,Lisp 却继续发展,并在历史上产生了多种 Lisp 方言。Clojure 是一种简单而强大的 Lisp 方言,首次发布于 2007 年。在撰写本书时,Clojure 是 JVM 上增长最快的编程语言之一。它目前支持一些最先进的语言特性和编程方法,如可选类型、软件事务内存、异步编程和逻辑编程。Clojure 社区以其优雅而强大的库而闻名,这也是使用 Clojure 的另一个令人信服的理由。

机器学习技术基于统计学和基于逻辑的推理。在这本书中,我们将关注机器学习的统计方面。这些技术中的大多数都基于人工智能革命的原则。机器学习仍然是一个活跃的研究和开发领域。软件世界的巨头,如谷歌和微软,也为机器学习做出了重大贡献。现在越来越多的软件公司意识到,使用机器学习技术的应用程序能为用户提供更好的体验。

尽管机器学习中涉及大量的数学知识,但我们将更多地关注这些技术的思想和实际应用,而不是集中在其理论及所使用的数学符号上。本书旨在为机器学习技术提供一个温和的介绍,以及它们如何在 Clojure 语言中应用。

本书涵盖内容

第一章, 处理矩阵,解释了矩阵及其在实现机器学习算法中有用的基本操作。

第二章, 理解线性回归,介绍了线性回归作为一种监督学习形式。我们还将讨论梯度下降算法和普通最小二乘法(OLS)用于拟合线性回归模型。

第三章, 数据分类,涵盖了分类,这是另一种监督学习形式。我们将研究分类的贝叶斯方法、决策树和 k 近邻算法。

第四章, 构建神经网络,解释了在非线性数据分类中有用的人工神经网络(ANNs),并描述了一些 ANN 模型。我们还将研究和实现用于训练 ANN 的反向传播算法,并描述自组织映射(SOMs)。

第五章, 选择和评估数据,涵盖了机器学习模型的评估。在本章中,我们将讨论几种可以用来提高特定机器学习模型有效性的方法。我们还将实现一个工作垃圾邮件分类器,作为如何构建包含评估的机器学习系统的示例。

第六章, 构建支持向量机,涵盖了支持向量机(SVMs)。我们还将描述如何使用 SVMs 来对线性和非线性样本数据进行分类。

第七章, 聚类数据,解释了聚类技术作为一种无监督学习形式,以及我们如何使用它们来发现未标记样本数据中的模式。在本章中,我们将讨论 K-means 和期望最大化(EM)算法。我们还将探讨降维技术。

第八章, 异常检测与推荐,解释了异常检测,这是另一种有用的无监督学习形式。我们还将讨论推荐系统和几种推荐算法。

第九章, 大规模机器学习,涵盖了处理大量数据的技术。在这里,我们解释了 MapReduce 的概念,这是一种并行数据处理技术。我们还将演示如何将数据存储在 MongoDB 中,以及如何使用 BigML 云服务构建机器学习模型。

附录, 参考文献,列出了本书各章节中使用的所有参考文献。

你需要这本书的内容

本书所需的软件之一是 Java 开发工具包(JDK),您可以从www.oracle.com/technetwork/java/javase/downloads/获取。JDK 是运行和开发 Java 平台上的应用程序所必需的。

您还需要的其他主要软件是 Leiningen,您可以从github.com/technomancy/leiningen下载并安装。Leiningen 是一个用于管理 Clojure 项目和它们依赖关系的工具。我们将在第一章使用矩阵中解释如何使用 Leiningen。

在本书的整个过程中,我们将使用包括 Clojure 本身在内的多个其他 Clojure 和 Java 库。Leiningen 将为我们下载这些库。您还需要一个文本编辑器或集成开发环境(IDE)。如果您已经有一个喜欢的文本编辑器,您可能可以使用它。导航到dev.clojure.org/display/doc/Getting+Started以检查使用您特定首选环境所需的提示和插件。如果您没有偏好,我建议您考虑使用 Eclipse 与 Counterclockwise。有关如何设置此环境的说明可在dev.clojure.org/display/doc/Getting+Started+with+Eclipse+and+Counterclockwise找到。

在第九章大规模机器学习中,我们也使用了 MongoDB,您可以从www.mongodb.org/下载并安装。

本书的目标读者

这本书是为熟悉 Clojure 并希望用它来构建机器学习系统的程序员或软件架构师而写的。本书不介绍 Clojure 语言的语法和功能(您应熟悉该语言,但不需要成为 Clojure 专家)。

同样,尽管您不需要成为统计学和坐标几何的专家,但您应该熟悉这些概念,以便理解我们将讨论的几种机器学习技术背后的理论。如有疑问,不要犹豫,查阅并学习本书中使用的数学概念。

约定

在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词如下所示:“之前定义的probability函数需要一个参数来表示我们希望计算其发生概率的属性或条件。”

代码块设置如下:

(defn predict [coefs X]
  {:pre [(= (count coefs)
            (+ 1 (count X)))]}
  (let [X-with-1 (conj X 1)
        products (map * coefs X-with-1)]
    (reduce + products)))

当我们希望将您的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:

:dependencies [[org.clojure/clojure "1.5.1"]
 [incanter "1.5.2"]
        [clatrix "0.3.0"]
        [net.mikera/core.matrix "0.10.0"]]

任何命令行输入或输出都按照以下方式编写:

$ lein deps

我们还使用的一个简单约定是始终显示以user>提示符开始的 Clojure 代码,该代码是在 REPL(读取-评估-打印循环)中输入的。在实践中,这个提示符将根据我们当前使用的 Clojure 命名空间而变化。然而,为了简单起见,REPL 代码以user>提示符开始,如下所示:

user> (every? #(< % 0.0001)
              (map - ols-linear-model-coefs
              (:coefs iris-linear-model))
true

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“点击下一个按钮将您带到下一屏幕”。

注意

警告或重要注意事项以如下框的形式出现。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中获得最大收益的标题非常重要。

要发送给我们一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在您的邮件主题中提及书名。

如果您在某个主题上具有专业知识,并且您对撰写或为本书做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您在www.packtpub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/4351OS_Graphics.pdf下载此文件。

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。任何现有勘误都可以通过从 www.packtpub.com/support 选择您的标题来查看。

盗版

在互联网上,版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者方面的帮助,以及我们为您提供有价值内容的能力。

询问

如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. 矩阵操作

在本章中,我们将探讨一个基本而优雅的数学数据结构——矩阵。大多数计算机科学和数学毕业生已经熟悉矩阵及其应用。在机器学习的背景下,矩阵用于实现多种机器学习技术,如线性回归和分类。我们将在后面的章节中更深入地研究这些技术。

虽然这个章节一开始可能看起来主要是理论性的,但很快我们会看到矩阵是快速组织和索引多维度数据的非常有用的抽象。机器学习技术使用的数据包含多个维度的大量样本值。因此,矩阵可以用来存储和处理这些样本数据。

使用矩阵的一个有趣应用是谷歌搜索,它建立在PageRank算法之上。尽管这个算法的详细解释超出了本书的范围,但值得知道的是,谷歌搜索本质上是在寻找一个极其庞大的数据矩阵的特征向量(更多信息,请参阅《大规模超文本搜索引擎的解剖结构》)。矩阵在计算机科学中有各种应用。尽管我们在这本书中不讨论谷歌搜索使用的特征向量矩阵运算,但在实现机器学习算法的过程中,我们会遇到各种矩阵运算。在本章中,我们将描述我们可以对矩阵执行的有用操作。

介绍 Leiningen

在本书的整个过程中,我们将使用 Leiningen(leiningen.org/)来管理第三方库和依赖项。Leiningen,或lein,是标准的 Clojure 包管理和自动化工具,具有用于管理 Clojure 项目的几个强大功能。

要获取如何安装 Leiningen 的说明,请访问项目网站leiningen.org/lein程序的第一次运行可能需要一段时间,因为它在第一次运行时会下载和安装 Leiningen 的二进制文件。我们可以使用leinnew子命令创建一个新的 Leiningen 项目,如下所示:

$ lein new default my-project

之前的命令创建了一个新的目录,my-project,它将包含 Clojure 项目的所有源文件和配置文件。这个文件夹包含src子目录中的源文件和一个单独的project.clj文件。在这个命令中,default是新项目要使用的项目模板的类型。本书中的所有示例都使用上述default项目模板。

project.clj文件包含与项目相关的所有配置,并将具有以下结构:

(defproject my-project "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license 
  {:name "Eclipse Public License"
   :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.5.1"]])

小贴士

下载示例代码

您可以从您在www.packtpub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

第三方 Clojure 库可以通过向带有:dependencies键的向量中添加声明来包含在项目中。例如,Clojars 上的 core.matrix Clojure 库包(clojars.org/net.mikera/core.matrix)为我们提供了包声明[net.mikera/core.matrix "0.20.0"]。我们只需将此声明粘贴到:dependencies向量中,即可将 core.matrix 库包作为依赖项添加到我们的 Clojure 项目中,如下面的代码所示:

  :dependencies [[org.clojure/clojure "1.5.1"]
                 [net.mikera/core.matrix "0.20.0"]])

要下载project.clj文件中声明的所有依赖项,只需运行以下deps子命令:

$ lein deps

Leiningen 还提供了一个REPL读取-评估-打印循环),它简单来说是一个包含在project.clj文件中声明的所有依赖项的交互式解释器。此 REPL 还将引用我们在项目中定义的所有 Clojure 命名空间。我们可以使用以下leinrepl子命令来启动 REPL。这将启动一个新的 REPL 会话:

$ lein repl

矩阵表示

矩阵简单来说就是按行和列排列的数据矩形数组。大多数编程语言,如 C#和 Java,都直接支持矩形数组,而其他语言,如 Clojure,则使用异构的数组-数组表示法来表示矩形数组。请注意,Clojure 没有直接支持处理数组,并且惯用的 Clojure 代码使用向量来存储和索引元素数组。正如我们稍后将会看到的,矩阵在 Clojure 中表示为一个向量,其元素是其他向量。

矩阵支持多种算术运算,如加法和乘法,这些构成了数学中一个重要的领域,称为线性代数。几乎每种流行的编程语言至少有一个线性代数库。Clojure 通过让我们从多个此类库中选择,并且所有这些库都有一个与矩阵一起工作的单一标准化 API 接口,从而更进一步。

core.matrix库是一个多功能的 Clojure 库,用于处理矩阵。Core.matrix 还包含处理矩阵的规范。关于 core.matrix 的一个有趣的事实是,虽然它提供了此规范的默认实现,但它还支持多个实现。core.matrix 库托管和开发在 GitHub 上github.com/mikera/core.matrix

注意

可以通过在project.clj文件中添加以下依赖项将 core.matrix 库添加到 Leiningen 项目中:

[net.mikera/core.matrix "0.20.0"]

对于即将到来的示例,命名空间声明应类似于以下声明:

(ns my-namespace
  (:use clojure.core.matrix))

注意,在 Clojure 中使用 :import 来包含库命名空间通常是不推荐的。相反,使用 :require 形式进行别名命名空间是首选的。然而,对于下一节中的示例,我们将使用前面的命名空间声明。

在 Clojure 中,一个矩阵简单地说就是一个向量的向量。这意味着矩阵被表示为一个其元素是其他向量的向量。向量是一个元素数组,检索元素的时间几乎恒定,与具有线性查找时间的列表不同。然而,在矩阵的数学上下文中,向量仅仅是具有单行或单列的矩阵。

要从一个向量的向量创建一个矩阵,我们使用以下 matrix 函数,并将一个向量的向量或一个引用列表传递给它。请注意,矩阵的所有元素都内部表示为 double 数据类型(java.lang.Double),以增加精度。

user> (matrix [[0 1 2] [3 4 5]])    ;; using a vector
[[0 1 2] [3 4 5]]
user> (matrix '((0 1 2) (3 4 5)))   ;; using a quoted list
[[0 1 2] [3 4 5]]

在前面的示例中,矩阵有两行三列,或者更简洁地说是一个 2 x 3 矩阵。应注意,当矩阵由向量的向量表示时,表示矩阵各个行的所有向量应该具有相同的长度。

创建的矩阵以向量的形式打印出来,这不是最佳的可视化表示方法。我们可以使用 pm 函数如下打印矩阵:

user> (def A (matrix [[0 1 2] [3 4 5]]))
#'user/A
user> (pm A)
[[0.000 1.000 2.000]
 [3.000 4.000 5.000]]

这里,我们定义了一个矩阵 A,它用以下方式在数学上表示。请注意,使用大写变量名只是为了说明,因为所有 Clojure 变量都按照惯例以小写形式书写。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_04.jpg

矩阵 A 由元素 a[i,j] 组成,其中 i 是矩阵的行索引,j 是列索引。我们可以用以下方式用括号表示数学上的矩阵 A

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_05.jpg

我们可以使用 matrix? 函数来检查一个符号或变量是否实际上是一个矩阵。matrix? 函数将对实现 core.matrix 规范的所有矩阵返回 true。有趣的是,matrix? 函数也会对普通向量的向量返回 true

core.matrix 的默认实现是用纯 Clojure 编写的,这会影响处理大型矩阵时的性能。core.matrix 规范有两个流行的贡献实现,即使用纯 Java 实现的 vectorz-clj (github.com/mikera/vectorz-clj) 和通过本地库实现的 clatrix (github.com/tel/clatrix)。虽然还有其他几个库实现了 core.matrix 规范,但这两个库被视为最成熟的。

注意

Clojure 有三种类型的库,即 core、contrib 和第三方库。core 和 contrib 库是标准 Clojure 库的一部分。core 和 contrib 库的文档可以在 clojure.github.io/ 找到。core 和 contrib 库之间的唯一区别是,contrib 库不是与 Clojure 语言一起分发的,必须单独下载。

任何人都可开发第三方库,并通过 Clojars (clojars.org/) 提供这些库。Leiningen 支持所有这些库,并且在这些库之间没有太大的区别。

contrib 库通常最初是作为第三方库开发的。有趣的是,core.matrix 首先作为一个第三方库开发,后来被提升为 contrib 库。

clatrix 库使用 基本线性代数子程序BLAS)规范来接口它所使用的本地库。BLAS 也是矩阵和向量线性代数操作的稳定规范,这些操作主要被本地语言使用。在实践中,clatrix 的性能显著优于 core.matrix 的其他实现,并定义了几个用于处理矩阵的实用函数。你应该注意,clatrix 库将矩阵视为可变对象,而 core.matrix 规范的其他实现则习惯上将矩阵视为不可变类型。

在本章的大部分内容中,我们将使用 clatrix 来表示和操作矩阵。然而,我们可以有效地重用 core.matrix 中的函数,这些函数在通过 clatrix 创建的矩阵上执行矩阵操作(如加法和乘法)。唯一的区别是,我们不应该使用 core.matrix 命名空间中的 matrix 函数来创建矩阵,而应该使用 clatrix 库中定义的函数。

注意

可以通过在 project.clj 文件中添加以下依赖项将 clatrix 库添加到 Leiningen 项目中:

[clatrix "0.3.0"]

对于即将到来的示例,命名空间声明应类似于以下声明:

(ns my-namespace
  (:use clojure.core.matrix)
  (:require [clatrix.core :as cl]))

请记住,我们可以在同一个源文件中使用 clatrix.coreclojure.core.matrix 命名空间,但一个好的做法是将这两个命名空间导入到别名命名空间中,以防止命名冲突。

我们可以使用以下 cl/matrix 函数从 clatrix 库创建矩阵。请注意,clatrix 产生的矩阵表示与 core.matrix 略有不同,但更具有信息量。如前所述,可以使用 pm 函数将矩阵打印为向量向量:

user> (def A (cl/matrix [[0 1 2] [3 4 5]]))
#'user/A
user> A
 A 2x3 matrix
 -------------
 0.00e+00  1.00e+00  2.00e+00 
 3.00e+00  4.00e+00  5.00e+00 
user> (pm A)
[[0.000 1.000 2.000]
 [3.000 4.000 5.000]]
nil

我们还可以使用matrix函数的重载版本,它将矩阵实现名称作为第一个参数,后面跟着矩阵作为向量的常规定义,来创建矩阵。实现名称指定为一个关键字。例如,默认的持久向量实现指定为:persistent-vector,而 clatrix 实现指定为:clatrix。我们可以通过指定这个关键字参数来调用matrix函数,创建不同实现的矩阵,如下述代码所示。在第一次调用中,我们使用:persistent-vector关键字调用matrix函数来指定默认的持久向量实现。同样,我们使用:clatrix关键字调用matrix函数来创建 clatrix 实现。

user> (matrix :persistent-vector [[1 2] [2 1]])
[[1 2] [2 1]]
user> (matrix :clatrix [[1 2] [2 1]])
 A 2x2 matrix
 -------------
 1.00e+00  2.00e+00 
 2.00e+00  1.00e+00

一个有趣的观点是,clatrix 将向量数组和数字的向量都视为matrix函数的有效参数,这与 core.matrix 的处理方式不同。例如,[0 1]生成一个 2 x 1 的矩阵,而[[0 1]]生成一个 1 x 2 的矩阵。core.matrix 的matrix函数没有这个功能,并且始终期望传递一个向量数组的向量。然而,使用[0 1][[0 1]]调用cl/matrix函数将创建以下矩阵而不会出现任何错误:

user> (cl/matrix [0 1])
 A 2x1 matrix
 -------------
 0.00e+00 
 1.00e+00 
user> (cl/matrix [[0 1]])
 A 1x2 matrix
 -------------
 0.00e+00  1.00e+00 

matrix?函数类似,我们可以使用cl/clatrix?函数来检查一个符号或变量是否来自 clatrix 库的矩阵。实际上,matrix?函数检查的是核心矩阵规范或协议的实现,而cl/clatrix?函数检查的是特定类型。如果cl/clatrix?函数对一个特定变量返回true,则matrix?也应该返回true;然而,这个公理的反面并不成立。如果我们使用matrix函数而不是cl/matrix函数创建一个矩阵并调用cl/clatrix?,它将返回false;这在下述代码中显示:

user> (def A (cl/matrix [[0 1]]))
#'user/A
user> (matrix? A)
true
user> (cl/clatrix? A)
true
user> (def B (matrix [[0 1]]))
#'user/B
user> (matrix? B)
true
user> (cl/clatrix? B)
false

矩阵的大小是一个重要的属性,通常需要计算。我们可以使用row-count函数来找到矩阵中的行数。实际上,这仅仅是组成矩阵的向量的长度,因此,我们也可以使用标准的count函数来确定矩阵的行数。同样,column-count函数返回矩阵中的列数。考虑到矩阵由等长的向量组成,列数应该是任何内部向量,或者说任何矩阵行的长度。我们可以在 REPL 中对以下示例矩阵的countrow-countcolumn-count函数的返回值进行检查:

user> (count (cl/matrix [0 1 2]))
3
user> (row-count (cl/matrix [0 1 2]))
3
user> (column-count (cl/matrix [0 1 2]))
1

要使用矩阵的行和列索引检索元素,请使用以下 cl/get 函数。除了执行操作的矩阵外,此函数还接受两个参数作为矩阵的索引。请注意,在 Clojure 代码中,所有元素都是相对于 0 进行索引的,这与矩阵的数学表示法不同,后者将 1 视为矩阵中第一个元素的位置。

user> (def A (cl/matrix [[0 1 2] [3 4 5]]))
#'user/A
user> (cl/get A 1 1)
4.0
user> (cl/get A 3)
4.0

如前例所示,cl/get 函数还有一个接受单个索引值作为函数参数的替代形式。在这种情况下,元素通过行优先遍历进行索引。例如,(cl/get A 1) 返回 3.0,而 (cl/get A 3) 返回 4.0。我们可以使用以下 cl/set 函数来更改矩阵的元素。此函数接受与 cl/get 相似的参数——一个矩阵、一个行索引、一个列索引,以及最后要设置在矩阵指定位置的新元素。实际上,cl/set 函数会修改或修改它提供的矩阵。

user> (pm A)
[[0.000 1.0002.000]
 [3.000 4.0005.000]]
nil
user> (cl/set A 1 2 0)
#<DoubleMatrix [0.000000, 1.000000, … , 0.000000]>
user> (pm A)
[[0.000 1.000 2.000]
 [3.000 4.000 0.000]]
nil

Clatrix 库还提供了两个用于函数组合的便捷函数:cl/mapcl/map-indexed。这两个函数都接受一个函数和一个矩阵作为参数,并将传递的函数应用于矩阵中的每个元素,其方式类似于标准的 map 函数。此外,这两个函数都返回新的矩阵,并且不会修改它们作为参数提供的矩阵。请注意,传递给 cl/map-indexed 的函数应该接受三个参数——行索引、列索引以及元素本身:

user> (cl/map-indexed 
      (fn [i j m] (* m 2)) A)
 A 2x3 matrix
 -------------
 0.00e+00  2.00e+00  4.00e+00 
 6.00e+00  8.00e+00  1.00e+01 
user> (pm (cl/map-indexed (fn [i j m] i) A))
[[0.000 0.000 0.000]
 [1.000 1.000 1.000]]
nil
user> (pm (cl/map-indexed (fn [i j m] j) A))
[[0.000 1.000 2.000]
 [0.000 1.000 2.000]]
nil

生成矩阵

如果矩阵的行数和列数相等,则我们称该矩阵为 方阵。我们可以通过使用 repeat 函数重复单个元素来轻松生成一个简单的方阵,如下所示:

(defn square-mat
  "Creates a square matrix of size n x n 
  whose elements are all e"
  [n e]
  (let [repeater #(repeat n %)]
    (matrix (-> e repeater repeater))))

在前例中,我们定义了一个闭包来重复值 n 次,这显示为 repeater。然后我们使用 thread 宏 (->) 将元素 e 通过闭包传递两次,最后将 matrix 函数应用于 thread 宏的结果。我们可以扩展此定义,以便我们可以指定用于生成的矩阵的矩阵实现;这是如下完成的:

(defn square-mat
  "Creates a square matrix of size n x n whose 
  elements are all e. Accepts an option argument 
  for the matrix implementation."
  [n e & {:keys [implementation] 
          :or {implementation :persistent-vector}}]
  (let [repeater #(repeat n %)]
    (matrix implementation (-> e repeater repeater))))

square-mat 函数被定义为接受可选的关键字参数,这些参数指定了生成的矩阵的矩阵实现。我们将 core.matrix 的默认 :persistent-vector 实现指定为 :implementation 关键字的默认值。

现在,我们可以使用这个函数来创建方阵,并在需要时指定矩阵实现:

user> (square-mat 2 1)
[[1 1] [1 1]]
user> (square-mat 2 1 :implementation :clatrix)
 A 2x2 matrix
 -------------
 1.00e+00  1.00e+00
 1.00e+00  1.00e+00

经常使用的一种特殊类型的矩阵是单位矩阵。单位矩阵是一个对角线元素为 1 而其他所有元素为 0 的方阵。我们正式定义单位矩阵 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_07.jpg 如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_08.jpg

我们可以使用之前提到的 cl/map-indexed 函数来实现一个创建单位矩阵的函数,如下代码片段所示。我们首先使用之前定义的 square-mat 函数创建一个 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_06.jpg 大小的正方形矩阵 init,然后使用 cl/map-indexed 将所有对角线元素映射为 1

(defn id-mat
  "Creates an identity matrix of n x n size"
  [n]
  (let [init (square-mat :clatrix n 0)
       identity-f (fn [i j n]
                     (if (= i j) 1 n))]
    (cl/map-indexed identity-f init)))

core.matrix 库也有自己的这个函数版本,命名为 identity-matrix

user> (id-mat 5)
 A 5x5 matrix
 -------------
 1.00e+00  0.00e+00 0.00e+00 0.00e+00 0.00e+00
 0.00e+00  1.00e+00 0.00e+00 0.00e+00 0.00e+00
 0.00e+00  0.00e+00 1.00e+00 0.00e+00 0.00e+00
 0.00e+00  0.00e+00 0.00e+00 1.00e+00 0.00e+00 
 0.00e+00  0.00e+00 0.00e+00 0.00e+00 1.00e+00 
user> (pm (identity-matrix 5))
[[1.000 0.000 0.000 0.000 0.000]
 [0.000 1.000 0.000 0.000 0.000]
 [0.000 0.000 1.000 0.000 0.000]
 [0.000 0.000 0.000 1.000 0.000]
 [0.000 0.000 0.000 0.000 1.000]]
nil

我们还会遇到另一个常见场景,即需要生成一个包含随机数据的矩阵。让我们实现以下函数来生成随机矩阵,就像之前定义的 square-mat 函数一样,使用 rand-int 函数。请注意,rand-int 函数接受一个参数 n,并返回一个介于 0n 之间的随机整数:

(defn rand-square-mat 
  "Generates a random matrix of size n x n"
  [n]
  ;; this won't work
  (matrix (repeat n (repeat n (rand-int 100))))) 

但这个函数生成的矩阵的所有元素都是单个随机数,这并不太有用。例如,如果我们用任何整数作为参数调用 rand-square-mat 函数,它将返回一个包含单个不同随机数的矩阵,如下代码片段所示:

user> (rand-square-mat 4)
[[94 94] [94 94] [94 94] [94 94]]

相反,我们应该使用 rand-int 函数映射 square-mat 函数生成的正方形矩阵的每个元素,为每个元素生成一个随机数。不幸的是,cl/map 只能与由 clatrix 库创建的矩阵一起使用,但我们可以通过使用 repeatedly 函数返回的惰性序列轻松地复制这种行为。请注意,repeatedly 函数接受一个惰性生成序列的长度和一个用作该序列生成器的函数作为参数。因此,我们可以实现使用 clatrix 和 core.matrix 库生成随机矩阵的函数如下:

(defn rand-square-clmat
  "Generates a random clatrix matrix of size n x n"
  [n]
  (cl/map rand-int (square-mat :clatrix n 100)))

(defn rand-square-mat
  "Generates a random matrix of size n x n"
  [n]
  (matrix
   (repeatedly n #(map rand-int (repeat n 100)))))

这个实现按预期工作,新矩阵的每个元素现在都是一个独立生成的随机数。我们可以在 REPL 中通过调用以下修改后的 rand-square-mat 函数来验证这一点:

user> (pm (rand-square-mat 4))
[[97.000 35.000 69.000 69.000]
 [50.000 93.000 26.000  4.000]
 [27.000 14.000 69.000 30.000]
 [68.000 73.000 0.0007 3.000]]
nil
user> (rand-square-clmat 4)
 A 4x4 matrix
 -------------
 5.30e+01  5.00e+00  3.00e+00  6.40e+01 
 6.20e+01  1.10e+01  4.10e+01  4.20e+01 
 4.30e+01  1.00e+00  3.80e+01  4.70e+01 
 3.00e+00  8.10e+01  1.00e+01  2.00e+01

我们还可以使用 clatrix 库中的 cl/rnorm 函数生成随机元素矩阵。这个函数生成一个具有可选指定均值和标准差的正态分布随机元素矩阵。矩阵是正态分布的,意味着所有元素都均匀分布在指定的均值周围,其分布由标准差指定。因此,低标准差会产生一组几乎等于均值的值。

cl/rnorm 函数有几个重载版本。让我们在 REPL 中检查其中几个:

user> (cl/rnorm 10 25 10 10)
 A 10x10 matrix
 ---------------
-1.25e-01  5.02e+01 -5.20e+01  .  5.07e+01  2.92e+01  2.18e+01 
-2.13e+01  3.13e+01 -2.05e+01  . -8.84e+00  2.58e+01  8.61e+00 
 4.32e+01  3.35e+00  2.78e+01  . -8.48e+00  4.18e+01  3.94e+01 
 ... 
 1.43e+01 -6.74e+00  2.62e+01  . -2.06e+01  8.14e+00 -2.69e+01 
user> (cl/rnorm 5)
 A 5x1 matrix
 -------------
 1.18e+00 
 3.46e-01 
-1.32e-01 
 3.13e-01 
-8.26e-02 
user> (cl/rnorm 3 4)
 A 3x4 matrix
 -------------
-4.61e-01 -1.81e+00 -6.68e-01  7.46e-01 
 1.87e+00 -7.76e-01 -1.33e+00  5.85e-01 
 1.06e+00 -3.54e-01  3.73e-01 -2.72e-02 

在前面的例子中,第一次调用指定了均值、标准差以及矩阵的行数和列数。第二次调用指定了一个单个参数 n 并生成一个 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_09.jpg 大小的矩阵。最后,第三次调用指定了矩阵的行数和列数。

core.matrix库还提供了一个compute-matrix函数来生成矩阵,这对 Clojure 程序员来说会感觉非常自然。此函数需要一个表示矩阵大小的向量,以及一个接受与矩阵维度数量相等的参数的函数。实际上,compute-matrix足够灵活,可以用来生成单位矩阵,以及随机元素矩阵。

我们可以使用compute-matrix函数实现以下函数来创建单位矩阵以及随机元素矩阵:

(defn id-computed-mat
  "Creates an identity matrix of size n x n 
  using compute-matrix"
  [n]
  (compute-matrix [n n] #(if (= %1 %2) 1 0)))

(defn rand-computed-mat
  "Creates an n x m matrix of random elements 
  using compute-matrix"
  [n m]
  (compute-matrix [n m] 
   (fn [i j] (rand-int 100))))

添加矩阵

Clojure 语言不直接支持矩阵操作,但通过core.matrix规范实现。在 REPL 中尝试添加两个矩阵,如下面的代码片段所示,只会抛出一个错误,指出在期望整数的地方找到了向量:

user> (+ (matrix [[0 1]]) (matrix [[0 1]]))
ClassCastException clojure.lang.PersistentVector cannot be cast to java.lang.Number  clojure.lang.Numbers.add (Numbers.java:126)

这是因为+函数作用于数字而不是矩阵。要添加矩阵,我们应该使用core.matrix.operators命名空间中的函数。在包含core.matrix.operators之后,命名空间声明应该看起来像以下代码片段:

(ns my-namespace
  (:use clojure.core.matrix)
  (:require [clojure.core.matrix.operators :as M]))

注意,这些函数实际上被导入到一个别名命名空间中,因为像+*这样的函数名与默认 Clojure 命名空间中的函数名冲突。在实践中,我们应该始终尝试通过:require:as过滤器使用别名命名空间,并避免使用:use过滤器。或者,我们可以在命名空间声明中使用:refer-clojure过滤器简单地不引用冲突的函数名,如下面的代码所示。然而,这应该谨慎使用,并且仅作为最后的手段。

对于本节中的代码示例,我们将使用之前的声明以提高清晰度:

(ns my-namespace
  (:use clojure.core.matrix)
  (:require clojure.core.matrix.operators)
  (:refer-clojure :exclude [+ - *])) 

我们可以使用M/+函数对两个或多个矩阵执行矩阵加法。要检查任意数量矩阵的相等性,我们使用M/==函数:

user> (def A (matrix [[0 1 2] [3 4 5]]))
#'user/A
user> (def B (matrix [[0 0 0] [0 0 0]]))
#'user/B
user> (M/== B A)
false
user> (def C (M/+ A B))
#'user/C
user> C
[[0 1 2] [3 4 5]]
user> (M/== C A)
true

如果两个矩阵 AB 满足以下等式,则称它们是相等的:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_10.jpg

因此,前面的等式解释了两个或多个矩阵相等当且仅当满足以下条件:

  • 每个矩阵都有相同数量的行和列

  • 所有具有相同行和列索引的元素都相等

以下是一个简单而优雅的矩阵相等实现。它基本上是使用标准的reducemap函数比较向量相等性:

(defn mat-eq
  "Checks if two matrices are equal"
  [A B]
  (and (= (count A) (count B))
       (reduce #(and %1 %2) (map = A B))))

我们首先使用count=函数比较两个矩阵的行长度,然后使用reduce函数比较内部向量元素。本质上,reduce函数反复将一个接受两个参数的函数应用于序列中的连续元素,并在序列中的所有元素都被应用函数“减少”后返回最终结果。

或者,我们也可以使用类似的组合使用 every?true? Clojure 函数。使用表达式 (every? true? (map = A B)),我们可以检查两个矩阵的相等性。请记住,true? 函数在传入 true 时返回 true(否则返回 false),而 every? 函数在给定的谓词函数对给定序列中的所有值返回 true 时返回 true

要加两个矩阵,它们必须有相等数量的行和列,和本质上是一个由具有相同行和列索引的元素之和组成的矩阵。两个矩阵 AB 的和已经正式定义为以下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_11.jpg

使用标准的 mapv 函数实现矩阵加法几乎是显而易见的,它只是 map 函数的一个变体,返回一个向量。我们将 mapv 应用到矩阵的每一行以及整个矩阵上。请注意,此实现旨在用于向量(向量中的向量),尽管它可以很容易地与 core.matrix 中的 matrixas-vec 函数一起使用来操作矩阵。我们可以实现以下函数,使用标准 mapv 函数执行矩阵加法:

(defn mat-add
  "Add two matrices"
  [A B]
  (mapv #(mapv + %1 %2) A B))

我们同样可以很容易地将 mat-add 函数推广到任意数量的矩阵,使用 reduce 函数。如下面的代码所示,我们可以扩展 mat-add 的先前定义,使其使用 reduce 函数适用于任意数量的矩阵:

(defn mat-add
  "Add two or more matrices"
  ([A B]
     (mapv #(mapv + %1 %2) A B))
  ([A B & more]
     (let [M (concat [A B] more)]
       (reduce mat-add M))))

在一个 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_06.jpg 矩阵 A 上有一个有趣的单一运算,即矩阵的迹,表示为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_12.jpg。矩阵的迹本质上是其对角元素的求和:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_13.jpg

使用前面描述的 cl/map-indexedrepeatedly 函数实现矩阵的迹函数相当简单。我们在这里省略了它,以便作为你的练习。

矩阵乘法

乘法是矩阵上的另一个重要二元运算。在更广泛的意义上,矩阵乘法这一术语指的是几种将矩阵相乘以产生新矩阵的技术。

让我们在 REPL 中定义三个矩阵 ABC 以及一个单一值 N。这些矩阵具有以下值:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_14.jpg

我们可以使用 core.matrix 库中的 M/* 函数来乘以矩阵。除了用于乘以两个矩阵外,此函数还可以用于乘以任意数量的矩阵以及标量值。我们可以在 REPL 中尝试以下 M/* 函数来乘以两个给定的矩阵:

user> (pm (M/* A B))
[[140.000 200.000]
 [320.000 470.000]]
nil
user> (pm (M/* A C))
RuntimeException Mismatched vector sizes  clojure.core.matrix.impl.persistent-vector/... 
user> (def N 10)
#'user/N
user> (pm (M/* A N))
[[10.000 20.000 30.000]
 [40.000 50.000 60.000]]
nil

首先,我们计算了两个矩阵的乘积。这个操作被称为矩阵-矩阵乘法。然而,矩阵 AC 的乘法是不行的,因为这两个矩阵的大小不兼容。这把我们带到了矩阵乘法的第一个规则:要乘以两个矩阵 ABA 中的列数必须等于 B 中的行数。结果矩阵具有与 A 相同的行数和与 B 相同的列数。这就是为什么 REPL 不同意乘以 AC,而是简单地抛出一个异常。

对于大小为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_15.jpg的矩阵 A 和大小为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_16.jpg的矩阵 B,这两个矩阵的乘积只有在https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_17.jpg的情况下才存在,而 AB 的乘积是一个大小为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_18.jpg的新矩阵。

矩阵 AB 的乘积是通过将 A 中行的元素与 B 中相应的列相乘,然后将结果值相加,为 A 的每一行和 B 的每一列产生一个单一值来计算的。因此,结果乘积具有与 A 相同的行数和与 B 相同的列数。

我们可以这样定义两个兼容大小的矩阵的乘积:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_19.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_20.jpg

以下是如何使用 AB 中的元素来计算两个矩阵的乘积的说明:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_01.jpg

这看起来稍微有些复杂,所以让我们用一个例子来演示前面的定义,使用我们之前定义的矩阵 AB。以下计算实际上与 REPL 产生的值一致:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_22.jpg

注意,矩阵乘法不是交换运算。然而,这个操作确实表现出函数的关联性质。对于乘积兼容大小的矩阵 ABC,以下性质始终成立,只有一个例外,我们稍后会揭露:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_23.jpg

一个明显的推论是,一个方阵与另一个相同大小的方阵相乘会产生一个结果矩阵,其大小与两个原始矩阵相同。同样,方阵的平方、立方以及其他幂次运算会产生相同大小的矩阵。

另一个有趣的性质是,方阵在乘法中有一个单位元素,即与乘积兼容大小的单位矩阵。但是,单位矩阵本身也是一个方阵,这使我们得出结论,方阵与单位矩阵的乘法是一个交换操作。因此,矩阵乘法不是交换的规则实际上在其中一个矩阵是单位矩阵而另一个是方阵时并不成立。这可以由以下等式形式化总结:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_24.jpg

矩阵乘法的简单实现的时间复杂度为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_25.jpg,对于一个https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_26.jpg矩阵需要八个乘法操作。时间复杂度指的是特定算法运行到完成所需的时间。因此,线性代数库使用更有效的算法,如Strassen 算法,来实现矩阵乘法,该算法只需要七个乘法操作,并将复杂度降低到https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_27.jpg

Clatrix 库对矩阵乘法的实现性能显著优于默认的持久向量实现,因为它与本地库进行了接口。在实践中,我们可以使用像 criterium 这样的基准测试库来对 Clojure 进行此比较(github.com/hugoduncan/criterium)。或者,我们也可以通过定义一个简单的函数来乘以两个矩阵,然后使用我们之前定义的rand-square-matrand-square-clmat函数将不同实现的大矩阵传递给它,简要比较这两种实现的性能。我们可以定义一个函数来测量乘以两个矩阵所需的时间。

此外,我们还可以定义两个函数来乘以使用我们之前定义的rand-square-matrand-square-clmat函数创建的矩阵,如下所示:

(defn time-mat-mul
  "Measures the time for multiplication of two matrices A and B"
  [A B]
  (time (M/* A B)))

(defn core-matrix-mul-time []
  (let [A (rand-square-mat 100)
        B (rand-square-mat 100)]
    (time-mat-mul A B)))

(defn clatrix-mul-time []
  (let [A (rand-square-clmat 100)
        B (rand-square-clmat 100)]
    (time-mat-mul A B)))

我们可以看到,core.matrix 实现平均需要一秒钟来计算两个随机生成矩阵的乘积。然而,clatrix 实现平均不到一毫秒,尽管第一次调用的通常需要 35 到 40 毫秒来加载本地 BLAS 库。当然,这个值可能会根据计算它的硬件略有不同。尽管如此,除非有有效的理由,例如硬件不兼容或避免额外的依赖,否则在处理大型矩阵时,clatrix 是首选。

接下来,让我们看看 标量乘法,它涉及将单个值 N 或标量与矩阵相乘。结果矩阵的大小与原始矩阵相同。对于一个 2 x 2 矩阵,我们可以定义标量乘法如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_28.jpg

对于矩阵 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_29.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_30.jpg,其乘积如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_31.jpg

注意,我们还可以使用 core.matrix 库中的 scale 函数来执行标量乘法:

user> (pm (scale A 10))
[[10.000 20.000 30.000]
 [40.000 50.000 60.000]]
nil
user> (M/== (scale A 10) (M/* A 10))
true

最后,我们将简要地看一下矩阵乘法的一种特殊形式,称为 矩阵-向量乘法。向量可以简单地看作是一个只有一行的矩阵,它与大小与乘积兼容的方阵相乘,产生一个与原始向量大小相同的新向量。将大小为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_32.jpg 的矩阵 A 和向量 V 的转置 V’(大小为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_33.jpg)相乘,产生一个大小为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_34.jpg 的新向量 V"。如果 A 是一个方阵,那么 V" 的大小与转置 V’ 相同。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_35.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_36.jpg

矩阵转置和求逆

另一个常用的基本矩阵操作是矩阵的 转置。矩阵 A 的转置表示为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_37.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_38.jpg。定义矩阵转置的一个简单方法是通过反射矩阵到其 主对角线。主对角线是指由行和列索引相等的元素组成的对角线。我们还可以通过交换矩阵的行和列来描述矩阵的转置。我们可以使用 core.matrix 中的以下 transpose 函数来执行此操作:

user> (def A (matrix [[1 2 3] [4 5 6]]))
#'user/A
user> (pm (transpose A))
[[1.000 4.000]
 [2.000 5.000]
 [3.000 6.000]]
nil

我们可以定义以下三种获取矩阵转置的可能方法:

  • 原始矩阵沿主对角线进行反射

  • 矩阵的行变成其转置的列

  • 矩阵的列变成其转置的行

因此,矩阵中的每个元素在其转置中行和列都交换了,反之亦然。这可以用以下方程正式表示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_39.jpg

这引出了可逆矩阵的概念。如果一个方阵存在另一个方阵作为其逆矩阵,并且与原矩阵相乘时产生单位矩阵,则称该方阵是可逆的。一个大小为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_06.jpg 的矩阵 A,如果以下等式成立,则称其有一个逆矩阵 B

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_41.jpg

让我们使用 core.matrix 的 inverse 函数来测试这个等式。请注意,core.matrix 库的默认持久实现没有实现逆操作,所以我们使用 clatrix 库中的矩阵。在以下示例中,我们使用 cl/matrix 函数从 clatrix 库创建一个矩阵,使用 inverse 函数确定其逆,然后使用 M/* 函数将这两个矩阵相乘:

user> (def A (cl/matrix [[2 0] [0 2]]))
#'user/A
user> (M/* (inverse A) A)
 A 2x2 matrix
 -------------
 1.00e+00  0.00e+00 
 0.00e+00  1.00e+00

在前面的例子中,我们首先定义了一个矩阵 A,然后将其与其逆矩阵相乘以产生相应的单位矩阵。当我们使用双精度数值类型作为矩阵的元素时,一个有趣的观察是,并非所有矩阵与它们的逆矩阵相乘都会产生单位矩阵。

对于某些矩阵,可以观察到一些小的误差,这是由于使用 32 位表示浮点数的限制造成的;如下所示:

user> (def A (cl/matrix [[1 2] [3 4]]))
#'user/A
user> (inverse A)
 A 2x2 matrix
 -------------
-2.00e+00  1.00e+00 
 1.50e+00 -5.00e-01

为了找到一个矩阵的逆,我们首先必须定义该矩阵的 行列式,这仅仅是从给定矩阵中确定的一个值。首先,行列式仅存在于方阵中,因此,逆矩阵仅存在于行数和列数相等的矩阵中。矩阵的行列式表示为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_42.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_43.jpg。行列式为零的矩阵被称为 奇异矩阵。对于一个矩阵 A,我们定义其行列式如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_44.jpg

我们可以使用前面的定义来表示任何大小的矩阵的行列式。一个有趣的观察是,单位矩阵的行列式始终是 1。作为一个例子,我们将如下找到给定矩阵的行列式:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_45.jpg

对于一个 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_46.jpg 矩阵,我们可以使用 萨鲁斯规则 作为计算矩阵行列式的另一种方法。要使用此方案计算矩阵的行列式,我们首先将矩阵的前两列写在第三列的右侧,使得一行中有五列。接下来,我们加上从上到下对角线的乘积,并减去从下到上对角线的乘积。这个过程可以用以下图表说明:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_03.jpg

通过使用萨鲁斯规则,我们正式地将矩阵 A 的行列式表达如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_48.jpg

我们可以使用 core.matrix 的以下 det 函数在 REPL 中计算矩阵的行列式。请注意,此操作不是由 core.matrix 的默认持久向量实现实现的。

user> (def A (cl/matrix [[-2 2 3] [-1 1 3] [2 0 -1]]))
#'user/A
user> (det A)
6.0

现在我们已经定义了矩阵的行列式,让我们用它来定义矩阵的逆。我们已经讨论了可逆矩阵的概念;找到矩阵的逆就是确定一个矩阵,当它与原矩阵相乘时会产生一个单位矩阵。

对于矩阵的逆存在,其行列式必须不为零。接下来,对于原矩阵中的每个元素,我们找到去掉所选元素的行和列的矩阵的行列式。这会产生一个与原矩阵大小相同的矩阵(称为原矩阵的余子式矩阵)。余子式矩阵的转置称为原矩阵的伴随矩阵。通过将伴随矩阵除以原矩阵的行列式,可以得到逆矩阵。现在,让我们正式定义一个 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_26.jpg 矩阵 A 的逆。我们用 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_49.jpg 表示矩阵 A 的逆,它可以正式表示如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_50.jpg

以一个例子来说明,让我们找到一个样本 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_26.jpg 矩阵的逆。实际上,我们可以验证,当与原矩阵相乘时,逆矩阵会产生一个单位矩阵,如下面的例子所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_51.jpg

同样,我们按照以下方式定义一个 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_46.jpg 矩阵的逆:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_52.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_53.jpg

现在,让我们计算一个 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_46.jpg 矩阵的逆:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_54.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_55.jpg

我们提到奇异和非方阵没有逆,我们可以看到当inverse函数接收到这样的矩阵时,会抛出错误。如下面的 REPL 输出所示,如果给定的矩阵不是方阵,或者给定的矩阵是奇异的,inverse函数将抛出错误:

user> (def A (cl/matrix [[1 2 3] [4 5 6]]))
#'user/A
user> (inverse A)
ExceptionInfo throw+: {:exception "Cannot invert a non-square matrix."}  clatrix.core/i (core.clj:1033)
user> (def A (cl/matrix [[2 0] [2 0]]))
#'user/A
user> (M/* (inverse A) A)
LapackException LAPACK DGESV: Linear equation cannot be solved because the matrix was singular.  org.jblas.SimpleBlas.gesv (SimpleBlas.java:274)

使用矩阵进行插值

让我们尝试一个示例来演示我们如何使用矩阵。这个例子使用矩阵在给定的一组点之间插值曲线。假设我们有一个代表某些数据的给定点集。目标是追踪点之间的平滑线,以产生一个估计数据形状的曲线。尽管这个例子中的数学公式可能看起来很复杂,但我们应该知道,这种技术实际上只是线性回归模型正则化的一种形式,被称为 Tichonov 正则化。现在,我们将专注于如何在这个技术中使用矩阵,我们将在第二章 理解线性回归 中深入探讨正则化。

我们首先定义一个插值矩阵 L,它可以用来确定给定数据点的估计曲线。它本质上是一个向量 [-1, 2, -1] 在矩阵的列中斜向移动。这种矩阵被称为 带宽矩阵

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_56.jpg

我们可以使用以下 compute-matrix 函数简洁地定义矩阵 L。注意,对于给定的大小 n,我们生成一个大小如 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_57.jpg 的矩阵:

(defn lmatrix [n]
  (compute-matrix :clatrix [n (+ n 2)]
                  (fn [i j] ({0 -1, 1 2, 2 -1} (- j i) 0))))

在前面的例子中,匿名闭包使用一个映射来决定指定行和列索引处的元素值。例如,行索引 2 和列索引 3 的元素是 2,因为 (- j i)1,映射中的键 1 的值是 2。我们可以通过以下方式在 REPL 中验证生成的矩阵与 lmatrix 矩阵具有相似的结构:

user> (pm (lmatrix 4))
[[-1.000 2.000 -1.000  0.000  0.000  0.000]
[ 0.000 -1.000  2.000 -1.000  0.000  0.000]
[ 0.000  0.000 -1.000  2.000 -1.000  0.000]
[ 0.000  0.000  0.000 -1.000  2.000 -1.000]]
nil

接下来,我们定义如何表示我们打算进行插值的数值点。每个点都有一个观测值 x,它被传递给某个函数以产生另一个观测值 y。在这个例子中,我们简单地为一个 x 选择一个随机值,并为 y 选择另一个随机值。我们重复执行此操作以产生数据点。

为了表示与兼容大小的 L 矩阵一起的数据点,我们定义了一个名为 problem 的简单函数,该函数返回问题定义的映射。这包括 L 矩阵、x 的观测值、x 的隐藏值(对于这些值,我们必须估计 y 的值以创建曲线),以及 y 的观测值。

(defn problem
  "Return a map of the problem setup for a
  given matrix size, number of observed values 
  and regularization parameter"
  [n n-observed lambda]
  (let [i (shuffle (range n))]
    {:L (M/* (lmatrix n) lambda)
     :observed (take n-observed i)
     :hidden (drop n-observed i)
     :observed-values (matrix :clatrix
                              (repeatedly n-observed rand))}))

函数的前两个参数是 L 矩阵中的行数 n 和观测的 x 值数 n-observed。函数接受第三个参数 lambda,这实际上是我们的模型的正则化参数。此参数决定了估计曲线的准确性,我们将在后面的章节中更详细地研究它与此模型的相关性。在前面的函数返回的映射中,xy 的观测值具有键 :observed:observed-values,而 x 的隐藏值具有键 :hidden。同样,键 :L 映射到兼容大小的 L 矩阵。

现在我们已经定义了我们的问题(或模型),我们可以在给定的点上绘制一条平滑的曲线。这里的“平滑”是指曲线上的每个点都是其直接邻居的平均值,以及一些高斯噪声。因此,所有这些噪声曲线上的点都服从高斯分布,其中所有值都围绕某个平均值散布,并带有由某个标准差指定的范围。

如果我们将矩阵 L 分别在观测点和隐藏点上进行 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_58.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_59.jpg 的划分,我们可以定义一个公式来确定曲线如下。以下方程可能看起来有些令人畏惧,但如前所述,我们将在接下来的章节中研究这个方程背后的推理。曲线可以用一个矩阵来表示,该矩阵可以通过以下方式计算,使用矩阵 L

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_60.jpg

我们使用原始观测的 y 值,即 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_62.jpg,来估计隐藏的 x 值,使用从插值矩阵 L 计算出的两个矩阵。这两个矩阵仅使用矩阵的转置和逆函数来计算。由于此方程右侧的所有值要么是矩阵要么是向量,我们使用矩阵乘法来找到这些值的乘积。

之前的方程可以使用我们之前探索过的函数来实现。实际上,代码只包含这个方程,以作为我们之前定义的 problem 函数返回的映射的前缀表达式。我们现在定义以下函数来解决 problem 函数返回的问题:

(defn solve
  "Return a map containing the approximated value 
y of each hidden point x"
  [{:keys [L observed hidden observed-values] :as problem}]
  (let [nc  (column-count L)
        nr  (row-count L)
        L1  (cl/get L (range nr) hidden)
        L2  (cl/get L (range nr) observed)
        l11 (M/* (transpose L1) L1)
        l12 (M/* (transpose L1) L2)]
    (assoc problem :hidden-values
      (M/* -1 (inverse l11) l12 observed-values))))

前面的函数计算了 y 的估计值,并简单地使用 assoc 函数将它们添加到原始映射中,键为 :hidden-values

要在心理上可视化曲线的计算值相当困难,因此我们现在将使用 Incanter 库(github.com/liebke/incanter)来绘制估计曲线和原始点。这个库本质上提供了一个简单且符合习惯的 API 来创建和查看各种类型的图表和图表。

注意

可以通过在 project.clj 文件中添加以下依赖项将 Incanter 库添加到 Leiningen 项目中:

[incanter "1.5.4"]

对于即将到来的示例,命名空间声明应类似于以下内容:

(ns my-namespace
  (:use [incanter.charts :only [xy-plot add-points]]
        [incanter.core   :only [view]])
  (:require [clojure.core.matrix.operators :as M]
            [clatrix.core :as cl]))

现在,我们定义一个简单的函数,它将使用 Incanter 库中的函数,如 xy-plotview,来绘制给定数据的图表:

(defn plot-points
  "Plots sample points of a solution s"
  [s]
  (let [X (concat (:hidden s) (:observed s))
        Y (concat (:hidden-values s) (:observed-values s))]
    (view
     (add-points
      (xy-plot X Y) (:observed s) (:observed-values s)))))

由于这是我们第一次接触 Incanter 库,让我们讨论一些用于实现 plot-points 的函数。我们首先将所有 x 轴上的值绑定到 X,将所有 y 轴上的值绑定到 Y。然后,我们使用 xy-plot 函数将点绘制为曲线,该函数接受两个参数,用于在 xy 轴上绘制,并返回一个图表或图表。接下来,我们使用 add-points 函数将原始观察到的点添加到图表中。add-points 函数需要三个参数:原始图表、x 轴组件的所有值的向量,以及 y 轴组件的所有值的向量。此函数也返回一个类似于 xy-plot 函数的图表,我们可以使用 view 函数查看此图表。请注意,我们也可以等效地使用线程宏(->)来组合 xy-plotadd-pointsview 函数。

现在,我们可以直观地使用 plot-points 函数在随机数据上可视化估计曲线,如下面的函数所示:

(defn plot-rand-sample []
  (plot-points (solve (problem 150 10 30))))

当我们执行 plot-rand-sample 函数时,会显示以下值的图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_02.jpg

摘要

在本章中,我们通过 core.matrix 和 clatrix 库介绍了矩阵。以下是我们所涵盖的要点:

  • 我们已经讨论了如何通过 core.matrix 和 clatrix 来表示、打印和从矩阵中获取信息。我们还讨论了如何使用一些随机数据生成矩阵。

  • 我们已经讨论了一些矩阵的基本操作,例如相等、加法、乘法、转置和逆运算。

  • 我们还介绍了一个多功能的 Incanter 库,它用于通过矩阵使用示例来可视化数据图表。

接下来,我们将研究一些使用线性回归进行预测的基本技术。正如我们将看到的,其中一些技术实际上是基于简单的矩阵运算。线性回归实际上是一种监督学习类型,我们将在下一章中讨论。

第二章:理解线性回归

在本章中,我们开始探索机器学习模型和技术。机器学习的最终目标是泛化从某些经验样本数据中得出的事实。这被称为泛化,本质上是指使用这些推断事实以准确率在新的、未见过的数据上准确执行的能力。机器学习的两大类别是监督学习和无监督学习。监督学习这个术语用来描述机器学习中的任务,即从某些标记数据中制定理解或模型。通过标记,我们指的是样本数据与某些观察到的值相关联。在基本意义上,模型是数据的统计描述以及数据如何在不同参数上变化。监督机器学习技术用来创建模型的初始数据被称为模型的训练数据。另一方面,无监督学习技术通过在未标记数据中寻找模式来估计模型。由于无监督学习技术使用的数据是未标记的,通常没有基于是或否的明确奖励系统来确定估计的模型是否准确和正确。

现在,我们将考察线性回归,这是一个有趣的预测模型。作为一种监督学习,回归模型是从某些数据中创建的,其中一些参数以某种方式组合产生几个目标值。该模型实际上描述了目标值与模型参数之间的关系,并在提供模型参数的值时可以用来预测目标值。

我们首先将研究单变量和多变量线性回归,然后描述可以用来从一些给定数据中制定机器学习模型的算法。我们将研究这些模型背后的推理,并同时展示如何在 Clojure 中实现这些算法来创建这些模型。

理解单变量线性回归

我们经常遇到需要从一些样本数据中创建近似模型的情况。然后,可以使用该模型在提供所需参数时预测更多此类数据。例如,我们可能想研究特定城市某一天降雨的频率,我们假设这取决于那一天的湿度。一个制定好的模型可以用来预测某一天降雨的可能性,如果我们知道那一天的湿度。我们从一些数据开始制定模型,首先通过一些参数和系数在这个数据上拟合一条直线(即方程)。这种类型的模型被称为线性回归模型。如果我们假设样本数据只有一个维度,我们可以将线性回归视为在样本数据上拟合一条直线,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_01.jpg

线性回归模型简单地说是一个线性方程,它表示模型的回归量因变量。根据可用数据,建立的回归模型可以有一个或多个参数,这些模型的参数也被称为回归变量特征独立变量。我们将首先探讨具有单个独立变量的线性回归模型。

使用单变量线性回归的一个示例问题可能是预测特定一天降雨的概率,这取决于那一天的湿度。这些训练数据可以用以下表格形式表示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_85.jpg

对于单变量线性模型,因变量必须相对于单个参数变化。因此,我们的样本数据本质上由两个向量组成,即一个用于依赖参数 Y 的值,另一个用于独立变量 X 的值。这两个向量长度相同。这些数据可以用以下两种形式正式表示为两个向量,或单列矩阵:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_03.jpg

让我们快速定义以下两个矩阵,Clojure 中的 XY,以表示一些样本数据:

(def X (cl/matrix [8.401 14.475 13.396 12.127 5.044
                      8.339 15.692 17.108 9.253 12.029]))

(def Y (cl/matrix [-1.57 2.32  0.424  0.814 -2.3
           0.01 1.954 2.296 -0.635 0.328]))

在这里,我们定义了 10 个数据点;这些点可以用以下 Incanter scatter-plot函数轻松地绘制在散点图上:

(def linear-samp-scatter
  (scatter-plot X Y))

(defn plot-scatter []
  (view linear-samp-scatter))

(plot-scatter)

上述代码显示了以下数据点的散点图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_04.jpg

之前的散点图是我们定义在 XY 中的 10 个数据点的简单表示。

注意

scatter-plot 函数可以在 Incanter 库的 charts 命名空间中找到。使用此函数的文件命名空间声明应类似于以下声明:

(ns my-namespace
  (:use [incanter.charts :only [scatter-plot]]))

现在我们已经对我们的数据有了可视化,让我们在给定的数据点上估计一个线性模型。我们可以使用 Incanter 库中的linear-model函数生成任何数据的线性模型。这个函数返回一个映射,描述了构建的模型,以及关于这个模型的大量有用数据。首先,我们可以使用这个映射中的:fitted键值对在我们的先前散点图上绘制线性模型。我们首先从返回的映射中获取:fitted键的值,并使用add-lines函数将其添加到散点图中;这在上面的代码中有所展示:

(def samp-linear-model
  (linear-model Y X))
(defn plot-model []
  (view (add-lines samp-scatter-plot 
          X (:fitted linear-samp-scatter))))

(plot-model)

这段代码生成了以下自解释的线性模型图,该图覆盖了我们之前定义的散点图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_06.jpg

之前的图表描绘了线性模型samp-linear-model,作为在XY中定义的 10 个数据点上绘制的直线。

注意

linear-model函数可以在 Incanter 库的stats命名空间中找到。使用linear-model的文件命名空间声明应类似于以下声明:

(ns my-namespace
  (:use [incanter.stats :only [linear-model]]))

好吧,看起来 Incanter 的linear-model函数为我们做了大部分工作。本质上,这个函数通过使用普通最小二乘法OLS)曲线拟合算法来创建我们数据的线性模型。我们很快就会深入了解这个算法的细节,但首先让我们了解曲线是如何精确地拟合到一些给定数据上的。

让我们先定义一条直线如何表示。在坐标几何学中,一条直线简单地是一个独立变量x的函数,它有一个给定的斜率m和截距c。直线的函数y可以正式写成https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_01.jpg。直线的斜率表示当x的值变化时,y的值变化了多少。这个方程的截距就是直线与图表的y轴相交的地方。请注意,方程yY不同,Y实际上代表了我们提供的方程的值。

类似于从坐标几何学中定义的直线,我们使用我们对矩阵XY的定义,正式地使用单变量线性回归模型进行定义,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_09.jpg

由于我们可以使用相同的方程来定义具有多个变量的线性模型,因此这个单变量线性模型的定义实际上非常灵活;我们将在本章后面看到这一点。在前面的定义中,术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg是一个系数,它表示y相对于x的线性尺度。从几何学的角度来看,它就是拟合给定数据矩阵XY的直线的斜率。由于X是一个矩阵或向量,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg也可以被视为矩阵X的缩放因子。

此外,术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg是另一个系数,它解释了当x为零时y的值。换句话说,它是方程的y截距。所构建模型的系数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg被称为线性模型的回归系数效应,而系数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg被称为模型的误差项偏差。一个模型甚至可能有多个回归系数,正如我们将在本章后面看到的。结果证明,误差https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg实际上只是另一个回归系数,并且可以传统地与其他模型的效应一起提及。有趣的是,这个误差决定了数据的一般散点或方差。

使用我们之前示例中linear-model函数返回的映射,我们可以轻松地检查生成的模型的系数。返回的映射有一个:coefs键,它映射到一个包含模型系数的向量。按照惯例,误差项也包含在这个向量中,简单地作为另一个系数:

user> (:coefs samp-linear-model)
[-4.1707801647266045 0.39139682427040384]

现在我们已经定义了数据上的线性模型。很明显,并非所有点都会落在表示所构建模型的线条上。每个数据点在y轴上与线性模型的绘图都有一定的偏差,这种偏差可以是正的也可以是负的。为了表示模型与给定数据之间的整体偏差,我们使用残差平方和均方误差均方根误差函数。这三个函数的值代表了对所构建模型中误差量的标量度量。

术语 误差残差 之间的区别在于,误差是衡量观察值与其预期值差异的量度,而残差是对不可观察的统计误差的估计,这简单地没有被我们使用的统计模型所建模或理解。我们可以这样说,在观察值集中,一个观察值与所有值的平均值之间的差异是一个残差。构建模型中的残差数量必须等于样本数据中依赖变量的观察值数量。

我们可以使用 :residuals 关键字从由 linear-model 函数生成的线性模型中获取残差,如下面的代码所示:

user> (:residuals samp-linear-model)
[-0.6873445559690581 0.8253111334125092 -0.6483716931997257 0.2383108767994172 -0.10342541689331242 0.9169220471357067 -0.01701880172457293 -0.22923670489146497 -0.08581465024744239 -0.20933223442208365]

预测均方误差SSE)简单地是构建模型中误差的总和。注意,在以下方程中,误差项 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_12.jpg 的符号并不重要,因为我们平方了这个差异值;因此,它总是产生一个正值。SSE 也被称为 残差平方和RSS)。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_13.jpg

linear-model 函数还计算构建模型的 SSE,并且可以使用 :sse 关键字检索此值;以下代码行展示了这一点:

user> (:sse samp-linear-model)
2.5862250345284887

均方误差MSE)衡量了在构建的模型中误差的平均幅度,不考虑误差的方向。我们可以通过平方所有给定依赖变量的值与其在构建的线性模型中的对应预测值的差异,并计算这些平方误差的平均值来计算这个值。MSE 也被称为模型的 均方预测误差。如果一个构建的模型的 MSE 为零,那么我们可以说该模型完美地拟合了给定的数据。当然,这在实际数据中是几乎不可能的,尽管在理论上我们可以找到一组产生零 MSE 的值。

对于依赖变量的给定的一组 N 个值 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_14.jpg 和从构建的模型中计算出的估计值 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_15.jpg,我们可以正式表示构建模型的 MSE 函数 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_16.jpg 如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_17.jpg

均方根误差RMSE)或 均方根偏差简单地是 MSE 的平方根,常用于衡量构建的线性模型的偏差。RMSE 对较大误差有偏,因此是尺度相关的。这意味着当不希望有较大误差时,RMSE 特别有用。

我们可以如下正式定义一个公式的模型的均方根误差(RMSE):

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_18.jpg

另一个衡量公式的线性模型准确度的指标是确定系数,表示为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_19.jpg。确定系数表示公式的模型与给定样本数据拟合得有多好,其定义如下。该系数是根据样本数据中观察值的平均值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_20.jpg、SSE 和总误差和https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_21.jpg来定义的。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_22.jpg

我们可以通过使用linear-model函数生成的模型,并使用:r-square关键字来检索https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_19.jpg的计算值,如下所示:

user> (:r-square samp-linear-model)
0.8837893226172282

为了制定一个最适合样本数据的模型,我们应该努力最小化之前描述的值。对于某些给定数据,我们可以制定几个模型,并计算每个模型的总体误差。然后,可以使用这个计算出的误差来确定哪个公式的模型最适合数据,从而选择给定数据的最佳线性模型。

根据公式的均方误差(MSE),模型被认为有一个成本函数。在数据上拟合线性模型的问题等价于最小化公式的线性模型成本函数的问题。成本函数,表示为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_23.jpg,可以简单地看作是公式模型参数的函数。通常,这个成本函数转化为模型的均方误差。由于 RMSE 随模型的公式参数变化,以下模型的成本函数是这些参数的函数:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_24.jpg

这将我们引向以下关于在数据上拟合线性回归模型问题的正式定义,对于线性模型的估计效应https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_25.jpg

该定义指出,我们可以通过确定这些参数的值来估计一个线性模型,这些参数由https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg表示,在这些参数下,成本函数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_23.jpg取最小可能值,理想情况下为零。

注意

在前面的公式中,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_26.jpg表达式代表成本函数的 N 维欧几里得空间的标准范数。通过“范数”一词,我们指的是在 N 维空间中只有正值的功能。

让我们可视化构建的模型成本函数的欧几里得空间如何随模型参数的变化而变化。为此,我们假设https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg参数,它代表常数误差为零。线性模型在参数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg上的成本函数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_27.jpg的图将理想地呈现为抛物线形状,类似于以下图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_28.jpg

对于单个参数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg,我们可以绘制前面的二维图表。同样,对于两个参数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg,构建的模型,将产生三维图表。此图表呈现为碗形或具有凸表面的形状,如图所示。此外,我们可以将此推广到构建模型的 N 个参数,并生成https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_30.jpg维度的图表。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_31.jpg

理解梯度下降

梯度下降算法是构建线性模型的最简单技术之一,尽管它不是最有效的方法,可以使得成本函数或模型误差尽可能小。该算法本质上寻找构建的线性模型成本函数的局部最小值。

如我们之前所述,单变量线性回归模型成本函数的三维图将呈现为凸或碗形的表面,具有全局最小值。通过“最小值”,我们指的是成本函数在图表表面的这一点上具有可能的最小值。梯度下降算法本质上从表面的任何一点开始,执行一系列步骤来接近表面的局部最小值。

这个过程可以想象成把一个球扔进山谷或两个相邻的山丘之间,结果球会慢慢滚向海平面以上最低的点。算法会重复进行,直到从表面当前点的明显成本函数值收敛到零,这比喻地意味着滚下山的球停止了,正如我们之前描述的那样。

当然,如果图表的表面上存在多个局部最小值,梯度下降可能根本不起作用。然而,对于适当缩放的单一变量线性回归模型,图表的表面总是有一个唯一的全局最小值,正如我们之前所展示的。因此,在这种情况下,我们仍然可以使用梯度下降算法来找到图表表面的全局最小值。

这个算法的精髓是从表面上某个点开始,然后朝着最低点迈出几步。我们可以用以下等式正式表示这一点:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_32.jpg

在这里,我们从成本函数 J 的图表上表示为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_33.jpg 的点开始,并逐步减去成本函数一阶偏导数 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_34.jpg 的乘积,该偏导数是根据公式的模型参数导出的。这意味着我们缓慢地向表面下方移动,朝着局部最小值前进,直到我们无法在表面上找到更低的点。术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_35.jpg 决定了我们朝着局部最小值迈出的步子有多大,被称为梯度下降算法的 步长。我们重复这个迭代过程,直到 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_36.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_33.jpg 之间的差异收敛到零,或者至少减少到接近零的阈值值。

下图展示了向成本函数图表表面局部最小值下降的过程:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_37.jpg

前面的插图是图表表面的等高线图,其中圆形线条连接了等高点的位置。我们从点 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_39.jpg 开始,执行一次梯度下降算法的迭代,将表面下降到点 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_40.jpg。我们重复这个过程,直到达到相对于初始起始点 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_39.jpg 的表面局部最小值。请注意,通过每次迭代,步长的大小都会减小,因为当接近局部最小值时,该表面的切线斜率也趋于零。

对于误差常数为零的单变量线性回归模型https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg,我们可以简化梯度下降算法的偏导数组件https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_34.jpg。当模型只有一个参数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg时,一阶偏导数简单地是该点在图表表面切线的斜率。因此,我们计算这条切线的斜率,并沿着这个斜率方向迈出一步,以便到达一个高于y轴的点。这如下公式所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_41.jpg

我们可以将这个简化的梯度下降算法实现如下:

(def gradient-descent-precision 0.001)

(defn gradient-descent
  "Find the local minimum of the cost function's plot"
  [F' x-start step]
  (loop [x-old x-start]
    (let [x-new (- x-old
                   (* step (F' x-old)))
          dx (- x-new x-old)]
      (if (< dx gradient-descent-precision)
        x-new
        (recur x-new)))))

在前面的函数中,我们从x-start点开始,递归地应用梯度下降算法,直到x-new值收敛。请注意,这个过程是通过使用loop形式实现的尾递归函数。

使用偏导数,我们可以正式表达如何使用梯度下降算法计算参数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_42.jpg

理解多元线性回归

多元线性回归模型可以包含多个变量或特征,这与我们之前研究过的单变量线性回归模型不同。有趣的是,单变量线性模型的定义本身可以通过矩阵扩展来应用于多个变量。

我们可以将我们之前用于预测特定一天降雨概率的例子扩展到包含更多独立变量的多元变量模型中,例如最小和最大温度。因此,多元线性回归模型的训练数据将类似于以下插图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_43.jpg

对于多元线性回归模型,训练数据由两个矩阵定义,XY。在这里,X是一个https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_44.jpg矩阵,其中P是模型中的独立变量数量。矩阵Y是一个长度为N的向量,就像在单变量线性模型中一样。该模型如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_45.jpg

对于以下 Clojure 中的多元线性回归示例,我们不会通过代码生成样本数据,而是使用 Incanter 库中的样本数据。我们可以使用 Incanter 库的get-dataset函数获取任何数据集。

注意

在即将到来的示例中,可以从 Incanter 库中导入selto-matrixget-dataset函数到我们的命名空间中,如下所示:

(ns my-namespace
  (:use [incanter.datasets :only [get-dataset]]
        [incanter.core :only [sel to-matrix]]))

我们可以通过使用带有:iris关键字参数的get-dataset函数来获取Iris数据集;如下所示:

(def iris
  (to-matrix (get-dataset :iris)))

(def X (sel iris :cols (range 1 5)))
(def Y (sel iris :cols 0))

我们首先使用to-matrixget-dataset函数将变量iris定义为矩阵,然后定义两个矩阵XY。在这里,Y实际上是一个包含 150 个值的向量,或者是一个大小为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_46.jpg的矩阵,而X是一个大小为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_47.jpg的矩阵。因此,X可以用来表示四个独立变量的值,而Y表示因变量的值。请注意,sel函数用于从iris矩阵中选择一组列。实际上,我们可以从iris数据矩阵中选择更多的此类列,但为了简化起见,在下面的例子中我们只使用四个。

注意

在前面的代码示例中,我们使用的数据集是Iris数据集,该数据集可在 Incanter 库中找到。这个数据集具有相当大的历史意义,因为它被罗纳德·费舍尔爵士首次用于开发线性判别分析LDA)方法进行分类(更多信息请参阅“Iris 中的物种问题”)。该数据集包含三种不同的 Iris 植物物种的 50 个样本,即SetosaVersicolorVirginica。在每个样本中测量这些物种的花朵的四个特征,即花瓣宽度、花瓣长度、萼片宽度和萼片长度。请注意,在本书的后续内容中,我们将多次遇到这个数据集。

有趣的是,linear-model函数接受一个多列矩阵,因此我们可以使用此函数来拟合单变量和多变量数据的线性回归模型,如下所示:

(def iris-linear-model
  (linear-model Y X))
(defn plot-iris-linear-model []
  (let [x (range -100 100)
        y (:fitted iris-linear-model)]
    (view (xy-plot x y :x-label "X" :y-label "Y"))))

(plot-iris-linear-model)

在前面的代码示例中,我们使用xy-plot函数绘制线性模型,同时提供可选参数来指定定义的图中轴的标签。我们还通过使用range函数生成一个向量来指定x轴的范围。plot-iris-linear-model函数生成了以下图形:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_48.jpg

尽管从前面示例生成的曲线看起来没有明确的形状,我们仍然可以使用这个生成的模型通过为模型提供独立变量的值来估计或预测因变量的值。为了做到这一点,我们必须首先定义具有多个特征的线性回归模型中因变量和自变量之间的关系。

具有 P 个独立变量的线性回归模型产生https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_50.jpg个回归系数,因为我们除了包括模型的其他系数外,还定义了一个额外的变量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_39.jpg,该变量总是1

linear-model函数与命题一致,即所构建模型中的系数数量P总是比样本数据中的自变量总数N多一个;这在下述代码中显示:

user> (= (count (:coefs iris-linear-model)) 
         (+ 1 (column-count X)))
true

我们正式表达多元回归模型中因变量和自变量之间的关系如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_52.jpg

由于变量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_39.jpg在先前的方程中总是1,因此值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_53.jpg与单变量线性模型定义中的误差常数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_11.jpg类似。

我们可以定义一个向量来表示前述方程中所有的系数,即https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg。这个向量被称为所构建回归模型的参数向量。此外,模型的独立变量也可以用一个向量表示。因此,我们可以定义回归变量Y为参数向量的转置与模型独立变量向量的乘积:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_54.jpg

多项式函数也可以通过将多项式方程中的每个高阶变量替换为一个单一变量来简化为标准形式。例如,考虑以下多项式方程:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_55.jpg

我们可以用https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_56.jpg替换https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_57.jpg,将方程简化为多元线性回归模型的标准形式。

这将我们引向以下具有多个变量的线性模型的成本函数的正式定义,这仅仅是单个变量线性模型成本函数定义的扩展:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_58.jpg

注意,在先前的定义中,我们可以将模型的各个系数与参数向量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg互换使用。

类似于我们定义的问题,即对给定数据拟合单变量模型,我们可以将构建多变量线性模型的问题定义为最小化先前成本函数的问题:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_59.jpg

多变量梯度下降

我们可以将梯度下降算法应用于寻找具有多个变量的模型局部最小值。当然,由于模型中有多个系数,我们必须对所有的这些系数应用算法,而不是像单变量回归模型中只对两个系数应用算法。

因此,梯度下降算法可以用来找到多变量线性回归模型参数向量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg中所有系数的值,并且形式上定义为以下内容:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_60.jpg

在前面的定义中,术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_61.jpg简单地指的是构建模型中独立变量的样本值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_62.jpg。此外,变量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_63.jpg始终为1。因此,这个定义可以应用于与之前定义的单变量线性回归模型的梯度下降算法相对应的两个系数。

如我们之前所见,梯度下降算法可以应用于具有单变量和多变量的线性回归模型。然而,对于某些模型,梯度下降算法实际上可能需要很多迭代,或者说很多时间,才能收敛到模型系数的估计值。有时,算法也可能发散,因此在这种情况下我们无法计算出模型的系数。让我们来考察一些影响该算法行为和性能的因素:

理解普通最小二乘法

估计线性回归模型参数向量的另一种技术是普通最小二乘法OLS)。OLS 方法本质上是通过最小化线性回归模型中的平方误差和来工作的。

线性回归模型的预测平方误差和(SSE)可以用模型的实际值和期望值来定义如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_68.jpg

前面的 SSE 定义可以用矩阵乘法进行因式分解如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_69.jpg

我们可以通过使用全局最小值的定义来解前面的方程,以求解估计的参数向量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg。由于这个方程是二次方程的形式,并且项https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_70.jpg总是大于零,因此成本函数表面的全局最小值可以定义为在该点切线斜率变化率为零的点。此外,该图是线性模型参数的函数,因此表面图的方程应该对估计的参数向量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg进行微分。因此,我们可以如下求解所构建模型的最佳参数向量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_71.jpg

前面推导中的最后一个等式给出了最优参数向量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_10.jpg的定义,它正式表达如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_72.jpg

我们可以通过使用 core.matrix 库的transposeinverse函数以及 Incanter 库的bind-columns函数来实现先前的参数向量定义的 OLS 方法:

(defn linear-model-ols
  "Estimates the coefficients of a multi-var linear
  regression model using Ordinary Least Squares (OLS) method"
  [MX MY]
  (let [X (bind-columns (repeat (row-count MX) 1) MX)
        Xt (cl/matrix (transpose X))
        Xt-X (cl/* Xt X)]
    (cl/* (inverse Xt-X) Xt MY)))

(def ols-linear-model
  (linear-model-ols X Y))

(def ols-linear-model-coefs
  (cl/as-vec ols-linear-model))

在这里,我们首先添加一个列,其中每个元素都是1,因为矩阵MX的第一列使用bind-columns函数。我们添加的额外列代表独立变量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_39.jpg,其值始终为1。然后我们使用transposeinverse函数计算矩阵MXMY中数据的线性回归模型的估计系数。

注意

对于当前示例,可以将 Incanter 库中的bind-columns函数导入我们的命名空间,如下所示:

(ns my-namespace
  (:use [incanter.core :only [bind-columns]]))

可以将先前定义的函数应用于我们先前定义的矩阵(XY),如下所示:

(def ols-linear-model
  (linear-model-ols X Y))

(def ols-linear-model-coefs
  (cl/as-vec ols-linear-model))

在前面的代码中,ols-linear-model-coefs只是一个变量,而ols-linear-model是一个单列矩阵,它被表示为一个向量。我们使用 clatrix 库中的as-vec函数执行此转换。

实际上,我们可以验证由ols-linear-model函数估计的系数实际上与 Incanter 库的linear-model函数生成的系数相等,如下所示:

user> (cl/as-vec (ols-linear-model X Y))
[1.851198344985435 0.6252788163253274 0.7429244752213087 -0.4044785456588674 -0.22635635488532463]
user> (:coefs iris-linear-model)
[1.851198344985515 0.6252788163253129 0.7429244752213329 -0.40447854565877606 -0.22635635488543926]
user> (every? #(< % 0.0001) 
                      (map - 
                         ols-linear-model-coefs 
                         (:coefs iris-linear-model)))
true

在前面代码示例的最后表达式中,我们找到了由ols-linear-model函数产生的系数之间的差异,由linear-model函数产生的差异,并检查这些差异中的每一个是否小于0.0001

使用线性回归进行预测

一旦我们确定了线性回归模型的系数,我们可以使用这些系数来预测模型因变量的值。预测值由线性回归模型定义为每个系数与其对应自变量值的乘积之和。

我们可以轻松定义以下通用函数,当提供系数和自变量的值时,它预测给定公式的线性回归模型中因变量的值:

(defn predict [coefs X]
  {:pre [(= (count coefs)
            (+ 1 (count X)))]}
  (let [X-with-1 (conj X 1)
        products (map * coefs X-with-1)]
    (reduce + products)))

在前面的函数中,我们使用一个先决条件来断言系数的数量和自变量的值。这个函数期望自变量的值数量比模型的系数数量少一个,因为我们添加了一个额外的参数来表示一个值始终为1的自变量。然后,该函数使用map函数计算相应的系数和自变量值的乘积,然后使用reduce函数计算这些乘积项的总和。

理解正则化

线性回归使用线性方程估计一些给定的训练数据;这种解决方案可能并不总是给定数据的最佳拟合。当然,这很大程度上取决于我们试图建模的问题。正则化是一种常用的技术,用于提供更好的数据拟合。通常,一个给定的模型通过减少模型中一些自变量的影响来进行正则化。或者,我们也可以将其建模为更高阶的多项式。正则化并不局限于线性回归,大多数机器学习算法都使用某种形式的正则化,以便从给定的训练数据中创建更精确的模型。

当一个模型未能估计出与训练数据中依赖变量的观察值接近的值时,我们称其为欠拟合高偏差。另一方面,当一个估计模型完美地拟合数据,但不够通用以至于不能用于预测时,我们也可以称之为过拟合高方差。过拟合模型通常描述的是训练数据中的随机误差或噪声,而不是模型中依赖变量和自变量之间的基本关系。最佳拟合回归模型通常位于欠拟合和过拟合模型之间,可以通过正则化过程获得。

对于欠拟合或过拟合模型的正则化,常用的方法是Tikhonov 正则化。在统计学中,这种方法也称为岭回归。我们可以将 Tikhonov 正则化的通用形式描述如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_73.jpg

假设A代表从自变量向量x到依赖变量y的映射。值A类似于回归模型的参数向量。向量x与依赖变量的观察值之间的关系,用b表示,可以表达如下。

一个欠拟合模型与实际数据存在显著的误差,或者说偏差。我们应该努力最小化这个误差。这可以形式化地表达如下,并且基于估计模型的残差之和:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_74.jpg

Tikhonov 正则化向先前的方程添加了一个惩罚最小二乘项,以防止过拟合,其形式如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_75.jpg

先前的方程中的术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_76.jpg被称为正则化矩阵。在 Tikhonov 正则化的最简单形式中,这个矩阵取值为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_77.jpg,其中https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_78.jpg是一个常数。尽管将此方程应用于回归模型超出了本书的范围,但我们可以使用 Tikhonov 正则化来生成具有以下成本函数的线性回归模型:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_79.jpg

在先前的方程中,术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_80.jpg被称为模型的正则化参数。此值必须选择适当,因为此参数的较大值可能会产生欠拟合模型。

使用先前定义的成本函数,我们可以应用梯度下降来确定参数向量,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_81.jpg

我们也可以将正则化应用于确定参数向量的 OLS 方法,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_82.jpg

在先前的方程中,L被称为平滑矩阵,可以采用以下形式。请注意,我们在第一章中使用了L定义的后一种形式,即矩阵操作

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_01_0100.jpg

有趣的是,当先前的方程中的正则化参数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_02_80.jpg0时,正则化解简化为使用 OLS 方法得到的原始解。

概述

在本章中,我们学习了线性回归以及一些可以用来从样本数据中构建最优线性回归模型的算法。以下是我们所涵盖的一些其他要点:

  • 我们讨论了单变量和多变量的线性回归

  • 我们实现了梯度下降算法来构建一个单变量线性回归模型

  • 我们实现了普通最小二乘法OLS)来找到最优线性回归模型的系数

  • 我们介绍了正则化及其在线性回归中的应用

在下一章中,我们将研究机器学习的另一个领域,即分类。分类也是一种回归形式,用于将数据分类到不同的类别或组中。

第三章. 数据分类

在本章中,我们将探讨分类,这是监督学习中的另一个有趣问题。我们将检查一些用于分类数据的技术,并研究我们如何在 Clojure 中利用这些技术。

分类可以定义为根据一些经验训练数据识别观测数据的类别或类别的问题。训练数据将包含可观测特征或独立变量的值。它还将包含这些观测值的已知类别。在某种程度上,分类与回归相似,因为我们根据另一组值预测一个值。然而,对于分类,我们感兴趣的不仅仅是观测值的类别,而是基于给定的值集预测一个值。例如,如果我们从一个输出值范围在05的集合中训练一个线性回归模型,训练好的分类器可以预测一组输入值的输出值为10或*-1*。然而,在分类中,输出变量的预测值始终属于一组离散的值。

分类模型的独立变量也被称为模型的解释变量,而因变量也被称为观测值的结果类别类别。分类模型的结果始终是离散值,即来自一组预定的值。这是分类与回归之间的一项主要区别,因为在回归建模中,我们预测的变量可以有一个连续的范围。请注意,在分类的上下文中,“类别”和“类别”这两个术语可以互换使用。

实现分类技术的算法被称为分类器。一个分类器可以正式定义为将一组值映射到类别或类别的函数。分类仍然是计算机科学中的一个活跃研究领域,今天在软件中使用了几个突出的分类器算法。分类有几个实际应用,例如数据挖掘、机器视觉、语音和手写识别、生物分类和地理统计学。

理解二分类和多分类

我们首先将研究一些关于数据分类的理论方面。与其他监督机器学习技术一样,目标是根据样本数据估计一个模型或分类器,并使用它来预测一组给定的结果。分类可以被视为确定一个函数,该函数将样本数据的特征映射到特定的类别。预测的类别是从一组预定的类别中选择出来的。因此,与回归类似,对给定独立变量的观测值进行分类的问题与确定给定训练数据的最优拟合函数是相似的。

在某些情况下,我们可能只对单个类别感兴趣,即观测值是否属于特定类别。这种分类形式被称为二进制分类,模型的输出变量可以是01的值。因此,我们可以说https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_01.jpg,其中y是分类模型的输出或因变量。当https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_03.jpg时,结果被认为是负的,反之,当https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_04.jpg时,结果被认为是正的。

从这个角度来看,当提供了模型独立变量的某些观测值时,我们必须能够确定正结果的概率。因此,给定样本数据的估计模型具有https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_05.jpg的概率,可以表示如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_06.jpg

在前面的方程中,参数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_07.jpg代表估计分类模型https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_08.jpg的独立变量,而项https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_09.jpg代表该模型的估计参数向量。

这种分类的一个例子是决定一封新邮件是否为垃圾邮件,这取决于发件人或邮件中的内容。另一个简单的二进制分类例子是根据某一天的观测湿度以及当天的最低和最高温度来确定该日降雨的可能性。这个例子中的训练数据可能类似于以下表格中的数据:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_10.jpg

我们可以使用来建模二进制分类的数学函数是sigmoidlogistic函数。如果特征X的输出有一个估计的参数向量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_12.jpg,我们可以定义正结果的估计概率Y(作为 sigmoid 函数)如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_13.jpg

为了可视化前面的方程,我们可以通过将Z替换为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_14.jpg来简化它,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_15.jpg

我们还可以使用其他几个函数来模拟数据。然而,二分类器的样本数据可以很容易地转换,使其可以用 S 型函数进行模拟。使用逻辑函数对分类问题进行建模的过程称为逻辑回归。前面方程中定义的简化 S 型函数产生了以下图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_16.jpg

注意,如果术语Z具有负值,则图表会反转,并且是前一个图表的镜像。我们可以通过以下图表可视化 S 型函数相对于术语Z的变化:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_18.jpg

在前面的图表中,展示了不同值下的 S 型函数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_20.jpg;其范围从*-55*。请注意,对于二维情况,术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_20.jpg是独立变量x的线性函数。有趣的是,对于https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_21.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_22.jpg,S 型函数看起来或多或少像一条直线。当https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_23.jpg时,该函数简化为一条直线,可以用常数y值(在这种情况下,方程https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_24.jpg)表示。

我们观察到,估计的输出Y始终在01之间,因为它代表给定观察值的正结果概率。此外,输出Y的范围不受术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_25.jpg符号的影响。因此,回顾起来,S 型函数是二分类的有效表示。

要使用逻辑函数从训练数据估计分类模型,我们可以将逻辑回归模型的成本函数定义为以下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_26.jpg

前面的方程本质上总结了模型输出变量实际值和预测值之间的差异,就像线性回归一样。然而,由于我们处理的是介于01之间的概率值,我们使用前面的对数函数来有效地衡量实际值和预测输出值之间的差异。请注意,术语N表示训练数据中的样本数量。我们可以将梯度下降应用于此成本函数,以确定一组观察值的局部最小值或预测类别。此方程可以正则化,产生以下成本函数:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_27.jpg

注意,在这个公式中,第二个求和项被添加为一个正则化项,就像我们在第二章中讨论的那样,理解线性回归。这个项基本上防止了估计模型在样本数据上的欠拟合和过拟合。注意,项 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_28.jpg 是正则化参数,必须根据我们希望模型有多准确来适当选择。

多类分类,这是分类的另一种形式,将分类结果预测为特定预定义值集中的值。因此,结果是从 k 个离散值中选择,即 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_29.jpg。该模型为每个可能的观察值类别产生 k 个概率。这使我们得到了多类分类的以下正式定义:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_30.jpg

因此,在多类分类中,我们预测 k 个不同的值,其中每个值表示输入值属于特定类别的概率。有趣的是,二分类可以被视为多类分类的一种特殊情况,其中只有两个可能的类别,即 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_31.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_01.jpg

作为多类分类的一个特殊情况,我们可以说,具有最大概率的类别是结果,或者简单地说,给定观察值集合的预测类别。这种多类分类的特殊化称为一对多分类。在这里,从给定的观察值集合中确定具有最大(或最小)发生概率的单个类别,而不是找到我们模型中所有可能类别的发生概率。因此,如果我们打算从特定类别集合中预测单个类别,我们可以定义结果 C 如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_32.jpg

例如,假设我们想要确定一个鱼包装厂的分类模型。在这种情况下,鱼被分为两个不同的类别。比如说,我们可以将鱼分类为海鲈鱼或三文鱼。我们可以通过选择足够大的鱼样本并分析它们在某些选定特征上的分布来为我们的模型创建一些训练数据。比如说,我们已经确定了两个特征来分类数据,即鱼的长度和皮肤的亮度。

第一特征,即鱼的长度的分布可以如下可视化:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_33.jpg

同样,样本数据中鱼皮光亮度的分布可以通过以下图来可视化:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_35.jpg

从前面的图中,我们可以看出,仅指定鱼的长度的信息不足以确定其类型。因此,这个特征在分类模型中的系数较小。相反,由于鱼皮的光亮度在确定鱼类型方面起着更大的作用,这个特征将在估计的分类模型的参数向量中具有更大的系数。

一旦我们建模了一个给定的分类问题,我们可以将训练数据划分为两个(或更多)集合。在向量空间中分割这两个集合的表面被称为所制定分类模型的决策边界。决策边界一侧的所有点属于一个类别,而另一侧的点属于另一个类别。一个明显的推论是,根据不同类别的数量,一个给定的分类模型可以有几个这样的决策边界。

我们现在可以将这两个特征结合起来训练我们的模型,这会产生两个鱼类别之间的估计决策边界。这个边界可以在训练数据的散点图上如下可视化:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_37.jpg

在前面的图中,我们通过使用直线来近似分类模型,因此,我们有效地将分类建模为线性函数。我们也可以将数据建模为多项式函数,因为它会产生更准确的分类模型。这样的模型产生的决策边界可以如下可视化:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_39.jpg

决策边界将样本数据划分为两个维度,如图中所示。当样本数据具有更多特征或维度时,决策边界将变得更加复杂,难以可视化。例如,对于三个特征,决策边界将是一个三维表面,如图中所示。请注意,为了清晰起见,未显示样本数据点。此外,假设图中绘制的两个特征在https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_41.jpg范围内变化,第三个特征在https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_42.jpg范围内变化。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_43.jpg

理解贝叶斯分类

我们现在将探讨用于分类数据的贝叶斯技术。贝叶斯 分类器本质上是一种基于贝叶斯条件概率定理构建的概率分类器。基于贝叶斯分类的模型假设样本数据具有高度独立的特征。这里的“独立”意味着模型中的每个特征可以独立于模型中的其他特征变化。换句话说,模型的特征是互斥的。因此,贝叶斯分类器假设特定特征的呈现与否完全独立于分类模型中其他特征的呈现与否。

术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_45.jpg 用于表示条件或特征 A 发生的概率。其值始终是介于 01 之间的分数值,包括两端。它也可以表示为百分比值。例如,概率 0.5 也可以写作 50%50 percent。假设我们想要找到从给定样本中发生特征 Ahttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_46.jpg 的概率。因此,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_46.jpg 的值越高,特征 A 发生的可能性就越高。我们可以将概率 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_46.jpg 正式表示如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_47.jpg

如果 AB 是我们分类模型中的两个条件或特征,那么我们使用术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_48.jpg 来表示当已知 B 已经发生时 A 的发生。这个值被称为 AB 条件下的 条件概率,术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_48.jpg 也可以读作 AB 条件下的概率。在术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_48.jpg 中,B 也被称为 A 的证据。在条件概率中,两个事件 AB 可能相互独立,也可能不独立。然而,如果 AB 确实是相互独立的条件,那么概率 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_48.jpg 等于 AB 单独发生概率的乘积。我们可以将这个公理表达如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_49.jpg

贝叶斯定理描述了条件概率https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_48.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_50.jpg与概率https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_46.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_51.jpg之间的关系。它使用以下等式正式表达:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_52.jpg

当然,为了使前面的关系成立,概率https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_46.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_51.jpg都必须大于0

让我们重新回顾一下我们之前描述的鱼包装厂的分类示例。问题是,我们需要根据鱼的外部特征来确定它是不是海鲈鱼还是三文鱼。现在,我们将使用贝叶斯分类器来实现这个问题的解决方案。然后,我们将使用贝叶斯定理来建模我们的数据。

假设每种鱼类都有三个独立且不同的特征,即皮肤的光泽度、长度和宽度。因此,我们的训练数据将类似于以下表格:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_53.jpg

为了简化实现,让我们使用 Clojure 符号来表示这些特征。我们需要首先生成以下数据:

(defn make-sea-bass []
  ;; sea bass are mostly long and light in color
  #{:sea-bass
    (if (< (rand) 0.2) :fat :thin)
    (if (< (rand) 0.7) :long :short)
    (if (< (rand) 0.8) :light :dark)})

(defn make-salmon []
  ;; salmon are mostly fat and dark
  #{:salmon
    (if (< (rand) 0.8) :fat :thin)
    (if (< (rand) 0.5) :long :short)
    (if (< (rand) 0.3) :light :dark)})

(defn make-sample-fish []
  (if (< (rand) 0.3) (make-sea-bass) (make-salmon)))

(def fish-training-data
  (for [i (range 10000)] (make-sample-fish)))

在这里,我们定义了两个函数,make-sea-bassmake-salmon,以创建一组符号来表示两种鱼类的类别。我们方便地使用:salmon:sea-bass关键字来表示这两个类别。同样,我们也可以使用 Clojure 关键字来枚举鱼的特征。在这个例子中,皮肤的光泽度是:light:dark,长度是:long:short,宽度是:fat:thin。此外,我们定义了make-sample-fish函数来随机创建一个由之前定义的特征集表示的鱼。

注意,我们定义这两种鱼类的类别,使得海鲈鱼通常较长且皮肤颜色较浅,而三文鱼通常较肥且颜色较深。此外,我们在make-sample-fish函数中生成了更多的三文鱼而不是海鲈鱼。我们只在数据中添加这种偏差,以提供更具说明性的结果,并鼓励读者尝试更真实的数据分布。在第二章中介绍的 Incanter 库中可用的Iris数据集,是可用于研究分类的真实世界数据集的例子。

现在,我们将实现以下函数来计算特定条件的概率:

(defn probability
  "Calculates the probability of a specific category
   given some attributes, depending on the training data."
  [attribute & {:keys
                [category prior-positive prior-negative data]
                :or {category nil
                     data fish-training-data}}]
  (let [by-category (if category
                      (filter category data)
                      data)
        positive (count (filter attribute by-category))
        negative (- (count by-category) positive)
        total (+ positive negative)]
    (/ positive negative)))

我们实际上通过出现次数的基本定义来实现概率。

在前一段代码中定义的probability函数需要一个参数来表示我们想要计算其发生概率的属性或条件。此外,该函数还接受几个可选参数,例如用于计算此值的fish-training-data序列,默认为我们之前定义的,以及一个类别,这可以简单地理解为另一个条件。参数categoryattribute实际上与https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_48.jpg中的条件AB相对应。probability函数通过使用filter函数过滤训练数据来确定条件的总积极发生次数。然后,它通过计算样本数据中由(count by-category)表示的积极值和总值的差来确定消极发生次数。最后,该函数返回条件积极发生次数与给定数据中总发生次数的比率。

让我们使用probability函数来了解我们的训练数据如下:

user> (probability :dark :category :salmon)
1204/1733
user> (probability :dark :category :sea-bass)
621/3068
user> (probability :light :category :salmon)
529/1733
user> (probability :light :category :sea-bass)
2447/3068

如前述代码所示,三文鱼外观为暗色的概率很高,具体为1204/1733。与海鲈鱼为暗色和三文鱼为亮色的概率相比,海鲈鱼为亮色和三文鱼为暗色的概率也较低。

假设我们对鱼的特征的观察值是它皮肤暗,长,且胖。在这些条件下,我们需要将鱼分类为海鲈鱼或三文鱼。从概率的角度来看,我们需要确定在鱼是暗、长、胖的情况下,鱼是三文鱼或海鲈鱼的概率。正式来说,这个概率由https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_55.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_56.jpg这两个术语表示,针对鱼的任何一个类别。如果我们计算这两个概率,我们可以选择这两个概率中较高的类别来确定鱼的类别。

使用贝叶斯定理,我们定义术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_55.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_56.jpg如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_57.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_58.jpg

术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_55.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_59.jpg可能有点令人困惑,但这两个术语之间的区别在于指定条件的出现顺序。术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_60.jpg表示深色、长而胖的鱼是三文鱼的概率,而术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_61.jpg表示三文鱼是深色、长而胖的鱼的概率。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_62.jpg概率可以从给定的训练数据中计算如下。由于假设鱼的三个特征是相互独立的,因此术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_62.jpg仅仅是每个单个特征发生概率的乘积。相互独立意味着这些特征的方差或分布不依赖于分类模型中的任何其他特征。

术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_62.jpg也称为给定类别的证据,在本例中是类别“三文鱼”。我们可以将https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_62.jpg概率表示为模型独立特征的概率乘积;如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_63.jpg

有趣的是,术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_64.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_65.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_66.jpg可以很容易地从训练数据和之前实现的probability函数中计算出来。同样,我们可以找到鱼是三文鱼或https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_67.jpg的概率。因此,在https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_60.jpg的定义中,唯一没有考虑到的术语是https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_68.jpg。实际上,我们可以使用概率中的一个简单技巧来完全避免计算这个术语。

由于鱼是深色、长而胖的,它可能是三文鱼或海鲈鱼。这两种鱼类的发生概率都是互补的,也就是说,它们共同解释了我们模型中可能出现的所有条件。换句话说,这两个概率的总和为1。因此,我们可以正式地表达术语,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_68.jpg,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_70.jpg

上述等式右侧的两个术语都可以从训练数据中确定,这些术语类似于 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_67.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_71.jpg 以及等等。因此,我们可以直接从我们的训练数据中计算出 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_60.jpg 概率。我们通过以下等式来表示这个概率:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_72.jpg

现在,让我们使用训练数据和之前定义的 probability 函数来实现前面的等式。首先,给定鱼的外观是深色、长和胖,鱼是鲑鱼的证据可以表示如下:

(defn evidence-of-salmon [& attrs]
  (let [attr-probs (map #(probability % :category :salmon) attrs)
        class-and-attr-prob (conj attr-probs
                                  (probability :salmon))]
    (float (apply * class-and-attr-prob))))

为了明确起见,我们实现一个函数来计算从给定训练数据中术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_73.jpg 的概率。术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_62.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_64.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_65.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_66.jpg 的等式将作为此实现的基线。

在前面的代码中,我们使用 probability 函数确定 i 的所有属性或条件的术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_67.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_74.jpg。然后,我们使用 apply* 函数的组合乘以所有这些术语。由于所有计算出的概率都是 probability 函数返回的比率,我们使用 float 函数将最终比率转换为浮点值。我们可以在 REPL 中尝试此函数如下:

user> (evidence-of-salmon :dark)
0.4816
user> (evidence-of-salmon :dark :long)
0.2396884
user> (evidence-of-salmon)
0.6932

如 REPL 输出所示,训练数据中所有鱼中有 48.16% 的鱼是皮肤较暗的鲑鱼。同样,所有鱼中有 23.96% 的鱼是深色长鲑鱼,而所有鱼中有 69.32% 的鱼是鲑鱼。(evidence-of-salmon :dark :long) 调用返回的值可以表示为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_75.jpg,同样,(evidence-of-salmon) 也返回 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_76.jpg

类似地,我们可以定义一个evidence-of-sea-bass函数,该函数根据鱼的一些观察到的特征来确定海鲈鱼出现的证据。由于我们只处理两个类别,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_77.jpg,我们可以在 REPL 中轻松验证这个等式。有趣的是,观察到一个小错误,但这个错误与训练数据无关。实际上,这个小错误是一个浮点数舍入错误,这是由于浮点数的限制造成的。在实践中,我们可以使用十进制或BigDecimal(来自java.lang)数据类型来避免这种情况,而不是使用浮点数。我们可以使用 REPL 中的evidence-of-sea-bassevidence-of-salmon函数来验证这一点,如下所示:

user> (+ (evidence-of-sea-bass) (evidence-of-salmon))
1.0000000298023224

我们可以将evidence-of-salmonevidence-of-sea-bass函数泛化,以便我们能够根据一些观察特征确定任何类别的概率;以下代码展示了这一点:

(defn evidence-of-category-with-attrs
  [category & attrs]
  (let [attr-probs (map #(probability % :category category) attrs)
        class-and-attr-prob (conj attr-probs
                                  (probability category))]
    (float (apply * class-and-attr-prob))))

前面代码中定义的函数返回的值与以下evidence-of-salmonevidence-of-sea-bass函数返回的值一致:

user> (evidence-of-salmon :dark :fat)
0.38502988
user> (evidence-of-category-with-attrs :salmon :dark :fat)
0.38502988

使用evidence-of-salmonevidence-of-sea-bass函数,我们可以按照以下方式计算以probability-dark-long-fat-is-salmon为单位的概率:

(def probability-dark-long-fat-is-salmon
  (let [attrs [:dark :long :fat]
        sea-bass? (apply evidence-of-sea-bass attrs)
        salmon? (apply evidence-of-salmon attrs)]
    (/ salmon?
       (+ sea-bass? salmon?))))

我们可以在 REPL 中检查probability-dark-long-fat-is-salmon值,如下所示:

user> probability-dark-long-fat-is-salmon
0.957091799207812

probability-dark-long-fat-is-salmon值表明,一条深色、长而胖的鱼有 95.7%的概率是鲑鱼。

使用前面定义的probability-dark-long-fat-is-salmon函数作为模板,我们可以泛化它所执行的计算。让我们首先定义一个简单的数据结构,它可以被传递。在 Clojure 的惯用风格中,我们方便地使用映射来完成这个目的。使用映射,我们可以在我们的模型中表示一个类别及其出现的证据和概率。此外,给定几个类别的证据,我们可以计算特定类别出现的总概率,如下面的代码所示:

(defn make-category-probability-pair
  [category attrs]
  (let [evidence-of-category (apply
  evidence-of-category-with-attrs
                              category attrs)]
    {:category category
     :evidence evidence-of-category}))

(defn calculate-probability-of-category
  [sum-of-evidences pair]
  (let [probability-of-category (/ (:evidence pair)
                                   sum-of-evidences)]
    (assoc pair :probability probability-of-category)))

make-category-probability-pair函数使用我们在前面代码中定义的evidence-category-with-attrs函数来计算类别的证据及其条件或属性。然后,它以映射的形式返回这个值,以及类别本身。此外,我们还定义了calculate-probability-of-category函数,该函数使用sum-of-evidences参数和make-category-probability-pair函数返回的值来计算类别及其条件的总概率。

我们可以将前面两个函数组合起来,确定给定一些观察值的所有类别的总概率,然后选择概率最高的类别,如下所示:

(defn classify-by-attrs
  "Performs Bayesian classification of the attributes,
   given some categories.
   Returns a map containing the predicted category and
   the category's
   probability of occurrence."
  [categories & attrs]
  (let [pairs (map #(make-category-probability-pair % attrs)
                   categories)
        sum-of-evidences (reduce + (map :evidence pairs))
        probabilities (map #(calculate-probability-of-category
                              sum-of-evidences %)
                           pairs)
        sorted-probabilities (sort-by :probability probabilities)
        predicted-category (last sorted-probabilities)]
    predicted-category))

前述代码中定义的classify-by-attrs函数将所有可能的类别映射到make-category-probability-pair函数,给定一些条件或我们模型特征的观察值。由于我们处理的是make-category-probability-pair返回的序列对,我们可以使用reducemap+函数的简单组合来计算此序列中所有证据的总和。然后,我们将calculate-probability-of-category函数映射到类别-证据对的序列,并选择概率最高的类别-证据对。我们通过按概率升序排序序列来实现这一点,并选择排序序列中的最后一个元素。

现在,我们可以使用classify-by-attrs函数来确定一个观察到的鱼(外观为暗、长、胖)是鲑鱼的概率。它也由我们之前定义的probability-dark-long-fat-is-salmon值表示。这两个表达式都产生了相同的概率,即 95.7%,表示外观为暗、长、胖的鱼是鲑鱼。我们将在以下代码中实现classify-by-attrs函数:

user> (classify-by-attrs [:salmon :sea-bass] :dark :long :fat)
{:probability 0.957091799207812, :category :salmon, :evidence 0.1949689}
user> probability-dark-long-fat-is-salmon
0.957091799207812

classify-by-attrs函数还返回给定观察条件:dark:long:fat的预测类别(即:salmon)。我们可以使用此函数来了解更多关于训练数据的信息,如下所示:

user> (classify-by-attrs [:salmon :sea-bass] :dark)
{:probability 0.8857825967670728, :category :salmon, :evidence 0.4816}
user> (classify-by-attrs [:salmon :sea-bass] :light)
{:probability 0.5362699908806723, :category :sea-bass, :evidence 0.2447}
user> (classify-by-attrs [:salmon :sea-bass] :thin)
{:probability 0.6369809383442954, :category :sea-bass, :evidence 0.2439}

如前述代码所示,外观为暗的鱼主要是鲑鱼,外观为亮的鱼主要是海鲈鱼。此外,体型瘦的鱼很可能是海鲈鱼。以下值实际上与我们之前定义的训练数据相符:

user> (classify-by-attrs [:salmon] :dark)
{:probability 1.0, :category :salmon, :evidence 0.4816}
user> (classify-by-attrs [:salmon])
{:probability 1.0, :category :salmon, :evidence 0.6932}

注意,仅使用[:salmon]作为参数调用classify-by-attrs函数会返回任何给定鱼是鲑鱼的概率。一个明显的推论是,给定一个单一类别,classify-by-attrs函数总是以完全的确定性预测提供的类别,即概率为1.0。然而,该函数返回的证据取决于传递给它的观察特征以及我们用来训练模型的样本数据。

简而言之,前面的实现描述了一个可以使用一些样本数据进行训练的贝叶斯分类器。它还分类了我们的模型特征的某些观察值。

我们可以通过构建我们之前示例中https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_60.jpg概率的定义来描述一个通用的贝叶斯分类器。为了快速回顾,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_60.jpg这个术语可以正式表达如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_72.jpg

在前一个等式中,我们处理一个单一类别,即鲑鱼,以及三个相互独立特征,即鱼皮的长度、宽度和亮度。我们可以将这个等式推广到 N 个特征,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_78.jpg

在这里,术语 Z 是分类模型的证据,我们在前一个方程中进行了描述。我们可以使用求和和乘积符号来更简洁地描述前一个等式,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_79.jpg

前一个等式描述了单个类别 C 的发生概率。如果我们给定多个类别可供选择,我们必须选择发生概率最高的类别。这引出了贝叶斯分类器的基本定义,其形式表达如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_80.jpg

在前一个方程中,函数 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_81.jpg 描述了一个贝叶斯分类器,它选择发生概率最高的类别。请注意,术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_82.jpg 代表我们分类模型的各个特征,而术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_83.jpg 代表这些特征的观测值集合。此外,方程右侧的变量 c 可以取分类模型中所有不同类别的值。

我们可以通过 最大后验概率 (MAP) 估计进一步简化贝叶斯分类器的先前的方程,这可以被视为贝叶斯统计中特征的正规化。简化的贝叶斯分类器可以形式表达如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_84.jpg

这个定义本质上意味着 classify 函数确定给定特征的最高发生概率的类别。因此,前一个方程描述了一个可以使用一些样本数据进行训练,然后用于预测给定观测值集合类别的贝叶斯分类器。我们现在将专注于使用现有的贝叶斯分类器实现来建模给定的分类问题。

clj-ml库(github.com/joshuaeckroth/clj-ml)包含几个实现算法,我们可以从中选择来模拟给定的分类问题。实际上,这个库只是流行的Weka库(www.cs.waikato.ac.nz/ml/weka/)的 Clojure 包装器,Weka 是一个包含多个机器学习算法实现的 Java 库。它还有几个用于评估和验证生成的分类模型的方法。然而,我们将专注于本章中clj-ml库的分类器实现。

备注

可以通过在project.clj文件中添加以下依赖项将clj-ml库添加到 Leiningen 项目中:

[cc.artifice/clj-ml "0.4.0"]

对于即将到来的示例,命名空间声明应类似于以下声明:

(ns my-namespace
  (:use [clj-ml classifiers data]))

现在,我们将通过一个贝叶斯分类器的实现来介绍clj-ml库,以模拟我们之前涉及鱼包装厂的问题。首先,让我们精炼我们的训练数据,使用数值而不是我们之前描述的关键字来表示模型的各种特征。当然,我们将在训练数据中保持部分性,使得鲑鱼大多是肥的和深色的,而海鲈鱼大多是长的和浅色的。以下代码实现了这一点:

(defn rand-in-range
  "Generates a random integer within the given range"
  [min max]
  (let [len      (- max min)
        rand-len (rand-int len)]
    (+ min rand-len)))

;; sea bass are mostly long and light in color
(defn make-sea-bass []
  (vector :sea-bass
          (rand-in-range 6 10)          ; length
          (rand-in-range 0 5)           ; width
          (rand-in-range 4 10)))        ; lightness of skin

;; salmon are mostly fat and dark
(defn make-salmon []
  (vector :salmon
          (rand-in-range 0 7)           ; length
          (rand-in-range 4 10)          ; width
          (rand-in-range 0 6)))         ; lightness of skin

在这里,我们定义了rand-in-range函数,该函数简单地生成给定值范围内的随机整数。然后我们重新定义了make-sea-bassmake-salmon函数,使用rand-in-range函数来生成鱼的三个特征(长度、宽度和皮肤颜色深浅)的值,这些值在010之间。皮肤颜色较浅的鱼,这个特征的值会更高。请注意,我们重用了make-sample-fish函数的定义和fish-dataset变量的定义来生成我们的训练数据。此外,鱼是由一个向量而不是一个集合来表示的,正如在make-sea-bassmake-salmon函数的早期定义中所述。

我们可以使用clj-ml库中的make-classifier函数创建一个分类器,该函数位于clj-ml.classifiers命名空间中。我们可以通过将两个关键字作为参数传递给函数来指定要使用的分类器类型。由于我们打算使用贝叶斯分类器,我们将关键字:bayes:naive传递给make-classifier函数。简而言之,我们可以使用以下声明来创建一个贝叶斯分类器。请注意,以下代码中使用的关键字:naive表示一个朴素贝叶斯分类器,它假设我们模型中的特征是独立的:

(def bayes-classifier (make-classifier :bayes :naive))

clj-ml 库的分类器实现使用通过 clj-ml.data 命名空间中的函数定义或生成的数据集。我们可以使用 make-dataset 函数将 fish-dataset 序列(一个向量序列)转换为这样的数据集。此函数需要一个数据集的任意字符串名称、每个集合项的模板以及要添加到数据集中的项目集合。提供给 make-dataset 函数的模板很容易用映射表示,如下所示:

(def fish-template
  [{:category [:salmon :sea-bass]}
   :length :width :lightness])

(def fish-dataset
  (make-dataset "fish" fish-template fish-training-data))

在前面的代码中定义的 fish-template 映射只是简单地说明,作为一个向量表示的鱼,由鱼的类别、长度、宽度和皮肤的亮度组成,顺序如下。请注意,鱼的类别是用 :salmon:sea-bass 描述的。现在我们可以使用 fish-dataset 来训练由 bayes-classifier 变量表示的分类器。

虽然该 fish-template 映射定义了鱼的全部属性,但它仍然缺少一个重要的细节。它没有指定这些属性中哪一个代表鱼的类别或分类。为了指定向量中的一个特定属性以代表整个观察值集合的类别,我们使用 dataset-set-class 函数。此函数接受一个参数,指定属性的索引,并用于在向量中代表观察值集合的类别。请注意,此函数实际上会修改或修改它提供的数据集。然后我们可以使用 classifier-train 函数来训练我们的分类器,该函数接受一个分类器和数据集作为参数;如下代码所示:

(defn train-bayes-classifier []
  (dataset-set-class fish-dataset 0)
  (classifier-train bayes-classifier fish-dataset))

前面的 train-bayes-classifier 函数只是调用了 dataset-set-classclassifier-train 函数来训练我们的分类器。当我们调用 train-bayes-classifier 函数时,分类器会使用以下提供的数据进行训练,然后打印到 REPL 输出:

user> (train-bayes-classifier)
#<NaiveBayes Naive Bayes Classifier

                     Class
Attribute        salmon  sea-bass
                  (0.7)    (0.3)
=================================
length
  mean            2.9791   7.5007
  std. dev.       1.9897   1.1264
  weight sum        7032     2968
  precision            1        1

width
  mean            6.4822   1.9747
  std. dev.        1.706    1.405
  weight sum        7032     2968
  precision            1        1

lightness
  mean            2.5146   6.4643
  std. dev.       1.7047   1.7204
  weight sum        7032     2968
  precision            1        1

>

此输出为我们提供了关于训练数据的一些基本信息,例如我们模型中各种特征的均值和标准差。现在我们可以使用这个训练好的分类器来预测我们模型特征的观察值的类别。

让我们先定义我们打算分类的观察值。为此,我们使用以下 make-instance 函数,该函数需要一个数据集和一个与提供的数据集数据模板相匹配的观察值向量:

(def sample-fish
  (make-instance fish-dataset [:salmon 5.0 6.0 3.0]))

在这里,我们只是使用 make-instance 函数定义了一个样本鱼。现在我们可以如下预测由 sample-fish 表示的鱼的类别:

user> (classifier-classify bayes-classifier sample-fish)
:salmon

如前述代码所示,鱼被分类为salmon。请注意,尽管我们在定义sample-fish时提供了鱼的类别为:salmon,但这只是为了符合fish-dataset定义的数据模板。实际上,我们可以将sample-fish的类别指定为:sea-bass或第三个值,例如:unknown,以表示一个未定义的值,分类器仍然会将sample-fish分类为salmon

当处理给定分类模型的各种特征的连续值时,我们可以指定一个贝叶斯分类器来使用连续特征的离散化。这意味着模型的各种特征的值将通过概率密度估计转换为离散值。我们可以通过简单地向make-classifier函数传递一个额外的参数{:supervised-discretization true}来指定此选项。这个映射实际上描述了可以提供给指定分类器的所有可能选项。

总之,clj-ml库提供了一个完全可操作的贝叶斯分类器,我们可以用它来对任意数据进行分类。尽管我们在前面的例子中自己生成了训练数据,但这些数据也可以从网络或数据库中获取。

使用 k-最近邻算法

一种可以用来对一组观测值进行分类的简单技术是k-最近邻(简称k-NN)算法。这是一种懒惰学习的形式,其中所有计算都推迟到分类阶段。在分类阶段,k-NN 算法仅使用训练数据中的少数几个值来近似观测值的类别,其他值的读取则推迟到实际需要时。

虽然我们现在在分类的背景下探索 k-NN 算法,但它也可以通过简单地选择预测值作为一组观测特征值的依赖变量的最近值的平均值来应用于回归。有趣的是,这种建模回归的技术实际上是对线性插值(更多信息,请参阅An introduction to kernel and nearest-neighbor nonparametric regression)的推广。

k-NN 算法读取一些训练数据,并懒惰地分析这些数据,也就是说,只有在需要时才会分析。除了训练数据外,该算法还需要一组观测值和一个常数k作为参数来对观测值集进行分类。为了对这些观测值进行分类,算法预测与观测值集最近的k个训练样本中最频繁的类别。这里的“最近”是指,在训练数据的欧几里得空间中,代表观测值集的点与具有最小欧几里得距离的点。

一个明显的推论是,当https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_85.jpg时,预测的类别是观察值集合最近的单个邻居的类别。k-NN 算法的这个特殊情况被称为最近邻算法。

我们可以使用clj-ml库的make-classifier函数创建一个使用 k-NN 算法的分类器。这样的分类器通过将:lazy:ibk作为make-classifier函数的参数来指定。我们现在将使用这样的分类器来模拟我们之前的鱼包装厂示例,如下所示:

(def K1-classifier (make-classifier :lazy :ibk))

(defn train-K1-classifier []
  (dataset-set-class fish-dataset 0)
  (classifier-train K1-classifier fish-dataset))

前述代码定义了一个 k-NN 分类器为K1-classifier,以及一个train-K1-classifier函数,使用fish-dataset(我们在前述代码中定义)来训练分类器。

注意,make-classifier函数默认将常数k或更确切地说,邻居的数量设置为1,这意味着单个最近邻。我们可以选择性地通过将:num-neighbors键作为键值对传递给make-classifier函数来指定常数k,如下述代码所示:

(def K10-classifier (make-classifier
                     :lazy :ibk {:num-neighbors 10}))

我们现在可以调用train-K1-classifier函数来按照以下方式训练分类器:

user> (train-K1-classifier)
#<IBk IB1 instance-based classifier
using 1 nearest neighbour(s) for classification
>

我们现在可以使用classifier-classify函数来对之前定义的sample-fish表示的鱼进行分类,使用的是由K1-classifier变量表示的分类器:

user> (classifier-classify K1-classifier sample-fish)
:salmon

如前述代码所示,k-NN 分类器预测鱼类为鲑鱼,这与我们之前使用贝叶斯分类器做出的预测一致。总之,clj-ml库提供了一个简洁的实现,使用 k-NN 算法来预测一组观察值的类别。

clj-ml库提供的 k-NN 分类器默认使用这些特征的值均值和标准差对分类模型的特征进行归一化。我们可以通过传递一个包含:no-normalization键的映射条目到make-classifier函数的选项映射中,来指定一个选项给make-classifier函数以跳过这个归一化阶段。

使用决策树

我们还可以使用决策树来建模给定的分类问题。实际上,决策树是从给定的训练数据构建的,我们可以使用这个决策树来预测一组给定观察值的类别。构建决策树的过程大致基于信息论中的信息熵和信息增益的概念(更多信息,请参阅决策树归纳)。它也常被称为决策树学习。不幸的是,对信息论进行详细研究超出了本书的范围。然而,在本节中,我们将探讨一些将在机器学习环境中使用的信息论概念。

决策树是一种树或图,描述了决策及其可能后果的模型。决策树中的一个内部节点代表一个决策,或者更确切地说,在分类的上下文中,代表特定特征的某种条件。它有两个可能的结果,分别由节点的左右子树表示。当然,决策树中的节点也可能有超过两个的子树。决策树中的每个叶节点代表我们分类模型中的一个特定类别或后果。

例如,我们之前的涉及鱼包装工厂的分类问题可能有以下决策树:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_86.jpg

之前展示的决策树使用两个条件来分类鱼是鲑鱼还是海鲢。内部节点代表基于我们分类模型特征的两种条件。请注意,决策树只使用了我们分类模型的三个特征中的两个。因此,我们可以说这棵树是剪枝的。我们将在本节中简要探讨这项技术。

要使用决策树对一组观测值进行分类,我们从根节点开始遍历树,直到达到代表观测值集合预测类别的叶节点。从决策树预测一组观测值类别的这种技术始终相同,无论决策树是如何构建的。对于之前描述的决策树,我们可以通过首先比较鱼的长度,然后比较其皮肤的亮度来对鱼进行分类。第二次比较只有在鱼的长度大于6(如决策树中带有表达式Length < 6的内部节点所指定)时才需要。如果鱼的长度确实大于6,我们使用鱼的皮肤亮度来决定它是鲑鱼还是海鲢。

实际上,有几种算法用于从一些训练数据中构建决策树。通常,树是通过根据属性值测试将训练数据中的样本值集合分割成更小的子集来构建的。这个过程在每个子集上重复进行,直到分割给定样本值子集不再向决策树添加内部节点。正如我们之前提到的,决策树中的内部节点可能有多于两个的子树。

现在我们将探讨C4.5算法来构建决策树(更多信息,请参阅*《C4.5:机器学习程序》*)。这个算法使用信息熵的概念来决定必须对样本值集合进行分割的特征和相应的值。信息熵被定义为给定特征或随机变量的不确定性度量(更多信息,请参阅“通信的数学理论”)。

对于给定特征或属性 f,其值在 1m 的范围内,我们可以定义该https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_88.jpg特征的信息熵如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_89.jpg

在前面的方程中,术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_90.jpg表示特征 f 相对于值 i 的出现次数。基于特征信息熵的定义,我们定义归一化信息增益https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_91.jpg。在以下等式中,术语 T 指的是提供给算法的样本值或训练数据集:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_92.jpg

从信息熵的角度来看,给定属性的先前的信息增益定义是当从模型中给定的特征集中移除属性 f 时,整个值集的信息熵的变化。

算法从训练数据中选择的特征 A,使得特征 A 在特征集中具有最大的可能信息增益。我们可以借助以下等式来表示这一点:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_93.jpg

在前面的方程中,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_94.jpg表示特征 a 所知可能具有的所有可能值集。https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_95.jpg集表示特征 a 具有值 v 的观察值集,而术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_03_96.jpg表示该值集的信息熵。

使用前面的方程从训练数据中选择具有最大信息增益的特征,我们可以通过以下步骤描述 C4.5 算法:

  1. 对于每个特征 a,找到在特征 a 上划分样本数据时的归一化信息增益。

  2. 选择具有最大归一化信息增益的特征 A

  3. 根据所选特征 A 创建一个内部决策节点。从这个步骤创建的两个子树要么是叶节点,要么是进一步划分的新样本值集。

  4. 在上一步产生的每个样本值分区集上重复此过程。我们重复前面的步骤,直到样本值子集中的所有特征都具有相同的信息熵。

一旦创建了一个决策树,我们可以选择性地对该树进行剪枝。剪枝简单地说就是从树中移除任何多余的决策节点。这可以被视为通过正则化决策树来防止估计的决策树模型欠拟合或过拟合的一种形式。

J48是 C4.5 算法在 Java 中的开源实现,clj-ml库包含一个有效的 J48 决策树分类器。我们可以使用make-classifier函数创建一个决策树分类器,并向此函数提供:decision-tree:c45关键字作为参数来创建一个 J48 分类器,如下述代码所示:

(def DT-classifier (make-classifier :decision-tree :c45))

(defn train-DT-classifier []
  (dataset-set-class fish-dataset 0)
  (classifier-train DT-classifier fish-dataset))

前述代码中定义的train-DT-classifier函数简单地将DT-classifier分类器用我们之前鱼包装厂示例中的训练数据训练。classifier-train函数还会打印以下训练好的分类器:

user> (train-DT-classifier)
#<J48 J48 pruned tree
------------------
width <= 3: sea-bass (2320.0)
width > 3
|   length <= 6
|   |   lightness <= 5: salmon (7147.0/51.0)
|   |   lightness > 5: sea-bass (95.0)
|   length > 6: sea-bass (438.0)

Number of Leaves  : 4

Size of the tree : 7
>

上述输出很好地说明了训练好的决策树的外观,以及决策树的大小和叶节点数量。显然,决策树有三个不同的内部节点。树的根节点基于鱼的宽度,后续节点基于鱼的长度,最后一个决策节点基于鱼皮肤的亮度。

现在,我们可以使用决策树分类器来预测鱼的类别,我们使用以下classifier-classify函数来进行这个分类:

user> (classifier-classify DT-classifier sample-fish)
:salmon

如前述代码所示,分类器将代表sample-fish的鱼的类别预测为:salmon关键字,就像之前示例中使用的其他分类器一样。

clj-ml库提供的 J48 决策树分类器实现,在训练分类器时将剪枝作为最后一步。我们可以通过在传递给make-classifier函数的选项映射中指定:unpruned键来生成未经修剪的树,如下述代码所示:

(def UDT-classifier (make-classifier
                     :decision-tree :c45 {:unpruned true}))

之前定义的分类器在用给定的训练数据训练决策树时不会进行剪枝。我们可以通过定义和调用train-UDT-classifier函数来检查未经修剪的树的外观,该函数简单地使用classifier-train函数和fish-dataset训练数据来训练分类器。此函数可以定义为与train-UDT-classifier函数类似,并在调用时产生以下输出:

user> (train-UDT-classifier)
#<J48 J48 unpruned tree
------------------
width <= 3: sea-bass (2320.0)
width > 3
|   length <= 6
|   |   lightness <= 5
|   |   |   length <= 5: salmon (6073.0)
|   |   |   length > 5
|   |   |   |   width <= 4
|   |   |   |   |   lightness <= 3: salmon (121.0)
|   |   |   |   |   lightness > 3
|   |   |   |   |   |   lightness <= 4: salmon (52.0/25.0)
|   |   |   |   |   |   lightness > 4: sea-bass (50.0/24.0)
|   |   |   |   width > 4: salmon (851.0)
|   |   lightness > 5: sea-bass (95.0)
|   length > 6: sea-bass (438.0)

Number of Leaves  : 8

Size of the tree : 15

如前述代码所示,未经修剪的决策树与修剪后的决策树相比,拥有更多的内部决策节点。现在我们可以使用以下classifier-classify函数来预测一条鱼的类别,使用的是训练好的分类器:

user> (classifier-classify UDT-classifier sample-fish)
:salmon

有趣的是,未经修剪的树也预测了代表sample-fish的鱼的类别为:salmon,因此与之前描述的修剪决策树预测的类别一致。总之,clj-ml库为我们提供了一个基于 C4.5 算法的决策树分类器的有效实现。

make-classifier函数支持 J48 决策树分类器的一些有趣选项。我们已经探讨了:unpruned选项,它表示决策树没有被剪枝。我们可以将:reduced-error-pruning选项指定给make-classifier函数,以强制使用减少误差剪枝(更多信息,请参阅“基于树大小的悲观决策树剪枝”),这是一种基于减少模型整体误差的剪枝形式。我们还可以指定给make-classifier函数的另一个有趣选项是剪枝决策树时可以移除的最大内部节点或折叠数。我们可以使用:pruning-number-of-folds选项来指定此选项,并且默认情况下,make-classifier函数在剪枝决策树时不会施加此类限制。此外,我们还可以通过指定:only-binary-splits选项给make-classifier函数,来指定决策树中的每个内部决策节点只有两个子树。

摘要

在本章中,我们探讨了分类以及可以用来对给定分类问题进行建模的各种算法。尽管分类技术非常有用,但当样本数据具有大量维度时,它们的性能并不太好。此外,特征可能以非线性方式变化,正如我们将在第四章“构建神经网络”中描述的那样。我们将在接下来的章节中进一步探讨这些方面以及监督学习的替代方法。以下是本章中我们关注的一些要点:

  • 我们描述了两种广泛的分类类型,即二分类和多分类。我们还简要研究了逻辑函数以及如何通过逻辑回归来使用它来建模分类问题。

  • 我们研究和实现了贝叶斯分类器,它使用用于建模分类的概率模型。我们还描述了如何使用clj-ml库的贝叶斯分类器实现来对给定的分类问题进行建模。

  • 我们还探讨了简单的 k-最近邻算法以及我们如何利用clj-ml库来利用它。

  • 我们研究了决策树和 C4.5 算法。clj-ml库为我们提供了一个基于 C4.5 算法的可配置分类器实现,我们描述了如何使用此实现。

在下一章中,我们将探讨人工神经网络。有趣的是,我们可以使用人工神经网络来建模回归和分类问题,我们也将研究这些神经网络的方面。

第四章:建立神经网络

在本章中,我们将介绍人工神经网络ANNs)。我们将研究 ANNs 的基本表示,然后讨论可用于监督和未监督机器学习问题的多个 ANN 模型。我们还介绍了Enclog Clojure 库来构建 ANNs。

神经网络非常适合在给定的数据中寻找模式,并在计算领域有多个实际应用,例如手写识别和机器视觉。人工神经网络(ANNs)通常被组合或相互连接来模拟给定的问题。有趣的是,它们可以应用于多个机器学习问题,如回归和分类。ANNs 在计算领域的多个领域都有应用,并不局限于机器学习的范围。

无监督学习是一种机器学习方法,其中给定的训练数据不包含任何关于给定输入样本属于哪个类别的信息。由于训练数据是未标记的,无监督学习算法必须完全依靠自己确定给定数据中的各种类别。通常,这是通过寻找不同数据之间的相似性,然后将数据分组到几个类别中实现的。这种技术称为聚类分析,我们将在以下章节中更详细地研究这种方法。ANNs 在无监督机器学习技术中的应用主要是由于它们能够快速识别某些未标记数据中的模式。这种 ANNs 表现出的特殊形式的无监督学习被称为竞争学习

关于 ANNs 的一个有趣的事实是,它们是从具有学习能力的更高阶动物的中枢神经系统的结构和行为中建模的。

理解非线性回归

到目前为止,读者必须意识到梯度下降算法可以用来估计回归和分类问题的线性回归和逻辑回归模型。一个明显的问题会是:当我们可以使用梯度下降从训练数据中估计线性回归和逻辑回归模型时,为什么还需要神经网络?为了理解 ANNs 的必要性,我们首先必须理解非线性回归

假设我们有一个单个特征变量X和一个随X变化的因变量Y,如下面的图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_01.jpg

如前图所示,将因变量 Y 模型化为自变量 X 的线性方程是困难的,甚至是不可能的。我们可以将因变量 Y 模型为自变量 X 的高阶多项式方程,从而将问题转化为线性回归的标准形式。因此,说因变量 Y 非线性地随自变量 X 变化。当然,也有可能数据无法使用多项式函数进行建模。

还可以证明,使用梯度下降法计算多项式函数中所有项的权重或系数的时间复杂度为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_02.jpg,其中 n 是训练数据中的特征数量。同样,计算三次多项式方程中所有项的系数的算法复杂度为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_03.jpg。很明显,梯度下降的时间复杂度随着模型特征数量的增加而呈几何级数增长。因此,仅使用梯度下降本身不足以对具有大量特征的非线性回归模型进行建模。

另一方面,神经网络在建模具有大量特征的数据的非线性回归模型方面非常高效。我们现在将研究神经网络的基础理念以及可用于监督学习和无监督学习问题的几种神经网络模型。

表示神经网络

神经网络(ANNs)是从能够学习的生物体(如哺乳动物和爬行动物)的中枢神经系统行为中建模的。这些生物体的中枢神经系统包括生物体的脑、脊髓和一系列支持性神经组织。大脑处理信息并产生电信号,这些信号通过神经网络纤维传输到生物体的各个器官。尽管生物体的脑执行了许多复杂的处理和控制,但实际上它是由神经元组成的集合。然而,实际处理感觉信号的是这些神经元的一些复杂组合。当然,每个神经元都能够处理大脑处理的信息的极小部分。大脑实际上是通过将来自身体各种感觉器官的电信号通过这个复杂的神经元网络路由到其运动器官来发挥作用的。以下图示了单个神经元的细胞结构:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_04.jpg

神经元有几个接近细胞核的树突和一个单一的轴突,轴突用于从细胞的核传递信号。树突用于接收来自其他神经元的信号,可以将其视为神经元的输入。同样,神经元的轴突类似于神经元的输出。因此,神经元可以用一个处理多个输入并产生单个输出的函数来数学地表示。

其中一些神经元是相互连接的,这种网络被称为神经网络。神经元通过从其他神经元中传递微弱的电信号来执行其功能。两个神经元之间的连接空间称为突触

一个人工神经网络(ANN)由多个相互连接的神经元组成。每个神经元都可以用一个数学函数来表示,该函数消耗多个输入值并产生一个输出值,如下面的图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_05.jpg

可以用前面的图来表示单个神经元。从数学的角度来看,它只是一个将一组输入值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_07.jpg映射到输出值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_08.jpg的函数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_06.jpg。这个函数https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_06.jpg被称为神经元的激活函数,其输出值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_08.jpg被称为神经元的激活。这种神经元的表示称为感知器。感知器可以单独使用,并且足够有效,可以估计监督机器学习模型,如线性回归和逻辑回归。然而,复杂非线性数据可以用多个相互连接的感知器更好地建模。

通常,会将一个偏差输入添加到供给感知器的输入值集合中。对于输入值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_07.jpg,我们添加项https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_09.jpg作为偏差输入,使得https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_10.jpg。具有这个附加偏差值的神经元可以用以下图来表示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_11.jpg

供给感知器的每个输入值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_12.jpg都有一个相关的权重https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_13.jpg。这个权重与线性回归模型特征的系数类似。激活函数应用于这些权重及其相应的输入值。我们可以如下形式地定义感知器的估计输出值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_06.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_14.jpg

ANN(人工神经网络)的节点所使用的激活函数很大程度上取决于需要建模的样本数据。通常,Sigmoid双曲正切函数被用作分类问题的激活函数(更多信息,请参阅基于汽油近红外(NIR)光谱的校准模型构建的波形神经网络(WNN)方法)。据说 Sigmoid 函数在给定的阈值输入时会被激活

我们可以通过绘制 Sigmoid 函数的方差来描述这种行为,如下面的图表所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_15.jpg

ANN 可以广泛地分为前馈神经网络循环神经网络(更多信息,请参阅双向循环神经网络)。这两种类型 ANN 之间的区别在于,在前馈神经网络中,ANN 节点的连接不形成一个有向循环,而循环神经网络中节点之间的连接则形成一个有向循环。因此,在前馈神经网络中,ANN 给定层的每个节点只接收来自 ANN 中直接前一层的节点的输入。

有几种 ANN 模型具有实际应用,我们将在本章中探讨其中的一些。

理解多层感知器 ANN

现在我们介绍一个简单的前馈神经网络模型——多层感知器模型。该模型代表了一个基本的前馈神经网络,并且足够灵活,可以用于监督学习领域中回归和分类问题的建模。所有输入都通过一个前馈神经网络以单一方向流动。这是没有从或向任何层反馈的事实的一个直接后果。

通过反馈,我们指的是给定层的输出被反馈作为 ANN 中前一层的感知器的输入。此外,使用单层感知器意味着只使用一个激活函数,这相当于使用逻辑回归来建模给定的训练数据。这意味着该模型不能用于拟合非线性数据,而这正是 ANN 的主要动机。我们必须注意,我们在第三章数据分类中讨论了逻辑回归。

多层感知器 ANN 可以通过以下图表来表示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_16.jpg

多层感知器 ANN 由多个感知器节点层组成。它具有单个输入层、单个输出层和多个隐藏层。输入层只是将输入值传递给 ANN 的第一个隐藏层。然后这些值通过其他隐藏层传播到输出层,在这些层中使用激活函数进行加权求和,最终产生输出值。

训练数据中的每个样本由https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_17.jpg元组表示,其中https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_18.jpg是期望输出,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_19.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_20.jpg训练样本的输入值。https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_19.jpg输入向量包含与训练数据中特征数量相等的值。

每个节点的输出被称为该节点的激活,对于https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_20.jpg层中的https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_21.jpg节点,用术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_22.jpg表示。正如我们之前提到的,用于生成此值的激活函数是 Sigmoid 函数或双曲正切函数。当然,任何其他数学函数都可以用来拟合样本数据。多层感知器网络的输入层只是将一个偏置输入加到输入值上,并将提供给 ANN 的输入集传递到下一层。我们可以用以下等式正式表示这个关系:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_23.jpg

ANN 中每一对层之间的突触都有一个相关的权重矩阵。这些矩阵的行数等于输入值的数量,即 ANN 输入层附近层的节点数,列数等于突触层中靠近 ANN 输出层的节点数。对于层l,权重矩阵用术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_24.jpg表示。

l的激活值可以使用 ANN 的激活函数来确定。激活函数应用于权重矩阵和由 ANN 中前一层的激活值产生的乘积。通常,用于多层感知器的激活函数是 Sigmoid 函数。这个等式可以正式表示如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_25.jpg

通常,用于多层感知器的激活函数是 Sigmoid 函数。请注意,我们不在 ANN 的输出层中添加偏置值。此外,输出层可以产生任意数量的输出值。为了建模一个k类分类问题,我们需要一个产生k个输出值的 ANN。

要进行二元分类,我们只能对最多两个类别的输入数据进行建模。用于二元分类的 ANN 生成的输出值总是 0 或 1。因此,对于https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_26.jpg类,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_27.jpg

我们也可以使用k个二进制输出值来模拟多类分类,因此,人工神经网络(ANN)的输出是一个https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_28.jpg矩阵。这可以形式化地表达如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_29.jpg

因此,我们可以使用多层感知器 ANN 来执行二类和多类分类。多层感知器 ANN 可以使用反向传播算法进行训练,我们将在本章后面学习和实现它。

假设我们想要模拟逻辑异或门的行为。异或门可以被看作是一个需要两个输入并生成单个输出的二进制分类器。模拟异或门的人工神经网络将具有以下图中所示的结构。有趣的是,线性回归可以用来模拟 AND 和 OR 逻辑门,但不能用来模拟异或门。这是因为异或门输出的非线性特性,因此,ANN 被用来克服这一限制。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_30.jpg

前图中所示的多层感知器有三个输入层节点,四个隐藏层节点和一个输出层节点。观察发现,除了输出层之外,每个层都会向下一层的节点输入值集合中添加一个偏置输入。ANN 中有两个突触,如图所示,它们与权重矩阵https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_31.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_32.jpg相关联。请注意,第一个突触位于输入层和隐藏层之间,第二个突触位于隐藏层和输出层之间。权重矩阵https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_31.jpg的大小为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_33.jpg,权重矩阵https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_32.jpg的大小为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_34.jpg。此外,术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_35.jpg用来表示 ANN 中的所有权重矩阵。

由于多层感知器 ANN 中每个节点的激活函数是 Sigmoid 函数,我们可以将 ANN 节点权重的成本函数定义为与逻辑回归模型的成本函数类似。ANN 的成本函数可以用权重矩阵来定义,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_36.jpg

前面的代价函数本质上是对 ANN 输出层中每个节点代价函数的平均值(更多信息,请参阅材料科学中的神经网络)。对于具有 K 个输出值的多层感知器 ANN,我们对 K 个项进行平均。请注意,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_37.jpg 代表 ANN 的https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_38.jpg 输出值,https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_39.jpg 代表 ANN 的输入变量,N 是训练数据中的样本值数量。代价函数本质上与逻辑回归相同,但在这里应用于 K 个输出值。我们可以在前面的代价函数中添加一个正则化参数,并使用以下方程表示正则化代价函数:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_40.jpg

在前一个方程中定义的代价函数添加了一个类似于逻辑回归的正则化项。正则化项本质上是由 ANN 的多个层中所有输入值的权重平方和组成的,但不包括添加的偏置输入的权重。此外,术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_41.jpg指的是 ANN 中第 l 层的节点数。值得注意的是,在前面的正则化代价函数中,只有正则化项依赖于 ANN 中的层数。因此,估计模型的泛化能力基于 ANN 中的层数。

理解反向传播算法

反向传播学习算法用于从给定的一组样本值中训练一个多层感知器 ANN。简而言之,该算法首先计算给定输入值集的输出值,并计算 ANN 输出的误差量。ANN 中的误差量是通过将 ANN 的预测输出值与训练数据提供给 ANN 的给定输入值的预期输出值进行比较来确定的。然后,计算出的误差用于修改 ANN 的权重。因此,在用合理数量的样本训练 ANN 后,ANN 将能够预测输入值集的输出值。该算法由三个不同的阶段组成。具体如下:

  • 前向传播阶段

  • 反向传播阶段

  • 权重更新阶段

ANN 中突触的权重最初被初始化为在https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_42.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_43.jpg范围内的随机值。我们将权重初始化为这个范围内的值,以避免权重矩阵中的对称性。这种避免对称性的做法称为对称破缺,其目的是使反向传播算法的每次迭代都能在 ANN 中突触的权重上产生明显的变化。这在人工神经网络中是可取的,因为每个节点都应该独立于 ANN 中的其他节点进行学习。如果所有节点都具有相同的权重,那么估计的学习模型可能会过拟合或欠拟合。

此外,反向传播学习算法还需要两个额外的参数,即学习率https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_44.jpg和学习动量https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_45.jpg。我们将在本节后面的示例中看到这些参数的影响。

算法的正向传播阶段简单地计算 ANN 各个层中所有节点的激活值。正如我们之前提到的,输入层中节点的激活值是 ANN 的输入值和偏置输入。这可以通过以下方程形式化定义:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_23.jpg

使用来自人工神经网络(ANN)输入层的这些激活值,可以确定 ANN 其他层中节点的激活状态。这是通过将给定层的权重矩阵与 ANN 前一层中节点的激活值相乘,然后应用激活函数来实现的。这可以形式化地表示如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_46.jpg

前面的方程解释了层 l 的激活值等于将前一层(或激活)值和给定层的权重矩阵的输出(或激活)值应用激活函数的结果。接下来,输出层的激活值将被反向传播。通过这种方式,我们指的是激活值从输出层通过隐藏层传播到 ANN 的输入层。在这个阶段,我们确定 ANN 中每个节点的误差或 delta 值。输出层的 delta 值是通过计算期望输出值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_18.jpg和输出层的激活值https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_47.jpg之间的差异来确定的。这种差异计算可以总结为以下方程:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_48.jpg

l的术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_49.jpg是一个大小为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_50.jpg的矩阵,其中j是层l中的节点数。这个术语可以正式定义为以下内容:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_51.jpg

ANN 中除了输出层之外的其他层的 delta 项由以下等式确定:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_52.jpg

在前面的方程中,二进制运算https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_53.jpg用于表示两个相同大小的矩阵的逐元素乘法。请注意,这种运算与矩阵乘法不同,逐元素乘法将返回一个由两个相同大小矩阵中相同位置的元素乘积组成的矩阵。术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_54.jpg表示在 ANN 中使用的激活函数的导数。由于我们使用 sigmoid 函数作为激活函数,因此术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_55.jpg的值为https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_56.jpg

因此,我们可以计算人工神经网络(ANN)中所有节点的 delta 值。我们可以使用这些 delta 值来确定 ANN 中突触的梯度。我们现在继续到反向传播算法的最后一步,即权重更新阶段。

各个突触的梯度首先初始化为所有元素均为 0 的矩阵。给定突触的梯度矩阵的大小与该突触的权重矩阵的大小相同。梯度项https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_57.jpg表示在 ANN 中位于层l之后的突触层的梯度。ANN 中突触梯度的初始化可以正式表示如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_58.jpg

对于训练数据中的每个样本值,我们计算 ANN 中所有节点的 delta 和激活值。这些值通过以下方程添加到突触的梯度中:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_59.jpg

然后,我们计算所有样本值的梯度的平均值,并使用给定层的 delta 和梯度值来更新权重矩阵,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_60.jpg

因此,算法的学习率和学习动量参数仅在权重更新阶段发挥作用。前面的三个方程代表反向传播算法的单次迭代。必须执行大量迭代,直到 ANN 的整体误差收敛到一个很小的值。现在我们可以使用以下步骤总结反向传播学习算法:

  1. 将 ANN 的突触权重初始化为随机值。

  2. 选择一个样本值,并通过 ANN 的几层前向传播样本值以生成 ANN 中每个节点的激活。

  3. 将 ANN 最后一层生成的激活反向传播通过隐藏层到 ANN 的输入层。通过这一步,我们计算 ANN 中每个节点的误差或 delta。

  4. 计算从步骤 3 生成的误差与 ANN 中所有节点的突触权重或输入激活的乘积。这一步产生了网络中每个节点的权重梯度。每个梯度由一个比率或百分比表示。

  5. 使用给定层的梯度和 delta 来计算 ANN 中突触层权重的变化。然后,将这些变化从 ANN 中突触的权重中减去。这本质上是反向传播算法的权重更新步骤。

  6. 对训练数据中的其余样本重复步骤 2 到 5。

反向传播学习算法有几个不同的部分,我们现在将实现每个部分并将它们组合成一个完整的实现。由于 ANN 中的突触和激活的 delta 和权重可以用矩阵表示,我们可以编写这个算法的向量化实现。

注意

注意,对于以下示例,我们需要从 Incanter 库的incanter.core命名空间中获取函数。这个命名空间中的函数实际上使用 Clatrix 库来表示矩阵及其操作。

假设我们需要实现一个人工神经网络(ANN)来模拟逻辑异或(XOR)门。样本数据仅仅是异或门的真值表,可以表示为一个向量,如下所示:

;; truth table for XOR logic gate
(def sample-data [[[0 0] [0]]
                  [[0 1] [1]]
                  [[1 0] [1]]
                  [[1 1] [0]]])

在前面定义的向量 sample-data 中,每个元素本身也是一个向量,包含异或门的输入和输出值。我们将使用这个向量作为我们的训练数据来构建 ANN。这本质上是一个分类问题,我们将使用 ANN 来模拟它。在抽象意义上,ANN 应该能够执行二进制和多类分类。我们可以定义 ANN 的协议如下:

(defprotocol NeuralNetwork
  (run        [network inputs])
  (run-binary [network inputs])
  (train-ann  [network samples]))

前述代码中定义的 NeuralNetwork 协议有三个函数。train-ann 函数可以用来训练 ANN,并需要一些样本数据。runrun-binary 函数可以用于此 ANN 来执行多类和二分类,分别。runrun-binary 函数都需要一组输入值。

反向传播算法的第一步是初始化 ANN 突触的权重。我们可以使用 randmatrix 函数生成这些权重作为矩阵,如下所示:

(defn rand-list
  "Create a list of random doubles between 
  -epsilon and +epsilon."
  [len epsilon]
  (map (fn [x] (- (rand (* 2 epsilon)) epsilon))
         (range 0 len)))

(defn random-initial-weights
  "Generate random initial weight matrices for given layers.
  layers must be a vector of the sizes of the layers."
  [layers epsilon]
  (for [i (range 0 (dec (length layers)))]
    (let [cols (inc (get layers i))
          rows (get layers (inc i))]
      (matrix (rand-list (* rows cols) epsilon) cols))))

前述代码中显示的 rand-list 函数在 epsilon 的正负范围内创建一个随机元素列表。如我们之前所述,我们选择这个范围来打破权重矩阵的对称性。

random-initial-weights 函数为 ANN 的不同层生成多个权重矩阵。如前述代码中定义的,layers 参数必须是一个向量,包含 ANN 各层的尺寸。对于一个输入层有两个节点、隐藏层有三个节点、输出层有一个节点的 ANN,我们将 layers 作为 [2 3 1] 传递给 random-initial-weights 函数。每个权重矩阵的列数等于输入的数量,行数等于 ANN 下一个层的节点数。我们设置给定层的权重矩阵的列数为输入的数量,并额外添加一个用于神经网络偏置的输入。请注意,我们使用了一个稍微不同的 matrix 函数形式。这种形式接受一个单一向量,并将该向量分割成一个矩阵,其列数由该函数的第二个参数指定。因此,传递给这种 matrix 函数的向量必须包含 (* rows cols) 个元素,其中 rowscols 分别是权重矩阵的行数和列数。

由于我们需要将 sigmoid 函数应用于 ANN 中某一层的所有激活,我们必须定义一个函数,该函数将对给定矩阵中的所有元素应用 sigmoid 函数。我们可以使用 incanter.core 命名空间中的 divplusexpminus 函数来实现这样的函数,如下所示:

(defn sigmoid
  "Apply the sigmoid function 1/(1+exp(-z)) to all 
  elements in the matrix z."
  [z]
  (div 1 (plus 1 (exp (minus z)))))

注意

注意,所有之前定义的函数都在给定矩阵的所有元素上执行相应的算术运算,并返回一个新的矩阵。

我们还需要在 ANN 的每一层中隐式地添加一个偏置节点。这可以通过围绕 bind-rows 函数进行操作来实现,该函数向矩阵添加一行,如下所示:

(defn bind-bias
  "Add the bias input to a vector of inputs."
  [v]
  (bind-rows [1] v))

由于偏置值始终为 1,我们将元素行 [1] 指定给 bind-rows 函数。

使用之前定义的函数,我们可以实现前向传播。我们本质上需要在人工神经网络(ANN)中两个层之间给定突触的权重相乘,然后对每个生成的激活值应用 sigmoid 函数,如下面的代码所示:

(defn matrix-mult
  "Multiply two matrices and ensure the result is also a matrix."
  [a b]
  (let [result (mmult a b)]
    (if (matrix? result)
      result
      (matrix [result]))))

(defn forward-propagate-layer
  "Calculate activations for layer l+1 given weight matrix 
  of the synapse between layer l and l+1 and layer l activations."
  [weights activations]
  (sigmoid (matrix-mult weights activations)))

(defn forward-propagate
  "Propagate activation values through a network's
  weight matrix and return output layer activation values."
  [weights input-activations]
  (reduce #(forward-propagate-layer %2 (bind-bias %1))
          input-activations weights))

在前面的代码中,我们首先定义了一个matrix-mult函数,该函数执行矩阵乘法并确保结果是矩阵。请注意,为了定义matrix-mult,我们使用mmult函数而不是mult函数,后者用于乘以两个相同大小的矩阵中的对应元素。

使用matrix-multsigmoid函数,我们可以实现 ANN 中两个层之间的前向传播步骤。这通过forward-propagate-layer函数完成,该函数简单地乘以代表 ANN 中两个层之间突触权重的矩阵和输入激活值,同时确保返回的值始终是一个矩阵。为了将给定的一组值通过 ANN 的所有层传播,我们必须添加一个偏置输入,并对每个层应用forward-propagate-layer函数。这可以通过在forward-propagate函数中定义的forward-propagate-layer函数的闭包上使用reduce函数来简洁地完成。

虽然forward-propagate函数可以确定 ANN 的输出激活,但我们实际上需要 ANN 中所有节点的激活来进行反向传播。我们可以通过将reduce函数转换为递归函数并引入一个累加器变量来存储 ANN 中每一层的激活来实现这一点。在下面的代码中定义的forward-propagate-all-activations函数实现了这个想法,并使用loop形式递归地应用forward-propagate-layer函数:

(defn forward-propagate-all-activations
  "Propagate activation values through the network 
  and return all activation values for all nodes."
  [weights input-activations]
  (loop [all-weights     weights
         activations     (bind-bias input-activations)
         all-activations [activations]]
    (let [[weights
           & all-weights']  all-weights
           last-iter?       (empty? all-weights')
           out-activations  (forward-propagate-layer
                             weights activations)
           activations'     (if last-iter? out-activations
                                (bind-bias out-activations))
           all-activations' (conj all-activations activations')]
      (if last-iter? all-activations'
          (recur all-weights' activations' all-activations')))))

在前面的代码中定义的forward-propagate-all-activations函数需要 ANN 中所有节点的权重和输入值作为激活值通过 ANN。我们首先使用bind-bias函数将偏置输入添加到 ANN 的输入激活中。然后我们将此值存储在一个累加器中,即变量all-activations,作为一个包含 ANN 中所有激活的向量。然后,forward-propagate-layer函数被应用于 ANN 各个层的权重矩阵,每次迭代都会向 ANN 中相应层的输入激活添加一个偏置输入。

注意

注意,在最后一次迭代中我们不添加偏置输入,因为它计算 ANN 的输出层。因此,forward-propagate-all-activations函数通过 ANN 对输入值进行前向传播,并返回 ANN 中每个节点的激活值。注意,这个向量中的激活值是按照 ANN 层的顺序排列的。

我们现在将实现反向传播学习算法的反向传播阶段。首先,我们必须实现一个函数来从方程https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_61.jpg计算误差项https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_49.jpg。我们将借助以下代码来完成这项工作:

(defn back-propagate-layer
  "Back propagate deltas (from layer l+1) and 
  return layer l deltas."
  [deltas weights layer-activations]
  (mult (matrix-mult (trans weights) deltas)
        (mult layer-activations (minus 1 layer-activations))))

在前面的代码中定义的back-propagate-layer函数计算 ANN 中突触层l的误差或 delta 值,这些误差或 delta 值来自层的权重和 ANN 中下一层的 delta 值。

注意

注意,我们仅使用矩阵乘法通过matrix-mult函数计算术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_62.jpg。所有其他乘法操作都是矩阵的逐元素乘法,这使用mult函数完成。

实质上,我们必须从输出层通过 ANN 的各个隐藏层应用到输入层,以产生 ANN 中每个节点的 delta 值。然后,这些 delta 值可以添加到节点的激活中,从而产生通过调整 ANN 中节点权重所需的梯度值。我们可以以类似于forward-propagate-all-activations函数的方式来做这件事,即通过递归地应用back-propagate-layer函数到 ANN 的各个层。当然,我们必须以相反的顺序遍历 ANN 的层,即从输出层,通过隐藏层,到输入层。我们将借助以下代码来完成这项工作:

(defn calc-deltas
  "Calculate hidden deltas for back propagation.
  Returns all deltas including output-deltas."
  [weights activations output-deltas]
  (let [hidden-weights     (reverse (rest weights))
        hidden-activations (rest (reverse (rest activations)))]
    (loop [deltas          output-deltas
           all-weights     hidden-weights
           all-activations hidden-activations
           all-deltas      (list output-deltas)]
      (if (empty? all-weights) all-deltas
        (let [[weights
               & all-weights']      all-weights
               [activations
                & all-activations'] all-activations
              deltas'        (back-propagate-layer
                               deltas weights activations)
              all-deltas'    (cons (rest deltas') 
                                    all-deltas)]
          (recur deltas' all-weights' 
                 all-activations' all-deltas'))))))

calc-deltas函数确定 ANN 中所有感知器节点的 delta 值。为此计算,不需要输入和输出激活。只需要与hidden-activations变量绑定的隐藏激活来计算 delta 值。此外,输入层的权重被跳过,因为它们绑定到hidden-weights变量。然后calc-deltas函数将back-propagate-layer函数应用于 ANN 中每个突触层的所有权重矩阵,从而确定矩阵中所有节点的 delta 值。请注意,我们不将偏置节点的 delta 值添加到计算出的 delta 集合中。这是通过在给定突触层的计算 delta 值上使用rest函数(rest deltas')来完成的,因为第一个 delta 是给定层中偏置输入的 delta。

根据定义,给定突触层https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_57.jpg的梯度向量项是通过乘以矩阵https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_63.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_64.jpg来确定的,这些矩阵分别表示下一层的 delta 值和给定层的激活。我们将借助以下代码来完成这项工作:

(defn calc-gradients
  "Calculate gradients from deltas and activations."
  [deltas activations]
  (map #(mmult %1 (trans %2)) deltas activations))

前面代码中显示的calc-gradients函数是对术语https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_65.jpg的简洁实现。由于我们将处理一系列 delta 和激活项,我们使用map函数将前面的等式应用于 ANN 中相应的 delta 和激活项。使用calc-deltascalc-gradient函数,我们可以确定给定训练样本中 ANN 所有节点权重中的总误差。我们将通过以下代码来完成这项工作:

(defn calc-error
  "Calculate deltas and squared error for given weights."
  [weights [input expected-output]]
  (let [activations    (forward-propagate-all-activations 
                        weights (matrix input))
        output         (last activations)
        output-deltas  (minus output expected-output)
        all-deltas     (calc-deltas 
                        weights activations output-deltas)
        gradients      (calc-gradients all-deltas activations)]
    (list gradients
          (sum (pow output-deltas 2)))))

前面代码中定义的calc-error函数需要两个参数——ANN 中突触层的权重矩阵和一个样本训练值,这显示为[输入期望输出]。首先使用forward-propagate-all-activations函数计算 ANN 中所有节点的激活值,然后计算最后一层的 delta 值,即期望输出值与 ANN 产生的实际输出值之间的差值。ANN 计算得出的输出值仅仅是 ANN 产生的最后一个激活值,如前面代码中所示为(last activations)。使用计算出的激活值,通过calc-deltas函数确定所有感知器节点的 delta 值。这些 delta 值随后用于通过calc-gradients函数确定 ANN 中各层的权重梯度。对于给定的样本值,ANN 的均方误差(MSE)也通过添加输出层 delta 值的平方来计算。

对于 ANN 中某一层的给定权重矩阵,我们必须初始化该层的梯度为一个与权重矩阵具有相同维度的矩阵,并且梯度矩阵中的所有元素都必须设置为0。这可以通过使用dim函数的组合来实现,该函数返回矩阵的大小为一个向量,以及matrix函数的变体形式,如下面的代码所示:

(defn new-gradient-matrix
  "Create accumulator matrix of gradients with the
  same structure as the given weight matrix
  with all elements set to 0."
  [weight-matrix]
  (let [[rows cols] (dim weight-matrix)]
    (matrix 0 rows cols)))

在前面代码中定义的new-gradient-matrix函数中,matrix函数期望一个值、行数和列数来初始化一个矩阵。此函数生成一个具有与提供的权重矩阵相同结构的初始化梯度矩阵。

我们现在实现calc-gradients-and-error函数,以便在一系列权重矩阵和样本值上应用calc-error函数。我们基本上需要将calc-error函数应用于每个样本,并累积梯度值和均方误差(MSE)的总和。然后我们计算这些累积值的平均值,以返回给定样本值和权重矩阵的梯度矩阵和总 MSE。我们将通过以下代码来完成这项工作:

(defn calc-gradients-and-error' [weights samples]
  (loop [gradients   (map new-gradient-matrix weights)
         total-error 1
         samples     samples]
    (let [[sample
           & samples']     samples
           [new-gradients
            squared-error] (calc-error weights sample)
            gradients'     (map plus new-gradients gradients)
            total-error'   (+ total-error squared-error)]
      (if (empty? samples')
        (list gradients' total-error')
        (recur gradients' total-error' samples')))))

(defn calc-gradients-and-error
  "Calculate gradients and MSE for sample
  set and weight matrix."
  [weights samples]
  (let [num-samples   (length samples)
        [gradients
         total-error] (calc-gradients-and-error'
                       weights samples)]
    (list
      (map #(div % num-samples) gradients)    ; gradients
      (/ total-error num-samples))))          ; MSE

在前面的代码中定义的calc-gradients-and-error函数依赖于calc-gradients-and-error'辅助函数。calc-gradients-and-error'函数初始化梯度矩阵,执行calc-error函数的应用,并累计计算出的梯度值和 MSE。calc-gradients-and-error函数简单地计算从calc-gradients-and-error'函数返回的累计梯度矩阵和 MSE 的平均值。

现在,我们实现中唯一缺少的部分是使用计算出的梯度修改 ANN 中节点的权重。简而言之,我们必须反复更新权重,直到观察到 MSE 的收敛。这实际上是对 ANN 节点应用的一种梯度下降形式。我们现在将实现这种梯度下降的变体,通过反复修改 ANN 中节点的权重来训练 ANN,如下面的代码所示:

(defn gradient-descent-complete?
  "Returns true if gradient descent is complete."
  [network iter mse]
  (let [options (:options network)]
    (or (>= iter (:max-iters options))
        (< mse (:desired-error options)))))

在前面的代码中定义的gradient-descent-complete?函数简单地检查梯度下降的终止条件。这个函数假设 ANN,作为一个网络,是一个包含:options关键字的映射或记录。这个键的值反过来又是一个包含 ANN 各种配置选项的映射。gradient-descent-complete?函数检查 ANN 的总均方误差(MSE)是否小于由:desired-error选项指定的期望 MSE。此外,我们还添加了另一个条件来检查执行的迭代次数是否超过了由:max-iters选项指定的最大迭代次数。

现在,我们将为多层感知器人工神经网络(ANNs)实现一个梯度下降函数。在这个实现中,权重的变化是通过梯度下降算法提供的step函数来计算的。然后,这些计算出的变化简单地添加到 ANN 的突触层现有权重中。我们将使用以下代码帮助实现多层感知器 ANN 的梯度下降函数:

(defn apply-weight-changes
  "Applies changes to corresponding weights."
  [weights changes]
  (map plus weights changes))

(defn gradient-descent
  "Perform gradient descent to adjust network weights."
  [step-fn init-state network samples]
  (loop [network network
         state init-state
         iter 0]
    (let [iter     (inc iter)
          weights  (:weights network)
          [gradients
           mse]    (calc-gradients-and-error weights samples)]
      (if (gradient-descent-complete? network iter mse)
        network
        (let [[changes state] (step-fn network gradients state)
              new-weights     (apply-weight-changes 
                               weights changes)
              network         (assoc network 
                              :weights new-weights)]
          (recur network state iter))))))

在前面的代码中定义的apply-weight-changes函数简单地添加了 ANN 的权重和计算出的权重变化。gradient-descent函数需要一个step函数(指定为step-fn)、ANN 的初始状态、ANN 本身以及用于训练 ANN 的样本数据。这个函数必须从 ANN、初始梯度矩阵和 ANN 的初始状态计算权重变化。step-fn函数还返回 ANN 的更改状态。然后,使用apply-weight-changes函数更新 ANN 的权重,并且这个迭代过程会重复执行,直到gradient-descent-complete?函数返回true。ANN 的权重由network映射中的:weights关键字指定。然后,简单地通过覆盖由:weights关键字指定的network上的值来更新这些权重。

在反向传播算法的上下文中,我们需要指定 ANN 必须训练的学习率和学习动量。这些参数用于确定 ANN 中节点权重的变化。然后必须指定一个实现此计算的函数作为 gradient-descent 函数的 step-fn 参数,如下面的代码所示:

(defn calc-weight-changes
  "Calculate weight changes:
  changes = learning rate * gradients + 
            learning momentum * deltas."
  [gradients deltas learning-rate learning-momentum]
  (map #(plus (mult learning-rate %1)
              (mult learning-momentum %2))
       gradients deltas))

(defn bprop-step-fn [network gradients deltas]
  (let [options             (:options network)
        learning-rate       (:learning-rate options)
        learning-momentum   (:learning-momentum options)
        changes             (calc-weight-changes
                             gradients deltas
                             learning-rate learning-momentum)]
    [(map minus changes) changes]))

(defn gradient-descent-bprop [network samples]
  (let [gradients (map new-gradient-matrix (:weights network))]
    (gradient-descent bprop-step-fn gradients
                      network samples)))

在前面的代码中定义的 calc-weight-changes 函数根据给定 ANN 中某一层的梯度值和 delta 值计算权重的变化,称为 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_66.jpgbprop-step-fn 函数从表示为 network 的 ANN 中提取学习率和学习动量参数,并使用 calc-weight-changes 函数。由于权重将由 gradient-descent 函数添加变化,我们使用 minus 函数以负值返回权重的变化。

gradient-descent-bprop 函数简单地初始化 ANN 给定权重的梯度矩阵,并通过指定 bprop-step-fn 作为要使用的 step 函数来调用 gradient-descent 函数。使用 gradient-descent-bprop 函数,我们可以实现我们之前定义的抽象 NeuralNetwork 协议,如下所示:

(defn round-output
  "Round outputs to nearest integer."
  [output]
  (mapv #(Math/round ^Double %) output))

(defrecord MultiLayerPerceptron [options]
  NeuralNetwork

  ;; Calculates the output values for the given inputs.
  (run [network inputs]
    (let [weights (:weights network)
          input-activations (matrix inputs)]
      (forward-propagate weights input-activations)))

  ;; Rounds the output values to binary values for
  ;; the given inputs.
  (run-binary [network inputs]
    (round-output (run network inputs)))

  ;; Trains a multilayer perceptron ANN from sample data.
  (train-ann [network samples]
    (let [options         (:options network)
          hidden-neurons  (:hidden-neurons options)
          epsilon         (:weight-epsilon options)
          [first-in
           first-out]     (first samples)
          num-inputs      (length first-in)
          num-outputs     (length first-out)
          sample-matrix   (map #(list (matrix (first %)) 
                                      (matrix (second %)))
                               samples)
          layer-sizes     (conj (vec (cons num-inputs 
                                           hidden-neurons))
                                num-outputs)
          new-weights     (random-initial-weights 
                           layer-sizes epsilon)
          network         (assoc network :weights new-weights)]
      (gradient-descent-bprop network sample-matrix))))

在前面的代码中定义的 MultiLayerPerceptron 记录使用 gradient-descent-bprop 函数训练一个多层感知器人工神经网络 (ANN)。train-ann 函数首先从指定的 ANN 选项映射中提取隐藏神经元的数量和常数 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_43.jpg 的值。ANN 中各种突触层的尺寸首先从样本数据中确定,并绑定到 layer-sizes 变量。然后使用 random-initial-weights 函数初始化 ANN 的权重,并在 network 记录中使用 assoc 函数更新。最后,通过指定 bprop-step-fn 作为要使用的 step 函数,调用 gradient-descent-bprop 函数来使用反向传播学习算法训练 ANN。

MultiLayerPerceptron 记录定义的 ANN 还实现了 NeuralNetwork 协议中的两个其他函数,runrun-binaryrun 函数使用 forward-propagate 函数确定训练好的 MultiLayerPerceptron ANN 的输出值。run-binary 函数简单地将 run 函数为给定输入值集返回的输出值四舍五入。

使用 MultiLayerPerceptron 记录创建的 ANN 需要一个包含我们可以为 ANN 指定的各种选项的单个 options 参数。我们可以如下定义此类 ANN 的默认选项:

(def default-options
  {:max-iters 100
   :desired-error 0.20
   :hidden-neurons [3]
   :learning-rate 0.3
   :learning-momentum 0.01
   :weight-epsilon 50})

(defn train [samples]
  (let [network (MultiLayerPerceptron. default-options)]
    (train-ann network samples)))

default-options 变量定义的映射包含以下键,这些键指定了 MultiLayerPerceptron ANN 的选项:

  • :max-iter: 此键指定运行 gradient-descent 函数的最大迭代次数。

  • :desired-error:此变量指定 ANN 中期望或可接受的均方误差(MSE)。

  • :hidden-neurons:此变量指定网络中隐藏神经节点的数量。值[3]表示一个包含三个神经元的单个隐藏层。

  • :learning-rate:learning-momentum:这些键指定反向传播学习算法权重更新阶段的学习率和学习动量。

  • :epsilon:此变量指定random-initial-weights函数用于初始化 ANN 权重的常数。

我们还定义了一个简单的辅助函数train,用于创建MultiLayerPerceptron类型的 ANN,并使用train-ann函数和由samples参数指定的样本数据来训练 ANN。现在,我们可以根据sample-data变量指定的训练数据创建一个训练好的 ANN,如下所示:

user> (def MLP (train sample-data))
#'user/MLP

我们可以使用训练好的 ANN 来预测一些输入值的输出。由MLP定义的 ANN 生成的输出与 XOR 门的输出非常接近,如下所示:

user> (run-binary MLP  [0 1])
[1]
user> (run-binary MLP  [1 0])
[1]

然而,训练好的 ANN 对于某些输入集产生了不正确的输出,如下所示:

user> (run-binary MLP  [0 0])
[0]
user> (run-binary MLP  [1 1]) ;; incorrect output generated
[1]

为了提高训练好的 ANN 的准确性,我们可以实施几种措施。首先,我们可以使用 ANN 的权重矩阵来正则化计算的梯度。这种修改将使先前的实现产生明显的改进。我们还可以增加要执行的最大迭代次数。我们还可以调整算法,通过调整学习率、学习动量和 ANN 中的隐藏节点数量来提高性能。这些修改被跳过,因为它们需要由读者来完成。

Enclog库(github.com/jimpil/enclog)是一个 Clojure 包装库,用于机器学习算法和 ANN 的Encog库。Encog 库(github.com/encog)有两个主要实现:一个在 Java 中,一个在.NET 中。我们可以使用 Enclog 库轻松生成定制的 ANN 来模拟监督和非监督机器学习问题。

注意

可以通过在project.clj文件中添加以下依赖项将 Enclog 库添加到 Leiningen 项目中:

[org.encog/encog-core "3.1.0"]
[enclog "0.6.3"]

注意,Enclog 库需要 Encog Java 库作为依赖项。

对于接下来的示例,命名空间声明应类似于以下声明:

(ns my-namespace
  (:use [enclog nnets training]))

我们可以使用enclog.nnets命名空间中的neural-patternnetwork函数从 Enclog 库创建一个 ANN。neural-pattern函数用于指定 ANN 的神经网络模型。network函数接受从neural-pattern函数返回的神经网络模型,并创建一个新的 ANN。我们可以根据指定的神经网络模型向network函数提供几个选项。以下是一个前馈多层感知器网络的定义:

(def mlp (network (neural-pattern :feed-forward)
                  :activation :sigmoid
                  :input      2
                  :output     1
                  :hidden     [3]))

对于前馈神经网络,我们可以通过将:activation键指定给network函数来指定激活函数。在我们的例子中,我们使用了 sigmoid 函数,它被指定为:sigmoid作为 ANN 节点的激活函数。我们还使用:input:output:hidden键指定了 ANN 的输入、输出和隐藏层的节点数量。

要使用一些样本数据训练由network函数创建的 ANN,我们使用enclog.training命名空间中的trainertrain函数。用于训练 ANN 的学习算法必须作为trainer函数的第一个参数指定。对于反向传播算法,此参数是:back-prop关键字。trainer函数返回的值代表一个 ANN 以及用于训练 ANN 的学习算法。然后使用train函数在 ANN 上实际运行指定的训练算法。我们将通过以下代码来完成这项工作:

(defn train-network [network data trainer-algo]
  (let [trainer (trainer trainer-algo
                         :network network
                         :training-set data)]
    (train trainer 0.01 1000 []))) ;; 0.01 is the expected error

在前面的代码中定义的train-network函数接受三个参数。第一个参数是由network函数创建的 ANN,第二个参数是用于训练 ANN 的训练数据,第三个参数指定了 ANN 必须通过的学习算法。如前所述的代码所示,我们可以使用关键字参数:network:training-set将 ANN 和训练数据指定给trainer函数。然后使用train函数通过样本数据在 ANN 上运行训练算法。我们可以将 ANN 中期望的错误和训练算法的最大迭代次数作为train函数的第一个和第二个参数指定。在前面的例子中,期望的错误是0.01,最大迭代次数是 1000。传递给train函数的最后一个参数是一个指定 ANN 行为的向量,我们通过传递一个空向量来忽略它。

在对人工神经网络(ANN)运行训练算法时使用的训练数据可以通过使用 Enclog 的data函数来创建。例如,我们可以使用data函数创建一个用于逻辑异或门(XOR gate)的训练数据,如下所示:

(def dataset
  (let [xor-input [[0.0 0.0] [1.0 0.0] [0.0 1.0] [1.0 1.0]]
        xor-ideal [[0.0]     [1.0]     [1.0]     [0.0]]]
        (data :basic-dataset xor-input xor-ideal)))

data函数需要将数据类型作为函数的第一个参数,随后是训练数据的输入和输出值作为向量。在我们的例子中,我们将使用:basic-dataset:basic参数。:basic-dataset关键字可以用来创建训练数据,而:basic关键字可以用来指定一组输入值。

使用由dataset变量定义的数据和train-network函数,我们可以训练 ANN 的MLP来模拟异或门的输出,如下所示:

user> (def MLP (train-network mlp dataset :back-prop))
Iteration # 1 Error: 26.461526% Target-Error: 1.000000%
Iteration # 2 Error: 25.198031% Target-Error: 1.000000%
Iteration # 3 Error: 25.122343% Target-Error: 1.000000%
Iteration # 4 Error: 25.179218% Target-Error: 1.000000%
...
...
Iteration # 999 Error: 3.182540% Target-Error: 1.000000%
Iteration # 1,000 Error: 3.166906% Target-Error: 1.000000%
#'user/MLP

如前述输出所示,训练好的 ANN 的错误率约为 3.16%。现在我们可以使用训练好的 ANN 来预测一组输入值的输出。为此,我们使用 Java 的computegetData方法,分别由.compute.getData指定。我们可以定义一个简单的辅助函数来调用.compute方法,为输入值向量计算结果,并将输出四舍五入到二进制值,如下所示:

(defn run-network [network input]
  (let [input-data (data :basic input)
        output     (.compute network input-data)
        output-vec (.getData output)]
    (round-output output-vec)))

我们现在可以使用run-network函数,通过输入值向量来测试训练好的 ANN,如下所示:

user> (run-network MLP [1 1])
[0]
user> (run-network MLP [1 0])
[1]
user> (run-network MLP [0 1])
[1]
user> (run-network MLP [0 0])
[0]

如前述代码所示,由MLP表示的训练好的 ANN 完全符合 XOR 门的行为。

总结来说,Enclog 库为我们提供了一组强大的函数,可以用来构建 ANN。在前面的例子中,我们探讨了前馈多层感知器模型。该库还提供了其他几种 ANN 模型,例如自适应共振理论ART)、自组织映射SOM)和 Elman 网络。Enclog 库还允许我们自定义特定神经网络模型中节点的激活函数。在我们的例子中,我们使用了 sigmoid 函数。库还支持几种数学函数,如正弦、双曲正切、对数和线性函数。Enclog 库还支持一些机器学习算法,可用于训练 ANN。

理解循环神经网络

我们现在将关注点转向循环神经网络,并研究一个简单的循环神经网络模型。Elman 神经网络是一个简单的循环 ANN,具有单个输入、输出和隐藏层。还有一个额外的上下文层的神经网络节点。Elman 神经网络用于模拟监督和无监督机器学习问题中的短期记忆。Enclog 库确实包括对 Elman 神经网络的支持,我们将演示如何使用 Enclog 库构建 Elman 神经网络。

Elman 神经网络的上下文层从 ANN 的隐藏层接收无权重的输入。这样,ANN 可以记住我们使用隐藏层生成的先前值,并使用这些值来影响预测值。因此,上下文层充当 ANN 的短期记忆。以下图示可以说明 Elman 神经网络:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_67.jpg

如前述图所示,Elman 网络的结构类似于前馈多层感知器 ANN。Elman 网络向 ANN 添加了一个额外的上下文层神经网络。前述图中的 Elman 网络接受两个输入并产生两个输出。Elman 网络的输入和隐藏层添加了一个额外的偏置输入,类似于多层感知器。隐藏层神经元的激活直接馈送到两个上下文节点https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_68.jpghttps://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_69.jpg。这些上下文节点中存储的值随后被 ANN 隐藏层的节点使用,以回忆先前的激活并确定新的激活值。

我们可以创建一个 Elman 网络,将:elman关键字指定给 Enclog 库中的neural-pattern函数,如下所示:

(def elman-network (network (neural-pattern :elman)
                             :activation :sigmoid
                             :input      2
                             :output     1
                             :hidden     [3]))

要训练 Elman 网络,我们可以使用弹性传播算法(更多信息,请参阅Empirical Evaluation of the Improved Rprop Learning Algorithm)。此算法也可以用于训练 Enclog 支持的其他循环网络。有趣的是,弹性传播算法还可以用于训练前馈网络。此算法的性能也显著优于反向传播学习算法。尽管此算法的完整描述超出了本书的范围,但鼓励读者了解更多关于此学习算法的信息。弹性传播算法指定为train-network函数的:resilient-prop关键字,这是我们之前定义的。我们可以使用train-network函数和dataset变量来训练 Elman 神经网络,如下所示:

user> (def EN (train-network elman-network dataset 
                             :resilient-prop))
Iteration # 1 Error: 26.461526% Target-Error: 1.000000%
Iteration # 2 Error: 25.198031% Target-Error: 1.000000%
Iteration # 3 Error: 25.122343% Target-Error: 1.000000%
Iteration # 4 Error: 25.179218% Target-Error: 1.000000%
...
...
Iteration # 99 Error: 0.979165% Target-Error: 1.000000%
#'user/EN

如前述代码所示,与反向传播算法相比,弹性传播算法需要相对较少的迭代次数。现在我们可以使用这个训练好的 ANN 来模拟一个 XOR 门,就像我们在上一个例子中所做的那样。

总结来说,循环神经网络模型和训练算法是其他有用的模型,可以用于使用 ANN 来建模分类或回归问题。

构建 SOMs

SOM(发音为ess-o-em)是另一个有趣的 ANN 模型,它对无监督学习很有用。SOMs 被用于多个实际应用中,如手写识别和图像识别。当我们讨论第七章中的聚类时,我们也将重新审视 SOMs,聚类数据

在无监督学习中,样本数据不包含预期的输出值,人工神经网络(ANN)必须完全依靠自身识别和匹配输入数据中的模式。SOM 用于竞争学习,这是无监督学习的一个特殊类别,其中 ANN 输出层的神经元相互竞争以激活。激活的神经元决定了 ANN 的最终输出值,因此,激活的神经元也被称为获胜神经元

神经生物学研究表明,大脑接收到的不同感官输入以有序的模式映射到大脑大脑皮层的相应区域。因此,处理密切相关操作的神经元被保持在一起。这被称为拓扑形成原理,而 SOM 实际上是基于这种行为构建的。

自组织映射(SOM)本质上是将具有大量维度的输入数据转换为一个低维离散映射。通过将神经元放置在这个映射的节点上对 SOM 进行训练。SOM 的内部映射通常有一到两个维度。SOM 中的神经元会选择性地调整到输入值中的模式。当 SOM 中的某个特定神经元被激活以响应特定的输入模式时,其邻近的神经元往往会变得更加兴奋,并且更倾向于调整到输入值中的模式。这种行为被称为一组神经元的横向交互。因此,SOM 可以在输入数据中找到模式。当在输入数据集中找到相似模式时,SOM 会识别这个模式。SOM 中神经节点层的结构可以描述如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_70.jpg

自组织映射(SOM)有一个输入层和一个计算层,如前图所示。计算层也被称为 SOM 的特征图。输入节点将输入值映射到计算层中的几个神经元。计算层中的每个节点都有其输出连接到其邻近节点,并且每个连接都有一个与之相关的权重。这些权重被称为特征图的连接权重。SOM 通过调整其计算层中节点的连接权重来记住输入值中的模式。

自组织映射(SOM)的自组织过程可以描述如下:

  1. 连接权重最初被初始化为随机值。

  2. 对于每个输入模式,计算层中的神经节点使用判别函数计算一个值。然后,这些值被用来决定获胜的神经元。

  3. 具有最小判别函数值的神经元被选中,并且对其周围神经元的连接权重进行修改,以便激活输入数据中的相似模式。

必须修改权重,使得对于输入中的给定模式,判别函数对邻近节点的值减少。因此,获胜节点及其周围节点对于输入数据中的相似模式产生更高的输出或激活值。权重调整的量取决于指定给训练算法的学习率。

对于输入数据中的给定维度数 D,判别函数可以正式定义为以下内容:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_71.jpg

在前面的方程中,术语 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_72.jpg 是 SOM 中 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_73.jpg 神经元的权重向量。向量 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_72.jpg 的长度等于连接到 https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/clj-ml/img/4351OS_04_73.jpg 神经元的神经元数量。

一旦我们在 SOM 中选定了获胜神经元,我们必须选择获胜神经元的邻近神经元。我们必须调整这些邻近神经元的权重以及获胜神经元的权重。可以使用各种方案来选择获胜神经元邻近节点。在最简单的情况下,我们可以选择一个邻近神经元。

我们可以改用 bubble 函数或 radial bias 函数来选择围绕获胜神经元的一组邻近神经元(更多信息,请参阅 Multivariable functional interpolation and adaptive networks)。

要训练一个 SOM,我们必须在训练算法中执行以下步骤:

  1. 将计算层中节点的权重设置为随机值。

  2. 从训练数据中选择一个样本输入模式。

  3. 找到所选输入模式集的获胜神经元。

  4. 更新获胜神经元及其周围节点的权重。

  5. 对于训练数据中的所有样本,重复步骤 2 到 4。

Enclog 库支持 SOM 神经网络模型和训练算法。我们可以按照以下方式从 Enclog 库创建和训练一个 SOM:

(def som (network (neural-pattern :som) :input 4 :output 2))

(defn train-som [data]
  (let [trainer (trainer :basic-som :network som
                         :training-set data
                         :learning-rate 0.7
                         :neighborhood-fn 
          (neighborhood-F :single))]
    (train trainer Double/NEGATIVE_INFINITY 10 [])))

在前面的代码中出现的 som 变量代表一个自组织映射(SOM)。可以使用 train-som 函数来训练 SOM。SOM 的训练算法指定为 :basic-som。注意,我们使用 :learning-rate 键将学习率指定为 0.7

在前面的代码中传递给 trainer 函数的 :neighborhood-fn 键指定了对于给定的一组输入值,我们在 SOM 中如何选择获胜节点的邻近节点。我们指定必须使用 (neighborhood-F :single) 来选择获胜节点的单个邻近节点。我们还可以指定不同的邻域函数。例如,我们可以指定 bubble 函数为 :bubble 或径向基函数为 :rbf

我们可以使用train-som函数使用一些输入模式来训练 SOM。请注意,用于训练 SOM 的训练数据将没有任何输出值。SOM 必须自行识别输入数据中的模式。一旦 SOM 被训练,我们可以使用 Java 的classify方法来检测输入中的模式。对于以下示例,我们只提供两个输入模式来训练 SOM:

(defn train-and-run-som []
  (let [input [[-1.0, -1.0, 1.0, 1.0 ]
               [1.0, 1.0, -1.0, -1.0]]
        input-data (data :basic-dataset input nil) ;no ideal data
        SOM        (train-som input-data)
        d1         (data :basic (first input))
        d2         (data :basic (second input))]
    (println "Pattern 1 class:" (.classify SOM d1))
    (println "Pattern 2 class:" (.classify SOM d2))
    SOM))

我们可以运行前面代码中定义的train-and-run-som函数,并观察到 SOM 将训练数据中的两个输入模式识别为两个不同的类别,如下所示:

user> (train-and-run-som)
Iteration # 1 Error: 2.137686% Target-Error: NaN
Iteration # 2 Error: 0.641306% Target-Error: NaN
Iteration # 3 Error: 0.192392% Target-Error: NaN
...
...
Iteration # 9 Error: 0.000140% Target-Error: NaN
Iteration # 10 Error: 0.000042% Target-Error: NaN
Pattern 1 class: 1
Pattern 2 class: 0
#<SOM org.encog.neural.som.SOM@19a0818>

总之,SOMs 是处理无监督学习问题的优秀模型。此外,我们可以轻松地使用 Enclog 库构建 SOM 来模拟这些问题。

摘要

我们在本章中探索了几种有趣的 ANN 模型。这些模型可以应用于解决监督学习和无监督机器学习问题。以下是我们所涵盖的一些其他要点:

  • 我们探讨了 ANN 的必要性和它们的广泛类型,即前馈和循环 ANN。

  • 我们研究了多层感知器 ANN 及其用于训练此 ANN 的反向传播算法。我们还提供了一个使用矩阵和矩阵运算在 Clojure 中实现的简单反向传播算法。

  • 我们介绍了 Enclog 库,该库可用于构建 ANN。这个库可以用于模拟监督学习和无监督机器学习问题。

  • 我们探索了循环 Elman 神经网络,它可以用于在相对较少的迭代次数中产生具有小误差的 ANN。我们还描述了如何使用 Enclog 库创建和训练这样的 ANN。

  • 我们介绍了 SOMs,这些神经网络可以应用于无监督学习的领域。我们还描述了如何使用 Enclog 库创建和训练 SOM。

Logo

更多推荐