通俗解读SGD、Momentum、Nestero Momentum、AdaGrad、RMSProp、Adam优化算法

写在前面

这篇文章的写作参考了不少资料,其中两篇作为主要参考资料和cs231n重点给出,因为很多图和文字是直接copy他们的。这两篇文章写的挺不错,读者有兴趣自行翻看。
[1]link
[2]link
[3] CS231n课程,无标注来源图片也均来自于此。

序言

在这里插入图片描述
图片来源link
在深度学习过程中,我们通常会预先定义一个损失函数。有了损失函数以后,我们就可以使用优化算法试图将其最小化。这样的损失函数通常被称作优化问题的目标函数( objective function )。
这里还要说明的是,我们优化算法的目标函数通常是基于训练数据集的损失函数,优化的目标在于降低训练误差。而深度学习的目标不仅仅在于降低训练误差,还要降低在测试集中的泛化误差。在这里,我们只关注在训练集上的训练误差
由上图我们也能看到,采用不同的优化算法,得到的效果也往往不一样。
目前SGD、Momentum是手动指定学习率的优化算法,而后面的AdaGrad、RMSProp,Adam则是可以自动调节学习率的优化算法。

补充知识:BATCH_SIZE,epoch,iteration

在正式讲优化算法之前,有必要先讲下深度学习中的一些基本概念,有助于对后续优化算法的理解。
BATCH_SIZE,中文翻译为批大小(批尺寸)。简单说,假如我们有1000个样本,我们如果一次性将这1000个样本都输入到神经网络中进行训练,这无疑使非常不现实的(其实1000个也还行,但我们在实际过程中,比如训练GAN时常常会用到几万张甚至几十万张图片)。训练速度会很慢。于是,我们想到了一个办法,将1000个样本分10次输入神经网络中去训练,每次输入100个样本。那么BATCH_SIZE的值就是100。这种方式也称为批训练,所以BATCHSIZE指的就是输入神经网络中进行训练的最小单位中样本的个数。
iteration,翻译过来大概是“迭代”的意思。1个iteration等于使用BATCH_SIZE个样本训练一次。
epoch:1个epoch等于使用训练集中的全部样本训练一次
继续上面的例子,训练集有1000个样本,BATCH_SIZE=100,那么,训练完整个训练集一次需要100次iteration,1个epoch。
解释清楚以上三个方法后,再来正式介绍优化算法。

1.梯度下降法

1.1 批量梯度下降BGD

批量梯度下降(Batch gredient descent),每迭代一步都要用到训练集中所有的数据.也就是说,利用现有参数对训练集中的每一个输入生成一个估计输出 y ^ i y\hat{}_{i} y^i,然后和实际输出 y i y_{i} yi比较,统计所有误差,求平均以后得到平均误差,依此来作为更新参数的依据。
具体实现
学习速率: μ \mu μ,初始参数: θ \theta θ
每个iteration的迭代过程:
1.提取训练集中的所有内容 { x 1 , x 2 , . . . , x n } \left \{ x_{1},x_{2},...,x_{n}\right \} {x1,x2,...,xn},以及相关的输出 y i y_{i} yi
2. 计算梯度和误差并更新参数
g ^ = 1 n ∇ θ ∑ i L ( f ( x i ; θ ) , y i ) g\hat{}=\frac{1}{n}\nabla_{\theta}\sum_{i}^{}L(f(x_{i};\theta),y_{i}) g^=n1θiL(f(xi;θ),yi)
θ = θ − μ g ^ \theta=\theta-\mu g\hat{} θ=θμg^
实现结束
优点:由于每一步都利用了训练集中的所有数据,因此当loss达到最小值以后,能够保证此时计算出的梯度为0,也就是能够收敛。因此,使用BGD时不需要逐渐减小学习速率 μ \mu μ
缺点:由于每一步都要使用所有的数据,因此随着数据集的增大,运行速度会越来越慢。
具体代码实现如下:

for i in range(epochs):
    params_grad = evaluate_gradient(loss_function,data,params)
    params = params - learning_rate * params_grad

1.2随机梯度下降SGD

随机梯度下降(stochastic gradient descent)方法,指的是在迭代的每次过程中,我们随机均匀采样的一个样本索引 i ∈ { 1 , . . . , n } i \in \left \{ 1,...,n \right \} i{1,...,n},并计算梯度 ∇ f \nabla f_{} f来迭代x。可以看到,每次迭代的计算开销从梯度下降的O(n)降到了常数O(1)
优点:训练速度快,对于很大的数据集,也能以较快的速度进行收敛。
缺点:对于参数比较敏感,需要注意参数的初始化;由于是抽取的批量数据,因此得到的梯度不可避免有误差。学习速率需要逐渐减小,否则模型会无法收敛。模型在每一次的iteration中受抽样的影响比较大,也就是说梯度含有较大的噪声,不能很好地反应真实梯度。

具体实现
g ^ = ∇ θ L ( f ( x i ; θ ) , y i ) g\hat{}=\nabla_{\theta}L(f(x_{i};\theta),y_{i}) g^=θL(f(xi;θ),yi)
θ = θ − μ g ^ \theta=\theta-\mu g\hat{} θ=θμg^
此处注意一下这里的公式和前面BGD中公式的区别。再理解下计算开销由O(n)下降到O(1)这句话。
代码实现

for i in range(epochs):
    np.random.shuffle(data)
    for example in data:
        params_grad = evaluate_gradient(loss_function,example,params)
        params = params - learning_rate * params_grad

前面说过SGD最大的缺点在于每次计算得到的梯度有误差,每次更新时可能并不会按照正确的方向进行,因此会带来优化波动。如下图:
在这里插入图片描述
图片来源(参考资料[5])

1.3小批量梯度下降MBGD

MBGD方法介于BGD和SGD之间。再两者之间取了一个平衡。大家看下公式就会明白。
具体实现
g ^ = ∇ θ L ( f ( x i : i + m ; θ ) , y i : i + m ) g\hat{}=\nabla_{\theta}L(f(x_{i:i+m};\theta),y_{i:i+m}) g^=θL(f(xi:i+m;θ),yi:i+m)
θ = θ − μ g ^ \theta=\theta-\mu g\hat{} θ=θμg^
代码实现

for i in range(epochs):
    np.random.shuffle(data)
    for batch in get_batches(data, batch_size=100):
        params_grad = evaluate_gradient(loss_function,batch,params)
        params = params - learning_rate * params_grad

这里举个例子大家就会明白。假设我们现在的训练集中样本总数为1000,假如BATCH_SIZE=1000,这时使用的就是BGD。此时iteration=epoch,梯度下降的1个iteration内模型参数只迭代了一次。
假如BATCH_SIZE=1,则这时优化使用的就是SGD。在SGD中,每处理一个样本就会更新一次模型参数。在一个epoch中,模型参数会更新1000次。
这里我们要注意的是,尽管在一个epoch中,BGD和SGD都处理了1000个样本。但是显然SGD耗费的时间更多。因为每次模型参数的更新需要耗费时间。
假如BATCH_SIZE=10,则这时使用的则是MBGD。在一个epoch中,模型参数会更新100次。每次更新时,计算的是这10个样本的梯度取平均
我们可以看出,在每个epoch中,MBGD的计算耗时在BGD和SGD之间。
注意:我翻阅了一些资料,发现很多人都把SGD和MBGD说成是一种算法。严格来说,确实有些不一样。从上面我的讲解就可以看出来。不过,这一点就随大流吧。大流就是我在实际中碰到的SGD,用的都是MBGD的算法。在实际情况中,BGD和SGD其实并不常用,因此,后面我会选择SGD作为代表方法(实际是MBGD算法)和其他方法进行对比。这里大家注意这个概念就行了。

总结:
SGD存在三个缺点:
第一个问题是SGD会陷入局部最小值或鞍点处,因为这连个地方梯度同样为0。这里值得注意的是在高维空间中,当神经网络存在很多参数时,更多发生的是陷入鞍点问题,较少发生陷入局部最小值问题。
第二个问题请看下图:在这里插入图片描述
问题是在这种情况下(一个轴梯度很大,而另一个轴梯度较小),使用SGD会如何收敛呢。我们同样用一张图来回答。
在这里插入图片描述
这里会出现这种情况的原因在于横轴梯度小,而纵轴梯度大,因此总体梯度会像梯度大的方向倾斜。这就导致我们每走一步,都会出现上图所示的情况。
第三个问题则是前面提到的噪音问题。

