Capsule官方代碼開源之後,機器之心做了份核心代碼解讀

2020-12-17 噼啪音樂圈

前幾天,Sara Sabour 開源了一份 Capsule 代碼,該代碼是論文 Dynamic Routing between Capsules 中所採用的實現。其實早在去年剛公布此論文,機器之心就曾詳解解讀過核心思想與基本代碼,我們採用的代碼也是各研究者嘗試復現論文結果的模型。而最近 Sara 開放的代碼是標準的官方實現,因此我們希望能解讀部分核心代碼,並探討其與 naturomics 等人實現過程的差異。

Sara 實現地址:https://github.com/Sarasra/models/tree/master/research/capsules

我們主要根據 Sara 的代碼解釋了 CapsNet 的架構與實現方法,包括 Squash 非線性函數、動態路由更新方法、PrimaryCaps 層與 DigitCaps 的實現過程,還有最後的 Margin Loss 度量函數。我們希望入門讀者先了解 Capsule 的概念與 CapsNet 的基本架構,以下我們提供了 Capsule 論文解讀與基本概念。

先讀懂 CapsNet 架構然後用 TensorFlow 實現,這應該是最詳細的教程了

終於,Geoffrey Hinton 那篇備受關注的 Capsule 論文公開了

淺析 Geoffrey Hinton 最近提出的 Capsule 計劃

本文章所解釋的代碼均來自於 capsule_model.py 和 layers.py 兩個文件,它們也是整個實現的核心部分。下面,我們將從基本的 Capsule 概念與 Squash 非線性函數開始解析 Sara 所完成的實現。

在論文中,Geoffrey Hinton 介紹 Capsule 為:「Capsule 是一組神經元,其輸入輸出向量表示特定實體類型的實例化參數(即特定物體、概念實體等出現的概率與某些屬性)。我們使用輸入輸出向量的長度表徵實體存在的概率,向量的方向表示實例化參數(即實體的某些圖形屬性)。同一層級的 capsule 通過變換矩陣對更高級別的 capsule 的實例化參數進行預測。當多個預測一致時(本論文使用動態路由使預測一致),更高級別的 capsule 將變得活躍。」

Capsule 中神經元的激活情況表示了圖像中存在的特定實體的各種性質。這些性質可以包含很多種不同的參數,例如姿勢(位置、大小、方向)、變形、速度、反射率、色彩、紋理等等。而輸入輸出向量的長度表示了某個實體出現的概率,所以它的值必須在 0 到 1 之間。

為了實現這種壓縮,並完成 Capsule 層級的激活功能,Hinton 等人使用了一個被稱為「squashing」的非線性函數。該非線性函數確保短向量的長度能夠縮短到幾乎等於零,而長向量的長度壓縮到接近但不超過 1 的情況。以下是該非線性函數的表達式:

其中 v_j 為 Capsule j 的輸出向量,s_j 為上一層所有 Capsule 輸出到當前層 Capsule j 的向量加權和,簡單說 s_j 是 Capsule j 的輸入向量。

在 Sara 提供的實現中,她們使用以下方法定義非線性激活函數。其中輸入張量對於全連接 Capsule 層來說維度為 [batch, num_channels, num_atoms],對於卷積 Capsule 層來說,輸入的維度為 [batch, num_channels, num_atoms, height, width]。該函數將輸出一組激活張量 v_j,其維度等於輸入張量的維度。

def _squash(input_tensor):

"""Applies norm nonlinearity (squash) to a capsule layer.

Args:

input_tensor: Input tensor. Shape is [batch, num_channels, num_atoms] for a

fully connected capsule layer or

[batch, num_channels, num_atoms, height, width] for a convolutional

capsule layer.

Returns:

A tensor with same shape as input (rank 3) for output of this layer.

"""

with tf.name_scope('norm_non_linearity'):

norm = tf.norm(input_tensor, axis=2, keep_dims=True)

norm_squared = norm * norm

return (input_tensor / norm) * (norm_squared / (1 + norm_squared))

因為按照 Hinton 的思想,找到最好的處理路徑就等價於正確處理了圖像,所以在 Capsule 中加入 Routing 機制可以找到一組係數 c_ij,它們能令預測向量 u_j|i hat 最符合輸出向量 v_j,即最符合輸出的輸入向量,這樣我們就找到了最好的路徑。

圖1:該圖展示了 Capsule 的層級結構與動態 Routing 的過程

按照論文 Dynamic Routing between Capsules 所述,c_ij 為耦合係數,該係數由動態 Routing 過程迭代地更新與確定。Capsule i 和後一層級所有 Capsule 間的耦合係數和為 1。此外,該耦合係數由「routing softmax」決定,且 softmax 函數中的 logits b_ij 初始化為 0,耦合係數 c_ij 的 softmax 計算方式為:

b_ij 依賴於兩個 Capsule 的位置與類型,但不依賴於當前的輸入圖像。我們可以通過測量後面層級中每一個 Capsule j 的當前輸出 v_j 和 前面層級 Capsule i 的預測向量間的一致性,然後藉助該測量的一致性迭代地更新耦合係數。本論文簡單地通過內積度量這種一致性,即

,這一部分也就涉及到使用 Routing 更新耦合係數。

