对于DNN,大多数时间咱们都只须要利用成熟的第三方库配置网络结构和超参就能实现咱们想要的模型。
底层的求导、梯度降低永远是一个黑盒,咱们不知道发生了什么,只知道使用optimizer使loss函数收敛,获得好的模型表现能力。node
本文从零开始,实现DNN网络中须要的各类操做,拼凑出一个简单的模型在iris数据集上验证效果。python
梯度是一个向量,指向函数值降低最快的方向
若是将函数图像视为一个实体面,梯度近似于水自由流动的方向,向着这个方向走能最快获得最的小函数值。
从数值上来说,梯度的每个份量分别对应该点在该轴方向上的偏导数算法
想象一下这样一个公式数组
对x求导得网络
x的偏导数等于包含x项的导数之和
复合函数求导等于外层函数导数与内层函数导数之积 这个法则称为链式求导法则dom
矩阵乘法其实是多项式乘法的另外一种直观写法机器学习
实际上是由两个多项式函数组成函数
对x,y进行求偏导写成矩阵的形式为学习
该矩阵被称为jacobian矩阵测试
若e,f会被带到外层函数中继续参与计算 获得最终结果z
根据链式求导法则,z对xy求导的jacobian矩阵为
最终x,y的偏导数为
假设矩阵(e f)的导数为G 输入矩阵(a b, c d)为I 则x y的梯度应该为
若a,b,c,d不是常数而是复合函数 则a,b,c,d也有对应的偏导数 其jacobian矩阵为
使用了numpy库用做矩阵操做,pandas库用来读取iris数据集,没有使用现有机器学习库
import numpy as np import pandas as pd # 定义各类操做的基类 class Operation: def __init__(self): self.inp = np.zeros(0) # 前向生成结果 inp为输入 返回值为操做的计算结果 def forward(self, inp): self.inp = inp # 反向求导生成梯度 grad为出参的导数矩阵(链式求导法则的系数) 返回值为入参的导数矩阵 def backward(self, grad): pass # 模仿pytorch形式的调用方式 def __call__(self, *args, **kwargs): return self.forward(args[0])
class Linear(Operation): def __init__(self, inp_size, out_size): super().__init__() self.k = np.random.rand(inp_size, out_size) - 0.5 self.k_grad = np.zeros((inp_size, out_size)) - 0.5 self.b = np.random.rand(out_size) - 0.5 self.b_grad = np.zeros(out_size) - 0.5 def forward(self, inp): super().forward(inp) return np.matmul(inp, self.k) + self.b def backward(self, grad): self.b_grad = grad.sum(axis=0) # b的jacobian矩阵 self.k_grad = np.matmul(self.inp.transpose(), grad) # k的jacobian矩阵 return np.matmul(grad, self.k.transpose()) # 入参的jacobian矩阵
线性函数y=kx+b
因为操做包含两个变量矩阵k,b 在backward方法中不只要对入参进行求导 也须要对这两个参数进行求导 求导法则参照上文矩阵求导法则部分
class ReLU(Operation): def __init__(self): super().__init__() def forward(self, inp): super().forward(inp) inp[inp < 0] = 0 return inp def backward(self, grad): grad[self.inp < 0] = 0 return grad
非线性函数ReLU自己是一个max(0, x)
前向操做将负数置为0便可
反向求梯度操做,饱和部分梯度为0 非饱和部分梯度为1 将输入中负数部分对应的出参导数置为0即为入参导数矩阵
class Sigmoid(Operation): def __init__(self): super().__init__() def forward(self, inp): super().forward(inp) return 1 / (np.exp(-inp) + 1) def backward(self, grad): output = 1 / (np.exp(-self.inp) + 1) return out(1-out) * grad
Sigmoid函数,这里使用其主要目的是生成几率。
Sigmoid能够将函数值映射到0-1之间,能够将网络计算的数值结果映射成几率结果 其导数为
class L2Loss(Operation): def __init__(self, label): super().__init__() self.label = label def forward(self, inp): super().forward(inp) return np.power((inp - self.label), 2).sum() def backward(self, grad): return 2 * (self.inp - self.label) * grad
L2 loss 用来定义损失函数
class Dnn: # 模型定义 input->hidden_layer[ReLU]->output[Sigmoid] def __init__(self, inp_size, hidden_nodes, batch_size): self.l1 = Linear(inp_size, hidden_nodes) self.relu = ReLU() self.l2 = Linear(hidden_nodes, 1) self.sigmoid = Sigmoid() self.loss = L2Loss([]) self.batch_size = batch_size # 计算模型输出 def forward(self, inp): x = self.l1(inp) x = self.relu(x) x = self.l2(x) return self.sigmoid(x) # 计算损失函数 def loss_value(self, inp, label): output = self.forward(inp) self.loss = L2Loss(label) return self.loss(output) # 反向传播计算梯度 def backward(self): x = self.loss.backward(1) x = self.sigmoid.backward(x) x = self.l2.backward(x) x = self.relu.backward(x) return self.l1.backward(x) # 梯度降低 步长为l 这里使用固定方向固定步长的梯度降低 并无使用更复杂的随机梯度降低 # 注意:没使用随机梯度降低等优化算法,若是初始状态得位置很差,有可能出现没法收敛的状况 def optm(self, l): self.l1.k -= l * self.l1.k_grad self.l2.k -= l * self.l2.k_grad self.l1.b -= l * self.l1.b_grad self.l2.b -= l * self.l2.b_grad def __call__(self, *args, **kwargs): return self.forward(args[0])
datas = pd.read_csv("/data1/iris.data") # 把原本的多分类问题转变成2分类问题方便实现 datas.loc[datas.query("label == 'Iris-setosa'").index, "label_bool"] = 1 datas.loc[datas.query("label != 'Iris-setosa'").index, "label_bool"] = 0 datas.sample(10) # sample用于随机抽样 返回数据集中随机n行 """ f1 f2 f3 f4 label label_bool 121 5.6 2.8 4.9 2.0 Iris-virginica 0.0 147 6.5 3.0 5.2 2.0 Iris-virginica 0.0 53 5.5 2.3 4.0 1.3 Iris-versicolor 0.0 22 4.6 3.6 1.0 0.2 Iris-setosa 1.0 129 7.2 3.0 5.8 1.6 Iris-virginica 0.0 57 4.9 2.4 3.3 1.0 Iris-versicolor 0.0 52 6.9 3.1 4.9 1.5 Iris-versicolor 0.0 11 4.8 3.4 1.6 0.2 Iris-setosa 1.0 20 5.4 3.4 1.7 0.2 Iris-setosa 1.0 70 5.9 3.2 4.8 1.8 Iris-versicolor 0.0 """ datas = datas[["f1", "f2", "f3", "f4", "label_bool"]] # 划分训练集和测试集 train = datas.head(130) test = datas.tail(20) m = Dnn(4, 20, 15) # 迭代1000次 步长0.1 batch大小15 每100次迭代输出一次loss m = Dnn(4, 20, 15) for i in range(0, 1000): t = train.sample(15) tf = t[["f1", "f2", "f3", "f4"]] m(tf.values) loss = m.loss_value(tf.values, t["label_bool"].values.reshape(15, 1)) if i % 100 == 0: print(loss) m.backward() m.optm(0.1) """ 9.906133281506982 5.999831306129411 2.9998719755558625 2.9998576731173774 3.999596848200819 3.9935063085696276 0.014295955296485221 0.0009333518819264006 0.0010388260442712738 0.0010148543295591939 """ # 从训练结果上来看是收敛了的 接下来看泛化能力如何 t = test.sample(15) tf = t[["f1", "f2", "f3", "f4"]] m(tf.values) """ array([[1.06040557e-03], [9.95898652e-01], [8.77277996e-05], [3.03117186e-05], [4.66427694e-04], [9.96013317e-01], [8.69158240e-04], [9.99175120e-01], [1.97615481e-06], [6.21071987e-06], [9.93056596e-01], [9.93803044e-01], [1.84890487e-03], [3.39268278e-04], [4.31708336e-05]]) """ t """ f1 f2 f3 f4 label_bool 62 6.0 2.2 4.0 1.0 0.0 1 4.9 3.0 1.4 0.2 1.0 68 6.2 2.2 4.5 1.5 0.0 149 5.9 3.0 5.1 1.8 0.0 58 6.6 2.9 4.6 1.3 0.0 20 5.4 3.4 1.7 0.2 1.0 89 5.5 2.5 4.0 1.3 0.0 4 5.0 3.6 1.4 0.2 1.0 143 6.8 3.2 5.9 2.3 0.0 114 5.8 2.8 5.1 2.4 0.0 26 5.0 3.4 1.6 0.4 1.0 45 4.8 3.0 1.4 0.3 1.0 69 5.6 2.5 3.9 1.1 0.0 55 5.7 2.8 4.5 1.3 0.0 133 6.3 2.8 5.1 1.5 0.0 """ m.loss_value(tf.values, t["label_bool"].values.reshape(15, 1)) """ 0.0001256497782966175 """ # 对于模型历来没见过的测试数据,模型也能够准确的识别 代表模型拥有至关的泛化能力