代码示例 / 自然语言处理 / 使用FNet进行文本分类

使用FNet进行文本分类

作者: Abheesht Sharma
创建日期: 2022/06/01
最后修改: 2022/12/21
描述: 使用 keras_nlp.layers.FNetEncoder 层在IMDb数据集上进行文本分类。

在Colab中查看 GitHub源代码


介绍

在这个示例中,我们将演示FNet在文本分类任务上与普通Transformer模型达到可比结果的能力。我们将使用IMDb数据集,它是一个包含被标记为正面或负面的电影评论的集合(情感分析)。

为了构建分词器、模型等,我们将使用来自KerasNLP的组件。KerasNLP为想要构建NLP管道的人们提供便利! :)

模型

基于Transformer的语言模型(LMs)如BERT、RoBERTa、XLNet等已经证明了自注意力机制在计算输入文本的丰富嵌入方面的有效性。然而,自注意力机制是一项耗时的操作,时间复杂度为 O(n^2),其中 n 是输入中的标记数。因此,已经努力减少自注意力机制的时间复杂度,并在不牺牲结果质量的情况下提高性能。

在2020年,一篇名为 FNet: Mixing Tokens with Fourier Transforms 的论文用一个简单的傅里叶变换层替代了BERT中的自注意力层用于“标记混合”。这导致了可比的准确性和训练期间的加速。特别是,论文中的几个要点值得注意:

  • 作者声称FNet在GPU上比BERT快80%,在TPU上快70%。这一加速的原因有两个方面:a) 傅里叶变换层是无参数的,它没有任何参数,b) 作者使用快速傅里叶变换(FFT);这一点将时间复杂度从 O(n^2)(在自注意力情况下)降低到 O(n log n)
  • FNet能够在GLUE基准上达到BERT的92-97%的准确率。

设置

在开始实现之前,让我们导入所有必要的包。

!pip install -q --upgrade keras-nlp
!pip install -q --upgrade keras  # 升级到Keras 3.
import keras_nlp
import keras
import tensorflow as tf
import os

keras.utils.set_random_seed(42)

让我们还定义我们的超参数。

BATCH_SIZE = 64
EPOCHS = 3
MAX_SEQUENCE_LENGTH = 512
VOCAB_SIZE = 15000

EMBED_DIM = 128
INTERMEDIATE_DIM = 512

加载数据集

首先,让我们下载IMDB数据集并提取它。

!wget http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
!tar -xzf aclImdb_v1.tar.gz
--2023-11-22 17:59:33--  http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
正在解析 ai.stanford.edu (ai.stanford.edu)... 171.64.68.10
正在连接 ai.stanford.edu (ai.stanford.edu)|171.64.68.10|:80... 已连接。
HTTP请求已发送,等待响应... 200 OK
长度: 84125825 (80M) [application/x-gzip]
保存到: ‘aclImdb_v1.tar.gz’
aclImdb_v1.tar.gz   100%[===================>]  80.23M  93.3MB/s    in 0.9s    
2023-11-22 17:59:34 (93.3 MB/s) - ‘aclImdb_v1.tar.gz’ saved [84125825/84125825]

样本以文本文件的形式存在。让我们检查一下目录的结构。

print(os.listdir("./aclImdb"))
print(os.listdir("./aclImdb/train"))
print(os.listdir("./aclImdb/test"))
['README', 'imdb.vocab', 'imdbEr.txt', 'train', 'test']
['neg', 'unsup', 'pos', 'unsupBow.feat', 'urls_unsup.txt', 'urls_neg.txt', 'urls_pos.txt', 'labeledBow.feat']
['neg', 'pos', 'urls_neg.txt', 'urls_pos.txt', 'labeledBow.feat']

目录包含两个子目录:traintest。每个子目录又分别包含两个文件夹:posneg,用于正面和负面评论。在加载数据集之前,让我们删除 ./aclImdb/train/unsup 文件夹,因为它包含未标记的样本。

!rm -rf aclImdb/train/unsup

我们将使用 keras.utils.text_dataset_from_directory 工具从文本文件生成我们标记的 tf.data.Dataset 数据集。

train_ds = keras.utils.text_dataset_from_directory(
    "aclImdb/train",
    batch_size=BATCH_SIZE,
    validation_split=0.2,
    subset="training",  # 训练集
    seed=42,
)
val_ds = keras.utils.text_dataset_from_directory(
    "aclImdb/train",
    batch_size=BATCH_SIZE,
    validation_split=0.2,
    subset="validation",  # 验证集
    seed=42,
)
test_ds = keras.utils.text_dataset_from_directory("aclImdb/test", batch_size=BATCH_SIZE)  # 测试集
找到25000个文件,属于2个类别。
使用20000个文件进行训练。
找到25000个文件,属于2个类别。
使用5000个文件进行验证。
找到25000个文件,属于2个类别。

我们现在将文本转换为小写字母。

train_ds = train_ds.map(lambda x, y: (tf.strings.lower(x), y))
val_ds = val_ds.map(lambda x, y: (tf.strings.lower(x), y))
test_ds = test_ds.map(lambda x, y: (tf.strings.lower(x), y))

让我们打印几个样本。

for text_batch, label_batch in train_ds.take(1):
    for i in range(3):
        print(text_batch.numpy()[i])
        print(label_batch.numpy()[i])
