作者归档:feiguyun

2.Pandas提高篇

2.1 如何提高数据的颜值?

很多时候面对各种数据,我们想要让不同DataFrame有不同的颜色或格式来显示(styling),这时可以使用pandas Styler底下的format函式来实现,如何实现?以下通过一个实例来说明。
(1)生成数据

(2)数据浏览

(3)对dataframe不同数据设置不同的显示风格

(4)显示结果

2.2如何使你的分析更专业?

通常的数据分析有:排序、分组汇总平均分析、同比、环比等等,如何使你的分析更专业?可以通过引入一些更具专业的KPI指标分析,如反应客户偏好的TGI分析、反应客户价值的RMF指标、投资回报比(ROI)等等,这里以TGI分析为例,这也是用户画像中的一个重要指标。

2.2.1 TGI指标解释

1.TGI(Target Group Index的简称)指标计算公式:

2.TGI关键特征
TGI计算公式中,有三个关键点:总体,目标群体,某一特征。
 总体:是我们研究的所有对象,比如一个学校里的所有人;
 目标群体:是总体中我们感兴趣的一个分组,比如一个班;
 某一特征:想要分析的某种行为或者状态,比如打篮球。
3、TGI计算示例
假设一个学校有1000人,有300个人喜欢打篮球,那么这个比例就是30%,即“总体中具有相同特征的群体所占比例” = 30%。
假设一个班有50个人,有20个人喜欢打篮球,那么这个比例就是40%,即“目标群体中具有某一特征的群体所占比例” = 40%。
TGI指数 = (40% / 30%) * 100 = 133
TGI指数大于100,代表着某类用户更具有相应的倾向或者偏好,数值越大则倾向和偏好越强;小于100,则说明该类用户相关倾向较弱(和平均相比);而等于100则表示在平均水平。

2.2.2 导入数据

本数据为客户购买商品的订单信息,具体数据包括品牌名、买家姓名、付款时间、订单状态和地域等字段,一共28832条数据。
1、导入数据

2、数据浏览

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 28832 entries, 0 to 28831
Data columns (total 9 columns):
品牌名称 28832 non-null object
买家昵称 28832 non-null object
付款日期 28832 non-null datetime64[ns]
订单状态 28832 non-null object
实付金额 28832 non-null float64
邮费 28832 non-null int64
省份 28832 non-null object
城市 28832 non-null object
购买数量 28832 non-null int64
dtypes: datetime64[ns](1), float64(1), int64(2), object(5)
memory usage: 2.0+ MB

2.2.3 选择一个特征

这里我们以每个客户平均销售额50为界限,划分“高客单”或"低客单",接下来我们以“高客单”为特征进行TGI分析。
1、对dataframe进行分组汇总

2、增加一个客单类别指标

2.2.4 获取客户地域信息

df_users有单个用户的金额和客户类型,为便于各省市消费情况的比较,接下来就是添加每个用户的地域字段, 这个可以同pd.merge函数实现,具体与两表关联类型。由于源数据是未去重的,我们得先按昵称去重,不然匹配的结果会有许多重复的数据。
1、关联前对原df进行预处理

【说明】
DataFrame.duplicated函数格式为:
DataFrame.duplicated(subset=None, keep='first') 参数说明:
 subset可选
 keep{‘first’, ‘last’, False}, default ‘first’ df.loc函数功能:
通过标签或布尔数组访问一组行和列

2.2.5 计算高客单TGI指标

这样得到的结果包含了层次化索引,要索引得到“高客单”列,需要先索引“买家昵称”,再索引“高客单”。

这样,拿到了每个省市的高客单人数,然后再拿到低客单的人数,进行横向合并:

再看看每个城市总人数以及高客单人数占比,来完成“目标群体(如:高客单)中具有某一特征的群体所占比例”这个分子的计算:

有些非常小众的城市,高客单或者低客单人数等于1甚至没有,而这些值尤其是空值会影响结果的计算,我们要提前检核数据:

0.41528303343887557
最后一步,就是TGI指数的计算,顺便排个序

2.2.6 重要结论

基于各城市高客单TGI指数,我们发现福州、珠海、北京、厦门、佛山、南昌、成都、上海、无锡和深圳,是高客单偏好排名前10的城市!咱们要试销的高客单新产品,如果仅从客单角度,可以优先考虑他们!

2.3 如何使你的分析结果更具震撼效果?

为使分析结果更直观、更具震撼效果,可以采用pycharts工具,该工具提供了很多富有感染力的画图模块,除通常的图形外,Pycharts还提供热力图、日历图、迁徙图、地图等等,这里我们以地图为例。
1、导入需要的库

2、对tgi增加一个字段
为便于使用中国地图,这里增加一个中国字段。

3、创建画图函数

运行该函数

显示各省或直辖市,高客单的用户数。

另外,向大家提供一个有关如何提升数据分析能力的博客,供大家参考
月薪3000和30000的数据分析师差在哪?

1. Pandas基础篇

Python有了NumPy的Pandas,用Python处理数据就像使用Exel或SQL一样简单方便。
Pandas是基于NumPy的Python 库,它被广泛用于快速分析数据,以及数据清洗和准备等工作。可以把 Pandas 看作是 Python版的Excel或Table。Pandas 有两种数据结构:
Series和DataFrame,Pandas经过几个版本的更新,目前已经成为数据清洗、处理和分析的不二选择。

1.1 问题:Pandas有哪些优势?

科学计算方面NumPy是优势,但NumPy中没有标签,数据清理、数据处理就不是其强项了。而DataFrame有标签,就像SQL中的表一样,所以在数据处理方面DataFrame就更胜一筹了,具体包含以下几方面:
(1)读取数据方面
Pandas提供强大的IO读取工具,csv格式、Excel文件、数据库等都可以非常简便地读取,对于大数据,pandas也支持大文件的分块读取。
(2)在数据清洗方面
面对数据集,我们遇到最多的情况就是存在缺失值,Pandas把各种类型数据类型的缺失值统一称为NaN,Pandas提供许多方便快捷的方法来处理这些缺失值NaN。
(3)分析建模阶段
在分析建模阶段,Pandas自动且明确的数据对齐特性,非常方便地使新的对象可以正确地与一组标签对齐,由此,Pandas就可以非常方便地将数据集进行拆分-重组操作。
(4)结果可视化方面
结果展示方面,我们都知道Matplotlib是个数据视图化的好工具,Pandas与Matplotlib搭配,不用复杂的代码,就可以生成多种多样的数据视图。

1.2 Pandas数据结构

Pandas中两个最常用的对象是Series和DataFrame。使用pandas前,需导入以下内容:

Pandas主要采用Series和DataFrame两种数据结构。Series是一种类似一维数据的数据结构,由数据(values)及索引(indexs)组成,而DataFrame是一个表格型的数据结构,它有一组序列,每列的数据可以为不同类型(NumPy数据组中数据要求为相同类型),它既有行索引,也有列索引。

图1-1 DataFrame结构

1.3 Series

上章节我们介绍了多维数组(ndarray),当然,它也包括一维数组,Series类似一维数组,为啥还要介绍Series呢?或Series有哪些特点?
Series一个最大特点就是可以使用标签索引,序列及ndarray也有索引,但都是位置索引或整数索引,这种索引有很多局限性,如根据某个有意义标签找对应值,切片时采用类似[2:3]的方法,只能取索引为2这个元素等等,无法精确定位。
Series的标签索引(它位置索引自然保留)使用起来就方便多了,且定位也更精确,不会产生歧义。以下通过实例来说明。
(1)使用Series

0 1
1 3
2 6
3 -1
4 2
5 8
dtype: int64
(2)使用Series的索引

a 1
c 3
d 6
e -1
b 2
g 8
dtype: int64
(3)根据索引找对应值

1.4 DataFrame

DataFrame除了索引有位置索引也有标签索引,而且其数据组织方式与MySQL的表极为相似,除了形式相似,很多操作也类似,这就给操作DataFrame带来极大方便。这些是DataFrame特色的一小部分,它还有比数据库表更强大的功能,如强大统计、可视化等等。
DataFrame有几个要素:index、columns、values等,columns就像数据库表的列表,index是索引,values就是值。

图1-2 DataFrame结果

1.4.1 生成DataFrame

生成DataFrame有很多,比较常用的有导入等长列表、字典、numpy数组、数据文件等。

1.4.2 获取数据

获取DataFrame结构中数据可以采用obj[]操作、obj.iloc[]、obj.loc[]等命令。
(1)使用obj[]来获取列或行

(2)使用obj.loc[] 或obj.iloc[]获取行或列数据。
loc通过行标签获取行数据,iloc通过行号获取行数据。
loc 在index的标签上进行索引,范围包括start和end.
iloc 在index的位置上进行索引,不包括end.
这两者的主要区别可参考如下示例:

【说明】
除使用iloc及loc外,早期版本还有ix格式。pandas0.20.0及以上版本,ix已经丢弃,请尽量使用loc和iloc;

1.4.3 修改数据

我们可以像操作数据库表一样操作DataFrame,删除数据、插入数据、修改字段名、索引名、修改数据等,以下通过一些实例来说明。

图1-3 数据结构

1.4.4 汇总统计

Pandas有一组常用的统计方法,可以根据不同轴方向进行统计,当然也可按不同的列或行进行统计,非常方便。
常用的统计方法有:
表1-1 Pandas统计方法

以下通过实例来说明这些方法的使用
(1)把csv数据导入pandas

(2)查看df的统计信息

【说明】

即各项-均值的平方求和后再除以N 。
std:表示标准差,是var的平方根。

1.4.5选择部分列

这里选择学生代码、课程代码、课程名称、程程成绩,注册日期等字段

1.4.6删除重复数据

如果有重复数据(对df1的所有列),则删除最后一条记录。

1.4.7补充缺省值

(1)用指定值补充NaN值
这里要求把stat_date的缺省值(NaN)改为'2018-09-01'

(2)可视化,并在图形上标准数据

结果为:

(3)导入一些库及支持中文的库

(4)画图

运行结果


图1-4 可视化结果

1.4.8 Pandas操作MySQL数据库

(1)从MySQL数据库中获取学生基本信息表

(2)查看df_info前3行数据

(3)选择前两个字段

(4)df2 与df_info1 根据字段stud_code 进行内关联

(5)对df3 根据字段stud_code,sub_code进行分组,并求平均每个同学各科的平均成绩。

【备注】
如果需要合计各同学的成绩,可用如下语句。

(6)选择数学分析课程,并根据成绩进行降序。

(7)取前5名

注:DataFrame数据结构的函数或方法有很多,大家可以通过df.[Tab键]方式查看,具体命令的使用方法,如df.count(),可以在Ipython命令行下输入:?df.count() 查看具体使用,退出帮助界面,按q即可。

1.4.9 Pandas操作excel

把DataFrame数据写入excel中的多个sheet中

1.4.10 Pandas的三板斧

我们知道数据库中有很多函数可用作用于表中元素,DataFrame也可将函数(内置或自定义)应用到各列或行上,而且非常方便和简洁,具体可用通过DataFrame的apply、applymap和map来实现,其中apply、map对数据集中的每列或每行的逐元操作,applymap对dataframe的每个元素进行操作,这些函数对数据处理的强大工具。以下通过实例说明具体使用。

1.4.11 处理时间序列

pandas最基本的时间序列类型就是以时间戳(时间点)(通常以python字符串或datetime对象表示)为索引的Series:

索引为日期的DataFrame数据的索引、选取以及子集构造

1.4.12 数据离散化

如何离散化连续性数据?在一般开发语言中,可以通过控制语句来实现,但如果分类较多时,这种方法不但繁琐,效率也比较低。在Pandas中是否有更好方法?如果有,又该如何实现呢?
pandas有现成方法,如cut或qcut等,不需要编写代码,至于如何使用还是通过实例来说明。

现在需要对age字段进行离散化, 划分为(20,30],(30,40],(40,50].

1.4.13 交叉表

我们平常看到的数据格式大多像数据库中的表,如购买图书的基本信息:
表1-2 客户购买图书信息

这样的数据比较规范,比较适合于一般的统计分析。但如果我们想查看客户购买各种书的统计信息,就需要把以上数据转换为如下格式:
表1-3 客户购买图书的对应关系

我们观察一下不难发现,把表1-3中书代码列旋转为行就得到表2数据。如何实现行列的互换呢?编码能实现,但比较麻烦,还好,pandas提供了现成的方法或函数,如stack、unstack、pivot_table函数等。以下以pivot_table为例说明具体实现。

实现行列互换,把书代码列转换为行或索引

第1章 NumPy基础

在机器学习和深度学习中,图像、声音、文本等输入数据最终都要转换为数组或矩阵。如何有效进行数组和矩阵的运算?这就需要充分利用NumPy。NumPy是数据科学的通用语言,而且与Pytorch关系非常密切,它是科学计算、深度学习的基石。尤其对Pytorch而言,其重要性更加明显。Pytorch中的Tensor与NumPy非常相似,它们之间可以非常方便地进行转换,掌握NumPy是学好Pytorch的重要基础,故我们把它列为全书第1章。
为什么是NumPy?实际上Python本身含有列表(list)和数组(array),但对于大数据来说,这些结构有很多不足。因列表的元素可以是任何对象,因此列表中所保存的是对象的指针。例如为了保存一个简单的[1,2,3],都需要有3个指针和三个整数对象。对于数值运算来说这种结构显然比较浪费内存和CPU等宝贵资源。 至于array对象,它直接保存数值,和C语言的一维数组比较类似。但是由于它不支持多维,在上面的函数也不多,因此也不适合做数值运算。
NumPy(Numerical Python 的简称)的诞生弥补了这些不足,NumPy提供了两种基本的对象:ndarray(N-dimensional array object)和 ufunc(universal function object)。ndarray是存储单一数据类型的多维数组,而ufunc则是能够对数组进行处理的函数。
NumPy的主要特点:
(1)ndarray,快速和节省空间的多维数组,提供数组化的算术运算和高级的广播功能。
(2)使用标准数学函数对整个数组的数据进行快速运算,而不需要编写循环。
(3)读取/写入磁盘上的阵列数据和操作存储器映像文件的工具。
(4)线性代数,随机数生成,和傅里叶变换的能力。
(5)集成C,C++,Fortran代码的工具。
本章主要内容如下:
 如何生成NumPy数组
 如何存取元素
 NumPy的算术运算
 数组变形
 批量处理
 NumPy的通用函数
 NumPy的广播机制

1.1 生成NumPy数组

NumPy是Python的外部库,不在标准库中。因此,若要使用它,需要先导入NumPy。

导入NumPy后,可通过np.+Tab键,查看可使用的函数,如果对其中一些函数的使用不很清楚,还可以在对应函数+?,再运行,就可很方便的看到如何使用函数的帮助信息。
np.然后按'Tab'键,将出现如下界面:

运行如下命令,便可查看函数abs的详细帮助信息。

NumPy不但强大,而且还非常友好。
接下来我们将介绍NumPy的一些常用方法,尤其是与机器学习、深度学习相关的一些内容。
NumPy封装了一个新的数据类型ndarray(n-dimensional array),它是一个多维数组对象。该对象封装了许多常用的数学运算函数,方便我们做数据处理、数据分析等。如何生成ndarray呢?这里我们介绍生成ndarray的几种方式,如从已有数据中创建、利用random创建、创建特殊多维数组、使用arange函数等。

1.1.1 从已有数据中创建数组

直接对 Python 的基础数据类型(如列表、元组等) 进行转换来生成 ndarray:
(1)将列表转换成 ndarray

(2)嵌套列表可以转换成多维 ndarray

如果把上面示例中的列表换成元组也同样适合。

1.1.2 利用 random 模块生成数组

在深度学习中,我们经常需要对一些参数进行初始化,为了更有效训练模型,提高模型的性能,有些初始化还需要满足一定条件,如满足正态分布或均匀分布等。这里我们介绍几种常用的方法,表1-1列举了 np.random 模块常用的函数。
表1-1 np.random模块常用函数

下面我们来看看一些函数的具体使用:

为了每次生成同一份数据,可以指定一个随机种子,使用shuffle函数打乱生成的随机数。

输出结果:
[[-1.0856306 0.99734545 0.2829785 ]
[-1.50629471 -0.57860025 1.65143654]]
随机打乱后数据:
[[-1.50629471 -0.57860025 1.65143654]
[-1.0856306 0.99734545 0.2829785 ]]

1.1.3 创建特定形状的多维数组

参数初始化时,有时需要生成一些特殊矩阵,如全是 0 或 1 的数组或矩阵,这时我们可以利用np.zeros、np.ones、np.diag来实现,如表1-2所示。
表1-2 NumPy 数组创建函数

下面我们通过几个示例来说明:

有时我们可能需要把生成的数据暂时保存起来,以备后续使用。

输出结果:
[[0.41092437 0.5796943 0.13995076 0.40101756 0.62731701]
[0.32415089 0.24475928 0.69475518 0.5939024 0.63179202]
[0.44025718 0.08372648 0.71233018 0.42786349 0.2977805 ]
[0.49208478 0.74029639 0.35772892 0.41720995 0.65472131]
[0.37380143 0.23451288 0.98799529 0.76599595 0.77700444]]

1.1.4 利用 arange、linspace 函数生成数组

arange 是 numpy 模块中的函数,其格式为:

其中start 与 stop 指定范围,step 设定步长,生成一个 ndarray,start 默认为 0,步长 step 可为小数。Python有个内置函数range功能与此类似。

linspace 也是 numpy 模块中常用的函数,其格式为:

它可以根据输入的指定数据范围以及等份数量,自动生成一个线性等分向量,其中endpoint (包含终点)默认为 True,等分数量num默认为 50。如果将retstep设置为 True,则会返回一个带步长的 ndarray。

值得一提的,这里并没有像我们预期的那样,生成 0.1, 0.2, ... 1.0 这样步长为0.1的 ndarray,这是因为 linspace 必定会包含数据起点和终点,那么其步长则为(1-0) / 9 = 0.11111111。如果需要产生 0.1, 0.2, ... 1.0 这样的数据,只需要将数据起点 0 修改为 0.1 即可。
除了上面介绍到的 arange 和 linspace,NumPy还提供了 logspace 函数,该函数使用方法与 linspace 使用方法一样,读者不妨自己动手试一下。

1.2 获取元素

上节我们介绍了生成ndarray的几种方法,数据生成后,如何读取我们需要的数据?这节我们介绍几种常用方法。

如果对上面这些获取方式还不是很清楚,没关系,下面我们通过图形的方式进一步说明,如图1-1所示,左边为表达式,右边为表达式获取的元素。注意不同的边界,表示不同的表达式。

图1-1 获取多维数组中的元素
获取数组中的部分元素除通过指定索引标签外,还可以使用一些函数来实现,如通过random.choice函数可以从指定的样本中进行随机抽取数据。

打印结果:
随机可重复抽取
[[ 7. 22. 19. 21.]
[ 7. 5. 5. 5.]
[ 7. 9. 22. 12.]]
随机但不重复抽取
[[ 21. 9. 15. 4.]
[ 23. 2. 3. 7.]
[ 13. 5. 6. 1.]]
随机但按制度概率抽取
[[ 15. 19. 24. 8.]
[ 5. 22. 5. 14.]
[ 3. 22. 13. 17.]]

1.3 NumPy的算术运算

在机器学习和深度学习中,涉及大量的数组或矩阵运算,这节我们重点介绍两种常用的运算。一种是对应元素相乘,又称为逐元乘法(element-wise product),运算符为np.multiply(), 或 *。另一种是点积或内积元素,运算符为np.dot()。

1.3.1对应元素相乘

对应元素相乘(element-wise product)是两个矩阵中对应元素乘积。np.multiply 函数用于数组或矩阵对应元素相乘,输出与相乘数组或矩阵的大小一致,其格式如下:

其中x1,x2之间的对应元素相乘遵守广播规则,NumPy的广播规则本章第7小节将介绍。以下我们通过一些示例来进一步说明。

矩阵A和B的对应元素相乘,用图1-2直观表示为:

图1-2 对应元素相乘示意图
NumPy数组不仅可以和数组进行对应元素相乘,也可以和单一数值(或称为标量)进行运算。运算时,NumPy数组每个元素和标量进行运算,其间会用到广播机制(1.7小节将详细介绍)。

[[ 2. 4.]
[-2. 8.]]
[[ 0.5 1. ]
[-0.5 2. ]]
由此,推而广之,数组通过一些激活函数后,输出与输入形状一致。

输入参数X的形状: (2, 3)
激活函数softmoid输出形状: (2, 3)
激活函数relu输出形状: (2, 3)
激活函数softmax输出形状: (2, 3)

1.3.2 点积运算

点积运算(dot product)又称为内积,在NumPy用np.dot表示,其一般格式为:

以下通过一个示例来说明dot的具体使用及注意事项。

[[21 24 27]
[47 54 61]]
以上运算,用图1-3可表示为:

图1-3 矩阵的点积示意图,对应维度的元素个数需要保持一致
如图1-3所示,矩阵X1和矩阵X2进行点积运算,其中X1和X2对应维度(即X1的第2个维度与X2的第1个维度)的元素个数必须保持一致,此外,矩阵X3的形状是由矩阵X1的行数与矩阵X2的列数构成的。

1.4 数组变形

在机器学习以及深度学习的任务中,通常需要将处理好的数据以模型能接受的格式喂给模型,然后模型通过一系列的运算,最终返回一个处理结果。然而,由于不同模型所接受的输入格式不一样,往往需要先对其进行一系列的变形和运算,从而将数据处理成符合模型要求的格式。最常见的是矩阵或者数组的运算,经常会遇到需要把多个向量或矩阵按某轴方向合并,或需要展平(如在卷积或循环神经网络中,在全连接层之前,需要把矩阵展平)。下面介绍几种常用数据变形方法。

1.4.1 更改数组的形状

修改指定数组的形状是 NumPy 中最常见的操作之一,常见的方法有很多,表1-3 列出了一些常用函数。
表1-3 NumPy中改变向量形状的一些函数

下面我们来看一些示例:
(1)reshape

输出结果:
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
[5 6 7 8 9]]
[[0 1]
[2 3]
[4 5]
[6 7]
[8 9]]
[[0 1 2 3 4]
[5 6 7 8 9]]
值得注意的是,reshape 函数支持只指定行数或列数,剩余的设置为-1即可。所指定的行数或列数一定要能被整除(如10不能被3整除),例如上面代码如果修改为arr.reshape(3,-1)将报错误。
(2)resize

输出结果:
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
[5 6 7 8 9]]
(3).T

输出结果:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[ 0 4 8]
[ 1 5 9]
[ 2 6 10]
[ 3 7 11]]
(4)ravel

输出结果:
[[0 1 2]
[3 4 5]]
按照列优先,展平
[0 3 1 4 2 5]
按照行优先,展平
[0 1 2 3 4 5]
(5)flatten
把矩阵转换为向量,这种需求经常出现在卷积网络与全连接层之间。

输出结果:
[[4. 0. 8. 5.]
[1. 0. 4. 8.]
[8. 2. 3. 7.]]
[4. 0. 8. 5. 1. 0. 4. 8. 8. 2. 3. 7.]
(6)squeeze
这是一个重要用来降维的函数,把矩阵中含1的维度去掉。在Pytorch中还有一种与之相反的操作,torch.unsqueeze这个后面将介绍。

(7)transpose
对高维矩阵进行轴对换,这个在深度学习中经常使用,比如把图片表示颜色的RGB顺序,改为GBR的顺序。

1.4.2 合并数组

合并数组也是最常见的操作之一,表1-4列举了常用的用于数组或向量合并的方法。
表1-4 NumPy 数组合并方法

[说明]
①append、concatnate以及stack都有一个 axis 参数,用于控制数组合并是按行还是按列。
②对于append和concatnate,待合并的数组必须有相同的行数或列数(满足一个即可)。
③stack、hstack、dstack待合并的数组必须具有相同的形状( shape)。
下面选择一些常用函数进行说明。
(1)append
合并一维数组

合并多维数组

输出结果:
按行合并后的结果
[[0 1]
[2 3]
[0 1]
[2 3]]
合并后数据维度 (4, 2)
按列合并后的结果
[[0 1 0 1]
[2 3 2 3]]
合并后数据维度 (2, 4)
(2)concatenate
沿指定轴连接数组或矩阵

输出结果:
[[1 2]
[3 4]
[5 6]]
[[1 2 5]
[3 4 6]]
(3)stack
沿指定轴堆叠数组或矩阵

输出结果:
[[[1 2]
[3 4]]

[[5 6]
[7 8]]]

1.5 批量处理

