1.赛题背景
RNA干扰(RNAi)是生物细胞内天然存在的一种基因表达调控机制,可抵御外来核酸的入侵和控制基因表达。其中小干扰RNA(siRNA)是RNAi机制的主要作用分子。siRNA相关现象及作用机制的发现获得了2006年诺贝尔生理学或医学奖,2018年世界上首款siRNA药物获得美国FDA批准。相比于传统小分子药物,siRNA具有可成药靶点多、药效强、安全性好、成本低的优势,其研发是全球范围内极具发展潜力的前沿医药领域之一。siRNA的化学修饰对siRNA在体内的稳定性、毒性、药代动力学特性至关重要,是siRNA研发中的重要影响因素,本赛题聚焦经过化学修饰的siRNA序列数据预测其对相应的信使RNA(mRNA)沉默效率指标,对指导siRNA药物设计具有重要指向性作用。
2.作品提交
task01完整教程如下:
链接: 零基础速通baseline
完整代码如下:
- 依赖库的导入
import os # 文件操作
import torch # 深度学习框架
import random # 随机数生成
import numpy as np # 数值计算
import pandas as pd # 数据处理
import torch.nn as nn # 神经网络模块
import torch.optim as optim # 优化器模块
from tqdm import tqdm # 进度条显示
from rich import print # 美化打印输出
from collections import Counter # 计数器工具
from torch.utils.data import Dataset, DataLoader # 数据集和数据加载器
from sklearn.model_selection import train_test_split # 数据集划分
from sklearn.metrics import precision_score, recall_score, mean_absolute_error # 模型评估指标
# 这些库包括了文件操作、深度学习、数据处理、模型评估等必要的工具。
# 该函数确保了在使用NumPy、Python内置随机数生成器和PyTorch时,所有的随机数生成都是可控的和可复现的,有助于实验结果的一致性。
def set_random_seed(seed):
# 设置NumPy的随机种子
np.random.seed(seed)
# 设置Python内置的随机数生成器的种子
random.seed(seed)
# 设置PyTorch的随机种子
torch.manual_seed(seed)
# 设置CUDA的随机种子
torch.cuda.manual_seed(seed)
# 设置所有CUDA设备的随机种子
torch.cuda.manual_seed_all(seed)
# 确保每次卷积算法选择都是确定的
torch.backends.cudnn.deterministic = True
# 关闭CuDNN自动优化功能,确保结果可复现
torch.backends.cudnn.benchmark = False
- 基因组分词器类
class GenomicTokenizer:
def __init__(self, ngram=5, stride=2):
# 初始化分词器,设置n-gram长度和步幅
self.ngram = ngram
self.stride = stride
def tokenize(self, t):
# 将输入序列转换为大写
t = t.upper()
if self.ngram == 1:
# 如果n-gram长度为1,直接将序列转换为字符列表
toks = list(t)
else:
# 否则,按照步幅对序列进行n-gram分词
toks = [t[i:i+self.ngram] for i in range(0, len(t), self.stride) if len(t[i:i+self.ngram]) == self.ngram]
# 如果最后一个分词长度小于n-gram,移除最后一个分词
if len(toks[-1]) < self.ngram:
toks = toks[:-1]
# 返回分词结果
return toks
- 基因组词汇类
class GenomicVocab:
def __init__(self, itos):
# 初始化词汇表,itos是一个词汇表列表
self.itos = itos
# 创建从词汇到索引的映射
self.stoi = {v: k for k, v in enumerate(self.itos)}
@classmethod
def create(cls, tokens, max_vocab, min_freq):
# 创建词汇表类方法
# 统计每个token出现的频率
freq = Counter(tokens)
# 选择出现频率大于等于min_freq的token,并且最多保留max_vocab个token
itos = ['<pad>'] + [o for o, c in freq.most_common(max_vocab - 1) if c >= min_freq]
# 返回包含词汇表的类实例
return cls(itos)
- siRNA数据集类
class SiRNADataset(Dataset):
def __init__(self, df, columns, vocab, tokenizer, max_len, is_test=False):
# 初始化数据集
self.df = df # 数据框
self.columns = columns # 包含序列的列名
self.vocab = vocab # 词汇表
self.tokenizer = tokenizer # 分词器
self.max_len = max_len # 最大序列长度
self.is_test = is_test # 指示是否是测速集
def __len__(self):
# 返回数据集的长度
return len(self.df)
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)
- siRNA Model
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()
- 评估指标计算函数
def calculate_metrics(y_true, y_pred, threshold=30):
# 计算平均绝对误差
mae = np.mean(np.abs(y_true - y_pred))
# 将实际值和预测值转换为二进制分类(低于阈值为1,高于或等于阈值为0)
y_true_binary = (y_true < threshold).astype(int)
y_pred_binary = (y_pred < threshold).astype(int)
# 创建掩码,用于筛选预测值在0和阈值之间的样本
mask = (y_pred >= 0) & (y_pred <= threshold)
range_mae = mean_absolute_error(y_true[mask], y_pred[mask]) if mask.sum() > 0 else 100
# 计算精确度、召回率和F1得分
precision = precision_score(y_true_binary, y_pred_binary, average='binary')
recall = recall_score(y_true_binary, y_pred_binary, average='binary')
f1 = 2 * precision * recall / (precision + recall)
# 计算综合评分
score = (1 - mae / 100) * 0.5 + (1 - range_mae / 100) * f1 * 0.5
return score
- 模型评估函数
def evaluate_model(model, test_loader, device='cuda'):
# 设置模型为评估模式
model.eval()
predictions = []
targets = []
# 禁用梯度计算
with torch.no_grad():
# 遍历测试数据加载器中的每个批次
for inputs, target in test_loader:
# 将输入数据移动到指定设备上
inputs = [x.to(device) for x in inputs]
# 获取模型的输出
outputs = model(inputs)
# 将预测结果从GPU移到CPU,并转换为numpy数组,添加到predictions列表中
predictions.extend(outputs.cpu().numpy())
# 将目标值转换为numpy数组,添加到targets列表中
targets.extend(target.numpy())
# 将预测结果和目标值转换为numpy数组
y_pred = np.array(predictions)
y_test = np.array(targets)
# 计算评估指标
score = calculate_metrics(y_test, y_pred)
# 打印测试得分
print(f"Test Score: {score:.4f}")
- 模型训练函数
def train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs=50, device='cuda', output_dir: str=""):
# 将模型移动到指定设备
model.to(device)
best_score = -float('inf') # 初始化最佳得分
best_model = None # 初始化最佳模型
for epoch in range(num_epochs):
model.train() # 设置模型为训练模式
train_loss = 0 # 初始化训练损失
for inputs, targets in tqdm(train_loader, desc=f'Epoch {epoch+1}/{num_epochs}'):
inputs = [x.to(device) for x in inputs] # 将输入移动到设备
targets = targets.to(device) # 将目标值移动到设备
optimizer.zero_grad() # 清空梯度
outputs = model(inputs) # 前向传播
loss = criterion(outputs, targets) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新参数
train_loss += loss.item() # 累加训练损失
model.eval() # 设置模型为评估模式
val_loss = 0 # 初始化验证损失
val_preds = []
val_targets = []
with torch.no_grad():
for inputs, targets in val_loader:
inputs = [x.to(device) for x in inputs] # 将输入移动到设备
targets = targets.to(device) # 将目标值移动到设备
outputs = model(inputs) # 前向传播
loss = criterion(outputs, targets) # 计算损失
val_loss += loss.item() # 累加验证损失
val_preds.extend(outputs.cpu().numpy()) # 收集预测值
val_targets.extend(targets.cpu().numpy()) # 收集目标值
train_loss /= len(train_loader) # 计算平均训练损失
val_loss /= len(val_loader) # 计算平均验证损失
val_preds = np.array(val_preds)
val_targets = np.array(val_targets)
score = calculate_metrics(val_targets, val_preds) # 计算验证集上的得分
print(f'Epoch {epoch+1}/{num_epochs}')
print(f'Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}')
print(f'Learning Rate: {optimizer.param_groups[0]["lr"]:.6f}')
print(f'Validation Score: {score:.4f}')
if score > best_score:
best_score = score # 更新最佳得分
best_model = model.state_dict().copy() # 更新最佳模型
torch.save(model.state_dict(), os.path.join(output_dir, "best.pt".format(epoch))) # 保存最佳模型
print(f'New best model found with score: {best_score:.4f}')
return best_model # 返回最佳模型
- 训练主程序
# 设置参数
bs = 64 # 批次大小
epochs = 50 # 训练的迭代次数
lr = 0.001 # 学习率
seed = 42 # 随机种子
output_dir = "output/models" # 模型保存路径
# 选择设备
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# 设置随机种子以确保结果可重复
set_random_seed(seed)
# 创建输出目录
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 加载数据
train_data = pd.read_csv('train_data.csv')
# 指定需要处理的列
columns = ['siRNA_antisense_seq', 'modified_siRNA_antisense_seq_list']
# 删除包含空值的行
train_data.dropna(subset=columns + ['mRNA_remaining_pct'], inplace=True)
# 将数据分为训练集和验证集
train_data, val_data = train_test_split(train_data, test_size=0.1, random_state=42)
# 创建分词器
tokenizer = GenomicTokenizer(ngram=3, stride=3)
# 创建词汇表
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)
# 创建数据集
train_dataset = SiRNADataset(train_data, columns, vocab, tokenizer, max_len)
val_dataset = SiRNADataset(val_data, columns, vocab, tokenizer, max_len)
# 创建数据加载器
train_loader = DataLoader(train_dataset, batch_size=bs, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=bs)
# 初始化模型
model = SiRNAModel(len(vocab.itos))
criterion = nn.MSELoss()
# 初始化优化器
optimizer = optim.Adam(model.parameters(), lr=lr)
# 训练模型
best_model = train_model(model, train_loader, val_loader, criterion, optimizer, epochs, device, output_dir=output_dir)
- 测试程序
# 设置输出目录
output_dir = "result"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 加载测试数据
test_data = pd.read_csv('sample_submission.csv')
columns = ['siRNA_antisense_seq', 'modified_siRNA_antisense_seq_list']
test_data.dropna(subset=columns, inplace=True)
# 创建分词器
tokenizer = GenomicTokenizer(ngram=3, stride=3)
# 创建词汇表
all_tokens = []
for col in columns:
for seq in test_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 test_data[col]) for col in columns)
# 创建测试数据集
test_dataset = SiRNADataset(test_data, columns, vocab, tokenizer, max_len, is_test=True)
# 创建数据加载器
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
# 初始化模型
model = SiRNAModel(len(vocab.itos))
model.load_state_dict(best_model) # 加载最佳模型权重
model.to(device=device)
model.eval() # 切换到评估模式,这对于某些模块如Dropout和BatchNorm是必需的
# 进行预测
preds = []
with torch.no_grad():
for inputs in tqdm(test_loader):
# import pdb;pdb.set_trace()
inputs = [x.to(device) for x in inputs]
outputs = model(inputs)
preds.extend(outputs.cpu().numpy())
# 将预测结果添加到测试数据中
test_data["mRNA_remaining_pct"] = preds
df = pd.DataFrame(test_data)
# 保存预测结果
output_csv = os.path.join(output_dir, "submission.csv")
print(f"submission.csv 保存在 {output_csv}")
df.to_csv(output_csv, index=False)