如何在Python中有效计算大型矩阵乘法(tfidf特征)?

4 投票
3 回答
3095 浏览
提问于 2025-04-18 15:49

我现在想用余弦相似度和Tfidf特征在Python中计算所有文档之间的相似度。我的基本方法是这样的:

from sklearn.feature_extraction.text import TfidfVectorizer
#c = [doc1, doc2, ..., docn]
vec = TfidfVectorizer()
X = vec.fit_transform(c)
del vec
Y = X * X.T

这个方法运行得很好,但不幸的是,对于我非常大的数据集来说就不行了。X的维度是(350363, 2526183),所以输出矩阵Y应该是(350363, 350363)。由于使用了tfidf特征,X的数据非常稀疏,因此可以轻松放入内存(大约只需要2GB)。然而,进行矩阵乘法时,经过一段时间后我就会遇到内存错误(尽管内存并没有满,但我想scipy可能很聪明,预判了内存的使用情况)。

我已经尝试过调整数据类型,但没有成功。我还确保numpy和scipy的BLAS库已经链接,但这对csr_matrix的点乘功能没有影响,因为它是用C语言实现的。我考虑过使用像memmap这样的东西,但对此我不太确定。

有没有人知道该如何更好地处理这个问题呢?

3 个回答

1

你可以做的是从X中切一行和一列,计算它们的乘积,然后把结果保存到一个文件里。接着再移动到下一行和下一列。

这样做计算的工作量还是一样的,但你就不会耗尽内存了。

如果你使用 multiprocessing.Pool.map()multiprocessing.Pool.map_async(),可能会加快速度,前提是你在映射函数中使用 numpy.memmap() 来读取矩阵。而且你可能需要把每一行计算的结果写到一个单独的文件里,以便后面合并。如果你想从映射函数返回这一行结果,它就得传回原来的进程。这会占用很多内存和进程间通信的带宽。

7

虽然 X 是稀疏的,但 X * X.T 可能不会稀疏。注意,只要在一对行中有一个共同的非零元素,就会影响结果。你在做自然语言处理(NLP)的任务,所以我敢肯定,有很多词几乎出现在所有文档中(而且之前提到的,不需要每一对都只有一个相同的词,而是每一对可以有一个(可能不同的)词)。这样一来,你就会得到一个包含 350363^2 个元素的矩阵,这大约有 1220 亿个元素。如果你的内存没有 200GB,那看起来是无法计算的。你可以尝试更严格地过滤词汇,以便让 X * X.T 保持稀疏(去掉很多常见词)。

总的来说,你无法计算大数据的 Gram 矩阵,除非你强制让 X * X.T 保持稀疏,这样大多数向量对(文档)之间的“相似度”就是 0。实现这个有很多方法,最简单的办法是设定一个阈值 T,低于这个值就把 <a,b> 视为 0,然后自己计算点积,只有当 <a,b> > T 时,才在结果的稀疏矩阵中创建一个条目。

6

你可以看看scikit-learn里的random_projection模块。约翰逊-林登施特劳斯引理说,随机投影矩阵可以在一定的容忍度eta内保持成对距离,这个容忍度是计算所需随机投影数量时的一个超参数。

简单来说,scikit-learn中的SparseRandomProjection在这里可以找到,它可以帮你完成这个工作。如果你在经过vec.fit_transform之后对X运行它,你应该会看到特征数量大幅减少。

来自sklearn.random_projection.johnson_lindenstrauss_min_dim的公式显示,为了保持10%的容忍度,你只需要johnson_lindenstrauss_min_dim(350363, .1) 10942个特征。这是一个上限,所以你可能只需要更少的特征。即使是1%的容忍度,也只需要johnson_lindenstrauss_min_dim(350363, .01) 1028192个特征,这仍然比你现在拥有的要少得多。

编辑: 一个简单的尝试是,如果你的数据类型是'dtype=float64',可以试试用'float32'。这样就能节省大量空间,特别是当你不需要双精度时。

如果问题是你无法在内存中存储“最终矩阵”,我建议使用HDF5Store来处理数据(在pandas中使用pytables可以看到)。这个链接有一些不错的入门代码,你可以逐步计算点积的块并写入磁盘。我最近在一个45GB的数据集项目中广泛使用了这个方法,如果你决定走这条路,我可以提供更多帮助。

撰写回答