在深度学习中,由于源数据都比较大,所以通常需要采用批处理。如利用批量来计算梯度的随机梯度法(SGD),就是一个典型应用。深度学习的计算一般比较复杂,加上数据量一般比较大,如果一次处理整个数据,往往出现资源瓶颈。为了更有效的计算,一般将整个数据集分成小批量。与处理整个数据集的另一个极端是每次处理一条记录,这种方法也不科学,一次处理一条记录无法充分发挥GPU、NumPy平行处理优势。因此,实际使用中往往采用批量处理(mini-batch)。
如何把大数据拆分成多个批次呢?可采用如下步骤:
(1)得到数据集
(2)随机打乱数据
(3)定义批大小
(4)批处理数据集
以下我们通过一个示例来具体说明:

最后5行结果:
第9500批次,该批次的数据之和:17.63702580438092
第9600批次,该批次的数据之和:-1.360924607368387
第9700批次,该批次的数据之和:-25.912226239266445
第9800批次,该批次的数据之和:32.018136957835814
第9900批次,该批次的数据之和:2.9002576614446935
【说明】
批次从0开始,所以最后一个批次是9900。

1.6 通用函数

NumPy提供了两种基本的对象,即ndarray和ufunc对象。前面我们介绍了ndarray,本节将介绍NumPy的另一个对象通用函数(ufunc),ufunc是universal function的缩写,它是一种能对数组的每个元素进行操作的函数。许多ufunc函数都是用c语言级别实现的,因此它们的计算速度非常快。此外,它们比math模块中函数更灵活。math模块的输入一般是标量,但NumPy中函数可以是向量或矩阵,而利用向量或矩阵可以避免使用循环语句,这点在机器学习、深度学习中非常重要。表1-5为NumPy常用的几个通用函数。
表1-5 NumPy几个常用通用函数

(1)math与numpy函数的性能比较:

打印结果
math.sin: 0.5169950000000005
numpy.sin: 0.05381199999999886
由此可见,numpy.sin比math.sin快近10倍。
(2)循环与向量运算比较:
充分使用Python的NumPy库中的内建函数(built-in function),实现计算的向量化,可大大提高运行速度。NumPy库中的内建函数使用了SIMD指令。如下使用的向量化要比使用循环计算速度快得多。如果使用GPU,其性能将更强大,不过NumPy不支持GPU。Pytorch支持GPU,第5章将介绍Pytorch如何使用GPU来加速算法。

输出结果
dot = 250215.601995
for loop----- Computation time = 798.3389819999998ms
dot = 250215.601995
verctor version---- Computation time = 1.885051999999554ms
从运行结果上来看,使用for循环的运行时间大约是向量运算的400倍。因此,深度学习算法中,一般都使用向量化矩阵运算。

1.7 广播机制

NumPy的Universal functions 中要求输入的数组shape是一致的,当数组的shape不相等的时候,则会使用广播机制。不过,调整数组使得shape一样,需满足一定规则,否则将出错。这些规则可归结为以下四条:
(1)让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分都通过在前面加1补齐;
如:a:2x3x2 b:3x2,则b向a看齐,在b的前面加1:变为:1x3x2
(2)输出数组的shape是输入数组shape的各个轴上的最大值;
(3)如果输入数组的某个轴和输出数组的对应轴的长度相同或者其长度为1时,这个数组能够用来计算,否则出错;
(4)当输入数组的某个轴的长度为1时,沿着此轴运算时都用(或复制)此轴上的第一组值。
广播在整个NumPy中用于决定如何处理形状迥异的数组;涉及算术运算包括(+,-,*,/…)。这些规则说的很严谨,但不直观,下面我们结合图形与代码进一步说明:
目的:A+B
其中A为4x1矩阵,B为一维向量 (3,)
要相加,需要做如下处理:
(1)根据规则1,B需要向看齐,把B变为(1,3)
(2)根据规则2,输出的结果为各个轴上的最大值,即输出结果应该为(4,3)矩阵
那么A如何由(4,1)变为(4,3)矩阵?B如何由(1,3)变为(4,3)矩阵?
3)根据规则4,用此轴上的第一组值(要主要区分是哪个轴),进行复制(但在实际处理中不是真正复制,否则太耗内存,而是采用其它对象如ogrid对象,进行网格处理)即可,
详细处理如图1-4所示。

图1-4 NumPy广播规则示意图
代码实现

运行结果
A矩阵的形状:(4, 1),B矩阵的形状:(3,)
C矩阵的形状:(4, 3)
[[ 0 1 2]
[10 11 12]
[20 21 22]
[30 31 32]]

1.8 小结

本章主要介绍了NumPy模块的常用操作,尤其涉及对矩阵的操作,这些操作在后续程序中经常使用。NumPy内容很丰富,这里只列了一些主要内容,如果你想了解更多内容,可登录NumPy官网:http://www.numpy.org/

3.4 优化RNN


RNN擅长处理序列数据,尤其当后面数据与前面数据有依赖关系时。不过,如果这种依赖涉及长依赖的话,使用RNN的效果将受到较大影响,因长依赖就意味着时间步就要大,而根据梯度的关联规则,会发现累乘会导致激活函数导数的累乘,如果取tanh或sigmoid函数作为激活函数的话,那么必然是一堆小数在做乘法,结果就是越乘越小。随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于0,这就是“梯度消失“现象。实际使用中,会优先选择tanh函数,原因是tanh函数相对于sigmoid函数来说梯度较大,收敛速度更快且引起梯度消失更慢。通常减缓梯度消失的几种方法:
1、选取更好的激活函数,如Relu激活函数。ReLU函数的左侧导数为0,右侧导数恒为1,这就避免了“梯度消失“的发生。但大于1的导数容易导致“梯度爆炸“,但设定合适的阈值可以解决这个问题。
2、加入BN层,其优点包括可加速收敛、控制过拟合,可以少用或不用Dropout和正则、降低网络对初始化权重不敏感,且能允许使用较大的学习率等。
3、改变传播结构,LSTM结构可以有效解决这个问题。接下来我们将介绍LSTM相关内容。
这些方法中,LSTM比其他几种方法效果要好,LSTM的核心思想就是增加一条记忆以往信息的传送带,这条传送带通过各种更新信息相加,而从有效避免梯度消失问题。
LSTM成功用于许多应用(如语言建模,手势预测,用户建模)。 LSTM基于记忆的序列建模架构非常有影响力——它启发了许多最新的改进方法,例如Transformers。

3.4.1 LSTMCell

1.RNN与LSTM的关系图

 

图1-14 RNN与LSTM的关系
从图1-14 可知,RNN对应LSTM中方框部分,方框部分的信息就是当前状态信息,该信息将通过输入门之后加入到传送带C中。

2.LSTMCell整体架构图
LSTM 通过精心设计的称作为“门”的结构来去除或者增加信息到传送带C状态的能力。门是一种让信息选择式通过的方法。他们通过含激活 sigmoid 神经网络层形成门(如图1-15中等信息进行过滤。

图1-15 LSTM架构图
LSTM三个门的主要功能:

输入门对当前信息C ̃_t进行过滤,其表示式为:

2.LSTMCell的详细结构

图1-16 LSTM的详细内部结构图
为简明起见,图1-16中假设输入为3个元素的向量,隐藏状态及传送信息都是2个元素的向量。隐藏状态与输入拼接成5个元素的向量。实际应用中输入、状态一般加上批量等维度,如[batch,input_size],如果在LSTM模块中还需加上序列长度,形如[batch,seg_len,input_size]。multiplication是指哈达玛积,S表示Softmoid,T表示Tanh。

3.4.2 LSTMCell

LSTMCell这是LSTM的一个单元,如图1-15所示。该单元可以用PyTorch的 nn.LSTMCell模块来实现,该模块构建LSTM中的一个Cell,同一层会共享这一个Cell,但要手动处理每个时刻的迭代计算过程。如果要建立多层的LSTM,就要建立多个nn.LSTMCell。
1 .构造方法
构造方法和nn.RNNcell类似,依次传入feature_len和hidden_len,因为这只是一个计算单元,所以不涉及层数。
2. forward方法
回顾一下nn.RNNCell的forward方法,它是:
ht=nn.rnncell(x,ht-1)
即上一时刻的输出ht−1经nn.RNNCell前向计算得到这一时刻的ht。
对于nn.LSTMCell也是类似,但因为LSTM计算单元中还涉及对上一时刻的记忆Ct−1的使用,所以是
ht,ct=lstmcell(xt,(ht-1,ct-1))
因为输入xt只是t时刻的输入,不涉及seq_len,所以其shape是[batch,feature_len]
而ht和Ct在这里只是t时刻本层的隐藏单元和记忆单元,不涉及num_layers,所以其shape是 [batch,hidden_len]

3.4.3 一层的例子

每个时刻传入新的输入xt和上一时刻的隐藏单元ht−1和记忆单元Ct−1,并把这两个单元更新。

3.4.4 两层的例子

在最底下一层l0层和上面的例子一样,上层还需要接受下层的输出ht作为当前输入,然后同样是依赖本层上一时刻的h和C更新本层的h和C。注意LSTM单元的输入输出结构,向上层传递的是h而不是C。

3.4.5 PyTorch的LSTM模块

https://zhuanlan.zhihu.com/p/139617364
1、多层LSTM的网络架构

图1-17 多层LSTM架构图
2、LSTM模块

3、输入
input(seq_len, batch, input_size)
参数有:
seq_len:序列长度,在NLP中就是句子长度,一般都会用pad_sequence补齐长度
batch:每次喂给网络的数据条数,在NLP中就是一次喂给网络多少个句子
input_size:特征维度,和前面定义网络结构的input_size一致

图1-18 LSTM的输入格式
如果LSTM的参数 batch_first=True,则要求输入的格式是:input(batch, seq_len, input_size)
4、隐含层
LSTM有两个输入是 h0 和 c0,可以理解成网络的初始化参数,用随机数生成即可。

参数:
num_layers:隐藏层数
num_directions:如果是单向循环网络,则num_directions=1,双向则num_directions=2
batch:输入数据的batch
hidden_size:隐藏层神经元个数
注意,如果我们定义的input格式是:
input(batch, seq_len, input_size)
则H和C的格式也是要变的:

5、输出
LSTM的输出是一个tuple,如下:
output,(ht, ct) = net(input)
output: 最后一个状态的隐藏层的神经元输出
ht:最后一个状态的隐含层的状态值
ct:最后一个状态的隐含层的遗忘门值
output的默认维度是:

和input的情况类似,如果我们前面定义的input格式是:
input(batch, seq_len, input_size)
则ht和ct的格式也是要变的:

3.5 LSTM实现文本生实例

本实例使用2014人民日报上的一些新闻,使用PyTorch提供的nn.LSTM模型,根据提示字符串预测给定长度的语句。整个处理与3.3小节基本相同,构建模型时稍有不同,LSTM有两个变量h和c,RNN只要一个h。

3.6 GRU结构

https://blog.csdn.net/Jerr__y/article/details/58598296
LSTM门比较多,状态有C和H,其中C就相当于中间变量一样,输出只有H。有关LSTM的改进方案比较多,其中比较著名的变种 GRU(Gated Recurrent Unit ),这是由 Cho, et al. (2014) 提出。在 GRU 中,如 下图所示,只有两个门:重置门(reset gate)和更新门(update gate)。同时在这个结构中,把细胞状态和隐藏状态进行了合并。最后模型比标准的 LSTM 结构要简单,而且这个结构后来也非常流行。

图1-19 GRU模型结构图
其中,r_t 表示重置门,z_t表示更新门。重置门决定是否将之前的状态(h_(t-1))忘记。当r_t趋于 0 的时候,前一个时刻的状态信息 h_(t-1)将被忘掉,隐藏状态 h_t会被重置为当前输入的信息。更新门决定是否要将隐藏状态更新为新的状态 h ̃_t (更新门的作用相当于合并了 LSTM 中的遗忘门和输入门)。
和 LSTM 比较一下:
(1) GRU 少一个门,同时少了传送带状态 C_t。
(2) 在 LSTM 中,通过遗忘门和输入门控制信息的保留和传入;GRU 则通过重置门来控制是否要保留原来隐藏状态的信息,但是不再限制当前信息的传入。
(3) 在 LSTM 中,虽然得到了新的传送带状态 C_t,但是它仅仅作为一个中间变量,不直接输出,而是需要经过一个过滤的处理:
h_t=O_t 〖⊗tanh⁡(C〗_t)
同样,在 GRU 中, 虽然 (2) 中也得到了新的隐藏状态h_t, 但是还不能直接输出,而是通过更新门来控制最后的输出:
h_t=(1-z_t )⊗h_(t-1)+z_t⊗h ̃_t
保留以往隐藏状态(h_(t-1))和保留当前隐藏状态(h ̃_t)之间是一种互斥关系,这点从上面这个公式也可说明。如果z_t 越接近1,则(1-z_t )就越接近于0;反之,也成立。

3.7 biRNN结构

1.biRNN的网络架构
图1-20 为biRNN的网络架构图,从图中还可以看出,对于一个序列,一次向前遍历得到左LSTM,向后遍历得到右LSTM。隐层矢量直接通过拼接(concat)得到,最终损失函数直接相加,具体如下图。

图1-20 biRNN模型结构图
2、biRNN的不足
(1)biRNN模型的前向和后向LSTM模型是分开训练的,故它不是真正双向学习的模型,即biRNN无法同时向前和向后学习。
(2)biRNN如果层数多了,将出现自己看到自己的情况。下图中第2行第2列中的A|CD,此结果是第一层Bi LSTM的B位置输出内容,包括正向A和反向CD,然后直接拼接就得到A| CD。下图中第3行第2列中的ABCD, 此结果为正向BCD和反向AB | D拼接而成,当前位置需要预测的是B,但B已经在这里ABCD出现了。所以对于Bi LSTM,只要层数增加,会有一个“看到你自己”的问题。

 

图1-21 多层biRNN将自己看到自己示意图

利用RNN生成中文语句实例

本实例使用2014人民日报上的一些新闻(约28万条,60M),使用PyTorch提供的nn.rnn模型,根据提示字符串预测给定长度的语句。

数据下载

提取码:6l9e

3.3.1 模型架构

图1-13 RNN实例模型架构图

3.3.2 导入需要的模块

3.3.3 定义预处理函数

使用torch.utils.data生成可迭代的数据集。

3.3.4 定义模型

根据图1-13构建模型,以Embedding为输入层,隐含节点数为256,共两层。为何使用Embedding层作为输入层,而不使用One-hot编码作为输入层?Embedding输入层,除可以有效压缩维度空间外(与以one-hot编码为输入层),更重要的是Embedding层在整个迭代过程中参与学习。

3.3.5 定义训练模型函数

为便于管理,这里参数传入采用argparse方式。

3.3.6 设置参数

这里参数设置运行环境为jupyter notebook ,如果在命令行运行需要做一些改动。

3.3.7 运行模型

3.3.8 练习

1.把Emedding层改为One-hot层
2.目前学习率为固定,把固定改为动态(如与迭代次数相关关联),查看损失值的变化
3.修改学习率及迭代次数等,比较损失值的变化。
4.使用LSTM或GRU模型
5.使用GPT或BERT模型

3.2 循环神经网络语言模型

神经网络语言模型(NNLM)对统计语言模型而言(如n-gram),前进了一大步。它用词嵌入代替词索引,把词映射到低维向量,用神经网络计算代替词组的统计频率计算,从而有效避免维度灾难、增强了词的表现力和模型的泛化力。不过NNLM除计算效率问题外,主要还是基于n-gram的思想,虽然对n有所扩充,但是本质上仍然是使用神经网络编码的n-gram模型,而且对输入数据要求固定长度(一般取5-10),这严重影响模型的性能和泛化能力,所以,如何打破n的"桎梏"一直是人们追求的方向。
循环神经网络(RNN)(尤其其改进版本LSTM)序列语言模型的提出,较好的解决了如何学习到长距离的依赖关系的问题,接下来将从多个角度、多个层次详细介绍RNN。

3.2.1 RNN结构

图1-8是循环神经网络的经典结构,从图中可以看到输入x、隐含层、输出层等,这些与传统神经网络类似,不过自循环W却是它的一大特色。这个自循环直观理解就是神经元之间还有关联,这是传统神经网络、卷积神经网络所没有的。

图1-8 循环神经网络的结构
其中U是输入到隐含层的权重矩阵,W是状态到隐含层的权重矩阵,s为状态,V是隐含层到输出层的权重矩阵。图1-8比较抽象,将它展开成图1-9,就更好理解。

图1-9循环神经网络的展开结构
这是一个典型的Elman循环神经网络,从图7-2不难看出,它的共享参数方式是各个时间节点对应的W、U、V都是不变的,这个机制就像卷积神经网络的过滤器机制一样,通过这种方法实现参数共享,同时大大降低参数量。
图1-9中隐含层不够详细,把隐含层再细化就可得图1-10。

图1-10 循环神经网络Cell的结构图
这个网络在每一时间t有相同的网络结构,假设输入x为n维向量,隐含层的神经元个数为m,输出层的神经元个数为r,则U的大小为n×m维;W是上一次的s_(t-1)作为这一次输入的权重矩阵,大小为m×m维;V是连输出层的权重矩阵,大小为m×r维。而x_t、s_t 和o_t 都是向量,它们各自表示的含义如下:
x_t是时刻t的输入;
s_t是时刻t的隐层状态。它是网络的记忆。s_t基于前一时刻的隐层状态和当前时刻的输入进行计算,
函数f通常是非线性的,如tanh或者ReLU。s_(t-1)为前一个时刻的隐藏状态,其初始化通常为0;
o_t是时刻t的输出。例如,如想预测句子的下一个词,它将会是一个词汇表中的概率向量,o_t=softmax(Vs_t);
s_t认为是网络的记忆状态,s_t可以捕获之前所有时刻发生的信息。输出o_t的计算仅仅依赖于时刻t的记忆。
图1-10是RNN Cell的结构图,我们可以放大这个图,看看其内部结构,如图1-11所示。

图1-11 RNN 的Cell的内部结构
在图1-11中,把隐含状态s_(t-1) (假设为一个4维向量)
和输入x_t(假设为一个3维向量)拼接成一个7维向量,这个7维向量构成全连接神经网络的输入层,输出为4个节点的向量,激活函数为f,输入与输出间的权重矩阵的维度为[7,4]。这样式(1.6)可写成如下形式:

3.2.2 RNNCell代码实现

这里用Python实现(1.8)、(1.9)式,为简便起见,这里不考虑偏移量b。
1、定义激活函数

2、定义状态及输入值,并进行拼接。

3、计算输出值

3.2.3 多层RNNCell

上节我们介绍了单个RNN的结构(即RNNCell),有了这个基础之后,我们就可对RNNCell进行拓展。RNN可以横向拓展(增加时间步或序列长度),也可纵向拓展成多层循环神经网络,如图1-12所示。

图1-12多层循环神经网络
图1-12中,序列长度为T,网络层数为3。接下来为帮助大家更好的理解,接下来我们用PyTorch实现一个2层RNN。

3.2.4 多层RNNCell的PyTorch代码实现

Pytorch提供了两个版本的循环神经网络接口,单元版的输入是每个时间步,或循环神经网络的一个循环,而封装版的是一个序列。下面我们从简单的封装版torch.nn.RNN开始,其一般格式为:

由图7-3 可知,RNN状态输出a_t的计算公式为:

nn.RNN函数中的参数说明如下:
input_size : 输入x的特征数量。
hidden_size : 隐含层的特征数量。
num_layers : RNN的层数。
nonlinearity : 指定非线性函数使用tanh还是relu。默认是tanh。
bias : 如果是False,那么RNN层就不会使用偏置权重 b_i和b_h,默认是True
batch_first : 如果True的话,那么输入Tensor的shape应该是(batch, seq, feature),输出也是这样。默认网络输入是(seq, batch, feature),即序列长度、批次大小、特征维度。
dropout : 如果值非零(该参数取值范围为0~1之间),那么除了最后一层外,其它层的输出都会加上一个dropout层,缺省为零。
bidirectional : 如果True,将会变成一个双向RNN,默认为False。

函数nn.RNN()的输入包括特征及隐含状态,记为(x_t 、h_0),输出包括输出特征及输出隐含状态,记为(output_t 、h_n)。
其中特征值x_t的形状为(seq_len, batch, input_size), h_0的形状为(num_layers * num_directions, batch, hidden_size),其中num_layers为层数,num_directions方向数,如果取2表示双向(bidirectional,),取1表示单向。
output_t的形状为(seq_len, batch, num_directions * hidden_size), h_n的形状为(num_layers * num_directions, batch, hidden_size)。
为使大家对循环神经网络有个直观理解,下面先用Pytorch实现简单循环神经网络,然后验证其关键要素。
首先建立一个简单循环神经网络,输入维度为10,隐含状态维度为20,单向两层网络。

因输入节点与隐含层节点是全连接,根据输入维度、隐含层维度,可以推算出相关权重参数的维度,w_ih应该是20x10,w_hh是20x20, b_ih和b_hh都是hidden_size。以下我们通过查询weight_ih_l0、weight_hh_l0等进行验证。

RNN网络已搭建好,接下来将输入(x_t 、h_0)传入网络,根据网络配置及网络要求,我们生成输入数据。输入特征长度为100,批量大小为32,特征维度为10的张量。隐含状态按网络要求,其形状为(2,32,20)。

将输入数据传入RNN网络,将得到输出及更新后隐含状态值。根据以上规则,输出output的形状应该是(100,32,20),隐含状态的输出形状应该与输入的形状一致。

其结果与我们设想的完全一致。

3 神经网络语言模型

  n-gram语言模型简单明了,解释性较好,但有几个较大缺陷:
1、维度灾难,随着n值越大,单词组合数指数级增长,由此带来相关联合或条件概率大量为0;
2、因n一般不能超过3或4,这影响模型利用单词更多邻居信息;
3、n-gram使用单词组合的频度作为计算基础,这需要提前计算,且无法泛化到相似语句或相似单词的情况。
接下来将介绍的神经网络语言模型(NNLM),可有效避免n-gram的这些不足,NNLM使用哪些方法或技术来解决这些问题的呢?请看下节内容。

3.1 神经网络语言模型

  Yoshua Bengio团队在2003年在论文《A Neural Probabilistic Language Model》中提出神经网络语言模型(NNLM),可以说是后续神经网络语言模型的鼻祖,其创新点有以下几点,这些亮点也是它避免n-gram模型不足的重要方法。
1、使用词嵌入(word Embedding)代替单词索引;
2、使用神经网络计算概率
当然,这个NNLM还有很多不足,其中整个模型因使用softmax,tanh等激活函数,在面对较大的语料库时(如词汇量在几万、几百万、甚至更多)计时效率很低,而且模型有点繁琐不够简练,后续我们将介绍一些改进模型。

3.1.1 神经网络语言模型(NNLM)

Yoshua Bengio团队提出的这个NNLM的架构图1-2所示。

图1-2 神经网络架构
假设该模型训练的语料库的词汇量为|V|,语料库中每个单词w_i转换成词向量的大小维度为m。把每个单词w_i转换为词嵌入的矩阵为C,其形状为|V|xm。其过程如图1-3所示。

图1-3 通过矩阵C把词索引转换为词嵌入

整个网络架构用表达式表示:
y=b+Wx+U tanh(d+Hx)
其中Wx表示输入层与输出层有直接联系(图1-2中的虚线部分),如果不要这个链接,直接设置W为0即可,b是输出层的偏置向量,d是隐层的偏置向量,里面的x即是单词到特征向量的映射,计算如下:
x=(C(w_(t-1) ),C(w_(t-2) ),⋯,C(w_(t-n+1)))
其中C是一个矩阵,其形状为|V|xm
假设隐层的神经元个数为h,那么整个模型的参数可以细化为θ = (b, d, W, U, H, C)。下面各参数含义及形状:
b是词向量x到输出层的偏移量,维度为|V|
W是词向量x到输出层的权重矩阵,维度为|V|x(n−1)m
d是隐含层的偏移量,维度为h
H是输入x到隐含层的权重矩阵,形状为hx(n-1)m
U是隐含层到输出层的权重矩阵,形状为|V|xh

网络的第一层(输入层)是将C(w_(t-1) ),C(w_(t-2) ),⋯,C(w_(t-n+1))这已知的n-1和单词的词向量首尾相连拼接起来,形成(n-1)m的向量x。
网络的第二层(隐藏层)直接用d+Hx计算得到,d是一个偏置项。之后,用tanh作为激活函数。
网络的第三层(输出层)一共有|V|个节点,最后使用softmax函数将输出值y归一化成概率。
最后,用随机梯度下降法把这个模型优化出来就可以了。

3.1.2 NNLM的PyTorch实现

这样用一个简单实例,实现3.1.1节的计算过程。
1、导入需要的库或模块

2、定义语料库及预处理函数

3、构建模型

4、训练模型

5、运行结果
Epoch: 1000 cost = 0.113229
Epoch: 2000 cost = 0.015353
Epoch: 3000 cost = 0.004546
Epoch: 4000 cost = 0.001836
Epoch: 5000 cost = 0.000853
[['我喜欢苹果'], ['我爱运动'], ['我讨厌老鼠']] -> ['苹果', '运动', '老鼠']

3.1.3词嵌入特征

  在NNLM语言模型中,通过神经网络的线性/非线性转换以及激活函数(例如softmax)得到似然条件概率值。同时因为保存了隐层的系数矩阵,因此可以得到每个文本序列以及每个词的词嵌入(Word Embedding)。词嵌入一般通过神经网络能学习语料库中的一些特性或知识, 如捕获词之间的多种相似性。图1-4为在某语料库上训练得到的一个简单词嵌入矩阵,从这个特征我们可以看出,有些词是相近的,如男与女,国王与王后,苹果与橘子等,这些相似性是从语料库学习得到。如何从语料库中学习这些特征或知识?人们研究出多种有效方法,其中最著名的就是Word2vec。
另外我们可以把词嵌入这些特性,通过迁移方法,应用到下游项目中。

图1-4 词嵌入特征示意图
与从语料库中学习词嵌入类似,在视觉处理领域中,也是通过学习图像,把图像特征转换为编码,整个过程如下图1-5所示。只不过在视觉处理中我们一般不把学到的向量为词嵌入,而往往称之为编码。

图1-5 把图像转换为编码示意图

3.1.4 word2vec简介

  词嵌入(word Embedding)最早由 Hinton 于 1986 年提出的,可以克服独热表示的缺点。解决词汇与位置无关问题,可以通过计算向量之间的距离(欧式距离、余弦距离等)来体现词与词的相似性。其基本想法是直接用一个普通的向量表示一个词,此向量为:
[0.792, -0.177, -0.107, 0.109, -0.542, ...],常见维度50或100。用这种方式表示的向量,“麦克”和“话筒”的距离会远远小于“麦克”和“天气”的距离。
词嵌入表示的优点是解决了词汇与位置无关问题,不足是学习过程相对复杂且受训练语料的影响很大。训练这种向量表示的方法较多,常见的有LSA、PLSA、LDA、Word2Vec等,其中Word2Vec是Google在2013年开源的一个工具,Word2Vec是一款用于词向量计算的工具,同时也是一套生成词向量的算法方案。Word2Vec算法的背后是一个浅层神经网络,其网络深度仅为3层,所以,严格说Word2Vec并非深度学习范畴。但其生成的词向量在很多任务中都可以作为深度学习算法的输入,因此,在一定程度上可以说Word2Vec技术是深度学习在NLP领域的基础。训练Word2Vec主要有以下两种模型来训练得到:
1、CBOW模型
CBOW模型包含三层,输入层、映射层和输出层。其架构如图1-6。CBOW模型中的w(t)为目标词,在已知它的上下文w(t-2),w(t-1),w(t+1),w(t+2)的前提下预测词w(t)出现的概率,即:p(w/context(w))。 目标函数为:

图1-6 CBOW模型
CBOW模型训练其实就是根据某个词前后若干词来预测该词,这其实可以看成是多分类。最朴素的想法就是直接使用softmax来分别计算每个词对应的归一化的概率。但对于动辄十几万词汇量的场景中使用softmax计算量太大,于是需要用一种二分类组合形式的hierarchical softmax,即输出层为一棵二叉树。
2、Skip-gram模型
Skip-gram模型同样包含三层,输入层,映射层和输出层。其架构如图1-7。Skip-Gram模型中的w(t)为输入词,在已知词w(t)的前提下预测词w(t)的上下文w(t-2),w(t-1),w(t+1),w(t+2),条件概率写为:p(context(w)/w)。目标函数为:


图1-7 Skip-gram模型

1.序列建模概述


卷积神经网络(CNN)善于处理网格化数据,如图像、视频等,虽然也可处理序列化数据,但应该不是它的强项,因序列化数据往往长短不一,而且序列数据往往与数据的前后位置有关,如我们经常看到的文本就是典型的序列数据。
如何对序列数据构建模型?早期通常采用统计语言模型(),统计语言模型的计算使用链式法则,把联合概率转换为条件概率,但随着序列长度的增长,其复杂度会指数级增长,为简化统计语言模型的计算,人们利用马尔科夫假设,这个假设的内容是:任何一个词出现的概率只和它前面的 1 个或若干个词有关。基于这个假设,人们提出n元语法(n -gram)模型。n -gram中的“n”很重要,它表示任何一个词出现的概率,只和它前面的 n-1 个词有关。
n元语法( n -gram),时间步t的词wt基于前面所有词的条件概率只考虑了最近时间步的n−1个词。如果要考虑比t−(n−1)更早时间步的词对wt的可能影响,我们需要增大n。但这样模型参数的数量将随之呈指数级增长,而一个词的语义往往跟比较长,为此,必须另辟蹊径。
n-gram 的优点有很多,首先它是一种直观的自然语言理解与处理方式,对参数空间进行了优化,并具有很强的解释性。它包含前 n-1 个词的全部信息,不会产生丢失和遗忘。除此以外,它还具有计算逻辑简单的优点 。
但与此同时,n-gram 也具有本质上的缺陷:n-gram 无法建立长期依赖,当 n 过大时仍会被数据的稀疏性严重影响,实际使用中往往仅使用 bigram 或 trigram;n-gram 基于频次进行统计,没有足够的泛化能力。因此,神经网络语言模型逐渐取代传统的统计自然语言模型成为主流,接下来我们对神经网络语言模型进行介绍。
首先实现这个转换的就是AI大咖Bengio ,在2003年发表在JMLR上发表了题为《A Neural Probabilistic Language Model》论文,在这篇具有开创性的论文中,有两个对NLP影响深远的创新点。一个是词向量,另一个是使用神经网络实现条件概率的计算。
不过Bengio提出的这个神经网络语言模型(NNLM),主要是实现和优化n-gram模型,除计算效率不高外,对序列基于固定长度,这点严重限制了NNLM的进一步发展。为解决长度可变及实现更长记忆,人们利用循环神经网络(RNN)来解决序列数据建模问题。利用循环神经网络无需刚性地记忆所有固定长度的序列,而是通过隐藏状态来存储(或记忆)之前时间步的信息,当然,由于RNN存在梯度消失问题,这种记忆一般是短时的,无法记忆较长的依赖关系,这点严重影响模型的性能。
解决长距离记忆问题,人们又提出一种RNN的几种改进模型,如LSTM模型、GRU模型等,这些模型中隐含状态的生成不像RNN一次性生成方法隐含状态,而是在原来内容的基础上添加一些新的内容。
虽然LSTM解决了序列模型的长距离依赖问题,但其计算只能严格从左到到右,或从右到左,无法实现并发处理。当前为了提升模型性能所要求的语料库也愈来愈大,这严重影响计算效率。
为了解决计算效率和长期依赖问题,最近人们又提出Transformer架构,使用这种架构既可解决长期依赖问题,又可很好的使用并发处理,从而极大提升模型性能及训练效率。在Transformer的基础上,人们提出了GPT、BERT、T5等预训练模型。这些模型在NLP领域打破了很多历史记录。
这里我们对序列建模的历史做了一个简单介绍,不涉及很多细节,接下来从业务场景、模型分解、可视化模型、模型拓展、模型实现等多个角度、多种方式、多个层次分别进行说明。

2统计语言模型

2.1统计语言模型

统计语言模型从统计的角度预测标记序列的概率分布,根据模型的设计,标记可以是词、字符等,这里我们以标记为词为例,对于句子ω_1,ω_2,⋯,ω_n的概率p(ω_1,ω_2⋯ω_n ),根据概率的链式法则,可求导整个句子的概率:
p(ω_1,ω_2⋯ω_n )=p(ω_1 )p(ω_2 |ω_1)p(ω_3 |ω_1,ω_2)⋯p(ω_n |ω_1,⋯ω_(n-1)) (1.1)
如何计算p(ω_n |ω_1,⋯ω_(n-1))?根据大数定律,该条件概率可利用极大似然估计来计算。
即,p(ω_n│ω_1,⋯ω_(n-1) )=(C(ω_1,⋯ω_(n-1),ω_n))/(C(ω_1,⋯ω_(n-1))) (1.2)
其中,C(.)表示子序列在语料库或数据集上的出现的次数。
这种计算方法存在两个致命的缺陷:一个缺陷是参数空间过大;另外一个缺陷是数据稀疏严重,长序列出现的频率非常低。
为了解决这个问题,通常引入了马尔科夫假设:一个词的出现仅仅依赖于它前面1个或有限的几个词,这就是接下来将介绍的n-gram模型。

2.2 n-gram模型

n-gram 是最为普遍的统计语言模型。它的基本思想是将文本里面的内容进行大小为 n 的滑动窗口操作,形成长度为 n 的短语子序列,利用最大似然估计,把条件概率的计算转换为对所有短语子序列的出现频度的计算。 直观上理解,n-gram 就是将句子长度缩短为只考虑前 n-1 个词。
如果一个词的出现不依赖于它前面的词,即句子中各词为相互独立,那么我们就称之为一元语法(或unigram),式(1.1)可表示为:
p(ω_1,ω_2⋯ω_n )=p(ω_1 )p(ω_2)p(ω_3)⋯p(ω_n)
如果一个词的出现仅依赖于它前面一个词,那么我们就称之为二元语法(或bigram)。式(1.1)可表示为:
p(ω_1,ω_2⋯ω_n )=p(ω_1 )p(ω_2 |ω_1)p(ω_3 |ω_2)⋯p(ω_n |ω_(n-1))
如果一个词的出现仅依赖于它前面出现的两个词,那么我们就称之为三元语法(或trigram)。式(1.1)可表示为:
p(ω_1,ω_2⋯ω_n )=p(ω_1 )p(ω_2 |ω_1)p(ω_3 |〖ω_1,ω〗_2)⋯p(ω_n |〖ω_(n-2),ω〗_(n-1))
在实践中用的最多的就是bigram和trigram了,而且效果很不错。高于四元的用的较少,因为训练它需要更庞大的语料,而且数据稀疏严重,时间复杂度高,精度却提高的不多。
这里举一个小数量的例子进行辅助说明:假设我们有一个语料库,如下:

<s>表示一个语句的开始,</s>表示一个语句的结束。
想要预测“我喜欢”这一句话的下一个字。我们分别通过 trigram 进行预测
通过 trigram,便是要对 P(w | 我喜欢)进行计算,经统计,“我喜欢她”出现了2次,“我喜欢车”出现了1次,通过最大似然估计可以求得P(她|我喜欢)=2/3,P(车|我喜欢)=1/3, 因此我们通过 bigram 预测出的整句话为: 我喜欢她

随着n的提升,我们拥有了更多的前置信息量,可以更加准确地预测下一个词。但这也带来了一个问题,数据随着n的提升变得更加稀疏了,导致很多预测概率结果为0。当遇到零概率问题时,我们可以通过平滑来缓解 n-gram 的稀疏问题。

2.3 n-gram的简单应用

搜索引擎(Baidu或者Google)、或者输入法的猜想或者提示。你在用百度时,输入一个或几个词,搜索框通常会以下拉菜单的形式给出几个像下图1-1一样的备选内容,这些备选内容其实是在猜想你想要搜索的那个词串。


图1-1 在百度输入关键字
图1-1中出现的词串(或短语)的排序,通过上面的介绍,你或许已发现,这其实是以n-gram模型为基础来实现的。

3 神经语言模型

3.2 循环神经网络语言模型

3.2.1 RNNCell架构
3.2.2 RNNCell代码实现
3.2.3 多层RNNCell
3.2.4 多层RNNCell代码实现

3.3 RNN生成文本实例

3.4 优化RNN

3.4.1 LSTMCELL
3.4.2 LSTMCell代码实现
3.4.3 多层LSTMCell结构
3.4.4 多层LSTMCell代码实现
3.4.5 LSTM架构
3.4.6 LSTM代码实现
3.4.7 biRNN

4 RNN应用实例(即将完成)

5 RNN拓展(即将完成)

5.1 ELMO
5.2 Encoder-Decoder
5.2 Attention and self-Attention
5.3 Transformer简介
5.4 BERT简介
5.5 GPT简介


BERT以Transformer的Encoder为架构,已MLM为模型,在很多领域取得历史性的的突破。这里以Transformers上基于中文语料库上训练的预训练模型bert-base-chinese为模型,以BertForSequenceClassification为下游任务模型,在一个中文数据集上进行语句分类。具体包括如下内容:
 使用BERT的分词库Tokenizer
 可视化BERT注意力权重
 用BERT预训练模型微调下游任务
 训练模型

14.1 背景说明

本章用到预训练模型库Transformers,Transformers为自然语言理解(NLU)和自然语言生成(NLG)提供了最先进的通用架构(BERT、GPT、GPT-2、Transformer-XL、XLNET、XLM、T5等等),其中有超过32个100多种语言的预训练模型并同时支持TensorFlow 2.0和Pythorch1.0两大深度学习框架。可用pip安装Transformers。

Transformers的官网:https://github.com/huggingface
这章使用BERT模型中汉语版本:BERT-Base, Chinese: 包括简体和繁体汉字,共12层,768个隐单元,12个Attention head,110M参数。中文 BERT 的字典大小约有 2.1 万个标识符(tokens),这些预训练模型可以从Transformers官网下载。
使用了可视化工具BertViz,它的安装步骤如下:
1.下载bertviz:
https://github.com/jessevig/bertviz
2.解压到jupyter notebook当前目录下
bertviz-master

14.1.1 查看中文BERT字典里的一些信息

1.导入需要的库
指定使用预训练模型bert-base-chinese。

2. 查看tokenizer的信息

运行结果:
字典大小: 21128
3.查看分词的一些信息

运行结果:
token index
------------------------------
##san 10978
王 4374
##and 9369
蚀 6008
60 8183

BERT 使用当初 Google NMT 提出的 WordPiece Tokenization ,将本来的 words 拆成更小粒度的 wordpieces,有效处理不在字典里头的词汇 。中文的话大致上就像是 character-level tokenization,而有 ## 前缀的 tokens 即为 wordpieces。
除了一般的wordpieces以外,BERT还有5个特殊tokens:
 [CLS]:在做分类任务时其最后一层的表示.会被视为整个输入序列的表示;
 [SEP]:有两个句子的文本会被串接成一个输入序列,并在两句之间插入这个token作为分割;
 [UNK]:没出现在BERT字典里头的字会被这个token取代;
 [PAD]:zero padding掩码,将长度不一的输入序列补齐方便做batch运算;
 [MASK]:未知掩码,仅在预训练阶段会用到。

14.1.2 使用Tokenizer分割中文语句

让我们利用中文BERT的tokenizer将一个中文句子断词。

运行结果:
[CLS] 他移开这[MASK]桌子,就看到他的手表了。
['[CLS]', '他', '移', '开', '这', '[MASK]', '桌', '子', ',', '就'] ...
[101, 800, 4919, 2458, 6821, 103, 3430, 2094, 8024, 2218] ...

14.2 可视化BERT注意力权重

现在马上让我们看看给定上面有 [MASK] 的句子,BERT会填入什么字。

14.2.1 BERT对MAKS字的预测

运行结果:
輸入 tokens : ['[CLS]', '他', '移', '开', '这', '[MASK]', '桌', '子', ',', '就'] ...
--------------------------------------------------
Top 1 (83%):['[CLS]', '他', '移', '开', '这', '张', '桌', '子', ',', '就'] ...
Top 2 ( 7%):['[CLS]', '他', '移', '开', '这', '个', '桌', '子', ',', '就'] ...
Top 3 ( 0%):['[CLS]', '他', '移', '开', '这', '间', '桌', '子', ',', '就'] ...

BERT透过关注这桌这两个字,从2万多个wordpieces的可能性中选出"张"作为这个情境下[MASK] token的预测值,效果还是不错的。

14.2.2 导入可视化需要的库

1.导入需要的库

2.创建可视化使用html配置函数

14.2.3 可视化

运行结果:

图14-1 某词对其它词注意力权重示意图
这是BERT第 10 层 Encoder block 其中一个 head 的注意力结果,从改图可以看出,左边的这个他对右边的“宏”字关注度较高。