机器学习-中文文本预处理

中文文本挖掘预处理特点

参考:https://www.cnblogs.com/pinard/p/6744056.html

首先我们看看中文文本挖掘预处理和英文文本挖掘预处理相比的一些特殊点。

首先,中文文本是没有像英文的单词空格那样隔开的,因此不能直接像英文一样可以直接用最简单的空格和标点符号完成分词。所以一般我们需要用分词算法来完成分词,在文本挖掘的分词原理中,我们已经讲到了中文的分词原理,这里就不多说。

第二,中文的编码不是utf8,而是unicode。这样会导致在分词的时候,和英文相比,我们要处理编码的问题。

这两点构成了中文分词相比英文分词的一些不同点,后面我们也会重点讲述这部分的处理。当然,英文分词也有自己的烦恼,这个我们在以后再讲。了解了中文预处理的一些特点后,我们就言归正传,通过实践总结下中文文本挖掘预处理流程。

数据集收集

在文本挖掘之前,我们需要得到文本数据,文本数据的获取方法一般有两种:使用别人做好的语料库和自己用爬虫去在网上去爬自己的语料数据。

对于第一种方法,常用的文本语料库在网上有很多,如果大家只是学习,则可以直接下载下来使用,但如果是某些特殊主题的语料库,比如“机器学习”相关的语料库,则这种方法行不通,需要我们自己用第二种方法去获取。

对于第二种使用爬虫的方法,开源工具有很多,通用的爬虫我一般使用beautifulsoup。但是我们我们需要某些特殊的语料数据,比如上面提到的“机器学习”相关的语料库,则需要用主题爬虫(也叫聚焦爬虫)来完成。这个我一般使用ache。 ache允许我们用关键字或者一个分类算法来过滤出我们需要的主题语料,比较强大。

除去数据中非文本部分

这一步主要是针对我们用爬虫收集的语料数据,由于爬下来的内容中有很多html的一些标签,需要去掉。少量的非文本内容的可以直接用Python的正则表达式(re)删除, 复杂的则可以用beautifulsoup来去除。去除掉这些非文本的内容后,我们就可以进行真正的文本预处理了。

处理中文编码问题

由于Python2不支持unicode的处理,因此我们使用Python2做中文文本预处理时需要遵循的原则是,存储数据都用utf8,读出来进行中文相关处理时,使用GBK之类的中文编码,在下面一节的分词时,我们再用例子说明这个问题。

中文分词

常用的中文分词软件有很多,个人比较推荐结巴分词。安装也很简单,比如基于Python的,用”pip install jieba”就可以完成。下面我们就用例子来看看如何中文分词。

首先我们准备了两段文本,这两段文本在两个文件中。两段文本的内容分别是nlp_test0.txt和nlp_test2.txt:

1
2
3
4
5
6
7
8
9
10
11

import jieba

with open("./nlp_test1.txt") as f:

document = f.read() # 如果是python2,则需要用 decode("GBK")

document_cut = jieba.cut(document)

document_cut

<generator object Tokenizer.cut at 0x7f6a84cf09e8>
1
2
3
4
5

result = " ".join(document_cut)

result

Building prefix dict from the default dictionary ...

Loading model from cache /tmp/jieba.cache

Loading model cost 0.438 seconds.

Prefix dict has been built succesfully.











'        沙 瑞金 赞叹 易 学习 的 胸怀 , 是 金山 的 百姓 有福 , 可是 这件 事对 李达康 的 触动 很大 。 易 学习 又 回忆起 他们 三人 分开 的 前一晚 , 大家 一起 喝酒 话别 , 易 学习 被 降职 到 道口 县当 县长 , 王 大路 下海经商 , 李达康 连连 赔礼道歉 , 觉得 对不起 大家 , 他 最 对不起 的 是 王 大路 , 就 和 易 学习 一起 给 王 大路 凑 了 5 万块 钱 , 王 大路 自己 东挪西撮 了 5 万块 , 开始 下海经商 。 没想到 后来 王 大路 竟然 做 得 风生水 起 。 沙 瑞金 觉得 他们 三人 , 在 困难 时期 还 能 以沫 相助 , 很 不 容易 。 \n \n         沙 瑞金 向 毛娅 打听 他们 家 在 京州 的 别墅 , 毛娅 笑 着 说 , 王 大路 事业有成 之后 , 要 给 欧阳 菁 和 她 公司 的 股权 , 她们 没有 要 , 王 大路 就 在 京州帝 豪园 买 了 三套 别墅 , 可是 李达 康和易 学习 都 不要 , 这些 房子 都 在 王 大路 的 名下 , 欧阳 菁 好像 去 住 过 , 毛娅 不想 去 , 她 觉得 房子 太大 很 浪费 , 自己 家住 得 就 很 踏实 。'
1
2
3
4
5

with open("./nlp_test2.txt", "w") as f2:

f2.write(result)

可以发现对于一些人名和地名,jieba处理的不好,不过我们可以帮jieba加入词汇如下:

1
2
3
4
5
6
7
8
9

jieba.suggest_freq('沙瑞金', True)

jieba.suggest_freq('易学习', True)

jieba.suggest_freq('王大路', True)

jieba.suggest_freq('京州', True)

3

所以在很多 NLP 任务中先做命令实体识别的意义就在这里对吧?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

with open("./nlp_test1.txt", "r") as f1:

text = f1.read()

text_cut = jieba.cut(text) # list

result = " ".join(text_cut)

print(result)

with open("./nlp_test2.txt", "w") as f2:

f2.write(result)

        沙瑞金 赞叹 易学习 的 胸怀 , 是 金山 的 百姓 有福 , 可是 这件 事对 李达康 的 触动 很大 。 易学习 又 回忆起 他们 三人 分开 的 前一晚 , 大家 一起 喝酒 话别 , 易学习 被 降职 到 道口 县当 县长 , 王大路 下海经商 , 李达康 连连 赔礼道歉 , 觉得 对不起 大家 , 他 最 对不起 的 是 王大路 , 就 和 易学习 一起 给 王大路 凑 了 5 万块 钱 , 王大路 自己 东挪西撮 了 5 万块 , 开始 下海经商 。 没想到 后来 王大路 竟然 做 得 风生水 起 。 沙瑞金 觉得 他们 三人 , 在 困难 时期 还 能 以沫 相助 , 很 不 容易 。



         沙瑞金 向 毛娅 打听 他们 家 在 京州 的 别墅 , 毛娅 笑 着 说 , 王大路 事业有成 之后 , 要 给 欧阳 菁 和 她 公司 的 股权 , 她们 没有 要 , 王大路 就 在 京州 帝豪园 买 了 三套 别墅 , 可是 李达康 和 易学习 都 不要 , 这些 房子 都 在 王大路 的 名下 , 欧阳 菁 好像 去 住 过 , 毛娅 不想 去 , 她 觉得 房子 太大 很 浪费 , 自己 家住 得 就 很 踏实 。

引入停用词

在上面我们解析的文本中有很多无效的词,比如“着”,“和”,还有一些标点符号,这些我们不想在文本分析的时候引入,因此需要去掉,这些词就是停用词。常用的中文停用词表是1208个,下载地址在这。当然也有其他版本的停用词表,不过这个1208词版是我常用的。

在我们用scikit-learn做特征处理的时候,可以通过参数stop_words来引入一个数组作为停用词表。

1
2
3
4
5
6
7
8
9

stpword_path = "stop_words.txt"

with open(stpword_path, encoding="gbk") as f:

stpword_content = f.read()

stpword_list = stpword_content.splitlines()

1
2
3

print(stpword_list[:100])

[',', '?', '、', '。', '“', '”', '《', '》', '!', ',', ':', ';', '?', '人民', '末##末', '啊', '阿', '哎', '哎呀', '哎哟', '唉', '俺', '俺们', '按', '按照', '吧', '吧哒', '把', '罢了', '被', '本', '本着', '比', '比方', '比如', '鄙人', '彼', '彼此', '边', '别', '别的', '别说', '并', '并且', '不比', '不成', '不单', '不但', '不独', '不管', '不光', '不过', '不仅', '不拘', '不论', '不怕', '不然', '不如', '不特', '不惟', '不问', '不只', '朝', '朝着', '趁', '趁着', '乘', '冲', '除', '除此之外', '除非', '除了', '此', '此间', '此外', '从', '从而', '打', '待', '但', '但是', '当', '当着', '到', '得', '的', '的话', '等', '等等', '地', '第', '叮咚', '对', '对于', '多', '多少', '而', '而况', '而且', '而是']

特征处理

现在我们就可以用scikit-learn来对我们的文本特征进行处理了,在文本挖掘预处理之向量化与Hash Trick中,我们讲到了两种特征处理的方法,向量化与Hash Trick。而向量化是最常用的方法,因为它可以接着进行TF-IDF的特征处理。在文本挖掘预处理之TF-IDF中,我们也讲到了TF-IDF特征处理的方法。这里我们就用scikit-learn的TfidfVectorizer类来进行TF-IDF特征处理。

向量化与 Hash Trick

词袋模型

在讲向量化与Hash Trick之前,我们先说说词袋模型(Bag of Words,简称BoW)。词袋模型假设我们不考虑文本中词与词之间的上下文关系,仅仅只考虑所有词的权重。而权重与词在文本中出现的频率有关。

词袋模型首先会进行分词,在分词之后,通过统计每个词在文本中出现的次数,我们就可以得到该文本基于词的特征,如果将各个文本样本的这些词与对应的词频放在一起,就是我们常说的向量化。向量化完毕后一般也会使用TF-IDF进行特征的权重修正,再将特征进行标准化。 再进行一些其他的特征工程后,就可以将数据带入机器学习算法进行分类聚类了。

总结下词袋模型的三部曲:分词(tokenizing),统计修订词特征值(counting)与标准化(normalizing)。

词袋模型有很大的局限性,因为它仅仅考虑了词频,没有考虑上下文的关系,因此会丢失一部分文本的语义。但是大多数时候,如果我们的目的是分类聚类,则词袋模型表现的很好。

词袋模型之向量化

在词袋模型的统计词频这一步,我们会得到该文本中所有词的词频,有了词频,我们就可以用词向量表示这个文本。这里我们举一个例子,例子直接用scikit-learn的CountVectorizer类来完成,这个类可以帮我们完成文本的词频统计与向量化,代码如下:

1
2
3
4
5

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer()

1
2
3
4
5
6
7
8
9
10
11

corpus=["I come to China to travel",

"This is a car polupar in China",

"I love tea and Apple ",

"The work is to write some papers in science"]

