作者: Darshan Deshpande
创建日期: 2021/10/29
最后修改: 2024/05/08
描述: 通过评论分类演示主动学习的优势。
随着以数据为中心的机器学习的增长,主动学习在企业和研究人员中变得越来越流行。主动学习旨在逐步训练机器学习模型,使得最终模型所需的训练数据量较少,从而获得竞争力的评分。
主动学习管道的结构包括一个分类器和一个 oracle。oracle 是一个注释者,负责清理、选择、标记数据,并在需要时将其输入模型。oracle 是经过培训的个人或一组个人,确保新数据的标记一致性。
该过程从注释整个数据集中一个小子集开始,并训练一个初始模型。保存最佳模型检查点,然后在平衡测试集上进行测试。测试集必须经过仔细采样,因为整个训练过程将依赖于它。一旦我们得到初步评估分数,oracle 就会负责标记更多样本;样本数据点的数量通常由业务需求决定。此后,将新采样的数据添加到训练集中,训练过程重复进行。这个循环会一直持续,直到达到可接受的分数或满足其他业务指标。
本教程提供了主动学习原理的基本演示,通过展示一种基于比率(最小置信度)的采样策略,该策略与在整个数据集上训练的模型相比,导致整体假阳性和假阴性率更低。这种采样属于 不确定性采样 领域,其中新的数据集是根据模型对相应标签输出的不确定性进行采样的。在我们的例子中,我们对模型的假阳性和假阴性率进行比较,并基于这些比率注释新数据。
其他一些采样技术包括:
import os
os.environ["KERAS_BACKEND"] = "tensorflow" # @param ["tensorflow", "jax", "torch"]
import keras
from keras import ops
from keras import layers
import tensorflow_datasets as tfds
import tensorflow as tf
import matplotlib.pyplot as plt
import re
import string
tfds.disable_progress_bar()
我们将使用 IMDB 评论数据集进行实验。此数据集共有 50,000 条评论,包括训练和测试拆分。我们将合并这些拆分并抽样我们自己的平衡训练、验证和测试集。
dataset = tfds.load(
"imdb_reviews",
split="train + test",
as_supervised=True,
batch_size=-1,
shuffle_files=False,
)
reviews, labels = tfds.as_numpy(dataset)
print("总例数:", reviews.shape[0])
总例数: 50000
主动学习从标记一部分数据开始。对于我们将使用的比率采样技术,我们需要良好平衡的训练、验证和测试拆分。 val_split = 2500 test_split = 2500 train_split = 7500
x_positives, y_positives = reviews[labels == 1], labels[labels == 1] x_negatives, y_negatives = reviews[labels == 0], labels[labels == 0]
x_val, y_val = ( tf.concat((x_positives[:val_split], x_negatives[:val_split]), 0), tf.concat((y_positives[:val_split], y_negatives[:val_split]), 0), ) x_test, y_test = ( tf.concat( ( x_positives[val_split : val_split + test_split], x_negatives[val_split : val_split + test_split], ), 0, ), tf.concat( ( y_positives[val_split : val_split + test_split], y_negatives[val_split : val_split + test_split], ), 0, ), ) x_train, y_train = ( tf.concat( ( x_positives[val_split + test_split : val_split + test_split + train_split], x_negatives[val_split + test_split : val_split + test_split + train_split], ), 0, ), tf.concat( ( y_positives[val_split + test_split : val_split + test_split + train_split], y_negatives[val_split + test_split : val_split + test_split + train_split], ), 0, ), )
x_pool_positives, y_pool_positives = ( x_positives[val_split + test_split + train_split :], y_positives[val_split + test_split + train_split :], ) x_pool_negatives, y_pool_negatives = ( x_negatives[val_split + test_split + train_split :], y_negatives[val_split + test_split + train_split :], )
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train)) val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val)) test_dataset = tf.data.Dataset.from_tensor_slices((x_test, y_test))
pool_negatives = tf.data.Dataset.from_tensor_slices( (x_pool_negatives, y_pool_negatives) ) pool_positives = tf.data.Dataset.from_tensor_slices( (x_pool_positives, y_pool_positives) )
print(f"初始训练集大小: {len(train_dataset)}") print(f"验证集大小: {len(val_dataset)}") print(f"测试集大小: {len(test_dataset)}") print(f"未标记的负样本池: {len(pool_negatives)}") print(f"未标记的正样本池: {len(pool_positives)}")
<div class="k-default-codeblock">
初始训练集大小:15000 验证集大小:5000 测试集大小:5000 未标记的负样本池:12500 未标记的正样本池:12500
</div>
### 拟合 `TextVectorization` 层
由于我们正在处理文本数据,我们需要将文本字符串编码为向量,然后通过 `Embedding` 层传递。为了加快这个标记化过程,我们使用 `map()` 函数及其并行化功能。
```python
vectorizer = layers.TextVectorization(
3000, standardize="lower_and_strip_punctuation", output_sequence_length=150
)
# 调整数据集
vectorizer.adapt(
train_dataset.map(lambda x, y: x, num_parallel_calls=tf.data.AUTOTUNE).batch(256)
)
def vectorize_text(text, label):
text = vectorizer(text)
return text, label
train_dataset = train_dataset.map(
vectorize_text, num_parallel_calls=tf.data.AUTOTUNE
).prefetch(tf.data.AUTOTUNE)
pool_negatives = pool_negatives.map(vectorize_text, num_parallel_calls=tf.data.AUTOTUNE)
pool_positives = pool_positives.map(vectorize_text, num_parallel_calls=tf.data.AUTOTUNE)
val_dataset = val_dataset.batch(256).map(
vectorize_text, num_parallel_calls=tf.data.AUTOTUNE
)
test_dataset = test_dataset.batch(256).map(
vectorize_text, num_parallel_calls=tf.data.AUTOTUNE
)
# 合并新的历史对象与旧的对象的辅助函数
def append_history(losses, val_losses, accuracy, val_accuracy, history):
losses = losses + history.history["loss"]
val_losses = val_losses + history.history["val_loss"]
accuracy = accuracy + history.history["binary_accuracy"]
val_accuracy = val_accuracy + history.history["val_binary_accuracy"]
return losses, val_losses, accuracy, val_accuracy
# 绘图函数
def plot_history(losses, val_losses, accuracies, val_accuracies):
plt.plot(losses)
plt.plot(val_losses)
plt.legend(["train_loss", "val_loss"])
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.show()
plt.plot(accuracies)
plt.plot(val_accuracies)
plt.legend(["train_accuracy", "val_accuracy"])
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.show()
我们创建一个小的双向LSTM模型。当使用主动学习时,您应确保模型架构能够对初始数据进行过拟合。 过拟合强烈暗示模型将具有足够的能力处理未来未见的数据。
def create_model():
model = keras.models.Sequential(
[
layers.Input(shape=(150,)),
layers.Embedding(input_dim=3000, output_dim=128),
layers.Bidirectional(layers.LSTM(32, return_sequences=True)),
layers.GlobalMaxPool1D(),
layers.Dense(20, activation="relu"),
layers.Dropout(0.5),
layers.Dense(1, activation="sigmoid"),
]
)
model.summary()
return model
为了展示主动学习的有效性,我们将首先在包含40,000个标记样本的整个数据集上训练模型。该模型将在后续作为比较使用。
def train_full_model(full_train_dataset, val_dataset, test_dataset):
model = create_model()
model.compile(
loss="binary_crossentropy",
optimizer="rmsprop",
metrics=[
keras.metrics.BinaryAccuracy(),
keras.metrics.FalseNegatives(),
keras.metrics.FalsePositives(),
],
)
# 我们将在每个epoch保存最佳模型,并在测试集上评估时加载最佳模型
history = model.fit(
full_train_dataset.batch(256),
epochs=20,
validation_data=val_dataset,
callbacks=[
keras.callbacks.EarlyStopping(patience=4, verbose=1),
keras.callbacks.ModelCheckpoint(
"FullModelCheckpoint.keras", verbose=1, save_best_only=True
),
],
)
# 绘制历史
plot_history(
history.history["loss"],
history.history["val_loss"],
history.history["binary_accuracy"],
history.history["val_binary_accuracy"],
)
# 加载最佳检查点
model = keras.models.load_model("FullModelCheckpoint.keras")
print("-" * 100)
print(
"测试集评估: ",
model.evaluate(test_dataset, verbose=0, return_dict=True),
)
print("-" * 100)
return model
# 抽样完整训练数据集进行训练
full_train_dataset = (
train_dataset.concatenate(pool_positives)
.concatenate(pool_negatives)
.cache()
.shuffle(20000)
)
# 训练完整模型
full_dataset_model = train_full_model(full_train_dataset, val_dataset, test_dataset)
模型: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ 层 (类型) ┃ 输出形状 ┃ 参数 # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ embedding (嵌入) │ (无, 150, 128) │ 384,000 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ bidirectional (双向) │ (无, 150, 64) │ 41,216 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ global_max_pooling1d │ (无, 64) │ 0 │ │ (全局最大池化1D) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense (密集) │ (无, 20) │ 1,300 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout (丢弃) │ (无, 20) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (密集) │ (无, 1) │ 21 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
总参数: 426,537 (1.63 MB)
可训练参数: 426,537 (1.63 MB)
非可训练参数: 0 (0.00 B)
轮次 1/20
156/157 ━━━━━━━━━━━━━━━━━━━━ 0s 73ms/步 - 二进制准确率: 0.6412 - 假阴性: 2084.3333 - 假阳性: 5252.1924 - 损失: 0.6507
轮次 1: val_loss 从 inf 改进到 0.57198, 保存模型到 FullModelCheckpoint.keras
157/157 ━━━━━━━━━━━━━━━━━━━━ 15s 79ms/步 - 二进制准确率: 0.6411 - 假阴性: 2135.1772 - 假阳性: 5292.4053 - 损失: 0.6506 - val_binary_accuracy: 0.7356 - val_false_negatives: 898.0000 - val_false_positives: 424.0000 - val_loss: 0.5720
轮次 2/20
第二轮:val_loss 从 0.57198 改进到 0.41756,正在保存模型到 FullModelCheckpoint.keras
第3轮/20
第三轮:val_loss 从 0.41756 改进到 0.38233,正在保存模型到 FullModelCheckpoint.keras
第4轮/20
第四轮:val_loss 从 0.38233 改进到 0.36235,正在保存模型到 FullModelCheckpoint.keras
第5轮/20
第五轮:val_loss 未从 0.36235 改进
第6轮/20
第6轮:val_loss 从 0.36235 改进到 0.35041,正在保存模型到 FullModelCheckpoint.keras
第7轮/20
第7轮:val_loss 从 0.35041 改进到 0.32680,正在保存模型到 FullModelCheckpoint.keras
第8轮/20
第8轮:val_loss 未从 0.32680 改进
第9轮/20
第9轮:val_loss 未从 0.32680 改进
第10轮/20
轮次 10: val_loss 没有从 0.32680 改善
第 11 轮/20
第 11 轮: val_loss 没有从 0.32680 改进
第 11 轮: 提前停止
----------------------------------------------------------------------------------------------------
测试集评估: {'binary_accuracy': 0.8507999777793884, 'false_negatives': 397.0, 'false_positives': 349.0, 'loss': 0.3372706174850464}
----------------------------------------------------------------------------------------------------
def train_active_learning_models(
train_dataset,
pool_negatives,
pool_positives,
val_dataset,
test_dataset,
num_iterations=3,
sampling_size=5000,
):
# 创建用于存储指标的列表
losses, val_losses, accuracies, val_accuracies = [], [], [], []
model = create_model()
# 我们将监控模型预测的假阳性和假阴性
# 这些将决定每个主动学习循环后续的采样比率
model.compile(
loss="binary_crossentropy",
optimizer="rmsprop",
metrics=[
keras.metrics.BinaryAccuracy(),
keras.metrics.FalseNegatives(),
keras.metrics.FalsePositives(),
],
)
# 定义检查点。
# 检查点回调在整个训练过程中重用,因为它只保存最佳的整体模型。
checkpoint = keras.callbacks.ModelCheckpoint(
"AL_Model.keras", save_best_only=True, verbose=1
)
# 这里,耐心值设置为4。如果需要,可以设置得更高。
early_stopping = keras.callbacks.EarlyStopping(patience=4, verbose=1)
print(f"开始训练 {len(train_dataset)} 个样本")
# 在小部分训练集上进行初始拟合
history = model.fit(
train_dataset.cache().shuffle(20000).batch(256),
epochs=20,
validation_data=val_dataset,
callbacks=[checkpoint, early_stopping],
)
# 追加历史记录
losses, val_losses, accuracies, val_accuracies = append_history(
losses, val_losses, accuracies, val_accuracies, history
)
for iteration in range(num_iterations):
# 从之前训练的模型获取预测
predictions = model.predict(test_dataset)
# 从输出概率生成标签
rounded = ops.where(ops.greater(predictions, 0.5), 1, 0)
# 评估错误分类的零和一的数量
_, _, false_negatives, false_positives = model.evaluate(test_dataset, verbose=0)
print("-" * 100)
print(
f"错误分类的零的数量: {false_negatives}, 错误分类的一的数量: {false_positives}"
)
# 这种主动学习技术演示了基于比例的采样,其中
# 采样的正样本/负样本数量 = 错误分类的正样本/负样本数量 / 总的错误分类数量
if false_negatives != 0 and false_positives != 0:
total = false_negatives + false_positives
sample_ratio_ones, sample_ratio_zeros = (
false_positives / total,
false_negatives / total,
)
# 如果所有样本都被正确预测,我们可以均匀采样两个类别
else:
sample_ratio_ones, sample_ratio_zeros = 0.5, 0.5
print(
f"正样本的采样比率: {sample_ratio_ones}, 负样本的采样比率: {sample_ratio_zeros}"
)
# 采样所需数量的正样本和负样本
sampled_dataset = pool_negatives.take(
int(sample_ratio_zeros * sampling_size)
).concatenate(pool_positives.take(int(sample_ratio_ones * sampling_size)))
# 跳过采样的数据点以避免样本重复
pool_negatives = pool_negatives.skip(int(sample_ratio_zeros * sampling_size))
pool_positives = pool_positives.skip(int(sample_ratio_ones * sampling_size))
# 将 train_dataset 与 sampled_dataset 进行连接
train_dataset = train_dataset.concatenate(sampled_dataset).prefetch(
tf.data.AUTOTUNE
)
print(f"开始训练 {len(train_dataset)} 个样本")
print("-" * 100)
# 我们重新编译模型以重置优化器状态并重新训练模型
model.compile(
loss="binary_crossentropy",
optimizer="rmsprop",
metrics=[
keras.metrics.BinaryAccuracy(),
keras.metrics.FalseNegatives(),
keras.metrics.FalsePositives(),
],
)
history = model.fit(
train_dataset.cache().shuffle(20000).batch(256),
validation_data=val_dataset,
epochs=20,
callbacks=[
checkpoint,
keras.callbacks.EarlyStopping(patience=4, verbose=1),
],
)
# 追加历史记录
losses, val_losses, accuracies, val_accuracies = append_history(
losses, val_losses, accuracies, val_accuracies, history
)
# 从这个训练循环中加载最佳模型
model = keras.models.load_model("AL_Model.keras")
# 绘制整体历史记录并评估最终模型
plot_history(losses, val_losses, accuracies, val_accuracies)
print("-" * 100)
print(
"测试集评估: ",
model.evaluate(test_dataset, verbose=0, return_dict=True),
)
print("-" * 100)
return model
active_learning_model = train_active_learning_models(
train_dataset, pool_negatives, pool_positives, val_dataset, test_dataset
)
模型: "sequential_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ 层 (类型) ┃ 输出形状 ┃ 参数 # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ embedding_1 (嵌入层) │ (无, 150, 128) │ 384,000 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ bidirectional_1 (双向层) │ (无, 150, 64) │ 41,216 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ global_max_pooling1d_1 │ (无, 64) │ 0 │ │ (全局最大池化 1D) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_2 (全连接层) │ (无, 20) │ 1,300 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout_1 (丢弃层) │ (无, 20) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_3 (全连接层) │ (无, 1) │ 21 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
总参数: 426,537 (1.63 MB)
可训练参数: 426,537 (1.63 MB)
非可训练参数: 0 (0.00 B)
开始训练,样本数为15000
第 1 种/20 种
第 1 轮: val_loss 从 inf 改进到 0.67428,正在将模型保存到 AL_Model.keras
第 2 轮/20
第 2 轮: 验证损失从 0.67428 改进到 0.59133,保存模型至 AL_Model.keras
第 3 轮/20
第 3 轮: 验证损失从 0.59133 改进到 0.51602,保存模型至 AL_Model.keras
第 4 轮/20
第 4 轮: 验证损失从 0.51602 改进到 0.43948,保存模型至 AL_Model.keras
第 5 轮/20
第 5 轮: 验证损失从 0.43948 改进到 0.41679,保存模型至 AL_Model.keras
第 6 轮/20
第 6 轮: 验证损失从 0.41679 改进到 0.39680,保存模型至 AL_Model.keras
第 7 轮/20
第 7 轮: 验证损失未改进,维持在 0.39680
第 8 轮/20
第 8 轮: 验证损失未改进,维持在 0.39680
第 9 轮/20
第 9 轮: 验证损失从 0.39680 改进到 0.37727,保存模型至 AL_Model.keras
第 10 轮/20
第10轮: val_loss 从 0.37727 改进为 0.37354,模型保存至 AL_Model.keras
第11/20轮
第11轮: val_loss 从 0.37354 改进为 0.37074,模型保存至 AL_Model.keras
第12/20轮
第12轮: val_loss 未从 0.37074 改进
第13/20轮
第13轮: val_loss 未从 0.37074 改进
第14/20轮
第14轮: val_loss 未从 0.37074 改进
第15/20轮
第15轮: val_loss 未从 0.37074 改进
第15轮: 提前停止
----------------------------------------------------------------------------------------------------
错误分类为零的数量: 290.0,错误分类为一的数量: 538.0
正样本的样本比例: 0.6497584541062802,负样本的样本比例:0.3502415458937198
开始训练,样本数量为 19999
----------------------------------------------------------------------------------------------------
第1/20轮
第1轮: val_loss 未从 0.37074 改进
第2/20轮
第2轮: val_loss 未从 0.37074 改进
第 3 轮/20
第 3 轮: val_loss 没有从 0.37074 改进
第 4 轮/20
第 4 轮: val_loss 没有从 0.37074 改进
第 5 轮/20
第 5 轮: val_loss 没有从 0.37074 改进
第 6 轮/20
第 6 轮: val_loss 没有从 0.37074 改进
第 7 轮/20
第 7 轮: val_loss 没有从 0.37074 改进
第 7 轮: 提前停止
----------------------------------------------------------------------------------------------------
错误分类的零数量: 376.0, 错误分类的一数量: 442.0
正样本的样本比例: 0.5403422982885085, 负样本的样本比例: 0.45965770171149145
开始使用 24998 个样本进行训练
----------------------------------------------------------------------------------------------------
第 1 轮/20
第 1 轮: val_loss 从 0.37074 改进到 0.36196,正在保存模型到 AL_Model.keras
第 2 轮/20
第 2 轮: val_loss 没有从 0.36196 改进
第 3 轮/20
第 3 轮: val_loss 没有从 0.36196 改进
第 4 轮/20
第 4 轮: val_loss 未能改善,仍为 0.36196
第 5 轮/20
第 5 轮: val_loss 未能改善,仍为 0.36196
第 5 轮: 提前停止
----------------------------------------------------------------------------------------------------
错误分类的零的数量: 407.0,错误分类的一的数量: 410.0
正样本比例: 0.5018359853121175,负样本比例:0.4981640146878825
开始训练,共 29997 个样本
----------------------------------------------------------------------------------------------------
第 1 轮/20
第 1 轮: val_loss 未能改善,仍为 0.36196
第 2 轮/20
第 2 轮: val_loss 从 0.36196 改善到 0.35707,已保存模型到 AL_Model.keras
第 3 轮/20
第 3 轮: val_loss 未能改善,仍为 0.35707
第 4 轮/20
第 4 轮: val_loss 未能改善,仍为 0.35707
第 5 轮/20
第 5 轮: val_loss 未能改善,仍为 0.35707
第 6 轮/20
第6轮: val_loss 没有改善,仍为 0.35707
第6轮: 提前停止
----------------------------------------------------------------------------------------------------
测试集评估: {'binary_accuracy': 0.8424000144004822, 'false_negatives_4': 491.0, 'false_positives_4': 297.0, 'loss': 0.3661557137966156}
----------------------------------------------------------------------------------------------------