基于协方差矩阵自适应演化策略(CMA-ES)的高效特征选择

news2024/10/7 8:29:53

特征选择是指从原始特征集中选择一部分特征,以提高模型性能、减少计算开销或改善模型的解释性。特征选择的目标是找到对目标变量预测最具信息量的特征,同时减少不必要的特征。这有助于防止过拟合、提高模型的泛化能力,并且可以减少训练和推理的计算成本。

如果特征N的数量很小,那么穷举搜索可能是可行的:比如说尝试所有可能的特征组合,只保留成本/目标函数最小的那一个。但是如果N很大,那么穷举搜索肯定是不可能的。因为对于N的组合是一个指数函数,所以在这种情况下,必须使用启发式方法:以一种有效的方式探索搜索空间,寻找能够最小化用于执行搜索的目标函数的特征组合。

找到一个好的启发式算法并非易事。R中的regsubsets()函数有一些可以使用的选项。此外,scikit-learn提供了几种执行启发式特征选择的方法,但是找到一个好的、通用的启发式——以最通用的形式——是一个难题。所以本文将探讨一些即使在N相当大的情况下也能很好地工作的方法。

数据集

我们这里使用Kaggle上非常流行的House Prices数据集(MIT许可)——然后经过一些简单的特征转换后,最终得到一个213个特征(N=213)和1453个观察值的数据集。我使用的模型是线性回归,statsmodels.api.OLS(),我们试图最小化的目标函数是BIC,贝叶斯信息标准,一种信息损失的度量,所以BIC越低越好。它与AIC相似,只不过BIC倾向于生成更简洁的模型(它更喜欢特征较少的模型)。最小化AIC或BIC可以减少过拟合。但也可以使用其他目标函数,例如r方(目标中已解释的方差)或调整后的r方——只要记住r平方越大越好,所以这是一个最大化问题。

目标函数的选择在这里是无关紧要的。重要的是我们要有一个目标函数,因为需要尝试使用各种技术来优化它。

但首先,让我们看看一些众所周知的、久经考验的特征选择技术,我们将使用它与后面描述的更复杂的技术进行基线比较。

顺序特征搜索 SFS

SFS相当简单,它从选择一个特征开始,然后选择使目标函数最小的特征。一旦一个特性被选中,它就会永远被选中。然后它会尝试添加另一个特征(这时就有2个了),然后再次最小化目标。当所有特征一起被尝试时,搜索就结束了,使目标最小化的组合最为特征选择的结果。

SFS是一种贪婪算法——每个选择都是局部最优的——而且它永远不会回去纠正自己的错误。但他有一个优点就是即使N很大,它还是很快的。它尝试的组合总数是N(N+1)/2,这是一个二次多项式。

在Python中使用mlextend库的SFS代码是这样的

 import statsmodels.api as sm
 from mlxtend.feature_selection import SequentialFeatureSelector as SFS
 from sklearn.base import BaseEstimator
 
 class DummyEstimator(BaseEstimator):
     # mlxtend wants to use an sklearn estimator, which is not needed here
     # (statsmodels OLS is used instead)
     # create a dummy estimator to pacify mlxtend
     def fit(self, X, y=None, **kwargs):
         return self
 
 def neg_bic(m, X, y):
     # objective function
     lin_mod_res = sm.OLS(y, X, hasconst=True).fit()
     return -lin_mod_res.bic
 
 seq_selector = SFS(
     DummyEstimator(),
     k_features=(1, X.shape[1]),
     forward=True,
     floating=False,
     scoring=neg_bic,
     cv=None,
     n_jobs=-1,
     verbose=0,
     # make sure the intercept is not dropped
     fixed_features=['const'],
 )
 
 n_features = X.shape[1] - 1
 objective_runs_sfs = round(n_features * (n_features + 1) / 2)
 t_start_seq = time.time()
 # mlxtend will mess with your dataframes if you don't .copy()
 seq_res = seq_selector.fit(X.copy(), y.copy())
 t_end_seq = time.time()
 run_time_seq = t_end_seq - t_start_seq
 seq_metrics = seq_res.get_metric_dict()

它快速找到了这些组合:

 best k:         36
 best objective: 33708.98602877906
 R2 @ best k:    0.9075677543649224
 objective runs: 22791
 total run time: 44.850 sec

213个特征,最好的是36个。最好的BIC是33708.986,在我的系统上完成不到1分钟。它调用目标函数22.8k次。

以下是最佳BIC值和r平方值,作为所选特征数量的函数:

现在我们来试试更复杂的。

协方差矩阵自适应演化 CMA-ES

这是一个数值优化算法。它与遗传算法属于同一类(它们都是进化的),但CMA-ES与遗传算法截然不同。它是一个随机算法,没有导数,不需要计算目标函数的导数(不像梯度下降,它依赖于偏导数)。它的计算效率很高,被用于各种数值优化库,如Optuna。在这里我们只简要介绍一下CMA-ES,有关更详细的解释,请参阅最后链接部分的文献。

考虑二维Rastrigin函数:

下面的热图显示了这个函数的值——颜色越亮意味着值越高。该函数在原点(0,0)处具有全局最小值,但它夹杂着许多局部极值。我们想通过CMA-ES找到全局最小值。

CMA-ES基于多元正态分布。它从这个分布中生成搜索空间中的测试点。但在开始之前,必须必须猜测分布的原始均值,以及它的标准差,但之后算法会迭代地修改所有这些参数,在搜索空间中扫描分布,寻找最佳的目标函数值。

Xi是算法在搜索空间中每一步产生的点的集合。Lambda是生成的点的个数。分布的平均值在每一步都会被更新,并且最终会收敛于真正的解。Sigma是分布的标准差——测试点的分布。C是协方差矩阵,它定义了分布的形状。根据C值的不同,分布可能呈“圆形”或更细长的椭圆形。对C的修改允许CMA-ES“潜入”搜索空间的某些区域,或避开其他区域。

上图中生成了6个点的种群,这是优化器为这个问题选择的默认种群大小。这是第一步。然后算法进行下面的步骤:

1、计算每个点的目标函数(Rastrigin)

2、更新均值、标准差和协方差矩阵,根据从目标函数中学到的信息,有效地创建一个新的多元正态分布

3、从新的分布中生成一组新的测试点

4、重复,直到满足某个准则(要么收敛于某个平均值,要么超过最大步数等)。

对于更新分布的均值是很简单的:计算每个测试点的目标函数后,给这些点分配权重,目标值越好的点权重越大,并从它们的位置计算加权和,这就成为新的均值。CMA-ES有效地将分布均值向目标值较好的点移动:

如果算法收敛于真解,那么分布的平均值也将收敛于该解。标准差会收敛于0。协方差矩阵将根据目标函数的位置改变分布的形状(圆形或椭圆形),扩展到有希望的区域,并避开不好的区域。

下面是一个显示了CMA-ES解决拉斯特里金问题时测试点的时间演变的GIF动画:

将CMA-ES用于特征选择

2D Rastrigin函数相对简单,因为它只有2个维度。但对于我们的特征选择问题,有N=213个维度。而且空间是不连续的。每个测试点是一个n维向量,其分量值为{0,1}。换句话说,每个测试点看起来像这样:[1,0,0,1,1,1,0,…]-一个二进制向量。

下面是使用cmaes库进行特征选择的CMA-ES代码的简单版本:

 def cma_objective(fs):
     features_use = ['const'] + [
         f for i, f in enumerate(features_select) if fs[i,] == 1
     ]
     lin_mod = sm.OLS(y_cmaes, X_cmaes[features_use], hasconst=True).fit()
     return lin_mod.bic
 
 
 X_cmaes = X.copy()
 y_cmaes = y.copy()
 features_select = [f for f in X_cmaes.columns if f != 'const']
 
 dim = len(features_select)
 bounds = np.tile([0, 1], (dim, 1))
 steps = np.ones(dim)
 optimizer = CMAwM(
     mean=np.full(dim, 0.5),
     sigma=1 / 6,
     bounds=bounds,
     steps=steps,
     n_max_resampling=10 * dim,
     seed=0,
 )
 
 max_gen = 100
 best_objective_cmaes_small = np.inf
 best_sol_raw_cmaes_small = None
 for gen in tqdm(range(max_gen)):
     solutions = []
     for _ in range(optimizer.population_size):
         x_for_eval, x_for_tell = optimizer.ask()
         value = cma_objective(x_for_eval)
         solutions.append((x_for_tell, value))
         if value < best_objective_cmaes_small:
             best_objective_cmaes_small = value
             best_sol_raw_cmaes_small = x_for_eval
     optimizer.tell(solutions)
 
 best_features_cmaes_small = [
     features_select[i]
     for i, val in enumerate(best_sol_raw_cmaes_small.tolist())
     if val == 1.0
 ]
 print(f'best objective: {best_objective_cmaes_small}')
 print(f'best features:  {best_features_cmaes_small}')

CMA-ES优化器定义了对平均值和标准差的一些初始猜测。然后它循环许多代,创建测试点x_for_eval,用目标评估,修改分布(mean, sigma, covariance matrix)等等。每个x_for_eval点都是一个二进制向量[1,1,1,0,0,1,…]]用于从数据集中选择特征。

这里使用的是CMAwM()优化器(带边距的CMA)而不是默认的CMA()。默认的优化器可以很好地处理规则的、连续的问题,但是这里的搜索空间是高维的,并且只允许两个离散值(0和1)。默认优化器会卡在这个空间中。CMAwM()稍微扩大了搜索空间(虽然它返回的解仍然是二进制向量),可以以解除阻塞。

下图显示了CMA-ES代码寻找最佳解决方案的运行记录。热图显示了每一代每个特征的流行/流行程度(越亮=越受欢迎)。你可以看到有些特征总是很受欢迎,而另一些特征很快就过时了,还有一些特征后来才被“发现”。给定该问题的参数,优化器选择的总体大小为20个点(个体),因此特征受欢迎程度是在这20个点上平均的。

运行结果如下:

 best objective:  33703.070530508514
 best generation: 921
 objective runs:  20000
 time to best:    46.912 sec

它能够找到比SFS更好(更小)的目标值,它调用目标函数的次数更少(20k),并且花费了大约相同的时间。

在研究了传统的优化算法(遗传算法、模拟退火等)之后,CMA-ES是一个非常好的解决方案,它几乎没有超参数,计算量很轻,它只需要少量的个体(点)来探索搜索空间,但它的性能却很好。如果需要解决优化问题,用它来测试对比是非常有帮助的。

遗传算法

最后我们再看看常用的遗传算法

遗传算法受到生物进化和自然选择的启发。在自然界中,生物(粗略地说)是根据它们所处环境中促进生存和繁殖成功的基因(特征)而被选择的。

对于特征选择,有N个特征并试图找到n个长度的二进制向量[1,0,0,1,1,1,…],选择特征(1 =特征选择,0 =特征拒绝),以最小化成本/目标函数。

每个这样的向量可以被认为是一个“个体”。每个向量分量(值0或值1)成为一个“基因”。通过应用进化和选择,有可能进化出一个个体群体,使其接近于我们感兴趣的目标函数的最佳值。

以下是GA的简要介绍。首先生成一群个体(向量),每个向量的长度为n。向量分量值(基因)是从{0,1}中随机选择的。在下面的图表中,N=12,总体规模为8。

在群体创建后,通过目标函数评估每个个体。

保留客观价值最好的个体,抛弃客观价值最差的个体。这里有许多可能的策略,从单纯的排名(这与直觉相反,并不是很有效)到随机对比选择(从长远来看,这是非常有效的)。还记得“explore-exploit ”困境吗?使用GA,我们很容易陷入这个问题,所以随机对比会比排名好好很多。

一旦最优秀的个体被选择出来,不太适合的个体被抛弃,就可以通过两种技术在基因库中引入变异了:交叉和突变。

交叉的工作原理与自然界完全一样,当两个生物交配并产生后代时:来自父母双方的遗传物质在后代中“混合”,具有一定程度的随机性。

突变也是自然中发生的事情,当遗传物质发生随机突变时,新的价值被引入基因库,增加了它的多样性。

然后个体再次通过目标函数进行评估,选择发生,然后交叉,突变等等。

如果目标函数在某些代数内停止改进,则循环可能会被中断。或者可以为评估的总代数设置一个值,或者运行时间等等,在停止后具有最佳客观价值的个人应该被认为是问题的“解决方案”。

我们可以将GA封装在Optuna中,让Optuna自己寻找最佳的超参数——但这在计算上是非常非常耗时的。

将遗传算法用于特征选择

为了演示,我们使用了非常强大的deep库,如果你不熟悉这个库可能看起来有点困难,我们将尽量保持代码的简单。

 # to maximize the objective
 # fitness_weights = 1.0
 # to minimize the objective
 fitness_weights = -1.0
 
 # copy the original dataframes into local copies, once
 X_ga = X.copy()
 y_ga = y.copy()
 
 # 'const' (the first column) is not an actual feature, do not include it
 X_features = X_ga.columns.to_list()[1:]
 
 try:
     del creator.FitnessMax
     del creator.Individual
 except Exception as e:
     pass
 
 creator.create("FitnessMax", base.Fitness, weights=(fitness_weights,))
 creator.create(
     "Individual", array.array, typecode='b', fitness=creator.FitnessMax
 )
 
 try:
     del toolbox
 except Exception as e:
     pass
 
 toolbox = base.Toolbox()
 # Attribute generator
 toolbox.register("attr_bool", random.randint, 0, 1)
 # Structure initializers
 toolbox.register(
     "individual",
     tools.initRepeat,
     creator.Individual,
     toolbox.attr_bool,
     len(X_features),
 )
 toolbox.register("population", tools.initRepeat, list, toolbox.individual)
 
 
 def evalOneMax(individual):
     # objective function
     # create True/False selector list for features
     # and add True at the start for 'const'
     cols_select = [True] + [i == 1 for i in list(individual)]
     # fit model using the features selected from the individual
     lin_mod = sm.OLS(y_ga, X_ga.loc[:, cols_select], hasconst=True).fit()
     return (lin_mod.bic,)
 
 
 toolbox.register("evaluate", evalOneMax)
 toolbox.register("mate", tools.cxTwoPoint)
 toolbox.register("mutate", tools.mutFlipBit, indpb=0.05)
 toolbox.register("select", tools.selTournament, tournsize=3)
 
 random.seed(0)
 pop = toolbox.population(n=300)
 hof = tools.HallOfFame(1)
 pop, log = algorithms.eaSimple(
     pop, toolbox, cxpb=0.5, mutpb=0.2, ngen=10, halloffame=hof, verbose=True
 )
 
 best_individual_ga_small = list(hof[0])
 best_features_ga_small = [
     X_features[i] for i, val in enumerate(best_individual_ga_small) if val == 1
 ]
 best_objective_ga_small = (
     sm.OLS(y_ga, X_ga[['const'] + best_features_ga_small], hasconst=True)
     .fit()
     .bic
 )
 print(f'best objective: {best_objective_ga_small}')
 print(f'best features:  {best_features_ga_small}')

代码创建了定义个体和整个种群的对象,以及用于评估(目标函数)、交叉、突变和选择的策略。它从300个个体的种群开始,然后调用eaSimple()(一个交叉、突变、选择的固定序列),我们这里只运行10代,其中绝对最好的个体被保留下来,以免在选择过程中意外突变或被跳过等。

这段简单的代码很容易理解,但效率不高,运行的结果如下:

 best objective:  33705.569572544795
 best generation: 787
 objective runs:  600525
 time to best:    157.604 sec

下面的热图显示了各代中每个特征的受欢迎程度(颜色越亮=越受欢迎)。可以看到有些特征总是很受欢迎,有些特征很快就被拒绝了,而另一些特征可能随着时间的推移变得更受欢迎或不那么受欢迎。

方法对比总结

我们尝试了三种不同的技术:SFS、CMA-ES和GA。

这些测试是在AMD Ryzen 7 5800X3D(8/16核)机器上进行的,运行Ubuntu 22.04和Python 3.11.7。SFS和GA是使用有16个线程来运行目标函数。CMA-ES是单进程的——在多线程中运行它似乎并没有提供显著的改进,这可能是算法没有支持多线程,下面是运行时间

 SFS:    44.850 sec
 GA:     157.604 sec
 CMA-ES: 46.912 sec

目标函数调用的次数:

 SFS:    22791
 GA:     600525
 CMA-ES: 20000

目标函数的最佳值-越少越好:

SFS:    33708.9860
GA:     33705.5696
CMA-ES: 33703.0705

CMA-ES找到了最佳目标函数。它的运行时间与SFS相当。它只调用目标函数20k次,是所有方法中调用次数最少的。别忘了,他还是单线程的

GA能够在目标函数上超过SFS,但它是最慢的。它调用目标函数的次数比其他方法多一个数量级。这是因为我们可以认为他是一个半随机的过程,因为遗传突变这个东西没有算法可解释。

