0%

Sahlgren M. An Introduction to Random Indexing[C]// Methods & Applications of Semantic Indexing Workshop at International Conference on Terminology & Knowledge Engineering. 2005:194–201.

论文信息

文章作者为 Sahlgren M

论文概述

本文主要内容分为了 4 段,分别为:

  • The word space methodology
  • Problems and solutions
  • Random Indexing
  • Results

文章从文本空间讲起,简述了使用向量表示词的作用。接着以 LSA 加上 SVD 降维为例,简单说明了传统词向量表示算法的一些局限性(向量维度依然过大,计算代价大等),引出了 Random Indexing 算法。

Random Indexing

文中描述: • First, each context (e.g. each document or each word) in the data is assigned a unique and randomly generated representation called an index vector. These index vectors are sparse, high-dimensional, and ternary, which means that their dimensionality (d) is on the order of thousands, and that they consist of a small number of randomly distributed +1s and -1s, with the rest of the elements of the vectors set to 0. • Then, context vectors are produced by scanning through the text, and each time a word occurs in a context (e.g. in a document, or within a sliding context window), that context's d-dimensional index vector is added to the context vector for the word in question. Words are thus represented by d-dimensional context vectors that are effectively the sum of the words' contexts.

结合下面的论文提到的解释理解 Random Indexing algorithm。

论文 2 -- 熊玮, 白越, 刘爱国,等. 基于改进RI方法的文本聚类[J]. 南昌大学学报(理科版), 2016, 40(5):426-430.

第一步:生成随机索引向量

为正文、单词生成随机索引向量。这些随机索引向量是稀疏、高维的。随机索引向量的值可以为 (-1, +1, 0) 三种。大多数的向量值都为 0,只有少数向量值为 -1 和 +1。在论文 2 中提到随机索引向量可以使用二元组 $ (d,) \(表示。 其中,\)d$ 为向量维度,\(\epsilon\)为不同索引向量元素数量参数。对于所有文本来说,它们向量空间中出现的 -1 与 +1 的数量是相同的,在 \(d\) 确定后由 \(\epsilon\) 决定它们出现的数量。 令文本集全集为 \(W\),文本集的子集(单词)为$j W,j {1,2,3,...,n} $,此时生成的随机索引向量为 $ R{_j} = (r_1^j,r_2^j,r_3^j,...,r_d^j ) $,其中 \(r\nu_{h^j} \in \{+1,-1,0\}, h \in \{1,2,3,...,d\}\)\(\epsilon\)的取值远小于\(d\)。总体来说,+1 与 -1 分别占随机索引向量总维度的概率为 \(\frac{\epsilon /2}{d}\),显然有 \[\frac{\epsilon /2}{d} + \frac{d - \epsilon}{d} + \frac{\epsilon /2}{d} = 1\]

第二步:生成文本向量

根据滑动窗口包含的上下文生成上下文向量,接着根据上下文向量计算文本向量。 在论文 2 中,这一步又分为了两步: #### 生成特征词汇的上下文向量 设滑动窗口大小为 2L,则窗口范围为 [-L, L]。记特征词 \(\omega_j\) 在文本 \(d_i\) 中的上下文向量为 \(c^i_j\),则其表达式为:

\[c^i_j = \sum^{k = L}_{k = -L}Rv_{j+k} * \omega f(\omega_{j+k})\]

其中 \(Rv_{j+k}\) 表示特征词 \(\omega_j\) 在窗口范围内共现词 \(\omega_{j+k}\) 对应的随机索引向量; \(\omega f(\omega_{j+k})\) 为特征词 \(\omega_j\) 在窗口范围上下文中共现特征词 \(\omega_{j+k}\) 在文本 \(d_i\) 中的加权权重。论文 2 中采用了 tf-idf 加权计算算法。论文 2 此时引用了 > Gorman J, Curran J R. Random Indexing using Statistical Weight Functions[C]// EMNLP 2007, Proceedings of the 2006 Conference on Empirical Methods in Natural Language Processing, 22-23 July 2006, Sydney, Australia. DBLP, 2006:457-464.

根据 tf-idf 加权算法,可以得到 \(\omega f(\omega_{j+k})\) 的表达式:

\[\omega f(\omega_{j+k}) = \frac{f(\omega,\omega')}{n(\omega')} = \frac{n(\omega,\omega')}{n(\omega) * n(\omega')}\]

