写点什么

TensorFlow 工程实战(四):使用带注意力机制的模型分析评论者是否满意

  • 2019-08-15
  • 本文字数:8482 字

    阅读完需:约 28 分钟

TensorFlow工程实战(四):使用带注意力机制的模型分析评论者是否满意

本文介绍了如何利用 tf.keras 接口搭建一个只带有注意力机制的模型,实现文本分类。

本文摘选自电子工业出版社出版、李金洪编著的《深度学习之TensorFlow工程化项目实战》一书的实例 41:TensorFlow 用带注意力机制的模型分析评论者是否满意。

实例描述

有一个记录评论语句的数据集,分为正面和负面两种情绪。通过训练模型,让其学会正面与负面两种情绪对应的语义。


注意力机制是解决 NLP 任务的一种方法。其内部的实现方式与卷积操作非常类似。在脱离 RNN 结构的情况下,单独的注意力机制模型也可以很好地完成 NLP 任务。具体做法如下。

一、熟悉样本:了解 tf.keras 接口中的电影评论数据集

IMDB 数据集中含有 25000 条电影评论,从情绪的角度分为正面、负面两类标签。该数据集相当于图片处理领域的 MNIST 数据集,在 NLP 任务中经常被使用。


在 tf.keras 接口中,集成了 IMDB 数据集的下载及使用接口。该接口中的每条样本内容都是以向量形式存在的。


调用 tf.keras.datasets.imdb 模块下的 load_data 函数即可获得数据,该函数的定义如下:


