写点什么

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

评论

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

卧薪尝胆30天!啃透京东大牛的高并发设计进阶手册,终获P7意向书

做梦都在改BUG

Java 系统设计 高并发

500代码行代码手写docker-设置网络命名空间

蓝胖子的编程梦

k8s 容器网络 ,docker 容器网络方案 容器网络平台

惊艳!京东T8纯手码的Redis核心原理手册,基础与源码齐下

做梦都在改BUG

Java 数据库 redis 缓存

听听飞桨框架硬核贡献者如何玩转开源!

飞桨PaddlePaddle

开源社区 百度飞桨 PaddlePaddle

Java中synchronized锁的深入理解

做梦都在改BUG

Java synchronized

如何设计一个自动化测试平台

老张

自动化测试 测试开发 测试平台

阿里技术大佬限产的Netty核心原理剖析手册,看完你不心动?

做梦都在改BUG

Netty

【实践篇】手把手教你落地DDD | 京东云技术团队

京东科技开发者

DDD Archetype 企业号 5 月 PK 榜 三层架构

CISA零信任成熟度模型(译文)

权说安全

阿里逆天级调优方案,内部这套Java性能调优实战宝典,堪称教科书

做梦都在改BUG

Java 性能优化 性能调优

渲大师云主机按量付费功能上线!

Finovy Cloud

渲大师 按量付费

敏捷项目管理中缺陷bug的跟踪和管理

顿顿顿

Scrum 敏捷开发 缺陷管理 敏捷项目管理 敏捷开发管理工具

Tomcat处理http请求之源码分析 | 京东云技术团队

京东科技开发者

tomcat container HTTP 企业号 5 月 PK 榜

ERP已死,秒杀系统称王!阿里巴巴内部「10亿级并发设计文档」

Java你猿哥

数据库 缓存 分布式 消息队列 秒杀系统

从源码全面解析 dubbo 服务注册的来龙去脉

做梦都在改BUG

Java 源码 dubbo

首届百度商业AI技术创新大赛启动 点燃AIGC革新“星火”

百度Geek说

人工智能 百度 AIGC 企业号 5 月 PK 榜

2023年互联网大厂Java面试八股文整理(1200+面试题附答案解析)

架构师之道

Java 程序员 面试

我翻遍整个牛客网,整理出了全网最全的Java面试八股文大合集,整整6000多页

采菊东篱下

Java 程序员 面试

最佳实践:基于vite3的monorepo前端工程搭建 | 京东云技术团队

京东科技开发者

前端 vite Monorepo lodash vue3 vite 企业号 5 月 PK 榜

腾讯高工内产,Github都没的SpringBoot源码手册

做梦都在改BUG

Java spring Spring Boot 框架

堪称一绝!阿里技术人都用的Nginx笔记手册,应用到架构齐全

做梦都在改BUG

nginx

5G和led显示屏有什么关系

Dylan

技术 5G LED显示屏

英特尔黑科技加持,腾讯应用宝登陆电脑:安卓应用完美移植PC 更有神器辅助

E科讯

2023年天津等级测评机构有哪些?具体位置在哪里?

行云管家

等保 等保测评 等级 天津

深度学习进阶篇-国内预训练模型[6]:ERNIE-Doc、THU-ERNIE、K-Encoder融合文本信息和KG知识;原理和模型结构详解。

汀丶人工智能

人工智能 自然语言处理 深度学习 预训练模型 Transformer

NFTScan | 05.22~05.28 NFT 市场热点汇总

NFT Research

NFT 热点

校园共享电动车发展现状及未来趋势

共享电单车厂家

共享电动车厂家 校园共享电单车 校内共享电动车

Flutter调优--深入探究MediaQuery引起界面Rebuild的原因及解决办法 | 京东云技术团队

京东科技开发者

flutter 企业号 5 月 PK 榜 MediaQuery rebuild

GitHub星标126K的京东「微服务进阶笔记」首次开源!好评如潮

Java你猿哥

Java 架构 微服务 微服务架构 架构师

CST如何查看哪些 GPU 在线?

思茂信息

cst cst使用教程 cst操作 cst电磁仿真 cst仿真软件

盘点一款好用的运维团队协同软件,用过真香!

行云管家

运维 IT运维 协同合作

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