机器学习生产系统——特征工程与特征选择

293 阅读41分钟

特征工程和特征选择是机器学习数据预处理的核心,尤其是在模型训练过程中。进行推理时同样需要特征工程,且推理阶段的预处理必须与训练阶段的预处理相匹配,这一点至关重要。

如果你曾在学术或研究环境等非生产场景下从事机器学习工作,本章中的部分内容可能看起来像复习。但我们将在本章中重点讨论生产中的问题。我们将探讨的一个主要问题是如何以可重复且一致的方式大规模地进行特征工程。

此外,我们还将讨论特征选择及其在生产环境中的重要性。通常,你会拥有比实际所需更多的特征,而你的目标应是仅包括那些对你试图解决的问题最具预测信息的特征。加入过多的特征不仅增加了成本和复杂性,还可能导致质量问题,如过拟合。

特征工程简介

“提出特征是困难且耗时的,需要专业知识。应用机器学习通常需要对特征和数据集进行精细的工程化。”
—— Andrew Ng

特征工程是一种预处理,旨在帮助模型学习。特征工程对于最大化利用数据至关重要,且具有一定的艺术性。目标是从数据中提取尽可能多的信息,以有利于模型学习的方式呈现数据。数据的表示方式对模型的学习效果有很大影响。例如,数值数据标准化后,模型往往能够更快更可靠地收敛。因此,选择和转换输入数据的技术是提高模型预测质量的关键,并在可能的情况下推荐降维。

在特征工程中,我们需要确保保留最相关的信息,同时增强表示和预测信号,并减少所需的计算资源。请记住,在生产机器学习中,计算资源在训练和推理中都是运行模型的关键成本因素。

特征工程的艺术在于在尽量减少模型所需计算资源的同时,提高模型的学习能力。它通过转换、投影、消除和/或组合原始数据中的特征来形成数据集的新版本。像机器学习中的许多事情一样,这往往是一个随着数据和模型演变而不断迭代的过程。

特征工程通常以两种截然不同的方式应用。在训练过程中,通常可以访问整个数据集,这使得在特征工程转换中使用单个特征的全局属性成为可能。例如,可以计算特征在所有样本中的标准差,然后使用它进行标准化。

在服务训练好的模型时,必须对传入的预测请求进行与训练时完全相同的特征工程,以确保输入模型的数据类型与训练数据一致。例如,如果在训练时为分类特征创建了独热向量,那么在服务模型时也需要创建相应的独热向量。

然而,在服务时,你无法使用整个数据集进行操作,通常是逐个处理每个请求,因此服务过程需要访问特征的全局属性(如标准差)。这意味着如果在训练中使用了标准差,那么在服务时也必须在特征工程中包含该标准差。未能做到这一点是生产系统中一个非常常见的问题源,称为“训练-服务偏差”,而且这些错误通常难以发现。本章稍后将对此进行详细讨论。

总结几点关键内容,特征工程可能非常困难且耗时,但对成功至关重要。你需要通过特征工程从数据中提取最大的价值,从而使模型学习得更好。还需确保集中预测信息,并将数据压缩成尽可能少的特征,以最有效、最具成本效益的方式利用计算资源。此外,必须确保在服务时应用的特征工程与训练时相同。

预处理操作

有一次,当我们刚开始做机器学习时,突然想到可以跳过数据归一化,于是我们就试了一下。我们训练了一个模型,结果当然是它没有收敛。我们开始担心模型和代码的问题,完全忘了之前决定不做归一化,于是我们尝试调整超参数、改变模型的层次结构,并寻找数据中的问题。花了一些时间后我们才想起来:“哦对了,我们没做归一化!”于是我们添加了归一化操作,果然,模型开始收敛了。哎呀!从那以后,我们就再没犯过这个错误了。

在本节中,我们将讨论以下预处理操作,这些操作代表了数据预处理中的主要步骤:

  • 数据整理和清洗
  • 归一化
  • 分桶化
  • 独热编码
  • 降维
  • 图像转换

预处理的第一步几乎总是进行一定程度的数据清理,通常称为数据整理。它包括一些基本操作,例如确保所有样本中的每个特征数据类型正确,且数据值有效。这部分工作也会涉及到特征工程。在此步骤中,我们从映射原始数据到特征开始。当然,我们还会检查不同类型的特征,例如数值特征和类别特征。我们对数据的了解应该有助于我们在工程化更优质特征的目标上取得进展。

在此步骤中,我们还会进行数据清洗,广义上来说,数据清洗包括去除或纠正错误数据。这部分工作与领域相关。例如,如果数据在商店营业期间收集,而你知道商店在午夜不营业,那么任何带有午夜时间戳的数据可能都应该被丢弃。

通过对每个特征进行变换,例如缩放、归一化或对数值进行分桶处理,通常可以提高结果效果。例如,整数数据可以映射为浮点数,数值数据可以归一化,类别值可以创建成独热向量。特别是归一化有助于梯度下降。

