第二章:TensorFlow.js中的简单线性回归(下卷)

936 阅读10分钟

第二章:TensorFlow.js中的简单线性回归(上卷)

2.3 具有多个输入特征的线性回归

在第一个示例中,我们只有一个输入特征sizeMB ,以及一个可用来预测的目标timeSec 。更为常见的情况是具有多个输入特征,不确切哪些特征最具有预测性,哪些特征与目标之间的关系松散,仍需要同时使用它们并让算法对其进行分类。在本章中,我们将解决这个更复杂的问题。

到本节末,您将

  • 了解如何建立一个可以输入多种特征并从中学习的模型
  • 使用yarn,git和标准JS项目打包来构建和运行带有ML的网络应用
  • 知道如何规范化数据以稳定学习过程
  • 训练时使用tf.Model.fit()回调来更新Web UI

2.3.1 波士顿房屋价格数据集

波士顿房屋价格数据集[41]收集了1970年代后期在马萨诸塞州波士顿及其周边地区收集的500条简单的房地产记录,几十年来一直用作介绍性统计数据和机器学习问题的标准数据集。数据集中的每个独立记录都包含波士顿邻域的数字度量,其中包括例如房屋的大小,该区域距离最近的高速公路有多远,该区域是否具有滨水性质等。下面提供了特征的精确排序列表以及每个特征的平均值。

表2.1波士顿住房数据集的特征
指数 特征简称 特征说明 平均值 范围(最大-最小)
0 CRIM 犯罪率 3.62 88.9
1 ZN 占地超过25,000平方英尺的住宅用地比例 11.4 100
2 INDUS 城镇非零售营业面积(工业)的比重 11.2 27.3
3 CHAS 该地区是否毗邻查尔斯河 0.0694 1
4 NOX 一氧化氮浓度(百万分之一) 0.555 0.49
5 RM 每个住宅的平均房间数 6.28 5.2
6 AGE 1940年之前建造的自有住房的部分 68.6 97.1
7 DIS 到五个波士顿就业中心的加权距离 3.80 11.0
8 RAD 径向公路通达性指数 9.55 23.0
9 TAX 每10,000美元的税率 408 524.0
10 PTRATIO 师生比例 18.5 9.40
11 LATAT 未接受高中教育的在职男性百分比 12.7 36.2
12 MEDV 拥有住房的中位数价值,单位为$ 1,000 US 22.5 45

在本节中,我们将构建,训练和评估一个学习系统,以根据所有输入特征来估计房价的中位数(MEDV )。您可以将其想象为一个根据社区的可测量属性估算房地产价格的系统。因为此问题更大,并且涉及更多问题,所以我们将以工作代码存储库的形式提供解决方案,然后指导您完成该过程。

2.3.2 从GitHub获取并运行Boston-housing项目

由于此问题比下载时间预测示例要大一些,并且涉及更多问题,因此我们将从以工作代码存储库的形式提供解决方案开始,然后指导您完成该过程。如果您已经是git 工作流程和npm / yarn包管理的专家,则可能需要快速浏览此小节。

我们将从在GitHub [42]中源代码克隆项目存储库开始,以获取项目所需的HTML,JS和配置文件的副本。除了最简单的代码托管在CodePen上,本书中的所有示例都收集在两个git存储库之一中,然后按存储库中的目录分隔。这两个存储库是tensorflow / tfjs-examples 和tensorflow / tfjs-models ,它们都托管在GitHub上。以下命令将在本地克隆此示例所需的存储库,并将工作目录更改为波士顿住房预测项目。

git clone https://github.com/tensorflow/tfjs-examples.git
 cd tfjs-examples/boston-housing
