一个individual project,记录一下
1.原理
全连接神经网络,顾名思义,就是每一层的神经元,都和上一层的所有神经元全部相连,同时也和下一层的神经元全部相连,就像下图所示。
具体是怎么“相连”的呢,很简单,就是“加权求和”。每条线就是一个权重w,每个神经元的值是x,那么下一层的某个神经元的值就是上一层所有神经元的值x乘上对应的w,然后求和,最后可能还会加一个偏置b。即y=wx+b。
对于某一层全连接层,其本质无非就是有一堆权重w,以及一个偏置b。在代码中可以直接用一个np.array表示。对于层与层之间的运算,本质上就是加权求和,在代码中可以用直接使用矩阵运算,方便快捷。
难点在于如何进行反向传播。我们知道权重参数最开始是随机取值,然后通过训练,对损失值求梯度,再利用梯度对参数进行更新。在pytorch等框架中,这个过程好像是通过计算图来自动的完成的。但我们手写代码,只能根据求导公式一步一步写。这篇文章讲的很详细:全连接神经网络反向传播详解_全连接反向传播-CSDN博客
除此之外,还有就是参数的保存,参数的加载,以及结果的可视化等操作。
2.代码
import random
import numpy as np
import matplotlib.pyplot as plt
# 用于添加一个维度
def expand_dim(input,T=True):
return np.expand_dims(input, axis=1).T if T else np.expand_dims(input, axis=1)
# 用于删去一个维度
def squeeze(input):
return np.squeeze(input,axis=0)
# 用于打乱训练样本顺序(其实没有意义,因为不是用一个minibatch来update)
def shuffle(X,y):
n=len(X)
X_new=X.copy()
y_new=y.copy()
shuffle_list=list(range(n))
random.shuffle(shuffle_list)
for idx,i in enumerate(shuffle_list):
X_new[idx]=X[i]
y_new[idx]=y[i]
return X,y
class myNet:
def __init__(self, input_size, hidden1_size, hidden2_size, output_size):
self.W1 = np.random.randn(hidden1_size, input_size) # 输入层到第一个隐藏层的权重
self.b1 = np.zeros((hidden1_size, 1)) # 第一个隐藏层的偏置
self.W2 = np.random.randn(hidden2_size, hidden1_size) # 第一个隐藏层到第二个隐藏层的权重
self.b2 = np.zeros((hidden2_size, 1)) # 第二个隐藏层的偏置
self.W3 = np.random.randn(output_size, hidden2_size) # 第二个隐藏层到输出层的权重
self.b3 = np.zeros((output_size, 1)) # 输出层的偏置
self.activation=self.sigmoid # 激活函数
self.activation_derivative=self.sigmoid_derivative # 激活函数的导函数
# sigmoid激活函数
def sigmoid(self, X):
return 1 / (1 + np.exp(-X))
# sigmoid激活函数的求导
def sigmoid_derivative(self, x):
return self.sigmoid(x) * (1 - self.sigmoid(x))
# relu激活函数
def relu(self,x):
return np.maximum(0, x)
# relu激活函数的求导
def relu_derivative(self,x):
x[x <= 0] = 0
x[x > 0] = 1
return x
# silu激活函数
def silu(self, x):
return x * self.sigmoid(x)
# silu激活函数的求导
def silu_derivative(self, x):
return self.sigmoid(x) + x * self.sigmoid_derivative(x)
# 正向传播
def forward(self, X):
# 前向传播
self.Z1 = np.dot(self.W1, X) + self.b1 # 第一个隐藏层的加权和
self.A1 = self.activation(self.Z1) # 第一个隐藏层的激活值
self.Z2 = np.dot(self.W2, self.A1) + self.b2 # 第二个隐藏层的加权和
self.A2 = self.activation(self.Z2) # 第二个隐藏层的激活值
self.Z3 = np.dot(self.W3, self.A2) + self.b3 # 输出层的加权和
return self.Z3
def backward(self, X, y, learning_rate):
# 反向传播并更新参数,这里代码对应的loss函数是均方误差MSE
m = X.shape[1] # 样本数量(可以理解为batchsize,不过这里是训练集数量)
# 计算输出层的误差
dZ3 = self.Z3 - y # 误差
dW3 = np.dot(dZ3, self.A2.T) / m # 这里是关键,梯度=误差*上一层的输入
db3 = np.sum(dZ3, axis=1, keepdims=True) / m
# 计算第二个隐藏层的误差,同上
dA2 = np.dot(self.W3.T, dZ3)
dZ2 = dA2 * self.activation_derivative(self.Z2)
dW2 = np.dot(dZ2, self.A1.T) / m
db2 = np.sum(dZ2, axis=1, keepdims=True) / m
# 计算第一个隐藏层的误差,同上
dA1 = np.dot(self.W2.T, dZ2)
dZ1 = dA1 * self.activation_derivative(self.Z1)
dW1 = np.dot(dZ1, X.T) / m
db1 = np.sum(dZ1, axis=1, keepdims=True) / m
# 更新参数
self.W3 -= learning_rate * dW3
self.b3 -= learning_rate * db3
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
# 训练函数
def train(self, X, y, num_epochs, learning_rate):
for epoch in range(num_epochs):
X,y=shuffle(X,y)
# 前向传播
predictions = self.forward(X)
# 计算损失,均方误差
loss = np.mean((predictions - y)**2)
# 反向传播和参数更新
self.backward(X, y, learning_rate)
# 每隔100轮输出
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss}")
# 预测时的代码(单个数字的输入)
def predict(self,x):
predictions = self.forward(x)
return squeeze(squeeze(predictions))
# print(predictions)
# 保存权重为xxx.npz
def save_weights(self, path):
weights = {
'W1': self.W1,
'b1': self.b1,
'W2': self.W2,
'b2': self.b2,
'W3': self.W3,
'b3': self.b3
}
np.savez(path, **weights)
# 从xxx.npz加载权重
def load_weights(self, filepath):
weights = np.load(filepath)
self.W1 = weights['W1']
self.b1 = weights['b1']
self.W2 = weights['W2']
self.b2 = weights['b2']
self.W3 = weights['W3']
self.b3 = weights['b3']
if __name__ == '__main__':
# 生成 x 值
x = np.linspace(-3*np.pi, 3*np.pi, 100) # 在-3π到3π的区间内,生成100个点
# 生成 y 值
a,b,c,d = 0.8,0.5,1.2,0.3
y = a*np.sin(b*x) + c*np.cos(d*x)
# 添加一个维度,使其是矩阵的形式(1,n)
train_x=expand_dim(x)
train_y=expand_dim(y)
# 实例化线性回归模型
model = myNet(input_size=1, hidden1_size=32,hidden2_size=16, output_size=1)
# 训练模型
model.train(train_x, train_y, num_epochs=5000, learning_rate=0.1)
# 保存模型
model.save_weights('model_weights.npz')
# 加载模型
model.load_weights('model_weights.npz')
# 单个值预测
print(model.predict(0))
# 批量预测
test_x = expand_dim(x)
test_y = expand_dim(y)
predictions = model.forward(test_x)
pred_y=squeeze(predictions)
# 可视化
plt.plot(x, y) # 画出标准数据
plt.plot(x, pred_y) # 画出拟合得到的数据
# 添加标题和坐标轴标签
plt.xlabel("x")
plt.ylabel("y")
# 显示图形
plt.show()
3.结果
如图所示,当所有超参数如上述代码中所示时,拟合的效果还是不错的(蓝色是真实值,黄色是预测值)。为全面了解,还可更改超参数反复玩玩。
tips:当激活函数选择relu这种,learning_rate需要设特别低(0.0000001)否则很容易出现梯度爆炸。