singleConv = (input, kernel, stride=1, padding=0) => {
  // TODO: implement padding

  // Only support square input and kernel
  console.assert(input.length === input[0].length,
     'Conv input is not square');
  console.assert(kernel.length === kernel[0].length,
    'Conv kernel is not square');

  let stepSize = (input.length - kernel.length) / stride + 1;

  let result = init2DArray(stepSize, stepSize, 0);

  // Window sliding
  for (let r = 0; r < stepSize; r++) {
    for (let c = 0; c < stepSize; c++) {
      let curWindow = matrixSlice(input, r * stride, r * stride + kernel.length,
        c * stride, c * stride + kernel.length);
      let dot = matrixDot(curWindow, kernel);
      result[r][c] = dot;
    }
  }
  return result;
}
Compute convolutions of one kernel on one matrix (one slice of a tensor).
@param {[[number]]} input Input, square matrix
@param {[[number]]} kernel Kernel weights, square matrix
@param {int} stride Stride size
@param {int} padding Padding size