《Python深度学习》笔记(三):机器学习基础
机器学习的四个分支
监督学习
监督学习是目前最常见的机器学习类型。给定一组样本(通常由人工标注),它可以学会将输入数据映射到已知目标【也叫标注(annotation)】。主要包括分类、回归,还有更多的奇特变体。主要包括如下几种:
- 序列生成( sequence generation)。给定一张图像,预测描述图像的文字。序列生成有时可以被重新表示为一系列分类问题,比如反复预测序列中的单词或标记。
- 语法树预测( syntax tree prediction)。给定一个句子,预测其分解生成的语法树。
- 目标检测( object detection)。给定一张图像,在图中特定目标的周围画一个边界框。这个问题也可以表示为分类问题(给定多个候选边界框,对每个框内的目标进行分类)或分类与回归联合问题(用向量回归来预测边界框的坐标)。
- 图像分割( image segmentation)。给定一张图像,在特定物体上画一个像素级的掩模( mask)。
无监督学习
无监督学习是指在没有目标的情况下寻找数据的有趣变换,其目的在于数据可视化、数据压缩、数据去噪或更好地理解数据中地相关性。比较常见的学习方法:降维(dimensionality reducation)和聚类(clustering).
自监督学习
自监督学习是监督学习的特例。自监督学习是没有人工标注的标签的监督学习,可以看作是没有人类参与的监督学习。标签仍然存在(因为总需要有什么东西来监督学习过程),但他们是从输入数据中生成的,通常是使用启发式算法生成。比较常见的,自编码器(autocoder),其生成的目标就是未经修改的输入。以上三者之间的区别有时很模糊,这三个类别更像是没有明显界限的连续体。
强化学习
目前,强化学习主要集中在研究领域,除游戏外还没有取得实践上的重大成功。但是,我们期待强化学习未来能够实现越来越多的实际应用:自动驾驶汽车、机器人、资源管理、教育等。强化学习的时代已经到来,或即将到来。
评估机器学习模型
数据集的划分:训练集、验证集和测试集。
机器学习的目的是得到可以泛化( generalize)的模型,即在前所未见的数据上表现很好的模型,而过拟合则是核心难点。
评估模型的重点是将数据划分为三个集合:训练集、验证集和测试集。在训练数据上训练模型,在验证数据上评估模型。一旦找到了最佳参数,就在测试数据上最后测试一次。
为社么不是两个集合:一个训练集和一个测试集?
原因在于开发模型时总需要调节模型配置,比如选择层数或每层大小[这叫做模型的超参数(hyperparameter),以便与模型参数(即权重)区分开]。这个调节过程需要模型在验证数据上的性能作为反馈信号。这个调节过程需要模型在验证数据上的性能作为反馈信号。这调节过程本质上就是一种学习:在某个参数空间中寻找良好的模型配置。因此,如果基于模型的验证集上的性能来调节模型的配置,会很快导致模型在验证集上的过拟合,即使你并没有在验证集上直接训练模型也会如此。
造成这一现象的关键在于【信息泄露(information leak)】.每一次基于模型在验证集上的性能来调节模型超参数,都会有一些关于验证数据的信息泄露到模型中。如果对每一个参数只调节一次,那么信息的泄露很少,验证集仍然很可能可以很可靠地评估模型。但如果你多次重复这一过程(运行一次实验,在验证集上评估,然后据此修改模型),那么将会有越来越多地关于验证集地信息泄露到模型中。最后,你得到地模型在验证集上地性能非常好(人为造成的),因为这正是你优化的目的。你关心的是模型在全新数据上的性能,而不是在验证数据上的性能,因此你需要使用一个完全不同、前所未见的数据集来评估模型,他就是测试集。
你的模型一定不能读取与测试集有关的任何信息,即使间接读取也不行。如果基于测试集性能来调节模型,那么对泛化能力的衡量是不准确的。
将数据划分为训练集、验证集和测试集可能看起来很简单,但如果可用数据很少,还有几种高级方法可以派上用场。我们先来介绍三种经典的评估方法:简单的留出验证、 K 折验证,以及带有打乱数据的重复 K 折验证
1. 简单的留出验证、
留出一定比例的数据作为测试数据。在剩余的数据上训练模型,然后在测试集上评估模型。如前所述,为了防止歇息泄露,你不能基于测试集来调节模型,所以还应该保留一个验证集。
#留出验证
num_validation_samples=10000
#通常需要打乱数据
np.random.shuffle(data)
#划分数据集
#定义验证集
validation_data=data[:num_validation_samples]
data=data[num_validation_samples:]
#定义训练集
training=data[:]
#在训练数据上训练模型,并在测试集上评估模型
model=get_model()
model.train(training_data)
validation_score=model.evaluate(validation_data)
#现在你可以调节模型、重现训练、评估、然后再次调节
#一旦调整好超参数、通常就在非测试数据上从头开始训练最终模型
model=get_model()
model.train(np.concatenate([training_data,validation_data]))
test_score=model.evaluate(test_data)
这是最简单的评估方法,但有一个缺点:如果可用的数据很少,那么可能验证集和测试集包含的样本就太少,从而无法在统计学上代表数据。这个问题很容易发现:如果在划分数据前进行不同的随机打乱,最终得到的模型性能差别很大,那么就存在这个问题。
2. K折验证
K 折验证( K-fold validation)将数据划分为大小相同的 K 个分区。 对于每个分区 i,在剩余的 K-1 个分区上训练模型,然后在分区 i 上评估模型。最终分数等于 K 个分数的平均值。对于不同的训练集 - 测试集划分,如果模型性能的变化很大,那么这种方法很有用。与留出验证一样,这种方法也需要独立的验证集进行模型校正。
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)]
#这里的+表示数据集的合并
train_data=data[:num_validation_samples*fold]+
data[num_validation_samples*(fold+1):]
model=get_model()
model.train(training_data)
validation_score=model.evalution(validation_data)
validation_scores.append(validation_score)
#最终验证分数:K折验证分数的平均值
validation_score=np.average(validation_scores)
#最终在所有非测试数据上训练最终模型
model=get_model()
model.train(data)
test_score=model.evaluate(test_data)
3.带有打乱数据的重复K折验证
如果可用的数据相对较少,而你又需要尽可能精确地评估模型,那么可以选择带有打乱数据的重复 K 折验证( iterated K-fold validation with shuffling)。我发现这种方法在 Kaggle 竞赛中特别有用。具体做法是多次使用 K 折验证,在每次将数据划分为 K 个分区之前都先将数据打乱。最终分数是每次 K 折验证分数的平均值。注意,这种方法一共要训练和评估 P× K 个模型( P是重复次数),计算代价很大。
注意事项
- 数据代表性( data representativeness)。你希望训练集和测试集都能够代表当前数据。例如,你想要对数字图像进行分类,而图像样本是按类别排序的,如果你将前 80% 作为训练集,剩余 20% 作为测试集,那么会导致训练集中只包含类别 0~7,而测试集中只包含类别 8~9。这个错误看起来很可笑,却很常见。因此,在将数据划分为训练集和测试集之前,通常应该随机打乱数据。
- 时间箭头( the arrow of time)。如果想要根据过去预测未来(比如明天的天气、股票走势等),那么在划分数据前你不应该随机打乱数据,因为这么做会造成时间泄露( temporalleak):你的模型将在未来数据上得到有效训练。在这种情况下,你应该始终确保测试集中所有数据的时间都晚于训练集数据。
- 数据冗余( redundancy in your data)。如果数据中的某些数据点出现了两次(这在现实中的数据里十分常见),那么打乱数据并划分成训练集和验证集会导致训练集和验证集之间的数据冗余。从效果上来看,你是在部分训练数据上评估模型,这是极其糟糕的!
一定要确保训练集和验证集之间没有交集。
数据预处理、特征工程和特征学习
神经网络的数据预处理
-
向量化
神经网络的所有输入和目标都必须是浮点型张量(特定情况下可以是整数张量)。无论处理什么数据,都必须首先将其转换为张量,这一步叫作数据向量化(data vectorization)。 -
值标准化
在手写数字分类的例子中,开始时图像数据被编码为 0~255 范围内的整数,表示灰度值。将这一数据输入网络之前,你需要将其转换为 float32 格式并除以 255,这样就得到 0~1 范围内的浮点数。同样,预测房价时,开始时特征有各种不同的取值范围,有些特征是较小的浮点数,有些特征是相对较大的整数。将这一数据输入网络之前,你需要对每个特征分别做标准化,使其均值为 0、标准差为 1。
一般来说,将取值相对较大的数据(比如多位整数,比网络权重的初始值大很多)或异质数据( heterogeneous data,比如数据的一个特征在 0~1 范围内,另一个特征在 100~200 范围内)[输入到神经网络中是不安全的。这么做可能导致较大的梯度更新,进而导致网络无法收敛]为了让网络的学习变得更容易,输入数据应该具有以下特征。
- 取值较小:大部分值都应该在 0~1 范围内。
- 同质性( homogenous):所有特征的取值都应该在大致相同的范围内。
此外,下面这种更严格的标准化方法也很常见,而且很有用,虽然不一定总是必需的(例如,对于数字分类问题就不需要这么做)。 - 将每个特征分别标准化,使其平均值为 0。
- 将每个特征分别标准化,使其标准差为 1。
- 处理缺失值
一般来说,对于神经网络,将缺失值设置为0是安全的,只要0不是一个有意义的值。网络能够从数据中学到0意味着缺失数据,并且会忽略这个值。
注:
如果测试数据中可能有缺失值,而网络是没有缺失值的数据上训练的,那么网络不可能学会忽略缺失值,在这种情况下,你应该始终确保测试集你应该人为生成一些有缺失项的训练样本:多次复制一些训练样本,然后删除测试数据中可能缺失的某些特征。
3.1 特征工程
特征工程(feature engineering)是指将数据输入模型之前,利用你子集关于数据和机器学习(这里指神经网络)的知识对数据进行硬编码的变换(不是模型学到的),以改善模型的效果。多数情况下,一个机器学习模型无法从完全任意的数据中进行学习。呈现给模型的数据应该便于模型进行学习。
特征工程的本质:用更简单的方式表述问题,从而使问题变得更容易。
它通常需要深入理解问题。深度学习出现之前,特征工程曾经非常重要,因为经典的浅层算法没有足够大的假设空间来自己学习有用的表示。将数据呈现给算法的方式对解决问题至关重要。例如,卷积神经网络在 MNIST 数字分类问题上取得成功之前,其解决方法通常是基于硬编码的特征,比如数字图像中的圆圈个数、图像中每个数字的高度、像素值的直方图等。幸运的是,对于现代深度学习,大部分特征工程都是不需要的,因为神经网络能够从原始数据中自动提取有用的特征。这是否意味着,只要使用深度神经网络,就无须担心特征工程呢?并不是这样,原因有两点。
- 良好的特征仍然可以让你用更少的资源更优雅地解决问题。例如,使用卷积神经网络来读取钟面上的时间是非常可笑的。
- 良好的特征可以让你用更少的数据解决问题。深度学习模型自主学习特征的能力依赖于大量的训练数据。如果只有很少的样本,那么特征的信息价值就变得非常重要。
- 过拟合与欠拟合
机器学习的根本问题是优化和泛化之间的对立。优化(optimization) 是指调节模型以在训练数据上得到最佳性能(即机器学习中的学习),而泛化(generalization)是指训练好的模型在前所未有见的而数据上的性能好坏。机器学习的目的在于得到良好的泛化,但你无法控制泛化,只能基于训练数据调节模型。
训练开始时,优化和泛化是相关的:训练数据上的损失越小,测试数据上的损失也越小。这时的模型是欠拟合( underfit)的,即仍有改进的空间,网络还没有对训练数据中所有相关模式建模。但在训练数据上迭代一定次数之后,泛化不再提高,验证指标先是不变,然后开始变差,即模型开始过拟合。这时模型开始学习仅和训练数据有关的模式,但这种模式对新数据来说是错误的或无关紧要的。
为了防止模型从训练数据中学到错误或无关紧要的模式, 最优解决方法是获取更多的训练数据。模型的训练数据越多,泛化能力自然也越好。如果无法获取更多数据,次优解决方法是调节模型允许存储的信息量,或对模型允许存储的信息加以约束。如果一个网络只能记住几个模式,那么优化过程会迫使模型集中学习最重要的模式,这样更可能得到良好的泛化。这种降低过拟合的方法叫作正则化( regularization)。
4.1 减小网络大小
防止过拟合的最简单的方法就是减小模型大小,即减小模型中可学习参数的个数(这由层数和每层的单元个数决定)。在深度学习中,模型可学习参数的个数通常被称为模型的容量(capacity).直观上来看,参数更多的模型拥有更大的记忆容量( memorization capacity),因此能够在训练样本和目标之间轻松地学会完美的字典式映射,这种映射没有任何泛化能力.
始终牢记:深度学习模型通常都很擅长拟合训练数据,但真正的挑战在于泛化,而不是拟合。
与此相反,如果网络的记忆资源有限,则无法轻松学会这种映射。因此, 为了让损失最小化,网络必须学会对目标具有很强预测能力的压缩表示,这也正是我们感兴趣的数据表示。同时请记住,你使用的模型应该具有足够多的参数,以防欠拟合,即模型应避免记忆资源不足。在容量过大与容量不足之间要找到一个折中。
不幸的是,没有一个魔法公式能够确定最佳层数或每层的最佳大小。你必须评估一系列不同的网络架构(当然是在验证集上评估,而不是在测试集上),以便为数据找到最佳的模型大小。要找到合适的模型大小,一般的工作流程是开始时选择相对较少的层和参数,然后逐渐增加层的大小或增加新层,直到这种增加对验证损失的影响变得很小。
4.2 添加权重正则化
奥卡姆剃刀( Occam’s razor)原理
如果一件事情有两种解释,那么最可能正确的解释就是最简单的那个,即假设更少的那个。
这个原理也适用于神经网络:给定一些训练数据和一种网络架构,很多组权重值(即很多模型)都会有一些关于验证数据的信息泄露到模型中。如果对每一个参数只调节一次,可以解释这些数据,简单模型比复杂模型更不容易过拟合。
简单模型
简单模型( simple model)是指参数值分布的熵更小的模型(或参数更少的模型,比如上一节的例子)。因此,[一种常见的降低过拟合的方法就是强制让模型权重只能取较小的值,从而限制模型的复杂度,这使得权重值的分布更加规则( regular)。]这种方法叫作权重正则化( weight regularization),其实现方法是向网络损失函数中添加与较大权重值相关的成本( cost)。这个成本有两种形式。
- L1 正则化( L1 regularization):添加的成本与权重系数的绝对值[权重的 L1 范数( norm)]成正比。
- L2 正则化( L2 regularization):添加的成本与权重系数的平方(权重的 L2 范数)成正比。神经网络的 L2 正则化也叫权重衰减( weight decay)。不要被不同的名称搞混,权重衰减与 L2 正则化在数学上是完全相同的。
在Keras中,添加权重正则化的方法是向层传递权重正则化实例(weight regularizer instance)作为关键参数。
from keras import models,layers,regularizers
model=models.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'))
#正则化
from keras import regularizers
regularizers.l1(0.001)
regularizers.l1_l2(l1=0.001,l2=0.001)#同时L1和L2正则化
4.3 添加dropout正则化
dropout 是神经网络最有效也最常用的正则化方法之一,它是由多伦多大学的 Geoffrey Hinton和他的学生开发的。对某一层使用 dropout,就是在训练过程中随机将该层的一些输出特征舍弃(设置为 0)。假设在训练过程中,某一层对给定输入样本的返回值应该是向量 [0.2, 0.5,1.3, 0.8, 1.1]。使用 dropout 后,这个向量会有几个随机的元素变成 0,比如 [0, 0.5,1.3, 0, 1.1]。 dropout 比率( dropout rate)是被设为 0 的特征所占的比例,通常在 0.2~0.5范围内。测试时没有单元被舍弃,而该层的输出值需要按 dropout 比率缩小,因为这时比训练时有更多的单元被激活,需要加以平衡。
注:
这一方法可能看起来有些奇怪和随意。它为什么能够降低过拟合? Hinton 说他的灵感之一来自于银行的防欺诈机制。用他自己的话来说:“我去银行办理业务。柜员不停地换人,于是我问其中一人这是为什么。他说他不知道,但他们经常换来换去。我猜想, 银行工作人员要想成功欺诈银行,他们之间要互相合作才行。这让我意识到,在每个样本中随机删除不同的部分神经元,可以阻止它们的阴谋,因此可以降低过拟合。” a 其核心思想是在层的输出值中引入噪声,打破不显著的偶然模式( Hinton 称之为阴谋)。如果没有噪声的话,网络将会记住这些偶然模式。
在Keras中,可以通过Dropout层向网络中引入dropout,dropout将被应用于前面一层的输出。
model.add(layers.Dropout(0.5))
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'))
小结:防止神经网络过拟合的常用方法:
- 获取更多的训练数据
- 减小网络容量
- 添加权重正则化
- 添加 dropout
机器学习的通用工作流程
-
定义问题,收集数据
-
选择衡量成功的指标
对于平衡分类问题(每个类别的可能性相同),精度和接收者操作特征曲线下面积( areaunder the receiver operating characteristic curve, ROC AUC)是常用的指标。对于类别不平衡的问题,你可以使用准确率和召回率。对于排序问题或多标签分类,你可以使用平均准确率均值( mean average precision)。自定义衡量成功的指标也很常见。 -
确定评估方法
一旦明确了目标,你必须确定如何衡量当前的进展。前面介绍了三种常见的评估方法。
- 留出验证集。数据量很大时可以采用这种方法。
- K 折交叉验证。如果留出验证的样本量太少,无法保证可靠性,那么应该选择这种方法。
- 重复的 K 折验证。如果可用的数据很少,同时模型评估又需要非常准确,那么应该使用这种方法。
只需选择三者之一。大多数情况下,第一种方法足以满足要求。
- 准确数据
首先你应该将数据格式化,使其可以输入到机器学习模型中(这里假设模型为深度神经网络)。
- 如前所述,应该将数据格式化为张量。
- 这些张量的取值通常应该缩放为较小的值,比如在 [-1, 1] 区间或 [0, 1] 区间。
- 如果不同的特征具有不同的取值范围(异质数据),那么应该做数据标准化。
- 你可能需要做特征工程,尤其是对于小数据问题。准备好输入数据和目标数据的张量后,你就可以开始训练模型了。
- 开发比基准更好的模型
这一阶段的目标是获得统计功效( statistical power),即开发一个小型模型,它能够打败纯随机的基准( dumb baseline)。
注意,不一定总是能获得统计功效。如果你尝试了多种合理架构之后仍然无法打败随机基准,那么原因可能是问题的答案并不在输入数据中。要记住你所做的两个假设。
- 假设输出是可以根据输入进行预测的。
- 假设可用的数据包含足够多的信息,足以学习输入和输出之间的关系。
这些假设很可能是错误的,这样的话你需要从头重新开始。如果一切顺利,你还需要选择三个关键参数来构建第一个工作模型。
最后一层的激活。它对网络输出进行有效的限制。例如, IMDB 分类的例子在最后一层使用了 sigmoid,回归的例子在最后一层没有使用激活,等等。
- 损失函数。它应该匹配你要解决的问题的类型。例如, IMDB 的例子使用 binary_crossentropy、回归的例子使用 mse, 等等。
- 优化配置。你要使用哪种优化器?学习率是多少?大多数情况下,使用 rmsprop 及其默认的学习率是稳妥的。
常见问题类型的最后一层激活和损失函数,可以帮你进行选择。
- 扩大模型规模:开发过拟合的模型
一旦得到了具有统计功效的模型,问题就变成了:模型是否足够强大?它是否具有足够多的层和参数来对问题进行建模?例如,只有单个隐藏层且只有两个单元的网络,在 MNIST 问题上具有统计功效,但并不足以很好地解决问题。请记住,机器学习中无处不在的对立是优化和泛化的对立,理想的模型是刚好在欠拟合和过拟合的界线上,在容量不足和容量过大的界线上。为了找到这条界线,你必须穿过它。要搞清楚你需要多大的模型,就必须开发一个过拟合的模型,这很简单。
- 添加更多的层。
- 让每一层变得更大。
- 训练更多的轮次。
要始终监控训练损失和验证损失,以及你所关心的指标的训练值和验证值。如果你发现模型在验证数据上的性能开始下降,那么就出现了过拟合。下一阶段将开始正则化和调节模型,以便尽可能地接近理想模型,既不过拟合也不欠拟合
- 模型正则化与调节超参数
这一步是最费时间的:你将不断地调节模型、训练、在验证数据上评估(这里不是测试数据)、再次调节模型,然后重复这一过程,直到模型达到最佳性能。你应该尝试以下几项。
- 添加 dropout。
- 尝试不同的架构:增加或减少层数。
- 添加 L1 和 / 或 L2 正则化。
- 尝试不同的超参数(比如每层的单元个数或优化器的学习率),以找到最佳配置。
- (可选)反复做特征工程:添加新特征或删除没有信息量的特征。
请注意:每次使用验证过程的反馈来调节模型,都会将有关验证过程的信息泄露到模型中。如果只重复几次,那么无关紧要;但如果系统性地迭代许多次,最终会导致模型对验证过程过拟合(即使模型并没有直接在验证数据上训练)。这会降低验证过程的可靠性。一旦开发出令人满意的模型配置,你就可以在所有可用数据(训练数据 + 验证数据)上训练最终的生产模型,然后在测试集上最后评估一次。如果测试集上的性能比验证集上差很多,那么这可能意味着你的验证流程不可靠,或者你在调节模型参数时在验证数据上出现了过拟合。在这种情况下,你可能需要换用更加可靠的评估方法,比如重复的 K 折验证。