# 记忆增强神经网络 到目前为止,在前面的章节中,我们已经学习了几种基于距离的度量学习算法。 我们从连体网络开始,了解了连体网络如何学会区分两个输入,然后我们研究了原型网络以及原型网络的变体,例如高斯原型网络和半原型网络。 展望未来,我们探索了有趣的匹配网络和关系网络。 在本章中,我们将学习用于一次性学习的**记忆增强神经网络**(**MANN**)。 在进入 MANN 之前,我们将了解他们的前身**神经图灵机**(**NTM**)。 我们将学习 NTM 如何使用外部存储器来存储和检索信息,并且还将看到如何使用 NTM 执行复制任务。 在本章中,我们将学习以下内容: * NTM * 在 NTM 中读写 * 寻址机制 * 使用 NTM 复制任务 * 人 * 曼语中的读写 # NTM NTM 是一种有趣的算法,能够存储和检索内存中的信息。 NTM 的想法是用外部存储器来增强神经网络-也就是说,它不是使用隐藏状态作为存储器,而是使用外部存储器来存储和检索信息。 NTM 的体系结构如下图所示: ![](img/94347b9c-5fca-4272-9de0-c6c802eb7525.png) NTM 的重要组成部分如下: * **控制器**:这基本上是前馈神经网络或循环神经网络。 它从内存中读取和写入。 * **内存**:我们将在其中存储信息的存储矩阵或存储库,或简称为存储。 内存基本上是由内存单元组成的二维矩阵。 存储器矩阵包含`N`行和`M`列。 使用控制器,我们可以从内存中访问内容。 因此,控制器从外部环境接收输入,并通过与存储矩阵进行交互来发出响应。 * **读写头**:读写头是包含必须从其读取和写入的存储器地址的指针。 好的,但是我们如何从内存中访问信息? 我们是否可以通过指定行索引和列索引来访问内存中的信息? 我们可以。 但是问题在于,如果我们按索引访问信息,则无法使用梯度下降来训练 NTM,因为我们无法计算索引的梯度。 因此,NTM 的作者定义了使用控制器进行读写的模糊操作。 模糊操作将在某种程度上与内存中的所有元素进行交互。 基本上,它是一种关注机制,主要关注内存中对读/写很重要的特定位置,而忽略了对其他位置的关注。 因此,我们使用特殊的读取和写入操作来确定要聚焦在存储器上的哪个位置。 我们将在接下来的部分中探索更多有关读写操作的信息。 # 在 NTM 中读写 现在,我们将看到如何读取和写入内存矩阵。 # 读取操作 读取操作从内存中读取一个值。 但是,由于我们的存储矩阵中有许多存储块,我们需要选择从存储器中读取哪一个? 这由权重向量确定。 权重向量指定内存中哪个区域比其他区域更重要。 我们使用一种注意力机制来获得该权重向量。 我们将在接下来的部分中进一步探讨如何精确计算此权重向量。 权重向量已归一化,这意味着其值的范围从零到一,并且值的总和等于一。 下图显示了长度的权重向量`N`: ![](img/53982af6-a5b7-4b34-bf95-f2fdb6fb121e.png) 让我们用`w[t]`表示归一化权重向量,其中下标`t`表示时间,`w[t](i)`表示权重向量中的元素,其索引为`i`,和时间`t`: ![](img/3785fb36-a7c4-401d-a42f-9b505f26a800.png) 我们的存储矩阵由`N`行和`M`列组成,如下图所示。 让我们将`t`时的存储矩阵表示`M[t]`: ![](img/3f50d7d5-cd0c-4e69-b381-d1f21dbb1683.png) 现在我们有了权重向量和存储矩阵,我们执行了存储矩阵`M[t]`和权重向量`w[t]`,以获取读取向量`r[t]`,如下图所示: ![](img/8352e643-7590-42e7-a342-dd8556ff8ca6.png) 可以表示为以下形式: ![](img/a274afdc-2759-43bc-8fe0-c3516401b0ab.png) 如上图所示,我们具有`N`行和`M`列的存储矩阵,大小为`N`的权重向量包含 所有`N`个位置。 执行这两个的线性组合,我们得到长度为`M`的读取向量。 # 写操作 与读取操作不同,写入操作由两个称为擦除和添加操作的子操作组成,这两个子操作分别擦除旧信息并将新信息添加到存储器。 # 擦除操作 我们使用擦除操作来删除内存中不需要的信息。 执行擦除操作后,我们将拥有一个新的更新的存储矩阵,其中的存储器中的某些元素将被擦除。 我们如何擦除存储矩阵中特定单元的值? 在这里,我们引入了另一种称为擦除向量![](img/592ff992-bf67-45d4-a87e-652d35f4a98a.png)的向量,其长度与权重向量![](img/aa3bf363-a0c0-412e-b71d-735cb8b7e48c.png)相同。 擦除向量由 0s 和 1s 组成。 好的。 我们有一个擦除向量。 但是,我们如何擦除值并获取更新的存储矩阵? 在上一步![](img/d894a959-d27f-4a05-a29c-2cae9c9dbfb9.png)中,我们将![](img/18040eae-c41c-44e2-869d-047472d48d28.png)与我们的存储矩阵相乘,得到更新后的存储矩阵![](img/0782ada6-0c39-4a11-ad47-eacf63563661.png)。 即,![](img/f7d6e7e3-a785-47aa-ad6b-4f4a05fed752.png)。 但这如何工作? 仅当索引为`i`的权重元素和擦除元素都为 1 时,存储器中的特定元素才会被设置为 0,换言之,被擦除; 否则,它将保留自己的价值。 例如,查看下图。 首先,我们将权重向量![](img/3bbfa27c-00fd-429b-a740-87abdd4860b4.png)和擦除向量![](img/1b7b8f56-f4ca-4c6e-bf34-a1063b93351e.png)相乘: ![](img/367864fc-07bb-46fa-9d44-9331803a5ab7.png) 然后,我们从中减去 1,即![](img/d62670b4-cda4-46ba-8912-d79e621a9d17.png),然后得到一个新的向量,如下所示: ![](img/1584c3bc-b5d9-4d86-8a49-789514b6fd58.png) 接下来,我们将![](img/18040eae-c41c-44e2-869d-047472d48d28.png)与上一个时间步![](img/d894a959-d27f-4a05-a29c-2cae9c9dbfb9.png)的存储矩阵相乘,得到更新后的存储矩阵![](img/0782ada6-0c39-4a11-ad47-eacf63563661.png): ![](img/1e27ee5e-77f2-4515-b584-491c4bf2c6a7.png) # 添加操作 完成擦除操作后,我们获得了更新的存储矩阵![](img/df9e8911-4333-4fa6-b9aa-0e6a8f4a392a.png),其中存储器中的某些元素将被擦除。 现在,我们要向存储矩阵中添加新信息。 我们该怎么做? 我们引入了另一个向量,称为加法向量![](img/cad32116-c1e4-47ee-9404-2985497832a0.png),该向量具有要添加到存储器中的值。 我们将权重向量![](img/05610d67-b12e-4cd1-868e-5c3cc338b48d.png)的元素相乘,然后将向量![](img/358a6ae6-ee7e-4822-a6e0-57ca4696b639.png)相加,然后将它们添加到内存中,即![](img/a65aa92d-ae65-4f76-a905-703888481d47.png)。 ![](img/e9792850-28d9-487d-832c-f0a4bfb2893d.png) # 寻址机制 到目前为止,我们已经了解了如何执行读写操作,还了解了如何使用权重向量执行这些操作。 但是我们如何计算这个权重向量呢? 我们使用注意力机制和不同的寻址方案来计算它。 我们使用两种寻址机制来访问内存中的信息: * 基于内容的寻址 * 基于位置的寻址 # 基于内容的寻址 在基于内容的寻址中,我们基于相似性从内存中选择值。 控制器返回一个称为![](img/ee41bbf6-c590-46e1-bdc4-df4c57f9de70.png)的密钥向量。 我们将这个关键向量![](img/998590ab-cea5-4355-81f0-d903fca5688b.png)与存储矩阵![](img/589dba56-8e10-4494-a2b2-601700da5656.png)中的每一行进行比较,以了解相似性。 我们使用余弦相似度作为检查相似度的相似度度量,可以表示为: ![](img/8f047120-7950-4ed1-955b-038fca78e39d.png) 我们引入了一个称为![](img/e46b6a16-fa8d-410e-be3f-4611970f74c2.png)的新参数,称为密钥强度。 它决定了我们的权重向量应有多集中。 基于![](img/8c1fa8e3-0378-4854-b59b-7be2a045d885.png)的值,我们可以增加或减小焦点-也就是说,我们可以基于按键强度![](img/6b225181-9edb-4e4a-8e4d-1f0adfd2088c.png)的值将注意力转移到特定位置。 当![](img/7ad200a5-9543-47ee-bb7a-8571aee7b506.png)的值较低时,我们将同等地关注所有位置; 当![](img/0c4470b7-0b16-4c51-b0a6-a79d88d951e7.png)的值较高时,我们将重点放在特定位置。 因此,我们的权重向量变为: ![](img/05e054c0-4540-4830-9db1-359c3e2f2ba1.png) 也就是说,密钥向量![](img/9e1cd03c-9808-4d5d-a46b-660b2558504c.png)和存储矩阵![](img/cb025cd6-4607-4512-be51-e5a9bb7372f8.png)之间的余弦相似度乘以密钥强度![](img/6927ea0a-bb10-4b95-a08b-467c6eb178c0.png)。 ![](img/a9ad52e5-2c92-4cbd-a981-b37f9382fadc.png)中的上标`c`表示它们是基于内容的权重。 代替直接使用它,我们对权重应用 softmax。 因此,我们的最终权重如下: ![](img/a2609108-50cd-476d-b757-fe14244827f8.png) # 基于位置的寻址 与基于内容的寻址不同,在基于位置的寻址中,我们专注于位置而不是内容相似性。 它包括三个步骤: 1. 插补 2. 卷积移位 3. 锐化 # 插补 基于位置的寻址的第一步称为插值。 它用于决定我们应该使用在上一个时间步获得的权重![](img/b65b55d1-8a36-4c42-bdb5-aaccca4b888c.png),还是使用通过基于内容的寻址获得的权重![](img/fef2781d-a19b-4486-b653-a95050a4944b.png)。 但是我们如何决定呢? 我们使用一个新的标量参数![](img/bdfa92a5-66fe-418e-9613-7d2284fe9d1b.png),该参数用于确定应使用的权重。 ![](img/bdfa92a5-66fe-418e-9613-7d2284fe9d1b.png)的值可以为 0 或 1。 我们可以表示权重向量的计算如下: ![](img/b7e0ca28-2e27-4adf-a9a0-a393e859a82e.png) * 当![](img/dbbeb354-27f5-4ed1-af8f-8e629d320701.png)的值为 0 时,我们的方程变为![](img/1f30551c-5205-4333-a337-6f6f05684377.png),这意味着我们的权重向量是我们在上一个时间步获得的权重向量。 * 当![](img/b349dd74-b13d-4982-9bde-3afb2ed36c8a.png)的值为 1 时,我们的方程变为![](img/5eccd874-c6b5-4469-b596-61888e74a561.png),这意味着我们的权重向量是我们通过基于内容的寻址获得的权重向量。 因此,![](img/1a11049a-bb4a-46d9-a4e6-9a9040ae2ff5.png)的值用作在我们必须使用的权重之间进行切换的门。 # 卷积移位 下一步称为卷积移位。 用于移动头部位置。 即,它用于将焦点从一个位置转移到另一位置。 每个磁头发出一个称为移位权重![](img/55b42a60-c235-432c-b1a2-40da0ccfdba3.png)的参数,该参数为我们提供了一个分布,在该分布上可以执行允许的整数移位。 例如,假设我们在-1 和 1 之间进行了转换,那么![](img/838da61f-2b69-45c6-a18c-f405970a8071.png)的长度将变为 3,包括`{-1, 0, 1}`。 那么,这些转变究竟意味着什么? 假设权重向量中有三个元素![](img/6f1e25f0-feea-4ea0-b690-69f4dfd45171.png)-即![](img/9fdc6124-c3ae-49ba-bcdc-20e603d86f61.png),而移位权重向量中有三个元素-![](img/1593106b-61ec-476e-979e-105bdb56dcdd.png)。 移位-1 表示我们将![](img/95f79f8e-f6c3-43d5-8d5a-74076154dd03.png)中的元素从左向右移动。 移位 0 将元素保持在相同位置,而移位+1 意味着我们将元素从右移到左。 在下图中可以看到: ![](img/dbfdaf4d-bc48-423d-9611-7e4cd02fb76d.png) 现在,看下面的图,其中我们有移位权重![](img/b6092aa7-ca75-4147-946e-c613c0a64be1.png),这意味着我们执行了左移位,因为在其他位置移位值为 0: ![](img/d8ce60f2-2f5a-4702-a999-b4da1b249d01.png) 同样,当![](img/03732dc6-f836-4130-a79a-4e30c944bda9.png)时,我们执行右移,因为在其他位置上的移位值为 0,如下图所示: ![](img/51f8b11f-7921-424a-a41f-1fcc6c03314f.png) 因此,以这种方式,我们对权重矩阵中的元素执行卷积移位。 如果我们将 0 到`N-1`个存储位置,则可以表示卷积移位如下: ![](img/1bb69aaa-0933-48bc-9f40-90bf6c2819a3.png) # 锐化 最后一步称为锐化。 卷积移位的结果是,权重![](img/f1565868-0d7c-488c-8836-654258fbc4c8.png)不会很尖锐,换句话说,由于移位,聚焦在单个位置的权重将分散到其他位置。 为了减轻这种影响,我们执行锐化。 我们使用一个称为![](img/7421eba6-f3ad-4e05-8151-647951036419.png)的新参数,该参数应大于或等于 1 以进行锐化,并且可以表示为: ![](img/99d7e67e-ddd1-4966-b2e5-3196b2bc88c0.png) # 使用 NTM 复制任务 现在,我们将看到如何使用 NTM 执行复制任务。 复制任务的目的是了解 NTM 如何存储和调用任意长度的序列。 我们将为网络提供一个随机序列,以及一个指示序列结束的标记。 它必须学习输出给定的输入序列。 因此,网络会将输入序列存储在内存中,然后从内存中回读。 现在,我们将逐步了解如何执行复制任务,然后在最后看到整个最终代码。 [您还可以在此处查看 Jupyter Notebook 中提供的代码,并附带说明](https://github.com/sudharsan13296/Hands-On-Meta-Learning-With-Python/blob/master/05.%20Memory%20Augmented%20Networks/5.4%20Copy%20Task%20Using%20NTM.ipynb)。 首先,我们将了解如何实现 NTM 单元。 而不是查看整个代码,我们将逐行查看它。 我们定义`NTMCell`类,在其中实现整个 NTM 单元: ```py class NTMCell(): ``` 首先,我们定义`init`函数,在其中初始化所有变量: ```py def __init__(self, rnn_size, memory_size, memory_vector_dim, read_head_num, write_head_num, addressing_mode='content_and_location', shift_range=1, reuse=False, output_dim=None): #initialize all the variables self.rnn_size = rnn_size self.memory_size = memory_size self.memory_vector_dim = memory_vector_dim self.read_head_num = read_head_num self.write_head_num = write_head_num self.addressing_mode = addressing_mode self.reuse = reuse self.step = 0 self.output_dim = output_dim self.shift_range = shift_range #initialize controller as the basic rnn cell self.controller = tf.nn.rnn_cell.BasicRNNCell(self.rnn_size) ``` 接下来,我们定义`__call__`方法,在其中实现 NTM 操作: ```py def __call__(self, x, prev_state): ``` 我们通过将`x`输入与先前读取的向量列表组合来获得控制器输入: ```py prev_read_vector_list = prev_state['read_vector_list'] prev_controller_state = prev_state['controller_state'] controller_input = tf.concat([x] + prev_read_vector_list, axis=1) ``` 我们通过输入`controller_input`和`prev_controller_state`作为输入来构建控制器,即 RNN 单元: ```py with tf.variable_scope('controller', reuse=self.reuse): controller_output, controller_state = self.controller(controller_input, prev_controller_state) ``` 现在,我们初始化读写头: ```py num_parameters_per_head = self.memory_vector_dim + 1 + 1 + (self.shift_range * 2 + 1) + 1 num_heads = self.read_head_num + self.write_head_num total_parameter_num = num_parameters_per_head * num_heads + self.memory_vector_dim * 2 * self.write_head_num ``` 接下来,我们初始化权重矩阵并进行偏置并使用前馈操作计算参数: ```py with tf.variable_scope("o2p", reuse=(self.step > 0) or self.reuse): o2p_w = tf.get_variable('o2p_w', [controller_output.get_shape()[1], total_parameter_num], initializer=tf.random_normal_initializer(mean=0.0, stddev=0.5)) o2p_b = tf.get_variable('o2p_b', [total_parameter_num], initializer=tf.random_normal_initializer(mean=0.0, stddev=0.5)) parameters = tf.nn.xw_plus_b(controller_output, o2p_w, o2p_b) ``` ```py head_parameter_list = tf.split(parameters[:, :num_parameters_per_head * num_heads], num_heads, axis=1) erase_add_list = tf.split(parameters[:, num_parameters_per_head * num_heads:], 2 * self.write_head_num, axis=1) ``` 接下来,我们获得先前的权重向量和先前的内存: ```py #previous weight vector prev_w_list = prev_state['w_list'] #previous memory prev_M = prev_state['M'] w_list = [] p_list = [] ``` 现在,我们将初始化一些用于寻址的重要参数: ```py for i, head_parameter in enumerate(head_parameter_list): #key vector k = tf.tanh(head_parameter[:, 0:self.memory_vector_dim]) #key strength(beta) beta = tf.sigmoid(head_parameter[:, self.memory_vector_dim]) * 10 #interpolation gate g = tf.sigmoid(head_parameter[:, self.memory_vector_dim + 1]) #shift matrix s = tf.nn.softmax( head_parameter[:, self.memory_vector_dim + 2:self.memory_vector_dim + 2 + (self.shift_range * 2 + 1)] ) #sharpening factor gamma = tf.log(tf.exp(head_parameter[:, -1]) + 1) + 1 with tf.variable_scope('addressing_head_%d' % i): w = self.addressing(k, beta, g, s, gamma, prev_M, prev_w_list[i]) w_list.append(w) p_list.append({'k': k, 'beta': beta, 'g': g, 's': s, 'gamma': gamma}) ``` **读取操作:** 选择读取头,如下所示: ```py read_w_list = w_list[:self.read_head_num] ``` 我们知道`read`操作是权重和内存的线性组合: ```py read_vector_list = [] for i in range(self.read_head_num): #linear combination of the weights and memory read_vector = tf.reduce_sum(tf.expand_dims(read_w_list[i], dim=2) * prev_M, axis=1) read_vector_list.append(read_vector) ``` **写入操作:** 与读取操作不同,写入操作涉及擦除和添加两个步骤。 选择要写入的头,如下所示: ```py write_w_list = w_list[self.read_head_num:] #update the memory M = prev_M ``` 执行擦除和添加操作: ```py for i in range(self.write_head_num): #the erase vector will be multipled with weight vector to denote which location to erase or keep unchanged w = tf.expand_dims(write_w_list[i], axis=2) erase_vector = tf.expand_dims(tf.sigmoid(erase_add_list[i * 2]), axis=1) #next we perform the add operation add_vector = tf.expand_dims(tf.tanh(erase_add_list[i * 2 + 1]), axis=1) M = M * (tf.ones(M.get_shape()) - tf.matmul(w, erase_vector)) + tf.matmul(w, add_vector) ``` 获取控制器输出: ```py if not self.output_dim: output_dim = x.get_shape()[1] else: output_dim = self.output_dim with tf.variable_scope("o2o", reuse=(self.step > 0) or self.reuse): o2o_w = tf.get_variable('o2o_w', [controller_output.get_shape()[1], output_dim], initializer=tf.random_normal_initializer(mean=0.0, stddev=0.5)) o2o_b = tf.get_variable('o2o_b', [output_dim], initializer=tf.random_normal_initializer(mean=0.0, stddev=0.5)) NTM_output = tf.nn.xw_plus_b(controller_output, o2o_w, o2o_b) state = { 'controller_state': controller_state, 'read_vector_list': read_vector_list, 'w_list': w_list, 'p_list': p_list, 'M': M } self.step += 1 ``` **寻址机制:** 众所周知,我们使用两种寻址方式:基于内容的寻址和基于位置的寻址。 **基于内容的寻址:** 计算关键向量和存储矩阵之间的余弦相似度: ```py k = tf.expand_dims(k, axis=2) inner_product = tf.matmul(prev_M, k) k_norm = tf.sqrt(tf.reduce_sum(tf.square(k), axis=1, keepdims=True)) M_norm = tf.sqrt(tf.reduce_sum(tf.square(prev_M), axis=2, keepdims=True)) norm_product = M_norm * k_norm K = tf.squeeze(inner_product / (norm_product + 1e-8)) ``` 现在,我们根据相似度和关键强度(beta)生成归一化的权重向量。 Beta 用于调整头部聚焦的精度: ```py K_amplified = tf.exp(tf.expand_dims(beta, axis=1) * K) w_c = K_amplified / tf.reduce_sum(K_amplified, axis=1, keepdims=True) # eq (5) ``` **基于位置的寻址:** 基于位置的寻址涉及其他三个步骤: 1. 插补 2. 卷积移位 3. 锐化 **插值:** 这用于决定我们应该使用在上一个时间步获得的权重`prev_w`还是使用通过基于内容的寻址获得的权重`w_c`。 但是我们如何决定呢? 我们使用一个新的标量参数`g`,该参数用于确定应使用的权重: ```py g = tf.expand_dims(g, axis=1) w_g = g * w_c + (1 - g) * prev_w ``` **卷积移位:** 插值后,我们执行卷积移位,以便控制器可以专注于其他行: ```py s = tf.concat([s[:, :self.shift_range + 1], tf.zeros([s.get_shape()[0], self.memory_size - (self.shift_range * 2 + 1)]), s[:, -self.shift_range:]], axis=1) t = tf.concat([tf.reverse(s, axis=[1]), tf.reverse(s, axis=[1])], axis=1) s_matrix = tf.stack( [t[:, self.memory_size - i - 1:self.memory_size * 2 - i - 1] for i in range(self.memory_size)], axis=1 ) w_ = tf.reduce_sum(tf.expand_dims(w_g, axis=1) * s_matrix, axis=2) # eq (8) ``` **锐化:** 最后,我们执行锐化操作以防止偏移的权重向量模糊: ```py w_sharpen = tf.pow(w_, tf.expand_dims(gamma, axis=1)) w = w_sharpen / tf.reduce_sum(w_sharpen, axis=1, keepdims=True) ``` 接下来,我们定义一个名为`zero_state`的函数,用于初始化控制器的所有状态,读取向量,权重和内存: ```py def zero_state(self, batch_size, dtype): def expand(x, dim, N): return tf.concat([tf.expand_dims(x, dim) for _ in range(N)], axis=dim) with tf.variable_scope('init', reuse=self.reuse): state = { 'controller_state': expand(tf.tanh(tf.get_variable('init_state', self.rnn_size, initializer=tf.random_normal_initializer(mean=0.0, stddev=0.5))), dim=0, N=batch_size), 'read_vector_list': [expand(tf.nn.softmax(tf.get_variable('init_r_%d' % i, [self.memory_vector_dim], initializer=tf.random_normal_initializer(mean=0.0, stddev=0.5))), dim=0, N=batch_size) for i in range(self.read_head_num)], 'w_list': [expand(tf.nn.softmax(tf.get_variable('init_w_%d' % i, [self.memory_size], initializer=tf.random_normal_initializer(mean=0.0, stddev=0.5))), dim=0, N=batch_size) if self.addressing_mode == 'content_and_loaction' else tf.zeros([batch_size, self.memory_size]) for i in range(self.read_head_num + self.write_head_num)], 'M': expand(tf.tanh(tf.get_variable('init_M', [self.memory_size, self.memory_vector_dim], initializer=tf.random_normal_initializer(mean=0.0, stddev=0.5))), dim=0, N=batch_size) } return state ``` 接下来,我们定义一个名为`generate_random_strings`的函数,该函数会生成一个长度为`seq_length`的随机序列,并将这些序列馈送到复制任务的 NTM 输入: ```py def generate_random_strings(batch_size, seq_length, vector_dim): return np.random.randint(0, 2, size=[batch_size, seq_length, vector_dim]).astype(np.float32) ``` 现在,我们创建`NTMCopyModel`以执行整个复制任务: ```py class NTMCopyModel(): def __init__(self, args, seq_length, reuse=False): #input sequence self.x = tf.placeholder(name='x', dtype=tf.float32, shape=[args.batch_size, seq_length, args.vector_dim]) #output sequence self.y = self.x #end of the sequence eof = np.zeros([args.batch_size, args.vector_dim + 1]) eof[:, args.vector_dim] = np.ones([args.batch_size]) eof = tf.constant(eof, dtype=tf.float32) zero = tf.constant(np.zeros([args.batch_size, args.vector_dim + 1]), dtype=tf.float32) if args.model == 'LSTM': def rnn_cell(rnn_size): return tf.nn.rnn_cell.BasicLSTMCell(rnn_size, reuse=reuse) cell = tf.nn.rnn_cell.MultiRNNCell([rnn_cell(args.rnn_size) for _ in range(args.rnn_num_layers)]) elif args.model == 'NTM': cell = NTMCell(args.rnn_size, args.memory_size, args.memory_vector_dim, 1, 1, addressing_mode='content_and_location', reuse=reuse, output_dim=args.vector_dim) #initialize all the states state = cell.zero_state(args.batch_size, tf.float32) self.state_list = [state] for t in range(seq_length): output, state = cell(tf.concat([self.x[:, t, :], np.zeros([args.batch_size, 1])], axis=1), state) self.state_list.append(state) #get the output and states output, state = cell(eof, state) self.state_list.append(state) self.o = [] for t in range(seq_length): output, state = cell(zero, state) self.o.append(output[:, 0:args.vector_dim]) self.state_list.append(state) self.o = tf.sigmoid(tf.transpose(self.o, perm=[1, 0, 2])) eps = 1e-8 #calculate loss as cross entropy loss self.copy_loss = -tf.reduce_mean(self.y * tf.log(self.o + eps) + (1 - self.y) * tf.log(1 - self.o + eps)) #optimize using RMS prop optimizer with tf.variable_scope('optimizer', reuse=reuse): self.optimizer = tf.train.RMSPropOptimizer(learning_rate=args.learning_rate, momentum=0.9, decay=0.95) gvs = self.optimizer.compute_gradients(self.copy_loss) capped_gvs = [(tf.clip_by_value(grad, -10., 10.), var) for grad, var in gvs] self.train_op = self.optimizer.apply_gradients(capped_gvs) self.copy_loss_summary = tf.summary.scalar('copy_loss_%d' % seq_length, self.copy_loss) ``` 我们使用以下命令重置 TensorFlow 图: ```py tf.reset_default_graph() ``` 然后,我们将所有参数定义如下: ```py parser = argparse.ArgumentParser() parser.add_argument('--mode', default="train") parser.add_argument('--restore_training', default=False) parser.add_argument('--test_seq_length', type=int, default=5) parser.add_argument('--model', default="NTM") parser.add_argument('--rnn_size', default=16) parser.add_argument('--rnn_num_layers', default=3) parser.add_argument('--max_seq_length', default=5) parser.add_argument('--memory_size', default=16) parser.add_argument('--memory_vector_dim', default=5) parser.add_argument('--batch_size', default=5) parser.add_argument('--vector_dim', default=8) parser.add_argument('--shift_range', default=1) parser.add_argument('--num_epoches', default=100) parser.add_argument('--learning_rate', default=1e-4) parser.add_argument('--save_dir', default= os.getcwd()) parser.add_argument('--tensorboard_dir', default=os.getcwd()) args = parser.parse_args(args = []) ``` 最后,我们定义`training`函数: ```py def train(args): model_list = [NTMCopyModel(args, 1)] for seq_length in range(2, args.max_seq_length + 1): model_list.append(NTMCopyModel(args, seq_length, reuse=True)) with tf.Session() as sess: if args.restore_training: saver = tf.train.Saver() ckpt = tf.train.get_checkpoint_state(args.save_dir + '/' + args.model) saver.restore(sess, ckpt.model_checkpoint_path) else: saver = tf.train.Saver(tf.global_variables()) tf.global_variables_initializer().run() #initialize summary writer for visualizing in tensorboard train_writer = tf.summary.FileWriter(args.tensorboard_dir, sess.graph) plt.ion() plt.show() for b in range(args.num_epoches): #initialize the sequence length seq_length = np.random.randint(1, args.max_seq_length + 1) model = model_list[seq_length - 1] #generate our random input sequence as an input x = generate_random_strings(args.batch_size, seq_length, args.vector_dim) #feed our input to the model feed_dict = {model.x: x} if b % 100 == 0: p = 0 print("First training batch sample",x[p, :, :]) #compute model output print("Model output",sess.run(model.o, feed_dict=feed_dict)[p, :, :]) state_list = sess.run(model.state_list, feed_dict=feed_dict) if args.model == 'NTM': w_plot = [] M_plot = np.concatenate([state['M'][p, :, :] for state in state_list]) for state in state_list: w_plot.append(np.concatenate([state['w_list'][0][p, :], state['w_list'][1][p, :]])) #plot the weight matrix to see the attention plt.imshow(w_plot, interpolation='nearest', cmap='gray') plt.draw() plt.pause(0.001) #compute loss copy_loss = sess.run(model.copy_loss, feed_dict=feed_dict) #write to summary merged_summary = sess.run(model.copy_loss_summary, feed_dict=feed_dict) train_writer.add_summary(merged_summary, b) print('batches %d, loss %g' % (b, copy_loss)) else: sess.run(model.train_op, feed_dict=feed_dict) #save the model if b % 5000 == 0 and b > 0: saver.save(sess, args.save_dir + '/' + args.model + '/model.tfmodel', global_step=b) ``` 然后,我们开始使用以下命令训练 NTM: ```py train(args) ``` 我们可以看到输出如下,其中可以看到注意力集中在权重矩阵上: ![](img/a7da2ffa-7dad-4de4-b5ea-8923d5b6951c.png) # 记忆增强神经网络(MANN) 现在,我们将看到一个有趣的 NTM 变体,称为 MANN。 它广泛用于一键式学习任务。 MANN 旨在使 NTM 在一次性学习任务中表现更好。 我们知道 NTM 可以使用基于内容的寻址或基于位置的寻址。 但是在 MANN 中,我们仅使用基于内容的寻址。 MANN 使用一种称为最少最近访问的新寻址方案。 顾名思义,它写入最近最少使用的内存位置。 等待。 什么? 我们刚刚了解到 MANN 不是基于位置的,那么为什么我们要写入最近最少使用的位置? 这是因为最近最少使用的存储位置由读取操作确定,而读取操作由基于内容的寻址执行。 因此,我们基本上执行基于内容的寻址,以读取和写入最近最少使用的位置。 # 读写操作 现在,我们将看到如何在 MANN 中执行读写操作以及它们与 NTM 的区别。 # 读取操作 与 NTM 不同,在 MANN 中,我们使用两个不同的权重向量执行读取和写入操作。 MANN 中的读取操作与 NTM 相同。 因为我们知道,在 MANN 中,我们使用基于内容的相似度执行读取操作,所以我们将控制器发出的键向量![](img/46e98f00-cd89-4b44-8c8e-316c13664833.png)与存储矩阵![](img/8558460d-fdb9-4899-b674-dced43c22791.png)中的每一行进行比较,以了解相似度 。 我们使用余弦相似度作为检查相似度的相似度度量,可以表示为: ![](img/dc111894-ccf1-40ad-9b69-b12463c44ab6.png) 因此,我们的权重向量变为: ![](img/c820a84a-3694-4a00-af7b-8442b29b3142.png) 但是,与 NTM 不同,我们在这里不使用键强度![](img/34015b39-fbe2-4901-9e42-63bd1c13f7af.png)。 ![](img/b83f722d-0a81-46ed-ad74-81f2f7ffeddc.png)中的上标`r`表示它是读取的权重向量。 我们最终的权重向量是权重上的 softmax,即: ![](img/72b85ed0-fa1d-43a9-a6e8-e275cf545633.png) 我们的读取向量是权重![](img/1885f025-972d-43be-8fff-6b383df8273b.png)和存储矩阵![](img/8bc8c170-5374-4326-b71f-8883e9b48765.png)的线性组合,如下所示: ![](img/cb1325d6-db2e-4bef-b2e6-7d4baf7e1cae.png) 让我们看看如何在 TensorFlow 中构建它。 首先,我们使用基于内容的相似度计算读取权重向量: ```py def read_head_addressing(k, prev_M): k = tf.expand_dims(k, axis=2) inner_product = tf.matmul(prev_M, k) k_norm = tf.sqrt(tf.reduce_sum(tf.square(k), axis=1, keep_dims=True)) M_norm = tf.sqrt(tf.reduce_sum(tf.square(prev_M), axis=2, keep_dims=True)) norm_product = M_norm * k_norm K = tf.squeeze(inner_product / (norm_product + 1e-8)) K_exp = tf.exp(K) w = K_exp / tf.reduce_sum(K_exp, axis=1, keep_dims=True) return w ``` 然后,我们获得读取的权重向量: ```py w_r = read_head_addressing(k, prev_M) ``` 我们执行读取操作,这是读取的权重向量和内存的线性组合: ```py read_vector_list = [] with tf.variable_scope('reading'): for i in range(self.head_num): read_vector = tf.reduce_sum(tf.expand_dims(w_r_list[i], dim=2) * M, axis=1) read_vector_list.append(read_vector) ``` # 写操作 在执行写操作之前,我们要找到最近最少使用的内存位置,因为这是我们必须写的位置。 我们如何找到最近最少使用的内存位置? 为了找到这一点,我们计算了一个新的向量,称为使用权重向量。 它由![](img/482205b9-592e-4efa-9bf8-025e8373b7d1.png)表示,并将在每个读取和写入步骤之后进行更新。 它只是读取权重向量和写入权重向量的总和,即![](img/7e11a466-e9a2-4233-868f-36393c249afe.png)。 除了添加读取和权重向量外,我们还通过添加衰减的先前使用权重向量![](img/e93c91a8-63c0-43d0-bff0-35185cf50ce0.png)来更新使用权重向量。 我们使用称为![](img/350feb5d-35bf-4594-9311-362e684a6350.png)的衰减参数,该参数用于确定以前的使用权重必须如何衰减。 因此,我们最终的使用权重向量是衰减的先前使用权重向量,读取权重向量和写入权重向量的总和: ![](img/5652885b-2476-4d25-a5c8-db704d656211.png) 现在我们已经计算了使用权重向量,如何计算最近最少使用的位置? 为此,我们引入了另一个权重向量,称为最不常用的权重向量![](img/73fb4317-5188-4ad7-bd53-4c981974788e.png)。 从使用权重向量![](img/d1371973-a42c-4bdc-8efb-770a6e213fef.png)计算最少使用的权重向量![](img/870d2425-20f7-489c-83d2-51043b4b3253.png)非常简单。 我们只需将使用权重向量中的最低值的索引设置为 1,将其余值设置为 0,因为使用权重向量中的最低值意味着它最近最少使用: ![](img/0a6d7a21-f2e4-41f6-b506-4487a851b36e.png) 好的,接下来是什么? 我们已经计算出最少使用的权重向量。 现在,我们如何计算写权重向量![](img/86f0f5f7-3fb9-421f-b148-786d107e2989.png)? 我们使用 S 形门计算写入权重向量,它用于计算先前读取的权重向量![](img/8214eee0-6a22-4d28-9090-f3a4e9cc71f9.png)和先前最少使用的权重向量![](img/e6979f22-2982-4833-a17e-e1a7829b0de8.png)的凸组合: ![](img/453b7cfc-2e44-4d2a-aa5f-cc8167952005.png) 在计算写权重向量之后,我们最终更新我们的存储矩阵: ![](img/81e47382-03c1-43c5-b0c8-04f333c3a518.png) 我们将看到如何在 TensorFlow 中构建它。 我们计算使用权重向量: ```py w_u = self.gamma * prev_w_u + tf.add_n(w_r_list) + tf.add_n(w_w_list) ``` 然后,我们计算最少使用的权重向量: ```py def least_used(w_u): _, indices = tf.nn.top_k(w_u, k=self.memory_size) w_lu = tf.reduce_sum(tf.one_hot(indices[:, -self.head_num:], depth=self.memory_size), axis=1) return indices, w_lu ``` 我们存储先前的索引和先前最少使用的权重向量: ```py prev_indices, prev_w_lu = least_used(prev_w_u) ``` 我们计算写权重向量: ```py def write_head_addressing(sig_alpha, prev_w_r, prev_w_lu): return sig_alpha * prev_w_r + (1\. - sig_alpha) * prev_w_lu ``` 然后,我们更新内存: ```py M_ = prev_M * tf.expand_dims(1\. - tf.one_hot(prev_indices[:, -1], self.memory_size), dim=2) ``` 我们执行写操作: ```py M = M_ with tf.variable_scope('writing'): for i in range(self.head_num): w = tf.expand_dims(w_w_list[i], axis=2) k = tf.expand_dims(k_list[i], axis=1) M = M + tf.matmul(w, k) ``` # 概要 我们看到了神经图灵机如何从内存中存储和检索信息,以及它如何使用不同的寻址机制(例如基于位置和基于内容的寻址)来读写信息。 我们还学习了如何使用 TensorFlow 实施 NTM 以执行复制任务。 然后,我们了解了 MANN 以及 MANN 与 NTM 的不同之处。 我们还了解了 MANN 如何使用最近最少使用的访问方法来克服 NTM 的缺点。 在下一章中,我们将学习**模型不可知元学习**(**MAML**)以及如何在监督和强化学习环境中使用它。 # 问题 1. 什么是 NTM? 2. NTM 中的控制器是什么? 3. 为什么我们使用读写头? 4. 什么叫记忆? 5. NTM 中使用哪些不同类型的寻址机制? 6. 什么叫插补门? 7. 如何从使用权重向量中计算出最少使用的权重向量? # 进一步阅读 * [NTM 论文](https://arxiv.org/pdf/1410.5401.pdf) * [使用记忆增强神经网络的一次性学习](https://arxiv.org/pdf/1605.06065.pdf)