@Dave เป็นคนแรกที่โพสต์คำตอบสำหรับสิ่งนี้ (พร้อมรหัสการทำงาน) และคำตอบของเขาเป็นแหล่งข้อมูลอันล้ำค่าของสำเนาไร้ยางอายและวางแรงบันดาลใจให้ฉัน โพสต์นี้เริ่มต้นจากความพยายามที่จะอธิบายและปรับแต่งคำตอบของ @ Dave แต่ตั้งแต่นั้นมาก็ได้พัฒนาเป็นคำตอบของตัวเอง
วิธีการของฉันเร็วกว่ามาก ตามมาตรฐาน jsPerfบนที่สร้างแบบสุ่มสี RGB, @ อัลกอริทึมของเดฟทำงานใน600 มิลลิวินาทีในขณะที่ทำงานในเหมือง30 มิลลิวินาที สิ่งนี้มีความสำคัญอย่างแน่นอนเช่นในเวลาโหลดซึ่งความเร็วเป็นสิ่งสำคัญ
นอกจากนี้สำหรับบางสีอัลกอริทึมของฉันยังทำงานได้ดีกว่า:
- สำหรับ
rgb(0,255,0)
@ Dave's ผลิต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()
มีการดำเนินการโดย primative กรอง<feColorMatrix>
ซึ่งดำเนินการคูณเมทริกซ์ระหว่างเมทริกซ์ที่ระบุโดยตัวกรอง (มักจะสร้างแบบไดนามิก) และเมทริกซ์ที่สร้างขึ้นจากสี แผนภาพ:
มีการเพิ่มประสิทธิภาพบางอย่างที่เราสามารถทำได้ที่นี่:
1
องค์ประกอบสุดท้ายของเมทริกซ์สีเป็นและจะเป็น ไม่มีจุดคำนวณหรือเก็บไว้
- ไม่มีจุดใดในการคำนวณหรือจัดเก็บค่าอัลฟา / ความโปร่งใส (
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 ค่า (เช่นวี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หรือวี
- สูตรกำหนด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);
}
Interlude: อัลกอริธึมกำลังดุร้ายของ @ 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()
ตัวกรอง (0deg, 5deg, 10deg, ... , 360deg)
จะคำนวณตัวกรองตามลำดับต่อไปนี้:
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อัลกอริทึม ( เว็บไซต์ , ข้อมูลเพิ่มเติม , กระดาษ , กระดาษการดำเนินงาน , รหัสอ้างอิง ) เป็นสิ่งที่ดีมากในตอนนี้ ได้รับการออกแบบมาเพื่อเพิ่มประสิทธิภาพระบบที่ซับซ้อนด้วย minima ในพื้นที่ฟังก์ชันการสูญเสียที่มีเสียงดัง / ไม่เชิงเส้น / หลายตัวแปร ฯลฯมันถูกใช้เพื่อปรับแต่งเครื่องมือหมากรุกจะได้รับการใช้ในการปรับแต่งเครื่องยนต์หมากรุกและแตกต่างจากอัลกอริทึมอื่น ๆ อีกมากมายเอกสารที่อธิบายมันสามารถเข้าใจได้จริง (แม้ว่าจะใช้ความพยายามอย่างมากก็ตาม)
การดำเนินงาน:
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 แบบ จำกัด หากผลลัพธ์ไม่เป็นที่น่าพอใจ
- สเตจ "แคบ" ซึ่งให้ผลลัพธ์ที่ดีที่สุดจากสเตจกว้างและพยายาม "ปรับแต่ง" จะใช้ค่าแบบไดนามิกสำหรับและ
การดำเนินงาน:
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; DR