46、深度学习模型构建与CNN解释:从基础到应用

深度学习模型构建与CNN解释:从基础到应用

1. 模型的前向与反向传播

在构建模型时,前向传播和反向传播的实现经过重构后变得十分简单。以下是相关代码示例:

for l in self.layers: x = l(x) 
return self.loss(x, targ) 

def backward(self): 
    self.loss.backward() 
    for l in reversed(self.layers): l.backward()

若要实例化模型,只需这样写:

model = Model(w1, b1, w2, b2)

前向传播可以这样执行:

loss = model(x, y)

反向传播则为:

model.backward()
2. 向PyTorch靠拢

我们编写的 Lin Mse Relu 类有很多共同之处,因此可以让它们继承同一个基类 LayerFunction

class LayerFunction(): 
    def __call__(self, *args): 
        self.args = args 
        self.out = self.forward(*args) 
        return self.out 

    def forward(self):  raise Exception('not implemented') 
    def bwd(self):      raise Exception('not implemented') 
    def backward(self): self.bwd(self.out, *self.args)

然后在各个子类中实现 forward bwd 方法:

class Relu(LayerFunction): 
    def forward(self, inp): return inp.clamp_min(0.) 
    def bwd(self, out, inp): inp.g = (inp>0).float() * out.g

class Lin(LayerFunction): 
    def __init__(self, w, b): self.w,self.b = w,b 

    def forward(self, inp): return inp@self.w + self.b 

    def bwd(self, out, inp): 
        inp.g = out.g @ self.w.t() 
        self.w.g = self.inp.t() @ self.out.g 
        self.b.g = out.g.sum(0)

class Mse(LayerFunction): 
    def forward (self, inp, targ): return (inp.squeeze() - targ).pow(2).mean() 
    def bwd(self, out, inp, targ): 
        inp.g = 2*(inp.squeeze()-targ).unsqueeze(-1) / targ.shape[0]

这已经越来越接近 PyTorch 的实现方式。在 PyTorch 中,每个需要求导的基本函数都被写成 torch.autograd.Function 对象,该对象有 forward backward 方法。以下是自定义 MyRelu 函数的示例:

from torch.autograd import Function 

class MyRelu(Function): 
    @staticmethod 
    def forward(ctx, i): 
        result = i.clamp_min(0.) 
        ctx.save_for_backward(i) 
        return result 

    @staticmethod 
    def backward(ctx, grad_output): 
        i, = ctx.saved_tensors 
        return grad_output * (i>0).float()
3. 构建复杂模型的结构

构建更复杂模型时,会使用 torch.nn.Module 作为基础结构,它有助于注册所有可训练参数。实现 nn.Module 需遵循以下步骤:
1. 初始化时先调用父类的 __init__ 方法。
2. 使用 nn.Parameter 将模型的参数定义为属性。
3. 定义一个 forward 函数,返回模型的输出。

以下是一个从零实现的线性层示例:

import torch.nn as nn 

class LinearLayer(nn.Module): 
    def __init__(self, n_in, n_out): 
        super().__init__() 
        self.weight = nn.Parameter(torch.randn(n_out, n_in) * sqrt(2/n_in)) 
        self.bias = nn.Parameter(torch.zeros(n_out)) 

    def forward(self, x): return x @ self.weight.t() + self.bias

使用 PyTorch 的线性层构建模型的示例如下:

class Model(nn.Module): 
    def __init__(self, n_in, nh, n_out): 
        super().__init__() 
        self.layers = nn.Sequential( 
            nn.Linear(n_in,nh), nn.ReLU(), nn.Linear(nh,n_out)) 
        self.loss = mse 

    def forward(self, x, targ): return self.loss(self.layers(x).squeeze(), targ)

fastai 提供了与 nn.Module 相同的 Module 变体,但无需手动调用 super().__init__()

4. 关键知识点总结
  • 神经网络本质上是一系列矩阵乘法,中间穿插着非线性操作。
  • 由于 Python 运行速度较慢,为编写快速代码,需进行向量化,并利用元素级运算和广播等技术。
  • 两个张量可广播的条件是从末尾开始向前的维度匹配(相同或其中一个为 1),可使用 unsqueeze None 索引添加大小为 1 的维度。
  • 正确初始化神经网络对训练至关重要,当有 ReLU 非线性激活函数时,应使用 Kaiming 初始化。
  • 反向传播是多次应用链式法则,从模型输出开始计算梯度,逐层回溯。
  • 子类化 nn.Module 时(若不使用 fastai 的 Module ),需在 __init__ 方法中调用父类的 __init__ 方法,并定义一个 forward 函数。
5. 类激活图(CAM)与钩子

类激活图(CAM)由 Bolei Zhou 等人提出,它结合最后卷积层的输出和预测结果,为我们提供模型决策原因的热力图可视化。在 PyTorch 中,可使用钩子来访问模型训练时内部的激活值。钩子类似于 fastai 的回调函数,但它允许在正向和反向计算中注入代码。

以下是使用 CAM 的示例代码:

# 加载数据和模型
path = untar_data(URLs.PETS)/'images'
def is_cat(x): return x[0].isupper()
dls = ImageDataLoaders.from_name_func( 
    path, get_image_files(path), valid_pct=0.2, seed=21, 
    label_func=is_cat, item_tfms=Resize(224))

learn = cnn_learner(dls, resnet34, metrics=error_rate)
learn.fine_tune(1)

# 获取图片和数据
img = PILImage.create('images/chapter1_cat_example.jpg')
x, = first(dls.test_dl([img]))

# 定义钩子类
class Hook(): 
    def hook_func(self, m, i, o): self.stored = o.detach().clone()

# 实例化钩子并挂载到指定层
hook_output = Hook()
hook = learn.model[0].register_forward_hook(hook_output.hook_func)

# 前向传播并获取激活值
with torch.no_grad(): output = learn.model.eval()(x)
act = hook_output.stored[0]

# 计算 CAM 图
cam_map = torch.einsum('ck,kij->cij', learn.model[1][-1].weight, act)

# 可视化
x_dec = TensorImage(dls.train.decode((x,))[0][0])
_,ax = plt.subplots()
x_dec.show(ctx=ax)
ax.imshow(cam_map[1].detach().cpu(), alpha=0.6, extent=(0,224,224,0), 
          interpolation='bilinear', cmap='magma');

# 移除钩子
hook.remove()
6. 梯度类激活图(Gradient CAM)

普通的 CAM 方法只能计算最后一层的热力图,而梯度类激活图(Gradient CAM)则解决了这个问题。它使用所需类的最终激活值的梯度。我们可以通过注册反向传播钩子来存储梯度。

以下是使用 Gradient CAM 的示例代码:

# 定义反向传播钩子类
class HookBwd(): 
    def __init__(self, m): 
        self.hook = m.register_backward_hook(self.hook_func) 
    def hook_func(self, m, gi, go): self.stored = go[0].detach().clone() 
    def __enter__(self, *args): return self 
    def __exit__(self, *args): self.hook.remove()

# 计算梯度和 CAM 图
cls = 1
with HookBwd(learn.model[0]) as hookg: 
    with Hook(learn.model[0]) as hook: 
        output = learn.model.eval()(x.cuda()) 
        act = hook.stored 
    output[0,cls].backward() 
    grad = hookg.stored

w = grad[0].mean(dim=[1,2], keepdim=True)
cam_map = (w * act[0]).sum(0)

# 可视化
_,ax = plt.subplots()
x_dec.show(ctx=ax)
ax.imshow(cam_map.detach().cpu(), alpha=0.6, extent=(0,224,224,0), 
          interpolation='bilinear', cmap='magma');
7. 总结

本文介绍了模型前向传播和反向传播的实现,向 PyTorch 实现方式的过渡,以及类激活图(CAM)和梯度类激活图(Gradient CAM)的原理和使用方法。通过这些技术,我们可以更好地理解模型的决策过程,为模型优化提供依据。

以下是一个简单的流程图,展示了 CAM 的工作流程:

graph TD;
    A[输入图片] --> B[前向传播];
    B --> C[获取最后卷积层激活值];
    C --> D[计算 CAM 图];
    D --> E[可视化 CAM 图];

同时,为了更清晰地对比不同方法,以下是一个简单的表格:
| 方法 | 适用层 | 原理 |
| ---- | ---- | ---- |
| CAM | 最后一层 | 结合最后卷积层输出和最后权重矩阵 |
| Gradient CAM | 任意层 | 使用所需类的最终激活值的梯度 |

深度学习模型构建与CNN解释:从基础到应用

8. 相关技术操作要点

在实际应用中,还有一些操作要点需要注意,以下为你详细介绍:

8.1 矩阵乘法的效率问题

在纯 Python 中实现矩阵乘法非常慢,原因在于 Python 的解释执行特性以及循环操作的低效性。为了提高效率,我们需要进行向量化操作,利用元素级运算和广播技术。

元素级运算 :指的是对张量中对应元素进行相同的运算,例如:

import torch
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
c = a + b  # 对应元素相加
print(c)

广播规则 :两个张量可广播的条件是从末尾开始向前的维度匹配(相同或其中一个为 1)。例如:

a = torch.tensor([[1, 2, 3], [4, 5, 6]])
b = torch.tensor([1, 2, 3])
c = a + b  # b 会自动广播到与 a 相同的形状
print(c)
8.2 张量的操作
  • t 方法 :在 PyTorch 中, t 方法用于对二维张量进行转置操作。例如:
a = torch.tensor([[1, 2], [3, 4]])
b = a.t()
print(b)
  • squeeze 方法 :用于移除张量中维度大小为 1 的维度。例如:
a = torch.tensor([[[1, 2, 3]]])
b = a.squeeze()
print(b)
9. 问题解答

以下是一些常见问题的解答:

  1. 如何实现单个神经元?
import torch

# 定义输入和权重
x = torch.tensor([1.0, 2.0, 3.0])
w = torch.tensor([0.1, 0.2, 0.3])
b = torch.tensor(0.5)

# 计算神经元的输出
output = torch.dot(x, w) + b
print(output)
  1. 如何实现 ReLU?
import torch

def relu(x):
    return torch.clamp_min(x, 0.)

x = torch.tensor([-1.0, 2.0, -3.0])
output = relu(x)
print(output)
  1. 如何用矩阵乘法实现密集层?
import torch

# 定义输入和权重
x = torch.tensor([[1.0, 2.0, 3.0]])
w = torch.tensor([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
b = torch.tensor([0.1, 0.2])

# 计算密集层的输出
output = torch.matmul(x, w) + b
print(output)
10. 进一步探索

如果你想深入学习,可以尝试以下内容:

  1. 实现自定义的 torch.autograd.Function :例如实现自定义的 ReLU 函数,并用于训练模型。
from torch.autograd import Function
import torch

class MyRelu(Function):
    @staticmethod
    def forward(ctx, i):
        result = i.clamp_min(0.)
        ctx.save_for_backward(i)
        return result

    @staticmethod
    def backward(ctx, grad_output):
        i, = ctx.saved_tensors
        return grad_output * (i > 0).float()

# 使用自定义的 ReLU 函数
x = torch.tensor([-1.0, 2.0, -3.0], requires_grad=True)
relu = MyRelu.apply
output = relu(x)
output.backward(torch.tensor([1.0, 1.0, 1.0]))
print(x.grad)
  1. 使用 unfold 方法实现 2D 卷积 unfold 方法可以将卷积操作转化为矩阵乘法,从而实现自定义的 2D 卷积函数。
import torch
import torch.nn.functional as F

# 输入特征图
x = torch.randn(1, 3, 32, 32)
# 卷积核
weight = torch.randn(64, 3, 3, 3)

# 使用 unfold 方法
unfolded = F.unfold(x, kernel_size=3)
weight_flat = weight.view(weight.size(0), -1)
output = torch.matmul(weight_flat, unfolded)
output = output.view(1, 64, 30, 30)
print(output.shape)
11. 总结回顾

通过前面的介绍,我们已经了解了深度学习模型构建的多个方面,包括模型的前向传播和反向传播、向 PyTorch 实现方式的过渡、类激活图(CAM)和梯度类激活图(Gradient CAM)的原理和使用方法,以及相关技术的操作要点和常见问题解答。

为了更清晰地展示整个知识体系,以下是一个流程图,展示了从模型构建到解释的整体流程:

graph TD;
    A[模型构建基础] --> B[前向传播与反向传播];
    B --> C[向 PyTorch 过渡];
    C --> D[复杂模型构建];
    D --> E[模型解释(CAM 和 Gradient CAM)];
    E --> F[操作要点与问题解答];
    F --> G[进一步探索];

同时,我们可以通过以下表格对不同的模型解释方法进行对比:
| 方法 | 适用范围 | 原理 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- | ---- |
| CAM | 最后一层 | 结合最后卷积层输出和最后权重矩阵 | 简单直观,可解释模型决策原因 | 仅适用于最后一层 |
| Gradient CAM | 任意层 | 使用所需类的最终激活值的梯度 | 可应用于任意层,灵活性高 | 计算相对复杂 |

在实际应用中,我们可以根据具体需求选择合适的方法,深入理解模型的决策过程,从而更好地优化模型。希望本文能为你在深度学习的学习和实践中提供有价值的参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值