关于在Python中实现深度学习库

从简单的神经网络到相当复杂的架构,深度学习技术在短时间内取得了长足的进步。为了支持这些技术的快速传播,已经开发了各种库和深度学习平台。这种库的主要目标之一是为开发人员提供简单的界面,以创建和训练神经网络模型。这样的库允许其用户更加关注要解决的任务,而不是模型实现的精妙之处。为此,您可能需要将基本机制的实现隐藏在几个抽象级别的后面。而这反过来又使对深度学习库所基于的基本原理的理解更加复杂。



这篇文章(我们正在翻译的译本)旨在分析深度学习库的低级构建块设备的功能。首先,我们简要讨论深度学习的本质。这将使我们了解相应软件的功能要求。然后,我们看一下使用NumPy在Python中开发一个简单但有效的深度学习库。该库能够为简单的神经网络模型提供端到端的培训。在此过程中,我们将讨论深度学习框架的各个组成部分。我们将考虑的库很小,不到100行代码。这意味着将很容易弄清楚。我们将处理的完整项目代码可在此处找到

一般信息


通常,深度学习库(例如TensorFlow和PyTorch)由下图所示的组件组成。


深度学习框架

的组件让我们分析这些组件。

▍运算符


“操作员”和“层”(层)的概念通常可以互换使用。这些是任何神经网络的基本构建块。运算符是转换数据的向量函数。在经常使用的运算符中,可以区分诸如线性和卷积层,子采样层(合并),半线性(ReLU)和S形(Sigmoid)激活函数。

▍优化器(optimizers)


优化器是深度学习库的基础。他们描述了使用某些标准并考虑优化目标来调整模型参数的方法。在著名的优化器中,可以注意到SGD,RMSProp和Adam。

▍损失函数


损失函数是一种解析和微分的数学表达式,可用来替代解决问题时的优化目标。例如,交叉熵函数和分段线性函数通常用于分类问题。

▍初始化器


初始化程序为模型参数提供初始值。这些参数在训练开始时就是这些值。初始化程序在神经网络的训练中起着重要作用,因为初始参数不成功可能意味着网络学习缓慢,或者根本无法学习。有很多方法可以初始化神经网络的权重。例如-您可以从正态分布中为它们分配较小的随机值。这是一个页面,您可以在其中了解不同类型的初始化程序。

▍调节器


正则化工具是避免网络重新训练并帮助网络获得泛化的工具。您可以以显式或隐式方式处理网络培训。显式方法涉及重量的结构限制。例如,将它们的L1-Norm和L2-Norm最小化,从而使重量值更好地分散和更均匀地分布。隐式方法由执行中间表示形式转换的专业运算符表示。这可以通过显式标准化(例如,使用数据包标准化技术(BatchNorm))或通过使用DropOut和DropConnect算法更改网络连接来完成。

以上组件通常属于库的接口部分。在这里,“界面部分”是指用户可以与之交互的实体。他们为他提供了有效设计神经网络体系结构的便捷工具。如果我们谈论库的内部机制,它们可以为损失函数的梯度自动计算提供支持,同时考虑模型的各种参数。此技术通常称为自动微分(AD)。

自动区分


每个深度学习库为用户提供了一些自动区分功能。这使他有机会专注于模型结构的描述(计算图),并将计算梯度的任务转移到AD模块。让我们举个例子,让我们知道一切的运作方式。假设我们要针对输入变量X 1和X 2计算以下函数的偏导数:

Y = sin(x 1)+ X 3 * X 2

下图是我从这里借来的,它显示了计算图和使用链式法则的导数计算。


计算图和通过链式规则进行导数计算

您在这里看到的就像是自动微分的“反向模式”。对于位于顶部的函数是损失函数的情况,众所周知的错误反向传播算法是上述算法的特例。AD利用以下事实:任何复杂的函数都由基本算术运算和基本函数组成。结果,可以通过将链式规则应用于这些操作来计算导数。

实作


在上一节中,我们检查了创建深度学习库所必需的组件,该库专门用于创建和端对端训练神经网络。为了不使示例复杂化,我在这里模仿了Caffe库的设计模式。在这里,我们声明了两个抽象类- FunctionOptimizer。此外,还有一个class Tensor,它是一个简单的结构,其中包含两个多维NumPy数组。其中一个用于存储参数值,另一个用于存储其梯度。不同层(操作员)中的所有参数均为类型Tensor。在继续之前,请看一下库的总体轮廓。


库UML图

在编写本文时,该库包含线性层,ReLU激活功能,SoftMaxLoss层和SGD优化器的实现。结果,事实证明该库可用于训练由完全连接的层组成并使用非线性激活函数的分类模型。现在,让我们看一下有关抽象类的一些细节。

抽象类Function为操作员提供接口。这是他的代码:

class  Function(object):
    def forward(self): 
        raise NotImplementedError
    
    def backward(self): 
        raise NotImplementedError
    
    def getParams(self): 
        return []

所有运算符都是通过抽象类的继承实现的Function。每个操作员必须提供方法forward()的实现backward()。运算符可能包含getParams()返回其参数(如果有)的可选方法的实现。该方法forward()接收输入数据,并由操作员返回其转换结果。此外,他解决了计算梯度所需的内部问题。该方法backward()接受相对于操作员的输出的损失函数的偏导数,并实现相对于操作员的输入数据和参数(如果有)的损失函数的偏导数的计算。注意方法backward()本质上讲,为我们的库提供了执行自动区分的能力。

为了用一个特定的例子来处理所有这些,让我们看一下该函数的实现Linear

class Linear(Function):
    def __init__(self,in_nodes,out_nodes):
        self.weights = Tensor((in_nodes,out_nodes))
        self.bias    = Tensor((1,out_nodes))
        self.type = 'linear'

    def forward(self,x):
        output = np.dot(x,self.weights.data)+self.bias.data
        self.input = x 
        return output

    def backward(self,d_y):
        self.weights.grad += np.dot(self.input.T,d_y)
        self.bias.grad    += np.sum(d_y,axis=0,keepdims=True)
        grad_input         = np.dot(d_y,self.weights.data.T)
        return grad_input

    def getParams(self):
        return [self.weights,self.bias]

该方法forward()实现视图的转换Y = X*W+b并返回结果。此外,这样可以节省输入值X的,因为它需要计算的偏导数dY的损失函数相对于输出值Y的方法backward()。方法backward()接收的偏导数,相对于所述输入值计算X和所述参数Wb。此外,它返回相对于输入值计算的偏导数X,它将被传输到上一层。

抽象类Optimizer为优化器提供了一个接口:

class Optimizer(object):
    def __init__(self,parameters):
        self.parameters = parameters
    
    def step(self): 
        raise NotImplementedError

    def zeroGrad(self):
        for p in self.parameters:
            p.grad = 0.

所有优化器都是通过从基类继承来实现的Optimizer描述特定优化的类应提供该方法的实现step()该方法使用相对于损失函数的优化值计算出的偏导数来更新模型参数。函数中提供了各种模型参数的链接__init__()请注意,用于重置梯度值的通用功能是在基类本身中实现的。

现在,为了更好地理解所有这些,请考虑一个特定示例-随机梯度下降算法(SGD,随机梯度下降)的实现,该算法支持脉冲调整和权重降低:

class SGD(Optimizer):
    def __init__(self,parameters,lr=.001,weight_decay=0.0,momentum = .9):
        super().__init__(parameters)
        self.lr           = lr
        self.weight_decay = weight_decay
        self.momentum     = momentum
        self.velocity     = []
        for p in parameters:
            self.velocity.append(np.zeros_like(p.grad))

    def step(self):
        for p,v in zip(self.parameters,self.velocity):
            v = self.momentum*v+p.grad+self.weight_decay*p.data
            p.data=p.data-self.lr*v

真正问题的解决方案


现在,我们有了使用我们的库训练(深度)神经网络模型所需的一切。为此,我们需要以下实体:

  • 型号:计算图。
  • 数据和目标值:用于网络培训的数据。
  • 损失函数:替代优化目标。
  • 优化器:一种更新模型参数的机制。

以下伪代码描述了典型的测试周期:

model # 
data,target # 
loss_fn # 
optim #,         
Repeat:#   ,    ,     
   optim.zeroGrad() #    
   output = model.forward(data) #   
   loss   = loss_fn(output,target) # 
   grad   = loss.backward() #      
   model.backward(grad) #    
   optim.step() #  

尽管这在深度学习库中不是必需的,但将上述功能包含在单独的类中可能会很有用。这将使我们在学习新模型时不必重复相同的步骤(此想法与Keras之类的框架的高级抽象哲学相对应)。为了实现这一点,声明一个类Model

class Model():
    def __init__(self):
        self.computation_graph = []
        self.parameters        = []

    def add(self,layer):
        self.computation_graph.append(layer)
        self.parameters+=layer.getParams()

    def __innitializeNetwork(self):
        for f in self.computation_graph:
            if f.type=='linear':
                weights,bias = f.getParams()
                weights.data = .01*np.random.randn(weights.data.shape[0],weights.data.shape[1])
                bias.data    = 0.

    def fit(self,data,target,batch_size,num_epochs,optimizer,loss_fn):
        loss_history = []
        self.__innitializeNetwork()
        data_gen = DataGenerator(data,target,batch_size)
        itr = 0
        for epoch in range(num_epochs):
            for X,Y in data_gen:
                optimizer.zeroGrad()
                for f in self.computation_graph: X=f.forward(X)
                loss = loss_fn.forward(X,Y)
                grad = loss_fn.backward()
                for f in self.computation_graph[::-1]: grad = f.backward(grad) 
                loss_history+=[loss]
                print("Loss at epoch = {} and iteration = {}: {}".format(epoch,itr,loss_history[-1]))
                itr+=1
                optimizer.step()
        
        return loss_history
    
    def predict(self,data):
        X = data
        for f in self.computation_graph: X = f.forward(X)
        return X

此类包含以下功能:

  • : add() , . computation_graph.
  • : , , , .
  • : fit() . , .
  • : predict() , , .

由于该课程不是深度学习系统的基本组成部分,因此我在单独的模块中实现了它utilities.py请注意,该方法fit()使用的类DataGenerator的实现在同一模块中。此类仅是训练数据的包装,并且为每次训练迭代生成微型包装。

模型训练


现在考虑使用上述库在其中训练神经网络模型的最后一段代码。我将训练螺旋形排列的数据的多层网络。出版物提示我在文件中可以找到用于生成此数据并对其进行可视化的代码utilities.py


三个类别的数据呈螺旋状排列, 

上图显示了我们将在其上训练模型的数据的可视化。该数据是非线性可分离的。我们可以希望具有隐藏层的网络可以正确地找到非线性决策边界。如果将我们讨论的所有内容放在一起,则会得到以下代码片段,可用于训练模型:

import dl_numpy as DL
import utilities

batch_size        = 20
num_epochs        = 200
samples_per_class = 100
num_classes       = 3
hidden_units      = 100
data,target       = utilities.genSpiralData(samples_per_class,num_classes)
model             = utilities.Model()
model.add(DL.Linear(2,hidden_units))
model.add(DL.ReLU())
model.add(DL.Linear(hidden_units,num_classes))
optim   = DL.SGD(model.parameters,lr=1.0,weight_decay=0.001,momentum=.9)
loss_fn = DL.SoftmaxWithLoss()
model.fit(data,target,batch_size,num_epochs,optim,loss_fn)
predicted_labels = np.argmax(model.predict(data),axis=1)
accuracy         = np.sum(predicted_labels==target)/len(target)
print("Model Accuracy = {}".format(accuracy))
utilities.plot2DDataWithDecisionBoundary(data,target,model)

下图显示了相同的数据以及训练模型的决定性边界。


训练模型的数据和决策边界

摘要


鉴于深度学习模型的复杂性不断增加,趋势是增加各个库的功能并增加实现这些功能所需的代码量。但是,此类库的最基本功能仍可以以相对紧凑的形式实现。尽管我们创建的库可用于简单网络的端到端培训,但在许多方面,它仍然受到限制。我们正在谈论的功能方面的局限性使深度学习框架可以用于机器视觉,语音和文本识别等领域。当然,这种框架的可能性不受限制。

我相信每个人都可以参与该项目,我们将在此处检查其代码,并作为练习向他们介绍他们希望在其中看到的内容。您可以尝试以下一些机制来实现自己:

  • 运算符:卷积,二次采样。
  • 优化程序:Adam,RMSProp。
  • 调节器:BatchNorm,DropOut。

我希望该材料至少可以让您从眼角看到深度学习图书馆的肠子中正在发生的事情。

亲爱的读者们!您使用什么深度学习库?

Source: https://habr.com/ru/post/undefined/


All Articles