🌻个人主页:相洋同学
🥇学习在于行动、总结和坚持,共勉!
神经网络的本质就是通过参数、线性函数与激活函数来拟合特征与目标之间的真实函数关系。
01 神经网络简介
1.1 引入
神经网络是一门重要的机器学习技术,是一种模拟人脑的神经网络以期能够实现类人工智能的机器学习技术。
人脑中的神经网络是一个非常复杂的组织。成人的大脑中估计有1000亿个神经元之多。
机器学习中的神经网络是如何实现这种模拟,并达到如此惊人的效果的?本篇文章将从简单介绍,并手动基于numpy来复现一个神经网络结构,以期能够全面了解神经网络的构建过程。
1.2 神经元
我们首先来看一下构成大脑的基本结构:神经元
神经元的树突用来接收其他细胞传输来的信号,突触用来传输电信号。其内部可以对信息进行处理。
我们再来看一下深度学习中的神经网络结构:
神经网络一般由输入层、若干个隐藏层和输出层构成,整体来看我们似乎看不出什么联系。我们将单独的神经元抽离出来看
在单个神经元中,左边的输入点类似树突来接收信息,右边的点类似突触用来传送信息。所以深度学习中的神经网络结构与人脑的神经网络结构极其类似。
1.3 如何理解神经元
世界具有普遍联系,事物具有各种特征。自然界中人类通过学习和探索现实世界的特征总结成规律和模式来指导自己的行为。
通过事物的一部分特征来推测事物的另一部分特征是神经网络的主要功能。那为什么要设置不同层数的网络结构呢?
我们可以这样来想:我们都知道,事物具有表面现象和内部特征和联系。通过深层次,多维度的特征抽象,我们能更好的推测事物现象。
更多的关于神经网络发展历史和拓展参照这篇文章:
神经网络——最易懂最清晰的一篇文章
本文着重复现神经网络结构
02 复现神经网络结构
2.1 神经网络总体工作流程
要想复现神经网络结构,我们必须了解基于神经网络的深度学习任务训练全流程:
总体来说神经网络训练体现在以下几个阶段:
1.参数随机初始化:需要提前根据输入样本特征维度、隐藏层维度与输出层维度随机初始化参数;
2.前向传播:将样本矩阵输入模型以计算出预测值;
3.反向传播:根据预测值和真实值通过损失函数来计算出损失loss,通过对损失函数求导利用优化器对模型权重进行更新。
4.迭代更新参数:将新的样本送入模型继续前向传播和反向传播,直到达到预定的训练频次或提前达到训练目标。
2.2 神经网络代码实现(使用numpy库)
基于神经网络的结构,我们先对任务进行描述和分解
目标:总目标是撰写一个神经网络结构能够完成简单分类任务
1.模型初始化,输入应当包含batch_size,学习率,输入维度,隐藏层维度,输出维度;
2.实现损失函数和激活函数的封装,便于调用;
3.撰写前向传播方法,能够根据模型参数计算出预测值;
4.撰写反向传播方法,能够根据真实值和预测值来更新模型参数;
5.构造数据集,并实现模型的训练,最后输入自己构造的样本进行预测。
2.2.1 模型初始化
值得注意的是我们一定要时刻关注模型参数的维度。
我们在这里构造一个两层的网络,假设其输入维度是n*m,隐藏层维度是h,输出层维度是1(假设我们构建一个二分类任务)
则其w1维度应该为m*h,w2维度为h*1,这样我们才能得到n*h的隐藏层,和n*1的输出层(矩阵内积)
该代码还设计了字典memory来存储中间状态的矩阵,以便进行反向传播。
class FullyConnectedNetwork:
'''
这是一个自定义的简单的神经网络,有一个隐藏层和一个输出层
'''
def __init__(self, batch_size, learning_rate, input_size, hidden_size, output_size):
'''
初始化神经网络
:batch_size: 批量大小
:learning_rate: 学习率
:param input_size: 输入层大小,就是输入特征的维度(特征数量)
:param hidden_size: 隐藏层大小
:param output_size: 输出层大小,就是输出的维度(分类数量)
'''
self.batch_size = batch_size
self.learning_rate = learning_rate
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.W1 = np.random.randn(input_size, hidden_size)
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size)
self.b2 = np.zeros((1, output_size))
self.memory = {}
2.2.2 实现损失函数和激活函数的封装
为了更加清楚得了解反向传播的过程,本文手动封装了损失函数与激活函数,并对损失函数和激活函数的导数进行提前封装。
关于损失函数还有均方差等不同的方式,激活函数也有softmax,relu等一系列激活函数。这里我们选用sigmoid以计算概率实现二分类。
def sigmoid(self, x):
'''
sigmoid激活函数
:param x: 特征向量矩阵
:return: sigmoid(x)
'''
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(self, x):
'''
sigmoid激活函数的导数
:param x: 特征向量矩阵
:return: sigmoid(x)*(1-sigmoid(x))
'''
return x * (1 - x)
def cross_entropy(self, y_pred, y_true):
'''
交叉熵损失函数
:param y_pred: 预测值
:param y_true: 真实值
:return: 损失值
'''
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
def cross_entropy_derivative(self, y_pred, y_true):
'''
交叉熵损失函数的导数
:param y_pred: 预测值
:param y_true: 真实值
:return: 损失函数对y_pred的导数
'''
return - (y_true / y_pred) + (1 - y_true) / (1 - y_pred)
2.2.3 前向传播
前向传播的本质是做矩阵内积,这个过程我们保存中间态A1,Z1和A2,Z2以方便进行反向传播
def forward(self, X):
'''
前向传播
:param X: 输入的特征向量矩阵
:return: 返回输出层的输出值(预测值)
'''
Z1 = np.dot(X, self.W1) + self.b1
A1 = self.sigmoid(Z1)
self.memory['A1'] = A1
self.memory['Z1'] = Z1
Z2 = np.dot(A1, self.W2) + self.b2
A2 = self.sigmoid(Z2)
self.memory['A2'] = A2
self.memory['Z2'] = Z2
return A2
2.3.4 反向传播
针对模型的反向传播过程涉及到链式求导的知识,需要分别计算输出层的梯度,输出层的权重和偏执梯度,隐藏层的梯度,隐藏层权重和偏执的梯度,最后根据梯度更新参数。
关于梯度下降参考这篇文章:【深度学习】梯度下降与反向传播
后序还会对神经网络中的反向传播过程进行深层次的拆解,敬请期待
这里先罗列一下整个过程:
def backward(self, X, y_true, y_pred):
'''
反向传播
:param X:
'''
dA2 = self.cross_entropy_derivative(y_pred, y_true)
dZ2 = dA2 * self.sigmoid_derivative(self.memory['A2'])
dW2 = 1. / self.batch_size * np.dot(self.memory['A1'].T, dZ2)
db2 = 1. / self.batch_size * np.sum(dZ2, axis=0, keepdims=True)
dA1 = np.dot(dZ2, self.W2.T)
dZ1 = dA1 * self.sigmoid_derivative(self.memory['A1'])
dW1 = 1. / self.batch_size * np.dot(X.T, dZ1)
db1 = 1. / self.batch_size * np.sum(dZ1, axis=0, keepdims=True)
return dW1, db1, dW2, db2
def update_parameters(self, dW1, db1, dW2, db2):
'''
更新参数
:param dW1:
:param db1:
:param dW2:
:param db2:
'''
self.W1 -= self.learning_rate * dW1
self.b1 -= self.learning_rate * db1
self.W2 -= self.learning_rate * dW2
self.b2 -= self.learning_rate * db2
2.3.5 构建数据集
最后本文设计了如下任务:随机生层一批具有五个特征的样本,如果样本的第二个数大于第四个数,标记为1,否则为零
def build_simple():
"""
构建一个简单的样本:随机生成5个特征判断第二个特征是否大于第四个特征,如果大于,目标值记为1,否则记为0
:return: list(1*5), list(1*1)
"""
simple = np.random.randint(0,10,size=(5))
if simple[1] > simple[3]:
y = 1
else:
y = 0
return simple, y
def build_data(num):
"""
生成数据集
:param num: 生成样本数量
:return: 返回X,y
"""
X = []
y = []
for i in range(num):
x, y_ = build_simple()
X.append(x)
y.append(y_)
return np.array(X), np.array(y).reshape(-1,1)
2.3.6 模型训练
这里的训练轮数,样本数均可以进行修改,可以多多尝试,体验不同参数下的训练。
def main():
# 定义神经网络
batch_size = 10
num_epochs = 10000
FCNN = FullyConnectedNetwork(batch_size=batch_size, learning_rate=0.1, input_size=5, hidden_size=5, output_size=1)
X,y = build_data(100000)
# 训练神经网络
for i in range(num_epochs):
# 前向传播
x_ = X[i*batch_size:(i+1)*batch_size,:]
y_ = y[i*batch_size:(i+1)*batch_size,:]
y_pred = FCNN.forward(x_)
# 计算损失
loss = FCNN.cross_entropy(y_pred, y_)
# 反向传播
dW1, db1, dW2, db2 = FCNN.backward(x_, y_, y_pred)
# 更新参数
FCNN.update_parameters(dW1, db1, dW2, db2)
if i % 100 == 0:
print("第{}次训练,损失为{}".format(i, loss))
print('==========================')
# 定义输入数据
test = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [5, 8, 5, 4, 1], [2, 3, 1, 1, 1]])
# 预测输出
y_pred = FCNN.forward(test)
y_pred = np.squeeze(y_pred)
for i in range(len(y_pred)):
if y_pred[i] > 0.5:
y_pred[i] = 1
else:
y_pred[i] = 0
for i in range(len(test)):
print("输入数据为{}".format(test[i]))
print("预测输出为{}".format(y_pred[i]))
测试结果:
可以看到我们的训练训练结果还是相当不错,这里没有写评估准确率的逻辑,学有余力的朋友可以尝试写一下。
参考文章:
自己动手写神经网络(一)——初步搭建全连接神经网络框架-CSDN博客
神经网络——最易懂最清晰的一篇文章-CSDN博客
下面是完整代码:
'''
标题: 手动实现神经网络
作者:相洋同学
日期:2024年3月18日
'''
import numpy as np
class FullyConnectedNetwork:
'''
这是一个自定义的简单的神经网络,有一个隐藏层和一个输出层
'''
def __init__(self, batch_size, learning_rate, input_size, hidden_size, output_size):
'''
初始化神经网络
:batch_size: 批量大小
:learning_rate: 学习率
:param input_size: 输入层大小,就是输入特征的维度(特征数量)
:param hidden_size: 隐藏层大小
:param output_size: 输出层大小,就是输出的维度(分类数量)
'''
self.batch_size = batch_size
self.learning_rate = learning_rate
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.W1 = np.random.randn(input_size, hidden_size)
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size)
self.b2 = np.zeros((1, output_size))
self.memory = {}
def sigmoid(self, x):
'''
sigmoid激活函数
:param x: 特征向量矩阵
:return: sigmoid(x)
'''
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(self, x):
'''
sigmoid激活函数的导数
:param x: 特征向量矩阵
:return: sigmoid(x)*(1-sigmoid(x))
'''
return x * (1 - x)
def cross_entropy(self, y_pred, y_true):
'''
交叉熵损失函数
:param y_pred: 预测值
:param y_true: 真实值
:return: 损失值
'''
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
def cross_entropy_derivative(self, y_pred, y_true):
'''
交叉熵损失函数的导数
:param y_pred: 预测值
:param y_true: 真实值
:return: 损失函数对y_pred的导数
'''
return - (y_true / y_pred) + (1 - y_true) / (1 - y_pred)
def forward(self, X):
'''
前向传播
:param X: 输入的特征向量矩阵
:return: 返回输出层的输出值(预测值)
'''
Z1 = np.dot(X, self.W1) + self.b1
A1 = self.sigmoid(Z1)
self.memory['A1'] = A1
self.memory['Z1'] = Z1
Z2 = np.dot(A1, self.W2) + self.b2
A2 = self.sigmoid(Z2)
self.memory['A2'] = A2
self.memory['Z2'] = Z2
return A2
def backward(self, X, y_true, y_pred):
'''
反向传播
:param X:
'''
dA2 = self.cross_entropy_derivative(y_pred, y_true)
dZ2 = dA2 * self.sigmoid_derivative(self.memory['A2'])
dW2 = 1. / self.batch_size * np.dot(self.memory['A1'].T, dZ2)
db2 = 1. / self.batch_size * np.sum(dZ2, axis=0, keepdims=True)
dA1 = np.dot(dZ2, self.W2.T)
dZ1 = dA1 * self.sigmoid_derivative(self.memory['A1'])
dW1 = 1. / self.batch_size * np.dot(X.T, dZ1)
db1 = 1. / self.batch_size * np.sum(dZ1, axis=0, keepdims=True)
return dW1, db1, dW2, db2
def update_parameters(self, dW1, db1, dW2, db2):
'''
更新参数
:param dW1:
:param db1:
:param dW2:
:param db2:
'''
self.W1 -= self.learning_rate * dW1
self.b1 -= self.learning_rate * db1
self.W2 -= self.learning_rate * dW2
self.b2 -= self.learning_rate * db2
def build_simple():
"""
构建一个简单的样本:随机生成5个特征判断第二个特征是否大于第四个特征,如果大于,目标值记为1,否则记为0
:return: list(1*5), list(1*1)
"""
simple = np.random.randint(0,10,size=(5))
if simple[1] > simple[3]:
y = 1
else:
y = 0
return simple, y
def build_data(num):
"""
生成数据集
:param num: 生成样本数量
:return: 返回X,y
"""
X = []
y = []
for i in range(num):
x, y_ = build_simple()
X.append(x)
y.append(y_)
return np.array(X), np.array(y).reshape(-1,1)
def main():
# 定义神经网络
batch_size = 10
num_epochs = 10000
FCNN = FullyConnectedNetwork(batch_size=batch_size, learning_rate=0.1, input_size=5, hidden_size=5, output_size=1)
X,y = build_data(100000)
# 训练神经网络
for i in range(num_epochs):
# 前向传播
x_ = X[i*batch_size:(i+1)*batch_size,:]
y_ = y[i*batch_size:(i+1)*batch_size,:]
y_pred = FCNN.forward(x_)
# 计算损失
loss = FCNN.cross_entropy(y_pred, y_)
# 反向传播
dW1, db1, dW2, db2 = FCNN.backward(x_, y_, y_pred)
# 更新参数
FCNN.update_parameters(dW1, db1, dW2, db2)
if i % 100 == 0:
print("第{}次训练,损失为{}".format(i, loss))
print('==========================')
# 定义输入数据
test = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [5, 8, 5, 4, 1], [2, 3, 1, 1, 1]])
# 预测输出
y_pred = FCNN.forward(test)
y_pred = np.squeeze(y_pred)
for i in range(len(y_pred)):
if y_pred[i] > 0.5:
y_pred[i] = 1
else:
y_pred[i] = 0
for i in range(len(test)):
print("输入数据为{}".format(test[i]))
print("预测输出为{}".format(y_pred[i]))
if __name__ == '__main__':
main()
以上
互联网是最好的课本,实践是最好的老师,AI是最好的学习助手
行动起来,共勉