盘一盘 Python 系列 11 - Keras (中)

2021-01-06 15:26:20 浏览数 (1)

0

引言

本文是 Python 系列的第十四篇,也是深度学习框架的 Keras 中篇,离上篇相隔时间太久。整套 Python 盘一盘系列目录如下:

  • Python 入门篇 (上)
  • Python 入门篇 (下)
  • 数组计算之 NumPy (上)
  • 数组计算之 NumPy (下)
  • 科学计算之 SciPy
  • 数据结构之 Pandas (上)
  • 数据结构之 Pandas (下)
  • 基本可视化之 Matplotlib
  • 统计可视化之 Seaborn
  • 炫酷可视化之 PyEcharts
  • 机器学习之 Sklearn
  • 机学可视化之 Scikit-Plot
  • 深度学习之 Keras (上)
  • 深度学习之 Keras (中)

1

数据分析

在本贴中使用的是从 1985 年到 2018 年美国大学篮球比赛 (US College Basketball tournament games)数据。它属于 NCAA 下面的篮球比赛,始于每年的十一月下旬,跨时四个月,约在三月初左右结束常规赛 (season)。在三月初常规赛季结束后,绝大多数联盟都会取该联盟的前八名球队,交叉配对 (第一名对第八名,第二名对第七名,以此类推)进行新一轮一场定胜负的锦标赛 (tourament)。

读取常规赛和锦标赛的数据,发现前者比后者的数据量大很多,而本贴的目标就是用常规赛数据来训练神经网络再预测锦标赛结果。首先读取数据并查看数据特征。

代码语言:javascript复制
season = pd.read_csv('season.csv')
tourney = pd.read_csv('tourney.csv')
代码语言:javascript复制
season.info()
代码语言:javascript复制
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 312178 entries, 0 to 312177
Data columns (total 8 columns):
 # Column Non-Null Count Dtype
--- ------ -------------- -----
 0   season 312178 non-null  int64
 1   team_1 312178 non-null  int64
 2   team_2 312178 non-null  int64
 3   home 312178 non-null  int64
 4   score_diff 312178 non-null  int64
 5   score_1 312178 non-null  int64
 6   score_2 312178 non-null  int64
 7   won 312178 non-null  int64
dtypes: int64(8)
memory usage: 19.1 MB
代码语言:javascript复制
tourney.info()
代码语言:javascript复制
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4234 entries, 0 to 4233
Data columns (total 9 columns):
 # Column Non-Null Count Dtype
--- ------ -------------- -----
 0   season 4234 non-null   int64
 1   team_1 4234 non-null   int64
 2   team_2 4234 non-null   int64
 3   home 4234 non-null   int64
 4   seed_diff 4234 non-null   int64
 5   score_diff 4234 non-null   int64
 6   score_1 4234 non-null   int64
 7   score_2 4234 non-null   int64
 8   won 4234 non-null   int64
dtypes: int64(9)
memory usage: 297.8 KB

前者有 8 个特征,而后者有 9 个特征,多了一个 seed_diff。球队打完常规赛后会排名,按照名次来打锦标赛,这个 seed_diff 就是两队的排名差。训练一个模型,输入是排名差,输出是比分差 (score_diff)。

  • 所有排名以 1 到 16 来表示,那么排名差的范围从 -15 到 15
  • 比分差大概范围从 -50 到 50

打印出锦标赛数据的前五行,发现队伍的字符串都由整数来编码了。再者该数据是对称的,比如行 0 和 4,是队伍 288 和 73 的一场比赛结果,但分别把 288 当作 team_1 和 team_2,因此两行中的 seed_diff 和 score_diff 都互为相反数。

代码语言:javascript复制
tourney.head()

有了 NCAA 数据,在接下三节里构建简单模型、多输入模型 (multi-input model) 和多输出模型 (multi-output model)。

2

简单模型

2.1

知识回顾

虽然 Keras 构建的都是神经网络,但也能构建线性回归和对率回归模型,因为两种模型都是神经网络的特殊形式,即不带隐含层。再精简一步,对于单变量线性回归和对率回归模型,那么该神经网络就只有一个输入和一个输出,示意图如下:

上图中 是输入, 是激活函数,对于线性回归 ;对于对率回归 ,因此 将输入转换成输出。从功能上讲,Keras 将输入和输出类比成张量 (tensor),将函数类比成层 (layer),将输入经过若干层得到输出的流程类比成模型 (model)。结合 Keras 中定义的示意图如下:

根据上图在牢记以下四点便可以轻松在 Keras 中构建模型了:

  1. Input()中形状参数代表输入维度,Input((1,))指输入张量是一维标量
  2. Dense()中参数代表输出维数,Dense(1)指输出一个标量
  3. 层函数作用在张量上并返回另一个张量,这两个张量分别称为该层的输入张量和输出张量
  4. 构建模型只需将最初的输入张量和最终的输出张量“捆绑”在一起即可

趁热打铁用代码巩固以上知识,首先引入需要的模块,Input 创建输入张量,Dense 创建层,Model 构建模型,plot_model 可视模型。

代码语言:javascript复制
from keras.layers import Input, Dense
from keras.models import Model
from keras.utils import plot_model

用 Input()创建输入张量,检查其类型是 Tensor,形状是 (None, 1),None 指的是每批训练的数据个数,通常在训练时 fit() 函数中 batch_size 参数决定。

代码语言:javascript复制
input_tensor = Input(shape=(1,))
print(type(input_tensor), 'n', input_tensor)
代码语言:javascript复制
<class 'tensorflow.python.framework.ops.Tensor'> 
 Tensor("input_1:0", shape=(None, 1), dtype=float32)

用 Dense()创建输出层,检查其类型是 layers,是层对象。

代码语言:javascript复制
output_layer = Dense(1)
print(type(output_layer), 'n', output_layer)
代码语言:javascript复制
<class 'tensorflow.python.keras.layers.core.Dense'> 
 <tensorflow.python.keras.layers.core.Dense object at 0x7fb1df536e80>

将输出张量传入层得到输出张量,检查其类型是 Tensor,形状是 (None, 1)。

代码语言:javascript复制
output_tensor = output_layer(input_tensor)
print(type(output_tensor), 'n', output_tensor)
代码语言:javascript复制
<class 'tensorflow.python.framework.ops.Tensor'> 
 Tensor("dense/BiasAdd:0", shape=(None, 1), dtype=float32)

用 Model()创建模型,将输入和输出张量“绑在”一起。检查其类型是 Functional,是函数对象。

代码语言:javascript复制
model = Model(input_tensor, output_tensor)
print(type(model), 'n', model)
代码语言:javascript复制
<class 'tensorflow.python.keras.engine.functional.Functional'> 
 <tensorflow.python.keras.engine.functional.Functional object at 0x7fb1df5591d0>

模型创建完后,用 plot_model()函数可视化网络结构。

代码语言:javascript复制
plot_model(model, to_file='model.png')

还可用 .summary()函数来打出网络每层信息,以及参数总数。

代码语言:javascript复制
model.summary()

根据上面信息发现 Input 也当成一层了,之前提到过层其实类比函数,但输入只是一个张量,如真要当成函数,那么对应的就是个自身函数 f(x) = x。在 Dense 层对应的参数是 2,因为多了一个偏置项,但这些细节没有在上面可视图中体现。补充完细节的示意图如下:

最后用 .compile()函数来编译模型,在函数中设定优化器、损失函数和监控指标等参数。

代码语言:javascript复制
model.compile(optimizer='adam', loss='mae', metrics=['accuracy'])

2.2

线性回归模型

把 2010 年之前的锦标赛当作训练集,2011年之后的锦标赛当作测试集。构建-编译-拟合-评估走一波。

该模型是单变量线性回归 y = wx b,顺着模型调出最后一层再使用 get_weights() 方法打印权重,并可视化拟合效果。

代码语言:javascript复制
w, b = model.layers[1].get_weights()
print(w, b)
代码语言:javascript复制
[[1.2168913]] [0.00527138]
代码语言:javascript复制
plt.scatter(test['seed_diff'], test['score_diff'])
x = np.linspace(test['seed_diff'].min(),test['seed_diff'].max(), 200)
plt.plot( x, w[0]*x b, color='red' );