其中 \(n(\omega)\) 表示特征词汇 \(\omega\) 在上下文中出现的数量,\(n(\omega,\omega')\) 表示上下文中 \(\omega\)\(\omega'\) 共同出现的数量。

最终,某个特征词汇 \(\omega_j\) 在滑动窗口上下文中的上下文向量表示为

\[ C_j = \sum^{n}_{i=1} c^i_j\]

生成文本向量

  • 计算文本集中所有特征词汇上下文向量的平均值

\[\tau = \frac{\sum^n_{i=1}\sum^{z_i}_{j=1}Cj}{m}\]

其中 \(z_i\) 表示文档 \(d_i\) 中特征词汇总数,m 表示文 本集中所有不同特征词汇的总数量,n 表示文本集的文本总数量。

  • 生成文档 \(d_i\) 的文本向量

\[V_i =\frac{\sum^{z_i}_{j=1}C_j}{z_i} - \tau\]

Result

文章最后总结了一些经典数据集与实验应用 RI 算法之后准确率大多有所上升。论文 2 中最终总结了 RI 算法的优缺点。

Advantages

  • 计算量小
  • 容易实现
  • 处理效率高
  • 潜在语义表现好,利用了上下文信息表示特征词的词向量,容易解决同义词、近义词等问题
  • 降维性能好

Disadvantages

  • 随机向量元素 (-1, +1, 0) 的随机性可能导致在计算特征词上下文向量时发生相加消减的情况,导致潜在语义信息丢失
  • 论文 2 中选用的 tf-idf 计算出的加权值过小(此条仅对全文特征向量计算而言)

总结

了解了 Random Indexing algorithm 的基本原理及应用。之后有精力希望能将 RI 算法的代码实现,并将其与其它词向量表示算法进行对比。

在文档的头部加上如下代码:

1
2
3
4
~~ <script type="text/x-mathjax-config">
~~ MathJax.Hub.Config({tex2jax: {inlineMath:[['$latex','$']]}});
~~ </script>
~~ <script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>

即可在 Ulysses 中正常使用 Latex 公式啦。

输入

1
~~ $latex a = frac{1}{b} $

得到

upload successful

试试在 wp 里 latex 的显示:

~~ $latex a = frac{1}{b} $

除了多了两波浪号没别的问题,手动去掉吧。

自早期的工业时代以来,人类就被能自主操作的设备迷住了。因为,它们代表了科技的“人化”。

而在今天,各种软件也在逐渐变得人性化。其中变化最明显的当属“聊天机器人”。

但是这些“机械”是如何运作的呢?首先,让我们回溯过去,探寻一种原始,但相似的技术。

音乐盒是如何工作的

upload successful

早期自动化的样例 —— 机械音乐盒。 一组经过调音的金属齿排列成梳状结构,置于一个有针的圆柱边上。每根针都以一个特定的时间对应着一个音符。

当机械转动时,它便会在预定好的时间通过单个或者多个针的拨动来产生乐曲。如果要播放不同的歌,你得换不同的圆柱桶(假设不同的乐曲对应的特定音符是一样的)。

除了发出音符之外,圆筒的转动还可以附加一些其它的动作,例如移动小雕像等。不管怎样,这个音乐盒的基本机械结构是不会变的。

聊天机器人是如何工作的

输入的文本将经过一种名为“分类器”的函数处理,这种分类器会将一个输入的句子和一种“意图”(聊天的目的)联系起来,然后针对这种“意图”产生回应。

upload successful

一个聊天机器人的例子

你可以将分类器看成是将一段数据(一句话)分入几个分类中的一种(即某种意图)的一种方式。输入一句话“how are you?”,将被分类成一种意图,然后将其与一种回应(例如“I’m good”或者更好的“I am well”)联系起来。

我们在基础科学中早学习了分类:黑猩猩属于“哺乳动物”类,蓝鸟属于“鸟”类,地球属于“行星”等等。

一般来说,文本分类有 3 种不同的方法。可以将它们看做是为了一些特定目的制造的软件机械,就如同音乐盒的圆筒一样。

聊天机器人的文本分类方法

  • 模式匹配
  • 算法
  • 神经网络

无论你使用哪种分类器,最终的结果一定是给出一个回应。音乐盒可以利用一些机械机构的联系来完成一些额外的“动作”,聊天机器人也如此。回应中可以使用一些额外的信息(例如天气、体育比赛比分、网络搜索等等),但是这些信息并不是聊天机器人的组成部分,它们仅仅是一些额外的代码。也可以根据句子中的某些特定“词性”来产生回应(例如某个专有名词)。此外,符合意图的回应也可以使用逻辑条件来判断对话的“状态”,以提供一些不同的回应,这也可以通过随机选择实现(好让对话更加“自然”)。

模式匹配

早期的聊天机器人通过模式匹配来进行文本分类以及产生回应。这种方法常常被称为“暴力法”,因为系统的作者需要为某个回应详细描述所有模式。

这些模式的标准结构是“AIML”(人工智能标记语言)。这个名词里用了“人工智能”作为修饰词,但是它们完全不是一码事

下面是一个简单的模式匹配定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<aiml version = "1.0.1" encoding = "UTF-8"?>
<category>
<pattern> WHO IS ALBERT EINSTEIN </pattern>
<template>Albert Einstein was a German physicist.</template>
</category>

<category>
<pattern> WHO IS Isaac NEWTON </pattern>
<template>Isaac Newton was a English physicist and mathematician.</template>
</category>

<category>
<pattern>DO YOU KNOW WHO * IS</pattern>
<template>
<srai>WHO IS <star/></srai>
</template>
</category>
</aiml>

然后机器经过处理会回答:

Human: Do you know who Albert Einstein is
Robot: Albert Einstein was a German physicist.

它之所以知道别人问的是哪个物理学家,只是靠着与他或者她名字相关联的模式匹配。同样的,它靠着创作者预设的模式可以对任何意图进行回应。在给予它成千上万种模式之后,你终将能看到一个“类人”的聊天机器人出现。

2000 年的时候,John Denning 和他的同事就以这种方法做了个聊天机器人(相关新闻),并通过了“图灵测试”。它设计的目标是模仿来自乌克兰的一个 13 岁的男孩,这孩子的英语水平很蹩脚。我在 2015 年的时候和 John 见过面,他没有矢口否认这个自动机的内部原理。因此,这个聊天机器人很可能就是用“暴力”的方法进行模式匹配。但它也证明了一点:在足够大的模式匹配定义的支持下,可以让大部分对话都贴近“自然”的程度。同时也符合了图灵(Alan Turing)的断言:制作用来糊弄人类的机器是“毫无意义”的。

使用这种方法做机器人的典型案例还有 PandoraBots,他们宣称已经用他们的框架构建了超过 28.5 万个聊天机器人。

算法

暴力穷举法做自动机让人望而却步:对于每个输入都得有可用的模式来匹配其回应。人们由“老鼠洞”得到灵感,创建了模式的层级结构。

我们可以使用算法这种方法来减少分类器以便对机器进行管理,或者也可以说我们为它创建一个方程。这种方法是计算机科学家们称为“简化”的方法:问题需要缩减,那么解决问题的方法就是将其简化。

有一种叫做“朴素贝叶斯多项式模型”的经典文本分类算法,你可以在这儿或者别的地方学习它。下面是它的公式:

upload successful

实际用起它来比看上去要简单的多。给定一组句子,每个句子对应一个分类;接着输入一个新的句子,我们可以通过计算这个句子的单词在各个分类中的词频,找出各个分类的共性,并给每个分类一个分值(找出共性这点是很重要的:例如匹配到单词“cheese”(奶酪)比匹配到单词“it”要有意义的多)。最后,得到最高分值的分类很可能就是输入句子的同类。当然以上的说法是经过简化的,例如你还得先找到每个单词的词干才行。不过,现在你应该对这种算法已经有了基本的概念。

下面是一个简单的训练集:

class: weather
    "is it nice outside?"
    "how is it outside?"
    "is the weather nice?"

class: greeting
    "how are you?"
    "hello there"
    "how is it going?"

让我们来对几个简单的输入句子进行分类:

input: "Hi there"
 term: "hi" (**no matches)**
 term: "there" **(class: greeting)**
 classification: **greeting **(score=1)

input: "What’s it like outside?"
 term: "it" **(class: weather (2), greeting)**
 term: "outside **(class: weather (2) )**
 classification: **weather **(score=4)

请注意,“What’s it like outside”在分类时找到了另一个分类的单词,但是正确的分类给了单词较高的分值。通过算法公式,我们可以为句子计算匹配每个分类对应的词频,因此不需要去标明所有的模式。

这种分类器通过标定分类分值(计算词频)的方法给出最匹配语句的分类,但是它仍然有局限性。分值与概率不同,它仅仅能告诉我们句子的意图最有可能是哪个分类,而不能告诉我们它的所有匹配分类的可能性。因此,很难去给出一个阈值来判定是接受这个得分结果还是不接受这个结果。这种类型的算法给出的最高分仅仅能作为判断相关性的基础,它本质上作为分类器的效果还是比较差的。此外,这个算法不能接受 is not 类型的句子,因为它仅仅计算了 it 可能是什么。也就是说这种方法不适合做为包含 not 的否定句的分类。

有许多的聊天机器人框架都是用这种方法来判断意图分类。而且大多数都是针对训练集进行词频计算,这种“幼稚”的方法有时还意外的有效。

神经网络

人工神经网络发明于 20 世纪 40 年代,它通过迭代计算训练数据得到连接的加权值(“突触”),然后用于对输入数据进行分类。通过一次次使用训练数据计算改变加权值以使得神经网络的输出得到更高的“准确率”(低错误率)。

upload successful

上图为一种神经网络结构,其中包括神经元(圆)和突触(线)

其实除了当今的软件可以用更快的处理器、更大的内存外,这些结构并没有出现什么新奇的东西。当做数十万次的矩阵乘法(神经网络中的基本数学运算)的时候,运行内存和计算速度成为了关键问题。

在前面的方法里,每个分类都会给定一些例句。接着,根据词干进行分句,将所有单词作为神经网络的输入。然后遍历数据,进行成千上万次迭代计算,每次迭代都通过改变突触权重来得到更高的准确率。接着反过来通过对训练集输出值和神经网络计算结果的对比,对各层重新进行计算权重(反向传播)。这个“权重”可以类比成神经突触想记住某个东西的“力度”,你能记住某个东西是因为你曾多次见过它,在每次见到它的时候这个“权重”都会轻微地上升。

有时,在权重调整到某个程度后反而会使得结果逐渐变差,这种情况称为“过拟合”,在出现过拟合的情况下继续进行训练,反而会适得其反。

upload successful

训练好的神经网络模型的代码量其实很小,不过它需要一个很大的潜在权重矩阵。举个相对较小的样例,它的训练句子包括了 150 个单词、30 种分类,这可能产生一个 150x30 大小的矩阵;你可以想象一下,为了降低错误率,这么大的一个矩阵需要反复的进行 10 万次矩阵乘法。这也是为什么说需要高性能处理器的原因。

神经网络之所以能够做到既复杂又稀疏,归结于矩阵乘法和一种缩小值至 -1,1 区间的公式(即激活函数,这里指的是 Sigmoid),一个中学生也能在几小时内学会它。其实真正困难的工作是清洗训练数据。

就像前面的模式匹配和算法匹配一样,神经网络也有各种各样的变体,有一些变体会十分复杂。不过它的基本原理是相同的,做的主要工作也都是进行分类。

机械音乐盒并不了解乐理,同样的,聊天机器人并不了解语言

聊天机器人实质上就是寻找短语集合中的模式,每个短语还能再分割成单个单词。在聊天机器人内部,除了它们存在的模式以及训练数据之外的单词其实并没有意义。为这样的“机器人”贴上“人工智能”的标签其实也很糟糕

总结:聊天机器人就像机械音乐盒一样:它就是一个根据模式来进行输出的机器,只不过它不用圆筒和针,而是使用软件代码和数学原理。

发布于掘金 https://juejin.im/post/599155d86fb9a03c467c151d

ionic3 修复了2.x 存在的 ion-select 组件的 interface 等 bug,因此对其进行升级。修改 package.json,删除 node_module 目录,在 npm I 的时候依次按照提示在 package.json 中将不符合版本的库改为兼容版本。

升级完成之后 build 时提示 webpackjsonp is not defined,翻阅 README 的 Change log 发现新版 cli 脚手架写的 webpack 配置有所改变,将公用部分使用 CommonsChunkPlugin 额外打了一个包,此包命名为 vendor。在 app 入口文件中引用该公用包解决问题。

CNN 是怎么学习的?学习了什么?

这篇文章是深度学习系列的一部分。你可以在这里查看第一部分,以及在这里查看第三部分。

upload successful

这一周,我们将探索卷积神经网络(CNN)的内部工作原理。你可能会问:在网络内部究竟发生了什么?它们是怎样学习的?

这门课程遵循自上而下的学习方法与理念。因此一般来说,我们在开始学习的时候就能立即玩到所有的模型,然后我们会逐渐深入其内部的工作原理。因此,本系列也将会逐渐深入探索神经网络的内部工作原理。现在仅仅是第二周,让我们朝着最终的目标迈进吧!

在上周,我在猫狗图像集上训练了 Vgg 16 模型。我想先聊一下为什么说使用预先训练好的模型是一种很好的方法。为了使用这些模型,首先你得要弄清楚这些模型到底学习的是什么。从本质上说,CNN 学习的是过滤器,并将学习到的过滤器应用于图像。当然,这些“过滤器”和你在 Instagram 里用的滤镜(英文也为“filter”)并不是一种东西,但它们其实有一些相同之处。CNN 会使用一个小方块遍历整张图片,通常将这个小方块称为“窗口”。接下来,网络会在图片中查找与过滤器匹配的图片内容。在第一层,网络可能只学习到了一些简单的事物(例如对角线)。在之后的每一层中,网络都将结合前面找到的特征,持续学习更加复杂的概念。单单听这些概念可能会让人比较迷糊,让我们直接来看一些例子。Zeiler and Fergus (2013) 为可视化 CNN 学习过程做出了一项很棒的工作。下图是他们在论文中用的 CNN 模型,赢得 Imagenet 竞赛的 Vgg16 模型就是基于这个模型做出来的。

upload successful

CNN,作者:Zeiler & Fergus (2013)

可能你现在会觉得这个图片很难懂,请不要慌!让我们先从我们可以在图中看到的东西说起吧。首先,输入图像是正方形,大小为 224x224 像素。我之前说的过滤器大小是 7x7 像素大小。该模型有一个输入层,7 个隐藏层以及一个输出层。输出层的“C”指的是模型的预测分类数量。现在让我们来了解 CNN 中最有趣的部分:这个神经网络在每一层中都学到了什么!

upload successful

上图为 CNN 的第二层。左边的图像代表了 CNN 的这层网络在右边的真实图片中学习到的内容。 在 CNN 的第二层中,你可以发现这个模型已经不仅仅是去提取对角线了,它找到了一些更有意思的形状特征。例如在第二排第二列的方块中,你可以看到模型正在提取圆形;还有,最后一个方块表明模型正在专注于识别图中的一个直角作为特征。

upload successful

上图为 CNN 的第三层。 在第三层中,我们可以看到模型已经开始学习一些更具体的东西。第一个方块中的图像表明模型已经能够识别出一些地理特征;第二排第二列的方块表明模型正在识别车轮;倒数第二个方块表明模型正在识别人类。

upload successful

CNN 的第四层与第五层

在最后,第四层与第五层保持前面模型越来越具体的趋势。第五层找到了对解决我们的猫狗问题非常有帮助的特征。与此同时,它还识别出了独轮车,以及鸟类、爬行动物的眼睛。请注意,这些图像仅仅展示了每一层学习到的东西的极小一部分。

希望上面的文字已经告诉了你为什么使用预先训练好的模型是很有用的。如果你想更多的了解这块领域的研究,你可以搜索“迁移学习”(transfer learning)的相关内容。虽然我们的猫狗问题训练集仅仅只有 25000 张图片,一个新的模型可能还无法从这些图片中学习到所有的特征,但我们的 Vgg16 模型已经相当“了解”怎么去识别猫和狗了。最后,通过“微调”(Finetuning) Vgg16 模型的最后一层,让其不再输出 1000 多种分类的概率,而是直接输出二分类 —— 猫和狗。

如果你对深度学习背后的数学知识感兴趣,Stanford’s CNN pages 是很好的参考材料。他们首次以“数学之美”解释了浅层神经网络。


微调及线性层(全连接层)

上周,我用这个预先训练好的 Vgg16 模型不能很自然的区分猫和狗这两个分类下的图片,而是提出了 1000 余种分类。此外,这个模型并不会直接输出“猫”和“狗”的分类,而是输出猫和狗的一些特定品种。那我们如何修改这个模型,让它能够有效地对猫和狗进行分类呢?

有种可选方案:手动将这些品种分到猫和狗中去,然后计算其概率之和。但是,这种做法会丢弃一些关键信息。例如,如果图片中只有一根骨头,但它很可能是一张属于狗的照片。如果我们仅查看这些品种分为猫狗的概率,前面提到的这种信息很可能会丢失。因此在模型的最后,我们加入一个线性层(全连接层),它将仅输出两种分类。实际上,Vgg16 模型的最后有 3 层全连接层。我们可以微调这些层,通过反向传播来训练它们。反向传播算法常常被人看成是一种抽象的魔法,但其实它只是简单应用链式求导法则。你可以暂时忽略这些数学上的细节,TensorFlow、Theano 和其它深度学习库已经帮你做好了这些工作。

如果你正在运行 Fast AI 课程 lesson 2 的 notebook,我建议你最好先只使用 notebook 的样例图片。如果你运行 p2 的实例,可能会由于保存、加载 numpy 数组将内存耗尽。


激活函数

前面我们讨论了网络最后的线性层(全连接层)。然而,神经网络的所有层都不是线性的。在神经网络计算出每个神经元的参数之后,我们需要将它们的计算结果作为参数输入到激活函数中。人工神经网络基本上由矩阵乘法组成,如果我们只使用线性计算的话,我们只能将它们一个个叠加在一起,并不能做成一个很深的网络。因此,我们会经常在网络的各层使用非线性的激活函数。通过将重重线性与非线性函数叠加在一起,理论上我们可以对任何事物进行建模。下面是三种最受欢迎的非线性激活函数:

  • Sigmoid (将值转换到 0,1 间)
  • TanH (将值转换到 -1,1 间)
  • ReLu (如果值为负则输出 0,否则输出原值)
upload successful

上图为最常用的激活函数:Sigmoid、Tanh 和 ReLu(又名修正线性单元) 目前,ReLu 是使用的最多的非线性激活函数,主要原因是它可以减少梯度消失的可能性,以及保持稀疏特征。稍后会讨论这方面的更多详情。因为我们希望模型最后能够输出确定的内容,因此模型的最后一层通常使用一种另外的激活函数 —— softmax。softmax 函数是一种非常受欢迎的分类器。

在微调完 Vgg16 模型的最后一层之后,它总共有 138357544 个参数。谢天谢地,我们不需要手动计算各种梯度 XD。下一周我们将更深入地了解 CNN 的工作原理,讨论主题为欠拟合和过拟合。

如果你喜欢这篇文章,请将它推荐给其他人吧!你也可以关注此系列文章,跟上 Fast AI 课程的进度。下篇文章再会!

发布于掘金 https://juejin.im/post/598ac6a55188257dd366367f

一些机器学习方法(例如深度学习)可以用于进行时间序列预测。

在使用这些机器学习方法前,必须先将时间序列预测问题转化为监督学习问题。也就是说,需要将一个时间序列转换成一组包含成对输入输出的序列。

在这篇教程里,你将了解如何将单变量时间序列预测问题和多变量时间序列预测问题转换成监督学习问题,以使用机器学习算法。

读完这篇教程,你将会了解:

  • 如何编写一个将时间序列数据集转换为监督学习数据集的函数。
  • 如何转换一元时间序列数据以使用机器学习。
  • 如何转换多元时间序列数据以使用机器学习。

让我们开始吧。

upload successful

题图:如何将时间序列问题用 Python 转换成为监督学习问题

Quim Gil 拍摄,版权所有。

时间序列 vs 监督学习

在正式开始之前,让我们先花点时间更好地了解一下时间序列和监督学习的数据集结构。

单个时间序列由一系列按照时间排序的数字序列组成。可以将其理解为一列有序值。

例如:

1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

而一个监督学习问题是由一组输入(X)和一组输出(y)组成,算法可以学会如何通过输入值来预测输出值。

例如:

1
2
3
4
5
6
7
8
9
X,  y
1 2
2, 3
3, 4
4, 5
5, 6
6, 7
7, 8
8, 9

可以参阅这篇文章,学习更多有关知识:

Pandas 的 shift() 函数

我们将时间序列数据转化为监督学习问题的关键就是使用 Pandas 的 shift() 函数。

给定一个 DataFrame,shift() 函数会将输入的列复制一份,然后将副本列整体往后移动(最前面的数据空位会用 NaN 填充)或者往前移动(最后面的数据空位会用 NaN 填充)。

这样可以创建一个滞后值列,加上观察列,就能将时间序列数据集变成监督学习数据集的格式。

让我们看看 shift 函数实际用起来效果如何。

我们可以通过下面的代码模拟一个长度为 10 的时间序列数据集,此时它在 DataFrame 中为单独的一列:

1
2
3
4
from pandas import DataFrame
df = DataFrame()
df['t'] = [x for x in range(10)]
print(df)

运行上面的样例,将时间序列数据输出,其每一行都为带有索引的观察组数据。

1
2
3
4
5
6
7
8
9
10
11
   t
0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9

我们可以在数据顶部插入一行,将观察组的数据整体下挪一位。由于最上面插入的新行没有数据,因此我们可以用 NaN 填充来表示这儿“没有数据”。

shift 函数可以完成这些操作。我们可以将 shift 函数“挪动”过的新列插入原始序列的旁边。

1
2
3
4
5
from pandas import DataFrame
df = DataFrame()
df['t'] = [x for x in range(10)]
df['t-1'] = df['t'].shift(1)
print(df)

运行上面的样例,你将得到一个包含两列的数据集。第一列是原始的观察组,第二列是经由 shift 函数挪动生成的新列。

可以看到,经过将序列移动一次的操作之后,我们得到了一个原始的监督学习问题(虽然此时的 Xy 的排序明显是错的)。忽略最前面的表头,第一行存在 NaN 值,因此需要将其丢弃。在第二行,我们可以将第二列的 0.0 作为输入值(也就是 X),将第一列的 1 作为输出值(或 y)。

1
2
3
4
5
6
7
8
9
10
11
   t  t-1
0 0 NaN
1 1 0.0
2 2 1.0
3 3 2.0
4 4 3.0
5 5 4.0
6 6 5.0
7 7 6.0
8 8 7.0
9 9 8.0

如果我们重复 shift 步骤,让原始列挪动 2 位、3 位或者更多位,我们就能得到一系列的输入数据(X),由这些输入值就能去预测输出值(y)了。

shift 操作能也能接受负整数作为参数。如果你这么做,它会在列底部插入新行,从而使得原列向上移动。下面是例子:

1
2
3
4
5
from pandas import DataFrame
df = DataFrame()
df['t'] = [x for x in range(10)]
df['t+1'] = df['t'].shift(-1)
print(df)

运行上面的样例,可以看到新列中的最后一个值为 NaN。

此时可以将预测列作为输入值(X),将第二列作为输出值(y)。也就是给定输入值 0 可以用于预测输出值 1。

1
2
3
4
5
6
7
8
9
10
11
   t  t+1
0 0 1.0
1 1 2.0
2 2 3.0
3 3 4.0
4 4 5.0
5 5 6.0
6 6 7.0
7 7 8.0
8 8 9.0
9 9 NaN

从技术上说,在时间序列预测问题的术语中,当前时间(t)和未来时间(t+1, t+n)为待预测时间,过去时间(t-1, t-n)则用于预测。

从上面的例子中,我们可以学会如何使用通过 shift 函数正向或反向移动序列,生成新的 DataFrame,将时间序列问题转变成监督学习问题的输入-输出模式。

这不仅可以解决经典的 X -> y 类预测问题,也可以用于输入输出值都是序列的 X -> Y 类预测。

另外,shift 函数也能用于多元时间序列问题中。这类问题中包含多列观察组(例如温度、气压等)。时间序列中的所有变量都能用通过向前或向后挪动,生成多元输入值与输出值序列。稍后我们将探讨这类问题。

series_to_supervised() 函数

我们可以使用 Pandas 的 shift() 函数,在给定希望得到的输入值、输出值序列长度后自动生成时间序列问题的新格式数据。

这是个很有用的工具。我们可以通过机器学习算法研究各种时间序列问题格式,探究哪种格式能够得到效果更佳的模型。

在本节中,我们将创建一个新的 Python 函数,名为 series_to_supervised()。它可以将多元时间序列问题与一元时间序列问题转换为监督学习数据集的格式。

这个函数接收以下 4 个参数:

  • data:必填,待转换的序列,数据类型为 list 或 2 维 NumPy array。
  • n_in: 可选,滞后组(作为输入值 X)的数量。范围可以在 [1..len(data)] 之间,默认值为 1。
  • n_out: 可选,观察组(作为输出值 y)的数量。范围可以在 [0..len(data)-1] 之间,默认值为 1。
  • dropnan:选填,决定是否抛去包含 NaN 的行。类型为 Boolean,默认值为 True。

函数将会返回一个值:

  • return:返回监督学习格式的数据集,数据类型为 Pandas DataFrame。

新数据集 DataFrame 格式,每一列都由原变量名称和移动步数命名,让你可以根据给定的一元或多元时间序列问题设计出各种移动步数的序列。

在 DataFrame 返回时,你可以对其行进行分割,根据你的需要决定如何将返回的 DataFrame 分成 X 和 y 两部分。

这个函数的参数都设置了默认值,因此可以直接调用它处理你的数据,这种默认情况它将会返回一个 t-1 作为 X,t 作为 y 的 DataFrame。

这个函数已确定同时兼容 Python2 和 Python3。

下面为完整代码,并写好了注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pandas import DataFrame
from pandas import concat

def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
"""
函数用途:将时间序列转化为监督学习数据集。
参数说明:
data: 观察值序列,数据类型可以是 list 或者 NumPy array。
n_in: 作为输入值(X)的滞后组的数量。
n_out: 作为输出值(y)的观察组的数量。
dropnan: Boolean 值,确定是否将包含 NaN 的行移除。
返回值:
经过转换的用于监督学习的 Pandas DataFrame 序列。
"""
n_vars = 1 if type(data) is list else data.shape[1]
df = DataFrame(data)
cols, names = list(), list()
# 输入序列 (t-n, ... t-1)
for i in range(n_in, 0, -1):
cols.append(df.shift(i))
names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]
# 预测序列 (t, t+1, ... t+n)
for i in range(0, n_out):
cols.append(df.shift(-i))
if i == 0:
names += [('var%d(t)' % (j+1)) for j in range(n_vars)]
else:
names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]
# 将所有列拼合
agg = concat(cols, axis=1)
agg.columns = names
# drop 掉包含 NaN 的行
if dropnan:
agg.dropna(inplace=True)
return agg

