您好,欢迎访问一九零五行业门户网

逆天了!用Numpy开发深度学习框架,透视神经网络训练过程

哈喽,大家好。
今天给大家分享一个非常牛逼的开源项目,用numpy​开发了一个深度学习框架,语法与 pytorch 基本一致。
今天以一个简单的卷积神经网络为例,分析神经网络训练过程中,涉及的前向传播、反向传播、参数优化等核心步骤的源码。
使用的数据集和代码已经打包好,文末有获取方式。
1. 准备工作先准备好数据和代码。
1.1 搭建网络首先,下载框架源码,地址:https://github.com/duma-repo/pydynet
git clone https://github.com/duma-repo/pydynet.git
搭建lenet卷积神经网络,训练三分类模型。
在pydynet目录直接创建代码文件即可。
from pydynet import nn class lenet(nn.module): def __init__(self): super().__init__() self.conv1 = nn.conv2d(1, 6, kernel_size=5, padding=2) self.conv2 = nn.conv2d(6, 16, kernel_size=5) self.avg_pool = nn.avgpool2d(kernel_size=2, stride=2, padding=0) self.sigmoid = nn.sigmoid() self.fc1 = nn.linear(16 * 5 * 5, 120) self.fc2 = nn.linear(120, 84) self.fc3 = nn.linear(84, 3) def forward(self, x): x = self.conv1(x) x = self.sigmoid(x) x = self.avg_pool(x) x = self.conv2(x) x = self.sigmoid(x) x = self.avg_pool(x) x = x.reshape(x.shape[0], -1) x = self.fc1(x) x = self.sigmoid(x) x = self.fc2(x) x = self.sigmoid(x) x = self.fc3(x) return x
可以看到,网络的定义与pytorch语法完全一样。
我提供的源代码里,提供了 summary 函数可以打印网络结构。
1.2 准备数据训练数据使用fanshion-mnist数据集,它包含10个类别的图片,每个类别 6k 张。
为了加快训练,我只抽取了前3个类别,共1.8w张训练图片,做一个三分类模型。
1.3 模型训练import pydynet from pydynet import nn from pydynet import optim lr, num_epochs = 0.9, 10 optimizer = optim.sgd(net.parameters(), lr=lr) loss = nn.crossentropyloss() for epoch in range(num_epochs): net.train() for i, (x, y) in enumerate(train_iter): optimizer.zero_grad() y_hat = net(x) l = loss(y_hat, y) l.backward() optimizer.step() with pydynet.no_grad(): metric.add(l.numpy() * x.shape[0], accuracy(y_hat, y), x.shape[0])
训练代码也跟pytorch一样。
下面重点要做的就是深入模型训练的源码,来学习模型训练的原理。
2. train、no_grad和eval模型开始训练前,会调用net.train。
def train(self, mode: bool = true): set_grad_enabled(mode) self.set_module_state(mode)
可以看到,它会将grad​(梯度)设置成true​,之后创建的tensor​是可以带梯度的。tensor带上梯度后,便会将其放入计算图中,等待求导计算梯度。
而下面的with no_grad(): 代码
class no_grad: def __enter__(self) -> none: self.prev = is_grad_enable() set_grad_enabled(false)
会将grad​(梯度)设置成false​,这样之后创建的tensor不会放到计算图中,自然也不需要计算梯度,可以加快推理。
我们经常在pytorch​中看到net.eval()的用法,我们也顺便看一下它的源码。
def eval(self): return self.train(false)
可以看到,它直接调用train(false)​来关闭梯度,效果与no_grad()类似。
所以,一般在训练前调用train​打开梯度。训练后,调用eval关闭梯度,方便快速推理。
3. 前向传播前向传播除了计算类别概率外,最最重要的一件事是按照前传顺序,将网络中的 tensor​ 组织成计算图,目的是为了反向传播时计算每个tensor的梯度。
tensor在神经网络中,不止用来存储数据,还用计算梯度、存储梯度。
以第一层卷积操作为例,来查看如何生成计算图。
def conv2d(x: tensor.tensor, kernel: tensor.tensor, padding: int = 0, stride: int = 1): '''二维卷积函数 ''' n, _, _, _ = x.shape out_channels, _, kernel_size, _ = kernel.shape pad_x = __pad2d(x, padding) col = __im2col2d(pad_x, kernel_size, stride) out_h, out_w = col.shape[-2:] col = col.transpose(0, 4, 5, 1, 2, 3).reshape(n * out_h * out_w, -1) col_filter = kernel.reshape(out_channels, -1).t out = col @ col_filter return out.reshape(n, out_h, out_w, -1).transpose(0, 3, 1, 2)
x​是输入的图片,不需要记录梯度。kernel是卷积核的权重,需要计算梯度。
所以,pad_x = __pad2d(x, padding)​ 生成的新的tensor也是不带梯度的,因此也不需要加入计算图中。
而kernel.reshape(out_channels, -1)​产生的tensor则是需要计算梯度,也需要加入计算图中。
下面看看加入的过程:
def reshape(self, *new_shape): return reshape(self, new_shape) class reshape(unaryoperator): ''' 张量形状变换算子,在tensor中进行重载 parameters ---------- new_shape : tuple 变换后的形状,用法同numpy ''' def __init__(self, x: tensor, new_shape: tuple) -> none: self.new_shape = new_shape super().__init__(x) def forward(self, x: tensor) return x.data.reshape(self.new_shape) def grad_fn(self, x: tensor, grad: np.ndarray) return grad.reshape(x.shape)
reshape​函数会返回一个reshape​类对象,reshape​类继承了unaryoperator​类,并在__init__函数中,调用了父类初始化函数。
class unaryoperator(tensor): def __init__(self, x: tensor) -> none: if not isinstance(x, tensor): x = tensor(x) self.device = x.device super().__init__( data=self.forward(x), device=x.device, # 这里 requires_grad 为 true requires_grad=is_grad_enable() and x.requires_grad, )
unaryoperator​类继承了tensor​类,所以reshape​对象也是一个tensor。
在unaryoperator的__init__​函数中,调用tensor​的初始化函数,并且传入的requires_grad​参数是true,代表需要计算梯度。
requires_grad​的计算代码为is_grad_enable() and x.requires_grad,is_grad_enable()​已经被train​设置为true​,而x​是卷积核,它的requires_grad​也是true。
class tensor: def __init__( self, data: any, dtype=none, device: union[device, int, str, none] = none, requires_grad: bool = false, ) -> none: if self.requires_grad: # 不需要求梯度的节点不出现在动态计算图中 graph.add_node(self)
最终在tensor​类的初始化方法中,调用graph.add_node(self)​将当前tensor加入到计算图中。
同理,下面使用requires_grad=true的tensor​常见出来的新tensor都会放到计算图中。
经过一次卷积操作,计算图中会增加 6 个节点。
4. 反向传播一次前向传播完成后,从计算图中最后一个节点开始,从后往前进行反向传播。
l = loss(y_hat, y) l.backward()
经过前向网络一层层传播,最终传到了损失张量l。
以l​为起点,从前向后传播,就可计算计算图中每个节点的梯度。
backward的核心代码如下:
def backward(self, retain_graph: bool = false): for node in graph.node_list[y_id::-1]: grad = node.grad for last in [l for l in node.last if l.requires_grad]: add_grad = node.grad_fn(last, grad) last.grad += add_grad
graph.node_list[y_id::-1]将计算图倒序排。
node​是前向传播时放入计算图​中的每个tensor。
node.last​ 是生成当前tensor的直接父节点。
调用node.grad_fn计算梯度,并反向传给它的父节点。
grad_fn​其实就是tensor的求导公式,如:
class pow(binaryoperator): ''' 幂运算算子,在tensor类中进行重载 see also -------- add : 加法算子 ''' def grad_fn(self, node: tensor, grad: np.ndarray) if node is self.last[0]: return (self.data * self.last[1].data / node.data) * grad
return​后的代码其实就是幂函数求导公式。
假设y=x^2,x​的导数为2x。
5. 更新参数反向传播计算梯度后,便可以调用优化器,更新模型参数。
l.backward() optimizer.step()
本次训练我们用梯度下降sgd算法优化参数,更新过程如下:
def step(self): for i in range(len(self.params)): grad = self.params[i].grad + self.weight_decay * self.params[i].data self.v[i] *= self.momentum self.v[i] += self.lr * grad self.params[i].data -= self.v[i] if self.nesterov: self.params[i].data -= self.lr * grad
self.params​是整个网络的权重,初始化sgd时传进去的。
step​函数最核心的两行代码,self.v[i] += self.lr * grad​ 和 self.params[i].data -= self.v[i]​,用当前参数 - 学习速率 * 梯度​更新当前参数。
这是机器学习的基础内容了,我们应该很熟悉了。
一次模型训练的完整过程大致就串完了,大家可以设置打印语句,或者通过debug的方式跟踪每一行代码的执行过程,这样可以更了解模型的训练过程。
以上就是逆天了!用numpy开发深度学习框架,透视神经网络训练过程的详细内容。
其它类似信息

推荐信息