【AICon】AI 基础设施、LLM运维、大模型训练与推理,一场会议,全方位涵盖! >>> 了解详情
写点什么

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 手机上识别男女并进行活体检测


公众号推荐:

跳进 AI 的奇妙世界,一起探索未来工作的新风貌!想要深入了解 AI 如何成为产业创新的新引擎?好奇哪些城市正成为 AI 人才的新磁场?《中国生成式 AI 开发者洞察 2024》由 InfoQ 研究中心精心打造,为你深度解锁生成式 AI 领域的最新开发者动态。无论你是资深研发者,还是对生成式 AI 充满好奇的新手,这份报告都是你不可错过的知识宝典。欢迎大家扫码关注「AI前线」公众号,回复「开发者洞察」领取。

2019-08-15 08:009003

评论

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

Java | IO流文件专属流

陌上

Java 编程 11月月更

2022一线大厂10w字面试总结,每日刷几道,明年金三银四稳了

程序员小毕

spring 程序员 JVM 架构师 java面试

一文搞懂MySQL表字段类型长度的含义

海风极客

MySQL 数据库 11月月更

集合工具类Collections指南,以及Comparable和Comparator排序详解

共饮一杯无

Java 集合 11月月更

2022全网最全最新Java面试题-独家内部教材

钟奕礼

Java 程序员 java面试 java编程

8年Java开发含泪刷题,架构岗现在好难进,有点崩溃

钟奕礼

Java 程序员 java面试 java编程

如何召回流失用户

穿过生命散发芬芳

11月月更 流失召回

Java | IO流数据流和标准输出流

陌上

Java 编程 11月月更

拿下大厂?这几道jvm面试题必须要懂

钟奕礼

Java 程序员 java面试 java编程

【C语言】int 关键字

謓泽

11月月更

Python进阶(五十三)Flask Web开发实现将表单渲染成HTML

No Silver Bullet

Python flask web开发 11月月更

仅hashmap一道面试题我就搞定了面试官成功入职面试官:我裂开了

钟奕礼

Java java面试 java编程 程序员、

网络核心笔记(一)

lxmoe

学习笔记 网络 11月月更

网络核心笔记(二)

lxmoe

学习笔记 网络 11月月更

Java | IO流缓冲流和转换流

陌上

Java 编程 11月月更

面试中如何才能拿到阿里 P7 的职级?

Java永远的神

Java 阿里巴巴 程序员 架构师 程序员晋升

Python冷知识:如何找出新版本增加或删除了哪些标准库?

Python猫

Python

【Logback+Spring-Aop】实现全面生态化的全链路日志追踪系统服务插件「Logback-MDC篇」

洛神灬殇

log4j logback 全链路追踪 11月日更 MDC

GitHub标星75k,阿里15W字的Spring高级文档(全彩版),真的太香了

程序知音

Java spring ssm java架构 后端技术

10道不得不会的 SpringBoot 面试题

JavaPub

Java 后端 springboot

3年Java研发,突击30天,从14K变成了30K

程序知音

Java java面试 大厂面试 java架构 后端技术

2022成功入职阿里:阿里的三套Java研发岗面试题总结(文末有答案)

钟奕礼

Java java面试 java编程 程序员、

三到五年互联网公司Java面试题大全

钟奕礼

Java 程序员 java面试 java编程

阿里、百度、美团、面试题大集合,愿你更轻松拿下大厂offer

钟奕礼

Java java面试 java编程 程序员、

2022年华为Java面经,还没搞懂JVM

钟奕礼

Java 程序员 Java 面试 java编程

Java 后端 100多道面试题,多看点题,没坏处!

钟奕礼

Java 程序员 java面试 java编程

从基础到实战,阿里巴巴高并发系统设计全彩版手册限时开源

Java全栈架构师

程序员 面试 程序员人生 高并发 架构师

亿级万物互联新时代的物联网消息中间件EMQX调研

宋小生

物联网 mqtt emqx

【LeetCode】无重复字符的最长子串题解

Albert

算法 LeetCode 11月月更

这个bug,你中招了吗!!!

石臻臻的杂货铺

kafka 后端 11月月更

Java中的String类常用方法

共饮一杯无

Java string 11月月更

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