再次注意:这里又来提醒大家了。下面所讲的优化算法,无一例外都是在SGD(实际上是MBGD算法)的基础上进行的。即每次梯度的计算,都是从N个样本取出m个计算梯度,并取平均。

2.动量法Momentum

Momentum,翻译成中文就是动量的意思。相对于SGD算法,则是采取了不同的学习率更新方式。
上面的SGD的问题在于每次迭代计算时,梯度含有较大的噪音。而Momentum可以比较好的缓解这个问题。尤其是在面对小而连续的梯度,但是含有很多噪音时,可以很好的加速学习。Momentum借用了物理中的动量概念,即前一次的梯度也会参与运算。为了表示动量,引入了一个新的变量v(velosity)。v是之前的梯度的累加,但是每次更新参数时会有一定的衰减。
具体实现:
需要:学习速率 μ \mu μ,初始参数 θ \theta θ,初始速率v,动量衰减参数 α \alpha α
每个iteration迭代过程:
1.提取训练集中的所有内容 { x 1 , x 2 , . . . , x m } \left \{ x_{1},x_{2},...,x_{m}\right \} {x1,x2,...,xm},以及相关的输出 y i y_{i} yi
2.计算梯度和误差,并更新速度v和参数 θ \theta θ
v t + 1 = ρ v t + ∇ f ( x t ) v_{t+1}=\rho v_{t}+\nabla f(x_{t}) vt+1=ρvt+f(xt)
θ t + 1 = θ t − α v t + 1 \theta_{t+1}=\theta_{t}-\alpha v_{t+1} θt+1=θtαvt+1

代码实现

vx=0
while True:
	dx=compute_gradient(x)
	vx=rho*vx+dx
	theta+=learning_rate*vx

实现结束
Momentum的具体过程是我们采用当前的系数,然后使用摩擦系数进行衰减,之后加到梯度上。现在,我们在速度向量的方向上进行步进,而不是在原始梯度向量的方向上进行步进。
其中参数 α \alpha α表示每回合速率v的衰减程度。同时也可推断得到,如果每次计算得到的梯度都是g,那么最后得到的v的稳定值为: μ ∥ g ∥ 1 − α \frac{\mu\left \| g \right \|}{1-\alpha} 1αμg
也就是说,Momentum最好情况下可以将学习速率加速 1 1 − α \frac{1}{1-\alpha} 1α1倍。一般 α \alpha α的取值有0.5,0.9,0.99几种。
加上动量项就像从山顶滚下一个球,在往下滚球的时候累积了前面的动量(动量不断增加),因此速度会变得越来越快,直到到达终点。同理,在更新模型参数时,对于当前的梯度方向与上一次梯度方向相同的参数,则进行加强,即在这里方向上更快了。对于那些梯度方向与上一次梯度方向不同的参数,则进行削减,即这些方向上减慢了,因此可以获得更快的收敛速度与减少震荡。
这里还需和大家讲清楚的一点是,Momentum方法会越过
局部极小值点和鞍点
,这克服了SGD方法的这个缺陷。同理,Momentum方法最后也是会越过全局最小值,即目标值的。这时我们最后得到的是和目标值有一定误差的,把这个误差控制在可接受的范围内即可。但是总体上来说,Momentum相对于SGD的收敛速度快非常多。
在这里插入图片描述

3.Nesterov Momentum

在这里插入图片描述
Nesterov Momentum相对于 Momentum,则是做了一些轻微的改变。在Nesterov Momentum中,如上图的红点所示,我们首先在速度方向上进行步进,之后,我们在评估这个位置的梯度。评估完梯度之后,又回到之前的位置,将当前速度方向和沿速度步进位置的梯度方向相加,得到实际的前进方向。这里肯定有人会问,为何要这样吃饱了撑的呢?有Momentum使用不好吗?
为了解答这个问题,这里,我只能负责人地和大家说,Nesterov Momentum包含了当前速度向量和先前速度向量的误差修正。另一方面我们可以看到Nesterov Momentum和Momentum的一个不同就是由于Nesterov Momentum有校正因子的存在,它不会那么剧烈地越过局部极小值点
实现方式:
v t + 1 = ρ v t − α ∇ f ( θ t + ρ v t ) v_{t+1}=\rho v_{t}-\alpha\nabla f(\theta_{t}+\rho v_{t}) vt+1=ρvtαf(θt+ρvt)
θ t + 1 = θ t + v t + 1 \theta_{t+1}=\theta_{t}+ v_{t+1} θt+1=θt+vt+1

然而,Nesterov Momentum 公式会令人们在实际应用中感到不方便,因为当在优化神经网络时,我们通常会希望能同时计算损失函数和梯度。而Nesterov Momentum 的优化形式会对此造成破坏。
我们可以用换元法改进Nesterov Momentum 公式。
我们可以令 x ˉ = x t + ρ v t \bar{x}=x_{t}+\rho v_{t} xˉ=xt+ρvt,便可以将公式重写为:
v t + 1 = ρ v t − α ∇ f ( θ ˉ ) v_{t+1}=\rho v_{t}-\alpha\nabla f(\bar{\theta}) vt+1=ρvtαf(θˉ)
θ t + 1 ˉ = θ t ˉ − ρ v t + ( 1 + ρ ) v t + 1 = θ t ˉ + v t + 1 + ρ ( v t + 1 − v t ) \bar{\theta_{t+1}}=\bar{\theta_{t}}-\rho v_{t}+(1+\rho) v_{t+1}=\bar{\theta_{t}}+v_{t+1}+\rho (v_{t+1}-v_{t}) θt+1ˉ=θtˉρvt+(1+ρ)vt+1=θtˉ+vt+1+ρ(vt+1vt)
这时我们便可以同时计算loss和梯度。
代码实现

dx=compute_gradient(x)
old_v=v
v=rho*v-learning_rate*dx
theta+=-rho*old_v+(1+rho)*v

4.AdaGrad算法

还记得前面我们讲SGD的问题二时的那副图吗?SGD在碰到横轴梯度小,纵轴梯度大的情况优化效果会很慢。前面的Momentum和NAG算法只是解决了局部极小值/鞍点和梯度中的噪音问题,遗留的问题二并未解决。这里,用AdaGrad便可解决这个问题。

在前面所介绍的梯度下降法和动量法中,目标函数自始自终都是使用同一个学习率。本节介绍的AdaGrad算法,它根据自变量在每个维度的梯度值大小来调整各个维度上的学习率,从而避免统一的学习率难以适应所有维度的问题。具体而言,对低频出现的参数进行大的更新,对高频出现的参数进行小的更新,这句话后面再理解。
具体实现:
需要:全局学习速率 μ \mu μ,初始参数 θ \theta θ,数值稳定量 ϱ \varrho ϱ
说明一下:这里的数值稳定量 ϱ \varrho ϱ的作用主要是保证我们在迭代时不会出现分母为0的情况,一般值设为1e-7或1e-8。在其他地方出现也是一样,不再说明。
中间变量:梯度累积量r(初始化为0)。
每个iteration迭代过程:
1.提取训练集中的所有内容 { x 1 , x 2 , . . . , x m } \left \{ x_{1},x_{2},...,x_{m}\right \} {x1,x2,...,xm},以及相关的输出 y i y_{i} yi
2.计算梯度和误差,更新r,再根据r和梯度计算参数更新量。
具体实现
g ^ + = 1 m ∇ θ ∑ L ( f ( x i : i + m ; θ ) , y i + m ) g\hat{}+=\frac{1}{m}\nabla_{\theta}\sum L(f(x_{i:i+m};\theta),y_{i+m}) g^+=m1θL(f(xi:i+m;θ),yi+m)
r = r + g ^ ⊙ g ^ r= r+g\hat{} \odot g\hat{} r=r+g^g^
△ θ = − μ ϱ + r ⊙ g ^ \triangle \theta=-\frac{\mu}{\varrho+\sqrt{r}}\odot g\hat{} θ=ϱ+r μg^
θ = θ + △ θ \theta=\theta+\triangle \theta θ=θ+θ
代码实现
注意,这里的 ⊙ \odot 是点乘的意思。