你觉得可以怎样提高这个函数的鲁棒性或者可读性吗?请留言在评论区。

至此我们已经得到了整个函数,接下来探索它的用法。

单步或单变量预测

在时间序列预测问题中通常使用滞后时间(例如 t-1)作为输入变量来预测当前时间(t)。

这种问题被称为单步预测。

下面展示了使用滞后一个时间步的时间(t-1)来预测当前时间(t)的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pandas import DataFrame
from pandas import concat

def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
"""
函数用途:将时间序列转化为监督学习数据集。
参数说明:
data: 观察值序列,数据类型可以是 list 或者 NumPy array。
n_in: 作为输入值(X)的滞后组的数量。
n_out: 作为输出值(y)的观察组的数量。
dropnan: Boolean 值,确定是否将包含 NaN 的行移除。
返回值:
经过转换的用于监督学习的 Pandas DataFrame 序列。
"""
n_vars = 1 if type(data) is list else data.shape[1]
df = DataFrame(data)
cols, names = list(), list()
# 输入序列 (t-n, ... t-1)
for i in range(n_in, 0, -1):
cols.append(df.shift(i))
names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]
# 预测序列 (t, t+1, ... t+n)
for i in range(0, n_out):
cols.append(df.shift(-i))
if i == 0:
names += [('var%d(t)' % (j+1)) for j in range(n_vars)]
else:
names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]
# 将所有列拼合
agg = concat(cols, axis=1)
agg.columns = names
# drop 掉包含 NaN 的行
if dropnan:
agg.dropna(inplace=True)
return agg

