首页 > 其他分享 >lrn-tfjs-merge-4

lrn-tfjs-merge-4

时间:2024-02-08 15:46:00浏览次数:36  
标签:10 卷积 模型 js merge lrn 图像 tfjs 数据

TensorFlow.js 学习手册(五)

原文:Learning TensorFlow.js

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:分类模型和数据分析

“先见之明,后事之师。”

—Amelia Barr

你不仅仅是把数据丢进模型中是有原因的。神经网络以极快的速度运行并执行复杂的计算,就像人类可以瞬间做出反应一样。然而,对于人类和机器学习模型来说,反应很少包含合理的上下文。处理脏乱和令人困惑的数据会导致次优的模型,甚至什么都不会得到。在这一章中,你将探索识别、加载、清理和优化数据的过程,以提高 TensorFlow.js 模型的训练准确性。

我们将:

  • 确定如何创建分类模型

  • 学习如何处理 CSV 数据

  • 了解 Danfo.js 和 DataFrames

  • 确定如何将混乱的数据输入训练中(整理你的数据)

  • 练习绘制和分析数据

  • 了解机器学习笔记本

  • 揭示特征工程的核心概念

当你完成这一章时,你将有信心收集大量数据,分析数据,并通过使用上下文来创建有助于模型训练的特征来测试你的直觉。

在这一章中,你将构建一个Titanic生死分类器。30 岁的 Kate Connolly 小姐,持有三等舱票,会生还吗?让我们训练一个模型来获取这些信息,并给出生还的可能性。

分类模型

到目前为止,你训练了一个输出数字的模型。你消化的大多数模型与你创建的模型有些不同。在第八章中,你实现了线性回归,但在这一章中,你将实现一个分类模型(有时称为逻辑回归)。

毒性、MobileNet,甚至井字棋模型输出一种选择,从一组选项中选择。它们使用一组总和为一的数字,而不是一个没有范围的单个数字。这是分类模型的常见结构。一个旨在识别三种不同选项的模型将给出与每个选项对应的数字。

试图预测分类的模型需要一种将输出值映射到其相关类别的方法。到目前为止,在分类模型中,最常见的方法是输出它们的概率。要创建一个执行此操作的模型,你只需要在最终层实现特殊的激活函数:

提示

记住,激活函数帮助你的神经网络以非线性方式运行。每个激活函数使得一个层以所需的非线性方式运行,最终层的激活直接转化为输出。重要的是确保你学会了哪种激活函数会给你所需的模型输出。

在这本书中使用的模型中,你一遍又一遍看到的激活函数被称为softmax激活。这是一组值,它们的总和为一。例如,如果你的模型需要一个 True/False 输出,你会期望模型输出两个值,一个用于识别true的概率,另一个用于false。例如,对于这个模型,softmax 可能输出[0.66, 0.34],经过一些四舍五入。

这可以扩展到 N 个值的 N 个分类只要类别是互斥的。在设计模型时,你会在最终层强制使用 softmax,并且输出的数量将是你希望支持的类别数量。为了实现 True 或 False 的结果,你的模型架构将在最终层上使用 softmax 激活,有两个输出。

// Final layer softmax True/False
model.add(
  tf.layers.dense({
    units: 2,
    activation: "softmax"
  })
);

如果您试图从输入中检测几件事情会发生什么?例如,胸部 X 光可能同时对肺炎和肺气肿呈阳性。在这种情况下,Softmax 不起作用,因为输出必须总和为一,对一个的信心必须与另一个对抗。在这种情况下,有一种激活可以强制每个节点的值在零和一之间,因此您可以实现每个节点的概率。这种激活称为 sigmoid 激活。这可以扩展到 N 个值,用于 N 个不相互排斥的分类。这意味着您可以通过具有 sigmoid 的单个输出来实现真/假模型(二元分类),其中接近零为假,接近一为真:

// Final layer sigmoid True/False
model.add(
  tf.layers.dense({
    units: 1,
    activation: "sigmoid",
  })
);

是的,这些激活名称很奇怪,但它们并不复杂。您可以通过研究这些激活函数的工作原理背后的数学,在 YouTube 的兔子洞中轻松度过一天。但最重要的是,了解它们在分类中的用法。在 表 9-1 中,您将看到一些示例。

表 9-1. 二元分类示例

激活 输出 结果分析
sigmoid [0.999999] 99% 确定是真的
softmax [0.99, 0.01] 99% 确定是真的
sigmoid [0.100000] 10% 确定是真的(因此 90% 是假的)
softmax [0.10, 0.90] 90% 确定是假的

当您处理真/假时,您使用 softmaxsigmoid 的区别消失。在选择最终层的激活时,您选择哪种激活没有真正的区别,因为没有一种可以排除另一种。在本章中,我们将在最后一层使用 sigmoid 以简化。

如果您试图对多个事物进行分类,您需要在 sigmoidsoftmax 之间做出明智的选择。本书将重申和澄清这些激活函数的使用情况。

泰坦尼克号

1912 年 4 月 15 日,“不沉的” RMS 泰坦尼克号(请参见 图 9-1)沉没了。这场悲剧在历史书籍中广为流传,充满了傲慢的故事,甚至有一部由莱昂纳多·迪卡普里奥和凯特·温丝莱特主演的电影。这场悲剧充满了一丝令人敬畏的死亡好奇。如果您在拉斯维加斯卢克索的 泰坦尼克号 展览中,您的门票会分配给您一位乘客的名字,并告诉您您的票价、舱位等等关于您生活的几件事。当您浏览船只和住宿时,您可以通过您门票上的人的眼睛体验它。在展览结束时,您会发现您门票上印刷的人是否幸存下来。

泰坦尼克号概况

图 9-1. RMS 泰坦尼克号

谁生存谁没有是 100%随机的吗?熟悉历史或看过电影的人都知道这不是一个抛硬币的事情。也许您可以训练一个模型来发现数据中的模式。幸运的是,客人日志和幸存者名单可供我们使用。

泰坦尼克数据集

与大多数事物一样,数据现在已转录为数字格式。 泰坦尼克 名单以逗号分隔的值(CSV)形式可用。这种表格数据可以被任何电子表格软件读取。有很多副本的 泰坦尼克 数据集可用,并且它们通常具有相同的信息。我们将使用的 CSV 文件可以在本章的相关代码中的 额外文件夹 中找到。

这个 泰坦尼克 数据集包含在 表 9-2 中显示的列数据。

表 9-2. 泰坦尼克数据

定义 图例
生存 生存 0 = 否,1 = 是
pclass 票类 1 = 1 等,2 = 2 等,3 = 3 等
性别 性别
年龄 年龄
兄弟姐妹或配偶数量 兄弟姐妹或配偶在船上的数量
父母或子女数量 父母或子女在船上的数量
票号 票号
票价 乘客票价
船舱 船舱号码
embarked 登船港口 C = 瑟堡, Q = 昆士敦, S = 南安普敦

那么如何将这些 CSV 数据转换为张量形式呢?一种方法是读取 CSV 文件,并将每个输入转换为张量表示进行训练。当您试图尝试哪些列和格式对训练模型最有用时,这听起来是一个相当重要的任务。

在 Python 社区中,一种流行的加载、修改和训练数据的方法是使用一个名为Pandas的库。这个开源库在数据分析中很常见。虽然这对 Python 开发人员非常有用,但 JavaScript 中存在类似工具的需求很大。

Danfo.js

Danfo.js是 Pandas 的 JavaScript 开源替代品。Danfo.js 的 API 被故意保持与 Pandas 接近,以便利用信息体验共享。甚至 Danfo.js 中的函数名称都是snake_case而不是标准的 JavaScriptcamelCase格式。这意味着您可以在 Danfo.js 中最小地进行翻译,利用 Pandas 的多年教程。

我们将使用 Danfo.js 来读取Titanic CSV 并将其修改为 TensorFlow.js 张量。要开始,您需要将 Danfo.js 添加到项目中。

要安装 Danfo.js 的 Node 版本,您将运行以下命令:

$ npm i danfojs-node

如果您使用简单的 Node.js,则可以require Danfo.js,或者如果您已经配置了代码以使用 ES6+,则可以import

const dfd = require("danfojs-node");
注意

Danfo.js 也可以在浏览器中运行。本章依赖于比平常更多的打印信息,因此利用完整的终端窗口并依赖 Node.js 的简单性来访问本地文件是有意义的。

Danfo.js 在幕后由 TensorFlow.js 提供支持,但它提供了常见的数据读取和处理实用程序。

为泰坦尼克号做准备

机器学习最常见的批评之一是它看起来像一个金鹅。您可能认为接下来的步骤是将模型连接到 CSV 文件,点击“训练”,然后休息一天,去公园散步。尽管每天都在努力改进机器学习的自动化,但数据很少以“准备就绪”的格式存在。

本章中的Titanic数据包含诱人的 Train 和 Test CSV 文件。然而,使用 Danfo.js,我们很快就会看到提供的数据远未准备好加载到张量中。本章的目标是让您识别这种形式的数据并做好适当的准备。

读取 CSV

CSV 文件被加载到一个称为 DataFrame 的结构中。DataFrame 类似于带有可能不同类型列和适合这些类型的行的电子表格,就像一系列对象。

DataFrame 有能力将其内容打印到控制台,以及许多其他辅助函数以编程方式查看和编辑内容。

让我们回顾一下以下代码,它将 CSV 文件读入 DataFrame,然后在控制台上打印几行:

const df = await dfd.read_csv("file://../../extra/titanic data/train.csv");  // ①
df.head().print(); // ②

read_csv方法可以从 URL 或本地文件 URI 中读取。

DataFrame 可以限制为前五行,然后打印。

正在加载的 CSV 是训练数据,print()命令将 DataFrame 的内容记录到控制台。结果显示在控制台中,如图 9-2 所示。

Head printout

图 9-2。打印 CSV DataFrame 头

在检查数据内容时,您可能会注意到一些奇怪的条目,特别是在Cabin列中,显示为NaN。这些代表数据集中的缺失数据。这是您不能直接将 CSV 连接到模型的原因之一:重要的是要确定如何处理缺失信息。我们将很快评估这个问题。

Danfo.js 和 Pandas 有许多有用的命令,可以帮助您熟悉加载的数据。一个流行的方法是调用.describe(),它试图分析每列的内容作为报告:

// Print the describe data
df.describe().print();

如果打印 DataFrame 的describe数据,您将看到您加载的 CSV 有 891 个条目,以及它们的最大值、最小值、中位数等的打印输出,以便您验证信息。打印的表格看起来像图 9-3。

描述打印输出

图 9-3。描述 DataFrame

一些列已从图 9-3 中删除,因为它们包含非数字数据。这是您将在 Danfo.js 中轻松解决的问题。

调查 CSV

这个 CSV 反映了数据的真实世界,其中经常会有缺失信息。在训练之前,您需要处理这个问题。

您可以使用isna()找到所有缺失字段,它将为每个缺失字段返回truefalse。然后,您可以对这些值进行求和或计数以获得结果。以下是将报告数据集的空单元格或属性的代码:

// Count of empty spots
empty_spots = df.isna().sum();
empty_spots.print();
// Find the average
empty_rate = empty_spots.div(df.isna().count());
empty_rate.print();

通过结果,您可以看到以下内容:

  • 空的Age数值:177(20%)

  • 空的Cabin数值:687(77%)

  • 空的Embarked数值:2(0.002%)

从对缺失数据量的简短查看中,您可以看到您无法避免清理这些数据。解决缺失值问题将至关重要,删除像PassengerId这样的无用列,并最终对您想保留的非数字列进行编码。

为了不必重复操作,您可以将 CSV 文件合并、清理,然后创建两个准备好用于训练和测试的新 CSV 文件。

目前,这些是步骤:

  1. 合并 CSV 文件。

  2. 清理 DataFrame。

  3. 从 DataFrame 重新创建 CSV 文件。

合并 CSV

要合并 CSV 文件,您将创建两个 DataFrame,然后沿着轴连接它们,就像对张量一样。您可能会感觉到张量训练引导您管理和清理数据的路径,并且这并非偶然。尽管术语可能略有不同,但您从前几章积累的概念和直觉将对您有所帮助。

// Load the training CSV
const df = await dfd.read_csv("file://../../extra/titanic data/train.csv");
console.log("Train Size", df.shape[0]) // ①

// Load the test CSV
const dft = await dfd.read_csv("file://../../extra/titanic data/test.csv");
console.log("Test Size", dft.shape[0]) // ②

const mega = dfd.concat({df_list: [df, dft], axis: 0})
mega.describe().print() // ③

打印“训练集大小为 891”

打印“测试集大小为 418”

显示一个包含 1,309 的表

使用熟悉的语法,您已经加载了两个 CSV 文件,并将它们合并成一个名为mega的 DataFrame,现在您可以对其进行清理。

清理 CSV

在这里,您将处理空白并确定哪些数据实际上是有用的。您需要执行三个操作来正确准备用于训练的 CSV 数据:

  1. 修剪特征。

  2. 处理空白。

  3. 迁移到数字。

修剪特征意味着删除对结果影响很小或没有影响的特征。为此,您可以尝试实验、绘制数据图表,或者简单地运用您的个人直觉。要修剪特征,您可以使用 DataFrame 的.drop函数。.drop函数可以从 DataFrame 中删除整个列或指定的行。

对于这个数据集,我们将删除对结果影响较小的列,例如乘客的姓名、ID、票和舱位。您可能会认为其中许多特征可能非常重要,您是对的。但是,我们将让您在本书之外的范围内研究这些特征。

// Remove feature columns that seem less useful
const clean = mega.drop({
  columns: ["Name", "PassengerId", "Ticket", "Cabin"],
});

要处理空白,您可以填充或删除行。填充空行是一种称为插补的技术。虽然这是一个很好的技能可以深入研究,但它可能会变得复杂。在本章中,我们将采取简单的方法,仅删除任何具有缺失值的行。要删除任何具有空数据的行,我们可以使用dropna()函数。

警告

这是在删除列之后之后完成的至关重要。否则,Cabin列中 77%的缺失数据将破坏数据集。

您可以使用以下代码删除所有空行:

// Remove all rows that have empty spots
const onlyFull = clean.dropna();
console.log(`After mega-clean the row-count is now ${onlyFull.shape[0]}`);

此代码的结果将数据集从 1,309 行减少到 1,043 行。将其视为一种懒惰的实验。

最后,您剩下两列是字符串而不是数字(EmbarkedSex)。这些需要转换为数字。

Embarked的值,供参考,分别是:C = 瑟堡,Q = 昆士敦,S = 南安普敦。有几种方法可以对其进行编码。一种方法是用数字等价物对其进行编码。Danfo.js 有一个LabelEncoder,它可以读取整个列,然后将值转换为数字编码的等价物。LabelEncoder将标签编码为介于0n-1之间的值。要对Embarked列进行编码,您可以使用以下代码:

// Handle embarked characters - convert to numbers
const encode = new dfd.LabelEncoder(); // ①
encode.fit(onlyFull["Embarked"]); // ②
onlyFull["Embarked"] = encode.transform(onlyFull["Embarked"].values); // ③
onlyFull.head().print(); // ④

创建一个新的LabelEncoder实例。

适合对Embarked列的内容进行编码的实例。

将列转换为值,然后立即用生成的列覆盖当前列。

打印前五行以验证替换是否发生。

您可能会对像第 3 步那样覆盖 DataFrame 列的能力感到惊讶。这是处理 DataFrame 而不是张量的许多好处之一,尽管 TensorFlow.js 张量在幕后支持 Danfo.js。

现在您可以使用相同的技巧对male / female字符串进行编码。(请注意,出于模型目的和乘客名单中可用数据的考虑,我们将性别简化为二进制。)完成后,您的整个数据集现在是数字的。如果在 DataFrame 上调用describe,它将呈现所有列,而不仅仅是几列。

保存新的 CSV 文件

现在您已经创建了一个可用于训练的数据集,您需要返回两个 CSV 文件,这两个文件进行了友好的测试和训练拆分。

您可以使用 Danfo.js 的.sample重新拆分 DataFrame。.sample方法会从 DataFrame 中随机选择 N 行。从那里,您可以将剩余的未选择值创建为测试集。要删除已抽样的值,您可以按索引而不是整个列删除行。

DataFrame 对象具有to_csv转换器,可选择性地接受要写入的文件参数。to_csv命令会写入参数文件并返回一个 promise,该 promise 解析为 CSV 内容。重新拆分 DataFrame 并写入两个文件的整个代码可能如下所示:

// 800 random to training
const newTrain = onlyFull.sample(800)
console.log(`newTrain row count: ${newTrain.shape[0]}`)
// The rest to testing (drop via row index)
const newTest = onlyFull.drop({index: newTrain.index, axis: 0})
console.log(`newTest row count: ${newTest.shape[0]}`)

// Write the CSV files
await newTrain.to_csv('../../extra/cleaned/newTrain.csv')
await newTest.to_csv('../../extra/cleaned/newTest.csv')
console.log('Files written!')

现在您有两个文件,一个包含 800 行,另一个包含 243 行用于测试。

泰坦尼克号数据的训练

在对数据进行训练之前,您需要处理最后一步,即经典的机器学习标记输入和预期输出(X 和 Y,分别)。这意味着您需要将答案(Survived列)与其他输入分开。为此,您可以使用iloc声明要创建新 DataFrame 的列的索引。

