目录

前言

一、深度学习概述

1.什么是深度学习

2.深度学习发展历史

3.深度学习的优势

二、神经网络

1.感知神经网络

2.人工神经元

2.1 构建人工神经元

2.2 组成部分

2.3 数学表示

2.4 对比生物神经元

3.深入神经网络

3.1 基本结构

3.2 网络构建

3.3 全连接神经网络

3.3.1 特点

3.3.2 计算步骤

3.3.3 基本组件认知

三、数据准备

1.数据加载器

1.1 构建数据类

1.1.1 Dataset类

1.1.2 TensorDataset类

1.2 数据加载器

2.数据集加载案例

2.1 加载csv数据集

2.2 加载图片数据集

2.3 加载官方数据集

四、激活函数

1.基础概念

1.1 线性理解

1.2 非线性可视化

2.常见激活函数

2.1 sigmoid

2.1.1 公式

2.1.2 特征

2.1.3 缺点

2.1.4 函数绘制

2.2 tanh

2.2.1 公式

2.2.2 特征

2.2.3 缺点

2.2.4 函数绘制

2.3 ReLU

2.3.1 公式

2.3.2 特征

2.3.3 缺点

2.3.4 函数绘图

2.4 LeakyReLU

2.4.1 公式

2.4.2 特征

2.4.3 缺点

2.4.4 函数绘制

​编辑2.5 softmax

2.5.1 公式

2.5.2 特征

2.5.3 缺点

2.5.4 代码实现

3.如何选择

3.1 隐藏层

3.2 输出层

1.固定值初始化

1.1 全零初始化

1.2 全1初始化

1.3 任意常数初始化

2.随机初始化

3.Xavier 初始化

4.He初始化

​编辑

5.总结

总结


前言

这一章主要了解一下神经网络,然后知道所有的激活函数


一、深度学习概述

1.什么是深度学习

人工智能、机器学习和深度学习之间的关系:

机器学习是实现人工智能的一种途径,深度学习是机器学习的子集,区别如下:

传统机器学习算法依赖人工设计特征、提取特征,而深度学习依赖算法自动提取特征。深度学习模仿人类大脑的运行方式,从大量数据中学习特征,这也是深度学习被看做黑盒子、可解释性差的原因。

随着算力的提升,深度学习可以处理图像,文本,音频,视频等各种内容,主要应用领域有:

  1. 图像处理:分类、目标检测、图像分割(语义分割)

  2. 自然语言处理:LLM、NLP、Transformer

  3. 语音识别:对话机器人、智能客服(语音+NLP)

  4. 自动驾驶:语义分割(行人、车辆、实线等)

  5. LLM:大Large语言Language模型Model

  6. 机器人:非常火的行业

有了大模型的加持,AI+各行各业。

2.深度学习发展历史

深度学习其实并不是新的事物,深度学习所需要的神经网络技术起源于20世纪50年代,叫做感知机。当时使用单层感知机,因为只能学习线性可分函数,连简单的异或(XOR)等线性不可分问题都无能为力,1969年Marvin Minsky写了一本叫做《Perceptrons》的书,他提出了著名的两个观点:1.单层感知机没用,我们需要多层感知机来解决复杂问题 2.没有有效的训练算法。

20世纪80年代末期,用于人工神经网络的反向传播算法(也叫Back Propagation算法或者BP算法)的发明,给机器学习带来了希望,掀起了基于统计模型的机器学习热潮。这个热潮一直持续到今天。人们发现,利用BP算法可以让一个人工神经网络模型从大量训练样本中学习统计规律,从而对未知事件做预测。这种基于统计的机器学习方法比起过去基于人工规则的系统,在很多方面显出优越性。这个时候的人工神经网络,虽也被称作多层感知机(Multi-layer Perceptron),但实际是种只含有一层隐层节点的浅层模型。

2006年,杰弗里·辛顿以及他的学生鲁斯兰·萨拉赫丁诺夫正式提出了深度学习的概念。

2012年,在著名的ImageNet图像识别大赛中,杰弗里·辛顿领导的小组采用深度学习模型AlexNet一举夺冠。AlexNet采用ReLU激活函数,从根本上解决了梯度消失问题,并采用GPU极大的提高了模型的运算速度。

同年,吴恩达教授和Jeff Dean主导的深度神经网络DNN技术在ImageNet评测中把错误率从26%降低到15%,再一次吸引了学术界和工业界对于深度学习领域的关注。

2016年,随着谷歌公司基于深度学习开发的AlphaGo以4:1的比分战胜了国际顶尖围棋高手李世石,深度学习的热度一时无两。后来,AlphaGo又接连和众多世界级围棋高手过招,均取得了完胜。这也证明了在围棋界,基于深度学习技术的机器人已经超越了人类。

2017年,基于强化学习算法的AlphaGo升级版AlphaGo Zero横空出世。其采用“从零开始”、“无师自通”的学习模式,以100:0的比分轻而易举打败了之前的AlphaGo。除了围棋,它还精通国际象棋等其它棋类游戏,可以说是真正的棋类“天才”。此外在这一年,深度学习的相关算法在医疗、金融、艺术、无人驾驶等多个领域均取得了显著的成果。所以,也有专家把2017年看作是深度学习甚至是人工智能发展最为突飞猛进的一年。

2019年,基于Transformer 的自然语言模型的持续增长和扩散,这是一种语言建模神经网络模型,可以在几乎所有任务上提高NLP的质量。Google甚至将其用作相关性的主要信号之一,这是多年来最重要的更新。

2020年,深度学习扩展到更多的应用场景,比如积水识别,路面塌陷等,而且疫情期间,在智能外呼系统,人群测温系统,口罩人脸识别等都有深度学习的应用。

3.深度学习的优势

二、神经网络

我们要学习的深度学习(Deep Learning)是神经网络的一个子领域,主要关注更深层次的神经网络结构,也就是深层神经网络(Deep Neural Networks,DNNs)。所以,我们需要先搞清楚什么是神经网络.

1.感知神经网络

神经网络(Neural Networks)是一种模拟人脑神经元网络结构的计算模型,用于处理复杂的模式识别、分类和预测等任务。生物神经元如下图:

生物学:

人脑可以看做是一个生物神经网络,由众多的神经元连接而成

  • 树突:从其他神经元接收信息的分支

  • 细胞核:处理从树突接收到的信息

  • 轴突:被神经元用来传递信息的生物电缆

  • 突触:轴突和其他神经元树突之间的连接

人脑神经元处理信息的过程:

  • 多个信号到达树突,然后整合到细胞体的细胞核中

  • 当积累的信号超过某个阈值,细胞就会被激活

  • 产生一个输出信号,由轴突传递。

神经网络由多个互相连接的节点(即人工神经元)组成。

2.人工神经元

人工神经元(Artificial Neuron)是神经网络的基本构建单元,模仿了生物神经元的工作原理。其核心功能是接收输入信号,经过加权求和和非线性激活函数处理后,输出结果。

2.1 构建人工神经元

人工神经元接受多个输入信息,对它们进行加权求和,再经过激活函数处理,最后将这个结果输出。

