定义一些代码中使用的缩写或约定。这些约定可以帮助理解代码的工作原理。以下是对每个
缩写或约定的解释:
1. "pd_"作为变量前缀意味着"partial derivative"(偏导数)。在计算反向传播时,我们需要计算损
失函数相对于模型参数的偏导数。
2. "d_"作为变量前缀表示"derivative"(导数)。导数是一个函数在某一点的斜率,它表示函数在该
点的变化率。
3. "_wrt_"是"with respect to"(相对于)的缩写。在计算导数或偏导数时,我们需要明确我们是相
对于哪个变量进行计算的。
4. "w_ho" 和 "w_ih" 是神经网络中的权重索引。"w_ho" 表示从隐藏层到输出层神经元的权
重,"w_ih" 表示从输入层到隐藏层神经元的权重。
这些缩写和约定在神经网络和反向传播算法的代码实现中很常见。
class NeuralNetwork:
# 神经网络类
LEARNING_RATE = 0.5
# 设置学习率为0.5
def __init__(self, num_inputs, num_hidden, num_outputs, hidden_layer_weights=None, hidden_layer_bias=None,
output_layer_weights=None, output_layer_bias=None):
# 初始化一个三层神经网络结构
self.num_inputs = num_inputs
self.hidden_layer = NeuronLayer(num_hidden, hidden_layer_bias)
self.output_layer = NeuronLayer(num_outputs, output_layer_bias)
self.init_weights_from_inputs_to_hidden_layer_neurons(hidden_layer_weights)
self.init_weights_from_hidden_layer_neurons_to_output_layer_neurons(output_layer_weights)
def init_weights_from_inputs_to_hidden_layer_neurons(self, hidden_layer_weights):
weight_num = 0
for h in range(len(self.hidden_layer.neurons)): # num_hidden,遍历隐藏层
for i in range(self.num_inputs): # 遍历输入层
if not hidden_layer_weights:
# 如果hidden_layer_weights的值为空,则利用随机化函数对其进行赋值,否则利用hidden_layer_weights中的值对其进行更新
self.hidden_layer.neurons[h].weights.append(random.random())
else:
self.hidden_layer.neurons[h].weights.append(hidden_layer_weights[weight_num])
weight_num += 1
def init_weights_from_hidden_layer_neurons_to_output_layer_neurons(self, output_layer_weights):
weight_num = 0
for o in range(len(self.output_layer.neurons)): # num_outputs,遍历输出层
for h in range(len(self.hidden_layer.neurons)): # 遍历输出层
if not output_layer_weights:
# 如果output_layer_weights的值为空,则利用随机化函数对其进行赋值,否则利用output_layer_weights中的值对其进行更新
self.output_layer.neurons[o].weights.append(random.random())
else:
self.output_layer.neurons[o].weights.append(output_layer_weights[weight_num])
weight_num += 1
def inspect(self): # 输出神经网络信息
print('------')
print('* Inputs: {}'.format(self.num_inputs))
print('------')
print('Hidden Layer')
self.hidden_layer.inspect()
print('------')
print('* Output Layer')
self.output_layer.inspect()
print('------')
def feed_forward(self, inputs): # 返回输出层y值
hidden_layer_outputs = self.hidden_layer.feed_forward(inputs)
return self.output_layer.feed_forward(hidden_layer_outputs)
# Uses online learning, ie updating the weights after each training case
# 使用在线学习方式,训练每个实例之后对权值进行更新
def train(self, training_inputs, training_outputs):
self.feed_forward(training_inputs)
# 反向传播
# 1. Output neuron deltas输出层deltas
pd_errors_wrt_output_neuron_total_net_input = [0]*len(self.output_layer.neurons)
for o in range(len(self.output_layer.neurons)):
# 对于输出层∂E/∂zⱼ=∂E/∂a*∂a/∂z=cost'(target_output)*sigma'(z)
pd_errors_wrt_output_neuron_total_net_input[o] = self.output_layer.neurons[
o].calculate_pd_error_wrt_total_net_input(training_outputs[o])
# 2. Hidden neuron deltas隐藏层deltas
pd_errors_wrt_hidden_neuron_total_net_input = [0]*len(self.hidden_layer.neurons)
for h in range(len(self.hidden_layer.neurons)):
# We need to calculate the derivative of the error with respect to the output of each hidden layer neuron
# 我们需要计算误差对每个隐藏层神经元的输出的导数,由于不是输出层所以dE/dyⱼ需要根据下一层反向进行计算,即根据输出层的函数进行计算
# dE/dyⱼ = Σ ∂E/∂zⱼ * ∂z/∂yⱼ = Σ ∂E/∂zⱼ * wᵢⱼ
d_error_wrt_hidden_neuron_output = 0
for o in range(len(self.output_layer.neurons)):
d_error_wrt_hidden_neuron_output += pd_errors_wrt_output_neuron_total_net_input[o]* \
self.output_layer.neurons[o].weights[h]
# ∂E/∂zⱼ = dE/dyⱼ * ∂zⱼ/∂
pd_errors_wrt_hidden_neuron_total_net_input[h] = d_error_wrt_hidden_neuron_output*self.hidden_layer.neurons[
h].calculate_pd_total_net_input_wrt_input()
# 3. Update output neuron weights 更新输出层权重
for o in range(len(self.output_layer.neurons)):
for w_ho in range(len(self.output_layer.neurons[o].weights)):
# 注意:输出层权重是隐藏层神经元与输出层神经元连接的权重
# ∂Eⱼ/∂wᵢⱼ = ∂E/∂zⱼ * ∂zⱼ/∂wᵢⱼ
pd_error_wrt_weight = pd_errors_wrt_output_neuron_total_net_input[o]*self.output_layer.neurons[
o].calculate_pd_total_net_input_wrt_weight(w_ho)
# Δw = α * ∂Eⱼ/∂wᵢ
self.output_layer.neurons[o].weights[w_ho] -= self.LEARNING_RATE*pd_error_wrt_weight
# 4. Update hidden neuron weights 更新隐藏层权重
for h in range(len(self.hidden_layer.neurons)):
for w_ih in range(len(self.hidden_layer.neurons[h].weights)):
# 注意:隐藏层权重是输入层神经元与隐藏层神经元连接的权重
# ∂Eⱼ/∂wᵢ = ∂E/∂zⱼ * ∂zⱼ/∂wᵢ
pd_error_wrt_weight = pd_errors_wrt_hidden_neuron_total_net_input[h]*self.hidden_layer.neurons[
h].calculate_pd_total_net_input_wrt_weight(w_ih)
# Δw = α * ∂Eⱼ/∂wᵢ
self.hidden_layer.neurons[h].weights[w_ih] -= self.LEARNING_RATE*pd_error_wrt_weight
def calculate_total_error(self, training_sets):
# 使用平方差计算训练集误差
total_error = 0
for t in range(len(training_sets)):
training_inputs, training_outputs = training_sets[t]
self.feed_forward(training_inputs)
for o in range(len(training_outputs)):
total_error += self.output_layer.neurons[o].calculate_error(training_outputs[o])
return total_error
__init__
: 这个方法是在创建神经网络对象时被调用的。它的参数有:
num_inputs
: 输入层的神经元数量。num_hidden
: 隐藏层的神经元数量。num_outputs
: 输出层的神经元数量。hidden_layer_weights
: 隐藏层神经元的初始权重(可选)。hidden_layer_bias
: 隐藏层的偏置值(可选)。output_layer_weights
: 输出层神经元的初始权重(可选)。output_layer_bias
: 输出层的偏置值(可选)。 在构造函数中,它首先保存了num_inputs
。然后,它创建了一个隐藏层和一个输出层。之
后,它初始化从输入层到隐藏层和从隐藏层到输出层的权重。
init_weights_from_inputs_to_hidden_layer_neurons(self, hidden_layer_weights)
: 这个
方法负责初始化从输入层到隐藏层的权重。它首先遍历每个隐藏层神经元,然后在每个隐藏层神经
元内部遍历每个输入。如果没有提供hidden_layer_weights
,则为该链接生成一个随机权重。如果
提供了hidden_layer_weights
,则使用提供的权重。每个隐藏层神经元的权重是一个数组,它的长
度与输入层神经元的数量相同。
init_weights_from_hidden_layer_neurons_to_output_layer_neurons(self,
output_layer_weights)
: 这个方法负责初始化从隐藏层到输出层的权重。它首先遍历每个输出层神
经元,然后在每个输出层神经元内部遍历每个隐藏层神经元。如果没有提供
output_layer_weights
,则为该链接生成一个随机权重。如果提供了output_layer_weights
,则使
用提供的权重。每个输出层神经元的权重是一个数组,它的长度与隐藏层神经元的数量相同。
这两个方法的目的是创建一个全连接神经网络,其中每个神经元与前一层的每个神经元都有
一个权重连接。这些权重在训练神经网络时将会更新,以最小化网络的输出误差。
inspect(self)
: 这个方法打印出神经网络的详细信息。它首先打印出输入的数量,然后描述
隐藏层和输出层。对于隐藏层和输出层,这个方法调用了它们各自的inspect
方法来打印相关信
息。这可能包括每层的神经元数量,以及每个神经元的权重和偏置。
feed_forward(self, inputs)
: 这个方法实现了神经网络的前向传播过程,也就是根据输入数
据通过神经网络计算输出结果。这个过程从输入层开始,然后逐层通过网络,直到输出层。具体来
说,它首先将输入数据传递给隐藏层,然后将隐藏层的输出作为输入传递给输出层。最后,输出层
的输出就是神经网络的最终输出。在这个过程中,每一层的feed_forward
方法都会根据其神经元的
权重和偏置,以及激活函数来计算其输出。
train函数接受两个参数:training_inputs(训练输入)和 training_outputs(训练输出),
这代表了一个训练样本的输入和期望输出。
self.feed_forward(training_inputs)是前向传播的步骤,它将输入数据传递通过神经网络,计算
神经网络的输出。在这个过程中,每个神经元的激活(输出)以及各层的总输入(加权输入)都会
被计算。
接下来是反向传播,这是神经网络训练的核心部分。反向传播用于调整神经元之间的连接权
重,以减小预测输出与实际输出之间的误差。这个过程分为几个步骤:
计算输出层神经元的误差(`pd_errors_wrt_output_neuron_total_net_input`):对于输出层
的每个神经元,首先计算输出的误差,即实际输出与期望输出之间的差,乘以激活函数对总输入的
导数。
计算隐藏层神经元的误差(`pd_errors_wrt_hidden_neuron_total_net_input`):对于隐藏层
的每个神经元,需要计算其误差。这个误差是由于输出层神经元误差反向传播而来,根据权重的贡
献进行分配。
接下来是权重的更新:
更新输出层神经元的权重:对于输出层的每个神经元和与之连接的权重,计算权重的梯度
(即误差对权重的导数),然后使用学习率乘以梯度来更新权重。
更新隐藏层神经元的权重:对于隐藏层的每个神经元和与之连接的权重,同样计算权重的梯
度,然后使用学习率乘以梯度来更新权重。
calculate_total_error(self, training_sets)这个方法是用来计算神经网络在训练集上的总体误差
的,主要步骤如下:
遍历训练集中的每个训练样本;
对于每个训练样本,进行前向传播,计算网络输出;
对比网络实际输出和样本的期望输出,计算单个样本的误差;
这里使用平方差误差计算方法(calculate_error函数实现),即对每个输出节点,计算(网络输出 - 期
望输出)^2;
将所有样本的误差累加求和,得到整个训练集上的总体误差。
class NeuronLayer:
# 神经层类
def __init__(self, num_neurons, bias):
# Every neuron in a layer shares the same bias 一层中的所有神经元共享一个bias
self.bias = bias if bias else random.random()
random.random()
# 生成0和1之间的随机浮点数float,它其实是一个隐藏的random.Random类的实例的random方法。
# random.random()和random.Random().random()作用是一样的。
self.neurons = []
for i in range(num_neurons):
self.neurons.append(Neuron(self.bias))
# 在神经层的初始化函数中对每一层的bias赋值,利用神经元的init函数对神经元的bias赋值
def inspect(self):
# print该层神经元的信息
print('Neurons:', len(self.neurons))
for n in range(len(self.neurons)):
print(' Neuron', n)
for w in range(len(self.neurons[n].weights)):
print(' Weight:', self.neurons[n].weights[w])
print(' Bias:', self.bias)
def feed_forward(self, inputs):
# 前向传播过程outputs中存储的是该层每个神经元的y/a的值(经过神经元激活函数的值有时被称为y有时被称为a)
outputs = []
for neuron in self.neurons:
outputs.append(neuron.calculate_output(inputs))
return outputs
def get_outputs(self):
outputs = []
for neuron in self.neurons:
outputs.append(neuron.output)
return outputs
NeuronLayer 这个类有两个主要的属性:`bias` 和 `neurons`。
__init__(self, num_neurons, bias): 这是类的初始化方法,它接受两个参数:
num_neurons: 这个参数指定了该层要包含的神经元的数量。
bias: 这个参数指定了该层的偏置值。在神经网络中,偏置是一个可调整的参数,可以改变每个
神经元的激活阈值。在这个类中,所有神经元共享一个偏置值。
在这个方法中,首先检查是否提供了偏置值,如果没有提供(即 bias 为 `None` 或者
`False`),则使用 random.random() 生成一个 0 到 1 之间的随机浮点数作为偏置值。
接下来,它创建一个空的 neurons 列表,然后用一个循环创建指定数量的 Neuron 对象,并将
它们添加到 neurons 列表中。每个 Neuron 对象在创建时都会接收到当前层的偏置值。
inspect
方法: 这个方法用于打印出这一层的信息,包括神经元数量、每个神经元的权重以及
层的偏置:
首先,它使用print
方法来显示这一层有多少个神经元。
接着,对于这一层中的每个神经元,它显示出神经元的序号、权重和偏置值。
feed_forward
方法: 这是前向传播的过程。这个方法接受输入inputs
(前一层神经元的输
出),并为这一层的每个神经元计算输出值:
它首先创建一个空的outputs
列表。
对于这一层的每个神经元,它会调用神经元的calculate_output
方法(假设这个方法是用于
计算神经元的输出值的)来计算输出值,并将其添加到outputs
列表中。
最后,这个方法返回outputs
列表,这个列表包含了这一层所有神经元的输出值。
get_outputs
方法: 这个方法用于获取这一层每个神经元的输出值:
同样,它首先创建一个空的outputs
列表。
对于这一层的每个神经元,它获取该神经元的output
属性值(假设每个神经元对象都有一个
output
属性,存储了神经元的最后输出值),并将其添加到outputs
列表中。
这个方法最后返回outputs
列表。
class Neuron:
# 神经元类
def __init__(self, bias):
self.bias = bias
self.weights = []
def calculate_output(self, inputs):
self.inputs = inputs
self.output = self.squash(self.calculate_total_net_input())
# output即为输入即为y(a)意为从激活函数中的到的值
return self.output
def calculate_total_net_input(self):
# 此处计算的为激活函数的输入值即z=W(n)x+b
total = 0
for i in range(len(self.inputs)):
total += self.inputs[i]*self.weights[i]
return total + self.bias
# Apply the logistic function to squash the output of the neuron
# 使用sigmoid函数为激励函数,一下是sigmoid函数的定义
# The result is sometimes referred to as 'net' [2] or 'net' [1]
def squash(self, total_net_input):
return 1/(1 + math.exp(-total_net_input))
# Determine how much the neuron's total input has to change to move closer to the expected output
# 确定神经元的总输入需要改变多少,以接近预期的输出
# Now that we have the partial derivative of the error(Cost function) with respect to the output (∂E/∂yⱼ)
# 我们可以根据cost function对y(a)神经元激活函数输出值的偏导数和激活函数输出值y(a)对激活函数输入值z=wx+b的偏导数计算delta(δ).
# the derivative of the output with respect to the total net input (dyⱼ/dzⱼ) we can calculate
# the partial derivative of the error with respect to the total net input.
# This value is also known as the delta (δ) [1]
# δ = ∂E/∂zⱼ = ∂E/∂yⱼ * dyⱼ/dzⱼ 关键key
#
def calculate_pd_error_wrt_total_net_input(self, target_output):
return self.calculate_pd_error_wrt_output(target_output)*self.calculate_pd_total_net_input_wrt_input()
# The error for each neuron is calculated by the Mean Square Error method:
# 每个神经元的误差由平均平方误差法计算
def calculate_error(self, target_output):
return 0.5*(target_output - self.output) ** 2
# The partial derivate of the error with respect to actual output then is calculated by:
# 对实际输出的误差的偏导是通过计算得到的--即self.output(y也常常用a表示经过激活函数后的值)
# = 2 * 0.5 * (target output - actual output) ^ (2 - 1) * -1
# = -(target output - actual output)BP算法最后隐层求cost function 对a求导
#
# The Wikipedia article on backpropagation [1] simplifies to the following, but most other learning material does not [2]
# 维基百科关于反向传播[1]的文章简化了以下内容,但大多数其他学习材料并没有简化这个过程[2]
# = actual output - target output
#
# Note that the actual output of the output neuron is often written as yⱼ and target output as tⱼ so:
# = ∂E/∂yⱼ = -(tⱼ - yⱼ)
# 注意我们一般将输出层神经元的输出为yⱼ,而目标标签(正确答案)表示为tⱼ.
def calculate_pd_error_wrt_output(self, target_output):
return -(target_output - self.output)
# The total net input into the neuron is squashed using logistic function to calculate the neuron's output:
# yⱼ = φ = 1 / (1 + e^(-zⱼ)) 注意我们对于神经元使用的激活函数都是logistic函数
# Note that where ⱼ represents the output of the neurons in whatever layer we're looking at and ᵢ represents the layer below it
# 注意我们用j表示我们正在看的这层神经元的输出,我们用i表示这层的后一层的神经元.
# The derivative (not partial derivative since there is only one variable) of the output then is:
# dyⱼ/dzⱼ = yⱼ * (1 - yⱼ)这是sigmoid函数的导数表现形式.
def calculate_pd_total_net_input_wrt_input(self):
return self.output*(1 - self.output)
# The total net input is the weighted sum of all the inputs to the neuron and their respective weights:
# 激活函数的输入是所有输入的加权权重的总和
# = zⱼ = netⱼ = x₁w₁ + x₂w₂ ...
# The partial derivative of the total net input with respective to a given weight (with everything else held constant) then is:
# 总的净输入与给定的权重的偏导数(其他所有的项都保持不变)
# = ∂zⱼ/∂wᵢ = some constant + 1 * xᵢw₁^(1-0) + some constant ... = xᵢ
def calculate_pd_total_net_input_wrt_weight(self, index):
return self.inputs[index]
Neuron 的类代表神经网络中的单个神经元。这个类有四个主要的方法:
__init__(self, bias): 这是类的初始化方法。它接受一个参数 bias,该参数代表神经元的偏置。
这个方法中将偏置值存储在 self.bias 中,并初始化一个空的权重列表 self.weights。
calculate_output(self, inputs): 这个方法是用来计算神经元的输出值的。它接受一个输入列表
inputs,然后调用 calculate_total_net_input 方法来计算总输入,然后调用 squash 方法(sigmoid
函数)来将总输入转换为输出值。
calculate_total_net_input(self): 这个方法是用来计算神经元的总输入的,这是通过将每个输入
值与其相应的权重相乘,然后所有结果相加,最后加上偏置来实现的。
squash(self, total_net_input): 这个方法是sigmoid函数,将神经元的总输入转换为输出值。这
个函数可以将任何实数压缩到0和1之间,使其可以解释为概率值。
calculate_pd_error_wrt_total_net_input(self, target_output):这个函数用于计算损失函数(这
里使用的是平均平方误差)关于神经元总输入的偏导数。这个值在反向传播算法中更新神经元的权
重和偏置是非常重要的。
calculate_error(self, target_output):这个函数用于计算神经元的误差,这里使用的是平均平
方误差。误差是神经元输出值与目标输出值的差的平方的一半。
calculate_pd_error_wrt_output(self, target_output):这个函数用于计算损失函数关于神经元
输出的偏导数。这个值也是反向传播算法中更新神经元的权重和偏置的重要计算步骤。 calculate_pd_total_net_input_wrt_input(self):这个函数用于计算神经元的输出(经过激活函
数后的值)关于总输入的偏导数,也就是激活函数的导数。这里使用的是 logistic 函数(sigmoid
函数)作为激活函数,其导数可以简化为 output*(1 - output)
calculate_pd_total_net_input_wrt_weight(self, index):这个函数用于计算神经元的总输入关
于给定权重的偏导数。当我们只改变一个权重(其他权重保持不变)时,总输入相对于这个权重的
变化就是对应的输入值。
# Blog post example:
nn = NeuralNetwork(2, 2, 2, hidden_layer_weights=[0.15, 0.2, 0.25, 0.3], hidden_layer_bias=0.35,
output_layer_weights=[0.4, 0.45, 0.5, 0.55], output_layer_bias=0.6)
for i in range(10000):
nn.train([0.05, 0.1], [0.01, 0.99])
print(i, round(nn.calculate_total_error([[[0.05, 0.1], [0.01, 0.99]]]), 9)) # 截断处理只保留小数点后9位
# XOR example:
# training_sets = [
# [[0, 0], [0]],
# [[0, 1], [1]],
# [[1, 0], [1]],
# [[1, 1], [0]]
# ]
# nn = NeuralNetwork(len(training_sets[0][0]), 5, len(training_sets[0][1]))
# for i in range(10000):
# training_inputs, training_outputs = random.choice(training_sets)
# nn.train(training_inputs, training_outputs)
# print(i, nn.calculate_total_error(training_sets))