NLP基础 Tokenization TF BOW TF-IDF

标识化 Tokenization

基础概念

标识化即将文本分割成一个小块一个小块,如以一个英文单词或者汉字为单位,方便更集中地分析文本信息的内容和文本想表达含义;

分割是一个大范围,不仅仅可以分成不同的词,也可以分成段落,进而是句子,进而是词;

一般来说标识化是将整句分割成单个标识符Tokens;

其目的是将没有结构的数据unstructured data 或自然语言文本,分割成分散discrete块信息,从而进行统计计算。这样可通过计算每个标识符tokens 在文本中出现的频次,形成一个频次向量vector 组来表示。完成转换后,可直接将这些频次向量作为特征输入到算法模型进行训练;

如下简单分词实例:

sentence_example="I love deep learning, I love natural language processing."
token_seq1=str.split(sentence_example)
token_seq2=sentence_example.split(' ')
print("token_seq1:", token_seq1)
print("token_seq2:", token_seq2)

输出结果:

token_seq1: [‘I’, ‘love’, ‘deep’, ‘learning,’, ‘I’, ‘love’, ‘natural’, ‘language’, ‘processing.’]
token_seq2: [‘I’, ‘love’, ‘deep’, ‘learning,’, ‘I’, ‘love’, ‘natural’, ‘language’, ‘processing.’]

One-hot encoding 独热编码

one-hot-encodeing 创建一个数组(矩阵),数组由多个向量组成,每个向量中有一个数字1,其余数字为0, one-hot 因此而来;

下面简易构造one-hot-encoding模型:

import numpy as np
token_seq=str.split(sentence_example)
print(token_seq)
vocab=sorted(set(token_seq)) #set函数过滤重复词汇,sortet将数字,字母(先大写后小写)方式
print(vocab)

输出结果:

[‘I’, ‘love’, ‘deep’, ‘learning,’, ‘I’, ‘love’, ‘natural’, ‘language’, ‘processing.’]
[‘I’, ‘deep’, ‘language’, ‘learning,’, ‘love’, ‘natural’, ‘processing.’]

num_tokens=len(token_seq)
print(num_tokens)
vocab_size=len(vocab)
print(vocab_size)

输出结果:

9
7

one_hot_vector=np.zeros((num_tokens,vocab_size),int)
for i,word in enumerate(token_seq):
    one_hot_vector[i,vocab.index(word)]=1
print(one_hot_vector)

输出:

[[1 0 0 0 0 0 0]
[0 0 0 0 1 0 0]
[0 1 0 0 0 0 0]
[0 0 0 1 0 0 0]
[1 0 0 0 0 0 0]
[0 0 0 0 1 0 0]
[0 0 0 0 0 1 0]
[0 0 1 0 0 0 0]
[0 0 0 0 0 0 1]]

以上很容易看到其缺点:
(1)以上矩阵大小是9X7, 而平时处理文本或文集非常庞大,需要创建非常大的矩阵来表示出现过的单词,计算效率很低,且需要非常大的储存空间;
(2)向量组极度稀疏,因为只包含一个非0数字,对于计算及寻找特征没有太大意义;

可以用一个更简单些方法,如用字典来标识已有字符

sent_bow={}
i=1
for token in vocab:
    sent_bow[token]=i
    i+=1
print(sent_bow)
print(sorted(sent_bow.items()))

输出:

{‘I’: 1, ‘deep’: 2, ‘language’: 3, ‘learning,’: 4, ‘love’: 5, ‘natural’: 6, ‘processing.’: 7}
[(‘I’, 1), (‘deep’, 2), (‘language’, 3), (‘learning,’, 4), (‘love’, 5), (‘natural’, 6), (‘processing.’, 7)]

N-grams 标识

上面仅仅将文本字符串分割成单独的文本,此时只是简单的去分析文本中每个字符串所代表的潜在意义,然而忽略了一个重要信息,就是文本顺序。实际操作中,将这种把文本顺序保留下来的行为成为建立N-grams模型,也就是把一个字符串分割成含有多个词的标识符(tokens)。

import re
from nltk.util import ngrams

sentence="I love deep learning as it can help me resolve some complicated problems in 2018."

#tokenize the sentence into tokens
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)

bigrams=list(ngrams(tokens,2)) #ngrams(tokens,num),tokens为原句去掉符号
print(bigrams)
print([" ".join(x) for x in bigrams])

输出:

[‘I’, ‘love’, ‘deep’, ‘learning’, ‘as’, ‘it’, ‘can’, ‘help’, ‘me’, ‘resolve’, ‘some’, ‘complicated’, ‘problems’, ‘in’, ‘2018’]
[(‘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’)]
[‘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)标识符,并利用正则表达式进行分割和去除无用符号。然后利用了nltk.ngrams 模块来分割一个含有2个词的标识符(Bi-Gram, 代码中的2也可设成其他正数,来创建n-Gram)。

虽然N-grams模型可以让我们更好地分割出具有更好语义的标识符,但缺点依然明显:会让词汇量成指数级增长,并且不是所有的Bi-gram都含有有用信息,而这个情况在Trigram或Quad gram等含有更多单独字符的N-gram模型会更严重。

这样做,最终提取的特征向量维数会超过我们本身文件样本数,当放入机器学习算法时,会导致过拟合。没有什么太好的performance和预测能力。

Stopwords 停顿词

造成上述问题一个原因是分割处理的标识符(n-grams)含有太多不具备有用信息的组合,如带有停顿词(stop words)的词组组合,如a, an, and, or, of, at, the等。它们携带的信息量(substantive information)极度有限。所以需要去除,进而降低特征向量维度。

但是我们还是需要再次注意一个问题,就是虽然停顿词本身携带信息不多,但是却可能在n-grams中存在关系性信息,如:

Mark reported to the CEO Susan reported as the CEO to the board. 上述例子中,如果把to the 和as the去掉,就会得到reported CEO, 非常迷惑。这是因为我们去掉了stop words导致了关系信息的缺失。 正常情况,我们需要创建一个4-grams的词。

这也就延伸到我们需要讨论的关于NLP模型创建过程中遇到的一个问题,就是特定问题需要特定解决方法。具体根据实际运用而定,创建一个过滤器适当地过滤掉我们不需要的stop words。

如下是nltk.corpus下的stopwords列举

import nltk
nltk.download('stopwords')
stopwords=nltk.corpus.stopwords.words('english')
print(len(stopwords))
print(stopwords[:50])

输出:

[nltk_data] Downloading package stopwords to
[nltk_data] C:\Users\Liang\AppData\Roaming\nltk_data…
[nltk_data] Unzipping corpora\stopwords.zip.
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的表现,很大程度取决于所拥有词汇量。这部分主要介绍如何缩减tokenize后的feature vector维度,从而减少词汇量,且最大程度保留我们需要的有用信息。具体先介绍三种处理方式:CASE Folding(改变大小写), Stemming和Lemmatization(单词还原)。

CASE Folding 大小写还原