其他类型的变换更具有全局性质,会影响多个特征。例如,降维涉及减少特征数量,有时通过将特征投影到不同的空间。可以使用多种技术创建新特征,包括组合或从其他特征派生特征。

文本是一种具有多种预处理转换方法的数据类型。模型只能处理数值数据,因此对于文本特征,有多种技术可以将文本转换为数值数据。例如,如果文本表示类别,通常会使用独热编码等技术。如果类别数量很大,或者每个文本值可能唯一,通常会使用词汇表,将特征转换为词汇表中的索引。如果文本用于自然语言处理(NLP)且其意义很重要,则使用嵌入空间,将特征值中的单词表示为该空间中的坐标。文本预处理还包括词干提取和词形还原等操作,以及TF-IDF、n-grams等归一化技术。

图像数据与文本相似,预处理时可以对其应用多种转换。已经开发出一些技术可以提升图像的预测质量,包括旋转、翻转、缩放、剪裁、调整大小、裁剪或模糊图像;使用Canny滤波器或Sobel滤波器等专门滤波器;或实现其他光度失真。图像数据的变换也广泛用于数据增强。

特征工程技术

特征工程涵盖了广泛的数据操作,既包括最初应用于统计学和数据科学中的操作,也包括专为机器学习开发的新技术。对所有技术的讨论本身足以成书,实际上,已经有几本书专门探讨这一主题。因此,在本节中,我们将重点介绍一些最常见的技术,并为你提供对特征工程的基本理解,以及为何它如此重要。

归一化和标准化

一般来说,所有数值特征值都应该进行归一化或标准化。如以下公式所示,归一化(也称为最小-最大缩放)将特征值移位并缩放到[0,1]的范围内。标准化(也称为z分数)将特征值移位并缩放到均值为0、标准差为1的范围内,公式如下。归一化和标准化均通过提升梯度下降找到极小值的能力来帮助模型学习:

截屏2024-11-12 22.37.23.png

在归一化和标准化中,都需要特征值的全局属性。归一化需要知道最小值和最大值,而标准化需要知道均值和标准差。这意味着必须遍历数据集中的每个样本,计算这些值。对于大型数据集,这可能需要大量处理。

在归一化和标准化之间的选择通常可以通过实验来确定哪种方法效果更好,也可以根据对数据的了解来决定。如果特征值呈现高斯分布,那么标准化可能是更好的选择。否则,归一化通常更优。需要注意的是,归一化也经常在神经网络结构中作为一层应用,通过提升梯度下降来帮助反向传播。

分桶

数值特征可以通过分桶转化为类别特征。分桶创建了一系列值的范围,每个特征根据其值所在的范围被分配到相应的桶中。

桶可以是等间距的,也可以基于每个桶中包含的样本数量来划分,使每个桶包含相同数量的样本,这称为分位数分桶。等间距桶只需选择桶的大小,但可能导致某些桶包含的样本数量远多于其他桶,甚至出现空桶。分位数分桶需要遍历整个数据集,以计算每个不同大小的桶中应包含的样本数量。因此,选择分桶方式时,需要考虑数据的分布情况。对于更均匀的分布,使用不需要遍历数据集的等间距桶可能是合适的。如果数据分布偏斜,则可能值得遍历数据集以实现分位数分桶。

分桶对那些数值特征非常有用,尤其当这些特征在模型中本质上更接近于类别特征时。例如,对于地理数据,预测精确的纬度和经度可能会掩盖数据的全局特性,而将其分组为区域可能会揭示模式。

特征交叉

特征交叉将多个特征组合成一个新特征。这种方法在特征空间中编码非线性关系,或使用更少的特征编码相同的信息。我们可以创建许多不同类型的特征交叉,具体取决于数据。这需要一定的想象力,寻找组合特征的方法。例如,如果我们有数值特征,可以将两个特征相乘,生成一个包含这两个特征信息的新特征。我们还可以将类别特征甚至数值特征以符合语义的方式组合,以更少的特征捕捉信息的含义。

例如,如果我们有两个不同的特征,一周中的某一天和一天中的小时,我们可以将它们组合为一周中的小时数。这会生成一个单一特征,保留了之前两个特征中的信息。

降维和嵌入

降维技术有助于在保留最大方差的同时减少模型的输入特征数量。主成分分析(PCA)是最广为人知的降维算法,它将数据投影到沿主成分的低维空间,以降低数据的维度。t分布随机邻域嵌入(t-SNE)和统一流形近似与投影(UMAP)也是降维技术,但它们通常用于将高维数据可视化为二维或三维。

将数据投影到低维空间用于可视化是一种嵌入。但通常在讨论嵌入时,我们指的是语义嵌入空间或词嵌入。这些嵌入捕捉数据中不同项目之间的语义关系,最常用于自然语言处理中。例如,单词“apple”在意义上会比“sailboat”更接近“orange”,因为前两个都是水果,而后者与它们的概念关联较少。这类语义嵌入在自然语言模型中广泛应用,但也可用于图像或其他具备概念含义的项目。通过训练模型理解项目之间的关系,通常通过在非常大的数据集或语料库上进行自监督训练,将数据投影到语义嵌入空间中。

可视化

能够在低维空间中可视化数据通常对理解数据特性非常有帮助,例如那些在其他情况下可能难以察觉的聚类。换句话说,这有助于你对数据形成直观的理解。这正是特征工程中的一些“艺术”发挥作用的地方,作为开发者,你可以通过此来加深对数据的理解。对于高维数据来说尤其重要,因为人类可以直观地理解的维度数很有限——三维是可以的,四维就很难,20维更是不可想象。像TensorFlow嵌入投影器这样的工具在这方面非常有价值。这个工具是免费的,使用起来也很有趣,同时它也是帮助你理解数据的绝佳工具。

大规模特征转换

当你从课堂学习或研究人员转向从事生产环境中的机器学习时,你会发现,在笔记本上处理几兆字节的数据做特征工程是一回事,而在生产环境中处理几TB的数据并实现可重复、自动化的过程又是另一回事。

在过去,当机器学习管道尚处于初期阶段时,数据科学家通常会使用笔记本在一种语言(如Python)中创建模型,然后在不同的平台上部署,可能还要将特征工程代码重写成另一种语言(如Java)。这种从开发到部署的转换经常会带来难以识别和解决的问题。后来,发展出一种更好的方法:机器学习从业者使用管道和统一的框架来进行训练和部署,从而实现一致和可重复的结果。让我们来看看如何利用这样的系统在大规模下进行特征工程。

选择一个可扩展性好的框架

在大规模场景下,你的训练数据集可能达数TB,因此你希望每个转换都尽可能高效并最大限度地利用计算资源。因此,在第一次编写特征工程代码时,通常最好从数据子集开始,在全面使用数据集之前尽可能多地解决问题。只要选择一个可扩展性好的框架,你可以在开发机器或笔记本上使用与大规模环境相同的数据处理框架。但在生产环境中,它会被配置得有所不同。

例如,Apache Beam包含一个Direct Runner,可以直接在笔记本电脑上运行,然后你可以替换为Google Dataflow Runner或Apache Flink Runner,以扩展到整个数据集。因此,Apache Beam的扩展性很好。而Pandas的扩展性不佳,因为它假定整个数据集可以放入内存中,且没有分布式处理的功能。

避免训练-服务偏差

训练和服务之间的一致转换极其重要。记住,训练数据上的任何转换都需要在服务模型时准确地应用于预测请求的数据。如果在服务模型时进行的转换与训练时的转换不同,甚至即使使用不同的代码来执行相同的转换,也会产生问题,而且这些问题往往难以发现,甚至可能意识不到。模型结果看起来可能合理,也没有任何错误提示,但实际上由于给模型提供了错误的数据或与训练数据不匹配的数据,模型的结果远低于预期。这称为训练-服务偏差。

特征工程中的不一致或训练-服务偏差通常源于使用不同的代码来转换训练和服务的数据。在训练模型时,你有用于训练的代码。如果代码库不同(例如,使用Python进行训练,而使用Java进行服务),那就是潜在的问题来源。最初,这个问题的解决方案似乎很简单:在训练和服务中使用相同的代码。但这在某些部署场景下可能不可行。例如,如果你要将模型部署到服务器集群并在物联网(IoT)设备上使用,可能无法在两种环境中使用相同的代码,因为配置和可用资源的不同。

考虑实例级转换与全遍历转换

根据对数据进行的转换类型,你可能可以对每个样本单独转换而无需引用数据集中的其他样本,这称为实例级转换;也可能需要在执行任何转换之前分析整个数据集,这称为全遍历转换。显然,全遍历转换的计算需求比实例级转换要高得多,因此需要谨慎设计全遍历转换。

即使是像归一化这样基本的操作,你也需要确定特征的最小值、最大值和标准差,这需要检查每个样本,因此需要进行全遍历转换。如果你有数TB的数据,这将需要大量的处理。相比之下,进行一个简单的特征交叉相乘则可以在实例级别完成。分桶也可以在实例级别完成,前提是你提前知道桶的划分;有时你需要进行全遍历以确定合适的桶划分。

一旦完成全遍历收集到数值特征的最小值、最大值和标准差等统计信息,最好将这些值保存下来并将其包含在服务过程的配置中,以便在对预测请求进行转换时在实例级使用这些值。对于归一化操作,如果已经知道最小值、最大值和标准差,你可以单独处理每个请求。实际上,在在线服务中,由于每个请求独立到达服务器,通常很难进行类似于全遍历的操作。对于批量服务,如果批量大小足够大且具有代表性,可以进行全遍历,但若能避免这种操作则更好。

使用TensorFlow Transform

