The Elements of Differentiable Programming

Chapter 6Data structures

In computer science, a data structure is a specialized format for organizing, storing and accessing data. Mathematically, a data structure forms a so-called algebraic structure: it consists of a set and the functions to operate on that set. In this chapter, we review how to incorporate data structures into differentiable programs, with a focus on lists and dictionaries.

6.1 Lists

A list is an ordered sequence of elements. We restrict ourselves to lists whose elements all belong to the same value space V\cV. Formally, we denote a list of fixed length KK with values in V\cV by a KK-tuple l≔(l1,…,lK)∈LK(V) \l \coloneqq (\l_1, \dots, \l_K) \in \cL_{K}(\cV) where each li∈V\l_i \in \cV and where LK(V)≔VK=V×⋯×V⏟K times. \cL_K(\cV) \coloneqq \cV^K = \underbrace{\cV \times \dots \times \cV}_{K\text{ times}}.

6.1.1 Basic operations

Getting values

We first present how to retrieve values from a list l∈LK(V)\l \in \cL_K(\cV). We define the function list.get ⁣:LK(V)×[K]→V\mathrm{list.get} \colon \cL_K(\cV) \times [K] \to \cV as list.get(l,i)≔li. \mathrm{list.get}(\l, i) \coloneqq \l_i. The function is continuous and differentiable in l∈LK(V)\l \in \cL_K(\cV) but not in i∈[K]i \in [K], as it is a discrete variable. In the particular case V=R\cV = \RR, LK(V)\cL_K(\cV) is equivalent to RK\RR^K and we can therefore write list.get(l,i)=⟨l,ei⟩, \mathrm{list.get}(\l, i) = \langle \l, \e_i \rangle, where {e1,…,eK}\{\e_1,\dots,\e_K\} is the standard basis of RK\RR^K.

Setting values

