@Dave, buna (çalışma koduyla) bir yanıt gönderen ilk kişiydi ve cevabı, bana utanmaz bir kopya ve yapıştırma ilhamı için paha biçilmez bir kaynak oldu . Bu gönderi @ Dave'in cevabını açıklama ve iyileştirme girişimi olarak başladı, ancak o zamandan beri kendi başına bir cevaba dönüştü.
Yöntemim önemli ölçüde daha hızlı. Rastgele oluşturulmuş RGB renkleri üzerine bir jsPerf karşılaştırmasına göre , @ Dave'in algoritması 600 ms'de çalışırken benimki 30 ms'de çalışıyor . Bu, örneğin hızın kritik olduğu yükleme süresinde kesinlikle önemli olabilir.
Ayrıca, bazı renkler için algoritmam daha iyi performans gösteriyor:
- İçin
rgb(0,255,0)
Dave üretir @, rgb(29,218,34)
ve üretirrgb(1,255,0)
- İçin
rgb(0,0,255)
Dave üretir @, rgb(37,39,255)
ve mayın üretiyorrgb(5,6,255)
- İçin
rgb(19,11,118)
Dave üretir @, rgb(36,27,102)
ve mayın üretiyorrgb(20,11,112)
gösteri
"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>
kullanım
let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.css;
açıklama
Biraz Javascript yazarak başlayacağız.
"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)}%);`;
}
}
Açıklama:
Color
Sınıfı, bir RGB renk temsil eder.
- İşlevi
toString()
, rengi bir CSS rgb(...)
renk dizesinde döndürür.
- İşlevi
hsl()
, HSL'ye dönüştürülen rengi döndürür .
- İşlevi
clamp()
, belirli bir renk değerinin sınırlar içinde (0-255) olmasını sağlar.
Solver
Sınıf bir hedef renk için çözmeye çalışacaktır.
- İşlevi
css()
, bir CSS filtre dizesinde belirli bir filtreyi döndürür.
Uygulama grayscale()
, sepia()
vesaturate()
CSS / SVG filtrelerinin kalbi filtre temel öğeleridir bir görüntüde düşük düzeyli değişiklikleri temsil eden .
Filtreler grayscale()
, sepia()
ve saturate()
filtre tarafından belirtilen (genellikle dinamik olarak oluşturulan) bir matris ile renkten oluşturulan bir matris arasında matris çarpımını<feColorMatrix>
gerçekleştiren filtre ilkeli tarafından uygulanır . Diyagram:
Burada yapabileceğimiz bazı optimizasyonlar var:
- Renk matrisinin son öğesi, ve her zaman öyle olacaktır
1
. Hesaplamanın veya saklamanın bir anlamı yok.
A
RGBA ile değil, RGB ile uğraştığımız için alfa / şeffaflık değerini ( ) hesaplamanın veya saklamanın da bir anlamı yoktur .
- Bu nedenle, filtre matrislerini 5x5'ten 3x5'e ve renk matrisini 1x5'ten 1x3'e kırpabiliriz. . Bu biraz iş tasarrufu sağlar.
- Tüm
<feColorMatrix>
filtreler, 4. ve 5. sütunları sıfır olarak bırakır. Bu nedenle, filtre matrisini 3x3'e daha da düşürebiliriz .
- Çarpma işlemi nispeten basit olduğundan, bunun için karmaşık matematik kitaplıklarında sürüklemeye gerek yoktur . Matris çarpım algoritmasını kendimiz uygulayabiliriz.
Uygulama:
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;
}
(Her satır çarpımının sonuçlarını tutmak için geçici değişkenler kullanıyoruz, çünkü this.r
sonraki hesaplamaları etkileyen değişikliklerin vs. olmasını istemiyoruz .)
Artık <feColorMatrix>
uyguladığımıza grayscale()
göre sepia()
, ve saturate()
bunu belirli bir filtre matrisiyle basitçe çağırabiliriz:
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
]);
}
Uygulama hue-rotate()
hue-rotate()
Filtre tarafından uygulanmaktadır <feColorMatrix type="hueRotate" />
.
Filtre matrisi aşağıda gösterildiği gibi hesaplanır:
Örneğin, eleman bir 00 şöyle hesaplanır:
Bazı notlar:
- Dönme açısı derece cinsinden verilmiştir.
Math.sin()
Veya 'a geçmeden önce radyana dönüştürülmelidir Math.cos()
.
Math.sin(angle)
ve bir Math.cos(angle)
kez hesaplanmalı ve sonra önbelleğe alınmalıdır.
Uygulama:
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
]);
}
Uygulama brightness()
vecontrast()
brightness()
Ve contrast()
filtreler tarafından uygulanır <feComponentTransfer>
ile <feFuncX type="linear" />
.
Her <feFuncX type="linear" />
öğe bir eğim ve kesişim niteliği kabul eder . Daha sonra her yeni renk değerini basit bir formülle hesaplar:
value = slope * value + intercept
Uygulanması kolaydır:
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);
}
Bu kez uygulanan, brightness()
ve contrast()
sıra uygulanabilir:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
Uygulama invert()
invert()
Filtre tarafından uygulanan <feComponentTransfer>
ile <feFuncX type="table" />
.
Spesifikasyon şunları belirtir:
Aşağıda, C başlangıç bileşenidir ve C ' yeniden eşlenen bileşendir; her ikisi de kapalı aralıkta [0,1].
"Tablo", bu fonksiyon özelliği verilen değerler arasında lineer interpolasyon ile tanımlanır tableValues . Tabloda, n tane eşit boyutlu enterpolasyon bölgesi için başlangıç ve bitiş değerlerini belirten n + 1 değerleri (yani, v 0 ila v n ) vardır . Enterpolasyonlar aşağıdaki formülü kullanır:
C değeri için k bul şu şekilde :
k / n ≤ C <(k + 1) / n
C ' sonucunu şu şekilde verir:
C '= v k + (C - k / n) * n * (v k + 1 - v k )
Bu formülün bir açıklaması:
invert()
[- değer, 1]: Filtre bu tabloyu tanımlar. Bu, tableValues veya v .
- Formül, n + 1 tablonun uzunluğu olacak şekilde n'yi tanımlar . Tablonun uzunluğu 2 olduğu için n = 1.
- Formül k'yi tanımlar , k ve k + 1 tablonun indeksleridir. Tablonun 2 öğesi olduğundan, k = 0.
Böylece formülü şu şekilde basitleştirebiliriz:
C '= v 0 + C * (v 1 - v 0 )
Tablonun değerlerini satır içine alırsak:
C '= değer + C * (1 - değer - değer)
Bir basitleştirme daha:
C '= değer + C * (1-2 * değer)
Spesifikasyon, C ve C ' yi 0-1 sınırları içinde (0-255'in aksine) RGB değerleri olarak tanımlar . Sonuç olarak, hesaplamadan önce değerleri küçültmeli ve sonra tekrar ölçeklendirmeliyiz.
Böylece uygulamamıza ulaşıyoruz:
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);
}
Ara: @ Dave'in kaba kuvvet algoritması
@ Dave'in kodu , aşağıdakiler dahil 176.660 filtre kombinasyonu oluşturur :
- 11
invert()
filtre (% 0,% 10,% 20, ...,% 100)
- 11
sepia()
filtre (% 0,% 10,% 20, ...,% 100)
- 20
saturate()
filtre (% 5,% 10,% 15, ...,% 100)
- 73
hue-rotate()
filtre (0deg, 5deg, 10deg, ..., 360deg)
Filtreleri aşağıdaki sırayla hesaplar:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
Daha sonra tüm hesaplanan renkleri yineler. Tolerans dahilinde oluşturulmuş bir renk bulduğunda durur (tüm RGB değerleri, hedef rengin 5 birim içindedir).
Ancak bu yavaş ve verimsizdir. Böylece kendi cevabımı sunuyorum.
SPSA'nın uygulanması
İlk olarak, bir filtre kombinasyonu tarafından üretilen renk ile hedef renk arasındaki farkı döndüren bir kayıp fonksiyonu tanımlamalıyız . Filtreler mükemmelse, kayıp işlevi 0 döndürmelidir.
Renk farkını iki ölçümün toplamı olarak ölçeceğiz:
- RGB farkı, çünkü amaç en yakın RGB değerini üretmektir.
- HSL farkı, çünkü birçok HSL değeri filtrelere karşılık gelir (örn. Ton kabaca
hue-rotate()
, doygunluk ile ilişkilidir saturate()
, vb.) Bu, algoritmayı yönlendirir.
Kayıp işlevi bir bağımsız değişken alır - bir dizi filtre yüzdesi.
Aşağıdaki filtre sırasını kullanacağız:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
Uygulama:
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);
}
Kayıp işlevini şu şekilde en aza indirmeye çalışacağız:
loss([a, b, c, d, e, f]) = 0
SPSA algoritması ( web sitesi , daha fazla bilgi , kağıt , uygulama kağıt , referans kodu ) bu işte çok iyidir. Karmaşık sistemleri yerel minimum, gürültülü / doğrusal olmayan / çok değişkenli kayıp fonksiyonları vb. İle optimize etmek için tasarlanmıştır . Satranç motorlarını ayarlamak için kullanılmıştır . Ve diğer birçok algoritmanın aksine, onu açıklayan makaleler aslında anlaşılırdır (büyük bir çabayla da olsa).
Uygulama:
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'da bazı değişiklikler / optimizasyonlar yaptım:
- Sonuncusu yerine üretilen en iyi sonucu kullanmak.
- Tüm diziler yeniden kullanma (
deltas
, highArgs
, lowArgs
) yerine her yineleme ile yeniden yaratma.
- Tek bir değer yerine a için bir değer dizisi kullanma . Bunun nedeni, tüm filtrelerin farklı olmasıdır ve bu nedenle farklı hızlarda hareket etmeleri / birleşmeleri gerekir.
fix
Her yinelemeden sonra bir işlevi çalıştırma . saturate
(Maksimumun% 7500 olduğu) brightness
ve contrast
(maksimumun% 200 olduğu) ve hueRotate
(değerlerin kelepçelenmek yerine etrafına sarıldığı ) hariç tüm değerleri% 0 ile% 100 arasında sıkıştırır.
SPSA'yı iki aşamalı bir süreçte kullanıyorum:
- Arama alanını "keşfetmeye" çalışan "geniş" aşama. Sonuçlar tatmin edici değilse, SPSA için sınırlı yeniden deneme yapacaktır.
- Geniş sahneden en iyi sonucu alan ve onu "iyileştirmeye" çalışan "dar" sahne. A ve a için dinamik değerler kullanır .
Uygulama:
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'yı Ayarlama
Uyarı: Ne yaptığınızı bildiğinizden emin olmadığınız sürece SPSA koduyla, özellikle de sabitleriyle uğraşmayın .
Önemli sabitler bir , bir , C , başlangıç değerleri, yeniden deneme eşikleri, değerleri max
içinde fix()
ve her bir aşamanın yineleme sayısı. Tüm bu değerler, iyi sonuçlar elde etmek için dikkatlice ayarlandı ve bunlarla rastgele karıştırmak, algoritmanın kullanışlılığını neredeyse kesin olarak azaltacaktır.
Değiştirmekte ısrar ediyorsanız, "optimize etmeden" önce ölçüm yapmalısınız.
İlk önce bu yamayı uygulayın .
Ardından kodu Node.js'de çalıştırın. Oldukça uzun bir süre sonra, sonuç şu şekilde olmalıdır:
Average loss: 3.4768521401985275
Average time: 11.4915ms
Şimdi sabitleri kalbinizin içeriğine göre ayarlayın.
Bazı ipuçları:
- Ortalama kayıp 4 civarında olmalıdır. 4'ten büyükse, çok uzak sonuçlar veriyor demektir ve doğruluk için ayarlamanız gerekir. 4'ten azsa, zaman kaybediyordur ve yineleme sayısını azaltmalısınız.
- Yineleme sayısını artırır / azaltırsanız, A'yı ayarlayın uygun şekilde .
- A'yı artırır / azaltırsanız , bir uygun.
--debug
Her yinelemenin sonucunu görmek istiyorsanız bayrağı kullanın .
TL; DR