纯手写版BP神经网络实战之Python实现

1、理论来源

       这篇文章是我觉得讲解BP神经网络比较透彻的文章,想要入门的朋友可以看一看,传送门
       该文章有实例、有公式讲解,很不错。而这次手写BP神经网络是博主个人的兴趣和学习需要,不调用任何深度学习架构实现神经网络的代码在网上非常少而我并没有参考任何网上的代码,并已反复对代码进行核验。这个全连接网络可以用在CNN的全连接层,同样也是基于此,想着手写一次BP神经网络。一年过去了,希望能在DL领域有新的发现和突破。
       提示:博文我前后更新了四五次,大改的时候标注了更改日期,这也是我的一个学习和思考历程。如果你对网络没有有比较深的了解,不妨看到最后~或者直接点击目录跳转。

2、代码

       实现层面上,代码里面我会有注释,基本依赖于numpy库来实现矩阵的操作,这也是手写BP神经网络的难点之一。其次,权重一定要初始化,而且不能都相等。比方说我直接np.ones() 初始化权重全部为1 ,最后面的网络输出一定是一样的,这是错误的做法。
       理论层面上,难点在于对误差反向传播的理解和推导。一定要记住每个神经元都有一个输入和输出,这个在一轮迭代中要放在一个二维矩阵中保存,下一轮迭代再清空数组。而且每一个误差项都是针对于当前这个神经元。
       ps:本篇文章的代码仅用于学习,不得转载(by_Neuer_桓)

# by_Neuer_桓 2021-06-27
import math
import numpy as np

# by_Neuer_桓 2021-06-27
def initial_bp_neural_network(n_inputs, n_hidden_layers, n_outputs):
    # hidden_layers 是一个列表,表示第n层隐含层神经元有几个
    global hidden_layers_weights
    global hidden_layers_bias
    # 一定要有隐含层, np.ones((行,列))
    # 每个神经元对应着一个偏置
    if len(n_hidden_layers) > 0:
        hidden_layers_weights.append(np.random.rand(n_hidden_layers[0], n_inputs) * 0.01)
        for i in range(len(n_hidden_layers) - 1):
            hidden_layers_weights.append(np.random.rand(n_hidden_layers[i + 1], n_hidden_layers[i]) * 0.01)
        for i in n_hidden_layers:
            hidden_layers_bias.append(np.zeros(i))
        hidden_layers_weights.append(np.random.rand(n_outputs, n_hidden_layers[len(n_hidden_layers) - 1]) * 0.01)
        hidden_layers_bias.append(np.zeros(n_outputs))
    # print(hidden_layers_weights)
    return hidden_layers_weights

# by_Neuer_桓 2021-06-27
def forward_propagate(inputs):
    global train_outputs
    global train_inputs
    train_inputs = []
    train_outputs = []
    function_vector = np.vectorize(logistic)
    for i in range(len(hidden_layers_weights)):
        if i == 0:
            outputs = np.array(hidden_layers_weights[i]).dot(test_inputs)
        else:
            outputs = np.array(hidden_layers_weights[i]).dot(inputs)
        outputs = np.array(outputs) + np.array(hidden_layers_bias[i])
        train_inputs.append(outputs)
        outputs = function_vector(outputs)
        # 把每一层的输出都记录进去
        train_outputs.append(outputs)
        inputs = outputs.copy()
        # print(inputs)
    return train_outputs

# by_Neuer_桓 2021-06-27
def backward_error_propagate():
    global error
    error = []
    function_vector = np.vectorize(logistic_derivative)
    for i in range(len(train_outputs)):
        if i == 0:
            # 一个是对均方误差求导数,一个是对激活函数求导数
            error.append(error_function_derivative(test_outputs, train_outputs[len(train_outputs) - 1]) *
                         function_vector(np.array(train_inputs[len(train_inputs) - 1])))
        else:
            # 激活函数求导,直接点乘
            # print(function_vector(np.array(train_inputs[len(train_inputs) - 1 - i])))
            # 一个神经元对应的权重的误差求和
            error.append(function_vector(np.array(train_inputs[len(train_inputs) - 1 - i])) *
                         hidden_layers_weights[len(train_outputs) - i].T.dot(error[i - 1]))

# by_Neuer_桓 2021-06-27
def update():
    # 更新权重
    for i in range(len(hidden_layers_weights)):
        if i == 0:
            hidden_layers_weights[i] = hidden_layers_weights[i] - learning_rate * (
                    error[len(error) - i - 1].reshape(len(error[len(error) - i - 1]), 1) *
                    np.array(test_inputs).reshape(1, len(test_inputs)) + hidden_layers_weights[i])
        else:
            hidden_layers_weights[i] = hidden_layers_weights[i] - learning_rate * (
                    error[len(error) - i - 1].reshape(len(error[len(error) - i - 1]), 1) *
                    np.array(train_inputs[i - 1]).reshape(1, len(train_inputs[i - 1])) + hidden_layers_weights[i])
    for i in range(len(hidden_layers_bias)):
        hidden_layers_bias[i] = hidden_layers_bias[i] - learning_rate * (error[len(error) - i - 1])

# by_Neuer_桓 2021-06-27
# 均方误差函数
def error_function(actual_outputs, predict_outputs):
    function_vector = np.vectorize(pow)
    return 1 / 2 * (sum(function_vector(np.array(actual_outputs) - np.array(predict_outputs), 2)))


# 均方误差函数的导数,针对单个神经元,我认为均方误差本应该是多个值拟合,但是输出有多个神经元的情况下,针对每个神经元的输出结果只能做出相应的单个误差判断
# 这个顺序一定不能颠倒
def error_function_derivative(actual_outputs, predict_outputs):
    return np.array(predict_outputs)-np.array(actual_outputs)


# 激活函数
def logistic(x):
    return 1 / (1 + math.exp(-x))

# by_Neuer_桓 2021-06-27
# 激活函数logistic sigmoid函数的导数
def logistic_derivative(x):
    return logistic(x) * (1 - logistic(x))


# 判断是否达到收敛或者是和预期结果一样,精度可以自己调试
def judge():
    for i in range(len(train_outputs[len(train_outputs) - 1])):
        if abs(train_outputs[len(train_outputs) - 1][i] - test_outputs[i]) > 0.00001:
            return 0
    return 1

# by_Neuer_桓 2021-06-27
def train_network(iteration):
    for i in range(iteration):
        forward_propagate(test_inputs)
        backward_error_propagate()
        update()
        print('第' + str(i) + '次迭代结果')
        print(train_outputs[len(train_outputs) - 1])
        if judge() == 1:
            print('第' + str(i) + '次迭代达到收敛')
            break


if __name__ == '__main__':
    learning_rate = 0.1
    hidden_layers_weights = []
    hidden_layers_bias = []
    train_outputs = []
    train_inputs = []
    # 误差项
    error = []
    test_inputs = [1, 2]
    test_outputs = [0.5, 0.8, 0.7]
    initial_bp_neural_network(2, [3, 2], 3)
    train_network(7000)

3、运行结果

运行结果
       又是一年毕业季~祝学长学姐们毕业快乐,一切顺利!也祝大家学有所成!
——————————————————————————————————————————————————————————

4、更新(2021年8月3日)

       鉴于上一次手写的代码对多分类问题的学习能力存在的问题和做出的改进,我将进行如下的说明。上一个问题的场景是我随便写的,回归用于预测数值(用两个数预测三个数),没有实际应用场景也就是测试集,训练轮数过多会造成过拟合。感兴趣的朋友也可以对比一下两份代码的区别,首先我先贴出更新后的源码(基于上面我自己手写的代码的改进),然后再用数据说明一下:

# 手写BP神经网络
import math
import numpy as np
import csv
from sklearn import preprocessing
import xlsxwriter


def initial_bp_neural_network(n_inputs, n_hidden_layers, n_outputs):
    # hidden_layers 是一个列表,表示第n层隐含层神经元有几个
    global hidden_layers_weights
    global hidden_layers_bias
    hidden_layers_weights = []
    hidden_layers_bias = []
    # 一定要有隐含层, np.ones((行,列))
    # 每个神经元对应着一个偏置
    if len(n_hidden_layers) > 0:
        hidden_layers_weights.append(np.random.randn(n_hidden_layers[0], n_inputs) * 0.01)
        # hidden_layers_weights.append(np.ones((n_hidden_layers[0], n_inputs)))
        for i in range(len(n_hidden_layers) - 1):
            hidden_layers_weights.append(np.random.rand(n_hidden_layers[i + 1], n_hidden_layers[i]) * 0.01)
            # hidden_layers_weights.append(np.ones((n_hidden_layers[i + 1], n_hidden_layers[i])))
        for i in n_hidden_layers:
            hidden_layers_bias.append(np.zeros(i))
            # hidden_layers_bias.append(np.random.rand(i) / math.sqrt(i))
        hidden_layers_weights.append(np.random.rand(n_outputs, n_hidden_layers[len(n_hidden_layers) - 1]) * 0.01)
        # hidden_layers_weights.append(np.ones((n_outputs, n_hidden_layers[len(n_hidden_layers) - 1])))
        hidden_layers_bias.append(np.zeros(n_outputs))
        # hidden_layers_bias.append(np.random.rand(n_outputs) / math.sqrt(n_outputs))
    # print(hidden_layers_bias)
    # print(hidden_layers_weights)
    return hidden_layers_weights


def forward_propagate(inputs):
    global hidden_layers_weights
    global hidden_layers_bias
    global train_outputs
    global train_inputs
    train_inputs = []
    train_outputs = []
    function_vector = np.vectorize(logistic)
    for i in range(len(hidden_layers_weights)):
        outputs = np.array(hidden_layers_weights[i]).dot(inputs)
        outputs = np.array(outputs) + np.array(hidden_layers_bias[i])
        outputs = np.array(batch_normalization(outputs))
        train_inputs.append(outputs)
        outputs = function_vector(outputs)
        # 把每一层的输出都记录进去
        train_outputs.append(outputs)
        inputs = outputs.copy()
    return train_outputs


def backward_error_propagate(outputs):
    global error
    # error 是倒着来存放的
    error = []
    function_vector = np.vectorize(logistic_derivative)
    # train_outputs 包含了每一层的输出,最后一个才是最终的结果
    for i in range(len(train_outputs)):
        if i == 0:
            # 一个是对均方误差求导数,一个是对激活函数求导数
            # error.append(error_function_derivative(outputs, train_outputs[len(train_outputs) - 1]) *
            #              function_vector(np.array(train_inputs[len(train_inputs) - 1])))
            error.append(error_function_derivative(outputs, soft_max()) *
                         function_vector(np.array(train_inputs[len(train_inputs) - 1])))
        else:
            # 激活函数求导,直接点乘
            # print(function_vector(np.array(train_inputs[len(train_inputs) - 1 - i])))
            # 一个神经元对应的权重的误差求和
            error.append(function_vector(np.array(train_inputs[len(train_inputs) - 1 - i])) *
                         hidden_layers_weights[len(train_outputs) - i].T.dot(error[i - 1]))


def update(inputs):
    global hidden_layers_weights
    global hidden_layers_bias
    global error
    global train_inputs
    # 更新权重
    for i in range(len(hidden_layers_weights)):
        if i == 0:
            hidden_layers_weights[i] = hidden_layers_weights[i] - learning_rate * (
                    error[len(error) - i - 1].reshape(len(error[len(error) - i - 1]), 1) *
                    np.array(inputs).reshape(1, len(inputs)) + np.array(hidden_layers_weights[i]))
        else:
            hidden_layers_weights[i] = hidden_layers_weights[i] - learning_rate * (
                    error[len(error) - i - 1].reshape(len(error[len(error) - i - 1]), 1) *
                    np.array(train_outputs[i - 1]).reshape(1, len(train_outputs[i - 1])) +
                    np.array(hidden_layers_weights[i]))
            # print('error:')
            # print(learning_rate * (
            #         error[len(error) - i - 1].reshape(len(error[len(error) - i - 1]), 1) *
            #         np.array(train_inputs[i - 1]).reshape(1, len(train_inputs[i - 1])) + hidden_layers_weights[i]))
            # print('hidden_layer_weight:')
            # print(hidden_layers_weights[i])
    for i in range(len(hidden_layers_bias)):
        hidden_layers_bias[i] = hidden_layers_bias[i] - learning_rate * (error[len(error) - i - 1])


