文本挖掘从小白到精通(二):料库和词向量空间

2 评论 8542 浏览 12 收藏 28 分钟

写笔者最近在梳理自己的文本挖掘知识结构,借助gensim、sklearn、keras等库的文档做了些扩充,会陆陆续续介绍文本向量化、tfidf、主题模型、word2vec,既会涉及理论,也会有详细的代码和案例进行讲解,希望在梳理自身知识体系的同时也能对想学习文本挖掘的朋友有一点帮助,这是笔者写该系列的初衷。

在本文中,笔者将会紧接着上文提及的3个概念,拓展到文本挖掘中一个重要的概念 —(文本)向量空间,它是将自然语言转化为机器可识别符号的关键一步,文本相似度、文本聚类、文本分类等实际应用皆以此为基础。

培养码代码的好习惯,设置日志,打印程序运行中的细节,以便调试代码。

import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
import os 
import tempfile
TEMP_FOLDER = tempfile.gettempdir()
print('文件夹"{}" 将被用来存储语料和临时性的字典'.format(TEMP_FOLDER))

文件夹”C:UsershpAppDataLocalTemp” 将被用来存储语料和临时性的字典

一、从字符串到向量(From Strings to Vectors)

这次,笔者还是使用之前关于“知识图谱”报道的标题语料库作为示例:

from gensim import corpora
import jieba

2019-05-06 09:59:43,964 : INFO : ‘pattern’ package not found; tag filters are not available for English

根据打印出的日志可知,’pattern’没正确安装上,这个库是自然语言处理里一个很棒的库,不过目前没怎么更新了,且对中文的支持不给力,所以不影响接下来的分析。

jieba.add_word('知识图谱') #防止“知识图谱”被切错词
docs = ['商业新知:知识图谱为内核,构建商业创新服务完整生态。',
'如何更好利用知识图谱技术做反欺诈? 360金融首席数据科学家沈赟开讲。',
'知识管理 | 基于知识图谱的国际知识管理领域可视化分析。',
'一文详解达观数据知识图谱技术与应用。',
'知识图谱技术落地金融行业的关键四步。',
'一文读懂知识图谱的商业应用进程及技术背景。',
'海云数据CPO王斌:打造大数据可视分析与AI应用的高科技企业。',
'智能产业|《人工智能标准化白皮书2018》带来创新创业新技术标准。',
'国家语委重大科研项目“中华经典诗词知识图谱构建技术研究”开题。',
'最全知识图谱介绍:关键技术、开放数据集、应用案例汇总。',
'中译语通Jove Mind知识图谱平台 引领企业智能化发展。',
'知识图谱:知识图谱赋能企业数字化转型,为企业升级转型注入新能量。']

再对文本进行分词,用空格隔开变成字符串,方便进行下一步的处理:

documents = [' '.join(jieba.lcut(i)) for i in docs]
print(documents)

[‘商业 新知 : 知识图谱 为 内核 , 构建 商业 创新 服务 完整 生态 。’,

‘如何 更好 利用 知识图谱 技术 做 反 欺诈 ? 360 金融 首席 数据 科学家 沈赟 开讲 。’,

‘知识 管理 | 基于 知识图谱 的 国际 知识 管理 领域 可视化 分析 。’,

‘一文 详解 达观 数据 知识图谱 技术 与 应用 。’,

‘知识图谱 技术 落地 金融 行业 的 关键 四步 。’,

‘一文 读懂 知识图谱 的 商业 应用 进程 及 技术 背景 。’,

‘海云 数据 CPO 王斌 : 打造 大 数据 可视 分析 与 AI 应用 的 高科技 企业 。’,

‘智能 产业 | 《 人工智能 标准化 白皮书 2018 》 带来 创新 创业 新 技术标准 。’,

‘国家语委 重大 科研项目 “ 中华 经典 诗词 知识图谱 构建 技术 研究 ” 开题 。’,

‘最全 知识图谱 介绍 : 关键技术 、 开放 数据 集 、 应用 案例 汇总 。’,

‘中译 语通 Jove Mind 知识图谱 平台 引领 企业 智能化 发展 。’,

‘知识图谱 : 知识图谱 赋能 企业 数字化 转型 , 为 企业 升级 转型 注入 新 能量 。’]

