自己动手写一个简单的神经网络框架(1)

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第27天,点击查看活动详情

  • 准备数据
  • 定义模型结构
  • 训练模型
  • 定义损失函数(CrossEntropy)
  • 定义优化器(Adam)
  • 评估(Accuracy)
  • 用测试集来验证模型

准备数据集

%pylab inline
import numpy as np
from tqdm import trange
np.set_printoptions(suppress=True)
复制代码

george_hotz.jpeg

今天还是想跟随 george hotz 的脚步自己写一个神经网络,这里我们先引入 numpy 表示自己要实现框架是基于 numpy 的,这个可能之前也尝试做过,不过都是中途暂停

下载 mnist 数据集

要训练模型,首先要做的是准备数据集,没有数据集,模型就失去培养的土壤,只有肥沃的土壤才能长出茁壮的模型,有关这一点当你成为一名 AI 工程师后,关于数据重要性会体会更加深刻。mnist 是众多的 AI 入门集数据集,可以说算是数据集的 Hello world,当然也是我第一个接触到的数据集。

minist_dataset.png

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:]
复制代码

上面代码的功能主要是下载 mnist 数据集,对于一个不熟悉数据集,首先要做的工作就是要了解数据集格式,包括数据或文件存放的格式,还有我们如何解析数据拿到 label,label 对于不同数据集或者不同任务格式也不尽相同,好接下来的工作就是观察一下数据集。

X_train.reshape(-1,28*28).shape
复制代码
imshow(X_train[0]),Y_train[0]
复制代码

minist_sample.png

import torch
import torch.nn as nn
复制代码

定义模型

这里模型很简单,我们定义 2 层全连接结果网络,经过一个 RelU 非线性激活函数

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()
复制代码

在将数据输入到模型前需要对数据进行简单的处理,首先我们需要将 28 × 28 28 \times 28 2 维矩阵展平为一维向量,这样才能够输入一个由全连接组成的网络。而且需要将其转换为 tensor 类型,且数值类型为浮点型

model(torch.tensor(X_train[0:10].reshape((-1,28*28))).float())
复制代码

tensor([[ 1.2083e+01, -6.2084e+00, -4.2361e+01, 4.9658e+01, 3.9477e+01, -3.0356e+01, -2.2372e+01, 8.0428e+00, 3.2656e+01, -9.3094e+00], [ 2.2671e+01, 3.3245e+00, -4.8243e+01, 3.8604e+01, 3.9542e+01, -2.7933e+01, -1.2826e+01, 8.0161e+00, 3.1077e+01, 1.0993e+01], [-5.2120e+00, -1.1790e+01, -2.8112e+01, 1.9791e+01, 4.3261e+00, -3.8595e+01, -2.8635e+01, 1.4265e+01, 6.1028e+00, 6.4931e+00], [ 3.0938e+00, 5.9621e+00, -5.1074e+01, 2.4239e+01, 1.9193e+01, -1.3659e+01, -9.5052e+00, 1.3481e+01, 2.8320e+01, -1.1060e+01], [ 1.2244e+01, -3.2318e+00, -2.1307e+01, 1.0067e+01, 3.1964e+01, -3.7613e+01, -1.4156e+01, 4.2481e+00, 1.9008e+01, 1.0778e+01], [ 1.1739e+01, -9.3160e+00, -2.9011e+01, 4.4082e+01, 2.8370e+01, -2.6199e+01, -1.0949e+01, 2.2683e+01, -5.4921e+00, 6.5689e+00], [-6.0162e+00, -9.0770e+00, -3.2087e+01, 2.3763e+01, 4.9858e+01, -3.9102e+01, -1.7722e+01, 4.3634e+00, 3.0049e+01, -8.6414e+00], [ 2.1456e+01, -3.3260e+00, -4.4883e+01, 5.1880e+01, 3.7950e+01, -5.3068e+01, -2.3070e+01, 1.0404e+01, 2.0473e+01, 1.3999e+01], [-4.3020e-01, -1.5772e+01, -3.2923e+01, 1.4884e+01, 3.1533e+01, -1.6083e+01, 7.7693e-02, -4.1197e-02, 2.5434e+01, -1.7755e+01], [ 1.2163e+01, -1.6169e+01, -1.0706e+01, 3.6653e+01, 1.0580e+01, -3.4649e+01, -2.0310e+01, 2.0701e+01, 2.2188e+01, -1.5845e+01]], grad_fn=<AddmmBackward0>)
复制代码

训练模型

损失函数

对于分类问题,通常都会选择交叉熵作为损失函数,这里也不例外使用交叉熵作为损失

batch_size = 32;
epochs = 2
loss_fun = nn.CrossEntropyLoss()
for i in range(epochs):
  sample = np.random.randint(0,X_train.shape[0],size=(batch_size))
  X = torch.tensor(X_train[sample].reshape((-1,28*28))).float()
  Y = torch.tensor(Y_train[sample]).long()
  out = model(X)
  loss = loss_fun(out,Y)
  print(loss)

