从文档中提取关键字

想象一下你手上有数百万(也许数十亿)的文本文档。无论是社交媒体数据还是社区论坛帖子。生成数据时没有标记。给那些文件贴标签真是费劲。

手工标注不实用;现有的标签列表很快就会过时。雇用一家供应商公司来做标记工作太贵了。

你可能会说,为什么不使用机器学习呢?比如,普通网络深度学习。但是,神经网络首先需要一些训练数据。并且要适合你数据集的训练数据。

那么,有没有一个解决方案可以让我们满足:

  • 不需要训练数据。
  • 手动干扰最小,可自动运行。
  • 自动捕获新单词和短语。

这篇文章记录了我是如何在Python中提取关键字,并且它是如何工作的。

请注意,本文中的代码是在Jupyter Notebook中运行和测试的。如果你运行一个代码块,但是遇到了缺少导入包的错误,那么这个包一定已经在前面某处导入过了。

核心思想

TF-IDF是一种广泛使用的算法,用于评估单词与文档集合中文档的相关性。

基于TF-IDF,那些独特而重要的词在文档中应该具有较高的TF-IDF值。因此,理论上,我们应该能够利用文本权重来提取文档中最重要的单词。

例如,一个关于sciket learn的文档应该包含很多高密度的关键字scikit learn,而另一个关于“pandas”的文档应该对pandas具有很高的TF-IDF值。

目标数据

我发现来自NLTK的Reuters文档语料库是一个很好的关键词提取目标:https://www.nltk.org/

如果你不熟悉NLTK语料库,本文可能有助于在不到一个小时的时间内开始NLTK,https://towardsdatascience.com/book-writing-pattern-analysis-625f7c47c9ad。

下载路透社语料库。运行Python代码:

import nltk
nltk.download("reuters")

列出我们刚刚下载的语料库中的所有文档ID。

from nltk.corpus import reuters
reuters.fileids()

检查一个文档的内容及其类别。

fileid = reuters.fileids()[202]
print(fileid,"\n"
      ,reuters.raw(fileid),"\n"
      ,reuters.categories(fileid),"\n")

路透社语料库是由重叠的类别组成的。我们还可以按类别名称获取文档。对于完整的NLTK语料库操作,请查看这篇精彩的文章:访问文本语料库和词汇资源:https://www.nltk.org/book/ch02.html#fig-inaugural2

生成停用词列表

为了节省时间和计算资源,我们最好排除诸如“am”、“I”、“should”之类的停用词。NLTK提供了一个很好的英语停用词列表。

from nltk.corpus import stopwords
ignored_words = list(stopwords.words('english'))

你也可以用你自己的停用词扩展这个列表,这些词不包括在NLTK停用词列表中。

ignored_words.extend(
'''get see seeing seems back join 
excludes has have other that are likely like 
due since next 100 take based high day set ago still 
however long early much help sees would will say says said 
applying apply remark explain explaining
'''.split())

建立关键词词汇表-单字

在使用TF-IDF提取关键字之前,我将建立自己的词汇表,包括单个单词(例如“Python”)和两个单词(例如“white house”)。

在这里,我将使用scikit-learn中的CountVectorizer来执行单个单词提取工作。

from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
count_vec = CountVectorizer(
    ngram_range = (1,1)   #1
    ,stop_words = ignored_words
)

text_set = [reuters.raw(fileid).lower() for fileid in reuters.fileids()] #2
tf_result = count_vec.fit_transform(text_set)

tf_result_df = pd.DataFrame(tf_result.toarray()
                               ,columns=count_vec.get_feature_names()) #3

the_sum_s = tf_result_df.sum(axis=0) #4

the_sum_df = pd.DataFrame({
    
     #5
    'keyword':the_sum_s.index
    ,'tf_sum':the_sum_s.values
})

the_sum_df = the_sum_df[
    the_sum_df['tf_sum']>2  #6
].sort_values(by=['tf_sum'],ascending=False)

代码#1,指定CountVectorizer只对单个单词计数。你可能会问,为什么不使用ngram_range = (1,2),然后同时得到1gram和bigram呢?这是因为这里的捕获bigram会得到“they are”、“I will”和“will be”这样的短语。这些是连词短语,通常不是文档的关键字或关键短语。

