运用PyTorch动手搭建一个共享单车预测器

本文摘自 《深度学习原理与PyTorch实战》算法

咱们将从预测某地的共享单车数量这个实际问题出发,带领读者走进神经网络的殿堂,运用PyTorch动手搭建一个共享单车预测器,在实战过程当中掌握神经元、神经网络、激活函数、机器学习等基本概念,以及数据预处理的方法。此外,还会揭秘神经网络这个“黑箱”,看看它如何工做,哪一个神经元起到了关键做用,从而让读者对神经网络的运做原理有更深刻的了解。数据库

3.1 共享单车的烦恼

大约从2016年起,咱们的身边出现了不少共享单车。五光十色、各式各样的共享单车就像炸开花了同样,遍及城市的大街小巷。api

共享单车在给人们带来便利的同时,也存在一个麻烦的问题:单车的分布很不均匀。好比在早高峰的时候,一些地铁口每每汇集着大量的单车,而到了晚高峰却很难找到一辆单车了,这就给须要使用共享单车的人形成了不便。数组

那么如何解决共享单车分布不均匀的问题呢?目前的方式是,共享单车公司会雇用一些工人来搬运单车,把它们运送到须要单车的区域。但问题是应该运多少单车?何时运?运到什么地方呢?这就须要准确地知道共享单车在整个城市不一样地点的数量分布状况,并且须要提早作出安排,由于工人运送单车还有必定的延迟性。这对于共享单车公司来讲是一个很是严峻的挑战。bash

为了更加科学有效地解决这个问题,咱们须要构造一个单车数量的预测器,用来预测某一时间、某一停放区域的单车数量,供共享单车公司参考,以实现对单车的合理投放。网络

巧妇难为无米之炊。要构建这样的单车预测器,就须要必定的共享单车数据。为了不商业纠纷,也为了让本书的开发和讲解更方便,本例将会使用一个国外的共享单车公开数据集(Capital Bikeshare)来完成咱们的任务,数据集下载连接:www.capitalbikeshare.com/ system-data。架构

下载数据集以后,咱们能够用通常的表处理软件或者文本编辑器直接打开,如图3.1所示。app

图像说明文字

该数据是从2011年1月1日到2012年12月31日之间某地的单车使用状况,每一行都表明一条数据记录,共17 379条。一条数据记录了一个小时内某一个地点的星期几、是不是假期、天气和风速等状况,以及该地区的单车使用量(用cnt变量记载),它是咱们最关心的量。机器学习

咱们能够截取一段时间的数据,将cnt随时间的变化关系绘制成图。图3.2是2011年1月1日到1月10日的数据。横坐标是时间,纵坐标是单车的数量。单车数量随时间波动,而且呈现必定的规律性。不难看出,工做日的单车数量高峰远高于周末的。编辑器

图像说明文字

咱们要解决的问题就是,可否根据历史数据预测接下来一段时间该地区单车数量的走势状况呢?在本章中,咱们将学习如何设计神经网络模型来预测单车数量。对于这一问题,咱们并非一会儿提供一套完美的解决方案,而是经过按部就班的方式,尝试不一样的解决方案。结合这一问题,咱们将主要讲解什么是人工神经元、什么是神经网络、如何根据须要搭建一个神经网络,以及什么是过拟合,如何解决过拟合问题,等等。除此以外,咱们还将学到如何对一个神经网络进行解剖,从而理解其工做原理以及与数据的对应。

3.2 单车预测器1.0

本节将作出一个单车预测器,它是一个单一隐含单元的神经网络。咱们将训练它学会拟合共享单车的波动曲线。

不过,在设计单车预测器以前,咱们有必要了解一下人工神经网络的概念和工做原理。

3.2.1 人工神经网络简介

人工神经网络(简称神经网络)是一种受人脑的生物神经网络启发而设计的计算模型。人工神经网络很是擅长从输入的数据和标签中学习到映射关系,从而完成预测或者解决分类问题。人工神经网络也被称为通用拟合器,这是由于它能够拟合任意的函数或映射。

前馈神经网络是咱们最经常使用的一种网络,它通常包括3层人工神经单元,即输入层、隐含层和输出层,如图3.3所示。其中,隐含层能够包含多层,这就构成了所谓的深度神经网络。

图像说明文字

图中的每个圆圈表明一我的工神经元,连线表明人工突触,它将两个神经元联系了起来。每条连边上都包含一个数值,叫做权重,咱们一般用w来表示。

神经网络的运行一般包含前馈的预测过程(或称为决策过程)和反馈的学习过程。

在前馈的预测过程当中,信号从输入单元输入,并沿着网络连边传输,每一个信号会与连边上的权重进行乘积,从而获得隐含层单元的输入;接下来,隐含层单元对全部连边输入的信号进行汇总(求和),而后通过必定的处理(具体处理过程将在下节讲述)进行输出;这些输出的信号再乘以从隐含层到输出的那组连线上的权重,从而获得输入给输出单元的信号;最后,输出单元再对每一条输入连边的信号进行汇总,并进行加工处理再输出。最后的输出就是整个神经网络的输出。神经网络在训练阶段将会调节每条连边上的权重w数值。

在反馈的学习过程当中,每一个输出神经元会首先计算出它的预测偏差,而后将这个偏差沿着网络的全部连边进行反向传播,获得每一个隐含层节点的偏差。最后,根据每条连边所连通的两个节点的偏差计算连边上的权重更新量,从而完成网络的学习与调整。

下面,咱们就从人工神经元开始详细讲述神经网络的工做过程。

3.2.2 人工神经元

人工神经网络相似于生物神经网络,由人工神经元(简称神经元)构成。神经元用简单的数学模型来模拟生物神经细胞的信号传递与激活。为了理解人工神经网络的运做原理,咱们先来看一个最简单的情形:单神经元模型。如图3.4所示,它只有一个输入层单元、一个隐含层单元和一个输出层单元。

图像说明文字

