⭐心动不如行动,一起来参加Datawhale的活动吧:
Task2:深入理解赛题,入门RNN和特征工程 - 飞书云文档 (feishu.cn)
目录
✈️一、学习baseline.py
虽然现在可以利用chat写代码,但是自己也要学能看懂代码、改代码,这很重要🌵
Docshttps://datawhaler.feishu.cn/wiki/E2egwwoWEieE0Lk0aYmccSXOnjd#HeRAdyTELoAHRfxrD1wcZciKnqd
1.1 数据预处理
1. 首先让我们眼熟两个名词
siRNA_antisense_seq
和 modified_siRNA_antisense_seq_list
- siRNA_antisense_seq:这是 siRNA 分子的反义链序列。
- 👾我们这里叫他s1
- modified_siRNA_antisense_seq_list:这是经过化学修饰的 siRNA 反义链序列列表。
- 👾我们这里叫他s2
2. 分词
使用 GenomicTokenizer 类对 s1 和 s2
进行分词处理。
- s1采用 n-gram(默认为3)分词,,比如AGCCGAGAU会被分为[AGC, CGA, GAU]
- s2采用按空格分词
3.词汇表构建
基于分词结果,创建词汇表 GenomicVocab,该词汇表将 token 映射到唯一索引。
此时,对于某一行数据,其两个特征分别为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]
- 👾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)
1.2 数据集和数据加载器
1.SiRNADataset 类
封装了数据预处理逻辑,包括分词、编码和填充到最大序列长度。
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)
2. DataLoader
用于按批次加载数据,确保每个批次的输入具有统一的长度。
1.3 前向传播
1. forward
方法
输入 x
是一批数据中序列特征的张量表示,其尺寸为 [batch_size, sequence_length]
。
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()
- inputs和x是一个东西
inputs包含两个元素,每个元素的尺寸
[batch_size, sequence_length]
都是64*25,64代表batch的大小,25代表序列的长度。inputs[0][0]是 s1 被向量化后的情况,这个例子中我们发现前面的7位是非零数,表示其序列编码后每一位的唯一标识;而后面都是0,这是因为RNN模型的输入需要每个样本的长度一致,因此我们需要事先算出一个所有序列编码后的最大长度,然后补0。
1.4 模型定义
1. SiRNAModel
类
定义了包含嵌入层、GRU 层和全连接层的 RNN 模型
嵌入层将得到的索引进行embedding
embedding:
- 将离散的符号(如单词、字符、或基因序列片段)映射到连续的向量空间的过程。
尺寸会从BatchSize * Length——>BatchSize * Length * EmbeddingSize
EmbeddingSize即embed_dim=200
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()
2. RNN理解
递归神经网络,擅长处理序列数据;不同:能够记住之前的信息
🍟举个例子:想象你在看书的时候,当你阅读每一页,你不仅看着一页,还能记得之前的内容,这些记忆帮助你理解当前的情节并预测接下来的发展。
🍪RNN例子:假设你要预测一个句子的下一个单词是什么;
句子是:“我今天早上吃了一个”。RNN 会根据之前看到的单词(“我今天早上吃了一个”),预测下一个可能是“苹果”或“香蕉”等。它记住了之前的单词,并利用这些信息来做出预测。
RNN的局限:
- 长期依赖:RNN难以记住和利用很久以前的信息。随着时间步的增加,早期的信息会逐渐被后来的信息覆盖或淡化。这会导致RNN在处理长序列时效果不佳。
- 梯度消失或爆炸:反向传播过程中,RNN的梯度可能会变得非常小(梯度消失)或非常大(梯度爆炸)。梯度消失会导致网络难以学习长序列中的信息,而梯度爆炸则会导致训练过程变得不稳定。
变种:
-
LSTM:LSTM通过引入门控机制(输入门、遗忘门和输出门)来解决长期依赖问题。
-
GRU:是LSTM的简化版本,它将LSTM中的三个门控机制合并为两个门(更新门、重置门),结构简单,性能与LSTM相当。
✈️二、数据的特征工程
之前score为0.6;原因在于数据用的特征较为简单,序列特征的构造较为粗糙,数据量不太充足的下面介绍把序列特征的问题转化为表格问题的方法,并介绍在表格数据上如何做特征工程。为提高模型性能做出努力!
2.1处理类别变量
1.判断是否为类别型(变量值分布+唯一值个数)
df.gene_target_symbol_name.nunique()
df.gene_target_symbol_name.value_counts()
2.判断类别分布:使用df_gene_target_symbol_name 、df_gene_target_symbol_name.columns判断分布;如果类别数少,我们可以使用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
]
2.2可能的时间特征构造
1.分析序列数据(siRNA)构造特征:分析siRNA_duplex_id的编码方式,其格式为AD-1810676.1,我们猜测AD是某个类别,后面的.1是版本,当中的可能是按照一定顺序的序列号,因此提取序列号和版本号,并将它们转换为整数类型。构造如下特征:
siRNA_duplex_id_values = df.siRNA_duplex_id.str[3:-2].str.strip(".").astype("int")
2.3包含某些单词的特征
通过检查cell_line_donor是否包含“Hepatocytes”和”Ceels“来构造特征。
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")
)
2.4根据序列模式提取特征
通过分析序列数据(如 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:]
2.5基于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],
)
✈️三、学习思考
🥕3.1 LightGBM和RNN什么关系?
两种不同类型的机器学习模型
-
LightGBM: 集成模型,它是一个强大的梯度提升框架,适用于各种数据集。LightGBM是GBDT的一个框架,GBDT是机器学习一个长盛不衰的模型,主要利用决策树(弱分类器)迭代训练已得到最优模型。通常用于表格数据的分类和回归问题。
-
RNN:PyTorch 构建一个深度学习模型,比如循环神经网络(RNN)或门控循环单元(GRU),来学习序列数据的复杂模式。
🥕3.2 我的训练结果过拟合?
在验证集上我的rmse一直在增加,说明训练集表现效果好,验证集在下降,可能过拟合
[1409] rmse's valid_0: 20.4058984294526
[14999] rmse's valid_0: 22.13054724251936
RMSE:均方根误差,衡量观测值与真实值之间的误差,RMSE越小,误差越小。
我通过使用早停法,暂时解决过拟合,现在:
[4687] train's rmse: 18.135 valid's rmse: 21.0444
from lightgbm import log_evaluation, early_stopping
callbacks = [log_evaluation(period=100), early_stopping(stopping_rounds=30)]
gbm = lgb.train(
params,
train_data,
num_boost_round=15000,
valid_sets=[train_data, test_data],
valid_names=['train', 'valid'],
callbacks=callbacks,
)
🎈在看不清的道路上,做可能的各种尝试,这个过程很漫长,需要耐心更需要热情 🧡