2021SC@SDUSC
核心内容
一、本文分析目标
前面我们分析了BinaryDocumentIndex类的基本实现,其中以bigram为基础,并提前去除了停用词,每当有新的词进入的时候,就可以将其的文档频率输出。我还分析了源码中为了表征某个bigram的具体位置而创建的结构体:SequencePosition,了解了其组成原理,以及使用方法。
本文我将进一步分析,以便为最终的提取算法作准备。我们将先考察前面BinaryDocumentIndex类中遗留下来的问题:WordIdxBigram类的具体实现,然后再考察,其余部分。
二、WordIdxBigram的原理
这个类位于ElskeLib的Model模块下,说明他是模型的主要组成部分。它的声明也是为了提高效率,而其提高效率的机理就是通过快得比较操作。通过这个类,我们能够将它以两种不同的方式看待:
- 1.看作一个long;
- 2.看作两个独立的integer.
类的定义过程中用到了一个[FieldOffset(0)],其作用应该是自定义结构在内存中布局方式。紧接着定义了combine和idx1以及idx2。
[FieldOffset(0)]
private readonly long Combined;
[FieldOffset(0)]
public readonly int Idx1;
[FieldOffset(4)]
public readonly int Idx2;
这样定义的意义在于,每次输入两个整数参数,大小各为4字节,第一个combine变量是这两个整数变量的直接组合,大小为二者之和,刚好为long类型的大小8字节,因此我们将combine位置设置为和Idx1一样的位置0,而Idx2设置为4字节处“[FieldOffset(4)]”。
通过比较输入的另一个WordIdxBigram对象的combine属性,我们就可以将二者进行比较;假如输入的对象不是WordIdxBigram的,则返回false。
public override bool Equals(object obj)
{
if (obj is WordIdxBigram p)
return Equals(p);
return false;
}
public bool Equals(WordIdxBigram other)
{
return Combined == other.Combined;
}
接下来重定义==和!=运算符,以及输出方法。
public static bool operator ==(WordIdxBigram pair1, WordIdxBigram pair2)
{
return pair1.Equals(pair2);
}
public static bool operator !=(WordIdxBigram pair1, WordIdxBigram pair2)
{
return !pair1.Equals(pair2);
}
public override string ToString()
{
return $"{Idx1}|{Idx2}";
}
最后是一个GetHashCode()函数,其主要目的是将这个数值映射到一个hash表上,它更加复杂,但是效率更高,并且能减少碰撞数量。
public override int GetHashCode()
{
unchecked
{
return(int) ((((2166136261u ^ (uint)Idx1) * 16777619u) ^ (uint)Idx2) ); //return (Idx1 * 397) ^ Idx2;
}
}
三、WordIdxMap的实现与与原理
1. 概述
接下来我们考察如何实现WordIdxMap。这个类也是位于ElskeLib的Model模块下的。根据注释信息我们不难知道,这个类将独立的词语映射成他们独特的数值表达。其中包含一些方法,可以将文档进行分词,然后将其的每一个词映射成为数值。
2.基本数据定义
我们仍然从WordIdxMap的基本数据定义开始阅读。
首先是一个只读的MemoryComparer,其类型相对复杂,是CharRoMemoryContentEqualityComparer,我们可以在文件的末尾找到这个类,它实现了一个比较两个类是否相等的IEqualityComparer接口,其具体比较的是一个只读的内存区域ReadOnlyMemory,并且其区域中存储的数据为char。因此,这个MemoryComparer,可以简单地理解为用于比较的一个对象。
private static readonly CharRoMemoryContentEqualityComparer MemoryComparer = new();
随后是一个分词设置,其所对应的类TokenizationSettings也在文件末尾有相应的定义。包含以下内容,其中每一项为一个布尔型变量:
bool型变量 | 设置内容 |
---|---|
ConvertToLowercase | 是否都转化为小写字母 |
RetainPunctuationCharacters | 保留停顿符号比如句号,他们会被额外分出 |
HtmlDecode | 对HTML-specific 编码例如 &进行解码 |
TwitterRemoveRetweetInfo | Twitter去除转发信息 |
TwitterRemoveHashtags | Twitter去除标签,以#开头 |
TwitterStripHashtagsinx | Twitter去除标签中的# |
TwitterRemoveUserMentions | Twitter去除@user项 |
TwitterRemoveUrls | Twitter去除http://… 和 https://… |
可以看到有很大一部分内容是关于特定领域的分词细节。在这里尤其表现为对Twitter的分词设置。
/// <summary>
/// Settings that configure how documents will be tokenized. Should not be changed
/// after the map has been populated to avoid inconsistencies.
/// </summary>
public TokenizationSettings TokenizationSettings { get; set; } = new();
接下来是一个字典_wordToIdx,它是这个类的核心部分,将一个个词项转化成对应的数值项。_idxToWord是一个列表,将index映射到word上。最后再设置一把自旋锁:_spinLock。
Count表示具体的分词数量(互不相同的词语)
private readonly Dictionary<ReadOnlyMemory<char>, int> _wordToIdx = new(MemoryComparer);
private readonly List<ReadOnlyMemory<char>> _idxToWord = new();
private SpinLock _spinLock = new();
/// <summary>
/// Number of tokens (= words) in this map.
/// </summary>
public int Count => _idxToWord.Count;
3. TokenizeDocument函数
这个函数主要作用是根据之前的TokenizationSettings中的具体参数,将给定的文档分词。可以看到这个函数中调用了大量的Tokenizer文件中的函数。
if (TokenizationSettings.HtmlDecode)
document = WebUtility.HtmlDecode(document);
if (TokenizationSettings.ConvertToLowercase)
document = document.ToLowerInvariant();
var words = document.SplitSpaces();
if (TokenizationSettings.TwitterRemoveHashtags
|| TokenizationSettings.TwitterRemoveRetweetInfo
|| TokenizationSettings.TwitterRemoveUrls
|| TokenizationSettings.TwitterRemoveUserMentions
|| TokenizationSettings.TwitterStripHashtags)
words = words.CleanTweets(TokenizationSettings.TwitterRemoveHashtags,
TokenizationSettings.TwitterRemoveUserMentions,
TokenizationSettings.TwitterRemoveUrls, TokenizationSettings.TwitterRemoveRetweetInfo,
TokenizationSettings.TwitterStripHashtags);
words = words.Tokenize();
if (!TokenizationSettings.RetainPunctuationCharacters)
words = words.RemovePunctuationChars();
return words;
下表分别说明了每个调用的具体作用,我们暂时将不再对其内部结构进行细致分析。
函数句柄 | 具体作用 |
---|---|
SplitSpaces | 按照空格分词 |
CleanTweets | 按照参数清理推文 |
Tokenize | 分词 |
RemovePunctuationChars | 去除标点符号 |
四、总结
本文主要分析了WordIdxBigram以及WordIdxMap两个类,其中第二个还有部分函数没有完全分析。我们通过读源码可以发现,WordIdxMap中调用到了Tokenizer文件中的部分函数,而由于Tokenizer涉及到底层的字符表示等内容,我们在此不进行更多的分析。
后文,我们将分析WordIdxMap的更多函数实现,包括DocumentToIndexes等。具体细节将在后续内容中展开。