打算更新一个小的系列,不定期更新,但是题目定为《每周一个机器学习小项目》,主要偏重于深度学习内容,穿插一些传统的机器学习算法的理论和实现。没有什么特色,也没有什么噱头,甚至于名字都很普通。主要是想用200行左右的代码(限定为 Python,如果超过再改简介)实现一些机器学习算法。过去说完全熟悉机器学习至少需要三年的时间,所以大概会更新这么久。希望读者如果真的想从事机器学习工作,还是脚踏实地一步一步来,学习这种事快不得,学成后工程实践又是三年。门槛这种事情都是时间和精力堆起来的,所有速成都会增加以后学习的成本。
主要几个特色:
- 主要使用的语言为 Python3+Numpy。
- 尽量给出中英文对照,每篇文章都会推荐相关文献;
- 描述理论过程中尽量不使用图片、比喻;
- 后期有时间会更新GPU实现相关文章,这肯定不止200行;
- 不会使用 TensorFlow、Caffe 以及其他任何机器学习库,cuDNN 除外。
推荐阅读时间:20min
推荐阅读文章:Rumelhart D E, Hinton G E, Williams R J. Learning representations by back-propagating errors.[J]. 1986, 323(6088):399-421.
2. 软件环境- Python3
- Numpy
- 导数
- 矩阵运算
- 数据来源:kaggle
- 数据下载:kanggle 竞赛页面
- 数据描述:欧洲的信用卡持卡人在 2013 年 9 月 2 天时间里的 284807 笔交易数据,其中有 492 笔交易是欺诈交易,占比 0.172%。数据采用 PCA 变换映射为 V1、V2、…、V28 数值型属性,只有交易时间和金额这两个变量没有经过 PCA 变换。输出变量为二值变量,1 为正常,0 为欺诈交易。
全链接输入与输出矩阵格式为 [BATCHSIZE, Features], 假设网络某一层输入为 $x^l$,输出为 $x^{l+1}$,那么全链接网络输入与输出层间关系为:
链式求导是目前为止整个深度学习的基础,有人将链式求导法则称之为反向传播。反向传播是从 $loss$ 函数开始的。
传播过程之中每一层均会计算两个内容:
- 本层可训练参数的导数,
- 本层向前传播误差(链式求导)。
举个例子来说:
这里定义了几个计算:
$1.3$ 中将 $1.2$ 中每个操作均算为一个计算单元,对于 $g=a\cdot f_2$ 这一层来说,有一个可训练参数 a,那么反向传播需要计算可训练参数 a 的导数 $1.1-(e)$,同时为了计算 $f_3$ 之中的可训练参数,此层需要产生新的 $e_3$。对于 $g=f_1(\cdot)$ 这一层来说,由于没有可训练参数,因此仅产生反向传播误差 $e_2$。
再次强调 $1.2$ 中将每一步计算,包括相乘、相加、通过函数均算为单独的计算层。因此全链接层包括:矩阵相乘(wx)-矩阵相加(wx+b)-函数计算(f(wx+b))三个计算层。
5.3 矩阵相乘正向计算矩阵正向计算过程中比较简单,仅是一个矩阵相乘:$$x^{l+1}=W^l\cdot x^l$$(1.4)
$1.4$ 为 $1.1-a$,这里将其独立为单独一层。上标 $l$ 代表层号。
5.4 矩阵相乘反向传播矩阵相乘 $1.4$,假设此层反向传播误差为 $e^l$,那么误差传递函数为:
此层可训练参数的导数为:
偏置项计算正向计算方式为 $1.1-b$,所描述的过程:$$x^{l+1}= x^l+b^l$$(1.7)
5.6 偏置项导数可训练参数导数与误差传播加入偏置项对应于 $1.7$,此步之中误差传播方式为:
$$e^{l}=\frac{\partial loss}{\partial x^l}=\frac{\partial loss}{x^{l+1}}\frac{\partial x^l}{\partial x^{l}}=e^{l+1}$$(1.8)
也就是不发生变化,而可训练参数的导数为:
$$\frac{\partial loss}{\partial b^l}=e^l$$(1.9)
5.7 激活函数层可训练参数导数与误差传播激活函数对应于 $1.1-c$,此步之中无可训练参数,因此仅需计算误差传播项:
$$e^{l+1}=\frac{\partial loss}{\partial x^l}=\frac{\partial loss}{\partial x^{l+1}}\frac{\partial (x^{l+1})}{\partial x^l}=e^lf'(u)$$(1.10)
6. 代码部分 6.1 结构分析可以看到将神经网络拆分成几个计算层后,每一层都有两个导数需要计算:反向传播误差与可训练参数的导数。每一层又分为两个部分,第一个部分用于计算正向传播过程,第二个部分用于计算反向传播过程。导数与误差均在反向传播过程之中计算,全链接层包括四个组件:矩阵相乘、偏置相加、激活函数、损失函数:
6.2 矩阵相乘 def _matmul(self, inputs, W, *args, **kw): """ 正向传播 """ return np.dot(inputs, W) def _d_matmul(self, in_error, n_layer, layer_par): """ 反向传播 """ W = self.value[n_layer] inputs = self.outputs[n_layer] self.d_value[n_layer] = np.dot(inputs.T, in_error) error = np.dot(in_error, W.T) return error def matmul(self, filters, *args, **kw): self.value.append(filters) self.d_value.append(np.zeros_like(filters)) self.layer.append((self._matmul, None, self._d_matmul, None)) self.layer_name.append("matmul")
6.3 加入偏置
def _bias_add(self, inputs, b, *args, **kw): return inputs + b def _d_bias_add(self, in_error, n_layer, layer_par): self.d_value[n_layer] = np.sum(in_error, axis=0) return in_error def bias_add(self, bias, *args, **kw): self.value.append(bias) self.d_value.append(np.zeros_like(bias)) self.layer.append((self._bias_add, None, self._d_bias_add, None)) self.layer_name.append("bias_add")
6.4 通过激活函数
def _sigmoid(self, X, *args, **kw): return 1/(1+np.exp(-X)) def _d_sigmoid(self, in_error, n_layer, *args, **kw): X = self.outputs[n_layer] return in_error * np.exp(-X)/(1 + np.exp(-X)) ** 2 def sigmoid(self): self.value.append([]) self.d_value.append([]) self.layer.append((self._sigmoid, None, self._d_sigmoid, None)) self.layer_name.append("sigmoid")
6.5 loss 函数
为了简便,使用二范数作为 $loss$ 函数:
def _loss_square(self, Y, *args, **kw): B = np.shape(Y)[0] return np.square(self.outputs[-1] - Y)/B def _d_loss_square(self, Y, *args, **kw): B = np.shape(Y)[0] return 2 * (self.outputs[-2] - Y) def loss_square(self): self.value.append([]) self.d_value.append([]) self.layer.append((self._loss_square, None, self._d_loss_square, None)) self.layer_name.append("loss")
6.6 代码集成
将上面所叙述的代码进行集成,集成过程需要用到的函数为正向、反向传播代码:
def forward(self, X): self.outputs = [] self.outputs.append(X) net = X for idx, lay in enumerate(self.layer): method, layer_par, _, _ = lay net = method(net, self.value[idx], layer_par) self.outputs.append(net) return def backward(self, Y): error = self.layer[-1][2](Y) self.n_layer = len(self.value) for itr in range(self.n_layer-2, -1, -1): _, _, method, layer_par = self.layer[itr] error = method(error, itr, layer_par)
训练过程需要不断的正向传播-反向传播循环,并将所计算的可训练参数的偏导数加入原有变量之中,这称之为随机梯度下降法,整个执行为:
$$w^{new}\leftarrow w^{old}-\eta \cdot dw$$(1.11)
$1.11$ 为梯度下降法的标准迭代过程,$dw$ 为我们所计算的所有可训练参数的导数。
对应代码为:
def apply_gradient(self, eta): for idx, itr in enumerate(self.d_value): if len(itr) == 0: continue self.value[idx] -= itr * eta def fit(self, X, Y): self.forward(X) self.backward(Y) self.apply_gradient(0.1) def predict(self, X): self.forward(X) return self.outputs[-2]
将所有函数作为一个类,类变量里包含可训练参数 $vlaue$ 以及可训练参数偏导数 $d_value$。
class NN(): def __init__(self): # 所有可训练参数 self.value = [] # 可训练参数的导数 self.d_value = [] # 每一层输出 self.outputs = [] # 每一层所用函数 self.layer = [] # 层名 self.layer_name = []
7. 程序运行
程序运行过程,需要网络进行描述:
# 初始化值iw1 = np.random.normal(0, 0.1, [28, 28])ib1 = np.zeros([28])iw2 = np.random.normal(0, 0.1, [28, 28])ib2 = np.zeros([28])iw3 = np.random.normal(0, 0.1, [28, 2])ib3 = np.zeros([2])# 神经网络描述mtd = NN()mtd.matmul(iw1)mtd.bias_add(ib1)mtd.sigmoid()mtd.matmul(iw2)mtd.bias_add(ib2)mtd.sigmoid()mtd.matmul(iw3)mtd.bias_add(ib3)mtd.sigmoid()mtd.loss_square()# 训练for itr in range(100): ... mtd.fit(inx, iny)
运行结果
输出模型:Layer 0: matmulLayer 1: bias_addLayer 2: sigmoidLayer 3: matmulLayer 4: bias_addLayer 5: sigmoidLayer 6: matmulLayer 7: bias_addLayer 8: sigmoidLayer 9: loss
迭代 100 次后精度 96%。
接下来的内容- 卷积神经网络以及相关组件
- 多种迭代算法比如 Adam 实现
- 加入其它类型 loss 函数
本文首发于GitChat,未经授权不得转载,转载需与GitChat联系。
阅读全文: http://gitbook.cn/gitchat/activity/5af557a7e210d5096e9e789f
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。