小目标
从今天开始,尝试用 numpy 实现一个深度神经网络的框架,参考 pytorch 实现方式来进行实现,目的对神经网络有一个更深一层的认识。
参考资料
- George Hotz 分享关于 tinygrad 分享
- pytorch 官方文档
基本要求
- 了解深度学习一般知识
- 熟悉 python 编程语言
- 精通 numpy、matplotlab 等 python 主流库
- 熟悉 pytorch
准备工作
构建深度学习框架暂时来看,准备基于 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:]
X_train.shape #(60000, 28, 28)
可以看到在训练数据集一共有 60k 张图片,每张图像大小为 28 x 28,查看一个下数据样本图像效果
imshow(X_train[0],cmap='gray')
定义模型
写一个简单 2 层的神经网络,通常输入层是不会计入神经网络的层数中。输入向量维度 784 向量是将图像 28 x 28 展平为 784 输入,输入样本形状(m,784) ,这里 m 表示一批次样本的数量。第一层神经网络参数(784x128) 经过第一层后形状(m,128)。这里激活函数选择 ReLU 这个激活函数。第二层神经网络参数(128,10) 输出形状(m,10) 10 对应于 10 个类别。
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)
训练
开始训练,我们需要定义 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
损失函数采用多分类 nn.CrossEntropyLoss()
,其实 CrossEntropyLoss
包括将输出进行 softmax
将输出标准化为概率,然后在用负对数似然来计算两个概率之间距离,
这是 正确标签
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)
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。