如果我们想搜索某一篇文章(text)中相似的词(word),可以使用nltk这个强大的NLP模块。下面以nltk自带的shakespeare数据集来做示例。
第一次使用nltk,需要先运行下面的代码来下载shakespeare数据集。
import nltk
nltk.download('shakespeare')
然后,我们就可以加载shakespeare数据集来做实验了:
import nltk
text = nltk.Text(word.lower() for word in nltk.corpus.shakespeare.words(('hamlet.xml')))
只要使用nltk.Text.similar(word)
,就能找到text中跟某个word相似的其他所有word了,如下示例
text.similar('woman')
我们就能得到与woman
相似的所有词:
matter at eaten but fit this to vulcan by like servant disclose
follows twice laertes it cat such sin
可以看到,这些输出的词中,有的确实与woman
有一点点相似,比如“vulcan”和“servant”。但还有大量的词是与woman
没有任何关系的,比如“twice”。
那问题来了,nltk.Text.similar是根据什么来衡量两个词的相似度呢?
网上并没有太多关于nltk.Text.similar的原理解释,所以只有查阅源码,才能看到细节。
nltk.Text.similar的源码在https://github.com/nltk/nltk/blob/develop/nltk/text.py,其中有一个函数similar(self, word, num=20)
,这就是nltk.Text.similar的实现,核心代码如下:
def similar(self, word, num=20):
"""
Distributional similarity: find other words which appear in the
same contexts as the specified word; list most similar words first.
:param word: The word used to seed the similarity search
:type word: str
:param num: The number of words to generate (default=20)
:type num: int
:seealso: ContextIndex.similar_words()
"""
word = word.lower()# 统一转换为小写进行比较
wci = self._word_context_index._word_to_contexts
if word in wci.conditions():
# 根据上下文context相同来查找其它词
contexts = set(wci[word])
fd = Counter(
w
for w in wci.conditions()
for c in wci[w]
if c in contexts and not w == word
)
# most_common是根据出现次数从到到低来输出
words = [w for w, _ in fd.most_common(num)]
print(tokenwrap(words))
从源码中,我们可以看到, nltk.Text.similar是用Distributional similarity
来衡量两个word是否相似。Distributional similarity
的做法,首先找到与给定词(the specified word)具有相同上下文(same context)的所有词,然后根据这些词的出现次数,按出现次数从高到低依次输出(most similar words first)。
举个例子,假如有如下一篇文档:
C3 W4 C4
C1 W3 C2
C1 W3 C2
C1 W3 C2
C1 W2 C2
C1 W2 C2
C1 W1 C2
C1 W C2
与词W有相同上下文(C1 X C2)的,是词W1,W2,W3。但W3出现了3次,W2出现了2次,所以W3先输出,W2后输出。
通过阅读nltk.Text.similar源码,我们理解了它判断两个word是否相似,是根据上下文来判断,而非我们人理解的相似度。因为nltk.Text.similar根据上下文,只能找到相似,或不相似的词,所以也就无法做量化的相似度衡量(比如W1与W2相似度为0.8)。
我们再来人为构建几个例子,通过实例深入理解nltk.Text.similar。
通过如下代码,我们可以找到语料s中与boy相似的其它词。
import nltk
s = '''A B C boy D E F G
A B C dog D E F G
A B C cat D E F G
A A A man B B B B
'''
tokens = nltk.word_tokenize(s)
text = nltk.Text(tokens)
text.similar('boy')
可以得到结果为“cat dog”,这个容易理解,应为“cat”和“dog”的上下文(A B C X D E F G)与“boy”相同。预料中的“man”虽然逻辑上与“boy”应该更具有相似性,但应为nltk.Text.similar是根据上下文来判断相似性,而不是根据逻辑来判断相似性,所以输出结果中没有“man”。
根据源码中similar()的定义similar(self, word, num=20)
,我们还发现similar()的另一个参数num
,它默认是20,指的是输出20个与给定词相似的词,我们将本例中的代码更改为num=1,即text.similar('boy',1)
,则输出只有1个词“cat”。
本例中,boy的上下文为"A B C" 与 “D E F G”,那similar()在进行相似度搜索是,是完全根据"A B C" 与 "D E F G"都相同才算相似词吗?还是上下文有一个搜索长度的限制,比如只考虑boy之前的3个单词和与之后的3个单词?我发现源码中并无类似的参数,所以设计了如下实验来进行验证。
下面是改动了各个词相似的上下文(单词)长度,用来测试上下文长度的例子(查找语料s中与boy相似的词):
import nltk
s = '''A B C boy D E F G
C dog D
A B B cat D E F G
A A c man d B B B
'''
tokens = nltk.word_tokenize(s)
text = nltk.Text(tokens)
text.similar('boy')
代码的输出是“dog man”,所以我们得到两个结论
上下文,只考虑待搜索词的前一个词和后一个词,即boy(上下文为C X D)与dog(上下文为C X D)相似
上下文搜索时,不考虑大小写,即boy(上下文为C X D)与man(上下文为c X d)相似