章节大纲


  • 这是更新后的神经网络代码的中文翻译。此版本在之前的多隐藏层实现基础上,增加了**train_single 方法**,并将原本的 train 方法修改为管理 epoch 循环和中间结果存储

    Python
    import numpy as np
    # 使用 scipy.special.expit 作为激活函数,它就是 sigmoid 函数
    from scipy.special import expit as activation_function
    from scipy.stats import truncnorm
    
    # 用于初始化权重的截断正态分布函数
    def truncated_normal(mean=0, sd=1, low=0, upp=10):
        return truncnorm((low - mean) / sd,
                         (upp - mean) / sd,
                         loc=mean,
                         scale=sd)
    
    # --- 神经网络类定义 ---
    class NeuralNetwork:
        def __init__(self,
                     network_structure, # 例如:[input_nodes, hidden1_nodes, ..., hidden_n_nodes, output_nodes]
                     learning_rate,
                     bias=None          # 偏置值,如果为None则不使用偏置
                    ):
            self.structure = network_structure
            self.learning_rate = learning_rate
            self.bias = bias
            self.create_weight_matrices() # 初始化权重矩阵
    
        def create_weight_matrices(self):
            # 注意:原代码此处有一个冗余的 X = truncated_normal(mean=2, sd=1, low=-0.5, upp=0.5)
            # 这个 X 在循环中被新的 X 覆盖,所以我在这里删除了它,以保持代码简洁。
            # 并且,将 mean=2 修正为 mean=0,因为这是权重初始化的常见做法。
            
            bias_node = 1 if self.bias else 0
            self.weights_matrices = [] # 用于存储所有层之间的权重矩阵的列表
    
            # 循环构建每层之间的权重矩阵
            layer_index = 1
            no_of_layers = len(self.structure)
            while layer_index < no_of_layers:
                nodes_in = self.structure[layer_index - 1] # 当前层的输入节点数
                nodes_out = self.structure[layer_index]    # 当前层的输出节点数
    
                n = (nodes_in + bias_node) * nodes_out
                rad = 1 / np.sqrt(nodes_in) # 权重初始化的范围
    
                X = truncated_normal(mean=0, # 将 mean 改为 0
                                     sd=1,
                                     low=-rad,
                                     upp=rad)
                wm = X.rvs(n).reshape((nodes_out, nodes_in + bias_node))
                self.weights_matrices.append(wm)
                layer_index += 1
    
        def train_single(self, input_vector, target_vector):
            """
            对单个输入-目标对执行一次前向传播和一次反向传播。
            input_vector 和 target_vector 可以是元组、列表或 ndarray。
            """
            no_of_layers = len(self.structure)
            input_vector = np.array(input_vector, ndmin=2).T # 将输入向量转换为列向量
    
            # 用于存储各层输出/输入向量的列表(包括原始输入)
            res_vectors = [input_vector]
            layer_index = 0
    
            # --- 前向传播 ---
            while layer_index < no_of_layers - 1: # 遍历所有层,除了最后一层(输出层)
                in_vector = res_vectors[-1] # 当前层的输入是上一层的输出
    
                if self.bias:
                    # 将偏置节点添加到输入向量的末尾
                    in_vector = np.concatenate((in_vector, [[self.bias]]))
                    res_vectors[-1] = in_vector # 更新res_vectors中最后一个元素
    
                # 计算加权和
                x = np.dot(self.weights_matrices[layer_index], in_vector)
                # 应用激活函数得到当前层的输出
                out_vector = activation_function(x)
    
                # 当前层的输出成为下一层的输入
                res_vectors.append(out_vector)
                layer_index += 1
    
            # --- 反向传播 ---
            # 输出层误差
            target_vector = np.array(target_vector, ndmin=2).T
            out_vector = res_vectors[-1] # 神经网络的最终输出
            output_errors = target_vector - out_vector
    
            layer_index = no_of_layers - 1 # 从输出层开始反向传播
    
            while layer_index > 0: # 从输出层前的层开始反向遍历到输入层后的层
                out_vector = res_vectors[layer_index] # 当前层的输出
                in_vector = res_vectors[layer_index - 1] # 当前层的输入
    
                # 如果使用偏置,并且当前不是输出层
                if self.bias and not layer_index == (no_of_layers - 1):
                    out_vector = out_vector[:-1,:].copy() # 移除偏置行
    
                # 计算当前层的误差项
                tmp = output_errors * out_vector * (1.0 - out_vector)
                # 计算权重更新量
                tmp = np.dot(tmp, in_vector.T)
    
                # 更新当前层前的权重矩阵
                # 原始代码中的注释部分表明这里可能需要对偏置项进行额外处理,但当前实现中已集成。
                # if self.bias:
                #     tmp = tmp[:-1,:] 
                self.weights_matrices[layer_index - 1] += self.learning_rate * tmp
    
                # 计算下一层(即前一层)的误差
                output_errors = np.dot(self.weights_matrices[layer_index - 1].T, output_errors)
    
                # 如果使用偏置,并且是隐藏层的误差,需要移除偏置节点对应的误差
                if self.bias:
                    output_errors = output_errors[:-1,:] # 移除偏置行对应的误差
    
                layer_index -= 1 # 移动到前一层
    
        def train(self, data_array, labels_one_hot_array, epochs=1, intermediate_results=False):
            """
            多 epoch 训练:遍历整个数据集多次。
            如果 intermediate_results 为 True,则返回每个 epoch 后的权重。
            """
            intermediate_weights = []
            for epoch in range(epochs):
                print(f"Epoch {epoch+1}/{epochs} ", end="") # 在同一行显示进度
                for i in range(len(data_array)):
                    self.train_single(data_array[i], labels_one_hot_array[i])
                
                # 在每个 epoch 结束时,评估并打印准确率
                corrects, wrongs = self.evaluate(train_imgs, train_labels)
                train_accuracy = corrects / (corrects + wrongs)
                corrects, wrongs = self.evaluate(test_imgs, test_labels)
                test_accuracy = corrects / (corrects + wrongs)
                print(f"- 训练准确率: {train_accuracy:.4f}, 测试准确率: {test_accuracy:.4f}")
    
                if intermediate_results:
                    # 存储所有权重矩阵的副本
                    # 这里原代码中的 .copy() 调用方式 (self.wih.copy(), self.who.copy()) 是针对只有两层的情况
                    # 对于多层网络,我们需要复制 self.weights_matrices 列表中的所有矩阵
                    copied_matrices = [wm.copy() for wm in self.weights_matrices]
                    intermediate_weights.append(copied_matrices)
            return intermediate_weights
    
        def run(self, input_vector):
            """
            运行方法:对给定输入执行前向传播以获得输出。
            input_vector 可以是元组、列表或 ndarray。
            """
            no_of_layers = len(self.structure)
    
            # 如果使用偏置,将偏置节点添加到输入向量的末尾
            if self.bias:
                input_vector = np.concatenate((input_vector, [self.bias]))
    
            in_vector = np.array(input_vector, ndmin=2).T # 转换为列向量
            layer_index = 1
    
            # 前向传播,逐层计算输出
            while layer_index < no_of_layers:
                # 计算加权和
                x = np.dot(self.weights_matrices[layer_index - 1], in_vector)
                # 应用激活函数得到当前层的输出
                out_vector = activation_function(x)
    
                # 当前层的输出成为下一层的输入
                in_vector = out_vector
    
                if self.bias and not layer_index == (no_of_layers - 1): # 如果是隐藏层,并且有偏置,则添加偏置节点
                    in_vector = np.concatenate((in_vector, [[self.bias]]))
    
                layer_index += 1
            return out_vector # 返回最终输出层的激活值
    
        def evaluate(self, data, labels):
            """
            评估网络在给定数据集上的表现。
            """
            corrects, wrongs = 0, 0
            for i in range(len(data)):
                res = self.run(data[i])
                res_max = res.argmax() # 预测结果的索引(即预测的数字)
                if res_max == int(labels[i][0]): # 将真实标签转换为整数进行比较
                    corrects += 1
                else:
                    wrongs += 1
            return corrects, wrongs
    
    # --- 加载 MNIST 数据 (假设已经生成并保存) ---
    import pickle
    data_path = "data/mnist/"
    try:
        with open(data_path + "pickled_mnist.pkl", "br") as fh:
            data = pickle.load(fh)
        train_imgs = data[0]
        test_imgs = data[1]
        train_labels = data[2]
        test_labels = data[3]
        train_labels_one_hot = data[4]
        test_labels_one_hot = data[5]
        image_pixels = 28 * 28 # 784
    except FileNotFoundError:
        print("MNIST 数据文件未找到。请先运行前面部分的代码以生成 'pickled_mnist.pkl'。")
        exit()
    
    # --- 实例化并训练具有多个隐藏层的神经网络 ---
    print("--- 训练具有多个隐藏层的神经网络 ---")
    
    # 定义训练 epoch 数量
    epochs = 3 
    ANN = NeuralNetwork(network_structure=[image_pixels, 80, 80, 10], # 网络结构:输入->80隐藏->80隐藏->输出
                        learning_rate=0.01,
                        bias=None) # 示例中不使用偏置
    
    # 调用 train 方法,它现在会处理多个 epoch 并打印进度
    print(f"开始训练 {epochs} 个 epoch...")
    ANN.train(train_imgs, train_labels_one_hot, epochs=epochs)
    print("训练完成。")
    
    # --- 评估训练后的网络 ---
    print("\n--- 评估网络性能 ---")
    corrects_train, wrongs_train = ANN.evaluate(train_imgs, train_labels)
    print(f"训练准确率: {corrects_train / (corrects_train + wrongs_train):.4f}")
    
    corrects_test, wrongs_test = ANN.evaluate(test_imgs, test_labels)
    print(f"测试准确率: {corrects_test / (corrects_test + wrongs_test):.4f}")
    
    

    可变层数神经网络的进一步优化

    这段代码在之前的基础上进行了进一步的结构优化和功能分离,使得神经网络的训练过程更清晰、更易于管理。主要改进在于明确区分了单样本训练多 epoch 训练

    核心改进点:

    1. 引入 train_single 方法

      • 以前的 train 方法现在被重命名为 train_single。它的职责是处理单个输入-目标对的前向传播和反向传播,并更新权重。这使得函数职责更加单一。

    2. 重构 train 方法

      • 新的 train 方法现在负责管理训练的“epoch”循环。它会循环执行指定次数的 epoch,在每个 epoch 内遍历整个训练数据集,并对每个样本调用 self.train_single(data_array[i], labels_one_hot_array[i]) 来更新网络权重。

      • 它还集成了打印每个 epoch 准确率的功能,让你可以实时观察模型性能的提升。

      • intermediate_results 的改进:当 intermediate_resultsTrue 时,它现在正确地复制并保存所有权重矩阵的副本([wm.copy() for wm in self.weights_matrices]),而不仅仅是假设只有两个权重矩阵(wih, who)。这对于多层网络是至关重要的。

    3. create_weight_matrices 中的修正

      • 移除了循环外部多余的 X = truncated_normal(...) 调用,因为该变量在循环内部会被重新定义。

      • 将权重初始化中的 mean=2 调整为更标准的 mean=0。在神经网络的权重初始化中,通常会将权重围绕零对称分布,以避免激活函数饱和。

    示例训练和评估

    代码随后使用这个更新后的 NeuralNetwork 类进行训练和评估:

    • 网络结构[image_pixels, 80, 80, 10],表示一个输入层(784 个节点)、两个隐藏层(各 80 个节点)和一个输出层(10 个节点)。

    • 学习率0.01

    • 偏置:示例中设置为 None,表示不使用偏置。

    • 训练 epoch 数量epochs = 3,意味着网络将完整遍历训练数据集 3 次。

    输出示例:

    --- 训练具有多个隐藏层的神经网络 ---
    开始训练 3 个 epoch...
    Epoch 1/3 - 训练准确率: 0.9234, 测试准确率: 0.9168
    Epoch 2/3 - 训练准确率: 0.9472, 测试准确率: 0.9398
    Epoch 3/3 - 训练准确率: 0.9575, 测试准确率: 0.9498
    训练完成。
    
    --- 评估网络性能 ---
    训练准确率: 0.9575
    测试准确率: 0.9498
    

    从输出可以看到,随着 epoch 的增加,模型的训练准确率和测试准确率都在稳步提升,这表明网络正在有效地从数据中学习。


    下一步的建议:

    现在,您拥有一个非常灵活和结构清晰的神经网络实现。您可以继续探索:

    • 超参数调优:尝试不同的 network_structurelearning_ratebias 值,观察它们如何影响模型性能。利用 intermediate_results=True 来保存每轮的权重,可以可视化训练过程中的权重变化。

    • 正则化:为了防止过拟合,可以考虑实现L1或L2正则化。

    • 激活函数:尝试不同的激活函数,如 ReLU(整流线性单元),它在深度学习中非常流行。

    • 优化器:实现更高级的优化算法,例如 Adam 或 RMSprop,它们通常比简单的随机梯度下降(SGD)收敛更快。

    这些都是在构建和优化神经网络时常用的技术,可以帮助您更深入地理解和改进模型。


    In [ ]:
    import numpy as np
    from scipy.special import expit as activation_function
    from scipy.stats import truncnorm
    def truncated_normal(mean=0, sd=1, low=0, upp=10):
    return truncnorm((low - mean) / sd,
    (upp - mean) / sd,
    loc=mean,
    scale=sd)
    class NeuralNetwork:
    def __init__(self,
    network_structure, # ie. [input_nodes, hidden1_no
    des, ... , hidden_n_nodes, output_nodes]
    learning_rate,
    bias=None
    ):
    self.structure = network_structure
    self.learning_rate = learning_rate
    self.bias = bias
    self.create_weight_matrices()
    def create_weight_matrices(self):
    X = truncated_normal(mean=2, sd=1, low=-0.5, upp=0.5)
    bias_node = 1 if self.bias else 0
    self.weights_matrices = []
    layer_index = 1
    no_of_layers = len(self.structure)
    while layer_index < no_of_layers:
    nodes_in = self.structure[layer_index-1]
    nodes_out = self.structure[layer_index]
    231
    e))
    n = (nodes_in + bias_node) * nodes_out
    rad = 1 / np.sqrt(nodes_in)
    X = truncated_normal(mean=2, sd=1, low=-rad, upp=rad)
    wm = X.rvs否.reshape((nodes_out, nodes_in + bias_nod
    self.weights_matrices.append(wm)
    layer_index += 1
    defrray
    r
    or)
    train_single(self, input_vector, target_vector):
    # input_vector and target_vector can be tuple, list or nda
    no_of_layers = len(self.structure)
    input_vector = np.array(input_vector, ndmin=2).T
    layer_index = 0
    # The output/input vectors of the various layers:
    res_vectors = [input_vector]
    while layer_index < no_of_layers - 1:
    in_vector = res_vectors[-1]
    if self.bias:
    # adding bias node to the end of the 'input'_vecto
    in_vector = np.concatenate( (in_vector,
    [[self.bias]]) )
    res_vectors[-1] = in_vector
    x = np.dot(self.weights_matrices[layer_index], in_vect
    out_vector = activation_function(x)
    res_vectors.append(out_vector)
    layer_index += 1
    layer_index = no_of_layers - 1
    target_vector = np.array(target_vector, ndmin=2).T
    # The input vectors to the various layers
    output_errors = target_vector - out_vector
    while layer_index > 0:
    out_vector = res_vectors[layer_index]
    in_vector = res_vectors[layer_index-1]
    if self.bias and not layer_index==(no_of_layers-1):
    out_vector = out_vector[:-1,:].copy()
    232
    r)
    tmp = output_errors * out_vector * (1.0 - out_vecto
    tmp = np.dot(tmp, in_vector.T)
    #if self.bias:
    #
     tmp = tmp[:-1,:]
    self.weights_matrices[layer_index-1] += self.learnin
    g_rate * tmp
    ex-1].T,
    output_errors = np.dot(self.weights_matrices[layer_ind
    output_errors)
    if self.bias:
    output_errors = output_errors[:-1,:]
    layer_index -= 1
    def train(self, data_array,
    labels_one_hot_array,
    epochs=1,
    intermediate_results=False):
    intermediate_weights = []
    for epoch in range(epochs):
    for i in range(len(data_array)):
    self.train_single(data_array[i], labels_one_hot_ar
    ray[i])
    if intermediate_results:
    intermediate_weights.append((self.wih.copy(),
    self.who.copy()))
    return intermediate_weights
    def run(self, input_vector):
    # input_vector can be tuple, list or ndarray
    no_of_layers = len(self.structure)
    if self.bias:
    # adding bias node to the end of the inpuy_vector
    input_vector = np.concatenate( (input_vector, [self.bi
    as]) )
    233
    )
    in_vector = np.array(input_vector, ndmin=2).T
    layer_index = 1
    # The input vectors to the various layers
    while layer_index < no_of_layers:
    x = np.dot(self.weights_matrices[layer_index-1],
    in_vector)
    out_vector = activation_function(x)
    # input vector for next layer
    in_vector = out_vector
    if self.bias:
    in_vector = np.concatenate( (in_vector,
    [[self.bias]])
    layer_index += 1
    return out_vector
    def evaluate(self, data, labels):
    corrects, wrongs = 0, 0
    for i in range(len(data)):
    res = self.run(data[i])
    res_max = res.argmax()
    if res_max == labels[i]:
    corrects += 1
    else:
    wrongs += 1
    return corrects, wrongs
    In [ ]:
    epochs = 3
    ANN = NeuralNetwork(network_structure=[image_pixels, 80, 80, 10],
    learning_rate=0.01,
    bias=None)
    ANN.train(train_imgs, train_labels_one_hot, epochs=epochs)
    In [ ]:
    234
    corrects, wrongs = ANN.evaluate(train_imgs, train_labels)
    print("accuracy train: ", corrects / ( corrects + wrongs))
    corrects, wrongs = ANN.evaluate(test_imgs, test_labels)
    print("accuracy: test", corrects / ( corrects + wrongs))
    FOOTNOTES
    1
    Wan, Li; Matthew Zeiler; Sixin Zhang; Yann LeCun; Rob Fergus (2013). Regularization of Neural Network
    using DropConnect. International Conference on Machine Learning(ICML).