由于第一列是Survived列,您将使 X 跳过该列并抓取其余所有列。您将从 DataFrame 的索引一到末尾进行识别。这写作1:。您可以写1:9,这将抓取相同的集合,但1:表示“从索引零之后的所有内容”。iloc索引格式表示您为 DataFrame 子集选择的范围。

Y 值,或答案,是通过抓取Survived列来选择的。由于这是单列,无需使用iloc不要忘记对测试数据集执行相同操作

机器学习模型期望张量,而由于 Danfo.js 建立在 TensorFlow.js 上,将 DataFrame 转换为张量非常简单。最终,您可以通过访问.tensor属性将 DataFrame 转换为张量。

// Get cleaned data
const df = await dfd.read_csv("file://../../extra/cleaned/newTrain.csv");
console.log("Train Size", df.shape[0]);
const dft = await dfd.read_csv("file://../../extra/cleaned/newTest.csv");
console.log("Test Size", dft.shape[0]);

// Split train into X/Y
const trainX = df.iloc({ columns: [`1:`] }).tensor;
const trainY = df["Survived"].tensor;

// Split test into X/Y
const testX = dft.iloc({ columns: [`1:`] }).tensor;
const testY = dft["Survived"].tensor;

这些值已准备好被馈送到一个用于训练的模型中。

我在这个问题上使用的模型经过很少的研究后是一个具有三个隐藏层和一个具有 Sigmoid 激活的输出张量的序列层模型。

模型的组成如下:

model.add(
  tf.layers.dense({
    inputShape,
    units: 120,
    activation: "relu", // ①
    kernelInitializer: "heNormal", // ②
  })
);
model.add(tf.layers.dense({ units: 64, activation: "relu" }));
model.add(tf.layers.dense({ units: 32, activation: "relu" }));
model.add(
  tf.layers.dense({
    units: 1,
    activation: "sigmoid", // ③
  })
);

model.compile({
  optimizer: "adam",
  loss: "binaryCrossentropy", // ④
  metrics: ["accuracy"],      // ⑤
});

每一层都使用 ReLU 激活,直到最后一层。

这一行告诉模型根据算法初始化权重,而不是简单地将模型的初始权重设置为完全随机。这有时可以帮助模型更接近答案。在这种情况下并不是关键,但这是 TensorFlow.js 的一个有用功能。

最后一层使用 Sigmoid 激活来打印一个介于零和一之间的数字(生存或未生存)。

在训练二元分类器时,最好使用一个与二元分类一起工作的花哨命名的函数来评估损失。

这显示了日志中的准确性,而不仅仅是损失。

当您将模型fit到数据时,您可以识别测试数据,并获得模型以前从未见过的数据的结果。这有助于防止过拟合:

await model.fit(trainX, trainY, {
  batchSize: 32,
  epochs: 100,
  validationData: [testX, testY] // ①
})

提供模型应该在每个 epoch 上验证的数据。

注意

在前面的fit方法中显示的训练配置没有利用回调。如果您在tfjs-node上训练,您将自动看到训练结果打印到控制台。如果您使用tfjs,您需要添加一个onEpochEnd回调来打印训练和验证准确性。这两者的示例都在相关的本章源代码中提供。

在训练了 100 个 epoch 后,这个模型在训练数据上的准确率为 83%,在测试集的验证上也是 83%。从技术上讲,每次训练的结果会有所不同,但它们应该几乎相同:acc=0.827 loss=0.404 val_acc=0.831 val_loss=0.406

该模型已经识别出一些模式,并击败了纯粹的机会(50%准确率)。很多人在这里停下来庆祝创造一个几乎没有努力就能工作 83%的模型。然而,这也是一个很好的机会来认识 Danfo.js 和特征工程的好处。

特征工程

如果你在互联网上浏览一下,80%是Titanic数据集的一个常见准确率分数。我们已经超过了这个分数,而且没有真正的努力。然而,仍然有改进模型的空间,这直接来源于改进数据。

抛出空白数据是一个好选择吗?存在可以更好强调的相关性吗?模式是否被正确组织为模型?您能预先处理和组织数据得越好,模型就越能找到和强调模式。许多机器学习的突破都来自于在将模式传递给神经网络之前简化模式的技术。

这是“只是倾倒数据”停滞不前的地方,特征工程开始发展。Danfo.js 让您通过分析模式和强调关键特征来提升您的特征。您可以在交互式的 Node.js 读取求值打印循环(REPL)中进行这项工作,或者甚至可以利用为评估和反馈循环构建的网页。

让我们尝试通过确定并向数据添加特征来提高上述模型的准确率至 83%以上,使用一个名为 Dnotebook 的 Danfo.js Notebook。

Dnotebook

Danfo 笔记本,或Dnotebook,是一个交互式网页,用于使用 Danfo.js 实验、原型设计和定制数据。Python 的等价物称为 Jupyter 笔记本。您可以通过这个笔记本实现的数据科学将极大地帮助您的模型。

我们将使用 Dnotebook 来创建和共享实时代码,以及利用内置的图表功能来查找泰坦尼克号数据集中的关键特征和相关性。

通过创建全局命令来安装 Dnotebook:

$ npm install -g dnotebook

当您运行$ dnotebook时,将自动运行本地服务器并打开一个页面到本地笔记本站点,它看起来有点像图 9-4。

Dnotebook 新鲜截图

图 9-4。正在运行的新鲜 Dnotebook

每个 Dnotebook 单元格可以是代码或文本。文本采用 Markdown 格式。代码可以打印输出,并且未使用constlet初始化的变量可以在单元格之间保留。请参见图 9-5 中的示例。

Dnotebook 演示截图

图 9-5。使用 Dnotebook 单元格

图 9-5 中的笔记本可以从本章的extra/dnotebooks文件夹中的explaining_vars.json文件中下载并加载。这使得它适合用于实验、保存和共享。

泰坦尼克号视觉

如果您可以在数据中找到相关性,您可以将其作为训练数据中的附加特征强调,并在理想情况下提高模型的准确性。使用 Dnotebook,您可以可视化数据并在途中添加评论。这是分析数据集的绝佳资源。我们将加载两个 CSV 文件并将它们组合,然后直接在笔记本中打印结果。

您可以创建自己的笔记本,或者可以从相关源代码加载显示的笔记本的 JSON。只要您能够跟上图 9-6 中显示的内容,任何方法都可以。

指导性代码截图

图 9-6。加载 CSV 并在 Dnotebook 中组合它们

load_csv命令类似于read_csv命令,但在加载 CSV 内容时在网页上显示友好的加载动画。您可能还注意到了table命令的使用。table命令类似于 DataFrame 的print(),只是它为笔记本生成了输出的 HTML 表格,就像您在图 9-6 中看到的那样。

现在您已经有了数据,让我们寻找可以强调的重要区别,以供我们的模型使用。在电影《泰坦尼克号》中,当装载救生艇时他们大声喊着“妇女和儿童优先”。那真的发生了吗?一个想法是检查男性与女性的幸存率。您可以通过使用groupby来做到这一点。然后您可以打印每个组的平均值。

grp = mega_df.groupby(['Sex'])
table(grp.col(['Survived']).mean())

而且哇啊!您可以看到 83%的女性幸存下来,而只有 14%的男性幸存下来,正如图 9-7 中所示。

幸存率截图

图 9-7。女性更有可能幸存

您可能会想知道也许只是因为泰坦尼克号上有更多女性,这就解释了倾斜的结果,所以您可以快速使用count()来检查,而不是像刚才那样使用mean()

survival_count = grp.col(['Survived']).count()
table(survival_count)

通过打印的结果,您可以看到尽管幸存比例偏向女性,但幸存的男性要多得多。这意味着性别是幸存机会的一个很好的指标,因此应该强调这一特征。

使用 Dnotebook 的真正优势在于它利用了 Danfo.js 图表。例如,如果我们想看到幸存者的直方图,而不是分组用户,您可以查询所有幸存者,然后绘制结果。

要查询幸存者,您可以使用 DataFrame 的 query 方法:

survivors = mega_df.query({column: "Survived", is: "==", to: 1 })

然后,要在 Dnotebooks 中打印图表,您可以使用内置的viz命令,该命令需要一个 ID 和回调函数,用于填充笔记本中生成的 DIV。

直方图可以使用以下方式创建:

viz(`agehist`, x => survivors["Age"].plot(x).hist())

然后笔记本将显示生成的图表,如图 9-8 所示。

存活直方图的屏幕截图

图 9-8. 幸存者年龄直方图

在这里,您可以看到儿童的显着存活率高于老年人。再次,确定每个年龄组的数量和百分比可能值得,但似乎特定的年龄组或区间比其他年龄组表现更好。这给了我们可能改进模型的第二种方法。

让我们利用我们现在拥有的信息,再次尝试打破 83%准确率的记录。

创建特征(又称预处理)

