TensorFlow从1到2(七)线性回归模型预测汽车油耗以及训练过程优化

线性回归模型

“回归”这个词,既是Regression算法的名称,也表明了不一样的计算结果。固然结果也是由算法决定的。
不一样于前面讲过的多个分类算法或者逻辑回归,线性回归模型的结果是一个连续的值。
实际上咱们第一篇的房价预测就属于线性回归算法,若是把这个模型用于预测,结果是一个连续值而不是有限的分类。
从代码上讲,那个例子更多的是为了延续从TensorFlow 1.x而来的解题思路,我不想在这个系列的第一篇就给你们印象,TensorFlow 2.0成为了彻底不一样的另外一个东西。在TensorFlow 2.0中,有更方便的方法能够解决相似问题。
回归算法在大多数机器学习课程中,也都是最先会学习的算法。因此对这个算法,咱们都不陌生。
所以本篇的重点不在算法自己,也不在油耗的预测,而是经过油耗预测这样简单的例子,介绍在TensorFlow 2.0中,如何更好的对训练过程进行监控和管理,还有其它一些方便有效的小技巧。python

了解样本数据

在机器学习算法自己没有大的突破的状况下,对样本数据的选取、预处理每每是项目成功的关键。咱们接续前一篇继续说一说样本数据。
样本数据的预处理依靠对样本数据的了解和分析。Python的交互模式配合第三方工具包则是对样本数据分析的强力武器。
下面咱们使用Python的交互模式,载入油耗预测的样本数据,先直观的看一下样本数据:
(第一次载入样本数据会从网上下载,速度比较慢)算法

