DarkMatter in Cyberspace
  • Home
  • Categories
  • Tags
  • Archives

Natural Language Processing in Action


本文是 Natural Language Processing in Action by Hobson Lane, 2019 的读书笔记, 相关代码见 my fork of the official repo。

Environment Setup

git clone git@github.com:slm-bj/nlpia.git
cd nlpia
python -m venv env
. env/bin/activate
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade pip
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -e .
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pynvim vaderSentiment

pip 的 -e 选项使得 pip 以 development mode 安装本地包(这里是 nlpia), 也就是在开发路径下新建一个以 .egg-info 结尾的目录(这里是 src/nlpia.egg-info) 作为本地包的安装路径(而不是安装到 PYTHON_HOME/site-packages 下), 另外安装的包可以不完全遵循 requirements.txt 的要求, 比如 tensorflow 实际安装的版本是 2.3.0,与 requirements.txt 指定的版本不符, 并且 requirements.txt 中的 AIML-Bot, Keras-Applications 等包都没有装。

Preface

如果思维是用自然语言表达的, NLP 就是实现通用人工智能 AGI 的关键步骤。

NLP 已经在影响社会和自我训练方面形成了闭环,发挥了很大作用, 未来的影响会越来越大。

About this book

Roadmap

运行代码:将数据文件(csv)放入 src/nlpia/data 目录, 然后运行 nlpia.data.loaders.get_data().

Part 1: 使用算数方法构建基本的 NLP 算法,比如 email spam filter;

Part 2: 使用神经网络构建 NLP 算法;

Part 3: 使用前面学到的方法构造真实的聊天机器人。

Chapter 1

1.4.3

bag-of-words 是输入文本去掉了 stop word(例如 a, of 等)和 rare word(例如人名) 后的词频统计表,见 p18 图示和下面的 Python 代码。 可以用 Python 的 collections.Counter 对象实现。

使用 bag-of-words 代表一句话(比如用户的提问), 再用 bag-of-words 代表一篇文档(最能回答用户提问的文档), 寻找这两个 vector 之间的关系。

1.5

第一种实用的 NLP 编码方法:one-hot encoded vectors (p20)

1.6

第一段谈到前面的 bag-of-words 方法虽然丢弃了单词顺序,但处理比较短的文本仍然效果良好, 我觉得原因在于英语、德语这类语言对语法成分的顺序不敏感, 但对于中文这样的分析型语言,顺序是特别重要的,采用 bag-of-words 效果大概不会好。

Chapter 2

2.2

bag-of-words vector = word frequency vector 只包含 word frequency,不包含词的顺序,每个向量长度为词汇表长度。 见 p39.

2.2.1

p40:

df = pd.DataFrame(pd.Series(dict([(token, 1) for token in sentence.split()])), columns=['sent']).T

注意这里如何用 pd.DataFrame 的 T 属性实现 dataframe 的行列对调。

p41: dot product 相当于 数据库的 inner join,相当于 矩阵乘法, 一个行向量 dot product 一个列向量得到一个标量。

2.2.2

Listing 2.6:

采用 dot 计算重合度的原理是:如果其中有一个向量在某个词下频率为0,则乘积为0:

              sent0  sent1  sent2  sent3
Thomas            1      0      0      0
Jefferson         1      0      0      0
began             1      0      0      0
building          1      0      0      0
Monticello        1      0      0      1
at                1      0      0      0
the               1      0      1      0
age               1      0      0      0
of                1      0      0      0
26.               1      0      0      0
Construction      0      1      0      0
was               0      1      0      1
done              0      1      0      0
mostly            0      1      0      0
by                0      1      0      0
local             0      1      0      0
masons            0      1      0      0
and               0      1      0      0
carpenters.       0      1      0      0
He                0      0      1      0
moved             0      0      1      0
into              0      0      1      1
South             0      0      1      0
Pavilion          0      0      1      0
in                0      0      1      0
1770.             0      0      1      0
Turning           0      0      0      1
a                 0      0      0      1
neoclassical      0      0      0      1
masterpiece       0      0      0      1
Jefferson's       0      0      0      1
obsession         0      0      0      1

2.2.3

p49. n-gram: 长度为n的词组,这里的词指的是空格分开的连续英文字母。 例如 "ice cream" 是 2-gram,"beyond the pale" 是 3-gram。

n-gram 可以有效的解决 tokenize 丢失词序导致的意思错误, 比如 "was not" 是一个 2-gram,整体作为一个 token, 比单个的 "was"、"not" 含义更准确。

p52, Stop Words

当资源重复,词汇表又比较大时,不要忽略 stop words. 由于 NLTK 的 stop words 列表随版本变化, 忽略 stop words 可能会导致程序运行结果随 NLTK 版本变化而变化。

2.2.5

缩小词汇表有助于降低 overfitting 的可能性。

Case Folding

将不同大小写的单词当成相同的单词。

最简单的方法:全部转小写。

会丢失信息,例如 Doctor 和 doctor。

可以只转换句首的大写字母,

仍然会丢失信息,要在效率和准确性之间权衡。

Stemming

去掉名词复数、所有格 (possessive words)、动行变形(时态等)产生的变化。

p58(拷贝自 2020.7.15 日记):

import re
>>> re.findall('^(.*ss|.*?)(s)?$', "housees")
[('housee', 's')]

| 的优先级比较低,所以正则表达式的意思是: 要么匹配 ss 结尾,要么以 non-greedy 方式匹配任何单词。

在 .*ss 情形下,由于采用贪婪模式,即使有多于两个的 s, 也仍然匹配最后的两个 s,所以第二组 (s)? 总为空。

如果不是以两个或两个以上 s 结尾,则由于采用 non-greedy 模式(星号后面的问好将星号转为非贪婪模式), 第二组能匹配则尽量匹配,也就是说,只要是一个 s 结尾,则一定被分配到第二组。

p59:Porter 一生心血打造的 stemmer 算法,就在那 300 行 Python 代码里, 让人好生感慨。

Lemmatization

lemma: 词根

Lemmatization 也会导致单词含义被改变,从而造成分析失真。

Lemmatizer 在 NLP pipeline 中的位置比 stmmer 靠前, 因为 lemmatizer 处理后的 token 仍然是正常的单词, 可以被 stemmer 使用。

POS: part of speech,大致相当于一个单词在句子中的角色,或者词性, 比如是名词、形容词还是动词。

p61: Princeton WordNet 居然连 best 和 good 的联系都没有,实在有失水准。

Use Case

是否使用 normalization 技术要随算法目标而定。 信息搜索场景中,如果需要找到尽可能多的结果,容忍一定的假阳性,则使用 normalization, 而聊天机器人对准确性的要求更高,所以一般不用 normalization。

Bottom line:除非特殊情况(比如研究对象是充满专业术语的论文等), 不要使用 normalization。

2.3

计算机技术中,heuristic 是人类通过经验编写的 if-else 式的算法, 与之相对的是基于机器学习,或者数学公式的算法。

Colins Cobuild 的解释是:

A heuristic computer program uses rules based on previous experience in order to solve a problem, rather than using a mathematical procedure.

Vader 使用 heuristic 方法做 sentiment 分析。

Naive Bayes 是有监督学习,首先基于一个打好标记的语料库上建立模型。

2.3.2

p67:

movies['predicted_sentiment'] = nb.predict_proba(df_bows) * 8 - 4

应改为

movies['predicted_sentiment'] = nb.predict_proba(df_bows)[:, 1] * 8 - 4

nb.predict_proba(df_bows) 的计算结果是一个形状为 (10605, 2) 的 numpy array, 行数与 movies 的观测数相等,每行包含两个值,分别是为 0 (代表文档有否定倾向) 和 为 1 (代表文档有肯定倾向)的概率,和为 1(没有其他情况)。 所以这里只取每个文档肯定倾向的概率作为 movies 的预测值。

赞同的概率再 * 8 - 4 的原因是赞同概率原来分布范围是 [0, 1], 但 movies.sentiment 分布范围是 [-4, 4](基于 movies.sentiment.describe())。

pandas.DataFrame.append() 可以将两个列不一样的 dataframe 以并集的方式连在一起:

In [10]: d1 = pd.DataFrame({"a": [1, 2], "b": [3, 4]})

In [12]: d2 = pd.DataFrame({"c": [11, 12, 5], "d": [13, 14, 18]})

In [13]: d1.append(d2)
Out[13]:
     a    b     c     d
0  1.0  3.0   NaN   NaN
1  2.0  4.0   NaN   NaN
0  NaN  NaN  11.0  13.0
1  NaN  NaN  12.0  14.0
2  NaN  NaN   5.0  18.0

In [14]: d3 = pd.DataFrame({"c": [11, 12, 5], "b": [13, 14, 18]})
In [16]: d1.append(d3)
Out[16]:
     a   b     c
0  1.0   3   NaN
1  2.0   4   NaN
0  NaN  13  11.0
1  NaN  14  12.0
2  NaN  18   5.0

In [20]: df_bows.shape[1] + df_pbows.shape[1] - df_all_bows.shape[1]
Out[20]: 3141
In [48]: df_pbows.shape[1] - 3141
Out[48]: 2546

表明 df_bows 和 df_pbows 中有 3141 个 token 是重合的, 从 products 总词汇表容量为 df_pbows.shape[1], 去掉重合的 3141 个,剩下正好是书中所说的,有 2546 个 token 不在 movies 的词汇表里, 被下面的操作去掉了:

df_product_bows = df_all_bows.iloc[len(movies):][df_bows.columns]

改成在 products 上拟合,而不是使用 movies 上的拟合模型 nb:

products = get_data('hutto_products')
bows = []
for text in products.text:
    bows.append(Counter(casual_tokenize(text)))

df_pbows = pd.DataFrame.from_records(bows)
df_pbows = df_pbows.fillna(0).astype(int)

nbp = MultinomialNB().fit(df_pbows, products.sentiment > 0)  # 在 products 上拟合
products['predicted_sentiment'] = nbp.predict_proba(df_pbows)[:, 1] * 8 - 4
products['sentiment_ispositive'] = (products.sentiment > 0).astype(int)
products['predicted_ispositive'] = (products.predicted_sentiment
                                    > 0).astype(int)
(products.predicted_ispositive ==
 products.sentiment_ispositive).sum() / len(products)

准确率 0.8847,远好于示例代码 0.5572 的准确率。

Chapter 3

本章讲如何用数值表示一个 token 的重要性,使用的三种方法都是基于频率的。 第 4 章讲如何用数值表示一个 token 的含义。

3.1

上一章 one-hot 编码中,dataframe 的结构是: 每行代表一个文档(例如一条twitter,或者一条电影评论), 没列是一个词,每个 cell 代表当前词(列)在当前文档(行)中出现的次数。

term frequency, TF: the number of times a word occurs in a document.

p74 代码:

tf = times_harry_appears / num_unique_words

对单词 harry 的 TF 做了 normalization,也就是除以文档总词汇量。

A is tempered by B 表示 A 被 B 温和化,去极端化, 这里用 A 除以 B,从而避免 B 不同时,只用 A 比较导致的不准确。

3.2

Lexicon: 词汇表,所有 doc 中词汇的并集。

p77 代码分析:

doc_tokens 是一个元素为 list (对应一个 doc,元素是 token,即单词)的 list, all_doc_tokens = sum(doc_tokens, []) 将嵌套结构打平, 不改变顺序直接连在一起。

lexicon 是一个排序后长度为 18 的 list,代表 3 个文档组成的完整单词表。

zero_vector 是一个 lexicon 中所有 token 对应 TF 全为 0 的字典, 后面每轮循环处理一个 doc,在 zero_vector 中,当前文档包含的单词上更新 TF, 而没有被更新到的 token(lexicon 里有,但当前文档里没有的 token)仍然保持 TF 为 0。

3.2.1

一般用 K 表示 lexicon 的大小,有些论文里用 [V] 表示。 上面 p77 3 个文档的例子里,K = len(lexicon) = 18。

Fig 3.2 和 Fig 3.3 中图例标注错误,应为 doc_0, doc_1, doc_2, 而不是两个 doc_0 和一个 doc_2。

p82 第2行 Python 表达式有误,应为 a.dot(b) = norm(a) * norm(b) * cos(theta) 而非 a.dot(b) = norm(a) * norm(b) / cos(theta)。

下面这句话成立的前提是 A 和 B 长度相同(p82 倒数第2段倒数第2句话):

A、B (假设 A 比 B 长)夹角的 cos 值,相当于 B 在 A 上的投影长度与 A 长度的比值。

如果两个 doc 的 TF cos 相似度为 1,说明二者使用相同的词,且这选词在文档中的比例相同, 从而证明这两个文档高度类似。 cos 相似度为 0 不一定表明两个文档讨论的不是一个话题, 只是表示两个文档使用的词完全不同。 由于词出现在文档中的频率不可能是负数,导致 cos 相似度最小为 0,而不会小于 0。

3.3

Zipf(p 不发音)定律:一篇文档,按单词出现频率对所有单词排序,比如 "the" 排第一,"a" 排第二, "of" 排第三,则每个单词在整个文档中所占的比重是其排名的倒数。 排名第一的作为标杆(1 的倒数还是 1),第二名("a")出现的次数是它("the")的 1/2, 第三名是它的 1/3,等等。

详见 Zipf's law 第2段的例子。

获取 nltk_data 方法

nltk 使用 nltk.data.path 寻找数据位置,这是一个标准的 Python string list, 目录最后必须是 nltk_data,例如 ~/Documents/machineLearningDatasets/nltk_data, 将原始 brown.zip 文件保存到 .../nltk_data/corpora 下, 并在这个目录下解压 zip 文件,得到 .../nltk_data/corpora/brown 目录, 下面包括许多文本文件。

下载原始数据:

git clone https://github.com/nltk/nltk_data.git

zip 文件在 nltk_data/packages/corpora 下。

使用方法:在需要使用 nltk_data 的 Python 代码里添加:

nltk.data.path.append('/home/leo/Documents/machineLearningDatasets/nltk_data')

然后就可以正常使用了:

from nltk.corpus import brown

3.4

IDF



Published

Jul 29, 2020

Last Updated

Aug 14, 2020

Category

Tech

Tags

  • NLP 1
  • Python 136

Contact

  • Powered by Pelican. Theme: Elegant by Talha Mansoor