在成长过程中,我被告知激活的神经元越多,记忆就会越强烈,因此请记住气味、颜色和事实。让我们看看神经网络是否也是如此。我们将乘客性别移动到两个输入,并创建一个经常称为分桶分箱的年龄分组。

我们要做的第一件事是将性别从一列移动到两列。这通常称为独热编码。目前,Sex具有数字编码。乘客性别的独热编码版本将0转换为[1, 0],将1转换为[0, 1],成功地将值移动到两列/单元。转换后,您删除Sex列并插入两列,看起来像图 9-9。

Danfo One-Hot Coded

图 9-9. 描述性别独热编码

要进行独热编码,Danfo.js 和 Pandas 都有一个get_dummies方法,可以将一列转换为多个列,其中只有一个列的值为 1。在 TensorFlow.js 中,进行独热编码的方法称为oneHot,但在 Danfo.js 中,get_dummies是向二进制变量致敬的方法,统计学中通常称为虚拟变量。编码结果后,您可以使用dropaddColumn进行切换:

// Handle person sex - convert to one-hot
const sexOneHot = dfd.get_dummies(mega['Sex']) // ①
sexOneHot.head().print()
// Swap one column for two
mega.drop({ columns: ['Sex'], axis: 1, inplace: true }) // ②
mega.addColumn({ column: 'male', value: sexOneHot['0'] }) // ③
mega.addColumn({ column: 'female', value: sexOneHot['1'] })

使用get_dummies对列进行编码

Sex列上使用inplace删除

添加新列,将标题切换为男性/女性

接下来,您可以使用apply方法为年龄创建桶。apply方法允许您在整个列上运行条件代码。根据我们的需求,我们将定义一个在我们的图表中看到的重要年龄组的函数,如下所示:

// Group children, young, and over 40yrs
function ageToBucket(x) {
  if (x < 10) {
    return 0
  } else if (x < 40) {
    return 1
  } else {
    return 2
  }
}

然后,您可以使用您定义的ageToBucket函数创建并添加一个完全新的列来存储这些桶:

// Create Age buckets
ageBuckets = mega['Age'].apply(ageToBucket)
mega.addColumn({ column: 'Age_bucket', value: ageBuckets })

这添加了一个值范围从零到二的整列。

最后,我们可以将我们的数据归一化为介于零和一之间的数字。缩放值会使值之间的差异归一化,以便模型可以识别模式和缩放原始数字中扭曲的差异。

注意

将归一化视为一种特征。如果您正在处理来自各个国家的 10 种不同货币,可能会感到困惑。归一化会缩放输入,使它们具有相对影响的大小。

const scaler = new dfd.MinMaxScaler()
scaledData = scaler.fit(featuredData)
scaledData.head().print()

从这里,您可以为训练编写两个 CSV 文件并开始!另一个选项是您可以编写一个单独的 CSV 文件,而不是使用特定的 X 和 Y 值设置validationData,您可以设置一个名为validationSplit的属性,该属性将为验证数据拆分出一定比例的数据。这样可以节省我们一些时间和麻烦,所以让我们使用validationSplit来训练模型,而不是显式传递validationData

生成的fit如下所示:

await model.fit(trainX, trainY, {
  batchSize: 32,
  epochs: 100,
  // Keep random 20% for validation on the fly.
  // The 20% is selected at the beginning of the training session.
  validationSplit: 0.2,
})

模型使用新数据进行 100 个时代的训练,如果您使用tfjs-node,即使没有定义回调函数,也可以看到打印的结果。

特征工程训练结果

上次,模型准确率约为 83%。现在,使用相同的模型结构但添加了一些特征,我们达到了 87%的训练准确率和 87%的验证准确率。具体来说,我的结果是acc=0.867 loss=0.304 val_acc=0.871 val_loss=0.370

准确性提高了,损失值低于以前。真正了不起的是,准确性和验证准确性都是对齐的,因此模型不太可能过拟合。这通常是神经网络在泰坦尼克号数据集中的较好得分之一。对于这样一个奇怪的问题,创建一个相当准确的模型已经达到了解释如何从数据中提取有用信息的目的。

审查结果

解决泰坦尼克号问题以达到 87%的准确率需要一些技巧。您可能仍然在想结果是否可以改进,答案肯定是“是”,因为其他人已经在排行榜上发布了更令人印象深刻的分数。在没有排行榜的情况下,评估是否有增长空间的常见方法是与一个受过教育的人在面对相同问题时的得分进行比较。

如果您是一个高分狂热者,章节挑战将有助于改进我们已经创建的令人印象深刻的模型。一定要练习工程特征,而不是过度训练,从而使模型过度拟合以基本上记住答案。

查找重要值、归一化特征和强调显著相关性是机器学习训练中的一项有用技能,现在您可以使用 Danfo.js 来实现这一点。

章节回顾

那么在本章开始时我们识别的那个个体发生了什么?凯特·康诺利小姐,一个 30 岁的持有三等舱票的女人,确实幸存了泰坦尼克号事故,模型也认同。

我们是否错过了一些提高机器学习模型准确性的史诗机会?也许我们应该用-1填充空值而不是删除它们?也许我们应该研究一下泰坦尼克号的船舱结构?或者我们应该查看parchsibsppclass,为独自旅行的三等舱乘客创建一个新列?“我永远不会放手!”

并非所有数据都可以像泰坦尼克号数据集那样被清理和特征化,但这对于机器学习来说是一次有用的数据科学冒险。有很多 CSV 文件可用,自信地加载、理解和处理它们对于构建新颖模型至关重要。像 Danfo.js 这样的工具使您能够处理这些海量数据,现在您可以将其添加到您的机器学习工具箱中。

注意

如果您已经是其他 JavaScript 笔记本的粉丝,比如ObservableHQ.com,Danfo.js 也可以导入并与这些笔记本轻松集成。

处理数据是一件复杂的事情。有些问题更加明确,根本不需要对特征进行任何调整。如果您感兴趣,可以看看像帕尔默企鹅这样的更简单的数据集。这些企鹅根据它们的嘴的形状和大小明显地区分为不同的物种。另一个简单的胜利是第七章中提到的鸢尾花数据集。

章节挑战:船只发生了什么

您知道在泰坦尼克号沉没中没有一个牧师幸存下来吗?像先生、夫人、小姐、牧师等这样的头衔桶/箱可能对模型的学习有用。这些敬称——是的,就是它们被称为的——可以从被丢弃的Name列中收集和分析。

在这个章节挑战中,使用 Danfo.js 识别在泰坦尼克号上使用的敬称及其相关的生存率。这是一个让您熟悉 Dnotebooks 的绝佳机会。

您可以在附录 B 中找到这个挑战的答案。

审查问题

让我们回顾一下你在本章编写的代码中学到的教训。花点时间回答以下问题:

  1. 对于一个石头-剪刀-布分类器,你会使用什么样的激活函数?

  2. 在一个 sigmoid“狗还是不是狗”模型的最终层中会放置多少个节点?

  3. 加载一个具有内置 Danfo.js 的交互式本地托管笔记本的命令是什么?

  4. 如何将具有相同列的两个 CSV 文件的数据合并?

  5. 你会使用什么命令将单个列进行独热编码成多个列?

  6. 你可以使用什么来将 DataFrame 的所有值在 0 和 1 之间进行缩放?

这些练习的解决方案可以在附录 A 中找到。

第十章:图像训练

“一切都应该尽可能简单。但不要过于简单。”

—阿尔伯特·爱因斯坦

我将向您描述一个数字。我希望您根据其特征的描述来猜出这个数字。我想的数字顶部是圆的,右侧只有一条线,底部有一个重叠的环状物。花点时间,心理上映射我刚刚描述的数字。有了这三个特征,你可能可以猜出来。

视觉数字的特征可能会有所不同,但聪明的描述意味着您可以在脑海中识别数字。当我说“圆顶”时,您可能会立即排除一些数字,同样的情况也适用于“只有右侧有一条线”。这些特征组成了数字的独特之处。

如果您按照图 10-1 中显示的数字描述,并将其描述放入 CSV 文件中,您可能可以通过训练好的神经网络在这些数据上获得 100%的准确性。整个过程都很顺利,只是依赖于人类描述每个数字的顶部、中部和底部。如何自动化这个人类方面呢?如果您可以让计算机识别图像的独特特征,如环、颜色和曲线,然后将其输入神经网络,机器就可以学习将描述分类为图像所需的模式。

数字二的三部分。

图 10-1。如果您发现这是数字二,恭喜您。

幸运的是,通过出色的计算机视觉技巧,解决了图像特征工程的问题。

我们将:

  • 学习模型的卷积层的概念

  • 构建您的第一个多类模型

  • 学习如何读取和处理图像以供 TensorFlow.js 使用

  • 在画布上绘制并相应地对绘图进行分类

完成本章后,您将能够创建自己的图像分类模型。

理解卷积

