feat: PhysicalFeature 扩展到全部63个水色指数 + MNF成分数自动确定

PhysicalFeatureExtractor:
- 废弃硬编码3指数(NDVI/NDWI/AlgaeRatio),改为调用
  WaterQualityIndexCalculator.calculate_many() 计算全部63个公式
- 涵盖 BGA(25)/Chl(16)/Turb(10)/TSM(3) 等完整指数集

MNFTransformer:
- n_components 支持 float(0~1): 根据累积方差比自动确定成分数
  n_components=0.95 → 保留95%方差的成分
- fit 后保存 n_components_ (实际成分数) 供部署导出使用
- Wp 显式按特征值降序排列
This commit is contained in:
duxin
2026-07-29 10:37:09 +08:00
parent 495857913d
commit 58bad4081e
2 changed files with 41 additions and 60 deletions

View File

@ -774,7 +774,7 @@ class WaterQualityModelingBatch:
_deploy = { _deploy = {
'mnf_mean': np.asarray(_mnf.mean_).ravel(), 'mnf_mean': np.asarray(_mnf.mean_).ravel(),
'mnf_W': np.asarray(_mnf.W_mnf_), 'mnf_W': np.asarray(_mnf.W_mnf_),
'mnf_n_components': int(_mnf.n_components), 'mnf_n_components': int(getattr(_mnf, 'n_components_', _mnf.n_components)),
} }
# SVR 部署矩阵rbf kernel 需要 support_vectors_ # SVR 部署矩阵rbf kernel 需要 support_vectors_
_deploy['svr_dual_coef'] = np.asarray(_svr.dual_coef_) _deploy['svr_dual_coef'] = np.asarray(_svr.dual_coef_)

View File

