Datawhale AI夏令营学习笔记 Task2:baseline调优

1,043 阅读6分钟

0.前言

在本章节中我们会简单介绍一下夏令营中我对baseline调优的一些方法。本章主要分为两个部分,基于Lightgbm优化baseline,以及通过模型混合、模型替换的方案来尝试优化baseline

1. 基于特征工程与Lightgbm优化baseline

1.1 特征选择与Lightgbm简介

这次datawahle的官方教程中给出了一个不错的调优方案,基于Lightgbm来优化baseline,这里我们先简单了解一下Lightgbm和特征工程。

LightGBM(Light Gradient Boosting Machine)是一种基于梯度提升框架的机器学习算法,专门用于解决分类和回归等问题。它是由微软团队开发的,旨在提供高效、快速和准确的梯度提升算法实现。

特征工程是将原始数据转化为特征,更好表示预测模型处理的实际问题,提升对于未知数据的准确性。它是用目标问题所在的特定领域知识或者自动化的方法来生成、提取、删减或者组合变化得到特征。

在本次的任务中,我们可以观察到几个个比较明显的可以用来作为特征的字段数据特点,以下重点介绍几个比较重要的特征构造

  1. 可能的时间特征构造:在数据观察的时候发现,siRNA_duplex_id可能是按某种序列号进行编号
siRNA_duplex_id_values = df.siRNA_duplex_id.str.split("-|.").str[1].astype("int")
  1. 包含特定单词:Hepatocytes,Cells
df_cell_line_donor = pd.get_dummies(df.cell_line_donor)
df_cell_line_donor.columns = [
    f"feat_cell_line_donor_{c}" for c in df_cell_line_donor.columns
]
# 包含Hepatocytes
df_cell_line_donor["feat_cell_line_donor_hepatocytes"] = (
    (df.cell_line_donor.str.contains("Hepatocytes")).fillna(False).astype("int")
)
# 包含Cells
df_cell_line_donor["feat_cell_line_donor_cells"] = (
    df.cell_line_donor.str.contains("Cells").fillna(False).astype("int")
)
  1. 根据siRNA序列模式提取特征:根据碱基对应的情况来提取特征
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"):
            # 第一个和最后一个是否是A/U/G/C
            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"
    )
    # 第二位和倒数第二位是否为A
    df[f"feat_siRNA_{name}_seq_pattern_9"] = s.str[1] == "A"
    df[f"feat_siRNA_{name}_seq_pattern_10"] = s.str[-2] == "A"
    # GC占整体长度的比例
    df[f"feat_siRNA_{name}_seq_pattern_GC_frac"] = (
        s.str.contains("G") + s.str.contains("C")
    ) / s.str.len()
    return df.iloc[:, 1:]

那么接下来我们来看看lightbm的基础构造


train_data = lgb.Dataset(X_train, label=y_train)
test_data = lgb.Dataset(X_test, label=y_test, reference=train_data)

def print_validation_result(env):
    result = env.evaluation_result_list[-1]
    print(f"[{env.iteration}] {result[1]}'s {result[0]}: {result[2]}")

params = {
    "boosting_type": "gbdt",
    "objective": "regression",
    "metric": "root_mean_squared_error",
    "max_depth": 7,
    "learning_rate": 0.02,
    "verbose": 0,
}

gbm = lgb.train(
    params,
    train_data,
    num_boost_round=15000,
    valid_sets=[test_data],
    callbacks=[print_validation_result],
)

这里重点留意一下params字段,这里有一些背景知识需要补充

  1. "boosting_type": "gbdt": GBDT(Gradienta Boosting Decision Tree),全名叫梯度提升决策树,是一种迭代的决策树算法,又叫MART(Multiple Additive Regression Tree),它通过构造一组弱的学习器(树),并把多棵决策树的结果累加起来作为最终的预测输出。该算法将决策树与集成思想进行了有效的结合。

image.png

  1. "max_depth": 7:max_depth主要用来限制树的深度
  2. "learning_rate": 0.02:一个非常重要的超参数,用于控制每次优化迭代的步长,学习率需要调整在一个适中的范围内,太高或者太低都不行。

image.png

1.2 速通优化baseline