values = [x for x in range(10)]
data = series_to_supervised(values)
print(data)

运行样例,输出转换后的时间序列。

1
2
3
4
5
6
7
8
9
10
   var1(t-1)  var1(t)
1 0.0 1
2 1.0 2
3 2.0 3
4 3.0 4
5 4.0 5
6 5.0 6
7 6.0 7
8 7.0 8
9 8.0 9

可以看到,观察组被命名为“var1”,作为输入值的观察组被命名为(t-1),输出值组被命名为(t)。

此外,可以看到包含 NaN 的行已经被自动从 DataFrame 中移除。

我们可以任意给定输入序列数量的值来重复运行这个例子。例如输入 3,我们事先已经将输入序列的数量定义为了一个参数。例如:

1
data = series_to_supervised(values, 3)

完整样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pandas import DataFrame
from pandas import concat

def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
"""
函数用途:将时间序列转化为监督学习数据集。
参数说明:
data: 观察值序列,数据类型可以是 list 或者 NumPy array。
n_in: 作为输入值(X)的滞后组的数量。
n_out: 作为输出值(y)的观察组的数量。
dropnan: Boolean 值,确定是否将包含 NaN 的行移除。
返回值:
经过转换的用于监督学习的 Pandas DataFrame 序列。
"""
n_vars = 1 if type(data) is list else data.shape[1]
df = DataFrame(data)
cols, names = list(), list()
# 输入序列 (t-n, ... t-1)
for i in range(n_in, 0, -1):
cols.append(df.shift(i))
names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]
# 预测序列 (t, t+1, ... t+n)
for i in range(0, n_out):
cols.append(df.shift(-i))
if i == 0:
names += [('var%d(t)' % (j+1)) for j in range(n_vars)]
else:
names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]
# 将所有列拼合
agg = concat(cols, axis=1)
agg.columns = names
# drop 掉包含 NaN 的行
if dropnan:
agg.dropna(inplace=True)
return agg


values = [x for x in range(10)]
data = series_to_supervised(values, 3)
print(data)

再次运行样例,输出重新构造的序列,可以看到输入序列准确无误地从左至右裴烈,作为预测项的输入值在最右边。

1
2
3
4
5
6
7
8
   var1(t-3)  var1(t-2)  var1(t-1)  var1(t)
3 0.0 1.0 2.0 3
4 1.0 2.0 3.0 4
5 2.0 3.0 4.0 5
6 3.0 4.0 5.0 6
7 4.0 5.0 6.0 7
8 5.0 6.0 7.0 8
9 6.0 7.0 8.0 9

多步或序列预测

还有一类预测问题:使用过去的观察组来对未来的观察组序列做预测。

可以将这类问题成为序列预测问题或者多步预测问题。

我们可以通过规定另一个参数来将序列预测问题的时间序列重新构造。例如,我们可以把 2 个过去的观察组转变为 2 个未来的观察组,从而重新构造预测问题:

1
data=series_to_supervised(values,2,2)

完整样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pandas import DataFrame
from pandas import concat

def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
"""
函数用途:将时间序列转化为监督学习数据集。
参数说明:
data: 观察值序列,数据类型可以是 list 或者 NumPy array。
n_in: 作为输入值(X)的滞后组的数量。
n_out: 作为输出值(y)的观察组的数量。
dropnan: Boolean 值,确定是否将包含 NaN 的行移除。
返回值:
经过转换的用于监督学习的 Pandas DataFrame 序列。
"""
n_vars = 1 if type(data) is list else data.shape[1]
df = DataFrame(data)
cols, names = list(), list()
# 输入序列 (t-n, ... t-1)
for i in range(n_in, 0, -1):
cols.append(df.shift(i))
names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]
# 预测序列 (t, t+1, ... t+n)
for i in range(0, n_out):
cols.append(df.shift(-i))
if i == 0:
names += [('var%d(t)' % (j+1)) for j in range(n_vars)]
else:
names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]
# 将所有列拼合
agg = concat(cols, axis=1)
agg.columns = names
# drop 掉包含 NaN 的行
if dropnan:
agg.dropna(inplace=True)
return agg

values = [x for x in range(10)]
data = series_to_supervised(values, 2, 2)
print(data)

运行样例,可以看到将(t-n)作为输入变量、将(t+n)作为输出变量时,与将当前观察组(t)作为输出的不同之处。

1
2
3
4
5
6
7
8
9
   var1(t-2)  var1(t-1)  var1(t)  var1(t+1)
2 0.0 1.0 2 3.0
3 1.0 2.0 3 4.0
4 2.0 3.0 4 5.0
5 3.0 4.0 5 6.0
6 4.0 5.0 6 7.0
7 5.0 6.0 7 8.0
8 6.0 7.0 8 9.0

多元预测

还有一种重要的时间序列类型,叫做多元时间序列。

这种情况我们会将多个不同的指标作为观察组,并预测它们中的一个或多个的值。

例如,我们有两组时间序列观察组 obs1 和 obs2,希望预测它们或它们中的一者。

我们同样可以调用 series_to_supervised()。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pandas import DataFrame
from pandas import concat

def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
"""
函数用途:将时间序列转化为监督学习数据集。
参数说明:
data: 观察值序列,数据类型可以是 list 或者 NumPy array。
n_in: 作为输入值(X)的滞后组的数量。
n_out: 作为输出值(y)的观察组的数量。
dropnan: Boolean 值,确定是否将包含 NaN 的行移除。
返回值:
经过转换的用于监督学习的 Pandas DataFrame 序列。
"""
n_vars = 1 if type(data) is list else data.shape[1]
df = DataFrame(data)
cols, names = list(), list()
# 输入序列 (t-n, ... t-1)
for i in range(n_in, 0, -1):
cols.append(df.shift(i))
names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]
# 预测序列 (t, t+1, ... t+n)
for i in range(0, n_out):
cols.append(df.shift(-i))
if i == 0:
names += [('var%d(t)' % (j+1)) for j in range(n_vars)]
else:
names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]
# 将所有列拼合
agg = concat(cols, axis=1)
agg.columns = names
# drop 掉包含 NaN 的行
if dropnan:
agg.dropna(inplace=True)
return agg


raw = DataFrame()
raw['ob1'] = [x for x in range(10)]
raw['ob2'] = [x for x in range(50, 60)]
values = raw.values
data = series_to_supervised(values)
print(data)

运行样例,将会得到经过重新构造后的数据。数据显示了分别处于同一个时间的两组变量作为输入组以及输出组。

与之前一样,根据问题的需要,可以将列分入 Xy 两个子集中,需要注意的是如果放入了 var1 做为观察组,那就要放入 var2 作为待预测组。

1
2
3
4
5
6
7
8
9
10
   var1(t-1)  var2(t-1)  var1(t)  var2(t)
1 0.0 50.0 1 51
2 1.0 51.0 2 52
3 2.0 52.0 3 53
4 3.0 53.0 4 54
5 4.0 54.0 5 55
6 5.0 55.0 6 56
7 6.0 56.0 7 57
8 7.0 57.0 8 58
9 8.0 58.0 9 59

可以看到,通过上面这样给定输入序列和输出序列的数量生成的新的序列,可以帮助你轻松地完成多元时间序列的预测。

例如,下面将把 1 作为输入列数量,将 2 作为输出列(预测列)数量,重新构造预测序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from pandas import DataFrame
from pandas import concat

def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
"""
函数用途:将时间序列转化为监督学习数据集。
参数说明:
data: 观察值序列,数据类型可以是 list 或者 NumPy array。
n_in: 作为输入值(X)的滞后组的数量。
n_out: 作为输出值(y)的观察组的数量。
dropnan: Boolean 值,确定是否将包含 NaN 的行移除。
返回值:
经过转换的用于监督学习的 Pandas DataFrame 序列。
"""
n_vars = 1 if type(data) is list else data.shape[1]
df = DataFrame(data)
cols, names = list(), list()
# 输入序列 (t-n, ... t-1)
for i in range(n_in, 0, -1):
cols.append(df.shift(i))
names += [('var%d(t-%d)' % (j+1, i)) for j in range(n_vars)]
# 预测序列 (t, t+1, ... t+n)
for i in range(0, n_out):
cols.append(df.shift(-i))
if i == 0:
names += [('var%d(t)' % (j+1)) for j in range(n_vars)]
else:
names += [('var%d(t+%d)' % (j+1, i)) for j in range(n_vars)]
# 将所有列拼合
agg = concat(cols, axis=1)
agg.columns = names
# drop 掉包含 NaN 的行
if dropnan:
agg.dropna(inplace=True)
return agg

raw = DataFrame()
raw['ob1'] = [x for x in range(10)]
raw['ob2'] = [x for x in range(50, 60)]
values = raw.values
data = series_to_supervised(values, 1, 2)
print(data)

运行样例,将会展示重新构造的很大的 DataFrame。

1
2
3
4
5
6
7
8
9
   var1(t-1)  var2(t-1)  var1(t)  var2(t)  var1(t+1)  var2(t+1)
1 0.0 50.0 1 51 2.0 52.0
2 1.0 51.0 2 52 3.0 53.0
3 2.0 52.0 3 53 4.0 54.0
4 3.0 53.0 4 54 5.0 55.0
5 4.0 54.0 5 55 6.0 56.0
6 5.0 55.0 6 56 7.0 57.0
7 6.0 56.0 7 57 8.0 58.0
8 7.0 57.0 8 58 9.0 59.0

你可以用你自己的数据集多做几次实验,来试试哪种重构的效果更好。

总结

在这篇教程中,你已经了解了如何使用 Python 将时间序列数据集转换为监督学习问题。

特别的,你了解了:

  • 有关 Pandas shift() 函数的知识,以及它如何自动将时间序列数据转化为监督学习数据集。
  • 如何将一元时间序列重构成单步或多步监督学习问题。
  • 如何将多元时间序列重构成单步或多步监督学习问题。

简介

自然语言处理(NLP)是人工智能领域最重要的部分之一。它在许多智能应用中担任了关键的角色,例如聊天机器人、正文提取、多语翻译以及观点识别等应用。业界 NLP 相关的公司都意识到了,处理非结构文本数据时,不仅要看正确率,还需要注意是否能快速得到想要的结果。

NLP 是一个很宽泛的领域,它包括了文本分类、实体识别、机器翻译、问答系统、概念识别等子领域。在我最近的一篇文章中,我探讨了许多用于实现 NLP 的工具与组件。在那篇文章中,我更多的是在描述NLTK(Natural Language Toolkit)这个伟大的库。

在这篇文章中,我会将 spaCy —— 这个现在最强大、最先进的 NLP python 库分享给你们。


内容提要

  1. spaCy 简介及安装方法
  2. spaCy 的管道与属性
    • Tokenization
    • 词性标注
    • 实体识别
    • 依存句法分析
    • 名词短语
  3. 集成词向量计算
  4. 使用 spaCy 进行机器学习
  5. 与 NLTK 和 CoreNLP 对比

1. spaCy 简介及安装方法

1.1 简介

spaCy 由 cython(Python 的 C 语言拓展,旨在让 python 程序达到如同 C 程序一样的性能)编写,因此它的运行效率非常高。spaCy 提供了一系列简洁的 API 方便用户使用,并基于已经训练好的机器学习与深度学习模型实现底层。


1.2 安装

spaCy 及其数据和模型可以通过 pip 和安装工具轻松地完成安装。使用下面的命令在电脑中安装 spaCy:

sudo pip install spacy

如果你使用的是 Python3,请用 “pip3” 代替 “pip”。

或者你也可以在 这儿 下载源码,解压后运行下面的命令安装:

python setup.py install

在安装好 spacy 之后,请运行下面的命令以下载所有的数据集和模型:

python -m spacy.en.download all

一切就绪,现在你可以自由探索、使用 spacy 了。

2. spaCy 的管道(Pipeline)与属性(Properties)

spaCy 的使用,以及其各种属性,是通过创建管道实现的。在加载模型的时候,spaCy 会将管道创建好。在 spaCy 包中,提供了各种各样的模块,这些模块中包含了各种关于词汇、训练向量、语法和实体等用于语言处理的信息。

下面,我们会加载默认的模块(english-core-web 模块)。

import spacy
nlp = spacy.load(“en”)

“nlp” 对象用于创建 document、获得 linguistic annotation 及其它的 nlp 属性。首先我们要创建一个 document,将文本数据加载进管道中。我使用了来自猫途鹰网的旅店评论数据。这个数据文件可以在这儿下载。

document = unicode(open(filename).read().decode('utf8'))
document = nlp(document)

这个 document 现在是 spacy.english 模型的一个 class,并关联上了许多的属性。可以使用下面的命令列出所有 document(或 token)的属性:

dir(document)
>> [ 'doc', 'ents', … 'mem']

它会输出 document 中各种各样的属性,例如:token、token 的 index、词性标注、实体、向量、情感、单词等。下面让我们会对其中的一些属性进行一番探究。

2.1 Tokenization

spaCy 的 document 可以在 tokenized 过程中被分割成单句,这些单句还可以进一步分割成单词。你可以通过遍历文档来读取这些单词:

# document 的首个单词
document[0]
>> Nice

# document 的最后一个单词  
document[len(document)-5]
>> boston

# 列出 document 中的句子
list(document.sents)
>> [ Nice place Better than some reviews give it credit for.,
 Overall, the rooms were a bit small but nice.,
...
Everything was clean, the view was wonderful and it is very well located (the Prudential Center makes shopping and eating easy and the T is nearby for jaunts out and about the city).]

2.2 词性标注(POS Tag)

词性标注即标注语法正确的句子中的词语的词性。这些标注可以用于信息过滤、统计模型,或者基于某些规则进行文本解析。

来看看我们的 document 中所有的词性标注:

# 获得所有标注
all_tags = {w.pos: w.pos_ for w in document}
>> {97:  u'SYM', 98: u'VERB', 99: u'X', 101: u'SPACE', 82: u'ADJ', 83: u'ADP', 84: u'ADV', 87: u'CCONJ', 88: u'DET', 89: u'INTJ', 90: u'NOUN', 91: u'NUM', 92: u'PART', 93: u'PRON', 94: u'PROPN', 95: u'PUNCT'}

# document 中第一个句子的词性标注
for word in list(document.sents)[0]:  
    print word, word.tag_
>> ( Nice, u'JJ') (place, u'NN') (Better, u'NNP') (than, u'IN') (some, u'DT') (reviews, u'NNS') (give, u'VBP') (it, u'PRP') (creit, u'NN') (for, u'IN') (., u'.')

来看一看 document 中的最常用词汇。我已经事先写好了预处理和文本数据清洗的函数。

#一些参数定义
noisy_pos_tags = [“PROP”]
min_token_length = 2

#检查 token 是不是噪音的函数
def isNoise(token):     
    is_noise = False
    if token.pos_ in noisy_pos_tags:
        is_noise = True
    elif token.is_stop == True:
        is_noise = True
    elif len(token.string) <= min_token_length:
        is_noise = True
    return is_noise
def cleanup(token, lower = True):
    if lower:
       token = token.lower()
    return token.strip()

# 评论中最常用的单词
from collections import Counter
cleaned_list = [cleanup(word.string) for word in document if not isNoise(word)]
Counter(cleaned_list) .most_common(5)
>> [( u'hotel', 683), (u'room', 652), (u'great', 300),  (u'sheraton', 285), (u'location', 271)]

2.3 实体识别

spaCy 拥有一个快速实体识别模型,这个实体识别模型能够从 document 中找出实体短语。它能识别各种类型的实体,例如人名、位置、机构、日期、数字等。你可以通过“.ents”属性来读取这些实体。

下面让我们来获取我们 document 中所有类型的命名实体:

labels = set([w.label_ for w in document.ents])
for label in labels:
    entities = [cleanup(e.string, lower=False) for e in document.ents if label==e.label_]
    entities = list(set(entities))
    print label,entities

2.4 依存句法分析

spaCy 最强大的功能之一就是它可以通过调用轻量级的 API 来实现又快又准确的依存分析。这个分析器也可以用于句子边界检测以及区分短语块。依存关系可以通过“.children”、“.root”、“.ancestor”等属性读取。

# 取出所有句中包含“hotel”单词的评论
hotel = [sent for sent in document.sents if 'hotel' in sent.string.lower()]

# 创建依存树
sentence = hotel[2] for word in sentence:
print word, ': ', str(list(word.children))
>> A :  []  cab :  [A, from]
from :  [airport, to]
the :  []
airport :  [the]
to :  [hotel]
the :  [] hotel :  
[the] can :  []
be :  [cab, can, cheaper, .]
cheaper :  [than] than :  
[shuttles]
the :  []
shuttles :  [the, depending]
depending :  [time] what :  []
time :  [what, of] of :  [day]
the :  [] day :  
[the, go] you :  
[]
go :  [you]
. :  []

解析所有居中包含“hotel”单词的句子的依存关系,并检查对于 hotel 人们用了哪些形容词。我创建了一个自定义函数,用于分析依存关系并进行相关的词性标注。

# 检查修饰某个单词的所有形容词
def pos_words (sentence, token, ptag):
    sentences = [sent for sent in sentence.sents if token in sent.string]     
    pwrds = []
    for sent in sentences:
        for word in sent:
            if character in word.string:
                   pwrds.extend([child.string.strip() for child in word.children
                                                      if child.pos_ == ptag] )
    return Counter(pwrds).most_common(10)

pos_words(document, 'hotel', “ADJ”)
>> [(u'other', 20), (u'great', 10), (u'good', 7), (u'better', 6), (u'nice', 6), (u'different', 5), (u'many', 5), (u'best', 4), (u'my', 4), (u'wonderful', 3)]

2.5 名词短语(NP)

依存树也可以用来生成名词短语:

# 生成名词短语
doc = nlp(u'I love data science on analytics vidhya')
for np in doc.noun_chunks:
    print np.text, np.root.dep_, np.root.head.text
>> I nsubj love
   data science dobj love
   analytics pobj on

3. 集成词向量

spaCy 提供了内置整合的向量值算法,这些向量值可以反映词中的真正表达信息。它使用 GloVe 来生成向量。GloVe 是一种用于获取表示单词的向量的无监督学习算法。

让我们创建一些词向量,然后对其做一些有趣的操作吧:

from numpy import dot
from numpy.linalg import norm
from spacy.en import English
parser = English()

# 生成“apple”的词向量 
apple = parser.vocab[u'apple']

# 余弦相似性计算函数
cosine = lambda v1, v2: dot(v1, v2) / (norm(v1) * norm(v2))
others = list({w for w in parser.vocab if w.has_vector and w.orth_.islower() and w.lower_ != unicode("apple")})

# 根据相似性值进行排序
others.sort(key=lambda w: cosine(w.vector, apple.vector))
others.reverse()


print "top most similar words to apple:"
for word in others[:10]:
    print word.orth_
>> apples iphone f ruit juice cherry lemon banana pie mac orange

4. 使用 spaCy 对文本进行机器学习

将 spaCy 集成进机器学习模型是非常简单、直接的。让我们使用 sklearn 做一个自定义的文本分类器。我们将使用 cleaner、tokenizer、vectorizer、classifier 组件来创建一个 sklearn 管道。其中的 tokenizer 和 vectorizer 会使用我们用 spaCy 自定义的模块构建。

from sklearn.feature_extraction.stop_words import ENGLISH_STOP_WORDS as stopwords
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics import accuracy_score
from sklearn.base import TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.svm import LinearSVC

import string
punctuations = string.punctuation

from spacy.en import English
parser = English()

# 使用 spaCy 自定义 transformer
class predictors(TransformerMixin):
    def transform(self, X, **transform_params):
        return [clean_text(text) for text in X]
    def fit(self, X, y=None, **fit_params):
        return self
    def get_params(self, deep=True):
        return {}

# 进行文本清洗的实用的基本函数
def clean_text(text):     
    return text.strip().lower()

