yuzhenning / Udacity-Deep-Learning

This is for Udacity Deep Learning Nano-Degree only
0 stars 0 forks source link

Uda-DL-Lesson-4 神经网络入门 #1

Open yuzhenning opened 7 years ago

yuzhenning commented 7 years ago

0 Announcement 声明

本文件仅用于个人学术交流,版权所有udacity培训机构 由Yu Zhenning 个人整理

yuzhenning commented 7 years ago

1 最简单的神经网络

输出非0即1,输出单元的输入经过了一个激活函数 f(h) 在此处就是指阶跃函数。 输出单元返回的是 f(h) 的结果,其中 h 是输出单元的输入: h=∑iw_ix_i 这个架构最酷的一点,也是使得神经网络可以实现的原因,就是激活函数 f(h) 可以是 任何函数,并不只是上面提到的阶跃函数。 常见激活函数还有对数几率(又称作 sigmoid),tanh 和 softmax。这节课中我们主要使用 sigmoid 函数:

sigmoid 函数值域是 0 到 1 之间,它的输出还可以被解释为成功的概率。实际上,用 sigmoid 函数作为激活函数的结果,跟对数几率回归是一样的。

简单网络练习:

接下来你要用 NumPy 来计算一个简单网络的输出,它有两个输入节点,一个输出节点,激活函数是 sigmoid。你需要做的有:

实现 sigmoid 激活函数 计算神经网络输出 sigmoid 函数公式是:

sigmoid(x)=1/(1+e^{-x})

指数你可以使用 NumPy 的指数函数 np.exp。

这个网络的输出为:

y=f(h)=sigmoid(∑i​​ *w​i *​​ x_​i​​ +b)

要计算加权求和,你可以让元素相乘再相加,或者使用 NumPy 的 点乘函数.

yuzhenning commented 7 years ago

2.1 单层感知器

output = sigmoid(np.dot(weights, inputs) +bias) 输出端为sigmoid函数,其中变量为:weights, inputs(数据裤)和bias。 案例python代码:

 import numpy as np # 导入numpy库
 def sigmoid(x):  # 定义sigmod函数
      res = 1/(1+np.exp(-x))
      return res
      # TODO: Implement sigmoid function
      # pass

 inputs = np.array([0.7, -0.3]) # 输入端array定义
 weights = np.array([0.1, 0.8]) # 权重array定义
 bias = -0.1  # 定义偏置值

 output = sigmoid(np.dot(weights, inputs) +bias)

 print('Output:')
 print(output)
yuzhenning commented 7 years ago

2.2 学习权重

你了解了如何使用感知器来构建 AND 和 XOR 运算,但它们的权重都是人为设定的。如果你要进行一个运算,例如预测大学录取结果,但你不知道正确的权重是什么,该怎么办?你要从样本中学习权重,然后用这些权重来做预测。

要了解我们将如何找到这些权重,可以从我们的目标开始考虑。我们想让网络做出的预测与真实值尽可能接近。为了能够衡量,我们需要有一个指标来了解预测有多差,也就是误差 (error)。一个普遍的指标是误差平方和 sum of the squared errors (SSE): E=1/2​​ ∑μ ​​ ∑​j [y_j^μ​​ −​y_j^​μ​​ ]^​2

yuzhenning commented 7 years ago

2.3 梯度下降

如 Luis 所说,用梯度下降,我们通过多个小步骤来实现目标。在这个例子中,我们希望一步一步改变权重来减小误差。借用前面的比喻,误差就像是山,我们希望走到山下。下山最快的路应该是最陡峭的那个方向,因此我们也应该寻找能够使误差最小化的方向。我们可以通过计算误差平方的梯度来找到这个方向。

要计算变化率,我们要转向微积分,具体来说是导数。一个函数 f(x) 的导函数 f​′​(x) 给到你的是 f(x) 在 x 这一点的斜率。例如 x2 ,x2​​ 的导数是 f′​​ (x)=2x。所以,在 x=2 这个点斜率 f′​​ (2)=4。画出图来就是: 梯度就是对多变量函数导数的泛化。我们可以用微积分来寻找误差函数中任意一点的梯度,它与输入权重有关,下一节你可以看到如何推导梯度下降的步骤。 下面我画了一个拥有两个输入的神经网络误差示例,相应的,它有两个权重。你可以将其看成一个地形图,同一条线代表相同的误差,较深的线对应较大的误差。

每一步,你计算误差和梯度,然后用它们来决定如何改变权重。重复这个过程直到你最终找到接近误差函数最小值的权重,即中间的黑点。

注意事项

因为权重会走向梯度带它去的位置,它们有可能停留在误差小,但不是最小的地方。这个点被称作局部最低点。如果权重初始值有错,梯度下降可能会使得权重陷入局部最优,例如下图所示。

有方法可以避免这一点,被称作 momentum.

yuzhenning commented 7 years ago

2.4 梯度下降: 代码

之前我们看到一个权重的更新可以这样计算: Δwi​ =ηδx​i 这里 error term δ 是指δ =(y−​y​^​​ )f​′​​ (h)=(y−y​^​​ )f​′​​ (∑w​i​​ x​i​​) 记住,上面公式中 (y−y​^​​ ) 是输出误差,f′​​ (h) 是激活函数 f(h) 的导函数,我们把这个导函数称做输出的梯度。

现在假设只有一个输出单元,我来把这个写成代码。 我们还是用 sigmoid 来作为激活函数 f(h)。

yuzhenning commented 7 years ago

2.5 梯度下降:实验代码

Defining the sigmoid function for activations 定义 sigmoid 激活函数

 def sigmoid(x):
 return 1/(1+np.exp(-x)) 

 # Derivative of the sigmoid function  
 # 激活函数的导数
 def sigmoid_prime(x):
 return sigmoid(x) * (1 - sigmoid(x))

 # Input data 
 # 输入数据
 x = np.array([0.1, 0.3])

 # Target 目标(输出)
 y = 0.2

 # Input to output weights  
 # 输入到输出的权重
 weights = np.array([-0.8, 0.5])

 # The learning rate, eta in the weight step equation  
 # 权重更新的学习率
 learnrate = 0.5

 # the linear combination performed by the node (h in f(h) and f'(h)) 
 # 输入和权重的线性组合
 h = x[0]*weights[0] + x[1]*weights[1]
 # or h = np.dot(x, weights) 

 # The neural network output (y-hat) 
 # 神经网络输出
 nn_output = sigmoid(h)

 # output error (y - y-hat)  
 #  输出误差
 error = y - nn_output

 # output gradient (f'(h))  
 #  输出梯度
 output_grad = sigmoid_prime(h)

 # error term (lowercase delta)
 error_term = error * output_grad

 # Gradient descent step  
 #  梯度下降一步
 del_w = [ learnrate * error_term * x[0], learnrate * error_term * x[1]]
 # or del_w = learnrate * error_term * x
yuzhenning commented 7 years ago

2.6 作业代码:梯度下降 ( important !!! )

代码总结: output = sigmoid( np.dot(x,weights) )

代码总结:error = y - output

 import numpy as np
 def sigmoid(x):
 # Calculate sigmoid
 return 1/(1+np.exp(-x))

 def sigmoid_prime(x):
 # Derivative of the sigmoid function
 return sigmoid(x) * (1 - sigmoid(x))

 learnrate = 0.5
 x = np.array([1, 2, 3, 4])
 y = np.array(0.5)

 # Initial weights
 weight = np.array([0.5, -0.5, 0.3, 0.1])

 ### Calculate one gradient descent step for each weight
 ### Note: Some steps have been consilated, so there are
 ###       fewer variable names than in the above sample code

 # TODO: Calculate the node's linear combination of inputs and weights
 h = np.dot(x , weight)

 # TODO: Calculate output of neural network
 nn_output = sigmoid(h)

 # TODO: Calculate error of neural network
 error = y - nn_output

 # TODO: Calculate the error term
 #       Remember, this requires the output gradient, which we haven't
 #       specifically added a variable for.
 output_grad = sigmoid_prime(h)
 error_term = error * output_grad

 # TODO: Calculate change in weights
 del_w = learnrate * error_term * x

 print('Neural Network output:')
 print(nn_output)
 print('Amount of Error:')
 print(error)
 print('Change in Weights:')
 print(del_w)
yuzhenning commented 7 years ago

2.7 实现梯度下降

现在我们知道了如何更新我们的权重: Δwij​​ =ηδ​j x_​i​​ ,

你看到的是如何实现一次更新,那我们如何把代码转化为能够计算多次权重更新,使得我们的网络能够真正学习呢?

作为示例,我们拿一个研究生学院录取数据,用梯度下降训练一个网络。数据可以在这里找到。数据有三个输入特征:GRE 分数、GPA 分数和本科院校排名(从 1 到 4)。排名 1 代表最好,排名 4 代表最差。

我们的目标是基于这些特征来预测一个学生能否被研究生院录取。这里,我们将使用有一个输出层的网络。用 sigmoid 做为激活函数。

yuzhenning commented 7 years ago

2.8 数据清理

你也许认为有三个输入单元,但实际上我们要先做数据转换。rank 是类别特征,其中的数字并不表示任何相对的值。排名第 2 并不是排名第 1 的两倍;排名第 3 也不是排名第 2 的 1.5 倍。因此,我们需要用 dummy variables 来对 rank 进行编码。把数据分成 4 个新列,用 0 或 1 表示。排名为 1 的行对应 rank_1 列的值为 1 ,其余三列的值为 0;排名为 2 的行对应 rank_2 列的值为 1 ,其余三列的值为 0,以此类推。

