from pathlib import Path
import requests
DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist" # 斜杠 / 操作符用于拼接路径,比如创建子路径
# Path.mkdir 创建目录
# mkdir(parents=True, exist_ok=True)
# parents:如果父目录不存在,是否创建父目录。
# exist_ok:只有在目录不存在时创建目录,目录已存在时不会抛出异常
PATH.mkdir(parents=True, exist_ok=True)
URL = "http://deeplearning.net/data/mnist/" # 下载 mnist 数据集的地址
Filename = "mnist.pkl.gz"
if not (PATH / Filename).exists(): # 如果"mnist.pkl.gz"文件不存在
req = requests.get(URL + Filename) # 请求下载数据集地址的Filename文件
# 把Reponse对象的内容以二进制数据的形式返回
# .content 返回的是可以自由加工的(以你想要的编码格式来进行编码)的字节串,
# 是只高于二进制数据的一种数据存储单位。
pic = req.content
# (PATH / Filename) :是你下载的好的数据,存储在PATH(date/mnist路径下的文件1=="mnist.pkl.gz")
(PATH / Filename).open("wb").write(pic)
# 文件1 "mnist.pkl.gz",打开
# Path.open("wb") w表示写入
# w:open for writing写入, truncating the file first【截断】
# b:binary mode 二进制方式
# write(pic):写入pic
import pickle
import gzip
# from pathlib import Path
# 路径分隔符表示函数: Path.as_posix()
# 作用:将 Windows目录中的路径分隔符 ‘\’ 改为 Unix 样式 ‘/’。
# 将所有连续的正斜杠、反斜杠,统一修改为单个正斜杠
with gzip.open((PATH / Filename).as_posix(), "rb") as f:
# 该 pickle 模块实现了用于序列化和反序列化 Python 对象结构的二进制协议
# pickle.load 反序列化
((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")
# x_train.shape (50000, 784)
# y_train.shape (50000,)
# x_valid.shape (10000, 784)
# y_valid.shape (10000, )
from matplotlib import pyplot as plt
import numpy as np
# .reshape 将其形成一个28*28数组
# x_train[0].shape = (784, ) 784是mnist数据集每个样本的像素点个数
plt.imshow(x_train[0].reshape((28, 28)), cmap="gray")
plt.show()
print(x_train.shape)
import torch
# 注意数据需转换成tensor才能参与后续建模训练
# pytorch 的 map()函数接收两个参数,一个是函数,一个是Iterable,【可以进行数据格式转换】
# map将传入的函数【数据转换成torch.tensor】依次作用到序列的每个元素,并把结果作为新的Iterator返回
x_train, y_train, x_valid, y_valid = map(
torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape # n=50000 c= 784
# x_train.shape == torch.Size([50000, 784])
x_train, x_train.shape, y_train.min(), y_train.max()
# y_train.min()==tensor(0) y_train.max()==tensor(9)
# print(x_train, y_train)
# print(x_train.shape)
# print(y_train.min(), y_train.max())
# 那什么时候使用nn.Module,什么时候使用nn.functional呢?
# 一般情况下,如果模型有可学习的参数,最好用nn.Module,其他情况nn.functional相对更简单一些
import torch.nn.functional as F
# 包含 torch.nn 库中所有函数
# 同时包含大量 loss 和 activation function
loss_func = F.cross_entropy
def model(xb):
return xb.mm(weights) + bias
# weights.shape [784, 10] 分成10类,总共784个像素点【特征点】
bs = 64 # batch_size
# xb.shape = torch.Size([64, 784])
xb = x_train[0:bs] # a mini-batch from x
yb = y_train[0:bs]
# 权重随机初始化 weights 需要对权重更新的,追踪该参数, 要自动求梯度的
# dtype = torch.float:返回张量所需的数据类型
# requires_grad=True autograd是否应该记录对返回张量的操作(说明当前量是否需要在计算中保留对应的梯度信息)
# 返回一个符合均值为0,方差为1的正态分布(标准正态分布)中填充随机数的张量
weights = torch.randn([784, 10], dtype = torch.float, requires_grad = True)
bs = 64
# torch.zeros 张量全部赋值0,且 权重需要更新的,追踪该参数,要自动求梯度的
bias = torch.zeros(10, requires_grad=True)
print(loss_func(model(xb), yb))
# 无需写反向传播函数,nn.Module能够利用autograd自动实现反向传播
# torch.nn 实现一个简单的线性回归模型,
# 主要是根据 nn.Module 和 torch.nn 中的 Linear 模型, 来创建一个自己的线性模型.
from torch import nn
# 可以通过继承 nn.Module(它本身是一个类并且能够跟踪状态)建立神经网络。
# 我们想要建立一个包含权重、偏置和前向传播的方法的类
class Mnist_NN(nn.Module):
# 创建Mnist_NN类 子类:Mnist_NN 父类:nn.Module
def __init__(self):
# 初始化属性hidden1、hidden2、out
super(Mnist_NN,self).__init__()
# super().__init__() 可行
# 这里__init__()函数里面的值要写,不然就被父类的默认值覆盖了!
self.hidden1 = nn.Linear(784, 128) # 两层全连接神经网络
self.hidden2 = nn.Linear(128, 256)
self.out = nn.Linear(256, 10)
# class 子类(父类):
# def 子类的方法(self, 参数):
# 父类.父类的方法(self, 参数)
def forward(self, x):
x = F.relu(self.hidden1(x)) # 前向传播的激活函数relu
x = F.relu(self.hidden2(x))
x = self.out(x)
return x # 传出x
net = Mnist_NN()
print(net)
'''
Mnist_NN(
(hidden1): Linear(in_features=784, out_features=128, bias=True)
(hidden2): Linear(in_features=128, out_features=256, bias=True)
(out): Linear(in_features=256, out_features=10, bias=True)
)
'''
# net.named_parameters() 可以查看神经网络的参数信息,用于更新参数,或者用于模型的保存
# list(net.named_parameters()) 查看神经网络的参数信息
# 'hidden1.weight' 'hidden1.bias'
# 'hidden2.weight' 'hidden2.bias'
# 'out.weight' 'out.bias' 以上6组
# name, parameter 名字 参数tensor值
# 'hidden1.weight' 以及对应的tensor值
for name, parameter in net.named_parameters():
print(name, parameter,parameter.size())
# 使用TensorDataset和DataLoader来简化
# torch.utils.data.DataLoader(): 构建可迭代的数据装载器, 我们在训练的时候,每一个for循环,
# 每一次iteration,就是从DataLoader中获取一个batch_size大小的数据的
from torch.utils.data import TensorDataset # 数据预处理
from torch.utils.data import DataLoader # DataLoader :(构建可迭代的数据装载器)
train_ds = TensorDataset(x_train, y_train) # 把数据放在数据库中
# 从dataset数据库中每次抽出batch_size = bs = 64个数据
train_dl = DataLoader(train_ds, # 决定数据从哪读取以及如何读取
batch_size=bs,
shuffle=True # 每个epoch 将数据打乱
)
valid_ds = TensorDataset(x_valid, y_valid)
valid_dl = DataLoader(valid_ds, batch_size=bs * 2) # 为什么2倍的batch
def get_data(train_ds, valid_ds, bs):
return (
DataLoader(train_ds, batch_size=bs, shuffle=True),
DataLoader(valid_ds, batch_size=bs * 2),
)
from torch import optim
def get_model():
model = Mnist_NN()
return model, optim.SGD(model.parameters(), lr=0.001)
# optim.SGD “随机梯度下降”,但是本质上还是还是实现的批量梯度下降,即用全部样本梯度的均值更新可学习参数
'''
- 一般在训练模型时加上model.train(),这样会正常使用Batch Normalization和 Dropout
- 测试的时候一般选择model.eval(),这样就不会使用Batch Normalization和 Dropout '''
import numpy as np
# 定义更新权值的训练函数
# 把整个训练循环封装成fit()函数,以便后续再次调用它
def fit(steps, model, loss_func, opt, train_dl, valid_dl):
for step in range(steps): # 开始step=0
# 训练开始之前写上 model.train() ,在测试时写上 model.eval()
# 训练过程中会在程序上方添加一句model.train(),作用是:启用 batch normalization 和 dropout
model.train()
# xb.shape = torch.Size([64, 784]) yb.shape =torch.Size([64])
for xb, yb in train_dl: # 对当前batch进行损失率及参数更新
loss_batch(model, loss_func, xb, yb, opt) # 计算损失率以及进行参数更新
# 一个step【或epoch】,就是把所有的训练或验证的数据用一遍
# 加入验证集
model.eval() # 评估模型
# 使用with torch.no_grad():
# 表明当前计算不需要反向传播,所有计算得出的tensor的requires_grad都自动设置为False
with torch.no_grad():
# len(muns)=79 用*号操作符,可以将list unzip(解压)
# nums =(128,128,...,128,16) 总共10000个验证集 10000=128*78+16
# losses= (2.279... , ...... , 2.275...)
losses, nums = zip(
*[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl]
)
# np.sum() 求和
# 矩阵点乘——对应元素相乘:np.sum(np.multiply(a,b))=a*b = 128*2.279..+...+16*2.275..
# np.sum(nums)= 10000
val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)
print('当前step:'+str(step), '验证集损失:'+str(val_loss))
# loss_func = F.cross_entropy
# steps=要迭代多少次
# model模型
# loss_func损失函数
# opt优化器
# train_dl训练数据
# valid_dl验证数据
def loss_batch(model, loss_func, xb, yb, opt=None):
loss = loss_func(model(xb), yb)
# 因为训练的过程通常使用mini-batch方法,所以如果不将梯度清零的话,
# 梯度会与上一个batch的数据相关,因此该函数要写在反向传播和梯度下降之前。
# 每次迭代之后,需要将梯度还原为零,否则loss.backward() 将梯度增加到已经存在的值上,而不是替代它
if opt is not None:
# compute gradient and do SGD step
loss.backward() # 反向传播计算得到每个参数的梯度值(loss.backward())
opt.step() # 通过梯度下降执行一步参数更新(optimizer.step())
opt.zero_grad() # 将梯度归零(optimizer.zero_grad())
return loss.item(), len(xb) # item():取出张量具体位置的元素元素值 len(xb)= 64
# 获取训练的x,y数据的时候,一次抓取batch=64 和 获取验证的x,y 的数据数据的时候,一次抓取batch=128
# valid_dl.batch_size= 2*bs = 128
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
model, opt = get_model()
fit(30, model, loss_func, opt, train_dl, valid_dl)