1

泛化&泛化数据集&实验

泛化 (Generalization):过拟合的风险


  • 泛化:泛化能力(generalization ability)是指机器学习算法对新鲜样本的适应能力。学习的目的是学到隐含在数据对背后的规律,对具有同一规律的学习集以外的数据,经过训练的网络也能给出合适的输出,该能力称为泛化能力。

前两篇实现了线性回归模型得训练,并再最后进行了采用合成特征作为特征输入,且过滤了离群值,效果似乎不错,但是这个不错的结果来自于我们采用了原来的训练数据进行的测试,这样的评估意义大吗?如果我们使用了新的(训练模型从未遇见过的)数据进行预测,得到的结果可能会不敬人意。
假设我对模型不断的进行训练,不断调整超参数,经过若干天后,得到的模型能够达到完美的预测效果,对于输入的任意特征都能得到正确的target,但是这并不是我们想要的模型,因为其只达到了对训练数据的完美贴合,但并不一定能对它从未见过的新数据做出正确的预测,并且其训练了过久,模型也会变的过于复杂,这样就叫做过拟合

大概过程如下图(图片来源于谷歌机器学习样图):
为了让您直观地理解这一概念,我们将展示 3 张图。假设这些图中的每个点代表一棵树在森林中的位置。图中的两种颜色分别代表以下含义:

  • 蓝点代表生病的树。
  • 橙点代表健康的树。

GeneralizationA.png

图 1. 生病(蓝色)和健康(橙色)的树。

您能设想出一个有效的模型来预测以后的生病或健康的树吗?花点时间在脑海里绘制一条弧线将蓝点与橙点分开,或者在脑海中圈住一些橙点或蓝点。然后再看看图 2,它显示某种机器学习模型如何将生病的树与健康的树区分开。请注意,该模型产生的损失非常低。

GeneralizationB.png

图 2. 用于区分生病的树与健康的树的复杂模型。
乍一看,图 2 所示的模型在将健康的树与生病的树区分开方面似乎表现得非常出色。真的是这样吗?

GeneralizationC.png

图 3. 该模型在预测新数据方面表现非常糟糕。
图 3 显示我们向该模型中添加了新数据后所发生的情况。结果表明,该模型在处理新数据方面表现非常糟糕。请注意,该模型对大部分新数据的分类都不正确。


图 2 和图 3 所示的模型过拟合了训练数据的特性。过拟合模型在训练过程中产生的损失很低,但在预测新数据方面的表现却非常糟糕。如果某个模型在拟合当前样本方面表现良好,那么我们如何相信该模型会对新数据做出良好的预测呢?正如您稍后将看到的,过拟合是由于模型的复杂程度超出所需程度而造成的。机器学习的基本冲突是适当拟合我们的数据,但也要尽可能简单地拟合数据。

一种解决方法是将您的数据集分成两个子集:

  • 训练集 - 用于训练模型的子集。
  • 测试集 - 用于测试模型的子集。

一般来说,在-测试集上表现是否良好是衡量能否在新数据上表现良好的有用指标,前提是:

  • 测试集足够大。
  • 不会反复使用相同的测试集来作假。

我们从分布中随机抽取样本,且这些样本是独立同分布 (i.i.d) 的

  • 独立同分布:1、每次抽取是随机的,不被其他条件所约束 2、所抽取的样本是同分布的,即满足同一个分布规律(如,抽纸牌,得从同样得一副牌中抽取)3、分布是平稳的,即分布在数据集内不会发生变化

接下来我们来谈谈训练集测试集

训练集和测试集 (Training and Test Sets):拆分数据


  • 训练集 - 用于训练模型的子集。
  • 测试集 - 用于测试训练后模型的子集

如果我们只有一个数据集,但是需要训练集测试集,办法很简单,拆分就好了,比例大概可以是4:1这样子

不过,拆分数据时候得遵循几个原则
1.数据集够大
2.随机抽取的,能代表数据集的水平

注意请勿对测试数据进行训练,如果最后测试得到的结果意外的好,那最好检查一下,多数是因为对测试数据进行了误训练

划分A.png

这是我们刚才讨论的划分方式--训练集和测试集

但是这样的划分方式,会不会有问题呢?如果为了在最后的测试数据上获得最佳效果,从而更改学习速率、添加或移除特征,到从头开始设计全新模型。当该工作流程结束时,在测试数据上表现很好,那么最终依然很有可能是过拟合的。

所以我们可以进一步划分,即训练集+验证集合+测试集合
划分BB.png

划分B.png

在这一经过改进的工作流程中:

  1. 选择在验证集上获得最佳效果的模型。
  2. 使用测试集再次检查该模型。

该工作流程之所以更好,原因在于它暴露给测试集的信息更少

接下来,我们将对之前的理论进行验证

