How does finding the error work in the back propogation of a CNN when 4D tensors are used? I am currently creating a CNN from scratch in C++ only using vectors, and Matrix classes I have created. Mainly doing this as an exercise so that I can make sure I fully understand the process. I am currently able to do the forward pass, along with the backwards pass as it pertains to dw and db. dw being the gradient of the weight tensor, and db being the gradient of the bias tensor. However, I am having an issue finding dx which is the error as it pertains to the next layer. I will be calling it dx for this post. Currently when I need to find dx when only dealing with 2 dimensional matricies, it works perfectly.
The code for finding dx would be like this (for 2D matricies):
Matrix<float> rotated = kernel.rotate_180();
Matrix<float> dx = dz.convolute_full(rotated);

and dw would be (for 2D matricies):
Matrix<float> dw = a_prev.convolute(dz);
Which works.
When working with four dimensional tensors, this is my code for finding dw:
//(a_prev and dz both have a layer of 1)
std::vector<Matrix> a_prevs = a_prev.at(0);
std::vector<Matrix> dzs = dz.at(0);
std::vector<std::vector<Matrix>> dw;
for(uint32_t i = 0; i < dzs.size(); i++){
   std::vector<Matrix> part;
   for(uint32_t j = 0; j < a_prevs.size(); j++){
      part.push_back(a_prevs.at(j).convolute(dzs.at(i)));
   }
   dw.push_back(part);
}

And this entirely works for getting dw when using multi-dimensional tensors. However, when getting dx it doesn't work as smoothly. This is what I would think the code for finding dx would be:
//in this case, dz has the dimensions (1,4,6,6) and the kernel corresponding to this layer with dimensions (4,4,3,3)
//after the kernel is rotated, the rotated should have the same dimensions as the kernel which are (4,4,3,3)

std::vector<Matrix> dzs = dz.at(0);
std::vector<std::vector<Matrix>> rotated = rotate_180(kernel);
std::vector<std::vector<Matrix>> combos;
for(uint32_t i = 0; i < dzs.size(); i++){
   std::vector<Matrix> combo;
   for(uint32_t j = 0; j < rotated.at(i).size(); j++){
      combo.push_back(dzs.at(i).convolute_full(rotated.at(i).at(j)));
   }
   combos.push_back(combo);
}
std::vector<std::vector<Matrix>> dx = {combos.at(0)};
for(uint32_t i = 1; i < combos.size(); i++){
   dx = add(dx, combos.at(i));
}


However, when I use this as dx, the errors only become larger. I have already confirmed that dw works for four dimensional tensors. I have gotten the following formulas for when dw and dx are 4 dimensional tensors from this website.
For the finding of dw I used this formula:
∂/∂(,)=(−1)⊗(,),=1,2,⋯,()
For the finding of dx I used this formula:
(−1)=∑=1()[(,)⊗180((,))
On the website linked above, the formula includes a hadamard product of what was then dz sent through the derivative of the activation function. I did not include this in my code. I am trying to implement the back propgation of dx without the ativation function, purely for self learning purposes.

Basically, the code posted above is not incorrect, except for the fact it is not memory efficient. However, at the end a function similar to numpy.clip needs to be implemented to dx. So the code would be:
std::vector<Matrix> dzs = dz.at(0);
std::vector<std::vector<Matrix>> rotated = rotate_180(kernel);
std::vector<std::vector<Matrix>> combos;
for(uint32_t i = 0; i < dzs.size(); i++){
   std::vector<Matrix> combo;
   for(uint32_t j = 0; j < rotated.at(i).size(); j++){
      combo.push_back(dzs.at(i).convolute_full(rotated.at(i).at(j)));
   }
   combos.push_back(combo);
}
std::vector<std::vector<Matrix>> dx = {combos.at(0)};
for(uint32_t i = 1; i < combos.size(); i++){
   dx = add(dx, combos.at(i));
}

dx = clip(dx, CLIP_MAX, CLIP_MIN);

Notice the only difference is the last line. For those that don't know, the function of numpy.clip is to make all values above CLIP_MAX equal to CLIP_MAX and all values below CLIP_MIN equal to CLIP_MIN.