b'一名非法移民抵制社会支援系统,导致许多人面临可怕的后果。尽管故事有些牵强,但拍摄和表演都很出色,缓慢的节奏突出了结局。迷失在大城市的感觉得到了有效传达。小人物在大社会中迷失是我们都能感同身受的,但我不能支持特意去看这部电影。'
0
b'要体会这部电影之美,请仔细留意配乐,不仅是音乐,所有声音如何帮助编织画面。开场场景多么漂亮地建立了道德模糊的主题,导致吉诺被驱逐!注意音乐是如何引入角色的,我们被带入乔凡娜的婚姻。不要指望在这里找到1943年意大利的政治生活。这并不是这部电影的主题。另一方面,如果你对影像和声音的音乐敏感,你将被引导进入一个超越新现实主义的世界。到电影结束时,会有几处安东尼奥尼式的风景,它与角色的内心生活关系更大,而不是与真实地方的关系。这是我最喜欢的维斯孔蒂电影之一。'
1
b'"好莱坞酒店"与许多电影如"艾拉·辛德斯"和"梅尔顿的电影"有关系,讲述某人赢得一场比赛,包括在好莱坞制作电影的合同,却发现通往明星之路要么布满陷阱,要么根本不存在。事实上,当我今晚在经典电影频道观看时,我在考虑晚期音乐经典"雨中歌"的作者是否可能从"好莱坞酒店"中获得了一些灵感,最显著的是电影制片厂里的一个情绪化女星和一个人一边在银幕上假唱,一边让另一个人获得了现场音乐的信用。<br /><br />"好莱坞酒店"是1930年代电影制作的一个迷人例子。在配角中,有露埃拉·帕森斯,她饰演自己(尽管我看到一些负面评论,但她在银幕上有着非常讨人喜欢的个性和自然的台词掌控能力)。她不是剧本中唯一的真实人物。化妆专家帕克·韦斯莫尔简短地以自己的身份出现,试图让一个角色看起来像另一个角色。<br /><br />这部电影也是年轻的罗纳德·里根职业生涯中的第一部之一,饰演一个在电影首映会上采访的电台记者。里根在短暂的场景中表现得相当不错——特别是当他意识到没人关注迪克·鲍威尔即将接管麦克风,而该麦克风应该用于更重要的人物时。<br /><br />迪克·鲍威尔在一场比赛中赢得了好莱坞合同,正准备离开自己在本尼·古德曼乐队担任萨克斯演奏者的工作。顺便提一下,这部电影的开头非常令人印象深刻,因为乐队驱车游行,正要向鲍威尔道别。他们最后唱起了"好莱坞万岁"。这个美妙的曲目有一个有趣之处,那就是歌词故意缺失。在约翰尼·麦瑟的歌词中,有对霍莉伍德的诸多提及,如化妆之王马克斯·法克特、林青霞甚至还有对泰山的暗示。但原歌词提到的是看起来像泰龙·鲍华。显然,杰克·华纳和他的兄弟们并不打算宣传20世纪福克斯的男主角,而是用唐纳德·鸭的名字代替。无论如何,这个数字展示了古德曼乐队的歌手和乐器演奏者们的最佳表现。电影的后续五分钟部分也是如此,乐队在排练。<br /><br />鲍威尔离开了乐队和他的女友(弗朗西斯·朗福德),去到好莱坞,结果发现自己成了一名合同演员(最有可能参加涉及萨克斯演奏者的音乐剧)。他被影楼公关阿伦·乔斯林接待(老板是格兰特·米切尔)。乔斯林不是个坏人,但他很忙,通常会不太关心别人,除非有必要和他们交谈。他把鲍威尔安排在好莱坞酒店的一个房间,而这也是明星(洛拉·莱恩)和她的父亲(休·赫伯特)、她的姐妹(梅布尔·托德)以及她那个理智却愤世嫉俗的助理(格伦达·法雷尔)住的地方。莱恩就像"雨中歌"中的简·哈根,只是她的说话声好听。她对"丹·洛克伍德"的诠释是"亚历山大·迪普雷"(艾伦·莫伯雷,多次轻松夺取镜头)。唯一的不同是,莫伯雷并不像吉恩·凯利那样是个好人,而莱恩(当不被自我束缚时)对此十分清楚。由于没有得到她想要的非同寻常的角色,她大发雷霆,拒绝出席她最新电影的首映。乔斯林为她找到了一个替身(洛拉真实生活中的妹妹罗丝玛丽·莱恩),罗丝玛丽化妆成明星,参加首映和随后的聚会。但她与鲍威尔一同出席(乔斯林想要一个不知道真实洛拉的人)。这导致鲍威尔在莫伯雷烦扰时将其击倒。但总体上,晚上非常成功,当他们在一起时,开始发现对彼此的吸引。<br /><br />复杂的事情在于洛拉回来后,给鲍威尔一记耳光,莫伯雷抱怨自己被鲍威尔袭击(“和他的暴徒团伙”)。鲍威尔的合同被解约。与转行做经纪人的摄影师泰德·希利合作(在这部电影中其实并不差——他甚至试图模仿乔尔森),两人试图找到工作,最后在由脾气暴躁的埃德加·肯尼迪经营的汉堡摊上工作(餐厅里破碎碗碟和歌唱顾客的数量,使得埃德加有很多时间尽情演绎他的慢燃烧)。最后,鲍威尔通过被雇用担任迪普雷的歌声,在"乱世佳人"的抄袭中得到了一个"机会"。这导致电影的最终部分,罗丝玛丽·莱恩、赫伯特和希利帮助鲍威尔展示这是他的声音,而不是莫伯雷的。<br /><br />这部电影即使在现在也相当可爱和吸引人。最糟糕的方面源于当时。有几个与非裔美国人有关的笑话已经不再被接受(当希利试图拍摄鲍威尔抵达好莱坞的画面时,不小心拍到了一个搬运工,并对乔斯林说要小心,鲍威尔的肤色拍得太黑——明白这一点了吗?)。还涉及一个名叫柯特·博伊斯的时装设计师设计洛拉·莱恩的情节,简直(可以说)太紧张了。赫伯特的"呼喊"似乎有点多(太频繁),但在1937年非常流行。希利在首映时差点卷入一场斗殴的事件(这是他最后几部电影之一),让人们想起这位喜剧演员在1937年12月悲惨且仍然神秘的结局。但这部电影的大部分内容相当不错,不会让2008年的观众感到失望。'
1

数据标记化

我们将使用 keras_nlp.tokenizers.WordPieceTokenizer 层来标记化文本。 keras_nlp.tokenizers.WordPieceTokenizer 接受一个 WordPiece 词汇表,并具有标记化文本和将标记序列反标记的功能。

