如何在Python中有效计算大型矩阵乘法(tfidf特征)?
我现在想用余弦相似度和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 个回答
你可以做的是从X中切一行和一列,计算它们的乘积,然后把结果保存到一个文件里。接着再移动到下一行和下一列。
这样做计算的工作量还是一样的,但你就不会耗尽内存了。
如果你使用 multiprocessing.Pool.map()
或 multiprocessing.Pool.map_async()
,可能会加快速度,前提是你在映射函数中使用 numpy.memmap()
来读取矩阵。而且你可能需要把每一行计算的结果写到一个单独的文件里,以便后面合并。如果你想从映射函数返回这一行结果,它就得传回原来的进程。这会占用很多内存和进程间通信的带宽。
虽然 X
是稀疏的,但 X * X.T
可能不会稀疏。注意,只要在一对行中有一个共同的非零元素,就会影响结果。你在做自然语言处理(NLP)的任务,所以我敢肯定,有很多词几乎出现在所有文档中(而且之前提到的,不需要每一对都只有一个相同的词,而是每一对可以有一个(可能不同的)词)。这样一来,你就会得到一个包含 350363^2
个元素的矩阵,这大约有 1220 亿个元素。如果你的内存没有 200GB,那看起来是无法计算的。你可以尝试更严格地过滤词汇,以便让 X * X.T
保持稀疏(去掉很多常见词)。
总的来说,你无法计算大数据的 Gram 矩阵,除非你强制让 X * X.T
保持稀疏,这样大多数向量对(文档)之间的“相似度”就是 0。实现这个有很多方法,最简单的办法是设定一个阈值 T
,低于这个值就把 <a,b>
视为 0
,然后自己计算点积,只有当 <a,b> > T
时,才在结果的稀疏矩阵中创建一个条目。
你可以看看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的数据集项目中广泛使用了这个方法,如果你决定走这条路,我可以提供更多帮助。