本章的核心目标是对图片分类,和之前线性回归中求w和b的值不同,本章的目的是求出来所属类别,即一个是连续的,一个是离散的。在编程之前,可以想象该系统为一个黑箱,输入值是各个图片,输出值是图片所属类别的序号。
将问题细化思考,输入的是图片数字化后的值,输出是图片属于各个分类的概率,选择最大概率的类别作为输出。
在确定到这个程度之后就需要考虑黑箱内部的东西了,根据线性模型,我们首先想到的是,将数字化后的各个值赋予权重后相加,之后加上偏差b。得到的输出通过某种办法,转化成概率。将最大的概率的结果与实际结果做对比,如果不同,则通过损失函数对w和b进行迭代,减小误差。
首先考虑将输出值转化成概率。上述过程中我们明确了希望找到最大概率作为结果输出。根据概率的定义,我们知道每个值都应该是非负数,所有情况相加应该得0。因此,输出结果首先通过某个方程y将在实数范围内的结果映射到非负数范围内,同时不会改变相对大小。之后则可以通过用yi除以全部y求和,这样则可以满足相加得1。
前人提出了softmax函数,满足了上述的要求。
根据定义我们再次分析需求,
1. 对输出值求指数可以将在实数范围内的数映射到非负数范围内,符合概率要求;
2. 指数后的值本身除以所有指数后的值的和。显然,当全部输出相加时等于1,符合概率要求。
在确定输出后就需要考虑通过损失函数对w和b进行迭代更新。
在线性回归时我们选择了均方损失(MSE),在此处我们将选择另一种损失函数,交叉熵损失函数。(由于本人学艺不精,此处根据最大似然函数推导得出交叉熵损失没有完全弄明白,为防止误人子弟,不具体说明。)
我们已知标签y只有1和0两个值,因此公式简化为当yj=1时的值。
此处附一篇对于交叉熵损失和均方损失对比的讲解。
损失函数|交叉熵损失函数https://zhuanlan.zhihu.com/p/35709485
我们将图像分类中应用最广泛的数据集之一Fashion-MNIST。可以通过torchvision调用。
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l
from matplotlib import pyplot as plt
d2l.use_svg_display() #上一章有对该函数的调用,本章不重复展示
trans = transforms.ToTensor()
"""
通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,
并除以255使得所有像素的数值均在0到1之间
"""
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)
"""
root表示文件下载到本地的位置,其中..表示的是上级目录,你写代码地方的上级。
root="../data"表示在上级目录中创建一个data文件夹并进入下载,如果已有则直接进入
train=True表示的是下载后加载训练集到mnist_train上
transform=trans更改类型到指定类型
download=True表示下载,如果指定文件夹里有对应内容,可以为False
"""
首先考虑一下实现步骤:
if __name__ == "__main__":
# 学习率 learning rate: lr
lr = 0.1
# 循环次数,刷数据的遍数
num_epochs = 5
# 一个批次包含的图片量
batch_size = 256
# 已知图片长宽是28 28,则拉长后为28*28
num_inputs = 28 * 28
# 输出图片类别
num_outputs = 10
# 读取一个批次的训练集和测试集
train_iter, test_iter = load_data_fashion_mnist(batch_size)
# 初始化权重和偏移,需要计算梯度
w = torch.normal(0, 0.01, [num_inputs, num_outputs],requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
"""第一步及第二步完成"""
根据softmax定义首先写出softmax函数:
def softmax(X):
return torch.exp(X) / torch.exp(X).sum(dim=1, keepdim=True)
和线性回归不同的是,线性回归的输出数为1,而softmax根据类别的种类有多个输出数。已知fashion-mnist数据集有10个种类,因此输出是10。下面是训练网络函数:
def net(X):
"""
将每个图片reshape成一条数据(1, 28*28), reshape后的数据是第0
维数量是图片数量,第一维数量是图片图片的像素数
与W做叉乘并加上偏移b并将结果做softmax得到概率
"""
out = torch.matmul(X.reshape(-1, W.shape[0]), W) + b
return softmax(out)
损失函数。我们已确定损失函数是交叉熵函数。
def corss_entropy(y_hat, y):
return -torch.log(y_hat[range(len(y)), y])
更新函数。SGD函数,随机梯度下降。在此之前我们应该已经计算过梯度。
def SGD(params, lr, batch_size):
"""params是[W, b]"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
接下来需要编写单次训练的函数,我们希望对全部样本训练一次,得到训练后的损失率(即错误样本 / 全部样本)和训练精度(即训练正确样本/全部样本)。
为了记录每一次的结果,可以简单的直接定义变量对每个批量的结果累加,或者定义一个加法器类,记录结果。
此处考虑加法器类。首先考虑加法器实现的功能:累加,输出结果。
class Accumulator:
def __init__(self, n) -> None:
"""n意味着需要记住几个值"""
self.data = [0.0] * n
def add(self, *args):
"""累加"""
self.data = [a + float(b) for a, b in zip(self.data, args)]
def __getitem__(self, item):
"""用于直接读数"""
return self.data[item]
此外,因为需要训练精度,因此需要求的正确判断样本类型的个数。定义accuracy函数
def accuracy(y_hat:torch.Tensor, y):
"""求每一行的最大值的位置:
例如y_hat[0] = [0.1, 0.1, 0.2, 0.5, 0.1]
最大值在第三位,输出3 """
y_hat = y_hat.argmax(dim=1)
"""将取等后的布尔结果赋值给cmp"""
cmp = y_hat.ttype(y.dtype) == y
return float(cmp.type(y.dtype).sum)
单次训练
# 首先确定训练网络,训练数据集,随时函数和更新函数
def train_epoch_ch3(net, train_iter, loss, updater):
metric = Accumulator(3)
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y)
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]