本文参考书籍:《机器学习实战(基于Scikit-Learn和TensorFlow)》
使用Scikit-Learn训练并运行一个线性模型:
若使用K-近邻算法,将sklearn.linear_model.LinearRegression()
改为sklearn.neighbors.KNeighborsRegressor(n_neighbors=3)
通过info()方法可以快速获取数据集的简单描述,如总行数、每个属性的类型和非空值的数量
可使用value_counts()方法查看ocean_proximity有多少种分类存在,每种类别下分别有多少个区域:
通过describe()方法可以显示数值属性的摘要:
这里空值会被忽略。百分位数表示一组观测值中给定百分比的观测值都低于该值
可以在整个数据集上调用hist()方法,绘制每个属性的直方图:
随机选择一些实例,通常是数据集的20%:
为避免每次运行一遍产生不同的数据集,解决方案之一是第一次运行后即保存测试集,另一种方法是在调用np.random.permutation()时设置一个随机数生成器的种子,从而让它始终生成相同的随机索引(如np.random.permutation(42))
上述两种方法在下一次获取更新的数据时都会中断。可为每个实例都使用一个标识符(identifier)来决定是否进入测试集(假定每个实例都有一个唯一且不变的标识符)。如,可以计算每个实例标识符的hash值,只取hash的最后一个字节,若该值小于等于51(约256的20%),则将该实例放入测试集。这样可以确保测试集在多个运行里都是一致的,即便更新数据集也仍然一致。新实例的20%将被放入新的测试集,而之前训练集中的实例也不会被放入新测试集:
housing数据集没有标识符列。可使用行索引作为ID:
若使用行索引作为唯一标识符,需确保在数据集的末尾添加新数据,并且不会删除任何行。若不能保证,可以使用某个最稳定的特征来创建唯一标识符,如将一个地区的经纬度组合成如下的ID:
Scikit-learn提供了函数可以通过多种方式将数据集分成多个子集。train_test_split与之前我们定义的函数split_train_test几乎相同,除了几个额外特征。它也有random_state参数,可以设置随机生成器种子;其次,可以把行数相同的多个数据集一次性发送给它,它会根据相同的索引将其拆分(例如,当有一个单独的DataFrame用于标记时,这就非常有用)
目前为止,我们使用的是纯随机的抽样方法,若数据集不是足够庞大,可能会造成抽样偏差。可以采用分层抽样的方法,若希望确保在收入属性上,测试集能够代表整个数据集中各种不同类型的收入。由于收入中位数是一个连续的数值属性,所以得先创建一个收入类别的属性。
在数据集中,每一层都要有足够数量的实例,不然数据不足的层,其重要程度很有可能会被错估。下面这段代码是这样创建收入类别属性的:将收入中位数除以1.5(限制收入类别的数量),然后使用ceil进行取整(得到离散类别),最后将所有大于5的类别合并为类别5:
现在,可以根据收入类别进行分层抽样了。使用Scikit-learn的Stratified Shuffle Split类:
经计算,在此例子中,分层抽样的测试集中的比例分布与完整数据集中的分布几乎一致,而纯随机抽样的测试集结果出现较大的偏离。
现在删除income_cat属性:
将alpha选项设为0.1,可以看出高密度数据点的位置
现在,再来看看房价。每个圆的半径大小代表了每个地区的人口数量(选项s),颜色代表价格(选项c)。使用一个名叫jet的预定义颜色表(选项cmap)来进行可视化,颜色范围从蓝(低)到红(高):
可以用corr()方法计算出每对属性之间的标准相关系数:
现在看看每个属性与房价中位数的相关性分别是多少:
下图显示了横轴和纵轴之间相关性系数的多种绘图:
pandas的scatter_matrix函数可绘制出每个数值属性相对于其他数值属性的相关性:
最有潜力能够预测房价中位数的属性是收入中位数:
在准备给机器学习算法输入数据之前,要做的最后一件事应该是尝试各种属性的组合。例如,创建新属性:
新的属性bedrooms_per_room与rooms_per_household都比原属性与房价中位数的相关性要高
现在,让我们回到一个干净的数据集(再次复制strat_train_set),然后将预测器和标签分开,因为这里我们不一定对它们使用相同的转换方式( drop()会创建一个数据副本,但不影响strat_train_set ):
total_bedrooms属性有部分值缺失,有以下三种选择方法:
通过DataFrame的dropna()、drop()和fillna()方法,可以完成以上操作:
Scikit-Learn的imputer可以处理缺失值。首先,需要创建一个imputer实例,指定要用属性的中位数值替换该属性的缺失值:
由于中位数值只能在数值属性上计算,所以我们需要创建一个没有文本属性的数据副本:
使用fit()方法将imputer实例适配到训练集:
imputer计算了每个属性的中位数值,并将结果存储在其实例变量statistics_中。可以将imputer应用于所有的数值属性
使用imputer将缺失值替换成中位数值完成训练集转换,结果是一个包含转换后特征的Numpy数组。可将其放回Pandas DataFrame
Scikit-Learn的设计:
1.一致性:
2.检查
所有估算器的超参数都可以通过公共实例变量(如imputer.strategy)直接访问,并且所有估算器的学习参数也可以通过有下划线后缀的公共实例变量来访问(如,imputer.strategy_)
3.防止类扩散
数据集被表示为NumPy数组或是SciPy稀疏矩阵,而不是自定义的类型。超参数只是普通的Python字符串或数字
4.合理的默认值
Scikit-Learn为大多数参数提供了合理的默认值
可以将文本标签转化为数字,Scikit-Learn提供了一个转换器LabelEncoder:
可以使用classes_属性来查看这个编码器已学习的映射:
这种代表方式产生的一个问题是,机器学习算法会以为两个相近的数字比两个离得较远的数字更为相似一些。事实情况并非如此。为解决此问题,可为每个类别创建一个二进制的属性:当类别是<1H OCEAN时,一个属性为1(其他为0),依次类推。这就是独热编码,只有一个属性为1(热),其他均为0(冷)
Scikit-learn提供了一个OneHotEncoder编码器,可以将整数分类值转换为独热向量。由于fit_transform()需要一个二维数组,需将housing_cat_encoded重塑:
输出是一个SciPy稀疏矩阵,仅存储非零元素的位置。若你实在想把它转换成一个(密集的)NumPy数组,只需调用toarray()方法:
使用LabelBinarizer类可以一次性完成两个转换(从文本类别转换为整数类别,再从整数类别转换为独热向量):
默认返回的是一个密集的NumPy数组。通过发送sparse_output=True给LabelBinarizer构造函数,可以得到稀疏矩阵
需要创建一个类,然后应用以下三个方法:fit() (返回自身)、transform()、fit_transform()。若添加TransformerMixin作为基类,就可以直接得到最后一个方法。同时,若添加BaseEstimator作为基类(并在构造函数中避免*args和**kargs),能额外获得两个自动调整超参数的方法(get_params()和set_params())。例如,这里有个简单的转换器类,用于添加组合后的属性:
在本例中,转换器有一个超参数add_bedrooms_per_room默认设置为True。该超参数可以让你知道添加这个属性是否有助于机器学习的算法。若对数据准备的步骤没有充分的信心,就可添加这个超参数进行把关。
常用的两种方法:最小-最大缩放和标准化
最小-最大缩放(又称归一化):将值重新缩放使其最终范围归于0到1之间。实现方法是将值减去最小值并除以最大值和最小值的差。对此,Scikit-learn提供了一个名为MinMaxScaler的转换器。若希望范围不是0~1,可以通过调整超参数feature_range进行更改。
标准化首先减去平均值(所以标准化值的均值总是0),然后除以方差,从而使得结果的分布具备单位方差。不同于最小-最大缩放的是,标准化不将值绑定到特定范围,对某些算法而言,这可能是个问题(例如,神经网络期望的输入值范围通常是0到1)。但是标准化的方法受异常值的影响更小,例如,假设某个地区的平均收入等于100(错误数据)。最小-最大缩放会将所有其他值从0~ 15降到0~ 0.15,而标准化则不会受到很大影响。Scikit-Learn提供了一个标准化的转换器StandadScaler
跟所有转换一样,缩放器仅用来拟合训练集,而不是完整的数据集(包括测试集)。只有这样,才能使用它们来进行转换
许多数据转换的步骤需要以正确的顺序来执行。而Scikit-Learn正好提供了Pipeline来支持这样的转换。下面是一个数值属性的流水线的例子:
Pipeline构造函数会通过一系列名称/估算器的配对来定义步骤的序列。除了最后一个是估算器之外,前面都必须是转换器(即,必须有fit_transform()方法)。命名可以随意
当调用流水线的fit()方法时,会在所有转换器上按照顺序依次调用fit_transform(),将一个调用的输出作为参数传递给下一个调用方法,直到传递到最终的估算器,则只会调用fit()方法
流水线的方法与最终的估算器的方法相同。在本例中,最后一个估算器是StandardScaler,这是个转换器,因此Pipeline有transform()方法可以按顺序将所有的转换应用到数据中(若不希望先调用fit()再调用transform(),也可以直接调用fit_transform()方法)
现在,已经有了一个处理数值的流水线,接下来需要在分类值上应用LabelBinarizer。Scikit-learn提供了FeatureUnion类,只需要提供一个转换器列表(可以是整个转换器流水线),当transform()方法被调用时,它会并行运行每个转换器的transform()方法,等待它们的输出,然后将它们连结起来,返回结果(同样地,调用fit()方法也会调用每个转换器的fit()方法)。一个完整的处理数值和分类属性的流水线可能如下所示:
运行整条流水线:
每条子流水线从选择器转换器开始:只需要挑出所需的属性(数值或分类),删除其余的数据,然后将生成的DataFrame转换为NumPy数组,数据转换就完成了。Scikit-learn中没有可以用来处理Pandas DataFrames的,因此我们需要为此任务编写一个简单的自定义转换器:
先训练一个线性回归模型:
用几个训练集的实例试试:
可以用Scikit-learn的mean_squared_error函数来测量整个训练集上回归模型的RMSE(均方根误差):
模型欠拟合,我们来训练一个决策树DecisionTreeRegressor,它能从数据中找到复杂的非线性关系:
很有可能出现了严重的过拟合,为确认这一点,可以用训练集的一部分用于模型的验证
评估决策树模型的一种方法是使用train_test_split函数将训练集分为较小的训练集和验证集,然后根据这些较小的训练集来训练模型,并对其进行评估。
另一个选择是使用Scikit-learn的交叉验证功能。以下是执行K-折(K-fold)交叉验证的代码:它将训练集随机分割成10个不同的子集,每个子集称为一个折叠(fold),然后对决策树模型进行10次训练和评估——每次挑选1个折叠进行评估,使用另外的9个折叠进行训练。产出的结果是一个包含10次评估分数的数组:
Scikit-learn的交叉验证功能更倾向于使用效用函数(越大越好)而不是成本函数(越小越好),所以计算分数的函数实际上是负的MSE(均方根误差)函数,这就是为什么上面的代码在计算平方根之前会先计算出-scores
决策树得出的评分约为71200
决策树模型确实严重过拟合了,以至于表现得比线性回归模型还要差
再来试试最后一个模型:RandomForestRegressor。随机森林通过对特征的随机子集进行许多个决策树的训练,然后对其预测取平均。在多个模型的基础之上建立模型,称为集成学习。
随机森林要比之前的两个算法好,但训练集上的分数仍然远低于验证集,说明仍然过拟合。过拟合的可能解决方案包括简化模型、约束模型(正则化),或者获得更多的训练数据。不过在深入探索随机森林之前,应该先尝试一遍各种机器学习算法的其他模型(几种具有不同内核的支持向量机、神经网络模型等),但是别花太多时间去调整超参数。我们的目的是筛选出几个(2~5个)有效的模型
每一个尝试过的模型都应该妥善保存,这样将来可以轻松回到你想要的模型当中。记得还要同时保存超参数和训练过的参数,以及交叉验证的评分和实际预测的结果。这样你就可以轻松地对比不同模型类型的评分,以及不同模型造成的错误类型。通过Python的pickel模块或是sklearn.externals.joblib,可以轻松保存Scikit-Learn模型,这样可以更有效地将大型NumPy数组序列化:
一种微调的方法是手动调整超参数,不过你会坚持不到足够的时间来探索出各种组合。
Scikit-Learn的GridSearchCV可以进行探索,需告知要进行实验的超参数,以及需要尝试的值,它将会使用交叉验证来评估超参数值的所有可能的组合。例如,下面这段代码搜索RandomForestRegressor的超参数值的最佳组合:
这个param_grid告诉Scikit-Learn,首先评估第一个dict中的n_estimator和max_features的所有3X4=12种超参数值组合,接着,尝试第二个dict中超参数值的所有2X3=6种组合,但这次超参数bootstrap需要设置为False而不是True(True是该超参数的默认值)
网格搜索将探索RandomForestRegressor超参数值的18种组合,并对每个模型进行5次训练(因为我们使用的是5-折交叉验证)。总共会完成90次训练。完成后可以获得最佳的参数组合:
可以直接得到最好的估算器:
若GridSearchCV被初始化为refit=True(这也是默认值),那么一旦通过交叉验证找到了最佳估算器,它将在整个训练集上重新训练,这通常是个好办法,因为提供更多的数据可能提升其性能。
评估分数:
有些数据准备的步骤也可以当作超参数来处理。例如,网格搜索会自动查找是否添加你不确定的特征(比如是否使用转换器CombinedAttributesAdder的超参数add_bedrooms_per_room)。同样,还可以用它来自动寻找处理问题的最佳方法,例如处理异常值、缺失特征,以及特征选择等。
若探索的组合数量较少——例如上一个示例,网格搜索是一个不错的方法;但当超参数的搜索范围较大时,通常会优先选择使用RandomizedSearchCV。这个类用起来与GridSearchCV类大致相同,但它不会尝试所有可能的组合,而是在每次迭代中为每个超参数选择一个随机值,然后对一定数量的随机组合进行评估。
还有一种微调系统的方法是将表现最优的模型组合起来。组合(或集成)方法通常比最佳的单一模型更好(就像随机森林比其所依赖的任何单个决策树模型更好一样),特别是当单一模型会产生严重不同类型的错误时更是如此
通过检查最佳模型,总是可以得到一些好的洞见。例如在进行准确预估时,RandomForestRegressor可以指出每个属性的相对重要程度:
将这些重要性分数显示在对应的属性名称旁边:
有了这些信息,可以尝试删除一些不太有用的特征(例如,本例中只有一个ocean_proximity(<1H OCEAN)是有用的,可以试着删除其他所有特征)
从测试集中获取预测器和标签,运行full_pipeline来转换数据(调用transform()而不是fit_transform()),然后在测试集上评估最终模型:
若之前进行过大量的超参数调整,这时的评估结果通常会略逊于之前使用交叉验证时的表现结果(因为通过不断调整,系统在验证数据上终于表现良好,在未知数据集上可能达不到这么好的效果)。当这种情况发生时,一定要忍住继续调整超参数的诱惑,不要试图再努力让测试集的结果也变得好看,这些改进在泛化到新的数据集时又会变成徒劳
Scikit-learn加载的数据集通常具有类似的字典结构,包括:
共有7万张图片,每张图片有784个特征。因为图片是28x28像素,每个特征代表了一个像素点的强度,从0(白色)到255(黑色)。若需看数据集中的一个数字,只需抓取一个实例的特征向量,将其重新形成一个28x28数组,然后使用Matplotlib的imshow()函数将其显示出来:
MNIST数据集已经分成训练集(前6万张图像)和测试集(最后1万张图像):
将训练集数据洗牌,这样能保证交叉验证时所有的折叠都差不多(肯定不希望某个折叠丢失一些数字)。此外,有些机器学习算法对训练实例的顺序敏感,如果连续输入许多相似的实例,可能导致执行性能不佳。给数据集洗牌正是为了确保这种情况不会发生:
先简化问题,只尝试识别一个数字——比如数字5。先为此分类任务创建目标向量:
接着挑选一个分类器并开始训练。一个好的初始选择是随机梯度下降(SGD)分类器,使用Scikit-learn的SGDClassifier类即可。随机梯度下降分类器能够有效处理非常大型的数据集,因为SGD独立处理训练实例,一次一个
先创建一个SGDClassifier并在整个训练集上进行训练:
SGDClassifier在训练时是完全随机的,若需要可复现的结果,需要设置参数random_state
现在可以用它来检测数字5的图像:
实施交叉验证:
若不使用cross_val_score()这一类交叉验证的函数,可以自行实施交叉验证:
每个折叠由StratifiedKFold执行分层抽样产生,其所包含的各个类的比例符合整体比例。每个迭代会创建一个分类器的副本,用训练集对这个副本进行训练,然后用测试集进行预测。最后计算正确预测的次数,输出正确预测的比率
现在,用cross_val_score()函数来评估SGDClassifier模型,采用K-fold交叉验证法,3个折叠。K-fold交叉验证的意思是将训练集分解成K个折叠,然后每次留其中1个折叠进行预测,剩余的折叠用来训练:
将每张图都分类为非5也有90%的准确率,因为只有大约10%的图像是数字5,准确率通常无法成为分类器的首要性能指标,特别是处理偏斜数据集时
评估分类器性能的更好方法是混淆矩阵。即统计A类别实例被分成B类别实例的次数。例如,想知道分类器将数字3和数字5混淆多少次,只需要通过混淆矩阵的第5行第3列来查看
使用cross_val_predict()函数,与cross_val_score()函数一样,cross_val_predict()函数同样执行K-fold交叉验证,但返回的不是评估分数,而是每个折叠的预测。
可以使用confusion_matrix()函数来获取混淆矩阵,只需要给出目标类别(y_train_5)和预测类别(y_train_pred)即可:
混淆矩阵中的行表示实际类别,列表示预测类别。本例中第一行表示所有“非5”(负类)的图片中:52419张被正确地分为“非5”(真负类),2160张被错误地分类成了5(假正类);第二行表示所有5(正类)的图片中:811被错误地分为“非5”(假负类),4610张被正确地分为5(真正类)。一个完美的分类器只有真正类和真负类,所以它的混淆矩阵只会在其对角线(左上到右下)上有非零值:
正类预测的准确率也称为分类器的精度:
精度=TP/(TP+FP) (TP为真正类数量,FP为假正类数量)
精度通常和召回率一起使用,召回率又称真正类率,是分类器正确检测到的正类实例的比率:
召回率=TP/(TP+FN) (FN为假负类数量)
当它说一张图片是5时,只有68%的时间是准确的,并且只有85%的数字5被它检测出来了
可将精度和召回率组合成一个单一的指标:F1分数。F1分数是精度和召回率的谐波平均值。正常的平均值平等对待所有的值,而谐波平均值会给予较低的值更高的权重。
可调用f1_score()计算F1分数:
F1分数对那些具有相近的精度和召回率的分类器更为有利。这不一定能符合你的期望,在某些情况下,更关心的是精度(不希望有负类混进来),而另一些情况下,更关心的是召回率(不希望有正类被放跑,没有检测到)
先看看SGDClassifier如何进行分类决策。对于每个实例,它会基于决策函数计算出一个分值,若该值大于阈值,则将该实例判为正类,否则便将其判为负类。
下图显示了从左边最低分到右边最高分的几个数字,若提高决策阈值,精度提升,召回率降低;若降低决策阈值,召回率增加,精度降低
Scikit-Learn不允许直接设置阈值,但是可以访问它用于预测的决策分数。调用分类器的decision_function()方法可返回每个实例的分数,然后可以根据这些分数,使用任意阈值进行预测
SGDClassifier分类器使用的阈值是0,所以前面的代码返回结果与predict()方法一样(True)。来试试提升阈值:
提高阈值后降低了召回率,错过了这张图
使用cross_val_predict()函数获取训练集中所有实例的分数,这次需要它返回决策分数而不是预测结果:
有了这些分数,可以使用precision_recall_curve()函数来计算所有可能的阈值的精度和召回率:
最后,用Matplotlib绘制精度和召回率相对于阈值的函数图:
精度曲线比召回率曲线要崎岖不平,因为当提高阈值时,精度有时有可能下降(尽管总体趋势是上升的)。当阈值上升时,召回率只会下降,召回率曲线很平滑
假如你想要90%的精度,通过第一张图,需要阈值约7000。要进行预测(在训练集上),除了调用分类器的predict()方法,也可以运行下列代码:
检查一下这些预测结果的精度和召回率:
然而,若召回率太低,精度再高也不怎么有用
还有一种与二元分类器一起使用的工具,称为受试者工作特征曲线(ROC),它绘制的是真正类率(召回率)和假正类率(FPR)。FPR是被错误分为正类的负类实例比率。它等于1减去真负类率(TNR)。
要绘制ROC曲线,首先需要使用roc_curve()函数计算多种阈值的TPR和FPR:
绘制FPR对TPR的曲线:
召回率(TPR)越高,分类器产生的假正类(FPR)就越多。虚线表示纯随机分类器的ROC曲线;一个优秀的分类器应该离这条线越远越好(向左上角)
一种比较分类器的方法是测量曲线下面积(AUG)。完美的分类器的ROC AUC等于1,而纯随机分类器的ROC AUG等于0.5。Scikit-learn提供计算ROC AUC的函数:
当正类非常少或者更关注假正类而不是假负类时,应选择精度/召回率(PR)曲线,反之则是ROC曲线
训练一个RandomForestClassifier分类器,并比较它和SGDClassifier分类器的ROC曲线和ROC AUC分数。首先,获取训练集中每个实例的分数。但是由于它的工作方式不同,RandomForestClassifier类没有decision_function()方法,相反,它有的是dict_proba()方法。Scikit-learn的分类器通常都有这两种方法的其中一种。dict_proba()方法会返回一个数组,其中每行为一个实例,每列代表一个类别,意思是某个给定实例属于某个给定类别的概率:
但要绘制ROC曲线,需要的是分数值而不是概率大小。一个简单的解决方案是:直接用正类的概率作为分数值:
RandomForestClassifier的ROC曲线比SGDClassifier好
二元分类器在两个类别中区分,而多类别分类器(多项分类器)可以区分两个以上的类别
有一些算法(如随机森林分类器或朴素贝叶斯分类器)可以直接处理多个类别。也有一些严格的二元分类器(如支持向量机分类器或线性分类器)。但是,有多种策略可以让你用几个二元分类器实现多类别分类的目的
例如,要创建一个系统将数字图片分为10类(0~9),一种方法是训练10个二元分类器,每个数字一个。然后,当需要对一张图片进行检测分类时,获取每个分类器的决策分数,哪个分类器给分最高,就将其分为哪个类。这称为一对多(OvA)策略(也称为one-versus-the-rest)
另一种方法是,为每一对数字训练一个二元分类器:一个用于区分0和1,一个区分0和2,一个区分1和2,依次类推。这称为一对一(OvO)策略。若存在N个类别,则需要训练Nx(N-1)/2个分类器。对于MNIST问题,当需要对一张图片进行分类时,需要运行45个分类器来对图片进行分类,最后看哪个类别获胜最多。OvO的优点在于,每个分类器只需要用到部分训练集对其必须区分的两个类别进行训练。
有些算法(例如支持向量机分类器)在数据规模扩大时表现糟糕,因此对于这类算法,OvO是一个优先的选择,由于在较小训练集上分别训练多个分类器比在大型数据集上训练少数分类器要快得多。但对于大多数二元分类器来说,OvA策略是更好的选择
Scidit-learn可以检测到你尝试使用二元分类算法进行多类别分类任务,它会自动运行OvA(SVM分类器除外,它会使用OvO)。用SGDClassifier试试:
这段代码使用原始目标类别0~9进行训练。在内部,Scikit-learn实际上训练了10个二元分类器,获得它们对图片的决策分数,然后选择了分数最高的类别。调用decision_function()方法,它会返回10个分数,每个类别1个:
目标类别的列表会存储在classes_这个属性中,按值的大小排序
若想要强制Scikit-learn使用一对一或一对多策略,可以使用OneVsOneClassifier或OneVsRestClassifier类。只需要创建一个实例,然后将二元分类器传给其构造函数。
训练RandomForestClassifier:
这次Scikit-learn不必运行OvA或者OvO了,因为随机森林分类器直接可以将实例分为多个类别。调用predict_proba()可以获得分类器将每个实例分类为每个类别的概率列表:
使用cross_val_score()函数评估SGDClassifier的准确率:
在所有测试折叠上都超过了83%
将输入进行简单缩放,可以提高准确率:
首先,看看混淆矩阵。
使用Matplotlib的matshow()函数来查看混淆矩阵的图像表示:
混淆矩阵看起来还不错,因为大多数图片都在主对角线上,这说明它们被正确分类
将混淆矩阵中的每个值除以相应类别中的图片数量,这样比较的是错误率而不是错误的绝对值
用0填充对角线,只保留错误,重新绘制:
每行代表实际类别,每列表示预测类别。第8列非常亮,说明有许多图片被错误地分类为数字8了。类别8和9的行也偏亮,说明数字8和9经常会跟其他数字混淆。行1和2很暗,大多数数字1和2都被正确分类(仅仅有一些与数字8弄混)。
分析单个的错误也可以为分类器提供洞察,例如,来看数字3和5的例子:
左侧的两个矩阵为被分类为数字3的图片,右侧的矩阵为分类为数字5的图片。左下方和右上方的矩阵里,出现了一些看起来非常明显的错误,因为我们使用的简单的SGDClassifier模型是一个线性模型,它所做的就是为每个像素分配一个各个类别的权重,当它看到新的图像时,将加权后的像素强度汇总,从而得到一个分数进行分类。而数字3和数字5只在一部分像素位上有区别,所以分类器很容易将其弄混
在某些情况下,希望分类器为每个实例产出多个类别。例如,人脸识别的分类器:若在一张照片里识别出多个人,应该为识别出来的每个人都附上一个标签。假设分类器经过训练,已经可以识别出三张脸:Alice、Bob、Chali,当看到一张Alice和Chali的照片时,应该输出[1,0,1](是Alice,不是Bob,是Chali)。这种输出多个二元标签的分类系统称为多标签分类系统
这段代码会创建一个y_multilabel数组,其中包含两个数字图片的目标标签:第一个表示数字是否是大数(>=7),第二个表示是否为奇数。创建一个KNeighborsClassifier实例(它支持多标签分类,不是所有的分类器都支持)。
结果是正确的
评估多标签分类器的方法有很多,比如方法之一是测量每个标签的F1分数(或是其他二元分类器指标),然后简单地平均
这里假设了所有的标签都同等重要。也可给每个标签设置一个等于其自身支持的权重(即具有该目标标签的实例的数量),只需在上面的代码中设置average=weighted
多输出-多类别分类(多输出分类)是多标签分类的泛化,其标签也可以是多种类别的(比如可以有两个以上可能的值)
多输出分类器系统的例子:构建一个系统去除图片中的噪声,给它输入一张有噪声的图片,它将输出一张干净的数字图片,根其他MNIST图片一样,以像素强度的一个数组作为呈现方式。该分类器的输出是多个标签(一个像素点一个标签),每个标签可以有多个值(像素强度范围为0到255)
使用numpy的randint()函数为MNIST图片的像素强度增加噪声
应用梯度下降时,需保证所有特征值的大小比例都差不多(如使用Scikit-learn的StandardScaler类),否则收敛的时间会长很多
在计算梯度下降的每一步时,都基于完整的训练集,即每一步都使用整批训练数据。
随机梯度下降每一步在训练集中随机选择一个实例,并且仅基于该单个实例来计算梯度
当成本函数非常不规则时,随机梯度下降可以帮助算法跳出局部最小值。随机性的好处在于可以逃离局部最优,但缺点是永远定位不出最小值。解决该问题的方法之一是逐步降低学习率,开始的步长比较大(这有助于快速进展和逃离局部最小值),然后越来越小,让算法尽量靠近全局最小值。这个过程叫做模拟退火,因为它类似于冶金时熔化的金属慢慢冷却的退火过程。
每一步的梯度计算,既不是基于整个训练集(批量梯度下降)也不是基于单个实例(随机梯度下降),而是基于一小部分随机的实例集也就是小批量。
比较一下目前讨论过的线性回归算法(m为训练实例的数量,n是特征数量)
可以用线性模型来拟合非线性数据。一个简单的方法是将每个特征的幂次方添加为一个新特征,然后在这个拓展过的特征集上训练线性模型。这种方法被称为多项式回归
基于简单的二次方程制造一些非线性数据(添加随机噪声):
使用Scikit-learn的PolynomialFeatures类来对训练数据进行转换,将每个特征的平方作为新特征加入训练集:
X_poly现在包含原本的特征X和该特征的平方。对这个拓展后的训练集匹配一个LinearRegression模型:
学习曲线绘制的是模型在训练集和验证集上,关于训练集大小的性能函数。要生成该曲线,需在不同大小的训练子集上多次训练模型。下面的代码在给定训练集下定义了一个函数,绘制模型的学习曲线:
看看纯线性回归模型的学习曲线:
该学习曲线是典型的欠拟合,两条曲线均达到高地,非常接近,并且相当高
若模型欠拟合,添加训练示例于事无补,需要使用更复杂的模型或找到更好的特征
现在再来看看在同样的数据集上,一个10阶多项式模型的学习曲线:
训练数据的误差远低于线性回归模型,两条曲线之间有一定差距,这意味着该模型在训练数据上的表现比验证集上要好很多,这正是过拟合的标志。
改进模型过拟合的方法之一是提供更多的训练数据,直到验证误差接近训练误差
偏差/方差权衡:
模型的泛化误差可以被表示为三个截然不同的误差之和:
偏差
这部分泛化误差的原因在于错误的假设,比如假设数据是线性的,而实际上是二次的。高偏差模型最有可能欠拟合
方差
这部分误差是由于模型对训练数据的微小变化过度敏感导致的。具有高自由度的模型(例如高阶多项式模型)很可能也有高方差,很容易过拟合
不可避免的误差
这部分误差是因为数据本身的噪声所致,减少这部分误差的唯一方法是清理数据
增加模型的复杂度通常会显著提升模型的方差,减少偏差。降低模型的复杂度则会提升模型的偏差,降低方差
减少过拟合的一个方法是对模型正则化(即约束它):它拥有的自由度越低,就越不容易过拟合数据。
对线性模型来说,正则化通常通过约束模型的权重来实现。
岭回归是线性回归的正则化版:在成本函数中添加一个等于
的正则项。正则项只能在训练的时候添加到成本函数中,一旦训练完成,需使用未经正则化的性能指标来评估模型性能
超参数α控制的是模型进行正则化的程度。若α=0,则岭回归就是线性模型。若α非常大,那么所有的权重都将非常接近于0,结果是一条穿过数据平均值的水平线
使用Scikit-learn执行闭式解的岭回归:
使用随机梯度下降:
超参数penalty设置的是使用正则项的类型。设为l2表示SGD在成本函数中添加的正则项等于权重向量的l2范数的平方的一半,即岭回归
Lasso回归(套索回归)增加的正则项是权重向量的l1范数
Lasso回归的一个重要特点是它倾向于完全消除掉最不重要特征的权重(也即将它们设置为0)。Lasso回归会自动执行特征选择并输出一个稀疏模型(即只有很少的特征有非零权重)
弹性网络的正则项是岭回归和Lasso回归的正则项的混合,混合比例通过r来控制。当r=0时,弹性网络等同于岭回归,当r=1时,相当于Lasso回归
岭回归是不错的默认选择,若觉得实际用到的特征只有少数几个,应更倾向于Lasso回归或弹性网络,因为它们将无用特征的权重降为0。一般而言,弹性网络优于Lasso回归,因为当特征数量超过训练实例数量,又或者是几个特征强相关时,Lasso回归的表现可能非常不稳定
对于梯度下降这一类迭代学习的算法,还有一个与众不同的正则化方法,就是在验证误差达到最小值时停止训练,该方法称为早期停止法。
当warm_start=True时,调用fit()方法,会从停下的地方继续开始训练,而不会重新开始
使用鸢尾植物数据集来说明逻辑回归,150朵鸢尾花分别来自3个不同品种:Setosa鸢尾花、Versicolor鸢尾花和Virginica鸢尾花,数据里包含花的萼片以及花瓣的长度和宽度
试试仅基于花瓣宽度这一特征,创建一个分类器来检测Virginica鸢尾花。首先加载数据:
训练逻辑回归模型:
来看看对于花瓣宽度在0到3cm之间的鸢尾花,模型估算出的概率:
在大约1.6cm处存在一个决策边界,这里是和不是的可能性都是50%,若花瓣宽度大于1.6cm,分类器就预测它为Virginica鸢尾花,否则就预测不是(即使它没什么把握):
使用两个特征,花瓣宽度和花瓣长度,虚线表示模型的决策边界,每条平行线都分别代表一个模型输出的特定概率
与其他线性模型一样,逻辑回归模型可以用l1或l2惩罚函数来正则化。Scikit-learn默认添加的是l2函数。控制Scikit-Learn的LogisticRegression模型正则化的超参数不是alpha(其他线性模型使用alpha),而是它的逆反:C,C的值越高,模型正则化程度越高
逻辑回归模型经过推广,可以直接支持多个类别,而不需要训练并组合多个二元分类器。这就是Softmax回归,或称多元逻辑回归
Softmax回归分类器一次只会预测一个类别(即,它是多类别但不是多输出),所以它应仅适用于互斥的类别之上,例如植物的不同种类,不能用它来识别一张照片中的多个人
来使用Softmax回归将鸢尾花分为3类,当用两个以上的类别训练时,Scikit-learn的LogisticRegression默认选择使用的是一对多的训练方式,将超参数multi_class设置为multinomial,可以将其切换成Softmax回归。还必须指定一个支持Softmax回归的求解器,如lbfgs求解器。默认使用l2正则化,可通过超参数C进行控制: