作者: Aritra Roy Gosthipaty,Sayak Paul(共同贡献),转为Keras 3的 Muhammad Anas Raza
创建日期: 2021/12/10
最后修改: 2023/08/14
描述: 自适应生成更少数量的标记用于视觉变换器。
视觉变换器(Dosovitskiy et al.)和许多其他基于变换器的架构(Liu et al.,Yuan et al.等)在图像识别中显示出强大的结果。以下是视觉变换器架构在图像分类中涉及的组件的简要概述:
如果我们取224x224的图像并提取16x16的块,则每个图像将得到196个块(也称为标记)。随着分辨率的提高,块的数量增加,从而导致更高的内存占用。我们能否使用减少的块数量而不牺牲性能?Ryoo等人在TokenLearner: Adaptive Space-Time Tokenization for Videos中探讨了这个问题。他们引入了一个新模块称为TokenLearner,它可以以自适应的方式帮助减少视觉变换器(ViT)使用的块的数量。通过在标准ViT架构中引入TokenLearner,他们能够减少模型使用的计算量(以FLOPS衡量)。
在这个示例中,我们实现了TokenLearner模块并展示了它在小型ViT和CIFAR-10数据集上的性能。我们利用以下参考资料:
import keras
from keras import layers
from keras import ops
from tensorflow import data as tf_data
from datetime import datetime
import matplotlib.pyplot as plt
import numpy as np
import math
请随意更改超参数并检查您的结果。发展对架构的直觉的最好方法是进行实验。
# 数据
BATCH_SIZE = 256
AUTO = tf_data.AUTOTUNE
INPUT_SHAPE = (32, 32, 3)
NUM_CLASSES = 10
# 优化器
LEARNING_RATE = 1e-3
WEIGHT_DECAY = 1e-4
# 训练
EPOCHS = 1
# 增强
IMAGE_SIZE = 48 # 我们将调整输入图像到这种大小。
PATCH_SIZE = 6 # 从输入图像中提取的块的大小。
NUM_PATCHES = (IMAGE_SIZE // PATCH_SIZE) ** 2
# ViT架构
LAYER_NORM_EPS = 1e-6
PROJECTION_DIM = 128
NUM_HEADS = 4
NUM_LAYERS = 4
MLP_UNITS = [
PROJECTION_DIM * 2,
PROJECTION_DIM,
]
# TOKENLEARNER
NUM_TOKENS = 4
# 加载CIFAR-10数据集。
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()
(x_train, y_train), (x_val, y_val) = (
(x_train[:40000], y_train[:40000]),
(x_train[40000:], y_train[40000:]),
)
print(f"训练样本: {len(x_train)}")
print(f"验证样本: {len(x_val)}")
print(f"测试样本: {len(x_test)}")
# 转换为tf.data.Dataset对象。
train_ds = tf_data.Dataset.from_tensor_slices((x_train, y_train))
train_ds = train_ds.shuffle(BATCH_SIZE * 100).batch(BATCH_SIZE).prefetch(AUTO)
val_ds = tf_data.Dataset.from_tensor_slices((x_val, y_val))
val_ds = val_ds.batch(BATCH_SIZE).prefetch(AUTO)
test_ds = tf_data.Dataset.from_tensor_slices((x_test, y_test))
test_ds = test_ds.batch(BATCH_SIZE).prefetch(AUTO)
训练样本: 40000
验证样本: 10000
测试样本: 10000
增强管道包括:
data_augmentation = keras.Sequential(
[
layers.Rescaling(1 / 255.0),
layers.Resizing(INPUT_SHAPE[0] + 20, INPUT_SHAPE[0] + 20),
layers.RandomCrop(IMAGE_SIZE, IMAGE_SIZE),
layers.RandomFlip("horizontal"),
],
name="data_augmentation",
)
注意,图像数据增强层在推理时不应用数据变换。这意味着当这些层以 training=False
被调用时,它们的行为有所不同。有关更多详细信息,请参阅 文档。
一个 Transformer 架构由 多头自注意力 层和 全连接前馈 神经网络(MLP)作为主要组件。这两个组件都是 置换不变的:它们不意识到特征顺序。
为了克服这个问题,我们注入带有位置信息的标记。position_embedding
函数将这个位置信息添加到线性投影的标记中。
class PatchEncoder(layers.Layer):
def __init__(self, num_patches, projection_dim):
super().__init__()
self.num_patches = num_patches
self.position_embedding = layers.Embedding(
input_dim=num_patches, output_dim=projection_dim
)
def call(self, patch):
positions = ops.expand_dims(
ops.arange(start=0, stop=self.num_patches, step=1), axis=0
)
encoded = patch + self.position_embedding(positions)
return encoded
def get_config(self):
config = super().get_config()
config.update({"num_patches": self.num_patches})
return config
这作为我们 Transformer 的全连接前馈块。
def mlp(x, dropout_rate, hidden_units):
# 遍历隐藏单元并
# 添加 Dense => Dropout。
for units in hidden_units:
x = layers.Dense(units, activation=ops.gelu)(x)
x = layers.Dropout(dropout_rate)(x)
return x
下面的图展示了该模块的图示概述 (来源)。
TokenLearner 模块接受一个图像形状的张量作为输入。然后它通过多个单通道卷积层,提取关注输入不同部分的不同空间注意力图。这些注意力图随后与输入逐元素相乘,并通过池化进行汇总。这个池化输出可以被视为输入的摘要,且相较于原始的补丁数量(例如 196),数量大大减少(例如 8)。
使用多个卷积层有助于表达性。施加一种空间注意力的形式有助于保留输入中的相关信息。这两个组件对 TokenLearner 的工作至关重要,尤其是在我们显著减少补丁数量时。
def token_learner(inputs, number_of_tokens=NUM_TOKENS):
# 对输入进行层归一化。
x = layers.LayerNormalization(epsilon=LAYER_NORM_EPS)(inputs) # (B, H, W, C)
# 应用 Conv2D => 重塑 => 转置
# 进行重塑和转置以帮助后续的
# 乘法和全局平均池化步骤。
attention_maps = keras.Sequential(
[
# 3 层带有 gelu 激活的卷积,如论文中建议的。
layers.Conv2D(
filters=number_of_tokens,
kernel_size=(3, 3),
activation=ops.gelu,
padding="same",
use_bias=False,
),
layers.Conv2D(
filters=number_of_tokens,
kernel_size=(3, 3),
activation=ops.gelu,
padding="same",
use_bias=False,
),
layers.Conv2D(
filters=number_of_tokens,
kernel_size=(3, 3),
activation=ops.gelu,
padding="same",
use_bias=False,
),
# 这个卷积层将生成注意力图
layers.Conv2D(
filters=number_of_tokens,
kernel_size=(3, 3),
activation="sigmoid", # 注意 sigmoid 输出范围为 [0, 1]
padding="same",
use_bias=False,
),
# 重塑和转置
layers.Reshape((-1, number_of_tokens)), # (B, H*W, num_of_tokens)
layers.Permute((2, 1)),
]
)(
x
) # (B, num_of_tokens, H*W)
# 重塑输入以与卷积块的输出对齐。
num_filters = inputs.shape[-1]
inputs = layers.Reshape((1, -1, num_filters))(inputs) # inputs == (B, 1, H*W, C)
# 注意力图和输入的逐元素乘法
attended_inputs = (
ops.expand_dims(attention_maps, axis=-1) * inputs
) # (B, num_tokens, H*W, C)
# 对逐元素乘法结果进行全局平均池化。
outputs = ops.mean(attended_inputs, axis=2) # (B, num_tokens, C)
return outputs
def transformer(encoded_patches):
# 层归一化 1.
x1 = layers.LayerNormalization(epsilon=LAYER_NORM_EPS)(encoded_patches)
# 多头自注意力层 1.
attention_output = layers.MultiHeadAttention(
num_heads=NUM_HEADS, key_dim=PROJECTION_DIM, dropout=0.1
)(x1, x1)
# 跳过连接 1.
x2 = layers.Add()([attention_output, encoded_patches])
# 层归一化 2.
x3 = layers.LayerNormalization(epsilon=LAYER_NORM_EPS)(x2)
# MLP 层 1.
x4 = mlp(x3, hidden_units=MLP_UNITS, dropout_rate=0.1)
# 跳过连接 2.
encoded_patches = layers.Add()([x4, x2])
return encoded_patches
def create_vit_classifier(use_token_learner=True, token_learner_units=NUM_TOKENS):
inputs = layers.Input(shape=INPUT_SHAPE) # (B, H, W, C)
# 数据增强.
augmented = data_augmentation(inputs)
# 创建补丁并投影补丁.
projected_patches = layers.Conv2D(
filters=PROJECTION_DIM,
kernel_size=(PATCH_SIZE, PATCH_SIZE),
strides=(PATCH_SIZE, PATCH_SIZE),
padding="VALID",
)(augmented)
_, h, w, c = projected_patches.shape
projected_patches = layers.Reshape((h * w, c))(
projected_patches
) # (B, number_patches, projection_dim)
# 将位置嵌入添加到投影的补丁中.
encoded_patches = PatchEncoder(
num_patches=NUM_PATCHES, projection_dim=PROJECTION_DIM
)(
projected_patches
) # (B, number_patches, projection_dim)
encoded_patches = layers.Dropout(0.1)(encoded_patches)
# 遍历层数并堆叠 Transformer 块.
for i in range(NUM_LAYERS):
# 添加一个 Transformer 块.
encoded_patches = transformer(encoded_patches)
# 在架构中间添加 TokenLearner 层. 论文建议在 1/2 到 3/4 之间都很好.
if use_token_learner and i == NUM_LAYERS // 2:
_, hh, c = encoded_patches.shape
h = int(math.sqrt(hh))
encoded_patches = layers.Reshape((h, h, c))(
encoded_patches
) # (B, h, h, projection_dim)
encoded_patches = token_learner(
encoded_patches, token_learner_units
) # (B, num_tokens, c)
# 层归一化和全局平均池化.
representation = layers.LayerNormalization(epsilon=LAYER_NORM_EPS)(encoded_patches)
representation = layers.GlobalAvgPool1D()(representation)
# 分类输出.
outputs = layers.Dense(NUM_CLASSES, activation="softmax")(representation)
# 创建 Keras 模型.
model = keras.Model(inputs=inputs, outputs=outputs)
return model
如TokenLearner 论文所示,在网络中间包含 TokenLearner 模块几乎总是有利的。
def run_experiment(model):
# 初始化 AdamW 优化器.
optimizer = keras.optimizers.AdamW(
learning_rate=LEARNING_RATE, weight_decay=WEIGHT_DECAY
)
# 使用优化器、损失函数和指标编译模型.
model.compile(
optimizer=optimizer,
loss="sparse_categorical_crossentropy",
metrics=[
keras.metrics.SparseCategoricalAccuracy(name="accuracy"),
keras.metrics.SparseTopKCategoricalAccuracy(5, name="top-5-accuracy"),
],
)
# 定义回调
checkpoint_filepath = "/tmp/checkpoint.weights.h5"
checkpoint_callback = keras.callbacks.ModelCheckpoint(
checkpoint_filepath,
monitor="val_accuracy",
save_best_only=True,
save_weights_only=True,
)
# 训练模型.
_ = model.fit(
train_ds,
epochs=EPOCHS,
validation_data=val_ds,
callbacks=[checkpoint_callback],
)
model.load_weights(checkpoint_filepath)
_, accuracy, top_5_accuracy = model.evaluate(test_ds)
print(f"测试准确率: {round(accuracy * 100, 2)}%")
print(f"测试 top 5 准确率: {round(top_5_accuracy * 100, 2)}%")
vit_token_learner = create_vit_classifier()
run_experiment(vit_token_learner)
157/157 ━━━━━━━━━━━━━━━━━━━━ 303s 2s/step - accuracy: 0.1158 - loss: 2.4798 - top-5-accuracy: 0.5352 - val_accuracy: 0.2206 - val_loss: 2.0292 - val_top-5-accuracy: 0.7688
40/40 ━━━━━━━━━━━━━━━━━━━━ 5s 133ms/step - accuracy: 0.2298 - loss: 2.0179 - top-5-accuracy: 0.7723
测试准确率: 22.9%
测试 top 5 准确率: 77.22%
我们在实现的迷你 ViT 内部实验了有无 TokenLearner(使用本示例中呈现的相同超参数)。以下是我们的结果:
| TokenLearner | # tokens in
TokenLearner | Top-1 Acc
(Averaged across 5 runs) | GFLOPs | TensorBoard |
|:—:|:—:|:—:|:—:|:—:|
| N | - | 56.112% | 0.0184 | Link |
| Y | 8 | 56.55% | 0.0153 | Link |
| N | - | 56.37% | 0.0184 | Link |
| Y | 4 | 56.4980% | 0.0147 | Link |
| N | - (# Transformer 层:8) | 55.36% | 0.0359 | Link |
TokenLearner 能够持续地超越我们没有该模块的 mini ViT。 同样有趣的是,它也能够超越我们更深版本的 mini ViT(带有 8 层)。 作者在论文中也报告了类似的观察,并将其归因于 TokenLearner 的自适应性。
还应该注意的是,FLOPs 计数在添加 TokenLearner 模块时显著 减少。 随着 FLOPs 计数的减少,TokenLearner 模块能够提供更好的结果。 这与作者的发现非常一致。
此外,作者引入了一种新的 TokenLearner 版本,适用于较小的训练数据集。 引用作者的说法:
这个版本不再使用 4 个小通道的卷积层来实现空间注意力,而是使用 2 个具有更多通道的分组卷积层。 它还使用 softmax 而不是 sigmoid。 我们确认,当训练数据有限时,例如从头开始使用 ImageNet1K 进行训练时,这个版本的效果更好。
我们对该模块进行了实验,以下表格总结了结果:
# 组 | # Tokens | Top-1 Acc | GFLOPs | TensorBoard |
---|---|---|---|---|
4 | 4 | 54.638% | 0.0149 | Link |
8 | 8 | 54.898% | 0.0146 | Link |
4 | 8 | 55.196% | 0.0149 | Link |
请注意,我们使用了此示例中呈现的相同超参数。 我们的实现可以在 该笔记本中找到。 我们承认,这个新的 TokenLearner 模块的结果略有偏差,可能需要通过超参数调整来改善。
注意: 为了计算我们模型的 FLOPs,我们使用了 这个工具 来自这个库。
您可能注意到,添加 TokenLearner 模块会增加基础网络的参数数量。 但这并不意味着它的效率较低,正如Dehghani et al.所显示的那样。 Bello et al.也报告了类似的发现。 TokenLearner 模块有助于降低整个网络的 FLOPS,从而帮助减少内存占用。
我们感谢 JarvisLabs 和 Google Developers Experts 项目提供的 GPU 额度支持。同时,感谢 TokenLearner 的第一作者 Michael Ryoo 的富有成效的讨论。