购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.4 案例学习

本书我们会继续做面向对象设计综合案例——鸢尾花分类器。我们在前面的章节中就在设计这个案例了,在后面的章节中我们还会继续。在这一节中,我们会回顾前面的 UML 图,以帮助描述和总结我们将要构建的软件。我们从第2章开始就添加特性来描绘多种不同的KNN算法。KNN算法有好几种,我们可以通过这个示例来学习类的层级关系。

随着设计不断完善,我们将学习几个设计原则。比较流行的一套设计原则是 SOLID ,它们是:

· S :单一责任原则。一个类应该只有一个责任。这意味着当需求变化的时候,只有一种原因会造成这个类的更改。

· O :开放/关闭原则。类可以被扩展但不应该被更改。

· L :里氏(Liskov)替换原则(以Barbara Liskov命名,她创建了第一个面向对象编程语言,CLU)。任何一个子类都可以替换其父类。这让我们聚焦于类层级中的通用类接口,子类可以形成多态。这个原则正是继承的本质。

· I :接口隔离原则。一个类应该有最小化的接口。这也许是这几个原则中最重要的一个。类应该相对比较小,且各功能独立。

· D :依赖反转原则。这个名词有点儿奇怪。我们需要知道什么是糟糕的依赖关系,这样我们才能把它反转成一个好的关系。编程时,我们希望类能各自独立,这样在做里氏替换的时候不需要改动很多代码。在Python中,这通常意味着在类型提示中引用父类,这样才能确保可以灵活地将其替换为子类。在某些情况下,它还意味着提供参数,以便我们可以在不修改任何代码的情况下进行全局类更改。

我们不会在本章中讨论所有这些原则,因为我们在学习继承,我们的设计会倾向于遵循里氏替换原则。而其他章节将会涉及其他原则。

3.4.1 逻辑视图

图3.3是我们在前面章节的案例学习中为这个案例画的类图。这个图有一个重要遗漏,它没有描述Hyperparameter类中使用的classify算法。

图3.3 类图概览

在第2章中,我们避免深入研究分类算法。这反映了一种常见的设计策略,有时被称为“困难的部分,以后再做”,也被称为“先做容易的部分”。这个策略鼓励尽可能遵循通用设计模式来隔离困难部分。实际上,简单的部分定义了一些边界,可以先有效地隔离困难和未知的部分。

这个分类器是基于 k 个最近邻的算法,也被称为KNN。给定一些已知样本和一个未知样本,我们想要找出 k 个和这个未知样本最近邻;最多最近邻所属的分类就是这个未知样本的分类。这意味着 k 通常是奇数,这样才容易得出结论。我们之前一直在规避这个问题:如何计算距离?

在传统的二维坐标中,我们可以使用样本之间的欧几里得距离。给定一个未知样本的坐标( u x u y )和一个训练样本的坐标( t x t y ),它们之间的欧几里得距离ED2( t u )是:

可视化后是这样的,如图3.4所示。

图3.4 欧几里得距离

我们称它为ED2是因为它是二维的。在我们的案例中,实际上是四维的:萼片长度、萼片宽度、花瓣长度、花瓣宽度。这很难可视化,但是数学公式并不复杂。虽然很难想象,但我们可以写出完整的公式:

虽然很难想象,但我们可以很容易地把二维的情况扩展到四维,甚至更多维。本节后面的一些图仍然以二维为示例,但实际上想表达的是四维数据的计算。

我们可以用一个类实现这个计算。Hyperparameter类将会使用这个ED类的一个实例:

我们使用了math模块的hypot()函数做平方和平方根的运算。我们使用了一个还没定义的父类Distance。我们很确定需要一个父类,不过我们稍后再定义。

欧几里得距离是计算已知样本和未知样本之间距离的多个方法之一。有两个相对比较简单的方法,它们通常可以产生不错的结果,但不需要做复杂的平方根运算:

· 曼哈顿距离 :这是你在一个有方形街区的城市中行走的距离(有点儿像曼哈顿市的部分地区),如图3.5所示。

图3.5 曼哈顿距离

· 切比雪夫距离 :这是各坐标数值差的最大值。以( x 1 y 1 )和( x 2 y 2 )两点为例,其切比雪夫距离为max(|x2-x1|,|y2-y1|)。切比雪夫距离得名于俄罗斯数学家切比雪夫。

对于不同的算法,我们需要定义不同的子类。这意味着我们需要定义一个代表距离的父类。这个父类可以被定义成这样:

这个父类抽象了距离算法的核心方法和参数,但并没有具体的实现。现在我们来实现几个具体的子类。

曼哈顿距离是 x 轴的距离加上 y 轴的距离之和。公式可以用距离的绝对值(写作 )来表示:

这个距离会比欧几里得距离长41%,然后从比较意义上来说基本不会改变比较的结果,因为仍然可以得出比较好的KNN结果。但它不用做平方和平方根运算,因此计算速度更快。

下面是实现了曼哈顿距离的子类:

切比雪夫距离是 x 轴距离或 y 轴距离最大的那个。它可以最小化多个维度的影响:

下面是切比雪夫距离的示意图,它倾向于强化彼此更接近的邻居,如图3.6所示。

图3.6 切比雪夫距离

下面是实现了切比雪夫距离的子类:

不同距离算法对KNN性能的影响可以查阅链接11的论文。这篇论文中包含了54种不同的距离算法。我们案例中的这几种算法统一被称为“Minkowski”度量,因为它们相似并且平等地测量每个轴。在给定一组训练数据的情况下,使用不同距离算法的模型对未知样本可能会产生不同的分类结果。

这也改变了Hyperparameter类的设计:我们现在有两个不同的超参数了。 k 值决定最近邻的数量,而距离算法告诉我们如何计算“最近”距离。这两个参数都是可以改变的,我们需要测试不同的组合,找出对我们的数据来说最好的方案。

我们如何实现各种不同的距离算法?简单说,我们需要为不同的距离算法分别定义子类。上面提到的论文帮我们指定了几种最有用的算法。为了确保我们的设计有效,下面再来看一种距离算法。

3.4.2 另一种距离算法

为了演示添加子类是多么容易,我们定义一个有点儿复杂的距离算法。这就是索伦森距离,也被称为布雷-柯蒂斯距离。如果我们的距离算法类可以处理这些更复杂的公式,我们就有信心它可以处理其他的:

我们通过把每个坐标轴的距离除以可能的值范围有效地标准化了曼哈顿距离的每个分量。

图3.7展示了索伦森距离的工作原理。

图3.7 曼哈顿距离与索伦森距离

无论要计算的点距离原点有多远,简单的曼哈顿距离算法的结果是一样的。索伦森距离降低了距离原点较远的点的重要性,因此它们不会让某些大异常值主导KNN的结果。

我们可以通过添加一个Distance的子类来引入这种算法。这种算法和曼哈顿距离有点儿像,但最好还是放在独立的类中:

这种设计方法让我们可以利用面向对象的继承来实现距离算法的多态家族。我们可以基于前面这几个子类,不断构建出庞大的距离算法类家族,并将它们作为超参数逐个尝试,找出最适合的距离算法来执行所需的分类。

我们需要将一个Distance对象集成到Hyperparameter类中。也就是提供这些子类之一的实例。因为它们都包含distance()方法,因此我们可以方便地替换不同的子类,以找到最适合我们数据和属性集合的距离算法。

现在,我们可以在Hyperparameter类中引用一个具体的距离子类。在第11章中,我们将了解如何灵活地往Distance类定义层级中插入任何一个可能的距离算法实例。 H7OBiRuSqtMtJpARC++Aht4EOwMeBIHXBBcN7r8GAaayjCeq7UoBoNfD0NkMWefI

点击中间区域
呼出菜单
上一章
目录
下一章
×