- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
前言
-
什么是循环神经网络(RNN)?
- 前面学过的CNN(卷积神经网络)擅长处理图像这种空间结构数据,但它没法处理文本中这种先后的联系。比如,看一句话我今天心情好,CNN可以提取每个字的特征,但它不知道"今天"在"心情"前面,也不知道"我"是这句话的主语。而RNN的核心思想是:网络在处理当前时刻的输入时,会同时记住上一时刻的状态,然后把两者结合起来做判断。
代码实现
设置 GPU 与导入依赖
python
# 设置 GPU 与导入依赖
import numpy as np
import pandas as pd
import torch
from torch import nn
import torch.nn.functional as F
import seaborn as sns
#设置GPU训练,也可以使用CPU
device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
device

本地读取并加载数据
-
数据分析思路:
- 本次实验的数据是结构化表格数据(
heart.csv),包含 13 项特征(年龄、性别、血压等生理指标)和一个二分类标签。说明如下:- age:年龄
- sex:性别
- cp:胸痛类型 (4 values)
- trestbps:静息血压
- chol:血清胆甾醇 (mg/dl
- fbs:空腹血糖 > 120 mg/dl
- restecg:静息心电图结果 (值 0,1 ,2)
- thalach:达到的最大心率
- exang:运动诱发的心绞痛
- oldpeak:相对于静止状态,运动引起的ST段压低
- slope:运动峰值 ST 段的斜率
- ca:荧光透视着色的主要血管数量 (0-3)
- thal:0 = 正常;1 = 固定缺陷;2 = 可逆转的缺陷
- target: 0 = 心脏病发作的几率较小 1 = 心脏病发作的几率更大
- 本次实验的数据是结构化表格数据(
python
# 读取 CSV 数据文件
df = pd.read_csv("heart.csv")
print(f"数据形状: {df.shape[0]} 行 × {df.shape[1]} 列")
print(f"特征列名: {list(df.columns[:-1])}")
print(f"标签列名: {df.columns[-1]}")
print(df.iloc[:,-1].value_counts().to_string())
df.head()

python
# 特征工程 & 数据集划分
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
X = df.iloc[:,:-1]
y = df.iloc[:,-1]
# 将每一列特征标准化为标准正太分布
sc = StandardScaler()
X = sc.fit_transform(X)
print(f"标准化后的特征范围: [{X.min():.2f}, {X.max():.2f}]")
X = torch.tensor(np.array(X), dtype=torch.float32)
y = torch.tensor(np.array(y), dtype=torch.int64)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.1, random_state = 70)
print(f"训练集样本数: {len(X_train)}")
print(f"测试集样本数: {len(X_test)}")
# 维度扩增使其符合RNN模型可接受shape
X_train = X_train.unsqueeze(1)
X_test = X_test.unsqueeze(1)
print(f"X_train shape: {X_train.shape} → (样本数, seq_len=1, 特征数=13)")
print(f"X_test shape: {X_test.shape}")
print(f"y_train shape: {y_train.shape}")
print(f"y_test shape: {y_test.shape}")

python
# DataLoader
from torch.utils.data import TensorDataset, DataLoader
train_dl = DataLoader(TensorDataset(X_train, y_train),
batch_size=64,
shuffle=False)
test_dl = DataLoader(TensorDataset(X_test, y_test),
batch_size=64,
shuffle=False)
# 取出一个 batch 看看实际形状
sample_X, sample_y = next(iter(train_dl))
print(f"一个 batch 的数据形状: {sample_X.shape}")
print(f"一个 batch 的标签形状: {sample_y.shape}")
print(f"标签示例: {sample_y[:5].tolist()}")

构建循环神经网络(RNN)模型
-
模型结构分析:
-
本次构建的是一个单层单向 RNN + 两层全连接的二分类模型,结构如下:
-
RNN 层:
nn.RNN(input_size=13, hidden_size=200, num_layers=1, batch_first=True)。输入为 13 维特征向量,隐藏状态维度为 200,单层 RNN。batch_first=True让输入张量的形状为(batch, seq_len, input_size)。 -
全连接层 1:
nn.Linear(200, 50),将 RNN 输出的 200 维隐藏状态压缩到 50 维,进一步提取高层特征。 -
全连接层 2(输出层):
nn.Linear(50, 2),输出 2 个类别的 logits,配合 CrossEntropyLoss 得到最终分类概率。
-
-
python
class model_rnn(nn.Module):
def __init__(self):
super(model_rnn, self).__init__()
self.rnn0 = nn.RNN(input_size=13 ,hidden_size=200,
num_layers=1, batch_first=True)
self.fc0 = nn.Linear(200, 50)
self.fc1 = nn.Linear(50, 2)
def forward(self, x):
out, _ = self.rnn0(x)
out = out[:, -1, :] # 只取最后一个时间步的输出
out = self.fc0(out)
out = self.fc1(out)
return out
model = model_rnn().to(device)
model

python
model(torch.rand(30, 1, 13).to(device)).shape

训练函数
python
# 训练循环
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset) # 训练集的大小
num_batches = len(dataloader) # 批次数目, (size/batch_size,向上取整)
train_loss, train_acc = 0, 0 # 初始化训练损失和正确率
for X, y in dataloader: # 获取图片及其标签
X, y = X.to(device), y.to(device)
# 计算预测误差
pred = model(X) # 网络输出
loss = loss_fn(pred, y) # 计算网络输出和真实值之间的差距,targets为真实值,计算二者差值即为损失
# 反向传播
optimizer.zero_grad() # grad属性归零
loss.backward() # 反向传播
optimizer.step() # 每一步自动更新
# 记录acc与loss
train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
train_loss += loss.item()
train_acc /= size
train_loss /= num_batches
return train_acc, train_loss
测试函数
python
def test (dataloader, model, loss_fn):
size = len(dataloader.dataset) # 测试集的大小
num_batches = len(dataloader) # 批次数目, (size/batch_size,向上取整)
test_loss, test_acc = 0, 0
# 当不进行训练时,停止梯度更新,节省计算内存消耗
with torch.no_grad():
for imgs, target in dataloader:
imgs, target = imgs.to(device), target.to(device)
# 计算loss
target_pred = model(imgs)
loss = loss_fn(target_pred, target)
test_loss += loss.item()
test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()
test_acc /= size
test_loss /= num_batches
return test_acc, test_loss
超参数配置与正式训练
python
loss_fn = nn.CrossEntropyLoss() # 创建损失函数
learn_rate = 1e-3 # 学习率
opt = torch.optim.Adam(model.parameters(),lr=learn_rate)
epochs = 30
train_loss = []
train_acc = []
test_loss = []
test_acc = []
for epoch in range(epochs):
model.train()
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
model.eval()
epoch_test_acc, epoch_test_loss = test(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)
# 获取当前的学习率
lr = opt.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(epoch+1, epoch_train_acc*100, epoch_train_loss,
epoch_test_acc*100, epoch_test_loss, lr))
print("训练完成")


可视化训练结果
python
import matplotlib.pyplot as plt
from datetime import datetime
# 隐藏警告
import warnings
warnings.filterwarnings("ignore") # 忽略警告信息
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.dpi'] = 200 # 分辨率
# 获取当前时间
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
epochs_range = range(epochs)
plt.figure(figsize=(12, 3))
# 左图:准确率
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc, label='训练准确率')
plt.plot(epochs_range, test_acc, label='测试准确率')
plt.legend(loc='lower right')
plt.title('训练与验证准确率')
# 右图:损失
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='训练损失')
plt.plot(epochs_range, test_loss, label='测试损失')
plt.legend(loc='upper right')
plt.title('训练与验证损失')
# 在整个图的上方添加时间
plt.suptitle(f"训练完成时间: {current_time}", fontsize=10, y=1.02)
plt.tight_layout()
plt.show()

模型评估与混淆矩阵
- 除了准确率外,混淆矩阵的四个象限分别表示:
- 左上:真阴性(TN)------ 预测没病,实际也没病
- 右上:假阳性(FP)------ 预测有病,实际没病(误报)
- 左下:假阴性(FN)------ 预测没病,实际有病(漏报,危害最大)
- 右下:真阳性(TP)------ 预测有病,实际也有病
python
print("输入数据Shape为")
print("X_test.shape:",X_test.shape)
print("y_test.shape:",y_test.shape)
pred = model(X_test.to(device)).argmax(1).cpu().numpy()
print("\n输出数据Shape为")
print("pred.shape:",pred.shape)
python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
# 计算混淆矩阵
cm = confusion_matrix(y_test, pred)
plt.figure(figsize=(4,3))
plt.suptitle('')
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues")
# 修改字体大小
plt.xticks(fontsize=10)
plt.yticks(fontsize=10)
plt.title("混淆矩阵", fontsize=12)
plt.xlabel("预测标签", fontsize=10)
plt.ylabel("真实标签", fontsize=10)
# 显示图
plt.tight_layout() # 调整布局防止重叠
plt.show()

单样本预测测试
python
test_X = X_test[0].unsqueeze(1) # X_test[0]即我们的输入数据
pred = model(test_X.to(device)).argmax(1).item()
print("模型预测结果为:",pred)
print("0:不会患心脏病")
print("1:可能患心脏病")

学习总结
- 这次做实验让我对RNN有了简单理解。简单说,RNN 就是一个会记事的神经网络。普通神经网络看每个输入都是孤立的,但RNN不一样,它在看当前这个词(或数据)的时候,还会把过去的信息一起考虑进去。但RNN到底能记住多长的事?这次处理的是心脏病预测,每个样本就是一堆静态指标,没有时间顺序。为了用 RNN,硬是把每条数据都孤立,根本没用上 RNN 的记忆能力。但真的给它一长串有前后关系的数据,它能回溯到多久以前的信息呢?