14-神经网络的实现
7.4.3 神经网络的实现
神经网络对象本身只包含一种状态数据,即神经网络管理的层对象。 Network
类负责初始化其构成层。
__init__()
方法的参数为描述网络结构的 int
列表。例如,列表 [2, 4, 3]
描述的网络为:输入层有2个神经元,隐藏层有4个神经元,输出层有3个神经元。在这个简单的网络中,假设网络中的所有层都将采用相同的神经元激活函数和学习率。具体代码如代码清单7-7所示。
代码清单7-7 network.py
from __future__ import annotations
from typing import List, Callable, TypeVar, Tuple
from functools import reduce
from layer import Layer
from util import sigmoid, derivative_sigmoid
T = TypeVar('T') # output type of interpretation of neural network
class Network:
def __init__(self, layer_structure: List[int], learning_rate: float,
activation_function: Callable[[float], float] = sigmoid,
derivative_activation_function:
Callable[[float], float] = derivative_sigmoid) -> None:
if len(layer_structure) < 3:
raise ValueError("Error: Should be at least 3 layers (1 input, 1 hidden,
1 output)")
self.layers: List[Layer] = []
# input layer
input_layer: Layer = Layer(None, layer_structure[0], learning_rate,
activation_function, derivative_activation_function)
self.layers.append(input_layer)
# hidden layers and output layer
for previous, num_neurons in enumerate(layer_structure[1::]):
next_layer = Layer(self.layers[previous], num_neurons, learning_rate,
active ation_function, derivative_activation_function)
self.layers.append(next_layer)
神经网络的输出是信号经由其所有层传递之后的结果。注意简洁的 reduce()
函数是如何用在 outputs()
中,将信号反复从一层传递到下一层,进而传遍整个网络的。具体代码如代码清单7-8所示。
代码清单7-8 network.py(续)
# Pushes input data to the first layer, then output from the first
# as input to the second, second to the third, etc.
def outputs(self, input: List[float]) -> List[float]:
return reduce((lambda inputs, layer: layer.outputs(inputs)), self.layers, input)
backpropagate()
方法负责计算网络中每个神经元的delta。它会依次调用 Layer
类的 calculate_deltas_for_output_layer()
方法和 calculate_deltas_for_hidden_layer()
方法。还记得吧,在反向传播时要反向计算delta。它会把给定输入集的预期输出值传递给 calculate_ deltas_for_output_layer()
。该方法将用预期输出值求出误差,以供计算delta时使用。具体代码如代码清单7-9所示。
代码清单7-9 network.py(续)
# Figure out each neuron's changes based on the errors of the output
# versus the expected outcome
def backpropagate(self, expected: List[float]) -> None:
# calculate delta for output layer neurons
last_layer: int = len(self.layers) - 1
self.layers[last_layer].calculate_deltas_for_output_layer(expected)
# calculate delta for hidden layers in reverse order
for l in range(last_layer - 1, 0, -1):
self.layers[l].calculate_deltas_for_hidden_layer(self.layers[l + 1])
backpropagate()
的确负责计算所有的delta,但它不会真的去修改网络中的权重。 update_weights()
必须在 backpropagate()
之后才能被调用,因为权重的修改依赖delta。 update_weights()
方法直接来自图7-6中的公式。具体代码如代码清单7-10所示。
代码清单7-10 network.py(续)
# backpropagate() doesn't actually change any weights
# this function uses the deltas calculated in backpropagate() to
# actually make changes to the weights
def update_weights(self) -> None:
for layer in self.layers[1:]: # skip input layer
for neuron in layer.neurons:
for w in range(len(neuron.weights)):
neuron.weights[w] = neuron.weights[w] + (neuron.learning_rate *
(layer.previous_layer.output_cache[w]) * neuron.delta)
在每轮训练结束时,会对神经元的权重进行修改。必须向神经网络提供训练数据集(同时给出输入与预期的输出)。 train()
方法的参数即为输入列表的列表和预期输出列表的列表。 train()
方法在神经网络上运行每一组输入,然后以预期输出为参数调用 backpropagate()
,然后再调用 update_weights()
,以更新网络的权重。不妨试着在 train()
方法中添加代码,使得在神经网络中传递训练数据集时能把误差率打印出来,以便于查看梯度下降过程中误差率是如何逐渐降低的。具体代码如代码清单7-11所示。
代码清单7-11 network.py(续)
# train() uses the results of outputs() run over many inputs and compared
# against expecteds to feed backpropagate() and update_weights()
def train(self, inputs: List[List[float]], expecteds: List[List[float]]) -> None:
for location, xs in enumerate(inputs):
ys: List[float] = expecteds[location]
outs: List[float] = self.outputs(xs)
self.backpropagate(ys)
self.update_weights()
在神经网络经过训练后,我们需要对其进行测试。 validate()
的参数为输入和预期输出(与 train()
的参数没什么区别),但它们不会用于训练,而会用来计算准确度的百分比。这里假定网络已经过训练。 validate()
还有一个参数是函数 interpret_output()
,该函数用于解释神经网络的输出,以便将其与预期输出进行比较。或许预期输出不是一组浮点数,而是像 "Amphibian"
这样的字符串。 interpret_output()
必须读取作为网络输出的浮点数,并将其转换为可以与预期输出相比较的数据。 interpret_output()
是特定于某数据集的自定义函数。 validate()
将返回分类成功的类别数量、通过测试的样本总数和成功分类的百分比。具体代码如代码清单7-12所示。
代码清单7-12 network.py(续)
# for generalized results that require classification
# this function will return the correct number of trials
# and the percentage correct out of the total
def validate(self, inputs: List[List[float]], expecteds: List[T], interpret_output:
Callable[[List[float]], T]) -> Tuple[int, int, float]:
correct: int = 0
for input, expected in zip(inputs, expecteds):
result: T = interpret_output(self.outputs(input))
if result == expected:
correct += 1
percentage: float = correct / len(inputs)
return correct, len(inputs), percentage
至此神经网络就完工了!已经可以用来进行一些实际问题的测试了。虽然此处构建的架构是通用的,足以应对各种不同的问题,但这里将重点解决一种流行的问题,即分类问题。