2.2 组成部分

  • 输入(Inputs): 代表输入数据,通常用向量表示,每个输入值对应一个权重。

  • 权重(Weights): 每个输入数据都有一个权重,表示该输入对最终结果的重要性。

  • 偏置(Bias): 一个额外的可调参数,作用类似于线性方程中的截距,帮助调整模型的输出。

  • 加权求和: 神经元将输入乘以对应的权重后求和,再加上偏置。

  • 激活函数(Activation Function): 用于将加权求和后的结果转换为输出结果,引入非线性特性,使神经网络能够处理复杂的任务。常见的激活函数有Sigmoid、ReLU(Rectified Linear Unit)、Tanh等。

2.3 数学表示

如果有 n 个输入x_1, x_2, \ldots, x_n,权重分别为 w_1, w_2, \ldots, w_n,偏置为 b,则神经元的输出 y 表示为:

z=\sum_{i=1}^nw_i\cdot x_i+b \\ y=\sigma(z)

其中,\sigma(z) 是激活函数

线性回归:

y=\sum_{i=1}^nw_i\cdot x_i+b \\

线性回归不需要激活函数

逻辑回归:

z=\sum_{i=1}^nw_i\cdot x_i+b \\ y=\sigma(z)=sigmoid(z)=\frac{1}{1+e^{-z}}

2.4 对比生物神经元

人工神经元和生物神经元对比如下表:

生物神经元人工神经元
细胞核节点 (加权求和 + 激活函数)
树突输入
轴突带权重的连接
突触输出

3.深入神经网络

神经网络是由大量人工神经元按层次结构连接而成的计算模型。每一层神经元的输出作为下一层的输入,最终得到网络的输出。

3.1 基本结构

神经网络有下面三个基础层(Layer)构建而成:

  • 输入层(Input): 神经网络的第一层,负责接收外部数据,不进行计算。

  • 隐藏层(Hidden): 位于输入层和输出层之间,进行特征提取和转换。隐藏层一般有多层,每一层有多个神经元。

  • 输出层(Output): 网络的最后一层,产生最终的预测结果或分类结果

3.2 网络构建

我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个权重,经典如下:

注意:同一层的各个神经元之间是没有连接的。

3.3 全连接神经网络

前馈神经网络(Feedforward Neural Network,FNN)是一种最基本的神经网络结构,其特点是信息从输入层经过隐藏层单向传递到输出层,没有反馈或循环连接

全连接神经网络(Fully Connected Neural Network,FCNN)是前馈神经网络的一种,每一层的神经元与上一层的所有神经元全连接,常用于图像分类、文本分类等任务。

如上图,网络中每个神经元:

z_1 = x_1*w_1 + x_2*w_2+b_1 \\ z_2 = x_1*w_1 + x_2*w_2+b_2 \\ z_3 = x_1*w_1 + x_2*w_2+b_3

说明:三个等式中的w1和w2在这里只是为了方便表示对应x1和x2的权重,实际三个等式中的w值是不同的。

向量x为:[x_1,x_2]

向量w:\begin{pmatrix}w_1,w_2\\w_1,w_2\\w_1,w_2 \end{pmatrix},其形状为(3,2),3是神经元节点个数,2是向量x的个数

向量z:[z_1,z_2,z_3]

向量b:[b_1,b_2,b_3]

所以用向量表示为:

z = \begin{pmatrix}z_1,z_2,z_3 \end{pmatrix}=\begin{pmatrix}x_1,x_2 \end{pmatrix}\begin{pmatrix}w_1,w_1,w_1\\w_2,w_2,w_2\end{pmatrix}+\begin{pmatrix}b_1,b_2,b_3 \end{pmatrix}=\begin{pmatrix}x_1,x_2 \end{pmatrix}\begin{pmatrix}w_1,w_2\\w_1,w_2\\w_1,w_2 \end{pmatrix}^T + \begin{pmatrix}b_1,b_2,b_3 \end{pmatrix}=xw^T+b

  • x是输入数据,形状为 (batch_size, in_features)。

  • W是权重矩阵,形状为 (out_features, in_features)。

  • b是偏置项,形状为 (out_features,)。

  • z是输出数据,形状为 (batch_size, out_features)。

3.3.1 特点
  • 全连接层: 层与层之间的每个神经元都与前一层的所有神经元相连。

  • 权重数量: 由于全连接的特点,权重数量较大,容易导致计算量大、模型复杂度高。

  • 学习能力: 能够学习输入数据的全局特征,但对于高维数据却不擅长捕捉局部特征(如图像就需要CNN)。

3.3.2 计算步骤
  1. 数据传递: 输入数据经过每一层的计算,逐层传递到输出层。

  2. 激活函数: 每一层的输出通过激活函数处理。

  3. 损失计算: 在输出层计算预测值与真实值之间的差距,即损失函数值。

  4. 反向传播(Back Propagation): 通过反向传播算法计算损失函数对每个权重的梯度,并更新权重以最小化损失。

3.3.3 基本组件认知

官方文档:torch.nn — PyTorch 2.8 documentation

线性层组件

nn.Linear是 PyTorch 中的一个非常重要的模块,用于实现全连接层(也称为线性层)。它是神经网络中常用的一种层类型,主要用于将输入数据通过线性变换映射到输出空间。

torch.nn.Linear(in_features, out_features, bias=True)

参数说明:

in_features:

  • 输入特征的数量(即输入数据的维度)。

  • 例如,如果输入是一个长度为 100 的向量,则 in_features=100。

out_features:

  • 输出特征的数量(即输出数据的维度)。

  • 例如,如果希望输出是一个长度为 50 的向量,则 out_features=50。

bias:

  • 是否使用偏置项(默认值为 True)。

  • 如果设置为 False,则不会学习偏置项。

示例:构建3层全连接神经网络:在__init__方法中定义网络结构,在forward定义前向传播

import torch
from torch import nn
# 定义全连接神经网络模型
class MyFcnn(nn.Module):
    def __init__(self, input_size,out_size):
        # 父类初始化
        super(MyFcnn, self).__init__()
        # 定义线性层1
        self.fc1 = nn.Linear(input_size, 64)

        # 定义线性层2,输入要和第一层的输出一致
        self.fc2 = nn.Linear(64, 32)

        # 定义线性层3,输入要和第二层的输出一致
        self.fc3 = nn.Linear(32, out_size)

    def forward(self, x):
        x = self.fc1(x)
        x = self.fc2(x)
        x = self.fc3(x)
        return x

input_size = 32
out_size = 1
model = MyFcnn(input_size,out_size)
print(model)

如果模型中线性层按顺序叠加,也可以使用nn.Sequential构建模型。nn.Sequential 是一个顺序容器,内置了自动的前向传播逻辑,它会自动将输入数据依次传递给其中的每一层,并执行前向传播,不需要显式定义 forward() 方法。

import torch
from torch import nn

input_size = 32
# 定义全连接神经网络模型
model = nn.Sequential(
    nn.Linear(input_size, 64),
    nn.Linear(64, 32),
    nn.Linear(32, 1)
)
print(model)

优化器

官方文档:torch.optim — PyTorch 2.8 documentation

在PyTorch中,优化器(Optimizer)是用于更新模型参数以最小化损失函数的核心工具。

