@Dave 是第一个对此问题发布答案的人(使用工作代码),他的回答是无耻的复制和粘贴灵感的宝贵来源。这篇文章最初是为了解释和完善@Dave 的答案,但后来演变成了自己的答案。
我的方法要快得多。根据随机生成的 RGB 颜色的jsPerf 基准测试,@Dave 的算法在600 ms 内运行,而我的在30 ms 内运行。这肯定很重要,例如在加载时间,速度至关重要。
此外,对于某些颜色,我的算法表现更好:
- 对于
rgb(0,255,0)
,@Dave 的产品rgb(29,218,34)
和我的产品rgb(1,255,0)
- 对于
rgb(0,0,255)
,@Dave 的产品rgb(37,39,255)
和我的产品rgb(5,6,255)
- 对于
rgb(19,11,118)
,@Dave 的产品rgb(36,27,102)
和我的产品rgb(20,11,112)
演示
"use strict";
class Color {
constructor(r, g, b) { this.set(r, g, b); }
toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }
set(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
}
hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
let sin = Math.sin(angle);
let cos = Math.cos(angle);
this.multiply([
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
]);
}
grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
]);
}
sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
]);
}
saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
]);
}
multiply(matrix) {
let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR; this.g = newG; this.b = newB;
}
brightness(value = 1) { this.linear(value); }
contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}
invert(value = 1) {
this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
}
hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
let r = this.r / 255;
let g = this.g / 255;
let b = this.b / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if(max === min) {
h = s = 0;
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
} h /= 6;
}
return {
h: h * 100,
s: s * 100,
l: l * 100
};
}
clamp(value) {
if(value > 255) { value = 255; }
else if(value < 0) { value = 0; }
return value;
}
}
class Solver {
constructor(target) {
this.target = target;
this.targetHSL = target.hsl();
this.reusedColor = new Color(0, 0, 0); // Object pool
}
solve() {
let result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values)
};
}
solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];
let best = { loss: Infinity };
for(let i = 0; best.loss > 25 && i < 3; i++) {
let initial = [50, 20, 3750, 50, 100, 100];
let result = this.spsa(A, a, c, initial, 1000);
if(result.loss < best.loss) { best = result; }
} return best;
}
solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}
spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;
let best = null;
let bestLoss = Infinity;
let deltas = new Array(6);
let highArgs = new Array(6);
let lowArgs = new Array(6);
for(let k = 0; k < iters; k++) {
let ck = c / Math.pow(k + 1, gamma);
for(let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i] = values[i] - ck * deltas[i];
}
let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for(let i = 0; i < 6; i++) {
let g = lossDiff / (2 * ck) * deltas[i];
let ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}
let loss = this.loss(values);
if(loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
} return { values: best, loss: bestLoss };
function fix(value, idx) {
let max = 100;
if(idx === 2 /* saturate */) { max = 7500; }
else if(idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }
if(idx === 3 /* hue-rotate */) {
if(value > max) { value = value % max; }
else if(value < 0) { value = max + value % max; }
} else if(value < 0) { value = 0; }
else if(value > max) { value = max; }
return value;
}
}
loss(filters) { // Argument is array of percentages.
let color = this.reusedColor;
color.set(0, 0, 0);
color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);
let colorHSL = color.hsl();
return Math.abs(color.r - this.target.r)
+ Math.abs(color.g - this.target.g)
+ Math.abs(color.b - this.target.b)
+ Math.abs(colorHSL.h - this.targetHSL.h)
+ Math.abs(colorHSL.s - this.targetHSL.s)
+ Math.abs(colorHSL.l - this.targetHSL.l);
}
css(filters) {
function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
}
}
$("button.execute").click(() => {
let rgb = $("input.target").val().split(",");
if (rgb.length !== 3) { alert("Invalid format!"); return; }
let color = new Color(rgb[0], rgb[1], rgb[2]);
let solver = new Solver(color);
let result = solver.solve();
let lossMsg;
if (result.loss < 1) {
lossMsg = "This is a perfect result.";
} else if (result.loss < 5) {
lossMsg = "The is close enough.";
} else if(result.loss < 15) {
lossMsg = "The color is somewhat off. Consider running it again.";
} else {
lossMsg = "The color is extremely off. Run it again!";
}
$(".realPixel").css("background-color", color.toString());
$(".filterPixel").attr("style", result.filter);
$(".filterDetail").text(result.filter);
$(".lossDetail").html(`Loss: ${result.loss.toFixed(1)}. <b>${lossMsg}</b>`);
});
.pixel {
display: inline-block;
background-color: #000;
width: 50px;
height: 50px;
}
.filterDetail {
font-family: "Consolas", "Menlo", "Ubuntu Mono", monospace;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input class="target" type="text" placeholder="r, g, b" value="250, 150, 50" />
<button class="execute">Compute Filters</button>
<p>Real pixel, color applied through CSS <code>background-color</code>:</p>
<div class="pixel realPixel"></div>
<p>Filtered pixel, color applied through CSS <code>filter</code>:</p>
<div class="pixel filterPixel"></div>
<p class="filterDetail"></p>
<p class="lossDetail"></p>
用法
let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.css;
解释
我们将从一些 Javascript 开始。
"use strict";
class Color {
constructor(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
} toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }
hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
let r = this.r / 255;
let g = this.g / 255;
let b = this.b / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if(max === min) {
h = s = 0;
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
} h /= 6;
}
return {
h: h * 100,
s: s * 100,
l: l * 100
};
}
clamp(value) {
if(value > 255) { value = 255; }
else if(value < 0) { value = 0; }
return value;
}
}
class Solver {
constructor(target) {
this.target = target;
this.targetHSL = target.hsl();
}
css(filters) {
function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
}
}
解释:
- 本
Color
类代表一个RGB色彩。
- 它的
toString()
函数返回 CSSrgb(...)
颜色字符串中的颜色。
- 它的
hsl()
函数返回颜色,转换为HSL。
- 它的
clamp()
功能确保给定的颜色值在范围内 (0-255)。
- 该
Solver
课程将尝试求解目标颜色。
- 它的
css()
函数在 CSS 过滤器字符串中返回给定的过滤器。
实施grayscale()
, sepia()
, 和saturate()
CSS/SVG 过滤器的核心是过滤器基元,它表示对图像的低级修改。
过滤器grayscale()
、sepia()
和saturate()
由过滤器原语实现<feColorMatrix>
,它在过滤器指定的矩阵(通常是动态生成的)和从颜色创建的矩阵之间执行矩阵乘法。图表:
我们可以在这里进行一些优化:
- 颜色矩阵的最后一个元素是并且永远是
1
。没有必要计算或存储它。
- 也没有必要计算或存储 alpha/透明度值 (
A
),因为我们处理的是 RGB,而不是 RGBA。
- 因此,我们可以将过滤器矩阵从 5x5 修剪到 3x5,将颜色矩阵从 1x5 修剪到 1x3。这可以节省一些工作。
- 所有
<feColorMatrix>
过滤器都将第 4 列和第 5 列留为零。因此,我们可以进一步将滤波器矩阵减少到 3x3。
- 由于乘法相对简单,因此无需为此拖入复杂的数学库。我们可以自己实现矩阵乘法算法。
执行:
function multiply(matrix) {
let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR; this.g = newG; this.b = newB;
}
(我们使用临时变量来保存每行乘法的结果,因为我们不希望对this.r
等的更改影响后续计算。)
现在我们已经实现了<feColorMatrix>
,我们可以实现grayscale()
, sepia()
, 和saturate()
,它简单地使用给定的过滤器矩阵调用它:
function grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
]);
}
function sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
]);
}
function saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
]);
}
实施 hue-rotate()
该hue-rotate()
过滤器是通过实施<feColorMatrix type="hueRotate" />
。
过滤矩阵计算如下:
例如,元素a 00 的计算方式如下:
笔记:
- 旋转角度以度为单位,在传递给
Math.sin()
或之前必须将其转换为弧度Math.cos()
。
Math.sin(angle)
并且Math.cos(angle)
应该计算一次然后缓存。
执行:
function hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
let sin = Math.sin(angle);
let cos = Math.cos(angle);
this.multiply([
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
]);
}
实施brightness()
和contrast()
在brightness()
和contrast()
过滤器由实现<feComponentTransfer>
用<feFuncX type="linear" />
。
每个<feFuncX type="linear" />
元素都接受一个斜率和截距属性。然后它通过一个简单的公式计算每个新的颜色值:
value = slope * value + intercept
这很容易实现:
function linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}
一旦实现,brightness()
并且contrast()
可以实现为好:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
实施 invert()
该invert()
过滤器是通过实现<feComponentTransfer>
用<feFuncX type="table" />
。
规范指出:
下文中,C为初始分量,C'为重映射分量;都在闭区间 [0,1] 内。
对于“表”,该函数由属性tableValues 中给定值之间的线性插值定义。该表具有n + 1 个值(即 v 0到 v n),指定了n 个大小均匀的插值区域的开始和结束值。插值使用以下公式:
对于值C找到k使得:
k / n ≤ C < (k + 1) / n
结果C'由下式给出:
C' = v k + (C - k / n) * n * (v k+1 - v k )
这个公式的解释:
- 所述
invert()
过滤器定义该表:[数值,1 -值]。这是tableValues或v。
- 该公式定义了n,因此n + 1 是表格的长度。由于表的长度为 2,因此n = 1。
- 该公式定义了k,其中k和k + 1 是表的索引。由于该表有 2 个元素,因此k = 0。
因此,我们可以将公式简化为:
C' = v 0 + C * (v 1 - v 0 )
内联表的值,我们剩下:
C' = 值 + C * (1 - 值 - 值)
还有一种简化:
C' = 值 + C * (1 - 2 * 值)
该规范将C和C'定义为 RGB 值,在 0-1 范围内(而不是 0-255)。因此,我们必须在计算之前缩小这些值,然后再将它们放大。
因此,我们到达了我们的实现:
function invert(value = 1) {
this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
}
插曲:@Dave 的蛮力算法
@Dave的代码生成176,660 个过滤器组合,包括:
- 11 个
invert()
过滤器(0%、10%、20%、...、100%)
- 11 个
sepia()
过滤器(0%、10%、20%、...、100%)
- 20 个
saturate()
过滤器(5%、10%、15%、...、100%)
- 73 个
hue-rotate()
过滤器(0 度、5 度、10 度、...、360 度)
它按以下顺序计算过滤器:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
然后它遍历所有计算出的颜色。一旦发现生成的颜色在容差范围内(所有 RGB 值都在目标颜色的 5 个单位内),它就会停止。
然而,这是缓慢且低效的。因此,我提出了我自己的答案。
实施 SPSA
首先,我们必须定义一个损失函数,它返回过滤器组合产生的颜色与目标颜色之间的差异。如果过滤器是完美的,损失函数应该返回 0。
我们将测量色差作为两个指标的总和:
- RGB 差异,因为目标是产生最接近的 RGB 值。
- HSL 差异,因为许多 HSL 值对应于过滤器(例如,色调与 大致相关
hue-rotate()
,饱和度与 相关saturate()
,等等)。这指导了算法。
损失函数将采用一个参数——一组过滤器百分比。
我们将使用以下过滤顺序:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
执行:
function loss(filters) {
let color = new Color(0, 0, 0);
color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);
let colorHSL = color.hsl();
return Math.abs(color.r - this.target.r)
+ Math.abs(color.g - this.target.g)
+ Math.abs(color.b - this.target.b)
+ Math.abs(colorHSL.h - this.targetHSL.h)
+ Math.abs(colorHSL.s - this.targetHSL.s)
+ Math.abs(colorHSL.l - this.targetHSL.l);
}
我们将尝试最小化损失函数,使得:
loss([a, b, c, d, e, f]) = 0
该SPSA算法(网站,更多的信息,纸,实现纸张,参考代码)是在这个非常好。它旨在优化具有局部最小值、噪声/非线性/多元损失函数等的复杂系统。它已用于调整国际象棋引擎。与许多其他算法不同,描述它的论文实际上是可以理解的(尽管需要付出很大的努力)。
执行:
function spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;
let best = null;
let bestLoss = Infinity;
let deltas = new Array(6);
let highArgs = new Array(6);
let lowArgs = new Array(6);
for(let k = 0; k < iters; k++) {
let ck = c / Math.pow(k + 1, gamma);
for(let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i] = values[i] - ck * deltas[i];
}
let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for(let i = 0; i < 6; i++) {
let g = lossDiff / (2 * ck) * deltas[i];
let ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}
let loss = this.loss(values);
if(loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
} return { values: best, loss: bestLoss };
function fix(value, idx) {
let max = 100;
if(idx === 2 /* saturate */) { max = 7500; }
else if(idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }
if(idx === 3 /* hue-rotate */) {
if(value > max) { value = value % max; }
else if(value < 0) { value = max + value % max; }
} else if(value < 0) { value = 0; }
else if(value > max) { value = max; }
return value;
}
}
我对 SPSA 进行了一些修改/优化:
- 使用产生的最佳结果,而不是最后一个。
- 重用所有数组 (
deltas
, highArgs
, lowArgs
),而不是在每次迭代时重新创建它们。
- 对a使用一组值,而不是单个值。这是因为所有滤波器都不同,因此它们应该以不同的速度移动/收敛。
fix
每次迭代后运行一个函数。它将所有值限制在 0% 和 100% 之间,除了saturate
(最大值为 7500%)brightness
和contrast
(最大值为 200%)和hueRotate
(值被环绕而不是被限制)。
我在两阶段过程中使用 SPSA:
- “宽”阶段,试图“探索”搜索空间。如果结果不令人满意,它将对 SPSA 进行有限的重试。
- “窄”舞台,从宽舞台中取最好的结果,并试图“细化”它。它使用A和a 的动态值。
执行:
function solve() {
let result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values)
};
}
function solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];
let best = { loss: Infinity };
for(let i = 0; best.loss > 25 && i < 3; i++) {
let initial = [50, 20, 3750, 50, 100, 100];
let result = this.spsa(A, a, c, initial, 1000);
if(result.loss < best.loss) { best = result; }
} return best;
}
function solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}
调整 SPSA
警告:不要弄乱 SPSA 代码,尤其是它的常量,除非您确定自己知道自己在做什么。
重要的常量是A、a、c、初始值、重试阈值、max
in的值fix()
以及每个阶段的迭代次数。所有这些值都经过仔细调整以产生良好的结果,并且随机使用它们几乎肯定会降低算法的实用性。
如果你坚持要改变它,你必须在“优化”之前进行测量。
首先,应用这个补丁。
然后在 Node.js 中运行代码。一段时间后,结果应该是这样的:
Average loss: 3.4768521401985275
Average time: 11.4915ms
现在将常量调整到您满意的程度。
一些技巧:
- 平均损失应该在 4 左右。如果它大于 4,则产生的结果相差太远,您应该调整精度。如果小于4,就是浪费时间,应该减少迭代次数。
- 如果增加/减少迭代次数,请适当调整A。
- 如果增加/减少A,请适当调整a。
--debug
如果您想查看每次迭代的结果,请使用该标志。
TL; 博士