ARTICLE DETAIL

资讯详情

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

Python-100-Days 机器学习实战:k 最近邻(kNN)算法从原理到 scikit-learn 实现

Python-100-Days 机器学习实战:k 最近邻(kNN)算法从原理到 scikit-learn 实现 文档教程【免费下载链接】Python-100-DaysPython - 100天从新手到大师项目地址https://gitcode.com/GitHub_Trending/py/Python-100-Days点击查看免费下载本文是《Python - 100天从新手到大师》Day 82 的核心内容对应仓库文档 82.k最近邻算法.md。本篇指南以经典的鸢尾花数据集为主线系统讲解 k 最近邻kNN算法的距离度量、数据划分、手撕实现与 scikit-learn 实现、模型评估混淆矩阵、精确率、召回率、F1、ROC/AUC以及网格搜索与交叉验证调参最后扩展到 kNN 回归。读完本文你将能够在真实数据集上独立完成 kNN 分类与回归的全流程建模与评估。算法概述无显式训练过程的“近朱者赤”k 最近邻算法k-Nearest Neighbors简称 kNN是一种用于分类和回归的非参数统计方法由美国统计学家伊芙琳·费克斯Evelyn Fix和小约瑟夫·霍奇斯Joseph Hodges Jr.于 1951 年提出。它的原理非常简单从历史数据中找到 $\small{k}$ 个跟新输入实例最邻近的实例根据它们中多数所属的类别对新实例进行分类或根据它们的目标值输出新实例的预测值。与主流的机器学习算法不同kNN 没有显式的学习训练过程它不建立模型、不拟合参数而是直接“用历史数据说话”其思想可以概括为“近朱者赤近墨者黑”。kNN 有两个关键问题$\small{k}$ 值如何选择——用多少个最近邻来判定新实例的类别或确定目标值如何判定两个实例是近还是远——这就涉及到特征空间中距离的度量方式。距离的度量如何定义“近”与“远”我们可以用距离distance来衡量特征空间中两个实例之间的相似度常用的距离度量包括闵氏距离、马氏距离、余弦距离、编辑距离等。闵氏距离Minkowski Distance是其中最具代表性的一种对于两个 $\small{n}$ 维向量 $\small{\mathbf{x}(x_{1}, x_{2}, \cdots, x_{n})}$ 和 $\small{\mathbf{y}(y_{1}, y_{2}, \cdots, y_{n})}$它们之间的距离定义为$$ d(\mathbf{x}, \mathbf{y}) (\sum_{i1}^{n}{\vert x_{i} - y_{i} \rvert}^{p})^{\frac{1}{p}} $$其中 $\small{p \ge 1}$。虽然 $\small{p \lt 1}$ 时公式仍可计算但不再严格满足距离的定义三角不等式等性质失效因此通常不被视为真正的距离。改变 $\small{p}$ 的取值闵氏距离会退化为我们熟知的各种具体距离当 $\small{p 1}$ 时闵氏距离即曼哈顿距离Manhattan Distance$$ d(\mathbf{x}, \mathbf{y}) \sum_{i1}^{n} \lvert x_{i} - y_{i} \rvert $$当 $\small{p 2}$ 时闵氏距离即欧几里得距离Euclidean Distance也是使用最广泛的度量方式$$ d(\mathbf{x}, \mathbf{y}) \sqrt{\sum_{i1}^{n}(x_{i} - y_{i})^{2}} $$当 $\small{p \to \infty}$ 时闵氏距离即切比雪夫距离Chebyshev Distance它只关心所有维度上差距的最大值$$ d(\mathbf{x}, \mathbf{y}) \underset{i}{max}(\lvert x_{i} - y_{i} \rvert) $$在使用 kNN 做分类时我们的数据集通常都是数值型数据此时直接使用欧几里得距离是一个不错的选择。下图直观展示了欧几里得距离与曼哈顿距离的几何含义差异补充说明在 kNN 的分类场景中如果特征之间存在量纲差异例如一个特征单位是“厘米”、另一个是“千克”直接计算距离会导致量纲大的特征主导结果实际应用中通常需要先对特征做标准化或归一化处理。本文使用的鸢尾花数据集四个特征同以厘米为单位量纲一致因此可以直接计算欧氏距离。数据集介绍机器学习入门的经典——鸢尾花数据集接下来隆重介绍本文后续会持续使用的重要数据集——鸢尾花数据集iris dataset。它是机器学习领域中最著名、最经典的数据集之一由植物学家 Edgar S. Anderson 在加拿大魁北克加斯帕半岛采集由英国统计学家 Ronald A. Fisher 于 1936 年在论文《The Use of Multiple Measurements in Taxonomic Problems》中首次引入被广泛用于机器学习算法的入门和实验。鸢尾花数据集共有150 条样本包含 3 种类型的鸢尾花分别是山鸢尾Iris setosa多彩鸢尾Iris versicolor维吉尼亚鸢尾Iris virginica每种各 50 条样本。每条样本包含4 个特征features和 1 个类别标签class label特征名含义单位Sepal length花萼长度厘米Sepal width花萼宽度厘米Petal length花瓣长度厘米Petal width花瓣宽度厘米数据集中的类别标签取值为 0、1、2分别对应上述三种鸢尾花类型。数据集的加载我们可以通过著名的 Python 机器学习库 scikit-learn 来加载这个数据集。scikit-learn 包含了各种分类、回归、聚类算法同时还提供了多层感知机、支持向量机、随机森林等模型覆盖了从数据预处理、特征工程到模型训练、模型评估和参数调优等各项功能。由于后续课程基本都会用到这个库推荐先安装它pip install scikit-learn如果你已经打开了 IPython 或 Jupyter 但尚未安装 scikit-learn可以通过下面的魔法指令来安装%pip install scikit-learn安装完成后用下面的代码加载鸢尾花数据集并查看数据集的介绍from sklearn.datasets import load_iris # 加载鸢尾花数据集 iris load_iris() # 查看数据集的介绍 print(iris.DESCR)接下来获取数据集中的特征和标签# 特征150行4列的二维数组分别是花萼长、花萼宽、花瓣长、花瓣宽 X iris.data # 标签150个元素的一维数组包含0、1、2三个值分别代表三种鸢尾花 y iris.target如果希望更直观地查看鸢尾花数据集可以用上面获得的特征和标签创建一个 pandas 的 DataFrame 对象有兴趣的读者可以自己动手试一试。数据集的划分通常我们需要将原始数据划分成训练集和测试集训练集用于训练模型测试集用于检验模型训练效果。对于鸢尾花数据集我们可以选择 80% 的数据120 条作为训练集保留 20% 的数据30 条作为测试集。下面的代码用 NumPy 实现了对数据集的划分import numpy as np # 将特征和标签堆叠到同一个数组中 data np.hstack((X, y.reshape(-1, 1))) # 通过随机乱序函数将原始数据打乱 np.random.shuffle(data) # 选择80%的数据作为训练集 train_size int(y.size * 0.8) train, test data[:train_size], data[train_size:] X_train, y_train train[:, :-1], train[:, -1] X_test, y_test test[:, :-1], test[:, -1]更简便的划分方式是用 scikit-learn 封装好的train_test_split函数只需传入特征和标签并指定train_size或test_sizefrom sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split(X, y, train_size0.8, random_state3)train_test_split返回一个四元组四个元素分别代表用于训练的特征、用于训练的标签、用于测试的特征、用于测试的标签。说明train_test_split函数的random_state参数可以理解成随机数的种子。如果使用相同的随机数种子那么划分出的训练集和测试集也是完全相同的这保证了实验的可复现性。kNN 分类的实现下面我们先不用 scikit-learn而是用基础的数据科学库 NumPy 和 SciPy 手撕 kNN 算法。这样做的目的是帮助大家更好地理解算法原理在此基础上再感受 scikit-learn 的强大并明白其中类、函数、参数为什么要如此设定。基于 NumPy 的手撕实现kNN 算法需要计算距离先设计一个计算两个数据点欧氏距离的函数import numpy as np def euclidean_distance(u, v): 计算两个n维向量的欧式距离 return np.sqrt(np.sum(np.abs(u - v) ** 2))接下来设计根据邻居的标签为新数据生成标签的函数from scipy import stats def make_label(X_train, y_train, X_one, k): 根据历史数据中k个最近邻为新数据生成标签 :param X_train: 训练集中的特征 :param y_train: 训练集中的标签 :param X_one: 待预测的样本新数据特征 :param k: 邻居的数量 :return: 为待预测样本生成的标签邻居标签的众数 # 计算x跟每个训练样本的距离 distes [euclidean_distance(X_one, X_i) for X_i in X_train] # 通过一次划分找到k个最小距离对应的索引并获取到相应的标签 labels y_train[np.argpartition(distes, k - 1)[:k]] # 获取标签的众数 return stats.mode(labels).mode说明np.partition函数可以对数组进行一次划分将 k 个比较小的元素放在数组的左边n - k 个比较大的元素放在数组的右边跟快速排序算法中做一次划分操作的效果是一样的。np.argpartition则是把 k 个较小元素的索引放在数组的左边这样经过[:k]切片操作和对y_train的花式索引运算就可以获得 k 个跟新数据距离较小的样本对应的标签。最后通过 scipy 的 stats 模块的mode函数获得标签的众数并用它作为给新数据预测的类别标签。在完成上述准备工作后用 k 最近邻做预测的函数就呼之欲出了def predict_by_knn(X_train, y_train, X_new, k5): KNN算法 :param X_train: 训练集中的特征 :param y_train: 训练集中的标签 :param X_new: 待预测的样本构成的数组 :param k: 邻居的数量默认值为5 :return: 保存预测结果标签的数组 return np.array([make_label(X_train, y_train, X, k) for X in X_new])用上面准备好的鸢尾花训练集和测试集做实验看看predict_by_knn能否很好地运转起来。下面的y_pred是函数预测出的 30 条鸢尾花类型跟y_test做比较即可看到预测效果y_pred predict_by_knn(X_train, y_train, X_test) y_pred y_test输出array([ True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, False, True, True, True, True, True, True, True, True, True, True, True, True, True])结果中有一个False表示这条样本的预测标签与真实标签不同即准确率为 $\small{\frac{29}{30}}$也就是96.67%。当然如果你划分训练集和测试集时使用的random_state与上文不同得到的结果可能会有所不同。基于 scikit-learn 的实现使用 scikit-learn 实现 kNN 分类要简单得多基本上只需要三个动作from sklearn.neighbors import KNeighborsClassifier # 创建模型 model KNeighborsClassifier() # 训练模型 model.fit(X_train, y_train) # 预测结果 y_pred model.predict(X_test)看看输出结果是不是跟我们自己手撕的代码完全一致y_pred y_test输出array([ True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, False, True, True, True, True, True, True, True, True, True, True, True, True, True])当然scikit-learn 考虑得比手撕代码丰富得多例如想知道模型的预测准确率accuracy可以直接调用score方法model.score(X_test, y_test)输出0.9666666666666667模型评估准确率之外还要看什么评价一个分类器的预测效果是否良好不能只看准确率——在类别不平衡的情况下准确率很可能误导你的判断。例如若测试数据中维吉尼亚鸢尾花的样本数量非常小即便模型根本无法判断维吉尼亚鸢尾花模型也会表现出很高的准确率。因此我们还需要考察查准率精确率、查全率召回率、F1 分数等指标而混淆矩阵则是用于详细展示分类模型性能的工具适用于二分类和多分类任务。二分类问题的混淆矩阵与指标二分类问题的混淆矩阵如下所示预测为正类Positive预测为负类Negative实际为正类PositiveTrue PositiveTPFalse NegativeFN实际为负类NegativeFalse PositiveFPTrue NegativeTN用一个例子说明如何利用混淆矩阵计算评估指标假设某种医学检测系统用于预测是否患有某种疾病类别“正类”表示“患病”类别“负类”表示“未患病”1000 个样本的测试结果如下预测为患病预测为未患病实际为患病80TP20FN实际为未患病30FP870TN由此可以计算出以下六类指标准确率Accuracy——所有样本中预测正确的比例$$ \text{Accuracy} \frac{\text{TP} \text{TN}}{\text{TP} \text{FP} \text{FN} \text{TN}} $$上面的例子$\frac{80 870}{80 30 20 870} \frac{950}{1000} 0.95$。精确率Precision——在所有被预测为正类的样本中实际属于正类的比例也称查准率$$ \text{Precision} \frac{\text{TP}}{\text{TP} \text{FP}} $$上面的例子$\frac{80}{80 30} \frac{80}{110} 0.73$。召回率Recall——在所有实际为正类的样本中被模型正确预测为正类的比例也称查全率或真正例率True Positive Rate$$ \text{Recall} \frac{\text{TP}}{\text{TP} \text{FN}} $$上面的例子$\frac{80}{80 20} \frac{80}{100} 0.8$。F1 分数F1 Score——精确率和召回率的调和平均数在两者之间寻求平衡尤其适用于两者有权衡取舍的情况$$ \text{F1 Score} \frac{2}{\frac{1}{\text{Precision}} \frac{1}{\text{Recall}}} 2 \times \frac{\text{Precision} \times \text{Recall}}{\text{Precision} \text{Recall}} $$上面的例子$2 \times \frac{0.7273 \times 0.8}{0.7273 0.8} 0.76$。特异度Specificity和假正例率False Positive RateFPR——特异度衡量在所有实际为负类的样本中被模型正确预测为负类的比例类似于针对负类样本的“召回率”$$ \text{Specificity} \frac{\text{TN}}{\text{TN} \text{FP}} \\ \text{FPR} 1 - \text{Specificity} $$上面的例子模型预测的特异度为$\frac{870}{870 30} \frac{870}{900} 0.97$。ROC 曲线与 AUC 值ROCReceiver Operating Characteristic Curve绘制了召回率TPR与假正例率FPR的关系是衡量分类器阈值特性的标准工具AUCArea Under the Curve是 ROC 曲线下的面积衡量模型区分正类和负类的能力。AUC 的取值范围是 $\small[0, 1]$值越接近 1 表示模型区分正负类的能力越强。其中0.5 AUC 1说明模型优于随机猜测只要妥善设置阈值就具备预测价值AUC 0.5说明模型与随机猜测无异没有预测价值AUC 0.5说明模型比随机猜测还差但只要总能反向预测其实际效果反而优于随机猜测。多分类问题的混淆矩阵与评估报告对于多分类问题混淆矩阵的行数和列数都等于类别数是一个 $\small{n \times n}$ 的方阵。根据上面鸢尾花数据集的预测结果先输出真实值和预测值print(y_test) print(y_pred)输出[0 0 0 0 0 2 1 0 2 1 1 0 1 1 2 0 1 2 2 0 2 2 2 1 0 2 2 1 1 1] [0 0 0 0 0 2 1 0 2 1 1 0 1 1 2 0 2 2 2 0 2 2 2 1 0 2 2 1 1 1]可以制作出如下的三分类混淆矩阵预测为类别0山鸢尾预测为类别1多彩鸢尾预测为类别2维吉尼亚鸢尾实际为类别0山鸢尾1000实际为类别1多彩鸢尾091实际为类别2维吉尼亚鸢尾0010用 scikit-learn 的confusion_matrix和classification_report函数输出混淆矩阵和评估报告from sklearn.metrics import classification_report, confusion_matrix # 输出分类模型混淆矩阵 print(混淆矩阵: ) print(confusion_matrix(y_test, y_pred)) # 输出分类模型评估报告 print(评估报告: ) print(classification_report(y_test, y_pred))输出混淆矩阵: [[10 0 0] [ 0 9 1] [ 0 0 10]] 评估报告: precision recall f1-score support 0 1.00 1.00 1.00 10 1 1.00 0.90 0.95 10 2 0.91 1.00 0.95 10 accuracy 0.97 30 macro avg 0.97 0.97 0.97 30 weighted avg 0.97 0.97 0.97 30如果希望用可视化的方式输出混淆矩阵import matplotlib.pyplot as plt from sklearn.metrics import ConfusionMatrixDisplay # 创建混淆矩阵显示对象 cm_display_obj ConfusionMatrixDisplay(confusion_matrix(y_test, y_pred), display_labelsiris.target_names) # 绘制并显示混淆矩阵 cm_display_obj.plot(cmapplt.cm.Reds) plt.show()说明iris.target_names就是类别标签 0、1、2 对应的三种鸢尾花的英文名。对于二分类问题如果要绘制 ROC 曲线并显示 AUC 值可以使用如下代码from sklearn.metrics import roc_curve, auc from sklearn.metrics import RocCurveDisplay # 手动构造一组真实值和对应的预测值 y_test_ex np.array([0, 0, 0, 1, 1, 0, 1, 1, 1, 0]) y_pred_ex np.array([1, 0, 0, 1, 1, 0, 1, 1, 0, 1]) # 通过roc_curve函数计算出FPR假正例率和TPR真正例率 fpr, tpr, _ roc_curve(y_test_ex, y_pred_ex) # 通过auc函数计算出AUC值并通过RocCurveDisplay类绘制图形 RocCurveDisplay(fprfpr, tprtpr, roc_aucauc(fpr, tpr)).plot() plt.show()参数调优k 值选择与网格搜索交叉验证之前说过kNN 算法有两个关键问题距离的度量与 k 值的选择。使用 scikit-learn 的KNeighborsClassifier创建分类器模型时可以对模型的超参数进行设置这里有几个比较重要的参数参数默认值含义与说明n_neighbors5近邻的数量即 kNN 算法中 k 的值weightsuniform可选uniform或distance前者所有样本权重相同后者距离越近权重越高也可传入自定义函数确定每个样本的权重algorithmauto可选auto、ball_tree、kd_tree、brute见下方详解leaf_size30使用ball_tree或kd_tree时限制树结构叶子节点的最大样本数量影响树的构建和节点查找性能p2闵可夫斯基距离公式中的 p默认值为 2即计算欧氏距离algorithm四个选项的差异如下ball_tree一种基于球体划分的树形结构将数据点分配到层次化的树结构中在高维数据和稀疏数据场景下有较好的性能kd_tree通过选择一个维度将空间划分为若干个子区域再进行搜索从而避免跟所有邻居逐一比较对于低维度和空间分布均匀的数据效果较好但在高维空间会遇到维度灾难问题auto根据输入数据的维度自动选择ball_tree或kd_treebrute使用暴力搜索穷举法处理小数据集时是简单而有效的选择。网格搜索与 K-Fold 交叉验证可以使用网格搜索Grid Search和交叉验证Cross Validation的方式对超参数进行调整评估模型的泛化能力提升预测效果网格搜索通过穷举法遍历给定的超参数空间找到最优的超参数组合交叉验证将训练集分成多个子集通过在不同的训练集和验证集上进行多次训练和评估对模型的预测效果进行综合评判。K-Fold 交叉验证是最常用的交叉验证方法将数据集划分为 K 个子集每次选取其中一个子集作为验证集剩下的 K-1 个子集作为训练集对每个子集重复这个过程完成 K 次训练和评估并将平均值作为模型的最终性能评估下面的代码用 scikit-learn 的GridSearchCV做网格搜索和交叉验证寻找针对鸢尾花数据集实施 kNN 的最优参数from sklearn.model_selection import GridSearchCV # 网格搜索交叉验证 gs GridSearchCV( estimatorKNeighborsClassifier(), param_grid{ n_neighbors: [1, 3, 5, 7, 9, 11, 13, 15], weights: [uniform, distance], p: [1, 2] }, cv5 ) gs.fit(X_train, y_train)通过下面的代码获得最优参数及其评分print(最优参数:, gs.best_params_) print(评分:, gs.best_score_)输出最优参数: {n_neighbors: 5, p: 2, weights: uniform} 评分: 0.9666666666666666如果需要使用训练好的最优模型进行预测gs.predict(X_test)kNN 回归从分类走向数值预测kNN 算法通常用于解决分类问题当然也可以用于解决回归问题其基本思想与分类一致找出跟新实例最近的 k 个邻居然后根据这 k 个邻居的目标值预测新实例的目标值。与分类不同回归预测的是一个数值而非类别标签因此通常会使用平均或加权平均的方式处理 k 个邻居的目标值。回到上一课 81.浅谈机器学习.md 中用月收入预测网购支出的例子用 scikit-learn 的KNeighborsRegressor构建回归模型。先做一些准备工作# 每月收入 incomes np.array([ 9558, 8835, 9313, 14990, 5564, 11227, 11806, 10242, 11999, 11630, 6906, 13850, 7483, 8090, 9465, 9938, 11414, 3200, 10731, 19880, 15500, 10343, 11100, 10020, 7587, 6120, 5386, 12038, 13360, 10885, 17010, 9247, 13050, 6691, 7890, 9070, 16899, 8975, 8650, 9100, 10990, 9184, 4811, 14890, 11313, 12547, 8300, 12400, 9853, 12890 ]) # 每月网购支出 outcomes np.array([ 3171, 2183, 3091, 5928, 182, 4373, 5297, 3788, 5282, 4166, 1674, 5045, 1617, 1707, 3096, 3407, 4674, 361, 3599, 6584, 6356, 3859, 4519, 3352, 1634, 1032, 1106, 4951, 5309, 3800, 5672, 2901, 5439, 1478, 1424, 2777, 5682, 2554, 2117, 2845, 3867, 2962, 882, 5435, 4174, 4948, 2376, 4987, 3329, 5002 ]) X np.sort(incomes).reshape(-1, 1) # 将收入排序后处理成二维数组 y outcomes[np.argsort(incomes)] # 将网购支出按照收入进行排序说明即便只有一个特征我们也需要将自变量处理成二维数组的形式因为训练模型时使用的fit方法并不能接受一维数组作为它的第一个参数。这里提前对 X 和 y 排序是为了待会绘制散点图和折线图时根据 X 的值从小到大依次绘制。接下来创建 kNN 回归模型from sklearn.neighbors import KNeighborsRegressor # 创建模型 model KNeighborsRegressor() # 训练模型 model.fit(X, y) # 预测结果 y_pred model.predict(X)上面直接用所有历史数据进行模型训练通过绘制图形来看看预测效果# 原始数据散点图 plt.scatter(X, y, colornavy) # 预测结果折线图 plt.plot(X, y_pred, colorcoral) plt.show()从图中可以看出kNN 回归的本质是对样本点做局部的平滑拟合。kNN 回归模型计算复杂度较高而且对噪声数据非常敏感在很多场景下它可能并不是一个很好的选择。至于如何评估一个回归模型的效果如 MSE、R² 等指标后续章节参见 85.回归模型.md会继续讲解。总结kNN 的优势与局限kNN 算法是一种简单但不失强大的机器学习算法适用于小数据集上的分类和回归任务。它的优点非常突出简单易懂思想朴素“近朱者赤近墨者黑”无需复杂的模型解释没有显式的训练过程不需要拟合参数新数据到来时即时计算不依赖于对数据分布的假设可以适应复杂的数据模式。但 kNN 的缺点也同样明显选择算法前务必权衡计算效率问题预测时需要计算新样本与所有训练样本的距离数据集较大时效率低下是 kNN 最大的短板对噪声敏感个别噪声样本可能直接影响近邻的选择结果预测结果依赖 k 的取值k 值过小容易过拟合只看到局部少数邻居k 值过大又容易欠拟合被较远的邻居干扰受样本均衡性影响不均衡的样本可能导致类别偏倚Class Imbalance Bias——如果大部分训练样本属于类别 A 而类别 B 样本较少分类时 kNN 更容易将测试样本预测为类别 A。掌握 kNN 之后建议继续深入学习仓库中的后续章节83.决策树和随机森林.md、84.朴素贝叶斯算法.md、85.回归模型.md并在 90.机器学习实战.md 中将所学算法应用到真实业务数据集上完成从理论到实战的完整闭环。赞分享文档教程【免费下载链接】Python-100-DaysPython - 100天从新手到大师项目地址https://gitcode.com/GitHub_Trending/py/Python-100-Days点击查看免费下载相关推荐Python-100-Days 机器学习实战K-Means 聚类算法从原理到 scikit-learn 实现Python 100 Days 机器学习实战K Means 聚类算法从原理到 scikit learn 实现 聚类Clustering是数据挖掘与机器学习文档教程Python-100-Days 机器学习篇Day 85回归模型从理论到 scikit-learn 实战Python 100 Days 机器学习篇Day 85回归模型从理论到 scikit learn 实战 回归模型是机器学习和统计学中最基础的模型之一它负文档教程OpenCV 机器学习入门深入理解 k 近邻k-Nearest Neighbour算法原理与实现OpenCV 机器学习入门深入理解 k 近邻k Nearest Neighbour算法原理与实现 本篇技术指南围绕 OpenCV Contrib 仓库中计算机视觉图像处理深度学习机器学习创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表