# 均方误差函数
def error_function(actual_outputs, predict_outputs):
    function_vector = np.vectorize(pow)
    return 1 / 2 * (sum(function_vector(np.array(actual_outputs) - np.array(predict_outputs), 2)))


# 均方误差函数的导数,针对单个神经元,我认为均方误差本应该是多个值拟合,但是输出有多个神经元的情况下,针对每个神经元的输出结果只能做出相应的单个误差判断
# 这个顺序一定不能颠倒
def error_function_derivative(actual_outputs, predict_outputs):
    return np.array(predict_outputs) - np.array(actual_outputs)


# 激活函数
def logistic(x):
    return 1.0 / (1.0 + math.exp(-x))


# 激活函数logistic sigmoid函数的导数
def logistic_derivative(x):
    return logistic(x) * (1 - logistic(x))


# 预测判断,判断是否达到收敛或者是和预期结果一样,精度可以自己调试
def judge_predict():
    for i in range(len(train_outputs[len(train_outputs) - 1])):
        if abs(train_outputs[len(train_outputs) - 1][i] - test_outputs[i]) > 0.00001:
            return 0
    return 1


# 分类判断,输出精确度
def judge_classification(label):
    max_value = max(soft_max())
    max_index = soft_max().index(max_value)
    # print(label)
    # print(max_index+1)
    # print(soft_max())
    if max_index + 1 == label:
        # return 1
        return label
    return 0


# 将输出的数值转换为概率
def soft_max():
    return [math.exp(train_outputs[len(train_outputs) - 1][0]) / (math.exp(train_outputs[len(train_outputs) - 1][0]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][1]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][2])),
            math.exp(train_outputs[len(train_outputs) - 1][1]) / (math.exp(train_outputs[len(train_outputs) - 1][0]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][1]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][2])),
            math.exp(train_outputs[len(train_outputs) - 1][2]) / (math.exp(train_outputs[len(train_outputs) - 1][0]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][1]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][2]))]


# 训练网络
def train_network(epoch):
    global data
    global test_inputs
    global test_outputs
    with open('wheat-seeds.csv', 'r') as file:
        reader = csv.reader(file)
        column = [row for row in reader]
        # 乱序排列
        data = np.array(column).astype(float)

    # batch_size
    batch_size = 30
    iteration = 7

    #
    workbook = xlsxwriter.Workbook('data.xlsx')
    worksheet = workbook.add_worksheet()

    for _epoch in range(epoch):
        # 获取乱序, 让每次迭代都不一样
        if _epoch != 1:
            np.random.shuffle(data)

        # 获取输入
        test_inputs = [row[0:7] for row in data]
        test_inputs = min_max_scaler.fit_transform(test_inputs)

        # 获取输出
        test_outputs = [row[7] for row in data]
        # 训练一个batch就是一次iteration
        for i in range(iteration):
            n1 = 0
            n2 = 0
            n3 = 0
            # 一次迭代更新网络的参数
            # len(test_outputs)
            for j in range(batch_size):
                forward_propagate(test_inputs[j + i * batch_size])
                # 使用独热编码
                if test_outputs[j + i * batch_size] == 1:
                    backward_error_propagate(np.array([1, 0, 0]))
                elif test_outputs[j + i * batch_size] == 2:
                    backward_error_propagate(np.array([0, 1, 0]))
                else:
                    backward_error_propagate(np.array([0, 0, 1]))
                update(test_inputs[j + i * batch_size])
            # print("迭代完成")
            # 使用训练好的网络来得到测试的结果

        print('--------开始测试集的工作--------')
        # for j in range(len(test_inputs)):
        #     forward_propagate(test_inputs[j])
        #     print(soft_max())
        for j in range(len(test_outputs)):
            forward_propagate(test_inputs[j])
            # print(judge_classification(test_outputs[j]))
            if judge_classification(test_outputs[j]) == 1:
                # n = n + 1
                n1 = n1 + 1
            elif judge_classification(test_outputs[j]) == 2:
                n2 = n2 + 1
            elif judge_classification(test_outputs[j]) == 3:
                n3 = n3 + 1
        # acc = n / len(test_outputs)
        acc1 = n1 / 70
        acc2 = n2 / 70
        acc3 = n3 / 70
        worksheet.write(_epoch, 0, acc1)
        worksheet.write(_epoch, 1, acc2)
        worksheet.write(_epoch, 2, acc3)
        print('第' + str(_epoch) + "次迭代")
        print('准确率为:')
        print(acc1)
        print(acc2)
        print(acc3)
    workbook.close()


