1 宝可梦

我也不知道为啥会有这一段,大概是因为我没童年吧~

宝可梦,全名「精灵宝可梦」,英文名Pokemon

其实我刚听到这个词的时候是有点懵的,看到给出的一些样例图才发现,这不就是我们小时候看的「神奇宝贝」嘛!(暴露年龄🤦‍)。我现在只记得会用十万伏特的皮卡丘,皮卡~皮卡~

另外我知道好像还有个叫「宠物小精灵」的,去查了一波才发现它们是同一个东西。

《精灵宝可梦》,另有常见非官方译名:《口袋妖怪》(中国大陆民间译名)、《宠物小精灵》(中国香港译名)、《神奇宝贝》(中国台湾译名)。——「百度百科」

玩过Pokemon Go的同学都应该知道,宝可梦是可以花费星尘和糖给宝可梦升级的!升级后宝可梦会更加厉害,即它的CP 值(战斗力Combat Power)会增加,PK起来杠杠的!

可是,宝可梦升级后战斗力究竟会增加多少呢?目前似乎没有人能给出,也许这个问题的答案只有大木博士知道!(大木博士是口袋妖怪研究专家,是宝可梦图鉴的创作人之一,也有传闻是小智的爸爸。。。)

作为一名掌握机器学习高端技术的炼金术士,我们也许能找到解决办法。

没错!今天我们就来尝试使用线性回归预测宝可梦升级后的战斗力!

2 数据

俗话说的好——「巧妇难为无米之炊」。没有数据,我们也是无法预测宝可梦的 CP 值的。

正巧,好心的 OpenIntro 提供了开源的数据: pokemon data

下面是官方给出的数据简介:

Pokemon Go 的一个关键部分是利用进化来获得更强大的宝可梦,更深入地了解进化是成为有史以来最伟大的 Pokemon Go 玩家的关键。这个数据集包括分布在四个物种中的75个宝可梦演变。提供了一系列变量,可以更深入地了解哪些特征对于预测宝可梦的最终战斗力(CP)非常重要。

数据集给出了很多宝可梦的很多特征,我选取了对本次任务有用两个特征:

  • cp: 进化前的战斗力
  • cp_new: 进化后的战斗力

接下来,我们读入下载得到的数据并观察它。

import pandas as pd

# 使用 pandas 读入数据
data_path = "./pokemon.csv"
df_origin = pd.read_csv(data_path)

# 筛选出 cp 和 cp_new 两列特征数据
df_data = df_origin[["cp", "cp_new"]]

# 先看一下前5条数据,确认是否有问题
print(df_data.head())

# 输出:
#    cp   cp_new
# 0  384  694
# 1  366  669
# 2  353  659
# 3  338  640
# 4  242  457

虽说看到了输出出来的数据,但是总觉得不够直观,毕竟一图胜千言

# 使用 matplotlib 将数据可视化
from matplotlib import pyplot as plt

plt.scatter(df_data["cp"], df_data["cp_new"], s=20, c="green", alpha=0.5)
plt.xlabel('cp')
plt.ylabel('cp_new')
plt.title('Pokemon Data')
plt.show()

# 输出

至此,数据准备完毕。(上吧,皮卡丘!)

3 线性回归

我打算用一元线性方程y=b+w*x来预测宝可梦升级战斗力,你在中学时就应该学过它,这里就不累述一遍了。

下图是随手画的一条直线y=b+w*x,如果想要预测的准确,所有宝可梦到这条线的距离之和应该是最短的,即误差的平方和最小。

那么预测宝可梦升级后的战斗力问题,就转化成解线性回归方程的问题,即求直线方程的bw使得误差有最小平方和。

方程的解法并不是特别复杂,就是大家耳熟能详的最小二乘法Least Squares

先来复习一波基础知识:

最小二乘法(又称最小平方法)是一种数学优化技术。它通过最小化误差的平方和寻找数据的最佳函数匹配。利用最小二乘法可以简便地求得未知的数据,并使得这些求得的数据与实际数据之间误差的平方和为最小。最小二乘法还可用于曲线拟合。其他一些优化问题也可通过最小化能量或最大化熵用最小二乘法来表达。——百度百科

通常情况下,对于一个回归任务,如果是线性方程去拟合,那么可以使用最小二乘法的公式直接计算;而如果是使用非线性方程去拟合,就必须是使用「迭代法」去拟合,常见的迭代法有「梯度下降法」和「牛顿法」。同样,如果你不嫌麻烦也可以用迭代法去拟合线性方程的。

这里我们就先用公式进行求解,然后再尝试用「迭代法」中的「梯度下降法」去拟合,最后用公式得到的数值来验证梯度下降法的拟合度。

TODO 最小二乘法公式推导

下面是公式求解:

# 最小二乘法
import numpy as np
import matplotlib.pyplot as plt

X = np.matrix([np.ones(df_data.index.size), df_data["cp"].values]).T
y = np.matrix(df_data["cp_new"].values).T
mat_result = np.linalg.inv(X.T.dot(X)).dot(X.T).dot(y)
b = round(mat_result.tolist()[0][0], 2)
w = round(mat_result.tolist()[1][0], 2)
print(f"b: {b}, w: {w}")

plt.scatter(df_data["cp"], df_data["cp_new"], s=20, c="green", alpha=0.5)

line_x = np.linspace(0, 630)
line_y = np.array(mat_result[0] + mat_result[1] * line_x)
plt.plot(line_x, line_y.T, color='red')

plt.text(200, 1080, f"y={b}+{w}*x", rotation=30, fontsize=14, fontstyle="italic")

plt.xlabel('cp')
plt.ylabel('cp_new')
plt.title('Least Squares: Pokemon')
plt.show()

# 输出
# b: -119.35, w: 2.41

通过公式我们得到直线方程y=-119.35+2.41*x,接下来我们试试梯度下降法能不能成功拟合数据,若是可以拟合,和公式的求得的差距有多大。

4 梯度下降法

TODO 梯度下降法介绍

fig = plt.figure()
line = plt.plot([], [], color="red")
line = line[0]
label_epoch = plt.text([], [], '', fontsize=14, fontstyle="italic")
label_wb = plt.text([], [], '', fontsize=14, fontstyle="italic")

def init():
    plt.scatter(df_data["cp"], df_data["cp_new"], s=20, c="green", alpha=0.5)
    plt.xlabel('cp')
    plt.ylabel('cp_new')
    plt.title('Gradient Descent: Pokemon')

def grad_step(w, b):
    mse = 0
    w_grad = 0
    b_grad = 0
    n = df_data.index.size
    for x, y in zip(df_data["cp"].values, df_data["cp_new"].values):
        mse += (1/n) * (y-(w*x+b))**2
        w_grad += -(2/n) * x * (y-(w*x+b))
        b_grad += -(2/n) * (y-(w*x+b))

    return mse, w_grad, b_grad

def animate(data):
    e, mse, w, b = data
    if not e % 10:
        print(f"epoch: {e}, mse: {mse}, b: {b}, w: {w}")
    x1 = 0
    y1 = x1 * w + b
    x2 = 700
    y2 = x2 * w + b
    line.set_data([x1, x2], [y1, y2])
    label_epoch.set_text(f"epoch: {e}")
    label_epoch.set_position([50, 1400])
    label_wb.set_text(f"b: {b}, w: {w}")
    label_wb.set_position([50, 1200])

def data_gen():
    # 初始值
    w = 100
    b = 100
    # 学习率
    lr_w = 5*10e-7
    lr_b = 10e-2
    # 迭代次数
    epoch_num = 200
    for e in range(1, epoch_num+1):
        mse, w_g, b_g = grad_step(w, b)
        w = w - (lr_w * w_g)
        b = b - (lr_b * b_g)
        yield [e, round(mse, 2), round(w, 2), round(b, 2)]

anim = animation.FuncAnimation(fig, animate, data_gen, init_func=init, save_count=1000, interval=50)

anim.save('pokemon-gradient-descent.gif', writer='imagemagick')
plt.show()

# 输出
# epoch: 10, mse: 3762331.46, b: -3197.93, w: 13.68
# epoch: 20, mse: 1202723.93, b: -1855.07, w: 8.77
# epoch: 30, mse: 389084.12, b: -1097.96, w: 5.99
# epoch: 40, mse: 130446.92, b: -671.09, w: 4.43
# epoch: 50, mse: 48232.15, b: -430.42, w: 3.55
# epoch: 60, mse: 22097.99, b: -294.73, w: 3.06
# epoch: 70, mse: 13790.55, b: -218.23, w: 2.78
# epoch: 80, mse: 11149.8, b: -175.1, w: 2.62
# epoch: 90, mse: 10310.37, b: -150.78, w: 2.53
# epoch: 100, mse: 10043.54, b: -137.07, w: 2.48
# epoch: 110, mse: 9958.71, b: -129.34, w: 2.45
# epoch: 120, mse: 9931.75, b: -124.98, w: 2.43
# epoch: 130, mse: 9923.18, b: -122.52, w: 2.43
# epoch: 140, mse: 9920.46, b: -121.14, w: 2.42
# epoch: 150, mse: 9919.59, b: -120.36, w: 2.42
# epoch: 160, mse: 9919.32, b: -119.91, w: 2.42
# epoch: 170, mse: 9919.23, b: -119.67, w: 2.41
# epoch: 180, mse: 9919.2, b: -119.53, w: 2.41
# epoch: 190, mse: 9919.19, b: -119.45, w: 2.41
# epoch: 200, mse: 9919.19, b: -119.4, w: 2.41

我们使用梯度下降法进行 200 次训练,最终结果得到的方程为y=-119.35+2.41*x

对比最小二乘法公式求得的结果,我们发现它们几乎是一致的!也就是说我们成功的拟合了数据,而且效果不错!

下面是训练过程,你可以清晰的看到bw的变换。

5 参考

本文还留了很多坑,先发再补。