现在让我们使用 spaCy 的解析器和一些基本的数据清洗函数来创建一个自定义的 tokenizer 函数。值得一提的是,你可以用词向量来代替文本特征(使用深度学习模型效果会有较大的提升)

#创建 spaCy tokenizer,解析句子并生成 token
#也可以用词向量函数来代替它
def spacy_tokenizer(sentence):
    tokens = parser(sentence)
    tokens = [tok.lemma_.lower().strip() if tok.lemma_ != "-PRON-" else tok.lower_ for tok in tokens]
    tokens = [tok for tok in tokens if (tok not in stopwords and tok not in punctuations)]     return tokens

#创建 vectorizer 对象,生成特征向量,以此可以自定义 spaCy 的 tokenizer
vectorizer = CountVectorizer(tokenizer = spacy_tokenizer, ngram_range=(1,1)) classifier = LinearSVC()

现在可以创建管道,加载数据,然后运行分类模型了。

# 创建管道,进行文本清洗、tokenize、向量化、分类操作
pipe = Pipeline([("cleaner", predictors()),
                 ('vectorizer', vectorizer),
                 ('classifier', classifier)])

# Load sample data
train = [('I love this sandwich.', 'pos'),          
         ('this is an amazing place!', 'pos'),
         ('I feel very good about these beers.', 'pos'),
         ('this is my best work.', 'pos'),
         ("what an awesome view", 'pos'),
         ('I do not like this restaurant', 'neg'),
         ('I am tired of this stuff.', 'neg'),
         ("I can't deal with this", 'neg'),
         ('he is my sworn enemy!', 'neg'),          
         ('my boss is horrible.', 'neg')]
test =   [('the beer was good.', 'pos'),     
         ('I do not enjoy my job', 'neg'),
         ("I ain't feelin dandy today.", 'neg'),
         ("I feel amazing!", 'pos'),
         ('Gary is a good friend of mine.', 'pos'),
         ("I can't believe I'm doing this.", 'neg')]

# 创建模型并计算准确率
pipe.fit([x[0] for x in train], [x[1] for x in train])
pred_data = pipe.predict([x[0] for x in test])
for (sample, pred) in zip(test, pred_data):
    print sample, pred
print "Accuracy:", accuracy_score([x[1] for x in test], pred_data)

>>    ('the beer was good.', 'pos') pos
      ('I do not enjoy my job', 'neg') neg
      ("I ain't feelin dandy today.", 'neg') neg
      ('I feel amazing!', 'pos') pos
      ('Gary is a good friend of mine.', 'pos') pos
      ("I can't believe I'm doing this.", 'neg') neg
      Accuracy: 1.0

5. 和其它库的对比

Spacy 是一个非常强大且具备工业级能力的 NLP 包,它能满足大多数 NLP 任务的需求。可能你会思考:为什么会这样呢?

让我们把 Spacy 和另外两个 python 中有名的实现 NLP 的工具 —— CoreNLP 和 NLTK 进行对比吧!

支持功能表

功能 Spacy NLTK Core NLP
简易的安装方式 Y Y Y
Python API Y Y N
多语种支持 N Y Y
分词 Y Y Y
词性标注 Y Y Y
分句 Y Y Y
依存性分析 Y N Y
实体识别 Y Y Y
词向量计算集成 Y N N
情感分析 Y Y Y
共指消解 N N Y

速度:主要功能(Tokenizer、Tagging、Parsing)速度

Tokenizer Tagging Parsing
spaCy 0.2ms 1ms 19ms
CoreNLP 2ms 10ms 49ms
NLTK 4ms 443ms

准确性:实体抽取结果

准确率 Recall F-Score
spaCy 0.72 0.65 0.69
CoreNLP 0.79 0.73 0.76
NLTK 0.51 0.65 0.58

结束语

本文讨论了 spaCy —— 这个基于 python,完全用于实现 NLP 的库。我们通过许多用例展示了 spaCy 的可用性、速度及准确性。最后我们还将其余其它几个著名的 NLP 库 —— CoreNLP 与 NLTK 进行了对比。

如果你能真正理解这篇文章要表达的内容,那你一定可以去实现各种有挑战的文本数据与 NLP 问题。

希望你能喜欢这篇文章,如果你有疑问、问题或者别的想法,请在评论中留言。

作者介绍:

Shivam Bansal

Shivam Bansal 是一位数据科学家,在 NLP 与机器学习领域有着丰富的经验。他乐于学习,希望能解决一些富有挑战性的分析类问题。

发布于掘金 https://juejin.im/post/5971a4b9f265da6c42353332

这很大一部分都取决于这名软件工程师的背景,以及他希望掌握机器学习的哪一部分。为了具体讨论,现在假设这是一名初级工程师,他读了 4 年本科,从业 2 年,现在想从事计算广告学(CA)、自然语言处理(NLP)、图像分析、社交网络分析、搜索、推荐排名相关领域。现在,让我们从机器学习的必要课程开始讨论(声明:下面的清单很不完整,如果您的论文没有被包括在内,提前向您抱歉)。

  • 线性代数 很多的机器学习算法、统计学原理、模型优化都依赖线性代数。这也解释了为何在深度学习领域 GPU 要优于 CPU。在线性代数方面,你至少得熟练掌握以下内容:

    • 标量、向量、矩阵、张量。你可以将它们看成零维、一维、二维、三维与更高维的对象,可以对它们进行各种组合、变换,就像乐高玩具一样。它们为数据变换提供了最基础的处理方法。
    • 特征向量、标准化、矩阵近似、分解。实质上这些方法都是为了方便线性代数的运算。如果你想分析一个矩阵是如何运算的(例如检查神经网络中梯度消失问题,或者检查强化学习算法发散的问题),你得了解矩阵与向量应用了多少种缩放方法。而低阶矩阵近似与 Cholesky 分解可以帮你写出性能更好、稳定性更强的代码。
    • 数值线性代数 如果你想进一步优化算法的话,这是必修课。它对于理解核方法与深度学习很有帮助,不过对于图模型及采样来说它并不重要。
    • 推荐书籍 《Serge Lang, Linear Algebra》 很基础的线代书籍,很适合在校学生。 《Bela Bolobas, Linear Analysis》 这本书目标人群是那些想做数学分析、泛函分析的人。当然它的内容更加晦涩难懂,但更有意义。如果你攻读 PhD,值得一读。 《Lloyd Trefethen and David Bau, Numerical Linear Algebra》 这本书是同类书籍中较为推荐的一本。《Numerical Recipes》也是一本不错的书,但是里面的算法略为过时了。另外,推荐 Golub 和 van Loan 合著的书《Matrix Computations》
  • 优化与基础运算

    大多数时候提出问题是很简单的,而解答问题则是很困难的。例如,你想对一组数据使用线性回归(即线性拟合),那么你应该希望数据点与拟合线的距离平方和最小;又或者,你想做一个良好的点击预测模型,那么你应该希望最大程度地提高用户点击广告概率估计的准确性。也就是说,在一般情况下,我们会得到一个客观问题、一些参数、一堆数据,我们要做的就是找到通过它们解决问题的方法。找到这种方法是很重要的,因为我们一般得不到闭式解。

    • 凸优化

      在大多情况下,优化问题不会存在太多的局部最优解,因此这类问题会比较好解决。这种“局部最优即全局最优”的问题就是凸优化问题。

      (如果你在集合的任意两点间画一条直线,整条线始终在集合范围内,则这个集合是一个凸集合;如果你在一条函数曲线的任意两点间画一条直线,这两点间的函数曲线始终在这条直线之下,则这个函数是一个凸函数)

      Steven Boyd 与 Lieven Vandenberghe 合著的书可以说是这个领域的规范书籍了,这本书非常棒,而且是免费的,值得一读;此外,你可以在 Boyd 的课程中找到很多很棒的幻灯片;Dimitri Bertsekas 写了一系列关于优化、控制方面的书籍。读通这些书足以让任何一个人在这个领域立足。

    • 随机梯度下降(SGD)

      大多数问题其实最开始都是凸优化问题的特殊情况(至少早期定理如此),但是随着数据的增加,凸优化问题的占比会逐渐减少。因此,假设你现在得到了一些数据,你的算法将会需要在每一个更新步骤前将所有的数据都检查一遍。

      现在,我不怀好意地给了你 10 份相同的数据,你将不得不重复 10 次没有任何帮助的工作。不过在现实中并不会这么糟糕,你可以设置很小的更新迭代步长,每次更新前都将所有的数据检查一遍,这种方法将会帮你解决这类问题。小步长计算在机器学习中已经有了很大的转型,配合上一些相关的算法会使得解决问题更加地简单。

      不过,这样的做法对并行化计算提出了挑战。我们于 2009 年发表的《Slow Learners are Fast》论文可能就是这个方向的先导者之一。2013 年牛峰等人发表的《Hogwild》论文给出了一种相当优雅的无锁版本变体。简而言之,这类各种各样的算法都是通过在单机计算局部梯度,并异步更新共有的参数集实现并行快速迭代运算。

      随机梯度下降的另一个难题就是如何控制过拟合(例如可以通过正则化加以控制)。另外还有一种解决凸优化的惩罚方式叫近端梯度算法(PGD)。最流行的当属 Amir Beck 和 Marc Teboulle 提出的 FISTA 算法了。相关代码可以参考 Francis Bach 的 SPAM toolbox

    • 非凸优化方法

      许多的机器学习问题是非凸的。尤其是与深度学习相关的问题几乎都是非凸的,聚类、主题模型(topic model)、潜变量方法(latent variable method)等各种有趣的机器学习方法也是如此。一些最新的加速技术将对此有所帮助。例如我的学生 Sashank Reddy 最近展示了如何在这种情况下得到良好的收敛速率

      也可以用一种叫做谱学习算法(Spectral Method)的技术。Anima Anandkumar 在最近的 Quora session 中详细地描述了这项技术的细节。请仔细阅读她的文章,因为里面干货满满。简而言之,凸优化问题并不是唯一能够可靠解决的问题。在某些情况中你可以试着找出其问题的数学等价形式,通过这样找到能够真正反映数据中聚类、主题、相关维度、神经元等一切信息的参数。如果你愿意且能够将一切托付给数学解决,那是一件无比伟大的事。

      最近,在深度神经网络训练方面涌现出了各种各样的新技巧。我将会在下面介绍它们,但是在一些情况中,我们的目标不仅仅是优化模型,而是找到一种特定的解决方案(就好像旅途的重点其实是过程一样)。

  • (分布式)系统

    机器学习之所以现在成为了人类、测量学、传感器及数据相关领域几乎是最常用的工具,和过去 10 年规模化算法的发展密不可分。Jeff Dean 过去的一年发了 6 篇机器学习教程并不是巧合。在此简单介绍一下他:点击查看,他是 MapReduce、GFS 及 BigTable 等技术背后的创造者,正是这些技术让 Google 成为了伟大的公司。

    言归正传,(分布式)系统研究为我们提供了分布式、异步、容错、规模化、简单(Simplicity)的宝贵工具。最后一条“简单”是机器学习研究者们常常忽视的一件事。简单(Simplicity)不是 bug,而是一种特征。下面这些技术会让你受益良多:

    • 分布式哈希表

      它是 memcacheddynamopastry 以及 ceph 等的技术基础。它们所解决的都是同一件事情 —— 如何将对象分发到多台机器上,从而避免向中央存储区提出请求。为了达到这个目的,你必须将数据位置进行随机但确定的编码(即哈希)。另外,你需要考虑到当有机器出现故障时的处理方式。

      我们自己的参数服务器就是使用这种数据布局。这个项目的幕后大脑是我的学生 Mu Li 。请参阅 DMLC 查看相关的工具集。

    • 一致性与通信

      这一切的基础都是 Leslie Lamport 的 PAXOS 协议。它解决了不同机器(甚至部分机器不可用)的一致性问题。如果你曾经使用过版本控制工具,你应该可以直观地明白它是如何运行的——比如你有很多机器(或者很多开发者)都在进行数据更新(或更新代码),在它们(他们)不随时进行交流的情况下,你会如何将它们(他们)结合起来(不靠反复地求 diff)?

      在(分布式)系统中,解决方案是一个叫做向量时钟的东西(请参考 Google 的 Chubby)。我们也在参数服务器上使用了这种向量时钟的变体,这个变体与本体的区别就是我们仅使用向量时钟来限制参数的范围(Mu Li 做的),这样可以确保内存不会被无限增长的向量时钟时间戳给撑爆,正如文件系统不需要给每个字节都打上时间戳。

    • 容错机制、规模化与云

      学习这些内容最简单的方法就是在云服务器上运行各种算法,至于云服务可以找 Amazon AWSGoogle GWCMicrosoft Azure 或者 其它各种各样的服务商。一次性启动 1,000 台服务器,意识到自己坐拥如此之大的合法“僵尸网络”是多么的让人兴奋!之前我在 Google 工作,曾在欧洲某处接手 5,000 余台高端主机作为主题模型计算终端,它们是我们通过能源法案获益的核电厂相当可观的一部分资源。我的经理把我带到一旁,偷偷告诉我这个实验是多么的昂贵……

      可能入门这块最简单的方法就是去了解 docker 了吧。现在 docker 团队已经开发了大量的规模化工具。特别是他们最近加上的 Docker MachineDocker Cloud,可以让你就像使用打印机驱动一样连接云服务。

    • 硬件

      说道硬件可能会让人迷惑,但是如果你了解你的算法会在什么硬件上运行,对优化算法是很有帮助的。这可以让你知道你的算法是否能在任何条件下保持巅峰性能。我认为每个入门者都应该看看 Jeff Dean 的 《每个工程师都需要记住的数值》。我在面试时最喜欢的问题(至少现在最喜欢)就是“请问你的笔记本电脑有多快”。了解是什么限制了算法的性能是很有用的:是缓存?是内存带宽?延迟?还是磁盘?或者别的什么?Anandtech 在微处理器架构与相关方面写了很多很好的文章与评论,在 Intel、ARM、AMD 发布新硬件的时候不妨去看一看他的评论。

  • 统计学

    我故意把这块内容放在文章的末尾,因为几乎所有人都认为它是(它的确是)机器学习的关键因而忽视了其它内容。统计学可以帮你问出好的问题,也能帮你理解你的建模与实际数据有多接近。

    大多数图模型、核方法、深度学习等都能从“问一个好的问题”得到改进,或者说能够定义一个合理的可优化的目标函数。

