目 录CONTENT

文章目录

如何在Scikit-learn管道中组合LLM嵌入、TF-IDF和元数据

Administrator
2026-02-25 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

📢 转载信息

原文链接:https://machinelearningmastery.com/how-to-combine-llm-embeddings-tf-idf-metadata-in-one-scikit-learn-pipeline/

原文作者: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. 创建模拟数据集

我们将创建一个包含textmetadata(结构化数据)和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)的组合时,需要一些变通的方法。

如上文所示,最稳健的流程是:

  1. 预计算:独立于Scikit-learn管道计算LLM嵌入。
  2. 转换元数据/文本:使用ColumnTransformer处理原始的textmetadata列,生成TF-IDF和编码后的元数据。
  3. 合并:将预计算的LLM嵌入与ColumnTransformer的输出进行水平堆叠np.hstack)。
  4. 训练:将合并后的密集特征矩阵输入到最终的分类器管道中。

通过这种方法,您可以充分利用深度学习模型的语义理解能力(LLM嵌入)和传统机器学习的效率(TF-IDF和元数据),从而为您的NLP任务构建更全面、更强大的特征集。




🚀 想要体验更好更全面的AI调用?

欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。

0

评论区