我们还需要把 GRE 和 GPA 数据标准化,也就是说使得它们的均值为 0,标准偏差为 1。因为 sigmoid 函数会挤压很大或者很小的输入,所以这一步是必要的。很大或者很小输入的梯度为 0,这意味着梯度下降的步长也会是 0。由于 GRE 和 GPA 的值都相当大,我们在初始化权重的时候需要非常小心,否则梯度下降步长将会消失,网络也没法训练了。相对地,如果我们对数据做了标准化处理,就能更容易地对权重进行初始化。

这只是一个简单介绍,你之后还会学到如何预处理数据,如果你想了解我是怎么做的,可以查看下面编程练习中的 data_prep.py 文件。

yuzhenning commented 7 years ago

2.9 练习题原始数据生成 data_prep.py 文件

 import numpy as np
 import pandas as pd

 admissions = pd.read_csv('binary.csv')

 #### Make dummy variables for rank
 data = pd.concat([admissions, pd.get_dummies(admissions['rank'], prefix='rank')], axis=1)
 data = data.drop('rank', axis=1)

 #### Standarize features
 for field in ['gre', 'gpa']:
     mean, std = data[field].mean(), data[field].std()
     data.loc[:,field] = (data[field]-mean)/std

 #### Split off random 10% of the data for testing
 np.random.seed(42)
 sample = np.random.choice(data.index, size=int(len(data)*0.9), replace=False)
 data, test_data = data.ix[sample], data.drop(sample)

 #### Split into features and targets
 features, targets = data.drop('admit', axis=1), data['admit']
 features_test, targets_test = test_data.drop('admit', axis=1), test_data['admit']
yuzhenning commented 7 years ago

现在数据已经准备好了,我们看到有六个输入特征:gre、gpa,以及四个 rank 的虚拟变量 (dummy variables)。

2.10 均方差

这里我们要对如何计算误差做一点小改变。我们不计算 SSE,而是用误差平方的均值(mean of the square errors,MSE)。现在我们要处理很多数据,把所有权重更新加起来会导致很大的更新,使得梯度下降无法收敛。为了避免这种情况,你需要一个很小的学习率。这里我们还可以除以数据点的数量 m 来取平均。这样,无论我们有多少数据,我们的学习率通常会在 0.01 to 0.001 之间。我们用 MSE(下图)来计算梯度,结果跟之前一样,只是取了平均而不是取和。

yuzhenning commented 7 years ago

2.11 这是用梯度下降来更新权重的算法概述:

你也可以对每条记录更新权重,而不是把所有记录都训练过之后再取平均。

这里我们还是使用 sigmoid 作为激活函数 f(h)=1/(1+e−h)

sigmoid 的梯度是: f′ (h)=f(h)(1−f(h))

其中 h 是输出单元的输入: h=∑i​​ w​i​​ x_​i ​​

yuzhenning commented 7 years ago

2.12 用 NumPy 来实现

这里大部分都可以用 NumPy 很方便的实现。

首先你需要初始化权重。我们希望它们比较小,这样输入在 sigmoid 函数那里可以在接近 0 的位置,而不是最高或者最低处。很重要的一点是要随机地初始化它们,这样它们有不同的初始值,是发散且不对称的。

所以我们用一个中心为 0 的正态分布来初始化权重, 此正态分布的标准差(scale 参数)最好使用 1/√​n,其中 n 是输入单元的个数。这样就算是输入单元的数量变多,sigmoid 的输入还能保持比较小。

    weights = np.random.normal(scale=1/n_features**.5, size=n_features)

NumPy 提供了一个可以让两个数组做点乘的函数,它可以让我们方便地计算 h。点乘就是把两个数组的元素对应位置相乘之后再相加。

    # input to the output layer 
    # 输出层的输入
     output_in = np.dot(weights, inputs)

最后我们可以用 weights += ... 更新 Δw​i​​ 和 w​i​​ ,weights += ... 是 weights = weights + ... 的简写。

yuzhenning commented 7 years ago

效率提示

因为这里我们用的是 sigmoid 函数,你可以节省一些计算。对于 sigmoid 函数来说,f′​(h)=f(h)(1−f(h))。也就是说一旦你有了 f(h),你就可以直接用它的值来计算误差的梯度了。

yuzhenning commented 7 years ago

2.13 编程练习

接下来,你要实现一个梯度下降,用录取数据来训练它。你的目标是训练一个网络直到你达到训练数据的最小的均方差 mean square error (MSE)。你需要实现:

在你写完这几部分之后,点击“测试答案”按钮来进行训练,均方差会被打印出来,同时也会打印出测试集的准确率,即录取情况的正确预测比率。

你可以任意调节超参数 hyperparameters 来看下它对均方差 MSE 有什么影响。

yuzhenning commented 7 years ago

2.14 编程练习 code答案 ( Important !!! )

import numpy as np
from data_prep import features, targets, features_test, targets_test

def sigmoid(x):
# Calculate sigmoid
return 1 / (1 + np.exp(-x))

# TODO: We haven't provided the sigmoid_prime function like we did in
#       the previous lesson to encourage you to come up with a more
#       efficient solution. If you need a hint, check out the comments
#       in solution.py from the previous lecture.

# Use to same seed to make debugging easier
np.random.seed(42)

n_records, n_features = features.shape    
last_loss = None

# Initialize weights
weights = np.random.normal(scale=1 / n_features**.5, size=n_features)

# Neural Network hyperparameters
epochs = 1000
learnrate = 0.5

for e in range(epochs):
    del_w = np.zeros(weights.shape)
    for x, y in zip(features.values, targets):
        # Loop through all records, x is the input, y is the target

        # Activation of the output unit
        #   Notice we multiply the inputs and the weights here 
        #   rather than storing h as a separate variable 
        output = sigmoid(np.dot(x, weights))

        # The error, the target minus the network output
        error = y - output

        # The error term
        #   Notice we calulate f'(h) here instead of defining a separate
        #   sigmoid_prime function. This just makes it faster because we
        #   can re-use the result of the sigmoid function stored in
        #   the output variable
        error_term = error * output * (1 - output)

        # The gradient descent step, the error times the gradient times the inputs
        del_w += error_term * x

    # Update the weights here. The learning rate times the 
    # change in weights, divided by the number of records to average
    weights += learnrate * del_w / n_records

    # Printing out the mean square error on the training set
    if e % (epochs / 10) == 0:
        out = sigmoid(np.dot(features, weights))
        loss = np.mean((out - targets) ** 2)
        if last_loss and last_loss < loss:
            print("Train loss: ", loss, "  WARNING - Loss Increasing")
        else:
            print("Train loss: ", loss)
        last_loss = loss

# Calculate accuracy on test data
tes_out = sigmoid(np.dot(features_test, weights))
predictions = tes_out > 0.5
accuracy = np.mean(predictions == targets_test)
print("Prediction accuracy: {:.3f}".format(accuracy))
yuzhenning commented 7 years ago

单层感知器与梯度下降小结

yuzhenning commented 7 years ago

3.1 实现隐藏层

先修要求 接下来我们会讲神经网络在多层感知器里面的数学部分。讲多层感知器我们会用到向量和矩阵。你可以通过下列讲解对此做个回顾: 向量入门和矩阵入门

由来

之前我们研究的是只有一个输出节点网络,代码也很直观。但是现在我们有多个输入单元和多个隐藏单元,它们的权重需要有两个索引 w_ij​,其中 i 表示输入单元,j 表示隐藏单元。

例如在下面这个网络图中,输入单元被标注为 x_1​​ ,x2​​ , x​3​​ ,隐藏层节点是 h​1​ 和 h​2。 代表指向 h_​1​​ 的权重的线条被标成了红色,这样更好区分。 为了定位权重,我们把输入节点的索引 i​​ 和隐藏节点的索引 j 结合,得到: w_11 代表从 x​_1到 h_1的权重; w​_12 ​​代表从 x_1​​到 h_2​​ 的权重。

下图包括了从输入层到隐藏层的所有权重,用 w​_ij​​ 表示:

之前我们可以把权重写成数组,用 w_​i​ 来索引。 现在,权重被储存在矩阵中,由 w_ij​​ 来索引。矩阵中的每一行对应从同一个输入节点发出的权重,每一列对应传入同一个隐藏节点的权重。 这里我们有三个输入节点,两个隐藏节点,权重矩阵表示为:

对比上面的示意图,确保你了解了不同的权重在矩阵中与在神经网络中的对应关系。 用 NumPy 来初始化这些权重,我们需要提供矩阵的形状(shape),如果特征是包一个包含以下数据的二维数组:

# Number of records and input units
# 数据点数量以及每个数据点有多少输入节点
n_records, n_inputs = features.shape
# Number of hidden units
# 隐藏层节点个数
n_hidden = 2
weights_input_to_hidden = np.random.normal(0, n_inputs**-0.5, size=(n_inputs, n_hidden))

这样创建了一个名为 weights_input_to_hidden 的二维数组,维度是 n_inputs 乘 n_hidden。记住,输入到隐藏节点是所有输入乘以隐藏节点权重的和。所以对每一个隐藏层节点 h_j​​ ,我们需要计算:

为了实现这点,我们需要运用矩阵乘法,如果你对线性代数的知识有些模糊,我们建议你看下之前先修部分的资料。这里你只需要了解矩阵与向量如何相乘。

在这里,我们把输入(一个行向量)与权重相乘。要实现这个,要把输入点乘(内积)以权重矩阵的每一列。例如要计算到第一个隐藏节点 j=1 的输入,你需要把这个输入与权重矩阵的第一列做点乘:

针对第二个隐藏节点的输入,你需要计算输入与第二列的点积,以此类推。 在 NumPy 中,你可以直接使用 np.dot hidden_inputs = np.dot(inputs, weights_input_to_hidden)

你可以定义你的权重矩阵的维度是 n_hidden 乘 n_inputs 然后与列向量形式的输入相乘:

注意:

这里权重的索引在上图中做了改变,与之前图片并不匹配。这是因为,在矩阵标注时行索引永远在列索引之前,所以用之前的方法做标识会引起误导。你只需要了解这跟之前的权重矩阵是一样的,只是做了转换,之前的第一列现在是第一行,之前的第二列现在是第二行。如果用之前的标记,权重矩阵是下面这个样子的:

切记,上面标注方式是不正确的,这里只是为了让你更清楚这个矩阵如何跟之前神经网络的权重匹配。 矩阵相乘最重要的是他们的维度相匹配。因为它们在点乘时需要有相同数量的元素。在第一个例子中,输入向量有三列,权重矩阵有三行;第二个例子中,权重矩阵有三列,输入向量有三行。 如果维度不匹配,你会得到:

# Same weights and features as above, but swapped the order
hidden_inputs = np.dot(weights_input_to_hidden, features)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-11-1bfa0f615c45> in <module>()
----> 1 hidden_in = np.dot(weights_input_to_hidden, X)

ValueError: shapes (3,2) and (3,) not aligned: 2 (dim 1) != 3 (dim 0)

3x2 的矩阵跟 3 个元素的数组是没法相乘的。因为矩阵中的两列与数组中的元素个数并不匹配。能够相乘的矩阵如下:

这里的规则是,如果是数组在左边,数组的元素个数必须与右边矩阵的行数一样。如果矩阵在左边,那么矩阵的列数,需要与右边向量的行数匹配。

构建一个列向量

看上面的介绍,你有时会需要一个列向量,尽管 NumPy 默认是行向量。你可以用 arr.T 来对数组进行转置,但对一维数组来说,转置还是行向量。所以你可以用 arr[:,None] 来创建一个列向量:

print(features)
> array([ 0.49671415, -0.1382643 ,  0.64768854])

print(features.T)
> array([ 0.49671415, -0.1382643 ,  0.64768854])

print(features[:, None])
> array([[ 0.49671415],
       [-0.1382643 ],
       [ 0.64768854]])

当然 你可以创建一个二维数组,然后用 arr.T 得到列向量。

np.array(features, ndmin=2)
> array([[ 0.49671415, -0.1382643 ,  0.64768854]])

np.array(features, ndmin=2).T
> array([[ 0.49671415],
       [-0.1382643 ],
       [ 0.64768854]])

我个人更倾向于保持所有向量为一维数组,这样可以更好认知。

yuzhenning commented 7 years ago

3.2 双层感知器编程练习

下面你要实现一个 4x3x2 网络的正向传播,用 sigmoid 作为两层的激活函数。 要做的事情:

yuzhenning commented 7 years ago

3.3 双层感知器solution代码 ( Important!!! )

 # 导入numpy package
 import numpy as np
 def sigmoid(x):
     # 计算sigmoid 函数 , Calculate sigmoid
     return 1/(1+np.exp(-x))

 # Network size 神经网络数据尺寸
 N_input = 4
 N_hidden = 3
 N_output = 2

 np.random.seed(42)
 # Make some fake data 生成虚拟数据
 X = np.random.randn(4)

以下代码很重要:

 # 各层权值计算:
 weights_input_to_hidden = np.random.normal(0, scale=0.1, size=(N_input, N_hidden))
 weights_hidden_to_output = np.random.normal(0, scale=0.1, size=(N_hidden, N_output))

 # TODO: Make a forward pass through the network

 # 隐藏层输入数据 = np.dot( 系统输入数据, 输入-隐藏层权值 )
 hidden_layer_in = np.dot(X, weights_input_to_hidden)
 # 隐藏层输出数据 = sigmoid( 隐藏层输入数据 )
 hidden_layer_out = sigmoid(hidden_layer_in)

 print('Hidden-layer Output:')
 print(hidden_layer_out)

 # 输出层输入数据 = np.dot( 隐藏层输出数据, 隐藏层-输出权值 )
 output_layer_in = np.dot(hidden_layer_out, weights_hidden_to_output)
 # 输出层输出数据 = sigmoid( 输出层输入数据 )
 output_layer_out = sigmoid(output_layer_in)

 print('Output-layer Output:')
 print(output_layer_out)
yuzhenning commented 7 years ago

4.1 反向传播 (Back Proragation- BP神经网络)

又称:误差反传算法,是计算隐藏层权值的重要方法

如何让多层神经网络学习呢? 我们已了解了使用梯度下降来更新权重,反向传播算法则是它的一个延伸。 以一个两层神经网络为例,可以使用链式法则计算输入层-隐藏层间权重的误差。

要使用 梯度下降法 更新隐藏层的权重,你需要知道 各隐藏层节点的误差对最终输出的影响 。 每层的输出是由两层间的权重决定的,两层之间产生的误差,按权重缩放后在网络中向前传播。 既然我们知道输出误差,便可以用权重来反向传播到隐藏层。

例如,输出层每个输出节点 k 的误差是 δ_k^o​​ ,隐藏节点 j 的误差即为输出误差乘以输出层-隐藏层间的权重矩阵(以及梯度)。

然后,梯度下降与之前相同,只是用新的误差:

其中 w_{ij} 是输入和隐藏层之间的权重, x_i 是输入值。这个形式可以表示任意层数。权重更新步长等于步长乘以层输出误差再乘以该层的输入值。

现在,你有了输出误差,\delta{output},便可以反向传播这些误差了。 V{in} 是该层的输入,比如经过隐藏层激活函数的输出值。

yuzhenning commented 7 years ago

4.2 范例

以一个简单的两层神经网络为例,计算其权重的更新过程。 假设该神经网络包含两个输入值,一个隐藏节点和一个输出节点,隐藏层和输出层的激活函数都是 sigmoid,如下图所示。(注意:图底部的节点为输入值,图顶部的 y​^为输出值。输入层不计入层数,所以该结构被称为两层神经网络。)

