关于直接使用numpy数组实现bp神经网络的问题

问题产生背景

当我打算直接使用numpy设计一个bp神经网络时,发现不仅运行时间长,而且准确率无法达到百分之八十以上(无论怎么去调整参数),我的代价函数使用的是交叉熵函数,隐藏层激活函数为sigmoid,输出层激活函数为softmax,训练集为MNIST,总共有一个隐藏层,而且为了简化问题我将MNIST数据集的文件转化为了只包含0和1的csv文件

问题相关代码,下面贴出我设计的代码

代码有点杂乱,实在是无力再去整理:

 """main.py"""
import numpy as np
from neural_network import ANN

if __name__ == "__main__":
    # 实例化
    n1 = 784
    n2 = 8
    n3 = 10
    my_ann = ANN(n1, n2, n3)
    # 载入学习数据
    my_ann.load_train_file("数据集/train.csv")
    # eta_func 表示学习率的调整函数,这里略去
    # 训练
    eta = 0.001     #学习率
    max_n = 100000   # 学习轮数
    appro = 0.00001   # 达到设定的梯度精度时退出
    batch_size = 20   # 分批处理
    circle, time = my_ann.train(max_n, appro, eta, batch_size, eta_func=None, SGD=False, initial=False)  
    # 测试网络
    rate = my_ann.test("数据集/test.csv")
    rate_train = my_ann.test("数据集/train.csv")
    print(f"测试集通过率为: {rate}\t训练集通过率为{rate_train}")
"""代价函数使用交叉熵,激活函数使用softmax"""
import numpy as np
import matplotlib.pyplot as plt
import time
import random


class ANN:
    def __init__(self, n1, n2, n3):
        # 神经网络每层的神经元数量
        self.n1 = n1
        self.n2 = n2
        self.n3 = n3
        # 权重
        self.w21 = None
        self.w32 = None
        # 输入
        self.input = []
        # 每层输出
        self.output2 = None
        self.output3 = None
        # 偏置
        self.b21 = None
        self.b32 = None
        # 加权输入
        self.z2 = None
        self.z3 = None
        # 神经单元偏差
        self.delta2 = None
        self.delta_l = None
        # 计算中间变量
        # cost_function
        self.cost = None
        self.cost_functions = []
        # 训练数据的大小
        self.data_num = None

    def load_train_file(self, train_filename):
        """
        预处理训练文件
        :param train_filename: 训练文件名称
        :return:
        """
        i = 0
        with open(train_filename) as f:
            for line in f:
                line = line.split(",")
                self.input.append(np.array(line))
                i += 1
        print("数据处理完成")
        self.data_num = i
        return self.input

    def train(self, max_n, approximate, eta=0.01, batch_size=None, eta_func=None, SGD=False, initial=True):
        """
        训练模型
        使用softmax激活函数搭配交叉熵cross_entropy代价函数
        :param initial: 是否进行参数初始化,False可以导入数据后接着训练
        :param SGD: 是否使用SGD算法(是否随机取样)
        :param eta_func: 学习率调整策略
        :param batch_size: 分批次
        :param max_n: 最大循环次数
        :param approximate: 精度
        :param eta: 学习率
        :return: 返回学习迭代次数,时间
        """
        # 分批次训练
        batch_index = 0
        # SGD算法
        start = time.perf_counter()
        batch_num = self.data_num // batch_size
        # 初始化参数值
        if initial:
            self.w21 = np.random.randn(self.n2, self.n1)*np.sqrt(2./self.n1)
            self.w32 = np.random.randn(self.n3, self.n2)*np.sqrt(2./self.n2)
            self.b21 = np.zeros(self.n2)
            self.b32 = np.zeros(self.n3)
        # 开始训练
        N = 1
        I = np.identity(10)
        nabala_max = np.inf
        while N <= max_n and nabala_max > approximate:
            # 预处理初始化梯度
            nabala_w21 = np.zeros((self.n2, self.n1))
            nabala_w32 = np.zeros((self.n3, self.n2))
            nabala_b21 = np.zeros(self.n2)
            nabala_b32 = np.zeros(self.n3)
            # SGD
            if SGD:
                batch = [random.choice(self.input) for _ in range(batch_size)]
            elif batch_size:
                batch = self.input[batch_index:(batch_index+1)*batch_size]
            else:
                batch = self.input
            # 正向顺序计算
            temple = np.array([])
            for x in batch:
                # 预处理
                real_datas = int(x[0])
                x = x.astype("int")[1:]
                # 计算每层输入输出
                self.z2 = self.w21 @ x + self.b21       
                self.output2 =1. / (1. - np.exp(self.z2))
                self.z3 = self.w32 @ self.output2 + self.b32
                max_z3 = np.max(self.z3)
                self.temple2 = np.exp(self.z3 - max_z3)
                temple2_sum = np.sum(self.temple2)
                self.output3 = self.temple2 / temple2_sum
                # 计算代价函数
                # I[:, real_datas]表示数据真实标签
                self.cost = -np.sum(I[:, real_datas] * np.log(self.output3), dtype="float64")  # 注意numpy数组中的特殊索引方法
                # 计算delta
                # 此计算公式参见https://www.jianshu.com/p/c02a1fbffad6多分类时计算稍稍复杂因为值和同层的其他单元也有关
                self.delta_l = self.output3 - I[:, real_datas]
                # 误差反向传播
                self.delta2 = self.delta_l @ self.w32 * self.output2*(1-self.output2)
                # 计算梯度
                nabala_w21 += np.outer(self.delta2, x)  # 使用外积来得到矩阵
                nabala_w32 += np.outer(self.delta_l, self.output2)
                nabala_b21 += self.delta2
                nabala_b32 += self.delta_l
                # 存储每幅图像的计算结果
                temple = np.append(temple, self.cost)
            # 将nabala合起来
            Nabala = np.concatenate([nabala_w21.flatten(), nabala_w32.flatten(), nabala_b32, nabala_b21]) / batch_size
            # 计算梯度误差
            nabala_norm = np.linalg.norm(Nabala)
            # 最大绝对梯度
            nabala_max = np.max(np.abs(Nabala))
            # 更新参数,梯度下降
            # 可以做单位化处理一下
            self.w32 += -eta * nabala_w32 / batch_size
            self.w21 += -eta * nabala_w21 / batch_size
            self.b21 += -eta * nabala_b21 / batch_size
            self.b32 += -eta * nabala_b32 / batch_size
            # 计算和
            # 代价函数
            cost_function = np.array(temple).mean
            self.cost_functions.append(cost_function)
            print(f"第{N}次迭代完成\t代价函数:{cost_function}\t梯度误差:{nabala_norm}\t最大梯度{nabala_max}\t当前学习率:{eta}")
            # 更新学习率
            # 这里的η是局部变量(形参中定义)
            if eta_func:
                eta = eta_func(N)
            N += 1
            # 普通批次更新
            batch_index += 1
        # 保存计算出来的数据
        np.savez('data', self.w21, self.w32, self.b21, self.b32)
        end = time.perf_counter()
        print(f"训练完成!  耗时: {end - start}\t迭代总次数为: {N-1}\n代价函数收敛于: {self.cost_functions[-1]}")
        return N, end - start

    def judge(self, input_data):
        """
        判断图片是什么数字
        :param input_data: ndarray: 输入图片数据
        :return: 数字, 相似度
        """
        # 计算每层输入输出
        # 这里就是train部分的向前传播代码,其实可以写个函数同构一下,这里省略
        max_probability = np.max(self.output3)
        r = np.where(self.output3 == max_probability)
        return int(r[0]), max_probability

    def test(self, test_filename):
        """
        测试神经网络
        :param test_filename: 测试数据文件名(csv)
        :return: 通过率
        """
        i = 1
        rate = 0
        with open(test_filename) as f:
            for line in f:
                flag = False
                r = int(line[0])
                line = line.split(",")
                x = line[1:]
                x = np.array(x).astype("int32")
                number, p = self.judge(x)
                if number == r:
                    rate += 1
                    flag = True
                print(f"测试第{i}条数据: 预测值:{number}\t真实值:{r}\t{flag}")
                i += 1
        return rate / (i - 1)
运行结果及报错内容

运行起来非常慢,而且准确率不高

我的解答思路和尝试过的方法

贴上来的代码的算法细节已经经历过多次修改,使用tf不需要使用更先进的算法就能轻松达到90%正确率,可这个我无论如何去改变(包括将激活函数全部使用sigmoid,loss使用均方和计算)都对结果影响不大

我想要达到的结果

计算效率提高,最重要的就是提高准确率,至少要到百分之九十。
所以试问一下代码部分是哪里出了问题

你好,我是有问必答小助手,非常抱歉,本次您提出的有问必答问题,技术专家团超时未为您做出解答


本次提问扣除的有问必答次数,将会以问答VIP体验卡(1次有问必答机会、商城购买实体图书享受95折优惠)的形式为您补发到账户。


因为有问必答VIP体验卡有效期仅有1天,您在需要使用的时候【私信】联系我,我会为您补发。

你好,我是有问必答小助手,非常抱歉,本次您提出的有问必答问题,技术专家团超时未为您做出解答


本次提问扣除的有问必答次数,将会以问答VIP体验卡(1次有问必答机会、商城购买实体图书享受95折优惠)的形式为您补发到账户。


因为有问必答VIP体验卡有效期仅有1天,您在需要使用的时候【私信】联系我,我会为您补发。