深度学习的定义

深度学习是机器学习 (ML) 的一个子集,指人工神经网络(由算法建模而成,能够像人的大脑一样工作)学习大量数据。

深度学习的工作原理是什么?

深度学习由神经网络层驱动。神经网络由一系列算法按照人类大脑的工作方式松散建模而成,而使用大量数据进行训练,即对神经网络的神经进行配置。经过训练后,深度学习模型可以处理新数据,能够摄取并实时分析多个来源的数据,无需人为干预。在深度学习中,图形处理单元 (GPU) 可以同时处理多个计算,以优化方式训练深度学习模型。

深度学习的概念源于对人工神经网络的研究,很多深度学习算法都使用神经网络进行表示,因为神经网络的性能精度和通用效果都非常好,于是业界习惯性地把深度学习算法等同于AI。

目标函数softmax

在使用深度学习来处理分类问题的时候,需要衡量目标类别与当前预测的差距,目标函数可以帮助衡量模型的好坏,softmax 就是一个目标函数: 讲结果变为一个概率值

Softmax函数,又称 归一化指数函数

该函数 是最流行的 分类任务目标函数,也是 Sigmoid函数 的一种 推广。可转换为交叉熵误差 (CE)

它将原始的实数输入(通常是神经网络的输出)转换成概率值。具体来说,Softmax 操作会把一个向量的每个元素转换为 [0, 1] 范围内的值,并且所有输出的和为 1,从而可以将它们解释为概率。

给定一个向量 z=[z1,z2,…,zn]z=[z1,z2,…,zn] 作为输入,Softmax 函数输出一个与输入向量形状相同的概率分布:

  • zi 是输入向量中的第 ii 个元素。
  • 输出的每个 Softmax(zi)Softmax(zi) 是一个概率值,所有概率值的和为 1。

