上一节笔记传送门:

学弱猹:数值优化|笔记整理(2)——线搜索:步长选取条件的收敛性​zhuanlan.zhihu.com

————————————————————————————————————

大家好!

我们在上一节花了很多篇幅介绍了线搜索中,步长选取条件的收敛性。那么在这一节,我们会开始关注线搜索中,如何实操中设计不同步长选取条件算法,并且还会关注线搜索中初始步长的选取。当然了,这些部分没有太多的理论性,因此不会占据太长的时间,所以我们可能还会介绍一下优化中的共轭梯度法

那么我们开始吧。

目录

  • 回溯法多项式插值
  • Strong-Wolfe条件下的多项式插值
  • 初始步长选取——BB步长
  • 非单调下降迭代法简介
  • 共轭梯度法
    • 线性情形

Source

  • J. Nocedal, S. J. Wright, Numerical Optimization
  • J. Barzilai and J. M. Borwein, Two-Point Step Size Gradient Methods
  • 课堂笔记,教授主页:https://www.math.fsu.edu/~whuang2/

回溯法多项式插值

我们在上一节主要介绍了线搜索算法的收敛性,同时我们给出了最速下降法(也叫梯度下降法)的一个算法模板。那么为什么这一节还要在这上面花篇幅呢?事实上我们把我们之前的话反过来说,就是优化具有很强的理论性,但是同样具有很强的实用性。也就是说,如果我们没有考虑到实际应用中所面对的问题,那么一样不能够算学好了优化。因此仅仅通过回溯法(backtracking)去寻找步长自然是不够的,我们还有一些其他的方法可以用来帮助我们选取步长。事实上我们只需要知道这些方法在实操中具有很好的效果就足够了,因为他们的理论比较超纲,掌握并没有太大的必要(事实上在优化中,这样的情况非常常见)。也因此在这一部分,虽然我们的内容量不如之前的理论那么多,也不会有理论部分那么详细,但是其最重要的部分在于实操,也因此在这一块,寻找数值例子和编程才是最重要的。

好的,我们回到我们的正题,也就是插值法。为什么使用插值法呢?这是使用了一个插值逼近的思想。假如说我们有一些点,那么通过这一些点就可以做插值,得到一条曲线。这一条曲线就可以算是插值函数。如果我们只有点本身的信息(比方说

),那么这就是拉格朗日插值,如果还会利用上这个点导数的信息,那么这就是埃尔米特插值(注意Hermite的开头字母H

是不发音的)插值函数可以用来逼近我们的实际的函数曲线。

现在我们考虑我们构造的函数

,其中
为步长,
为当前迭代点,
为搜索方向。并且我们有了一个初始步长
(如果是我们的上一节所提到的A-G条件对应的步长,这个初始步长就是1)。那么这样的话实际上我们有了3个信息:

这是一个带有函数导数的值,所以通过埃尔米特插值,我们可以得到一个二次函数。我们也会用这个二次函数来近似我们的

上的行为。

有了插值函数之后,我们希望取插值函数所对应的最小值点作为我们的步长。这是因为我们的思想就是用插值函数来代替我们的原始的函数

,那么插值函数对应的最小值点,我们也

认为

的最小值点(当然一般来说,实际情况不是这样)。换句话说,我们的这个步长,可以使得我们的函数值

局部上下降的最多,那我们自然就认为它容易满足步长选取条件。这就一定程度上降低了我们选取步长的困难。

下面这一张图直观地展示了我们的想法

当然这并没有结束,我们只是找到了一个合适的值,但是还要做一些处理。比方说如果我们是以回溯法作为步长选取条件,那么一般来说我们的下一个选取的步长是

(理解这里的=为计算机编程里的赋值),这里的

就是对应的选取步长的上下界,不清楚的可以参考第一节。

当然,我们不会那么好运,即使这样,我们的步长也有可能不会被接收。但是没有关系,因为这个时候,我们就又多了一个信息

,也就是说我们可以用来插值的点变成了4个,即

这样的话通过埃尔米特插值,我们就可以得到一个三次函数,这个三次函数我们也可以取最小值点,作为我们的步长

当然了,即使是使用三次函数,你也有可能失败。那么难道我们要开始使用四次函数了吗?其实不必,因为四次函数的最小值点无法再解析表示了。如果要数值上求解,其复杂度就远远超出了我们这里的要求。所以我们还是使用三次函数的插值,也就是说,我们**抛弃最开始的点

。**只考虑这四个点

然后通过三次插值,寻找最小值点,得到我们的步长

,以此类推即可。

我们坚持使用三次函数的原因就是我们可以直接通过表达式找到我们的最小值点。具体的可以看下面这张图,这张图的标记会和我们前面的说法不太一样,大家注意区分。

Strong-Wolfe条件下的多项式插值

我们在之前有讨论过在A-G条件下的多项式插值。那么对于Strong-Wolfe条件,因为多了一个不等式(在这里我们为了方便,记它为curvature条件),所以对应的算法就变得复杂了很多。但是如果我们能够说清楚Strong-Wolfe条件的算法,那么对于Weak-Wolfe条件自然也就不在话下。

和之前我们插值寻找点不同,这里我们的思路是先寻找区间。具体来说就是

1. 找到存在步长的区间

2. 缩短区间

这里我们逐一介绍各种情况以理清算法的思路。

Case 1:
选取的步长

不满足Armijo条件。

图示如下

这个时候,点一定是在

的交点右侧,所以我们要缩短区间,就是设置
即可。

Case 2:
选取的步长

满足Armijo条件,但是不满足Strong-Wolfe条件,且
在右侧。

图示如下

这个时候对应的条件就是

,同样的根据图片你也能看出来,也是设置

Case 3:
选取的步长

满足Armijo条件,但是不满足Strong-Wolfe条件,且
在左侧。

图示如下

这个时候对应的条件就是

,同样的根据图片你也能看出来,也是设置

Case 4:

已选取之后,新的步长仍然不满足Strong-Wolfe条件,且
在右侧。

图示如下

这个条件其实是为了加速使用的,很明显这个时候,步长也是太大了,设置

即可。

到此我们再给出算法的具体信息其实就会比较好理解了。也就是说我们有了缩短区间的这些想法之后,剩下的也就是插值法了。完整算法如下

读者可以有空在软件上自己实现这一个算法(数值领域最有名的当然还是Matlab,当然Python的数值计算包也可以,Fortran太古老了……),毕竟优化如果只关注理论而不进行实操,那最多也只算学会了一半

初始步长选取——BB步长

最后我们来提一下线搜索中的初始步长选取策略。

我们在回溯法中有说过,我们会先选取一个初始的步长为1,然后每一次都缩小一些,直到满足Armijo条件即可。但是有个问题是,这个“初始的步长”是不是一定要为1?答案显然是否定的。也就是说,我们可以考虑一些其他的方法,使得步长一开始就是一个我们给定的算法得到的值。如果我们的初始步长取得很好,那么即使是在最简单的回溯法中,也可以大幅度减少我们步长选取的迭代步数,这自然就会加快我们的收敛速度。

这一部分我们不会给出理论证明,而只是在数值上提供一些视角。

我们主要说的就是Barzilai-Borwein步长,也叫BB步长,这个步长有两种选取方法,分别为

,

一般来说,我们称BB2这个步长为短步长,而BB1这个步长为长步长。这是因为根据Cauchy不等式,容易得到

因此可以发现

仅仅是长步或者短步那么简单吗?答案也不完全是,事实上,容易证明的是下面这个性质。

Proposition 1:

这里我们给出一个二次凸问题情况下的证明作为理解即可,更加一般的版本可以参考一下原始论文。

如果是二次凸问题,那么对应的优化问题就是

,对应的
,其中
是对称正定矩阵,因此我们可以直接考虑我们要关心的函数,然后把它写成

因为函数是一个关于

的二次函数,所以直接通过二次函数的极小值点公式就可以得到我们的表达式

注意到因为我们有

,而上下又是其次的,所以将
换成
也是没有问题的(想想为什么),这就证明了结论。

我们可以看出来,对于BB长步长,它的本质就是寻找当前迭代的上一步的精确步长,也就是在当前方向下所能够达到的最小值对应的步长。

如果是BB短步长,那么也可以得到一个类似的结果。

Proposition 2:

这又是为什么呢?这是因为,如果我们写开我们的表达式,会得到

