《Machine Learning in Action》—— 剖析支持向量机,单手狂撕线性SVM
《Machine Learning in Action》—— 剖析支持向量机,单手狂撕线性SVM
前面在写NumPy文章的结尾处也有提到,本来是打算按照《机器学习实战 / Machine Learning in Action》这本书来手撕其中代码的,但由于实际原因,可能需要先手撕SVM了,这个算法感觉还是挺让人头疼,其中内部太复杂了,涉及到的数学公式太多了,也涉及到了许多陌声的名词,如:非线性约束条件下的最优化、KKT条件、拉格朗日对偶、最大间隔、最优下界、核函数等等,天书或许、可能、大概就是这样的吧。
记得与SVM初次邂逅是在17年,那个时候的自己年少轻狂,视图想着站在巨人的肩膀上,把所有自己感兴趣的内容都搞懂,深入骨髓的那种。但后来残酷的现实让我明白一个道理:你知道的越多,你不知道的也越多。而且那个时候自己也没有能力、资源和机会去深入SVM内部,完全无法理解SVM的内部原理。所以,当时自己对SVM的收获只有一个:SVM主要是用来做分类任务的,仅此而已。
第二次接触SVM是在准备考研复试吧,当时复试并没有给出具体内容和范围,而且自己也还是个初出茅庐的小子,对这种所谓的复试有种莫名的恐惧感。也只有从上届学长学姐的口中,得知复试的时候老师会考究学生是否有科研的潜力,所以最好把机器学习熟知一下。那个时候也是处于新冠疫情的紧张时期嘛,就疯狂补习机器学习的内容,其中就包括支持向量机——SVM,主要的学习渠道是吴恩达老师的机器学习课程,感觉讲的的确不错,非常适合我这种菜鸟级选手学习。当时也算是对SVM有了一定的认识吧,也大致了解了SVM的工作原理,当然了,也只是对SVM有了个的浅显的认识,没有手撕SVM的过程,也没有完全把它整明白。尽管如此,复试的过程依然被面试导师锤的体无完肤,除了问了机器学习相关内容之外,编译原理等一些专业知识对于我这个贸易专业的学生来讲可太痛苦了,之前也没有接触过,全程阿巴阿巴。想到这,眼角又又。。。
第三次面对SVM也就是现在了,想着无论如何也要打通我的任督二脉,一定要搞清楚SVM的来龙去脉,也要像面试老师捶我那样,把SVM往死里锤。于是有了下文学习SVM之后的总结,一方面算是重新梳理一遍SVM,另一方面也希望来访的读者能够有所收获。
对于刚刚接触SVM的读者,Taoye主要有以下几条建议,也是我学习SVM过程中的一个小总结吧:
- SVM内部的数学公式很多,但请不要未战先怯,犯下兵家大忌。无论是阅读该篇文章也好,学习其他相关SVM资源也罢,还请诸君耐心、认真且完整的看完。
- SVM的原理过程会涉及到很多的符号或记号,一定要梳理清楚他们所代表的含义。另外,推导过程中会存在很多的向量或矩阵,一定要明白其中shape,这一点可能在不同的资料中会有不一样的处理方式。
- 在阅读的同时,一定要拿出稿纸手动推演SVM的过程,尽可能明白整个过程的来龙去脉,有不明白的地方可以留言或查找其他相关资料来辅助自己的理解。
- 阅读一遍或许有不少知识不理解,但多阅读几遍相信一定会有不少收获
本文参考了不少书籍资料以及许多大佬的技术文章,行文风格尽可能做到通俗易懂,但其中涉及到的数学公式在所难免,还请诸读者静下心来慢慢品尝。由于个人水平有限,才疏学浅,对于SVM也只是略知皮毛,可能文中有不少表述稍有欠妥、有不少错误或不当之处,还请诸君批评指正。
我是Taoye,爱专研,爱分享,热衷于各种技术,学习之余喜欢下象棋、听音乐、聊动漫,希望借此一亩三分地记录自己的成长过程以及生活点滴,也希望能结实更多志同道合的圈内朋友,更多内容欢迎来访微信公主号:玩世不恭的Coder
符号说明
符号 | 说明 |
---|---|
表示单个样本,其中包含多个属性,显示为一个行向量 | |
表示单个样本中的某个属性特征 | |
表示单个样本所对应的标签(具体分类),为整数,非1即-1 | |
表示的是权值行向量,其中 | |
表示的是决策面中的 | |
表示函数间隔,具体解释见下文 | |
表示几何间隔,具体解释见下文 | |
在这篇文章中表示线性核函数 |
关于上述的符号说明,仅仅只是本篇文章的一部分,其他符号可通过正文了解。上述符号可能部分暂时不懂,但没关系,读者可在阅读的过程中,随时返回来查看,即可理解每个符号所代表的意义。
一、SVM是什么
关于SVM是什么,之前在Byte Size Biology上看到有篇文章很好的解释了SVM,在知乎上也有一位名叫“简之”的用户通过故事的形式来将其进行转述,通俗易懂,很好的向首次接触SVM的读者介绍了SVM能干嘛。
- Support Vector Machines explained well(可能需要翻墙):http://bytesizebio.net/2014/02/05/support-vector-machines-explained-well/
- 支持向量机(SVM)是什么意思?:https://www.zhihu.com/question/21094489/answer/86273196