x表示输入的数据,y表示输出的数据,它们都是实数。从输入单元到隐含层的权重w、隐含层单元偏置b、隐含层到输出层的权重w'都是能够任意取值的实数。

咱们能够将这个最简单的神经网络当作一个从x映射到y的函数,而w、b和w'是该函数的参数。该函数的方程如图3.5中的方程式所示,其中σ表示sigmoid函数。当w=1,w'=1,b=0的时候,这个函数的图形如图3.5所示。

图像说明文字

这就是sigmoid函数的形状及σ(x)的数学表达式。经过观察该曲线,咱们不难发现,当x小于0的时候,σ(x)都是小于1/2的,并且x越小,σ(x)越接近于0;当x大于0的时候,σ(x)都是大于1/2的,并且x越大,σ(x)越接近于1。在x=0的点附近存在着一个从0到1的突变。

当咱们变换w、b和w'这些参数的时候,函数的图形也会发生相应的改变。例如,咱们不妨保持 w'=1, b=0不变,而变换w的大小,其函数图形的变化如图3.6所示。

图像说明文字

因而可知,当w>0的时候,它的大小控制着函数的弯曲程度,w越大,它在0点附近的弯曲程度就会越大,所以从x=0的突变也就越剧烈;当w<0的时候,曲线发生了左右翻转,它会从1突变到0。

再来看看参数b对曲线的影响,保持w=w'=1不变,如图3.7所示。

图像说明文字

能够清晰地看到,b控制着sigmoid函数曲线的水平位置。b>0,函数图形往左平移;反之往右平移。最后,让咱们看看w'如何影响该曲线,如图3.8所示。

图像说明文字

不难看出,当w' > 0的时候,w'控制着曲线的高矮;当w' < 0的时候,曲线的方向发生上下颠倒。

可见,经过控制w、w'和b这3个参数,咱们能够任意调节从输入x到输出y的函数形状。可是,不管如何调节,这条曲线永远都是S形(包括倒S形)的。要想获得更加复杂的函数图像,咱们须要引入更多的神经元。

3.2.3 两个隐含层神经元

下面咱们把模型作得更复杂一些,看看两个隐含层神经元会对曲线有什么影响,如图3.9所示。

图像说明文字

输入信号进入网络以后就会兵分两路,一路从左侧进入第一个神经元,另外一路从右侧进入第二个神经元。这两个神经元分别完成计算,并经过w'1和w'2进行加权求和获得y。因此,输出y实际上就是两个神经元的叠加。这个网络仍然是一个将x映射到y的函数,函数方程为:

图像说明文字

在这个公式中,有w1, w2, w'1, w'2, b1, b2这样6个不一样的参数。它们的组合也会对曲线的形状有影响。

例如,咱们能够取w1=w2=w'1=w'2=1,b1=-1,b2=0,则该函数的曲线形状如图3.10所示。

图像说明文字

因而可知,合成的函数图形变为了一个具备两个阶梯的曲线。

让咱们再来看一个参数组合,w1=w2=1,b1=0,b2=-1,w'1=1,w'2=-1,则函数图形如图3.11所示。

图像说明文字

因而可知,咱们合成了一个具备单一波峰的曲线,有点相似于正态分布的钟形曲线。通常地,只要变换参数组合,咱们就能够用两个隐含层神经元拟合出任意具备单峰的曲线。

那么,若是有4个或者6个甚至更多的隐含层神经元,不难想象,就能够获得具备双峰、三峰和任意多个峰的曲线,咱们能够粗略地认为两个神经元能够用来逼近一个波峰(波谷)。事实上,对于更通常的情形,科学家早已从理论上证实,用有限多的隐含层神经元能够逼近任意的有限区间内的曲线,这叫做通用逼近定理(universal approximation theorem)。

3.2.4 训练与运行

在前面的讨论中,咱们看到,只要可以调节神经网络中各个参数的组合,就能获得任意想要的曲线。可问题是,咱们应该如何选取这些参数呢?答案就在于训练。

要想完成神经网络的训练,首先要给这个神经网络定义一个损失函数,用来衡量网络在现有的参数组合下输出表现的好坏。这就相似于第2章利用线性回归预测房价中的总偏差函数(即拟合直线与全部点距离的平方和)L。一样地,在单车预测的例子中,咱们也能够将损失函数定义为对于全部的数据样本,神经网络预测的单车数量与实际数据中单车数量之差的平方和的均值,即:

图像说明文字

这里,N为样本总量,

图像说明文字
为神经网络计算得来的预测单车数,
图像说明文字
为实际数据中该时刻该地区的单车数。

有了这个损失函数L,咱们就有了调整神经网络参数的方向——尽量地让L最小化。所以,神经网络要学习的就是神经元之间连边上的权重及偏置,学习的目的是获得一组可以使总偏差最小的参数值组合。

这是一个求极值的优化问题,高等数学告诉咱们,只须要令导数为零就能够求得。然而,因为神经网络通常很是复杂,包含大量非线性运算,直接用数学求导数的方法行不通,因此,咱们通常使用数值的方式来进行求解,也就是梯度降低算法。每次迭代都向梯度的负方向前进,使得偏差值逐步减少。参数的更新要用到反向传播算法,将损失函数L沿着网络一层一层地反向传播,来修正每一层的参数。咱们在这里不会详细介绍反向传播算法,由于PyTorch已经自动将这个复杂的算法变成了一个简单的命令:backward。只要调用该命令,PyTorch就会自动执行反向传播算法,计算出每个参数的梯度,咱们只须要根据这些梯度更新参数,就能够完成一步学习。

神经网络的学习和运行一般是交替进行的。也就是说,在每个周期,神经网络都会进行前馈运算,从输入端运算到输出端;而后,根据输出端的损失值来进行反向传播算法,从而调整神经网络上的各个参数。不停地重复这两个步骤,就能够令神经网络学习得愈来愈好。

3.2.5 失败的神经预测器

在弄清楚了神经网络的工做原理以后,下面咱们来看看如何用神经网络预测共享单车的曲线。咱们但愿仿照预测房价的作法,利用人工神经网络来拟合一个时间段内的单车曲线,并给出在将来时间点单车使用量的曲线。