卷积来自于数学世界中表达形状和函数的概念。您可以深入研究卷积在数学中的概念,然后从头开始将这些知识重新应用到数字图像信息的收集概念。如果您是数学和计算机图形的爱好者,这是一个非常令人兴奋的领域。

然而,当你有像 TensorFlow.js 这样的框架时,花费一周学习卷积操作的基础并不是必要的。因此,我们将专注于卷积操作的高级优势和特性,以及它们在神经网络中的应用。始终鼓励您在这个快速入门之外深入研究卷积的深层历史。

让我们看看您应该从非数学解释的卷积中获得的最重要概念。

图 10-2 中的两个数字二的图像是完全相同的数字,只是它们在边界框中从左到右移动。将这两个数字转换为张量后,会创建出两个明显不同的不相等张量。然而,在本章开头描述的特征系统中,这些特征将是相同的。

卷积特征与无特征

图 10-2。卷积简化了图像的本质

对于视觉张量,图像的特征比每个像素的确切位置更重要。

卷积快速总结

卷积操作用于提取高级特征,如边缘、梯度、颜色等。这些是分类给定视觉的关键特征。

那么应该提取哪些特征呢?这并不是我们实际决定的。您可以控制在查找特征时使用的滤波器数量,但最好定义可用模式的实际特征是在训练过程中定义的。这些滤波器从图像中突出和提取特征。

例如,看一下图 10-3 中的照片。南瓜灯是多种颜色,几乎与模糊但略带明暗的背景形成对比。作为人类,您可以轻松识别出照片中的内容。

南瓜灯艺术的照片

图 10-3. 南瓜灯艺术

现在这是同一图像,通过 3 x 3 的边缘检测滤波器卷积在像素上。注意结果在图 10-4 中明显简化和更明显。

不同的滤波器突出显示图像的不同方面,以简化和澄清内容。不必止步于此;您可以运行激活以强调检测到的特征,甚至可以在卷积上运行卷积。

结果是什么?您已经对图像进行了特征工程。通过各种滤波器对图像进行预处理,让您的神经网络看到原本需要更大、更慢和更复杂模型才能看到的模式。

前一图像的卷积结果

图 10-4. 卷积结果

添加卷积层

感谢 TensorFlow.js,添加卷积层与添加密集层一样简单,但称为conv2d,并具有自己的属性。

tf.layers.conv2d({
  filters: 32, // ①
  kernelSize: 3, // ②
  strides: 1, // ③
  padding: 'same', // ④
  activation: 'relu',  // ⑤
  inputShape: [28, 28, 1] // ⑥
})

确定要运行多少个滤波器。

kernelSize控制滤波器的大小。这里的3表示 3 x 3 的滤波器。

小小的 3 x 3 滤波器不适合您的图像,因此需要在图像上滑动。步幅是滤波器每次滑动的像素数。

填充允许卷积在strideskernelSize不能均匀地分割成图像宽度和高度时决定如何处理。当将填充设置为same时,会在图像周围添加零,以保持生成的卷积图像的大小不变。

然后将结果通过您选择的激活函数运行。

输入是一个图像张量,因此输入图像是模型的三维形状。这不是卷积的必需限制,正如您在第六章中学到的那样,但如果您不是在制作完全卷积模型,则建议这样做。

不要被可能的参数列表所压倒。想象一下自己必须编写所有这些不同设置。您可以像现有模型那样配置您的卷积,也可以使用数字进行调整以查看其对结果的影响。调整这些参数并进行实验是 TensorFlow.js 等框架的好处。最重要的是,随着时间的推移建立您的直觉。

重要的是要注意,这个conv2d层是用于图像的。同样,您将在线性序列上使用conv1d,在处理 3D 空间对象时使用conv3d。大多数情况下,使用 2D,但概念并不受限制。

理解最大池化

通过卷积层使用滤波器简化图像后,您在过滤后的图形中留下了大量空白空间。此外,由于所有图像滤波器,输入参数的数量显着增加。

最大池化是简化图像中识别出的最活跃特征的一种方法。

最大池化快速总结

为了压缩生成的图像大小,您可以使用最大池化来减少输出。简单地说,最大池化是将窗口中最活跃的像素保留为该窗口中所有像素块的表示。然后您滑动窗口并取其中的最大值。只要窗口的步幅大于 1,这些结果就会汇总在一起,以生成一个更小的图像。

以下示例通过取每个子方块中的最大数来将图像的大小分成四分之一。研究图 10-5 中的插图。

最大池化演示

图 10-5。2 x 2 核和步幅为 2 的最大池

在图 10-5 中的kernelSize为 2 x 2。因此,四个左上角的方块一起进行评估,从数字[12, 5, 11, 7]中,最大的是12。这个最大数传递给结果。步幅为 2,核窗口的方块完全移动到前一个方块的相邻位置,然后重新开始使用数字[20, 0, 12, 3]。这意味着每个窗口中最强的激活被传递下去。

你可能会觉得这个过程会切割图像并破坏内容,但你会惊讶地发现,生成的图像是相当容易识别的。最大池化甚至强调了检测,并使图像更容易识别。参见图 10-6,这是在之前的南瓜灯卷积上运行最大池的结果。

最大池化强调检测

图 10-6。卷积的 2 x 2 核最大池结果

虽然图 10-4 和图 10-6 在插图目的上看起来相同大小,但后者由于池化过程而稍微清晰一些,并且是原始大小的四分之一。

添加最大池化层

类似于conv2d,最大池化被添加为一层,通常紧跟在卷积之后:

tf.layers.maxPooling2d({
  poolSize: 2, // ①
  strides: 2   // ②
})

poolSize是窗口大小,就像kernelSize一样。之前的例子都是 2(代表 2 x 2)。

strides是在每次操作中向右和向下移动窗口的距离。这也可以写成strides: [2, 2]

通常,阅读图像的模型会有几层卷积,然后池化,然后再次卷积和池化。这会消耗图像的特征,并将它们分解成可能识别图像的部分。⁠²

训练图像分类

经过几层卷积和池化后,你可以将结果滤波器展平或序列化成一个单一链,并将其馈送到一个深度连接的神经网络中。这就是为什么人们喜欢展示 MNIST 训练示例;它是如此简单,以至于你实际上可以在一个图像中观察数据。

看一下使用卷积和最大池化对数字进行分类的整个过程。图 10-7 应该从底部向顶部阅读。

MNIST 逐层输出

图 10-7。MNIST 处理数字五

如果你跟随图 10-7 中显示的这幅图像的过程,你会看到底部的输入,然后是该输入与六个滤波器的卷积直接在其上方。接下来,这六个滤波器被最大池化或“下采样”,你可以看到它们变小了。然后再进行一次卷积和池化,然后将它们展平到一个完全连接的密集网络层。在展平的层上方是一个密集层,顶部的最后一个小层是一个具有 10 个可能解的 softmax 层。被点亮的是“五”。

从鸟瞰视角看,卷积和池化看起来像魔术,但它将图像的特征消化成神经元可以识别的模式。

在分层模型中,这意味着第一层通常是卷积和池化风格的层,然后它们被传递到神经网络中。图 10-8 展示了这个过程的高层视图。以下是三个阶段:

  1. 输入图像

  2. 特征提取

  3. 深度连接的神经网络

CNN 流程图

图 10-8。CNN 的三个基本阶段

处理图像数据

使用图像进行训练的一个缺点是数据集可能非常庞大且难以处理。数据集通常很大,但对于图像来说,它们通常是巨大的。这也是为什么相同的视觉数据集一遍又一遍地被使用的另一个原因。

即使图像数据集很小,当加载到内存张量形式时,它可能占用大量内存。你可能需要将训练分成张量的块,以处理庞大的图像集。这可能解释了为什么像 MobileNet 这样的模型被优化为今天标准下被认为相对较小的尺寸。在所有图像上增加或减少一个像素会导致指数级的尺寸差异。由于数据的本质,灰度张量在内存中是 RGB 图像的三分之一大小,是 RGBA 图像的四分之一大小。

分拣帽

现在是时候进行你的第一个卷积神经网络了。对于这个模型,你将训练一个 CNN 来将灰度绘画分类为 10 个类别。

如果你是 J.K.罗琳的流行书系列《哈利·波特》的粉丝,这将是有意义且有趣的。然而,如果你从未读过一本《哈利·波特》的书或者看过任何一部电影,这仍然是一个很好的练习。在书中,魔法学校霍格沃茨有四个学院,每个学院都有与之相关联的动物。你将要求用户画一幅图片,并使用该图片将它们分类到各个学院。我已经准备了一个数据集,其中的绘画在某种程度上类似于每个组的图标和动物。

我准备的数据集是从Google 的 Quick, Draw!数据集中的一部分绘画中制作的。类别已经缩减到 10 个,并且数据已经经过了大幅清理。

