该项目是机器学习的课程作业,代码实现了一个1D卷积神经网络(1D CNN)模型,通过三层卷积层(通道数16→32→64)和最大池化层提取一维序列特征,最终通过全局平均池化与全连接层完成二分类任务。模型使用Adam优化器和BCEWithLogitsLoss损失函数,输入数据为320维特征(经CSVDataset转换为[batch, 1, 320]张量),适用于处理如时间序列或基因序列的二分类问题。
1.导入库
代码开始导入了以下的python库:
PyTorch:用于构建和训练神经网络的核心框架。
nn:神经网络模块(层、激活函数、损失函数)。
optim:优化器(如Adam)。
Dataset 和 DataLoader:数据加载工具。
Pandas:用于读取和处理CSV文件。
NumPy:数值计算工具。
scikit-learn:提供balanced_accuracy_score用于评估分类性能(适用于类别不平衡数据)。
2.CSVDataset类
该自定义数据集类用于加载CSV文件中的二分类数据,适配PyTorch的Dataset接口,支持数据划分、标签编码、张量转换及可选的预处理操作。
首先读取CSV文件,处理标签编码,划分特征和标签。将标签列中的"A"替换为1,"B"替换为0(适用于二分类任务)。根据split列的值筛选数据,选取第4列及之后的列作为特征,选取第3列作为标签列。
class CSVDataset(Dataset):
def __init__(self, path, type, transform=None):
data = pd.read_csv(path)
data = data.replace("A", 1)
data = data.replace("B", 0)
self.x = data.loc[data['split'] == type].iloc[:, 3:].values
self.y = data.loc[data['split'] == type].iloc[:, 2].values.reshape(-1, 1)
self.transform = transform
之后返回数据集的样本总数(即特征数据self.x的行数)。
def __len__(self):
return len(self.x)
之后根据索引idx返回单个样本的特征和标签。将NumPy数组转为PyTorch张量。为特征添加通道维度,适配Conv1d输入格式,通过transform参数支持自定义预处理。
3.CNN1D类
这是一个用于二分类任务的1D卷积神经网络(CNN),通过多层卷积和池化提取时序特征,最终通过全连接层输出分类结果。
首先进行初始化。卷积层中,conv1:输入单通道(如时序信号),输出16通道,卷积核大小3,填充1(保持输入长度不变)。conv2 和 conv3:通道数递增(16,32,64),逐步提取深层特征。池化层中最大池化(kernel_size=2),每次将序列长度减半。
正则化:Dropout层(丢弃率0.3)减少过拟合风险。全局池化:AdaptiveAvgPool1d(1) 将每个通道的时序特征压缩为1个值,生成全局特征向量。全连接层:将64维特征映射到128维,再输出1维。
class CNN1D(nn.Module):
def __init__(self):
super(CNN1D, self).__init__()
self.conv1 = nn.Conv1d(in_channels=1, out_channels=16, kernel_size=3, padding=1)
self.relu = nn.ReLU()
self.pool = nn.MaxPool1d(kernel_size=2)
self.conv2 = nn.Conv1d(in_channels=16, out_channels=32, kernel_size=3, padding=1)
self.conv3 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1)
self.dropout = nn.Dropout(p=0.3)
self.global_pool = nn.AdaptiveAvgPool1d(1)
self.fc1 = nn.Linear(64, 128)
self.fc2 = nn.Linear(128, 1)
输入数据依次通过三个卷积层(conv1、conv2、conv3),每层后接ReLU激活函数和最大池化(pool),逐步提取特征并压缩序列长度。使用自适应平均池化(global_pool)将特征压缩为全局向量,展平后通过Dropout层随机丢弃部分神经元以防止过拟合。经过两个全连接层(fc1、fc2)映射到最终输出,并通过squeeze(1)调整维度,得到形状为[batch]的二分类结果。
4.train函数代码
训练模式与梯度清零,设置模型为训练模式,遍历数据加载器的每个批次,将输入和标签移至指定设备(如GPU),并清空优化器的历史梯度。
前向传播与损失计算,通过模型前向传播得到预测值,计算损失(criterion,如交叉熵),并调用loss.backward()进行反向传播,生成参数梯度。
参数更新与损失累计,使用优化器更新模型参数,累计批次损失,最终返回整个数据集上的平均损失(总损失除以样本总数)。
def train(model, dataloader, criterion, optimizer, device):
model.train()
total_loss = 0.0
for x, y in dataloader:
x, y = x.to(device), y.to(device)
optimizer.zero_grad()
outputs = model(x)
loss = criterion(outputs, y.squeeze())
loss.backward()
optimizer.step()
total_loss += loss.item() * x.size(0)
return total_loss / len(dataloader.dataset)
5.evaluate函数
此函数用于评估模型性能,首先调用 model.eval() 将模型设置为评估模式,同时使用 torch.no_grad() 上下文管理器来禁止梯度计算,以减少内存消耗并加快推理速度。接着遍历数据加载器 dataloader 中的每个批次数据,将输入 x 和标签 y 移动到指定设备 device 上,通过模型 model 得到输出 outputs,再使用 torch.sigmoid 函数将输出转换为概率值,最后以 0.5 为阈值将概率值转换为预测标签 preds。最后将真实标签 y 和预测标签 preds 移回 CPU,使用 balanced_accuracy_score 函数计算平衡准确率并返回该评估指标。
def evaluate(model, dataloader, device):
model.eval()
with torch.no_grad():
for x, y in dataloader:
x, y = x.to(device), y.to(device)
outputs = model(x)
preds = (torch.sigmoid(outputs) > 0.5).float()
return balanced_accuracy_score(y.cpu(), preds.cpu())
6.程序主入口
主要功能即初始化训练所需的各项参数和组件,然后进行模型训练与评估,最后保存训练好的模型。
设备选择、批次大小和训练轮数设置。device:检查系统是否支持CUDA,若支持则使用 GPU 进行计算,否则使用CPU。batch_size:每次训练时输入到模型中的样本数量,这里设置为60。epochs:模型对整个训练数据集进行训练的轮数,设置为 500 轮。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
batch_size = 60
epochs = 500
数据集和数据加载器创建。train_dataset 和 test_dataset:分别从 CSV 文件中加载训练集和测试集数据,使用自定义的 CSVDataset 类进行处理。 train_loader 和 test_loader:使用 DataLoader 类将数据集封装成可迭代的数据加载器,方便按批次加载数据。训练集数据加载器会在每个 epoch 打乱数据顺序(shuffle=True),测试集则不打乱(shuffle=False)。
train_dataset = CSVDataset('./ACE_features_320_dimension.csv', type='train')
test_dataset = CSVDataset('./ACE_features_320_dimension.csv', type='test')
train_loader = DataLoader(train_dataset, batch_size=60, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=277, shuffle=False)
模型、损失函数和优化器初始化。model:实例化自定义的一维卷积神经网络模型 CNN1D,并将其移动到指定设备(GPU 或 CPU)上。criterion:使用二元交叉熵损失函数 nn.BCEWithLogitsLoss,适用于二分类问题。optimizer:使用 Adam 优化器来更新模型的参数,学习率设置为 0.002。
model = CNN1D().to(device)
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=0.002)
模型训练与评估。acc_best:用于记录模型在测试集上的最佳准确率。
for 循环:进行 500 轮训练和评估。
train_loss:调用 train 函数对模型进行一轮训练,并返回该轮的平均训练损失。
acc:调用 evaluate 函数在测试集上评估模型的准确率。
acc_best:更新最佳准确率。
print:打印当前轮次的训练损失、测试准确率和最佳准确率。
best_acc = 0.0
best_epoch = 0
best_model_path = 'cnn1d_best.pth'
for epoch in range(1, epochs + 1):
train_loss = train(model, train_loader, criterion, optimizer, device)
test_acc = evaluate(model, test_loader, device)
if test_acc > best_acc:
best_acc = test_acc
best_epoch = epoch
torch.save(model.state_dict(), best_model_path)
print(f"Epoch {epoch}/{epochs} - Loss: {train_loss:.4f} - Test Acc: {test_acc:.4f} - Best Acc: {best_acc:.4f} (Epoch {best_epoch})")
模型保存。torch.save(model, "cnn1d_binary.pth"):将训练好的模型保存到文件 cnn1d_binary.pth 中。print:输出提示信息,表示训练完成且模型已保存。
torch.save(model, "cnn1d_binary.pth")
print("train finished, the model has been saved to cnn1d_binary.pth")
- 运行结果
附录:整体代码:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import pandas as pd
from sklearn.metrics import balanced_accuracy_score
class CSVDataset(Dataset):
def __init__(self, path, type, transform=None):
data = pd.read_csv(path)
data = data.replace("A", 1)
data = data.replace("B", 0)
self.x = data.loc[data['split'] == type].iloc[:, 3:].values
self.y = data.loc[data['split'] == type].iloc[:, 2].values.reshape(-1, 1)
self.transform = transform
def __len__(self):
return len(self.x)
def __getitem__(self, idx):
xi = torch.from_numpy(self.x[idx]).type(torch.FloatTensor)
xi = xi.unsqueeze(0)
yi = torch.tensor(self.y[idx]).type(torch.FloatTensor)
if self.transform:
xi = self.transform(xi)
return xi, yi
class CNN1D(nn.Module):
def __init__(self):
super(CNN1D, self).__init__()
self.conv1 = nn.Conv1d(in_channels=1, out_channels=16, kernel_size=3, padding=1)
self.relu = nn.ReLU()
self.pool = nn.MaxPool1d(kernel_size=2)
self.conv2 = nn.Conv1d(in_channels=16, out_channels=32, kernel_size=3, padding=1)
self.conv3 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1)
self.dropout = nn.Dropout(p=0.3)
self.global_pool = nn.AdaptiveAvgPool1d(1)
self.fc1 = nn.Linear(64, 128)
self.fc2 = nn.Linear(128, 1)
def forward(self, x):
x = self.conv1(x)
x = self.relu(x)
x = self.pool(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool(x)
x = self.conv3(x)
x = self.relu(x)
x = self.pool(x)
x = self.global_pool(x)
x = x.view(x.size(0), -1)
x = self.dropout(x)
x = self.fc1(x)
x = self.relu(x)
x = self.dropout(x)
x = self.fc2(x)
return x.squeeze(1)
def train(model, dataloader, criterion, optimizer, device):
model.train()
total_loss = 0.0
for x, y in dataloader:
x, y = x.to(device), y.to(device)
optimizer.zero_grad()
outputs = model(x)
loss = criterion(outputs, y.squeeze())
loss.backward()
optimizer.step()
total_loss += loss.item() * x.size(0)
return total_loss / len(dataloader.dataset)
def evaluate(model, dataloader, device):
model.eval()
with torch.no_grad():
for x, y in dataloader:
x, y = x.to(device), y.to(device)
outputs = model(x)
preds = (torch.sigmoid(outputs) > 0.5).float()
return balanced_accuracy_score(y.cpu(), preds.cpu())
if __name__ == "__main__":
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
batch_size = 60
epochs = 500
train_dataset = CSVDataset('./ACE_features_320_dimension.csv', type='train')
test_dataset = CSVDataset('./ACE_features_320_dimension.csv', type='test')
train_loader = DataLoader(train_dataset, batch_size=60, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=277, shuffle=False)
model = CNN1D().to(device)
criterion = nn.BCEWithLogitsLoss()
#optimizer = optim.SGD(model.parameters(), lr=0.0005, momentum=0.9)
optimizer = optim.Adam(model.parameters(), lr=0.002)
best_acc = 0.0
best_epoch = 0
best_model_path = 'cnn1d_best.pth'
for epoch in range(1, epochs + 1):
train_loss = train(model, train_loader, criterion, optimizer, device)
test_acc = evaluate(model, test_loader, device)
if test_acc > best_acc:
best_acc = test_acc
best_epoch = epoch
torch.save(model.state_dict(), best_model_path)
print(f"Epoch {epoch}/{epochs} - Loss: {train_loss:.4f} - Test Acc: {test_acc:.4f} - Best Acc: {best_acc:.4f} (Epoch {best_epoch})")
print(f"Training complete. Best accuracy {best_acc:.4f} achieved at epoch {best_epoch}.")
print(f"Best model weights saved to '{best_model_path}'.")