使用scikit-learn DictVectorizer时如何规避numpy ArrayMemoryError
问题原因分析
你遇到的内存爆炸是one-hot编码的固有问题,并非操作错误。DictVectorizer默认会对所有类别型特征(比如word、previous_word这类字符串特征)执行one-hot编码:每个不同的词都会被转换成一个独立的特征维度。从报错的(334043, 200643)形状来看,你的数据仅类别特征就衍生出20万+维度,float64类型的稠密矩阵所需内存计算为:334043 * 200643 * 8字节 ≈ 499GiB,这显然远超你的硬件容量。
初始的3.3MB是字典列表的存储大小,和one-hot后的稠密矩阵完全不是一个量级。
可行解决方案
1. 使用稀疏矩阵(最快速的临时解决方案)
DictVectorizer默认参数就是sparse=True,你之前手动设置为sparse=False是导致内存爆炸的直接原因。改成稀疏矩阵后,只会存储非零值,内存占用会骤降:
from sklearn.feature_extraction import DictVectorizer # 启用稀疏矩阵模式(默认就是True,显式声明更清晰) dict_vectoriser = DictVectorizer(sparse=True) dict_vectoriser.fit(word_dicts) X_train_sparse = dict_vectoriser.transform(word_dicts) # 后续在Keras中使用稀疏矩阵:直接转换为TensorFlow稀疏张量 import tensorflow as tf X_train_tensor = tf.sparse.from_value(X_train_sparse)
稀疏矩阵的内存占用通常只有稠密矩阵的几百分之一甚至几千分之一,完全可以在64GB内存下运行。
2. 用词嵌入替代one-hot编码(NLP任务的最优方案)
对于词性标注这类NLP任务,word、previous_word、following_word这类高基数文本特征,**词嵌入(Embedding)**比one-hot编码高效得多,还能保留语义信息。具体步骤:
步骤1:分离高基数特征与低基数特征
把需要做嵌入的文本特征单独提取,剩下的低基数特征(比如sent_len、first_word等)用DictVectorizer处理:
# 提取高基数文本特征 words = [d['word'] for d in word_dicts] prev_words = [d['previous_word'] for d in word_dicts] next_words = [d['following_word'] for d in word_dicts] # 过滤出低基数特征,避免one-hot膨胀 filtered_dicts = [ {k: v for k, v in d.items() if k not in ['word', 'previous_word', 'following_word']} for d in word_dicts ]
步骤2:对文本特征做整数编码
用LabelEncoder把字符串转换成整数索引,供Embedding层使用:
from sklearn.preprocessing import LabelEncoder word_encoder = LabelEncoder() word_indices = word_encoder.fit_transform(words) prev_word_indices = word_encoder.transform(prev_words) # 复用同一个编码器,保证索引一致 next_word_indices = word_encoder.transform(next_words)
步骤3:处理低基数特征
这些特征基数低,用稠密矩阵没问题:
dict_vectoriser = DictVectorizer(sparse=False) other_features = dict_vectoriser.fit_transform(filtered_dicts)
步骤4:构建Keras模型,拼接嵌入与其他特征
from keras.layers import Input, Embedding, Dense, Concatenate, Flatten from keras.models import Model # 定义输入层 word_input = Input(shape=(1,), name='word_input') prev_word_input = Input(shape=(1,), name='prev_word_input') next_word_input = Input(shape=(1,), name='next_word_input') other_input = Input(shape=(other_features.shape[1],), name='other_input') # 定义共享的Embedding层 embedding_dim = 100 # 可根据任务调整 embedding_layer = Embedding( input_dim=len(word_encoder.classes_), # 词汇表大小 output_dim=embedding_dim, input_length=1 ) # 获取各文本特征的嵌入向量 word_emb = Flatten()(embedding_layer(word_input)) prev_word_emb = Flatten()(embedding_layer(prev_word_input)) next_word_emb = Flatten()(embedding_layer(next_word_input)) # 拼接所有特征 concat_features = Concatenate()([word_emb, prev_word_emb, next_word_emb, other_input]) # 构建词性标注的输出层(假设num_tags是词性标签的数量) num_tags = 10 # 替换成你的实际标签数 output = Dense(num_tags, activation='softmax')(concat_features) # 编译模型 model = Model( inputs=[word_input, prev_word_input, next_word_input, other_input], outputs=output ) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy') # 训练模型 # 假设y_train是你的词性标签的整数索引 model.fit( [word_indices, prev_word_indices, next_word_indices, other_features], y_train, epochs=10, batch_size=32 )
3. 可选:减少不必要的高基数特征
如果某些特征(比如start_letters-3、end_letters-3)对模型性能贡献不大,可以考虑删除这些特征,进一步降低维度。但这个方法优先级低于前两个,因为它可能损失模型精度。
总结
- 内存爆炸是one-hot编码针对高基数特征的必然结果,不是你的操作错误。
- 优先选择稀疏矩阵快速解决内存问题,或者用词嵌入实现更高效的NLP特征处理。
内容的提问来源于stack exchange,提问作者AdeDoyle

