PyTorchのフック関数(register_hook、register_forward_hook、register_backward_hook、register_forward_pre_hook)

1.フック機能

1.1 フック関数の概念

フック機能の仕組み: 本体は変更せず、ペンダント、フック→フックなどの追加機能を実装します。

では、なぜフック関数メカニズムがあるのでしょうか?これは、PyTorch 動的グラフ実行メカニズムに関連しています。動的グラフ実行メカニズムでは、操作が完了すると、非リーフ ノードの特徴マップや勾配などのいくつかの中間変数が解放されます。ただし、これらの中間変数に引き続き注目したい場合は、フック関数を使用してメイン コードで中間変数を抽出できます。メインコードは主にモデルの順伝播と逆伝播です。

簡単に言うと、フック関数は本体を変更するのではなく、追加の機能を実装します。 PyTorch に相当するもので、本体は forwardbackward で、追加機能は次のようなモデルの変数を操作することです。

  1. 特徴マップを「抽出」する
  2. 非リーフテンソルの勾配を「抽出」する
  3. テンソル勾配を変更する

フックが非リーフ テンソルの勾配を抽出する方法を示す例を示します。

import torch


# 定义钩子操作
def grad_hook(grad):
    y_grad.append(grad)
    
# 创建一个list来保存钩子获取的梯度
y_grad = list()

# 创建输入变量
x = torch.tensor([[1., 2.], [3., 4.]], requires_grad=True)  # requires_grad=True表明该节点为叶子节点
y = x + 1  # 没有明确requires_grad=True,所以是非叶子节点

# 为非叶子节点y注册钩子
y.register_hook(grad_hook)  # 这是传入的是函数而非函数的调用

# y.retain_grad()  # 如果想要将 y 设置为叶子节点,可以设置 y.retain_grad()

# 计算z节点(z 也是一个非叶子节点,因为它是通过对非叶子节点 y 进行操作而得到的)
z = torch.mean(y * y)

# 反向传播
z.backward()

print(f"y.type: {
      
      type(y)}")
print(f"y.grad: {
      
      y.grad}")
print(f"y_grad: {
      
      y_grad}")

結果は次のとおりです。

y.type: <class 'torch.Tensor'>
y.grad: None
y_grad: [tensor([[1.0000, 1.5000],
        [2.0000, 2.5000]])]

y. grad の値が None であることがわかります。これは、y が非リーフ ノードであるためです。 tensor、in z. backward() が完了すると、y の勾配はメモリを節約するために解放されますが、y の勾配は torch のクラス メソッド register_hook を通じて抽出できます。テンソル。


ここで、PyTorch は警告を報告する可能性があります。

/root/anaconda3/envs/wsss/lib/python3.9/site-packages/torch/_tensor.py:1083: 
UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its .grad attribute won't be populated during autograd.backward(). 
If you indeed want the .grad field to be populated for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. 
If you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor instead. 
See github.com/pytorch/pytorch/pull/30531 for more informations. (Triggered internally at  aten/src/ATen/core/TensorBody.h:482.)

翻訳するとこうなります。

/root/anaconda3/envs/wsss/lib/python3.9/site-packages/torch/_tensor.py:1083: UserWarning: 
正在访问不是叶张量的张量的 .grad 属性。 在 autograd.backward() 期间不会填充其 .grad 属性。 
如果我们确实希望为非叶张量填充 .grad 字段,请在非叶张量上使用 .retain_grad() 。 
如果我们错误地访问了非叶张量,请确保我们改为访问叶张量。 
有关更多信息,请参阅 github.com/pytorch/pytorch/pull/30531。 (在 aten/src/ATen/core/TensorBody.h:482 内部触发。)

この警告は通常、リーフ ノードではないテンソルの勾配情報にアクセスしようとしたときに表示されます。PyTorch は、勾配情報は非リーフ ノードでは利用できないことを通知します。非リーフ ノードで勾配情報を使用する必要がある場合は、 .retain_grad() メソッドを使用して勾配情報の記録を有効にすることができます。それ以外の場合は、勾配情報への通常のアクセスを許可するために、操作がリーフ ノードで実行されていることを確認してください。


Python で特定の警告を無視するには、warnings モジュールを使用できます。この場合、次の方法で PyTorch のユーザー警告を無視できます。

import warnings

# 忽略特定的警告
warnings.filterwarnings("ignore", category=UserWarning, module="torch")

上記のコードは、 モジュールから UserWarning カテゴリの警告をフィルタリングして、表示されないようにします。警告を無視することはグローバルな操作であるため、Python 環境全体に影響を与える可能性があることに注意してください。重要な警告情報が隠されないよう、この方法を使用するときは慎重に選択してください。 torch

1.2 PyTorchが提供するフック関数

  1. torch.Tensor.register_hook (Python method, in torch.Tensor)
  2. torch.nn.Module.register_forward_hook (Python method, in torch.nn)
  3. torch.nn.Module.register_backward_hook (Python method, in torch.nn)
  4. torch.nn.Module.register_forward_pre_hook (Python method, in torch.nn)

これら 4 つのフックのうち 1 つは tensor に適用され、他の 3 つは nn.Module に適用されます。

1.2.1 Tensor.register_hook

  • 関数: 逆伝播フック関数を登録します。 Tensor はバックプロパゲーションのみを実行するため、葉ノードでない場合は独自の勾配が解放されます。したがって、このフック関数は Tensor 用に特別に設計されています。

    Tensor クラスの register_hook メソッドは、PyTorch にグラデーション フックを登録するために使用されます。勾配フックは、テンソルの勾配計算中に勾配情報のキャプチャ、変更、記録などのカスタム操作を実行できるようにするコールバック関数です。以下は、register_hook メソッドの役割、使用法、戻り値の詳細な説明です。

  • 语法

    def hook(grad):
        ...
    
    tensor.register_hook(hook)
    
  • 作用

    • register_hookこのメソッドの主な目的は、バックプロパゲーション中に勾配を操作したり情報を記録したりするために、ユーザーがテンソルの勾配計算にカスタム関数を登録できるようにすることです。

    • これは、カスタム グラデーション処理、グラデーション クリッピング、グラデーション情報の視覚化、グラデーションの変更などのタスクに役立ちます。

  • 返回值

    • register_hook メソッドの戻り値は、グラデーション フックをキャンセルするために使用できるフック ハンドルです。 remove() メソッドを呼び出すことで、登録された勾配フックをいつでもキャンセルして、メモリ リークを回避できます。
  • アプリケーション シナリオの例: フック関数では、勾配 grad をインプレースで実行して、テンソルの grad 値を変更できます。これは素晴らしい機能です。たとえば、浅いレイヤーの勾配がなくなったときに、浅いレイヤーの勾配を特定の倍数で乗算して勾配を増加させることができます。また、勾配を切り詰めて勾配を特定の間隔に制限することもできます勾配が大きすぎる場合、重みパラメータが変更されます。

    • 例 1: 中間変数 y の勾配を取得する
    • 例 2: フック関数を使用して変数 x の勾配を 2 倍に拡張します

ここに画像の説明を挿入します

"""例 1:获取中间变量 a 的梯度"""
import torch
import warnings


# 忽略特定的警告
warnings.filterwarnings("ignore", category=UserWarning, module="torch")


# 自定义hook操作: 梯度处理或记录操作
def grad_hook(grad):
    a_grad.append(grad)


if __name__ == "__main__":
    w = torch.tensor([1.], requires_grad=True)  # 定义叶子节点
    x = torch.tensor([2.], requires_grad=True)  # 定义叶子节点
    a = torch.add(w, x)  # 非叶子节点
    b = torch.add(w, 1)  # 非叶子节点
    y = torch.mul(a, b)  # 非叶子节点

    # 存放梯度
    a_grad = []
    
    # 注册梯度钩子
    handle = a.register_hook(grad_hook)
    
    # 反向传播
    y.backward()

    # 查看梯度
    print(f"w.grad: {
      
      w.grad}")  # tensor([5.])
    print(f"x.grad: {
      
      x.grad}")  # tensor([2.])
    print(f"a.grad: {
      
      a.grad}")  # None
    print(f"b.grad: {
      
      b.grad}")  # None
    print(f"y.grad: {
      
      y.grad}")  # None
    print(f"a_grad: {
      
      a_grad}")  # [tensor([2.])]
    print(f"a_grad[0]: {
      
      a_grad[0]}")  # tensor([2.])
    
    # 取消钩子,避免内存泄漏
    handle.remove()
w.grad: tensor([5.])
x.grad: tensor([2.])
a.grad: None
b.grad: None
y.grad: None
a_grad: [tensor([2.])]
a_grad[0]: tensor([2.])

上記の例では、grad_hook 関数は tensor テンソルに登録され、バックプロパゲーション中にトリガーされ、その勾配を に保存します。 a_grad リストを作成し、その勾配情報を保持します。

"""例 2:利用 hook 函数将变量 x 的梯度扩大 2 倍"""
import torch
import warnings


# 忽略特定的警告
warnings.filterwarnings("ignore", category=UserWarning, module="torch")

# 定义钩子操作
def grad_hook(grad):
    grad *= 2
    return grad


# 创建输入变量
x = torch.tensor([2., 2., 2., 2.], requires_grad=True)  # requires_grad=True表明该节点为叶子节点
y = torch.pow(x, 2)  # 没有明确requires_grad=True,所以是非叶子节点
z = torch.mean(y)  # 对非叶子节点 y 进行操作,所以 z 也不是叶子节点

# 为非叶子节点 y 注册钩子, 返回值为 Handler
handler = x.register_hook(grad_hook)

# 反向传播
z.backward()

print(f"x.grad: {
      
      x.grad}")

# 取消梯度钩子
handler.remove()
x.grad: tensor([2., 2., 2., 2.]

x の元の勾配は tensor([1., 1., 1., 1. ]) です。grad_hook 操作後の勾配は tensor([2., 2., 2., 2. ]) です。

要約すると、register_hook メソッドを使用すると、PyTorch の勾配処理ロジックをカスタマイズして、勾配計算に追加の制御と機能を追加できます。

1.2.2 nn.Module.register_forward_hook

nn.Module.register_forward_hook は、ニューラル ネットワーク モジュール (nn.Module) の順伝播中にコールバック関数 (フック) を登録するために使用される PyTorch のメソッドです。これにより、カスタム操作やログ情報のモジュールの入出力をキャプチャできるようになります。

  • 関数

    1. モジュールの入力と出力を監視する: 順伝播フックを登録することで、ニューラル ネットワーク モジュールの入力と出力をキャプチャできます。これは、モジュールがデータを処理する方法を理解し、中間状態を監視するのに役立ちます。

    2. 中間状態を記録する: 順伝播中のモジュールの中間状態 (隠れ層の出力など) をキャプチャできます。これは、中間機能の視覚化、モデルのデバッグ、およびモデルの解釈可能性を確認するのに非常に役立ちます。

    3. カスタム操作: 順伝播フックでは、モジュールの出力の変更、ノイズの追加、その他のカスタム処理の実行などのカスタム操作を実行できます。

    4. 特定のタスクのアプリケーション: 一部のタスクでは、前方伝播フックを使用して、アクティベーション関数の出力に対して特定の処理を実行するなど、特定の関数を実装できます。機能は他のモジュールに渡されます。

  • 语法

    def hook(module, input, output) -> None:
        ...
    
    model/layer.register_forward_hook(hook)
    
    • module:現在のネットワーク層

    • input: 現在のネットワーク層入力データ

    • output: 現在のネットワーク層出力データ

    入力と出力は変更できないことに注意してください

  • アプリケーション シナリオの例: 特徴マップの抽出に使用されます
    ネットワークは畳み込み層で構成されていると仮定しますconv1プーリング層 pool1 は次のように構成されます。入力 1 つ 4 × 4 4\times4 4×4 の画像は、forward_hook を取得するために使用されるようになりました。 4> 機能マップの概略図は次のとおりです。moduleconv1

    ここに画像の説明を挿入します

    import torch
    import torch.nn as nn
    import warnings
    
    
    # 忽略特定的警告
    warnings.filterwarnings("ignore", category=UserWarning, module="torch")
    
    
    class CustomModel(nn.Module):
        def __init__(self):
            super(CustomModel, self).__init__()
            self.conv1 = nn.Conv2d(in_channels=1, out_channels=2, kernel_size=3)
            self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
    
        def forward(self, x):
            x = self.conv1(x)
            x = self.pool1(x)
            return x
    
    
    def forward_hook(module, input, output):
        outputs_fmap_list.append(output)
        inputs_fmap_list.append(input)
    
    
    if __name__ == "__main__":
        # 初始化网络
        model = CustomModel()
        model.conv1.weight[0].data.fill_(1)
        model.conv1.weight[1].data.fill_(2)
        model.conv1.bias.data.zero_()
    
        # 定义保存输入、输出feature maps的list
        outputs_fmap_list = list()
        inputs_fmap_list = list()
    
        # 注册hook
        model.conv1.register_forward_hook(forward_hook)
    
        # 模型前向推理
        dummy_img = torch.ones((1, 1, 4, 4))   # batch size * channel * H * W
        output = model(dummy_img)
    
        # 观察
        print(f"output shape: \n\t{
            
            output.shape}")
        print(f"output value: \n\t{
            
            output}")
        print("---" * 30)
        print(f"feature maps shape: \n\t{
            
            outputs_fmap_list[0].shape}")
        print(f"output value: \n\t{
            
            outputs_fmap_list[0]}")
        print("---" * 30)
        print(f"input shape: \n\t{
            
            inputs_fmap_list[0][0].shape}")
        print(f"input value: \n\t{
            
            inputs_fmap_list[0]}")
    
    output shape: 
            torch.Size([1, 2, 1, 1])
    output value: 
            tensor([[[[ 9.]],
    
             [[18.]]]], grad_fn=<MaxPool2DWithIndicesBackward0>)
    ------------------------------------------------------------------------------------------
    feature maps shape: 
            torch.Size([1, 2, 2, 2])
    output value: 
            tensor([[[[ 9.,  9.],
                      [ 9.,  9.]],
                      
                     [[18., 18.],
                      [18., 18.]]]], grad_fn=<ConvolutionBackward0>)
    ------------------------------------------------------------------------------------------
    input shape: 
            torch.Size([1, 1, 4, 4])
    input value: 
            (tensor([[[[1., 1., 1., 1.],
                       [1., 1., 1., 1.],
                 	   [1., 1., 1., 1.],
              		   [1., 1., 1., 1.]]]]),)
    

    最初にネットワークを初期化します。畳み込み層には 2 つの畳み込みカーネルがあり、重みはすべて 1 とすべて 2、バイアスは 0 に設定され、プーリング層は 2 × を採用します。 2 2\times2 2×最大プール数は 2 です。
    forward に進む前に、 関数をモジュールの conv1 に登録してから、順伝播 ()、順伝播が完了すると、 リストの最初の要素は、 ここで、 関数には 2 つの変数 () があり、特性があることに注意してください。マップは がこの変数、 レイヤーの入力データ、および レイヤーはタプル形式です。 forward_hookoutput = model(dummy_img)outputs_fmap_listconv1
    forward_hookinputoutputoutputinputconv1conv1


モジュールがフック関数をどのように呼び出すかを分析してみましょう?

  1. model 是一个 module 类,对 module 执行 module(input)output = model(dummy_img))是会调用 module.call

  2. module.__call__ 実行プロセスは次のとおりです。

    def __call__(self, *input, **kwargs):
        for hook in self._forward_pre_hooks.values():
            hook(self, input)
        if torch._C._get_tracing_state():
            result = self._slow_forward(*input, **kwargs)
        else:
            result = self.forward(*input, **kwargs)
        for hook in self._forward_hooks.values():
            hook_result = hook(self, input, result)
            if hook_result is not None:
                raise RuntimeError(
                    "forward hooks should never return any values, but '{}'"
                    "didn't return None".format(hook))
    ...
    
    • 首先判断 module(即 model)是否有 forward_pre_hook(在执行 forward 之前的 hook);

    • 然后执行 forward

    • forward終わってから到着するだけforward_hook

      ただし、ここで注意してください。現在実行されているのはmodel.call、作成したフックはモジュール model.conv1
      2 番目のジャンプは model.__call__ です。result = self.forward(*input, **kwargs)

  3. model.forward

    def forward(self, x):
        x = self.conv1(x)
        x = self.pool1(x)
        return x
    

    model.forward では、self.conv1(x) が最初に実行され、conv1nn.Conv2d です。 (モジュールクラスでもあります)。ステップ 1 で述べたように、モジュールで module(input) を実行すると が呼び出されます。module.call

  4. nn.Conv2d.call

    nn.Conv2d.__call__ のプロセスはステップ 2 と同じです。

    def __call__(self, *input, **kwargs):
        for hook in self._forward_pre_hooks.values():
            hook(self, input)
        if torch._C._get_tracing_state():
            result = self._slow_forward(*input, **kwargs)
        else:
            result = self.forward(*input, **kwargs)
        for hook in self._forward_hooks.values():
            hook_result = hook(self, input, result)
            if hook_result is not None:
                raise RuntimeError(
                    "forward hooks should never return any values, but '{}'"
                    "didn't return None".format(hook))
    

    ここで、最後に、登録した forward_hook 関数をここ hook_result = hook(self, input, result) で実行する必要があります。この時点で、次の 2 つの点に注意する必要があります。

    1. hook_result = hook(self, input, result)inputresult の値は変更できません。
      ここで、 inputforward_hook 関数の入力に対応し、結果は では、input はこのレイヤーの入力データであり、result は レイヤー操作後の出力特徴マップです。これらのデータはフックを通じて操作できますが、これらの値は変更できません。変更しないと、モデルの計算が破壊されます。 forward_hookconv1conv1

    2. 登録されたフック関数は戻り値を返すことができません。それ以外の場合は例外がスローされます。

      if hook_result is not None:
      	raise RuntimeError
      

