"我要指出 CNN 的問題,告訴你們爲什麼 CNN 是垃圾。"
偉大先驅Hinton發表在NIPS2017的首次正式提出Capsules,但其實Hinton早在2011年就有類似的想法了,原因是他認爲CNN不夠好,有很多的缺點,其中最突出的就是:CNN 不使用座標系,當我們人類觀察東西的時候,只要看到一個形狀,我們就會給它假定一個座標系。但CNN不具備這種旋轉組合的能力。
- 不變性 (invariance) 。指不隨一些變換來識別一個物體,具體變換包括平移 (translation),旋轉 (rotation),視角 (viewpoint),放縮 (scale) 等。不變性通常在物體識別上是好事,因爲不管一個房子怎麼平移、2D旋轉、3D旋轉和放縮,我們都可以識別出它是房子。但如果我們的任務比物體識別稍微困難一點,比如我想知道它平移了多少個單位,旋轉了多少度,放縮了百分之多少,那麼不變性遠遠不夠,這時需要的是同變性。
- 同變性(equivariance) 。不變性是表示不隨變換 (transformation) 變化,而同變性就是表示的變換等價於變換的表示。
著名的就是將人臉的眼睛和鼻子互換之後這種很不合理的圖仍然被識別成person咯,由於不變性,對於模型來說圖片中某些部分不管怎麼變動是一樣的,即便它是不合理的。所以我們想要的不僅僅是有眼睛,鼻子,嘴巴這些組件就能夠成爲person了,而且需要這三者能夠有一定的空間相互關係纔行,這是因爲CNN在pooling的時候,會丟失掉一些特徵間的關係(如max pooling,不管感受野裏面哪一塊數據最大,結果都是一樣的),雖然對於識別整張圖像做分類之類沒有很大的影響,但在需要更精確得到位置的場景下就無能爲力了,所以在這種情況下pooling是沒有用的(只能給出這個卷積核區域中的最大值,但是並不知道是從什麼方向來的)。
所以能否把這個普通的值,變成方向呢??即向量不僅僅只是向量了,應該變成能夠表示這種空間關係的張量!帶着方向感,所以某種程度上,膠囊網絡叫做向量神經元 (vector neuron) 甚至張量神經元 (tensor neuron) 更貼切。爲了解決這個問題,Hinton提出使用一個叫做“routing-by-agreement”的過程。這意味着,較爲底層的特徵(手、眼睛、嘴巴等)將只被傳送到與之匹配的高層。如果,底層特徵包含的是類似於眼睛或者嘴巴的特徵,它將傳遞到“面部”的高層,如果底層特徵包含的是類似手指、手掌等特徵,它將傳遞到“手”的高層。
「膠囊」的出發點是,在神經網絡中建立更多的結構,然後希望這些新增的結構可以幫助模型更好的泛化。即capsule想要學習到的就是object part組件之間的方向性,以更好的保留對象的姿態信息。如上圖裏面的v1,v2兩個組件通過一些空間關係最後組成一個entity,其中的v1組件的空間位置仍然是用向量,如v1的向左和向右對於向量的變化雖然是某一維的對立如是-1,1,但我們想要的結果是 :
- 向量不同,但表達的事物是一致的,不影響分類
- 通過控制這一維,可以得到特定旋轉的組件
- 並通過多個這樣的組件,組合成爲最後的結果
如何做到?Dynamic Routing Between Capsules。
Dynamic Routing Between Capsules
所謂的一個膠囊Capsule其實就是一種試圖在給定位置上預測是否有特定對象存在,以及其實例化參數的方法。激活向量的方向是由這些對象的實例化參數編碼而成,然後用每個激活向量的模長代表着預測出的要找的東西確實存在的概率。如上圖是控制方向的capsule(即一個組件),藍色箭頭和黑色箭頭,如果要組合成一個帆船,兩個箭頭有多種旋轉的組合,最匹配當前image的就是activations中最長的兩個箭頭。當然除了旋轉角度還可以有很多其他種,比如對象的大小,如何延伸,如何扭曲等等,即很多Capsules去組合更復雜的結果。
首先比較一下傳統神經網絡和膠囊的區別:
- Neuron:output a value。每個Neuron只做一件事,識別某個特定的區域,靜態平移不變的。
- Capsule:output a vector。而capsule想要識別某一類某一組件的特徵表達,動態可組合的。
在傳統神經網絡裏,一個神經元一般會進行如下的標量操作:
- 輸入標量的標量加權,。
- 對加權後的標量求和,。
- 對和進行非線性變換生成新標量,Sigmoid,ReLU等等。
而在膠囊網絡裏面:
- 輸入向量與權重矩陣的矩陣乘法,泛函空間映射,。因爲對於向量的矩陣變化是一種空間變化關係,通過W可以編碼圖像中低級特徵和高級特徵之間非常重要的空間關係。
- 加權輸入向量,。這些權重決定當前膠囊將其輸出發送到哪個更高級的膠囊(這個c就和神經網絡裏面的W很類似了,確定每個膠囊的權重),通過動態路由(dynamic routing)的過程完成的。
- 對加權後的向量求和,。 這一點沒什麼差別。
- 非線性化使用squashing函數生成新向量。該函數將向量進行壓縮使得它的最大長度爲1,最小長度爲0,同時保持其方向不變。看圖中的公式可以發現,在向量維度很長的時候它近乎於1,很短的時候近乎於0,這和Sigmoid函數很像,也不是線性變換的。
通過對比,最大的不同應該就是vector to vector(tensor to tensor)了,以及處理這種輸入的動態路由。動態路由的流程示意圖如下。
先看算法流程如下:
- 對於底層的兩個膠囊,,先通過進行空間映射得到,.
- 然後利用,計算權重,,加和得到
- 使用squashing激活就得到了
- 利用更新,,即
- 通過,重複第二步,如圖中的紅線
- 就得到下一次的,,同樣加和得到
- 也squashing得到,同理得到
- 即是更新後的新向量
這個過程看起來有些奇怪,但實際上不就是對每個底層膠囊計算一個softmax權重之後,再用類似RNN的循環操作不斷的調整每個膠囊對結果的權重,這也就是“動態”的含義了。這裏權重是基於一次結果的a與每個u的點積之後再softmax得到的(很像Attention),然後不斷每次的調整。如果預測向量a與上層膠囊的u具有較大的內積(內積描述兩個向量的相關度!),則存在自頂向下的反饋(相關度更高就應該加強這個方向的權重),所以就具有增加上層膠囊耦合係數,減小其他膠囊耦合係數的效果(softmax重新分配權重)。
直觀來講這樣的處理,是第一層的膠囊試圖預測下一層膠囊將要輸出什麼樣的結果,所以不斷更新b然後更新c的實質是學習到了部分與整體的關係(part-whole)(部分與整體這一點體現在,整體的結果與每個part膠囊的內積決定,這也就達成了不僅僅是學到圖像中有什麼組件,還想學到它們更爲細緻的關係),並自動地將學到的知識推廣到不同的新場景中。
這種類似RNN串聯的思路,使每次節點得到結果後,需要再回過頭比較當前結果的平均方向(sum的整體)和各個節點的建議方向(部分)是否一致,即動態路由以決定重要程度,再通過激活函數表示能夠學習到底層節點間的位置空間關係。其實這個更新和knn聚類很像,等於說離羣的點就幾乎沒可能。
Capsules Network:
完整的結構如下圖。
首先由普通的Conv得到向量,然後直接reshape成多個(32個),即得到它們的“部分”,將這些part輸出到caps膠囊網絡中,用上面的方法進行更新和計算。對於minist任務來說,對每個數字的模式各設置一個cap膠囊,即圖中10維的DigitCaps,最後使用向量模長代表概率(這也是squashing的結果需要設計在0-1之間)。
實際上最後還會有一個3層的FC做圖像重構,然後loss由兩部分得到margin loss就是強制使capsule之間的差別越來越大(如預測1的膠囊要和2的膠囊越大越好,),而採用reconstruction重建損失的好處是可以強制網絡保留所有重建圖像所需要的信息(其實強烈懷疑capsule之所以work,這個重建loss作用很大)。
pytorch版本的實現代碼如下:
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
from torch.optim import Adam
from torchvision import datasets, transforms
USE_CUDA = True #gpu
#載入Mnist數據
class Mnist:
def __init__(self, batch_size):
dataset_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST('../data', train=True, download=True, transform=dataset_transform)
test_dataset = datasets.MNIST('../data', train=False, download=True, transform=dataset_transform)
self.train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
self.test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
#按模型框架圖,先由普通卷積層再PrimaryCaps,最後映射到DigitCaps。
#普通卷積層
class ConvLayer(nn.Module):
def __init__(self, in_channels=1, out_channels=256, kernel_size=9):
#9x9卷積,256個通道,輸出的大小是20x20x256
#大一些的感受野能在層數較少的情況下得到更多的信息
super(ConvLayer, self).__init__()
self.conv = nn.Conv2d(in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=1
)
def forward(self, x):
return F.relu(self.conv(x))
#Primarycaps卷積
class PrimaryCaps(nn.Module):
def __init__(self, num_capsules=8, in_channels=256, out_channels=32, kernel_size=9):
#32個平行的卷積,每個數據爲8個分量
super(PrimaryCaps, self).__init__()
self.capsules = nn.ModuleList([
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=2, padding=0)
for _ in range(num_capsules)])
def forward(self, x):
u = [capsule(x) for capsule in self.capsules]#num_capsules個卷積層
u = torch.stack(u, dim=1)
u = u.view(x.size(0), 32 * 6 * 6, -1)#窗口大小是6x6
return self.squash(u)#squash激活函數擠壓向量
def squash(self, input_tensor):
#實現0-1的壓縮,同時保持其方向不變
squared_norm = (input_tensor ** 2).sum(-1, keepdim=True)
output_tensor = squared_norm * input_tensor / ((1. + squared_norm) * torch.sqrt(squared_norm))
return output_tensor
#DigitCaps膠囊層,最後輸出爲10個16分量的向量,分類結果是向量長度最大的輸出
class DigitCaps(nn.Module):
def __init__(self, num_capsules=10, num_routes=32 * 6 * 6, in_channels=8, out_channels=16):
super(DigitCaps, self).__init__()
self.in_channels = in_channels
self.num_routes = num_routes
self.num_capsules = num_capsules
self.W = nn.Parameter(torch.randn(1, num_routes, num_capsules, out_channels, in_channels))
def forward(self, x):
#先計算中間向量u
batch_size = x.size(0)
x = torch.stack([x] * self.num_capsules, dim=2).unsqueeze(4)
W = torch.cat([self.W] * batch_size, dim=0)
u_hat = torch.matmul(W, x)#輸入x通過W進行空間映射,編碼圖像中低級和高級特徵之間的空間關係
#b的初始化爲0
b_ij = Variable(torch.zeros(1, self.num_routes, self.num_capsules, 1))
if USE_CUDA:
b_ij = b_ij.cuda()
#動態路由
num_iterations = 3
for iteration in range(num_iterations):
c_ij = F.softmax(b_ij) #用b計算softmax的權重c
c_ij = torch.cat([c_ij] * batch_size, dim=0).unsqueeze(4)
s_j = (c_ij * u_hat).sum(dim=1, keepdim=True)#加權和
v_j = self.squash(s_j)#當前迭代的輸出
if iteration < num_iterations - 1: #更新a和b
a_ij = torch.matmul(u_hat.transpose(3, 4), torch.cat([v_j] * self.num_routes, dim=1))
b_ij = b_ij + a_ij.squeeze(4).mean(dim=0, keepdim=True)
return v_j.squeeze(1)#最後的輸出
def squash(self, input_tensor):
squared_norm = (input_tensor ** 2).sum(-1, keepdim=True)
output_tensor = squared_norm * input_tensor / ((1. + squared_norm) * torch.sqrt(squared_norm))
return output_tensor
#重構函數,強制網絡保留所有重建圖像所需要的信息
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
#從DigitCaps的16x10開始重建完整圖片
self.reconstraction_layers = nn.Sequential(
nn.Linear(16 * 10, 512),
nn.ReLU(inplace=True),
nn.Linear(512, 1024),
nn.ReLU(inplace=True),
nn.Linear(1024, 784),
nn.Sigmoid()
)
def forward(self, x, data):
classes = torch.sqrt((x ** 2).sum(2))
classes = F.softmax(classes)#最後輸出的x向量最長的爲最後的結果
_, max_length_indices = classes.max(dim=1)
masked = Variable(torch.sparse.torch.eye(10))#one-hot
if USE_CUDA:
masked = masked.cuda()
masked = masked.index_select(dim=0, index=max_length_indices.squeeze(1).data)
#3層FC做重建
reconstructions = self.reconstraction_layers((x * masked[:, :, None, None]).view(x.size(0), -1))
reconstructions = reconstructions.view(-1, 1, 28, 28)
return reconstructions, masked
#CapsNet完整的流程
class CapsNet(nn.Module):
def __init__(self):
super(CapsNet, self).__init__()
#由四個class組成
self.conv_layer = ConvLayer()
self.primary_capsules = PrimaryCaps()
self.digit_capsules = DigitCaps()
self.decoder = Decoder()
self.mse_loss = nn.MSELoss()#均方差
def forward(self, data):
#三層的膠囊網絡結構得到output
output = self.digit_capsules(self.primary_capsules(self.conv_layer(data)))
#用輸出重建圖像
reconstructions, masked = self.decoder(output, data)
return output, reconstructions, masked
def loss(self, data, x, target, reconstructions):
#完整的loss由margin和reconstruction兩部分組成
return self.margin_loss(x, target) + self.reconstruction_loss(data, reconstructions)
def margin_loss(self, x, labels, size_average=True):
#margin loss強制使capsule之間(如預測1和預測2)的差別越來越大
batch_size = x.size(0)
v_c = torch.sqrt((x**2).sum(dim=2, keepdim=True))#長度表示某個類別的概率
#上邊界和下邊界
left = F.relu(0.9 - v_c).view(batch_size, -1)
right = F.relu(v_c - 0.1).view(batch_size, -1)
#懲罰偏離邊緣(錯位的分類對邊緣0.1 or 0.9的距離)
#如果預測是0.8,label是1,那麼loss是0.1很小
#如果label是0,那麼loss的懲罰要算與right的距離,其中0.5是downweight
loss = labels * left + 0.5 * (1.0 - labels) * right
loss = loss.sum(dim=1).mean()
return loss
def reconstruction_loss(self, data, reconstructions):
loss = self.mse_loss(reconstructions.view(reconstructions.size(0), -1), data.view(reconstructions.size(0), -1))
return loss * 0.0005 #這個係數爲0.0005
capsule_net = CapsNet()
if USE_CUDA:
capsule_net = capsule_net.cuda()
optimizer = Adam(capsule_net.parameters())#優化器
batch_size = 100
mnist = Mnist(batch_size)#導入數據
n_epochs = 30#週期
#開始訓練
for epoch in range(n_epochs):
capsule_net.train()#調到訓練模式
train_loss = 0
for batch_id, (data, target) in enumerate(mnist.train_loader):
target = torch.sparse.torch.eye(10).index_select(dim=0, index=target)#手寫數字10維
data, target = Variable(data), Variable(target)
if USE_CUDA:#gpu
data, target = data.cuda(), target.cuda()
optimizer.zero_grad() #梯度清零
output, reconstructions, masked = capsule_net(data) #得到模型輸出
loss = capsule_net.loss(data, output, target, reconstructions) #計算loss
loss.backward() #反向傳播
optimizer.step() #參數更新
train_loss += loss.data[0]#記錄總loss
if batch_id % 100 == 0: #定期打印結果
print "train accuracy:", sum(np.argmax(masked.data.cpu().numpy(), 1) ==
np.argmax(target.data.cpu().numpy(), 1)) / float(batch_size)
print train_loss / len(mnist.train_loader)
capsule_net.eval()#評估模式
test_loss = 0
for batch_id, (data, target) in enumerate(mnist.test_loader):
target = torch.sparse.torch.eye(10).index_select(dim=0, index=target)
data, target = Variable(data), Variable(target)
if USE_CUDA:#gpu
data, target = data.cuda(), target.cuda()
output, reconstructions, masked = capsule_net(data)#得到評估結果
loss = capsule_net.loss(data, output, target, reconstructions)#計算loss
test_loss += loss.data[0]
if batch_id % 100 == 0: #定期打印結果
print "test accuracy:", sum(np.argmax(masked.data.cpu().numpy(), 1) ==
np.argmax(target.data.cpu().numpy(), 1)) / float(batch_size)
print test_loss / len(mnist.test_loader)
#可視化
import matplotlib
import matplotlib.pyplot as plt
def plot_images_separately(images):
"Plot the six MNIST images separately."
fig = plt.figure()
for j in xrange(1, 7):
ax = fig.add_subplot(1, 6, j)
ax.matshow(images[j-1], cmap = matplotlib.cm.binary)
plt.xticks(np.array([]))
plt.yticks(np.array([]))
plt.show()
plot_images_separately(data[:6,0].data.cpu().numpy())#原結果
plot_images_separately(reconstructions[:6,0].data.cpu().numpy())#重建結果
完整逐行代碼解析::https://github.com/nakaizura/Source-Code-Notebook/tree/master/Capsules
Capsules的重點
- 想學到具體的實體或者關係模式,等變性
- 得到組件方向性,能動態的調整agreement routing
- Margin + reconstruction
Capsules的優缺點
優點主要有3點:
- 膠囊的輸出只會被引導至合適的下一層膠囊,這些膠囊將會獲得非常乾淨的輸入信號,並且更加精確的特定對象的姿態。
- 通過審視激活的路徑,我們能清楚的觀察到組件的層次結構。如下圖,可以發現某些中間cap確實學習到了一些姿態信息,某些控制粗細,某些與傾斜度,方向有關等。
- routing將會幫助解析擁有大量重疊在一起的對象所構成的擁擠場景或者曖昧不清。即overlapping handing,也是paper中的一個實驗,如下下圖,能比較好的顯示出overlapping 的現象。
缺點是:
- 訓練非常的慢,最大原因是routing有內部循環。
- 無論是什麼類型的對象,在給定位置永遠只有一個膠囊,所以它不能檢測出非常靠近的同一類型的對象,這種效應叫“擠出效應”,在人類視覺中也有這種現象,所以其實不算是很大的問題。
Capsules的到底學習什麼?
下圖是李宏毅老師對invariance和equivariance的discusstion了。對於左邊的圖是普通的CNN經過max pooling的結果,對於不同的輸入max的結果都是3,這種情況就容易導致在人眼鏡,嘴巴等一些組件明明調換了,CNN卻仍然能識別出是person的不合理現象,“i dont know the difference”。因爲CNN想要學習到的是某一類整體的通用表示,不管什麼樣的圖,都得到同樣的結果就是最好的。
而對於右邊capsule,輸入的兩個“1”的傾斜度是對稱的,通過不同的cap得到的v是可以不一樣的,這能包含一些姿態信息,但只有保證最後的結果||v||值類似的就行,“i know the difference,but i do not react to it”。
Capsules的本質是什麼?
Hopping Memory Networks+Attention(或rnn)。畢竟每次都從前一次取值(memory)做調整(attention),而且多個(hopping)。
Capsules已經升級
從NeurIPS 2017 是關於路由。
ICLR 2018 使用了 EM 算法。
然後在 NeurIPS 2019 變成Stacked Capsule Auto-Encoders。
Matrix Capsules with EM Routing
使用EM算法做動態路由。在Dynamic Routing Between Capsules中動態路由是用內積算相似度然後得到權重再分配的,而作者認爲這種計算方法不能很好地區分“quite good”和“very good”的areement,所以改成高斯混合原型聚類的log variance做爲相似度(它是一個含有隱變量的多高斯生成模型,用EM進行參數估計)。在每對相鄰的膠囊層之間使用路由過程。 對於卷積膠囊,層L + 1中的每個膠囊僅向層L中的接收場內的膠囊發送反饋。
另外還把capsule的表示形式爲n維的vector(vector的每個維度表示一種特徵,||v||模表示顯著程度概率的大小)變成了n*n的姿態矩陣(pose matrix,可以學習表徵該實體與觀看者之間的關係),再另加一個scalar表示其activation。整個模型如下:
首先是5x5的CNN(圖中的ReLU Conv1),然後緊跟主膠囊primaryCaps和另外兩個膠囊ConvCaps1和ConvCaps2。PrimaryCaps和最初版的一樣做分組卷積,ConvCaps是基於前面得到capsule的卷積層,最後得到class。“傳播損失”來直接最大化目標類(at)的激活和其他類的激活之間的差距。
這些係數使用 EM 算法迭代式地更新,這樣每一個 capsule 的輸出都會被路由到上一層的一個 capsule,它會收到一組相似投票的集羣。
爲什麼要高斯再EM?
爲了推廣到新的觀點。即想通過尋找含有隱變量的模型表示來代表更高維的概念,如果多個底層capsule的點聚集在了一起,它們可能就是因爲同一個更高維的概念所生成的。
Stacked Capsule Auto-Encoders
https://arxiv.org/pdf/1906.06818.pdf
首先要把之前的那些版本的膠囊網絡的一切都忘了,它們都是錯的,只有現在這個是對的。–Hinton…
之前的版本都用了判別式學習,即尋找「部件-整體」的關係,嘗試把很多的部件進行拼接組合,預測組合是否是一個合理的整體。但用「部件-整體」關係的時候,如果部件的自由度比整體的自由度要少,好比部件是點,然後你要用點組成星座,那你很難從一個點的位置預測整個星座的位置,你需要用到很多點的位置;所以不能從單個部件出發對整體做預測。如何能用「整體-部件」關係就很好了。(果然無監督是未來。。。)
看論文名字,就是結合AE的無監督學習了。主要是設計了一個堆棧式膠囊自編碼器(SCAE)。一開始先用貪婪的方法訓練它 —— 從像素得到部件,從部件得到更大的部件,從更大的部件得到再大的部件。
- 生成器:部件膠囊自編碼器(PCAE)將圖像分割爲組成部分,推斷其姿態,並將每個圖像像素重建爲變換組件模板的像素混合(高斯混合)。如上圖從一個房子圖片中得到一些部分結構的姿態(觀察者和這些部件之間的關係,方向)。
- 判別器:目標膠囊自編碼器(OCAE)嘗試將發現的部件及其姿態安排在一個更小的目標集合中。這個目標集合對每個部件進行預測,從而解釋每個部件的姿態。如從姿態中嘗試還原一些“積木”,並嘗試尋找拼湊出一個完整的目標。
- 通過將它們的姿態——目標-觀察者關係(OV)和相關的目標-部件關係(OP)相乘,每個目標膠囊都會貢獻這些混合的一部分。
需要說明的是,生成式模型裏有兩種思想。首先,每個低層次的膠囊只會被一個高層次膠囊解釋 —— 這就形成了一個解析樹,在解析樹裏每個元素只有一個父項。其次,低層次的膠囊的姿態可以從高層次膠囊推導得到,就是通過高層次膠囊相對於觀察者的位姿和整體相對於部件的位姿做矩陣相乘,就得到了低層次膠囊相對於觀察者的位姿。視覺裏非常重要的兩件事,處理視角變化,以及建立解析樹,就這樣設計到了模型裏面。
非常全的膠囊網絡資源:
https://zhuanlan.zhihu.com/p/34336279
Hinton AAAI2020 演講:
https://www.sohu.com/a/372899758_651893