Datawhale AI 夏令营:siRNA药物药效预测----Task 2

Task2:深入理解赛题,入门RNN和特征工程

【前面都是理论加代码讲解,不感兴趣请直接点【目录:实际操作】,但是基础知识最好是有所了解,不然可能会有看不懂代码或者不理解为什么这么写的情况】

内容概要

本任务我们对官方的baseline进行分析解读,之后介绍RNN相关的基础知识,包括其适用范围和问题。随后,我们从特征工程构建的角度来重新分析赛题数据,并将其和lightgbm结合,最终给出一个更好的baseline。

【LightGBM(Light Gradient Boosting Machine)是一个开源的梯度提升框架,由微软开发和维护。它在机器学习领域被广泛使用,特别是在处理大规模数据集和高维特征时表现优异。
以下是关于LightGBM的一些关键特点和优势:

1.高效性:LightGBM以高效处理大规模数据而闻名。它使用了基于直方图的决策树算法,能够更快地训练模型和进行预测。
2.低内存使用:相比其他梯度提升框架,LightGBM在处理大规模数据时内存占用更少,这使得它在内存受限的情况下依然能够高效运行。
3.并行和分布式学习:LightGBM支持并行学习,可以利用多核处理器加速训练过程。此外,它还支持分布式学习,能够在分布式计算框架上运行,处理更大规模的数据集。
4.高准确性:LightGBM在处理复杂的问题和高维特征时,通常能够取得比较好的预测结果,因为它能够更好地处理特征之间的关联性。
5.自定义损失函数:除了支持常见的损失函数(如均方误差、交叉熵等),LightGBM还支持自定义损失函数,使得用户可以根据特定问题定制适合的损失函数。
6.稀疏优化:LightGBM能够直接处理稀疏数据,这对于自然语言处理(NLP)等应用尤为重要。

Part1: 官方baseline分析 

在baseline中,我们只用到了siRNA_antisense_seq和modified_siRNA_antisense_seq_list,它们都是由一串符号标记的序列,我们希望的是把这些序列特征能够输入RNN模型,因此需要对其做一定处理。在SiRNAModel类的forward方法中,展示了在得到序列特征的tensor表示后的处理步骤:

def forward(self, x):
    # 将输入序列传入嵌入层
    embedded = [self.embedding(seq) for seq in x]
    outputs = []
    ...

那么这里的输入x是什么呢?我们可以通过train_loader来查看一个batch内的输入情况,这里的inputs和上面的x是一个东西。我们首先发现inputs包含两个元素,它们分别对应的是前面提到的两个使用的特征,每个元素的尺寸都是64*25,64代表batch的大小,25代表序列的长度。这里我们可以从inputs[0][0]看到每一行数据的siRNA_antisense_seq被向量化后的情况,这个例子中我们发现前面的7位是非零数,表示其序列编码后每一位的唯一标识;而后面都是0,这是因为RNN模型的输入需要每个样本的长度一致,因此我们需要事先算出一个所有序列编码后的最大长度,然后补0。

【好像知道为什么按上一篇我写的把64改为100以后分数反而下降了,torch.Size([64]),torch.Size是一个元组,用于表示张量的维度大小,bs=64,在深度学习中,通常会将数据按照设定的batch size分批次输入到模型中进行训练,一致时能让运行更顺利和高效】

那么我们怎么能得到这个唯一标识呢?我们首先需要把序列给进行分词,siRNA_antisense_seq的分词策略是3个一组(GenomicTokenizer的ngram和stride都取3)进行token拆分,比如AGCCGAGAU会被分为[AGC, CGA, GAU],而modified_siRNA_antisense_seq_list会进行按照空格分词(因为它本身已经根据空格分好了)。由此我们可以从整个数据集构建出一个词汇表,他负责token到唯一标识(索引)的映射:

# 创建词汇表
all_tokens = []
for col in columns:
    for seq in train_data[col]:
        if ' ' in seq:  # 修饰过的序列
            all_tokens.extend(seq.split())
        else:
            all_tokens.extend(tokenizer.tokenize(seq))
vocab = GenomicVocab.create(all_tokens, max_vocab=10000, min_freq=1)

有了这个词汇表,我们就可以

  • 来获得序列的最大长度

  • max_len = max(max(len(seq.split()) if ' ' in seq else len(tokenizer.tokenize(seq)) 
                        for seq in train_data[col]) for col in columns)
  • 在loader获取样本的时候把token转为索引

  • def __getitem__(self, idx):
        # 获取数据集中的第idx个样本
        row = self.df.iloc[idx]  # 获取第idx行数据
        
        # 对每一列进行分词和编码
        seqs = [self.tokenize_and_encode(row[col]) for col in self.columns]
        if self.is_test:
            # 仅返回编码后的序列(测试集模式)
            return seqs
        else:
            # 获取目标值并转换为张量(仅在非测试集模式下)
            target = torch.tensor(row['mRNA_remaining_pct'], dtype=torch.float)
            # 返回编码后的序列和目标值
            return seqs, target
    
    def tokenize_and_encode(self, seq):
        if ' ' in seq:  # 修饰过的序列
            tokens = seq.split()  # 按空格分词
        else:  # 常规序列
            tokens = self.tokenizer.tokenize(seq)  # 使用分词器分词
        
        # 将token转换为索引,未知token使用0(<pad>)
        encoded = [self.vocab.stoi.get(token, 0) for token in tokens]
        # 将序列填充到最大长度
        padded = encoded + [0] * (self.max_len - len(encoded))
        # 返回张量格式的序列
        return torch.tensor(padded[:self.max_len], dtype=torch.long)

这里给了个例子:但我没看懂,欢迎解答

此时,对于某一行数据,其两个特征分别为AGCCUUAGCACA和u u g g u u Cf c,假设整个数据集对应token编码后序列的最大长度为10,那么得到的特征就可能是

  • [25, 38, 25, 24, 0, 0, 0, 0, 0, 0]

  • [65, 65, 63, 63, 65, 65, 74, 50, 0, 0]

那么假设batch的大小为16,此时forword函数的x就会是两个列表,每个列表的tensor尺寸为16 * 10

 Part2: RNN模型分析

上一小节已经得到了数据的张量化表示,此时就要把它输入模型。

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)
        # 初始化GRU层
        self.gru = nn.GRU(embed_dim, hidden_dim, n_layers, bidirectional=True, batch_first=True, dropout=dropout)
        # 初始化全连接层
        self.fc = nn.Linear(hidden_dim * 4, 1)  # hidden_dim * 4 因为GRU是双向的,有n_layers层
        # 初始化Dropout层
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x):
        # 将输入序列传入嵌入层
        embedded = [self.embedding(seq) for seq in x]
        outputs = []
        
        # 对每个嵌入的序列进行处理
        for embed in embedded:
            x, _ = self.gru(embed)  # 传入GRU层
            x = self.dropout(x[:, -1, :])  # 取最后一个隐藏状态,并进行dropout处理
            outputs.append(x)
        
        # 将所有序列的输出拼接起来
        x = torch.cat(outputs, dim=1)
        # 传入全连接层
        x = self.fc(x)
        # 返回结果
        return x.squeeze()

RNN,全称为递归神经网络(Recurrent Neural Network),是一种人工智能模型,特别擅长处理序列数据。它和普通的神经网络不同,因为它能够记住以前的数据,并利用这些记忆来处理当前的数据。想象你在读一本书。你在阅读每一页时,不仅仅是单独理解这一页的内容,还会记住前面的情节和信息。这些记忆帮助你理解当前的情节并预测接下来的发展。这就是 RNN 的工作方式。假设你要预测一个句子中下一个单词是什么。例如,句子是:“我今天早上吃了一个”。RNN 会根据之前看到的单词(“我今天早上吃了一个”),预测下一个可能是“苹果”或“香蕉”等。它记住了之前的单词,并利用这些信息来做出预测。

  • RNN 在处理序列数据时具有一定的局限性:

    • 长期依赖问题:RNN 难以记住和利用很久以前的信息。这是因为在长序列中,随着时间步的增加,早期的信息会逐渐被后来的信息覆盖或淡化。

    • 梯度消失和爆炸问题:在反向传播过程中,RNN 的梯度可能会变得非常小(梯度消失)或非常大(梯度爆炸),这会导致训练过程变得困难。

  • LSTM 的改进

    • LSTM 通过引入一个复杂的单元结构来解决 RNN 的局限性。LSTM 单元包含三个门(输入门、遗忘门和输出门)和一个记忆单元(细胞状态),这些门和状态共同作用,使 LSTM 能够更好地捕捉长期依赖关系。

      • 输入门:决定当前输入的信息有多少会被写入记忆单元。

      • 遗忘门:决定记忆单元中有多少信息会被遗忘。

      • 输出门:决定记忆单元的哪些部分会作为输出。

    • 通过这些门的控制,LSTM 可以选择性地保留或遗忘信息,从而有效地解决长期依赖和梯度消失的问题。

  • GRU 的改进

    • GRU 是 LSTM 的一种简化版本,它通过合并一些门来简化结构,同时仍然保留了解决 RNN 局限性的能力。GRU 仅有两个门:更新门和重置门。

      • 更新门:决定前一个时刻的状态和当前输入信息的结合程度。

      • 重置门:决定忘记多少之前的信息。

    • GRU 的结构更简单,计算效率更高,同时在许多应用中表现出与 LSTM 类似的性能。

