动手写一个深度学习框架(1) 用 pytorch 搭建神经网络

1,734 阅读5分钟

小目标

从今天开始,尝试用 numpy 实现一个深度神经网络的框架,参考 pytorch 实现方式来进行实现,目的对神经网络有一个更深一层的认识。

objective.png

参考资料

  • George Hotz 分享关于 tinygrad 分享
  • pytorch 官方文档

reference.jpeg

基本要求

  • 了解深度学习一般知识
  • 熟悉 python 编程语言
  • 精通 numpy、matplotlab 等 python 主流库
  • 熟悉 pytorch

requirement.jpeg

准备工作

构建深度学习框架暂时来看,准备基于 numpy 来进行搭建,numpy 提供很好关于矩阵操作与运算,可以节省了很多制作过于基础轮子的时间。深度学习框架 API 设计会借鉴 torch 这个面向对象模块化的深度学习框架,将 torch 作为老师,一步一步对比和模仿来进行搭建。

%pylab inline
import numpy as np
from tqdm import trange
np.set_printoptions(suppress=True)
import torch
import torch.nn as nn
# torch.set_printoptions(precision=2)
torch.set_printoptions(sci_mode=False)

准备数据集

先用 pytorch 来实现一个简单的神经网络,用神经网络来去识别手写数字,输入是一个数字图像展平的向量,输出是一个数值,表示数字。

在开始之前先准备一个合适数据集,这次选用的是经典的入门级数据集— MNIST 数据集,这个数据集中 有 60k 张训练样本和 10k 张测试样本。有了数据集之后就先 pytorch 定义 2 层神经网络,并训练,这样网络来识别 MNIST 数据集。然后尝试实现基于 numpy 的实现一个类的神经网络,包括前向传播和反向传播。

def fetch(url):
  import requests, gzip, os, hashlib, numpy
  fp = os.path.join("/tmp", hashlib.md5(url.encode('utf-8')).hexdigest())
  if os.path.isfile(fp):
    with open(fp, "rb") as f:
      dat = f.read()
  else:
    with open(fp, "wb") as f:
      dat = requests.get(url).content
      f.write(dat)
  return numpy.frombuffer(gzip.decompress(dat), dtype=np.uint8).copy()
X_train = fetch("http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz")[0x10:].reshape((-1, 28, 28))
Y_train = fetch("http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz")[8:]
X_test = fetch("http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz")[0x10:].reshape((-1, 28, 28))
Y_test = fetch("http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz")[8:]

minist_dataset.png

X_train.shape #(60000, 28, 28)

可以看到在训练数据集一共有 60k 张图片,每张图像大小为 28 x 28,查看一个下数据样本图像效果

imshow(X_train[0],cmap='gray')

001.png

定义模型

写一个简单 2 层的神经网络,通常输入层是不会计入神经网络的层数中。输入向量维度 784 向量是将图像 28 x 28 展平为 784 输入,输入样本形状(m,784) ,这里 m 表示一批次样本的数量。第一层神经网络参数(784x128) 经过第一层后形状(m,128)。这里激活函数选择 ReLU 这个激活函数。第二层神经网络参数(128,10) 输出形状(m,10) 10 对应于 10 个类别。

neural_network.png

class ANet(torch.nn.Module):
    def __init__(self):
        super(ANet,self).__init__()
        self.l1 = nn.Linear(784,128)
        self.act = nn.ReLU()
        self.l2 = nn.Linear(128,10)
    def forward(self,x):
        x = self.l1(x)
        x = self.act(x)
        x = self.l2(x)
        return x
model = ANet()

需要输入数据格式和类型需要满足模型要求

  • 最后两个维度进行展平输入维度(m,784)
  • 类型为 pytorch 提供的 tensor 类型,数值类型为浮点类型的数据
model(torch.tensor(X_train[0:10].reshape((-1,28*28))).float())

epochs = 10
tbar = trange(epochs)
for i in tbar:
    tbar.set_description(f"iterate {i}\n")

每次迭代随机从数据集中抽取一定数量的样本,这里使用 np.random,.randint随机在指定区间内生成 size 个整数。

epochs = 10
batch_size = 32
tbar = trange(epochs)
for i in tbar:
    samp = np.random.randint(0,X_train.shape[0],size=(batch_size))
    print(samp)

训练

training.jpeg

开始训练,我们需要定义 epochs 也就是迭代次数,而不是 epoch,名字起的有点容易产生歧义,epoch 是将数据集所有数据都参与到训练一次,每次迭代样本数量用 batch_size 来定义也就是定义.

epochs = 10
batch_size = 32
tbar = trange(epochs)
# 定义损失函数,损失函数使用交叉熵损失函数
loss_fn = nn.CrossEntropyLoss()
# 定义优化器
optim = torch.optim.Adam(model.parameters())
for i in (t:=trange(epochs)):
    #对数据集中每次随机抽取批量数据用于训练
    samp = np.random.randint(0,X_train.shape[0],size=(batch_size))
    X = torch.tensor(X_train[samp].reshape((-1,28*28))).float()
    Y = torch.tensor(Y_train[samp]).long()
    # 将梯度初始化
    optim.zero_grad()
    
    # 模型输出
    out = model(X)
    #计算损失值
    loss = loss_fn(out,Y)
    # 计算梯度
    loss.backward()
    # 更新梯度
    optim.step()
    t.set_description(f"loss {loss.item():0.2f}")

定义损失函数

数据经过神经网络输出为 10 维数据,这里 10 就是分类数量,也可以 C 来表示分类数量,那么就是 C 维,也就是输出为 (m,C) 数据,m 表示样本数量,C 表示每一个样本会对每一个类别输出一个值,表示属于某一个类别可能性。所以需要对这些数字进行一个标准化,也就是让这些输出为一个概率分布,概率值大表示属于某一个类别可能性大。通常用 softmax

exp(xi)j=1nexp(xj)\frac{\exp(x_i)}{\sum_{j=1}^n \exp(x_j)}

损失函数采用多分类 nn.CrossEntropyLoss() ,其实 CrossEntropyLoss 包括将输出进行 softmax 将输出标准化为概率,然后在用负对数似然来计算两个概率之间距离,yilog(si)-y_i \log(s_i) 这是 yiy_i 正确标签

l=(x,y)=L{l1,,lN}Tl=(x,y) = L\{l_1,\cdots,l_N\}^T
ln=wynexp(xn,yn)c=1Cexp(xn,c)l_n = -w_{y_n} \frac{\exp(x_{n,y_n})}{\sum_{c=1}^C \exp(x_{n,c})}
epochs = 10
batch_size = 32
tbar = trange(epochs)
# 定义损失函数,损失函数使用交叉熵损失函数
loss_fn = nn.CrossEntropyLoss()
# 定义优化器
optim = torch.optim.Adam(model.parameters())

losses,accs = [],[]

for i in (t:=trange(epochs)):
    #对数据集中每次随机抽取批量数据用于训练
    samp = np.random.randint(0,X_train.shape[0],size=(batch_size))
    X = torch.tensor(X_train[samp].reshape((-1,28*28))).float()
    Y = torch.tensor(Y_train[samp]).long()
    # 将梯度初始化
    optim.zero_grad()
    
    # 模型输出
    out = model(X)
    #计算准确度
    pred = torch.argmax(out,dim=1)
    acc = (pred == Y).float().mean()
    
    #计算损失值
    loss = loss_fn(out,Y)
    # 计算梯度
    loss.backward()
    # 更新梯度
    optim.step()
    # 
    loss, acc = loss.item(),acc.item()
    losses.append(loss)
    accs.append(acc)
    t.set_description(f"loss:{loss:0.2f}, acc: {acc:0.2f}")

在开始计算梯度之前,需要将之前计算的梯度进行清空 optim.zero_grad() 不然梯度会进行累加。然后将真实标签 Y 和预测输出 out 输入到损失函数来计算损失值,然后调用 loss.backward() 方法来进行反向传播,这样就会对各个模型参数进行计算导数,接下来利用梯度对每一个参数做一次更新,这是optim.step()要做的工作。

plot(losses)

002.png

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