Python训练营---Day42

article/2025/8/13 19:15:50
DAY 42 Grad-CAM与Hook函数

知识点回顾

  1. 回调函数
  2. lambda函数
  3. hook函数的模块钩子和张量钩子
  4. Grad-CAM的示例

作业:理解下今天的代码即可

1、回调函数

回调函数(Callback Function)是一种特殊的函数,它作为参数传递给另一个函数,然后在那个函数内部被调用执行。简单来说,回调函数就是 "被别人调用的函数"。

回调函数的核心价值在于:

  • 解耦逻辑:将通用逻辑与特定处理逻辑分离,使代码更模块化。
  • 事件驱动编程:在异步操作、事件监听(如点击按钮、网络请求完成)等场景中广泛应用。
  • 延迟执行:允许在未来某个时间点执行特定代码,而不必立即执行。
# 定义一个回调函数
def handle_result(result):"""处理计算结果的回调函数"""print(f"计算结果是: {result}")# 定义一个接受回调函数的函数
def calculate(a, b, callback): # callback是一个约定俗成的参数名"""这个函数接受两个数值和一个回调函数,用于处理计算结果。执行计算并调用回调函数"""result = a + bcallback(result)  # 在计算完成后调用回调函数# 使用回调函数
calculate(3, 5, handle_result)  # 输出: 计算结果是: 8

2、lambda函数

lambda函数,它是一种匿名函数(没有正式名称的函数),最大特点是用完即弃,无需提前命名和定义。它的语法形式非常简约,仅需一行即可完成定义,格式如下:

lambda 参数列表: 表达式

  • 参数列表:可以是单个参数、多个参数或无参数
  • 表达式:函数的返回值(无需 return 语句,表达式结果直接返回)
square=lambda a:a**2
print(square(4))    #输出:16

3、Hook函数

Hook 函数是一种回调函数,它可以在不干扰模型正常计算流程的情况下,插入到模型的特定位置,以便获取或修改中间层的输出或梯度。(这里的 "正常计算流程" 指的是前向传播反向传播的整体执行顺序。)

3.1 模块钩子(Module Hooks)

模块钩子允许我们在模块的输入或输出经过时进行监听。PyTorch 提供了两种模块钩子:

  • register_forward_hook:在前向传播时监听模块的输入和输出
  • register_backward_hook:在反向传播时监听模块的输入梯度和输出梯度
3.1.1 前向钩子(Forward Hook)

前向钩子是一个函数,它会在模块的前向传播完成后立即被调用。这个函数可以访问模块的输入和输出,但不能修改它们。

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plttorch.manual_seed(42)
np.random.seed(42)# 定义一个简单的卷积神经网络
class SimpleModel(nn.Module):def __init__(self):super(SimpleModel, self).__init__()# 定义卷积层:输入通道1,输出通道2,卷积核3x3,填充1保持尺寸不变self.conv = nn.Conv2d(1, 2, kernel_size=3, padding=1)# 定义ReLU激活函数self.relu = nn.ReLU()# 定义全连接层:输入特征2*4*4,输出10分类self.fc = nn.Linear(2 * 4 * 4, 10)def forward(self, x):# 卷积层x = self.conv(x)# ReLU激活x = self.relu(x)# 展平x = x.view(-1,2*4*4)# 全连接层x = self.fc(x)return x# 创建模型实例
model = SimpleModel()# 创建一个列表用于存储中间层的输出
conv_outputs = []# 定义前向钩子函数 - 用于在模型前向传播过程中获取中间层信息
def forward_hook(module, input, output):"""前向钩子函数,会在模块每次执行前向传播后被自动调用参数:module: 当前应用钩子的模块实例input: 传递给该模块的输入张量元组output: 该模块产生的输出张量"""print(f"钩子被调用!模块类型: {type(module)}")print(f"输入形状: {input[0].shape}") #  input是一个元组,对应 (image, label)print(f"输出形状: {output.shape}")# 保存卷积层的输出用于后续分析# 使用detach()避免追踪梯度,防止内存泄漏conv_outputs.append(output.detach())# 在卷积层注册前向钩子
# register_forward_hook返回一个句柄,用于后续移除钩子
hook_handle = model.conv.register_forward_hook(forward_hook) # 创建一个随机输入张量 (批次大小=1, 通道=1, 高度=4, 宽度=4)
x = torch.randn(1, 1, 4, 4)# 执行前向传播 - 此时会自动触发钩子函数
output = model(x)# 释放钩子 - 重要!防止在后续模型使用中持续调用钩子造成意外行为或内存泄漏
hook_handle.remove()# 设置中文字体支持
plt.rcParams["font.family"] = ["SimHei"]
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
# 让我们可视化卷积层的输出
if conv_outputs:plt.figure(figsize=(10, 5))# 原始输入图像plt.subplot(1, 3, 1)plt.title('输入图像')plt.imshow(x[0, 0].detach().numpy(), cmap='gray') # 显示灰度图像# 第一个卷积核的输出plt.subplot(1, 3, 2)plt.title('卷积核1输出')plt.imshow(conv_outputs[0][0, 0].detach().numpy(), cmap='gray')# 第二个卷积核的输出plt.subplot(1, 3, 3)plt.title('卷积核2输出')plt.imshow(conv_outputs[0][0, 1].detach().numpy(), cmap='gray')plt.tight_layout()plt.show()

