一些简单的深度学习模型从零实现看着教程看似简单,敲着代码就过去了,但还是做一下自己的初步总结。里面的思维过程无论放在今后什么问题里都是适用的,包括实现的基本步骤、模型的构思如何反映到代码,定义训练的过程需要哪些参数等等。
本文以Softmax回归从零实现为例,总结一下一个深度学习模型的实现过程,每个步骤都附上自己的总结思考。
比如我们想要用Softmax回归模型,去实现一个图片分类的问题。
后面是目的,前面是手段。
然后脑子里需要浮现出解决问题的几个基本步骤:
对于获取数据来说,一般初学者都是从一些公开数据集获取数据的,获取数据一般会用到pytorch相关库的一些方法。
比如我们想要读取 FashionMNIST 数据集,用于通过Softmax模型解决该数据集的分类问题(或者评估Softmax模型的分类性能,Whatever,不同表述而已)。
我们通常使用torchvision.datasets
下的类创建数据集,这个类下有许多公开数据集可以直接下载获取,如我们需要FashionMNIST
数据集:
# 对于机器学习问题,一般都会生成一个训练集,一个测试集
mnist_train = torchvision.datasets.FashionMNIST(
root='~/Datasets/FashionMNIST', train=True, download=True,
transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(
root='~/Datasets/FashionMNIST', train=False, download=True,
transform=transforms.ToTensor())
注意其中的transform参数,需要传入一个数据转换器(位于torchvision.transforms下),常用的有ToTensor()
方法,将图片数据转换为尺寸为(C x H x W)且大小位于[0.0, 1.0]的float32数据类型的Tensor。
创建的数据集对象属于torch.utils.data.Dataset
的子类。
我们创建一个Dataset后,一般直接传入torch.utils.data.Dataloader() 生成一个Dataloader对象。
batch_size = 256
train_iter = torch.utils.data.DataLoader(
mnist_train, batch_size=batch_size, shuffle=True, num_workers=0)
test_iter = torch.utils.data.DataLoader(
mnist_test, batch_size=batch_size, shuffle=False, num_workers=0)
# 这里的num_workers表示数据读取的线程数,Windows系统一般默认设0,这里更多信息可以自行搜索学习。
生成Dataloader后,就可以使用for循环读取批量数据了,一次循环是一个batch_size数量的数据。
batch_size是一个重要的超参数,横贯机器学习过程的始终,不仅方便计算机按批次读取数据减少内存开销,并且在计算梯度时,使用一个batch_size的数据进行迭代更新,大大减少计算量。
我们也可以使用next(iter(dataloader))
手工读取一个批次的数据。
Dataloader是一个可迭代对象,它通过生成迭代器,来读取批量数据。
对于一些数据,我们还可以构建生成器来读取批量数据,这部分扩展阅读可以参考我这篇文章。
我们选择Softmax回归模型时,心中需要构思好这个模型的数学表达,见下。
需要注意的是,softmax回归本身是一个单层神经网络,并且和线性回归一样是全连接的:
这里提示我们要有一个思维:把一个数学模型用深度神经网络去构建解释。
我们的Softmax回归数学模型是(以4像素图片,3分类标签为例):
o ( i ) = x ( i ) W + b y ^ ( i ) = softmax ( o ( i ) ) p = arg max p y ^ p \begin{aligned} \boldsymbol{o}^{(i)} &=\boldsymbol{x}^{(i)} \boldsymbol{W}+\boldsymbol{b} \\ \hat{\boldsymbol{y}}^{(i)} &=\operatorname{softmax}\left(\boldsymbol{o}^{(i)}\right) \end{aligned} \\ p = \underset{p}{\argmax } \hat{y}_{p} o(i)y^(i)=x(i)W+b=softmax(o(i))p=pargmaxy^p
x ( i ) = [ x 1 ( i ) x 2 ( i ) x 3 ( i ) x 4 ( i ) ] , o ( i ) = [ o 1 ( i ) o 2 ( i ) o 3 ( i ) ] , y ^ ( i ) = [ y ^ 1 ( i ) y ^ 2 ( i ) y ^ 3 ( i ) ] \boldsymbol{x}^{(i)}=\left[x_{1}^{(i)} \quad x_{2}^{(i)} \quad x_{3}^{(i)} \quad x_{4}^{(i)}\right], \boldsymbol{o}^{(i)}=\left[\begin{array}{lll} o_{1}^{(i)} & o_{2}^{(i)} & o_{3}^{(i)} \end{array}\right], \\ \hat{\boldsymbol{y}}^{(i)}=\left[\begin{array}{lll} \hat{y}_{1}^{(i)} & \hat{y}_{2}^{(i)} & \hat{y}_{3}^{(i)} \end{array}\right] x(i)=[x1(i)x2(i)x3(i)x4(i)],o(i)=[o1(i)o2(i)o3(i)],y^(i)=[y^1(i)y^2(i)y^3(i)]
参数:
W = [ w 11 w 12 w 13 w 21 w 22 w 23 w 31 w 32 w 33 w 41 w 42 w 43 ] , b = [ b 1 b 2 b 3 ] \boldsymbol{W}=\left[\begin{array}{lll}w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ w_{31} & w_{32} & w_{33} \\ w_{41} & w_{42} & w_{43}\end{array}\right], \quad \boldsymbol{b}=\left[\begin{array}{lll}b_{1} & b_{2} & b_{3}\end{array}\right] W=⎣⎢⎢⎡w11w21w31w41w12w22w32w42w13w23w33w43⎦⎥⎥⎤,b=[b1b2b3]
本例中,我们需要学习的模型系数是 矩阵W和偏倚项b。
我们将28281大小的图片拉伸为28×28=784长度的向量(输入的是一个batch的X,即X形状为256×784),输出的是10分类,因此W的形状为:784×10
偏倚项b的形状为10×1
然后,我们一般使用(0,0.01)的正态分布去初始化参数的数值:
num_inputs = 784
num_outputs = 10
W = torch.tensor(
np.random.normal(loc=0, scale=0.01, size=(num_inputs, num_outputs)),
dtype=torch.float)
b = torch.zeros(num_outputs , dtype=torch.float)
最后,最重要的一步,W和b设上梯度,因为我们需要学习这个参数!
# 设上梯度
W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
先定义softmax:
def softmax(X):
X_exp = X.exp()
partition = X_exp.sum(dim=1, keepdim=True)
return X_exp / partition # 这里使用了广播机制
定义模型:
def net(X):
return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)
定义交叉熵损失函数:
def cross_entropy(y_hat, y):
return -torch.log(y_hat.gather(dim=1, index=y.view(-1,1)))
# y就是待传入的那个批量的label数据
(定义交叉熵损失函数的细节可以参考我这篇文章)
我们在定义模型和损失函数时,在心中对模型的计算图最好有个大致的把握。
我们可以先看看本模型损失函数的梯度节点可视化:
(可视化可参考我的这篇文章)
可以看到在这个损失函数的计算图里,我们要求的是顶端的两个参数W,b,整个损失函数是关于参数的函数。DivBackward0节点及以上部分是模型计算结果y_hat,节点以下部分流向损失函数的计算。
本例中我们依然可使用随机梯度下降法(SGD)作为Optimizer,届时在训练时每个batch数据计算完后梯度后,利用当前梯度迭代更新一次参数。
优化器除了需要传入的待学习参数(本例为W,b)外,还需要传入一些超参数。
# lr是学习率。作为超参数。
def sgd(params, lr, batch_size):
for param in params:
param.data -= lr * param.grad / batch_size
接下去就是重头戏的训练环节了,我们一般定义一个训练函数train()作为一套训练的过程的打包。
记得回顾上面那张计算图,只有待学习的参数W,b带梯度,是届时需要传入优化器更新的。
同时注意损失函数定义时一般返回的是一个batch_size长度的向量,对齐求sum()转换成标量以方便求导(见下方代码)。
l.backward()
针对这一批量的数据结果,反向传播,求出当前梯度W . d a t a = W . d a t a − l r ∗ W . g r a d / b a t c h s i z e W.data = W.data -lr * W.grad / batchsize W.data=W.data−lr∗W.grad/batchsize
b . d a t a = b . d a t a − l r ∗ b . g r a d / b a t c h s i z e b.data = b.data -lr * b.grad / batchsize b.data=b.data−lr∗b.grad/batchsize
以上完成一个批次,参数就迭代更新一次。
完成所有批次,一轮训练就完成了。
完成设定的训练轮次(num_epochs),训练结束。
下面附上代码:
num_epochs, lr = 5, 0.1
def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
params = None, lr = None, optimizer = None):
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n = 0.0, 0.0, 0 # 显示总体损失值、准确率用
for X, y in train_iter:
print(X.shape)
y_hat = net(X)
print(y_hat.shape)
l = loss(y_hat, y).sum()
# 梯度清零
if optimizer is not None:
# 在这个例子中,optimizer没传入,用默认的sgd,这里不会被执行
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_() # 参数的梯度数据清零
l.backward() # 小批量的损失对模型参数求梯度
if optimizer is None:
sgd(params, lr, batch_size)
# 传入优化器sgd对参数进行一次迭代更新
else:
optimizer.step()
# 在这个例子中,optimizer没传入,用默认的sgd,这里不会被执行
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item() # 计算训练准确率
n += y.shape[0]
print('epoch %d, loss %.4f, train acc %0.3f' % (epoch +1, train_l_sum / n, train_acc_sum / n))
可以定义一个函数评估测试集上的准确率,这里就不详述了。
具体直接参考下一节的完整版代码,并对train_ch3训练函数补充了测试集评估的内容。
代码是自己修订的个人笔记版,可配合我的博文食用。
不需要安装 d2l、d2lzh_pytorch库。
Github版:
3.6Softmax从零实现笔记.ipynb
直接运行版:
# %%
# 导包
import torch
import torchvision
import torchvision.transforms as transforms
import numpy as np
import sys
# %% [markdown]
# ### 获取和读取数据
# %%
mnist_train = torchvision.datasets.FashionMNIST(
root='~/Datasets/FashionMNIST', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(
root='~/Datasets/FashionMNIST', train=False, download=True, transform=transforms.ToTensor())
batch_size = 256
if sys.platform.startswith('win'):
num_workers = 0 # 0表示不用额外的进程来加速读取数据
else:
num_workers = 4
train_iter = torch.utils.data.DataLoader(
mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(
mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
# %% [markdown]
# ### 初始化模型参数
# 我们使用向量表示每个样本,已知每个样本输入是高和宽均为28像素的图像,向量长度:28*28=784
# 图像有10个类别,单层神经网络输出层的输出个数为10,so,softmax回归的权重w和偏差b参数的矩阵形状为:784×10和1×10(还是个线性模型)
# %%
num_inputs = 784
num_outputs = 10
W = torch.tensor(
np.random.normal(loc=0, scale=0.01, size=(num_inputs, num_outputs)),
dtype=torch.float)
b = torch.zeros(num_outputs , dtype=torch.float) # 这里直接定义为了 shape 为 10 的矩阵,利用后面的广播原则可扩展维度
print(W.shape, b.shape)
# %%
# 设上梯度
W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
# %% [markdown]
# ### 构建模型
# 实现softmax运算
# 首先描述一下,如何对多维Tensor按维度操作。
# 比如,给定一个矩阵X,可以对其中同一列(dim=0)或同一行(dim=1)的元素求和,并在结果中保留行和列这两个维度(keepdim=True)
# %% [markdown]
# #### 定义softmax运算
# 设矩阵X为一个批次的数据,行数是样本数,列数是特征数。
# 先对每个元素进行exp运算,再对运算好的矩阵进行同行元素求和,最后令矩阵每行各元素与该行元素之和相除。得到每行的概率分布。
# 即,softmax运算的输出矩阵中,任意一行元素代表了一个样本在各个输出类别上的预测概率。
# %%
def softmax(X):
X_exp = X.exp()
partition = X_exp.sum(dim=1, keepdim=True)
return X_exp / partition # 这里使用了广播机制
# %% [markdown]
# #### 定义模型
# 通过view函数将每张原始图像改成长度为num_inputs的向量。
# %%
def net(X):
return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)
# %% [markdown]
# #### 定义损失函数
# %%
# 定义交叉熵损失函数
def cross_entropy(y_hat, y):
return -torch.log(y_hat.gather(dim=1, index=y.view(-1,1)))
# y就是待传入的那个批量的label数据
# %% [markdown]
# ### 计算分类准确率
# 给定一个类别的概率分布y_hat,如果它与真实类别(索引矩阵)y一致,说明预测正确。
# 准确率:正确预测数量 / 总预测数量
# 我们定义accuracy函数。使用argmax()方法,y_hat.argmax(dim=1)返回y_hat每行中最大元素的索引,其与(索引矩阵)y形状相同。
# 在pytorch中,相等条件判断式 (y_hat.argmax(dim=1) == y) 是一个类型为 ByteTensor 的Tensor,里面元素为布尔变量,可用float()将其转换为值为0或1(相等为真) 的浮点型Tensor
# %%
# # 定义准确率函数
# y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
# y = torch.LongTensor([0, 2])
# def accuracy(y_hat, y):
# return (y_hat.argmax(dim=1) == y).float().mean().item()
# print(accuracy(y_hat, y))
# %% [markdown]
# #### 评价模型net在数据集 data_iter 上的准确率
# %%
# net即上面定义的模型。即每张转换成长向量后,赋予线性参数W,b,然后softmax,得到一个batch的y_hat
def evaluate_accuracy(data_iter, net):
acc_sum, n = 0.0, 0
for X, y in data_iter:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
# %%
# 因为随机初始化了参数 W,b ,模型net也初始化了,现在已经可以这个求未训练过的随机模型的准确率了。
# 随机模型的准确率应该与10分类的自然概率0.1相近
print(evaluate_accuracy(test_iter, net))
# %% [markdown]
# ### 训练模型
# **我们同样使用小批量随机梯度下降来优化模型的损失函数。**
# 训练模型时,迭代周期数 num_epochs 和学习率 lr 都是可调超参数。
# %%
num_epochs, lr = 5, 0.1
def sgd(params, lr, batch_size):
for param in params:
param.data -= lr * param.grad / batch_size
def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
params = None, lr = None, optimizer = None):
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
for X, y in train_iter:
# print(X.shape)
y_hat = net(X)
# print(y_hat.shape)
l = loss(y_hat, y).sum()
# 梯度清零
if optimizer is not None:
# 在这个例子中,optimizer没传入,所以用默认的sgd,这里不会被执行
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_() # 参数的梯度数据清零
l.backward() # 小批量的损失对模型参数求梯度
if optimizer is None:
sgd(params, lr, batch_size)
else:
optimizer.step()
# 在这个例子中,optimizer没传入,所以就用默认的sgd,这里不会被执行
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
n += y.shape[0]
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %0.3f, test acc %.3f' % (epoch +1, train_l_sum / n, train_acc_sum / n, test_acc))
# %%
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)
# %% [markdown]
# ### 评估模型
# %%
from matplotlib import pyplot as plt
def get_fashion_mnist_labels(labels):
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_fashion_mnist(images, labels):
_, figs = plt.subplots(1, len(images), figsize=(12, 12))
for f, img, lbl in zip(figs, images, labels):
f.imshow(img.view((28, 28)).numpy())
f.set_title(lbl)
f.axes.get_xaxis().set_visible(False)
f.axes.get_yaxis().set_visible(False)
plt.show()
X, y = iter(test_iter).next()
true_labels = get_fashion_mnist_labels(y.numpy())
pred_labels = get_fashion_mnist_labels(net(X).argmax(dim=1).numpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]
show_fashion_mnist(X[0:9], titles[0:9])