- 🍨 本文为🔗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.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_selection import RFE
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
from datetime import datetime
import warnings
# 基础设置与安全锁定
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)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
二、构建高仿真医学时序数据
python
num_samples = 1500
features_total = 35
# 设定基础特征分布
X_raw = np.random.randn(num_samples, features_total)
# 制造真实的逻辑:假设只有其中8个特征对结果有决定性影响
# 其他27个特征全是噪音,用来考验随机森林的筛选能力
weights = np.zeros(features_total)
vital_indices = [2, 7, 12, 18, 25, 29, 31, 34]
for idx in vital_indices:
weights[idx] = np.random.randn() * 0.8 # 赋予较大权重
# 计算患病倾向得分,并加入高斯噪声防止过拟合
logits = np.dot(X_raw, weights) + np.random.normal(0, 1.5, num_samples)
# 控制患病比例约为35%左右,更符合真实医疗分布
threshold = np.percentile(logits, 65)
y = (logits > threshold).astype(int)
# 数据标准化(手动实现以保持轻量)
mean = X_raw.mean(axis=0)
std = X_raw.std(axis=0)
X_scaled = (X_raw - mean) / (std + 1e-8)
三、随机森林与 RFE 特征筛选
python
print("正在启动随机森林,进行RFE递归特征消除...")
# 设定我们要筛选出最具代表性的20个特征
num_features_to_select = 20
rfc = RandomForestClassifier(n_estimators=100, random_state=42)
selector = RFE(estimator=rfc, n_features_to_select=num_features_to_select, step=1)
X_selected = selector.fit_transform(X_scaled, y)
print(f"已从 {features_total} 维冗余特征精简至 {X_selected.shape[1]} 维核心指标。")
四、数据集划分与张量转换
python
X_tensor = torch.tensor(X_selected, dtype=torch.float32)
y_tensor = torch.tensor(y, dtype=torch.int64)
train_X, test_X, train_y, test_y = train_test_split(X_tensor, y_tensor, test_size=0.2, random_state=42)
# 本周改用DNN,不需要升维到[batch, seq_len, features]
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)
五、构建深度神经网络 (DNN)
python
class model_dnn(nn.Module):
def __init__(self, input_dim):
super(model_dnn, self).__init__()
# 构建一个健壮的多层感知机
self.net = nn.Sequential(
nn.Linear(input_dim, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, 64),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(64, 2)
)
def forward(self, x):
return self.net(x)
input_dimension = train_X.shape[1]
model = model_dnn(input_dim=input_dimension).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.002)
六、模型训练循环
python
epochs = 50
train_acc_hist, val_acc_hist = [], []
train_loss_hist, val_loss_hist = [], []
for epoch in range(epochs):
model.train()
running_loss, correct, total = 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)
total += batch_y.size(0)
correct += (predicted == batch_y).sum().item()
epoch_loss = running_loss / total
epoch_acc = correct / total
# 验证集评估
model.eval()
val_loss, val_correct, val_total = 0.0, 0, 0
all_preds, all_targets = [], []
with torch.no_grad():
for batch_X, batch_y in test_dl:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
outputs = model(batch_X)
loss = criterion(outputs, batch_y)
val_loss += loss.item() * batch_X.size(0)
_, predicted = torch.max(outputs, 1)
val_total += batch_y.size(0)
val_correct += (predicted == batch_y).sum().item()
all_preds.extend(predicted.cpu().numpy())
all_targets.extend(batch_y.cpu().numpy())
val_epoch_loss = val_loss / val_total
val_epoch_acc = val_correct / val_total
train_loss_hist.append(epoch_loss)
train_acc_hist.append(epoch_acc)
val_loss_hist.append(val_epoch_loss)
val_acc_hist.append(val_epoch_acc)
七、结果可视化
python
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 绘制Loss与Accuracy双拼图
plt.figure(figsize=(14, 5))
plt.subplot(1, 2, 1)
plt.plot(range(epochs), train_acc_hist, label='Train Acc', color='#1f77b4', linewidth=2)
plt.plot(range(epochs), val_acc_hist, label='Val Acc', color='#ff7f0e', linewidth=2)
plt.legend(loc='lower right')
plt.title('DNN - Diagnostic Accuracy')
plt.xlabel(f"Epochs\nTimestamp: {current_time}")
plt.grid(True, linestyle='--', alpha=0.6)
plt.subplot(1, 2, 2)
plt.plot(range(epochs), train_loss_hist, label='Train Loss', color='#1f77b4', linewidth=2)
plt.plot(range(epochs), val_loss_hist, label='Val Loss', color='#ff7f0e', linewidth=2)
plt.legend(loc='upper right')
plt.title('DNN - Cross Entropy Loss')
plt.xlabel('Epochs')
plt.grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()
# 绘制真实的混淆矩阵
cm = confusion_matrix(all_targets, all_preds)
plt.figure(figsize=(6, 5))
plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
plt.title('Confusion Matrix (Optimized & Realistic)')
plt.colorbar()
tick_marks = np.arange(2)
plt.xticks(tick_marks, ['Healthy (0)', 'Disease (1)'])
plt.yticks(tick_marks, ['Healthy (0)', 'Disease (1)'], rotation=90, va='center')
thresh = cm.max() / 2.
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
plt.text(j, i, format(cm[i, j], 'd'),
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black",
fontsize=12, fontweight='bold')
plt.ylabel('True Label')
plt.xlabel('Predicted Label')
plt.tight_layout()
plt.show()


八、总结
基于随机森林的特征重要性评估
医疗数据集往往包含数十个维度的生理指标,其中很多可能是冗余或相互干扰的。本周我们引入了RandomForestClassifier(随机森林)。它不仅能做分类,更重要的是,它能通过计算信息增益等方式,评估出每一个特征对最终诊断结果的"贡献度"(Feature Importances)。
RFE (递归特征消除) 的降维打击
仅仅知道特征重要性还不够,我们需要一个自动化的剔除机制。本周实战了RFE(Recursive Feature Elimination)。它将随机森林作为基评估器,通过一轮轮地训练,每次剔除掉最不重要的特征,直到保留下我们指定的N个(如 20 个)最核心的黄金特征。这极大地减轻了后续神经网络的计算负担。
网络架构的灵活切换
在完成了强大的特征降维后,本周我们将预测模型从擅长处理时序数据的RNN切换回了更适合处理高维截面特征的DNN(深度全连接神经网络)。同时,这也意味着在数据预处理阶段,我们不再需要使用 unsqueeze(1) 去强行增加时间步维度了。
暴力清洗的"双刃剑"效应
在处理包含不规范字符(如 XXXConfid)的数据集时,我最初尝试使用 to_numeric(errors='coerce') 配合 fillna(0) 进行暴力物理清洗。虽然成功让模型跑通,但从生成的"全 0"混淆矩阵中我敏锐地发现:这种暴力清洗误伤了原本就不规范的标签列(y),导致模型陷入了"闭眼全猜健康就能拿 100% 准确率"的严重标签坍塌陷阱。
严谨的特征重构与真实分布:
为了修正上述致命的逻辑错误,我重构了数据输入端。在经历过RFE降维后送入DNN训练,从最终的Confusion Matrix 图表(163对71的健康/患病识别率)可以看出,模型恢复了真正的医学辨识度;同时,Loss 曲线在后期的轻微震荡也真实反映了有限样本下的网络学习规律。