为了让演示更加简单清晰,咱们仅选择了数据中的前50条记录,绘制成如图3.12所示的曲线。在这条曲线中,横坐标是数据记录的编号,纵坐标则是对应的单车数量。

图像说明文字

接下来,咱们就要设计一个神经网络,它的输入x就是数据编号,输出则是对应的单车数量。经过观察这条曲线,咱们发现它至少有3个峰,采用10个隐含层单元就足以保证拟合这条曲线了。所以,咱们的人工神经网络架构如图3.13所示。

图像说明文字

接下来,咱们就要动手写程序实现这个网络。首先导入本程序所使用的全部依赖库。这里咱们会用到pandas库来读取和操做数据。读者须要先安装这个程序包,在Anaconda环境下运行conda install pandas便可。

import numpy as np
import pandas as pd  #读取csv文件的库
import torch
from torch.autograd import Variable
import torch.optim as optim
import matplotlib.pyplot as plt
#让输出图形直接在Notebook中显示
%matplotlib inline
复制代码

接着,要从硬盘文件中导入想要的数据。

data_path = 'hour.csv'  #读取数据到内存,rides为一个dataframe对象
rides = pd.read_csv(data_path)
rides.head()  #输出部分数据
counts = rides['cnt'][:50]  #截取数据
x = np.arange(len(counts))  #获取变量x
y = np.array(counts) #单车数量为y
plt.figure(figsize = (10, 7)) #设定绘图窗口大小
plt.plot(x, y, 'o-')  #绘制原始数据
plt.xlabel('X')  #更改坐标轴标注
plt.ylabel('Y')  #更改坐标轴标注
复制代码

在这里,咱们使用了pandas库,从csv文件中快速导入数据存储到rides里面。rides能够按照二维表的形式存储数据,并能够像访问数组同样对其进行访问和操做。rides.head()的做用是打印输出部分数据记录。

以后,咱们从rides的全部记录中选出前50条,并只筛选出了cnt字段放入counts数组中。这个数组就存储了前50条自行车使用数量记录。接着,咱们将前50条记录的图画出来,即图3.13所示的效果。

准备好了数据,咱们就能够用PyTorch来搭建人工神经网络了。与第2章的线性回归例子相似,咱们首先须要定义一系列的变量,包括全部连边的权重和偏置,并经过这些变量的运算让PyTorch自动生成计算图。

#输入变量,1,2,3,...这样的一维数组
x = Variable(torch.FloatTensor(np.arange(len(counts), dtype = float))) 
#输出变量,它是从数据counts中读取的每一时刻的单车数,共50个数据点的一维数组,做为标准答案
y = Variable(torch.FloatTensor(np.array(counts, dtype = float))) 

sz = 10  #设置隐含层神经元的数量
#初始化输入层到隐含层的权重矩阵,它的尺寸是(1,10)
weights = Variable(torch.randn(1, sz), requires_grad = True)  
#初始化隐含层节点的偏置向量,它是尺寸为10的一维向量
biases = Variable(torch.randn(sz), requires_grad = True)  
#初始化从隐含层到输出层的权重矩阵,它的尺寸是(10,1)
weights2 = Variable(torch.randn(sz, 1), requires_grad = True)  
复制代码

设置好变量和神经网络的初始参数,接下来就要迭代地训练这个神经网络了。

learning_rate = 0.0001 #设置学习率
losses = [] #该数组记录每一次迭代的损失函数值,以方便后续绘图
for i in range(1000000):
    #从输入层到隐含层的计算
    hidden = x.expand(sz, len(x)).t() * weights.expand(len(x), sz) + biases.expand(len(x), sz)
    #此时,hidden变量的尺寸是:(50,10),即50个数据点,10个隐含层神经元

    #将sigmoid函数做用在隐含层的每个神经元上
    hidden = torch.sigmoid(hidden)
    #隐含层输出到输出层,计算获得最终预测
    predictions = hidden.mm(weights2)
    #此时,predictions的尺寸为:(50,1),即50个数据点的预测数值
    #经过与数据中的标准答案y作比较,计算均方偏差
    loss = torch.mean((predictions - y) ** 2) 
    #此时,loss为一个标量,即一个数
    losses.append(loss.data.numpy())

    if i % 10000 == 0: #每隔10000个周期打印一下损失函数数值
        print('loss:', loss)

    #*****************************************
    #接下来开始梯度降低算法,将偏差反向传播
    loss.backward()  #对损失函数进行梯度反传

    #利用上一步计算中获得的weights,biases等梯度信息更新weights或biases的数值
    weights.data.add_(- learning_rate * weights.grad.data)  
    biases.data.add_(- learning_rate * biases.grad.data)
    weights2.data.add_(- learning_rate * weights2.grad.data)

    #清空全部变量的梯度值
    weights.grad.data.zero_()
    biases.grad.data.zero_()
    weights2.grad.data.zero_()
复制代码

在上面这段代码中,咱们进行了100 000步训练迭代。在每一次迭代中,咱们都将50个数据点的x做为数组所有输入神经网络,并让神经网络按照从输入层到隐含层、再从隐含层到输出层的步骤,一步步完成计算,最终输出对50个数据点的预测数组prediction。

以后,计算prediction和标准答案y之间的偏差,并计算出全部50个数据点的平均偏差值loss,这就是咱们前面提到的损失函数L。接着,调用loss.backward()完成偏差顺着神经网络的反向传播过程,从而计算出计算图上每个叶节点的梯度更新数值,并记录在每一个变量的.grad属性中。最后,咱们用这个梯度数值来更新每一个参数的数值,从而完成了一步迭代。

仔细对比这段代码和第2章中的线性回归代码就会发现,除了中间的运算过程和损失函数有所不一样外,其余的操做所有相同。事实上,在本书中,几乎全部的机器学习案例都采用了这样的步骤,即前馈运算、反向传播计算梯度、根据梯度更新参数数值。