print(vectorizer.fit_transform(corpus))

  (0, 16)    1

  (0, 3)    1

  (0, 15)    2

  (0, 4)    1

  (1, 5)    1

  (1, 9)    1

  (1, 2)    1

  (1, 6)    1

  (1, 14)    1

  (1, 3)    1

  (2, 1)    1

  (2, 0)    1

  (2, 12)    1

  (2, 7)    1

  (3, 10)    1

  (3, 8)    1

  (3, 11)    1

  (3, 18)    1

  (3, 17)    1

  (3, 13)    1

  (3, 5)    1

  (3, 6)    1

  (3, 15)    1

可以看出4个文本的词频已经统计出,在输出中,左边的括号中的第一个数字是文本的序号,第2个数字是词的序号,注意词的序号是基于所有的文档的。第三个数字就是我们的词频。

我们可以进一步看看每个文本的词向量特征和各个特征代表的词,代码如下:

1
2
3

print(vectorizer.fit_transform(corpus).toarray())

[[0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 2 1 0 0]

 [0 0 1 1 0 1 1 0 0 1 0 0 0 0 1 0 0 0 0]

 [1 1 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0]

 [0 0 0 0 0 1 1 0 1 0 1 1 0 1 0 1 0 1 1]]
1
2
3

print(vectorizer.get_feature_names())

['and', 'apple', 'car', 'china', 'come', 'in', 'is', 'love', 'papers', 'polupar', 'science', 'some', 'tea', 'the', 'this', 'to', 'travel', 'work', 'write']

也就是先统计整个文本corpus, 去掉停用词,剩下的词就是向量的维度。然后统计每一行文字出现的词频,得到相应的向量。显然词表是按照字母顺序排序的。

可以看到我们一共有19个词,所以4个文本都是19维的特征向量。而每一维的向量依次对应了下面的19个词。另外由于词”I”在英文中是停用词,不参加词频的统计。

由于大部分的文本都只会使用词汇表中的很少一部分的词,因此我们的词向量中会有大量的0。也就是说词向量是稀疏的。在实际应用中一般使用稀疏矩阵来存储。

这里有个疑问? 向量化之后的维度是根据自己的数据集来定,为什么不就是词表大小呢。这里是根据自己的数据集来的,但我们对测试集分类时,会出现 UNK 词吧,但是这个词其实在词表中是有的。那么在训练集中如果加上这个维度,其实也没有太大意义,因为在训练集中这个维度上所有的值都为0.

将文本做了词频统计后,我们一般会通过TF-IDF进行词特征值修订,这部分我们后面再讲。

向量化的方法很好用,也很直接,但是在有些场景下很难使用,比如分词后的词汇表非常大,达到100万+,此时如果我们直接使用向量化的方法,将对应的样本对应特征矩阵载入内存,有可能将内存撑爆,在这种情况下我们怎么办呢?第一反应是我们要进行特征的降维,说的没错!而Hash Trick就是非常常用的文本特征降维方法。

Hash Trick

在大规模的文本处理中,由于特征的维度对应分词词汇表的大小,所以维度可能非常恐怖,此时需要进行降维,不能直接用我们上一节的向量化方法。而最常用的文本降维方法是Hash Trick。说到Hash,一点也不神秘,学过数据结构的同学都知道。这里的Hash意义也类似。

在Hash Trick里,我们会定义一个特征Hash后对应的哈希表的大小,这个哈希表的维度会远远小于我们的词汇表的特征维度,因此可以看成是降维。具体的方法是,对应任意一个特征名,我们会用Hash函数找到对应哈希表的位置,然后将该特征名对应的词频统计值累加到该哈希表位置。如果用数学语言表示,假如哈希函数h使第i个特征哈希到位置j,即 $h(i)=j$,则第i个原始特征的词频数值 $\phi(i)$ 将累加到哈希后的第j个特征的词频数值 $\hat \phi(i)$上,即:

$$\hat \phi(i)=\sum_{i\in J;h(i)=j}\phi(i)$$

其中 J 是原始特征的维度。

但是上面的方法有一个问题,有可能两个原始特征的哈希后位置在一起导致词频累加特征值突然变大,为了解决这个问题,出现了hash Trick的变种signed hash trick,此时除了哈希函数h,我们多了一个一个哈希函数:

$$\xi:N\rightarrow \pm1$$

此时我们有

$$\hat \phi(j)=\sum_{i\in J;h(i)=j}\phi(i)\xi(i)$$

这样做的好处是,哈希后的特征仍然是一个无偏的估计,不会导致某些哈希位置的值过大。

当然,大家会有疑惑,这种方法来处理特征,哈希后的特征是否能够很好的代表哈希前的特征呢?从实际应用中说,由于文本特征的高稀疏性,这么做是可行的。如果大家对理论上为何这种方法有效,建议参考论文:Feature hashing for large scale multitask learning.这里就不多说了。

在scikit-learn的HashingVectorizer类中,实现了基于signed hash trick的算法,这里我们就用HashingVectorizer来实践一下Hash Trick,为了简单,我们使用上面的19维词汇表,并哈希降维到6维。当然在实际应用中,19维的数据根本不需要Hash Trick,这里只是做一个演示,代码如下:

1
2
3
4
5
6
7

from sklearn.feature_extraction.text import HashingVectorizer

vectorizer2 = HashingVectorizer(n_features=6, norm=None)

print(vectorizer2.fit_transform(corpus))

  (0, 1)    2.0

  (0, 2)    -1.0

  (0, 4)    1.0

  (0, 5)    -1.0

  (1, 0)    1.0

  (1, 1)    1.0

  (1, 2)    -1.0

  (1, 5)    -1.0

  (2, 0)    2.0

  (2, 5)    -2.0

  (3, 0)    0.0

  (3, 1)    4.0

  (3, 2)    -1.0

  (3, 3)    1.0

  (3, 5)    -1.0

大家可以看到结果里面有负数,这是因为我们的哈希函数ξ可以哈希到1或者-1导致的。

和PCA类似,Hash Trick降维后的特征我们已经不知道它代表的特征名字和意义。此时我们不能像上一节向量化时候可以知道每一列的意义,所以Hash Trick的解释性不强。

向量化与 Hash Track 小结

这里我们对向量化与它的特例Hash Trick做一个总结。在特征预处理的时候,我们什么时候用一般意义的向量化,什么时候用Hash Trick呢?标准也很简单。

一般来说,只要词汇表的特征不至于太大,大到内存不够用,肯定是使用一般意义的向量化比较好。因为向量化的方法解释性很强,我们知道每一维特征对应哪一个词,进而我们还可以使用TF-IDF对各个词特征的权重修改,进一步完善特征的表示。

而Hash Trick用大规模机器学习上,此时我们的词汇量极大,使用向量化方法内存不够用,而使用Hash Trick降维速度很快,降维后的特征仍然可以帮我们完成后续的分类和聚类工作。当然由于分布式计算框架的存在,其实一般我们不会出现内存不够的情况。因此,实际工作中我使用的都是特征向量化。

向量化与Hash Trick就介绍到这里,下一篇我们讨论TF-IDF。

文本向量化特征的不足

在将文本分词并向量化后,我们可以得到词汇表中每个词在各个文本中形成的词向量,比如在文本挖掘预处理之向量化与Hash Trick这篇文章中,我们将下面4个短文本做了词频统计:

1
2
3
4
5
6
7
8
9

corpus=["I come to China to travel",

"This is a car polupar in China",

"I love tea and Apple ",

"The work is to write some papers in science"]

不考虑停用词,处理后得到的词向量如下:

1
2
3
4
5
6
7
8
9

[[0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 2 1 0 0]

[0 0 1 1 0 1 1 0 0 1 0 0 0 0 1 0 0 0 0]

[1 1 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0]

[0 0 0 0 0 1 1 0 1 0 1 1 0 1 0 1 0 1 1]]

如果我们直接将统计词频后的19维特征做为文本分类的输入,会发现有一些问题。比如第一个文本,我们发现”come”,”China”和“Travel”各出现1次,而“to“出现了两次。似乎看起来这个文本与”to“这个特征更关系紧密。但是实际上”to“是一个非常普遍的词,几乎所有的文本都会用到,因此虽然它的词频为2,但是重要性却比词频为1的”China”和“Travel”要低的多。如果我们的向量化特征仅仅用词频表示就无法反应这一点。因此我们需要进一步的预处理来反应文本的这个特征,而这个预处理就是TF-IDF。

TF-IDF概述

TF-IDF是Term Frequency - Inverse Document Frequency的缩写,即“词频-逆文本频率”。它由两部分组成,TF和IDF。

前面的TF也就是我们前面说到的词频,我们之前做的向量化也就是做了文本中各个词的出现频率统计,并作为文本特征,这个很好理解。关键是后面的这个IDF,即“逆文本频率”如何理解。在上一节中,我们讲到几乎所有文本都会出现的”to”其词频虽然高,但是重要性却应该比词频低的”China”和“Travel”要低。我们的IDF就是来帮助我们来反应这个词的重要性的,进而修正仅仅用词频表示的词特征值。

概括来讲, IDF反应了一个词在所有文本中出现的频率,如果一个词在很多的文本中出现,那么它的IDF值应该低,比如上文中的“to”。而反过来如果一个词在比较少的文本中出现,那么它的IDF值应该高。比如一些专业的名词如“Machine Learning”。这样的词IDF值应该高。一个极端的情况,如果一个词在所有的文本中都出现,那么它的IDF值应该为0。

上面是从定性上说明的IDF的作用,那么如何对一个词的IDF进行定量分析呢?这里直接给出一个词x的IDF的基本公式如下:

$$IDF(x)=\dfrac{N}{N(x)}$$

其中,N代表语料库中文本的总数,而 $N(x)$ 代表语料库中包含词x的文本总数。为什么IDF的基本公式应该是是上面这样的而不是像 $N/N(x)$ 这样的形式呢?这就涉及到信息论相关的一些知识了。感兴趣的朋友建议阅读吴军博士的《数学之美》第11章。

上面的IDF公式已经可以使用了,但是在一些特殊的情况会有一些小问题,比如某一个生僻词在语料库中没有,这样我们的分母为0, IDF没有意义了。所以常用的IDF我们需要做一些平滑,使语料库中没有出现的词也可以得到一个合适的IDF值。平滑的方法有很多种,最常见的IDF平滑后的公式之一为:

$$IDF(x)=log\dfrac{N+1}{N(x)+1}+1$$

有了IDF的定义,我们就可以计算某一个词的TF-IDF值了:

$$\text{TF-IDF(x)}=TF(x)*IDF(x)$$

其中TF(x)指词x在当前文本中的词频。

用scikit-learn进行TF-IDF预处理

在scikit-learn中,有两种方法进行TF-IDF的预处理。

第一种方法是在用CountVectorizer类向量化之后再调用TfidfTransformer类进行预处理。第二种方法是直接用TfidfVectorizer完成向量化与TF-IDF预处理。

首先我们来看第一种方法,CountVectorizer+TfidfTransformer的组合,代码如下:

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

from sklearn.feature_extraction.text import TfidfTransformer

