📢 转载信息
原文作者:Jason Brownlee
在信息检索(IR)和自然语言处理(NLP)任务中,将文本数据与结构化元数据组合在一起通常是一个良好的策略。在Scikit-learn中,ColumnTransformer是实现此目的的理想工具,因为它允许您在不同的列上使用不同的预处理器,并将结果组合成一个单一的特征集,供下游模型使用。
在本文中,我们将探索如何将来自大型语言模型(LLM)的嵌入(Embeddings)、来自TF-IDF的稀疏特征以及结构化元数据组合到一个Scikit-learn管道中。
本文将从以下几个方面进行:
- 设置和导入所需的库。
- 创建一个模拟数据集,包含文本、元数据和目标变量。
- 定义LLM嵌入生成器。
- 构建一个Scikit-learn管道来组合所有特征。
- 训练和评估组合模型。
1. 设置和导入
我们将使用Scikit-learn和NumPy库。对于LLM嵌入,我们将使用一个简单的模拟函数来替代实际的嵌入模型,但概念是相同的。我们将使用TfidfVectorizer来处理文本特征。
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
# 模拟LLM嵌入生成器(通常您会使用预训练模型,如Sentence Transformers)
def generate_llm_embedding(texts):
"""模拟LLM嵌入生成。"""
print(f"Generating mock embeddings for {len(texts)} texts...")
# 为每个文本生成一个固定维度的随机向量
embedding_dim = 32
embeddings = np.random.rand(len(texts), embedding_dim)
return embeddings
2. 创建模拟数据集
我们将创建一个包含text、metadata(结构化数据)和target(分类标签)的数据集。
# 模拟数据
data = {
'text': [
"这是一个关于机器学习的好故事",
"深度学习正在改变世界",
"自然语言处理是一个热门领域",
"如何训练一个高效的神经网络",
"本文讨论了数据科学的未来",
"Scikit-learn管道的强大功能"
],
'metadata_feature_1': [10, 25, 5, 50, 30, 15],
'metadata_feature_2': ['A', 'B', 'A', 'C', 'B', 'A'],
'target': [0, 1, 0, 1, 1, 0]
}
df = pd.DataFrame(data)
# 准备特征和目标
X = df[['text', 'metadata_feature_1', 'metadata_feature_2']]
y = df['target']
# 拆分数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
print("训练数据样本数:", len(X_train))
print("测试数据样本数:", len(X_test))
3. 定义特征预处理
这里的关键在于使用ColumnTransformer来指定哪些列应该使用TF-IDF,哪些应该用于元数据处理。
3.1 LLM嵌入(模拟)
LLM嵌入通常需要一个外部函数或模型来生成,并且它们通常是密集向量。由于嵌入的生成过程通常与Scikit-learn的管道转换器不同步(它们可能在数据加载后计算一次,而不是在每次transform调用时重新计算),我们将其视为一个预先计算的步骤,但在管道中,我们可以将其包装起来。
注意: 在实际应用中,LLM嵌入步骤通常在外部完成,然后将生成的嵌入(一个NumPy数组)与DataFrame的其余部分连接起来,或者使用自定义转换器在管道内执行。
为了演示管道的组合,我们假设LLM嵌入已经在训练数据上生成好了。对于本例,我们将侧重于文本(TF-IDF)和元数据(数值/类别)的组合。
3.2 TF-IDF 和 元数据处理
我们将使用ColumnTransformer来处理不同的列类型:
- 文本列 (
text):应用TfidfVectorizer。 - 数值列 (
metadata_feature_1):不进行任何处理(或进行标准化)。 - 类别列 (
metadata_feature_2):应用独热编码 (One-Hot Encoding)。
为了简化,我们定义一个函数来生成嵌入,并在管道中使用自定义的预处理步骤来组合一切。
首先,我们定义文本特征处理器,它将使用TF-IDF,并在管道中处理text列:
# 文本特征(将由TF-IDF处理)
text_features = 'text'
# 元数据特征
numeric_features = ['metadata_feature_1']
categorical_features = ['metadata_feature_2']
# 用于元数据的预处理器
from sklearn.preprocessing import StandardScaler, OneHotEncoder
# 组合数值和类别元数据
metadata_transformer = Pipeline(steps=[
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# 完整的数据转换器
preprocessor = ColumnTransformer(
transformers=[
('tfidf', TfidfVectorizer(), text_features),
('metadata_numeric', StandardScaler(), numeric_features),
('metadata_categorical', metadata_transformer, categorical_features)
],
remainder='passthrough' # 保留任何未指定的列(在本例中,没有)
)
但是,我们如何整合LLM嵌入呢?
LLM嵌入是密集的,并且通常具有固定的维度(例如32维)。TF-IDF会产生一个稀疏的矩阵。Scikit-learn的ColumnTransformer在处理不同密度的输出时可能会遇到挑战,特别是当一个转换器(如TF-IDF)返回稀疏矩阵而另一个返回密集数组时。
为了成功组合它们,我们通常需要确保所有输出都是可以堆叠的(例如,都转换为密集格式,或者使用自定义连接器)。
在Scikit-learn中,最直接的方法是将所有特征在ColumnTransformer内进行转换,然后将结果连接起来。对于LLM嵌入,我们采用一个更实际的方法:在管道之外生成它们,然后将它们与管道生成的特征堆叠起来。
4. 组合特征和构建最终管道
由于LLM嵌入的生成往往是一个独立于文本转换步骤的重计算过程,我们将在管道外部处理它,并使用一个自定义的转换步骤来整合所有内容。
4.1 准备嵌入
首先,生成训练和测试集的LLM嵌入。
# 假设我们已经计算了嵌入
X_train_text = X_train['text'].tolist()
X_test_text = X_test['text'].tolist()
# 生成嵌入
llm_embed_train = generate_llm_embedding(X_train_text)
llm_embed_test = generate_llm_embedding(X_test_text)
print(f"训练嵌入形状: {llm_embed_train.shape}")
4.2 自定义合并转换器
我们需要一个自定义转换器,它可以接收文本/元数据(来自ColumnTransformer的输出),接收预先计算的嵌入,并将它们水平堆叠(hstack)。
然而,在标准的ColumnTransformer流程中,我们无法直接将管道外的变量传递给转换步骤。
替代方案: 我们将使用ColumnTransformer来处理所有非嵌入特征(TF-IDF + 元数据),然后使用一个自定义的最终步骤来合并预先计算的嵌入。
让我们修改preprocessor,只处理TF-IDF和元数据:
from sklearn.base import BaseEstimator, TransformerMixin
# 自定义转换器,用于合并预计算的嵌入和管道输出
class FeatureCombiner(BaseEstimator, TransformerMixin):
def __init__(self, combiner_transformer, llm_embeddings):
self.combiner_transformer = combiner_transformer
self.llm_embeddings = llm_embeddings
def fit(self, X, y=None):
# 仅对组合器进行拟合
self.combiner_transformer.fit(X)
return self
def transform(self, X):
# 转换非嵌入特征
non_llm_features = self.combiner_transformer.transform(X)
# 确保输出是密集矩阵,以便堆叠
if hasattr(non_llm_features, 'toarray'):
non_llm_features = non_llm_features.toarray()
# 水平堆叠
combined_features = np.hstack((non_llm_features, self.llm_embeddings))
print(f"Combined features shape: {combined_features.shape}")
return combined_features
# 在fit阶段,我们需要将X_train传递给组合器进行拟合
# 但FeatureCombiner的transform阶段需要X_test(或X_train本身)
# 准备用于管道的数据(仅包含需要ColumnTransformer处理的列)
X_train_pipe = X_train
X_test_pipe = X_test
# 最终管道步骤:首先使用ColumnTransformer处理TF-IDF和元数据
# 然后使用FeatureCombiner合并LLM嵌入
# 1. 创建ColumnTransformer处理所有非LLM特征
text_processor = TfidfVectorizer(max_features=100) # 限制特征数量以保持示例可控
metadata_transformer_final = ColumnTransformer(
transformers=[
('tfidf', text_processor, 'text'),
('numeric', StandardScaler(), numeric_features),
('categorical', OneHotEncoder(handle_unknown='ignore'), categorical_features)
],
remainder='drop'
)
# 2. 创建合并器,它将使用ColumnTransformer进行拟合/转换,并附加预先计算的嵌入
# 注意:这里我们使用一个特殊技巧,因为LLM嵌入是在外部计算的,我们不能直接在管道内拟合它。
# 我们需要确保FeatureCombiner的fit只拟合ColumnTransformer。
# 为了满足Scikit-learn API,我们需要一个更简单的设计:
# 1. 拟合ColumnTransformer(TF-IDF/元数据)
metadata_transformer_final.fit(X_train_pipe)
# 2. 转换所有数据
train_metadata_features = metadata_transformer_final.transform(X_train_pipe)
test_metadata_features = metadata_transformer_final.transform(X_test_pipe)
# 确保都是密集矩阵
if hasattr(train_metadata_features, 'toarray'):
train_metadata_features = train_metadata_features.toarray()
if hasattr(test_metadata_features, 'toarray'):
test_metadata_features = test_metadata_features.toarray()
print(f"TF-IDF/元数据训练特征形状: {train_metadata_features.shape}")
# 3. 水平堆叠(Hstack)
# 训练集组合
X_train_combined = np.hstack((train_metadata_features, llm_embed_train))
# 测试集组合
X_test_combined = np.hstack((test_metadata_features, llm_embed_test))
print(f"最终训练特征形状: {X_train_combined.shape}")
# 4. 构建最终分类器管道
# 现在我们有了一个包含所有特征的密集矩阵,可以直接用于分类器
classifier_pipeline = Pipeline(steps=[
('classifier', LogisticRegression(max_iter=1000, solver='liblinear'))
])
5. 训练和评估模型
使用组合后的特征集来训练和评估模型。
# 训练模型
print("\n开始训练最终模型...")
classifier_pipeline.fit(X_train_combined, y_train)
# 预测
y_pred = classifier_pipeline.predict(X_test_combined)
# 评估
accuracy = accuracy_score(y_test, y_pred)
print(f"\n模型准确率 (组合特征): {accuracy:.4f}")
# ----------------------------------------------------------------------
# 理论上,如果使用一个统一的管道,它将如下所示(但需要自定义转换器来处理外部计算的嵌入)
# 实际项目中,分离预计算和管道训练是更常见的做法。
# ----------------------------------------------------------------------
# 示例:仅使用ColumnTransformer(不包含LLM嵌入,因为它们不是数据列的一部分)
# full_pipeline = Pipeline(steps=[
# ('preprocessor', preprocessor),
# ('classifier', LogisticRegression())
# ])
# full_pipeline.fit(X_train, y_train)
总结
虽然Scikit-learn的ColumnTransformer是组合不同数据列转换的强大工具,但当涉及到外部预计算的密集特征(如LLM嵌入)与动态生成的稀疏特征(如TF-IDF)的组合时,需要一些变通的方法。
如上文所示,最稳健的流程是:
- 预计算:独立于Scikit-learn管道计算LLM嵌入。
- 转换元数据/文本:使用
ColumnTransformer处理原始的text和metadata列,生成TF-IDF和编码后的元数据。 - 合并:将预计算的LLM嵌入与
ColumnTransformer的输出进行水平堆叠(np.hstack)。 - 训练:将合并后的密集特征矩阵输入到最终的分类器管道中。
通过这种方法,您可以充分利用深度学习模型的语义理解能力(LLM嵌入)和传统机器学习的效率(TF-IDF和元数据),从而为您的NLP任务构建更全面、更强大的特征集。
🚀 想要体验更好更全面的AI调用?
欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。
评论区