首届世界科学智能大赛:生命科学赛道——生物学年龄评价与年龄相关疾病风险预测(第二次笔记)

首届世界科学智能大赛:生命科学赛道——生物学年龄评价与年龄相关疾病风险预测(第二次笔记)

背景介绍见第一次笔记: http://t.csdn.cn/ylDRW

在任务一的 Baseline 代码提供了一个基本的框架,但可能需要进一步优化和改进,以提高模型的性能。以下是一些可能的改进方向:

特征工程:可以尝试更复杂的特征工程,包括基于领域知识的特征构建,以及使用降维技术来减少特征数量。

模型调优:可以对模型的超参数进行更详细的调优,包括学习率、树的深度、迭代次数等。还可以考虑尝试不同的梯度提升树模型,如XGBoost、LightGBM等。

模型融合:考虑使用模型融合技术,将多个模型的预测结果进行组合,以提高预测性能。

特征选择:通过分析特征重要性,可以选择最重要的特征,以减少模型的复杂性和训练时间。

处理缺失值:可以尝试不同的方法来处理缺失值,而不仅仅是替换为0.0。例如,可以使用插值方法来估计缺失值。

模型解释:考虑使用模型解释技术,以更好地理解模型的决策过程和特征重要性。

数据扩充:如果有额外的数据可用,可以考虑将其与训练数据集合并,以增加模型的训练数据量。

其中,在本次竞赛中可以考虑的方向有特征工程、模型调优、模型融合、特征选择、处理缺失值,我们做一些尝试看能不能提高模型的性能。

1.对特征进行进一步优化

在处理大型数据集时,进行特征选择是非常重要的,以减少计算和内存需求,提高模型的效率。特征选择有多种方法,下面介绍一些常用的特征选择技术:

  1. 相关性过滤: 通过计算每个特征与目标变量之间的相关性(例如,Pearson相关系数或Spearman秩相关系数),可以识别与目标变量关系较强的特征。你可以设置一个相关性阈值,选择相关性高于阈值的特征。

  2. 方差过滤: 移除方差很低的特征,因为它们可能对模型的预测没有太大帮助。你可以计算每个特征的方差,然后选择方差高于阈值的特征。

  3. 递归特征消除(Recursive Feature Elimination,RFE): 这是一种递归方法,首先使用所有特征训练模型,然后排除最不重要的特征,然后再次训练模型。这个过程一直重复,直到达到所需数量的特征或者达到性能指标。

  4. L1正则化(Lasso): 使用L1正则化的线性模型(如Lasso回归)可以将一些特征的系数推向零,从而实现特征选择。系数为零的特征可以被移除。

  5. 树模型特征重要性: 随机森林、XGBoost、LightGBM等树模型可以提供每个特征的重要性分数。你可以根据重要性分数选择排名靠前的特征。

  6. 特征选择库: 有一些Python库专门用于特征选择,如scikit-learn中的SelectKBestSelectFromModel,以及boruta等库。这些库提供了方便的工具来执行各种特征选择方法。

  7. 特征降维技术: 除了特征选择,特征降维技术如主成分分析(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值在甲基化数据中的含义可以归纳为以下几种情况:

  1. 未检测到甲基化: 在某些情况下,实验技术可能无法准确检测到某些位点的甲基化状态,导致在这些位点上的值被标记为NaN。这可能是因为技术灵敏度不足或者存在干扰因素。

  2. 测量误差: 在其他情况下,NaN值可能是由于测量误差引起的,即在测量甲基化时出现了随机或系统性的误差。这些误差可能源自实验过程中的各种因素,包括样本制备、测序等。

  3. 位点缺失: 在某些情况下,位点本身可能在某些样本中确实没有甲基化。这可能是因为特定的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的效果要好

猜你喜欢

转载自blog.csdn.net/qq_42859625/article/details/132433012