$ python3
Python 3.7.3 (default, Mar 27 2019, 09:23:39) 
[Clang 10.0.0 (clang-1000.11.45.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from tensorflow import keras
>>> import pandas as pd
>>> dataset_path = keras.utils.get_file("auto-mpg.data", "http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")
>>> column_names = ['MPG','Cylinders','Displacement','Horsepower','Weight',
...                 'Acceleration', 'Model Year', 'Origin']
>>> raw_dataset = pd.read_csv(dataset_path, names=column_names,
...                       na_values = "?", comment='\t',
...                       sep=" ", skipinitialspace=True)
>>> 
>>> raw_dataset.tail()
      MPG  Cylinders  Displacement  Horsepower  Weight  Acceleration  Model Year  Origin
393  27.0          4         140.0        86.0  2790.0          15.6          82       1
394  44.0          4          97.0        52.0  2130.0          24.6          82       2
395  32.0          4         135.0        84.0  2295.0          11.6          82       1
396  28.0          4         120.0        79.0  2625.0          18.6          82       1
397  31.0          4         119.0        82.0  2720.0          19.4          82       1
>>> raw_dataset
      MPG  Cylinders  Displacement  Horsepower  Weight  Acceleration  Model Year  Origin
0    18.0          8         307.0       130.0  3504.0          12.0          70       1
1    15.0          8         350.0       165.0  3693.0          11.5          70       1
2    18.0          8         318.0       150.0  3436.0          11.0          70       1
3    16.0          8         304.0       150.0  3433.0          12.0          70       1
..    ...        ...           ...         ...     ...           ...         ...     ...
373  24.0          4         140.0        92.0  2865.0          16.4          82       1
374  23.0          4         151.0         NaN  3035.0          20.5          82       1
..    ...        ...           ...         ...     ...           ...         ...     ...
396  28.0          4         120.0        79.0  2625.0          18.6          82       1
397  31.0          4         119.0        82.0  2720.0          19.4          82       1

[398 rows x 8 columns]
>>>

本篇就不用表格来逐行解释属性列的含义了,咱们会在用到的每一列单独说明。
使用raw_dataset.tail()列出数据的最后几行,显示数据一共只有398行。说明数据集并非很大,能够直接所有列出来粗略的看一下。这一步使用Excel之类的工具效果可能更好。不过习惯命令行操做的工程师直接列出也是同样的。
数据中能够看到第374行,在Horsepower(发动机功率)一列,意外的有NaN未知数据。这样的数据固然是无效的,须要首先进行数据清洗。大数据转行过来的技术人员都熟悉,数据清洗是保证数据有效性必不可少的手段。
其实这里的NaN并不能彻底说意外,咱们在使用Pandas打开数据集的时候使用了参数:na_values = "?",这是指数据集中若是有“?”字符,则数据当作无效数据,方便后续使用内置方法处理。这个参数能够根据你获取的数据集修改。
好比检查数据集是否有无效数据可使用isna()方法:编程

>>> # 继续上面的交互操做
... 
>>> raw_dataset.isna().sum()
MPG             0
Cylinders       0
Displacement    0
Horsepower      6
Weight          0
Acceleration    0
Model Year      0
Origin          0
dtype: int64
>>> 
>>> # 确认有6个无效数据,须要抛弃相应行
... # 将数据复制一份,防止误操做
... 
>>> dataset = raw_dataset.copy()
>>> 
>>> # 抛弃无效数据所在行
... 
>>> dataset = dataset.dropna()
>>>

接着Origin一列,实际是一个分类类型,并非数字。分别表明车型的产地为美国、欧洲或者日本。上一篇中咱们已经有了经验,咱们要把这个数据列转成one-hot编码方式:bash

>>> # 取出Origin数据列,原数据集中将不会再有这一列
... 
>>> origin = dataset.pop('Origin')
>>> 
>>> # 根据分类编码,分别为新对应列赋值1.0
... 
>>> dataset['USA'] = (origin == 1)*1.0
>>> dataset['Europe'] = (origin == 2)*1.0
>>> dataset['Japan'] = (origin == 3)*1.0
>>> 
>>> # 列出新的数据集尾部,以观察结果
... 
>>> dataset.tail()
      MPG  Cylinders  Displacement  Horsepower  Weight  Acceleration  Model Year  USA  Europe  Japan
393  27.0          4         140.0        86.0  2790.0          15.6          82  1.0     0.0    0.0
394  44.0          4          97.0        52.0  2130.0          24.6          82  0.0     1.0    0.0
395  32.0          4         135.0        84.0  2295.0          11.6          82  1.0     0.0    0.0
396  28.0          4         120.0        79.0  2625.0          18.6          82  1.0     0.0    0.0
397  31.0          4         119.0        82.0  2720.0          19.4          82  1.0     0.0    0.0
>>>

不一样前一篇,此次作One-hot编码的方式直接使用了Python语言的逻辑计算表达式,效果同样好。
关于怎么知道哪一个数字表明哪一个产地,若是是本身设计的数据采集方式,你本身固然应当知道。若是使用了别人的数据集,应当仔细阅读数据的说明。这里就很少解释了。
咱们还可使用seaborn工具(第一篇中已经安装了)对数据作进一步分析,seaborn包含一组散列图绘制工具,能够更直观的揭示数据之间的关联:网络

>>> # 继续上面的交互操做
... 
>>> import seaborn as sns
>>> import matplotlib.pyplot as plt
>>> sns.pairplot(dataset[["MPG", "Cylinders", "Displacement", "Weight"]], diag_kind="kde")
<seaborn.axisgrid.PairGrid object at 0x139405358>
>>> plt.show()


上图选取了MPG(油耗)、Cylinders(气缸数量)、Displacement(排气量)、Weight(车重)四项数据,作两两对比造成的散点图。
散点矩阵图(SPLOM:Scatterplot Matrix)能够用于粗略揭示数据中,不一样列之间的相关性。能够粗略估计哪些变量是正相关的,哪些是负相关的,进而为下一步数据分析提供决策。固然这些图须要行业专家的理解和分析。而后为程序人员提供间接帮助。dom

数据规范化

从刚才的样本数据中,咱们能够看出各列的数据,取值范围仍是很不均衡的。在进入模型以前,咱们须要作数据规范化。也就是将全部列的数据统一为在同一个取值范围的浮点数。
咱们能够利用Pandas中对数据的统计结果作数据的规范化,这样能够省去本身写程序作数据统计。机器学习

>>> # 继续上面的交互操做
... 
>>> data_stats=dataset.describe()
>>> data_stats
              MPG   Cylinders  Displacement  Horsepower  ...  Model Year         USA      Europe       Japan
count  392.000000  392.000000    392.000000  392.000000  ...  392.000000  392.000000  392.000000  392.000000
mean    23.445918    5.471939    194.411990  104.469388  ...   75.979592    0.625000    0.173469    0.201531
std      7.805007    1.705783    104.644004   38.491160  ...    3.683737    0.484742    0.379136    0.401656
min      9.000000    3.000000     68.000000   46.000000  ...   70.000000    0.000000    0.000000    0.000000
25%     17.000000    4.000000    105.000000   75.000000  ...   73.000000    0.000000    0.000000    0.000000
50%     22.750000    4.000000    151.000000   93.500000  ...   76.000000    1.000000    0.000000    0.000000
75%     29.000000    8.000000    275.750000  126.000000  ...   79.000000    1.000000    0.000000    0.000000
max     46.600000    8.000000    455.000000  230.000000  ...   82.000000    1.000000    1.000000    1.000000

[8 rows x 10 columns]
>>>

对于每一列,Pandas都进行了记录总数、平均值、标准差、最小值等统计。咱们作数据规范化,能够直接使用这些参数来进行。函数

>>> # 继续上面的交互操做
... 
>>> data_stats=data_stats.transpose()
>>> data_stats
              count         mean         std     min       25%      50%       75%     max
MPG           392.0    23.445918    7.805007     9.0    17.000    22.75    29.000    46.6
Cylinders     392.0     5.471939    1.705783     3.0     4.000     4.00     8.000     8.0
Displacement  392.0   194.411990  104.644004    68.0   105.000   151.00   275.750   455.0
Horsepower    392.0   104.469388   38.491160    46.0    75.000    93.50   126.000   230.0
Weight        392.0  2977.584184  849.402560  1613.0  2225.250  2803.50  3614.750  5140.0
Acceleration  392.0    15.541327    2.758864     8.0    13.775    15.50    17.025    24.8
Model Year    392.0    75.979592    3.683737    70.0    73.000    76.00    79.000    82.0
USA           392.0     0.625000    0.484742     0.0     0.000     1.00     1.000     1.0
Europe        392.0     0.173469    0.379136     0.0     0.000     0.00     0.000     1.0
Japan         392.0     0.201531    0.401656     0.0     0.000     0.00     0.000     1.0
>>> norm_data = (dataset - data_stats['mean'])/data_stats['std']
>>> norm_data.tail()
          MPG  Cylinders  Displacement  Horsepower    Weight  Acceleration  Model Year       USA    Europe     Japan
393  0.455359  -0.862911     -0.519972   -0.479835 -0.220842      0.021267    1.634321  0.773608 -0.457538 -0.501749
394  2.633448  -0.862911     -0.930889   -1.363154 -0.997859      3.283479    1.634321 -1.289347  2.180035 -0.501749
395  1.095974  -0.862911     -0.567753   -0.531795 -0.803605     -1.428605    1.634321  0.773608 -0.457538 -0.501749
396  0.583482  -0.862911     -0.711097   -0.661694 -0.415097      1.108671    1.634321  0.773608 -0.457538 -0.501749
397  0.967851  -0.862911     -0.720653   -0.583754 -0.303253      1.398646    1.634321  0.773608 -0.457538 -0.501749
>>>

初步的程序

有了上面这些尝试,开始着手编程,主要有如下几步工做:工具

  • 将样本数据集分为训练集和测试集两部分
  • 将数据集中的MPG(百英里油耗数)去掉,单独出来做为数据集的标注结果,达成监督学习
  • 构建模型,编译模型
  • 使用训练集数据对模型进行训练
  • 使用测试集样本进行数据预测,评估模型效果

咱们使用附带注释的源码来代替讲解:学习

#!/usr/bin/env python3

from __future__ import absolute_import, division, print_function

# 引入各项扩展库
import pathlib

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers

# 下载样本数据
dataset_path = keras.utils.get_file("auto-mpg.data", "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")
# 样本中所须要的列名称
column_names = ['MPG', 'Cylinders', 'Displacement', 'Horsepower', 'Weight',
                'Acceleration', 'Model Year', 'Origin'] 
# 从样本文件中读取指定列的数据
raw_dataset = pd.read_csv(dataset_path, names=column_names,
                          na_values="?", comment='\t',
                          sep=" ", skipinitialspace=True)

# 复制一份数据作后续操做
dataset = raw_dataset.copy()

# 数据清洗,去掉无心义的数据
dataset = dataset.dropna()

# 将Origin数据作one-hot编码,至关于转换成3个产地字段
origin = dataset.pop('Origin')
dataset['USA'] = (origin == 1)*1.0
dataset['Europe'] = (origin == 2)*1.0
dataset['Japan'] = (origin == 3)*1.0

# 随机分配80%的数据做为训练集
# frac是保留80%的数据
# random_state至关于随机数的种子,在这里固定一个值是为了每次运行,随机分配获得的样本集是相同的
train_dataset = dataset.sample(frac=0.8, random_state=0)
# 训练集的数据去除掉,剩下的是20%,做为测试集
test_dataset = dataset.drop(train_dataset.index)
# 获取数据集的统计信息
train_stats = train_dataset.describe()
# MPG是训练模型要求的结果,也就是标注字段,没有意义,从统计中去除
train_stats.pop("MPG")
# 对统计结果作行列转置,方便将统计结果做为下面作数据规范化的参数
train_stats = train_stats.transpose()

# 训练集和测试集的数据集都去掉MPG列,单独取出做为标注
train_labels = train_dataset.pop('MPG')
test_labels = test_dataset.pop('MPG')

# 定义一个数据规范化函数帮助简化操做
def norm(x):
    return (x - train_stats['mean']) / train_stats['std']
# 训练集和测试集数据规范化
normed_train_data = norm(train_dataset)
normed_test_data = norm(test_dataset)

# 构建回归模型
def build_model():
    model = keras.Sequential([
        layers.Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]),
        layers.Dense(64, activation='relu'),
        layers.Dense(1)    # 回归的主要区别就是最后不须要激活函数,从而保证最后是一个连续值
    ])

    optimizer = tf.keras.optimizers.RMSprop(0.001)

    model.compile(loss='mse',
                  optimizer=optimizer,
                  metrics=['acc'])
    return model

