153 lines
6.2 KiB
Python
153 lines
6.2 KiB
Python
#!/usr/bin/env python
|
||
# -*- encoding: utf-8 -*-
|
||
'''
|
||
@file: RFE_cuml_permutation.py
|
||
@Description: 使用GPU cuML实现特征选择,手动实现RFECV,并通过置换重要性计算来剔除最不重要的特征
|
||
@Date: 2025/03/01
|
||
@Author: lyzeng
|
||
'''
|
||
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
# ---------------------------
|
||
# 数据加载与特征构造
|
||
# ---------------------------
|
||
data = pd.read_csv("../data_smi.csv", index_col="Entry ID")
|
||
target_data = data[["SMILES", "S.aureus ATCC25923"]]
|
||
target_data.columns = ["SMILES", "TARGET"]
|
||
|
||
from rdkit import Chem
|
||
from rdkit.Chem import Descriptors
|
||
|
||
def getMolDescriptors(smiles: str, missingVal=None):
|
||
mol = Chem.MolFromSmiles(smiles)
|
||
res = {}
|
||
for nm, fn in Descriptors._descList:
|
||
try:
|
||
val = fn(mol)
|
||
except Exception as e:
|
||
val = missingVal
|
||
res[nm] = val
|
||
return res
|
||
|
||
# 构建特征矩阵与目标变量
|
||
X_df = pd.DataFrame([getMolDescriptors(smi) for smi in target_data['SMILES']])
|
||
y = target_data['TARGET'].values
|
||
|
||
# 剔除无效特征(取值唯一的列)
|
||
invalid_features = [col for col in X_df.columns if X_df[col].nunique() <= 1]
|
||
X_df.drop(columns=invalid_features, inplace=True)
|
||
X = X_df.values
|
||
|
||
print(f"训练样本数: {len(y)}, 特征数量: {X.shape[1]}")
|
||
|
||
# ---------------------------
|
||
# 转换为GPU数据格式(cupy数组)
|
||
# ---------------------------
|
||
import cupy as cp
|
||
X_gpu = cp.asarray(X)
|
||
y_gpu = cp.asarray(y)
|
||
|
||
# ---------------------------
|
||
# 定义辅助函数:计算模型的MSE
|
||
# ---------------------------
|
||
def model_mse(model, X_data, y_data):
|
||
y_pred = model.predict(X_data)
|
||
mse = cp.mean((y_data - y_pred) ** 2)
|
||
return mse.get()
|
||
|
||
# ---------------------------
|
||
# 定义置换重要性计算函数(在GPU上)
|
||
# ---------------------------
|
||
def permutation_importance_gpu(model, X_data, y_data, n_repeats=3, random_state=0):
|
||
"""
|
||
对于给定的模型,计算每个特征的置换重要性:
|
||
重要性 = (打乱特征后的MSE均值) - (原始MSE)
|
||
随机打乱每个特征的值,并观察模型性能的下降程度来衡量特征的重要性。 如果打乱某个特征导致模型性能显著下降,说明该特征对模型很重要;反之,如果打乱某个特征对模型性能没有明显影响,说明该特征不太重要。
|
||
"""
|
||
# 先将数据复制到CPU进行打乱操作(数据量较小时不会有太大开销)
|
||
X_cpu = cp.asnumpy(X_data)
|
||
baseline = model_mse(model, X_data, y_data)
|
||
importances = np.zeros(X_cpu.shape[1])
|
||
rng = np.random.RandomState(random_state)
|
||
for j in range(X_cpu.shape[1]):
|
||
scores = []
|
||
for _ in range(n_repeats):
|
||
X_permuted = X_cpu.copy()
|
||
rng.shuffle(X_permuted[:, j])
|
||
X_permuted_gpu = cp.asarray(X_permuted)
|
||
score = model_mse(model, X_permuted_gpu, y_data)
|
||
scores.append(score)
|
||
importances[j] = np.mean(scores) - baseline
|
||
return importances
|
||
|
||
# ---------------------------
|
||
# 使用 cuML 随机森林和置换重要性手动实现 RFECV
|
||
# ---------------------------
|
||
from cuml.ensemble import RandomForestRegressor as cuRF
|
||
from sklearn.model_selection import KFold
|
||
|
||
def cross_val_score_gpu(X_data, y_data, cv=5):
|
||
"""
|
||
使用 KFold 分割数据,利用 cuML 随机森林评估当前特征子集的CV均方误差
|
||
"""
|
||
kf = KFold(n_splits=cv, shuffle=True, random_state=0)
|
||
mse_scores = []
|
||
# 使用 CPU 索引进行分割
|
||
X_np = cp.asnumpy(X_data)
|
||
for train_index, test_index in kf.split(X_np):
|
||
X_train = X_data[train_index, :]
|
||
X_test = X_data[test_index, :]
|
||
y_train = y_data[train_index]
|
||
y_test = y_data[test_index]
|
||
model = cuRF(n_estimators=100, random_state=0)
|
||
model.fit(X_train, y_train)
|
||
mse = model_mse(model, X_test, y_test)
|
||
mse_scores.append(mse)
|
||
return np.mean(mse_scores)
|
||
|
||
def manual_rfecv(X_data, y_data, cv=5):
|
||
"""
|
||
手动实现 RFECV:
|
||
- 在当前特征子集上计算CV MSE
|
||
- 使用置换重要性评估各特征的重要性
|
||
- 移除置换重要性最低的特征
|
||
- 记录使CV MSE最小的特征组合
|
||
找到一个最佳的特征组合,用于训练最终的模型。 这个子集的大小在每次迭代中都在变化,最终目标是找到一个使交叉验证 MSE 最小的子集
|
||
cuml.ensemble.RandomForestRegressor 的 max_features 默认值通常是 sqrt (特征总数的平方根)。 这与 scikit-learn 的 RandomForestRegressor 的默认值相同。
|
||
"""
|
||
n_features = X_data.shape[1]
|
||
features = list(range(n_features))
|
||
best_score = float('inf')
|
||
best_features = features.copy()
|
||
scores_history = []
|
||
|
||
while len(features) > 0:
|
||
current_score = cross_val_score_gpu(X_data[:, features], y_data, cv=cv)
|
||
scores_history.append((features.copy(), current_score))
|
||
print(f"当前特征数: {len(features)}, CV MSE: {current_score:.4f}")
|
||
if current_score < best_score:
|
||
best_score = current_score
|
||
best_features = features.copy()
|
||
if len(features) == 1:
|
||
break
|
||
# 训练模型并计算置换重要性
|
||
model = cuRF(n_estimators=100, random_state=0) # cuml.ensemble.RandomForestRegressor 的 max_features 默认值通常是 sqrt (特征总数的平方根)。 这与 scikit-learn 的 RandomForestRegressor 的默认值相同。
|
||
model.fit(X_data[:, features], y_data)
|
||
imp = permutation_importance_gpu(model, X_data[:, features], y_data, n_repeats=3, random_state=0)
|
||
idx_to_remove = int(np.argmin(imp))
|
||
removed_feature = features[idx_to_remove]
|
||
print(f"剔除特征索引: {removed_feature},置换重要性: {imp[idx_to_remove]:.4f}")
|
||
del features[idx_to_remove]
|
||
|
||
return best_features, best_score, scores_history
|
||
|
||
# 执行手动 RFECV,使用5折交叉验证
|
||
cv_folds = 5
|
||
best_features_rfecv, best_mse_rfecv, history = manual_rfecv(X_gpu, y_gpu, cv=cv_folds)
|
||
|
||
print(f"\n手动RFECV选择了 {len(best_features_rfecv)} 个特征,CV MSE: {best_mse_rfecv:.4f}")
|
||
selected_feature_names = [X_df.columns[i] for i in best_features_rfecv]
|
||
print("选定特征名称:", selected_feature_names)
|