深度学习(第1章——神经网络原理和Pytorch入门)
前言:
本章将讲解神经网络原理,神经元如何处理输入并输出,什么是梯度,多层感知机中梯度的计算,Pytoch自动梯度效果,如何使用原生Python实现一个简单的神经网络,以及对应Pytorch实现。
神经网络原理:
神经网络的设计灵感来源于人脑的结构和功能。人脑由数十亿个神经元组成,这些神经元通过突触相互连接,形成复杂的网络。
神经元的基本单元:
人脑的基本单元是神经元,每个神经元接收来自其他神经元的信号,并通过突触将信号传递给下一个神经元。在人工神经网络中,神经元被模拟为节点,每个节点接收输入并生成输出。
层次结构:
人脑的神经元通常以层次结构组织,信息从感知层(如视觉、听觉)传递到更高层次的处理区域。类似地,人工神经网络通常由输入层、隐藏层和输出层组成,信息在这些层之间传递和处理。
激活函数:
神经元在接收到足够的刺激后会“激活”,并产生输出。这个过程可以通过激活函数来模拟。在神经网络中,激活函数(如ReLU、Sigmoid等)决定了节点的输出,模拟了生物神经元的激活机制。
学习与适应:
人脑通过突触的强度变化来学习和记忆,这种过程称为突触可塑性。人工神经网络通过调整权重来学习,使用反向传播算法来优化网络的性能。
并行处理:
人脑能够同时处理大量信息,具有高度的并行处理能力。神经网络也能够并行处理输入数据,特别是在使用GPU等硬件加速时。
如果是初学者,可能对上面的一些名词有些陌生,不过影响不大,可以先简单的将神经网络看作一个黑盒,其在对训练数据 进行一段时间的训练后,能够对于测试数据
输出其预测值
,其中
表示一次训练的(特征,标签)对的数目,即批次,
特征空间,
的维度数目即为特征的数目,比如一个人的身高,体重两项数据对应特征空间维度为2,
,
表示标签的种类。神经网络相当于一个映射函数
。
神经元:
先看神经网络的基本单位:神经元:
每个神经元均会根据输入的特征X给出一个输出值y,神经元会对每个特征进行权衡,毕竟每个特征对最终输出的影响结果也会不同,并会将加权后的特征相加后在加上一个偏置量b,这样即使所有特征的权重都为0的情况下也会产生一个非0的输出,同时会加上一个激活函数,将无边界的输入控制在一个可控的范围,也可以结合实际生物中当刺激达到一定程度时即会产生一个电位,但这个电位有最大值(与Na+ K+离子的进出速率有关,但速率也有上限,不可能被针扎一下直接220V)因此最终可以得到以下公式:
其中即为激活函数,这里拿Sigmod激活函数举例,其表达式为:
对其求导可发现:
其波形如下,能够将正负无穷的数据最终统一到0到1的范围内。
梯度:
梯度(偏导的集合)是多变量微积分中的一个基本概念,表示一个标量函数在某一点的变化率和方向。具体而言,梯度是一个向量,它指向函数在该点上升最快的方向,其大小表示函数在该方向上的变化率。
简单而言,对于的函数,先计算
相对于每个
的偏导,(如果
对于
的偏导为2,即表示
每增加1,
增加2),将
,
,
看作一个整体
,如果需要知道
相对于
的梯度,其值即为计算结果
对每个
的偏导并组成一个向量。
简单介绍上面代码,创建了一个值为(1,2,3)的向量,并开启自动梯度,开启后通过backward调用能够获取该向量经过一定变化后输出对于原向量的梯度,这里输出:
求偏导后容易得到相对于
的梯度为(2,2,2)。
多层感知机:
将多个神经元组装起来即可得到神经网络,下图是一个两层的神经网络,包含三个神经元
其中:
这里把看为神经网络的一层,
为神经网络的另一层,神经网络每层的输入是上一层的输出,类似生物中信号传递。尽管这里仅仅只由两层网络且使用的函数为线性变化,但由于激活函数引入非线性变化,这个神经网络以及能够模拟大部分非线性函数。
这里假设需要用上述神经网络完成一项根据身高和体重判别性别的任务,标签和数据如下:
初始时随机化权重,每一对(体重,身高)输入网络后,由于最后经过一层激活函数都归一化到0到1的范围。就例如对于某次初始权重Alice(-2,-1)最终输入为0.2,这显然与正确性别1有较大差距,这里将这个差距定义为损失Loss。损失用于衡量真实标签和预测标签的差距程度,显然这个程度越小表明模型效果越好。当然由于有多个数据,可能仅仅只是这个数据记录错误将男生记录成了女生,而神经网络最终实现的效果应该是在大部分样本上取得较好效果,应该损失应该累加每组的损失。使用均方差损失,计算如下:
(这里如果采用距离损失则显然损失越趋近于0越好,但如果使用别的损失计算发现损失可能为负数,且损失越小越好,则可以使用Sigmod进行一次激活。)
当计算出损失后,如何通过损失优化权重,这里有前人做出了数学证明,只需要知道结论即可:用当前权重的每一个参数减一个逐渐递减的值(学习率)乘损失并更新该参数,重复无穷次最终得到的值即为让损失为0的值。不过在实践中发现,当学习率恒定取较小的值时也能取得较好的效果,当然如果太小取学习率为0.00001,当
初始值为0,真实值为1时,在上述损失始终在(0,1)的范围内时至少需要迭代100000次。但取太大又会振荡,因此需要合理选取学习率。
Python原生实现神经网络:
在知道神经网络原理后,显然最终要的是求出梯度,由于torch提供了自动梯度所以不需要手动计算,先介绍单神经元的梯度计算方式。虽然前面梯度部分也又提到,这里加深理解。
这里x可以看作一维特征,y为标签,w为神经网络初始权重(1,1),z为输出通过z的计算方式能够发现其实这里神经网络是一个简单的线性模型,损失为z和y插值的平方。梯度计算如下:
使用torch计算结果如下:
下面展示在两层神经网络中损失对于的偏导计算:
同理能够求出关于所有参数的梯度,同时能够发现其中有很多公共项能够复用,这里不再赘述。
代码如下,其中权重全部初始化为0.1,方便复现结果。
import numpy as npdef sigmoid(x):return 1 / (1 + np.exp(-x))def deriv_sigmoid(x):fx = sigmoid(x)return fx * (1 - fx)def mse_loss(y_true, y_pred):return ((y_true - y_pred) ** 2).mean()class OurNeuralNetwork:def __init__(self):# 权重,Weightsself.w1 = 0.1self.w2 = 0.1self.w3 = 0.1self.w4 = 0.1self.w5 = 0.1self.w6 = 0.1# 截距项,Biasesself.b1 = 0.1self.b2 = 0.1self.b3 = 0.1def feedforward(self, x):h1 = sigmoid(self.w1 * x[0] + self.w2 * x[1] + self.b1)h2 = sigmoid(self.w3 * x[0] + self.w4 * x[1] + self.b2)o1 = sigmoid(self.w5 * h1 + self.w6 * h2 + self.b3)return o1def train(self, data, all_y_trues):learn_rate = 0.1epochs = 1000 # 遍历整个数据集的次数for epoch in range(epochs):for x, y_true in zip(data, all_y_trues):sum_h1 = self.w1 * x[0] + self.w2 * x[1] + self.b1h1 = sigmoid(sum_h1)sum_h2 = self.w3 * x[0] + self.w4 * x[1] + self.b2h2 = sigmoid(sum_h2)sum_o1 = self.w5 * h1 + self.w6 * h2 + self.b3o1 = sigmoid(sum_o1)y_pred = o1d_L_d_ypred = -2 * (y_true - y_pred)# Neuron o1d_ypred_d_w5 = h1 * deriv_sigmoid(sum_o1)d_ypred_d_w6 = h2 * deriv_sigmoid(sum_o1)d_ypred_d_b3 = deriv_sigmoid(sum_o1)d_ypred_d_h1 = self.w5 * deriv_sigmoid(sum_o1)d_ypred_d_h2 = self.w6 * deriv_sigmoid(sum_o1)# Neuron h1d_h1_d_w1 = x[0] * deriv_sigmoid(sum_h1)d_h1_d_w2 = x[1] * deriv_sigmoid(sum_h1)d_h1_d_b1 = deriv_sigmoid(sum_h1)# Neuron h2d_h2_d_w3 = x[0] * deriv_sigmoid(sum_h2)d_h2_d_w4 = x[1] * deriv_sigmoid(sum_h2)d_h2_d_b2 = deriv_sigmoid(sum_h2)# Neuron h1self.w1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w1self.w2 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w2self.b1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_b1# Neuron h2self.w3 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w3self.w4 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w4self.b2 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_b2# Neuron o1self.w5 -= learn_rate * d_L_d_ypred * d_ypred_d_w5self.w6 -= learn_rate * d_L_d_ypred * d_ypred_d_w6self.b3 -= learn_rate * d_L_d_ypred * d_ypred_d_b3if epoch % 10 == 0:y_preds = np.apply_along_axis(self.feedforward, 1, data)loss = mse_loss(all_y_trues, y_preds)print("Epoch %d loss: %.3f" % (epoch, loss))# 定义数据集
data = np.array([[-2, -1], # Alice[25, 6], # Bob[17, 4], # Charlie[-15, -6], # Diana
])
all_y_trues = np.array([1, # Alice0, # Bob0, # Charlie1, # Diana
])network = OurNeuralNetwork()
network.train(data, all_y_trues)
emily = np.array([-7, -3])
frank = np.array([20, 2])
print("Emily: %.3f" % network.feedforward(emily))
print("Frank: %.3f" % network.feedforward(frank))
最终结果如下:
Pytorch实现神经网络:
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np# 定义神经网络类
class OurNeuralNetwork(nn.Module):def __init__(self):super(OurNeuralNetwork, self).__init__()# 定义两个线性层self.layer1 = nn.Linear(2, 2)self.layer2 = nn.Linear(2, 1)# 初始化权重和偏置nn.init.constant_(self.layer1.weight, 0.1)nn.init.constant_(self.layer1.bias, 0.1)nn.init.constant_(self.layer2.weight, 0.1)nn.init.constant_(self.layer2.bias, 0.1)def forward(self, x):# 第一个隐藏层h1 = torch.sigmoid(self.layer1(x))# 输出层o1 = torch.sigmoid(self.layer2(h1))return o1.flatten()# 定义数据集
data = np.array([[-2, -1], # Alice[25, 6], # Bob[17, 4], # Charlie[-15, -6], # Diana
])
all_y_trues = np.array([1, # Alice0, # Bob0, # Charlie1, # Diana
])# 将numpy数组转换为torch张量
data = torch.tensor(data, dtype=torch.float32)
all_y_trues = torch.tensor(all_y_trues, dtype=torch.float32)# 初始化网络
network = OurNeuralNetwork()# 定义损失函数和优化器
loss_fn = nn.MSELoss()
optimizer = optim.SGD(network.parameters(), lr=0.1)# 训练网络
epochs = 1000
for epoch in range(epochs):for i in range(len(data)):optimizer.zero_grad()y_pred = network(data[i])loss = loss_fn(y_pred, all_y_trues[i].unsqueeze(0))loss.backward()optimizer.step()if epoch % 10 == 0:with torch.no_grad():y_preds = network(data)loss = loss_fn(y_preds, all_y_trues)print("Epoch %d loss: %.3f" % (epoch, loss.item()))# 测试新样本
emily = torch.tensor([-7, -2], dtype=torch.float32)
frank = torch.tensor([20, 2], dtype=torch.float32)
with torch.no_grad():print("Emily (Linear): %.3f" % network(emily).item()) # 输出预测结果print("Frank (Linear): %.3f" % network(frank).item()) # 输出预测结果
以下为逐段代码分析:
模型定义了两个线性层,第一个输入维度为2,输出维度也为2,因为前面提到每个神经元可以接受若干特征输入但只有一个特征输出,所以能够通过输出维度判断该层只有两个神经元。同理第二层只有一个神经元(当然理论上单个神经元确实可以有多个输出但有以下问题,1:与生物上单一电信号输出相悖,2:实际等效于多个神经元堆叠,3:破坏了pytoch现有框架,反向传播需要重新推导)
第一层可以看作:
其中为1*2矩阵
,
为2*2矩阵
,
为矩阵
,
为
,
第二层以此类推(打公式太麻烦了)
在定义完毕后,初始化所有参数为0.1,确保效果与之前原生实现一致。
forward函数定义最终输出结果流程,这里表示输入x经过了sigmod(h1)和sigmod(o1)得到。
数据集。
损失。
定义优化器,优化为该神经网络的所有参数,即,学习率为0.1,即每次用当前权重减去梯度乘0.1。
训练1000次,每轮训练前先清空梯度,因为torch的梯度计算会一直累计,不清空会将上轮训练的梯度也算进来。
计算预测值。
计算损失,损失必须为标量,所以需要unsqueeze。
计算损失相对参与损失计算的所有设置自动梯度的向量梯度,这里指上面9个权重,nn会自动为权重设置自动梯度记录。
优化器将其里面的向量根据梯度值和学习率优化。
每十轮打印损失。
进行预测,关闭自动梯度加快预测速度。
最终结果与之前原生实现一致。