SVM支持向量机实现兵王问题的分类(Python版)

2020-07-04  本文已影响0人  正能量y先生

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等合适。

上一篇下一篇

猜你喜欢

热点阅读