Pytorch的hook函数

hook函数是勾子函数,用于在不改变原始模型结构的情况下,注入一些新的代码用于调试和检验模型,常见的用法有保留非叶子结点的梯度数据(Pytorch的非叶子节点的梯度数据在计算完毕之后就会被删除,访问的时候会显示为None),又或者查看模型的层与层之间的数据传递情况(数据维度、数据大小等),抑或是在不修改原始模型代码的基础上可视化各个卷积特征图。

Pytorch提供了四种hook函数

  1. torch.tensor.register_hook(hooc_func)
  2. torch.nn.Module.register_forward_hook(hook_func)
  3. torch.nn.Module.register_forward_pre_hook(hook_func)
  4. torch.nn.Module.register_backward_hook

1. torch.tensor.register_hook(hooc_func)

解释:注册一个反向传播hook函数,其函数签名如下

def hook(grad):...

输入参数为张量的梯度,实现的hook函数可以在此修改梯度数据(原地修改或者通过返回值返回),或者在此将梯度数据保存、裁剪等。

示例 1

# leaf node data
x = torch.Tensor([0, 1, 2, 3]).requires_grad_()
y = torch.Tensor([4, 5, 6, 7]).requires_grad_()
w = torch.Tensor([1, 2, 3, 4]).requires_grad_()# intermediate variable
z = x + y# output 
o = torch.dot(w, z)# backward to calculate gradient
o.backward()# print gradient infomation
print('x.grad:', x.grad) # tensor([1., 2., 3., 4.])
print('y.grad:', y.grad) # tensor([1., 2., 3., 4.])
print('w.grad:', w.grad) # tensor([ 4.,  6.,  8., 10.])
print('z.grad:', z.grad) # None
print('o.grad:', o.grad) # None

输出:

x.grad: tensor([1., 2., 3., 4.])
y.grad: tensor([1., 2., 3., 4.])
w.grad: tensor([ 4.,  6.,  8., 10.])
z.grad: None
o.grad: None

可以看到代码中的非叶子节点z, o的梯度信息(grad)在计算之后立即被释放,因此都等于None,如果需要显式地声明需要保留非叶子节点的grad,需要使用retain_grad方法,如下例:

import torch 
a = torch.ones(5)
a.requires_grad = Trueb = 2*ab.retain_grad()   # 让非叶子节点b的梯度保持
c = b.mean()
c.backward()print(f'a.grad = {a.grad}\nb.grad = {b.grad}')

输出:

a.grad = tensor([0.4000, 0.4000, 0.4000, 0.4000, 0.4000])
b.grad = tensor([0.2000, 0.2000, 0.2000, 0.2000, 0.2000])

retain_grad()方法会增加显存的占用,我们可以使用hook获取梯度信息而不需要显式地使用retain_grad()强制系统保存梯度信息,如下例:

import torcha = torch.ones(5).requires_grad_()b = 2 * aa.register_hook(lambda x:print(f'a.grad = {x}'))
b.register_hook(lambda x: print(f'b.grad = {x}'))  c = b.mean()print('begin backward'.center(30, '-'))
c.backward()
print('end backward'.center(30, '-'))

输出:

--------begin backward--------
b.grad = tensor([0.2000, 0.2000, 0.2000, 0.2000, 0.2000])
a.grad = tensor([0.4000, 0.4000, 0.4000, 0.4000, 0.4000])
---------end backward---------

上述例子中我们使用hooktensorgrad进行访问,没有使用retain_grad对信息进行保存。输出结果表明,hook执行的时间是在backward之间,从后往前依次执行,首先输出bgrad,然后输出agrad,最后结束backward过程。

上述过程都没有对梯度信息进行改变,其实,如果hook函数的有返回值或者将输入参数grad原地进行修改的话,那么之后的梯度信息都会被改变,这一机制简直就是为梯度裁剪量身定制的。

如下例:

import torchdef hook(grad):torch.clamp_(grad, min=0.5, max=0.2)print(grad)a = torch.ones(5).requires_grad_()
b = 2 * aa.register_hook(hook)
b.register_hook(hook)  c = b.mean()print('begin backward'.center(30, '-'))
c.backward()
print('end backward'.center(30, '-'))

输出:

--------begin backward--------
tensor([0.2000, 0.2000, 0.2000, 0.2000, 0.2000])
tensor([0.2000, 0.2000, 0.2000, 0.2000, 0.2000])
---------end backward---------

