前向传播

神经网络每层都包含有若干神经元,当信息传递时,第i层神经元接受上层的输入,经激励函数作用后,会产生一个激活向量,此向量将作为下一层神经元的输入值,以此规律向下不断传递。整个过程因为发生顺序是不断地将刺激由前一层传向下一层,故而称之为前向传递。

TensorFlow中添加一层的代码如下:

def add_layer(inputs, in_size, out_size, activation_function=None):
    W = tf.Variable(tf.random_normal([in_size, out_size])) # 权值矩阵
    b = tf.Variable(tf.zeros([1, out_size]) + 0.1) # 偏移
    f = tf.matmul(inputs, W) + b # 输出
    if activation_function is None:
        outputs = f
    else:
        outputs = activation_function(f) # 激励函数
    return outputs

假设隐藏层只有一层,核心代码如下:

l1 = add_layer(x, 1, 10, tf.nn.relu) # 隐藏层, 输入向量为x,有10个神经元,激励函数为ReLU
prediction = add_layer(l1, 10, 1, None) # 将隐藏层的输出作为输入,输出预测值

反向传播

在神经网络中,我们也需要通过最小化代价函数来优化预测精度。但由于神经网络各层的神经元都会产出预测,因此,不能直接利用传统的梯度下降法来最小化,而需要逐层考虑预测误差,并且逐层优化。为此,在多层神经网络中,使用反向传播算法来优化预测。本层的误差由下一层的误差反向推导。

loss = tf.reduce_mean(tf.reduce_sum(tf.square(y - prediction), axis=1)) # 损失函数,y为训练数据
train = tf.train.GradientDescentOptimizer(0.1).minimize(loss)

表面上看,代码似乎只构建了正向的传播,而整个反向传播过程其实都是在GradientDescentOptimizer函数中完成的。具体过程可查看参考资料。

示例

从训练集的5000个样本中随机选取100个样本进行可视化。每个样本是一个400维的向量,需要在可视化时重组为20×20的像素网格。

data = loadmat('ex4data1.mat')
X = data['X']
y = data['y'].flatten()

images = X[np.random.choice(range(5000), 100)] # 随机取100个数字
fig, ax_array = plt.subplots(10, 10, sharex=True, sharey=True, figsize=(8, 8)) # 生成10*10个子图
for r in range(10):
    for c in range(10):
        ax_array[r, c].matshow(images[r * 10 + c].reshape(20, 20), cmap='gray_r') # 每一个子图显示一个图片数据
plt.xticks([]) # 坐标轴内容设置为空
plt.yticks([])
plt.show()

我们需要对标签进行独热编码,将其转换为长度向量。本例有十个标签值,那么像数字6就要转化为[0,0,0,0,0,1,0,0,0,0]。使用scikit-learn中OneHotEncoder函数解决。

encoder = OneHotEncoder(sparse=False, categories='auto')
y = encoder.fit_transform(y.reshape(-1, 1))

从文件读取已经训练好的参数$/Theta1$,$/Theta2$,利用前向传播计算输出结果。本例有一个输入层、一个隐藏层和一个输出层,注意添加偏置单元。

data2 = loadmat('ex4weights.mat')
theta1 = data2['Theta1']
theta2 = data2['Theta2']

def sigmoid(z): # 激励函数
    return 1 / (1 + np.exp(-z))

def forward(X, theta1, theta2):
    a1 = np.insert(X, 0, 1, axis=1) # 添加偏置单元
    z2 = np.dot(a1, theta1.T)
    a2 = np.insert(sigmoid(z2), 0, 1, axis=1) # 添加偏置单元
    z3 = np.dot(a2, theta2.T)
    h = sigmoid(z3)
    return a1, z2, a2, z3, h

计算代价函数并添加正则化,注意不要将偏置项正则化。前向传播算法就到此结束了。

m = len(X)

def cost(theta, X, y):
    a1, z2, a2, z3, h = forward(theta, X)
    J = np.sum(-y * np.log(h) - (1 - y) * np.log(1 - h)) / m
    return J

def regularized_cost(theta, X, y, l=1.):
    theta1, theta2 = deserialize(theta)
    reg = np.sum(theta1[:, 1:] ** 2) + np.sum(theta2[:, 1:] ** 2)
    return cost(theta, X, y) + l / (2 * m) * reg

接下来是反向传播算法,首先计算输出层的误差向量,然后反向计算其他层的误差向量。再求各层权值参数(theta1和theta2)的梯度,进而得到更新增量。正则化时注意不处理偏置单元的参数,可将参数置零以统一正则化公式。

def sigmoid_gradient(z):
    return sigmoid(z) * (1 - sigmoid(z))

def gradient(theta, X, y):
    theta1, theta2 = deserialize(theta)
    a1, z2, a2, z3, h = forward(theta, X)
    # 误差向量
    d3 = h - y
    d2 = np.dot(d3, theta2[:, 1:]) * sigmoid_gradient(z2)
    # 梯度
    D2 = np.dot(d3.T, a2)
    D1 = np.dot(d2.T, a1)
    # 更新增量
    D = (1 / m) * serialize(D1, D2)
    return D

def regularized_gradient(theta, X, y, l=1.):
    D1, D2 = deserialize(gradient(theta, X, y))
    theta1[:, 0] = 0  # 偏置单元置0
    theta2[:, 0] = 0
    reg_D1 = D1 + (l / m) * theta1
    reg_D2 = D2 + (l / m) * theta2

    return serialize(reg_D1, reg_D2)

进行梯度校验,比较实际值和估测值的相似程度。

def gradient_checking(X, y, epsilon):
    grad_approx = []
    for i in range(len(theta)):  # 计算grad_approx
        plus = theta.copy()
        minus = theta.copy()
        plus[i] += epsilon
        minus[i] -= epsilon
        approx = (regularized_cost(plus, X, y) - regularized_cost(minus, X, y)) / (epsilon * 2)
        grad_approx.append(approx)

    grad_approx = np.array(grad_approx)
    grad_compute = regularized_gradient(theta, X, y)
    diff = np.linalg.norm(grad_approx - grad_compute) / np.linalg.norm(grad_approx + grad_compute)  # 比较相似程度,使用欧式距离
    return diff

参考资料: 前向传播与反向传播 建造我们第一个神经网络 tensorflow的自动求导具体是在哪部分代码里实现的? 数据预处理:独热编码(One-Hot Encoding)