TensorFlow 实战(八)(2)https://developer.aliyun.com/article/1522968
附录 B:计算机视觉
B.1 Grad-CAM:解释计算机视觉模型
Grad-CAM(代表梯度类激活映射)在第七章介绍过,是由 Ramprasaath R. Selvaraju 等人在“Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization”(arxiv.org/pdf/1610.02391.pdf
)中介绍的一种深度神经网络模型解释技术。深度网络以其难以解释的特性而臭名昭著,因此被称为黑盒子。因此,我们必须进行一些分析,并确保模型按预期工作。
让我们在第七章实现的模型上刷新一下记忆:一个名为 InceptionResNet v2 的预训练模型,其顶部是一个具有 200 个节点的 softmax 分类器(即我们的图像分类数据集 TinyImageNet 中的类别数量相同;请参阅下面的列表)。
清单 B.1 我们在第七章定义的 InceptionResNet v2 模型
import tensorflow as tf import tensorflow.keras.backend as K from tensorflow.keras.applications import InceptionResNetV2 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Input, Dense, Dropout K.clear_session() def get_inception_resnet_v2_pretrained(): model = Sequential([ ❶ Input(shape=(224,224,3)), ❷ InceptionResNetV2(include_top=False, pooling='avg'), ❸ Dropout(0.4), ❹ Dense(200, activation='softmax') ❺ ]) loss = tf.keras.losses.CategoricalCrossentropy() adam = tf.keras.optimizers.Adam(learning_rate=0.0001) model.compile(loss=loss, optimizer=adam, metrics=['accuracy']) return model model = get_inception_resnet_v2_pretrained() model.summary()
❶ 使用 Sequential API 定义一个模型。
❷ 定义一个输入层来接收大小为 224 × 224 × 3 的图像批次。
❸ 下载并使用预训练的 InceptionResNetV2 模型(不包括内置分类器)。
❹ 添加一个 dropout 层。
❺ 添加一个具有 200 个节点的新分类器层。
如果你打印此模型的摘要,你将得到以下输出:
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= inception_resnet_v2 (Model) (None, 1536) 54336736 _________________________________________________________________ dropout (Dropout) (None, 1536) 0 _________________________________________________________________ dense (Dense) (None, 200) 307400 ================================================================= Total params: 54,644,136 Trainable params: 54,583,592 Non-trainable params: 60,544 _________________________________________________________________
如您所见,InceptionResNet v2 模型被视为我们模型中的单个层。换句话说,它是一个嵌套模型,其中外层模型(sequential)有一个内层模型(inception_resnet_v2)。但是我们需要更多的透明度,因为我们将要访问 inception_resnet_v2 模型内的特定层,以实现 Grad-CAM。因此,我们将“解开”或移除此嵌套,并且只描述模型的层。我们可以使用以下代码实现这一点:
K.clear_session() model = load_model(os.path.join('models','inception_resnet_v2.h5')) def unwrap_model(model): inception = model.get_layer('inception_resnet_v2') inp = inception.input out = model.get_layer('dropout')(inception.output) out = model.get_layer('dense')(out) return Model(inp, out) unwrapped_model = unwrap_model(model) unwrapped_model.summary()
实质上我们正在做的是取现有模型并略微更改其输入。在取得现有模型后,我们将输入更改为 inception_resnet_v2 模型的输入层。然后,我们定义一个新模型(本质上使用与旧模型相同的参数)。然后你将看到以下输出。没有更多的模型在模型内部:
Model: "model" ___________________________________________________________________________ ➥ ________________ Layer (type) Output Shape Param # Connected ➥ to =========================================================================== ➥ ================ input_2 (InputLayer) [(None, None, None, 0 ___________________________________________________________________________ ➥ ________________ conv2d (Conv2D) (None, None, None, 3 864 ➥ input_2[0][0] ___________________________________________________________________________ ➥ ________________ batch_normalization (BatchNorma (None, None, None, 3 96 ➥ conv2d[0][0] ___________________________________________________________________________ ➥ ________________ activation (Activation) (None, None, None, 3 0 ➥ batch_normalization[0][0] ___________________________________________________________________________ ➥ ________________ ... ___________________________________________________________________________ ➥ ________________ conv_7b (Conv2D) (None, None, None, 1 3194880 ➥ block8_10[0][0] ___________________________________________________________________________ ➥ ________________ conv_7b_bn (BatchNormalization) (None, None, None, 1 4608 ➥ conv_7b[0][0] ___________________________________________________________________________ ➥ ________________ conv_7b_ac (Activation) (None, None, None, 1 0 ➥ conv_7b_bn[0][0] ___________________________________________________________________________ ➥ ________________ global_average_pooling2d (Globa (None, 1536) 0 ➥ conv_7b_ac[0][0] ___________________________________________________________________________ ➥ ________________ dropout (Dropout) (None, 1536) 0 ➥ global_average_pooling2d[0][0] ___________________________________________________________________________ ➥ ________________ dense (Dense) (None, 200) 307400 ➥ dropout[1][0] =========================================================================== ➥ ================ Total params: 54,644,136 Trainable params: 54,583,592 Non-trainable params: 60,544 ___________________________________________________________________________ ➥ ________________
接下来,我们将进行一次更改:向我们的模型引入一个新输出。请记住,我们使用功能 API 来定义我们的模型。这意味着我们可以在我们的模型中定义多个输出。我们需要的输出是 inception_resnet_v2 模型中最后一个卷积层产生的特征图。这是 Grad-CAM 计算的核心部分。您可以通过查看解开模型的模型摘要来获得最后一个卷积层的层名称:
last_conv_layer = 'conv_7b' # This is the name of the last conv layer of the model grad_model = Model( inputs=unwrapped_model.inputs, outputs=[ unwrapped_model.get_layer(last_conv_layer).output, unwrapped_model.output ] )
有了我们的模型准备好后,让我们转向数据。我们将使用验证数据集来检查我们的模型。特别地,我们将编写一个函数(见清单 B.2)来接收以下内容:
- image_path(str)- 数据集中图像的路径。
- val_df(pd.DataFrame)—一个包含从图像名称到 wnid(即 WordNet ID)的映射的 pandas 数据框。请记住,wnid 是用于识别特定对象类的特殊编码。
- class_indices(dict)—一个 wnid(字符串)到类别(0-199 之间的整数)的映射。这保留了关于哪个 wnid 在模型的最终输出层中由哪个索引表示的信息。
- words(pd.DataFrame)—一个包含从 wnid 到类别的可读描述的映射的 pandas 数据框。
清单 B.2 检索转换后的图像、类别索引和人类可读标签
img_path = 'data/tiny-imagenet-200/val/images/val_434.JPEG' val_df = pd.read_csv( ❶ os.path.join('data','tiny-imagenet-200', 'val', 'val_annotations.txt'), sep='\t', index_col=0, header=None ) with open(os.path.join('data','class_indices'),'rb') as f: ❷ class_indices = pickle.load(f) words = pd.read_csv( ❸ os.path.join('data','tiny-imagenet-200', 'words.txt'), sep='\t', index_col=0, header=None ) def get_image_class_label(img_path, val_df, class_indices, words): """ Returns the normalized input, class (int) and the label name for a given image""" img = np.expand_dims( ❹ np.asarray( Image.open(img_path).resize((224,224) ❺ ) img /= 127.5 ❻ img -= 1 ❻ if img.ndim == 3: img = np.repeat(np.expand_dims(img, axis=-1), 3, axis=-1) ❼ _, img_name = os.path.split(img_path) wnid = val_df.loc[img_name,1] ❽ cls = class_indices[wnid] ❾ label = words.loc[wnid, 1] ❿ return img, cls, label # Test the function with a test image img, cls, label = get_image_class_label(img_path, val_df, class_indices, words)⓫
❶ 读取 val_annotations.txt。这将创建一个数据框,其中包含从图像文件名到 wnid(即 WordNet ID)的映射。
❷ 加载将 wnid 映射到类索引(整数)的类索引。
❸ 这将创建一个数据框,其中包含从 wnid 到类描述的映射。
❹ 加载由文件路径给出的图像。首先,我们添加一个额外的维度来表示批次维度。
❺ 将图像调整大小为 224×224 大小的图像。
❻ 将图像像素值调整到[-1, 1]的范围内。
❼ 如果图像是灰度的,则在通道维度上将图像重复三次,以与 RGB 图像具有相同的格式。
❽ 获取图像的 wnid。
❾ 获取图像的类别索引。
❿ 获取类的字符串标签。
⓫ 对一个示例图像运行该函数。
TensorFlow 实战(八)(4)https://developer.aliyun.com/article/1522971