一、前言:医学影像 AI,真的只是"分类"吗?
目前在医学影像领域,90% 的深度学习项目都停留在分类阶段:
-
良性 / 恶性
-
有病灶 / 无病灶
-
高风险 / 低风险
但在真实临床场景中,医生更关心的问题往往是:
病灶 有没有变大?
生长速度 快还是慢?
下一阶段 可能发展到什么程度?
这类问题,本质上已经不再是"分类问题",而是变化预测(Change Prediction)问题。
本文将从建模思路 出发,并结合完整可运行的代码示例,讲清楚医学影像中"变化预测"到底应该怎么做。
二、分类任务 vs 变化预测:从数据结构就已经不同
1️⃣ 传统分类任务的数据形式
Image → Label
对应代码通常是:
image, label
模型只关心:
👉 这一刻是什么状态
2️⃣ 变化预测任务的数据形式(关键差异)
Image(t1), Image(t2) → Change
也就是:
image_t1, image_t2, delta_label
模型关心的是:
👉 从 t1 到 t2,发生了什么变化
这是后续所有建模差异的根源。
三、变化预测数据集:完整 Dataset 代码示例
下面是一个标准的变化预测 Dataset 实现(PyTorch):
python
import torch
from torch.utils.data import Dataset
import numpy as np
class LesionChangeDataset(Dataset):
def __init__(self, data_list):
"""
data_list: [
{
't1': 'path/to/image_t1.npy',
't2': 'path/to/image_t2.npy',
'delta': 2.3
},
...
]
"""
self.data_list = data_list
def __len__(self):
return len(self.data_list)
def __getitem__(self, idx):
img_t1 = np.load(self.data_list[idx]['t1'])
img_t2 = np.load(self.data_list[idx]['t2'])
delta = self.data_list[idx]['delta']
img_t1 = torch.tensor(img_t1).float().unsqueeze(0)
img_t2 = torch.tensor(img_t2).float().unsqueeze(0)
delta = torch.tensor(delta).float()
return img_t1, img_t2, delta
📌 注意 :
变化预测不是"一个 image + 一个 label",
而是一对影像 + 一个变化标签。
四、共享 CNN 特征提取器(变化预测的核心原则)
在变化预测中,有一个非常重要但经常被忽略的原则:
不同时间点的影像,必须使用同一个 CNN(共享权重)
否则模型学到的是网络差异 ,而不是病灶变化。
CNN 特征提取器实现
python
import torch.nn as nn
class FeatureCNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(16, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2)
)
self.fc = nn.Linear(32 * 32 * 32, 128)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
return self.fc(x)
五、变化建模方式一:特征差分(最推荐)
这是医学影像变化预测中最直观、最稳定的方法。
1️⃣ 特征差分函数
python
def feature_difference(f1, f2):
return f2 - f1
2️⃣ 变化回归模型
python
class ChangeRegressor(nn.Module):
def __init__(self):
super().__init__()
self.regressor = nn.Sequential(
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
def forward(self, x):
return self.regressor(x)
3️⃣ 前向传播逻辑(完整)
python
cnn = FeatureCNN()
regressor = ChangeRegressor()
f1 = cnn(img_t1)
f2 = cnn(img_t2)
diff_feature = feature_difference(f1, f2)
pred_delta = regressor(diff_feature)
📌 核心思想 :
模型不再"猜类别",而是直接学习变化本身。
六、对比方案:特征拼接(不推荐但常见)
python
combined_feature = torch.cat([f1, f2], dim=1)
python
class ChangeConcatModel(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Sequential(
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 1)
)
def forward(self, f1, f2):
x = torch.cat([f1, f2], dim=1)
return self.fc(x)
📌 实践中发现:
-
拼接模型能学
-
但对"变化"的表达不如差分明确
七、多时间点变化预测(进阶工程版)
真实医学随访往往是:
t1 → t2 → t3 → t4
这时可以引入 CNN + LSTM。
LSTM 变化趋势模型
python
class ChangeLSTM(nn.Module):
def __init__(self):
super().__init__()
self.lstm = nn.LSTM(
input_size=128,
hidden_size=64,
batch_first=True
)
self.fc = nn.Linear(64, 1)
def forward(self, feature_seq):
_, (h_n, _) = self.lstm(feature_seq)
return self.fc(h_n[-1])
构造时间序列特征
python
features = []
for img in images: # 多时间点影像
features.append(cnn(img))
feature_seq = torch.stack(features, dim=1)
pred = lstm_model(feature_seq)
📌 到这一步,模型已经能学习病灶生长趋势。
八、训练代码:变化预测该怎么训?
损失函数(回归,而不是分类)
criterion = nn.MSELoss()
训练循环(完整示例)
python
optimizer = torch.optim.Adam(
list(cnn.parameters()) + list(regressor.parameters()),
lr=1e-3
)
for img_t1, img_t2, delta in dataloader:
optimizer.zero_grad()
f1 = cnn(img_t1)
f2 = cnn(img_t2)
diff = f2 - f1
pred = regressor(diff)
loss = criterion(pred.squeeze(), delta)
loss.backward()
optimizer.step()
📌 如果你在这里用的是 CrossEntropyLoss,
那本质上还是分类思维。
九、评估指标:为什么 Accuracy 不适合?
变化预测更适合以下指标:
python
mae = torch.mean(torch.abs(pred - target))
rmse = torch.sqrt(torch.mean((pred - target) ** 2))
-
MAE:平均变化误差
-
RMSE:对极端增长更敏感
这比 Accuracy 更符合医学意义。
十、总结:变化预测不是"高级分类"
当真正把代码写完整后会发现:
变化预测 ≠ 分类 + 换个模型
而是:
数据结构不同
建模思路不同
评估方式不同
这一步,正是医学影像 AI 从"能跑"走向"有用"的关键。