0. 前言
这是DataWhale的最后一期调优,本次调优的两个主要方向是:
- 尝试引入新特征来进行优化
- 尝试通过对于lgm本身结构的改进来进行优化
1. 基于新特征的优化
这里我们需要先介绍一下为什么引入新的特征特征可为我们带来更好的模型表现
- 特征的引入有助于我们更好的拟合训练数据
- 新特征的引入可以带来更好的特征拟合:引入目标变量相关的特征往往可以使得特征更精确地拟合测试集
- 捕捉非线性关系:通过组合或者变换现有特征,够着新的特征可以增加特征的完善性
在本次的优化中我们会引入三个新特征
- 反义链与目标基因:siRNA的反义链与目标基因的匹配程度以及匹配位置均会对预测i目标产生影响
2. GC含量:siRNA的GC含量或者特定片段中特定位置的GC含量对沉默效率的影响很大
GC含量又称为G+C比值或GC比值,在序列中,鸟嘌呤(G)和胞嘧啶(C)所占的比率称为GC含量。
- 将修饰过的碱基序列进行编码:就是说将不同修饰的核苷酸编码在化学上的差异也加入模型的考虑当中
针对这个模块,我们需要对代码进行部分修改
def siRNA_feat_builder(s: pd.Series, anti: bool = False):
name = "anti" if anti else "sense"
df = s.to_frame()
df[f"feat_siRNA_{name}_seq_len"] = s.str.len()
for pos in [0, -1]:
for c in list("AUGC"):
df[f"feat_siRNA_{name}_seq_{c}_{'front' if pos == 0 else 'back'}"] = (
s.str[pos] == c
)
# 模式特征
df[f"feat_siRNA_{name}_seq_pattern_1"] = s.str.startswith("AA") & s.str.endswith("UU")
df[f"feat_siRNA_{name}_seq_pattern_2"] = s.str.startswith("GA") & s.str.endswith("UU")
df[f"feat_siRNA_{name}_seq_pattern_3"] = s.str.startswith("CA") & s.str.endswith("UU")
df[f"feat_siRNA_{name}_seq_pattern_4"] = s.str.startswith("UA") & s.str.endswith("UU")
df[f"feat_siRNA_{name}_seq_pattern_5"] = s.str.startswith("UU") & s.str.endswith("AA")
df[f"feat_siRNA_{name}_seq_pattern_6"] = s.str.startswith("UU") & s.str.endswith("GA")
df[f"feat_siRNA_{name}_seq_pattern_7"] = s.str.startswith("UU") & s.str.endswith("CA")
df[f"feat_siRNA_{name}_seq_pattern_8"] = s.str.startswith("UU") & s.str.endswith("UA")
df[f"feat_siRNA_{name}_seq_pattern_9"] = s.str[1] == "A"
df[f"feat_siRNA_{name}_seq_pattern_10"] = s.str[-2] == "A"
df[f"feat_siRNA_{name}_seq_pattern_GC_frac"] = (
s.str.contains("G") + s.str.contains("C")
) / s.str.len()
for c in list("AUGC"):
df[f"feat_siRNA_{name}_seq_count_{c}"] = s.str.count(c)
df[f"feat_siRNA_{name}_seq_GC_content"] = (s.str.count("G") + s.str.count("C")) / s.str.len()
df[f"feat_siRNA_{name}_seq_contains_UG"] = s.str.contains("UG")
return df.iloc[:, 1:]
这里还有修饰siRNA特征构建以及siRNA与目标序列的对比,笔者尚在实验中,会在之后进行更新。
2.基于lgm的结构优化
这里的优化主要有三个:低Remaining范围样本高权重,利用官方指标作为损失函数,以及自适应学习率
这个方案的主要优化原理是在样本五彩的预测误差较小的情况下,这些样本本身就属于模型可以较好理解拟并且拟合的样本,对这些样本赋予更高的权重,从而进一步优化模型的参数。同时,在模型的预测的过程中也会存在一些异常或者不具备代表性的样本,故此我们在这里可以通过提高低Remaining范围的样本的来降低影响
具体代码为
weight_ls = np.array(feats['mRNA_remaining_pct'].apply(lambda x:2 if ((x<=30)and(x>=0)) else 1))
损失函数是用来估量模型的预测值与真实值不一致程度的函数,此处我们通过将原本的评价指标替换为官方的评价分数来进一步提升模型的表现
# calculate_metrics函数用于计算评估指标
def calculate_metrics(preds, data, threshold=30):
y_pred = preds
y_true = data.get_label()
mae = np.mean(np.abs(y_true - y_pred))
# if mae < 0: mae = 0
# elif mae >100: mae = 100
y_true_binary = ((y_true <= threshold) & (y_true >= 0)).astype(int)
y_pred_binary = ((y_pred <= threshold) & (y_pred >= 0)).astype(int)
mask = (y_pred >= 0) & (y_pred <= threshold)
range_mae = (
mean_absolute_error(y_true[mask], y_pred[mask]) if np.sum(mask) > 0 else 100
)
# if range_mae < 0: range_mae = 0
# elif range_mae >100: range_mae = 100
# precision = precision_score(y_true_binary, y_pred_binary, average="binary")
# recall = recall_score(y_true_binary, y_pred_binary, average="binary")
if np.sum(y_pred_binary) > 0:
precision = (np.array(y_pred_binary) & y_true_binary).sum()/np.sum(y_pred_binary)
else:
precision = 0
if np.sum(y_true_binary) > 0:
recall = (np.array(y_pred_binary) & y_true_binary).sum()/np.sum(y_true_binary)
else:
recall = 0
if precision + recall == 0:
f1 = 0
else:
f1 = 2 * precision * recall / (precision + recall)
score = (1 - mae / 100) * 0.5 + (1 - range_mae / 100) * f1 * 0.5
return "custom_score", score, True # True表示分数越高越好
自适应学习率:学习率主要用于控制模型参数更新的步长和变化速度,这里要提到一个概念,自适应学习率,自适应学习率本身会根据整体模型训练过程中的情况进行变化,常见的几种自适应学习算法有三种AdaGrad,RmSprop与Adam。
AdaGrad:根据过去的梯度来动态调整每个参数的学习率
Rmsprop:算是AdaGrad的一种改进,采用移动平均替代了累计平方和,避免了AdaGrad学习率过快衰减的问题
Adam:同时通过一阶矩和二阶矩来对学习率进行调整
本次的自适应学习率的代码
# adaptive_learning_rate函数用于自适应学习率
def adaptive_learning_rate(decay_rate=0.8, patience=50):
best_score = float("-inf") # 初始化为负无穷,因为分数越高越好
wait = 0
def callback(env):
nonlocal best_score, wait
current_score = env.evaluation_result_list[-1][2] # 假设使用的是最后一个评估指标
current_lr = env.model.params.get('learning_rate')
if current_score > best_score:
best_score = current_score
# wait = 0 # 需要连续的score没有上升
else:
wait += 1
if wait >= patience:
new_lr = float(current_lr) * decay_rate
wait = 0
env.model.params['learning_rate'] = new_lr
print(f"Learning rate adjusted to {env.model.params.get('learning_rate')}")
return callback
多折交叉学习
# train函数用于训练模型
def train(feats, n_original):
# 定义k折交叉验证
n_splits = 10
kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)
# 开始k折交叉验证
gbms = []
for fold, (train_idx, val_idx) in enumerate(
kf.split(feats.iloc[:n_original, :]), 1
):
# 准备训练集和验证集
X_train, X_val = feats.iloc[train_idx, :-1], feats.iloc[val_idx, :-1]
y_train, y_val = feats.iloc[train_idx, -1], feats.iloc[val_idx, -1]
w_train = weight_ls[train_idx]
# 创建LightGBM数据集
train_data = lgb.Dataset(X_train, label=y_train, weight=w_train)
val_data = lgb.Dataset(X_val, label=y_val, reference=train_data)
boost_round = 25000
early_stop_rounds = int(boost_round*0.1)
# 显示metric
lgb_log = lgb.log_evaluation(period=200, show_stdv=True)
lgb_stop = lgb.early_stopping(stopping_rounds=early_stop_rounds, first_metric_only=True, verbose=True, min_delta=0.00001)
# 设置LightGBM参数
params = {
"boosting_type": "gbdt",
"objective": "regression",
"metric": "None",
# "metric": "root_mean_squared_error",
"max_depth": 8,
"num_leaves": 63,
"min_data_in_leaf": 2,
"learning_rate": 0.05,
"feature_fraction": 0.9,
"lambda_l1": 0.1,
"lambda_l2": 0.2,
"verbose": -1, # -1时不输出
"early_stopping_round": early_stop_rounds,
"num_threads": 8,
}
# 在训练时使用自适应学习率回调函数
adaptive_lr = adaptive_learning_rate(decay_rate=0.9, patience=1000)
gbm = lgb.train(
params,
train_data,
num_boost_round=boost_round,
valid_sets=[val_data],
feval=calculate_metrics, # 将自定义指标函数作为feval参数传入
# callbacks=[print_validation_result, adaptive_lr, lgb_log, lgb_stop],
callbacks=[adaptive_lr, lgb_log, lgb_stop],
)
valid_score = gbm.best_score["valid_0"]["custom_score"]
print(f"best_valid_score: {valid_score}")
gbms.append(gbm)
return gbms