def batch_normalization(outputs):
    output = []
    # 求均值
    arr_mean = np.mean(outputs)
    # 求方差
    arr_var = np.var(outputs)
    for i in outputs:
        output.append((i-arr_mean)/(math.pow(arr_var, 0.5)))
    return output


if __name__ == '__main__':
    learning_rate = 0.0005
    hidden_layers_weights = []
    hidden_layers_bias = []
    train_outputs = []
    train_inputs = []
    # 误差项
    error = []
    test_inputs = []
    test_outputs = []
    min_max_scaler = preprocessing.MinMaxScaler()
    data = []

    # 这是一个三分类的问题,一共有三个标签
    initial_bp_neural_network(7, [11], 3)
    train_network(100)

5、模型改进及数据集

       之所以进行改进,是因为我发现之前的模型因为模型参数设置的不合理,很容易陷入局部最优,模型学习能力极低;所以,我进行了如下的改进(提高模型的学习能力),并在wheat-seeds.csv数据集上有了很好的效果:
在这里插入图片描述
       其中数据集链接,传送门在这里插入图片描述

6、实验结果

       针对不同的小麦品种,经过n次迭代,以准确率为评价指标。与2010年发表的论文 Complete Gradient Clustering Algorithm for Features Analysis of X-ray Images 进行对比,该论文用聚类的方法对小麦品种进行分类,使用我的网络,在学习率设置为0.0005时,在对Rosa和Canadian达到超过95%的准确率,特别是Rosa类有较大的提升。
       用网络进行100次迭代,可视化三类品种的分类精度趋势变化图,网络在第62次迭代达到最佳:
精度趋势图

小麦种类KamaRoseCanadian
BP神经网络训练86%97%90%
论文中的聚类方法96%84%96%

——————————————————————————————————————————————————————

7、更新(2021年10月13日)

通过近期的学习和反思,对上一次的代码进行了更新。

7.1 针对Batch、Iteration和Epoch

       针对Iteration和Epoch,其中Epoch > Iteration

名词定义
Epoch使用训练集的全部数据对模型进行一次完整的训练,是“一代训练
Iteration使用一个Batch数据对模型进行一次参数更新的过程,是“一次训练
Batch使用训练集中的一小部分样本对模型权重进行一次反向传播的参数更新,这一小部分样本被称为“一批数据
       以上参考自:训练神经网络中最基本的三个概念:Epoch, Batch, Iteration

7.2 针对损失函数

       对回归分类问题,应该选择不同的损失函数。但是,在我上一份代码中,我使用了MSE均方误差函数用来解决一个三分类的问题,取得了不错的效果。但是严谨来说,不应该在分类问题中使用MSE,而应该使用BCE(解决二分类问题)/CE(解决多分类问题)的CrossEntropyLoss交叉熵损失函数。其中 y i y_i yi一般都经过softmax函数用独热编码表示。 L = ∑ i = 1 n y i l n p i L=\sum_{i=1}^ny_ilnp_i L=i=1nyilnpi
       在上面写的代码中,我的想法是在最终的输出经过 s o f t m a x softmax softmax函数,比较哪一个标签出现的概率大,将 [ p 1 , p 2 , p 3 ] [p_1,p_2,p_3] [p1p2p3] p 1 + p 2 + p 3 = 1 p_1+p_2+p_3=1 p1+p2+p3=1)转换为独热编码,进行 A c c Acc Acc准确率的计算。而一维的概率向量将与真实的独热标签进行比较。

7.3 针对神经网络权重初始化的方式

       在上一份代码我在中间层使用了logistic(最经典的sigmoid函数)来处理分类问题,因为我的思路还是基于回归的方法。在最后一层使用了softmax函数作为输出,但是中间层可以考虑用ReLU等函数。而初始化权重的时候,为了防止w过于大而使得输出接近于1(导致梯度消失),我使用的是随机数乘0.01。
       在实验中,我还使用了tanh函数(但是效果不佳),对该激励函数的权重初始化应该使用Xavier初始化,即 /np.sqrt(上一层的输入神经元个数)。
——————————————————————————————————————————————————————

8、更新(2021年12月12日)

       通过近期的反思,对上一次的代码进行了更新。趁着年末修改一下。

8.1 代码

       首先我贴出更新后的代码,接着讲解一下:

# 手写BP神经网络
import math
import numpy as np
import csv
from sklearn import preprocessing
import xlsxwriter
import matplotlib.pyplot as plt


def initial_bp_neural_network(n_inputs, n_hidden_layers, n_outputs):
    # hidden_layers 是一个列表,表示第n层隐含层神经元有几个
    global hidden_layers_weights
    global hidden_layers_bias
    hidden_layers_weights = []
    hidden_layers_bias = []
    # 一定要有隐含层, np.ones((行,列));每个神经元对应着一个偏置
    if len(n_hidden_layers) > 0:
        hidden_layers_weights.append(np.random.randn(n_hidden_layers[0], n_inputs) * 0.01)
        for i in range(len(n_hidden_layers) - 1):
            hidden_layers_weights.append(np.random.rand(n_hidden_layers[i + 1], n_hidden_layers[i]) * 0.01)
        for i in n_hidden_layers:
            hidden_layers_bias.append(np.zeros(i))
        hidden_layers_weights.append(np.random.rand(n_outputs, n_hidden_layers[len(n_hidden_layers) - 1]) * 0.01)
        hidden_layers_bias.append(np.zeros(n_outputs))
    return hidden_layers_weights


def forward_propagate(inputs):
    global hidden_layers_weights
    global hidden_layers_bias
    global train_outputs
    global train_inputs
    train_inputs = []
    train_outputs = []
    function_vector = np.vectorize(Leaky_ReLu)
    for i in range(len(hidden_layers_weights)):
        outputs = np.array(hidden_layers_weights[i]).dot(inputs)
        outputs = np.array(outputs) + np.array(hidden_layers_bias[i])
        outputs = np.array(batch_normalization(outputs))
        train_inputs.append(outputs)
        outputs = function_vector(outputs)
        # 把每一层的输出都记录进去
        train_outputs.append(outputs)
        inputs = outputs.copy()
    return train_outputs


def backward_error_propagate(outputs):
    global error
    global loss_average
    # error 是倒着来存放的
    error = []
    function_vector = np.vectorize(Leaky_ReLu_derivative)
    # train_outputs 包含了每一层的输出,最后一个才是最终的结果
    for i in range(len(train_outputs)):
        # 一个是对损失函数(基于softmax的交叉熵函数)求导数,一个是对激活函数求导数
        if i == 0:
            error.append(error_function_derivative(outputs, soft_max()) *
                         function_vector(np.array(train_inputs[len(train_inputs) - 1])))
            loss_average.append(error_function(outputs, soft_max()))
        else:
            # 激活函数求导,直接点乘;一个神经元对应的权重的误差求和
            error.append(function_vector(np.array(train_inputs[len(train_inputs) - 1 - i])) *
                         hidden_layers_weights[len(train_outputs) - i].T.dot(error[i - 1]))


def update(inputs):
    global hidden_layers_weights
    global hidden_layers_bias
    global error
    global train_inputs

    # 更新权重
    for i in range(len(hidden_layers_weights)):
        if i == 0:
            hidden_layers_weights[i] = hidden_layers_weights[i] - learning_rate * (
                    error[len(error) - i - 1].reshape(len(error[len(error) - i - 1]), 1) *
                    np.array(inputs).reshape(1, len(inputs)) + np.array(hidden_layers_weights[i]))
        else:
            hidden_layers_weights[i] = hidden_layers_weights[i] - learning_rate * (
                    error[len(error) - i - 1].reshape(len(error[len(error) - i - 1]), 1) *
                    np.array(train_outputs[i - 1]).reshape(1, len(train_outputs[i - 1])) +
                    np.array(hidden_layers_weights[i]))
    for i in range(len(hidden_layers_bias)):
        hidden_layers_bias[i] = hidden_layers_bias[i] - learning_rate * (error[len(error) - i - 1])


# 交叉熵函数作为损失函数
def error_function(actual_outputs, predict_outputs):
    function_vector = np.vectorize(math.log)
    return -sum(actual_outputs * function_vector(predict_outputs))


# 交叉熵函数的导数,针对输出层所有神经元
# 这个顺序一定不能颠倒
def error_function_derivative(actual_outputs, predict_outputs):
    return predict_outputs - actual_outputs


# 激活函数 Leaky_ReLu
def Leaky_ReLu(x):
    if x > 0:
        return x
    return 0.01 * x


# 激活函数Leaky_ReLu函数的导数
def Leaky_ReLu_derivative(x):
    if x > 0:
        return 1
    return 0.01


# 预测判断,判断是否达到收敛或者是和预期结果一样,精度可以自己调试
def judge_predict():
    for i in range(len(train_outputs[len(train_outputs) - 1])):
        if abs(train_outputs[len(train_outputs) - 1][i] - test_outputs[i]) > 0.00001:
            return 0
    return 1


# 分类判断,输出精确度
def judge_classification(label):
    max_value = max(soft_max())
    max_index = soft_max().index(max_value)
    if max_index + 1 == label:
        # return 1
        return label
    return 0


# 将输出的数值转换为概率
def soft_max():
    return [math.exp(train_outputs[len(train_outputs) - 1][0]) / (math.exp(train_outputs[len(train_outputs) - 1][0]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][1]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][2])),
            math.exp(train_outputs[len(train_outputs) - 1][1]) / (math.exp(train_outputs[len(train_outputs) - 1][0]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][1]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][2])),
            math.exp(train_outputs[len(train_outputs) - 1][2]) / (math.exp(train_outputs[len(train_outputs) - 1][0]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][1]) +
                                                                  math.exp(train_outputs[len(train_outputs) - 1][2]))]


# 训练网络
def train_network(epoch):
    global data
    global test_inputs
    global test_outputs
    global loss_average, loss_average_plot
    with open('wheat-seeds.csv', 'r') as file:
        reader = csv.reader(file)
        column = [row for row in reader]
        # 乱序排列
        data = np.array(column).astype(float)

    # batch_size
    batch_size = 30
    iteration = 7

    # 从excel中读取数据
    workbook = xlsxwriter.Workbook('data.xlsx')
    worksheet = workbook.add_worksheet()

    for _epoch in range(epoch):
        # 获取乱序, 让每次迭代都不一样
        if _epoch != 1:
            np.random.shuffle(data)

        # 获取输入
        test_inputs = [row[0:7] for row in data]
        test_inputs = min_max_scaler.fit_transform(test_inputs)

        # 获取输出
        test_outputs = [row[7] for row in data]
        # 训练一个batch就是一次iteration
        for i in range(iteration):
            n1 = 0
            n2 = 0
            n3 = 0
            # 一次迭代更新网络的参数
            for j in range(batch_size):
                forward_propagate(test_inputs[j + i * batch_size])
                # 使用独热编码
                if test_outputs[j + i * batch_size] == 1:
                    backward_error_propagate(np.array([1, 0, 0]))
                elif test_outputs[j + i * batch_size] == 2:
                    backward_error_propagate(np.array([0, 1, 0]))
                else:
                    backward_error_propagate(np.array([0, 0, 1]))
                update(test_inputs[j + i * batch_size])

        # 使用训练好的网络来得到测试的结果
        print('--------开始测试集的工作--------')
        for j in range(len(test_outputs)):
            forward_propagate(test_inputs[j])
            # print(judge_classification(test_outputs[j]))
            if judge_classification(test_outputs[j]) == 1:
                # n = n + 1
                n1 = n1 + 1
            elif judge_classification(test_outputs[j]) == 2:
                n2 = n2 + 1
            elif judge_classification(test_outputs[j]) == 3:
                n3 = n3 + 1
        # acc = n / len(test_outputs)
        acc1 = n1 / 70
        acc2 = n2 / 70
        acc3 = n3 / 70
        worksheet.write(_epoch, 0, acc1)
        worksheet.write(_epoch, 1, acc2)
        worksheet.write(_epoch, 2, acc3)
        print('第' + str(_epoch) + "次迭代")
        print('准确率为:')
        print(acc1)
        print(acc2)
        print(acc3)
        loss_average_plot.append(np.sum(np.array(loss_average)) / (iteration * batch_size))
        loss_average = []
    loss_plot()
    workbook.close()


