09 Softmax 回归 + 损失函数 + 图片分类数据集【动手学深度学习v2

----未完待续----
softmax回归

- softmax回归是机器学习中非常重要而且经典的模型,他的名字虽然叫回归,但实际上是一个分类问题
分类与回归
- 回归是估计一个连续值
- 分类是预测一个连续的类别
- 分类问题举例




从回归到多类分类

- 分类问题从单输出变成了多输出,输出的个数等于类别的个数
从回归到多类分类

- 类别是一个数,也可能是一个字符串
- 一位有效编码:刚好有一个位置有效的编码方式(有效的那一位为 1 ,其它位上全部置 0 )

- 不关心置信度的值是多少,只关心正确类别的置信度的值要远远高于其他非正确类置信度,使得正确的类别能够与其他类别拉开距离
- 虽然关心的是一个相对值,但是需要把这些值放在一个合适的区间

- 希望是输出能够尽量是一个概率
- y^:一个长为 n 的向量,每个元素都非负,而且这些元素加起来的和为 1
- exp(o):e 的 o 次方,e 是一个指数常数。指数的好处是:不管是什么值都能将它变成非负
- 通过 softmax 将输出 o 变成了概率
softmax 和交叉熵损失

- 一般来说使用真实概率与预测概率的区别来作为损失
- 假设 p 和 q 是两个离散概率
- 上图中第二式的化简:根据前面的一位有效编码,yi中只有一位有效元素为1,其他都为0
- 对分类问题来讲,不关心对于非正确类的预测值,只关心对于正确类的预测值及其置信度的大小
总结
- softmax 回归是一个多分类分类模型
- 使用 softmax 操作子得到每个类的预测置信度(使得每个类的置信度是一个概率,非负且和为1)
- 使用交叉熵来衡量预测和标号的区别(作为损失函数)
损失函数

- 损失函数用来衡量预测值和真实值之间的区别
常用的损失函数
1、L2 Loss(均方损失)

- 蓝色曲线表示 y = 0 的时候变化预测值 y‘ 的函数,这是一个二次函数
- 绿色曲线是它的似然函数,即exp( - l ),它的似然函数是一个高斯分布
- 橙色曲线表示损失函数的梯度,它是一条过原点的直线
- 在梯度下降的时候是根据负梯度方向来更新梯度,所以它的导数决定如何更新参数

- 当 y 和 y‘ 离得比较远(横轴到零点的距离越远),梯度越大,对参数的更新越多,更新的幅度越大,反之亦然
2、L2 Loss(绝对值损失函数)

- 蓝色曲线表示 y = 0 的时候变化预测值 y‘ 的函数
- 绿色曲线是它的似然函数,即exp( - l )
- 橙色曲线表示损失函数的梯度,当 距离 > 1 的时候他的值为1,当 距离 < 1 的时候他的值为-1
- 绝对值函数在0点处不可导
- 在梯度下降的时候是根据负梯度方向来更新梯度,它的梯度永远是常数,所以就算 y 和 y‘ 离得比较远,参数更新的幅度也不会太大,会带来稳定性上的好处
- 它的缺点是在 0 点处不可导,另外在 0 点处有一个 -1 到 1 的剧烈变化,不平滑性导致预测值和真实值靠的比较近的时候,也就是优化到了末期的时候这个地方可能就变得不那么稳定
Huber's Robust Loss(Huber鲁棒损失)

- 当真实值和预测值相差较大时,他是一个绝对误差,而当他们相差较小时是一个均方误差
- 减去 1 / 2 的作用是使得分段函数的曲线能够连起来
- 蓝色曲线表示 y = 0 的时候变化预测值 y‘ 的函数,在 -1 到 1 之间是一个比较平滑的二次函数,在这个区间外是一条直线
- 绿色曲线是它的似然函数,即exp( - l ),和高斯分布有点类似,但是不想绝对值误差那样在 0 点处有一个很尖的地方
- 橙色曲线表示损失函数的梯度,当 距离 > 1 的时候他的值为 -1 或者 1,是一个常数,当距离 < 1 的时候是一个渐变的过程,这样的好处是当预测值和真实值差得比较远的时候,参数值的更新比较均匀,而当预测值和真实值相差比较近的时候,即在参数更新的末期,梯度会越来越小,保证优化过程是比较平滑的,不会出现数值上的剧烈变化
图片分类数据集
MNIST数据集(对手写数字的识别)是图像分类中广泛使用的数据集之一,但是作为基准数据集过于简单,因此使用类似但是更复杂的Fashion-MNIST数据集
1、导包
%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l
d2l.use_svg_display()
- torchvision:pytorch对于机器学习实现的一个库
- from torch.utils import data:方便读取数据的一些小批量的函数
- transforms:对数据进行操作的模块
- from d2l import torch as d2l:将一些函数实现好之后存在 d2l 中
- d2l.use_svg_display():使用 svg 来显示图片,清晰度会更高一些
2、通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中
trans = transforms.ToTensor()
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)
- trans = transforms.ToTensor():将图片转换成pytorch中的tensor,通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,并除以255使得所有像素的数值均在 0 到 1 之间
- train=True:代表下载的是训练集数据集
- download=True:可以将数据集加载好然后放在指定的目录下就可以不用download了
- train=False:代表下载的是测试集数据集
- 报错:<urlopen error [WinError 10060] 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。>,科学上网之后可以解决这个问题
输出:
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-images-idx3-ubyte.gz
Using downloaded and verified file: ../data\FashionMNIST\raw\train-images-idx3-ubyte.gz
Extracting ../data\FashionMNIST\raw\train-images-idx3-ubyte.gz to ../data\FashionMNIST\raw
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-labels-idx1-ubyte.gz
Using downloaded and verified file: ../data\FashionMNIST\raw\train-labels-idx1-ubyte.gz
Extracting ../data\FashionMNIST\raw\train-labels-idx1-ubyte.gz to ../data\FashionMNIST\raw
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-images-idx3-ubyte.gz
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-images-idx3-ubyte.gz to ../data\FashionMNIST\raw\t10k-images-idx3-ubyte.gz
100.0%
Extracting ../data\FashionMNIST\raw\t10k-images-idx3-ubyte.gz to ../data\FashionMNIST\raw
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-labels-idx1-ubyte.gz
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-labels-idx1-ubyte.gz to ../data\FashionMNIST\raw\t10k-labels-idx1-ubyte.gz
119.3%
Extracting ../data\FashionMNIST\raw\t10k-labels-idx1-ubyte.gz to ../data\FashionMNIST\raw
(60000, 10000)
mnist_train[0][0].shape
输出:
torch.Size([1, 28, 28])
- torch.Size([1, 28, 28]):因为它是一个黑白图片,所以channel数是 1
3、定义两个可视化数据集的函数
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
"""Plot a list of images."""
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
4、几个样本的图像及其相应的标签
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));
- next:拿到第一个小批量
输出:

- X.reshape(18, 28, 28):18张28 * 28的图片
- 2, 9:2 行 9 列
- titles=get_fashion_mnist_labels(y):图片的名称显示其对应的标号
5、读取一小批量数据,大小为batch_size
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())
timer = d2l.Timer()
for X, y in train_iter:
continue
f'{timer.stop():.2f} sec'
- 定义一个函数 get_dataloader_workers() 每次返回 4 ,使用 4 个进程数来进行读取,可以根据 cpu 的大小来寻阿泽一个小一点的数或者大一点的数
- shuffle=True:指定是否打乱顺序
- num_workers=get_dataloader_workers():分配多少个进程,这里给定的是 4
- timer = d2l.Timer():timer 用来测试速度
输出:
'4.00 sec'
- 读取一次数据的时间是 4 秒
- 经常会遇到一个性能问题:模型训练速度比较快,但是数据读不过来,训练之前可以看一下数据读取的速度,一般要比模型的训练速度要快,这是一个常见瓶颈
6、整合 load_data_fashion_mnist() 函数
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载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()))
- resize:是否将图片变得更大,后续可能需要更大的图片
softmax回归的从零开始实现
1、导包
import torch
from IPython import display
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
- batch_size = 256:每次随机读取256张图片
- d2l.load_data_fashion_mnist(batch_size):调用存储在 d2l 中的读取数据集的函数(上面已经实现了)
- train_iter, test_iter:返回训练集和测试集的迭代器
2、展平每个图像,将他们视为长度为 784 的向量,因为数据集有 10 个类别,所以网络的输出维度是 10
num_inputs = 784
num_outputs = 10
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
- 每张图片都是 28 *28,通道数为 1.是一个三维的输入,但是对于 softmax 回归来讲,它的输入需要是一个向量,所以需要将图片拉长成为一个向量
- 这个“拉长”的操作会损失很多的空间信息,这里留给卷积神经网络来提取特征信息
- W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True):定义权重 W,将他初始为一个高斯随机分布的值,均值为 0,方差为 0.01.它的形状是行数等于输入的个数,列数等于输出的个数,因为要计算梯度,所以requires_grad=True
- b = torch.zeros(num_outputs, requires_grad=True):定义偏移量 b:它的长度等于输出的个数,因为要计算梯度,所以requires_grad=True
3、回顾:给定一个矩阵 X ,对所有元素求和
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:将 shape 中的第 0 个元素变成 1,这就变成了一个行向量
- 1:将 shape 中的第 1 个元素变成 1,这就变成了一个列向量
输出:
(tensor([[5., 7., 9.]]),
tensor([[ 6.],[15.]]))
4、实现 softmax

def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里应用了广播机制
- torch.exp(X):将 X 的每一个元素做指数运算
- X_exp.sum(1, keepdim=True):按照 shape 中的第一个元素求和,即按照每一行来进行求和
5、将每一个元素变成一个非负数。根据概率原理,每行的总和为 1
X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
- X = torch.normal(0, 1, (2, 5)):创建一个均值为 0,方差为 1 的2行5列的矩阵 X
输出:
(tensor([[0.3740, 0.1609, 0.1182, 0.0572, 0.2897],
[0.2156, 0.2721, 0.1700, 0.3240, 0.0184]]),
tensor([1., 1.]))
6、实现 softmax 回归模型
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
7、实现交叉熵损失
补充:创建一个数据 y_hat,其中包含 2 个样本在 3 个类别的预测概率,使用 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_hat[[0, 1], y]给定一个预测值,然后拿出给定标号的真实类别的预测值是多少。对应[0][0],[1][2]
输出:
tensor([0.1000, 0.5000])
8、实现交叉熵损失函数
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
cross_entropy(y_hat, y)
输出:
tensor([2.3026, 0.6931])
9、将预测类别和真实 y 元素进行比较
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)
输出:
0.5
10、评估在任意模型 net 的准确率
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度。"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 正确预测数、预测总数
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
11、Accumulator实例中创建了 2 个变量,用于分别储存正确预测的数量和预测的总数量
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]
evaluate_accuracy(net, test_iter)
输出:
0.1274
12、softmax回归的训练