在上一节《Tokenization - NLP(1)》的学习中,我们主要学习了如何将一串字符串分割成单独的字符,并且形成一个词汇集(vocabulary),之后我们将形成的词汇集合转换成计算机可以处理的数字信息,以方便我们做进一步文本分析。这篇博客的主题还是我们如何将文本转成成更有用的成分,让我们能从文本当中提取到更多的信息以便作为特征输入到模型中训练,首先会介绍一下N-grams算法,之后会提到停顿词及英文文本常见的标准化处理手段,如大小写的改变、词干提取等(文章中的某些例子会涉及到正则表达式的使用,但是因为不是主要的内容,对使用到的正则表达式不做过多解释,如果有需要的话自己找下书籍和在网上搜索下如何使用正则表达式)。
一、N-grams
自然语言处理过程中,一个值得我们注意的是,如果我们仅仅是将文本字符串分割成单独的文本,此时我们只是简单的去分析文本中每个字符所代表的潜在意义与我们需要分析的结果的关系性,然而我们忽略一个非常重要的信息,文本的顺序是含有非常的重要信息。举一个简单的例子,“钓鱼”两个词,如果我们单独去分析这两个词,而不是看作一个整体的话,那么我们得到的语意意思就是“钓”是一个动作词,“鱼”是一个名词,而当两个字放在一起的时候,我们知道其实我们想表述的“钓鱼”是我们要做的一个活动(event)。又比如英文“hot dog",我们都知道这个词组想表达的是我们吃的食物”热狗香肠包“,所以我们不希望单独去看hot和dog两个意思,如果是这样子我们可以看出意思相差非常的远,由此我们可以看出文本顺序的重要性。
而实际操作中,我们将这种把文本顺序保留下来的行为称之为建立N-grams模型,也就是我们将一个字符串分割成含有多个词的标识符(tokens)。当然,需要记住的一点是不论是上一节说的还是N-grams,他们都属于文本字符串Tokenization的一个过程。
1 import re 2 from nltk.util import ngrams 3 4 sentence = "I love deep learning as it can help me resolve some complicated problems in 2018." 5 6 # tokenize the sentence into tokens 7 pattern = re.compile(r"([-\s.,;!?])+") 8 tokens = pattern.split(sentence) 9 tokens = [x for x in tokens if x and x not in '- \t\n.,;!?'] 10 11 bigrams = list(ngrams(tokens, 2)) 12 print([" ".join(x) for x in bigrams])
上述代码的输出结果是:
['I love', 'love deep', 'deep learning', 'learning as', 'as it', 'it can', 'can help', 'help me', 'me resolve', 'resolve some', 'some complicated', 'complicated problems', 'problems in', 'in 2018']
上述代码的执行是首先将文本字符串分割成单独(unique)标识符,并且引入了正则表达式(更多的正则表达式请参看其他资料,这里有必要指出,当我们做一些大型的文本分析时,其实真正用正则表达式去书写相应的规则执行起来的效率是很低的,因为文本是千变万化,几乎没有相同的,例如每个人在微博上post的东西,所附在文本上的字符是千差万别,然后我们一般电子书上的文本又与网络的不同,所以就形成了无法用一套正则表达式的规则去完成所有的任务,普适性是很差的。)来更精准的分割字符串。除此之外,我们运用了NLTK的库来分割出一个含有两个词(Bi-Gram)的标识符。所以从上面我们可以看出,如“deep learning"和”complicated problems“这样子的组合更切合我们想要表达的意思,但是独个字符看的话我们就未必看得出了。
虽然N-grams模型可以让我们更好的去分割出具有更好语意的标识符,进而让我们做进一步文本分析,但是缺点也是同样明显,那就是运用N-grams模型可能让我们的词汇量成指数级的增长,并且并不是所有的Bigram都含有有用信息,而这个情况在甚至乎在Trigram或者Quad gram等含有更多单独字符在内的N-grams模型会更严重。这样子做产生的问题就是我们最终拿到的特征向量(the dimension of the feature vectors)的维度将会超过我们本身的文件样本数(length of the documents),而最终当我们将这些提取出来的特征放入到机器学习算法中的话,就会导致过拟合(over fitting)的情况。如此训练出来的模型将没有什么太好的performance和预测能力。
二、Stop Words
造成上述问题的一个原因可能是我们分割出来的标识符(n-grams)含有太多的不具备有用信息的组合,如带有停顿词(stop words)的词组组合,停顿词在英文中出现的频率是非常高的,如a, an, and, or, of, at, the等等单词,这些单词携带的信息量(substantive information)是极度有限的。所以我们需要做的就是在NLP分析过程中将文本中的停顿词去掉,这样子做的好处是我们减少词汇量,进而降低我们特征向量的维度。But.......我们还是需要再次注意一个问题,那就是虽然停顿词本身所携带的信息不是很多,但是stop words却可能在n-grams中存在关系性信息(relational information),考虑下面两种情况:
- Mark reported to the CEO
- Susan reported as the CEO to the board
在上述例子中,如果我们将to the和as the去掉的话,那么我们就会得到reported CEO,这是很迷惑的,因为这两个句子中本身是有一个层级意思的,但是因为我们remove掉了as,the和to这三个stop words导致了关系信息的缺失。正常情况下,我们需要创建一个4-grams的词(如上述紫色字部分标注高亮的部分)。这也就延申出我们需要讨论的关于NLP模型创立过程中碰到的一个问题,那就是基本是特定问题需要特定的解决办法。具体为根据实际运用而定,创建一个过滤器适当的过滤掉我们不需要的stop words。
下面我们通过NLTK的库来看看英文中大概都有那些stop words:
1 import nltk 2 3 nltk.download("stopwords") 4 stopwords = nltk.corpus.stopwords.words("english") 5 print(len(stopwords)) 6 print(stopwords[:50])
输出结果为:
[nltk_data] Downloading package stopwords to [nltk_data] C:\Users\JielongSSS\AppData\Roaming\nltk_data... [nltk_data] Package stopwords is already up-to-date! 179 ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're", "you've", "you'll", "you'd", 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', "she's", 'her', 'hers', 'herself', 'it', "it's", 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', "that'll", 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be']
三、标准化处理(Normalization)
讨论那么多,我们也应该意识到一个问题:一个NLP模型的表现(performance)很大程度取决于我们所拥有的词汇量(额。。。其实嘛,很难有一个定量的分析,起码目前在学习过程中给我的感觉是如此,词汇量应该掌握在一个具体什么样的程度呢?希望有大神看了我的博客文章也留言给我,是否有一个有效的衡量方法去查看究竟我们所需要的词汇量是多少?)。当然这部分我们要讲的主要是如何通过其他方式来缩减我们tokenize之后创建的feature vector的维度,也就是减少我们的词汇量,以最大程度的保留我们所需要的觉得有用的信息。具体一般有三种处理方式:CASE FOLDING(大小写的改变),Stemming和Lemmatization,下面会展开详细的说明。
3.1 CASE Folding
在英文的NLP模型中,单词的大小写是非常敏感的,这跟我们中文比较不一样,中文是没有所谓的大小写之说的,在这里因为主要以英文NLP为主,所以就只讲英文的标准了,未来有机会更新博客的时候我会尝试引入中文相关的NLP处理方式。我们都知道,在书写英文句子的时候,我们总是让开头的第一个单词的首字母处于大写状况,或者说我们想要强调某些事件的时候,我们就希望用全大写来表示,但是我们同样知道But和but是同一个单词but并且表示同一个意思,然而文本分析过程中这是两个不同的单词,仅仅是因为他们的首字母不一样,这样子计算机自动分析的时候得到的结果就会导致有偏差,所以我们需要对这But这个单词进行大小写的规范处理,从而减少我们的词汇量。
1 tokens = ['Horse', 'horse', 'Dog', 'dog', 'Cat', 'cat'] 2 print(tokens) 3 print("单词数量为: ",len(set(tokens))) 4 5 normalized_tokens = [x.lower() for x in tokens] 6 print(normalized_tokens) 7 print("Normalized之后的单词数量为: ",len(set(normalized_tokens)))
输出结果为:
['Horse', 'horse', 'Dog', 'dog', 'Cat', 'cat'] 单词数量为: 6 ['horse', 'horse', 'dog', 'dog', 'cat', 'cat'] Normalized之后的单词数量为: 3
从上面的结果我们可以看出,我们单词的数量从6个变为了3个,因为Horse和horse表达的就是同一个东西。当然,就如我们开头所说的,英文单词对于大小写是很敏感的,也就意味着大小写的单词对于英文单词所要表达的意思可能是不同的,如Doctor和doctor在大小写方面前者表示为博士,后者我们说的一般是医生的意思,这是我们需要注意的一点,当然你并无法完全针对每个大小写敏感的单词去做case normalization,所以一般情况我们根据需求而定,取舍来做分析,大部分时候的做法是我们只对句子的首个单词的首字母进行case normalization,这只是提供一种分析方法,根据学习过程获得信息,英文的NLP模型最终都是不采用case normalization的,以免丢失太多的信息,对于中文等一些语言,大小写不敏感的,这个就更没意义了。
3.2 Stemming
Stemming是另外一个处理英文文本会用到的技巧,主要是单词的复数形式中或者指代所有格结果等单词中提取出相应的词干(stem)。例如,我们知道cats,horses的词干形式是cat和horse,又比如doing的词干为do。通过这样子的处理,我们将很多不同形式的词回复为其原本的词干形式,这样子做有很大的作用。一个实例就是搜索引擎,当你搜索某样的东西的时候,很多时候你可能不知道你所需要搜索的东西的具体拼写方式,所以我们只是键入你觉得可能的词,但是此时我们需要机器反馈给我具有相关联系的搜索结果,这个结果不仅仅是需要语意上尽可能地相同,大部分时候我们是基于关键字匹配的,如果采取的是100%的匹配的话,得到的结果将会是很有限,这时候通过词干的匹配来检索呈现出相应的结果就显得异常的重要。而对于我们搭建模型,在预处理文本的阶段,则大大的减少了我们的词汇量(意味着我们不需要大空间储存)与此同时它也尽可能地规避减少信息地丢失。不仅如此,提取词干也同时让我们地模型更具普适性,这点符合我们刚才说的搜索引擎的例子。这里有一点需要注意的是,这里的词干并非严格意义上的词干,而只是我们所说的字符或者标识符,这个标识符可能表示的是好几种不同拼写形式的单词。
1 def stemming(sent): 2 return ' '.join([re.findall('^(.*ss|.*?)(s)?$', word)[0][0].strip("'") for word in sent.lower().split()]) 3 4 stemming('horses')
上述代码的输出结果为:
'horse'
正则表达式中想要表达的是如果一个单词的结尾为s的话则词干为去掉s之后的单词,如果多余一个s作为结尾的话,那么这个词保持原型。上述的代码示例能解决的问题是很有限的,因为更复杂的诸如dishes这样子的单词,我们知道去掉的是es,而不仅仅是s,如果要达到足够高的精准度,那我们需要写的正则表达式也会逐步增多。这样子代码执行起来的效率也不够高。下面介绍一下用NLTK库中的PorterStemmer来提取文本的词干。
1 from nltk.stem.porter import PorterStemmer 2 3 stemmer = PorterStemmer() 4 print(' '.join([stemmer.stem(w).strip("'") for w in "dishes washer's washed dishes".split()]))
输出结果为:
dish washer wash dish
3.3 Lemmatization
词形还原(lemmatization)也是一种在英文语言处理中比较常见的的技巧,大致的作用与词干提取类似,也是希望不同形式的单词可以在经过处理之后恢复为他们原本的模样,但是词形还原更多的是放在了单词本身的语意上。所以,词形还原其实比词干提取和大小写的改变更适合预处理文本,因为他们不是简单的改变单词的大小写或者单复数或者所有格的形式,而是基于语意去做还原。比如,我们如果用词干提取去处理better这个单词的时候,我们可能会把单词的er去掉,这样子单词就会编程bet或者bett,这完全改变了单词的意思,但是如果是基于词形还原,那么我们就得到类似的词,如good,best等等。在正式NLP模型创建过程中,我们一般是希望词形还原的运用是在词干提取前面,因为在英文文本中,lemmatization处理过后的单词更接近单词本身所要表达的意思,并且同样的也可以减少我们特征的维度。下面是通过NLTK上的WordNetLemmatizer函数来让你了解下词形还原是如何工作的:
1 from nltk.stem import WordNetLemmatizer 2 3 lemmatizer = WordNetLemmatizer() 4 print(lemmatizer.lemmatize('better')) 5 print(lemmatizer.lemmatize('better', pos='a'))
输出结果为:
better
good
上述代码第五行中的pos是part of speech是词性标注的意思,a代表的形容词的形式。
综上,我们可以看出,词干提取和词形还原都可以减少单词的词汇量,但是同时他们也增加了文本的迷惑性,因为不可能将不同形式的单词100%的恢复成所要表达的单词形式,更需要明白的是,即使词干一样,基于该呈现出来的不同形式的单词的意思也会差很多,所以迷惑性也就增加了,这样子对我们自然语言文本分析其实变相的增加了难度,在实际的运用做,我们需要根据实际情况运用上述讲到的算法原理和技巧。