@Dave là người đầu tiên đăng câu trả lời cho vấn đề này (với mã làm việc), và câu trả lời của anh ấy là nguồn cảm hứng vô giá cho việc sao chép và dán vào tôi. Bài đăng này bắt đầu như một nỗ lực giải thích và tinh chỉnh câu trả lời của @ Dave, nhưng kể từ đó nó đã phát triển thành một câu trả lời của riêng mình.
Phương pháp của tôi nhanh hơn đáng kể. Theo điểm chuẩn jsPerf trên các màu RGB được tạo ngẫu nhiên, thuật toán của @ Dave chạy trong 600 mili giây , trong khi thuật toán của tôi chạy trong 30 mili giây . Điều này chắc chắn có thể quan trọng, chẳng hạn như thời gian tải, nơi tốc độ là rất quan trọng.
Hơn nữa, đối với một số màu, thuật toán của tôi hoạt động tốt hơn:
- Đối với
rgb(0,255,0)
, @ Dave's sản xuất rgb(29,218,34)
và sản xuấtrgb(1,255,0)
- Đối với
rgb(0,0,255)
, @ Dave's sản xuất rgb(37,39,255)
và của tôi sản xuấtrgb(5,6,255)
- Đối với
rgb(19,11,118)
, @ Dave's sản xuất rgb(36,27,102)
và của tôi sản xuấtrgb(20,11,112)
Bản giới thiệu
"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>
Sử dụng
let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.css;
Giải trình
Chúng ta sẽ bắt đầu bằng cách viết một số 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)}%);`;
}
}
Giải trình:
- Các
Color
lớp đại diện cho một màu RGB.
toString()
Hàm của nó trả về màu trong một rgb(...)
chuỗi màu CSS .
hsl()
Hàm của nó trả về màu, được chuyển đổi thành HSL .
clamp()
Chức năng của nó đảm bảo rằng một giá trị màu nhất định nằm trong giới hạn (0-255).
- Các
Solver
lớp học sẽ cố gắng giải quyết cho một màu sắc mục tiêu.
css()
Hàm của nó trả về một bộ lọc nhất định trong một chuỗi bộ lọc CSS.
Thực hiện grayscale()
, sepia()
vàsaturate()
Trung tâm của bộ lọc CSS / SVG là bộ lọc nguyên thủy , đại diện cho các sửa đổi cấp thấp đối với hình ảnh.
Các bộ lọc grayscale()
, sepia()
và saturate()
được thực hiện bởi các primative lọc <feColorMatrix>
, mà thực hiện phép nhân ma trận giữa một ma trận xác định bởi các bộ lọc (thường tạo động), và một ma trận được tạo ra từ màu sắc. Biểu đồ:
Chúng tôi có thể thực hiện một số tối ưu hóa ở đây:
- Phần tử cuối cùng của ma trận màu đang và sẽ luôn như vậy
1
. Không có điểm nào để tính toán hoặc lưu trữ nó.
- Không có điểm nào để tính toán hoặc lưu trữ giá trị alpha / trong suốt (
A
), vì chúng tôi đang xử lý RGB, không phải RGBA.
- Do đó, chúng ta có thể cắt ma trận bộ lọc từ 5x5 thành 3x5 và ma trận màu từ 1x5 thành 1x3 . Điều này giúp tiết kiệm một chút công việc.
- Tất cả các
<feColorMatrix>
bộ lọc để lại cột 4 và 5 là số 0. Do đó, chúng ta có thể giảm thêm ma trận bộ lọc xuống 3x3 .
- Vì phép nhân tương đối đơn giản, không cần phải kéo trong các thư viện toán học phức tạp cho việc này. Chúng ta có thể tự thực hiện thuật toán nhân ma trận.
Thực hiệ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;
}
(Chúng tôi sử dụng các biến tạm thời để giữ kết quả của mỗi phép nhân hàng, vì chúng tôi không muốn các thay đổi đối với this.r
, v.v. ảnh hưởng đến các phép tính tiếp theo.)
Bây giờ chúng ta đã thực hiện <feColorMatrix>
, chúng ta có thể thực hiện grayscale()
, sepia()
và saturate()
, mà chỉ đơn giản gọi nó với một ma trận lọc đưa ra:
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
]);
}
Thực thi hue-rotate()
Bộ hue-rotate()
lọc được thực hiện bởi <feColorMatrix type="hueRotate" />
.
Ma trận bộ lọc được tính như hình dưới đây:
Ví dụ, phần tử a 00 sẽ được tính như vậy:
Một số lưu ý:
- Góc quay được cho bằng độ. Nó phải được chuyển đổi sang radian trước khi chuyển đến
Math.sin()
hoặc Math.cos()
.
Math.sin(angle)
và Math.cos(angle)
nên được tính toán một lần và sau đó lưu vào bộ nhớ cache.
Thực hiệ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
]);
}
Thực hiện brightness()
vàcontrast()
Bộ lọc brightness()
và contrast()
được triển khai bằng <feComponentTransfer>
với <feFuncX type="linear" />
.
Mỗi <feFuncX type="linear" />
phần tử chấp nhận một thuộc tính độ dốc và đánh chặn . Sau đó, nó sẽ tính toán từng giá trị màu mới thông qua một công thức đơn giản:
value = slope * value + intercept
Điều này rất dễ thực hiện:
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);
}
Khi điều này được triển khai brightness()
và contrast()
cũng có thể được triển khai:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
Thực thi invert()
Bộ invert()
lọc được triển khai bằng <feComponentTransfer>
với <feFuncX type="table" />
.
Thông số kỹ thuật cho biết:
Trong phần sau, C là thành phần ban đầu và C ' là thành phần được ánh xạ lại; cả trong khoảng đóng [0,1].
Đối với "bảng", hàm được xác định bằng nội suy tuyến tính giữa các giá trị được cho trong bảng thuộc tính tableValues . Bảng có n + 1 giá trị (tức là v 0 đến v n ) xác định giá trị bắt đầu và giá trị kết thúc cho n vùng nội suy có kích thước đồng đều. Nội suy sử dụng công thức sau:
Với giá trị C tìm k sao cho:
k / n ≤ C <(k + 1) / n
Kết quả C ' được cho bởi:
C '= v k + (C - k / n) * n * (v k + 1 - v k )
Giải thích về công thức này:
- Bộ
invert()
lọc xác định bảng này: [giá trị, 1 - giá trị]. Đây là tableValues hoặc v .
- Công thức xác định n , sao cho n + 1 là độ dài của bảng. Vì chiều dài của bảng là 2 nên n = 1.
- Công thức xác định k , với k và k + 1 là các chỉ số của bảng. Vì bảng có 2 phần tử nên k = 0.
Do đó, chúng ta có thể đơn giản hóa công thức thành:
C '= v 0 + C * (v 1 - v 0 )
Nội tuyến các giá trị của bảng, chúng ta còn lại:
C '= giá trị + C * (1 - giá trị - giá trị)
Một đơn giản hóa nữa:
C '= giá trị + C * (1 - 2 * giá trị)
Thông số xác định C và C ' là các giá trị RGB, nằm trong giới hạn 0-1 (trái ngược với 0-255). Do đó, chúng ta phải thu nhỏ các giá trị trước khi tính toán và sao lưu chúng sau đó.
Vì vậy, chúng tôi đi đến triển khai của mình:
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: Thuật toán bạo lực của @ Dave
Mã của @ Dave tạo ra 176.660 kết hợp bộ lọc, bao gồm:
- 11
invert()
bộ lọc (0%, 10%, 20%, ..., 100%)
- 11
sepia()
bộ lọc (0%, 10%, 20%, ..., 100%)
- 20
saturate()
bộ lọc (5%, 10%, 15%, ..., 100%)
- 73
hue-rotate()
bộ lọc (0deg, 5deg, 10deg, ..., 360deg)
Nó tính toán các bộ lọc theo thứ tự sau:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
Sau đó, nó lặp lại qua tất cả các màu được tính toán. Nó dừng lại khi nó đã tìm thấy một màu được tạo trong phạm vi dung sai (tất cả các giá trị RGB nằm trong vòng 5 đơn vị tính từ màu đích).
Tuy nhiên, việc này diễn ra chậm và không hiệu quả. Vì vậy, tôi trình bày câu trả lời của riêng tôi.
Triển khai SPSA
Đầu tiên, chúng ta phải xác định một hàm mất mát , hàm này trả về sự khác biệt giữa màu được tạo ra bởi tổ hợp bộ lọc và màu đích. Nếu bộ lọc hoàn hảo, hàm mất mát sẽ trả về 0.
Chúng tôi sẽ đo lường sự khác biệt màu sắc dưới dạng tổng của hai số liệu:
- Sự khác biệt RGB, bởi vì mục tiêu là tạo ra giá trị RGB gần nhất.
- Sự khác biệt HSL, vì nhiều giá trị HSL tương ứng với các bộ lọc (ví dụ: màu sắc tương quan với
hue-rotate()
, độ bão hòa tương quan với saturate()
, v.v.) Điều này hướng dẫn thuật toán.
Hàm mất mát sẽ nhận một đối số - một mảng phần trăm bộ lọc.
Chúng tôi sẽ sử dụng thứ tự bộ lọc sau:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
Thực hiệ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);
}
Chúng tôi sẽ cố gắng giảm thiểu hàm mất mát, như vậy:
loss([a, b, c, d, e, f]) = 0
Các SPSA thuật toán ( trang web , biết thêm , giấy , giấy thi , mã tham chiếu ) là rất tốt lúc này. Nó được thiết kế để tối ưu hóa các hệ thống phức tạp với các hàm cực tiểu cục bộ, nhiễu / phi tuyến / đa biến, v.v. Nó đã được sử dụng để điều chỉnh các động cơ cờ vua . Và không giống như nhiều thuật toán khác, các bài báo mô tả nó thực sự có thể hiểu được (mặc dù rất nỗ lực).
Thực hiệ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;
}
}
Tôi đã thực hiện một số sửa đổi / tối ưu hóa cho SPSA:
- Sử dụng kết quả tốt nhất được tạo ra, thay vì kết quả cuối cùng.
- Tái sử dụng tất cả các mảng (
deltas
, highArgs
, lowArgs
), thay vì tái tạo chúng với mỗi lần lặp.
- Sử dụng một mảng giá trị cho a , thay vì một giá trị duy nhất. Điều này là do tất cả các bộ lọc đều khác nhau, và do đó chúng sẽ di chuyển / hội tụ với tốc độ khác nhau.
- Chạy một
fix
hàm sau mỗi lần lặp. Nó kẹp tất cả các giá trị trong khoảng từ 0% đến 100%, ngoại trừ saturate
(trong đó giá trị tối đa là 7500%), brightness
và contrast
(trong đó giá trị tối đa là 200%) và hueRotate
(trong đó các giá trị được quấn quanh thay vì kẹp).
Tôi sử dụng SPSA trong một quy trình hai giai đoạn:
- Giai đoạn "rộng", cố gắng "khám phá" không gian tìm kiếm. Nó sẽ giới hạn số lần thử lại SPSA nếu kết quả không đạt yêu cầu.
- Giai đoạn "hẹp", lấy kết quả tốt nhất từ giai đoạn rộng và cố gắng "tinh chỉnh" nó. Nó sử dụng các giá trị động cho A và a .
Thực hiệ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);
}
Điều chỉnh SPSA
Cảnh báo: Không làm rối mã SPSA, đặc biệt là với các hằng số của nó, trừ khi bạn chắc chắn rằng mình biết mình đang làm gì.
Các hằng số quan trọng là A , a , c , giá trị ban đầu, ngưỡng thử lại, giá trị max
trong fix()
và số lần lặp của mỗi giai đoạn. Tất cả các giá trị này đã được điều chỉnh cẩn thận để tạo ra kết quả tốt và việc thay đổi ngẫu nhiên chúng gần như chắc chắn sẽ làm giảm tính hữu dụng của thuật toán.
Nếu bạn khăng khăng muốn thay đổi nó, bạn phải đo lường trước khi bạn "tối ưu hóa".
Đầu tiên, hãy áp dụng bản vá này .
Sau đó chạy mã trong Node.js. Sau một thời gian, kết quả sẽ như thế này:
Average loss: 3.4768521401985275
Average time: 11.4915ms
Bây giờ hãy điều chỉnh các hằng số cho phù hợp với trái tim của bạn.
Một số lời khuyên:
- Mức tổn thất trung bình nên vào khoảng 4. Nếu lớn hơn 4, nó đang tạo ra kết quả quá xa và bạn nên điều chỉnh để có độ chính xác. Nếu nhỏ hơn 4 thì rất lãng phí thời gian và bạn nên giảm số lần lặp lại.
- Nếu bạn tăng / giảm số lần lặp, hãy điều chỉnh A hợp lý.
- Nếu bạn tăng / giảm A , hãy điều chỉnh a hợp lý.
- Sử dụng
--debug
cờ nếu bạn muốn xem kết quả của mỗi lần lặp.
TL; DR