这篇文章已经写的够久了,不知道有没有人能读到这里,我要去休息啦。现在网上有很多很棒的视频内容可以帮助你学习,许多教师现在都开通了他们的 Youtube 频道,上传他们的上课内容。这些课程有时可以帮你解决一些复杂的问题。这儿是我的 Youtube 频道欢迎订阅。顺便推荐 Nando de Freitas 的 Youtube 频道,他比我讲得好多了。

最后推荐一个非常好用的工具:DMLC。它很适合入门,包含了大量的分布式、规模化的机器学习算法,还包括了通过 MXNET 实现的神经网络。

虽然本文还有很多方面没有提到(例如编程语言、数据来源等),但是这篇文章已经太长了,这些内容请参考其他文章吧~

发布于掘金 https://juejin.im/post/596323416fb9a06bae1dff63

简介

这篇文章将会给你一些建议,让你避免写出性能远低于期望值的代码。在此特别指出有一些代码会导致 V8 引擎(涉及到 Node.JS、Opera、Chromium 等)无法对相关函数进行优化。

vhf 正在做一个类似的项目,试图将 V8 引擎的性能杀手全部列出来:V8 Bailout Reasons

V8 引擎背景知识

V8 引擎中没有解释器,但有 2 种不同的编译器:普通编译器与优化编译器。编译器会将你的 JavaScript 代码编译成汇编语言后直接运行。但这并不意味着运行速度会很快。被编译成汇编语言后的代码并不能显著地提高其性能,它只能省去解释器的性能开销,如果你的代码没有被优化的话速度依然会很慢。

例如,在普通编译器中 a + b 将会被编译成下面这样:

1
2
3
mov eax, a
mov ebx, b
call RuntimeAdd

换句话说,其实它仅仅调用了 runtime 函数。但如果 ab 能确定都是整型变量,那么编译结果会是下面这样:

1
2
3
mov eax, a
mov ebx, b
add eax, ebx

它的执行速度会比前面那种去在 runtime 中调用复杂的 JavaScript 加法算法快得多。

通常来说,使用普通编译器将会得到前面那种代码,使用优化编译器将会得到后面那种代码。走优化编译器的代码可以说比走普通编译器的代码性能好上 100 倍。但是请注意,并不是任何类型的 JavaScript 代码都能被优化。在 JS 中,有很多种情况(甚至包括一些我们常用的语法)是不能被优化编译器优化的(这种情况被称为“bailout”,从优化编译器降级到普通编译器)。

记住一些会导致整个函数无法被优化的情况是很重要的。JS 代码被优化时,将会逐个优化函数,在优化各个函数的时候不会关心其它的代码做了什么(除非那些代码被内联在即将优化的函数中。)。

这篇文章涵盖了大多数会导致函数坠入“无法被优化的深渊”的情况。不过在未来,优化编译器进行更新后能够识别越来越多的情况时,下面给出的建议与各种变通方法可能也会变的不再必要或者需要修改。

主题

  1. 工具
  2. 不支持的语法
  3. 使用 arguments
  4. Switch-case
  5. For-in
  6. 退出条件藏的很深,或者没有定义明确出口的无限循环

1. 工具

你可以在 node.js 中使用一些 V8 自带的标记来验证不同的代码用法对优化的影响。通常来说你可以创建一个包括特定模式的函数,然后使用所有允许的参数类型去调用它,再使用 V8 的内部去优化与检查它:

test.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//创建包含需要检查的情况的函数(检查使用 `eval` 语句是否能被优化)
function exampleFunction() {
return 3;
eval('');
}

function printStatus(fn) {
switch(%GetOptimizationStatus(fn)) {
case 1: console.log("Function is optimized"); break;
case 2: console.log("Function is not optimized"); break;
case 3: console.log("Function is always optimized"); break;
case 4: console.log("Function is never optimized"); break;
case 6: console.log("Function is maybe deoptimized"); break;
case 7: console.log("Function is optimized by TurboFan"); break;
default: console.log("Unknown optimization status"); break;
}
}

//识别类型信息
exampleFunction();
//这里调用 2 次是为了让这个函数状态从 uninitialized -> pre-monomorphic -> monomorphic
exampleFunction();

%OptimizeFunctionOnNextCall(exampleFunction);
//再次调用
exampleFunction();

//检查
printStatus(exampleFunction);

运行它:

1
2
3
$ node --trace_opt --trace_deopt --allow-natives-syntax test.js
(v0.12.7) Function is not optimized
(v4.0.0) Function is optimized by TurboFan

https://codereview.chromium.org/1962103003

为了检验我们做的这个工具是否真的有用,注释掉 eval 语句然后再运行一次:

1
2
3
$ node --trace_opt --trace_deopt --allow-natives-syntax test.js
[optimizing 000003FFCBF74231 <JS Function exampleFunction (SharedFunctionInfo 00000000FE1389E1)> - took 0.345, 0.042, 0.010 ms]
Function is optimized

事实证明,使用这个工具来验证处理方法是可行且必要的。

2. 不支持的语法

有一些语法结构是不支持被编译器优化的,用这类语法将会导致包含在其中的函数不能被优化。

请注意,即使这些语句不会被访问到或者不会被执行,它仍然会导致整个函数不能被优化。

例如下面这样做是没用的:

1
2
3
if (DEVELOPMENT) {
debugger;
}

即使 debugger 语句根本不会被执行到,上面的代码将会导致包含它的整个函数都不能被优化。

目前不可被优化的语法有:

  • Generator 函数V8 5.7 对其做了优化)
  • 包含 for of 语句的函数 (V8 commit 11e1e20 对其做了优化)
  • 包含 try catch 语句的函数 (V8 commit 9aac80f / V8 5.3 / node 7.x 对其做了优化)
  • 包含 try finally 语句的函数 (V8 commit 9aac80f / V8 5.3 / node 7.x 对其做了优化)
  • 包含let 复合赋值的函数 (Chrome 56 / V8 5.6! 对其做了优化)
  • 包含 const 复合赋值的函数 (Chrome 56 / V8 5.6! 对其做了优化)
  • 包含 __proto__ 对象字面量、get 声明、set 声明的函数

看起来永远不会被优化的语法有:

  • 包含 debugger 语句的函数
  • 包含字面调用 eval() 的函数
  • 包含 with 语句的函数

最后明确一下:如果你用了下面任何一种情况,整个函数将不能被优化:

1
2
3
function containsObjectLiteralWithProto() {
return {__proto__: 3};
}
1
2
3
4
5
6
7
function containsObjectLiteralWithGetter() {
return {
get prop() {
return 3;
}
};
}
1
2
3
4
5
6
7
function containsObjectLiteralWithSetter() {
return {
set prop(val) {
this.val = val;
}
};
}

另外在此要特别提一下 evalwith,它们会导致它们的调用栈链变成动态作用域,可能会导致其它的函数也受到影响,因为这种情况无法从字面上判断各个变量的有效范围。

变通办法

前面提到的不能被优化的语句用在生产环境代码中是无法避免的,例如 try-finallytry-catch。为了让使用这些语句的影响尽量减小,它们需要被隔离在一个最小化的函数中,这样主要的函数就不会被影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var errorObject = {value: null};
function tryCatch(fn, ctx, args) {
try {
return fn.apply(ctx, args);
}
catch(e) {
errorObject.value = e;
return errorObject;
}
}

var result = tryCatch(mightThrow, void 0, [1,2,3]);
//明确地报出 try-catch 会抛出什么
if(result === errorObject) {
var error = errorObject.value;
}
else {
//result 是返回值
}

3. 使用 arguments

有许多种使用 arguments 的方式会导致函数不能被优化。因此当使用 arguments 的时候需要格外小心。

3.1. 在非严格模式中,对一个已经被定义,同时在函数体中被 arguments 引用的参数重新赋值。典型案例:

1
2
3
function defaultArgsReassign(a, b) {
if (arguments.length < 2) b = 5;
}

变通方法 是将参数值保存在一个新的变量中:

1
2
3
4
5
function reAssignParam(a, b_) {
var b = b_;
//与 b_ 不同,可以安全地对 b 进行重新赋值
if (arguments.length < 2) b = 5;
}

如果仅仅是像上面这样用 arguments(上面代码作用为检测第二个参数是否存在,如果不存在则赋值为 5),也可以用 undefined 检测来代替这段代码:

1
2
3
function reAssignParam(a, b) {
if (b === void 0) b = 5;
}

但是之后如果需要用到 arguments,很容易忘记需要在这儿加上重新赋值的语句。

变通方法 2:为整个文件或者整个函数开启严格模式 ('use strict')。

3.2. arguments 泄露:

1
2
3
function leaksArguments1() {
return arguments;
}
1
2
3
function leaksArguments2() {
var args = [].slice.call(arguments);
}
1
2
3
4
5
6
function leaksArguments3() {
var a = arguments;
return function() {
return a;
};
}

arguments 对象在任何地方都不允许被传递或者被泄露。

变通方法 可以通过创建一个数组来代理 arguments 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function doesntLeakArguments() {
//.length 仅仅是一个整数,不存在泄露
//arguments 对象本身的问题
var args = new Array(arguments.length);
for(var i = 0; i < args.length; ++i) {
//i 是 arguments 对象的合法索引值
args[i] = arguments[i];
}
return args;
}

function anotherNotLeakingExample() {
var i = arguments.length;
var args = [];
while (i--) args[i] = arguments[i];
return args
}

但是这样要写很多让人烦的代码,因此得判断是否真的值得这么做。后面一次又一次的优化会代理更多的代码,越来越多的代码意味着代码本身的意义会被逐渐淹没。

不过,如果你有 build 这个过程,可以将上面这一系列过程由一个不需要 source map 的宏来实现,保证代码为合法的 JavaScript:

1
2
3
4
function doesntLeakArguments() {
INLINE_SLICE(args, arguments);
return args;
}

Bluebird 就使用了这个技术,上面的代码经过 build 之后会被拓展成下面这样:

1
2
3
4
5
6
7
8
function doesntLeakArguments() {
var $_len = arguments.length;
var args = new Array($_len);
for(var $_i = 0; $_i < $_len; ++$_i) {
args[$_i] = arguments[$_i];
}
return args;
}

3.3. 对 arguments 进行赋值:

在非严格模式下可以这么做:

1
2
3
4
function assignToArguments() {
arguments = 3;
return arguments;
}

变通方法:犯不着写这么蠢的代码。另外,在严格模式下它会报错。

那么如何安全地使用 arguments 呢?

只使用:

  • arguments.length
  • arguments[i] i 需要始终为 arguments 的合法整型索引,且不允许越界
  • 除了 .length[i],不要直接使用 arguments
  • 严格来说用 fn.apply(y, arguments) 是没问题的,但除此之外都不行(例如 .slice)。 Function#apply 是特别的存在。
  • 请注意,给函数添加属性值(例如 fn.$inject = ...)和绑定函数(即 Function#bind 的结果)会生成隐藏类,因此此时使用 #apply 不安全。

如果你按照上面的安全方式做,毋需担心使用 arguments 导致不确定 arguments 对象的分配。

4. Switch-case

在以前,一个 switch-case 语句最多只能包含 128 个 case 代码块,超过这个限制的 switch-case 语句以及包含这种语句的函数将不能被优化。

1
2
3
4
5
6
7
8
9
10
function over128Cases(c) {
switch(c) {
case 1: break;
case 2: break;
case 3: break;
...
case 128: break;
case 129: break;
}
}

你需要让 case 代码块的数量保持在 128 个之内,否则应使用函数数组或者 if-else。

这个限制现在已经被解除了,请参阅此 comment

5. For-in

For-in 语句在某些情况下会导致整个函数无法被优化。

这也解释了”For-in 速度不快“之类的说法。

5.1. 键不是局部变量:

1
2
3
4
5
6
7
function nonLocalKey1() {
var obj = {}
for(var key in obj);
return function() {
return key;
};
}
1
2
3
4
5
var key;
function nonLocalKey2() {
var obj = {}
for(key in obj);
}

这两种用法db都将会导致函数不能被优化的问题。因此键不能在上级作用域定义,也不能在下级作用域被引用。它必须是一个局部变量。

5.2. 被遍历的对象不是一个”简单可枚举对象“

5.2.1. 处于”哈希表模式“(又被称为”归一化对象“或”字典模式对象“ - 这种对象将哈希表作为其数据结构)的对象不是简单可枚举对象。
1
2
3
4
function hashTableIteration() {
var hashTable = {"-": 3};
for(var key in hashTable);
}

如果你给一个对象动态增加了很多的属性(在构造函数外)、delete 属性或者使用不合法的标识符作为属性,这个对象将会变成哈希表模式。换句话说,当你把一个对象当做哈希表来用,它就真的会变成哈希表。请不要对这种对象使用 for-in。你可以用过开启 Node.JS 的 --allow-natives-syntax,调用 console.log(%HasFastProperties(obj)) 来判断一个对象是否为哈希表模式。


5.2.2. 对象的原型链中存在可枚举属性
1
Object.prototype.fn = function() {};

上面这么做会给所有对象(除了用 Object.create(null) 创建的对象)的原型链中添加一个可枚举属性。此时任何包含了 for-in 语法的函数都不会被优化(除非仅遍历 Object.create(null) 创建的对象)。

你可以使用 Object.defineProperty 创建不可枚举属性(不推荐在 runtime 中调用,但是在定义一些例如原型属性之类的静态数据的时候它很高效)。


5.2.3. 对象中包含可枚举数组索引

ECMAScript 262 规范 定义了一个属性是否有数组索引:

数组对象会给予一些种类的属性名特殊待遇。对一个属性名 P(字符串形式),当且仅当 ToString(ToUint32(P)) 等于 P 并且 ToUint32(P) 不等于 232−1 时,它是个 数组索引 。一个属性名是数组索引的属性也叫做元素 。

一般只有数组有数组索引,但是有时候一般的对象也可能拥有数组索引: normalObj[0] = value;

1
2
3
4
5
6
function iteratesOverArray() {
var arr = [1, 2, 3];
for (var index in arr) {

}
}

因此使用 for-in 进行数组遍历不仅会比 for 循环要慢,还会导致整个包含 for-in 语句的函数不能被优化。


如果你试图使用 for-in 遍历一个非简单可枚举对象,它会导致包含它的整个函数不能被优化。

变通方法:只对 Object.keys 使用 for-in,如果要遍历数组需使用 for 循环。如果非要遍历整个原型链上的属性,需要将 for-in 隔离在一个辅助函数中以降低影响:

1
2
3
4
5
6
7
function inheritedKeys(obj) {
var ret = [];
for(var key in obj) {
ret.push(key);
}
return ret;
}

6. 退出条件藏的很深,或者没有定义明确出口的无限循环

有时候在你写代码的时候,你需要用到循环,但是不确定循环体内的代码之后会是什么样子。所以这时候你用了一个 while (true) { 或者 for (;;) {,在之后将终止条件放在循环体中,打断循环进行后面的代码。然而你写完这些之后就忘了这回事。在重构时,你发现这个函数很慢,出现了反优化情况 - 上面的循环很可能就是罪魁祸首。

重构时将循环内的退出条件放到循环的条件部分并不是那么简单。

  1. 如果代码中的退出条件是循环最后的 if 语句的一部分,且代码至少要运行一轮,那么你可以将这个循环重构为 do{} while ();
  2. 如果退出条件在循环的开头,请将它放在循环的条件部分中去。
  3. 如果退出条件在循环体中部,你可以尝试”滚动“代码:试着依次将一部分退出条件前的代码移到后面去,然后在之前的位置留下它的引用。当退出条件可以放在循环条件部分,或者至少变成一个浅显的逻辑判断时,这个循环就不再会出现反优化的情况了。

发布于掘金 https://juejin.im/post/5959edfc5188250d83241399

由于对效果的要求,需要加入透明背景的video。经过了解,现代浏览器(新版 Chrome、Firefox、Safari 等)已经全面支持 webM 格式的视频了,因此可以使用带 alpha 通道的 webM 格式视频满足要求。

要得到透明 webM 格式视频,则需要来源视频已经带有透明通道。

目前有几种方法:

1、使用 blander 进行绿幕抠图,将 green screen 扣去,生成背景透明的 png 帧序列,然后使用 ffmpeg 之类的工具将其生成 webM 文件。

2、使用 Adobe After Effect 之类的软件,在渲染时直接输出 Alpha + RGB 通道文件,然后使用 ffmpeg 之类的工具将其转换为 webM 文件。

ffmpeg 软件下载:https://ffmpeg.org/download.html

由于视频是自己用 AE 做的,因此直接选用第二种方法很方便。

首先,将视频背景颜色改成透明的:

upload successful

模式选择 Alpha 添加。在工作区下方将网络切换为透明网络

upload successful

如果改成功了,此时应该看到工作区的背景是灰白两色栅格。

将合成添加到渲染队列,进行设置:

upload successful

将输出模块改为“使用 Alpha 无损耗”,此时详情应该可以看到格式为 QuickTime,通道是 RGB + Alpha

upload successful

渲染输出,得到 .mov 文件,使用 ffmpeg 对其进行压缩编码。

执行命令:

ffmpeg -i in.mov -c:a libvorbis -ac 1 -b:a 96k -ar 48000 -b:v 1100k -maxrate 1100k -bufsize 1835k out.webm

得到 out.webm,它就是所需要的透明背景的 webM 文件了,可以在网页中使用 <video> tag 引用。