要在大规模环境中进行特征工程,我们需要扩展性好的工具。TensorFlow Transform(以下简称“TF Transform”)就是一个广泛使用且高效的工具。在本节中,我们将深入了解TF Transform的工作原理、功能及其目的。我们会探讨使用TF Transform的优势、它如何应用特征转换,并了解TF Transform的一些分析器及其在特征工程中的作用。虽然TF Transform是一个独立的开源库,可以单独使用,但我们主要聚焦于在TensorFlow Extended(TFX)管道中使用TF Transform。我们将在第18和19章详细介绍TFX管道,现在你可以将它理解为一个设计用于生产部署的完整训练过程。

TF Transform可用于处理训练数据和服务请求,尤其当你在TensorFlow中开发模型时。如果你没有使用TensorFlow,仍然可以使用TF Transform,但对于服务请求,则需要在模型外部使用它。当与TensorFlow一起使用时,TF Transform执行的转换可以包含在模型中,这意味着无论将训练好的模型部署到何处进行服务,转换都是完全一致的。

在典型的TFX管道中,我们从原始训练数据开始。(虽然我们讨论的是一个典型管道,但TFX允许你创建几乎任何你能想到的管道架构。)我们使用管道中的第一个组件ExampleGen进行数据拆分。默认情况下,ExampleGen会导入数据并将其拆分为训练和评估数据集,但这种拆分是可配置的。

拆分后的数据集被传递给StatisticsGen组件。StatisticsGen对数据计算统计信息,完整遍历数据集。例如,对于数值特征,它计算均值、标准差、最小值和最大值等;对于类别特征,它收集训练数据中包含的有效类别值。

这些统计信息会传递给SchemaGen组件,SchemaGen推断每个特征的类型。SchemaGen生成的模式会被下游组件使用,包括ExampleValidator,ExampleValidator利用之前生成的统计信息和模式查找数据中的问题。例如,如果某个特征中的样本类型错误(比如,我们期望是浮点型但得到了整数),ExampleValidator会标记该错误。

在典型管道中,Transform是下一个组件。Transform将利用从原始训练数据集中生成的模式,并根据我们提供的代码进行特征工程。转换后的数据会传递给Trainer和其他下游组件。

图3-1展示了一个简化的TFX管道,训练数据流经管道,训练后的模型则流向服务系统。数据和各种工件在过程中流入并流出元数据存储系统。为了提供一个高层次的视图,图3-1省略了流程的详细信息,我们将在后续章节中详细介绍这些细节。

image.png

ransform组件从ExampleGen、StatisticsGen和SchemaGen获取输入,包括数据集及其模式。顺便提一下,这个模式很可能由了解数据预期特性且比SchemaGen推断更深入的开发人员进行审核和改进,这个过程称为“模式管理”。TF Transform还需要你的用户代码,因为你需要指定所要进行的特征工程。例如,如果你要对某个特征进行归一化,就需要提供相应的用户代码给TF Transform。

TF Transform生成以下内容:

  • 一个TensorFlow图,称为“转换图”(transform graph)
  • 转换后的数据的新模式和统计信息
  • 转换后的数据本身

转换图以TensorFlow图的形式表达我们对数据进行的所有转换。转换后的数据则是我们进行转换的结果。图和数据都将传递给Trainer组件,Trainer将使用转换后的数据进行训练,并将转换图添加到训练好的模型之前。

训练TensorFlow模型会生成一个TensorFlow图,称为SavedModel,这是模型参数和操作的计算图。将转换图添加到SavedModel之前很重要,因为这意味着无论模型在哪里或如何部署服务,我们都始终进行完全相同的转换,避免了训练-服务偏差的风险。转换图也经过优化,可以将不变转换的结果(如数值特征的标准差)捕获为常量。

由于TF Transform被设计为支持非常大的数据集,它通过Apache Beam进行处理。这样TF Transform可以扩展,从在单个CPU上运行到在大型计算集群上运行,通常只需更改一行代码。

分析器

许多数据转换需要对整个数据集进行计算或统计。例如,无论是简单地计算数值特征的最小值,还是在由一组特征描述的空间上进行相对高级的PCA分析,都需要对数据集进行完整遍历。而数据集可能包含大量TB级数据,这将需要大量计算资源。

为执行这些计算,TF Transform定义了“分析器”的概念。分析器对数据执行各个独立操作,包括以下内容:

功能分析器
缩放scale_to_z_score scale_to_0_1
分桶quantiles apply_buckets bucketize
词汇表bag_of_words tfidf ngrams
降维pca

分析器使用Apache Beam进行处理,从而支持可扩展性。每个分析器仅在每次模型训练工作流中运行一次,在服务过程中不会运行。相反,每个分析器生成的结果被捕获为转换图中的常量,并包含在SavedModel中。在训练和服务期间,这些常量用于转换各个样本。

代码示例

现在来看一些代码。我们从创建预处理函数开始,用于定义用户代码,表达要进行的特征工程:

import tensorflow_transform as tft
def preprocessing_fn(inputs):
    ...
    # 特征工程代码

例如,我们可能希望使用z分数归一化数值特征:

for key in DENSE_FLOAT_FEATURE_KEYS:
    outputs[key] = tft.scale_to_z_score(inputs[key])

这只是一个示例,DENSE_FLOAT_FEATURE_KEYS 是你预先定义的特征名称列表。你将进行必要的特征工程,但这里的代码风格就是这种Python代码形式。为基于文本的类别特征生成词汇表也非常相似:

for key in VOCAB_FEATURE_KEYS:
    outputs[key] = tft.vocabulary(inputs[key], vocab_filename=key)

我们还可能希望创建一些桶特征,这是指将数值特征基于值范围分配到“桶”中,从而转化为类别特征:

for key in BUCKET_FEATURE_KEYS:
    outputs[key] = tft.bucketize(inputs[key], FEATURE_BUCKET_COUNT)

这些只是示例,并非所有内容都需要用这种“for循环”风格。

在生产环境中,TF Transform通常使用Apache Beam将处理分布到计算集群中。在开发过程中,你也可以在单机系统上使用Beam——例如,你可以在笔记本电脑上运行,使用Direct Runner。在开发中,这非常有用。

特征选择

在生产环境中,你将拥有多个可以提供给模型的数据源。通常,一些可用数据并不能帮助模型学习和生成预测。例如,如果你想预测法国用户在网页上可能感兴趣的广告,那么将当前日本的温度数据提供给模型,可能不会对模型的学习有帮助。

特征选择是一组算法和技术,旨在通过确定数据中哪些特征实际上有助于模型学习,从而提升数据的质量。在本节中,我们将讨论特征选择技术,但首先从一个相关的概念——特征空间——开始。

特征空间

特征空间是由特征定义的n维空间。如果你有两个特征,那么特征空间是二维的;如果有三个特征,则是三维的,以此类推。特征空间不包括目标标签。

对于数值特征,特征空间是最容易理解的。每个特征的最小值和最大值决定了该空间每个维度的范围。模型只能在这些范围内的值中实际学习预测,尽管当提供超出这些范围的样本时,它仍然会尝试进行预测。模型在这种情况下的表现取决于其鲁棒性,我们将在后面讨论。

因此,特征空间的覆盖率非常重要。我们将由训练数据定义的特征空间称为“训练特征空间”,而模型在生产环境中接收到的预测请求所定义的特征空间称为“服务特征空间”。理想情况下,训练特征空间应该覆盖整个服务特征空间,甚至比服务特征空间稍大会更好。

请记住,随着数据漂移,服务特征的值范围也会变化,因此有必要进行监控,以在预测请求漂移过大时发出信号,从而可以使用新数据重新训练模型。

训练数据在特征空间不同区域中的密度也很重要。模型在包含更多样本的区域中可能更准确,而在样本较少的区域中准确性可能较低。通常,训练数据样本的绝对数量不如样本的多样性及其特征空间覆盖率重要。初学者常犯的一个错误是认为更多的数据自动会更好,但如果数据中存在大量重复或近似重复的样本,增加数据量不太可能改善模型。

特征选择概述

回到本节的主要主题:特征选择。可以将特征选择视为优化数据的一个部分。目标是仅包含最少数量的特征,以提供最大量的预测信息,从而帮助模型学习。

我们试图选择真正需要的特征,去除不需要的特征,从而缩小特征空间。减少维度进一步减少了所需的训练数据量,并且通常增加了特征空间覆盖的密度。

每个包含的特征都会增加收集和维护系统、带宽及存储资源的需求,以便创建训练数据集并在服务时提供该特征。特征还会增加模型的复杂性,甚至可能降低模型的准确性。此外,增加的特征使模型的服务成本和复杂性增加,因为需要处理更多数据且更大的模型需要更多计算资源。

特征选择算法种类繁多,(与建模类似)可以是监督的或无监督的。接下来,我们将讨论一些因素,以帮助你决定选择监督或无监督的特征选择。

顾名思义,无监督特征选择不考虑特征和标签之间的关系,而是寻找相关性较高的特征。当有两个或多个高度相关的特征时,只需要其中一个,并尝试选择那个能够提供最佳结果的特征。

监督特征选择专注于每个特征和标签之间的关系,尝试评估每个特征中包含的预测信息量(通常称为特征重要性)。监督特征选择算法包括过滤方法、包裹方法和嵌入方法。以下各节将介绍每类算法。

过滤方法

在过滤方法中,主要使用相关性来寻找包含我们将用于预测目标的特征。这可以是单变量或多变量的,其中单变量计算量较少。

衡量相关性的方法有多种,包括以下几种:

  • 皮尔逊相关系数(Pearson correlation)用于线性关系的相关性测量,可能是最常用的。
  • 肯德尔τ(Kendall’s Tau)是一种秩相关系数,关注单调关系,通常用于样本量较小的情况以提高效率。
  • 斯皮尔曼相关系数(Spearman correlation)测量两个变量之间单调关联的强度和方向。