信息框2.3
本书中使用的示例的基本JavaScript项目结构
我们将在本书示例中使用的标准项目结构包括三种重要的文件类型。第一个是HTML。我们将使用的HTML文件主要用作包含一些组件的基本结构。通常,只有一个html文件,名为index.html ,它将包含一些div 标签,也许一些UI元素以及一个源标签,以JavaScript代码插入例如index.js 。
JS代码通常分为几个文件,以提高良好的可读性和工程样式。在这个波士顿住房项目中,用于更新视觉元素的代码位于ui.js中,而用于下载数据的代码位于data.js中。两者都是通过index.js的import语句引用的。
我们将使用的第三个重要文件类型是软件包元数据package.json文件,这是npm [43]软件包管理器。如果您以前从未使用过npm 或yarn ,建议您在https://docs.npmjs.com/getting-started/what-is-npm上浏览一下npm“入门”文档,并逐渐熟悉以便能够构建并运行示例代码。我们将使用yarn [44]作为我们的包管理器,但如果npm包更适合您的需求,您可以用npm来代替。
在存储库中,请注意以下重要文件:
1. index.html :HTML根文件,提供DOM根并调用JS脚本。
2. index.js :根文件,用于加载数据,定义模型,训练循环并指定UI元素
3. data.js :实现下载和访问Boston Housing Dataset所需的数据结构
4. ui.js :用于将UI元素连接到动作的UI钩的实现。
5. normalization.js 运算方法,例如,从数据中减去平均值
6. package.json :标准的npm软件包定义文件,描述了构建和运行此示例所需的依赖项(例如tfjs!)。
请注意,我们没有遵循将HTML文件和JS文件放在特定类型的子目录中的标准做法。这种模式虽然是大型存储库的最佳实践,但对于我们在本书中使用的较小示例或您可以在github.com/tensorflow/tfjs-examples上找到的较小示例而言,其模糊性更大。

要运行演示,请使用yarn:yarn && yarn watch

在浏览器中打开一个新标签,指向将运行示例的localhost 上的端口。如果浏览器没有自动响应,则可以导航到命令行上的URL输出。单击标记为“ Train Linear Regressor”的按钮将触发,以建立线性模型并将其拟合到Boston Housing数据,并在每个循环训练之后在训练和测试数据集上输出损失的动画图,如图2.11所示。

本节的其余部分将详细介绍此波士顿房屋线性回归网络应用演示的构建要点。我们将首先回顾如何收集和处理数据,以便与TensorFlow.js一起使用。然后,我们将专注于模型的构建,训练和评估,最后,我们将在网页上展示如何将模型用于实时预测。

图2.11 tfjs-examples中波士顿住房的线性回归例子

2.3.3 访问波士顿住房数据

在清单2.1中的第一个项目中,我们将数据硬编码为JS数组,并使用tf.tensor2d 函数将其转换为张量。硬编码对于一些演示很好,但是显然不能扩展到更大的应用程序。通常,JS开发人员会发现他们的数据以某种序列化格式位于某个URL(可能是本地)上。例如,可以从以下网址的Google Cloud中以CSV格式公开和免费获得Boston Housing数据:

https://storage.googleapis.com/tfjs-examples/multivariate-linear-regression/data/train-data.csv
https://storage.googleapis.com/tfjs-examples/multivariate-linear-regression/data/train-target.csv
https://storage.googleapis.com/tfjs-examples/multivariate-linear-regression/data/test-data.csv
https://storage.googleapis.com/tfjs-examples/multivariate-linear-regression/data/test-target.csv

通过将样本随机分配到训练和测试部分中,可以对数据进行预分割。⅔ 样品来训练,其余⅓ 来评估训练的模型。此外,对于每种拆分,目标特征已与其他特征分开,从而在下表中列出了四个文件名。

表2.2 Boston-housing数据集按文件名和内容划分的文件名
特征 (12个) 目标变量 (1 个)
训练 train-data.csv train-target.csv
测试 test-data.csv test-target.csv

为了将它们引入我们的应用程序,我们需要能够下载此数据并将其转换为适当类型和形状的张量。为此,波士顿住房项目在data.js中定义了一个类BostonHousingDataset 。该类抽象了数据集流操作,提供了一个API来检索原始数据作为数字矩阵。在内部,该类使用公共开源papaparse [45]库流式传输和解析远程CSV文件。加载并解析文件后,该库将向我们返回一个数字数组。然后使用与第一个示例相同的API将其转换为张量,如下面的清单2.7所示,这是index.js中经过精简的示例,以关注相关位。

清单2.7 将Boston-housing数据转换为index.js中的张量
// Initialize a BostonHousingDataset object defined in data.js.
 const bostonData = new BostonHousingDataset();
 const tensors = {};
  
 // Convert the loaded csv data, of type number[][] into 2d tensors.
 export const arraysToTensors = () => {
   tensors.rawTrainFeatures = tf.tensor2d(bostonData.trainFeatures);
   tensors.trainTarget = tf.tensor2d(bostonData.trainTarget);
   tensors.rawTestFeatures = tf.tensor2d(bostonData.testFeatures);
   tensors.testTarget = tf.tensor2d(bostonData.testTarget);
 }
  
 // Trigger the data to load asynchronously once the page has loaded.
 let tensors;
 document.addEventListener('DOMContentLoaded', async () => {
   await bostonData.loadData();
   arraysToTensors();
 }, false);