有意思的是,Sara 的實現會添加一個 leaky_routing 函數,按照該函數的定義,它會添加額外的維度以路由分對數(logits)。如果需要執行路由的張量維度與上層任意 Capsule 單元不匹配,那麼該函數將允許激活的 Capsule 單元在額外的維度中進行路由。如下參數 logits 為需要路由的張量,其中它的維度在全連接層的情況下為 [input_capsule_num, output_capsule_num],在卷積層的情況下回多增加兩個維度。output_dim 為分對數的第二個維度,即輸出的 Capsule 單元數。

def _leaky_routing(logits, output_dim):

"""Adds extra dimmension to routing logits.

This enables active capsules to be routed to the extra dim if they are not a

good fit for any of the capsules in layer above.

Args:

logits: The original logits. shape is

[input_capsule_num, output_capsule_num] if fully connected. Otherwise, it

has two more dimmensions.

output_dim: The number of units in the second dimmension of logits.

Returns:

Routing probabilities for each pair of capsules. Same shape as logits.

"""

# leak is a zero matrix with same shape as logits except dim(2) = 1 because

# of the reduce_sum.

leak = tf.zeros_like(logits, optimize=True)

leak = tf.reduce_sum(leak, axis=2, keep_dims=True)

leaky_logits = tf.concat([leak, logits], axis=2)

leaky_routing = tf.nn.softmax(leaky_logits, dim=2)

return tf.split(leaky_routing, [1, output_dim], 2)[1]

如上所示,tf.zeros_like 將構建一個與 logits 維度相同的張量,其中每個元素都為 0。在求和處理後,leaky_logits 將在第三個維度拼接 leak 和 logits 張量。在對 leaky_logits 第 3 個維度進行 Softmax 後就相當於計算了以下偽代碼中的耦合係數 c_ij,我們需要使用它執行進一步的路由。此外,_leaky_routing 應該是沒有應用 Squash 非線性激活。因此該函數真實的意義與過程可能需要進一步探討,這裡的理解不是很完全。

以上_leaky_routing 函數會在完整執行路由和非線性壓縮的_update_routing 函數中調用,所以在我們查看完整的路由函數前可以先複習以下原論文中所描述的動態路由偽代碼。

Routing 過程就是圖1右邊表述的更新過程,我們會計算 v_j 與 u_j|i hat 的乘積並將它與原來的 b_ij 相加而更新 b_ij,然後利用 softmax(b_ij) 更新 c_ij 而進一步修正了後一層的 Capsule 輸入 s_j。當輸出新的 v_j 後又可以迭代地更新 c_ij,這樣我們不需要反向傳播而直接通過計算輸入與輸出的一致性更新參數。

對於所有在 l 層的 Capsule i 和在 l+1 層的 Capsule j,先初始化 b_ij 等於零。然後迭代 r 次,每次先根據 b_i 計算 c_i,然後在利用 c_ij 與 u_j|i hat 計算 s_j 與 v_j。利用計算出來的 v_j 更新 b_ij 以進入下一個迭代循環更新 c_ij。該 Routing 算法十分容易收斂,基本上通過 3 次迭代就能有不錯的效果。

在以下定義的路由更新過程中,_update_routing 函數會對經精煉的輸入張量求和並執行 Squash 非線性變換。它的輸出激活值可作為 PrimaryCaps 層和 DigitCaps 層的最終輸出,後面我們將詳細討論這兩個層的實現。按照 _update_routing 函數的說明,它會基於當前層的激活值與前一層投票結果(即線性組合結果)之間的相似性,迭代地更新 logits 路由結果,即對輸入張量進行精煉。

def _update_routing(votes, biases, logit_shape, num_dims, input_dim, output_dim,

num_routing, leaky):

votes_t_shape = [3, 0, 1, 2]

for i in range(num_dims - 4):

votes_t_shape += [i + 4]

r_t_shape = [1, 2, 3, 0]

for i in range(num_dims - 4):

r_t_shape += [i + 4]

votes_trans = tf.transpose(votes, votes_t_shape)

def _body(i, logits, activations):

"""Routing while loop."""

# route: [batch, input_dim, output_dim, ...]

if leaky:

route = _leaky_routing(logits, output_dim)

else:

route = tf.nn.softmax(logits, dim=2)

preactivate_unrolled = route * votes_trans

preact_trans = tf.transpose(preactivate_unrolled, r_t_shape)

preactivate = tf.reduce_sum(preact_trans, axis=1) + biases

activation = _squash(preactivate)

activations = activations.write(i, activation)

# distances: [batch, input_dim, output_dim]

act_3d = tf.expand_dims(activation, 1)

tile_shape = np.ones(num_dims, dtype=np.int32).tolist()

tile_shape[1] = input_dim

act_replicated = tf.tile(act_3d, tile_shape)

distances = tf.reduce_sum(votes * act_replicated, axis=3)

logits += distances

return (i + 1, logits, activations)

activations = tf.TensorArray(

dtype=tf.float32, size=num_routing, clear_after_read=False)

logits = tf.fill(logit_shape, 0.0)

i = tf.constant(0, dtype=tf.int32)

_, logits, activations = tf.while_loop(

lambda i, logits, activations: i < num_routing,

_body,

loop_vars=[i, logits, activations],

swap_memory=True)

return activations.read(num_routing - 1)

如上所示,votes 為前一層經轉換的輸出張量。num_dims 為輸入 votes 的維度數量,對於全連接 Capsule 層來說,它的維度為 4,對於卷積層來說,它的維度為 6。input_dim 為 輸入層的 Capsule 單元數,output_dim 為輸出層的 Capsule 單元數。num_routing 為路由的迭代次數,而 leaky 則代表著是否使用前面定義的滲漏路由 _leaky_routing。

以上 _update_routing 函數最終會輸出一個激活張量,即上面原論文偽代碼中的 v_j。在初步討論了路由算法後,我們可以查看它到底用在了哪些地方,即哪些運算需要執行路由算法。從 Sara 的代碼上看,CapsNet 應該使用了兩次 Routing,即在第二層的卷積層和第三層的 Capsule 全連接層後各調用了一次。這一點與論文的描述和其他研究者所實現的復現有所不同。

下面,我們將依據原論文與 Sara 開源的實現討論 CapsNet 主體架構和 Margin loss 度量。這一部分是該論文與實現的核心,因此我們將重點關注這一部分而忽略後面構建的重構網絡與重構損失。

以下是 CapsNet 的整體架構:

第一個卷積層使用了 256 個 9×9 卷積核,步幅為 1,且使用了 ReLU 激活函數。該卷積操作應該沒有使用 Padding,輸出的張量才能是 20×20×256。此外,CapsNet 的卷積核感受野使用的是 9×9,相比於其它 3×3 或 5×5 的要大一些,這個能是因為較大的感受野在 CNN 層次較少的情況下能感受的信息越多。這兩層間的權值數量應該為 9×9×256+256=20992。

隨後,第二個卷積層 PrimaryCaps 開始作為 Capsule 層的輸入而構建相應的張量結構,我們可以從上圖看出第二層卷積操作後生成的張量維度為 6×6×8×32。如果我們先考慮 32 個(32 channel)9×9 的卷積核在步幅為 2 的情況下做卷積,那麼實際上得到的是傳統的 6×6×32 的張量,即等價於 6×6×1×32。因為傳統卷積操作每次計算的輸出都是一個標量,而 PrimaryCaps 的輸出需要是一個長度為 8 的向量,因此傳統卷積下的三維輸出張量 6×6×1×32 就需要變化為四維輸出張量 6×6×8×32。

第三層 DigitCaps 在第二層輸出的向量基礎上進行傳播與 Routing 更新。第二層共輸出 6×6×32=1152 個向量,每一個向量的維度為 8,即第 i 層共有 1152 個 Capsule 單元。而第三層 j 有 10 個標準的 Capsule 單元,每個 Capsule 的輸出向量有 16 個元素。前一層的 Capsule 單元數是 1152 個,那麼 w_ij 將有 1152×10 個,且每一個 w_ij 的維度為 8×16。當 u_i 與對應的 w_ij 相乘得到預測向量後,我們會有 1152×10 個耦合係數 c_ij,對應加權求和後會得到 10 個 16×1 的輸入向量。將該輸入向量輸入到「squashing」非線性函數中求得最終的輸出向量 v_j,其中 v_j 的長度就表示識別為某個類別的概率。

如下所示定義了構建 Capsule 層級的主體函數:

def _build_capsule(self, input_tensor, num_classes):

capsule1 = layers.conv_slim_capsule(

input_tensor,

input_dim=1,

output_dim=self._hparams.num_prime_capsules,

layer_name='conv_capsule1',

num_routing=1,

input_atoms=256,

output_atoms=8,

stride=2,

kernel_size=9,

padding=self._hparams.padding,

leaky=self._hparams.leaky,)

capsule1_atom_last = tf.transpose(capsule1, [0, 1, 3, 4, 2])

capsule1_3d = tf.reshape(capsule1_atom_last,

[tf.shape(input_tensor)[0], -1, 8])

_, _, _, height, width = capsule1.get_shape()

input_dim = self._hparams.num_prime_capsules * height.value * width.value

return layers.capsule(

input_tensor=capsule1_3d,

input_dim=input_dim,

output_dim=num_classes,

layer_name='capsule2',

input_atoms=8,

output_atoms=16,

num_routing=self._hparams.routing,

leaky=self._hparams.leaky,)

該函數在輸入 5 維張量 [batch, 1, 256, height, width] 和目標類別的數量 num_classes 後會輸出一個 3 維張量,它將表示 10 個類別的 Capsule 嵌入向量。也就是說,該函數將構建完整的 CapsNet 架構,並輸出 DigitCaps 層最後得到的 10 個 16 維向量。

該主體函數主要調用了一個 slim 卷積 Capsule 層和一個 Capsule 層。slim 卷積 Capsule 層主要將輸入張量轉換為 Capsule 格式,即上圖的 PrimaryCaps 層。而為了連接卷積 Capsule 層和頂部的全連接 Capsule 層,卷積 Capsule 層的網格位置將與不同類型的 Capsule 維度相合併,並且 Capsule 將為嵌入向量學習不同的變換。下面我們會詳細討論構建以上網絡主體的兩個函數。

在主體模型的代碼中(capsule_model.py 第 54 行),第二個卷積層需要通過調用 Sara 等人定義的 conv_slim_capsule 函數實現,以下的代碼構建了原論文中的 PrimaryCaps 層,其中 input_tensor 為原圖片經過一次卷積後的特徵圖,並增加一個維度以作為一個 Capsule 單元包含神經元的個數(構成向量)。