除了相关性,一些算法还使用其他指标,包括互信息、F检验和卡方检验。

以下是使用Pandas计算特征选择的皮尔逊相关系数的方法:

# 默认使用皮尔逊相关系数
cor = df.corr()
cor_target = abs(cor["feature_name"])
# 选择高度相关的特征以去除冗余
redundant_features = cor_target[cor_target > 0.8]

接下来,我们来看使用scikit-learn包进行单变量特征选择。该包提供了几种单变量算法,包括SelectKBestSelectPercentile和相对通用的GenericUnivariateSelect。这些算法支持使用统计测试,包括用于回归问题的互信息和F检验。对于分类,scikit-learn提供了卡方检验、用于分类的F检验以及分类的互信息版本。以下是单变量特征选择的代码示例:

def univariate_selection():
    X_train, X_test, Y_train, Y_test = train_test_split(
                                           X, Y,
                                           test_size=0.2,
                                           stratify=Y,
                                           random_state=123)
    
    X_train_scaled = StandardScaler().fit_transform(X_train)
    X_test_scaled = StandardScaler().fit_transform(X_test)
    min_max_scaler = MinMaxScaler()
    Scaled_X = min_max_scaler.fit_transform(X_train_scaled)
    selector = SelectKBest(chi2, k=20)  # 使用卡方检验
    X_new = selector.fit_transform(Scaled_X, Y_train)
    feature_idx = selector.get_support()
    feature_names = df.drop("diagnosis_int", axis=1).columns[feature_idx]
    return feature_names

上述代码展示了使用scikit-learn进行特征选择的典型模式。

包裹方法

包裹方法是有监督的,这意味着需要对数据集进行标记。包裹方法使用模型来衡量从数据集中迭代添加或移除特征的影响。所有包裹方法的核心过程包括:

  • 选择一个要包含在本次迭代中的特征集合
  • 使用该特征集合训练并评估模型
  • 将评估指标与其他特征集合的指标进行比较,以确定下一次迭代的起始特征集合

包裹方法通常比其他特征选择技术计算量更大,尤其是对于包含大量潜在特征的情况。包裹方法的三种主要类型是前向选择、后向消除和递归特征消除。

前向选择

前向选择是一种迭代的贪婪搜索算法。我们从一个特征开始,训练模型并评估模型性能。我们重复这个过程,保留先前添加的特征,并逐个添加新的特征。在每轮测试中,逐一尝试所有剩余特征,测量性能,并保留能提供最佳性能的特征进入下一轮。我们不断重复这个过程,直到性能不再提升,此时我们得到了最佳的特征子集。

可以看到,前向选择每次迭代都需要训练一个新模型,并且迭代次数随着潜在特征数量的增加而呈指数增长。如果你认为最终的特征集相对于潜在特征集来说会较小,那么前向选择是一个不错的选择。

后向消除

顾名思义,后向消除基本上是前向选择的相反过程。后向消除从所有特征开始,评估在移除每个特征时的模型性能。我们逐个移除特征,试图在减少特征数量的同时提升性能,直到性能不再提升。

同样地,后向消除每次迭代都需要训练一个新模型,并且迭代次数随潜在特征数量增加而呈指数增长。如果你认为最终的特征集将占潜在特征集的大部分,那么后向消除是一个不错的选择。

递归特征消除

递归特征消除使用特征重要性来选择保留的特征,而不是依赖模型性能。我们首先选择希望保留的特征数量,然后从全部潜在特征开始训练模型,并每次消除一个特征。我们按特征重要性对特征进行排序,这意味着需要一种分配特征重要性的方法,然后丢弃最不重要的特征。重复这一过程,直到达到希望保留的特征数量。

一个重要方面是模型需要能够衡量特征重要性,但并非所有模型都具备这一能力。最常见的能够衡量特征重要性的模型类别是基于树的模型。另一个方面是需要事先决定保留的特征数量,这并不总是显而易见的。前向选择和后向消除都能自动确定该数量,当性能不再提高时自动停止。

代码示例

以下是使用scikit-learn进行递归特征消除的代码示例:

def run_rfe(label_name, X, Y, num_to_keep):
    X_train, X_test, y_train, y_test = train_test_split(
                                           X, Y,
                                           test_size=0.2,
                                           random_state=0)
    
    X_train_scaled = StandardScaler().fit_transform(X_train)
    X_test_scaled = StandardScaler().fit_transform(X_test)
    model = RandomForestClassifier(criterion='entropy',
                                   random_state=47)
    
    rfe = RFE(model, n_features_to_select=num_to_keep)
    rfe = rfe.fit(X_train_scaled, y_train)
    
    feature_names = df.drop(label_name, axis=1).columns[rfe.get_support()]
    return feature_names

此代码示例使用随机森林分类器,这是能够衡量特征重要性的模型之一。

嵌入方法

嵌入方法的特征选择很大程度上依赖于模型设计本身。例如,L1或L2正则化本质上是一种粗略且效率较低的嵌入式特征选择方法,因为它们可以禁用对结果贡献不大的特征。

一个更好的例子是使用特征重要性(这是大多数基于树的模型架构的一个属性)来选择重要特征。这在许多常见框架中都有很好的支持,包括scikit-learn,其中可以使用SelectFromModel方法进行特征选择。

需要注意的是,要使用嵌入方法,模型必须至少在合理的程度上训练,以衡量每个特征对结果的影响(即特征重要性)。这导致了一个迭代过程,类似于前向选择、后向消除和递归消除,用于评估不同特征集合的效果。

LLM和生成式AI的特征与样本选择

迄今为止的讨论主要关注经典和深度学习应用中的特征选择技术,目标是提升训练数据集的质量。数据质量的重要性也延伸到了大型语言模型(LLM)和其他生成式AI(GenAI)应用中,并且研究表明,提高数据集质量对结果有显著影响。这推动了专门面向生成式AI数据集的新技术的发展,但在这些情况下,关注的重点通常是样本选择而非特征选择。

生成式AI数据集(如用于预训练LLM的数据集)通常是从互联网上抓取的大量数据集合。例如,Common Crawl数据集的大小可能从数百TB到PB级别。然而,这些数据集中的特征数量非常少,通常只有单一的文本特征,用于训练LLM。

选择原始数据集中哪些样本包含在最终数据集中的技术已显示出越来越显著的效果。例如,当本书付印时,Google DeepMind发表了一篇关于多模态对比学习与联合样本选择(JEST)的论文,作者提出了一种基于批处理的算法来识别高质量的训练数据。通过使用他们的技术,作者能够在多模态学习中实现显著的效率提升。这些改进带来的诸多优势之一是显著降低了训练最先进的生成式AI模型所需的能耗,仅仅通过提升数据质量就能实现。

示例:使用TF Transform对文本进行分词

由于文本是一种常见的数据类型,语言模型的强大功能也非常值得关注,让我们来看一个应用于所有语言模型的特征工程示例。之前我们讨论了如何使用TF Transform在模型训练之前对数据集进行预处理。在本示例中,我们深入探讨一个常见的预处理步骤:非结构化文本的分词。

基于分词的语言模型(如BERT、T5和LLaMa)要求将原始文本转换为词元,确切地说是转换为词元ID。语言模型使用一个词汇表进行训练,通常仅限于最常用的词片段和控制词元。

如果你想训练一个BERT模型来对文本的情感进行分类,需要使用分词器将输入文本预处理为词元ID:

  • 文本:“I like pistachio ice cream.”
  • 词元:['i', 'like', 'pi', '##sta', '##chio', 'ice', 'cream', '.']
  • 词元ID:[1045, 2066, 14255, 9153, 23584, 3256, 6949, 1012]

此外,语言模型期望使用“控制词元”,例如起始词元、结束词元或填充词元。在本示例中,我们展示如何预处理文本数据以准备对BERT模型进行微调。然而,这些步骤(稍作修改)同样适用于其他语言模型,如T5和LLaMa。

TensorFlow和PyTorch等机器学习框架提供了支持此类转换的框架特定库。在本示例中,我们使用TensorFlow Text和TF Transform。如果你更喜欢PyTorch,可以查看TorchText。

在将文本转换为词元之前,建议将文本标准化为支持的字符编码(例如UTF-8)。同时,可以“清理”文本,例如删除每个样本中常见的文本模式。

一旦文本数据被标准化和清理后,我们将对文本进行分词。根据使用的自然语言库,可以直接将文本分词为词元ID,或先分词为词元字符串然后再转换为词元ID。在我们的示例中,TensorFlow Text支持直接转换为词元ID。著名的BERT模型使用WordPiece分词,而更新的模型(如T5和LLaMa)则依赖于SentencePiece分词。

你应该使用哪种分词器?

分词类型的选择取决于基础模型,本例中是BERT。你的分词方式需要与语言模型初始训练时使用的分词器相匹配。同时,还需要使用初始训练中的相同词汇表,否则微调期间生成的词元ID将与初始训练时生成的词元ID不一致。这将导致灾难性遗忘,并影响模型性能。

不同类型的分词器在分词速度、空白字符处理和多语言支持方面存在差异。

语言模型还期望一组控制词元来表示输入的开始或结束,以及填充词元和未知词元的数量。 未知词元是分词器无法转换为词元ID的词元,因此会使用固定的ID表示此类词元。

语言模型期望固定的输入长度。这意味着较短的文本需要填充。在这种情况下,我们将文本填充到语言模型期望的最大词元数。对于BERT模型,这通常是512个词元(除非另有定义)。

