当一张图像被摊平成特征向量时,相邻像素的关系很容易丢失。scikit-learn 的这个例子把 40×40 的平滑随机图像作为输入,用两个已知的局部区域构造回归目标,再比较两条降维路径:逐个挑选与目标相关的像素,或借助空间邻接关系把像素合并为区域。两条路径都接入贝叶斯岭回归,最后把降维空间中的权重映回网格。
本文根据 scikit-learn developers 的 Feature agglomeration vs. univariate selection 完整示例翻译整理,覆盖数据生成、参数搜索、逆变换、绘图与临时缓存管理。2026 年 10 月 5 日读取的 stable 页面标识为 scikit-learn 1.9.1;本文未安装依赖或执行示例。编辑示意图根据算法与常量绘制,原文结果图另附;均不是本文重新运行所得。

构造一个知道答案的空间问题
例子只有 200 个样本,却有 1600 个像素特征。真实系数矩阵 coef 初始为零:左上角 15×15 区域赋值 −1,右下角 15×15 区域赋值 +1。这样可以直接观察估计方法能否找回两块影响目标的区域,而不是只看一个预测分数。
输入先从独立标准正态随机数生成,再对每个 40×40 样本施加 sigma=1.0 的高斯平滑,让相邻像素出现结构。随后按列中心化并除以标准差,目标为标准化输入与真实系数向量的点积,再加上经过幅度调整的随机噪声。
# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause
# 整理版:导入与临时缓存管理见下文说明;未执行。
import tempfile
import matplotlib.pyplot as plt
import numpy as np
from joblib import Memory
from scipy import linalg, ndimage
from sklearn import feature_selection
from sklearn.cluster import FeatureAgglomeration
from sklearn.feature_extraction.image import grid_to_graph
from sklearn.linear_model import BayesianRidge
from sklearn.model_selection import GridSearchCV, KFold
from sklearn.pipeline import Pipeline
n_samples = 200
size = 40
roi_size = 15
snr = 5.0
np.random.seed(0)
coef = np.zeros((size, size))
coef[:roi_size, :roi_size] = -1.0
coef[-roi_size:, -roi_size:] = 1.0
X = np.random.randn(n_samples, size**2)
for x in X:
x[:] = ndimage.gaussian_filter(
x.reshape(size, size), sigma=1.0
).ravel()
# 保留原例流程:全体样本上标准化,仅用于此教学演示。
X -= X.mean(axis=0)
X /= X.std(axis=0)
y = np.dot(X, coef.ravel())
noise = np.random.randn(y.shape[0])
noise_coef = (
linalg.norm(y, 2) / np.exp(snr / 20.0)
) / linalg.norm(noise, 2)
y += noise_coef * noise
这里的 snr 是原例变量名。公式实际使用 np.exp(snr / 20.0),也就是以自然常数为底的指数;不能直接把 snr=5.0 宣称为采用常见以 10 为底定义的“5 dB 信噪比”。本稿保留原式,只说明其作用是按信号与噪声的二范数调整噪声幅度。
另一个重要边界是标准化顺序:X.mean 和 X.std 使用了全部 200 个样本,之后才进行交叉验证。这保留了原教学图的构造方式,却不适合据此报告严格的泛化成绩。真实评价应把数据划分和所有需要学习参数的预处理边界安排好,后文再说明。
第一条路径:用 Ward 将相邻特征合并
grid_to_graph(n_x=size, n_y=size) 建立像素网格的邻接图。FeatureAgglomeration 聚类的是特征,即这里的像素列,不是把 200 张图像分组。Ward 合并根据特征在样本上的变化选择区域,连接图限制可合并的空间邻接关系。
聚合后的每个区域产生一个特征;默认池化函数是均值。若聚成 20 个区域,回归器面对的是 20 个区域平均值,而不是 1600 个单独像素。区域数太少可能把不同信号混合,太多又可能失去降维的作用,因此原例搜索 10、20、30 三种候选。
第二条路径:按单变量统计量保留像素
另一条路径用 f_regression 对每个特征与响应之间的线性关系打分,再由 SelectPercentile 保留高分特征。它没有使用网格邻接关系,因此可以选中离散位置;高度相关的相邻像素也可能得到相近评分。
原页将该方法称为 ANOVA 单变量筛选,但实际调用的是回归用的 f_regression,不是分类用的 f_classif。这里搜索保留 5%、10%、20% 的特征,分别对应 1600 个特征中的约 80、160、320 个。
把降维与回归放在同一条搜索流水线中
两条路径都使用 BayesianRidge,并以 KFold(2) 做参数选择。原例没有开启 shuffle,两个候选流程看到相同的折划分。GridSearchCV 在每个训练折里拟合降维器与回归器,选择参数后按默认行为在完整输入上重新拟合最佳流水线。
下面保留两条搜索的统计流程与候选值,但把原来的 mkdtemp() 加末尾 shutil.rmtree(..., ignore_errors=True) 改成 TemporaryDirectory 上下文。这样清理目标始终是本段创建的临时目录,异常离开时也会尝试清理;不会让读者把递归删除目标改成共享或个人目录。步骤访问改用 named_steps,便于对应概念。
cv = KFold(2)
connectivity = grid_to_graph(n_x=size, n_y=size)
with tempfile.TemporaryDirectory(prefix="wwj-spatial-cache-") as cachedir:
mem = Memory(location=cachedir, verbose=1)
ward_pipeline = Pipeline([
("ward", FeatureAgglomeration(
n_clusters=10,
connectivity=connectivity,
memory=mem,
)),
("ridge", BayesianRidge()),
])
ward_search = GridSearchCV(
ward_pipeline,
{"ward__n_clusters": [10, 20, 30]},
n_jobs=1,
cv=cv,
)
ward_search.fit(X, y)
best_ward = ward_search.best_estimator_
ward_fitted = best_ward.named_steps["ward"]
ward_weights = best_ward.named_steps["ridge"].coef_
coef_agglomeration = ward_fitted.inverse_transform(
ward_weights
).reshape(size, size)
cached_f = mem.cache(feature_selection.f_regression)
selection_pipeline = Pipeline([
("anova", feature_selection.SelectPercentile(cached_f)),
("ridge", BayesianRidge()),
])
selection_search = GridSearchCV(
selection_pipeline,
{"anova__percentile": [5, 10, 20]},
cv=cv,
)
selection_search.fit(X, y)
best_selection = selection_search.best_estimator_
selector = best_selection.named_steps["anova"]
selected_weights = best_selection.named_steps["ridge"].coef_
coef_selection = selector.inverse_transform(
selected_weights.reshape(1, -1)
).reshape(size, size)
Memory 缓存 Ward 树或 F 统计计算,以减少同一搜索中的重复工作。缓存是执行优化,不会把交叉验证自动变成独立测试。缓存中可能保存训练数据的派生信息,应使用可信、受控的目录;不要从别人可修改的缓存位置加载 joblib 序列化结果。本文只检查了这些调用与文件边界,没有运行它们。
原页面展示了 Memory 日志以及脚本总耗时,但这些是源站构建示例时的输出,不能作为本机性能保证。本稿不补造最佳簇数、最佳筛选比例、分数或耗时;它们必须通过目标环境运行得到。
逆变换得到的图,究竟表示什么
筛选器的 inverse_transform 会把保留下来的系数放回原来的像素位置,其余特征填零。这能给出同一特征尺度下的稀疏系数图。
聚合器的逆变换则把每个簇的值赋回该簇所有像素。这样容易看出空间分区,却不能直接当作严格等价的原像素线性预测系数。关键在于 FeatureAgglomeration 默认使用均值池化。
设簇 C 的平均特征为 z_C = Σ(x_j) / |C|,回归权重为 w_C。该簇对预测的贡献是 w_C × z_C,所以还原到每个像素时,对应系数应为 w_C / |C|。原示例的展示逆变换只是把 w_C 复制了 |C| 次;如果直接拿这张图与像素作点积,就会把簇贡献放大。
整理版补充,非原例代码:在保持默认均值池化、单输出线性回归的条件下,可以根据每个簇的大小得到标准化特征空间中的等价系数:
labels = ward_fitted.labels_
cluster_sizes = np.bincount(labels)
coef_for_prediction = (
ward_weights[labels] / cluster_sizes[labels]
)
intercept_for_prediction = best_ward.named_steps["ridge"].intercept_
# 数学上对应:X @ coef_for_prediction + intercept_for_prediction
# 应与 best_ward.predict(X) 在浮点容差内核对;本文未执行验证。
若还要还原到标准化之前的原始像素尺度,应进一步除以每列标准差,并对截距减去“原始均值与还原系数的点积”。原例的 coef 真值作用在已经标准化的 X 上,也应在解释图时注明尺度。若修改池化函数为非线性操作,上述线性系数公式就不再直接适用。
绘制图形,但不要把视觉相似等同于方法优胜
原例并排展示真值、单变量筛选与空间聚合图,用红蓝发散色图显示系数正负。以下采用独立 figure,避免原文 plt.close("all") 关闭会话中其他图形;其余展示意图不变。
fig, axes = plt.subplots(1, 3, figsize=(7.3, 2.7))
panels = [
(coef, "True weights"),
(coef_selection, "Feature Selection"),
(coef_agglomeration, "Feature Agglomeration"),
]
for ax, (weights, title) in zip(axes, panels):
ax.imshow(weights, interpolation="nearest", cmap=plt.cm.RdBu_r)
ax.set_title(title)
fig.subplots_adjust(0.04, 0.0, 0.98, 0.94, 0.16, 0.26)
plt.show()
每张图默认各自确定颜色范围,因此相似的红色不一定代表相同的系数幅度。加上聚合逆变换的簇尺度问题,原图适合观察空间支持和分区形态,不足以证明系数幅度恢复准确,也不能单凭更连贯的图认定预测能力更强。
这两个方法体现的是不同的结构假设:单变量筛选评价每个像素与目标的关系,空间聚合先利用相邻特征的共同变化。真实信号若确实连续成块,空间约束可能有帮助;若信号十分离散或邻接定义错误,这种约束也可能抹平差异。原例的两块平滑信号只是一个教学场景。
把演示改成正式评价时,需要改变哪些环节
原例只有两折参数选择,没有未参与搜索的最终测试集。它还在搜索前用全部样本标准化。要评估新样本上的表现,应从原始特征开始划出独立测试集,把 StandardScaler 和降维步骤一起放进被搜索的 Pipeline,让训练折自己学习均值、标准差、分区或筛选结果。scikit-learn 的数据泄漏指南给出了这一边界。
对真实数据,不要先对全量数据做标准化,再在外面多套一层 Pipeline;那样并没有撤销已经发生的信息共享。本合成例的响应本身也是根据全量标准化后的特征生成,若设计严格评测,应重新定义独立于测试集统计量的数据生成与划分流程,并在报告中说明它与原例不同。
参数选择完成后,再以未参与选择的数据报告合适的回归指标,并检查不同随机种子、折数及空间结构下的稳定性。若没有额外测试集,可考虑嵌套交叉验证。用于比较系数幅度时,还应统一原空间系数定义与色标;这些属于进一步实验,不是本文已经取得的结论。
来源、许可与代码检查
原作:scikit-learn developers;原文为前述官方完整示例,代码标识 SPDX-License-Identifier: BSD-3-Clause。中文翻译整理、系数解释及原创示意图:未完纪。
本次仅静态审查:核对了数组尺寸、参数网格、缓存路径、逆变换和绘图范围;没有运行聚类、回归或绘图代码。源例未出现网络请求、shell 拼接或硬编码秘密;缓存与递归清理范围、预处理泄漏、系数尺度及图形状态已逐项标注。这些检查不等于程序不存在漏洞或统计评价已经通过。
与原代码的修改包括 TemporaryDirectory 管理缓存、named_steps 访问步骤、每条流水线独立构造回归器和独立 figure;保留原数据生成、噪声公式、两折选择及候选参数,并显式说明限制。许可证完整声明保留如下:
BSD 3-Clause License Copyright (c) 2007-2026 The scikit-learn developers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
原文图示













暂无评论内容