ARTICLE DETAIL

资讯详情

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

机器学习代码实战:从环境配置到回归、SVM聚类跑通

机器学习代码实战:从环境配置到回归、SVM聚类跑通 简介一套覆盖回归、支持向量机SVM、聚类等经典算法的机器学习代码集合内容源自邹博老师的课程讲解适合机器学习初学者以及需要巩固算法基础、准备竞赛或工程落地的开发者作为学习参考。压缩包共391个文件内部以Python、R、Java等语言源码为主另有50个Markdown笔记文档、C工程文件以及各类配置文件整体大小约105.63MB包内还整合了xgboost-master源码库可对照学习梯度提升决策树GBDT的实现与参数调优思路。目前这套资料已有448人学习。学习时可重点关注支持向量机的核函数选择与间隔最大化推导以及XGBoost的树模型构造、正则化和分布式训练逻辑结合代码运行还能掌握特征缩放、缺失值处理、交叉验证与网格搜索等完整建模流程并借助准确率、召回率、AUC等指标判断模型效果。资源内既有从零手写的基础模型也有面向集成学习的工程化封装适合不同阶段学习者按需查阅让算法理论真正转化为可复用的工程能力。1. 从一份打包代码到能跑的机器学习这中间隔了多少个坑你手上这份《邹博机器学习全套代码(含回归、svm、聚类等常规算法).rar》大概率是很多初学者硬盘里的“老朋友”了。下载它很容易解压它也很容易但真正把它变成自己能跑通、能改、能讲清楚每个参数含义的实战能力往往要跨过好几道坎。这份资料的核心价值不在于代码本身写得多完美而在于它用最小依赖实现了机器学习里最经典的那几条主线——线性回归、逻辑回归、SVM、聚类——刚好是你从理论走向实践的第一组阶梯。但有一句实话得说在前面这类历史稍微有点年头的代码包直接跑起来的成功率并不高。Python 版本、NumPy 和 SciPy 的兼容性、Matplotlib 的中文显示、sklearn 接口的变动任何一个环节不对都会让学习热情当场熄火。这篇文章的目的就是帮你把这套代码从头到尾拆开揉碎搞清楚每一段是干什么的、参数为什么这么设、数据从哪来以及——最关键的是——当它跑不起来的时候你到底该从哪下手。我不会假装见过这份代码的原始内容但我可以告诉你所有这类“教学全代码包”的常见组织方式和经典踩坑点你照着下面的思路去整理、修复和运行大概率能在两小时以内把所有算法都跑起来。2. 先立住概念回归、SVM、聚类到底在解决什么问题代码分别长什么样2.1 回归从“拟合一条线”到理解损失函数和梯度下降回归是整个机器学习里最直观、也最适合做入门剖析的算法。它的核心任务是用一条曲线或一个超平面去逼近一组数据点让预测值和真实值之间的误差尽可能小。在这类教学代码里最常见的是两种线性回归和逻辑回归。线性回归解决的是连续值预测问题比如根据房屋面积预测房价。代码的核心其实只有三块假设函数、损失函数、参数更新。教学版本一般会手写梯度下降而不是直接调 sklearn目的就是让你看见“学习率太大导致 loss 爆炸”“learning rate 太小收敛慢得像乌龟”这些现象。import numpy as np import matplotlib.pyplot as plt # 构造一份带噪声的线性数据 np.random.seed(42) X np.linspace(0, 10, 100) true_w, true_b 2.0, 1.0 y true_w * X true_b np.random.randn(100) * 2.0 # 初始化参数 w, b 0.0, 0.0 lr 0.01 # 学习率后面会演示调参 epochs 1000 for epoch in range(epochs): y_pred w * X b loss np.mean((y_pred - y) ** 2) # 均方误差 # 手动求梯度 grad_w 2 * np.mean((y_pred - y) * X) grad_b 2 * np.mean(y_pred - y) w - lr * grad_w b - lr * grad_b if epoch % 200 0: print(fepoch {epoch}, loss {loss:.4f}, w {w:.4f}, b {b:.4f}) print(f最终: w{w:.4f}, b{b:.4f}真实值 w2.0, b1.0)这段代码的逻辑是每个 epoch 先算出预测值再用均方误差计算出损失然后对 w 和 b 分别求梯度让参数沿着负梯度方向更新。你注意看grad_w那行它多乘了一个X这是链式法则的结果——误差要通过预测值对 w 求导而预测值对 w 的导数是 X 本身。很多人抄代码的时候不理解这里为什么不一样导致自己写的时候梯度算错loss 越跑越高。参数上最值得调整的是lr。你可以试着把它改成 0.1 或 0.5大概率会看到 loss 变成 inf——那个瞬间你就理解了“梯度爆炸”是什么感觉。这份全套代码里的回归部分大概率就是围绕类似的逻辑展开的重点不是代码有多高级而是它演示了从数学公式到数值计算的完整映射。2.2 SVM从“找一条分界线”到理解支持向量和核函数SVM支持向量机解决的是分类问题它的核心思想比回归更几何化在两类数据之间找一条“间隔最大”的分界线。教学代码里通常分两步走——先用线性核跑一个简单的二分类再引入高斯核RBF来处理线性不可分的数据。教学版本的 SVM 实现一般不会让你从头写二次规划求解器而是直接调用 sklearn 的SVC接口。但理解原理仍然必要因为后面调参数的时候你至少得知道C和gamma各自影响的是什么。from sklearn import datasets from sklearn.model_selection import train_test_split from sklearn.svm import SVC from sklearn.metrics import accuracy_score # 用鸢尾花数据集只取前两类变成二分类问题 iris datasets.load_iris() X iris.data[:100] # 取前100条样本 y iris.target[:100] # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) # 线性核SVM svm_linear SVC(kernellinear, C1.0) svm_linear.fit(X_train, y_train) y_pred svm_linear.predict(X_test) print(f线性核准确率: {accuracy_score(y_test, y_pred):.4f}) # RBF核SVM svm_rbf SVC(kernelrbf, C1.0, gammascale) svm_rbf.fit(X_train, y_train) y_pred_rbf svm_rbf.predict(X_test) print(fRBF核准确率: {accuracy_score(y_test, y_pred_rbf):.4f})在这段代码里kernel参数决定你要把数据映射到什么空间里找分界C是误分类惩罚系数越大越不愿意容忍错误分类但也越容易过拟合gamma只在 RBF 核里有效它控制单个样本的影响范围——gamma 越大决策边界越复杂。这套代码的使用逻辑是先用线性核看基线分数如果不理想再换 RBF 核并给gamma调几个量级试试。2.3 聚类从“无监督的直觉”到理解 KMeans 的迭代本质聚类和回归、SVM 最大的区别在于它没有标签属于无监督学习。你的目标不是预测而是把数据按照某种距离度量分成几堆。教学代码里最常出现的是 KMeans因为它思想简单——先随机选 K 个中心点然后交替执行两步把每个点分给最近的中心再把中心移到这一堆点的均值位置。from sklearn.cluster import KMeans import numpy as np import matplotlib.pyplot as plt # 生成三簇模拟数据 np.random.seed(10) cluster1 np.random.randn(50, 2) np.array([5, 5]) cluster2 np.random.randn(50, 2) np.array([-5, 5]) cluster3 np.random.randn(50, 2) np.array([0, -5]) X np.vstack([cluster1, cluster2, cluster3]) # 训练KMeans kmeans KMeans(n_clusters3, random_state42, n_init10) kmeans.fit(X) labels kmeans.labels_ # 可视化 plt.scatter(X[:, 0], X[:, 1], clabels, cmapviridis) plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], markerx, s200, colorred) plt.title(KMeans 聚类结果) plt.show()这里有几个值得注意的点。第一n_init10表示算法会从 10 组不同的随机中心开始跑最后取结果最好的一次——KMeans 对初始中心敏感不设这个参数就可能在局部最优里出不来。第二random_state必须固定否则你每次跑出来的聚类结果可能不一样这在写实验报告的时候会很头疼。第三数据本身要先做标准化否则两个特征的量纲不一致时距离会被数值大的那个特征主导。这份全套代码里的聚类部分大概率还会包含层次聚类或 DBSCAN 的演示但 KMeans 是地基你先把它吃透后面看别的聚类算法会轻松很多。3. 从 .rar 到能跑的工程整理目录、修复依赖、跑通第一个脚本3.1 解压后的第一件事不要双击运行先建工程目录很多人拿到 .rar 后做的第一件事是解压然后找个 .py 文件双击或者拖进 IDE 里按运行然后一脸懵地看着报错。正确的做法是先把这个压缩包当成一个“待整理的项目”来对待——建一个干净的目录结构让数据和代码分离这能省掉后面无数个“文件找不到”的报错。./ml_learning_project/ ├── code/ # 存放所有 .py 或 .ipynb 文件 ├── data/ # 存放数据集可能原本在压缩包内也可能需要下载 ├── output/ # 存放运行结果、图表、模型文件 ├── requirements.txt # 项目依赖清单后面会讲怎么写 └── README.md # 自己记录每个脚本的用途和坑我的做法是先解压到一个临时目录然后把所有代码文件复制到code/下把所有.csv、.txt、.mat这类数据文件复制到data/下再全局搜索代码里的.read_csv(或loadtxt这类调用看它到底引用了哪些文件。如果压缩包里本来就附带数据这一步能直接完成如果没有你就得根据代码里的路径提示去补数据。这一步的意义在于机器学习代码几乎都会读写文件路径是你和数据之间的契约。很多原版代码用的是相对路径比如../data/dataset.csv这意味着它假设你当前的工作目录在code/这个层级。如果你直接在压缩包根目录运行路径就断了。所以我强烈建议运行任何脚本之前先手动cd到代码所在目录或者在 IDE 里把工作目录设置好。3.2 把环境一次配到位Python 版本、依赖包和虚拟环境这类教学代码的“年龄”通常不小而 Python 生态这几年变化很大。最常见的翻车是import module报错——原因可能是包没装也可能是包名变了或者某个函数在新版本里被删了。与其逐个报错逐个查不如先一步到位把环境配好。先创建虚拟环境把项目依赖隔离出来——这一步很重要因为如果你后面要跑深度学习或者别的项目依赖之间的版本冲突会让你怀疑人生。# 创建虚拟环境Python 3.9 左右兼容性较好 python3.9 -m venv ml_env # 激活虚拟环境 source ml_env/bin/activate # Linux / macOS # ml_env\Scripts\activate # Windows # 安装核心依赖先装大的再装小的 pip install numpy1.23.5 scipy1.10.1 matplotlib3.7.3 pip install scikit-learn1.2.2 pandas1.5.3 jupyter notebook这里每个包的版本号我都特意选在了“新老兼容”的过渡区。numpy不要选太新的因为某些老代码用到的np.float在 NumPy 1.24 之后的版本里彻底被移除了换成np.float64才能用——如果你不锁定版本而直接装最新版跑老代码大概率会踩到这个坑。scikit-learn同理很多老教程里的train_test_split用法没变但部分算法的参数名有调整比如svm.SVC里的gammaauto在旧版本是默认行为新版里变成了scale。如果代码包里有requirements.txt这个文件那就简单了直接pip install -r requirements.txt。但如果这份资料有点年头requirements.txt里的依赖可能本身就过时了——我最常遇到的是人家锁了numpy1.11.0这个版本在很多新平台上压根装不上。遇到这种情况我的处理方式是先尝试直接装装不上就在requirements.txt里把固定版本号去掉改成numpy1.20,2.0这种宽松范围至少能保证不被版本锁死。提示如果你在 macOS 上遇到scipy编译报错多半是 Python 版本太新导致的。换成 Python 3.9 或 3.10 能省掉很多不必要的麻烦。3.3 让第一个脚本跑通从最无依赖的代码开始验证环境环境配好以后不建议直接去跑完整的 SVM 或聚类脚本而是先跑一个只依赖 NumPy 和 Matplotlib 的最小脚本验证整条链路是通的——这就像装修之前先通水通电而不是把所有家具搬进来才发现电路有问题。import numpy as np import matplotlib.pyplot as plt # 测试NumPy是否正常 arr np.array([1, 2, 3, 4]) print(NumPy OK:, arr.mean()) # 测试Matplotlib是否能显示图片注意先确认plt.show()会不会卡住 plt.plot([1, 2, 3], [4, 5, 6]) plt.title(Test Plot) plt.savefig(../output/test.png) # 如果交互显示有问题就先保存 print(Matplotlib OK)这段代码能验证两件事NumPy 是否能正常导入并执行数学运算Matplotlib 在后端backend正常的情况下能否输出图片。很多新手在这一步就卡住了——程序跑完没报错但图片窗口不弹出来或者弹出来就卡死。如果遇到这个问题大概率是 Matplotlib 的后端配置问题。你可以强制指定非交互后端或者直接用savefig保存图片这虽然不是最优雅的解决方案但对跑通流程来说已经完全够用了。当这个最小脚本跑通之后你才真正拿到了“环境没问题”的保证。接下来任何一个更复杂的脚本报错时你就能安心地认为是代码本身的兼容性或者数据路径有问题而不是环境配置的问题——这能帮你排除掉 50% 的无效排查。4. 避坑手册数据路径、版本兼容和“玄学”报错的集中排查4.1 坑一报错FileNotFoundError——数据路径永远是最常见的翻车点现象运行任何一个需要加载数据集的脚本直接抛FileNotFoundError: [Errno 2] No such file or directory: data/iris.csv。原因99% 的情况是你的“当前工作目录”和代码里写的相对路径对应不上。代码写的是data/iris.csv但它实际是相对于某个目录来定位的——而这个目录可能不是你当前所在的目录。解决在脚本开头加一小段“自动切换目录”的代码让它把工作目录改为脚本自身所在的目录然后再读文件。这是所有教学代码包通用的修复手段。import os # 获取当前文件所在目录并把工作目录切换过去 current_dir os.path.dirname(os.path.abspath(__file__)) os.chdir(current_dir) # 如果数据在上一级目录的 data 文件夹里可以这样拼路径 data_path os.path.join(os.path.dirname(current_dir), data, iris.csv) import pandas as pd df pd.read_csv(data_path)这段代码的原理是__file__是 Python 内置变量表示当前脚本的完整路径用abspath把它转成绝对路径再取它的目录名然后切换到那个目录。这样你无论在哪个终端目录下运行脚本它都自动“回家”了不会再迷路。很多老项目是直接解压到桌面然后拖进 IDE 运行的处理完这一步路径问题基本清零。4.2 坑二numpy.float不存在了——老代码和现代 NumPy 的兼容性危机现象运行 SVM 或聚类相关的教学代码时报错AttributeError: module numpy has no attribute float。原因NumPy 1.24 版本移除了np.float、np.int、np.bool这些别名要求使用np.float64、np.int64、np.bool_。老教学代码尤其是 2018 年以前写的大量使用np.float作为默认浮点类型于是集体失效。解决最省事的办法是降级——把 NumPy 固定到 1.23.5 或更低版本这能直接避开这个坑。如果你不太想降级也可以全局搜索代碼里的np.float手动替换为np.float64——但注意要小心身体里有没有同时替换掉np.float32之类的内容因为np.float是子串匹配手一抖就全改错了。我的建议是你的虚拟环境里直接装numpy1.23.5这是性价比最高的方案。scikit-learn 老版本的某些接口在这个版本下都还能正常工作。4.3 坑三plt.show()不弹窗或闪退——Matplotlib 后端问题现象聚类代码跑完了plt.show()不报错但也没有任何窗口弹出来或者弹出来闪一下就消失。原因最常见的是你的环境是远程服务器或容器里没有图形界面或者 Matplotlib 默认的后端如 QtAgg与当前系统环境不兼容。解决最直接的方案是绕开交互式显示直接保存成图片文件。也可以手动切换一个更轻量级的后端。import matplotlib matplotlib.use(Agg) # 使用非交互式后端不依赖GUI环境 import matplotlib.pyplot as plt # ... 聚类代码和绘图代码 ... plt.savefig(../output/kmeans_result.png, dpi150, bbox_inchestight) print(图片已保存到 output/kmeans_result.png)我把matplotlib.use(Agg)放在import matplotlib.pyplot之前这一步是关键——后端必须在 pyplot 导入之前就设好否则设置无效。Agg后端不依赖任何窗口系统把图形直接渲染成文件。对跑教学代码来说保存成图片的体验甚至更好因为你可以随时翻看历史实验结果不用在桌面上堆满窗口。4.4 坑四ConvergenceWarning——算法不收敛问题可能出在数据和参数上现象跑逻辑回归或 SVM 时控制台输出警告ConvergenceWarning: Maximum iterations (100) reached and the optimization has not converged yet.而且模型分数明显偏低。原因数据没有标准化或者迭代次数上限太小或者学习率/容忍度设置不合理。特征数值范围差距巨大时比如一列是 0~1另一列是 10000~20000SVM 的优化过程会非常挣扎。解决先用StandardScaler标准化特征再调大max_iter。这不是在“作弊”而是机器学习实践里的标准流程。教学代码里为了演示“最质朴”的算法流程往往省略了预处理但你在实际跑的时候必须把它加上。from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.pipeline import make_pipeline # 用pipeline把标准化和分类器串起来避免数据泄漏 model make_pipeline( StandardScaler(), SVC(kernelrbf, C1.0, gammascale, max_iter2000) ) model.fit(X_train, y_train)这里的make_pipeline是一个很实用的技巧它保证在每一次交叉验证里标准化都在训练集上 fit、在测试集上 transform而不是对整个数据集先归一化再切分——后者会造成信息泄漏让验证结果虚高。很多人不懂这一点直接先scaler.fit(X_all)再划分数据结果模型在测试集上的表现总是比真实情况好一截然后到线上就被打脸。4.5 坑五中文标签和注释全变方块——编码问题现象Matplotlib 绘制的图里所有中文标签显示成一堆方块或乱码或者运行 Python 脚本时直接报SyntaxError: Non-ASCII character ...。原因Matplotlib 默认字体DejaVu Sans不含中文字形老代码在 Python 2 时代是默认 ASCII 编码代码里有中文注释时如果不声明编码就会直接红屏。解决在绘图之前设置支持中文的字体。Windows 下通常用SimHei黑体macOS 下用PingFang SCLinux 下如果有wqy-microhei也可以用。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # Windows黑体 plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块的问题 plt.plot([1, 2, 3], [-1, 2, -3]) plt.title(中文标题测试) plt.savefig(../output/chinese_test.png, dpi150)注意第二行axes.unicode_minus——如果你不设置它图表里的负号会显示成一个奇怪的方块和中文乱码长得一模一样很容易让你误以为是字体问题而疯狂换字体折腾半小时。这两个参数一起设基本上教学代码里出现的图表显示问题可以全部解决。另外如果你的 Python 脚本在运行时报编码错误在文件第一行加上这句注释即可# -*- coding: utf-8 -*-这在 Python 3 里已经不是必须的了但很多老代码文件仍然保留着留着也不会有副作用。5. 让代码开始“为你服务”参数调优、替换数据集和手写评估报告5.1 不满足于“跑通”给代码加上交叉验证看真实分数当你把全套代码都跑通之后下一步是让它从“能出结果”进化为“结果可信”。教学代码的通常做法是简单地把数据切一次训练集和测试集——但如果运气不好某一刀切下去测试集特别好或特别差你的结论就会被带偏。交叉验证是解决这个问题的标准手段。from sklearn.model_selection import cross_val_score from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline model make_pipeline( StandardScaler(), SVC(kernelrbf, C1.0, gammascale) ) # 5折交叉验证返回5个分数 scores cross_val_score(model, X, y, cv5, scoringaccuracy) print(f每折分数: {scores}) print(f平均准确率: {scores.mean():.4f} ± {scores.std():.4f})输出里的± std比单一测试集准确率信息量大得多——它告诉你模型的稳定性如果均值高但波动极大说明模型对数据划分敏感可能存在过拟合或数据分布不均匀如果均值普通但波动极小说明模型稳定但上限有限接下来该做特征工程而不是继续调参。很多教学材料不讲这一点导致初学的人整天盯着单次测试集的准确率来回调参调了个寂寞。5.2 如何把“自带数据”换成你自己的数据这套代码包里的数据大概率是鸢尾花、波士顿房价或者随机生成的点——这些数据干净得像洗过一样真实世界里的数据没这么友好。当你试图用代码处理自己的数据时最大的需求量不是“改算法”而是“接数据”。常见的操作是把 .xlsx 文件读进来、处理缺失值、统一特征格式。import pandas as pd # 读取Excel文件需要先安装 openpyxl df pd.read_excel(your_data.xlsx, engineopenpyxl) # 查看基本信息 print(df.shape) print(df.dtypes) print(df.isnull().sum()) # 检查缺失值 # 最简单的缺失值处理数值列用中位数填充类别列用众数填充 for col in df.columns: if df[col].dtype object: df[col] df[col].fillna(df[col].mode()[0]) else: df[col] df[col].fillna(df[col].median()) # 分离特征和标签假设最后一列是label X_new df.iloc[:, :-1].values y_new df.iloc[:, -1].values这一段看起来平平无奇但你在实际替换数据时大概率会被两个问题绊住一是特征里混入了字符串类型直接用 sklearn 会报could not convert string to float二是缺失值占比过大全用中位数填充会让数据特征扭曲。遇到这种问题我的习惯是先print(df.describe())看数值分布把明显有明显异常的数据列直接去掉再做填充——这一步叫“先看图再动手”虽然在代码里没有体现但它是你从一个“能跑代码的人”变成“会做数据的人”的分水岭。5.3 为你的实验建立“后悔药”模型保存与结果记录经常会遇到这样的情况你辛苦调好了一组参数跑了十几个小时结果换了台机器或者重启了一下所有实验结果全没了。所以把模型和结果持久化保存下来是必须养成的习惯。import joblib import json import datetime # 训练模型 model.fit(X_train, y_train) # 保存模型到文件 joblib.dump(model, ../output/model_{}.pkl.format( datetime.datetime.now().strftime(%Y%m%d_%H%M%S) )) # 保存实验参数和评估结果 record { model: SVM_RBF, C: 10.0, gamma: scale, accuracy: float(accuracy_score(y_test, model.predict(X_test))), timestamp: datetime.datetime.now().isoformat() } with open(../output/experiment_record.json, a) as f: f.write(json.dumps(record) \n)joblib.dump是 sklearn 生态里推荐的模型持久化方式它比 pickle 更快而且对 numpy 数组的类型保留更可靠。往后每次实验的结果都追加到同一个 JSONL 文件里你就有了一个“调参历史账簿”——哪组参数分数高、哪组是玄学跑出来的一目了然。这相当于给你的实验过程加了一个后悔药随时可以回退参考。6. 最后一步把代码当作教材而不是答案——你的复现实验清单与学习习惯到这一步你大概已经把这套代码全部跑通、修好、换成自己的数据并输出了可信的评估结果。但到这里其实才到了一半——另一半是你要把每一段代码当作一张考卷合上代码重新考自己一遍。具体做法是按算法把代码内容遮住只保留数据加载、模型定义和评估三行的注释然后试着凭记忆把逻辑补全。补不出来就回头看原代码补出来再对照看哪里不一样——这个“手写复现”的过程比你把代码跑十遍都更有用。我自己的习惯是给这份资料做一个“复现实验清单”每完成一项就打勾例如实验项目数据来源核心超参数评估指标状态线性回归 手动梯度下降自带数据lr0.01, epoch1000MSE已完成逻辑回归二分类自带数据C1.0Accuracy已完成SVM 线性核 vs RBF 核鸢尾花前两类C / gammaAccuracy已完成KMeans 聚类三簇模拟数据n_clusters3轮廓系数待做层次聚类对比同一数据linkageward树状图待做你可以直接把他当成一个验证自己的过程比如“我能不能把一个新算法如随机森林回归按照同样流程复用这套代码里的数据加载和评估模板”如果答案是“能”说明你已经把这套代码真正转化成自己的基本功了而不仅仅是“能跑通别人的代码”。最后留一句个人的经验调试机器学习代码和学习机器学习算法本质上是一个反复“卡住、看报错、查文档、验证假设”的循环。每份带年头的教学代码包里都埋着好几个过时的 API 调用和踩坑点你每修好一个就比昨天更懂一点 Python 和机器学习生态的真实面貌——修复本身就是最好的学习过程。希望你今天跑的第一行代码能在明天变成你自信建模的起点。希望帮到你。本文还有配套的精品资源点击获取
返回列表