写在前面
过拟合与欠拟合
欠拟合: 是指在模型学习能力较弱,而数据复杂度较高的情况下,模型无法学习到数据集中的“一般规律”,因而导致泛化能力弱。此时,算法在训练集上表现一般,但在测试集上表现较差,泛化性能不佳。
过拟合: 是指模型在训练数据上表现很好,但在测试数据上表现不佳。这是由于模型过于复杂,记住了训练数据中的噪声和模式,而没有学到一般规则。本文将探讨过拟合问题以及其解决方法。
过拟合的解决方法
方法 | 描述 |
---|---|
增加训练数据 | |
(More data) | 通过增加训练数据量,简单粗暴最有效,可以减少过拟合现象。 |
正则化 | |
(regularization) | 在损失函数中添加一项,以惩罚模型的复杂度。常用的正则化方法包括L1正则化、L2正则化和dropout。 |
早停法 | |
(Early stopping) | 在训练过程中,每次迭代后都会评估模型在验证集上的性能。如果性能在连续若干次迭代中没有提高,就停止训练。 |
数据增强 | |
(Data augmentation) | 通过改变原有数据减少过拟合。例如,可以通过旋转、缩放等方式对图像数据进行增强。 |
写在中间
可以看到随着网络层数增加,模型变得复杂,过拟合现象变得愈发严重。接下来,我们将介绍一系列方法来帮助检测并抑制过拟合现象。
1. 交叉验证
增加数据集是最有效的方法,但是代价往往是昂贵的,所以要充分利用好现有的数据集。前面我们介绍了数据集需要划分为训练集和测试集,但我们为了挑选模型超参数和检测过拟合线性,一般需要将原来的训练集再次切分为新的训练集和验证集(validation set)。最终数据集被切分为 训练集、验证集、测试集。这三部分数据集的功能如下:
类别 | 描述 |
---|---|
训练集 | 用于训练模型的参数,通过学习训练数据集来进行模型训练 |
验证集 | 用于评估训练过程中的模型表现,调整模型的超参数 |
测试集 | 用于评估最终训练好的模型在真实数据上的表现,测试验证模型的性能,评估模型的预测能力和泛化能力 |
验证集和测试集的区分
验证集使命:根据验证集的表现来调整模型的各种超参数的设置,提升模型的泛化能力。
测试集使命:就是检验模型的能力,其表现不能用来反馈模型的调整(就如你不能拿着期末考试原题来练习,否则期末高分就不能体现出你平时学习的真实状况),我们的办法就是从平常的练习题中抽取几道题组成验证集来检验你的能力。
这是一个将mnist手写数字识别测试集切分的例子,将6万张图像的前5万张划分为训练集,后1万张划分为验证集
(x, y), (x_test, y_test) = datasets.mnist.load_data()
# 60k训练集切分为 50k训练集和 10k验证集
# (x_train, y_train), (x_val, y_val)
x_train, x_val = tf.split(x, num_or_size_splits=[50000, 10000])
y_train, y_val = tf.split(y, num_or_size_splits=[50000, 10000])
# 训练集
train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_db = train_db.map(preprocess).shuffle(10000).batch(128)
# 验证集
val_db = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_db = val_db.map(preprocess).shuffle(10000).batch(128)
# 测试集
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.map(preprocess).shuffle(10000).batch(128)
但是这样切分还是有局限性的,训练集只能在前5万张图像中出现,验证集只能在后1万张图像中出现,是否有方法让验证集能使用前5万张的图像呢?还真有,这就是标题所提及的**交叉验证:**我们将训练集的6万张图像划成n份,每训练取其中的 n - 1 份作为训练集,取 1 份作为验证集,而非固定前5万张为训练集,后 1 万张为验证集。
# 创建一个范围为60000的索引数组
idx = tf.range(60000)
# 随机打乱索引数组
idx = tf.random.shuffle(idx)
# 使用索引数组从x和y中获取训练集的样本
# 训练集的样本数量为50000
x_train, y_train = tf.gather(x, idx[:50000]), tf.gather(y, idx[:50000])
# 使用索引数组从x和y中获取验证集的样本
# 验证集的样本数量为10000
x_val, y_val = tf.gather(x, idx[-10000:]), tf.gather(y, idx[-10000:])
也可以不用手动划分,在训练函数中增加参数,即可自动化操作
# 使用60k训练集对网络进行训练,设置训练周期数为6
# 设置验证集占比为0.1,即数据集中10%的数据将作为验证集
# 设置每个2个训练周期进行一次验证
network.fit(train_db, epochs=6, validation_split=0.1, validation_freq=2)
2. 正则化
之所以出现过拟合的现象,是因为模型太过复杂,这里通过限制网络参数的稀疏性来约束网络的实际容量,这种约束一般通过在损失函数上添加额外的参数稀疏性惩罚项实现,常用的正则化的方式有L0、L1、L2正则化,dropout正则化。
简单介绍
关于正则化的数学原理,本人也搞不明白,也就不滥竽充数了。
有关实现简单概括就是在损失函数中引入模型权重参数的L范数,使学习到的权重参数稀疏化。
正则化的方式可以手动实现,也可以调用API实现:其中手动实现主要在计算loss值的后面,调用API主要在创建层的时候。
import tensorflow as tf
from tensorflow.keras import layers, regularizers
# 网络构建
# 这会在模型损失函数中加入权重参数的L2范数作为惩罚项,力度由0.001控制。
network = Sequential([layers.Dense(256, kernel_regularizer=regularizers.l2(0.001), activation='relu'),
layers.Dense(128, kernel_regularizer=regularizers.l2(0.001), activation='relu'),
layers.Dense(64, kernel_regularizer=regularizers.l2(0.001), activation='relu'),
layers.Dense(32, kernel_regularizer=regularizers.l2(0.001), activation='relu'),
layers.Dense(10)])
# 参数构建
network.build(input_shape=(None, 28*28))
# 模型展示
network.summary()
# 截取手动前向计算的代码
for step, (x, y) in enumerate(train_db):
# 创建一个 GradientTape,用于记录计算过程
with tf.GradientTape() as tape:
x = tf.reshape(x, (-1, 28*28)) # [b, 28, 28] => [b, 784]
out = network(x) # [b, 784] => [b, 10]
y_onehot = tf.one_hot(y, depth=10) # [b] => [b, 10]
# 使用交叉熵损失函数计算 loss
loss = tf.reduce_mean(tf.losses.categorical_crossentropy(y_onehot, out, from_logits=True))
loss_regularization = []
for p in network.trainable_variables: # 重点在这里,遍历网络中所有的可训练参数(network.trainable_variables)。
loss_regularization.append(tf.nn.l2_loss(p)) # # 对每个参数计算L2正则化项(tf.nn.l2_loss(p)),这会返回一个标量。
loss_regularization = tf.reduce_sum(tf.stack(loss_regularization)) # # 将所有参数的L2正则化项求和,得到正则化损失loss_regularization。
# 将损失函数定义为交叉熵损失和 L2 正则化损失的和
loss = loss + 0.0001 * loss_regularization
# 使用 tape 计算损失函数关于网络参数的梯度,并应用优化器进行反向传播更新参数
grads = tape.gradient(loss, network.trainable_variables)
optimizer.apply_gradients(zip(grads, network.trainable_variables))
正则化效果
Dropout
通过随机断开神经网络的连接,减少每次训练时实际参与计算的模型的参数量,但是在测试时,Dropout会恢复所有的连接,保证模型测试时获得最好的性能。
我们以层方式来实现以上功能
import tensorflow as tf
from tensorflow.keras import layers, regularizers
# 网络构建
network = Sequential([layers.Dense(256, activation='relu'),
layers.Dropout(0.5), # 有0.5的概率断开与下一层神经元的连接
layers.Dense(128, activation='relu'),
layers.Dropout(0.5),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(10)])
# 参数构建
network.build(input_shape=(None, 28*28))
# 模型展示
network.summary()
for step, (x, y) in enumerate(train_db):
# 训练时
with tf.GradientTape() as tape:
out = network(x, training=True)
# 测试时
out = network(x, training=False)
3. Early stopping
早停法
那么如何选择合适的 Epoch 就提前停止训练(Early Stopping),避免出现过拟合现象呢?我们可以通过观察验证指标的变化,来预测最适合的 Epoch 可能的位置。具体地,对于分类问题,我们可以记录模型的验证准确率,并监控验证准确率的变化,当发现验证准确率连续𝑛个 Epoch 没有下降时,可以预测可能已经达到了最适合的 Epoch 附近,从而提前终止训练。
from tensorflow.keras.callbacks import EarlyStopping
# 数据集读取···
# 定义早停法回调函数
early_stopping = EarlyStopping(monitor='val_loss', # 监视验证集loss
patience=3, # 当验证集loss在3个epoch内都没有改善则停止训练
mode='min', # 监测loss时一般设置为min,监测准确值时一般设置为max
verbose=1, # 检测值改善时打印一条信息
restore_best_weights=True # 将权重恢复到最好的一个epoch
)
# 网络构建
# 参数构建
# 模型装配
# 模型训练,添加参数
network.fit(train_db, epochs=100,
validation_data=val_db, validation_steps=10,
callbacks=[early_stopping])
这里我们会对手写数字识别的代码再次进行修改,来使用上面提及的方法,你可以通过修改repeat
的方式来复制数据集使训练数据增多,更改epochs
的方式来增加训练次数,经过测试,这段代码在10 epochs 之后便达到了过拟合
import tensorflow as tf
from tensorflow.keras import datasets, layers, optimizers, Sequential, metrics, regularizers
from tensorflow.keras.callbacks import EarlyStopping
# 处理每一张图像
def preprocess(x, y):
x = tf.cast(x, dtype=tf.float32) / 255.
x = tf.reshape(x, [28 * 28])
y = tf.cast(y, dtype=tf.int32)
y = tf.one_hot(y, depth=10)
return x, y
# 数据集读取
(x, y), (x_test, y_test) = datasets.mnist.load_data()
# # 固定切分
# # 60k训练集切分为 50k训练集和 10k验证集
# # (x_train, y_train), (x_val, y_val)
# x_train, x_val = tf.split(x, num_or_size_splits=[50000, 10000])
# y_train, y_val = tf.split(y, num_or_size_splits=[50000, 10000])
# 交叉验证切分
idx = tf.range(60000)
idx = tf.random.shuffle(idx)
x_train, y_train = tf.gather(x, idx[:50000]), tf.gather(y, idx[:50000])
x_val, y_val = tf.gather(x, idx[-10000:]), tf.gather(y, idx[-10000:])
# 训练集
train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_db = train_db.map(preprocess).shuffle(10000).batch(128).repeat(10)
# 验证集
val_db = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_db = val_db.map(preprocess).shuffle(10000).batch(128)
# 测试集
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.map(preprocess).shuffle(10000).batch(128)
# 定义早停法回调函数
early_stopping = EarlyStopping(monitor='val_loss', # 监视验证集loss
patience=3, # 当验证集loss在2个epoch内都没有改善则停止训练
mode='min', # 监测loss时一般设置为min,监测准确值时一般设置为max
verbose=1, # 检测值改善时打印一条信息
restore_best_weights=True # 将权重恢复到最好的一个epoch
)
# 网络构建
# 正则化:在模型损失函数中加入权重参数的L2范数作为惩罚项,力度由0.001控制。
# Dropout:添加dropout层来随机断开连接
network = Sequential([layers.Dense(256, kernel_regularizer=regularizers.l2(0.001), activation='relu'),
layers.Dropout(0.5),
layers.Dense(128, kernel_regularizer=regularizers.l2(0.001), activation='relu'),
layers.Dropout(0.5),
layers.Dense(64, kernel_regularizer=regularizers.l2(0.001), activation='relu'),
layers.Dense(32, kernel_regularizer=regularizers.l2(0.001), activation='relu'),
layers.Dense(10)])
# 参数构建
network.build(input_shape=(None, 28*28))
# 模型展示
network.summary()
# 模型装配
network.compile(optimizer=optimizers.Adam(learning_rate=0.01),
loss=tf.losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
# 模型训练
network.fit(train_db, epochs=50,
validation_data=val_db, validation_steps=10,
callbacks=[early_stopping])
# 模型评估
print('模型评估:')
network.evaluate(test_db)
写在最后
👍🏻点赞,你的认可是我创作的动力!
⭐收藏,你的青睐是我努力的方向!
✏️评论,你的意见是我进步的财富!