文本的向量表示

1. 为什么需要文本的表示?

文字是人类认知过程中产生的高层认知抽象实体,我们需要将其转换为数字向量或矩阵作为机器学习算法模型以及神经网络模型的标准输入输出。

2. 词袋模型(Bag-of-words)

Bag-of-words模型是信息检索领域常用的文档表示方法。在文本特征生成过程中,对于一个文档,词袋模型忽略其单词顺序和语法、句法等要素,将其仅仅看作是若干个词汇的集合,文档中每个单词的出现都是独立的,不依赖于其它单词是否出现。也就是说,文档中任意一个位置出现的任何单词,都不受该文档语意影响而独立选择的。主要方法包括one-hot编码、tf-idf、n-gram模型

2.1 One-hot 编码

2.1.1 对于单词的one-hot表示

假设给定一个词典为 [今天, 天气, 真好,他们,去,爬山]。该词典可以看作一个容量为6的词袋,每个单词的表示为:
今天: [1, 0, 0, 0, 0, 0]
天气: [0, 1, 0, 0, 0, 0]
真好: [0, 0, 1, 0, 0, 0]
......
爬山: [0, 0, 0, 0, 0, 1]

在one-hot表示中,每个单词的向量长度为词袋的容量(假设词袋有10000个单词,某单词的向量长度为10000)。并且该单词在词袋中的对应位置为1,其余位置为0。 python中sklearn可以生成:

import numpy as np
from sklearn import preprocessing
words = np.array(["今天", "天气", "真好","他们","去","爬山"])  # 词袋
words=words.reshape((6,1))  # 转化成6,1矩阵

enc = preprocessing.OneHotEncoder()
enc.fit(words)  
enc.categories_   # fit之后内部会改变顺序 用词语句查看
# [array(['今天', '他们', '去', '天气', '爬山', '真好'], dtype='<U2')]

result = enc.transform([["天气"]]).toarray()   # 天气 对应的向量
# array([[0., 0., 0., 1., 0., 0.]])

2.1.2 对于句子的one-hot表示

给定三个句子“小明 今天 爬山”“小红 昨天 跑步”“小红 今天 又 爬山 又 跑步”。首先根据这些句子构造词袋(可以用分词、去重)为 [小明 小红 爬山 跑步 又 今天 昨天]。各句子的向量表示为:
“小明 今天 爬山”:[1, 0, 1, 0, 0, 1, 0]
“小红 昨天 跑步”:[0, 1, 0, 1, 0, 0, 1]
“小红 今天 又 爬山 又 跑步”:[0, 1, 1, 1, 1, 1, 0]
每个句子中所有单词都会出现在词袋中,其向量长度为词袋的容量。以“小明 今天 爬山”为例,“小明”出现在词袋中,在词袋对应位置设为1。“今天”出现在词袋中第6个位置,对应位置为1。“爬山”出现在词袋中第3个位置,对应位置为1。其余位置为0。由于第三句中“又”出现两次,其向量也可以表示为 [0, 1, 1, 2, 1, 1, 0]。
当“小红 今天 又 爬山 又 跑步”表示为 [0, 1, 1, 2, 1, 1, 0] 时,其中“又”出现的频率为2,一般会认为其比较重要,但是从实际而言,“又”的重要性比不上“爬山”、“跑步”等频率为1的单词。所以,并不是出现的次数越多越重要、并不是出现的越少就越不重要。由此可以引出 tf-idf。

2.2 n-gram模型

n-gram模型为了保持词的顺序,做了一个滑窗的操作,这里的n表示的就是滑窗的大小,例如2-gram模型,也就是把2个词当做一组来处理,然后向后移动一个词的长度,再次组成另一组词,把这些生成一个字典,按照词袋模型的方式进行编码得到结果。该模型考虑了词一定范围内的关联性。
假设给定句子:
John likes to watch movies. Mary likes too
John also likes to watch football games.
以上两句可以构造一个词典,{"John likes”: 1, "likes to”: 2, "to watch”: 3, "watch movies”: 4, "Mary likes”: 5, "likes too”: 6, "John also”: 7, "also likes”: 8, “watch football”: 9, "football games": 10}
那么第一句的向量表示为:[1, 1, 1, 1, 1, 1, 0, 0, 0, 0],其中第一个1表示John likes在该句中出现了1次,依次类推。
缺点:  随着n的大小增加,词表会成指数型膨胀,会越来越大。

2.3 tf-idf

TF-IDF(term frequency–inverse document frequency,词频-逆向文件频率)是一种用于信息检索(information retrieval)与文本挖掘(text mining)的常用加权技术。
      TF-IDF是一种统计方法,用以评估字词对于一个文件集或一个语料库中的其中一份文件的重要程度字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。
      TF-IDF的主要思想是:如果某个单词在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。

(1)TF(Term Frequency)词频

词频(TF)表示词条(关键词)在一个文本中出现的频率。计算公式为:

其中表示单词在文档中的出现次数,分母表示文档中所有单词出现的次数之和。通俗理解为:

(2)IDF(Inverse Document Frequency)逆向文档频率

逆向文件频率 (IDF) :某一特定词语的IDF,可以由总文件数目除以包含该词语的文件的数目再将得到的商取对数得到。如果包含单词的文档越少, IDF越大,则说明词条具有很好的类别区分能力。计算公式为:

其中表示所有文档数,分母表示包含单词的所有文档数。通俗理解为(加1防止分母为0):

(3)TF-IDF实际是TF*IDF







对IDF的理解:语料库的文档总数实际上是一个词分布的可能性大小,n篇文档,有n种可能。包含词的文档数m,表示词的真实分布有m个“可能”。那么log(n/m) = log(n) - log(m)就可以表示词在m篇文档中的出现,导致的词分布可能性的减少(即信息增益),这个值越小,表示词分布越散,
我们认为一个词越集中出现在某一类文档,它对这类文档的分类越有贡献,那么当一个词分布太散了,那他对文档归类的作用也不那么大了。**

举例

给定3篇文档 ,“今天 上 NLP 课程”,“今天 的 课程 有 意思”,“数据 课程 也 有 意思”。词袋为[今天 上 NLP 课程 的 有 意思 数据 也],容量为9。以第一句为例,“今天”的tf-idf值为;“上”的tf-idf值为;“NLP”的tf-idf值为,“课程”的tf-idf值为
则“今天 上 NLP 课程”的向量可以表示为[,0, 0, 0, 0, 0]。以此类推,各句子可以表示为:

“今天 上 NLP 课程”:[,0, 0, 0, 0, 0]

“今天 的 课程 有 意思”:[,0,0,, , , 0, 0]

“数据 课程 也 有 意思”:[0,0,0,,0,, , , ]

(4)利用sklearn库实现tf-idf

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
 
x_train = ['TF-IDF 主要 思想 是','算法 一个 重要 特点 可以 脱离 语料库 背景',
           '如果 一个 网页 被 很多 其他 网页 链接 说明 网页 重要']
x_test=['原始 文本 进行 标记','主要 思想']
 
#该类会将文本中的词语转换为词频矩阵,矩阵元素a[i][j] 表示j词在i类文本下的词频
vectorizer = CountVectorizer(max_features=10)
#该类会统计每个词语的tf-idf权值
tf_idf_transformer = TfidfTransformer()
#将文本转为词频矩阵并计算tf-idf
tf_idf = tf_idf_transformer.fit_transform(vectorizer.fit_transform(x_train))
#将tf-idf矩阵抽取出来,元素a[i][j]表示j词在i类文本中的tf-idf权重
x_train_weight = tf_idf.toarray()
 
#对测试集进行tf-idf权重计算
tf_idf = tf_idf_transformer.transform(vectorizer.transform(x_test))
x_test_weight = tf_idf.toarray()  # 测试集TF-IDF权重矩阵
 
print('输出x_train文本向量:')
print(x_train_weight)
print('输出x_test文本向量:')
print(x_test_weight)

输出x_train文本向量:
[[0.70710678 0.         0.70710678 0.         0.         0.
  0.         0.         0.         0.        ]
 [0.         0.3349067  0.         0.44036207 0.         0.44036207
  0.44036207 0.44036207 0.         0.3349067 ]
 [0.         0.22769009 0.         0.         0.89815533 0.
  0.         0.         0.29938511 0.22769009]]
输出x_test文本向量:
[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]]

(5)自己实现tf-idf

# -*- coding: utf-8 -*-
from collections import defaultdict
import math
import operator
 
"""
函数说明:创建数据样本
Returns:
    dataset - 实验样本切分的词条
    classVec - 类别标签向量
"""
def loadDataSet():
    dataset = [ ['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],    # 切分的词条
                   ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
                   ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
                   ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
                   ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
                   ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid'] ]
    classVec = [0, 1, 0, 1, 0, 1]  # 类别标签向量,1代表好,0代表不好
    return dataset, classVec
 
 
"""
函数说明:特征选择TF-IDF算法
Parameters:
     list_words:词列表
Returns:
     dict_feature_select:特征选择词字典
"""
def feature_select(list_words):
    #总词频统计
    doc_frequency=defaultdict(int)
    for word_list in list_words:
        for i in word_list:
            doc_frequency[i]+=1
 
    #计算每个词的TF值
    word_tf={}  #存储没个词的tf值
    for i in doc_frequency:
        word_tf[i]=doc_frequency[i]/sum(doc_frequency.values())
 
    #计算每个词的IDF值
    doc_num=len(list_words)
    word_idf={} #存储每个词的idf值
    word_doc=defaultdict(int) #存储包含该词的文档数
    for i in doc_frequency:
        for j in list_words:
            if i in j:
                word_doc[i]+=1
    for i in doc_frequency:
        word_idf[i]=math.log(doc_num/(word_doc[i]+1))
 
    #计算每个词的TF*IDF的值
    word_tf_idf={}
    for i in doc_frequency:
        word_tf_idf[i]=word_tf[i]*word_idf[i]
 
    # 对字典按值由大到小排序
    dict_feature_select=sorted(word_tf_idf.items(),key=operator.itemgetter(1),reverse=True)
    return dict_feature_select
 
if __name__=='__main__':
    data_list,label_list=loadDataSet() #加载数据
    features=feature_select(data_list) #所有词的TF-IDF值
    print(features)
    print("词袋容量:",len(features))

    
 # 结果:
[('to', 0.0322394037469742), ('stop', 0.0322394037469742),
 ('worthless', 0.0322394037469742),
 ('my', 0.028288263356383563), ('dog', 0.028288263356383563), 
 ('him', 0.028288263356383563), ('stupid', 0.028288263356383563),
 ('has', 0.025549122992281622), ('flea', 0.025549122992281622), 
 ('problems', 0.025549122992281622), ('help', 0.025549122992281622),
 ('please', 0.025549122992281622), ('maybe', 0.025549122992281622),
 ('not', 0.025549122992281622), ('take', 0.025549122992281622), 
 ('park', 0.025549122992281622), ('dalmation', 0.025549122992281622), 
 ('is', 0.025549122992281622), ('so', 0.025549122992281622), 
 ('cute', 0.025549122992281622), ('I', 0.025549122992281622), 
 ('love', 0.025549122992281622), ('posting', 0.025549122992281622), 
 ('garbage', 0.025549122992281622), ('mr', 0.025549122992281622),
 ('licks', 0.025549122992281622), ('ate', 0.025549122992281622), 
 ('steak', 0.025549122992281622), ('how', 0.025549122992281622), 
 ('quit', 0.025549122992281622), ('buying', 0.025549122992281622), 
 ('food', 0.025549122992281622)]
词袋容量: 32

(6)tf-idf的不足

**
  TF-IDF算法实现简单快速,但是仍有许多不足之处:


(1)没有考虑特征词的位置因素对文本的区分度,词条出现在文档的不同位置时,对区分度的贡献大小是不一样的。


(2)按照传统TF-IDF,往往一些生僻词的IDF(反文档频率)会比较高、因此这些生僻词常会被误认为是文档关键词。


(3)传统TF-IDF中的IDF部分只考虑了特征词与它出现的文本数之间的关系,而忽略了特征项在一个类别中不同的类别间的分布情况。


(4)对于文档中出现次数较少的重要人名、地名信息提取效果不佳。