【深度学习】PyTorch框架(2):激活函数

1.引言

在文中,我们将深入探讨流行的激活函数,并分析它们在神经网络优化特性中的作用。激活函数在深度学习模型中扮演着至关重要的角色,因为它们为网络引入了非线性特性。尽管文献中描述了众多的激活函数,但它们并非一视同仁,各有优势。本文旨在阐明恰当选择激活函数的重要性,展示正确选择激活函数的方法,并讨论若不这样做可能引发的问题。

在我们深入讨论之前,首先导入标准库并构建基础功能。

# 导入os库,用于与操作系统交互,如文件路径操作  
import os  
# 导入json库,用于处理JSON数据格式  
import json  
# 导入math库,提供了数学函数的实现  
import math  
# 导入numpy库,一个强大的数学库,支持大量的维度数组与矩阵运算  
import numpy as np
# 导入matplotlib.pyplot用于绘图  
import matplotlib.pyplot as plt  
# 设置matplotlib为内联显示模式,主要用于Jupyter Notebook中  
%matplotlib inline  
# 设置matplotlib的输出格式为svg和pdf,以便于高质量地导出图表  
from IPython.display import set_matplotlib_formats  
set_matplotlib_formats('svg', 'pdf')  
# 导入seaborn库,一个基于matplotlib的高级绘图库,提供美观的统计图形  
import seaborn as sns  
# 设置seaborn的默认样式  
sns.set()
# 从tqdm.notebook导入tqdm,用于在Jupyter Notebook中显示进度条  
from tqdm.notebook import tqdm
# 导入PyTorch库,一个流行的深度学习框架  
import torch  
# 导入torch.nn模块,提供了构建神经网络所需的所有组件  
import torch.nn as nn  
# 导入torch.nn.functional模块,提供了神经网络中常用的激活函数等  
import torch.nn.functional as F  
# 导入torch.utils.data模块,提供了加载数据的工具,如Dataset和DataLoader  
import torch.utils.data as data  
# 导入torch.optim模块,提供了各种优化算法,如SGD、Adam等  
import torch.optim as optim

在本文中,我们将创建一个函数,用于为我们在本教程中可能用到的所有库(这里是NumPy和PyTorch)设置随机种子。这样做的目的是确保我们的训练过程是可复现的。不过,需要指出的是,与在CPU上操作不同,即使设置了相同的随机种子,在不同的GPU架构上可能会得到不同的训练结果。本文中讨论的所有模型都是在NVIDIA GTX1080Ti GPU上进行训练的。

此外,接下来的代码单元定义了两个关键路径变量:DATASET_PATHCHECKPOINT_PATHDATASET_PATH是用于存储我们在教程中使用的下载数据集的目录。为了避免重复下载,建议将所有PyTorch数据集存储在一个统一的目录中。CHECKPOINT_PATH则是用于保存训练好的模型权重和其它相关文件的地方。所需的文件将会自动下载到指定位置。

# 定义数据集存放路径,下载的数据集(例如MNIST)将被保存在此文件夹
DATASET_PATH = "../data"
# 定义预训练模型存放路径,训练好的模型将被保存在此文件夹
CHECKPOINT_PATH = "../saved_models/tutorial3"# 设置随机种子的函数,确保实验的可重复性
def set_seed(seed):# 为numpy设置随机种子np.random.seed(seed)# 为PyTorch设置随机种子torch.manual_seed(seed)# 如果GPU可用,则为GPU操作设置单独的种子if torch.cuda.is_available():torch.cuda.manual_seed(seed)torch.cuda.manual_seed_all(seed)
# 调用函数设置随机种子为42
set_seed(42)# 由于GPU上一些操作为提高效率而采用随机实现,
# 我们需要确保如果使用GPU,则所有操作都是确定性的,以保证结果的复现性
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False# 获取将在本教程中使用的设备,如果GPU可用则使用GPU,否则使用CPU
device = torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:0")
# 打印出所使用的设备
print("Using device", device)

在本文中,我们将使用一系列预训练的模型。为了减小存储库的大小,特别是为了方便在ReadTheDocs上构建文档,这些模型文件被存储在了一个单独的仓库中。现在,下面的单元格将会尝试下载这些预训练模型。

import urllib.request  # 导入urllib.request模块,用于请求网络资源
from urllib.error import HTTPError  # 导入HTTPError,用于捕获HTTP请求过程中的错误# 定义教程中预训练模型的存储Github URL基础路径
base_url = "https://raw.githubusercontent.com/phlippe/saved_models/main/tutorial3/" 
# 定义需要下载的预训练模型文件列表
pretrained_files = ["FashionMNIST_elu.config", "FashionMNIST_elu.tar","FashionMNIST_leakyrelu.config", "FashionMNIST_leakyrelu.tar","FashionMNIST_relu.config", "FashionMNIST_relu.tar","FashionMNIST_sigmoid.config", "FashionMNIST_sigmoid.tar","FashionMNIST_swish.config", "FashionMNIST_swish.tar","FashionMNIST_tanh.config", "FashionMNIST_tanh.tar"
]# 如果检查点路径不存在,则创建它
os.makedirs(CHECKPOINT_PATH, exist_ok=True)# 遍历每个文件名,检查文件是否已经存在;如果不存在,尝试下载
for file_name in pretrained_files:file_path = os.path.join(CHECKPOINT_PATH, file_name)  # 完整的文件路径if not os.path.isfile(file_path):  # 如果文件不存在file_url = base_url + file_name  # 完整的文件URLprint(f"Downloading {file_url}...")  # 打印下载信息try:urllib.request.urlretrieve(file_url, file_path)  # 尝试下载文件except HTTPError as e:  # 如果下载过程中出现HTTP错误print("下载过程中出现问题。请尝试从其他途径下载文件,或者联系作者,并附上完整的错误输出,包括以下错误信息:\n", e)

2.常见激活函数深度解析

在本文中,我们将探索几种常见的激活函数,并亲手实现它们。尽管这些函数大多可以在PyTorch的torch.nn模块中找到,但我们自己实现这些函数,可以更深入地理解其原理和应用。

为了便于后续比较不同激活函数的特性,我们首先定义一个基类ActivationFunction,后续的激活函数类都将继承自此基类。

class ActivationFunction(nn.Module):def __init__(self):super().__init__()self.name = self.__class__.__name__self.config = {"name": self.name}

2.1.激活函数实现

我们将实现几个至今仍被广泛使用的“古老”激活函数:Sigmoid和Tanh。这两种激活函数在PyTorch中已有内置的函数和模块形式,但我们仍将手动实现它们,以加强理解。

class Sigmoid(ActivationFunction):def forward(self, x):return 1 / (1 + torch.exp(-x))class Tanh(ActivationFunction):def forward(self, x):x_exp, neg_x_exp = torch.exp(x), torch.exp(-x)return (x_exp - neg_x_exp) / (x_exp + neg_x_exp)

2.2.ReLU及其变体

近年来,随着深度学习网络的发展,ReLU(修正线性单元)激活函数因其在大范围值内提供稳定梯度的强大优势而广受欢迎。我们将实现几种ReLU的变体:LeakyReLU、ELU和Swish。

  • LeakyReLU在负值部分用较小的斜率替代了零,允许梯度在输入的这部分流动。
  • ELU用指数衰减替换了负值部分。
  • Swish是最近提出的激活函数,它平滑且非单调,有助于解决深层网络中的“死神经元”问题。

下面是这些激活函数的具体实现:

class ReLU(ActivationFunction):def forward(self, x):return x * (x > 0).float()class LeakyReLU(ActivationFunction):def __init__(self, alpha=0.1):super().__init__()self.config["alpha"] = alphadef forward(self, x):return torch.where(x > 0, x, self.config["alpha"] * x)class ELU(ActivationFunction):def forward(self, x):return torch.where(x > 0, x, torch.exp(x)-1)class Swish(ActivationFunction):def forward(self, x):return x * torch.sigmoid(x)

2.3.激活函数字典

为了方便以后使用,我们将实现的激活函数汇总到一个字典中,以名称映射到类对象。如果您实现了新的激活函数,也可以将其添加到此字典中,以便在未来的比较中使用。

act_fn_by_name = {"sigmoid": Sigmoid,"tanh": Tanh,"relu": ReLU,"leakyrelu": LeakyReLU,"elu": ELU,"swish": Swish
}

以上就是对常见激活函数的深度解析和实现。通过这些示例,您可以更深入地理解每个激活函数的特性,并根据需要选择适合您模型的激活函数。

2.4.激活函数的可视化

为了直观了解每种激活函数的实质作用,我们将在下文中对它们进行可视化。除了实际的激活值外,函数的梯度也是一个重要方面,因为它对神经网络的优化至关重要。PyTorch允许我们通过简单地调用backward函数来计算梯度:

def get_grads(act_fn, x):"""计算指定位置的激活函数的梯度。输入:act_fn - 具有实现前向传播的"ActivationFunction"类的对象。x - 1D输入张量。输出:一个与x大小相同的张量,包含在x处的act_fn的梯度。"""x = x.clone().requires_grad_() # 将输入标记为需要存储梯度的张量out = act_fn(x)out.sum().backward() # 求和导致梯度在x的每个元素中均匀流动return x.grad # 通过"x.grad"访问x的梯度

现在我们可以可视化我们所有的激活函数,包括它们的梯度:

def vis_act_fn(act_fn, ax, x):# 运行激活函数y = act_fn(x)y_grads = get_grads(act_fn, x)# 将x,y和梯度推回cpu以进行绘图x, y, y_grads = x.cpu().numpy(), y.cpu().numpy(), y_grads.cpu().numpy()# 绘图ax.plot(x, y, linewidth=2, label="ActFn")ax.plot(x, y_grads, linewidth=2, label="Gradient")ax.set_title(act_fn.name)ax.legend()ax.set_ylim(-1.5, x.max())# 如果需要添加激活函数
act_fns = [act_fn() for act_fn in act_fn_by_name.values()]
x = torch.linspace(-5, 5, 1000) # 我们想要可视化激活函数的范围
# 绘图
rows = math.ceil(len(act_fns)/2.0)
fig, ax = plt.subplots(rows, 2, figsize=(8, rows*4))
for i, act_fn in enumerate(act_fns):vis_act_fn(act_fn, ax[divmod(i,2)], x)
fig.subplots_adjust(hspace=0.3)
plt.show()

在这里插入图片描述

3.分析激活函数的效果

在实现和可视化激活函数之后,我们的目标是深入了解它们的效果。我们通过使用一个简单的神经网络,在FashionMNIST上进行训练,并检查模型的各个方面,包括性能和梯度流动。

3.1.配置

首先,让我们配置神经网络。所选网络将图像视为1D张量,并通过一系列线性层和指定的激活函数进行处理。请随时尝试其他网络架构。

class BaseNetwork(nn.Module):def __init__(self, act_fn, input_size=784, num_classes=10, hidden_sizes=[512, 256, 256, 128]):"""输入:act_fn - 应在网络中用作非线性的激活函数对象。input_size - 输入图像的像素大小num_classes - 我们想要预测的类别数量hidden_sizes - 指定神经网络中隐藏层大小的整数列表"""super().__init__()# 根据指定的隐藏尺寸创建网络layers = []layer_sizes = [input_size] + hidden_sizesfor layer_index in range(1, len(layer_sizes)):layers += [nn.Linear(layer_sizes[layer_index-1], layer_sizes[layer_index]),act_fn]layers += [nn.Linear(layer_sizes[-1], num_classes)]self.layers = nn.Sequential(*layers) # nn.Sequential将一系列模块总结为一个单独的模块,依次应用它们# 我们将所有超参数存储在字典中,以保存和加载模型self.config = {"act_fn": act_fn.config, "input_size": input_size, "num_classes": num_classes, "hidden_sizes": hidden_sizes}def forward(self, x):x = x.view(x.size(0), -1) # 将图像重塑为平向量out = self.layers(x)return out    

我们还添加了加载和保存模型的函数。超参数存储在配置文件中(简单的json文件):

def _get_config_file(model_path, model_name):# 存储超参数详细信息的文件名return os.path.join(model_path, model_name + ".config")def _get_model_file(model_path, model_name):# 存储网络参数的文件名return os.path.join(model_path, model_name + ".tar")def load_model(model_path, model_name, net=None):"""从磁盘加载保存的模型。输入:model_path - 检查点目录的路径model_name - 模型的名称(str)net - (可选)如果提供,状态字典将加载到此模型中。否则,将创建一个新模型。"""config_file, model_file = _get_config_file(model_path, model_name), _get_model_file(model_path, model_name)assert os.path.isfile(config_file), f"找不到配置文件\"{config_file}\"。确定这是正确的路径,并且您的模型配置存储在这里吗?"assert os.path.isfile(model_file), f"找不到模型文件\"{model_file}\"。确定这是正确的路径,并且您的模型存储在这里吗?"with open(config_file, "r") as f:config_dict = json.load(f)if net is None:act_fn_name = config_dict["act_fn"].pop("name").lower()act_fn = act_fn_by_name[act_fn_name](**config_dict.pop("act_fn"))net = BaseNetwork(act_fn=act_fn, **config_dict)net.load_state_dict(torch.load(model_file, map_location=device))return netdef save_model(model, model_path, model_name):"""给定一个模型,我们保存state_dict和超参数。输入:model - 要保存参数的网络对象model_path - 检查点目录的路径model_name - 模型的名称(str)"""config_dict = model.configos.makedirs(model_path, exist_ok=True)config_file, model_file = _get_config_file(model_path, model_name), _get_model_file(model_path, model_name)with open(config_file, "w") as f:json.dump(config_dict, f)torch.save(model.state_dict(), model_file)

我们还设置了要训练的数据集,即FashionMNIST。FashionMNIST是MNIST的一个更复杂的版本,包含衣服的黑白图像,而不是数字。10个类别包括裤子、外套、鞋子、包包等。为了加载这个数据集,我们将使用另一个PyTorch包,即torchvision(文档)。torchvision包包括流行的数据集、模型架构和计算机视觉的常见图像转换。我们将在本课程的许多笔记本中使用该包,以简化我们的数据集处理。

让我们在下面加载数据集,并可视化一些图像,以获得对数据的印象。

import torchvision
from torchvision.datasets import FashionMNIST
from torchvision import transforms# 应用于每个图像的转换 => 首先使它们成为张量,然后将其标准化到-1到1的范围内
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])# 加载训练数据集。我们需要将其分为训练和验证部分
train_dataset = FashionMNIST(root=DATASET_PATH, train=True, transform=transform, download=True)
train_set, val_set = torch.utils.data.random_split(train_dataset, [50000, 10000])# 加载测试集
test_set = FashionMNIST(root=DATASET_PATH, train=False, transform=transform, download=True)# 我们定义一组数据加载器,我们以后可以用于各种目的。
# 注意,对于实际训练模型,我们将使用不同的数据加载器
# 具有较小的批量大小。
train_loader = data.DataLoader(train_set, batch_size=1024, shuffle=True, drop_last=False)
val_loader = data.DataLoader(val_set, batch_size=1024, shuffle=False, drop_last=False)
test_loader = data.DataLoader(test_set, batch_size=1024, shuffle=False, drop_last=False)exmp_imgs = [train_set[i][0] for i in range(16)]
# 将图像组织成一个网格,以便更美观地可视化
img_grid = torchvision.utils.make_grid(torch.stack(exmp_imgs, dim=0), nrow=4, normalize=True, pad_value=0.5)
img_grid = img_grid.permute(1, 2, 0)plt.figure(figsize=(8,8))
plt.title("FashionMNIST示例")
plt.imshow(img_grid
)
plt.axis('off')
plt.show()
plt.close()

在这里插入图片描述

3.2.可视化初始化后的梯度流动

如前所述,激活函数的一个重要方面是它们如何通过网络传播梯度。设想我们有一个超过50层的非常深的神经网络。输入层即第一层的梯度,已经通过了超过50次激活函数,但我们仍希望它们保持合理的大小。如果激活函数的梯度(预期)明显小于1,我们的梯度将在到达输入层之前消失。如果激活函数的梯度大于1,梯度将指数增长并可能爆炸。

为了感受每个激活函数如何影响梯度,我们可以观察一个新初始化的网络,并为256张图像批量测量每个参数的梯度:

def visualize_gradients(net, color="C0"):"""输入:net - BaseNetwork类的对象color - 我们希望以何种颜色可视化直方图(便于激活函数的区分)"""net.eval()small_loader = data.DataLoader(train_set, batch_size=256, shuffle=False)imgs, labels = next(iter(small_loader))imgs, labels = imgs.to(device), labels.to(device)# 将一批数据通过网络传递,并为权重计算梯度net.zero_grad()preds = net(imgs)loss = F.cross_entropy(preds, labels)loss.backward()# 我们将可视化限制在权重参数上,并排除偏差以减少图表数量grads = {name: params.grad.data.view(-1).cpu().clone().numpy() for name, params in net.named_parameters() if "weight" in name}net.zero_grad()# 绘图columns = len(grads)fig, ax = plt.subplots(1, columns, figsize=(columns*3.5, 2.5))fig_index = 0for key in grads:key_ax = ax[fig_index%columns]sns.histplot(data=grads[key], bins=30, ax=key_ax, color=color, kde=True)key_ax.set_title(str(key))key_ax.set_xlabel("梯度大小")fig_index += 1fig.suptitle(f"激活函数{net.config['act_fn']['name']}的梯度大小分布", fontsize=14, y=1.05)fig.subplots_adjust(wspace=0.45)plt.show()plt.close() 

Seaborn在直方图包含小值时会打印警告。我们现在可以忽略它们。

import warnings
warnings.filterwarnings('ignore')
# 为每种激活函数创建一个图表
for i, act_fn_name in enumerate(act_fn_by_name):set_seed(42) # 设置种子确保每种激活函数的权重初始化相同act_fn = act_fn_by_name[act_fn_name]()net_actfn = BaseNetwork(act_fn=act_fn).to(device)visualize_gradients(net_actfn, color=f"C{i}")

sigmoid激活函数表现出明显不良的行为。虽然输出层的梯度非常大,高达0.1,但输入层在所有激活函数中梯度范数最低,仅为1e-5。这是由于其最大梯度为1/4,在此设置中找不到适合所有层的学习率。

所有其他激活函数在所有层中显示出相似的梯度范数。有趣的是,ReLU激活函数在0附近有一个峰值,这是由其左侧的零部分和死神经元(我们将稍后仔细研究)造成的。

请注意,除了激活函数外,权重参数的初始化可能至关重要。默认情况下,PyTorch对线性层使用针对ReLU激活优化的Kaiming初始化。在第4个教程中,我们将更仔细地研究初始化,但目前假设Kaiming初始化对所有激活函数都相当有效。

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

3.3.模型训练

接下来,我们希望在FashionMNIST数据集上用不同的激活函数训练我们的模型,并比较所获得的性能。总的来说,我们的最终目标是在我们选择的数据集上实现最佳可能的性能。因此,我们在下一个单元格中编写了一个训练循环,包括每个epoch之后的验证,以及对最佳模型的最终测试:

def train_model(net, model_name, max_epochs=50, patience=7, batch_size=256, overwrite=False):"""在FashionMNIST训练集上训练模型输入:net - BaseNetwork类型的对象model_name - (str) 模型名称,用于创建检查点名称max_epochs - 我们想要(最大)训练的epoch数量patience - 如果验证集上的性能在#patience个epoch内没有改善,我们将提前停止训练batch_size - 训练中使用的批量大小overwrite - 确定如何处理已经存在检查点的情况。如果为True,将被覆盖。否则,我们将跳过训练。"""file_exists = os.path.isfile(_get_model_file(CHECKPOINT_PATH, model_name))if file_exists and not overwrite:print("模型文件已存在。跳过训练...")else:if file_exists:print("模型文件存在,但将被覆盖...")# 定义优化器、损失和数据加载器optimizer = optim.SGD(net.parameters(), lr=1e-2, momentum=0.9) # 默认参数,可自由更改loss_module = nn.CrossEntropyLoss()train_loader_local = data.DataLoader(train_set, batch_size=batch_size, shuffle=True, drop_last=True, pin_memory=True)val_scores = []best_val_epoch = -1for epoch in range(max_epochs):############## 训练 ##############net.train()true_preds, count = 0., 0for imgs, labels in tqdm(train_loader_local, desc=f"Epoch {epoch+1}", leave=False):imgs, labels = imgs.to(device), labels.to(device) # 到GPUoptimizer.zero_grad() # 在"loss.backward()"之前任何地方清零梯度preds = net(imgs)loss = loss_module(preds, labels)loss.backward()optimizer.step()# 记录训练期间的统计数据true_preds += (preds.argmax(dim=-1) == labels).sum()count += labels.shape[0]train_acc = true_preds / count########### 验证 ###########val_acc = test_model(net, val_loader)val_scores.append(val_acc)print(f"[Epoch {epoch+1:2d}] 训练准确率: {train_acc*100.0:05.2f}%, 验证准确率: {val_acc*100.0:05.2f}%")if len(val_scores) == 1 or val_acc > val_scores[best_val_epoch]:print("\t   (新的最佳性能,正在保存模型...)")save_model(net, CHECKPOINT_PATH, model_name)best_val_epoch = epochelif best_val_epoch <= epoch - patience:print(f"由于最近{patience}个epochs没有改善,提前停止")break# 绘制验证准确率的曲线plt.plot([i for i in range(1,len(val_scores)+1)], val_scores)plt.xlabel("Epochs")plt.ylabel("验证准确率")plt.title(f"{model_name}的验证性能")plt.show()plt.close()load_model(CHECKPOINT_PATH, model_name, net=net)
test_acc = test_model(net, test_loader)
print((f" 测试准确率: {test_acc*100.0:4.2f}% ").center(50, "=")+"\n")
return test_acc

我们将为每种激活函数训练一个模型。如果您在CPU上运行这个笔记本,我们建议使用预训练模型以节省时间。

for act_fn_name in act_fn_by_name:print(f"正在训练使用{act_fn_name}激活的BaseNetwork...")set_seed(42)act_fn = act_fn_by_name[act_fn_name]()net_actfn = BaseNetwork(act_fn=act_fn).to(device)train_model(net_actfn, f"FashionMNIST_{act_fn_name}", overwrite=False)

毫不奇怪,使用sigmoid激活函数的模型显示出失败,并没有比随机性能更好(10个类别 => 随机概率为1/10)。

所有其他激活函数获得类似的性能。为了得出更准确的结论,我们必须使用多个种子训练模型并查看平均值。然而,“最佳”激活函数还取决于许多其他因素(隐藏大小、层数、层类型、任务、数据集、优化器、学习率等),因此在我们的案例中进行彻底的网格搜索是没有用的。在文献中,已经证明与深度网络一起工作的激活函数都是我们在这里实验的所有类型的ReLU函数,在特定网络中特定激活函数有小幅增益。

接下来,我们将训练我们的模型,使用不同的激活函数在FashionMNIST上,并比较所获得的性能。总之,我们的最终目标是在所选数据集上实现最佳可能的性能。因此,我们在下一个单元格中编写了一个训练循环,包括每个epoch之后的验证和对最佳模型的最终测试:

我们训练了一个针对每种激活函数的模型。如果您在CPU上运行这个笔记本,我们建议使用预训练模型以节省时间。

使用sigmoid激活函数的模型不出所料地失败了,并没有比随机性能更好(10个类别意味着随机概率为1/10)。

所有其他激活函数都获得了类似的性能。为了得出更准确的结论,我们需要使用多个不同的种子训练模型,并查看它们的平均性能。然而,“最佳”激活函数还取决于许多其他因素,如隐藏层大小、层数、层类型、任务、数据集、优化器、学习率等,因此在我们的情况下进行全面的网格搜索是没有用的。在文献中,已经表明与深度网络一起工作良好的激活函数都是我们在这里实验的ReLU函数类型,特定激活函数在特定网络中有小幅增益。

def test_model(net, data_loader):"""在指定的数据集上测试模型。输入:net - 已训练的BaseNetwork类型的模型data_loader - 要测试的数据集的DataLoader对象(验证或测试)"""net.eval()true_preds, count = 0., 0for imgs, labels in data_loader:imgs, labels = imgs.to(device), labels.to(device)with torch.no_grad():preds = net(imgs).argmax(dim=-1)true_preds += (preds == labels).sum().item()count += labels.shape[0]test_acc = true_preds / countreturn test_acc

3.4.可视化激活分布

在模型训练完成后,我们可以观察模型内部实际的激活值。例如,ReLU中有多少神经元被设置为零?Tanh中的大部分值在哪里?为了回答这些问题,我们可以编写一个简单函数,该函数采用一个训练好的模型,将其应用于一批图像,并绘制网络内部激活的直方图:

def visualize_activations(net, color="C0"):activations = {}net.eval()small_loader = data.DataLoader(train_set, batch_size=1024)imgs, labels = next(iter(small_loader))with torch.no_grad():layer_index = 0imgs = imgs.to(device)imgs = imgs.view(imgs.size(0), -1)# 我们需要手动遍历层以保存所有激活for layer_index, layer in enumerate(net.layers[:-1]):imgs = layer(imgs)activations[layer_index] = imgs.view(-1).cpu().numpy()# 绘图columns = 4rows = math.ceil(len(activations)/columns)fig, ax = plt.subplots(rows, columns, figsize=(columns*2.7, rows*2.5))fig_index = 0for key in activations:key_ax = ax[fig_index//columns][fig_index%columns]sns.histplot(data=activations[key], bins=50, ax=key_ax, color=color, kde=True, stat="density")key_ax.set_title(f"Layer {key} - {net.layers[key].__class__.__name__}")fig_index += 1fig.suptitle(f"Activation distribution for activation function {net.config['act_fn']['name']}", fontsize=14)fig.subplots_adjust(hspace=0.4, wspace=0.4)plt.show()plt.close()

为每种激活函数创建一个图表:

for i, act_fn_name in enumerate(act_fn_by_name):net_actfn = load_model(model_path=CHECKPOINT_PATH, model_name=f"FashionMNIST_{act_fn_name}").to(device)visualize_activations(net_actfn, color=f"C{i}")

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

由于sigmoid激活的模型未能正确训练,激活值也不太有信息量,都集中在0.5(输入0时的激活)附近。

tanh显示出更多样化的行为。虽然输入层有更多神经元接近-1和1,梯度接近零,但连续两层的激活值更接近零。这可能是因为输入层在输入图像中寻找特定特征,而连续层将这些特征结合起来。最后一层的激活值再次更倾向于极端点,因为分类层可以看作是这些值的加权平均值(梯度将激活推向这些极端)。

ReLU在0处有一个强烈的峰值,正如我们最初所预期的。由于负值没有梯度,网络在线性层之后的分布不是类似高斯的分布,而是对正值有更长的尾部。LeakyReLU表现出非常类似的行为,而ELU再次呈现出更类似高斯的分布。Swish激活似乎介于两者之间,尽管值得注意的是,Swish使用的值比其他激活函数显著更高(高达20)。

由于所有激活函数虽然表现出略有不同的行为,但在我们的简单网络中获得了类似的性能,显然“最佳”激活函数的选择实际上取决于许多因素,并且并不适用于所有可能的网络。

3.5.在ReLU网络中寻找死神经元

ReLU激活的一个已知缺点是“死神经元”的出现,即对任何训练输入都没有梯度的神经元。死神经元的问题在于,由于该层没有提供梯度,我们无法训练前一层的参数以获得除零以外的输出值。要使死神经元发生,ReLU前线性层的特定神经元的输出值必须对所有输入图像都是负的。考虑到神经网络中神经元的数量,这种情况的发生并不是不可能的。

为了更好地了解这是一个多么严重的问题,以及我们需要小心的时候,我们将测量不同网络中有多少死神经元。为此,我们实现了一个函数,该函数在整个训练集上运行网络,并记录是否有神经元对所有数据点都是0:

def measure_number_dead_neurons(net):# 对于每个神经元,我们最初设置一个布尔变量为1。如果它在任何时候的激活不等于0,# 我们将这个变量设置为0。在运行完整个训练集后,只有死神经元将保持为1。neurons_dead = [torch.ones(layer.weight.shape[0], device=device, dtype=torch.bool) for layer in net.layers[:-1] if isinstance(layer, nn.Linear)] # 与BaseNetwork中的隐藏大小相同net.eval()with torch.no_grad():for imgs, labels in tqdm(train_loader, leave=False): # 运行整个训练集layer_index = 0imgs = imgs.to(device)imgs = imgs.view(imgs.size(0), -1)for layer in net.layers[:-1]:imgs = layer(imgs)if isinstance(layer, ActivationFunction):# 如果批量中的所有激活都是0,并且我们没有在最后几批中记录相反的情况?neurons_dead[layer_index] = torch.logical_and(neurons_dead[layer_index], (imgs == 0).all(dim=0))layer_index += 1number_neurons_dead = [t.sum().item() for t in neurons_dead]print("Number of dead neurons:", number_neurons_dead)print("In percentage:", ", ".join([f"{(100.0 * num_dead / tens.shape[0]):4.2f}%" for tens, num_dead in zip(neurons_dead, number_neurons_dead)]))

首先,我们可以为一个未训练的网络测量死神经元的数量:

set_seed(42)
net_relu = BaseNetwork(act_fn=ReLU()).to(device)
measure_number_dead_neurons(net_relu)

我们看到只有少数神经元是死的,但它们随着层的深度而增加。然而,由于前层权重的更新改变了后层的输入,我们拥有的少量死神经元并不是问题。因此,后层中的死神经元可能再次变为“活跃”。

对于训练过的网络(使用相同的初始化)情况如何?

net_relu = load_model(model_path=CHECKPOINT_PATH, model_name="FashionMNIST_relu").to(device)
measure_number_dead_neurons(net_relu)

确实,在后层中死神经元的数量减少了。然而,应该注意的是,死神经元在输入层中尤为成问题。由于输入在epochs中不改变(训练集保持不变),训练网络无法使这些神经元重新活跃。尽管如此,输入数据通常具有足够高的标凊差,以降低死神经元的风险。

最后,我们检查死神经元数量如何随着层深度的增加而变化。例如,让我们看看以下10层神经网络:

set_seed(42)
net_relu = BaseNetwork(act_fn=ReLU(), hidden_sizes=[256, 256, 256, 256, 256, 128, 128, 128, 128, 128]).to(device)
measure_number_dead_neurons(net_relu)

死神经元的数量明显高于以前,这在第一次迭代中尤其损害了梯度流动。例如,倒数第二层中超过56%的神经元是死的,这造成了相当大的瓶颈。因此,对于非常深的网络,建议使用其他非线性激活,如Swish。

4.结论

在本文中,我们回顾了一系列六种激活函数(sigmoid、tanh、ReLU、LeakyReLU、ELU和Swish)在神经网络中的作用,并讨论了它们如何影响各层之间的梯度分布。Sigmoid倾向于在深层神经网络中失败,因为它所提供的最高梯度是0.25,导致早期层中的梯度消失。所有基于ReLU的激活函数都表现出良好的性能,并且除了原始的ReLU之外,它们没有死神经元的问题。在实现自己的神经网络时,建议从基于ReLU的网络开始,并根据网络的特性选择特定的激活函数。

参考文献

[1] Ramachandran, Prajit, Barret Zoph, 和 Quoc V. Le. “Searching for activation functions.” arXiv 预印本 arXiv:1710.05941 (2017). 论文链接

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

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

相关文章

220v降压5v用几k电阻?

# 220V降压至5V的电阻选择与AH8652和AH8696芯片的应用 在电子电路设计中&#xff0c;将高电压转换为低电压是常见的需求。例如&#xff0c;将220V交流电&#xff08;AC&#xff09;降压至5V直流电&#xff08;DC&#xff09;用于低功耗设备。这通常通过使用降压转换器&#xf…

C判断一个点在三角形上

背景 鼠标操作时&#xff0c;经常要判断是否命中显示控件&#xff0c;特开发此算法快速判断。 原理 三角形三等分点定理是指在任意三角形ABC中&#xff0c;可以找到三个点D、E和F&#xff0c;使得线段AD、BE和CF均等分三角形ABC。 这意味着三个等分点分别位于三个边界上&…

Golang中init()函数初始化顺序

Q24.init()函数初始化顺序 init() 函数是用于程序执行前做包的初始化的函数&#xff0c;比如初始化包里的变量等一个包可以出线多个 init() 函数,一个源文件也可以包含多个 init() 函数同一个包中多个 init() 函数的执行顺序没有明确定义&#xff0c;但是不同包的init函数是根…

线程的复习

目录 大纲Java中的线程概念创建线程的方法线程的生命周期线程的同步和通信线程的优先级和调度线程的中断 案例 大纲 Java中的线程概念 在Java中&#xff0c;线程是操作系统能够进行运算调度的最小单位&#xff0c;它被包含在进程之中&#xff0c;是进程中实际运作的部分。一个…

网络配置命令

文章目录 一、查看网络接口信息 ifconfig1.1 网络接口名称1.2 使用 ifconfig 查看网络接口信息1.2.1 输出示例1.2.2 输出解释 1.3 查看特定网络接口信息1.3.1 输出示例 1.4 查看所有网络接口信息1.5 特殊网络接口 二、修改网络配置文件2.1 配置文件示例2.2 使配置生效2.3 关闭 …

图——图的遍历(DFS与BFS)

前面的文章中我们学习了图的基本概念和存储结构&#xff0c;大家可以通过下面的链接学习&#xff1a; 图的定义和基本术语 图的类型定义和存储结构 这篇文章就来学习一下图的重要章节——图的遍历。 目录 一&#xff0c;图的遍历定义&#xff1a; 二&#xff0c;深度优先…

应用实践之基于MindNLP+MusicGen生成自己的个性化音乐

前言 MusicGen是基于单个语言模型&#xff08;LM&#xff09;的音乐生成模型&#xff0c;使用文本描述或音频提示生成高质量的音乐样本。它基于Transformer结构&#xff0c;包括文本编码器模型和音频压缩模型&#xff0c;以及一个解码器来预测离散的隐形状态音频token。与传统…

uni-data-select 插件配置接收字段,更改默认的text,value

当后台返回的数据源格式不是如下value,text字段时&#xff0c;需要自定义字段配置 range: [{ value: 0, text: "篮球" },{ value: 1, text: "足球" },{ value: 2, text: "游泳" },], 思路有两个&#xff0c; 思路一&#xff1a;前端遍历更改为…

PE文件(十一)移动导出表和重定位表

移动表的原因 一个PE文件中有很多节&#xff0c;每个节都存储不同的数据。而PE文件中的各种表也都分散存储在这些节当中。此时各种表的信息与程序的代码和数据相互混合在一起&#xff0c;如果我们直接对整个程序进行加密&#xff0c;那系统在初始化程序时就会出问题。比如&…

DHCP原理及配置

目录 一、DHCP原理 DHCP介绍 DHCP工作原理 DHCP分配方式 工作原理 DHCP重新登录 DHCP优点 二、DHCP配置 一、DHCP原理 1 DHCP介绍 大家都知道&#xff0c;现在出门很多地方基本上都有WIFI&#xff0c;那么有没有想过这样一个问题&#xff0c;平时在家里都是“固定”的…

【总结】实际业务场景中锁、事务、异常如何考虑使用?

文章目录 锁处理目的&#xff1a;考虑锁控制思路&#xff1a;生命周期接口并发控制解决方案&#xff1a;测试锁是否生效&#xff1a;模拟多线程并发场景的2种方式&#xff1a; 事务处理目的&#xff1a;考虑事务控制思路&#xff1a;解决方案&#xff1a; 总结 锁处理 目的&am…

利用AI辅助制作ppt封面

如何利用AI辅助制作一个炫酷的PPT封面 标题使用镂空字背景替换为动态视频 标题使用镂空字 1.首先&#xff0c;新建一个空白的ppt页面&#xff0c;插入一张你认为符合主题的图片&#xff0c;占满整个可视页面。 2.其次&#xff0c;插入一个矩形&#xff0c;右键选择设置形状格式…

北京交通大学《深度学习》专业课,实验2-前馈神经网络

1. 源代码 见资源“北京交通大学《深度学习》专业课&#xff0c;实验2-前馈神经网络” 2. 实验内容 &#xff08;1&#xff09;手动实现前馈神经网络解决上述回归、二分类、多分类任务 分析实验结果并绘制训练集和测试集的loss曲线 &#xff08;2&#xff09;利用to…

keepalive:

keepalive&#xff1a; 调度器的高可用 vip地址在主备之间的切换&#xff0c;主在工作时&#xff0c;vip地址只在主上&#xff0c;主停止工作&#xff0c;vip漂移到备服务器。 在主备的优先级不变的情况下&#xff0c;主恢复工作&#xff0c;vip会飘回到主服务器。 1、配优…

企业网络运维-给华为交换机配置sftp,浏览交换机文件并下载上传

文章目录 需求实验开户stelnet权限已完成stelnet账号下的sftp配置使用xshell-sftp访问 需求 浏览交换机文件并下载上传 实验 开户stelnet权限 参考https://blog.csdn.net/xzzteach/article/details/140419150 已完成stelnet账号下的sftp配置 服务类型all包括stelnet和sf…

强化学习编程实战-5 基于时间差分的方法

第4章中&#xff0c;当模型未知时&#xff0c;由于状态转移概率P未知&#xff0c;动态规划中值函数的评估方法不再适用&#xff0c;用蒙特卡洛的方法聘雇值函数。 在蒙特卡洛方法评估值函数时&#xff0c;需要采样一整条轨迹&#xff0c;即需要从初始状态s0到终止状态的整个序列…

探索“搭旅万物皆可搭”小程序——构建旅行搭伴平台的创新实践

摘要 随着旅游市场的不断发展和个性化需求的日益增长&#xff0c;旅行搭伴平台逐渐成为连接志同道合旅者的桥梁。本文旨在介绍“搭旅万物皆可搭”小程序的设计理念、核心功能及其背后的技术实现&#xff0c;探讨如何通过算法优化、安全保障、社交互动等手段&#xff0c;打造一…

GUI界面开发之tkinter(一)

Tkinter是一个内置的Python库&#xff0c;用于创建图形用户界面&#xff08;GUI&#xff09;。它提供了一组工具和小部件&#xff0c;用于创建窗口、对话框、按钮、菜单和其他GUI元素。 在本篇文章中&#xff0c;主要介绍了窗口等知识点。 大家好&#xff01;我是码银&#x1…

《昇思25天学习打卡营第22天|onereal》

文本解码原理--以MindNLP为例 回顾&#xff1a;自回归语言模型 根据前文预测下一个单词 一个文本序列的概率分布可以分解为每个词基于其上文的条件概率的乘积 &#x1d44a;_0:初始上下文单词序列&#x1d447;: 时间步当生成EOS标签时&#xff0c;停止生成。 MindNLP/huggi…

MySQL 时区问题:设置了 my.ini 并重启了服务,依旧是 0 时区

1、问题再现 在撰写 飞书 API 2-5 时&#xff0c;需要新建一些数据表&#xff0c;以便实施从数据库到多维表的数据同步。我建了2个测试数据表&#xff0c;连表查询之后&#xff0c;将时间戳转为时间格式返回&#xff0c;结果发现少了 8 小时。 具体逻辑抽象为以下&#xff0c…