目录
第4章 机器学习基础
4.1 机器学习的四个分支
4.1.1 监督学习
4.1.2 无监督学习
4.1.3 自监督学习
4.1.4 强化学习
4.2 评估机器学习模型
4.2.1 训练集、验证集和测试集
4.2.2 评估模型的注意事项
4.3 数据预处理、特征工程和特征学习
4.3.1 神经网络的数据预处理
4.3.2 特征工程
4.4 过拟合与欠拟合
4.4.1 减小网络大小
4.4.2 添加权重正则化
4.4.3 添加dropout正则化
4.5 机器学习的通用工作流程
4.5.1 定义问题,收集数据集
4.5.2 选择衡量成功的指标
4.5.3 确定评估方法
4.5.4 准备数据
4.5.5 开发比基准更好的模型
4.5.6 扩大模型规模:开发过拟合的模型
4.5.7 模型正则化与调节超参数
本章小结
监督学习是目前最常见的机器学习类型,给定一组样本(通常由人工标注),它可以学会将输入数据映射到已知目标,近年来广受关注的深度学习应用几乎都属于监督学习,比如光学字符识别、语音识别、图像分类和语言翻译。
监督学习主要包括分类和回归,但还有更多的奇特变体,主要包括如下几种:
1.序列生成。给定一张图像,预测描述图像的文字。
2.语法树预测。给定一个句子,预测其分解生成的语法树。
3.目标检测。给定一个图像,在图中特定目标的周围画一个边界框,这个问题也可以表示为分类问题(给定多个候选边界框,对每个框内的目标进行分类)或分类与回归联合问题(用向量回归来预测边界框的坐标)。
4.图像分割。给定一张图像,在特定物体上画一个像素级的掩模。
无监督学习是指在没有目标的情况下寻找输入数据的有趣变换,其目的在于数据可视化、数据压缩、数据去噪或更好地理解数据中的相关性。无监督学习是数据分析的必备技能,在解决监督学习问题之前,为了更好地了解数据集,它通常是一个必要步骤。降维和聚类都是众所周知的无监督学习方法。
自监督学习是监督学习的一个特例,它与众不同,值得单独归为一类。自监督学习是没有人工标注标签的监督学习,你可以将它看作没有人类参与的监督学习。标签仍然存在(因为总要有什么东西来监督学习过程),但它们是从输入数据中生成的,通常是使用启发式算法生成的。自编码器是有名的自监督学习的例子,其生成的目标就是未经修改的输入。监督学习、自监督学习和无监督学习之间的区别有时很模糊,这三个类别更像是没有明确界限的连续体,自监督学习可以被重新解释为监督学习或无监督学习,这取决于你关注的是学习机制还是应用场景。
在强化学习中,智能体接收有关其环境的信息,并学会选择使其某种奖励最大化的行动。
分类和回归术语表:
样本或输入:进入模型的数据点。
预测或输出:从模型出来的结果。
目标:真实值。对于外部数据源,理想情况下,模型应该能够预测出目标。
类别:分类问题中供选择的一组标签。
标签:分类问题中类别标注的具体例子。
真值或标注:数据集的所有目标,通常由人工收集。
二分类:一种分类任务,每个输入样本都应被划分到两个互斥的类别中。
多分类:一种分类任务,每个输入样本都应被划分到两个以上的类别中,比如手写数字分类。
多标签分类:一种分类任务,每个输入样本都可以分配多个标签。
标量回归:目标是连续标量值的任务。
向量回归:目标是一组连续值的任务。
小批量或批量:模型同时处理的一小部分样本(样本数通常为8~128)。
随着训练的进行,模型在训练数据上的性能始终在提高,但在前所未见的数据上的性能则不再变化或者开始下降。
机器学习的目的是得到可以泛化的模型,即在前所未见的数据上表现很好的模型,而过拟合是核心难点。你只能控制可以观察的事情,所以能够可靠地衡量模型的泛化能力非常重要。
评估模型的重点是将数据划分为三个集合:训练集、验证集和测试集。在训练数据上训练模型,在验证数据上评估模型。一旦找到了最佳参数,就在测试数据上最后测试一次。
如果基于模型在验证集上的性能来调节模型配置,会很快导致模型在验证集上过拟合,即使你并没有在验证集上直接训练模型也会如此。
造成这一现象的关键在于信息泄露,每次基于模型在验证集上的性能来调节模型超参数,都会有一些关于验证数据的信息泄露到模型中。如果对每次参数只调节一次,那么泄露的信息很少,验证集仍然可以可靠地评估模型。但如果你多次重复这一过程(运行一次实验,在验证集上评估,然后据此修改模型),那么将会有越来越多的关于验证集的信息泄露到模型中。
最后,你得到的模型在验证集上的性能非常好(人为造成的),因为这正是你优化的目的。你关心的是模型在全新数据上的性能,而不是在验证数据上的性能,因此你需要使用一个完全不同的、前所未见的数据集来评估模型,它就是测试集。你的模型一定不能读取与测试集有关的任何信息,即使间接读取也不行。如果基于测试集性能来调节模型,那么对泛化能力的衡量是不准确的。
三种经典的评估方法:简单的留出验证、K折验证,以及带有打乱数据的重复K折验证。
1.简单的留出验证
留出一定比例的数据作为测试集,在剩余的数据上训练模型,然后在测试集上评估模型,如前所述,为了防止信息泄露,你不能基于测试集来调节模型,所以还应该保留一个验证集。
留出验证:
num_validation_samples = 10000
np.random.shuffle(data)
data = data[num_validation_samples:]
training_data = data[:]
model = get_model()
model.train(training_data)
validation_score = model.evaluate(validataion_data)
model = get_model()
model.train(np.concatenate([training_data, validation_data]))
test_score = model.evaluate(test_data)
这是最简单的评估方法,但有一个缺点:如果可用的数据很少,那么可能验证集和测试集包含的样本就太少,从而无法在统计学上代表数据。
2.K折验证
K折验证将数据划分为大小相同的K个分区,对于每个分区i,在剩余的K-1个分区上训练模型,然后再分区i上评估模型。最终分数等于K个分数的平均值。对于不同的训练集-测试集划分,如果模型性能的变化很大,那么这种方法很有用。与留出验证一样,这种方法也需要独立的验证集进行模型校正。
K折交叉验证:
import numpy as np
k = 4
num_validation_samples = len(data) // k
np.random.shuffle(data)
validation_scores = []
for fold in range(k):
validation_data = data[num_validation_samples * fold : num_validation_samples * (fold + 1)]
training_data = data[:num_validation_samples * fold] + data[num_validation_samples * (fold + 1):]
model = get_model()
model.train(training_data)
validation_score = model.evaluate(validation_data)
validation_scores.append(validation_score)
validation_score = np.average(validation_scores)
model = get_model()
model.train(data)
test_score = model.evaluate(test_data)
3.带有打乱数据的重复K折验证
如果可用的数据相对较少,而你又需要尽可能精确地评估模型,那么可以选择带有打乱数据的重复K折验证。这种方法一共要训练和评估PxK个模型(P是重复次数),计算代价很大。
1.数据代表性。你希望训练集合测试集都能够代表当前数据,将数据划分为训练集和测试集之前,通常应该随机打乱数据。
2.时间箭头。如果想要根据过去预测未来,那么在划分数据前你不应该随机打乱数据,因为这么做会造成时间泄露:你的模型将在未来数据上得到有效训练。在这种情况下,你应该始终确保测试集中所有数据的时间都晚于训练集数据。
3.数据冗余。如果数据中的某些数据点出现了两次,那么打乱数据并划分为训练集合验证集会导致训练集合验证集之间的数据冗余。从效果上看,你是在部分训练数据上评估模型,一定要确保训练集和验证集之间没有交集。
数据预处理的目的是使原始数据更适于用神经网络处理,包括向量化、标准化、处理缺失值和特征提取。
1.向量化
神经网络的所有输入和目标都必须是浮点数张量(在特定情况下可以是整数张量)。无论处理什么数据(声音、图像还是文本),都必须首先将其转换为张量,这一步叫作数据向量化。
2.值标准化
一般来说,将取值相对较大的数据(比如多位整数,比网络权重的初始值大很多)或异质数据输入到神经网络中是不安全的,这么做可能导致较大的梯度更新,进而导致网络无法收敛。为了让网络的学习变得更容易,输入数据应该具有以下特征。
(1)取值较小:大部分值都应该在0~1范围内。
(2)同质性:所有特征的取值都应该在大致相同的范围内。
3.处理缺失值
你的数据中有时可能会有缺失值,一般来说,对于神经网络,将缺失值设置为0是安全的,只要0不是一个有意义的值。网络能够从数据中学到0意味着缺失数据,并且会忽略这个值。
如果测试数据中可能有缺失值,而网络是在没有缺失值的数据上训练的,那么网络不可能学会忽略缺失值。在这种情况下,你应该人为生成一些有缺失项的训练样本:多次复制一些训练样本,然后删除测试数据中可能缺失的某些特征。
特征工程是指将数据输入模型之前,利用你自己关于数据和机器学习算法(这里指神经网络)的知识对数据进行硬编码的变换(不是模型学到的),以改善模型的效果。多数情况下,一个机器学习模型无法从完全任意的数据中进行学习,呈现给模型的数据应该便于模型进行学习。
特征工程的本质:用更简单的方式表述问题,从而使问题变得更容易,它通常需要深入理解问题。
深度学习出现之前,特征工程曾经非常重要,因为经典的浅层算法没有足够大的假设空间来自己学习有用的表示,将数据呈现给算法的方式对解决问题至关重要。
幸运的是,对于现代深度学习,大部分特征工程都是不需要的,因为神经网络能够从原始数据中自动提取有用的特征,但这并不意味着只要使用深度神经网络,就无须担心特征工程,这是因为良好的特征仍然可以让你用更少的资源更优雅地解决问题,也可以让你用更少的数据解决问题。
过拟合存在于所有机器学习问题中,学会如何处理过拟合对掌握机器学习至关重要。
机器学习的根本问题是优化和泛化的之间的对立。优化是指调节模型以在训练数据上得到最佳性能(即机器学习中的学习),而泛化是指训练好的模型在前所未有的数据上的性能好坏。机器学习的目的当然是得到良好的泛化,但你无法控制泛化,只能基于训练数据调节模型。
训练开始时,优化和泛化是相关的:训练数据上的损失越小,测试数据上的损失也越小,这时的模型是欠拟合的,即仍有改进的空间,网络还没有对训练数据中所有相关模式建模。但在训练数据上迭代一定次数之后,泛化不再提高,验证指标先是不变,然后开始变差,即模型开始过拟合。这时模型开始学习仅和训练数据有关的模式,但这种模式对新数据来说是错误的或无关紧要的。
为了防止模型从训练数据中学到错误或无关紧要的模式,最优解决方法是获取更多的训练数据。模型的训练数据越多,泛化能力自然也越好。如果无法获取更多数据,次优解决方法是调节模型允许存储的信息量,或对模型允许存储的信息加以约束。如果一个网络只能记住几个模式,那么优化过程会迫使模型集中学习最重要的模式,这样更可能得到良好的泛化。
这种降低过拟合的方法叫做正则化。
防止过拟合的最简单的方法就是减小模型大小,即减少模型中可学习参数的个数(这由层数和每层的单元个数决定)。在深度学习中,模型中可学习参数的个数通常被称为模型的容量。直观上来看,参数更多的模型拥有更大的记忆容量,因此能够在训练样本和目标之间轻松地学会完美的字典式映射,这种映射没有任何泛化能力。深度学习模型都很擅长拟合训练数据,但真正的挑战在于泛化,而不是拟合。
与此相反,如果网络的记忆资源有限,则无法轻松学会这种映射。因此,为了让损失最小化,网络必须学会对目标具有很强预测能力的压缩表示,这也正是我们感兴趣的数据表示。同时你使用的模型应该具有足够多的参数,以防欠拟合,即模型应避免记忆资源不足。在容量过大与容量不足之间要找到一个折中。
但没有一个魔法公式能够确定最佳层数或每层的最佳大小,你必须评估一系列不同的网络架构(当然是在验证集上评估,而不是在测试集上),以便为数据找到最佳的模型大小。要找到合适的模型大小,一般的工作流程是开始时选择相对较少的层和参数,然后逐渐增加层的大小或增加新层,直到这种增加对验证损失的影响变得很小。
原始模型:
from keras import models
from keras import layers
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
容量更小的模型:
model = model.Sequential()
model.add(layers.Dense(4, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(4, activaiton='relu'))
model.add(layers.Dense(1, activaiton='sigmoid'))
如图,更小的网络开始过拟合的时间要晚于参考网络,而且开始过拟合之后,它的性能变差的速度也更慢。
容量更大的模型:
model = model.Sequential()
model.add(layers.Dense(512, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
更大的网络只过了一轮就开始过拟合,过拟合也更严重,其验证损失的波动也更大。
更大的网络的训练舒适很快就接近于零,网络的容量越大,它拟合训练数据(即得到很小的训练损失)的速度就越快,但也更容易过拟合(导致训练损失和验证损失有很大差异) 。
奥卡姆剃刀原理:如果一件事有两种解释,那么最可能正确的解释是最简单的那个,即假设更少的那个。这个原理也适用于神经网络学到的模型:给定一些训练数据和一种网络架构,很多组权重值(即很多模型)都可以解释这些数据,简单模型比复杂模型更不容易过拟合。
这里的简单模型是指参数值分布的熵更小的模型(或参数更少的模型),因此,一种常见的降低过拟合的方法就是强制让模型权重只能取较小的值,从而限制模型的复杂度,这使得权重值的分布更加规则。这种方法叫作权重正则化,其实现方法是向网络损失函数中添加与较大权重值相关的成本。这个成本有两种形式:
1.L1正则化:添加的成本与权重系数的绝对值成正比;
2.L2正则化:添加的成本与权重系数的平方成正比。神经网络的L2正则化也叫权重衰减。
在Keras中,添加权重正则化的方法是向层传递权重正则化项实例作为关键字参数。
向模型添加L2权重正则化:
from keras import regularizers
model = model.Sequential()
model.add(layers.Dense(16, kernel_regularizer=regularizers.l2(0.001), activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, kernel_regularizer=regularizers.l2(0.001), activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
由于这个惩罚项只在训练时添加,所以这个网络的训练损失会比测试损失大很多。
即使两个模型的参数个数相同,具有L2正则化的模型比参考模型更不容易过拟合。
Keras中不同的权重正则化项:
from keras import regularizers
regularizers.l1(0.001)
regularizers.l1_l2(l1=0.001, l2=0.001)
dropout是神经网络最有效也最常用的正则化方法之一。对某一层使用dropout,就是在训练过程中随机将该层的一些输出特征舍弃(设置为0)。dropout比率是被设为0的特征所占的比例,通常在0.2~0.5范围内,测试时没有单元被舍弃,而该层的输出值需要按dropout比率缩小,因为这时比训练时有更多的单元被激活,需要加以平衡。
测试时:
layer_output *= np.random.randint(0, high=2, size=layer_output.shape)
layer_output *= 0.5
训练时:
layer_output *= np.random.randint(0, high=2, size=layer_output.shape)
layer_output /= 0.5
在Keras中,你可以通过Dropout层向网络中引入dropout,dropout将被应用于前面一层的输出。
向IMDB网络中添加dropout:
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))
总结一下,防止神经网络过拟合的常用方法包括:
1.获取更多的训练数据;
2.减小网络容量;
3.添加权重正则化;
4.添加dropout。
首先,你必须定义所面对的问题。
1.你的输入数据是什么?你要预测什么?只有拥有可用的训练数据,你才能学习预测某件事情。
2.你面对的是什么类型的问题?是二分类问题、多分类问题、标量回归问题、向量回归问题,还是多分类、多标签问题?或者是其他问题,比如聚类、生成或强化学习?确定问题类型有助于你选择模型架构、损失函数等。
只有明确了输入、输出以及所用的数据,你才能进入下一阶段,注意你在这一阶段所做的假设。
1.假设输出是可以根据输入进行预测的。
2.假设可用数据包含足够多的的信息,足以学习输入和输出之间的关系。
在开发出工作模型之前,这些只是假设,等待验证真假,并非所有问题都可以解决。你收集了包含输入X和目标Y的很多样例,并不意味着X包含足够多的信息来预测Y。
有一类无法解决的问题你应该知道,那就是非平稳问题。你想要建模的对象随着时间推移而改变,在这种情况下,正确的做法是不断地利用最新数据重新训练模型,或者在一个问题是平稳的时间尺度上收集数据。
机器学习只能用来记忆训练数据中存在的模式,你只能识别出曾经见过的东西,在过去的数据上训练机器学习来预测未来,这里存在一个假设,就是未来的规律与过去相同,但事实往往并非如此。
要控制一件事物,就需要能够观察它,要取得成功,就必须给出成功的定义。衡量成功的指标将指引你选择损失函数,即模型要优化什么,它应该直接与你的目标保持一致。
对于平衡分类问题(每个类别的可能相同),精度和接受者操作特征曲线下面积是常用的指标;对于类别不平衡的问题,你可以使用准确率和召回率;对于排序问题或多标签分类,你可以使用平均准确率均值。
一旦明确了目标,你必须确定如何衡量当前的进展。
1.留出验证集。
2.K折交叉验证。
3.重复的K折验证。
一旦知道了要训练什么、要优化什么以及评估方法,那么你就几乎已经准备好训练模型了。但首先你应该将数据格式化,使其可以输入到机器学习模型中。
1.应该将数据格式化为张量。
2.这些张量的取值通常应该缩放为较小的值,比如在[-1,1]区间或[0,1]区间。
3.如果不同的特征具有不同的取值范围(异质数据),那么应该做数据标准化。
4.对于小数据问题可能需要做特征工程。
准备好输入数据和目标数据的张量后,就可以开始训练模型了。
这一阶段的目标是获得统计功效,即开发一个小型模型,它能够打败纯随机的基准。
注意,不一定总是能获得统计功效,如果你尝试了多种合理架构之后仍然无法打败随机基准,那么原因可能是问题的答案并不在输入数据中,要记住你所做的两个假设:
1.假设输出是可以根据输入进行预测的。
2.假设可用的数据包含足够多的信息,足以学习输入和输出之间的关系。
这些假设很可能是错误的,这样的话你需要从头重新开始。
如果一切顺利,还需要选择三个关键参数来构建第一个工作模型:
1.最后一层的激活。它对网络输出进行有效的限制。
2.损失函数。它应该匹配你要解决的问题的类型
3.优化配置。
关于损失函数的选择,需要注意,直接优化衡量问题成功的指标不一定总是可行的。有时难以将指标转化为损失函数,损失函数需要在只有小批量数据时即可计算(理想情况下,只有一个数据点时,损失函数应该也是可计算的),而且还必须是可微的(否则无法用反向传播来训练网络)。
一旦得到了具有统计功效的模型,问题就变成了:模型是否足够强大?它是否具有足够多的层和参数来对问题进行建模?机器学习中无处不在的对立是优化和泛化的对立,理想的模型是刚好在欠拟合和过拟合的界限上,在容量不足和容量过大的界线上。为了找到这条界线,你必须穿过它。
要搞清楚你需要多大的模型,就必须开发一个过拟合的模型。
1.添加更多的层。
2.让每一层变得更大。
3.训练更多的轮次。
要始终监控训练损失和验证损失,以及你所关心的指标的训练值和验证值。如果你发现模型在验证数据上的性能开始下降,那么就出现了过拟合。
这一步是最费时间的:你将不断地调节模型、训练、在验证数据上评估(这里不是测试数据)、再次调节模型,然后重复这一过程,直到模型达到最佳性能。
1.添加dropout。
2.尝试不同的架构:增加或减少层数。
3.添加L1和/或L2正则化。
4.尝试不同的超参数(比如每层的单元个数或优化器的学习率),以找到最佳配置。
5.反复做特征工程:添加新特征或删除没有信息量的特征。
每次使用验证过程的反馈来调节模型,都会将有关验证过程的信息泄露到模型中。如果只重复几次,那么无关紧要;但如果系统性地迭代许多次,最终会导致模型对验证过程过拟合(即使模型并没有直接在验证数据上训练),这会降低验证过程的可靠性。
一旦开发出令人满意的模型配置,你就可以在所有可用数据(训练数据+验证数据)上训练最终的生产模型,然后在测试集上最后评估一次。如果测试集上的性能比验证集上差很多,那么这意味着你的验证流程不可靠,或者你在调节模型参数时在验证数据上出现了过拟合,在这种情况下,你可能需要换用更加可靠的评估方法,比如重复的K折验证。
1.定义问题与要训练的数据。收集这些数据,有需要的话用标签来标注数据。
2.选择衡量问题成功的指标。
3.确定评估方法。
4.开发第一个比基准更好的模型,即一个具有统计功效的模型。
5.开发过拟合的模型。
6.基于模型在验证数据上的性能来进行模型正则化与调节超参数。