from sklearn.feature_extraction.text import CountVectorizer



corpus = ["I come to China to travel",

"This is a car polupar in China",

"I love tea and Apple ",

"The work is to write some papers in science"]



vectorizer = CountVectorizer()



transformer = TfidfTransformer()

tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus))

print(tfidf)

  (0, 4)    0.4424621378947393

  (0, 15)    0.697684463383976

  (0, 3)    0.348842231691988

  (0, 16)    0.4424621378947393

  (1, 3)    0.3574550433419527

  (1, 14)    0.45338639737285463

  (1, 6)    0.3574550433419527

  (1, 2)    0.45338639737285463

  (1, 9)    0.45338639737285463

  (1, 5)    0.3574550433419527

  (2, 7)    0.5

  (2, 12)    0.5

  (2, 0)    0.5

  (2, 1)    0.5

  (3, 15)    0.2811316284405006

  (3, 6)    0.2811316284405006

  (3, 5)    0.2811316284405006

  (3, 13)    0.3565798233381452

  (3, 17)    0.3565798233381452

  (3, 18)    0.3565798233381452

  (3, 11)    0.3565798233381452

  (3, 8)    0.3565798233381452

  (3, 10)    0.3565798233381452
1
2
3
4
5
6
7
8
9

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf2 = TfidfVectorizer()

re = tfidf2.fit_transform(corpus)

print(re)

  (0, 4)    0.4424621378947393

  (0, 15)    0.697684463383976

  (0, 3)    0.348842231691988

  (0, 16)    0.4424621378947393

  (1, 3)    0.3574550433419527

  (1, 14)    0.45338639737285463

  (1, 6)    0.3574550433419527

  (1, 2)    0.45338639737285463

  (1, 9)    0.45338639737285463

  (1, 5)    0.3574550433419527

  (2, 7)    0.5

  (2, 12)    0.5

  (2, 0)    0.5

  (2, 1)    0.5

  (3, 15)    0.2811316284405006

  (3, 6)    0.2811316284405006

  (3, 5)    0.2811316284405006

  (3, 13)    0.3565798233381452

  (3, 17)    0.3565798233381452

  (3, 18)    0.3565798233381452

  (3, 11)    0.3565798233381452

  (3, 8)    0.3565798233381452

  (3, 10)    0.3565798233381452

输出的各个文本各个词的TF-IDF值和第一种的输出完全相同。大家可以自己去验证一下。

由于第二种方法比较的简洁,因此在实际应用中推荐使用,一步到位完成向量化,TF-IDF与标准化。

TF-IDF是非常常用的文本挖掘预处理基本步骤,但是如果预处理中使用了Hash Trick,则一般就无法使用TF-IDF了,因为Hash Trick后我们已经无法得到哈希后的各特征的IDF的值。使用了IF-IDF并标准化以后,我们就可以使用各个文本的词特征向量作为文本的特征,进行分类或者聚类分析。

当然TF-IDF不光可以用于文本挖掘,在信息检索等很多领域都有使用。因此值得好好的理解这个方法的思想

还的好好理解下 TF-IDF 是怎么实现的!

建立分析模型

有了每段文本的TF-IDF的特征向量,我们就可以利用这些数据建立分类模型,或者聚类模型了,或者进行主题模型的分析。比如我们上面的两段文本,就可以是两个训练样本了。此时的分类聚类模型和之前讲的非自然语言处理的数据分析没有什么两样。因此对应的算法都可以直接使用。而 主题模型 是自然语言处理比较特殊的一块,这个我们后面再单独讲。

中文文本挖掘预处理总结

上面我们对中文文本挖掘预处理的过程做了一个总结,希望可以帮助到大家。需要注意的是这个流程主要针对一些常用的文本挖掘,并使用了词袋模型,对于某一些自然语言处理的需求则流程需要修改。比如我们涉及到词上下文关系的一些需求,此时不能使用词袋模型。而有时候我们对于特征的处理有自己的特殊需求,因此这个流程仅供自然语言处理入门者参考。

机器学习-英文文本预处理

转载自:http://www.cnblogs.com/pinard/p/6756534.html

英文文本挖掘预处理特点

英文文本的预处理方法和中文的有部分区别。首先,英文文本挖掘预处理一般可以不做分词(特殊需求除外),而中文预处理分词是必不可少的一步。第二点,大部分英文文本都是uft-8的编码,这样在大多数时候处理的时候不用考虑编码转换的问题,而中文文本处理必须要处理unicode的编码问题。

而英文文本的预处理也有自己特殊的地方,第三点就是拼写问题,很多时候,我们的预处理要包括拼写检查,比如“Helo World”这样的错误,我们不能在分析的时候讲错纠错。所以需要在预处理前加以纠正。第四点就是词干提取(stemming)和词形还原(lemmatization)。这个东西主要是英文有单数,复数和各种时态,导致一个词会有不同的形式。比如“countries”和”country”,”wolf”和”wolves”,我们期望是有一个词。

英文文本挖掘预处理一:数据收集

这部分英文和中文类似。获取方法一般有两种:使用别人做好的语料库和自己用爬虫去在网上去爬自己的语料数据。

对于第一种方法,常用的文本语料库在网上有很多,如果大家只是学习,则可以直接下载下来使用,但如果是某些特殊主题的语料库,比如“deep learning”相关的语料库,则这种方法行不通,需要我们自己用第二种方法去获取。

对于第二种使用爬虫的方法,开源工具有很多,通用的爬虫我一般使用beautifulsoup。但是我们我们需要某些特殊的语料数据,比如上面提到的“deep learning”相关的语料库,则需要用主题爬虫(也叫聚焦爬虫)来完成。这个我一般使用ache。 ache允许我们用关键字或者一个分类算法模型来过滤出我们需要的主题语料,比较强大。

英文文本挖掘预处理二:除去数据中非文本部分

这一步主要是针对我们用爬虫收集的语料数据,由于爬下来的内容中有很多html的一些标签,需要去掉。少量的非文本内容的可以直接用Python的正则表达式(re)删除, 复杂的则可以用beautifulsoup来去除。另外还有一些特殊的非英文字符(non-alpha),也可以用Python的正则表达式(re)删除。

re 模块

参考 blog

正则表达式(Regular Expression)是字符串处理的常用工具,通常被用来检索、替换那些符合某个模式(Pattern)的文本。很多程序设计语言都支持正则表达式,像Perl、Java、C/C++。在 Python 中是通过标准库中的 re 模块 提供对正则的支持。

关于正则表达式的语法可以看

编译正则表达式

re 模块提供了 re.compile() 函数将一个字符串编译成 pattern object,用于匹配或搜索。函数原型如下:

1
2
3

re.compile(pattern, flags=0)

re.compile() 还接受一个可选的参数 flag,用于指定正则匹配的模式。关于匹配模式,后面将会讲到。

反斜杠的困扰

在 python 的字符串中,\ 是被当做转义字符的。在正则表达式中,\ 也是被当做转义字符。这就导致了一个问题:如果你要匹配 \ 字符串,那么传递给 re.compile() 的字符串必须是 ”\\\\“

由于字符串的转义,所以实际传递给 re.compile() 的是 ”\\“,然后再通过正则表达式的转义,”\\“ 会匹配到字符”\“。这样虽然可以正确匹配到字符 \,但是很麻烦,而且容易漏写反斜杠而导致 Bug。那么有什么好的解决方案呢?

原始字符串很好的解决了这个问题,通过在字符串前面添加一个r,表示原始字符串,不让字符串的反斜杠发生转义。那么就可以使用r"\\\\"来匹配字符 \了。

patern object 执行匹配

一旦你编译得到了一个 pattern object,你就可以使用 pattern object 的方法或属性进行匹配了,下面列举几个常用的方法,更多请看这里

Pattern.match(string[, pos[, endpos]])

  • 匹配从 pos 到 endpos 的字符子串的开头。匹配成功返回一个 match object,不匹配返回 None。

  • pos 的默认值是0,endpos 的默认值是 len(string),所以默认情况下是匹配整个字符串的开头。

1
2
3
4
5
6
7
8
9
10
11

pattern = re.compile("d")

print(pattern.match('dog')) # 在字串开头,匹配成功

print(pattern.match('god')) # 不再子串开头,匹配不成功

print(pattern.match('ddaa', 1,5)) # 在子串开头,匹配成功

print(pattern.match('monday', 3))

1
2
3
4
5
6
7
8
9

<_sre.SRE_Match object; span=(0, 1), match='d'>

<_sre.SRE_Match object; span=(0, 1), match='g'>

<_sre.SRE_Match object; span=(1, 2), match='d'>

<_sre.SRE_Match object; span=(3, 4), match='d'>

regex.search(string[, pos[, endpos]])

  • 扫描整个字符串,并返回它找到的第一个匹配

  • 和 regex.match() 一样,可以通过 pos 和 endpos 指定范围

1
2
3
4
5
6
7
8
9
10
11

pattern = re.compile("ar{1}")

match = pattern.search("marray")

print(match)



<_sre.SRE_Match object; span=(1, 3), match='ar'>

regex.findall(string[, pos[, endpos]])

  • 找到所有匹配的子串,并返回一个 list

  • 可选参数 pos 和 endpos 和上面一样

1
2
3
4
5
6
7
8
9
10
11

pattern = re.compile(r"\d+") # 匹配字符串中的数字

lst = pattern.findall("abc1def2rst3xyz")

print(lst)



['1', '2', '3']

regex.finditer(string[, pos[, endpos]])

  • 找到所有匹配的子串,并返回由这些匹配结果(match object)组成的迭代器

  • 可选参数 pos 和 endpos 和上面一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

pattern = re.compile(r"\d+")

p = pattern.finditer("abc1def2rst3xyz")

for i in p:

print(i)



<_sre.SRE_Match object; span=(3, 4), match='1'>

<_sre.SRE_Match object; span=(7, 8), match='2'>

<_sre.SRE_Match object; span=(11, 12), match='3'>

match object 获取结果

在上面讲到,通过 pattern object 的方法(除 findall 外)进行匹配得到的返回结果都是 match object。每一个 match object 都包含了匹配到的相关信息,比如,起始位置、匹配到的子串。那么,我们如何从 match object 中提取这些信息呢?

match.group([group1, ...]):

  • 返回 match object 中的字符串。

  • 每一个 ( ) 都是一个分组,分组编号从1开始,从左往右,每遇到一个左括号,分组编号+1。

  • 组 0 总是存在的,它就是整个表达式

  • 没有参数时,group1默认为0,这时返回整个匹配到的字符串。

  • 指定一个参数(整数)时,返回该分组匹配到的字符串。

  • 指定多个参数时,返回由那几个分组匹配到的字符串组成的 tuple。

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

pattern = re.compile(r"(\w+) (\w+)") # \w 匹配任意字母,数字,下划线

m = pattern.match("He _ Kobe Bryant, Lakers player")

print(m)

print(m.group())

print(m.group(1))

print(m.group(2))

print(m.group(1,2))





<_sre.SRE_Match object; span=(0, 4), match='He _'>

He _

He

_

('He', '_')

match.groups()

  • 返回由所有分组匹配到的字符串组成的 tuple。
1
2
3
4
5
6
7
8
9
10
11

m = re.match(r"(\d+)\.(\d+)", '24.163')

m.groups()





('24', '163')

match.start([group])

  • 没有参数时,返回匹配到的字符串的起始位置。

  • 指定参数(整数)时,返回该分组匹配到的字符串的起始位置。

1
2
3
4
5
6
7
8
9

pattern = re.compile(r"(\w+) (\w+)")

m = pattern.match("Kobe Bryant, Lakers")

print(m.start()) # 0

print(m.start(2)) # 5

match.end([group]):

  • 没有参数时,返回匹配到的字符串的结束位置。

  • 指定参数(整数)时,返回该分组匹配到的字符串的结束位置。

1
2
3
4
5
6
7
8
9

pattern = re.compile(r"(\w+) (\w+)")

m = pattern.match("Kobe Bryant, Lakers")

print(m.end()) # 11

print(m.end(1)) # 4

match.span([group]):

  • 返回一个二元 tuple 表示匹配到的字符串的范围,即 (start, end)。

  • 指定参数时,返回该分组匹配到的字符串的 (start, end)。

1
2
3
4
5
6
7
8
9

pattern = re.compile(r"(\w+) (\w+)")

m = pattern.match("Kobe Bryant, Lakers")

print(m.span()) # (0, 11)

print(m.span(2)) # (5, 11)

模块级别的函数

上面讲到的函数都是对象的方法,要使用它们必须先得到相应的对象。本节将介绍一些Module-Level Functions,比如 match(),search(),findall() 等等。你不需要创建一个 pattern object 就可以直接调用这些函数。

re.match(pattern, string, flags=0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

pattern = re.compile(r"(\w+) (\w+)")

m = pattern.match("Kobe Bryant, Lakers")

print(m)



# 相当于



m = re.match(r"(\w+) (\w+)","Kobe Bryant, Lakers")

print(m)



<_sre.SRE_Match object; span=(0, 11), match='Kobe Bryant'>

<_sre.SRE_Match object; span=(0, 11), match='Kobe Bryant'>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

pattern = re.compile(r"(\w+) (\w+)")

m = pattern.search("Kobe Bryant, Lakers")

print(m)

# 相当于



m = re.search(r"(\w+) (\w+)","Kobe Bryant, Lakers")

print(m)



<_sre.SRE_Match object; span=(0, 11), match='Kobe Bryant'>

<_sre.SRE_Match object; span=(0, 11), match='Kobe Bryant'>



re.findall(pattern, string, flags=0):与上面类似。

re.finditer(pattern, string, flags=0):与上面类似

编译标志(匹配模式)

  • re.IGNORECASE:忽略大小写,同 re.I。
  • re.MULTILINE:多行模式,改变^和$的行为,同 re.M。
  • re.DOTALL:点任意匹配模式,让’.’可以匹配包括’\n’在内的任意字符,同 re.S。
  • re.LOCALE:使预定字符类 \w \W \b \B \s \S 取决于当前区域设定, 同 re.L。
  • re.ASCII:使 \w \W \b \B \s \S 只匹配 ASCII 字符,而不是 Unicode 字符,同 re.A。
  • re.VERBOSE:详细模式。这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释。主要是为了让正则表达式更易读,同 re.X。例如,以下两个正则表达式是等价的:
1
2
3
4
5
6
7
8
9
10
11
12
13

a = re.compile(r"\d + \. \d *#re.X") the integral part

b = re.compile(r"\d+\.\d*")

print(b.match("123.45"))



<_sre.SRE_Match object; span=(0, 6), match='123.45'>



修改字符串

第二部分讲的是字符串的匹配和搜索,但是并没有改变字符串。下面就讲一下可以改变字符串的操作。

分割字符串

split()函数在匹配的地方将字符串分割,并返回一个 list。同样的,re 模块提供了两种 split 函数,一个是 pattern object 的方法,一个是模块级的函数。

regex.split(string, maxsplit=0):

  • maxsplit用于指定最大分割次数,不指定将全部分割。
1
2
3
4
5
6
7
8
9
10
11

pattern = re.compile(r"[A-Z]+")

m = pattern.split("abcDefgHijkLmnoPqrs")

print(m)



['abc', 'efg', 'ijk', 'mno', 'qrs']

re.split(pattern, string, maxsplit=0, flags=0):

  • 模块级函数,功能与 regex.split() 相同。

  • flags用于指定匹配模式。

1
2
3
4
5
6
7
8
9
10
11

m = re.split(r"[A-Z]+","abcDefgHijkLmnoPqrs")

print(m)



# 输出结果:

['abc', 'efg', 'ijk', 'mno', 'qrs']

搜索与替换

另一个常用的功能是找到所有的匹配,并把它们用不同的字符串替换。re 模块提供了sub()和subn()来实现替换的功能,而它们也分别有自己两个不同版本的函数。

regex.sub(repl, string, count=0):

  • 使用 repl 替换 string 中每一个匹配的子串,返回替换后的字符串。若找不到匹配,则返回原字符串。

  • repl 可以是一个字符串,也可以是一个函数。

  • 当repl是一个字符串时,任何在其中的反斜杠都会被处理。

  • 当repl是一个函数时,这个函数应当只接受一个参数(pattern对象),对匹配到的对象进行处理,然后返回一个字符串用于替换。

  • count 用于指定最多替换次数,不指定时全部替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

pattern = re.compile(r"like", re.I)

s1 = pattern.sub(r"love", "I like you, do you like me?")

s2 = pattern.sub(lambda m:m.group().upper(), "I like you, do you like me?") # repl 是函数,其参数是 pattern

print(s1)

print(s2)





I love you, do you love me?

I LIKE you, do you LIKE me?

re.sub(pattern, repl, string, count=0, flags=0)

  • 模块级函数,与 regex.sub() 函数功能相同。

  • flags 用于指定匹配模式。

1
2
3
4
5
6
7
8
9

s1 = re.sub(r"(\w)'s\b", r"\1 is", "She's Xie Pan")

print(s1)



She is Xie Pan

regex.subn(repl, string, count=0)

  • 同 sub(),只不过返回值是一个二元 tuple,即(sub函数返回值, 替换次数)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

pattern = re.compile(r"like", re.I)

s1 = pattern.subn(r"love", "I like you, do you like me?")

s2 = pattern.subn(lambda m:m.group().upper(), "I like you, do you like me?") # repl 是函数,其参数是 pattern

print(s1)

print(s2)



('I love you, do you love me?', 2)

('I LIKE you, do you LIKE me?', 2)

re.subn(pattern, repl, string, count=0, flags=0):

  • 同上

英文文本挖掘预处理三:拼写检查

由于英文文本中可能有拼写错误,因此一般需要进行拼写检查。如果确信我们分析的文本没有拼写问题,可以略去此步。

拼写检查,我们一般用pyenchant类库完成。pyenchant的安装很简单:”pip install pyenchant”即可。

对于一段文本,我们可以用下面的方式去找出拼写错误:

1
2
3
4
5
6
7

# 发现这样安装并不是在虚拟环境下,需要去终端对应的虚拟环境下安装

# source avtivate NLP

!pip install pyenchant

/bin/sh: 1: source: not found

Requirement already satisfied: pyenchant in /home/panxie/anaconda3/lib/python3.6/site-packages (2.0.0)

distributed 1.21.8 requires msgpack, which is not installed.

You are using pip version 10.0.1, however version 18.0 is available.

You should consider upgrading via the 'pip install --upgrade pip' command.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

from enchant.checker import SpellChecker

chkr = SpellChecker('en_US')

chkr.set_text("Many peopel like too watch In the Name of people")

for err in chkr:

print("ERROR:", err.word)





ERROR: peopel

发现只能找单词拼写错误的,但 too 这样的是没办法找出的。找出错误后,我们可以自己来决定是否要改正。当然,我们也可以用pyenchant中的wxSpellCheckerDialog类来用对话框的形式来交互决定是忽略,改正还是全部改正文本中的错误拼写。

更多操作可参考:

英文文本挖掘预处理四:词干提取(stemming)和词形还原(lemmatization)

词干提取(stemming)和词型还原(lemmatization)是英文文本预处理的特色。两者其实有共同点,即都是要找到词的原始形式。只不过词干提取(stemming)会更加激进一点,它在寻找词干的时候可以会得到不是词的词干。比如”imaging”的词干可能得到的是”imag”, 并不是一个词。而词形还原则保守一些,它一般只对能够还原成一个正确的词的词进行处理。个人比较喜欢使用词型还原而不是词干提取。

在实际应用中,一般使用nltk来进行词干提取和词型还原。安装nltk也很简单,”pip install nltk”即可。只不过我们一般需要下载nltk的语料库,可以用下面的代码完成,nltk会弹出对话框选择要下载的内容。选择下载语料库就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

import nltk

nltk.download('wordnet')



[nltk_data] Downloading package wordnet to /home/panxie/nltk_data...

[nltk_data] Unzipping corpora/wordnet.zip.



True



在nltk中,做词干提取的方法有PorterStemmer,LancasterStemmer和SnowballStemmer。个人推荐使用SnowballStemmer。这个类可以处理很多种语言,当然,除了中文。

1
2
3
4
5
6
7
8
9
10
11

from nltk.stem import SnowballStemmer

stemmer = SnowballStemmer("english")

stemmer.stem("countries")



'countri'

输出是”countri”,这个词干并不是一个词。

而如果是做词型还原,则一般可以使用WordNetLemmatizer类,即wordnet词形还原方法。

1
2
3
4
5
6
7
8
9
10
11
12
13

from nltk.stem import WordNetLemmatizer

wnl = WordNetLemmatizer()

print(wnl.lemmatize('countries'))





country

输出是”country”,比较符合需求。

在实际的英文文本挖掘预处理的时候,建议使用基于wordnet的词形还原就可以了。

这里有个词干提取和词型还原的demo,如果是这块的新手可以去看看,上手很合适。

英文文本挖掘预处理五:转化为小写

1
2
3
4
5
6
7
8
9

text = 'XiePan'

print(text.lower())



xiepan

英文文本挖掘预处理六:引入停用词

在英文文本中有很多无效的词,比如“a”,“to”,一些短词,还有一些标点符号,这些我们不想在文本分析的时候引入,因此需要去掉,这些词就是停用词。个人常用的英文停用词表下载地址在这。当然也有其他版本的停用词表,不过这个版本是我常用的。

在我们用scikit-learn做特征处理的时候,可以通过参数stop_words来引入一个数组作为停用词表。这个方法和前文讲中文停用词的方法相同,这里就不写出代码,大家参考前文即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

from nltk import word_tokenize

from nltk.corpus import stopwords

stop = set(stopwords.words('english')) # 停用词

stop.add("foo") # 增加一个词

stop.remove("is") # 去掉一个词

sentence = "this is a foo bar sentence"

[i for i in word_tokenize(sentence.lower()) if i not in stop]

['is', 'bar', 'sentence']

英文文本挖掘预处理七:特征处理

现在我们就可以用scikit-learn来对我们的文本特征进行处理了,在文本挖掘预处理之向量化与Hash Trick中,我们讲到了两种特征处理的方法,向量化与Hash Trick。而向量化是最常用的方法,因为它可以接着进行TF-IDF的特征处理。在文本挖掘预处理之TF-IDF中,我们也讲到了TF-IDF特征处理的方法

TfidfVectorizer类可以帮助我们完成向量化,TF-IDF和标准化三步。当然,还可以帮我们处理停用词。这部分工作和中文的特征处理也是完全相同的,大家参考前文即可。

英文文本挖掘预处理八:建立分析模型

有了每段文本的TF-IDF的特征向量,我们就可以利用这些数据建立分类模型,或者聚类模型了,或者进行主题模型的分析。此时的分类聚类模型和之前讲的非自然语言处理的数据分析没有什么两样。因此对应的算法都可以直接使用。而主题模型是自然语言处理比较特殊的一块,这个我们后面再单独讲。

英文文本挖掘预处理总结

上面我们对英文文本挖掘预处理的过程做了一个总结,希望可以帮助到大家。需要注意的是这个流程主要针对一些常用的文本挖掘,并使用了词袋模型,对于某一些自然语言处理的需求则流程需要修改。比如有时候需要做词性标注,而有时候我们也需要英文分词,比如得到”New York”而不是“New”和“York”,因此这个流程仅供自然语言处理入门者参考,我们可以根据我们的数据分析目的选择合适的预处理方法。

文本分类系列5-Hierarchical Attention Networks

Hierarchical Attention Networks for Document Classification

paper reading

主要原理:

the Hierarchical Attention Network (HAN) that is designed to capture two basic insights about document structure. First, since **documents

have a hierarchical structure (words form sentences, sentences form a document)**, we likewise construct a document representation by first building representations of sentences and then aggregating those into

a document representation. Second, it is observed that different words and sentences in a documents are differentially informative.

对于一个document含有这样的层次结构,document由sentences组成,sentence由words组成。

the importance of words and sentences are highly context dependent, i.e. the same word or sentence may be differentially important in different context (x3.5). To include sensitivity to this fact, our model includes two levels of attention mechanisms (Bahdanau et al., 2014; Xu et al., 2015) — one at the word level and one at the sentence level — that let the model to pay more or less attention to individual words and sentences when constructing the representation of the document.

words和sentences都是高度上下文依赖的,同一个词或sentence在不同的上下文中,其表现的重要性会有差别。因此,这篇论文中使用了两个attention机制,来表示结合了上下文信息的词或句子的重要程度。(这里结合的上下文的词或句子,就是经过RNN处理后的隐藏状态)。

Attention serves two benefits: not only does it often result in better performance, but it also provides insight into which words and sentences contribute to the classification decision which can be of value in applications and analysis (Shen et al., 2014; Gao et

al., 2014)

attention不仅有好的效果,而且能够可视化的看见哪些词或句子对哪一类document的分类影响大。

本文的创新点在于,考虑了ducument中sentence这一层次结构,因为对于一个document的分类,可能前面几句话都是废话,而最后一句话来了一个转折,对document的分类起决定性作用。而之前的研究,只考虑了document中的词。

Model Architecture

GRU-based sequence encoder

reset gate: controls how much the past state contributes to the candidate state.

$$r_t=\sigma(W_rx_t+U_rh_{t-1}+b_r)$$

candidate state:

$$\tilde h_t=tanh(W_hx_t+r_t\circ (U_hh_{t-1})+b_h)$$

update gate: decides how much past information is kept and how much new information is added.

$$z_t=\sigma(W_zx_t+U_zh_{t-1}+b_z)$$

new state: a linear interpolation between the previous state $h_{t−1}$ and the current new state $\tilde h_t$ computed with new sequence information.

$$h_t=(1-z_t)\circ h_{t-1}+z_t\circ \tilde h_t$$

Hierarchical Attention

Word Encoder

$$x_{it}=W_ew_{it}, t\in [1, T]$$

$$\overrightarrow h_{it}=\overrightarrow {GRU}(x_{it}),t\in[1,T]$$

$$\overleftarrow h_{it}=\overleftarrow {GRU}(x_{it}),t\in [T,1]$$

$$h_{it} = [\overrightarrow h_{it},\overleftarrow h_{it}]$$

i means the $i^{th}$ sentence in the document, and t means the $t^{th}$ word in the sentence.

Word Attention

Not all words contribute equally to the representation of the sentence meaning.

Hence, we introduce attention mechanism to extract such words that are important to the meaning of the sentence and aggregate the representation of those informative words to form a sentence vector.

Attention机制说到底就是给予sentence中每个结合了上下文信息的词一个权重。关键在于这个权重怎么确定?

$$u_{it}=tanh(W_wh_{it}+b_w)$$

$$\alpha_{it}=\dfrac{exp(u_{it}^Tu_w)}{\sum_t^Texp(u_{it}^Tu_w)}$$

$$s_i=\sum_t^T\alpha_{it}h_{it}$$

这里首先是将 $h_{it}$ 通过一个全连接层得到 hidden representation $u_{it}$,然后计算 $u_{it}$ 与 $u_w$ 的相似性。并通过softmax归一化得到每个词与 $u_w$ 相似的概率。越相似的话,这个词所占比重越大,对整个sentence的向量表示影响越大。

那么关键是这个 $u_w$ 怎么表示?

The context vector $u_w$ can be seen as a high level representation of a fixed

query “what is the informative word” over the words like that used in memory networks (Sukhbaatar et al., 2015, End-to-end memory networks.; Kumar et al., 2015, Ask me anything: Dynamic memory networks for natural language processing.). The word context vector $u_w$ is randomly initialized and jointly learned during the training process.

Sentence Encoder

$$\overrightarrow h_{i}=\overrightarrow {GRU}(s_{i}),t\in[1,L]$$

$$\overleftarrow h_{i}=\overleftarrow {GRU}(s_{i}),t\in [L,1]$$

$$H_i=[\overrightarrow h_{i}, \overleftarrow h_{i}]$$

hi summarizes the neighbor sentences around sentence i but still focus on sentence i.

Sentence Attention

$$u_i=tanh(W_sH_i+b_s)$$

$$\alpha_i=\dfrac{exp(u_i^Tu_s)}{\sum_i^Lexp(u_i^Tu_s)}$$

$$v = \sum_i^L\alpha_ih_i$$

同样的 $u_s$ 表示: a sentence level context vector $u_s$

Document Classification

The document vector v is a high level representation

of the document and can be used as features for document classification:

$$p=softmax(W_cv+b_c)$$

代码实现

需要注意的问题

  • 如果使用tensorboard可视化

  • 变量范围的问题

Context dependent attention weights

Visualization of attention

文本分类系列4-textRCNN

paper reading

paper: Recurrent Convolutional Neural Networks for Text Classification

Introduction

先对之前的研究进行一番批判(0.0).

  1. 传统的文本分类方法都是基于特征工程 feature representation,主要包括:
  • 词袋模型 bag-of-words(BOW)model,用于提取unigram, bigram, n-grams的特征。

  • 常见的特征选择的方法:frequency, MI (Cover and Thomas 2012), pLSA (Cai and Hofmann 2003), LDA (Hingmire et al. 2013),用于选择具有更好的判别效果的特征。

其原理就是去噪声来提高分类效果。比如去掉停用词,使用信息增益,互信息,或者L1正则化来获取有用的特征。但传统的特征表示的方法通常忽视了上下文信息和词序信息。

  1. Richard Socher 提出的 Recursive Neural Network

RecusiveNN 通过语言的tree结构来获取句子的语义信息。但是分类的准确率太依赖文本的树结构。在文本分类之前建立一个树结构需要的计算复杂度就是 $O(n^2)$ (n是句子的长度)。所以对于很长的句子并不适用。

  1. 循环神经网络 Recurrent Neural Network

计算复杂度是 $O(n)$,优点是能够很好的捕获长文本的语义信息,但是在rnn模型中,later words are more dominatant than earlier words. 但是如果对与某一个文本的分类,出现在之前的word影响更大的话,RNN的表现就不会很好。

为解决RNN这个问题,可以将CNN这个没有偏见的模型引入到NLP的工作中来,CNN能公平的对待句子中的每一个短语。 To tackle the bias problem, the Convolutional Neural Network (CNN), an unbiased model is introduced to NLP tasks, which can fairly determine discriminative phrases in a text with a max-pooling layer.

但是呢,通过前面的学习我们知道CNN的filter是固定尺寸的(fixed window),如果尺寸太短,会丢失很多信息,如果尺寸过长,计算复杂度又太大。所以作者提出个问题:能不能通过基于窗口的神经网络(CNN)学到更多的上下文信息,更好的表示文本的语义信息呢? Therefore, it raises a question: can we learn more contextual information than conventional window-based neural networks and represent the semantic of texts more precisely for text classification.

于是,这篇论文提出了 Recurrent Concolution Neural Network(RCNN).

Model

$$c_l{(w_i)} = f(W^{(l)}c_l(w_{i-1})+W^{(sl)}e(w_{i-1}))$$

$$c_r{(w_i)} = f(W^{(r)}c_r(w_{i-1})+W^{(sr)}e(w_{i-1}))$$

这两个公式类似于双向RNN,将 $c_l(w_i)$ 看作前一个时刻的隐藏状态 $h_{t-1}$, $c_l(w_{i-1})$ 就是 t-2 时刻的隐藏状态 $h_{t-2}$… 所以这就是个双向RNN…. 然后比较有创新的是,作者将隐藏状态 $h_{t-1}$ 和 $\tilde h_{t+1}$ ($\tilde h$ 表示反向), 以及当前word的词向量堆在一起,作为当前词以及获取了上下文信息的向量表示。

$$x_i = [c_l(w_i);e(w_i);c_r(w_i)]$$

然后是一个全连接层,这个可以看做textCNN中的卷积层,只是filter_size=1:

$$y_i^{(2)}=tanh(W^{(2)}x_i+b^{(2)})$$

接着是最大池化层:

$$y^{(3)} = max_{i=1}^ny_i^{(2)}$$

然后是全连接层+softmax:

$$y^{(4)} = W^{(4)}y^{(3)}+b^{(4)}$$

$$p_i=\dfrac{exp(y_i^{(4)})}{\sum_{k=1}^nexp(y_k^{(4)})}$$

感觉就是双向rnn呀,只不过之前的方法是用最后一个隐藏层的输出作为整个sentence的向量表示,但这篇论文是用每一个时刻的向量表示(叠加了上下时刻的隐藏状态),通过卷积层、maxpool后得到的向量来表示整个sentence.

确实是解决了RNN过于重视句子中靠后的词的问题,但是RNN训练慢的问题还是没有解决呀。但是在这里 brightmart/text_classification 中textCNN 和 RCNN的训练时间居然是一样的。why?

Results and Discussion

代码实现

需要注意的问题:

  • tf.nn.rnn_cell.DropoutWrapper
  • tf.nn.bidirectional_dynamic_rnn
  • tf.einsum
  • 损失函数的对比 tf.nn.softmax_cross_entropy_with_logits
  • 词向量是否需要正则化
  • tensorflow.contrib.layers.python.layers import optimize_loss 和 tf.train.AdamOptimizer(learning_rate).minimize(self.loss, self.global_steps) 的区别

文本分类系列3-TextRNN

paper reading

尽管TextCNN能够在很多任务里面能有不错的表现,但CNN有个最大问题是固定 filter_size 的视野,一方面无法建模更长的序列信息,另一方面 filter_size 的超参调节也很繁琐。CNN本质是做文本的特征表达工作,而自然语言处理中更常用的是递归神经网络(RNN, Recurrent Neural Network),能够更好的表达上下文信息。具体在文本分类任务中,Bi-directional RNN(实际使用的是双向LSTM)从某种意义上可以理解为可以捕获变长且双向的的 “n-gram” 信息。

RNN算是在自然语言处理领域非常一个标配网络了,在序列标注/命名体识别/seq2seq模型等很多场景都有应用,Recurrent Neural Network for Text Classification with Multi-Task Learning文中介绍了RNN用于分类问题的设计,下图LSTM用于网络结构原理示意图,示例中的是利用最后一个词的结果直接接全连接层softmax输出了。

关于解决RNN无法并行化,计算效率低的问题

Factorization tricks for LSTM networks

  • We present two simple ways of reducing the number of parameters and accelerating the training of large Long Short-Term Memory (LSTM) networks: the first one is “matrix factorization by design” of LSTM matrix into the product of two smaller matrices, and the second one is partitioning of LSTM matrix, its inputs and states into the independent groups. Both approaches allow us to train large LSTM networks significantly faster to the near state-of the art perplexity while using significantly less RNN parameters.

Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer

  • The capacity of a neural network to absorb information is limited by its number of parameters. Conditional computation, where parts of the network are active on a per-example basis, has been proposed in theory as a way of dramatically increasing model capacity without a proportional increase in computation. In practice, however, there are significant algorithmic and performance challenges. In this work, we address these challenges and finally realize the promise of conditional computation, achieving greater than 1000x improvements in model capacity with only minor losses in computational efficiency on modern GPU clusters. We introduce a Sparsely-Gated Mixture-of-Experts layer (MoE), consisting of up to thousands of feed-forward sub-networks. A trainable gating network determines a sparse combination of these experts to use for each example. We apply the MoE to the tasks of language modeling and machine translation, where model capacity is critical for absorbing the vast quantities of knowledge available in the training corpora. We present model architectures in which a MoE with up to 137 billion parameters is applied convolutionally between stacked LSTM layers. On large language modeling and machine translation benchmarks, these models achieve significantly better results than state-of-the-art at lower computational cost.

文本分类系列2-textCNN

paper reading

主要框架和使用CNN进行文本分类的意图参考paper: Convolutional Neural Networks for Sentence Classification 可参考cs224d中的课堂笔记,这堂课就是讲的这篇paper:

cs224d-lecture13 卷积神经网络

TextCNN详细过程

  • 第一层是图中最左边的7乘5的句子矩阵,每行是词向量,维度=5,这个可以类比为图像中的原始像素点了,然后在图像中图像的表示是[length, width, channel],这里将文本的表示[sequence_len, embed_size, 1]。可以看到下面代码中:
1
2
3
4
5
6
7

embeded_words = tf.nn.embedding_lookup(self.embedding, self.input_x) # [None, sentence_len, embed_size]

# three channels similar to the image. using the tf.nn.conv2d

self.sentence_embedding_expanded = tf.expand_dims(embeded_words, axis=-1) # [None, sentence_len, embed_size, 1]

  • 然后经过有 filter_size=(2,3,4) 的一维卷积层,每个filter_size 有两个输出 channel。上图中的filter有3个,分别为:

    • filter:[2, 5, 2] ==> feature map:[6,1,2]

    • filter:[3, 5, 2] ==> feature map:[5,1,2]

    • filter:[4, 5, 2] ==> feature map:[4,1,2]

    • 第三维表示channels,卷积后得到两个feature maps.

  • 第三层是一个1-max pooling层,这样不同长度句子经过pooling层之后都能变成scale,这里每个fiter_size的channel为2,所以输入的pooling.shape=[batch_size,1,1,2], 然后concat为一个flatten向量。
  • 最后接一层全连接的 softmax 层,输出每个类别的概率。

特征:这里的特征就是词向量,有静态(static)和非静态(non-static)方式。static方式采用比如word2vec预训练的词向量,训练过程不更新词向量,实质上属于迁移学习了,特别是数据量比较小的情况下,采用静态的词向量往往效果不错。non-static则是在训练过程中更新词向量。推荐的方式是 non-static 中的 fine-tunning方式,它是以预训练(pre-train)的word2vec向量初始化词向量,训练过程中调整词向量,能加速收敛,当然如果有充足的训练数据和资源,直接随机初始化词向量效果也是可以的。

通道(Channels):图像中可以利用 (R, G, B) 作为不同channel,而文本的输入的channel通常是不同方式的embedding方式(比如 word2vec或Glove),实践中也有利用静态词向量和fine-tunning词向量作为不同channel的做法。下面代码中的通道为1.

一维卷积(conv-1d):图像是二维数据,经过词向量表达的文本为一维数据,因此在TextCNN卷积用的是一维卷积。一维卷积带来的问题是需要设计通过不同 filter_size 的 filter 获取不同宽度的视野。

Pooling层:利用CNN解决文本分类问题的文章还是很多的,比如这篇 A Convolutional Neural Network for Modelling Sentences 最有意思的输入是在 pooling 改成 (dynamic) k-max pooling ,pooling阶段保留 k 个最大的信息,保留了全局的序列信息。比如在情感分析场景,举个例子:

        “ 我觉得这个地方景色还不错,但是人也实在太多了 ”

虽然前半部分体现情感是正向的,全局文本表达的是偏负面的情感,利用 k-max pooling能够很好捕捉这类信息。

代码实现

参数设置和模型具体实现参考paper: A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classification

设计一个模型需要考虑的:

  • input word vector representations 输入的词向量表示

  • filter region size(s); 卷积核的大小

  • the number of feature maps; 特征图的通道数

  • the activation function 激活函数

  • the pooling strategy 池化的方式

  • regularization terms (dropout/l2) 正则化项(dropout/l2)

需要注意的问题

一个单本对应单个标签和多个标签的区别?

关于多标签分类,应该看看周志华老师的这篇文章A Review on Multi-Label Learning Algorithms, 知乎上还有其他资料多标签(multi-label)数据的学习问题,常用的分类器或者分类策略有哪些?

本文代码中的方法:

  • 真实值labels的输入:单个标签的真实值是 input_y.shape=[batch_size], 多个标签的真实值是 input_y_multilabels.shape=[batch_size, label_size]
1
2
3
4
5

self.input_y = tf.placeholder(dtype=tf.int32, shape=[None], name='input_y')

self.input_y_multilabels = tf.placeholder(dtype=tf.float32, shape=[None, num_classes], name="input_y_multilabels")

  • 损失函数的选择:
  • 评价指标的区别:

文本分类系列0:NLTK学习和特征工程

计算语言:简单的统计

1
2
3

from nltk.book import *

*** Introductory Examples for the NLTK Book ***

Loading text1, ..., text9 and sent1, ..., sent9

Type the name of the text or sentence to view it.

Type: 'texts()' or 'sents()' to list the materials.

text1: Moby Dick by Herman Melville 1851

text2: Sense and Sensibility by Jane Austen 1811

text3: The Book of Genesis

text4: Inaugural Address Corpus

text5: Chat Corpus

text6: Monty Python and the Holy Grail

text7: Wall Street Journal

text8: Personals Corpus

text9: The Man Who Was Thursday by G . K . Chesterton 1908

找出text1,《白鲸记》中的词monstrous,以及其上下文

1
2
3

text1.concordance("monstrous", width=40, lines=10)

Displaying 10 of 11 matches:

 was of a most monstrous size . ... Thi

 Touching that monstrous bulk of the wh

enish array of monstrous clubs and spea

 wondered what monstrous cannibal and s

e flood ; most monstrous and most mount

Moby Dick as a monstrous fable , or sti

PTER 55 Of the Monstrous Pictures of Wh

exion with the monstrous pictures of wh

ose still more monstrous stories of the

ed out of this monstrous cabinet there

找出text1中与monstrous具有相同语境的词。比如monstrous的上下文 the __ pictures, the __ size. 同样在text1中与monstrous类似的上下文的词。很好奇这个是怎么实现的?

1
2
3
4
5
6
7
8
9
10
11

def similar(self, word, num=20):

"""

Distributional similarity: find other words which appear in the

same contexts as the specified word; list most similar words first.

"""

1
2
3

text1.similar("monstrous")

true contemptible christian abundant few part mean careful puzzled

mystifying passing curious loving wise doleful gamesome singular

delightfully perilous fearless

共用两个或两个以上词汇的上下文,如monstrous和very

1
2
3

text2.common_contexts(["monstrous", "very"])

a_pretty am_glad a_lucky is_pretty be_glad

自动检测出现在文本中的特定词,并显示同一上下文中出现的其他词。text4是《就职演说语料》,

1
2
3
4
5

if __name__ == "__main__":

text4.dispersion_plot(["citizens", "liberty", "freedom"])

<matplotlib.figure.Figure at 0x7f3794818588>

如果不使用 if name==”main“ 的话会报错``` ‘NoneType’ object has no attribute ‘show’

1
2
3
4
5
6
7
8
9
10
11
12
13





```python

fdist1 = FreqDist(text1)

vocabulary1 = list(fdist1.keys()) # keys() 返回key值组成的list

print(vocabulary1[:10])

['[', 'Moby', 'Dick', 'by', 'Herman', 'Melville', '1851', ']', 'ETYMOLOGY', '.']

需要加list,不然回报错,“TypeError: 'dict_keys' object is not subscriptable”

dict.keys() returns an iteratable but not indexable object. The most simple (but not so efficient) solution would be:

1
2
3
4
5
6
7

# 同样的道理这里也需要加list,因为生成的<class 'dict_items'>z在python3中是迭代器

print(type(fdist1.items()))

print(list(fdist1.items())[:10])

<class 'dict_items'>

[('[', 3), ('Moby', 84), ('Dick', 84), ('by', 1137), ('Herman', 1), ('Melville', 1), ('1851', 3), (']', 1), ('ETYMOLOGY', 1), ('.', 6862)]
1
2
3
4
5
6
7
8
9

# dict.items() 实际上是将dict转换为可迭代对象list,list的对象是 ('[', 3), ('Moby', 84), ('Dick', 84), ('by', 1137)这样的

# 这下总能记住dict按照value排序了吧。。。尴尬,以前居然没弄懂??

fdist_sorted = sorted(fdist1.items(), key=lambda item:item[1], reverse=True)

print(fdist_sorted[:10])

[(',', 18713), ('the', 13721), ('.', 6862), ('of', 6536), ('and', 6024), ('a', 4569), ('to', 4542), (';', 4072), ('in', 3916), ('that', 2982)]
1
2
3
4
5
6
7

# 这个就是按照key排序。

fdist_sorted2 = sorted(fdist1.keys(), reverse=True)

print(fdist_sorted2[:10])

['zoology', 'zones', 'zoned', 'zone', 'zodiac', 'zig', 'zephyr', 'zeal', 'zay', 'zag']
1
2
3

fdist1.plot(20, cumulative=True)

png

可以看到高频词大都是无用的停用词

1
2
3
4
5
6
7
8
9
10
11
12
13

# 低频词 fdist.hapaxes() 出现次数为1的词

print(len(fdist1.hapaxes()))

for i in fdist1.hapaxes():

if fdist1[i] is not 1:

print("hh")



9002

可以看到低频词也很多,而且大都也是很无用的词。

词语搭配

1
2
3

list(bigrams(['more', 'is', 'sad', 'than', 'done']))

[('more', 'is'), ('is', 'sad'), ('sad', 'than'), ('than', 'done')]
1
2
3

text4.collocations(window_size=4)

United States; fellow citizens; four years; years ago; men women;

Federal Government; General Government; self government; Vice

President; American people; every citizen; within limits; Old World;

Almighty God; Fellow citizens; Chief Magistrate; Chief Justice; one

another; Declaration Independence; protect defend

文本4是就职演说语料,可以看到n-grams能够很好的展现出文本的特性,说明n-grams是不错的特征。

collections()源码

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
44
45
46
47
48
49

def collocations(self, num=20, window_size=2):

"""

Print collocations derived from the text, ignoring stopwords.



:seealso: find_collocations

:param num: The maximum number of collocations to print.

:type num: int

:param window_size: The number of tokens spanned by a collocation (default=2)

:type window_size: int

"""

if not ('_collocations' in self.__dict__ and self._num == num and self._window_size == window_size):

self._num = num

self._window_size = window_size



#print("Building collocations list")

from nltk.corpus import stopwords

ignored_words = stopwords.words('english')

finder = BigramCollocationFinder.from_words(self.tokens, window_size)

finder.apply_freq_filter(2)

finder.apply_word_filter(lambda w: len(w) < 3 or w.lower() in ignored_words)

bigram_measures = BigramAssocMeasures()

self._collocations = finder.nbest(bigram_measures.likelihood_ratio, num)

colloc_strings = [w1+' '+w2 for w1, w2 in self._collocations]

print(tokenwrap(colloc_strings, separator="; "))

自动理解自然语言

  • 词义消歧 Ambiguity 关于词义消歧的理解可以看之前的笔记chapter12-句法分析

  • 指代消解 anaphora resolution

  • 自动问答

  • 机器翻译

  • 人机对话系统

获得文本语料和词汇资源

布朗语料库

1
2
3

from nltk.corpus import brown

有以下这些类别的文本

1
2
3

print(brown.categories())

['adventure', 'belles_lettres', 'editorial', 'fiction', 'government', 'hobbies', 'humor', 'learned', 'lore', 'mystery', 'news', 'religion', 'reviews', 'romance', 'science_fiction']
1
2
3
4
5
6
7
8
9

import nltk

news_text = brown.words(categories="news")

fdist_news = nltk.FreqDist([w.lower() for w in news_text])

print(len(fdist_news))

13112

标注文本语料库

经过了标注的语料库,有词性标注、命名实体、句法结构、语义角色等。

分类和标注词汇

1
2
3
4
5
6
7

text = nltk.word_tokenize("and now for something completely differences!")

print(text)

print(nltk.pos_tag(text))

['and', 'now', 'for', 'something', 'completely', 'differences', '!']

[('and', 'CC'), ('now', 'RB'), ('for', 'IN'), ('something', 'NN'), ('completely', 'RB'), ('differences', 'VBZ'), ('!', '.')]
词性标注

NLTK中采用的方法可参考:A Good Part-of-Speech Tagger in about 200 Lines of Python

对于一些同形同音异义词,通过词性标注能消除歧义.很多文本转语音系统通常需要进行词性标注,因为不同意思发音会不太一样。

1
2
3
4
5
6
7
8
9

text1 = nltk.word_tokenize("They refuse to permit us tpo obtain the refuse permit")

print(nltk.pos_tag(text1))

text2 = nltk.word_tokenize("They refuse to permit us to obtain the refuse permit")

print(nltk.pos_tag(text2))

[('They', 'PRP'), ('refuse', 'VBP'), ('to', 'TO'), ('permit', 'VB'), ('us', 'PRP'), ('tpo', 'VB'), ('obtain', 'VB'), ('the', 'DT'), ('refuse', 'NN'), ('permit', 'NN')]

[('They', 'PRP'), ('refuse', 'VBP'), ('to', 'TO'), ('permit', 'VB'), ('us', 'PRP'), ('to', 'TO'), ('obtain', 'VB'), ('the', 'DT'), ('refuse', 'NN'), ('permit', 'NN')]
获取已经标注好的语料库
1
2
3

print(nltk.corpus.brown.tagged_words())

[('The', 'AT'), ('Fulton', 'NP-TL'), ...]
1
2
3
4
5

print(nltk.corpus.treebank.tagged_words())

print(nltk.corpus.treebank.tagged_sents()[0])

[('Pierre', 'NNP'), ('Vinken', 'NNP'), (',', ','), ...]

[('Pierre', 'NNP'), ('Vinken', 'NNP'), (',', ','), ('61', 'CD'), ('years', 'NNS'), ('old', 'JJ'), (',', ','), ('will', 'MD'), ('join', 'VB'), ('the', 'DT'), ('board', 'NN'), ('as', 'IN'), ('a', 'DT'), ('nonexecutive', 'JJ'), ('director', 'NN'), ('Nov.', 'NNP'), ('29', 'CD'), ('.', '.')]

查看brown语料库中新闻类最常见的词性

1
2
3
4
5
6
7

brown_news_tagged = brown.tagged_words(categories='news', tagset='universal')

tag_fd = nltk.FreqDist(tag for (word, tag) in brown_news_tagged)

tag_fd.keys()

dict_keys(['DET', 'NOUN', 'ADJ', 'VERB', 'ADP', '.', 'ADV', 'CONJ', 'PRT', 'PRON', 'NUM', 'X'])

文本分类

朴素贝叶斯分类

选取特征,将名字的最后一个字母作为特征. 返回的字典称为特征集

1
2
3
4
5
6
7

def gender_features(word):

return {'last_letter':word[-1]}

gender_features('Shrek')

{'last_letter': 'k'}

定义一个特征提取器

1
2
3
4
5
6
7
8
9

from nltk.corpus import names

import random

names = ([(name, 'male') for name in names.words('male.txt')] +

[(name, 'female') for name in names.words('female.txt')])

1
2
3
4
5

print(nltk.corpus.names.words('male.txt')[:10])

print(names[:10])

['Aamir', 'Aaron', 'Abbey', 'Abbie', 'Abbot', 'Abbott', 'Abby', 'Abdel', 'Abdul', 'Abdulkarim']

[('Aamir', 'male'), ('Aaron', 'male'), ('Abbey', 'male'), ('Abbie', 'male'), ('Abbot', 'male'), ('Abbott', 'male'), ('Abby', 'male'), ('Abdel', 'male'), ('Abdul', 'male'), ('Abdulkarim', 'male')]

使用特征提取器处理names数据,并把数据集分为训练集和测试集

1
2
3
4
5

# 二分类

features = [(gender_features(n), g) for (n, g) in names]

1
2
3

train_set, test_set = features[500:], features[:500]

1
2
3

print(train_set[:10])

[({'last_letter': 'n'}, 'male'), ({'last_letter': 'e'}, 'male'), ({'last_letter': 'e'}, 'male'), ({'last_letter': 'b'}, 'male'), ({'last_letter': 'b'}, 'male'), ({'last_letter': 'e'}, 'male'), ({'last_letter': 'y'}, 'male'), ({'last_letter': 'y'}, 'male'), ({'last_letter': 't'}, 'male'), ({'last_letter': 'e'}, 'male')]
1
2
3

classifier = nltk.NaiveBayesClassifier.train(train_set)

1
2
3
4
5

# 预测一个未出现的名字

classifier.classify(gender_features('Pan'))

'male'
1
2
3
4
5

# 测试集上的准确率

print(nltk.classify.accuracy(classifier, test_set))

0.602
1
2
3

classifier.show_most_informative_features(5)

Most Informative Features

             last_letter = 'a'            female : male   =     35.5 : 1.0

             last_letter = 'k'              male : female =     34.1 : 1.0

             last_letter = 'f'              male : female =     15.9 : 1.0

             last_letter = 'p'              male : female =     13.5 : 1.0

             last_letter = 'v'              male : female =     12.7 : 1.0

构建包含所有实例特征的单独list会占用大量内存,所有应该把这些特征集成起来。

定义一个特征提取器包含多个特征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 添加多个特征

from nltk.classify import apply_features

def gender_features2(word):

features = {}

features['firstletter'] = word[0].lower()

features['lastletter'] = word[-1].lower()

for letter in 'abcdefghijklmnopqrstuvwxyz':

features["count(%s)"%letter] = word.lower().count(letter)

return features

1
2
3
4
5

print(gender_features2('xiepan'))

print(len(gender_features2('xiepan'))) # 有28个特征, 2+26=28

{'firstletter': 'x', 'lastletter': 'n', 'count(a)': 1, 'count(b)': 0, 'count(c)': 0, 'count(d)': 0, 'count(e)': 1, 'count(f)': 0, 'count(g)': 0, 'count(h)': 0, 'count(i)': 1, 'count(j)': 0, 'count(k)': 0, 'count(l)': 0, 'count(m)': 0, 'count(n)': 1, 'count(o)': 0, 'count(p)': 1, 'count(q)': 0, 'count(r)': 0, 'count(s)': 0, 'count(t)': 0, 'count(u)': 0, 'count(v)': 0, 'count(w)': 0, 'count(x)': 1, 'count(y)': 0, 'count(z)': 0}

28
1
2
3
4
5
6
7

# 对每个样本进行特征处理

features = [(gender_features(n), g) for (n,g) in names]

print(len(features))

7944
1
2
3
4
5
6
7
8
9

# 训练集,开发集和测试集

train_set = features[1500:]

dev_set = apply_features(gender_features2, names[500:1500])

test_set = apply_features(gender_features2, names[:500])

1
2
3
4
5
6
7
8
9

classifier = nltk.NaiveBayesClassifier.train(train_set)

print(nltk.classify.accuracy(classifier, dev_set))

print(nltk.classify.accuracy(classifier, test_set))

print(nltk.classify.accuracy(classifier, train_set)) ## 明显过拟合了~

0.007

0.008

0.883302296710118

文档分类

1
2
3
4
5
6
7

from nltk.corpus import movie_reviews

documents = [(list(movie_reviews.words(fileid)), category) for category in movie_reviews.categories()

for fileid in movie_reviews.fileids(category) ]

1
2
3

movie_reviews.categories()

['neg', 'pos']
1
2
3
4
5
6
7
8
9

neg_docu = movie_reviews.fileids('neg')

print(len(neg_docu)) # neg类别的文档数 1000

print(len(documents)) # 总的文档数 1000

len(movie_reviews.words(neg_docu[0])) # 第一个文件中单词数 879

1000

2000

879
1
2
3

random.shuffle(documents)

文档分类的特征提取器

所谓特征提取器实际上就是将文档原本的内容用认为选定的特征来表示。然后用分类器找出这些特征和对应类标签的映射关系。

那么什么样的特征才是好的特征,这就是特征工程了吧。

文本分类概述

文本分类,顾名思义,就是根据文本内容本身将文本归为不同的类别,通常是有监督学习的任务。根据文本内容的长短,有做句子、段落或者文章的分类;文本的长短不同可能会导致文本可抽取的特征上的略微差异,但是总体上来说,文本分类的核心都是如何从文本中抽取出能够体现文本特点的关键特征,抓取特征到类别之间的映射。 所以,特征工程就显得非常重要,特征找的好,分类效果也会大幅提高(当然前提是标注数据质量和数量也要合适,数据的好坏决定效果的下限,特征工程决定效果的上限)。

也许会有人问最近的深度学习技术能够避免我们构造特征这件事,为什么还需要特征工程?深度学习并不是万能的,在NLP领域深度学习技术取得的效果有限(毕竟语言是高阶抽象的信息,深度学习在图像、语音这些低阶具体的信息处理上更适合,因为在低阶具体的信息上构造特征是一件费力的事情),并不是否认深度学习在NLP领域上取得的成绩,工业界现在通用的做法都是会把深度学习模型作为系统的一个子模块(也是一维特征),和一些传统的基于统计的自然语言技术的特征,还有一些针对具体任务本身专门设计的特征,一起作为一个或多个模型(也称Ensemble,即模型集成)的输入,最终构成一个文本处理系统。

特征工程

那么,对于文本分类任务而言,工业界常用到的特征有哪些呢?下面用一张图以概括:

我主要将这些特征分为四个层次,由下往上,特征由抽象到具体,粒度从细到粗。我们希望能够从不同的角度和纬度来设计特征,以捕捉这些特征和类别之间的关系。下面详细介绍这四个层次上常用到的特征表示。

文本分类系列1-fasttext

在Facebook fasttext github主页中,关于fasttext的使用包括两个方面,词向量表示学习以及文本分类。

Paper Reading1

这篇文章是用来进行文本分类的: Bag of Tricks for Efficient Text Classification, Armand Joulin, Edouard Grave, Piotr Bojanowski, Tomas Mikolov

这个模型跟word2vec 中的CBOw模型极其相似,区别在于将中心词换成文本标签。那么输入层是文本中单词经过嵌入曾之后的词向量构成的的n-gram,然后求平均操作得到一个文本sentence的向量,也就是隐藏层h,然后再经过一个输出层映射到所有类别中,论文里面还详细论述了如何使用n-gram feature考虑单词的顺序关系,以及如何使用Hierarchical softmax机制加速softmax函数的计算速度。模型的原理图如下所示:

目标函数:

$$\dfrac{1}{N}\sum_{n=1}^Ny_nlog(f(BAx_n))$$

  • N表示文本数量,训练时就是Batch size吧?

  • $x_n$ 表示第n个文本的 normalized bag of features

  • $y_n$ 表示第n个文本的类标签

  • A is the look up table over n-gram. 类似于attention中的权重吧

  • B is the weight matrix

隐藏层到输出层的计算复杂度是 $O(hk)$. h是隐藏层的维度,k是总的类别数。经过hierarchical softmax处理后,复杂度为 $O(hlog_2k)$

  • 这种模型的优点在于简单,无论训练还是预测的速度都很快,比其他深度学习模型高了几个量级

  • 缺点是模型过于简单,准确度较低。

paper reading2

这篇文章是在word2vec的基础上拓展了,用来学习词向量表示 Enriching Word Vectors with Subword Information

Abstract

Popular models that learn such representations ignore the morphology of words, by assigning a distinct vector to each word.

之前的模型在用离散的向量表示单词时都忽略了单词的形态。

In this paper, we propose a new approach based on the skipgram model, where each word is represented as a bag of character n-grams. A vector representation is associated to each character n-gram; words being represented as the sum of these representations.

这篇文章提出了一个skipgram模型,其中每一个单词表示为组成这个单词的字袋模型 a bag of character n-grams. 一个单词的词向量表示为这些 n-grams表示的总和。

Our main contribution is to introduce an extension of the continuous skipgram model (Mikolov et al., 2013b), which takes into account subword information. We evaluate this model on nine languages exhibiting different morphologies, showing the benefit of our approach. 这篇文章可以看作是word2vec的拓展,主要是针对一些形态特别复杂的语言。

word2vec在词汇建模方面产生了巨大的贡献,然而其依赖于大量的文本数据进行学习,如果一个word出现次数较少那么学到的vector质量也不理想。针对这一问题作者提出使用subword信息来弥补这一问题,简单来说就是通过词缀的vector来表示词。比如unofficial是个低频词,其数据量不足以训练出高质量的vector,但是可以通过un+official这两个高频的词缀学习到不错的vector。方法上,本文沿用了word2vec的skip-gram模型,主要区别体现在特征上。word2vec使用word作为最基本的单位,即通过中心词预测其上下文中的其他词汇。而subword model使用字母n-gram作为单位,本文n取值为3~6。这样每个词汇就可以表示成一串字母n-gram,一个词的embedding表示为其所有n-gram的和。这样我们训练也从用中心词的embedding预测目标词,转变成用中心词的n-gram embedding预测目标词。

Morphological word representations

针对形态词表示已有的工作:

传统的用单词的形态特征来表示单词:

  • [Andrei Alexandrescu and Katrin Kirchhoff. 2006. Factored neural language models. In Proc. NAACL] introduced factored neural language models. 因式分解模型

    • words are represented as sets of features.

    • These features might include morphological information

  • Schütze (1993) learned representations of character four-grams through singular value decomposition, and derived representations for words by summing

the four-grams representations. 这篇文正的工作跟本文的方法是比较接近的。

Character level features for NLP NLP的字符特征

字符级别的研究工作最近很多了。一类是基于RNN的,另一类是基于CNN的。

General Model

这一部分是对word2vec中跳字模型的回顾。Skip-gram predicts the distribution (probability) of context words from a center word.

giving a sequence of words $w_1, w_2,…,w_T$

那么skipgram 模型就是最大化对数似然函数:

$$\sum_{t=1}^T\sum_{c\in C_t}logp(w_c|w_t)$$

we are given a scoring function s which maps pairs of (word, context) to scores in R.

$$p(w_c|w_t)=\dfrac{e^{s(w_t,w_c)}}{\sum_{j=1}^We^{s(w_t,j)}}$$

The problem of predicting context words can instead be framed as a set of independent binary classification tasks. Then the goal is to independently predict the presence (or absence) of context words. For the word at position t we consider all context words as positive examples and sample negatives at

random from the dictionary.

这篇文章采用负采样的方法。与原本的softmax或者是hierarchical softmax不一样的是,负采样中预测一个上下文的单词context $w_c$ 是把它看做一个独立的二分类,存在或者是不存在。

因此选择一个上下文 position c, using binary logistic loss we obtain the following negative log-likelihood::

$$log(1+e^{-s(w_t,w_c)})+\sum_{n\in N_{t,c}}log(1+e^{s(w_t,n)})$$

其实跟word2vec中是一样的就是 $-log\sigma(s(w_t,w_c))=-log\dfrac{1}{1+e^{-s(w_t,w_c)}}=log(1+e^{-s(w_t,w_c)})$

那么对于整个Sequence,设定 $l: x\rightarrow log(1+e^{-x})$ 那么:

$$\sum_{t=1}^T[\sum_{c\in C_t}l(s(w_t,w_c))+\sum_{n\in N_{t,c}}l(-s(w_t,n))]$$

$N_{t,c}$ is a set of negative examples sampled from the vocabulary. 怎么选负采样呢? 每个单词都被给予一个等于它频率的权重(单词出现的数目)的3/4次方。选择某个单词的概率就是它的权重除以所有单词权重之和。

$$p(w_i)=\dfrac{f(w_i)^{3/4}}{\sum_{j=0}^W(f(w_j)^{3/4})}$$

Then the score can be computed as the scalar product between word and context vectors as:

$$s(w_t,w_c) = u_{w_t}^Tv_{w_v}$$

Subword model

By using a distinct vector representation for each word, the skipgram model ignores the internal structure of words. In this section, we propose a different scoring function s, in order to take into account this information. 单词的离散词向量表示是忽略了单词内部的结构信息的,也就是其字母组成。

给每个单词左右加上 < 和 >,用来区分前缀和后缀。对于单词 where 来说,用 character trigram 表示:

用 $z_g$ 表示n-gram g 的向量表示。那么 scoring function:

$$s(w,c)=\sum_{g\in G_w}z_g^Tv_c$$

如果词表很大的话,其对应的 n-gram 也会非常多吧,为了限制占用的内存,we use a hashing function that maps n-grams to integers in 1 to K. We hash character sequences using the Fowler-Noll-Vo hashing function (specifically the FNV-1a variant).1 We set $K = 2.10^6$ below.

代码实现

需要注意的问题

  • 代码实现中对于sentence的向量表示,是unigram的平均值,如果要让效果更好,可以添加bigram, trigram等。
  • tf.train.exponential_decay
  • tf.nn.nce_loss