这是一个包含12个文档的小型语料,每个文档仅包含1个语句。

首先,对这些文档进行分词处理,移除停用词,并去掉那些仅在本语料中出现一次的词汇:

from pprint import pprint
from collections import defaultdict
# 移除常用词以及分词

stoplist = [i.strip() for i in open('datasets/stopwords_zh.txt',encoding='utf-8').readlines()]
texts = [[word for word in document.lower().split() if word not in stoplist] for document in documents]# 移除仅出现一次的词汇
frequency = defaultdict(int)
for text in texts:for token in text:
frequency[token] += 1
texts = [[token for token in text if frequency[token] > 1] for text in texts]
#使打印的格式更齐整
pprint(texts)

[[‘商业’, ‘知识图谱’, ‘商业’, ‘创新’],

[‘知识图谱’, ‘技术’, ‘金融’, ‘数据’],

[‘知识’, ‘管理’, ‘知识图谱’, ‘知识’, ‘管理’, ‘分析’],

[‘一文’, ‘数据’, ‘知识图谱’, ‘技术’],

[‘知识图谱’, ‘技术’, ‘金融’],

[‘一文’, ‘知识图谱’, ‘商业’, ‘技术’],

[‘数据’, ‘数据’, ‘分析’, ‘企业’], [‘创新’],

[‘知识图谱’, ‘技术’],

[‘知识图谱’, ‘数据’],

[‘知识图谱’, ‘企业’],

[‘知识图谱’, ‘知识图谱’, ‘企业’, ‘转型’, ‘企业’, ‘转型’]]

处理文档的方式需要因时制宜,随机应变,尤其是在不同的应用场景中,比如电商评论、博客长文以及微博内容都需要使用不同的预处理方法,笔者会在后面的文章中提及这些技巧。在这里,基于上面的分词,笔者仅用空格分开,然后对语句中的西文词汇进行“小写化(Lowercasing)”。

机器是看不懂人类的自然语言(自然语言通常是指一种自然地随文化演化的语言。例如:英语、汉语、日语为自然语言的例子,而世界语则为人造语言,即是一种为某些特定目的而创造的语言),若要机器“读懂”自然语言,则需要将其转换为机器可识别的符号,比如”0″和”1″,且这种转换的过程中需要最大限度保留自然语言特有的语义特征,这是一个很有难度的任务。

在这里,笔者介绍一种常见的文本表示方法——称为词袋模型,即Bag-of-Words)。

在词袋模型模型下,像是句子或是文件这样的文字可以用一个袋子装着这些词的方式表现,这种表现方式不考虑文法以及词的顺序。

https://baike.baidu.com/item/%E8%AF%8D%E8%A2%8B%E6%A8%A1%E5%9E%8B/22776998?fr=aladdin

后面还会有很多不同的文本表示方法,比如TF-IDF、LSA、LSI、LDA、HDP、NMF、Word2vec等。但是,请记住,不同的应用场景需要不同的文本特征,没有百试不爽的方法,并且,请一如既往的记住这句名言:

垃圾入,垃圾出(garbage in, garbage out)。

使用词袋模型将多个文档转换为向量,每个文档由一个向量表示,其中向量元素“i”表示第i个单词出现在文档中的次数。

仅通过它们的(整型)id来表征词汇是有利的, 问题和ID之间的映射称为字典(dictionary):

dictionary = corpora.Dictionary(texts)
dictionary.save(os.path.join(TEMP_FOLDER, 'deerwester.dict')) # 保存字典,以备后续查找之用print(dictionary)

