CS231n笔记(3)线性分类器:最优化

版权声明:本文为博主原创文章,如未特别声明,均默认使用CC BY-SA 3.0许可。 https://blog.csdn.net/Geek_of_CSDN/article/details/81840055

先说下最优化是什么:最优化是寻找能使损失函数值最小化的参数 W 的过程。

铺垫:之后会把映射部分拓展成比线性函数更加复杂的函数(例如神经网络),但是损失函数和最优化过程这两个部分不会有很大改动,所以这两个部分的基础一定要打好。

损失函数可视化

因为我们现在的损失函数通常定义在高维空间里面,比较难可视化,所以要在1或者2个维度上对高维空间进行切片来可视化。例如随机生成一个权重矩阵 W ,这个矩阵对应高维空间里面的一个点,然后随机生成一个方向矩阵 W 1 (记住,矩阵在某种程度上就是向量),再沿着这个生成的方向来计算出这个方向上的损失值(就是计算 L ( W + a @ 1 ) 的值)来生成图表。图标x轴是 a ,y轴是损失函数值。如果是两个唯独的话就是计算 L ( W + a W 1 + b W 2 ) ,然后在给出的二维图像里面用红到蓝的颜色来表示损失值大小(和热度图差不多)。例子:

这里写图片描述

一个无正则化的多类SVM的损失函数的图示。左边和中间只有一个样本数据,右边是CIFAR-10中的100个数据。左:a值变化在某个维度方向上对应的的损失值变化。中和右:两个维度方向上的损失值切片图,蓝色部分是低损失值区域,红色部分是高损失值区域。注意损失函数的分段线性结构。多个样本的损失值是总体的平均值(不记得的话看回公式就知道了),所以右边的碗状结构是很多的分段线性结构的平均(比如中间这个就是其中之一)。

上面图左边的分段结构可以用损失函数的计算公式来解释,因为对于单个数据来说,损失函数的计算方法是:

L i = j y i [ max ( 0 , ω j T x i ω y i T x i + 1 ) ]

说白了就是计算在错误的类别上模型的评分和正确分类上的应有的评分进行相减(并加上边界值1,这是为了保证在错误类别上的评分比正确类别上的评分小边界值1),如果大于0那么就是不理想的,也就是说要把相减之后的结果加到最终损失值里面去。下面就是例子:

假设一个简单的数据集,其中包含3个只有1个维度的点,数据集数据点有3个类别,那么SVM的损失至计算如下:

L 0 = max ( 0 , ω 1 T x 0 ω 0 T x 0 + 1 ) + max ( 0 , ω 2 T x 0 ω 0 T x 0 + 1 ) L 1 = max ( 0 , ω 0 T x 1 ω 1 T x 1 + 1 ) + max ( 0 , ω 2 T x 1 ω 1 T x 1 + 1 ) L 2 = max ( 0 , ω 0 T x 2 ω 2 T x 2 + 1 ) + max ( 0 , ω 1 T x 2 ω 2 T x 2 + 1 ) L = ( L 0 + L 1 + L 2 ) / 3

因为例子是一维的,所以数据 x i 和权重 ω j 都是数字。最后出来的东西可以变成这幅图:

这里写图片描述

可以看到和小于0的部分都被截掉了。上面的x轴就是权重,y轴就是损失值。完整的SVM数据损失是30730维的(因为原数据是 10 × 3073 格式的)。

上面的损失函数可视化之后的图形是明显的简单的凸函数,在把映射关系变成神经网络之后损失函数可视化之后出来的图像就不再是凸函数了,而是更加崎岖的曲线(所以会出现局部最优的情况)。

注意,上面因为max函数的存在使得部分损失函数变得不可导,这些不可导点上没有梯度,所以只能够用次梯度(subgradient)。

最优化

