@Dave fue el primero en publicar una respuesta a esto (con código de trabajo), y su respuesta ha sido una fuente invaluable de copia descarada e inspiración para pegar para mí. Esta publicación comenzó como un intento de explicar y refinar la respuesta de @ Dave, pero desde entonces se ha convertido en una respuesta propia.
Mi método es significativamente más rápido. De acuerdo con un punto de referencia de jsPerf sobre colores RGB generados aleatoriamente, el algoritmo de @ Dave se ejecuta en 600 ms , mientras que el mío se ejecuta en 30 ms . Esto definitivamente puede importar, por ejemplo, en el tiempo de carga, donde la velocidad es crítica.
Además, para algunos colores, mi algoritmo funciona mejor:
- Porque
rgb(0,255,0)
@ Dave's produce rgb(29,218,34)
y producergb(1,255,0)
- Porque
rgb(0,0,255)
@ Dave's produce rgb(37,39,255)
y el mío producergb(5,6,255)
- Porque
rgb(19,11,118)
@ Dave's produce rgb(36,27,102)
y el mío producergb(20,11,112)
Manifestación
"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>
Uso
let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.css;
Explicación
Comenzaremos escribiendo algo de 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)}%);`;
}
}
Explicación:
- La
Color
clase representa un color RGB.
- Su
toString()
función devuelve el color en una rgb(...)
cadena de colores CSS .
- Su
hsl()
función devuelve el color, convertido a HSL .
- Su
clamp()
función asegura que un valor de color dado esté dentro de los límites (0-255).
- La
Solver
clase intentará encontrar un color objetivo.
- Su
css()
función devuelve un filtro dado en una cadena de filtro CSS.
Implementando grayscale()
,sepia()
ysaturate()
El corazón de los filtros CSS / SVG son los filtros primitivos , que representan modificaciones de bajo nivel en una imagen.
Los filtros grayscale()
, sepia()
y saturate()
son implementados por el filtro primativo <feColorMatrix>
, que realiza la multiplicación de matrices entre una matriz especificada por el filtro (a menudo generada dinámicamente) y una matriz creada a partir del color. Diagrama:
Hay algunas optimizaciones que podemos hacer aquí:
- El último elemento de la matriz de colores es y siempre será
1
. No tiene sentido calcularlo o almacenarlo.
- Tampoco tiene sentido calcular o almacenar el valor alfa / transparencia (
A
), ya que estamos tratando con RGB, no con RGBA.
- Por lo tanto, podemos recortar las matrices de filtro de 5x5 a 3x5 y la matriz de color de 1x5 a 1x3 . Esto ahorra un poco de trabajo.
- Todos los
<feColorMatrix>
filtros dejan las columnas 4 y 5 como ceros. Por lo tanto, podemos reducir aún más la matriz del filtro a 3x3 .
- Dado que la multiplicación es relativamente simple, no es necesario arrastrar bibliotecas matemáticas complejas para esto. Podemos implementar el algoritmo de multiplicación de matrices nosotros mismos.
Implementación:
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;
}
(Usamos variables temporales para contener los resultados de cada multiplicación de filas, porque no queremos cambios en this.r
, etc., afecten los cálculos posteriores).
Ahora que lo hemos implementado <feColorMatrix>
, podemos implementar grayscale()
, sepia()
y saturate()
, que simplemente lo invocan con una matriz de filtro dada:
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
]);
}
Implementar hue-rotate()
El hue-rotate()
filtro es implementado por <feColorMatrix type="hueRotate" />
.
La matriz de filtro se calcula como se muestra a continuación:
Por ejemplo, el elemento a 00 se calcularía así:
Algunas notas:
- El ángulo de rotación se expresa en grados. Debe convertirse a radianes antes de pasar a
Math.sin()
o Math.cos()
.
Math.sin(angle)
y Math.cos(angle)
debe calcularse una vez y luego almacenarse en caché.
Implementación:
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
]);
}
Implementar brightness()
ycontrast()
Los filtros brightness()
y contrast()
se implementan con <feComponentTransfer>
con <feFuncX type="linear" />
.
Cada <feFuncX type="linear" />
elemento acepta un atributo de pendiente e intersección . Luego calcula cada nuevo valor de color mediante una fórmula simple:
value = slope * value + intercept
Esto es fácil de implementar:
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);
}
Una vez que esto se implementa, brightness()
y también contrast()
se puede implementar:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
Implementar invert()
El invert()
filtro se implementa con <feComponentTransfer>
con <feFuncX type="table" />
.
La especificación dice:
A continuación, C es el componente inicial y C ' es el componente reasignado; ambos en el intervalo cerrado [0,1].
Para "tabla", la función se define por interpolación lineal entre los valores dados en el atributo tableValues . La tabla tiene n + 1 valores (es decir, v 0 av n ) que especifican los valores inicial y final para n regiones de interpolación de tamaño uniforme. Las interpolaciones utilizan la siguiente fórmula:
Para un valor C, encuentre k tal que:
k / n ≤ C <(k + 1) / n
El resultado C ' viene dado por:
C '= v k + (C - k / n) * n * (v k + 1 - v k )
Una explicación de esta fórmula:
- El
invert()
filtro define esta tabla: [valor, 1 - valor]. Esto es tableValues o v .
- La fórmula define n , de modo que n + 1 es la longitud de la tabla. Dado que la longitud de la mesa es 2, n = 1.
- La fórmula define k , siendo k y k + 1 índices de la tabla. Dado que la tabla tiene 2 elementos, k = 0.
Por tanto, podemos simplificar la fórmula a:
C '= v 0 + C * (v 1 - v 0 )
Al incluir los valores de la tabla, nos queda:
C '= valor + C * (1 - valor - valor)
Una simplificación más:
C '= valor + C * (1-2 * valor)
La especificación define C y C ' como valores RGB, dentro de los límites 0-1 (a diferencia de 0-255). Como resultado, debemos reducir la escala de los valores antes del cálculo y volver a escalarlos después.
Así llegamos a nuestra implementación:
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);
}
Interludio: algoritmo de fuerza bruta de @ Dave
El código de @ Dave genera 176,660 combinaciones de filtros, que incluyen:
- 11
invert()
filtros (0%, 10%, 20%, ..., 100%)
- 11
sepia()
filtros (0%, 10%, 20%, ..., 100%)
- 20
saturate()
filtros (5%, 10%, 15%, ..., 100%)
- 73
hue-rotate()
filtros (0deg, 5deg, 10deg, ..., 360deg)
Calcula los filtros en el siguiente orden:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
Luego, recorre todos los colores calculados. Se detiene una vez que ha encontrado un color generado dentro de la tolerancia (todos los valores RGB están dentro de las 5 unidades del color de destino).
Sin embargo, esto es lento e ineficaz. Por lo tanto, presento mi propia respuesta.
Implementación de SPSA
Primero, debemos definir una función de pérdida , que devuelva la diferencia entre el color producido por una combinación de filtros y el color de destino. Si los filtros son perfectos, la función de pérdida debería devolver 0.
Mediremos la diferencia de color como la suma de dos métricas:
- Diferencia RGB, porque el objetivo es producir el valor RGB más cercano.
- Diferencia de HSL, porque muchos valores de HSL corresponden a filtros (por ejemplo, el tono se correlaciona aproximadamente con
hue-rotate()
, la saturación se correlaciona con saturate()
, etc.) Esto guía el algoritmo.
La función de pérdida tomará un argumento: una matriz de porcentajes de filtro.
Usaremos el siguiente orden de filtro:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
Implementación:
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);
}
Intentaremos minimizar la función de pérdida, de manera que:
loss([a, b, c, d, e, f]) = 0
El algoritmo SPSA ( sitio web , más información , documento , documento de implementación , código de referencia ) es muy bueno en esto. Fue diseñado para optimizar sistemas complejos con mínimos locales, funciones de pérdida ruidosas / no lineales / multivariantes, etc. Se ha utilizado para ajustar motores de ajedrez . Y a diferencia de muchos otros algoritmos, los artículos que lo describen son realmente comprensibles (aunque con gran esfuerzo).
Implementación:
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;
}
}
Hice algunas modificaciones / optimizaciones a SPSA:
- Utilizando el mejor resultado producido, en lugar del último.
- La reutilización de todas las matrices (
deltas
, highArgs
, lowArgs
), en lugar de volver a crear con cada iteración.
- Usando una matriz de valores para a , en lugar de un solo valor. Esto se debe a que todos los filtros son diferentes y, por lo tanto, deberían moverse / converger a diferentes velocidades.
- Ejecutando una
fix
función después de cada iteración. Fija todos los valores entre 0% y 100%, excepto saturate
(donde el máximo es 7500%), brightness
y contrast
(donde el máximo es 200%) y hueRotate
(donde los valores se envuelven en lugar de fijar).
Utilizo SPSA en un proceso de dos etapas:
- La etapa "amplia", que intenta "explorar" el espacio de búsqueda. Hará reintentos limitados de SPSA si los resultados no son satisfactorios.
- El escenario "estrecho", que toma el mejor resultado del escenario ancho e intenta "refinarlo". Utiliza valores dinámicos para A y a .
Implementación:
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);
}
Tuning SPSA
Advertencia: No se meta con el código SPSA, especialmente con sus constantes, a menos que esté seguro de saber lo que está haciendo.
Las constantes importantes son A , a , c , los valores iniciales, los umbrales de reintento, los valores de max
in fix()
y el número de iteraciones de cada etapa. Todos estos valores se ajustaron cuidadosamente para producir buenos resultados, y usarlos al azar reducirá casi definitivamente la utilidad del algoritmo.
Si insiste en modificarlo, debe medir antes de "optimizar".
Primero, aplique este parche .
Luego ejecute el código en Node.js. Después de bastante tiempo, el resultado debería ser algo como esto:
Average loss: 3.4768521401985275
Average time: 11.4915ms
Ahora sintonice las constantes al contenido de su corazón.
Algunos consejos:
- La pérdida promedio debe ser de alrededor de 4. Si es mayor que 4, está produciendo resultados que están demasiado lejos y debe ajustar la precisión. Si es menor que 4, es una pérdida de tiempo y debería reducir el número de iteraciones.
- Si aumenta / disminuye el número de iteraciones, ajuste A de forma adecuada.
- Si aumenta / disminuye A , ajustar una manera apropiada.
- Utilice la
--debug
bandera si desea ver el resultado de cada iteración.
TL; DR