2019-05-06 15:58:09,861 : INFO : adding document #0 to Dictionary(0 unique tokens: [])2019-05-06 15:58:09,867 : INFO : built Dictionary(12 unique tokens: [‘创新’, ‘商业’, ‘知识图谱’, ‘技术’, ‘数据’]…)

12 documents (total 42 corpus positions)

2019-05-06 15:58:09,873 : INFO : saving Dictionary object under C:Users/hp/AppDataLocal/Tempdeerwester.dict, separately None

2019-05-06 15:58:09,879 : INFO : saved C:UsershpAppDataLocalTempdeerwester.dict

Dictionary(12 unique tokens: [‘创新’, ‘商业’, ‘知识图谱’, ‘技术’, ‘数据’]…)

在这里,我们通过gensim.corpora.dictionary.Dictionary这个类为处理过的语料库中出现的每个词汇分配一个独一无二的整数ID 。 这会扫描整个文本,统计所有的词汇计数和词汇相关数据。 最后,我们看到在处理的语料库中有12个不同的词汇,这意味着每个文档将由12个数字表示(即12-D向量)。

下面,查看每个词汇与其对应ID之间的映射关系:

print(dictionary.token2id)

{‘创新’: 0, ‘商业’: 1, ‘知识图谱’: 2, ‘技术’: 3, ‘数据’: 4, ‘金融’: 5, ‘分析’: 6, ‘知识’: 7, ‘管理’: 8, ‘一文’: 9, ‘企业’: 10, ‘转型’: 11}

将分词后的文档实际转换为向量:

new_doc = "知识图谱 为 企业 转型 助力"
new_vec = dictionary.doc2bow(new_doc.lower().split())
print(new_vec) # “为”、“助力”等词汇未出现在字典中,因而被忽略

[(2, 1), (10, 1), (11, 1)]

函数doc2bow()只是计算每个不同词汇的出现次数,将词汇转换为整数词汇id,并将结果作为一个词袋(bag-of-words)——一个稀疏向量返回,形式为( word_id1,word_count1),( word_id2,word_count2),( word_id3,word_count3)…

在token_id中,“创新”对应的为0,“商业”为1,…,’转型’为11。因而,新文档“知识图谱 为 企业 转型 助力(知识图谱为企业转型助力)”将被转换为[(2, 1), (10, 1), (11, 1)]。 “知识图谱”、“企业”、“转型” 出现在词典中并出现一次。

因此,它们在稀疏向量中分别变为(2, 1), (10, 1), (11, 1)。 “为”、“转型”、“助力”等词汇在字典中不存在,因此不会出现在稀疏向量中。词汇计数为0的词汇不会出现在稀疏向量中,并且稀疏向量中将永远不会出现像(3,0)这样的元素。

对于熟悉scikit-learn的人来说,doc2bow()与在CountVectorizer上调用transform()有类似的作用(http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html)。doc2bow()也可以像fit_transform()那样运作。

相关详细信息,请参阅 gensim API Doc

corpus = [dictionary.doc2bow(text) for text in texts]
corpora.MmCorpus.serialize(os.path.join(TEMP_FOLDER, 'deerwester.mm'), corpus) #保存到本地,以作后用
for c in corpus:
print(c)

2019-05-06 16:00:28,683 : INFO : storing corpus in Matrix Market format to C:Users/hp/AppData/Local/Tempdeerwester.mm

2019-05-06 16:00:28,688 : INFO : saving sparse matrix to C:Users/hp/AppData/Local/Temp/deerwester.mm

2019-05-06 16:00:28,690 : INFO : PROGRESS: saving document

2019-05-06 16:00:28,693 : INFO : saved 12×12 matrix, density=24.306% (35/144)

2019-05-06 16:00:28,695 : INFO : saving MmCorpus index to C:UsershpAppDataLocalTempdeerwester.mm.index