另一个原因是为了节省内存资源,捕获bigram短语在这个阶段会由于太多的组合而占用大量内存。

代码#2,使用Python生成式将路透社的所有文章放在一行代码中。

代码#3,将计数向量结果转换为可读的数据帧。

代码#4,产生一个列表,包含关键字及其在语料库中的总出现次数。

代码#5,将序列转换为数据帧,以便于读取和数据操作。

代码#6,选取只出现2次以上的单词。

如果你查看由the_sum_df[:10]设置的前10个结果,你将看到那些最常用的词:
在这里插入图片描述
最常见但毫无意义的是,我们可以通过Python切片轻松地按比例排除:

start_index = int(len(the_sum_df)*0.01) # 排除前1%的人
my_word_df  = the_sum_df.iloc[start_index:]
my_word_df  = my_word_df[my_word_df['keyword'].str.len()>2]

也可以删除少于2个字符的单词,如“vs”、“lt”等。

请注意,我使用的是.iloc而不是.loc。因为原始数据集是按TF(term frequency)值重新排序的。iloc将对索引的索引(或索引标签的序列)进行切片。但loc将在索引标签上切片。

建立关键词词汇表-bigram

在建立二元短语表时,不仅要考虑出现频率,还要考虑其与相邻词的关系。

例如短语“they are”,多次出现在一起,但他们只能跟在有限的词后面,就像他们是兄弟一样,他们是好人,这些词具有很高的内部粘性,但外部连接灵活性很低。

这通常可以用信息熵来度量。熵值越高,表示与其他词一起使用的可能性越大。
在这里插入图片描述
而那些对我们大脑具有高内部粘性(计数频率)和高外部熵的短语,我们称之为“普通短语”,这些是我们想要添加到提取词汇表中的。

NLTK提供了一个类似的解决方案来解决bigram短语提取问题。

from nltk.collocations import BigramAssocMeasures
from nltk.collocations import BigramCollocationFinder
from nltk.tokenize import word_tokenize
text_set_words  = [word_tokenize(reuters.raw(fileid).lower()) 
                   for fileid in reuters.fileids()] #1
bigram_measures = BigramAssocMeasures()
finder = BigramCollocationFinder.from_documents(text_set_words) #2
finder.apply_freq_filter(3) #3
finder.apply_word_filter(lambda w: 
                         len(w) < 3 
                         or len(w) > 15 
                         or w.lower() in ignored_words) #4
phrase_result = finder.nbest(bigram_measures.pmi, 20000) #5
colloc_strings = [w1+' '+w2 for w1,w2 in phrase_result] #6

代码#1,在这个Python理解表达式中,我使用word_tokenize将文档标记为单词列表。输出如下:

[
    ['word1','word2',...,'wordn'], 
    ['word1','word2',...,'wordn'],
    ...
    ['word1','word2',...,'wordn']
]

代码#2,从标记化文档列表启动bigram查找器对象。还有另一个函数from_words()可以处理标记词列表。

代码#3,删除频率小于3的候选项。

编码#4,删除单词长度小于3或大于15的候选词。

代码#5,使用BigramAssocMeasures中的pmi函数来测量2个单词短语的可能性。此链接列出所有其他度量函数和源代码:https://tedboy.github.io/nlps/generated/generated/nltk.BigramAssocMeasures.html#methods

代码#6,将结果转换为更可读的格式。

通过将BigramAssocMeasures、BigramCollocationFinder替换为TrigramAssocMeasures和TrigramCollocationFinder,你将得到3个单词的短语提取器。在路透社关键词提取样本中,我将跳过3个单词的短语。我在这里发布了示例代码,以防你需要它。

from nltk.collocations import TrigramAssocMeasures
from nltk.collocations import TrigramCollocationFinder
from nltk.tokenize import word_tokenize
text_set_words  = [word_tokenize(reuters.raw(fileid).lower()) 
                   for fileid in reuters.fileids()]
trigram_measures = TrigramAssocMeasures()
finder = TrigramCollocationFinder.from_documents(text_set_words)
finder.apply_freq_filter(3)
finder.apply_word_filter(lambda w: 
                         len(w) < 3 
                         or len(w) > 15 
                         or w.lower() in ignored_words)
tri_phrase_result = finder.nbest(bigram_measures.pmi, 1000)
tri_colloc_strings = [w1+' '+w2+' '+w3 for w1,w2,w3 in tri_phrase_result] 
tri_colloc_strings[:10]