@ -369,11 +369,11 @@ class MNFTransformer(TransformerMixin, BaseEstimator, _ArrayAsFloat64):
Parameters Parameters
---------- ----------
n_components : int, default=10 n_components : int or float, default=0.95
保留的 MNF 成分数量(降维后的特征维度) int 时保留固定数量的成分float(0~1) 时根据累积方差比自动确定
""" """
def __init__(self, n_components: int = 10): def __init__(self, n_components=0.95):
self.n_components = n_components self.n_components = n_components
def fit(self, X, y=None): def fit(self, X, y=None):
@ -382,18 +382,16 @@ class MNFTransformer(TransformerMixin, BaseEstimator, _ArrayAsFloat64):
n_samples, n_features = X.shape n_samples, n_features = X.shape
# 1) 噪声矩阵:相邻行差分 # 1) 噪声矩阵:相邻行差分
noise = X[1:] - X[:-1] # (n-1, p) noise = X[1:] - X[:-1]
Cn = np.cov(noise, rowvar=False) # (p, p) Cn = np.cov(noise, rowvar=False)
# 正则化:按总方差的 1% 加对角项,强于固定 1e-9
_reg = np.trace(Cn) / n_features * 0.01 _reg = np.trace(Cn) / n_features * 0.01
Cn += np.eye(n_features) * max(_reg, 1e-6) Cn += np.eye(n_features) * max(_reg, 1e-6)
# 2) 噪声白化:仅对显著特征值做白化,忽略噪声主导的成分 # 2) 噪声白化
eigvals_n, eigvecs_n = np.linalg.eigh(Cn) eigvals_n, eigvecs_n = np.linalg.eigh(Cn)
# 截断:特征值 < max * 1e-4 的视为噪声,不参与白化(置单位矩阵)
_keep_mask = eigvals_n > eigvals_n.max() * 1e-4 _keep_mask = eigvals_n > eigvals_n.max() * 1e-4
if _keep_mask.sum() < 2: if _keep_mask.sum() < 2:
_keep_mask[:2] = True # 至少保留 2 个成分 _keep_mask[:2] = True
_kept_vals = np.where(_keep_mask, eigvals_n, 1.0) _kept_vals = np.where(_keep_mask, eigvals_n, 1.0)
inv_sqrt = np.where(_keep_mask, 1.0 / np.sqrt(np.maximum(_kept_vals, 1e-12)), 1.0) inv_sqrt = np.where(_keep_mask, 1.0 / np.sqrt(np.maximum(_kept_vals, 1e-12)), 1.0)
Wn = eigvecs_n @ np.diag(inv_sqrt) @ eigvecs_n.T Wn = eigvecs_n @ np.diag(inv_sqrt) @ eigvecs_n.T
@ -405,9 +403,21 @@ class MNFTransformer(TransformerMixin, BaseEstimator, _ArrayAsFloat64):
X_w = (X - self.mean_) @ Wn X_w = (X - self.mean_) @ Wn
Cw = np.cov(X_w, rowvar=False) Cw = np.cov(X_w, rowvar=False)
eigvals_w, eigvecs_w = np.linalg.eigh(Cw) eigvals_w, eigvecs_w = np.linalg.eigh(Cw)
Wp = eigvecs_w[:, ::-1] # 降序排列
_order = np.argsort(eigvals_w)[::-1]
eigvals_w = eigvals_w[_order]
eigvecs_w = eigvecs_w[:, _order]
Wp = eigvecs_w
# 5) 最终变换矩阵 # 5) 自动确定成分数
if isinstance(self.n_components, float) and 0 < self.n_components <= 1:
_cumvar = np.cumsum(np.maximum(eigvals_w, 0)) / np.sum(np.maximum(eigvals_w, 0))
self.n_components_ = int(np.searchsorted(_cumvar, self.n_components)) + 1
self.n_components_ = max(2, min(self.n_components_, n_features))
else:
self.n_components_ = int(self.n_components)
# 6) 最终变换矩阵
self.W_mnf_ = Wn @ Wp self.W_mnf_ = Wn @ Wp
return self return self
@ -415,8 +425,7 @@ class MNFTransformer(TransformerMixin, BaseEstimator, _ArrayAsFloat64):
def transform(self, X): def transform(self, X):
X = self._to_ndarray(X) X = self._to_ndarray(X)
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0) X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
result = (X - self.mean_) @ self.W_mnf_[:, :self.n_components] result = (X - self.mean_) @ self.W_mnf_[:, :self.n_components_]
# 防 SVR 拒绝 infclip 到 float32 安全范围
result = np.nan_to_num(result, nan=0.0, posinf=0.0, neginf=0.0) result = np.nan_to_num(result, nan=0.0, posinf=0.0, neginf=0.0)
result = np.clip(result, -1e15, 1e15) result = np.clip(result, -1e15, 1e15)
return result return result
@ -427,39 +436,17 @@ class MNFTransformer(TransformerMixin, BaseEstimator, _ArrayAsFloat64):
# ============================================================================ # ============================================================================
class PhysicalFeatureExtractor(TransformerMixin, BaseEstimator, _ArrayAsFloat64): class PhysicalFeatureExtractor(TransformerMixin, BaseEstimator, _ArrayAsFloat64):
"""从原始高光谱波段中提取物理意义明确的比值/指数特征 """从原始高光谱波段计算全部水质/植被指数63 个公式)
输入 X 为 pandas DataFrame 时,列名应为波长字符串(如 "800.000000" 利用项目已有的 WaterQualityIndexCalculator自动根据 wavelength 列名
输入为 numpy ndarray 时,需在 fit 中传入 wavelengths 参数 匹配各公式所需的最近波段,计算涵盖 Chl/BGA/Turb/TSM 的完整指数集
计算的特征包括: 输入 X 为 DataFrame 时自动解析列名ndarray 时需在 __init__ 传入 wavelengths。
- NDVI: (R800 - R670) / (R800 + R670)
- NDWI: (R550 - R800) / (R550 + R800)
- 蓝藻比值: R705 / R670
Parameters
----------
wavelengths : list of float or None
ndarray 输入时需显式传入波长列表DataFrame 输入时自动从列名解析。
""" """
_TARGET_NAMES = ('NDVI', 'NDWI', 'AlgaeRatio')
# 默认目标波长 (nm) — 通过 fit 时扫描实际波长列自动修正为最接近值
_TARGET_WL = {
'NDVI': (800.0, 670.0),
'NDWI': (550.0, 800.0),
'AlgaeRatio': (705.0, 670.0),
}
def __init__(self, wavelengths=None): def __init__(self, wavelengths=None):
self.wavelengths = wavelengths self.wavelengths = wavelengths
def _find_nearest_wl(self, wl_array, target):
"""返回最接近 target 的波长值"""
idx = np.argmin(np.abs(np.asarray(wl_array) - target))
return float(wl_array[idx]), int(idx)
def fit(self, X, y=None): def fit(self, X, y=None):
if isinstance(X, pd.DataFrame): if isinstance(X, pd.DataFrame):
self.wavelengths = [float(str(c)) for c in X.columns] self.wavelengths = [float(str(c)) for c in X.columns]
@ -467,29 +454,23 @@ class PhysicalFeatureExtractor(TransformerMixin, BaseEstimator, _ArrayAsFloat64)
raise ValueError( raise ValueError(
"ndarray 输入时必须在 __init__ 中提供 wavelengths 参数" "ndarray 输入时必须在 __init__ 中提供 wavelengths 参数"
) )
# 预计算每个特征对应的列索引 # 预加载公式列表fit 时只做一次)
self._feat_cols_ = [] from src.utils.water_index import WaterQualityIndexCalculator
wl_arr = self.wavelengths self._calc = WaterQualityIndexCalculator()
for name, (wl_a, wl_b) in self._TARGET_WL.items(): self._formulas = self._calc.list_available()
_, ia = self._find_nearest_wl(wl_arr, wl_a) self._n_features_out_ = len(self._formulas)
_, ib = self._find_nearest_wl(wl_arr, wl_b)
self._feat_cols_.append((name, ia, ib))
return self return self
def transform(self, X): def transform(self, X):
X = self._to_ndarray(X) X = self._to_ndarray(X)
feats = [] # 重建 DataFrame列名 = 波长),供 WaterQualityIndexCalculator 使用
for name, ia, ib in self._feat_cols_: col_names = [f'{wl:.6f}' for wl in self.wavelengths]
a = X[:, ia] df = pd.DataFrame(X, columns=col_names)
b = X[:, ib] result_df = self._calc.calculate_many(self._formulas, df, fast=True)
denom = a + b out = np.asarray(result_df, dtype=np.float64)
denom = np.where(np.abs(denom) < 1e-12, np.sign(denom) * 1e-12, denom) out = np.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0)
if name == 'AlgaeRatio': out = np.clip(out, -1e15, 1e15)
denom2 = np.where(np.abs(b) < 1e-12, np.sign(b) * 1e-12, b) return out
feats.append((a / denom2).reshape(-1, 1))
else:
feats.append(((a - b) / denom).reshape(-1, 1))
return np.hstack(feats)
# ============================================================================ # ============================================================================
@ -535,7 +516,7 @@ def get_preprocessing_transformer(method: str, wavelengths=None):
from sklearn.pipeline import FeatureUnion from sklearn.pipeline import FeatureUnion
return FeatureUnion([ return FeatureUnion([
('physical', PhysicalFeatureExtractor(wavelengths=wavelengths)), ('physical', PhysicalFeatureExtractor(wavelengths=wavelengths)),
('mnf', MNFTransformer(n_components=10)), ('mnf', MNFTransformer()), # n_components=0.95 自动确定
]) ])
if method not in _PREPROCESSING_TRANSFORMERS: if method not in _PREPROCESSING_TRANSFORMERS:
print(f"未知预处理方法 '{method}',回退为 IdentityTransformer") print(f"未知预处理方法 '{method}',回退为 IdentityTransformer")