说明
- 大部分代码来源于网上,但网上的代码一下子可能难以入门或因版本原因报错,此处整理后进行详细分析。
??参考的代码来源1:Attention mechanism Implementation for Keras.网上大部分代码都源于此,直接使用时注意Keras版本,若版本不对应,在merge处会报错,解决办法为:导入Multiply层并将attention_dense.py第17行的:
attention_mul = merge([inputs, attention_probs], output_shape=32, name=‘attention_mul’, mode=‘mul’),改为:attention_mul = Multiply()([inputs, attention_probs])即可。
??参考的代码来源2:[深度应用]·Keras极简实现Attention结构。这相当于来源1的简化版本,其将注意力层还做了封装,可直接使用。但此方法运用了两个注意力层,使我有些不太理解,这个问题在后面会进行讨论。
??本文主体将在来源1的基础上进行分析探讨。 - Attention机制大致过程就是分配权重,所有用到权重的地方都可以考虑使用它,另外它是一种思路,不局限于深度学习的实现方法,此处仅代码上分析,且为深度学习的实现版本。更多理论请看解读大牛文章深度学习中的注意力机制(2017版),还可以看解读这篇文章的大牛文章:[深度概念]·Attention机制实践解读。
- 此处仅介绍Dense+Attention,进阶篇LSTM+Attention请看【深度学习】 基于Keras的Attention机制代码实现及剖析——LSTM+Attention。
??如果你对本系列感兴趣,可接以下传送门:
目录
- 说明
- 实验目的
- 数据集构造
- 模型搭建
- 模型训练及验证
- 拓展
-
- 多分类问题时,Attention效果如何?
- 拓展总结
实验目的
- 在简单的分类模型(如最简的全连接网络)基础上实现Attention机制的运用。
- 检验Attention是否真的捕捉到了关键特征,即被Attention分配的关键特征的权重是否更高。
- 在已有的模型基础上适当做些变化,如调参或新加层,看看Attention的稳定性如何。
数据集构造
??因为是在分类问题上进行应用,所以需要构造特征(X)和标签(Y),此处数据随机产生,但为了进行Attention机制的有效性验证,我们将特征X的某一列置成和标签完全相同,如果Attention有效,那么模型学出来,自然这一列的权重就要最高。
??默认设置:attention_column=1,即将 第“1”列(从0开始数) 与标签置成相同。
??同时为了简化问题,将分类设置为二分类问题,即randint的参数high设置为2。(注意randint是左闭右开,所以当high=2时,y要么为0,要么为1)
def get_data(n, input_dim, attention_column=1):"""Data generation. x is purely random except that it's first value equals the target y.In practice, the network should learn that the target = x[attention_column].Therefore, most of its attention should be focused on the value addressed by attention_column.:param n: the number of samples to retrieve.:param input_dim: the number of dimensions of each element in the series.:param attention_column: the column linked to the target. Everything else is purely random.:return: x: model inputs, y: model targets"""x = np.random.standard_normal(size=(n, input_dim))y = np.random.randint(low=0, high=2, size=(n, 1))x[:, attention_column] = y[:, 0]return x, y
?? 我们输出X,Y的前三行,看看是不是和我们想要的一致。可以看到每一个x∈X的“第1列”都等于标签号,一致了。
模型搭建
??下面开始在单隐层全连接网络的基础上用keras搭建注意力层。
def build_model():K.clear_session() #清除之前的模型,省得压满内存inputs = Input(shape=(input_dim,)) #输入层# ATTENTION PART STARTS HERE 注意力层attention_probs = Dense(input_dim, activation='softmax', name='attention_vec')(inputs)attention_mul = Multiply()([inputs, attention_probs])# ATTENTION PART FINISHES HEREattention_mul = Dense(64)(attention_mul) #原始的全连接output = Dense(1, activation='sigmoid')(attention_mul) #输出层model = Model(input=[inputs], output=output)return model
??可以看到注意力层就两行代码,分别是一个Dense(全连接)层和一个Multiply操作,注意Multiply是对应元素相乘。
??如果画出加Attention前后的结构图,可如下图所示:
模型训练及验证
??设置随机种子可以调试对比每次结果的异同,输入必要参数,调用模型就可以开始训练了,由于是二分类问题,所以损失函数用二分类交叉熵。训练集测试集8:2进行验证。
if __name__ == '__main__':np.random.seed(1337) # for reproducibilityinput_dim = 32 #特征数N = 10000 #数据集总记录数inputs_1, outputs = get_data(N, input_dim) #构造数据集m = build_model() #构造模型m.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])m.summary()m.fit([inputs_1], outputs, epochs=20, batch_size=64, validation_split=0.2)
??后台会输出我们的模型架构,看看是不是和设计的一样:
??之前设置的是20个Epoch,看看训练结果:
??有一列特征与标签完全相同,这么简单的规律神经网络显然轻而易举地学出来了,训练集和测试集正确率都到了100%,那么注意力机制发挥了什么作用呢?我们来进行可视化看看。
??可视化的思路为:将注意力中学到的α绘制成柱状图,根据我们之前的设置,特征“第1列”的权重要更高乃至最高。这涉及到神经网络中间层的输出,具体在下面这个函数:
def get_activations(model, inputs, print_shape_only=False, layer_name=None):# Documentation is available online on Github at the address below.# From: https://github.com/philipperemy/keras-visualize-activationsprint('----- activations -----')activations = []inp = model.inputif layer_name is None:outputs = [layer.output for layer in model.layers]else:outputs = [layer.output for layer in model.layers if layer.name == layer_name] # all layer outputsfuncs = [K.function([inp] + [K.learning_phase()], [out]) for out in outputs] # evaluation functionslayer_outputs = [func([inputs, 1.])[0] for func in funcs]for layer_activations in layer_outputs:activations.append(layer_activations)if print_shape_only:print(layer_activations.shape)else:print(layer_activations)return activations
??这个函数有些复杂,但只需要知道它的功能就行,下面我们在main函数里续写如下代码进行调用:
testing_inputs_1, testing_outputs = get_data(1, input_dim)# Attention vector corresponds to the second matrix.# The first one is the Inputs output.attention_vector = get_activations(m, testing_inputs_1,print_shape_only=True,layer_name='attention_vec')[0].flatten()print('attention =', attention_vector)# plot part.pd.DataFrame(attention_vector, columns=['attention (%)']).plot(kind='bar',title='Attention Mechanism as ''a function of input'' dimensions.')plt.show()
??看看结果:
??可以看到,果然是“第1列”权重最高,与理论完全吻合。
??完整代码如下(一个文件):
from keras.models import *
from keras.layers import Input, Dense, Multiply
import keras.backend as K
import numpy as np
import matplotlib.pyplot as plt
import pandas as pddef get_activations(model, inputs, print_shape_only=False, layer_name=None):# Documentation is available online on Github at the address below.# From: https://github.com/philipperemy/keras-visualize-activationsprint('----- activations -----')activations = []inp = model.inputif layer_name is None:outputs = [layer.output for layer in model.layers]else:outputs = [layer.output for layer in model.layers if layer.name == layer_name] # all layer outputsfuncs = [K.function([inp] + [K.learning_phase()], [out]) for out in outputs] # evaluation functionslayer_outputs = [func([inputs, 1.])[0] for func in funcs]for layer_activations in layer_outputs:activations.append(layer_activations)if print_shape_only:print(layer_activations.shape)else:print(layer_activations)return activationsdef get_data(n, input_dim, attention_column=1):"""Data generation. x is purely random except that it's first value equals the target y.In practice, the network should learn that the target = x[attention_column].Therefore, most of its attention should be focused on the value addressed by attention_column.:param n: the number of samples to retrieve.:param input_dim: the number of dimensions of each element in the series.:param attention_column: the column linked to the target. Everything else is purely random.:return: x: model inputs, y: model targets"""x = np.random.standard_normal(size=(n, input_dim))y = np.random.randint(low=0, high=2, size=(n, 1))x[:, attention_column] = y[:, 0]return x, ydef build_model():K.clear_session() #清除之前的模型,省得压满内存inputs = Input(shape=(input_dim,)) #输入层# ATTENTION PART STARTS HERE 注意力层attention_probs = Dense(input_dim, activation='softmax', name='attention_vec')(inputs)attention_mul = Multiply()([inputs, attention_probs])# ATTENTION PART FINISHES HEREattention_mul = Dense(64)(attention_mul) #原始的全连接output = Dense(1, activation='sigmoid')(attention_mul) #输出层model = Model(inputs=[inputs], outputs=output)return modelif __name__ == '__main__':np.random.seed(1337) # for reproducibilityinput_dim = 32 #特征数N = 10000 #数据集总记录数inputs_1, outputs = get_data(N, input_dim) #构造数据集m = build_model() #构造模型m.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])m.summary()m.fit([inputs_1], outputs, epochs=20, batch_size=64, validation_split=0.2)testing_inputs_1, testing_outputs = get_data(1, input_dim)# Attention vector corresponds to the second matrix.# The first one is the Inputs output.attention_vector = get_activations(m, testing_inputs_1,print_shape_only=True,layer_name='attention_vec')[0].flatten()print('attention =', attention_vector)# plot part.pd.DataFrame(attention_vector, columns=['attention (%)']).plot(kind='bar',title='Attention Mechanism as ''a function of input'' dimensions.')plt.show()
拓展
??以上是对别人的代码的学习理解,接下来做一些小改动,看看Attention表现如何。
多分类问题时,Attention效果如何?
??以上代码改为多分类,需要注意一下几点:
- 构造数据集时,randint的high置为类别个数。
- 将随机完毕的y由十进制数改为二进制one-hot形式,以待模型输入。
- 模型最后一层的结点个数置为类别个数,同时激活函数改为softmax。
- 损失函数改为:loss=‘categorical_crossentropy’
??我们将类别数设置为5,先看看结果:
??可以看到还是很轻松就学出了规律,再看看可视化的权重:
??对比可以发现,尽管“第1列”的权重仍是最高的,但这个优势已经不明显了,注意力机制的健壮性如何?是否因为是多分类,效果就下降了呢?那么增大类别个数来看看,我们将类别个数置为20,直接看图:
??可以观察到,“第1列”仍是最高的权重,并且比5分类时还要高,说明注意力机制确实非常强大,可我还是不死心,那么继续调,30个类的时候如何呢?
??综合这两张图,我们惊奇地发现,准确率下降了,注意力紊乱了!注意力没有集中在本来人工设置最特别的特征“第1列”上,而集中出现在“第17列”上,这是为什么呢?继续尝试看看,设为50,100,图片分别为:
??可以发现,“注意力紊乱” 的情况仍存在,即使注意力没有向我们预设的“焦点”集中,分类准确率降低到了16.9% 这种“注意力紊乱”的表现是因为什么呢?
??我们可以发现,原模型设置的特征数是32个,当分类数接近或者超过特征数时,注意力才发生紊乱,而特征数对应的就是权重数,也就是注意力层的“算力”。因此我们可以有如下猜测:注意力紊乱是因为问题规模变大,导致原先的学习能力不足,学的不好。
??对于做DL的人来讲,很容易能想到,学得不够好怎么办?加层里的结点数量!加网络的深度!我们将分类数设置为50,特征数设置为128看看效果:
??果然,注意力又能“集中”了,但分类效果依旧很差。那么再试试加深,我们将注意力里的全连接层,多增加一层,分类数设置为50,特征数设置为32,再看看效果:
??加深注意力网络后不仅注意力回归了,准确率也上升了!(重大发现啊!那是不是可以发文章了呢?),然鹅已经有大佬在2016年就已经发表了,还给了一个好听的名字:多层注意力网络(Hierarchical Attention Networks),论文名字是Hierarchical Attention Networks for Document Classification。还有谷歌大佬也提出了一个方法来增加算力,叫多头注意力机制(multi-headed self-attention),论文名字为:Attention Is All You Need。
拓展总结
??在拓展中,我们可以发现,注意力机制的效果在算力充足的情况下,是能很好捕捉重点特征的,而针对注意力算力的不足,可以使用加结点和加层级的方法,但加结点会增加特征,这与现实中客观任务不符(即分类的数据集特征一般是固定的),且准确率没有提升,而加层级已有人进行应用并证实有效,因此可以作为我们搭建自己网络,提高自己指标的一个小技巧。
??此次扩展的完整代码(仅到多分类):
from keras.models import *
from keras.layers import Input, Dense, Multiply
import keras.backend as K
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from keras.utils import to_categoricaldef get_activations(model, inputs, print_shape_only=False, layer_name=None):# Documentation is available online on Github at the address below.# From: https://github.com/philipperemy/keras-visualize-activationsprint('----- activations -----')activations = []inp = model.inputif layer_name is None:outputs = [layer.output for layer in model.layers]else:outputs = [layer.output for layer in model.layers if layer.name == layer_name] # all layer outputsfuncs = [K.function([inp] + [K.learning_phase()], [out]) for out in outputs] # evaluation functionslayer_outputs = [func([inputs, 1.])[0] for func in funcs]for layer_activations in layer_outputs:activations.append(layer_activations)if print_shape_only:print(layer_activations.shape)else:print(layer_activations)return activationsdef get_data(n, input_dim, class_num, attention_column=1):"""Data generation. x is purely random except that it's first value equals the target y.In practice, the network should learn that the target = x[attention_column].Therefore, most of its attention should be focused on the value addressed by attention_column.:param n: the number of samples to retrieve.:param input_dim: the number of dimensions of each element in the series.:param attention_column: the column linked to the target. Everything else is purely random.:return: x: model inputs, y: model targets"""x = np.random.standard_normal(size=(n, input_dim))y = np.random.randint(low=0, high=class_num, size=(n, 1))x[:, attention_column] = y[:, 0]y = np.array([to_categorical(yy,class_num) for yy in y]).reshape(n,class_num)return x, ydef build_model(input_dim,class_num):K.clear_session() #清除之前的模型,省得压满内存inputs = Input(shape=(input_dim,)) #输入层# ATTENTION PART STARTS HERE 注意力层attention_probs = Dense(input_dim, activation='softmax', name='attention_vec')(inputs)attention_mul = Multiply()([inputs, attention_probs])# ATTENTION PART FINISHES HEREattention_mul = Dense(64)(attention_mul) #原始的全连接output = Dense(class_num, activation='softmax')(attention_mul) #输出层model = Model(inputs=[inputs], outputs=output)return modelif __name__ == '__main__':np.random.seed(1337) # for reproducibilityinput_dim = 32 #特征数N = 10000 #数据集总记录数class_num = 20 #类别数inputs_1, outputs = get_data(N, input_dim, class_num) #构造数据集m = build_model(input_dim,class_num) #构造模型m.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])m.summary()m.fit([inputs_1], outputs, epochs=20, batch_size=64, validation_split=0.2)testing_inputs_1, testing_outputs = get_data(1, input_dim, class_num)# Attention vector corresponds to the second matrix.# The first one is the Inputs output.attention_vector = get_activations(m, testing_inputs_1,print_shape_only=True,layer_name='attention_vec')[0].flatten()print('attention =', attention_vector)# plot part.pd.DataFrame(attention_vector, columns=['attention (%)']).plot(kind='bar',title='Attention Mechanism as ''a function of input'' dimensions.')plt.show()