grad_squared=0
while True:
	dx=compute_gradient(x)
	grad_squared+=dx*dx
	theta-=learning_rate*dx/(np.sqrt(grad_squared)+1e-7)

实现结束
AdaGrad的核心思想是,我们在优化的过程中,需要保持一个在训练过程中每一步的梯度的平方和的一个持续估计。与速度项不同的是,我们现在有一个梯度平方项,在训练时,我们会一直累加当前梯度的平方到这个梯度平方项。当我们在更新我们的参数向量时,我们会除以这个梯度平方项。

在这里插入图片描述
对于这种情况,我们在横轴的步进速度会越来越快,在纵轴的步进速度则会越来越慢。在这里插入图片描述
这里引申出一个问题,随着时间的增加,AdaGrad最后的训练情况会如何呢?答案是步长会越来越小。因为梯度平方一直在累加。

Adagrad对于凸函数有个非常好的性质。对于非凸函数,则容易陷进局部极小值里面(为了解决这个问题,AdaGrad有一个变体叫RMSProp)。

优点
适合处理稀疏梯度,能实现学习率的自动更改。如果这次梯度大,那么学习速率衰减的就快一些。如果这次梯度小,那么学习速率就衰减的慢一些。
缺点
(1) 仍依赖于人工设置一个全局学习率
(2)中后期,分母上梯度平方的累加会越来越大,步长也越来越小,使gradient接近0,使得训练提前结束。我们通常不倾向于使用AdaGrad对神经网络做训练。

5.RMSProp算法

前面说过,AdaGrad算法的一个缺点是,随着时间越来越长,梯度平方项越来越大,步长则会越来越小。那么有什么方法可以解决这个问题吗?我们想到,给梯度平方项加上一个衰减系数,使得梯度平方项随着时间的增长有一定的衰减,便可解决这个问题。这里,就是RMSProp算法
RMSProp通过引入一个衰减系数,让r每回合都衰减一定的比例,类似于Momentum中的做法,衰减系数通常是0.9或0.99。

具体实现
需要:全局学习速率 μ \mu μ,初始参数 θ \theta θ,数值稳定量 ϱ \varrho ϱ,衰减速率 ρ \rho ρ
中间变量:梯度累加量r,(初始化为0)。
每个iteration迭代过程:
1.提取训练集中的所有内容 { x 1 , x 2 , . . . , x m } \left \{ x_{1},x_{2},...,x_{m}\right \} {x1,x2,...,xm},以及相关的输出 y i y_{i} yi
2.计算梯度和误差,更新r,再根据r和梯度计算参数更新量:
g ^ + = 1 m ∇ θ ∑ i L ( f ( x i : i + m ; θ ) , y i : i + m ) g\hat{}+=\frac{1}{m}\nabla_{\theta}\sum_{i}^{}L(f(x_{i:i+m};\theta),y_{i:i+m}) g^+=m1θiL(f(xi:i+m;θ),yi:i+m)
r = ρ r + ( 1 − ρ ) g ^ ⊙ g ^ r= \rho r+(1-\rho)g\hat{} \odot g\hat{} r=ρr+(1ρ)g^g^
△ θ = − μ ϱ + r ⊙ g ^ \triangle\theta=-\frac{\mu}{\varrho+\sqrt{r}}\odot g\hat{} θ=ϱ+r μg^
θ = θ + △ θ \theta=\theta+\triangle \theta θ=θ+θ
代码实现:

grad_squared=0
while True:
	dx=compute_gradient(x)
	grad_squared=decay_rate*grad_squared+(1-decay_rate)*dx*dx
	theta-=learning_rate*dx/np.sqrt(grad_squared)+1e-7)
	

优点
相比于AgaGrad算法,这种方法很好的解决了深度学习中训练过早结束的问题。适合处理非平稳目标,对于RNN效果很好。
缺点
又引入了新的超参数,衰减系数 ρ \rho ρ。依然依赖于全局学习率

6.Adam算法

截止到目前为止,我们在SGD的基础上共讲过两种类型的优化算法。一是给初始值添加一个动量如Momentum、Nestero Momentum,二是在训练时引入梯度平方项如AdaGrad、RMSProp。那么,能否有一个算法同时讲这两种类型的优点同时融合进去呢?这就是我们的Adam算法