3.1.2 反向钩子(Backward Hook)

反向钩子与前向钩子类似,但它是在反向传播过程中被调用的。反向钩子可以用来获取或修改梯度信息。

# 定义一个存储梯度的列表
conv_gradients = []# 定义反向钩子函数
def backward_hook(module, grad_input, grad_output):# 模块:当前应用钩子的模块# grad_input:模块输入的梯度# grad_output:模块输出的梯度print(f"反向钩子被调用!模块类型: {type(module)}")print(f"输入梯度数量: {len(grad_input)}")print(f"输出梯度数量: {len(grad_output)}")# 保存梯度供后续分析conv_gradients.append((grad_input, grad_output))# 在卷积层注册反向钩子
hook_handle = model.conv.register_backward_hook(backward_hook)# 创建一个随机输入并进行前向传播
x = torch.randn(1, 1, 4, 4, requires_grad=True)
output = model(x)# 定义一个简单的损失函数并进行反向传播
loss = output.sum()
loss.backward()# 释放钩子
hook_handle.remove()
3.2 张量钩子(Tensor Hook)

PyTorch 还提供了张量钩子,允许我们直接监听和修改张量的梯度。张量钩子有两种:

  • register_hook:用于监听张量的梯度
  • register_full_backward_hook:用于在完整的反向传播过程中监听张量的梯度(PyTorch 1.4+)
# 创建一个需要计算梯度的张量
x = torch.tensor([2.0], requires_grad=True)
y = x ** 2
z = y ** 3# 定义一个钩子函数,用于修改梯度
def tensor_hook(grad):print(f"原始梯度: {grad}")# 修改梯度,例如将梯度减半return grad / 2# 在y上注册钩子
hook_handle = y.register_hook(tensor_hook)# 计算梯度
z.backward()print(f"x的梯度: {x.grad}")# 释放钩子
hook_handle.remove()

∂z/∂z = 1.0

∂z/∂y = 3y² = 48.0 → 钩子修改为 24.0

∂z/∂x = (∂z/∂y) × (∂y/∂x) = 24.0 × 4.0 = 96.0

4、Grad-CAM的示例

Grad-CAM (Gradient-weighted Class Activation Mapping) 算法是一种强大的可视化技术,用于解释卷积神经网络 (CNN) 的决策过程。它通过计算特征图的梯度来生成类激活映射(Class Activation Mapping,简称 CAM ),直观地显示图像中哪些区域对模型的特定预测贡献最大。

Grad-CAM 的核心思想是:通过反向传播得到的梯度信息,来衡量每个特征图对目标类别的重要性。

1. 梯度信息:通过计算目标类别对特征图的梯度,得到每个特征图的重要性权重。

2. 特征加权:用这些权重对特征图进行加权求和,得到类激活映射。

3. 可视化:将激活映射叠加到原始图像上,高亮显示对预测最关键的区域。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image# 设置随机种子确保结果可复现
# 在深度学习中,随机种子可以让每次运行代码时,模型初始化参数、数据打乱等随机操作保持一致,方便调试和对比实验结果
torch.manual_seed(42)
np.random.seed(42)# 加载CIFAR-10数据集
# 定义数据预处理步骤,先将图像转换为张量,再进行归一化操作
# 归一化的均值和标准差是(0.5, 0.5, 0.5),这里的均值和标准差是对CIFAR-10数据集的经验值,使得数据分布更有利于模型训练
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])# 加载测试集,指定数据集根目录为'./data',设置为测试集(train=False),如果数据不存在则下载(download=True),并应用上述定义的预处理
testset = torchvision.datasets.CIFAR10(root='./cifar_data', train=False,download=True, transform=transform
)# 定义类别名称,CIFAR-10数据集包含这10个类别
classes = ('飞机', '汽车', '鸟', '猫', '鹿', '狗', '青蛙', '马', '船', '卡车')# 定义一个简单的CNN模型
class SimpleCNN(nn.Module):def __init__(self):super(SimpleCNN, self).__init__()# 第一个卷积层,输入通道为3(彩色图像),输出通道为32,卷积核大小为3x3,填充为1以保持图像尺寸不变self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)# 第二个卷积层,输入通道为32,输出通道为64,卷积核大小为3x3,填充为1self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)# 第三个卷积层,输入通道为64,输出通道为128,卷积核大小为3x3,填充为1self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)# 最大池化层,池化核大小为2x2,步长为2,用于下采样,减少数据量并提取主要特征self.pool = nn.MaxPool2d(2, 2)# 第一个全连接层,输入特征数为128 * 4 * 4(经过前面卷积和池化后的特征维度),输出为512self.fc1 = nn.Linear(128 * 4 * 4, 512)# 第二个全连接层,输入为512,输出为10(对应CIFAR-10的10个类别)self.fc2 = nn.Linear(512, 10)def forward(self, x):# 第一个卷积层后接ReLU激活函数和最大池化操作,经过池化后图像尺寸变为原来的一半,这里输出尺寸变为16x16x = self.pool(F.relu(self.conv1(x)))  # 第二个卷积层后接ReLU激活函数和最大池化操作,输出尺寸变为8x8x = self.pool(F.relu(self.conv2(x)))  # 第三个卷积层后接ReLU激活函数和最大池化操作,输出尺寸变为4x4x = self.pool(F.relu(self.conv3(x)))  # 将特征图展平为一维向量,以便输入到全连接层x = x.view(-1, 128 * 4 * 4)# 第一个全连接层后接ReLU激活函数x = F.relu(self.fc1(x))# 第二个全连接层输出分类结果x = self.fc2(x)return x# 初始化模型
model = SimpleCNN()
print("模型已创建")# 如果有GPU则使用GPU,将模型转移到对应的设备上
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)# 训练模型(简化版,实际应用中应该进行完整训练)
def train_model(model, epochs=1):# 加载训练集,指定数据集根目录为'./data',设置为训练集(train=True),如果数据不存在则下载(download=True),并应用前面定义的预处理trainset = torchvision.datasets.CIFAR10(root='./cifar_data', train=True,download=True, transform=transform)# 创建数据加载器,设置批量大小为64,打乱数据顺序(shuffle=True),使用2个线程加载数据trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,shuffle=True, num_workers=2)# 定义损失函数为交叉熵损失,用于分类任务criterion = nn.CrossEntropyLoss()# 定义优化器为Adam,用于更新模型参数,学习率设置为0.001optimizer = torch.optim.Adam(model.parameters(), lr=0.001)for epoch in range(epochs):running_loss = 0.0for i, data in enumerate(trainloader, 0):# 从数据加载器中获取图像和标签inputs, labels = data# 将图像和标签转移到对应的设备(GPU或CPU)上inputs, labels = inputs.to(device), labels.to(device)# 清空梯度,避免梯度累加optimizer.zero_grad()# 模型前向传播得到输出outputs = model(inputs)# 计算损失loss = criterion(outputs, labels)# 反向传播计算梯度loss.backward()# 更新模型参数optimizer.step()running_loss += loss.item()if i % 100 == 99:# 每100个批次打印一次平均损失print(f'[{epoch + 1}, {i + 1}] 损失: {running_loss / 100:.3f}')running_loss = 0.0print("训练完成")# 训练模型(可选,如果有预训练模型可以加载)
# 取消下面这行的注释来训练模型
# train_model(model, epochs=1)# 或者尝试加载预训练模型(如果存在)
try:# 尝试加载名为'cifar10_cnn.pth'的模型参数model.load_state_dict(torch.load('cifar10_cnn.pth'))print("已加载预训练模型")
except:print("无法加载预训练模型,使用未训练模型或训练新模型")# 如果没有预训练模型,可以在这里调用train_model函数train_model(model, epochs=1)# 保存训练后的模型参数torch.save(model.state_dict(), 'cifar10_cnn.pth')# 设置模型为评估模式,此时模型中的一些操作(如dropout、batchnorm等)会切换到评估状态
model.eval()# Grad-CAM实现
class GradCAM:def __init__(self, model, target_layer):self.model = modelself.target_layer = target_layerself.gradients = Noneself.activations = None# 注册钩子,用于获取目标层的前向传播输出和反向传播梯度self.register_hooks()def register_hooks(self):# 前向钩子函数,在目标层前向传播后被调用,保存目标层的输出(激活值)def forward_hook(module, input, output):self.activations = output.detach()# 反向钩子函数,在目标层反向传播后被调用,保存目标层的梯度def backward_hook(module, grad_input, grad_output):self.gradients = grad_output[0].detach()# 在目标层注册前向钩子和反向钩子self.target_layer.register_forward_hook(forward_hook)self.target_layer.register_backward_hook(backward_hook)def generate_cam(self, input_image, target_class=None):# 前向传播,得到模型输出model_output = self.model(input_image)if target_class is None:# 如果未指定目标类别,则取模型预测概率最大的类别作为目标类别target_class = torch.argmax(model_output, dim=1).item()# 清除模型梯度,避免之前的梯度影响self.model.zero_grad()# 反向传播,构造one-hot向量,使得目标类别对应的梯度为1,其余为0,然后进行反向传播计算梯度one_hot = torch.zeros_like(model_output)one_hot[0, target_class] = 1model_output.backward(gradient=one_hot)# 获取之前保存的目标层的梯度和激活值gradients = self.gradientsactivations = self.activations# 对梯度进行全局平均池化,得到每个通道的权重,用于衡量每个通道的重要性weights = torch.mean(gradients, dim=(2, 3), keepdim=True)# 加权激活映射,将权重与激活值相乘并求和,得到类激活映射的初步结果cam = torch.sum(weights * activations, dim=1, keepdim=True)# ReLU激活,只保留对目标类别有正贡献的区域,去除负贡献的影响cam = F.relu(cam)# 调整大小并归一化,将类激活映射调整为与输入图像相同的尺寸(32x32),并归一化到[0, 1]范围cam = F.interpolate(cam, size=(32, 32), mode='bilinear', align_corners=False)cam = cam - cam.min()cam = cam / cam.max() if cam.max() > 0 else camreturn cam.cpu().squeeze().numpy(), target_classimport warnings
warnings.filterwarnings("ignore")
import matplotlib.pyplot as plt
# 设置中文字体支持
plt.rcParams["font.family"] = ["SimHei"]
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
# 选择一个随机图像
# idx = np.random.randint(len(testset))
idx = 102  # 选择测试集中的第101张图片 (索引从0开始)
image, label = testset[idx]
print(f"选择的图像类别: {classes[label]}")# 转换图像以便可视化
def tensor_to_np(tensor):img = tensor.cpu().numpy().transpose(1, 2, 0)mean = np.array([0.5, 0.5, 0.5])std = np.array([0.5, 0.5, 0.5])img = std * img + meanimg = np.clip(img, 0, 1)return img# 添加批次维度并移动到设备
input_tensor = image.unsqueeze(0).to(device)# 初始化Grad-CAM(选择最后一个卷积层)
grad_cam = GradCAM(model, model.conv3)# 生成热力图
heatmap, pred_class = grad_cam.generate_cam(input_tensor)# 可视化
plt.figure(figsize=(12, 4))# 原始图像
plt.subplot(1, 3, 1)
plt.imshow(tensor_to_np(image))
plt.title(f"原始图像: {classes[label]}")
plt.axis('off')# 热力图
plt.subplot(1, 3, 2)
plt.imshow(heatmap, cmap='jet')
plt.title(f"Grad-CAM热力图: {classes[pred_class]}")
plt.axis('off')# 叠加的图像
plt.subplot(1, 3, 3)
img = tensor_to_np(image)
heatmap_resized = np.uint8(255 * heatmap)
heatmap_colored = plt.cm.jet(heatmap_resized)[:, :, :3]
superimposed_img = heatmap_colored * 0.4 + img * 0.6
plt.imshow(superimposed_img)
plt.title("叠加热力图")
plt.axis('off')plt.tight_layout()
plt.savefig('grad_cam_result.png')
plt.show()# print("Grad-CAM可视化完成。已保存为grad_cam_result.png")

