这篇文章主要讲解了“算法模型自动超参数优化方法教程”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“算法模型自动超参数优化方法教程”吧!
什么是超参数?
学习器模型中一般有两类参数,一类是可以从数据中学习估计得到,我们称为参数(Parameter)。还有一类参数时无法从数据中估计,只能靠人的经验进行设计指定,我们称为超参数(Hyper parameter)。超参数是在开始学习过程之前设置值的参数。相反,其他参数的值通过训练得出。
超参数:
定义关于模型的更高层次的概念,如复杂性或学习能力 不能直接从标准模型培训过程中的数据中学习,需要预先定义 可以通过设置不同的值,训练不同的模型和选择更好的测试值来决定 参数空间的搜索一般由以下几个部分构成:
一个estimator(回归器 or 分类器)
一个参数空间
一个搜索或采样方法来获得候选参数集合
一个交叉验证机制
一个评分函数
Scikit-Learn中的超参数优化方法
在机器学习模型中,比如随机森林中决策树的个数,人工神经网络模型中的隐藏层层数和每层的节点个数,正则项中常数大小等等,它们都需要事先指定。超参数选择不恰当,就会出现欠拟合或者过拟合的问题。在Scikit-Learn中,超参数是在学习过程开始之前设置其值的参数。典型的例子包括支持向量机里的C、kernel、gamma等。
class sklearn.svm.SVC(*, C=1.0, kernel='rbf', degree=3, gamma='scale', coef0=0.0, shrinking=True, probability=False, tol=0.001, cache_size=200, class_weight=None, verbose=False, max_iter=-1, decision_function_shape='ovr', break_ties=False, random_state=None)
使用过程中可以使用estimator.get_params() 获得学习器模型的超参数列表和当前取值。
Sklearn提供了两种通用的超参数优化方法:网格搜索与随机搜索。
交叉验证 (Cross-Validation)- CV 简介
在机器学习里,通常来说我们不能将全部用于数据训练模型,否则我们将没有数据集对该模型进行验证,从而评估我们的模型的预测效果。为了解决这一问题,有如下常用的方法:
The Validation Set Approach(验证集方案)
这种是方法最简单的,也是很容易就想到的。我们可以把整个数据集分成两部分,一部分用于训练,一部分用于验证,这也就是我们经常提到的训练集(training set)和测试集(test set)。
不过,这个简单的方法存在两个弊端:
最终模型与参数的选取将极大程度依赖于你对训练集和测试集的划分方法。在不同的划分方法下,test MSE的变动是很大的,而且对应的最优degree也不一样。所以如果我们的训练集和测试集的划分方法不够好,很有可能无法选择到最好的模型与参数。
该方法只用了部分数据进行模型的训练。当用于模型训练的数据量越大时,训练出来的模型通常效果会越好。所以训练集和测试集的划分意味着我们无法充分利用我们手头已有的数据,所以得到的模型效果也会受到一定的影响。
基于这样的背景,有人就提出了Cross-Validation方法,也就是交叉验证。
Cross-Validation
LOOCV(留一法)
LOOCV即(Leave-one-out cross-validation)。像Test set approach一样,LOOCV方法也包含将数据集分为训练集和测试集这一步骤。但是不同的是,我们只用一个数据作为测试集,其他的数据都作为训练集,并将此步骤重复N次(N为数据集的数据数量)。
假设我们现在有n个数据组成的数据集,那么LOOCV的方法就是每次取出一个数据作为测试集的唯一元素,而其他n-1个数据都作为训练集用于训练模型和调参。结果就是我们最终训练了n个模型,每次都能得到一个MSE。而计算最终test MSE则就是将这n个MSE取平均。
比起test set approach,LOOCV有很多优点。首先它不受测试集合训练集划分方法的影响,因为每一个数据都单独的做过测试集。同时,其用了n-1个数据训练模型,也几乎用到了所有的数据,保证了模型的bias更小。不过LOOCV的缺点也很明显,那就是计算量过于大,是test set approach耗时的n-1倍。
K-fold Cross Validation(k 折交叉验证)
K折交叉验证,和LOOCV的不同在于,我们每次的测试集将不再只包含一个数据,而是多个,具体数目将根据K的选取决定。比如,如果K=5,那么我们利用五折交叉验证的步骤就是:
将所有数据集分成5份
不重复地每次取其中一份做测试集,用其他四份做训练集训练模型,之后计算该模型在测试集上的MSE
将5次的MSE取平均作为最后而得到MSE
不难理解,其实LOOCV是一种特殊的K-fold Cross Validation(K=N)。最后K的选取是一个Bias和Variance的trade-off。K越大,每次投入的训练集的数据越多,模型的Bias越小。但是K越大,又意味着每一次选取的训练集之前的相关性越大(考虑最极端的例子,当k=N,也就是在LOOCV里,每次都训练数据几乎是一样的)。而这种大相关性会导致最终的test error具有更大的Variance。一般K值选择5或10。
网格搜索 GridSearchCV
我们在选择超参数有两个途径:1)凭经验;2)选择不同大小的参数,带入到模型中,挑选表现最好的参数。通过途径2选择超参数时,人力手动调节注意力成本太高,非常不值得。For循环或类似于for循环的方法受限于太过分明的层次,不够简洁与灵活,注意力成本高,易出错。GridSearchCV 称为网格搜索交叉验证调参,它通过遍历传入的参数的所有排列组合,通过交叉验证的方式,返回所有参数组合下的评价指标得分。
GridSearchCV听起来很高大上,其实就是暴力搜索。注意的是,该方法在小数据集上很有用,数据集大了就不太适用了。数据量比较大的时候可以使用一个快速调优的方法——坐标下降。它其实是一种贪心算法:拿当前对模型影响最大的参数调优,直到最优化;再拿下一个影响最大的参数调优,如此下去,直到所有的参数调整完毕。这个方法的缺点就是可能会调到局部最优而不是全局最优,但是省时间省力。
GridSearchCV使用说明
class sklearn.model_selection.GridSearchCV(estimator, param_grid, scoring=None, n_jobs=None, refit=True, cv='warn', verbose=0, pre_dispatch='2*n_jobs', error_score='raise-deprecating', return_train_score='warn')
参数详解:
estimator:所使用的模型,传入除需要确定最佳的参数之外的其他参数。模型都需要一个score方法,或传入scoring参数。
param_grid:需要搜索调参的参数字典,参数值类型为字典(dict)或由字典组成的列表(list)。用于设置待评测参数和对应的参数值。
scoring:模型评价标准,默认None,这时需要使用score函数;或者如scoring=’roc_auc’,根据所选模型不同,评价准则不同。字符串(函数名),或是可调用对象,需要其函数签名形如:scorer(estimator, X, y);如果是None,则使用estimator的误差估计函数。下文表格中详细指定了score可取的值和函数形式。
n_jobs:并行计算线程个数,1:默认值,可以设置为 -1(跟CPU核数一致),这样可以充分使用机器的所有处理器。
refit:默认为True,程序将会以交叉验证训练集得到的最佳参数。即在搜索参数结束后,用最佳参数结果再次fit一遍全部数据集。
cv:交叉验证参数,可接受的参数:
默认None,使用3折交叉验证。
指定fold数量
CV splitter
yield训练、测试数据的生成器。
verbose:日志冗长度
0:不输出训练过程
1:偶尔输出
dayda'y'd1:对每个子模型都输出
pre_dispatch:指定总共分发的并行任务数。当n_jobs大于1时,数据将在每个运行点进行复制,这可能导致内存问题,而设置pre_dispatch参数,则可以预先划分总共的job数量,使数据最多被复制pre_dispatch次。
error_score:拟合中发生错误时分配的值,如果设置为’raise’则会引发错误。如果设置的是一个数字,则为引发FitFailedWarning的警告信息,默认值将在22版本其由原先的’raise’ 更改为np.nan。
return_train_score:如果“False”,cv_results_属性将不包括训练分数。
GridSearchCV对象
cv_results_:用来输出cv结果的,可以是字典形式也可以是numpy形式,还可以转换成DataFrame格式
best_estimator_:通过搜索参数得到的最好的估计器,当参数refit=False时该对象不可用
best_score_:float类型,输出最好的成绩
best_params_:通过网格搜索得到的score最好对应的参数
best_index_:对应于最佳候选参数设置的索引(cv_results_数组)。cv_results _ [‘params’] [search.best_index_]中的dict给出了最佳模型的参数设置,给出了最高的平均分数(best_score_)。
scorer_:评分函数
n_splits_:交叉验证的数量
refit_time_:refit所用的时间,当参数refit=False时该对象不可用
GridSearchCV方法
decision_function(X):返回决策函数值(比如svm中的决策距离)
fit(X,y=None,groups=None,fit_params):在数据集上运行所有的参数组合
get_params(deep=True):返回估计器的参数
inverse_transform(Xt):Call inverse_transform on the estimator with the best found params.
predict(X):返回预测结果值(0/1)
predict_log_proba(X):Call predict_log_proba on the estimator with the best found parameters.
predict_proba(X):返回每个类别的概率值(有几类就返回几列值)
score(X, y=None):返回函数
set_params(**params):Set the parameters of this estimator.
transform(X):在X上使用训练好的参数
使用示例:
from sklearn.model_selection import GridSearchCV from sklearn.svm import SVR from sklearn import datasets dataset = datasets.load_iris() X = dataset.data y = dataset.target grid = GridSearchCV( estimator=SVR(kernel='rbf'), param_grid={ 'C': [0.1, 1, 10, 100], 'epsilon': [0.0001, 0.001, 0.01, 0.1, 1, 10], 'gamma': [0.001, 0.01, 0.1, 1] }, cv=5, scoring='neg_mean_squared_error', verbose=0, n_jobs=-1) grid.fit(X, y) print(grid.best_score_) print(grid.best_params_)
随机搜索 RandomizedSearchCV
我们在搜索超参数的时候,如果超参数个数较少(三四个或者更少),那么我们可以采用网格搜索,一种穷尽式的搜索方法。但是当超参数个数比较多的时候,我们仍然采用网格搜索,那么搜索所需时间将会指数级上升。所以有人就提出了随机搜索的方法,随机在超参数空间中搜索几十几百个点,其中就有可能有比较小的值。这种做法比上面稀疏化网格的做法快,而且实验证明,随机搜索法结果比稀疏网格法稍好。
RandomizedSearchCV使用方法和类GridSearchCV 很相似,但他不是尝试所有可能的组合,而是通过选择每一个超参数的一个随机值的特定数量的随机组合,这个方法有两个优点:
相比于整体参数空间,可以选择相对较少的参数组合数量。如果让随机搜索运行,它会探索每个超参数的不同的值 可以方便的通过设定搜索次数,控制超参数搜索的计算量。添加参数节点不会影响性能,不会降低效率。RandomizedSearchCV的使用方法其实是和GridSearchCV一致的,但它以随机在参数空间中采样的方式代替了GridSearchCV对于参数的网格搜索,在对于有连续变量的参数时,RandomizedSearchCV会将其当做一个分布进行采样进行这是网格搜索做不到的,它的搜索能力取决于设定的n_iter参数。
RandomizedSearchCV使用说明
class sklearn.model_selection.RandomizedSearchCV(estimator, param_distributions, *, n_iter=10, scoring=None, n_jobs=None , refit=True, cv=None, verbose=0, pre_dispatch='2*n_jobs', random_state=None, error_score=nan, return_train_score=False)
与GridSearchCV不同的主要有以下两参数:
param_distributions:参数分布,字典格式。将我们所传入模型当中的参数组合为一个字典。其搜索策略如下:
对于搜索范围是distribution的超参数,根据给定的distribution随机采样
对于搜索范围是list的超参数,在给定的list中等概率采样
n_iter:训练300次,数值越大,获得的参数精度越大,但是搜索时间越长 使用示例:
from scipy.stats import randint as sp_randint from sklearn.model_selection import RandomizedSearchCV from sklearn.datasets import load_digits from sklearn.ensemble import RandomForestClassifier # 载入数据 digits = load_digits() X, y = digits.data, digits.target # 建立一个分类器或者回归器 clf = RandomForestClassifier(n_estimators=20) # 给定参数搜索范围:list or distribution param_dist = {"max_depth": [3, None], # 给定list "max_features": sp_randint(1, 11), # 给定distribution "min_samples_split": sp_randint(2, 11), # 给定distribution "bootstrap": [True, False], # 给定list "criterion": ["gini", "entropy"]} # 给定list # 用RandomSearch+CV选取超参数 n_iter_search = 20 random_search = RandomizedSearchCV(clf, param_distributions=param_dist, n_iter=n_iter_search, cv=5, iid=False) random_search.fit(X, y) print(random_search.best_score_) print(random_search.best_params_)
自动超参数优化方法
贝叶斯优化方法(Bayesian Optimization)
贝叶斯优化用于机器学习调参由J. Snoek(2012)提出,主要思想是,给定优化的目标函数(广义的函数,只需指定输入和输出即可,无需知道内部结构以及数学性质),通过不断地添加样本点来更新目标函数的后验分布(高斯过程,直到后验分布基本贴合于真实分布。简单的说,就是考虑了上一次参数的信息,从而更好的调整当前的参数。
贝叶斯优化与常规的网格搜索或者随机搜索的区别是:
贝叶斯调参采用高斯过程,考虑之前的参数信息,不断地更新先验;网格搜索未考虑之前的参数信息
贝叶斯调参迭代次数少,速度快;网格搜索速度慢,参数多时易导致维度爆炸
贝叶斯调参针对非凸问题依然稳健;网格搜索针对非凸问题易得到局部最优
贝叶斯优化提供了一个优雅的框架来尽可能少的步骤中找到全局最小值。
让我们构造一个函数c(x)或者一个接收输入x的模型,如下图所示为c(x)的形状。当然,优化器并不知道该函数,称之为“目标函数”。
贝叶斯优化通过代理优化的方式来完成任务。代理函数通过采样点模拟构造(见下图)。
根据代理函数,我们大致可以确定哪些点是可能的最小值。然后再这些点附近做更多的采样,并随之更新代理函数。
每一次迭代,我们都会继续观察当前的代用函数,通过采样了解更多感兴趣的区域,并更新函数。需要注意的是,代用函数在数学上的表达方式将大大降低评估成本。经过一定的迭代次数后,我们注定要到达一个全局最小值,除非函数的形状非常诡异。
让我们仔细看看代用函数,通常用高斯过程来表示,它可以被认为是掷骰子,返回与给定数据点(如sin、log)拟合的函数,而不是1到6的数字。这个过程会返回几个函数,这些函数都附有概率。为什么用高斯过程,而不是其他的曲线拟合方法来模拟代用函数,有一个很好的理由:它是贝叶斯性质的。代用函数–表示为概率分布,即先验–被更新为 “获取函数”。这个函数负责在勘探和开发的权衡中提出新的测试点。
“开发”力求在代用模型预测的目标好的地方采样。这就是利用已知的有希望的点。但是,如果我们已经对某一区域进行了足够的探索,那么不断地利用已知的信息就不会有什么收获。
“探索”力求在不确定性较高的地点进行采样。这就确保了空间的任何主要区域都不会未被探索–全局最小值可能恰好就在那里。
一个鼓励过多的开发和过少探索的获取函数将导致模型只停留在它首先发现的最小值(通常是局部的–“只去有光的地方”)。一个鼓励相反的获取函数将不会首先停留在一个最小值,本地或全球。在微妙的平衡中产生良好的结果。acquisition 函数,我们将其表示为a(x),必须同时考虑开发和探索。常见的获取函数包括预期改进和最大改进概率,所有这些函数都是在给定先验信息(高斯过程)的情况下,衡量特定投入在未来可能得到回报的概率。
让我们把这些东西整合起来。贝叶斯优化可以这样进行。
鸿蒙官方战略合作共建——HarmonyOS技术社区
初始化一个高斯过程 “代用函数 “的先验分布。
选择几个数据点x,使在当前先验分布上运行的获取函数a(x)最大化。
评估目标成本函数c(x)中的数据点x,得到结果,y。
用新的数据更新高斯过程先验分布,以产生一个后验(它将成为下一步的先验)。
重复步骤2-5进行多次迭代。
解释当前的高斯过程分布(这是非常便宜的),以找到全局最小值。
贝叶斯优化就是把概率论的思想放在代入优化的思想后面。综上所述:
代用优化利用代用函数或近似函数通过抽样来估计目标函数。
贝叶斯优化将代用优化置于概率框架中,将代用函数表示为概率分布,可以根据新的信息进行更新。
获取函数用于评估在当前已知的先验条件下,探索空间中某一点会产生 “好 “收益的概率,平衡探索与开发
主要在目标函数评估成本很高的时候使用贝叶斯优化,常用于超参数调整。
Hyperopt
Hyperopt是一个强大的Python库,用于超参数优化,由jamesbergstra开发。Hyperopt使用贝叶斯优化的形式进行参数调整,允许你为给定模型获得最佳参数。它可以在大范围内优化具有数百个参数的模型。
Hyperopt包含4个重要的特性
1、搜索空间
hyperopt有不同的函数来指定输入参数的范围,这些是随机搜索空间。选择最常用的搜索选项:
choice(label, options)-这可用于分类参数,它返回其中一个选项,它应该是一个列表或元组。示例:hp.choice(“criterion”,[“gini”,”entropy”,])
randint(label, upper)-可用于整数参数,它返回范围(0,upper)内的随机整数。示例:hp.randint(“max_features”,50)
uniform(label, low, high)-它返回一个介于low和high之间的值。示例:hp.uniform(“max_leaf_nodes”,1,10)
你可以使用的其他选项包括:
normal(label, mu, sigma)-这将返回一个实际值,该值服从均值为mu和标准差为sigma的正态分布
qnormal(label, mu, sigma, q)-返回一个类似round(normal(mu, sigma) / q) * q的值
lognormal(label, mu, sigma)-返回exp(normal(mu, sigma))
qlognormal(label, mu, sigma, q) -返回一个类似round(exp(normal(mu, sigma)) / q) * q的值
2、目标函数
这是一个最小化函数,它从搜索空间接收超参数值作为输入并返回损失。这意味着在优化过程中,我们使用选定的超参数值训练模型并预测目标特征,然后评估预测误差并将其返回给优化器。优化器将决定要检查哪些值并再次迭代。你将在一个实际例子中学习如何创建一个目标函数。
3、fmin
fmin函数是对不同的算法集及其超参数进行迭代,然后使目标函数最小化的优化函数。fmin有5个输入是:
最小化的目标函数
定义的搜索空间
使用的搜索算法有随机搜索、TPE(Tree-Parzen估计器)和自适应TPE。注意:rand.suggest以及hyperopt.tpe.suggest为超参数空间的顺序搜索提供逻辑。
最大评估数
trials对象(可选)
4、试验对象
Trials对象用于保存所有超参数、损失和其他信息,这意味着你可以在运行优化后访问它们。此外,trials 可以帮助你保存和加载重要信息,然后继续优化过程。
Hyperopt的使用
在理解了Hyperopt的重要特性之后,下面将介绍Hyperopt的使用方法。
初始化要搜索的空间
定义目标函数
选择要使用的搜索算法
运行hyperopt函数
分析测试对象中存储的评估输出
from sklearn import datasets from hyperopt import fmin, tpe, hp, STATUS_OK, Trials from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import cross_val_score iris = datasets.load_iris() X = iris.data y = iris.target def hyperopt_train_test(params): clf = KNeighborsClassifier(**params) return cross_val_score(clf, X, y).mean() # 定义参数空间 space_knn = { 'n_neighbors': hp.choice('n_neighbors', range(1, 100)) } # 定义最小化函数(目标函数) def fn_knn(params): acc = hyperopt_train_test(params) return {'loss': -acc, 'status': STATUS_OK} # hyperopt最小化函数,所以在acc中添加了负号 # 实例化Trial 对象,对模型进行微调,然后用其超参数值打印出最佳损失 trials = Trials() best = fmin(fn_knn, space_knn, algo=tpe.suggest, max_evals=100, trials=trials) print("Best: {}".format(best)) print(trials.results) # 搜索期间“objective”返回的词典列表。
algo指定搜索算法,目前支持以下算法:
随机搜索(hyperopt.rand.suggest)
模拟退火(hyperopt.anneal.suggest)
TPE算法(tpe.suggest,算法全称为Tree-structured Parzen Estimator Approach)
除了Hyperopt外,贝叶斯优化方法的Python包还有:
https://github.com/optuna/optuna
https://github.com/fmfn/BayesianOptimization
https://github.com/HIPS/Spearmint
遗传算法(Genetic Algorithms)
遗传算法试图将自然选择机制应用于机器学习环境。它受到达尔文自然选择过程的启发,因此通常也称为进化算法。假设我们创建了具有一些预定义超参数的N个机器学习模型。然后,我们可以计算每个模型的准确性,并决定只保留一半模型(性能最好的模型)。现在,我们可以生成具有与最佳模型相似的超参数的后代,以便再次获得N个模型的种群。在这一点上,我们可以再次计算每个模型的准确性,并在定义的世代中重复该循环。这样,只有最佳模型才能在流程结束时生存下来。
TPOT是一种基于遗传算法优化机器学习管道(pipeline)的Python自动机器学习工具。简单来说,就是TPOT可以智能地探索数千个可能的pipeline,为数据集找到最好的pipeline,从而实现机器学习中最乏味的部分。
更重要地是,一旦TPOT完成搜索,TPOT同时也提供了Python代码。通过这个代码,我们可以具体地知道TPOT获得最优性能时的具体pipeline的内容,这对于后续修改是十分方便的!
TPOT是在sklearn的基础之上做的封装库。其主要封装了sklearn的模型相关模块、processesing模块和feature_selection模块,所以TPOT的主要功能是集中在使用pipeline的方式完成模型的数据预处理、特征选择和模型选择方面。此外,我们还发现了TPOT已经对xgboost进行了支持。
虽然TPOT使用遗传算法代替了传统的网格搜索进行超参数选择,但由于默认初始值的随机性,在少量的进化(迭代)次数下,TPOT最终选择的模型往往并不相同。
计算效率问题。作者在代码中写道:进化(迭代)次数和每一代保留的个体数量值越多,最终得模型得分会越高。但这同样也会导致耗时很长。如果使用相当复杂的数据集或运行TPOT短时间,不同的TPOT运行可能会导致不同的流水线推荐。TPOT的优化算法本质上是随机的,这意味着它使用随机性(部分地)来搜索可能的流水线空间。当两个TPOT运行推荐不同的管道时,这意味着TPOT运行由于时间不够而不收敛,或者多个管道在数据集上执行的次数大致相同。这实际上是一个优于固定网格搜索技术的优点:TPOT是一个助手,它通过探索您可能从未考虑过的流水线配置来提供解决如何解决特定机器学习问题的想法,然后将微调留给更受约束的参数调整技术,例如网格搜索。
使用TPOT(版本0.9.5)开发模型需要把握以下几点:
在使用TPOT进行建模前需要对数据进行必要的清洗和特征工程操作。
TPOT目前只能做有监督学习。
TPOT目前支持的分类器主要有贝叶斯、决策树、集成树、SVM、KNN、线性模型、xgboost。
TPOT目前支持的回归器主要有决策树、集成树、线性模型、xgboost。
TPOT会对输入的数据做进一步处理操作,例如二值化、聚类、降维、标准化、正则化、独热编码操作等。
根据模型效果,TPOT会对输入特征做特征选择操作,包括基于树模型、基于方差、基于F-值的百分比。
可以通过export()方法把训练过程导出为形式为sklearn pipeline的.py文件
示例代码:
from tpot import TPOTClassifier from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split iris = load_iris() X = iris.data y = iris.target X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) tpot = TPOTClassifier(generations=5, population_size=50, verbosity=2, n_jobs=-1) tpot.fit(X_train, y_train) print(tpot.score(X_test, y_test))
TPOT的主要参数:
generations – 确定创建子代(新个体)的迭代次数
population_size – 创建个体的初始数量(这些用于创建后代)
offspring_size – 每一代所需创造的新个体数
mutation_rate – 出现属性值随机更改的概率(包括新参数的方法,在初始群体中可能不可用)
crossover_rate –用于创造后代的个体所占的百分比
使用这个迭代过程,我们选出最佳配置。准备遗传算法的结果一般取决于初始状态。因此,它随机产生的初始种群影响输出,重新运行相同的设置可能会输出不同的结果。
感谢各位的阅读,以上就是“算法模型自动超参数优化方法教程”的内容了,经过本文的学习后,相信大家对算法模型自动超参数优化方法教程这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是编程网,小编将为大家推送更多相关知识点的文章,欢迎关注!