Adam算法(Adaptive Moment Estimation)本质上是带有动量项的RMSProp。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。
Adam常常是用来解决一个新问题的默认算法。
具体实现
需要:学习率 μ \mu μ,初始参数 θ \theta θ,数值稳定量 ϱ \varrho ϱ,一阶动量衰减系数 ρ 1 \rho_{1} ρ1,二阶动量衰减系数 ρ 2 \rho_{2} ρ2
1.提取训练集中的所有内容 { x 1 , x 2 , . . . , x m } \left \{ x_{1},x_{2},...,x_{m}\right \} {x1,x2,...,xm},以及相关的输出 y i y_{i} yi
2.计算梯度和误差,更新r和s,再根据r和s及梯度计算参数更新量:
g ^ = + 1 m ∇ θ ∑ L ( f ( x i : i + m ; θ ) , y i : i + m ) g\hat{}=+\frac{1}{m}\nabla_{\theta}\sum L(f(x_{i:i+m};\theta),y_{i:i+m}) g^=+m1θL(f(xi:i+m;θ),yi:i+m)
r = ρ r + ( 1 − ρ ) g ^ ⊙ g ^ r= \rho r+(1-\rho)g\hat{} \odot g\hat{} r=ρr+(1ρ)g^g^
△ θ = − μ ϱ + r ⊙ g ^ \triangle\theta=-\frac{\mu}{\varrho+\sqrt{r}}\odot g\hat{} θ=ϱ+r μg^
θ = θ + △ θ \theta=\theta+\triangle \theta θ=θ+θ
代码实现

first_moment=0
second_moment=0
while True:
	dx=compute_gradient(x)
	first_moment=beta1*first_moment+(1-beta)*dx
	second_moment=beta2*second_moment+(1-beta2)*dx*dx
	theta-=learning_rate*first_moment/(np.sqrt(second_moment)+1e-7)

我们现在来看下如何更新Adam算法。我们使用第一动量,优点类似于速度,并且除以第二动量,或者说第二动量的平方根(梯度平方项)。这里其实存在一个Adam的问题,这个问题是,在最初的第一步会发生什么?
在第一步,我们可以看到,在开始时,我们已将第二动量初始化为0。第二动量经过一步更新后,(通常beta2,也就是第二动量的衰减率),经过一次的更新,第二动量仍非常小,非常接近于0。因此,我们现在在这里做出更新步骤,除以第二动量,那么我们在一开始就会得到一个很大的步长。这个在开始时非常非常大的步长,并不是因为这一步的梯度太大,只是因为我们人为地将第二动量初始化为了0。为了解决这个问题,我们引入第一动量和第二动量的无偏估计校正项。
修正后的Adam算法代码实现

first_moment=0
second_moment=0
while True:
	dx=compute_gradient(x)
	first_moment=beta1*first_moment+(1-beta)*dx
	second_moment=beta2*second_moment+(1-beta2)*dx*dx
	first_unbias=first_moment/(1-beta1**t)
	second_unbias=second_moment/(1-beta2**t)
	theta-=learning_rate*first_moment/(np.sqrt(second_moment)+1e-7)

修正后的Adam几乎可以认为是完美的算法了,将前面所讲方法的优点融为一体。
全文结束。

后记

写这篇文章花了两个下午大概7-8个消失的时间,大概码了一万多字。其实打字还好,主要是csdn不支持向word一样的公式插入,只能用katex中的方法输入公式,着实费力。发现写博客最大的意义在于,写完后自己会记得非常深刻,往后再碰到,也不需要再翻书或者怎么样了。
顺便说下,有一处地方公式和前面写的有些不太一样,但我相信,理解了原理,看懂公式肯定不是问题,所以最后也懒得去统一。
最后再啰嗦一下,如有不对的地方,请在下方评论。十分感谢。

郑重声明:所有参考资料均在下方列出,若有异议,请直接联系笔者本人删除或修改。
其他参考资料
[1]https://blog.csdn.net/program_developer/article/details/78597738
[2]https://blog.csdn.net/manong_wxd/article/details/78735439
[3]cs231n
[4]https://blog.csdn.net/u014595019/article/details/52989301?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param
[5]https://blog.csdn.net/heyongluoyao8/article/details/52478715?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值