ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于机器学习的蘑菇分类实战:从随机森林到迁移学习

基于机器学习的蘑菇分类实战:从随机森林到迁移学习 简介基于机器学习的蘑菇分类项目压缩包面向计算机、人工智能、数据科学等专业学生与开发者主要解决蘑菇可食用性分类问题可作为毕业设计、课程设计或初期项目立项演示。项目围绕蘑菇数据集展开包含数据预处理、特征分析与分类模型训练等完整环节适合从入门到进阶的学习者参考。压缩包共5个文件涵盖Python主程序.py、数据集.csv、Jupyter Notebook交互式分析.ipynb、Markdown项目说明和PPT演示文稿.pptx整体大小仅1.79MB结构紧凑、模块清晰。已有84人学习浏览。资源内代码经过测试可正常运行配套README说明文档便于快速理解代码逻辑并复现实验Notebook交互式分析环境有助于逐步观察数据处理与建模过程PPT则可直接用于答辩或课堂展示。对需要完成课程大作业、毕业设计或进行机器学习入门实践的同学具有较强借鉴价值下载后即可上手使用。1. 基于机器学习的蘑菇分类到底做出了一个什么东西第一次看到「基于机器学习的蘑菇分类python源码项目说明PPT.zip」这个标题别被后面那一串交付物唬住它本质上是把一个经典的分类问题从数据到模型再到展示完整走了一遍拿到蘑菇样本的图像或者特征表训练一个模型让它判断这个蘑菇是“可食”还是“有毒”。它的现实意义很直白——很多人以为看颜色、看形状就能认蘑菇实际上同一品种在不同生长阶段可能长得很不一样人工规则极易翻车而机器学习能把这些视觉差异和特征组合学进参数里。这个项目适合三类人做机器学习课程设计的学生刚学完 Python 基础想拿一个完整案例练手的开发者以及要在答辩现场用 PPT 讲清“数据、模型、评估”全流程的竞赛选手。源码、项目说明和 PPT 三件套对应的正是“跑得起来、讲得清楚、拿得出手”这三个诉求。我按这个标题背后的常见实现方案给你拆开讲从环境准备、数据选型、模型训练到评估和避坑照着做就能复现一个能演示的蘑菇分类系统。2. 开工前先想清楚蘑菇数据怎么选环境怎么搭2.1 表格数据还是图像数据先搞清楚你拿到的是什么蘑菇分类这个方向在公开数据集里分两条路线拿到源码先看一眼项目说明里写的是哪条不要直接跑。第一条是经典的 UCI Mushroom 数据集每一行是一朵蘑菇包含菌盖形状、菌盖颜色、气味、附着方式等 22 个特征标签只有两类可食(e)和有毒(p)。缺点是特征已经帮你抽好了没有图像那种直观的视觉冲击力答辩 PPT 里不好放“原图进模型出结果”的效果图。第二条是蘑菇图像数据集比如从公开图库里按蘑菇种类整理的各种照片模型要自己从像素里学习纹理、颜色、形状特征。这条路做出来的项目效果更完整因为它的技术栈覆盖了图像读取、数据增强、迁移学习和模型保存更像一个“机器学习项目”而不是“调库比赛”。我的建议很直接先拿表格数据用随机森林这类模型跑一个 baseline把整个处理流程走通再用图像数据上迁移学习做最终的展示模型。两个都做进源码里项目说明里写清楚各自的效果对比这比单做一个表格分类要扎实得多答辩老师问起来你也有话说。2.2 Python 环境准备Anaconda 虚拟环境和最小依赖清单跑这个项目之前先把环境隔离做好不要直接往 base 环境里乱装包否则后面 torch 和 scikit-learn 的版本打架会让你怀疑人生。我习惯用 Anaconda 建一个独立虚拟环境Python 版本选 3.9 或 3.10 都行这两个版本对 pytorch 和 tensorflow 的支持都比较稳。新建环境并激活的命令如下如果你已经装好了 Anaconda直接在终端里执行conda create -n mushroom python3.9 -y conda activate mushroom pip install numpy pandas scikit-learn matplotlib pip install torch torchvision逻辑说明第一行创建名为mushroom的虚拟环境并指定 Python 3.9-y参数跳过确认提示第二行激活环境之后所有包都装在这一个环境里不会污染系统全局。第三行装的是数据处理和传统机器学习要用的库第四行装的是 PyTorch 全家桶迁移学习、训练 CNN 都依赖这两行。参数说明python3.9不是必须的但你电脑上如果还有别的项目在用其他 Python 版本虚拟环境能避免相互干扰。torch默认会装 CPU 版本对于蘑菇分类这种中小数据集完全够用跑一个 epoch 也就几秒到十几秒。如果你的机器有 NVIDIA 显卡再手动装 CUDA 版本的 torch训练速度能快好几倍但新手没必要一上来就折腾显卡驱动。2.3 项目目录结构源码、说明文档和 PPT 怎么组织压缩包文件名已经告诉你交付形态源码、项目说明、PPT。一个合格的工程结构应该是解压后看一眼 README 就能跑起来。我见过的常见做法是分成data/、src/、docs/三个目录数据放在外面而不是塞进代码里否则代码里写死的路径一换机器就报错。mushroom-project/ ├── data/ │ ├── images/ # 图像数据集按类别分子目录 │ │ ├── edible/ │ │ └── poisonous/ │ └── mushroom.csv # 表格数据集如果用 UCI 数据 ├── src/ │ ├── train.py # 训练脚本 │ ├── infer.py # 推理脚本 │ ├── data_utils.py # 数据加载与预处理 │ └── baseline.py # 表格数据随机森林基线 ├── docs/ │ └── README.md # 环境配置、运行步骤、结果说明 └── requirements.txt # 依赖清单这个结构最大的好处是每部分职责单一train.py只管训练和保存模型infer.py负责读入一张新图片并给出预测结果requirements.txt让换环境的人也能一键恢复依赖。图像数据按标签分子目录存放是 torchvision 的ImageFolder能识别的标准格式后面训练代码可以直接用省去自己写标签映射的麻烦。特别提醒一句如果你是要在自己的机器上复现这个项目解压 zip 之后先看docs/README.md核对数据路径和依赖版本再动手。上来就运行train.py报 FileNotFoundError八成是路径问题而不是代码问题。3. 把蘑菇分类真正跑起来从随机森林到迁移学习3.1 用随机森林快速建一个能用的 baseline不管最终方案是 CNN 还是别的先用表格数据跑一个随机森林基线好处是你对数据和任务的难度先有个体感。UCI 蘑菇数据集特征全是离散值随机森林天然擅长处理这种数据不需要做 one-hot 编码以外的复杂变换。下面这段代码是常见做法把 CSV 读进来、特征编码、划分训练测试集、训练随机森林并打印准确率import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelEncoder from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, classification_report # 读取 UCI 蘑菇数据集 df pd.read_csv(data/mushroom.csv) print(df.shape, df[class].value_counts()) # 对所有列做标签编码a-0, b-1 ... le_dict {} for col in df.columns: le LabelEncoder() df[col] le.fit_transform(df[col]) le_dict[col] le # 特征与标签分离 X df.drop(class, axis1) y df[class] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 训练随机森林 model RandomForestClassifier(n_estimators300, max_depthNone, min_samples_leaf1, random_state42) model.fit(X_train, y_train) # 评估 y_pred model.predict(X_test) print(acc:, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred))逻辑说明LabelEncoder把每一列的字母值映射成整数这是随机森林能处理的格式。train_test_split里stratifyy保证训练集和测试集的类别比例一致蘑菇数据集可食和有毒样本数量本来就不完全平衡不做分层抽样会放大偏差。RandomForestClassifier的核心参数n_estimators是树的数量300 棵在这个数据规模下已经足够max_depthNone表示不限制单棵树深度让树自由生长到纯叶节点。参数说明random_state42是固定随机种子跑出来的结果可复现答辩时老师问你为什么两次运行结果不一样你就能解释是种子问题。min_samples_leaf1是默认值如果发现训练集准确率 100% 而测试集只有 90% 出头说明过拟合把它调大到 5 或 10 试一下。这一步跑下来准确率普遍在 95% 以上UCI 这个数据集本身比较规整不要看到 95% 就觉得项目结束了重点是要上图像识别。3.2 图像分类核心用 ResNet18 迁移学习做最终模型表格数据的上限肉眼可见真正出效果的是图像分类。常见方案是用预训练的 ResNet18 做迁移学习把模型在 ImageNet 上学到的通用视觉特征迁移到蘑菇识别上。这样做的好处是收敛快、数据需求量小几百张图片就能训出能看的模型。数据加载和训练的核心代码逻辑如下我挑了最关键的一段import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms, models from torch.utils.data import DataLoader # 1. 数据增强与归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 2. 加载 ImageFolder 格式数据 train_dataset datasets.ImageFolder(data/images/train, transformtrain_transform) val_dataset datasets.ImageFolder(data/images/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers2) # 3. 加载预训练 ResNet18替换最后一层为二分类 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_features model.fc.in_features model.fc nn.Linear(num_features, 2) # 可食 / 有毒 # 4. 冻结除最后几层外的所有参数 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True # 5. 训练配置 device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.fc.parameters(), lr1e-3) # 6. 训练循环简化版只跑 10 个 epoch for epoch in range(10): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs.data, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc 100.0 * correct / total print(fEpoch {epoch1}: loss{running_loss/len(train_dataset):.4f}, val_acc{val_acc:.2f}%)逻辑说明第 1 步的RandomHorizontalFlip和RandomRotation是数据增强随机翻转和旋转能增加样本多样性是防止图像模型过拟合最有效的手段之一。Normalize的均值和标准差用的是 ImageNet 统计量因为预训练模型是在这个分布上学的新输入也要做同样的归一化。第 3 步是关键ResNet18从卷积层提取的是通用特征最后一层全连接改成输出 2 类fc.in_features读取原全连接层的输入维度保证前面层输出和新层能对上。第 4 步冻结前面所有层只训练最后那个新加的全连接层这样训练速度快、也不容易在小数据集上过拟合。参数说明batch_size32在一般笔记本上跑 CPU 也不会爆内存显存 4G 以上的显卡也带得动。lr1e-3对只训练最后几层的场景是安全值如果数据增强很强可以试着提到3e-3。num_workers2是数据加载线程数Windows 上如果报错可以改成 0因为 Windows 下多进程数据加载偶尔有兼容问题。3.3 训练时的三个必调参数和它们的取舍迁移学习最核心的三个参数是学习率lr、批大小batch_size、训练轮数epochs。lr决定模型往损失下降方向迈的步子大小太大容易震荡甚至发散太小学得慢还容易卡在局部最优batch_size决定每次更新参数用多少张图32 是普适选择显存够就上 64但 batch 太大模型的泛化能力通常反而稍差epochs不是越大越好模型在验证集上的准确率通常会先升后降那个拐点就是你要停的位置。我一般这样设初始值lr1e-3batch_size32epochs15。每 5 个 epoch 看一下验证集准确率如果第 5 轮就已经 95% 以上说明任务偏简单直接提前终止如果始终在 80% 以下优先调数据增强而不是盲目调大学习率。注意别把训练集准确率当信仰训练集 100% 而验证集只有 90%那是过拟合的信号不是模型变强了。典型参数范围迁移学习冻结骨干网络仅训练分类头 学习率 lr: 1e-4 ~ 3e-3 批大小 batch: 32 ~ 64 训练轮数 epochs: 10 ~ 20 优化器: Adam 或 SGD(momentum0.9) 损失函数: CrossEntropyLossAdam 的优点是自适应学习率几乎不需要手动调整就能收敛SGD 收敛更慢但最终精度通常略高。蘑菇分类这个任务对极致精度不敏感直接用 Adam 省心。CrossEntropyLoss 在 PyTorch 里自带 softmax所以模型最后一层不需要再额外接 softmax直接输出 logits 就行。4. 评估蘑菇分类效果别只盯着准确率召回率更要命4.1 为什么在蘑菇分类里召回率比准确率更值钱准确率是最直观的指标但它有个迷惑性如果测试集里 80% 是可食蘑菇模型全部预测成可食准确率也有 80%看起来不错实际却把所有的毒蘑菇都放过了。在蘑菇分类这个场景里把毒蘑菇误判成可食的代价是致命的而把可食蘑菇误判成有毒只是损失一顿饭。所以业务上真正要追求的是“有毒样本被正确识别出来的比例”也就是召回率。这里要区分两个容易混的指标。精确率问的是“模型说是可食的里面真正可食的占多少”召回率问的是“真实可食的蘑菇里模型找出来了多少”。对于毒蘑菇这一类召回率意味着漏网率——有毒的蘑菇必须尽量全部拦住宁可错杀也不可放过。落到项目答辩里能把这个业务逻辑讲清楚比你多跑两个模型更能体现你的思考深度。4.2 用混淆矩阵和分类报告定位模型的短板光看一个 acc 数字看不出问题在哪要把混淆矩阵打出来。下面这段代码在验证集上评估并输出分类报告和混淆矩阵from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 分类报告precision/recall/f1 每一类都有 print(classification_report(all_labels, all_preds, target_names[edible, poisonous])) # 混淆矩阵可视化 cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[edible, poisonous], yticklabels[edible, poisonous]) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(doc/confusion_matrix.png, dpi150)逻辑说明torch.max(outputs, 1)取每个样本在两个类别上得分更高的那个作为预测结果。classification_report按类别输出精确率、召回率和 F1重点看poisonous这一行的recall是不是接近 100%。混淆矩阵的四格分别对应真正可食且被识别、可食被误判有毒、有毒被误判可食、有毒且被正确识别。右下角如果有非零数字就是漏网的毒蘑菇这是最需要警惕的部分。参数说明dpi150保存图片用于 PPT 展示足够清晰。cmapBlues只是配色不影响数据。如果发现poisonous的召回率不高下一步不是瞎调模型而是先去查训练数据里毒蘑菇的图片是不是太少、太单一数据层面补强往往比调参更有效。4.3 安全阈值预测概率低于多少直接拒绝我在做这类落地项目时的习惯是模型输出的不是硬分类标签而是概率值。PyTorch 里对输出做torch.softmax(outputs, dim1)就能拿到两个类别的概率如果poisonous的概率超过 0.5 判有毒低于 0.5 判可食这是默认的二分界线。但在蘑菇分类这个任务里我会把可食的判定阈值往上抬。具体做法是edible_prob 0.8才判可食0.5 ~ 0.8之间一律判为“不确定”poisonous_prob 0.5判有毒。这样做的价值是留出了安全缓冲带模型自己都拿不准的样本对应的建议不是“吃”而是“别吃”。答辩 PPT 里放一张精度-召回率曲线展示阈值从 0.5 调到 0.8 的过程中毒蘑菇召回率如何提升这是一个很好的进阶亮点。调阈值比调模型快得多改一行代码就能重新评估这也是项目说明里值得单独写一节的内容。5. 蘑菇分类实战避坑数据泄露、标签错位与过拟合5.1 数据泄露归一化参数在拆分前就看了测试集现象训练过程一切正常验证集准确率也很高但拿到新蘑菇图片一测就翻车准确率掉到 60% 以下。原因预处理里的归一化均值、标准差是在全套数据上算出来的也就是说测试集的信息在训练之前就已经参与进来了。这是最常见的“数据泄露”虽然不像真实世界中“用了未来数据”那么严重但它会让评估结果虚高导致你对模型真实水平产生错误信心。图像分类里Normalize用的通常是 ImageNet 统计量不涉及这个问题但如果你做特征标准化比如StandardScaler直接 fit 在全部数据上就会踩这个坑。解决一切涉及统计量的预处理都必须在train_test_split之后进行只对训练集fit再对测试集transform。表格数据里就是scaler.fit(X_train)再scaler.transform(X_test)绝对不要对整个 X 做 fit。图像数据直接套用 ImageNet 的固定均值方差就行没有泄露风险。5.2 标签错位shuffle 之后列表对不上号现象训练 loss 正常下降但验证集准确率始终在 50% 左右徘徊跟抛硬币差不多。原因很多新手在组织自己的图片数据集时用os.listdir扫描文件夹然后手动拼 label 列表又在某个环节对图片列表做了shuffle标签列表没有跟着一起动数据从 epoch 一开始就是错位状态模型学到的是图片和瞎猜标签之间的“关系”。这种问题不会报错因为图片数量是对的、形状是对的、标签值域也是对的只是对应关系乱了。解决别自己手搓图片-标签对应逻辑直接用torchvision.datasets.ImageFolder。它按子文件夹名自动生成标签DataLoader的shuffleTrue会在每个 epoch 重新打乱索引但每张图片的标签绑定在 Dataset 内部不会错位。如果必须自定义 Dataset把图片路径和标签写进同一个类里用索引取数据不要维护两个独立列表。5.3 过拟合训练集 100%验证集掉到 85%现象训练集的 loss 一路降到接近 0准确率冲上 99%验证集却停滞在 85% 上下甚至随 epoch 增加开始下降。原因模型把训练集里每个样本的细节包括噪声都背下来了却没有学到真正可泛化的规律。这在小数据集上的表现尤其明显——蘑菇图片总共就几百张模型容量又够大背下来远比学会容易。我在新手项目里见过最多的问题就是这个他们还特别兴奋地说“我训练集到 100% 了”其实危险信号。解决按顺序做三件事。第一加数据增强把RandomHorizontalFlip、RandomRotation、ColorJitter一起用上让模型每次看到的都是不完全一样的图相当于扩大了数据集。第二把epochs降下来或者加一个早停逻辑验证集准确率连续 5 个 epoch 不升就保存当前模型并终止训练。第三迁移学习里可以只解冻并训练最后两层而不是整个模型前面层保持预训练参数不动可训练的参数量少了过拟合空间也就小了。5.4 类别不平衡毒蘑菇图片数少一大截怎么办现象训练集里可食蘑菇图片有 800 张毒蘑菇只有 200 张模型学到最后对所有图片都倾向输出“可食”毒蘑菇召回率惨不忍睹。原因交叉熵损失在类别数量差异大时会偏向多数类因为把多数类预测对的样本数量多累积的损失占比大模型发现“全猜可食”也能把平均损失压得很低。解决最简单的是在损失函数里加类别权重torch.nn.CrossEntropyLoss(weighttorch.tensor([1.0, 4.0]))给样本少的类别更高的惩罚权重权重直接按类别数量的反比设置。数据层面可以多做几种增强策略专门对少数类图片做更强的随机变换相当于过采样。这两个手段可以同时用一般能把有毒类的召回率拉回到 95% 以上。不要一开始就上过采样或者 SMOTE先试权重改动最小、效果最容易评估。5.5 CPU 推理太慢本地演示卡成幻灯片现象训练时用 CPU 还能忍但到了答辩现场加载一张图片预测一次要快十秒连点几次现场气氛尴尬。原因ResNet18 本身不算重但在 CPU 上跑一次推理也就是毫秒到秒级如果每次都重新加载模型文件、重新做Resize和Normalize大量时间浪费在初始化上。还有一个隐蔽点模型默认是训练模式model.train()每个 batch 都会算 dropout 和 batch norm 的统计量推理时不切到model.eval()速度会明显变慢。解决推理脚本里启动时只加载一次模型用model.eval()切到推理模式所有预处理在脚本开头配置好不要每次走完整变换流程。如果还是慢用torch.jit.script(model)把模型导出为 TorchScript 格式推理能再快 20% 到 30%而且部署时不需要原始网络结构定义。这一条属于项目说明里容易忽略的细节但现场演示效果全靠它。6. 把蘑菇分类项目变成一个能现场演示的交付物模型训练完项目还剩最后一步让外行人也能用起来。原始训练脚本的输出是准确率和损失数字答辩现场老师想看的是“我随便指一张照片模型告诉我这是可食还是有毒”。所以最后一步我建议加一个推理脚本做成命令行或者最简单的图形界面。一个能直接用的推理脚本核心逻辑如下import torch from torchvision import transforms, models from PIL import Image # 复用训练时的预处理 transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 加载训练好的模型 device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsNone) model.fc torch.nn.Linear(model.fc.in_features, 2) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.to(device).eval() classes [edible, poisonous] def predict(image_path): img Image.open(image_path).convert(RGB) x transform(img).unsqueeze(0).to(device) with torch.no_grad(): probs torch.softmax(model(x), dim1) p_edible probs[0, 0].item() p_poison probs[0, 1].item() if p_edible 0.8: return f可食置信度 {p_edible:.2f} elif p_poison 0.5: return f有毒置信度 {p_poison:.2f} else: return f不确定可食 {p_edible:.2f} / 有毒 {p_poison:.2f} print(predict(test_image.jpg))逻辑说明map_locationcpu保证训练时用 GPU 保存的模型也能在没显卡的机器上加载。unsqueeze(0)给单张图片加一个 batch 维度因为模型期望的输入形状是(batch_size, 3, 224, 224)。torch.softmax把输出转成两个类别的概率和正好为 1再按我前面说的安全阈值逻辑给出三段式结论可食、有毒、不确定。model.eval()是推理异步关键前面讲过了。参数说明概率阈值0.8可以按评估结果调整如果你验证集上可食类精确率足够高可以试着放到0.85甚至0.9让“确定”的结论更可信。best_model.pth是训练脚本保存的权重文件训练时建议每个 epoch 都记录验证集准确率达到历史最高就覆盖保存一次这样可以拿到全流程里最好的那版模型而不是最后一个 epoch 可能已经过拟合的模型。如果想做到 PPT 里展示的程度给推理脚本套一个 tkinter 或 Gradio 界面选一张图点一下按钮就出结果。Gradio 更省事两行代码就能起一个网页版 demo效果比命令行脚本适合现场演示。这个方向值不值得投入我的判断是值得尤其是课程设计和毕设场景数据可以跑通、模型可以聊原理、PPT 有真实结果可以展示整套代码也就几百行是机器学习入门投资回报率很高的项目。我自己每次做这类小项目都会坚持把推理脚本和 README 写完整因为一周后再看代码能救你的只有注释和文档。希望帮到你。本文还有配套的精品资源点击获取
返回列表