1 支持向量机介绍
支持向量机(support vector machine,SVM)是有监督学习中最有影响力的机器学习算法之一,该算法的诞生可追溯至上世纪 60 年代, 前苏联学者 Vapnik 在解决模式识别问题时提出这种算法模型,此后经过几十年的发展直至 1995 年, SVM 算法才真正的完善起来,其典型应用是解决手写字符识别问题。
SVM 是一种非常优雅的算法,有着非常完善的数学理论基础,其预测效果,在众多机器学习模型中“出类拔萃”。在深度学习没有普及之前,“支持向量机”可以称的上是传统机器学习中的“霸主”。
支持向量机是一种二分类模型,其基本模型定义为特征空间上的间隔最大的线性分类器,其学习策略便是间隔最大化,最终可转化为一个凸二次规划问题的求解。支持向量机的学习算法是求解凸二次规划的最优化算法。
基础的SVM算法是一个二分类算法,至于多分类任务,可以通过多次使用SVM进行解决。
1.1 线性可分
对于一个数据集合可以画一条直线将两组数据点分开,这样的数据成为线性可分(linearly separable),如下图所示:
- 分割超平面:将上述数据集分隔开来的直线成为分隔超平面。对于二维平面来说,分隔超平面就是一条直线。对于三维及三维以上的数据来说,分隔数据的是个平面,称为超平面,也就是分类的决策边界。
- 间隔:点到分割面的距离,称为点相对于分割面的间隔。数据集所有点到分隔面的最小间隔的2倍,称为分类器或数据集的间隔。论文中提到的间隔多指这个间隔。SVM分类器就是要找最大的数据集间隔。
- 支持向量:离分隔超平面最近的那些点。
SVM所做的工作就是找这样个超平面,能够将两个不同类别的样本划分开来,但是这种平面是不唯一的,即可能存在无数个超平面都可以将两种样本分开,那么我们如何才能确定一个分类效果最好的超平面呢?
Vapnik提出了一种方法,对每一种可能的超平面,我们将它进行平移,直到它与空间中的样本向量相交。我们称这两个向量为支持向量,之后我们计算支持向量到该超平面的距离d,分类效果最好的超平面应该使d最大。
1.2 寻找最大间隔
(1)分隔超平面
二维空间一条直线的方程为,y=ax+b,推广到n维空间,就变成了超平面方程,即
w是权重,b是截距,训练数据就是训练得到权重和截距。
(2)如何找到最好的参数
支持向量机的核心思想: 最大间隔化, 最不受到噪声的干扰。如上图所示,分类器A比分类器B的间隔(蓝色阴影)大,因此A的分类效果更好。
SVM划分的超平面:f(x) = 0,w为法向量,决定超平面方向,
假设超平面将样本正确划分
f(x) ≥ 1,y = +1
f(x) ≤ −1,y = −1
间隔:r=2/|w|
约束条件:
(3)转化为凸优化
设 f(x) 为定义在n维欧式空间中某个凸集 S 上的函数,若对于任何实数α(0 < α< 1 )以及 S 中的不同两点 x,y ,均有:
那么,f(x)为定义在凸集 S 上的凸函数。
有约束的凸优化问题:
如果f(x),g(x)为凸函数,h(x)为仿射函数时,这是一个凸优化的问题。
对于支持向量机:
SVM是一个凸二次规划问题,有最优解。
(4)拉格朗日对偶
通常我们需要求解的最优化问题有如下几类:
(i) 无约束优化问题,可以写为:
min f(x)
(ii) 有等式约束的优化问题,可以写为:
min f(x),
s.t. h_i(x) = 0;i =1, ..., n
(iii) 有不等式约束的优化问题,可以写为:
min f(x),
s.t. g_i(x) <= 0;i =1, ..., n
h_j(x) = 0;j =1, ..., m
对于第(i)类的优化问题,常常使用的方法就是Fermat定理,即使用求取f(x)的导数,然后令其为零,可以求得候选最优值,再在这些候选值中验证;如果是凸函数,可以保证是最优解。
拉格朗日乘子法与对偶问题
对于第(ii)类的优化问题,常常使用的方法就是拉格朗日乘子法(Lagrange Multiplier) ,即把等式约束h_i(x)用一个系数与f(x)写为一个式子,称为拉格朗日函数,而系数称为拉格朗日乘子。通过拉格朗日函数对各个变量求导,令其为零,可以求得候选值集合,然后验证求得最优值。
例如给定椭球:
求这个椭球的内接长方体的最大体积。
我们将这个转化为条件极值问题,即在条件
下,求f(x,y,z)=8xyz的最大值。
首先定义拉格朗日函数F(x)
λk是各个约束条件的待定系数
然后解变量的偏导方程:
如果有i个约束条件,就应该有i+1个方程。求出的方程组的解就可能是最优化值(极值),将结果带回原方程验证即可得解。
回到上面的题目,通过拉格朗日乘数法将问题转化为:
对F(x,y,z,λ)求偏导得:
联立前面三个方程得到bx=ay和az=cx,代入第四个方程解得:
最大体积为:
KKT条件
对于第(iii)类的优化问题,常常使用的方法就是KKT条件(Karush-Kuhn-Tucker)。同样地,我们把所有的等式、不等式约束与f(x)写为一个式子,也叫拉格朗日函数,系数也称拉格朗日乘子,通过一些条件,可以求出最优值的必要条件,这个条件称为KKT条件。
原始含有不等式约束问题描述为:
min f(x),
s.t. g(x)≤0
含有不等式约束的KKT条件为如下式(记为式①)所示:
注意:KKT条件是非线性规划最优解的必要条件
- KKT条件描述型理解
(i)当最优解 x* 满足g(x*) <0时,最优解位于可行域内部,此时不等式约束无效,λ=0。
(ii)当最优解 x*满足g(x*) = 0时,最优解位于可行域的边界,此时不等式约束变为等式约束,g(x)=0。
(iii)同时根据几何意义,λ必<0(根据梯度可得)。
根据上述讨论,由于我们所需的是必要条件,故将上述几种情况进行并集操作,可得最优解时的必要条件,即(记为式②):
∇xL=∇f+λ∇g=0
g(x)≤0
λ≥0
λg(x)=0
(iv)当有多个不等式约束时,可推广至式①形式。
KKT条件要求强对偶,形式如下:
前两条为x*满足的原问题的约束。第三条表示对偶变量满足的约束,第四条为互补松弛条件,第五条表示拉格朗日函数在x*处取得极小值(即x*是最优解,满足拉格朗日函数极小的条件)
1.3 线性不可分
对于线性不可分的数据集,我们无法找到这样一种直线,将不同类型的样本分割开来,SVM的方法好像就不适用了。但是Vapnik提出了一种观点,我们所认为的线性不可分,只是在当前维度下线性不可分,并不代表它在高维空间中线性不可分。比如有一组样本在二维空间线性不可分,但是在三维空间中,我们是有可能找到这样一条直线将其分隔开来的,Vapnik还认为,当维数趋于无穷时,一定存在这样一条线,可以将不同类型的样本分割开来。
Cover定理:
在提升维度后,原本非线性的数据点变得线性可分,这在数学上是有严格证明的,即Cover定理。Cover定理可以定性地描述为:将复杂的模式分类问题非线性地投射到高维空间将比投射到低维空间更可能是线性可分的,当空间的维数D越大时,在该空间的N个数据点间的线性可分的概率就越大。
或者再通俗的说,这个定理描述的是线性可分的概率,如果能把数据从低维空间映射到高维空间,我们就很可能在高维空间把数据做线性可分。对于在N维空间中线性不可分的数据,在N+1维以上的空间会有更大可能变成线性可分的。
所以人们就努力的寻找一种映射,这映射能将样本从原始空间(低维数据)转变到高维特征空间,从而把低维空间中线性不可分的两类点变成线性可分的。这种映射ϕ(X) 又可称为“特征构建”,映射后的向量可称之为“特征向量”。
首先,输入的数据样本集为一组N个m0维的向量x1,x1,...,xN,每个样本都被归类到两个类C1和C2之一。定义一组实值函数(也就是输入一个向量输出一个实数的函数)φ1(x),φ2(x),...,φm1(x),用来将输入数据映射到一个m1维的空间,将它们组成一个向量:
这个函数向量ϕ的输出可被认为是被映射到高维空间之后的输入数据x。φi(x)称为隐藏函数,其组成的向量ϕ所在的空间称为隐藏空间或特征空间。如果有那么个m1维的向量w,使得这个成立:
也就是说被ϕ映射到另一个高维空间的数据样本们成了线性可分的,就说这个把x分类到C1和C2的分法是ϕ可分的。
对于x来说, wTϕ(x)=0就是一个分类曲面。
于是模式可分性的Cover定理在这就包含这两部分:
- 隐藏函数的非线性转换。
- 高维的特征空间(这个高维是相对原始数据的维度来说的,由隐藏函数的个数决定)。
异或问题
异或问题因为是个典型的线性不可分问题。其点(0,0)和(1,1)归于类0,点(0,1)和点(1,0)归于类1。然后我们要拿一组隐藏函数将这些点映射到零一空间里。在这里使用高斯隐藏函数。因为问题简单,所以只用了两个隐藏函数,维度没有增加:
其中 t1=(1,1), t2=(0,0)。也就是拿样本点跟这两点的几何距离作为高斯函数的自变量。转换结果如下:
转换前 | 转换后 |
---|---|
(1,1) | (1.0000, 0.1353) |
(0,1) | (0.3678, 0.3678) |
(0,0) | (0.1353, 1.0000) |
(1,0) | (0.3678, 0.3678) |
映射后的数据如图所示,原来线性不可分的数据,已经变成了线性可分
1.3 核函数
映射可以看作是一种拉伸,把低维数据拉伸到了高维。虽然现在我们到了高维空间号称线性可分,但是有几个困难:
- 不知道什么样的映射函数是完美的。
- 难以在各种映射函数中找到一个合适的。
- 高维空间计算量比较大。这样就会产生维灾难,计算内积是不现实的。
幸运的是,在计算中发现,我们需要的只是两个向量在新的映射空间中的内积结果,而映射函数到底是怎么样的其实并不需要知道。于是这样就引入了核函数的概念。
核函数事先在低维上计算,而将实质上的分类效果表现在了高维上,也就是
-
包含映射,内积,相似度的逻辑。
-
消除掉把低维向量往高维映射的过程。
-
避免了直接在高维空间内的复杂计算。
即核函数除了能够完成特征映射,而且还能把特征映射之后的内积结果直接返回。即把高维空间得内积运算转化为低维空间的核函数计算。
注意,核函数只是将完全不可分问题,转换为可分或达到近似可分的状态。
在实际中,我们会经常遇到线性不可分的样例,此时,我们的常用做法是把样例特征映射到高维空间中去,但如果凡是遇到线性不可分的样例,一律映射到高维空间,那么这个维度大小是会高到可怕的,此时就需要使用核函数。核函数虽然也是将特征进行从低维到高维的转换,但核函数会先在低维上进行计算,而将实质上的分类效果表现在高维上,避免了直接在高维空间中的复杂计算。
如下图所示的两类数据,这样的数据本身是线性不可分的,当我们将二维平面的坐标值映射一个三维空间中,映射后的结果可以很明显地看出,数据是可以通过一个平面来分开的。
核函数方法处理非线性问题的基本思想:按一定的规则进行映射,使得原来的数据在新的空间中变成线性可分的,从而就能使用之前推导的线性分类算法进行处理。计算两个向量在隐式映射过后的空间中的内积的函数叫做核函数
核函数是这样的一种函数:
仍然以二维空间为例,假设对于变量x和y,将其映射到新空间的映射函数为φ,则在新空间中,二者分别对应φ(x)和φ(y),他们的内积则为<φ(x),φ(y)>。
我们令函数Kernel(x,y)=<φ(x),φ(y)>=k(x,y)
,
可以看出,函数Kernel(x,y)
是一个关于x和y的函数!而与φ无关!这是一个多么好的性质!我们再也不用管φ具体是什么映射关系了,只需要最后计算Kernel(x,y)
就可以得到他们在高维空间中的内积。
我们则称Κ(x,y)为核函数,φ(x)为映射函数。
我们大致能够得到核函数如下性质:
- 核函数给出了任意两个样本之间关系的度量,比如相似度。
- 每一个能被叫做核函数的函数,里面都藏着一个对应拉伸的函数。这些核函数的命名通常也跟如何做拉伸变换有关系。
- 核函数和映射本身没有直接关系。选哪个核函数,实际上就是在选择用哪种方法映射。通过核函数,我们就能跳过映射的过程。
- 我们只需要核函数,而不需要那个映射,也无法显式的写出那个映射。
- 选择核函数就是把原始数据集上下左右前后拉扯揉捏,直到你一刀下去正好把所有的 0 分到一边,所有的 1 分到另一边。这个上下左右前后拉扯揉捏的过程就是kernel.
核函数存在的条件
定理表明,只要一个对称函数所对应的核矩阵半正定,那么它就可以作为核函数使用。事实上,对于一个半正定核矩阵,总能找到一个与之对应的映射ϕ。换言之,任何一个核函数都隐式定义了一个称为“再生核希尔伯特空间”的特征空间
常见的核函数
通过前面的介绍,核函数的选择,对于非线性支持向量机的性能至关重要。但是由于我们很难知道特征映射的形式,所以导致我们无法选择合适的核函数进行目标优化。于是“核函数的选择”称为支持向量机的最大变数,我们常见的核函数有以下几种:
此外,还可以通过函数组合得到,例如:
对于非线性的情况,SVM 的处理方法是选择一个核函数 κ(⋅,⋅),通过将数据映射到高维空间,来解决在原始空间中线性不可分的问题。由于核函数的优良品质,这样的非线性扩展在计算量上并没有比原来复杂多少,这一点是非常难得的。当然,这要归功于核方法——除了 SVM 之外,任何将计算表示为数据点的内积的方法,都可以使用核方法进行非线性扩展。
1.4 正则化与软间隔
针对样本不是完全能够划分开的情况,可以允许支持向量机在一些样本上出错,为此要引入“软间隔”的概念。
引入正则化强度参数C(正则化:在一定程度上抑制过拟合,使模型获得抗噪声能力,提升模型对未知样本的预测性能的手段),损失函数重新定义为:
上式为采用hinge损失的形式,再引入松弛变量ξi≥0,重写为:
支持向量:
由此可以看出,软间隔支持向量机的最终模型仅与支持向量有关,即通过采用hinge损失函数仍保持了稀疏特性。
2 SVM的优缺点及应用场景
2.1 SVM的优缺点
(1)SVM的优点:
-
高效的处理高维特征空间:SVM通过将数据映射到高维空间中,可以处理高维特征,并在低维空间中进行计算,从而有效地处理高维数据。
-
适用于小样本数据集:SVM是一种基于边界的算法,它依赖于少数支持向量,因此对于小样本数据集具有较好的泛化能力。
-
可以处理非线性问题:SVM使用核函数将输入数据映射到高维空间,从而可以解决非线性问题。常用的核函数包括线性核、多项式核和径向基函数(RBF)核。
-
避免局部最优解:SVM的优化目标是最大化间隔,而不是仅仅最小化误分类点。这使得SVM在解决复杂问题时能够避免陷入局部最优解。
-
对于噪声数据的鲁棒性:SVM通过使用支持向量来定义决策边界,这使得它对于噪声数据具有一定的鲁棒性。
(2)SVM的缺点:
-
对大规模数据集的计算开销较大:SVM的计算复杂度随着样本数量的增加而增加,特别是在大规模数据集上的训练时间较长。
-
对于非线性问题选择合适的核函数和参数较为困难:在处理非线性问题时,选择适当的核函数和相应的参数需要一定的经验和领域知识。
-
对缺失数据敏感:SVM在处理含有缺失数据的情况下表现不佳,因为它依赖于支持向量的定义。
-
难以解释模型结果:SVM生成的模型通常是黑盒模型,难以直观地解释模型的决策过程和结果。
SVM在处理小样本数据、高维特征空间和非线性问题时表现出色,但对于大规模数据集和缺失数据的处理相对困难。同时,在模型的解释性方面也存在一定的挑战。
2.2 SVM的应用场景
SVM在许多其他领域也有广泛的应用,特别是在分类和回归问题中。它的灵活性和强大的泛化能力使其成为机器学习中的重要工具之一。主要应用场景总结如下:
-
文本分类:SVM可以用于对文本进行分类,如垃圾邮件分类、情感分析和文档分类等。
-
图像识别:SVM可用于图像分类、目标识别和人脸识别等任务。它可以通过提取图像的特征向量,并将其作为输入来训练SVM模型。
-
金融领域:SVM可用于信用评分、风险评估和股票市场预测等金融任务。
-
医学诊断:SVM可以应用于医学图像分析,如疾病检测、癌症诊断和医学影像分类等。
-
视频分类:SVM可以用于视频分类、行为识别和运动检测等任务,通过提取视频帧的特征并将其输入SVM模型进行分类。
-
推荐系统:SVM可以用于个性化推荐和用户分类等推荐系统任务,通过分析用户行为和特征来预测用户的兴趣和偏好。
3 基于SVM实现鸢尾花分类预测
3.1 数据集介绍
Iris 鸢尾花数据集是一个经典数据集,在统计学习和机器学习领域都经常被用作示例。数据集内包含 3 类共 150 条记录,每类各 50 个数据,每条记录都有 4 项特征:花萼长度、花萼宽度、花瓣长度、花瓣宽度,可以通过这4个特征预测鸢尾花卉属于(iris-setosa, iris-versicolour, iris-virginica)三种中的哪一品种。
数据内容:
sepal_len sepal_wid petal_len petal_wid label
0 5.1 3.5 1.4 0.2 0
1 4.9 3.0 1.4 0.2 0
2 4.7 3.2 1.3 0.2 0
3 4.6 3.1 1.5 0.2 0
4 5.0 3.6 1.4 0.2 0
.. ... ... ... ... ...
145 6.7 3.0 5.2 2.3 2
146 6.3 2.5 5.0 1.9 2
147 6.5 3.0 5.2 2.0 2
148 6.2 3.4 5.4 2.3 2
149 5.9 3.0 5.1 1.8 2
3.2 代码实现
(1)原生代码实现
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
def create_data():
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal_len', 'sepal_wid', 'petal_len', 'petal_wid', 'label']
print(df)
data = np.array(df.iloc[:100, [0, 1, -1]])
for i in range(len(data)):
if data[i,-1] == 0:
data[i,-1] = -1
return data[:,:2], data[:,-1]
class SVM:
def __init__(self, max_iter=100, kernel='poly'):
self.max_iter = max_iter
self._kernel = kernel
def init_args(self, features, labels):
self.m, self.n = features.shape
self.X = features
self.Y = labels
self.b = 0.0
# 将Ei保存在一个列表里
self.alpha = np.ones(self.m)
self.E = [self._E(i) for i in range(self.m)]
# 松弛变量
self.C = 1.0
def _KKT(self, i):
y_g = self._g(i)*self.Y[i]
if self.alpha[i] == 0:
return y_g >= 1
elif 0 < self.alpha[i] < self.C:
return y_g == 1
else:
return y_g <= 1
# g(x)预测值,输入xi(X[i])
def _g(self, i):
r = self.b
for j in range(self.m):
r += self.alpha[j]*self.Y[j]*self.kernel(self.X[i], self.X[j])
return r
# 核函数
def kernel(self, x1, x2):
if self._kernel == 'linear':
return sum([x1[k]*x2[k] for k in range(self.n)])
elif self._kernel == 'poly':
return (sum([x1[k]*x2[k] for k in range(self.n)]) + 1)**2
return 0
# E(x)为g(x)对输入x的预测值和y的差
def _E(self, i):
return self._g(i) - self.Y[i]
def _init_alpha(self):
# 外层循环首先遍历所有满足0<a<C的样本点,检验是否满足KKT
index_list = [i for i in range(self.m) if 0 < self.alpha[i] < self.C]
# 否则遍历整个训练集
non_satisfy_list = [i for i in range(self.m) if i not in index_list]
index_list.extend(non_satisfy_list)
for i in index_list:
if self._KKT(i):
continue
E1 = self.E[i]
# 如果E2是+,选择最小的;如果E2是负的,选择最大的
if E1 >= 0:
j = min(range(self.m), key=lambda x: self.E[x])
else:
j = max(range(self.m), key=lambda x: self.E[x])
return i, j
def _compare(self, _alpha, L, H):
if _alpha > H:
return H
elif _alpha < L:
return L
else:
return _alpha
def fit(self, features, labels):
self.init_args(features, labels)
for t in range(self.max_iter):
# train
i1, i2 = self._init_alpha()
# 边界
if self.Y[i1] == self.Y[i2]:
L = max(0, self.alpha[i1]+self.alpha[i2]-self.C)
H = min(self.C, self.alpha[i1]+self.alpha[i2])
else:
L = max(0, self.alpha[i2]-self.alpha[i1])
H = min(self.C, self.C+self.alpha[i2]-self.alpha[i1])
E1 = self.E[i1]
E2 = self.E[i2]
# eta=K11+K22-2K12
eta = self.kernel(self.X[i1], self.X[i1]) + self.kernel(self.X[i2], self.X[i2]) - 2*self.kernel(self.X[i1], self.X[i2])
if eta <= 0:
# print('eta <= 0')
continue
alpha2_new_unc = self.alpha[i2] + self.Y[i2] * (E2 - E1) / eta
alpha2_new = self._compare(alpha2_new_unc, L, H)
alpha1_new = self.alpha[i1] + self.Y[i1] * self.Y[i2] * (self.alpha[i2] - alpha2_new)
b1_new = -E1 - self.Y[i1] * self.kernel(self.X[i1], self.X[i1]) * (alpha1_new-self.alpha[i1]) - self.Y[i2] * self.kernel(self.X[i2], self.X[i1]) * (alpha2_new-self.alpha[i2])+ self.b
b2_new = -E2 - self.Y[i1] * self.kernel(self.X[i1], self.X[i2]) * (alpha1_new-self.alpha[i1]) - self.Y[i2] * self.kernel(self.X[i2], self.X[i2]) * (alpha2_new-self.alpha[i2])+ self.b
if 0 < alpha1_new < self.C:
b_new = b1_new
elif 0 < alpha2_new < self.C:
b_new = b2_new
else:
# 选择中点
b_new = (b1_new + b2_new) / 2
# 更新参数
self.alpha[i1] = alpha1_new
self.alpha[i2] = alpha2_new
self.b = b_new
self.E[i1] = self._E(i1)
self.E[i2] = self._E(i2)
return 'train done!'
def predict(self, data):
r = self.b
for i in range(self.m):
r += self.alpha[i] * self.Y[i] * self.kernel(data, self.X[i])
return 1 if r > 0 else -1
def score(self, X_test, y_test):
right_count = 0
for i in range(len(X_test)):
result = self.predict(X_test[i])
if result == y_test[i]:
right_count += 1
return right_count / len(X_test)
def _weight(self):
# linear model
yx = self.Y.reshape(-1, 1)*self.X
self.w = np.dot(yx.T, self.alpha)
return self.w
X, y = create_data()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25)
svm = SVM(max_iter=800)
print(svm.fit(X_train, y_train))
print(svm.score(X_train, y_train))
print(svm.score(X_test, y_test))
(2)基于sklearn的代码实现
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
def create_data():
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
data = np.array(df.iloc[:100, [0, 1, -1]])
for i in range(len(data)):
if data[i,-1] == 0:
data[i,-1] = -1
return data[:,:2], data[:,-1]
X, y = create_data()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25)
plt.scatter(X[:50,0],X[:50,1], label='0')
plt.scatter(X[50:,0],X[50:,1], label='1')
plt.show()
model = SVC()
model.fit(X_train, y_train)
SVC(C=1.0, break_ties=False, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape='ovr', degree=3, gamma='scale', kernel='rbf',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False)
print('train accuracy: ' + str(model.score(X_train, y_train)))
print('test accuracy: ' + str(model.score(X_test, y_test)))
3.3 运行结果
(1)数据分布展示:
(2)训练集和测试集上的准确性
train accuracy: 1.0
test accuracy: 0.96
4 完整代码
代码下载地址:代码下载