def load_data(path='imdb.npz',    #默认的数据集文件              num_words=None,      #单词数量,即文本转向量后的最大索引              skip_top=0,        #跳过前面频度最高的几个词              maxlen=None,      #只取小于该长度的样本              seed=113,        #乱序样本的随机种子              start_char=1,      #每一组序列数据最开始的向量值。              oov_char=2,        #在字典中,遇到不存在的字符用该索引来替换              index_from=3,      #大于该数的向量将被认为是正常的单词              **kwargs):        #为了兼容性而设计的预留参数
复制代码


该函数会返回两个元组类型的对象:


  • (x_train, y_train):训练数据集。如果指定了 num_words 参数,则最大索引值是 num_words-1。如果指定了 maxlen 参数,则序列长度大于 maxlen 的样本将被过滤掉。

  • (x_test, y_test):测试数据集。


提示:

由于 load_data 函数返回的样本数据没有进行对齐操作,所以还需要将其进行对齐处理(按照指定长度去整理数据集,多了的去掉,少了的补 0)后才可以使用。

二、代码实现:将 tf.keras 接口中的 IMDB 数据集还原成句子

本节代码共分为两部分,具体如下。


  • 加载 IMDB 数据集及字典:用 load_data 函数下载数据集,并用 get_word_index 函数下载字典。

  • 读取数据并还原句子:将数据集加载到内存,并将向量转换成字符。

1. 加载 IMDB 数据集及字典

在调用 tf.keras.datasets.imdb 模块下的 load_data 函数和 get_word_index 函数时,系统会默认去网上下载预处理后的 IMDB 数据集及字典。如果由于网络原因无法成功下载 IMDB 数据集与字典,则可以加载本书的配套资源:IMDB 数据集文件“imdb.npz”与字典“imdb_word_index.json”。


将 IMDB 数据集文件“imdb.npz”与字典文件“imdb_word_index.json”放到本地代码的同级目录下,并对 tf.keras.datasets.imdb 模块的源代码文件中的函数 load_data 进行修改,关闭该函数的下载功能。具体如下所示。


(1)找到 tf.keras.datasets.imdb 模块的源代码文件。以作者本地路径为例,具体如下:


C:\local\Anaconda3\lib\site-packages\tensorflow\python\keras\datasets\imdb.py


(2)打开该文件,在 load_data 函数中,将代码的第 80~84 行注释掉。具体代码如下:


#  origin_folder = 'https://storage.googleapis.com/tensorflow/tf-keras-datasets/'#  path = get_file(#      path,#      origin=origin_folder + 'imdb.npz',#      file_hash='599dadb1135973df5b59232a0e9a887c')
复制代码


(3)在 get_word_index 函数中,将代码第 144~148 行注释掉。具体代码如下:


#  origin_folder = 'https://storage.googleapis.com/tensorflow/tf-keras-datasets/'#  path = get_file(#      path,#      origin=origin_folder + 'imdb_word_index.json',#      file_hash='bfafd718b763782e994055a2d397834f')
复制代码

2. 读取数据并还原其中的句子

从数据集中取出一条样本,并用字典将该样本中的向量转成句子,然后输出结果。具体代码如下:


代码 1 用 keras 注意力机制模型分析评论者的情绪


from __future__ import print_functionimport tensorflow as tfimport numpy as npattention_keras = __import__("8-10  keras注意力机制模型")
#定义参数num_words = 20000maxlen = 80batch_size = 32
#加载数据print('Loading data...')(x_train, y_train), (x_test, y_test) = tf.keras.datasets.imdb.load_data(path='./imdb.npz',num_words=num_words)print(len(x_train), 'train sequences')print(len(x_test), 'test sequences')print(x_train[:2])print(y_train[:10])word_index = tf.keras.datasets.imdb.get_word_index('./imdb_word_index.json')#生成字典:单词与下标对应reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])#生成反向字典:下标与单词对应
decoded_newswire = ' '.join([reverse_word_index.get(i - 3, '?') for i in x_train[0]]) print(decoded_newswire)
复制代码


代码第 21 行,将样本中的向量转化成单词。在转化过程中,将每个向量向前偏移了 3 个位置。这是由于在调用 load_data 函数时使用了参数 index_from 的默认值 3(见代码第 13 行),表示数据集中的向量值,从 3 以后才是字典中的内容。


在调用 load_data 函数时,如果所有的参数都使用默认值,则所生成的数据集会比字典中多 3 个字符“padding”(代表填充值)、“start of sequence”(代表起始位置)和“unknown”(代表未知单词)分别对应于数据集中的向量 0、1、2。


代码运行后,输出以下结果:


(1)数据集大小为 25000 条样本。具体内容如下:


25000 train sequences25000 test sequences
复制代码


(2)数据集中第 1 条样本的内容。具体内容如下:


[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, ……15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]
复制代码


结果中第一个向量为 1,代表句子的起始标志。可以看出,tf.keras 接口中的 IMDB 数据集为每个句子都添加了起始标志。这是因为调用函数 load_data 时用参数 start_char 的默认值 1(见代码第 13 行)。


(3)前 10 条样本的分类信息。具体内容如下:


[1 0 0 1 0 0 1 0 1 0]


(4)第 1 条样本数据的还原语句。具体内容如下:


? this film was just brilliant casting location scenery story direction everyone’s really suited the part they played and you could just imagine being there robert ? is an amazing actor and now the …… someone’s life after all that was shared with us all


结果中的第一个字符为“?”,表示该向量在字典中不存在。这是因为该向量值为 1,代表句子的起始信息。而字典中的内容是从向量 3 开始的。在将向量转换成单词的过程中,将字典中不存在的字符替换成了“?”(见代码第 21 行)。

三、代码实现:用 tf.keras 接口开发带有位置向量的词嵌入层

在 tf.keras 接口中实现自定义网络层,需要以下几个步骤。


(1)将自己的层定义成类,并继承 tf.keras.layers.Layer 类。


(2)在类中实现__init__方法,用来对该层进行初始化。


(3)在类中实现 build 方法,用于定义该层所使用的权重。


(4)在类中实现 call 方法,用来相应调用事件。对输入的数据做自定义处理,同时还可以支持 masking(根据实际的长度进行运算)。


(5)在类中实现 compute_output_shape 方法,指定该层最终输出的 shape。


按照以上步骤,实现带有位置向量的词嵌入层。


具体代码如下:


代码 2 keras 注意力机制模型


import tensorflow as tffrom tensorflow import kerasfrom tensorflow.keras import backend as K     #载入keras的后端实现 class Position_Embedding(keras.layers.Layer):    #定义位置向量类      def __init__(self, size=None, mode='sum', **kwargs):        self.size = size #定义位置向量的大小,必须为偶数,一半是cos,一半是sin        self.mode = mode        super(Position_Embedding, self).__init__(**kwargs)            def call(self, x):               #实现调用方法        if (self.size == None) or (self.mode == 'sum'):            self.size = int(x.shape[-1])        position_j = 1. / K.pow(  10000., 2 * K.arange(self.size / 2, dtype='float32') / self.size  )        position_j = K.expand_dims(position_j, 0)        #按照x的1维数值累计求和,生成序列。        position_i = tf.cumsum(K.ones_like(x[:,:,0]), 1)-1         position_i = K.expand_dims(position_i, 2)        position_ij = K.dot(position_i, position_j)        position_ij = K.concatenate([K.cos(position_ij), K.sin(position_ij)], 2)        if self.mode == 'sum':            return position_ij + x        elif self.mode == 'concat':            return K.concatenate([position_ij, x], 2)            def compute_output_shape(self, input_shape): #设置输出形状        if self.mode == 'sum':            return input_shape        elif self.mode == 'concat':        return (input_shape[0], input_shape[1], input_shape[2]+self.size)
复制代码


代码第 3 行是原生 Keras 框架的内部语法。由于 Keras 框架是一个前端的代码框架,它通过 backend 接口来调用后端框架的实现,以保证后端框架的无关性。


代码第 5 行定义了类 Position_Embedding,用于实现带有位置向量的词嵌入层。它是用 tf.keras 接口实现的,同时也提供了位置向量的两种合入方式。


  • 加和方式:通过 sum 运算,直接把位置向量加到原有的词嵌入中。这种方式不会改变原有的维度。

  • 连接方式:通过 concat 函数将位置向量与词嵌入连接到一起。这种方式会在原有的词嵌入维度之上扩展出位置向量的维度。


代码第 11 行是 Position_Embedding 类 call 方法的实现。当调用 Position_Embedding 类进行位置向量生成时,系统会调用该方法。


在 Position_Embedding 类的 call 方法中,先对位置向量的合入方式进行判断,如果是 sum 方式,则将生成的位置向量维度设置成输入的词嵌入向量维度。这样就保证了生成的结果与输入的结果维度统一,在最终的 sum 操作时不会出现错误。

四、代码实现:用 tf.keras 接口开发注意力层

下面用 tf.keras 接口开发基于内部注意力的多头注意力机制 Attention 类。


在 Attention 类中用更优化的方法来实现多头注意力机制的计算。该方法直接将多头注意力机制中最后的全连接网络中的权重提取出来,并将原有的输入 Q、K、V 按照指定的计算次数展开,使它们彼此以直接矩阵的方式进行计算。


这种方法采用了空间换时间的思想,省去了循环处理,提升了运算效率。


具体代码如下:


代码 2 keras 注意力机制模型(续)


class Attention(keras.layers.Layer):      #定义注意力机制的模型类    def __init__(self, nb_head, size_per_head, **kwargs):        self.nb_head = nb_head          #设置注意力的计算次数nb_head        #设置每次线性变化为size_per_head维度        self.size_per_head = size_per_head        self.output_dim = nb_head*size_per_head   #计算输出的总维度        super(Attention, self).__init__(**kwargs)
def build(self, input_shape): #实现build方法,定义权重 self.WQ = self.add_weight(name='WQ', shape=(int(input_shape[0][-1]), self.output_dim), initializer='glorot_uniform', trainable=True) self.WK = self.add_weight(name='WK', shape=(int(input_shape[1][-1]), self.output_dim), initializer='glorot_uniform', trainable=True) self.WV = self.add_weight(name='WV', shape=(int(input_shape[2][-1]), self.output_dim), initializer='glorot_uniform', trainable=True) super(Attention, self).build(input_shape) #定义Mask方法,按照seq_len的实际长度对inputs进行计算 def Mask(self, inputs, seq_len, mode='mul'): if seq_len == None: return inputs else: mask = K.one_hot(seq_len[:,0], K.shape(inputs)[1]) mask = 1 - K.cumsum(mask, 1) for _ in range(len(inputs.shape)-2): mask = K.expand_dims(mask, 2) if mode == 'mul': return inputs * mask if mode == 'add': return inputs - (1 - mask) * 1e12 def call(self, x): if len(x) == 3: #解析传入的Q_seq、K_seq、V_seq Q_seq,K_seq,V_seq = x Q_len,V_len = None,None #Q_len、V_len是mask的长度 elif len(x) == 5: Q_seq,K_seq,V_seq,Q_len,V_len = x #对Q、K、V做线性变换,一共做nb_head次,每次都将维度转化成size_per_head Q_seq = K.dot(Q_seq, self.WQ) Q_seq = K.reshape(Q_seq, (-1, K.shape(Q_seq)[1], self.nb_head, self.size_per_head)) Q_seq = K.permute_dimensions(Q_seq, (0,2,1,3)) #排列各维度的顺序。 K_seq = K.dot(K_seq, self.WK) K_seq = K.reshape(K_seq, (-1, K.shape(K_seq)[1], self.nb_head, self.size_per_head)) K_seq = K.permute_dimensions(K_seq, (0,2,1,3)) V_seq = K.dot(V_seq, self.WV) V_seq = K.reshape(V_seq, (-1, K.shape(V_seq)[1], self.nb_head, self.size_per_head)) V_seq = K.permute_dimensions(V_seq, (0,2,1,3)) #计算内积,然后计算mask,再计算softmax A = K.batch_dot(Q_seq, K_seq, axes=[3,3]) / self.size_per_head**0.5 A = K.permute_dimensions(A, (0,3,2,1)) A = self.Mask(A, V_len, 'add') A = K.permute_dimensions(A, (0,3,2,1)) A = K.softmax(A) #将A再与V进行内积计算 O_seq = K.batch_dot(A, V_seq, axes=[3,2]) O_seq = K.permute_dimensions(O_seq, (0,2,1,3)) O_seq = K.reshape(O_seq, (-1, K.shape(O_seq)[1], self.output_dim)) O_seq = self.Mask(O_seq, Q_len, 'mul') return O_seq def compute_output_shape(self, input_shape): return (input_shape[0][0], input_shape[0][1], self.output_dim)
复制代码


在代码第 9 行(书中第 39 行)的 build 方法中,为注意力机制中的三个角色 Q、K、V 分别定义了对应的权重。该权重的形状为[input_shape,output_dim]。其中:


  • input_shape 是 Q、K、V 中对应角色的输入维度。

  • output_dim 是输出的总维度,即注意力的运算次数与每次输出的维度乘积(见代码 36 行)。


提示:

多头注意力机制在多次计算时权重是不共享的,这相当于做了多少次注意力计算,就定义多少个全连接网络。所以在代码第 9~21 行(书中第 39~51 行),将权重的输出维度定义成注意力的运算次数与每次输出的维度乘积。


代码第 47 行(书中第 77 行)调用了 K.permute_dimensions 函数,该函数实现对输入维度的顺序调整,相当于 transpose 函数的作用。


代码第 37 行(书中第 67 行)是 Attention 类的 call 函数,其中实现了注意力机制的具体计算方式,步骤如下:


(1)对注意力机制中的三个角色的输入 Q、K、V 做线性变化(见代码第 45~53 行,书中第 75~83 行)。


(2)调用 batch_dot 函数,对第(1)步线性变化后的 Q 和 K 做基于矩阵的相乘计算(见代码第 55~59 行,书中第 85~89 行)。


(3)调用 batch_dot 函数,对第(2)步的结果与第(1)步线性变化后的 V 做基于矩阵的相乘计算(见代码第 55~59 行,书中第 85~89 行)。


提示:

这里的全连接网络是不带偏置权重 b 的。没有偏置权重的全连接网络在对数据处理时,本质上与矩阵相乘运算是一样的。

因为在整个计算过程中,需要将注意力中的三个角色 Q、K、V 进行矩阵相乘,并且在最后还要与全连接中的矩阵相乘,所以可以将这个过程理解为是 Q、K、V 与各自的全连接权重进行矩阵相乘。因为乘数与被乘数的顺序是与结果无关的,所以在代码第 37 行(书中第 67 行)的 call 方法中,全连接权重最先参与了运算,并不会影响实际结果。

五、代码实现:用 tf.keras 接口训练模型

用定义好的词嵌入层与注意力层搭建模型,进行训练。具体步骤如下:


(1)用 Model 类定义一个模型,并设置好输入/输出的节点。


(2)用 Model 类中的 compile 方法设置反向优化的参数。


(3)用 Model 类的 fit 方法进行训练。


具体代码如下:


代码 1 用 keras 注意力机制模型分析评论者的情绪(续)


#数据对齐x_train =  tf.keras.preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)x_test =  tf.keras.preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)print('Pad sequences x_train shape:', x_train.shape)  #定义输入节点S_inputs = tf.keras.layers.Input(shape=(None,), dtype='int32')
#生成词向量embeddings = tf.keras.layers.Embedding(num_words, 128)(S_inputs)embeddings = attention_keras.Position_Embedding()(embeddings) #默认使用同等维度的位置向量
#用内部注意力机制模型处理O_seq = attention_keras.Attention(8,16)([embeddings,embeddings,embeddings])
#将结果进行全局池化O_seq = tf.keras.layers.GlobalAveragePooling1D()(O_seq)#添加dropoutO_seq = tf.keras.layers.Dropout(0.5)(O_seq)#输出最终节点outputs = tf.keras.layers.Dense(1, activation='sigmoid')(O_seq)print(outputs)#将网络结构组合到一起model = tf.keras.models.Model(inputs=S_inputs, outputs=outputs)
#添加反向传播节点model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])
#开始训练print('Train...')model.fit(x_train, y_train, batch_size=batch_size,epochs=5, validation_data=(x_test, y_test))
复制代码


代码第 14 行(书中第 36 行)构造了一个列表对象作为输入参数。该列表对象里含有 3 个同样的元素——embeddings,表示使用的是内部注意力机制。


代码第 17~22 行(书中第 39~44 行),将内部注意力机制的结果 O_seq 经过全局池化和一个全连接层处理得到了最终的输出节点 outputs。节点 outputs 是一个 1 维向量。


代码第 27 行(书中第 49 行),用 model.compile 方法,构建模型的反向传播部分,使用的损失函数是 binary_crossentropy,优化器是 adam。

六、运行程序

代码运行后,生成以下结果:


Epoch 1/525000/25000 [==============================] - 42s 2ms/step - loss: 0.5357 - acc: 0.7160 - val_loss: 0.5096 - val_acc: 0.7533Epoch 2/525000/25000 [==============================] - 36s 1ms/step - loss: 0.3852 - acc: 0.8260 - val_loss: 0.3956 - val_acc: 0.8195Epoch 3/525000/25000 [==============================] - 36s 1ms/step - loss: 0.3087 - acc: 0.8710 - val_loss: 0.4135 - val_acc: 0.8184Epoch 4/525000/25000 [==============================] - 36s 1ms/step - loss: 0.2404 - acc: 0.9011 - val_loss: 0.4501 - val_acc: 0.8094Epoch 5/525000/25000 [==============================] - 35s 1ms/step - loss: 0.1838 - acc: 0.9289 - val_loss: 0.5303 - val_acc: 0.8007
复制代码


可以看到,整个数据集迭代 5 次后,准确率达到了 80%以上。


提示:

本节实例代码可以直接在 TensorFlow 1.x 与 2.x 两个版本中运行,不需要任何改动。


本文摘选自电子工业出版社出版、李金洪编著的《深度学习之TensorFlow工程化项目实战》一书,更多实战内容点此查看。



本文经授权发布,转载请联系电子工业出版社。


系列文章:


TensorFlow 工程实战(一):用 TF-Hub 库微调模型评估人物年龄


TensorFlow 工程实战(二):用 tf.layers API 在动态图上识别手写数字


TensorFlow 工程实战(三):结合知识图谱实现电影推荐系统


TensorFlow 工程实战(四):使用带注意力机制的模型分析评论者是否满意(本文)


TensorFlow 工程实战(五):构建 DeblurGAN 模型,将模糊相片变清晰


TensorFlow 工程实战(六):在 iPhone 手机上识别男女并进行活体检测


2019-08-15 08:009654

评论

发布
暂无评论
发现更多内容

翻译:《实用的Python编程》06_01_Iteration_protocol

codists

Python

三步上线自己的在线监考系统

融云 RongCloud

恭喜自己2021金三银四收到的第五个Offer:字节跳动Java研发岗

比伯

Java 编程 架构 面试 程序人生

BOE(京东方)物联网解决方案让会议更“智慧”

爱极客侠

有状态容器应用,从入门到实践

焱融科技

Kubernetes 容器 云原生 焱融科技 分布式存储

数据营销“教父”宋星十年倾心之作,让数据真正赋能企业

博文视点Broadview

学以至用-从“0”到“1”设计千万级交易系统

ninetyhe

高可用 分布式系统 海量数据库的设计与实践 异步削峰

集成融云 IMLib 时,如何实现一套类似于 IMKit 的用户信息管理机制

融云 RongCloud

一分钟了解EFT公链新一代超级DeFi公链——EGG超级公链

币圈那点事

区块链 公链 挖矿

混合编程:如何用python11调用C++

华为云开发者联盟

c++ 编程 语言 python11 混合编程

基于 SparkMLlib 智能课堂教学评价系统 - 系统实现(四)

大数据技术指南

大数据 spark 智能时代 28天写作 3月日更

镁信健康“互联网+医+药+险”模式能否打造出中国版联合健康?

E科讯

万物互联网络在企业中的价值和展望 | 趋势解读

物联网

阿里P7亲自讲解!整理几个重要的Android知识,最全Android知识总结

欢喜学安卓

android 程序员 面试 移动开发

《精通比特币》学习笔记(第十一章)

棉花糖

区块链 学习 3月日更

【科创人】维格表创始人陈霈霖:喜茶数字化转型的结晶是vika维格表

科创人

阿里P7亲自教你!一线互联网大厂中高级Android面试真题收录!讲的明明白白!

欢喜学安卓

android 程序员 面试 移动开发

腾讯高级工程师保姆级“Java成长手册”,层层递进,全是精华

Java架构追梦

Java 腾讯 面试 架构师

Spring AOP 执行顺序 && Spring循环依赖(面试必问)

hepingfly

Java spring aop 循环依赖

【LeetCode】螺旋矩阵Java题解

Albert

算法 LeetCode 28天写作 3月日更

Elasticsearch Segments Merging 磁盘文件合并

escray

elastic 28天写作 死磕Elasticsearch 60天通过Elastic认证考试 3月日更

解析分布式应用框架Ray架构源码

华为云开发者联盟

gRPC API 框架 ray 分布式应用框架

TCP拥塞控制四种算法

赖猫

TCP 网络协议

学无定法——知识反转效应

Justin

心理学 28天写作 游戏设计

书单|互联网企业面试案头书之程序员软技能篇

博文视点Broadview

整理 自动备份MYSQL数据库shell脚本

edd

啥子叫递归哟!!!(阶乘)

依旧廖凯

28天写作 3月日更

Navicat操作MySQL简易教程

Simon

MySQL navicat

中台还没建就开始拆中台了?医疗中台何去何从?

菜根老谭

中台 医疗中台

一文搞懂PID控制算法

不脱发的程序猿

3月日更 PID 控制算法 智能控制 工业控制

Python 初学者必看:Python 异常处理集合

华为云开发者联盟

Python 异常 代码 程序 错误

TensorFlow工程实战(四):使用带注意力机制的模型分析评论者是否满意_AI&大模型_李金洪_InfoQ精选文章