
RNN实战心脏病预测 本文为365天深度学习训练营 中的学习记录博客 原作者K同学啊文章目录RNN实战心脏病预测目标一、前期准备1.前期库准备2.数据加载与标准化3.转Tensor与划分数据集二、构建网络结构RNN三、训练函数和测试函数的编写四、正式训练绘图五、模型评估混淆矩阵目标如何前期数据处理如何把表格数据适配成 RNN 的输入格式对RNN有一定理解一、前期准备1.前期库准备importnumpyasnpimportpandasaspdimporttorchimporttorch.nnasnnimporttorch.nn.functionalasFimportseabornassnsfromdatetimeimportdatetimefrommatplotlibimportpyplotaspltimportwarnings warnings.filterwarnings(ignore)# --- matplotlib 中文显示配置 ---plt.rcParams[figure.dpi]100plt.rcParams[font.sans-serif][SimHei]plt.rcParams[axes.unicode_minus]Falsedevicetorch.device(cudaiftorch.cuda.is_available()elsecpu)print(device,flushTrue)这一处除了额外导入seaborn用来画混淆矩阵和对 matplotlib 做中文显示配置外其余与常规流程并无差别。2.数据加载与标准化dfpd.read_csv(D:\deeplearning\heart\heart.csv)MODEtestfromsklearn.preprocessingimportStandardScaler#标准正太分布Xdf.iloc[:,:-1]#特征输入ydf.iloc[:,-1]#标签# df.iloc按位置取数据 df.iloc[行列]scalerStandardScaler()Xscaler.fit_transform(X)#将特征标准正太分布(x-mean)/stdheart.csv是一份心脏病数据集最后一列是标签0/1前面 13 列是特征。用df.iloc按位置取数据先列后行df.iloc[行列]把特征和标签拆开。特征之间量纲差异很大比如年龄和血压的数值根本不在一个量级如果不处理模型很难收敛所以用StandardScaler把每个特征都归一化到标准正态分布(x-mean)/std让均值归 0、标准差归 1。3.转Tensor与划分数据集##划分数据集Xtorch.tensor(np.array(X),dtypetorch.float32)##神经网络要求32位浮点ytorch.tensor(np.array(y),dtypetorch.int64)##交叉熵要求标签为int 64#numpy.array():把数据转成numpy数组#原先的DataFrame,有列名有行号Numpy纯数字方括号Tensor,外面套tensor()XX.unsqueeze(1)## 原本的X.shape为30313变为(303,1,13)符合RNN可接受的shapedatasettorch.utils.data.TensorDataset(X,y)train_sizeint(0.8*len(dataset))test_sizelen(dataset)-train_size train_dataset,test_datasettorch.utils.data.random_split(dataset,[train_size,test_size],generatortorch.Generator().manual_seed(42))# torch.utils.data.random_split 是懒加载返回的是特征标签对train_test_split返回的是数据train_dltorch.utils.data.DataLoader(train_dataset,batch_size32,shuffleTrue)test_dltorch.utils.data.DataLoader(test_dataset,batch_size32,shuffleTrue)这里要把数据从DataFrame一路转成神经网络能吃的Tensor先转成numpy数组再套上tensor()。神经网络要求特征用 32 位浮点而交叉熵要求标签是 int64所以分别指定dtype。关键的一步是X.unsqueeze(1)它把原本的(303, 13)变成(303, 1, 13)正好符合 RNN 可接受的[batch_size, seq_len, input_size]三维形状——这也是本次任务与普通全连接最大的不同点。注意torch.utils.data.random_split是懒加载的它返回的是特征-标签对而 sklearn 的train_test_split返回的是数据本身二者不要混用。二、构建网络结构RNN##模型构建classRNN(nn.Module):def__init__(self):super().__init__()self.rnnnn.RNN(input_size13,hidden_size200,num_layers1,batch_firstTrue)# input_size 单个时间步输入特征维度# hidden_size 隐藏状态的维度即输出的记忆向量长度# num_layers RNN层数# batch_first 输入数据形状True代表读取输入x的形状## x:[batch_size,seq_len,input_size]### batch_size:一批有多少样本### seq_len: 每条样本有多少时间步### input_size: 每个时间步有多少特征## 输入x₁(13维) → x₂(13维) → x₃(13维) → x₄(13维)## ↓ ↓ ↓ ↓## ┌────┐ ┌────┐ ┌────┐ ┌────┐## h₀ → │RNN│→ h₁→ │RNN│→ h₂→ │RNN│→ h₃→ │RNN│→ h₄## └────┘ └────┘ └────┘ └────┘## ↓ ↓ ↓ ↓## out₁ out₂ out₃ out₄## seq_len(时间步)就是离散时间序列x[n]代表你不同等间隔T采样得到的数据而inpute_size(输入特征)就是不同的传感器在同一时刻得到的数据## x[x[n0],x[n1],x[n2],x[n3]] ,x[n0]代表时刻n0的观测其中有13维向量代表该时刻13个传感器读数self.fc0nn.Linear(200,50)self.fc1nn.Linear(50,2)defforward(self,x):out,_self.rnn(x)# nn.RNN返回两个值out表示所有时间步的输出隐藏状态,shape为[batch,seq_len,hidden_size]; _ 表示最后一步隐藏状态shape为[num_layers,batch,hidden_size]outout[:,-1,:]# 切片原先的out shape[batch,seq_len,hidden_size] --[batch,hidden_size]outself.fc0(out)outself.fc1(out)returnout modelRNN().to(device)# model(torch.rand(30, 1, 13).to(device)).shapeRNN 的关键在于它会逐时间步地处理序列并且每一步都会把上一步的记忆隐藏状态传递下来相当于模型带着记忆看数据input_size13单个时间步输入特征的维度每条样本 13 个特征hidden_size200隐藏状态的维度即输出的记忆向量长度num_layers1RNN 层数batch_firstTrue输入数据以[batch_size, seq_len, input_size]排列理解形状时要把握seq_len时间步是离散时间序列如同在不同等间隔采样点上取到的数据而input_size输入特征是同一时刻不同传感器得到的数据。以这里为例x [x[n0], x[n1], x[n2], x[n3]]其中x[n0]代表时刻 n0 的观测内含 13 维向量即该时刻 13 个特征读数。nn.RNN会返回两个值out表示所有时间步的输出隐藏状态[batch, seq_len, hidden_size]_表示最后一步的隐藏状态[num_layers, batch, hidden_size]。因为我们要做的是二分类只需要保留序列末端的整体信息所以用out[:,-1,:]把最后一个时间步的结果切片出来变成[batch, hidden_size]再依次过两个全连接层200→50→2输出 2 个类别的得分。三、训练函数和测试函数的编写##训练函数deftrain(dataloader,model,loss_fn,optimizer):sizelen(dataloader.dataset)num_batcheslen(dataloader)train_loss,train_acc0,0forX,yindataloader:X,yX.to(device),y.to(device)predmodel(X)lossloss_fn(pred,y)#这里我不明白predmodel(X),返回的是一个二维向量而y是标签是表格里的值是0或1是一个一维向量二维向量怎么和一维向量做损失## y是索引不做数值比较x返回的两个值做softmax,即e^(x[0])/(e^(x[0])e^(x[1]))表示第0维的概率若y恰好0,则直接对第0维的概率做负对数似然得到损失optimizer.zero_grad()loss.backward()optimizer.step()train_acc(pred.argmax(1)y).type(torch.float).sum().item()train_lossloss.item()train_acc/size train_loss/num_batchesreturntrain_acc,train_lossdeftest(dataloader,model,loss_fn):sizelen(dataloader.dataset)num_batcheslen(dataloader)test_loss,test_acc0,0withtorch.no_grad():forX,yindataloader:X,yX.to(device),y.to(device)predmodel(X)lossloss_fn(pred,y)test_lossloss.item()test_acc(pred.argmax(1)y).type(torch.float).sum().item()test_acc/size test_loss/num_batchesreturntest_acc,test_loss刚开始我也有个疑问predmodel(X)返回的是一个二维向量而y是标签是 0 或 1是一维的二维向量怎么和一维向量做损失其实这里的y是索引并不做数值比较——模型输出的两个值先做 softmax即e^(x[0])/(e^(x[0])e^(x[1]))得到第 0 维的概率若y恰好等于 0就直接对第 0 维的概率取负对数似然得到该样本的损失。其余处理与常见分类一致准确率比对pred.argmax(1)取出得分最大的那个类别再与真实标签y逐样本比对。累加转标量.item()把张量转成 Python 标量否则后面 matplotlib 绘图会报错。测试关闭梯度用with torch.no_grad():包裹省显存、加速推理。四、正式训练ifMODEtrain:loss_fnnn.CrossEntropyLoss()learn_rate1e-4optimizertorch.optim.Adam(model.parameters(),lrlearn_rate)epochs30train_loss,train_acc,test_loss,test_acc[],[],[],[]best_acc0forepochinrange(epochs):model.train()epoch_train_acc,epoch_train_losstrain(train_dl,model,loss_fn,optimizer)model.eval()epoch_test_acc,epoch_test_losstest(test_dl,model,loss_fn)train_acc.append(epoch_train_acc)train_loss.append(epoch_train_loss)test_acc.append(epoch_test_acc)test_loss.append(epoch_test_loss)ifepoch_test_accbest_acc:best_accepoch_test_acc PATH./best_model.pth# 保存的参数文件名torch.save(model.state_dict(),PATH)lroptimizer.state_dict()[param_groups][0][lr]template(Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}, Lr:{:.2E})print(template.format(epoch1,epoch_train_acc*100,epoch_train_loss,epoch_test_acc*100,epoch_test_loss,lr))print(Done)这里用Adam优化器、CrossEntropyLoss损失函数训练 30 个 epoch。训练前用model.train()打开训练模式验证前用model.eval()切换到评估模式。同时记录下验证集准确率最高时的模型用torch.save(model.state_dict(), PATH)存成best_model.pth方便后面测试阶段直接载入最优参数。绘图current_timedatetime.now()# 获取当前时间epochs_rangerange(epochs)plt.figure(figsize(12,3))plt.subplot(1,2,1)plt.plot(epochs_range,train_acc,labelTraining Accuracy)plt.plot(epochs_range,test_acc,labelTest Accuracy)plt.legend(loclower right)plt.title(Training and Validation Accuracy)plt.xlabel(current_time)plt.subplot(1,2,2)plt.plot(epochs_range,train_loss,labelTraining Loss)plt.plot(epochs_range,test_loss,labelTest Loss)plt.legend(locupper right)plt.title(Training and Validation Loss)plt.show()可以看到训练准确率整体略高于测试准确率损失则训练低于测试两条曲线前几轮快速变化后趋于平稳说明模型收敛正常、没有明显过拟合。五、模型评估混淆矩阵ifMODEtest:## 混淆矩阵(最简单写法) fromsklearn.metricsimportconfusion_matrix model.load_state_dict(torch.load(./best_model.pth))# 载入训练时保存的最优模型model.eval()y_true,y_pred[],[]# 收集全部真实/预测标签withtorch.no_grad():forX,yintest_dl:X,yX.to(device),y.to(device)y_truey.cpu().tolist()# 真实标签y_predmodel(X).argmax(1).cpu().tolist()# 预测标签(取得分大的那维)cmconfusion_matrix(y_true,y_pred)# cm[i][j]真实i被预测成j的个数print(pd.DataFrame(cm,index[真实0,真实1],columns[预测0,预测1]))# 对角线预测对, 非对角线预测错sns.heatmap(cm,annotTrue,fmtd,cmapBlues,xticklabels[预测0,预测1],yticklabels[真实0,真实1])plt.xlabel(预测);plt.ylabel(真实);plt.title(Confusion Matrix)plt.show()测试阶段先载入训练时保存的最优模型best_model.pth然后遍历测试集把全部真实标签和预测标签收集起来用confusion_matrix统计错对情况。cm[i][j]表示真实 i 被预测成 j 的个数所以对角线代表预测正确、非对角线代表预测错误。从图里可以看出对角线上的 15 和 36 明显大于非对角线的 8 和 2说明模型对心脑血管疾病的二分类效果不错。