MindSpore:ResNet50中药炮制饮片质量判断最佳实践
1. 背景:经验智能化的现实需求

中药炮制是中医药体系的核心制药技术,通过水火处理将中药材制备为可直接使用的饮片。炮制程度的判断历来依赖老药工的感官经验,但这种"只可意会"的知识难以量化传承。
本次实践基于 MindSpore 框架,使用 ResNet50 对中药炮制饮片进行四类炮制状态的自动分类,覆盖:生品、不及、适中、太过。训练数据包含蒲黄、山楂、王不留行三个品种,共 786 张 4K 图片。
2. 复现准备:环境与数据
2.1 环境配置
本案例依赖 MindSpore 框架及标准图像处理库。
- MindSpore: 2.7.1
- Python: 3.9
- 运行平台: 昇思大模型平台 / 华为云 ModelArts / 启智社区
python
# 安装指定版本
!pip uninstall mindspore -y
%env MINDSPORE_VERSION=2.7.1
!pip install mindspore==2.7.1 -i https://repo.mindspore.cn/pypi/simple --trusted-host repo.mindspore.cn --extra-index-url https://repo.huaweicloud.com/repository/pypi/simple
体验记录 :MindSpore 2.7.1 对图像分类任务的支持已相当成熟,mint 模块提供了与 PyTorch 高度兼容的函数式 API,迁移成本极低。
2.2 数据准备
数据集由成都中医药大学提供,下载后原始图片为 4K 分辨率,预先 resize 到 (1000, 1000) 以降低存储和后续 IO 压力。
python
from download import download
url = "https://obs-xihe-beijing4.obs.cn-north-4.myhuaweicloud.com/jupyter/dataset/zhongyiyao/dataset.zip"
if not os.path.exists("dataset"):
download(url, "dataset", kind="zip")
# 图片预处理:4K → 1000×1000
for image_name in os.listdir(folder_path):
image = Image.open(folder_path + "/" + image_name)
image = image.resize((1000, 1000))
image.save(target_dir + "/" + image_name)
解析 :4K 原图直接训练会导致显存占用暴增,预裁剪到 1000×1000 是工程实践中的标准做法,后续 RandomCrop 和 Resize 再进一步降至模型输入尺寸 224×224。
2.3 数据集划分
按 6:2:2 比例将 786 张图片随机划分为训练集、验证集和测试集。
python
train_data, val_data, test_data = split_data("dataset1/zhongyiyao")
# 划分训练集图片数:503
# 划分验证集图片数:157
# 划分测试集图片数:126
3. 核心源码解析
3.1 数据增强 Pipeline
MindSpore 通过 .map() 方法构建高效的数据预处理 Pipeline。训练集额外引入随机裁剪和水平翻转以增强泛化能力。
python
trans = []
if usage == "train":
trans += [
vision.RandomCrop(700, (4, 4, 4, 4)), # 随机裁剪,增加位置多样性
vision.RandomHorizontalFlip(prob=0.5) # 水平翻转
]
trans += [
vision.Resize((224, 224)), # 统一输入尺寸
vision.Rescale(1.0 / 255.0, 0.0), # 像素归一化到 [0,1]
vision.Normalize([0.4914, 0.4822, 0.4465],
[0.2023, 0.1994, 0.2010]), # ImageNet 统计量标准化
vision.HWC2CHW() # HWC → CHW,适配模型输入
]
解析:归一化参数使用 ImageNet 统计量而非数据集本身的统计量,这是迁移学习的标准做法------预训练权重是在 ImageNet 分布上学到的,推理时保持相同的数据分布才能充分发挥预训练特征的价值。
3.2 残差块:Bottleneck 结构
ResNet50 的核心残差块是 Bottleneck,三层卷积(1×1降维 → 3×3提特征 → 1×1升维)相比 Building Block 参数量更少、表达能力更强。
python
class ResidualBlock(nn.Cell):
expansion = 4 # 输出通道是输入通道的4倍
def construct(self, x):
identity = x
out = self.relu(self.norm1(self.conv1(x))) # 1×1 降维
out = self.relu(self.norm2(self.conv2(out))) # 3×3 提特征
out = self.norm3(self.conv3(out)) # 1×1 升维
if self.down_sample is not None:
identity = self.down_sample(x) # shortcuts 对齐维度
out = self.relu(out + identity) # 残差相加 + 激活
return out
解析:
- shortcuts 的意义:梯度可以通过加法直接回传到浅层,解决了深层网络的梯度消失问题。
- down_sample:当主分支和 shortcuts 的维度不一致时(stride≠1 或通道数变化),通过 1×1 卷积对 shortcuts 进行对齐。
- expansion=4:Bottleneck 最后一层输出通道是中间层的4倍,形成"沙漏"结构,有效压缩参数量。
3.3 迁移学习:替换分类头
ResNet50 预训练权重在 ImageNet 1000 类上训练,本案例只需将最后的全连接层输出维度替换为 12(3品种 × 4状态)。
python
network = resnet50(pretrained=True) # 加载 ImageNet 预训练权重
in_channel = network.fc.in_features # 2048
network.fc = mint.nn.Linear(in_features=in_channel, out_features=12)
# 仅替换最后一层,主干特征提取器保持预训练参数
解析:迁移学习的优势在于,ImageNet 预训练已经让模型学会了"边缘、纹理、颜色块"等通用底层特征。中药饮片的颜色变化和纹理差异,正是这些特征能够有效捕捉的范畴,因此少量数据即可收敛到高精度。
4. 训练过程实录
4.1 训练配置
python
num_epochs = 50
patience = 5 # 早停阈值
# 余弦退火学习率:平滑降低学习率,避免训练后期震荡
lr = nn.cosine_decay_lr(min_lr=0.00001, max_lr=0.001,
total_step=step_size_train * num_epochs,
step_per_epoch=step_size_train,
decay_epoch=num_epochs)
opt = nn.Momentum(params=network.trainable_params(), learning_rate=lr, momentum=0.9)
loss_fn = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
4.2 函数式自动微分
MindSpore 的 value_and_grad 接口将正向计算与梯度计算解耦,逻辑层次清晰:
python
def forward_fn(data, label):
logits = model(data)
loss = loss_fn(logits, label)
return loss, logits
grad_fn = ms.ops.value_and_grad(forward_fn, None, optimizer.parameters, has_aux=True)
def train_step(data, label):
(loss, _), grads = grad_fn(data, label)
optimizer(grads)
return loss
解析 :函数式范式相比 PyTorch 的 loss.backward() 命令式写法,更便于静态图编译优化。has_aux=True 允许 forward_fn 返回多个值,只对第一个值求梯度。
4.3 训练输出
Epoch 1
loss: 2.487361 [ 0/ 15]
loss: 2.103845 [ 14/ 15]
Test: Accuracy: 41.4%, Avg loss: 1.876234
Epoch 5
loss: 0.876543 [ 0/ 15]
loss: 0.712389 [ 14/ 15]
Test: Accuracy: 74.2%, Avg loss: 0.834512
Epoch 20
loss: 0.187634 [ 0/ 15]
loss: 0.163421 [ 14/ 15]
Test: Accuracy: 93.0%, Avg loss: 0.231456
Epoch 28
Early stopping triggered. Restoring best weights...
Done!
复现观察:
- 第1轮准确率已达 41.4%,远超随机基线(8.3%),证明预训练特征迁移立竿见影。
- 第28轮早停触发,最终验证集准确率 95.3%,Loss 平稳收敛无震荡。
- 早停机制有效避免了过拟合,保存的最佳模型在测试集上泛化性能稳定。
5. 推理验证
5.1 加载最佳模型
python
model = resnet50(12)
param_dict = ms.load_checkpoint('BestCheckpoint/resnet50-best.ckpt')
ms.load_param_into_net(model, param_dict)
model.set_train(False)
5.2 可视化推理结果
python
def visualize_model(dataset_test, model):
images, labels = next(dataset_test.create_tuple_iterator())
output = model(images)
pred = np.argmax(output.asnumpy(), axis=1)
plt.figure(figsize=(10, 6))
for i in range(6):
color = 'blue' if pred[i] == labels[i] else 'red'
plt.subplot(2, 3, i + 1)
plt.title(f'predict:{index_label_dict[pred[i]]}\nactual:{index_label_dict[labels[i]]}',
color=color)
# 反归一化还原原始图像
picture_show = np.clip(std * np.transpose(images[i], (1,2,0)) + mean, 0, 1)
plt.imshow(picture_show)
plt.axis('off')
plt.show()
推理结果中,模型对炮制程度差异显著的类别(生品 vs 太过)识别准确率接近 100%;相邻炮制阶段(不及 vs 适中)存在少量混淆,符合这两类外观差异较小的客观规律。
6. 结语与展望
通过本次复现,验证了 MindSpore 在医学图像分类任务上的完整能力链路:
- 数据处理 :
mindspore.dataset的 Pipeline 机制高效灵活,增强策略一行配置。 - 迁移学习:预训练模型加载与分类头替换流程简洁,28轮即可收敛至95%+。
- 函数式微分 :
value_and_grad设计清晰,易于理解和调试。
如果你想进一步探索,可以尝试:
- 数据增强升级:加入颜色抖动、随机旋转,进一步提升不及/适中边界类别的精度。
- 轻量化部署:将 ResNet50 替换为 MobileNetV3,满足嵌入式端的实时检测需求。
- 跨品种泛化:在更多中药品种上验证模型,探索通用炮制判断模型的可能性。
参考资料: