写点什么

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:009457

评论

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

科普篇:新冠疫苗解读

石云升

28天写作 2月春节不断更 新冠疫苗

分盘存储:实现数据库备集群备份文件分散存储

华为云开发者联盟

数据库 数据 容灾 集群 分盘存储

勿让 Docker Volume 引发 Terminating Pod

黄久远

Docker 云计算 Kubernetes 容器 云原生

【Animate.css】CSS动画库

德育处主任

CSS css3 html/css 28天写作

博文视点算法书单|让算法学习不再难

博文视点Broadview

谁再把IDEA的Project比作Eclipse的Workspace,我就跟谁急

YourBatman

eclipse IntelliJ IDEA Project Workspace

DIY一款能随周围环境变化的智能灯泡,求婚必备!

IoT云工坊

人工智能 物联网 人脸识别 sdk IoT App

软件架构模式之事件驱动架构

架构精进之路

软件架构 七日更 28天写作 2月春节不断更

第二章作业二

LouisN

💻 一文读懂两台计算机之间是如何通信的

飞天小牛肉

面试 计算机网络 2月春节不断更

两个高频设计类面试题:如何设计HashMap和线程池

yes

面试 hashmap 线程池

这一年,像踏码进货一样!

小傅哥

Java 小傅哥 技术成长 平台羊毛

GitHub星标数超4.2万的火爆之作!

博文视点Broadview

单例模式原来是这么简单?!

后台技术汇

28天写作 2月春节不断更

python subprocess-更优雅的创建子进程

jeffery

Python

五种C语言非数值计算的常用经典排序算法

华为云开发者联盟

算法 记录 C语言 排序 非数值计算

PowerApps画布应用编码规范和指南

Changwei™

低代码 企业应用 Power Platform PowerApps

任务悬赏系统软件开发

v16629866266

Java之五种遍历Map集合的方式

华为云开发者联盟

Java 对象 Iterator 内容合集

话题讨论 | 你现在还会推荐亲朋做程序员吗?

石云升

话题讨论 2月春节不断更

专访京东科技张亮:本土开源需形成吸纳开发者的靶心

京东科技开发者

开源

智能对联模型太难完成?华为云ModelArts助你实现!手把手教学

华为云开发者联盟

人工智能 modelarts mindspore Seq2Seq

Java 多线程上下文传递在复杂场景下的实践

vivo互联网技术

Java 架构 编程语言 多线程高并发

如何理解Linux系统SSH协议和原理

Changing Lin

Linux 2月春节不断更

MySQL字段类型最全解析

Simon

MySQL 数据库数据类型

运动健身市场越来越大,你的客户却越来越少?

IoT云工坊

人工智能 App 物联网 健身房 智能健身房

领域的边界,一个小讨论

李小腾

领域驱动设计 DDD

WireGuard 教程:使用 DNS-SD 进行 NAT-to-NAT 穿透

米开朗基杨

wireguard

作业二

KYoKO

工业互联网平台:将为“补链”“优链”“强链”提供有力保障

工业互联网

如何理解平行宇宙

陈东泽 EuryChen

科普 物理 平行宇宙 平行世界

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