2021SC@SDUSC
核心内容
一、本文分析目标
前面我分析了ExtractPhrases实现中的前半部分,主要涉及到以下几个方面的工作:
- 使用PF-TIDF的策略转化TF到一个双精度浮点型数值;
- 将单词长度小于MinNumCharacters的单词加入tooShortWords中;
现在我们从文件第625行到1164行开始分析,先梳理整体结构,再深入到具体的细节实现。
二、ExtractPhrases的整体流程(上)
476-510行:寻找共同出现最大值
对于某一个词word,可以得到与它临近的所有词(可以是左临近;也可以是右临近)在一起出现的频率,在这其中找到最大的。这样做的目的,按照注释中的说法是:可以在要提取长的词组的情况下,更好地决定一个词/词组的最小频率。
511-536行:单个Term按规则加入表格
对每一个单词word(不是停用词或者前面找出的“过短”的词),我们可以计算得到它的改进版TF-IDF值。我们考察这个词是否是很大一部分都和另一个词结合在一起(也就说明这个词语往往只存在固定地搭配),我们不要这种词,把剩下的加入到一个新的词项的tfidf表格中(用列表表示)。
537-595行:单个phrase按阈值和其他规则加入表格
为了加快提取速度,我们应该去除一些出现频率少的词组,也就是不常用的哪些词组。每个词组pair都可以算出对应的TF-IDF,但却不将所有这些词的TF-IDF加入到表格中,而是考察哪些词组是可能连接在一起的。在此之前,我们还需要根据刚才的TFIDF表格计算一个阈值,然后只添加比这个阈值大,且不容易连接在一起的词组的TFIDF。
三、ExtractPhrases的实现细节(上)
476-510行:寻找共同出现最大值
由于我们需要对于一个词,找到与它临近的所有词(可以是左临近;也可以是右临近)在一起出现的频率,在这其中取最大的。
我们可以先用一个字典来存储上述内容
(wordToHighestBigramCount)。
首先,需要对每一个词组进行遍历,在对一个词组遍历之前,我们都需要知道这个词组中包不包含停用词(stopwords),假如包含,我们就希望它能不被算入到最终结果中(也就意味着不考虑停用词的临近词)。
假如说存在停用词在一个词组中,那么这个词组将会被跳过。
if (_stopWordsSet.Contains(p.Key.Idx1) || _stopWordsSet.Contains(p.Key.Idx2))
continue;
否则我们就需要获取左边(或者右边)的词在wordToHighestBigramCount中的计数值。
我们要确保每次合法的词组频率永远要小于左右词语在wordToHighestBigramCount字典中所对应的频率值。
这样就能确保wordToHighestBigramCount最终能够得到最大的取值。
if (wordToHighestBigramCount.TryGetValue(p.Key.Idx1, out var count))
{
if (count < p.Value)
wordToHighestBigramCount[p.Key.Idx1] = p.Value;
}
else
{
wordToHighestBigramCount.Add(p.Key.Idx1, p.Value);
}
if (wordToHighestBigramCount.TryGetValue(p.Key.Idx2, out count))
{
if (count < p.Value)
wordToHighestBigramCount[p.Key.Idx2] = p.Value;
}
else
{
wordToHighestBigramCount.Add(p.Key.Idx2, p.Value);
}
511-536行:单个Term按规则加入表格
实际源码中,我们的目标主要有两个:
1)一个是生成一个“单词(索引)-TF·IDF表格”(用字典wordTfIdfs表示)
2)另一个目标是生成一个同样结构的字典,只是它的名字叫做initialTfIdfList。
并不是所有的TotalCounts中的单词都能进入这两个字典。
要进入第一个字典,首先这个词
1.不能是停用词;
2.这个词不能过短;
在源码中体现为这些行:
if (_stopWordsSet.Contains(p.Key) || MinNumCharacters > 1 && tooShortWords.Contains(p.Key))
continue;
要进入第二个词典,这些词不可以有很大一部分出现时都和另一个固定的词结合在一起。下面源码用bigramCount >= p.Value / 2完成了这一条件约束。
if (wordToHighestBigramCount != null &&
wordToHighestBigramCount.TryGetValue(p.Key, out var bigramCount))
{
if (bigramCount >= p.Value / 2)
{
//term appears in majority of cases with another term
//do not use count for determining threshold
continue;
}
}
在能够通过第一次约束之后,我们需要先计算得到它的改进版TF-IDF值,然后在每一步,将其索引和对应的TFIDF加入到字典里。
var tfidf = TransformTf(p.Value) * GetIdf(p.Key);
537-595行:单个phrase按阈值和其他规则加入表格
根据我们的目标,我们需要让最后的结果尽量地高效。我们通过阈值来滤去一些不太常见的词组。阈值的设置需要根据之前我们得出的initialTfIdfList以及我们期望提取的Top K的值。取到第K个TFIDF,我们就能保证后续过程中,一定可以在候选词中选出新的Top K,并且不需要处理多余的信息。
var uniTfidfTh =
initialTfIdfList.Count < numTopPhrases ? 0 : initialTfIdfList[^numTopPhrases];
var uniMinTransTf = uniTfidfTh / maxIdf;
var uniMinTfTh = Math.Max(1, (int)(useRootExp ? Math.Pow(uniMinTransTf, powExp) : uniMinTransTf));
随后我们要计算包含的StopWord的个数,然后去除下面的词组:
- 并去除两个词项全部是停用词;
- 或者只有一个词是停用词,而这个词组出现的频率足够小(小于2);
- 还要去除过于短小的词
var numStopwords = _stopWordsSet.Contains(p.Key.Idx1) ? 1 : 0;
if (_stopWordsSet.Contains(p.Key.Idx2))
numStopwords++;
if (numStopwords == 2)
continue;
if (numStopwords == 1 && p.Value <= 2)
continue;
if (MinNumCharacters > 1 &&
tooShortWords.Contains(p.Key.Idx1) &&
tooShortWords.Contains(p.Key.Idx2))
continue; //at least one word has to meet min no chars requirement
我们不仅希望p.Value >= uniMinTfTh还希望,tfIdf >= uniTfidfTh,不满足这两个条件的将被排除。如果没有被前面条件过滤,则将其加入pairTfIdfs。
最后加入initialTfIdfList之前我们还需完成两道过滤:
- 一个是numStopwords >= 1;
- 另一个是单个词常常和某个词一起出现时。
var tfIdf = TransformTf(p.Value) * GetIdf(p.Key);
if (tfIdf < uniTfidfTh)
continue;
pairTfIdfs.Add((p.Key, tfIdf));
if (numStopwords >= 1)
continue;
if ((localCounts.TotalCounts.WordCounts[p.Key.Idx1] <= 2 * p.Value
|| localCounts.TotalCounts.WordCounts[p.Key.Idx2] <= 2 * p.Value))
{
//we ignore this tfidf value for determining the threshold as it could be merged at a later stage
continue;
}
initialTfIdfList.Add(tfIdf);
四、总结
本文主要分析了函数476到595行的内容,主要到了前面TFIDF表的初始化工作,并设置了相应的阈值。实际的代码实现远远要比论文中更复杂。分析耗时耗力,但是收获颇多。