计算给定单词在超过 10 亿个单词的文本语料库中出现的次数
Posted
技术标签:
【中文标题】计算给定单词在超过 10 亿个单词的文本语料库中出现的次数【英文标题】:Counting number of times a given word appears in a text corpus of greater than 1 billion words 【发布时间】:2016-05-10 17:51:36 【问题描述】:我正在设计一个程序,用户输入一个单词,程序会确定该单词在文本语料库中出现的次数。现在文本语料库太大而无法放入内存,为了优化内存,我决定使用磁盘数据结构。我想使用哈希表进行插入和搜索,但不知道如何为磁盘设计它。如何设计密钥,以便在磁盘上查找密钥的值需要恒定的时间。我是否应该为特定的键子集创建单独的文件,以便查找是 O(1)?我确实知道存在 B 树,但是如何为这样的应用程序设计这样的哈希表?提前感谢您的回答!
【问题讨论】:
从缓冲的std::ifstream
中读取单个单词并获得std::map<srd::string,int>
。
构建一个哈希图,其中单词是键,计数是存储的值。在大多数现代计算机上,这应该很容易放入内存中,因为单词的数量可能只有几千个(即使它们的数量非常多)。
数据结构不会存储在 RAM 上,如何使用磁盘内存来实现?
@physio 为什么您不想使用磁盘内存来执行此操作?创建一个可重新读取的索引文件?
语料库中有多少个dinstinct词?下限(“至少 n 个单词_”)也会有所帮助。
【参考方案1】:
这是否可以在您的 2MB 内存要求内完成取决于您的语料库中不同单词的数量。如果您使用上一个答案中提到的布朗语料库,您有:
49,815 words at 8.075 characters average length = 402,256 bytes
49,815 counts at 4 bytes per count = 199,260 bytes
如果要将所有内容打包到一个字符数组中以便按顺序搜索它,则需要再添加 49,815 个 nul 终止符。结构是:
word,\0,count,word,\0,count . . .
这总共需要 651,331 个字节。所以你至少知道你的原始数据将适合内存。
您可以发挥创意,并在该数组中添加一个带有额外 49,815 个指针的排序索引。这将花费您另外 199,260 个字节并为您提供 O(log2(n)) 查找。考虑到键的数量很少,这将是非常糟糕的快速查找。不是恒定的,但非常好,它不到一兆字节。
如果您想要恒定的查找时间,您可以为键生成一个Minimal perfect hash。然后你用一个指针数组替换我上面提到的排序索引。无需存储密钥。最小完美哈希生成一个从 0 到 n 的数字;称之为k
。您转到数组中的第 k
th 索引以将指针 p
检索到平面数组中。
生成哈希函数不应该花费太长时间。在this article 中,作者声称他在大约 2.5 秒内创建了一个 100,000 个单词的最小完美函数。您可以在预处理期间构建它,也可以让程序在启动时计算它。
所有这些都应该放在一兆字节的空间内,并且应该比标准地图执行得更快,因为它保证没有冲突。所以没有一个桶包含一个以上的值。内存分配开销也被最小化了,因为只有两种分配:一种用于原始数据数组,另一种用于索引数组。
【讨论】:
我承认这比我的解决方案优雅得多(尽管需要更多编码);荣誉。【参考方案2】:作为pjs said in a comment above,存储十亿个标记所需的实际内存占用可能会非常小:自然语言(和许多其他东西)遵循Zipf's law,它基本上表示您最常用的词将远比第二常见的常见得多,后者比第三常见的要常见得多,依此类推。因此,这 10 亿个令牌中的很大一部分将是 a 和 the,假设您正在为英语这样做:
换句话说,只需先尝试使用unsorted_map<string, uint_least32_t>
,看看它是如何工作的。
实验:内存中的实际大小
从you mentioned that the solution can occupy at most 2 MB of memory 开始,我决定看看unsorted_map<string, uint_least32_t>
是否可以容纳所需的所有类型及其计数。首先,我用Python的NLTK得到the Brown corpus中的唯一词个数:
from nltk.corpus import brown
token_types = set(word.lower() for word in brown.words())
print len(token_types)
这给了我 49815 个唯一词的结果。然后我用 49815 个键创建了一个 unsorted_map<string, uint_least32_t>
,然后通过修改 a solution from a related question 来估计它的大小:
#include <cstdint>
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
// Using uint_least32_t for token counts because uint_least16_t might be a bit too narrow for counting frequencies
typedef unordered_map<string, uint_least32_t> TokenFrequencyMap;
static size_t estimateMemoryUsage(const TokenFrequencyMap& map)
size_t entrySize = sizeof(TokenFrequencyMap::key_type) + sizeof(TokenFrequencyMap::mapped_type) + sizeof(void*);
size_t bucketSize = sizeof(void*);
size_t adminSize = 3 * sizeof(void*) + sizeof(TokenFrequencyMap::size_type);
return adminSize + map.size() * entrySize + map.bucket_count() * bucketSize;
int main()
constexpr TokenFrequencyMap::size_type vocabSize = 49815;
TokenFrequencyMap counts;
counts.reserve(vocabSize);
for (TokenFrequencyMap::size_type i = 0; i < vocabSize; ++i)
string token = to_string(rand());
uint_least32_t count = rand();
counts[token] = count;
size_t memoryUsage = estimateMemoryUsage(counts);
cout << memoryUsage << endl;
return EXIT_SUCCESS;
在我的系统上(x86_64-linux-gnu
带有标志 -fexceptions -march=corei7 -O2 -std=c++11
的 GCC 4.8.4),它输出 1421940 字节,大约是 1.36 MB。因此,假设您的文本分布与布朗语料库的分布相似,那么使用unsorted_map<string, uint_least32_t>
实现的内存解决方案应该没有问题。
【讨论】:
很有趣,但是...看起来您在实验中使用字符串“1”到“49815”作为单词。因此,字符串的最大长度为五个字符(加上终止符)。考虑到小字符串优化,这可能意味着“文本”直接在字符串对象中。我怀疑真实语料库中唯一词的平均长度会更长,因此会使用更多的堆。 事实上,布朗语料库中唯一标记的平均长度是 8.075 个字符;这会对堆的使用产生影响吗? 是的,可能有很大的不同。 std::string 通常有一个优化,其中非常短的字符串(例如,最多 8 个字节)存储在字符串对象而不是堆中。一旦字符串超过该长度,它就必须存储在堆中。堆分配通常四舍五入到倍数或 8 或 16 字节,并且可能有一个或两个指针的开销。因此,一个 9 个字母的单词可能比一个 7 个字母的单词多占用 20 个字节的堆空间。与一堆五位数的测试密钥相比,少量较长的单词可能会占用更多空间,从而使使用量超过 2MB。 @AdrianMcCarthy,我从来不知道std::string
背后有这么多“魔力”;我猜 C++ 是一个比人们想象的要高级得多的编程环境。【参考方案3】:
使用 trie 怎么样?您将创建一个包含相同记录的文件(一组整数索引,每个字母一个),将其视为一个大数组,以便随机访问成为可能。您将需要一次处理一个节点,因此无需担心 RAM 空间。这很占空间,但实施起来很容易。
【讨论】:
每层可以使用两个或三个字母以减少步骤数。以上是关于计算给定单词在超过 10 亿个单词的文本语料库中出现的次数的主要内容,如果未能解决你的问题,请参考以下文章