How do the transposed convolutional layer forward and backward passes work?
Before I present my problem, I want to apologize for those of you that feel this is more of a math post than a programming post. Neural networks are both mathematic and programming heavy, and I felt my problem was in the programming side. I have created a CNN from scratch in c++ (that works). For this reason, I feel as though the functions I use to create a convolution and a full convolution are correct. Programmatically below, I am going to show how I do the basic forward and backward of a CNN with a convolutional layer being the forward:
Matrix&lt;float&gt; cnn_forward(Matrix&lt;float&gt; weight, Matrix&lt;float&gt; prev){
    Matrix&lt;float&gt; output = prev.convolute(weight);
    return output;
}

And the backward pass (I am not using a bias or activation function in this case):
cnn_back cnn_backward(Matrix&lt;float&gt; a_prev, Matrix&lt;float&gt; dz, Matrix&lt;float&gt; kernel){
    Matrix&lt;float&gt; rotated = kernel.rotate_180();
    Matrix&lt;float&gt; dx = dz.convolute_full(rotated);
    Matrix&lt;float&gt; dw = a_prev.convolute(dz);
    cnn_back output;
    output.dw = std::move(dw);
    output.dx = std::move(dx);
    return output;  
}

Everything that I have seen online says that the transposed convolutional layer is just the reverse of the convolutional layer. So, I have tried implementing the following as the forward and backward passes of a transposed convolutional layer.
//forward
Matrix&lt;float&gt; fcn_forward(Matrix&lt;float&gt; weight, Matrix&lt;float&gt; prev){
    Matrix&lt;float&gt; output = prev.convolute_full(weight.rotate_180());
    return output;
} 

//backward
fcn_back fcn_backward(Matrix&lt;float&gt; a_prev, Matrix&lt;float&gt; dz, Matrix&lt;float&gt; kernel){
    Matrix&lt;float&gt; dx = dz.convolute(kernel);
    Matrix&lt;float&gt; dw = dz.convolute(a_prev);
    fcn_back output;
    output.dw = std::move(dw);
    output.dx = std::move(dx);
    return output;
}
//again, not using a bias or activation function

My goal is to basically implement torch.nn.ConvTranspose2d from pytorch with 2 dimensional matrices. I was hoping to parallel it to the basic convolution formula that I have above.
~EDIT~
This would be the translation into python using numpy arrays which is pretty much an exact replica of my c++ code.
def convolute(X, W, strides=(1,1)):
    new_row = (int)((X.shape[0] - W.shape[0])/strides[0] +1)
    new_col = (int)((X.shape[1] - W.shape[1])/strides[1] +1)
    out = np.zeros((new_row, new_col), dtype=float)
    x_last = 0
    y_last = 0
    for x in range(0, X.shape[0]-(W.shape[0] - 1), strides[0]):
        for y in range(0, X.shape[1]-(W.shape[1] - 1), strides[1]):
            amt = 0.0
            for i in range(0, W.shape[0]):
                for j in range(0, W.shape[1]):
                    amt += W[i][j] * X[x+i][y+j]
            out[x_last][y_last] = amt
            y_last += 1
        x_last += 1
        y_last = 0
    return out


def convolute_full(X, W, strides=(1, 1)):
    row_num = (X.shape[0] - 1) * strides[0] + W.shape[0]
    col_num = (X.shape[1] - 1) * strides[1] + W.shape[1]
    output = np.zeros([row_num, col_num])
    for i in range(0, X.shape[0]):
        i_prime = i * strides[0] 
        for j in range(0, X.shape[1]):
            j_prime = j * strides[1]
            for k_row in range(W.shape[0]):
                for k_col in range(W.shape[1]):
                    output[i_prime+k_row, j_prime+k_col] += W[k_row, k_col] * X[i, j]
    return output


def get_errors(predicted, label):
    return label - predicted

def fcn_forward(weight, prev):
    rotated = np.rot90(np.rot90(weight)) 
    output = convolute_full(prev, rotated)
    return output

