深度学习项目实操——区分癌症与正常样本
本文档从项目开始,搭建机器学习模型,用于属于博主的个人练手项目,也欢迎大家指正
项目内容
我们需要做的:
- 准备表达矩阵和标签,构建输入
- 构建模型,训练模型
- 验证模型能力,使用模型进行预测
一、准备输入
先中之先把所有要用的库一口气加载了
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset, random_split
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score, accuracy_score, roc_curve
import matplotlib.pyplot as plt
我们准备好 Exp.txt以及 Label.txt前者是表达矩阵,后者为标签信息,然后准备好验证集, Val.txt以及 Val_label.txt前者为验证用的表达矩阵以及对应的标签。这些数据很好准备,可以在TCGA或者GEO中下载不同中的表达矩阵,然后将正常样本和癌症样本的信息储存在 Label.txt中,第一列为样本名,第二列为标签信息,博主这里将癌症样本标记为1,正常样本标记为0,大家可以根据自己喜好。
随后我们就可以将我们准备的文件读入,可以通过以下代码:
def load_data(exp_file, label_file, val_file, val_label_file):
# 读取表达矩阵和标签
X = pd.read_csv(exp_file, sep='\t', index_col=0)
y = pd.read_csv(label_file, sep='\t').set_index("ID")
X_val = pd.read_csv(val_file, sep='\t', index_col=0)
y_val = pd.read_csv(val_label_file, sep='\t').set_index("ID")
# 如果列名为基因名,行名为样本名,注释掉这一行
X = X.T
X_val = X_val.T
# 取样本交集
common_ids = X.index.intersection(y.index)
common_val_ids = X_val.index.intersection(y_val.index)
common_f = X.columns.intersection(X_val.columns)
if len(common_ids) == 0:
raise ValueError("没有找到 X 和 Y 的共有样本")
if len(common_val_ids) == 0:
raise ValueError("没有找到验证集中 X 和 Y 的共有样本")
if len(common_f) == 0:
raise ValueError("没有找到验证集与训练集相同的特征")
# 只保留共有样本,并确保顺序一致
X = X.loc[common_ids, common_f]
y = y.loc[common_ids]
X_val = X_val.loc[common_val_ids, common_f]
y_val = y_val.loc[common_val_ids]
# 打印信息方便检查
print(f"样本匹配完成:共有 {len(common_ids)} 个样本")
print(f" X.shape = {X.shape}, y.shape = {y.shape}\n")
print(f"样本匹配完成:共有 {len(common_val_ids)} 个样本")
print(f" X.shape = {X_val.shape}, y.shape = {y_val.shape}")
return X.values, y["Label"].values, X_val.values, y_val["Label"]
# 读入数据
X_train, y_train, X_val, y_val = load_data("Exp.txt", "label.txt", "Val.txt", "Val_label.txt")
这里只return .values的原因是
Dataloader只接受数字信息,不接受字符串信息,方便后续数据转化成Tensor和进行DataLoader。
# 数据标准化
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_val = scaler.transform(X_val)
# 转换为 Tensor
X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32)
X_val = torch.tensor(X_val, dtype=torch.float32)
y_val = torch.tensor(y_val, dtype=torch.float32)
# 转化为 DataLoader
train_dataset = TensorDataset(X_train, y_train)
val_size = int(0.2 * len(train_dataset))
train_size = len(train_dataset) - val_size
train_dataset, val_dataset = random_split(train_dataset, [train_size, val_size], generator=torch.Generator().manual_seed(42))
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
这里为什么需要进行Tensor和DataLoader呢?进行Tensor的原因是我们需要把数据转化为机器看得懂的语言,进行运算。DataLoader则是分批读取数据,事实上如果你不使用DataLoader,可以通过以下流程完成相同的功能:
- 从训练集里挑选32条数据
- 传入模型
- 等待模型吃完
- 再进行下一次循环…
所以DataLoader相当于将上述流程打包好的一个函数,能够帮我们完成这些过程,在这之中batch_size为每次读入数据的大小,shuffle选择是否打乱顺序,True为打乱顺序。在训练集中,为了让模型学习到真正的规律,而不是记住数据,我们会打乱顺序;而在验证集中,我们不希望顺序随机带来的偶然因素,所以选择固定顺序,确保结果可重复。
二、模型构建
MLP神经网络
class MLP(nn.Module):
def __init__(self, input_dim):
super(MLP, self).__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, 512),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(512, 256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, 1),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x)
model = MLP(input_dim=X_train.shape[1])
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
定义MLP神经网络模型,使用两层隐藏层,一层输出层,线性全连接层作为模型骨架。
# 损失函数和优化器
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=1e-5, weight_decay=1e-5)
这里我们做得是二分类问题,所以我们选择
nn.BCELoss作为损失函数,Adam作为优化器。
在这里提供一些常见的损失函数和优化器,以及他们的应用场景:
损失函数
| 损失函数 | 适用场景 | 说明 |
|---|---|---|
nn.MSELoss() | 回归问题(预测数值) | 计算预测值与真实值的平方差 常用于基因表达预测、药物响应值预测等 |
nn.BCELoss() | 二分类问题(输出为概率) | 计算预测概率与真实标签(0/1)的交叉熵 要求最后一层用 Sigmoid |
nn.BCEWithLogitsLoss() | 二分类 | 和上面的类似,但自动处理 Sigmoid,更稳定(数值不容易溢出) |
nn.CrossEntropyLoss() | 多分类问题(如0,1,2,3类) | 自动结合 Softmax,适合多类别任务(如不同癌症亚型) |
nn.L1Loss() | 回归问题,对异常值不敏感 | 计算绝对误差,抗异常值能力强 |
nn.KLDivLoss() | 分布对齐(如知识蒸馏、生成模型) | 比较两个概率分布的差异 |
nn.NLLLoss() | 多分类 + log softmax 输出 | 用于输出已取 log 的概率 |
nn.MarginRankingLoss() | 排序/相似性任务 | 比如预测两个样本哪个“更相似” |
nn.CosineEmbeddingLoss() | 表征学习/对比学习 | 想让相似样本的向量角度更接近 |
优化器
| 优化器 | 特点 | 适用场景 |
|---|---|---|
SGD | 传统梯度下降;稳定但慢 | 小数据集、调参实验 |
SGD(momentum=0.9) | 加动量,减少震荡 | 稳定学习、收敛更快 |
Adam | 自动调节学习率;收敛快 | 默认首选优化器(通用) |
AdamW | 改进版 Adam,权重衰减独立控制 | 深度模型训练首选(BERT, CNN等) |
RMSprop | 平滑梯度,适合循环网络 | RNN、LSTM 常用 |
Adagrad | 每个参数独立学习率 | 稀疏数据(NLP、基因序列) |
Adadelta | 自动调整学习率,无需初始值 | 小样本、鲁棒性任务 |
Lion (2023新) | Meta提出,训练更快 | 大模型优化,稳定性高 |
模型训练
num_epochs = 50
best_auc = 0
best_val_loss = float('inf')
for epoch in range(num_epochs):
model.train()
total_loss = 0
for Xb, yb in train_loader:
Xb, yb = Xb.to(device), yb.to(device)
preds = model(Xb).flatten()
loss = criterion(preds, yb)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
avg_train_loss = total_loss / len(train_loader)
model.eval()
total_val_loss = 0
with torch.no_grad(): # 验证集不需要梯度
for Xv, yv in val_loader:
Xv, yv = Xv.to(device), yv.to(device)
preds = model(Xv).flatten()
loss = criterion(preds, yv)
total_val_loss += loss.item()
avg_val_loss = total_val_loss / len(val_loader)
# 每个epoch验证
val_auc, val_acc, _, _ = evaluate(model, val_loader)
print(f"Epoch {epoch+1:03d} | \nTrainLoss={total_loss/len(train_loader):.4f} | ValLoss={avg_val_loss:.4f}\n"
f"ValAUC={val_auc:.4f} | ValACC={val_acc:.4f}")
if avg_val_loss < best_val_loss:
best_val_loss = avg_val_loss
torch.save(model.state_dict(), "best_model.pth")
print("best model saved")
模型训练的核心内容是这一段:
for Xb, yb in train_loader:
Xb, yb = Xb.to(device), yb.to(device)
preds = model(Xb).flatten()
loss = criterion(preds, yb)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
avg_train_loss = total_loss / len(train_loader)
这里的 train_loader 是一个 PyTorch 的 DataLoader,负责把训练数据分批(batch)送入模型,Xb 是输入特征,yb 是对应的标签。Xb.to(device)将数据送到device中,在这里是GPU。model(Xb) 是把这一批输入 Xb 送入模型,得到预测值 preds。在将预测值送入criterion()中得到loss,调用 loss.backward() 把梯度累加到参数的 .grad 属性上,最后optimizer.step()更新参数。
总的来说,训练的核心就是数据输入 → 模型预测 → 计算损失 → 更新参数 → 统计平均损失。
三、模型验证
# 外部验证
model.load_state_dict(torch.load("best_model.pth"))
val_loader_ext = DataLoader(TensorDataset(X_val, y_val), batch_size=64, shuffle=False)
val_auc, val_acc, y_true_ext, y_pred_ext = evaluate(model, val_loader_ext)
print(f"外部验证集:ValAUC={val_auc:.4f} | ValACC={val_acc:.4f}")
# 绘制ROC曲线
fpr, tpr, _ = roc_curve(y_true_ext, y_pred_ext)
plt.plot(fpr, tpr, label=f"AUC={val_auc:.3f}")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("ROC Curve - External Validation")
plt.legend()
plt.savefig("ROC_curve_external.png", dpi=300)
plt.show()
print("模型训练完成,已保存最佳模型。")
最后是模型验证阶段,这段因为没有调参内容,如果对这部分不太懂直接用gpt生成代码效果也很不错,最终在独立验证集中的表现如下(后面学习率和调的epoch忘记是多少了,不过练手项目也不纠结了)

1048

被折叠的 条评论
为什么被折叠?



