zoukankan      html  css  js  c++  java
  • 自己动手实现深度学习框架-3 自动分批训练, 缓解过拟合

    代码仓库: https://github.com/brandonlyg/cute-dl

    目标

    1. 为Session类增加自动分批训练模型的功能, 使框架更好用。
    2. 新增缓解过拟合的算法: L2正则化, 随机丢弃。

    实现自动分批训练

    设计方案

    • 增加Dataset类负责管理数据集, 自动对数据分批。
    • 在Session类中增加fit方法, 从Dataset得到数据, 使用事件机制告诉外界训练情况, 最后返回一个训练历史记录。
    • 增加FitListener类, 用于监听fit方法训练过程中触发的事件。

    fit方法

    定义:

    fit(self, data, epochs, **kargs)
    

    data: 训练数据集Dataset对象。
    epochs: 训练轮数, 把data中的每一批数据遍历训练一次称为一轮。
    kargs:
    val_data: 验证数据集。
    val_epochs: 执行验证的训练轮数. 每val_epochs轮训练验证一次。
    val_steps: 执行验证的训练步数. 每val_steps步训练验证一次. 只有val_steps>0才有效, 优先级高于val_epochs。
    listeners: 事件监听器FitListener对象列表.

    fit方法触发的事件

    • epoch_start: 每轮训练开始时触发。
    • epoch_end: 每轮训练结束时触发。
    • val_start: 每次执行验证时触发。
    • val_end: 每次执行验证结束时触发。

    训练过程中, fit方法会把触发的事件派发到所有的FitListener对象, FitListener对象自己决定处理或忽略。

    训练历史记录(history)

            history的格式:

    {
      'loss': [],
      'val_loss': [],
      'steps': [],
      'val_pred': darray,
      'cost_time': float
    }
    
    • loss: 记录训练误差。
    • val_loss: 记录验证误差。loss会和val_loss同步记录。
    • steps: 每个误差记录对应的训练步数。
    • val_pred: 最后一次执行验证时模型使用验证数据集预测的结果。
    • cost_time: 整个训练过程花费的时间(s)。

    代码

    fit方法实现

            代码文件: cutedl/session.py。
            fit方法比较复杂, 先看主干代码:

        #初始化训练历史数据结构
        history = {
            'loss': [],
            'val_loss': [],
            'steps': [],
            'val_pred': None,
            'cost_time': 0
        }
        #打开训练开关, 当调用stop_fit方法后会关闭这个开关, 停止训练。
        self.__fit_switch = True
    
        #得到参数
        val_data = kargs.get('val_data')
        val_epochs = kargs.get('val_epochs', 1)
        val_steps = kargs.get('val_steps', 0)
        listeners = kargs.get('listeners', [])
    
        if val_data is None:
            history['val_loss'] = None
    
        #计算将会训练的最大步数
        if val_epochs <= 0 or val_epochs >= epochs:
            val_epochs = 1
    
        if val_steps <= 0:
            val_steps = val_epochs * data.batch_count
    
          #开始训练
          step = 0
          history['cost_time'] = time.time()
          for epoch in range(epochs):
              if not self.__fit_switch:
                  break
    
              #触发并派发事件
              event_dispatch("epoch_start")
              for batch_x, batch_y in data.as_iterator():
                  if not self.__fit_switch:
                      break
                  #pdb.set_trace()
                  loss = self.batch_train(batch_x, batch_y)
                  step += 1
                  if step % val_steps == 0:
                      #使用验证数据集验证模型
                      event_dispatch("val_start")
                      val_loss, val_pred = validation()
                      record(loss, val_loss, val_pred, step)
                      event_dispatch("val_end")
                      #显示训练进度
                      display_progress(epoch+1, epochs, step, val_steps, loss, val_loss)
                  else:
                      display_progress(epoch+1, epochs, step, val_steps, loss)
    
              event_dispatch("epoch_end")
    
          #记录训练耗时
          history['cost_time'] = time.time() - history['cost_time']
    
          return history
    

            主干代码中使用了一些局部函数, 这些局部函数每个都是实现了一个小功能。

            派发事件:

      def event_dispatch(event):
        #pdb.set_trace()
        for listener in listeners:
            listener(event, history)
    

            执行验证:

      def validation():
        if val_data is None:
            return None, None
    
        val_pred = None #保存所有的预测结果
        losses = [] #保存所有的损失值
        #分批验证
        for batch_x, batch_y in val_data.as_iterator():
            #pdb.set_trace()
            y_pred = self.__model.predict(batch_x)
            loss = self.__loss(batch_y, y_pred)
            losses.append(loss)
    
            if val_pred is None:
                val_pred = y_pred
            else:
                val_pred = np.vstach((val_pred, y_pred))
        #计算平均损失
        loss = np.mean(np.array(losses))
        return loss, val_pred
    

            记录训练历史:

      def record(loss, val_loss, val_pred, step):
        history['loss'].append(loss)
        history['steps'].append(step)
    
        if history['val_loss'] is not None and val_loss is not None :
            history['val_loss'].append(val_loss)
            history['val_pred'] = val_pred
    

            显示训练进度:

        def display_progress(epoch, epochs, step, steps, loss, val_loss=-1):
          prog = (step % steps)/steps
          w = 20
    
          str_epochs = ("%0"+str(len(str(epochs)))+"d/%d")%(epoch, epochs)
    
          txt = (">"*(int(prog * w))) + (" "*w)
          txt = txt[:w]
          if val_loss < 0:
              txt = txt + (" loss=%f   "%loss)
              print("%s %s"%(str_epochs, txt), end='
    ')
          else:
              txt = "loss=%f, val_loss=%f"%(loss, val_loss)
              print("")
              print("%s %s
    "%(str_epochs, txt))
    

    实现L2正则化参数优化器

    设计方案

    • 增强Optimizer类的功能, 能够自己匹配要更新的参数。
    • 给出L2正则化算法的Optimizer实现。
    • 在Session类中增加对广义优化器的支持(L2优化器就是广义优化器)。

    数学原理

            设模型每一层的损失函数为:

    [J=f(XW+b) ]

            X是数据, W是权重参数,b是偏移量参数. L2算法是在原损失函数上加上W范数平方的衰减量, 得到一个新的损失函数:

    [J_{L2} = J + frac{λ}{2}||W||^2 ]

            λ是衰减率, 是一个相当于学习率的超参数。对于一个模型来说, 只有输出层的损失函数是明确知道的, 其他层是不明确的。不过没关系, 更新参数是在反向传播阶段,这个时候需要的是梯度, 并不关心原函数的形式, 新损失函数的梯度为:

    [frac{partial}{partial W_i}J_{L2} = frac{partial}{partial W_i} J + λW_i ]

            其中

    [frac{partial}{partial W_i} J ]

            可以在反向传播时候得到. 在梯度下降法训练模型时, 更新参数的表达式变成:

    [W_i = W_i - (αfrac{partial}{partial W_i} J + λW_i) = (1-λ)W_i - αfrac{partial}{partial W_i} J, quad ext{α是学习率} ]

            这个表达式的含义是: 在使用学习率更新参数之前,先把参数(W的范数)缩小到原来的(1-λ)倍。

    代码

    增强Optimizer功能

            代码文件: cutedl/optimizer.py
            修改__call__代码:

    def __call__(self, model):
          params = self.match(model)
          for p in params:
              self.update_param(model, p)
    

    match方法用来把名字匹配的参数过滤出来。
    update_param方法实现实际的更新参数操作, 由子类实现。

            match实现:

      '''
      得到名字匹配pattern的参数
      '''
      def match(self, model):
          params = []
          rep = re.compile(self.pattern)
          for ly in model.layer_iterator():
              for p in ly.params:
                  if rep.match(p.name) is None:
                      continue
    
                  params.append(p)
    
          return params
    

            这个方法使用正则表达式通过参数名匹配参数, 并返回匹配的参数列表。pattern是正则表达式属性, 子类可以通过覆盖这个属性, 改变匹配行为。

    实现L2正则化优化器

    '''
    L2 正则化
    '''
    class L2(Optimizer):
        '''
        damping 参数衰减率
        '''
        def __init__(self, damping):
            self.__damping = damping
    
        def update_param(self, model, param):
            #pdb.set_trace()
            param.value = (1 - self.__damping) * param.value
    

    在Session中支持广义参数优化器

            代码文件: cutedl/session.py。
            首先为__init__ 方法添加参数:

    '''
    genoptms: list[Optimizer]对象, 广义参数优化器列表,
                      列表中的优化器将会在optimizer之前按顺序执行
    '''
    def __init__(self, model, loss, optimizer, genoptms=None):
      self.__genoptms = genoptms
    

            然后在batch_train方法中调用优化器:

        #执行广义优化器更新参数
        if self.__genoptms is not None:
            for optm in self.__genoptms:
                optm(self.__model)
    

    实现随机丢弃层: Dropout

    数学原理

            向前传播的函数:

    [Y_i = frac{A_i}{p} X_i, quad A_i服从参数为p的伯努利分布, p∈(0, 1) ]

            p是我们要给出的常数。算法使用p构造随机变量A, 使得A=1的概率为p, A=0的概率为1-p. 对这个函数的直观解释是: A将有1-p的概率被丢弃掉(置为0), p的概率被保留, 如果被保留, 它将会被拉伸1/p倍。 这个函数有一个很有用的性质, 它的输入和输出的均值不变:

    [E(Y_i) = frac{E(A_i)}{p} E(X_i) = frac{p}{p} E(X_i) = E(X_i) ]

            反向传播的梯度为:

    [frac{partial}{partial X_i} = frac{A_i}{p} ]

    代码

            代码文件: nn_layers.py。
            Dropout类实现了随机丢弃算法。向前传播实现:

    def forward(self, in_batch, training=False):
        kp = self.__keep_prob
        #pdb.set_trace()
        if not training or kp <= 0 or kp>=1:
            return in_batch
    
        #生成[0, 1)之间的均价分布
        tmp = np.random.uniform(size=in_batch.shape)
        #保留/丢弃索引
        mark = (tmp <= kp).astype(int)
        #丢弃数据, 并拉伸保留数据
        out = (mark * in_batch)/kp
    
        self.__mark = mark
    
        return out
    

            随机丢弃层传入的参数是keep_prob保留概率, 这意味这丢弃的概率为1 - keep_prob. 只有处于训练状态且0<keep_prob<1才执行丢弃操作。代码中的变量mark就是用保留概率构造随机变量, 它服从参数为keep_prob的伯努利分布。
            反向传播实现:

    def backward(self, gradient):
        #pdb.set_trace()
        if self.__mark is None:
            return gradient
    
        out = (self.__mark * gradient)/self.__keep_prob
    
        return out
    

    验证

            目前阶段所需要的代码已经完成,现在我们来进行验证,验证代码位于: examples/mlp/linear-regression-1.py。

    对比基准

            首先我们来构造一个欠拟合模型作为对比基准。

    '''
    过拟合对比基准
    '''
    def fit0():
        print("fit0")
        model = Model([
            nn.Dense(128, inshape=1, activation='relu'),
            nn.Dense(256, activation='relu'),
            nn.Dense(1)
        ])
        model.assemble()
    
        sess = Session(model,
                    loss=losses.Mse(),
                    optimizer = optimizers.Fixed(),
                )
    
        history = sess.fit(ds, 200000, val_data=val_ds, val_epochs=1000,
                        listeners=[
                            FitListener('val_end', callback=lambda h:on_val_end(sess, h))
                            ]
                        )
    
        fit_report(history, report_path+'00.png', 10)
    

            可以看到这里不再需要自己写训练函数, 直接调用fit方法即可实现自动训练。on_val_end函数监听val_end事件, 它的功能是在满是条件时调用Session的stop_fit方法停止训练, 这里停止训练的条件是: 最初的10次验证过后, 检查每次验证的val_loss值, 如果连续10次没有变得更小就停止训练。
            拟合报告:

    使用L2优化器缓解过拟合

    '''
    使用L2正则化缓解过拟合
    '''
    def fit1():
        print("fit1")
        model = Model([
            nn.Dense(128, inshape=1, activation='relu'),
            nn.Dense(256, activation='relu'),
            nn.Dense(1)
        ])
        model.assemble()
    
    
        sess = Session(model,
                    loss=losses.Mse(),
                    optimizer = optimizers.Fixed(),
                    #L2正则化
                    genoptms = [optimizers.L2(0.00005)]
                )
    
        history = sess.fit(ds, 200000, val_data=val_ds, val_epochs=1000,
                        listeners=[
                            FitListener('val_end', callback=lambda h:on_val_end(sess, h))
                            ]
                        )
        fit_report(history, report_path+'01.png', 10)
    

            拟合报告:

            从训练损失值图像上看有明显的缓解迹象。

    使用Dropout层缓解过拟合

    '''
    使用dropout缓解过拟合
    '''
    def fit2():
        print("fit2")
        model = Model([
            nn.Dense(128, inshape=1, activation='relu'),
            nn.Dense(256, activation='relu'),
            nn.Dropout(0.80), #0.8的保留概率
            nn.Dense(1)
        ])
        model.assemble()
    
        sess = Session(model,
                    loss=losses.Mse(),
                    optimizer = optimizers.Fixed(),
                )
    
        history = sess.fit(ds, 200000, val_data=val_ds, val_epochs=1000,
                        listeners=[
                            FitListener('val_end', callback=lambda h:on_val_end(sess, h))
                            ]
                        )
    
        fit_report(history, report_path+'02.png', 15)
    

            拟合报告:

            从训练损失值图像上随机丢弃的效果更好一些。

    总结

            验证结果表明, cute-dl目前可以用很少代码实现模型的自动分批训练, 和linear-regression.py相比, linear-regression-1.py中已经不需要关注具体的训练过程了, 并且能够得到基本训练历史记录。另外, L2正则化优化器和Dropout层也能有效地缓解过拟合。 本阶段目标基本达成。
            到目前为止, 用来验证框架的是一个线性回归任务, 数据集是从一个二次函数采样得到, 这个任务本质上是训练模型预测连续值。但是在深度学习领域,还要求模型能够预测离散值,即能够执行分类任务。下个阶段, 将会给框架添加新的损失函数, 使之能够支持分类任务, 并讨论这些损失函数的数学性质。

  • 相关阅读:
    “XXXXX” is damaged and can’t be opened. You should move it to the Trash 解决方案
    深入浅出 eBPF 安全项目 Tracee
    Unity3d开发的知名大型游戏案例
    Unity 3D 拥有强大的编辑界面
    Unity 3D物理引擎详解
    Unity 3D图形用户界面及常用控件
    Unity 3D的视图与相应的基础操作方法
    Unity Technologies 公司开发的三维游戏制作引擎——Unity 3D
    重学计算机
    windows cmd用户操作,添加,设备管理员组,允许修改密码
  • 原文地址:https://www.cnblogs.com/brandonli/p/12711913.html
Copyright © 2011-2022 走看看