代码仓库: https://github.com/brandonlyg/cute-dlpython
定义:git
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对象列表.github
训练过程当中, fit方法会把触发的事件派发到全部的FitListener对象, FitListener对象本身决定处理或忽略。正则表达式
history的格式:算法
{ 'loss': [], 'val_loss': [], 'steps': [], 'val_pred': darray, 'cost_time': float }
代码文件: cutedl/session.py。
fit方法比较复杂, 先看主干代码:session
#初始化训练历史数据结构 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
主干代码中使用了一些局部函数, 这些局部函数每一个都是实现了一个小功能。数据结构
派发事件:app
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
记录训练历史:dom
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='\r') else: txt = "loss=%f, val_loss=%f"%(loss, val_loss) print("") print("%s %s\n"%(str_epochs, txt))
设模型每一层的损失函数为:
X是数据, W是权重参数,b是偏移量参数. L2算法是在原损失函数上加上W范数平方的衰减量, 获得一个新的损失函数:
λ是衰减率, 是一个至关于学习率的超参数。对于一个模型来讲, 只有输出层的损失函数是明确知道的, 其余层是不明确的。不过不要紧, 更新参数是在反向传播阶段,这个时候须要的是梯度, 并不关心原函数的形式, 新损失函数的梯度为:
其中
能够在反向传播时候获得. 在梯度降低法训练模型时, 更新参数的表达式变成:
这个表达式的含义是: 在使用学习率更新参数以前,先把参数(W的范数)缩小到原来的(1-λ)倍。
代码文件: 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 正则化 ''' 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
代码文件: 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)
向前传播的函数:
p是咱们要给出的常数。算法使用p构造随机变量A, 使得A=1的几率为p, A=0的几率为1-p. 对这个函数的直观解释是: A将有1-p的几率被丢弃掉(置为0), p的几率被保留, 若是被保留, 它将会被拉伸1/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正则化缓解过拟合 ''' 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缓解过拟合 ''' 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层也能有效地缓解过拟合。 本阶段目标基本达成。 到目前为止, 用来验证框架的是一个线性回归任务, 数据集是从一个二次函数采样获得, 这个任务本质上是训练模型预测连续值。可是在深度学习领域,还要求模型可以预测离散值,即可以执行分类任务。下个阶段, 将会给框架添加新的损失函数, 使之可以支持分类任务, 并讨论这些损失函数的数学性质。