人工智能和机器学习是软件开发的一个令人兴奋的新前沿。虽然新的工具,网站和其他资源不断涌现,但其中大多数都是用Python(或者R)编写的。如果你是一个前端开发人员(他最喜欢的语言可能是JavaScript),你可能会想知道你是否有可能探索这个新世界。
幸运的是,这个问题的答案是肯定的。有一些库可以让你在不了解Python一行的情况下练习机器学习。在本教程中,我们将探讨其中之一 - 大脑.js - 并展示如何构建,训练和使用只有JavaScript的深度神经网络(DNN)。
我们将逐步完成一个预构建的 DNN。我们将展示它如何从 UI 中工作,深入研究将 UI 连接到 DNN 的代码,然后解释实际创建和训练 DNN 的代码。然后,我们将回过头来解释 DNN 是什么、它们如何工作以及人们如何创建和改进它们的一些基本概念。在此过程中,我们将发现DNN如何使我们能够以令人惊讶的少代码获得令人印象深刻的结果。
本文假定您之前没有 DNN 或机器学习方面的经验
For the tutorial, you will need:
克隆存储库后,在所选浏览器中打开数字计算器.html文件。加载时,您应该会看到以下内容。
这是数字预测应用程序的 UI,该应用程序使用 DNN 来猜测您在其网格中绘制的内容。
尝试在网格中绘制一个数字;它可以是 0 到 9 之间的任何值(仅限一位数字)。您可以通过单击网格中的正方形,按住鼠标左键并拖动它来绘制数字,完成后释放。
绘制完某些内容后,单击“获取评估”按钮。这样做应该会产生如图 2 所示的反馈。
若要进行其他预测,请单击“清除输入”按钮并在网格中绘制另一个数字,然后再次单击“获取评估”。
希望到现在为止,您已经对此程序的工作原理很感兴趣。到目前为止,您已经与预先训练的 DNN 进行了交互。
在非常基本的层面上,您正在创建输入 - 您绘制的数字 - 并将其馈送到DNN。反过来,DNN正在使用它以前教授的内容(稍后会详细介绍)来进行有根据的猜测或预测。代码在 UI 中显示此预测,并添加了一些详细信息,例如黄色突出显示,以使网络的选择更加明显。
我们现在将演练代码,以便您可以看到使用大脑.js库在JavaScript中实现DNN是什么样的。完成此操作后,我们将退后一步,更广泛地讨论 DNN 的工作原理,并回顾我们看到的代码,以将其全部联系在一起。
当我们单击“获取评估”按钮时,我们可以看到它调用了一个名为getAssessment():
<button onclick="getAssessment()" style="width:49%;">
Get Assessment
</button>
当我们在数字计算器.html文件中向下滚动时,我们可以看到 getAssessment() 的定义:
function getAssessment() {
const inputArray=captureAndTransformInput()
let ourNetwork=ourNeuralNetwork
const result=arrayToHTML(resultToArray(ourNetwork.run(inputArray)))
document.getElementById('assessment').innerHTML=result
}
getAssessment() 函数是我们创建的,因此我们的 UI 可以与大脑.js库创建的 DNN 对象进行通信(稍后会详细介绍)。
如果要使用 DNN,我们需要做的第一件事就是获取有关我们在屏幕上绘制的内容的数据。我们需要给 DNN 一些要检查的东西。我们使用为演示创建的另一个函数来执行此操作,该函数称为捕获和转换输入()。此函数循环访问我们网页上的网格,并检查哪些框被涂成红色,哪些框没有。然后,它采用所有这些数组,并将整体转换为 1 和 0 的数组,并返回结果:
function captureAndTransformInput() {
const boxes=Array.from(document.getElementsByClassName('cell'))
const toOnesOrZeros=box=> box.style.backgroundColor==='red' ? 1 : 0
return boxes.map(toOnesOrZeros)
}
我们需要执行此转换,因为 Brain.js 库提供给我们的 DNN 将不知道如何处理原始数据(来自 HTML 页面的颜色信息)。我们的 DNN 只知道如何处理 1 和 0 的列表。
使用深度学习术语,我们正在使用捕获和转换输入函数为我们的 DNN 创建一个输入向量。
所有 DNN 都需要一个输入向量来完成其工作。即使是你可能听说过的神经网络示例,例如那些处理照片以确定它们是否包含猫或狗的示例,也不能直接处理照片。
相反,脚本(通常是Python)从照片中获取像素信息并将其解卷成一长串值,这些值通常是表示照片中每个像素的每个RGB值的不同部分的数字。结果称为输入向量,对于照片,它可以包含许多值。我们正在对捕获和转换输入()执行类似的操作,尽管更简单。
现在我们有了输入向量,我们可以使用 DNN 对刚刚绘制的内容进行分类,并确定其准确性。
使用 Brain.js库时,将创建一个包含有关 DNN 的信息的对象。此对象的一个方面是名为 run() 的方法。这就是实际生成的预测,最终在我们看到的网页上结束:
ourNeuralNetwork.run(...)
但在我们实际使用我们的 DNN 对象之前,我们必须先训练它并设置一些参数。我们很快就会解释这一点,但现在我们将继续看到网络的预测如何将其返回到网页。
brain.js run() 方法将输入向量作为参数并返回结果。此结果是一个 JSON 有效负载,其中包含有关 DNN 对我们绘制的数字的有根据的猜测的信息。在我们的教程中,我们将captureAndTransformInput()函数的输出提供给run()方法:
const inputArray=captureAndTransformInput()
const result=arrayToHTML(resultToArray(ourNeuralNetwork.run(inputArray)))
The output of the run() method - i.e., our network’s prediction - is then passed to another “transformer” function we have created called resultToArray():
function resultToArray(resultToConvert) {
let arrayToReturn=[]
arrayToReturn.push({ label: 'Zero', likelihood: resultToConvert.Zero, topChoice: 0, ordinal: 0 })
arrayToReturn.push({ label: 'One', likelihood: resultToConvert.One, topChoice: 0, ordinal: 1 })
arrayToReturn.push({ label: 'Two', likelihood: resultToConvert.Two, topChoice: 0, ordinal: 2 })
arrayToReturn.push({ label: 'Three', likelihood: resultToConvert.Three, topChoice: 0, ordinal: 3 })
arrayToReturn.push({ label: 'Four', likelihood: resultToConvert.Four, topChoice: 0, ordinal: 4 })
arrayToReturn.push({ label: 'Five', likelihood: resultToConvert.Five, topChoice: 0, ordinal: 5 })
arrayToReturn.push({ label: 'Six', likelihood: resultToConvert.Six, topChoice: 0, ordinal: 6 })
arrayToReturn.push({ label: 'Seven', likelihood: resultToConvert.Seven, topChoice: 0, ordinal: 7 })
arrayToReturn.push({ label: 'Eight', likelihood: resultToConvert.Eight, topChoice: 0, ordinal: 8 })
arrayToReturn.push({ label: 'Nine', likelihood: resultToConvert.Nine, topChoice: 0, ordinal: 9 })
const byLikelihood=(x, y)=> x.likelihood < y.likelihood ? 1 : -1
const byOrdinal=(x, y)=> x.ordinal < y.ordinal ? -1 : 1
const topChoiceDesignation=(e, i, a)=> {
e.topChoice=i===0 ? 1 : 0 // mark the first entry (index 0) as the top choice - we can do this because we've already sorted them to ensure that the top choice is the first item
return e
}
return arrayToReturn.sort(byLikelihood)
.map(topChoiceDesignation)
.sort(byOrdinal)
}
此函数接收预测数据(结果到转换)并创建一个新数组(数组 ToReturn),其中包含可用于在 UI 中显示结果的信息。我们的 DNN 返回的输出将是一个具有 10 个属性(零、一、二、三、四、五、六、七、八、九)的对象,每个属性都有映射到它的概率(介于 0 和 1 之间)。
因此,例如,来自 DNN(结果到转换参数)的预测有效负载可能如下所示(我们的 DNN 非常确信,无论它所看到的是什么,都是数字 4):
// a possible payload for resultToConvert - this data came from the DNN
{
Zero: 0.1927014673128724,
One: 1.588071696460247,
Two: 0.09038684074766934,
Three: 0.0014367527001013514,
Four: 94.81642246246338,
Five: 0.8259298279881477,
Six: 0.10091685689985752,
Seven: 0.46726344153285027,
Eight: 0.30299206264317036,
Nine: 41.01988673210144
}
我们使用此输出来构建一个新数组,其中每个项目都有一个标签属性(我们创建并分配单词值,如“Zero”,“One”等),一个可能性属性(存储从该项的神经网络接收的概率),一个topChoice属性(我们计算)和一个序号属性(我们为其赋值, 如 0, 1, 2-9)。
我们按可能性对数组进行排序,以确定 DNN 的首选,然后使用此信息用 1(真)或 0(假)标记该项目的 topChoice 属性。作为最后一步,我们采用新构造的数组并将其馈送到最终函数 - arrayToHTML() - 该函数将此信息包装在HTML标记中:
function arrayToHTML(arr) {
let htmlToReturn=''
htmlToReturn=arr.map(x=> {
let styleToUse=x.topChoice===1 ? 'background-color: yellow;' : ''
return `<div style="${styleToUse}">${x.label} Confidence: ${x.likelihood * 100}%</div>`
}).join('')
return htmlToReturn;
}
除其他操作外,此函数还对它收到的每个项目执行检查,以确定其 topChoice 属性是否为 1。如果是,则用黄色标记包装项目。这是我们在网页上看到的突出显示。
有了这个,我们已经看到了从UI到DNN(作为用户输入)和再次返回(作为中继到UI的DNN输出)的完整往返。
早些时候,我们指出,Brain.js库使我们能够创建的DNN对象需要一些设置才能使用它。我们将在这里详细讨论这个问题。
在number-evaluator.html文件中,我们创建了一个名为 activate() 的函数。当 HTML 文档最初加载时,这将触发,并且我们在此函数中定义、创建和训练 DNN。
我们通过调用 Brain.js库中的 NeuralNetwork() 方法来做到这一点,就像这样(brain是我们从 Brain.js 库的本地副本中可用的变量,存储在本地brain.js文件中):
// setting up a BrainJS DNN object with two specified hyper parameters
ourNeuralNetwork=new brain.NeuralNetwork({
activation: 'sigmoid',
learningRate: 0.1
})
Hyper parameters 初始参数
当数据科学家和机器学习工程师创建神经网络时,他们通常需要提供一组初始值,用于说明网络在训练时的运行方式。这些初始值或参数称为超参数。
它们控制神经网络调整学习方式的速度,在其隐藏层中使用哪种操作,以及许多其他方面。在其他语言和设置中,我们通常需要提供许多超参数来训练神经网络。然而,有了Brain.js,我们根本不需要提供任何东西。如果省略任何超参数,该库将提供默认值。
在上面的示例中,我们指定了两个超参数。第一个是一个名为激活的参数,我们已将其设置为 sigmoid。激活函数是神经网络在其隐藏层中执行的东西,用于处理从一层到下一层的输入。它们对于帮助神经网络学习至关重要,因为它们有助于控制哪些节点应该继续触发,哪些节点应该在每一层中保持沉默。乙状体是一种激活函数。我们可以使用许多品种(ReLU,谭等)。每个激活函数都有自己的优点和缺点。使用 sigmoid 对于分类之类的事情特别有用,这是我们在本教程中要实现的目标。
我们指定的另一个超参数是学习速率。学习速率决定了神经网络在尝试创建有用的预测模型时(即,当它试图学习如何区分3和7时)调整其值的速度(或速度)。
指定学习速率,就像指定其他超参数一样,既是艺术,也是科学:如果将速率设置得太低,您的网络可能需要不可接受的时间来训练,例如,如果您在云服务上租用GPU,这可能会产生真正的财务后果;如果您将速率设置得太高,您的网络可能永远不会收敛到所谓的“全局最小值”(解决方案)。
时间和实践将使您能够掌握良好的初始值是什么样子的。
我们一直在谈论神经网络的学习以及我们必须如何训练它,但这意味着什么,它看起来是什么样子的?在我们的教程应用程序的上下文中,培训如下所示:
const ourTrainingData=getTrainingData()
ourNeuralNetwork.train(ourTrainingData)
训练是 DNN 工作原理的基础。通常,它的完成方式是使用监督学习方法:我们提供一个数据集,其中包含示例输入和答案(或标签,有时也称为真实标签),用于描述该数据行的正确分类。当我们训练一个DNN(大脑.js允许我们从有用的命名train()方法开始,它使用这些标签来帮助它学习。
查看 getTrainingData() 函数,我们可以知道DNN具体是如何工作的。
Brain.js允许我们通过训练数据来指定我们的输入数据将是什么样子的,以及我们希望从神经网络中看到什么样的输出。它的主要要求是我们提供输入和输出属性。有关详细信息,请参阅“训练选项”下的“大脑.js GitHub 存储库”。
下面是训练数据样例
{ "input": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"output": {
Zero: 1,
One: 0,
Two: 0,
Three: 0,
Four: 0,
Five: 0,
Six: 0,
Seven: 0,
Eight: 0,
Nine: 0
}
}
训练识别概率
{
Zero: 0.1927014673128724,
One: 1.588071696460247,
Two: 0.09038684074766934,
Three: 0.0014367527001013514,
Four: 94.81642246246338,
Five: 0.8259298279881477,
Six: 0.10091685689985752,
Seven: 0.46726344153285027,
Eight: 0.30299206264317036,
Nine: 41.01988673210144
}
自己创建训练数据
通过手写板准备数据,
本文中,我们展示了如何使用 JavaScript 实现和训练 DNN,这要归功于brain.js库 - 通常需要 Python 和 Keras、Pytorch 或 TensorFlow 等库的任务。
我们证明了一个有效的数字识别器可以用令人惊讶的少代码创建。我们学习了机器学习的一些基本构建块和实践,例如为DNN指定学习速率和激活函数(以及其他超参数)。
希望本文已经向您展示,在没有经验的情况下开始使用AI和机器学习不仅是可能的,而且在像JavaScript这样的熟悉语言中也是可行的。
HTML 颜色由红色、绿色、蓝色混合而成。
HTML 颜色由一个十六进制符号来定义,这个符号由红色、绿色和蓝色的值组成(RGB)。
每种颜色的最小值是0(十六进制:#00)。最大值是255(十六进制:#FF)。
这个表格给出了由三种颜色混合而成的具体效果:
1600万种不同颜色
三种颜色 红,绿,蓝的组合从0到255,一共有1600万种不同颜色(256 x 256 x 256)。
在下面的颜色表中你会看到不同的结果,从0到255的红色,同时设置绿色和蓝色的值为0,随着红色的值变化,不同的值都显示了不同的颜色。
灰暗色调
以下展示了灰色到黑色的渐变
Web安全色?
数年以前,当大多数计算机仅支持 256 种颜色的时候,一系列 216 种 Web 安全色作为 Web 标准被建议使用。其中的原因是,微软和 Mac 操作系统使用了 40 种不同的保留的固定系统颜色(双方大约各使用 20 种)。
我们不确定如今这么做的意义有多大,因为越来越多的计算机有能力处理数百万种颜色,不过做选择还是你自己。
最初,216 跨平台 web 安全色被用来确保:当计算机使用 256 色调色板时,所有的计算机能够正确地显示所有的颜色。
新的 CSS Color Module 规范引入了多种新的颜色表示法. 可以支持多种色彩空间和颜色模型. 这解锁了很多的玩法. 比如: 基于一个颜色生成更深或更浅的颜色; 根据背景自适应文本色; 使用广色域颜色等等.
CSS Color Module Level 4 规范
引入了新的颜色表示法, 同时引入了多种色彩空间, 不再仅限于sRGB, 这些方法现代浏览器均已支持:
再补充一点, 这些新方法中:
CSS Color Module Level 5 规范
CSS Color Module Level 6 规范
在详细介绍这些现代新方法之前有必要对一些术语进行解释:
名词解释:
颜色模型是指颜色与坐标系之间的映射和编码方式, 它定义维度分量与色彩空间的关系. 一个颜色模型就会有一个对应的色彩空间.
色彩空间是某一颜色模型所涵盖的颜色的定义和命名. 每个颜色空间都由数学模型和关联的规则集定义. 色彩空间是表示颜色的三维网格, 色彩空间中的每个颜色都由三个通道分量(维度)来表示. 每个颜色空间都有一个定义的色域。
色域指的是它可以表示的特定颜色的范围, 通常指设备可以显示的颜色范围. 如 sRGB, P3, Rec2020 等
可以看出三者有一些共性的东西, 通常来说, 当上下文中使用颜色空间时强调的是它的颜色模型和算法. 当使用色域时强调的是能不能显示某些颜色
所有色彩空间
比如, 以前用的最多的rgb方法, 带和不带alpha通道是不同的方法: 不带alpha通道的是: rgb(r, g, b), 而带alpha通道的是: rgba(r, g, b, a)
现在可以统一使用: rgb(R G B [/ A]), alpha通道值是可选的. 注意为了区分旧方法, 新方法不使用逗号分隔分量, 而是用空格替代.
上面只是拿rgb方法举了个例子, 其实Level 4 中的所有新方法都支持这种表示法. 如: oklch(L C H [/A])
语法: rgb(from <color> R G B[ / A]), hsl(from <color> H S L [/A]), oklch(from <color> L C H [/A]), ...
相对颜色是指从一个指定颜色的色彩空间转换到目标色彩空间, 通过对目标色彩空间中的维度变量进行微调后的结果作为输出.
这听起来比较绕, 简单点说就是可以根据原色, 对维度变量进行微调后输出. 主要特性:
这3个特性解锁了一些原本只能通过js才能实现的一些功能.
例子1: 鼠标覆盖按钮时加深背景色:
方法1: 使用Level 5 规范中的相对颜色表示法:
.btn {
--btn-bg: blue;
background-color: var(--btn-bg);
}
.btn:hover {
background-color: oklch(from var(--btn-bg) calc(l - 0.1) c h);
}
这个例子中--btn-bg自定义属性可以更改为任意颜色, 本例中使用了oklch作为目标色彩空间, 因为oklch可以做到调整亮度而不会影响色相.
从这个例子中可以看出, CSS自定义属性与相对颜色的结合使用, 可以创造出很多的新玩法.
方法2: 使用Level 5 规范中的color-mix()方法
.btn {
--btn-bg: blue;
background-color: var(--btn-bg);
}
.btn:hover {
background-color: color-mix(in oklch, var(--btn-bg), black 10%);
}
color-mix()方法的意思是将颜色1和颜色2先转换到in关键字指定的目标色彩空间, 然后按百分比混合它们后输出.由于black只有L分量, 因此混合只影响了L分量, 因此就得到了不改变色相的情况下加深了颜色.
例子2: 根据不同背景色自适应高对比度的文本色
这个场景需要一种方式确定高对比度的算法模型. 通用的是WCAG 2.1, 但它不太准确, 还有一种是APCA, 它相对准确性更高, 参考性更大. 在APCA算法下, 采用oklch颜色模型下L分量在72%左右是一个比较好的对比度分界线. 72%以上采用黑色文本, 72%以下采用白色文本.
好了, 有了这个基础, 现在可以使用纯CSS实现自适应高对比度的文本色:
.btn {
--btn-bg: blue;
background-color: var(--btn-bg);
color: oklch(from var(--btn-bg) clamp(0, calc((0.72 - l) * 10000), 1) 0 0);
}
这个例子中--bg自定义属性可以更改为任意颜色, 按钮文本都可以自适应的高对比度颜色. 本例中使用了oklch作为目标色彩空间, 因为oklch可以做到亮度是可预测性.
这里稍微解释这句:clamp(0, calc((0.72 - l)* 10000), 1), 意思是背景色的l维度分量 > 0.72说明背景是浅色的, 那么文本色的L分量就取0即黑色, 否则就说明背景是深色的,L分量就取1即白色. 如果不想纯白或纯黑, 适当调整各分量以及L的上下界即可.
这个例子还可以使用CSS Color Module Level 6中的color-contrast()实现相同的效果, 但目前还没有浏览器支持, 留着将来备用:
.btn {
--btn-bg: blue;
background-color: var(--btn-bg);
color: color-contrast(var(--btn-bg) vs white,black);
}
color-contrast()的意思是选择vs关键字之后与第一个参数指定的颜色对比度最高的颜色作为输出.
现代网页中推荐使用oklch颜色模型, 使用 OKLCH 的好处:
OKLCH 颜色由亮度(明度)、色度(饱和度或纯度)、色相三个维度组成, 这也是人类认知里的颜色的三个基本属性.
详细分析请看这篇文章: OKLCH in CSS: why we moved from RGB and HSL—Martian Chronicles, Evil Martians’ team blog
使用这个 colorjs.io npm包即可. 它完全支持CSS Color Module Level 4 和 Level 5 的规范
我们的正式开源轻量级的基于Tailwindcss的React 组件库中的颜色选取组件正在重构中
*请认真填写需求信息,我们会在24小时内与您取得联系。