赞
踩
从2017年起,RNN系列网络逐渐被一个叫Transformer的网络替代,发展到现在Transformer已经成为自然语言处理中主流的模型了,而且由Transformer引来了一股大语言模型热潮。从Bert到GPT3,再到如今的ChatGPT。Transformer实现了人类难以想象的功能,而且仍在不停发展。
本文将基于Transformer的Encoder部分,实现文本情感分析任务。
数据处理可以参考上一篇基于LSTM的文本情感分析(Keras版)的代码,本文使用另一种简单的方法实现。
首先需要下载对应的数据:ai.stanford.edu/~amaas/data…。点击下图位置:
数据解压后得到下面的目录结构:
数据解压后得到下面的目录结构:
- aclImdb
- test
- neg
- pos
- labeledBow.feat
- urls_neg.txt
- urls_pos.txt
- train
- neg
- pos
这是一个电影影评数据集,neg中包含的评论是评分较低的评论,而pos中包含的是评分较高的评论。我们需要的数据分别是test里面的neg和pos,以及train里面的neg和pos(neg表示negative,pos表示positive)。下面我们开始处理。
在开始写代码之前需要先导入相关模块:
import os
import keras
import tensorflow as tf
from keras import layers
我的环境是tensorflow2.7,部分版本的tensorflow导入方式不同,可以根据自己环境自行替换。
这里定义一个函数读取评论文件:
def load_data(data_dir=r'/home/zack/Files/datasets/aclImdb/train'): """ data_dir:train的目录或test的目录 输出: X:评论的字符串列表 y:标签列表(0,1) """ classes = ['pos', 'neg'] X, y = [], [] for idx, cls in enumerate(classes): # 拼接某个类别的目录 cls_path = os.path.join(data_dir, cls) for file in os.listdir(cls_path): # 拼接单个文件的目录 file_path = os.path.join(cls_path, file) with open(file_path, encoding='utf-8') as f: X.append(f.read().strip()) y.append(idx) return X, np.array(y)
上述函数会得到两个列表,便于我们后面处理。
前面部分的处理和之前的文章一样,而构建词表和tokenize的操作则用keras的api来实现。代码如下:
X, y = load_data()
vectorization = TextVectorization(max_tokens=vocab_size, output_sequence_length=seq_len)
# 构建词表
vectorization.adapt(X)
# tokenize
X = vectorization(X)
其中adapt方法接收的是句子列表,调用adapt方法后keras会帮我们构建词表,而后用vectorization(X)可以讲句子列表转换成词id列表。
这里使用Transformer的Encoder部分作为我们网络主干。我们需要实现两个部分,分别是PositionalEmbedding、TransformerEncoder,并将两个部分组成情感分类模型。
来简单介绍一下Transformer,这里粗略看一下Transformer的各个部件。Transformer结构如下图:
其中左半部分是Encoder,右半部分是Decoder,我们要实现的就是Encoder部分。我们从低向上来看一看Decoder部分。
Transformer的输入是一个id列表,形状为batch_size × sequence_len
,输入首先会经过一个简单的Embedding层(Input Embedding)得到一个形状为batch_size × sequence_len × embed_dim
,我们称为te
。te
里面包含了sequence_len
个词的嵌入,其中te
的第一个嵌入会与向量pe[0]
相加,te
的第二个嵌入会与向量t[1]
相加,依次类推。
因此pe
的形状应该为sequence_len × embed_dim
,pe
里面就包含了位置信息。在原始论文中pe
有固定公式获得,位置信息固定后pe
就固定了,而本文在实现时用一个叫Positional Embedding方式替代它,实现代码如下:
class PositionalEmbedding(layers.Layer):
def __init__(self, input_size, seq_len, embed_dim):
super(PositionalEmbedding, self).__init__()
self.seq_len = seq_len
# 词嵌入
self.tokens_embedding = layers.Embedding(input_size, embed_dim)
# 位置嵌入
self.positions_embedding = layers.Embedding(seq_len, embed_dim)
def call(self, inputs, *args, **kwargs):
# 生成位置id
positions = tf.range(0, self.seq_len, dtype='int32')
te = self.tokens_embedding(inputs)
pe = self.positions_embedding(positions)
return te + pe
这里使用了和词嵌入类似的思想,让网络自己学习位置信息。
Multi-Head Attention可以认为是对一个序列做了多次Self-Attention,然后把每次Self-Attention的结构拼接起来。在Keras和Pytorch中都有对应的实现,这里我们看看应该如何使用。
在创建MultiHeadAttention层时,需要指定头的数量以及key的维数,在正向传播时,如果传入两个相同的序列那么就是在做Self-Attention,代码如下:
from keras import layers
import tensorflow as tf
# 形状为batch_size × sequence_len × embed_dim
X = tf.random.uniform((3, 10, 5))
mta = layers.MultiHeadAttention(4, 10)
out = mta(X, X)
# 输出:(3, 10, 5)
print(out.shape)
从代码可以看出MultiHeadAttention的输入与输出形状一致。
在经过Attention后,我们把Attention的输入和Attention的输出都放入了一个叫Add & Norm的模块中。这里其实就是把两者相加,而后经过LayerNormalization,其结构如下图:
把词嵌入x1、x2输入Attention得到z1、z2,然后把x1、x2组成矩阵X,z1、z2组成矩阵Z,计算LayerNorm(X+Z),输入下一层,代码实现如下:
# 定义层
mta = layers.MultiHeadAttention(4, 10)
ln = layers.LayerNormalization()
# 正向传播
X = tf.random.uniform((3, 10, 5))
Z = mta(X, X)
out = ln(X+Z)
# 输出 (3, 10, 5)
print(out.shape)
Feed Forward就是简单的全连接层,不过这里是对单个向量进行全连接,即z1-zn每个向量都单独经过Linear层。另外Feed Forward层有两层全连接,先放大再缩小,代码如下:
import keras from keras import layers import tensorflow as tf mta = layers.MultiHeadAttention(4, 10) ln = layers.LayerNormalization() # Feed Forward层 ff = keras.Sequential([ layers.Dense(10, activation='relu'), layers.Dense(5) ]) X = tf.random.uniform((3, 10, 5)) Z = mta(X, X) Z = ln(X+Z) out = ff(Z) # 输出 (3, 10, 5) print(out.shape)
到此我们就把Encoder的各个部件说明了。下面来实现TransformerEncoder层。
现在我们把上面各个部分写成一个TransformerEncoder类,这里不包含PositionalEmbedding,代码如下:
class TransformerEncoder(layers.Layer): def __init__(self, embed_dim, hidden_dim, num_heads, **kwargs): super(TransformerEncoder, self).__init__(**kwargs) # Multi-Head Attention层 self.attention = layers.MultiHeadAttention( num_heads=num_heads, key_dim=embed_dim ) # Feed Forward层 self.feed_forward = keras.Sequential([ layers.Dense(hidden_dim, activation='relu'), layers.Dense(embed_dim) ]) # layernorm层 self.layernorm1 = layers.LayerNormalization() self.layernorm2 = layers.LayerNormalization() def call(self, inputs, *args, **kwargs): # 计算Self-Attention attention_output = self.attention(inputs, inputs) # 进行第一个Layer & Norm ff_input = self.layernorm1(inputs + attention_output) # Feed Forward ff_output = self.feed_forward(ff_input) # 进行第二个Layer & Norm outputs = self.layernorm2(ff_input + ff_output) return outputs
现在我们实现的这个TransformerEncoder它接收一个batch_size × sequence_len × embed_dim
的张量,输出一个形状一样的张量。如果要用于情感分析,我们可以在输出后面拼接全局平均池化和全连接层。
下面我们使用前面的PositionalEmbedding和TransformerEncoder实现我们的文本分类网络,代码如下:
# 超参数 vocab_size = 20000 seq_len = 180 batch_size = 64 hidden_size = 1024 embed_dim = 256 num_heads = 8 # 加载数据 X_train, y_train = load_data() X_test, y_test = load_data(r'/home/zack/Files/datasets/aclImdb/test') vectorization = layers.TextVectorization( max_tokens=vocab_size, output_sequence_length=seq_len, pad_to_max_tokens=True ) vectorization.adapt(X_train) X_train = vectorization(X_train) X_test = vectorization(X_test) # 构建模型 inputs = layers.Input(shape=(seq_len,)) x = PositionalEmbedding(vocab_size, seq_len, embed_dim)(inputs) x = TransformerEncoder(embed_dim, hidden_size, num_heads)(x) x = layers.GlobalAveragePooling1D()(x) x = layers.Dropout(0.5)(x) outputs = layers.Dense(1, activation='sigmoid')(x) model = keras.Model(inputs, outputs) # 训练 model.compile(loss='binary_crossentropy', metrics=['accuracy']) model.fit( X_train, y_train, epochs=20, batch_size=batch_size, validation_data=[X_test, y_test] )
最终训练后在测试集上准确率在85%左右。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。