在定义标记器之前,我们首先需要在我们拥有的数据集上训练它。WordPiece 标记化算法是一种子词标记化算法;在语料库上训练它可以为我们提供一个子词词汇表。子词标记器是单词标记器(单词标记器需要非常大的词汇表以良好覆盖输入单词)和字符标记器(字符并不能像单词那样真正编码意义)之间的折衷。幸运的是,KerasNLP 使在语料库上训练 WordPiece 变得非常简单,使用 keras_nlp.tokenizers.compute_word_piece_vocabulary 工具。

注意:FNet 的官方实现使用 SentencePiece Tokenizer。

def train_word_piece(ds, vocab_size, reserved_tokens):
    word_piece_ds = ds.unbatch().map(lambda x, y: x)
    vocab = keras_nlp.tokenizers.compute_word_piece_vocabulary(
        word_piece_ds.batch(1000).prefetch(2),
        vocabulary_size=vocab_size,
        reserved_tokens=reserved_tokens,
    )
    return vocab

每个词汇都有一些特殊的、保留的标记。我们有两个这样的标记:

  • "[PAD]" - 填充标记。填充标记附加到输入序列长度,当输入序列长度小于最大序列长度时。
  • "[UNK]" - 未知标记。
reserved_tokens = ["[PAD]", "[UNK]"]
train_sentences = [element[0] for element in train_ds]
vocab = train_word_piece(train_ds, VOCAB_SIZE, reserved_tokens)

让我们看看一些标记!

print("Tokens: ", vocab[100:110])
Tokens:  ['à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é']

现在,让我们定义标记器。我们将使用上面训练的词汇来配置标记器。我们将定义一个最大序列长度,以便所有序列都填充到相同的长度,如果序列的长度小于指定的序列长度。否则,序列将被截断。

tokenizer = keras_nlp.tokenizers.WordPieceTokenizer(
    vocabulary=vocab,
    lowercase=False,
    sequence_length=MAX_SEQUENCE_LENGTH,
)

让我们尝试标记化我们数据集中的一个示例!为了验证文本是否已正确标记化,我们还可以将标记列表反标记化回原始文本。

input_sentence_ex = train_ds.take(1).get_single_element()[0][0]
input_tokens_ex = tokenizer(input_sentence_ex)

print("Sentence: ", input_sentence_ex)
print("Tokens: ", input_tokens_ex)
print("Recovered text after detokenizing: ", tokenizer.detokenize(input_tokens_ex))
Sentence: tf.Tensor(b'这张图片似乎倾斜得太厉害了,几乎和那些说伊拉克一切都好的右翼狂热分子的鼓声一样糟糕。它描绘了一幅如此不可救药的画面,以至于我忍不住想要怀疑它的合法性和偏见。此外,它似乎从我们的军队的血腥屠杀转移到了美国对创伤后应激障碍的医疗保健不足的讨论。对我来说,这个主题显得混乱,它只关心把军队描绘得很糟糕,作为一个) 一个利用精神控制将普通和平爱好者变成杀婴者的组织以及b) 一个曾经使用和消耗其士兵的肉体然后将他们抛弃给VA的专制官僚体系的组织。这是一个合理的论点,但对我来说感觉偏离主题,几乎像是一部独立的电影。我觉得"战争录像"和"我的兄弟的血"更公正,让观众自己得出一些结论,而不是被电影制作者的观点强加。f-', shape=(), dtype=string) Tokens: [ 145 576 608 228 140 58 13343 13 143 8 58 360 148 209 148 137 9759 3681 139 137 344 3276 50 12092 164 169 269 424 141 57 2093 292 144 5115 15 143 7890 40 576 170 2970 2459 2412 10452 146 48 184 8 59 478 152 733 177 143 8 58 4060 8069 13355 138 8557 15 214 143 608 140 526 2121 171 247 177 137 4726 7336 139 395 4985 140 137 711 139 3959 597 144 137 1844 149 55 1175 288 15 140 203 137 1009 686 608 1701 13 143 197 3979 177 2514 137 1442 144 40 209 776 13 148 40 10 168 14198 13928 146 1260 470 1300 140 604 2118 2836 1873 9991 217 1006 2318 138 41 10 168 8469 146 422 400 480 138 1213 137 2541 139 143 8 58 1487 227 4319 10720 229 140 137 6310 8532 862 41 2215 6547 10768 139 137 61 15 40 15 145 141 40 7738 4120 13 152 569 260 3297 149 203 13 360 172 40 150 144 138 139 561 15 48 569 146 3 137 466 6192 3 138 3 665 139 193 707 3 204 207 185 1447 138 417 137 643 2731 182 8421 139 199 342 385 206 161 3920 253 137 566 151 137 153 1340 8845 15 45 14 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]Recovered text after detokenizing: tf.Tensor(b'这张图片似乎倾斜得太厉害了,几乎和那些说伊拉克一切都好的右翼狂热分子的鼓声一样糟糕。它描绘了一幅如此不可救药的画面,以至于我忍不住想要怀疑它的合法性和偏见。此外,它似乎从我们的军队的血腥屠杀转移到了美国对创伤后应激障碍的医疗保健不足的讨论。对我来说,这个主题显得混乱,它只关心把军队描绘得很糟糕,作为一个) 一个利用精神控制将普通和平爱好者变成杀婴者的组织以及b) 一个曾经使用和消耗其士兵的肉体然后将他们抛弃给VA的专制官僚体系的组织。这是一个合理的论点,但对我来说感觉偏离主题,几乎像是一部独立的电影。我觉得"战争录像"和"我的兄弟的血"更公正,让观众自己得出一些结论,而不是被电影制作者的观点强加。f - [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD]' , shape=(), dtype=string)

