计算给定单词出现在大于10亿字的文本语料库中的次数

时间:2016-05-10 17:51:37

标签: c++ algorithm data-structures

我正在设计一个程序,用户输入一个单词,程序确定该单词出现在文本语料库中的次数。现在文本语料库太大而无法放入内存并优化内存我决定使用磁盘数据结构。我想使用哈希表进行插入和搜索,但不知道如何为磁盘设计它。如何设计密钥,以便在磁盘上查找密钥值时需要花费一些时间。我应该为特定的键子集创建单独的文件,以便查找是O(1)吗?我知道B树存在,但是如何为这样的应用程序设计这样的哈希表呢?提前感谢您的回答!

3 个答案:

答案 0 :(得分:3)

作为pjs said in a comment above,存储10亿个令牌所需的实际内存占用量可能会非常小:自然语言(以及许多其他内容)遵循Zipf's law,这基本上就是说你最常见的词比第二常见词更常见,这比第三常见词更常见,等等。因此,假设您正在为英语执行此操作,那么十亿个令牌中的大量将是 a

Zipf plot for Brown corpus tokens

换句话说,首先尝试使用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的GCC 4.8.4的-fexceptions -march=corei7 -O2 -std=c++11)上,输出1421940字节,大约为1.36 MB。因此,假设您的文本分布与Brown语料库的分布类似,那么使用unsorted_map<string, uint_least32_t>实现的内存中解决方案应该没有问题。

答案 1 :(得分:3)

这是否可以在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(log 2 (n))查找。考虑到少量的键,这将是相当快速的查找。不恒定,但非常好,并且它适合不到一兆字节。

如果您想要不断查找时间,可以为密钥生成Minimal perfect hash。然后,用一个指针数组替换上面提到的排序索引。没有必要存储密钥。最小完美哈希生成从0到n的数字;称之为k。您转到数组中的k索引,以便在平面数组中检索指针p

生成哈希函数不应该花费太长时间。在this article中,作者声称他创造了一个最小完美函数,在大约2.5秒内有100,000个单词的函数。您可以在预处理期间构建它,也可以让程序在启动时计算它。

所有这些都应该适合在一兆字节的空间内,并且应该比标准地图执行得更快,因为它可以确保没有碰撞。因此,没有桶包含多个值。内存分配开销也被最小化,因为只有两个分配:一个用于原始数据数组,一个用于索引数组。

答案 2 :(得分:1)

使用特里怎么样?您将创建一个具有相同记录的文件(整数索引集,每个字母表字母一个),看作一个大型数组,以便可以进行radom访问。您将需要一次处理一个节点,因此不必担心RAM空间。这很空间,但实施起来很容易。