作者: Matthew Watson
创建日期: 2022/04/18
最后修改: 2023/07/15
描述: 使用 KerasNLP 从头训练一个 Transformer 模型。
KerasNLP 的目标是简化最先进的文本处理模型的构建。在本指南中,我们将展示库组件如何简化从头开始的 Transformer 模型的预训练和微调。
本指南分为三个部分:
以下指南使用 Keras 3 在 tensorflow
、jax
或 torch
中工作。我们在下面选择 jax
后端,这将使我们在后面的训练步骤中获得特别快的速度,但您可以随意混合使用。
!pip install -q --upgrade keras-nlp
!pip install -q --upgrade keras # 升级到 Keras 3。
import os
os.environ["KERAS_BACKEND"] = "jax" # 或 "tensorflow" 或 "torch"
import keras_nlp
import tensorflow as tf
import keras
接下来,我们可以下载两个数据集。
最后,我们将下载一个 WordPiece 词汇表,以便在本指南后面进行子词标记化。
# 下载预训练数据。
keras.utils.get_file(
origin="https://s3.amazonaws.com/research.metamind.io/wikitext/wikitext-103-raw-v1.zip",
extract=True,
)
wiki_dir = os.path.expanduser("~/.keras/datasets/wikitext-103-raw/")
# 下载微调数据。
keras.utils.get_file(
origin="https://dl.fbaipublicfiles.com/glue/data/SST-2.zip",
extract=True,
)
sst_dir = os.path.expanduser("~/.keras/datasets/SST-2/")
# 下载词汇数据。
vocab_file = keras.utils.get_file(
origin="https://storage.googleapis.com/tensorflow/keras-nlp/examples/bert/bert_vocab_uncased.txt",
)
接下来,我们定义一些训练过程中将使用的超参数。
# 预处理参数。
PRETRAINING_BATCH_SIZE = 128
FINETUNING_BATCH_SIZE = 32
SEQ_LENGTH = 128
MASK_RATE = 0.25
PREDICTIONS_PER_SEQ = 32
# 模型参数。
NUM_LAYERS = 3
MODEL_DIM = 256
INTERMEDIATE_DIM = 512
NUM_HEADS = 4
DROPOUT = 0.1
NORM_EPSILON = 1e-5
# 训练参数。
PRETRAINING_LEARNING_RATE = 5e-4
PRETRAINING_EPOCHS = 8
FINETUNING_LEARNING_RATE = 5e-5
FINETUNING_EPOCHS = 3
我们使用 tf.data 加载数据,这将允许我们定义输入管道以进行标记化和文本预处理。
# 加载 SST-2。
sst_train_ds = tf.data.experimental.CsvDataset(
sst_dir + "train.tsv", [tf.string, tf.int32], header=True, field_delim="\t"
).batch(FINETUNING_BATCH_SIZE)
sst_val_ds = tf.data.experimental.CsvDataset(
sst_dir + "dev.tsv", [tf.string, tf.int32], header=True, field_delim="\t"
).batch(FINETUNING_BATCH_SIZE)
# 加载 wikitext-103,并过滤掉短行。
wiki_train_ds = (
tf.data.TextLineDataset(wiki_dir + "wiki.train.raw")
.filter(lambda x: tf.strings.length(x) > 100)
.batch(PRETRAINING_BATCH_SIZE)
)
wiki_val_ds = (
tf.data.TextLineDataset(wiki_dir + "wiki.valid.raw")
.filter(lambda x: tf.strings.length(x) > 100)
.batch(PRETRAINING_BATCH_SIZE)
)
# 看看 sst-2 数据集的样子。
print(sst_train_ds.unbatch().batch(4).take(1).get_single_element())
(<tf.Tensor: shape=(4,), dtype=string, numpy=
array([b'hide new secretions from the parental units ',
b'contains no wit , only labored gags ',
b'that loves its characters and communicates something rather beautiful about human nature ',
b'remains utterly satisfied to remain the same throughout '],
dtype=object)>, <tf.Tensor: shape=(4,), dtype=int32, numpy=array([0, 0, 1, 0], dtype=int32)>)
您可以看到我们的 SST-2
数据集包含相对较短的电影评论文本片段。我们的目标是预测该片段的情感。标签 1 表示积极情感,标签 0 表示消极情感。
作为第一步,我们将建立良好表现的基线。实际上,我们不需要 KerasNLP,只需使用核心 Keras 层即可。
我们将训练一个简单的词袋模型,其中我们为词汇表中的每个单词学习一个正或负的权重。样本的分数只是样本中所有存在单词的权重之和。
# 该层将把我们的输入句子转换为与我们的词汇表大小相同的 1 和 0 的列表,
# 指示单词是否存在或缺失。
multi_hot_layer = keras.layers.TextVectorization(
max_tokens=4000, output_mode="multi_hot"
)
multi_hot_layer.adapt(sst_train_ds.map(lambda x, y: x))
multi_hot_ds = sst_train_ds.map(lambda x, y: (multi_hot_layer(x), y))
multi_hot_val_ds = sst_val_ds.map(lambda x, y: (multi_hot_layer(x), y))
# 然后我们在该层上学习线性回归,这就是我们的整个
# 基线模型!
inputs = keras.Input(shape=(4000,), dtype="int32")
outputs = keras.layers.Dense(1, activation="sigmoid")(inputs)
baseline_model = keras.Model(inputs, outputs)
baseline_model.compile(loss="binary_crossentropy", metrics=["accuracy"])
baseline_model.fit(multi_hot_ds, validation_data=multi_hot_val_ds, epochs=5)
Epoch 1/5
2105/2105 ━━━━━━━━━━━━━━━━━━━━ 2s 698us/step - accuracy: 0.6421 - loss: 0.6469 - val_accuracy: 0.7567 - val_loss: 0.5391
Epoch 2/5
2105/2105 ━━━━━━━━━━━━━━━━━━━━ 1s 493us/step - accuracy: 0.7524 - loss: 0.5392 - val_accuracy: 0.7868 - val_loss: 0.4891
Epoch 3/5
2105/2105 ━━━━━━━━━━━━━━━━━━━━ 1s 513us/step - accuracy: 0.7832 - loss: 0.4871 - val_accuracy: 0.7991 - val_loss: 0.4671
Epoch 4/5
2105/2105 ━━━━━━━━━━━━━━━━━━━━ 1s 475us/step - accuracy: 0.7991 - loss: 0.4543 - val_accuracy: 0.8069 - val_loss: 0.4569
Epoch 5/5
2105/2105 ━━━━━━━━━━━━━━━━━━━━ 1s 476us/step - accuracy: 0.8100 - loss: 0.4313 - val_accuracy: 0.8036 - val_loss: 0.4530
<keras.src.callbacks.history.History at 0x7f13902967a0>
词袋方法可以快速且出人意料地强大,特别是当输入示例包含大量单词时。对于较短的序列,它可能会达到性能上限。
为了做得更好,我们希望构建一个可以在上下文中评估单词的模型。我们需要利用输入的整个有序序列中包含的信息,而不是在空白处评估每个单词。
这给我们带来了一个问题。SST-2
数据集非常小,示例文本实在不够,无法尝试构建一个更大、更具参数化的模型来学习序列。我们会很快开始过拟合并记住我们的训练集,而不会提高我们对未见示例的泛化能力。
进入预训练,这将使我们能够在更大的语料库上学习,并将我们的知识转移到 SST-2
任务上。同时进入KerasNLP,这将使我们能够轻松地预训练一个特别强大的模型 —— Transformer。
为了超越我们的基线,我们将利用 WikiText103
数据集,这是一个未标记的维基百科文章集合,比 SST-2
大得多。
我们将训练一个变换器,这是一个高度表达的模型,将学习将输入中的每个单词嵌入为低维向量。我们的维基百科数据集没有标签,因此我们将使用一种名为掩蔽语言建模(MaskedLM)的无监督训练目标。
本质上,我们将进行一个大型的“猜测缺失单词”的游戏。对于每个输入样本,我们将遮蔽 25% 的输入数据,并训练我们的模型预测我们遮盖的部分。
我们对 MaskedLM 任务的文本预处理将分为两个阶段。
为了进行标记化,我们可以使用 keras_nlp.tokenizers.Tokenizer
—— KerasNLP 的构建块,用于将文本转换为整数标记 ID 的序列。
特别是,我们将使用 keras_nlp.tokenizers.WordPieceTokenizer
,它执行子词标记化。子词标记化在训练大文本语料库的模型时非常流行。本质上,它使我们的模型能够从不常见的单词中学习,同时不需要每个单词都拥有庞大的词汇量。
我们需要做的第二件事是为 MaskedLM 任务遮蔽输入。为此,我们可以使用 keras_nlp.layers.MaskedLMMaskGenerator
,它将随机选择每个输入中的一组标记并将其遮蔽。
标记器和掩蔽层都可以在调用 tf.data.Dataset.map 中使用。我们可以使用 tf.data
在 CPU 上高效地预计算每个批次,同时我们的 GPU 或 TPU 在处理前一个批次的训练。因为我们的掩蔽层每次会选择新单词进行遮蔽,所以每个时期通过我们的数据集将为我们提供一组全新的训练标签。
# Setting sequence_length will trim or pad the token outputs to shape
# (batch_size, SEQ_LENGTH).
tokenizer = keras_nlp.tokenizers.WordPieceTokenizer(
vocabulary=vocab_file,
sequence_length=SEQ_LENGTH,
lowercase=True,
strip_accents=True,
)
# Setting mask_selection_length will trim or pad the mask outputs to shape
# (batch_size, PREDICTIONS_PER_SEQ).
masker = keras_nlp.layers.MaskedLMMaskGenerator(
vocabulary_size=tokenizer.vocabulary_size(),
mask_selection_rate=MASK_RATE,
mask_selection_length=PREDICTIONS_PER_SEQ,
mask_token_id=tokenizer.token_to_id("[MASK]"),
)
def preprocess(inputs):
inputs = tokenizer(inputs)
outputs = masker(inputs)
# Split the masking layer outputs into a (features, labels, and weights)
# tuple that we can use with keras.Model.fit().
features = {
"token_ids": outputs["token_ids"],
"mask_positions": outputs["mask_positions"],
}
labels = outputs["mask_ids"]
weights = outputs["mask_weights"]
return features, labels, weights
# We use prefetch() to pre-compute preprocessed batches on the fly on the CPU.
pretrain_ds = wiki_train_ds.map(
preprocess, num_parallel_calls=tf.data.AUTOTUNE
).prefetch(tf.data.AUTOTUNE)
pretrain_val_ds = wiki_val_ds.map(
preprocess, num_parallel_calls=tf.data.AUTOTUNE
).prefetch(tf.data.AUTOTUNE)
# Preview a single input example.
# The masks will change each time you run the cell.
print(pretrain_val_ds.take(1).get_single_element())
({'token_ids': <tf.Tensor: shape=(128, 128), dtype=int32, numpy=
array([[7570, 7849, 2271, ..., 9673, 103, 7570],
[7570, 7849, 103, ..., 1007, 1012, 2023],
[1996, 2034, 3940, ..., 0, 0, 0],
...,
[2076, 1996, 2307, ..., 0, 0, 0],
[3216, 103, 2083, ..., 0, 0, 0],
[ 103, 2007, 1045, ..., 0, 0, 0]], dtype=int32)>, 'mask_positions': <tf.Tensor: shape=(128, 32), dtype=int64, numpy=
array([[ 5, 6, 7, ..., 118, 120, 126],
[ 2, 3, 14, ..., 105, 106, 113],
[ 4, 9, 10, ..., 0, 0, 0],
...,
[ 4, 11, 19, ..., 117, 118, 0],
[ 1, 14, 17, ..., 0, 0, 0],
[ 0, 3, 6, ..., 0, 0, 0]])>}, <tf.Tensor: shape=(128, 32), dtype=int32, numpy=
array([[ 1010, 2124, 2004, ..., 2095, 11300, 1012],
[ 2271, 13091, 2303, ..., 2029, 2027, 1010],
[23976, 2007, 1037, ..., 0, 0, 0],
...,
[ 1010, 1996, 1010, ..., 1999, 7511, 0],
[ 2225, 1998, 10722, ..., 0, 0, 0],
[ 9794, 1030, 2322, ..., 0, 0, 0]], dtype=int32)>, <tf.Tensor: shape=(128, 32), dtype=float32, numpy=
array([[1., 1., 1., ..., 1., 1., 1.],
[1., 1., 1., ..., 1., 1., 1.],
[1., 1., 1., ..., 0., 0., 0.],
...,
[1., 1., 1., ..., 1., 1., 0.],
[1., 1., 1., ..., 0., 0., 0.],
[1., 1., 1., ..., 0., 0., 0.]], dtype=float32)>)
上述代码块将我们的数据集整理为一个(features, labels, weights)
元组,可以直接传递给keras.Model.fit()
。
我们有两个特征:
"token_ids"
,其中一些标记已被我们的掩码标记 id 替换。"mask_positions"
,用于跟踪我们掩盖了哪些标记。我们的标签就是我们掩盖的 id。
由于并非所有序列的掩码数量相同,我们还保留了一个sample_weight
张量,通过给予它们零权重从我们的损失函数中去除填充标签。
KerasNLP 提供了所有构建块,可以快速构建 Transformer 编码器。
我们使用keras_nlp.layers.TokenAndPositionEmbedding
首先嵌入我们的输入标记 id。该层同时学习两个嵌入 - 一个用于句子中的单词,另一个用于句子中的整数位置。输出嵌入仅是两者的和。
然后我们可以添加一系列keras_nlp.layers.TransformerEncoder
层。这些是 Transformer 模型的核心,利用注意力机制关注输入句子的不同部分,随后是一个多层感知器块。
该模型的输出将是每个输入标记 id 的编码向量。与我们用作基线的词袋模型不同,该模型将每个标记嵌入时考虑其出现的上下文。
inputs = keras.Input(shape=(SEQ_LENGTH,), dtype="int32")
# 用位置嵌入嵌入我们的标记。
embedding_layer = keras_nlp.layers.TokenAndPositionEmbedding(
vocabulary_size=tokenizer.vocabulary_size(),
sequence_length=SEQ_LENGTH,
embedding_dim=MODEL_DIM,
)
outputs = embedding_layer(inputs)
# 对嵌入应用层归一化和丢弃。
outputs = keras.layers.LayerNormalization(epsilon=NORM_EPSILON)(outputs)
outputs = keras.layers.Dropout(rate=DROPOUT)(outputs)
# 添加多个编码器块
for i in range(NUM_LAYERS):
outputs = keras_nlp.layers.TransformerEncoder(
intermediate_dim=INTERMEDIATE_DIM,
num_heads=NUM_HEADS,
dropout=DROPOUT,
layer_norm_epsilon=NORM_EPSILON,
)(outputs)
encoder_model = keras.Model(inputs, outputs)
encoder_model.summary()
模型: "functional_3"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓ ┃ 层 (类型) ┃ 输出形状 ┃ 参数数量 ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩ │ input_layer_1 (输入层) │ (None, 128) │ 0 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ token_and_position_embedding │ (None, 128, 256) │ 7,846,400 │ │ (TokenAndPositionEmbedding) │ │ │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ 层归一化 │ (None, 128, 256) │ 512 │ │ (LayerNormalization) │ │ │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ dropout (Dropout) │ (None, 128, 256) │ 0 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ transformer_encoder │ (None, 128, 256) │ 527,104 │ │ (TransformerEncoder) │ │ │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ transformer_encoder_1 │ (None, 128, 256) │ 527,104 │ │ (TransformerEncoder) │ │ │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ transformer_encoder_2 │ (None, 128, 256) │ 527,104 │ │ (TransformerEncoder) │ │ │ └─────────────────────────────────┴───────────────────────────┴────────────┘
总参数: 9,428,224 (287.73 MB)
可训练参数: 9,428,224 (287.73 MB)
非可训练参数: 0 (0.00 B)
你可以将 encoder_model
视为它自己的模块单元,它是我们对下游任务真正感兴趣的模型部分。然而,我们仍然需要设置编码器以在 MaskedLM 任务上进行训练;为此,我们附加一个 keras_nlp.layers.MaskedLMHead
。
该层将作为一个输入 token 编码,作为另一个输入我们在原始输入中遮蔽的位置。它将收集我们遮蔽的 token 编码,并将它们转换为对整个词汇表的预测。
这样,我们就准备好编译并运行预训练。如果你在 Colab 中运行此代码,请注意,这大约需要一个小时。训练变换器的计算著名。 较为复杂,因此即使这个相对较小的Transformer也需要一些时间。
# 通过附加一个掩码语言模型头创建预训练模型。
inputs = {
"token_ids": keras.Input(shape=(SEQ_LENGTH,), dtype="int32", name="token_ids"),
"mask_positions": keras.Input(
shape=(PREDICTIONS_PER_SEQ,), dtype="int32", name="mask_positions"
),
}
# 编码标记。
encoded_tokens = encoder_model(inputs["token_ids"])
# 为每个被掩码的输入标记预测一个输出单词。
# 我们使用输入标记嵌入从编码向量投影到词汇对数,这已被证明可以提高训练效率。
outputs = keras_nlp.layers.MaskedLMHead(
token_embedding=embedding_layer.token_embedding,
activation="softmax",
)(encoded_tokens, mask_positions=inputs["mask_positions"])
# 定义并编译我们的预训练模型。
pretraining_model = keras.Model(inputs, outputs)
pretraining_model.compile(
loss="sparse_categorical_crossentropy",
optimizer=keras.optimizers.AdamW(PRETRAINING_LEARNING_RATE),
weighted_metrics=["sparse_categorical_accuracy"],
jit_compile=True,
)
# 在我们的维基文本数据集上预训练模型。
pretraining_model.fit(
pretrain_ds,
validation_data=pretrain_val_ds,
epochs=PRETRAINING_EPOCHS,
)
# 保存这个基础模型以便进一步微调。
encoder_model.save("encoder_model.keras")
Epoch 1/8
5857/5857 ━━━━━━━━━━━━━━━━━━━━ 242s 41ms/step - loss: 5.4679 - sparse_categorical_accuracy: 0.1353 - val_loss: 3.4570 - val_sparse_categorical_accuracy: 0.3522
Epoch 2/8
5857/5857 ━━━━━━━━━━━━━━━━━━━━ 234s 40ms/step - loss: 3.6031 - sparse_categorical_accuracy: 0.3396 - val_loss: 3.0514 - val_sparse_categorical_accuracy: 0.4032
Epoch 3/8
5857/5857 ━━━━━━━━━━━━━━━━━━━━ 232s 40ms/step - loss: 3.2609 - sparse_categorical_accuracy: 0.3802 - val_loss: 2.8858 - val_sparse_categorical_accuracy: 0.4240
Epoch 4/8
5857/5857 ━━━━━━━━━━━━━━━━━━━━ 233s 40ms/step - loss: 3.1099 - sparse_categorical_accuracy: 0.3978 - val_loss: 2.7897 - val_sparse_categorical_accuracy: 0.4375
Epoch 5/8
5857/5857 ━━━━━━━━━━━━━━━━━━━━ 235s 40ms/step - loss: 3.0145 - sparse_categorical_accuracy: 0.4090 - val_loss: 2.7504 - val_sparse_categorical_accuracy: 0.4419
Epoch 6/8
5857/5857 ━━━━━━━━━━━━━━━━━━━━ 252s 43ms/step - loss: 2.9530 - sparse_categorical_accuracy: 0.4157 - val_loss: 2.6925 - val_sparse_categorical_accuracy: 0.4474
Epoch 7/8
5857/5857 ━━━━━━━━━━━━━━━━━━━━ 232s 40ms/step - loss: 2.9088 - sparse_categorical_accuracy: 0.4210 - val_loss: 2.6554 - val_sparse_categorical_accuracy: 0.4513
Epoch 8/8
5857/5857 ━━━━━━━━━━━━━━━━━━━━ 236s 40ms/step - loss: 2.8721 - sparse_categorical_accuracy: 0.4250 - val_loss: 2.6389 - val_sparse_categorical_accuracy: 0.4548
在预训练之后,我们可以在SST-2
数据集上微调我们的模型。我们可以利用我们构建的编码器在上下文中预测单词的能力来提升我们在下游任务上的表现。
微调的预处理比我们的预训练MaskeLM任务要简单得多。我们只需对输入句子进行标记化,便可开始训练!
def preprocess(sentences, labels):
return tokenizer(sentences), labels
# 我们使用prefetch()在CPU上动态预计算预处理批次。
finetune_ds = sst_train_ds.map(
preprocess, num_parallel_calls=tf.data.AUTOTUNE
).prefetch(tf.data.AUTOTUNE)
finetune_val_ds = sst_val_ds.map(
preprocess, num_parallel_calls=tf.data.AUTOTUNE
).prefetch(tf.data.AUTOTUNE)
# 预览一个输入示例。
print(finetune_val_ds.take(1).get_single_element())
(<tf.Tensor: shape=(32, 128), dtype=int32, numpy=
array([[ 2009, 1005, 1055, ..., 0, 0, 0],
[ 4895, 10258, 2378, ..., 0, 0, 0],
[ 4473, 2149, 2000, ..., 0, 0, 0],
...,
[ 1045, 2018, 2000, ..., 0, 0, 0],
[ 4283, 2000, 3660, ..., 0, 0, 0],
[ 1012, 1012, 1012, ..., 0, 0, 0]], dtype=int32)>, <tf.Tensor: shape=(32,), dtype=int32, numpy=
array([1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0,
0, 1, 1, 0, 0, 1, 0, 0, 1, 0], dtype=int32)>)
为了将我们的编码标记输出转换为分类预测,我们需要在我们的Transformer模型上附加另一个“头”。这里我们可以简单处理。我们将编码的标记聚合在一起,并使用一个单一的稠密层进行预测。
# Reload the encoder model from disk so we can restart fine-tuning from scratch.
encoder_model = keras.models.load_model("encoder_model.keras", compile=False)
# Take as input the tokenized input.
inputs = keras.Input(shape=(SEQ_LENGTH,), dtype="int32")
# Encode and pool the tokens.
encoded_tokens = encoder_model(inputs)
pooled_tokens = keras.layers.GlobalAveragePooling1D()(encoded_tokens[0])
# Predict an output label.
outputs = keras.layers.Dense(1, activation="sigmoid")(pooled_tokens)
# Define and compile our fine-tuning model.
finetuning_model = keras.Model(inputs, outputs)
finetuning_model.compile(
loss="binary_crossentropy",
optimizer=keras.optimizers.AdamW(FINETUNING_LEARNING_RATE),
metrics=["accuracy"],
)
# Finetune the model for the SST-2 task.
finetuning_model.fit(
finetune_ds,
validation_data=finetune_val_ds,
epochs=FINETUNING_EPOCHS,
)
Epoch 1/3
2105/2105 ━━━━━━━━━━━━━━━━━━━━ 21s 9ms/step - accuracy: 0.7500 - loss: 0.4891 - val_accuracy: 0.8036 - val_loss: 0.4099
Epoch 2/3
2105/2105 ━━━━━━━━━━━━━━━━━━━━ 16s 8ms/step - accuracy: 0.8826 - loss: 0.2779 - val_accuracy: 0.8482 - val_loss: 0.3964
Epoch 3/3
2105/2105 ━━━━━━━━━━━━━━━━━━━━ 16s 8ms/step - accuracy: 0.9176 - loss: 0.2066 - val_accuracy: 0.8549 - val_loss: 0.4142
<keras.src.callbacks.history.History at 0x7f12d85c21a0>
预训练足以将我们的性能提升到84%,而且这几乎不是Transformer模型的上限。你可能在预训练期间注意到我们的验证性能仍在稳步上升。我们的模型仍然显著欠训练。继续进行更多的训练轮次,训练一个大型Transformer,以及在更多无标记文本上训练,都将显著提升性能。
KerasNLP的一个关键目标是提供一个模块化的方法来构建NLP模型。我们在这里展示了一种构建Transformer的方法,但KerasNLP支持日益增长的文本预处理和模型构建组件。我们希望这能让您更容易地尝试解决自然语言问题的方案。