深度学习模型构建与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. 问题解答
以下是一些常见问题的解答:
- 如何实现单个神经元?
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)
- 如何实现 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)
- 如何用矩阵乘法实现密集层?
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. 进一步探索
如果你想深入学习,可以尝试以下内容:
-
实现自定义的
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)
-
使用
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 | 任意层 | 使用所需类的最终激活值的梯度 | 可应用于任意层,灵活性高 | 计算相对复杂 |
在实际应用中,我们可以根据具体需求选择合适的方法,深入理解模型的决策过程,从而更好地优化模型。希望本文能为你在深度学习的学习和实践中提供有价值的参考。
超级会员免费看

被折叠的 条评论
为什么被折叠?