model = build_model()
# 显示构造的模型
model.summary()

EPOCHS = 1000
history = model.fit(
  normed_train_data, train_labels,
  epochs=EPOCHS, validation_split=0.2, verbose=1)

# 使用测试集预测数据
test_result = model.predict(normed_test_data)
# 显示预测结果
print('===================\ntest_result:', test_result)

使用的模型很是简单,训练和预测也没有什么特别之处,无需再讲解。执行程序的输出大体以下:

$ ./mpg1.py
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 64)                640       
_________________________________________________________________
dense_1 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 65        
=================================================================
Total params: 4,865
Trainable params: 4,865
Non-trainable params: 0
_________________________________________________________________
Train on 251 samples, validate on 63 samples
Epoch 1/1000
251/251 [==============================] - 0s 2ms/sample - loss: 582.3197 - acc: 0.0000e+00 - val_loss: 582.2971 - val_acc: 0.0000e+00
Epoch 2/1000
251/251 [==============================] - 0s 67us/sample - loss: 542.1007 - acc: 0.0000e+00 - val_loss: 541.7508 - val_acc: 0.0000e+00
    ......
Epoch 1000/1000
251/251 [==============================] - 0s 58us/sample - loss: 2.7232 - acc: 0.0000e+00 - val_loss: 9.4673 - val_acc: 0.0000e+00
===================
test_result: [[16.366997 ]
 [ 8.665408 ]
 [ 8.548    ]
 [25.14063  ]
 [18.678812 ]
    ......

输出的数据中,一开始是所使用的模型信息,这是model.summary()输出的结果。若是有时间,翻翻前面vgg-19和resnet50网络的模型,也试用一下,保证你获得一个惊讶的结果:)
随后是1000次迭代的训练输出。最后是预测的结果。
若是你细心的话,可能已经发现了问题,从第一个训练周期开始,一直到第1000次,虽然损失loss在下降,但正确率acc一直为0,这是为何?
其实看看最后的预测结果就知道了。对于这种连续输出值的回归问题,结果不是有限的分类,而是很精确的浮点数。这样的结果,只能保证大致比例上,同标注集是吻合的,不可能作到一一对应的相等。这是全部的正确率结果为0的缘由,也是咱们没有跟前面的例子同样,使用model.evaluate对模型进行评估的缘由。
因而可知,咱们在模型编译的时候选取评价指标参数为'acc'(正确率)就是不合理的。替代的,咱们可使用MAE(Mean Abs Error)平均绝对偏差或者MSE(Mean Square Error)均方根偏差/标准偏差。
此外你可能看到了,程序数据集简单、模型也简单,因此训练速度很快。1000次的迭代训练,信息输出自己占用的时间甚至多过了训练所需的时间。
model.fit()训练函数中能够指定verbose=0来屏蔽输出,但彻底没有输出也是很不友好的。咱们可使用前面用过的回调函数机制,来显示自定义的输出内容。好比咱们能够在每一个训练循环中输出一个“.”来显示训练的进展。
来改进一下程序,用如下程序段来替代上面代码中,”构建回归模型”以后全部的内容。

# 替代前面代码中,构建回归模型如下内容
# 构建回归模型
def build_model():
    model = keras.Sequential([
        layers.Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]),
        layers.Dense(64, activation='relu'),
        layers.Dense(1)    # 回归的主要区别就是最后不须要激活函数,从而保证最后是一个连续值
    ])

    optimizer = tf.keras.optimizers.RMSprop(0.001)

    model.compile(loss='mse',
                  optimizer=optimizer,
                  metrics=['mae', 'mse'])
    return model

# 自定义一个类,实现keras的回调,在训练过程当中显示“.”
class PrintDot(keras.callbacks.Callback):
    def on_epoch_end(self, epoch, logs):
        if epoch % 100 == 0:
            print('')
        print('.', end='')

model = build_model()
# 显示构造的模型
model.summary()

EPOCHS = 1000
history = model.fit(
  normed_train_data, train_labels,
  epochs=EPOCHS, validation_split=0.2, verbose=0,
  callbacks=[PrintDot()])

# 使用测试集评估模型
loss, mae, mse = model.evaluate(normed_test_data, test_labels, verbose=0)
# 显示评估结果
print("\nTesting set Mean Abs Error: {:5.2f} MPG".format(mae))

再次执行,输出结果看起来干净多了:

$ ./mpg2.py
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 64)                640       
_________________________________________________________________
dense_1 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 65        
=================================================================
Total params: 4,865
Trainable params: 4,865
Non-trainable params: 0
_________________________________________________________________