例如:输入向量 [ 1 , 2 , 3 , 4 , 1 , 2 , 3 ] 对应的Softmax函数的值为 [ 0.024 , 0.064 , 0.175 , 0.475 , 0.024 , 0.064 , 0.175 。输出向量中拥有最大权重的项对应着输入向量中的最大值“4”。这也显示了这个函数通常的意义:

对向量进行归一化,凸显其中最大的值并抑制远低于最大值的其他分量。

交叉熵损失

交叉熵损失函数(Cross-Entropy Loss)用来计算 分类任务中 预测概率分布与真实标签分布之间的差异。交叉熵损失衡量了两个概率分布之间的距离,特别是在分类任务中,衡量了预测类别分布和实际标签分布的差异。

交叉熵损失的公式:

给定真实标签分布 y=[y1,y2,…,yn]y=[y1,y2,…,yn] 和模型的预测输出概率分布 p=[p1,p2,…,pn]p=[p1,p2,…,pn],交叉熵损失定义为:

  • yi 是真实标签(通常是一个 one-hot 向量,表示类别标签)。
  • pii 是模型预测的概率值,通常由 Softmax 得到。

Softmax 和交叉熵损失的结合

在多分类问题中,通常会先通过 Softmax 转换模型的输出为概率分布,再使用交叉熵损失计算损失值。因此,Softmax 和交叉熵损失经常是紧密配合使用的,但它们的功能和作用是不同的。

  • Softmax:将神经网络的原始输出(通常是未归一化的 logits)转换为概率分布。
  • 交叉熵损失:计算模型预测的概率分布与真实标签之间的差异。

交叉熵损失和均方误差损失对比分析

均方误差

  • cross_entropy 具有 rmse 不具有的优点:避免学习速率降低的情况。(注意这种效果仅限于输出层,隐藏层的学习速率与其使用的激活函数密切相关。)
  • 主要原因是逻辑回归配合 MSE 损失函数时,采用梯度下降法进行学习时,会出现模型一开始训练时,学习速率非常慢的情况(MSE 损失函数)
  • 下面这个更清晰
    https://zhuanlan.zhihu.com/p/35709485 交叉熵损失 更能捕捉到预测效果的差异

举例:模型预测输出:

均方误差损失的结果:

交叉熵损失的结果:

  • 使用逻辑函数得到概率,并结合交叉熵当损失函数时,在模型效果差的时候学习速度比较快,在模型效果好的时候学习速度变慢。
  • 均方损失:假设误差是正态分布,适用于线性的输出(如回归问题),特点是对于与真实结果差别越大,则惩罚力度越大,这并不适用于分类问题
  • 交叉熵损失:假设误差是二值分布,可以视为预测概率分布和真实概率分布的相似程度。在分类问题中有良好的应用。

w的初始值分布

Xavier Normal(也称作 Glorot Normal)是 权重初始化 的一种方法,旨在避免深度神经网络中训练过程中的梯度消失和梯度爆炸问题。

在训练深度神经网络时,如果权重初始化得不好,可能会导致训练难以进行。特别是在网络层数很深的情况下,梯度在反向传播时可能会变得非常小(梯度消失)或非常大(梯度爆炸),这会导致训练过程不稳定或收敛困难。

Xavier 初始化(也叫 Glorot 初始化)是为了解决这个问题提出的一种权重初始化方法,尤其适用于 sigmoidtanh 等激活函数的神经网络。Xavier Normal正态分布(也叫高斯分布)初始化方法的一种。它的关键思想是,根据神经网络的输入和输出层的大小来决定初始化权重的标准差,从而保持信号在网络中的传播不会太大或太小。

公式:

对于层 ll 的权重初始化,Xavier Normal 的标准差(σσ)是根据输入单元数(ninnin)和输出单元数(noutnout)来计算的,标准差的公式如下:

其中:

  • Nin 是当前层的输入神经元数量。
  • Nout 是当前层的输出神经元数量。

因此,每一层的权重矩阵会从一个均值为 0,标准差为 σ 的 正态分布 中随机采样。

比如 784+300, 就是 np.sqrt(2/1084)

为什么是这种初始化方法?

Xavier 初始化方法的目标是让每一层的 输入信号梯度信号 都能通过网络正常传播而不会衰减(梯度消失)或爆炸(梯度爆炸)。

a. 激活值的分布

如果初始化权重过大,经过激活函数(如 sigmoidtanh)的输出可能会饱和(即接近 0 或 1),导致梯度接近于 0,这就会导致梯度消失问题。反之,如果权重过小,信号在通过网络时会过度缩小,可能导致训练非常缓慢,甚至无法收敛。

Xavier 初始化通过调节权重的标准差,使得每层的输入和输出信号在初始化时有适当的方差,从而避免了过度压缩或爆炸的情况。

b. 避免梯度消失或爆炸

  • 通过选择适当的初始化标准差,Xavier 方法帮助保持梯度在网络中传播时不会迅速消失或变得太大,尤其在深层网络中非常重要。
  • 在激活函数为 sigmoidtanh 时,Xavier 初始化能使得每层的激活值分布保持在合理的范围内,从而避免梯度消失问题。

如何在 PyTorch 中使用 Xavier Normal 初始化:

在 PyTorch 中,你可以使用 torch.nn.init.xavier_normal_() 来应用 Xavier Normal 初始化。该方法会将张量的权重初始化为符合 Xavier Normal 的分布。

例如,初始化一个卷积层或全连接层的权重:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
import torch.nn as nn
import torch.nn.init as init

# 假设我们有一个卷积层
conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3)

# 使用 Xavier Normal 初始化卷积层的权重
init.xavier_normal_(conv.weight)

# 如果有偏置项,一般初始化为零
if conv.bias is not None:
init.zeros_(conv.bias)

5. 与其他初始化方法的对比:

  • Xavier Normal:适用于 tanhsigmoid 激活函数的网络。通过 n_in + n_out 来平衡输入输出的方差,适合解决梯度消失/爆炸问题。
  • He 初始化:类似于 Xavier 初始化,但适用于 ReLU 激活函数,标准差为 2ninnin2。ReLU 激活函数的输出通常会有更多的零值(因为负数的输出被“抑制”),因此 He 初始化会稍微增大标准差。
  • 均匀初始化:与 Xavier 相比,均匀分布初始化是从一个均匀分布中采样,而不是正态分布。这个方法通常会导致训练的不稳定,尤其是对于深层网络。

深度学习实现分类问题

数据预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import sklearn
import pandas as pd
import os
import sys
import time
from tqdm.auto import tqdm
import torch
import torch.nn as nn
import torch.nn.functional as F

print(sys.version_info)
for module in mpl, np, pd, sklearn, torch:
print(module.__name__, module.__version__)

device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu")
print(device)

from torchvision import datasets
#from torchvision.transforms import ToTensor
from torchvision import transforms

# 定义数据集的变换
transform = transforms.Compose([
transforms.ToTensor(),# 转换为tensor,进行归一化
# transforms.Normalize(mean, std) # 标准化,mean和std是数据集的均值和方差
])

# fashion_mnist图像分类数据集,衣服分类,60000张训练图片,10000张测试图片
train_ds = datasets.FashionMNIST(
root="data",
train= True,
download=False,
transform=transform
)

test_ds = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=transform
)

# torchvision 数据集里没有提供训练集和验证集的划分
# 当然也可以用 torch.utils.data.Dataset 实现人为划分

#计算均值和方差
def cal_mean_std(ds):
mean = 0.
std = 0.
for img, _ in ds: # 遍历每张图片,img.shape=[1,28,28]
mean += img.mean(dim=(1, 2))
std += img.std(dim=(1,2))
mean /= len(ds)
std /= len(ds)
return mean, std

print(cal_mean_std(train_ds))

# 从数据集到dataloader
train_loader = torch.utils.data.DataLoader(train_ds, batch_size=32, shuffle=True) #batch_size分批,shuffle洗牌
val_loader = torch.utils.data.DataLoader(test_ds, batch_size=32, shuffle=False)


在PyTorch中,DataLoader是一个迭代器,它封装了数据的加载和预处理过程,使得在训练机器学习模型时可以方便地批量加载数据。DataLoader主要负责以下几个方面:

  1. 批量加载数据DataLoader可以将数据集(Dataset)切分为更小的批次(batch),每次迭代提供一小批量数据,而不是单个数据点。这有助于模型学习数据中的统计依赖性,并且可以更高效地利用GPU等硬件的并行计算能力。

  2. 数据打乱:默认情况下,DataLoader会在每个epoch(训练周期)开始时打乱数据的顺序。这有助于模型训练时避免陷入局部最优解,并且可以提高模型的泛化能力。

  3. 多线程数据加载DataLoader支持多线程(通过参数num_workers)来并行地加载数据,这可以显著减少训练过程中的等待时间,尤其是在处理大规模数据集时。

  4. 数据预处理DataLoader可以与transforms结合使用,对加载的数据进行预处理,如归一化、标准化、数据增强等操作。

  5. 内存管理DataLoader负责管理数据的内存使用,确保在训练过程中不会耗尽内存资源。

  6. 易用性DataLoader提供了一个简单的接口,可以很容易地集成到训练循环中。

定义模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class NeuralNetwork(nn.Module): # 都是继承自nn.Module
def __init__(self):
super().__init__() # 继承父类的初始化方法,子类有父类的属性
self.flatten = nn.Flatten() # 展平层
self.linear_relu_stack = nn.Sequential(
nn.Linear(784, 300), # in_features=784, out_features=300, 784是输入特征数,300是输出特征数 Linear:全连接层
nn.ReLU(), # 激活函数
nn.Linear(300, 100),#隐藏层神经元数100
nn.ReLU(), # 激活函数
nn.Linear(100, 10),#输出层神经元数10
)

def forward(self, x): # 前向计算 必须实现
# x.shape [batch size, 1, 28, 28]
x = self.flatten(x)
# 展平后 x.shape [batch size, 784]
logits = self.linear_relu_stack(x)
# logits.shape [batch size, 10]
return logits #没有经过softmax,称为logits

model = NeuralNetwork()

模型结构如下:

model.named_parameters()

这是查看模型中所有 可训练参数 的方法。它会返回一个生成器,包含每个参数的名称和形状。

1
2
for name, param in model.named_parameters(): # 打印模型参数
print(name, param.shape)

model.state_dict()

state_dict() 方法返回一个字典,其中包含模型的所有参数(权重和偏置),以及优化器的状态(如果有的话)。这个字典中的键是参数的名称,值是参数的值(tensor)。包括所有的权重信息,会很复杂

这种方法用于保存模型参数,能看见参数属于模型的哪一部分

1
2
# 看看模型参数
list(model.parameters()) # 这种方法拿到模型的所有可学习参数,requires_grad=True

训练

pytorch的训练需要自行实现,包括

  1. 定义损失函数
  2. 定义优化器
  3. 定义训练步
  4. 训练

注意:

  1. logits = model(datas) 的时候会调用进入到forward,进行前向计算,会生成计算得到的逻辑值logits,
  2. 再进入计算损失,使用交叉熵损失计算前会softmax,之后对其生成的概率值进行计算交叉熵
  3. 优化器Optimizer需要将模型参数都写入model.parameters():模型的所有可学习参数,requires_grad=True
1
2
3
4
5
# 1. 定义损失函数 采用交叉熵损失
loss_fct = nn.CrossEntropyLoss() #内部先做softmax,然后计算交叉熵
# 2. 定义优化器 采用SGD
# Optimizers specified in the torch.optim package,随机梯度下降
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 训练
def training(model, train_loader, val_loader, epoch, loss_fct, optimizer, eval_step=500):
record_dict = {
"train": [],
"val": []
}

global_step = 0
model.train()
with tqdm(total=epoch * len(train_loader)) as pbar: # 进度条 1875*20,60000/32=1875
for epoch_id in range(epoch): # 训练epoch次
# training
for datas, labels in train_loader: #执行次数是60000/32=1875
datas = datas.to(device) #datas尺寸是[batch_size,1,28,28]
labels = labels.to(device) #labels尺寸是[batch_size]
# 梯度清空
optimizer.zero_grad()
# 模型前向计算
logits = model(datas)
# 计算损失
loss = loss_fct(logits, labels)
# 梯度回传,loss.backward()会计算梯度,loss对模型参数求导
loss.backward()
# 调整优化器,包括学习率的变动等,优化器的学习率会随着训练的进行而减小,更新w,b
optimizer.step() #梯度是计算并存储在模型参数的 .grad 属性中,优化器使用这些存储的梯度来更新模型参数

preds = logits.argmax(axis=-1) # 训练集预测
acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy()) # 计算准确率,numpy可以
loss = loss.cpu().item() # 损失转到CPU,item()取值,一个数值
# record

record_dict["train"].append({
"loss": loss, "acc": acc, "step": global_step
}) # 记录训练集信息,每一步的损失,准确率,步数

# evaluating
if global_step % eval_step == 0:
model.eval() # 进入评估模式
val_loss, val_acc = evaluating(model, val_loader, loss_fct)
record_dict["val"].append({
"loss": val_loss, "acc": val_acc, "step": global_step
})
model.train() # 进入训练模式

# udate step
global_step += 1 # 全局步数加1
pbar.update(1) # 更新进度条
pbar.set_postfix({"epoch": epoch_id}) # 设置进度条显示信息

return record_dict


epoch = 20 #改为40
model = model.to(device)
record = training(model, train_loader, val_loader, epoch, loss_fct, optimizer, eval_step=1000)

loss.backward() 是 PyTorch 中用于计算梯度的方法。 它是在进行反向传播(backpropagation) 过程中的一个关键步骤。 下面是 loss.backward() 方法的主要作用:

  1. 根据计算图: PyTorch 中的计算图是由前向传播过程中的张量操作构建的。 当调用 loss.backward()` 时, 它会遵循计算图中的连接关系, 从损失节点开始向后传播, 计算每个相关参数的梯度。
  2. 梯度计算: loss.backward() 方法会根据链式法则自动计算每个参数的梯度。 它会沿着计算图反向传播梯度, 将梯度值累积到每个参数的 .grad 属性中。
  3. 梯度累积: 如果在调用 loss.backward() 前进行了多次前向传播和损失计算, 那么每次调用 loss.backward() 时, 梯度将被累积到参数的 .grad 属性中。 这通常用于在训练过程中使用小批量样本进行梯度更新
  4. 参数更新: 在计算完梯度后, 可以使用优化器(如 torch.optim 中的优化器) 来更新模型的参数, 将梯度信息应用于参数更新规则。

总而言之, loss.backward() 的作用是根据计算图和损失函数计算模型参数的梯度。 这些
梯度可以用于更新模型参数, 以便在训练过程中最小化损失函数。

评估

注意:当进行评估时,模型需要先进入评估模式:model.eval()# 进入评估模式

以确保不会进行梯度计算,以及dropout的丢失损失的情况。并且在下一次进入训练模式的时候也需要先进入训练模式:model.train() # 进入训练模式 以开启反向传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from sklearn.metrics import accuracy_score

@torch.no_grad() # 装饰器,禁止反向传播,节省内存
def evaluating(model, dataloader, loss_fct):
loss_list = [] # 记录损失
pred_list = [] # 记录预测
label_list = [] # 记录标签
for datas, labels in dataloader:#10000/32=312
datas = datas.to(device) # 转到GPU
labels = labels.to(device) # 转到GPU
# 前向计算
logits = model(datas)
loss = loss_fct(logits, labels) # 验证集损失
loss_list.append(loss.item()) # 记录损失

preds = logits.argmax(axis=-1) # 验证集预测,argmax返回最大值索引
# print(preds)
pred_list.extend(preds.cpu().numpy().tolist())#将PyTorch张量转换为NumPy数组。只有当张量在CPU上时,这个转换才是合法的
# print(preds.cpu().numpy().tolist())
label_list.extend(labels.cpu().numpy().tolist())

acc = accuracy_score(label_list, pred_list) # 计算准确率
return np.mean(loss_list), acc

# dataload for evaluating
model.eval()# 进入评估模式
loss, acc = evaluating(model, val_loader, loss_fct)
print(f"loss: {loss:.4f}\naccuracy: {acc:.4f}")

画图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#画线要注意的是损失是不一定在零到1之间的
def plot_learning_curves(record_dict, sample_step=1000):
# build DataFrame
train_df = pd.DataFrame(record_dict["train"]).set_index("step").iloc[::sample_step]
val_df = pd.DataFrame(record_dict["val"]).set_index("step")
last_step = train_df.index[-1] # 最后一步的步数
# print(train_df.columns)
print(train_df['acc'])
print(val_df['acc'])
# plot
fig_num = len(train_df.columns) # 画几张图,分别是损失和准确率
fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))
for idx, item in enumerate(train_df.columns):
# print(train_df[item].values)
axs[idx].plot(train_df.index, train_df[item], label=f"train_{item}")
axs[idx].plot(val_df.index, val_df[item], label=f"val_{item}")
axs[idx].grid() # 显示网格
axs[idx].legend() # 显示图例
axs[idx].set_xticks(range(0, train_df.index[-1], 5000)) # 设置x轴刻度
axs[idx].set_xticklabels(map(lambda x: f"{int(x/1000)}k", range(0, last_step, 5000))) # 设置x轴标签
axs[idx].set_xlabel("step")

plt.show()

plot_learning_curves(record) #横坐标是 steps