softmax回归的从零开始实现
- 实验前思考
- 获取和读取数据
- 获取数据集
- 查看数据集
- 查看下载后的.pt文件
- 查看mnist_train和mnist_test
- 读取数据集
- 查看数据迭代器内容
- 初始化模型参数
- 定义softmax函数
- 定义模型
- 定义损失函数
- 计算分类准确率
- 模型评价--准确率
- 开始训练
- 可视化
- 总结
- 完整代码
实验前思考
- 本文不使用框架解决多分类问题。
- 一定要先明白实验原理。可看博客深度学习pytorch–softmax回归(一)
- 一定要先弄清楚数据是怎么用Tensor表示的。
获取和读取数据
这里使用Fashion-MNIST为例,训练集60000张,测试集10000张,设置batch_size大小为256。
获取数据集
如果没有下载的话,将download设置为True。
mnist_train=torchvision.datasets.FashionMNIST('./Datasets/FashionMNIST',train=True,download=False,transform=transforms.ToTensor())
mnist_test=torchvision.datasets.FashionMNIST('./Datasets/FashionMNIST',train=False,download=False,transform=transforms.ToTensor())
查看数据集
查看下载后的.pt文件
要使用的是processed文件夹
下的training.pt
和test.pt
文件,包含了数据以及其标签,用元组的形式存储。
形如:
(tensor([],dtype=torch.unit8),tensor([]))
- 第一个tensor表示数据的数量,形状为torch.Size([60000,28,28]),60000表示.pt文件包含1000张图片,28为宽和高都是28个像素,每个像素用一个数字表示(也可以说是图像的特征)。
- 第二个tensor表示每张图片的真实标签(0-9)。形状为torch.Size([60000]),每个数字代表真实标签。
上次线性回归的数据集是自己手工构造的tensor,这次是图片的tensor表示。
查看mnist_train和mnist_test
查看经过torchvision.datasets.FashionMNIST处理后的mnist_train和mnist_test,
print(mnist_train) #查看mnist_train
print(type(mnist_train)) #查看它的类型
print(len(mnist_train), len(mnist_test))
feature, label = mnist_train[0] #访问数据集的任一个样本
print(feature.shape, label) # Channel x Height x Width
输出:
Dataset FashionMNISTNumber of datapoints: 60000Root location: ./Datasets/FashionMNISTSplit: TrainStandardTransformTransform: ToTensor()
<class 'torchvision.datasets.mnist.FashionMNIST'>
60000 10000
torch.Size([1, 28, 28]) 9
由上面代码可见,torchvision.datasets.FashionMNIST将原来存储在一个张量的训练数据分成了60000份,可迭代访问或下标访问mnist.train。
- feature为单张图片的tensor表示,形状为torch.Size([1, 28, 28]),其中1为通道数量.
- label为int类型,表示真实标签。
读取数据集
DataLoader将之前的数据做成一个批量、随机打乱且可以通过多进程加速读取的迭代器。
batch_size=256
num_workers=0 #多进程加速数据读取,0则不需要额外的进程
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)
查看数据迭代器内容
for X,y in train_iter:print(X.size())print(y.size())break
输出:
torch.Size([256, 1, 28, 28])
torch.Size([256])
一个X表示256个样本的特征(也就是softmax回归(一)中公式里的4个x,这里是28*28个x),一个y表示其每个样本的标签。
说明DataLoader只是将原来单个数据的张量通过增加一个维度合成了256个数据的张量。通俗地讲就是将256个1*28*28的张量放在一起,然后外面加一层[]。以后升维和降维要有这个思维。
关于该数据集的详细使用可看博客深度学习pytorch–MNIST数据集
初始化模型参数
由于像素(特征)有28*28=786个,所以输入层为一个786长度的向量。
由于为10分类任务,所以输出层的输出个数为10。
因此,根据softmax回归(一)的公式,可以推出 权重为参数为784X10的矩阵,偏置为1X10的矩阵。
上述分析的时候,脑子一定要有神经网络图和公式,就会觉得很明朗。根据公式还能够发现一点,不管是单样本还是批量样本,权重的形状是不发生改变的。
num_inputs=784 #28*28个像素(特征)
num_outputs=10 #分类的个数#权重初始化为均值为0,标准差为0.01的分布。
W=torch.tensor(np.random.normal(0,0.01,(num_inputs,num_outputs)),dtype=torch.float,requires_grad=True)
b=torch.zeros(num_outputs,dtype=torch.float,requires_grad=True) #偏置初始化为0
得到权重形状为torch.Size([786,10])的W和一个形状为torch.Size(1,10])的偏置b。
定义softmax函数
首先,想一下,根据公式,softmax函数的输入和输出是什么?
输入是网络输出层的结果,也就是每个样本10个输出。此处为批量处理,所以根据softmax回归(一)的公式,可以知道模型输出为一个256*10的矩阵。256为样本数量,10为10个分类的输出。
输出是根据softmax公式将其转化成概率的形式。
def softmax(X): #X为一个形状为256*10的二维张量X_exp=X.exp() #矩阵中每个元素取指数partition=X_exp.sum(dim=1,keepdim=True) #每行进行求和,并且保持维度不变,不懂的话可以看博客return X_exp/partition #实现softmax公式,用了广播机制
先随意定义一个X测试一下函数,下面的sum(dim=1)表示每一行相加,并且降低一个维度。如果dim=0则表示每一列相加。
X = torch.rand((2, 5))
X_prob = softmax(X)
print(X_prob, X_prob.sum(dim=1))
输出:
tensor([[0.2206, 0.1520, 0.1446, 0.2690, 0.2138],[0.1540, 0.2290, 0.1387, 0.2019, 0.2765]]) tensor([1., 1.])
从输出可以看到softmax函数定义的代码没有问题。
定义模型
定义模型之前想一想模型是什么,模型的输入是什么,输出是什么?
模型是一层神经网络+一层softmax函数,
模型的输入是迭代器的X,表示256个图片的像素(数据特征),形状为torch.Size([256, 1, 28, 28]),其中1表示通道数量
模型的输出是softmax函数的输出,形状为torch.Size([256, 10]),256表示batch_size(256)个样本,10表示每个样本对每个分类的概率分布情况。
def net(X):O=torch.mm(X.view(-1, num_inputs),W)+b #如果最后一个维度变了则,按以前的元素顺序来组合。return softmax(O)
由于迭代器中X的形状不对应神经网络输入层该有的形状,所以通过view把X从torch.Size([256, 1, 28, 28])变成torch.Size([256, 784])。
以前不理解为什么经常要像这样用view,现在思路很清晰。主要有2点原因:
- 1.脑子一定要有神经网络结构(此处只有一层786输入,10输出),可以在纸上画一画。
- 2.张量降维和升维的思维一定要正确,升维就是加括号,降维就是减少中括号,没有改变内容,不要被形状的数字给迷惑了。
定义损失函数
本实验采用交叉熵损失函数。
输入为预测结果y_hat
和真实标签y
。
其中y_hat
为torch.Size([256, 10])的张量,y
为torch.Size([256])的张量。
输出为运算结果。
def cross_entropy(y_hat,y): #y_hat为模型输出(所有样本对所有样本的softmax概率值),y为标签(真实值)return - torch.log(y_hat.gather(1, y.view(-1, 1)))
真正计算的时候还需要用.sum()将张量中的元素相加才是损失函数值。
对比之后用框架实现该实验后的loss值才发现在计算完log之后少乘了 预测标签概率值 。算啦,不重要,以后有机会再回来改叭。
这里用到了gather
函数,这个函数的作用就是找出y_hat中对应真实标签的概率,举例如下:
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = torch.LongTensor([0, 2])
y_hat.gather(1, y.view(-1, 1))
输出:
tensor([[0.1000],[0.5000]])
0.1就是将真实标签中的0作为索引,在y_hat中得到的,相当于y_hat[0][0]。
0.5就是将真实标签中的2作为索引,在y_hat中得到的,相当于y_hat[1][2]。
计算分类准确率
先计算预测正确的个数,也就是判断每个样本预测最大概率的标签是否和真实标签y相等就可以了。
再除以预测总数,就得到准确率。
下面的代码用的是y_hat处理后的张量和y张量中的每个元素比较,通过float()将true和false分别转换为1,0,再通过mean()求和平均
def accuracy(y_hat, y):return (y_hat.argmax(dim=1) == y).float().mean().item()
其中dim=1表示每行,dim=0表示每列。
模型评价–准确率
根据上面的思想,可以评价模型net
在测试集上的准确率。
def evaluate_accuracy(data_iter,net): #在所有样本上的准确率acc_sum,n=0.0,0for X,y in data_iter:acc_sum+=(net(X).argmax(dim=1) == y).float().sum().item()n+=y.shape[0] #获取总数量(此处每批256)return acc_sum / n
其中net(X)就是y_hat,因为都表示模型的输出,形状为torch.Size([256, 10])。
开始训练
训练步骤:
- 迭代数据
- 将数据导入模型得到输出
- 计算损失函数
- 反向传播计算梯度
- 使用优化算法更新参数
- 梯度清零
每个epoch过后将训练好的参数在测试集上计算一下准确率。
num_epochs=5
lr=0.1
for epoch in range(num_epochs):train_loss_sum=0.0train_acc_sum=0.0n=0 #用来计算数据总数for X,y in train_iter:y_hat=net(X) loss=cross_entropy(y_hat,y).sum() #计算损失函数loss.backward() #反向传播#参数更新sgd([W,b],lr,batch_size)#梯度清零W.grad.data.zero_()b.grad.data.zero_()train_loss_sum+=loss.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 {}, loss {:.4f}, train acc {:.3f}, test acc {:.3f}'.format(epoch + 1, train_loss_sum / n, train_acc_sum / n, test_acc))
可视化
对训练结果进行可视化。
下图第一行文本为真实标签。
第二行文本为模型预测结果。
第三行为图像输出(预先给定的图像)。
def use_svg_display():"""Use svg format to display plot in jupyter"""display.set_matplotlib_formats('svg')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):use_svg_display()# 这里的_表示我们忽略(不使用)的变量_, 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])
总结
- 所谓特征就是每个像素的的tensor表示。
- 提取特征就是通过一个权重w与特征x相乘,权重越大则说明该像素的特征提取得越多。
- 张量降维和升维的思维要正确,升维就是加括号,降维就是减少中括号,没有改变内容,不要被形状的数字给迷惑了。
- 写函数或者模型的时候,先想一想要实现什么功能,再想一想输入和输出是什么。
完整代码
#softmax回归的从零开始实现(MINST数据集)
import torch
import torchvision
import numpy as np
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
from IPython import display#获取数据集
mnist_train=torchvision.datasets.FashionMNIST('./Datasets/FashionMNIST',train=True,download=False,transform=transforms.ToTensor())
mnist_test=torchvision.datasets.FashionMNIST('./Datasets/FashionMNIST',train=False,download=False,transform=transforms.ToTensor())#读取数据集
batch_size=256
num_workers=0 #多进程加速数据读取,0则不使用多进程
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)#初始化模型参数
num_inputs=784 #28*28个像素(特征)
num_outputs=10 #分类的个数W=torch.tensor(np.random.normal(0,0.01,(num_inputs,num_outputs)),dtype=torch.float,requires_grad=True)
b=torch.zeros(num_outputs,dtype=torch.float,requires_grad=True)#定义模型
def softmax(X):X_exp=X.exp() #矩阵中每个元素取指数partition=X_exp.sum(dim=1,keepdim=True) #每行进行求和,并且保持维度不变,不懂的话可以看博客return X_exp/partition #实现softmax公式,用了广播机制def net(X):O=torch.mm(X.view(-1, num_inputs),W)+breturn softmax(O)#定义损失函数
def cross_entropy(y_hat,y): #y_hat为模型输出(所有样本对所有样本的softmax概率值),y为标签(真实值)return - torch.log(y_hat.gather(1, y.view(-1, 1)))#模型评价
def evaluate_accuracy(data_iter,net): #在所有样本上的准确率acc_sum,n=0.0,0for X,y in data_iter:acc_sum+=(net(X).argmax(dim=1) == y).float().sum().item()n+=y.shape[0] #获取总数量(此处每批256)return acc_sum / n#定义优化算法
def sgd(params,lr,batch_size):for param in params:param.data-=lr*param.grad/batch_size #除以batch_size之后计算批量loss的时候就不用求平均了,只需要sum()
#开始训练
num_epochs=5
lr=0.1
for epoch in range(num_epochs):train_loss_sum=0.0train_acc_sum=0.0n=0 #用来计算数据总数for X,y in train_iter:y_hat=net(X) loss=cross_entropy(y_hat,y).sum() #计算损失函数loss.backward() #反向传播#参数更新sgd([W,b],lr,batch_size)#梯度清零W.grad.data.zero_()b.grad.data.zero_()train_loss_sum+=loss.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 {}, loss {:.4f}, train acc {:.3f}, test acc {:.3f}'.format(epoch + 1, train_loss_sum / n, train_acc_sum / n, test_acc)) #可视化
def use_svg_display():"""Use svg format to display plot in jupyter"""display.set_matplotlib_formats('svg')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):use_svg_display()# 这里的_表示我们忽略(不使用)的变量_, 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])