Softmax

深度学习只有软聚类,经过Softmax Regression之后得到各个概率值

由于softmax保留了参数之间的顺序,也保留了原本的概率顺序,所以不需要再次计算哪个类别的概率更大

The idea of a softmax dates back to Gibbs (1902), who adapted ideas from physics. Dating even further back, Boltzmann, the father of modern statistical physics, used this trick to model a distribution over energy states in gas molecules. In particular, he discovered that the prevalence of a state of energy in a thermodynamic ensemble, such as the molecules in a gas, is proportional to exp⁡(−E/kT). Here, E is the energy of a state, T is the temperature, and k is the Boltzmann constant. When statisticians talk about increasing or decreasing the “temperature” of a statistical system, they refer to changing T in order to favor lower or higher energy states. Following Gibbs’ idea, energy equates to error. Energy-based models (Ranzato et al., 2007) use this point of view when describing problems in deep learning.

损失函数

softmax将输出映射到概率空间,但还需要一种方法来优化映射的准确性,在现代方法中一般采用的是对数似然(Log-Likelihood)

使用负对数将问题转换成最小化问题

这个式子简化后的结果称为交叉熵损失(cross-entropy loss)

其中是预测的概率,是01值,只对正确的类别为1,其他都是0,对其求导可以得到:

发现其实导数是 模型通过softmax进行分配的概率与实际发生的情况的差异,这与回归的损失函数求导结果十分相(观察值与估计值之间的差异),这使得梯度计算变得容易

信息论

在信息论的解释下,其实熵是知道真实概率的人所经历的惊讶程度(surprisal),交叉熵是观察者在看到实际的概率P生成数据时,基于主观概率Q的预期惊讶程度

详见:4.1. 软最大化回归 — 深度学习入门 1.0.3 文档 --- 4.1. Softmax Regression — Dive into Deep Learning 1.0.3 documentation

Image Classification

Fhasion-MNIST

这是一个17年发布的数据集,其包含10种类别,分辨率在28 * 28,每个类别训练集有6000张,测试机有1000张

很多主流框架提供了其处理方法

class FashionMNIST(d2l.DataModule):  #@save
    """The Fashion-MNIST dataset."""
    def __init__(self, batch_size=64, resize=(28, 28)):
        super().__init__()
        self.save_hyperparameters()
        trans = transforms.Compose([transforms.Resize(resize),
                                    transforms.ToTensor()])
        self.train = torchvision.datasets.FashionMNIST(
            root=self.root, train=True, transform=trans, download=True)
        self.val = torchvision.datasets.FashionMNIST(
            root=self.root, train=False, transform=trans, download=True)

其中tansforms.Compose([transforms.Resize(resize), transforms.ToTensor()])方法是构建了一个图像transform流水线,之后使用时图像会经过这个转换流程,可以独立定义

@d2l.add_to_class(FashionMNIST)  #@save
def text_labels(self, indices):
    """Return text labels."""
    labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
              'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
    return [labels[int(i)] for i in indices]

这个函数可以从类别下标转换成对应的文字类别

def get_dataloader(self, trian):
	data = self.train if train else self.val
	return torch.utils.data.DataLoader(data, self.batch_size, shuffle=trian, num_workers=self.num_workers)

获取一个minibatch的工具函数

tic = time.time()
for X, y in data.trian_dataloader():
	continue
f'{time.time() - tic:.2f} sec'

输出一个batch的读取时间,只要其小于图像处理时间就足够好了

def visualize(self, batch, nrows=1, ncols=8, labels=[]):
	X, y = batch
	if not labels:
		labels = self.text_labels(y)
	d2l.show_images(X.squeeze(1), nrows, ncols, titles=labels)
batch = next(iter(data.val_dataloader()))
data.visualize(batch)

可视化,输出一行8列的图像结果进行查看

import matplotlib.pyplot as plt
 def visualize(batch, nrows=1, ncols=8, labels=[]):
 X, y = batch
 X = X.squeeze(1) #去掉单通道维度,从[N, 1, H, W] 变为 [N, H, W]
 if not labels:
	 labels = [f"label {int(i)}" for i in y]
 fig, axes = plt.subplots(nrows, ncols, figsize=(ncols*2, nrows*2))
 axes = axes.flatten()
 for i in range(len(axes)):
	 if i < len(X):
		 axes[i].imshow(X[i])
		 axes[i].set_title(labels[i], fontsize=10)
		 axes[i].axis('off')  # 关闭坐标轴
	 else:
		 axes[i].axis('off') # 如果图像数量少于子图数量,隐藏多余的子图
	 plt.tight_layout()
	 plt.show()
	 
batch = next(iter(data.val_dataloader()))
visulize(batch, nrows=2, ncols=5)

使用matplotlib实现方法

Base Classification Model

基于之前的代码,先尝试构建一个基础类

Classifier 类

增加validation_step方法,每次报告上一批次的损失和分类准确度,为每个num_val_batches批次绘制一个更新,方便验证分析数据的损失和准确率,尽管最后一个批次可能较少(不一定能整除),但不做特殊处理

class Classifier(nn.Module):
	def validation_step(self, batch):
		Y_hat = self(*batch[:-1])
		self.plot('loss', self.loss(Y_hat, batch[-1]), train=False)
		self.plot('acc', self.accuracy(Y_hat, batch[-1]), train=False)

其中*batch[:-1] ,* 号是解包的含义,将原本作为列表的batch[:-1]解包成一个个元素,[:-1] 是取出出最后一个元素的所有元素(最后一个元素一般是标签),self() 其实是因为Module 实现了内置函数,__call__ ,调用self其实就是调用self.__call__ ,进行前向传播(forward函数),所以返回了预测的Y值

报告精度