def fcn_backward(a_prev, dz, kernel):
    dx = convolute(dz, kernel)
    dw = convolute(dz, a_prev)
    dx = np.clip(dx, 10, -10)
    return dx, dw


def forward(weights, X_init):
    values = []
    values.append(X_init)
    predicted = fcn_forward(weights[0], X_init)
    values.append(predicted)
    predicted = fcn_forward(weights[1], predicted)
    values.append(predicted)
    return values

def backward(weights, values, label, learningRate=0.001):
    dz = get_errors(values[-1], label)
    dx, dw = fcn_backward(values[-2], dz, weights[-1])
    weights[-1] = weights[-1] - learningRate*dw
    dz = dx
    dx, dw = fcn_backward(values[-3], dz, weights[-2])
    weights[-2] = weights[-2] - learningRate*dw
    return weights


def train_example():
    epoch = int(input(&quot;enter epoch: &quot;))
    #creating a random input
    inp = np.random.randn(10,10)
    #creating the weight matricies
    weights = [np.random.randn(3,3), np.random.randn(3,3)]
    #creating the wanted output
    label = np.random.randn(14,14)
    for i in range(0, epoch):
        values = forward(weights, inp)
        if(i == 0 or i == 1):
            errors = get_errors(values[-1], label)
            print(&quot;errors:&quot;)
            print(errors)
            print(&quot;error sum: &quot;, np.sum(errors))
        weights = backward(weights, values, label)
    print(&quot;current prediction:&quot;)
    print(values[-1])
    print(&quot;label: &quot;)
    print(label)
    errors = get_errors(values[-1], label)
    print(&quot;errors:&quot;)
    print(errors)
    print(&quot;error sum at end of training: &quot;, np.sum(errors))

Basically, this does not work. The weights are not corrected in the correct way. The errors only continue to get larger (the opposite of the wanted result). What is the correct way to forward and backward propagate a transposed convolutional layer?
EDIT
This is the answer for anyone wondering how it relates to my code above, due to @Bob's answer:
def convolute(X, W, strides=(1,1)):
    new_row = (int)((X.shape[0] - W.shape[0])/strides[0] +1)
    new_col = (int)((X.shape[1] - W.shape[1])/strides[1] +1)
    out = np.zeros((new_row, new_col), dtype=float)
    x_last = 0
    y_last = 0
    for x in range(0, X.shape[0]-(W.shape[0] - 1), strides[0]):
        for y in range(0, X.shape[1]-(W.shape[1] - 1), strides[1]):
            amt = 0.0
            for i in range(0, W.shape[0]):
                for j in range(0, W.shape[1]):
                    amt += W[i][j] * X[x+i][y+j]
            out[x_last][y_last] = amt
            y_last += 1
        x_last += 1
        y_last = 0
    return out


#this is the same result as scipy.signal.convolute2d
def convolute_full(X, W, strides=(1, 1)):
    row_num = (X.shape[0] - 1) * strides[0] + W.shape[0]
    col_num = (X.shape[1] - 1) * strides[1] + W.shape[1]
    output = np.zeros([row_num, col_num])
    for i in range(0, X.shape[0]):
        i_prime = i * strides[0] 
        for j in range(0, X.shape[1]):
            j_prime = j * strides[1]
            for k_row in range(W.shape[0]):
                for k_col in range(W.shape[1]):
                    output[i_prime+k_row, j_prime+k_col] += W[k_row, k_col] * X[i, j]
    return output

def convolute_full_backward(X, dZ, dW, strides=(1, 1)):
    for i in range(0, X.shape[0]):
        i_prime = i * strides[0] 
        for j in range(0, X.shape[1]):
            j_prime = j * strides[1]
            for k_row in range(dW.shape[0]):
                for k_col in range(dW.shape[1]):
                    dW[k_row, k_col] += dZ[i_prime+k_row, j_prime+k_col] * X[i, j]
    return dW


def get_errors(predicted, label):
    return label - predicted

def fcn_forward(W, X):
    rotated = np.rot90(np.rot90(W))
    output = convolute_full(X, rotated)
    return output

