深入探究 DNC:从原理到 TensorFlow 实现
1. DNC 无干扰写入机制
NTM 存在无法确保无干扰写入行为的局限性。为解决此问题,可设计架构使其专注于单个空闲内存位置,而非依赖 NTM 自行学习。这里引入了“使用向量”(usage vector)$u_t$,其大小为 $N$,每个元素值在 0 到 1 之间,表示对应内存位置的使用程度,0 表示完全空闲,1 表示完全使用。初始时 $u_0 = 0$,并随步骤更新使用信息。
为确定权重应重点关注的位置,需对使用向量排序,得到按使用程度升序排列的位置索引列表,即“空闲列表”$\phi_t$。基于此,可构建“分配权重”$a_t$,计算公式如下:
[
a_{t,\phi_t(j)} = (1 - u_{t,\phi_t(j)}) \prod_{i = 1}^{j - 1} u_{t,\phi_t(i)}
]
其中 $j \in {1, \cdots, N}$。
以 $u_t = [1, 0.7, 0.2, 0.4]$ 为例,经计算可得 $a_t = [0, 0.024, 0.8, 0.12]$。从计算中可知,$1 - u_{t,\phi_t(j)}$ 使位置权重与空闲程度成正比,随着在空闲列表中迭代,$\prod_{i = 1}^{j - 1} u_{t,\phi_t(j)}$ 逐渐变小,进一步降低了使用程度高的位置的权重,从而保证了能专注于单个位置,提高了可靠性并加快训练时间。
结合分配权重 $a_t$ 和基于内容寻址机制得到的查找权重 $c_{t}^w$($c_{t}^w = \mathcal{F}(M_{t - 1}, k_{t}^w, \beta_{t}^w)$,其中 $k_{t}^w$ 和 $\beta_{t}^w$ 是通过接口向量接收的查找键和查找强度),可构建最终写入权重:
[
w_{t}^w = g_{t}^w g_{t}^a a_t + (1 - g_{t}^a) c_{t}^w
]
其中 $g_{t}^w$ 和 $g_{t}^a$ 是介于 0 和 1 之间的“写入门”和“分配门”,由控制器通过接口向量提供。$g_{t}^w$ 决定是否进行写入操作,$g_{t}^a$ 则指定是使用分配权重写入新位置,还是修改查找权重指定的现有值。
2. DNC 内存重用机制
当计算分配权重时,若所有位置都被使用(即 $u_t = 1$),分配权重将全为 0,无法分配新数据。为此,需构建“保留向量”$\psi_t$,其大小为 $N$,每个元素值在 0 到 1 之间,0 表示对应位置可释放,1 表示应保留。计算公式为:
[
\psi_t = \prod_{i = 1}^{R} (1 - f_{t}^i w_{t - 1}^{r, i})
]
该公式表明,内存位置的释放程度与各读取头在上一时间步从该位置读取的量成正比。但连续释放读取后的数据并非总是理想的,因此由控制器通过一组 $R$ 个“释放门”$f_{t}^1, \cdots, f_{t}^R$(值在 0 到 1 之间)决定何时释放或保留位置。
得到保留向量后,可更新使用向量:
[
u_t = u_{t - 1} + w_{t - 1}^w - u_{t - 1} \circ w_{t - 1}^w \circ \psi_t
]
此式表示,若位置被保留($\psi_t$ 中值约为 1),且已在使用或刚被写入($u_{t - 1} + w_{t - 1}^w$ 指示),则该位置被使用。减去元素级乘积 $u_{t - 1} \circ w_{t - 1}^w$ 可确保使用值在 0 到 1 之间。
在计算分配权重前进行此更新步骤,可引入空闲内存,有效利用和重用有限内存,克服 NTM 的第二个局限。
3. DNC 写入的时间链接机制
DNC 的动态内存管理机制下,每次请求分配内存位置时,会得到最未使用的位置,该位置与前一次写入位置无位置关系,因此 NTM 通过连续性保留时间关系的方式不再适用,需明确记录写入数据的顺序。
为此引入两个额外的数据结构:
-
优先级向量
(precedence vector)$p_t$:大小为 $N$,可视为内存位置上的概率分布,每个值表示对应位置是最后写入位置的可能性。初始时 $p_0 = 0$,更新公式如下:
[
p_t = (1 - \sum_{i = 1}^{N} w_{t}^w(i)) p_{t - 1} + w_{t}^w
]
先根据写入权重分量的总和按比例重置优先级的先前值,再加上写入权重,使写入权重大的位置在优先级向量中值也大。
-
链接矩阵
(link matrix)$L_t$:$N \times N$ 矩阵,元素 $L_{t}(i, j)$ 值在 0 到 1 之间,表示位置 $i$ 在位置 $j$ 之后写入的可能性。初始化为 0,对角线元素始终为 0。更新公式为:
[
L_{t}(i, j) = (1 - w_{t}^w(i) - w_{t}^w(j)) L_{t - 1}(i, j) + w_{t}^w(i) p_{t - 1}(j)
]
先根据位置 $i$ 和 $j$ 的写入量按比例重置链接元素,再通过位置 $i$ 的写入权重与位置 $j$ 的先前优先级值的乘积更新链接。这克服了 NTM 的第三个局限,能跟踪时间信息。
4. DNC 读取头的工作原理
写入头完成内存矩阵和相关数据结构的更新后,读取头开始工作。其操作包括在内存中查找值,以及按时间顺序前后迭代数据。
查找能力可通过基于内容的寻址实现,对于每个读取头 $i$,计算中间权重 $c_{t}^{r, i} = \mathcal{F}(M_t, k_{t}^{r, i}, \beta_{t}^{r, i})$,其中 $k_{t}^{r, 1}, \cdots, k_{t}^{r, R}$ 和 $\beta_{t}^{r, 1}, \cdots, \beta_{t}^{r, R}$ 是从控制器通过接口向量接收的 $R$ 组读取键和强度。
为实现前后迭代,可通过将链接矩阵与上次读取权重相乘得到“前向权重”$f_{t}^i = L_t w_{t - 1}^{r, i}$,将链接矩阵的转置与上次读取权重相乘得到“后向权重”$b_{t}^i = L_{t - 1}^{\top} w_{t - 1}^{r, i}$。
新的读取权重计算公式为:
[
w_{t}^{r, i} = \pi_{t}^i(1) b_{t}^i + \pi_{t}^i(2) c_{t}^i + \pi_{t}^i(3) f_{t}^i
]
其中 $\pi_{t}^1, \cdots, \pi_{t}^R$ 是“读取模式”,为接口向量中来自控制器的三个元素的 softmax 分布,其三个值分别决定读取头对后向、查找和前向读取机制的重视程度。控制器学习使用这些模式指导内存读取数据。
5. DNC 控制器网络
控制器协调所有内存操作,其核心是一个神经网络(循环或前馈),接收输入步骤和上一步的读取向量,输出向量大小取决于网络架构,记为 $\mathcal{F}(\chi_t)$,其中 $\chi_t = [x_t; r_{t - 1}^1; \cdots; r_{t - 1}^R]$ 是输入步骤和上次读取向量的拼接,作用类似于常规 LSTM 中的隐藏状态。
从神经网络输出的向量中,需获取两部分信息:
-
接口向量
$\zeta_t$:包含内存操作所需的所有信息,可看作之前遇到的各个元素的拼接,大小为 $R \times W + 3W + 5R + 3$。通过可学习的权重矩阵 $W_{\zeta}$ 获得:
[
\zeta_t = W_{\zeta} \mathcal{F}(\chi_t)
]
在将 $\zeta_t$ 传递给内存前,需确保各组件值有效。例如,所有门和擦除向量值需在 0 到 1 之间,通过 sigmoid 函数处理:
[
e_t = \sigma(e_t), f_{t}^i = \sigma(f_{t}^i), g_{t}^a = \sigma(g_{t}^a), g_{t}^w = \sigma(g_{t}^w)
]
其中 $\sigma(z) = \frac{1}{1 + e^{-z}}$。查找强度需大于等于 1,通过“oneplus”函数处理:
[
\beta_{t}^{r, i} = \text{oneplus}(\beta_{t}^{r, i}), \beta_{t}^w = \text{oneplus}(\beta_{t}^w)
]
其中 $\text{oneplus}(z) = 1 + \log(1 + e^z)$。读取模式需为有效的 softmax 分布:
[
\pi_{t}^i = \text{softmax}(\pi_{t}^i)
]
其中 $\text{softmax}(z) = \frac{e^z}{\sum_j e^{z_j}}$。
-
预输出向量
$v_t$:与最终输出向量大小相同,但不是最终输出。通过可学习的权重矩阵 $W_y$ 获得:
[
v_t = W_y \mathcal{F}(\chi_t)
]
结合可学习的权重矩阵 $W_r$,最终输出为:
[
y_t = v_t + W_r [r_t^1; \cdots; r_t^R]
]
由于控制器除字长 $W$ 外对内存一无所知,已学习的控制器可扩展到更大内存而无需重新训练。且未指定神经网络的特定结构或损失函数,使 DNC 成为可应用于各种任务和学习问题的通用架构。
6. DNC 操作可视化
可通过在简单任务上训练 DNC 来观察其操作,这里使用修改后的“复制问题”。与复制单个二进制向量序列不同,任务是复制一系列此类序列。
例如,训练一个长度为 4 的序列,每个序列包含五个二进制向量和一个结束标记。使用只有 10 个内存位置的 DNC,无法存储输入的 20 个向量。使用前馈控制器确保无循环状态存储,仅使用一个读取头以便更清晰地可视化。这些约束迫使 DNC 学习如何释放和重用内存以成功复制整个输入。
可视化结果显示,DNC 将序列中的每个向量写入单个内存位置,看到结束标记后,读取头按写入顺序从这些位置读取。同时,分配门和释放门在写入和读取阶段交替激活,使用向量图显示写入后位置使用值为 1,读取后降为 0,表明位置被释放可重用。
7. TensorFlow 中实现 DNC 的挑战与解决方案
在 TensorFlow 中实现 DNC 时,链接矩阵更新和分配权重计算是较棘手的操作。若使用循环实现,会在计算图中添加大量相同节点,导致内存使用和执行时间问题。例如,链接矩阵更新的循环实现代码如下:
def Lt(L, wwt, p, N):
L_t = tf.zeros([N,N], tf.float32)
for i in range(N):
for j in range(N):
if i == j:
continue
_mask = np.zeros([N,N], np.float32);
_mask[i,j] = 1.0
mask = tf.convert_to_tensor(_mask)
link_t = (1 - wwt[i] - wwt[j]) * L[i,j] +
wwt[i] * p[j]
L_t += mask * link_t
return L_t
为克服此问题,可采用向量化方法。对于链接矩阵更新,可重写为:
[
L_t = (1 - w_{t}^w \oplus w_{t}^w) \circ L_{t - 1} + w_{t}^w p_{t - 1} \circ (1 - I)
]
其中 $I$ 是单位矩阵,$w_{t}^w p_{t - 1}$ 是外积,$\oplus$ 是新定义的向量逐对加法运算符:
[
u \oplus v =
\begin{bmatrix}
u_1 + v_1 & \cdots & u_1 + v_n \
\vdots & \ddots & \vdots \
u_n + v_1 & \cdots & u_n + v_n
\end{bmatrix}
]
向量化实现代码如下:
def Lt(L, wwt, p, N):
# we only need the case of adding a single vector to itself
def pairwise_add(v):
n = v.get_shape().as_list()[0]
# an NxN matrix of duplicates of u along the columns
V = tf.concat(1, [v] * n)
return V + V
I = tf.constant(np.identity(N, dtype=np.float32))
updated = (1 - pairwise_add(wwt)) * L + tf.matmul(wwt, p)
updated = updated * (1 - I) # eliminate self-links
return updated
对于分配权重规则,可分解为以下操作:
1. 对使用向量排序得到空闲列表时,同时获取排序后的使用向量。
2. 计算排序后使用向量的累积乘积向量。
3. 将累积乘积向量与 (1 - 排序后的使用向量) 相乘,得到按排序顺序的分配权重。
4. 根据空闲列表重新排序,得到正确的分配权重。
排序可使用
tf.nn.top_k
实现:
sorted_ut, free_list = tf.nn.top_k(-1 * ut, N)
sorted_ut *= -1
重新排序分配权重可使用
TensorArray
:
empty_at = tf.TensorArray(tf.float32, N)
full_at = empty_at.scatter(free_list, out_of_location_at)
a_t = full_at.pack()
实现中最后需要循环的部分是控制器循环,由于向量化仅适用于元素级操作,控制器循环无法向量化。但 TensorFlow 提供了符号循环
tf.while_loop
来避免 Python 循环的性能问题。例如,计算向量累积和的代码如下:
# 此处原文档未给出完整代码,可补充示例代码
import tensorflow as tf
def cumulative_sum(vector):
def cond(i, _):
return i < tf.size(vector)
def body(i, result):
result = result.write(i, tf.reduce_sum(vector[:i + 1]))
return i + 1, result
result = tf.TensorArray(tf.float32, size=tf.size(vector))
_, result = tf.while_loop(cond, body, [0, result])
return result.stack()
vector = tf.constant([1, 2, 3, 4, 5], dtype=tf.float32)
cumulative_sum_vector = cumulative_sum(vector)
with tf.Session() as sess:
print(sess.run(cumulative_sum_vector))
通过上述方法,可在 TensorFlow 中高效实现 DNC。
以下是 DNC 主要操作的流程图:
graph TD;
A[输入数据] --> B[控制器网络];
B --> C[计算接口向量和预输出向量];
C --> D[更新内存矩阵和相关数据结构];
D --> E[写入头操作];
E --> F[更新使用向量和优先级向量、链接矩阵];
F --> G[读取头操作];
G --> H[输出结果];
通过以上内容,我们详细了解了 DNC 的原理、操作机制以及在 TensorFlow 中的实现方法,希望能帮助你更好地应用 DNC 解决实际问题。
深入探究 DNC:从原理到 TensorFlow 实现
8. 操作步骤总结
为了更清晰地展示 DNC 的操作流程,下面以表格形式总结主要操作步骤:
|操作阶段|操作步骤|具体说明|
| ---- | ---- | ---- |
|写入前准备|1. 初始化使用向量 $u_0$、优先级向量 $p_0$ 和链接矩阵 $L_0$|$u_0 = 0$,$p_0 = 0$,$L_0$ 为全零矩阵|
||2. 接收输入步骤 $x_t$ 和上一步读取向量 $r_{t - 1}^1, \cdots, r_{t - 1}^R$|用于控制器网络计算|
|控制器网络计算|3. 计算 $\chi_t = [x_t; r_{t - 1}^1; \cdots; r_{t - 1}^R]$|作为神经网络输入|
||4. 计算 $\mathcal{F}(\chi_t)$|神经网络输出向量|
||5. 计算接口向量 $\zeta_t = W_{\zeta} \mathcal{F}(\chi_t)$|包含内存操作所需信息|
||6. 对接口向量各组件进行处理|如使用 sigmoid、oneplus、softmax 函数确保值有效|
||7. 计算预输出向量 $v_t = W_y \mathcal{F}(\chi_t)$|用于最终输出计算|
|写入操作|8. 计算分配权重 $a_t$|根据使用向量 $u_t$ 和空闲列表 $\phi_t$ 计算|
||9. 计算查找权重 $c_{t}^w$|基于内容寻址机制|
||10. 计算最终写入权重 $w_{t}^w = g_{t}^w g_{t}^a a_t + (1 - g_{t}^a) c_{t}^w$|$g_{t}^w$ 和 $g_{t}^a$ 为写入门和分配门|
||11. 更新内存矩阵 $M_t$|使用写入权重 $w_{t}^w$|
|内存管理更新|12. 计算保留向量 $\psi_t$|根据释放门 $f_{t}^i$ 和读取权重 $w_{t - 1}^{r, i}$ 计算|
||13. 更新使用向量 $u_t = u_{t - 1} + w_{t - 1}^w - u_{t - 1} \circ w_{t - 1}^w \circ \psi_t$|反映内存使用和释放情况|
||14. 更新优先级向量 $p_t = (1 - \sum_{i = 1}^{N} w_{t}^w(i)) p_{t - 1} + w_{t}^w$|记录最后写入位置可能性|
||15. 更新链接矩阵 $L_t$|使用更新公式 $L_{t}(i, j) = (1 - w_{t}^w(i) - w_{t}^w(j)) L_{t - 1}(i, j) + w_{t}^w(i) p_{t - 1}(j)$|
|读取操作|16. 计算中间查找权重 $c_{t}^{r, i}$|对于每个读取头 $i$|
||17. 计算前向权重 $f_{t}^i = L_t w_{t - 1}^{r, i}$ 和后向权重 $b_{t}^i = L_{t - 1}^{\top} w_{t - 1}^{r, i}$|实现时间顺序迭代|
||18. 计算新的读取权重 $w_{t}^{r, i} = \pi_{t}^i(1) b_{t}^i + \pi_{t}^i(2) c_{t}^i + \pi_{t}^i(3) f_{t}^i$|$\pi_{t}^i$ 为读取模式|
||19. 从内存矩阵 $M_t$ 中读取数据|使用读取权重 $w_{t}^{r, i}$|
|输出计算|20. 计算最终输出 $y_t = v_t + W_r [r_t^1; \cdots; r_t^R]$|结合预输出向量和读取向量|
9. 关键技术点分析
- 向量化的优势 :在 TensorFlow 实现中,向量化操作避免了循环带来的大量节点复制问题,显著提高了内存使用效率和执行速度。例如,链接矩阵更新和分配权重计算的向量化实现,减少了计算图的复杂度,使模型能够处理更大规模的内存。
-
符号循环的作用
:控制器循环无法向量化时,符号循环
tf.while_loop提供了一种高效的解决方案。它通过定义循环节点,在图执行时进行循环操作,避免了 Python 循环的性能瓶颈,确保了模型在处理序列输入时的高效性。 - 内存管理机制的重要性 :DNC 的内存管理机制,包括使用向量、保留向量、优先级向量和链接矩阵的更新,使得模型能够动态地分配和重用内存,克服了传统模型在内存使用上的局限性,提高了模型的灵活性和适应性。
10. 应用场景展望
DNC 作为一种通用的架构,具有广泛的应用前景:
-
自然语言处理
:在文本生成、机器翻译等任务中,DNC 可以利用其动态内存管理和时间信息跟踪能力,更好地处理长文本和上下文信息,提高生成文本的质量和连贯性。
-
强化学习
:在复杂环境中的决策任务中,DNC 可以作为智能体的记忆模块,帮助智能体更好地记住历史状态和动作,从而做出更合理的决策。
-
图像和视频处理
:在视频理解、图像序列分析等任务中,DNC 可以用于跟踪时间序列信息,例如视频中的物体运动轨迹、图像序列的变化趋势等。
11. 总结
本文深入探讨了 DNC 的原理、操作机制以及在 TensorFlow 中的实现方法。通过引入使用向量、优先级向量和链接矩阵等数据结构,DNC 实现了动态内存管理和时间信息跟踪,克服了传统模型的局限性。在 TensorFlow 实现中,向量化和符号循环等技术的应用提高了模型的性能和效率。
以下是 DNC 操作流程的另一个 mermaid 流程图,更详细地展示了各步骤之间的关系:
graph LR;
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(输入数据):::process;
B --> C(控制器网络计算):::process;
C --> D{是否写入数据?}:::decision;
D -- 是 --> E(写入操作):::process;
E --> F(内存管理更新):::process;
D -- 否 --> F;
F --> G{是否读取数据?}:::decision;
G -- 是 --> H(读取操作):::process;
H --> I(输出结果):::process;
G -- 否 --> I;
I --> J([结束]):::startend;
通过对 DNC 的学习和实践,我们可以将其应用于各种复杂的任务和学习问题中,为解决实际问题提供更强大的工具。希望本文能帮助你更好地理解和应用 DNC,在相关领域取得更好的成果。
超级会员免费看
786

被折叠的 条评论
为什么被折叠?



