😱 纯 CSS!神经网络 / AI……比你想象的还要简单!🤯

2025-06-05

😱 纯 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);
}
Enter fullscreen mode Exit fullscreen mode

什么?你以为会有几千行 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));  
Enter fullscreen mode Exit fullscreen mode

从技术上讲我们甚至不需要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);
Enter fullscreen mode Exit fullscreen mode

这看起来可能很复杂,所以让我们分解一下。

首先,我们需要找到 4 个输出神经元输出的最大值。

    --maxOut: max(var(--output1b), var(--output2b), var(--output3b), var(--output4b));
Enter fullscreen mode Exit fullscreen mode

因此,如果我们的 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 */
Enter fullscreen mode Exit fullscreen mode

但是,如果该值小于最大值,则会发生以下情况(假设最大值为 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 */
Enter fullscreen mode Exit fullscreen mode

这解决了我们的分类问题!

这就是我们构建神经网络所需要的全部内容(因为其余的工作只是将偏差与输入相乘,如果你读过我之前的文章,这些应该是不言自明的)。

输出一些值

这是一个我以前从未见过有人使用过的超级有用的技巧。

我们可以使用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!
Enter fullscreen mode Exit fullscreen mode

这就是为什么我们可以counter在紧要关头使用这个技巧!

现在需要注意的是,counter()CSS 有一些限制,它只允许使用整数。

这是一个问题,因为我们正在处理大量的小数。

幸运的是,由于这只是为了调试,我们有一个解决方法。

但在解决这个问题之前,让我们先向您展示如何使用 CSS 计数器从 CSS 中获取一些值。

#input1:after{
    counter-reset: input1 var(--input1);
    position: absolute;
    content: "input 1: " counter(input1);
}
Enter fullscreen mode Exit fullscreen mode

这里有几个技巧。首先,我们需要实际输出值,因此我们使用伪元素来利用content属性(记住我们不能使用 JS,因为它只会输出字符串值)。

第二个是我们使用 CSS 变量的值来初始化我们的计数器counter-reset:

这意味着如果我们的--input1值为 4,那么我们的计数器也将(重新)设置为 4。

现在,正如我所说,计数器使用整数。当我们有小数时,这就成了一个问题。答案很简单(尽管并不完美)。我们可以将小数乘以一个较大的值,使其成为整数。

counter-reset: output1 calc(var(--output1b) * 100000);
Enter fullscreen mode Exit fullscreen mode

这个快速技巧将小数点向右移动了 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;
}


Enter fullscreen mode Exit fullscreen mode

为了实现这一点,我们使用了+运算符。它会抓取与该选择器匹配的下一个同级项(同一“层级”上的项)。

因此,通过抓取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;
}
Enter fullscreen mode Exit fullscreen mode

上述 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)));
Enter fullscreen mode Exit fullscreen mode

对于我们的--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;
}
Enter fullscreen mode Exit fullscreen mode

然后根据哪个网格方块的输出为“1”,我们会得到一个深红色方块或一个带有深色轮廓的亮绿色方块。

就是这样!

正如许多这些比较愚蠢的 CSS 文章一样,结果不是很有用,但让它工作会产生一些有趣的解决方法和有用的技术!

如果你想了解一下,我在纯 CSS 冒泡排序的文章中使用了一些其他 CSS 技术:

下一步是什么?

哦,我只是想试试能不能用 CSS 搭建一个能进行光学字符识别的神经网络……没什么大不了的!😱🤣

如果您喜欢这篇文章并且希望了解 Web 技术的不同寻常的应用,请关注我,可以在这里关注我,也可以在 Twitter 上关注我

文章来源:https://dev.to/grahamthedev/pure-css-neural-network-aiits-easier-that-you-think-f02
PREV
快速提示:如何修复 Page Speed Insights / Lighthouse 中的“图像元素没有明确的宽度和高度”问题
NEXT
我的 2021 年写作数据、在 DEV 上发表文章的最佳时间以及 2022-2023 年的计划(计划发表超过 250 篇文章)