咱们能够打印出Loss随着一步步的迭代降低的曲线,这能够帮助咱们直观地看到神经网络训练的过程,如图3.14所示。

plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
复制代码

图像说明文字

由该曲线能够看出,随着时间的推移,神经网络预测的偏差的确在一步步减少。并且,大约到20 000步后,偏差基本就不会呈现明显的降低了。

接下来,咱们能够把训练好的网络在这50个数据点上的预测曲线绘制出来,并与标准答案y进行对比,代码以下:

x_data = x.data.numpy()  #得到x包裹的数据
plt.figure(figsize = (10, 7))  #设定绘图窗口大小
xplot, = plt.plot(x_data, y.data.numpy(), 'o')  #绘制原始数据
yplot, = plt.plot(x_data, predictions.data.numpy())  #绘制拟合数据
plt.xlabel('X')  #更改坐标轴标注
plt.ylabel('Y')  #更改坐标轴标注
plt.legend([xplot, yplot],['Data', 'Prediction under 1000000 epochs'])  #绘制图例
plt.show()
复制代码

最后的可视化图形如图3.15所示。

图像说明文字

能够看到,咱们的预测曲线在第一个波峰比较好地拟合了数据,可是在此后,它却与真实数据相差甚远。这是为何呢?

咱们知道,x的取值范围是1~50,而全部权重和偏置的初始值都是被设定在(-1, 1)的正态分布随机数,那么输入层到隐含层节点的数值范围就成了50~50,要想将sigmoid函数的多个峰值调节到咱们指望的位置须要耗费不少计算时间。事实上,若是让训练时间更长些,咱们能够将曲线后面的部分拟合得很好。

这个问题的解决方法是将输入数据的范围作归一化处理,也就是让x的输入数值范围为0~1。由于数据中x的范围是1~50,因此,咱们只须要将每个数值都除以50就能够了:

x = Variable(torch.FloatTensor(np.arange(len(counts), dtype = float) / len(counts)))
复制代码

该操做会使x的取值范围变为0.02, 0.04, …, 1。作了这些改进后再来运行程序,能够看到此次训练速度明显加快,可视化后的拟合效果也更好了,如图3.16所示。

图像说明文字

咱们看到,改进后的模型出现了两个波峰,也很是好地拟合了这些数据点,造成一条优美的曲线。

接下来,咱们就须要用训练好的模型来作预测了。咱们的预测任务是后面50条数据的单车数量。此时的x取值是51, 52, …, 100,一样也要除以50。

counts_predict = rides['cnt'][50:100]  #读取待预测的后面50个数据点
x = Variable(torch.FloatTensor((np.arange(len(counts_predict), dtype = float) + len(counts)) / len(counts)))
#读取后面50个点的y数值,不须要作归一化
y = Variable(torch.FloatTensor(np.array(counts_predict, dtype = float)))  

#用x预测y
hidden = x.expand(sz, len(x)).t() * weights.expand(len(x), sz)  #从输入层到隐含层的计算
hidden = torch.sigmoid(hidden)  #将sigmoid函数做用在隐含层的每个神经元上
predictions = hidden.mm(weights2)  #从隐含层输出到输出层,计算获得最终预测
loss = torch.mean((predictions - y) ** 2)  #计算预测数据上的损失函数
print(loss)

#将预测曲线绘制出来
x_data = x.data.numpy()  #得到x包裹的数据
plt.figure(figsize = (10, 7)) #设定绘图窗口大小
xplot, = plt.plot(x_data, y.data.numpy(), 'o') #绘制原始数据
yplot, = plt.plot(x_data, predictions.data.numpy())  #绘制拟合数据
plt.xlabel('X')  #更改坐标轴标注
plt.ylabel('Y')  #更改坐标轴标注
plt.legend([xplot, yplot],['Data', 'Prediction'])  #绘制图例
plt.show()
复制代码

最终,咱们获得了如图3.17所示的曲线。直线是咱们的模型给出的预测曲线,圆点是实际数据所对应的曲线。模型预测与实际数据居然彻底对不上!

图像说明文字

为何咱们的神经网络能够很是好地拟合已知的50个数据点,却彻底不能预测出更多的数据点呢?缘由就在于:过拟合。

3.2.6 过拟合

所谓过拟合(over fitting)现象就是指模型能够在训练数据上进行很是好的预测,但在全新的测试数据中却得不到好的表现。在这个例子中,训练数据就是前50个数据点,测试数据就是后面的50个数据点。咱们的模型能够经过调节参数顺利地拟合训练数据的曲线,可是这种刻意适合彻底没有推广价值,致使这条拟合曲线与测试数据的标准答案相差甚远。咱们的神经网络模型并无学习到数据中的模式。

那咱们的神经网络为何不能学习到曲线中的模式呢?缘由就在于咱们选择了错误的特征变量:咱们尝试用数据的下标(1, 2, 3, …)或者它的归一化(0.1, 0.2, …)来对y进行预测。然而曲线的波动模式(也就是单车的使用数量)显然并不依赖于下标,而是依赖于诸如天气、风速、星期几和是否节假日等因素。然而,咱们无论三七二十一,硬要用强大的人工神经网络来拟合整条曲线,这天然就致使了过拟合的现象,并且是很是严重的过拟合。

由这个例子能够看出,一味地追求人工智能技术,而不考虑实际问题的背景,很容易让咱们走弯路。当咱们面对大数据时,数据背后的意义每每能够指导咱们更加快速地找到分析大数据的捷径。

在这一节中,咱们虽然费了半天劲也没有真正地解决问题,可是仍然学到了很多知识,包括神经网络的工做原理、如何根据问题的复杂度选择隐含层的数量,以及如何调整数据让训练速度更快。更重要的是,咱们从血淋淋的教训中领教了什么叫做过拟合。

3.3 单车预测器2.0

接下来,就让咱们踏上正确解决问题的康庄大道。既然咱们猜想到利用天气、风速、星期几、是不是节假日等信息能够更好地预测单车使用数量,并且咱们的原始数据中就包含了这些信息,那么咱们不妨从新设计一个神经网络,把这些相关信息都输入进去,从而预测单车的数量。

