python学智能算法(六)|神经网络算法:BP神经网络算法入门

【1】引言

前序学习过程中,已经探索了模拟退火算法、遗传算法和差分进化算法,相关文章链接为:

python学智能算法(一)|模拟退火算法:原理解释和最小值求解_模拟退火算法python-CSDN博客

python学智能算法(二)|模拟退火算法:进阶分析-CSDN博客

python学智能算法(三)|模拟退火算法:深层分析-CSDN博客

python学智能算法(四)|遗传算法:原理认识和极大值分析-CSDN博客

python学智能算法(五)|差分进化算法:原理认识和极小值分析-CSDN博客

神经网络算法和这几个算法虽然同属于智能算法,但原理上已经关联不大。

粗略概括,模拟退火算法是简单的变异造作;遗传算法叠加了选择、交叉和变异三种操作;差分进化算法则是先变异、再交叉,最后选择。这三种智能算法在学习上具有一定的类似性,实际应用中,对于求既有数据的极值都有大显身手的地方,学习上适合放到一起。

神经网络算法,具体到BP神经网络算法,和上述三种智能算法的一个重大区别是:BP神经网络算法还没有发现具体的目标函数,运行这个算法的过程,一定程度上也是找目标函数的过程,或者说散点拟合的过程。

  • 声明:上述说法仅适用于初步学习阶段,仅代表个人理解。

在更早地学习中,我们也对神经网络进行了初步探索,相关文章链接为:

神经网络|(一)加权平均法,感知机和神经元-CSDN博客

神经网络|(二)sigmoid神经元函数_segmod函数-CSDN博客

神经网络|(三)线性回归基础知识-CSDN博客

在此基础上,今天就学习一个简单的BP神经网络算法入门python程序,感受一下BP神经网络算法的运行过程。

【2】代码分析

【2.1】模块引入

首先引入必要的模块:

import numpy as np #引入numpy模块
import matplotlib.pyplot as plt #引入matplotlib模块

【2.2】主函数

然后直接来看主函数:

# 主函数
def main():
    # 生成数据
    num_samples = 100
    X, Y = generate_data(num_samples)

    # 训练模型
    hidden_size = 10
    num_iterations = 1000
    learning_rate = 0.1
    # 训练参数
    W1, b1, W2, b2, costs = train_model(X, Y, hidden_size, num_iterations, learning_rate)

    # 生成测试数据
    x_test = np.linspace(0, 2 * np.pi, 200).reshape(-1, 1)
    y_test = np.sin(x_test)

    # 进行预测
    # 预测只在往前传递的运算中执行
    _, _, _, y_pred = forward_propagation(x_test, W1, b1, W2, b2)

    # 绘制结果
    plt.figure(figsize=(12, 6))

    plt.subplot(1, 2, 1)
    plt.scatter(X, Y, label='Training Data', color='blue')
    plt.plot(x_test, y_test, label='True Function', color='green')
    plt.plot(x_test, y_pred, label='Predicted Function', color='red')
    plt.legend()
    plt.title('BP Neural Network')
    plt.xlabel('x')
    plt.ylabel('y')

    plt.subplot(1, 2, 2)
    plt.plot(np.arange(0, num_iterations, 100), costs)
    plt.title('Cost over iterations')
    plt.xlabel('Iterations')
    plt.ylabel('Cost')

    plt.show()

if __name__ == "__main__":
    main()

这里直接看代码构成:主函数定义后,调用了一个子函数generate_data()生成数据,然后调用train_model()来训练数据,然后生成了一堆测试数据,把测试数据代入了forward_propagation()函数进行验证。

主函数构成的逻辑非常通畅,为此,转向对子函数的追溯。

【2.3】子函数

【2.3.1】generate_data()函数生成数据

# 生成带有噪声的正弦数据
def generate_data(num_samples):
    x = np.linspace(0, 2 * np.pi, num_samples)
    # 白噪声符合正态分布,并使用了0.1来控制噪声的幅度
    y = np.sin(x) + 0.1 * np.random.randn(num_samples)
    #print('y=',y)
    #print('noise=',np.random.randn(num_samples))
    #将x和y都输出为num_samples行一列的数组
    return x.reshape(-1, 1), y.reshape(-1, 1)

自定义的generate_data()函数用于生成数据,数据的主体是正弦函数,但在这个基础上叠加了符合正态分布的白噪声。

之后,代码经自变量x和因变量y都按照数组的形式输出。

【2.3.2】train_model()函数训练数据

# 训练模型
def train_model(X, Y, hidden_size, num_iterations, learning_rate):
    # 定义input_size是X的列数
    input_size = X.shape[1]
    # 定义output_size是Y的列数
    output_size = Y.shape[1]
    # 调用initialize_parameters()函数获得连接权重和偏置量
    # W1是按照正态分布获得的随机数值
    # b1是纯0矩阵
    # W2是按照正态分布获得的随机数值
    # b2是纯0矩阵
    W1, b1, W2, b2 = initialize_parameters(input_size, hidden_size, output_size)
    # 定义空矩阵costs
    costs = []

    #迭代运算
    for i in range(num_iterations):
        # 调用forward_propagation()函数获得正向运算值
        # Z1是将输入层连接隐藏层的权重代入X,再叠加偏置量后的输出
        # A1是将Z1代入激活函数后的输出
        # Z2是将隐藏层连接输出层的权重代入A1,再叠加偏置量后的输出
        # A2是Z2的直接赋值,没有新的变化
        # Z1是个线性变化量,A2叠加了激活函数中的非线性因素
        Z1, A1, Z2, A2 = forward_propagation(X, W1, b1, W2, b2)
        # 运算值和目标值差分
        cost = compute_cost(A2, Y)
        # 计算连接权重和偏置量的逆向变化量
        # dW2是A1的转置和(A2-Y)求均值后再矩阵相乘的值
        # db2是(A2-Y)按列求和之后再求均值后的值
        # dW1是X的转置和dZ1矩阵相乘的值
        # dZ1是(A2-Y)和W2的转置先矩阵相乘,再乘以Z1代入激活函数导函数后的值
        # db1是dZ1列求和之后再求均值后的值
        dW1, db1, dW2, db2 = backward_propagation(X, Y, Z1, A1, A2, W1, W2)
        # 更新所有连接权重和偏置量
        # W1是将上一步的dW1和学习效率相乘后,和原W1作差后的值
        # b1是将上一步的db1和学习效率相乘后,和原b1作差后的值
        # W2是将上一步的dW2和学习效率相乘后,和原W2作差后的值
        # b2是将上一步的db2和学习效率相乘后,和原b2作差后的值
        W1, b1, W2, b2 = update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate)

        if i % 100 == 0:
            costs.append(cost)
            print(f"Cost after iteration {i}: {cost}")

    return W1, b1, W2, b2, costs

自定义的train_model()函数用于训练数据,训练数据的目的是获得输入层到隐藏层、隐藏层到输出层的连接权重和偏置量等参数。

为了获得连接权重和偏置量等参数,train_model()函数继续调用了新的子函数:

  1. 调用initialize_parameters()函数初始化连接权重和偏置量;
  2. 定义激活函数sigmoid()及其导数;
  3. 在迭代计算中:
  • 调用forward_propagation()函数获得隐藏层和输出层的运算值,以及运算值代入激活函数后的输出;
  • 调用compute_cost()函数获得运算值和实际值之间的差异;
  • 调用backward_propagation()函数获得初始化连接权重和偏置量的变化量:
  • 调用update_parameters()函数获得新的连接权重和偏置量。

【2.3.3】 initialize_parameters()函数

