当前位置: 首页 > 后端技术 > Python

使用Numpy打造属于你自己的深度学习框架

时间:2023-03-26 01:01:18 Python

本文不是造轮子,而是通过手工实现来介绍搭建一个基础深度学习框架所需的组件和步骤。Numpy基本上已经提供了所有需要的计算操作。我们需要的是一个支持自动微分(autograd)来计算多个操作的梯度的框架,这是一种以模块化方式构建神经网络层的标准化方法。通过自动微分的框架,我们可以结合优化器、激活函数等来训练神经网络。所以一个基本的深度学习框架的组件总结如下:一个autograd系统神经网络层神经网络模型优化器激活函数数据集接下来,我们将一一介绍这些组件,看看它们的作用以及如何使用它们,这里我们会用到gradflow(这是个人开源的教育autograd系统),因为它支持深度神经网络,和PyTorchAPI基本一致。Autograd系统这是最重要的组件,它是每个深度学习框架的基础,因为系统会跟踪应用于输入张量的操作,并使用损失函数对每个参数的梯度来更新权重该模型。这里的一个必要条件是这些操作必须是可微的。我们autograd系统的基础是变量,通过为我们需要的操作实现dundermethods(dundermethods:Python中以双下划线开头的特殊方法),我们将能够跟踪每个实例的父实例是什么以及它们是如何计算梯度的。为了帮助进行一些操作,我们将使用一个numpy数组来保存实际数据。该变体的另一个重要部分是反向传播方法,它计算当前实例相对于计算图中每个父祖先的梯度。在具体步骤中,我们将使用父级的引用和原始操作中嵌入的梯度函数来更新grad成员字段。以下代码片段包含主要变量类初始化函数、add操作的dunder方法和反向传播方法:classVariable:def__init__(self,data:np.ndarray,parents:Tuple[Variable]=None,requires_grad:bool=False)->None:self.data=dataself.grad:Union[int,None]=.0ifrequires_gradelseNoneself.parents=parentsor()self._requires_grad=requires_gradself._back_grad_fn=lambda:无def__add__(self,other:Variable)->Variable:ifnotisinstance(other,Variable):raiseTypeError("第二个运算符必须是变量类型")result=self.data+other.datavariable=Variable(result,parents=(self,other))ifany((parent.requires_gradforparentinvariable.parents)):variable.requires_grad=Truedef_back_grad_fn():self.grad+=variable.gradother.grad+=variable.毕业变量._back_grad_fn=_back_grad_fn向后返回变量def(self,grad:Variable|np.ndarray=None)->None:如果grad为None:grad=np.array([1])self.grad=gradvariable_queue=[self]whilelen(variable_queue):variable=variable_queue.pop(0)variable._back_grad_fn()variable_queue.extend(list(variable.parents))s_back_grad_fn中还有两点需要注意,1.我们需要对已有的值加上梯度,因为在计算图中变量有多条路径的情况下需要累加,2,还需要利用子节点的梯度,如果想了解更多关于自动微分和矩阵演算的细节,我们将在后续文章中详细介绍神经网络模块。对于实际的神经网络模块,我们希望能够灵活地实现新层和现有模块的重用。所以这里hi使用了类似PyTorchAPI的架构来创建一个需要实现init和forward方法的基类Module。除了这两个方法之外,我们还需要几个基于实用程序的方法来访问参数和子模块。类模块(ABC):def__init__(self,training=True)->None:self._parameters:List[Variable]=[]self._modules:List[Module]=[]self._training=trainingself._module_name=DEFAULT_MODULE_NAME@abstractmethoddefforward(self,input:Variable)->变量:raiseNotImplementeddefadd_parameter(self,parameter:Variable)->变量:self._parameters.append(parameter)返回参数defadd_module(self,module:Module)->模块:self._modules.append(module)returnmoduledef__call__(self,input:Variable)->变量:returnself.forward(input)@propertydefmodules(self)->List[Module]:returnself._modules@propertydefparameters(self)->List[Variable]:modules_parameters=[]modules=[self]visited_modules:Set[Module]=set([])whilelen(modules)!=0:模块=modules.pop()ifmoduleinvisited_modules:raiseRecursionError("Modulealreadyvisited,cycledetected.")modules_parameters.extend(module._parameters)modules.extend(module.modules)visited_modules.add(module)returnmodules_parametersdefmodule_name(self)->str:returnself._module_namelinearlayerlinearlayer是神经网络模型中使用最多也是最简单的层,我们使用上一节的抽象模块来实现一个简单的linearlayer线性层的数学很简单:我们将使用前面实现的变量来自动计算操作的实际结果和梯度,所以实现很简单:classLinear(Module):def__init__(self,in_size,out_size)->无:超级().__init__()weights_data:np.ndarray=np.random.uniform(size=in_size*out_size).reshape((in_size,out_size))self.weights=Variable(weights_data,requires_grad=True)self.b=变量(np.random.uniform(size=out_size),requires_grad=True)self.add_parameter(self.weights)self.add_parameter(self.b)defforward(self,input:Variable):tmp=input@self.weightsout=tmp+self.breturnoutactivationfunction现实世界中的大多数数据在自变量和因变量之间存在非线性关系,我们希望我们的模型也能学习这种关系。如果我们不在线性层之上添加非线性激活函数,那么无论我们添加多少线性层,最后我们都可以只用一层(权重矩阵)来表示它们。所以这里介绍最简单最常用的激活函数ReLu。实现relu函数时,还需要指定反向传播函数:defrelu(input:Variable)->Variable:result=np.maximum(input.data,0)variable=Variable(result,parents=(input,))如果input.requires_grad:def_back_grad_fn():input.grad+=np.transpose((variable.data>0))*variable.gradvariable._back_grad_fn=_back_grad_fnvariable.requires_grad=Truereturnvariableoptimizer在通过我们的模型执行正向传递并通过我们的自定义层反向传播梯度之后,我们需要实际更新参数以使损失函数更小。最简单的优化器之一是SGD(随机梯度下降)。在本文的实现中,我们还是采用最简单的实现方式,只使用梯度和学习率来裁剪变化值增量和更新权重:classBaseOptimizer(ABC):def__init__(self,parameters:List[Variable],lr=0.0001)->None:super().__init__()self._parameters=parametersself._lr=lrdefzero_grad(self):forparameterinself._parameters:ifparameter.requires_grad==False:continueifisinstance(parameter.grad,np.ndarray):parameter.grad=np.zeros_like(parameter.grad)else:parameter.grad=np.array([0],dtype=np.float)@abstractmethoddefstep(self):raiseNotImplementedError类NaiveSGD(BaseOptimizer):def__init__(self,parameters:List[Variable],lr=0.001)->None:super().__init__(parameters=parameters,lr=lr)defstep(self):forparameter在self._parameters中:clipped_grad=np.clip(parameter.grad,-1000,1000)delta=-self._lr*clipped_graddelta=np.transpose(delta)parameter.data=parameter.data+delta数据集的最后一个组成部分是数据集。数据集虽然不是核心组件,但是非常重要。它可以帮助我们组织数据集并将其集成到训练过程中我们同样使用Pytorch的方法创建一个Dataset类,实现iterator的dunder方法,将featureX和labelY转为Variable类型:classDataset:def__init__(self,features:np.ndarray,labels:np.ndarray,batch_size=16)->无:self._features=featuresself._labels=labelsself._batch_size=batch_sizeself._cur_index=0def__iter__(self):self._cur_index=0returnselfdef__next__(self)->元组[变量,变量]:如果self._cur_index>=len(self):提高StopIterationsample_batch,label_batch=self[self._cur_index]self._cur_index+=self._batch_size返回sample_batch,label_batchdef__getitem__(self,idx)->[Variable,Variable]:如果idx>=len(self):提高IndexError:self._cur_index+self._batch_size])returnsample_batch,label_batchdef__len__(self)->int:returnint(len(self._features)/self._batch_size)train最后把所有东西放在一起,使用sklearn.datasets使用人工生成的数据集训练一个简单的线性模型:X,y=datasets.make_regression(n_samples=100,n_features=1,n_informative=1,noise=10,coef=False,random_state=0)y=np.power(y,2)dataset=Dataset(X,y,batch_size=1)返回数据集优化器=NaiveSGD(model.parameters,lr=config["lr"])training_loss=[]forepochinrange(epochs):epoch_loss=.0forX,yindataset:pred_y=model(X)loss=(pred_y-y)@(pred_y-y)epoch_loss+=loss.data.item()loss.backward()优化器.step()optimizer.zero_grad()epoch_loss/=len(数据集)training_loss.append(epoch_loss)print(f"Epoch{epoch}|损失:{epoch_loss}")总结如开头所述,本文展示的实现绝不是生产级的,而且非常有限,但它可以让我们更好地理解其他流行框架下发生的一些操作,这是我们学习和使用的深度学习框架的重要组成部分https://avoid.overfit.cn/post/06fddd582b27492cae9a00e9f600dce4作者:TudorSurdoiu