油管上也有更为直观认识SVM的短视频(翻墙):https://www.youtube.com/watch?v=3liCbRZPrZA
总结一哈:
对于一个二分类问题,样本数据是线性可分的,我们则需要通过一根直线(也就是上述例子当中枝条)将两个不同种类的样本进行分离。按道理来讲,我们已经实现了需求,但是,这根枝条的具体摆放位置可能有无数多个,而我们的最终目的是将枝条摆放到一个最好的位置,从而当我们引入了一些新样本的时候,依然能最好很好将两类的数据分离开来,也就是说我们需要将模型的“泛化性能”最大化。之前也看到过一个例子(来源忘了),这里分享下,大概就是讲:我们在通过悬崖吊桥的时候,会不自觉的尽可能往中间走,因为这样的话,当左右起风的时候,虽然我们的位置会左右稍微移动,但还不至于跌落悬崖。而越靠近边缘,风险就越大,就是这么个道理。而寻找最大“泛化性能”的过程,就是将枝条摆放在距离小球最远的位置,而小球相对于枝条的位置就是“间隔”,而我们要做的就是将这间隔最大化。
上述仅仅是对于线性可分数据分类的一种处理方式,但有的时候,理想是美好的,现实却是残酷的。在实际样本数据中,大多都是线性不可分的,也就是说我们无法找到合适位置的枝条将不同分类的数据分离开来,更别提“间隔最大化”了。这个时候,我们就需要将桌上的小球排起,然后用一个平面(纸)将不同分类小球分离开来。也就是说,我们将低维度映射到了高纬度,这样对于小球的分类就更加容易了。
再之后,无聊的大人们,把这些球叫做 「data」,把棍子 叫做 「classifier」, 最大间隙trick 叫做「optimization」, 拍桌子叫做「kernelling」, 那张纸叫做「hyperplane」。
二、线性可分SVM与间隔最大化
我们先来看具体看看线性可分的二分类问题。
假如说,我们这里有一堆样本,也就是我们常说的训练集属性特征向量,其内部有多个不同属性,这里我们不妨指定每个样本含有两个属性特征,也就是说
标签,由于这里的问题属性二分类,所以
import numpy as np
import pylab as pl
from sklearn import svm
%matplotlib inline
print(np.__version__)
"""
Author: Taoye
微信公众号:玩世不恭的Coder
"""
if __name__ == "__main__":
# np.random.seed(100) # 可自行设置随机种子每次随机产生相同的数据
X_data = np.concatenate((np.add(np.random.randn(20, 2), [3, 3]),
np.subtract(np.random.randn(20, 2), [3, 3])),
axis = 0) # random随机生成数据,+ -3达到不同类别数据分隔的目的
Y_label = np.concatenate((np.zeros([20]), np.ones([20])), axis = 0)
svc = svm.SVC(kernel = "linear")
svc.fit(X_data, Y_label)
w = svc.coef_[0]
ww = -w[0] / w[1] # 获取权重w
xx = np.linspace(-6, 6)
yy = ww * xx - (svc.intercept_[0]) / w[1] # intercept_获取结截距
b_down = svc.support_vectors_[0] # 得到对应的支持向量
yy_down = ww * xx + (b_down[1] - ww * b_down[0])
b_up = svc.support_vectors_[-1]
yy_up = ww * xx + (b_up[1] - ww * b_up[0])
pl.plot(xx, yy, "k-"); pl.plot(xx, yy_down, "k--"); pl.plot(xx, yy_up, "k--")
pl.scatter(X_data[:, 0], X_data[:, 1], c = Y_label, cmap = pl.cm.Paired)
pl.show()
执行代码,可以绘制如下所示图片,注意:以上代码每次运行都会随机产生不同的二分类数据集,如想每次随机产生相同的数据集,可自行配置np.random.seed
随机种子;另外,还有一点需要需要说明的是,上述代码使用到了NumPy,关于NumPy的使用,可自行参考之前写的一篇文章:print( "Hello,NumPy!" )
如上图所示,我们可以发现,棕色代表一类数据集,此时标签“决策面”或“超平面”,而其所表示的方程,我们一般称作“决策方程”或“超平面方程”,在这里可以表示为
从上图我们还可以观察得到,在所有样本数据集中,虚线上的样本距离决策面最近,我们把这种比较特殊的样本数据一般称之为“支持向量”,而支持向量到决策面之间的距离称为“间隔”。我们不难发现,决策面的位置主要取决于支持向量,而与支持向量除外的数据样本没有关系。(因为支持向量的确定就已经确定了最大间隔)
关于上述提到的一些关于SVM的名词概念,在正式推演之前,还是有必要理解清楚的。
前面我们也有提到,关于能将两类不同数据集相互分隔开来的直线有无数种,而我们要想在这无数种直线找到一条最合适的,也就是达到一个间隔最大化的目的,这就是一个“最优化”问题。而最优化问题,我们需要了解两个因素,分别是目标函数和优化对象。既然我们是想要达到间隔最大化的目标,那么目标函数自然就是间隔,而优化对象就是我们的决策面方程。所以,我们首先需要用数学来明确间隔和决策面方程:
我们知道,在平面直角坐标系中,一条直线可以用其一般式方程来来表示:
而根据上述图像,我们可以知道,横纵坐标代表的意义是一个样本的不同属性特征,而标签则是通过颜色(棕色和蓝色)来进行表示。所以上述的直线的一般式方程中的
不能识别此Latex公式:
\left(
\begin{matrix}
w_1, w_2 \\
\end{matrix}
\right)
\times
\left(
\begin{matrix}
x^{(1)} \\
x^{(2)}
\end{matrix}
\right) \
+b=0 \tag{2-1}
令
式(1-2)表示的就是我们优化问题的优化对象,也就是决策面方程。我们知道在平面直角坐标系中,一条直线可以通过其斜率和截距来确定,而在决策面方程里,我们不难得到:间隔的表达式了。
在此,我们需要引入函数间隔和几何间隔的概念了。
一般来讲,我们可以通过样本点到决策面的距离来表示分类预测的正确程度,距离决策面越远,一般分类就越正确(可根据图像自行理解),而在超平面函数间隔(这个概念务必要理解清楚),我们不妨用
而我们知道,上述的函数间隔定义为
我们只有函数间隔还不够,函数间隔描述的仅仅是样本分类的正确性和正确程度,而不是确切的间隔。当我们成比例的更改几何间隔,我们不妨用
我们可以将式子(1-5)理解成点到直线的距离公式(这个在中学时期就学过的)。对于这个二分类问题,我们不妨将二分类的标签定为 之所以乘以
):
而我们知道,上述的几何间隔定义为
通过上述对函数间隔和几何间隔的分析,我们可以得到他们之间的关系:
自此,我们已经分析得到了该优化问题的优化对象——决策面方程和目标函数——间隔(几何间隔)。在之前,我们提到了支持向量的概念,那么支持向量具有什么特性呢?细想一下不难发现,支持向量到决策平面的间隔是最近的,即满足如下式子:
对此,我们就可以通过数学来表达该优化问题:
前面,我们提到了,函数间隔描述的仅仅是样本分类的正确性和正确程度,而不是确切的间隔。当我们成比例的更改
关于如上提到的决策平面和间隔等概念,我们可以通过下图来直观感受下(理解清楚):

