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

2021SC@SDUSC

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

一、本文分析目标

前面我们分析了一个用于高效比较bigram的类WordIdxBigram,并分析了一个对文档进行分词,以及按照一系列设置的参数进行去除标点、去除Twitter标签、转发信息等操作的类WordIdxMap。关于WordIdxMap类,我们了解到他会调用Tokenizer文件的函数,并进行一些高效的底层的词项提取操作。

一方面,我们对于Tokenizer文件中返回的IEnumerable<ReadOnlyMemory>理解并不是很深刻,另一方面WordIdxMap类还有更多有用的函数可以被使用,我们也没有提到。

因此,本文主要针对上述遗漏问题进行补充,以及进一步深入理解。

二、TokensToIndexes的过程

在WordIdxMap类中还有一个函数负责将文档直接转化为索引,它的实现分为两步:

public int[] DocumentToIndexes(string document)
{
return TokensToIndexes(TokenizeDocument(document));
}

第一步先调用之前分析过的TokenizeDocument函数,将一个String类型的文本按照设置划分为一系列的分词,这个分词结果是以IEnumerable<ReadOnlyMemory>的形式返回,其中ReadOnlyMemory表示的是一个词项,这个词项存储方式不是一个局部变量String,而是在一个只读内存区域以char数组形式存储。每个词项都是一个 ReadOnlyMemory,而分词后词项的集合就是IEnumerable类来组织的。IEnumerable是C#中已经定义的类,不需要我们来实现,其具体的更深一层的含义我还不完全了解,相信之后的分析中会涉及到。

第二步是用TokensToIndexes函数计算出映射后的结果,传入的参数是上一步的结果。而这个函数也是我们需要重点分析的函数。

下面是这个函数的实现过程:

  • 首先给定一个list对象(前4行不能完全理解);
  • 然后调用GetIndex,返回转化后的数值,并默认情况下同时更新映射字典;
  • 最后将list转化成数组,也就是这个文档对应的数组形式。
public int[] TokensToIndexes(IEnumerable<string> tokens)
{
if (_intListBag.TryTake(out var l))
l.Clear();
else
l = new List<int>();
foreach (var w in tokens)
{
l.Add(GetIndex(w));
}
var res = l.ToArray();
l.Clear();
_intListBag.Add(l);
return res;
}

实际上,当我们仔细研究这个类,我们发现真正的TokensToIndexes函数其实一共有四个:

  • 1.一个是传入IEnumerable;
  • 2.而另一个则是传入IEnumerable<ReadOnlyMemory>;
  • 3.传入IList
  • 4.传入IList<ReadOnlyMemory>

第二个能够被之前的结构直接调用,因为它符合这个模型的默认使用策略。

三、GetIndex函数

简单地观察我们可以发现,其中调用了一个GetIndex函数,这个函数输入一个ReadOnlyMemory型的token;而第二个参数确是一个可有可无的布尔变量addIfNew,将它初始化为True,用它来表示这样一个操作需要被执行:“假如token是新的,则要将其装入到map (default)当中”,因此默认情况下,map (default)是会随着新的token而被不断更新的。

public int GetIndex(ReadOnlyMemory<char> token, bool addIfNew = true)

这个函数的具体完成的功能是:将一个单词转化成其整数型的表示方法,并返回转化后的索引(新词可能为-1也可能为新的索引,旧词就是旧的索引)。

    1. 如果addIfNew为True,则将新词加入到map中,并返回其新的索引;
    1. 否则一旦token无法在字典_wordToIdx中找到,则返回-1

可以看到其中问号表达式左边调用了GetIndexInternal函数,并且在进入调用之前设置了一把旋转锁,使得访问之间可以做到互斥。

问号表达式右边调用了GetValueOrDefault,通过查阅资料,我了解到,这个函数旨在考察是否在字典中存在某一项,如果存在就返回位置,若不存在就返回后面的内容(这里为-1)

bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
return addIfNew? GetIndexInternal(token): 
_wordToIdx.GetValueOrDefault(token, -1);
}
finally
{
if (lockTaken)
_spinLock.Exit();
}

GetIndexInternal可以在字典_wordToIdx中查找,并相应修改map的操作

    1. 如果找到,则返回这个词项对应的id是多少;
    1. 假如找不到就需要:
  • 1)修改_wordToId(即新增字典项目);
  • 2)并且修改_idxToWord(将词列表新增数据)
private int GetIndexInternal(ReadOnlyMemory<char> token)
{
if (_wordToIdx.TryGetValue(token, out var idx)) return idx;

  	idx = _idxToWord.Count;
  	_wordToIdx.Add(token, idx);
	_idxToWord.Add(token);

	return idx;
}

四、获取token

文件中也对获取特定token的需求设定了函数。

public ReadOnlyMemory<char> GetTokenAsMemory(int index)

函数传入的是具体token的具体数值索引,而输出的是token的具体词形式,由于也需要互斥访问,因此在访问之前先要加上锁。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlyMemory<char> GetTokenAsMemory(int index)
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
return _idxToWord[index];
	}
	finally
	{
		if (lockTaken)
			spinLock.Exit();
	}
}

如果我们想要获取token所对应的string值,我们并不用调用上面的函数再进行转化,而是可以直接调用GetToken(int index)函数。

五、数值索引列表的逆向转化

这个转化所用到的主要的函数是IndexListToString。这个函数用一个StringBuilder将很多索引数值转化后的String连在一起。

主要步骤如下:

  • 1.定义一个String生成器;
  • 2.进入锁
  • 3.对每一个序号找到其对应的String,然后使用Append将其连接;
  • 4.将最终结果转化为字符串
public string IndexListToString(ReadOnlySpan<int> indexes)
{
var sb = new StringBuilder();
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
for (int i = 0; i < indexes.Length; i++)
{
sb.Append(_idxToWord[indexes[i]]);
		sb.Append(' ');
	}

return sb.ToString(0, Math.Max(0, sb.Length - 1));
}
finally
{
if (lockTaken)
	_spinLock.Exit();
}
}

后面,如果输入一个按照文档中token次序的一个索引列表,就可以还原文档。
所用到的函数是WordSequenceToString()

六、返回部分数据(map的两种形式)

要查看这个类中的数据,我们可以将_idxToWord处理后返回,因为这个对象本身的序号就是map后那个token所对应的数值索引号。

要做到这一点主要有两个函数,这里不再展开分析,因为他们的基本原理和上述的其他函数类似,都是先进入锁,然后将列表按照想要的方式返回。主要的方式有下面两种:

    1. public List<ReadOnlyMemory> ToListAsMemory()
    1. public List ToList()

七、一个很有用的功能:RemoveTokensNotPresentInDictionary

有时候我们希望将一个文档中不在字典里的词语除去,比如我们想提取英文词,就需要把文档分词后不在英文词典中词除去,这样就能使得最后的分析不会超出dictionary预先给定的范围。

这个方法的实现就是使用了RemoveTokensNotPresentInDictionary函数:

internal void RemoveTokensNotPresentInDictionary<T>(IDictionary<int, T> dict)
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
for (int i = _idxToWord.Count - 1; i >= 0; i--)
{
if (dict.ContainsKey(i))
			break;
		var s = _idxToWord[i];
		_idxToWord.RemoveAt(i);
		_wordToIdx.Remove(s);
	}
}
finally
{
if (lockTaken)
_spinLock.Exit();
}
}

函数的实现很简单,其中主要调用的就是ContainsKey()这个函数,如果发现某个外来的token不属于词典,就将这个词视作噪声除去了。

实质上,这个函数对数据进行了清洗工作,保障了数据的可以处理性。

八、总结

本文分析到的内容较多,覆盖面较大,但是所有内容都是WordIdxMap的一些具体功能。我们通过观察这些函数的实现,可以总结出几个问题:

  • 1.为了加快效率,这个类使用了“只读内存区域”,来存储词项。
  • 2.这个类给出了丰富的分词方法、词项转化成索引表的方法,以及逆向转化方法
  • 3.此外,这个类还提供了清洗文档中非字典词项的功能。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值