复制代码
tensor(30.2542, grad_fn=<NllLossBackward0>) 
tensor(31.6924, grad_fn=<NllLossBackward0>)
复制代码

优化器

优化器先选择 Adam 然后可以尝试用 SGD 然后可以通过调整学习率、动量等参数来观察对训练过程 loss 和精度的影响效果。

batch_size = 32;
epochs = 100

loss_fun = nn.CrossEntropyLoss()
optim = torch.optim.Adam(model.parameters())

tbar = trange(epochs)
for i in tbar:
  sample = np.random.randint(0,X_train.shape[0],size=(batch_size))
  X = torch.tensor(X_train[sample].reshape((-1,28*28))).float()
  Y = torch.tensor(Y_train[sample]).long()

  optim.zero_grad()

  out = model(X)
  loss = loss_fun(out,Y)
  loss.backward()
  optim.step()
  # print(loss)
  tbar.set_description("loss %.2f" % loss.item())
复制代码

优化器是基于方向传播计算的梯度,通过在时间上策略来更新参数,所以每一次迭代时需要清空上一次优化器optim.zero_grad() 然后optim.step()来更新梯度。

评估

对于分类问题中,有许多评估指标用来衡量一个模型好坏,例如准确率、精准率和召回率等等,这里暂时就关注准确率,现在要做的工作就是把每一次迭代的准确率计算出,并以输出。

batch_size = 32;
epochs = 100

loss_fun = nn.CrossEntropyLoss()
optim = torch.optim.Adam(model.parameters())

tbar = trange(epochs)
for i in tbar:
  sample = np.random.randint(0,X_train.shape[0],size=(batch_size))
  X = torch.tensor(X_train[sample].reshape((-1,28*28))).float()
  Y = torch.tensor(Y_train[sample]).long()

  optim.zero_grad()

  out = model(X)
  category = torch.argmax(out,dim=1)

  acc = (category==Y).float().mean()
  # print(category)
  loss = loss_fun(out,Y)
  loss.backward()
  optim.step()
  # print(loss)
  tbar.set_description("loss: %.2f accuracy: %.2f" % (loss.item(),acc.item()))

复制代码

有时候我们需要观测整个训练过程中 loss 和 acc 变化,通过他们两者的变化来得到一些训练的信息,进而根据这些信息来调整超参数来拿到更好结果。

batch_size = 32;
epochs = 100

loss_fun = nn.CrossEntropyLoss()
optim = torch.optim.Adam(model.parameters())

tbar = trange(epochs)

losses = []
accs = []

for i in tbar:
  sample = np.random.randint(0,X_train.shape[0],size=(batch_size))
  X = torch.tensor(X_train[sample].reshape((-1,28*28))).float()
  Y = torch.tensor(Y_train[sample]).long()

  optim.zero_grad()

  out = model(X)
  category = torch.argmax(out,dim=1)

  acc = (category==Y).float().mean()
  # print(category)
  loss = loss_fun(out,Y)
  loss.backward()
  optim.step()
  # print(loss)

  loss,acc = loss.item(), acc.item()
  
  losses.append(loss)
  accs.append(acc)

  tbar.set_description("loss: %.2f accuracy: %.2f" % (loss,acc))

复制代码
plot(losses)
plot(accs)
复制代码

到这里代码就比较完整了,接下来将训练过程中,loss 和 acc 变化通过图像形式展现出来。

loss_and_acc_plot.png

y_test_preds = torch.argmax(model(torch.tensor( X_test.reshape((-1,28*28))).float()),dim=1 ).numpy()
(Y_test == y_test_preds).mean() #0.9333
复制代码

losses_and_acc_1000.png

batch_size = 128;
epochs = 500

loss_fun = nn.CrossEntropyLoss()
# optim = torch.optim.Adam(model.parameters())
optim = torch.optim.SGD(model.parameters(),lr=0.001,momentum=0)
复制代码

接下来我们就去用 numpy 来实现一个网络,从现在开始就逐步脱离 torchnumpy 来实现一个神经网络框架。首先我们把 2 层的 weight(权重)写出来。然后将通过上面 torch 学习到参数值赋值给这两层作为参数,

l1 = np.zeros((784,128),dtype=np.float32)
l2 = np.zeros((128,10),dtype=np.float32)
复制代码
model.l1.weight.detach().numpy().shape
复制代码

参数类型 (128, 784) 所以接下来对 l1l2 进行修改

l1 = np.zeros((128,784),dtype=np.float32)
l2 = np.zeros((10,128),dtype=np.float32)
复制代码

接下来将训练好模型 model 的参数复制给 l1l2

l1[:] = model.l1.weight.detach().numpy()
l2[:] = model.l2.weight.detach().numpy()
复制代码

猜你喜欢

转载自juejin.im/post/7111267468777095205