[浙大机器学习课程] 用libsvm求解兵王问题

背景

继续学习浙大《机器学习》课程。第一部分支持向量机学的差不多了,终于可以整点实践了。课程里展示了用libsvm求解兵王问题。

课程里是用matlab演示的。但我从来没用matlab,所以按照自己的理解用python写了一遍。自己实践的时候也发现一些看视频的时候没想到的问题。这里就讲解下我的实践。

兵王问题

简单搜了下网上居然没什么百科词条专门讲兵王问题。(看来这个问题也不是啥知名的问题啊😓)

兵王问题就是一个国际象棋棋盘上,一方只剩一个王,另一方剩一个王一个兵,需要判断出一王一兵的这方能不能赢得最终的胜利(还是会被对方逼和)。

兵王问题
注意,上图这个局面(现在轮到黑方行动),是黑方逼和成功。

没错,我也不理解。这个和中国象棋不一样。现在黑方是无路可走,所以是和棋。

Anyway,总之问题就是给出三个棋子的位置,判断结果一方是否能赢。

数据集

当然了,我们不用自己去整很多不同的局面出来再自行判断每个局面结果如何。原视频中给出了测试数据下载链接(UCI Machine Learning Repository)。里面已经提供了一组数据,一共有28000+个。每一组数据是三个棋子的位置加上这个局面的结果。

然而,在这个链接上我并没有搜到数据文件。不过在github的这个repo里找到了数据文件。

求解步骤

我看到网上也有很多文章也做了同样的事,大都把整个过程写在了一块。我趋向于把这些步骤分开,可以让我们对每一步的作用更清晰,也可以避免每次运行都要把整个过程都跑一遍。

先大体说一下步骤,再看每一步的细节。

  1. 将数据集抽出一部分作为训练集,剩下的作为测试集。这个大家也都明白,否则会产生过拟合。相当于拿练习的卷子当作考试的卷子。
  2. 对数据进行归一化。
  3. 用训练集训练模型,尝试不同的超参数 C C C γ \gamma γ,以寻找最优超参数。
  4. 以最优超参数,训练出最终模型。
  5. 使用最终模型在测试集上进行测试,查看模型的准确率。

分割测试集

课程视频里把28000+个数据中分出了5000个作为训练集。我们也分5000个。

这是第一部分的代码 (dataset_devide.py):

import pandas as pd

TRAIN_DATASET_SIZE = 5000

if __name__ == '__main__':
  df = pd.read_csv('krkopt.data', header=None)

  shuffled = df.sample(frac=1).reset_index(drop=True)

  train_data = shuffled[:TRAIN_DATASET_SIZE]
  test_data = shuffled[TRAIN_DATASET_SIZE:]

  train_data.to_csv('krkopt_train.data', header=False, index=False)
  test_data.to_csv('krkopt_test.data', header=False, index=False)

就是读取krkopt.data,然后打乱,再分成两部分。抽出5000个存入krkopt_train.data。剩下的存入krkopt_test.data

个人很喜欢用pandas,这里使用pandas非常容易。

寻找最优超参数

两个超参数

回顾一下,支持向量机问题的原问题。最小化:
1 2 ∥ ω ∥ 2 + C ⋅ ∑ i = 1 N δ i \frac{1}{2} \left \| \omega \right \|^{2} + C \cdot \sum_{i=1}^{N}\delta_{i} 21ω2+Ci=1Nδi
这里的 C C C是一个需要人为设定的超参数。

另外因为支持向量机问题的求解中用到了核函数戏法并且选择了高斯核函数:
e − γ ( ∥ X − X ′ ∥ 2 ) e^{-\gamma( \left \| X - X' \right \|^2)} eγ(XX2)

另一个超参数就是这里的 γ \gamma γ

啰嗦两句解释下 γ \gamma γ。在求解的过程中,实际上是对原来的维度(三个棋子的六个坐标)进行了高维映射,从而让它们在高维空间变得"更容易区分"。通过核函数戏法,使得我们在求解的过程中不用去关心这个高维映射的具体形式。这个 γ \gamma γ应该是和维度相关。

代码实现

这部分是整个过程中最重要的部分了。代码如下:

import numpy as np
import pandas as pd
from libsvm.svm import *
from libsvm.svmutil import svm_train, svm_save_model


def normalize_data(df: pd.DataFrame) -> pd.DataFrame:
  df['File_A'] = df['File_A'].map(lambda x: ord(x) - ord('a'))
  df['File_B'] = df['File_B'].map(lambda x: ord(x) - ord('a'))
  df['File_C'] = df['File_C'].map(lambda x: ord(x) - ord('a'))
  df['odw'] = df['odw'].map(lambda x: 1 if x == 'draw' else 0)

  normalized = pd.DataFrame({'odw': df['odw']})

  df = df.drop(columns=['odw'])
  df = (df - df.mean()) / df.std()

  normalized = pd.concat([df, normalized], axis=1)
  return normalized


def search_optimal_hyper_param(df: pd.DataFrame, C_list: list, Gamma_list: list) -> (float, float, float):
  Y = df['odw'].tolist()

  df = df.drop(columns=['odw'])
  X = df.values.tolist()

  prob = svm_problem(Y, X)
  max_r = 0
  hyper_C = None
  hyper_Gamma = None
  for C in C_list:
    for Gamma in Gamma_list:
      param = svm_parameter(f'-t 2 -v 5 -c {2 ** C} -g {2 ** Gamma} -h 0')
      m = svm_train(prob, param)
      if m > max_r:
        max_r = m
        hyper_C = C
        hyper_Gamma = Gamma

  return max_r, hyper_C, hyper_Gamma


def train_final_model(df: pd.DataFrame, C: float, Gamma: float, model_file_name: str) -> None:
  Y = df['odw'].tolist()

  df = df.drop(columns=['odw'])
  X = df.values.tolist()

  prob = svm_problem(Y, X)
  param = svm_parameter(f'-t 2 -c {2 ** C} -g {2 ** Gamma} -h 0')
  model = svm_train(prob, param)
  svm_save_model(model_file_name, model)


if __name__ == '__main__':
  df = pd.read_csv('krkopt_train.data', header=None)
  df.rename(columns={0: 'File_A', 1: 'Rank_A', 2: 'File_B', 3: 'Rank_B', 4: 'File_C', 5: 'Rank_C', 6: 'odw'},
            inplace=True)
  normalized_df = normalize_data(df)

  # Find optimal hyper parameters - coarse-grained
  C_list = list(range(-5, 16, 1))
  Gamma_list = list(range(-15, 4, 1))
  max_rate, C, Gamma = search_optimal_hyper_param(normalized_df, C_list, Gamma_list)

  # Find optimal hyper parameters - fine-grained
  n = 10
  C_low = 0.5 * (max(-5, C - 1) + C)
  C_upper = 0.5 * (min(15, C + 1) + C)
  C_list = list(np.arange(C_low, C_upper, (C_upper - C_low) / n))
  Gamma_low = 0.5 * (max(-15, Gamma - 1) + Gamma)
  Gamma_upper = 0.5 * (min(4, Gamma + 1) + Gamma) + 0.001
  Gamma_list = list(np.arange(Gamma_low, Gamma_upper, (Gamma_upper - Gamma_low) / n))
  max_rate, C, Gamma = search_optimal_hyper_param(normalized_df, C_list, Gamma_list)
  print(f'Optimal hyper parameters: C={C}, Gamma={Gamma}, max_rate={max_rate}')

  # Train final model
  train_final_model(normalized_df, C, Gamma, 'krkopt.model')
  1. 这里先读取训练集数据krkopt_train.data。并进行数据归一化(调用normalize_data)。
    • 所谓归一化,就是把一列数据的每一项减去这一列的平均值再除以这一列的标准差。以此保证所有数据都处于同一个范围中。
    • 用pandas(配合chatgpt,嘿嘿)可以很容易的实现。
  2. 然后先粗粒度的找出 C C C γ \gamma γ 的值。
    • 对应第一次调用search_optimal_hyper_param的地方。
    • 课程视频里说的是 C C C 的取值范围是 ( 2 − 5 , 2 15 ) (2^{-5}, 2^{15}) (25,215) γ \gamma γ的取值范围是 ( 2 − 15 , 2 3 ) (2^{-15}, 2^{3}) (215,23)
    • 我心想,龟龟,这么大的范围可怎么找呀。这里的细节视频里也并没有细说。
    • 我自己想的是,实际操作的时候应该是在每一个范围里都挑一个数字然后进行训练以及交叉验证吧。看了这个github上的repo,确实如此。
    • 所以这里粗粒度的先取 C C C [ 2 − 5 , 2 − 4 , 2 − 3 , . . . 2 15 ] [2^{-5}, 2^{-4}, 2^{-3}, ... 2^{15} ] [25,24,23,...215] γ \gamma γ [ 2 − 15 , 2 − 14 , 2 − 13 , . . . 2 3 ] [2^{-15}, 2^{-14}, 2^{-13}, ... 2^{3}] [215,214,213,...23]。遍历所有组合,我这边求出最优的情况是 C = 2 8 , γ = 2 − 4 C =2^{8}, \gamma = 2^{-4} C=28,γ=24
    • 五折交叉验证
      • 课程视频里非常强调这一点。就是我们在寻找最优超参数的过程中,依然不能用整个训练集的识别率作为这一组超参数的指标。而是要切出一部分作为测试来得到一个识别率。五折就是,分成五组,每次在四组上训练,在剩下的一组上测试得到识别率。
      • libsvm直接提供了交叉验证的功能,就是参数中的-v 5。但需要注意的是,在传了这个参数之后,svm_train返回的就是一个识别率。如果不传这个参数,svm_train返回的是一个训练之后的模型。当然这也正好符合我们的需求。我们现在是想找出不同超参数的识别率,还没到训练最终的模型。
  3. 然后再细粒度的优化 C C C γ \gamma γ 的值。
    • 对应第二次调用search_optimal_hyper_param
    • 过程和第一次基本是一样的。
    • 基于上次的结果 C = 2 8 , γ = 2 − 4 C =2^{8}, \gamma = 2^{-4} C=28,γ=24。这一次的搜索范围是 [ − 8.5 , 7.5 ] [-8.5, 7.5] [8.5,7.5] [ − 4.5 , − 3.5 ] [-4.5, -3.5] [4.5,3.5]。在这两个区间里均匀插10个值,再测一遍。
    • 这一次我得到的结果,是 C = 2 8 , γ = 2 − 3.6 C =2^{8}, \gamma = 2^{-3.6} C=28,γ=23.6
  4. 训练最终模型
    • 对应代码调用train_final_model
    • 有了最优超参数,我们就用这组超参数来训练最终的模型。
    • 这次是用全部的样本了,而不是交叉验证。所以没有-v选项。如上所述,没有-v选项的情况下返回的是一个model。
    • 训练完成后我们把model存储下来,这里我存为krkopt.model

使用最终模型

这就相当于我们已经炼好丹了,把它投入实际使用。代码:

from libsvm.svmutil import svm_load_model, svm_predict

from optimal_hyper_param import normalize_data
import pandas as pd

if __name__ == '__main__':
  df = pd.read_csv('krkopt_test.data', header=None)
  df.rename(columns={0: 'File_A', 1: 'Rank_A', 2: 'File_B', 3: 'Rank_B', 4: 'File_C', 5: 'Rank_C', 6: 'odw'},
            inplace=True)
  df = normalize_data(df)

  model = svm_load_model('krkopt.model')

  Y = df['odw'].tolist()
  df = df.drop(columns=['odw'])
  X = df.values.tolist()
  _, p_acc, _ = svm_predict(Y, X, model)

代码也非常简单啦,没什么好多说的。就是先读取测试集,做一下归一化。然后加载model。接着,测试!

我的模型最终结果是

Accuracy = 99.3885% (22916/23057) (classification)

这识别率有点拉-_-||

课程里的结果是99.6%+。我看别人自己做的也有99.5%+。不知道为啥我这边比较差一点。可能是我运气不好分的集合不够好。在这里也不多纠结了。道友们可以分享下得到的最优超参数是多少。

源代码

我的源码都放在这里了:ML-Pawn-King

欢迎参考,互相交流学习~

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值