capsule1 = layers.conv_slim_capsule(

input_tensor,

input_dim=1,

output_dim=self._hparams.num_prime_capsules,

layer_name='conv_capsule1',

num_routing=1,

input_atoms=256,

output_atoms=8,

stride=2,

kernel_size=9,

padding=self._hparams.padding,

leaky=self._hparams.leaky,)

在上面這些參數中,input_tensor 為五維張量即標準卷積的四維張量再加上一維 Capsule 單元數(capsule_model.py 第 194 行)。input_dim 為上一個 Capsule 層的單元數或維度,output_dim 為多個並行卷積操作後所得到的 Capsule 單元數或維度。input_atoms 為前一層 Capsule 單元的元素數,即一個 Capsule 單元包含的神經元數量,這裡 256 代表第一個卷積層所產生的 256 張特徵圖,而 output_atoms 表示當前層的 Capsule 單元元素數,這裡的 8 可以是代表 8 張 6×6×1×32 的特徵圖。其實小編認為我們可以如 Sara 等人的實現將 PrimaryCaps 層看成 32 個 Capsule 單元,每個單元包含 8 個標量神經元,或者將其看成 8 個 Capsule 單元,每個單元包含 32 個標量神經元,這兩種表示方法應該是等價的。剩下其它的參數就和標準卷積層所定義的參數意義一樣,所以讀者可以閱讀原始碼詳細地了解。

在 layers.py 文件中(268 行),Sara 等人定義了 conv_slim_capsule 函數以完成 PrimaryCaps 層的構建。該函數使用 slim 對給定五維的輸入張量執行二維卷積,輸入張量的維度一般為 [batch, input_dim, input_atoms, input_height, input_width]。然後該函數將使用動態路由算法精煉前面卷積運算的結果,並對每一個 Capsule 單元應用非線性 Squash 函數。

conv_slim_capsule 函數所輸出的激活值張量維度為 [batch, output_dim, output_atoms, out_height, out_width]。如果 Padding 選擇的是『SAME』,那麼輸出特徵圖的高和寬就與輸入張量的寬和高。我們注意到執行卷積操作具體的函數為前面定義的_depthwise_conv3d,該函數將返回經過 2 維卷積的 6 維張量。

_depthwise_conv3d 函數在給定一個 5 維輸入張量的情況下會執行 2 維卷積運算,輸入張量的維度與 conv_slim_capsule 函數的輸入相同。_depthwise_conv3d 函數會將輸入 5 維張量中 Batch 和 input_dim 的乘積作為 1 維而壓縮為 4 維張量,即壓縮輸入張量的第一維與第二維為一個維度。之所以需要壓縮為 4 維,是因為我們需要將其作為 tf.nn.conv2d 的輸入。在執行卷積後,我們需要重新將這 4 維張量分解為 6 維張量。即將第一維分解為 Batch 和 input_dim,將第二維分解為 output_dim 和 output_atom。

def _depthwise_conv3d(input_tensor,

kernel,

input_dim,

output_dim,

input_atoms=8,

output_atoms=8,

stride=2,

padding='SAME'):

with tf.name_scope('conv'):

input_shape = tf.shape(input_tensor)

_, _, _, in_height, in_width = input_tensor.get_shape()

# Reshape input_tensor to 4D by merging first two dimmensions.

# tf.nn.conv2d only accepts 4D tensors.

input_tensor_reshaped = tf.reshape(input_tensor, [

input_shape[0] * input_dim, input_atoms, input_shape[3], input_shape[4]

])

input_tensor_reshaped.set_shape((None, input_atoms, in_height.value,

in_width.value))

conv = tf.nn.conv2d(

input_tensor_reshaped,

kernel,

[1, 1, stride, stride],

padding=padding,

data_format='NCHW')

conv_shape = tf.shape(conv)

_, _, conv_height, conv_width = conv.get_shape()

# Reshape back to 6D by splitting first dimmension to batch and input_dim

# and splitting second dimmension to output_dim and output_atoms.

conv_reshaped = tf.reshape(conv, [

input_shape[0], input_dim, output_dim, output_atoms, conv_shape[2],

conv_shape[3]

])

conv_reshaped.set_shape((None, input_dim, output_dim, output_atoms,

conv_height.value, conv_width.value))

return conv_reshaped, conv_shape, input_shape

如上所示為 _depthwise_conv3d 函數,其中參數 input_dim、output_dim 和 input_atoms 等參數的意義與 conv_slim_capsule 函數一致。該函數會返回 6 維張量 [batch, input_dim, output_dim, output_atoms, out_height, out_width]、卷積後的維度和輸入張量的維度,並在 conv_slim_capsule 函數中做進一步處理,下面我們將回頭繼續討論構建 PrimaryCaps 層的函數。

如下定義了 conv_slim_capsule 函數,其層級的每一個 Capsule 單元都為卷積單元,它們在位置網格和不同的下層 Capsule 單元間共享卷積核權重。因此,該函數可訓練的變量為卷積核權重 [kernel_size, kernel_size, input_atoms, output_dim * output_atoms] 和偏置項 [output_dim, output_atoms]。二維卷積的輸出為一個單 Capsule 單元,其通道為 Capsule 單元(atoms)的數量。因此,conv_slim_capsule 函數可以構建在二維卷積層的頂部,其中該二維卷積的 num_routing=1、input_dim=1 和 input_atoms=conv_channels。

