Caffe、TensorFlow、PyTorch 等成熟的深度学习框架大行其道。使得很多小型团队可以在很短的时间内实现一个稳定高效的深度学习模型,这是技术普及和发展所带来的便利。但作为个人学习提高来讲,框架并非是一种良好的学习工具,其隐藏了太多重要的细节。而作为一门夕阳学科,深度学习有着一个成熟而系统知识体系。学习它并不像很多人渲染的需要太多数学知识,甚至于仅需要矩阵代数运算即可,深度学习是我见过对数学基础要求最低的机器学习分支。
本次 Chat 分为几个内容:
- 自动求导的计算图结构设计
- 神经网络的 NumPy 实现
- 一个卷积层和循环网络层有什么特殊的
适合人群: 所有人
1. 从简单的多层感知器开始
1.1 一个建模问题
首先给定数据 X 的形式为[600, 1]代表有 600 个样本每个样本一个特征,Y 的形式为[600, 1]。机器学问题就是如何估计 X 和 Y 之间的关系。假设二者的关系为一个线性关系,即:$$y=x\cdot w + b$$ 上式描述了一个非常通用的线性模型。在定义模型后,常需要给定一个模型输出和预测输出之间的接近程度的数学量,即损失函数(loss)$$loss=\frac{1}{B}\sum{i=1}^B(y{i}-d_{i})^2$$上式下标代表不同样本,B 为批尺寸(BATCHSIZE)。由此一个简单的线性回归模型即可以实现了:
import numpy as np # 定义数据X = np.random.normal(1, 1, [600, 1]) D = X ** 2 + np.random.normal(0, 0.3, [600, 1]) def model(x, w, b): """这是我们建立的模型""" y = x @ w + b return y def backward(x, d, w, b): """以 MSE 为 loss 的求导过程""" y = model(x, w, b) # dloss/dy dloss = 2 * (y-d) / len(x) # dw = dloss/dy * dy/dw dw = x.T @ dloss db = np.sum(dloss, axis=0) return dw, db, np.mean((y-d)**2)# 初始化w = np.random.random([1, 1]) b = np.zeros([1, 1]) # 优化BatchSize = 32 ## 学习率 eta = 0.01for step in range(100): idx = np.random.randint(0, len(X), BatchSize) inx = X[idx] ind = D[idx] dw, db, loss = backward(inx, ind, w, b) # 梯度下降法 w = w - eta * dw b = b - eta * db if (step+1)%10 == 0: print(f"Step:{step}, loss={loss}")import matplotlib.pyplot as plt plt.figure(figsize=[16, 12])plt.scatter(X, D, c="k", label="data", alpha=0.6)plt.xlabel("X", fontsize=26) plt.ylabel("Y", fontsize=26)xplt = np.linspace(-2, 4, 1000).reshape([1000, 1]) yplt = model(xplt, w, b) plt.plot(xplt, yplt, c="r", lw=3, label="fitting")plt.legend(loc="upper right", fontsize=26) plt.show()
绘制出结果如图:看起来还可以,当然也可以做一些特征,那么代码仅需稍作修改:
# 初始化,有两个特征需要处理w = np.random.random([2, 1]) b = np.zeros([1, 1]) # 优化BatchSize = 32 ## 学习率 eta = 0.01 ## 特征工程,构建平方特征X = np.concatenate([X, X**2], axis=1)
此时拟合结果如图实际上做特征这种方式,非常考验对于数据的了解程度。足够多的特征才能够拟合足够复杂的曲面。如果这些工作交给模型本身去做,就是深度学的思想了:让模型本身构建特征。这里最容易实现的便是多层感知器。
1.2 多层感知器和万能近似定理
万能近似定义描述了含有一个隐藏层的全连接网络可以拟合任意复杂的函数。这是一种新的建模思路,用复杂的模型自身去做特征工程,这个模型是:$$h^1 = x\cdot w^1+b^1\h=max(h^1,0) \y=h\cdot w^2+b^2$$如果此时输入的 x 依然是一个特征,而输出的 h 有 100 个特征即[600, 100],这意味着网络自身做了一个 100 个非线性特征。此时损失函数依然是均方误差(MSE)这种适用于回归问题的最简单的损失函数。首先来看一下代码:
def model(x, w1, w2, b1, b2): """ 含有一个隐藏层的多层感知器模型 w1,w2:可训练参数 b1,b2:偏置 """ # 隐藏层 h = x @ w1 + b1 # ReLU 激活函数 hrelu = np.clip(h, 0, np.inf) # 输出层 y = hrelu @ w2 + b2 return y, h, hreludef backward(x, d, w1, w2, b1, b2): """ 反向传播求解可训练参数的梯度 x,d:样本和标签 w1,w2:可训练参数 b1,b2:偏置 """ # 计算模型和中间结果 y, h, hrelu = model(x, w1, w2, b1, b2) # 定义 MSE 为损失函数 loss = np.mean((y-d)**2) # 计算反向传播误差 err1 = 2 * (y-d) / len(x) # 计算输出层可训练参数导数 dw2 = hrelu.T @ err1 db2 = np.sum(err1, axis=0) # 计算输出层反向传播误差 err2 = err1 @ w2.T mask = (h > 0).astype(np.float64) # 计算激活函数层反向传播误差 err3 = err2 * mask # 计算隐藏层可训练参数 dw1 = x.T @ err3 db1 = np.sum(err2, axis=0) return loss, dw1, dw2, db1, db2
拟合结果为:这是一种非常简单的拟合策略:无需复杂的特征工程,无需复杂的数学理论(严格来说是有的)。就可以实现一个深度学习算法。 而且效果还相当可以。
1.3 公式拆解以及计算图思想的引入
可以发现多层感知器可以拆解为更加简单的组件,每个组件单独有一个输出。输出间的依赖关系为:$$x\rightarrow h^1\rightarrow h \rightarrow y\rightarrow loss$$这种依赖关系是将多层感知器模型的拆解。而计算梯度的过程为:$$\frac{\partial loss}{\partial y}\rightarrow \frac{\partial y}{\partial h}\rightarrow \frac{\partial h}{\partial h^1}\rightarrow \frac{\partial h^1}{\partial x}$$
比如我想计算$loss$关于$h^1$的导数,那么实际上沿着反向的计算图直接走到相应的节点即可:$$\frac{\partial loss}{\partial h^1}=\frac{\partial loss}{\partial y}\cdot \frac{\partial y}{\partial h}\cdot \frac{\partial h}{\partial h^1}$$每一层几乎都仅需要计算本层向前传播的梯度,这是一种十分高效的计算方式。具体细节我们放到后面的章节进行详细说明,这里仅展示一个最简单的自动求导功能实现:
class Tensor: def __init__(self, data, depend=[]): """初始化""" self.data = data self.depend = depend self.grad = 0 def __mul__(self, data): """乘法""" def grad_fn1(grad): return grad * data.data def grad_fn2(grad): return grad * self.data depend = [(self, grad_fn1), (data, grad_fn2)] new = Tensor(self.data * data.data, depend) return new def __rmul__(self, data): def grad_fn1(grad): return grad * data.data def grad_fn2(grad): return grad * self.data depend = [(self, grad_fn1), (data, grad_fn2)] new = Tensor(self.data * data.data, depend) return new def __add__(self, data): """加法""" def grad_fn(grad): return grad depend = [(self, grad_fn), (data, grad_fn)] new = Tensor(self.data * data.data, depend) return new def __radd__(self, data): def grad_fn(grad): return grad depend = [(self, grad_fn), (data, grad_fn)] new = Tensor(self.data * data.data, depend) return new def __repr__(self): return f"Tensor:{self.data}" def backward(self, grad=None): """ 反向传播,需要递归计算 """ if grad == None: self.grad = 1 else: # 这一步用于计算图中的分支 self.grad += grad # 这一步是递归计算 for tensor, grad_fn in self.depend: bw = grad_fn(self.grad) tensor.backward(bw)
定义一个计算图
x = Tensor(2) f = x * x g = x * x y = f + gy.backward()print(y, g.grad, x.grad)
以上几乎已经可以完成自动求导功能了。
2. 当遇到卷积和循环网络层的时候怎么办
2.1 卷积层、循环网络层中计算图思想是否依然成立
当然是成立的,否则 TensrFlow 和 PyTorch 也不会强调自己是计算图了。那么卷积神经网络如何使用特征图呢?首先来看卷积函数$$h^{l+1}=h^l * w$$这假设 h 是卷积神经网络的输出,对于图像处理来说其格式通常为[批尺寸,通道,高,宽]。$w$为卷积核心,$*$为卷积算子。当我们将其看做一个图的局部时候,马上可以看到$h$是图的节点。$$\cdots \rightarrow h^l\rightarrow h^{l+1}\rightarrow \cdots$$我们仅需计算两个节点之间的导数即可。卷积计算非常容易实现的。由此可以发现,卷积的计算也是可以使用计算图表示的。 对于循环网络层的情况问题稍微复杂一些,对于最基础的循环网络层计算方式为:$$h{t}^{l+1}=\tanh(concat(h^{l}{t}, h^{l+1}{t-1})\cdot w + b)$$这个计算图就稍微复杂一些了,注意这里$ht$代表某个时间步的矩阵,其节点是依赖于时间的:$$\begin{matrix}\cdots \rightarrow h^{l+1}{t-1}\rightarrow h{t}^{l+1}\\cdots \rightarrow h^{l}_{t}\nearrow \end{matrix}$$可以看到由于时间维度的依赖,导致这个链式求导的层级非常深。这也是循环神经网络不招人喜欢的地方。但是这依然可以使用计算图来表示,只是时间步一多节点可能会非常多。
2.2 实现一个机器学习库的关键函数是什么
通过上面的例子可以发现实现一个基础的深度学习库的关键函数是很少的,这里分为四类。
激活函数:
- 这几乎是所有机器学习库都必须实现的,神经网络的非线性就来源于此。
- 这些函数包括常用的$e$指数和导数,可以实现 tanh 和 sigmoid 激活函数。当然也可以有很多快速实现的算法。$e$指数对于交叉熵的计算也是重要的。
- max(a, b)函数和导数。这是 LeakyRLU 和 ReLU 函数的关键函数。
- 卷积函数:
- 卷积函数是现今机器学习库的基础
- 其包括 1~3 维卷积,可以覆盖绝大部分场景。
- 扩张卷积和深度可分离卷积这是模型压缩的基础
- 矩阵乘法:
- 可以用来实现全连接层和循环网络层
代数计算以上几个关键计算完成后,一个基础但是完整的机器学习库几乎可以算作搭建完成了。
3. 总结和一些想说的
开始这个话题的时候想了挺久。因为最近写的书都是长篇大论,一个卷积神经网络实现就写了一章。这反而对于一些读者并不是很友好,毕竟人的精力是有限的。因此梳理了一个小的主线,并总结到了这里。很多人对于机器学习和数学的关系处理的并不好,包括一些教程也是。在下笔之前参阅了一些前人写的数学基础。比如有人写的“小白入门*”,讲了 PCA,讲了矩阵逆,甚至于还讲了遗传算法和拟牛顿法。但这并不是深度学习的数学基础,深度学习极少用到上面的概念。
深度学习是一种简单的机器学习算法,甚至可能是唯一能够被所有人实现的算法,无论找何理由都应该实现一下。即使是重复造轮子,即使其速度是非常缓慢的。实现了后才会有信心学习变分、图网络等稍微复杂的内容。
而深度学习瓶颈期的到来。越来越多的深度学习应用开始向低功耗场景下发展。未来需要什么技能还不好说(当然深度学习开发越来越底层是趋势),但仅会某些框架是没有知识壁垒的。
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
阅读全文: http://gitbook.cn/gitchat/activity/5dcbef0f58f21075f16552be
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。