我们将问题分为两部分:
- 给定单词列表
passage,找到索引i,其中passage[i] 不在另一个单词列表dictionary 中。
- 然后简单地将
<unk> 放在这些索引处。
1 中需要做的主要工作。为此,我们首先将字符串列表转换为 2D numpy 数组,以便我们可以有效地执行操作。此外,我们对二进制搜索中所需的字典进行排序。此外,我们用 0 填充字典以具有与 passage_enc 相同的列数。
# assume passage, dictionary are initially lists of words
passage = np.array(passage) # np array of dtype='<U4'
passage_enc = passage.view(np.uint8).reshape(-1, passage.itemsize)[:, ::4] # 2D np array of size len(passage) x max(len(x) for x in passage), with ords of chars
dictionary = np.array(dictionary)
dictionary = np.sort(dictionary)
dictionary_enc = dictionary.view(np.uint8).reshape(-1, dictionary.itemsize)[:, ::4]
pad = np.zeros((len(dictionary), passage_enc.shape[1] - dictionary_enc.shape[1]))
dictionary_enc = np.hstack([dictionary_enc, pad]).astype(np.uint8)
然后我们只遍历段落,并检查字符串(现在是数组)是否在字典中。它需要 O(n * m), n, m 分别是段落和字典的大小。
但是,我们可以通过事先对字典进行排序并在其中进行二进制搜索来改进这一点。所以,它变成了 O(n * logm)。
此外,我们 JIT 编译代码以使其更快。下面,我使用numba。
import numba as nb
import numpy as np
@nb.njit(cache=True) # cache as being used multiple times
def smaller(a, b):
n = len(a)
i = 0
while(i<n and a[i] == b[i]):
i+=1
if(i==n):
return False
return a[i] < b[i]
@nb.njit(cache=True)
def bin_index(array, item):
first, last = 0, len(array) - 1
while first <= last:
mid = (first + last) // 2
if np.all(array[mid] == item):
return mid
if smaller(item, array[mid]):
last = mid - 1
else:
first = mid + 1
return -1
@nb.njit(cache=True)
def replace(dictionary, passage):
unknown_indices = []
n = len(passage)
for i in range(n):
ind = bin_index(dictionary, passage[i])
if(ind == -1):
unknown_indices.append(i)
return unknown_indices
检查样本数据
import nltk
emma = nltk.corpus.gutenberg.words('austen-emma.txt')
passage = np.array(emma)
passage = np.repeat(passage, 50) # bloat coprus to have around 10mil words
passage_enc = passage.view(np.uint8).reshape(-1, passage.itemsize)[:, ::4]
persuasion = nltk.corpus.gutenberg.words('austen-persuasion.txt')
dictionary = np.array(persuasion)
dictionary = np.sort(dictionary) # sort for binary search
dictionary_enc = dictionary.view(np.uint8).reshape(-1, dictionary.itemsize)[:, ::4]
pad = np.zeros((len(dictionary), passage_enc.shape[1] - dictionary_enc.shape[1]))
dictionary_enc = np.hstack([dictionary_enc, pad]).astype(np.uint8) # pad with zeros so as to make dictionary_enc and passage_enc of same shape[1]
段落和字典的大小,最终符合 OP 要求的顺序,用于计时目的。这个电话:
unknown_indices = replace(dictionary_enc, passage_enc)
在我的 8 核 16G 系统上耗时 17.028s(包括预处理时间,显然不包括加载语料库的时间)。
那么,很简单:
passage[unknown_indices] = "<unk>"
P.S : 我想,我们可以通过在 replace 的 njit 装饰器中使用 parallel=True 来获得更快的速度。我遇到了一些奇怪的错误,如果我能够解决它会编辑。