在baseline阶段,我们使用CatBoost完成了解决机器学习问题的全部流程,得到了基础的分数。在进阶实践部分,我们将在原有Baseline基础上进行更多优化,通常从特征工程和模型选择两个方面着手。
优化方法建议
-
提取更多特征: 特征工程是数据挖掘比赛中制胜的关键。我们需要挖掘更多有价值的信息并将其转化为模型输入特征。对于本次赛题,可以从专业角度构建特征,除了Smiles特征外,还可以从InChI字符串中提取详细的分子结构信息。例如:
- 分子式:从InChI字符串提取分子式部分,例如
/C47H61N7O6S
表示分子由47个碳原子、61个氢原子、7个氮原子、6个氧原子和1个硫原子组成。 - 分子量:通过计算各原子数量乘以其原子质量来获得。例如:碳(C)原子质量约为12.01 g/mol,氢(H)原子质量约为1.008 g/mol,氮(N)原子质量约为14.01 g/mol,氧(O)原子质量约为16.00 g/mol,硫(S)原子质量约为32.07 g/mol。公式如下:
分子量 = (47 × 12.01) + (61 × 1.008) + (7 × 14.01) + (6 × 16.00) + (1 × 32.07)
尝试不同的模型: 不同模型的预测结果会有差异,通过不断实验和试错,找到最佳模型并增强对模型的理解能力。我们可以从以下模型进行尝试:
- LightGBM
- XGBoost
- CatBoost
- 分子式:从InChI字符串提取分子式部分,例如
特征优化
针对InChI字符串,我们可以进行以下特征提取:
-
提取分子式: 从InChI字符串中提取分子式信息。
import re from rdkit import Chem def parse_inchi(row): inchi_str = row['InChI'] formula = '' molecular_weight = 0 element_counts = {} formula_match = re.search(r"InChI=1S/([^/]+)/c", inchi_str) if formula_match: formula = formula_match.group(1) for element, count in re.findall(r"([A-Z][a-z]*)([0-9]*)", formula): count = int(count) if count else 1 element_mass = atomic_masses.get(element.upper(), 0) molecular_weight += element_mass * count element_counts[element.upper()] = count return pd.Series({'Formula': formula, 'MolecularWeight': molecular_weight, 'ElementCounts': element_counts})
-
计算分子量和原子计数: 计算各分子的分子量并展开原子计数。
train[['Formula', 'MolecularWeight', 'ElementCounts']] = train.apply(parse_inchi, axis=1)
-
生成原子计数特征: 将原子计数展开为独立的特征列。
keys = ['H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn'] df_expanded = pd.DataFrame({key: pd.Series() for key in keys}) for index, item in enumerate(train['ElementCounts'].values): for key in keys: df_expanded.at[index, key] = item.get(key, 0) df_expanded = pd.DataFrame(df_expanded)
模型融合
通过模型融合可以提高预测的稳定性和准确性。我们可以采用以下方式:
-
定义交叉验证模型训练和预测函数:
def cv_model(clf, train_x, train_y, test_x, clf_name, seed=2023): 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(f'************************************ {i+1} ************************************') trn_x, trn_y = train_x.iloc[train_index], train_y[train_index] val_x, val_y = 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': 'binary', '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.35, 'seed': 2024, 'nthread' : 16, 'verbose' : -1, } model = clf.train(params, train_matrix, 2000, valid_sets=[train_matrix, valid_matrix], categorical_feature=[], verbose_eval=1000, 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': 'binary:logistic', 'num_class':3, 'max_depth': 5, 'lambda': 10, 'subsample': 0.7, 'colsample_bytree': 0.7, 'colsample_bylevel': 0.7, 'eta': 0.35, '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=1000, early_stopping_rounds=100) val_pred = model.predict(valid_matrix) test_pred = model.predict(test_matrix) if clf_name == "cat": params = {'learning_rate': 0.35, 'depth': 5, 'bootstrap_type':'Bernoulli','random_seed':2024, '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=1000, use_best_model=True, cat_features=[], verbose=1) val_pred = model.predict_proba(val_x) test_pred = model.predict_proba(test_x) oof[valid_index] = val_pred test_predict += test_pred / kf.n_splits F1_score = f1_score(val_y, np.where(val_pred>0.5, 1, 0)) cv_scores.append(F1_score) print(cv_scores) return oof, test_predict
-
模型融合: 通过将多个模型的预测结果进行平均融合,或者采用stacking方法。
lgb_oof, lgb_test = cv_model(lgb, x_train, y_train, x_test, 'lgb') xgb_oof, xgb_test = cv_model(xgb, x_train, y_train, x_test, 'xgb') cat_oof, cat_test = cv_model(CatBoostClassifier, x_train, y_train, x_test, 'cat') final_test = (lgb_test + xgb_test + cat_test) / 3
将结果取平均进行融合是比较基础的融合的方式,另外一种经典融合方式为stacking,stacking是一种分层模型集成框架。以两层为例,第一层由多个基学习器组成,其输入为原始训练集,第二层的模型则是以第一层基学习器的输出作为特征加入训练集进行再训练,从而得到完整的stacking模型。
第一层:(类比cv_model函数)
-
划分训练数据为K折(5折为例,每次选择其中四份作为训练集,一份作为验证集);
-
针对各个模型RF、ET、GBDT、XGB,分别进行5次训练,每次训练保留一份样本用作训练时的验证,训练完成后分别对Validation set,Test set进行预测,对于Test set一个模型会对应5个预测结果,将这5个结果取平均;对于Validation set一个模型经过5次交叉验证后,所有验证集数据都含有一个标签。此步骤结束后:5个验证集(总数相当于训练集全部)在每个模型下分别有一个预测标签,每行数据共有4个标签(4个算法模型),测试集每行数据也拥有四个标签(4个模型分别预测得到的)
第二层:(类比stack_model函数)
-
将训练集中的四个标签外加真实标签当作五列新的特征作为新的训练集,选取一个训练模型,根据新的训练集进行训练,然后应用测试集的四个标签组成的测试集进行预测作为最终的result。
Stacking参考代码
def stack_model(oof_1, oof_2, oof_3, predictions_1, predictions_2, predictions_3, y):
train_stack = pd.concat([oof_1, oof_2, oof_3], axis=1)
test_stack = pd.concat([predictions_1, predictions_2, predictions_3], axis=1)
oof = np.zeros((train_stack.shape[0],))
predictions = np.zeros((test_stack.shape[0],))
scores = []
from sklearn.linear_model import Ridge
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import RepeatedKFold
folds = RepeatedKFold(n_splits=5, n_repeats=2, random_state=2021)
for fold_, (trn_idx, val_idx) in enumerate(folds.split(train_stack, train_stack)):
print(f"fold n°{fold_+1}")
trn_data, trn_y = train_stack.loc[trn_idx], y[trn_idx]
val_data, val_y = train_stack.loc[val_idx], y[val_idx]
clf = Ridge(random_state=2024)
clf.fit(trn_data, trn_y)
oof[val_idx] = clf.predict(val_data)
predictions += clf.predict(test_stack) / (5 * 2)
score_single = roc_auc_score(val_y, oof[val_idx])
scores.append(score_single)
print(f'{fold_+1}/{5}', score_single)
print('mean: ', np.mean(scores))
return oof, predictions
结语
通过本次分子性质预测挑战赛的探讨,我们从基础的CatBoost模型出发,逐步引入了特征工程和模型融合等优化方法。在特征工程方面,我们深入挖掘了InChI字符串中的信息,提取了丰富的分子结构特征;在模型优化方面,我们尝试了不同的机器学习模型并进行了融合,显著提升了预测性能。特别是通过Stacking方法,我们有效地整合了多种模型的优势,进一步提升了整体表现。希望这些实践经验能为大家在数据科学和机器学习领域的研究和应用提供有价值的参考。持续尝试和创新,将是我们不断进步的动力。
如果你觉得这篇博文对你有帮助,请点赞、收藏、关注我,并且可以打赏支持我!
欢迎关注我的后续博文,我将分享更多关于人工智能、自然语言处理和计算机视觉的精彩内容。
谢谢大家的支持!