面向特定问题的开源算法管理和推荐(六) | 2021SC@SDUSC

2021SC@SDUSC

面向特定问题的开源算法管理和推荐

一、本文分析目标

上一篇博客我们分析了创建ELSKE模型的主要函数调用,分别负责读取文件信息FromFile(),然后提取关键词ExtractPhrases(),我们认识到这两个函数的实现机制是在之前定义的变量的基础上,调用其他类的其他函数实现更复杂的操作。这一篇内容,我们来分析其部分细节。

二、BinaryDocumentIndex的基本实现

1. 基本的声明

我们首先关注BinaryDocumentIndex.cs这个文件的实现情况,其文件主要在ElskeLib的Utils模块下。根据模块的注释,这个类是用于快速得出>= 2个词语的词组的文档频率的。它的是一个基于整数的文档集合,并有着基于bigram的索引。

关注这个类的定义部分,主要有下面变量:

1.一个字典_dictionary,前一项是WordIdxBigram,后一项是 List,用于表示某一个词项出现在文档的具体位置。

2.一个文档列表,它的每一项都是一个int[]数组,代表一个文档,整个列表组成一个文档集合。

3.一个_stopWords停用词集合,其中每一个元素都是一个整数型的索引。这个变量是由HashSet实现的。通过查询资料,我们可以发现其实HashSet是一个容器,它就代表一个集合,并其中不能有重复的元素。其集合内的元素没有顺序,并且有一组高性能的操作。

4.接下来定义一个列表Documents其中的每一个元素是一个整数型的数组,它的类型是IReadOnlyList,通过查资料,我了解到这个类主要功能是使得一个列表是只读的,不可以进行修改。我们考虑到它实现的基础是_documents本身生成Documents,因此这里初始化使用了一个lambda表达式,使用了=>语法。

5.最后将_stopWords封装成一个只读的集合StopWords,具体的方式和上面的Documents类似。

变量名称具体类型
_dictionaryDictionary<WordIdxBigram, List>
_documentsList<int[]>
_stopWordsHashSet
DocumentsIReadOnlyList<int[]>
StopWordsIReadOnlyCollection
// 为了加速提取速度需要使用的结构与数据
private readonly Dictionary<WordIdxBigram, List<SequencePosition>> _dictionary =
            new Dictionary<WordIdxBigram, List<SequencePosition>>();

private readonly List<int[]> _documents = new List<int[]>();
private readonly HashSet<int> _stopWords;

public IReadOnlyList<int[]> Documents => _documents;

public IReadOnlyCollection<int> StopWords => _stopWords;

2.初始化

首先是这个类的构造函数,由于这个类完成一个基于整数的文档集合的转化。因此它需要知道哪些词语不可以转化。这样的一些词是停用词。我们将这些词进行作为唯一的参数输入,并将其赋值给_stopWords就完成了初始化工作。

public BigramDocumentIndex(HashSet<int>? stopWords)
{
_stopWords = stopWords;
}

还有另一类初始化函数,它除了输入停用词之外,还会继续输入文档集合以及字典。

private BigramDocumentIndex(HashSet<int> stopWords, List<int[]> documents,
            Dictionary<WordIdxBigram, List<SequencePosition>> dict) : this(stopWords)
{
_documents = documents;
_dictionary = dict;
}

3. 添加文档和获得文档频率

由于输入输出文件的函数FromFile和ToFile涉及到的是一系列与IO有关的操作,因此我们不再过多去分析他们,而是转向分析其他的函数。

我们的目标是分析AddDocument和GetDocumentFrequency这两个函数。

    1. 前者AddDocument完成将一个新文档加入到索引集合的操作。
    1. 后者GetDocumentFrequency完成的工作是给定一个词组(根据这个类的具体作用,这个词组至少是Bigram),他会对应一个编号,我们要找出其出现过的文档总数,也就是计算文档频率。下面两种情况会返回0:
  • a) 如果给定的词组只包含停用词表StopWords中的条目;
  • b) 如果所含的词数小于2。

4. AddDocument函数

我们先来考察如何添加新的文档用以了解文档的结构和组织。函数头如下:

public void AddDocument(int[] doc)
  • 首先判断文档是否等于null,若是则抛出异常,否则进行下一步;
  • 我们用documentIndex来表示文档的索引号;
  • 在_documents集合中添加文档doc;
  • 对文档的每一个词语进行遍历,每次遍历都创建一个新的bigram,然后考虑这个bigram是否可以被加入文档集合的这个文档中,作为一个指标。

考虑的方法是:

  • 若_stopWords不为null,并且_stopWords中同时存在bigram的两个组成词,就不将它放入文档集合,而是继续判断下一个bigram。

  • 否则就在_dictionary中添加这个词项,前一项为(doc[i - 1], doc[i])表示bigram,后一项为SequencePosition,用于表示该项bigram所在的文档位置documentIndex以及文档中出现的位置i-1。

if (doc == null)
throw new ArgumentNullException(nameof(doc));

var documentIndex = _documents.Count;
_documents.Add(doc);

for (int i = 1; i < doc.Length; i++)
{
var k = new WordIdxBigram(doc[i - 1], doc[i]);
if(_stopWords != null && _stopWords.Contains(k.Idx1) && _stopWords.Contains(k.Idx2))
continue;
                
_dictionary.AddToList(k, new SequencePosition(documentIndex, i-1));
}

通过分析这个类我们了解到,新添加的文档无非就是想要改变_documents和字典_dictionary,为了简化计算,我们把那些不合规定的bigram直接略去了。

5. GetDocumentFrequency函数

我们接下来继续考察如何根据已添加的文档计算给定的长词组(至少大于等于2)文档频率。这一步至关重要,涉及到了模型的核心部分。

函数输入给定的长词组,它是一个只读的span,其名称为tokens,类型为int,可以查询资料了解到span对于处理数组的某一段十分高效。

  • 先定义一个bigram的位置对象SequencePosition的列表bestList,将其初始化为null;
  • 然后定义tokenOffset初始化为0;
  • 对于每一个bigram,每次找出其在字典中出现的位置列表,然后考察列表长度,找出最小列表长度以及其对应的bigram。
  • 返回0,代表这个词组中确是存在匹配不上的bigram序列。
  • 返回docHashSet.Count,代表通过逐一考察最小词频对应的bigram,考察其位置是否有输入的词组,选出有完整词组的文档序号,计算其数量并成功返回。
public int GetDocumentFrequency(ReadOnlySpan<int> tokens)
{
List<SequencePosition> bestList = null;
            
var tokenOffset = 0;

for (int i = 1; i < tokens.Length; i++)
{
	var k = new WordIdxBigram(tokens[i - 1], tokens[i]);
	if (_dictionary.TryGetValue(k, out var list))
	{
		if (bestList == null || bestList.Count > list.Count)
		{
			bestList = list;
			tokenOffset = i - 1;
		}
	 }
	else
	{
		if (StopWords != null && (!_stopWords.Contains(k.Idx1) || !_stopWords.Contains(k.Idx2)))
			return 0; //we know already that there cannot be a match
	}

}

if (bestList == null || bestList.Count == 0)
	return 0;

var docHashSet = new HashSet<int>();

            

for (int i = 0; i < bestList.Count; i++)
{
var pos = bestList[i];
	if(docHashSet.Contains(pos.DocumentIndex))
    		continue; //we already know that this document contains the sequence

	var doc = _documents[pos.DocumentIndex];
	var startPosInDoc = pos.Position - tokenOffset;
	var endPosInDoc = startPosInDoc + tokens.Length;
	if(startPosInDoc < 0 || endPosInDoc > doc.Length)
		continue;

	if (tokens.SequenceEqual(new ReadOnlySpan<int>(doc, startPosInDoc, tokens.Length)))
                    docHashSet.Add(pos.DocumentIndex);
}

return docHashSet.Count;
}

三、SequencePosition结构

本文件中还定义了一个十分有用的结构体SequencePosition,这个结构体用于表示某个Bigram出现的具体位置,包含文档号和位置序号。

public readonly struct SequencePosition
{
        public SequencePosition(int documentIndex, int position)
        {
            DocumentIndex = documentIndex;
            Position = position;
        }

        public int DocumentIndex { get; }
        public int Position { get; }
}

四、总结

这个为难主要目的是,将文档组合成一个特殊的结构,其中以bigram为基础,并提前去除了停用词,每当有新的词进入的时候,就可以将其的文档频率输出。阅读这个文件,我发现它实质上就是实现了前文论文分析中的提取3,4,5…元词组一步。通过特殊的结构加速计算结果。

主要在寻找候选词的时候考虑到了如下规律:2元词组的频率 f s ( p j ) f_{s}(p^{j}) fs(pj)是任何包含这个词组的父词组的最大频率。

下文,我们将继续先分析其他的类,并最终进入到提取关键词的整体算法分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值