前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用带注意力机制的模型分析评论者是否满意

用带注意力机制的模型分析评论者是否满意

作者头像
代码医生工作室
发布2019-08-27 15:11:01
7030
发布2019-08-27 15:11:01
举报
文章被收录于专栏:相约机器人相约机器人

本内容取之电子工业出版社出版、李金洪编著的《深度学习之TensorFlow工程化项目实战》一书的实例36。

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

实例描述

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

注意力机制是解决NLP任务的一种方法(见《深度学习之TensorFlow工程化项目实战》一书的8.1.10小节)。其内部的实现方式与卷积操作非常类似。在脱离RNN结构的情况下,单独的注意力机制模型也可以很好地完成NLP任务。具体做法如下。

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

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

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

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

代码语言:javascript
复制
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行注释掉。具体代码如下:

代码语言:javascript
复制
#  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行注释掉。具体代码如下:

代码语言:javascript
复制
#  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注意力机制模型分析评论者的情绪

代码语言:javascript
复制
    from __future__ import print_function
    import tensorflow as tf
    import numpy as np
    attention_keras = __import__("8-10  keras注意力机制模型")

    #定义参数
    num_words = 20000
    maxlen = 80
    batch_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 sequences

25000 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。

按照以上步骤,结合《深度学习之TensorFlow工程化项目实战》一书的8.1.11小节中的描述,实现带有位置向量的词嵌入层。

具体代码如下:

代码2 keras注意力机制模型

代码语言:javascript
复制
    import tensorflow as tf
    from tensorflow import keras
    from 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,用于实现带有位置向量的词嵌入层。该代码与《深度学习之TensorFlow工程化项目实战》一书的8.1.11小节中代码的不同之处是:它是用tf.keras接口实现的,同时也提供了位置向量的两种合入方式。

  • 加和方式:通过sum运算,直接把位置向量加到原有的词嵌入中。这种方式不会改变原有的维度。
  • 连接方式:通过concat函数将位置向量与词嵌入连接到一起。这种方式会在原有的词嵌入维度之上扩展出位置向量的维度。

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

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

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

下面按照《深度学习之TensorFlow工程化项目实战》一书的8.1.10小节中的描述,用tf.keras接口开发基于内部注意力的多头注意力机制Attention类。

在Attention类中用比《深度学习之TensorFlow工程化项目实战》一书的8.1.10小节更优化的方法来实现多头注意力机制的计算。该方法直接将多头注意力机制中最后的全连接网络中的权重提取出来,并将原有的输入Q、K、V按照指定的计算次数展开,使它们彼此以直接矩阵的方式进行计算。

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

具体代码如下:

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

代码语言:javascript
复制
    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) 

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

  • input_shape是Q、K、V中对应角色的输入维度。
  • output_dim是输出的总维度,即注意力的运算次数与每次输出的维度乘积(见代码36行)。

提示:

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

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

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

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

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

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

提示:

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

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

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

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

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

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

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

具体代码如下:

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

代码语言:javascript
复制
    #数据对齐
    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)
    #添加dropout
    O_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))

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

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

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

六、运行程序

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

代码语言:javascript
复制
Epoch 1/5
25000/25000 [==============================] - 42s 2ms/step - loss: 0.5357 - acc: 0.7160 - val_loss: 0.5096 - val_acc: 0.7533
Epoch 2/5
25000/25000 [==============================] - 36s 1ms/step - loss: 0.3852 - acc: 0.8260 - val_loss: 0.3956 - val_acc: 0.8195
Epoch 3/5
25000/25000 [==============================] - 36s 1ms/step - loss: 0.3087 - acc: 0.8710 - val_loss: 0.4135 - val_acc: 0.8184
Epoch 4/5
25000/25000 [==============================] - 36s 1ms/step - loss: 0.2404 - acc: 0.9011 - val_loss: 0.4501 - val_acc: 0.8094
Epoch 5/5
25000/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两个版本中运行,不需要任何改动。

客官您学得怎么样? 要不要来一本这样的书,就是这么实战。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 相约机器人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
图片处理
图片处理(Image Processing,IP)是由腾讯云数据万象提供的丰富的图片处理服务,广泛应用于腾讯内部各产品。支持对腾讯云对象存储 COS 或第三方源的图片进行处理,提供基础处理能力(图片裁剪、转格式、缩放、打水印等)、图片瘦身能力(Guetzli 压缩、AVIF 转码压缩)、盲水印版权保护能力,同时支持先进的图像 AI 功能(图像增强、图像标签、图像评分、图像修复、商品抠图等),满足多种业务场景下的图片处理需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档