呼び出しプロセスを要約すると、次のようになります。

model(dummy_img) --> model.call:
    result = self.forward(*input, **kwargs)
--> model.forward: 
    x = self.conv1(x)
--> conv1.call:
    hook_result = hook(self, input, result)  # hook就是我们注册的forward_hook函数了

1.2.3 nn.Module.register_forward_pre_hook

nn.Module.register_forward_pre_hookPyTorchにおけるニューラルネットワークモジュールの順伝播処理の前にコールバック関数(フック)を登録するために使用されるメソッドです。これにより、モジュールの順方向パスが開始される前に、カスタム操作を実行したり、情報をログに記録したりすることができます。方法の詳細は次のとおりです。

  • 定義:register_forward_pre_hook このメソッドは、前方プリフックを登録するために使用され、モジュールの前方伝播の前に実行できるようにします。行動。

  • 功能

    • フォワード パス プリフックを使用すると、モジュールの入力を監視しながら、モジュールがフォワード パスの計算を実行する前に操作を実行できます。

    • これは、順方向パス中に変更を加えたり、入力を監視したり、情報を記録したりする場合に役立ちます。

  • 语法

    def hook(module, input) -> None:
        ...
    
    model/layer.register_forward_pre_hook(hook)
    

    hook は、モジュールの順方向パスの前に実行されるユーザー定義関数です。この関数は 2 つのパラメータ、moduleinput を受け入れます。これらはそれぞれニューラル ネットワーク モジュールとモジュールの入力を表します。

: register_forward_pre_hook メソッドの使用方法を示す例は次のとおりです。

import torch
import torch.nn as nn
import warnings


# 忽略特定的警告
warnings.filterwarnings("ignore", category=UserWarning, module="torch")

# 定义前向传播预钩子函数
def forward_pre_hook(module, input):
    print(f"Module: {
      
      module.__class__.__name__}")
    print(f"Input: {
      
      input}")

# 创建一个神经网络模块
class MyModule(nn.Module):
    def __init__(self):
        super(MyModule, self).__init__()
        self.fc = nn.Linear(3, 2)

    def forward(self, x):
        return self.fc(x)

# 创建模块实例
model = MyModule()

# 创建一个输入
input_data = torch.randn(1, 3, requires_grad=True)

# 注册前向传播预钩子
hook_handle = model.fc.register_forward_pre_hook(forward_pre_hook)

# 执行前向传播
output = model(input_data)
Module: Linear
Input: (tensor([[-0.2644,  1.9462,  2.2998]], requires_grad=True),)

この例では、フォワード パス プリフックでモジュールの入力情報をキャプチャし、フォワード パス プロセスが開始する前にカスタム操作を実行します。これにより、モジュールが実行される前に操作を実行したり、入力を記録したりすることができ、モジュールは通常のフォワード パス計算の実行に進みます。

1.2.4 nn.Module.register_backward_hook

nn.Moduleregister_backward_hook メソッドを使用すると、PyTorch モデルの逆伝播プロセス中にカスタム コールバック関数を登録して、勾配を計算するときに追加の操作を実行できます。これは、勾配の監視と変更、勾配解析の実行、またはその他のカスタム操作の実行に役立ちます。

  • 语法

    def hook(module, grad_input, grad_output) -> Tensor or None:
    	...
        
    model/layer.register_backward_hook(hook)
    
  • 参数说明

    • module: モデル内のレイヤーまたはモジュールを表します
    • grad_input: 入力勾配を含むタプル
    • grad_output: 出力勾配を含むタプル

コールバック関数は通常、勾配の記録、分析、変更などの特定の操作を実行するために使用されます。モデルの異なるレイヤーに異なるコールバック関数を登録して、必要に応じて異なるレイヤーに対して異なる操作を実行できます。

  • アプリケーション シナリオの例: 特徴マップの勾配を抽出する
    使用register_backward_hookして、特徴マップの勾配を抽出します。 Grad-CAM (クラス勾配ベースのクラス活性化マップ視覚化) 手法と組み合わせて、畳み込みニューラル ネットワークの学習モードを視覚化します。

register_backward_hook の使用例は次のとおりです。

import torch
import torch.nn as nn
import warnings


# 忽略特定的警告
warnings.filterwarnings("ignore", category=UserWarning, module="torch")

# 自定义的回调函数
def hook(module, grad_input, grad_output):
    # 打印梯度信息
    print(f"Module: \n{
      
      module}\n")
    print(f"Input Gradient: \n{
      
      grad_input}\n")
    print(f"Output Gradient: \n{
      
      grad_output}\n")

# 创建模型
class CustomModel(nn.Module):
    def __init__(self):
        super(CustomModel, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=2, kernel_size=3)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)

    def forward(self, x):
        x = self.conv1(x)
        x = self.pool1(x)
        return x

if __name__ == "__main__":
    model = CustomModel()
    loss_fn = nn.MSELoss()

    # 在模型的某一层上注册回调函数
    handler = model.conv1.register_backward_hook(hook)

    # 前向传播和反向传播
    dummy_img = torch.ones((1, 1, 4, 4))
    label = torch.randint(0, 10, size=[1, 2], dtype=torch.float)
    output = model(dummy_img)
    loss = loss_fn(output, label)  # 将输出转化为标量值
    loss.backward()  # 对损失进行反向传播


    # 注销回调函数
    handler.remove()
Module: Conv2d(1, 2, kernel_size=(3, 3), stride=(1, 1))

Input Gradient: 
(None, tensor([[[[-2.7914, -2.7914, -2.7914],
          		 [-2.7914, -2.7914, -2.7914],
          		 [-2.7914, -2.7914, -2.7914]]],


        		[[[-1.2109, -1.2109, -1.2109],
          		  [-1.2109, -1.2109, -1.2109],
          		  [-1.2109, -1.2109, -1.2109]]]]), tensor([-2.7914, -1.2109]))

Output Gradient: 
(tensor([[[[-2.7914,  0.0000],
           [ 0.0000,  0.0000]],

          [[-1.2109,  0.0000],
           [ 0.0000,  0.0000]]]]),)

この例では、カスタム コールバック関数 custom_backward_hook を作成し、モデルのレイヤーに登録します。次に、順伝播と逆伝播を実行し、勾配情報をコールバック関数に出力します。最後に、コールバック関数の登録を解除して、後続のバックプロパゲーションでコールバック関数が実行されないようにします。

register_backward_hook を使用すると、勾配の監視と操作、勾配解析の実行、またはその他の勾配関連のカスタム操作を実行できます。これは、モデルのデバッグや最適化に非常に役立ちます。

2. フック関数と特徴マップの抽出

"""
    使用hook函数可视化特征图
"""
import torch
import torch.nn as nn
import random
import numpy as np
from torch.utils.tensorboard import SummaryWriter
import torchvision.transforms as transforms
from PIL import Image
import torchvision.models as models
import torchvision.utils as vutils
import matplotlib.pyplot as plt


def set_seed(seed):
    # 设置Python内置的随机数生成器的种子
    random.seed(seed)

    # 设置NumPy的随机数生成器的种子
    np.random.seed(seed)

    # 设置PyTorch的随机数生成器的种子(如果使用了PyTorch)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False


if __name__ == "__main__":
    set_seed(1)  # 设置随机数种子
    # 实例化 Tensorboard
    writer = SummaryWriter(comment="test_your_comment", filename_suffix="_test_your_filename_suffix")

    # 读取数据并进行预处理
    image_path = "lena.png"
    MEAN = [0.49139968, 0.48215827, 0.44653124]
    STD = [0.24703233, 0.24348505, 0.26158768]

    normalization = transforms.Normalize(MEAN, STD)
    image_transforms = transforms.Compose([transforms.Resize(size=(224, 224)),
                                           transforms.ToTensor(),
                                           normalization])  # 标准化一定要在ToTensor之后进行

    # 使用 Pillow 读取图片
    image_pillow = Image.open(fp=image_path).convert('RGB')

    # 对图片进行预处理
    if image_transforms:
        image_tensor = image_transforms(image_pillow)

    # 添加 Batch 维度
    image_tensor = torch.unsqueeze(input=image_tensor, dim=0)  # [C, H, W] -> [B, C, H, W]

    # 创建模型
    alexnet = models.alexnet(weights=models.AlexNet_Weights.DEFAULT)  # <=> pretrained=True

    # 注册hook
    fmap_dict = dict()  # 存放所有卷积层的特征图
    for name, sub_module in alexnet.named_modules():
        """
            name: features.0
            sub_module: Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
        """
        if isinstance(sub_module, nn.Conv2d):  # 判断是否为卷积层,如果是则注册hook
            key_name = str(sub_module.weight.shape)  # torch.Size([64, 3, 11, 11])
            fmap_dict.setdefault(key_name, list())
            layer_name, index = name.split('.')  # 'features', 0

            def hook_func(m, i, o):  # m: module; i: input; o: output
                key_name = str(m.weight.shape)
                fmap_dict[key_name].append(o)

            # 给 nn.Conv2d层 添加hook函数
            # alexnet._modules[layer_name]._modules[index] -> Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
            alexnet._modules[layer_name]._modules[index].register_forward_hook(hook_func)

    # forward(在执行模型时会自动执行hook函数从而往fmap_dict字典中存放输出特征图)
    output = alexnet(image_tensor)

    # 添加图像
    for layer_name, fmap_list in fmap_dict.items():
        # layer_name: torch.Size([1, 3, 224, 224])
        # len(fmap_list): 1 -> shape: [B, C, H, W]
        fmap = fmap_list[0]  # 去掉[]
        fmap.transpose_(0, 1)  # [B, C, H, W] -> [C, B, H, W]

        nrow = int(np.sqrt(fmap.shape[0]))  # 开根号获取行号
        fmap_grid = vutils.make_grid(fmap, normalize=True, scale_each=True, nrow=nrow)  # [3, 458, 458]

        # 将结果存放到 Tensorboard 中
        writer.add_image(tag=f"feature map in {
      
      layer_name}", img_tensor=fmap_grid, global_step=1)

        # 也可以将结果直接用 Matplotlib 读取
        # 创建一个图像窗口
        plt.figure(figsize=(8, 8))

        # 使用imshow函数显示 grid_image
        plt.imshow(vutils.make_grid(fmap_grid, normalize=True).permute(1, 2, 0))  # 注意permute的用法,将通道维度移到最后
        plt.axis('off')  # 不显示坐标轴

        # 显示图像
        plt.savefig(f"feature map_in_{
      
      layer_name}.png")

结果

ここに画像の説明を挿入します

機能マップ_in_torch.Size([64, 3, 11, 11]).png

ここに画像の説明を挿入します

機能マップ_in_torch.Size([192, 64, 5, 5]).png

ここに画像の説明を挿入します

機能マップ_in_torch.Size([256, 256, 3, 3]).png

ここに画像の説明を挿入します

機能マップ_in_torch.Size([256, 384, 3, 3]).png

ここに画像の説明を挿入します

機能マップ_in_torch.Size([384, 192, 3, 3]).png

【拓展知识】1. dict.setdefault(key, default) 的作用

dict.setdefault(key, default) は、辞書内のキーのデフォルト値を設定する Python 辞書 (dict) のメソッドです。指定されたキー key が辞書内に存在する場合、このメソッドはキーに関連付けられた値を返します。指定されたキー key が辞書に存在しない場合は、キーを辞書に追加し、その値を default に設定し、 default

【拓展知识】2. alexnet._modulesalexnet.module 有什么区别?

PyTorch では、alexnet._modulesalexnet.module は異なるものを表します。

  1. alexnet._modules

    • alexnet._modulesAlexNet モデルの各サブモジュールを含む辞書です。各サブモジュールは名前で保存され、辞書キーでアクセスできます。これらのサブモジュールには、畳み込み層、全結合層、プーリング層などが含まれます。この方法を使用して、AlexNet のさまざまなコンポーネントを表示およびアクセスできます。
    • 例えば:

      import torchvision.models as models
      alexnet = models.alexnet()
      print(alexnet._modules)
      

      これにより、AlexNet のさまざまなサブモジュールを含む辞書が表示されます。

      OrderedDict([('features', Sequential(
        (0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
        (1): ReLU(inplace=True)
        (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
        (3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
        (4): ReLU(inplace=True)
        (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
        (6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (7): ReLU(inplace=True)
        (8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (9): ReLU(inplace=True)
        (10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (11): ReLU(inplace=True)
        (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
      )), ('avgpool', AdaptiveAvgPool2d(output_size=(6, 6))), ('classifier', Sequential(
        (0): Dropout(p=0.5, inplace=False)
        (1): Linear(in_features=9216, out_features=4096, bias=True)
        (2): ReLU(inplace=True)
        (3): Dropout(p=0.5, inplace=False)
        (4): Linear(in_features=4096, out_features=4096, bias=True)
        (5): ReLU(inplace=True)
        (6): Linear(in_features=4096, out_features=1000, bias=True)
      ))])
      
  2. alexnet.module

    • alexnet.module は通常、特に分散環境またはマルチ GPU 環境でトレーニングする場合に、AlexNet モデル全体を参照するために使用される属性です。この場合、alexnet.module はモデルのトップレベル ラッパーであり、実際の AlexNet モデルはその中に存在します。このアプローチにより、モデルを別の GPU に移動して並列トレーニングが可能になります。

    • 例えば:

      import torchvision.models as models
      alexnet = models.alexnet()
      print(alexnet.modules)
      

      結果は次のとおりです。

      <bound method Module.modules of AlexNet(
        (features): Sequential(
          (0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
          (1): ReLU(inplace=True)
          (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
          (3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
          (4): ReLU(inplace=True)
          (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
          (6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (7): ReLU(inplace=True)
          (8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (9): ReLU(inplace=True)
          (10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (11): ReLU(inplace=True)
          (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
        )
        (avgpool): AdaptiveAvgPool2d(output_size=(6, 6))
        (classifier): Sequential(
          (0): Dropout(p=0.5, inplace=False)
          (1): Linear(in_features=9216, out_features=4096, bias=True)
          (2): ReLU(inplace=True)
          (3): Dropout(p=0.5, inplace=False)
          (4): Linear(in_features=4096, out_features=4096, bias=True)
          (5): ReLU(inplace=True)
          (6): Linear(in_features=4096, out_features=1000, bias=True)
        )
      )>
      

      もう 1 つの一般的な使用法があります。

      import torch.nn as nn
      import torch.nn.parallel
      import torchvision.models as models
      
      alexnet = models.alexnet()
      # 如果使用多GPU训练,通常有一个外部的包装模块
      alexnet = nn.DataParallel(alexnet)
      
      # 在这种情况下,实际的AlexNet模型在alexnet.module中
      actual_alexnet = alexnet.module
      

要するに、alexnet._modules には AlexNet のさまざまなサブモジュールが含まれており、alexnet.module は通常マルチ GPU トレーニングに使用されるモデル ラッパーです。特定の用途に応じて、どちらかを使用することを選択できます。 AlexNet のサブモジュールだけを表示したい場合は、 alexnet._modules の方が適切なオプションです。マルチ GPU トレーニングを行う必要がある場合は、 alexnet.module の方が便利かもしれません。

[知識を広げる] 3. torchvision.utils.make_grid の関数、パラメータ、戻り値

torchvision.utils.make_gridPyTorch の torchvision ライブラリの関数で、複数の画像を大きなグリッドに結合して、視覚化とプレゼンテーションを容易にするために使用されます。この関数は通常、深層学習モデルの出力またはトレーニング データを視覚化するために使用されます。

make_grid 関数のパラメータと戻り値は次のとおりです。

  • パラメータ:

    • tensor (テンソル): グリッドにマージされるイメージを含む入力テンソル。通常、これは (batch_size, channels, height, width) の形状のテンソルであり、 batch_size はバッチ内の画像の数を表し、 channels はチャネルの数を表します。 heightwidth は、各画像の高さと幅を表します。
    • nrow(オプション): 1 行に表示する画像の数を指定します。デフォルトは 8 です。

    • padding(オプション): 各画像間のパディングピクセル数。デフォルトは 2 です。

    • normalize(オプション): 入力テンソルが 0 ~ 1 の範囲で表示されるように正規化されているかどうかを示すブール値。デフォルトは False です。 True に設定すると、入力テンソルは [0, 1] に正規化され、グリッドでの視覚化が容易になります。

    • range (オプション): テンソルのデータ範囲を指定する長さ 2 のタプル ((min_value, max_value) など)。 normalize が True に設定されている場合、このパラメータを使用してデータの正規化範囲を指定できます。

    • scale_each(オプション): 各イメージがその範囲に合わせて個別にスケーリングされるかどうかを示すブール値。True に設定すると、各イメージの範囲は個別に計算されます。デフォルトは False です。

    • pad_value(オプション): 塗りつぶし値の色。通常は長さ 3 の RGB カラータプルで、デフォルトは 0 (黒を意味します) です。

  • 戻り値:

    • grid_image (テンソル): (C, H, W) の形状を持つ、入力テンソル内のすべてのイメージをマージするテンソル。 C はチャネル数です。 a > はグリッドの幅です。通常、このテンソルは視覚化に直接使用することも、画像ファイルとして保存することもできます。 H はグリッドの高さ、W

使用例:

import torchvision.utils as vutils
import torch

# 假设有一个存储图像的张量 images
grid_image = vutils.make_grid(images, nrow=4, padding=2, normalize=True)

上記の例では、make_gridimages テンソル内の画像を 1 行あたり 4 つの画像を表示するグリッドに結合し、2 ピクセルが塗りつぶされます。視覚化のために正規化されています。結合されたイメージは grid_image 変数に保存されます。

[知識を広げる] 4. writer.add_image の関数、パラメータ、戻り値

writer.add_image は、TensorBoard で画像データを視覚化、監視、分析するために TensorBoard レコードに画像を追加する、PyTorch の SummaryWriter オブジェクト内のメソッドです。 writer.add_image メソッドのパラメータと戻り値は次のとおりです。

  • 効果:

    • writer.add_imageTensorBoard で視覚化するために画像データを記録するために使用されます。これは、画像データ、モデル出力、または深層学習のデータ処理ステップを視覚化するのに役立ちます。
  • パラメータ:

    • tag(文字列): 記録された画像を識別するために使用されるラベルまたは識別子。通常、これは TensorBoard で画像レコードを識別および整理する目的で画像を説明する文字列です。

    • img_tensor (Tensor): 記録される画像データを含む Tensor。通常、これは (C, H, W) の形状の 3 次元テンソルです。 C はチャネル数を表し、 H は高さを表します。 記録する画像のピクセル値が含まれます。 W は幅を表します。 img_tensor

    • global_step(整数、オプション): TensorBoard 内の異なるレコードからの画像を位置合わせするために使用される、記録のグローバル ステップまたは反復の数を表します。指定しない場合、TensorBoard は自動インクリメント ステップを使用します。 ——モデルトレーニングでは通常Epochです

  • 戻り値:

    • None: writer.add_image メソッドには戻り値がありません。主に、視覚化と分析のために画像データを TensorBoard に記録するために使用されます。
  • 使用例:

    from torch.utils.tensorboard import SummaryWriter
    import torch
    
    
    # 创建一个 SummaryWriter 对象,用于记录数据到 TensorBoard
    writer = SummaryWriter()
    
    # 假设我们已经创建了图像张量 img_tensor 和一个全局步骤 global_step
    # 将图像记录到 TensorBoard 中
    writer.add_image("Sample Image", img_tensor, global_step)
    
    # 关闭 SummaryWriter
    writer.close()
    

    上記の例では、add_image メソッドは img_tensor を TensorBoard レコードに追加し、「Sample Image」タグで画像を識別します。これらの画像レコードを TensorBoard で表示して分析できます。 global_step パラメータは、画像を TensorBoard 内の他のレコードと位置合わせするためのレコードのグローバル ステップ (一般に global_step をエポックまたは反復と考えます) を指定するために使用されます。

3. CAM(Class Activation Map、クラス活性化マップ)&Grad-CAM

は 1.2.4 で導入されました nn.module.register_backward_hook 実際、このフック関数は CAM でヒートマップを取得するためによく使用されます。

ここに画像の説明を挿入します

CAM には欠点があります。ネットワークの最終出力部分には、さまざまな特徴マップの重みを取得するために GAP (Global Average Pooling、グローバル平均プーリング) が必要です。ヒートマップと同じなので、CAMを利用するにはネットワーク構造を変更する必要があり、適用範囲はそれほど広くありません。 CAM の欠点を考慮して、Grad-CAM という新しい方法が提案されました。

ここに画像の説明を挿入します

図 2: Grad-CAM の概要: 画像と対象のクラス (例: 「tiger-cat」またはその他の微分可能な出力タイプ) を入力として指定すると、モデルの CNN 部分をフォワードパススルーし、タスクが計算されます。そのカテゴリの生のスコアを取得します。勾配は、目的のクラス (tiger cat) を除くすべてのクラスで 0 に設定され、目的のクラスでは 1 に設定されます。次に、この信号は対象となる修正された畳み込み特徴マップに逆伝播され、それらが組み合わされて粗い Grad-CAM 位置特定 (青色のヒートマップ) が計算され、特定の決定を下すためにモデルがどの部分を見る必要があるかを示します。最後に、ヒートマップにガイド付きバックプロパゲーションを乗算して、高解像度でコンセプトに特化した Guided Grad-CAM 視覚化を取得します。

CAM には、①特徴マップ、②特徴マップに対応する重みの 2 つのキーポイントがありますが、Grad-CAM では特徴マップの重みとして勾配が使用されます。


以下では、LeNet-5 を使用して、Grad-CAM での backward_hook のアプリケーションを示します。コード フローは次のとおりです。

  1. 创建网络 net
  2. Register forward_hook 関数は、特徴マップの最後のレイヤーを抽出するために使用されます。
  3. 登録 backward_hook この関数は、特徴マップ上のクラス ベクトル (ワンホット) の勾配を抽出するために使用されます。
  4. 特徴マップの勾配を平均し、特徴マップに重みを付けます。
  5. ヒートマップを視覚化します。
"""
通过实现 Grad-CAM 学习 module 中的 forward_hook 和 backward_hook 函数
"""
import cv2
import os
import numpy as np
from PIL import Image
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool1 = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.pool2 = nn.MaxPool2d(2, 2)
        
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool1(F.relu(self.conv1(x)))
        x = self.pool1(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


def img_transform(img_in, transform):
    """
    将img进行预处理,并转换成模型输入所需的形式—— B*C*H*W
    :param img_roi: np.array
    :return:
    """
    img = img_in.copy()
    img = Image.fromarray(np.uint8(img))
    img = transform(img)
    img = img.unsqueeze(0)    # C*H*W --> B*C*H*W
    return img


def img_preprocess(img_in):
    """
    读取图片,转为模型可读的形式
    :param img_in: ndarray, [H, W, C]
    :return: PIL.image
    """
    img = img_in.copy()
    img = cv2.resize(img, (32, 32))
    img = img[:, :, ::-1]   # BGR --> RGB
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize([0.4948052, 0.48568845, 0.44682974], [0.24580306, 0.24236229, 0.2603115])
    ])
    img_input = img_transform(img, transform)
    return img_input


def backward_hook(module, grad_in, grad_out):
    grad_block.append(grad_out[0].detach())


def farward_hook(m, i, o):
    fmap_block.append(o)


def show_cam_on_image(img, mask, out_dir):
    heatmap = cv2.applyColorMap(np.uint8(255*mask), cv2.COLORMAP_JET)
    heatmap = np.float32(heatmap) / 255
    cam = heatmap + np.float32(img)
    cam = cam / np.max(cam)

    path_cam_img = os.path.join(out_dir, "cam.jpg")
    path_raw_img = os.path.join(out_dir, "raw.jpg")
    if not os.path.exists(out_dir):
        os.makedirs(out_dir)
    cv2.imwrite(path_cam_img, np.uint8(255 * cam))
    cv2.imwrite(path_raw_img, np.uint8(255 * img))


def comp_class_vec(ouput_vec, index=None):
    """
    计算类向量
    :param ouput_vec: tensor
    :param index: int,指定类别
    :return: tensor
    """
    if not index:
        index = np.argmax(ouput_vec.cpu().data.numpy())
    else:
        index = np.array(index)
    index = index[np.newaxis, np.newaxis]
    index = torch.from_numpy(index)
    one_hot = torch.zeros(1, 10).scatter_(1, index, 1)
    one_hot.requires_grad = True
    class_vec = torch.sum(one_hot * output)  # one_hot = 11.8605

    return class_vec


def gen_cam(feature_map, grads):
    """
    依据梯度和特征图,生成cam
    :param feature_map: np.array, in [C, H, W]
    :param grads: np.array, in [C, H, W]
    :return: np.array, [H, W]
    """
    cam = np.zeros(feature_map.shape[1:], dtype=np.float32)  # cam shape (H, W)

    weights = np.mean(grads, axis=(1, 2))  #

    for i, w in enumerate(weights):
        cam += w * feature_map[i, :, :]

    cam = np.maximum(cam, 0)
    cam = cv2.resize(cam, (32, 32))
    cam -= np.min(cam)
    cam /= np.max(cam)

    return cam


if __name__ == '__main__':

    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    path_img = os.path.join("cam_img", "test_img_8.png")
    path_net = os.path.join("net_params_72p.pkl")
    output_dir = os.path.join("Result", "backward_hook_cam")

    classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
    fmap_block = list()
    grad_block = list()
    print(path_img)

    # 图片读取;网络加载
    img = cv2.imread(path_img, 1)  # H*W*C
    img_input = img_preprocess(img)
    net = Net()
    net.load_state_dict(torch.load(path_net))

    # 注册hook
    net.conv2.register_forward_hook(farward_hook)
    net.conv2.register_backward_hook(backward_hook)

    # forward
    output = net(img_input)
    idx = np.argmax(output.cpu().data.numpy())
    print("predict: {}".format(classes[idx]))

    # backward
    net.zero_grad()
    class_loss = comp_class_vec(output)
    class_loss.backward()

    # 生成cam
    grads_val = grad_block[0].cpu().data.numpy().squeeze()
    fmap = fmap_block[0].cpu().data.numpy().squeeze()
    cam = gen_cam(fmap, grads_val)

    # 保存cam图片
    img_show = np.float32(cv2.resize(img, (32, 32))) / 255
    show_cam_on_image(img_show, cam, output_dir)

backward_hook 関数では、grad_out はタプル タイプであることに注意してください。特徴マップの勾配を取得するには、次の操作を行う必要があります。これgrad_block. append(grad_out[0]. detach())

ここでは、下の図に示すように、3 つの航空機の写真のヒートマップを観察します。1 行目は元の写真、2 行目はヒートマップを重ね合わせた写真です。

ここに画像の説明を挿入します

ここで、模型が飛行機ではなく青空を基準に写真を飛行機と判断してしまうという興味深い現象が発見されました(図1~図3)。では、モデルに純粋な空色の絵を与えたら、モデルは何を判断するでしょうか?図 4 に示すように、発見モデルは画像が飛行機であると判断しました。

ここから、モデルは航空機を正しく分類できたものの、学習したのは航空機の特性ではないことが判明しました。これにより、モデルの汎化パフォーマンスが大幅に低下するため、航空機によく現れる青空の代わりにモデルに航空機を強制的に学習させるトリックを使用したり、データを調整したりすることが考えられます。


図 4 に関する質問: ヒートマップの青い領域は画像にまったく影響を与えませんか?画像の赤い部分だけで判断できるのでしょうか?

ここに画像の説明を挿入します

次に、正しく分類された自動車の画像 (図 5) を、図 4 の青い応答領域 (つまり、モデルが注意を払っていない領域) に重ね合わせます。結果を図に示します。 6. 自動車部品の応答値は非常に小さいですが、モデルは依然として空色の領域を通して画像を飛行機として識別します。次に、図 4 の赤い応答領域 (図の右下隅) に車が重ねられ、その結果が図 7 に示されています。
興味深いのは、図 7 の赤い応答領域に車が重ねて表示されていることです。モデルは画像をボートとして判断し、赤い応答領域は青い領域の下部です。これは海中のボートの位置と一致しており、非常に近いです。

上記のコードと Grad-CAM でのそのアプリケーションを通じて backward_hook の使用法を学習し、Grad-CAM を使用してモデルが主要な機能を学習したかどうかを診断します。

知識の源

  1. 【05-05-フック関数とCAMアルゴリズム.mp4】
  2. PyTorch フックと Grad-CAM_PyTorch のそのアプリケーションは、grad-CSDN ブログを観察するためのフックを定義します

おすすめ

転載: blog.csdn.net/weixin_44878336/article/details/133859089