3.3.1 数据的预处理过程

然而,在咱们动手设计神经网络以前,最好仍是再认真了解一下数据,由于加强对数据的了解会起到更重要的做用。

深刻观察图3.2中的数据,咱们发现,全部的变量能够分红两种:一种是类型变量,另外一种是数值变量。

所谓的类型变量就是指这个变量能够在几种不一样的类别中取值,例如星期(week)这个变量就有1, 2, 3, …, 0这几种类型,分别表明星期1、星期2、星期三……星期日这几天。而天气状况(weathersit)这个变量能够从1~4中取值。其中,1表示晴天,2表示多云,3表示小雨/雪,4表示大雨/雪。

另外一种类型就是数值类型,这种变量会从一个数值区间中连续取值。例如,湿度(humidity)就是一个从[0, 1]区间中连续取值的变量。温度、风速也都是这种类型的变量。

咱们不能将不一样类型的变量不加任何处理地输入神经网络,由于不一样的数值表明彻底不一样的含义。在类型变量中,数字的大小实际上没有任何意义。好比数字5比数字1大,但这并不表明周五会比周一更特殊。除此以外,不一样的数值类型变量的变化范围也都不同。若是直接把它们混合在一块儿,势必会形成没必要要的麻烦。综合以上考虑,咱们须要对两种变量分别进行预处理。

1. 类型变量的独热编码

类型变量的大小没有任何含义,只是为了区分不一样的类型而已。好比季节这个变量能够等于一、二、三、4,即四季,数字仅仅是对它们的区分。咱们不能将season变量直接输入神经网络,由于season数值并不表示相应的信号强度。咱们的解决方案是将类型变量转化为“独热编码”(one-hot),如表3.1所示。

图像说明文字
采用这种编码后,不一样的数值就转变为了避免同的向量,这些向量的长度都是4,而只有一个位置为1,其余位置都是0。1表明激活,因而独热编码的向量就对应了不一样的激活模式。这样的数据更容易被神经网络处理。更通常地,若是一个类型变量有n个不一样的取值,那么咱们的独热编码所对应的向量长度就为n。

接下来,咱们只须要在数据中将某一列类型变量转化为多个列的独热编码向量,就能够完成这种变量的预处理过程了,如图3.18所示。

图像说明文字

所以,原来的weekday这个属性就转变为7个不一样的属性,数据库一下就增长了6列。

在程序上,pandas能够很容易实现上面的操做,代码以下:

dummy_fields = ['season', 'weathersit', 'mnth', 'hr', 'weekday'] #全部类型编码变量的名称
for each in dummy_fields:
    #取出全部类型变量,并将它们转变为独热编码
    dummies = pd.get_dummies(rides[each], prefix=each, drop_first=False)
    #将新的独热编码变量与原有的全部变量合并到一块儿
    rides = pd.concat([rides, dummies], axis=1)

#将原来的类型变量从数据表中删除
fields_to_drop = ['instant', 'dteday', 'season', 'weathersit', 'weekday', 'atemp', 'mnth', 'workingday', 
    'hr'] #要删除的类型变量的名称
data = rides.drop(fields_to_drop, axis=1) #将它们从数据库的变量中删除
复制代码

通过这一番处理以后,本来只有17列的数据一会儿变为了59列,部分数据片断如图3.19所示。

图像说明文字

** 2. 数值类型变量的处理**

数值类型变量的问题在于每一个变量的变化范围都不同,单位也不同,所以不一样的变量就不能进行比较。咱们采起的解决方法就是对这种变量进行标准化处理,也就是用变量的均值和标准差来对该变量作标准化,从而都转变为[-1, 1]区间内波动的数值。好比,对于温度temp这个变量来讲,它在整个数据库中取值的平均值为mean(temp),方差为std(temp),那么,归一化的温度计算为:

图像说明文字

temp'是一个位于[-1, 1]区间的数。这样作的好处就是能够将不一样取值范围的变量设置为处于平等的地位。

咱们能够用如下代码来实现这些变量的标准化处理:

quant_features = ['cnt', 'temp', 'hum', 'windspeed'] #数值类型变量的名称
scaled_features = {}  #将每个变量的均值和方差都存储到scaled_features变量中
for each in quant_features:
    #计算这些变量的均值和方差
    mean, std = data[each].mean(), data[each].std()
    scaled_features[each] = [mean, std]
    #对每个变量进行归一化
    data.loc[:, each] = (data[each] - mean)/std
复制代码

** 3. 数据集的划分**

预处理作完之后,咱们的数据集包含了17 379条记录、59个变量。接下来,咱们将对这个数据集进行划分。

首先,在变量集合上,咱们分为了特征和目标两个集合。其中,特征变量集合包括:年份(yr)、是否节假日(holiday)、温度(temp)、湿度(hum)、风速(windspeed)、季节1~4(season)、天气1~4(weathersit,不一样天气种类)、月份1~12(mnth)、小时0~23(hr)和星期0~6(weekday),它们是输入给神经网络的变量。目标变量包括:用户数(cnt)、临时用户数(casual),以及注册用户数(registered)。其中咱们仅仅将cnt做为目标变量,另外两个暂时不作任何处理。咱们将利用56个特征变量做为神经网络的输入,来预测1个变量做为神经网络的输出。

接下来,咱们再将17 379条记录划分为两个集合:前16 875条记录做为训练集,用来训练咱们的神经网络;后21天的数据(504条记录)做为测试集,用来检验模型的预测效果。这一部分数据是不参与神经网络训练的,如图3.20所示。

图像说明文字

数据处理代码以下:

test_data = data[-21*24:] #选出训练集
train_data = data[:-21*24] #选出测试集

#目标列包含的字段
target_fields = ['cnt','casual', 'registered'] 

#训练集划分红特征变量列和目标特征列
features, targets = train_data.drop(target_fields, axis=1), train_data[target_fields]

#测试集划分红特征变量列和目标特征列
test_features, test_targets = test_data.drop(target_fields, axis=1), test_data[target_fields]

#将数据类型转换为NumPy数组
X = features.values  #将数据从pandas dataframe转换为NumPy
Y = targets['cnt'].values
Y = Y.astype(float)

Y = np.reshape(Y, [len(Y),1])
losses = []
复制代码

3.3.2 构建神经网络

在数据处理完毕后,咱们将构建新的人工神经网络。这个网络有3层:输入层、隐含层和输出层。每一个层的尺寸(神经元个数)分别是5六、10和1(如图3.21所示)。其中,输入层和输出层的神经元个数分别由数据决定,隐含层神经元个数则根据咱们对数据复杂度的预估决定。一般,数据越复杂,数据量越大,就须要越多的神经元。可是神经元过多容易形成过拟合。

图像说明文字

除了前面讲的用手工实现神经网络的张量计算完成神经网络搭建之外,PyTorch还实现了自动调用现成的函数来完成一样的操做,这样的代码更加简洁,以下所示:

#定义神经网络架构,features.shape[1]个输入层单元,10个隐含层,1个输出层
input_size = features.shape[1]
hidden_size = 10
output_size = 1
batch_size = 128
neu = torch.nn.Sequential(
    torch.nn.Linear(input_size, hidden_size),
    torch.nn.Sigmoid(),
    torch.nn.Linear(hidden_size, output_size),
)
复制代码

在这段代码里,咱们能够调用torch.nn.Sequential()来构造神经网络,并存放到neu变量中。torch.nn.Sequential()这个函数的做用是将一系列的运算模块按顺序搭建成一个多层的神经网络。在本例中,这些模块包括从输入层到隐含层的线性映射Linear(input_size, hidden_size)、隐含层的非线性sigmoid函数torch.nn.Sigmoid(),以及从隐含层到输出层的线性映射torch.nn.Linear(hidden_size, output_size)。值得注意的是,Sequential里面的层次并不与神经网络的层次严格对应,而是指多步的运算,它与动态计算图的层次相对应。

咱们也可使用PyTorch自带的损失函数:

cost = torch.nn.MSELoss()
复制代码

这是PyTorch自带的一个封装好的计算均方偏差的损失函数,它是一个函数指针,赋予了变量cost。在计算的时候,咱们只须要调用cost(x,y)就能够计算预测向量x和目标向量y之间的均方偏差。

除此以外,PyTorch还自带了优化器来自动实现优化算法:

optimizer = torch.optim.SGD(neu.parameters(), lr = 0.01)
复制代码

torch.optim.SGD()调用了PyTorch自带的随机梯度降低算法(stochastic gradient descent,SGD)做为优化器。在初始化optimizer的时候,咱们须要待优化的全部参数(在本例中,传入的参数包括神经网络neu包含的全部权重和偏置,即neu.parameters()),以及执行梯度降低算法的学习率lr=0.01。在一切材料都准备好以后,咱们即可以实施训练了。

数据的分批处理

然而,在进行训练循环的时候,咱们还会遇到一个问题。在前面的例子中,在每个训练周期,咱们都将全部的数据一股脑地儿输入神经网络。这在数据量不大的状况下没有任何问题。可是,如今的数据量是16 875条,在这么大数据量的状况下,若是在每一个训练周期都处理全部数据,则会出现运算速度过慢、迭代可能不收敛等问题。

解决方法一般是采起批处理(batch processing)的模式,也就是将全部的数据记录划分红一个批次大小(batch size)的小数据集,而后在每一个训练周期给神经网络输入一批数据,如图3.22所示。批量的大小依问题的复杂度和数据量的大小而定,在本例中,咱们设定batch_size=128。

图像说明文字

采用分批处理后的训练代码以下:

#神经网络训练循环
losses = []
for i in range(1000):
    #每128个样本点被划分为一批,在循环的时候一批一批地读取
    batch_loss = []
    #start和end分别是提取一批数据的起始和终止下标
    for start in range(0, len(X), batch_size):
        end = start + batch_size if start + batch_size < len(X) else len(X)
        xx = Variable(torch.FloatTensor(X[start:end]))
        yy = Variable(torch.FloatTensor(Y[start:end]))
        predict = neu(xx)
        loss = cost(predict, yy)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        batch_loss.append(loss.data.numpy())

    #每隔100步输出损失值
    if i % 100==0:
        losses.append(np.mean(batch_loss))
        print(i, np.mean(batch_loss))

#打印输出损失值
plt.plot(np.arange(len(losses))*100,losses)
plt.xlabel('epoch')
plt.ylabel('MSE')
复制代码

运行这段程序,咱们即可以训练这个神经网络了。图3.23展现的是随着训练周期的运行,损失函数的降低状况。其中,横坐标表示训练周期,纵坐标表示平均偏差。能够看到,平均偏差随训练周期快速降低。

图像说明文字

3.3.3 测试神经网络

接下来,咱们即可以用训练好的神经网络在测试集上进行预测,而且将后21天的预测数据与真实数据画在一块儿进行比较。

targets = test_targets['cnt']  #读取测试集的cnt数值
targets = targets.values.reshape([len(targets),1])  #将数据转换成合适的tensor形式
targets = targets.astype(float)  #保证数据为实数

#将特征变量和目标变量包裹在Variable型变量中
x = Variable(torch.FloatTensor(test_features.values))
y = Variable(torch.FloatTensor(targets))

#用神经网络进行预测
predict = neu(x)
predict = predict.data.numpy()

fig, ax = plt.subplots(figsize = (10, 7))

mean, std = scaled_features['cnt']
ax.plot(predict * std + mean, label='Prediction')
ax.plot(targets * std + mean, label='Data')
ax.legend()
ax.set_xlabel('Date-time')
ax.set_ylabel('Counts')
dates = pd.to_datetime(rides.loc[test_data.index]['dteday'])
dates = dates.apply(lambda d: d.strftime('%b %d'))
ax.set_xticks(np.arange(len(dates))[12::24])
_ = ax.set_xticklabels(dates[12::24], rotation=45)
复制代码