2.3.4 精确定义波士顿住房问题

现在,我们已经可以以所需的形式访问我们的数据了,现在是时候更精确地阐明我们的任务了。我们说过,我们想从其他领域预测房地产中位数(MEDV ),但我们将如何确定自己做得如何?我们如何才能将好的模型与更好的模型区分开?

我们在第一个示例中使用的指标meanAbsoluteError 均等地计算所有错误。如果只有10个样本,并且我们对所有10个样本都做出了预测,那么我们对其中9个样本的预测是正确的,但是在第10个样本上有30个预测,则meanAbsoluteError 将为3(因为30/10 为3)。相反,如果我们对每个样本的预测都偏离3,那么meanAbsoluteError 仍将为3。这种均等错误原则似乎是唯一显然正确的选择,但是除了meanAbsoluteError 之外,还有其他选择损失指标的方式。

另一种选择是权衡大错误而不是小错误。我们可以取误差的平方而不是取绝对误差的平均值。

继续上面的案例研究和十个样本,这种均方误差(MSE)方法发现,每个示例(10 x 3² = 90)减少3的损失比仅一个示例(1 x 30² = 900)减少30的损失要低。由于对大错误的敏感性,平方误差比绝对误差对样本离群值的敏感性更高。使MSE最小化的优化器拟合模型将优先选择犯一些小错误的模型,而不是偶尔给出非常差的估计的模型。显然,两种误差测量都希望使用完全没有误差的模型!但是,如果您的应用程序可能对错误的异常值非常敏感,则MSE可能是比MAE更好的选择。还有其他一些技术原因可以选择MSE或MAE,但目前它们并不重要。在此示例中,我们将使用MSE。

在继续之前,我们应该找到损失的基线估计。如果我们从非常简单的估计中不知道错误,那么我们就没有能力从更复杂的模型中评估错误。我们将使用平均房地产价格作为“最佳猜测”的代表,并通过始终猜测该值来计算误差。 清单2.8 计算猜测均价的基线损失(meanSquaredError)

export const computeBaseline = () => {
   const avgPrice = tf.mean(tensors.trainTarget);
   console.log(`Average price: ${avgPrice.dataSync()[0]}`);
  
   const baseline =
       tf.mean(tf.pow(tf.sub(tensors.testTarget, avgPrice), 2));
  
   console.log(`Baseline loss: ${baseline.dataSync()[0]}`);
 };

TensorFlow.js通过在GPU上进行调度来优化其计算,张量可能并不总是可由CPU访问。清单2.8中对dataSync的调用告诉TFJS完成张量的计算并将该值从GPU提取到CPU中,以便可以将其打印出来或与非张量流操作共享。

执行后,以上代码在控制台上产生以下内容:

Average price: 22.768770217895508
 Baseline loss: 85.58282470703125

这告诉我们,错误率约为85.58。如果我们要构建一个始终输出22.77的模型,则该模型将在测试数据上达到85.58的MSE。同样,请注意,我们根据训练数据计算了指标,并根据测试数据进行了评估,以避免产生误差。

85.58是平均平方误差,因此我们应该取平方根以获得平均误差。85.58的平方根约为9.25。因此,可以说我们期望我们的(常数)估计值平均偏离(高于或低于)大约9.25。由于根据表2.1的价值以千美元为单位,因此这个估计常数意味着上下浮动约9,250美元。如果这对于我们的应用程序足够好,我们可以在这里停止!明智的机器学习从业者知道什么时候可以避免不必要的复杂性。假设我们的价格估算器应用程序需要比这更接近。我们将通过对数据拟合线性模型来看看是否可以实现比85.58更好的MSE。

2.3.5 数据标准化

通过查看波士顿的住房功能,我们可以看到广泛的价值。NOX介于0.4到0.9之间,而TAX则介于180到711之间。为适应线性回归,优化器将尝试查找每个功能的权重,以使这些功能的总和乘以权重将近似等于房屋价格。回想一下,要找到这些权重,优化器会在权重空间中的梯度附近徘徊。如果某些特征的权重比例比其他特征大一些,则这些特征将比其他特征敏感得多。一个方向很小的移动会比另一个方向的很大移动对输出影响更大。这可能会导致不稳定并使其难以拟合模型。

为了解决这个问题,我们首先将数据标准化。这意味着我们将对特征进行缩放,以使它们具有零均值和单位标准偏差。这种类型的归一化非常普遍,也可以称为标准转换或“ z分数归一化”。这样做的算法很简单,我们首先计算每个特征的均值,然后从原始值中减去它,以便该特征的平均值为零。然后,我们用减去的平均值计算特征的标准偏差,然后除以该平均值。用伪代码:
normalizedFeature = (feature - mean(feature)) / std(feature)

例如,当feature为[10,20,30,40]时,规范化的版本将约为[-1.3,-0.4,0.4,1.3] ,其平均值显然为零,并且通过肉眼标准偏差约为1 。在Boston-housing的示例中,将规范化代码分解到一个单独的文件normalization.js中,该文件的内容在2.9中列出。在这里,我们看到两个函数,一个函数从提供的rank-2张量计算平均值和标准差,另一个函数在给定提供的预先计算的均值和std的情况下标准化张量。

清单2.9 数据归一化
/**
  * Calculates the mean and standard deviation of each column of a data array.
  *
  * @param {Tensor2d} data Dataset from which to calculate the mean and
  *                        std of each column independently.
  *
  * @returns {Object} Contains the mean and standard deviation of each vector
  *                   column as 1d tensors.
  */
 export function determineMeanAndStddev(data) {
   const dataMean = data.mean(0);
   const diffFromMean = data.sub(dataMean);
   const squaredDiffFromMean = diffFromMean.square();
   const variance = squaredDiffFromMean.mean(0);
   const std = variance.sqrt();
   return {mean, std};
 }
  
 /**
  * Given expected mean and standard deviation, normalizes a dataset by
  * subtracting the mean and dividing by the standard deviation.
  *
  * @param {Tensor2d} data: Data to normalize. Shape: [numSamples, numFeatures].
  * @param {Tensor1d} mean: Expected mean of the data. Shape [numFeatures].
  * @param {Tensor1d} std: Expected std of the data. Shape [numFeatures]
  *
  * @returns {Tensor2d}: Tensor the same shape as data, but each column
  * normalized to have zero mean and unit standard deviation.
  */
 export function normalizeTensor(data, dataMean, dataStd) {
   return data.sub(dataMean).div(dataStd);
 }

让我们深入研究这些功能。函数determineMeanAndStddev将张量用作输入数据。按照惯例,第一维是样本维度,每个索引对应一个独立的唯一样本。第二个维度是特征维度,因此12个元素对应于12个输入特征(例如CRIM ,ZN ,INDUS 等)。由于我们要独立计算每个特征的均值,因此我们将 使用const dataMean = data.mean(0);

在此调用中,“ 0 ”表示均值应在第0个索引(即第一)维上进行。回想一下,数据是2级张量,因此具有二维(又称轴)。第一个轴是样品维度。当我们沿着此轴从第一个元素移动到第二个元素到第三个元素时,我们指的是不同的样本,或者在这个清净下,指的是不同的房地产。第二维是特征维。当我们从该维度的第一个元素移到第二个元素时,我们所指的是不同的功能,例如表2.1中的CRIM ,ZN 和INDUS 。当我们沿轴0取平均值时,我们就是在采样方向上取平均值。结果是仅保留特征轴的1级张量。我们掌握了每个特征的平均值。取而代之的是,如果取轴1的均值,则仍将获得1级张量,但其余轴将为样本维度。这些值将对应于每个房地产的平均值。使用轴时,请注意要朝正确的方向进行计算,这经常产生错误。

当然,如果我们在此处设置一个断点[46],则可以使用JS控制台探索计算出平均值,并且我们看到平均值非常接近于我们为整个数据集计算出的平均值。这意味着我们的训练样本具有代表性。

> dataMean.shape
[12]
> dataMean.print();
[3.3603415, 10.6891899, 11.2934837, 0.0600601, 0.5571442, 6.2656188, 68.2264328, 3.7099338, 9.6336336, 409.2792969, 18.4480476, 12.5154343]

在下一行中,我们从数据中减去(使用tf.sub )平均值。 const diffFromMean = data.sub(dataMean); 如果您没有全力以赴,那么这条线可能隐藏了一段令人愉悦的小魔术。您会看到,data是形状为[ 333,12 ] 的2级张量,而dataMean 是形状[12] 的1级张量。通常,不可能减去两个具有不同形状的张量。但是,在这种情况下,TensorFlow使用广播来扩展第二张量的形状,实际上是将其重复333次,从而完全按照用户的意图进行操作而无需将其拼写出来。获取可用性很方便,但有时形状兼容广播的规则可能会有些混乱。如果您对广播的详细信息感兴趣,请直接跳到下面的信息框2.5。
determineMeanAndStddev 函数的接下来的几行没有新的惊喜。tf.square()是每个元素的平方,而tf.sqrt()取每个元素的平方根。TensorFlow.js API参考https://js.tensorflow.org/api/latest/中记录了每种方法的详细API 。文档页面还具有实时可编辑的小部件,使您可以探索函数如何使用自己的参数值,如图2.12所示。
在这个例子中, determineMeanAndStddev 方法可以更简洁地表示为:const std = data.sub(data.mean(0)).square().mean().sqrt();