SFS速度很快(可以在所有CPU内核上运行),但性能一般。但它是目前最简单的算法。

如果你只是想用一个简单的算法快速估计出最佳的特征集,那么SFS还不错。如果你想要绝对最好的客观价值,CMA-ES似乎是首选,并且它也不慢。

最后本文的代码:

https://avoid.overfit.cn/post/3bd329a18abe4ab4af36e6b7ceaef469

作者:Florin Andrei

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1385348.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CANFD数据记录仪在新能源汽车复杂路测下的应用

CANFD数据记录仪在新能源汽车复杂路测下的应用 汽车制造商在生产预批量阶段的耐久性测试中,为了检测潜在故障,必须让车辆在严酷的路况和环境下接受测试。为确保能回溯故障发生的现场情况,我们需要对测试数据精准记录与储存。这些数据是新车型优化迭代的关键,也是确保产品质量的…

【优选算法】滑动窗口 {何时使用滑动窗口?如何使用滑动窗口?如何确定更新结果的时机?滑动窗口是如何提高效率的?相关编程题解析}

一、经验总结 何时使用滑动窗口&#xff1f; 在使用暴力解法解题时&#xff0c;发现可以将其优化为同向双指针&#xff0c;既可以使用滑动窗口。 如何使用滑动窗口&#xff1f; 1. 定义窗口控制变量n&#xff0c;进窗口&#xff0c;判断&#xff0c;出窗口都需要操作窗口控制…

天软特色因子看板 (2024.01 第7期)

该因子看板跟踪天软特色因子A04001(当日趋势强度)&#xff0c;该因子为反映股价走势趋势强弱&#xff0c;用以反映股价走势趋势强弱&#xff0c;abs(值)越接近1&#xff0c;趋势 性越强&#xff0c;符号代表涨跌方向。 今日为该因子跟踪第7期&#xff0c;跟踪其在SW801050 (申万…

Open CV 图像处理基础:(六)在Java中使用 Open CV进行图片翻转和图片旋转

在Java中使用 Open CV进行图片翻转和图片旋转 目录 在Java中使用 Open CV进行图片翻转和图片旋转前言图片翻转函数代码示例其它翻转方向垂直翻转两轴翻转 图片旋转函数代码示例 Open CV 专栏导航 前言 在Java中使用OpenCV进行图片翻转和旋转是一种基本的图像处理技术&#xff0…

机器学习根据金标准标记数据-九五小庞

根据金标准标记数据是一种在机器学习和数据科学中常见的操作&#xff0c;主要用于评估分类模型的性能。其基本步骤如下&#xff1a; 收集数据&#xff1a;首先需要收集相关领域的原始数据&#xff0c;这些数据通常来自不同的来源和渠道。数据清洗和预处理&#xff1a;在这一步…

常见的限流算法

本文已收录至我的个人网站&#xff1a;程序员波特&#xff0c;主要记录Java相关技术系列教程&#xff0c;共享电子书、Java学习路线、视频教程、简历模板和面试题等学习资源&#xff0c;让想要学习的你&#xff0c;不再迷茫。 天下武学出同源 正所谓天下武学殊途同归&#xff…

怎么批量重命名图片?分享3个高效方法!

怎么批量重命名图片&#xff1f;在日常生活中&#xff0c;将图片批量重命名是一项非常实用的操作。有时候我们拍摄或收集了很多图片&#xff0c;需要对其进行整理和归类。通过批量重命名&#xff0c;我们可以快速为图片添加序号、日期或其他标识&#xff0c;使其更有条理。此外…

