1)什么是分类?
在此之前,我们一直使用的都是回归任务进行学习;这里我们将进一步学习什么是分类,我们先从训练模型的角度来看看二者的区别。
对于回归来说,它所作的是对模型输入相应的特征,然后模型给出相应的输出,需要让模型的输出和实际的标签值越接近越好;而对于分类来说,同样的是将相应的特征输入模型,模型输出相应的类型。
1.1问题一:
分类模型的输出不像回归模型一样输出是一个特定的数值,所以对于分类模型来说我们可以根据将不同的类别使用不同的数值来代替。
例如:
class1 --- 1
class2 --- 2
class3 --- 3
这样当模型认为输入的特征和class1更符合的话就会输出1,class3更符合的话就会输出3。但是又会出现新的问题,采用以上编码方式是否会导致模型认为class1和class2更相似;class1和class3更加的不同呢?因为class1和class2的距离上更近,比如网络的输出值是1.49,其实就表示很大概率是class1或者class2,这样其实就隐含表示class1或者class2更近一点。
1.2问题二:
对于有些分类任务来说,采用以上编码方式是不会具有问题的。比如使用升高和体重来预测小朋友的年级,例如:一年级 --- 1、二年级 --- 2、三年级 --- 3。这样是没问题的,因为一年级和二年级这两个类别来说是相对更近的, 一年级和三年级这两个类别来说是相对更远的。但是对于有的分类任务来说,再编码的时候就会产生这样的问题,于是在编码的时候采用one-hot vector的方式进行编码。
例如:
采用这种编码方式的话,就不会出现以上这种问题了,这样的话,他们之间的距离都是一样的啦。
1.3问题三:
在回归问题的时候,我们构建的神经网络只能输出一个数值,但是对于分类问题来说,要是采用one-hot的编码方式对类别进行编码,那么对于网络的输出就不能只有一个,所以网络的结构也必须改变。
所以如下图所示,只需要多输出两个就行。
至此对于一个分类任务的模型我们已经构架完成了,只不过是对于回归问题进行了一些小小的改进,但是其实对于分类问题来说,还有一些不太一样的问题。
我们来最终对比一下回归任务和分类任务,分类任务最终的输出要和实际的标签纸越接近越好;对于分类来说,其最终的输出也应该与实际的类别标签纸越接近越好。
可是实际在最后一步输出的过程中,即最后网络输出了y之后,对于分类问题来说会再加上一个softmax使其输出 ,然后希望的是 和实际的标签值越接近越好。
2)为什要加softmax?
简单理解即使,其实对于网络的输出的三个值是可以为任何值,但是在最终的标签我们是希望在零到一之间的,所以通过softmax就可以将网络输出的值规格化到零到一之间。
softmax的工作过程:
- 对其所以的输入的y值(网络最后的输出)取exp,也就是分别计算。
- 对进行求和
- 用分别计算得到的比上所有的和就得到了每个数值最后被规格化后的数值。
例子:
输入softmax的三个数值是3、1、-3。
- 取exp。得到
- 求和。
- 归一化。
其实在实际的分类问题当中,当分类任务是两个类别的时候,我们更常用的是使用sigmoid函数来进行最后的归一化;但是其实也可以使用softmax,他们二者在二分类的使用上无本质的区别。
2.1sigmoid函数:
sigmoid函数原型表达式如下:
以输入为例子。
- 对于二分类来说可以进一步将softmax写成:
由此可得对于二分类问题来说,其二者的公式无本质区别,即理论上来说,二者是没有任何区别的。
sigmoid和softmax函数的本质区别:
sigmoid函数用于多标签分类问题,选取多个标签作为正确答案,它是将任意值归一化为[0-1]之间,并不是不同概率之间的相互关联。
Softmax函数用于多分类问题,即从多个分类中选取一个正确答案。Softmax综合了所有输出值的归一化,因此得到的是不同概率之间的相互关联。
转载来源:深度学习随笔——Softmax函数与Sigmoid函数的区别与联系 - 知乎
Sigmoid函数针对两点分布提出。神经网络的输出经过它的转换,可以将数值压缩到(0,1)之间,得到的结果可以理解成分类成目标类别的概率P,而不分类到该类别的概率是(1 - P),这也是典型的两点分布的形式。
Softmax函数本身针对多项分布提出,当类别数是2时,它退化为二项分布。而它和Sigmoid函数真正的区别就在——二项分布包含两个分类类别(姑且分别称为A和B),而两点分布其实是针对一个类别的概率分布,其对应的那个类别的分布直接由1-P得出。
简单点理解就是,Sigmoid函数,我们可以当作成它是对一个类别的“建模”,将该类别建模完成,另一个相对的类别就直接通过1减去得到。而softmax函数,是对两个类别建模,同样的,得到两个类别的概率之和是1。
3)分类问题的损失函数
分类问题的损失函数同样的根据距离来计算,可以和之前的回归问题一样使用MSE误差来计算损失函数。但是更常用是使用下图中的 Cross- entropy来计算误差。
3.1 为什么选择Cross- entropy而不是Mean Square error
其实我们实际在使用pytorch进行构建网络实现分类的任务的时候,我们会发现找不到softmax,这是因为,我们再构建网络后,使用Cross-entropy来计算误差的时候,其会自动再网络的最后一层加上softmax,在 pytorch中Cross-entropy和softmax被内置成为了一个整体。
例子:
在某个训练过程中网络输出的三个数值分别是,然后再经softmax处理得到了最后的输出结果,我们分别使用Cross- entropy和Mean Square error进行求损失,然后根据损失计算下一步该往哪里走。
如上图所示得到了Cross- entropy和Mean Square error 的损失图,在图的右下角都是损失最小的地方,即的值变大, 的值变小就可以使得损失值变小;在图的左上角都是损失最大的地方,即的值变小, 的值变大会使得损失值变大。
所以当使用Mean Square error 的时候,很有可能会到达梯度不变的点,导致训练不下去(一般的训练过程训练不下去),但是对于 Cross- entropy 来说,确是有梯度了,可以进行下去。
所以可以得出对于损失函数的设计都会影响最后的一个训练优化过程。
4)pytorch实现分类代码
完整代码:
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
# 定义超参数
batch_size = 64
learning_rate = 0.001
num_epochs = 10
# 数据预处理
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 下载并加载 CIFAR-10 数据集
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# 定义 CNN 模型
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(64 * 8 * 8, 512)
self.fc2 = nn.Linear(512, 10)
self.relu = nn.ReLU()
def forward(self, x):
x = self.pool(self.relu(self.conv1(x)))
x = self.pool(self.relu(self.conv2(x)))
x = x.view(-1, 64 * 8 * 8)
x = self.relu(self.fc1(x))
x = self.fc2(x)
return x
# 实例化模型、定义损失函数和优化器
model = SimpleCNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# 训练模型
for epoch in range(num_epochs):
running_loss = 0.0
for i, (inputs, labels) in enumerate(train_loader):
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99: # 每100个mini-batch打印一次
print(f'Epoch [{epoch + 1}/{num_epochs}], Step [{i + 1}], Loss: {running_loss / 100:.4f}')
running_loss = 0.0
print('训练完成')
# 测试模型
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'测试准确率: {100 * correct / total:.2f}%')
接下来将一步一步剖析代码,我们先看网络架构。
4.1 __init__
方法中初始化和声明网络的各层
定义SimpleCNN类,让其继承prtorch中的nn.Module,并且
# 定义 CNN 模型
class SimpleCNN(nn.Module): #SimpleCNN 继承自 nn.Module
def __init__(self): #构造函数(初始化方法)
super(SimpleCNN, self).__init__() # 这一行代码调用了父类(nn.Module)的构造函数,确保在实例化SimpleCNN类时,也会执行父类的初始化操作。
网络1——卷积1:
self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
CIFAR-10图像都是3通道(RGB)的,尺寸为32x32像素。因此每个图像的数据形状是3x32x32,所以对于32个卷积核
- 输入通道数:3,对应于 RGB 图像的三个通道。
- 输出通道数:32,卷积核的数量,生成32个特征图。
- 卷积核大小:3x3。
- Padding:1,保持输出和输入的宽高相同。
CIFAR-10图像为3x32x32,输入通道是3,其实就对应着3个特征图,即RGB三个特征图。经过32个卷积核采样后,每个卷积核都能得到一个特征图,也就是32个特征图。假设一张3x6x6的图片在经32个卷积核采样,在不加padding的时候得到的特征图都是4x4的,加了padding之后,就能保证最后卷积后得到额特征是还是6x6的。而CIFAR-10图像为3x32x32,所以其在经过32个卷积核且加了padding采样后最终得到是32(32维度特征)x32x32(32x32为图片的大小padding保证)。
网络1——卷积2:
self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
- 输入通道数:32,来自前一层的输出。
- 输出通道数:64。
- 卷积核大小:3x3。
- Padding:1。
最终得到是64(64维度特征)x32x32(32x32为图片的大小padding保证)。
网络2——池化层
self.pool = nn.MaxPool2d(2, 2)
- 池化大小:2x2。
- 步幅:2,减少特征图尺寸,通常用于降采样。
其实就是缩小特征图的大小的,使用2x2的池化后,特征图的大小缩小;由于是2x2池化且步幅为2,若图片尺寸初始为8x8,那么最后的尺寸将变为4x4。
网络3——全连接层1
self.fc1 = nn.Linear(64 * 8 * 8, 512)
- 输入大小:64 * 8 * 8,来自池化层展平后的特征数。
- 输出大小:512,隐藏层的神经元个数。
网络3——全连接层2
self.fc2 = nn.Linear(512, 10)
- 输入大小:512,来自前一层的输出。
- 输出大小:10,对应于 CIFAR-10 数据集的10个类别。
激活函数
self.relu = nn.ReLU()
ReLU:一种常用的激活函数,引入非线性,计算简单,能有效缓解梯度消失问题。
在 __init__
方法中初始化和声明这些网络的各层,并设置了每层的参数和属性,例如输入通道数、输出通道数、卷积核大小,接下去将开始构建这个网络。
4.2 前向传播
def forward(self, x):
x = self.pool(self.relu(self.conv1(x)))
定义前向传播函数,需要注意的是前向传播函数的名称必须是 forward
。这是因为 nn.Module
的 __call__
方法在内部会默认调用 forward
方法来执行前向传播。如果使用其他名称,__call__
方法将无法找到并执行前向传播。
- 在 forward 函数参数列表中的x为输入的图片数据。
- 池化( 激活函数( 卷积网络(输入参数input)))= 输出参数
x = self.pool(self.relu(self.conv2(x)))
同样是一层卷积网络,其输入的CIFAR-10图像为3x32x32,经过卷积池化卷积池化,其最后的输出为(batch的大小,64x8x8),两次池化将32x32大小的图片变为8x8的大小。
x = x.view(-1, 64 * 8 * 8)
- view方法用于改变张量的形状,使其成为一维张量,前面的参数-1,表示根据batch的数量来定最后展开的张量大小。
x = self.relu(self.fc1(x))
x = self.fc2(x)
return x
- 将展开的一维张量输入到全连接层,最后得到网络的输出
到现在为止其实我们没有看见在最后一个全连接层没有加任何的softmax和sigmoid函数,但是对于分类任务来说,最后的输出都应该体现为概率,即需要将最后的结果映射为0到1之间。其实也就是我们提到的,要是我们使用的是Cross-entropy来计算误差的话,对于pytorch来说,其会自动在最后一层加上softmax。
4.3 例化模型、定义损失函数和优化器
model = SimpleCNN()
- 实例化了一个
SimpleCNN
类的对象,即创建了模型。SimpleCNN
继承自nn.Module
,并且在其中定义了网络的各个层和前向传播的方法。当调用SimpleCNN()
时,执行__init__()
构造函数来初始化所有层(卷积层、池化层、全连接层等),从而定义了模型的结构。
criterion = nn.CrossEntropyLoss() # Cross-entropy来计算误差而不是Mean Square error
nn.CrossEntropyLoss()
是 PyTorch 中用于 多分类问题 的常用损失函数,会自动在最后加上softmax。
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
- Adam(Adaptive Moment Estimation)是一种基于梯度下降的优化算法,它结合了我们之前学习的动量(Momentum),以及自适应学习率(AdaGrad)的方法。
model.parameters()
:获取模型中所有需要优化的参数,通常是卷积层和全连接层的权重和偏置。lr=learning_rate
:指定学习率learning_rate
,它决定了每次更新时步长的大小。学习率较大时,参数更新可能较快,但容易跳过最优解;学习率较小时,参数更新缓慢,可能会收敛得更好,但需要更多的迭代次数。
Adam 具有较好的性能,能够在许多任务中较快地收敛,且对超参数的选择不那么敏感。它适用于大部分神经网络训练场景,尤其是在深度学习中常常被使用。同时是一种自适应优化方法,它可以动态调整每个参数的学习率,因此可以有效地避免过大的梯度波动。
4.4 测试和训练
①训练
第一层循环,循环epoch次,同时初始化running_loss为零。
for epoch in range(num_epochs):
# 用于累加当前 epoch 中每个 mini-batch 的损失值,最后用来计算平均损失。
running_loss = 0.0
第二层循环,前面设置的batch_size = 64;其中 train_loader 负责从训练数据集中加载批次数据(mini-batch),通常会自动分配数据到多个 GPU 或 CPU 上;所以该层循环遍历训练数据集和,但是每一个遍历实际看过的数据是mini-batch,而不是我们自己设置的batch大小。
for i, (inputs, labels) in enumerate(train_loader):
- inputs是实际输入的训练数据图片
- labels是实际输入的训啦数据图片的标签。
- i 是统计mini-batch的数量。
第二层循环体内部:先将优化器中的梯度清零;将图像数据送入model,进入网络的前向推理阶段,得到网络的输出值outputs,然后使用Cross-entropy来计算误差。
optimizer.zero_grad()
outputs = model(inputs) # inputs 输入模型就会自动调用前向传播代码
loss = criterion(outputs, labels) # Cross-entropy来计算误差(输出值,标签值)
这里也能反映我们当时在学习batch的时候说,不同的batch,我们使用不同的损失函数来进行更新梯度会使得我们在训练的时候更好的跳开local minima。
对损失函数求导,链式法则逐层计算梯度。
loss.backward()
优化算法,优化更新模型的参数(权重和偏置)
optimizer.step()
②测试
model.eval()
eval()
方法将模型设置为评估模式(evaluation mode)。在此模式下,PyTorch 会关闭一些只在训练时需要的操作,如 Dropout 和 Batch Normalization。这些操作在测试过程中是不需要的,因为我们希望评估模型的最终表现,而不是其在训练过程中随机的行为。设置为 eval()
后,模型会以确定的方式进行预测。
correct = 0
total = 0
with torch.no_grad():
在 torch.no_grad()
中进行的操作不会计算梯度,因此更加高效
_, predicted = torch.max(outputs.data, 1)
torch.max(outputs.data, 1)
outputs.data
是模型的输出,但是模型可以同时预测多个样本,所以对于模型的输出来说,其输出的张量就会含有两个维度,第一个维度(dim=0,即行数)是样本的数量;第二个维度(dim=1,即列数)即类别维度。