前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >盘一盘 Python 系列 11 - Keras (中)

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

作者头像
用户5753894
发布2021-01-06 15:26:20
7950
发布2021-01-06 15:26:20
举报
文章被收录于专栏:王的机器王的机器

0

引言

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

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!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-12-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 王的机器 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档