机器学习——15分钟透彻理解感知机

时间:2021-06-13 05:45:41

前言

随着17年阿尔法狗(AlphaGo)击败人类职业围棋选手、战胜围棋世界冠军,AI、人工智能等词汇也成为了时下人们追求的一个潮流,各种相关产业和人工智能为主题的创业公司也如雨后春笋般相继涌现,因此人工智能也成为了2017年的关键词。关于人工智能的概念从计算机诞生之初就已经有了,1936年艾伦·图灵(Alan Turing)提出了著名的 “图灵机”(Turing Machine)的设想,在十多年后,其为了明确机器是否具备智能更是提出了著名的图灵测试。因此在过去的半个多世纪,人工智能其实不算什么新鲜的话题,甚至许多好莱坞大片都不吝以此概念作为噱头,比如黑客帝国人工智能机械姬等(PS:绝没有推荐电影的意思)。但是处在21世纪的我们听到这些概念时仍然会不自觉的感到兴奋,因为这是第一次科技的力量让我们感受到科幻与现实离我们是如此的接近,也正是在这个信息高度聚合与高速传播的时代,使我们大部分人都能够参与到其中,去实现每个人心中的科幻梦。

注意事项

技能要求

接下来的知识不会涉及到高深的数学知识,对编程的要求也是极低,并且所有涉及到的知识点都会用通俗的语言进行解释,所以不要担心数学不够好,不会编程等问题。当然,便于理解本文,如果你数学基础足够好那是那是有好处的。同时关键部分将会给出某些重要信息的解释或是数学名词的解释链接,在阅读本文过程中适当的去理解其中的某些概念是有必要的,必要的时候还请停下阅读进度确定自己已经理解。最后希望在看完这篇文章后能够让你产生对数学和机器学习的浓厚兴趣。所以Come on!请花15分钟认真看完!请花15分钟认真看完!请花15分钟认真看完!

内容概述

接下来我们将从机器学习中最基础的模型感知机(Perceptron)出发去探索机器学习的奥秘,在这儿我们可以不用理解它是什么意思,也不用去查各种资料使自己迷失在知识的海洋里,花15分钟时间仔细读完本文,你将了解人工智能现在真正的样子,同时你也将正式开始入门人工智能这个领域。人类在统计与概率的基础上建立起了人工智能这么一套体系,在今天这么个日新月异的世界,也是越加的蓬勃发展。虽然人工智能的内容涵盖很广,但是其主线脉络却是明确的,从发现问题,分析问题再到解决问题,在发展出的那一套基础框架下,人工智能在众多的领域遍地开花,产生了许许多多应用于不同领域的新奇想法。因此我们在追逐学科前沿,面多众多令人眼花缭乱的模型或算法时,夯实自己的基础,提升自己发现问题并解决问题的能力显得更加总要。

机器学习的核心

机器学习已经成为一门完整的学科,在学科建设的基础上,已经出现了针对机器学习领域标准的研究方法和技巧。下图展示了机器学习的三要素,也是其核心内容。

机器学习——15分钟透彻理解感知机

从分类问题开始

分类在我们日常生活中很常见,商品分类,垃圾分类,食物分类……这些分类的场景在我们的生活中都是司空见惯的。正因为司空见惯,所以就让人感觉分类是理所当然的事,其过程也没有任何难度。但这简单的事儿对机器而言却并不是那么简单。如果要让机器来完成各种分类它能不能完成呢?答案是能,只要我们为特定的机器设定特定的规则使它能循环运转起来就行,而这儿那个规则就是分类的关键。比如将商场卖的橘子分成两类,一类长得好看的为橘子君A,一类长得难看的为橘子君B,这儿有一个规则Rule,能够判断一个橘子是好看还是不好看,那么将Rule告诉这个机器岂不是完美?以后再也不用担心吃不到好看的橘子了。

机器学习——15分钟透彻理解感知机

好吧跑偏了,通过上面举的的例子其实就能够看到人工智能研究中的一个重要步骤——选择模型。橘子分类中的Rule就是需要选择的模型,有了模型,有了Rule,机器才能知道它拿到一个橘子应该怎么做。那么现在有了模型还要干什么?如果那模型是我们从一个有限的样本空间中通过严格的推导得到的,并且我们也只将该模型应用于该有限样本空间,那么就没有什么事儿需要做了,但实际上这在现实中是不可能的,因为我既然从那一堆橘子推导出了一个模型,那那堆橘子就肯定已经被我们全都分好类了,那这个模型拿来还有什么意义?虽然从上面的角度我们似乎做了无用功,但统计学的知识却给了我们启发。那就是,我们推导出来的模型除了应用于我们用于推导模型所用的那堆橘子外,很大概率还能应用于那些还没有被分类的橘子。从概率论统计学的观点上看,这是正确的。因为我们以橘子的美丑对橘子进行分类,而美和丑是那些橘子所共有的一些特征。就好比人,每个人都长着五官,在一小群人中我们以一个标准区分其中的美丑,那么将这个标准应用于全体人当中也是可行的,但也可能出现我们的标准无法区分的情况发生。

好了,到这儿有没有想到些什么?由概率论的知识我们能够想到,从部分数据中寻找其中我们关注的共性特征对其进行识别分类,那么这些共性特征也能够帮助我们识别分类那些未知的数据,这就是基于概率与统计学习的机器学习的核心原理与思想。看吧,其实很多感觉高深的知识就是我们身边随处可见的问题。有了这个思想有没有对机器学习的概念有一个初步的了解呢?机器从我们给的数据学习一个能够正确解决问题的方法,我们再用那个方法去应用到我们的实际问题中去,而那个方法就是上面多次提到的模型。

在二维平面中怎么分类?

读到这儿的朋友应该对机器学习还有较深的困惑,但这不要紧,现在需要你发挥一定的想象能力。同时如果手边有笔和纸也请拿起笔跟我一起画一个二维坐标系,这个坐标系是一个二维空间,在这个空间中分布着无数点它们都是这个二维世界的原住民,现在你是这个世界的上帝,你随意在这个空间中画了一条看不见直线,里面的人被你分成了两类A和B,分别位于直线两侧。下图展示了在平面上的一条直线 2xy+3=0 将平面上的点分成两类,

机器学习——15分钟透彻理解感知机

有一天,这个二维世界中穿越来了一个从三维世界来的人C,小C遇见了很多这个世界的人,他发现了这个世界上的人被分成了A,B两类。并且这些被分成两类的人类别存在较明显的位置特征,于是小C想了一个办法来描述这个世界上被分成两类的人,即假设这个世界的人可以用一条直线来划分。小C想到的办法如下:

f(x)=sign(x)={11x>0x<=0

y=f(wx+b),x

通过以上计算方法,能够将所有二维世界中的原住民分成两类,也就是以每个原住民的坐标为参数,通过模型计算结果为1的为一类,为-1的为另一类,So easy有木有!到目前为止小C离成功猜到你是怎么给这个二维世界的原住民分类的又近了一步。小C还需要什么?它需要知道 w b 的值是什么。它们显然是两个模型必须的参数值,这两个参数值影响着分类模型的结果,而现在要怎么来确定这两个值呢?可能有人会很快想到,我们拿一组二维人的坐标数据和它的实际分类作输入去算不就行了吗?是的,我们需要以有限的输入去算这两个值,许多人到这儿就迷茫了,怎么算?对于怎么算的问题才是机器学习中的关键,回到上面的模型,我们要开始算就需要一个明确的 w,b 值,这与我们所要解决的问题相矛盾了,因此只能假设一个初始值。这个初始值是什么不重要,重要的是正因为有了一个初始值小C的这个模型成为了一个可以实际进行运算的模型了,而我们运算的目的也就变成了不断进行迭代运算使得 w,b 的值不断向着接近这个世界分类真相的方向前进。自然的我们得到如下两个基本策略(迭代过程该怎么做的方法):

  1. 模型运算结果与实际结果相符,不做任何额外操作,继续输入新的数据
  2. 模型运算结果与实际结果不符(误分类),调整 w,b 的值,使得其朝真相的方向逐步靠近

到这儿我想大家应该知道需要做什么了,那就是找到一个方法来调整 w,b 的值。在讨论该怎么调整 w,b 的值之前先来想想进行这些计算的目的,其目的显然是为了得到一组 w,b ,使得模型运算与实际结果不符的数量最小。在机器学习中,称这么一个关于 w,b 的函数叫做损失函数,将损失函数极小化(极小化即求极小值)的过程就是求 w,b 的过程,而损失函数的一个自然选择是误分类的总数(自然选择就是最接近人类思维方向逻辑推断),但这样的话损失函数就不是 w,b 的连续可导函数。这儿为什么要求损失函数是关于 w,b 的连续可导函数,因为只有函数是连续可导的,我们才能方便的在该函数上确定极大值或极小值,对于这个问题可参考此处。好了,损失函数不适合表示为误分类点的总数,那么能寻找其它表现形式。这儿有一个选择就是将被误分类的所有点距离模型表示直线或平面的总距离作为损失函数的意义。这也是我们能想到的最自然的表示了,比如当点被误分类,误分类点肯定出现在了当前模型的错误一侧,我们的有效矫正方式就是调整 w,b 的值使得模型表示平面/直线往该点的方向平移一定距离,也就是缩小它们之间的距离,但是在最优化问题当中,我们对于单点来说可能使其达到最优了,即误差点相对模型的距离为0了,但对于这个空间中的所有点来说,可能反而会随着单个点这样的调整而出现更多的误差点,因此我们需要保证 w,b 的调整总是朝着好的方向进行,也就是总体误差点最少,换成距离的概念就变成了误差点距离平面/直线的总长度最小,这样就能保证我们训练得到的模型最接近真实模型。由点到平面的距离公式我们可以得到任意一点距离我们上面定义的模型的距离为:

len(xi)=1w|wxi+b|

在这里不加证明地给出这个公式,如果有兴趣自己推导的同学可参考 点到平面距离公式的七种推导方法探讨,这里需要解释的一点是因为我们的模型有两个变量 x(xRn) y,(y1,1) ,所以在这儿所谓的距离实际是指的我们的模型包含的某一维度的距离,更科学的描述称为到 超平面的距离,因此大家在计算距离公式时切记一点,针对 f(x)=wx+b 这个形式来求距离时,可能有人会很困惑,总感觉上面的距离公式是错的?其实从我们的场景来说,这个模型函数中的 x 实际上是一个向量,即它包含两个维度的值。这样我们就可以将能够影响一个点位置信息的 x 维度上的值和 y 维度上的值一起进行评估来得到一个综合的评估值。这一点从我们遇到的问题出发去看也是显而易见的,因为设计的这个分类方法中,实际输入就是一个坐标点,输出是一个其它值。因此针对上面的公式也就不难理解了,其结果实际上是求到 w1x1+w2x2+b=0 平面的距离。在这儿必须要理解这点,这将使我们更清晰的看出目前以及之后的所有公式推导的理由及意义。如果还是不能理解也没关系,只要知道为了明确知道模型的好坏,需要有一个方法来对其进行评估,在这儿我们只是选取了距离这个概念来描述模型的好坏,在其它更多场景中还有更多其它各种各样的方法,而重要的是能够在各种复杂场景中找到一个合适高效的方法。

有了计算距离的方式,下面我们来看看损失函数究竟怎么定义。由于对于模型来说,在分类错误的情况下,若 wxi+b>0 ,则实际的 yi 应该是等于-1,而当 wxi+b<0 时, yi 等于1,因此由这个特性我们可以去掉上面的绝对值符号,将公式转化为:

len(xi)=1wyi(wxi+b)

如此得到最终的损失函数为:

L(w,b)=xiMlen(xi)L(w,b)=xiMyi(wxi+b)

正如上面所示, 1w 这个因子在这儿可以不用考虑,因为它对结果的影响与 w,b 是等效的,因此只用单独考虑 w,b 就可以,这样可以减小运算复杂度。到这一步问题就变得简单了,那就是求L(w,b)的极小值。对于极大值极小值的求解方法有许多,这儿首先讲述一种梯度下降的方法求极小值,根据梯度的定义,我们可以得到损失函数的梯度有:

wL(w,b)=xiMyixibL(w,b)=xiMyi

根据梯度下降所描述的方法,我们只需要在每次出现误分类时按如下方法更新 w,b 的值即可,

ww+ηyixibb+ηyi

以上更新方法就是每次出现误分类时 w b 分别减去它们各自在该误分类点的梯度值,这儿更新 w,b 的方式称为随机梯度下降法,因此会发现更新 w,b 时是不带求和符号的,所谓随机梯度下降就是每次取梯度值是随机的取某点在该模型上的梯度值,这儿的随机性取决与你的输入。当然也可以通过计算求得一个总体平均的梯度,但那样的话当输入数据很多时训练模型将变成一个很漫长的事儿,因此到底哪种好哪种不好我们需要根据实际情况去权衡取舍。

到这儿是不是一切变得豁然开朗?小C在你所创造的二维世界中已经找到了方法来得到你对其中的二维原住民分类的方式,只要小C在那个世界发现足够多的原住民,每当找到一个原住民就用他的模型对其分类,只要分类结果与实际不符时,就用上面的方法更新模型,那么小C将得到一个无限接近你对二维世界原住民分类的模型。

让模型运转起来

上面我们已经确定了给二维世界原住民分类的方案,并且知道了怎么来使得随着数据的输入让模型变得越来越接近真实情况。而上面描述的模型还有一个高大上的名字叫感知机模型。是不是格调瞬间就上来了?那么我们来看看怎么用编程语言实现这个计算过程。

Python实现

Python具有很方便的数值计算库和简单的语法,因此我们用Python实现感知机模型试试看.

from random import randint
import numpy as np
import matplotlib.pyplot as plt


class TrainDataLoader:
    def __init__(self):
        pass
    def GenerateRandomData(self, count, gradient, offset):
        x1 = np.linspace(1, 5, count)
        x2 = gradient*x1 + np.random.randint(-10,10,*x1.shape)+offset
        dataset = []
        y = []
        for i in range(*x1.shape):
            dataset.append([x1[i], x2[i]])
            real_value = gradient*x1[i]+offset
            if real_value > x2[i]:
                y.append(-1)
            else:
                y.append(1)
        return x1,x2,np.mat(y),np.mat(dataset)


class SimplePerceptron:
    def __init__(self, train_data = [], real_result = [], eta = 1):
        self.w   =   np.zeros([1, len(train_data.T)], int)
        self.b   =   0
        self.eta =   eta
        self.train_data   = train_data
        self.real_result  = real_result
    def nomalize(self, x):
        if x > 0 :
            return 1
        else :
            return -1
    def model(self, x):
        # Here are matrix dot multiply get one value
        y = np.dot(x, self.w.T) + self.b
        # Use sign to nomalize the result
        predict_v = self.nomalize(y)
        return predict_v, y
    def update(self, x, y):
        # w = w + n*y_i*x_i
        self.w = self.w + self.eta*y*x
        # b = b + n*y_i
        self.b = self.b + self.eta*y
    def loss(slef, fx, y):
        return fx.astype(int)*y

    def train(self, count):
        update_count = 0
        while count > 0:
            # count--
            count = count - 1

            if len(self.train_data) <= 0:
                print("exception exit")
                break
            # random select one train data
            index = randint(0,len(self.train_data)-1)
            x = self.train_data[index]
            y = self.real_result.T[index]
            # wx+b
            predict_v, linear_y_v = self.model(x)
            # y_i*(wx+b) > 0, the classify is correct, else it's error
            if self.loss(y, linear_y_v) > 0:
                continue
            update_count = update_count + 1
            self.update(x, y)
        print("update count: ", update_count)
        pass
    def verify(self, verify_data, verify_result):
        size = len(verify_data)
        failed_count = 0
        if size <= 0:
            pass
        for i in range(size):
            x = verify_data[i]
            y = verify_result.T[i]
            if self.loss(y, self.model(x)[1]) > 0:
                continue
            failed_count = failed_count + 1
        success_rate = (1.0 - (float(failed_count)/size))*100
        print("Success Rate: ", success_rate, "%")
        print("All input: ", size, " failed_count: ", failed_count)

    def predict(self, predict_data):
        size = len(predict_data)
        result = []
        if size <= 0:
            pass
        for i in range(size):
            x = verify_data[i]
            y = verify_result.T[i]
            result.append(self.model(x)[0])
        return result



if __name__ == "__main__":
    # Init some parameters
    gradient = 2
    offset   = 10
    point_num = 1000
    train_num = 50000
    loader = TrainDataLoader()
    x, y, result, train_data =  loader.GenerateRandomData(point_num, gradient, offset)
    x_t, y_t, test_real_result, test_data =  loader.GenerateRandomData(100, gradient, offset)

    # First training
    perceptron = SimplePerceptron(train_data, result)
    perceptron.train(train_num)
    perceptron.verify(test_data, test_real_result)
    print("T1: w:", perceptron.w," b:", perceptron.b)

    # Draw the figure
    # 1. draw the (x,y) points
    plt.plot(x, y, "*", color='gray')
    plt.plot(x_t, y_t, "+")
    # 2. draw y=gradient*x+offset line
    plt.plot(x,x.dot(gradient)+offset, color="red")
    # 3. draw the line w_1*x_1 + w_2*x_2 + b = 0
    plt.plot(x, -(x.dot(float(perceptron.w.T[0]))+float(perceptron.b))/float(perceptron.w.T[1])
             , color='green')
    plt.show()

如下是由以上代码实现的模型分类结果图,其中红色直线为实际的分类模型,绿色直线为通过训练数据训练后得到的模型,灰色’*’符号组成的点集为训练数据集,蓝色的’+’号组成的点集为验证数据集:

机器学习——15分钟透彻理解感知机

感知机就这样?

看了以上的内容很多人可能感觉机器学习也不过如此!那么首先恭喜你,有这感觉证明对于机器学习你开始入门了,但是还有更多的东西在等着你。这儿有几个疑问步骤你有没有想过:

  1. 模型是怎么确定的,为什么就能想到用感知机这样的模型呢?
  2. 损失函数就只能靠那种思路得到吗?还有没有更好的方式?
  3. 损失函数都是求最小值吗,有没有求最大值的情形,最小/最大值真的能求出来吗?
  4. 求最小/最大值的方法还有什么?我们能不能换其它方法来替换随机梯度下降/上升?

我相信,读完整篇文章这些疑问应该是自然产生的,其中的这些问题希望大家自己能够随着学习的深入找到答案。

感知机的对偶形式变形

“对偶”一词听着挺奇怪的,但可以将其理解为形式不一样但结果相等的意思。如下就是感知机模型的一种对偶形式:

f(x)=signj=1Nαjyjxjx+b

从上式可以看出与前面的模型相比仅替换了 w 的值,这种变化是基于前面的模型推导得到的。当现在出现了i次误分类,而造成i次误分类的点分别为 (x1,y1),(x2,y2),,(xi,yi) ,则当前 w 的值必定为

w=η(y1x1+y2x2++yixi)

由此我们假设总共有N个点被错误分类, ni 表示这N个点中的第i个点在训练过程中被分类错误的总次数。因此上式可化简为:

w=i=1Nniηyixi

那么自然的,我们令 αi=niη ,则有

w=i=1Nαiyixi

经过如上变换后,每次训练迭代更新就需要更新 αb 值。而当 η 等于1时, α 的物理意义为与前面讨论的 ni 相同。因此,当 η 等于1时,每次更新失败 αi 都应该加1,从另一个角度说, αi 就变成了训练过程中的一个记录器,用于记录每个点分别被误分类的次数,因此得到训练过程的更新策略如下:

αiαi+1bb+ηyi

同时观察模型可以发现 xi xj 以内积的形式出现,通过这个特征可以想到Gram矩阵的定义为:

G=[xixj]N×N

因此我们可以提前计算出Gram矩阵用于后面直接通过查询Gram矩阵知道 xjxi 的值。上面没有讲到损失函数,实际上对偶形式的损失函数和原始形式的损失函数是一样的,而它们更新参数的策略都是围绕则一个目的实现的,那就是求得损失函数的极小值的最优解。在最前面介绍的感知机的原始形式求损失函数最优解的策略应该很多人都是很容易理解的,就是非常直观的梯度下降。而在对偶形式当中, α 与损失函数进行梯度下降的次数是紧密联系的,随着 α 的不断增加,损失函数执行梯度下降的次数不断增加,模型也越接近真实情况。如果你会matlab,使用matlab去模拟这个更新过程,你将能够更直观的看到 α 的增加是如何影响着其它值的。好了,知道了这些,我们就能够将这些公式转化为代码去实现这个模型了,还有不懂的地方可以结合代码在梳理一遍。

感知机对偶形式实现

# Init the parameter
from random import randint
import numpy as np
import matplotlib.pyplot as plt


class TrainDataLoader:
    def __init__(self):
        pass
    def GenerateRandomData(self, count, gradient, offset):
        x1 = np.linspace(1, 5, count)
        x2 = gradient*x1 + np.random.randint(-10,10,*x1.shape)+offset
        dataset = []
        y = []
        for i in range(*x1.shape):
            dataset.append([x1[i], x2[i]])
            real_value = gradient*x1[i]+offset
            if real_value > x2[i]:
                y.append(-1)
            else:
                y.append(1)
        return x1,x2,np.mat(y),np.mat(dataset)


class SimplePerceptron:
    def __init__(self, train_data = [], real_result = [], eta = 1):
        self.alpha   =   np.zeros([train_data.shape[0], 1], int)
        self.w   =   np.zeros([1, train_data.shape[1]], int)
        self.b   =   0
        self.eta =   eta
        self.train_data   = train_data
        self.real_result  = real_result
        self.gram         = np.matmul(train_data[0:train_data.shape[0]], train_data[0:train_data.shape[0]].T)
    def nomalize(self, x):
        if x > 0 :
            return 1
        else :
            return -1
    def train_model(self, index):
        temp = 0
        y = self.real_result.T
        # Here are matrix dot multiply get one value
        for i in range(len(self.alpha)):
            alpha      = self.alpha[i]
            if alpha == 0:
                continue
            gram_value = self.gram[index].T[i]
            temp = temp + alpha*y[i]*gram_value
        y = temp + self.b
        # Use sign to nomalize the result
        predict_v = self.nomalize(y)
        return predict_v, y
    def verify_model(self, x):
        # Here are matrix dot multiply get one value
        y = np.dot(x, self.w.T) + self.b
        # Use sign to nomalize the result
        predict_v = self.nomalize(y)
        return predict_v, y
    def update(self, index, x, y):
        # alpha = alpha + 1
        self.alpha[index] = self.alpha[index] + 1
        # b = b + n*y_i
        self.b = self.b + self.eta*y
    def loss(slef, fx, y):
        return fx.astype(int)*y

    def train(self, count):
        update_count = 0
        train_data_num = self.train_data.shape[0]
        print("train_data:", self.train_data)
        print("Gram:",self.gram)
        while count > 0:
            # count--
            count = count - 1

            if train_data_num <= 0:
                print("exception exit")
                break
            # random select one train data
            index = randint(0, train_data_num-1)
            if index >= train_data_num:
                print("exceptrion get the index")
                break;
            x = self.train_data[index]
            y = self.real_result.T[index]
            # w = \sum_{i=1}^{N}\alpha_iy_iGram[i]
            # wx+b
            predict_v, linear_y_v = self.train_model(index)
            # y_i*(wx+b) > 0, the classify is correct, else it's error
            if self.loss(y, linear_y_v) > 0:
                continue
            update_count = update_count + 1
            self.update(index, x, y)

        for i in range(len(self.alpha)):
            x = self.train_data[i]
            y = self.real_result.T[i]
            self.w = self.w + float(self.alpha[i])*x*float(y)
        print("update count: ", update_count)
        pass
    def verify(self, verify_data, verify_result):
        size = len(verify_data)
        failed_count = 0
        if size <= 0:
            pass
        for i in range(size-1):
            x = verify_data[i]
            y = verify_result.T[i]
            if self.loss(y, self.verify_model(x)[1]) > 0:
                continue
            failed_count = failed_count + 1
        success_rate = (1.0 - (float(failed_count)/size))*100
        print("Success Rate: ", success_rate, "%")
        print("All input: ", size, " failed_count: ", failed_count)

    def predict(self, predict_data):
        size = len(predict_data)
        result = []
        if size <= 0:
            pass
        for i in range(size):
            x = verify_data[i]
            y = verify_result.T[i]
            result.append(self.model(x)[0])
        return result



if __name__ == "__main__":
    # Init some parameters
    gradient = 2
    offset   = 10
    point_num = 1000
    train_num = 1000
    loader = TrainDataLoader()
    x, y, result, train_data =  loader.GenerateRandomData(point_num, gradient, offset)
    x_t, y_t, test_real_result, test_data =  loader.GenerateRandomData(100, gradient, offset)
    # train_data = np.mat([[3,3],[4,3],[1,1]])
    # First training
    perceptron = SimplePerceptron(train_data, result)
    perceptron.train(train_num)
    perceptron.verify(test_data, test_real_result)
    print("T1: w:", perceptron.w," b:", perceptron.b)

    # Draw the figure
    # 1. draw the (x,y) points
    plt.plot(x, y, "*", color='gray')
    plt.plot(x_t, y_t, "+")
    # 2. draw y=gradient*x+offset line
    plt.plot(x,x.dot(gradient)+offset, color="red")
    # 3. draw the line w_1*x_1 + w_2*x_2 + b = 0
    plt.plot(x, -(x.dot(float(perceptron.w.T[0]))+float(perceptron.b))/float(perceptron.w.T[1])
             , color='green')
    plt.show()

下图为以1000组数据训练,100组数据做验证的结果图,绿色直线为训练得到的模型。

机器学习——15分钟透彻理解感知机

感知机的限制与推广

感知机是什么?就是如上面所讲述的那种模型定义,而感知机有一个非常明显的特征——它是线性的。这儿先来各出一个结论:线性模型不可分类异或问题。到这儿很多人可能会糊涂了,异或问题是什么鬼?不能把话讲明白吗?这儿给出一个直观的例子,还是以前面描述的二维世界为例。我们知道二维世界的每个人都具有一个标签,就像身份证一样,那就是它们的坐标。假设你给它们分类的时候不是直接在里面画一条直线,直线一侧的是一类,另一侧的是另一类;而是以它们的 (x,y) 坐标值来分类, x , y 值相同的为一类,不同的为另一类,那么小C还能用上面的方法分类吗?显然是不可能的,而这个问题就属于异或问题,异或问题就属于线性不可分问题。就像下图,下图中相同符号表示的点属于一类,当你会发现不管你怎么画线去分,甚至画再多条直线也不可能如下图所示的两种类别分开,这就是最简单的一种线性不可分问题的情形。

机器学习——15分钟透彻理解感知机

现在我们知道用感知机来解决分类问题是有限制的,也就是不能解决线性不可分问题,因此在应用感知机模型之前需要判断该问题是否是线性不可分的,至于应该如何具体的去判断?这个将留到之后的章节去讨论。读完前面我们已经掌握了怎么用感知机讲一个东西分成两类了,但是在现实当中讲一个东西分成两类的场景太少也太简单了。真正有需求的是将事物分成多类,如果感知机模型具有这样的功能它在现实中才具有更多的价值,我们也才有学习它的意义。因此我们需要放飞我们的思维,来直观上看看怎么将事物分成N类.

多维空间的多分类问题

在这我们来进行一次逻辑推导.以感知机为例,假设 x w 都是一维的,那感知机的形式应该如下:

f(x)=sign(wx+b)

y=wx+b 是一条直线上的某点,我们通过函数 sign 将其结果y二值化为1或-1,这从分类的角度看就代表着 y=wx+b 这条直线上的点集可以被我们的模型分成两类,即在直线 y=wx+b 上的点((x,y) y 0 y$小于等于0时属于另一类。我们现在从一维直线的点分类推广到二维空间的点分类,我们从初等数学已经学过了,平面的表示如下:

ax+by+cz+d=0

那么我们可以得到一个关于平面的函数 g(x)=ax+by+d ,那如果要将其分成两类只需要将 g(x) 的结果使用 sign 函数进行二值化就可以了,其感知机模型的形式如下:

f(x)=sign(g(x))=sign(ax+by+d)

到这儿大家我想大家就能够想像在三维空间中的点集分类了,三维空间中的一个线性点集表示为 g(x)=ax+by+cz+d ,它表示三维空间中的一条线,在这儿我们要对三维空间中的点进行分类同样构建如下模型即可:

f(x)=sign(g(x))=sign(ax+by+cz+d)

一直到四维空间,五维空间甚至N维空间。从上面我们发现,每多一维, f(x) 就会多一个影响分类结果的因式(子),而在这为了表示方便,影响因式(子)的变量集合我们用 x=[x1,x2,x3,,xn] 表示,每个影响因式(子)中的变量所对应的常量因子的集合我们用 w=[w1,w2,w3,,wn] 表示,首先我们先将上面的式子化为更常规的形式:

f(x)=sign(w1x1+w2x2+w3x3++wnxn+b)

上面的公式的表达是不是感觉很复杂很乱?我们来对其进行整理,由向量的点积性质 xw=w1x1+w2x2+w3x3++wnxn ,可将 f(x) 作如下形式的化简:

f(x)=sign(wx+b)

看到没,化简后就得到了我们在文章开头所见到的那种形式,这也是获得感知机模型的一个自然推导的过程,而在这儿 x 也有了它的特殊命名——特征空间,因此当见到特征空间这个名词时回顾一下这儿就能够理解其是什么意思了。

说到这儿我们了解了分类在高维空间中的表现形式,但是却还没有提到多分类的事儿,不知道读到这里的朋友有没有自己想到多分类应该怎么做呢?道理很简单,我们看到以上的所有分类例子都是用 sign 函数的二值化特性进行分类,那么我们将其进行一下小小的修改不就实现了多分类吗?以下为修改示例:

sign(x)=101,x>0,x=0,x<0

像上面那样,岂不就实现了将点分成三类?分类从线性代数的概念来理解其实是一个映射的问题,前面我们提到的二分类问题的映射表示如下:

f:Rnx{1,1}

因此多分类的一般表示就应该如下:
f:RnxRny

其实经过前面将 w x 向量化之后,我们其实还可以更进一步对其进行扩展,如下所示:

f1(x)f2(x)fm(x)=w1,1w2,1wm,1w1,2w2,2wm,2w1,nw2,nwm,n[x1x2xn]+b1b2bn

进行如上扩展后我们就能够将N维特征向量空间直接映射到M维空间,通俗点讲就是将 x 表示的数据集分成了 m 类。有的人说上式还不够精简,那么我们运用矩阵加法与乘法的性质将其再次化简得:

f1(x)f2(x)fm(x)=w1,1w2,1wm,1w1,2w2,2wm,2w1,nw2,nwm,nb1b2bm[x1x2xn1]

好了根据这种矩阵表示我们应该也能够得到分类的一种直观表示,如图:

机器学习——15分钟透彻理解感知机

上图中有四类点,我们要将其各自分开就直接化多条直线就行。到这儿推导先告一段落,我们接下来看看多分类具体是怎么实现与应用的。

感知机如何应用在实际中

介绍与推导

相信到这你已经对感知机了解了,是不是产生了世界尽在我手的感觉?但也可能有可能会打击你,对你说:”你给我用感知机做个MNIST图片分类看看?”如果你真的是一个刚入门的人,经过短暂思考后可能会陷入迷茫,貌似感知机学是学了,但完全没法用来做事啊!别急,接下来让我们一起来分析MNIST分类究竟该怎么用感知机模型实现。

MNIST是什么很简单,网上资料也一大堆,总的来说它就是一堆由人手写的阿拉伯数字的图片。所谓的MNIST分类就是识别出某张图片上写的数字是几。但是怎么做呢?在这儿有一个关于MNIST的图片的信息,那就是其中的每一张图片的大小都是 28×28 。在这我们为了简单起见可以直接将图片中的每个像素作为特征,也就是特征向量 x 将会是一个拥有 28×28=784 个方向的向量。输入确定了那么再想想还缺什么?我们要能够识别数字换个说法其实就是需要将MNIST的图片分成10类,因为手写数字都是0到9的数字,如果包含几十上百的数字,那情况将变得更加复杂。

回到正题,图片的特征值有784个,且我们需要将图片分成10类,则我们的模型应该是这样的:

f1(x)f2(x)f10(x)=w1,1w2,1w10,1w1,2w2,2w10,2w1,784w2,784w10,784b1b2b10[x1x2x7841]

好了模型确定了,还需要做什么?回顾前面对二维世界的原住民分类问题,接下来应该是思考怎么来判断模型的好坏。由上式可知,在785维空间中,超平面 f(x) 将点 (x,f(x)) 分成了两类。因为该空间存在 m 个超平面,所以其中的点总共被分为 m 类。同时我们可以定义当结果 f(x) 的值越大,这表示该位置对应的数字是机器的预测值。这句话比较拗口,但结合模型表达式细细体味,很容易理解的。我们直接引用上面介绍的二分类问题的损失函数定义,易得 wb 矩阵的更新策略如下:

w1,1w2,1wi,1w10,1w1,2w2,2wi,2w10,2w1,784w2,784wi,784w10,784b1b2bib10=w1,1w2,1wi,1w10,1w1,2w2,2wi,2w10,2w1,784w2,784wi,784w10,784b1b2bib10+sign(1,x)ηsign(2,x)ηsign(i,x)ηsign(10,x)η[x1x2xix7841]

以上结果是根据之前感知机的推导过程自然推导得到,如果有不理解的可以再返回去看看感知机的推导过程,这儿就不在赘述了,其中唯一的区别就是这儿使用矩阵直接将更新方式表示了出来,同时涉及的消绝对值符号的地方是构造了一个辅助函数sign(i,x),其意义同前面的sign(x),只是分别将其应用与每个超平面上。到这一步,我们就可以以此写出代码了。

实现

Github

讨论

上面我们看到了,运用感知机再稍微进行一点原始且直观的扩展就能实现对图片的分类,那还可以怎么做呢?对于分类的问题在机器学习领域已经很成熟,因此也产生了许多应用于各种场景,用于解决各种不同分类问题的有效算法。而那些算法有人也直接将其称作分类器。因此,在这儿对MNIST进行分类只是进行了简单粗暴的运用了感知机的思想,真正工程中所有考虑的问题将更加复杂,比如还得考虑过拟合问题,算法是否能有效收敛,损失函数的惩值是否合理等等。因此在这留下几个问题:

  1. 特征空间应该怎么获得与选取?
  2. 损失函数的确定还需要注意什么?
  3. 如何用机器学习算法分类手写字母?
  4. 如何用机器学习算法分类音频?

结束语

通过感知机模型我们看到了机器学习的过程,当然感知机是非常非常非常简单的机器学习模型,它能够处理的问题也是非常有限。但这并不能妨碍我们了解机器学习是怎样一门学科,它应该去怎么学。在半个多世纪的发展中,机器学习也产生了很多分支,同时出现了无数经典的模型,但那些都是构建在机器学习基本理论框架下所产生的变化。因此立即机器学习的本质,对于去理解哪些种类繁多的算法将变得更加容易。同时这儿也要阐述一个事实就是,机器学习的核心不是那些各种算法,而是整个机器学习这门学科处理问题的基本思路和流程,我们通过时间的积累掌握可以应用于更多不同场景的算法,这可以帮助我们更快更好的处理问题,但是永远不要忘记我们使用那些工具的能力。

最后非常感谢您能读完本文!由于本人知识有限,其中不可避免有不当错漏之处,还请批评指出。同时也非常感谢李航老师所著《统计学习方法》,读完受益匪浅!由于篇幅有限很多严格的逻辑推导和基本概念在本文没能讲到,因此读完本文的朋友可以结合该书相互印证,相信你又能有不一样的理解。

任何意见或建议随时联系:
Gmail: yxhlfx@gmail.com
qq : 1137924614

2018.1