02 NLP合集-神经网络从0开始推理-一个间的神经网路的预测-没有backforward的情况下
1.一个简单神经网络的推理过程
用〇表示神经元,用箭头表示它们的连接
权重::箭头上上有权重,这个权重和对应的神经元的值分别相乘,其和(严格地讲,是经过激活函数变换后的值)作为下一个神经元的输入。
偏置:另外,此时还要加上一个不受前一层的神经元影响的常数,
全连接网络:所有相邻的神经元之间都存在由箭头表示的连接,
图1
此网络虽然有三层,但我们一般把它叫做两层神经网络:因为有权重的层实际上是2层
这里用(x1, x2)表示输入层的数据,用和表示权重,用b1表示偏置。这样一来,图1中的隐藏层的第1个神经元就可以如下进行计算:
--式1
隐藏层的神经元是基于加权和计算出来的。之后,改变权重和偏置的值,根据神经元的个数,重复进行相应次数的式1的计算,这样就可以求出所有隐藏层神经元的值
基于全连接层的变换可以通过矩阵乘积如下进行整理:
式2
这里,隐藏层的神经元被整理为(),它可以看作1×4的矩阵(或者行向量)。另外,输入是(),这是一个1×2的矩阵。再者,权重是2×4的矩阵,偏置是1×4的矩阵。这样一来,式1可以如下进行简化:
-式3
大写字母表示矩阵 小写字母加上箭头表示向量
在矩阵乘积中,要使对应维度的元素个数一致。通过像这样观察矩阵的形状,可以确认变换是否正确。
这样一来,我们就可以利用矩阵来整体计算全连接层的变换。不过,这里进行的变换只针对单笔样本数据(输入数据)。在神经网络领域,我们会同时对多笔样本数据(称为mini-batch,小批量)进行推理和学习。因此,我们将单独的样本数据保存在矩阵x的各行中。假设要将N笔样本数据作为mini-batch整体处理,关注矩阵的形状,其变换
图2
如图2所示,根据形状检查,可知各mini-batch被正确地进行了变换。此时,N笔样本数据整体由全连接层进行变换,隐藏层的N个神经元被整体计算出来。现在,我们用Python写出mini-batch版的全连接层变换。
W1=np.random.randn(2,4) #返回2×5的有标准正态分布的numpy数组
b1=np.random.randn(4) #返回1×4的有标准正态分布的numpy数组
x=np.random.randn(1,2) #返回1×2的有标准正态分布的numpy数组
h=np.dot(x,W1)+b1 #在上面的代码中,偏置b1的加法运算会触发广播功能。b1的形状是(4,),它会被自动复制,变成(10, 4)的形状。
全连接层的变换是线性变换。激活函数赋予它“非线性”的效果。严格地讲,使用非线性的激活函数,可以增强神经网络的表现力。激活函数有很多种,这里我们使用式(1.5)的sigmoid函数(sigmoid function):
式4
下图相信朋友们会很熟悉,这是sigmoid曲线
函数的代码如下:
def sigmoid(x):
return 1/(1+np.exp(-x))
将我们得到的输出结果用sigmoid函数转化
sigmoid(h)
array([[0.73624525, 0.42015471, 0.77171793, 0.83578355], [0.94334643, 0.99308407, 0.99282625, 0.20780671], [0.64833298, 0.32214671, 0.6904555 , 0.79127822], [0.55477809, 0.06710898, 0.39776656, 0.94636061], [0.52995509, 0.02206201, 0.24965226, 0.9835166 ], [0.8327653 , 0.44353438, 0.82131126, 0.93335014], [0.77760166, 0.08576874, 0.54631216, 0.98962505], [0.61892398, 0.3702811 , 0.70356881, 0.69552478], [0.5119634 , 0.47724136, 0.71414597, 0.36303062], [0.63143524, 0.62421249, 0.81191444, 0.41687422]])
x = np.random.randn(10,2)
W1 = np.random.randn(2,4)
b1 = np.random.randn(4)
W2 = np.random.randn(4, 3)
b2 = np.random.randn(3)
h = np.dot(x, W1)+ b1
a = sigmoid(h)
s = np.dot(a, W2)+ b2
我们就这样得到了输出神经元的结果:
这里,x的形状是(10, 2),表示10笔二维数据组织为了1个mini-batch。最终输出的s的形状是(10, 3)。同样,这意味着10笔数据一起被处理了,每笔数据都被变换为了三维数据。
2 层的类化及正向传播的实现
现在,我们将神经网络进行的处理实现为层。这里将全连接层的变换实现为Affine层,将sigmoid函数的变换实现为Sigmoid层。因为全连接层的变换相当于几何学领域的仿射变换,所以称为Affine层。另外,将各个层实现为Python的类,将主要的变换实现为类的forward()方法。
神经网络的推理所进行的处理相当于神经网络的正向传播。此时,构成神经网络的各层从输入向输出方向按顺序传播处理结果。之后我们会进行神经网络的学习,那时会按与正向传播相反的顺序传播数据(梯度),所以称为反向传播。
神经网络中有各种各样的层,我们将其实现为Python的类。通过这种模块化,可以像搭建乐高积木一样构建网络。
·所有的层都有forward()方法和backward()方法
·所有的层都有params和grads实例变量
简单说明一下这个代码规范。首先,forward()方法和backward()方法分别对应正向传播和反向传播。其次,params使用列表保存权重和偏置等参数(参数可能有多个,所以用列表保存)。grads以与params中的参数对应的形式,使用列表保存各个参数的梯度(后述)。这就是本专栏的代码规范。
遵循上述代码规范,代码看上去会更清晰。我们后面会说明为什么要遵循这样的规范,以及它的有效性。
因为这里只考虑正向传播,所以我们仅关注代码规范中的以下两点:一是在层中实现forward()方法;二是将参数整理到实例变量params中。我们基于这样的代码规范来实现层,首先实现Sigmoid层,如下所示:
class Sigmoid:
def __init__(self):
self.params=[]
pass
def forward(self,x):
return 1/(1+np.exp(-x))
如上所示,sigmoid函数被实现为一个类,主变换处理被实现为forward(x)方法。这里,因为Sigmoid层没有需要学习的参数,所以使用空列表来初始化实例变量params。下面,我们接着来看一下全连接层Affine层的实现,
class Affine:
def __init__(self,W,b):
self.params = [W,b]
pass
def forward(self,x):
W,b =self.params
out =np.dot(x,W)+b
return out
Affine层在初始化时接收权重和偏置。此时,Affine层的参数是权重和偏置(在神经网络的学习时,这两个参数随时被更新)。因此,我们使用列表将这两个参数保存在实例变量params中。然后,实现基于forward(x)的正向传播的处理。
根据本书的代码规范,所有的层都需要在实例变量params中保存要学习的参数。因此,可以很方便地将神经网络的全部参数整理在一起,参数的更新操作、在文件中保存参数的操作都会变得更容易。
现在,我们使用上面实现的层来实现神经网络的推理处理。这里实现如图4所示的层结构的神经网络。
图4
之前,我们在用图表示神经网络时,使用的是像图1那样的“神经元视角”的图。与此相对,图4是“层视角”的图。
class TwoLayerNet:
def __init__(self,input_size,hidden_size,output_size):
I,H,O = input_size,hidden_size,output_size
# 初始化权重和偏置
W1 = np.zeros((I,H))
b1= np.ones(H)
W2 = np.zeros((H,O))
b2=np.ones(O)
#生成层
self.layers = [
Affine(W1,b1),
Sigmoid(),
Affine(W2,b2)
]
#将所有的权重整理到列表中
self.params = []
for layer in self.layers:
self.params+=layer.params# 此时的params生成了四维数组 存储的值分别为W1,b1,W2,b2等参数
pass
pass
def predict(self,x):
for layer in self.layers:
x = layer.forward(x) #我们循环每一层的layer的一层一层的计算x的值
return x
现在即可计算出答案
x =np.ones((1,2))
model =TwoLayerNet(2,4,3)
model.predict(x)
我们在这里将要学习的参数都拼接为一个params数组,这样的神经网络更加直观和操作简单。