作者: Matthew Watson, Jonathan Bischof
创建日期 2022/12/15
最后修改日期 2024/10/17
描述: KerasHub API 的介绍。
KerasHub 是一个旨在简单、灵活和快速的预训练模型库。该库提供了 Keras 3 中流行的模型架构的实现,并结合了 Kaggle 上可用的一系列预训练检查点。这些模型可以用于在 TensorFlow、Jax 和 Torch 后端上的训练和推理。
KerasHub 是核心 Keras API 的扩展;KerasHub 组件以 keras.Layer
和 keras.Model
的形式提供。如果您熟悉 Keras,恭喜!您已经了解了 KerasHub 的大部分内容。
本指南旨在对整个库进行易于理解的介绍。我们将从使用高级 API 来分类图像和生成文本开始,然后逐步展示更深层次的模型定制和训练。在整个指南中,我们使用官方 Keras 吉祥物 Professor Keras 作为材料复杂性的视觉参考。
与往常一样,我们将 Keras 指南重点放在真实的示例代码上。您可以随时点击指南顶部的 Colab 链接来运行代码。
首先,让我们安装 keras-hub。该库在 PyPI 上可用,因此我们可以直接使用 pip 安装它。
!pip install --upgrade --quiet keras-hub-nightly keras-nightly
Keras 3 构建在 TensorFlow、Jax 和 Torch 后端之上。在编写 Keras 代码时,您应该在任何库导入之前首先指定后端。本指南中我们将使用 Jax 后端,但您可以使用 torch
或 tensorflow
而无需更改本指南其余部分中的任何一行代码。这就是 Keras 3 的强大之处!
我们还将设置 XLA_PYTHON_CLIENT_MEM_FRACTION
,它会从一开始就释放整个 GPU 以供 Jax 使用。
import os
os.environ["KERAS_BACKEND"] = "jax" # or "tensorflow" or "torch"
os.environ["XLA_PYTHON_CLIENT_MEM_FRACTION"] = "1.0"
最后,我们需要进行一些额外的设置才能访问本指南中使用的模型。许多流行的开放式 LLM,例如 Google 的 Gemma 和 Meta 的 Llama,都需要先接受社区许可才能访问模型权重。我们将在本指南中使用 Gemma,因此我们可以按照以下步骤操作
KAGGLE_USERNAME
(您的用户名)和 KAGGLE_KEY
(您刚刚创建的 API 密钥)。使这些密钥对您正在运行的笔记本可见。在我们开始之前,让我们看看将在 KerasHub 库中使用的关键类。
keras_hub.models.CausalLM
、keras_hub.models.ImageClassifier
和 keras_hub.models.TextClassifier
。backbone
和 preprocessor
。keras.Model
。keras_hub.models.Backbone
。keras.Model
。keras_hub.models.CausalLMPreprocessor
、keras_hub.models.ImageClassifierPreprocessor
和 keras_hub.models.TextClassifierPreprocessor
。tokenizer
、audio_converter
和/或 image_converter
。keras.layers.Layer
。keras_hub.tokenizers.Tokenizer
。detokenize()
方法)。keras.layers.Layer
。keras_hub.layers.ImageConverter
。keras.layers.Layer
。keras_hub.layers.AudioConveter
。keras.layers.Layer
。此处列出的所有类都具有 from_preset()
构造函数,它将使用给定预训练模型标识符的权重和状态实例化组件。例如,keras_hub.tokenizers.Tokenizer.from_preset("gemma2_2b_en")
将创建一个使用 Gemma2 分词器词汇表对文本进行标记化的层。
下图显示了所有这些核心类是如何交互的。箭头表示组合而不是继承(例如,任务具有骨干网络)。
设置就绪!让我们使用预训练模型来玩耍。让我们加载一张加州鹌鹑的测试图像并对其进行分类。
import keras
import numpy as np
import matplotlib.pyplot as plt
image_url = "https://upload.wikimedia.org/wikipedia/commons/a/aa/California_quail.jpg"
image_path = keras.utils.get_file(origin=image_url)
image = keras.utils.load_img(image_path)
plt.imshow(image)
我们可以使用在 ImageNet-1k 数据库上训练的 ResNet 视觉模型。该模型将为每个输入样本和输出标签从 [0, 1000)
中输出一个值,其中每个标签对应于一些真实世界的实体,例如“奶罐”或“豪猪”。该数据集实际上有一个鹌鹑的特定标签,索引为 85。让我们下载该模型并预测一个标签。
import keras_hub
image_classifier = keras_hub.models.ImageClassifier.from_preset(
"resnet_50_imagenet",
activation="softmax",
)
batch = np.array([image])
image_classifier.preprocessor.image_size = (224, 224)
preds = image_classifier.predict(batch)
preds.shape
1/1 ━━━━━━━━━━━━━━━━━━━━ 2s 2s/step
(1, 1000)
这些 ImageNet 标签不是特别“人类可读”的,因此我们可以使用内置的实用函数将预测解码为一组类名称。
keras_hub.utils.decode_imagenet_predictions(preds)
[[('quail', 0.9996534585952759),
('prairie_chicken', 8.45497488626279e-05),
('partridge', 1.4000976079842076e-05),
('black_grouse', 7.407367775158491e-06),
('bullet_train', 7.323932550207246e-06)]]
看起来不错!模型权重已成功下载,并且我们几乎可以肯定地预测了鹌鹑图像的正确分类标签。
这是我们在上面 API 快速入门中提到的高级任务 API 的第一个示例。keras_hub.models.ImageClassifier
是用于分类图像的任务,可以与许多不同的模型架构(ResNet、VGG、MobileNet 等)一起使用。您可以在 Kaggle 上查看 Keras 团队直接提供的模型的完整列表。
任务只是 keras.Model
的一个子类 — 您可以像使用任何其他模型一样在我们的 classifier
对象上使用 fit()
、compile()
和 save()
。但是任务还附带 KerasHub 库提供的一些额外功能。第一个也是最重要的一个功能是 from_preset()
,这是一个特殊的构造函数,您将在 KerasHub 中的许多类上看到它。
预设是模型状态的目录。它定义了我们应该加载的架构以及随之而来的预训练权重。from_preset()
允许我们从许多不同的位置加载预设目录
您可以查看 keras_hub.models.ImageClassifier.from_preset
文档,以更好地了解从预设构建 Keras 模型时的所有选项。
所有任务都使用两个主要的子对象。一个是 keras_hub.models.Backbone
,另一个是 keras_hub.layers.Preprocessor
。你可能已经熟悉计算机视觉中的骨干网络这个术语,它通常用来描述将图像映射到潜在空间的特征提取网络。KerasHub 的骨干网络是对这个概念的泛化,我们用它来指代任何没有特定任务头的预训练模型。也就是说,KerasHub 的骨干网络将原始图像、音频和文本(或这些输入的组合)映射到预训练模型的潜在空间。然后,我们可以将这个潜在空间映射到任意数量的特定任务输出,具体取决于我们想用模型做什么。
预处理器只是一个 Keras 层,它执行特定任务的所有预处理。在我们的例子中,预处理将使用一些 ImageNet 特定的均值和方差数据,将我们的输入图像调整大小并将其重新缩放到 [0, 1]
范围。让我们依次调用任务的预处理器和骨干网络,看看我们的输入形状会发生什么变化。
print("Raw input shape:", batch.shape)
resized_batch = image_classifier.preprocessor(batch)
print("Preprocessed input shape:", resized_batch.shape)
hidden_states = image_classifier.backbone(resized_batch)
print("Latent space shape:", hidden_states.shape)
Raw input shape: (1, 557, 707, 3)
Preprocessed input shape: (1, 224, 224, 3)
Latent space shape: (1, 7, 7, 2048)
我们的原始图像在预处理过程中被重新缩放到 (224, 224)
,最后被缩小到具有 2048 个特征向量的 (7, 7)
图像 —— ResNet 模型的潜在空间。请注意,ResNet 实际上可以处理任意大小的图像,但如果你的图像与预训练数据的大小差异很大,性能最终会下降。如果你想禁用预处理层中的调整大小功能,可以运行 image_classifier.preprocessor.image_size = None
。
如果你想知道加载的任务的确切结构,你可以像任何 Keras 模型一样使用 model.summary()
。任务的模型摘要将包含有关模型预处理的额外信息。
image_classifier.summary()
Preprocessor: "res_net_image_classifier_preprocessor"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Config ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ res_net_image_converter │ Image size: (224, 224) │ │ (ResNetImageConverter) │ │ └──────────────────────────────────────────────┴───────────────────────────────┘
Model: "res_net_image_classifier"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ input_layer (InputLayer) │ (None, None, None, 3) │ 0 │ ├───────────────────────────────────┼──────────────────────────┼───────────────┤ │ res_net_backbone (ResNetBackbone) │ (None, None, None, 2048) │ 23,561,152 │ ├───────────────────────────────────┼──────────────────────────┼───────────────┤ │ pooler (GlobalAveragePooling2D) │ (None, 2048) │ 0 │ ├───────────────────────────────────┼──────────────────────────┼───────────────┤ │ output_dropout (Dropout) │ (None, 2048) │ 0 │ ├───────────────────────────────────┼──────────────────────────┼───────────────┤ │ predictions (Dense) │ (None, 1000) │ 2,049,000 │ └───────────────────────────────────┴──────────────────────────┴───────────────┘
Total params: 25,610,152 (97.69 MB)
Trainable params: 25,557,032 (97.49 MB)
Non-trainable params: 53,120 (207.50 KB)
接下来,让我们尝试使用和生成文本。我们在生成文本时可以使用的任务是 keras_hub.models.CausalLM
(其中 LM 是语言模型的缩写)。让我们下载 20 亿参数的 Gemma 2 模型并试用一下。
由于此模型比我们刚刚下载的 ResNet 模型大约大 100 倍,因此我们需要更加注意 GPU 内存的使用情况。我们可以使用半精度类型将我们大约 25 亿个参数的每一个加载为双字节浮点数,而不是四字节。为此,我们可以将 dtype
传递给 from_preset()
构造函数。from_preset()
会将任何 kwargs 转发到该类的主要构造函数,因此你可以传递适用于所有 Keras 层(如 dtype
、trainable
和 name
)的 kwargs。
causal_lm = keras_hub.models.CausalLM.from_preset(
"gemma2_instruct_2b_en",
dtype="bfloat16",
)
我们刚刚加载的模型是 Gemma 的指令调整版本,这意味着该模型已针对聊天进行了进一步的微调。只要我们坚持使用训练模型时使用的特定文本模板,我们就可以利用这些功能。这些特殊标记因模型而异,可能难以跟踪,Kaggle 模型页面 将包含此类详细信息。
CausalLM
带有一个名为 generate()
的额外函数,该函数可用于循环生成预测令牌并将其解码为字符串。
template = "<start_of_turn>user\n{question}<end_of_turn>\n<start_of_turn>model"
question = """Write a python program to generate the first 1000 prime numbers.
Just show the actual code."""
print(causal_lm.generate(template.format(question=question), max_length=512))
<start_of_turn>user
Write a python program to generate the first 1000 prime numbers.
Just show the actual code.<end_of_turn>
<start_of_turn>model
def is_prime(n):
if n <= 1:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
count = 0
number = 2
primes = []
while count < 1000:
if is_prime(number):
primes.append(number)
count += 1
number += 1
print(primes)
<end_of_turn>
请注意,在 Jax 和 TensorFlow 后端,此 generate()
函数是编译的,因此第二次调用相同的 max_length
时,它实际上会快得多。KerasHub 将使用 Jax 和 TensorFlow 来计算可以重用的优化版本生成计算图。
question = "Share a very simple brownie recipe."
print(causal_lm.generate(template.format(question=question), max_length=512))
<start_of_turn>user
Share a very simple brownie recipe.<end_of_turn>
<start_of_turn>model
---
## Super Simple Brownies
**Ingredients:**
* 1 cup (2 sticks) unsalted butter, melted
* 2 cups granulated sugar
* 4 large eggs
* 1 teaspoon vanilla extract
* 1 cup all-purpose flour
* 1/2 cup unsweetened cocoa powder
* 1/4 teaspoon salt
**Instructions:**
1. Preheat oven to 350°F (175°C). Grease and flour a 9x13 inch baking pan.
2. In a large bowl, whisk together the melted butter and sugar until smooth.
3. Beat in the eggs one at a time, then stir in the vanilla extract.
4. In a separate bowl, whisk together the flour, cocoa powder, and salt.
5. Gradually add the dry ingredients to the wet ingredients, mixing until just combined. Do not overmix.
6. Pour the batter into the prepared pan and spread evenly.
7. Bake for 25-30 minutes, or until a toothpick inserted into the center comes out with a few moist crumbs attached.
8. Let cool completely before cutting and serving.
**Tips:**
* For extra fudgy brownies, underbake them slightly.
* Add chocolate chips, nuts, or other mix-ins to the batter for a personalized touch.
* Serve with a scoop of ice cream or whipped cream for a decadent treat.
Enjoy!
<end_of_turn>
与我们的图像分类器一样,我们可以使用模型摘要来查看任务设置的详细信息,包括预处理。
causal_lm.summary()
Preprocessor: "gemma_causal_lm_preprocessor"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Config ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ gemma_tokenizer (GemmaTokenizer) │ Vocab size: 256,000 │ └──────────────────────────────────────────────┴───────────────────────────────┘
Model: "gemma_causal_lm"
┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩ │ padding_mask │ (None, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├───────────────────────┼───────────────────┼─────────────┼────────────────────┤ │ token_ids │ (None, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├───────────────────────┼───────────────────┼─────────────┼────────────────────┤ │ gemma_backbone │ (None, None, │ 2,614,341,… │ padding_mask[0][0… │ │ (GemmaBackbone) │ 2304) │ │ token_ids[0][0] │ ├───────────────────────┼───────────────────┼─────────────┼────────────────────┤ │ token_embedding │ (None, None, │ 589,824,000 │ gemma_backbone[0]… │ │ (ReversibleEmbedding) │ 256000) │ │ │ └───────────────────────┴───────────────────┴─────────────┴────────────────────┘
Total params: 2,614,341,888 (4.87 GB)
Trainable params: 2,614,341,888 (4.87 GB)
Non-trainable params: 0 (0.00 B)
我们的文本预处理包括一个分词器,这是所有 KerasHub 模型处理输入文本的方式。让我们尝试直接使用它,以更好地了解其工作原理。所有分词器都包含 tokenize()
和 detokenize()
方法,用于将字符串映射到整数序列,并将整数序列映射到字符串。直接调用带有 tokenizer(inputs)
的层等同于调用 tokenizer.tokenize(inputs)
。
tokenizer = causal_lm.preprocessor.tokenizer
tokens_ids = tokenizer.tokenize("The quick brown fox jumps over the lazy dog.")
print(tokens_ids)
string = tokenizer.detokenize(tokens_ids)
print(string)
[ 651 4320 8426 25341 36271 1163 573 27894 5929 235265]
The quick brown fox jumps over the lazy dog.
CausalLM
模型的 generate()
函数涉及一个采样步骤。对于我们要生成的每个令牌,将调用一次 Gemma 模型,并返回所有令牌的概率分布。然后对这个分布进行采样,以选择序列中的下一个令牌。
对于 Gemma 模型,我们默认使用贪婪采样,这意味着我们只是在每一步中选择模型中最可能的输出。但我们实际上可以使用所有 Keras 模型上的标准 compile
函数的额外 sampler
参数来控制此过程。让我们试一下。
causal_lm.compile(
sampler=keras_hub.samplers.TopKSampler(k=10, temperature=2.0),
)
question = "Share a very simple brownie recipe."
print(causal_lm.generate(template.format(question=question), max_length=512))
<start_of_turn>user
Share a very simple brownie recipe.<end_of_turn>
<start_of_turn>model ## Ultimate Simple Brownies
This recipe requires NO oven or special equipment! Just microwave, mixing, and a few moments!
**Yields:** 6 large brownies
**Prep time:** 7 minutes
**Cook time:** 6-9 minutes, depending on your microwave
**What you need:**
* 3 ounces (about 2-3 tablespoons) chocolate chips
* 1/4 cup butter
* 1 large egg
* 1/2 cup granulated sugar
* 9 tablespoons all-purpose flour
**Optional Add-Ins (for extra fun):**
* 1/2 teaspoon vanilla
* 1/4 cup chopped walnuts or pecans
**Instructions:**
1. Place all microwave-safe mixing bowl ingredients:
- Chocolate Chips 🍫
- Butter 🧈
- Flour 🗲
- Egg (beaten!)
(You can add the optional add-INS like chopped nuts/extra vanilla, now is the good place to!)
2. Put all that in your microwave (microwave-safe dish or a heat-safe mug is fine!)
3. **Cook on:** Medium-high, stirring halfway.
* Time depends on your microwave, so keep checking, but aim for 6-9 minutes (if no stirring at least 8 mins). You want a thick, almost chewy-texture.
**To serve:** Cut up your brownies immediately and savor this classic treat. You'd also need a tall glass of cold milk or coffee (or both, if you've really enjoyed it).
Let me know if you want to experiment with a different chocolate or add-ins to make it even sweeter. Enjoy! 😉
<end_of_turn>
在这里,我们使用了 Top-K 采样器,这意味着我们将通过查看每个时间步预测的前 10 个令牌形成的局部分布来随机采样。我们还传递了 2 的 temperature
,这会在我们采样之前平滑我们的预测分布。
最终效果是,每次生成输出时,我们都会更广泛地探索模型的分布。生成现在将是一个随机过程,每次我们重新运行生成时,都会得到不同的结果。我们可以注意到,结果感觉比贪婪搜索“更宽松” —— 更多的小错误、不太一致,以及微波加热布朗尼的令人怀疑的建议。
你可以在 keras_hub.samplers 中查看 Keras 支持的所有采样器。
在跳到下一节之前,让我们释放大型 Gemma 模型的内存。
del causal_lm
现在我们已经尝试了运行图像和文本的推理,让我们尝试运行训练。我们将使用之前 ResNet 图像分类器,并在简单的猫与狗数据集上对其进行微调。我们可以从下载和提取数据开始。
import pathlib
extract_dir = keras.utils.get_file(
"cats_vs_dogs",
"https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_5340.zip",
extract=True,
)
data_dir = pathlib.Path(extract_dir) / "PetImages"
当处理大量真实世界的图像数据时,损坏的图像是一种常见现象。让我们过滤掉头部没有“JFIF”字符串的编码不良的图像。
num_skipped = 0
for path in data_dir.rglob("*.jpg"):
with open(path, "rb") as file:
is_jfif = b"JFIF" in file.peek(10)
if not is_jfif:
num_skipped += 1
os.remove(path)
print(f"Deleted {num_skipped} images.")
Deleted 1590 images.
我们可以使用 keras.utils.image_dataset_from_directory
加载数据集。这里需要注意的重要一点是,train_ds
和 val_ds
都将作为 tf.data.Dataset
对象返回,包括在 torch
和 jax
后端上。
KerasHub 将使用 tf.data 作为在 CPU 上运行多线程预处理的默认 API。tf.data
是一个强大的 API,用于训练输入管道,可以轻松扩展到复杂的多主机训练作业。使用它不会限制你对后端的选择,tf.data.Dataset
可以作为常规 numpy 数据的迭代器,并传递到任何 Keras 后端的 fit()
。
train_ds, val_ds = keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="both",
seed=1337,
image_size=(256, 256),
batch_size=32,
)
Found 23410 files belonging to 2 classes.
Using 18728 files for training.
Using 4682 files for validation.
最简单的做法是,训练我们的分类器可以简单地在带有数据集的模型上调用 fit()
。但为了使这个示例更有趣一些,让我们展示如何在任务中自定义预处理。
在第一个示例中,我们看到了默认情况下,我们的 ResNet 模型的预处理如何调整大小并重新缩放我们的输入。当我们创建模型时,可以自定义此预处理。我们可以使用 Keras 的图像预处理层来创建一个 keras.layers.Pipeline
,它将重新缩放、随机翻转和随机旋转我们的输入图像。这些随机图像增强将使我们较小的数据集可以用作更大、更多样化的数据集。让我们试一下。
preprocessor = keras.layers.Pipeline(
[
keras.layers.Rescaling(1.0 / 255),
keras.layers.RandomFlip("horizontal"),
keras.layers.RandomRotation(0.2),
]
)
现在我们已经创建了一个新的预处理层,我们可以在 from_preset()
构造函数中将其传递给 ImageClassifier
。我们还可以传递 num_classes=2
以匹配我们的“猫”和“狗”的两个标签。当像这样指定 num_classes
时,我们模型的头部权重将被随机初始化,而不是包含我们 1000 类图像分类的权重。
image_classifier = keras_hub.models.ImageClassifier.from_preset(
"resnet_50_imagenet",
activation="softmax",
num_classes=2,
preprocessor=preprocessor,
)
请注意,如果你想在 Keras 之外预处理你的输入数据,你可以简单地将 preprocessor=None
传递给任务的 from_preset()
调用。在这种情况下,KerasHub 将根本不应用预处理,你可以自由地使用任何库或工作流预处理你的数据,然后再将你的数据传递给 fit()
。
接下来,我们可以编译我们的模型进行微调。KerasHub 任务只是一个具有一些额外功能的常规 keras.Model
,因此我们可以像分类任务一样正常 compile()
。
image_classifier.compile(
optimizer=keras.optimizers.Adam(1e-4),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
有了这些,我们可以简单地运行 fit()
。在训练模型时,图像分类器会自动将我们的预处理应用于每个批次。
image_classifier.fit(
train_ds,
validation_data=val_ds,
epochs=3,
)
Epoch 1/3
586/586 ━━━━━━━━━━━━━━━━━━━━ 0s 122ms/step - accuracy: 0.8869 - loss: 0.2921
Epoch 2/3
586/586 ━━━━━━━━━━━━━━━━━━━━ 65s 105ms/step - accuracy: 0.9858 - loss: 0.0393 - val_accuracy: 0.9912 - val_loss: 0.0234
Epoch 3/3
586/586 ━━━━━━━━━━━━━━━━━━━━ 57s 96ms/step - accuracy: 0.9897 - loss: 0.0289 - val_accuracy: 0.9930 - val_loss: 0.0206
<keras.src.callbacks.history.History at 0x787e77fb2550>
经过三个 epoch 的数据后,我们在猫与狗验证数据集上实现了 99% 的准确率。这并不奇怪,因为我们开始使用的 ImageNet 预训练权重已经可以单独对某些品种的猫和狗进行分类。
现在我们有了一个微调模型,让我们尝试保存它。你可以通过简单地运行 task.save_to_preset()
为任何任务创建具有微调模型的新保存预设。
image_classifier.save_to_preset("cats_vs_dogs")
KerasHub 最强大的功能之一是将模型上传到 Kaggle 或 Huggingface 模型中心并与他人共享的能力。keras_hub.upload_preset
允许你上传保存的预设。
在这种情况下,我们将上传到 Kaggle。我们已经使用 Kaggle 进行了身份验证,以下载早期的 Gemma 模型。运行以下单元格会将新模型上传到 Kaggle。
from google.colab import userdata
username = userdata.get("KAGGLE_USERNAME")
keras_hub.upload_preset(
f"kaggle://{username}/resnet/keras/cats_vs_dogs",
"cats_vs_dogs",
)
Uploading Model https://www.kaggle.com/models/matthewdwatson/resnet/keras/cats_vs_dogs ...
Upload successful: cats_vs_dogs/task.json (5KB)
Upload successful: cats_vs_dogs/task.weights.h5 (270MB)
Upload successful: cats_vs_dogs/metadata.json (157B)
Upload successful: cats_vs_dogs/model.weights.h5 (90MB)
Upload successful: cats_vs_dogs/config.json (841B)
Upload successful: cats_vs_dogs/preprocessor.json (3KB)
Your model instance version has been created.
Files are being processed...
See at: https://www.kaggle.com/models/matthewdwatson/resnet/keras/cats_vs_dogs
让我们看一下我们数据集中的测试图像。
image = keras.utils.load_img(data_dir / "Cat" / "6779.jpg")
plt.imshow(image)
如果我们等待几分钟,让我们的模型上传在 Kaggle 端完成处理,我们可以继续下载我们刚刚创建的模型,并使用它来分类此测试图像。
image_classifier = keras_hub.models.ImageClassifier.from_preset(
f"kaggle://{username}/resnet/keras/cats_vs_dogs",
)
print(image_classifier.predict(np.array([image])))
1/1 ━━━━━━━━━━━━━━━━━━━━ 2s 2s/step
[[9.999286e-01 7.135461e-05]]
恭喜你使用 KerasHub 上传了你的第一个模型!如果你想与他人分享你的工作,你可以转到我们上传模型时打印出的模型链接,并在设置中将模型设置为公开。
在继续本指南的最后一个示例之前,让我们删除此模型以释放内存。
del image_classifier
作为本入门指南的最后一个示例,让我们看一下如何从较低级别的 Keras 和 KerasHub 组件构建自定义模型。我们将构建一个文本分类器,将 IMDb 数据集中的电影评论分类为正面或负面。
让我们下载数据集。
extract_dir = keras.utils.get_file(
"imdb_reviews",
origin="https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz",
extract=True,
)
data_dir = pathlib.Path(extract_dir) / "aclImdb"
IMDb 数据集包含大量未标记的电影评论。我们在这里不需要这些,我们可以简单地删除它们。
import shutil
shutil.rmtree(data_dir / "train" / "unsup")
接下来,我们可以使用 keras.utils.text_dataset_from_directory
加载我们的数据。与上面创建的图像数据集一样,返回的数据集将是 tf.data.Dataset
对象。
raw_train_ds = keras.utils.text_dataset_from_directory(
data_dir / "train",
batch_size=2,
)
raw_val_ds = keras.utils.text_dataset_from_directory(
data_dir / "test",
batch_size=2,
)
Found 25000 files belonging to 2 classes.
Found 25000 files belonging to 2 classes.
KerasHub 被设计为分层 API。在最顶层,任务旨在使快速解决问题变得容易。我们可以继续使用任务 API,并为像 BERT 这样的文本分类模型创建一个 keras_hub.models.TextClassifer
,并在大约 10 行代码中对其进行微调。
相反,为了使我们的最终示例更有趣,让我们展示如何使用较低级别的 API 组件来完成库中未直接内置的内容。我们将使用我们之前使用过的 Gemma 2 模型,该模型通常用于生成文本,并对其进行修改以输出分类预测。
使用生成模型进行分类的常见方法是通过提示评论和一个问题(“这个评论是正面还是负面?”
)在生成上下文中继续使用它。但是,如果您想要与标签关联的实际概率分数,那么创建一个实际的分类器会更有用。
我们可以不通过 CausalLM
任务加载 Gemma 2 模型,而是加载两个较低级别的组件:一个骨干网络和一个分词器。与我们目前为止使用的任务类非常相似,keras_hub.models.Backbone
和 keras_hub.tokenizers.Tokenizer
都具有用于加载预训练模型的 from_preset()
构造函数。如果您正在运行此代码,您会注意到您不必等待下载,因为我们第二次使用该模型,权重文件会在我们第一次使用该模型时在本地缓存。
tokenizer = keras_hub.tokenizers.Tokenizer.from_preset(
"gemma2_instruct_2b_en",
)
backbone = keras_hub.models.Backbone.from_preset(
"gemma2_instruct_2b_en",
)
我们在本指南的第二个示例中看到了分词器的作用。我们可以使用它以一种与 Gemma 模型的预训练权重相匹配的方式将字符串输入映射到标记 ID。
骨干网络会将一系列标记 ID 映射到模型潜在空间中的一系列嵌入标记。我们可以使用这种丰富的表示来构建分类器。
让我们从定义一个自定义预处理例程开始。keras_hub.layers
包含建模和预处理层的集合,包括一些用于标记预处理的层。我们可以使用 keras_hub.layers.StartEndPacker
,它将在每个评论的开头附加一个特殊的开始标记,在结尾附加一个特殊的结束标记,最后将每个评论截断或填充到固定长度。
如果将其与我们的 tokenizer
结合使用,我们可以构建一个预处理函数,该函数将输出形状为 (batch_size, sequence_length)
的标记 ID 批次。我们还应该输出一个填充掩码,该掩码标记哪些标记是填充标记,以便我们稍后可以将这些位置排除在 Transformer 的注意力计算之外。KerasNLP 中的大多数 Transformer 骨干网络都接受 "padding_mask"
输入。
packer = keras_hub.layers.StartEndPacker(
start_value=tokenizer.start_token_id,
end_value=tokenizer.end_token_id,
pad_value=tokenizer.pad_token_id,
sequence_length=None,
)
def preprocess(x, y=None, sequence_length=256):
x = tokenizer(x)
x = packer(x, sequence_length=sequence_length)
x = {
"token_ids": x,
"padding_mask": x != tokenizer.pad_token_id,
}
return keras.utils.pack_x_y_sample_weight(x, y)
定义了预处理之后,我们可以简单地使用 tf.data.Dataset.map
将预处理应用于我们的输入数据。
train_ds = raw_train_ds.map(preprocess, num_parallel_calls=16)
val_ds = raw_val_ds.map(preprocess, num_parallel_calls=16)
next(iter(train_ds))
({'token_ids': <tf.Tensor: shape=(2, 256), dtype=int32, numpy=
array([[ 2, 94300, 1185, ... 0]],
dtype=int32)>,
'padding_mask': <tf.Tensor: shape=(2, 256), dtype=bool, numpy=
array([[ True, True, True, ... False]])>},
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([1, 0], dtype=int32)>)
与我们之前训练的图像分类器相比,在一个 25 亿参数的模型上运行微调是相当昂贵的,原因很简单,这个模型是 ResNet 的 100 倍!为了加快速度,让我们将训练数据的大小减少到原始大小的十分之一。当然,与完全训练相比,这会损失一些性能,但它会使我们的指南运行速度更快。
train_ds = train_ds.take(1000)
val_ds = val_ds.take(1000)
接下来,我们需要将分类头附加到我们的骨干网络模型。通常,文本 Transformer 骨干网络将输出形状为 (batch_size, sequence_length, hidden_dim)
的张量。我们需要使用此输入进行分类的主要事情是在序列维度上进行池化,以便每个输入示例都有一个特征向量。
由于 Gemma 模型是生成模型,因此信息仅从序列的左侧传递到右侧。唯一可以“看到”整个电影评论输入的标记表示是每个评论中的最后一个标记。我们可以编写一个简单的池化层来实现这一点——我们只需抓取每个输入序列的最后一个非填充位置。编写这样的层没有特殊的过程,我们可以正常使用 Keras 和 keras.ops
。
from keras import ops
class LastTokenPooler(keras.layers.Layer):
def call(self, inputs, padding_mask):
end_positions = ops.sum(padding_mask, axis=1, keepdims=True) - 1
end_positions = ops.cast(end_positions, "int")[:, :, None]
outputs = ops.take_along_axis(inputs, end_positions, axis=1)
return ops.squeeze(outputs, axis=1)
有了这个池化层,我们就可以编写我们的 Gemma 分类器了。KerasHub 中的所有任务和骨干网络模型都是 函数式 模型,因此我们可以轻松地操作模型结构。我们将在输入上调用我们的骨干网络,添加我们新的池化层,最后添加一个中间带有 "relu"
激活的小型前馈网络。让我们试一试。
inputs = backbone.input
x = backbone(inputs)
x = LastTokenPooler(
name="pooler",
)(x, inputs["padding_mask"])
x = keras.layers.Dense(
2048,
activation="relu",
name="pooled_dense",
)(x)
x = keras.layers.Dropout(
0.1,
name="output_dropout",
)(x)
outputs = keras.layers.Dense(
2,
activation="softmax",
name="output_dense",
)(x)
text_classifier = keras.Model(inputs, outputs)
text_classifier.summary()
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ │ padding_mask │ (None, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ token_ids │ (None, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ gemma_backbone │ (None, None, │ 2,614,341… │ padding_mask[0][… │ │ (GemmaBackbone) │ 2304) │ │ token_ids[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ pooler │ (None, 2304) │ 0 │ gemma_backbone[0… │ │ (LastTokenPooler) │ │ │ padding_mask[0][… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ pooled_dense │ (None, 2048) │ 4,720,640 │ pooler[0][0] │ │ (Dense) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ output_dropout │ (None, 2048) │ 0 │ pooled_dense[0][… │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ output_dense │ (None, 2) │ 4,098 │ output_dropout[0… │ │ (Dense) │ │ │ │ └─────────────────────┴───────────────────┴────────────┴───────────────────┘
Total params: 2,619,066,626 (9.76 GB)
Trainable params: 2,619,066,626 (9.76 GB)
Non-trainable params: 0 (0.00 B)
在训练之前,我们应该采用最后一个技巧,使这段代码在免费层 colab GPU 上运行。我们可以从我们的模型摘要中看到,我们的模型占用了近 10 GB 的空间。优化器需要在训练期间创建每个参数的多个副本,从而使模型在训练期间的总空间接近 30 或 40 GB。
这将使许多 GPU 出现 OOM 错误。我们可以采用的一个有用的技巧是在我们的骨干网络上启用 LoRA。LoRA 是一种冻结整个模型的方法,仅训练大型权重矩阵的低参数分解。您可以在这个 Keras 示例 中阅读有关 LoRA 的更多信息。让我们尝试启用它并重新打印我们的摘要。
backbone.enable_lora(4)
text_classifier.summary()
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ │ padding_mask │ (None, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ token_ids │ (None, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ gemma_backbone │ (None, None, │ 2,617,270… │ padding_mask[0][… │ │ (GemmaBackbone) │ 2304) │ │ token_ids[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ pooler │ (None, 2304) │ 0 │ gemma_backbone[0… │ │ (LastTokenPooler) │ │ │ padding_mask[0][… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ pooled_dense │ (None, 2048) │ 4,720,640 │ pooler[0][0] │ │ (Dense) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ output_dropout │ (None, 2048) │ 0 │ pooled_dense[0][… │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ output_dense │ (None, 2) │ 4,098 │ output_dropout[0… │ │ (Dense) │ │ │ │ └─────────────────────┴───────────────────┴────────────┴───────────────────┘
Total params: 2,621,995,266 (9.77 GB)
Trainable params: 7,653,378 (29.20 MB)
Non-trainable params: 2,614,341,888 (9.74 GB)
启用 LoRA 后,我们的模型从 10GB 的可训练参数减少到仅 20MB。这意味着优化器变量使用的空间将不再是问题。
完成所有设置后,我们可以像平常一样编译和训练我们的模型。
text_classifier.compile(
optimizer=keras.optimizers.Adam(5e-5),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
text_classifier.fit(
train_ds,
validation_data=val_ds,
)
1000/1000 ━━━━━━━━━━━━━━━━━━━━ 295s 285ms/step - accuracy: 0.7733 - loss: 0.6511 - val_accuracy: 0.9370 - val_loss: 0.2814
<keras.src.callbacks.history.History at 0x787e103ae010>
我们能够在电影评论情感分类问题上达到 93% 以上的准确率。考虑到我们只使用了原始数据集的十分之一进行训练,这还不错。
总而言之,我们在此示例中创建的 backbone
和 tokenizer
使我们能够访问预训练 Gemma 检查点的全部功能,而不会限制我们对其进行操作。这是 KerasHub API 的中心目标。简单的工作流程应该很容易,并且随着您的深入,您将可以访问一组高度可定制的构建模块。
这只是您可以使用 KerasHub 完成的事情的冰山一角。
本指南展示了我们随 KerasHub 库提供的一些高级任务,但还有许多任务我们没有在这里介绍。例如,尝试 使用 Stable Diffusion 生成图像。
KerasHub 最显著的优势在于它使您可以灵活地将预训练的构建模块与 Keras 3 的全部功能相结合。您可以使用 keras.distribution API 在带有模型并行性的 TPU 上训练大型 LLM。您可以使用 Keras 的 量化方法 量化模型。您可以编写自定义训练循环,甚至可以混合使用直接的 Jax、Torch 或 Tensorflow 调用。
请访问 keras.io/keras_hub 查看完整的指南和示例列表,以继续深入研究该库。