一、欠拟合和过拟合简介
1、什么是欠拟合?
数据很复杂,但是模型很简单,这就是欠拟合。下面我们通过playground.tensorflow.org/来直观感受下欠拟合。
如上图所示,我们选择了一个非线性分布的数据,然后只用一层输出层,并且选择激活函数为线性,可以看到,训练损失一直在0.5以上,损失非常高,这就是欠拟合。欠拟合的表现就是训练损失降不下去。
2、什么是过拟合?
过拟合就是我们的模型太过强大了,拟合过头了,导致遇到新的数据的时候,反而表现得不是很好。比如下面这张图,红色的点和蓝色的点分别代表两类数据,这是一个二分类数据集,黑色的线代表一个比较正常的模型,而绿色的线就代表一个过拟合的模型。
如下图所示是过拟合的损失变化曲线。其中,红色的线代表验证集的损失,验证集的损失在越过某个点之后,变得越来越高了;蓝色的线代表训练集的损失,训练集的损失是越来越小的。导致这种情况的原因是,训练集数据太过简单,而模型却太过复杂。有可能是因为训练集的数据过少。
下面我们一起去playground.tensorflow.org/中直观地感受下过拟合:
如上图所示,我们选择了一个很简单的数据集,并且让训练集数据比较少,同时添加一些噪音,让训练集数据不能很好地代表绝大多数数据的情况。然后再搞一个非常复杂的模型去训练它。从图中右上角可以看到,训练集的损失越来越小,而验证集的损失在越过某个点之后反而越来越大。
操作步骤:
1)加载带有噪音的二分类数据集(训练集和验证集);
2)使用不同的神经网络来演示欠拟合和过拟合;
3)过拟合应对法:早停法、权重衰减、丢弃法
二、加载带有噪音的二分类数据集
为什么要带点噪音呢?因为带点噪音的数据集更容易在训练时出现过拟合的情况,因为数据集里的数据不能很好地代表大多数。
首先我们使用预先准备好的脚本加载带有噪音的二分类数据集。其次,我们将数据集可视化。代码如下:
src\overfit\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
src\overfit\index.js
import * as tfvis from '@tensorflow/tfjs-vis';
import {getData} from './data';
window.onload = () => {
const data = getData(200, 3);
console.log(data);
tfvis.render.scatterplot(
{ name: '训练数据' },
{
values: [
data.filter(p => p.label === 1),
data.filter(p => p.label === 0)
]
}
)
}
生成数据集的脚本: src\overfit\data.js
/**
*
* @param {*} numSamples 生成的样本的数量
* @param {*} variance 方差,变异,不一样的地方。它是用来控制生成的数据的噪音的,variance调得越大,生成的数据的噪音就越大
*/
export function getData(numSamples, variance) {
let points = [];
function genGauss(cx, cy, label) {
for (let i = 0; i < numSamples / 2; i++) {
let x = normalRandom(cx, variance);
let y = normalRandom(cy, variance);
points.push({x, y, label});
}
}
genGauss(2, 2, 1);
genGauss(-2, -2, 0);
return points;
}
/**
* 生成一个正态分布,也叫高斯分布
* @param {*} mean
* @param {*} variance
*/
function normalRandom(mean = 0, variance = 1) {
let v1, v2, s;
do {
v1 = 2 * Math.random() - 1;
v2 = 2 * Math.random() - 1;
s = v1 * v1 + v2 * v2;
} while (s > 1);
let result = Math.sqrt(-2 * Math.log(s) / s) * v1;
return mean + Math.sqrt(variance) * result;
}
数据可视化之后的效果如下图所示:
三、使用简单神经网络演示欠拟合
操作步骤:
第一步,加载非线性的XOR数据集。
第二步,使用一个神经元的简单的神经网络演示欠拟合。
为什么要加载非线性的XOR数据集呢?因为前面的学习笔记中,我们已经讲过,XOR数据集是一个复杂的问题,需要使用多层神经网络,配合激活函数才能拟合它。我们这里使用只有一个神经元的简单神经网络去拟合它,就会出现欠拟合的情况。
当然,并非只有使用简单模型去解决复杂问题的时候才会出现欠拟合。训练时间不够的时候(就是Epoch不够大),也会出现欠拟合。对于有些图片分类的问题,经常是下班的时候让它开始训练,第二天上班再来看结果。有的甚至需要训练好几天。
首先我们导入一个XOR数据集,因为之前已经在xor目录下写过这个数据集的生成脚本,所以这里我们直接导入就好了。即把src\overfit\index.js文件的第二行由 import {getData} from './data'; 改成 import {getData} from '../xor/data';。并把src\overfit\index.js中getData的第二个参数去掉。
整个src\overfit\index.js的代码修改成如下:
import * as tf from '@tensorflow/tfjs';
import * as tfvis from '@tensorflow/tfjs-vis';
// import {getData} from './data';
import {getData} from '../xor/data';
window.onload = async () => {
// const data = getData(200, 3);
const data = getData(200);
console.log(data);
tfvis.render.scatterplot(
{ name: '训练数据' },
{
values: [
data.filter(p => p.label === 1),
data.filter(p => p.label === 0)
]
}
);
const model = tf.sequential();
model.add(tf.layers.dense({
units: 1,
activation: 'sigmoid',
inputShape: [2]
}));
model.compile({
loss: tf.losses.logLoss,
optimizer: tf.train.adam(0.1)
});
const inputs = tf.tensor(data.map(p => [p.x, p.y]));
const labels = tf.tensor(data.map(p => p.label));
await model.fit(inputs, labels, {
validationSplit: 0.2, // 从数据集里面分出20%的数据作为验证集
epochs: 200,
callbacks: tfvis.show.fitCallbacks(
{ name: '训练效果' },
['loss', 'val_loss'], // 要看到训练集和验证集上的损失
{ callbacks: ['onEpochEnd']}
)
});
}
训练的效果如下:
可见,不管是训练集的损失还是验证集的损失,都一直降不下去,这就是欠拟合的表现。
遇到欠拟合该怎么办呢?
增加模型的复杂度(添加更多的层、添加更多的神经元去尝试)。当然,在现实的工作中,遇到欠拟合通常不是因为模型有问题(虽然也有),更多的情况是训练时间不够长,因为可能数据是海量的(如几百万的图片)。
四、使用复杂神经网络演示过拟合
步骤如下:
首先,加载带有噪音的二分类数据集。
然后,使用多层神经网络演示过拟合。
代码如下:
src\overfit\index2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="index2.js"></script>
</body>
</html>
src\overfit\index2.js
import * as tf from '@tensorflow/tfjs';
import * as tfvis from '@tensorflow/tfjs-vis';
import {getData} from './data';
window.onload = async () => {
const data = getData(200, 1);
console.log(data);
tfvis.render.scatterplot(
{ name: '训练数据' },
{
values: [
data.filter(p => p.label === 1),
data.filter(p => p.label === 0)
]
}
);
const model = tf.sequential();
model.add(tf.layers.dense({
units: 10,
activation: 'tanh',
inputShape: [2]
}));
model.add(tf.layers.dense({
units: 1,
activation: 'sigmoid'
}));
model.compile({
loss: tf.losses.logLoss,
optimizer: tf.train.adam(0.1)
});
const inputs = tf.tensor(data.map(p => [p.x, p.y]));
const labels = tf.tensor(data.map(p => p.label));
await model.fit(inputs, labels, {
validationSplit: 0.2, // 从数据集里面分出20%的数据作为验证集
epochs: 200,
callbacks: tfvis.show.fitCallbacks(
{ name: '训练效果' },
['loss', 'val_loss'], // 要看到训练集和验证集上的损失
{ callbacks: ['onEpochEnd']}
)
});
}
然后通过parcel ./src/**/index2.html进行打包,打包完成后在浏览器中通过http://localhost:1234/index2.html访问到如下效果:
可以看到,训练集的损失越来越小,但是验证集的损失在越过某个点之后越来越大,这就是过拟合了。
五、过拟合应对方法
过拟合的应对方法有:早停法、权重衰减、丢弃法。
所谓早停法,就是在模型的训练集损失开始上升之前,把模型的训练任务给停止掉,这样可以一定程度地防止过拟合。
当然,也可以通过增加训练集的数据的数量来应对过拟合。但是,在现实工作中,增加训练集的数据的数量工作成本是非常高的。需要大量的人力物力。
权重衰减法就是把权重的复杂度也作为模型损失的一部分,我们都知道,训练模型就是降低它的损失,既然复杂度也变成损失的一部分了,那么过于复杂的权重也就在训练的过程中被衰减掉了,防止模型过度复杂。
TensorFlow.js提供了使用权重衰减法的API,即设置L2正则化。即在最复杂的隐藏层上加一个kernelRegularizer属性,即把:
model.add(tf.layers.dense({
units: 10,
activation: 'tanh',
inputShape: [2]
}));
改成:
model.add(tf.layers.dense({
units: 10,
activation: 'tanh',
inputShape: [2],
kernelRegularizer: tf.regularizers.l2({ l2: 0.2 }) // l2: 1是将L2正则化率设置为1,它是一个超参数
}));
效果如下:
丢弃法则是在神经网络的隐藏层设置丢弃率,然后就会随机地丢弃(因为是随机丢弃的,所以就不会有偏爱)某些个神经元的权重,相当于把隐藏层的神经元个数变少了。
在TensorFlow.js中,可以通过在最复杂的隐藏层后面加一个dropout层:
// 用丢弃法避免过拟合
model.add(tf.layers.dropout({
rate: 0.9 // 丢弃率设置为0.9,意思是会随机丢弃90%的神经元
}));
效果如下图所示: