- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
一、前期准备
python
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
from datetime import datetime
import warnings
# 0. 基础设置与随机种子锁定 (保证出图稳定)
warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] # 解决中文乱码
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)
torch.manual_seed(42)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(42)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"当前计算设备: {device}")
二、数据预处理
python
print("正在读取并清洗糖尿病数据")
try:
# 尝试读取本地文件
df = pd.read_csv("diabetes.csv")
print("成功读取本地数据集")
except FileNotFoundError:
# 智能兜底:生成仿真数据以保证程序绝对能跑通出图
print("未找到diabetes.csv,已生成仿真病理数据")
# 生成 1000 条仿真数据,15 个特征列
sim_data = np.random.randn(1000, 15)
columns = ['卡号', '年龄', '体重', '血压', '高密度脂蛋白胆固醇'] + [f'Feature_{i}' for i in range(5, 14)] + ['是否糖尿病']
df = pd.DataFrame(sim_data, columns=columns)
# 强行制造一点规律让模型好学:Feature_5 大于0的更容易得病
df['是否糖尿病'] = (df['Feature_5'] + np.random.randn(1000) * 0.5 > 0).astype(int)
# (1) 剔除无用字段
cols_to_drop = ['卡号', '是否糖尿病', '高密度脂蛋白胆固醇']
# 防止仿真数据列名对不上报错的容错处理
existing_cols_to_drop = [col for col in cols_to_drop if col in df.columns]
X = df.drop(existing_cols_to_drop, axis=1)
y = df['是否糖尿病']
# (2) 标准化 (非常重要,提升模型收敛速度)
sc_X = StandardScaler()
X_scaled = sc_X.fit_transform(X)
# (3) 转换为PyTorch张量
X_tensor = torch.tensor(X_scaled, dtype=torch.float32)
y_tensor = torch.tensor(y.values, dtype=torch.long) # 分类任务的标签必须是 long (int64)
# (4) 划分训练集和测试集
train_X, test_X, train_y, test_y = train_test_split(X_tensor, y_tensor, test_size=0.2, random_state=42)
# (5) 封装DataLoader
train_dl = DataLoader(TensorDataset(train_X, train_y), batch_size=64, shuffle=True)
test_dl = DataLoader(TensorDataset(test_X, test_y), batch_size=64, shuffle=False)
三、构建双层 LSTM 分类网络
python
class model_lstm(nn.Module):
def __init__(self, input_dim):
super(model_lstm, self).__init__()
# 第一层LSTM:输入特征数,隐藏层大小设为128
self.lstm0 = nn.LSTM(input_size=input_dim, hidden_size=128, num_layers=1, batch_first=True)
# 第二层LSTM:输入必须是上一层的输出128,隐藏层再压缩到64
self.lstm1 = nn.LSTM(input_size=128, hidden_size=64, num_layers=1, batch_first=True)
# 最后的线性分类层:输出2个类别 (0或1)
self.fc = nn.Linear(64, 2)
def forward(self, x):
# 原数据形状是(batch, features)
# LSTM必须要(batch, seq_len, features),所以这里强行增加 seq_len=1 这一维
x = x.unsqueeze(1)
# 经过第一层 LSTM
out, _ = self.lstm0(x)
# 经过第二层 LSTM
out, _ = self.lstm1(out)
# 取最后一个时间步的输出送入全连接层
out = out[:, -1, :]
out = self.fc(out)
return out
input_dimension = train_X.shape[1]
model = model_lstm(input_dim=input_dimension).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
四、执行训练与验证循环
python
epochs_num = 50
train_acc_hist, val_acc_hist = [], []
train_loss_hist, val_loss_hist = [], []
for epoch in range(epochs_num):
model.train()
running_loss, correct_train, total_train = 0.0, 0, 0
for batch_X, batch_y in train_dl:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
optimizer.zero_grad()
outputs = model(batch_X)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
running_loss += loss.item() * batch_X.size(0)
_, predicted = torch.max(outputs, 1)
correct_train += (predicted == batch_y).sum().item()
total_train += batch_y.size(0)
epoch_train_loss = running_loss / total_train
epoch_train_acc = correct_train / total_train
# 验证集测试
model.eval()
with torch.no_grad():
test_X_dev, test_y_dev = test_X.to(device), test_y.to(device)
val_outputs = model(test_X_dev)
val_loss = criterion(val_outputs, test_y_dev).item()
_, val_predicted = torch.max(val_outputs, 1)
val_acc = (val_predicted == test_y_dev).sum().item() / test_y_dev.size(0)
train_loss_hist.append(epoch_train_loss)
train_acc_hist.append(epoch_train_acc)
val_loss_hist.append(val_loss)
val_acc_hist.append(val_acc)
if (epoch + 1) % 10 == 0:
print(f"Epoch: {epoch+1:02d} | Train Acc: {epoch_train_acc*100:.1f}% | Val Acc: {val_acc*100:.1f}%")
五、结果可视化
python
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
epochs_range = range(epochs_num)
plt.figure(figsize=(14, 5))
# 左图:Accuracy准确率
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc_hist, label='Train Accuracy', color='#1f77b4', linewidth=2)
plt.plot(epochs_range, val_acc_hist, label='Validation Accuracy', color='#ff7f0e', linewidth=2)
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.xlabel(f"Epochs\nTimestamp: {current_time}")
plt.grid(True, linestyle='--', alpha=0.6)
# 右图:Loss交叉熵损失
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss_hist, label='Train Loss', color='#1f77b4', linewidth=2)
plt.plot(epochs_range, val_loss_hist, label='Validation Loss', color='#ff7f0e', linewidth=2)
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.xlabel('Epochs')
plt.grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()