格式化数据集

接下来,我们将把数据集格式化为将被模型输入的形式。我们需要对文本进行分词。

def format_dataset(sentence, label):
    sentence = tokenizer(sentence)
    return ({"input_ids": sentence}, label)


def make_dataset(dataset):
    dataset = dataset.map(format_dataset, num_parallel_calls=tf.data.AUTOTUNE)
    return dataset.shuffle(512).prefetch(16).cache()


train_ds = make_dataset(train_ds)
val_ds = make_dataset(val_ds)
test_ds = make_dataset(test_ds)

构建模型

现在,让我们进入激动人心的部分 - 定义我们的模型! 我们首先需要一个嵌入层,也就是一个将输入序列中的每个标记映射到向量的层。这个嵌入层可以随机初始化。我们还需要一个位置嵌入层,它编码序列中的单词顺序。常规做法是将这两个嵌入相加,即求和。KerasNLP提供了一个 keras_nlp.layers.TokenAndPositionEmbedding 层,它为我们完成上述所有步骤。

我们的FNet分类模型由三个 keras_nlp.layers.FNetEncoder 层和一个 keras.layers.Dense 层构成。

注意:对于FNet,掩蔽填充标记对结果的影响很小。在官方实现中,填充标记不会被掩蔽。

input_ids = keras.Input(shape=(None,), dtype="int64", name="input_ids")

x = keras_nlp.layers.TokenAndPositionEmbedding(
    vocabulary_size=VOCAB_SIZE,
    sequence_length=MAX_SEQUENCE_LENGTH,
    embedding_dim=EMBED_DIM,
    mask_zero=True,
)(input_ids)

x = keras_nlp.layers.FNetEncoder(intermediate_dim=INTERMEDIATE_DIM)(inputs=x)
x = keras_nlp.layers.FNetEncoder(intermediate_dim=INTERMEDIATE_DIM)(inputs=x)
x = keras_nlp.layers.FNetEncoder(intermediate_dim=INTERMEDIATE_DIM)(inputs=x)


x = keras.layers.GlobalAveragePooling1D()(x)
x = keras.layers.Dropout(0.1)(x)
outputs = keras.layers.Dense(1, activation="sigmoid")(x)