PyTorch 在 torch.optim 模块中提供了多种优化器,常用的包括:

  • SGD(随机梯度下降)

  • Adagrad(自适应梯度)

  • RMSprop(均方根传播)

  • Adam(自适应矩估计)

核心方法有:

zero_grad():清空模型参数的梯度(将梯度置零)。必须在loss.backward()之前调用zero_grad(),避免梯度累积。

step():参数更新;是优化器的核心方法,用于根据计算得到的梯度更新模型参数。优化器会根据梯度和学习率等参数,调整模型的权重和偏置。

以SGD(随机梯度下降)优化器为例:

import torch
import torch.nn as nn
import torch.optim as optim

# 优化方法SGD的学习
def test3():
    model = nn.Linear(20, 60)
    criterion = nn.MSELoss()
    # 优化器:更新模型参数
    optimizer = optim.SGD(model.parameters(), lr=0.01)
    input = torch.randn(128, 20)
    output = model(input)
    # 计算损失及反向传播
    loss = criterion(output, torch.randn(128, 60))
    # 梯度清零
    optimizer.zero_grad()
    # 反向传播
    loss.backward()
    # 更新模型参数
    optimizer.step()
    
    print(loss.item())


if __name__ == "__main__":
    test3()
  • optim.SGD():优化器方法;是 PyTorch 提供的随机梯度下降(Stochastic Gradient Descent, SGD)优化器。

  • model.parameters():模型参数获取;是一个生成器,用于获取模型中所有可训练的参数(权重和偏置)。

注意:这里只是组件认识和用法演示,没有具体的模型训练功能实现

三、数据准备

1.数据加载器

分数据集和加载器2个步骤~

1.1 构建数据类
1.1.1 Dataset类

Dataset是一个抽象类,是所有自定义数据集应该继承的基类。它定义了数据集必须实现的方法。

必须实现的方法

  1. __len__: 返回数据集的大小

  2. __getitem__: 支持整数索引,返回对应的样本

在 PyTorch 中,构建自定义数据加载类通常需要继承 torch.utils.data.Dataset 并实现以下几个方法:

__init__ 方法 用于初始化数据集对象:通常在这里加载数据,或者定义如何从存储中获取数据的路径和方法。

def __init__(self, data, labels):
    self.data = data
    self.labels = labels

__len__ 方法 返回样本数量:需要实现,以便 Dataloader加载器能够知道数据集的大小。

def __len__(self):
    return len(self.data)

__getitem__ 方法 根据索引返回样本:将从数据集中提取一个样本,并可能对样本进行预处理或变换。

def __getitem__(self, index):
    sample = self.data[index]
    label = self.labels[index]
    return sample, label
1.1.2 TensorDataset类

TensorDatasetDataset的一个简单实现,它封装了张量数据,适用于数据已经是张量形式的情况。

特点

  1. 简单快捷:当数据已经是张量形式时,无需自定义Dataset类

  2. 多张量支持:可以接受多个张量作为输入,按顺序返回

  3. 索引一致:所有张量的第一个维度必须相同,表示样本数量

源码:

class TensorDataset(Dataset):
    def __init__(self, *tensors):
        # size(0)在python中同shape[0],获取的是样本数量
        # 用第一个张量中的样本数量和其他张量对比,如果全部相同则通过断言,否则抛异常
        assert all(tensors[0].size(0) == tensor.size(0) for tensor in tensors)
        self.tensors = tensors

    def __getitem__(self, index):
        return tuple(tensor[index] for tensor in self.tensors)

    def __len__(self):
        return self.tensors[0].size(0)

示例:

def test1():

    torch.manual_seed(0)

    # 创建特征张量和标签张量
    features = torch.randn(100, 5)  # 100个样本,每个样本5个特征
    labels = torch.randint(0, 2, (100,))  # 100个二进制标签

    # 创建TensorDataset
    dataset = TensorDataset(features, labels)

    # 使用方式与自定义Dataset相同
    print(len(dataset))  # 输出: 100
    print(dataset[0])  # 输出: (tensor([...]), tensor(0))

1.2 数据加载器

在训练或者验证的时候,需要用到数据加载器批量的加载样本。

DataLoader 是一个迭代器,用于从 Dataset 中批量加载数据。它的主要功能包括:

  • 批量加载:将多个样本组合成一个批次。

  • 打乱数据:在每个 epoch 中随机打乱数据顺序。

  • 多线程加载:使用多线程加速数据加载。

创建DataLoader:

# 创建 DataLoader
dataloader = DataLoader(
    dataset,          # 数据集
    batch_size=10,    # 批量大小
    shuffle=True,     # 是否打乱数据
    num_workers=2     # 使用 2 个子进程加载数据
)

遍历:

# 遍历 DataLoader
# enumerate返回一个枚举对象(iterator),生成由索引和值组成的元组
for batch_idx, (samples, labels) in enumerate(dataloader):
    print(f"Batch {batch_idx}:")
    print("Samples:", samples)
    print("Labels:", labels)

案例:

import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader


# 定义数据加载类
class CustomDataset(Dataset):
	#略......


def test1():
    # 简单的数据集准备
    data_x = torch.randn(666, 20, requires_grad=True, dtype=torch.float32)
    data_y = torch.randn(data_x.size(0), 1, dtype=torch.float32)
    dataset = CustomDataset(data_x, data_y)

    # 构建数据加载器
    data_loader = DataLoader(dataset, batch_size=8, shuffle=True)
    for i, (batch_x, batch_y) in enumerate(data_loader):
        print(batch_x, batch_y)
        break

if __name__ == "__main__":
    test1()

2.数据集加载案例

通过一些数据集的加载案例,真正了解数据类及数据加载器。

2.1 加载csv数据集

import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd


class MyCsvDataset(Dataset):
    def __init__(self, filename):
        df = pd.read_csv(filename)
        # 删除文字列
        df = df.drop(["学号", "姓名"], axis=1)
        # 转换为tensor
        data = torch.tensor(df.values)
        # 最后一列以前的为data,最后一列为label
        self.data = data[:, :-1]
        self.label = data[:, -1]
        self.len = len(self.data)

    def __len__(self):
        return self.len

    def __getitem__(self, index):
        idx = min(max(index, 0), self.len - 1)
        return self.data[idx], self.label[idx]


def test001():
    excel_path = r"./大数据答辩成绩表.csv"
    dataset = MyCsvDataset(excel_path)
    dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
    for i, (data, label) in enumerate(dataloader):
        print(i, data, label)


if __name__ == "__main__":
    test001()

上述示例数据构建器改成TensorDataset

def build_dataset(filepath):
    df = pd.read_csv(filepath)
    df.drop(columns=['学号', '姓名'], inplace=True)
    data = df.iloc[..., :-1]
    labels = df.iloc[..., -1]

    x = torch.tensor(data.values, dtype=torch.float)
    y = torch.tensor(labels.values)

    dataset = TensorDataset(x, y)

    return dataset


def test001():
    filepath = r"./大数据答辩成绩表.csv"
    dataset = build_dataset(filepath)
    dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
    for i, (data, label) in enumerate(dataloader):
        print(i, data, label)

2.2 加载图片数据集

使用ImageFolder加载图片集

ImageFolder 会根据文件夹的结构来加载图像数据。它假设每个子文件夹对应一个类别,文件夹名称即为类别名称。例如,一个典型的文件夹结构如下:

root/
    class1/
        img1.jpg
        img2.jpg
        ...
    class2/
        img1.jpg
        img2.jpg
        ...

在这个结构中:

  • root 是根目录。

  • class1class2 等是类别名称。

  • 每个类别文件夹中的图像文件会被加载为一个样本。

ImageFolder构造函数如下:

torchvision.datasets.ImageFolder(root, transform=None, target_transform=None, is_valid_file=None)

参数解释

  • root:字符串,指定图像数据集的根目录。

  • transform:可选参数,用于对图像进行预处理。通常是一个 torchvision.transforms 的组合。

  • target_transform:可选参数,用于对目标(标签)进行转换。

  • is_valid_file:可选参数,用于过滤无效文件。如果提供,只有返回 True 的文件才会被加载。

import torch
from torchvision import datasets, transforms
import os
from torch.utils.data import DataLoader
from matplotlib import pyplot as plt

torch.manual_seed(42)

def load():
    path = os.path.join(os.path.dirname(__file__), 'dataset')
    print(path)

    transform = transforms.Compose([
        transforms.Resize((112, 112)),
        transforms.ToTensor()
    ])

    dataset = datasets.ImageFolder(path, transform=transform)
    dataloader = DataLoader(dataset, batch_size=1, shuffle=True)

    for x,y in dataloader:
        x = x.squeeze(0).permute(1, 2, 0).numpy()
        plt.imshow(x)
        plt.show()
        print(y[0])
        break


if __name__ == '__main__':
    load()

2.3 加载官方数据集

在 PyTorch 中官方提供了一些经典的数据集,如 CIFAR-10、MNIST、ImageNet 等,可以直接使用这些数据集进行训练和测试。

数据集:Datasets — Torchvision 0.23 documentation

常见数据集:

  • MNIST: 手写数字数据集,包含 60,000 张训练图像和 10,000 张测试图像。

  • CIFAR10: 包含 10 个类别的 60,000 张 32x32 彩色图像,每个类别 6,000 张图像。

  • CIFAR100: 包含 100 个类别的 60,000 张 32x32 彩色图像,每个类别 600 张图像。

  • COCO: 通用对象识别数据集,包含超过 330,000 张图像,涵盖 80 个对象类别。

torchvision.transforms 和 torchvision.datasets 是 PyTorch 中处理计算机视觉任务的两个核心模块,它们为图像数据的预处理和标准数据集的加载提供了强大支持。

transforms 模块提供了一系列用于图像预处理的工具,可以将多个变换组合成处理流水线。

datasets 模块提供了多种常用计算机视觉数据集的接口,可以方便地下载和加载。

参考如下:

import torch
from torch.utils.data import Dataset, DataLoader
import torchvision
from torchvision import transforms, datasets


def test():
    transform = transforms.Compose(
        [
            transforms.ToTensor(),
        ]
    )
    # 训练数据集
    data_train = datasets.MNIST(
        root="./data",
        train=True,
        download=True,
        transform=transform,
    )
    trainloader = DataLoader(data_train, batch_size=8, shuffle=True)
    for x, y in trainloader:
        print(x.shape)
        print(y)
        break

    # 测试数据集
    data_test = datasets.MNIST(
        root="./data",
        train=False,
        download=True,
        transform=transform,
    )
    testloader = DataLoader(data_test, batch_size=8, shuffle=True)
    for x, y in testloader:
        print(x.shape)
        print(y)
        break


def test006():
    transform = transforms.Compose(
        [
            transforms.ToTensor(),
        ]
    )
    # 训练数据集
    data_train = datasets.CIFAR10(
        root="./data",
        train=True,
        download=True,
        transform=transform,
    )
    trainloader = DataLoader(data_train, batch_size=4, shuffle=True, num_workers=2)
    for x, y in trainloader:
        print(x.shape)
        print(y)
        break
    # 测试数据集
    data_test = datasets.CIFAR10(
        root="./data",
        train=False,
        download=True,
        transform=transform,
    )
    testloader = DataLoader(data_test, batch_size=4, shuffle=False, num_workers=2)
    for x, y in testloader:
        print(x.shape)
        print(y)
        break


if __name__ == "__main__":
    test()
    test006()

四、激活函数

激活函数的作用是在隐藏层引入非线性,使得神经网络能够学习和表示复杂的函数关系,使网络具备非线性能力,增强其表达能力。

1.基础概念

通过认识线性和非线性的基础概念,深刻理解激活函数存在的价值。

1.1 线性理解

如果在隐藏层不使用激活函数,那么整个神经网络会表现为一个线性模型。我们可以通过数学推导来展示这一点。

假设:

  • 神经网络有L 层,每层的输出为\mathbf{a}^{(l)}

  • 每层的权重矩阵为\mathbf{W}^{(l)} ,偏置向量为\mathbf{b}^{(l)}

  • 输入数据为\mathbf{x},输出为\mathbf{a}^{(L)}

一层网络的情况

对于单层网络(输入层到输出层),如果没有激活函数,输出\mathbf{a}^{(1)}可以表示为:\mathbf{a}^{(1)} = \mathbf{W}^{(1)} \mathbf{x} + \mathbf{b}^{(1)}

两层网络的情况

假设我们有两层网络,且每层都没有激活函数,则:

  • 第一层的输出:\mathbf{a}^{(1)} = \mathbf{W}^{(1)} \mathbf{x} + \mathbf{b}^{(1)}

  • 第二层的输出:\mathbf{a}^{(2)} = \mathbf{W}^{(2)} \mathbf{a}^{(1)} + \mathbf{b}^{(2)}

\mathbf{a}^{(1)}代入到\mathbf{a}^{(2)}中,可以得到:

\mathbf{a}^{(2)} = \mathbf{W}^{(2)} (\mathbf{W}^{(1)} \mathbf{x} + \mathbf{b}^{(1)}) + \mathbf{b}^{(2)}

\mathbf{a}^{(2)} = \mathbf{W}^{(2)} \mathbf{W}^{(1)} \mathbf{x} + \mathbf{W}^{(2)} \mathbf{b}^{(1)} + \mathbf{b}^{(2)}

我们可以看到,输出\mathbf{a}^{(2)}是输入\mathbf{x}的线性变换,因为:\mathbf{a}^{(2)} = \mathbf{W}' \mathbf{x} + \mathbf{b}'其中\mathbf{W}' = \mathbf{W}^{(2)} \mathbf{W}^{(1)}\mathbf{b}' = \mathbf{W}^{(2)} \mathbf{b}^{(1)} + \mathbf{b}^{(2)}

多层网络的情况

如果有L层,每层都没有激活函数,则第l层的输出为:\mathbf{a}^{(l)} = \mathbf{W}^{(l)} \mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}

通过递归代入,可以得到:

\mathbf{a}^{(L)} = \mathbf{W}^{(L)} \mathbf{W}^{(L-1)} \cdots \mathbf{W}^{(1)} \mathbf{x} + \mathbf{W}^{(L)} \mathbf{W}^{(L-1)} \cdots \mathbf{W}^{(2)} \mathbf{b}^{(1)} + \mathbf{W}^{(L)} \mathbf{W}^{(L-1)} \cdots \mathbf{W}^{(3)} \mathbf{b}^{(2)} + \cdots + \mathbf{b}^{(L)}

表达式可简化为:

\mathbf{a}^{(L)} = \mathbf{W}'' \mathbf{x} + \mathbf{b}''

其中,\mathbf{W}'' 是所有权重矩阵的乘积,\mathbf{b}''是所有偏置项的线性组合。

如此可以看得出来,无论网络多少层,意味着:

整个网络就是线性模型,无法捕捉数据中的非线性关系。

激活函数是引入非线性特性、使神经网络能够处理复杂问题的关键。

1.2 非线性可视化

我们可以通过可视化的方式去理解非线性的拟合能力:A Neural Network Playground

2.常见激活函数

激活函数通过引入非线性来增强神经网络的表达能力,对于解决线性模型的局限性至关重要。由于反向传播算法(BP)用于更新网络参数,因此激活函数必须是可微的,也就是说能够求导的。

2.1 sigmoid

Sigmoid激活函数是一种常见的非线性激活函数,特别是在早期神经网络中应用广泛。它将输入映射到0到1之间的值,因此非常适合处理概率问题。

2.1.1 公式

Sigmoid函数的数学表达式为:

f(x) = \sigma(x) = \frac{1}{1 + e^{-x}}

其中,e 是自然常数(约等于2.718),x 是输入。

2.1.2 特征
  1. 将任意实数输入映射到 (0, 1)之间,因此非常适合处理概率场景。

  2. sigmoid函数一般只用于二分类的输出层。

  3. 微分性质: 导数计算比较方便,可以用自身表达式来表示:

\sigma'(x)=\sigma(x)\cdot(1-\sigma(x))

2.1.3 缺点
  • 梯度消失:

    • 在输入非常大或非常小时,Sigmoid函数的梯度会变得非常小,接近于0。这导致在反向传播过程中,梯度逐渐衰减。

    • 最终使得早期层的权重更新非常缓慢,进而导致训练速度变慢甚至停滞。

  • 信息丢失:输入100和输入10000经过sigmoid的激活值几乎都是等于 1 的,但是输入的数据却相差 100 倍。

  • 计算成本高: 由于涉及指数运算,Sigmoid的计算比ReLU等函数更复杂,尽管差异并不显著。

2.1.4 函数绘制

通过代码实现函数和导函数绘制:

import torch
import matplotlib.pyplot as plt

# plt支持中文
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False


def test001():
    # 一行两列绘制图像
    _, ax = plt.subplots(1, 2)
    # 绘制函数图像
    x = torch.linspace(-10, 10, 100)
    y = torch.sigmoid(x)
    # 网格
    ax[0].grid(True)
    ax[0].set_title("sigmoid 函数曲线图")
    ax[0].set_xlabel("x")
    ax[0].set_ylabel("y")
    # 在第一行第一列绘制sigmoid函数曲线图
    ax[0].plot(x, y)

    # 绘制sigmoid导数曲线图
    x = torch.linspace(-10, 10, 100, requires_grad=True)
    # y = torch.sigmoid(x) * (1 - torch.sigmoid(x))
    # 自动求导
    torch.sigmoid(x).sum().backward()
    ax[1].grid(True)
    ax[1].set_title("sigmoid 函数导数曲线图", color="red")
    ax[1].set_xlabel("x")
    ax[1].set_ylabel("y")
    # ax[1].plot(x.detach().numpy(), y.detach())
    # 用自动求导的结果绘制曲线图
    ax[1].plot(x.detach().numpy(), x.grad.detach().numpy())
    # 设置曲线颜色
    ax[1].lines[0].set_color("red")

    plt.show()


if __name__ == "__main__":
    test001()

2.2 tanh

tanh(双曲正切)是一种常见的非线性激活函数,常用于神经网络的隐藏层。tanh 函数也是一种S形曲线,输出范围为(−1,1)。

2.2.1 公式

tanh数学表达式为:

{tanh}(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}
2.2.2 特征
  1. 输出范围: 将输入映射到(-1, 1)之间,因此输出是零中心的。相比于Sigmoid函数,这种零中心化的输出有助于加速收敛。

  2. 对称性: Tanh函数是关于原点对称的奇函数,因此在输入为0时,输出也为0。这种对称性有助于在训练神经网络时使数据更平衡。

  3. 平滑性: Tanh函数在整个输入范围内都是连续且可微的,这使其非常适合于使用梯度下降法进行优化。

\frac{d}{dx} \text{tanh}(x) = 1 - \text{tanh}^2(x)

2.2.3 缺点
  1. 梯度消失: 虽然一定程度上改善了梯度消失问题,但在输入值非常大或非常小时导数还是非常小,这在深层网络中仍然是个问题。这是因为每一层的梯度都会乘以一个小于1的值,经过多层乘积后,梯度会变得非常小,导致训练过程变得非常缓慢,甚至无法收敛。

  2. 计算成本: 由于涉及指数运算,Tanh的计算成本还是略高,尽管差异不大。

2.2.4 函数绘制
import torch
import matplotlib.pyplot as plt

# plt支持中文
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False


def test001():
    # 一行两列绘制图像
    _, ax = plt.subplots(1, 2)
    # 绘制函数图像
    x = torch.linspace(-10, 10, 100)
    y = torch.tanh(x)
    # 网格
    ax[0].grid(True)
    ax[0].set_title("tanh 函数曲线图")
    ax[0].set_xlabel("x")
    ax[0].set_ylabel("y")
    # 在第一行第一列绘制tanh函数曲线图
    ax[0].plot(x, y)

    # 绘制tanh导数曲线图
    x = torch.linspace(-10, 10, 100, requires_grad=True)
    # y = torch.tanh(x) * (1 - torch.tanh(x))
    # 自动求导:需要标量才能反向传播
    torch.tanh(x).sum().backward()
    ax[1].grid(True)
    ax[1].set_title("tanh 函数导数曲线图", color="red")
    ax[1].set_xlabel("x")
    ax[1].set_ylabel("x.grad")
    # ax[1].plot(x.detach().numpy(), y.detach())
    # 用自动求导的结果绘制曲线图
    ax[1].plot(x.detach().numpy(), x.grad.detach().numpy())
    # 设置曲线颜色
    ax[1].lines[0].set_color("red")

    plt.show()


if __name__ == "__main__":
    test001()

2.3 ReLU

ReLU(Rectified Linear Unit)是深度学习中最常用的激活函数之一,它的全称是修正线性单元。ReLU 激活函数的定义非常简单,但在实践中效果非常好。

2.3.1 公式

ReLU 函数定义如下:

\text{ReLU}(x) = \max(0, x)

即ReLU对输入x进行非线性变换:

2.3.2 特征
  1. 计算简单:ReLU 的计算非常简单,只需要对输入进行一次比较运算,这在实际应用中大大加速了神经网络的训练。

  2. ReLU 函数的导数是分段函数:
    \text{ReLU}'(x)=\begin{cases}1,&\text{if } x>0\\0,&\text{if }x\leq0\end{cases}

  3. 缓解梯度消失问题:相比于 Sigmoid 和 Tanh 激活函数,ReLU 在正半区的导数恒为 1,这使得深度神经网络在训练过程中可以更好地传播梯度,不存在饱和问题。

  4. 稀疏激活:ReLU在输入小于等于 0 时输出为 0,这使得 ReLU 可以在神经网络中引入稀疏性(即一些神经元不被激活),这种稀疏性可以减少网络中的冗余信息,提高网络的效率和泛化能力。