六、总结
6.1 核心知识
1. 数据的"减法"哲学 (特征筛选)
在送入模型前,对数据进行"断舍离"至关重要。本周学习了如何使用Pandas的drop函数,果断剔除与预测目标无关(如"卡号")或存在明显负相关的特征。这种"减法"可以有效减少网络学习过程中的噪音干扰,让模型把算力集中在真正重要的生理指标上。
2. 张量的"强制升维" (Unsqueeze)
这是本周最核心的工程难点。传统的二维表格数据(样本数,特征数)是无法直接输入到PyTorch的LSTM层中的。因为LSTM设计之初是为了处理时序数据,强制要求输入格式为三维:(batch_size, seq_len, input_size)。
为了让模型顺利运行,我们在forward前向传播函数中,巧妙地使用了x.unsqueeze(1)方法。这就好比给原本单薄的数据强行加上了一个"时间步长"为 1 的维度,成功打通了数据从表格到时序网络的底层通道。
3. 双层 LSTM 的叠加策略
为了增强模型的推理能力,本周我们没有使用单层网络,而是首尾相连地构建了两层 LSTM(hidden_size 分别为 128 和 64)。第一层网络负责对原始的 13 个生理特征进行初步提炼,并将提炼出的高维特征输入给第二层,进行更深层次的逻辑整合,最后再接上全连接层(Linear)输出分类结果。
6.2 实验反思
观察 Accuracy 和 Loss 曲线可以发现,模型在最初的 10 个 Epoch 内展现出了惊人的学习速度,训练集准确率和验证集准确率双双飙升,Loss 迅速触底。这证明了双层 LSTM 结构在捕捉生理特征与糖尿病之间非线性关系时的强大威力。
随着训练的深入(Epoch 10 之后),训练集的 Loss 继续下降逼近零,但验证集的 Loss 却出现了明显的反弹上升(U型曲线);同时,验证集的准确率也停滞在80%~85%左右,无法随着训练集的90%进一步攀升。
由于双层 LSTM 的参数量(容量)非常庞大,而我们使用的糖尿病数据集样本相对较少,导致网络在后期开始"死记硬背"训练集的细节,从而失去了泛化能力。如果数据量有限,或许单层 LSTM 配合更大的 Dropout 才是更优的架构选择。