支持向量机(Support Vector Mechine, SVM)是由统计学习之父弗拉基米尔·瓦普尼克提出的。他在 1963 年就提出了相关概念,但由于他当时身在苏联且当时苏联学术界几乎不与西方交流,所以并未得到重视。1990 年他前往美国,他将之前的研究工作整理发表,支持向量机很快就变得火热。
本文只是对 SVM 进行浅显的讨论,适合 SVM 的应用者,而非 SVM 的研究者。
支持向量机可以说是一种有监督的二分类模型,主要用于完成分类任务。我们首先来看一个简单的分类任务。
在上图的二维平面中有两类点,我们的任务就是找一条直线将这两类分割开来。将两类点分割开的直线有很多,如下图所示:
在上图的众多分割线当中,我们主观上感觉图中的红线是最优的分割线,因为它在“正中间”。图中的各点仅是训练集给出的,当使用训练集之外的数据时,红线的预测效果也是最佳的。因为训练集中多少会存在一些噪声点,而这些分割线中的红线受噪声的影响是最小的。换而言之,红线是泛化性能最好的分割线。那么我们如何找到最佳分割线呢?或者说我们如何定义最佳分割线?
我们以某条分割线的为基础,在其左右两侧各构造一条平行线,将这两条线以同样的步长不断向外移动直到碰到样本点时停止,如下图所示。
图中蓝色虚线即为红线两侧的平行线,它们到红线的距离是相等的。我们称蓝线触碰到的样本点为红线的支持向量(support vector),两条蓝线之间的距离为红线的间隔(margin)。我们认为最佳分割线就是拥有最大间隔(maximum margin)的分割线。图示中给出的是二维的样本点,我们使用直线进行分割;如果是三维样本点,我们使用平面分割;在更高的维度,我们使用超平面(hyperplane)进行分割。在学术上,统一将其称之为超平面。SVM 的训练任务就是在训练集上寻找拥有最大间隔的的超平面。
我们这里先给出线性可分的定义:面对一个二分类数据集
D
=
{
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
,
⋯
,
(
x
m
,
y
m
)
∣
x
i
∈
R
n
,
y
i
=
±
1
}
D=\left\{(\boldsymbol{x}_1,y_1), (\boldsymbol{x}_2,y_2), \cdots, (\boldsymbol{x}_m,y_m) | \boldsymbol{x}_i \in \mathbb{R}^n, y_i = \pm 1 \right\}
D={(x1,y1),(x2,y2),⋯,(xm,ym)∣xi∈Rn,yi=±1},如果存在一个线性方程
w
⊤
x
+
b
=
0
\boldsymbol{w}^\top \boldsymbol{x}+b=0
w⊤x+b=0,对于任意
(
x
i
,
y
i
)
∈
D
(\boldsymbol{x}_i,y_i) \in D
(xi,yi)∈D,若
y
i
=
1
y_i=1
yi=1,则
w
⊤
x
i
+
b
>
0
\boldsymbol{w}^\top \boldsymbol{x}_i+b>0
w⊤xi+b>0;若
y
i
=
−
1
y_i=-1
yi=−1,则
w
⊤
x
i
+
b
<
0
\boldsymbol{w}^\top \boldsymbol{x}_i+b<0
w⊤xi+b<0,则称二分类数据集
D
D
D 是线性可分的。
这里的
x
i
\boldsymbol{x}_i
xi 就是我们向分类模型输入的特征向量,
y
i
y_i
yi 就是
x
i
x_i
xi 对应的标签。由于是二分类任务,所以
y
i
y_i
yi 的取值只有两个
1
1
1 或
−
1
-1
−1。我们在上文的图中提到的分类任务就是线性可分的。需要注意的是,如果一个训练集是线性可分的,那么必然存在无数个可以对其进行分割的超平面,我们这里不进行证明。现在我们的任务就是将寻找拥有最大间隔的超平面的任务使用数学语言描述出来。
我们给出线性可分的二分类数据集
D
=
{
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
,
⋯
,
(
x
m
,
y
m
)
∣
x
i
∈
R
n
,
y
i
=
±
1
}
D=\left\{(\boldsymbol{x}_1,y_1), (\boldsymbol{x}_2,y_2), \cdots, (\boldsymbol{x}_m,y_m) | \boldsymbol{x}_i \in \mathbb{R}^n, y_i = \pm 1 \right\}
D={(x1,y1),(x2,y2),⋯,(xm,ym)∣xi∈Rn,yi=±1},并设进行分割的超平面的线性方程如下:
w
⊤
x
+
b
=
0
(1)
\boldsymbol{w}^\top \boldsymbol{x} + b = 0 \tag{1}
w⊤x+b=0(1) 我们寻找拥有最大间隔的超平面的过程其实就是确定
w
\boldsymbol{w}
w 和
b
b
b 的过程,现在我们将该超平面记作
(
w
,
b
)
(\boldsymbol{w}, b)
(w,b)。我们知道空间内任意一点
x
\boldsymbol{x}
x 到
(
w
,
b
)
(\boldsymbol{w}, b)
(w,b) 的距离
r
r
r 为:
r
=
∣
w
⊤
x
+
b
∣
∥
w
∥
(2)
r = \frac{\left| \boldsymbol{w}^\top\boldsymbol{x}+b \right|}{\| \boldsymbol{w} \|} \tag{2}
r=∥w∥∣
∣w⊤x+b∣
∣(2) 根据线性可分的定义,如果
(
w
,
b
)
(\boldsymbol{w}, b)
(w,b) 能将训练集正确分割,那么它必须满足如下限制条件:
{
w
⊤
x
i
+
b
>
0
,
y
i
=
1
w
⊤
x
i
+
b
<
0
,
y
i
=
−
1
(3)
{w⊤xi+b>0,yi=1w⊤xi+b<0,yi=−1 \tag{3}
{w⊤xi+b>0,w⊤xi+b<0,yi=1yi=−1(3) 根据定义我们可以得到支持向量
x
∗
\boldsymbol{x}^*
x∗ 为:
x
∗
=
arg
min
x
∣
w
⊤
x
+
b
∣
∥
w
∥
(4)
\boldsymbol{x}^* = \underset{\boldsymbol{x}}{\arg \min} \frac{\left| \boldsymbol{w}^\top\boldsymbol{x}+b \right|}{\| \boldsymbol{w} \|} \tag{4}
x∗=xargmin∥w∥∣
∣w⊤x+b∣
∣(4) 记
a
=
∣
w
⊤
x
∗
+
b
∣
a=\left| \boldsymbol{w}^\top\boldsymbol{x}^*+b \right|
a=∣
∣w⊤x∗+b∣
∣,我们以支持向量所在平面(上文图中的蓝色虚线)作为分割依据,则有如下条件成立:
{
w
⊤
x
i
+
b
⩾
a
,
y
i
=
1
w
⊤
x
i
+
b
⩽
−
a
,
y
i
=
−
1
(5)
{w⊤xi+b⩾a,yi=1w⊤xi+b⩽−a,yi=−1 \tag{5}
{w⊤xi+b⩾a,w⊤xi+b⩽−a,yi=1yi=−1(5) 由于
w
\boldsymbol{w}
w 和
b
b
b 取确定值时,
a
a
a 也就随之确定,所以令
w
~
=
w
a
\widetilde{\boldsymbol{w}}=\frac{\boldsymbol{w}}{a}
w
=aw,
b
~
=
b
a
\widetilde{b}=\frac{b}{a}
b
=ab,则可得如下条件成立:
{
w
~
⊤
x
i
+
b
~
⩾
1
,
y
i
=
1
w
~
⊤
x
i
+
b
~
⩽
−
1
,
y
i
=
−
1
(6)
{˜w⊤xi+˜b⩾1,yi=1˜w⊤xi+˜b⩽−1,yi=−1 \tag{6}
{w
⊤xi+b
⩾1,w
⊤xi+b
⩽−1,yi=1yi=−1(6) 可以进一步写作:
y
i
(
w
~
⊤
x
i
+
b
~
)
⩾
1
(7)
y_i\left(\widetilde{\boldsymbol{w}}^\top \boldsymbol{x}_i + \widetilde{b}\right) \geqslant 1 \tag{7}
yi(w
⊤xi+b
)⩾1(7) 因为条件
(
3
)
(3)
(3) 是条件
(
7
)
(7)
(7) 的前提条件,所以
(
7
)
(7)
(7) 成立时,
(
3
)
(3)
(3) 必然成立。因此,我们使用
(
7
)
(7)
(7) 作为新的限制条件。我们的目的是要在如上条件下寻找最大间隔,即:
max
w
,
b
∣
w
⊤
x
∗
+
b
∣
∥
w
∥
=
max
w
~
,
b
~
1
∥
w
~
∥
⟺
min
w
~
,
b
~
1
2
∥
w
~
∥
2
s
.
t
.
y
i
(
w
~
⊤
x
i
+
b
~
)
⩾
1
,
i
=
1
,
2
,
⋯
,
m
(8)
maxw,b|w⊤x∗+b|‖w‖=max˜w,˜b1‖˜w‖⟺min˜w,˜b12‖˜w‖2s.t.yi(˜w⊤xi+˜b)⩾1,i=1,2,⋯,m \tag{8}
w,bmax∥w∥∣
∣w⊤x∗+b∣
∣=w
,b
max∥w
∥1⟺w
,b
min21∥w
∥2s.t.yi(w
⊤xi+b
)⩾1,i=1,2,⋯,m(8) 我们可以看出
w
~
⊤
x
i
+
b
~
=
0
\widetilde{\boldsymbol{w}}^\top \boldsymbol{x}_i + \widetilde{b}=0
w
⊤xi+b
=0 与
w
⊤
x
i
+
b
=
0
\boldsymbol{w}^\top \boldsymbol{x}_i + b = 0
w⊤xi+b=0 是同一个超平面,所以我们不妨将最终表达式写作:
min
w
,
b
1
2
∥
w
∥
2
s
.
t
.
y
i
(
w
⊤
x
i
+
b
)
⩾
1
,
i
=
1
,
2
,
⋯
,
m
(8)
minw,b12‖w‖2s.t.yi(w⊤xi+b)⩾1,i=1,2,⋯,m \tag{8}
w,bmin21∥w∥2s.t.yi(w⊤xi+b)⩾1,i=1,2,⋯,m(8) 上述是一个凸二次优化问题,求出解即完成线性 SVM 的训练,这里我们不再讨论求解过程。训练完成后,可按照如下公式对模型进行测试:
y
^
=
s
g
n
(
w
⊤
x
+
b
)
(9)
\widehat{y}=\mathrm{sgn} \left( \boldsymbol{w}^\top \boldsymbol{x} + b \right) \tag{9}
y
=sgn(w⊤x+b)(9)
现在我们设想这样一个情况:我们要完成的分类任务实际上是线性可分的,但是由于训练集中存在个别噪声样本,使得训练集变为线性不可分,如下图所示。显然表达式
(
8
)
(8)
(8) 在下图所示的含有噪声的训练集上是无解的。那我们能否对表达式进行一些改造,使其能够容忍一定的噪声,从而在该训练集上有解?
我们也可以设想另一种情况:也许某个训练集中的噪声并没有那么“过分”,两个类别的噪声虽然靠的非常近,但是训练集仍然是线性可分的。然而,算法将噪声点当作支持向量进行训练,最终会导致过拟合现象,如下图所示。
SVM 为了解决噪声造成的不可解以及过拟合现象,对表达式
(
8
)
(8)
(8) 作出了如下改动:
min
w
,
b
1
2
∥
w
∥
2
+
C
∑
i
=
1
m
ξ
i
s
.
t
.
y
i
(
w
⊤
x
i
+
b
)
⩾
1
−
ξ
i
,
i
=
1
,
2
,
⋯
,
m
ξ
i
⩾
0
,
i
=
1
,
2
,
⋯
,
m
(10)
\begin{aligned} & \underset{\boldsymbol{w}, b}{\min} \, \frac{1}{2}\| \boldsymbol{w} \|^2 + C \sum_{i=1}^m{\xi_i} \\ & \begin{aligned} \mathrm{s.t.} \enspace & y_i\left( \boldsymbol{w}^\top \boldsymbol{x}_i + b \right) \geqslant 1-\xi_i, & i = 1, 2, \cdots, m \\ & \xi_i \geqslant 0, & i = 1, 2, \cdots, m \end{aligned} \end{aligned} \tag{10}
w,bmin21∥w∥2+Ci=1∑mξis.t.yi(w⊤xi+b)⩾1−ξi,ξi⩾0,i=1,2,⋯,mi=1,2,⋯,m(10) 其中
ξ
i
\xi_i
ξi 称为松弛变量(slack variables),它松弛了限制条件
y
i
(
w
⊤
x
i
+
b
)
⩾
1
−
ξ
i
y_i\left( \boldsymbol{w}^\top \boldsymbol{x}_i + b \right) \geqslant 1-\xi_i
yi(w⊤xi+b)⩾1−ξi。因此,当
ξ
i
\xi_i
ξi 足够大时,
x
i
\boldsymbol{x}_i
xi 就可以“越过”支持向量(下图中的蓝线)甚至超平面(下图中的红线)。但是,
ξ
i
\xi_i
ξi 并不能无限制的变大,最小化表达式中引入了其累加和
∑
i
=
1
m
ξ
i
\sum_{i=1}^m{\xi_i}
∑i=1mξi,使得其在优化过程中被抑制增大。
C
C
C 是一个常数,用于调节对松弛变量的抑制力,当其趋向于正无穷大时,表达式
(
10
)
(10)
(10) 也就等价于表达式
(
8
)
(8)
(8)。下图就是引入松弛变量的效果示意图。
表达式
(
10
)
(10)
(10) 就是常用的软间隔支持向量机,而表达式
(
8
)
(8)
(8) 就相应地被称为硬间隔支持向量机,二者最明显的区别就是是否允许部分样本点“越过”支持向量。此外,软间隔支持向量机在测试时仍然使用表达式
(
9
)
(9)
(9)。
在上文的所有讨论中,我们始终假设训练集是线性可分的或者“近似”线性可分的。然而,现实中有些问题不是线性可分的,最经典的就是异或问题。如下图所示的异或问题,我们无法找到一个超平面将其正确划分。
解决此问题的一种思路是:使用多个有限的超平面,将它们拼接起来,形成一种近似的“超曲面”来进行分割。然而,SVM 的思路却比以上想法高明的多,它将样本通过函数映射到更高的维度,然后在高维度用超平面进行分割。针对上图中的问题,我们定义二维向三维的映射
ϕ
\phi
ϕ 的表达式为
[
x
1
,
x
2
]
⊤
→
ϕ
[
x
1
,
x
2
,
x
1
⋅
x
2
]
⊤
[x_1, x_2]^\top \xrightarrow{\phi} [x_1, x_2, x_1 \cdot x_2]^\top
[x1,x2]⊤ϕ[x1,x2,x1⋅x2]⊤,那么使用超平面
[
−
1
,
−
1
,
2
]
⋅
x
+
0.5
=
0
[-1, -1, 2] \cdot \boldsymbol{x}+0.5=0
[−1,−1,2]⋅x+0.5=0 就可以将其分割,如下图所示。
胡浩基教授说,存在如下定理:将某维度中随机生成的有限个点,随机分为两类,而后将其转化为更高维度的点。维度越高,点集被正确线性分割的概率越大。当维度为无穷大时,点集被正确线性分割的概率为
1
1
1。
我们引入自低维向高维的映射
ϕ
\phi
ϕ 后,表达式
(
8
)
(8)
(8) 变为如下形式:
min
w
,
b
1
2
∥
w
∥
2
s
.
t
.
y
i
(
w
⊤
ϕ
(
x
i
)
+
b
)
⩾
1
,
i
=
1
,
2
,
⋯
,
m
(11)
minw,b12‖w‖2s.t.yi(w⊤ϕ(xi)+b)⩾1,i=1,2,⋯,m \tag{11}
w,bmin21∥w∥2s.t.yi(w⊤ϕ(xi)+b)⩾1,i=1,2,⋯,m(11) 测试时使用的表达式
(
9
)
(9)
(9) 变为如下形式:
y
^
=
s
g
n
(
w
⊤
ϕ
(
x
)
+
b
)
(12)
\widehat{y}=\mathrm{sgn} \left( \boldsymbol{w}^\top \phi(\boldsymbol{x}) + b \right) \tag{12}
y
=sgn(w⊤ϕ(x)+b)(12) 为了尽可能的增大训练集被正确线性分割的概率,SVM 采用的某些映射
ϕ
\phi
ϕ 可以将有限维度映射到超高维度甚至无穷大维度。这也就意味着
w
\boldsymbol{w}
w 将有超多个分量甚至无穷个分量,显然这在现实中是难以应用的。为此 SVM 引入了核函数(kernel function),下面给出表达式:
κ
(
x
i
,
x
j
)
=
ϕ
(
x
i
)
⊤
ϕ
(
x
j
)
(13)
\kappa(\boldsymbol{x}_i,\boldsymbol{x}_j)=\phi(\boldsymbol{x}_i)^\top \phi(\boldsymbol{x}_j) \tag{13}
κ(xi,xj)=ϕ(xi)⊤ϕ(xj)(13) 这样我们就可以通过一个核函数运算来代替两个超高维甚至是无限维的向量内积运算。常见的核函数有以下几个:
根据泛函分析中的 Mercer 定理,某函数
κ
(
x
i
,
x
j
)
\kappa(\boldsymbol{x}_i,\boldsymbol{x}_j)
κ(xi,xj) 可以写成
ϕ
(
x
i
)
⊤
ϕ
(
x
j
)
\phi(\boldsymbol{x}_i)^\top \phi(\boldsymbol{x}_j)
ϕ(xi)⊤ϕ(xj) 的充要条件是其具有交换性和半正定性。
如果我们能通过等价转换将表达式
(
11
)
(11)
(11) 和
(
12
)
(12)
(12) 中的
w
\boldsymbol{w}
w 和
ϕ
\phi
ϕ 替换成含核函数
κ
\kappa
κ 的表达式,也就解决了超高维度甚至无限维度映射无法应用的问题。下面我们给出表达式
(
11
)
(11)
(11) 的等价表达式(对偶问题):
max
α
∑
i
=
1
m
α
i
−
1
2
∑
i
=
1
m
∑
j
=
1
m
α
i
α
j
y
i
y
j
κ
(
x
i
,
x
j
)
s
.
t
.
∑
i
=
1
m
α
i
y
i
=
0
,
i
=
1
,
2
,
⋯
,
m
α
i
⩾
0
,
i
=
1
,
2
,
⋯
,
m
(14)
\begin{aligned} & \underset{\boldsymbol{\alpha}}{\max} \, \sum_{i=1}^m{\alpha_i} - \frac{1}{2} \sum_{i=1}^m{\sum_{j=1}^m {\alpha_i \alpha_j y_i y_j \kappa(\boldsymbol{x}_i,\boldsymbol{x}_j) } } \\ & \begin{aligned} \mathrm{s.t.} \enspace & \sum_{i=1}^m{\alpha_i y_i} = 0, & i = 1, 2, \cdots, m \\ & \alpha_i \geqslant 0, & i = 1, 2, \cdots, m \end{aligned} \end{aligned} \tag{14}
αmaxi=1∑mαi−21i=1∑mj=1∑mαiαjyiyjκ(xi,xj)s.t.i=1∑mαiyi=0,αi⩾0,i=1,2,⋯,mi=1,2,⋯,m(14) 其中
α
=
[
α
1
,
α
2
,
⋯
,
α
m
]
⊤
\boldsymbol{\alpha}=[\alpha_1, \alpha_2, \cdots, \alpha_m]^\top
α=[α1,α2,⋯,αm]⊤ 是新引入的待定参数向量。上式的具体推导过程这里不讨论,感兴趣的读者可以学习凸优化理论后自行推导。测试表达式
(
12
)
(12)
(12) 可以等价转化为如下形式:
y
^
=
s
g
n
(
∑
i
=
1
m
α
i
y
i
κ
(
x
i
,
x
)
+
b
)
\widehat{y} = \mathrm{sgn}\left( \sum_{i=1}^m{\alpha_i y_i \kappa(\boldsymbol{x}_i,\boldsymbol{x}) + b} \right)
y
=sgn(i=1∑mαiyiκ(xi,x)+b)
SVM 在测试时可以将 ∑ i = 1 m α i y i κ ( x i , x ) + b \sum_{i=1}^m{\alpha_i y_i \kappa(\boldsymbol{x}_i,\boldsymbol{x}) + b} ∑i=1mαiyiκ(xi,x)+b 的值通过变换输入到 Sigmoid 函数中,而后将 Sigmoid 的输出作为最终结果,从而实现了 SVM 预测结果的概率化输出。感性兴趣的读者可以参考Platt 的论文《Probabilistic outputs for support vector machines and comparisons to regularized likelihood methods》。
优点:
缺点:
周志华《机器学习》
胡浩基《浙江大学支持向量机》课程视频
《机器学习:支持向量机(SVM)》博客
以下是生成基本原理、硬间隔和软间隔章节中示意图的代码:
from matplotlib import pyplot as plt
import numpy as np
np.random.seed(123456)
ax = plt.gca()
ax.set(xticks=(), yticks=())
# 生成两类随机散点图
[x, y] = np.random.rand(2, 20)
y = y + x + 0.2
ax.scatter(x, y, marker='o', color='yellow', label='Class One')
[x, y] = np.random.rand(2, 20)
x = x + y + 0.2
ax.scatter(x, y, marker=',', color='limegreen', label='Class Two')
# 画出支持向量
# x = np.random.rand(3)
# x = x * 1.5
# y = x + 0.2
# ax.scatter(x, y, marker='o', color='orange', label='Support Vector of Class One')
# x = np.random.rand(3)
# x = x * 1
# y = x - 0.2
# ax.scatter(x, y, marker=',', color='green', label='Support Vector of Class Two')
# # 画出其它可行的分割线
# x = np.linspace(0, 1.5, 100)
# y = x * 0.5 + 0.35
# ax.plot(x, y, color='black')
# y = x * 1.45 - 0.3
# ax.plot(x, y, color='black')
# 画出最大间隔
x = np.linspace(0, 1.5, 100)
y = x + 0.2
ax.plot(x, y, '--', color='blue', label='Parallel Hyperplane')
y = x - 0.2
ax.plot(x, y, '--', color='blue')
# 画出超平面
y = x
ax.plot(x, y, color='red', label='Hyperplane')
# 画出第一种噪声点
# ax.scatter([1.0, 0.8], [0.5, 0.4], marker='o', color='none', edgecolor='yellow', label='Noise of Class One')
# ax.scatter([0.5, 0.4], [1.0, 0.8], marker=',', color='none', edgecolor='limegreen', label='Noise of Class Two')
# 画出第二种噪声点及其造成的过拟合分割线
x = np.array([0.5, 0.6])
y = x * 0.5 + 0.4
ax.scatter(x, y, marker='o', color='none', edgecolor='yellow', label='Noise of Class One')
y = x * 0.5 + 0.3
ax.scatter(x, y, marker=',', color='none', edgecolor='limegreen', label='Noise of Class Two')
# x = np.linspace(0, 1.5, 100)
# y = x * 0.5 + 0.35
# ax.plot(x, y, color='black', label='Hyperplane')
ax.legend()
plt.show()
以下为生成核函数章节中第一幅插图的代码:
from matplotlib import pyplot as plt
import numpy as np
ax = plt.gca()
ax.set(xticks=(0, 1), yticks=(0, 1))
ax.axis('equal')
ax.scatter([0, 1], [1, 0], marker='o', color='yellow', label='Class One')
ax.scatter([0, 1], [0, 1], marker=',', color='limegreen', label='Class Two')
# 画出椭圆
t = np.arange(0, 2 * np.pi, 0.01)
a = 1
b = 0.5
c = np.sqrt(a**2 - b**2)
e = c / a
r = a * (1 - e**2) / (1 - e * np.cos(t + np.pi / 4))
x = r * np.cos(t) - np.sqrt(c**2 / 2) + 0.5
y = r * np.sin(t) + np.sqrt(c**2 / 2) + 0.5
ax.plot(x, y, color='red', label='Non-hyperplane')
ax.legend()
plt.show()
以下为生成核函数章节中第二幅插图的代码:
from matplotlib import pyplot as plt
import numpy as np
ax = plt.gca(projection='3d')
ax.scatter([0, 1], [0, 1], [0, 1], color='limegreen', edgecolor='black', zorder=1, label='Class Two')
ax.scatter([0, 1], [1, 0], [0, 0], color='yellow', edgecolor='black', zorder=1, label='Class One')
x, y = np.meshgrid(np.linspace(-0.2, 1.2, 100), np.linspace(-0.2, 1.2, 100))
z = 0.5 * x + 0.5 * y - 0.25
ax.plot_surface(x, y, z, alpha=0.3, zorder=0)
ax.view_init(30, 10)
ax.legend()
plt.show()