Capsule是深度学习之父hinton在2017年提出来的一个较为轰动的网络结构。capsule这个结构主要的特点是:Vector in Vector out——向量进,向量出,而普通的神经元(Neuron)是Vector in Scalar out——向量进,标量出。capsule输出的向量比Neuron输出的标量表达出更丰富的特征。
下图台湾大学的李宏毅老师对capsule解读的slide。
- Neuron的输出标量只能表示到是否存在鸟嘴。
- capsule的输出的向量不仅能表示鸟嘴是否存在,而且还能表示出鸟嘴的方向(如图中向量第一维),鸟嘴的颜色等,鸟嘴的其他特征。
现在是不是开始感受到vector out 的威力了。
Capsule算法简介
了解到capsule的强大之后,接下来笔者对Capsule算法实现做一个简单的介绍,感受一下为什么Capsule这么强大。Capsule结构有两个比较重要的创新,如下图所示:
- Squash压缩激活函数
-
Dynamic Routing(动态路由)
Squash函数:
Squash压缩激活函数其实就是对向量进行一下压缩,但保留向量的模长信息,函数的表达式可以看作两部分:
- 这部分是用向量除以向量本身的模,其含义是将就是将模长压缩为1。
- 而这部分就可以看出S向量的模长||S||越大,则这部分值越大。其含义是如果一个向量的模长越长,其向量所代表的那个特征就越强,类比于神经网络中Neuron的输出值越大。
Dynamic Routing(动态路由):
这一部分作用的有如下2种理解方式:
- 对输入的特征向量进行聚类。
- 相似特征越多,这类特征越强的,从而弱化离群特征,类似于一个特征选择的过程,本质上也是个聚类过程。
如下图所示:Cupsule的输出向量 和 输入向量,,之间的内积相识度,决定了 最终包各个输入向量的信息程度。
从,,三个特征向量进来,通过动态路由循环,最后下图中 和 会比较大(和比较相似),会比较小。输出的 就更多的保留了和的信息。换个角度理解,如果有些特征向量很相似,他们的信息就会很大程度被保留下来。熟悉textrank算法的同学有没有感觉到,其过程很像通过文本相识度进行重要度排序的过程。
下图是Dynamic Routing(动态路由)的详细计算过程。有点类似于RNN的计算过程,或者直接理解成聚类的迭代过程。相识度系数
综上,特征向量输入到Capsule之后,比普通神经网络中的Neuron有如下三点优势:
- 做特征间的聚类,强化相似特征,
- 自动做特征选择,输出更有表达力的特征向量,
- 输出特征向量的每个维度代表是 特征的特征。
上述李老师的例子对3个特征向量只聚了一类。真实的情况一般是你输入一堆特征向量(matrix),返回一堆capsule处理后的特征向量(matrix),如下图所示:
Capsule实战部分——文本分类
数据载入
笔者这里使用的是评论情感分析数据集,之前的情感分析文章中介绍了这个数据集的数据格式,读者可以去这篇文章查看数据详情。
def read_data(data_path):
senlist = []
labellist = []
with open(data_path, "r",encoding='gb2312',errors='ignore') as f:
for data in f.readlines():
data = data.strip()
sen = data.split("\t")[2]
label = data.split("\t")[3]
if sen != "" and (label =="0" or label=="1" or label=="2" ) :
senlist.append(sen)
labellist.append(label)
else:
pass
assert(len(senlist) == len(labellist))
return senlist ,labellist
sentences,labels = read_data("data_train.csv")
char_set = set(word for sen in sentences for word in sen)
char_dic = {j:i+1 for i,j in enumerate(char_set)}
char_dic["unk"] = 0
数据预处理
这部分就是将句子进行向量化,同时做padding。
def process_data(data,labels,dic,maxlen):
sen2id = [[dic.get(char,0) for char in sen ] for sen in data]
labels = np_utils.to_categorical(labels)
return pad_sequences(sen2id,maxlen=maxlen),labels
train_data,train_labels = process_data(sentences,labels,char_dic,100)
模型定义
这里的代码笔者是借用的苏剑林大神基于pure Keras实现的Capsule。
import keras.backend as K
import numpy as np
#squash压缩函数和原文不一样,可自己定义
def squash(x, axis=-1):
s_squared_norm = K.sum(K.square(x), axis, keepdims=True)
scale = K.sqrt(s_squared_norm + K.epsilon())
return x / scale
class Capsule(Layer):
def __init__(self, num_capsule, dim_capsule, routings=3, kernel_size=(9, 1), share_weights=True,
activation='default', **kwargs):
super(Capsule, self).__init__(**kwargs)
self.num_capsule = num_capsule
self.dim_capsule = dim_capsule
self.routings = routings
self.kernel_size = kernel_size
self.share_weights = share_weights
if activation == 'default':
self.activation = squash
else:
self.activation = Activation(activation)
def build(self, input_shape):
super(Capsule, self).build(input_shape)
input_dim_capsule = input_shape[-1]
if self.share_weights:
self.W = self.add_weight(name='capsule_kernel',
shape=(1, input_dim_capsule,
self.num_capsule * self.dim_capsule),
# shape=self.kernel_size,
initializer='glorot_uniform',
trainable=True)
else:
input_num_capsule = input_shape[-2]
self.W = self.add_weight(name='capsule_kernel',
shape=(input_num_capsule,
input_dim_capsule,
self.num_capsule * self.dim_capsule),
initializer='glorot_uniform',
trainable=True)
def call(self, u_vecs):
if self.share_weights:
u_hat_vecs = K.conv1d(u_vecs, self.W)
else:
u_hat_vecs = K.local_conv1d(u_vecs, self.W, [1], [1])
batch_size = K.shape(u_vecs)[0]
input_num_capsule = K.shape(u_vecs)[1]
u_hat_vecs = K.reshape(u_hat_vecs, (batch_size, input_num_capsule,
self.num_capsule, self.dim_capsule))
u_hat_vecs = K.permute_dimensions(u_hat_vecs, (0, 2, 1, 3))
b = K.zeros_like(u_hat_vecs[:, :, :, 0]) # shape = [None, num_capsule, input_num_capsule]
#动态路由部分
for i in range(self.routings):
b = K.permute_dimensions(b, (0, 2, 1)) # shape = [None, input_num_capsule, num_capsule]
c = K.softmax(b)
c = K.permute_dimensions(c, (0, 2, 1))
b = K.permute_dimensions(b, (0, 2, 1))
outputs = self.activation(K.batch_dot(c, u_hat_vecs, [2, 2]))
if i < self.routings - 1:
b = K.batch_dot(outputs, u_hat_vecs, [2, 3])
return outputs
def compute_output_shape(self, input_shape):
return (None, self.num_capsule, self.dim_capsule)
这里定义了一个文本分类模型构建,采用双层LSTM加Capsule的结构,同时你需要定义Capsule出来的向量个数n_cap,以及向量维度cap_dim,和动态路由的轮数routings。
def build_model(vocab,emb_dim,maxlen,n_cap,cap_dim,n_class):
word_input = Input(shape=(None,), dtype="int32")
embed = Embedding(input_dim=len(vocab),
output_dim=100,
input_length=maxlen
)(word_input)
x = Bidirectional(LSTM(100,return_sequences=True))(embed)
x = Capsule(
num_capsule=n_cap,dim_capsule=cap_dim,
routings=3, share_weights=True)(x)
x = Flatten()(x)
x = Dropout(0.5)(x)
outputs = Dense(n_class, activation='softmax')(x)
model = Model(inputs=word_input, outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='nadam',metrics=['accuracy'])
model.summary()
return model
运行下方代码模型就构建成功了,同时从下图中keras的模型可视化输出可以看到,capsule的如果你的向量个数n_cap,以及向量维度cap_dim设置过大,参数还是挺多的。
model = build_model(char_dic,100,200,100,100,3)
模型训练
将数据喂给模型,指定好模型一些必要的参数,就可以训练起来了。
model.fit(train_data,train_labels,batch_size=16,epochs=3,validation_split=0.2)
结语
笔者这里没有去对比capsule结构和其他网络之间的性能,但是从一些capsule的实验中可以大致了解到capsule的 泛化能力较强,用向量代替标量表示特征,可以应付一下图片中pattern方向不同,大小不同,颜色不同等困难场景。所以这个网络还是很值得研究一番,笔者这里只是一个简介,大家可以看看我放在参考中的苏剑林大神的讲解和李宏毅教授的视频,甚至可以结合原文去仔细揣摩一番,可能收获更多。