model_def = 'example_caffe_mnist_model.prototxt'
model_weights = 'mnist.caffemodel'
net = caffe.Net(model_def, model_weights, caffe.TEST)

"""
Explanation: Loading the model
End of explanation
"""


net.layer_dict

conv_layer = net.layer_dict['conv2d_1']
conv_layer.type, conv_layer.blobs[0].data.shape

"""
Explanation: A Caffe net offers a layer dict that maps layer names to layer objects. These objects do not provide very much information though, but access to their weights and the type of the layer.
End of explanation
"""


for name, blob in net.blobs.items():
    print('{}: \t {}'.format(name, blob.data.shape))

"""
Explanation: Getting input and output shape.
The net provides a blobs dict. These blobs contain data, i.e. all the intermediary computation results and diff, i.e. the gradients.
End of explanation
"""


net.params

for name, param in net.params.items():
    print('{}:\t {} \t{}'.format(name, param[0].data.shape, param[1].data.shape))

"""
Explanation: Getting the weigths.
The net provides access to a param dict that contains the weights. The first entry in param corresponds to the weights, the second corresponds to the bias.
End of explanation
"""


for layer in net.layers:
    try:
        print (layer.type + '\t' + str(layer.blobs[0].data.shape), str(layer.blobs[1].data.shape))
    except:
        continue
    

weights = net.params['conv2d_1'][0].data
weights.shape

"""
Explanation: The weights are also accessible through the layer blobs.
End of explanation
"""


for i in range(32):
    plt.imshow(np.moveaxis(weights[i], 0, -1)[..., 0])
    plt.show()

"""
Explanation: For visualizing the weights the axis still have to be moved around.
End of explanation
"""


list(net.layer_dict['dropout_1'].blobs)

"""
Explanation: Layers that have no weights simply keep empty lists as their blob vector.
End of explanation
"""


def remove_inplace(model_def):
    protonet = caffe_pb2.NetParameter()
    with open(model_def, 'r') as fp:
        google.protobuf.text_format.Parse(str(fp.read()), protonet)
        
    replaced_tops = {}
    for layer in protonet.layer:
        # Check whehter bottoms were renamed.
        for i in range(len(layer.bottom)):
            if layer.bottom[i] in replaced_tops.keys():
                layer.bottom[i] = replaced_tops[layer.bottom[i]]
                
            
        if layer.bottom == layer.top:
            for i in range(len(layer.top)):
                # Retain the mapping from the old to the new name.
                new_top = layer.top[i] + '_' + layer.name
                replaced_tops[layer.top[i]] = new_top
                # Redefine layer.top
                layer.top[i] = new_top
            
    return protonet

model_def = 'example_caffe_mnist_model_deploy.prototxt'

protonet_no_inplace = remove_inplace(model_def)
protonet_no_inplace

model_def = 'example_caffe_network_no_inplace_deploy.prototxt'
model_weights = 'mnist.caffemodel'
net_no_inplace = caffe.Net(model_def, model_weights, caffe.TEST)

net_no_inplace.layer_dict

net_no_inplace.blobs

# Loading and preprocessing data.
data = mnist.load_data()[1][0]
# Normalize data.
data = data / data.max()
plt.imshow(data[0, :, :])
seven = data[0, :, :]
print(seven.shape)
seven = seven[np.newaxis, ...]
print(seven.shape)

"""
Explanation: Getting the activations and the net input.
For getting activations, first data has to be passed through the network. Then the activations can be read out from the blobs. If the activations are defined as in place operations, the net input will not be stored in any blob and can therefore not be recovered. This problem can be circumvented if the network definition is changed so that in place operations are avoided. This can also be done programatically as follows.
End of explanation
"""


net_no_inplace.blobs['data'].data[...] = seven
output = net_no_inplace.forward()
output['prob'][0].argmax()

activations = net_no_inplace.blobs['relu_1'].data

for i in range(32):
    plt.imshow(activations[0, i, :, :])
    plt.title('Feature map %d' % i)
    plt.show()

net_input = net_no_inplace.blobs['conv2d_1'].data

for i in range(32):
    plt.imshow(net_input[0, i, :, :])
    plt.title('Feature map %d' % i)
    plt.show()

"""
Explanation: Feeding the input and forwarding it.
End of explanation
"""


model_def = 'example_caffe_mnist_model.prototxt'
f = open(model_def, 'r')
protonet = caffe_pb2.NetParameter()
google.protobuf.text_format.Parse(str(f.read()), protonet)
f.close()
protonet

type(protonet)

"""
Explanation: Getting layer properties
From the layer object not more then type information is available. There the original .prototxt has to be parsed to access attributes such as kernel size.
End of explanation
"""


for i in range(0, len(protonet.layer)):
    if protonet.layer[i].type == 'Convolution':
        print('layer %s has kernel_size %d' 
              % (protonet.layer[i].name, 
                 protonet.layer[i].convolution_param.kernel_size[0]))
        lconv_proto = protonet.layer[i]

len(protonet.layer), len(net.layers)

"""
Explanation: Parsed messages for the layer can be found in message.layer list.
End of explanation
"""


