《统计学习方法》的Python实现:(1)感知机

作者: 松山剑客 | 来源:发表于2018-12-21 15:32 被阅读104次

    0. 假装有一个前言

    前几天看到有人转李航老师的《统计学习方法》python 3.6实现,突然发现书我是看了一半了,代码却只写过第三章的k近邻法。(不要问我为什么现在才看了一半,也不要问我为什么不一边看一边写)

    1. 感知机原理

    赶只鸡(划掉)

    感知机(Perceptron)是二分类的线性分类模型,只适用于线性可分的二分类问题。


    线性二分类问题
    输入 输出 模型类型 参数意义
    特征向量 类别(\pm1 判别模型 超平面参数

    感知机的损失函数为所有误分类点到分类超平面的距离之和,因此算法是误分类驱动的,正确分类的点不会对算法的结果做出贡献。

    2. 感知机学习算法的两种形式

    2.1 原始形式

    使用随机梯度下降法,针对每个误分类使其梯度下降。


    算法2.1_感知机学习算法的原始形式
    输入:训练数据集 T=\{(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\} ;学习率 \eta(0<\eta\leq1)
    输出:w,b;感知机模型f(x)=\text{sign}(w\cdot x+b).
    1 选取初值w_0,b_0
    2 在训练集中选取数据(x_i,y_i)
    3 如果y_i(w\cdot x+b)\leq 0
    w \leftarrow w+\eta y_ix_i \\ b \leftarrow b+ \eta y_i
    4 如果训练集中存在误分类点,转至 2;否则,结束


    def trainOri(self,yita = 0.1):
            self.w = self.w0
            self.b = self.b0
            misDivision = True
            self.yita = yita
            self.k = 0
            while misDivision:
                for it in range(len(self.data)):
                    if self.label[it] * (np.dot(self.w, self.data[it]) + self.b) <= 0:
                        self.w += self.yita * self.label[it] * self.data[it]
                        self.b += self.yita * self.label[it]
                        self.k += 1
                        break
                    if it == len(self.data) - 1:
                        misDivision = False
    

    原始形式这里没有问题,对1000个2维数据进行分类使用了0.14s,更新次数为7470

    PerceptronOri

    2.2 对偶形式

    使用随机梯度下降法,针对每个误分类使其梯度下降。


    算法2.2_感知机学习算法的对偶形式
    输入:训练数据集 T=\{(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\} ;学习率 \eta(0<\eta\leq1)
    输出:\alpha,\beta;感知机模型f(x)=\text{sign}(\sum_{j=1}^{N}\alpha_jy_jx_j\cdot x_i + \beta).
    1 选取初值\alpha_0=\{\alpha_1,\alpha_2,...\alpha_N\}=\{0,...,0\},\beta_0=0
    2 在训练集中选取数据(x_i,y_i)
    3 如果y_i(\sum_{j=1}^{N}\alpha_jy_jx_j\cdot x_i + \beta)\leq 0
    \alpha \leftarrow \alpha+\eta \\ \beta \leftarrow \beta+ \eta y_i
    4 如果训练集中存在误分类点,转至 2;否则,结束


    def trainDual(self, yita = 1):
            self.alpha = self.alpha0
            self.beta = self.beta0
            gram = []
            for it in self.data:
                temp = []
                for ot in self.data:
                    temp.append(np.dot(it,ot))
                gram.append(temp)        
            misDivision = True
            self.yita = yita
            self.k = 0
            self.kk = 0
            while misDivision:
                for it in range(len(self.data)):
                    temp = 0
                    self.kk +=1
                    if self.label[it] * (sum([self.alpha[i] * self.label[i] * gram[i][it] for i in range(len(self.data))]) + self.beta) <= 0:
                        self.alpha[it] += self.yita
                        self.beta += self.yita * self.label[it]
                        self.k += 1
                        break
                    if it == len(self.data) - 1:
                        misDivision = False
    

    对偶形式这里问题就大了,等了一分钟还以为是条件给错进入死循环了,反复检查确认没有问题,心想,跑去吧(其实去刷知乎了)。于是就有了下面这张图:


    PerceptrDual

    等一下,说好的使用Gram矩阵可以降低运算量呢?同样更新了七千多次为什么你跑了三分钟啊?!差了2000倍有木有啊!


    emm

    2.3 问题分析

    1) 从编程角度分析

    冷静分析一下,Gram矩阵计算时间只需0.6s基本可以忽略不记,由于刚刚只统计了参数更新次数,我们重新统计一下两种算法第三步的判别步骤:

    判别步骤

    原始算法判别15133次,更新参数239次,耗时0.016s
    对偶算法判别16300次,更新参数235次,耗时33.29s

    由算法2.1,2.2可知,参数更新基本不消耗时间,也即大部分时间用于判别步骤。原始算法平均耗时1.05\cdot 10^{-6}s,对偶算法平均耗时2ms。这中间也就差了,额,1931倍吧。
    继续冷静分析,算法2.2中第三步计算量大的主要原因是有一个求积再求和的过程,这个过程也可以当作向量内积来计算,这样就实现了在一次参数更新前只计算一次\alpha_jy_j。这个部分书中没有提及,可能因为不属于算法而是计算方法的一部分吧。

    更新对偶算法如下:

        def trainDual(self, yita = 1):
            self.alpha = self.alpha0
            self.beta = self.beta0
            gram = []
            for it in self.data:
                temp = []
                for ot in self.data:
                    temp.append(np.dot(it,ot))
                gram.append(temp)
            gramA = np.array(gram)
            misDivision = True
            self.yita = yita
            self.k = 0
            self.kk = 0
            while misDivision:
                ay = np.array([self.alpha[x] * self.label[x] for x in range(len(self.alpha))])
                for it in range(len(self.data)):
                    temp = 0
                    self.kk +=1
                    if self.label[it] * (np.dot(ay, gramA[it]) + self.beta) <= 0:
                        self.alpha[it] += self.yita
                        self.beta += self.yita * self.label[it]
                        self.k += 1
                        break
                    if it == len(self.data) - 1:
                        misDivision = False  
    

    同样,我们使用1000个2维数据进行测试,结果如下:


    更新对偶算法之后

    虽然对偶算法还是比原始算法慢了20倍左右,但最起码两者是接近量级的运行时间了。

    2)从算法角度分析

    样本包括三个属性:个数,特征向量尺寸和标签。对于感知机,标签与数据个数相同。Gram矩阵是将样本的特征向量两两做内积,当特征向量尺寸较大时对偶算法应该可以比原算法简化更多的计算量。
    我们将数据由1000个2维数据提升为1000个1000维数据,此时两种算法结果对比如下:


    1000维数据

    虽然对偶算法依然慢于原始算法,但两者间的差距已经由30倍缩小到了2.5倍。

    我们继续增加样本的维数,将维数提高到丧(gan)心(de)病(piao)狂(liang)的500,000维,为了硬盘着想,我们这次只生成了100条数据。


    绝不会用记事本打开的文本文档

    最终结果是,原始算法速度依然是对偶算法的两倍左右。


    50w维数据
    大概是我代码能力太烂?可能还需要进一步优化。另外1957年有500,000维的数据需要处理吗?

    代码

    项目地址

    P.S. 大概也许有可能还会更新

    相关文章

      网友评论

        本文标题:《统计学习方法》的Python实现:(1)感知机

        本文链接:https://www.haomeiwen.com/subject/taymkqtx.html