赞
踩
前面已经掌握了感知器和线性单元,接下来是另一种感知器:神经元。如果把每个神经元组合起来就可以构成一个神经网络。
神经元和感知器本质上是一样的,只不过我们说感知器的时候,它的激活函数是阶跃函数;而当我们说神经元时,激活函数往往选择为sigmoid函数或tanh函数。如下图所示:
图形如下:
可以看出其值的范围是0到1之间,而tanh函数则在-1到1之间。其导函数推导过程如下:
其中第一步用链式求导法即可,令t = 1+ e-z, 则 df/dz = df/dt * dt/dz = -1/(t)2 * (-e-z)
所以
接下来的内容跟作者的可能有点不一样,如果说反向传播,tensorflow提供的这个东东比较经典
https://google-developers.gonglchuangl.net/machine-learning/crash-course/backprop-scroll/
以下展示了一个全联接神经网络模型,每一层的输入是上一层的所有神经元的输出加权总和,当然也少不了偏置项。这是所谓的正向传播过程。
通过正向传播,可以计算出一个youtput, 该值和样本的标签y 之间的误差,还是用
回顾上节内容,为了降低误差,需要计算
得到之后,需要反向对输入(也就是上一级节点的输出加权和)进行调整,调整上一级节点各输出的权重w和偏置项b,使得E对于youtput的导数趋近于0. (比如y4所在节点的上一级节点是y2和y3所在的节点)
根据梯度下降算法和上节所述,优化公式为
w = w - η * E’(w) = w - η *
d
E
d
w
\frac{dE}{dw}
dwdE
接下来要求
d
E
d
w
\frac{dE}{dw}
dwdE
################### 数学推导Begin ######################
由于每层的输入是上一级输出的加权和
xj =
∑
i
=
1
n
y
i
\sum_{i=1}^{n} y_i
∑i=1nyi * wij
根据链式求导法则得到
d
E
d
w
i
j
\frac{dE}{dw_ij}
dwijdE
=
d
E
d
x
j
\frac{dE}{dx_j}
dxjdE *
d
x
j
d
w
i
j
\frac{dx_j}{dw_ij}
dwijdxj
=
d
E
d
x
j
\frac{dE}{dx_j}
dxjdE * yi -----------------------------(公式0)
再根据同一层的输出yj是输入xj的函数
d
E
d
x
j
\frac{dE}{dx_j}
dxjdE
=
d
E
d
y
j
\frac{dE}{dy_j}
dyjdE *
d
y
j
d
x
j
\frac{dy_j}{dx_j}
dxjdyj
=
d
E
d
y
j
\frac{dE}{dy_j}
dyjdE *
d
(
s
i
g
m
o
i
d
(
x
j
)
)
d
x
j
\frac{d (sigmoid(x_j))}{dx_j}
dxjd(sigmoid(xj))
=
d
E
d
y
j
\frac{dE}{dy_j}
dyjdE * yj(1-yj) --------------------------------(公式1)
下一步是求
d
E
d
y
j
\frac{dE}{dy_j}
dyjdE,这里输出层和隐藏层的推导不一样,分开推导
1)对于最后的输出层
由于E是输出y的函数
d
E
d
y
j
\frac{dE}{dy_j}
dyjdE
=
d
(
1
2
(
y
j
−
l
a
b
e
l
)
2
)
d
y
j
\frac{d(\frac{1}{2}(y_j-label)^2)}{dy_j}
dyjd(21(yj−label)2)
= yj-label --------------------------------(公式2)
2)对于中间的隐藏层
我这里采用的是tensorflow中的推导过程
假设节点i的输出yi 被作为下一级n个节点的输入xj, 那么E是xj的函数,xj是yi的函数,由于xj有n个,需要对各部分求导后求和
d
E
d
y
i
\frac{dE}{dy_i}
dyidE
=
∑
n
d
E
d
x
j
∗
d
x
j
d
y
i
\sum_{n}\frac{dE}{dx_j} * \frac{dx_j}{dy_i}
∑ndxjdE∗dyidxj
=
∑
n
d
E
d
x
j
\sum_{n}\frac{dE}{dx_j}
∑ndxjdE * wij --------------------------------(公式3)
/参考部分
ps:如果每个节点中都保存
d
E
d
x
j
\frac{dE}{dx_j}
dxjdE,计算到这里就可以了,以下部分作为参考,主要是已经推了这么多,删掉有点浪费?,以下结论跟作者的一致)
令δi= -
d
E
d
x
i
\frac{dE}{dx_i}
dxidE ,这个被作者成为误差项,在后面神经网络中使用了多次。
δi= -
d
E
d
x
i
\frac{dE}{dx_i}
dxidE
= -
d
E
d
y
i
\frac{dE}{dy_i}
dyidE *
d
y
i
d
x
i
\frac{dy_i}{dx_i}
dxidyi
= -
∑
n
d
E
d
x
j
\sum_{n}\frac{dE}{dx_j}
∑ndxjdE * wij *
d
y
i
d
x
i
\frac{dy_i}{dx_i}
dxidyi
= -
∑
n
−
δ
j
\sum_{n} -δ_j
∑n−δj * wij *
d
(
s
i
g
m
o
i
d
(
x
i
)
)
d
x
i
\frac{d (sigmoid(x_i))}{dx_i}
dxid(sigmoid(xi))
=
∑
n
δ
j
\sum_{n}δ_j
∑nδj * wij * yi(1-yi)
= yi(1-yi) *
∑
n
δ
j
\sum_{n}δ_j
∑nδj * wij
综上所述,
d
E
d
w
i
j
\frac{dE}{dw_ij}
dwijdE =
d
E
d
x
j
\frac{dE}{dx_j}
dxjdE * yi = -δi * yi
则对于输出层 δi = (label-yi) * yj(1-yj) (代入公式1)
对于隐藏层 δi = yi(1-yi) *
∑
n
δ
j
\sum_{n}δ_j
∑nδj * wij
/
################### 数学推导End ######################
好了,结合作者的思路,写一个全联接的神经网络。当然,用的是咱自己的推导公式。
首先观察模型的组成,为了解耦降低复杂度,大致将其分为网络network, 层layer,神经元节点node,连接connection。
可以看到一个神经元j为对象,包含的内容有,输入xj,输出yj,为了做反向传播,还要保存的
d
E
d
y
i
\frac{dE}{dy_i}
dyidE 和
d
E
d
x
i
\frac{dE}{dx_i}
dxidE ,还有输入链接,和输出链接。
因此,一个神经元节点的class如下
class Node(object): OUTPUT_LAYER_INDEX = -1 def __init__(self,layer_index,node_index): ''' 构造节点对象 layer_index: 节点所属层编号 , 为了方便输出层的编号 node_index: 节点编号 ''' self.layer_index = layer_index self.node_index = node_index self.input = 0 self.output = 0 self.dE_dy = 0 self.dE_dx = 0 self.downstream = [] self.upstream = []
当然还要有更新数据的能力
def set_output(self, output): ''' 设置节点的输出值。如果节点属于输入层会用到这个函数。 ''' self.output = output def append_downstream_connection(self, conn): ''' 添加一个到下游节点的连接 ''' self.downstream.append(conn) def append_upstream_connection(self, conn): ''' 添加一个到上游节点的连接 ''' self.upstream.append(conn) def calc_output(self): #通过上级连接计算输入 self.input = reduce(lambda result,conn: result + conn.upstream_node.output * conn.weight, self.upstream,0) #计算节点输出 y = sigmoid(x) self.output = sigmoid(self.input) def calc_dy_dx(self, label): ''' 计算dE_dy和dE_dx ''' if self.layer_index == self.OUTPUT_LAYER_INDEX: #根据输出层公式2 self.dE_dy = self.output-label else : #根据隐藏层公式3,通过下级连接的权重和下级节点dE_dx可反向求出 self.dE_dy = reduce( lambda ret, conn: ret + conn.downstream_node.dE_dx * conn.weight, self.downstream, 0.0) #根据公式1 self.dE_dx = self.dE_dy * self.output * (1-self.output)
可以看到,这里我采用tensorflow图中的表示法,缓存的是 d E d y i \frac{dE}{dy_i} dyidE 和 d E d x i \frac{dE}{dx_i} dxidE , 其中self.OUTPUT_LAYER_INDEX = -1 时,指定为输出层。
另外,对于偏置项前面说了,假设它是输出恒为1的神经元。所以继承一下Node
from my_node import Node
class ConstNode(Node):
def __init__(self,layer_index,node_index):
Node.__init__(self,layer_index,node_index)
self.output=1
#为了以防万一,覆盖一下父类方法,强制计算输出为1
def calc_output(self):
self.output=1
对于一个连接来说,它包含了如下信息:上游节点,下游节点,权重w,还有梯度也就是
d
E
d
w
\frac{dE}{dw}
dwdE
class Connection(object): def __init__(self, upstream_node, downstream_node): ''' 初始化连接,权重初始化为是一个很小的随机数 upstream_node: 连接的上游节点 downstream_node: 连接的下游节点 ''' self.upstream_node = upstream_node self.downstream_node = downstream_node self.weight = random.uniform(-0.1, 0.1) self.gradient = 0.0 def calc_gradient(self): #计算梯度,也就是dE/dw,根据公式0 self.gradient = self.downstream_node.dE_dx * self.upstream_node.output def get_gradient(self): ''' 获取当前的梯度 ''' return self.gradient def update_weight(self, rate): #根据梯度下降算法更新权重,w - η*dE/dw self.calc_gradient() self.weight -= rate * self.gradient
注意这里梯度优化公式采用的是我这里的公式0, 而且初始权重都是随机的(-0.1,0.1)之间,因为权重的初始化的问题,所以神经网络在初始训练的状态是不定的,只有经过足够的步数,最终收敛,才能得到想要的结果。
对于一层来说,负责创建和管理当层的所有神经元节点Node
class Layer(object): def __init__(self, layer_index, node_count): ''' 初始化一层 layer_index: 层编号 node_count: 层所包含的节点个数 ''' self.layer_index = layer_index self.nodes = [] for i in range(node_count): self.nodes.append(Node(layer_index, i)) self.nodes.append(ConstNode(layer_index, node_count)) def set_output(self, data): ''' 设置层的输出。当层是输入层时会用到。 ''' for i in range(len(data)): self.nodes[i].set_output(data[i]) def calc_output(self): ''' 计算层的输出向量 ''' for node in self.nodes[:-1]: node.calc_output()
提供层编号和当层节点数,可以创建所有节点,注意每层最后都会添加一个ConstNode用来实现偏置项
对于网络来说,管理整个神经网络的创建到最后的所有调度
class Network(object): def __init__(self, layers): ''' 初始化一个全连接神经网络 layers: 二维数组,描述神经网络每层节点数 ''' self.connections = [] self.layers = [] layer_count = len(layers) node_count = 0; for i in range(layer_count-1): self.layers.append(Layer(i, layers[i])) #注意最后一层层标指定为-1 self.layers.append(Layer(-1, layers[-1])) for layer in range(layer_count - 1): connections = [Connection(upstream_node, downstream_node) for upstream_node in self.layers[layer].nodes for downstream_node in self.layers[layer + 1].nodes[:-1]] for conn in connections: self.connections.append(conn) conn.downstream_node.append_upstream_connection(conn) conn.upstream_node.append_downstream_connection(conn)
初始化时,Network([10,8,6])传的参数[10,8,6],代表了创建3层网络,每层神经元个数分别是10,8,6. 这里注意强行指定了输出层编号为-1
然后是训练
def train(self, labels, data_set, rate, iteration): ''' 训练神经网络 labels: 数组,训练样本标签。每个元素是一个样本的标签。 data_set: 二维数组,训练样本特征。每个元素是一个样本的特征。 ''' for i in range(iteration): for d in range(len(data_set)): self.train_one_sample(labels[d], data_set[d], rate) def train_one_sample(self, label, sample, rate): ''' 内部函数,用一个样本训练网络 ''' self.predict(sample) self.calc_update(label) self.update_weight(rate)
训练一个样本的过程是,1)先执行正向传播
def predict(self, sample):
'''
根据输入的样本预测输出值
sample: 数组,样本的特征,也就是网络的输入向量
'''
self.layers[0].set_output(sample)
for i in range(1, len(self.layers)):
self.layers[i].calc_output()
return list(map(lambda node: node.output, self.layers[-1].nodes[:-1]))
将样本放入第一层的输出,然后对后面每层进行正向传播,每层得到最新的输出,顺便返回预测结果,也就是最后一层的输出
2)执行反向传播
def update_node(self, label):
'''
内部函数,计算并更新每个节点的缓存变量
'''
for layer in self.layers:
for node in layer.nodes:
node.calc_dy_dx(label)
对于每层的每个节点,更新其
d
E
d
y
i
\frac{dE}{dy_i}
dyidE 和
d
E
d
x
i
\frac{dE}{dx_i}
dxidE
3)最后更新连接权重
def update_weight(self, rate):
'''
内部函数,更新每个连接权重
'''
for layer in self.layers[:-1]:
for node in layer.nodes:
for conn in node.downstream:
conn.update_weight(rate)
瞎整些数据来玩玩
''' 模拟数据 胡子, 头发 ,胸部 ,声音粗犷度 -->判断性别 0女 1男 1 0.8 0.8 0.2 0 0 0.5 0.5 0.2 0 ''' data = [] #捏造一份数据出来 #先来500个男人 for i in range(0,5000): d = [] #男人基本有点胡子吧,让它的值在0.3以上 d.append(round(random.random()+0.3,2)) #头发貌似无所谓 d.append(round(random.random(),2)) #胸部,男人没啥胸部吧- -,让它不要超过0.2 d.append(round(random.random()*0.2,2)) #男人声音偏粗, d.append(round(random.random()+0.5,2)) #标注这个是男人 d.append(1) data.append(d) #再来500个女人 for i in range(0,5000): d = [] #女人有些有点微弱的胡子,让它的值不超过0.2 d.append(round(random.random()*0.2,2)) #头发貌似无所谓 d.append(round(random.random(),2)) #胸部,女人基本都有胸吧,最小不能低于0.3 d.append(round(random.random()+0.3,2)) #女人声音偏细,也有中音的,不超过0.3 d.append(round(random.random()*0.3,2)) #标注这个是女人 d.append(0) data.append(d) #随机打乱一下 random.shuffle(data) def parse_labels_and_features(datas): sample = [] label = [] for d in datas: label.append(d[-1]) sample.append(d[:-1]) return label,sample training_targets, training_examples = parse_labels_and_features(data[:8000]) test_targets, test_examples = parse_labels_and_features(data[8000:])
好了,来训练一下
ef get_result(vec): max_value_index = 0 max_value = 0 for i in range(len(vec)): if vec[i] > max_value: max_value = vec[i] max_value_index = i return max_value_index def evaluate(network, test_data_set, test_labels): error = 0 total = len(test_data_set) for i in range(total): label = test_labels[i] predict = get_result(network.predict(test_data_set[i])) if label != predict: error += 1 return float(error) / float(total) def train_and_evaluate(): last_error_ratio = 1.0 epoch = 0 network = Network([4, 5,3, 2]) while True: epoch += 1 network.train(training_targets, training_examples, 0.3, 1) if epoch % 5 == 0: print('%s epoch %d finished' % (datetime.now(), epoch)) error_ratio = evaluate(network, test_examples, test_targets) print('%s after epoch %d, error ratio is %f' % (datetime.now(), epoch, error_ratio)) if error_ratio > last_error_ratio: #break else: last_error_ratio = error_ratio
由于我们有4个特征和2种标签,所以输入层是4,输出层是2,中间构造了两层隐藏层
第一遍很逆天啊,一下搞定
也就是准确率0.83
老司机当然得再跑一遍
n久时间过去了…
算了算了,服了,好慢。
这点我确实第一次看到,对于神经网络模型的验证,作者采用了验证梯度的办法,原理是导数的定义
就是通过公式6计算一下每个连接的梯度应该是多少,然后现在模型训练完后的梯度是多少,最后看相差是不是在可以接受的范围内,这个知道一下就行了。
好了,到此为止,传统版本的神经网络就完成了。据后面介绍有个优化版本,这个下节再聊,因为我觉得这里已经够复杂了,我琢磨了好久呢。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。