new mars3d.layer.GeoJsonLayer({实现图标点billboard贴模型聚合效果

说明&#xff1a; 1.【mars3d】的依赖库cesium本身是不支持贴地/贴模型操作的 2.sdk内部异步计算了数据的贴地/高度值之后&#xff0c;更新到图层上实现贴地/贴模型效果的 3.相关的示例链接&#xff1a; 1.功能示例(Vue版) | Mars3D三维可视化平台 | 火星科技 4.相关的计算…

Java基础 - 黑马

我是南城余&#xff01;阿里云开发者平台专家博士证书获得者&#xff01; 欢迎关注我的博客&#xff01;一同成长&#xff01; 一名从事运维开发的worker&#xff0c;记录分享学习。 专注于AI&#xff0c;运维开发&#xff0c;windows Linux 系统领域的分享&#xff01; 知…

构建搜索引擎,而不是向量数据库

英文原文地址&#xff1a;Build a search engine, not a vector DB 构建搜索引擎&#xff0c;而不是矢量数据库 2023 年 12 月 19 日 在过去12个月里&#xff0c;向量数据库初创公司数量激增。我并不是来讨论其中任何一个的具体设计权衡。相反&#xff0c;我想回顾一下向量数…

斯坦福CS231n学习笔记:DL与CV教程 (1) | 引言与知识基础

前言 &#x1f4da; 笔记专栏&#xff1a;斯坦福CS231N&#xff1a;面向视觉识别的卷积神经网络&#xff08;23&#xff09;&#x1f517; 课程链接&#xff1a;https://www.bilibili.com/video/BV1xV411R7i5&#x1f4bb; CS231n: 深度学习计算机视觉&#xff08;2017&#xf…

设计3题目:各种排序算法及性能分析

1、设计3目的 掌握各种内排序算法设计及其执行绝对时间&#xff0c;并对其时间性能进行比较。 2、设计3正文 2.1 实验内容 内容&#xff1a;编写一个程序&#xff0c;随机产生n个1-99的正整数序列&#xff0c;分别采用直接插入排序、折半插入排序、希尔排序、冒泡排序、快速…

什么是MongoDB

概念&#xff1a; MongoDB 是一个文档数据库&#xff08;以 JSON 为数据模型&#xff09;&#xff0c;由 C 语言编写&#xff0c;旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。 MongoDB 是一个介于关系数据库和非关系数据库之间的产品&#xff0c;是非关系数据库当中…

白光LED驱动芯片的典型应用电路

小型白光LED驱动器LM2751 LM2751是美国国家半导体&#xff08;NS&#xff09;公司推出的一款小型白光LED驱动器&#xff0c;采用10PINLLP无铅封装&#xff0c;内置固定频率的电荷泵&#xff0c;在输入电压为2.8V&#xff5e;5.5V的情况下&#xff0c;可稳压输出4.5V或5.0V电压…

【树莓派】网线远程连接电脑和树莓派,实现SSH连接

目录 1、硬件连接&#xff1b; 2、电脑端&#xff1a; 3、查找树莓派的IP地址 4、开启树莓派的SSH接口 5、putty 6、命令行 参考文章 通过网线连接笔记本与树莓派 开启SSH和VNC功能 无显示器安装树莓派 实现&#xff1a;打开putty输入树莓派地址使用ssh方式登陆&…

【XR806开发板试用】单总线协议驱动DHT11温湿度传感器

1.昨天刚收到极速社区寄来的全志XR806开发板&#xff0c;之前用过很多全志的SOC芯片&#xff0c;但是像这种无线芯片还是第一次用。这次打算使用XR806芯片驱动一下DHT11温湿度传感器。 2.代码如下&#xff1a; #include "common/framework/platform_init.h" #inclu…

【计算机网络】TCP原理 | 可靠性机制分析(四)

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【网络编程】 本专栏旨在分享学习计算机网络的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 这里写目录标题 &#x1…

软信天成:数据安全管理解决方案分享

近年来&#xff0c;随着数据环境日趋复杂多变和潜在的数据隐私泄露风险潜伏&#xff0c;如何确保企业数据安全已成为众多企业亟待面对与妥善处理的重要问题。 为了应对这一严峻的现实挑战&#xff0c;软信天成凭借专业的知识体系和丰富的实战经验积累&#xff0c;总结出了一套…

【web服务搭建实验】之nginx基础学习

目录 一、nginx的简介二、nginx安装实验虚拟主机的配置web服务器的主流实现方式-LAMP和LNMP 一、nginx的简介 Nginx是一款轻量级HTTP服务器&#xff0c;同时也是代理邮箱服务器&#xff0c;具备反向代理&#xff0c;通用代理的功能。支持多个系统&#xff0c;和不同操作系统。…

如何注释 PDF?注释PDF文件方法详情介绍

大多数使用 PDF 文档的用户都熟悉处理这种格式的文件时出现的困难。有些人仍然认为注释 PDF 的唯一方法是打印文档&#xff0c;使用笔或荧光笔然后扫描回来。 您可能需要向 PDF 添加注释、添加注释、覆盖一些文本或几何对象。经理、部门负责人在编辑公司内的合同、订单、发票或…