[(0, 1), (1, 2), (2, 1)]

[(2, 1), (3, 1), (4, 1), (5, 1)]

[(2, 1), (6, 1), (7, 2), (8, 2)]

[(2, 1), (3, 1), (4, 1), (9, 1)]

[(2, 1), (3, 1), (5, 1)]

[(1, 1), (2, 1), (3, 1), (9, 1)]

[(4, 2), (6, 1), (10, 1)]

[(0, 1)]

[(2, 1), (3, 1)][(2, 1), (4, 1)]

[(2, 1), (10, 1)]

[(2, 2), (10, 2), (11, 2)]

到目前为止,应该清楚的是,带有id = 10的向量特征表示文档中出现“企业”一词的次数——在这12个文档中, 只有倒数前2个和和倒数第6个的值为1,其他皆为0。

二、语料库流(Corpus Streaming)——每次仅调用一个文档

请注意,上面的语料(corpus)完全驻留在内存中,作为普通的Python列表而存在。 在这个简单的例子中,它并不重要,只是为了阐述方便。让我们假想,手头的语料库中有数千万个文档,要将所有这些文档都存储在RAM中是行不通的,这会消耗大量的计算资源,速度奇慢!

相反,我们假设文档存储在本地的单个文件中,每行一个文档。 Gensim只要求语料库在需要使用时,能够一次返回一个文档向量:

from smart_open import smart_open
class MyCorpus(object):
def __iter__(self):
for line in smart_open('datasets/mycorpus.txt', 'r',encoding='utf-8'):
# 假设每一行一个文档,用jieba进行分词
yield dictionary.doc2bow(' '.join(jieba.lcut(line)).lower().split())

每个文档占用单个文件中一行的假设并不重要,你可以设计__iter__函数以适合你的特定输入格式,比如文档目录、待解析的XML、可访问的网络节点……只需解析你的输入以检索每个文档中的所用词汇,然后通过字典将这些词汇转换为它们对应的整数ID,并在__iter__中产生具有生成器属性的稀疏向量。

corpus_memory_friendly = MyCorpus() #不需要将语料载入到内存中!
print(corpus_memory_friendly)

<__main__.MyCorpus object at 0x00000160C6575320>

现在,corpus_memory_friendly是一个对象。 我们没有定义任何打印(print)方式,因此print只输出对象在内存中的地址, 这看起来没啥用。 要查看其中的向量构成,需要遍历语料库,并打印每个文档向量(一次一个):

for vector in corpus_memory_friendly: #每次载入一个文档向量
print(vector)

[(0, 1), (1, 2), (2, 1), (3, 1)]

[(2, 1), (3, 1), (4, 1), (5, 1), (6, 1)]

[(2, 1), (3, 3), (7, 1), (8, 2)]

[(2, 1), (3, 1), (4, 1), (5, 1), (9, 1)]

[(2, 1), (3, 1), (4, 1), (6, 1)]

[(1, 1), (2, 1), (3, 1), (4, 1), (9, 1)]

[(5, 2), (7, 1), (10, 1)]

[(0, 1)]

[(2, 1), (3, 1), (4, 1)]

[(2, 1), (3, 1), (5, 1)]

[(2, 1), (3, 1), (10, 1)]

[(2, 2), (3, 2), (10, 1)]

尽管输出与普通的Python列表的输出相同,但此时的语料库对内存更友好——一次最多只有一个向量驻留在RAM中, 现在,你的语料库想用多大就用多大,哪怕是成千上万个文档,只是速度稍慢罢了。

我们将使用mycorpus.txt这个文件来创建字典(Dictionary),但不是将整个文件加载到本地内存中。 然后,我们将过滤掉语料中的停用词以及词频为1的词汇,从而得到“净化”后的语料。

请记住,dictionary.filter_tokens(或者dictionary.add_document)将调用dictionary.compactify()来删除词汇id序列中的间隙,空置的占位符(””)将会被剔除。

