当前位置:   article > 正文

人工智能小白日记 DL算法学习之3 神经网络和反向传播算法

dl算法

前言

前面已经掌握了感知器和线性单元,接下来是另一种感知器:神经元。如果把每个神经元组合起来就可以构成一个神经网络。

正文内容

1 神经元

神经元和感知器本质上是一样的,只不过我们说感知器的时候,它的激活函数是阶跃函数;而当我们说神经元时,激活函数往往选择为sigmoid函数或tanh函数。如下图所示:
在这里插入图片描述

2 sigmoid函数

在这里插入图片描述
图形如下:
在这里插入图片描述
可以看出其值的范围是0到1之间,而tanh函数则在-1到1之间。其导函数推导过程如下:
在这里插入图片描述
其中第一步用链式求导法即可,令t = 1+ e-z, 则 df/dz = df/dt * dt/dz = -1/(t)2 * (-e-z)

所以
在这里插入图片描述

3 神经网络与反向传播算法

接下来的内容跟作者的可能有点不一样,如果说反向传播,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(yjlabel)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} ndxjdEdyidxj
= ∑ 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 ######################

4 python3实现全联接神经网络

好了,结合作者的思路,写一个全联接的神经网络。当然,用的是咱自己的推导公式。
首先观察模型的组成,为了解耦降低复杂度,大致将其分为网络network, 层layer,神经元节点node,连接connection。
在这里插入图片描述

4-1 神经元node

在这里插入图片描述
可以看到一个神经元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 = []
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

当然还要有更新数据的能力

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) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

可以看到,这里我采用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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

4-2 连接connection

在这里插入图片描述
对于一个连接来说,它包含了如下信息:上游节点,下游节点,权重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
        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

注意这里梯度优化公式采用的是我这里的公式0, 而且初始权重都是随机的(-0.1,0.1)之间,因为权重的初始化的问题,所以神经网络在初始训练的状态是不定的,只有经过足够的步数,最终收敛,才能得到想要的结果。

4-3 层layer

对于一层来说,负责创建和管理当层的所有神经元节点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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

提供层编号和当层节点数,可以创建所有节点,注意每层最后都会添加一个ConstNode用来实现偏置项

4-4 网络network

对于网络来说,管理整个神经网络的创建到最后的所有调度

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

初始化时,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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

训练一个样本的过程是,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]))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

将样本放入第一层的输出,然后对后面每层进行正向传播,每层得到最新的输出,顺便返回预测结果,也就是最后一层的输出

2)执行反向传播

 def update_node(self, label):
        '''
        内部函数,计算并更新每个节点的缓存变量
        '''
        for layer in self.layers:
            for node in layer.nodes:
                node.calc_dy_dx(label)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

对于每层的每个节点,更新其 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4-5 测试

瞎整些数据来玩玩


'''
模拟数据
胡子, 头发   ,胸部 ,声音粗犷度 -->判断性别 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:])

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

好了,来训练一下

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

由于我们有4个特征和2种标签,所以输入层是4,输出层是2,中间构造了两层隐藏层

第一遍很逆天啊,一下搞定
在这里插入图片描述
也就是准确率0.83
老司机当然得再跑一遍
在这里插入图片描述
n久时间过去了…
算了算了,服了,好慢。

5 模型验证

这点我确实第一次看到,对于神经网络模型的验证,作者采用了验证梯度的办法,原理是导数的定义
在这里插入图片描述
就是通过公式6计算一下每个连接的梯度应该是多少,然后现在模型训练完后的梯度是多少,最后看相差是不是在可以接受的范围内,这个知道一下就行了。

小结

好了,到此为止,传统版本的神经网络就完成了。据后面介绍有个优化版本,这个下节再聊,因为我觉得这里已经够复杂了,我琢磨了好久呢。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/144873?site
推荐阅读
相关标签
  

闽ICP备14008679号