We now present how to replace values from a list l∈LK(V)\l \in \cL_K(\cV). We define the function list.set ⁣:LK(V)×[K]×V→LK(V)\mathrm{list.set} \colon \cL_K(\cV) \times [K] \times \cV \to \cL_K(\cV) as [list.set(l,i,v)]j≔{v if i=jlj if i≠j, [\mathrm{list.set}(\l, i, \v)]_j \coloneqq \begin{cases} \v &\mbox{ if } i = j \\ \l_j &\mbox{ if } i \neq j \end{cases}, for j∈[K]j \in [K]. In the functional programming spirit, the function returns the whole new list, even though a single element has been modified. Again, the function is continuous and differentiable in l∈LK(V)\l \in \cL_K(\cV) and v∈V\v \in \cV but not in i∈[K]i \in [K]. In the particular case V=R\cV = \RR, given a list l=(l1,…,lK)\l = (l_1,\dots,l_K), we can set the value v∈Rv \in \RR by list.set(l,i,v)=l+(v−li)ei. \mathrm{list.set}(\l, i, v) = \l + (v - l_i) \e_i. That is, we subtract the old value lil_i and add the new value vv at the location i∈[K]i \in [K] in l\l.

Implementation

A fixed-length list can be implemented as an array, which enables O(1)O(1) random access to individual elements. The hardware counterpart of an array is random access memory (RAM), in which memory can be retrieved by address (location).

6.1.2 Operations on variable-length lists

So far, we focused on lists of fixed length KK. We now turn our attention to variable-length lists, whose size can decrease or increase over time. In addition to the list.get\mathrm{list.get} and list.set\mathrm{list.set} functions, they support functions that can change the size of a list.

Initializing lists

In order to initialize a list, we define list.init ⁣:V→L1(V)\mathrm{list.init} \colon \cV \to \cL_1(\cV) as list.init(v)≔(v), \mathrm{list.init}(\v) \coloneqq (\v), where we used (v)(\v) to denote a 11-tuple.

Pushing values

In order to add new values either to the left or to the right, we define list.pushLeft ⁣:LK(V)×V→LK+1(V)\mathrm{list.pushLeft} \colon \cL_K(\cV) \times \cV \to \cL_{K+1}(\cV) as list.pushLeft(l,v)≔(v,l1,…,lK). \mathrm{list.pushLeft}(\l, \v) \coloneqq (\v, \l_1, \dots, \l_K). and list.pushRight ⁣:LK(V)×V→LK+1(V)\mathrm{list.pushRight} \colon \cL_K(\cV) \times \cV \to \cL_{K+1}(\cV) as list.pushRight(l,v)≔(l1,…,lK,v). \mathrm{list.pushRight}(\l, \v) \coloneqq (\l_1, \dots, \l_K, \v).

Popping values

In order to remove values either from the left or from the right, we define list.popLeft ⁣:LK(V)→LK−1(V)×V\mathrm{list.popLeft} \colon \cL_K(\cV) \to \cL_{K-1}(\cV) \times \cV as list.popLeft(l)≔(l2,…,lK),l1 \mathrm{list.popLeft}(\l) \coloneqq (\l_2, \dots, \l_{K}), \l_1 and list.popRight ⁣:LK(V)→LK−1(V)×V\mathrm{list.popRight} \colon \cL_K(\cV) \to \cL_{K-1}(\cV) \times \cV as list.popRight(l)≔(l1,…,lK−1),lK. \mathrm{list.popRight}(\l) \coloneqq (\l_1, \dots, \l_{K-1}), \l_K. The set L0(V)\cL_0(\cV) is a singleton which contains the empty list.

Inserting values

The pushLeft and pushRight functions can only insert values at the beginning and at the end of a list, respectively. We now study the insert function, whose goal is to be able to add a new value at an arbitrary location, shifting all values to the right and increasing the list size by 11. We define the function list.insert:LK(V)×[K+1]×V→LK+1(V)\mathrm{list.insert}: \cL_K(\cV) \times [K+1] \times \cV \rightarrow \cL_{K+1}(\cV) as [list.insert(l,i,v)]j≔{ljif j<ivif j=ilj−1if j>i, [\mathrm{list.insert}(\l, i, \v)]_j \coloneqq \begin{cases} \l_j & \mbox{if} \ j < i \\ \v & \mbox{if} \ j = i \\ \l_{j-1} & \mbox{if} \ j > i \end{cases}, for j∈[K+1]j \in [K+1]. As for the list.set\mathrm{list.set} function, list.insert\mathrm{list.insert} is readily continuous and differentiable in l\l and v\v, but not in ii, as it is a discrete variable. As special cases, we naturally recover list.insert(l,1,v)=list.pushLeft(l,v),list.insert(l,K+1,v)=list.pushRight(l,v).\begin{align*} \mathrm{list.insert}(\l, 1, \v) & = \mathrm{list.pushLeft}(\l, \v), \\ \mathrm{list.insert}(\l, K+1, \v) & = \mathrm{list.pushRight}(\l, \v). \end{align*}

Differentiability

The list.init\mathrm{list.init}, list.push\mathrm{list.push} and list.pop\mathrm{list.pop} functions are readily continuous and differentiable with respect to their arguments (a continuous relaxation is not needed). As for the list.set\mathrm{list.set} function, the list.insert\mathrm{list.insert} function is continuous and differentiable in l\l and v\v, but not in ii.

Implementation

Under the hood, a variable-length list can be implemented as a linked list or as a dynamic array. A linked list gives O(K)O(K) random access while a dynamic array allows O(1)O(1) random access, at the cost of memory reallocations.

Stacks and queues

The list.pushRight\mathrm{list.pushRight} and list.popRight\mathrm{list.popRight} functions can be used to implement a stack (last in first out a.k.a. LIFO behavior). The list.pushLeft\mathrm{list.pushLeft} and list.popRight\mathrm{list.popRight} functions can be used to implement a queue (first in first out a.k.a. FIFO behavior).

6.1.3 Continuous relaxations using soft indexing

Getting values

In order to be able to differentiate list.get\mathrm{list.get} w.r.t. indexing, a natural idea is to replace the integer index i∈[K]i \in [K] by a distribution πi∈△K\piv_i \in \triangle^K, which we can interpret as a soft index. An integer index i∈[K]i \in [K] is then equivalent to a delta distribution πi∈{e1,…,eK}\piv_i \in \{\e_1, \dots, \e_K\}. We define the continuous relaxation list.softGet ⁣:LK(V)×△K→conv⁡(V)\mathrm{list.softGet} \colon \cL_K(\cV) \times \triangle^K \to \conv(\cV) as list.softGet(l,πi)≔∑j=1Kπi,jlj=cond(πi,l1,…,lK)=EI∼Categorical(πi)[lI],\begin{align*} \mathrm{list.softGet}(\l, \piv_i) &\coloneqq \sum_{j=1}^K \pi_{i,j} \l_j \\ &= \mathrm{cond}(\piv_i, \l_1, \dots, \l_K) \\ &= \EE_{I \sim \mathrm{Categorical}(\piv_i)}[\l_I], \end{align*} where cond\mathrm{cond} is studied in Section 5.7. In the particular case V=R\cV = \RR, we obtain list.softGet(l,i)=⟨l,πi⟩. \mathrm{list.softGet}(\l, i) = \langle \l, \piv_i \rangle. This is illustrated in Fig. 6.1.

The choice of the distribution πi=(πi,1,…,πi,K)\piv_i = (\pi_{i,1}, \dots, \pi_{i,K}) encodes the importance of the elements (l1,…,lK)(\l_1,\dots,\l_K) w.r.t. li\l_i. If we consider that the smaller ∣i−j∣|i-j| is, the more related li\l_i and lj\l_j are, then it makes sense to define a distribution centered around ii (i.e., such that the mode of the distribution is achieved at ii). For example, limiting ourselves to the neighbors li−1\l_{i-1} and li+1\l_{i+1} (i.e., a window of size 11), we can define the sparse distribution πi≔14⋅ei−1+12ei+14⋅ei+1∈△K. \piv_i \coloneqq \frac{1}{4} \cdot \e_{i-1} + \frac{1}{2} \e_i + \frac{1}{4} \cdot \e_{i+1} \in \triangle^K. In this particular case, the continuous relaxation of the list.get\mathrm{list.get} function can then be expressed as a discrete convolution, list.softGet(l,πi)=(list.get(l,⋅)∗κ)(i)=∑j=−∞∞list.get(l,i−j)κ(j), \mathrm{list.softGet}(\l, \piv_i) = \left(\mathrm{list.get}(\l, \cdot) \ast \kappa\right)(i) = \sum_{j=-\infty}^\infty \mathrm{list.get}(\l, i - j) \kappa(j), where κ(−1)≔14\kappa(-1) \coloneqq \frac{1}{4}, κ(1)≔14\kappa(1) \coloneqq \frac{1}{4}, κ(0)≔12\kappa(0) \coloneqq \frac{1}{2}, and κ(j)≔0\kappa(j) \coloneqq 0 for j∉{−1,0,1}j \not \in \{-1,0,1\}. Assuming V=RM\cV = \RR^M, the computational complexity of list.softGet\mathrm{list.softGet} is O(M⋅∣supp(πi)∣)O(M \cdot |\mathrm{supp}(\piv_i)|).

Figure 6.1: The list.get(l,i)\mathrm{list.get}(\l, i) function is continuous and differentiable in l\l but not in ii. Its relaxation list.softGet(l,πi)\mathrm{list.softGet}(\l, \piv_i) is differentiable in both l\l and πi\piv_i. When V=R\cV=\RR, list.softGet(l,πi)\mathrm{list.softGet}(\l, \piv_i) can be seen as taking the inner product between the list l\l and the probability distribution πi\piv_i, instead of the delta distribution (canonical vector) ei\e_i.

Setting values

To differentiate w.r.t. indexing, we can define the continuous relaxation list.softSet ⁣:LK(V)×△K×V→LK(conv⁡(V))\mathrm{list.softSet} \colon \cL_K(\cV) \times \triangle^K \times \cV \to \cL_K(\conv(\cV)) as [list.softSet(l,πi,v)]j≔E[list.set(l,I,v)]j=P(I=j)v+P(I≠j)lj=πi,jv+(1−πi,j)lj,\begin{align*} \left[\mathrm{list.softSet}(\l, \piv_i, \v)\right]_j &\coloneqq \EE[\mathrm{list.set}(\l, I, \v)]_j \\ &= \PP(I=j) \v + \PP(I \neq j) \l_j \\ &= \pi_{i,j} \v + (1 - \pi_{i,j}) \l_j, \end{align*} where j∈[K]j \in [K] and I∼Categorical(πi)I \sim \mathrm{Categorical}(\piv_i). Equivalently, we can write list.softSet(l,πi,v)=(πi,1v+(1−πi,1)l1,…,πi,Kv+(1−πi,K)lK)=(ifelse(πi,1,v,l1),…,ifelse(πi,K,v,lK)),\begin{align*} \mathrm{list.softSet}(\l, \piv_i, \v) &= (\pi_{i,1} \v + (1-\pi_{i,1}) \l_1, \dots, \pi_{i,K} \v + (1-\pi_{i,K}) \l_K) \\ &= (\mathrm{ifelse}(\pi_{i,1}, \v, \l_1), \dots, \mathrm{ifelse}(\pi_{i,K}, \v, \l_K)), \end{align*} where ifelse\mathrm{ifelse} is studied in Section 5.6. Since ifelse(π,u1,u0)=EI∼Bernoulli(π)[uI], \mathrm{ifelse}(\pi, \u_1, \u_0) = \EE_{I \sim \mathrm{Bernoulli}(\pi)}[\u_I], this relaxation amounts to using an element-wise expectation. As a result, the list output by list.softSet\mathrm{list.softSet} takes values in conv⁡(V)\conv(\cV) instead of V\cV. Note however that when V=RM\cV = \RR^M, then conv⁡(V)=RM\conv(\cV) = \RR^M as well.

Inserting values

To differentiate value insertion w.r.t. indexing, we can define the continuous relaxation list.softInsert ⁣:LK(V)×△K+1×V→LK+1(conv⁡(V))\mathrm{list.softInsert} \colon \cL_K(\cV) \times \triangle^{K+1} \times \cV \to \cL_{K+1}(\conv(\cV)) as [list.softInsert(l,πi,v)]j≔E[list.insert(l,I,v)]=P(I>j)lj+P(I=j)v+P(I<j)lj−1,\begin{align*} [\mathrm{list.softInsert}(\l, \piv_i, \v)]_j &\coloneqq \EE[\mathrm{list.insert}(\l, I, \v)] \\ & = \PP(I > j) \l_j + \PP(I = j) \v + \PP(I < j) \l_{j-1}, \end{align*} where I∼Categorical(πi)I \sim \mathrm{Categorical}(\piv_i). The three necessary probabilities can easily be calculated for j∈[K+1]j \in [K+1] by P(I>j)={0 if j=K+1∑k=j+1K+1πi,k otherwise P(I=j)=πi,jP(I<j)={0 if j=1∑k=1j−1πi,k otherwise .\begin{align*} \PP(I > j) &= \begin{cases} 0 &\mbox{ if } j = K+1 \\ \sum_{k=j+1}^{K+1} \pi_{i, k} &\mbox{ otherwise } \end{cases} \\ \PP(I = j) &= \pi_{i,j} \quad \\ \PP(I < j) &= \begin{cases} 0 &\mbox{ if } j = 1 \\ \sum_{k=1}^{j-1} \pi_{i, k} &\mbox{ otherwise } \end{cases}. \end{align*}

Multi-dimensional indexing

In multi-dimensional lists (arrays or tensors), each element li∈V\l_\iv \in \cV of a list l∈LK1,…,KT(V)\l \in \cL_{K_1,\dots,K_T}(\cV) can now be indexed by a multivariate integer i=(i1,…,iT)∈[K1]×⋯×[KT]\iv = (i_1, \dots, i_T) \in [K_1] \times \dots \times [K_T], where T∈NT \in \NN is the number of axes of l\l. We can always flatten a multi-dimensional list into a uni-dimensional list by replacing the multi-dimensional index i∈[K1]×⋯×[KT]\iv \in [K_1] \times \dots \times [K_T] by a flat index i∈[K1…KT]i \in [K_1 \dots K_T]. The converse operation, converting a flat uni-dimensional array into a multi-dimensional array, is also possible. Therefore, there is a bijection between [K][K] and [K1]×⋯×[KT][K_1] \times \dots \times [K_T] for K≔K1…KTK \coloneqq K_1 \dots K_T.

This means that the previous discussion on soft indexing in the uni-dimensional setting readily applies to the multi-dimensional setting. All it takes is the ability to define a probability distribution πi∈△K1×⋯×KT\piv_\iv \in \triangle^{K_1 \times \dots \times K_T}. For example, when working with images, we can define a probability distribution putting probability mass only on the neighboring pixels of pixel i\iv, a standard approach in image processing. Another simple approach is to use a product of axis-wise probability distributions.

6.2 Dictionaries

A dictionary (a.k.a. associative array or map) is an unordered list of key-value pairs, such that each possible key appears at most once in the list. We denote the set of keys by K\cK and the set of values by V\cV (both being potentially infinite). We can then define the set of dictionaries of size LL from K\cK to V\cV by DL(K,V)≔LL(K×V)=(K×V)L \cD_L(\cK,\cV) \coloneqq \cL_L(\cK \times \cV) = (\cK \times \cV)^L and one such dictionary by d≔((k1,v1),…,(kL,vL))∈DL(K,V). \d \coloneqq ((\kv_1,\v_1), \dots, (\kv_L,\v_L)) \in \cD_L(\cK,\cV).

6.2.1 Basic operations

Getting values

The goal of the dict.get\mathrm{dict.get} function is to retrieve the value associated with a key, assuming that the dictionary contains this key. Formally, we define the dict.get ⁣:DL(K,V)×K→V∪{∞}\mathrm{dict.get} \colon \cD_L(\cK,\cV) \times \cK \to \cV \cup \{\infty\} function as dict.get(d,k)≔{vi if ∃i∈[L] s.t. k=ki∞ if k∉{k1,…,kL}. \mathrm{dict.get}(\d, \kv) \coloneqq \begin{cases} \v_i &\mbox{ if } \exists i \in [L] \text{ s.t. } \kv = \kv_i \\ \infty & \mbox{ if } \kv \not \in \{\kv_1,\dots,\kv_L\} \end{cases}. The function is continuous and differentiable in the dictionary d\d, but not in the key k\kv. Equivalently, we can write the function as dict.get(d,k)≔∑i=1Leq(k,ki)vi∑i=1Leq(k,ki).\begin{align*} \mathrm{dict.get}(\d, \kv) \coloneqq \frac{\sum_{i=1}^L \eq(\kv, \kv_i) \v_i} {\sum_{i=1}^L \eq(\kv, \kv_i)}. \end{align*} The denominator encodes the fact that the function is undefined if no key in the dictionary d\d matches the key k\kv. Assuming k∈{k1,…,kL}\kv \in \{\kv_1,\dots,\kv_L\} and V=RM\cV = \RR^M, we can also write dict.get(d,k)=viwherei=arg max⁡j∈[L]∥k−kj∥2, \mathrm{dict.get}(\d, \kv) = \v_i \quad \text{where} \quad i = \argmax_{j \in [L]} \|\kv - \kv_j\|_2, which shows that we can see dict.get\mathrm{dict.get} as a nearest neighbor search.

Setting values

The goal of the dict.set\mathrm{dict.set} function is to replace the value associated with an existing key. Formally, we define the dict.set:DL(K,V)×K×V→DL(K,V)\mathrm{dict.set}: \cD_L(\cK, \cV) \times \cK \times \cV \rightarrow \cD_L(\cK, \cV) function as (dict.set(d,k,v))i≔{(ki,v)if ki=k(ki,vi)if ki≠k. (\mathrm{dict.set}(\d, \kv, \v))_i \coloneqq \begin{cases} (\kv_i, \v) & \mbox{if} \ \kv_i = \kv \\ (\kv_i, \v_i) & \mbox{if} \ \kv_i \neq \kv \end{cases}. The function leaves the dictionary unchanged if no key in the dictionary matches the input key k\kv. The function is continuous and differentiable in d\d and v\v, but not in k\kv.

Implementation

While we view dictionaries as lists of key-value pairs, in practice, a dictionary (a.k.a. associative array) is often implemented using a hash table or search trees. The hardware counterpart of a dictionary is called content-addressable memory (CAM), a.k.a. associative memory.

6.2.2 Continuous relaxation using kernel regression

Figure 6.2: Given a set of key-value pairs (ki,vi)∈K×V(\kv_i,\v_i) \in \cK \times \cV defining a dictionary d\d, we can estimate a continuous mapping from K\cK to V\cV using Nadaraya–Watson kernel regression (here, illustrated with K=V=R\cK = \cV = \RR). When keys are normalized to have unit norm, this recovers softargmax attention from Transformers.

A dictionary can be seen as a (potentially non-injective) function that associates a value v\v to each key k\kv. To obtain a continuous relaxation of the operations associated to a dictionary, we can adopt a probabilistic perspective of the mapping from keys to values. We can view keys and values as two continuous random variables KK and VV. We can express the conditional PDF f(v∣k)f(\v|\kv ) of V∣KV|K in terms of the joint PDF f(k,v)f(\kv, \v) of (K,V)(K,V) and the marginal PDF f(k)f(\kv) of KK as f(v∣k)=f(k,v)f(k). f(\v|\kv) = \frac{f(\kv, \v)}{f(\kv)}. Integrating, we obtain the conditional expectation E[V∣K=k]=∫Vf(v∣k)vdv=∫Vf(k,v)f(k)vdv.\begin{align*} \EE[V | K=\kv] = \int_{\cV} f(\v|\kv) \v d\v = \int_{\cV} \frac{f(\kv, \v)}{f(\kv)} \v d\v. \end{align*} This is the Bayes predictor, in the sense that E[V∣K]\EE[V|K] is the minimizer of E[(h(K)−V)2]\EE[(h(K) - V)^2] over the space of measurable functions h ⁣:K→Vh \colon \cK \to \cV. Using a sample of LL input-output pairs (ki,vi)(\kv_i, \v_i), corresponding to key-value pairs in our case, Nadaraya–Watson kernel regression estimates the joint PDF and the marginal PDF using kernel density estimation (KDE). Using a product of isotropic kernels κσ\kappa_\sigma and ρσ\rho_\sigma for key-value pairs, we can define f^σ(k,v)≔1L∑i=1Lκσ(k−ki)ρσ(v−vi). \widehat{f}_\sigma(\kv, \v) \coloneqq \frac{1}{L} \sum_{i=1}^L \kappa_\sigma(\kv - \kv_i) \rho_\sigma(\v - \v_i). The corresponding marginal distribution on the keys is then given as f^σ(k)≔∫Vf^σ(k,v)dv=1L∑i=1Lκσ(k−ki)∫Vρσ(v−vi)dv=1L∑i=1Lκσ(k−ki).\begin{align*} \widehat{f}_\sigma(\kv) &\coloneqq \int_{\cV} \widehat{f}_\sigma(\kv, \v)d\v\\ &= \frac{1}{L} \sum_{i=1}^L \kappa_\sigma(\kv - \kv_i) \int_{\cV} \rho_\sigma(\v - \v_i) d\v\\ &=\frac{1}{L} \sum_{i=1}^L \kappa_\sigma(\kv - \kv_i). \end{align*} Replacing ff with f^σ\widehat{f}_\sigma, we obtain the following estimator of the conditional expectation E^[V∣K=k]≔∫Vf^σ(k,v)f^σ(k)vdv=∫V1L∑i=1Lκσ(k−ki)ρσ(v−vi)1L∑i=1Lκσ(k−ki)vdv=∑i=1Lκσ(k−ki)∫Vρσ(v−vi)vdv∑i=1Lκσ(k−ki)=∑i=1Lκσ(k−ki)vi∑i=1Lκσ(k−ki).\begin{align*} \widehat{\EE}[V|K=\kv] &\coloneqq \int_{\cV} \frac{\widehat{f}_\sigma(\kv, \v)}{\widehat{f}_\sigma(\kv)} \v d\v \\ &= \int_{\cV} \frac{\frac{1}{L} \sum_{i=1}^L \kappa_\sigma(\kv - \kv_i) \rho_\sigma(\v - \v_i)}{\frac{1}{L} \sum_{i=1}^L \kappa_\sigma(\kv - \kv_i)} \v d\v \\ &= \frac{\sum_{i=1}^L \kappa_\sigma(\kv - \kv_i) \int_{\cV} \rho_\sigma(\v - \v_i) \v d\v}{\sum_{i=1}^L \kappa_\sigma(\kv - \kv_i)} \\ &= \frac{\sum_{i=1}^L \kappa_\sigma(\kv - \kv_i) \v_i}{\sum_{i=1}^L \kappa_\sigma(\kv - \kv_i)}. \end{align*} In the above, we assumed that ρσ(v−vi)=pvi,σ(v)\rho_\sigma(\v - \v_i) = p_{\v_i,\sigma}(\v), where pvi,σ(v)p_{\v_i,\sigma}(\v) is the PDF of a distribution whose mean is vi\v_i, so that ∫Vρσ(v−vi)vdv=EV∼pvi,σ[V]=vi. \int_{\cV} \rho_\sigma(\v - \v_i)\v d\v = \EE_{V \sim p_{\v_i,\sigma}}[V] = \v_i. Given a dictionary d=((k1,v1),…,(kL,vL))\d = ((\kv_1,\v_1),\dots,(\kv_L,\v_L)), we can therefore define the dict.softGet ⁣:DL(K,V)×K→conv⁡(V)\mathrm{dict.softGet} \colon \cD_L(\cK, \cV) \times \cK \to \conv(\cV) function as dict.softGet(d,k)≔∑i=1Lκσ(k−ki)vi∑i=1Lκσ(k−ki). \mathrm{dict.softGet}(\d, \kv) \coloneqq \frac{\sum_{i=1}^L \kappa_\sigma(\kv - \kv_i) \v_i}{ \sum_{i=1}^L \kappa_\sigma(\kv - \kv_i)}. This kernel regression perspective on dictionaries was previously pointed out by Zhang et al. (2021). It is illustrated in Fig. 6.2 with K=V=R\cK = \cV = \RR.

6.2.3 Discrete probability distribution perspective

While the set of possible keys K\cK is potentially infinite, the set of keys {k1,…,kL}⊂K\{\kv_1,\dots,\kv_L\} \subset \cK associated with a particular dictionary d=((k1,v1),…,(kL,vL))\d = ((\kv_1,\v_1),\dots,(\kv_L,\v_L)) is finite. To a particular key k\kv, we can therefore associate a discrete probability distribution πk=(πk,1,…,πk,L)∈△L\piv_\kv = (\pi_{\kv,1},\dots,\pi_{\kv,L}) \in \triangle^L over the keys (k1,…,kL)(\kv_1,\dots,\kv_L) of d\d, defined by πk,i≔κσ(k−ki)∑j=1Lκσ(k−kj)∀i∈[L]. \pi_{\kv,i} \coloneqq \frac{\kappa_\sigma(\kv - \kv_i)}{\sum_{j=1}^L \kappa_\sigma(\kv - \kv_j)} \quad \forall i \in [L]. This distribution captures the affinity between the input key k\kv and the keys (k1,…,kL)(\kv_1,\dots,\kv_L) of dictionary d\d. As illustrated in Fig. 6.3, we obtain dict.softGet(d,k)=Ei∼Categorical(πk)[vi]=∑i=1Lπk,ivi.\begin{align*} \mathrm{dict.softGet}(\d, \kv) &= \EE_{i \sim \mathrm{Categorical}(\piv_\kv)}[\v_i] \\ &= \sum_{i=1}^L \pi_{\kv,i} \v_i. \end{align*} In the limit σ→0\sigma \to 0, we recover dict.get(d,k)=∑i=1Leq(k,ki)vi∑i=1Leq(k,ki). \mathrm{dict.get}(\d, \kv) = \frac{\sum_{i=1}^L \mathrm{eq}(\kv,\kv_i) \v_i}{\sum_{i=1}^L \mathrm{eq}(\kv,\kv_i)}.

Figure 6.3: Computation graph of the dict.softGet\mathrm{dict.softGet} function. We can use a kernel κσ\kappa_\sigma to produce a discrete probability distribution πk=(πk,1,…,πk,L)∈△L\piv_\kv = (\pi_{\kv,1},\dots,\pi_{\kv,L}) \in \triangle^L, that captures the affinity between the dictionary keys (k1,…,kL)(\kv_1,\dots,\kv_L) and the input key k\kv. The dict.softGet\mathrm{dict.softGet} function can then merely be seen as a convex combination (weighted average) of values (v1,…,vL)(\v_1,\dots,\v_L) using the probability values (πk,1,…,πk,L)(\pi_{\kv,1},\dots,\pi_{\kv,L}) as weights.

While the dict.get\mathrm{dict.get} function is using a mapping from keys k∈{k1,…,kL}\kv \in \{\kv_1,\dots,\kv_L\} to integer indices [L][L], the dict.softGet\mathrm{dict.softGet} function is using a mapping from keys k∈{k1,…,kL}\kv \in \{\kv_1,\dots,\kv_L\} to distributions πk∈△L\piv_{\kv} \in \triangle^L. This perspective allows us to reuse the soft functions we developed for lists in Section 6.1. For example, we can softly replace the value associated with key k\kv by performing list.softSet(d,πk,(k,v)). \mathrm{list.softSet}(\d, \piv_\kv, (\kv, \v)). Unlike dict.set\mathrm{dict.set}, the function is differentiable w.r.t.  the distribution πk\piv_\kv.

6.2.4 Link with attention in Transformers

In the case when κσ\kappa_\sigma is the Gaussian kernel, assuming that the keys are normalized to have unit norm (which is often the case in practical implementations (Dehghani et al., 2023; Schlag et al., 2021)), we obtain κσ(k−ki)=exp⁡(−∥k−ki∥22/(2σ2))=exp⁡(−(∥k∥22+∥ki∥22)/(2σ2))exp⁡(⟨k,ki⟩/σ2)=exp⁡(−1/σ2)exp⁡(⟨k,ki⟩/σ2)\begin{align*} \kappa_\sigma(\kv - \kv_i) &= \exp(-\|\kv - \kv_i\|^2_2 / (2\sigma^2)) \\ &= \exp(-(\|\kv\|^2_2 + \|\kv_i\|^2_2) / (2\sigma^2)) \exp(\langle \kv, \kv_i \rangle / \sigma^2) \\ &= \exp(-1/\sigma^2) \exp(\langle \kv, \kv_i \rangle / \sigma^2) \end{align*} so that πk,i=κσ(k−ki)∑j=1Lκσ(k−kj)=exp⁡(⟨k,ki⟩/σ2)∑j=1Lexp⁡(⟨k,kj⟩/σ2).\begin{align*} \pi_{\kv,i} &= \frac{\kappa_\sigma(\kv - \kv_i)}{\sum_{j=1}^L \kappa_\sigma(\kv - \kv_j)}\\ &= \frac{\exp(\langle \kv, \kv_i \rangle / \sigma^2)}{ \sum_{j=1}^L \exp(\langle \kv, \kv_j \rangle / \sigma^2)}. \end{align*} We recognize the softargmax operator. Given a dictionary
d=((k1,v1),…,(kL,vL))\d = ((\kv_1,\v_1), \dots, (\kv_L,\v_L)), we thus recover attention from Transformers (Vaswani et al., 2017) as dict.softGet(d,k)=∑i=1Lπk,ivi=∑i=1Lexp⁡(⟨k,ki⟩/σ2)vi∑j=1Lexp⁡(⟨k,kj⟩/σ2).\begin{align*} \mathrm{dict.softGet}(\d, \kv) &= \sum_{i=1}^L \pi_{\kv,i} \v_i \\ &= \sum_{i=1}^L \frac{\exp(\langle \kv, \kv_i \rangle / \sigma^2) \v_i}{ \sum_{j=1}^L \exp(\langle \kv, \kv_j \rangle / \sigma^2)}. \end{align*} Transformers can therefore be interpreted as relying on a differentiable dictionary mechanism. Besides Transformers, content-based memory addressing is also used in neural Turing machines (Graves et al., 2014).

6.3 Summary