2.3

对率回归模型

该模型是单变量对率回归

,调出最后一层再使用 get_weights() 方法打印权重,并可视化预测结果。标签中的胜负各占一半,但该极简模型预测出来的胜比负略多一些。

代码语言:javascript复制
w, b = model.layers[1].get_weights()
print(w, b)
代码语言:javascript复制
[[0.16914853]] [0.00290928]
代码语言:javascript复制
results = test[['seed_diff','won']]
preds = model.predict(test['seed_diff'])
a = np.zeros(preds.shape, dtype=int)
a[preds>=0.5] = 1
results['pred_won'] = a
plt.figure( figsize=(8,4), dpi=100 )
plt.subplot(1,2,1)
sns.countplot(data=results, x='won')
plt.subplot(1,2,2)
sns.countplot(data=results, x='pred_won')

3

多输入模型

上节只用了 seed_diff 一个特征来构建模型,本节用队伍 ID team_1 和 team_2 和主客场 home 来建模。首先引入必要的模块。

代码语言:javascript复制
from keras.layers import Input, Dense, 
Embedding, Flatten, Substract, Concatenate

嵌入层 (Embedding Layer)

特征 team_1 和 team_2 的值类型虽然是整数,但本质上是个类别型标签 (categorical label),强队对应的 team 值不见得大,弱队对应的 team 值也不见得小,因此不能将它们当整数用。通常使用独热编码(one-hot encoding) 向量化,但有两个缺点:一是向量之间缺乏有意义的关系,二是总共有10000 多支球队而向量过于稀疏。解决这些问题的方法是使用嵌入层,将高维稀疏向量转换为低维稠密向量。

具体而言,构建一个嵌入层代表团队实力 (team strengh),输入维度 input_dim 就是所有队伍个数,输出维度 output_dim为 1,即用一个标量代表团队实力值 (类比 word2vec 中用 300 维的向量代表一个词)。每个输入长度 input_length 为 1,因为 team_1 的值就是一个标量。

嵌入层本质就是查找表 (lookup table),将输入team ID 和团队实力一一对应,接着将所有球队的实力值打平作为“团队实力模型”的输出。

嵌入层首先用独热编码将 Team ID 装成向量,再通过查找表矩阵(元素是训练出来的) 获取权重,最后打平拼接起来。整套流程的可视图如下。

球队个数为 10,888,由于输出维度是 1,那么对应的参数也是 10,888。

代码语言:javascript复制
team_strength_model.summary()
代码语言:javascript复制
plot_model(team_strength_model)

共享层 (Shared Layer)

数据中有 team_1 和 team_2,如果任何一对球队互相比赛,模型就可以预测得分,即便这两队之前从未参加过比赛。此外无论是主队还是客队,它们强度等级相同。为此,可使用一个共享层,即重用上面模型team_strength_model()。

了解了每队的实力后,有两种处理方式:

  1. 相减层 (Substract Layer)
  2. 拼接层 (Concatenate Layer)

先看相减层,对 team_1 和 team_2 实力相减来确定赢得比赛的队。这有点像比赛委员会使用的种子,也是衡量团队实力的标准。但是之后将不使用种子差异来预测得分差异,而是使用自己的团队实力模型差异来预测得分差异。

可视化该模型,发现 Team-1-In 和Team-2-In 共享之前构建好的“团队实力”模型,得出的两组实力值在“相减层”中做差值。

代码语言:javascript复制
plot_model(model)

现在有两个输入,将它们传入列表作为 model.fit() 的参数。

众所周知篮球中有一个有据可查的主队优势,因此将向模型中添加新的输入以捕获这种效果。该模型将具有三个特征 team_1,team_2 和h ome,前两个用嵌入层转成“团队实力”,而 home 是一个二进制变量,如果 team_1 作为主队比赛为 1;否则为 0。

使用拼接层将两队的实力、主客场结合在一起,然后将结果传递给稠密层。

可视化该模型,发现 Team-1-In 和 Team-2-In 共享之前构建好的“团队实力”模型,得出的两组实力值和额外的主客场在“拼接层”中做合并,最后连接一个稠密层。

代码语言:javascript复制
plot_model(model)

现在有三个输入,将它们传入列表作为 model.fit() 的参数,和上面两个输入的代码比较,唯一的区别就是列表从包含两个元素增加到三个元素。由此可见 Keras 写起来真的非常灵活和优雅。

打印嵌入层 (layers[2]) 和稠密层 (layers[5]) 的参数,具体索引哪层可参考 model.summary()的信息。

嵌入层中的参数有 10,888 个,而稠密层中的参数有 4 个,包括 3 个 w 和 1 个 b。

堆积法 (Stacking)

之前使用常规赛模型对锦标赛数据进行了预测,效果不错,因此可将常规赛模型预测作为锦标赛模型的输入,能进一步提高模型预测效果,这是“模型堆叠”的一种形式。首先用常规赛季模型为基础,并根据锦标赛数据进行预测,将此预测作为新列添加到锦标赛数据中。

代码语言:javascript复制
tourney['pred'] = model.predict([tourney['team_1'], 
                                 tourney['team_2'], 
                                 tourney['home']])

该模型的输入是锦标赛数据中 'seed_diff' 和'pred' 两列,而输出是得分差异。

在测试集得到的结果 9.13 比之前的 10.09 更好一些。

4

多输出模型

本节将构建具有多个输出的神经网络,这些神经网络可用于

  • 解决具有多个目标的回归问题。
  • 同时解决回归问题和分类问题。

用锦标赛数据来建立一个做两个预测的模型,输入是两队的种子差异,输出它们得分。注意代码中褐色部分,为什么使用这样的学习率 lr、期数epochs 和批大小 batch_size?通过调节超参数,在 Keras 下篇后会细讲。

由于该模型是单输入双输出,那么两个回归模型都有各自的 w 和 b,参数一共有 4 个。

代码语言:javascript复制
multi_reg_model.summary()

打印参数发现两队得分的模型为

由于 x 代表的是种子差,其均值为 0,那么两队得分的均值大概就是 70.9。对于一队,排名差距增加一个单位,分数可增加 0.63 分,对于二队,排名差距增加一个单位 (二队增加相当于一队减少),分数可增加 0.54 分。

代码语言:javascript复制
w, b = multi_reg_model.layers[-1].get_weights()
print(w, w.shape)
print(b, b.shape)
代码语言:javascript复制
[[ 0.62823033 -0.54407144]] (1, 2)
[70.91642 70.94076] (2,)

计算真实种子差的均值得到是 0,因为该数据是完全对称的,两队一场比赛分为两条数据记录。两队得分的均值为 71.8,跟上面模型学到的 70.9 也很接近。

代码语言:javascript复制
train[['seed_diff','score_1','score_2']].mean()
代码语言:javascript复制
seed_diff     0.000000
score_1      71.786711
score_2      71.786711
dtype: float64

接着将创建另一种类型的两输出模型。 它不仅能预测得分差异,还然后预测一队赢得比赛的概率。该模型非常酷,它将同时进行分类和回归。

由于该模型是单输入双输出,那么线性回归和对率回归模型都有各自的 w 和 b,参数一共有 4 个。

代码语言:javascript复制
multi_regcls_model.summary()
代码语言:javascript复制
plot_model(multi_regcls_model)

由上图可知用于对率回归的层紧接着用于线性回归的层,因此分别从 layers[1] 和 layers[2] 获取对应的参数。

用上面的结果可看出两队种子每相差一位,比分就相差 1.21 分。使用 tf.keras 中的 sigmoid() 函数,将训练好的权重和偏置带入,得到当一队比二队多 1 分的时候,一队的胜率为 0.53;当一队比二队少 10 分的时候,一队的胜率为 0.21

总结:在本贴中我们复习了 Keras 中构建、编译、拟合和评估模型的步骤,并从简单模型开始讲解,到多输入模型 (介绍了嵌入层、共享层、合并层和堆积法等知识点),到多输入模型 (同时做两个回归、同时做回归和分类)。

在 Keras 下篇会专门讲如何用 Keras Tuner 和 Keras Wrapper 来调节超参数。

Stay Tuned!

0 人点赞