简介:这份资源围绕Chinese-Medicine-163中草药图像数据集展开,面向从事深度学习图像分类、中医药AI识别研究的开发者与高校学生,帮助解决中草药识别任务中数据获取难、清洗成本高的问题。压缩包共3个文件,以inscode工程配置、html说明页面和gitignore忽略规则为主,整体约6KB,属于轻量级说明与可运行源码包,便于快速了解数据集结构与使用方式。数据集涵盖163种中药材、超过25万张图片,训练集每类约1575张、测试集每类约61张,来源于网络爬虫检索并经人工清洗整合,质量较为可靠。目前已有251人浏览学习,读者可据此获取数据集下载线索,并参考配套的深度学习分类识别思路,用于模型训练、算法验证与中草药自动化识别研究,节省数据采集与预处理成本。
1. 163种中草药数据集:从图像分类到可运行源码的落地路径
手里有一批中草药图像,想跑分类模型,却卡在数据整理这一步——这是很多做中医药信息化、植物识别或边缘端AI应用的开发者都遇到过的场景。163种中草药数据集配合可运行源码,解决的正是从原始图像到模型训练之间的断层问题。它不是单纯的图片压缩包,而是把类别标注、目录结构、训练脚本、推理入口打包成一条能跑通的链路。适合三类人:一是做图像分类练手、需要真实多类别数据的学生和转行者;二是做中药材识别App或小程序、需要快速验证模型效果的开发者;三是做中医药数字化、想评估现有模型在细粒度分类上表现的研究人员。数据集覆盖163个类别,属于中大规模细粒度分类任务,比CIFAR-10这类通用数据集更贴近实际业务分布。
2. 数据集结构与源码目录:先看清文件再动手
2.1 目录层级与类别映射关系
拿到压缩包后别急着解压到桌面就开跑。常见做法是先看目录树,确认图像是按类别分文件夹还是用CSV标注。中草药数据集通常采用ImageFolder兼容结构,即每个类别一个子目录,目录名就是类别标签。这种结构的好处是PyTorch的torchvision.datasets.ImageFolder可以直接读取,不需要额外写解析逻辑。但要注意目录名是否包含中文、空格或特殊符号,这些在跨平台时容易翻车。
# 查看压缩包内目录结构,不直接解压 unzip -l chinese_herbs_163.zip | head -40 # 解压到指定数据盘,避免系统盘空间不足 unzip chinese_herbs_163.zip -d /data/herbs163/ # 统计每个类别的图像数量,检查类别是否均衡 for d in /data/herbs163/train/*/; do echo "$(basename $d): $(ls $d | wc -l)" done | sort -t: -k2 -n | head -20上面三段命令分别做三件事:第一段不解压只看清单,确认文件数量和顶层目录;第二段指定解压路径,中草药图像数据集动辄几个GB,放系统盘容易把根分区撑满;第三段统计各类别样本数,按数量排序后能快速发现长尾类别。如果发现某个类别只有十几张图,而最多的有几百张,训练时就需要考虑重采样或类别权重。
2.2 源码入口与依赖版本
可运行源码一般包含train.py、predict.py、dataset.py、requirements.txt这几个核心文件。先看requirements.txt里的版本约束,这是最容易出问题的地方。常见做法是创建一个独立虚拟环境,不要和系统Python混用。
# dataset.py 中典型的数据增强与加载逻辑 import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader, WeightedRandomSampler def build_dataloaders(data_dir, batch_size=32, num_workers=4): # 训练集增强:随机裁剪、翻转、颜色抖动,提升细粒度分类泛化 train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 验证集只做缩放和归一化,保持评估一致性 val_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds = datasets.ImageFolder(f"{data_dir}/train", transform=train_tf) val_ds = datasets.ImageFolder(f"{data_dir}/val", transform=val_tf) # 类别不均衡时用加权采样,避免模型偏向多数类 targets = [s[1] for s in train_ds.samples] class_count = torch.bincount(torch.tensor(targets)) weights = 1.0 / class_count[targets].float() sampler = WeightedRandomSampler(weights, len(weights), replacement=True) train_loader = DataLoader(train_ds, batch_size=batch_size, sampler=sampler, num_workers=num_workers) val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers) return train_loader, val_loader, train_ds.classes这段代码的关键参数有三个:RandomResizedCrop的scale=(0.7, 1.0)控制裁剪范围,中草药图像主体通常居中,裁剪太激进会切掉关键特征;Normalize用的ImageNet均值方差,如果从头训练可以改成数据集自身统计值,但用预训练权重时保持一致;WeightedRandomSampler的replacement=True允许重复采样少数类,缓解长尾问题。num_workers在Windows上建议设为0或2,设太高容易在DataLoader处卡死。
3. 训练与推理全流程:从命令行到模型导出
3.1 训练脚本参数与启动方式
源码包里的train.py通常用argparse暴露超参数。不要直接python train.py就完事,先看帮助信息确认默认值。
# 查看可用参数 python train.py --help # 典型训练启动命令 python train.py \ --data-dir /data/herbs163 \ --model resnet50 \ --epochs 40 \ --batch-size 32 \ --lr 0.001 \ --pretrained \ --output-dir ./runs/herbs163_resnet50--pretrained表示加载ImageNet预训练权重,163类中草药属于细粒度分类,从头训练需要大量数据,预训练能显著加快收敛。--lr 0.001配合Adam优化器是常见起点,如果loss震荡明显就降到0.0005。--epochs 40不是固定值,看验证集准确率曲线,连续5个epoch不提升就可以停。输出目录建议按模型名和时间戳区分,方便对比不同实验。
3.2 推理与单张图像预测
训练完成后,predict.py负责加载权重并对单张图或整个文件夹做推理。这里容易踩的坑是预处理不一致——训练用了RandomResizedCrop,推理必须用Resize+CenterCrop,否则准确率会掉。
# predict.py 核心推理逻辑 import torch from PIL import Image from torchvision import transforms def predict(image_path, model, class_names, device="cuda"): model.eval() tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) img = Image.open(image_path).convert("RGB") tensor = tf(img).unsqueeze(0).to(device) with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1) top5 = torch.topk(probs, 5) for score, idx in zip(top5.values[0], top5.indices[0]): print(f"{class_names[idx]}: {score.item():.4f}")model.eval()必须调用,否则BatchNorm和Dropout会保持训练模式,推理结果不稳定。torch.no_grad()关闭梯度计算,减少显存占用。输出Top-5而不是Top-1,是因为细粒度分类中有些中草药外观极其相似,Top-5能给出候选供人工复核。
3.3 模型导出与部署格式选择
如果要把模型放到移动端或边缘设备,需要导出为ONNX或TorchScript。ONNX通用性好,TorchScript在PyTorch生态内更顺。
# 导出ONNX python export_onnx.py \ --checkpoint ./runs/herbs163_resnet50/best.pth \ --output ./herbs163.onnx \ --opset 12 # 验证ONNX模型输出与PyTorch一致 python -c " import onnxruntime as ort import numpy as np sess = ort.InferenceSession('herbs163.onnx') dummy = np.random.randn(1,3,224,224).astype(np.float32) out = sess.run(None, {'input': dummy}) print('ONNX output shape:', out[0].shape) "--opset 12是兼容性较好的版本,太低不支持某些算子,太高部分推理引擎不认。导出后务必用ONNX Runtime跑一遍,确认输出shape和数值范围与PyTorch一致,这一步能提前发现算子不支持的问题。
4. 避坑与排查:数据、显存、精度三类翻车现场
4.1 类别目录名含中文导致ImageFolder报错
现象:torchvision.datasets.ImageFolder读取时抛出FileNotFoundError或类别名为乱码。原因:部分系统默认编码不是UTF-8,中文目录名在Python读取时解码失败。解决:训练前用脚本批量重命名目录为拼音或英文编号,同时生成class_map.json保存映射关系。
import os, json src = "/data/herbs163/train" mapping = {} for i, name in enumerate(sorted(os.listdir(src))): new_name = f"class_{i:03d}" mapping[new_name] = name os.rename(os.path.join(src, name), os.path.join(src, new_name)) json.dump(mapping, open("class_map.json", "w"), ensure_ascii=False, indent=2)4.2 显存不足但batch size已经很小
现象:CUDA out of memory,把batch size降到8仍然报错。原因:num_workers过多导致每个worker都复制了一份数据到显存,或者验证集没有用torch.no_grad()。解决:先把num_workers设为2,确认验证循环包在torch.no_grad()里,再考虑用混合精度训练。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for imgs, labels in train_loader: imgs, labels = imgs.cuda(), labels.cuda() with autocast(): loss = criterion(model(imgs), labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad()混合精度能把显存占用降低约40%,同时训练速度提升,但要注意autocast只包裹前向传播,反向和优化器更新由GradScaler处理。
4.3 验证集准确率远高于测试集
现象:验证集准确率95%,换一批新图测试只有60%。原因:验证集和训练集来自同一批采集数据,分布一致,而实际测试图的光照、背景、拍摄角度不同。解决:从原始数据中划出一部分做“跨域验证集”,模拟真实场景,或者用更强的数据增强(随机背景替换、亮度对比度大幅扰动)提升泛化。
4.4 预训练权重加载时类别数不匹配
现象:--pretrained加载后报size mismatch for fc.weight。原因:ImageNet是1000类,中草药是163类,最后一层全连接维度不同。解决:加载时过滤掉最后一层,只加载特征提取部分。
state = torch.load("resnet50_imagenet.pth") model_state = model.state_dict() # 过滤掉分类层参数 filtered = {k: v for k, v in state.items() if k in model_state and v.shape == model_state[k].shape} model.load_state_dict(filtered, strict=False)strict=False允许部分参数不匹配,但加载后要打印哪些层被跳过,确认只是分类层。
4.5 推理速度慢但GPU利用率低
现象:单张推理耗时超过500ms,nvidia-smi显示GPU利用率不到20%。原因:数据预处理在CPU上串行执行,成为瓶颈。解决:把预处理放到GPU上做,或者用DALI加速。
# 将归一化放到GPU上,减少CPU-GPU数据传输 mean = torch.tensor([0.485, 0.456, 0.406]).cuda().view(1,3,1,1) std = torch.tensor([0.229, 0.224, 0.225]).cuda().view(1,3,1,1) tensor = tensor.cuda() tensor = (tensor - mean) / std5. 进阶技巧:用混淆矩阵定位易混类别并做定向优化
跑通训练只是第一步,真正让模型可用的是知道它在哪里出错。163类中草药里,同科同属的植物外观差异极小,比如某些伞形科药材,人眼都难分。我一般会在验证集上生成混淆矩阵,找出Top-10混淆对,然后针对性处理。
import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay def plot_confusion(model, val_loader, class_names, device="cuda"): model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: preds = model(imgs.to(device)).argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) # 只显示混淆最多的前20类,避免163x163矩阵看不清 top_idx = np.argsort(cm.sum(1))[-20:] disp = ConfusionMatrixDisplay(cm[np.ix_(top_idx, top_idx)], display_labels=[class_names[i] for i in top_idx]) disp.plot(xticks_rotation=90, cmap="Blues") plt.tight_layout() plt.savefig("confusion_top20.png", dpi=150) # 输出混淆对 for i in top_idx: for j in top_idx: if i != j and cm[i, j] > 5: print(f"{class_names[i]} -> {class_names[j]}: {cm[i,j]}次")拿到混淆对之后,三种处理方式按成本从低到高:一是对混淆类别做额外数据增强,比如随机旋转角度加大、加入高斯噪声;二是用Focal Loss替代CrossEntropy,让模型关注难分样本;三是收集更多混淆类别的图像补充训练。我一般先跑混淆矩阵,再决定要不要动损失函数,因为改损失函数需要重新调参,成本比加数据增强高。
还有一个实用技巧是测试时增强(TTA):对同一张图做多次不同裁剪和翻转,取平均概率。TTA能把准确率提升1到3个百分点,代价是推理时间翻倍。如果部署环境算力允许,值得加上。
def predict_tta(image_path, model, class_names, n_aug=5): model.eval() tf_list = [ transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean, std)]), transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224), transforms.RandomHorizontalFlip(p=1.0), transforms.ToTensor(), transforms.Normalize(mean, std)]), transforms.Compose([transforms.Resize(256), transforms.CenterCrop(200), transforms.Resize(224), transforms.ToTensor(), transforms.Normalize(mean, std)]), ] img = Image.open(image_path).convert("RGB") probs = torch.zeros(1, len(class_names)).cuda() with torch.no_grad(): for tf in tf_list: tensor = tf(img).unsqueeze(0).cuda() probs += torch.softmax(model(tensor), dim=1) probs /= len(tf_list) return probs.argmax(1).item(), probs.max().item()从那以后我每次拿到新的细粒度分类数据集,都强制先跑一遍类别分布统计和混淆矩阵,再决定用不用预训练、要不要加权采样。这两个动作花不了半小时,但能省掉后面反复调参的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取