英文NLP模型中,单词大小写非常敏感。在书写英文句子时,开头首字母常有大写情况,或者强调某些事件时,希望全用大写表示,但是我们希望But 和but当做同一个词。但在tokenize过程,这是不同的单词,仅仅因为他们首字母大小写不一样。所以可以对大小写进行规范处理,从而减小词汇量。

tokens=['Horse','horse','Dog','dog','Cat','cat']
print(tokens)
print('单词数量:',len(set(tokens)))

normalized_tokens=[x.lower() for x in tokens]
print(normalized_tokens)
print('Normalized之后单词数量:',len(set(normalized_tokens)))

[‘Horse’, ‘horse’, ‘Dog’, ‘dog’, ‘Cat’, ‘cat’]
单词数量: 6
[‘horse’, ‘horse’, ‘dog’, ‘dog’, ‘cat’, ‘cat’]
Normalized之后单词数量: 3

上面可以看到,规范大小写后可以减小维度。英文单词对于大小写是很敏感的,也就意味着大小写的单词对于英文单词所要表达的意思可能是不同的,如Doctor和doctor在大小写方面前者表示为博士,后者我们说的一般是医生的意思,这是我们需要注意的一点,当然你并无法完全针对每个大小写敏感的单词去做case normalization,所以一般情况我们根据需求而定,取舍来做分析,大部分时候的做法是我们只对句子的首个单词的首字母进行case normalization,这只是提供一种分析方法,根据学习过程获得信息,英文的NLP模型最终都是不采用case normalization的,以免丢失太多的信息,对于中文等一些语言,大小写不敏感的,这个就更没意义了。

Stemming 提取词干

Stemming是另外一个处理英文用到技巧,主要是单词复数或指代所有格结果等单词中 提取相应词干。如,cats ,horses的词干是cat和horse, doing的词干是do。这样可将不同形式的词恢复为其原本词干形式。

比如搜索引擎,当搜索某样东西时,很多时候可能不知道所需要搜索东西的具体拼写方式,只是输入觉得可能的词,但我们需要机器反馈具有联系的搜索结果,不仅仅要语义上尽可能相同,大部分时候是基于关键词匹配,若采用百分百匹配,得到结果很有限,通过词干匹配来检索呈现相应结果显得异常重要。

对于搭建模型,预处理文本阶段,大大减少了词汇量,与此同时,也要尽可能规避重要信息的丢失。

例子:

def stemming(sent):
    return ' '.join([re.findall('^(.*ss|.*?)(s)?$',word)[0][0].strip("'") for word in sent.lower().split()])
a='horse horses horss horsses'
stemming(a)

‘horse horse horss horsse’

上面例子里,如果一个单词结尾是s, 词干为去掉s的词,如果ss结尾,则保持原型。上述能解决问题有限,如dishes单词。这样的代码不够高效,一般用NLTK库里的ntk.stem.porter 下的PorterStemmer:

from nltk.stem.porter import PorterStemmer
stemmer=PorterStemmer()
print(' '.join([stemmer.stem(w).strip("'") for w in 'dishes horses washers washed dishes'.split()]))

dish hors washer wash dish

Lemmatization 词形还原

Lemmatization作用与词干提取类似,希望不同形式的单词可以经过处理后恢复原来,但词形还原更多放在单词本身语义上。

词形还原比词干提取和大小写改变更适合预处理文本,因为不是简单的改变单词大小写或单复数或所有格形式,而是基于语义去还原。如用词干提取处理better,可能会变成bet, bett, 完全改变了意思,但是如果基于词形还原,能得到类似词,如good, best等。

在正式NLP模型创建过程,一般是希望词形还原用在词干提取前面。因为在英文文本中,lemmatization处理后单词更接近单词本身要表达意思,也减少了特征的维度。

如下例子,利用nltk.stem下面的WordNetLemmatizer:

from nltk.stem import WordNetLemmatizer
import nltk
nltk.download('wordnet')
lemmatizer=WordNetLemmatizer()
print(lemmatizer.lemmatize('better')) #better
print(lemmatizer.lemmatize('better',pos='a')) #good

输出:

[nltk_data] Downloading package wordnet to
[nltk_data] C:\Users\Liang\AppData\Roaming\nltk_data…
[nltk_data] Unzipping corpora\wordnet.zip.
better
good

上述代码的pos意思是part of speech, 意思是词形标注,a代表形容词。

综上可以看出,词干提取和词形还原可以减少单词词汇量,但同时增加文本迷惑性,因为不可能将不同形式的单词百分百恢复成要表达的单词形式。

即使词干一样,基于该词干呈现出来的不同形式单词意思会差很多,所以迷惑性也就增加了,这样对自然语言文本分析变相地增加了难度,实际运作中,需要根据实际情况用上述讲到的算法原理和技巧。

文本向量化和词袋模型

总结之前学习内容:
用NLTK文本处理库将文本的句子成分分成N-grams模型,引入正则表达式去除多余句子成分,去除停顿词,通用标准化处理,如大小写,提取词干等。

接下来看看如何对文本的单词进行统计,并以此来看该词在特定文档或整个文本集中重要性。统计单词的任务是为了给特定词语或单词一个量化的衡量标准,根据该值可以做很多其他工作,如:关键字查找,判定积极消极等。

之前我们用独热编码one-hot encoding 方式对英文文本中的单词进行量化表示,但该方式有比较明显缺点,就是当文本集很大时,得到的向量维度也非常大,不利于计算机处理。因此引入下面的方法。

词袋模型Bag of Words

词袋是对文本单词进行统计,简单说就是统计某个单词在一个文本中出现的频率或次数。

假设一个单词出现频率足够高或者次数足够多,就是说该单词对于文本的重要性足够大或者说它就是在传达文本要表达意思。(上面只是假设,出现频率高,也可能没什么含义,如英文中的The, 比如中文的"的“, 所以需要先去除一些没用的单词或字)

下面用NLTK模块的nltk.tokenize下的TreebankWordTokenizer举例分词处理,用collections 模块下的Counter来统计单词出现次数:

from nltk.tokenize import TreebankWordTokenizer
from collections import Counter

sentence="The faster Ray get to the bus stop, the faster and the faster Ray, can get to the school"
tokenizer=TreebankWordTokenizer()
tokens=tokenizer.tokenize(sentence.lower())
print("The tokens are:",tokens)

bag_of_words=Counter(tokens)
print("The frequency of each word:",bag_of_words)

输出:

The tokens are: [‘the’, ‘faster’, ‘ray’, ‘get’, ‘to’, ‘the’, ‘bus’, ‘stop’, ‘,’, ‘the’, ‘faster’, ‘and’, ‘the’, ‘faster’, ‘ray’, ‘,’, ‘can’, ‘get’, ‘to’, ‘the’, ‘school’]
The frequency of each word: Counter({‘the’: 5, ‘faster’: 3, ‘ray’: 2, ‘get’: 2, ‘to’: 2, ‘,’: 2, ‘bus’: 1, ‘stop’: 1, ‘and’: 1, ‘can’: 1, ‘school’: 1})

但上面例子,仅仅对单词进行简单统计,忽略了单词间连续性,单词与单词的组合才有一定意思,所以简单统计可能导致不能很好的还原原本句子要表达意思。

忽略了顺序的重要性,词袋可能在大文本分析中意义不大,但对于一些小文本或句子分析能起到不错效果,如垃圾邮件检测。

上面Python字典bag_of_words中,实际统计了每个单词出现的频数/频率(Term Frequency), 就是我们常说的TF。如之前所说,去除一些非关键词,如the, 剩下的频次出现最高的前两个是faster 和 ray。可以计算ray 出现频率:

print(bag_of_words.most_common(4))
times_harry_appears=bag_of_words['ray']
num_unique_words=len(bag_of_words)
print(num_unique_words)
tf=times_harry_appears/num_unique_words
print(round(tf,4))

输出:

[(‘the’, 5), (‘faster’, 3), (‘ray’, 2), (‘get’, 2)]
11
0.1818

上述代码成功将文本转为数字,方便进一步计算分析,但还不够,因为我们仅仅将频次转为了频率,如果我们想要量化地去分析文本,特别是用计算机处理,需要将这些数字向量化。如下文本举例:

kite_text= 'A kite is traditionally a tethered heavier-than-air craft with wing surfaces that react\nagainst the air to create lift and drag. A kite consists of wings, tethers, and anchors. Kites\noften have a bridle to guide the face of the kite at the correct angle so the wind can lift it.\nA kite’s wing also may be so designed so a bridle is not needed; when kiting a sailplane\nfor launch, the tether meets the wing at a single point. A kite may have fixed or moving\nanchors. Untraditionally in technical kiting, a kite consists of tether-set-coupled wing\nsets; even in technical kiting, though, a wing in the system is still often called the kite.\nThe lift that sustains the kite in flight is generated when air flows around the kite’s\nsurface, producing low pressure above and high pressure below the wings. The\ninteraction with the wind also generates horizontal drag along the direction of the wind.\nThe resultant force vector from the lift and drag force components is opposed by the\ntension of one or more of the lines or tethers to which the kite is attached. The anchor\npoint of the kite line may be static or moving (such as the towing of a kite by a running\nperson, boat, free-falling anchors as in paragliders and fugitive parakites or vehicle).\nThe same principles of fluid flow apply in liquids and kites are also used under water.\nA hybrid tethered craft comprising both a lighter-than-air balloon as well as a kite lifting\nsurface is called a kytoon.\nKites have a long and varied history and many different types are flown individually and\nat festivals worldwide. Kites may be flown for recreation, art or other practical uses.\nSport kites can be flown in aerial ballet, sometimes as part of a competition. Power kites\nare multi-line steerable kites designed to generate large forces which can be used to\npower activities such as kite surfing, kite landboarding, kite fishing, kite buggying and a\nnew trend snow kiting. Even Man-lifting kites have been made.'
from collections import Counter
from nltk.tokenize import TreebankWordTokenizer

tokenizer=TreebankWordTokenizer()
tokens=tokenizer.tokenize(kite_text.lower())
token_counts=Counter(tokens)
print(token_counts)

输出结果:

Counter({‘the’: 26, ‘a’: 20, ‘kite’: 16, ‘,’: 14, ‘and’: 10, ‘of’: 10, ‘kites’: 8, ‘is’: 7, ‘in’: 7, ‘or’: 6, ‘as’: 6, ‘wing’: 5, ‘to’: 5, ‘be’: 5, ‘lift’: 4, ‘have’: 4, ‘may’: 4, ‘at’: 3, ‘so’: 3, ‘can’: 3, ‘also’: 3, ‘kiting’: 3, ‘are’: 3, ‘flown’: 3, ‘tethered’: 2, ‘craft’: 2, ‘with’: 2, ‘that’: 2, ‘air’: 2, ‘consists’: 2, ‘tethers’: 2, ‘anchors.’: 2, ‘often’: 2, ‘bridle’: 2, ‘wind’: 2, ‘’’: 2, ‘s’: 2, ‘designed’: 2, ‘;’: 2, ‘when’: 2, ‘for’: 2, ‘moving’: 2, ‘technical’: 2, ‘even’: 2, ‘called’: 2, ‘surface’: 2, ‘pressure’: 2, ‘drag’: 2, ‘force’: 2, ‘by’: 2, ‘which’: 2, ‘such’: 2, ‘.’: 2, ‘used’: 2, ‘power’: 2, ‘traditionally’: 1, ‘heavier-than-air’: 1, ‘surfaces’: 1, ‘react’: 1, ‘against’: 1, ‘create’: 1, ‘drag.’: 1, ‘wings’: 1, ‘guide’: 1, ‘face’: 1, ‘correct’: 1, ‘angle’: 1, ‘it.’: 1, ‘not’: 1, ‘needed’: 1, ‘sailplane’: 1, ‘launch’: 1, ‘tether’: 1, ‘meets’: 1, ‘single’: 1, ‘point.’: 1, ‘fixed’: 1, ‘untraditionally’: 1, ‘tether-set-coupled’: 1, ‘sets’: 1, ‘though’: 1, ‘system’: 1, ‘still’: 1, ‘kite.’: 1, ‘sustains’: 1, ‘flight’: 1, ‘generated’: 1, ‘flows’: 1, ‘around’: 1, ‘producing’: 1, ‘low’: 1, ‘above’: 1, ‘high’: 1, ‘below’: 1, ‘wings.’: 1, ‘interaction’: 1, ‘generates’: 1, ‘horizontal’: 1, ‘along’: 1, ‘direction’: 1, ‘wind.’: 1, ‘resultant’: 1, ‘vector’: 1, ‘from’: 1, ‘components’: 1, ‘opposed’: 1, ‘tension’: 1, ‘one’: 1, ‘more’: 1, ‘lines’: 1, ‘attached.’: 1, ‘anchor’: 1, ‘point’: 1, ‘line’: 1, ‘static’: 1, ‘(’: 1, ‘towing’: 1, ‘running’: 1, ‘person’: 1, ‘boat’: 1, ‘free-falling’: 1, ‘anchors’: 1, ‘paragliders’: 1, ‘fugitive’: 1, ‘parakites’: 1, ‘vehicle’: 1, ‘)’: 1, ‘same’: 1, ‘principles’: 1, ‘fluid’: 1, ‘flow’: 1, ‘apply’: 1, ‘liquids’: 1, ‘under’: 1, ‘water.’: 1, ‘hybrid’: 1, ‘comprising’: 1, ‘both’: 1, ‘lighter-than-air’: 1, ‘balloon’: 1, ‘well’: 1, ‘lifting’: 1, ‘kytoon.’: 1, ‘long’: 1, ‘varied’: 1, ‘history’: 1, ‘many’: 1, ‘different’: 1, ‘types’: 1, ‘individually’: 1, ‘festivals’: 1, ‘worldwide.’: 1, ‘recreation’: 1, ‘art’: 1, ‘other’: 1, ‘practical’: 1, ‘uses.’: 1, ‘sport’: 1, ‘aerial’: 1, ‘ballet’: 1, ‘sometimes’: 1, ‘part’: 1, ‘competition.’: 1, ‘multi-line’: 1, ‘steerable’: 1, ‘generate’: 1, ‘large’: 1, ‘forces’: 1, ‘activities’: 1, ‘surfing’: 1, ‘landboarding’: 1, ‘fishing’: 1, ‘buggying’: 1, ‘new’: 1, ‘trend’: 1, ‘snow’: 1, ‘kiting.’: 1, ‘man-lifting’: 1, ‘been’: 1, ‘made’: 1})

下面去掉stopwords词汇:

import nltk

stopwords=nltk.corpus.stopwords.words('english')
tokens=[x for x in tokens if x not in stopwords]
kite_counts=Counter(tokens)
print(kite_counts)

输出结果:

Counter({‘kite’: 16, ‘,’: 14, ‘kites’: 8, ‘wing’: 5, ‘lift’: 4, ‘may’: 4, ‘also’: 3, ‘kiting’: 3, ‘flown’: 3, ‘tethered’: 2, ‘craft’: 2, ‘air’: 2, ‘consists’: 2, ‘tethers’: 2, ‘anchors.’: 2, ‘often’: 2, ‘bridle’: 2, ‘wind’: 2, ‘’’: 2, ‘designed’: 2, ‘;’: 2, ‘moving’: 2, ‘technical’: 2, ‘even’: 2, ‘called’: 2, ‘surface’: 2, ‘pressure’: 2, ‘drag’: 2, ‘force’: 2, ‘.’: 2, ‘used’: 2, ‘power’: 2, ‘traditionally’: 1, ‘heavier-than-air’: 1, ‘surfaces’: 1, ‘react’: 1, ‘create’: 1, ‘drag.’: 1, ‘wings’: 1, ‘guide’: 1, ‘face’: 1, ‘correct’: 1, ‘angle’: 1, ‘it.’: 1, ‘needed’: 1, ‘sailplane’: 1, ‘launch’: 1, ‘tether’: 1, ‘meets’: 1, ‘single’: 1, ‘point.’: 1, ‘fixed’: 1, ‘untraditionally’: 1, ‘tether-set-coupled’: 1, ‘sets’: 1, ‘though’: 1, ‘system’: 1, ‘still’: 1, ‘kite.’: 1, ‘sustains’: 1, ‘flight’: 1, ‘generated’: 1, ‘flows’: 1, ‘around’: 1, ‘producing’: 1, ‘low’: 1, ‘high’: 1, ‘wings.’: 1, ‘interaction’: 1, ‘generates’: 1, ‘horizontal’: 1, ‘along’: 1, ‘direction’: 1, ‘wind.’: 1, ‘resultant’: 1, ‘vector’: 1, ‘components’: 1, ‘opposed’: 1, ‘tension’: 1, ‘one’: 1, ‘lines’: 1, ‘attached.’: 1, ‘anchor’: 1, ‘point’: 1, ‘line’: 1, ‘static’: 1, ‘(’: 1, ‘towing’: 1, ‘running’: 1, ‘person’: 1, ‘boat’: 1, ‘free-falling’: 1, ‘anchors’: 1, ‘paragliders’: 1, ‘fugitive’: 1, ‘parakites’: 1, ‘vehicle’: 1, ‘)’: 1, ‘principles’: 1, ‘fluid’: 1, ‘flow’: 1, ‘apply’: 1, ‘liquids’: 1, ‘water.’: 1, ‘hybrid’: 1, ‘comprising’: 1, ‘lighter-than-air’: 1, ‘balloon’: 1, ‘well’: 1, ‘lifting’: 1, ‘kytoon.’: 1, ‘long’: 1, ‘varied’: 1, ‘history’: 1, ‘many’: 1, ‘different’: 1, ‘types’: 1, ‘individually’: 1, ‘festivals’: 1, ‘worldwide.’: 1, ‘recreation’: 1, ‘art’: 1, ‘practical’: 1, ‘uses.’: 1, ‘sport’: 1, ‘aerial’: 1, ‘ballet’: 1, ‘sometimes’: 1, ‘part’: 1, ‘competition.’: 1, ‘multi-line’: 1, ‘steerable’: 1, ‘generate’: 1, ‘large’: 1, ‘forces’: 1, ‘activities’: 1, ‘surfing’: 1, ‘landboarding’: 1, ‘fishing’: 1, ‘buggying’: 1, ‘new’: 1, ‘trend’: 1, ‘snow’: 1, ‘kiting.’: 1, ‘man-lifting’: 1, ‘made’: 1})

接下来,把上面的频次tokens转化为一个向量,可以用于文本计算分析,但是独立分析单个文本意义不大,一般分析多个文本及多个文本集,得到一个相对大得多的向量组或矩阵。

不同文本因为含有的单词数不同,单独对每个文本进行量化,并不能得到统一大小的向量组,所以要求每个文本生成的向量维度必须要一样,涉及两步:
(1)计算每个标识符的TF而不仅仅是得到每个文档中单词频次;
(2)确保所有向量同一维度;

如下举例:

docs=['The faster Harry got to the store, the faster and faster Harry would get home.']
docs.append('Harry is hairy and faster than Jill.')
docs.append('Jill is not as hairy as Harry.')

doc_tokens=[]
for doc in docs:
    doc_tokens+=[sorted(tokenizer.tokenize(doc.lower()))]
print('文本标识符:',doc_tokens)
print('第一个文本长度',len(doc_tokens[0]))

all_doc_tokens=sum(doc_tokens,[])
print(all_doc_tokens)
print(len(all_doc_tokens))

lexicon=sorted(set(all_doc_tokens))
print('打印出的总词汇量:',lexicon)
print(len(lexicon))

import copy
from collections import OrderedDict

zero_vector=OrderedDict((token,0) for token in lexicon)
print(zero_vector, '\n\n')

doc_vectors=[]
for doc in docs:
    vec=copy.copy(zero_vector) #copy 0矩阵,格式[(..,num),...]
    tokens=tokenizer.tokenize(doc.lower()) #把doc进行分割
    token_counts=Counter(tokens) #计数
    for key,value in token_counts.items():
        vec[key]=round(value/len(lexicon),4) #根据计数更新key对应的value
    doc_vectors.append(vec)          
    #把各doc生成的[(..,num),..]汇总到一个列表中[[(..,num),(..,num)...],[],[]]
for i,doc_vec in enumerate(doc_vectors):
    print('{}:{}'.format(i+1,doc_vec),'\n')

输出:

文本标识符: [[’,’, ‘.’, ‘and’, ‘faster’, ‘faster’, ‘faster’, ‘get’, ‘got’, ‘harry’, ‘harry’, ‘home’, ‘store’, ‘the’, ‘the’, ‘the’, ‘to’, ‘would’], [’.’, ‘and’, ‘faster’, ‘hairy’, ‘harry’, ‘is’, ‘jill’, ‘than’], [’.’, ‘as’, ‘as’, ‘hairy’, ‘harry’, ‘is’, ‘jill’, ‘not’]]
第一个文本长度 17
[’,’, ‘.’, ‘and’, ‘faster’, ‘faster’, ‘faster’, ‘get’, ‘got’, ‘harry’, ‘harry’, ‘home’, ‘store’, ‘the’, ‘the’, ‘the’, ‘to’, ‘would’, ‘.’, ‘and’, ‘faster’, ‘hairy’, ‘harry’, ‘is’, ‘jill’, ‘than’, ‘.’, ‘as’, ‘as’, ‘hairy’, ‘harry’, ‘is’, ‘jill’, ‘not’]
33
打印出的总词汇量: [’,’, ‘.’, ‘and’, ‘as’, ‘faster’, ‘get’, ‘got’, ‘hairy’, ‘harry’, ‘home’, ‘is’, ‘jill’, ‘not’, ‘store’, ‘than’, ‘the’, ‘to’, ‘would’]
18
OrderedDict([(’,’, 0), (’.’, 0), (‘and’, 0), (‘as’, 0), (‘faster’, 0), (‘get’, 0), (‘got’, 0), (‘hairy’, 0), (‘harry’, 0), (‘home’, 0), (‘is’, 0), (‘jill’, 0), (‘not’, 0), (‘store’, 0), (‘than’, 0), (‘the’, 0), (‘to’, 0), (‘would’, 0)])
1:OrderedDict([(’,’, 0.0556), (’.’, 0.0556), (‘and’, 0.0556), (‘as’, 0), (‘faster’, 0.1667), (‘get’, 0.0556), (‘got’, 0.0556), (‘hairy’, 0), (‘harry’, 0.1111), (‘home’, 0.0556), (‘is’, 0), (‘jill’, 0), (‘not’, 0), (‘store’, 0.0556), (‘than’, 0), (‘the’, 0.1667), (‘to’, 0.0556), (‘would’, 0.0556)])
2:OrderedDict([(’,’, 0), (’.’, 0.0556), (‘and’, 0.0556), (‘as’, 0), (‘faster’, 0.0556), (‘get’, 0), (‘got’, 0), (‘hairy’, 0.0556), (‘harry’, 0.0556), (‘home’, 0), (‘is’, 0.0556), (‘jill’, 0.0556), (‘not’, 0), (‘store’, 0), (‘than’, 0.0556), (‘the’, 0), (‘to’, 0), (‘would’, 0)])
3:OrderedDict([(’,’, 0), (’.’, 0.0556), (‘and’, 0), (‘as’, 0.1111), (‘faster’, 0), (‘get’, 0), (‘got’, 0), (‘hairy’, 0.0556), (‘harry’, 0.0556), (‘home’, 0), (‘is’, 0.0556), (‘jill’, 0.0556), (‘not’, 0.0556), (‘store’, 0), (‘than’, 0), (‘the’, 0), (‘to’, 0), (‘would’, 0)])

过程中,首先创建了空向量,然后将计算得到的结果赋值给这个空向量,从而得到一个同一维度根据各文档单词频次数据的向量。从上面可以看出,有些标识符TF为0,因为不同文本含有单词不同。这就是文本向量化及词袋模型构造过程。

TF-IDF与主题模型

上文阐述了文本向量化及词袋模型。文本向量化为了将文本转换成机器学习算法可以直接处理的数字,或者说转换后数字代表了文本的特征(称为特征提取或特征编码),可直接为机器学习模型所用。 词袋模型(BOW)则指统计单词在一个文本中出现次数的表现形式(occurence of words within a specific document)。主要考虑两方面:

  1. 展现文本中出现的已知词汇-词汇量;
  2. 量化单词的存在

之所以叫词袋,是因为忽略了文本本来的有序性和结构性。一般词袋模型主要用来衡量文档相似性,因为两个类似文档含有类似的文本内容。紧接着,就可以用BOW来做进一步分析,如语义分析等。这部分由BOW模型过渡到TF-IDF模型。

1. 齐波夫定律 Zipf’s Law
正式介绍TF-IDF前,先看下什么是齐波夫定律,有助于理解TF-IDF含义,定义:
Zipf’s Law describes that given some corpus of natural language utterances, the frequency of any word is inversely proportional to its rank in the frequency table.
意思是:给定一个文档,任何一个单词出现的频次是与其在频次表上的位置排名成反比。 如一个单词出现在频次表第一位,则它出现的次数基本(非严格意义)是排在第二位单词出现次数的2倍,第三位的3倍,依次类推。并非严格意义,而是说一个文本足够大时情况如此。

2. 主题模型(Topic Modelling)

文本中各个单词出现的频次可提供机器学习一些初步特征,但只知道频次并不能做更多事,也无从得知某个单词对该文档重要性。

这里引入逆文本频率指数(IDF:Inverse Doccument Frequency), 通过了解IDF来引出最终需要的TF-IDF公式和运用。IDF意思是: 若一个单词(文本标识符)出现在一个文档中次数越多,但却很少出现在其他文档中,那就可以假设这个单词在这个特定文本中分量很重要。公式如下:
I D F = t o t a l − n u m b e r − o f − d o c u m e n t s ( 文 本 集 中 含 有 文 本 总 数 ) t h e − n u m b e r − o f − d o c u m e n t s − c o n t a i n i n g − a − t e r m ( 含 有 特 定 单 词 文 本 数 ) IDF=\frac{total-number-of-documents(文本集中含有文本总数)}{the-number-of-documents-containing-a-term(含有特定单词文本数)} IDF=thenumberofdocumentscontainingaterm()totalnumberofdocuments()

例如,设有一文本集,一共含5个不同文本内容,其中3个含China这个单词,那么IDF(‘China’)=5/3=1.67。

但是直接拿IDF衡量一个单词在一个文本中重要性,会碰到一个问题,就是数量级问题。如,有一文本集有100 0000个文档,在其中寻找apple和orange词,已知只有1个文档有apple, 10个文档有orange。那么这两个词的IDF分别为:100 0000和10 0000,可见量级差别太巨大,不适合比较。所以建议引入log()和exp()函数来让单词出现频次和文档频次处于同一水平。这样便于后期计算得到TF-IDF值会是均匀分布(uniformly distributed)。通过引入log, 此时变为:
I D F ( ′ a p p l e ′ ) = l o g ( 1000000 / 1 ) = 6 IDF('apple')=log(100 0000/1)=6 IDFapple=log(1000000/1)=6
I D F ( ′ o r a n g e ′ ) = l o g ( 100000 / 1 ) = 5 IDF ('orange')=log(10 0000/1)=5 IDF(orange)=log(100000/1)=5

综上,假设一个标识符 t t t出现在一个文本集 D D D中特定文档 d d d的频率可以定义为:
T F ( t , d ) = n u m b e r − o f − t − a p p e a r s − i n − d t o t a l − t o k e n s − i n − d TF(t,d)=\frac{number-of-t-appears-in-d}{total-tokens-in-d} TF(t,d)=totaltokensindnumberoftappearsind
I D F ( t , D ) = log ⁡ ( n u m b e r − o f − d o c u m e n t s n u m b e r − o f − d o c u m e n t s − c o n t a i n i n g − t ) IDF(t,D)=\log(\frac{number-of-documents}{number-of-documents-containing-t}) IDF(t,D)=log(numberofdocumentscontainingtnumberofdocuments)
T F − I D F ( t , d , D ) = T F ⋅ I D F TF-IDF(t,d,D)=TF\cdot IDF TFIDF(t,d,D)=TFIDF

由上可以看出,如果一个单词在特定文档中出现次数越多,TF越大,在其他文档中出现越少,则IDF越大,那么TF-IDF权重变大,这也就说明了:
TF-IDF值可以帮助理解一个文档想要表达或帮助理解主题模型,因为按照之前的假设,一个单词出现次数越多越能表达一个文档想要表达意思(去除停顿词)。如下代码举例:

import copy
from nltk.tokenize import TreebankWordTokenizer
from collections import OrderedDict

docs=['The faster Harry got to the store, the faster and faster Harry would get home.']
docs.append('Harry is hairy and faster than Jill.')
docs.append('Jill is not as hairy as Harry')

tokenizer=TreebankWordTokenizer()

doc_tokens=[]

for doc in docs:
    doc_tokens+=[sorted(tokenizer.tokenize(doc.lower()))]
all_doc_tokens=sum(doc_tokens,[])


lexicon=sorted(set(all_doc_tokens))


zero_vectors=OrderedDict((token,0) for token in lexicon)

document_tfidf_vectors=[]

for doc in docs:
    vec=copy.copy(zero_vectors) #[(token1,0),(token2,0),...], 对整个文本集
    tokens=tokenizer.tokenize(doc.lower()) #['token1', 'token2',...], 对某个文本doc
    token_counts=Counter(tokens) #{token1:num1, token2:num2, ...}, 对某个文本
    
    for key,value in token_counts.items():  #对该文本里的token和num
        docs_containing_key=0
        
        tf=value/len(lexicon)  #计算某个key的tf
        
        for _doc in docs:
            if key in _doc:
                docs_containing_key+=1 #计算该key在多少个文档出现过
      
        if docs_containing_key:
            idf=len(docs)/docs_containing_key  #如果不是在0个文档出现过,计算idf
        else:
            idf=0 #如果都没出现过,=0
        vec[key]=round(tf*idf,4)    #更改key的value, 为tf*idf, 生成针对该文本的ordereddict
    document_tfidf_vectors.append(vec) #列表中添加该ordereddict

print(document_tfidf_vectors)

输出

[OrderedDict([(’,’, 0.1667), (’.’, 0.0833), (‘and’, 0.0833), (‘as’, 0), (‘faster’, 0.25), (‘get’, 0.1667), (‘got’, 0.1667), (‘hairy’, 0), (‘harry’, 0.0), (‘home’, 0.1667), (‘is’, 0), (‘jill’, 0), (‘not’, 0), (‘store’, 0.1667), (‘than’, 0), (‘the’, 0.5), (‘to’, 0.1667), (‘would’, 0.1667)]), OrderedDict([(’,’, 0), (’.’, 0.0833), (‘and’, 0.0833), (‘as’, 0), (‘faster’, 0.0833), (‘get’, 0), (‘got’, 0), (‘hairy’, 0.0833), (‘harry’, 0.0), (‘home’, 0), (‘is’, 0.0833), (‘jill’, 0.0), (‘not’, 0), (‘store’, 0), (‘than’, 0.1667), (‘the’, 0), (‘to’, 0), (‘would’, 0)]), OrderedDict([(’,’, 0), (’.’, 0), (‘and’, 0), (‘as’, 0.1111), (‘faster’, 0), (‘get’, 0), (‘got’, 0), (‘hairy’, 0.0833), (‘harry’, 0.0), (‘home’, 0), (‘is’, 0.0833), (‘jill’, 0.0), (‘not’, 0.1667), (‘store’, 0), (‘than’, 0), (‘the’, 0), (‘to’, 0), (‘would’, 0)])]

中文分词

英文语言文字词与词间一般有空格(分隔符),这样分词处理相对容易,但中文没那么容易,因为中文字与字之间,词与词之间紧密连接在一起。所以第一件事就是,如何确认词。

中文文章的最小组成单位是字,但是独立的字并不能很好地传达想要表达整体的意思或者说欠缺表达能力,所以一篇成文的文章依旧是以词为基本单位来形成有意义的篇章,所以词是最小并且能独立活动的语言成分。所以在处理中文文本时,首先将句子转化为特定的词语结构(或称单词)。

规则分词(Rule based Tokenization) 是通过设立词典并不断对词典进行维护以确保分词准确性的分词技术。基于规则分词是一种匹配式分词技术,要进行分词时通过在词典中寻找相应的匹配,找到规则进行切分,否则不切分。传统的规则式分词主要有三种:正向最大匹配法Maximum Match Method, 逆向最大匹配法Reversed Maximum Match Method以及双向最大匹配Bi-direction Matching Method.

正向最大匹配法

基本思想是:假定设定好的词典最长词含有 i i i个汉字字符,那么则用被处理中文文本中当前字符串字段的前 i i i个字作为匹配字段去和词典中的词进行匹配查找,假如词典中含有这样一个 i i i字词则匹配成功,切分该词,反之则匹配失败,这时会去掉该字段中最后一个字符(变为 i − 1 i-1 i1),并对剩下的字段进行匹配处理,循环往复直到所有字段成功匹配,循环终止条件是切分出最后一个词或者剩余匹配的字符串程度为0。如下图示:
在这里插入图片描述
代码举例:

class MaximumMatching(object):
    def __init__(self):
        self.window_size=6 #定义词典中最长单词长度
        
    def tokenize(self,text):
        tokens=[]         #定义空列表保存切分结果
        index=0           #切分
        text_length=len(text)
        
        #定义被维护的词典,其中词典最长词为6
        maintained_dic=['研究','研究生','自然','自然语言','语言','自然语言处理','处理','是','一个','不错','的','科研','方向']
        
        while text_length>index: #循环结束判定条件
            print(text_length)
            print('index:',index)
            for size in range(self.window_size+index,index,-1): #如果index=0,size为 index+6,5,4,3,2,1
                print('此时切分起始index1:',index)
                print('window size:',size)
                piece=text[index:size] #文本的[0:6],[0:5],[0:4],...[0:1],包含index,不包含size
                if piece in maintained_dic: #如果字段在词典中,新的index为新的匹配字段起始位置
                    index=size-1  #下一次应该从size开始,再-1,再+1,刚好是size
                    print('切分位置index2:',index)
                    break       #终止size循环,跳出。到这里的前提是有匹配的。如果都不匹配,不会进入if
                    #如果以上都不匹配,piece就是一个中文字符[0:1]
            index+=1
            print('下一个切分起始index3:',index,'\n')
            tokens.append(piece) #保存匹配的字段
        return tokens
if __name__=='__main__':
    text='研究生啊不对吧研究自然语言处理是一个不错的研究方向'
    tokenizer=MaximumMatching()
    print(tokenizer.tokenize(text))

输出结果:

25
index: 0
此时切分起始index1: 0
window size: 6
此时切分起始index1: 0
window size: 5
此时切分起始index1: 0
window size: 4
此时切分起始index1: 0
window size: 3
切分位置index2: 2
下一个切分起始index3: 3 

25
index: 3
此时切分起始index1: 3
window size: 9
此时切分起始index1: 3
window size: 8
此时切分起始index1: 3
window size: 7
此时切分起始index1: 3
window size: 6
此时切分起始index1: 3
window size: 5
此时切分起始index1: 3
window size: 4
下一个切分起始index3: 4 

25
index: 4
此时切分起始index1: 4
window size: 10
此时切分起始index1: 4
window size: 9
此时切分起始index1: 4
window size: 8
此时切分起始index1: 4
window size: 7
此时切分起始index1: 4
window size: 6
此时切分起始index1: 4
window size: 5
下一个切分起始index3: 5 

25
index: 5
此时切分起始index1: 5
window size: 11
此时切分起始index1: 5
window size: 10
此时切分起始index1: 5
window size: 9
此时切分起始index1: 5
window size: 8
此时切分起始index1: 5
window size: 7
此时切分起始index1: 5
window size: 6
下一个切分起始index3: 6 

25
index: 6
此时切分起始index1: 6
window size: 12
此时切分起始index1: 6
window size: 11
此时切分起始index1: 6
window size: 10
此时切分起始index1: 6
window size: 9
此时切分起始index1: 6
window size: 8
此时切分起始index1: 6
window size: 7
下一个切分起始index3: 7 

25
index: 7
此时切分起始index1: 7
window size: 13
此时切分起始index1: 7
window size: 12
此时切分起始index1: 7
window size: 11
此时切分起始index1: 7
window size: 10
此时切分起始index1: 7
window size: 9
切分位置index2: 8
下一个切分起始index3: 9 

25
index: 9
此时切分起始index1: 9
window size: 15
切分位置index2: 14
下一个切分起始index3: 15 

25
index: 15
此时切分起始index1: 15
window size: 21
此时切分起始index1: 15
window size: 20
此时切分起始index1: 15
window size: 19
此时切分起始index1: 15
window size: 18
此时切分起始index1: 15
window size: 17
此时切分起始index1: 15
window size: 16
切分位置index2: 15
下一个切分起始index3: 16 

25
index: 16
此时切分起始index1: 16
window size: 22
此时切分起始index1: 16
window size: 21
此时切分起始index1: 16
window size: 20
此时切分起始index1: 16
window size: 19
此时切分起始index1: 16
window size: 18
切分位置index2: 17
下一个切分起始index3: 18 

25
index: 18
此时切分起始index1: 18
window size: 24
此时切分起始index1: 18
window size: 23
此时切分起始index1: 18
window size: 22
此时切分起始index1: 18
window size: 21
此时切分起始index1: 18
window size: 20
切分位置index2: 19
下一个切分起始index3: 20 

25
index: 20
此时切分起始index1: 20
window size: 26
此时切分起始index1: 20
window size: 25
此时切分起始index1: 20
window size: 24
此时切分起始index1: 20
window size: 23
此时切分起始index1: 20
window size: 22
此时切分起始index1: 20
window size: 21
切分位置index2: 20
下一个切分起始index3: 21 

25
index: 21
此时切分起始index1: 21
window size: 27
此时切分起始index1: 21
window size: 26
此时切分起始index1: 21
window size: 25
此时切分起始index1: 21
window size: 24
此时切分起始index1: 21
window size: 23
切分位置index2: 22
下一个切分起始index3: 23 

25
index: 23
此时切分起始index1: 23
window size: 29
切分位置index2: 28
下一个切分起始index3: 29 

['研究生', '啊', '不', '对', '吧', '研究', '自然语言处理', '是', '一个', '不错', '的', '研究', '方向']

通过上述结果,可看到,正向最大匹配算法切分结果不错,但并不意味着实际运用会十分精准,主要有以下三个方面:

  1. 新词层出不穷,不断维护词典非常困难,人工维护费时费力;
  2. 执行效率不高。执行算法时,程序为了能找到一个合适的窗口,会循环往复进行下去直到找到一个合适的。假设词典非常大,初始窗口也较大时,那匹配词段寻找的时间和循环次数相应增加,效率下降;
  3. 无法很好解决歧义问题。举例说明,如"南京市长江大桥", 设词典最长词长度5,若用正向最大匹配法,通过对前5个字符匹配,“南京市长江”,没有发现合适的,去掉最后一个字符,变成"南京市长", 进行匹配,结果匹配成功,而剩下的,“江大桥”,继续匹配分为"江"和“大桥”,最后切分为['南京市长‘,‘江‘,‘大桥’],但我们想要的切分结果应该是[‘南京市’, '长江大桥‘]

逆向最大匹配算法

其实现过程与正向最大匹配算法基本一致,唯一区别是分词的切分从后往前,和正向最大匹配方法相反。就是说,逆向是从字符串最后面开始扫描,每次选取最末端的 i i i个汉字字符作为匹配字段,若匹配成功则进行下一字符串匹配,否则移除该词段最前面一个字,继续匹配。

class ReversedMaximumMatching(object):
    
    def __init__(self):
        self.window_size=6
        
    def tokenize(self,text):
        tokens=[]
        index=len(text)
        
        maintained_dic=['研究','研究生','自然','自然语言','语言','自然语言处理','处理','是','一个','不错','的','科研','方向']
        
        while index>0:
            print('本次起始位置Index1:',index)
            for size in range(index-self.window_size, index): # size分别等于index-6,index-5,index-4,...index-1,len-1为最后一个字符
                print('Window Check Point:',size)
                w_piece=text[size:index]
                print('Checked Words:',w_piece)
                
                if w_piece in maintained_dic:
                    index=size+1  #下一次要从size位置开始进行匹配,+1再-1
                    print('本次截止位置Index2:',index)
                    break
            index-=1
            print('下次的开始位置index3:',index,'\n')
            tokens.append(w_piece)
        tokens.reverse()
        return tokens

if __name__=='__main__':
    text='研究生研究自然语言处理是一个不错的研究方向'
    tokenizer=ReversedMaximumMatching()
    print(tokenizer.tokenize(text))

输出:

本次起始位置Index1: 21
Window Check Point: 15
Checked Words: 错的研究方向
Window Check Point: 16
Checked Words: 的研究方向
Window Check Point: 17
Checked Words: 研究方向
Window Check Point: 18
Checked Words: 究方向
Window Check Point: 19
Checked Words: 方向
本次截止位置Index2: 20
下次的开始位置index3: 19 

本次起始位置Index1: 19
Window Check Point: 13
Checked Words: 个不错的研究
Window Check Point: 14
Checked Words: 不错的研究
Window Check Point: 15
Checked Words: 错的研究
Window Check Point: 16
Checked Words: 的研究
Window Check Point: 17
Checked Words: 研究
本次截止位置Index2: 18
下次的开始位置index3: 17 

本次起始位置Index1: 17
Window Check Point: 11
Checked Words: 是一个不错的
Window Check Point: 12
Checked Words: 一个不错的
Window Check Point: 13
Checked Words: 个不错的
Window Check Point: 14
Checked Words: 不错的
Window Check Point: 15
Checked Words: 错的
Window Check Point: 16
Checked Words: 的
本次截止位置Index2: 17
下次的开始位置index3: 16 

本次起始位置Index1: 16
Window Check Point: 10
Checked Words: 理是一个不错
Window Check Point: 11
Checked Words: 是一个不错
Window Check Point: 12
Checked Words: 一个不错
Window Check Point: 13
Checked Words: 个不错
Window Check Point: 14
Checked Words: 不错
本次截止位置Index2: 15
下次的开始位置index3: 14 

本次起始位置Index1: 14
Window Check Point: 8
Checked Words: 言处理是一个
Window Check Point: 9
Checked Words: 处理是一个
Window Check Point: 10
Checked Words: 理是一个
Window Check Point: 11
Checked Words: 是一个
Window Check Point: 12
Checked Words: 一个
本次截止位置Index2: 13
下次的开始位置index3: 12 

本次起始位置Index1: 12
Window Check Point: 6
Checked Words: 然语言处理是
Window Check Point: 7
Checked Words: 语言处理是
Window Check Point: 8
Checked Words: 言处理是
Window Check Point: 9
Checked Words: 处理是
Window Check Point: 10
Checked Words: 理是
Window Check Point: 11
Checked Words: 是
本次截止位置Index2: 12
下次的开始位置index3: 11 

本次起始位置Index1: 11
Window Check Point: 5
Checked Words: 自然语言处理
本次截止位置Index2: 6
下次的开始位置index3: 5 

本次起始位置Index1: 5
Window Check Point: -1
Checked Words: 
Window Check Point: 0
Checked Words: 研究生研究
Window Check Point: 1
Checked Words: 究生研究
Window Check Point: 2
Checked Words: 生研究
Window Check Point: 3
Checked Words: 研究
本次截止位置Index2: 4
下次的开始位置index3: 3 

本次起始位置Index1: 3
Window Check Point: -3
Checked Words: 
Window Check Point: -2
Checked Words: 
Window Check Point: -1
Checked Words: 
Window Check Point: 0
Checked Words: 研究生
本次截止位置Index2: 1
下次的开始位置index3: 0 

['研究生', '研究', '自然语言处理', '是', '一个', '不错', '的', '研究', '方向']

同正向最大匹配算法一样,程序执行效率不高,因为要不断去检测字段,需要维护的词典非常庞大且耗时耗力。

双向最大匹配算法

双向最大匹配算法是在正向和逆向最大匹配算法基础上延伸出来,基本思想很简单,主要以下:

  1. 若正向和反向词语数目不一样,则选择分词数量较少的那组分词结果;

    以“南京市长江大桥”为例,若词典里词有[‘南京市’,‘南京市长’,‘长江大桥’,‘江’,‘大桥’], 则正向切分结果为[‘南京市长’,‘江’,‘大桥’], 逆向结果[‘南京市’,‘长江大桥’],选择逆向,而逆向也是理想结果。

  2. 若分词数量相同,分两种情况考虑:
    (1)分词结果完全一样,不具备歧义,即两个方向都可以;
    (2)如果不一样,选取分词结果中单个汉字数目较少的一组。

    同样以“南京长江大桥”为例,若词典里词有[‘南京市’,‘南京市长’,‘长江‘,‘江’,‘大桥’](和上面比,词典没有完整的‘长江大桥’,而是只有‘长江’和‘大桥’), 则正向切分结果为[‘南京市长’,‘江’,‘大桥’], 逆向结果[‘南京市’,‘长江‘,’大桥’],此时选单个汉字数目较少的,即逆向,结果也比较理想。

下面是代码实现:

class BiDirectionMatching(object):
    
    def __init__(self):
        self.window_size=6
        self.dic=['研究','研究生','生命','命','的','起源','南京市','南京市长','长江大桥','大桥','长江']
        
    def mm_tokenize(self,text):
        tokens=[] #定义空列表保存切分结果
        index=0
        text_length=len(text)
        
        while text_length>index: 
            for size in range(self.window_size+index, index, -1):
                piece=text[index:size]
                if piece in self.dic:
                    index=size-1
                    break
            index+=1
            tokens.append(piece)
        return tokens
    def rmm_tokenize(self,text):
        tokens=[]
        index=len(text)
        
        while index>0:
            for size in range(index-self.window_size,index):
                w_piece=text[size:index]
                if w_piece in self.dic:
                    index=size+1
                    break
            index-=1
            tokens.append(w_piece)
        tokens.reverse()
        return tokens
    def bmm_tokenize(self,text):
        mm_tokens=self.mm_tokenize(text)
        print('正向最大匹配结果:',mm_tokens)
        rmm_tokens=self.rmm_tokenize(text)
        print('逆向最大匹配结果:',rmm_tokens)
        
        if len(mm_tokens) != len(rmm_tokens):
            if len(mm_tokens)>len(rmm_tokens):
                return rmm_tokens
            else:
                return mm_tokens
        elif len(mm_tokens)==len(rmm_tokens):
            if mm_tokens==rmm_tokens:
                return mm_tokens
            else:
                mm_count,rmm_count=0,0
                for mm_tk in mm_tokens:
                    if len(mm_tk)==1:
                        mm_count+=1
                for rmm_tk in rmm_tokens:
                    if len(rmm_tk)==1:
                        rmm_count+=1
                if mm_count>rmm_count:
                    return rmm_tokens
                else: return mm_tokens

if __name__=='__main__':
    text='研究生命的起源,南京市长江大桥'
    tokenizer=BiDirectionMatching()
    print('双向结果:',tokenizer.bmm_tokenize(text))

输出结果:

正向最大匹配结果: ['研究生', '命', '的', '起源', ',', '南京市长', '江', '大桥']
逆向最大匹配结果: ['研究', '生命', '的', '起源', ',', '南京市', '长江大桥']
双向结果: ['研究', '生命', '的', '起源', ',', '南京市', '长江大桥']

综上可得,规则分词的核心是需要一个完整的词典以便尽可能完整地覆盖到可能会被用到或查询的词语,但这是一个庞大的工程。

参考:
(1)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值