Datawhale Al夏令营——siRNA药物药效预测Baseline解读与改进尝试
解读baseline
1、为什么要self.ngram = ngram,而不是直接赋值
定义实例属性使得类(class)的其他方法(其他def)可以直接访问和使用 ngram 的值。例如,tokenize 方法需要使用 ngram 的值来执行分词操作,如果不将其定义为实例属性,就无法在 tokenize 方法中直接访问。
class GenomicTokenizer:
def __init__(self, ngram=5, stride=2):
self.ngram = ngram
self.stride = stride
# 假设使用实例属性
def tokenize(self, t):
t = t.upper()
if self.ngram == 1:
toks = list(t)
else:
toks = [t[i:i+self.ngram] for i in range(0, len(t), self.stride) if len(t[i:i+self.ngram]) == self.ngram]
return toks
# 假设不使用实例属性,而是硬编码
def tokenize(self, t):
t = t.upper()
ngram = 5
stride = 2
if ngram == 1:
toks = list(t)
else:
toks = [t[i:i+ngram] for i in range(0, len(t), stride) if len(t[i:i+ngram]) == ngram]
return toks
2、列表推导式
列表推导式(List Comprehension)是一种在Python中用来创建列表的简洁语法。它允许你从一个序列(如列表、元组、字符串等)或者另一个列表推导式,通过一个表达式来生成新的列表。列表推导式通常包含一个表达式,后面跟着一个for循环,以及可选的if条件语句。
基本语法如下:
[expression for item in iterable if condition]
expression
是对每个元素进行的操作。item
是从iterable
中取出来的元素。iterable
是一个序列或者任何可迭代的对象。condition
是一个可选的条件语句,用于筛选元素。
例如,如果你有一个数字列表,你想创建一个新的列表,其中只包含原列表中的偶数,你可以使用列表推导式如下:
original_list = [1, 2, 3, 4, 5, 6]
even_numbers = [num for num in original_list if num % 2 == 0]
在这个例子中,even_numbers
将会是 [2, 4, 6]
。
3、类方法和实例方法
"类方法"和"实例方法"是面向对象编程中的两个概念,它们都与类和对象的行为有关,但它们的使用场景和作用不同。
-
实例方法:这是最常见的方法类型,它们与类的实例(对象)相关联。实例方法的第一个参数通常是
self
,它代表当前对象的实例。当调用实例方法时,你通常是在对某个具体的对象进行操作。 -
类方法:与实例方法不同,类方法是与类本身相关联的方法。这意味着它们不需要创建类的实例就可以被调用。类方法的第一个参数通常是
cls
,它代表类本身。使用类方法可以访问类属性或执行与类相关的操作,而不需要创建类的实例。
在Python中,使用 @classmethod
装饰器来定义一个类方法。这个装饰器告诉Python解释器,接下来的函数是一个类方法,而不是一个实例方法。当你调用一个类方法时,你可以使用类名直接调用,也可以通过类的实例调用,但传递给方法的第一个参数将是类本身,而不是实例。
在Python中,类方法和实例方法的区别可以通过一个实际案例来更清晰地理解:
为了更好地理解类方法和实例方法的区别及其用途,下面是一个具体的例子。假设我们在开发一个员工管理系统,我们需要管理员工的信息并生成员工的工号。
实例方法
实例方法依赖于类的实例(对象),它们操作实例数据。实例方法的第一个参数通常是 self
,表示实例本身。
class Employee:
def __init__(self, name, age):
self.name = name
self.age = age
self.employee_id = None
def set_employee_id(self, employee_id):
self.employee_id = employee_id
def get_info(self):
return f"Name: {self.name}, Age: {self.age}, Employee ID: {self.employee_id}"
使用实例方法
# 创建员工实例
emp1 = Employee("Alice", 30)
emp2 = Employee("Bob", 35)
# 设置员工ID
emp1.set_employee_id("E001")
emp2.set_employee_id("E002")
# 获取员工信息
print(emp1.get_info()) # 输出: Name: Alice, Age: 30, Employee ID: E001
print(emp2.get_info()) # 输出: Name: Bob, Age: 35, Employee ID: E002
在这个例子中:
set_employee_id
和get_info
是实例方法,它们操作和访问实例的数据(name
、age
和employee_id
)。- 每个实例(
emp1
和emp2
)都有独立的数据。
类方法
类方法不依赖于类的实例,它们操作类本身的数据或提供一些通用功能。类方法的第一个参数通常是 cls
,表示类本身。
class Employee:
employee_count = 0 # 类变量,所有实例共享
def __init__(self, name, age):
self.name = name
self.age = age
self.employee_id = None
Employee.employee_count += 1
def set_employee_id(self, employee_id):
self.employee_id = employee_id
def get_info(self):
return f"Name: {self.name}, Age: {self.age}, Employee ID: {self.employee_id}"
@classmethod
def generate_employee_id(cls):
return f"E{cls.employee_count + 1:03d}"
使用类方法
# 创建员工实例
emp1 = Employee("Alice", 30)
emp2 = Employee("Bob", 35)
# 使用类方法生成员工ID
emp1.set_employee_id(Employee.generate_employee_id())
emp2.set_employee_id(Employee.generate_employee_id())
# 获取员工信息
print(emp1.get_info()) # 输出: Name: Alice, Age: 30, Employee ID: E003
print(emp2.get_info()) # 输出: Name: Bob, Age: 35, Employee ID: E004
# 查看总员工数
print(Employee.employee_count) # 输出: 2
在这个例子中:
generate_employee_id
是一个类方法,它不依赖于具体的实例,而是依赖于类的数据(employee_count
)。employee_count
是一个类变量,所有实例共享,用于跟踪员工的总数。
区分类方法和实例方法
-
作用域不同:
- 实例方法作用于类的实例,操作实例数据。
- 类方法作用于类本身,操作类的数据或提供一些通用功能。
-
使用场景不同:
- 当需要访问或修改实例的属性时,使用实例方法。
- 当需要执行与类相关的操作且不依赖于实例数据时,使用类方法。例如,统计类的实例数量、生成唯一的实例标识符等。
-
设计上的清晰性和组织性:
- 将不同类型的方法分开,可以使代码更清晰、更易于维护。类方法适用于全局操作或类级别的操作,实例方法适用于具体实例的操作。
4、class、def和类方法的关系,参见class GenomicVocab:
提高模型表现
序列编码
在处理基因序列数据时,通常需要将核酸序列转换为数值表示形式,以便输入到深度学习模型中。词汇表(vocab)是一种将序列中的每个元素(如核苷酸或核苷酸组合)映射到一个唯一的数值索引的结构。在baseline中,使用了一个基于3-gram的词汇表,这意味着每三个连续的核苷酸组合成一个“单词”。这种方法能够捕捉序列中的局部模式,并提高模型的预测能力。但是是否可以加入更多的编码以提高模型表现值得探究
特征选择
特征选择则是选择最能代表数据特征的字段,以提高模型的性能和训练效率。这里的特征包括siRNA序列、修饰后的siRNA序列、靶mRNA序列以及实验条件(如药物浓度、细胞系、转染方式等)。
在baseline中,只用到了
- siRNA_antisense_seq:siRNA的antisense序列
- modified_siRNA_antisense_seq_list:带修饰的siRNA的antisense序列
它们都是由一串符号标记的序列,为了提高其他信息的利用能力,在此我们对每一列进行逐一说明,并进行能否利用的判断。
表头字段 | 表头说明 | 利用判断 |
---|---|---|
id | 数据唯一识别号 | 略去 |
--- | --- | --- |
publication_id | 公开文献号 | 略去 |
gene_target_symbol_name | 靶基因符号名称,靶基因即为siRNA想要沉默的目标基因 | 与ncbi_id重复,保留一个即可 |
gene_target_ncbi_id | 靶基因的NCBI标识 | 可用于标识基因信息 |
gene_target_species | 靶基因参考序列的物种,一般物种应为人类 | 物种信息可能影响实验结果 |
siRNA_duplex_id | siRNA双链编号 | 略去,用于唯一识别,不影响沉默效率 |
siRNA_sense_seq | siRNA的sense序列 | 正链 |
siRNA_antisense_seq | siRNA的antisense序列 | 反链,实际与靶mRNA结合的链,因此不需要保留正链 |
cell_line_donor | 实验使用的细胞系,一般认为对沉默效率测定存在较大影响 | 可利用 |
siRNA_concentration | 实验使用的siRNA浓度,一般认为对沉默效率测定存在较大影响 | 可利用 |
concentration_unit | siRNA浓度单位,比如nM | 列表全是nM,因此略去 |
Transfection_method | 转染方法,即将siRNA植入细胞的方法,一般认为对沉默效率测定存在较大影响 | 可利用 |
Duration_after_transfection_h | 转染后持续时间(小时) | 可利用 |
modified_siRNA_sense_seq | 带修饰的siRNA的sense序列 | 修饰正链 |
modified_siRNA_antisense_seq | 带修饰的siRNA的antisense序列 | 修饰反链 |
modified_siRNA_sense_seq_list | 带修饰的siRNA的sense序列分解列表,基于标准词表将sense序列分解,以空格分隔 | 略去 |
modified_siRNA_antisense_seq_list | 带修饰的siRNA的antisense序列分解列表,基于标准词表将antisense序列分解,以空格分隔 | 可利用 |
gene_target_seq | 靶基因的参考序列,siRNA一般靶向序列中的一部分片段以达到沉默的效果 | 可利用 |
mRNA_remaining_pct | siRNA对靶基因沉默后的剩余mRNA百分比,值越低表示沉默效率越好 | 预测值,必须用 |
模型构建
Baseline使用的是递归神经网络(RNN),它是一类深度学习模型,特别适用于处理序列数据。RNN通过在隐藏层中引入循环连接,可以有效捕捉序列中的时间依赖关系。在RNAi效率预测任务中,RNN能够通过学习siRNA序列和靶mRNA序列之间的复杂关系,准确预测其基因沉默效果。
但是我们也可以通过更改RNN模型,尝试不同的深度学习模型(如LSTM、Transformer)和传统机器学习模型(如随机森林、XGBoost)。比较不同模型的性能,选择最佳模型。下面先对baseline的RNN构建进行解读:
nn.GRU
GRU(Gated Recurrent Unit)是一种改进的循环神经网络(RNN),旨在解决传统 RNN 的梯度消失和梯度爆炸问题。GRU 通过引入门控机制来捕捉序列中的长期依赖关系。
GRU 层的基本概念
GRU 是一种特殊的 RNN,它通过更新门(update gate)和重置门(reset gate)来控制信息的流动。与 LSTM 不同的是,GRU 只有两个门,比 LSTM 更简单但效果相近。
GRU 结构
LSTM总共有三个门,遗忘门,输入门,输出门,而GRU中使用的是重置门和更新门。
\(x_t\):当前时刻输入信息
\(h_{t-1}\):上一时刻的隐藏状态。隐藏状态充当了神经网络记忆,它包含之前节点所见过的数据的信息
\(h_t\):传递到下一时刻的隐藏状态
\(\~{h}_t\):候选隐藏状态
\(r_t\):重置门
\(z_t\):更新门
\(σ\):sigmoid函数,通过这个函数可以将数据变为0-1范围的数值。
tanh: tanh函数,通过这个函数可以将数据变为[-1,1]范围的数值
如果不看内部具体的复杂关系,可以将上图简化为下图:
结合\(x_t\)和\(h_{t-1}\),GRU会得到当前隐藏节点的输出\(y_{t}\)和传递给下一个节点的隐藏状态\(h_{t}\),这个\(h_{t}\)的推导是GRU的关键所在
推导\(h_{t}\)需要\(z_{t}\)、\(h_{t-1}\)和\(\~h_{t}\),而\(\~h_{t}\)需要\(r_{t}\),\(h_{t-1}\)
这四个公式互有关联,下面进行详细展开
- 重置门(Reset Gate):作用对象是前边的隐藏状态。控制前一时刻的隐藏状态有多少信息需要遗忘
可以看到这就是上面的公式:
这里的\(W_r\)并不是一个值,而是一个权重矩阵。用这个权重矩阵对\(x_{t}\)和\(h_{t-1}\)拼接而成的矩阵进行线性变换(两个矩阵相乘)。然后将两个矩阵相乘得到的值投入sigmoide函数,会得到r_{t}的值,比如:0.6 。
所得到的\(r_{t}\)会用到候选隐藏状态的公式中,即公式:
展开后为:
\(\tilde{h}_{t}=tanh(x_{t} W_{xh}+(r_{t}\bigodot h_{t-1})W_{hh}+b_{h})\)
不难看出,当\(r_{t}\)的值越小,它与\(h_{t-1}\)哈达玛积出来的矩阵数值越小,再与权重矩阵(\(W_{hh}\))相乘得到的值越小,说明上一时刻需要遗忘的越多,丢弃的越多。
反之,当\(r_{t}\)的值越大,说明上一时刻需要记住的越多,新的输入信息(也就是当前的输入信息x_{t})与前面的记忆相结合的越多。
- 更新门(Update Gate):作用对象是当前时刻和上一时刻的隐藏单元。控制当前时刻的隐藏状态有多少信息需要保留,也就是更新门帮助模型决定到底要将多少过去的信息传递到未来。
也即公式:
而\(z_t\)将会决定\(h_t\)
\((1-z_t)*h_{t-1}\):表示对上一时刻隐藏状态进行选择性“遗忘”。忘记\(h_{t-1}\)中一些不重要的信息,把不相关的丢弃。
\(z_{t}*\~h_t\):表示对候选隐藏状态的进一步选择性”记忆“。会忘记 \(\~h_t\)中的一些不重要的信息。也就是对\(\~h_t\)中的某些信息进一步选择。
因此,\(z_{t}\)越接近1,代表“记忆”下来的数据越多;而越接近0则代表“遗忘”的越多。
\(h_{t}\)忘记传递下来的\(h_{t-1}\)中的某些信息,并加入当前节点输入的某些信息。这就是最终的记忆。
具体代码解释
self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
参数解释
-
emb_dim
(输入特征的维度)- 这是 GRU 层的输入维度,即输入序列的每个时间步的特征向量的维度。在这个上下文中,
emb_dim
是嵌入层的输出维度。
- 这是 GRU 层的输入维度,即输入序列的每个时间步的特征向量的维度。在这个上下文中,
-
hid_dim
(隐藏层的维度)- 这是 GRU 层的隐藏状态的维度。每个时间步的隐藏状态会有
hid_dim
个特征。
- 这是 GRU 层的隐藏状态的维度。每个时间步的隐藏状态会有
-
n_layers
(GRU 层的数量)- 这是堆叠的 GRU 层的数量。多层 GRU 允许模型捕捉更复杂的序列模式。
-
dropout
(Dropout 比率)- 在各层之间应用 Dropout,用于防止过拟合。
-
batch_first=True
(批次维度放在第一位)- 指定输入和输出张量的形状,批次大小放在第一位。这意味着输入和输出张量的形状为
[batch_size, seq_len, feature_dim]
。
- 指定输入和输出张量的形状,批次大小放在第一位。这意味着输入和输出张量的形状为
GRU 的前向传播过程
当你将输入序列传递给 GRU 层时,它会进行以下步骤:
-
输入处理
- 输入序列的形状为
[batch_size, seq_len, emb_dim]
。
- 输入序列的形状为
-
计算每个时间步的隐藏状态
- GRU 使用当前时间步的输入和前一时间步的隐藏状态来计算当前时间步的隐藏状态。
-
输出和隐藏状态
- 输出:每个时间步的输出序列,形状为
[batch_size, seq_len, hid_dim]
。 - 隐藏状态:最后一个时间步的隐藏状态,形状为
[n_layers, batch_size, hid_dim]
。
- 输出:每个时间步的输出序列,形状为
具体实现步骤
1. 初始化 GRU 层
self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
2. 前向传播方法
def forward(self, src):
# src shape: [batch_size, src_len]
embedded = self.dropout(self.embedding(src))
# embedded shape: [batch_size, src_len, emb_dim]
outputs, hidden = self.rnn(embedded)
# outputs shape: [batch_size, src_len, hid_dim]
# hidden shape: [n_layers, batch_size, hid_dim]
return outputs, hidden
-
输入序列
src
src
的形状为[batch_size, src_len]
。- 经过嵌入层和 Dropout 后,得到
embedded
,形状为[batch_size, src_len, emb_dim]
。
-
传递给 GRU 层
embedded
作为输入传递给 GRU 层,得到outputs
和hidden
。outputs
包含每个时间步的输出,形状为[batch_size, src_len, hid_dim]
。hidden
是最后一个时间步的隐藏状态,形状为[n_layers, batch_size, hid_dim]
。
总结
- GRU 层的作用:GRU 层通过更新门和重置门控制信息流动,有效捕捉序列中的长期依赖关系,解决传统 RNN 的梯度消失问题。
- 参数:
emb_dim
:输入特征的维度,即嵌入向量的维度。hid_dim
:隐藏状态的维度。n_layers
:GRU 层的数量。dropout
:Dropout 比率,用于防止过拟合。batch_first=True
:指定输入和输出的张量形状。
通过这种方式,GRU 层能够处理序列数据,捕捉其中的时序信息,为后续的解码或其他处理提供有用的特征表示。
Transformer模型
尝试构建中,会努力在task3完成