梯度下降
之前我们看到一个权重的更新方法是:
这里 error term δ 是指
是输出误差,f' (h) 是激活函数 f(h) 的导函数,我们把这个导函数称做输出的梯度。
现在假设只有一个输出,把这个写成代码。我们还是用 sigmoid 来作为激活函数 f(h)。
# 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
实现梯度下降
我们拿一个研究生学院录取数据来用梯度下降训练一个网络。数据可以在这里找到。数据有三个输入特征,GRE分数,GPA,和本科院校排名(从1到4)。数字1代表最好,数字4代表最差。
我们的目标是基于这些特征来预测一个学生能否被研究生院录取。这里,我们用有一个输出层的网络。用 sigmoid 做为激活函数。
数据清理
你也许认为有三个输入特征,但是首先我们要做数据转换。rank是类别特征,数字并不包含任何想对的值。排名第 2 并不是排名第 1 的两倍;排名第 3 也不是排名第 2 的 1.5 倍。因此,我们需要用 dummy variables 来对rank进行编码。把数据分成 4 个新列,用 0 或 1 表示。排名第一的行 rank 1那一列的值是 1 ,其它是 0;排名第二的行 rank 2 那一列的值是 1 ,其它是 0,以此类推。
我们还需要把 GRE 和 GPA 分数标准化,也就是说使得他们的平均值是 0,标准差是 1。因为 sigmoid 函数会挤压很大或者很小的输入,所以这一步是必要的。很大或者很小输入的梯度是 0 意味着梯度下降的步长也会是 0。因为 GRE 和 GPA 的值都相对较大,我们在初始化权重的时候要非常小心,否则梯度下降就会死亡,网络也没发训练了。如果我们对数据做了标准化处理,我们能更容易地对权重进行初始化。
这只是一个简单介绍,你之后还会学到如何预处理数据,如果你想了解我是怎么做的,可以查看下面编程练习中的 data_prep.py文件。
现在数据已经准备好了,我们看到有六个输入特征:gre, gpa,和四个rank的虚拟变量 (dummy variables)。
梯度下降法训练过程
这是用梯度下降来更新权重的算法概述:
- 权重更新初始为 0:
- 训练数据中的每一条记录:
- 通过网络做正向传播,计算输出
- 计算输出的 error term,
- 更新权重步长
- 更新权重
η 是学习率, m 是数据点个数。 这里我们对权重步长做了平均,为的是降低训练数据中大的变化。
- 重复 e 次(epoch)。
你也可以选择每一个记录更新一下权重,而不是把所有记录都训练过之后再取平均。
这里我们还是使用 sigmoid 作为激活函数:
sigmoid 的梯度是:
h 从输入计算的输出,
用 NumPy 来实现
这里大部分都可以用 NumPy 很方便的实现。
首先你需要初始化权重。我们希望它们比较小,这样输入在 sigmoid 函数那里可以在接近 0 的位置,而不是最高或者最低处。很重要的一点是要随机地初始化它们,这样它们有不同的值,是发散且不对称的。所以我们从一个中心为 0 的正态分布来初始化权重。一个好的标准差的值是 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)
最后我们更新 Δwi和 wi。
编程练习
接下来,你要实现一个梯度下降,用录取数据(binary.csv)来训练它。你的目标是训练一个网络直到你达到训练数据的最小的均方差mean square error (MSE)。你需要实现:
- 网络的输出: output
- 输出误差: error
- 误差项: error_term
- 权重更新步长: del_w +=
- 更新权重: weights +=
你可以任意调节超参数 hyperparameters 来看下它对均方差 MSE 有什么影响。
train_gradient_descent.py
import numpy as np
from data_prep import features, targets, features_test, targets_test
# Practice implementing gradient descent and training the network on
# an admissions dataset. The goal of the exercise is to train the network
# until reaching the minimum mean sqauared error.
#
# Based on an exercise from Udacity.com
###################################################################
def sigmoid(x):
"""
Calculate sigmoid
"""
return 1 / (1 + np.exp(-x))
# 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
output = sigmoid(np.dot(weights, x))
error = y - output
del_w += error * output * (1 - output) * x
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))
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']
多层感知器
实现隐藏层
之前我们研究的是有一个输出节点网络,代码也很直观。但是现在我们有不同的输入,多个隐藏层,他们的权重需要有两个索引 wij,i 表示输入单位,j 表示隐藏单位。
例如在下面这个网络图中,输入被标注为 x1,x2, x3,隐藏层节点是 h1 和 h2。
指向 h1 的权重被标成了红色,这样更好区分。
为了定位权重,我们把输入节点的索引 i 和输出节点的索引 j 结合,得到:w11,代表从 x1 到 h1 的权重。
w12:代表从 x1 到 h2 的权重。
下图包括了从输入层到隐藏层用 wij 来标注的所有权重:
之前我们可以把权重写成序列,标记为 wi。
现在,权重被储存在矩阵中,由 wij 来标记。每一行表示从输入层发出的权重,每一列表示从输入到隐藏层的权重。这里我们由三个输入,两个因此节点,权重矩阵标示为:
记得比较一下上面的示意图来确定你了解不同的权重在矩阵中神经网络中的对应关系。
用 NumPy 来初始化权重,我们需要提供矩阵的维度,如果特征是包含输入的二维序列:
# 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 的 2D 序列,维度是 n_inputs乘 n_hidden。记住,输入到隐藏层是所有输入乘以隐藏层权重的和。所以对每一个隐藏层节点 hj,我们需要计算:
为了实现这点,我们需要运用矩阵乘法,如果你对线性代数有点忘了,我们建议你看下之前先修部分的资料。这里你只需要了解矩阵如何相乘。
在这里,我们把输入(一个行向量)与权重相乘。要实现这个,要把输入点乘(内积)以权重矩阵的每一列。例如要计算到第一个隐藏节点的输入 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]])
编程练习
实现一个 4x3x2 网络的正向传播,用 sigmoid 作为两层的激活函数。
要做的事情:
- 计算到隐藏层的输入
- 计算隐藏层输出
- 计算输出层的输入
- 计算神经网络的输出
import numpy as np
N_input = 4
N_hidden = 3
N_output = 2
np.random.seed(42)
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))
hidden_layer_in = np.dot(X, weights_input_to_hidden)
hidden_layer_out = sigmoid(hidden_layer_in)
output_layer_in = np.dot(hidden_layer_out, weights_hidden_to_output)
output_layer_out = sigmoid(output_layer_in)
print out_layer_out