fnet_classifier = keras.Model(input_ids, outputs, name="fnet_classifier")
/home/matt/miniconda3/envs/keras-io/lib/python3.10/site-packages/keras/src/layers/layer.py:861: UserWarning: Layer 'f_net_encoder' (of type FNetEncoder) 被传入了一个附带掩蔽的信息的输入。然而,这个层不支持掩蔽,因此会破坏掩蔽信息。下游层将看不到掩蔽。
  warnings.warn(

训练我们的模型

我们将使用准确率来监控在验证数据上的训练进度。让我们将模型训练3个周期。

fnet_classifier.summary()
fnet_classifier.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss="binary_crossentropy",
    metrics=["accuracy"],
)
fnet_classifier.fit(train_ds, epochs=EPOCHS, validation_data=val_ds)
模型: "fnet_classifier"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ 层 (类型)                      输出形状                  参数 # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ input_ids (输入层)          │ (None, None)              │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ token_and_position_embedding    │ (None, None, 128)         │  1,985,536 │
│ (TokenAndPositionEmbedding)     │                           │            │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ f_net_encoder (FNetEncoder)     │ (None, None, 128)         │    132,224 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ f_net_encoder_1 (FNetEncoder)   │ (, , 128)         │    132,224 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ f_net_encoder_2 (FNetEncoder)   │ (, , 128)         │    132,224 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ global_average_pooling1d        │ (, 128)               │          0 │
│ (GlobalAveragePooling1D)        │                           │            │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dropout (Dropout)               │ (, 128)               │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense (Dense)                   │ (, 1)                 │        129 │
└─────────────────────────────────┴───────────────────────────┴────────────┘
 总参数: 2,382,337 (9.09 MB)
 可训练参数: 2,382,337 (9.09 MB)
 非可训练参数: 0 (0.00 B)
Epoch 1/3

/home/matt/miniconda3/envs/keras-io/lib/python3.10/site-packages/keras/src/backend/jax/core.py:64: 用户警告: 显式请求的dtype int64在数组中不可用,将被截断为dtype int32。要启用更多数据类型,请设置jax_enable_x64配置选项或JAX_ENABLE_X64环境变量。请参见https://github.com/google/jax#current-gotchas获取更多信息。
  return jnp.array(x, dtype=dtype)

 313/313 ━━━━━━━━━━━━━━━━━━━━ 8s 18ms/step - accuracy: 0.5916 - loss: 0.6542 - val_accuracy: 0.8479 - val_loss: 0.3536
Epoch 2/3
 313/313 ━━━━━━━━━━━━━━━━━━━━ 4s 12ms/step - accuracy: 0.8776 - loss: 0.2916 - val_accuracy: 0.8532 - val_loss: 0.3387
Epoch 3/3
 313/313 ━━━━━━━━━━━━━━━━━━━━ 4s 12ms/step - accuracy: 0.9442 - loss: 0.1543 - val_accuracy: 0.8534 - val_loss: 0.4018

<keras.src.callbacks.history.History at 0x7feb7169c0d0>

我们获得了大约92%的训练准确率和大约85%的验证准确率。此外,训练模型需要约86秒(在使用16GB Tesla T4 GPU的Colab上)。

让我们计算测试准确率。

fnet_classifier.evaluate(test_ds, batch_size=BATCH_SIZE)
 391/391 ━━━━━━━━━━━━━━━━━━━━ 3s 5ms/step - accuracy: 0.8412 - loss: 0.4281

[0.4198716878890991, 0.8427909016609192]

与Transformer模型的比较

让我们将FNet分类器模型与Transformer分类器模型进行比较。我们保持所有参数/超参数相同。例如,我们使用三个TransformerEncoder层。

我们将头的数量设置为2。

NUM_HEADS = 2
input_ids = keras.Input(shape=(None,), dtype="int64", name="input_ids")


x = keras_nlp.layers.TokenAndPositionEmbedding(
    vocabulary_size=VOCAB_SIZE,
    sequence_length=MAX_SEQUENCE_LENGTH,
    embedding_dim=EMBED_DIM,
    mask_zero=True,
)(input_ids)

x = keras_nlp.layers.TransformerEncoder(
    intermediate_dim=INTERMEDIATE_DIM, num_heads=NUM_HEADS
)(inputs=x)
x = keras_nlp.layers.TransformerEncoder(
    intermediate_dim=INTERMEDIATE_DIM, num_heads=NUM_HEADS
)(inputs=x)
x = keras_nlp.layers.TransformerEncoder(
    intermediate_dim=INTERMEDIATE_DIM, num_heads=NUM_HEADS
)(inputs=x)


x = keras.layers.GlobalAveragePooling1D()(x)
x = keras.layers.Dropout(0.1)(x)
outputs = keras.layers.Dense(1, activation="sigmoid")(x)

transformer_classifier = keras.Model(input_ids, outputs, name="transformer_classifier")


transformer_classifier.summary()
transformer_classifier.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss="binary_crossentropy",
    metrics=["accuracy"],
)
transformer_classifier.fit(train_ds, epochs=EPOCHS, validation_data=val_ds)
模型: "transformer_classifier"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ 层 (类型)             输出形状         参数 #  连接到             ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ input_ids           │ (, )      │       0 │ -                    │
│ (输入层)        │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ token_and_position… │ (, , 128) │ 1,985,… │ input_ids[0][0]      │
│ (TokenAndPositionE… │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ transformer_encoder │ (, , 128) │ 198,272 │ token_and_position_… │
│ (TransformerEncode… │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ transformer_encode… │ (, , 128) │ 198,272 │ transformer_encoder… │
│ (TransformerEncode… │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ transformer_encode… │ (, , 128) │ 198,272 │ transformer_encoder… │
│ (TransformerEncode… │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ not_equal_1         │ (, )      │       0 │ input_ids[0][0]      │
│ (不等于)          │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ global_average_poo… │ (, 128)       │       0 │ transformer_encoder… │
│ (全局平均池化… │                   │         │ not_equal_1[0][0]    │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ dropout_4 (Dropout) │ (, 128)       │       0 │ global_average_pool… │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ dense_1 (全连接层)     │ (, 1)         │     129 │ dropout_4[0][0]      │
└─────────────────────┴───────────────────┴─────────┴──────────────────────┘
 总参数: 2,580,481 (9.84 MB)
 可训练参数: 2,580,481 (9.84 MB)
 非可训练参数: 0 (0.00 B)
第 1 轮/共 3 轮
 313/313 ━━━━━━━━━━━━━━━━━━━━ 14s 38ms/步 - 准确率: 0.5895 - 损失: 0.7401 - 验证准确率: 0.8912 - 验证损失: 0.2694
第 2 轮/共 3 轮
 313/313 ━━━━━━━━━━━━━━━━━━━━ 9s 29ms/步 - 准确率: 0.9051 - 损失: 0.2382 - 验证准确率: 0.8853 - 验证损失: 0.2984
第 3 轮/共 3 轮
 313/313 ━━━━━━━━━━━━━━━━━━━━ 9s 29ms/步 - 准确率: 0.9496 - 损失: 0.1366 - 验证准确率: 0.8730 - 验证损失: 0.3607

<keras.src.callbacks.history.History at 0x7feaf9c56ad0>

我们获得的训练准确率约为 94%,验证准确率约为 86.5%。训练模型大约需要 146 秒(在 Colab 上,使用 16 GB Tesla T4 GPU)。

让我们计算测试准确率。

transformer_classifier.evaluate(test_ds, batch_size=BATCH_SIZE)
 391/391 ━━━━━━━━━━━━━━━━━━━━ 4s 11ms/步 - 准确率: 0.8399 - 损失: 0.4579

[0.4496161639690399, 0.8423193097114563]

让我们制作一个表格并比较这两个模型。可以看到,FNet 显著加快了我们的运行时间(1.7 倍),整体准确率仅小幅下降(下降 0.75%)。

FNet 分类器 变换器分类器
训练时间 86 秒 146 秒
训练准确率 92.34% 93.85%
验证准确率 85.21% 86.42%
测试准确率 83.94% 84.69%
参数数量 2,321,921 2,520,065