因为结构上的惊奇的相似性,我们可以得到

可以这么写的理由和上面是相同的。

回头再看,对于BB短步长,它的本质就是寻找当前迭代上一步在目前搜索方向上的梯度最小值。从直觉上来看,这样子会使得步长更容易被接收,毕竟这也是我们的优化目标嘛。

事实上,BB步长不一定是正数,不过如果配合Wolfe条件,这个问题就不存在了。我们当成一个性质来说明。

Proposition 3:
在Wolfe条件下,BB长短步长恒正。

我们观察一下BB步长,

为正自然不必多说。所以关键就是看
是否为正。

注意到,Wolfe条件多了一个不等式为

那么这样的话,简单计算即可得到

这是因为

是下降方向,同时
。注意到左边其实就是
,所以我们也就得到结论了。

最后我们简单提一下它的使用。虽然BB步长经验性很好,但是很明显,如果是迭代的第一步,那肯定是不能使用的。同时BB步长是否可以被很快接收,也会影响到我们的算法效率,因此我们还会加一些其他的保障。有一个之前提到的就是插值的保障

如果我们考虑让步长选取为

这三个值所得到的二次函数,这个时候可以得到步长为

左边是我们要的步长,右边就是我们可以得到的最小值点的表达式。解这个等式就可以得到

这个可以作为我们的其中一个保障值。也就是说我们的BB步长并不是每一次都直接使用的,而是要做这样的一步处理。

这里的

就是我们的保障值,可以是上面那个,也可以是1。这里的
就是对应的

保障参数。一般来说

会取得很小,
会取得很大,基本的思路就是要我们的步长有一个

硬性的上下界,确保我们的优化可以进行下去。

事实上包括我们之前所说的BB步长是否为正的问题,有了这个硬性的上下界之后,配合A-G条件使用也不会有太大的问题。

非单调下降迭代法简介

对于我们之前的线搜索方法而言,我们的每一步都在寻找下降方向,但是我们归根到底,还是希望我们的每一步都能够有一个充分的下降。但是如果我们去掉这个条件,能不能够得到一个好的优化算法呢?答案其实依然是可以的。它的算法非常简单,只是修改了我们的Armijo条件中的一小部分,具体的算法图如下

你可以看到,我们这里使用了回溯法,一个最关键的地方就在于条件

这个条件就告诉了我们,我们每一次不再参考上一次的步长值,而参考之前所有步步长的最大值。这个情况下,理论上有证明依然可以收敛,但是它不再会产生一个单调下降的序列。

事实上,非单调下降迭代法具有更高的计算精度。要解释这个,我们首先要来观察一下正常的线搜索方法的精度问题。

注意到无论是A-G条件,还是Wolfe条件,我们都会遇到Armijo条件。如果说步长为

的话,那么一定有不等式

这个时候你可以发现,如果说

的值接近了机器精度(换句话说,在计算机内计算到时候,它的数值为
这样的级别),那么这个时候,
的差距就微乎其微了,也就是说不等式的计算就会有

巨大的数值误差。而在梯度下降法中,因为我们有

,因此我们实际上可以发现,虽然我们要求最终的
,但是如果按照A-G条件去走,最后只能计算到

对于Matlab来说,

,也就是说计算到
的时候,就应该停止了。这样的精度我们是不太满意的,毕竟
还是有很大的差距的。但是注意到如果是非单调下降方法,右边的
就会变成“迭代前几步中的最大值”,因此数值误差不会太容易出现(因为相邻两步的差距微乎其微,不代表中间相隔了很多步的差距依然是微乎其微)。

下面是一个非单调下降法的迭代实例图

你也能看出来,虽然我们没有每一步都下降,但是依然可以保证最后能够使得我们的梯度下降到我们想要的值。

到此,非常一般的线搜索方法我们算是介绍完毕了。值得强调的是,线搜索方法既可以是一个具体的方法,也可以是一个框架。我们在之后学习具体的算法的时候,也会回来使用线搜索的框架来进行应用。下面这两张图给出了几个常见算法的搭配在二次凸问题上的比较,从此也可以看出每个算法的不同的特点。

第一张图可以看出来,如果我们采用BB步长,可以大幅度的减少我们的迭代次数。

第二张图可以看出来非单调线搜索方法可以很好地使得算法获得更高的精度。这里因为我们的

