实战:FashionMNIST时装分类中自己一些收获
任务简介
- 这里的任务是对10个类别的“时装”图像进行分类,使用FashionMNIST数据集。
- FashionMNIST中数据的若干样例图,其中每个小图对应一个样本。
- FashionMNIST数据集中包含已经预先划分好的训练集和测试集,其中训练集共60,000张图像,测试集共10,000张图像。
- 每张图像均为单通道黑白图像,大小为28*28pixel,分属10个类别。
导入必要的包
import os
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
配置训练环境和超参
先要查看GPU是否可用
torch.cuda.is_available()
- 输出
True
,就说明是好的
配置GPU,有两种方式
- 方案1:使用os.environ 这种之后用xxx.cuda()
os.environ['CUDA_VISIBLE_DEVICES'] = '0,1' # 这里可以是多块 0,1,2,3
- 方案2:使用device,后续要使用GPU直接用xxx.to_device(device)
device = torch.device('cuda:1' if torch.cuda.is_available() else 'cpu')
- 这边可以查一下
单卡和多卡
配置
数据的读入与加载
from torchvision import transforms
-
1.图像预处理
- Compose把多个步骤整合到一起:
data_transform = transforms.Compose([ transforms.ToPILImage(), transforms.Resize(img_size), transforms.ToTensor() ])
- 解析:
# 示例代码 transform.ToTensor(), transform.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
ToTensor()
能够把灰度范围从0-255变换到0-1之间- 后面的
transform.Normalize()
则把0-1变换到(-1,1) - 解析:
Normalize:image = (image - mean) / std
- 其中mean和std分别通过(0.5,0.5,0.5)和(0.5,0.5,0.5)进行指定
- 将ToTensor()转换的(0, 1)变成(0-0.5)/0.5 = -1 (1-0.5)/0.5=1
-
2.读取数据
- 先读入csv格式的数据:60000张训练集和10000张测试集,784个特征,1个标签
- 再构建自己的
Dataset
:- 继承
Dataset
,重写__init__
、__len__
、__getitem__
- 继承
def __init__(self, df, transform=None):
self.df = df
# 图像转换
self.transform = transform
# 取df的所有行第1列到最后的内容,第0列是label,从后面开始才是图的内容
self.images = df.iloc[:,1:].values.astype(np.uint8) # 专门的图像格式
self.labels = df.iloc[:,0].values
def __len__(self):
return len(self.images)
def __getitem__(self, idx):
# 一条一条的读
image = self.images[idx].reshape(28, 28, 1) # 单一通道的
label = int(self.labels[idx])
# 对imgae和label进行处理
if self.transform is not None:
image = self.transform(image)
else:
# /255是归一化为0-1,方便处理
image = torch.tensor(image/255, dtype=torch.float)
label = torch.tensor(label, dtype=torch.long)
return image, label
-
3.实例化,并用DataLoader加载
train_data = FMDataset(train_df, data_transform) test_data = FMDataset(test_df, data_transform)
- 构建好Dataset后,就可以使用DataLoader来按批次读入数据
- data_set:构建好的样本
- batch_size:样本是按“批”读入的,batch_size就是每次读入的样本数 这里为256
- num_workers:有多少个进程用于读取数据
- shuffle:是否将读入的数据打乱
- drop_last:对于样本最后一部分没有达到批次数(就是不满一个batch的数量)的样本,使其不再参与训练
- pin_memory:锁页内存,设置为True,直接将内存的张量转义到GPU的显存速度会快,如果显存爆炸,就设置为False
train_loader = DataLoader(dataset=train_data, batch_size=batch_size, num_workers=num_workers, shuffle=True, drop_last=True)
test_loader = DataLoader(dataset=test_data, batch_size=batch_size, num_workers=num_workers, shuffle=False)
读取一张图片看看
import matplotlib.pyplot as plt
# next是获取下一个项目
# next(iterator[,default])
# iterator :要读取行的文件对象
# default :如果迭代器耗尽则返回此默认值。 如果没有给出此默认值,则抛出 StopIteration 异常
images, labels = next(iter(train_loader))
print(images.shape, labels.shape)
# 展示第0个数据,第二个0为color
plt.imshow(images[0][0], cmap='gray')
plt.show()
模型构建
-
神经网络的构造:基于nn.Module
- init, forward
-
神经网络是通过“层定义+层顺序”的方式构建起来的
-
神经网络常见层
- nn.Conv2d, nn.MaxPool2d, nn.Linear, nn.ReLU,…
-
卷积层
-
- 二维卷积
nn.Conv2d(1, 32, 5)
-
in_channels
:对于最初输入图片样本的通道数 in_channels- 取决于图片的类型这里数据是灰色的通道就是1个,所以就传入1;如果是RGB的图,3通道,这里就传3
-
out_channels
:卷积完成之后,输出的通道数 out_channels 取决于过滤器的数量,- 这里的 out_channels 设置的就是过滤器的数目。
-
-
对于第二层或者更多层的卷积,此时的 in_channels 就是上一层的 out_channels
, out_channels 还是取决于过滤器数目 -
kernel_size
:卷积核设置为5*5
- 二维卷积
-
- 池化:
nn.MaxPool2d(2, stride=2)
kernel_size
:表示做最大池化的窗口大小,可以是单个值(单*单),也可以是tuple元组stride
:步长,可以是单个值(向右滑动2个窗口,向下滑动2个窗口),也可以是tuple元组padding
:填充,可以是单个值,也可以是tuple元组dilation
:控制窗口中元素步幅return_indices
:布尔类型,返回最大值位置索引ceil_mode
:布尔类型,为True,用向上取整的方法,计算输出形状;默认是向下取整。
- 池化:
-
- nn.Dropout(0.3)
- Dropout是为了防止过拟合而设置的
- Dropout顾名思义有丢掉的意思
- nn.Dropout(p = 0.3) # 表示每个神经元有0.3的可能性不被激活
- Dropout只能用在训练部分而不能用在测试部分
- Dropout一般用在全连接神经网络映射层之后,如代码的nn.Linear(20, 30)之后
-
-
全连接层 这里做了两次,最后一次输出10类,这里有10类
- nn.Linear(6444, 512)
- in_features:输入的神经元个数
- out_features:输出神经元个数
- bias=True:是否包含偏置
- 其实Linear其实就是对输入X(n*i)执行了一个线性变换,即:
- y = XW + b 其实W是模型要学习的参数,W的维度为i*o,b是o维的向量偏置,n微输入向量的行数
- (例如,一次输入10个样本,即batch_size为10,则n=10),i为输入神经元的个数(例如样本的特征书为5,则i=5)
- o为输出神经元的个数
- 例如,定义线性层,我们的输入特征为5,所以in_feature=5,
- 我们想让下一层的神经元个数为10,所以out_feature=10,则模型参数为:W(5 × 10)
- 这里输入特征数为6444,输出特征数为512
- nn.Linear(6444, 512)
损失函数
- 损失函数常用的操作
- backward()
- 使用nn自带的CrossEntropy损失
- PyTorch会自动把整型的label转为one-hot型,用于计算CE loss。这里需要确保label是从0开始的,同时模型不加softmax层(使用logits计算),所以PyTorch训练中各个部分不是独立的,需要通盘考虑
训练与测试(验证)
-
各自封装成函数,方便后续调用
-
关注两者的主要区别:
- 模型状态设置
- 是否需要初始化优化器
- 是否需要将loss传回到网络
- 是否需要每步更新optimizer
-
训练与评估
- 模型状态设置
- model.train(), model.eval()
- 训练流程:读取、转换、梯度清零、输入、计算损失、反向传播、参数更新
- 验证流程:读取、 转换、输入、计算损失、计算指标
- 模型状态设置
def train(epoch):
# model.train()的作用是启用 Batch Normalization 和 Dropout
# 如果模型中有BN层(Batch Normalization)和Dropout,需要在训练时添加model.train()。
# model.train()是保证BN层能够用到每一批数据的均值和方差。
# 对于Dropout,model.train()是随机取一部分网络连接来训练更新参数。
model.train()
# 如果模型中有BN层(Batch Normalization)和Dropout,在测试时添加model.eval()。
# model.eval()是保证BN层能够用全部训练数据的均值和方差,即测试过程中要保证BN层的均值和方差不变。
# 对于Dropout,model.eval()是利用到了所有网络连接,即不进行随机舍弃神经元。
# model.eval()
train_loss = 0
for data, label in train_loader: # for i, (data, label) in enumerate(train_loader):
# data.cuda()就将其转换为GPU的张量类型
data, label = data.cuda(), label.cuda()
# 梯度清零
optimizer.zero_grad()
# 训练集上得到结果
output = model(data)
loss = criterion(output, label)
loss.backward()
optimizer.step()
# data.size(0) data的第1维为为256 一个batch_size为256
train_loss += loss.item() * data.size(0)
train_loss = train_loss / len(train_loader.dataset)
print('Epoch:{} \t Training Loss:{:.6f}'.format(epoch, train_loss))
def val(epoch):
model.eval()
val_loss = 0
gt_labels = []
pred_labels = []
with torch.no_grad():
for data, label in test_loader:
data, label = data.cuda(), label.cuda()
output = model(data)
# 每一维列的最大值的下标
# 因为这里是10个类别,要看他哪一个是1,得到那个类别
preds = torch.argmax(output, 1)
# 真实的标签
# 一次传入256长得list到一个list里面
# 这样pred_labels就是n*256
gt_labels.append(label.cpu().data.numpy())
# print(gt_labels.shape)
# 算出来的标签
# 一次传入256长得list到一个list里面
# 这样pred_labels就是n*256
pred_labels.append(preds.cpu().data.numpy())
loss = criterion(output, label)
val_loss += loss.item()*data.size(0)
val_loss = val_loss / len(test_loader.dataset)
# print(len(gt_labels), len(pred_labels))
# 这里拼接成一维
gt_labels, pred_labels = np.concatenate(gt_labels), np.concatenate(pred_labels)
# print('gt_labels:', gt_labels, 'pred_labels:',pred_labels)
acc = np.sum(gt_labels == pred_labels) / len(pred_labels)
print('Epoch:{} \t Validation Loss:{:.6f}, ACC:{:6f}'.format(epoch, val_loss, acc))
for epoch in range(1, epochs+1):
train(epoch)
val(epoch)