用Python从零开始构建反向传播算法

反向传播算法是经典的前馈人工神经网络。

这项技术也被用来训练大型的深度学习网络。

在本教程中,你将探索如何使用Python从零开始构建反向传播算法。

完成本教程后,你将知道:

  • 如何正向传播输入以计算输出。
  • 如何反向传播误差并训练网络。
  • 如何将反向传播算法应用于现实世界的预测建模问题。

让我们开始吧。

照片来源:NICHD.https://www.flickr.com/photos/nichd/21086425615/,保留部分权利

描述

本节简要介绍反向传播算法和本教程中将使用的小麦种子数据集。

反向传播算法

在人工神经网络领域,反向传播算法是多层前馈网络中的监督学习方法。

前馈神经网络接受一个或多个神经元处理后的信息作为输入激励。神经元通过它的树突来接受输入信号,树突将电信号传递给细胞体。轴突将信号传递给突触(突触是细胞轴突与其他细胞树突连接的部位)。

反向传播算法的原理是通过修改输入信号的内部权重产生预期的输出信号来拟合给定的函数。系统使用监督学习的方法进行训练,系统的输出和已知的预期输出之间的误差将被提供给系统来修改其内部状态。

从技术上来讲,反向传播算法是多层前馈神经网络训练权重的方法。因此,需要定义一个单层或多层的网络结构,其中的每一层与下一层完全连接。标准的网络结构由一个输入层,一个隐藏层和一个输出层构成。

反向传播即可以用于分类问题中,也可以用在回归问题当中,在本教程中我们关注其在分类问题上的应用。

在分类问题当中,最理想的结果是输出层中的每一个神经元对应着一个类别的值。举例来说,假设有一个二分类问题,两个类别为A和B。此时预期的类别输出必定可以转化为一列数值,每一行的值代表着其属于该类的概率,比如说A => 1, 0, B => 0, 1,这种编码方式也被称作One-hot编码。

小麦种子数据集

根据小麦种子数据集中不同种类小麦种子样本的观测值,其可以用于预测小麦种子的所属品种。

数据集中有201条样本记录和7个数值输入变量。这个分类问题有三个可能的输出类别。每个输入变量的变化范围是不同的,所以可能需要进行归一化处理,尤其像反向传播算法一样需要赋予输入值权重的算法。

下面给出数据集中前五行的样本。

15.26,14.84,0.871,5.763,3.312,2.221,5.22,1

14.88,14.57,0.8811,5.554,3.333,1.018,4.956,1

14.29,14.09,0.905,5.291,3.337,2.699,4.825,1

13.84,13.94,0.8955,5.324,3.379,2.259,4.805,1

16.14,14.99,0.9034,5.658,3.562,1.355,5.175,1

使用零规则算法,预测结果为数据集中最常见的样本类别,得到基线预测正确率为28.095%。

你可以从UCI机器学习数据库中下载数据集,了解与其相关的更多信息。

将种子数据集下载到当前的工作目录后重命名为seeds_dataset.csv。下载的数据集使用制表符作为分割符,所以你必须使用文本编辑器或者电子表格程序将其转换为CSV。

教程

本教程分为6个部分:

  1. 初始化网络。
  2. 前向传播。
  3. 误差反向传播。
  4. 训练网络。
  5. 预测。
  6. 种子数据集案例研究。

这些步骤将为你从头开始实施反向传播算法并将其应用于你自己的预测建模问题提供所需基础。

1.初始化网络

让我们从简单的事情开始,首先创建一个新的网络以供训练。

每个神经元都有一组需要被维护的权重值。神经元间每个连接都需要一个权重值,除此之外每个神经元还有一个额外的偏置值需要维护。在训练过程中,我们需要存储神经元的这些附带属性,因此我们使用字典来表示每个神经元,并用weight作为键名来存储权重值。

网络会以层级的形式来组织。输入层实际上就是来自训练数据集中的一行数据输入,因此真正的第一层输入位于隐藏层,隐藏层之后是输出层,其中每一个类别的分值输出都对应着输出层中的一个神经元。

我们将这些层以字典数组的方式组织起来,即将整个网络当作不同层元素构成的数组来看待。

网络的权重值初始化为小的随机数为宜,考虑这个因素,我们使用0-1范围内的随机数做权重的初始化。

下面的initialize_network()函数可以新建用于训练的神经网络。它接受三个参数值:输入层神经元数,隐藏层神经元数和输出层神经元数。

从代码中可以看出我们将创建n_hidden个隐藏层神经元,每个隐藏层神经元有n_input + 1个权重值需要维护,对应着n_input个输入神经元的连接权重值和一个偏置值。

同样地,你也可以看到与隐藏层连接的输出层有n_output个神经元,每个神经元有n_hidden + 1个权重值需要维护,这意味着输出层中任一个神经元都维护着隐含层每一个神经元的连接(权重值)。

# 初始化网络
def initialize_network(n_inputs, n_hidden, n_outputs):
    network = list()
    hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
    network.append(hidden_layer)
    output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
    network.append(output_layer)
    return network

让我们来测试一下这个函数。下面是创建一个小型网络的完整样例。

from random import seed
from random import random
# 初始化网络
def initialize_network(n_inputs, n_hidden, n_outputs):
    network = list()
    hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
    network.append(hidden_layer)
    output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
    network.append(output_layer)
    return network

seed(1)
network = initialize_network(2, 1, 2)
for layer in network:
    print(layer)

运行这个样例,可以看到代码会依次打印各层的信息。可以看出隐藏层有一个神经元,神经元有两个输入权重和一个偏置值。输出层有两个神经元,每个神经元有一个权重值和一个偏置值。

[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}]
[{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381, 0.651592972722763]}]

现在我们知道了如何创建和初始化一个网络。下面让我们看看如何使用它来计算输出。

2.前向传播

我们让输入信号依次通过每一层直至输出层,最后输出的值即我们要计算的输出值。

这个过程我们称为前向传播。

这是我们在训练过程中网络预测的方式,其输出还需要进一步的纠正,训练完成的网络对新数据的预测也是利用前向传播实现的。

我们可以把传播过程分解成三部分:

  1. 神经元的激活过程。
  2. 神经元的传递过程。
  3. 前向传播。

2.1. 神经元的激活过程

第一步是计算在给定输入情况下神经元的激活值(或活化值,表征神经元的激活程度)。

对于隐藏层来说,输入可以是训练数据集中的一行。对于输出层来说,输入是隐藏层中每个神经元的输出。

神经元的激活值可以通过计算输入的加权和得到,与线性回归十分相似。

activation = sum(weight_i * input_i) + bias

weight代表网络权重,input代表输入,i为权重或输入的编号,bias是一个特殊的权重(偏置),它不需要与任何输入相乘(你也可以理解为与它相乘的输入值恒为1.0)。

下面是 activate() 函数的实现,从中可以看出这个函数假定权重列表中的最后一个权重值为偏置。这个假设可以提升这里和后面代码的可读性。

# 计算给定输入情况下神经元的激活值
def activate(weights, inputs):
    activation = weights[-1]
    for i in range(len(weights)-1):
        activation += weights[i] * inputs[i]
    return activation

现在,让我们看看如何使用神经元的激活值。

2.2. 神经元的传递过程

每当神经元被激活,我们就需要转换它的激活值来看一下神经元的输出到底是什么。

我们可以使用不同的传递函数。传统的方法是使用sigmoid激活函数,但是你也可以使用tanh(双曲正切)函数来作为传输函数。在最近的大型深度学习网络中,整流传递函数(Relu函数等)的使用更为普遍。

Sigmoid函数曲线呈S形,也被称为逻辑函数。它可以接受任意的输入值并在S曲线上映射产生0-1之间的输出值。选择这个函数也为我们后续计算反向传播误差所需导数(斜率)提供了便利。

我们可以使用sigmoid函数来传递激活值,数学形式如下:

output = 1 / (1 + e^(-activation))

其中e是自然对数的底数(欧拉数)。

下面是 transfer() 函数,它实现了sigmoid函数。

# 传递神经元的激活值
def transfer(activation):
    return 1.0 / (1.0 + exp(-activation))

现在我们有了这些东西,下面让我们看看如何使用他们。

2.3. 前向传播

前向传播直接接受输入值。

通过计算当前层中所有神经元的输出,信号可以依次通过网络中的每一层。每一层的输出将称为下一层的输入。

下面是 forward_propagate() 函数的实现,它实现了从单行输入数据在神经网络中的前向传播。

从代码中可以看到神经元的输出被存储在neuronoutput属性下,我们使用 new_input 数组来存储当前层的输出值,它将作为下一层的 input (输入)继续向前传播。

该函数在最后一层计算完成后输出返回值,最后一层也被称作输出层。

# 从网络输入到网络输出的前向传播过程
def forward_propagate(network, row):
    inputs = row
    for layer in network:
        new_inputs = []
        for neuron in layer:
            activation = activate(neuron['weights'], inputs)
            neuron['output'] = transfer(activation)
            new_inputs.append(neuron['output'])
        inputs = new_inputs
    return inputs

让我们把上面所有的代码片段放在一起并测试一下本节我们完成的前向传播函数。

我们用内联定义的方式定义我们的测试网络,假定网络的预期输入值为两个,输出层有两个神经元。

from math import exp
def activate(weights, inputs):
    activation = weights[-1]
    for i in range(len(weights)-1):
        activation += weights[i] * inputs[i]
    return activation

def transfer(activation):
    return 1.0 / (1.0 + exp(-activation))

# 从网络输入到网络输出的前向传播
def forward_propagate(network, row):
    inputs = row
    for layer in network:
        new_inputs = []
        for neuron in layer:
            activation = activate(neuron['weights'], inputs)
            neuron['output'] = transfer(activation)
            new_inputs.append(neuron['output'])
        inputs = new_inputs
    return inputs

# 前向传播测试
network = [[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}],
[{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381,0.651592972722763]}]]
row = [1, 0, None]
output = forward_propagate(network, row)
print(output)

运行示例代码将向网络中传入1, 0作为输入并产生相应的输出。因为输出层有两个神经元,所以我们得到了包含两个元素的输出列表。

现在网络的输出值并没有任何实际意义,不过下面我们将学习如何得到使网络中的权重更有价值。

[0.6629970129852887, 0.7253160725279748]

3.误差反向传播

反向传播算法的名字是由它训练权重的方式得来的。

误差是通过预期输出和网络前向传播输出值计算得到的。这些误差通过网络从输出层反向传播至隐藏层,分配误差并即时更新权重。

反向传播误差的数学形式源于微积分,但本节我们将以高层次的角度来关注它计算了什么以及怎样实现它特定的计算形式而不是为何选取这种形式。

本节分为两部分。

  1. 传递导数。
  2. 误差反向传播。

3.1. 传递导数

给定了神经元的输出值,我们需要计算它的斜率。

我们选取的传递函数是sigmoid函数,其导数可以通过下面的公式计算:

derivative = output * (1.0 - output)

下面的 transfer_derivative() 函数实现了这个公式:

# 计算神经元输出的导数
def transfer_derivative(output):
    return output * (1.0 - output)

现在,让我们看看如何使用它。

3.2. 误差反向传播

第一步是计算每个输出神经元的误差,这将为我们提供网络反向传播所需的误差信号(输入)。

对于给定的神经元,它的误差可以通过下式计算得到:

error = (expected - output) * transfer_derivative(output)

except为神经元的预期输出值,output为神经元的实际输出值,transfer_derivative() 是我们上面用于计算神经元输出值斜率的函数。

误差计算将应用于输出层。对于输出层来说,预期输出即样本真实的类别。在隐含层中则会复杂一些。

隐藏层中神经元的误差信号将通过输出层中每个神经元误差的加权误差计算得到。我们将误差通过输出层权重传递给隐藏层中的每一个神经元。

反向传播积累的误差信号将用于确定隐藏层中神经元的误差:

error = (weight_k * error_j) * transfer_derivative(output)

其中,error_j是输出层中第j个神经元的误差信号,weight_k是第k个神经元到当前神经元的连接权重,output是当前神经元的输出。

下面的 backward_propagate_error() 函数实现了这个过程。

可以看到每个神经元计算得到的误差信号将存储在其delta属性下。可以看到,网络的各层将以反向的顺序迭代,从输出层开始反向传播。这确保了输出层的神经元首先计算delta值以供隐藏层的神经元可以在后续的迭代中使用。我使用delta作为属性名来反映这是这是神经元上误差的变化(例:weight delta)。

可以看到,隐藏层中的误差信号是从输出层神经元中积累的,其中隐藏层的神经元序号j也是输出层神经元权重的索引 neuron'weight'

# 误差的反向传播以及在神经元中的存储
def backward_propagate_error(network, expected):
    for i in reversed(range(len(network))):
        layer = network[i]
        errors = list()
        if i != len(network)-1:
            for j in range(len(layer)):
                error = 0.0
                for neuron in network[i + 1]:
                    error += (neuron['weights'][j] * neuron['delta'])
                errors.append(error)
        else:
            for j in range(len(layer)):
                neuron = layer[j]
                errors.append(expected[j] - neuron['output'])
        for j in range(len(layer)):
            neuron = layer[j]
            neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

让我们把所有的代码段放在一起,看看它们如何工作。

我们定义一个具有设定输出值的固定神经网络,然后用预期的输出实现反向传播。完整的代码样例如下所示:

# 计算神经元输出的导数
def transfer_derivative(output):
    return output * (1.0 - output)

# 误差的反向传播以及在神经元中的存储
def backward_propagate_error(network, expected):
    for i in reversed(range(len(network))):
        layer = network[i]
        errors = list()
        if i != len(network)-1:
            for j in range(len(layer)):
                error = 0.0
                for neuron in network[i + 1]:
                    error += (neuron['weights'][j] * neuron['delta'])
                errors.append(error)
        else:
            for j in range(len(layer)):
                neuron = layer[j]
                errors.append(expected[j] - neuron['output'])
        for j in range(len(layer)):
            neuron = layer[j]
            neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

# 测试误差反向传播
network = [[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327,0.763774618976614]}],
[{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095]}, {'output':0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763]}]]
expected = [0, 1]
backward_propagate_error(network, expected)
for layer in network:
    print(layer)

运行代码,在误差反向传播完成后会打印输出网络信息,可以看到输出层和隐藏层的神经元的误差都已经被计算并保存在了对应的神经元当中。

[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614], 'delta': -0.0005348048046610517}]
[{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095], 'delta': -0.14619064683582808}, {'output': 0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763], 'delta': 0.0771723774346327}]

现在让我们使用误差反向传播来训练网络。

4.训练网络

使用随机梯度下降算法训练网络。

这涉及了网络在训练数据集上的多次迭代,对于每一行数据则包括了输入数据的前向传播,误差的反向传播以及权重的更新。

这节分为两部分:

  1. 更新权重。
  2. 训练网络。

4.1. 更新权重

当我们通过上述的反向传播方法计算得到了每个神经元的误差之后,就可以用它们来更新权重值。

网络权重更新公式如下:

weight = weight + learning_rate * error * input

weight为给定的权重,learning_rate为手动指定的超参数,error为神经元通过反向传播计算得到的误差,input为产生误差的输入值。

偏置权重的更新公式也是一样的,只是没有输入项或者说输入值永远为1.0而已。

学习率控制着纠正误差时权重的变化大小。举例来说,0.1的学习率将更新可能需要更新权重量的10%。小的学习率会导致更大的训练批次和更慢的学习速率。这增加了网络在所有层上寻找到一组最优的权重的可能性而不是更快地得到一组最小化误差的次优权重(Premature convergence

,这种现象也称为过早收敛,早熟收敛)

下面是 update_weights() 函数,在给定输入数据和学习率且前向传播和反向传播进行完毕时对网络权重进行更新。

请记住,输出层的输入是隐藏层输出的集合。

# 根据误差更新网络权重
def update_weights(network, row, l_rate):
    for i in range(len(network)):
        inputs = row[:-1]
        if i != 0:
            inputs = [neuron['output'] for neuron in network[i - 1]]
        for neuron in network[i]:
            for j in range(len(inputs)):
                neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j]
            neuron['weights'][-1] += l_rate * neuron['delta']

现在我们知道了如何更新网络权重,下面让我们看看我们如何重复以上过程。

4.2. 训练网络

如前所述,使用随机梯度下降算法来更新网络。

该过程需要完成指定批次数量的循环过程,在每个训练批次中需要根据训练数据集中对应的输入来更新网络权重。

因为每个训练样本的输入都会导致网络的更新,这种学习方式被称作在线学习。如果每个批次中的训练样本不止一个,即在每次更新前都有误差的积累过程,这种学习方式称为批量学习或者批量梯度下降(Batch gradient descent)。

下面的函数实现了给定训练数据集,学习率,epochs(批次数),预期输出和初始化网络时网络的训练过程。

训练数据集中的预期输出是类别经过One-hot编码后的输出,为列矢量。我们将使用这个二值向量来与网络的输出进行比对,这是计算输出层误差必需的过程。

从代码中还可以看到,预期输出和网络输出间的平方误差会在每个训练批次(epoch)中积累,在每个训练批次结束后会打印输出误差,这有助于我们观察网络在训练中学习和提升的过程。

# 训练网络(手动指定批次数 n_epoch)
def train_network(network, train, l_rate, n_epoch, n_outputs):
    for epoch in range(n_epoch):
    sum_error = 0
    for row in train:
        outputs = forward_propagate(network, row)
        expected = [0 for i in range(n_outputs)]
        expected[row[-1]] = 1
        sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))])
        backward_propagate_error(network, expected)
        update_weights(network, row, l_rate)
    print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))

我们现在已经完成了训练网络所需的全部代码段,下面让我们在一个小的数据集上完成一个囊括之前所有内容(网络初始化,网络训练)的测试吧。

下面是一个设计好的小数据集,我们用它来测试我们神经网络的训练过程。

X1	X2	Y
2.7810836	2.550537003	0
1.465489372	2.362125076	0
3.396561688	4.400293529	0
1.38807019	1.850220317	0
3.06407232	3.005305973	0
7.627531214	2.759262235	1
5.332441248	2.088626775	1
6.922596716	1.77106367	1
8.675418651	-0.242068655	1
7.673756466	3.508563011	1

下面给出完整的代码示例。我们设定隐藏层的神经元数量为2,因为这是一个二分类问题,所以输出层需要两个神经元。设定训练批次数为20,学习率为0.5,学习率设置这么大的原因是训练的迭代次数过少。

from math import exp
from random import seed
from random import random

# 初始化网络
def initialize_network(n_inputs, n_hidden, n_outputs):
    network = list()
    hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
    network.append(hidden_layer)
    output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
    network.append(output_layer)
    return network

# 计算给定输入情况下神经元的激活值
def activate(weights, inputs):
    activation = weights[-1]
    for i in range(len(weights)-1):
        activation += weights[i] * inputs[i]
    return activation

# 传递神经元的激活值
def transfer(activation):
    return 1.0 / (1.0 + exp(-activation))

# 从网络输入到网络输出的前向传播
def forward_propagate(network, row):
    inputs = row
    for layer in network:
        new_inputs = []
        for neuron in layer:
            activation = activate(neuron['weights'], inputs)
            neuron['output'] = transfer(activation)
            new_inputs.append(neuron['output'])
        inputs = new_inputs
    return inputs

# 计算神经元输出值的导数
def transfer_derivative(output):
    return output * (1.0 - output)

# 误差反向传播并将误差存储在神经元中
def backward_propagate_error(network, expected):
    for i in reversed(range(len(network))):
        layer = network[i]
        errors = list()
        if i != len(network)-1:
            for j in range(len(layer)):
                error = 0.0
                for neuron in network[i + 1]:
                    error += (neuron['weights'][j] * neuron['delta'])
                errors.append(error)
        else:
            for j in range(len(layer)):
                neuron = layer[j]
                errors.append(expected[j] - neuron['output'])
        for j in range(len(layer)):
            neuron = layer[j]
            neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

# 根据误差更新网络权重
def update_weights(network, row, l_rate):
    for i in range(len(network)):
        inputs = row[:-1]
        if i != 0:
            inputs = [neuron['output'] for neuron in network[i - 1]]
        for neuron in network[i]:
            for j in range(len(inputs)):
                neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j]
            neuron['weights'][-1] += l_rate * neuron['delta']

# 训练网络(手动指定批次数 n_epoch)
def train_network(network, train, l_rate, n_epoch, n_outputs):
    for epoch in range(n_epoch):
    sum_error = 0
    for row in train:
        outputs = forward_propagate(network, row)
        expected = [0 for i in range(n_outputs)]
        expected[row[-1]] = 1
        sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))])
        backward_propagate_error(network, expected)
        update_weights(network, row, l_rate)
    print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))

# 测试反向传播算法实现的训练过程
seed(1)
dataset = [[2.7810836,2.550537003,0],
	[1.465489372,2.362125076,0],
	[3.396561688,4.400293529,0],
	[1.38807019,1.850220317,0],
	[3.06407232,3.005305973,0],
	[7.627531214,2.759262235,1],
	[5.332441248,2.088626775,1],
	[6.922596716,1.77106367,1],
	[8.675418651,-0.242068655,1],
	[7.673756466,3.508563011,1]]
n_inputs = len(dataset[0]) - 1
n_outputs = len(set([row[-1] for row in dataset]))
network = initialize_network(n_inputs, 2, n_outputs)
train_network(network, dataset, 0.5, 20, n_outputs)
for layer in network:
    print(layer)

运行代码,首相将看到每个训练批次结束时打印的平方和误差。整体来看误差呈下降趋势。

训练过程一结束,就可以看到网络的输出,其中包括网络习得的权重。除此之外,网络的输出和小到可以忽略的delta值也一同打印了出来。如果需要的话,我们可以更新我们的网络训练函数在训练结束后删除这些数据。

>epoch=0, lrate=0.500, error=6.350
>epoch=1, lrate=0.500, error=5.531
>epoch=2, lrate=0.500, error=5.221
>epoch=3, lrate=0.500, error=4.951
>epoch=4, lrate=0.500, error=4.519
>epoch=5, lrate=0.500, error=4.173
>epoch=6, lrate=0.500, error=3.835
>epoch=7, lrate=0.500, error=3.506
>epoch=8, lrate=0.500, error=3.192
>epoch=9, lrate=0.500, error=2.898
>epoch=10, lrate=0.500, error=2.626
>epoch=11, lrate=0.500, error=2.377
>epoch=12, lrate=0.500, error=2.153
>epoch=13, lrate=0.500, error=1.953
>epoch=14, lrate=0.500, error=1.774
>epoch=15, lrate=0.500, error=1.614
>epoch=16, lrate=0.500, error=1.472
>epoch=17, lrate=0.500, error=1.346
>epoch=18, lrate=0.500, error=1.233
>epoch=19, lrate=0.500, error=1.132
[{'output': 0.029980305604426185, 'weights': [-1.4688375095432327, 1.850887325439514, 1.0858178629550297], 'delta': -0.0059546604162323625}, {'output': 0.9456229000211323, 'weights': [0.37711098142462157, -0.0625909894552989, 0.2765123702642716], 'delta': 0.0026279652850863837}]
[{'output': 0.23648794202357587, 'weights': [2.515394649397849, -0.3391927502445985, -0.9671565426390275], 'delta': -0.04270059278364587}, {'output': 0.7790535202438367, 'weights': [-2.5584149848484263, 1.0036422106209202, 0.42383086467582715], 'delta': 0.03803132596437354}]

网络的训练一经完成,我们就需要用它来实现预测功能。

5.预测

用训练好的神经网络做出预测是很容易的。

我们已经看到了如何通过前向传播输入来获得输出。这是我们预测所需的全部过程。我们可以直接将输出值中每一行的值当作样本属于对应类的概率。

我们可以通过选择具有较大概率的类做为预测结果,这样输出就转换为了一个清晰的类别预测,这样的结果对我们来说更有帮助。实现这个操作的函数被称为argmax函数

下面是 predict() 函数,它实现了这个过程。它返回网络输出中最大概率值的索引。假定类的值已经转换为从0开始的整数。

# 利用网络的输出值进行预测
def predict(network, row):
    outputs = forward_propagate(network, row)
    return outputs.index(max(outputs))

我们可以把这个函数和上面实现前向输入的代码放在一起,用我们设计好的小数据集测试训练得到的网络的预测功能。不过现在,我们先暂时把上面训练得到的网络硬编码下来进行测试。

例子的完整代码如下所示。

from math import exp

# 计算给定输入情况下神经元的激活值
def activate(weights, inputs):
	activation = weights[-1]
	for i in range(len(weights)-1):
		activation += weights[i] * inputs[i]
	return activation

# 传递神经元的激活值
def transfer(activation):
	return 1.0 / (1.0 + exp(-activation))

# 从网络输入到网络输出的前向传播
def forward_propagate(network, row):
	inputs = row
	for layer in network:
		new_inputs = []
		for neuron in layer:
			activation = activate(neuron['weights'], inputs)
			neuron['output'] = transfer(activation)
			new_inputs.append(neuron['output'])
		inputs = new_inputs
	return inputs

# 利用网络的输出值进行预测
def predict(network, row):
	outputs = forward_propagate(network, row)
	return outputs.index(max(outputs))

# 测试网络的预测功能
dataset = [[2.7810836,2.550537003,0],
	[1.465489372,2.362125076,0],
	[3.396561688,4.400293529,0],
	[1.38807019,1.850220317,0],
	[3.06407232,3.005305973,0],
	[7.627531214,2.759262235,1],
	[5.332441248,2.088626775,1],
	[6.922596716,1.77106367,1],
	[8.675418651,-0.242068655,1],
	[7.673756466,3.508563011,1]]
network = [[{'weights': [-1.482313569067226, 1.8308790073202204, 1.078381922048799]}, {'weights': [0.23244990332399884, 0.3621998343835864, 0.40289821191094327]}],
	[{'weights': [2.5001872433501404, 0.7887233511355132, -1.1026649757805829]}, {'weights': [-2.429350576245497, 0.8357651039198697, 1.0699217181280656]}]]
for row in dataset:
	prediction = predict(network, row)
	print('Expected=%d, Got=%d' % (row[-1], prediction))

运行代码,可以看到训练数据集中每个样本的预期输出以及网络根据输入作出的预测。

输出结果表明网络在这个小数据集上达到了100%的准确性。

Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1

现在我们准备将反向传播算法应用到真实世界的数据集中。

6.小麦种子数据集

本节将反向传播算法应用于小麦种子数据集。

