语言模型:使用NLTK训练并计算困惑度和文本熵
Author: Sixing Yan
这一部分主要记录我在阅读NLTK的两种语言模型源码时,一些遇到的问题和理解。
1. NLTK中训练语言模型MLE和Lidstone有什么不同
NLTK 中两种准备ngram
语言模型的方式:最大似然估计MLE
和平滑Lidstone
。两者的实现方式都是基于统计信息,而没有进行又梯度更新的估计。
(1)使用语言模型计算困惑度或文本熵
使用NLTK构建了一个语言模型后,一般用于计算给定输入的困惑度 perplexity 或者文本熵 entropy 。这两个打分函数都调用了它们(MLE
和Lidstone
)父类(LanguageModel
)的score
函数。
外部调用score
的时候,其实是通过各自的unmasked_score
函数计算的,MLE
和Lid
有不同的unmasked_score
函数
def score(self, word, context=None):
"""Masks out of vocab (OOV) words and computes their model score.
For model-specific logic of calculating scores, see the `unmasked_score`
method.
"""
return self.unmasked_score(
self.vocab.lookup(word), self.vocab.lookup(context) if context else None
)
(2)lm.MLE
vs. lm.Lidstone
的区别和联系
最大似然估计的核心方法
class MLE(LanguageModel):
"""Class for providing MLE ngram model scores.
Inherits initialization from BaseNgramModel.
"""
def unmasked_score(self, word, context=None):
"""Returns the MLE score for a word given a context.
Args:
- word is expcected to be a string
- context is expected to be something reasonably convertible to a tuple
"""
return self.context_counts(context).freq(word)
加k
平滑的核心方法
def unmasked_score(self, word, context=None):
"""Add-one smoothing: Lidstone or Laplace.
To see what kind, look at `gamma` attribute on the class.
"""
counts = self.context_counts(context)
word_count = counts[word]
norm_count = counts.N()
return (word_count + self.gamma) / (norm_count + len(self.vocab) * self.gamma)
这两个模型都是用了LanguageModel.context_counts()
函数。
(3)现在探究一下两者unmasked_score
函数都调用的context_counts
函数。
def context_counts(self, context):
"""Helper method for retrieving counts for a given context.
Assumes context has been checked and oov words in it masked.
:type context: tuple(str) or None
"""
return (
self.counts[len(context) + 1][context] if context else self.counts.unigrams
)
如果没指定上下文的时候,这个函数返回的是self.counts.unigrams
对象
那么再看self.counts
是什么:默认情况下,是一个NgramCounter()
对象
self.counts = NgramCounter() if counter is None else counter
(4)所以我希望知道self.counts.unigrams.freq(word)
计算的是什么
self._counts[1] = self.unigrams = FreqDist()
可知,FreqDist().freq(word)
对应是word
的频率
word_count = counts[word]
的作用也是调取word
的频率,所以和FreqDist().freq(word)
的作用一样。
(5)结论
lm.MLE
是没加平滑的 lm.Lidstone
方法
2. 另一个问题:如何使用context
这个功能呢
从context_counts()
可知有context
的时候会返回 self.counts[len(context) + 1][context]
。
然后看self.counts = NgramCounter()
的[len(context) + 1][context]
调用,它会使用self._counts
这个成员:
def __getitem__(self, item):
"""User-friendly access to ngram counts."""
if isinstance(item, int):
return self._counts[item]
elif isinstance(item, string_types):
return self._counts.__getitem__(1)[item]
elif isinstance(item, Sequence):
return self._counts.__getitem__(len(item) + 1)[tuple(item)]
self._counts = defaultdict(ConditionalFreqDist)
由此初始化,由第7行[len(word)][word]
取到同长度下的该word
的频数。
而从counts = self.context_counts(context);word_count = counts[word]
这部分看,它返回一个Key-value型的数据类型,类似于如果context=(word1, word2);len(content)=2
,那么应该返回trigrams
的类实例(这里用了len(item)+1
做索引)。于是乎就是考虑了前面两个词,然后计量(word1, word2, word)
这个组合。
(1)结论
使用context
不用改变原有类的初始化,直接使用即可。
NgramCounter
会根据输入的List[Tuple]
的tuple
长度len(tuple)
来设置ngram_order
的值(具体可看update()
函数)。所以如果fit( )
的时候用的是ngrams(2)
的列表,那么context
输入的长度就必须是1。
问题是,self.context_counts(context)
返回int
值,它是怎么可以使用word_count = counts[word]
这种调用呢?因为在计算 entropy 的时候,它会放入context
内容。
具体实现细节,参考
http://www.nltk.org/_modules/nltk/lm/models.html#Lidstone
http://www.nltk.org/_modules/nltk/lm/api.html#LanguageModel
http://www.nltk.org/_modules/nltk/lm/counter.html#NgramCounter
http://www.nltk.org/_modules/nltk/probability.html#FreqDist.freq