您应该能够看到TensorFlow允许我们在无需太多代码的情况下表达很多计算。

图2.12 js.tensorflow.org上的TensorFlow.js API文档可让您直接在文档内浏览TensorFlow API并与之交互。这使得了解功能用途和棘手的边缘情况变得简单而快捷。

专栏1.1
广播
考虑张量运算,例如C = tf.someOperation(A,B),其中A 和B 是张量。如果可能,并且没有歧义,将广播较小的张量匹配较大张量的形状。广播包括两个步骤:
将轴(称为广播轴)添加到较小的张量以匹配较大张量的等级。
在这些新轴旁边会重复出现较小的张量,以匹配较大张量的完整形状。
在实现方面,实际上不会创建新的张量,因为这将非常低效。重复操作完全是虚拟的,它发生在算法级别而不是内存级别。但是,考虑到沿着新轴重复的较小张量是一个有用的思维模型。
在广播中,如果一个张量的形状为(a,b,…,n,n + 1,…m),而另一个张量的形状为(n,n + 1,…,m),则通常可以应用两张量元素运算。 )。然后,广播将自动从轴a到n-1发生。例如,以下示例通过广播将元素级最大运算应用于不同形状的两个随机张量。
x = tf.randomUniform([64,3,11,9]); #A
y = tf.randomUniform([11,9]); #B
z = tf.maximum(x,y); #C
#A x是形状为[64、3、11、9]的随机张量。
#B y是形状为[11,9]的随机张量。
#C 输出z具有x的形状[64,3,11,9]。

2.3.6 波士顿住房数据的线性回归

我们的数据已标准化,我们已经完成了数据处理工作,以计算出合理的基线-下一步是建立并拟合模型,以查看我们是否能跑赢基线。在下面的清单2.10中,我们定义了线性回归模型,就像在2.1节中所做的那样。代码非常相似;我们从下载时间预测模型中看到的唯一区别是在inputShape 配置中,该配置现在接受长度为12的向量,而不是1。单个密集层仍具有单位:1 ,指示输出一个数字。

清单2.10 为波士顿住房定义线性回归模型(来自index.js)
export const linearRegressionModel = () => {
   const model = tf.sequential();
   model.add(tf.layers.dense({inputShape: [bostonData.numFeatures], units: 1}));
   return model;
 };

回想一下,在定义了模型后,开始训练之前,我们必须通过调用model.compile来指定损失和优化器。在清单2.11中,我们看到指定了“ meanSquaredError” 损失,并且优化器正在使用自定义的学习率。在我们之前的示例中,optimizer参数设置为字符串'sgd' ,但是现在它是tf.train.sgd(学习率)。此函数将返回一个对象,该对象表示SGD优化算法,使用我们的自定义学习率进行了参数设置。这是TensorFlow.js中的一种常见模式,是从Keras借来的,您将看到它被许多可配置选项采用。对于标准的众所周知的默认参数,字符串哨兵值可以替代所需的对象类型,而TensorFlow.js将使用默认参数替代所需对象的字符串。在这种情况下,“ sgd” 将替换为tf.train.sgd(0.01)。如果需要其他自定义,则用户可以通过工厂模式构建对象,并提供所需的自定义值。这使代码在大多数情况下都简洁明了,但允许高级用户在需要时覆盖默认行为。

清单2.11 用于波士顿住房的模型编译(来自index.js)
const LEARNING_RATE = 0.01;
 model.compile({optimizer: tf.train.sgd(LEARNING_RATE), loss: 'meanSquaredError'});

现在我们可以使用训练数据集训练模型。在清单2.12到2.14中,我们将使用model.fit()调用的一些附加功能,但实际上它的作用与图2.6相同。在每个步骤中,它从特征(tensors.trainFeatures )和目标(tensors.trainTarget )中选择许多新样本,计算损失,然后更新内部权重以减少损失。该过程将重复NUM_EPOCHS次训练数据,并在每个步骤中选择BATCH_SIZE个样本。

清单2.12 在波士顿住房数据上训练模型
await model.fit(tensors.trainFeatures, tensors.trainTarget, {
  batchSize: BATCH_SIZE
  epochs: NUM_EPOCHS,
});

在波士顿住房应用中,我们演示了模型训练时训练损失的图表。这需要使用model.fit()的回调功能来更新UI。所述model.fit()回调API允许用户提供回调函数,这将在特定事件被执行。从0.12.0版本开始,回调触发器的完整列表是onTrainBegin ,onTrainEnd ,onEpochBegin ,onEpochEnd ,onBatchBegin 和onBatchEnd 。

清单2.13 model.fit中的回调
let trainLoss;
 await model.fit(tensors.trainFeatures, tensors.trainTarget, {
   batchSize: BATCH_SIZE,
   epochs: NUM_EPOCHS,
   callbacks: {
     onEpochEnd: async (epoch, logs) => {
       await ui.updateStatus(`Epoch ${epoch + 1} of ${NUM_EPOCHS} completed.`);
       trainLoss = logs.loss;
       await ui.plotData(epoch, trainLoss);
     }
   }
 });

验证是机器学习的一个概念。在上面的下载时间示例中,我们希望将训练数据与测试数据分开,因为我们希望对模型在新的看不见的数据上的表现进行无偏估计。但是,经常发生的是还有另一个拆分称为验证数据。验证数据与训练数据和测试数据分开。验证数据做什么用?ML工程师将在验证数据上看到结果,并使用该结果来更改模型的某些配置[47],以提高验证数据的准确性。这一切都很好。但是,如果此循环完成了足够的次数,那么我们实际上是在验证数据上进行调整。如果我们使用相同的验证数据来评估模型的最终准确性,则最终评估的结果将不再可以推广,因为模型已经看到了数据并且不能保证评估的结果能够反映模型处理将来那些看不见的数据。这是将验证与测试数据分开的目的。我们的想法是使我们的模型适合训练数据,并基于对验证数据的评估来调整模型的超参数。当我们全部完成并对该过程感到满意时,我们只需对测试数据进行一次模型评估即可获得最终的,可概括的性能评估。下面我们总结训练集,验证集和测试集,以及如何在TensorFlow.js中使用它们。
并非所有项目都将使用所有三种类型的数据。通常,快速勘探或研究项目将仅使用训练和验证数据,而不会为测试保留一组“纯”数据。虽然不太严格,但这有时是有限资源的最佳使用。

  • 训练数据:使用梯度下降拟合模型权重。
  • 在TensorFlow.js中的用法:通常,训练数据使用主要参数(x 和y )来调用Model.fit(x,y,config)。
  • 验证数据:用于选择模型结构和超参数。
  • TensorFlow.js中的用法:Model.fit()有两种指定验证数据的方式,它们都是config参数。如果您(用户)具有用于验证的显式数据,则可以将其指定为config.validationData 。相反,如果您希望框架拆分一些训练数据并将其用作验证数据,请在config.validationSplit中指定要使用的分数。该框架将注意不要使用验证数据来训练模型,因此不会有重叠。
  • 测试数据:对模型性能的最终无偏估计。
  • TensorFlow.js中的用法:通过将评估数据作为x 和y 参数传递给Model.evaluate(x,y,config)来向系统公开评估数据。
    在清单2.14中,验证损失与训练损失一起计算。所述validationSplit:0.2 字段表示选择训练数据的最后20%作为验证数据来使用。此数据将不会用于训练(它不会影响梯度下降)。
清单2.14 包括验证数据的model.fit
let trainLoss;
 let valLoss;
 await model.fit(tensors.trainFeatures, tensors.trainTarget, {
   batchSize: BATCH_SIZE,
   epochs: NUM_EPOCHS,
   validationSplit: 0.2,
   callbacks: {
     onEpochEnd: async (epoch, logs) => {
       await ui.updateStatus(`Epoch ${epoch + 1} of ${NUM_EPOCHS} completed.`);
       trainLoss = logs.loss;
       valLoss = logs.val_loss;
       await ui.plotData(epoch, trainLoss, valLoss);
     }
   }
 });

在浏览器中,将此模型训练到200个循环大约需要11秒钟。现在,我们可以在测试集中评估模型,以查看它是否比基线更好。清单2.15显示了如何使用model.evaluate()来收集我们保留的测试数据上的模型性能,然后调用我们的自定义UI例程来更新视图。