from six import iteritems
from smart_open import smart_open   
#收集所有词汇的统计信息
dictionary = corpora.Dictionary(''.join(jieba.lcut(line)).lower().split() for line in
smart_open('datasets/mycorpus.txt','r',encoding='utf-8')) 
 
#停用词和低频词(这里指仅出现1次的词汇)的ID集合
stop_ids = [dictionary.token2id[stopword] for stopword in stoplist 
if stopword in dictionary.token2id]
once_ids = [tokenid for tokenid, docfreq in iteritems(dictionary.dfs) if docfreq == 1] 

#真正实施去停用词和低频次的操作
dictionary.filter_tokens(stop_ids + once_ids)
print(dictionary)

2019-05-06 15:44:52,297 : INFO : adding document #0 to Dictionary(0 unique tokens: [])

2019-05-06 15:44:52,303 : INFO : built Dictionary(100 unique tokens: [‘,’, ‘:’, ‘。’, ‘为’, ‘内核’]…) from 12 documents (total 164 corpus positions)

Dictionary(10 unique tokens: [‘创新’, ‘商业’, ‘图谱’, ‘知识’, ‘技术’]…)

到这里,词袋表示(Bag-of-words Representation)的原理和操作就说完了。 当然,我们用这种语料库可以做什么是另一个问题; 计算不同单词的出现频率可能是有用的,但在实际场景中,这还不够。

事实证明,我们经常需要对这个简单的表示进行转换(Transformation),之后才能进行文档相似度、文本聚类或者文本分类这样的任务。 转换后面会提到,但在此之前,让我们将注意力集中在语料库持久性(Corpus Persistency)上。

三、语料格式(Corpus Formats)

存在几种用于将向量空间(Vector Space)语料库(向量序列)序列化到本地的文件格式。Gensim通过前面提到的流式语料库接口(Streaming Corpus Interface)实现:以惰性方式(A Lazy Fashion)从本地读取大量语料,一次一个文档,而不是一次性将整个语料库读入本地内存中,这在语料库极为庞大时是很折腾电脑的。

