《深度学习》读书系列分享第四章:数值计算 | 分享总结
「Deep Learning」这本书是机器学习领域的重磅书籍,三位作者分别是机器学习界名人、GAN的提出者、谷歌大脑研究科学家 Ian Goodfellow,神经网络领域创始三位创始人之一的蒙特利尔大学教授 Yoshua Bengio(也是 Ian Goodfellow的老师)、同在蒙特利尔大学的神经网络与数据挖掘教授 Aaron Courville。只看作者阵容就知道这本书肯定能够从深度学习的基础知识和原理一直讲到最新的方法,而且在技术的应用方面也有许多具体介绍。这本书面向的对象也不仅是学习相关专业的高校学生,还能够为研究人员和业界的技术人员提供稳妥的指导意见、提供解决问题的新鲜思路。
面对着这样一本内容精彩的好书,不管你有没有入手开始阅读,雷锋网 AI 研习社都希望借此给大家提供一个共同讨论、共同提高的机会。如果大家有看过之前的分享的话,现在除了王奇文之外,我们还继续邀请到了多位机器学习方面优秀、热心的分享人参与这本书的系列分享。这期邀请到的是陈安宁与大家一起分享他对这本书第四章的读书感受。
分享人:陈安宁,Jakie,名古屋大学计算力学博士。
「Deep learning」读书分享(四) —— 第四章 数值计算
大家好,我叫陈安宁,目前在名古屋大学攻读计算力学博士。今天我要和大家分享的是「Deep Learning」这本书的第四章节,Numerical Calculation,即“数值计算”。
其实大家如果翻过这本书的话,可以看出第四章是整本书所有章节里面篇幅最少的一章。为什么,是因为其实我们大部分人在运用机器学习或者深度学习的时候是不需要考虑这一章的内容的,这章的内容更多是针对算法的数学分析,包括误差的增长以及系统的稳定性。
今天分享的主要轮廓包括以下四个点,
第一,在机器学习、包括了深度学习中数值计算的应用。
第二,数值误差的问题
第三,简单的分析机器学习系统的稳定性问题
最后,针对优化问题给出了两种不同的优化算法,一种是梯度下降法,一种是限制优化算法。
我们首先来看一下机器学习中的数值计算问题。所谓的机器学习或者深度学习,其实最终的目标大部分都是极值优化问题,或者是求解线性方程组的问题。这两个问题无论哪个,我们现在的求解办法基本上都是基于计算机的反复迭代更新来求解。因为目前肯定是没有解析解的,大家都是通过离散数学来求解这两个问题。
既然这个过程有迭代或者大量的重复计算,那么肯定会牵扯到数据的累积。数据累积就极有可能会有误差的产生。误差如果过于大或者过于小,在某些特定的情况下都会对系统产生非常致命的影响。
数值误差的产生原因和避免方法
首先我们来看数值误差。所谓的数值误差是指由于计算机系统本身的一些特性产生的误差,比如说我们知道,无论你使用任何编程语言,它里面都有很多的数据类型,包括单精度、双精度、整形、长整型。那么每一种数据当你定义以后,它在计算机的内存里面都是有对应的数值范围和精度范围的。如果在反复的迭代计算过程中,你产生的数据超过了数据类型定义的范围,计算机会自动的进行取舍。
这时就会产生一个问题,因为取舍就导致了和真实值之间的变化,这个变化就极有可能产生很大的麻烦,如果一个很小的数出现在了分母上,那么计算机在计算过程中就会得到一个非常大的数,如果这个非常大的数超过了你所定义的数据类型的范围,计算机就会出现错误。
我们可以简单看一下PPT中这个函数,它叫softmax函数,softmax函数经常会在概率里面用到。它有很多特性,它的所有元素的softmax之和是等于1的;然后如果所有元素Xi也是相等的话,那么softmax的每一个元素也是相等的,等于所有n个元素合的1/n。
我们考虑一些比较特殊的情况,比如X是一个非常小的一个量,在指数函数中当这个X非常小的时候,这个指数函数也是非常小,无限趋于零的。无限趋于零的话,假如有限个值相加,比如n=10的话,十个数以后这个分母也是一个非常小的值;如果特别小,比如10-10,这个在计算机里面一算的话,softmax就会产生一个很大的数,经过多次累积的话,产生的这个大数极有可能超过你的所定义的数据范围,这个时候你的程序就会报错。所以我们在计算的时候要避免分母上出现一个极小的数的情况。
同理,分子 xi 如果是一个非常大的数字的话,它的指数也是趋向于无穷的,整个的softmax也是一个非常大的数。这也就是说,分子过大或者是分母过小,都是我们应该在计算过程中极力避免的事情。
举一个实际应用的例子,为什么会有这种过小或过大的情况产生。比如说有一条线,我们要计算某一个点到这个线的距离,这个距离d之后会出现在分母上。对于这样一个式子,如果这个点我们取得离线过于近的话,这个距离就非常之小,这在实际应用中是经常出现的。这种情况下softmax这个函数就极容易出现问题。
那么有人会问了,怎么样去避免这个问题呢?当然有很多方法,可以的最简单的办法就是定义一个max函数,里面带有一个常数比如10-4;如果这个距离D很小的话,我们就取这个10-4,限定了d的最小的值就是10-4。
当然这是一个朴素简单的想法,在实际应用当中,我们可以使用很多其他的方法,比如可以再取一个指数,那么如果这个值非常小的话,它的整个值就会是趋向于1的,实际上也是一个解决问题的办法。
这两个问题,一个叫做分母趋近于0,或者是分子趋近于无穷大,一个叫underflow,下溢,就是指分母过于小;一个是overflow,是指分子过于大,趋近于无穷。这两个问题都是由于计算机的数据有一个有限的范围而产生的,并不是我们的算法本身系统产生的。这是Numerical error其中的一种,我们可以把它理解为,数据类型的范围限定而导致的对于分子或者分母不能过大或过小而产生的限制。
还有一种极容易出现错误的方式,是我们所构造的系统产生的。比如我们在求解线性方程组Ax=B的时候,如果这个矩阵A的是一个病态矩阵,所谓的病态矩阵,最简单形象的理解就是其中的某些列向量,它们之间的相关性过于大,也就是说列向量非常的接近。
假设这是其中的两个列向量,取了其中一个列向量上的点。这两个列向量过于接近的话,对点进行一个微小的变化,它就有可能跑到了另外一个向量上,就是说它的解发生了发生了很大的变化;按理说这个点是属于向量1的,但仅仅是因为很小的一个扰动,它就跑到了向量2上,它的解就发生了很大的变化。
有一个一般的办法判断矩阵是否病态,就是把矩阵A所有的特征值λ求出来以后,然后把所有λ里的最大的值除以最小值,然后取它的模。我们根据这个值可以判断一个矩阵是否是病态矩阵。
所以很多时候,在进行machine learning或者deep learning之前,我们会对数据进行一个筛选。筛选时候有时候很大的一个目的就是为了把其中的特征叫量过于接近的一些数据排除出去,让我们经过筛选后的矩阵,在它的每一个列向量上有明显的差异,尽量避免过于接近的列向量的产生。
优化算法的意义以及如何选择
下面我们来简单说一下优化算法。绝大部分的机器学习或者说深度学习,都是可以归结为一个求极值的最优化问题。最优化问题,我们想到的简单的办法当然可以有很多,比如说梯度下降,就是仅仅求一个导数就可以判断求极值点的方向。
最优化问题,所谓的最优化去找最小值或者是最大值,涉及到两个问题,一是我怎么找、往哪个方向走;第二个问题是,我知道了这个方案以后我应该怎么走,每一步走多少。这基本上是所有求最值的两个问题,一个是找方向,第二个是找步长。
这是Deep Learning书中关于一些基本函数的定义,包括objective funtion目标函数,或者也可以称为损失函数,或者也可以称为误差函数。这时候我们一般都是要求它的最小值,让误差或者损失尽量的小。
这里我们看一个非常简单的例子,怎么解释刚才说的两个问题,一个是找方向,一个是找步长。这是一个目标函数,一个非常简单的二次函数。我们看红色箭头指的这一点,先看刚才说的取方向、怎么走的问题。这里有无数种方法,每一条直线都可以代表它可以前进的一个方向。但是我们要从中找到一个,因为这个最低点是我们的目标点,我们要找到从这个点出发到目标点的最快的路径、一个方向。
这里面这条红线是书中原有的,我做了两条蓝色的线。我们从这三条线中可以比较出来,红线是这三条线里面朝目标点下降最快的一条线,因为红色线在这个点和目标函数的角度是最小的,所以它是过这个点的下降最快的一条线。
然后我们看第二个问题,就是知道了方向以后怎么去走。对于每一个步长,我们在这里面引入一个ε的权值,为了保持系统的稳定性,一般会取一个比较小的值,比如说0.001或者是10-4这样的一个小值,让这个点缓慢地沿着这个红色的这个方向,一小步一小步地,朝着目标函数前进。
但是这里面会有一些问题,比如说我们会遇到一些特殊的点。刚才的比较简单的二次函数是没有问题的,但是看一下后面一些复杂的函数。
这里是一些特殊的点,critical points,我们可以把它称为临界点。
所谓的临界点是指,它的一次导数为零,也就是说这个点往左或者往右都会都会变大或变小,这个点本身就是这个小的局部系统里面的一个极值点。如果你往两边走都是变大,那么它就是一个极小值点;如果你往两边走都是变小,那么它就是一个极大值点;如果一边减小、一边变大,这种情况是我们在计算里面最不想看到的情况,叫做驻点,虽然它的导数也是零,但是这个点并不是我们所期待的那个objective point,不是我们想要找的目标点。
我们看一个复杂一点的。像这个函数曲线,图中有三个点都满足我们刚才说的一阶导数为零,但是右侧这两个点就不是我们想要的,最左侧点的值更小。这个时候就有两个问题,就是局部极值和全局最值的问题。这三个点都可以称为局部的极值点,只要满足一阶导数为零,但是怎么判断你所求的局部极值点是否是全局的最值点?有一个简单的办法是把整个系统所有的极值点都找到,然后比从里面比较出最小值;但是在实际应用中是不会这么做的,一是浪费太多的计算资源,二是因为起点的不同,找这个局部极值点也会有很多的问题。
所以如果要是把每一个极值点都找的话,会非常的繁琐,会浪费大量的资源。那么,我们设计的系统怎么样保证找到的这个点是一个最优点、或者说是全局的最值点呢?
之前介绍的都是只有单个变量的系统,现在看一下有多个变量的系统。在单变量系统里面,我们只需要求一个输入的导数;但是在多变量的系统里面,有很多的输入,就有一个偏导数的概念,假定其它的变量固定、系统对其中的某一个变量求导的话,就称之为关于这个变量的偏导数。
把所有的变量的偏导数求出来,并用向量的形式表示出来,可以表示成这个形式。刚才我们分析过了,如果要找到局部极值点的话,我们最快的方向是求导数、沿着梯度的方向;那么多变量系统里面也一样,就是说我们要求一个系统的最小值的话,还是通过求导,但这次是多变量的系统,所以我们的求导要改成偏导数向量的方向来去寻找新的最值。
这种梯度下降算法在实现的时候会有一些不同,比如根据每次下降所采用的系统点数的不同,可以大致分为两大类,一种叫做Batch Gradient Desecent,就是批梯度下降。所谓的“批”就是批量,比如说我们现在有一个系统h(x)等于θixi的合集(右上角),这是一个非常简单的线性系统。按照我们之前所说的,首先要求出这个系统的目标函数,我们这里用了一个最小二乘法的目标函数,然后求这个目标函数的最小值问题。
首先我们要求它的偏导数,∂J(θ)/∂θj,它表示一个方向,然后沿着这个方向更新那个变量。在变量更新的时候,批梯度下降是指每一次的变量更新,都会用到所有的xj;然后从i=1到m,会用到所有的单独变量的偏导数。比如假设这个系统里面的每一个样本有五个特征的话,那么在更新任意一个权值的时候都要把这五个特征遍历一遍。
这样的话,如果是一个非常小的系统,比如说样本数量不是很多、每一个样本所包含的特征也不是很多的话,这个是完全可以的,因为它求解的是一个全局的最优,考虑了考虑到了每一个变量方向的梯度问题,所以求的是全局的最优下降的方向。但是所求的系统往往有大量的样本,同时每一个样本还包含了不少的特征,简单分析一下这个系统的计算量的话,假设它的样本数量是n,然后每一个的特征是m,那么其中一个样本的计算量是m×m;有n个样本的话,总的计算量是m×m×n。如果样本1万、2万、10万超级大的话,每一次迭代的计算量是非常大的。
这个时候大家就想到另外一种办法,我能不能在每一次更新权值的时候,不用到所有的特征,只用其中的所求变量的特征,这就是我们所谓的随机梯度下降Stochastic Gradient Descent。随机梯度就是说,每一次针对权值的更新,只随机取其中的一个i,就是随机取其中的一个特征来计算。这样它的计算量立马就下降了,同样是n个样本就变成了m×n。因为原来的公式里面有一个求和符号,需要求m个特征的值;这里面每次只求一个特征的。所以这个计算量就少了非常多。
这又引发了一个问题,通过刚才分析,我们知道BGD是全局自由梯度下降,SGD是随机梯度现象,随机梯度中只找了其中一个变量所在的方向进行搜索,向目标点前进,那么这种方法是否能保证最后到达目标呢?理论上是有证明的,是可以的,只是这个会收敛的非常慢。
这两个方法就有点矛盾,一个是计算量大,但是全局最优,收敛比较快;一个是计算量小,但是收敛比较慢,只能找到最优目标值的附近。所以又产生了一种调和的算法,叫做小批量梯度下降,Mini-Batch Gradient Descent。其实很简单,既不像批量用到所有的特征去更新权值,也不像随机梯度下降只用其中一个,我选取一部分,假设每个样本有100个特征,我只取其中的10个特征用于每一次的权值更新。那么首先它的计算量是下降的,其次它也并不是仅仅按照其中某一个、而是它是按照了10个特征向量所在的方向进行搜索,既保证了搜索速度,又保证了计算量,这是目前在梯度下降中用的比较多的一个方法,算是一个BGD和SGD两种方法的折中方法。
它们三者的优缺点分别就是,批量是计算量大,随机是计算量小,但是搜索精度有一定的问题;Mini-batch就是权衡了两者。
刚才所有的分析都是基于一阶导数,这也是在简单的线性系统优化中常用的。其实二阶导数对于系统的分析也是非常有用的。
看一下这几个简单的例子。我们知道一阶导数的意义表示的是f(x)的变化,二阶导数的意义就是一阶导数的变化情况。比如说第一幅图,它的一阶导数从正(上升)到0(水平)再到负的(下降),不停地减小,就可以知道它的二阶导数是小于0的。第二幅图一条直线的话,它的斜率也就是一阶导数是永远不变,那么它的二阶导数就永远是0;同理第三个图指的是二阶导数大于零的情况。
二阶导数的意义就是我们可以分析这个系统。下面先介绍一个雅克比矩阵(Jacobian Matrix),我们的系统是一个多输入、多输出的系统,它变量的范围是Rm 域的范围,输出是Rn 域的范围,那么f(x) 的雅克比矩阵就是,针对所有的输入啊求导,比如第一行是那个f1对所有的输入变量求导,第二行就是f2,f的第二个变量,对所有的变量求导;同理,最后一行就是fm对所有的变量求导。这就是雅克比矩阵的定义。
雅克比矩阵是一阶的求导矩阵,还有二阶求导矩阵黑塞矩阵(Hessian Matrix)。
黑塞矩阵的定义其实也很简单,每一个f(x) 同时对两个方向的变量求二次导数。当然你也可以把它看成雅克比矩阵的变形,黑塞矩阵里的每一项相当于雅克比矩阵里面的每一项再求导,因为二阶导数可以看成一次求导再求导。这是黑塞矩阵的定义。
黑塞矩阵有一个特点,对于一个系统,如果它的偏导数不分方向的,就是说先对xi求导、或者先对xj求导,求导的先后顺序不影响二次导数值的话,那么黑塞矩阵就明显是一个对称矩阵,因为xi、xj可以互相交换。就是说对先对x2求导或者先对x1求导是没有关系的,那么∂x1∂x2和∂x2*∂x1是相等的。
那么二阶矩阵有什么影响,它对先前的一阶矩阵梯度下降的不足有什么样的改进呢?简单分析一下,一个f(x) 可以做这样的泰勒展开,其中包含特定点的值,这个g 表示的是一阶导数,也就是梯度,然后H是一个二阶的梯度矩阵。
当我们更新x值的时候,比如说现在是x0,然后下一步更新到x0-εg的时候(这是刚才梯度下降的定义嘛),带入这个泰勒展开会得到图中下方的公式。
列出这个公式的主要目的是为了考察梯度下降的步长系数应该怎么取值比较好。刚才讲过了,刚开始我们可以随便给一个比较小的值,0.01、0.004或者更小的值。但是实际的情况下,我们不想随机给一个,而是通过数学的分析得到一个比较好的值,从而定义这个步长系数,可以走得既快又准确。
带入得到这个公式之后(当然这时候我们可以把约等号当作等号),我们可以把它当做一个关于ε的函数,其它的变量可以都当作常数。如果要得ε的一个比较优化的值的话,我们可以看作f(ε) 等于这个式子,然后对它关于ε求导,最后在所有可能的系数里面得到一个比较好的系数。有了这个系数就可以保证我们的步长取得又大又稳。
下面我介绍两个方法,一个是仅仅用了一阶导数的、我们前面提到的gradient descent;另一个是牛顿方法,这是用到二阶导数的方法。梯度下降仅仅用到了一阶导数,所以我们把它称为一阶优化算法;牛顿方法用到了二阶,我们就把牛顿方法称为二阶优化算法。
我们看一下牛顿迭代方法,这是刚才提到的泰勒展开,然后现在想要找到这个系统的极值点,当然,仅仅求导就行了。根据一阶导数为0,它的临界点就是图中下方这个公式。这样我们更新就按照这个公式。
这个公式有什么意义呢?就是一次直接找到了这个critical point,过程中用到的是黑塞矩阵。因为在这里面用到了黑塞矩阵,所以我们把牛顿方法称为一个二阶方法。
这之前,我们遇到的所有求极值的问题都是就是无约束的,就是freestyle,x没有任何的约束。仅仅是求目标函数的最小值问题。但是实际情况里有大量的约束问题,这就牵扯到了另外的约束优化的问题。
这是维基百科上关于约束优化的定义。
首先f(x) 是目标函数,如果没有下面这一堆subject to的话,它就是我们之前讲到的最优化问题,你可以用梯度下降,也可以用牛顿法来求解。但是这个时候它有了很多的约束,比如x必须满足某一个函数,xi代进去要等于一个特定的值ci。这是一个等式,所以又把它称作等式约束;相反就是不等式约束问题。
遇到这样问题应该怎么做?很容易想到能不能把这两个约束的条件整合到目标函数里面,然后对这个整合的系统再求优化问题。其实我们做工程很多时候都是这样的,之前先有一个基本的、best的处理方法,再遇到一个问题以后,就想办法把新产生的问题去往已知的基本问题上靠拢。
这里介绍一个KKT的约束优化算法。KKT优化算法其实很简单,它就是构造了一个广义的拉格朗日函数,然后我们针对这个广义的拉格朗日函数,或者是这个系统来求它的极值。
我们可以从图片上来看这个约束问题。比如我们选了一个初始点,如果没有阴影部分的面积,那就从初始点随便怎么走去找这个最优的x。走的方法就是它的梯度方向。但是现在有约束问题的话,x的取值必须要在阴影范围之内走动,这是一个比较形象的约束问题的表征。
前面提到我们要构造拉格朗日函数。要构造拉格朗日函数也简单,我们现在有一个等式约束,还有一个不等式约束,只要在等式约束和不能约束之前加入一个系数,当然我们是把这些系数看作变量的。把这些系数加入到原来的函数之上,构成了一个新的函数系统,我们就可以把它叫做广义拉格朗日函数。
之前我们是仅仅是求f(x) 的最小值,现在加入了这两个,我们可以根据它的特征分析一下。
首先,h(x) 小于等于0的话,针对它的系数α,我们就要求它的最大值;然后看 λ,因为 λ 是一个常数,求最大或者最小是一样的;最后又归结到f(x),还是求它的最小值。当然,我们也可以两个累加前面都变成负号,那么同理下面可以变成要求它的最小值。
其实也可以很好理解,就是说原来是一个f(x),现在加入了一个东西,这个东西满足的条件是对于任意的x,h(x)都必须是小于等于0的。那么如果我的最大值都小于等于0的话,那肯定所有值都小于等于0了。所以我这边要求一个最小值。
当然我假设加入的这部分是正的,这边所有的α都是大于零的,那么L(x,λ,α) 里αjhj(x) 就始终是小于等于0的;小于等于0的话,我只要让它的最大值满足的小于等于0,那么它所有的其他值肯定也是满足这个条件的。这就是如何构建一个拉格朗日函数的方法。
有了这个构建的函数以后,它的极值问题就可以用梯度下降的方法来求解。
我们举一个简单的例子,最简单的,线性最小二乘法,这个是在求误差的时候最常用的损失函数或者目标函数了。那么我们可以用到前面讲到的梯度下降法,求它的导数,然后x更新的话就是用一个小的补偿系数乘以Δx,就是它的梯度。当然你也可以用牛顿方法,用求它的二阶导数来更新。
现在我们把这个系统稍微改一下,把它变成一个受限的系统。比如我们要求向量x满足这个条件,这样它就变成了一个带有限制的优化问题。这个时候我们可以构造拉格朗日函数,原函数不变,加上它的限制条件,前面加上一个λ变量,然后就可以写出它的目标函数。
首先f(x) 是不变的,然后因为xTx小于等于1,所以这边要求最大的(当然如果xTx大于等于1,你这边要求最小的)。然后怎么更新这个系统呢,x可以这样来表示
基本上就是求逆的操作。λ满足的一个梯度条件是,把它看作单变量,对它求导,它的导数需要满足
这样Deep Learning书的第四章书基本上就讲完了。
总结
最后简单总结一下,这一章主要讲的问题。
第一,我们在做数值计算,包括深度学习或者机器学习的时候,我们要注意里面的变量,尤其是在分母上的变量,不要出于出现过小的值,比如距离,分母不要过桥,分子不要过大。现在是有软件是可以帮助我们检测的,但是因为我们平时用到的算法基本上是成熟的,或者是用了很多Library/库,其中已经对一些异常状况做过提前预防,所以我们的计算中是不存在这个问题的。一般是针对我们要自己动手设计出新的计算方法时才会考虑这个问题;平时的计算过程中一般不需要考虑系统的稳定性问题的。你如果设计一个新的系统,你就要分析一下这个系统的稳定性。
然后就是梯度下降的意义,就是我们找了一个什么样的方向去接近目标函数,有了方案以后我们应该怎么走,每一步应该走多少;有时候你走的过大的话,也会导致系统的发散。
其实在这本书的最后作者也说了,目前Deep Learning系统缺少严格的理论保障。为什么我们做机器学习的时候经常说调参数、调参数,就是因为很多东西可以说是试出来的,并没有严格的数学证明说某一个值应该怎么取。这一章节在最后也说了一个目前使用的深度学习算法的缺点,就是因为它的系统目前过于复杂,比如一层接一层的函数的叠加或者是相乘,它的系统分析就会很复杂,很难有一个明确的理论去分析这个系统的各种特征。如果仅仅是一个简单的f(x)=x2,这种系统无论怎么做都行,它已经被分析的太彻底了,无论怎么算都会有一个精确的算法在那里。所以前面讲的误差也仅仅是在一个常见的容易出错的地方给了一个比较好的指导,但实际的计算过程中还会遇到各种各样的问题。这个时候一是要靠经验,二是也希望会有越来越多的数学理论来支持深度学习的系统分析。
还有就是,我们在做计算的时候都知道有一个天然的矛盾,就是计算量和精度的问题。计算量大就会让精度提高,但是有时候过大的计算量又是我们承受不了的,所以这也是一个矛盾。现在的很多算法其实也就是在中和这个矛盾,既要降低计算量,要保持我们能够接受的精度。所以现在有很多前处理的方式,针对大量的数据要怎么样处理,让设计的系统最后既能够满足我们的要求,又尽量的减少计算量,同时也尽量避免一些不必要的误差。其实这是就是一个洗数据的过程,把数据洗得干净一点,把噪音和没有用的数据都淘汰掉的过程。
今天就和大家分享到这里,如果有什么问题的话,欢迎大家在群里面讨论。
机器学习的数学数学理论其实比较匮乏,所以有很多值得讨论的问题,包括其实有我刚才有好几个点想讲没有讲的,因为时间有限,比如说二阶的优化问题,怎么样去用二阶的优化问题去保证一阶优化找到那个全局的最小点,而不是局部的最小点。其实这个在多目标、多变量的系统里面,目前还没有特别好的方法,当然在单系统里面就不存在这个问题,有很多方法去解决。今天就先到这里,谢谢大家。
(完)
- 点赞
- 收藏
- 关注作者
评论(0)