1. 概述
第二次机器学习小实验要求“体验”一下SVM对数据进行分类,并对分类结果进行ACC精度计算。本次实验并不要求实现SVM,只需要调通各种库(例如MATLAB、sklearn等等)里已经集成好的函数即可,所以一开始我觉得很简单(事实上也确实很简单),但是在这个过程中因为我写代码不够规范,而出现了一些意想不到的错误,在解决这个错误的过程中,我又学到了一些知识,所以还是在这里记录一下。
2. sklearn SVM自定义核
关于sklearn中SVM的自定义核,网上有很多关于这方面的博客,但如果作为一个“新人”去看的话,总觉得还是有些云里雾里,所以在这里以我亲身 自定义Laplace核 来作为例子稍微介绍一下。
2.1 什么是Laplace核
要了解Laplace核,那肯定首先需要了解什么是核函数。最简单的解释,就是能够保留某种特征的“最简单”的函数。例如,对于标准正态分布函数:
f
(
x
)
=
1
2
π
e
−
x
2
2
f(x)=\frac{1}{\sqrt{2\pi}}e^{-\frac{x^2}{2}}
f(x)=2π1e−2x2其中
f
(
x
)
f(x)
f(x)的核函数就是
k
σ
,
μ
(
x
)
=
e
x
2
2
,
σ
=
1
,
μ
=
0
k_{\sigma,\mu}(x)=e^{\frac{x^2}{2}}, \sigma=1,\mu=0
kσ,μ(x)=e2x2,σ=1,μ=0 ,而这个
k
σ
,
μ
k_{\sigma,\mu}
kσ,μ 就是 高斯核函数 也叫 径向基核函数(rbf),也就是说核函数是某一类函数,是具有某种特征的函数簇。
好了,现在大概知道了什么是核函数,我们设数据为
X
X
X,标签为
Y
Y
Y,
x
i
x_i
xi表示第
i
i
i个样本,每个样本是
n
n
n维的,也就是说
x
i
x_i
xi是一个
n
n
n维向量,那么在SVM中一般常用的有以下几种核函数:
1.线性核函数: k ( x 1 , x 2 ) = < x 1 , x 2 > k(x_1,x_2)= <x_1,x_2> k(x1,x2)=<x1,x2>,就是两个向量对应元素相乘后求和,结果为一个标量值。
2.多项式核函数: k ( x 1 , x 2 ) = ( γ < x 1 , x 2 > + c ) n k(x_1,x_2)=(\gamma<x_1,x_2>+c)^n k(x1,x2)=(γ<x1,x2>+c)n
3.高斯核函数(径向基核函数rbf): k ( x 1 , x 2 ) = e − ∥ x 1 − x 2 ∥ 2 2 σ 2 k(x_1,x_2)=e^{-\frac {\|x_1-x_2\|2}{2\sigma^2}} k(x1,x2)=e−2σ2∥x1−x2∥2
4.laplace核函数: k ( x 1 , x 2 ) = e − ∥ x 1 − x 2 ∥ σ k(x_1,x_2)=e^{-\frac {\|x_1-x_2\|}{\sigma}} k(x1,x2)=e−σ∥x1−x2∥
可以看到,laplace核与高斯核有点相似,但是差别还是有些大的。具体每种核函数的特征以及适用情况还需要进一步深入理解其中原理(这里不做解释)。注意这里有个问题就是,虽然定义的核函数知道了,但是这里的 x i , x j x_i,x_j xi,xj是什么意思,虽然上面说的是代表着第 i , j i,j i,j个样本,但是怎么去使用呢,继续看下面。
2.2 sklearn SVM中如何自定义核函数
目前据我所知,上述四种核函数中,sklearn包含了前三个,但是并没有laplace核,所以当需要用到laplace核来进行分类时,就需要自行定义一个核函数。
通过查阅各种资料发现,自定义一个核函数非常简单,通过以下两种方式,均可实现核函数的自定义。
- 内部核(示例代码如下)
def my_kernel(X1, X2):
return np.dot(X1, X2.T)
clf = svm.SVC(kernel=my_kernel)
clf.fit(X,Y)
通过改变SVC( )函数中的kernel参数来改变调用的核函数,(我的感觉)在调用sklearn中集成的核函数时,使用的应该就是内部核的方式,例如(这里以高斯核为例)
svm.SVC(kernel='rbf')
- 外部核 (这里以线性核为例)
gram = np.dot(X1,X2)
clf = svm.SVC(kernel='precomputed')
clf.fit(gram,Y)
看到这里感觉很简单,所以我选择了内部核的方式,然后很自信的写了几行代码,以为就能够轻易地实现在svm中调用自定义的和函数了。想要调用自定义的核函数的话,首先肯定要知道自定义核函数里的参数以及返回值是什么才能够写得出函数体。
首先,我写了以下几行代码,在调用的过程中将参数X1和X2对比,发现结果为True,然后就天真的以为X1和X2始终都是完全一样的。
def my_kernel(X1, X2):
print(np.all(X1)==np.all(X2))
return np.dot(X1,X2.T)
然后接下来就要确定返回值是什么了。经过各种挣扎推理,终于发现自定义核函数的目的就是计算一个核矩阵,设数据中一共有m个样本,则这个核矩阵如下:
k
(
x
1
,
x
1
)
k
(
x
1
,
x
2
)
k
(
x
1
,
x
3
)
⋯
k
(
x
1
,
x
m
)
k
(
x
2
,
x
1
)
k
(
x
2
,
x
2
)
k
(
x
2
,
x
3
)
⋯
k
(
x
2
,
x
m
)
⋯
k
(
x
m
,
x
1
)
k
(
x
m
,
x
2
)
k
(
x
m
,
x
3
)
⋯
k
(
x
m
,
x
m
)
\begin{matrix} k(x_1,x_1)&k(x_1,x_2)&k(x_1,x_3)&\cdots&k(x_1,x_m)\\ k(x_2,x_1)&k(x_2,x_2)&k(x_2,x_3)&\cdots&k(x_2,x_m)\\ \cdots \\ k(x_m,x_1)&k(x_m,x_2)&k(x_m,x_3)&\cdots&k(x_m,x_m) \end{matrix}
k(x1,x1)k(x2,x1)⋯k(xm,x1)k(x1,x2)k(x2,x2)k(xm,x2)k(x1,x3)k(x2,x3)k(xm,x3)⋯⋯⋯k(x1,xm)k(x2,xm)k(xm,xm)这样就很好理解了,这里的
k
(
x
i
,
x
j
)
k(x_i,x_j)
k(xi,xj)就能够跟上面定义的核函数中的
x
i
,
x
j
x_i,x_j
xi,xj对应起来了。最终把这个核矩阵返回给SVC( ),通过这个核矩阵就能够来训练和预测标签了。
2.3 数据的训练集和测试集区分
实习中,老师给的数据并没有区分训练集和测试集,所以我们需要对整个数据进行划分,将训练集用来训练,测试集用来预测并计算最终的分类精确度。其实思路很简单,就是在数据中每个簇抽取其中前80%(当然这个比例可以自己改动)的数据作为训练集,剩下的20%数据作为测试集。这里直接给出划分测试集和训练集代码(完整代码在最后):
for i in range(len(num_Y)):
temp = dataY == num_Y[i]
temp.astype(float)
num_Y[i] = np.sum(temp)
flag = 0
for j in range(len(dataY)):
if temp[j] == 1:
if flag < int(round(0.8*num_Y[i])):
dataX_train.append(dataX[j])
dataY_train.append(dataY[j])
flag += 1
else:
dataX_predict.append(dataX[j])
dataY_predict.append(dataY[j])
这里的num_Y表示数据标签的种类,data_Y是标签数据,后缀为_train的是用来作为训练集的数据,后缀为_predict的用来作为测试集的数据。
2.4 调用自定义核时遇到的错误和错误理解
通过2.2的分析,这时候就应该知道如何去写这个自定义的内部核函数了。一开始实现的内部核函数代码是这样的:
def laplace(X1, X2):
K = np.zeros((len(X1), len(X1)), dtype=np.float)
for i in range(len(X1)):
for j in range(len(X1)):
K[i][j] = math.exp(-math.sqrt(np.dot(X1[i] - X2[j], (X1[i] - X2[j]).T))/2)
return K
可以看到,因为前面通过输出X1和X2的对比结果发现是一致的,而我想偷懒,直接就复制了,就没有很规范的把np.zeros里的第二个X1和第二层循环for语句里的X1改成X2,以为既然两个是一样的,那对计算结果肯定没有影响。但是,在进行数据预测时,出现了意想不到的错误,如下:
里面的40是测试集的数据量,163是训练集的数据量,错误产生的原因居然是要求这两个数据要相等,而产生这个错误的位置是在执行 clf.predict(dataX_predict) 的地方,想了一下,在预测测试集的标签时怎么会跟训练集产生关联。这就逼着我回头再仔细看看SVC中调用核函数的过程到底是什么样的。后来发现,执行
clf.fit(dataX_train, dataY_train)
和执行
clf.predict(dataX_predict)
这两行代码时调用的核函数是有差别的。在训练时,也就是 fit( )函数会调用一次自定义的核函数,此时会向核函数传入X1和X2两个一样的数据,都是训练集,利用这个训练集来进行卷积计算得到核矩阵,然而,在预测时,也就是 predict()函数也会调用一次自定义的核函数,此时向核函数虽然也会传入两个参数,但是两个参数的来源不一样,其中X1就是 clf.predict( )函数中的测试集参数 dataX_predict,而另一个X2是保存在内部的第一次用来训练而传入的训练集参数 dataX_train,也就是说,在预测标签时,是利用测试集和训练集两个数据在一起卷积计算一个核矩阵的。后来发现在sklearn中明确说明了 在预测时,同时需要训练集和测试集两部分数据。所以,我只要将上面提到的两个X1改成X2就成功了。
3. 实验结果及代码
实验数据与第一次数据一样,
使用自定义的laplace核对这三个数据进行训练和测试得到的ACC准确度分别为 0.908,0.7,0.7(注意,这里的
σ
=
1
\sigma=1
σ=1,
σ
\sigma
σ的取值对于最终预测的精确度影响较大)。
接下来就是全部代码了。
Acc.py:
import numpy as np
def acc(L1, L2):
sum = np.sum(L1[:]==L2[:])
return sum/len(L2)
SVM.py:
import numpy as np
from scipy.io import loadmat
from sklearn import svm
import Acc
import math
filename = 'C:/Users/ALIENWARE/Documents/作业/机器学习/datasets/' + input("input name of data file: ")
data = loadmat(filename)
dataX = data['X']
dataY = data['Y'].T[0]
dataX_train = []
dataX_predict = []
dataY_train = []
dataY_predict = []
num_Y = np.unique(dataY).astype(int)
for i in range(len(num_Y)):
temp = dataY == num_Y[i]
temp.astype(float)
num_Y[i] = np.sum(temp)
flag = 0
for j in range(len(dataY)):
if temp[j] == 1:
if flag < int(round(0.8*num_Y[i])):
dataX_train.append(dataX[j])
dataY_train.append(dataY[j])
flag += 1
else:
dataX_predict.append(dataX[j])
dataY_predict.append(dataY[j])
dataX_train = np.array(dataX_train)
dataX_predict = np.array(dataX_predict)
dataY_train = np.array(dataY_train)
dataY_predict = np.array(dataY_predict)
def laplace(X1, X2):
K = np.zeros((len(X1), len(X2)), dtype=np.float)
for i in range(len(X1)):
for j in range(len(X2)):
K[i][j] = math.exp(-math.sqrt(np.dot(X1[i] - X2[j], (X1[i] - X2[j]).T))/2)
return K
method = int(input("choose a kernel function(1/linear, 2/rbf, 3/laplace, 4/poly):"))
clf = svm.SVC()
if method == 1:
clf = svm.SVC(kernel="linear")
print("you choose linear kernel")
if method == 2:
clf = svm.SVC(kernel="rbf")
print("you choose rbf kernel")
if method == 3:
clf = svm.SVC(kernel=laplace)
print("you choose laplace kernel")
if method == 4:
clf = svm.SVC(kernel="poly")
print("you choose poly kernel")
clf.fit(dataX_train, dataY_train)
print(clf.predict(dataX_predict))
print(Acc.acc(clf.predict(dataX_predict), dataY_predict))
上面代码除了数据集的划分和Laplace自定义的核函数,另外三种核函数都是直接调用sklearn中现成的。