通過觀察定義卷積權重的 kernel 變量,我們可以了解該運算本質上就是執行上一層特徵圖數為 input_atoms、本層卷積核數為 output_dim * output_atoms 的卷積操作。在完成卷積運算後,Sara 的實現接著調用了一次前面定義的路由算法,這似乎與 naturomics 等人復現的代碼有一些不同,他們在實現卷積後會將卷積結果直接投入 Squash 非線性函數。當然,原論文似乎也沒有體現這一點,我們都以為只有在 DigitCaps 層才會進行動態路由過程。

在後面的 capsule 函數中(layers.py 第 138 行),Sara 確實又調用了一次動態路由算法,我們會在後面討論該函數。因為路由算法計算了 Squash 值,因此返回的激活值可作為 PrimaryCaps 層的輸出。

def conv_slim_capsule(input_tensor,

input_dim,

output_dim,

layer_name,

input_atoms=8,

output_atoms=8,

stride=2,

kernel_size=5,

padding='SAME',

**routing_args):

with tf.variable_scope(layer_name):

kernel = variables.weight_variable(shape=[

kernel_size, kernel_size, input_atoms, output_dim * output_atoms

])

biases = variables.bias_variable([output_dim, output_atoms, 1, 1])

votes, votes_shape, input_shape = _depthwise_conv3d(

input_tensor, kernel, input_dim, output_dim, input_atoms, output_atoms,

stride, padding)

with tf.name_scope('routing'):

logit_shape = tf.stack([

input_shape[0], input_dim, output_dim, votes_shape[2], votes_shape[3]

])

biases_replicated = tf.tile(biases,

[1, 1, votes_shape[2], votes_shape[3]])

activations = _update_routing(

votes=votes,

biases=biases_replicated,

logit_shape=logit_shape,

num_dims=6,

input_dim=input_dim,

output_dim=output_dim,

**routing_args)

return activations

在返回 PrimaryCaps 層的 5 維張量後,主體函數會將其轉換維 3 維張量並饋送到 capsule 函數,從而構建一個全連接 Capsule 層。在給定輸入張量 [batch, input_dim, input_atoms] 後,Capsule 層將執行以下操作:

對於每一個輸入 Capsule 單元,將它與權重變量相乘以得到線性組合的結果(函數中表示為 votes 變量),這一步將得到原論文所述的 u_j|i hat,即 u_j|i hat = W_ij * u_i。線性組合後的結果維度為 [batch, input_dim, output_dim, output_atoms]。

通過迭代地執行路由過程更新與精煉前面線性組合的結果,即原論文中的 s_j = ∑ c_ij * u_j|i hat,其中 c_ij = softmax(b_ij)。

最後使用 Squash 函數將每個 Capsule 單元的輸出壓縮到 L2 範數小於 1 的情況。

此外,當前層的每一個 Capsule 單元對前一層的 Capsule 單元都保留一個權重張量。因此在訓練中,capsule 函數的權重 [input_dim * num_in_atoms, output_dim * num_out_atoms] 和偏置項 [output_dim * num_out_atoms] 都是需要更新的參數。

如下展示了 capsule 函數的定義,其中 input_dim 同樣為前一層的 Capsule 單元數,input_atoms 同樣為前一層每個 Capsule 單元的元素數,其它參數的意義與上面幾個函數都差不多。該函數將輸出張量 [batch, output_dim, output_atoms]。

def capsule(input_tensor,

input_dim,

output_dim,

layer_name,

input_atoms=8,

output_atoms=8,

**routing_args):

with tf.variable_scope(layer_name):

# weights variable will hold the state of the weights for the layer

weights = variables.weight_variable(

[input_dim, input_atoms, output_dim * output_atoms])

biases = variables.bias_variable([output_dim, output_atoms])

with tf.name_scope('Wx_plus_b'):

# Depthwise matmul: [b, d, c] ** [d, c, o_c] = [b, d, o_c]

# To do this: tile input, do element-wise multiplication and reduce

# sum over input_atoms dimmension.

input_tiled = tf.tile(

tf.expand_dims(input_tensor, -1),

[1, 1, 1, output_dim * output_atoms])

votes = tf.reduce_sum(input_tiled * weights, axis=2)

votes_reshaped = tf.reshape(votes,

[-1, input_dim, output_dim, output_atoms])

with tf.name_scope('routing'):

input_shape = tf.shape(input_tensor)

logit_shape = tf.stack([input_shape[0], input_dim, output_dim])

activations = _update_routing(

votes=votes_reshaped,

biases=biases,

logit_shape=logit_shape,

num_dims=4,

input_dim=input_dim,

output_dim=output_dim,

**routing_args)

return activations

如上所示,tf.expand_dims 會將輸入張量在最後擴充一維,而 tf.tile 會將擴充後的 4 維張量在最後一維複製 output_dim * output_atoms 次。在執行逐元素的乘積後,沿著第三維 input_atoms 對乘積結果求和。求和後的張量可作為最後的線性組合結果而投入路由算法中進行迭代更新,返回的張量即 DigitCaps 層最終輸出的 10 個 16 維的向量,每個向量編碼並表徵著 10 類手寫數字。

在調用完 capsule 函數後,整個_build_capsule 函數所構建的 CapsNet 架構就完成了。原論文使用了 Margin loss 來衡量這 10 個輸出向量預測類別的準確度,而後面也可以使用全連接網絡將這 10 個向量重構為不同手寫數字的圖像,並使用歐幾裡得距離度量重構損失。

在論文解讀中,我們已經了解 DigitCaps 層輸出向量的長度即某個類別的概率,那麼我們該如何構建損失函數,並根據該損失函數迭代地更新整個網絡?前面我們耦合係數 c_ij 是通過一致性 Routing 進行更新的,他並不需要根據損失函數更新,但整個網絡其它的卷積參數和 Capsule 內的 W_ij 都需要根據損失函數進行更新。一般我們就可以對損失函數直接使用標準的反向傳播更新這些參數,而在原論文中,作者採用了 SVM 中常用的 Margin loss,該損失函數的表達式為:

其中 c 是分類類別,T_c 為分類的指示函數(c 存在為 1,c 不存在為 0),m+ 為上邊界,m- 為下邊界。此外,v_c 的模即向量的 L2 距離。

因為實例化向量的長度來表示 Capsule 要表徵的實體是否存在,所以若且唯若圖片裡出現屬於類別 k 的手寫數字時,我們希望類別 k 的最頂層 Capsule 的輸出向量長度很大(在本論文 CapsNet 中為 DigitCaps 層的輸出)。為了允許一張圖裡有多個數字,我們對每一個表徵數字 k 的 Capsule 分別給出單獨的 Margin loss。

以下_margin_loss 定義了 CapsNet 的損失函數,它會懲罰每個輸入分對數偏離邊緣的程度。如函數說明所示,該函數將衡量每一個錯誤分對數對於邊緣的距離。對於負的分對數來說,邊緣為 0.1,對於正的分對數,邊緣為 0.9。若我們同時對這兩個邊緣先減去 0.5,那麼當前的邊緣將會都變為 0.4。

def _margin_loss(labels, raw_logits, margin=0.4, downweight=0.5):

"""Penalizes deviations from margin for each logit.

Each wrong logit costs its distance to margin. For negative logits margin is

0.1 and for positives it is 0.9. First subtract 0.5 from all logits. Now

margin is 0.4 from each side.

Args:

labels: tensor, one hot encoding of ground truth.

raw_logits: tensor, model predictions in range [0, 1]

margin: scalar, the margin after subtracting 0.5 from raw_logits.

downweight: scalar, the factor for negative cost.

Returns:

A tensor with cost for each data point of shape [batch_size].

"""

logits = raw_logits - 0.5

positive_cost = labels * tf.cast(tf.less(logits, margin),

tf.float32) * tf.pow(logits - margin, 2)

negative_cost = (1 - labels) * tf.cast(

tf.greater(logits, -margin), tf.float32) * tf.pow(logits + margin, 2)

return0.5 * positive_cost + downweight * 0.5 * negative_cost

如上所示,margin 為 raw_logits 減去 0.5 的邊緣,而 downweight 負成本的因素。

以上是 CapsNet 的主體代碼,也是整個 Capsule 的核心。Sara 開源的實現還有很多重要的代碼與函數,我們也將繼續探討與思考其中具體的過程,尤其是理解_leaky_routing 的作用。我們也希望有讀者和我們一起解析與分析 Sara 的實現,並探討其最終實現的基線結果。

以下是 Sara 實現的說明,其使用的是 Python 2.7,不過 naturomics 等研究者已經在 GitHub 上修改為了 Python 3。讀者可進一步測試與實現它們:

要求:

TensorFlow

NumPy

GPU

運行以下命令以測試配置是否正確:

python layers_test.py

若我們下載了 Sara 等人提供的 MNIST 數據集和預訓練模型,並把它們放入$DATA_DIR/和$CKPT_DIR/ 目錄下。那麼我們可以運行以下命令而快速獲得 CapsNet 在 MNIST 上的測試結果。

數據集:https://storage.googleapis.com/capsule_toronto/mnist_data.tar.gz

模型:https://storage.googleapis.com/capsule_toronto/mnist_checkpoints.tar.gz

python experiment.py --data_dir=$DATA_DIR/mnist_data/ --train=false \

--summary_dir=/tmp/ --checkpoint=$CKPT_DIR/mnist_checkpoint/model.ckpt-1

下載並抽取二進位版的 cifar10 數據集到 $DATA_DIR/,cifar 10 預訓練模型到 $CKPT_DIR/。

數據集:https://www.cs.toronto.edu/~kriz/cifar.html

模型:https://storage.googleapis.com/capsule_toronto/cifar_checkpoints.tar.gz

python experiment.py --data_dir=$DATA_DIR --train=false --dataset=cifar10 \

--hparams_override=num_prime_capsules=64,padding=SAME,leaky=true,remake=false \

--summary_dir=/tmp/ --checkpoint=$CKPT_DIR/cifar/cifar{}/model.ckpt-600000 \

--num_trials=7

Sample CIFAR10 訓練命令:

python experiment.py --data_dir=$DATA_DIR --dataset=cifar10 --max_steps=600000\

--hparams_override=num_prime_capsules=64,padding=SAME,leaky=true,remake=false \

--summary_dir=/tmp/

Sample MNIST 全部訓練命令:

訓練-驗證傳播模式: --validate=true

在更多 GPU 上訓練:—num_gpus=NUM_GPUS

python experiment.py --data_dir=$DATA_DIR/mnist_data/ --max_steps=300000\

--summary_dir=/tmp/attempt0/

Sample MNIST 基線訓練命令:

python experiment.py --data_dir=$DATA_DIR/mnist_data/ --max_steps=300000\

--summary_dir=/tmp/attempt1/ --model=baseline

該實現的詳細使用方法請查閱 README 文件,我們以上只簡要介紹了一部分。我們也嘗試著實現 Sara 開源的模型,最開始是 xrange 等 Python 2.7 的函數會報錯,在改為 Python 3 後運行仍然會有問題。所以我們更希望有讀者能完成該模型的測試,並向大家展示 Dynamic Routing Between Capsules 論文所實現的結果。

相關焦點

  • 代碼搜尋引擎和代碼瀏覽器 Sourcegraph 宣布開源
    Sourcegraph 是一款能夠根據語義來把 Web 上的開原始碼編入索引的代碼搜索瀏覽工具。你可以從代碼倉庫和安裝包,甚至是函數裡搜索代碼,同時也可以直接點擊被完全創建了連結的代碼來閱讀文檔、跳轉到變量定義或者馬上找到可用的 Demo。總而言之,你可以在你的 Web 瀏覽器上完成這一切,而不需要配置任何編輯器。
  • 解讀NeurIPS-AutoDL 總決賽冠軍解決方案,代碼已開源
    機器之心發布作者:深度賦智由 NeurIPS 舉辦歷時四個月的 AutoDL 2019-2020(自動深度學習) 系列競賽總決賽在 4 月 18 日落下帷幕,來自深度賦智的 DeepWisdom 團隊榮獲冠軍。本文介紹了來自冠軍團隊的解決方案。
  • 機器之心年度盤點:2018年重大研究與開源項目
    機器之心原創作者:思源、劉曉坤2018 年即將結束,要問今年深度學習領域有什麼要關注的進展,恐怕首先想到的就是 Deepmind 的 BigGAN 和 Google在這篇文章中,機器之心從想法到實踐介紹了 2018 年令人矚目的研究工作,它們共同構建了機器學習的當下。我們主要按領域從模型到開源工具展開,其中算法或模型的選擇標準主要是效果和潛力,而開源工具的選擇主要憑藉 GitHub 的收藏量與效果。
  • 幾行代碼搞定ML模型,低代碼機器學習Python庫正式開源
    機器之心機器之心報導機器之心編輯部PyCaret 庫支持在「低代碼」環境中訓練和部署有監督以及無監督的機器學習模型,提升機器學習實驗的效率。想提高機器學習實驗的效率,把更多精力放在解決業務問題而不是寫代碼上?低代碼平臺或許是個不錯的選擇。最近,機器之心發現了一個開源低代碼機器學習 Python 庫 PyCaret,它支持在「低代碼」環境中訓練和部署有監督以及無監督的機器學習模型。
  • imToken錢包開原始碼 開發者最好的節日禮物
    除了節日本身,加密錢包imToken宣布正式開源的消息,又為加密世界帶來了一種新的歡快。  正式宣布開源之前,imToken其實已經開源過EOS投票工具StakeVote、去中心化交易所功能工具Tokenlon SDK。雖然之前的開源可能跟加密錢包開發沒有太大關係,此次開源的TokenCore部分則涉及imToken應用對「錢包私鑰」的管理和維護。
  • 封禁醜聞不斷,開原始碼託管動了誰的奶酪?
    Aurelia是微軟開發的JavaScript框架,已開源了5年,由一家美國公司管理。JavaScript是一種高級的解釋性程式語言,與HTML、CSS一起被認為是網際網路內容工程的三大核心技術,它可用於生成交互式的動態網頁,並且能夠提供視頻遊戲等在線程序。
  • 擁抱開源:軟體時代的車企新挑戰|linux|github|原始碼|作業系統...
    無論是在公司電腦硬體端禁用USB等外接儲存設備接口,還是在電子郵箱中設置敏感詞和代碼過濾機制,在過去數十年裡,製造業最重要的核心資產始終是濃縮了工程師心血的設計圖紙以及精密的生產工藝。出於對後發國家復刻設計方案的提防,對技術和專利的共享或開放從未在車企內部扮演過重要角色。
  • Hinton膠囊網絡代碼正式開源,你也能為GitHub1.4萬fork的庫貢獻
    Hinton膠囊網絡論文《Dynamic Routing between Capsules》的一作Sara Sabour日前在GitHub公布了代碼,使用TensorFlow和NumPy實現,只有一臺GPU也行,僅僅5天,fork的數量就超過了1.4萬。實際上,在官方代碼公布前,已經有很多其他版本和實現。
  • 商湯開源最大目標跟蹤庫PySOT,代碼已正式上線!
    商湯開源最大目標跟蹤庫PySOT:含SiamRPN++和SiamMask等算法,介紹了來自商湯科技的STVIR(SenseTime Video Intelligence Research team)開源的目標跟蹤庫:PySOT。當時PySOT並沒有上傳代碼,所以之前的文章僅介紹了新特性,但得到大家廣泛關注。
  • 不到1000行代碼,GitHub 1400星,天才黑客開源深度學習框架tinygrad
    來源:機器之心 本文約2000字,建議閱讀5分鐘 最近,天才黑客 George Hotz 開源了一個小型深度學習框架 tinygrad,兼具 PyTorch
  • Python 代碼轉 Latex 公式,這個開源庫用一行代碼幫你搞定
    轉自 | 機器之心數學是數據科學和機器學習的重要基礎,
  • 論文不公開代碼,應該被直接拒稿?
    機器之心報導機器之心編輯部論文代碼是否應該公開已是爭論已久的問題,有從業者呼籲通過代碼提交減少當下各類論文中的「水分」,也有研發人員表示「代碼提交」類問題得因「研究」而異。例如,一篇偏理論的論文其算法可能不是核心,又或者由於研究所用數據涉及所有權問題,因此代碼無法公開,從而導致可復現性受到阻礙。那麼研究論文的代碼是否應該「開源」?我們來看開發者們的觀點。論文代碼是否應該「強制」開源?
  • GitHub官方代碼掃描工具上線,免費查找漏洞
    在 GitHub 發布項目之前,你可以用免費的官方代碼掃描程序來檢查 Bug 了。機器之心報導,作者:蛋醬編程很難,難就難在常有 Bug 而不自知。有程式設計師調侃:「我不是在寫代碼,我是在寫 Bug。」從現在開始,你在 GitHub 上傳的代碼可以免費使用 Bug 篩查程序了。早發現,早報告,早診斷…… 以及早修復。去年 9 月,GitHub 收購代碼分析平臺企業 Semmle,宣布將在 GitHub 的開發者工作流程中引入代碼安全性流程。
  • Python代碼轉Latex公式,這個開源庫用一行代碼幫你搞定
    機器之心報導  編輯:小舟  你的代碼中有數學公式嗎?  數學是數據科學和機器學習的重要基礎,數學運算的結果對於機器學習項目而言是至關重要的。在編寫代碼時,我們常常需要定義數學公式的計算形式。像 S=r^2 這樣簡單的數學公式,大概不會出現拼寫錯誤。
  • 主動公開原始碼,企業在做賠本買賣?
    前段時間,騰訊雲相關負責人在公開場合正式發布其在基礎設施層面的四大核心技術項目,分別涵蓋數據中心、網絡、伺服器以及自動化平臺領域。同時,為全面擁抱開源,騰訊雲宣布後續會將這四大技術項目全部貢獻給OCP(Open Compute Project,開源計算項目)社區。
  • 免費/開源/輕量級的網頁代碼編輯器推薦(Mac/Win)
    如果用系統自帶的文本編輯器修改保存後,這些隱藏信息會丟失,文件代碼雖然看上去一樣,但是上傳到伺服器上,會導致網站出錯。先說下我對文本編輯器的要求這種代碼編輯器軟體,我也一直在用,以前用的最多的是Dreamweaver,現在有時候也用,但是這個軟體有兩個缺點,一是太大了,安裝麻煩,啟動慢,如果只是修改幾個參數,可以選擇輕量級免費開源的軟體。
  • 谷歌開源量子計算軟體原始碼,便利科學家利用量子計算機
    開源、開源、開源,重要的事情說三遍。繼開源tensorflow、caffe等深度學習開發框架後,當地時間10月24日,谷歌在自己的官方博客上宣布,開源量子計算軟體OpenFermion,從而讓科學家更方便的使用量子計算機。
  • 機器編程來了!未來全球78億人都能寫代碼?
    賈斯汀·戈茨利希認為,這從軟體誕生之日就困擾著一代又一代程式設計師的問題其實並非無解,他認為:「我們能創造一個人人都是軟體開發者的社會,屆時機器將會承擔編程部分的工作即機器編程,讓代碼不再是『手工藝品』。」  吳家驥向記者介紹,所謂機器編程,就是通過機器學習和其他自動化方法,設計可以自動編寫軟體的軟體,它涉及形式化方法、程式語言、編譯器、計算機系統等多個領域。
  • 英特爾機器編程工具可檢測代碼中的Bug
    ControlFlag,它可以自主檢測代碼中的錯誤。通過ControlFlag以及類似的系統,程式設計師有望大幅減少Debug的時間並把更多時間用於人類程式設計師最擅長的工作——向機器展現有創造性的新想法。」此外,找到能夠為跨架構的硬體正確、高效、安全地寫代碼的程式設計師非常困難,這同樣也增加了代碼中出現難以發現的新錯誤的可能性。因此,Debug代碼工作將給開發者和整個行業帶來更高的代價。
  • 交付程序不給錢,程式設計師一怒之下開源客戶項目代碼
    Jason 沒有取得相應的報酬,對其創造的代碼自然有全權處理的資格,但如果取得了報酬或者部分報酬,這樣的問題該如何處置?當開發者面對道德選擇與法律困境時,又該作何取捨?這事兒是職業道德問題?跟 Jason 相類似的情況在國內也有發生過,但更多的是一些私自開源公司代碼、刪庫跑路的相關新聞,這背後,折射的是開發者的職業道德問題嗎?