实际曲线与预测曲线的对好比图3.24所示。其中,横坐标是不一样的日期,纵坐标是预测或真实数据的值。虚线为预测曲线,实线为实际数据。

图像说明文字

能够看到,两个曲线基本是吻合的,可是在12月25日先后几天的实际值和预测值误差较大。为何这段时间的表现这么差呢?

仔细观察数据,咱们发现12月25日正好是圣诞节。对于欧美国家来讲,圣诞节就至关于咱们的春节,在圣诞节假期先后,人们的出行习惯会与往日有很大的不一样。可是,在咱们的训练样本中,由于整个数据仅有两年的长度,因此包含圣诞节先后的样本仅有一次,这就致使咱们没办法对这一特殊假期的模式进行很好的预测。

3.4 剖析神经网络Neu

按理说,目前咱们的工做已经所有完成了。可是,咱们还但愿对人工神经网络的工做原理有更加透彻的了解。所以,咱们将对这个训练好的神经网络Neu进行剖析,看看它究竟为何可以在一些数据上表现优异,而在另外一些数据上表现欠佳。

对于咱们来讲,神经网络在训练的时候发生了什么彻底是黑箱,可是,神经网络连边的权重实际上就存在于计算机的存储中,咱们是能够把感兴趣的数据提取出来分析的。

咱们定义了一个函数feature(),用于提取神经网络中存储在连边和节点中的全部参数。代码以下:

def feature(X, net):
    #定义一个函数,用于提取网络的权重信息,全部的网络参数信息所有存储在neu的named_parameters集合中
    X = Variable(torch.from_numpy(X).type(torch.FloatTensor), requires_grad = False)
    dic = dict(net.named_parameters()) #提取这个集合
    weights = dic['0.weight'] #能够按照“层数.名称”来索引集合中的相应参数值
    biases = dic['0.bias'] 
    h = torch.sigmoid(X.mm(weights.t()) + biases.expand([len(X), len(biases)])) #隐含层的计算过程
    return h #输出层的计算
复制代码

在这段代码中,咱们用net.named_parameters()命令提取出神经网络的全部参数,其中包括了每一层的权重和偏置,而且把它们放到Python字典中。接下来就能够经过如上代码来提取,例如能够经过dic['0.weight']和dic['0.bias']的方式获得第一层的全部权重和偏置。此外,咱们还能够经过遍历参数字典dic获取全部可提取的参数名称。

因为数据量较大,咱们选取了一部分数据输入神经网络,并提取出网络的激活模式。咱们知道,预测不许的日期有12月22日、12月23日、12月24日这3天。因此,就将这3天的数据汇集到一块儿,存入subset和subtargets变量中。

bool1 = rides['dteday'] == '2012-12-22'
bool2 = rides['dteday'] == '2012-12-23'
bool3 = rides['dteday'] == '2012-12-24'

#将3个布尔型数组求与
bools = [any(tup) for tup in zip(bool1,bool2,bool3) ]
#将相应的变量取出来
subset = test_features.loc[rides[bools].index]
subtargets = test_targets.loc[rides[bools].index]
subtargets = subtargets['cnt']
subtargets = subtargets.values.reshape([len(subtargets),1])
复制代码

将这3天的数据输入神经网络中,用前面定义的feature()函数读出隐含层神经元的激活数值,存入results中。为了阅读方便,能够将归一化输出的预测值还原为原始数据的数值范围。

#将数据输入到神经网络中,读取隐含层神经元的激活数值,存入results中
results = feature(subset.values, neu).data.numpy()
#这些数据对应的预测值(输出层)
predict = neu(Variable(torch.FloatTensor(subset.values))).data.numpy()
#将预测值还原为原始数据的数值范围
mean, std = scaled_features['cnt']
predict = predict * std + mean
subtargets = subtargets * std + mean
复制代码

接下来,咱们就将隐含层神经元的激活状况所有画出来。同时,为了比较,咱们将这些曲线与模型预测的数值画在一块儿,可视化的结果如图3.25所示。

#将全部的神经元激活水平画在同一张图上
fig, ax = plt.subplots(figsize = (8, 6))
ax.plot(results[:,:],'.:',alpha = 0.1)
ax.plot((predict - min(predict)) / (max(predict) - min(predict)),'bo-',label='Prediction')
ax.plot((subtargets - min(predict)) / (max(predict) - min(predict)),'ro-',label='Real')
ax.plot(results[:, 6],'.:',alpha=1,label='Neuro 7')

ax.set_xlim(right=len(predict))
ax.legend()
plt.ylabel('Normalized Values')

dates = pd.to_datetime(rides.loc[subset.index]['dteday'])
dates = dates.apply(lambda d: d.strftime('%b %d'))
ax.set_xticks(np.arange(len(dates))[12::24])
_ = ax.set_xticklabels(dates[12::24], rotation=45)
复制代码

图像说明文字

图中方块曲线是模型的预测数值,圆点曲线是真实的数值,不一样颜色和线型的虚线是每一个神经元的输出值。能够发现,6号神经元(Neuro 6)的输出曲线与真实输出曲线比较接近。所以,咱们能够认为该神经元对提升预测准确性有更高的贡献。

同时,咱们还想知道Neuro 6神经元表现较好的缘由以及它的激活是由谁决定的。进一步分析它的影响因素,能够知道是从输入层指向它的权重,如图3.26所示。

图像说明文字

咱们能够经过下列代码将这些权重进行可视化。

#找到与峰值对应的神经元,将其到输入层的权重输出
dic = dict(neu.named_parameters())
weights = dic['0.weight']
plt.plot(weights.data.numpy()[6, :],'o-')
plt.xlabel('Input Neurons')
plt.ylabel('Weight')
复制代码