验证

  • 使用多个特征而非单个特征来进一步提高模型的有效性
  • 调试模型输入数据中的问题
  • 使用测试数据集检查模型是否过拟合验证数据

与在之前的练习中一样,我们将依然使用加利福尼亚州住房数据集,尝试根据 1990 年的人口普查数据在城市街区级别预测 median_house_value

设置和测试

我们首先加载并准备数据。这一次,我们将使用多个特征,因此我们会将逻辑模块化,以对特征进行预处理:


import math

from IPython import display
from matplotlib import cm
from matplotlib import gridspec
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
from sklearn import metrics
import tensorflow as tf
from tensorflow.python.data import Dataset

tf.logging.set_verbosity(tf.logging.ERROR)
pd.options.display.max_rows = 10
pd.options.display.float_format = '{:.1f}'.format

california_housing_dataframe = pd.read_csv("https://storage.googleapis.com/mledu-datasets/california_housing_train.csv", sep=",")

# california_housing_dataframe = california_housing_dataframe.reindex(
#     np.random.permutation(california_housing_dataframe.index))

# 对特征预处理
def preprocess_features(california_housing_dataframe):
  selected_features = california_housing_dataframe[
    ["latitude",
     "longitude",
     "housing_median_age",
     "total_rooms",
     "total_bedrooms",
     "population",
     "households",
     "median_income"]]
  processed_features = selected_features.copy()
  # 此外多创建一个合成特征
  processed_features["rooms_per_person"] = (
    california_housing_dataframe["total_rooms"] /
    california_housing_dataframe["population"])
  return processed_features
# 对target预处理
def preprocess_targets(california_housing_dataframe):
# output_targets为pd.DataFrame()类型的数据结构(这种结构类似于表格,有行有列的索引)
  output_targets = pd.DataFrame()
  output_targets["median_house_value"] = (
    california_housing_dataframe["median_house_value"] / 1000.0)
  return output_targets

# 训练集取前12000(共17000样本)
training_examples = preprocess_features(california_housing_dataframe.head(12000))
training_examples.describe()
latitude longitude housing_median_age total_rooms total_bedrooms population households median_income rooms_per_person
count 12000.0 12000.0 12000.0 12000.0 12000.0 12000.0 12000.0 12000.0 12000.0
mean 34.6 -118.5 27.5 2655.7 547.1 1476.0 505.4 3.8 1.9
std 1.6 1.2 12.1 2258.1 434.3 1174.3 391.7 1.9 1.3
min 32.5 -121.4 1.0 2.0 2.0 3.0 2.0 0.5 0.0
25% 33.8 -118.9 17.0 1451.8 299.0 815.0 283.0 2.5 1.4
50% 34.0 -118.2 28.0 2113.5 438.0 1207.0 411.0 3.5 1.9
75% 34.4 -117.8 36.0 3146.0 653.0 1777.0 606.0 4.6 2.3
max 41.8 -114.3 52.0 37937.0 5471.0 35682.0 5189.0 15.0 55.2
training_targets = preprocess_targets(california_housing_dataframe.head(12000))
training_targets.describe()
median_house_value
count 12000.0
mean 198.0
std 111.9
min 15.0
25% 117.1
50% 170.5
75% 244.4
max 500.0
# 测试集取尾5000
validation_examples = preprocess_features(california_housing_dataframe.tail(5000))
validation_examples.describe()
latitude longitude housing_median_age total_rooms total_bedrooms population households median_income rooms_per_person
count 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0
mean 38.1 -122.2 31.3 2614.8 521.1 1318.1 491.2 4.1 2.1
std 0.9 0.5 13.4 1979.6 388.5 1073.7 366.5 2.0 0.6
min 36.1 -124.3 1.0 8.0 1.0 8.0 1.0 0.5 0.1
25% 37.5 -122.4 20.0 1481.0 292.0 731.0 278.0 2.7 1.7
50% 37.8 -122.1 31.0 2164.0 424.0 1074.0 403.0 3.7 2.1
75% 38.4 -121.9 42.0 3161.2 635.0 1590.2 603.0 5.1 2.4
max 42.0 -121.4 52.0 32627.0 6445.0 28566.0 6082.0 15.0 18.3
validation_targets = preprocess_targets(california_housing_dataframe.tail(5000))
validation_targets.describe()
median_house_value
count 5000.0
mean 229.5
std 122.5
min 15.0
25% 130.4
50% 213.0
75% 303.2
max 500.0

在上面我们是把数据集中除了median_house_value作为target外,其他得所有列都作为了feature输入
我们虽然没有什么数据分析或者统计学得背景知识,但是其实还是可以看一看,或者说猜一猜输入的这些特征到底是些什么,它们可能具有哪些意义?取值范围是怎么样的,数值的大小正常嘛?等等等

