学习资料:
09 Softmax 回归 + 损失函数 + 图片分类数据集【动手学深度学习v2】_哔哩哔哩_bilibili
torchvision.transforms.ToTensor详解 | 使用transforms.ToTensor()出现用户警告 | 图像的H W C 代表什么_LolitaAnn的博客-CSDN博客_transforms.totensor
图像的通道_高小喵的博客-CSDN博客_图像通道
pytorch数据处理之 transforms.ToTensor()解释_菜根檀的博客-CSDN博客_transforms.totensor()
https://blog.csdn.net/sdu_hao/article/details/103025175
1 Softmax回归
虽然名字是回归,但其实是一个分类问题
分类问题把单输出的回归变成了多输出,多输出的个数就是类别的个数
1.1 回归与分类的区别
1.2 从回归到多类分类——均方损失
1.3 从回归到多类分类-无校验比例
1.4 从回归到多类分类-校验比例
1.5 Softmax和交叉熵损失
2 损失函数
介绍三个常用的损失函数
2.1 L2 Loss
平方损失
2.2 L1 Loss
绝对值损失
2.3 Huber's Robust Loss
鲁棒损失
3 图片分类数据集
3.1 读取数据集
图像通道的理解
单通道:也就是通常所说的灰度图,每个像素点只有一个值表示,如果图像的深度是4-(256 = 2*2*2*2) 为2^4(下同),那么他的像素取值范围是:0(黑)~255(白),并且仅用一个数字表示该点的像素值;
三通道:也就是通过见到的彩色图,每个像素点有三个值表示,如果图像深度是4-(256 = 2*2*2*2),那么他的像素值有红(0~255)、绿(0~255)、蓝(0~255)叠加表示,色彩更加艳丽,每一个像素值为三个数字(a,b,c),分别代表了三原色的一种;
四通道:也就是在三通道图像基础上加上透明程度,Alpha色彩空间,如果图像深度是4-(256 = 2*2*2*2),那么0是完全透明,255是完全不透明;
torchvision包
它是服务于 PyTorch深度学习框架的,主要⽤来构建计算机视觉模型。 torchvision主要由以下⼏部分构成:
1)torchvision.datasets:⼀些加载数据的函数及常⽤的数据集接口;
2) torchvision.models:包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;
3)torchvision.transforms:常用的图⽚变换,例如裁剪、旋转等;
4)torchvision.utils:其他的一些有用的方法。
%matplotlib inline
import torch
import torchvision # pytorch对于计算机视觉模型实现的一个库
from torch.utils import data
from torchvision import transforms # 导入对数据操作的模具
from d2l import torch as d2l
d2l.use_svg_display() # 使用svg来显示图片
torchvision.transforms.ToTensor()
对图片进行预处理,把PIL.Image或ndarray从 (H x W x C)形状转换为 可被pytorch快速处理的张量类型 (C x H x W)
[h, w, c]:数组中最外层即hight,表示图像像素有几行;第二层元素width,表示图像像素几列,最后一层元素为每一个通道的数值。
[c, h, w]:数组中第一层元素为图像有一个通道,第二层元素为某个通道上的一行像素,第三层为该通道上某列的像素值。
输入模式为(L、LA、P、I、F、RGB、YCbCr、RGBA、CMYK、1)的PIL Image 或 numpy.ndarray (形状为H x W x C)数据范围是[0, 255] 到一个 Torch.FloatTensor,其形状 (C x H x W) 在 [0.0, 1.0] 范围内
注意:由于像素值为0到255的整数,所以刚好是uint8所能表示的范围,包括transforms.ToTensor()在内的⼀些关于图片的函数就默认输入的是uint8型,若不是,可能不会报错但可能得不到想要的结果。所以,如果⽤像素值(0-255整数)表示图⽚数据,那么一律将其类型设置成uint8,避免不必要的bug。
# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,
# 并除以255使得所有像素的数值均在0~1之间
trans = transforms.ToTensor() #对图片进行预处理,转化为tensor格式
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
len(mnist_train), len(mnist_test)
(60000,10000)
图像.shape
123
mnist_train[0][0].shape
#索引到第一张图片,描述它的形状.已经转化为张量,故形状为(c×h×w)
#1表示channel值为1,是一张黑白图片
#长和宽都是28
torch.Size([1,28,28])
Fashion-MNIST中⼀共包括了10个类别,分别为t-shirt(T恤)、trouser(裤⼦子)、pullover(套衫)、 dress(连⾐衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、 bag(包)和ankle boot(短靴)。以下函数可以将数值标签转成相应的⽂本标签
def get_fashion_mnist_labels(labels): #@save
"""返回Fashion-MNIST数据集的文本标签"""
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
现在可以创建一个函数来可视化这些样本,在一行⾥画出多张图像和对应标签
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save
"""绘制图像列表"""
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
# 图片张量
ax.imshow(img.numpy())
else:
# PIL图片
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes
以下输出训练数据集中前几个样本的图像及其相应的标签
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))# 拿到第一个小批量的X和y
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));
# 绘制两行图片,每一行有9张图片
# titles是每张图片的标号
3.2 读取小批量
在每次迭代中,数据加载器每次都会读取一小批量数据,大小为batch_size。 通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏见地读取小批量
mnist_train是torch.utils.data.Dataset的子类,所以我们可以将其传入torch.utils.data.DataLoader来创建一个读取⼩批量数据样本的DataLoader实例
在实践中,数据读取经常是训练的性能瓶颈,特别当模型较简单或者计算硬件性能较高时。PyTorch的DataLoader中一个很⽅便的功能是允许使用多进程来加速数据读取。这⾥我们通过参数num_workers来设置4个进程读取数据
batch_size = 256
def get_dataloader_workers(): #@save
"""使用4个进程来读取数据"""
return 4
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers())
# 训练集需要设置shuffle为True,测试集不一定
我们将获取并读取Fashion-MNIST数据集的逻辑封装在d2lzh_pytorch.load_data_fashion_mnist函数中供后面实验调用。该函数将返回train_iter和test_iter两个变量。随着内容的不断深入,我们会进⼀步改进该函数
f-string 格式化输出
可用{content:format}自定义格式:对齐、宽度、符号、补零、精度、进制能
其中content是替换并填入字符串的内容,format是格式描述符,采用默认格式时不必指定{:format},只写{content}即可
常见的f-string格式写法及含义:
a = 123.456
f'a is {a:.2f}'
输出为:'a is 123.56'
解读:输出的字符串为单引号中的内容,其中{}中的内容指定为a,{:format}为{:.2f}表示精度为2位小数
看一下读取训练数据所需的时间
timer = d2l.Timer() # Timer函数用于测试速度
for X, y in train_iter:
continue
f'{timer.stop():.2f} sec' # 输出读取数据所用的秒数,精度为2位小数
3.3 整合所有组件
现在我们定义load_data_fashion_mnist函数,用于获取和读取Fashion-MNIST数据集。
这个函数返回训练集和验证集的数据迭代器。
此外,这个函数还接受一个可选参数resize,用来将图像大小调整为另一种形状
def load_data_fashion_mnist(batch_size, resize=None): #@save
# resize
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))
3.4 小结
Fashion-MNIST是一个服装分类数据集,由10个类别的图像组成。我们将在后续章节中使用此数据集来评估各种分类算法
数据迭代器是获得更高性能的关键组件。依靠实现良好的数据迭代器,利用高性能计算来避免减慢训练过程
4.Softmax回归从零开始实现
像我们从零开始实现线性回归一样, 我们认为softmax回归也是重要的基础,因此应该知道实现softmax回归的细节。 本节我们将使用刚刚在 3.5节中引入的Fashion-MNIST数据集, 并设置数据迭代器的批量大小为256
4.1 初始化模型参数
原始数据集中的每个样本都是28×28的图像。 本节将展平每个图像,把它们看作长度为784的向量。
在softmax回归中,我们的输出与类别一样多。 因为我们的数据集有10个类别,所以网络输出维度为10。 因此,权重将构成一个784×10的矩阵, 偏置将构成一个1×10的行向量。
num_inputs = 784 #(28*28)将原来的图片拉长展平,视为长度为784的向量
num_outputs = 10 #数据集中有10个类别,故设定网络的输出维度为10
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
# 设定偏移量b,对每个输出都需要有一个偏移
4.2 定义softmax操作
.sum(index,keepdim)
index=0表示将X[0](行)变为1,keepdim=True表示X仍保持二维
index=1表示将X[1](列)变为1,keepdim=True表示X仍保持二维
在实现softmax回归模型之前,我们简要回顾一下sum运算符如何沿着张量中的特定维度工作
X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdim=True), X.sum(1, keepdim=True)
# 0表示将X[0](行)从2变为1,keepdim=True表示X仍保持二维
# 1表示将X[1](列)从3变为1,keepdim=True表示X仍保持二维
下⾯我们就可以定义前面⼩节里介绍的softmax运算了。在下⾯的函数中,矩阵X的行数是样本数,列数是输出个数。为了表达样本预测各个输出的概率,softmax运算会先通过exp函数对每个元素做指数运算,再对exp矩阵同行元素求和,最后令矩阵每⾏各元素与该行元素之和相除。这样一来,最终得到的矩阵每⾏元素和为1且非负。因此,该矩阵每行都是合法的概率分布。softmax运算的输出矩阵中的任意⼀行元素代表了⼀个样本在各个输出类别上的预测概率。
def softmax(X):
X_exp = torch.exp(X) #对X的每一个元素做指数运算
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里应用了广播机制
正如上述代码,对于任何随机输入,我们将每个元素变成一个非负数。 此外,依据概率原理,每行总和为1
下面是一个测试:
X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
(tensor([[0.2968,0.4115,0.0945,0.1603,0.0368],[0.2128,0.5422,0.0865,0.1104,0.0481]]),tensor([1.0000,1.0000]))
4.3 定义模型
.view()和.reshape()
都可以对数据进行维度转换
view():不改变内存,只改变tensor的维度信息,要求输入是contiguous的。
reshape():对输入进行判断,如果内存连续,则只改变维度信息,否则进行数据拷贝,返回指向新的内存的tensor
定义softmax操作后,我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape函数将每张原始图像展平为向量。
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
#-1表示系统自动求值,这里实际指的是批量大小256
#W.shape[0]为784,则这里X被reshape成为256*784的矩阵
#这里的加法运用了广播机制
4.4 定义损失函数(交叉熵损失函数)
接下来,我们实现 3.4节中引入的交叉熵损失函数。 这可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题的数量
我们创建一个数据样本y_hat,其中包含2个样本在3个类别的预测概率, 以及它们对应的标签y。 有了y,我们知道在第一个样本中,第一类是正确的预测; 而在第二个样本中,第三类是正确的预测。 然后使用y作为y_hat中概率的索引, 我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率
y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], y]
# 对y[0](y的第0个样本)拿出对应输出的第0个元素即y_hat[0][0]
# 对y[1](y的第1个样本)拿出对应输出的第2个元素即y_hat[1][2]
tensor([0.1000,0.5000])
下⾯实现(softmax回归)中介绍的交叉熵损失函数
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
cross_entropy(y_hat, y)
4.5 分类精度
.argmax(axis)
返回指定轴上最大值的索引
float(cmp.type(y.dtype).sum())
def accuracy(y_hat, y): #@save
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())
# 预测正确的样本数
accuracy(y_hat, y) / len(y)
# 预测正确的样本数除以整个y的长度即为预测正确的概率
.eval()
Accumulator()
.numel()
用于返回数组中元素的个数
def evaluate_accuracy(net, data_iter): #@save
"""计算模型在指定数据集(数据迭代器)上的精度"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 正确预测数、预测总数
with torch.no_grad():
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1] # 分类正确的样本数与总样本数相除
????
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
由于我们使用随机权重初始化net模型, 因此该模型的精度应接近于随机猜测
evaluate_accuracy(net, test_iter)
4.6 训练
首先,我们定义一个函数来训练一个迭代周期
.train()
updater()函数
if isinstance(updater, torch.optim.Optimizer):
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train() # 告诉pytorch我要开启训练模式,准备计算梯度吧
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3) #用长度为3的迭代器来累加我们需要的信息
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
# 含义:如果条件updater是torch.optim.Optimizer的话,执行以下代码块
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.mean().backward()
updater.step() # 对参数进行自更新
else:
# 使用定制的优化器和损失函数
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
# metric[0]:loss的样本数
# metric[1]:分类正确的样本数
# metric[2]:样本总数
在展示训练函数的实现之前,我们定义一个在动画中绘制数据的实用程序类Animator, 它能够简化本书其余部分的代码
class Animator: #@save
"""在动画中绘制数据"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
d2l.use_svg_display()
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(
self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型(定义见第3章)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
# 首先定义一个可视化的animator
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
# 得到模型训练后的误差矩阵
test_acc = evaluate_accuracy(net, test_iter)
# 在测试数据集上评估精度
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc
作为一个从零开始的实现,我们使用 3.2节中定义的 小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1
lr = 0.1
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
现在,我们训练模型10个迭代周期。
迭代周期(num_epochs)和学习率(lr)都是可调节的超参数。
通过更改它们的值,我们可以提高模型的分类精度。
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
4.7 预测
给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)
def predict_ch3(net, test_iter, n=6): #@save
"""预测标签(定义见第3章)"""
for X, y in test_iter:
#在测试的数据集中拿出一个样本
break
trues = d2l.get_fashion_mnist_labels(y)# 取出真实标号
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1)) # 取出预测标号
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
predict_ch3(net, test_iter)
4.8 小结
借助softmax回归,我们可以训练多分类的模型。
训练softmax回归循环模型与训练线性回归模型非常相似:先读取数据,再定义模型和损失函数,然后使用优化算法训练模型。大多数常见的深度学习模型都有类似的训练过程
5 Softmax回归简洁实现
5.1 初始化模型参数
通过深度学习框架的高级API也能更方便地实现softmax回归模型。 本节如在 3.6节中一样, 继续使用Fashion-MNIST数据集,并保持批量大小为256。
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
# Flatten() 把任意维度的tensor变为2d的tensor,即第0维度保留,剩下的维度全部展成一个向量
# nn.Linear() 输入为784,输出为10
# 放入Sequential类的构造器中,得到net
def init_weights(m): # m指的是当前层
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
5.2 重新审视Softmax的实现
loss = nn.CrossEntropyLoss(reduction='none')
5.3 优化算法
使用学习率为0.1的小批量随机梯度下降作为优化算法。 这与我们在线性回归例子中的相同,这说明了优化器的普适性。
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
5.4 训练
调用 3.6节中 定义的训练函数来训练模型
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)