首届世界科学智能大赛:生命科学赛道——生物学年龄评价与年龄相关疾病风险预测(第二次笔记)
背景介绍见第一次笔记: http://t.csdn.cn/ylDRW
在任务一的 Baseline 代码提供了一个基本的框架,但可能需要进一步优化和改进,以提高模型的性能。以下是一些可能的改进方向:
特征工程:可以尝试更复杂的特征工程,包括基于领域知识的特征构建,以及使用降维技术来减少特征数量。
模型调优:可以对模型的超参数进行更详细的调优,包括学习率、树的深度、迭代次数等。还可以考虑尝试不同的梯度提升树模型,如XGBoost、LightGBM等。
模型融合:考虑使用模型融合技术,将多个模型的预测结果进行组合,以提高预测性能。
特征选择:通过分析特征重要性,可以选择最重要的特征,以减少模型的复杂性和训练时间。
处理缺失值:可以尝试不同的方法来处理缺失值,而不仅仅是替换为0.0。例如,可以使用插值方法来估计缺失值。
模型解释:考虑使用模型解释技术,以更好地理解模型的决策过程和特征重要性。
数据扩充:如果有额外的数据可用,可以考虑将其与训练数据集合并,以增加模型的训练数据量。
其中,在本次竞赛中可以考虑的方向有特征工程、模型调优、模型融合、特征选择、处理缺失值,我们做一些尝试看能不能提高模型的性能。
1.对特征进行进一步优化
在处理大型数据集时,进行特征选择是非常重要的,以减少计算和内存需求,提高模型的效率。特征选择有多种方法,下面介绍一些常用的特征选择技术:
-
相关性过滤: 通过计算每个特征与目标变量之间的相关性(例如,Pearson相关系数或Spearman秩相关系数),可以识别与目标变量关系较强的特征。你可以设置一个相关性阈值,选择相关性高于阈值的特征。
-
方差过滤: 移除方差很低的特征,因为它们可能对模型的预测没有太大帮助。你可以计算每个特征的方差,然后选择方差高于阈值的特征。
-
递归特征消除(Recursive Feature Elimination,RFE): 这是一种递归方法,首先使用所有特征训练模型,然后排除最不重要的特征,然后再次训练模型。这个过程一直重复,直到达到所需数量的特征或者达到性能指标。
-
L1正则化(Lasso): 使用L1正则化的线性模型(如Lasso回归)可以将一些特征的系数推向零,从而实现特征选择。系数为零的特征可以被移除。
-
树模型特征重要性: 随机森林、XGBoost、LightGBM等树模型可以提供每个特征的重要性分数。你可以根据重要性分数选择排名靠前的特征。
-
特征选择库: 有一些Python库专门用于特征选择,如
scikit-learn
中的SelectKBest
和SelectFromModel
,以及boruta
等库。这些库提供了方便的工具来执行各种特征选择方法。 -
特征降维技术: 除了特征选择,特征降维技术如主成分分析(PCA)、线性判别分析(LDA)和奇异值分解(SVD)也可以帮助减少特征的数量。
在实际应用中,通常会结合多种特征选择方法来找到最佳的特征子集。特征选择的具体方法取决于数据集的特点和机器学习任务的需求。你可以尝试不同的方法,评估其对模型性能的影响,并选择最适合你的情况的特征选择策略。
1、原始特征进行特征选择,过滤熟悉唯一、缺失率过高、高相关特征;
drop_cols = []
print('过滤异常特征...')
for col in traindata.columns[1:]: # 从第2列开始,第1列是'sample_id'
if traindata[col].nunique() == 1: # 唯一属性值
drop_cols.append(col)
if traindata[col].isnull().sum() / traindata.shape[0] > 0.6: # 缺失率大于0.95
drop_cols.append(col)
print('过滤高相关特征...')
def correlation(data, threshold):
col_corr = set()
numeric_cols = data.select_dtypes(include=[np.number]).columns # 仅选择数值型特征
corr_matrix = data[numeric_cols].corr()
for i in range(len(corr_matrix.columns)):
for j in range(i):
if abs(corr_matrix.iloc[i, j]) > threshold:
colname = corr_matrix.columns[i]
col_corr.add(colname)
return list(col_corr)
selected_cols = [col for col in traindata.columns if col not in drop_cols]
drop_cols += correlation(traindata[selected_cols], 0.98)
# 从特征中移除不需要的列
selected_cols = [col for col in selected_cols if col not in drop_cols]
注意:过滤高相关特征时会因为特征过多占用内存过大导致内核中断
2、提取PCA、LDA、SVD这类降维特征。因为特征纬度过大,读取全部特征或者直接使用全部特征训练要求很大的内存,可以将数据逐组进行降维操作,比如将特征分10组,每组进行PCA、LDA、SVD操作,然后将10组降维后的结果合并起来作为入模特征。
# 特征提取
from sklearn.decomposition import PCA
import numpy as np
n_components = 16 # 设置PCA的降维维度
n_groups = 10 # 将数据分成10组
# 分组降维并合并结果
pca_features = []
for i in range(n_groups):
# 每组的起始和结束索引
start_idx = i * (len(selected_cols) // n_groups)
end_idx = (i + 1) * (len(selected_cols) // n_groups)
# 提取当前组的特征
group_data = traindata[['sample_id'] + selected_cols[start_idx:end_idx]]
# 使用PCA进行降维
pca = PCA(n_components=n_components)
pca_result = pca.fit_transform(group_data.drop(columns=['sample_id']))
# 将降维后的结果合并到pca_features列表中
pca_features.append(pca_result)
# 将降维后的结果水平合并
pca_features = np.hstack(pca_features)
# 创建新的DataFrame并添加降维后的特征
pca_columns = [f'pca_{
i}' for i in range(n_components * n_groups)]
pca_df = pd.DataFrame(data=pca_features, columns=pca_columns)
# 将降维后的特征添加到训练数据中
traindata = pd.concat([traindata, pca_df], axis=1)
这种方法由于PCA需要处理缺失值,暂时没有很好的思路处理
2. 缺失值
要想对缺失值进行处理,我们要明白甲基化数据中的NaN值在生物学上的含义是什么?
在生物学上,甲基化数据中的NaN值通常表示在该特定位点未检测到或未测量到甲基化信息。这可能有多种原因,包括实验技术的限制、测量误差或者某些位点在特定样本中确实没有甲基化。
NaN值在甲基化数据中的含义可以归纳为以下几种情况:
-
未检测到甲基化: 在某些情况下,实验技术可能无法准确检测到某些位点的甲基化状态,导致在这些位点上的值被标记为NaN。这可能是因为技术灵敏度不足或者存在干扰因素。
-
测量误差: 在其他情况下,NaN值可能是由于测量误差引起的,即在测量甲基化时出现了随机或系统性的误差。这些误差可能源自实验过程中的各种因素,包括样本制备、测序等。
-
位点缺失: 在某些情况下,位点本身可能在某些样本中确实没有甲基化。这可能是因为特定的DNA序列在某些样本中没有甲基化位点,因此在这些样本中的数据被标记为NaN。
在分析甲基化数据时,处理NaN值非常重要。通常,处理NaN值的方法包括数据填充(用平均值、中位数等替代NaN值)、删除包含NaN值的样本或位点、使用统计方法估算NaN值等,具体取决于研究的目标和数据的性质。确保根据研究需求和数据特点来选择合适的NaN值处理策略,以确保分析的准确性和可靠性。
在这里我统计了前10个特征的缺失率,如下:
特征0,对应的缺失率为0.5364994534191667
特征1,对应的缺失率为0.570144540264788
特征2,对应的缺失率为0.9562735333414308
特征3,对应的缺失率为0.5799829952629662
特征4,对应的缺失率为0.6345196161787927
特征5,对应的缺失率为0.42791206121705333
特征6,对应的缺失率为0.4956880845378355
特征7,对应的缺失率为0.530547795457306
特征8,对应的缺失率为0.5468237580468844
特征9,对应的缺失率为0.5464593708247297
可以看到,甲基化数据的缺失率很高。因此在处理甲基化数据时,可以首先评估这些NaN值是否包含有用信息。如果缺失值与研究问题相关,并且不能简单地用统计值填充,那么可以考虑保留这些NaN值并将其视为有意义的类别。
如果NaN值对于研究问题不重要,你可以选择删除包含NaN值的样本或特征,或使用统计值来填充NaN值。最终的决策应该基于领域知识、数据分析的需要以及研究目标。在处理NaN值时,建议始终进行记录和文档化,以便将来的数据分析和解释。
在处理甲基化数据中的NaN值时,应该根据数据的性质和研究目标来选择适当的处理方法。以下是一些处理NaN值的常见方法,你可以根据具体情况进行评估和选择:
删除包含NaN的样本或特征:
删除包含NaN值的样本:如果某些样本中有太多的NaN值或者这些NaN值无法合理填充,你可以选择删除这些样本。这样做可能会减少样本数量,但可以确保分析的可靠性。
删除包含NaN值的特征:如果某些特征中的NaN值占比太高,而且这些特征对于问题的解决没有重要性,可以选择删除这些特征。
填充NaN值:
使用平均值、中位数、众数等统计值来填充NaN值:
对于连续型数据,你可以使用该特征的平均值、中位数或其他统计值来填充NaN值。
使用固定值进行填充:有时候可以根据领域知识或实验设计,选择一个合适的固定值来填充NaN值。
使用插值方法进行填充:对于时间序列等数据,插值方法(如线性插值或插值法)可以用来填充NaN值。
建立模型进行填充:
如果你认为NaN值的分布与其他特征相关,可以考虑使用机器学习模型(如回归模型或分类模型)来预测NaN值。
由于这里的数据难以判断他缺失的类型,因此这里暂不对缺失值专门做处理
3. 模型融合
将多个效果较好的模型的预测结果进行组合,在这里是可以行得通的!
模型融合(集成学习)主要有两种策略:
1. bagging
Bagging 的思路是所有基础模型都一致对待,每个基础模型手里都只有一票。然后使用民主投票的方式得到最终的结果。
大部分情况下,经过 bagging 得到的结果方差(variance)更小
我们构建了cv_model函数,内部可以选择使用lightgbm、xgboost和catboost模型,可以依次跑完这三个模型,然后将三个模型的结果进行取平均进行融合。
def cv_model(clf, train_x, train_y, test_x, clf_name, seed = 2023):
'''
clf:调用模型
train_x:训练数据
train_y:训练数据对应标签
test_x:测试数据
clf_name:选择使用模型名
seed:随机种子
'''
folds = 5
kf = KFold(n_splits=folds, shuffle=True, random_state=seed)
oof = np.zeros(train_x.shape[0])
test_predict = np.zeros(test_x.shape[0])
cv_scores = []
for i, (train_index, valid_index) in enumerate(kf.split(train_x, train_y)):
print('************************************ {} ************************************'.format(str(i+1)))
trn_x, trn_y, val_x, val_y = train_x.iloc[train_index], train_y[train_index], train_x.iloc[valid_index], train_y[valid_index]
if clf_name == "lgb":
train_matrix = clf.Dataset(trn_x, label=trn_y)
valid_matrix = clf.Dataset(val_x, label=val_y)
params = {
'boosting_type': 'gbdt',
'objective': 'regression',
'metric': 'mae',
'min_child_weight': 6,
'num_leaves': 2 ** 6,
'lambda_l2': 10,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 4,
'learning_rate': 0.1,
'seed': 2023,
'nthread' : 16,
'verbose' : -1,
}
model = clf.train(params, train_matrix, 2000, valid_sets=[train_matrix, valid_matrix],
categorical_feature=[], verbose_eval=200, early_stopping_rounds=100)
val_pred = model.predict(val_x, num_iteration=model.best_iteration)
test_pred = model.predict(test_x, num_iteration=model.best_iteration)
if clf_name == "xgb":
xgb_params = {
'booster': 'gbtree',
'objective': 'reg:squarederror',
'eval_metric': 'mae',
'max_depth': 5,
'lambda': 10,
'subsample': 0.7,
'colsample_bytree': 0.7,
'colsample_bylevel': 0.7,
'eta': 0.1,
'tree_method': 'hist',
'seed': 520,
'nthread': 16
}
train_matrix = clf.DMatrix(trn_x , label=trn_y)
valid_matrix = clf.DMatrix(val_x , label=val_y)
test_matrix = clf.DMatrix(test_x)
watchlist = [(train_matrix, 'train'),(valid_matrix, 'eval')]
model = clf.train(xgb_params, train_matrix, num_boost_round=2000, evals=watchlist, verbose_eval=200, early_stopping_rounds=100)
val_pred = model.predict(valid_matrix)
test_pred = model.predict(test_matrix)
if clf_name == "cat":
params = {
'learning_rate': 0.1, 'depth': 5, 'bootstrap_type':'Bernoulli','random_seed':2023,
'od_type': 'Iter', 'od_wait': 100, 'random_seed': 11, 'allow_writing_files': False}
model = clf(iterations=2000, **params)
model.fit(trn_x, trn_y, eval_set=(val_x, val_y),
metric_period=200,
use_best_model=True,
cat_features=[],
verbose=1)
val_pred = model.predict(val_x)
test_pred = model.predict(test_x)
oof[valid_index] = val_pred
test_predict += test_pred / kf.n_splits
score = mean_absolute_error(val_y, val_pred)
cv_scores.append(score)
print(cv_scores)
return oof, test_predict
# 选择lightgbm模型
lgb_oof, lgb_test = cv_model(lgb, traindata[cols], traindata['label'], testdata[cols], 'lgb')
# 选择xgboost模型
xgb_oof, xgb_test = cv_model(xgb, traindata[cols], traindata['label'], testdata[cols], 'xgb')
# 选择catboost模型
cat_oof, cat_test = cv_model(CatBoostRegressor, traindata[cols], traindata['label'], testdata[cols], 'cat')
# 进行取平均融合
final_test = (lgb_test + xgb_test + cat_test) / 3
2.boosting
Boosting 和 bagging 最本质的差别在于他对基础模型不是一致对待的,而是经过不停的考验和筛选来挑选出「精英」,然后给精英更多的投票权,表现不好的基础模型则给较少的投票权,然后综合所有人的投票得到最终结果。
大部分情况下,经过 boosting 得到的结果偏差(bias)更小。
4. 适量增加读取的特征数量、增加iterations迭代次数可以继续提高性能,注意要防止读取特征数量过多,造成内核崩溃
实践单纯调这两个参数,可以score降到3.5以下
总结:
- 一次性读取10万个特征扛不住,老是内核崩掉
- 缺失值该如何处理
- catboost是否一定比xgboost和lightgbm的效果要好