def accuracy(self, Y_hat, Y, averaged=True):
	Y_hat = Y_hat.reshape((-1, Y_hat.shape[-1]))
	preds = Y_hat.argmax(axis=1).type(Y.dtype)
	compare = (preds == Y.reshape(-1)).type(torch.float32)
	return compare.mean() if averaged else compare

其中,

  • Y_hat.rashape((-1, Y_hat.shape[-1]))的含义是取出Y_hat形状的最后一维(也就是类别数)作为第二维来reshape Y_hat,-1 代表的是自动计算第一维的大小
  • 另外,代码假设预测的概率值在Y_hat的第二维,对其取最大值并进行类型转换,因为后续的 == 对类别敏感
  • 最后转换成float32数值

Softmax Regression Implementation from Scratch

Softmax实现

实现只是为了理解原理,实际上还有很多处理的缺陷,使用时还是用框架实现的softmax比较好

def softmax(X):
	X_exp = torch.exp(X)
	partition = X_exp.sum(1, keepdim=True)
	return X_exp / partition

Model

这里模型与之前无二,初始化W为高斯分布,b为0

forward之前先将图片展平,因为是线性分类,所以先展成一维,而且列维度要和权重W行维度一致

def forward(self, X):
	X = X.reshape((-1, self.W.shape[0]))
	return softmax(torch.matmul(X, self.W) + self.b)

预测结束之后可以通过下方的代码查看预测错误

wrong = preds.type(y.dtype) != y
X,y,preds - X[wrong], y[wrong], preds[wrong]
labels = [a+'\n'+b for a, b in zip(
	data.text_labels(y), data.text_labels(preds)
)] # 定义两个标签,一个是正确的结果,一个是错误的预测
 
data.visualize([X, y], labels=labels)

Concise Implementation of Softmax Regression

定义模型

class SoftmaxRegression(nn.Classifier):
	def __init__(self, num_output, lr):
		super()__init__()
		self.save_hyperparameters()
		self.net = nn.Sequential(nn,Flatten(), nn.LazyLinear(num_output)
	
	def forward(self, x):
		return self.net(x)

Softmax

这里提到两个softmax实现会遇到的问题,一个是指数溢出和下溢,因为通过exp()来计算softmax,如果指数很大,那很可能会超float32的精度,如果指数很小,则会得到非常大的负数,发生下溢。

实际的解决方法是在指数中减去最大的指数值

这样一来,分子会保持在1以内,分母则在之间(m为指数的数量),只有当分子为0时,在后续的取对数运算中会发生下溢。这就是另一个NaN问题。

解决方法是将softmax与交叉熵结合,将中间的过程结果(而不是计算之后的输出)传递给交叉熵,让取对数操作对exp进行,化简之后得到

这种方法称为“LogSumExp trick”,传递的中间结果称为logits,在交叉熵损失中一次计算softmax与对数结果。

Generalization in Classification

The Test Set

对测试集的评估其实属于对基础种群的一种统计估计,因为假设测试集是随机均匀采样,因此,泛化问题其实是一个均值估计问题

中心极限定理, 从均值 标准差 分布中抽取n随机样本,当n的数量趋于无穷大时,样本均值趋于以真实均值为中心,标准差为 的正态分布。

从这个角度来看,随着实例数量的增加,测试集的误差以的速度接近真实误差,也就是如果要精度增到 倍,就得有 倍的测试集

由于随机变量 只能取值0和1,因此是伯努利随机变量,特征是一个参数,表示取值1的概率,这里随机变量参数实际上是真是错误率。

伯努利分布的方差取决于其参数

对于这种情况下,也就是参数为真实错误率的情况下,参数最大为1,最小为0,所以方差最大时错误率为0.5,标准差为

这说明,当想让测试误差近似于总体误差,使得标准差在正负0.01之间有效,那么应该收集2500个样本,如果要有95%的把握,则需要10000个样本

上述其实只给出了一个渐进关系,由于随机变量是有界的,可以通过Hoeffding不等式来获得有效的有限样本界限

对于独立同分布的随机变量 ,若每个 (有界),则Hoeffding不等式给出样本均值与期望值的偏差概率上界:

在分类错误率问题中:

  • (伯努利变量,故
  • (真实错误率)
  • 样本均值
  • 就是误差容忍值,表示已P()的概率认为误差小于

代入参数后,Hoeffding不等式简化为:

根据这个式子可以计算出满足大多的置信度时需要多少样本

Test Set Reuse

在收集测试集时,一般考虑的是当前分类器的精度需求,当想采用多个分类器时,每个分类器有一定概率出错(例如95%),当多个分类器一起训练时,就无法确定这次训练的有效性,因为涉及到多个假设等

同时存在自适应过拟合问题,因为针对某个数据集的精度提高,已经修改了多次分类器,这可能引入测试集中的信息,导致测试集不能完全视为真正的测试集

VC维

为了衡量模型类的复杂性,解决泛化能力问题,解释为什么、何时基于经验数据训练的模型可以泛化到没见过的数据,用于分析经验误差与总体误差之间的差距。

VC维其实是衡量一个模型类别的表达能力,具体来说,其表示模型能“完美记忆”的最大样本量,例如直线分类器的VC维是3,3点内的数据总能被一条直线分开,而4个点不行

引入VC维,可以将泛化误差的概率上界定义为:

其中:

  • :模型在真实分布上的误差(总体风险)

  • :模型在训练集上的误差(经验风险)

  • :泛化误差的最大允许差值(如要求泛化误差不超过经验误差+2%)

  • :违反该条件的概率(如设定为5%)

  • VC维度()和样本量()的影响

    • 是与损失函数范围相关的常数

通过VC维可以指导统计概率模型样本量,但是对于深度来说并不适用