第一步是加载数据集并将加载的数据转换为我们可以在我们的神经网络中使用的数值量。为此,我们将使用辅助函数 load_csv() 来加载文件,用 str_column_to_float() 函数将字符串数字转换为float类型,str_column_to_int() 将整数列转换为int类型。

各个输入值的变化范围大小不同,需要归一化至0-1的范围,将输入值归一化至传递函数的范围内是一个很好的习惯。在本文中,我们使用的sigmoid函数的输出区间为0-1,使用 dataset_minmax()normalize_dataset() 辅助函数对输入值进行归一化。

我们将使用5次交叉验证来评估算法(K次交叉验证)。这意味着每一份中包含着 40/41 个样本记录。后面我们会使用辅助函数 evalute_algotithm() 来对算法进行交叉验证,用 accuracy_metric() 函数来计算预测的准确性。

除此之外,我们新开发了 back_propagation() 函数来管理反向传播算法的应用,首先初始化网络,然后在训练数据集上进行训练,最后使用训练好的网络在测试数据集上进行预测。

完整的代码示例如下所示。

# 在种子数据集上实现反向传播

from random import seed
from random import randrange
from random import random
from csv import reader
from math import exp

# 载入CSV数据
def load_csv(filename):
	dataset = list()
	with open(filename, 'r') as file:
		csv_reader = reader(file)
		for row in csv_reader:
			if not row:
				continue
			dataset.append(row)
	return dataset

# 将字符串的列转化为float类型
def str_column_to_float(dataset, column):
	for row in dataset:
		row[column] = float(row[column].strip())

# 将字符串的列转化为int类型
def str_column_to_int(dataset, column):
	class_values = [row[column] for row in dataset]
	unique = set(class_values)
	lookup = dict()
	for i, value in enumerate(unique):
		lookup[value] = i
	for row in dataset:
		row[column] = lookup[row[column]]
	return lookup

# 找到每一列的最小值和最大值
def dataset_minmax(dataset):
	minmax = list()
	stats = [[min(column), max(column)] for column in zip(*dataset)]
	return stats

# 将数据集归一化至0-1的范围内
def normalize_dataset(dataset, minmax):
	for row in dataset:
		for i in range(len(row)-1):
			row[i] = (row[i] - minmax[i][0]) / (minmax[i][1] - minmax[i][0])

# 将数据集分为k份
def cross_validation_split(dataset, n_folds):
	dataset_split = list()
	dataset_copy = list(dataset)
	fold_size = int(len(dataset) / n_folds)
	for i in range(n_folds):
		fold = list()
		while len(fold) < fold_size:
			index = randrange(len(dataset_copy))
			fold.append(dataset_copy.pop(index))
		dataset_split.append(fold)
	return dataset_split

# 计算准确率
def accuracy_metric(actual, predicted):
	correct = 0
	for i in range(len(actual)):
		if actual[i] == predicted[i]:
			correct += 1
	return correct / float(len(actual)) * 100.0

# 用分割好的数据集来评估算法
def evaluate_algorithm(dataset, algorithm, n_folds, *args):
	folds = cross_validation_split(dataset, n_folds)
	scores = list()
	for fold in folds:
		train_set = list(folds)
		train_set.remove(fold)
		train_set = sum(train_set, [])
		test_set = list()
		for row in fold:
			row_copy = list(row)
			test_set.append(row_copy)
			row_copy[-1] = None
		predicted = algorithm(train_set, test_set, *args)
		actual = [row[-1] for row in fold]
		accuracy = accuracy_metric(actual, predicted)
		scores.append(accuracy)
	return scores

# 计算给定输入情况下神经元的激活值
def activate(weights, inputs):
	activation = weights[-1]
	for i in range(len(weights)-1):
		activation += weights[i] * inputs[i]
	return activation

# 传递神经元的激活值
def transfer(activation):
	return 1.0 / (1.0 + exp(-activation))

# 前向传播输入得到输出
def forward_propagate(network, row):
	inputs = row
	for layer in network:
		new_inputs = []
		for neuron in layer:
			activation = activate(neuron['weights'], inputs)
			neuron['output'] = transfer(activation)
			new_inputs.append(neuron['output'])
		inputs = new_inputs
	return inputs

# 计算神经元输出值的导数
def transfer_derivative(output):
	return output * (1.0 - output)

# 误差反向传播并将误差存储在神经元中
def backward_propagate_error(network, expected):
	for i in reversed(range(len(network))):
		layer = network[i]
		errors = list()
		if i != len(network)-1:
			for j in range(len(layer)):
				error = 0.0
				for neuron in network[i + 1]:
					error += (neuron['weights'][j] * neuron['delta'])
				errors.append(error)
		else:
			for j in range(len(layer)):
				neuron = layer[j]
				errors.append(expected[j] - neuron['output'])
		for j in range(len(layer)):
			neuron = layer[j]
			neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

# 根据误差更新网络权重
def update_weights(network, row, l_rate):
	for i in range(len(network)):
		inputs = row[:-1]
		if i != 0:
			inputs = [neuron['output'] for neuron in network[i - 1]]
		for neuron in network[i]:
			for j in range(len(inputs)):
				neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j]
			neuron['weights'][-1] += l_rate * neuron['delta']

# 训练网络(手动指定批次数 n_epoch)
def train_network(network, train, l_rate, n_epoch, n_outputs):
	for epoch in range(n_epoch):
		for row in train:
			outputs = forward_propagate(network, row)
			expected = [0 for i in range(n_outputs)]
			expected[row[-1]] = 1
			backward_propagate_error(network, expected)
			update_weights(network, row, l_rate)


# 初始化网络
def initialize_network(n_inputs, n_hidden, n_outputs):
	network = list()
	hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
	network.append(hidden_layer)
	output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
	network.append(output_layer)
	return network

# 使用网络进行预测
def predict(network, row):
	outputs = forward_propagate(network, row)
	return outputs.index(max(outputs))

# 使用随机梯度下降实现反向传播算法
def back_propagation(train, test, l_rate, n_epoch, n_hidden):
	n_inputs = len(train[0]) - 1
	n_outputs = len(set([row[-1] for row in train]))
	network = initialize_network(n_inputs, n_hidden, n_outputs)
	train_network(network, train, l_rate, n_epoch, n_outputs)
	predictions = list()
	for row in test:
		prediction = predict(network, row)
		predictions.append(prediction)
	return(predictions)

# 在种子数据集上测试反向传播
seed(1)

# 导入准备的数据
filename = 'seeds_dataset.csv'
dataset = load_csv(filename)
for i in range(len(dataset[0])-1):
	str_column_to_float(dataset, i)

# 将类别一类转换为int类型
str_column_to_int(dataset, len(dataset[0])-1)

# 输入变量归一化
minmax = dataset_minmax(dataset)
normalize_dataset(dataset, minmax)

# 评估算法
n_folds = 5
l_rate = 0.3
n_epoch = 500
n_hidden = 5
scores = evaluate_algorithm(dataset, back_propagation, n_folds, l_rate, n_epoch, n_hidden)
print('Scores: %s' % scores)
print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores))))

我们在代码中构建了具有5个神经元的隐藏层和具有3个神经元的输出层的网络。网络训练批次数为500,学习率为0.3。这些参数时经过少量试错后得到的,也许你可以找到更好的组合。

运行代码,每一份数据的平均分类准确率和整体的性能将被分别打印出来。

可以看到所选配置下反向传播算法的平均分类准确率达到了95.238%,比零规则算法的准确度提高了28.095%。

Scores: [95.23809523809523, 97.61904761904762, 95.23809523809523, 92.85714285714286, 95.23809523809523]
Mean Accuracy: 95.238%

扩展

本节列出了你可能希望探索尝试的扩展功能。

  • 算法参数调优。选用更大规模或更小规模的网络,调整训练批次更大或更小。看看能否在种子数据集上获得更好的性能。
  • 其他方法。试用不同的权重初始化技术(如正态分布随机,平均分布随机等)和不同的传递函数(如tanh)。
  • 更多层。增加对更多隐藏层的支持,训练方式与本教程中使用的隐藏层相同。
  • 回归。使网络输出层中只有一个神经元,并用它预测实值。选择一个回归数据集来练习。输出层中的神经元可以使用线性传递函数作为传递函数,或者将所选数据集的输出值缩放到0到1之间的值。
  • 批量梯度下降。将训练过程从在线学习更改为批量梯度下降,并仅在每个epoch结束时更新权重。

如果你尝试了以上扩展,欢迎在下面的评论中分享你的经验。

总结回顾

在本教程中,你了解了如何从头开始构建反向传播算法。

具体来说,你了解到:

  • 如何前向传播输入来计算网络输出。
  • 如何反向传播误差并更新网络权重。
  • 如何将反向传播算法应用于真实世界的数据集。

翻译人:ArrayZoneYour,该成员来自云+社区翻译社

原文链接:https://machinelearningmastery.com/implement-backpropagation-algorithm-scratch-python/

原文作者:Jason Brownlee

发表于 11天前
8

ArrayZoneYour的专栏

11 篇文章1 人订阅

0 条评论

相关文章