算法对比图
1. 算法时间复杂度比较:根号n与logn相比哪个更优优多少试根据下图猜想其算法
米勒罗宾是logn的算法,但是实际应用上它并不稳定,一般在范围较大(int64范围)才会用,一般的情况用的都是sqrt(n)的算法,但是在需要判断大量素数的情况下(假设判断次数为m),一般是比较m*sqrt(n)和n的大小,如果前者小就暴力判断,否则用筛法会更快。
然后比较,在不考虑常数的情况下是logn更优,但是算法常数导致在数据较小的一些情况下sqrt(n)反而更快。
第一个根号n的:
#include<cmath>
inlineboolisPrime(intx){
if(x==2){returntrue;}
if(x<2){returnfalse;}
intpos=int(sqrt(x))+1;
for(inti=2;i<=pos;++i){
if(x%i==0){returnfalse;}
}
returntrue;
}
然后logn的米勒罗宾你可以看下博客网页链接
然后提供一个筛法的代码(stl版本)
#include<vector>
boolvis[MAXNUM];//MAXNUM就是最大数字
std::vector<int>primes;//储存素数
inlinevoidgetPrimes(intmaxn){
for(inti=2;i<=maxn;++i){
if(!vis[i]){primes.push_back(i);}
for(size_tj=0;j<primes.size()&&primes[j]*i<=maxn;++j){
vis[primes[j]*i]=true;
}
}
}
实际应用一般用筛法或者sqrt(n)算法,只有大数据才会用米勒罗宾
2. matlab怎么把两个不同算法的仿真图放在一个图里作比较
在画春物完一张图后,加一条hold on语句,再画第二者森羡张图,以此类推,就可以在一张图上花画多条曲线首拍,另外可以通过代码设置成不同颜色或者线形加以区分
3. 机器学习中有哪些重要的优化算法
梯度下降是非常常用的优化算法。作为机器学习的基础知识,这是一个必须要掌握的算法。借助本文,让我们来一起详细了解一下这个算法。
前言
本文的代码可以到我的Github上获取:
https://github.com/paulQuei/gradient_descent
本文的算法示例通过Python语言实现,在实现中使用到了numpy和matplotlib。如果你不熟悉这两个工具,请自行在网上搜索教程。
关于优化
大多数学习算法都涉及某种形式的优化。优化指的是改变x以最小化或者最大化某个函数的任务。
我们通常以最小化指代大多数最优化问题。最大化可经由最小化来实现。
我们把要最小化或最大化的函数成为目标函数(objective function)或准则(criterion)。
我们通常使用一个上标*表示最小化或最大化函数的x值,记做这样:
[x^* = arg; min; f(x)]
优化本身是一个非常大的话题。如果有兴趣,可以通过《数值优化》和《运筹学》的书籍进行学习。
模型与假设函数
所有的模型都是错误的,但其中有些是有用的。– George Edward Pelham Box
模型是我们对要分析的数据的一种假设,它是为解决某个具体问题从老洞数据中学习到的,因此它是机器学习最核心的概念。
针对一个问题,通常有大量的模型可以选择。
本文不会深入讨论这方面的内容,关于各种模型请参阅机器学习的相关书籍。本文仅以最简单的线性模型为基础来讨论梯度下降算法。
这里我们先介绍一下在监督学习(supervised learning)中常见的三个符号:
m,描述训练样本的数量
x,描述输入变量或特征
y,描述输出变量或者叫目标值
- 请注意,一个样本笑或可能有很多的特征,因此x和y通常是一个向量。不过在刚开始学习的时候,为了便于理解,你可以暂时理解为这就是一个具体的数值。
- 代价函数也叫损失函数。
- 不同的模型可能会用不同的损失函数。例如,logistic回归的假设函数是这样的:。其代价函数是这样的:
对于一个函数,怎么确定下行的方向?
每一步该往前走多远?
有没有可能停留在半山腰的平台上?
- 这里的下标i表示第i个参数。 上标k指的是第k步的计算结果,而非k次方。在能够理解的基础上,下文的公式中将省略上标k。
收敛是指函数的变化率很小。具体选择多少合适需要根据具体的项目来确定。在演示项目中我们可以选择0.01或者0.001这样的值。不同的值将影响算法的迭代次数,因为在梯度下降的最后,我们会越来越接近平坦的地方,这个时候函数的变化率也越来越小。如果选择一个很小的值,将可能导致算法迭代次数暴增。
公式中的 称作步长,也称作学习率(learning rate)。它决定了每一步往前走多远,关于这个值我们会在下文中详细讲解。你可以暂时人为它是一个类似0.01或0.001的固定值。
在具体的项目,我们不会让算法无休止的运行下去,所以通常会设置一个迭代次数的最大上限。
我们随机选择了 都为10作为起点
设置最多迭代1000次
收敛的范围设为0.001
学习步长设为0.01
如果样本数量较小(例如小于等于2000),选择BGD即可。
如果样本数量很大,选择 来进行MBGD,例如:64,128,256,512。
- 《深度学习》一书中是这样描述的:“与其说是科学,这更像是一门艺术,我们应该谨慎地参考关于这个问题的大部分指导。”。
对于凸函数或者凹函数来说,不存在局部极值的问题。其局部极值一定是全局极值。
最近的一些研究表明,某些局部极值并没有想象中的那么糟糕,它们已经非常的接近全局极值所带来的结果了。
Wikipeida: Gradient descent
Sebastian Ruder: An overview of gradient descent optimization algorithms
吴恩达:机器学习
吴恩达:深度学习
Peter Flach:机器学习
李宏毅 - ML Lecture 3-1: Gradient Descent
PDF: 李宏毅 - Gradient Descent
Intro to optimization in deep learning: Gradient Descent
Intro to optimization in deep learning: Momentum, RMSProp and Adam
Stochastic Gradient Descent – Mini-batch and more
刘建平Pinard - 梯度下降(Gradient Descent)小结
多元函数的偏导数、方向导数、梯度以及微分之间的关系思考
[Machine Learning] 梯度下降法的三种形式BGD、SGD以及MBGD
- 作者:阿Paul https://paul.pub/gradient-descent/
训练集会包含很多的样本,我们用 表示其中第i个样本。
x是数据样本的特征,y是其目标值。例如,在预测房价的模型中,x是房子的各种信息,例如:面积,楼层,位置等等,y是房子的价格。在图像识别的任务中,x是图形的所有像素点数据,y是图像中包含的目标对象。
我们是希望寻找一个函数,将x映射到y,这个函数要足够的好,以至于能够预测对应的y。由于历史原因,这个函数叫做假设函数(hypothesis function)。
学习的过程如下图所示。即:首先根据已有的数据(称之为训练集)训练我们的算法模型,然后根据模型的假设函数来进行新数据的预测。
线性模型(linear model)正如其名称那样:是希望通过一个直线的形式来描述模式。线性模型的假设函数如下所示:
[h_{ heta}(x) = heta_{0} + heta_{1} * x]
这个公式对于大家来说应该都是非常简单的。如果把它绘制出来,其实就是一条直线。
下图是一个具体的例子,即: 的图形:
在实际的机器学习工程中碰含伍,你会拥有大量的数据。这些数据会来自于某个数据源。它们存储在csv文件中,或者以其他的形式打包。
但是本文作为演示使用,我们通过一些简单的代码自动生成了需要的数据。为了便于计算,演示的数据量也很小。
import numpy as np
max_x = 10
data_size = 10
theta_0 = 5
theta_1 = 2
def get_data:
x = np.linspace(1, max_x, data_size)
noise = np.random.normal(0, 0.2, len(x))
y = theta_0 + theta_1 * x + noise
return x, y
这段代码很简单,我们生成了x范围是 [1, 10] 整数的10条数据。对应的y是以线性模型的形式计算得到,其函数是:。现实中的数据常常受到各种因素的干扰,所以对于y我们故意加上了一些高斯噪声。因此最终的y值为比原先会有轻微的偏离。
最后我们的数据如下所示:
x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
y = [6.66, 9.11, 11.08, 12.67, 15.12, 16.76, 18.75, 21.35, 22.77, 24.56]
我们可以把这10条数据绘制出来这样就有一个直观的了解了,如下图所示:
虽然演示用的数据是我们通过公式计算得到的。但在实际的工程中,模型的参数是需要我们通过数据学习到的。所以下文我们假设我们不知道这里线性模式的两个参数是什么,而是通过算法的形式求得。
最后再跟已知的参数进行对比以验证我们的算法是否正确。
有了上面的数据,我们可以尝试画一条直线来描述我们的模型。
例如,像下面这样画一条水平的直线:
很显然,这条水平线离数据太远了,非常的不匹配。
那我们可以再画一条斜线。
我们初次画的斜线可能也不贴切,它可能像下面这样:
最后我们通过不断尝试,找到了最终最合适的那条,如下所示:
梯度下降算法的计算过程,就和这种本能式的试探是类似的,它就是不停的迭代,一步步的接近最终的结果。
代价函数
上面我们尝试了几次通过一条直线来拟合(fitting)已有的数据。
二维平面上的一条直线可以通过两个参数唯一的确定,两个参数的确定也即模型的确定。那如何描述模型与数据的拟合程度呢?答案就是代价函数。
代价函数(cost function)描述了学习到的模型与实际结果的偏差程度。以上面的三幅图为例,最后一幅图中的红线相比第一条水平的绿线,其偏离程度(代价)应该是更小的。
很显然,我们希望我们的假设函数与数据尽可能的贴近,也就是说:希望代价函数的结果尽可能的小。这就涉及到结果的优化,而梯度下降就是寻找最小值的方法之一。
对于每一个样本,假设函数会依据计算出一个估算值,我们常常用来表示。即 。
很自然的,我们会想到,通过下面这个公式来描述我们的模型与实际值的偏差程度:
[(h_ heta(x^i) - y^i)^2 = (widehat{y}^{i} - y^i)^2 = ( heta_{0} + heta_{1} * x^{i} - y^{i})^2]
请注意, 是实际数据的值, 是我们的模型的估算值。前者对应了上图中的离散点的y坐标,后者对应了离散点在直线上投影点的y坐标。
每一条数据都会存在一个偏差值,而代价函数就是对所有样本的偏差求平均值,其计算公式如下所示:
[L( heta) = frac {1}{m} sum_{i=1}^{m}(h_ heta(x^i) - y^i)^2 = frac {1}{m} sum_{i=1}^{m}( heta_{0} + heta_{1} * x^{i} - y^{i})^2]
当损失函数的结果越小,则意味着通过我们的假设函数估算出的结果与真实值越接近。这也就是为什么我们要最小化损失函数的原因。
借助上面这个公式,我们可以写一个函数来实现代价函数:
def cost_function(x, y, t0, t1):
cost_sum = 0
for i in range(len(x)):
cost_item = np.power(t0 + t1 * x[i] - y[i], 2)
cost_sum += cost_item
return cost_sum / len(x)
这个函数的代码应该不用多做解释,它就是根据上面的完成计算。
我们可以尝试选取不同的 和 组合来计算代价函数的值,然后将结果绘制出来:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
theta_0 = 5
theta_1 = 2
def draw_cost(x, y):
fig = plt.figure(figsize=(10, 8))
ax = fig.gca(projection='3d')
scatter_count = 100
radius = 1
t0_range = np.linspace(theta_0 - radius, theta_0 + radius, scatter_count)
t1_range = np.linspace(theta_1 - radius, theta_1 + radius, scatter_count)
cost = np.zeros((len(t0_range), len(t1_range)))
for a in range(len(t0_range)):
for b in range(len(t1_range)):
cost[a][b] = cost_function(x, y, t0_range[a], t1_range[b])
t0, t1 = np.meshgrid(t0_range, t1_range)
ax.set_xlabel('theta_0')
ax.set_ylabel('theta_1')
ax.plot_surface(t0, t1, cost, cmap=cm.hsv)
在这段代码中,我们对 和 各自指定了一个范围进行100次的采样,然后以不同的 组合对来计算代价函数的值。
如果我们将所有点的代价函数值绘制出来,其结果如下图所示:
从这个图形中我们可以看出,当 越接近 [5, 2]时其结果(偏差)越小。相反,离得越远,结果越大。
直观解释
从上面这幅图中我们可以看出,代价函数在不同的位置结果大小不同。
从三维的角度来看,这就和地面的高低起伏一样。最高的地方就好像是山顶。
而我们的目标就是:从任意一点作为起点,能够快速寻找到一条路径并以此到达图形最低点(代价值最小)的位置。
而梯度下降的算法过程就和我们从山顶想要快速下山的做法是一样的。
在生活中,我们很自然会想到沿着最陡峭的路往下行是下山速度最快的。如下面这幅图所示:
针对这幅图,细心的读者可能很快就会有很多的疑问,例如:
这些问题也就是本文接下来要讨论的内容。
算法描述
梯度下降算法最开始的一点就是需要确定下降的方向,即:梯度。
我们常常用 来表示梯度。
对于一个二维空间的曲线来说,梯度就是其切线的方向。如下图所示:
而对于更高维空间的函数来说,梯度由所有变量的偏导数决定。
其表达式如下所示:
[ abla f({ heta}) = ( frac{partial f({ heta})}{partial heta_1} , frac{partial f({ heta})}{partial heta_2} , ... , frac{partial f({ heta})}{partial heta_n} )]
在机器学习中,我们主要是用梯度下降算法来最小化代价函数,记做:
[ heta ^* = arg min L( heta)]
其中,L是代价函数,是参数。
梯度下降算法的主体逻辑很简单,就是沿着梯度的方向一直下降,直到参数收敛为止。
记做:
[ heta ^{k + 1}_i = heta^{k}_i - lambda abla f( heta^{k})]
这里有几点需要说明:
线性回归的梯度下降
有了上面的知识,我们可以回到线性模型代价函数的梯度下降算法实现了。
首先,根据代价函数我们可以得到梯度向量如下:
[ abla f({ heta}) = (frac{partial L( heta)}{ partial heta_{0}}, frac{ partial L( heta)}{ partial heta_{1}}) = (frac {2}{m} sum_{i=1}^{m}( heta_{0} + heta_{1} * x^{i} - y^{i}) , frac {2}{m} sum_{i=1}^{m}( heta_{0} + heta_{1} * x^{i} - y^{i}) x^{i})]
接着,将每个偏导数带入迭代的公式中,得到:
[ heta_{0} := heta_{0} - lambda frac{partial L( heta_{0})}{ partial heta_{0}} = heta_{0} - frac {2 lambda }{m} sum_{i=1}^{m}( heta_{0} + heta_{1} * x^{i} - y^{i}) heta_{1} := heta_{1} - lambda frac{partial L( heta_{1})}{ partial heta_{1}} = heta_{1} - frac {2 lambda }{m} sum_{i=1}^{m}( heta_{0} + heta_{1} * x^{i} - y^{i}) x^{i}]
由此就可以通过代码实现我们的梯度下降算法了,算法逻辑并不复杂:
learning_rate = 0.01
def gradient_descent(x, y):
t0 = 10
t1 = 10
delta = 0.001
for times in range(1000):
sum1 = 0
sum2 = 0
for i in range(len(x)):
sum1 += (t0 + t1 * x[i] - y[i])
sum2 += (t0 + t1 * x[i] - y[i]) * x[i]
t0_ = t0 - 2 * learning_rate * sum1 / len(x)
t1_ = t1 - 2 * learning_rate * sum2 / len(x)
print('Times: {}, gradient: [{}, {}]'.format(times, t0_, t1_))
if (abs(t0 - t0_) < delta and abs(t1 - t1_) < delta):
print('Gradient descent finish')
return t0_, t1_
t0 = t0_
t1 = t1_
print('Gradient descent too many times')
return t0, t1
这段代码说明如下:
如果我们将算法迭代过程中求得的线性模式绘制出来,可以得到下面这幅动态图:
最后算法得到的结果如下:
Times: 657, gradient: [5.196562662718697, 1.952931052920264]
Times: 658, gradient: [5.195558390180733, 1.9530753071808193]
Times: 659, gradient: [5.194558335124868, 1.9532189556399233]
Times: 660, gradient: [5.193562479839619, 1.9533620008416623]
Gradient descent finish
从输出中可以看出,算法迭代了660次就收敛了。这时的结果[5.193562479839619, 1.9533620008416623],这已经比较接近目标值 [5, 2]了。如果需要更高的精度,可以将delta的值调的更小,当然,此时会需要更多的迭代次数。
高维扩展
虽然我们举的例子是二维的,但是对于更高维的情况也是类似的。同样是根据迭代的公式进行运算即可:
[ heta_{i} = heta_{i} - lambda frac {partial L( heta)}{partial heta_i} = heta_{i} - frac{2lambda}{m} sum_{i=1}^{m}(h_ heta(x^{k})-y^k)x_i^k]
这里的下标i表示第i个参数,上标k表示第k个数据。
梯度下降家族BGD
在上面的内容中我们看到,算法的每一次迭代都需要把所有样本进行遍历处理。这种做法称为之Batch Gradient Descent,简称BGD。作为演示示例只有10条数据,这是没有问题的。
但在实际的项目中,数据集的数量可能是几百万几千万条,这时候每一步迭代的计算量就会非常的大了。
于是就有了下面两个变种。
SGD
Stochastic Gradient Descent,简称SGD,这种算法是每次从样本集中仅仅选择一个样本来进行计算。很显然,这样做算法在每一步的计算量一下就少了很多。
其算法公式如下:
[ heta_{i} = heta_{i} - lambda frac {partial L( heta)}{partial heta_i} = heta_{i} - lambda(h_ heta(x^k)-y^k)x_i^k]
当然,减少算法计算量也是有代价的,那就是:算法结果会强依赖于随机取到的数据情况,这可能会导致算法的最终结果不太令人满意。
MBGD
以上两种做法其实是两个极端,一个是每次用到了所有数据,另一个是每次只用一个数据。
我们自然就会想到两者取其中的方法:每次选择一小部分数据进行迭代。这样既避免了数据集过大导致每次迭代计算量过大的问题,也避免了单个数据对算法的影响。
这种算法称之为Mini-batch Gradient Descent,简称MBGD。
其算法公式如下:
[ heta_{i} = heta_{i} - lambda frac {partial L( heta)}{partial heta_i} = heta_{i} - frac{2lambda}{m} sum_{i=a}^{a + b}(h_ heta(x^k)-y^k)x_i^k]
当然,我们可以认为SGD是Mini-batch为1的特例。
针对上面提到的算法变种,该如何选择呢?
下面是Andrew Ng给出的建议:
下表是 Optimization for Deep Learning 中对三种算法的对比
方法准确性更新速度内存占用在线学习BGD好慢高否SGD好(with annealing)快低是MBGD好中等中等是
算法优化
式7是算法的基本形式,在这个基础上有很多人进行了更多的研究。接下来我们介绍几种梯度下降算法的优化方法。
Momentum
Momentum是动量的意思。这个算法的思想就是借助了动力学的模型:每次算法的迭代会使用到上一次的速度作为依据。
算法的公式如下:
[v^t = gamma v^{t - 1} + lambda abla f( heta) heta = heta - v_t]
对比式7可以看出,这个算法的主要区别就是引入了,并且,每个时刻的受前一个时刻的影响。
从形式上看,动量算法引入了变量 v 充当速度角色——它代表参数在参数空间移动的方向和速率。速度被设为负梯度的指数衰减平均。名称动量来自物理类比,根据牛顿运动定律,负梯度是移动参数空间中粒子的力。动量在物理学上定义为质量乘以速度。在动量学习算法中,我们假设是单位质量,因此速度向量 v 也可以看作是粒子的动量。
对于可以取值0,而是一个常量,设为0.9是一个比较好的选择。
下图是momentum算法的效果对比:
对原来的算法稍加修改就可以增加动量效果:
def gradient_descent_with_momentum(x, y):
t0 = 10
t1 = 10
delta = 0.001
v0 = 0
v1 = 0
gamma = 0.9
for times in range(1000):
sum1 = 0
sum2 = 0
for i in range(len(x)):
sum1 += (t0 + t1 * x[i] - y[i])
sum2 += (t0 + t1 * x[i] - y[i]) * x[i]
v0 = gamma * v0 + 2 * learning_rate * sum1 / len(x)
v1 = gamma * v1 + 2 * learning_rate * sum2 / len(x)
t0_ = t0 - v0
t1_ = t1 - v1
print('Times: {}, gradient: [{}, {}]'.format(times, t0_, t1_))
if (abs(t0 - t0_) < delta and abs(t1 - t1_) < delta):
print('Gradient descent finish')
return t0_, t1_
t0 = t0_
t1 = t1_
print('Gradient descent too many times')
return t0, t1
以下是该算法的输出:
Times: 125, gradient: [4.955453758569991, 2.000005017897775]
Times: 126, gradient: [4.955309381126545, 1.9956928964532015]
Times: 127, gradient: [4.9542964317327005, 1.9855674828684156]
Times: 128, gradient: [4.9536358220657, 1.9781180992510465]
Times: 129, gradient: [4.95412496254411, 1.9788858350530971]
Gradient descent finish
从结果可以看出,改进的算法只用了129次迭代就收敛了。速度比原来660次快了很多。
同样的,我们可以把算法计算的过程做成动态图:
对比原始的算法过程可以看出,改进算法最大的区别是:在寻找目标值时会在最终结果上下跳动,但是越往后跳动的幅度越小,这也就是动量所产生的效果。
Learning Rate 优化
至此,你可能还是好奇该如何设定学习率的值。
事实上,这个值的选取需要一定的经验或者反复尝试才能确定。
关键在于,这个值的选取不能过大也不能过小。
如果这个值过小,会导致每一次迭代的步长很小,其结果就是算法需要迭代非常多的次数。
那么,如果这个值过大会怎么样呢?其结果就是:算法可能在结果的周围来回震荡,却落不到目标的点上。下面这幅图描述了这个现象:
事实上,学习率的取值未必一定要是一个常数,关于这个值的设定有很多的研究。
下面是比较常见的一些改进算法。
AdaGrad
AdaGrad是Adaptive Gradient的简写,该算法会为每个参数设定不同的学习率。它使用历史梯度的平方和作为基础来进行计算。
其算法公式如下:
[ heta_i = heta_i - frac{lambda}{sqrt{G_t + epsilon}} abla f( heta)]
对比式7,这里的改动就在于分号下面的根号。
根号中有两个符号,第二个符号比较好理解,它就是为了避免除0而人为引入的一个很小的常数,例如可以设为:0.001。
第一个符号的表达式展开如下:
[G_t = sum_{i = 1}^{t} abla f( heta){i} abla f( heta){i}^{T}]
这个值其实是历史中每次梯度的平方的累加和。
AdaGrad算法能够在训练中自动的对learning rate进行调整,对于出现频率较低参数采用较大的学习率;相反,对于出现频率较高的参数采用较小的学习率。因此,Adagrad非常适合处理稀疏数据。
但该算法的缺点是它可能导致学习率非常小以至于算法收敛非常的慢。
关于这个算法的直观解释可以看李宏毅教授的视频课程:ML Lecture 3-1: Gradient Descent。
RMSProp
RMS是Root Mean Square的简写。RMSProp是AI教父Geoff Hinton提出的一种自适应学习率方法。AdaGrad会累加之前所有的梯度平方,而RMSProp仅仅是计算对应的平均值,因此可缓解Adagrad算法学习率下降较快的问题。
该算法的公式如下:
[E[ abla f( heta_{i})^2]^{t} = gamma E[ abla f( heta_{i})^2]^{t - 1} + (1-gamma)( abla f( heta_{i})^{t})^{2} heta_i = heta_i - frac{lambda}{sqrt{E[g^2]^{t+1} + epsilon}} abla f( heta_{i})]
类似的,是为了避免除0而引入。 是衰退参数,通常设为0.9。
这里的 是t时刻梯度平方的平均值。
Adam
Adam是Adaptive Moment Estimation的简写。它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。
Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。
该算法公式如下:
[m^{t} = eta_{1} m^{t-1} + (1-eta_{1}) abla f( heta) v^{t} = eta_{2} v^{t-1} + (1-eta_{2}) abla f( heta)^2 widehat{m}^{t} = frac{m^{t}}{1 - eta^{t}_1} widehat{v}^{t} = frac{v^{t}}{1 - eta^{t}_2} heta = heta - frac{lambda}{sqrt{widehat{v}^{t}} + epsilon}widehat{m}^{t}]
,分别是对梯度的一阶矩估计和二阶矩估计。, 是对,的校正,这样可以近似为对期望的无偏估计。
Adam算法的提出者建议 默认值为0.9,默认值为0.999,默认值为 。
在实际应用中 ,Adam较为常用,它可以比较快地得到一个预估结果。
优化小结
这里我们列举了几种优化算法。它们很难说哪种最好,不同的算法适合于不同的场景。在实际的工程中,可能需要逐个尝试一下才能确定选择哪一个,这个过程也是目前现阶段AI项目要经历的工序之一。
实际上,该方面的研究远不止于此,如果有兴趣,可以继续阅读 《Sebastian Ruder: An overview of gradient descent optimization algorithms》 这篇论文或者 Optimization for Deep Learning 这个Slides进行更多的研究。
由于篇幅所限,这里不再继续展开了。
算法限制
梯度下降算法存在一定的限制。首先,它要求函数必须是可微分的,对于不可微的函数,无法使用这种方法。
除此之外,在某些情况下,使用梯度下降算法在接近极值点的时候可能收敛速度很慢,或者产生Z字形的震荡。这一点需要通过调整学习率来回避。
另外,梯度下降还会遇到下面两类问题。
局部最小值
局部最小值(Local Minima)指的是,我们找到的最小值仅仅是一个区域内的最小值,而并非全局的。由于算法的起点是随意取的,以下面这个图形为例,我们很容易落到局部最小值的点里面。
这就是好像你从上顶往下走,你第一次走到的平台未必是山脚,它有可能只是半山腰的一个平台的而已。
算法的起点决定了算法收敛的速度以及是否会落到局部最小值上。
坏消息是,目前似乎没有特别好的方法来确定选取那个点作为起点是比较好的,这就有一点看运气的成分了。多次尝试不同的随机点或许是一个比较好的方法,这也就是为什么做算法的优化这项工作是特别消耗时间的了。
但好消息是:
鞍点
除了Local Minima,在梯度下降的过程中,还有可能遇到另外一种情况,即:鞍点(Saddle Point)。鞍点指的是我们找到点某个点确实是梯度为0,但它却不是函数的极值,它的周围既有比它小的值,也有比它大的值。这就好像马鞍一样。
如下图所示:
多类随机函数表现出以下性质:在低维空间中,局部极值很普遍。但在高维空间中,局部极值比较少见,而鞍点则很常见。
不过对于鞍点,可以通过数学方法Hessian矩阵来确定。关于这点,这里就不再展开了,有兴趣的读者可以以这里提供的几个链接继续探索。
参考资料与推荐读物
4. 从头理解JPS寻路算法
本文的思路受到博客: http://blog.sina.com.cn/s/blog_4a5c75d40102wo5l.html
和论文: http://www.doc88.com/p-6099778647749.html 的启发和借鉴。
JPS(jump point search)算法实际上是对A 寻路算李祥桐法的一个改进,即在扩展搜索节点时,提出了更优化的策略,A 在扩展节点时会把节点所有邻居都考虑进去,这样openlist中点的数量会很多,搜索效率较慢。JPS算法通过寻找跳点的方式,排除了大量不感兴趣的点,减少了openlist中搜索的点的数量,速度大大提高。
水平(垂直)搜索:如图右边部分描述,点1和4如果经过x到达,还不如从点p(x)到达,所以不用考虑点1,4,同理继续向右搜索时,点2,5和点3,6,都是类似的情况,直到遇到黑色的障碍,没有发现感兴趣的点,x处水平方向搜索完成,垂直方向搜索类似。如图左边部分描述,在点x处向右搜索至点y时,点y有一个强迫邻居(点7),因此y是从点x处沿水平方向搜索的一个跳点,需要将y加入到openlist中。水平搜索结束。
对角搜索:斜向搜索时,需要先在当前点的水平和垂直方向搜索一下,看能否找到感兴趣的点,如果找到,则将当前点加到openlist,结束斜向搜索,如果找不到,则继续宴卜斜向多走一步,继续,直到找到感兴趣的点或者斜向方向遇到障碍。
一条完整路线的搜索过程:
搜索算法:
下面两张对比图,摘自上面的论文,可以看出,JPS算法实在优秀。
github链接
上图中红色块为节点x,基于前节点为P(x)的情况下的自然邻居节点。而被迫邻居则是图中绿色的方块(其对称情况未画出)。x被迫邻居包含三层隐藏含义:首先被迫邻居的位置是基于P(x)、x、阻挡块的相对关系定的,如第三个图所示,如果第三个图中,P(x)的位置在节点6的位置,那么绿色方块就不是x的被迫邻居了。其次,被迫邻居一点是考察非自然邻居的节点。最后被迫邻居一定是P(x)经过x到达才能取得最短路径的点。
起点S,终点T,黑色块为阻挡黄色块为跳点,红色箭头是搜索方向,但是没有找到跳点,绿色箭头表示找到跳点。横坐标A-N,纵坐标1-10。
起始位置S,将A10加入openlist里,开始算法流程。
从openlist里取出代价最小的节点(现在为S),当前节点没有方向,所以需要搜索8个方向,只有右上方B9点是跳点。因为根据跳点定义iii,斜向搜索时,需要在两个分量方向查找跳点,右方是墙壁,略过,上方B8点有强迫邻居点C7,所以B8是跳点(根据跳点定义ii),所以B9是跳点。将B9加入openlist里。A10处理完后,将其从openlist中移除,加入closelist里。
从openlist中取出B9点,该点的父亲是S,所以搜索方向为右斜上,需要在右斜上,右方,上方搜索跳点,只有B8满足要求,将B8加入openlist。处理完B9点将其移到closelist。
从openlist取出B8点,该点搜索哪坦方向为上,B8有被邻居C7点,在斜上方搜索跳点,可以看出D8是C7的被迫邻居,所以C7是跳点,B8处继续向上搜索结束。
从openlist中取出C7点。需要搜索右上,右,上三个方向。水平搜索时发现被迫邻居D8,加入openlist。右上搜索时发现跳点G3(因其在上方搜索时发现具有被迫邻居节点的G2),将G3加入openlist。C7点处理结束。
取出G3点(为啥是G3,而不是D8点呢,因为从总代价来说G3比D8更小,总代价=已经走过的距离 + 估值,估值可采用曼哈顿距离或者高斯距离),需要搜索右上,右,上三个方向。向上搜索到跳点G2。
其余点路径在图上标注,就不再重复了。
(1) 若current方向是直线
i. 如果current左后方不可走且左方可走,则沿current左前方和左方寻找跳点。
ii. 如果current当前方向可走,则沿current方向寻找跳点。
iii. 如果current右后方不可走且右方可走,则沿current右前方和右方寻找跳点。
(2) 若current方向是斜线
i. 如果当前方向的水平分量可走,则沿current方向的水平分量方向寻找跳点。
ii. 如果当前方向可走,沿current方向寻找跳点。
iii. 如果当前方向的垂直分量可走,则沿current方向的垂直分量方向寻找跳点。
上述算法流程是建立在斜向不可穿过阻挡基础上。
5. 6. Actor-Critic算法
本文主要介绍如下几个内容:
首先我们肆誉还是回顾一下之前提到的REINFORCE算法:
在这个算法的第二步骤里面我们引入了“reward to go”这一项洞皮,该 表示了从当前的时间步t开始,所有的reward的期望之和。
我们可以把这个由casuality引出的期望称之为“true expected reward-to-go”, 之所以我们这里考虑的是期望,是因为裂颤段我们在实际中每个trajectory采样出来都是不一样的,我们需要把这不同的采样结果进行最后的平均以求期望。
优势函数(Advantage function)
我们在policy gradient的方法中为了降低variance,也考虑过引入一个基线来减少梯度的方差。这里我们更进一步一点,我们使用 代替原来的"reward to go",并且使用值函数(V function)去代替原来的baseline,这样我们就有了新的估计值,即优势函数。
与原始版本的baseline相比,原来的估计是无偏估计,但是在单个采样估计中具有很高的方差(variance),现在使用了优势函数之后可以降低方差。他们的比较如下图:
三个函数的比较:Q, V and A
在模型拟合的阶段,我们需要去评估结果,这个时候就要考虑去拟合Q, V 还是A。他们之间是有密切关系的:
其中对于Q函数他是在已经确定了 的情况下,并且已经产生了 ,即我们可以写为:
既然他们是有关系的,那么我们在实际使用的时候只需要计算一个期望函数 .
对于如何去评估一个Policy是好是坏,我们从 的定义可以知道,强化学习的目标函数其实就是这个值关于初始状态 的期望值。
这里有两种Monte Carlo的方法来评估:
第一种是在一个sample里面累积 reward,第二种还考虑了多个sample的平均。
我们从上面的第一种简单得到 的方法中得到了训练数据,我们就可以把评估的问题转换成了一个监督学习的问题:
形式化理解为,我们用同一个函数去拟合了很多很多的样本。
上面使用的方法会有很高的方差,在实际中我们是用一种近似的方式去减少方差:
这里我们是直接使用了之前的V值( )去近似,然后得到了训练数据的样本 ,这种方式称之为bootstrap。
有了前面的基础,介绍了如何去拟合 ,计算优势函数我们就能够导出Actor-critic算法,
上面的actor-critic算法里第一步还需要采样一整个trajectory。想要变成每次只采样一个状态就需要先引入Discount factors的概念。
因为值函数V的定义是当前状态以后所有反馈值的和,在有限步长的任务中没有问题,但是如果是一个无限步长的任务,那么这个值有可能是无限大的。因此需要引入一个折损系数 ,它的意义在于让离当前状态比较近的反馈值更重要,而离得比较远的可能不那么看重.
上面举了个例子,在机器人做操作这种episodic tasks的时候是有限步长的就不需要discount factor,但是另外一种continuous任务,就不需要设定episodic所以这种情况加入discount factor就很重要。
加入了Dicount factor,我们对应的目标函数也变化:
这种情况下MC policy gradients有两种选择:
第一种是直接从当前时间t开始加系数 ,而第二种是从最开始t=1就开始加系数 。然后再通过利用causasity去掉 之前的反馈值。这样最终两种写法的系数还是有一些差别。
一般情况下两种方式有两种不同的解释和应用场景。第二种写法是对应着带有死亡状态的MDP形式。系数从第一步就开始加入,这就意味着这种写法更在意从头开始的动作,对于往后的动作给的关注更少。
而第一种写法是从时刻t开始加系数,也就是说它会一直在意从当前时刻开始的动作。这种形式一般用在一直连续运动的场景里。
第一种写法实际上不是一个正确的加了discount factor后的写法。它相当于是对平均反馈值加了一个系数来减小方差,它去除掉那些距离太远的反馈值的影响,因为可能太远了已经没有了意义。当然这样会是平均反馈的有偏估计。
第一种写法实际中更常用,也就是作为减小方差的方式。而第二种写法能够向我们解释在经典的场景里discount factor的意义。
加入了discount factors之后的actor-critic算法可以采用对每个状态进行采用的形式,这样就有了online形式的算法,两种算法对比如下
在实际实现actor-critic算法的时候可以选择两种结构。一种是让策略函数与值函数分别训练。这样做可能比较简单而且稳定,但是这样就不能共享一些提取特征的网络层。第二种是两种函数共享一部分网络,这样就能够共享前面提取特征的部分。
实际中如果实现一个online形式的算法,最好的做法并不是对每一个状态都做一次更新,而是得到足够多的样本里作为一个batch来更新。因为这样能够减小更新的方差。而实现这样的方式也有两种,一种是同步的一种是异步的。
我们把AC和PG对比一下,AC一般会因为有critic所以有更低的方差,但是如果critic不好那么他的bias会很大。PG是没有bias的,但是他的方差很高。把他们结合在一起就能产生比较好的unbias,低方差的算法。
之前用到作为baseline的函数一直都是V,实际上Q也能够作为baseline。只不过这样做实际上得到的不是一个advantage函数,在期望上得到一个期望为0的函数。因为减小了这部分的值,就能够减小对应部分的方差。
但是期望为0直接带入得不到目标函数的梯度值,因此计算梯度值的时候还需要把 以期望的形式修正回来,这样目标函数梯度值的期望与原来保持一致
目前我们有两种得到advantage函数的形式,一种 是bootstrap的,有更低的方差,但是有比较高的偏差。第二种是蒙特卡洛采样减去估计值的 ,这样做没有偏差,但是方差比较大。因此我们需要想办法把这两种结合起来。
我们可以使用n-step和Eligibility trace的方法来改善。
使用n-step的直觉如下图:
6. java 对比图片相似度的算法。。说说想法也行
每张图分成四块,将每部分的图片混成一种纯色,对比这四个纯色可以筛掉大部分的图片
7. RAFT与PBFT
【一.raft算法】
因为网上已经有大量文章对raft算法进行过详细的介绍,因此这部分只会简单的阐述算法的基本原理和流程。raft算法包含三种角色,分别是:跟随者(follower),候选人(candidate)和领导者(leader)。集群中的一个节点在某一时刻只能是这三种状态的其中一种,这三种角色是可以随着时间和条件的变化而互相转换的。
raft算法主要有两个过程:一个过程是领导者选举,另一个过程是日志复制,其中日志复制过程会分记录日志和提交数据两个阶段。raft算法支持最大的容错故障节点是(N-1)/2,其中N为 集群中总的节点数量。
国外有一个动画介绍raft算法介绍的很透彻,链接地址在这里[1]。这个动画主要包含三部分内容,第一部分介绍简单版的领导者选举和日志复制的过程,第二部分内容介绍详细版的领导者选举和日志复制的过程,第三部分内容介绍的是如果遇到网络分区(脑裂),raft算法是如何恢复网络一致的。有兴趣的朋友可以结合这个动画来更好的理解raft算法。
【二.pbft算法】
pbft算法的提出主要是为了解决拜占庭将军问题。什么是拜占庭将军问题呢?拜占庭位于如今的土耳其的伊斯坦布尔,是古代东罗马帝国的首都。拜占庭罗马帝国国土辽阔,为了达到防御目的,每块封地都驻扎一支由将军统领的军队,每个军队都分隔很远,将军与将军之间只能靠信差传递消息。 在战争的时候,拜占庭军队内所有将军必需达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,左右将军们的决定影响将军们达成一致共识。在已知有将军是叛徒的情况下,其余忠诚的将军如何达成一致协议的问题,这就是拜占庭将军问题。
下图列出了raft算法和pbft算法在适用环境,通信复杂度,最大容错节点数和流程上的对比。
关于两个算法的适用环境和最大容错节点数,前文已经做过阐述,这里不再细说。而对于算法通信复杂度,为什么raft是o(n),而pbft是o(n^2)呢?这里主要考虑算法的共识过程。
对于raft算法,核心共识过程是日志复制这个过程,这个过程分两个阶段,一个是日志记录,一个是提交数据。两个过程都只需要领导者发送消息给跟随者节点,跟随者节点返回消息给领导者节点即可完成,跟随者节点之间是无需沟通的。所以如果集群总节点数为 n,对于日志记录阶段,通信次数为n-1,对于提交数据阶段,通信次数也为n-1,总通信次数为2n-2,因此raft算法复杂度为O(n)。
对于pbft算法,核心过程有三个阶段,分别是pre-prepare(预准备)阶段,prepare(准备)阶段和commit(提交)阶段。对于pre-prepare阶段,主节点广播pre-prepare消息给其它节点即可,因此通信次数为n-1;对于prepare阶段,每个节点如果同意请求后,都需要向其它节点再 广播parepare消息,所以总的通信次数为n (n-1),即n^2-n;对于commit阶段,每个节点如果达到prepared状态后,都需要向其它节点广播commit消息,所以总的通信次数也为n (n-1),即n 2-n。所以总通信次数为(n-1)+(n 2-n)+(n 2-n),即2n 2-n-1,因此pbft算法复杂度为O(n^2)。
流程的对比上,对于leader选举这块,raft算法本质是谁快谁当选,而pbft算法是按编号依次轮流做主节点。对于共识过程和重选leader机制这块,为了更形象的描述这两个算法,接下来会把raft和pbft的共识过程比喻成一个团队是如何执行命令的过程,从这个角度去理解raft算法和pbft的区别。
一个团队一定会有一个老大和普通成员。对于raft算法,共识过程就是:只要老大还没挂,老大说什么,我们(团队普通成员)就做什么,坚决执行。那什么时候重新老大呢?只有当老大挂了才重选老大,不然生是老大的人,死是老大的鬼。
对于pbft算法,共识过程就是:老大向我发送命令时,当我认为老大的命令是有问题时,我会拒绝执行。就算我认为老大的命令是对的,我还会问下团队的其它成员老大的命令是否是对的,只有大多数人(2f+1)都认为老大的命令是对的时候,我才会去执行命令。那什么时候重选老大呢?老大挂了当然要重选,如果大多数人都认为老大不称职或者有问题时,我们也会重新选择老大。
8. 拍照硬件和算法谁更重要看完OPPO三星苹果的对比就知道
要知道,现在影像体验已经成为了用户购机的关键因素之一,尤其是手机摄像头像素从11万到200万、500万再到如今甚至有上亿像素,再加上各大厂商纷纷在影像算法方面的调教,手机的综合成像质量也不断在提高,现在甚至可以取代卡片机成为人们在身边最好的“摄影器材”。下面我们就以三星S21 Ultra、OPPO Reno6 Pro+以及iPhone 12 Pro为例,来看看手机的影像体验,究竟“进化”到什么程度。
(左:三星S21 Ultra,中:OPPO Reno6 Pro+,右:iPhone 12 Pro)
摄像头配置大比拼:谁是真“硬核”
首先,我们还是来看看这三款手机的硬件配置,先从iPhone 12 Pro说起,因为它的硬件配置看起来似乎是里面相对最差的,三颗摄像头(主摄、超广角以及长焦)均为1200万像素;而OPPO Reno6 Pro+则搭载5000万像素主摄(索尼IMX766旗舰传感器)+1600万像素超广角镜头+1300万像素长焦镜头+200万像素微距镜头;而三星S21 Ultra搭载的则是1.08亿像素主摄 +1000万像素长焦镜头+1000万像素潜望式长焦镜头+1200万像素超广角镜头,所以毫无疑问硬件配置是三星 OPPO iPhone。
(中:三星S21 Ultra,左:OPPO Reno6 Pro+,右:iPhone 12 Pro)
人像实拍:谁最会玩?
不过手机拍照并不只是看硬件,各大厂商在算法方面的调教也是十分重要的,那么下面我们一起来看看经过算法调教以后,三款机型各自的表现如何?首先,我们来看看人像方面的实拍照片,下面这组对比图上,很明显拥有焕采光斑人像视频加持的OPPO Reno6 Pro+拍出来的照片更具质山配感,大家可以看到前景小姐姐的人物主体很清晰,还有就是色彩还原很到位,小姐姐的肤色以及衣服的颜色已经很接近肉眼所见了,而背景的虚化模拟出光斑的效果,照片的质感就上来了;而三星S21 Ultra拍出来的照片人物和背景之间的过渡以及虚化做得并不是很到位;至于iPhone 12 Pro拍出来的照片只能说“平平无奇”。反正在人像这个环节,我觉得OPPO Reno6 Pro+ 三星S21 Ultra iPhone 12 Pro。
(人像实拍 左:三星S21 Ultra,中:OPPO Reno6 Pro+,右:iPhone 12 Pro)
白天大场景:谁更好“色”
接着,更换为白天的大场景,再来看看它们三者的实拍效果。从下面这组对比照片来看,尽管当时没有大太阳,但是三部手机拍出来的照片总体表现都不错,不过在一些细节的处理上,我觉得还是OPPO Reno6 Pro+更好,比如遮雨棚的顶部的红色、紫色、黄色、绿色、蓝色逗枯指都一一精准还原了,还有就是光线的捕捉也很到位,看出了光影的效果;而三星S21 Ultra在光线的捕捉上则稍微差了一些,整体画面比较暗;至于iPhone 12 Pro在光线和色彩方面的调教还有待改进,整体画面偏暗,而且画面上的色彩也不够讨好眼球,说白了就是看着照片有种“食欲不强”的感觉。
(白天大场景实拍 左:三星S21 Ultra,中:OPPO Reno6 Pro+,右:iPhone 12 Pro)
夜景实拍:谁才是真正的“暗影精灵”
最后,我们来看看夜景方面的实拍对比。大家可以看看我用红圈标记的地方,它们能够充分表现出这三款手机的综合夜景实拍能力。其实夜景拍摄,无非就是对光线的把控,尤其是高光的压制,毕竟拍摄夜景照片的时候,不可控的光源太多,而且那些高亮的灯光,往往会一“亮”毁所有。从实拍的效果来看,OPPO Reno6 Pro的高光压制以及色彩还原是最到位的,高楼的广告灯箱上的字样都能够看得十分清晰,而三星和iPhone的则有过分提亮的“嫌疑”,所以照片上的广告灯箱字样不是看得很清楚败宏。
(白天大场景实拍 左:三星S21 Ultra,中:OPPO Reno6 Pro+,右:iPhone 12 Pro)
综合来看,手机影像在各大厂商不断的调教下,已经成为了当下人们最爱的“摄影器材”,毕竟它太方便了,看见什么好吃的、漂亮的,随手都可以从口袋里掏出手机“咔嚓”将它们定格下来,并且分享到各大社交平台。在影像实测对比中,我们可以发现,其实手机影像最重要的还是软硬件相互相成,才可以发挥出最大的“功效”。OPPO Reno6 Pro+在色彩还原以及细节处理上做得十分出色,而三星S21 Ultra则在解析力上有不错的提升,至于iPhone 12 Pro则相对比较中庸,没有很亮眼的表现。最近有购机需求的网友不妨关注一下,这三款机型各有千秋,消费者可以根据自己的实际需求来选购。
9. Q-Learning 和 SARSA 算法对比
Q-learning算法的过程可以根据下面的步骤:
首先,我们会初始化一个Q-table,可以是全0或者是其他的数值,一般咐裤悉都是全0,然后我们设定训练的轮数episodes,这里从初始状态直到终止状态算作一轮。那么在每一轮中,我们会有一个初始状态,然后会不断的采取动作,这里每一个动作叫做一个step。在每一个step中,我们根据当前的状态通过一定的策略选择动作A,这里的策略可能是以0.9的概率选择Q-table中当前状态对应的q值最大的动作,以0.1的概率选择随机动作。然后在选择动作A之后,我们可以得到奖励值R和新的状态S,那么当前q值的更新基于下面的公式:
其中alpha是学习率,表示保留之前训练效果的多少,学习速率α越大,保留之前训练的效果就越少,如果学习率是1的话,完全用新学到的q值替换掉了原来的q值。R是从环境获得的奖励,而gamma是折现系数,我们会选择到新状态之后能够得到的最大的Q值来更新当前的Q值。
对于Q-learning来说,
1.在状态s'时,只是计算了 在s'时要采取哪个a'可以得到更大的Q值,并没有真的采取这个动作a'。
2.动作a的选取是根据当前Q网络以及策略(e-greedy),即每一步都会根据当前的状况选择一个动作A,目标Q值的计算是根据Q值最大的动作a'计算得来,因此为off-policy学习。
SARSA算法根Q-learning很像,也是基于Q-table,但是不同的是,在每一个episode的每一个step,我们会确定下一步采取的动作,而不是在下一个衡乎step开始时动态的确定step,算法步骤看下面的图示。
因此对于SARSA来说
1.在状态s'时,就知道了要采取哪个a',并真的采取了这个动作。
2.动作a的选取遵循e-greedy策略,目标Q值的计算也是根据(e-greedy)策略得到的动作a'纯档计算得来,因此为on-policy学习。