对比上一例可以发现a的梯度从0.4被裁剪到了0.2,这里使用的clamp_是直接原地修改,所以不需要返回值。

也可将上述例子中的hook更改为有返回值的函数,效果相同。

部分例子参考:https://zhuanlan.zhihu.com/p/662760483

2. torch.nn.Module.register_forward_hook(hook_func)

除了register_hook是对tensor操作的hook之外,其他的hook都是对module进行操作的,这里的module包括各种layer,例如:Conv2d, Linear

register_forward_hook在执行moduleforward函数之后执行,其函数签名为

def hook(module, inputs, outpus):pass

注意:这里的module是当前被注册的moduleinputs是执行forward之前的inputs,而outputs则是执行forward之后的outputs ,这么设计可能是为了方便读取执行之前的intputs

如下例所示:

import torch
import torch.nn as nn# 定义一个简单的模块
class MyModule(nn.Module):def forward(self, x):print('forward'.center(20, '-'))return x * 2  # 假设这个模块简单地将输入乘以2# 创建模块实例
module = MyModule()# 定义一个hook函数,它接受输入和输出作为参数
def my_hook(module, input, output):print(f"Input: {input}")print(f"Output: {output}")# 注册hook函数
module.register_forward_hook(my_hook)# 创建一个输入张量
input_tensor = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)# 执行前向传播,这将触发hook函数的调用
output_tensor = module(input_tensor)

输出:

------forward-------
Input: (tensor([1., 2., 3.], requires_grad=True),)
Output: tensor([2., 4., 6.], grad_fn=<MulBackward0>)

从中我们可以看到,这里的Input还是执行forward之前的input,但是outputs是执行forward之后的outputs,从打印的------forward-------位置可以知道,这里的forward函数是在执行之后调用的hook

我们可以使用hook实现torchsummary类似的功能,查看resnet18的各个层的输出情况,如下例

import torch
from torch import nn
from torchvision.models import resnet18class Visualize(nn.Module):def __init__(self, model) -> None:super().__init__()self.model = model# Register a hook for each layerfor name, layer in self.model.named_children():# add a property dynamicallylayer.name = name# module.name is the newly added propertylayer.register_forward_hook(lambda module, inputs, outputs:print(f"{module.name}".ljust(10), '-->', f'{outputs.shape}'))def forward(self, x):return self.model(x)model = resnet18()
inputs = torch.randn(1, 3, 224, 224)
vis = Visualize(model)
output = vis(inputs)

输出:

conv1      --> torch.Size([1, 64, 112, 112])
bn1        --> torch.Size([1, 64, 112, 112])
relu       --> torch.Size([1, 64, 112, 112])
maxpool    --> torch.Size([1, 64, 56, 56])
layer1     --> torch.Size([1, 64, 56, 56])
layer2     --> torch.Size([1, 128, 28, 28])
layer3     --> torch.Size([1, 256, 14, 14])
layer4     --> torch.Size([1, 512, 7, 7])
avgpool    --> torch.Size([1, 512, 1, 1])
fc         --> torch.Size([1, 1000])

如果使用使用applyhook进行注册,apply会递归地将model里面的所有layer都进行相同的操作,于是结果就和for name, layer in self.model.named_modules()类似。

import torch
from torch import nn
from torchvision.models import resnet18def hook(module, inputs, outputs):print(module.__class__.__name__.ljust(10), end='')print(outputs.shape)def register(module):if isinstance(module, nn.Conv2d):module.register_forward_hook(hook)model = resnet18()
inputs = torch.randn(1, 3, 224, 224)
# 这里的apply会递归地把所有层都遍历,因此register_forward_hook注册到的层
# 是所有的Conv2d,包括子层,子层中的子层...
model.apply(register)
outputs = model(inputs)

输出为:

Conv2d    torch.Size([1, 64, 112, 112])
Conv2d    torch.Size([1, 64, 56, 56])
Conv2d    torch.Size([1, 64, 56, 56])
Conv2d    torch.Size([1, 64, 56, 56])
Conv2d    torch.Size([1, 64, 56, 56])
Conv2d    torch.Size([1, 128, 28, 28])
Conv2d    torch.Size([1, 128, 28, 28])
Conv2d    torch.Size([1, 128, 28, 28])
Conv2d    torch.Size([1, 128, 28, 28])
Conv2d    torch.Size([1, 128, 28, 28])
Conv2d    torch.Size([1, 256, 14, 14])
Conv2d    torch.Size([1, 256, 14, 14])
Conv2d    torch.Size([1, 256, 14, 14])
Conv2d    torch.Size([1, 256, 14, 14])
Conv2d    torch.Size([1, 256, 14, 14])
Conv2d    torch.Size([1, 512, 7, 7])
Conv2d    torch.Size([1, 512, 7, 7])
Conv2d    torch.Size([1, 512, 7, 7])
Conv2d    torch.Size([1, 512, 7, 7])
Conv2d    torch.Size([1, 512, 7, 7])

