diff --git a/parameters.pkl b/parameters.pkl new file mode 100644 index 0000000..6472348 Binary files /dev/null and b/parameters.pkl differ diff --git a/神经网络/main.py b/神经网络/main.py index 57cb471..00bd4d1 100644 --- a/神经网络/main.py +++ b/神经网络/main.py @@ -34,7 +34,6 @@ class NeuralNetwork: :param epsilon: 极小值,默认为1e-9 """ print("正在初始化神经网络...", end="") - if not ( len(structure) >= 3 and all(x >= 1 if isinstance(x, int) else False for x in structure) @@ -55,66 +54,18 @@ class NeuralNetwork: # 神经网络层数(定义,第0层为输入层,第l层为隐含层(l=1,2,...,L-1),第L层为输出层(L为神经网络层数),深度为L+1) self.layer_counts = len(structure) - 1 - # 初始化是否训练 - self.training = True - - numpy.random.seed(seed) # 设置随机种子 - - self.parameters = {0: {}} # 初始化神经网络参数 - for layer_index in range(1, self.layer_counts + 1): - self.parameters[layer_index] = { - "activate": ( - activate := ( - self.output_activate - if layer_index == self.layer_counts - else self.hidden_activate - ) - ), # 激活函数 - "weight": self._init_weight( - activate=activate, - previous_layer_neuron_counts=self.structure[layer_index - 1], - current_layer_neuron_counts=( - current_layer_neuron_counts := self.structure[layer_index] - ), - ), # 权重,维度为[当前层神经元数,上一层神经元数],适配加权输入=权重*输入+平移 - "bias": numpy.zeros((current_layer_neuron_counts, 1)), # 平移 - } + self.parameters = {} + # 初始化模式(包括训练模式和推理模式) + self.training = None + # 初始化随机种子 + self.seed = seed + # 初始化极小值 self.epsilon = epsilon print("已完成") - def _init_weight( - self, - activate: Literal["relu", "linear", "softmax"], - previous_layer_neuron_counts: int, - current_layer_neuron_counts: int, - ) -> numpy.floating: - """ - 初始化权重 - :param activate: 激活函数 - :param previous_layer_neuron_counts: 上一层神经元数 - :param current_layer_neuron_counts: 当前层神经元数 - :return: 初始化后的权重,维度为[当前层神经元数,上一层神经元数] - """ - weight = numpy.random.randn( - current_layer_neuron_counts, previous_layer_neuron_counts - ) - match activate: - case "relu": - return weight * numpy.sqrt( - 2 / previous_layer_neuron_counts - ) # 使用He初始化权重方法 - case "linear": - return weight * numpy.sqrt( - 2 / previous_layer_neuron_counts - ) # 使用He初始化权重方法 - case "softmax": - return weight * numpy.sqrt( - 2 / (previous_layer_neuron_counts + current_layer_neuron_counts) - ) # 使用Xavier初始化权重方法 - def train( self, X: numpy.ndarray, @@ -145,7 +96,10 @@ class NeuralNetwork: raise RuntimeError( "输入和真实输出应为数组,其中输入维度应为[输入神经元数, 样本数],真实输出维度应为[输出神经元数, 样本数],样本数应需相同" ) + # 默认为训练模式 self.training = True + # 初始化神经网络参数 + self._init_parameters() # 归一化输入 self.parameters[0].update({"activation": self._normalize(input=X)}) @@ -174,12 +128,80 @@ class NeuralNetwork: epoch += 1 - try: - with open("neural_network.pkl", "wb") as file: - pickle.dump(self, file, protocol=pickle.HIGHEST_PROTOCOL) - print(f"模型保存成功") - except Exception as exception: - raise RuntimeError(f"模型保存失败:{str(exception)}") from exception + # 保存神经网络参数 + self._save_parameters() + + def _init_parameters(self) -> None: + """ + 初始化神经网络参数 + :return: 无 + """ + self.parameters = {0: {}} + # 初始化神经网络参数 + for layer_index in range(1, self.layer_counts + 1): + self.parameters[layer_index] = { + "activate": ( + activate := ( + self.output_activate + if layer_index == self.layer_counts + else self.hidden_activate + ) + ), # 激活函数 + "weight": self._init_weight( + activate=activate, + previous_layer_neuron_counts=self.structure[layer_index - 1], + current_layer_neuron_counts=( + current_layer_neuron_counts := self.structure[layer_index] + ), + ), # 权重,维度为[当前层神经元数,上一层神经元数],适配加权输入=权重*输入+平移 + "bias": self._init_bias( + current_layer_neuron_counts=current_layer_neuron_counts + ), # 平移 + } + + def _init_weight( + self, + activate: str, + previous_layer_neuron_counts: int, + current_layer_neuron_counts: int, + ) -> numpy.ndarray: # pyright: ignore[reportReturnType] + """ + 初始化权重 + :param activate: 激活函数 + :param previous_layer_neuron_counts: 上一层神经元数 + :param current_layer_neuron_counts: 当前层神经元数 + :return: 初始化后的权重,维度为[当前层神经元数,上一层神经元数] + """ + # 设置随机种子 + numpy.random.seed(self.seed) + # 基于正态分布生成权重 + weight = numpy.random.randn( + current_layer_neuron_counts, previous_layer_neuron_counts + ) + match activate: + case "relu": + return weight * numpy.sqrt( + 2 / previous_layer_neuron_counts + ) # 使用He初始化权重方法 + case "linear": + return weight * numpy.sqrt( + 2 / previous_layer_neuron_counts + ) # 使用He初始化权重方法 + case "softmax": + return weight * numpy.sqrt( + 2 / (previous_layer_neuron_counts + current_layer_neuron_counts) + ) # 使用Xavier初始化权重方法 + + def _init_bias( + self, + current_layer_neuron_counts: int, + ) -> numpy.ndarray: + """ + 初始化平移 + :param current_layer_neuron_counts: 当前层神经元数 + :return: 初始化后的平移,维度为[当前层神经元数, 1] + """ + return numpy.zeros((current_layer_neuron_counts, 1)) def _normalize( self, @@ -206,7 +228,7 @@ class NeuralNetwork: def _forward_propagate(self) -> None: """ 前向传播 - :return: 输出层的输出预测,维度为[输出神经元数, 样本数] + :return: 输出层的预测输出,维度为[输出神经元数, 样本数] """ for layer_index in range(1, self.layer_counts + 1): self.parameters[layer_index].update( @@ -229,9 +251,9 @@ class NeuralNetwork: def _activate( self, - activate: Literal["relu", "linear", "softmax"], + activate: str, input: numpy.ndarray, - ) -> numpy.ndarray: + ) -> numpy.ndarray: # pyright: ignore[reportReturnType] """ 激活 :param activate: 激活函数 @@ -385,23 +407,66 @@ class NeuralNetwork: } ) - def _reason(self, input: numpy.ndarray) -> numpy.ndarray: + def _save_parameters(self) -> None: + """ + 保存神经网络参数 + :return: 无 + """ + with open("parameters.pkl", "wb") as file: + pickle.dump( + obj=self.parameters, + file=file, + protocol=pickle.HIGHEST_PROTOCOL, + ) + + def reason(self, X: numpy.ndarray) -> numpy.ndarray: """ 推理 - :param input: 输入,维度为[输入神经元数, 样本数] - :return: 输出,维度为[输出神经元数, 样本数] + :param X: 输入,维度为[输入神经元数, 样本数] + :return: 预测输出,维度为[输出神经元数, 样本数] """ + print(f"基于已训练神经网络进行推理...") + if not ( + X.shape[0] == self.structure[0] if isinstance(X, numpy.ndarray) else False + ): + raise RuntimeError("输入应为数组,输入维度应为[输入神经元数, 样本数]") + # 默认为推理模式 self.training = False + # 加载神经网络参数 + self._load_parameters() # 归一化输入 - self.parameters[0].update({"activation": self._normalize(input=input)}) + self.parameters[0].update({"activation": self._normalize(input=X)}) - return self._forward_propagate() + # 前向传播 + self._forward_propagate() + + return self.parameters[self.layer_counts]["activation"] + + def _load_parameters(self) -> None: + """ + 加载神经网络参数 + :return: 无 + """ + with open("parameters.pkl", "rb") as file: + self.parameters = pickle.load(file=file) + # 校验神经网络参数 + for layer_index in range(1, self.layer_counts + 1): + if not ( + self.parameters[layer_index]["weight"].shape + == (self.structure[layer_index], self.structure[layer_index - 1]) + and self.parameters[layer_index]["bias"].shape + == (self.structure[layer_index], 1) + if isinstance(self.parameters[layer_index]["weight"], numpy.ndarray) + and isinstance(self.parameters[layer_index]["bias"], numpy.ndarray) + else False + ): + raise RuntimeError("神经网络参数中权重和偏置的维度与神经网络结构不匹配") # 测试代码 if __name__ == "__main__": - X = numpy.random.randn(2, 100) + X = numpy.random.randn(2, 5000) # 真实函数:y = 2*x1 + 3*x2 + 1 y_true = 2 * X[0:1, :] ** 2 + 3 * X[1:2, :] + 1 @@ -412,5 +477,9 @@ if __name__ == "__main__": # 训练 neural_network.train( - X=X, y_true=y_true, target_loss=0.1, epochs=1_000, learning_rate=0.05 + X=X, y_true=y_true, target_loss=0.01, epochs=1_000, learning_rate=0.1 ) + + print(f"推理结果:{y_true[:, 0:5]}") + + print(f"推理结果:{neural_network.reason(X=X)[:, 0:5]}")