Scikit-learn:将数据拟合到块中与一次性拟合之间的区别

9
我正在使用scikit-learn构建分类器,它可以处理(较大的)文本文件。目前我需要一个简单的词袋特征,因此我尝试使用TfidfVectorizer/HashingVectorizer/CountVectorizer来获取特征向量。
然而,一次性处理整个训练数据以获取特征向量会导致numpy/scipy内存错误(取决于使用哪个矢量化器)。
当从原始文本中提取文本特征时:如果我将数据分块拟合到矢量化器中,那么这是否与一次性拟合所有数据相同?
为了用代码说明这一点,以下是示例:
vectoriser = CountVectorizer() # or TfidfVectorizer/HashingVectorizer
train_vectors = vectoriser.fit_transform(train_data)

与以下内容不同:

vectoriser = CountVectorizer() # or TfidfVectorizer/HashingVectorizer


start = 0
while start < len(train_data):
    vectoriser.fit(train_data[start:(start+500)])
    start += 500

train_vectors = vectoriser.transform(train_data)

你的代码中是否有任何地方可以释放内存,例如通过摆脱对已解析文本数据的引用?或者你是否可以切换到64位Python和/或不同的操作系统(例如Windows将限制分配给每个进程的内存,而Ubuntu则不会)? - Aleksander Lidtke
@AleksanderLidtke,感谢您的评论。回答您的问题:
  1. 我无法释放更多内存。我不会将整个文本文件加载到内存中,我只会将文件名传递给scikit-learn向量化器。这意味着如果可以进行任何内存使用优化,那就在scikit-learn内部。
  2. 更改为另一个操作系统可能会有问题。
  3. 我不认为问题是由于使用太多内存造成的。我曾经看到我的Python应用程序使用了高达1.8GB的RAM,而现在它在大约500MB时崩溃。我应该打开另一个问题并发布我收到的异常跟踪吗?
- DarkMatter
没问题。好的,我明白了。我不知道scikit的底层工作原理,我只使用其中的一些部分。也许你可以自己读取数据,然后提取相关部分,并将其与一些更节省内存的scikit函数一起使用?或者写信给开发人员?另外,可能运行时耗尽内存的进程是你的Python进程的子进程。运行脚本时查看是否有任何占用大量内存的内容。 - Aleksander Lidtke
@DarkMatter scikit-learn没有问题使用我所有的8GB RAM(甚至更多)。可能是因为它想要在内存中分配一个巨大的矩阵,所以你会遇到内存错误。因此实际上并没有使用RAM,但你仍然有内存问题。对我来说看起来像是一个合法的问题 ;)。 - ldirer
2个回答

6
这取决于您使用的向量化程序。CountVectorizer计算文档中单词的出现次数。它为每个文档输出一个(n_words, 1)向量,其中包含每个单词在文档中出现的次数。n_words是文档中单词的总数(也就是词汇量的大小)。同时,它还适配了一个词汇表,因此您可以自行检查模型(查看哪个单词很重要等)。您可以使用vectorizer.get_feature_names()查看。
当您将其拟合到前500个文档时,词汇表只由这500个文档中的单词组成。假设有30k个单词,则fit_transform会输出一个500x30k的稀疏矩阵。
现在,您再次使用fit_transform处理接下来的500个文档,但是它们只包含29k个单词,因此您会得到一个500x29k的矩阵...
那么如何对齐矩阵以确保所有文档具有一致的表示形式呢?
目前我想不到简单的方法。TfidfVectorizer有另一个问题,即逆文档频率:要能够计算文档频率,您需要同时查看所有文档。
然而,TfidfVectorizer只是一个后接TfIdfTransformerCountVectorizer,因此,如果您设法正确获取CountVectorizer的输出,那么就可以在数据上应用TfIdfTransformerHashingVectorizer则不同,这里没有词汇表。
In [51]: hvect = HashingVectorizer() 
In [52]: hvect.fit_transform(X[:1000])       
<1000x1048576 sparse matrix of type '<class 'numpy.float64'>'
 with 156733 stored elements in Compressed Sparse Row format>   

这里的前1000个文档中没有1M+不同的单词,但我们得到的矩阵有1M+列。
HashingVectorizer不会在内存中存储单词。这使其更加节省内存,并确保它返回的矩阵始终具有相同数量的列。 因此,在这里你不会遇到CountVectorizer的问题。
这可能是你所描述的批处理的最佳解决方案。缺点是你无法获得idf加权,并且你不知道单词和特征之间的映射关系。 HashingVectorizer文档引用了一个示例,该示例对文本数据进行离线分类。虽然可能有点混乱,但它确实可以做到你想做的事情。
希望这可以帮助你。 编辑: 如果你有太多数据,则使用HashingVectorizer是正确的方法。 如果你仍然想使用CountVectorizer,一个可能的解决方法是自己拟合词汇表,并将其传递给向量化器,以便你只需要调用transform
以下是你可以适应的示例:
import re
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer

news = fetch_20newsgroups()
X, y = news.data, news.target

现在不起作用的方法如下:
# Fitting directly:
vect = CountVectorizer()
vect.fit_transform(X[:1000])
<1000x27953 sparse matrix of type '<class 'numpy.int64'>'
with 156751 stored elements in Compressed Sparse Row format>

请注意我们得到的矩阵的大小。
手动适配词汇:
def tokenizer(doc):
    # Using default pattern from CountVectorizer
    token_pattern = re.compile('(?u)\\b\\w\\w+\\b')
    return [t for t in token_pattern.findall(doc)]

stop_words = set() # Whatever you want to have as stop words.
vocabulary = set([word for doc in X for word in tokenizer(doc) if word not in stop_words])

vectorizer = CountVectorizer(vocabulary=vocabulary)
X_counts = vectorizer.transform(X[:1000])
# Now X_counts is:
# <1000x155448 sparse matrix of type '<class 'numpy.int64'>'
#   with 149624 stored elements in Compressed Sparse Row format>
#   
X_tfidf = tfidf.transform(X_counts)

在您的示例中,您需要先构建整个矩阵X_counts(适用于所有文档),然后再应用tfidf转换。


1
非常好的解释,谢谢!我认为如果我通过训练数据进行两次遍历,在第一次遍历中只执行“fit”,然后在第二次遍历中执行“transform”,这将解决您提到的CountVectorizer问题,因为它首先构建词汇表,然后给出向量。但是,这似乎效率要低得多,因为fit在CountVectorizer内部调用fit_transform(?),所以我基本上做了相同的工作两次。我仍然不太确定的是:这是否适用于TfidfVectorizer? - DarkMatter
如果你的所有文档一次性能够放下,那么进行两次遍历是可行的。否则,你可以手动收集词汇,并将其传递给CountVectorizer构造函数,以便它能够使用固定的词汇表。要获得一个TfidfVectorizer,你只需在CountVectorizer的输出上使用TfidfTransformer。我很惊讶地发现fit确实调用了fit_transform!一个可能的解决方法是通过遍历文档来自己构建词汇表。 - ldirer
为什么我需要一次性适配所有文档?我认为会发生的情况是每次适配另一个批次时,词汇表都会扩展。因此,在稍后转换数据时,将使用从所有文档中提取的最终词汇表,即使是分块的文档也是如此。如果我没有看到明显的问题,那就抱歉了 :( - DarkMatter
否则,感谢您的答案。我会接受它,但请在此处更新我们的对话想法,以便对其他人有所帮助 :) - DarkMatter
1
很不幸,每次调用fit时,向量化器都是从头开始拟合的。虽然您所描述的行为在您的情况下确实很好,但我建议您自己构建词汇表,我会为此编辑我的答案。否则,如果您有太多数据,则可以使用HashingVectorizer。 - ldirer

1

我不是文本特征提取专家,但根据文档和我的其他分类器基础经验:

如果我在训练数据的块上进行多次拟合,是否与一次性拟合整个数据相同?

您不能直接合并提取的特征,因为您将获得不同的重要性,即从不同块中获取的相同标记/单词的权重与块的其他单词以不同的比例表示,并用不同的键表示。

您可以使用任何特征提取方法,结果的有用性取决于任务,我认为。

但是,如果您可以在相同数据上使用不同块的不同特征进行分类。一旦您使用相同的特征提取方法(或者您也可以使用不同的提取方法)获得了几个不同的输出,您可以将它们用作“合并”机制的输入,例如baggingboosting等。实际上,在上述整个过程之后,您通常会获得一个更好的最终输出,而不是将完整文件馈入一个“全功能”的但甚至是简单分类器中。


我必须说,我并不完全确定我理解了你的回答。使用集成方法进行分类似乎确实是一个好主意。然而,这并没有回答我的问题,即如何提取特征(包括未知数据)。在训练过程中,我可以轻松地为不同的“突发事件”使用不同的向量化器,但当到达未知数据时,我应该使用哪个向量化器来将其转换为特征向量以进行分类呢?我以前没有实现过像装袋这样的方法,所以对此的答案可能非常明显... - DarkMatter
@DarkMatter,你的回答如下:“那么我的问题是:如果我对训练数据的多个块进行拟合,是否与一次性拟合整个数据相同?”但请根据您的需求更新您的问题,我会尽力回答,或者您可以提出一个新问题。 - Geeocode
谢谢您的评论。我已经相应地更新了问题,希望现在更清楚了。代码示例最能说明我的问题。 - DarkMatter
@DarkMatter 我现在明白我回答了你的问题,但可能有点含糊不清,所以我更新了我的回答,请参考“你已经获得的功能”。 - Geeocode

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接