前言
本文记录在学习pytorch的一些心得
手写数字识别的数据来自pytorch的自带数据集中 api是 torchvision.datasets.MNIST(root='/files/', train=True, download=True, transform=)
参数有
root
参数表示数据存放的位置
train:
bool类型,表示是使用训练集的数据还是测试集的数据
download:
bool类型,表示是否需要下载数据到root目录
transform:
实现的对图片的处理函数
而MNIST是由Yann LeCun
等人提供的免费的图像识别的数据集,其中包括60000个训练样本和10000个测试样本,其中图拍了的尺寸已经进行的标准化的处理,都是黑白的图像,大小为28X28
数据加载知识的了解
当我们获取了这个数据集时,我们还需要了解一下torch的数据加载是个什么流程
在torch中提供了数据集的基类torch.utils.data.Dataset
,继承这个基类,我们能够进行数据的快速加载
源码如下:
class Dataset (object ): """An abstract class representing a Dataset. All other datasets should subclass it. All subclasses should override ``__len__``, that provides the size of the dataset, and ``__getitem__``, supporting integer indexing in range from 0 to len(self) exclusive. """ def __getitem__ (self, index ): raise NotImplementedError def __len__ (self ): raise NotImplementedError def __add__ (self, other ): return ConcatDataset([self, other])
可知:我们需要在自定义的数据集类中继承Dataset类,同时还需要实现两个方法:
__len__
方法,能够实现通过全局的len()
方法获取其中的元素个数
__getitem__
方法,能够通过传入索引的方式获取数据,例如通过dataset[i]
获取其中的第i
条数据
这里可以写个例子
数据加载案例
读取SMS Spam Collection
数据来源:http://archive.ics.uci.edu/ml/datasets/SMS+Spam+Collection
数据介绍:SMS Spam Collection是用于骚扰短信识别的经典数据集,完全来自真实短信内容,包括4827条正常短信和747条骚扰短信,总数据条数 5574。
正常短信和骚扰短信保存在一个文本文件中。 每行完整记录一条短信内容,每行开头通过ham和spam标识正常短信和骚扰短信
数据实例:
实现读取代码如下:
from torch.utils.data import Dataset, DataLoaderimport pandas as pddata_path = './data/SMSSpamCollection' class my_data_set (Dataset ): def __init__ (self ): self.lines = open (data_path, encoding='utf-8' ).readlines() def __getitem__ (self, index ): line_cut = self.lines[index].strip() lable = line_cut[:4 ].strip() content = line_cut[4 :].strip() return lable, content def __len__ (self ): return len (self.lines) if __name__ == '__main__' : data = my_data_set() print(len (data))
但是这样的数据加载在很多时候不能满足我们的需要
我们有时候需要打乱数据,同时对于数据进行分块
这个时候我们就需要torch.utils.data.DataLoader这个api了
使用api过程如下
dataload = DataLoader(dataset=data, batch_size=10, shuffle=True, num_workers=2, drop_last=True)
dataset:提前定义的dataset的实例
batch_size:传入数据的batch的大小,常用128,256等等
shuffle:bool类型,表示是否在每次获取数据的时候提前打乱数据
num_workers
:加载数据的线程数(一般可以写作0,当开启时可能报错,报错内容可以百度解决也可以参考这个博客 https://www.cnblogs.com/20183544-wangzhengshuai/p/14814459.html )
drop_last:丢弃最后一个不足batch_size的数据
当我们数据处理完成后,我们可以进行打印观察
for index, (lable, content) in enumerate (dataload): print(index, lable, content) print("*" * 100 )
运行结果如图
我们把10个数据当做一块输入,所以我们输出数据也就是10个ham或spam拼成的一条数据
同时我们设置了drop_last这样我们重新加载出的数据条数只有556条
因为总数据是5574,最后4个数据无法满足batch_size的大小,就被舍弃了
这样我们的数据就加载完成了
这也是为我们的手写数字识别案例的数据加载做了前置知识准备
手写数字识别案例
我们可以先加载一下数字识别案例的数据集看看是什么样子的
from torchvision.datasets import MNISTdata=MNIST(root='data' ,train=True ,download=False ) print(data[0 ]) img=data[0 ][0 ] img.show()
运行会发现打开了一张图片,显示为5
之前我们在写MNIST的api的时候写到了transforms参数,这里我们需要知道transforms的图形处理方法
一般用到的有这3个api:
ToTensor:作用把一个取值范围是[0,255]
的PIL.Image
或者shape
为(H,W,C)
的numpy.ndarray
,转换成形状为[C,H,W]
,其中(H,W,C)
意思为(高,宽,通道数)
,黑白图片的通道数只有1,其中每个像素点的取值为[0,255],彩色图片的通道数为(R,G,B),每个通道的每个像素点的取值为[0,255],三个通道的颜色相互叠加,形成了各种颜色
Normalize:作用把数据标准化
Compose:作用将多个transform
组合起来使用。
下面分别举例子来使用上面这些api
为了模拟和数据集相似的数据类型,所以在这个例子中数据通道也为1
import numpy as npfrom torchvision import transformsdata=np.random.randint(0 ,255 ,size=6 ) img=data.reshape(1 ,2 ,3 ) print(img) print("------------" ) img=transforms.ToTensor()(img) print(img) for x,y in enumerate (img): y=y.float () y_mean=y.mean().item() y_std= y.std().item() nor_img=transforms.Normalize((y_mean),(y_std))(img.float ()) print(nor_img)
结果为:
原数据 [[[172 47 117] [192 67 251]]] ------------ 变化过的 tensor([[[172, 192]], [[ 47, 67]], [[117, 251]]], dtype=torch.int32) 标准化后的 tensor([[[-0.1266, 0.0844]], [[-1.4459, -1.2348]], [[-0.7071, 0.7071]]])
这样我们差不多就对于数据的处理就有了前置的知识准备
准备数据
def data_load (train=True ,BATCH_SIZE=BATCH_SIZE ): dataset = MNIST(root='./data' , train=train, transform=torchvision.transforms.Compose( [torchvision.transforms.ToTensor(), torchvision.transforms.Normalize(mean=(0.1307 ,), std=(0.3081 ,)) ] )) data_loader = DataLoader(dataset=dataset, batch_size=BATCH_SIZE, shuffle=True ) return data_loader
这里的0.1307和0.3081都是之前算出来的,计算结果大概如下:
import torchvision.transformsfrom torchvision.datasets import MNISTfrom torch.utils.data import Dataset, DataLoadertransforms = torchvision.transforms.Compose([torchvision.transforms.ToTensor()]) train_data = MNIST('data' , train=True , download=False , transform=transforms) train_loader = DataLoader(dataset=train_data, shuffle=True , batch_size=60000 ) for batch_idx, data in enumerate (train_loader): inputs, targets = data x = inputs.view(-1 , 28 * 28 ) x_std = x.std().item() x_mean = x.mean().item() print(x_std) print(x_mean)
结果为:
0.30810782313346863 0.13066047430038452
构建模型
模型中使用全连接层:
当前一层的神经元和前一层的神经元相互链接,其核心操作就是y = w x y = wx y = w x ,即矩阵的乘法,实现对前一层的数据的变换
模型的构建使用了一个三层的神经网络,其中包括两个全连接层和一个输出层,第一个全连接层会经过激活函数的处理,将处理后的结果交给下一个全连接层,进行变换后输出结果
这里补充一下激活函数的api
Relu激活函数由import torch.nn.functional as F
提供,F.relu(x)
即可对x进行处理
模型中数据形状
原数据形状在经过数据加载处理后是:[batch_size,1,28,28]
进行形状的修改:[batch_size,28*28]
,(全连接层是在进行矩阵的乘法操作,所以我们要转换数据形状)
第一个全连接层的输出形状:`[batch_size,28]
激活函数不会修改数据的形状
第二个全连接层的输出形状:[batch_size,10]
,因为手写数字有10个类别
构建模型代码
class mnist_module (nn.Module ): def __init__ (self ): super (mnist_module, self).__init__() self.fc1 = nn.Linear(28 * 28 * 1 , 28 ) self.fc2 = nn.Linear(28 , 10 ) def forward (self, input ): X = input .view(-1 , 1 * 28 * 28 ) X = self.fc1(X) X = F.relu(X) out = self.fc2(X) return out
损失函数
当前我们手写字体识别的问题是一个多分类的问题,所谓多分类对比的是之前学习的2分类
我们在逻辑回归中,我们使用sigmoid进行计算对数似然损失,来定义我们的2分类的损失。
在2分类中我们有正类和负类,正类的概率为P ( x ) = 1 1 + e − x = e x 1 + e x P(x) = \frac{1}{1+e^{-x}} = \frac{e^x}{1+e^x} P ( x ) = 1 + e − x 1 = 1 + e x e x ,那么负类的概率为1 − P ( x ) 1-P(x) 1 − P ( x )
将这个结果进行计算对数似然损失− ∑ y l o g ( P ( x ) ) -\sum y log(P(x)) − ∑ y l o g ( P ( x ) ) 就可以得到最终的损失
而在多分类的过程中我们不能够再使用sigmoid函数来计算当前样本属于某个类别的概率,而应该使用softmax函数。softmax和sigmoid的区别在于我们需要去计算样本属于每个类别的概率,需要计算多次,而sigmoid只需要计算一次
softmax的公式如下:
σ ( z ) j = e z j ∑ k = 1 K e z K , j = 1 ⋯ k \sigma(z)_j = \frac{e^{z_j}}{\sum^K_{k=1}e^{z_K}} ,j=1 \cdots k
σ ( z ) j = ∑ k = 1 K e z K e z j , j = 1 ⋯ k
对于这个softmax输出的结果,是在[0,1]区间,我们可以把它当做概率
和前面2分类的损失一样,多分类的损失只需要再把这个结果进行对数似然损失的计算即可
J = − ∑ Y l o g ( P ) , 其中 P = e z j ∑ k = 1 K e z K , Y 表示真实值 \begin{aligned}
& J = -\sum Y log(P) &, 其中 P = \frac{e^{z_j}}{\sum^K_{k=1}e^{z_K}} ,Y表示真实值
\end{aligned}
J = − ∑ Y l o g ( P ) , 其 中 P = ∑ k = 1 K e z K e z j , Y 表 示 真 实 值
最后,会计算每个样本的损失,即上式的平均值
我们把softmax概率传入对数似然损失得到的损失函数称为交叉熵损失
在pytorch中有两种方法实现交叉熵损失
criterion = nn.CrossEntropyLoss() loss = criterion(input ,target)
output = F.log_softmax(x,dim=-1 ) loss = F.nll_loss(output,target)
带权损失定义为:l n = − ∑ w i x i l_n = -\sum w_{i} x_{i} l n = − ∑ w i x i ,其实就是把l o g ( P ) log(P) l o g ( P ) 作为x i x_i x i ,把真实值Y作为权重
训练模型
实例化模型,设置模型为训练模式
实例化优化器类,实例化损失函数
获取,遍历dataloader
梯度置为0
进行向前计算(反向传播)
计算损失
反向传播
更新参数
model = mnist_module() optim = Adam(model.parameters(), lr=lr) criterion = nn.CrossEntropyLoss() def train (epoch ): dataload = data_load(BATCH_SIZE=BATCH_SIZE) for idx, (input , target) in enumerate (dataload): optim.zero_grad() output = model(input ) loss=criterion(output,target) loss.backward() optim.step() if idx % 10 == 0 : print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}' .format ( epoch, idx * len (input ), len (dataload.dataset), 100 * idx / len (dataload), loss.item()))
模型保存和加载
当我们模型训练成功肯定是要保存模型数据的
保存模型api如下:
torch.save(model.state_dict(), "./model/model.pk1" ) torch.save(optim.state_dict(), "./model/optimizer.pk1" )
加载模型api如下:
model.load_state_dict(torch.load("./model/model.pk1" )) optim.load_state_dict(torch.load("./model/optimizer.pk1" ))
模型评估
评估的过程和训练的过程相似,但是:
不需要计算梯度
需要收集损失和准确率,用来计算平均损失和平均准确率
损失的计算和训练时候损失的计算方法相同
准确率的计算:
模型的输出为[batch_size,10]的形状
其中最大值的位置就是其预测的目标值(预测值进行过sotfmax后为概率,sotfmax中分母都是相同的,分子越大,概率越大)
最大值的位置获取的方法可以使用torch.max
,返回最大值和最大值的位置
返回最大值的位置后,和真实值([batch_size]
)进行对比,相同表示预测成功
代码如下:
def test (): test_dataload = data_load(train=False ) loss_dict = [] acc_dict = [] for idx, (input , target) in enumerate (test_dataload): with torch.no_grad(): output = model(input ) cur_loss = criterion(output, target) loss_dict.append(cur_loss) predict = output.max (dim=-1 )[-1 ] cur_acc = predict.eq(target).float ().mean() acc_dict.append(cur_acc) print("平均准确率{},平均损失{}" .format (np.mean(acc_dict), np.mean(loss_dict)))
完整流程
完整代码如下:
import numpy as npimport torchimport osimport torchvision.transformsfrom torchvision.datasets import MNISTfrom torch.utils.data import DataLoaderfrom torch import nnimport torch.nn.functional as Ffrom torch.optim import AdamBATCH_SIZE = 100 TEST_BATCH_SIZE = 1000 lr = 0.001 def data_load (train=True , BATCH_SIZE=BATCH_SIZE ): dataset = MNIST(root='./data' , train=train, transform=torchvision.transforms.Compose( [torchvision.transforms.ToTensor(), torchvision.transforms.Normalize(mean=(0.1307 ,), std=(0.3081 ,)) ] )) data_loader = DataLoader(dataset=dataset, batch_size=BATCH_SIZE, shuffle=True ) return data_loader class mnist_module (nn.Module ): def __init__ (self ): super (mnist_module, self).__init__() self.fc1 = nn.Linear(28 * 28 * 1 , 28 ) self.fc2 = nn.Linear(28 , 10 ) def forward (self, input ): X = input .view(-1 , 1 * 28 * 28 ) X = self.fc1(X) X = F.relu(X) out = self.fc2(X) return out model = mnist_module() optim = Adam(model.parameters(), lr=lr) criterion = nn.CrossEntropyLoss() if os.path.exists("./model/model.pk1" ): model.load_state_dict(torch.load("./model/model.pk1" )) optim.load_state_dict(torch.load("./model/optimizer.pk1" )) def train (epoch ): dataload = data_load(BATCH_SIZE=BATCH_SIZE) for idx, (input , target) in enumerate (dataload): optim.zero_grad() output = model(input ) loss = criterion(output, target) loss.backward() optim.step() if idx % 10 == 0 : print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}' .format ( epoch, idx * len (input ), len (dataload.dataset), 100 * idx / len (dataload), loss.item())) if idx % 100 == 0 : torch.save(model.state_dict(), "./model/model.pk1" ) torch.save(optim.state_dict(), "./model/optimizer.pk1" ) def test (): test_dataload = data_load(train=False ) loss_dict = [] acc_dict = [] for idx, (input , target) in enumerate (test_dataload): with torch.no_grad(): output = model(input ) cur_loss = criterion(output, target) loss_dict.append(cur_loss) predict = output.max (dim=-1 )[-1 ] cur_acc = predict.eq(target).float ().mean() acc_dict.append(cur_acc) print("平均准确率{},平均损失{}" .format (np.mean(acc_dict), np.mean(loss_dict))) if __name__ == '__main__' : test() for i in range (3 ): train(i) test()
运行结果部分截图