latitude:纬度
longitude:经度
housing_median_age:房子年纪(中值)
total_rooms:房间总数(每个街区)
total_bedrooms:卧室总数(每个街区)
total_bedrooms:人口数(每个街区)
households:户(一家人为一户)
median_income :收入(中值)

再看看这些feature的数值(最大值,最小值),理解一下它们的单位(当然,会有不合理的,因为存在数据比较特殊,而且这是1990年的数据了),这些数据会帮助我们在宏观上了解这些数据集,特别是一旦发现不合理的地方更是要注意了(尤其在训练自己的数据时)

异常情况:

  • median_income 位于 3 到 15 的范围内。我们完全不清楚此范围究竟指的是什么,看起来可能是某对数尺度?无法找到相关记录;我们所能假设的只是,值越高,相应的收入越高。
  • median_house_value 的最大值是 500001。这看起来像是某种人为设定的上限。
  • rooms_per_person 特征通常在正常范围内,其中第 75 百分位数的值约为 2。但也有一些非常大的值(例如 18 或 55),这可能表明数据有一定程度的损坏。

我们将暂时使用提供的这些特征。但希望这些示例可帮助您较为直观地了解如何检查来自未知来源的数据

### 绘制纬度/经度与房屋价值中位数的曲线图

    • *

    我们来详细了解一下 latitudelongitude 这两个特征。它们是相关城市街区的地理坐标。

利用这两个特征可以提供出色的可视化结果 - 我们来绘制 latitudelongitude 的曲线图,然后用颜色标注 median_house_value

plt.figure(figsize=(13, 8))

ax = plt.subplot(1, 2, 1)
ax.set_title("Validation Data")

# 取消y轴的自动缩放,并定义上下限
ax.set_autoscaley_on(False)
ax.set_ylim([32, 43])

# 取消x轴的自动缩放,并定义上下限
ax.set_autoscalex_on(False)
ax.set_xlim([-126, -112])

# plt.scatter()参数说明
# validation_examples["longitude"],validation_examples["latitude"]:代表x,y
# cmap:Colormap,颜色表
# c:color(色彩,或者颜色序列)
plt.scatter(validation_examples["longitude"],
            validation_examples["latitude"],
            cmap="coolwarm",
            c=validation_targets["median_house_value"] / validation_targets["median_house_value"].max())

ax = plt.subplot(1,2,2)
ax.set_title("Training Data")

ax.set_autoscaley_on(False)
ax.set_ylim([32, 43])
ax.set_autoscalex_on(False)
ax.set_xlim([-126, -112])
plt.scatter(training_examples["longitude"],
            training_examples["latitude"],
            cmap="coolwarm",
            c=training_targets["median_house_value"] / training_targets["median_house_value"].max())
_ = plt.plot()

坐标_housevalue.png

当当当当,这个图的问题很明显呢,训练和测试的数据差异很大呢,问题在哪里呢? 都是一个数据集的哎,其实就是处理数据的时候,忘记了随机排序了(再次说明打乱顺序真的很重要,我们永远无法预知原本序列的数据可能出现哪些问题),此外采用图标分析也很重要,能够在训练之前帮助我们发现问题,不然后面就完蛋啦,怎么训练都不会有好的结果的。

调整后的结果

坐标_housevalue1.png

现在很像一个地图了

训练和评估模型


我们会使用数据集中的所有特征训练一个线性回归器,定义一下以前将数据加载到 TensorFlow 模型中时所使用的同一输入函数

def my_input_fn(features, targets, batch_size=1, shuffle=True, num_epochs=None):  
    features = {key:np.array(value) for key,value in dict(features).items()}   
                                        
    ds = Dataset.from_tensor_slices((features,targets)) # warning: 2GB limit
    ds = ds.batch(batch_size).repeat(num_epochs)

    if shuffle:
      ds = ds.shuffle(10000)
    
    features, labels = ds.make_one_shot_iterator().get_next()
    return features, labels

由于我们现在使用的是多个输入特征,因此需要把用于将特征列配置为独立函数的代码模块化。(目前此代码相当简单,因为我们的所有特征都是数值,但当我们在今后的练习中使用其他类型的特征时,会基于此代码进行构建。)

def construct_feature_columns(input_features):
  return set([tf.feature_column.numeric_column(my_feature)
              for my_feature in input_features])

接下来,继续完成下面的 train_model() 代码,以设置输入函数和计算预测。
但要确保针对相应数据集调用 predict()比较训练数据和验证数据的损失