def fcn_backward(X, dZ, kernel):
    dw = np.zeros(kernel.shape)
    dw = convolute_full_backward(X, dZ, dw)
    dw = np.rot90(np.rot90(dw))
    dx = convolute(dZ, np.rot90(np.rot90(kernel)))
    np.clip(dx, 10, -10)
    return dx, dw


def forward(weights, X):
    values = []
    values.append(X)
    predicted = fcn_forward(weights[0], X)
    values.append(predicted)
    predicted = fcn_forward(weights[1], predicted)
    values.append(predicted)
    return values

def backward(weights, values, label, learningRate=0.001):
    dz = get_errors(values[-1], label)
    dx, dw = fcn_backward(values[-2], dz, weights[-1])
    weights[-1] = weights[-1] + learningRate*dw
    dz = dx
    dx, dw = fcn_backward(values[-3], dz, weights[-2])
    #new apply dw:
    weights[-2] = weights[-2] + learningRate*dw
    return weights
    

def train_example():
    epoch = int(input(&quot;please enter epoch: &quot;))
    inp = np.random.randn(10,10)
    weights = [np.random.randn(3,3), np.random.randn(3,3)]
    label = np.random.randn(14,14)
    for i in range(0, epoch):
        values = forward(weights, inp)
        errors = get_errors(values[-1], label)
        print(&quot;error sum at {} is: {}&quot;.format(i, np.sum(errors)))
        weights = backward(weights, values, label)

    errors = get_errors(values[-1], label)
    print(&quot;error sum at end of training: &quot;, np.sum(errors))


since your implementation has every scalar multiplication performed explicitly, the backward step can be done very clear. You keep all the loops unchanged, and when you see an update to your accumulator you compute the gradient for that.
import numpy as np
def convolute_full(X, W, strides=(1, 1)):
    row_num = (X.shape[0] - 1) * strides[0] + W.shape[0]
    col_num = (X.shape[1] - 1) * strides[1] + W.shape[1]
    output = np.zeros([row_num, col_num])
    for i in range(0, X.shape[0]):
        i_prime = i * strides[0] 
        for j in range(0, X.shape[1]):
            j_prime = j * strides[1]
            for k_row in range(W.shape[0]):
                for k_col in range(W.shape[1]):
                    output[i_prime+k_row, j_prime+k_col] += W[k_row, k_col] * X[i, j]
    return output

def convolute_full_backward(X, dZ, dW, strides=(1, 1)):
    row_num = (X.shape[0] - 1) * strides[0] + W.shape[0]
    col_num = (X.shape[1] - 1) * strides[1] + W.shape[1]
    output = np.zeros([row_num, col_num])
    for i in range(0, X.shape[0]):
        i_prime = i * strides[0] 
        for j in range(0, X.shape[1]):
            j_prime = j * strides[1]
            for k_row in range(W.shape[0]):
                for k_col in range(W.shape[1]):
                    # Only this line changed compard to forward pass
                    dW[k_row, k_col] += dZ[i_prime+k_row, j_prime+k_col] * X[i, j]


def fcn_forward(X, W):
    output = convolute_full(X, W[::-1,::-1])
    return output

def fcn_backward(X, dZ, kernel_shape):
    dW = np.zeros(kernel_shape)
    convolute_full_backward(X, dZ, dW[::-1,::-1])
    return dW

To validate I created a simple example with  a linear loss function
X = np.random.randn(20, 20)
W = np.random.randn(5, 5)
Z = fcn_forward(X, W)
# pick a random loss with known gradient
dZ = np.random.randn(*Z.shape)
F = np.sum(Z * dZ)

dW = fcn_backward(X, dZ, W.shape)
# random perturbation
W_ = W + np.random.randn(*W.shape)
# expected change to the loss function
dF = np.sum(dW * (W_ - W))
Z_ = fcn_forward(X, W_)
F_ = np.sum(Z_ * dZ)
print('Predicted loss change: %f' % dF)
print('Actual loss change: %f' % (F_ - F))

Run and see.
