You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

381 lines
23 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 12深度学习如何用RNN预测激活率走势
你好,我是黄佳。
欢迎来到零基础实战机器学习。在上一讲中我们通过给鲜花图片分类学习了CNN在图像识别方面的应用。这一讲呢我们就来学习另外一种深度学习模型——循环神经网络RNNRecurrent Neural Network。那么进入正题之前我先给你讲个段子让你直观理解一下循环神经网络和其它神经网络模型有啥不同。
假如我和你开车去商场,然后我说“嘿!你知道吗,昨天老王的夫人生二胎了!”你说:“是吗?这么大年纪,真不容易。对了,上次你说他的项目没上线,对吧,后来那个项目到底怎么样了呢?”我回答说:“嗨,那个项目啊,别提了,把他整惨了,三次上线都失败了,现在公司在考虑放弃老王负责的这个项目。嗯,到了,你等我一会儿,我去给老王买个\_\_\_\_\_此处为填空选项1项目选项2玩具慰问一下他。”
若是这个选择题给循环神经网络之外的其它神经网络做,我们得到的答案可能是“项目”,因为在输入的文本(特征)中,一直是在谈项目,可正确答案显然是“玩具”。也就是说,如果让神经网络实现类似于人脑的语义判别能力,有一个很重要的前提是,**必须从所有过去的句子中保留一些信息,以便能理解整个故事的上下文**。而循环神经网络中的记忆功能恰恰解决了这个“对前文的记忆”功能。
而这种对前文记忆的功能,让循环神经网络特别适合处理自然语言、文本和时间序列这种“新内容依赖于上下文或者历史信息”的数据。
好,明白了这一点,现在我们来看看这节课要解决的问题吧!
## 定义问题
今天这个项目的业务场景非常简单易速鲜花App从2019年上线以来App的日激活数稳步增长。运营团队已经拉出两年以来每天的具体激活数字现在要交给你的任务是建立起一个能够预测未来激活率走势的机器学习模型。
![](https://static001.geekbang.org/resource/image/0e/a8/0ec7c86547ed4a2b4b3280ec09d30ba8.png?wh=196x304)
你可以在[这里](https://www.kaggle.com/tohuangjia/flower-app)下载这个数据集或者就基于这个数据集在Kaggle网站上创建新的Notebook。[代码](https://www.kaggle.com/tohuangjia/rnn-network)呢,我把它发布在[Kaggle](https://www.kaggle.com/tohuangjia/rnn-network)网站你也可以Copy & Edit它到自己的Notebook。
看到这个数据集,你可能会觉得和之前的数据集不太一样了,它的字段特别少,只有两个。现在,我给你几秒钟思考一下,这个数据集的特征和标签分别是什么?你可能会说,标签比较容易找,因为要预测激活数,所以“激活数”就是标签。
你可能还会进一步分析说,从机器学习的类型来看,这里我们有要预测的标签,所以它是一个监督学习问题,而且,标签是连续性的数值,因此它是一个回归问题。
这些都没错,不过,特征在哪里呢?难道说,“日期”字段就是这个数据集的特征吗?这些日期看起来像是普普通通的编号,它既不是有意义的值(比如分数),也不是分类的类别编码(比如男生/女生难道机器学习模型这么厉害输入这样的编号就能够预测出未来的App激活数
而且如果你回忆一下我们之前见过的那些数据集就会发现特征和标签之间会呈现出一种相关性。比如说点赞数、转发数多的时候往往浏览量也多。但是在这个数据集中日期和App的激活数之间是不存在这种相关性的。所以日期并不是一个合适的特征。
不过,在这个数据集中,“日期”仍然是非常重要的信息。让我们一起来回顾一下这个问题的目标:本质上,我们是希望从过去的状态中,找到蛛丝马迹,来预测未来的状态。而过去的状态,肯定是和日期相关联的。
所以我们可以这样来构建这个数据集的特征从当前要预测的日期开始假如是2020年6月30日我们回推60天拥有从2020年5月1日到2020年6月29日这60天的激活数历史数值。那么如果我们把这些历史数值都做为特征输入机器学习。并且我们有理由相信2020年6月30日往后的激活数与前60天的激活数有很强的相关性。根据这样的特征数据机器就可以学习到一条预测激活数的趋势线了。
其实,像这样和日期、时间相关的,并且以连续性数值为特征的数据集,就叫做**时间序列数据**,简称**时序数据**。典型的时序数据包括股票价格、点击数、产品销售数量等等。
那到这里呢,我们就已经把问题定义好了,数据也有了,紧接着下一步就是数据的预处理。
## 收集数据和预处理
### 1\. 数据可视化
我们先把数据集导入程序。其中parse\_dates=\[Date\]是把Date字段以日期格式导入。
```
import numpy as np #导入NumPy
import pandas as pd #导入Pandas
df_app = pd.read_csv('app.csv', index_col='Date', parse_dates=['Date']) #导入数据
df_app #显示数据
```
输出如下:
![](https://static001.geekbang.org/resource/image/9d/e5/9de7486982f923c5567b8e3da706ede5.png?wh=203x377)
对于时间序列数据可视化是相当有必要的。我们用plot这个API绘制出激活数的历史走势图其中时间为横坐标App激活数为纵坐标的图像
```
import matplotlib.pyplot as plt #导入matplotlib.pyplot
plt.style.use('fivethirtyeight') #设定绘图风格
df_app["Activation"].plot(figsize=(12,4),legend=True) #绘制激活数
plt.title('App Activation Count') #图题
plt.show() #绘图
```
这里需要注意的是我通过语句plt.style.use(fivethirtyeight)设定了图像的风格。这种风格是从[538网站](https://fivethirtyeight.com/)(一个对各种事件和趋势进行预测的网站)的画风中移植过来的,很适合展示时序数据。
输出如下:
![](https://static001.geekbang.org/resource/image/8a/b7/8abaafa09017781766e3d5bc7bda85b7.png?wh=790x314)
看得出自APP上线以来日激活数整体呈上升的走势。从函数图像上看也没有任何缺失的数据点。下面我们再来看看这个数据集是否需要做数据清洗工作。
### 2\. 数据清洗
我们可以用下面这个语句看看有没有NaN值
```
df_app.isna().sum() #有NaN吗
```
结果显示数据集中没有NaN值。
对于App激活数我只想保证数据集里全部都是正值所以可以用下面的语句查看有没有负值和零值
```
(df_app.Activation < 0).values.any() #有负值吗?
```
其中values.any()这个API表示只要Orders字段中有任意一个值是负值或者零值就会返回True。
输出如下:
```
False
```
说明整个数据集没有一个0值或者负值。那我们也不需要做任何的清洗工作了。接下来我们进入直接训练集和测试集的拆分。
### 3\. 拆分训练集和测试集
我们假设以2020年10月1日为界只给模型读入2020年10月1日之前的数据之后的数据留作对模型的测试。那么我们就以2020年10月1日为界来拆分训练集和测试集
```
# 按照2020年10月1日为界拆分数据集
Train = df_app[:'2020-09-30'].iloc[:,0:1].values #训练集
Test = df_app['2020-10-01':].iloc[:,0:1].values #测试集
```
对于这段代码,我解释几点:
* df\_app\[:2020-09-30\]代表10月1日之前的数据用于训练模型df\_app\[2020-10-01:\]代表10月1日之后用于对训练好的模型进行测试。
* 代码中的iloc属性是Pandas中对DataFrame对象以行和列位置为索引抽取数据。其中的第一个参数代表行索引指定“:”就表示抽取所有行而第二个参数中的“0:1”代表要抽取的列索引的位置为第2列也就是“激活数”这一列。
* 最后,.values这个属性就把Pandas对象转换成了Numpy数组我们在[上一讲](https://time.geekbang.org/column/article/420372)中说过机器学习中也把Numpy数组称为张量神经网络模型需要Numpy张量类型作为输入。
这时候如果我们显示这个数据对象Train就会发现它已经成为了一个NumPy数组
```
Train #显示训练集对象
```
输出如下:
```
array([[419],
[432],
...
[872],
[875]])
```
对于神经网络来说输入的张量形状非常重要我们要显式指明输入维度绝不能错所以我们要经常用NumPy中的.shape属性查看当前数据对象的形状。
```
print('训练集的形状是:', Train.shape)
print('测试集的形状是:', Test.shape)
```
输出如下:
```
训练集的形状是: (639, 1)
测试集的形状是: (117, 1)
```
目前训练集是639行的一维数组测试集是117行的一维数组但它们都是二阶张量。
为了直观地显示拆分,我们可以把原始数据集按照拆分日期用不同颜色进行显示。
```
# 以不同颜色为训练集和测试集绘图
df_app["Activation"][:'2020-09-30'].plot(figsize=(12,4),legend=True) #训练集
df_app["Activation"]['2020-10-01':].plot(figsize=(12,4),legend=True) #测试集
plt.legend(['Training set (Before October 2020)','Test set (2020 October and beyond)']) #图例
plt.title('App Activation Count') #图题
plt.show() #绘图
```
输出如下:
![](https://static001.geekbang.org/resource/image/fb/fb/fb1fd22eb1a4ac3c8445231c729346fb.png?wh=715x292)
好啦,拆分出训练集和测试集后,下面我们做特征工程。
### 4\. 特征工程
对于神经网络来说,这是一个特别重要的步骤。我们上节课说过,神经网络非常不喜欢数值跨度大的数据,所以,我们对训练特征数据集进行归一化缩放。
```
from sklearn.preprocessing import MinMaxScaler #导入归一化缩放器
Scaler = MinMaxScaler(feature_range=(0,1)) #创建缩放器
Train = Scaler.fit_transform(Train) #拟合缩放器并对训练集进行归一化
```
归一化完成之后,我们来做最后一个数据预处理的步骤:构建特征集和标签集。
### 5\. 构建特征集和标签集
我们前面说过这个数据集的标签就是App激活数特征是时序数据。如果我们要预测今天的App下载数量那时序数据特征的典型构造方法就是把之前30天或者60天的App下载数量作为特征信息被输入机器学习模型。
所以,在下面的这段代码中,我们创建了一个具有 60 个时间步长(所谓步长,就是时间相关的历史特征数据点)和 1 个输出的数据结构。对于训练集的每一行,我们有 60 个之前的App下载数作为特征1个当前训练集元素作为标签。
```
# 创建具有 60 个时间步长和 1 个输出的数据结构 - 训练集
X_train = [] #初始化
y_train = [] #初始化
for i in range(60,Train.size):
X_train.append(Train[i-60:i,0]) #构建特征
y_train.append(Train[i,0]) #构建标签
X_train, y_train = np.array(X_train), np.array(y_train) #转换为NumPy数组
X_train = np.reshape(X_train, (X_train.shape[0],X_train.shape[1],1)) #转换成神经网络所需的张量形状
```
在这个过程中的最后一步我是用NumPy中的reshape方法把特征数据集转换成神经网络所需要的形状的。我之所以强调这一点是因为如果形状不对程序在训练时就会报错这个错误对初学者来说很常见。
这里我显示一下目前X\_train集的形状
```
X_train.shape #X_train的形状
```
输出如下:
```
X_train的形状是 (579, 60, 1)
```
输出显示它是一个三阶张量每一阶上面的维度是579579行数据、60每一个当日激活数往前60天的激活数和1每一个时间点只有激活数一个特征
我们用同样的方法构建测试集:
```
TrainTest = df_app["Activation"][:] #整体数据
inputs = TrainTest[len(TrainTest)-len(Test) - 60:].values #Test加上前60个时间步
inputs = inputs.reshape(-1,1) #转换形状
inputs = Scaler.transform(inputs) #归一化
# 创建具有 60 个时间步长和 1 个输出的数据结构 - 测试集
X_test = [] #初始化
y_test = [] #初始化
for i in range(60,inputs.size):
X_test.append(inputs[i-60:i,0]) #构建特征
y_test.append(inputs[i,0]) #构建标签
X_test = np.array(X_test) #转换为NumPy数组
X_test = np.reshape(X_test, (X_test.shape[0],X_test.shape[1],1)) #转换成神经网络所需的张量形状
```
如果我们显示测试集的形状,会发现它是形状为(117, 60, 1)的张量。
好啦,至此,数据预处理的工作就终于完成了。我们进入到下一个环节:选择算法,创建模型。
## 选择算法并建立模型
前面我已经给你剧透了对于预测App未来激活数的问题我们会选择神经网络中的RNN。其实我们可选的算法还是挺多的比如说普通的回归算法还有像AR自回归、MA滑动平均、ARMA自回归滑动平均、ARIMA差分自回归滑动平均等专门处理时序数据的模型还有Facebook发布的Prophect算法也可以用于时间序列的预测。
当然最为高效、最常用的时序预测模型仍然莫过于深度学习中的RNN了。这也是我们这次实战中会选用RNN模型。那么为什么RNN擅长时序数据的处理呢这里我们就要多说两句它的原理了。
我们知道,循环神经网络是神经网络的一种,不过,和其它类型的神经网络相比,最大的不同是它建立了自身的记忆机制,增加了时间相关的状态信息在各层神经元间的循环传递机制。即一个序列的当前输出与前面各个神经元的输出也是有关的,即隐藏层之间不再是不相连的,而是有连接的。这就让循环神经网络能够更加自由和动态地获取输入的信息,而不受到定长输入空间的限制。
![](https://static001.geekbang.org/resource/image/78/y7/78189a848e83facdf0dc641ec66c0yy7.png?wh=500x275 "循环神经网络建立了与时间状态相关的记忆机制")
因此,向循环神经网络中输入的数据都有这样的特点, 数据集中不仅要包含当前的特征值,还要包括前一刻或者前几刻的状态。
循环神经网络的应用场景正是这种有“上下文历史”的数据比如说我们文章开头说的那个给老王买礼物的例子那其实是在对自然语言处理。再比如视频处理当前帧中发生的事情在很大程度上取决于之前数帧中的内容。另外还有一个典型的应用场景就是我们这里的实战案例循环神经网络要处理的是一个典型的时间序列分析数据集根据前面60天的激活数对未来的数字进行预测。
你会发现,这些场景和数据集,都有一个显著的特点,就是要预测的内容,和之前一段时间的特征值密切相关,就适合选择循环神经网络来建立模型进行处理。
到这里你应该理解了为什么我们的时序数据预测要用RNN来处理现在我们就来考虑怎么搭建RNN。
在Kares中主要有三种循环神经网络层可以搭建循环神经网络分别是Simple RNN、LSTM和GRU那我们该选哪一个呢我们来做个甄别。
Simple RNN顾名思义就是最简单的循环神经网络结构它的结构如下图所示。这个结构比较简单只是在输入特征X的基础之上加入了$h\_{t}$这个时间状态信息,也就是“记忆”功能。
![](https://static001.geekbang.org/resource/image/7b/f0/7b5b14a9f68dfe7ce36d8d981269aef0.jpg?wh=2000x950 "Simple RNN神经元结构")
不过这种结构有一个缺陷,就是会出现“短期记忆的问题”。我们知道,神经网络在训练的过程中,参数是从后面的层向前面的层反向传播的,同时还会在每一步计算梯度,做梯度下降,用于更新神经网络中的权重。
如果前一层对当前层的影响很小那么梯度值就会很小反之亦然。如果前一层的梯度很小那么当前层的梯度会更小。这就使得梯度在我们反向传播时呈指数缩小。而较小的梯度意味着它不会影响权重更新。所以对于Simple RNN来说较早期输入的信息对预测判断的影响会比较小这就是我们前面说的“短期记忆问题”。
对于我们这个项目的数据集来说时间跨度比较大Simple RNN是很难捕捉到这种长期的时间关联的。
不过LSTMLong Short-Term Memory长短期记忆网络可以很好地解决这个问题。LSTM的神经元由一个遗忘门、一个输入门、一个输出门和一个记忆细胞组成来记录额外的信息。记忆细胞负责记住时间相关的信息而三个门负责调节进出神经元的信息流。这个过程中的数学原理我们这里不详述你只需了解在这个过程中每个记忆单元可获得连续的梯度流能学习数百个时间步长的序列而误差保持原值从而解决梯度消失问题。
![](https://static001.geekbang.org/resource/image/0e/a9/0e776737ea64ede63bd0e8ecd7f6e1a9.png?wh=2000x950 "LSTM神经元结构")
所以说LSTM网络可以弥补Simple RNN对较长时期前历史信息相对不敏感的缺陷它也被称为“穿越时空的旅程”它的这种结构非常适合处理时间序列数据。
那么GRU呢它适不适合我们这个项目呢其实GRU也是为了解决Simple RNN的短期记忆问题它的复杂性介于Simple RNN和LSTM之间在结构上要比LSTM简单一些。这里你只需要理解GRU是速度和性能的折衷选择就行。
![](https://static001.geekbang.org/resource/image/cf/9b/cfb58909c2379995800968e4f0084c9b.jpg?wh=2000x950 "GRU神经元结构")
对于预测App激活数走势这个项目来说如果仅从性能角度考虑那LSTM是最理想的所以我们就构建一个以LSTM为主要层结构的循环神经网络
```
from tensorflow.keras.models import Sequential #导入序贯模型
from tensorflow.keras.layers import Dense, LSTM #导入全连接层和LSTM层
# LSTM网络架构
RNN_LSTM = Sequential() #序贯模型
RNN_LSTM.add(LSTM(units=50, return_sequences=True, input_shape=(X_train.shape[1],1))) #输入层LSTM,return_sequences返回输出序列
RNN_LSTM.add(LSTM(units=50, return_sequences=True)) #中间1层LSTMreturn_sequences返回输出序列
RNN_LSTM.add(LSTM(units=50, return_sequences=True)) #中间2层LSTMreturn_sequences返回输出序列
RNN_LSTM.add(LSTM(units=50)) #中间3层LSTM
RNN_LSTM.add(Dense(units=1)) #输出层Dense
# 编译网络
RNN_LSTM.compile(loss='mean_squared_error', #损失函数
optimizer='rmsprop', #优化器
metrics=['mae']) #评估指标
RNN_LSTM.summary() #输出神经网络结构信息
```
这个神经网络的结构如下所示:
![](https://static001.geekbang.org/resource/image/69/d0/69384c44a82fyy05b5abf72a2e6829d0.png?wh=478x397)
到这里呢我们就搭建好了合适的循环神经网络模型搭建的过程和上一讲中搭建CNN的方法非常相似。接下来我们开始做模型训练。
## 训练并评估模型
这里我们训练50次并在训练的同时进行80/20比例的数据验证
```
history = regressor.fit(X_train, y_train, epochs=50, validation_split=0.2) # 训练并保存训练历史信息
```
输出如下:
```
Epoch 1/50
15/15 [==============================] - 11s 258ms/step - loss: 0.0813 - val_loss: 0.0640
Epoch 2/50
15/15 [==============================] - 2s 122ms/step - loss: 0.0101 - val_loss: 0.0511
Epoch 3/50
15/15 [==============================] - 2s 117ms/step - loss: 0.0125 - val_loss: 0.0064
... ....
Epoch 48/50
15/15 [==============================] - 2s 118ms/step - loss: 0.0056 - val_loss: 0.0084
Epoch 49/50
15/15 [==============================] - 2s 121ms/step - loss: 0.0033 - val_loss: 0.0367
Epoch 50/50
15/15 [==============================] - 2s 119ms/step - loss: 0.0036 - val_loss: 0.0151
```
由于这是一个回归值的预测问题,没有分类准确率指标,仅有损失值这个指标,我们就用[上一讲](https://time.geekbang.org/column/article/420372)中说过的损失曲线来显示训练过程中损失值的变化:
![](https://static001.geekbang.org/resource/image/47/e1/479a41e5a4bba451f1c7a65a246bb5e1.png?wh=376x283)
可以看到训练50轮之后训练集上的损失已经很小了。不过测试集上面的损失存在着振荡上升现象这也是过拟合的一个标志。
## 利用模型进行预测
好啦现在模型有了评估也结束了现在我们就来到了最令人激动的环节让我们用模型预测一下测试集也就是代表着未来的激活数吧。不过在预测结束之后需要用inverse\_transform对预测值做反归一化。否则激活数将是一个0-1之间的值。
```
predicted_stock_price = regressor.predict(X_test) #预测
predicted_stock_price = sc.inverse_transform(predicted_stock_price) #反归一化
plot_predictions(test_set,predicted_stock_price) #绘图
```
输出如下:
![](https://static001.geekbang.org/resource/image/a9/fa/a9b4e7e27d297fd33fa85f5caea67afa.png?wh=437x300)
看得出来我们的回归曲线和实际曲线的走势是相当接近的。不过这个模型是否有进一步优化的可能这个问题我们下一讲继续探讨。那到这里我们的App激活率走势预测的任务就顺利结束了。
## 总结一下
现在,我们来回顾一下重点内容。
在这一讲中, 我们介绍了时间序列数据以及善于处理时间序列数据集的循环神经网络算法RNN。RNN和其它类型的神经网络相比它的特点是建立了自身的记忆机制善于根据历史信息预测后续走势。通过Keras中的API我们轻松搭建起了一个循环神经网络对数据集进行App激活数的预测而这个神经网络中最主要的层是LSTM层。
还有一点请你注意要通过历史数据预测未来是非常有难度的事情所以我们并不能够单纯的依赖于模型给出的结果。在实际情况中影响未来的因子有很多就拿股票的价格为例一个预测系统不仅仅要有历史股价还要结合当前市场的整体行情、新闻信息、公司财报等多方面的数据来建立模型才较为完善。但即使如此我们仍然是很难预测走势的。像我们的App激活数的预测会比股票价格预测简单一些那也需要更多的产品、运营等更多信息把这些信息和历史数据同时传递给模型才能够训练出更有意义的模型。
## 思考题
这节课就到这里了,我给你留一个思考题:
我们说在Keras中一共有三种主要的循环神经网络层分别是SimpleRNN、GRU和LSTM我们今天使用了其中最强大的LSTM层请你试着用其它两种解决今天的问题。
欢迎你在留言区和我分享你的观点,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲再见!
![](https://static001.geekbang.org/resource/image/d5/40/d5f7c3591c99e0fd04997ef76738e940.jpg?wh=2284x1280)