《深度学习入门》第3章 - 神经网络
一、从感知机到神经网络
1、神经网络结构:用图来表示的话,把最左一列称为输入层,最右一列称为输出层,中间一列称为中间层(也称隐藏层),隐藏的意思是,隐藏层的神经元肉眼不可见。
2、在神经网络中,激活函数的作用是能够给神经网络加入一些非线性因素,使得神经网络可以更好地解决较为复杂的问题。如在数据线性不可分的情况下,尝试引入非线性的因素,对样本进行分类。
二、激活函数
1、什么是激活函数:在神经元中,输入的 inputs 通过加权,求和后,还被作用了一个函数,这个函数就是激活函数 Activation Function
2、sigmod函数:
Sigmoid函数计算公式:
曲线:
sigmoid函数也叫 Logistic 函数,用于隐层神经元输出,取值范围为(0,1),它可以将一个实数映射到(0,1)的区间,可以用来做二分类。在特征相差比较复杂或是相差不是特别大时效果比较好。
sigmoid缺点:
激活函数计算量大,反向传播求误差梯度时,求导涉及除法
反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练
Sigmoids函数饱和且kill掉梯度。
Sigmoids函数收敛缓慢。
3、RELU函数
Rectified Linear Unit(ReLU) - 用于隐层神经元输出
公式:
曲线:
RELU特点:
输入信号 <0 时,输出都是0,>0 的情况下,输出等于输入
ReLU 的优点:
Krizhevsky et al. 发现使用 ReLU 得到的 SGD 的收敛速度会比 sigmoid/tanh 快很多
ReLU 的缺点:
训练的时候很”脆弱”,很容易就”die”了
例如,一个非常大的梯度流过一个 ReLU 神经元,更新过参数之后,这个神经元再也不会对任何数据有激活现象了,那么这个神经元的梯度就永远都会是 0.
如果 learning rate 很大,那么很有可能网络中的 40% 的神经元都”dead”了。
4、Softmax函数
Softmax - 用于多分类神经网络输出
公式:
Sigmod和Softmax比较:
sigmoid将一个real value映射到(0,1)的区间,用来做二分类。
而 softmax 把一个 k 维的real value向量(a1,a2,a3,a4….)映射成一个(b1,b2,b3,b4….)其中 bi 是一个 0~1 的常数,输出神经元之和为 1.0,所以相当于概率值,然后可以根据 bi 的概率大小来进行多分类的任务。
二分类问题时 sigmoid 和 softmax 是一样的,求的都是 cross entropy loss,而 softmax 可以用于多分类问题。
三、3层神经网络的实现
import numpy as np
def sigmoid(X, deriv = False):
if(deriv == True):
return X*(1-X)
return 1/(1+ np.exp(-X))
#构造数据集
x = np.array([[0,0,1],
[0,1,1],
[1,0,1],
[1,1,1],
[0,0,1]])
print(x.shape)
y = np.array([[0],[1],[1],[0],[0]])
print(y.shape)
#指定随机的种子,每次运行产生相同数据
np.random.seed(1)
#L0有三个特征,L1有4个神经元,所以w0为3行4列,取值范围(-1,1)
w0 = 2*np.random.random((3,4))-1
w1 = 2*np.random.random((4,1))-1
print(w0)
#神经网络模型构造及训练
for j in range(1000001):
#L0层
l0 = x
#前向传播,计算后l1为5行4列
l1 = sigmoid(np.dot(l0,w0))
#前向传播,计算后l2为5行1列
l2 = sigmoid(np.dot(l1,w1))
#计算预测值与标签的差异值
l2_error = y - l2
if(j %100000)==0:
print('Error'+str(np.mean(np.abs(l2_error))))
print(j)
#方向传播,计算梯度值
#如l2_error很大,则需要大力度更新;如果l2_error很小,则只需要更新一点点
#所以导数乘以l2_error, *为连个5行一列的矩阵对应位置相乘
l2_delta = l2_error * sigmoid(l2, deriv=True)
#l2_delta5行1列,w14行1列,
l1_error = l2_delta.dot(w1.T)
l1_delta = l1_error * sigmoid(l1, deriv=True)
#更新参数
#l15行4列,l2_delta5行1列,w1为4行一列
w1 += l1.T.dot(l2_delta)
#l05行3列,l2_delta5行4列,w0为3行4列
w0 += l0.T.dot(l1_delta)
四、输出层的设计
softMax函数设计:
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
# optimized
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 溢出对策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
softmax()的结果在0~1之间。所以把softmax()的输出称为概率。
softmax()是单调递增的:
也就是说如果输入数据本来就是最大的,经过softmax()计算之后还是最大。
如果输入数据本来排行老二,经过softmax()计算之后还是排行老二。
所以神经网络在'推理'的过程可以省略softmax()。
'推理'是指用学到的模型对未知的数据进行分类,也称为神经网络的前向传播(forward propagation)。
五、手写数字的识别
首先,将一个手写数字图像分成一个m×n个部分。例如将图像分成28×28个区域,则输出层包含了784(28×28)个输入神经元,其神经网络的架构如图。
对于输入神经元,当黑色部分占这个区域超过50%时,则这个神经元的值为1,否则为0。
对于输出神经元,一共有10个。当第一个神经元被激活,它的输出为1时,即识别这个数字为0。
神经网络结构代码如下:
class Network(object):
def __init__(self, sizes):
# 获取神经网络的层数
self.num_layers = len(sizes)
# sizes即每层神经元的个数
self.sizes = sizes
# 赋随机值(服从高斯分布),对权重和偏向进行初始化
# bais从第2行开始
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
# zip从传入的可循环的两组量中取出对应数据组成一个tuple
self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]
# print self.weights
# print self.biases
# 根据当前输入利用sigmoid函数来计算输出
def feedforward(self, a):
for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a) + b)
return a
# epochs训练多少轮, mini_batch_size抽取多少实例,eta学习率
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)
# j代表第几轮
for j in xrange(epochs):
# 将training_data中的数据随机打乱
random.shuffle(training_data)
# mini_batchs每次抽取mini_batch_size大小的数据作为一小块
mini_batches = [training_data[k:k + mini_batch_size]
# 从0到n每次间隔mini_batch_size张图片
for k in xrange(0, n, mini_batch_size)]
# 对取出来的mini_batchs逐个进行更新,更新weights和biases
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)
# eta:学习率 传入单个的mini_batch,根据其x.y值,对整个神经网络的wights和biases进行更新
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)]
# 权重weights更新 W'k=Wk-(eta/m)&C/&Wk
self.weights = [w - (eta / len(mini_batch)) * nw
for w, nw in zip(self.weights, nabla_w)]
# 偏向biases更新 b'k=bk-(ets/m)&C/&bk
self.biases = [b - (eta / len(mini_batch)) * nb
for b, nb in zip(self.biases, nabla_b)]
# 计算对应的偏导数
def backprop(self, x, y):
# 返回一个元组(nabla_b,nabla_w)代表成本函数C_x的渐变。
# nabla_b和nabla_w是numpy数组np.array的逐层列表,类似于self.biases和self.weights.
# 分别生成与biases weights等大小的0矩阵
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]
# 列表存储所有中间向量z,逐层
zs = []
for b, w in zip(self.biases, self.weights):
# 计算中间变量 Z=W*X+b
z = np.dot(w, activation) + b
# 列表存储所有中间向量z
zs.append(z)
# 激活activation=sigmoid(W*X+b)
activation = sigmoid(z)
# 列表存储所有的激活
activations.append(activation)
# 反向更新
# ### 输出层
# 计算输出层error=Oj(1-Oj)(Tj-Oj);
# cost_derivative(activations[-1], y)即C对a的梯度:(Tj-Oj)
# sigmoid_prime(zs[-1])即:Oj(1-Oj)
delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
# 更新输出层的nabla_b,nabla_w
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# ### 隐藏层
# l = 1表示神经元的最后一层,l = 2是第二层,依此类推.反向更新直到初始层
for l in xrange(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
# weights[-l + 1]即下一层的权重,
delta = np.dot(self.weights[-l + 1].transpose(), delta) * sp
# 输出C对w,b的偏导
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
return (nabla_b, nabla_w)
def evaluate(self, test_data):
# 返回神经网络输出正确结果的测试输入的数量。 注意,神经网络的输出被假定为最终层中具有最高激活的神经元的指数。
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):
# 返回输出激活的偏导数 partial C_x,partial a的向量。
return (output_activations - y)
# sigmoid函数
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
# sigmoid函数的导数
def sigmoid_prime(z):
return sigmoid(z) * (1 - sigmoid(z))
# 向量化
def vectorized_result(j):
e = np.zeros((10, 1))
e[j] = 1.0
return e
# 数据转换,将数据转换为长度为784的numpy.ndarray组成的list,将对应label向量化
def load_data_wrapper():
tr_d, va_d, te_d = load_data()
# # 数据转换
# tr_d是由50000个长度为784的numpy.ndarray组成的tuple
# 转换后的training_inputs是由50000个长度为784的numpy.ndarray组成的list
training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
training_results = [vectorized_result(y) for y in tr_d[1]]
# 训练集 training_data
# zip()返回一个列表的元组,其中每个元组包含从每个参数序列的第i个元素。
training_data = zip(training_inputs, training_results)
validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
# 验证集 validation_data
validation_data = zip(validation_inputs, va_d[1])
test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]]
# 测试集 test_data
test_data = zip(test_inputs, te_d[1])
return (training_data, validation_data, test_data)
if __name__ == '__main__':
training_data, valivation_data, test_data =load_data_wrapper()
# # 显示图像
# ShowImage()
net = Network([784, 30, 10])
# 训练集training_data,训练10轮,每次取样10个作为mini_batch,学习率为3
net.SGD(training_data, 10, 10, 3.0, test_data=test_data)
六、小结
1、神经网络中的激活函数使用平滑变化的sigmod函数或ReLU函数。
2、通过巧妙地使用Numpy数组,可以高效地实现神经网络。
3、机器学习的问题大体上可以分为回归问题和分类问题。
4、关于输出层的激活函数,回归问题中一般使用恒等函数,分类问题中一般使用softmax函数。
5、分类问题中,输出层的神经元的数量设置为要分类的类别数。
6、输入数据的集合称为批,通过一批为单位进行推理处理,能够实现高速的运算。