2.3.3 缺点

神经元死亡:由于ReLU在x≤0时输出为0,如果某个神经元输入值是负,那么该神经元将永远不再激活,成为“死亡”神经元。随着训练的进行,网络中可能会出现大量死亡神经元,从而会降低模型的表达能力。

2.3.4 函数绘图
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt

# 中文问题
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False


def test006():
    # 输入数据x
    x = torch.linspace(-20, 20, 1000)
    y = F.relu(x)
    # 绘制一行2列
    _, ax = plt.subplots(1, 2)
    ax[0].plot(x.numpy(), y.numpy())
    # 显示坐标格子
    ax[0].grid()
    ax[0].set_title("relu 激活函数")
    ax[0].set_xlabel("x")
    ax[0].set_ylabel("y")

    # 绘制导数函数
    x = torch.linspace(-20, 20, 1000, requires_grad=True)
    F.relu(x).sum().backward()
    ax[1].plot(x.detach().numpy(), x.grad.numpy())
    ax[1].grid()
    ax[1].set_title("relu 激活函数导数", color="red")
    # 设置绘制线色颜色
    ax[1].lines[0].set_color("red")
    ax[1].set_xlabel("x")
    ax[1].set_ylabel("x.grad")

    plt.show()


if __name__ == "__main__":
    test006()

2.4 LeakyReLU

Leaky ReLU是一种对 ReLU 函数的改进,旨在解决 ReLU 的一些缺点,特别是Dying ReLU 问题。Leaky ReLU 通过在输入为负时引入一个小的负斜率来改善这一问题。

2.4.1 公式

Leaky ReLU 函数的定义如下:

\text{Leaky ReLU}(x)=\begin{cases}x,&\text{if } x>0\\\alpha x,&\text{if } x\leq0\end{cases}

其中,\alpha 是一个非常小的常数(如 0.01),它控制负半轴的斜率。这个常数 \alpha是一个超参数,可以在训练过程中可自行进行调整。

2.4.2 特征
  1. 避免神经元死亡:通过在x\leq 0 区域引入一个小的负斜率,这样即使输入值小于等于零,Leaky ReLU仍然会有梯度,允许神经元继续更新权重,避免神经元在训练过程中完全“死亡”的问题。

  2. 计算简单:Leaky ReLU 的计算与 ReLU 相似,只需简单的比较和线性运算,计算开销低。

2.4.3 缺点
  1. 参数选择:\alpha 是一个需要调整的超参数,选择合适的\alpha 值可能需要实验和调优。

  2. 出现负激活:如果\alpha 设定得不当,仍然可能导致激活值过低。

2.4.4 函数绘制
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt

# 中文设置
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False


def test006():
    x = torch.linspace(-5, 5, 200)
    # 设置leaky_relu的负斜率超参数
    slope = 0.03
    y = F.leaky_relu(x, slope)
    # 一行两列
    _, ax = plt.subplots(1, 2)
    # 开始绘制函数曲线图
    ax[0].plot(x, y)
    ax[0].set_title("Leaky ReLU 函数曲线图")
    ax[0].set_xlabel("x")
    ax[0].set_ylabel("y")
    ax[0].grid(True)

    # 绘制leaky_relu的梯度曲线图
    x = torch.linspace(-5, 5, 200, requires_grad=True)
    F.leaky_relu(x, slope).sum().backward()
    ax[1].plot(x.detach().numpy(), x.grad)
    ax[1].set_title("Leaky ReLU 梯度曲线图", color="red")
    ax[1].set_xlabel("x")
    ax[1].set_ylabel("x.grad")
    ax[1].grid(True)
    # 设置线的颜色
    ax[1].lines[0].set_color("red")

    plt.show()


if __name__ == "__main__":
    test006()

2.5 softmax

Softmax激活函数通常用于分类问题的输出层,它能够将网络的输出转换为概率分布,使得输出的各个类别的概率之和为 1。Softmax 特别适合用于多分类问题。

2.5.1 公式

假设神经网络的输出层有n个节点,每个节点的输入为z_i,则 Softmax 函数的定义如下:

\mathrm{Softmax}(z_i)=\frac{e^{z_i}}{\sum_{j=1}^ne^{z_j}}

给定输入向量 z=[z_1,z_2,…,z_n]

1.指数变换:对每个 z_i进行指数变换,得到t = [e^{z_1},e^{z_2},...,e^{z_n}],使z的取值区间从(-\infty,+\infty)变为(0,+\infty)

2.将所有指数变换后的值求和,得到s = e^{z_1} + e^{z_2} + ... + e^{z_n} = \Sigma_{j=1}^ne^{z_j}

3.将t中每个 e^{z_i}除以归一化因子s,得到概率分布:

softmax(z) =[\frac{e^{z_1}}{s},\frac{e^{z_2}}{s},...,\frac{e^{z_n}}{s}]=[\frac{e^{z_1}}{\Sigma_{j=1}^ne^{z_j}},\frac{e^{z_2}}{\Sigma_{j=1}^ne^{z_j}},...,\frac{e^{z_n}}{\Sigma_{j=1}^ne^{z_j}}]

\mathrm{Softmax}(z_i)=\frac{e^{z_i}}{\sum_{j=1}^ne^{z_j}}

从上述公式可以看出:

  1. 每个输出值在 (0,1)之间

  2. Softmax()对向量的值做了改变,但其位置不变

  3. 所有输出值之和为1,即

sum(softmax(z)) =\frac{e^{z_1}}{s}+\frac{e^{z_2}}{s}+...+\frac{e^{z_n}}{s}=\frac{s}{s}=1

2.5.2 特征

将输出转化为概率:通过Softmax,可以将网络的原始输出转化为各个类别的概率,从而可以根据这些概率进行分类决策。

概率分布:Softmax的输出是一个概率分布,即每个输出值\text{Softmax}(z_i)都是一个介于0和1之间的数,并且所有输出值的和为 1:

\sum_{i=1}^n\text{Softmax}(z_i)=1

突出差异:Softmax会放大差异,使得概率最大的类别的输出值更接近1,而其他类别更接近0。

在实际应用中,Softmax常与交叉熵损失函数Cross-Entropy Loss结合使用,用于多分类问题。在反向传播中,Softmax的导数计算是必需的。

2.5.3 缺点

数值不稳定性:在计算过程中,如果z_i的数值过大,e^{z_i}可能会导致数值溢出。因此在实际应用中,经常会对z_i进行调整,如减去最大值以确保数值稳定。

\mathrm{Softmax}(z_i)=\frac{e^{z_i-\max(z)}}{\sum_{j=1}^ne^{z_j-\max(z)}}

z_i-\max(z)是一个非正数,由于e^{z_i−max(z)}的形式,当z_i接近 max(z) 时,e^{z_i−max(z)} 的值会接近 1,而当 z_i 远小于 max(z) 时,e^{z_i−max(z)} 的值会接近 0。这使得 Softmax 函数的输出中,最大值对应的概率会相对较大,而其他值对应的概率会相对较小,从而提高数值稳定性。

这种调整不会改变Softmax的概率分布结果,因为从数学的角度讲相当于分子、分母都除以了e^{\max(z)}。

在 PyTorch 中,torch.nn.functional.softmax 函数就自动处理了数值稳定性问题。

难以处理大量类别:Softmax在处理类别数非常多的情况下(如大模型中的词汇表)计算开销会较大。

2.5.4 代码实现
import torch
import torch.nn as nn

# 表示4分类,每个样本全连接后得到4个得分,下面示例模拟的是两个样本的得分
input_tensor = torch.tensor([[-1.0, 2.0, -3.0, 4.0], [-2, 3, -3, 9]])

softmax = nn.Softmax()
output_tensor = softmax(input_tensor)
# 关闭科学计数法
torch.set_printoptions(sci_mode=False)
print("输入张量:", input_tensor)
print("输出张量:", output_tensor)

"""
输入张量: tensor([[-1.,  2., -3.,  4.],
        [-2.,  3., -3.,  9.]])
输出张量: tensor([[    0.0059,     0.1184,     0.0008,     0.8749],
        [    0.0000,     0.0025,     0.0000,     0.9975]])
"""

3.如何选择

更多激活函数可以查看官方文档:https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity

那这么多激活函数应该如何选择呢?

3.1 隐藏层
  1. 优先选ReLU;

  2. 如果ReLU效果不咋地,那么尝试其他激活,如Leaky ReLU等;

  3. 使用ReLU时注意神经元死亡问题, 避免出现过多神经元死亡;

  4. 避免使用sigmoid,尝试使用tanh;

3.2 输出层
  1. 二分类问题选择sigmoid激活函数;

  2. 多分类问题选择softmax激活函数;

五、参数初始化

神经网络的参数初始化是训练深度学习模型的关键步骤之一。初始化参数(通常是权重和偏置)会对模型的训练速度、收敛性以及最终的性能产生重要影响。下面是关于神经网络参数初始化的一些常见方法及其相关知识点。

官方文档参考:torch.nn.init — PyTorch 2.8 documentation

1.固定值初始化

固定值初始化是指在神经网络训练开始时,将所有权重或偏置初始化为一个特定的常数值。这种初始化方法虽然简单,但在实际深度学习应用中通常并不推荐。

1.1 全零初始化

将神经网络中的所有权重参数初始化为0。

方法:将所有权重初始化为零。

缺点:导致对称性破坏,每个神经元在每一层中都会执行相同的计算,模型无法学习。

应用场景:通常不用来初始化权重,但可以用来初始化偏置。

对称性问题

  • 现象:同一层的所有神经元具有完全相同的初始权重和偏置。

  • 后果:

    • 在反向传播时,所有神经元会收到相同的梯度,导致权重更新完全一致。

    • 无论训练多久,同一层的神经元本质上会保持相同的功能(相当于“一个神经元”的多个副本),极大降低模型的表达能力。

import torch
import torch.nn as nn

def test004():
    # 3. 全0参数初始化
    linear = nn.Linear(in_features=6, out_features=4)
    # 初始化权重参数
    nn.init.zeros_(linear.weight)
    # 打印权重参数
    print(linear.weight)


if __name__ == "__main__":
    test004()
"""
Parameter containing:
tensor([[0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.]], requires_grad=True)
"""
1.2 全1初始化

全1初始化会导致网络中每个神经元接收到相同的输入信号,进而输出相同的值,这就无法进行学习和收敛。所以全1初始化只是一个理论上的初始化方法,但在实际神经网络的训练中并不适用。

import torch
import torch.nn as nn


def test003():
    # 3. 全1参数初始化
    linear = nn.Linear(in_features=6, out_features=4)
    # 初始化权重参数
    nn.init.ones_(linear.weight)
    # 打印权重参数
    print(linear.weight)


if __name__ == "__main__":
    test003()

"""
Parameter containing:
tensor([[1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1.]], requires_grad=True)
"""
1.3 任意常数初始化

将所有参数初始化为某个非零的常数(如 0.1,-1 等)。虽然不同于全0和全1,但这种方法依然不能避免对称性破坏的问题。

import torch
import torch.nn as nn


def test002():
    # 2. 固定值参数初始化
    linear = nn.Linear(in_features=6, out_features=4)
    # 初始化权重参数
    nn.init.constant_(linear.weight, 0.63)
    # 打印权重参数
    print(linear.weight)
    pass


if __name__ == "__main__":
    test002()

"""
Parameter containing:
tensor([[0.6300, 0.6300, 0.6300, 0.6300, 0.6300, 0.6300],
        [0.6300, 0.6300, 0.6300, 0.6300, 0.6300, 0.6300],
        [0.6300, 0.6300, 0.6300, 0.6300, 0.6300, 0.6300],
        [0.6300, 0.6300, 0.6300, 0.6300, 0.6300, 0.6300]], requires_grad=True)
"""
import torch
import torch.nn as nn


def test002():
    net = nn.Linear(2, 2, bias=True)
    # 假设一个数值
    x = torch.tensor([[0.1, 0.95]])

    # 初始化权重参数
    net.weight.data = torch.tensor([
        [0.1, 0.2], 
        [0.3, 0.4]
    ])

    # 输出什么:权重参数会转置
    output = net(x)
    print(output, net.bias)
    pass


if __name__ == "__main__":
    test002()
  • 所有输入特征被同等对待

  • 无法学习特征间的不同重要性

2.随机初始化

方法:将权重初始化为随机的小值,通常从正态分布或均匀分布中采样。

应用场景:这是最基本的初始化方法,通过随机初始化避免对称性破坏。

代码:随机分布之均匀初始化

import torch
import torch.nn as nn


def test001():
    # 1. 均匀分布随机初始化
    linear = nn.Linear(in_features=6, out_features=4)
    # 初始化权重参数
    nn.init.uniform_(linear.weight)
    # 打印权重参数
    print(linear.weight)


if __name__ == "__main__":
    test001()

"""
Parameter containing:
tensor([[0.4080, 0.7444, 0.7616, 0.0565, 0.2589, 0.0562],
        [0.1485, 0.9544, 0.3323, 0.9802, 0.1847, 0.6254],
        [0.6256, 0.2047, 0.5049, 0.3547, 0.9279, 0.8045],
        [0.1994, 0.7670, 0.8306, 0.1364, 0.4395, 0.0412]], requires_grad=True)
"""

代码:正态分布初始化

import torch
import torch.nn as nn


def test005():
    # 5. 正太分布初始化
    linear = nn.Linear(in_features=6, out_features=4)
    # 初始化权重参数
    nn.init.normal_(linear.weight, mean=0, std=1)
    # 打印权重参数
    print(linear.weight)


if __name__ == "__main__":
    test005()

"""
Parameter containing:
tensor([[ 1.5321,  0.2394,  0.0622,  0.4482,  0.0757, -0.6056],
        [ 1.0632,  1.8069,  1.1189,  0.2448,  0.8095, -0.3486],
        [-0.8975,  1.8253, -0.9931,  0.7488,  0.2736, -1.3892],
        [-0.3752,  0.0500, -0.1723, -0.4370, -1.5334, -0.5393]],
       requires_grad=True)
"""

