写在前面:本节内容主要参考自《动手学深度学习》,本文对其中内容进行了补充,并将可能疑问的地方进行了标注和详细的解释。完整代码实现参考【】,简洁实现参考【】。如有疑问和问题欢迎给位交流和指出。
softmax 从零开始实现
1. 图像分类数据集
1.1 数据集加载与处理
我们使用Fashion-MNIST进行softmax模型的训练。我们可以通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。torchvision.datasets 中提供了很多常见数据集的下载与处理。torchvision.transforms 中提供了各种图片类型的转换。
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(root='../data',train=True,transform=trans,download=True)
mnist_test = torchvision.datasets.FashionMNIST(root=r'../data',train=False,transform=trans,download=True)
Fashion-MNIST 由10个类别的图像组成,每个输入图像的宽度和高度均为28像素,其通道数为1.
print(len(mnist_train))
>>> 60000
print(len(mnist_test))
>>> 10000
print(mnist_train[0])
>>> (tensor([[[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
...
0.0000, 0.0000, 0.0000, 0.0000]]]), 9)
print(mnist_train[0][0].shape)
>>> torch.Size([1, 28, 28])
注:1)由上述代码可见,数据集的标签并不是onehot-encoding 类型!!而是类别数字。
1.2 读取小批量
我们通过内置函数,创建一个数据迭代器。
def get_dataloader_works():
"使用4个进程来读取数据"
return 4
batch_size = 256
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=get_dataloader_works())
1.3 整合所有组件
我们定义load_data_fashion_mnist 函数,用于获取和读取Fashion-MNIST数据集。它返回训练集和验证集的数据迭代器。此外,它还接受一个可选参数,用来将图像大小调整为另一种形状。
def load_data_fashion_mnist(batch_size, resize=None):
"""下载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=r'../data', train=False, transform=trans, download=True)
return(data.DataLoader(mnist_train,batch_size,shuffle=True,num_workers=get_dataloader_works()),
data.DataLoader(mnist_test,batch_size,shuffle=True,num_workers=get_dataloader_works()))
注:1)trans.insert 为列表在指定索引处添加数据的方法。
? 2)transforms.Compose([]) 可以用来定义对数据集进行多种处理。其参数需要为数据集转换方法列表。
2. 初始化模型参数
由上文,原始数据集中的每个样本都是28*28的图像。我们将展平每个图像,把他看作长度为784的向量,这里我们认为一个像素代表一个特征,而没有考虑空间结构(cnn)。因为数据集有10个类别所以输出维度为10。因此,权重将构成一个784*10的矩阵,偏置将构成一个1*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)
3. 定义softmax操作
3.1 sum()方法基础
给定一个矩阵我们可以使用sum() 方法,在默认情况下对所有元素求和。我们也可以通过传入参数只求同一轴上的元素,即同一列 (轴0) 或同一行 (轴1)。假设X是一个形状为(2,3)的张量,我们对列进行求和,则结果将是一个具有形状(3,)的向量。当调用sum运算符时,**我们可以令参数keepdim 为True,使得求和结果保留原始张量的轴数。**这将产生一个(1,3)的二维张量。
3.2 softmax操作
回顾softmax公式
s
o
f
t
m
a
x
(
X
)
i
j
=
e
x
p
(
X
i
j
)
∑
k
e
x
p
(
X
i
k
)
softmax(\mathbf{X})_{ij}=\frac{exp(\mathbf{X_{ij}})}{\sum_kexp(\mathbf{X_{ik}})}
softmax(X)ij?=∑k?exp(Xik?)exp(Xij?)? 代码实现
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1,keepdim=True)
return X_exp / partition
注:虽然在数学上看起来是正确的,但我们在代码实现中有些草率。矩阵中非常大或非常小的元素有可能造成数值上溢或下溢,但我们没有采取措施来防止这点。
4 定义模型
下面将定义输入如何通过网络映射到输出。注意,在将数据传递到我们模型之前,我们使用reshape 函数将每张原始图像展平为向量。
def net(X,W,b):
"""定义模型"""
return softmax(torch.matmul(X.reshape((-1,W.shape[0])),W)+b)
5. 定义损失函数
我们使用交叉熵损失函数,这一损失函数在深度学习中最为常见,因为分类问题要远远多于回归问题。
l
(
y
,
y
^
)
=
?
∑
j
=
1
q
y
i
?
l
o
g
?
y
j
^
l(y,\hat{y})=-\sum_{j=1}^qy_i\ log\ \hat{y_j}
l(y,y^?)=?j=1∑q?yi??log?yj?^? 由于
y
y
y为onehot-encoding格式,所以
y
i
y_i
yi?的值只能为1和0,所以交叉熵的结果可以简化为该组数据正确类别的预测概率的负对数似然。所以为了减少for 循环的使用,我们只需要通过数据集的原始onehot-encoding 标签,找到该组数据的真实类别。例如我们下面定义一个y_hat,其中包含两个样本在三个类别中的预测概率,找到一个定义他们正确类别的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]
>>> output
tensor([0.1000,0.5000])
注:列表的索引方式:[[行索引数组],[列索引数组]]
根据以上分析我们可以简单的定义我们的损失函数
def cross_entropy(y_hat, y):
"""定义交叉熵损失函数"""
return -torch.log(y_hat[range(len(y_hat)), y])
6. 分类准确率
虽然直接优化准确率可能很困难(因为准确率的计算不可导),但准确率通常是我们最关心的性能衡量标准。
为了计算准确率,我们执行以下操作:(需要学习借鉴这种思想!!!)
首先,如果 y_hat 是矩阵,那么假定第二个维度存储每个类的预测分数。我们使用 argmax 获得每行中最大元素的索引来获得预测类别。然后我们[将预测类别与真实 y 元素进行比较]。由于等式运算符 == 对数据类型很敏感,因此我们将 y_hat 的数据类型转换为与 y 的数据类型一致。结果是一个包含 0(错)和 1(对)的张量。进行求和会得到正确预测的数量。
def accuracy(y_hat,y):
"""计算预测准确率"""
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())
注:1)argmax:返回最大值所在得索引,axis = 1表示行中最大值,axis = 0表示列中最大值。
? 2)**tensor张量获取和改变类型的方法:cmp.type(y.dtype) **
? 3)== 也算是运算符,在代码过程中的使用。
同样,对于任意数据迭代器data_iter 可访问的数据集,我们可以评估在任意模型net 的准确率。
def evaluate_accuracy(net, data_iter):
"""计算在指定数据集上的模型精度"""
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]
注:1)python函数isinstance :函数isinstance()可以判断一个变量的类型,既可以用在Python内置的数据类型如str、list、dict,也可以用在我们自定义的类,它们本质上都是数据类型。参考博文
? 2)**net.eval()**会将模型设置为评估模式,即不会对以下运算建立计算图。
? 3)y.numel() 返回输入张量中元素的总的个数。
扩展:
这里 Accumulator 是一个实用程序类,用于对多个变量进行累加。 在上面的 evaluate_accuracy 函数中,我们在 (Accumulator 实例中创建了 2 个变量,用于分别存储正确预测的数量和预测的总数量)。当我们遍历数据集时,两者都将随着时间的推移而累加。
class Accumulator:
"""在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, item):
return self.data[item]
注:1)列表操作:[0.0]*2 会变为变为[0.0,0.0]
? 2)列表操作:zip()操作,具体见博客
? 3) 类内置方法__getitem__(self,item) :Python的特殊方法__getitem_() 主要作用是可以让对象实现迭代功能。同时,定义好__getitem__ 方法后我们可以通过直接对对象索引,可以得到相应位置的值。参考
a = Accumulator(2)
a[1]
for x,y in a:
pass
7. 训练
我们定义一个函数来训练一个迭代周期.其中updater 是更新模型参数的常用函数,他接受批量大小作为参数。它可以是封装的d2l.sgd 函数,也可以是框架的内置优化函数。
def train_epoch_ch3(net, train_iter, loss, updater):
"""训练模型一个迭代周期"""
if isinstance(net, torch.nn.Module):
net.train()
metric = Accumulator(3)
for X,y in train_iter:
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
updater.zero_grad()
l.backward()
updater.step()
metric.add(float(l)*len(y), accuracy(y_hat, y),y.size().numel())
else:
l.sum().bachward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
return metric[0] / metric[2], metric[1] / metric[2]
注:1)y.size() tensor y 的形状,y.size().numel 计算元素总的个数。
接下来我们定义一个训练函数:
(此处并没有定义可视化函数,用输出进行代替。)
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter,loss,updater)
train_loss, train_acc = train_metrics
print(f'the loss is {train_loss:.2f} , the accuracy is {train_acc:.2f}')
test_acc = evaluate_accuracy(net, test_iter)
print(f'the test accuracy is {test_acc}')
7.2 定义优化器
我们依旧使用之前定义好的小批量随机梯度下降来优化模型的损失函数:
lr = 0.1
def updater(batch_size,lr):
return d2l.sgd([W,b], lr, batch_size)
|