损失函数用来量化某个权重集 W 的质量,而最优化的目标就是找到能够最小化损失函数值的 W 。注意:最终目标是优化神经网络,而这里的例子是SVM损失函数,是一个凸函数问题,所以这里的知识点不能够直接用在神经网络上。

随机搜索

效果看脸。。。可以用来测试人品(雾)。这个方法就是通过尝试不同的权重,然后确定哪个最好:

# 假设X_train的每一列都是一个数据样本(比如3073 x 50000)
# 假设Y_train是数据样本的类别标签(比如一个长50000的一维数组)
# 假设函数L对损失函数进行评价

bestloss = float("inf") # Python assigns the highest possible float value
for num in xrange(1000):
  W = np.random.randn(10, 3073) * 0.0001 # generate random parameters
  loss = L(X_train, Y_train, W) # get the loss over the entire training set
  if loss < bestloss: # keep track of the best solution
    bestloss = loss
    bestW = W
  print 'in attempt %d the loss was %f, best %f' % (num, loss, bestloss)

# 输出:
# in attempt 0 the loss was 9.401632, best 9.401632
# in attempt 1 the loss was 8.959668, best 8.959668
# in attempt 2 the loss was 9.044034, best 8.959668
# in attempt 3 the loss was 9.278948, best 8.959668
# in attempt 4 the loss was 8.857370, best 8.857370
# in attempt 5 the loss was 8.943151, best 8.857370
# in attempt 6 the loss was 8.605604, best 8.605604
# ... (trunctated: continues for 1000 lines)

拿到随机搜索中找到的最好的权重,然后跑测试集:

# 假设X_test尺寸是[3073 x 10000], Y_test尺寸是[10000 x 1]
scores = Wbest.dot(Xte_cols) # 10 x 10000, the class scores for all test examples
# 找到在每列中评分值最大的索引(即预测的分类)
Yte_predict = np.argmax(scores, axis = 0)
# 以及计算准确率
np.mean(Yte_predict == Yte)
# 返回 0.1555

准确率是15.5%,比完全瞎猜的10%好。。。看来跑这段代码的人没充钱。

迭代优化

这个方法的核心思路就是从随机生成的矩阵开始,对它进行迭代取优,使这个矩阵最终输出的损失值减少到理想程度。

蒙眼徒步者的比喻:一个助于理解的比喻是把你自己想象成一个蒙着眼睛的徒步者,正走在山地地形上,目标是要慢慢走到山底。在CIFAR-10的例子中,这山是30730维的(因为W是3073x10)。我们在山上踩的每一点都对应一个的损失值,该损失值可以看做该点的海拔高度。

随机本地搜索

这个方法是每走一步都随机尝试几个方向,如果某方向是向山下的,那么就向这个方向走一步(将这个方向更新到权重矩阵中)。就是从随机生成的权重矩阵 W 开始,生成一个随机的扰动 σ W ,当 W + σ W 的损失值变低,那么就更新 W W + σ W

W = np.random.randn(10, 3073) * 0.001 # 生成随机初始W
bestloss = float("inf")
for i in xrange(1000):
  step_size = 0.0001
  Wtry = W + np.random.randn(10, 3073) * step_size
  loss = L(Xtr_cols, Ytr, Wtry)
  if loss < bestloss:
    W = Wtry
    bestloss = loss
  print 'iter %d loss is %f' % (i, bestloss)

最后大概可以得到 21.4 的准确率(贫僧没跑代码,这里这些数据都是来自原文的。。。)。

跟随梯度

这种方法是通过数学的方法来计算出能够降低损失函数的损失值的方向。这种方法就是直接计算出损失函数的梯度。就好像是让登山者直接用脚来感受下山的倾斜程度,然后往最陡峭的地方走。

梯度其实是函数斜率的一般化表达,是一个向量。在输入空间中,梯度是各个斜率组成的向量(或者导数)。如果是一维的话求导就是:

d f ( x ) d x = lim h 0 f ( x + h ) f ( x ) h

如果函数有多个参数的话那就是偏导,梯度就是每个维度上偏导数形成的向量。

计算梯度有两种方法:1. 缓慢地近似(数值梯度法),实现相对简单;2. 分析梯度法,计算快,结果精确,但是实现起来容易出错,并且需要微分
下面就是详细的介绍。

利用有限差值计算梯度

计算梯度的方法上面有提到(就是那个求导的公式,一维的话)。下面代码会接受函数f和向量x,计算f的梯度然后返回函数f在x处的梯度:

def eval_numerical_gradient(f, x):
  """  
  一个f在x处的数值梯度法的简单实现
  - f是只有一个参数的函数
  - x是计算梯度的点
  """ 

  fx = f(x) # 在原点计算函数值
  grad = np.zeros(x.shape)
  h = 0.00001

  # 对x中所有的索引进行迭代
  it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
  while not it.finished:

    # 计算x+h处的函数值
    ix = it.multi_index
    old_value = x[ix]
    x[ix] = old_value + h # 增加h
    fxh = f(x) # 计算f(x + h)
    x[ix] = old_value # 存到前一个值中 (非常重要)

    # 计算偏导数
    grad[ix] = (fxh - fx) / h # 坡度
    it.iternext() # 到下个维度

  return grad

代码对所有维度进行迭代,在每个维度上产生一个很小的变化h,通过观察函数变化计算函数在该维度上的偏导数。最后所有梯度存储在变量grad中。