如上一节一样,我们在魔搭平台开始测试

在环境中创建baseline_lgbopt目录,并在baseline_lgbopt下创建data目录

image.png

image.png

这里要用terminal安装一下lightbm,具体指令为pip install lightgbm,我这里已经安装过了,所以显示包已经安装

image.png

接下来就是跑一下lgb.iqynb,步骤与Task1一样,

来看看结果,还是比baseline提升了不少了。

task2_7_30.png

2. 尝试通过模型替换和模型融合来提升baseline

2.1 LSTM替换GRU

LSTM和GRU一样是为了解决RNN中梯度爆炸与梯度消失的产物,两者在结构上相似,GRU对LSTM进行了优化,将LSTM三个门(遗忘门,输入门,输出门)减少到了两个门(更新门与重置门),具体的图片如下所示

image.png

根据笔者之前的经验LSTM在某些情况下表现会比GRU更好,在本次任务中也尝试使用了一下这个方法,重新编写后的模型代码如下

class SiRNAModel(nn.Module): 
    def __init__(self, vocab_size, embed_dim=200, hidden_dim=256, n_layers=3, dropout=0.5): super(SiRNAModel, self).**init**()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.lstm = nn.LSTM(embed_dim, hidden_dim, n_layers, bidirectional=True, batch_first=True, dropout=dropout, batch_first=True)
        self.fc = nn.Linear(hidden_dim * 4, 1)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        embedded = [self.embedding(seq) for seq in x]
        outputs = []
        for embed in embedded:
            x, _ = self.lstm(embed) 
            x = self.dropout(x[:, -1, :])  
            outputs.append(x)

        x = torch.cat(outputs, dim=1)
        x = self.fc(x)
        return x.squeeze()

那么我们实验一下:

image.png

这个是一个令我非常疑惑的结果,在训练函数完全相同的情况下,得分掉了,理论上LSTM在门更多的情况下应该可以保证结果较GRU好些或者持平,具体的机理还是不明,笔者LSTM和GRU的工作机理理解还是不够透彻,在之后的学习里面还是得加强。这里有两个怀疑:

  1. 笔者的代码优化不到位,或者有错误的地方
  2. GRU在部分场景下确实比LSTM会有更好的准确度表现

2.2 模型融合:

这次我们采用的模型是Transformer,这里我们编写TrasnformerModel函数

class TransformerModel(nn.Module):
    def __init__(self, vocab_size, embed_dim=200, hidden_dim=256, n_heads=4, num_layers=2, dropout=0.5):
        super(TransformerModel, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.transformer = nn.TransformerEncoder(
            nn.TransformerEncoderLayer(d_model=embed_dim, nhead=n_heads, dim_feedforward=hidden_dim, dropout=dropout),
            num_layers=num_layers
        )
        self.fc = nn.Linear(embed_dim, 1)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        embedded = [self.embedding(seq) for seq in x]
        outputs = []
        for embed in embedded:
            embed = embed.permute(1, 0, 2)  
            x = self.transformer(embed)
            x = x[-1, :, :]  z
            x = self.dropout(x)
            outputs.append(x)
        x = torch.cat(outputs, dim=1)
        x = self.fc(x)
        return x.squeeze()

本次的设计大约是分配给每一个模型一个权重,根据权重来得出最终的预测结果,基于权重的预测代码如下

def blend_models(models, weights, dataloader, device='cuda'):
    model_preds = np.zeros((len(dataloader.dataset), 1))
    total_weight = sum(weights)
    
    for model, weight in zip(models, weights):
        model.eval()
        preds = []
        with torch.no_grad():
            for inputs, _ in dataloader:
                inputs = [x.to(device) for x in inputs]
                outputs = model(inputs).cpu().numpy()
                preds.append(outputs)
        preds = np.concatenate(preds, axis=0)
        model_preds += preds * weight

    model_preds /= total_weight
    targets = np.array([target for _, target in dataloader.dataset])
    score = calculate_metrics(targets, model_preds)
    return model_preds, score

由于笔者的测试次数已经用完,故此基于权重的测试结果需要等待新的测试之后进行讨论,目前只是有一个初步的代码结构。