我们在pytorch的GRU文档中可以找到对应可选的参数信息,我们需要特别关注的参数如下,它们决定了模型的输入输出的张量维度

  • input_size(200)

  • hidden_size(256)

  • bidirectional(True)

假设输入的BatchSize为16,序列最大长度为10,即x尺寸为16 * 10 * 200,那么其输出的张量尺寸为 16 * 10 * (256 * 2)。

在从GRU模型输出后,x = self.dropout(x[:, -1, :])使得输出变为了BatchSize * (hidden_dim * 2),此处取了序列最后一个位置的输出数据(注意RNN网络的记忆性),这里的2是因为bidirectional参数为True,随后x = torch.cat(outputs, dim=1)指定在第二个维度拼接后,通过全连接层再映射为标量,因此最后经过squeeze(去除维数为1的维度)后得到的张量尺寸为批大小,从而可以后续和target值进行loss计算,迭代模型。

  Part3: 数据的特征工程

  • 处理类别型变量

如何知道一个变量是类别型的呢,只需看下其值的分布,或者唯一值的个数

df.gene_target_symbol_name.nunique()
df.gene_target_symbol_name.value_counts()

 如果相较于数据的总行数很少,那么其很可能就是类别变量了,比如gene_target_symbol_name。此时,我们可以使用get_dummie函数来实现one-hot特征的构造

# 如果有40个类别,那么会产生40列,如果第i行属于第j个类别,那么第j列第i行就是1,否则为0
df_gene_target_symbol_name = pd.get_dummies(df.gene_target_symbol_name)
df_gene_target_symbol_name.columns = [
    f"feat_gene_target_symbol_name_{c}" for c in df_gene_target_symbol_name.columns
]
  • 可能的时间特征构造

在数据观察的时候发现,siRNA_duplex_id的编码方式很有意思,其格式为AD-1810676.1,我们猜测AD是某个类别,后面的.1是版本,当中的可能是按照一定顺序的序列号,因此可以构造如下特征

 

siRNA_duplex_id_values = df.siRNA_duplex_id.str.split("-|\.").str[1].astype("int")
  • 包含某些单词

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")
)
  • 根据序列模式提取特征

假设siRNA的序列为ACGCA...,此时我们可以根据上一个task中提到的rna背景知识,对碱基的模式进行特征构造

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:]

Part4: 基于lightgbm的baseline

 在得到了表格数据之后,我们可以使用任意适用于表格数据的机器学习回归模型来进行预测,此处我们简单使用了lightgbm模型:

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],
)

实际操作

1.魔搭社区选择第方式二点开启【如果这里用的人多会出现打不开说GPU资源紧张的情况,可以尝试刷新再点开启,或者就是先开方式一,然后关掉再用方式二开,就开开了,不知道是不是巧合还是卡bug了】

2.导入代码

如果是文件夹就全选导入即可

点Upload即可,然后点overwrite

3.改输入路径(最简单的方法是把data/去掉就行)

4.导入包

这样是没有包,运行一下看报错也能看出来,需要自己下载。

 很多人找不到终端,下面讲下怎么找终端。

在终端输入

pip install lightgbm

可能会有报错,这是因为有的包有冲突,你看只要Successfully下载了它就行,暂时不需要解决这部分冲突,你发现可以正常运行,如果你想要解决只需要上述命令加‘=(版本)’ ,比如

pip install numpy=1.17.0【这里只是个例子,请看提示要版本在什么区间】

然后就可以运行得到submission.csv文件了

 上海科学智能研究院去提交就行

我昨天说要改算法,但我还没尝试,尝试了一下改数值大概能高个0.01分,感兴趣可以试试,我数值差不多都改了,记得数值应该是减小的,效果最好目前是到十九,等下再试试,写文没关GPU,不多说了,还是老话,别光尝试改数值,试试改算法

  • 20
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值