😱 纯 CSS!神经网络 / AI……比你想象的还要简单!🤯
不,你没有看错标题。
用纯 CSS 构建神经网络是可行的。我做了一个很酷的 demo,什么都有!
calc()
然而,由于依赖前一个语句的语句数量有限,用 CSS 训练该神经网络几乎不可能!所以我们就不在这里讲了!
现在,本文的重点不是提倡用 CSS 构建神经网络(你不应该这样做),而是向你介绍我必须使用的一些 CSS 技巧才能使其发挥作用!
请先阅读此内容
现在,如果您还没有读过,我建议您阅读我之前的文章(或者至少查看演示),其中我使用 vanilla JS 构建了一个神经网络。
原因是,这是完全相同的神经网络,但遗憾的是缺乏所有的训练能力。
除此之外,让我们立即开始吧!
给我看看
钱代码!
这里是:
:root{
--inputX: 0.9;
--inputY: -1;
--output1: 0;
--output2: 0;
--output3: 0;
--output4: 0;
--output1bias: 0.006412765611663633;
--output2bias: 0.007072853542676219;
--output3bias: 0.0064746685639952214;
--output4bias: 0.004851470988693036;
--weights1-1: -0.9807254999119579;
--weights1-2: 0.9813663133332142;
--weights1-3: -0.9817224902785696;
--weights1-4: 0.9817919593383302;
--weights2-1: -0.9809082670731147;
--weights2-2: -0.9816176935504328;
--weights2-3: 0.9815336794202348;
--weights2-4: 0.9815925299039976;
/* propogate */
--output1a: calc((var(--weights1-1) * var(--inputX)) + (var(--weights2-1) * var(--inputY)));
--output2a: calc((var(--weights1-2) * var(--inputX)) + (var(--weights2-2) * var(--inputY)));
--output3a: calc((var(--weights1-3) * var(--inputX)) + (var(--weights2-3) * var(--inputY)));
--output4a: calc((var(--weights1-4) * var(--inputX)) + (var(--weights2-4) * var(--inputY)));
--output1b: calc(max(0, var(--output1a)) + var(--output1bias));
--output2b: calc(max(0, var(--output2a)) + var(--output2bias));
--output3b: calc(max(0, var(--output3a)) + var(--output3bias));
--output4b: calc(max(0, var(--output4a)) + var(--output4bias));
/* categorise */
--maxOut: max(var(--output1b), var(--output2b), var(--output3b), var(--output4b));
--output1c: max(calc(1 - ((var(--output1b) - var(--maxOut)) * (var(--output1b) - var(--maxOut)) * 1000000000)), 0);
--output2c: max(calc(1 - ((var(--output2b) - var(--maxOut)) * (var(--output2b) - var(--maxOut)) * 1000000000)), 0);
--output3c: max(calc(1 - ((var(--output3b) - var(--maxOut)) * (var(--output3b) - var(--maxOut)) * 1000000000)), 0);
--output4c: max(calc(1 - ((var(--output4b) - var(--maxOut)) * (var(--output4b) - var(--maxOut)) * 1000000000)), 0);
}
什么?你以为会有几千行 CSS 代码吗?失望了吗?
别担心,在本系列的下一篇文章中,我会更上一层楼,尝试用类似的技术进行光学字符识别(OCR)!(不过可能得等上几周,哈哈)
但是,请继续关注,即使通过这个简单的演示,我们也可以通过将 CSS 推向极限来学到一些有趣的东西!
演示
该演示实际上比神经网络更难编写(因为我希望演示界面也是纯 CSS / HTML!)。
当您在我们预先训练的神经网络的第一部分中选择一个方块时,它会计算出它认为该值位于哪个象限内(如“神经网络预测”部分所示)。a
第一部分中的每个方块代表 x, y 坐标,该坐标在每个轴上介于 -1 和 +1 之间。例如,左上角的方块代表 -0.8, 0.8,右下角的方块代表 0.8, -0.8。
最后,如果向下滚动,您将看到来自神经网络的一些值,以显示“引擎盖下”发生的事情。
玩一玩,然后我们可以看看我使用的一些技巧来让演示运行起来!
注意:您可能需要上下滚动才能查看输入与预测结果。为了减少滚动操作,建议在电脑上查看。
一些有趣的技巧
好的,首先让我们从神经网络本身开始。
Sigmoid 已出,向 ReLU 问好!
我们的第一个问题是,exp()
如果我们希望我们的解决方案在大多数浏览器上运行,我们就不能使用 (exponent),因为CSSexp()
仅在 FireFox 和 Safari 上受支持。
这意味着我们无法为我们的输出创建 S 型函数。
因此我们需要另一种方法来替代我们的 S 型函数。
幸运的是,在使用神经网络时还有另一个流行的选择......整流线性单元(ReLU)。
这些只是忽略所有小于 0 的值并只返回正值。
在 CSS 中实现这一点(相对)简单:
--ReLU: calc(max(0, var(--output)) + var(--outputBias));
从技术上讲我们甚至不需要calc
这里(但我总是喜欢将它包括在内)。
这句话的意思是“给我“0”的最大值或我们的输出+偏差”。
如果我们的输出 + 偏差小于 0,那么我们返回 0(因为这是max
数字),否则我们返回我们的输出 + 偏差!
直接解决方案!
获得标准化结果
这里还有另一个大问题需要解决,我们需要我们的神经网络能够将其猜测输出为 1(猜测)或 0(不是正确值)。
不幸的是,神经网络的工作原理并非如此。事实上,它们对所有值都输出确定性。
类似于:
- x < 0, y < 0: 0.845 <- 最高概率
- x > 0,y < 0: 0.283
- x < 0,y > 0: 0.154
- x > 0,y > 0: 0.319
我们需要将其转化为:
- x < 0, y < 0: 1 <- 最高概率变为 1
- x > 0, y < 0: 0
- x < 0,y > 0: 0
- x > 0,y > 0: 0
这就是这个技巧的用武之地:
--maxOut: max(var(--output1b), var(--output2b), var(--output3b), var(--output4b));
--output1c: max(calc(1 - ((var(--output1b) - var(--maxOut)) * (var(--output1b) - var(--maxOut)) * 1000000000)), 0);
这看起来可能很复杂,所以让我们分解一下。
首先,我们需要找到 4 个输出神经元输出的最大值。
--maxOut: max(var(--output1b), var(--output2b), var(--output3b), var(--output4b));
因此,如果我们的 4 个输出神经元分别为 20、11、16、4,那么--maxOut
就是 20。
然后我们用这个数字来做以下事情:
- 从每个输出中减去最大值。
- 再次执行此操作并将它们相乘(这解释了负值)。
- 然后我们将这个值乘以 1000000000,以确保舍入没有问题。
- 然后我们从“1”中减去这个值。
- 然后我们取其输出的最大值或 0。
这样做的效果就好像该值与最大值相同,我们本质上是在执行以下操作:
--output1c: 1 - ((20 - 20) * (20 - 20) * 1000000000)
--output1c: 1 - ((0) * (0) * 1000000000) /* which is 1 - 0 */
--output1c: max(1 - (0), 0) /* the max is 1 - 0 which is 1 */
但是,如果该值小于最大值,则会发生以下情况(假设最大值为 20,而我们的值为 14):
--output1c: 1 - ((14 - 20) * (14 - 20) * 1000000000)
--output1c: 1 - ((-6) * (-6) * 1000000000) /* which is 1 - (36 * 1000000000) */
--output1c: max(1 - (36000000000), 0) /* the max is 0 as it is greater than 1 - 36000000000 which is -35999999999 */
这解决了我们的分类问题!
这就是我们构建神经网络所需要的全部内容(因为其余的工作只是将偏差与输入相乘,如果你读过我之前的文章,这些应该是不言自明的)。
输出一些值
这是一个我以前从未见过有人使用过的超级有用的技巧。
我们可以使用CSScounter()
来调试我们的“应用程序”。
你可能想知道为什么我们需要这个?好吧,如果你尝试获取 CSS 表达式的值,calc
很快就会遇到问题。你得到的是字符串,而不是实际的值!
// --example-var: calc(20 * 3)
console.log(window.getComputedStyle(div).getPropertyValue('--example-var'))
// console will output "calc(20 * 3)" instead of 60!
这就是为什么我们可以counter
在紧要关头使用这个技巧!
现在需要注意的是,counter()
CSS 有一些限制,它只允许使用整数。
这是一个问题,因为我们正在处理大量的小数。
幸运的是,由于这只是为了调试,我们有一个解决方法。
但在解决这个问题之前,让我们先向您展示如何使用 CSS 计数器从 CSS 中获取一些值。
#input1:after{
counter-reset: input1 var(--input1);
position: absolute;
content: "input 1: " counter(input1);
}
这里有几个技巧。首先,我们需要实际输出值,因此我们使用伪元素来利用content
属性(记住我们不能使用 JS,因为它只会输出字符串值)。
第二个是我们使用 CSS 变量的值来初始化我们的计数器counter-reset:
。
这意味着如果我们的--input1
值为 4,那么我们的计数器也将(重新)设置为 4。
现在,正如我所说,计数器使用整数。当我们有小数时,这就成了一个问题。答案很简单(尽管并不完美)。我们可以将小数乘以一个较大的值,使其成为整数。
counter-reset: output1 calc(var(--output1b) * 100000);
这个快速技巧将小数点向右移动了 6 位。
正如我所说,它很丑陋(我们可能会做一些奇特的技巧来获得小数位......这可能是未来有趣的实验!)但它确实给了我们一些输出。
如果您想知道我是如何在那里输出文本的,这就是整个演示第三部分所使用的技术。
重要提示:遗憾的是content:
,辅助技术无法显示值。这就是为什么“仅限 CSS”在大多数情况下只是为了好玩。
不要在生产中使用此技巧,只需将其保存起来以便在其他方法失败时进行调试即可。
无论如何,这就是窍门,我希望有一天它能帮助你!
演示本身的技巧
我希望演示本身也只包含 CSS。为了实现这一点,这里有两个技巧。
输入网格
第一个是我们创建的输入“网格”。
因为我们只想一次选择一个值(我们的 x 和 y 坐标),所以我使用了<input type="radio">
,然后使用浮点数和清除数将其布置成网格形状!
但主要的技巧是我仍然想让这个键盘可访问,所以我使用了一个技巧,即在视觉上隐藏输入,然后根据状态调整标签外观。
然后我们使用标签本身来创建网格形状。
/* our label is the actual grid square */
label{
--wh: min(10vw, 60px);
width: var(--wh);
height: var(--wh);
display: block;
background-color: #bb0000;
float: left;
text-align: center;
font-size: 0.2vw;
outline: 1px solid #666;
}
input[type="radio"]{
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
input:checked + label{
background-color: #00ff00;
outline: 4px solid #000;
outline-offset: -4px;
}
input:focus + label{
outline: 8px solid #333;
outline-offset: -8px;
border-radius: 16px;
}
为了实现这一点,我们使用了+
运算符。它会抓取与该选择器匹配的下一个同级项(同一“层级”上的项)。
因此,通过抓取input:checked
然后使用操作员找到label
下一个,我们能够使用标签作为我们显示的项目,而不是无线电输入本身,同时使无线电输入仍然可访问。input
+
现在,说到无线电输入,我确实提到过我们需要在视觉上隐藏它们!
input[type="radio"]{
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
上述 CSS 意味着输入仍然可以通过辅助技术访问并且仍然可以聚焦,但它在视觉上不会占用单个像素。
将此与之前的技巧相结合,我们可以创建一个无线电输入网格!
输出网格
啊,这是最后的诀窍。
如果您还记得,我们之前创建了一种方法,让我们的神经网络输出 1 或 0。
但是,我们如何根据输出将其转换为 4 个象限的“红色”和“绿色”?
在这里,我们可以使用一个巧妙的技巧,分别对填充和轮廓使用线性渐变和不透明度!
--color1: #00ff00;
--color2: #990000;
--switch1: var(--color1) calc(100% * var(--output1c)),
var(--color2) 0;
--switch1outline: rgba(0,0,0, calc(100% * var(--output1c)));
对于我们的--switch1
,我们希望正方形要么是红色(如果值为 0),要么是绿色(如果值为 1)。
通过将第一种颜色的百分比在 100%(覆盖正方形)或 0%(第二种颜色将占据正方形)之间切换,我们得到了一种切换颜色的简洁方法。
我们在我们的上使用类似的技术outline
,将 alpha 值(透明度)调整为 100%(可见)或 0%(不可见)。
通过将这些样式应用于组成输出网格的 4 个方块中的每一个,如下所示:
#out-x-1y-1{
background: linear-gradient(var(--switch1));
outline: 4px solid var(--switch1outline);
outline-offset: -4px;
}
然后根据哪个网格方块的输出为“1”,我们会得到一个深红色方块或一个带有深色轮廓的亮绿色方块。
就是这样!
正如许多这些比较愚蠢的 CSS 文章一样,结果不是很有用,但让它工作会产生一些有趣的解决方法和有用的技术!
如果你想了解一下,我在纯 CSS 冒泡排序的文章中使用了一些其他 CSS 技术:
下一步是什么?
哦,我只是想试试能不能用 CSS 搭建一个能进行光学字符识别的神经网络……没什么大不了的!😱🤣
如果您喜欢这篇文章并且希望了解 Web 技术的不同寻常的应用,请关注我,可以在这里关注我,也可以在 Twitter 上关注我。
文章来源:https://dev.to/grahamthedev/pure-css-neural-network-aiits-easier-that-you-think-f02