http://www.hkcw.cn/article/HhUtpqLFfr.shtml

相关文章

Git远程操作

目录 1. 理解分布式版本控制系统 2. 远程仓库 3. 新建远程仓库 4. 克隆远程仓库 4.1 使用HTTPS方式: 4.2 使用SSH方式: 5. 向远程仓库推送 总结: 问题: 6. 拉取远程仓库 7. 配置Git 7.1 忽略特殊文件 8. 给命令配置别…

SolidWorks软件的安装与卸载

文章目录 软件的下载途径软件的安装软件的卸载 简介:这篇文章介绍了SolidWorks软件的安装与卸载,步骤是比较繁琐的,但照着步骤一步一步的来15分钟就能安装成功。这里要特别的注意一点的是,文件的安装位置一定要集中(别…

Python 验证码识别(使用pytesseract库)

文章目录 摘要1、安装Tesseract-OCR2、在python中使用安装依赖 3、本地图片识别4、结合playwright动态识别网站验证码 摘要 python中使用pytesseract库进行ocr识别,需要安装Tesseract-OCR,通过指定pytesseract.tesseract_cmd路径,可以将esser…

日志与策略模式

什么是设计模式 IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是 设计模式 日志认识 计算机中的日志是记录系统和软件运行中…

ToolsSet之:XML工具

ToolsSet是微软商店中的一款包含数十种实用工具数百种细分功能的工具集合应用,应用基本功能介绍可以查看以下文章: Windows应用ToolsSet介绍https://blog.csdn.net/BinField/article/details/145898264 ToolsSet中Text菜单下的XML Tool工具是一个Xml工…

2025年目前最新版本Android Studio自定义xml预览的屏幕分辨率

一、前言 在实际开发项目当中,我们的设备的分辨率可能会比较特殊,AS并没有自带这种屏幕分辨率的设备,但是我们又想一边编写XML界面,一边实时看到较为真实的预览效果,该怎么办呢?在早期的AS版本中&#xff…

sql知识梳理(超全,超详细,自用)

目录 通识 查询的基本语法 数据库(database)操作 表(table)的操作 表中列的操作 索引操作 表中行的操作 insert into语句 update语句 删除语句 select语句 表与表之间的关系 连接查询 子查询 视图 数据备份与还原 …

数据分析图表类型及其应用场景

说明:顶部HTML文件下载后可以直接查看,带有示图。 摘要 数据可视化作为现代数据分析的核心环节,旨在将复杂、抽象的数据转化为直观、易懂的图形形式。这种转化显著提升了业务决策能力,优化了销售与营销活动,开辟了新…

1、树莓派更换软件下载源

树莓派官方系统raspbian自带的是国外的软件源,在国内使用经常会遇到无法下载软件的问题。 以下是把raspbian系统(buster版本)的下载源改为阿里云软件源的方法。 1、修改sources.list文件 sudo nano /etc/apt/sources.list 将初始化中的代…

TDengine 集群容错与灾备

简介 为了防止数据丢失、误删操作,TDengine 提供全面的数据备份、恢复、容错、异地数据实时同步等功能,以保证数据存储的安全。本节简要说明 TDengine 中的容错与灾备。 容错 TDengine 支持 WAL 机制,实现数据的容错能力,保证数…

第十五章 访问控制

系列文章目录 第一章 总体概述 第二章 在实体机上安装ubuntu 第三章 Windows远程连接ubuntu 第四章 使用Docker安装和运行EMQX 第五章 Docker卸载EMQX 第六章 EMQX客户端MQTTX Desktop的安装与使用 第七章 EMQX客户端MQTTX CLI的安装与使用 第八章 Wireshark工具的安装与使用 …

LeetCode算法题 (搜索二维矩阵)Day18!!!C/C++

https://leetcode.cn/problems/search-a-2d-matrix/description/ 一、题目分析 给你一个满足下述两条属性的 m x n 整数矩阵: 每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target ,如果 ta…

基于谷歌ADK的智能客服系统简介

Google的智能体开发工具包(Agent Development Kit,简称ADK)是一个开源的、以代码为中心的Python工具包,旨在帮助开发者更轻松、更灵活地构建、评估和部署复杂的人工智能智能体(AI Agent)。ADK 是一个灵活的…

MySql(十三)

目录 mysql外键约束 准备工作 创建表 插入数据 创建表时添加外键 1..格式 2..创建表student表时,为其添加外键 3.插入数据测试 正常数据 异常数据 3.使用alter添加外键 删除外键 添加外键 4.Mysql外键不生效的原因 修改引擎 phpystudy的mysql位置 mysql外键约束 注&…

WEBSTORM前端 —— 第3章:移动 Web —— 第2节:空间转换、转化

目录 一、空间转换 1.空间转换 2.空间转换 – 平移 3.视距 perspective 4.空间 – 旋转 ③空间旋转——Z轴代码与效果视频 ④空间旋转——X轴代码与效果视频 ⑤空间旋转——Y轴代码与效果视频 5.立体呈现 – transform-style 案例 – 3D 导航 6.空间转换 – 缩放 …

【AI论文】R2R:通过小型与大型模型之间的令牌路由高效导航发散推理路径

摘要:大型语言模型(LLMs)以巨大的推理开销为代价,实现了令人印象深刻的推理能力,这带来了巨大的部署挑战。 尽管蒸馏的小语言模型(SLM)显著提高了效率,但由于它们无法遵循LLM的推理路…

学习日记-day20-6.1

完成目标&#xff1a; 知识点&#xff1a; 1.集合_Collections集合工具类 方法:static <T> boolean addAll(Collection<? super T> c, T... elements)->批量添加元素 static void shuffle(List<?> list) ->将集合中的元素顺序打乱static <T>…

区块链可投会议CCF B--EDBT 2026 截止10.8 附录用率

Conference&#xff1a;EDBT: 29th International Conference on Extending Database Technology CCF level&#xff1a;CCF B Categories&#xff1a;数据库&#xff0f;数据挖掘&#xff0f;内容检索 Year&#xff1a;2026 Conference time&#xff1a;24th March - 27th…

蓝光过滤APP:护眼小助手,守护您的视力健康

在数字时代&#xff0c;手机和平板电脑已成为我们生活中不可或缺的工具。无论是工作、学习还是娱乐&#xff0c;长时间使用这些设备已成为常态。然而&#xff0c;长时间盯着屏幕不仅会导致眼睛疲劳&#xff0c;还可能对视力造成不可逆的损害。蓝光过滤APP正是为了解决这一问题而…

AAA基础配置

文章目录 组网需求组网拓扑实验步骤测试结果配置文件 组网需求 为组网安全&#xff0c;经常会使用AAA技术&#xff0c;本次以CE12800交换机Window为例&#xff0c;实现AAA本地认证登录 组网拓扑 实验步骤 配置接口IP&#xff0c;连通终端进入AAA视图配置用户名密码配置账户权…