其中,一种比较值得注意的文件格式是Matrix Market格式(http://math.nist.gov/MatrixMarket/formats.html)。

下面,将文档以Matrix Market格式保存:

#创建一个包含2个文档的微小语料,以一个python列表呈现
corpus = [[(1, 0.5)], []] # 其中一个文档故意搞成空的
corpora.MmCorpus.serialize(os.path.join(TEMP_FOLDER, 'corpus.mm'), corpus)

2019-05-06 16:36:54,265 : INFO : storing corpus in Matrix Market format to C:Users/hp/AppData/Local/Temp/corpus.mm

2019-05-06 16:36:54,281 : INFO : saving sparse matrix to C:Users/hp/AppData/Local/Temp/corpus.mm

2019-05-06 16:36:54,285 : INFO : PROGRESS: saving document #

2019-05-06 16:36:54,290 : INFO : saved 2×2 matrix, density=25.000% (1/4)

2019-05-06 16:36:54,293 : INFO : saving MmCorpus index to C:Users/hp/AppData/Local/Temp/corpus.mm.index

其他的存储格式还有Joachim’s SVMlight format(http://svmlight.joachims.org/)、Blei’s LDA-C format(http://www.cs.columbia.edu/~blei/lda-c/) 和GibbsLDA++ format(http://gibbslda.sourceforge.net/)。

corpora.SvmLightCorpus.serialize(os.path.join(TEMP_FOLDER, 'corpus.svmlight'), corpus)
corpora.BleiCorpus.serialize(os.path.join(TEMP_FOLDER, 'corpus.lda-c'), corpus)
corpora.LowCorpus.serialize(os.path.join(TEMP_FOLDER, 'corpus.low'), corpus)

反向操作,从Matrix Market文件加载语料库迭代器(Corpus Iterator):

corpus = corpora.MmCorpus(os.path.join(TEMP_FOLDER, 'corpus.mm'))

语料库对象是流式的(Streams),因此,我们通常无法直接打印它们,只有通过遍历才能看到其中的元素:

print(corpus)

MmCorpus(2 documents, 2 features, 1 non-zero entries)

通过迭代器列表化来查看语料库中的元素:

# 一种打印语料库的方式是 --- 将其整个载入内存中
print(list(corpus)) # 调用 list() 能将任何序列转化为普通的Python list

[[(1,0.5)],[]]

或者这样:

# 另一种方法:一次打印一个文档
for doc in corpus:
print(doc)

[(1, 0.5)][]

显然,第二种方式对内存更友好,但是出于测试和开发的目的,没有什么比调用list(corpus)更简单、快捷!。

接下来,以Blei的LDA-C格式保存相同的Matrix Market文档流:

corpora.BleiCorpus.serialize(os.path.join(TEMP_FOLDER, 'corpus.lda-c'), corpus)

通过这种方式,gensim也可以用作内存高效的I / O格式转换工具:只需使用一种格式加载文档流,然后立即以另一种格式进行保存。

四、与NumPy、SciPy的兼容性

Gensim还囊括许多高效且实用的函数,可以在(http://radimrehurek.com/gensim/matutils.html)看到,通过这些函数,我们可以轻松的进行numpy矩阵的转换:

import gensim
import numpy as np
numpy_matrix = np.random.randint(10, size=[5,2])
numpy_matrix

array([[0, 4],

[0, 7],

[8, 2],

[7, 0],

[2, 1]])

corpus = gensim.matutils.Dense2Corpus(numpy_matrix)
numpy_matrix_dense = gensim.matutils.corpus2dense(corpus, num_terms=10)
numpy_matrix_dense

array([[0., 4.],

[0., 7.],

[8., 2.],

[7., 0.],

[2., 1.],

[0., 0.],

[0., 0.],

[0., 0.],

[0., 0.],

[0., 0.]], dtype=float32)

与scipy.sparse矩阵相互转换:

import scipy.sparse
scipy_sparse_matrix = scipy.sparse.random(5,2)
scipy_sparse_matrix

<5×2 sparse matrix of type ‘<class ‘numpy.float64′>’ with 0 stored elements in COOrdinate format>

corpus = gensim.matutils.Sparse2Corpus(scipy_sparse_matrix)
corpus

<gensim.matutils.Sparse2Corpus at 0x160c65759e8>

scipy_csc_matrix = gensim.matutils.corpus2csc(corpus)
scipy_csc_matrix

<0x2 sparse matrix of type ‘<class ‘numpy.float64′>’ with 0 stored elements in Compressed Sparse Column format>

要获得完整的参考(想要将字典的规模精简下以节约内存?优化语料库和NumPy / SciPy数组之间的转换?),请参阅gensim的API文档。

在下一篇文章中,笔者会接着本次的主题,说说关于主题模型和文本数据转换,即TF-IDF模型、潜在语义索引(LSI)、随机映射(Random Projections)、隐狄利克雷分配模型(Latent Dirichlet Allocation, LDA)、层次狄利克雷过程(Hierarchical Dirichlet Process,HDP)的教程。

#专栏作家

苏格兰折耳喵(微信公众号:Social Listening与文本挖掘),人人都是产品经理专栏作家,数据PM一只,擅长数据分析和可视化表达,热衷于用数据发现洞察,指导实践。

本文原创发布于人人都是产品经理。未经许可,禁止转载。

题图来自Unsplash,基于CC0协议

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
评论
评论请登录
  1. 请问数据是用Python代码爬的还是用什么工具爬的呀

    来自广东 回复
    1. 异步爬虫爬取的,半小时内搞定,如果是类似八爪鱼的采集工具,你得爬半个月。。。

      来自江苏 回复