假设我们试着训练一些二进制数据,目标值是 y=1。我们从正向传播开始,首先计算输入到隐藏层节点 h=∑​i​​` w​i x_i = 0.1×0.4−0.2×0.3=−0.02

以及隐藏层节点的输出 a=f(h)=sigmoid(−0.02)=0.495.

然后将其作为输出节点的输入,该神经网络的输出可表示为 y^ =f(W⋅a)=sigmoid(0.1×0.495)=0.512.

基于该神经网络的输出,就可以使用反向传播来更新各层的权重了。 sigmoid 函数的导数 f′(W⋅a)=f(W⋅a)(1−f(W⋅a)),输出节点的误差项(error term)可表示为 δ^o​​ =(y−y^)f′​​ (W⋅a)=(1−0.512)×0.512×(1−0.512)=0.122.

现在我们要通过反向传播来计算隐藏节点的误差项。这里我们把输出节点的误差项与隐藏层到输出层的权重 W 相乘。 隐藏节点的误差项 δj​^h​​ =∑​​k​​ W_{jk}​​ δ​_k​^o f​′​​(h​_j), 因为该案例只有一个隐藏节点,这就比较简单了 δ​^h=W δ^​o​​ f​′​​(h)=0.1×0.122×0.495×(1−0.495)=0.003

有了误差,就可以计算梯度下降步长了。 隐藏层-输出层权重更新步长是学习速率乘以输出节点误差再乘以隐藏节点激活值。

ΔW=η δ^o​​ a=0.5×0.122×0.495=0.0302

然后,输入-隐藏层权重 w_i​​ 是学习速率乘以隐藏节点误差再乘以输入值。

Δw​i =ηδ​^h x​i​​ =(0.5×0.003×0.1,0.5×0.003×0.3)=(0.00015,0.00045)

从这个例子中你可以看到 sigmoid 做激活函数的一个缺点。 sigmoid 函数导数的最大值是 0.25,因此输出层的误差被减少了至少 75%,隐藏层的误差被减少了至少 93.75%!如果你的神经网络有很多层,使用 sigmoid 激活函数会很快把靠近输入层的权重步长降为很小的值,该问题称作梯度消失。 后面的课程中你会学到在这方面表现更好,也被广泛用于最新神经网络中的其它激活函数。

yuzhenning commented 7 years ago

4.3 用 NumPy 来实现

现在你已经有了大部分用 NumPy 来实现反向传播的知识。

但是之前接触的只是单个节点的误差项。现在在更新权重时,我们需要考虑隐藏层 每个节点 的误差 δ_j: Δw_ij​​ =ηδ_j x_​i ​​

首先,会有不同数量的输入和隐藏节点,所以试图把误差与输入当作行向量来乘会报错

hidden_error*inputs
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-22-3b59121cb809> in <module>()
----> 1 hidden_error*x
ValueError: operands could not be broadcast together with shapes (3,) (6,)

另外,现在 w_ij 是一个矩阵,所以右侧对应也应该有跟左侧同样的维度。 幸运的是,NumPy 这些都能搞定。 如果你用一个列向量数组和一个行向量数组相乘,它会把列向量的第一个元素与行向量的每个元素相乘,组成一个新的二维数组的第一行。 列向量的每一个元素依次重复该过程,最后你会得到一个二维数组, 形状是(len(column_vector), len(row_vector))

hidden_error*inputs[:,None]
array([[ -8.24195994e-04,  -2.71771975e-04,   1.29713395e-03],
       [ -2.87777394e-04,  -9.48922722e-05,   4.52909055e-04],
       [  6.44605731e-04,   2.12553536e-04,  -1.01449168e-03],
       [  0.00000000e+00,   0.00000000e+00,  -0.00000000e+00],
       [  0.00000000e+00,   0.00000000e+00,  -0.00000000e+00],
       [  0.00000000e+00,   0.00000000e+00,  -0.00000000e+00]])

这正好是我们计算权重更新的步长的方式。跟以前一样,如果你的输入是一个一行的二维数组,你可以用 hidden_error*inputs.T,但是如果 inputs 是一维数组,就不行了。

yuzhenning commented 7 years ago

4.4 反向传播练习

接下来你将用代码来实现一次两个权重的反向传播更新。我们提供了正向传播的代码,你来实现反向传播的部分。 要做的事:

-计算网络输出误差 -计算输出层误差项 -用反向传播计算隐藏层误差项 -计算反向传播误差的权重更新步长

yuzhenning commented 7 years ago

4.5 Back Proragation solution code

import numpy as np

def sigmoid(x):
    """
    Calculate sigmoid
    """
    return 1 / (1 + np.exp(-x))

x = np.array([0.5, 0.1, -0.2])
target = 0.6
learnrate = 0.5

weights_input_hidden = np.array([[0.5, -0.6],
                                 [0.1, -0.2],
                                 [0.1, 0.7]])

weights_hidden_output = np.array([0.1, -0.3])

## Forward pass
hidden_layer_input = np.dot(x, weights_input_hidden)
hidden_layer_output = sigmoid(hidden_layer_input)

output_layer_in = np.dot(hidden_layer_output, weights_hidden_output)
output = sigmoid(output_layer_in)

## Backwards pass
## TODO: Calculate output error
error = target - output

# TODO: Calculate error term for output layer
output_error_term = error * output * (1 - output)

# TODO: Calculate error term for hidden layer
hidden_error_term = np.dot(output_error_term, weights_hidden_output) * \
                    hidden_layer_output * (1 - hidden_layer_output)

# TODO: Calculate change in weights for hidden layer to output layer
delta_w_h_o = learnrate * output_error_term * hidden_layer_output

# TODO: Calculate change in weights for input layer to hidden layer
delta_w_i_h = learnrate * hidden_error_term * x[:, None]

print('Change in weights for hidden layer to output layer:')
print(delta_w_h_o)
print('Change in weights for input layer to hidden layer:')
print(delta_w_i_h)
yuzhenning commented 7 years ago

4.6 实现反向传播 (Application)

现在我们知道输出层的误差是: δ_k =(y_k - ​y​^_k​​ ) f​′​​ (a​_k​)

隐藏层误差是: δ_j = sum[w_jk * \delta_k ] f​′​​(h_j)

现在我们只考虑一个简单神经网络,它只有一个隐藏层和一个输出节点。这是通过反向传播更新权重的算法概述:

yuzhenning commented 7 years ago

4.7 反向传播练习

现在你来实现一个通过反向传播训练的神经网络,数据集就是之前的研究生院录取数据。通过前面所学你现在有能力完成这个练习:

你的目标是:

yuzhenning commented 7 years ago

4.8 Solution code

import numpy as np
from data_prep import features, targets, features_test, targets_test

np.random.seed(21)

def sigmoid(x):
    """
    Calculate sigmoid
    """
    return 1 / (1 + np.exp(-x))

# Hyperparameters
n_hidden = 2  # number of hidden units
epochs = 900
learnrate = 0.005

n_records, n_features = features.shape
last_loss = None
# Initialize weights
weights_input_hidden = np.random.normal(scale=1 / n_features ** .5,
                                        size=(n_features, n_hidden))
weights_hidden_output = np.random.normal(scale=1 / n_features ** .5,
                                         size=n_hidden)

for e in range(epochs):
    del_w_input_hidden = np.zeros(weights_input_hidden.shape)
    del_w_hidden_output = np.zeros(weights_hidden_output.shape)
    for x, y in zip(features.values, targets):
        ## Forward pass ##
        # TODO: Calculate the output
        hidden_input = np.dot(x, weights_input_hidden)
        hidden_output = sigmoid(hidden_input)

        output_layer_in = np.dot(hidden_output, weights_hidden_output)
        output = sigmoid(output_layer_in)

        ## Backward pass ##
        # TODO: Calculate the network's prediction error
        error = y - output 
        # error = targets - output

        # TODO: Calculate error term for the output unit
        output_error_term = error * output * (1 - output)

        ## propagate errors to hidden layer

        # TODO: Calculate the hidden layer's contribution to the error
        # Important !!!
        hidden_error = np.dot(output_error_term, weights_hidden_output)

        # TODO: Calculate the error term for the hidden layer
        # hidden_error_term = np.dot(output_error_term, weights_hidden_output) * \
        #            hidden_output * (1 - hidden_output)

        hidden_error_term = hidden_error * hidden_output * (1 - hidden_output)

        # TODO: Update the change in weights
        # del_w_hidden_output += learnrate * output_error_term * hidden_output
        # del_w_input_hidden += learnrate * hidden_error_term * x[:, None]

        del_w_hidden_output += output_error_term * hidden_output
        del_w_input_hidden += hidden_error_term * x[:, None]

    # TODO: Update weights
    weights_input_hidden += learnrate * del_w_input_hidden / n_records
    weights_hidden_output += learnrate * del_w_hidden_output / n_records

    # Printing out the mean square error on the training set
    if e % (epochs / 10) == 0:
        hidden_output = sigmoid(np.dot(x, weights_input_hidden))
        out = sigmoid(np.dot(hidden_output,
                             weights_hidden_output))
        loss = np.mean((out - targets) ** 2)

        if last_loss and last_loss < loss:
            print("Train loss: ", loss, "  WARNING - Loss Increasing")
        else:
            print("Train loss: ", loss)
        last_loss = loss

# Calculate accuracy on test data
hidden = sigmoid(np.dot(features_test, weights_input_hidden))
out = sigmoid(np.dot(hidden, weights_hidden_output))
predictions = out > 0.5
accuracy = np.mean(predictions == targets_test)
print("Prediction accuracy: {:.3f}".format(accuracy))
yuzhenning commented 7 years ago

4.9 BP code 小结

yuzhenning commented 7 years ago

5.0 Yes you should understand backprop

https://medium.com/@karpathy/yes-you-should-understand-backprop-e2f06eab496b

When we offered CS231n (Deep Learning class) at Stanford, we intentionally designed the programming assignments to include explicit calculations involved in backpropagation on the lowest level. The students had to implement the forward and the backward pass of each layer in raw numpy. Inevitably, some students complained on the class message boards: “Why do we have to write the backward pass when frameworks in the real world, such as TensorFlow, compute them for you automatically?” This is seemingly a perfectly sensible appeal - if you’re never going to write backward passes once the class is over, why practice writing them? Are we just torturing the students for our own amusement? Some easy answers could make arguments along the lines of “it’s worth knowing what’s under the hood as an intellectual curiosity”, or perhaps “you might want to improve on the core algorithm later”, but there is a much stronger and practical argument, which I wanted to devote a whole post to:

The problem with Backpropagation is that it is a leaky abstraction. In other words, it is easy to fall into the trap of abstracting away the learning process — believing that you can simply stack arbitrary layers together and backprop will “magically make them work” on your data. So lets look at a few explicit examples where this is not the case in quite unintuitive ways.

Vanishing gradients on sigmoids

We’re starting off easy here. At one point it was fashionable to use sigmoid (or tanh) non-linearities in the fully connected layers. The tricky part people might not realize until they think about the backward pass is that if you are sloppy with the weight initialization or data preprocessing these non-linearities can “saturate” and entirely stop learning — your training loss will be flat and refuse to go down. For example, a fully connected layer with sigmoid non-linearity computes (using raw numpy):

z = 1/(1 + np.exp(-np.dot(W, x))) # forward pass
dx = np.dot(W.T, z*(1-z)) # backward pass: local gradient for x
dW = np.outer(z*(1-z), x) # backward pass: local gradient for W

If your weight matrix W is initialized too large, the output of the matrix multiply could have a very large range (e.g. numbers between -400 and 400), which will make all outputs in the vector z almost binary: either 1 or 0. But if that is the case, z*(1-z), which is local gradient of the sigmoid non-linearity, will in both cases become zero (“vanish”), making the gradient for both x and W be zero. The rest of the backward pass will come out all zero from this point on due to multiplication in the chain rule.