在数学公式里面h的取值是趋于0的,但是实际中用很小的值就够了(例如上面就是用了1e-5),在计算不出错的前提下h越小越好。而且用中心差值公式效果会更加好(就是 f ( x + h ) f ( x h ) 2 h

之后就可以用上面定义的函数来计算任意点上的梯度了,例如计算权重在某些随机点上对应的CIFAR-10损失函数值的梯度:

# 要使用上面的代码我们需要一个只有一个参数的函数
# (在这里参数就是权重)所以也包含了X_train和Y_train
def CIFAR10_loss_fun(W):
  return L(X_train, Y_train, W)

W = np.random.rand(10, 3073) * 0.001 # 随机权重向量
df = eval_numerical_gradient(CIFAR10_loss_fun, W) # 得到梯度

L函数的内容要参考之前的笔记,总之知道能够返回损失函数的值就是了。知道了损失函数在每个维度(这里的一个维度就相当于是多元函数里面的某个变量)上的斜率,就可以进行更新:

loss_original = CIFAR10_loss_fun(W) # 初始损失值
print 'original loss: %f' % (loss_original, )

# 查看不同步长的效果
for step_size_log in [-10, -9, -8, -7, -6, -5,-4,-3,-2,-1]:
  step_size = 10 ** step_size_log
  W_new = W - step_size * df # 权重空间中的新位置
  loss_new = CIFAR10_loss_fun(W_new)
  print 'for step size %f new loss: %f' % (step_size, loss_new)

# 输出:
# original loss: 2.200718
# for step size 1.000000e-10 new loss: 2.200652
# for step size 1.000000e-09 new loss: 2.200057
# for step size 1.000000e-08 new loss: 2.194116
# for step size 1.000000e-07 new loss: 2.135493
# for step size 1.000000e-06 new loss: 1.647802
# for step size 1.000000e-05 new loss: 2.844355
# for step size 1.000000e-04 new loss: 25.558142
# for step size 1.000000e-03 new loss: 254.086573
# for step size 1.000000e-02 new loss: 2539.370888
# for step size 1.000000e-01 new loss: 25392.214036

注意是在梯度负方向上更新,因为是要让损失值降低而不是升高。梯度决定了走的方向,而步长决定了走的距离。步长也叫学习率,这个会是比较难设定的参数,如果太小那么训练时间会太长,如果太大那么又可能走不到最低的损失值(直接跳过去了,甚至绕着损失值最低的地方来回走)。

上面的方法有个效率的问题,因为每走一步就要计算30731次损失函数的梯度,所以计算量会比较大。计算量是和参数的数量线性相关的,而现在的神经网络很容易就有上千万个参数,所以如果都直接用上面的方法的话效率会很低。

微分分析计算梯度

上面的有限差值近似计算梯度缺点是计算结果只是近似(h的取值不是无穷小),且耗费资源太多。微分分析计算梯度是用微分来分析,是直接用计算梯度的公式来计算,速度会很快,但是容易出错。所以通常会两种方法一起用,然后用数值梯度法的值来检查微分分析梯度法计算出来的值(梯度检查)。

例子:

在某点上的SVM损失函数计算:

L i = j y i [ max ( 0 , ω j T x i ω y i T x i + Δ ) ]

对函数进行微分,例如对 ω y i 进行微分:

ω y i L i = ( j y i 1 ( ω j T x i ω y i T x i + Δ > 0 ) ) x i

上面的1类似条件选择了,只要括号内条件为真那么函数值就等于1,否则等于0。代码实现思路:计算出没有满足边界值的分类(这里原本的公式是计算损失值,所以是“不满足”)的数量然后乘以 x i

上面求出来的梯度只是对应正确分类的 W 的行向量的梯度,如果是 j y i 行的话就是:

ω j L i = 1 ( ω j T x i ω y i T x i + Δ > 0 ) x i

梯度下降

有了上面的公式之后就可以计算损失函数的梯度了,程序重复地计算梯度然后对参数进行更新的过程就是梯度下降。下面是常见的实现方式:

# 普通的梯度下降

while True:
  weights_grad = evaluate_gradient(loss_fun, data, weights)
  weights += - step_size * weights_grad # 进行梯度更新

上面这种方法在训练集中数据是相关的情况下效果会比较好,这是因为如果数据集中很多数据都是相似甚至是相同的话,那么只需要拿一小部分有代表性的数据,然后在这些抽出来的数据上进行梯度计算就可以得到整个数据集的梯度。实际情况下数据集里面的数据之间是不会完全相同的,那么用上面这种方法的时候就可以先拿一部分数据来计算梯度,然后去近似整个数据集的梯度。这样的好处是重复进行小批量数据的梯度计算可以更快实现收敛,并更加频繁地更新网络的参数。

小批量数据策略的极端情况就是每个批量里面只有一个数据样本,这种策略被叫做“随机梯度下降”(Stochastic Gradient Descent 简称SGD),有时也被称为在线梯度下降(效果并不是很好。。。因为有时候单个数据的梯度并不是整个数据集的梯度,所以如果将梯度下降的“路线”画出来的话会发现路线是弯弯曲曲的,而且还不一定往全局最优的方向走)。这种策略比较少用(太极端了。。。),因为向量化操作的代码一次计算100个数据比100次计算1个数据高效很多(所以最近显卡的地位越来越高了。。。)。有时人们会用SGD来指代小批量数据梯度下降(或者用MGD指代,比较少见用BGD指代的)。小批量数据的多少是一个超参数,但是通常不用通过交叉验证来进行调参。这个超参数一般由存储器的大小限制来决定的,或者干脆设置为同样大小,例如32、64。设置成2的指数是因为实际中很多向量化操作实现的时候如果输入数据量是2的指数那么运算会更快。

补充

信息流:

这里写图片描述

上面这张图可以直接总结出损失函数L的构成,大概看下就行了。

参考

英文原文笔记
CS231n课程笔记翻译:最优化笔记(上):大部分内容都是从这里来的。有翻译的话还是比较方便的,但是贫僧总觉得有些地方翻译得有点怪(某些地方还是有一点翻译腔。。。),所以就算是看翻译也建议对照着原文来看(但是总体上而言质量还是很高的,英语苦手。。。还是直接看翻译吧)

猜你喜欢

转载自blog.csdn.net/Geek_of_CSDN/article/details/81840055