比赛官网链接:https://tianchi.aliyun.com/competition/entrance/532114/introduction首届世界科学智能大赛:生命科学赛道——生物学年龄评价与年龄相关疾病风险预测_算法大赛_天池大赛-阿里云天池 (aliyun.com)
一、赛题介绍
1、赛事任务
本次比赛提供健康人和年龄相关疾病患者的甲基化数据,需要通过分析甲基化数据的模式和特征建立预测模型,可以根据某个人的甲基化数据来预测其生物学年龄。
任务:构建一个生物学时钟能够评价所提供样本的生物学年龄,
- 对于健康样本,生物学年龄尽可能接近真实年龄。
- 对于患病样本,生物学年龄比真实年龄更大。
2、数据集介绍
公开数据包含10296个样本,其中7833个样本为健康样本。每一个样本提供485512个位点的甲基化数据、年龄与患病情况。抽取80%作为训练样本,20%作为测试样本。
以训练集为例,一共包括8233样本,其中健康样本6266个,其余为患病样本,共涉及Alzheimer's disease,schizophrenia,Parkinson's disease,rheumatoid arthritis,stroke,Huntington's disease,Graves' disease,type 2 diabetes,Sjogren's syndrome等类型。
数据集格式如下:
trainmap.csv
: 每一行代表一个样本
sample_id | age | gender | sample_type | disease | |
---|---|---|---|---|---|
1 | train10001 | 86 | M | disease tissue | Alzheimer's disease |
2 | train10002 | 89 | M | disease tissue | Alzheimer's disease |
3 | train10003 | 80 | F | disease tissue | Alzheimer's disease |
traindata.csv
: 大小为(8233, 485512),每一列代表一个样本,每一行代表一个甲基化位点。
测试集testdata.csv
格式与训练集traindata.csv
格式一致。testmap.csv
仅提供sample_id
与gender
。
3、评价指标
二、赛题理解
1、初步解读
(1)不需要太过深入理解背景,当成一个数值化的任务就行。
(2)评价指标:对于健康样本,就是传统的MAE;对于患病样本,对MAE进行了一个扩展,增加了预测结果与真实结果之间的差异性,更考验模型,需要预测值比真实值大。
(3)数据量大:体现在数据样本不算多,但特征非常多,有48万余个特征,难以一次性将特征全部加载进来。
2、解题思路
(1)这是一个典型的回归问题,数据比较简单,数据量大,存在大量缺失值,可以选择使用传统的机器学习,如树模型XGBoost、CatBoost模型等,不需要进行缺失值的填充,也更稳定,不需要做太多的调参工作。
(2)数据清洗部分:缺失值、异常值的处理。
(3)特征工程部分,不需要考虑构造更多的特征,要想办法加入更多的原始特征。
三、Baseline精读
1、导入需要用到的相关库与模块
# 导入numpy库,用于进行数值计算
import numpy as np
# 导入pandas库,用于数据处理和分析
import pandas as pd
# 导入polars库,用于处理大规模数据集
import polars as pl
# 导入collections库中的defaultdict和Counter,用于统计
from collections import defaultdict, Counter
# 导入xgboost库,用于梯度提升树模型
import xgb
# 导入lightgbm库,用于梯度提升树模型
import lgb
# 导入CatBoostRegressor库,用于梯度提升树模型
from catboost import CatBoostRegressor
# 导入StratifiedKFold、KFold和GroupKFold,用于交叉验证
from sklearn.model_selection import StratifiedKFold, KFold, GroupKFold
# 导入mean_squared_log_error,用于评估模型性能
from sklearn.metrics import mean_squared_log_error
# 导入sys、os、gc、argparse和warnings库,用于处理命令行参数和警告信息
import sys, os, gc, argparse, warnings
# 忽略警告信息
warnings.filterwarnings('ignore')
2、当处理大型数据集时,内存的使用是一个重要的考虑因素。数据类型可以影响数据在内存中的存储方式,从而影响内存占用。
所以此处定义函数 reduce_mem_usage
,来减少 Pandas DataFrame 的内存使用。通过将列的数据类型降低到适当的范围,从而减少内存占用。
# 定义一个函数,用于减少DataFrame的内存使用
def reduce_mem_usage(df, verbose=True):
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64'] # 定义数值类型列表
start_mem = df.memory_usage().sum() / 1024**2 # 计算初始内存使用量(以 MB 为单位)
for col in df.columns: # 遍历DataFrame的每一列
col_type = df[col].dtypes # 获取该列的数据类型
if col_type in numerics: # 如果该列是数值类型
c_min = df[col].min() # 获取该列的最小值
c_max = df[col].max() # 获取该列的最大值
if str(col_type)[:3] == 'int': # 如果该列是整数类型
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max: # 如果该列的数值范围在int8的范围内
df[col] = df[col].astype(np.int8) # 将该列的数据类型转换为int8
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max: # 如果该列的数值范围在int16的范围内
df[col] = df[col].astype(np.int16) # 将该列的数据类型转换为int16
elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max: # 如果该列的数值范围在int32的范围内
df[col] = df[col].astype(np.int32) # 将该列的数据类型转换为int32
elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max: # 如果该列的数值范围在int64的范围内
df[col] = df[col].astype(np.int64) # 将该列的数据类型转换为int64
else: # 如果该列不是整数类型
if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max: # 如果该列的数值范围在float16的范围内
df[col] = df[col].astype(np.float16) # 将该列的数据类型转换为float16
elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max: # 如果该列的数值范围在float32的范围内
df[col] = df[col].astype(np.float32) # 将该列的数据类型转换为float32
else: # 如果该列的数值范围不在上述三种类型的范围内
df[col] = df[col].astype(np.float64) # 将该列的数据类型转换为float64
end_mem = df.memory_usage().sum() / 1024**2 # 计算结束时的内存使用量
if verbose: print('Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction)'.format(end_mem, 100 * (start_mem - end_mem) / start_mem)) # 输出内存使用量的减少情况
return df # 返回处理后的DataFrame
3、用pandas读取训练集与测试集
# 读取数据
path = 'ai4bio' # 定义数据集路径
# 可能因为内存问题所导致数据读取困难,可以选择放弃部分特征,仅读取部分行,baseline仅读取前10000行
#根据自己的算力情况,适当读取数据
traindata = pd.read_csv(f'{path}/traindata.csv', nrows=10000) # 读取训练数据
trainmap = pd.read_csv(f'{path}/trainmap.csv') # 读取训练数据的映射信息
testdata = pd.read_csv(f'{path}/ai4bio_testset_final/testdata.csv', nrows=10000) # 读取测试数据
testmap = pd.read_csv(f'{path}/ai4bio_testset_final/testmap.csv') # 读取测试数据的映射信息
# 压缩内存,因时间较长,所以暂时注释掉,按自身情况选择使用
# traindata = reduce_mem_usage(traindata)
# testdata = reduce_mem_usage(testdata)
4、因为traindata和testdata 每一列代表一个样本,每一行代表一个甲基化位点,所以需要转置处理,变成每一行代表一个样本。
# 数据预处理
traindata = traindata.set_index('cpgsite') # 将训练数据的索引设置为'cpgsite'列
traindata = traindata.T # 转置训练数据
traindata = traindata.reset_index() # 重置训练数据的索引
traindata = traindata.rename(columns={'index':'sample_id'}) # 重命名训练数据的列名
traindata.columns = ['sample_id'] + [i for i in range(10000)] # 设置训练数据的列名为'sample_id'加上一列自增的数字
traindata.to_pickle(f'{path}/traindata.pkl') # 将处理后的训练数据保存为pickle文件
testdata = testdata.set_index('cpgsite') # 将测试数据的索引设置为'cpgsite'列
testdata = testdata.T # 转置测试数据
testdata = testdata.reset_index() # 重置测试数据的索引
testdata = testdata.rename(columns={'index':'sample_id'}) # 重命名测试数据的列名
testdata.columns = ['sample_id'] + [i for i in range(10000)] # 设置测试数据的列名为'sample_id'加上一列自增的数字
testdata.to_pickle(f'{path}/testdata.pkl') # 将处理后的测试数据保存为pickle文件
5、通过观察数据,可以发现trainmap中的gender、sample_type、disease为字符串类型的特征,需要进行转换为数值类型。还需要进行处理的部分包括缺失值、数据类型转换,其中对于缺失值的处理,由于baseline方案使用的是catboost模型,并不需要进行缺失值的处理。
# 数据拼接
# 将trainmap数据集中的'sample_id', 'age', 'gender', 'sample_type', 'disease'这几列数据合并到traindata数据集中,合并的依据是两个数据集中的'sample_id'列相同,合并后的结果存储在traindata中。
traindata = traindata.merge(trainmap[['sample_id', 'age', 'gender', 'sample_type', 'disease']],on='sample_id',how='left')
# 将testmap数据集中的'sample_id', 'gender'这两列数据合并到testdata数据集中,合并的依据是两个数据集中的'sample_id'列相同,合并后的结果存储在testdata中,由于使用了左连接(how='left'),如果testmap中没有与testdata匹配的'sample_id',那么对应的'gender'值将会被填充为NaN。
testdata = testdata.merge(testmap[['sample_id', 'gender']],on='sample_id',how='left')
# 定义了一个名为disease_mapping的字典,它将疾病名称映射为对应的数值。例如,'Alzheimer's disease'被映射为1,'Parkinson's disease'被映射为4,以此类推。这样的映射通常用于机器学习模型中的特征编码,以便将文本形式的类别标签转换为可以输入到模型的数字形式。
disease_mapping = {
'control': 0,
"Alzheimer's disease": 1,
"Graves' disease": 2,
"Huntington's disease": 3,
"Parkinson's disease": 4,
'rheumatoid arthritis': 5,
'schizophrenia': 6,
"Sjogren's syndrome": 7,
'stroke': 8,
'type 2 diabetes': 9
}
sample_type_mapping = {'control': 0, 'disease tissue': 1}
gender_mapping = {'F': 0, 'M': 1}
# 将traindata数据集中'disease'列中的疾病名称使用disease_mapping字典进行映射,将疾病名称替换为对应的数值。这样处理后,'disease'列中的值将变为0到9之间的整数。
traindata['disease'] = traindata['disease'].map(disease_mapping)
# 将traindata数据集中'sample_type'列中的样本类型使用sample_type_mapping字典进行映射,将样本类型替换为对应的数值,处理后,'sample_type'列中的值将变为0或1,表示不同的样本类型。
traindata['sample_type'] = traindata['sample_type'].map(sample_type_mapping)
# 将traindata和testdata数据集中'gender'列中的性别使用gender_mapping字典进行映射,将性别替换为对应的数值。这样处理后,'gender'列中的值将变为0或1,表示不同的性别。
traindata['gender'] = traindata['gender'].map(gender_mapping)
testdata['gender'] = testdata['gender'].map(gender_mapping)
6、特征工程部分,原始字段特征以连续型数值为主,可以按行进行聚合统计操作,如构建max、min、mean、std、skew等常见的数值统计特征。
# 特征工程
# 计算traindata和testdata数据集中前10000行每一列的最大值、最小值、标准差、方差、偏度、均值和中位数,并将结果分别存储在traindata和testdata的'max'、'min'、'std'、'var'、'skew'、'mean'和'median'列中,这些统计量可以用于描述数据集的特征和分布情况。
traindata['max'] = traindata[[i for i in range(10000)]].max(axis=1)
traindata['min'] = traindata[[i for i in range(10000)]].min(axis=1)
traindata['std'] = traindata[[i for i in range(10000)]].std(axis=1)
traindata['var'] = traindata[[i for i in range(10000)]].var(axis=1)
traindata['skew'] = traindata[[i for i in range(10000)]].skew(axis=1)
traindata['mean'] = traindata[[i for i in range(10000)]].mean(axis=1)
traindata['median'] = traindata[[i for i in range(10000)]].median(axis=1)
testdata['max'] = testdata[[i for i in range(10000)]].max(axis=1)
testdata['min'] = testdata[[i for i in range(10000)]].min(axis=1)
testdata['std'] = testdata[[i for i in range(10000)]].std(axis=1)
testdata['var'] = testdata[[i for i in range(10000)]].var(axis=1)
testdata['skew'] = testdata[[i for i in range(10000)]].skew(axis=1)
testdata['mean'] = testdata[[i for i in range(10000)]].mean(axis=1)
testdata['median'] = testdata[[i for i in range(10000)]].median(axis=1)
# 入模特征选择
cols = [i for i in range(10000)] + ['gender','max','min','std','var','skew','mean','median']
7、定义catboost_model()函数,实现使用 CatBoostRegressor 进行模型训练和交叉验证的过程。该函数接受训练集特征、训练集标签和测试集特征作为输入,并使用 K 折交叉验证来训练多个 CatBoostRegressor 模型。在每个交叉验证迭代中,代码按照设定的参数和流程执行以下操作:
- 将训练集分成训练集和验证集。
- 定义 CatBoostRegressor 模型的参数。
- 创建 CatBoostRegressor 模型实例,使用训练集和验证集拟合模型。
- 使用模型对验证集和测试集进行预测。
- 计算验证集预测结果与真实值之间的平均绝对误差(MAE),并将 MAE 添加到列表中。
- 将验证集预测结果存储到
oof
数组中,计算测试集预测结果的平均值。
整个过程循环遍历多个交叉验证折叠,最终返回交叉验证中的验证集预测结果(oof
数组)和测试集预测结果(test_predict
数组)。
此外,还计算了特征的重要性分数,并将其保存为 CSV 文件,以帮助评估特征在模型中的影响力。
# 模型训练与验证
# 定义一个名为catboost_model的函数,接收四个参数:train_x, train_y, test_x和seed
def catboost_model(train_x, train_y, test_x, seed = 2023):
folds = 5 # 设置K折交叉验证折数为5
kf = KFold(n_splits=folds, shuffle=True, random_state=seed) # 使用KFold方法创建一个交叉验证对象kf,设置折数、是否打乱顺序和随机种子
oof = np.zeros(train_x.shape[0]) # 初始化一个全零数组oof,长度为train_x的长度
test_predict = np.zeros(test_x.shape[0]) # 初始化一个全零数组test_predict,长度为test_x的长度
cv_scores = [] # 初始化一个空列表cv_scores,用于存储交叉验证得分
# 使用for循环遍历kf的每个折叠
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]
# 定义CatBoostRegressor模型的参数
params = {'learning_rate': 0.1, # 学习率,控制模型参数更新的速度。值越大,模型更新越快,但可能陷入局部最优解;值越小,模型更新越慢,但可能收敛到更好的解。
'depth': 5, # 树的深度,即决策树的最大层数。树的深度越深,模型的复杂度越高,可能导致过拟合;树的深度越浅,模型的复杂度越低,可能导致欠拟合。
'bootstrap_type':'Bernoulli', # 自助法的类型,用于有放回地抽样。'Bernoulli'表示使用伯努利分布进行抽样,每次抽样后将结果反馈到训练集中。
'random_seed':2023, # 随机种子,用于控制随机过程。设置相同的随机种子可以保证每次运行代码时得到相同的结果。
'od_type': 'Iter', # 迭代次数优化方法的类型。'Iter'表示使用迭代次数优化方法,通过多次迭代来寻找最优的迭代次数。
'od_wait': 100, # 迭代次数优化方法的等待时间,即两次迭代之间的最小间隔。设置较长的等待时间可以加快收敛速度,但可能导致过拟合;设置较短的等待时间可以加快收敛速度,但可能导致欠拟合。
'allow_writing_files': False, # 是否允许写入文件。设置为False表示不保存模型参数,只返回模型对象。
'task_type':"GPU", # 任务类型,表示模型运行在GPU还是CPU上。设置为"GPU"表示模型运行在GPU上,如果计算机没有GPU,可以设置为"CPU"。
'devices':'0:1' # 设备列表,表示使用哪些GPU设备。"0:1"表示只使用第一个GPU设备。}
# 创建CatBoostRegressor模型实例
# 根据自己的算力与精力,调整iterations,V100环境iterations=500需要跑10min
model = CatBoostRegressor(iterations=500, **params)
# 使用训练集和验证集拟合模型
model.fit(trn_x, trn_y, # 训练集的特征和标签,用于模型的训练。
eval_set=(val_x, val_y), # 验证集的特征和标签,用于在训练过程中评估模型性能。
metric_period=500, # 定评估指标的计算周期,即每隔多少次迭代计算一次评估指标。
use_best_model=True, # 设置为True表示在训练过程中使用验证集上性能最好的模型参数。
cat_features=[], # 包含需要转换为类别特征的特征名称,没有需要转换的特征,所以为空列表。
verbose=1 # 设置日志输出的详细程度,1表示输出详细信息。)
# 使用模型对测试集进行预测
val_pred = model.predict(val_x)
test_pred = model.predict(test_x)
# 将验证集预测结果存储到oof数组中
oof[valid_index] = val_pred
# 计算K折测试集预测结果的平均值并累加到test_predict数组中
test_predict += test_pred / kf.n_splits
# 暂时忽略健康样本和患病样本在计算MAE上的差异,仅使用常规的MAE指标
# 计算验证集预测结果与真实值之间的平均绝对误差(MAE)
score = mean_absolute_error(val_y, val_pred)
# 将MAE添加到cv_scores列表中
cv_scores.append(score)
print(cv_scores) # 打印cv_scores列表
# 获取特征重要性打分,便于评估特征
if i == 0:
# 将特征名称和打分存储到DataFrame中
fea_ = model.feature_importances_
fea_name = model.feature_names_
fea_score = pd.DataFrame({'fea_name':fea_name, 'score':fea_})
# 按照打分降序排列DataFrame
fea_score = fea_score.sort_values('score', ascending=False)
# 将排序后的DataFrame保存为CSV文件(命名为feature_importances.csv)
fea_score.to_csv('feature_importances.csv', index=False)
return oof, test_predict # 返回oof和test_predict数组
# 调用catboost_model函数,进行模型训练与结果预测
cat_oof, cat_test = catboost_model(traindata[cols], traindata['age'], testdata[cols])
8、将得到的结果处理成比赛要求的格式,并保存成txt文件。
# 输出赛题提交格式的结果
testdata['age'] = cat_test # 将testdata数据框中的age列赋值为cat_test。
testdata['age'] = testdata['age'].astype(float) # 将age列的数据类型转换为浮点数。
testdata['age'] = testdata['age'].apply(lambda x: x if x>0 else 0.0) # 使用lambda函数对age列中的每个元素进行判断,如果大于0,则保持不变,否则将其替换为0.0。
testdata['age'] = testdata['age'].apply(lambda x: '%.2f' % x) # 使用lambda函数将age列中的每个元素格式化为保留两位小数的字符串。
testdata['age'] = testdata['age'].astype(str) # 将age列的数据类型转换为字符串。
testdata[['sample_id','age']].to_csv('submit.txt',index=False) # 将sample_id和age两列保存到名为submit.txt的文件中,不包含索引。
四、Baseline学习笔记
1、k折交叉验证
(1)概念
K折交叉验证(K-fold cross-validation)是一种常用的评估机器学习模型性能的方法。它可以有效地利用有限的数据集,并且更客观地评估模型的泛化能力。K折交叉验证将数据集分成K个子集(也称为折叠),其中K通常是一个整数,比如5或10。
(2)步骤:
- 数据集划分:将原始数据集随机分成K个大小相似的子集。每个子集称为一个折叠。
- 模型训练和评估:重复K次,每次使用其中的K-1个折叠作为训练数据,留下一个折叠作为验证数据。在每一次迭代中,使用K-1个折叠训练模型,并使用留下的一个折叠进行模型评估。
- 性能度量:在每次迭代中,记录模型的性能度量(例如准确率、精确度、召回率等)。
- 平均性能:完成K次迭代后,计算K次性能度量的平均值,得到最终模型的性能评估。
(3)优缺点
K折交叉验证的主要优点是:在数据有限的情况下,能够更好地利用数据,降低模型评估结果对数据集划分的依赖性。同时,它也可以减轻因为特定数据划分方式而导致的模型性能波动问题。
然而,K折交叉验证也有一些缺点:由于需要训练K次模型,它可能需要更长的计算时间和计算资源。同时,对于某些数据集,K折交叉验证可能无法捕捉到数据的时间相关性,特别是在时间序列数据上的应用。在这种情况下,应该考虑使用时间序列交叉验证的方法。
总结一下,K折交叉验证是一种重要的模型评估方法,可以更好地评估机器学习模型的泛化性能,并且在数据集较小或者不平衡的情况下特别有用。
2、CatBoost模型
(1)概念
CatBoost是一种集成学习方法,通过将多个决策树组合起来形成强大的预测模型,常用于分类和回归任务。
(2)主要特点和优势:
- 自动处理分类特征:CatBoost在训练过程中自动处理分类特征,无需进行额外的数据预处理。它能够直接处理类别型特征,并将其转换成数值型特征,避免了手动编码或独热编码等处理步骤。
- 处理缺失值:CatBoost可以自动处理缺失值,无需对缺失值进行填充。它会将缺失值作为一个独立的类别来处理。
- 避免过拟合:CatBoost使用一种称为“Ordered Boosting”的技术,通过对训练样本的顺序进行排序来避免过拟合。这有助于提高模型的泛化能力。
- 支持GPU加速:CatBoost支持在GPU上进行训练,可以显著加快训练速度,特别是在处理大规模数据集时。
- 优秀的性能:CatBoost在性能上表现出色,通常能够取得较好的预测效果。CatBoost在很多实际应用中都表现良好,特别是在具有分类特征和缺失值的数据集上。
五、改进与提升
Baseline中只取了前10000特征,而想要让模型得到更好的效果,就得尽量喂入更多的有效特征。比如,在喂入30000个特征时,分数提升了0.5。
但当尝试喂入更多的特征时,计算资源不够,内核总是崩。
所以考虑了几种方法来尝试减少数据量。
(一)特征选择
1、代码
print('过滤异常特征...')
for col in cols:
if traindata[col].nunique()==1: # 属性值唯一
cols.remove(col)
if traindata[col].isnull().sum() / traindata.shape[0] > 0.7: # 缺失率大于0.7
cols.remove(col)
len(cols)
print('过滤高相关特征...')
def correlation(data, threshold):
col_corr = []
corr_matrix = data.corr()
for i in range(len(corr_matrix)):
for j in range(i):
if abs(corr_matrix.iloc[i,j]) > threshold: # 相关性大于0.95
colname = corr_matrix.columns[i]
col_corr.append(colname)
return list(set(col_corr))
drop_cols += correlation(traindata[cols], 0.95)
len(drop_cols)
2、效果
使用了过滤缺失率高的特征和过滤高相关性的特征的方法来进行特征选择。但是缺失率高的特征占比很小,过滤之后特征数目还是很多,而计算高相关性也需要耗费很大的计算资源,难以实现,效果不好。
3、思考
(1)或可考虑按部分计算相关性,每次计算一个批次,而不是一次性计算所有特征的相关性;
(2)也可以考虑别的特征选择的角度和方法。
(二)PCA降维
1、代码
from sklearn.decomposition import IncrementalPCA
import numpy as np
np.random.seed(0)
X_train = traindata[cols]
X_test = testdata[cols]
# 初始化增量PCA模型,指定主成分数量
n_components = 233
ipca = IncrementalPCA(n_components=n_components)
# 逐块处理数据
batch_size = 500
for i in range(0, len(X_train), batch_size):
batch = X_train[i:i+batch_size]
ipca.partial_fit(batch)
# 获取降维后的数据
transformed_train = ipca.transform(X_train)
transformed_test = ipca.transform(X_test)
train = pd.DataFrame(transformed_train)
test = pd.DataFrame(transformed_test)
2、理论知识
(1)主成分分析的概念
主成分分析(Principal Component Analysis,PCA)是一种常用的降维技术,用于将高维数据转换为低维空间,同时保留原始数据中的主要信息。PCA的主要目标是找到一组新的坐标轴,称为主成分,将数据投影到这些轴上,使得投影后的数据具有最大的方差。
(2)主成分分析的基本步骤
- 数据标准化: 首先,需要对原始数据进行标准化,确保每个特征的均值为0,方差为1。这是为了确保不同特征之间的尺度差异不会影响PCA的结果。
- 计算协方差矩阵: 计算原始数据中各个特征之间的协方差矩阵。协方差矩阵反映了特征之间的线性关系程度。
- 计算特征值和特征向量: 对协方差矩阵进行特征值分解,得到特征值和对应的特征向量。特征值代表了数据在对应特征向量方向上的方差。
- 选择主成分: 根据特征值的大小,选择保留的主成分数量。通常,可以根据保留的方差比例来确定主成分的数量。
- 构建投影矩阵: 将选定数量的特征向量构建成一个投影矩阵,将原始数据投影到新的低维空间中。
- 降维转换: 将原始数据通过投影矩阵映射到新的低维空间,得到降维后的数据。
PCA的应用有很多,包括数据可视化、数据压缩、去噪等。通过主成分分析,可以找到数据中的主要方向,将高维数据降低到更易处理和理解的低维空间,同时保留了数据的主要信息。
(3)主成分分析的优缺点
优点:
- 降维: PCA能够将高维数据降低到较低维度,减少数据的复杂性,同时保留了数据的主要特征。
- 信息保留: PCA选择保留的主成分方向是原始数据中方差最大的方向,因此在降维的同时尽可能保留了原始数据的主要信息。
- 数据去噪: PCA可以通过保留最主要的成分来过滤掉数据中的噪声和不重要的信息,从而提高模型的稳定性和性能。
- 可视化: 降维后的数据可以更容易地进行可视化,使得数据的特征和模式更容易理解和分析。
- 特征提取: PCA可以帮助识别原始数据中的重要特征,有助于进一步的特征工程和模型建立。
缺点:
- 信息丢失: 虽然PCA可以保留主要信息,但在降维过程中仍然会丢失一部分数据的细节,尤其是低方差的特征。
- 解释性差: 降维后的主成分可能很难解释,因为它们是原始特征的线性组合,可能不直观。
- 依赖数据分布: PCA在处理非线性数据分布时可能效果不佳,因为它是一种线性降维方法。
- 计算复杂度: PCA需要计算协方差矩阵的特征值分解,对于大规模数据集计算复杂度较高。
- 选择主成分数: 如何选择保留的主成分数量是一个需要人工决策的问题,选择不当可能影响降维效果。
综上所述,主成分分析作为一种降维技术在很多场景下具有很大的优势,但也需要在实际应用中权衡其优缺点,考虑数据的性质和问题需求。
(4)主成分分析的时间复杂度与空间复杂度
主成分分析(PCA)的时间复杂度和空间复杂度都与数据的维度和样本数量相关。具体来说,PCA的时间复杂度和空间复杂度在一定程度上取决于以下因素:
- 样本数量(n): 如果样本数量较大,那么计算协方差矩阵和特征值分解的时间复杂度会增加。
- 原始特征的维度(d): 原始数据的维度越高,计算协方差矩阵的复杂度越高。
- 保留的主成分数量(k): 如果选择保留的主成分数量较大,那么计算复杂度会增加。
总体来说,PCA的计算复杂度主要由计算协方差矩阵和进行特征值分解两个步骤决定。计算协方差矩阵的时间复杂度为O(n * d^2),特征值分解的时间复杂度通常为O(d^3),其中n是样本数量,d是原始数据的维度。因此,当n和d较大时,PCA的计算复杂度会相应增加。
关于空间复杂度,主要考虑在计算协方差矩阵和特征值分解过程中所需的存储空间。计算协方差矩阵需要存储原始数据的协方差矩阵,其大小为O(d^2)。特征值分解通常需要存储特征值和特征向量,其大小为O(d)。因此,PCA的空间复杂度与数据的维度有关。
需要注意的是,虽然PCA的时间复杂度和空间复杂度可能会相对较高,但在实际应用中,有许多针对PCA的优化算法和技术,可以减少计算和存储的开销,以提高效率。
(5)主成分分析的优化算法和技术
在主成分分析(PCA)中,有一些优化算法和技术可以用来减少计算和存储开销,提高PCA的效率。以下是一些常见的优化算法和技术:
- 随机PCA(Randomized PCA): 随机PCA是一种基于随机采样的方法,通过在原始数据矩阵上进行随机投影,来降低计算复杂度。它可以在保留较高方差的情况下,使用较少的样本进行计算。
- 增量PCA(Incremental PCA): 增量PCA是一种处理大规模数据集的方法,它将数据分割成小块,逐块进行PCA计算,然后将结果合并。这种方法可以减少内存使用并允许逐步更新PCA结果。
- 快速PCA(Fast PCA): 快速PCA使用特殊的数值计算技巧,如Krylov子空间方法和迭代技术,来加速特征值分解的计算过程。
- 分布式PCA(Distributed PCA): 当数据集非常大时,可以使用分布式计算框架,如Apache Spark,将PCA计算分布在多个计算节点上,从而减少计算时间。
- Kernel PCA: 对于非线性数据,Kernel PCA使用核技巧将数据映射到高维空间,然后在高维空间中进行PCA,以处理非线性关系。
- PCA加速算法: 一些近似算法,如Randomized SVD和Truncated SVD,通过近似特征值分解来加速PCA计算。
- 稀疏PCA(Sparse PCA): 稀疏PCA引入稀疏性约束,可以在PCA的过程中将某些系数置为零,从而降低计算复杂度。
- 压缩PCA(Compressed PCA): 压缩PCA使用矩阵压缩技术,如SVD的截断奇异值分解,来减少PCA计算中的存储开销。
这些优化算法和技术可以根据问题的需求和数据的性质选择合适的方法,以提高PCA的效率并处理大规模或高维数据。
(6)增量PCA(Incremental PCA)
增量主成分分析(Incremental Principal Component Analysis,IPCA)是一种用于处理大规模数据集的主成分分析(PCA)方法。它的主要思想是将数据集分割成小块,然后逐块进行PCA计算,最后将结果合并,以降低计算复杂度和内存需求。增量PCA适用于在处理大型数据集时,不需要将整个数据集加载到内存中的情况。
以下是增量PCA的基本思路和步骤:
- 数据分块: 将大规模数据集分成较小的块或批次。每个块的大小可以根据可用的内存和计算资源来确定。
- 块内PCA: 对每个块内的数据进行PCA计算。这样,每个块的主成分和变换矩阵可以在内存中计算和存储。
- 合并结果: 将每个块内计算得到的主成分和变换矩阵进行合并。这可以通过计算每个块的均值和协方差矩阵的加权平均来实现。
- 重建数据: 使用合并后的主成分和变换矩阵,可以对新的数据进行降维转换,从而获得降维后的数据。
增量PCA的优点在于它可以避免一次性加载整个大型数据集,而是逐块加载和处理数据,从而减少内存需求。这使得增量PCA适用于处理无法一次性载入内存的数据,如大规模图像、生物学数据等。
需要注意的是,增量PCA的计算结果可能会与传统PCA略有不同,因为分块处理可能引入一些近似误差。因此,在实际应用中,需要根据问题需求权衡增量PCA的优势和局限性。
3、效果
在本次尝试中,我对数据集使用了增量PCA降维技术,按每个批次500条数据(取多了内核会崩),主成分数量取233(已是最大)进行降维。然后继续使用Catboost模型训练降维后的数据。
但很遗憾,效果并不像预期的那样好,甚至比之前更低。
我想原因可能是我对增量PCA技术还不够深入了解,用法不当的缘故。在之后我会继续学习改进。
(三)分批次训练
1、代码
def catboost_model(train_x, train_y, test_x, seed = 42):
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]
params = {'learning_rate': 0.1,
'depth': 5,
'bootstrap_type':'Bernoulli',
'random_seed':42,
'od_type': 'Iter',
'od_wait': 100,
'random_seed': 11,
'allow_writing_files': False,
'task_type':"GPU",
'devices':'0:1'}
##iterations是迭代轮数,可以根据自己的算力与精力,选择合适的轮数,示例给出的是500
model = CatBoostRegressor(iterations=500, **params)
# 分批次训练
batch_size = 1000
for batch_start in range(0, len(trn_x), batch_size):
batch_end = min(batch_start + batch_size, len(trn_x))
model.fit(trn_x.iloc[batch_start:batch_end], trn_y[batch_start:batch_end],
eval_set=(val_x, val_y),
metric_period=500,
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)
# 获取特征重要性打分,便于评估特征
if i == 0:
fea_ = model.feature_importances_
fea_name = model.feature_names_
fea_score = pd.DataFrame({'fea_name':fea_name, 'score':fea_})
fea_score = fea_score.sort_values('score', ascending=False)
fea_score.to_csv('feature_importances.csv', index=False)
return oof, test_predict
2、效果
由于一次喂入所有数据会导致计算资源不够而内核终止,所以考虑采用将数据集进行切分然后分批次训练的方法来避免。
但是效果非常差,而且耗时也很长。有待改进。
(四)XGboost模型
1、代码
def xgboost_model(train_x, train_y, test_x, seed = 42):
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]
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': 'gpu_hist',
'seed': 520,
'nthread': 16
}
train_matrix = xgb.DMatrix(trn_x , label=trn_y)
valid_matrix = xgb.DMatrix(val_x , label=val_y)
test_matrix = xgb.DMatrix(test_x)
watchlist = [(train_matrix, 'train'),(valid_matrix, 'eval')]
model = xgb.train(xgb_params, train_matrix, num_boost_round=2400, evals=watchlist, verbose_eval=400, early_stopping_rounds=100)
val_pred = model.predict(valid_matrix)
test_pred = model.predict(test_matrix)
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
xgb_oof, xgb_test = xgboost_model(traindata[cols], traindata['age'], testdata[cols])
2、理论知识
(1)概念与特点
XGBoost(Extreme Gradient Boosting)是一种强大的机器学习算法,属于集成学习的一种。它是一种梯度提升算法的扩展,旨在提高预测模型的性能和精度。XGBoost 在各种机器学习竞赛和实际应用中表现优秀,因此在数据科学领域广受欢迎。
以下是关于 XGBoost 模型的一些重要概念和特点:
-
梯度提升: XGBoost 是一种梯度提升算法,它通过将多个弱学习器(通常是决策树)组合在一起,逐步减小预测误差来提升模型性能。
-
正则化: XGBoost 支持 L1(Lasso)和 L2(Ridge) 正则化项,用于控制模型的复杂度,避免过拟合。
-
自适应梯度提升: XGBoost 会在每次迭代中根据之前迭代的性能调整学习率,从而更有效地逼近最优解。
-
特征重要性评估: XGBoost 提供了一种方法来评估特征在模型中的重要性,可以帮助你理解哪些特征对预测起到了关键作用。
-
缺失值处理: XGBoost 能够处理缺失值,无需显式地对其进行处理。
-
并行计算: XGBoost 可以并行处理数据,加速模型训练过程。
-
特征工程: 虽然 XGBoost 能够处理原始数据,但进行适当的特征工程仍然可以显著提高模型性能。
-
集成学习: XGBoost 支持多种集成策略,包括 Bagging 和 Stacking,可以进一步提升模型的性能。
(2)步骤
-
数据准备: 准备特征矩阵和目标向量。
-
模型配置: 选择适当的参数,例如树的深度、学习率、正则化项等。
-
模型训练: 使用训练数据拟合 XGBoost 模型。
-
模型验证: 使用验证数据评估模型性能,可以使用交叉验证等技术。
-
参数调优: 根据验证结果调整模型参数,以优化性能。
-
模型预测: 使用训练好的模型进行预测。
-
特征重要性分析: 分析特征重要性,理解模型对特征的依赖程度。
总的来说,XGBoost 是一种强大而灵活的机器学习算法,适用于分类和回归问题,可以在各种领域中取得优秀的预测结果。不过,对于每个具体问题,都需要仔细调整参数以及进行适当的特征工程,以获得最佳的性能。
3、效果
使用XGBoost模型,可以喂入5万个特征(Catboost只能喂入3万个,多了内核就会崩),取得了分数上的提升。
在排行榜上排60名左右
六、总结与反思
此次赛题于我而言是一个很新奇的尝试 。第一次了解到AI4S的概念,第一次做与医学相关的课题,也是第一次接触数据集如此之大的任务。纵然困难重重,但我也最终克服掉了许多,这得益于DataWhale给出的如此详细的学习资料。
整个流程下来,其实感觉就是一个数据量非常大的数据挖掘任务,不需要考虑太多数据处理、构造特征方面的工作,而是需要去思考如何喂入更多的原始特征。
还有许多需要去实践尝试的方式。