文本向量化方法的原理及实现详解


在自然语言处理(NLP)中,我们必须将文本转换成机器能够理解的东西,而机器只能理解数值型数据。也就是说,我们需要将字符类型的文本转换成有意义的数字向量(或数组)。

本文将介绍各种常见的文本向量化的方法原理以及python实现:

  • 词袋(BOW)
  • N-Grams
  • TF-IDF

词袋(BOW)

在深度学习出现以前的时代,使用词袋方法一度成为了文本向量化的事实标准。这种方法背后的思想虽然很简单,但是非常强大。

首先,我们定义一个固定长度的向量,其中每个条目对应于预定义单词字典中的一个单词,向量的大小等于字典的大小。然后,为了用这个向量表示文本,我们计算字典中的每个单词在文本中出现的次数,并将这个数字放入对应的向量条目中,这种向量也称为词频向量。 例如,如果我们的字典包含如下单词:

{machine, learning, is, the, not, great}

如果我们想对文本machine learning is great进行向量化,我们将得到以下向量:

(1,1,1,0,0,1) 

1表示该字典中的单词在文本中出现过一次,0则表示没出现过。如果某个单词在文本中出现多次,则向量对应位置的数字为出现的次数

比如文本the machine learning is the great向量化后得到以下向量:

(1,1,1,2,0,1) 

字典中的第四个单词the在文本中出现两次,因此向量的第四个数为2。

通常情况下,为了改进这种表示方法,您可以使用一些更高级的技术,如删除停用词词形还原、使用 n-grams 或使用 tf-idf 代替计数。

词袋方法的问题是,它不能捕获文本的含义或文本的上下文。即使使用n-grams方法,也存在同样的问题。

下面介绍如何使用Python将文本转换为词频向量scikit-learn提供了CountVectorizer来实现此功能,如下代码示例:

from sklearn.feature_extraction.text import CountVectorizer

texts = [
    'There used to be Stone Age',
    'There used to be Bronze Age bronze',
    'There used to be Iron Age',
    'There was Age of Revolution',
    'Now it is Digital Age'
]
vectorizer = CountVectorizer(analyzer='word')
vec = vectorizer.fit_transform(texts)
# 打印字典
print(vectorizer.vocabulary_)

先打印字典出来看看:

{'there': 11, 'used': 13, 'to': 12, 'be': 1, 'stone': 10, 'age': 0, 'bronze': 2, 'iron': 4, 'was': 14, 'of': 8, 'revolution': 9, 'now': 7, 'it': 6, 'is': 5, 'digital': 3}

字典是一个dict,每个单词对应一个索引。字典中每个单词都是唯一的,它们来自输入文本,可以看到字典里有15个不重复的单词。字典dict的key是单词,value是索引。

输出所有文本的向量看看:

print(vec)

输出如下:

  (0, 0)    1
  (0, 10)    1
  (0, 1)    1
  (0, 12)    1
  (0, 13)    1
  (0, 11)    1
  (1, 2)    2
  (1, 0)    1
  (1, 1)    1
  (1, 12)    1
  (1, 13)    1
  (1, 11)    1
  (2, 4)    1
  (2, 0)    1
  (2, 1)    1
  (2, 12)    1
  (2, 13)    1
  (2, 11)    1
  (3, 9)    1
  (3, 8)    1
  (3, 14)    1
  (3, 0)    1
  (3, 11)    1
  (4, 3)    1
  (4, 5)    1
  (4, 6)    1
  (4, 7)    1
  (4, 0)    1

这个似乎看起来不像向量啊!这个其实是稀疏矩阵。这是一种压缩向量的表示方法,只保存了非0值,而其他未保存的都是0。比如(1, 2) 2,其中(1, 2)表示第2个向量的索引2位置。括号后面的2表示该位置的值为2

第2个向量是第2条文本的向量,也就是文本There used to be Bronze Age bronze,字典中索引为2的单词是bronze,而单词bronze在第2条文本中出现次数为2,因此值为2。

我们可以用toarray()函数将稀疏矩阵转化成数组向量:

print(vec.toarray())

输出如下:

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

可以看到位置(1, 2)的值确实为2,即第二行第三列的值,与稀疏矩阵一致。另外,由于字典长度为15,因此每个向量都是15维的。

接下来可以用创建的vectorizer转换任意文本为向量,使用transform()函数:

vec = vectorizer.transform(['There was Stone Age'])
print(vec.toarray())

输出:

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

N-Grams

前面的词袋方法只是取单个单词构成字典,我们还可以取相邻的多个单词来构成字典,从而统计多个相邻单词的出现频率,这就是所谓的N-Grams方法。

比如,我们将窗口(表示取多少个相邻单词)设为2,也就是将所有相邻的两个单词取出。例如对于文本There used to be Stone Age,我们将得到如下的字典集合:

{'there used, 'used to', 'to be', 'be stone', 'stone age'}

如果窗口设为3,则得到如下集合:

{‘there used to’,‘used to be’,‘to be stone’,‘be tone age’}

我们可以将窗口设为任意大小n,这就是所谓的n-grams

当我们按照窗口大小将文本拆分构成字典后,对于任意输入文本,我们将采用类似词频统计的方法,统计字典中所有文本的出现频次,最终构成词频向量。

scikit-learnCountVectorizer就支持n-grams,如下示例:

texts = ['There used to be Stone Age']
vectorizer = CountVectorizer(analyzer='word', ngram_range=(2, 2))
vec = vectorizer.fit_transform(texts)
print(vectorizer.get_feature_names())

可以看到,我们提供了ngram_range参数,该参数接受一个元组,第一个值表示最小窗口,第二个值表示最大窗口。当我们提供参数值(2, 2)时,就表示窗口为2

构造的字典集合如下:

['be stone', 'stone age', 'there used', 'to be', 'used to']

打印向量看看:

print(vec)

输出如下:

  (0, 1)    1
  (0, 0)    1
  (0, 3)    1
  (0, 4)    1
  (0, 2)    1

打印数组向量:

print(vec.toarray())

输出如下:

[[1 1 1 1 1]]

我们可以通过ngram_range参数提供窗口区间,比如(1, 3),表示分别使用窗口大小1,2,3来生成字典集合,如下:

texts = [
    'There used to be Stone Age'
]
vectorizer = CountVectorizer(analyzer='word', ngram_range=(1, 3))
vec = vectorizer.fit_transform(texts)
print(vectorizer.get_feature_names())

得到如下字典集合,可以看到,集合中包含了单个单词,双词以及三个单词:

['age', 'be', 'be stone', 'be stone age', 'stone', 'stone age', 'there', 'there used', 'there used to', 'to', 'to be', 'to be stone', 'used', 'used to', 'used to be']

TF-IDF

TF-IDF是 Term Frequency-Inverse Document Frequency 的缩写,即词频-逆文件频率,分为两部分,第一部分词频(Term Frequency,缩写为TF),第二部分逆文件频率(Inverse Document Frequency,缩写为IDF)。

TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加(TF部分),但同时会随着它在语料库中出现的频率成反比下降(IDF部分。因此我们需要将TFIDF两部分综合起来表示词的重要性。

  • 词频 (term frequency, TF) : 指的是某一个给定的词语在该文件中出现的次数,计算公式如下:

    $$TF = \frac{在某一文档中词条w出现的次数}{该文档中所有的词条数目}$$

    公式中除以该文档中所有的词条数目是为了归一化词频,以防止它偏向长的文件(同一个词语在长文件里可能会比短文件有更高的词频,但它不一定就重要)。

  • 逆文件频率 (inverse document frequency, IDF) : 是一个词语普遍重要性的度量,如果包含词条的文档越少, IDF越大,则说明词条具有很好的类别区分能力。某一特定词语的IDF,可以由总文件数目除以包含该词语之文件的数目,再将得到的商取对数得到。计算公式如下:

    $$IDF = log(\frac{语料库的文档总数}{包含词条w的文档数+1})$$

    如果一个词越常见,那么分母就越大,逆文档频率就越小越接近0。分母之所以要加1,是为了避免分母为0(即所有文档都不包含该词)

TF-IDF的最终值由TF乘以IDF,公式如下:

$$TF-IDF = TF * IDF$$

某一特定文件内的高词语频率,以及该词语在整个文件集合中的低文件频率,可以产生出高权重的TF-IDF。因此,TF-IDF倾向于过滤掉常见的词语,保留重要的词语。

scikit-learn提供了TfidfVectorizer来实现将文本转换为IF-IDF向量,代码示例如下:

from sklearn.feature_extraction.text import TfidfVectorizer

texts = [
    'There used to be Stone Age',
    'There used to be Bronze Age bronze',
    'There used to be Iron Age',
    'There was Age of Revolution',
    'Now it is Digital Age'
]
vectorizer = TfidfVectorizer(analyzer='word')
vec = vectorizer.fit_transform(texts)
print(vectorizer.vocabulary_)

得到字典集合如下,这个与CountVectorizer得到的是相同的集合:

{'there': 11, 'used': 13, 'to': 12, 'be': 1, 'stone': 10, 'age': 0, 'bronze': 2, 'iron': 4, 'was': 14, 'of': 8, 'revolution': 9, 'now': 7, 'it': 6, 'is': 5, 'digital': 3}

打印IF-IDF稀疏矩阵:

print(vec)

输出如下:

  (0, 11)    0.33140159786840845
  (0, 13)    0.3939481437168047
  (0, 12)    0.3939481437168047
  (0, 1)    0.3939481437168047
  (0, 10)    0.5882354607969754
  (0, 0)    0.28029734885918384
  (1, 11)    0.23213777065833785
  (1, 13)    0.27594991824306836
  (1, 12)    0.27594991824306836
  (1, 1)    0.27594991824306836
  (1, 0)    0.1963406395869283
  (1, 2)    0.8240857580041683
  (2, 11)    0.33140159786840845
  (2, 13)    0.3939481437168047
  (2, 12)    0.3939481437168047
  (2, 1)    0.3939481437168047
  (2, 0)    0.28029734885918384
  (2, 4)    0.5882354607969754
  (3, 11)    0.2992461174212536
  (3, 0)    0.25310044945192844
  (3, 14)    0.5311597134872388
  (3, 8)    0.5311597134872388
  (3, 9)    0.5311597134872388
  (4, 0)    0.2317654623904255
  (4, 7)    0.48638584746139363
  (4, 6)    0.48638584746139363
  (4, 5)    0.48638584746139363
  (4, 3)    0.48638584746139363

打印数组向量:

print(vec.toarray())

输出如下:

[[0.28029735 0.39394814 0.         0.         0.         0.
  0.         0.         0.         0.         0.58823546 0.3314016
  0.39394814 0.39394814 0.        ]
 [0.19634064 0.27594992 0.82408576 0.         0.         0.
  0.         0.         0.         0.         0.         0.23213777
  0.27594992 0.27594992 0.        ]
 [0.28029735 0.39394814 0.         0.         0.58823546 0.
  0.         0.         0.         0.         0.         0.3314016
  0.39394814 0.39394814 0.        ]
 [0.25310045 0.         0.         0.         0.         0.
  0.         0.         0.53115971 0.53115971 0.         0.29924612
  0.         0.         0.53115971]
 [0.23176546 0.         0.         0.48638585 0.         0.48638585
  0.48638585 0.48638585 0.         0.         0.         0.
  0.         0.         0.        ]]

文章作者: yglong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yglong !
评论
  目录