....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
Testing set Mean Abs Error:  1.98 MPG

最后咱们也敢使用model.evaluate对模型进行评估了,获得的MAE是1.98。

可是MAE、MSE的数据,重点的是看训练过程当中的动态值,根据趋势调整咱们的程序,才谈得上优化。只有最终一个值其实意义并不大。
咱们继续为程序增长功能,用图形绘制出训练过程的指标变化状况。前面的程序中,咱们已经使用history变量保存了训练过程的输出信息,下面就是使用matplotlib将数值绘出。

# 接着上面的代码,在最后添加如下内容:  
def plot_history(history):
    hist = pd.DataFrame(history.history)
    hist['epoch'] = history.epoch

    # plt.figure()
    plt.figure('MAE --- MSE', figsize=(8, 4))
    plt.subplot(1, 2, 1)
    plt.xlabel('Epoch')
    plt.ylabel('Mean Abs Error [MPG]')
    plt.plot(
        hist['epoch'], hist['mae'],
        label='Train Error')
    plt.plot(
        hist['epoch'], hist['val_mae'],
        label='Val Error')
    plt.ylim([0, 5])
    plt.legend()

    plt.subplot(1, 2, 2)
    plt.xlabel('Epoch')
    plt.ylabel('Mean Square Error [$MPG^2$]')
    plt.plot(
        hist['epoch'], hist['mse'],
        label='Train Error')
    plt.plot(
        hist['epoch'], hist['val_mse'],
        label='Val Error')
    plt.ylim([0, 20])
    plt.legend()
    plt.show()

