从零构建递归神经网络:仅用NumPy实现
作者:一滴水 | 来源:互联网 | 2024-12-26 11:29
尽管使用TensorFlow和PyTorch等成熟框架可以显著降低实现递归神经网络(RNN)的门槛,但对于初学者来说,理解其底层原理至关重要。本文将引导您使用NumPy从头构建一个用于自然语言处理(NLP)的RNN模型。
在现代深度学习框架如TensorFlow和PyTorch的帮助下,递归神经网络(RNN)的实现变得更加容易。然而,对于初学者而言,掌握其基本原理和内部机制是至关重要的。本文将详细介绍如何仅使用NumPy库从头开始构建一个RNN,并应用于自然语言处理任务。 ### 初始化参数 与传统神经网络不同,RNN具有三个权重参数:输入权重、内部状态权重和输出权重。这些参数需要初始化为随机值。此外,还需要设置词嵌入维度和输出维度。例如,假设词嵌入维度为100,输出维度为80(表示词汇表中的唯一词向量总数)。代码如下: ```python hidden_dim = 100 output_dim = 80 # 总词汇表中唯一的词数 input_weights = np.random.uniform(0, 1, (hidden_dim, hidden_dim)) internal_state_weights = np.random.uniform(0, 1, (hidden_dim, hidden_dim)) output_weights = np.random.uniform(0, 1, (output_dim, hidden_dim)) ``` 变量`prev_memory`表示前一时间步的内部状态,其他参数如学习率、序列长度和截断反向传播的时间戳数也需初始化。 ### 前向传播 考虑一个简单的句子“I like to play.”,其中每个单词映射到词汇表中的索引。为了展示从输入到输出的过程,我们先随机初始化每个单词的词嵌入。 ```python input_string = [2, 45, 10, 65] # 单词索引列表 embeddings = [] # 存储每个单词的嵌入向量 for i in range(len(input_string)): x = np.random.randn(hidden_dim, 1) embeddings.append(x) ``` RNN单元接受输入后,输出下一个最可能出现的单词。训练时,给定第t+1个词作为输出,将第t个词作为输入。计算损失函数所需的输出格式为独热编码(One-Hot)矢量。 ### RNN的黑箱计算 有了权重参数和输入输出,接下来进行前向传播的计算。关键公式包括输入权重乘以输入向量、内部状态权重乘以前一层的激活,以及使用tanh激活函数。 ```python def tanh_activation(Z): return np.tanh(Z) def softmax_activation(Z): e_x = np.exp(Z - np.max(Z)) return e_x / e_x.sum(axis=0) def rnn_forward(input_embedding, input_weights, internal_state_weights, prev_memory, output_weights): W_frd = np.dot(internal_state_weights, prev_memory) U_frd = np.dot(input_weights, input_embedding) sum_s = W_frd + U_frd ht_activated = tanh_activation(sum_s) yt_unactivated = np.dot(output_weights, ht_activated) yt_activated = softmax_activation(yt_unactivated) return ht_activated, yt_activated ``` ### 计算损失函数 损失函数采用交叉熵损失函数,计算公式如下: ```python def calculate_loss(output_mapper, predicted_output): total_loss = 0 for y, y_ in zip(output_mapper.values(), predicted_output): loss = -sum(y[i] * np.log2(y_[i]) for i in range(len(y))) loss /= float(len(y)) total_loss += loss return total_loss / float(len(predicted_output)) ``` ### 反向传播 反向传播通过链式法则计算梯度。对于RNN,需要计算三个梯度:输入权重、内部状态权重和输出权重的梯度。 ```python def delta_cross_entropy(predicted_output, original_t_output): grad = predicted_output.copy() for i, l in enumerate(original_t_output): if l == 1: grad[i] -= 1 return grad # 梯度计算函数 def multiplication_backward(weights, x, dz): gradient_weight = np.dot(dz, x.T) chain_gradient = np.dot(weights.T, dz) return gradient_weight, chain_gradient def add_backward(x1, x2, dz): dx1 = dz * np.ones_like(x1) dx2 = dz * np.ones_like(x2) return dx1, dx2 def tanh_activation_backward(x, top_diff): output = np.tanh(x) return (1.0 - np.square(output)) * top_diff # 单个时间戳的反向传播 def single_backprop(X, input_weights, internal_state_weights, output_weights, ht_activated, dLo, forward_params_t, diff_s, prev_s): W_frd = forward_params_t[0][0] U_frd = forward_params_t[0][1] ht_unactivated = forward_params_t[0][2] yt_unactivated = forward_params_t[0][3] dV, dsv = multiplication_backward(output_weights, ht_activated, dLo) ds = np.add(dsv, diff_s) dadd = tanh_activation_backward(ht_unactivated, ds) dmulw, dmulu = add_backward(U_frd, W_frd, dadd) dW, dprev_s = multiplication_backward(internal_state_weights, prev_s, dmulw) dU, dx = multiplication_backward(input_weights, X, dmulu) return dprev_s, dU, dW, dV # 截断反向传播 def rnn_backprop(embeddings, memory, output_t, dU, dV, dW, bptt_truncate, input_weights, output_weights, internal_state_weights): T = len(embeddings) for t in range(T-1, -1, -1): prev_s_t = np.zeros((hidden_dim, 1)) diff_s = np.zeros((hidden_dim, 1)) predictiOns= memory[f"yt{t}"] ht_activated = memory[f"ht{t}"] forward_params_t = memory[f"params{t}"] dLo = delta_cross_entropy(predictions, output_t[t]) dprev_s, dU_t, dW_t, dV_t = single_backprop(embeddings[t], input_weights, internal_state_weights, output_weights, ht_activated, dLo, forward_params_t, diff_s, prev_s_t) dU += dU_t dW += dW_t dV += dV_t if t > 0: for i in range(t-1, max(-1, t-bptt_truncate), -1): forward_params_t = memory[f"params{i}"] ht_activated = memory[f"ht{i}"] prev_s_i = np.zeros((hidden_dim, 1)) if i == 0 else memory[f"ht{prev}"] dprev_s, dU_i, dW_i, dV_i = single_backprop(embeddings[t], input_weights, internal_state_weights, output_weights, ht_activated, dLo, forward_params_t, dprev_s, prev_s_i) dU += dU_i dW += dW_i dV += dV_i return dU, dW, dV ``` ### 权重更新 使用批量梯度下降法更新权重。 ```python def gd_step(learning_rate, dU, dW, dV, input_weights, internal_state_weights, output_weights): input_weights -= learning_rate * dU internal_state_weights -= learning_rate * dW output_weights -= learning_rate * dV return input_weights, internal_state_weights, output_weights ``` ### 训练过程 完成所有步骤后,可以开始训练神经网络。训练过程中,可以选择静态或动态调整学习率。 ```python def train(T, embeddings, output_t, output_mapper, input_weights, internal_state_weights, output_weights, dU, dW, dV, prev_memory, learning_rate=0.001, nepoch=100, evaluate_loss_after=2): losses = [] for epoch in range(nepoch): if epoch % evaluate_loss_after == 0: output_string, memory = full_forward_prop(T, embeddings, input_weights, internal_state_weights, prev_memory, output_weights) loss = calculate_loss(output_mapper, output_string) losses.append(loss) print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Loss after epoch={epoch}: {loss}") dU, dW, dV = rnn_backprop(embeddings, memory, output_t, dU, dV, dW, bptt_truncate, input_weights, output_weights, internal_state_weights) input_weights, internal_state_weights, output_weights = gd_step(learning_rate, dU, dW, dV, input_weights, internal_state_weights, output_weights) return losses losses = train(T, embeddings, output_t, output_mapper, input_weights, internal_state_weights, output_weights, dU, dW, dV, prev_memory, learning_rate=0.0001, nepoch=10, evaluate_loss_after=2) ``` 恭喜!您已经成功从零构建了一个递归神经网络。接下来,可以进一步探索LSTM和GRU等更高级的架构。
推荐阅读
本文详细介绍了如何使用机器学习和深度学习技术对垃圾邮件和短信进行分类。内容涵盖从数据集介绍、预处理、特征提取到模型训练与评估的完整流程,并提供了具体的代码示例和实验结果。 ...
[详细]
蜡笔小新 2024-12-25 17:38:50
本文详细介绍TensorFlow中的数据读写操作,包括TFRecord文件的创建与读取,以及数据集(dataset)的相关概念和使用方法。 ...
[详细]
蜡笔小新 2024-12-19 16:23:17
本文汇总了2020年至2022年间《计算机学报》上发表的若干重要论文,旨在为即将投稿的研究者提供参考。 ...
[详细]
蜡笔小新 2024-11-20 11:08:21
主要用了2个类来实现的,话不多说,直接看运行结果,然后在奉上源代码1.Index.javaimportjava.awt.Color;im ...
[详细]
蜡笔小新 2024-12-27 18:18:10
本文详细介绍了如何构建一个高效的UI管理系统,集中处理UI页面的打开、关闭、层级管理和页面跳转等问题。通过UIManager统一管理外部切换逻辑,实现功能逻辑分散化和代码复用,支持多人协作开发。 ...
[详细]
蜡笔小新 2024-12-27 10:28:40
本文介绍了如何在C#中启动一个应用程序,并通过枚举窗口来获取其主窗口句柄。当使用Process类启动程序时,我们通常只能获得进程的句柄,而主窗口句柄可能为0。因此,我们需要使用API函数和回调机制来准确获取主窗口句柄。 ...
[详细]
蜡笔小新 2024-12-27 03:39:09
本文探讨了如何在给定整数N的情况下,找到两个不同的整数a和b,使得它们的和最大,并且满足特定的数学条件。 ...
[详细]
蜡笔小新 2024-12-26 19:26:18
本文详细介绍了使用Splay Tree进行区间操作的实现方法,包括插入、删除、修改、翻转和求和等操作。通过这些操作,可以高效地处理动态序列问题,并且代码实现具有一定的挑战性,有助于编程能力的提升。 ...
[详细]
蜡笔小新 2024-12-26 18:47:12
本文探讨了如何优化和正确配置Kafka Streams应用程序以确保准确的状态存储查询。通过调整配置参数和代码逻辑,可以有效解决数据不一致的问题。 ...
[详细]
蜡笔小新 2024-12-26 18:17:14
本文探讨了如何在编程中正确处理包含空数组的 JSON 对象,提供了详细的代码示例和解决方案。 ...
[详细]
蜡笔小新 2024-12-26 16:33:40
2013年5月9日,PHP官方发布了PHP 5.5.0rc1和PHP 5.4.15正式版,这两个版本均支持64位环境。本文将详细介绍Zend OPcache的功能及其在Windows环境下的配置与测试。 ...
[详细]
蜡笔小新 2024-12-26 12:56:20
本文探讨了图像标签的多种分类场景及其在以图搜图技术中的应用,涵盖了从基础理论到实际项目实施的全面解析。 ...
[详细]
蜡笔小新 2024-12-07 14:28:06
本文详细记录了作者从7月份的提前批到9、10月份正式批的秋招经历,包括各公司的面试流程、技术问题及HR面的常见问题。通过这次秋招,作者深刻体会到了技术积累和面试准备的重要性。 ...
[详细]
蜡笔小新 2024-12-06 12:48:28
本文介绍了一个使用Keras框架构建的卷积神经网络(CNN)实例,主要利用了Keras提供的MNIST数据集以及相关的层,如Dense、Dropout、Activation等,构建了一个具有两层卷积和两层全连接层的CNN模型。 ...
[详细]
蜡笔小新 2024-12-03 19:35:35
本文讨论了在处理分页数据时常见的低级错误,并提供了优化后的代码示例,以减少重复代码并提高可读性和维护性。 ...
[详细]
蜡笔小新 2024-11-28 15:27:32