# 初始化网络参数
def initialize_parameters(input_size, hidden_size, output_size):
    # 输入的基本参数包括输入层、隐藏层和输出层大大小,实际对应各层的行数
    # W1是从输入层进入隐藏层的连接权重
    # W1=先按照正态分布的形式生成随机数,然后乘以系数0.01
    # W1是一个input_size行hidden_size列的矩阵
    W1 = np.random.randn(input_size, hidden_size) * 0.01
    # b1是一个1行hidden_size列的矩阵
    b1 = np.zeros((1, hidden_size))
    # W2是从输入层进入隐藏层的连接权重
    # W2=先按照正态分布的形式生成随机数,然后乘以系数0.01
    # W2是一个hidden_size行output_size列的矩阵
    W2 = np.random.randn(hidden_size, output_size) * 0.01
    # b2是一个1行output_size列的矩阵
    b2 = np.zeros((1, output_size))
    return W1, b1, W2, b2

自定义的initialize_parameters()函数生成了初始连接权重和偏置量参数:

连接权重都是符合正态分布的随机数乘以微小系数0.01后获得;

偏置量都是0。

【2.3.4】 sigmoid()函数及其导函数

# 定义激活函数及其导数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    return sigmoid(x) * (1 - sigmoid(x))

自定义的 sigmoid()函数及其导函数是为了在连接权重中引入非线性成分。

【2.3.5】forward_propagation()函数

# 前向传播
def forward_propagation(X, W1, b1, W2, b2):
    # 按照矩阵乘法的运算原则,将X和W1相乘,然后叠加偏置量b1
    # X、W1和b1都是矩阵数据,会按照顺序注意叠加,类似于x[1]w1[1]+b1[1]
    # 这一步的计算是从输入层接入隐藏层
    # Z1是一个num_samples行hidden_size列的矩阵
    Z1 = np.dot(X, W1) + b1
    # 把A1代入激活函数
    # 这一步的计算式获得隐藏层的输出
    # 获得的A1是一个num_samples行hidden_size列的矩阵向量
    A1 = sigmoid(Z1)
    # 按照矩阵乘法的运算原则,将A1和W2相乘,然后叠加偏置量b2
    # A1、W2和b2都是矩阵数据,会按照顺序注意叠加,类似于A1[1]w2[1]+b2[1]
    # 获得的Z2是一个num_samples行output_size列的矩阵
    Z2 = np.dot(A1, W2) + b2
    # 这一步的计算是从隐藏层接入输出层,可以理解为所有权重都是1,偏置量都是0
    # 获得的A2是一个num_samples行output_size列的矩阵
    A2 = Z2
    return Z1, A1, Z2, A2

自定义的 forward_propagation()函数包括线性运算和激活函数运算:

先进行线性运算,让所有输入层的自变量分别和输入层到隐藏层之间的连接权重相乘,再叠加偏置量;

然后把计算值代入激活函数,激活函数运算获得的值再和隐藏层到输出层之间的连接权重相乘,此时的值直接赋给输出值。

【2.3.6】compute_cost()函数

# 计算损失(均方误差)
def compute_cost(A2, Y):
    # 将矩阵Y的行数取出来赋值给m
    m = Y.shape[0]
    # A2是输出值,先将所有输出值和目标值作差,然后再对差进行平方求和,再对差平方和取均值
    cost = (1 / (2 * m)) * np.sum(np.square(A2 - Y))
    return cost

自定义的compute_cost()函数将运算所的值和实际值做差分运算,然后所有差分值求平方和再求均值。

【2.3.7】backward_propagation()函数

# 反向传播
def backward_propagation(X, Y, Z1, A1, A2, W1, W2):
    # 将矩阵X的行数取出来赋值给m
    m = X.shape[0]
    # A2是输出值,将所有输出值和目标值作差后赋值给DZ2
    # dZ2是一个num_samples行output_size列的矩阵
    dZ2 = A2 - Y
    # 首先将A1转置为hidden_size行num_samples列的矩阵A1.T
    # 然后A1和dZ2按照矩阵乘法的原则进行计算获得dW2
    # dW2是一个hidden_size行output_size列的矩阵
    dW2 = (1 / m) * np.dot(A1.T, dZ2)
    # db2是将所有的dZ2叠加后求平均值
    # axis=0表示每列从上到下求和
    # dZ2有output_size列,所以会获得output_size个值
    # keepdims=True会将output_size个值记录为1行output_size列的矩阵
    # db2表示将1行output_size列的矩阵取均值,依然是1行output_size列的矩阵
    db2 = (1 / m) * np.sum(dZ2, axis=0, keepdims=True)
    # 首先将W2转置为output_size行hidden_size列的矩阵
    # dZ2是一个num_samples行output_size列的矩阵
    # dZ2和W2.T按照矩阵乘法的原则计算后,获得num_samples行hidden_size列的矩阵
    # Z1是一个num_samples行hidden_size列的矩阵
    # sigmoid_derivative(Z1)是把Z1代入激活函数的导函数的计算值,是一个num_samples行hidden_size列的矩阵
    dZ1 = np.dot(dZ2, W2.T) * sigmoid_derivative(Z1)
    # dZ1是np.dot(dZ2, W2.T)和sigmoid_derivative(Z1)相同位置的元素彼此相乘获得的
    # dZ1是一个num_samples行hidden_size列的矩阵
    # 首先将X转置成一个一行num_samples列的矩阵X.T
    # 然后X.T再和num_samples行hidden_size列的矩阵dZ1相乘,获得dW1
    # dW1是一个num_samples行hidden_size列的矩阵
    dW1 = (1 / m) * np.dot(X.T, dZ1)
    # dZ1先每一列求和,获得hidden_size个数,keepdims=True将求和结果转化为1行hidden_size列矩阵,然后再求均值
    # db1是一个一行hidden_size列的矩阵
    db1 = (1 / m) * np.sum(dZ1, axis=0, keepdims=True)
    return dW1, db1, dW2, db2

自定义的backward_propagation()函数获得四个参数:

先获得运算值和实际值的差分值;

对隐藏层的输出值和差分值做矩阵运算,此时获得隐藏层和输出层之间的权重偏移量;

将差分值求均值,获得隐藏层到输出层的偏置量偏移量;

再计算输入层打破隐藏层的线性运算值,这个线性运算值代入激活函数的导函数后获得导函数值,差分值和隐藏层到输出层的连接权重做矩阵运算,然后矩阵运算值和导函数值相乘,获得隐藏层的输出偏移量;

将输入层的自变量和隐藏层的输出偏移量做矩阵运算,获得输入层到隐藏层的连接权重偏移量;

将隐藏层的输出偏移量求均值,获得输入层到隐藏层的偏置量偏移量。

【2.3.8】update_parameters()函数

# 更新参数
def update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate):
    # W1是一个input_size行hidden_size列的矩阵
    # dW1是一个num_samples行hidden_size列的矩阵
    # 实际上输入的input_size行=num_samples行
    # 新获得的W1也是num_samples行hidden_size列的矩阵
    W1 = W1 - learning_rate * dW1
    # b1是一个1行hidden_size列的矩阵
    # db1是一个一行hidden_size列的矩阵
    # 新获得的b1是一个一行hidden_size列的矩阵
    b1 = b1 - learning_rate * db1
    # W2是一个hidden_size行output_size列的矩阵
    # dW2是一个hidden_size行output_size列的矩阵
    # 新获得的W2是一个hidden_size行output_size列的矩阵
    W2 = W2 - learning_rate * dW2
    # b2是一个1行output_size列的矩阵
    # db2是一个1行output_size列的矩阵
    # 新获得的b2是一个一行output_size列的矩阵
    b2 = b2 - learning_rate * db2
    return W1, b1, W2, b2

自定义的update_parameters()函数,将上一步获得的连接权重和偏置量使用线性方程进行更新。

由于forward_propagation()函数、compute_cost()函数、backward_propagation()函数和update_parameters()函数是在迭代计算反复参与计算,所以最后会获得相对合理的连接权重和偏置量。

此时的完整代码为:

import numpy as np #引入numpy模块
import matplotlib.pyplot as plt #引入matplotlib模块

# 生成带有噪声的正弦数据
def generate_data(num_samples):
    x = np.linspace(0, 2 * np.pi, num_samples)
    # 白噪声符合正态分布,并使用了0.1来控制噪声的幅度
    y = np.sin(x) + 0.1 * np.random.randn(num_samples)
    #print('y=',y)
    #print('noise=',np.random.randn(num_samples))
    #将x和y都输出为num_samples行一列的数组
    return x.reshape(-1, 1), y.reshape(-1, 1)

# 定义激活函数及其导数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    return sigmoid(x) * (1 - sigmoid(x))

# 初始化网络参数
def initialize_parameters(input_size, hidden_size, output_size):
    # 输入的基本参数包括输入层、隐藏层和输出层大大小,实际对应各层的行数
    # W1是从输入层进入隐藏层的连接权重
    # W1=先按照正态分布的形式生成随机数,然后乘以系数0.01
    # W1是一个input_size行hidden_size列的矩阵
    W1 = np.random.randn(input_size, hidden_size) * 0.01
    # b1是一个1行hidden_size列的矩阵
    b1 = np.zeros((1, hidden_size))
    # W2是从输入层进入隐藏层的连接权重
    # W2=先按照正态分布的形式生成随机数,然后乘以系数0.01
    # W2是一个hidden_size行output_size列的矩阵
    W2 = np.random.randn(hidden_size, output_size) * 0.01
    # b2是一个1行output_size列的矩阵
    b2 = np.zeros((1, output_size))
    return W1, b1, W2, b2

# 前向传播
def forward_propagation(X, W1, b1, W2, b2):
    # 按照矩阵乘法的运算原则,将X和W1相乘,然后叠加偏置量b1
    # X、W1和b1都是矩阵数据,会按照顺序注意叠加,类似于x[1]w1[1]+b1[1]
    # 这一步的计算是从输入层接入隐藏层
    # Z1是一个num_samples行hidden_size列的矩阵
    Z1 = np.dot(X, W1) + b1
    # 把A1代入激活函数
    # 这一步的计算式获得隐藏层的输出
    # 获得的A1是一个num_samples行hidden_size列的矩阵向量
    A1 = sigmoid(Z1)
    # 按照矩阵乘法的运算原则,将A1和W2相乘,然后叠加偏置量b2
    # A1、W2和b2都是矩阵数据,会按照顺序注意叠加,类似于A1[1]w2[1]+b2[1]
    # 获得的Z2是一个num_samples行output_size列的矩阵
    Z2 = np.dot(A1, W2) + b2
    # 这一步的计算是从隐藏层接入输出层,可以理解为所有权重都是1,偏置量都是0
    # 获得的A2是一个num_samples行output_size列的矩阵
    A2 = Z2
    return Z1, A1, Z2, A2

# 计算损失(均方误差)
def compute_cost(A2, Y):
    # 将矩阵Y的行数取出来赋值给m
    m = Y.shape[0]
    # A2是输出值,先将所有输出值和目标值作差,然后再对差进行平方求和,再对差平方和取均值
    cost = (1 / (2 * m)) * np.sum(np.square(A2 - Y))
    return cost

# 反向传播
def backward_propagation(X, Y, Z1, A1, A2, W1, W2):
    # 将矩阵X的行数取出来赋值给m
    m = X.shape[0]
    # A2是输出值,将所有输出值和目标值作差后赋值给DZ2
    # dZ2是一个num_samples行output_size列的矩阵
    dZ2 = A2 - Y
    # 首先将A1转置为hidden_size行num_samples列的矩阵A1.T
    # 然后A1和dZ2按照矩阵乘法的原则进行计算获得dW2
    # dW2是一个hidden_size行output_size列的矩阵
    dW2 = (1 / m) * np.dot(A1.T, dZ2)
    # db2是将所有的dZ2叠加后求平均值
    # axis=0表示每列从上到下求和
    # dZ2有output_size列,所以会获得output_size个值
    # keepdims=True会将output_size个值记录为1行output_size列的矩阵
    # db2表示将1行output_size列的矩阵取均值,依然是1行output_size列的矩阵
    db2 = (1 / m) * np.sum(dZ2, axis=0, keepdims=True)
    # 首先将W2转置为output_size行hidden_size列的矩阵
    # dZ2是一个num_samples行output_size列的矩阵
    # dZ2和W2.T按照矩阵乘法的原则计算后,获得num_samples行hidden_size列的矩阵
    # Z1是一个num_samples行hidden_size列的矩阵
    # sigmoid_derivative(Z1)是把Z1代入激活函数的导函数的计算值,是一个num_samples行hidden_size列的矩阵
    dZ1 = np.dot(dZ2, W2.T) * sigmoid_derivative(Z1)
    # dZ1是np.dot(dZ2, W2.T)和sigmoid_derivative(Z1)相同位置的元素彼此相乘获得的
    # dZ1是一个num_samples行hidden_size列的矩阵
    # 首先将X转置成一个一行num_samples列的矩阵X.T
    # 然后X.T再和num_samples行hidden_size列的矩阵dZ1相乘,获得dW1
    # dW1是一个num_samples行hidden_size列的矩阵
    dW1 = (1 / m) * np.dot(X.T, dZ1)
    # dZ1先每一列求和,获得hidden_size个数,keepdims=True将求和结果转化为1行hidden_size列矩阵,然后再求均值
    # db1是一个一行hidden_size列的矩阵
    db1 = (1 / m) * np.sum(dZ1, axis=0, keepdims=True)
    return dW1, db1, dW2, db2

# 更新参数
def update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate):
    # W1是一个input_size行hidden_size列的矩阵
    # dW1是一个num_samples行hidden_size列的矩阵
    # 实际上输入的input_size行=num_samples行
    # 新获得的W1也是num_samples行hidden_size列的矩阵
    W1 = W1 - learning_rate * dW1
    # b1是一个1行hidden_size列的矩阵
    # db1是一个一行hidden_size列的矩阵
    # 新获得的b1是一个一行hidden_size列的矩阵
    b1 = b1 - learning_rate * db1
    # W2是一个hidden_size行output_size列的矩阵
    # dW2是一个hidden_size行output_size列的矩阵
    # 新获得的W2是一个hidden_size行output_size列的矩阵
    W2 = W2 - learning_rate * dW2
    # b2是一个1行output_size列的矩阵
    # db2是一个1行output_size列的矩阵
    # 新获得的b2是一个一行output_size列的矩阵
    b2 = b2 - learning_rate * db2
    return W1, b1, W2, b2

# 训练模型
def train_model(X, Y, hidden_size, num_iterations, learning_rate):
    # 定义input_size是X的列数
    input_size = X.shape[1]
    # 定义output_size是Y的列数
    output_size = Y.shape[1]
    # 调用initialize_parameters()函数获得连接权重和偏置量
    # W1是按照正态分布获得的随机数值
    # b1是纯0矩阵
    # W2是按照正态分布获得的随机数值
    # b2是纯0矩阵
    W1, b1, W2, b2 = initialize_parameters(input_size, hidden_size, output_size)
    # 定义空矩阵costs
    costs = []

    #迭代运算
    for i in range(num_iterations):
        # 调用forward_propagation()函数获得正向运算值
        # Z1是将输入层连接隐藏层的权重代入X,再叠加偏置量后的输出
        # A1是将Z1代入激活函数后的输出
        # Z2是将隐藏层连接输出层的权重代入A1,再叠加偏置量后的输出
        # A2是Z2的直接赋值,没有新的变化
        # Z1是个线性变化量,A2叠加了激活函数中的非线性因素
        Z1, A1, Z2, A2 = forward_propagation(X, W1, b1, W2, b2)
        # 运算值和目标值差分
        cost = compute_cost(A2, Y)
        # 计算连接权重和偏置量的逆向变化量
        # dW2是A1的转置和(A2-Y)求均值后再矩阵相乘的值
        # db2是(A2-Y)按列求和之后再求均值后的值
        # dW1是X的转置和dZ1矩阵相乘的值
        # dZ1是(A2-Y)和W2的转置先矩阵相乘,再乘以Z1代入激活函数导函数后的值
        # db1是dZ1列求和之后再求均值后的值
        dW1, db1, dW2, db2 = backward_propagation(X, Y, Z1, A1, A2, W1, W2)
        # 更新所有连接权重和偏置量
        # W1是将上一步的dW1和学习效率相乘后,和原W1作差后的值
        # b1是将上一步的db1和学习效率相乘后,和原b1作差后的值
        # W2是将上一步的dW2和学习效率相乘后,和原W2作差后的值
        # b2是将上一步的db2和学习效率相乘后,和原b2作差后的值
        W1, b1, W2, b2 = update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate)

        if i % 100 == 0:
            costs.append(cost)
            print(f"Cost after iteration {i}: {cost}")

    return W1, b1, W2, b2, costs

# 主函数
def main():
    # 生成数据
    num_samples = 100
    X, Y = generate_data(num_samples)

    # 训练模型
    hidden_size = 10
    num_iterations = 1000
    learning_rate = 0.1
    # 训练参数
    W1, b1, W2, b2, costs = train_model(X, Y, hidden_size, num_iterations, learning_rate)

    # 生成测试数据
    x_test = np.linspace(0, 2 * np.pi, 200).reshape(-1, 1)
    y_test = np.sin(x_test)

    # 进行预测
    # 预测只在往前传递的运算中执行
    _, _, _, y_pred = forward_propagation(x_test, W1, b1, W2, b2)

    # 绘制结果
    plt.figure(figsize=(12, 6))

    plt.subplot(1, 2, 1)
    plt.scatter(X, Y, label='Training Data', color='blue')
    plt.plot(x_test, y_test, label='True Function', color='green')
    plt.plot(x_test, y_pred, label='Predicted Function', color='red')
    plt.legend()
    plt.title('BP Neural Network')
    plt.xlabel('x')
    plt.ylabel('y')

    plt.subplot(1, 2, 2)
    plt.plot(np.arange(0, num_iterations, 100), costs)
    plt.title('Cost over iterations')
    plt.xlabel('Iterations')
    plt.ylabel('Cost')

    plt.show()

if __name__ == "__main__":
    main()

代码运行获得的图像为:

图1  BP神经网络运算效果-1000次循环

【3】代码改写

图1所示的BP神经网络运算效果实际上尚未实现很好拟合,因为左侧图的预测曲线红色线和实际的正弦函数曲线相差较大,因此还有继续优化的空间。

最简单的方法之一就是想办法增加循环次数,获得更准确的连接权重和偏置量参数:

先将循环次数从1000增加到10000:

    num_iterations = 10000

获得的运算图像为:

 图2  BP神经网络运算效果-10000次循环

由图2可见,增大循环次数后,预测曲线和实际正弦曲线已经非常接近。

继续增加循环次数到100000次:

    num_iterations = 100000

获得的运算图像为:

 图3  BP神经网络运算效果-100000次循环  

由图3可见,继续增加循环次数到100000次以后,图像的拟合效果已经较为准确。

【4】细节说明

激活函数的定义还有很多方法,此处只选择了其中一种常用的函数。

【5】总结

进行了BP神经网络算法入门学习,掌握了python使用BP神经网络算法进行函数拟合的初步技巧。