plot_history(history)

执行程序,能够获得下图的结果:

从图中能够看出,虽然随着迭代次数的增长,训练错误率在下降,但大体从100次迭代以后,验证的错误率就基本稳定不变了。限于样本集数量及维度选取、模型设计等方面的缘由,对这个结果的满意度先放在一边。这个模型在100次迭代以后就长时间无效的训练显然是一个可优化的点。
TensorFlow/Keras提供了EarlyStopping机制来应对这种问题,EarlyStopping也是一个回调函数,请看咱们实现的代码:

# 如下代码添加到前面代码的最后
# 设置EarlyStopping回调函数,监控val_loss指标
# 当该指标在10次迭代中均不变化后退出
early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)
# 再次训练模型
history = model.fit(normed_train_data, train_labels, epochs=EPOCHS,
                    validation_split = 0.2, verbose=0, callbacks=[early_stop, PrintDot()])
# 绘制本次训练的指标曲线
plot_history(history)

执行后,此次获得的结果使人满意了,大体在60次迭代以后,就获得了同前面1000次迭代基本类似的结果:

既然训练完成,虽然咱们使用模型预测的结果没法跟原标注一对一比较,咱们能够用图形的方式来比较一下两组值,并作一下预测错误统计:

# 继续在最后增长以下代码
# 使用测试集数据用模型进行预测
test_predictions = model.predict(normed_test_data).flatten()

# 绘制同标注的对比图和二者偏差分布的直方图
plt.figure('Prediction & TrueValues  --- Error', figsize=(8, 4))
plt.subplot(1, 2, 1)
plt.scatter(test_labels, test_predictions)
plt.xlabel('True Values [MPG]')
plt.ylabel('Predictions [MPG]')
plt.axis('equal')
plt.axis('square')
plt.xlim([0, plt.xlim()[1]])
plt.ylim([0, plt.ylim()[1]])
_ = plt.plot([-100, 100], [-100, 100])

error = test_predictions - test_labels
plt.subplot(1, 2, 2)
plt.hist(error, bins=25)
plt.xlabel("Prediction Error [MPG]")
_ = plt.ylabel("Count")
plt.show()

程序获得结果图以下:

左边的图中,若是预测结果同标注结果彻底一致,蓝色的点将落在主对角线上,偏离对角线则表明预测偏差。从图中能够看出,全部的点大体是落在主对角线周边的。这表示预测结果同标注值基本吻合。
右边的图是二者之差的范围统计结果,能够理解为左图逆时针逆时针旋转45度后全部点统计的直方图,对角线就是偏差为0的位置。图中能看出偏差基本符合正态分布,表明预测数值偏大、偏小的数量和比例基本类似,模型是可信的。
固然限于样本数量过少的问题,模型的优化余地仍是很大的。

(待续...)

相关文章
相关标签/搜索