目录
n-gram概念
n-gram是一个最多包含n个元素的序列,这些元素从由它们组成的序列(通常是字符串)中提取而成。一般来说,n-gram的“元素”可以使字符、音节、词,甚至是像A、T、G、C等表示DNA序列的符号。
n-gram不一定要求像复合词一样具有特定的含义,而仅仅要求出现频率足够高以引起词条计数器的注意。当一个词条序列向量化成词袋向量时,它丢失了次序中所包含的很多含义。将单词条的概念扩招到多词条构成的n-gram,NLP流水线就可以保留语句词序中隐含的很多含义。例如,相比与词袋向量中的1-gram,2-gram “was not”保留了两个独立词“was”、“not”的更多部分的含义。在流水线中如果把一个词和其相邻词捆绑起来,就会使词的一部分上下文被保留。
下面是之前用过的例子下,进行2-gram分词的结果:
from nltk.tokenize import word_tokenize
from nltk import ngrams
# import nltk
# nltk.download('punkt_tab')
sentence="""
Thomas Jefferson Began buliding Monticelli as the age of 26.\n
"""
n=2
n2grams=ngrams(word_tokenize(sentence),2)
print(type(n2grams))
print(list(n2grams))
可以看到,上述2-gram序列会比原来的词序列包含更多的信息。由于NLP流水线的后续步骤只能访问前面分词器生成的词条,因此,我们必须要让后续阶段知道“Thomas”并不与“Isaiah Thomas”或 “Thomas & friends”有关。n-gram是当数据在流水线中传输时保留上下文信息的一种办法。
下面是原始的1-gram分词器:
sentence="""
Thomas Jefferson Began buliding Monticelli as the age of 26.\n
"""
pattern=re.compile(r"([-\s.,;!?])+")
tokens=pattern.split(sentence)
tokens=[x for x in tokens if x and x not in '- \t\n.,;!?']
print(tokens)
下面是bltk中的n-gram分词器的实际运行效果:
from nltk.util import ngrams
print(list(ngrams(tokens,2)))
print(list(ngrams(tokens,3)))
在上面的代码中,n-gram是以一个个元组的方式来提供的,但是如果希望流水线中的所有词条都是字符串的话,那么也可以很容易地将这些元组连接在一起。这将允许流水线的后续阶段输入的数据类型保持一致,即都是字符串序列:
two_grams=list(ngrams(tokens,2))
print([" ".join(x) for x in two_grams])
如果词条或者n-gram出现的特别少,它们就不会承载太多其他词的关联信息,而这些关联信息可以用于帮助识别文档的主题,这些主题可以将多篇文档或者多个文档连接起来。因此,函件的n-gram对分类问题作用并不显著。我们可以想象,大部分2-gram都十分罕见,更不用说3-gram和4-gram了。
由于词的组合结果比独立的词多得多,因此词汇表的大小会以指数方式接近语料库中的所有文档中的n-gram数。如果特征向量的维度超过所有文档的总长度,特征提取过程就不会达到预期的目的。事实上机器学习模型和向量之间的过拟合几乎不可能避免,这是由于向量的维数多余语料库中的文档数而造成的。通常来说,出现次数过少的n-gram会被过滤掉。
但是像是“at the”这类意义不大的常见短语,出现过于频繁的n-gram也会被过滤掉,因为它们没办法提供什么预测能力。
停用词
在任何一种语言中,停用词指的是那些出现频率非常高的常见词,但是对短语的含义而言,这些词承载的实质性信息却少得多。一些常见的停用词如下:
1、the,this,a,an,of,on,and
2、的、那、人
从传统上说,NLP流水线都会剔除停用词,一遍减小从文本中提取信息时的计算压力。虽然词本身可能承载很小的信息,但是停用词可以提供n-gram中的重要关系信息。例如,下面的两个例子:
1、Mark reported to the CEO
2、Suzanne reported as the CEO to the board
在NLP流水线中,我们可能会产生 reported to the CEO 和 reported as the CEO 这样的4-gram。如果从这些4-gram中剔除了停用词,那么上面两个例子都会变成 reported CEO ,这样就会丢失其中的上下属关系信息。第一个case 中,Mark可能是CEO的助理,而第二个case中,Suzanne则是向董事会汇报的CEO。
但是,保留流水线中的停用词可能会带来另一个问题:它会增加所需的n-gram的长度(即n),长度增加是为了保住上述由原本毫无意义的停用词所产生的关联关系。基于这个原因,如果要避免上述例子中的歧义,我们至少要保留4-gram。
如何设计停用词过滤器依赖具体的应用。词汇表的大小会决定NLP流水线所有后续步骤的计算复杂性和内层开销。但是,停用词只占词汇表的很小一部分。一个典型的停用词大概只包含100个左右高频的非重要词。但是,要记录大规模推文、博客和新闻中出现的95%的词,需要大概20000个词的词汇表,而这只考虑了1-gram或者说单个词的词条。要容纳某个大规模英文语料库中的95%的2-gram,需要设计的2-gram词汇表通常会包括超过100万个不同的2-gram词条。
一般来说,训练要足够大以避免对任何具体词或者词的组合造成过拟合,训练集的大小会决定对它的处理量,但是,从20000个词中剔除100个停用词不会显著加快上述处理过程,对于2-gram词汇表胃炎,通过剔除停用词而获得的好处无足轻重。此外,如果不对使用停用词的2-gram频率进行检查就武断的剔除这些停用词的话,可能会丢失很多信息。
因此,如果我们有足够的内存和处理带宽来运行大规模词汇表下NLP流水线中的所有步骤,那么可能不必为在这里或那里忽略几个不重要的词而忧虑。如果担心大规模词汇表或小规模训练集之间发生过拟合的话,那么有比忽略停用词更好的方法来选择词汇表或者降维。在词汇表中保留停用词能够运行文档频率过滤器更精准地识别或者忽略那些在具体领域中包含最小信息内容的词或者n-gram。
如果确实想在分词过程中粗暴地去掉停用词的话,使用Python中的列表解析式就足够了。下面是一些停用词以及在词条列表中迭代以剔除它们的代码片段:
stop_words=['a','an','the','on','of','off','this','is']
tokens=['the','house','is','on','fire']
tokens_without_stopwords=[x for x in tokens if x not in stop_words]
print(tokens_without_stopwords)
我们可以看到,某些词会比其他词承载更多的意义。在某些句子中可以去掉超过一半的词但是句子的意义并不会收到显著影响。即使没有冠词、介词甚至动词“to be”的各种形式。
为了得到一个完整的“标准”停用词表,可以参考NLTK,NLTK可能提供了使用最普遍的停用词表:
nltk.download('stopwords')
stop_words=nltk.corpus.stopwords.words('english')
print(len(stop_words))
print(stop_words[:7])
NLTK包中奖代词(不止是第一人称)纳入其停用词表中。此外,单个字母的停用词看上去很古怪,但是如果多次使用NLTK分词器和Porter词干还原工具的话,这些停用词是讲得通的。当使用NLTK分词器和词干还原工具对缩略语进行分割和词干还原时,这些单字母的词条就会经常出现。
根据想忽略的自然语言信息的多少,可以为流水线使用多个停用词的并集或交集。下面是sklearn和nltk之间停用词的比较情况:
from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS as sklearn_stop_words
print(len(sklearn_stop_words))
print(len(stop_words))
print(len(set(stop_words).union(set(sklearn_stop_words))))
print(len(set(stop_words).intersection(set(sklearn_stop_words))))