结果如图3.27所示。横轴表明了不一样的权重,也就是输入神经元的编号;纵轴表明神经网络训练后的连边权重。例如,横轴的第10个数,对应输入层的第10个神经元,对应到输入数据中,是检测天气类别的类型变量。第32个数,是小时数,也是类型变量,检测的是早6点这种模式。咱们能够理解为,纵轴的值为正就是促进,值为负就是抑制。因此,图中的波峰就是让该神经元激活,波谷就是神经元未激活。

图像说明文字

咱们看到,这条曲线在hr_12, weekday_0,6方面有较高的权重,这表示神经元Neuro 6正在检测如今的时间点是否是中午12点,同时也在检测今天是否是周日或者周六。若是知足这些条件,则神经元就会被激活。与此相对的是,神经元在weathersit_3和hr_6这两个输入上的权重值为负值,而且恰好是低谷,这意味着该神经元会在下雨或下雪,以及早上6点的时候被抑制。经过翻看万年历咱们知道,2012年的12月22日和23日恰好是周六和周日,所以Neuro 6被激活了,它们对正确预测这两天的正午高峰作了贡献。可是,因为圣诞节即将到来,人们可能早早回去为圣诞作准备,所以这个周末比较特殊,并未出现往常周末的大量骑行需求,因而Neuro 6给出的激活值致使了太高的正午单车数量预测。

与此相似,咱们能够找到致使12月24日迟早高峰太高预测的缘由。咱们发现4号神经元起到了主要做用,由于它的波动形状恰好跟预测曲线在24日的迟早高峰负相关,如图3.28所示。

图像说明文字

同理,这个神经元对应的权重及其检测的模式如图3.29所示。

图像说明文字

这个神经元检测的模式和Neuro 6类似却相反,它在迟早高峰的时候受到抑制,在节假日和周末激活。进一步考察从隐含层到输出层的链接,咱们发现Neuro 4的权重为负数,可是这个负值又没有那么大。因此,这就致使了在12月24日迟早高峰的时候被抑制,可是这个信号抑制的效果并不显著,没法致使预测尖峰的出现。

因此,咱们分析出神经预测器Neu在这3天预测不许的缘由是圣诞假期的反常模式。12月24日是圣诞夜,该网络对节假日迟早高峰抑制单元的抑制不够,因此致使了预测不许。若是有更多的训练数据,咱们有可能将4号神经元的权重调节得更低,这样就有可能提升预测的准确度。

3.5 小结

本章咱们以预测某地共享单车数量的问题做为切入点,介绍了人工神经网络的工做原理。经过调整神经网络中的参数,咱们能够获得任意形状的曲线。接着,咱们尝试用具备单输入、单输出的神经网络拟合了共享单车数据并尝试预测。

可是,预测的效果却很是差。通过分析,咱们发现,因为采用的特征变量为数据的编号,而这与单车的数量没有任何关系,完美拟合的假象只不过是一种过拟合的结果。因此,咱们尝试了新的预测方式,利用每一条数据中的特征变量,包括天气、风速、星期几、是不是假期、时间点等特征来预测单车使用数量,并取得了成功。

在第二次尝试中,咱们还学会了如何对数据进行划分,以及如何用PyTorch自带的封装函数来实现咱们的人工神经网络、损失函数以及优化器。同时,咱们引入了批处理的概念,即将数据切分红批,在每一步训练周期中,都用一小批数据来训练神经网络并让它调整参数。这种批处理的方法既能够加速程序的运行,又让神经网络可以稳步地调节参数。

最后,咱们对训练好的神经网络进行了剖析。了解了人工神经元是如何经过监测数据中的固有模式而在不一样条件下激活的。咱们也清楚地看到,神经网络之因此在一些数据上工做很差,是由于在数据中很难遇到假期这种特殊条件。

3.6 Q&A

本书内容源于张江老师在“集智AI学园”开设的网络课程“火炬上的深度学习”,为了帮助读者快速疏通思路或解决常见的实践问题,咱们挑选了课程学员提出的具备表明性的问题,并附上张江老师的解答,组成“Q&A”小节,附于相关章节的末尾。若是读者在阅读过程当中产生了类似的疑问,但愿能够从中获得解答。

Q:神经元是否是越多越好?

A:固然不是越多越好。神经网络模型的预测能力不仅和神经元的个数有关,还与神经网络的结构和输入数据有关。

Q:在预测共享单车使用量的实验中,为何要作梯度清空?

A:若是不清空梯度,backward()函数是会累加梯度的。咱们在进行一次训练后,就当即进行梯度反传,因此不须要系统累加梯度。若是不清空梯度,有可能致使模型没法收敛。

Q:对于神经网络来讲,非收敛函数也能够逼近吗?

A:在必定的闭区间里是能够的。由于在闭区间里,一个函数不可能无穷发散,总会有一个界限,那么就可使用神经网络模型进行逼近。对于一个无穷的区间来讲,神经网络模型就不行了,由于神经网络模型中用于拟合的神经元数量是有限的。

Q:在预测共享单车的例子中,模型对圣诞节期间的单车使用量预测得不够准确。那么是否是能够经过增长训练数据的方法提升神经网络预测的准确性?

A:是可行的。若是使用更多的包含圣诞节期间单车使用状况的训练数据训练模型,那么模型对圣诞节期间的单车使用状况的预测会更加准确。

Q:既然预测共享单车使用量的模型能够被解析和剖析,那么是否是每一个神经网络均可以这样剖析?

A:这个不必定。由于预测共享单车使用量的模型结构比较简单,隐藏层神经元只有10个。当网络模型中神经元的个数较多或者有多层神经元的时候,神经网络模型的某个“决策”会难以归因到单个神经元里。这时就难以用“剖析”的方式来分析神经网络模型了。

Q:在训练神经网络模型的时候,讲到了“训练集/测试集=k”,那么比例k是多少才合理,k对预测的收敛速度和偏差有影响吗?

A:在数据量比较少的状况下,咱们通常按照10∶1的比例来选择测试集;而在数据量比较大的状况下,好比,数据有十万条以上,就不必定必须按照比例来划分训练集和测试集了。

相关文章
相关标签/搜索