图像分类中,一旦卷积核检测到类似于眼睛、鼻子、嘴巴等特征,从数据角度上说,就是相关卷积核对鼻子、眼睛等卷积出来值很大,也就是与人脸相关的神经元就相当兴奋。那么就会将图像分类到人脸这一类中。那么就到导致一个问题:**如右图,眼睛、鼻子、嘴巴都有了,CNN理所应当地将其归于人脸。**这就暴露出CNN的两个问题:
- 组件的朝向和空间上的相对关系对它来说不重要,它只在乎有没有特征。
- 从网络设计上来说,池化层不仅 减少了参数,还可以避免过拟合。但是它的确 抛弃了一些信息,比如位置关系。
再比如说这张图:
尽管拍摄角度不同,但是大脑可以轻易辨识出这些都是同一对象,但 CNN 没有这样的能力。它不能举一反三,其只能通过 扩大训练的数据量 才能达到相似的能力。
上一列和下一列图片属于同一类,仅仅视角不同。CapsNet和其他模型相比表现好很多。
所谓胶囊 ,就是一个向量。它可包含任意个值, 每个值代表了当前需要识别的物体(比如图片)的一个特征。结合之前对传统CNN的学习,我们知道,卷积层的每个值 ,都是上一层某一块区域和卷积核完成卷积操作,即线性加权求和的结果,它只有 一个值 ,所以是标量。而我们的胶囊网络,它的每个值都是向量,也就是说,这个向量 不仅可表示物体的特征 ,还可以包括物体的方向、状态等等。
CapsNet的网络连接方式与全连接网络非常相似,在权重更新、网络输入还有激活函数方面有较大区别,其实代码上并不复杂,奈何,老爷子的文笔……最关键的是公式符号,实在是……好吧,下面根据论文的逻辑给大家一一梳理:
V j = s q u a s h ( S j ) = ∥ S j ∥ 2 1 + ∥ S j ∥ 2 S j ∥ S j ∥ → ∥ S j ∥ 1 + ∥ S j ∥ 2 S j → ∥ S j ∥ 0.5 + ∥ S j ∥ 2 S j {V_j} =squash(S_j)= \frac{{{{\left\| {{S_j}} \right\|}^2}}}{{1 + {{\left\| {{S_j}} \right\|}^2}}}\frac{{{S_j}}}{{\left\| {{S_j}} \right\|}} \to \frac{{\left\| {{S_j}} \right\|}}{{1 + {{\left\| {{S_j}} \right\|}^2}}}{S_j} \to \frac{{\left\| {{S_j}} \right\|}}{{0.5 + {{\left\| {{S_j}} \right\|}^2}}}{S_j} Vj=squash(Sj)=1+∥Sj∥2∥Sj∥2∥Sj∥Sj→1+∥Sj∥2∥Sj∥Sj→0.5+∥Sj∥2∥Sj∥Sj
其中:
我们在这里将1改为0.5, 激活函数的 前一部分 是输入向量 S S S 的的 缩放尺度,后一部分是 S S S 的 单位向量,该激活函数:
保留了输入向量的方向
将输入向量的模压缩到了0-1之间
也符合我们之前说的:用向量的模的大小衡量某个实体出现的概率,模值越大,概率越大。
代码
def squash(x, axis=-1):
s_squared_norm = K.sum(K.square(x), axis=axis, keepdims=True) + K.epsilon() # 即S向量模长的平方
scale = K.sqrt(s_squared_norm) /(0.5 + s_squared_norm)
return scale * x
张量Shape没有发生变化,因为代码效率的关系,我们会将论文中的某些公式用更加高效的方式实现,因为这样无需用Python中的for循环,可以借助Keras后端TensorFlow的底层库来提升运算效率。
其中 S j S_j Sj根据下列公式计算
S j = ∑ i c i j u ^ j ∣ i , u ^ j ∣ i = W i j u i {S_j} = \sum\limits_i^{} {{c_{ij}}{{\hat u}_{j|i}},{{\hat u}_{j|i}} = {W_{ij}}{u_i}} Sj=i∑ciju^j∣i,u^j∣i=Wijui
其中:
耦合系数根据下面公式计算,即为softmax函数,对 b i j b_{ij} bij进行softmax归一化,该公式由于后面需要特别指定轴,因此需要自己编写,公式如下:
c i j = e b i j ∑ k e b i k {c_{ij}} = \frac{{{e^{{b_{ij}}}}}}{{\sum\limits_k {{e^{{b_{ik}}}}} }} cij=k∑ebikebij
代码
"""定义我们自己的softmax函数
因为在后面的步骤中,轴发生了交换,因此需要特别指定softmax轴,其余的与softmax一样
"""
def softmax(x, axis=-1):
# -K.max(x, axis=axis, keepdims=True)为数值计算方法,不影响softmax结果,只是避免某个数值太大,导致计算误差
ex = K.exp(x - K.max(x, axis=axis, keepdims=True))
sum_ex = K.sum(ex, axis=axis, keepdims=True)
return ex / sum_ex
这里的耦合系数 c i j c_{ij} cij需要利用 b i j b_{ij} bij计算。
我们会将 b i j b_{ij} bij初始化为0,第一次得到的耦合系数 c i j c_{ij} cij值相同,并不能表现出 前一层的胶囊 和 后一层的胶囊 之间的关系。因此我们通过更新 b i j b_{ij} bij来更新 c i j c_{ij} cij, b i j b_{ij} bij的更新公式如下:
本论文通过 计算内积 的方式来 改变 b i j b_{ij} bij,从而改变 c i j c_{ij} cij。为什么这么做?如图所示:
点积运算
点积运算接收两个向量,并输出一个标量。 对于给定长度但方向不同的两个向量而言,点积有下列几种情况:正值,零,负值。因此:
通过迭代确定 c i j c_{ij} cij。也就确定了一条路线,这条路线上的胶囊神经元的模特别大,路线的尽头就是那个正确预测的胶囊。
根据论文的描述,代码如下:
核心代码
输入张量shape:(batch_size, input_num_capsule, input_dim_capsule)
输出张量shape:(batch_size, num_capsule, dim_capsule)
卷积核的kernel_size:(1, input_dim_capsule, num_capsule*dim_capsule)
卷积shape的计算公式是: (w+2p-f)/s+1向下取整
下面的代码删除掉了一些不必要的部分,另外尽量通过向量化以及一维卷积替代全连接运算来提升运算效率:
def call(self, inputs):
batch_size = K.shape(inputs)[0] # 批次数目
input_num_capsule = K.shape(inputs)[1] # 输入胶囊的个数
# --------------------- 这里用了一个比较精妙的实现,首先计算输入向量u_hat,其不随动态路由算法迭代改变
hat_inputs = K.conv1d(inputs, kernel=self.kernel) # 用一维卷积替代类似的全连接层
hat_inputs = K.reshape(hat_inputs, shape=(batch_size, input_num_capsule,
self.num_capsule, self.dim_capsule))
hat_inputs = K.permute_dimensions(hat_inputs, (0, 2, 1, 3)) # 交换轴
# ---------------------
# 动态路由算法更新b
b = K.zeros_like(hat_inputs[:, :, :, 0]) # 步骤1,初始化为0
for i in range(self.routing): # 迭代 步骤2-5 r次
c = softmax(b, axis=1) # 步骤2,softmax
s = K.batch_dot(c, hat_inputs, [2, 2]) # 步骤3,批量矩阵相乘
o = self.activation(s) # 步骤4
if i < self.routings - 1:
b += K.batch_dot(o, hat_inputs, [2, 3]) # 步骤5
return o
这里弄不明白没有关系,大神也是从菜鸟一步步过来的,等回过头来再弄明白也不迟。
其中 T c T_c Tc代表真实标签, m + = 0.9 m^+=0.9 m+=0.9, m − = 0.1 m_-=0.1 m−=0.1,推荐 λ = 0.5 \lambda=0.5 λ=0.5,总损失是所有胶囊损失简单的求和:
def margin_loss(y_true, y_pred):
lamb, margin = 0.5, 0.1
loss = y_true * K.square(K.relu((1-margin) - y_pred)) +
lamb * (1-y_true) * K.square(K.relu(y_pred - margin))
losses = K.sum(loss, axis=-1) # 这里 * 默认逐元素相乘
return losses
懒得解释,准备等草稿全部写完再统一整理,有些许参考价值就看吧,之后会有代码详解:
from keras import backend as K
from keras.layers import Layer
from keras import activations
from keras import utils
from keras.datasets import cifar10
from keras.models import Model
from keras.layers import *
from keras.preprocessing.image import ImageDataGenerator
from keras.utils import plot_model
from keras.callbacks import TensorBoard
batch_size = 128
num_classes = 10
epochs = 20
"""
压缩函数,我们使用0.5替代hinton论文中的1,如果是1,所有的向量的范数都将被缩小。
如果是0.5,小于0.5的范数将缩小,大于0.5的将被放大
"""
def squash(x, axis=-1):
s_quared_norm = K.sum(K.square(x), axis, keepdims=True) + K.epsilon()
scale = K.sqrt(s_quared_norm) / (0.5 + s_quared_norm)
result = scale * x
return result
# 定义我们自己的softmax函数,而不是K.softmax.因为K.softmax不能指定轴
def softmax(x, axis=-1):
ex = K.exp(x - K.max(x, axis=axis, keepdims=True))
result = ex / K.sum(ex, axis=axis, keepdims=True)
return result
# 定义边缘损失,输入y_true, p_pred,返回分数,传入即可fit时候即可
def margin_loss(y_true, y_pred):
lamb, margin = 0.5, 0.1
result = K.sum(y_true * K.square(K.relu(1 - margin -y_pred))
+ lamb * (1-y_true) * K.square(K.relu(y_pred - margin)), axis=-1)
return result
class Capsule(Layer):
"""编写自己的Keras层需要重写3个方法以及初始化方法
1.build(input_shape):这是你定义权重的地方。
这个方法必须设self.built = True,可以通过调用super([Layer], self).build()完成。
2.call(x):这里是编写层的功能逻辑的地方。
你只需要关注传入call的第一个参数:输入张量,除非你希望你的层支持masking。
3.compute_output_shape(input_shape):
如果你的层更改了输入张量的形状,你应该在这里定义形状变化的逻辑,这让Keras能够自动推断各层的形状。
4.初始化方法,你的神经层需要接受的参数
"""
def __init__(self,
num_capsule,
dim_capsule,
routings=3,
share_weights=True,
activation='squash',
**kwargs):
super(Capsule, self).__init__(**kwargs) # Capsule继承**kwargs参数
self.num_capsule = num_capsule
self.dim_capsule = dim_capsule
self.routings = routings
self.share_weights = share_weights
if activation == 'squash':
self.activation = squash
else:
self.activation = activation.get(activation) # 得到激活函数
# 定义权重
def build(self, input_shape):
input_dim_capsule = input_shape[-1]
if self.share_weights:
# 自定义权重
self.kernel = self.add_weight(
name='capsule_kernel',
shape=(1, input_dim_capsule,
self.num_capsule * self.dim_capsule),
initializer='glorot_uniform',
trainable=True)
else:
input_num_capsule = input_shape[-2]
self.kernel = self.add_weight(
name='capsule_kernel',
shape=(input_num_capsule, input_dim_capsule,
self.num_capsule * self.dim_capsule),
initializer='glorot_uniform',
trainable=True)
super(Capsule, self).build(input_shape) # 必须继承Layer的build方法
# 层的功能逻辑(核心)
def call(self, inputs):
if self.share_weights:
hat_inputs = K.conv1d(inputs, self.kernel)
else:
hat_inputs = K.local_conv1d(inputs, self.kernel, [1], [1])
batch_size = K.shape(inputs)[0]
input_num_capsule = K.shape(inputs)[1]
hat_inputs = K.reshape(hat_inputs,
(batch_size, input_num_capsule,
self.num_capsule, self.dim_capsule))
hat_inputs = K.permute_dimensions(hat_inputs, (0, 2, 1, 3))
b = K.zeros_like(hat_inputs[:, :, :, 0])
for i in range(self.routings):
c = softmax(b, 1)
o = self.activation(K.batch_dot(c, hat_inputs, [2, 2]))
if K.backend() == 'theano':
o = K.sum(o, axis=1)
if i < self.routings-1:
b += K.batch_dot(o, hat_inputs, [2, 3])
if K.backend() == 'theano':
o = K.sum(o, axis=1)
return o
def compute_output_shape(self, input_shape): # 自动推断shape
return (None, self.num_capsule, self.dim_capsule)
def MODEL():
input_image = Input(shape=(32, 32, 3))
x = Conv2D(64, (3, 3), activation='relu')(input_image)
x = Conv2D(64, (3, 3), activation='relu')(x)
x = AveragePooling2D((2, 2))(x)
x = Conv2D(128, (3, 3), activation='relu')(x)
x = Conv2D(128, (3, 3), activation='relu')(x)
"""
现在我们将它转换为(batch_size, input_num_capsule, input_dim_capsule),然后连接一个胶囊神经层。模型的最后输出是10个维度为16的胶囊网络的长度
"""
x = Reshape((-1, 128))(x) # (None, 100, 128) 相当于前一层胶囊(None, input_num, input_dim)
capsule = Capsule(num_capsule=10, dim_capsule=16, routings=3, share_weights=True)(x) # capsule-(None,10, 16)
output = Lambda(lambda z: K.sqrt(K.sum(K.square(z), axis=2)))(capsule) # 最后输出变成了10个概率值
model = Model(inputs=input_image, output=output)
return model
if __name__ == '__main__':
# 加载数据
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
y_train = utils.to_categorical(y_train, num_classes)
y_test = utils.to_categorical(y_test, num_classes)
# 加载模型
model = MODEL()
model.compile(loss=margin_loss, optimizer='adam', metrics=['accuracy'])
model.summary()
tfck = TensorBoard(log_dir='capsule')
# 训练
data_augmentation = True
if not data_augmentation:
print('Not using data augmentation.')
model.fit(
x_train,
y_train,
batch_size=batch_size,
epochs=epochs,
validation_data=(x_test, y_test),
callbacks=[tfck],
shuffle=True)
else:
print('Using real-time data augmentation.')
# This will do preprocessing and realtime data augmentation:
datagen = ImageDataGenerator(
featurewise_center=False, # set input mean to 0 over the dataset
samplewise_center=False, # set each sample mean to 0
featurewise_std_normalization=False, # divide inputs by dataset std
samplewise_std_normalization=False, # divide each input by its std
zca_whitening=False, # apply ZCA whitening
rotation_range=0, # randomly rotate images in 0 to 180 degrees
width_shift_range=0.1, # randomly shift images horizontally
height_shift_range=0.1, # randomly shift images vertically
horizontal_flip=True, # randomly flip images
vertical_flip=False) # randomly flip images
# Compute quantities required for feature-wise normalization
# (std, mean, and principal components if ZCA whitening is applied).
datagen.fit(x_train)
# Fit the model on the batches generated by datagen.flow().
model.fit_generator(
datagen.flow(x_train, y_train, batch_size=batch_size),
epochs=epochs,
validation_data=(x_test, y_test),
callbacks=[tfck],
workers=4)
plot_model(model, to_file='model.png', show_shapes=True)
回头来看,下图是论文中一个简单的CapsNet网络,只用到了一层胶囊,很好地展示了CapsNet是如何工作的:
输入是一张手写字的图片:
首先对这张图片做了常规的卷积操作,得到ReLU Conv1。
再对ReLU Conv1做卷积操作,并将其调整成适用于CapsNet的向量神经层 PrimaryCaps。
PrimaryCaps 到 DigitCaps 层的传播是 CapsNet 和以往 CNN 操作最大的区别,也就是 动态路由算法
最后 DigitCaps 中一共 10 个向量,每个向量中元素的个数为 16 。
对这10个向量 求模,求得 模值最大 的那个 向量代表就是图片概率最大的那个分类。
在胶囊网络中:用向量模的大小衡量某个实体出现的概率,模值越大,概率越大。
论文《Dynamic Routing Between Capsules》
代码《cifar10_cnn_capsule》
博客《CapsNet ——胶囊网络原理》
最后有些实现细节不明白也没关系,人生苦短,会用就行。