背景
继续学习浙大《机器学习》课程。第一部分支持向量机学的差不多了,终于可以整点实践了。课程里展示了用libsvm求解兵王问题。
课程里是用matlab演示的。但我从来没用matlab,所以按照自己的理解用python写了一遍。自己实践的时候也发现一些看视频的时候没想到的问题。这里就讲解下我的实践。
兵王问题
简单搜了下网上居然没什么百科词条专门讲兵王问题。(看来这个问题也不是啥知名的问题啊😓)
兵王问题就是一个国际象棋棋盘上,一方只剩一个王,另一方剩一个王一个兵,需要判断出一王一兵的这方能不能赢得最终的胜利(还是会被对方逼和)。
注意,上图这个局面(现在轮到黑方行动),是黑方逼和成功。
没错,我也不理解。这个和中国象棋不一样。现在黑方是无路可走,所以是和棋。
Anyway,总之问题就是给出三个棋子的位置,判断结果一方是否能赢。
数据集
当然了,我们不用自己去整很多不同的局面出来再自行判断每个局面结果如何。原视频中给出了测试数据下载链接(UCI Machine Learning Repository)。里面已经提供了一组数据,一共有28000+个。每一组数据是三个棋子的位置加上这个局面的结果。
然而,在这个链接上我并没有搜到数据文件。不过在github的这个repo里找到了数据文件。
求解步骤
我看到网上也有很多文章也做了同样的事,大都把整个过程写在了一块。我趋向于把这些步骤分开,可以让我们对每一步的作用更清晰,也可以避免每次运行都要把整个过程都跑一遍。
先大体说一下步骤,再看每一步的细节。
- 将数据集抽出一部分作为训练集,剩下的作为测试集。这个大家也都明白,否则会产生过拟合。相当于拿练习的卷子当作考试的卷子。
- 对数据进行归一化。
- 用训练集训练模型,尝试不同的超参数 C C C 和 γ \gamma γ,以寻找最优超参数。
- 以最优超参数,训练出最终模型。
- 使用最终模型在测试集上进行测试,查看模型的准确率。
分割测试集
课程视频里把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+C⋅i=1∑Nδi
这里的
C
C
C是一个需要人为设定的超参数。
另外因为支持向量机问题的求解中用到了核函数戏法并且选择了高斯核函数:
e
−
γ
(
∥
X
−
X
′
∥
2
)
e^{-\gamma( \left \| X - X' \right \|^2)}
e−γ(∥X−X′∥2)
另一个超参数就是这里的 γ \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')
- 这里先读取训练集数据
krkopt_train.data
。并进行数据归一化(调用normalize_data
)。- 所谓归一化,就是把一列数据的每一项减去这一列的平均值再除以这一列的标准差。以此保证所有数据都处于同一个范围中。
- 用pandas(配合chatgpt,嘿嘿)可以很容易的实现。
- 然后先粗粒度的找出
C
C
C 和
γ
\gamma
γ 的值。
- 对应第一次调用
search_optimal_hyper_param
的地方。 - 课程视频里说的是 C C C 的取值范围是 ( 2 − 5 , 2 15 ) (2^{-5}, 2^{15}) (2−5,215), γ \gamma γ的取值范围是 ( 2 − 15 , 2 3 ) (2^{-15}, 2^{3}) (2−15,23)。
- 我心想,龟龟,这么大的范围可怎么找呀。这里的细节视频里也并没有细说。
- 我自己想的是,实际操作的时候应该是在每一个范围里都挑一个数字然后进行训练以及交叉验证吧。看了这个github上的repo,确实如此。
- 所以这里粗粒度的先取 C C C为 [ 2 − 5 , 2 − 4 , 2 − 3 , . . . 2 15 ] [2^{-5}, 2^{-4}, 2^{-3}, ... 2^{15} ] [2−5,2−4,2−3,...215], γ \gamma γ取 [ 2 − 15 , 2 − 14 , 2 − 13 , . . . 2 3 ] [2^{-15}, 2^{-14}, 2^{-13}, ... 2^{3}] [2−15,2−14,2−13,...23]。遍历所有组合,我这边求出最优的情况是 C = 2 8 , γ = 2 − 4 C =2^{8}, \gamma = 2^{-4} C=28,γ=2−4。
- 五折交叉验证
- 课程视频里非常强调这一点。就是我们在寻找最优超参数的过程中,依然不能用整个训练集的识别率作为这一组超参数的指标。而是要切出一部分作为测试来得到一个识别率。五折就是,分成五组,每次在四组上训练,在剩下的一组上测试得到识别率。
- libsvm直接提供了交叉验证的功能,就是参数中的
-v 5
。但需要注意的是,在传了这个参数之后,svm_train
返回的就是一个识别率。如果不传这个参数,svm_train
返回的是一个训练之后的模型。当然这也正好符合我们的需求。我们现在是想找出不同超参数的识别率,还没到训练最终的模型。
- 对应第一次调用
- 然后再细粒度的优化
C
C
C 和
γ
\gamma
γ 的值。
- 对应第二次调用
search_optimal_hyper_param
。 - 过程和第一次基本是一样的。
- 基于上次的结果 C = 2 8 , γ = 2 − 4 C =2^{8}, \gamma = 2^{-4} C=28,γ=2−4。这一次的搜索范围是 [ − 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,γ=2−3.6
- 对应第二次调用
- 训练最终模型
- 对应代码调用
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
欢迎参考,互相交流学习~