与本章相关的代码可以在chapter10/node/node-train-houses找到,你会发现一个包含数万个 28 x 28 绘画的 ZIP 文件,包括以下内容:

  1. 鸟类

  2. 猫头鹰

  3. 鹦鹉

  4. 蜗牛

  5. 狮子

  6. 老虎

  7. 浣熊

  8. 松鼠

  9. 头巾

这些绘画变化很大,但每个类别的特征是可辨认的。这里是一些涂鸦的随机样本,详见图 10-9。一旦你训练了一个模型来识别这 10 个类别中的每一个,你就可以使用该模型将类似特定动物的绘画分类到其相关联的学院。鸟类去拉文克劳,狮子和老虎去格兰芬多,等等。

霍格沃茨学院绘画网格形式

图 10-9. 绘画的 10 个类别

处理这个问题的方法有很多,但最简单的方法是使用 softmax 对最终层进行模型分类。正如你记得的那样,softmax 会给我们 N 个数字,它们的总和都为一。例如,如果一幅图是 0.67 的鸟,0.12 的猫头鹰,和 0.06 的鹦鹉,因为它们都代表同一个学院,我们可以将它们相加,结果总是小于一。虽然你熟悉使用返回这种结果的模型,但这将是你从头开始创建的第一个 softmax 分类模型。

入门

有几种方法可以使用 TensorFlow.js 来训练这个模型。将几兆字节的图像加载到浏览器中可以通过几种方式完成:

  • 你可以使用后续的 HTTP 请求加载每个图像。

  • 你可以将训练数据合并到一个大的精灵表中,然后使用你的张量技能来提取和堆叠每个图像到 X 和 Y 中。

  • 你可以将图像加载到 CSV 中,然后将它们转换为张量。

  • 你可以将图像进行 Base64 编码,并从单个 JSON 文件加载它们。

你在这里看到的一个常见问题是,你必须做一些额外的工作,将数据加载到浏览器的沙盒中。因此,最好使用 Node.js 进行具有大规模数据集的图像训练。我们将在本书后面讨论这种情况不那么重要的情况。

与本章相关的 Node.js 代码包含了你需要的训练数据。你会在存储库中看到一个接近 100MB 的文件(GitHub 对单个文件的限制),你需要解压到指定位置(见图 10-10)。

解压文件.zip 截图

图 10-10. 将图像解压缩到文件夹中

现在你有了图片,也知道如何在 Node.js 中读取图片,训练这个模型的代码会类似于示例 10-1。

示例 10-1. 理想的设置
  // Read images
  const [X, Y] = await folderToTensors() // ①

  // Create layers model
  const model = getModel() // ②

  // Train
  await model.fit(X, Y, {
    batchSize: 256,
    validationSplit: 0.1,
    epochs: 20,
    shuffle: true, // ③
  })

  // Save
  model.save('file://model_result/sorting_hat') // ④

  // Cleanup!
  tf.dispose([X, Y, model])
  console.log('Tensors in memory', tf.memory().numTensors)

创建一个简单的函数将图片加载到所需的 X 和 Y 张量中。

创建一个适合的 CNN 层模型。

使用shuffle属性,该属性会对当前批次进行混洗。

将生成的训练模型保存在本地。

注意

示例 10-1 中的代码没有提及设置任何测试数据。由于这个项目的性质,真正的测试将在绘制图像并确定每个笔画如何使图像更接近或更远离期望目标时进行。在训练中仍将使用验证集。

转换图像文件夹

folderToTensors函数需要执行以下操作:

  1. 识别所有 PNG 文件路径。

  2. 收集图像张量和答案。

  3. 随机化两组数据。

  4. 归一化和堆叠张量。

  5. 清理并返回结果。

