JavaScript Numerical Computational Series
Conv2d.
In this post, convolution is applied to an image array called the conv2d layer. There are two ways to perform convolution:
- numerically with a while loop,
- and with an equation: Math.floor(((n+p)-w)/s) + 1, where n, p, w, and s represent input widthORheight, padding, kernel widthORheight, and stride. (n+p)-w is the number of rows one can start convolution from, let’s call the rows “start rows”. Divide the “start rows” by the stride to get the desired rows to perform convolution with, and then add one because the first or last start row is not counted for. Typically, the equation Math.floor((n + 2*p - w)/s) is used to determine the number of convolution loops, but for cases like w=3, n=7, p=0, s=1 the output is incorrectly 4 instead of 5.
var w=3;
var n=7;
var p=0;
var s=1;
var typically_equation = Math.floor((n + 2*p - w)/s);
var equation = Math.floor(((n+p)-w)/s) + 1;
console.log("typically_equation: ", typically_equation);
console.log("equation: ", equation);
// "typically_equation: " 4"
// equation: " 5
The conv2d layer consists of the elements: input shape (w,h,d), output shape (w,h,d), K (kernel, the number trainable parameters) (w,h), P (padding) (default: 0), S (stride) (default: 1), G (groups, the number of blocked connections from input channels to output channels) (default: 1).
Ways to multiple the kernel by the image array:
- A single kernel can be applied to a single inupt image array. kernel w0(3,3) * input F(x=7,y=7) = output w0*F(x,y)
- A different kernel can be applied to a single input image array. ([w0]_{3,3}, [w1]_{3,3}, [w2]_{3,3}, [w3]_{3,3}) * input F(1,7,7) = ([w0*F(x=7,y=7)], [w1*F(x=7,y=7)], [w2*F(x=7,y=7)], [w3*F(x=7,y=7)])_{4,5,5}
- A different kernel can be applied to many different input image arrays, like RGB. ([w0]_{3,3}, [w1]_{3,3}, [w2]_{3,3}, [w3]_{3,3}) * ([input0 F0(1,7,7)], [input1 F1(1,7,7)], [input2 F2(1,7,7)])_{3,7,7} = ([w0*F0(x=7,y=7)+w0*F1(x=7,y=7)+w0*F2(x=7,y=7)], [w1*F0+w1*F1+w1*F2], [w2*F0+w2*F1+w2*F2], [w3*F0+w3*F1+w3*F2])_{4,5,5}
- Groups: The three cases before are when Groups=1, ([w0]_{3,3}, [w1]_{3,3}) * ([input0 F0(1,7,7)], [input1 F1(1,7,7)])_{2,7,7} = ([w0*F0(x=7,y=7)+(w0*F1)], [w1*F0(x=7,y=7)+(w1*F1)])_{2,5,5}. However, if Groups=2, inputs can be concatenated instead of summed with respect to each kernel. ([w0]_{3,3}, [w1]_{3,3}) * ([input0 F0(1,7,7)], [input1 F1(1,7,7)])_{2,7,7} = ([w0*F0(x=7,y=7)], [w1*F0(x=7,y=7)], [w0*F1], [w1*F1])_{4,5,5}.
<!DOCTYPE>
<html>
<head></head>
<body>
<div class="javascript_css"></div>
<script>
function functionName() {
var inputElement = document.createElement("input");
inputElement.setAttribute("id", "input0");
inputElement.setAttribute("class", "input_simple");
inputElement.setAttribute("placeholder", "inputs|kernels");
inputElement.setAttribute("onfocusin", "processEvent_focusin(this.id)");
inputElement.setAttribute("onfocusout", "processEvent_focusout(this.id)");
inputElement.style.position = "absolute";
inputElement.style.top = "60px";
inputElement.style.left = "0px";
inputElement.style.width = "300px";
inputElement.style.height = "20px";
inputElement.style.backgroundColor = "hsl(60 100% 90%)";
inputElement.value = "[[[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 155, 151, 0, 0], [0, 0, 156, 0, 155, 0, 0], [0, 0, 0, 0, 156, 0, 0], [0, 0, 0, 155, 0, 0, 0], [0, 0, 157, 167, 135, 150, 0], [0, 0, 0, 0, 0, 0, 0]]]|[[[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], [[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]]]";
document.getElementsByClassName("javascript_css")[0].appendChild(inputElement);
var pElement = document.createElement("p");
pElement.setAttribute("id", "p0");
pElement.setAttribute("class", "p_simple");
pElement.style.position = "absolute";
pElement.style.top = "90px";
pElement.style.left = "0px";
pElement.style.width = "300px";
pElement.style.height = "100px";
pElement.style.backgroundColor = "hsl(60 100% 90%)";
pElement.style.overflow = "scroll";
document.getElementsByClassName("javascript_css")[0].appendChild(pElement);
}
function processEvent_focusin(this_id) {
document.getElementById(this_id).style.backgroundColor = "hsl(120 100% 90%)";
}
function processEvent_focusout(this_id) {
document.getElementById(this_id).style.backgroundColor = "hsl(60 100% 90%)";
var arrStr = document.getElementById(this_id).value.split("|");
var inputs = JSON.parse(arrStr.at(0).trim());
document.getElementById("p0").innerHTML += "inputs size (width/columns,height/rows,depth): "+[inputs[0][0].length, inputs[0].length, inputs.length]+"<br>";
var kernels = JSON.parse(arrStr.at(1).trim());
document.getElementById("p0").innerHTML += "kernels: "+kernels+"<br>";
document.getElementById("p0").innerHTML += "kernels size (width/columns,height/rows,depth): "+[kernels[0][0].length, kernels[0].length, kernels.length]+"<br>";
var outputs = conv2d(inputs, kernels);
document.getElementById("p0").innerHTML += "outputs size (width/columns,height/rows,depth): "+[outputs[0][0].length, outputs[0].length, outputs.length]+"<br>";
// Plotting.
var divElement = document.createElement("div");
divElement.setAttribute("id", "div0");
divElement.setAttribute("class", "div_simple");
divElement.style.position = "absolute";
divElement.style.top = "210px";
divElement.style.left = "0px";
divElement.style.width = "300px";
divElement.style.height = "150px";
divElement.style.border = "1px solid gray";
divElement.style.borderRadius = "2px";
document.getElementsByClassName("javascript_css")[0].appendChild(divElement);
var identity = Array.from({length:inputs[0].length}, (v0,i0) => { return Array.from({length:inputs[0].length}, (v1,i1) => { return 150; }); });
var cnt = 0;
for (var i=0; i<inputs.length; i++) {
var divElement = document.createElement("div");
divElement.setAttribute("id", "div_"+cnt);
divElement.setAttribute("class", "div_simple_");
divElement.style.width = "50px";
divElement.style.height = "50px";
divElement.style.border = "1px solid gray";
divElement.style.borderRadius = "2px";
document.getElementById("div0").appendChild(divElement);
plot1(inputs[i], identity, identity, "div_"+cnt);
cnt = cnt + 1;
}
var identity = Array.from({length:outputs[0].length}, (v0,i0) => { return Array.from({length:outputs[0].length}, (v1,i1) => { return 150; }); });
for (var i=0; i<outputs.length; i++) {
var divElement = document.createElement("div");
divElement.setAttribute("id", "div_"+cnt);
divElement.setAttribute("class", "div_simple_");
divElement.style.width = "50px";
divElement.style.height = "50px";
divElement.style.border = "1px solid gray";
divElement.style.borderRadius = "2px";
document.getElementById("div0").appendChild(divElement);
plot1(identity, outputs[i], identity, "div_"+cnt);
cnt = cnt + 1;
}
}
function conv2d(inputs, kernels) {
var num_of_outputs = kernels.length;
var num_of_inputs = inputs.length;
var outputs = [];
for (var i=0; i<num_of_outputs; i++) {
var output_per_kernel = [];
for (var j=0; j<num_of_inputs; j++) {
var p = 0; // p=1 returns the same size array.
var s = 1;
var type = "convolution";
var new_output = convolution_way0(inputs[j], kernels[i], p, s, type);
if (output_per_kernel.length == new_output.length) {
output_per_kernel = add(new_output, output_per_kernel);
} else {
output_per_kernel = new_output;
}
} // end of for j
outputs.push(output_per_kernel);
} // end of for i
return outputs;
}
</script>
<script>
const add = (arr0, arr1) => {
function reshape(r, c, arrflat) {
var out = [];
for (var i=0; i<r; i++) {
var row = arrflat.slice(i*c, (i*c)+c);
out.push(row);
}
return out;
}
var r = arr0.length;
try { var c = arr0.at(0).length; } catch (err) { var c = 1; }
/*console.log("r,c: ", [r,c]);*/
if (arr0.flat(Infinity).length == arr1.flat(Infinity).length) {
/*Same length array element-wise multiplication.*/
arr1 = arr1.flat(Infinity);
var arrflat = arr0.flat(Infinity).map((v,i) => { return Number(Number(v)+Number(arr1.at(i))); });
var out = reshape(r, c, arrflat);
} else if (arr0.flat(Infinity).length == 1 || arr1.flat(Infinity).length == 1) {
/*Scalar and array multiplication.*/
if (arr0.flat(Infinity).length == 1) { var scalar = arr0.flat(Infinity); var arr = arr1.flat(Infinity); } else { var scalar = arr1.flat(Infinity); var arr = arr0.flat(Infinity); }
var arrflat = arr.map((v,i) => { return Number(Number(v)+Number(scalar)); });
var out = reshape(r, c, arrflat);
} else {
var out = "Arrays can not be added; both arrays must be the same size, or one array must be a scalar.";
}
return out;
}
</script>
<script>
function pad(arr, p) {
var [r, c] = [arr.length, arr.at(0).length];
var n = r + 2*p;
var arr0 = Array.from({length:n}, (v0,i0) => { return Array.from({length:n}, (v1,i1) => { return 0; }); });
return arr0.map((row,i) => { return (i<p || i>(r-1+p)) ? arr0.at(0) : row.map((v,j) => { return (j<p || j>(c-1+p)) ? 0 : arr[i-p][j-p]; }); });
}
function calculate_start_end_way0(n, w, s) {
// n = width or height of arr/image with padding.
// w = width or height of filter.
// s = stride.
var start = [];
var end = [];
var status = "run";
var i = 0;
while (status == "run") {
start.push(i*s);
end.push(start.at(-1) + w);
if (end.at(-1)+1 > n) { status = "norun"; } else { i = i + 1; }
}
return [start, end];
}
function convolution_way0(arr, filter, p, s, type) {
// p = pad around arr.
arr = pad(arr, p);
var r0 = arr.length;
var c0 = arr.at(0).length;
// console.log("padded [r0, c0]: ", [r0, c0]);
var r1 = filter.length;
var c1 = filter.at(0).length;
var filterFlat = filter.flat(Infinity);
var [start, end] = calculate_start_end_way0(arr.length, r1, s);
// console.log("start: ", start);
// console.log("end: ", end);
var out = [];
for (var ri=0; ri<start.length; ri++) {
var rout = [];
for (var ci=0; ci<start.length; ci++) {
var temp = arr.slice(start[ri], end[ri]).map((row,i1) => { return row.slice(start[ci], end[ci]); });
// console.log("temp: ", temp);
if (type == "convolution") {
var multSum = temp.flat(Infinity).map((v,i) => { return v*filterFlat.at(i); }).reduce((a,c) => a+c, 0);
// console.log("multSum: ", multSum);
} else if (type == "maxpooling") {
var multSum = temp.flat(Infinity).map((v,i) => { return v*filterFlat.at(i); }).sort().at(-1);
// console.log("multSum: ", multSum);
} else if (type == "averagepooling") {
var multSum = temp.flat(Infinity).map((v,i) => { return v*filterFlat.at(i); }).reduce((a,c) => a+c, 0)/temp.length;
// console.log("multSum: ", multSum);
}
rout.push(multSum);
}
out.push(rout);
}
return out;
}
function calculate_start_end_way1(n, w, p, s) {
// n = width or height of arr/image without padding.
// w = width or height of filter.
// s = stride.
// p = padding.
var start = [];
var end = [];
// document.getElementById("p0").innerHTML += "[n,p,w,s]: "+[n,p,w,s]+"<br>";
// var loops = Math.floor((n + 2*p - w)/s); // typically taught but sometimes not correct.
var loops = Math.floor(((n+p)-w)/s) + 1; // (n+p)-w is the number of rows one can start convolution from. Divid the start rows by stride to get the desired rows. Add one because the first_or_last row is not counted.
// document.getElementById("p0").innerHTML += "loops: "+loops+"<br>";
for (var i=0; i<loops; i++) {
start.push(i*s);
end.push(start.at(-1) + w);
}
return [start, end];
}
function convolution_way1(arr, filter, p, s, type) {
// p = pad around arr.
arr = pad(arr, p);
// document.getElementById("p0").innerHTML += "arr [w,h] after pad: "+[arr[0].length, arr.length]+"<br>";
// console.log("arr [w,h] after pad: ", [arr[0].length, arr.length]);
var filterFlat = filter.flat(Infinity);
// console.log("filterFlat: ", filterFlat);
var [start, end] = calculate_start_end_way1(arr.length, filter.length, p, s);
// console.log("start: ", start); // "start: " Array [0, 1, 2, 3, 4, 5, 6]
// document.getElementById("p0").innerHTML += "start: "+start+"<br>";
// console.log("end: ", end); // "end: " Array [3, 4, 5, 6, 7, 8, 9]
// document.getElementById("p0").innerHTML += "end: "+end+"<br>";
var out = [];
for (var ri=0; ri<start.length; ri++) {
var rout = [];
for (var ci=0; ci<start.length; ci++) {
var temp = arr.slice(start[ri], end[ri]).map((row,i1) => { return row.slice(start[ci], end[ci]); });
// console.log("temp: ", temp);
if (type == "convolution") {
var multSum = temp.flat(Infinity).map((v,i) => { return v*filterFlat.at(i); }).reduce((a,c) => a+c, 0);
// console.log("multSum: ", multSum);
} else if (type == "maxpooling") {
var multSum = temp.flat(Infinity).map((v,i) => { return v*filterFlat.at(i); }).sort().at(-1);
// console.log("multSum: ", multSum);
} else if (type == "averagepooling") {
var multSum = temp.flat(Infinity).map((v,i) => { return v*filterFlat.at(i); }).reduce((a,c) => a+c, 0)/temp.length;
// console.log("multSum: ", multSum);
}
rout.push(multSum);
}
out.push(rout);
}
return out;
}
</script>
<script>
function make_xy(htmlElement_width, htmlElement_height, width, height) {
var x = [];
var y = [];
for (var i=0; i<parseInt(htmlElement_width/width); i++) {
var x_row = [];
var y_row = [];
for (var j=0; j<parseInt(htmlElement_height/height); j++) {
x_row.push(j*width);
y_row.push(i*height);
}
x.push(x_row);
y.push(y_row);
}
return [x, y];
}
function plot1(r, g, b, str) {
var htmlElement_width = parseInt(document.getElementById(str).style.width.split("px")[0]);
var htmlElement_height = parseInt(document.getElementById(str).style.height.split("px")[0]);
var width = Math.floor(htmlElement_width/r.length);
var height = Math.floor(htmlElement_height/r.at(0).length);
var [x, y] = make_xy(htmlElement_width, htmlElement_height, width, height);
x = x.flat();
y = y.flat();
var r_flat = r.flat(Infinity);
var g_flat = g.flat(Infinity);
var b_flat = b.flat(Infinity);
var tot_html_string = "";
for (var i=0; i<x.length; i++) {
tot_html_string = tot_html_string + `<rect id="rect${i}" class="rect_simple" width="${width}" height="${height}" x="${x.at(i)}" y="${y.at(i)}" fill="rgb(${r_flat.at(i)} ${g_flat.at(i)} ${b_flat.at(i)})" stroke="gray"/>`;
}
var html_string = `<svg id="svg0" class="svg_simple" xmlns="http://www.w3.org/2000/svg" width="${htmlElement_width}" height="${htmlElement_height}" viewBox="0 0 ${htmlElement_width} ${htmlElement_height}">${tot_html_string}</svg>`;
var url_string = `url(https://medium.com/'data:image/svg+xml;utf8,${html_string}')`;
document.getElementById(str).style.backgroundImage = url_string;
}
</script>
<script>
topbar();
function topbar() {
document.getElementsByClassName("javascript_css")[0].style.position = "absolute";
document.getElementsByClassName("javascript_css")[0].style.width = "300px";
document.getElementsByClassName("javascript_css")[0].style.height = "50px";
document.getElementsByClassName("javascript_css")[0].style.border = "1px solid gray";
document.getElementsByClassName("javascript_css")[0].style.borderRadius = "2px";
document.getElementsByClassName("javascript_css")[0].style.fontSize = "12px";
document.getElementsByClassName("javascript_css")[0].innerHTML = "Conv2d. Click-on the input and click-off to view the input and kernel convolved with each other.";
functionName();
}
</script>
</body>
</html>
Results
inputs size (width/columns,height/rows,depth): 7,7,1
kernels: -1,-1,-1,-1,8,-1,-1,-1,-1,-1,-1,-1,-1,8,-1,-1,-1,-1
kernels size (width/columns,height/rows,depth): 3,3,2
outputs size (width/columns,height/rows,depth): 5,5,2
Get Practicing DatScy’s stories in your inbox
Join Medium for free to get updates from this writer.
Good Luck with your workflows and Happy Practicing! 👋