用TF-IDF测量关键词权重

现在,让我们将单字和双字短语组合在一起,构建路透社定制的词汇表。

my_vocabulary = []
my_vocabulary.extend(my_word_df['keyword'].tolist()) 
my_vocabulary.extend(colloc_strings)

我们启动吧。请注意,请找到一台至少有16g RAM的机器来运行代码。TF-IDF计算需要一段时间,可能会占用大量内存。

from sklearn.feature_extraction.text import TfidfVectorizer
vec          = TfidfVectorizer(
                    analyzer     ='word'
                    ,ngram_range =(1, 2)
                    ,vocabulary  =my_vocabulary)
text_set     = [reuters.raw(fileid) for fileid in reuters.fileids()]
tf_idf       = vec.fit_transform(text_set)
result_tfidf = pd.DataFrame(tf_idf.toarray()
                            , columns=vec.get_feature_names()) #1

将结果集转换为代码#1中的Dateframe后,result_tfidf将保存所有关键字的TF-IDF值:

图片

看看结果

让我们查看其中一篇文章,并将其与上面提取器提取的关键字进行比较,以验证其有效性。
通过指定fileid索引输出一个原始文档。

file_index= 202 # 更改号码以检查不同的文章
fileid = reuters.fileids()[file_index]
print(fileid,"\n"
        ,reuters.raw(fileid),"\n"
        ,reuters.categories(fileid),"\n")

返回fileid、raw及其categories(嗯,很多年前,美国和日本打了一场关税战)

test/15223 
 WHITE HOUSE SAYS JAPANESE TARRIFFS LIKELY
  The White House said high U.S.
  Tariffs on Japanese electronic goods would likely be imposed as
  scheduled on April 17, despite an all-out effort by Japan to
  avoid them.
      Presidential spokesman Marlin Fitzwater made the remark one
  day before U.S. And Japanese officials are to meet under the
  emergency provisions of a July 1986 semiconductor pact to
  discuss trade and the punitive tariffs.
      Fitzwater said: "I would say Japan is applying the
  full-court press...They certainly are putting both feet forward
  in terms of explaining their position." But he added that "all
  indications are they (the tariffs) will take effect."

 ['trade']

打印出我们dataframe对象中的前10个关键字。

test_tfidf_row = result_tfidf.loc[file_index]
keywords_df = pd.DataFrame({
    
    
    'keyword':test_tfidf_row.index,
    'tf-idf':test_tfidf_row.values
})
keywords_df = keywords_df[
    keywords_df['tf-idf'] >0
].sort_values(by=['tf-idf'],ascending=False)
keywords_df[:10]

十大关键词:
在这里插入图片描述
看起来这里的white,house和white house是一样的。我们需要删除那些已经出现在两个单词短语中的单词。

bigram_words = [item.split() 
                    for item in keywords_df['keyword'].tolist() 
                    if len(item.split())==2]
bigram_words_set = set(subitem 
                        for item in bigram_words 
                        for subitem in item) 
keywords_df_new = keywords_df[~keywords_df['keyword'].isin(bigram_words_set)]

上面的代码首先构建一个单词集,其中包含来自2个单词短语的单词。然后,通过~xxxx.isin(xxxx)过滤出已经在两个单词短语中使用的单个单词。

其他考虑因素

你拥有的文本语料库越大,TF-IDF提取关键词的效果就越好。路透社语料库包含10788篇文章,结果表明它是有效的。我相信这个解决方案对于更大的文本数据库会更好。

上面的代码在我的macbookairm1中运行不到2分钟,这意味着每日刷新结果集是可行的。

如果你有数百GB甚至TB大小的数据。你可能需要考虑重写C/C++或GO中的逻辑,并且还可以利用GPU的功率来提高性能。

本文所描述的解决方案远不是完美的,例如,我没有过滤掉动词和形容词。解决方案的主干可以扩展到其他语言。

提取的关键字

让我们再次打印出最终结果。

keywords_df_new[:10]

关税得到最高的TF-IDF值,其余关键字看起来很好地代表了路透社的这篇文章。达到目标!
在这里插入图片描述

参考文献

Guess you like

Origin blog.csdn.net/weixin_35770067/article/details/119576701