基于Transformer的语言模型通常还期望input_mask,有时还包括input_type_idsinput_mask通过聚焦数据输入的相关部分来加速语言模型的计算。对于BERT模型,该模型在训练时使用了不同的目标(例如,第二个句子是否是第一个句子的后续句子)。为了支持这些目标,模型需要区分不同的句子,这通过input_type_ids来实现。

现在我们将以下四个步骤整合成一个示例:

  1. 文本标准化
  2. 文本分词
  3. 词元截断/填充
  4. 创建输入掩码和类型ID
import tensorflow as tf
import tensorflow_hub as hub  
import tensorflow_text as tf_text
…

START_TOKEN_ID = 101
END_TOKEN_ID = 102
TFHUB_URL = ("https://www.kaggle.com/models/tensorflow/bert/tensorFlow2/"
              "en-uncased-l-12-h-768-a-12/3")

def load_bert_model(model_url=TFHUB_URL):
    bert_layer = hub.KerasLayer(handle=model_url, trainable=False)
    return bert_layer

def _preprocessing_fn(inputs):
      vocab_file_path = load_bert_model().resolved_object.vocab_file.asset_path

      bert_tokenizer = tf_text.BertTokenizer(
          vocab_lookup_table=vocab_file_path,
          token_out_type=tf.int64,
          lower_case=True)

      text = inputs['message']
      category = inputs['category']

      # 文本标准化
      text = tf_text.normalize_utf8(text)

      # 分词
      tokens = bert_tokenizer.tokenize(text).merge_dims(1, -1)

      # 添加控制词元
      tokens, input_type_ids = tf_text.combine_segments(
          tokens,
          start_of_sequence_id=START_TOKEN_ID, 
          end_of_segment_id=END_TOKEN_ID)

      # 词元截断/填充
      tokens, input_mask_ids = tf_text.pad_model_inputs(
        tokens, max_seq_length=128)

      # 将类别转换为标签
      labels = tft.compute_and_apply_vocabulary(
          label, vocab_filename="category")

      return {
        "labels": labels,
        "input_ids": tokens,
        "input_mask_ids": input_mask_ids,
        "input_type_ids": input_type_ids,
      }

使用上述预处理函数可以准备文本数据,以微调BERT模型。若要微调其他语言模型,只需更新分词器函数和预处理步骤中的预期输出数据结构。

使用TF Transform的优势

之前我们提到,TF Transform的优势在于其高效的预处理。然而,不同于之前的示例,本示例中的每次转换是逐行进行的,TF Transform的分析遍历可能不是必需的。尽管如此,仍有几个理由在此情况下使用TF Transform:

  • 将类别转换为标签通常需要一次分析遍历,因此词元转换实际上是一个附加的好处。
  • 它可以防止训练-服务偏差,确保训练数据和服务数据的一致性。
  • 由于其预处理图计算能力,它可以随着数据量扩展,允许通过Apache Beam和Google Cloud Dataflow等工具实现预处理并行化。
  • 通过将特征预处理与实际训练分离,帮助保持复杂模型的可理解性和可维护性。
  • 它通过Transform标准管道组件集成到TFX中。

不过,使用TF Transform需要一定的初始实现投入。如果TF Transform的设置过于复杂,建议查看下一节列出的替代方案。

TF Transform的替代方案

TF Transform并不是处理文本和语言模型的唯一库。还有许多其他用于不同机器学习框架的自然语言库,包括:

  • KerasNLP
    KerasNLP抽象了分词和数据结构的创建。截至撰写本文时,它支持TensorFlow模型,但仅限于一组语言模型。不过,它允许快速启动原型模型。
  • SpaCy
    这款与框架无关的NLP库提供了多种预处理功能。如果你需要一个独立于机器学习框架的解决方案,这是一个很好的选择。
  • TorchText
    如果你正在开发PyTorch模型,TorchText是一个完美的NLP库选择。它为基于PyTorch的机器学习项目提供了类似于TensorFlow Text的功能。

总结

本章继续了我们对数据的讨论,重点介绍了一些技术,以改进我们现有的数据,从而获得更好的结果。2024年,机器学习领域重新关注数据的重要性,促使Andrew Ng发起了“数据驱动的AI运动”。在生成式AI中,也日益关注构建高度精细化、高质量的数据集,用于微调如PaLM和LLaMa等预训练基础模型。

人们为什么关注数据?原因相对简单。越来越多的大规模数据集的出现,使许多人倾向于关注数据量而非数据质量。领域内的领导者现鼓励开发者更加关注数据质量,因为最终重要的不是数据量,而是数据中包含的信息。以人类的视角来看,你可以阅读一千本关于南极的书籍,却对计算机科学一无所知,但读一本关于计算机科学的书则可以学到许多计算机科学的知识。对你或你的模型来说,重要的是那些书籍或数据集中包含的信息。

本章讨论的特征工程旨在使这些信息更易于被模型访问,从而让模型更容易学习。本章讨论的特征选择旨在将数据中的信息集中到最高质量的形式,帮助你在有效利用计算资源方面做出权衡。