3.Xavier 初始化

前置知识

均匀分布:

均匀分布的概率密度函数(PDF):

计算期望值(均值):

计算方差(二阶矩减去均值的平方):

  • 先计算E[X^2]

代入方差公式:

Xavier 初始化(由 Xavier Glorot 在 2010 年提出)是一种自适应权重初始化方法,专门为解决神经网络训练初期的梯度消失或爆炸问题而设计。Xavier 初始化也叫做Glorot初始化。Xavier 初始化的核心思想是根据输入和输出的维度来初始化权重,使得每一层的输出的方差保持一致。具体来说,权重的初始化范围取决于前一层的神经元数量(输入维度)和当前层的神经元数量(输出维度)。

方法:根据输入和输出神经元的数量来选择权重的初始值。

数学原理:

(1) 前向传播的方差一致性

假设输入 x 的均值为 0,方差为\sigma _x^2,权重 W的均值为 0,方差为\sigma _x^2,则输出 z=Wx的方差为:

为了使 Var(z)=Var(x),需要:

其中 n_{in}是输入维度(fan_in)。这里乘以 nin 的原因是,输出 z 是由 nin 个输入 x 的线性组合得到的,每个输入 x 都与一个权重 W 相乘。因此,输出 z 的方差是 nin 个独立的 Wx 项的方差之和。

(2) 反向传播的梯度方差一致性

在反向传播过程中,梯度 {\frac{\partial_L }{\partial_x}}{}=w 是通过链式法则计算得到的,其中 L 是损失函数,x 是输入,z 是输出。梯度 {\frac{\partial_L }{\partial_x}}{}可以表示为:

假设 z=Wx,其中 W 是权重矩阵,那么 {\frac{\partial_x }{\partial_x}}{}=W。因此,梯度 {\frac{\partial_L }{\partial_x}}{}可以写为:{\frac{\partial_L }{\partial_x}}{}={\frac{\partial_L }{\partial_x}}{}W

反向传播时梯度 {\frac{\partial_L }{\partial_x}}{}的方差应与{\frac{\partial_L }{\partial_z}}{}相同,因此

其中 n_{out}是输出维度(fan_out)。为了保持梯度的方差一致性,我们需要确保每个输入维度 nin 的梯度方差与输出维度 nout 的梯度方差相同。因此,我们需要将 W 的方差乘以 nout,以确保梯度的方差在反向传播过程中保持一致。

(3) 综合考虑

为了同时平衡前向传播和反向传播,Xavier 采用:

权重从以下分布中采样:

均匀分布

正态分布

其中 n_{\text{in}} 是当前层的输入神经元数量,n_{\text{out}}是输出神经元数量。

在前向传播中,输出的方差受 n_{in} 影响。在反向传播中,梯度的方差受 n_{out} 影响。

优点:平衡了输入和输出的方差,适合Sigmoid 和 Tanh 激活函数。

应用场景:常用于浅层网络或使用Sigmoid 、Tanh 激活函数的网络。

import torch
import torch.nn as nn


def test007():
    # Xavier初始化:正态分布
    linear = nn.Linear(in_features=6, out_features=4)
    nn.init.xavier_normal_(linear.weight)
    print(linear.weight)

    # Xavier初始化:均匀分布
    linear = nn.Linear(in_features=6, out_features=4)
    nn.init.xavier_uniform_(linear.weight)
    print(linear.weight)


if __name__ == "__main__":
    test007()

"""
Parameter containing:
tensor([[-0.4838,  0.4121, -0.3171, -0.2214, -0.8666, -0.4340],
        [ 0.1059,  0.6740, -0.1025, -0.1006,  0.5757, -0.1117],
        [ 0.7467, -0.0554, -0.5593, -0.1513, -0.5867, -0.1564],
        [-0.1058,  0.5266,  0.0243, -0.5646, -0.4982, -0.1844]],
       requires_grad=True)
Parameter containing:
tensor([[-0.5263,  0.3455,  0.6449,  0.2807, -0.3698, -0.6890],
        [ 0.1578, -0.3161, -0.1910, -0.4318, -0.5760,  0.3746],
        [ 0.2017, -0.6320, -0.4060,  0.3903,  0.3103, -0.5881],
        [ 0.6212,  0.3077,  0.0783, -0.6187,  0.3109, -0.6060]],
       requires_grad=True)
"""

4.He初始化

也叫kaiming 初始化。He 初始化的核心思想是调整权重的初始化范围,使得每一层的输出的方差保持一致。与 Xavier 初始化不同,He 初始化专门针对 ReLU 激活函数的特性进行了优化。

数学推导

(1) 前向传播的方差一致性

对于 ReLU 激活函数,输出的方差为:

(2) 反向传播的梯度一致性

类似地,反向传播时梯度方差需满足:

(3) 两种模式

  • fan_in 模式(默认):优先保证前向传播稳定,方差 \frac{2}{n_{in}}。

  • fan_out 模式:优先保证反向传播稳定,方差\frac{2}{n_{out}}。

方法:专门为 ReLU 激活函数设计。权重从以下分布中采样:

均匀分布:

正态分布:

其中 n_{\text{in}} 是当前层的输入神经元数量。

优点:适用于ReLU 和 Leaky ReLU 激活函数。

应用场景:深度网络,尤其是使用 ReLU 激活函数时。

import torch
import torch.nn as nn


def test006():
    # He初始化:正态分布
    linear = nn.Linear(in_features=6, out_features=4)
    nn.init.kaiming_normal_(linear.weight, nonlinearity="relu", mode='fan_in')
    print(linear.weight)

    # He初始化:均匀分布
    linear = nn.Linear(in_features=6, out_features=4)
    nn.init.kaiming_uniform_(linear.weight, nonlinearity="relu", mode='fan_out')
    print(linear.weight)


if __name__ == "__main__":
    test006()
"""
Parameter containing:
tensor([[ 1.4020,  0.2030,  0.3585, -0.7419,  0.6077,  0.0178],
        [-0.2860, -1.2135,  0.0773, -0.3750, -0.5725,  0.9756],
        [ 0.2938, -0.6159, -1.1721,  0.2093,  0.4212,  0.9079],
        [ 0.2050,  0.3866, -0.3129, -0.3009, -0.6659, -0.2261]],
       requires_grad=True)

Parameter containing:
tensor([[-0.1924, -0.6155, -0.7438, -0.2796, -0.1671, -0.2979],
        [ 0.7609,  0.9836, -0.0961,  0.7139, -0.8044, -0.3827],
        [ 0.1416,  0.6636,  0.9539,  0.4735, -0.2384, -0.1330],
        [ 0.7254, -0.4056, -0.7621, -0.6139, -0.6093, -0.2577]],
       requires_grad=True)
"""

5.总结

在使用Torch构建网络模型时,每个网络层的参数都有默认的初始化方法,同时还可以通过以上方法来对网络参数进行初始化。


总结

激活函数是重点,辨别Sigmoid、Tanh、ReLU、LeakyReLU和Softmax等常见激活函数的适用场景,要知道激活函数在引入非线性中的关键作用。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