要识别和访问所有图像,可以使用类似glob的库,它接受一个给定的路径,如files/**/.png*,并返回一个文件名数组。/**会遍历该文件夹中的所有子文件夹,并找到每个文件夹中的所有 PNG 文件。

你可以通过以下方式使用 NPM 安装glob

$ npm i glob

现在节点模块可用,可以被要求或导入:

const glob = require('glob')
// OR
import { default as glob } from 'glob'

由于 glob 是通过回调来操作的,你可以将整个函数包装在 JavaScript promise 中,以将其转换为异步/等待。如果你对这些概念不熟悉,可以随时查阅相关资料或仅仅学习本章提供的代码。

在收集了一组文件位置之后,你可以加载文件,将其转换为张量,甚至通过查看图像来自哪个文件夹来确定每个图像的“答案”或“y”。

记住,每次需要修改张量时都会创建一个新张量。因此,最好将张量保存在 JavaScript 数组中,而不是在进行归一化和堆叠张量时逐步进行。

将每个字符串读入这两个数组的过程可以通过以下方式完成:

files.forEach((file) => {
  const imageData = fs.readFileSync(file)
  const answer = encodeDir(file)
  const imageTensor = tf.node.decodeImage(imageData, 1)

  // Store in memory
  YS.push(answer)
  XS.push(imageTensor)
})

encodeDir函数是我编写的一个简单函数,用于查看每个图像的路径并返回一个相关的预测数字:

function encodeDir(filePath) {
  if (filePath.includes('bird')) return 0
  if (filePath.includes('lion')) return 1
  if (filePath.includes('owl')) return 2
  if (filePath.includes('parrot')) return 3
  if (filePath.includes('raccoon')) return 4
  if (filePath.includes('skull')) return 5
  if (filePath.includes('snail')) return 6
  if (filePath.includes('snake')) return 7
  if (filePath.includes('squirrel')) return 8
  if (filePath.includes('tiger')) return 9

  // Should never get here
  console.error('Unrecognized folder')
  process.exit(1)
}

一旦将图片转换为张量形式,你可能会考虑堆叠和返回它们,但在此之前至关重要的是在混洗之前对它们进行混洗。如果不混合数据,你的模型将会以最奇怪的方式快速训练。请容我用一个奇特的比喻。

想象一下,如果我让你在一堆形状中指出我在想的形状。你很快就会发现我总是在想圆形,然后你开始获得 100%的准确率。在我们的第三次测试中,我开始说,“不,那不是正方形!你错得很离谱。”于是你开始指向正方形,再次获得 100%的准确率。每三次测试,我都会改变形状。虽然你的分数超过 99%的准确率,但你从未学会选择哪个形状的实际指标。因此,当形状每次都在变化时,你就会失败。你从未学会指标,因为数据没有被混洗。

未经混洗的数据将产生相同的效果:训练准确率接近完美,而验证和测试分数则很差。即使你对每个数据集进行混洗,大部分时间你只会对相同的 256 个值进行混洗。

要对 X 和 Y 进行相同的排列混洗,可以使用tf.utils.shuffleCombo我听说向 TensorFlow.js 添加此功能的人非常酷。

// Shuffle the data (keep XS[n] === YS[n])
tf.util.shuffleCombo(XS, YS)

由于这是对 JavaScript 引用进行混洗,因此在此混洗中不会创建新的张量。

最后,您将希望将答案从整数转换为独热编码。独热编码是因为您的模型将使用 softmax,即 10 个值相加为 1,其中正确答案是唯一的主导值。

TensorFlow.js 有一个名为oneHot的方法,它将数字转换为独热编码的张量值。例如,从 5 个可能类别中的数字3将被编码为张量[0,0,1,0,0]。这就是我们希望格式化答案以匹配分类模型预期输出的方式。

现在,您可以将 X 和 Y 数组值堆叠成一个大张量,并通过除以255来将图像归一化为值0-1。堆叠和编码看起来像这样:

// Stack values
console.log('Stacking')
const X = tf.stack(XS)
const Y = tf.oneHot(YS, 10)

console.log('Images all converted to tensors:')
console.log('X', X.shape)
console.log('Y', Y.shape)

// Normalize X to values 0 - 1
const XNORM = X.div(255)
// cleanup
tf.dispose([XS, X])

由于处理数千个图像,您的计算机可能会在每个日志之间暂停。代码打印以下内容:

Stacking
Images all converted to tensors:
X [ 87541, 28, 28, 1 ]
Y [ 87541, 10 ]

现在我们有了用于训练的 X 和 Y,它们的形状是我们将创建的模型的输入和输出形状。

CNN 模型

现在是创建卷积神经网络模型的时候了。该模型的架构将是三对卷积和池化层。在每个新的卷积层上,我们将使滤波器的数量加倍。然后我们将将模型展平为一个具有 128 个单元的单个密集隐藏层,具有tanh激活,并以具有 softmax 激活的 10 个可能输出的最终层结束。如果您对为什么使用 softmax 感到困惑,请回顾我们在第九章中介绍的分类模型的结构。

您应该能够仅通过描述编写模型层,但这里是创建所描述的顺序模型的代码:

const model = tf.sequential()

// Conv + Pool combo
model.add(
  tf.layers.conv2d({
    filters: 16,
    kernelSize: 3,
    strides: 1,
    padding: 'same',
    activation: 'relu',
    kernelInitializer: 'heNormal',
    inputShape: [28, 28, 1],
  })
)
model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }))

// Conv + Pool combo
model.add(
  tf.layers.conv2d({
    filters: 32,
    kernelSize: 3,
    strides: 1,
    padding: 'same',
    activation: 'relu',
  })
)
model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }))

// Conv + Pool combo
model.add(
  tf.layers.conv2d({
    filters: 64,
    kernelSize: 3,
    strides: 1,
    padding: 'same',
    activation: 'relu',
  })
)
model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }))

// Flatten for connecting to deep layers
model.add(tf.layers.flatten())

// One hidden deep layer
model.add(
  tf.layers.dense({
    units: 128,
    activation: 'tanh',
  })
)
// Output
model.add(
  tf.layers.dense({
    units: 10,
    activation: 'softmax',
  })
)

这个新的用于非二进制分类数据的最终层意味着您需要将损失函数从binaryCrossentropy更改为categoricalCrossentropy。因此,现在model.compile的代码看起来像这样:

model.compile({
  optimizer: 'adam',
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
})

让我们通过我们学到的关于卷积和最大池化的知识来审查model.summary()方法,以确保我们已经正确构建了一切。您可以在示例 10-2 中看到结果的打印输出。

示例 10-2。model.summary()的输出
_________________________________________________________________
Layer (type)                 Output shape              Param #
=================================================================
conv2d_Conv2D1 (Conv2D)      [null,28,28,16]           160         // ①
_________________________________________________________________
max_pooling2d_MaxPooling2D1  [null,14,14,16]           0           // ②
_________________________________________________________________
conv2d_Conv2D2 (Conv2D)      [null,14,14,32]           4640        // ③
_________________________________________________________________
max_pooling2d_MaxPooling2D2  [null,7,7,32]             0
_________________________________________________________________
conv2d_Conv2D3 (Conv2D)      [null,7,7,64]             18496       // ④
_________________________________________________________________
max_pooling2d_MaxPooling2D3  [null,3,3,64]             0
_________________________________________________________________
flatten_Flatten1 (Flatten)   [null,576]                0           // ⑤
_________________________________________________________________
dense_Dense1 (Dense)         [null,128]                73856       // ⑥
_________________________________________________________________
dense_Dense2 (Dense)         [null,10]                 1290        // ⑦
=================================================================
Total params: 98442
Trainable params: 98442
Non-trainable params: 0
_________________________________________________________________

第一个卷积层的输入为[stacksize, 28, 28, 1],卷积输出为[stacksize, 28, 28, 16]。大小相同是因为我们使用了padding: 'same',而 16 是当我们指定filters: 16时得到的 16 个不同的滤波器结果。您可以将其视为每个堆栈中的每个图像的 16 个新滤波图像。这为网络提供了 160 个新参数进行训练。可训练参数的计算方式为(图像数量) * (卷积核窗口) * (输出图像) + (输出图像),计算结果为1 * (3x3) * 16 + 16 = 160

最大池化将滤波后的图像行和列大小减半,从而将像素分成四分之一。由于算法是固定的,因此此层没有任何可训练参数。

卷积和池化再次发生,并且在每个级别都使用更多的滤波器。图像的大小正在缩小,可训练参数的数量迅速增长,即16 * (3x3) * 32 + 32 = 4,640

在这里,有一个最终的卷积和池化。池化奇数会导致大于 50%的减少。

将 64 个 3 x 3 图像展平为一个由 576 个单元组成的单层。

这 576 个单元中的每一个都与 128 个单元的层密切连接。使用传统的线+节点计算,这将得到(576 * 128) + 128 = 73,856个可训练参数。

最后,最后一层有 10 个可能的值对应每个类别。

您可能想知道为什么我们评估model.summary()而不是检查正在发生的事情的图形表示。即使在较低的维度,图形表示正在发生的事情也很难说明。我已经尽力在图 10-11 中创建了一个相对详尽的插图。

CNN 神经网络

图 10-11. 每一层的可视化

与以往的神经网络图不同,CNN 的可视化解释可能有些局限性。堆叠在一起的滤波图像只能提供有限的信息。卷积过程的结果被展平并连接到一个深度连接的神经网络。您已经达到了一个复杂性的程度,summary()方法是理解内容的最佳方式。

提示

如果您想要一个动态的可视化,并观看每个训练滤波器在每一层的激活结果,数据科学 Polo Club 创建了一个美丽的CNN 解释器在 TensorFlow.js 中。查看交互式可视化器

你已经到了那里。您的结果[3, 3, 64]在连接到神经网络之前展平为 576 个人工神经元。您不仅创建了图像特征,还简化了一个[28, 28, 1]图像的输入,原本需要 784 个密集连接的输入。有了这种更先进的架构,您可以从folderToTensors()加载数据并创建必要的模型。您已经准备好训练了。

训练和保存

由于这是在 Node.js 中进行训练,您将不得不直接在机器上设置 GPU 加速。这通常是通过 NVIDIA CUDA 和 CUDA 深度神经网络(cuDNN)完成的。如果您想使用@tensorflow/tfjs-node-gpu进行训练并获得比普通tfjs-node更快的速度提升,您将不得不正确设置 CUDA 和 cuDNN 以与您的 GPU 一起工作。请参阅图 10-12。

CUDA GPU 截图

图 10-12. 使用 GPU 可以提高 3-4 倍的速度

在 20 个时代之后,生成的模型在训练中的准确率约为 95%,在验证集中的准确率约为 90%。生成模型的文件大小约为 400 KB。您可能已经注意到训练集的准确率不断上升,但验证集有时会下降。不管好坏,最后一个时代将是保存的模型。如果您想确保最高可能的验证准确性,请查看最后的章节挑战。

注意

如果您对这个模型运行了太多时代,模型将过拟合,并接近 100%的训练准确率,而验证准确率会降低。

测试模型

要测试这个模型,您需要用户的绘图。您可以在网页上创建一个简单的绘图表面,使用一个画布。画布可以订阅鼠标按下时、鼠标沿着画布移动时以及鼠标释放时的事件。使用这些事件,您可以从一个点绘制到另一个点。

构建一个草图板

您可以使用这三个事件构建一个简单的可绘制画布。您将使用一些新方法来移动画布路径和绘制线条,但这是相当易读的。以下代码设置了一个:

const canvas = document.getElementById("sketchpad");
const context = canvas.getContext("2d");
context.lineWidth = 14;
context.lineCap = "round";
let isIdle = true;

function drawStart(event) {
  context.beginPath();
  context.moveTo(
    event.pageX - canvas.offsetLeft,
    event.pageY - canvas.offsetTop
  );
  isIdle = false;
}
function drawMove(event) {
  if (isIdle) return;
  context.lineTo(
    event.pageX - canvas.offsetLeft,
    event.pageY - canvas.offsetTop
  );
  context.stroke();
}
function drawEnd() { isIdle = true; }
// Tie methods to events
canvas.addEventListener("mousedown", drawStart, false);
canvas.addEventListener("mousemove", drawMove, false);
canvas.addEventListener("mouseup", drawEnd, false);

这些图纸是由一堆较小的线条制成的,线条的笔画宽度为 14 像素,并且在边缘自动圆润。您可以在图 10-13 中看到一个测试绘图。

示例绘图

图 10-13. 运行得足够好

当用户在画布上单击鼠标时,任何移动都将从一个点绘制到新点。每当用户停止绘制时,将调用drawEnd函数。您可以添加一个按钮来对画布进行分类,或者直接将其连接到drawEnd函数并对图像进行分类。

阅读草图板

当你在画布上调用 tf.browser.fromPixels 时,你会得到 100% 的黑色像素。为什么会这样?答案是画布的某些地方没有绘制任何内容,而其他地方是黑色像素。当画布转换为张量值时,它会将空白转换为黑色。画布可能看起来有白色背景,但实际上是透明的,会显示底部的任何颜色或图案(参见 图 10-14)。

一个空的画布

图 10-14. 一个画布是透明的,所以空白像素为零

为了解决这个问题,你可以添加一个清除函数,在画布上绘制一个大的白色正方形,这样黑色线条就会在白色背景上,就像训练图像一样。这也是你可以在绘画之间清除画布的函数。要用白色背景填充画布,你可以使用 fillRect 方法,就像你在 第六章 中用来勾画标签的方法一样。

context.fillStyle = "#fff";
context.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight);

一旦画布用白色背景初始化,你就可以对画布绘制进行预测了。

async function makePrediction(canvas, model) {
  const drawTensor = tf.browser.fromPixels(canvas, 1) // ①
  const resize = tf.image.resizeNearestNeighbor(drawTensor, [28,28], true) // ②

  const batched = resize.expandDims() // ③
  const results = await model.predict(batched)
  const predictions = await results.array()

  // Display
  displayResults(predictions[0]) // ④
  // Cleanup
  tf.dispose([drawTensor, resize, batched, results])
}

当你读取画布时,不要忘记标识你只对单个通道感兴趣;否则,你需要在继续之前将 3D 张量转换为 1D 张量。

使用最近邻算法将图像调整为 28 x 28 的大小,以输入到模型中。最近邻引起的像素化在这里并不重要,所以这是一个明智的选择,因为它比 resizeBilinear 更快。

模型期望一个批次数据,所以准备数据作为一个批次的数据。这将创建一个 [1, 28, 28, 1] 的输入张量。

预测结果已经作为一个包含 10 个数字的批次返回到 JavaScript。想出一种创造性的方式来展示结果。

现在你已经得到了结果,你可以以任何你喜欢的格式展示答案。我个人是按照房间组织了分数,并用它们来设置标签的不透明度。这样,你可以在每画一条线时得到反馈。标签的不透明度取决于值 0-1,这与 softmax 预测结果非常契合。

function displayResults(predictions) {
  // Get Scores
  const ravenclaw = predictions[0] + predictions[2] + predictions[3]
  const gryffindor = predictions[1] + predictions[9]
  const hufflepuff = predictions[4] + predictions[8]
  const slytherin = predictions[6] + predictions[7]
  const deatheater = predictions[5]

  document.getElementById("ravenclaw").style.opacity = ravenclaw
  document.getElementById("gryffindor").style.opacity = gryffindor
  document.getElementById("hufflepuff").style.opacity = hufflepuff
  document.getElementById("slytherin").style.opacity = slytherin

  // Harry Potter fans will enjoy this one
  if (deatheater > 0.9) {
    alert('DEATH EATER DETECTED!!!')
  }
}

你可能会想知道拉文克劳是否有轻微的数学优势,因为它由更多类别组成,你是对的。在所有条件相同的情况下,一组完全随机的线更有可能被分类为拉文克劳,因为它拥有大多数类别。然而,当图像不是随机的时,这在统计上是不显著的。如果你希望模型只有九个类别,可以移除 bird 并重新训练,以创建最平衡的分类谱。

提示

如果你有兴趣确定哪些类别可能存在问题或混淆,你可以使用视觉报告工具,如混淆矩阵或 t-SNE 算法。这些工具对于评估训练数据特别有帮助。

我强烈建议你从chapter10/simple/simplest-draw加载本章的代码,并测试一下你的艺术技能!我的鸟类绘画将我分类到了拉文克劳,如 图 10-15 所示。

网页正确识别出一只鸟

图 10-15. 一个 UI 和绘画杰作

我能够糟糕地画出并被正确分类到其他可能的房间中。然而,我不会再用我的“艺术”来惩罚你。

章节回顾

你已经在视觉数据上训练了一个模型。虽然这个数据集仅限于灰度图,但你所学到的经验可以适用于任何图像数据集。有很多优秀的图像数据集可以与你创建的模型完美配合。我们将在接下来的两章中详细介绍。

我为本章中的绘画识别特色创建了一个更加复杂的页面。

章节挑战:保存魔法

如果您最感兴趣的是获得最高验证准确性模型,那么您的最佳模型版本很可能不是最后一个版本。例如,如果您查看图 10-16,90.3%的验证准确性会丢失,最终验证模型为 89.6%。

对于本章挑战,与其保存模型的最终训练版本,不如添加一个回调函数,当验证准确性达到新的最佳记录时保存模型。这种代码非常有用,因为它允许您运行多个时期。随着模型过拟合,您将能够保留最佳的通用模型用于生产。

验证与训练准确性

图 10-16。评估哪个准确性更重要

您可以在附录 B 中找到此挑战的答案。

复习问题

让我们回顾一下你在本章编写的代码中学到的教训。花点时间回答以下问题:

  1. 卷积层有许多可训练的什么,可以帮助提取图像的特征?

  2. 控制卷积窗口大小的属性名称是什么?

  3. 如果你希望卷积结果与原始图像大小相同,应该将填充设置为什么?

  4. 真或假?在将图像插入卷积层之前,必须将其展平。

  5. 在 81 x 81 图像上,步幅为 3 的最大池 3 x 3 的输出大小将是多少?

  6. 如果您要对数字 12 进行独热编码,您是否有足够的信息来这样做?

这些练习的解决方案可以在附录 A 中找到。

¹ YouTube 上的3Blue1Brown的视频和讲座是任何想要深入了解卷积的人的绝佳起点。

² TensorFlow.js 中还有其他可用于实验的池化方法。

标签:10,卷积,模型,js,merge,lrn,图像,tfjs,数据
From: https://www.cnblogs.com/apachecn/p/18011864

相关文章

  • lrn-tfjs-merge-3
    TensorFlow.js学习手册(四)原文:LearningTensorFlow.js译者:飞龙协议:CCBY-NC-SA4.0第七章:模型制作资源“通过寻找和失误我们学习。”—约翰·沃尔夫冈·冯·歌德你不仅限于来自TensorFlowHub的模型。每天都有新的令人兴奋的模型被推文、发表和在社区中受到关注。这......
  • lrn-tfjs-merge-2
    TensorFlow.js学习手册(三)原文:LearningTensorFlow.js译者:飞龙协议:CCBY-NC-SA4.0第五章:介绍模型“他从哪里弄来那些美妙的玩具?”—杰克·尼科尔森(蝙蝠侠)现在您已经进入大联盟。在第二章中,您访问了一个完全训练好的模型,但您根本不需要了解张量。在这里的第五章,您将能......
  • lrn-tfjs-merge-1
    TensorFlow.js学习手册(二)原文:LearningTensorFlow.js译者:飞龙协议:CCBY-NC-SA4.0第三章:引入张量“哇!”—基努·里维斯(《比尔和特德的冒险》)我们已经多次提到张量这个词,它是TensorFlow.js中的主要词汇,所以是时候了解这些结构是什么了。这一关键章节将让您亲身体验管......
  • lrn-tf-merge-2
    TensorFlow学习手册(三)原文:LearningTensorFlow译者:飞龙协议:CCBY-NC-SA4.0第六章:文本II:词向量、高级RNN和嵌入可视化在本章中,我们深入探讨了在第五章中讨论的与文本序列处理相关的重要主题。我们首先展示了如何使用一种称为word2vec的无监督方法训练词向量,以及如何使......
  • lrn-tf-merge-3
    TensorFlow学习手册(四)原文:LearningTensorFlow译者:飞龙协议:CCBY-NC-SA4.0第八章:队列、线程和读取数据在本章中,我们介绍了在TensorFlow中使用队列和线程的方法,主要目的是简化读取输入数据的过程。我们展示了如何编写和读取TFRecords,这是高效的TensorFlow文件格式。......
  • 聊聊ClickHouse MergeTree引擎的固定/自适应索引粒度
    ​ 前言我们在刚开始学习ClickHouse的MergeTree引擎时,就会发现建表语句的末尾总会有SETTINGSindex_granularity=8192这句话(其实不写也可以),表示索引粒度为8192。在每个datapart中,索引粒度参数的含义有二:每隔index_granularity行对主键组的数据进行采样,形成稀疏索引,并存储......
  • ABC306F Merge Sets
    原题链接分析观察要求的式子:\(\sum_{1\leqi\ltj\leqN}f(S_i,S_j)\),发现可以拆成每一个集合\(S_i\)的贡献的和。那么我们考虑每一个集合\(S_i\)的贡献。显然,对于每一个\(S_i\),其贡献就是\(\sum_{i\ltj\leqN}f(S_i,S_j)\),也就是它与其后每一个集合\(S_j\)的......
  • SQL Server MERGE(合并)语句
    来源 https://www.cnblogs.com/yigegaozhongsheng/p/11941734.html如何使用SQLServerMERGE语句基于与另一个表匹配的值来更新表中的数据。  SQLServer MERGE语句 假设有两个表,分别称为源表和目标表,并且需要根据与源表匹配的值来更新目标表。有以下三种情况: 源表......
  • Git必知必会基础(12):远程冲突(conflicts)解决--merge
     演示场景虽然每次合并代码前会先把分支更新到最新,但是在你pull后到push前这段时间,可能其它小伙伴又push了,那么你的分支就不是最新的了在push的时候就会失败,比如遇到这种提示信息:Togitee.com:qzcsbj/pytest_apiautotest.git![rejected]master->master(fetchfirst)error:......
  • 使用mergekit 合并大型语言模型
    模型合并是近年来兴起的一种新技术。它允许将多个模型合并成一个模型。这样做不仅可以保持质量,还可以获得额外的好处。假设我们有几个模型:一个擅长解决数学问题,另一个擅长编写代码。在两种模型之间切换是一个很麻烦的问题,但是我们可以将它们组合起来,利用两者的优点。而且这种组......