在机器学习的实际项目中,我们经常面临这样一个棘手的问题:数据集中有成百上千个特征,但并非所有特征都对模型预测有帮助。有些特征是纯粹的噪音,有些则是高度相关的。如果我们把所有特征都“喂”给模型,不仅会降低模型的泛化能力,还会极大地增加计算成本。
这就是我们进行特征选择的原因。在众多的特征选择方法中,Fisher Score(费舍尔得分)是一种简单但极其强大的过滤式方法。它基于统计学原理,能够快速量化特征对分类任务的贡献程度。
在这篇文章中,我们将深入探讨 Fisher Score 的数学原理,通过实战代码演示如何应用它,并分享一些在实际开发中处理高维数据的经验。无论你是在处理文本分类、基因数据还是常规的结构化数据,掌握这一工具都将为你的数据预处理流程增添一份底气。
Fisher Score 的核心逻辑
Fisher Score 的核心思想非常直观:一个好的特征,应该让不同类别的数据“离得远”,让同一类别的数据“聚得拢”。
为了让你更好地理解,让我们设想一个场景:我们要区分“猫”和“狗”。如果我们选择“体重”作为特征,你会发现猫的体重通常集中在一个较小的范围(比如 3-6kg),而狗的体重集中在另一个较大的范围(比如 10-30kg)。这两个群体的体重均值差异很大,且各自内部波动较小。这时候,“体重”就是一个 Fisher Score 很高的特征。
反之,如果我们选择“是否有尾巴”作为特征,猫和狗几乎都有尾巴(或者都有类似的结构),这两个群体在这一点上没有显著差异,那么这个特征的 Fisher Score 就会很低,对分类几乎没有帮助。
数学定义与公式解析
让我们稍微严谨一点,看看数学上是如何定义这个指标的。对于给定的特征 $x_j$ 和包含 $C$ 个类别的数据集,Fisher Score 定义如下:
$$
F(xj) = \frac{ \sum\limits{c=1}^{C} nc \left( \muj^{(c)} – \muj \right)^2 }{ \sum\limits{c=1}^{C} nc \left( \sigmaj^{(c)} \right)^2 }
$$
这个公式看起来有点吓人,但其实它就是一个简单的比率。让我们把它拆解开来:
- 分子(类间方差 Between-class Variance):
$$ \sum\limits{c=1}^{C} nc \left( \muj^{(c)} – \muj \right)^2 $$
这里衡量的是不同类别之间的距离。$\muj^{(c)}$ 是第 $c$ 类中特征 $j$ 的均值,$\muj$ 是整个数据集的均值。如果各类别的均值与整体均值差异很大(也就是类别之间隔得远),分子就会变大。
- 分母(类内方差 Within-class Variance):
$$ \sum\limits{c=1}^{C} nc \left( \sigma_j^{(c)} \right)^2 $$
这里衡量的是同一类别内部的紧密程度。$\sigma_j^{(c)}$ 是第 $c$ 类中特征 $j$ 的标准差。如果每个类别内部的数据点都非常紧密地围绕在均值周围(波动小),分母就会变小。
结论: 我们的目标是找到 分子大、分母小 的特征。也就是说,我们希望特征在类别之间有显著的差异性,同时在类别内部具有高度的稳定性。
Python 实战指南
理论讲完了,让我们把双手放在键盘上。在实际的 Python 开发中,我们通常使用 scikit-learn 库来实现特征选择。
1. 基础示例:Iris 数据集
INLINECODEaf7839de 提供了 INLINECODEfc5ed1a2 函数,它计算的是 ANOVA F 值,这在数学上与我们讨论的 Fisher Score 是等价的(对于分类问题)。让我们先在一个经典的数据集上试一试。
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.feature_selection import SelectKBest, f_classif
# 1. 加载数据
# 我们使用经典的 Iris 数据集,包含 3 种鸢尾花和 4 个特征
data = load_iris()
X, y = data.data, data.target
feature_names = data.feature_names
print(f"原始数据集形状: {X.shape}")
# 2. 计算 Fisher Score (使用 f_classif)
# k=‘all‘ 表示我们计算所有特征的分数,而不进行实际的选择(为了演示)
selector = SelectKBest(score_func=f_classif, k=‘all‘)
selector.fit(X, y)
# 3. 提取并展示结果
# 我们可以创建一个 DataFrame 来美观地展示结果
scores_df = pd.DataFrame({
‘特征名称‘: feature_names,
‘Fisher Score‘: selector.scores_
})
# 按照得分降序排列,看看哪个特征最重要
print("
特征的 Fisher Score 排名:")
print(scores_df.sort_values(by=‘Fisher Score‘, ascending=False))
代码解析:
在这段代码中,我们首先加载了数据。INLINECODE44a52e81 会自动计算每个特征的 F 值。运行后,你通常会发现 INLINECODE1c6634ef 的分数最高。这说明在 Iris 数据集中,花瓣长度是区分这三种花最关键的特征,而 sepal width (萼片宽度) 往往得分最低。
2. 进阶实战:合成数据集与可视化
仅仅知道分数是不够的,我们需要知道如何根据分数来“筛选”特征。让我们构建一个具有明显噪音特征的数据集,看看 Fisher Score 如何帮我们去伪存真。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
# 1. 创建一个模拟数据集
# 生成 1000 个样本,共 20 个特征
# 其中只有 5 个是“信息特征”(有用的),剩下的 15 个是“冗余特征”(噪音)
X, y = make_classification(
n_samples=1000,
n_features=20,
n_informative=5,
n_redundant=15,
n_classes=2,
random_state=42
)
# 将特征转换为 DataFrame 格式方便处理
feature_names = [f‘Feature_{i+1}‘ for i in range(X.shape[1])]
X_df = pd.DataFrame(X, columns=feature_names)
# 2. 使用 SelectKBest 选取前 10 个特征
# 注意:这里我们实际上是想保留最重要的 10 个
selector_k = SelectKBest(score_func=f_classif, k=10)
X_new = selector_k.fit_transform(X_df, y)
# 3. 查看哪些特征被选中了
mask = selector_k.get_support() # 这是一个布尔数组
selected_features = X_df.columns[mask]
print(f"原始特征数量: {X_df.shape[1]}")
print(f"保留特征数量: {X_new.shape[1]}")
print("
被选中的特征:")
print(selected_features.tolist())
# 4. 可视化所有特征的得分,直观感受差异
plt.figure(figsize=(12, 6))
scores = selector_k.scores_
plt.bar(feature_names, scores, color=‘skyblue‘)
plt.axhline(y=np.mean(scores), color=‘r‘, linestyle=‘--‘, label=‘平均分‘)
plt.xticks(rotation=45)
plt.ylabel(‘Fisher Score‘)
plt.title(‘所有特征的 Fisher 得分对比‘)
plt.legend()
plt.tight_layout()
plt.show()
实战经验分享:
当你运行上面的可视化代码时,你会清晰地看到,大部分噪音特征的分数都非常低,甚至接近于 0。而那 5 个真正的信息特征会像柱状图中的“摩天大楼”一样耸立。在实际项目中,如果不确定 $k$ 设为多少合适,画出这个直方图是一个非常有效的策略。你可以观察到一个明显的“断层”,分数断崖式下降的地方,就是你可以设置 $k$ 值的参考点。
3. 处理非负数据:文本分类案例
Fisher Score(以及 ANOVA F 值)是基于方差计算的,它假设特征是连续变量。但在文本分类中,我们的数据通常是词频(Term Frequency),是非负的。这时候 Fisher Score 还适用吗?
答案是肯定的,但需要注意一些细节。对于文本数据,我们通常将其视为离散的数值分布。让我们看一个简化的文本分类模拟。
from sklearn.feature_extraction.text import CountVectorizer
# 模拟简单的文本数据
corpus = [
‘This is a spam message offering free money‘,
‘Win a free prize now‘,
‘Meeting agenda for today‘,
‘Project update attached‘
]
# 标签: 1 代表 Spam, 0 代表 Ham
labels = [1, 1, 0, 0]
# 1. 文本向量化
vectorizer = CountVectorizer()
X_text = vectorizer.fit_transform(corpus).toarray()
print(f"词汇表: {vectorizer.get_feature_names_out()}")
# 2. 计算 Fisher Score
selector_text = SelectKBest(score_func=f_classif, k=‘all‘)
selector_text.fit(X_text, labels)
# 3. 分析结果
# 让我们把结果排个序,看看哪些词最能区分 Spam 和 Ham
text_scores = pd.DataFrame({
‘Word‘: vectorizer.get_feature_names_out(),
‘Score‘: selector_text.scores_
}).sort_values(by=‘Score‘, ascending=False)
print("
区分 Spam/Ham 能力最强的单词排名:")
print(text_scores.head(5))
在这个例子中,你会发现像 “free”、“money” 这样的词得分很高,而 “is”、“a” 这种在所有文档中都常见的词得分很低。这展示了 Fisher Score 在特征筛选方面的敏锐度。
Fisher Score vs 其他方法
作为开发者,我们在工具箱里有很多工具。为什么选择 Fisher Score 而不是其他方法?让我们对比一下。
Fisher Score vs 互信息
- Fisher Score 衡量的是线性关系(基于均值和方差)。它假设数据服从正态分布,且各类别的方差大致相等。
- 互信息 衡量的是任意依赖关系。它不仅能捕捉线性关系,还能捕捉复杂的非线性关系。
- 建议: 如果你假设特征和类别之间是线性相关的(例如:数值越大,越可能是 A 类),Fisher Score 计算更快,效果很好。如果你怀疑存在复杂的非线性关系,互信息可能更稳健,但计算成本稍高。
Fisher Score vs 递归特征消除
- RFE 是一种包装式方法,它需要训练具体的模型(比如 SVM 或 Logistic Regression)来评估特征的重要性。
- Fisher Score 是一种过滤式方法,它不依赖于任何具体的模型,纯粹看数据本身的统计特性。
- 建议: 在项目初期,数据量非常大且维度极高时,先用 Fisher Score 快速剔除大部分噪音特征,然后再使用 RFE 进行精细调优。这种“组合拳”往往能节省大量时间。
最佳实践与常见陷阱
在使用 Fisher Score 进行特征选择时,有几个经验之谈希望能帮到你:
- 数据预处理至关重要:Fisher Score 对特征的尺度非常敏感。如果你有一个特征是“身高(米)”,范围是 1.5-1.9;另一个特征是“年薪(万元)”,范围是 10-100。如果不进行标准化(Standardization),方差大的特征可能会在公式中占据主导地位。务必在使用前进行
StandardScaler标准化。
- 处理缺失值:公式中包含均值和方差的计算。如果数据中有 INLINECODE4ff45c7a,会导致计算崩溃。在调用 INLINECODE0253ab4d 之前,请确保你已经用
SimpleImputer或其他策略填补了缺失值。
- 不要忽略相关性:Fisher Score 是单变量 分析方法。它独立地评估每一个特征。这意味着,如果特征 A 和特征 B 完全相关(例如:一个是英寸,一个是厘米),它们都会获得高分。如果你的模型对多重共线性敏感(比如线性回归),你还需要后续配合相关性分析来去重。
总结
Fisher Score 是机器学习工程师手中一把精准的“手术刀”。它利用类间距离和类内紧密度的数学直觉,帮助我们从杂乱的数据中提炼出最精华的信息。
通过这篇文章,我们不仅理解了其背后的 $\frac{\text{类间方差}}{\text{类内方差}}$ 的数学逻辑,还通过 Python 代码实践了从基础数据集到复杂文本分类的处理流程。
你的下一步行动:
下次当你拿到一个新的高维数据集时,不要急着直接丢进模型。试着先把 Fisher Score 这一环加入你的预处理 Pipeline 中。画出特征的得分分布图,观察一下数据的本质。你会发现,更好的数据预处理,往往比调整模型的超参数更能带来性能的提升。