清单2.15 在测试数据上评估我们的模型并更新UI(来自index.js)
await ui.updateStatus('Running on test data...');
 const result = model.evaluate(
     tensors.testFeatures, tensors.testTarget, {batchSize: BATCH_SIZE});
 const testLoss = result.dataSync()[0];
 await ui.updateStatus(
     `Final train-set loss: ${trainLoss.toFixed(4)}\n` +
     `Final validation-set loss: ${valLoss.toFixed(4)}\n` +
     `Test-set loss: ${testLoss.toFixed(4)}`);

在这里,model.evaluate()返回一个标量,其中包含在测试集上计算的损失。
由于梯度下降的随机性,您可能会得到不同的结果,但是以下结果是典型的:

  • 最终训练集损失:21.9864
  • 最终验证集损失:31.1396
  • 测试集损失:25.3206
  • 基准损失: 85.58

由此可见,我们对损失的最终无偏估计约为25.3,这比我们的85.6基准要好得多。回想一下,我们的损失是使用meanSquaredError计算的。取平方根,我们看到基线估计值通常偏离9.2以上,而线性模型仅偏离约5.0。很大的进步!如果我们是世界上唯一可以访问此信息的人,那么我们可以成为1978年波士顿最好的房地产投资者!除非使用其他更有效的方式,否则没人能够建立更准确的估算…

如果您满怀好奇心,单击“训练神经网络回归器”,便知道”可能”更好的估计。在下一章中,我们将介绍非线性深度模型,以说明如何实现这种壮举。

2.4 如何解释模型

现在我们已经训练了模型,并且能够做出合理的预测,很自然地想知道它学到了什么。有什么方法可以窥视模型以了解其如何理解数据?当模型预测某种物品的特定价格时,您是否有可能找到一个可理解的解释,说明其为何会产生该价格?对于大型深度网络的一般情况,模型理解(也称为模型可解释性)仍然是活跃的研究领域,在学术会议上有着很多讨论和演讲。但是对于这个简单的线性回归模型来说,它非常简单。
到本节末,您将

  • 能够从模型中提取学习到的权重
  • 能够解释这些权重和权重应该是什么。

2.4.1 从学习的权重中提取

我们在2.3节中建立的简单线性模型包含13个学习的参数,就像2.1.3节中的第一个线性模型包含kernel和bias一样 output = kernel · features + bias

kernel和bias都是在拟合模型时学习的。与第2.1.3节中学习的标量线性函数相反,此处的特征和kernel都是矢量,而“ · ”号表示内积,即标量乘以矢量的一般化。内积,也称为“点积” ,仅是匹配元素的乘积之和。下面清单2.16中的伪代码更精确地定义了内部乘积。

因此,我们应该认为,特征元素与kernel元素之间存在关联。如表2.1所示,对于每个单独的特征元素,例如“犯罪率”和“一氧化氮浓度”,kernel中都有一个相关的数值。每个值都告诉我们有关模型从该特征中学到了什么以及该特征如何影响输出的信息。

清单2.16 内部产品伪代码
function innerProduct(a, b) {
    output = 0;
    for (let i = 0 ; i < a.length ; i++) {
        output += a[i] * b[i];
    }
    return output;
}

例如,如果模型得知kernel [i] 为正,则意味着feature [i] 值越大,输出就越大。反之亦然,如果模型得知kernel [j] 为负,则feature [j] 值的较大值会降低预测的输出。值很小的学习值表示模型认为关联的特征对预测影响很小,而值很大的学习值表示模型将重点放在特征上,此特征变化很小,但会对预测产生较大影响。[48]

更具体的说,在图2.13中,以绝对值排在波士顿房屋示例的输出区域中的前五个特征值被打印出来。我们可以看到,对房地产价格产生负面影响的功能,这些值是负值,例如本地居民辍学的速度以及房地产与理想工作地点之间的距离。学习得到的权重对于得到与我们期望价格有着直接相关的特征(例如酒店的房间数量)具有积极意义。

图2.13 按绝对值排序,它们是在线性模型中对波士顿住房预测问题学习到的前五名权重的特征。请注意,您希望对房屋价格产生负面影响的要素呈负值。
辍学率 -3.8119
通勤距离 -3.7278
每间房屋的房间数 2.8451
距公路距离 2.2949
一氧化氮浓度 -2.1190

2.4.2 从模型中提取权重

学习模型的模块化结构使提取相关权重变得容易,我们可以直接访问它们,但是要获取原始值,需要达到一些API级别。重要的是,由于该值可能在GPU上,并且通信非常昂贵,因此请求这些值是异步的。下面清单2.17中的代码是对model.fit 回调的补充,扩展了清单2.14来说明每次学习到的权重。我们将逐步介绍API调用。