def train_model(
    learning_rate,
    steps,
    batch_size,
    training_examples,
    training_targets,
    validation_examples,
    validation_targets):

  periods = 10
  steps_per_period = steps / periods
  
  # 创建线性回归模型并设定好特征列和优化器
  my_optimizer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
  my_optimizer = tf.contrib.estimator.clip_gradients_by_norm(my_optimizer, 5.0)
  linear_regressor = tf.estimator.LinearRegressor(
      feature_columns=construct_feature_columns(training_examples),
      optimizer=my_optimizer
  )
  
  # 创建训练、预测(使用训练集的数据)、验证(使用验证集的数据)的输入函数
  training_input_fn = lambda: my_input_fn(
      training_examples, 
      training_targets["median_house_value"], 
      batch_size=batch_size)
  predict_training_input_fn = lambda: my_input_fn(
      training_examples, 
      training_targets["median_house_value"], 
      num_epochs=1, 
      shuffle=False)
  predict_validation_input_fn = lambda: my_input_fn(
      validation_examples, validation_targets["median_house_value"], 
      num_epochs=1, 
      shuffle=False)

  # 训练(周期性输出结果)
  print "Training model..."
  print "RMSE (on training data):"
  training_rmse = []
  validation_rmse = []
  for period in range (0, periods):
    # 按照训练steps进行周期性训练
    linear_regressor.train(
        input_fn=training_input_fn,
        steps=steps_per_period,
    )
    # 记录预测值(分别使用训练集和验证集)
    training_predictions = linear_regressor.predict(input_fn=predict_training_input_fn)
    training_predictions = np.array([item['predictions'][0] for item in training_predictions])
    
    validation_predictions = linear_regressor.predict(input_fn=predict_validation_input_fn)
    validation_predictions = np.array([item['predictions'][0] for item in validation_predictions])
    
    
    # 计算RMSE(使用训练值和验证值)
    training_root_mean_squared_error = math.sqrt(
        metrics.mean_squared_error(training_predictions, training_targets))
    validation_root_mean_squared_error = math.sqrt(
        metrics.mean_squared_error(validation_predictions, validation_targets))

    print "  period %02d : %0.2f" % (period, training_root_mean_squared_error)

    training_rmse.append(training_root_mean_squared_error)
    validation_rmse.append(validation_root_mean_squared_error)
  print "Model training finished."

  plt.ylabel("RMSE")
  plt.xlabel("Periods")
  plt.title("Root Mean Squared Error vs. Periods")
  plt.tight_layout()
  plt.plot(training_rmse, label="training")
  plt.plot(validation_rmse, label="validation")
  # plt.legend()画图例,图中右上角
  plt.legend()

  return linear_regressor

训练...

linear_regressor = train_model(
    learning_rate=0.00003,
    steps=500,
    batch_size=5,
    training_examples=training_examples,
    training_targets=training_targets,
    validation_examples=validation_examples,
    validation_targets=validation_targets)

因为使用的features较多,要耐心等一会儿就可以看到使用训练值和校验值的区别了,以及多特征的效果怎么样

Training model...
RMSE (on training data):
period 00 : 217.67
period 01 : 201.13
period 02 : 186.67
period 03 : 176.46
period 04 : 170.31
period 05 : 167.41
period 06 : 166.75
period 07 : 166.49
period 08 : 167.72
period 09 : 169.76
Model training finished.
learning_rate=0.00003
0.00003.png

要注意这是两条线哦,还有一条是校验集的结果,真的是很贴合了哎,说明模型效果挺好的

上面的learning_rate=0.00003,最终的rmse为169.76,这比上次的合成特征的高了好多,所以下面尝试修改learning_rate
learning_rate=0.00015

0.00015.png

learning_rate=0.005

0.005.png

learning_rate=0.05

0.05.png

我们会发现这次的损失函数曲线好像很复杂的样子。。。。emmmmmmm,其实到这里,我还没有调好,不过不管啦,下一篇我会说明一下,多特征的时候怎么来调节超参数(其实我只是比较懒.....)

最后,再看看以学习率为0.00003训练出来的模型,遇到测试集的效果吧

california_housing_test_data = pd.read_csv("https://storage.googleapis.com/mledu-datasets/california_housing_test.csv", sep=",")

test_examples = preprocess_features(california_housing_test_data)
test_targets = preprocess_targets(california_housing_test_data)

predict_test_input_fn = lambda: my_input_fn(
      test_examples, 
      test_targets["median_house_value"], 
      num_epochs=1, 
      shuffle=False)

test_predictions = linear_regressor.predict(input_fn=predict_test_input_fn)
test_predictions = np.array([item['predictions'][0] for item in test_predictions])

root_mean_squared_error = math.sqrt(
    metrics.mean_squared_error(test_predictions, test_targets))

print "Final RMSE (on test data): %0.2f" % root_mean_squared_error

Final RMSE (on test data): 162.84
结果很接近了呢,说明没有过拟合。

讲完。


风吹花落念白衣
180 声望60 粉丝

当个树洞,不定期更新~