取得非常的大,所以可以看到回溯法的曲线保持的非常的平稳,也可以看到它确实在近似
的地方就无法再下降了。 当然了,对于实际的问题,不同的方法可能会有不同的效果。

共轭梯度法

下面我们来关注优化中的另外一个重要的算法:共轭梯度法(Conjugate Gradient)。虽然名字叫共轭梯度法,但是实际上这个算法完全没有引入“共轭梯度”这样的概念……

与之前所说的线搜索不同,共轭梯度法的关键在于修改方向。考虑一下之前所说的最速下降法,如果我们的初始点取得很不好,或者函数的性质不好,就很容易出现迭代步数过多的问题,下面的这张图就展现了这种情况。

这里的每一条线都是切线,换句话说我们每一步都是精确步长。但是你可以看到实际上的迭代效果并不显著,曲线也显得歪歪扭扭。

但是如果我们使用了共轭梯度法,能够使得我们的迭代步长经过选取之后能够大大降低我们的迭代步数,那自然就可以很好的解决我们上面的问题。为了解释这个方法,我们会先考虑线性情况,再将这个思想推广到非线性的情形。

线性情形

线性情形的含义就是考察一个线性系统的优化问题,比方说二次凸问题的一般形式

,也就是说我们考虑一个
维的欧氏空间。如果求梯度,并令梯度为0的话,就对应
(如果忘记了求梯度的方法就得回到第一节去看看技巧了)。

那么在这里,我们的共轭梯度法,其核心,就是希望能够取一系列的共轭方向

,使得下面的条件满足

(我们一般称为共轭性

然后我们重复

次,每一次按照下面的规则进行迭代
,

这里的

取得是精确步长,具体精确步长的定义可以参考上面BB步长介绍的部分。这个方法也被称为

共轭方向法

这个方法为什么奏效呢?是因为下面这个定理。

Theorem 1:
给定任意的初始步长

,那么
最多
步就会收敛到解

我们证明一下这个结论。首先要观察到,如果我们完整走完这个迭代,那么在第

步,我们就会得到

也就是说我们可以得到一个

的线性表出为
。因为我们的所有的共轭方向相互之间是线性无关的(这是一个非常简单的高等代数习题,我们留给读者思考),所以我们有一个思路是考虑证明它与

坐标在这个基

下是相同的

现在我们假设

,那么利用上共轭性,左乘
,就可以得到

这个结论得到之后,我们联想到

,而经过简单的运算,又可以得到

所以代入我们可以写成

所以我们只需要说明,在

的情况下,我们有
即可,而这个只需要注意到式子

(只是把前面的式子下标改成了

)再左乘
即可,也就完成了证明。这里还是要利用共轭性,读者写一下很容易就能看出来。

你可以看出来,共轭梯度法的核心就是希望选取一系列的共轭方向,使得共轭性得到满足。而这个算法是通过之前一步的搜索方向,来得到当前的搜索方向,使得共轭性能够得到满足。简单来说就是

,

也就是说,除了第一步的搜索方向仍然是负梯度方向以外,之后的方向就会根据那个公式来进行更新,这里的

,而
的选取就是希望共轭性能够满足,具体的公式会放到之后一起说。

如果要说明这个算法奏效,那么下面这个定理自然是跑不掉的。

Theorem 2:
设线性共轭梯度法的第

步迭代的结果
不是解,那么有以下结论成立

(1)

(2)

(3)

(4)

要说明这个大定理需要很多步骤,但是每一步都容易想到。我们一个一个来看。

对于第一个,首先注意到

,所以对于
,我们有
,左右乘
再减去
也就自然容易推出

有了这个式子就自然可以得到

的情形。

而对于一般的情形,我们只需要采取相同的策略,考虑

,这样的话就可以得到

有了这个递推式,事实上可以看到,

所属的线性空间,是由
来决定的,所以我们再来看看
的情况。

时,我们有
,因此可以得到的是

所以你可以发现,如果我们讨论

,那么容易得到它所属的线性空间是由
决定的,也就是说

然后再看

的表达式可以得到它是由
决定的,所以可以得到

所以实际上我们相当于通过递推得到了一个相互关联的证明链。也就可以通过归纳原理直接得到前两个结论的证明。换句话说我们虽然刚开始只想证明第一个,但可以通过观察法和分析法直接得到两个小结论的证明。

下面来考虑一下第三个性质。事实上这个性质肯定也是要考虑到我们的步长是精确步长。也就是说它满足

把它化简开就可以得到

注意到在线性问题中,

,因此我们可以得到
,而又因为我们有

(注意

之间的正交性)就可以得到,假如说有
,那么一定有
,结合上
即可得到结论成立。

接下来我们就只差最后一个性质了。注意我们之前说过,

的设立就是为了使得
成立。所以我们采用同样的套路,如果我们有
成立,那么只需要再证明
成立即可。

利用迭代的更新公式,我们可以得到

要说明这一项为0,只需要考虑

所在的线性空间,再观察
是否与这个空间正交即可。而注意到
所属的线性空间是由
所决定的,那么写开就不难得到

注意我们的前两个性质即可。而这个等价性只需要再结合第三个性质,我们就证明好了这个结论。

我们在整个证明中事实上用到了相当多的高代中线性空间的这一套语言。下面这个性质我们也可以通过这一套语言来给出一个大致的证明。

Proposition 4:

是函数
这个空间上的最小值。

我们注意到,

是一组正交基,用线性空间的语言来说,

它们每一维的信息都相互独立。因此我们如果在一个维度上使得函数的表现最优化了,就可以不用再管这个维度,而是再考虑剩下的维度。而线性共轭梯度法也正是这么做的(它每一步都取了精确步长),所以直观上很好理解这个结论。当然了,严格的证明就要严格的走线性表示的写法,通过求偏导的方式来解决。

这个性质事实上就是说,如果我们考虑一个

维的线性问题,那么不难得到这个迭代

步即可收敛

好了,到此我们已经算是将共轭梯度法的核心思想介绍完毕了。但是共轭梯度法的完整算法还没有给出,我们会在下一节再继续说。

小结

本节我们关注了线搜索方法中步长选取的插值法与初始步长选取的重要方法。线搜索方法最关心的就是步长的选取,而这一节所提到的思路和方法,也是优化中用的最多,最经典的一些方法。除此之外,我们还给共轭梯度法开了一个头,介绍了一下线性共轭梯度法。事实上线性共轭梯度法的良好性质,引导了我们之后去介绍非线性共轭梯度法,不过篇幅有限,我们就只能下一节继续说了。

因为毕业论文答辩时间临近,近期这个系列可能会更新的很慢很慢,也请大家谅解。待毕业论文完成之后再回来写这一部分的笔记。

——————————————————————————————————————

本专栏为我的个人专栏,也是我学习笔记的主要生产地。任何笔记都具有著作权,不可随意转载和剽窃

个人微信公众号:cha-diary,你可以通过它来获得最新文章更新的通知。

《一个大学生的日常笔记》专栏目录:笔记专栏|目录

《GetDataWet》专栏目录:GetDataWet|目录

想要更多方面的知识分享吗?可以关注专栏:一个大学生的日常笔记。你既可以在那里找到通俗易懂的数学,也可以找到一些杂谈和闲聊。也可以关注专栏:GetDataWet,看看在大数据的世界中,一个人的心路历程。我鼓励和我相似的同志们投稿于此,增加专栏的多元性,让更多相似的求知者受益~

回溯法采用的搜索策略_数值优化|笔记整理(3)——线搜索中的步长选取方法,线性共轭梯度法...相关推荐

  1. el-select 多选取值_数值优化|笔记整理(3)——线搜索中的步长选取方法,线性共轭梯度法...

    上一节笔记传送门: 学弱猹:数值优化|笔记整理(2)--线搜索:步长选取条件的收敛性​zhuanlan.zhihu.com ------------------------------------ 大 ...

  2. 回溯法采用的搜索策略_强化学习基础篇(三十四)基于模拟的搜索算法

    强化学习基础篇(三十四)基于模拟的搜索算法 上一篇Dyna算法是基于真实经验数据和模拟经验数据来解决马尔科夫决策过程的问题.本篇将结合前向搜索和采样法,构建更加高效的搜索规划算法,即基于模拟的搜索算法 ...

  3. 回溯法采用的搜索策略_下列那种函数是回溯法中为避免无效搜索采取的策略( )_学小易找答案...

    [填空题]图示刚架,单元编号.结点编号和结点位移编号如图所示,则单元 3的单元定位向量为 _________ .提示:写成 [ , , , , ,]T的形式 [单选题]下列测量仪器中,最适宜用于多点水 ...

  4. 回溯法采用的搜索策略_五大常用算法之四:回溯法

    1.概念 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就"回溯"返回,尝试别的路径.回溯法是一种选优搜索法,按选优条件向 ...

  5. 回溯法采用的搜索策略_急性阑尾炎最典型的症状为:

    票的的持定价按一它授先购买股格在内优权利予它有者一定时期,急性普通的一认股股东种特权是权优先. 阑尾物屠岸个人中杜撰出迁在记·晋世家>贾这司马<史. 比较悲剧文化地反中国集中精神映了,炎最 ...

  6. 回溯法采用的搜索策略_17图的搜索算法之回溯法

    回 溯 法 回溯算法实际是一个类似枚举的搜索尝试方法,它的主题思想是在搜索尝试中找问题的解,当不满足求解条件就"回溯"返回,尝试别的路径.回溯算法是尝试搜索算法中最为基本的一种算法 ...

  7. l bfgs算法java代码_数值优化:理解L-BFGS算法

    译自<Numerical Optimization: Understanding L-BFGS>,本来只想作为学习CRF的补充材料,读完后发现收获很多,把许多以前零散的知识点都串起来了.对 ...

  8. 回溯法遵循深度优先吗_闲来刷下「回溯算法」

    定义 ❝ 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就"回溯"返回,尝试别的路径.回溯法是一种选优搜索法,按选优条件向 ...

  9. 回溯法遵循深度优先吗_回溯算法(一)

    一.什么是回溯算法 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就"回溯"返回,尝试别的路径.许多复杂的,规模较大的问题 ...

最新文章

  1. RESTful之视图集ViewSet
  2. mysql 倒序 分页_nodeJS与MySQL实现分页数据以及倒序数据
  3. 如何访问 SAP Screen Personas 培训系统以及完成一个最简单的例子
  4. mysql数据库的分离_数据库分离和附加 (SQL Server)
  5. 计算机应用综合实践实验心得,综合实践活动培训心得体会范文(精选5篇)
  6. 【爬虫剑谱】二卷4章 实战篇-模拟登录铁路12306网站(滑块验证)
  7. vray5.1 for sketchup 安装教程
  8. visio2013画图时两条直线交叉, 如何让它不弯曲
  9. Spring源码解析之AOP篇(三)----Spring开启AOP的两种方式
  10. 新西兰计算机科学硕士哪所大学最好,2020年新西兰哪些大学计算机科学专业比较好及其优势介绍...
  11. Linux安装必须建立的三个分区
  12. windows 环境下,编译android 版opencv-4.5.5,并添加opencv_contrib-4.5.5 扩展模块
  13. Eclipse中设置作者日期等Java注释模板
  14. java文件打包成jar文件_将java文件打包成jar包步骤
  15. windows embed sapi php,嵌入式SAPI - PHP 扩展开发及内核应用相关内容 - UDN开源文档
  16. Furucombo被盗1400万美元启示录:切勿过度授权
  17. C++基础之关键字——virtual详解
  18. Go语言段子爬虫--捧腹网
  19. 材料力学Ⅰ(第六版)第五章课后习题答案
  20. 2023年中南财经政法大学应用统计考研上岸前辈初复试备考经验

热门文章

  1. iOS 摇一摇功能的实现
  2. Objective-C中的@Property详解
  3. Java--Socket客户端,服务端通信
  4. python程序实例讲解_Python编程之属性和方法实例详解
  5. 无法重新声明块范围变量。此处也声明了 。_Go 语句块与作用域
  6. mysql binary 查询_MYSQL的binary解决mysql数据大小写敏感问题 《转载》
  7. bytes数组转string指定编码_好程序员Java学习路线分享Java基础之string
  8. c 获得java数据,获得jar中数据,获得jar数据,// example c
  9. 配色没有灵感?最流行的配色案例!没有一个人不爱的
  10. UI登陆页面素材|让设计师在竞争中脱颖而出