@Daveはこれに(動作するコードを使用して)最初に回答を投稿し、彼の回答は恥知らずなコピーと貼り付けの非常に貴重な情報源でしたインスピレーションのた。この投稿は、@ Daveの回答を説明および改善するための試みとして始まりましたが、その後、独自の回答に発展しました。
私の方法は大幅に高速です。によるとランダムに生成されたRGBカラーのjsPerfベンチマークに、@ Daveのアルゴリズムは600ミリ秒で実行され、鉱山は30ミリ秒で実行されます。これは、例えば、速度が重要であるロード時間など、間違いなく問題になる可能性があります。
さらに、一部の色については、私のアルゴリズムのパフォーマンスが向上しています。
- の場合
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()
関数は、CSS rgb(...)
カラー文字列で色を返します。
- その
hsl()
関数は、HSLに変換された色を返します。
- その
clamp()
機能は、指定された色の値が範囲(0〜255)内にあることを保証します。
Solver
クラスは、ターゲット色の解決しようとします。
- その
css()
関数は、CSSフィルター文字列で指定されたフィルターを返します。
実装grayscale()
、sepia()
およびsaturate()
CSS / SVGフィルターの中心は フィルタープリミティブであり、画像に対する低レベルの変更を表します。
フィルタgrayscale()
、sepia()
、及びはsaturate()
フィルタプリミティブによって実装されている<feColorMatrix>
行い、行列乗算行列の間にはフィルタ(多くの場合、動的に生成された)、および色から作成された行列で指定されました。図:
ここでは、いくつかの最適化を行うことができます。
- カラーマトリックスの最後の要素はであり、常にになります
1
。計算したり保存したりする意味はありません。
A
RGBAではなくRGBを扱っているため、アルファ/透明度の値()を計算または保存する意味もありません。
- したがって、フィルターマトリックスを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-値-値)
もう1つの簡略化:
C '=値+ C *(1-2 *値)
仕様では、CおよびC 'を0〜255の範囲ではなく0〜1の範囲内のRGB値として定義しています。結果として、計算前に値を縮小し、後で拡大する必要があります。
したがって、実装に到達します。
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を返します。
色の違いを2つの指標の合計として測定します。
- 目標は最も近いRGB値を生成することなので、RGBの違い。
- HSLの違い。多くのHSL値がフィルターに対応しているためです(たとえば、色相はと大まかに相関し
hue-rotate()
、彩度はと相関しsaturate()
ます)。これはアルゴリズムをガイドします。
損失関数は1つの引数(フィルターのパーセンテージの配列)を取ります。
次のフィルター次数を使用します。
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
)、代わりに、各反復でそれらを再作成します。
- 値の配列を使用して、代わりに単一の値を、。これは、すべてのフィルターが異なるため、異なる速度で移動/収束する必要があるためです。
fix
各反復後に関数を実行します。saturate
(最大値が7500%の場合)brightness
およびcontrast
(最大値が200%の場合)とhueRotate
(値が固定されずに折り返されている場合を除いて)すべての値を0%から100%に固定します。
SPSAは2段階のプロセスで使用します。
- サーチスペースを「探索」しようとする「ワイド」ステージ。結果が満足のいくものでない場合、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を増減する場合は、適宜調整してください。
--debug
各反復の結果を確認する場合は、フラグを使用します。
TL; DR