SVM支持向量机实现兵王问题的分类(Python版)
1.说明
最近在B站看了浙江大学胡浩基老师的机器学习课程,完全面向入门人群感觉挺好。其中有关原理的部分讲的很细。其中在第六章-支持向量机的例题兵王问题中课程只给了MATLAB的版本,没有Python语言的优势。所以本文首先根据胡老师的MATLAB版的思路改写成Python版,然后使用Python的优势重新编写一版。
2.问题分析
在国际象棋中,存在着一种残局的现象。剩余三子,分别是黑方的王,白方的王和兵,那么无论这三子在棋盘的布局如何,只有两种结果,白方胜利和逼和。这就是一个二分类问题。
3. 数据集
关于这个问题的数据集krkopt.DATA可以在老师给的代码里面找到,然后老师也推荐了一个网址UCI Machine Learning
数据形式:前面六个就是棋子的位置,draw就是逼和,后面的数字eight就代表,白棋最少用8步就能将死对方。
a,1,b,3,c,2,draw
a,1,c,1,c,2,draw
c,2,c,6,e,1,eight
...
4.代码实现
4.1 MATLAB思想的Python实现
使用了LIBSVM -- A Library for Support Vector Machines
# -*- coding: utf-8 -*-
import numpy as np
from libsvm.svm import *
from libsvm.svmutil import *
def data_read_mat(file_name):
'''
从文件中取出数据
:param file_name: 文件名称
:return: 返回一个n*7的矩阵,前6项是三个坐标,第七项是标签
'''
num_list = []
'''
一下是对数据进行读入并且处理,其中open的参数中encoding之所以设置成UTF-8-sig
是因为如果我们把这个参数设置为UTF-8或者不设置,在读入的开头多出\ufeff这么一串
东西,有时候会以中文字的形式出现。
'''
with open(file_name, "r", encoding='UTF-8-sig') as file:
for l in file:
l = l.split(',')
list_k = []
for j in range(3):
list_k.append(ord(l[j * 2]) - ord('a'))
list_k.append(ord(l[j * 2 + 1]) - ord('0'))
if (l[6][0] == 'd'):
list_k.append(0)
else:
list_k.append(1)
num_list.append(list_k)
num_mat = np.array(num_list, dtype="float")
'''
在此处是以numpy的二维数据矩阵的形式存储的,本以为使用numpy的数据进行运算可以使得
训练的速度快一些。结果发现如果要往libsvm中的函数传入参数只能传入list型不能传入numpy
的数据类型。所以,后面又把数据类型转回了list型。但是,我猜应该是有方法可以把numpy
的数据类型传入使用的。于是我在读取数据后任然返回的是numpy的形式。
'''
return num_mat
def data_deal(mat, len_train, len1, len_test, len2):
'''
将数据进行处理,分出训练数据和测试数据
:param mat: 大矩阵,其中包括训练数据和测试数据
:param len_train:训练数据
:param len1: 输入坐标
:param len_test: 测试数据
:param len2: 标签
:return: 返回的依次是训练输入数据,测试输入数据,训练输入数据的标签,测试输入数据的标签
'''
np.random.shuffle(mat) # 先将矩阵按行打乱。然后根据要求对矩阵进行分割,第一部分就是训练集,第二部分就是测试集
x_part1 = mat[0:len_train, 0:len1]
x_part2 = mat[len_train:, 0:len1]
y_part1 = mat[0:len_train, len1]
y_part2 = mat[len_train:, len1]
# 标准化
# 根据训练集求出均值和方差
avgX = np.mean(x_part1)
stdX = np.std(x_part1)
# print(avgX,stdX)
# 将训练集和测试集都进行标准化处理
for data in x_part1:
for j in range(len(data)):
data[j] = (data[j] - avgX) / stdX
for data in x_part2:
for j in range(len(data)):
data[j] = (data[j] - avgX) / stdX
return x_part1, y_part1, x_part2, y_part2
def TrainModel(CScale, gammaScale, prob):
'''
:param CScale: 参数C的取值序列
:param gammaScale: 参数γ的取值序列
:param prob: 训练集合对应的标签
:return: maxACC(最高正确率),maxACC_C(最优参数C),maxACC_gamma(最优参数γ)
'''
maxACC = 0
maxACC_C = 0
maxACC_gamma = 0
for C in CScale:
C_ = pow(2, C)
for gamma in gammaScale:
gamma_ = pow(2, gamma)
# 设置训练的参数
# 其中-v 5表示的是2折交叉验证
# “-q”可以去掉这样也就可以看到训练过程
param = svm_parameter('-t 2 -c ' + str(C_) + ' -g ' + str(gamma_) + ' -v 5 -q')
ACC = svm_train(prob, param) # 进行训练,但是传回的不是训练模型而是5折交叉验证的准确率
# 更新数据
if (ACC > maxACC):
maxACC = ACC
maxACC_C = C
maxACC_gamma = gamma
return maxACC, maxACC_C, maxACC_gamma
def getNewList(L, U, step):
l = []
while (L < U):
l.append(L)
L += step
return l
def TrainModelSVM(data, label, iter, model_file):
'''
模型训练并保存
:param data: 数据
:param label: 标签
:param iter:训练次数
:param model_file:模型的保存位置
:return: 返回最优参数
'''
# 将数据转换成list型的数据。因为,在svm的函数中好像只能传入list型的数据进行训练使用
X = data.tolist()
Y = label.tolist()
CScale = [-5, -3, -1, 1, 3, 5, 7, 9, 11, 13, 15] # 参数C的2^C
gammaScale = [-15, -13, -11, -9, -7, -5, -3, -1, 1, 3] # 参数γ的取值2^γ
cnt = iter
step = 2 # 用于重新生成CScale和gammaScale序列
maxACC = 0 # 训练过程中的最大正确率
bestACC_C = 0 # 训练过程中的最优参数C
bestACC_gamma = 0 # 训练过程中的最优参数γ
prob = svm_problem(Y, X) # 传入数据
while (cnt):
# 用传入的参数序列进行训练,返回的是此次训练的最高正确率,最优参数C,最优参数γ
maxACC_train, maxACC_C_train, maxACC_gamma_train = TrainModel(CScale, gammaScale, prob)
# 数据更新
if (maxACC_train > maxACC):
maxACC = maxACC_train
bestACC_C = maxACC_C_train
bestACC_gamma = maxACC_gamma_train
# 根据返回的参数重新生成CScale序列和gammaScale序列用于再次训练,下一次训练的C参数和γ参数的精度会比之前更高
# step就是CScale序列和gammaScale序列的相邻两个数之间的间隔
new_step = step * 2 / 10
CScale = getNewList(maxACC_C_train - step, maxACC_C_train + step + new_step, new_step)
gammaScale = getNewList(maxACC_gamma_train - step, maxACC_gamma_train + step + new_step, new_step)
cnt -= 1
# 获得最优参数后计算出对应的C和γ,并且训练获得“最优模型”
C = pow(2, bestACC_C)
gamma = pow(2, bestACC_gamma)
param = svm_parameter('-t 2 -c ' + str(C) + ' -g ' + str(gamma))
model = svm_train(prob, param) # 交叉验证准确率
svm_save_model(model_file, model) # 保存模型
return model
def main():
data_file = r"/Users/apple/Documents/PycharmPro/svmtest2/krkopt.data" # 数据存放的位置(需要修改)
mode_file = r"/Users/apple/Documents/PycharmPro/svmtest2/model_file" # 训练模型保存的位置(需要修改)
data_mat = data_read_mat(data_file) # 从文件中读取数据并处理
# 以下是对数据训练进行分配,可以根据你的需要进行调整
train = 5000 # 5000组数据作为训练数据
test = len(data_mat) - 5000 # 剩下的数据作为测试数据
# ————————————————————————————————————————————————————————————#
x_len = 6 # 输入数据的维度是6维,即三个棋子的坐标
y_len = len(data_mat[0] - x_len) # 输出的数据时1维,即两种结果
iter = 2 # 训练的次数,训练的次数越多参数就调整的精度就越高
x_train, y_train, x_test, y_test = data_deal(data_mat, train, x_len, test, y_len) # 对数据进行分割
if (input("是否需要进行训练?") == 'y'): # 如果输入y就会进行训练,否则就可以直接使用之前训练的完成的模型
model = TrainModelSVM(x_train, y_train, iter, mode_file) # 传入输入数据,标签进行模型的训练
else:
model = svm_load_model(mode_file) # 直接加载现有模型
X = x_test.tolist() # 将测试集的输入集转换成list
Y = y_test.tolist() # 将测试集的输出集转换成list
p_labs, p_acc, p_vals = svm_predict(Y, X, model)
if __name__ == "__main__":
main()
4.2 Python库简化后代码实现
(1) 使用了Pandas处理数据集
(2) 使用了sklearn.model_selection数据集分割,自动调参
(3) 使用了sklearn.svm线性支持向量分类
import pandas as pd
from sklearn.model_selection import train_test_split,cross_val_score,GridSearchCV
from sklearn.svm import SVC
import numpy as np
def svm_c(x_train, x_test, y_train, y_test):
# rbf核函数,设置数据权重
svc = SVC(kernel='rbf', class_weight='balanced',)
c_range = np.logspace(-5, 15, 11, base=2)
gamma_range = np.logspace(-9, 3, 13, base=2)
# 网格搜索交叉验证的参数范围,cv=3,3折交叉
param_grid = [{'kernel': ['rbf'], 'C': c_range, 'gamma': gamma_range}]
grid = GridSearchCV(svc, param_grid, cv=3, n_jobs=-1)
# 训练模型
clf = grid.fit(x_train, y_train)
# 计算测试集精度
score = grid.score(x_test, y_test)
print('精度为%s' % score)
# 读取数据
data = pd.read_csv('/Users/apple/Documents/PycharmPro/svmtest/krkopt.data', header=None)
data.dropna(inplace=True) # 不创建新的对象,直接对原始对象进行修改
# 样本数值化 a,b,c...h 转化为 1,2,3...8
for i in [0, 2, 4]:
data.loc[data[i] == 'a', i] = 1
data.loc[data[i] == 'b', i] = 2
data.loc[data[i] == 'c', i] = 3
data.loc[data[i] == 'd', i] = 4
data.loc[data[i] == 'e', i] = 5
data.loc[data[i] == 'f', i] = 6
data.loc[data[i] == 'g', i] = 7
data.loc[data[i] == 'h', i] = 8
# 将标签数值化 -1表示将死,1表示和棋
data.loc[data[6] != 'draw', 6] = -1
data.loc[data[6] == 'draw', 6] = 1
# 归一化处理 Z-score标准化方法 (经过处理的数据符合标准正态分布,即均值为0,标准差为1)
# from sklearn.preprocessing import StandardScaler
for i in range(6):
data[i] = (data[i]-data[i].mean())/data[i].std()
# 拆分训练集和测试集 train_teat_split() https://www.cnblogs.com/bonelee/p/8036024.html 省略random_state=0
X_train, X_test, y_train, y_test = train_test_split(data.iloc[:, :6], data[6].astype(int), test_size=0.82178500142572)
svm_c(X_train, X_test, y_train, y_test)
4.3 实验结果
老师用Matlab得到的结果是99.61%,那么上述程序的多次实验,平均正确率为99.53%,可以看到差距不是很大,而且呢就算是同一个程序得到的结果可能也会有差距,这是因为划分测试集和训练集的不同造成的。
AUC和EER曲线:
AUC是指黄色曲线和x轴的面积,EER是指蓝色曲线与黄色曲线的交点的横坐标,衡量一个系统的好坏就在于auc越大,性能越好,eer越小性能越好。
auc_err.png
5.SVM支持向量机总结
5.1 SVM的步骤
(1) 将原始数据转化为SVM算法软件或包所能识别的数据格式;
(2) 将数据标准化;(防止样本中不同特征数值大小相差较大影响分类器性能)
(3) 不知使用什么核函数,考虑使用RBF;
(4) 利用交叉验证网格搜索寻找最优参数(C, γ);(交叉验证防止过拟合,网格搜索在指定范围内寻找最优参数)
(5) 使用最优参数来训练模型;
(6) 测试。
5.2 网格搜索参数小技巧
网格搜索法中寻找最优参数中为寻找最优参数,网格大小如果设置范围大且步长密集的话难免耗时,但是不这样的话又可能找到的参数不是很好,针对这解决方法是,先在大范围,大步长的粗糙网格内寻找参数。在找到的参数左右在设置精细步长找寻最优参数比如:
一开始寻找范围是 C = 2^−5 , 2^−3 , . . . , 2^15 and γ = 2^−15 , 2^−13 , . . . , 2^3 .由此找到的最优参数是(2^3 , 2^−5 );
然后设置更小一点的步长,参数范围变为2^1 , 2^1.25 , . . . , 2^5 and γ = 2^−7 , 2^−6.75 , . . . , 2^−3 在这个参数范围再寻找最优参数。
这样既可以避免一开始就使用大范围,小步长而导致分类器进行过于多的计算而导致计算时间的增加。
5.3 线性核和RBF的选择
如果训练样本的特征数量过于巨大,也许就不需要通过RBF等非线性核函数将其映射到更高的维度空间上,利用非线性核函数也并不能提高分类器的性能。利用linear核函数也可以获得足够好的结果,此外,也只需寻找一个合适参数C,但是利用RBF核函数取得与线性核函数一样的效果的话需要寻找两个合适参数(C, γ)。
分三种情况讨论:
(1) 样本数量远小于特征数量:这种情况,利用情况利用linear核效果会高于RBF核。
(2) 样本数量和特征数量一样大:线性核合适,且速度也更快。liblinear更适合
(3) 样本数量远大于特征数量: 非线性核RBF等合适。