简易神经网络_推理和正向传播
化其万物而不知其禅之者,焉知其所终?焉知其所始?正而待之而已耳。
作为《深度学习入门》的阅读笔记,本文简略概述正向传播和激活函数的内容,并辅以python实现
需要事先了解的
本文并不是从零开始的,需要实现了解
蜻蜓点水python
- 蜻蜓点水python_dlc
- 简略感知机
学习和推理
和求解机器学习问题的步骤(分成学习和推理两个阶段进行)一样,使用神经网络解决问题时,也需要首先使用训练数据(学习数据)进行权重参数的学习;进行推理时,使用刚才学习到的参数,对输入数据进行分类。
首先,通过机器学习解决问题通常分为两个阶段:学习和推理;
- 对于感知机或者神经网络,学习的过程就是调整参数的过程,是需要事先执行的步骤;
- 在完成学习后,使用确定的参数解决实际问题的过程,称为推理;在神经网络或感知机中,通过前向传播来实现;
从感知机到神经网络
回顾下异或问题,在感知机的实现中,输入先通过与与非门和或门的处理,再使用其输出经过与门的处理就完成了异或的操作:

这种多层感知机就是一种二层神经网络,层数的算法和感知机的层数算法一致,只是表示法不同:

先解释结构,x1 x2的位置称为输入层,y所在位置称为输出层,两层之间的不管有多少层都是中间层或者隐藏层,
不同点在于门的运算过程都跑到边上去了,回顾感知机的实现,所谓门的实现就是不同权重的调整,所以边上既包含了参数也包含了固定的传播算法,节点或者称神经元上包含了输出输入的结果,但也包含了一些其他处理过程,如激活函数
一般而言,“朴素感知机”是指单层网络,指的是激活函数使用了阶跃函数(后述)的模型。“多层感知机”是指神经网络,即使用sigmoid函数(后述)等平滑的激活函数的多层网络。
传播算法和激活函数
首先,回顾下感知机中的权重和偏置和感知机的实现:

简单来说感知机的实现就是该公式的实现,其中b是被称为偏置的参数,用于控制神经元被激活的容易程度;而w1和w2是表示各个信号的权重的参数,用于控制各个信号的重要性。 而b+w1x1+w2x2就是层和层之间的传播算法,从下图可以看出,偏置也可作为神经元为1而权重为b进行传播:

解决了传播问题,再看一眼公式可以发现0和1的判断是依赖于b+w1x1+w2x2的输出的,而这0和1的判断规则也不一定是以公式中的方式出现的,是可以记作函数h(x)来表示的,这个函数就是激活函数,激活函数的输出才是神经元最后表示的输出。
简而言之:激活函数,是用来决定何时激活信号的函数

阶跃函数
就从感知机使用的激活函数来看:

该函数称为阶跃函数;实现和图像如下,注意到这并不是一个平滑的函数
def step_function(x):
y = x > 0
return y.astype(int)
x = np.array([-1.0,1.0,2.0])
print(x)
print(x>0)
y = step_function(x)
print(type(y))
#[-1. 1. 2.]
#[False True True]
#<class 'numpy.ndarray'>
import matplotlib.pyplot as plt
x = np.arange(-5.,5.,0.1)
y = step_function(x)
plt.plot(x,y)
plt.ylim(-0.1,1.1)
plt.show()
Sigmoid函数
Sigmoid函数是神经网络经常使用一个函数,公式,实现和图像如下:

def sigmoid(x):
return 1/(1+np.exp(-x))
x = np.arange(-5.,5.,0.1)
y = sigmoid(x)
plt.plot(x,y)
plt.ylim(-0.1,1.1)
plt.show()
函数对比

上面说到,阶跃函数并不是平滑的函数,而sigmoid是,然后二者都是非线性函数,这里先给出结论:
- 神经网络的激活函数必须使用非线性函数。
原因是由线性函数的性质造成的,如果把中间层合并在一起,所做的处理更加类似于一个超级复杂的复合函数,但是线性函数再怎么复合,都可以找到一个参数来等价的表示这些复合的操作,对于中间层来说就是可以用一层来代表N层,这样中间层的存在意义就没有了,神经网络也就无法发挥多层网络带来的优势。下图为简单的举例:

ReLU函数
sigmoid函数很早就开始被使用了,而最近则主要使用ReLU(Rectified Linear Unit)函数。
公式,实现和图像如下:

def relu(x):
return np.maximum(0,x)
x = np.arange(-5.,5.,0.1)
y = relu(x)
plt.plot(x,y)
神经网络和矩阵
矩阵乘法
回顾一下numpy的相关操作,shape表示数组形状,dim表示数组维数;多维数组可以代表矩阵,横为行竖为列;

然后简单了解下矩阵乘法:

代码如下:
A=np.array([[1,2,3],[4,5,6]])
B=np.array([[1,2],[3,4],[5,6]])
print(A.shape,B.shape)
C=np.dot(A,B)
print(C)
print(C.shape)
#(2, 3) (3, 2)
#[[22 28]
# [49 64]]
#(2, 2)在矩阵的乘积运算中,对应维度的元素个数要保持一致 :

神经网络和矩阵
从下图可见,之前所说层与层之间的传播算法,可以统统用矩阵乘法表示。而这一层接一层的传播就是所谓的(前)正向传播。

前向传播
前向传播用语言表达比较繁琐,直接看图和代码则比较方便,无非是一层接一层的矩阵计算和激活函数计算:

输出层的激活函数
激活函数已经了解,但是输出层的激活函数稍微有些不同,但也只是输出函数类型的问题。
首先 ,对于机器学习解决的问题有两种,一种是所谓分类问题,就好比数字识别,从图片判断是0-9的哪一个,还有一种是回归问题,好比从历史数据判断今天地铁的客流量,一个是选择题,一个是填空题。
在输出层,对于选择题使用softmax函数,对于回归问题使用恒等函数。
顺带一提,输出层神经元的数量在分类问题中取决于分类的个数。
恒等函数
恒等函数图像如下,其实就是将输入原样输出,这里便不列出代码identity_function了

SoftMax函数
对于分类使用的SoftMax函数则有一些说法,先看公式和基本实现:

首先这涉及了前一层的所有输入,类比sql语句,这便成为全连接,意思是每一个输出都完整的接受了上一个输入层中的所有数据;而计算时用所有输入做分母,其中一个做分子,这又有点概率的意思,事实上也是这样
最后,SoftMax函数一般只用于学习阶段,因为在推理过程中,一般而言只把输出值最大的神经元所对应的类别作为识别结果,使用softmax会造成额外的计算量
下面是代码实现:
def softmax(a):
exp_a = np.exp(a)
sum_exp_a=np.sum(exp_a)
y=exp_a/sum_exp_a
return y
a = np.array([0.3,2.9,4.0])
print(softmax(a))
#下两行会引发溢出异常
a = np.array([1010,1000,990])
print(softmax(a))
这样是用问题的,因为对于指数函数有溢出的风险,但如果按如下步骤进行改造则可以解决这个问题:

其中 C'一般取a_n这个数组中的最大值取反,这样削减一下大小,就不会有溢出的问题了:
# softmax的改进变形
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
a = np.array([1010,1000,990])
print(softmax(a))
#[9.99954600e-01 4.53978686e-05 2.06106005e-09]总结和阶段性实现
目前,一个支持前向传播的神经网络便可以实现,但是目前只能假设一个权重参数(因为没有学习阶段的支持),然后通过矩阵乘法和中间层和输出层的激活函数来实现前向传播。
所以目前以MNIST数据集为例来实现一个简易的前向传播
测试数据和训练数据
机器学习中,一般将数据分为训练数据(监督数据 )和测试数据两部分来进行学习和实验等。
- 首先,使用训练数据进行学习,寻找最优的参数;
- 然后,使用测试数据评价训练得到的模型的实际能力
在处理测试数据过程中,如果发现效果很好,则说明该模型的泛化能力很好,即面对未知数据表现很好;反面的,如果只对训练数据表现好,则说明泛化能力差,这种现象称为过拟合
MNIST数据集的读取
读取该数据集需要一些工具函数的辅助,这里直接使用《深度学习入门》中提供的工具实现:
# 这里的路径按本地实际来
from dfs.dataset.mnist import load_mnist
import sys,os
sys.path.append(os.pardir)
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True,
normalize=False,one_hot_label=False)
# 输出各个数据的形状
print(x_train.shape) # (60000, 784)
# 输出各个数据的形状
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)- train代表训练数据,test表示测试数据
- x代表输入的图片,t代表最终的标签(在one_hot_label是false时会在显示标签0-9,在true时会返回一个10位数组,在标签数字对应的位置是1,其余为0)
from PIL import Image
def img_show(img):
pil_img = Image.fromarray(np.uint8(img))
pil_img.show()
img = x_train[0]
label = t_train[0]
print(label) # 5
print(img.shape) # (784,)
img = img.reshape(28, 28) # 把图像的形状变成原来的尺寸
print(img.shape) # (28, 28)
img_show(img)- 该数据集中,输入的是一堆图片和图片对应的标签,在这里将图片展开为一维数组
(flatten=True),并使用原始像素信息,而不是将0-255的颜色值压缩到0-1的范围(normalize=false),而将数据压缩到0-1范围这种操作称为标准化或正交化
one-hot表示是仅正确解标签为1,其余皆为0的数组,就像[0,0,1,0,0,0,0,0,0,0]这样。当one_hot_label为False时,只是像 7、 2这样简单保存正确解标签;当 one_hot_label为 True时,标签则保存为one-hot表示。
实现
首先,由于并没有学习这一步骤,所以会给所有权重和偏置设置符合正态分布的默认值;
然后因为传播的实现就是矩阵乘法的实现,现在假设使用的是3层神经网络,则三个权重矩阵的形状要符合矩阵乘法的要求,所以假设矩阵形状如下:
最后,由于一个图片一个图片处理过于缓慢,而矩阵乘法却能吃到SIMD的优化福利,所以会使用批处理的方法,即如下图所示,一次性打包处理100张图像,为此,可以把x的形状改为100 × 784,将100张图像打包作为输入数据:
如此实现如下,首先是神经网络的实现和使用
import numpy as np
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
def sigmoid(x):
return 1/(1+np.exp(-x))
class SigMoidLayer:
def __init__(self,w,b,s="SigMoid"):
self.layer_name = s
self.w=w
self.b = b
def forward(self,x):
a=np.dot(x,self.w)+self.b
return sigmoid(a)
class OutputLayer2:
def __init__(self,w,b,s="Ouput"):
self.layer_name = s
self.w=w
self.b = b
def forward(self,x):
a=np.dot(x,self.w)+self.b
return softmax(a)
class TestMNIST:
# 正常情况下,权重或者偏置都应该是学习的结果,这里直接赋值只是一种假设
def __init__(self):
self.network_w = {}
self.network_b = {}
self.network_w['L1'] = np.random.randn(784,50)
self.network_b['L1'] = 1
self.network_w['L2'] = np.random.randn(50,100)
self.network_b['L2'] = 1
# 输出层
self.network_w['L3'] = np.random.randn(100,10)
self.network_b['L3'] = 1
self.layers=['L1','L2']
def forward(self,x):
tmp=x
for l in self.layers:
tmp = SigMoidLayer(self.network_w[l],self.network_b[l],l).forward(tmp)
print(l,tmp)
tmp = OutputLayer2(self.network_w['L3'],self.network_b['L3'],'L3').forward(tmp)
return tmp
nett = TestMNIST()
#测试代码
#nett.forward(np.random.randn(100,784))
batch_size=100
for i in range(0,len(x_test),batch_size):
x_batch = x_test[i:i+batch_size]
#print(x_batch.shape)
y_batch = nett.forward(x_batch)
p=np.argmax(y_batch,axis=1)
print(p)
#[8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
# 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
# 8 8 8 8 8 8 6 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8]
可见结果并不好,都只会输出8,即使如此也要测一下准确率:
accuracy_cnt=0
for i in range(0,len(x_test),batch_size):
x_batch = x_test[i:i+batch_size]
y_batch = nett.forward(x_batch)
p=np.argmax(y_batch,axis=1)
accuracy_cnt += np.sum(p == t_test[i:i+batch_size])
print("Accuracy:" + str(float(accuracy_cnt) / len(x_test)))
#Accuracy:0.0965太好了,基本都对不上。
评论已关闭