Target localization with image classification

Source: Deep Learning on Medium

Target localization with image classification

HELL YEAH! We’re back!!

Depois de muito trabalho, graças a DEUS, estamos de volta, e nesse artigo vamos abordar uma coisa bem bacana: visualização das camadas de uma rede neural convolucional. WTF isso tem a ver com o título do artigo? Vamos por partes.

Image Classification VS Object Detection

Qual a diferença básica entre um e outro meu considerado?

Imagine que você está assistindo um esporte na TV. Daí eu te pergunto “qual esporte você tá assistindo meu parceiro?” e você me responde “futebol meu chegado!”. Image classification, pois você sabe as features do gramado, como é o formato da bola, entre outras características desse esporte, entonces, essa cena (imagem) se caracteriza como o esporte futebol no meio de diversos outros esportes. Agora eu te peço para apontar onde está a bola enquanto o jogo está rolando, e você vai indicando com o dedo exatamente onde a bola se encontra em cada momento. Object detection, pois você sabe as features da bola, e consegue me dizer exatamente onde ela se encontra naquela cena (imagem), inclusive, se outra bola aparecer no meio.

Simples, entretanto, você pode achar bem chato treinar uma rede neural para identificação de objetos no começo da sua incrível jornada pela IA. Então compadre, eu te digo que para problemas mais simples, existe uma solução se você, além de classificar, deseja identificar o que e/ou onde a rede “enxergou” na imagem dada o que levou ela a classificar aquela cena (lembre-se então que vamos fazer Image Classification). Isso pode ser feito através do GAP — Global Average Pooling.

Global Average Pooling

Não vamos entrar em detalhes como essa técnica funciona, mas vou deixar aqui o paper original que apresentou esse layer pela primeira vez. Sendo suscinto, ele veio para evitar overfiting nas redes, substituindo os layers fully connected finais (antes do layer de softmax) pelo GAP. Obs.: em alguns casos, como na ResNet-50, o GAP é seguido por um outro fully connected, e depois vem o softmax. A rede que vamos usar aqui usa somente o GAP + softmax.

O que acontece é que esse pessoal aqui mostrou que CNNs que possuem GAP e são treinadas para Image Classification possuem a capacidade de informar localmente onde está seu objeto alvo. Interessante também é que, através da análise do CAM — Class Activation Map, nós podemos fazer o debugging da rede. Lembre que uma imagem é interpretável para nós, humanos. Se nós entramos com uma imagem de um cachorro e a rede classifica a imagem como realmente sendo de um cachorro, mas o mapa de ativação final foi uma coisa completamente fora do cachorro na imagem, alguma coisa de errado não está certo! Pense nisso também como aquele print que você usa pra ver se seu código está certo. Obs.: eu quero mostrar como usar essas técnicas, e não explicar a teoria, então quem quiser saber de verdade como isso funciona debaixo do capô, leia os artigos que estão sendo colocados aqui, que se eu tentar explicar pode ser que eu confunda ainda mais!

Let’s do this!

Usei transfer learning com as redes VGG16 e MobileNet, e o dataset desse pessoal (detectar fumaça) para treinar os modelos— não esqueçam de pré-processar a imagem corretamente.

Vamos lá. Vou considerar que já treinamos a rede para classificação das duas classes do dataset acima: com fumaça e sem. Vou apresentar a construção do modelo só para ficar um pouco mais claro. Importamos o que precisamos e montamos o modelo. Vamos ver um exemplo utilizando a VGG16.

from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Input
from tensorflow.keras.models import Model
shape = (224, 224, 3)tensor_in = Input(shape=shape)
vgg = VGG16(input_tensor=tensor_in, input_shape=shape, weights='imagenet', include_top=False)
m = vgg.output
m = GlobalAveragePooling2D()(m)
m = Dense(2, activation='softmax')(m)
model = Model(inputs=[tensor_in], outputs=[m, vgg.get_layer("block5_conv3").output])
model.load_weights('path_to_pretrained_weights.hf5')

Então, como eu comentei, nós utilizamos a rede VGG16, porém somente as camadas de convolução, e então inserimos o GAP e a camada de softmax com as duas classes que queremos classificar. Repare que nós vamos colocar também como saída da rede, além da classificação, a saída da última camada de convolução da VGG16. No caso do uso da MobileNet seria get_layer(“conv_pw_13”). Para saber qual camada você quer usar, utilize modelo_que_está_utilizando.summary() e escolha a última camada de convolução. Existem outros meios de se fazer isso, mas eu particularmente prefiro utilizar get_layer pois assim você pega pelo nome do layer, e não por um índice de uma lista.

gap_filters = model.layers[-2].output_shape[-1]

Aqui nós pegamos o tamanho do layer GAP. Isso vai servir logo logo. Repare que eu utilizei uma outra maneira, e não o get_layer. Desse jeito você tem que saber onde o layer está posicionado no modelo.

from tensorflow.keras.applications.vgg16 import preprocess_inputimage = 'some_target_image'
target = cv.resize(image, (224, 224), interpolation=cv.INTER_CUBIC)
target = preprocess_input(target)
target = np.expand_dims(target, 0).astype(np.float32)
pred, convolution_output = model.predict(target)
convolution_output = convolution_output.squeeze()
pred_class = np.argmax(pred)
final_weights = model.layers[-1].get_weights()[0]
class_weights = final_weights[:, pred_class]

Deixei em blocos separados as operações até pegarmos os pesos referentes à classe que o modelo previu. Agora vem a parte bacana e que nos fornece o mapa de ativação da classe.

n = convolution_output.shape[0]*convolution_output.shape[1]
activation_map = np.dot(convolution_output.reshape([n, gap_filters]), class_weights)
activation_map = activation_map.reshape(convolution_output.shape[:2])image_shape = image.shape[:2][::-1]
activation_map = cv.resize(activation_map, image_shape, interpolation=cv.INTER_CUBIC)
activation_map = cv.normalize(activation_map, None, 0, 255, cv.NORM_MINMAX).astype(np.uint8) # just for OpenCV
activation_maps = cv.applyColorMap(activation_maps, cv.COLORMAP_JET)
show = np.uint8(image * 0.7 + activation_maps * 0.3)

Pronto! O segredo é o produto escalar feito pelo np.dot. Depois disso é só fazer as operações para juntar a imagem original com o mapa de ativação resultante!! Aplicanto tudo isso em imagens que coletei aleatoriamente no Google temos o resultado: