第一部分
个人信息:
本人今年本科计算机专业毕业,将于9月继续攻读计算机类的硕士。借此时间段和阿里云平台来入门深度学习中的图像分类。我是通过同学介绍来了解“飞天加速计划·高校学生在家实践”活到,之前有过几次的服务器租用,主要为了熟悉Linux操作系统的使用和本科课程相关实验的需要。现在借此博客机会来分享自己在阿里云平台的使用感受。
第二部分
使用内容
大多数人可以轻易识别下面这个手写序列。但是这个轻易是假象。
在我们大脑的每个半球,人类都有一个初级视觉皮层,也被称为$V1$,包含1.4亿个神经元,它们之间有几百亿个连接。然而,人类的视觉不仅涉及$V1$,还涉及整个系列的视觉皮层--$V2、V3、V4$和$V5$--正在进行逐渐复杂的图像处理。我们的大脑经过数亿
年的进化,适应了对视觉世界的理解。但是这个简单的识别过程是在无意识中完成的,why?
对于普通的编程方式而言,模式识别会很难办,且看起来毫无希望。
神经网络以一种截然不同的方式来处理这个问题,即把大量的手写数字,称为训练样本,然后开发一个能够从这些训练实例中学习的系统。换句话说,神经网络使用这些样本来自动推断识别手写数字的规则。(我们人类的直觉是,例如数字‘9’,上面有一个环,下面有一个竖线。)
我们将编写一个计算机程序,实现一个学习识别手写数字的神经网络(代码仅有74行)。这个简短的程序可以识别数字,准确率超过96%,而且无需人工干预。后面我们会做的更好!事实上,最好的商业神经网络现在已经非常好,以至于银行用它来处理支票,邮局用它来识别地址。
真正的目的是学习一些关于神经网络的关键思想,包括两个重要的神经元-感知器和激活神经元(the perceptron and the sigmoid neuron)和标准的神经网络学习算法-随机梯度下降(stochastic gradient descent)。
感知器(perceptron)
是一种人工神经元,是科学家Frank Rosenblatt在1950s-1960s开发,现代工作中更常见的神经元是一个叫做sigmoid神经元的模型。
感知器如何工作? 一个感知器需要一些二进制输入,并且产生一个二进制输出:
Rosenblatt提出了一个简单的规则来计算输出。他引入了权重来对应神经元从输入到输出的重要性。神经元的输出取决于输入与权重是否到达一定的阈值。这里权重和阈值都是参数,为了简便,我们将阈值放到等号另外一边,同时令阈值的相反数为感知器的偏置-衡量使感知器输出1(激活-fire)的数值。
为了方便,通常将输入的变量作为输入层来看,这是一个没有输入,只有输出的特殊神经元(简单定义为输出所需值x1的特殊单元)
感知器可以模拟NAND门电路,加法器等等...
感知器的计算普遍性有利有弊,利: 说明感知器想其他计算设备一样强大,弊:这些仅仅说明感知器是新型NAND门的一种?并不是什么大事!
事实并非如此,我们可以设计学习算法来自动调整人工网络的权重和偏执,这种自动来自外部的刺激,而非人为干预。这种方式可以解决对传统电路而言,很难解决的问题。
激活函数(Sigmoid neurons)
为了了解学习是如何进行的,假设我们对网络中的一些权重(或偏置)做了一个小的改变。我们希望这个权重的微小变化只导致网络输出的微小相应变化。这种特性使得学习成为可能。
why?
例如,假设该网络错误地将一张图片分类为 "8",而它应该是 "9"。我们可以想办法对权重和偏置做一个小小的改变,使网络更亲近于将图像分类为 "9"。然后我们会重复这个过程,一次又一次地改变权重和偏置,以产生越来越好的输出。该网络将进行学习。
这种特性对感知器而言,很难做到。因为对权重和偏置的微小改变,可能会使感知器输出完全相反的结果。
对此,我们引入与感知器差不多的神经元-激活神经元(sigmoid neurons),尤其在权重和偏置取向两极的情况下极为相似。
该特性使得在输入和输出上有存在与感知器一定的区别。即输入/输出不是二进制数值,而是[0,1]之间任意实数。该函数的光滑性使得权重和偏置的微小改变影响输出产生微小变化。
神经网络的层次结构
最左边是含有输入神经元的输入层,最右边是含有输出神经元的输出层,中间叫做隐藏层(仅仅是因为它既不是输入层,也不是输出层)。这里对于输入层和输出层的神经元数量都是直截了当的,但是隐藏层的设计需要一些经验(启发式方法)。
一个简单的手写数字分类器
我们可以将识别手写体问题分解成两个子问题:
- 将一张含有多个数字序列的图像分解成含有单个数字图像的方法
- 识别图像中的单个数字
第二个问题的解决可以间接解决第一个问题,因为如果能够解决好第二个问题,那么说明此时的分割图像也是解决好的。所以我们将注意力集中到第二个问题上来,即识别单个数字。
我们使用三层神经网络解决该问题:
注意:
- 输入是像素的灰度值,范围在0-1之间,其中0.0代表纯白色,1.0代表纯黑色。
- 中间的隐藏层根据实验的结果来确定其神经元数量。
- 输出层有10个神经单元,分别对应0~9,若第一个神经元被激活,则表示网络认为图像是0,等等。更准确的说,我们取输出层最高的值对应的神经元,如果是第6个神经元,则网络认为输入的图像是数字6。
思考:
第四层将原先的输出层转化成二进制编码格式:
学习梯度下降
MNIST dataset分为从一批250个人收集的图像(60,000)组成训练集和另外一批250个人的图像(10,000)组成测试集。每个样本形如(x, y=y(x)),其中x为28x28=784维,每一维代表图像的像素值(缩放为[0,1]),y为10维,例如$[0,0,0,0,0,0,1,0,0,0]^T$(代表该输入图像的标签为6)
我们需要一个帮助我们找到合适的权重和偏置,以至让所有样本的网络输出与真实标签接近的学习算法,为了量化我们实现这个目标的距离,我们定义了代价函数,这里是平方误差代价函数。从而将问题转化成使用一个方法自动找到合适的权重和偏置使得代价函数尽可能的小,这个方法叫梯度下降(gradient descent)。
为什么使用平方误差作为代价函数,而不是简单的神经网络正确分类的数量?
因为后者不是关于权重和偏置的平滑函数,也就是说,大多数情况下,对权重和偏置的微小改变不会导致正确分类的训练图像数量有任何变化。而平方误差代价函数可以轻易看见在权重和偏置发生微小变化后导致代价函数上的改善。
为了集中精力,我们将问题先简化成一个关于多变量的函数,并且最小化函数,然后再回到神经网络。例如函数$C(v_1, v_2)$是关于两个自变量的函数:
也许我们可以使用微积分来找到函数极值的解析解,但是对于成千上万个自变量而言,是一个噩梦!
我们可以将此问题变成球从山的斜坡滚到低处的情景,此时我们需要求导这个数学方法来辅助,因为导数可以告诉我们当地山谷的局部地理情况。该如何移动使得球总是往低处走?
为了数学表示简便,我们定义函数的梯度-由所有偏导数组成的向量,自变量的变化量向量-由所有自变量的变化量组成的向量。所以函数的变化量表示为梯度向量与自变量变化量向量的点积。此处也间接表示了为什么偏导数构成的向量叫梯度,因为它描述了自变量变化量与函数变化量的关系
这里重点在于如何选择当前位置自变量的变化量方向($-\eta\bigtriangledown C$),使得函数变化量保持负号且最小!这样我们重复这样做,直到达到全局最小值。
总结起来,就是梯度下降法不断计算相对于当前位置的梯度向量(由偏导数组成),然后朝着反方向,可以尽可能快的到达谷底。形如:
注意:
- 学习率$lr$不能太大,否则不满足可微的条件,从而导致代价函数变大
- 学习率$lr$不能太小,否则会很慢。
- 梯度下降方法耗时
值得注意的是,平方误差代价函数可以表示为所有样本代价的均值。也就是说,为了得到自变量当前位置的梯度,需要单独计算所有样本的梯度,这对样本量很大的网络而言,也是噩梦!为此,我们使用随机梯度下降(stochastic gradient descent),其思想在于使用随机小批量样本m来计算的偏导数平均值代替所有样本n计算偏导数平均值。速度增加了n/m倍!
若总样本为n,每次的小批量样本为m,则每次选择样本中不同的m个样本来更新参数,当整个训练集参与一遍后,成为一个epoch!
实现我们的神经网络来分类手写数字
$$sophisticated algorithm \le simple learning algorithm + good training data.$$
走向深度学习
尽管我们的神经网络给我们印象深刻的性能,但是这个性能显着神秘,难以琢磨。 我们能否找到一些方法来理解我们的网络在对手写数字进行分类时所依据的原则?而且,鉴于这样的原则,我们能否做得更好?
神经元作为一种衡量权重证据的手段,假设我们想确定一张图像是否是一张人脸,我们可以使用神经网络,将图像像素作为输入,使用一个输出神经元来判断是否是一张人脸。
我们可以手动选择合适的权重和偏置,但是如何做呢?
一个合理的方式是将这个问题变成多个子问题,
- 图像左上角有眼睛?
- 图像右上角有眼睛?
- 图像中间有鼻子?
- 图像下面有嘴巴?
- ....
如何上面大部份问题神经网络给出的回答都是‘Yes',则可以认为该图像是一张人脸,反之则不是人脸。
不过这只是一个大概的方法,还有许多特殊的情况,例如,图像只有部分脸,侧面脸,带了帽子...。不过,请注意这是为了帮助我们建立关于网络功能的直觉。 矩形是解决子问题对应的网络
同时子问题还可以进一步的分解,例如:
最终具体到图像的某个像素,这样的问题可以由连接到图像中原始像素的单个神经元来回答。
最终的结果是一个网络,它将一个非常复杂的问题--这幅图像是否显示了一张脸--分解为非常简单的问题,可以在单个像素的层面上进行回答。它通过一系列的多层来实现这一目标,前期的层回答关于输入图像的非常简单和具体的问题,而后来的层则建立了一个更加复杂和抽象的概念的层次结构。具有这种多层结构的网络--两个或多个隐藏层--被称为深度神经网络(deep neural networks)。
将深层网络与浅层网络相比较,有点像将有函数调用能力的编程语言与没有这种调用能力的精简语言相比较。在神经网络中,抽象的形式与在传统编程中不同,但它同样重要。
代码:
'''
MNIST
50,000 训练集
10,000 测试集
10,000 验证集
'''
import random
import numpy as np
class Network(object):
# 列表sizes 包含了各层的神经元数量。
def __init__(self, sizes):
self.num_layers = len(sizes)
self.sizes = sizes
# np.random.randn函数用于生成均值为0、标准差为1的高斯分布
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:])]
# print(self.weights[0], self.weights[1], sep='\n')
# print(self.weights, self.biases, sep='\n')
def feedforward(self, a):
# a是输入,返回每层相应的输出(输入层除外)
for b, w in zip(self.biases, self.weights):
# print('b:\n', b, '\nw\n', w)
a = sigmoid(np.dot(w, a)+b)
# print('a:\n', a)
return a
def SGD(self, traing_data, epochs, mini_batch_size, eta, test_data=None):
'''
:param traing_data: 元组的列表,(x,y),x为输入,y为对应的标签
:param epochs: 希望训练的周期数
:param mini_batch_size: 每次梯度更新所需的样本数
:param eta: 学习率
:param test_data: 测试集
:return:
'''
if test_data:
n_test = len(test_data)
n = len(traing_data)
''' 在python2中
xrange() 函数用法与 range 完全相同,
所不同的是生成的不是一个数组,而是一个生成器。
在python3中
xrange被range替代,同时range类型为range,
是一个生成器,而非list。
'''
for j in range(epochs):
random.shuffle(traing_data)
mini_batches = [
traing_data[k: k+mini_batch_size]
for k in range(0, n, mini_batch_size)
]
# 对每个mini_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 update_mini_batch(self, mini_batch, eta):
'''
通过对单个小批次应用反向传播法的梯度下降,更新网络的权重和偏置。
:param mini_batch: 一个小批样本,是一个元组列表
:param eta: 学习率
:return:
'''
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 = []
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 range(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l + 1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
return (nabla_b, nabla_w)
def evaluate(self, test_data):
'''
:param test_data:
:return: 测试集中正确分类的数量,
其中分类结果为最后一层神经元中最高激活值的位置
'''
test_results = [(np.argmax(self.feedforward(x)), y) for (x, y) in test_data]
return sum(int(x==y) for (x,y) in test_results)
def cost_derivative(self, output_activations, y):
return (output_activations-y)
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
def sigmoid_prime(z):
# sigmoid 的导数
return sigmoid(z)*(1-sigmoid(z))
# net = Network([2, 3, 1])
# print(net.feedforward([[1], [2]]))
实验截图:
第三部分:
使用感受和未来展望
首先很感谢阿里云平台对高校学生给予如此实惠的活动,我使用的是Ubuntu操作系统,该平台较为稳定。同时对安装配置相对其他系统也是非常的方便,快捷。自己即将开学,老师布置的任务,自己也是时而有头绪,时而摆烂,希望自己摆正心态,做好自己的人生道路!