给定模型,我们首先希望访问正确的层。这很容易,因为此模型中只有一层,因此我们可以在model.layers [0] 处得到它的句柄。现在我们有了图层,我们可以使用getWeights()访问内部权重,该权重返回权重的数组。对于密集层,它将始终包含两个权重,即kernel权重和bias权重。因此,我们可以在以下位置访问正确的张量 。> model.layers[0].getWeights()[0]

现在我们有了正确的Tensor ,我们可以通过调用tensor的data()方法来访问其内容。由于GPU的异步性质↔ CPU通信,data()是一个异步并返回一个promise张量的值,而不是实际的数值。在下面的代码清单中,传递给promise 的then()方法的回调将张量值绑定到一个名为kernelAsArr 的变量。如果未注释console.log()语句,则每训练一次将如下所示的语句(列出kernel的值)记录到控制台。

清单2.17 访问内部模型值
let trainLoss;
let valLoss;
await model.fit(tensors.trainFeatures, tensors.trainTarget, {
  batchSize: BATCH_SIZE,
  epochs: NUM_EPOCHS,
  validationSplit: 0.2,
  callbacks: {
    onEpochEnd: async (epoch, logs) => {
      await ui.updateStatus(`Epoch ${epoch + 1} of ${NUM_EPOCHS} completed.`);
      trainLoss = logs.loss;
      valLoss = logs.val_loss;
      await ui.plotData(epoch, trainLoss, valLoss);
      model.layers[0].getWeights()[0].data().then(kernelAsArr => {
        #Console.log(kernelAsArr);
        const weightsList = describeKerenelElements(kernelAsArr);
        ui.updateWeightDescription(weightsList);
      });
    }
  }
});

2.4.3 关于可解释性

作为人类读者,您可能会看图2.13所描述的结果,并承认该模型已得到“每间房屋的房间数”特征与价格输出呈正相关,或者房地产“ AGE”特征(未列出)相对于前五个功能而言,其绝对值的重要性较低。但是这种分析可能会失败,原因之一即是两个输入特征之间具有很强的相关性。 考虑一个假设的示例,其中同一特征被两次输入,称它们为FEAT1和FEAT2。想象一下,从这两个功能中学到的权重是10和-5。您可能倾向于说增加FEAT1会导致更大的输出,而FEAT2则相反。但是,由于特征是等效的,即使权重反转,模型也将输出完全相同的值。

需要注意的是相关性和因果关系之间的差异。想象一个简单的模型,我们希望通过屋顶的潮湿程度来预测外面下雨的程度。如果我们测量了屋顶的湿度,我们可能可以预测过去一个小时的降雨量。但是,我们不能将水溅到传感器来模拟这个问题!

2.5 小结

  • 使用TensorFlow.js在五行JavaScript中可以轻松构建,训练和评估简单的机器学习模型

  • 梯度下降是深度学习背后的基本算法结构,从概念上讲很简单,实际上意味着在计算出的方向上以较少的步骤重复更新模型参数,将最大程度地提高模型的拟合度。

  • 模型的损失表面说明了模型与参数值网格的拟合程度。由于参数空间的高维性,损失表面通常无法计算,但可以说明并思考机器学习的工作原理。

  • 单个密集层足以解决一些简单的问题,并且可以在房地产定价问题上实现合理的预测。

2.6 练习

  1. 选择第2.1节中的硬编码时间估计问题是因为数据大致是线性的。其他数据集在拟合时将具有不同的损失曲面和动力学。希望在此处尝试替换您自己的数据,以探索模型的反应。您可能需要研究学习率,初始化或规范化,以使模型得到一些有趣的东西。
  2. 在第2.3.5节中,我们花了一些时间描述归一化的重要性,以及如何归一化输入数据的均值和单位方差。您应该能够修改示例以删除规范化并看到模型不再训练。您还应该能够将规范化例程修改为具有例如0以外的平均值或较低但不那么低的标准偏差。一些规范化会对模型起作用,而某些标准化将导致模型永不收敛。
  3. 众所周知,波士顿房屋价格数据集的某些特征比其他特征更能预测目标。如果要删除除一个功能以外的所有功能,我们应该保留哪个功能?如果我们要保留两个功能,该如何选择呢?尝试使用波士顿住房示例中的代码进行探索。
  4. 描述梯度下降如何通过比随机方式更好的方式更新权重来优化模型。
  5. 波士顿住房的示例打印出前5个影响最大的特征权重。尝试修改代码以打印出最小权重相关的特征。您能想象为什么这些重量很小?如果有人要问你这些权重为什么会是这些,你能告诉他们什么?