至此,我们已经得到了该优化问题的数学表达,我们不妨通过一个小例子来检测下:
例子来源:李航-《统计学习方法》第七章内容
例子1:已知一个如图所示的训练数据集,其正例点是
根据前面的优化问题表达,我们可以得到如下表示:
求解可以得到目标函数的最小时,
其中,
三、拉格朗日乘数法和对偶问题
首先,我们有必要先了解下什么是拉格朗日乘数法。
关于对偶问题,《统计学习方法》一书中是如此描述的:
为了求解线性可分的支持向量机的最优化问题,将它作为原始最优化问题,应用拉格朗日对偶性,通过求解对偶问题(dual problem)得到原始问题(primary problem)的最优解,这就是线性可分支持向量机的对偶算法(dual algorithm)。这样做的优点,一是对偶问题往往更容易求解;二是自然引入核函数,进而推广到非线性分类问题。
按照前面对优化问题的求解思路,构造拉格朗日方程的目的是将约束条件放到目标函数中,从而将有约束优化问题转换为无约束优化问题。我们仍然秉承这一思路去解决不等式约束条件下的优化问题,那么如何针对不等式约束条件下的优化问题构建拉格朗日函数呢?
因为我们要求解的是最小化问题,所以一个直观的想法是如果我能够构造一个函数,使得该函数在可行解区域内与原目标函数完全一致,而在可行解区域外的数值非常大,甚至是无穷大,那么这个没有约束条件的新目标函数的优化问题就与原来有约束条件的原始目标函数的优化是等价的问题。
对此,我们重新回顾下原优化问题的数学表达:
关于拉个朗日乘数法的解题思路,其实早在大学《高等数学》这门课程中就已经提到过,我们不妨通过一个小例子来了解下其解题的一般形式:
例子来源:李亿民-《微积分方法》第二章内容
例子2:求函数
做拉格朗日函数
上面的拉格朗日函数,我们分别对
求解得到
上例就是利用拉格朗日求解最优问题的一般思路,先构造拉格朗日函数,然后分别对参数求偏导,最终求解得到最优解。而我们要想得到最大间隔,同样可以根据该思路进行,只不过式子更加复杂,而我们还需要利用拉格朗日的对偶性来简化优化问题。
在此之前,我们先来回顾下该优化问题下的数学表达:
按照我们例2的思路,我们首先构造出该优化问题的拉格朗日函数,注意:(2-1)式的限制条件是对于样本
其中,
令
根据上述目标函数,我们可以发现是先求最大值,再求最小值,而内层最小值是关于
且原问题和对偶问题满足如关系:
- 疑问1:为什么前面我们令$\theta(w,b)=\mathop{max}\limits_{\alpha>0} \ L(w,b,\alpha)$,而不是其他的表达形式呢?
主要是因为,这样替换之后,我们能使得该函数在可行解区域内与原目标函数完全一致,而在可行解区域外的数值非常大,甚至是无穷大,那么这个没有约束条件的新目标函数的优化问题就与原来有约束条件的原始目标函数的优化是等价的问题。(这句话要重点理解)
令:
之后,
此时,我们要求的是
- 疑问2:为什么将原问题转化为对偶问题之后,在什么样的条件下,才会满足$d^= p^$?
转化为对偶问题之后,要使得KKT条件:
前两个条件我们不难得到,前面我们也有过对其进行分析,那么第三个条件为什么需要满足呢?
在介绍支持向量和决策平面的时候,我们有提到,最终的决策平面只会与支持向量相关,而与其他大多数样本数据(非支持向量)无关。我们不妨来对它们分别介绍,当样本点为支持向量的时候,此时
综上,只有满足KKT条件的时候,才能到达
而要解决这个优化问题,我们可以分为两步来进行求解:
- 先求内层关于
- 再求外层关于
经过上述对目标函数的问题分析,我们下面根据上述的两个步骤来手握手式的进行求解。
四、模型的求解
关于拉格朗日的内层求解
首先,我们需要对内层的最小值问题进行求解,即求:
注意,此时详细过程如下:
为了让读者彻底明白上述过程,所以步骤有点多,这里就不采用Latex语法来编辑上述公式的推导过程了,当然了,Taoye会尽可能地将过程写的足够详细。上述关于拉格朗日函数求偏导的过程,自认为已经写的很详细了,最主要的是要区分。对于上述过程有不清楚的,随时欢迎联系Taoye或在下方留言,也欢迎更多读者来访微信公众号:玩世不恭的Coder。
对此,我们已经通过上面过程得到
之后,我们将式子(4-2)重新带回到(4-1)中的最小值问题中,即可消掉
上图就是将对
关于这个代回的过程,有两点还是有必要说一下的,这也是前几天实验室的同学存在疑问的地方。(参照上述图片来解疑)
- 疑问1:这里前一项难道不是和后一项同样为0么?因为不是说了$\sum_{i=1}^N\alpha_iy_i=0$啊???
其实这个地方的后一项是为0,而前一项并不一定为0。因为后一项中的
而我们再看看前一项,可以发现前一项中除了依然成立,而
2+33-45=02∗2+3∗3−4∗5=0就不成立了。
就是这个道理,务必要好好理解清楚。
- 疑问2:为什么这一步可以推导至下面那一步呢???
其实这个问题很好理解,因为这两个成绩形式的式子是相互独立的,也就是说笛卡尔积的形式,所以将前一个
依然成立,所以能推导至下面那一步。。。
通过上述过程,我们已经得到了代回之后的式子,如下:
并且我们观察可以发现,式子此时仅仅只存在
至此,我们已经解决了对偶问题的内层最小值问题,接下来我们就要求解外层的最大值问题了,将最小值的式子代回原对偶问题,我们更新下对偶问题,得到如下:
如上,已经将原对偶转换为了上式的样子,下面我们据此再来看之前的例1
例子来源:李航-《统计学习方法》第七章内容
例子3:已知一个如图所示的训练数据集,其正例点是
根据所给数据,其对偶问题是:
我们将
对
当
这样的话,就说明 综上,我们可以得到决策面最终为: 其中, 历经九九八十一难,终于来到了这最后一步了,只要我们能求得上式的最小值,那么模型的求解也就该到一段落了。 那么,问题来了,关于上式,我们应当如何求解呢??? 下面就应该是我们的重头戏闪亮登场了——SMO算法,各位看官掌声欢迎一下,有请SMO大佬登台表演。。。 关于SMO算法,李航老师的《统计学习方法》一书中是这么描述的, 关于外层问题的求解,我们有许多方法可供使用。当我们的数据样本比较少的时候,可以将支持向量机的学习问题转换为凸二次规划问题,这样的凸二次规划问题具有全局最优解,并且有许多最优化算法可以用于这一问题的求解。但是当训练样本容量很大时,这些算法往往变得非常低效,以致无法使用。所以,如何高效地实现支持向量机学习就成为一个重要的问题。目前人们已提出许多快速实现算法。而在这些算法中,要数Platt的序列最小最优化(sequential minimal optimization SMO)算法最为人所知。 SMO算法是一种启发式算法,其基本思想是:如果所有变量的解都满足此最优化问题的KKT条件,那么这个最优化问题的解就得到了。因为KKT条件是该最优化问题的充分必要条件。否则,选择两个变量,固定其他变量,针对这两个变量构建一个二次规划问题。这个二次规划问题关于这两个变量的解就应该更接近原始二次规划问题的解,因为这会使得原始二次规划问题的目标函数值变得更小。更重要的是,这时子问题可以通过解析方法求解,这样就可以大大提高整个算法的计算速度。子问题有两个变量,一个是违反KKT条件最严重的那一个,另一个由约束条件自动确定。如此,SMO算法将原问题不断分解为子问题并对子问题求解,进而达到求解原问题的目的。 上述内容来自李航——《统计学习方法》第二版。 不知道大伙读完上述关于内容是什么感受,这里简单总结一下李航老师所表达的意思吧。 在Taoye的印象里,小学时期上语文课的时候学习过一篇文章叫做《走一步,再走一步》。(具体几年级就记不清楚了) 嘿!您还别说,刚刚去搜索了下这篇课文,还真就叫这个名儿。第一次读李航老师《统计学习方法》中关于SMO的内容之后,就让我想起这篇文章。我还专门重新读了一下这篇文章,主要讲的内容是这样的: 文章中主人公名叫小亨特,他不是天生体弱怯懦嘛,在一次和小伙伴攀登悬崖的时候,由于内心的恐惧、害怕,在攀登途中上不去,也下不来。然后呢,他的小伙伴杰里就把小亨特的父亲找来了,父亲对小亨特说:“不要想有多远,有多困难,你需要想的是迈一小步。这个你能做到。看着手电光指的地方。看到那块石头没有?”,最终通过父亲的鼓励,小亨特成功脱险。文末作者还总结道:在我生命中有很多时刻,每当我遇到一个遥不可及、令人害怕的情境,并感到惊慌失措时,我都能够应付——因为我回想起了很久以前自己上过的那一课。我提醒自己不要看下面遥远的岩石,而是注意相对轻松、容易的第一小步,迈出一小步、再一小步,就这样体会每一步带来的成就感,直到完成了自己想要完成的,达到了自己的目标,然后再回头看时,不禁对自己走过的这段漫漫长路感到惊讶和自豪。 把《走一步,再走一步》这篇文章搬出来,真的不是在凑字数从而给大家阅读带来压力,只是觉得SMO算法描述的就是这么个理儿。算了,不多说了,说多了还真的会有凑字数的嫌疑。(ノへ ̄、) 下面我们开始进入到SMO吧。。。 在这之前,我们把外层的最小值问题再搬出来: 在这里,我们是假设对于所有的样本数据都是100%线性可分的。 对于该优化问题的SMO算法,我们可以这样理解:因为在我们的数据集中, 讲到这里,SMO算法是不是和《走一步,再走一步》中主人公类似呢?将一个大的、复杂的问题转换成多个小问题,然后不断的迭代更新。 为什么我们每次都同时优化两个参数,而不是一个呢?因为每次更新两个参数,才能确保 据SMO的思想,我们不妨把目标函数中的 注意:因为我们的 我们知道对于这个式子是有一个约束条件的,我们可以根据这个用 通过上式,用 此时的 上述就是SMO中限制其中两个变量的推到过程的推到过程(公式太多,过程有点复杂,确实不方便使用Latex语法,不过过程都已经写的很详细了,还是需要静下心来慢慢手动推导的)下面总结一下上述SMO算法的过程吧: 前面我们不是得到了仅仅关于 此时,我们可以发现除了数据样本相关信息和 所以,我们重新将 之后我们通过前面拉格朗日得到的关系式,用 PS:关于 通过如上式子,我们就能求得更新之后的 还有一点需要注意的是,上述过程都是默认所有样本数据都是线性可分的,也就是说没有一个样本会被误分类。但这只是理想状态下,而实际不免会有个别数据不得不被误分类,这时我们需要定义惩罚参数和容错率,而容错率是用来不断优化 而我们知道: 综合上式,可以确定 而这个在不同情况下的 接下来,就是更新 前面,我们已经得到: 因为我们是打算通过 同时,因为上述中的级数形式可以使用 同理,可以得到 当更新之后的 如此一来,我们就已经完成了SMO算法的流程,该有的参数都已经求解出来了。 说实话,写到这,Taoye的确有点累了,脑细胞也严重不足了,但为了各位“老婆们”的正常阅读,还是得继续写下去才行。 下面,我们就通过编程来实现线性SVM算法吧!(本次手撕SVM的数据集依然采用我们前面所随机创建的),根据
基于SMO算法的外层求解
只是对
,而
此时我们得到关于
五、编程手撕线性SVM
在前面,我们其实已经实现了线性SVM的分类,只不过那个时候使用的是sklearn
内置的接口。但既然是手撕SVM,当然是需要自己来实现这一功能的。
在这里需要提前说明的是,上述代码大量使用到了NumPy操作,关于NumPy的使用,可自行参考之前写的一篇文章:print( "Hello,NumPy!" )
训练SVM模型,没数据集可不行,本次手撕SVM的数据集依然采用我们前面所随机创建,对此,我们定义一个etablish_data
方法来随机创建一个SVM二分类数据集:
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 用于生成训练数据集
Parameters:
data_number: 样本数据数目
Return:
x_data: 数据样本的属性矩阵
y_label: 样本属性所对应的标签
"""
def etablish_data(data_number):
x_data = np.concatenate((np.add(np.random.randn(data_number, 2), [3, 3]),
np.subtract(np.random.randn(data_number, 2), [3, 3])),
axis = 0) # random随机生成数据,+ -3达到不同类别数据分隔的目的
temp_data = np.zeros([data_number])
temp_data.fill(-1)
y_label = np.concatenate((temp_data, np.ones([data_number])), axis = 0)
return x_data, y_label
前面,我们在讲解SMO算法的时候提到,每次都会选取随机两个 我们知道,每一个更新之后的 我们模型训练是一个迭代更新的过程,而更新的前提是误差比较大,所以我们需要定义一个方法来进行更新,这里我们不妨将第一个
np.random模块来随机选取,这里需要主要的是,第二个选取的
random_select_alpha_j来随机选取第二个
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 随机选取alpha_j
Parameters:
alpha_i_index: 第一个alpha的索引
alpha_number: alpha总数目
Return:
alpha_j_index: 第二个alpha的索引
"""
def random_select_alpha_j(alpha_i_index, alpha_number):
alpha_j_index = alpha_i_index
while alpha_j_index == alpha_i_index:
alpha_j_index = np.random.randint(0, alpha_number)
return alpha_j_indexmodify_alpha来确定
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 使得alpha_j在[L, R]区间之内
Parameters:
alpha_j: 原始alpha_j
L: 左边界值
R: 右边界值
Return:
L,R,alpha_j: 修改之后的alpha_j
"""
def modify_alpha(alpha_j, L, R):
if alpha_j < L: return L
if alpha_j > R: return R
return alpha_jcalc_E_i
来计算误差,但误差又怎么计算呢?这一点其实我们在最开始就已经提到过了,误差是通过模型计算出来的值与其真实值最差得到,也就是前面提到的下面的推导务必要理解清楚,矩阵变换要十分熟悉
根据上述误差的推导,我们现在就可以通过代码来计算误差了(上面的推导务必要理解清楚,矩阵变换要十分熟悉,才能理解下面代码所表达的含义):
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 计算误差并返回
"""
def calc_E(alphas, y_lable, x_data, b, i):
f_x_i = float(np.multiply(alphas, y_lable).T * (x_data * x_data[i, :].T)) + b
return f_x_i - float(y_label[i])
同样的,我们把其他一些用于整体代换的单独拎出来,并通过方法进行返回,除了上述的误差
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 计算eta并返回
"""
def calc_eta(x_data, i, j):
eta = 2.0 * x_data[i, :] * x_data[j, :].T \
- x_data[i, :] * x_data[i, :].T \
- x_data[j, :] * x_data[j,:].T
return eta
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 计算b1, b2并返回
"""
def calc_b(b, x_data, y_label, alphas, alpha_i_old, alpha_j_old, E_i, E_j, i, j):
b1 = b - E_i \
- y_label[i] * (alphas[i] - alpha_i_old) * x_data[i, :] * x_data[i, :].T \
- y_label[j] * (alphas[j] - alpha_j_old) * x_data[i, :] * x_data[j, :].T
b2 = b - E_j \
- y_label[i] * (alphas[i] - alpha_i_old) * x_data[i, :] * x_data[j, :].T \
- y_label[j] * (alphas[j] - alpha_j_old) * x_data[j, :] * x_data[j, :].T
return b1, b2
OK,准备工作已经完成了,接下来是时候放出我们的核心SMO算法的代码了,大家可根据前面的SMO思想来理解,下面代码也会放出详细的注释来帮助大家理解:
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: SMO核心算法,求得b和laphas向量
Parameters:
x_data: 样本属性特征矩阵
y_label: 属性特征对应的标签
C:惩罚参数
toler:容错率
max_iter:迭代次数
Return:
b: 决策面的参数b
alphas:获取决策面参数w所需要的alphas
"""
def linear_smo(x_data, y_label, C, toler, max_iter):
x_data = np.mat(x_data); y_label = np.mat(y_label).T # 将数据转换为矩阵类型
m, n = x_data.shape # 得到数据样本的shape信息
b, alphas, iter_num = 0, np.mat(np.zeros((m, 1))), 0 # 初始化参数b和alphas和迭代次数
while (iter_num < max_iter): # 最多迭代max_iter次
alpha_optimization_num = 0 # 定义优化次数
for i in range(m): # 遍历每个样本,一次选取一个样本计算误差
E_i = calc_E(alphas, y_label, x_data, b, i) # 样本i的误差计算
if ((y_label[i] * E_i < -toler) and (alphas[i] < C)) or ((y_label[i] * E_i > toler) and (alphas[i] > 0)):
j = random_select_alpha_j(i, m) # 随机选取一个不与i重复j
E_j = calc_E(alphas, y_label, x_data, b, j) # 计算样本j的误差
alpha_i_old = alphas[i].copy(); alpha_j_old = alphas[j].copy();
if (y_label[i] != y_label[j]): # 重新规范alphas的左右区间
L, R = max(0, alphas[j] - alphas[i]), min(C, C + alphas[j] - alphas[i])
else:
L, R = max(0, alphas[j] + alphas[i] - C), min(C, alphas[j] + alphas[i])
if L == R: print("L==R"); continue # L==R时选取下一个样本
eta = calc_eta(x_data, i, j) # 计算eta值
if eta >= 0: print("eta>=0"); continue
alphas[j] -= y_label[j] * (E_i - E_j) / eta
alphas[j] = modify_alpha(alphas[j], L, R) # 修改alpha[j]
if (abs(alphas[j] - alpha_j_old) < 0.00001): print("alpha_j更改太小"); continue
alphas[i] += y_label[j] * y_label[i] * (alpha_j_old - alphas[j]) # 修改alphas[i]
b1, b2= calc_b(b, x_data, y_label, alphas, alpha_i_old, alpha_j_old, E_i, E_j, i, j) # 计算b值
if (0 < alphas[i]) and (C > alphas[i]): b = b1
elif (0 < alphas[j]) and (C > alphas[j]): b = b2
else: b = (b1 + b2)/2.0
alpha_optimization_num += 1
print("迭代次数:%d,样本:%d,alphas向量的优化次数:%d" % (iter_num, i+1, alpha_optimization_num))
if (alpha_optimization_num == 0): iter_num += 1
else: iter_num = 0
print("迭代次数:%d" % iter_num)
return b, alphas
上述SMO核心方法,我们可以通过定义输入样本的属性特征、标签以及迭代次数等来得到
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 根据公式计算出w权值向量
Parameters:
x_data: 样本属性特征矩阵
y_label: 属性特征对应的标签
alphas:linear_smo方法所返回的alphas向量
Return:
w: 决策面的参数w
"""
def calc_w(x_data, y_label, alphas):
x_data, y_label, alphas = np.array(x_data), np.array(y_label), np.array(alphas)
return np.dot((np.tile(y_label.reshape(1, -1).T, (1, 2)) * x_data).T, alphas).tolist()
好的,plot_result方法来展示模型分类结果:
import pylab as pl
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 绘制出分类结果
Parameters:
x_data: 样本属性特征矩阵
y_label: 属性特征对应的标签
w:决策面的w参数
b:决策面的参数b
"""
def plot_result(x_data, y_label, w, b):
data_number, _ = x_data.shape; middle = int(data_number / 2)
plt.scatter(x_data[:, 0], x_data[:, 1], c = y_label, cmap = pl.cm.Paired)
x1, x2 = np.max(x_data), np.min(x_data)
w1, w2 = w[0][0], w[1][0]
y1, y2 = (-b - w1 * x1) / w2, (-b - w1 * x2) / w2
plt.plot([float(x1), float(x2)], [float(y1), float(y2)]) # 绘制决策面
for index, alpha in enumerate(alphas):
if alpha > 0:
b_temp = - w1 * x_data[index][0] - w2 * x_data[index][1]
y1_temp, y2_temp = (-b_temp - w1 * x1) / w2, (-b_temp - w1 * x2) / w2
plt.plot([float(x1), float(x2)], [float(y1_temp), float(y2_temp)], "k--") # 绘制支持向量
plt.scatter(x_data[index][0], x_data[index][1], s=150, c='none', alpha=0.7, linewidth=2, edgecolor='red') # 圈出支持向量
plt.show()
if __name__ == "__main__":
x_data, y_label = etablish_data(50)
b, alphas = linear_smo(x_data, y_label, 0.8, 0.0001, 40)
w = calc_w(x_data, y_label, alphas)
plot_result(x_data, y_label, w, b)

完整代码:
import numpy as np
import pylab as pl
from matplotlib import pyplot as plt
class TearLinearSVM:
def __init__(self):
pass
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 用于生成训练数据集
Parameters:
data_number: 样本数据数目
Return:
x_data: 数据样本的属性矩阵
y_label: 样本属性所对应的标签
"""
def etablish_data(self, data_number):
x_data = np.concatenate((np.add(np.random.randn(data_number, 2), [3, 3]),
np.subtract(np.random.randn(data_number, 2), [3, 3])),
axis = 0) # random随机生成数据,+ -3达到不同类别数据分隔的目的
temp_data = np.zeros([data_number])
temp_data.fill(-1)
y_label = np.concatenate((temp_data, np.ones([data_number])), axis = 0)
return x_data, y_label
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 随机选取alpha_j
Parameters:
alpha_i_index: 第一个alpha的索引
alpha_number: alpha总数目
Return:
alpha_j_index: 第二个alpha的索引
"""
def random_select_alpha_j(self, alpha_i_index, alpha_number):
alpha_j_index = alpha_i_index
while alpha_j_index == alpha_i_index:
alpha_j_index = np.random.randint(0, alpha_number)
return alpha_j_index
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 使得alpha_j在[L, R]区间之内
Parameters:
alpha_j: 原始alpha_j
L: 左边界值
R: 右边界值
Return:
L,R,alpha_j: 修改之后的alpha_j
"""
def modify_alpha(self, alpha_j, L, R):
if alpha_j < L: return L
if alpha_j > R: return R
return alpha_j
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 计算误差并返回
"""
def calc_E(self, alphas, y_lable, x_data, b, i):
f_x_i = float(np.dot(np.multiply(alphas, y_lable).T, x_data * x_data[i, :].T)) + b
return f_x_i - float(y_label[i])
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 计算eta并返回
"""
def calc_eta(self, x_data, i, j):
eta = 2.0 * x_data[i, :] * x_data[j, :].T \
- x_data[i, :] * x_data[i, :].T \
- x_data[j, :] * x_data[j,:].T
return eta
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 计算b1, b2并返回
"""
def calc_b(self, b, x_data, y_label, alphas, alpha_i_old, alpha_j_old, E_i, E_j, i, j):
b1 = b - E_i \
- y_label[i] * (alphas[i] - alpha_i_old) * x_data[i, :] * x_data[i, :].T \
- y_label[j] * (alphas[j] - alpha_j_old) * x_data[i, :] * x_data[j, :].T
b2 = b - E_j \
- y_label[i] * (alphas[i] - alpha_i_old) * x_data[i, :] * x_data[j, :].T \
- y_label[j] * (alphas[j] - alpha_j_old) * x_data[j, :] * x_data[j, :].T
return b1, b2
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: SMO核心算法,求得b和laphas向量
Parameters:
x_data: 样本属性特征矩阵
y_label: 属性特征对应的标签
C:惩罚参数
toler:容错率
max_iter:迭代次数
Return:
b: 决策面的参数b
alphas:获取决策面参数w所需要的alphas
"""
def linear_smo(self, x_data, y_label, C, toler, max_iter):
x_data = np.mat(x_data); y_label = np.mat(y_label).T # 将数据转换为矩阵类型
m, n = x_data.shape # 得到数据样本的shape信息
b, alphas, iter_num = 0, np.mat(np.zeros((m, 1))), 0 # 初始化参数b和alphas和迭代次数
while (iter_num < max_iter): # 最多迭代max_iter次
alpha_optimization_num = 0 # 定义优化次数
for i in range(m): # 遍历每个样本,一次选取一个样本计算误差
E_i = self.calc_E(alphas, y_label, x_data, b, i) # 样本i的误差计算
if ((y_label[i] * E_i < -toler) and (alphas[i] < C)) or ((y_label[i] * E_i > toler) and (alphas[i] > 0)):
j = self.random_select_alpha_j(i, m) # 随机选取一个不与i重复j
E_j = self.calc_E(alphas, y_label, x_data, b, j) # 计算样本j的误差
alpha_i_old = alphas[i].copy(); alpha_j_old = alphas[j].copy();
if (y_label[i] != y_label[j]): # 重新规范alphas的左右区间
L, R = max(0, alphas[j] - alphas[i]), min(C, C + alphas[j] - alphas[i])
else:
L, R = max(0, alphas[j] + alphas[i] - C), min(C, alphas[j] + alphas[i])
if L == R: print("L==R"); continue # L==R时选取下一个样本
eta = self.calc_eta(x_data, i, j) # 计算eta值
if eta >= 0: print("eta>=0"); continue
alphas[j] -= y_label[j] * (E_i - E_j) / eta
alphas[j] = self.modify_alpha(alphas[j], L, R) # 修改alpha[j]
if (abs(alphas[j] - alpha_j_old) < 0.00001): print("alpha_j更改太小"); continue
alphas[i] += y_label[j] * y_label[i] * (alpha_j_old - alphas[j]) # 修改alphas[i]
b1, b2= self.calc_b(b, x_data, y_label, alphas, alpha_i_old, alpha_j_old, E_i, E_j, i, j) # 计算b值
if (0 < alphas[i]) and (C > alphas[i]): b = b1
elif (0 < alphas[j]) and (C > alphas[j]): b = b2
else: b = (b1 + b2)/2.0
alpha_optimization_num += 1
print("迭代次数:%d,样本:%d,alphas向量的优化次数:%d" % (iter_num, i+1, alpha_optimization_num))
if (alpha_optimization_num == 0): iter_num += 1
else: iter_num = 0
print("迭代次数:%d" % iter_num)
return b, alphas
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 根据公式计算出w权值向量
Parameters:
x_data: 样本属性特征矩阵
y_label: 属性特征对应的标签
alphas:linear_smo方法所返回的alphas向量
Return:
w: 决策面的参数w
"""
def calc_w(self, x_data, y_label, alphas):
x_data, y_label, alphas = np.array(x_data), np.array(y_label), np.array(alphas)
return np.dot((np.tile(y_label.reshape(1, -1).T, (1, 2)) * x_data).T, alphas).tolist()
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain: 绘制出分类结果
Parameters:
x_data: 样本属性特征矩阵
y_label: 属性特征对应的标签
w:决策面的w参数
b:决策面的参数b
"""
def plot_result(self, x_data, y_label, w, b):
data_number, _ = x_data.shape; middle = int(data_number / 2)
plt.scatter(x_data[:, 0], x_data[:, 1], c = y_label, cmap = pl.cm.Paired)
x1, x2 = np.max(x_data), np.min(x_data)
w1, w2 = w[0][0], w[1][0]
y1, y2 = (-b - w1 * x1) / w2, (-b - w1 * x2) / w2
plt.plot([float(x1), float(x2)], [float(y1), float(y2)]) # 绘制决策面
for index, alpha in enumerate(alphas):
if alpha > 0:
b_temp = - w1 * x_data[index][0] - w2 * x_data[index][1]
y1_temp, y2_temp = (-b_temp - w1 * x1) / w2, (-b_temp - w1 * x2) / w2
plt.plot([float(x1), float(x2)], [float(y1_temp), float(y2_temp)], "k--") # 绘制支持向量
plt.scatter(x_data[index][0], x_data[index][1], s=150, c='none', alpha=0.7, linewidth=2, edgecolor='red') # 圈出支持向量
plt.show()
if __name__ == '__main__':
linear_svm = TearLinearSVM()
x_data, y_label = linear_svm.etablish_data(50)
b, alphas = linear_svm.linear_smo(x_data, y_label, 0.8, 0.0001, 40)
w = linear_svm.calc_w(x_data, y_label, alphas)
linear_svm.plot_result(x_data, y_label, w, b)
呼呼呼!可算是结束了,做个小总结吧。
SVM是学习机器学习必然接触到的一个重要算法,所以一定要对其内在原理了解清楚,并不是说一定要手撕SVM的完整代码,但最起码使用框架的时候要了解内部都做了什么“小动作”,不要为了用而用。
本文介绍了线性SVM的算法原理,主要分为了五个部分的内容。一、首先通过参考比较权威的书籍以及优秀资料对SVM做了一个比较“良心”的介绍,让读者对SVM有一个比较宏观的概念,这小子(SVM)究竟是谁?竟如此骚气,让不少研究者拜倒其石榴裙下。二、其次向读者介绍了线性SVM以及最大间隔,这部分也是手撕SVM必须要掌握的一些基本概念,并且最终得到了SVM最初的优化问题。三、利用拉格朗日乘数法构建最值问题,将优化问题中的约束问题集成到了目标函数本身,之后利用拉格朗日的对偶性,将最初的优化问题转化成了内层关于
说实话,这篇文章有点肝,也是挪用了不少其他任务的时间。
这篇文章仅仅只是手撕线性SVM,也就是说大多数据样本都可以被正确分类,但在实际中,许多的数据集都是线性不可分的,这个时候可能就要引入核函数的概念了。关于非线性SVM,我们留在之后再来肝。。。
本文参考了不少书籍资料以及许多大佬的技术文章,行文风格尽可能做到了通俗易懂,但其中涉及到的数学公式在所难免,还请诸读者静下心来慢慢品尝。由于个人水平有限,才疏学浅,对于SVM也只是略知皮毛,可能文中有不少表述稍有欠妥、有不少错误或不当之处,还请诸君批评指正,有任何问题欢迎在下方留言。
我是Taoye,爱专研,爱分享,热衷于各种技术,学习之余喜欢下象棋、听音乐、聊动漫,希望借此一亩三分地记录自己的成长过程以及生活点滴,也希望能结实更多志同道合的圈内朋友,更多内容欢迎来访微信公主号:玩世不恭的Coder
参考资料:
[1] 《机器学习实战》:Peter Harrington 人民邮电出版社
[2] 《统计学习方法》:李航 第二版 清华大学出版社
[3] 《机器学习》:周志华 清华大学出版社
[4] 《微积分方法》:李亿民 中国海洋出版社
[5] Support Vector Machines explained well(翻墙):http://bytesizebio.net/2014/02/05/support-vector-machines-explained-well/
[6] 关于更为直观认识SVM的video(翻墙):https://www.youtube.com/watch?v=3liCbRZPrZA
[7] 支持向量机(SVM)是什么意思?:https://www.zhihu.com/question/21094489/answer/86273196
[8] 看了这篇文章你还不懂SVM你就来打我:https://zhuanlan.zhihu.com/p/49331510
[9] 拉格朗日乘数法:https://www.cnblogs.com/maybe2030/p/4946256.html
推荐阅读
print( "Hello,NumPy!" )
干啥啥不行,吃饭第一名
Taoye渗透到一家黑平台总部,背后的真相细思极恐
《大话数据库》-SQL语句执行时,底层究竟做了什么小动作?
那些年,我们玩过的Git,真香
基于Ubuntu+Python+Tensorflow+Jupyter notebook搭建深度学习环境
网络爬虫之页面花式解析
手握手带你了解Docker容器技术
一文详解Hexo+Github小白建站
打开ElasticSearch、kibana、logstash的正确方式
《Machine Learning in Action》—— 剖析支持向量机,单手狂撕线性SVM的更多相关文章
- 《Machine Learning in Action》—— 剖析支持向量机,优化SMO
<Machine Learning in Action>-- 剖析支持向量机,优化SMO 薄雾浓云愁永昼,瑞脑销金兽. 愁的很,上次不是更新了一篇关于支持向量机的文章嘛,<Machi ...
- 《Machine Learning in Action》—— 懂的都懂,不懂的也能懂。非线性支持向量机
说在前面:前几天,公众号不是给大家推送了第二篇关于决策树的文章嘛.阅读过的读者应该会发现,在最后排版已经有点乱套了.真的很抱歉,也不知道咋回事,到了后期Markdown格式文件的内容就解析出现问题了, ...
- 《Machine Learning in Action》—— Taoye给你讲讲决策树到底是支什么“鬼”
<Machine Learning in Action>-- Taoye给你讲讲决策树到底是支什么"鬼" 前面我们已经详细讲解了线性SVM以及SMO的初步优化过程,具体 ...
- 《Machine Learning in Action》—— 小朋友,快来玩啊,决策树呦
<Machine Learning in Action>-- 小朋友,快来玩啊,决策树呦 在上篇文章中,<Machine Learning in Action>-- Taoye ...
- 《Machine Learning in Action》—— 白话贝叶斯,“恰瓜群众”应该恰好瓜还是恰坏瓜
<Machine Learning in Action>-- 白话贝叶斯,"恰瓜群众"应该恰好瓜还是恰坏瓜 概率论,可以说是在机器学习当中扮演了一个非常重要的角色了.T ...
- 《Machine Learning in Action》—— 浅谈线性回归的那些事
<Machine Learning in Action>-- 浅谈线性回归的那些事 手撕机器学习算法系列文章已经肝了不少,自我感觉质量都挺不错的.目前已经更新了支持向量机SVM.决策树.K ...
- 《Machine Learning in Action》—— Taoye给你讲讲Logistic回归是咋回事
在手撕机器学习系列文章的上一篇,我们详细讲解了线性回归的问题,并且最后通过梯度下降算法拟合了一条直线,从而使得这条直线尽可能的切合数据样本集,已到达模型损失值最小的目的. 在本篇文章中,我们主要是手撕 ...
- 【机器学习实战】Machine Learning in Action 代码 视频 项目案例
MachineLearning 欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远 Machine Learning in Action (机器学习实战) | ApacheCN(apa ...
- 学习笔记之机器学习实战 (Machine Learning in Action)
机器学习实战 (豆瓣) https://book.douban.com/subject/24703171/ 机器学习是人工智能研究领域中一个极其重要的研究方向,在现今的大数据时代背景下,捕获数据并从中 ...
随机推荐
- 网站搭建-云服务器ECS-镜像管理
学习笔记: 快照,系统盘可创建镜像,数据盘不可以. 实例可以直接创建镜像,包括系统盘和数据盘 复制镜像: 新购服务器,选择镜像(又买). 共享镜像: 账号ID就是UID 云市场获取镜像; 1. 创建新 ...
- 实验二 HTML中图片和超链接的应用
实验二 HTML中图片和超链接的应用 [实验目的] 1.通过本例要求掌握常见的图像格式及图像的插入方法. 2.能够修改图像属性,利用外部图像处理软件编辑图像. 3.掌握设置各类超级连接的方法. 4.灵 ...
- 2014年 实验二 B2C网上购物
实验二 B2C网上购物 [实验目的] ⑴.熟悉虚拟银行和网上支付的应用 ⑵.熟悉并掌握消费者B2C网上购物和商家的销售处理 [实验条件] ⑴.个人计算机一台 ⑵.计算机通过局域网形式接入互联网 (3) ...
- day09 Pyhton学习
一.昨日内容回顾 文件操作 open(文件路径,mode="模式",encoding="编码") 文件路径: 1.绝对路径 从磁盘根目录寻找 2.相对路径 相对 ...
- python 不可变类型
不可变类型有:字符串,元祖,数字 可变类型:列表,字典 字典中,可变类型不能为key值 #在函数中 可变类型,为全局变量时,会变化 不可变类型,为全局变量时,不会变化
- C语言中数组与指针的异同之处!你不知道的编程奥秘~
C语言的数组和指针一直是两个容易混淆的东西,当初在学习的时候,也许为了通过考试会对指针和数组的一些考点进行突击,但是很多极其细节的东西也许并不是那么清楚.本篇侧重点在于分析数组与指针的关系,什么时候数 ...
- centos8平台:用fontconfig安装及管理字体(fc-list/fc-match/fc-cache)
一,fc-list所属的rpm包 [root@blog ~]$ whereis fc-list fc-list: /usr/bin/fc-list /usr/share/man/man1/fc-lis ...
- document.all.WebBrowser为空或不是对象
项目中也想用这个功能,发现出错,经过测试,一定要加<object id="WebBrowser" width=0 height=0 classid="CLSID:8 ...
- JS 计算日期相减得天数
言简意赅不呼哨直接懂,可以封装的可以根据自己的需求封装一下 var date1="2020-10-23";var date2="2020-10-26";var ...
- java 第一课 笔记
java是一种解释型语言 Java提供了内存自动管理:不涉及指针:单继承. classpath:字节码文件的路径,执行java.exe时,会查找并解释*.class文件 set classpath=. ...