手写全连接神经网络来拟合函数

一个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)否则很容易出现梯度爆炸。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值