本文章为《深度学习理论与实战(基础篇)》学习笔记。

神经网络

1 手写数字识别问题

我们在学习一门新的语言时会写一个“Hello World”程序,而MNIST数据的手写识别就是一个很好的学习机器学习(包括深度学习)的“Hello World”任务。
人类看到的图像和计算机“看到”的图像不同,计算机“看到”的图像是一个二维矩阵,是图片的灰度值。
MNIST数据集的每个图片经过缩放和居中等预处理之后,大小事28x28,每个点都是0~255之间的灰度值。

我们人类是怎么识别数字的呢?对于数字“9”来说,我们可能这样定义:9的上面有个圆圈,在这个圆圈的右下部有一个竖直的笔画。可如果我们把这个过程写成算法,有会遇到许许多多的问题,如:什么是圆圈? 右下方是哪?竖直的笔画怎么定义?

而对机器学习深度学习来说,则采用了不同的思路。为了更方便的分别机器学习和深度学习,我们定义两种对特征不同程度认识,第一种是“浅显的特征”,第二种是“本质的特征”。

机器学习的思路是,他不需要细节性地告诉计算机该怎么做,而是给计算机足够的样本,然后让他自己“学会”如何识别。我们通常认为,简单的机器学习线性模型学到的是“浅显的特征”。前面我们提到,MNIST的数据集都是经过缩放和居中的,如果数字在图片中出现了一点偏移,那么识别可能就不准确了。

我们需要一种真正认识到事务“本质的特征”的方法,深度学习应运而生,深度学习将原始的输入信号,通过多层的神经网络,就学习出最本质的特征,与此同时,由于深度学习使用了很多的中间层,深度学习的可解释性变得极差。

2 用代码实战多层神经网络

完整代码

import numpy as np

def Sigmoid(z):
    """Sigmoid函数"""
    return 1.0/(1.0+np.exp(-z))

def Sigmoid_prime(z):
    """Sigmoid的导数"""
    return Sigmoid(z)*(1-Sigmoid(z))

class Network(object):
    def __init__(self,sizes):
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y,1) for y in sizes[1:]]
        self.weights = [np.random.randn(y,x) for x, y in zip(sizes[:-1],sizes[1:])]


    def feedforward(self, a):
        """这个函数给定输入a(784维),计算出最终神经网络的输出(10维)。"""
        for b, w in zip(self.biases,self.weights):
            print(b, w)
            a = Sigmoid(np.dot(w, a)+b)
        return a

    def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
        if test_data:
            n_test = len(test_data)
            n = len(training_data)
            for j in np.xrange(epochs):     # 一共进行epochs = 30 轮迭代
                ## 训练数据随机打散
                np.random.shuffle(training_data)
                mini_batches = [training_data[k : k + mini_batch_size] for k in np.xrange(0, n, mini_batch_size)]
                                    # 这一步是将训练数据按照分为不同的batch,方便计算损失的梯度

                ## 对于每个batch
                for mini_batch in mini_batches:
                    ## 使用梯度下降更新参数
                    self.update_mini_batch(mini_batch, eta)
                if test_data:
                    ## 评估在测试数据集上的准确率
                    print("Epoch {0}:{1}/{2}".format(j, self.evaluate(test_data), n_test))
                else:
                    print("Epoch {0} complete".format(j))

    def evaluate(self, test_data):
        test_result = [(np.argmax(self.feedforward(x)),y)       # np.argmax的作用是返回最大值的下标
                       for (x, y) in test_data]
        return sum(int(x == y) for (x, y) in test_result)

    def update_mini_batch(self, mini_batch, eta):
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        for x, y in mini_batch:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]

    def backprop(self, x, y):
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # 前向传播计算
        activation = x
        activations = [x]       # 用来存储激活的列表
        zs = []                 # 用于存储z的列表,z是加权累加再加上b之后的结果,和输出a相比没有进行Sigmoid函数处理
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = Sigmoid(z)
            activations.append(activation)

        # 反向传播计算
        delta = self.cost_derivative(activations[-1], y)*Sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        for l in np.xrange(2, self.num_layers):
            z = zs[-1]
            sp = Sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-1] = delta
            nabla_w[-1] = np.dot(delta, activation[-l+1].transpose())
        return (nabla_b,nabla_w)

    def cost_derivative(self, activations, y):
        result = [(a-y) for a,y in zip(activations,y)]
        return result

mnist_loader函数

我们先来尝试一下怎么使用代码来训练模型,之后会详细解读代码。

training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
net = network.Network([784,30,10])		# 784 = 28*28
net.SGD(training_data, 30, 10, 3.0, test_data=test_data) 

mnist_loader函数:用来读取MNIST数据,数据十个gzip的压缩文件,是Pickle工具序列化到磁盘的格式。这个函数返回三个对象,分别是training_data,validation_data,test_data。

Network类构造函数:

class Network(object):
	def __init__(self,sizes):
		self.num_layers = len(sizes)
		self.sizes = sizes
		self.biases = [np.random.randn(y,1) for y in sizes[1:]]
		self.weights = [np.random.randn(y,x) for x, y in zip(sizes[:-1],sizes[1:])]

上面我们定义了一个神经网络类,该类的第一个属性num_layers表示有三层网络。每一层神经元的个数存储在sizes里。我们采用随机的方法构造初始得偏置值b,属性biases随机生成了一个30x1的二维矩阵,其实我们生成一个30维的一维矩阵就可以了,这里生成30x1的二维矩阵是为了和weights矩阵的二维矩阵一致,方便代码的编写和计算。weigths也是一样的初始化方法。

feedforward函数

这个函数给定输入aaa(784维),计算出最终神经网络的输出(10维)。

def feedforward(self, a):
    """这个函数给定输入a(784维),计算出最终神经网络的输出(10维)。"""
    for b, w in zip(self.biases,self.weights):
        a = Sigmoid(np.dot(w, a)+b)
    return a   

"""
此处我们假设biases中30x1的矩阵叫做b1,10x1的矩阵叫做b2
假设 weights中30x784的矩阵叫做w1,10x30的矩阵叫做w2
那么zip函数的作用可以看做
zip(biases, weights) => [(b1, w1), (b2, w2)]
"""

这里用到了np.dot函数,这个函数是进行向量乘法的,我们的biases中有一个30x1的矩阵和一个10x1的矩阵,而weights中存放了一个30x784的矩阵和一个10x30的矩阵,zip函数作用是将biases中30x1的矩阵和weights中的30x784的矩阵组合成一个元组,然后将元组返回给b和w,之后b、w、a进行矩阵乘法计算,最后返回给a。
zip函数的用法可以参考文末的函数学习模块。

这里我们还用到了Sigmoid函数,其输入是Numpy的ndarray,输出是同样大小的数组,它对每个元素都进行了Sigmoid的导数计算。

#### Miscellaneous functions
def Sigmoid(z):
    """Sigmoid函数"""
    return 1.0/(1.0+np.exp(-z))

def Sigmoid_prime(z):
    """Sigmoid的导数"""
    return Sigmoid(z)*(1-Sigmoid(z))

SGD函数

这个函数时训练的入口,比如:

net.SGD(training_data, 30, 10, 3.0, test_data=testdata)

该函数的定义如下:

def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
	"""
    :param training_data:训练数据
    :param epochs: 训练的迭代次数,按照上面的调用,我们这里设置的是三十次调用
    :param mini_batch_size: 每一个mini_batch的大小
    :param eta: 学习率,学习率太大,损失函数可能不收敛;学习率太小,损失函数收敛速度过慢
    :param test_data:如果有测试数据,那么每一次迭代过后进行一次验证
    :return:
    """
    if test_data:
        n_test = len(test_data)
        n = len(training_data)
        for j in np.xrange(epochs):     # 一共进行epochs = 30 轮迭代
            ## 训练数据随机打散
            np.random.shuffle(training_data)
            mini_batches = [training_data[k : k + mini_batch_size] for k in np.xrange(0, n, mini_batch_size)]
                                # 这一步是将训练数据按照分为不同的batch,方便计算损失的梯度
            ## 对于每个batch
            for mini_batch in mini_batches:
                ## 使用梯度下降更新参数
                self.update_mini_batch(mini_batch, eta)
            if test_data:
                ## 评估在测试数据集上的准确率
                print("Epoch {0}:{1}/{2}".format(j, self.evaluate(test_data), n_test))
            else:
                print("Epoch {0} complete".format(j))

验证函数

def evaluate(self, test_data):
    test_result = [(np.argmax(self.feedforward(x)),y) for (x, y) in test_data]
    # np.argmax的作用是返回最大值的下标
    return sum(int(x == y) for (x, y) in test_result)

根据batch更新w和b

    def update_mini_batch(self, mini_batch, eta):
    	# 创建两个和w、b相同大小的数组,全部赋值为0
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        
        for x, y in mini_batch:
        	# 使用数据x和标签y求得要更新的变化量
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            # 根据变化量,使用学习率:eta/len(mini_batch),对原来的权重w和偏置b进行更新
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]

反向传播更新权重

    def backprop(self, x, y):
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # 前向传播计算
        activation = x
        activations = [x]       # 用来存储激活的列表
        zs = []                 # 用于存储z的列表,z是加权累加再加上b之后的结果,和输出a相比没有进行Sigmoid函数处理
        # 前向传播计算,使用现在的w和b计算出累加求和的输出值z
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = Sigmoid(z)
            activations.append(activation)

        # 反向传播计算
        # delta计算出的是输出层的“错误”
        delta = self.cost_derivative(activations[-1], y)*Sigmoid_prime(zs[-1])		# 根据公式一,cost_derivate是损失函数对输出a的偏导
        # 这里就是最后一个的参数要更新的值
        nabla_b[-1] = delta				# 根据公式三,损失函数对b的偏导就是本层的“错误”
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())	# 根据公式4,activation里就是ain,delta就是“错误”out
		
		# 根据公式2计算之前的每一层的“错误”
        for l in np.xrange(2, self.num_layers):
            z = zs[-1]
            sp = Sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-1] = delta
            nabla_w[-1] = np.dot(delta, activation[-l+1].transpose())
        return (nabla_b,nabla_w)

cost_derivative函数

这个就是损失函数对输出结果a的偏导,我们使用的是MSE(最小平方误差)损失函数,它对于a求偏导的结果就是a-y,

    def cost_derivative(self, activations, y):
        result = [(a-y) for a,y in zip(activations,y)]
        return result

优化技巧

参数初始化

激活函数

最早流行的激活函数是Sigmoid,现在使用的比较多的是ReLU函数
ReLU(x)=max(0,x)ReLU(x)=max(0,x)ReLU(x)=max(0,x)

Dropout

随机的舍去一些中间神经元。
好处:
打破了局部特征
防止过拟合

Batch Normalization

我们假设训练到此时,第L-1层的参数是最完美的参数,但是L层的参数不太好,那么下一次进行参数更新时,可能将L层的参数调整好了,但是L-1层的参数调整的不会有以前好,这时模型整体的质量相对训练前反而下降了,我们使用Batch Normalization优化这种情况。

冲量

学习率自适应

函数学习

zip()函数

zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。

如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同,利用 * 号操作符,可以将元组解压为列表。

zip 方法在 Python 2 和 Python 3 中的不同:在 Python 3.x 中为了减少内存,zip() 返回的是一个对象。如需展示列表,需手动 list() 转换。

>>> a = [1,2,3]
>>> b = [4,5,6]
>>> c = [4,5,6,7,8]
>>> zipped = zip(a,b)     # 返回一个对象
>>> zipped
<zip object at 0x103abc288>
>>> list(zipped)  # list() 转换为列表
[(1, 4), (2, 5), (3, 6)]
>>> list(zip(a,c))              # 元素个数与最短的列表一致
[(1, 4), (2, 5), (3, 6)]

>>> a1, a2 = zip(*zip(a,b))          # 与 zip 相反,zip(*) 可理解为解压,返回二维矩阵式
>>> list(a1)
[1, 2, 3]
>>> list(a2)
[4, 5, 6]
>>>
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