def loss_plot():
    global loss_average_plot
    fig, ax = plt.subplots()
    # 绘制线条粗细
    ax.plot(loss_average_plot, linewidth=3)
    # 设置图表标题并给坐标轴加上标签
    ax.set_title("Loss Average by an Iteration", fontsize=24)
    ax.set_xlabel("Epoch", fontsize=14)
    ax.set_ylabel("Loss Average", fontsize=14)
    plt.xticks(range(0, len(loss_average_plot)))
    plt.show()


def batch_normalization(outputs):
    output = []
    # 求均值
    arr_mean = np.mean(outputs)
    # 求方差
    arr_var = np.var(outputs)
    for i in outputs:
        output.append((i-arr_mean)/(math.pow(arr_var, 0.5)))
    return output


if __name__ == '__main__':
    learning_rate = 0.0001
    hidden_layers_weights = []
    hidden_layers_bias = []
    train_outputs = []
    train_inputs = []
    loss_average = []
    loss_average_plot = []
    # 误差项
    error = []
    test_inputs = []
    test_outputs = []
    min_max_scaler = preprocessing.MinMaxScaler()
    data = []

    # 这是一个三分类的问题,一共有三个标签
    initial_bp_neural_network(7, [11], 3)
    train_network(10)

8.2 结果

       细心的朋友们看到上文用网络进行100次迭代,可视化三类品种的分类精度趋势变化图的时候,会发现精度并没有像想象的那样:最后三个类别都接近于1。也就是精度最后没有收敛,或者是精度消失了。为此,为了展示一下损失,我更新代码的时候特意绘制了Loss曲线。
Loss

8.3 总结

1、首先我改变了最后的损失函数,我使用了基于softmax的交叉熵函数,而不是用均方误差函数。上文中我也有提到。由于个人原因,就不打公式了,我提供一个b站的讲解给大家理解一下交叉熵函数的导数:交叉熵softmax求导简单解释
2、然后在激励函数上我舍弃了logistic sigmoid函数(因为不是回归问题),而采用了Leaky_ReLu函数,提高了收敛的效率,其中 α \alpha α是我自己设置的,尽量接近0。
3、因为这是一个简单的分类问题,数据量小。所以网络结构三层足矣,而且epoch不能太多,否则极有可能造成过拟合和有关梯度的问题(越训练越差了,Loss不收敛,震荡严重)。
4、在这篇文章中严格意义来说batch size还是等于1,我这样写是方便大家理解,希望大家明白。补充:如果输入层是三个神经元,意味着每个样本有三个特征,如果batch_size为10,如果一次性喂入神经网络,输入应该变为tensor(10,3)保证网络结构不变,其中batch一般是张量的第一维。用pytoch实现是很方便的。
5、在梯度下降中我认为我用的SGD,严谨来说,感觉是广义的随机梯度下降。因为我没有随机选择梯度,因为数据量实在太小,而是对每个神经元的输入输出进行求导。

       因为不断会用到神经网络,有时候闲下来就在想这篇文章有没有什么问题可能会误导大家,所以这篇文章反反复复更新、修改了多次,最近应该不会再更新了,希望大家理解。可能还是存在一些问题,也希望大家可以指正。最后的最后,希望看到这个教程的你能够学有所成,谢谢。

  • 39
    点赞
  • 235
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 32
    评论
评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TerryBlog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值