apply将所有的Conv2d都注册了,所以输出了所有的Conv2d的输出shape

3.torch.nn.Module.register_backward_hook

在了解了前一个hook的用法之后,这个hook的作用也就不言而喻了,在backward之后执行,这里的hook函数签名如下

def hook_fn(module, grad_in, grad_out):pass

输入参数包括三个,分别是modulegrad_ingrad_out,其中,grad_ingrad_out分别指代当前模块的输入和输出的梯度信息,若grad_ingrad_out包括多个输入输出,则grad_ingrad_out以元组形式呈现。

现在使用会register_backward_hook爆出警告:

module.py:1352: UserWarning: Using a non-full backward hook when the forward contains multiple autograd Nodes is deprecated and will be removed in future versions. This hook will be missing some grad_input. Please use register_full_backward_hook to get the documented behavior.warnings.warn("Using a non-full backward hook when the forward contains multiple autograd Nodes "

解决办法就是使用新的hook函数register_full_backward_hook,新的hook函数功能更加强大,不仅仅包括模块的输入输出梯度信息,还包括内部的一些其他变量的梯度信息,但是register_backward_hookregister_full_backward_hook两者之间的兼容性并不是很完美。

示例

import torch
from torch import nn
from torchvision.models import resnet18def hook_fn(module, grad_in, grad_out):# 当前module的输入和输出梯度# 若module有多个输入,则grad_in为一个元组# y = wx+bprint(module.__class__.__name__)print("------------Input Grad------------")# 容错处理,部分元组中的变量会是Nonefor grad in grad_in:try:print(grad.shape)except AttributeError: print ("None found for Gradient")print("------------Output Grad------------")for grad in grad_out:  try:print(grad.shape)except AttributeError: print ("None found for Gradient")print("\n")net = resnet18()
for name, layer in net.named_children():# 每一个大的子层都注册一个勾子函数layer.register_backward_hook(hook_fn)# 为了能够执行backward,构建一些虚拟的输入输出
dummy_inputs = torch.randn(10, 3, 224, 224)
dummy_labels = torch.randint(0, 1001, (10, ))
loss_fn = nn.CrossEntropyLoss()y_hat = net(dummy_inputs)loss = loss_fn(y_hat, dummy_labels)
loss.backward()

输出:

module.py:1352: UserWarning: Using a non-full backward hook when the forward contains multiple autograd Nodes is deprecated and will be removed in future versions. This hook will be missing some grad_input. Please use register_full_backward_hook to get the documented behavior.warnings.warn("Using a non-full backward hook when the forward contains multiple autograd Nodes "Linear
------------Input Grad------------
torch.Size([1000])
torch.Size([10, 512])
torch.Size([512, 1000])
------------Output Grad------------
torch.Size([10, 1000])AdaptiveAvgPool2d
------------Input Grad------------
torch.Size([10, 512, 7, 7])
------------Output Grad------------
torch.Size([10, 512, 1, 1])Sequential
------------Input Grad------------
torch.Size([10, 512, 7, 7])
------------Output Grad------------
torch.Size([10, 512, 7, 7])Sequential
------------Input Grad------------
torch.Size([10, 256, 14, 14])
------------Output Grad------------
torch.Size([10, 256, 14, 14])Sequential
------------Input Grad------------
torch.Size([10, 128, 28, 28])
------------Output Grad------------
torch.Size([10, 128, 28, 28])Sequential
------------Input Grad------------
torch.Size([10, 64, 56, 56])
------------Output Grad------------
torch.Size([10, 64, 56, 56])MaxPool2d
------------Input Grad------------
torch.Size([10, 64, 112, 112])
------------Output Grad------------
torch.Size([10, 64, 56, 56])ReLU
------------Input Grad------------
torch.Size([10, 64, 112, 112])
------------Output Grad------------
torch.Size([10, 64, 112, 112])BatchNorm2d
------------Input Grad------------
torch.Size([10, 64, 112, 112])
torch.Size([64])
torch.Size([64])
------------Output Grad------------
torch.Size([10, 64, 112, 112])Conv2d
------------Input Grad------------
None found for Gradient
torch.Size([64, 3, 7, 7])
None found for Gradient
------------Output Grad------------
torch.Size([10, 64, 112, 112])

最上面是警告信息可以忽略,然后根据backward的路径,从后往前进行返回。

使用如下代码查看resnet18的层级情况:

for name, layer in net.named_children():print(name)

输出:

conv1
bn1
relu
maxpool
layer1
layer2
layer3
layer4
avgpool
fc

可以看到这里的10个层对应上面hook函数返回的10个层。

综合以上两个部分,用一个示例演示同时构建前向和后向勾子函数:

import torch
import torch.nn as nn# 前向钩子示例
def forward_hook(module, input, output):print("{} forward hook:".format(module.__class__.__name__))print("Input:", input)print("Output:", output)print("")# 反向钩子示例
def backward_hook(module, grad_input, grad_output):print("{} backward hook:".format(module.__class__.__name__))print("Gradient input:")for item in grad_input:if item is not None:print(item.shape)print("Gradient output:")for item in grad_output:if item is not None:print(item.shape)print("")# 示例模型
class SimpleModel(nn.Module):def __init__(self):super(SimpleModel, self).__init__()self.fc1 = nn.Linear(10, 20)self.fc2 = nn.Linear(20, 1)def forward(self, x):x = torch.relu(self.fc1(x))x = self.fc2(x)return x# 示例
model = SimpleModel()# 注册前向钩子
hook_handle = model.fc1.register_forward_hook(forward_hook)# 注册反向钩子
hook_handle2 = model.fc1.register_backward_hook(backward_hook)# 示例输入数据
input_data = torch.randn(1, 10)# 前向传播
output = model(input_data)# 反向传播
loss = output.sum()
loss.backward()# 移除钩子
hook_handle.remove()
hook_handle2.remove()

输出:

Linear forward hook:
Input: (tensor([[-1.6549, -1.1471, -0.2341,  0.1456,  0.6528, -1.0562,  0.1078,  0.9752,0.8794,  1.0463]]),)
Output: tensor([[-0.6406,  0.0515,  0.1893, -0.5211, -0.2393,  0.2923,  0.0143,  0.6929,-0.4688, -0.1708, -0.6461,  0.5460, -0.1515, -0.1707, -0.5409, -0.6382,-0.9836,  0.3446,  0.2147, -0.7682]], grad_fn=<AddmmBackward0>)Linear backward hook:
Gradient input:
torch.Size([20])
torch.Size([10, 20])
Gradient output:
torch.Size([1, 20])

使用hook机制可视化resnet的特征图输出

import cv2
from torchvision import transforms
from torchvision.models import ResNet18_Weights, resnet18
import torch
import matplotlib.pyplot as pltdef viz(name):def imshow(module, input, output):feature_maps = input[0]# feature map dimension:# (batch_size, ch, width, height)# visualize 4 channels at mostmax_ch = min(feature_maps.size(1), 4)imgs = feature_maps[0, :max_ch, :, :]# print(imgs.shape)plt.figure(figsize=(12, 2))for i, img in enumerate(imgs):plt.subplot(1, 4, i+1)# plt.imshow(img.cpu(), cmap='gray')plt.imshow(img.cpu())plt.axis('off')if i == 0:plt.title(name)plt.show()return imshowdef main():trans = transforms.Compose([transforms.ToPILImage(),transforms.Resize((224, 224)),transforms.ToTensor(),transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])])device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")model = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1).to(device)for name, module in model.named_modules():# 这里只对卷积层的feature map进行显示if isinstance(module, torch.nn.Conv2d):module.register_forward_hook(viz(name))img = cv2.imread(r'faces\ftw1.jpg')img = trans(img).unsqueeze(0).to(device)with torch.no_grad():model(img)main()

输出示例:

在这里插入图片描述
在这里插入图片描述

总结:

  1. 勾子函数可以在不修改源代码的情况下实现功能的注入
  2. 实现过程需要重写对应的勾子函数,需要注意执行的顺序以及参数的含义
    • register_forward_hook:在forward函数之后执行,输入参数为inputoutput,其中inputforward函数之前的输入,outputforwad函数之后的输入。这个勾子函数一般用于可视化特征图
    • register_backward_hook:在执行backward之时执行,backward到哪一个层就执行哪一个层的勾子函数,需要注意的是,输入参数分别为当前层的梯度输入和梯度输出,也即grad_input, grad_output,再者,使用该函数不能有原地修改的操作,否则会报异常。

参考内容

  • 一文搞懂PyTorch Hook
  • Pytorch官方文档
    PyTorch Hook用法解析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/290145.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第二证券|刚刚,巨象突袭!发生了什么?

又是观看“巨象”表演的一天&#xff01; 今日早盘&#xff0c;A股总市值排前14的股票无一杀跌&#xff0c;三桶油更是集体拉升&#xff0c;我国石油涨超4%&#xff0c;我国海油涨近3%&#xff0c;我国石化涨超2%。排在后面的我国移动、我国电信等股价表现和走势也相当不错。 …

OneNote 中的云端字典在哪里? RoamingCustom.dic 在哪里?

早期懵懂不知&#xff0c;使用 OneNote 时直接将所有标红的单词添加到字典中了。后面才发现默认会添加到云端字典中。因为云端字典是存储在云端上的&#xff0c;所以 onenote 没有给出路径&#xff1a; 难道没办法获取了吗&#xff1f;有的&#xff01; 方案如下&#xff1a;考…

动态多态的注意事项

大家好&#xff1a; 衷心希望各位点赞。 您的问题请留在评论区&#xff0c;我会及时回答。 多态的基本概念 多态是C面向对象三大特性之一&#xff08;多态、继承、封装&#xff09; 多态分为两类&#xff1a; 静态多态&#xff1a;函数重载和运算符重载属于静态多态&#x…

php 快速入门(七)

一、操作数据库 1.1 操作MySQL的步骤 第一步&#xff1a;登录MySQL服务器 第二步&#xff1a;选择当前数据库 第三步&#xff1a;设置请求数据的字符集 第四步&#xff1a;执行SQL语句 1.2 连接MySQL 函数1&#xff1a;mysql_connect() 功能&#xff1a;连接&#xff08;登录…

八大技术趋势案例(人工智能物联网)

科技巨变,未来已来,八大技术趋势引领数字化时代。信息技术的迅猛发展,深刻改变了我们的生活、工作和生产方式。人工智能、物联网、云计算、大数据、虚拟现实、增强现实、区块链、量子计算等新兴技术在各行各业得到广泛应用,为各个领域带来了新的活力和变革。 为了更好地了解…

文件上传失败原因分析与解决

图片文件上传失败 问题描述&#xff1a;在前端开发时&#xff0c;需要通过表单元素上传图片或其他文本&#xff0c;但是上传不成功&#xff0c;后端接口也没问题 html <!--onChange用来绑定数据 handleUpload用来提交数据--><form onSubmit{handleUpload}><…

联想 lenovoTab 拯救者平板 Y700 二代_TB320FC原厂ZUI_15.0.677 firmware 线刷包9008固件ROM root方法

联想 lenovoTab 拯救者平板 Y700 二代_TB320FC原厂ZUI_15.0.677 firmware 线刷包9008固件ROM root方法 ro.vendor.config.lgsi.market_name拯救者平板 Y700 ro.vendor.config.lgsi.en.market_nameLegion Tab Y700 #ro.vendor.config.lgsi.short_market_name联想平板 ZUI T # B…

图论之路径条数专题

一直忙着金工实习蓝桥杯&#xff0c;好久没有看图论了&#xff0c;今天就小试几题享受下被虐的快感。 1.最短路拓扑 首先来几个结论&#xff1a; 1.最短路图没有环&#xff08;可以用反证法证明&#xff09; 2.dis[u]edge[u,v]dis[v]&#xff0c;那么u,v端点的边一定在最短路…

【Redis】redis主从复制

概述 常见的Redis高可用的方案包括持久化、主从复制&#xff08;及读写分离&#xff09;、哨兵和集群。其中持久化侧重解决的是Redis数据的单机备份问题&#xff08;从内存到硬盘的备份&#xff09;&#xff1b;而主从复制则侧重解决数据的多机热备。此外&#xff0c;主从复制…

AI浸入社交领域,泛娱乐APP如何抓住新风口?

2023年是大模型技术蓬勃发展的一年&#xff0c;自ChatGPT以惊艳姿态亮相以来&#xff0c;同年年底多模态大模型技术在国内及全球范围内的全面爆发&#xff0c;即模型能够理解并生成包括文本、图像、视频、音频等多种类型的内容。例如&#xff0c;基于大模型的文本到图像生成工具…

FPGA 图像边缘检测(Canny算子)

1 顶层代码 timescale 1ns / 1ps //边缘检测二阶微分算子&#xff1a;canny算子module image_canny_edge_detect (input clk,input reset, //复位高电平有效input [10:0] img_width,input [ 9:0] img_height,input [ 7:0] low_threshold,input [ 7:0] high_threshold,input va…

2024.3.28学习笔记

今日学习韩顺平java0200_韩顺平Java_对象机制练习_哔哩哔哩_bilibili 今日学习p286-p294 继承 继承可以解决代码复用&#xff0c;让我们的编程更加靠近人类思维&#xff0c;当多个类存在相同的属性和方法时&#xff0c;可以从这些类中抽象出父类&#xff0c;在父类中定义这些…

从人工智能入门到理解ChatGPT的原理与架构的第一天(First)(含机器学习特征工程详解)

目录 一.ChatGPT的发展历程 二.Attention is all you need 三.对于GPT-4的智能水平评估 四.大语言模型的技术演化 1.从符号主义到连接主义 2.特征工程 2.1数据探索 2.2数据清洗 2.3数据预处理 2.3.1无量纲化 2.3.1.1标准化 2.3.1.2区间缩放法 2.3.1.3标准化与归一…

42 ajax 下载文件未配置 responseType blob 导致的文件异常

前言 这是一个最近的关于文件下载碰到的一个问题 主要的情况是, 基于 xhr 发送请求, 获取下载的文件 然后 之后 xhr 这边拿到 字节序列之后, 封装 blob 来进行下载 然后 最开始我们这边没有配置 responseType 为 blob, arraybuffer, 然后 导致下载出来的 文件大小超过了…

语音模块摄像头模块阿里云结合,实现垃圾的智能识别

语音模块&摄像头模块&阿里云结合 文章目录 语音模块&摄像头模块&阿里云结合1、实现的功能2、配置2.1 软件环境2.2 硬件配置 3、程序介绍3.1 程序概况3.2 语言模块SDK配置介绍3.3 程序文件3.3.1 开启摄像头的程序3.3.2 云端识别函数( Py > C ) & 串口程序…

Flask学习(六):蓝图(Blueprint)

蓝图&#xff08;Blueprint&#xff09;&#xff1a;将各个业务进行区分&#xff0c;然后每一个业务单元可以独立维护&#xff0c;Blueprint可以单独具有自己的模板、静态文件或者其它的通用操作方法&#xff0c;它并不是必须要实现应用的视图和函数的。 Demo目录结构&#xf…

基于js css的瀑布流demo

要实现照片按照瀑布流展示&#xff0c;写个小demo&#xff0c;记录下。 瀑布流实现思路如下&#xff1a; CSS 弹性布局对 3 列按横向排列&#xff0c;对每一列内部按纵向排列 html代码&#xff1a; <div class"content"></div> css代码&#xff1a; …

2.4 比较检验 机器学习

目录 常见比较检验方法 总述 2.4.1 假设检验 2.4.2 交叉验证T检验 2.4.3 McNemar 检验 接我们的上一篇《性能度量》&#xff0c;那么我们在某种度量下取得评估结果后&#xff0c;是否可以直接比较以评判优劣呢&#xff1f;实际上是不可以的。因为我们第一&#xff0c;测试…

Unity 实现鼠标左键进行射击

发射脚本实现思路 分析 确定用户交互方式&#xff1a;通过鼠标左键点击发射子弹。确定子弹发射逻辑&#xff1a;每次点击后有一定时间间隔才能再次发射。确定子弹发射源和方向&#xff1a;子弹从枪口&#xff08;Transform&#xff09;位置发射&#xff0c;沿枪口方向前进。 变…

iOS客户端自动化UI自动化airtest+appium从0到1搭建macos+脚本设计demo演示+全网最全最详细保姆级有步骤有图

Android客户端自动化UI自动化airtest从0到1搭建macos脚本设计demo演示全网最全最详细保姆级有步骤有图-CSDN博客 避坑系列-必读&#xff1a; 不要安装iOS-Tagent &#xff0c;安装appium -这2个性质其实是差不多的都是为了安装wda。注意安装appium最新版本&#xff0c;安装完…