为了完成毕设, 最近开始入门深度学习.
在此和大家分享一下本人阅读鱼书时的笔记,若有遗漏,欢迎斧正!
若转载请注明出处!
感知机(perceptron)接收多个输入信号,输出一个信号。
如图感知机,其接受两个输入信号。其中 \(\theta\) 为阈值,超过阈值 神经元就会被激活。
感知机的局限性在于,它只能表示由一条直线分割的空间,即线性空间。多层感知机可以实现复杂功能。
神经网络由三部分组成:输入层、隐藏层、输出层
激活函数将输入信号的总和转换为输出信号,相当于对计算结果进行简单筛选和处理。
如图所示的激活函数为阶跃函数。
sigmoid函数是常用的神经网络激活函数。
其公式为:
\[h(x)=\frac{1}{1+e^{-x}} \]如图所示,其输出值在 0到 1 之间。
ReLU(Rectified Linear Unit)函数是最近常用的激活函数。
该神经网络包括:输入层、2 个隐藏层和输出层。
def forward(network, x): # x为输入数据 # 第1个隐藏层的处理,点乘加上偏置后传至激活函数 a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) # 第2个隐藏层的处理 a2 = np.dot(z1, W2) + b2 z2 = sigmoid(a2) #输出层处理 identidy_function原模原样输出a3 a3 = np.dot(z2, W3) + b3 y = identify_function(a3) return y # y为最终结果
一般来说,回归问题选择恒等函数,分类问题选择softmax函数。
softmax函数的公式:
\[y_{k}=\frac{e^{a_{k}}}{\sum_{i=1}^{n}e^{a_{i}}} \]假设输出层有 \(n\) 个神经元,计算第 \(k\) 个神经元的输出 \(y_{k}\) 。
softmax函数的输出值的总和为 1。因此我们可以将它的输出解释为概率。
输出层神经元数量一般和设定类别数量相等。
使用 MNIST 数据集。
使用 pickle 包序列化与反序列化所需数据,可以加快读取速度。
正规化 Normalization:将数据限定到某个范围内。
将输入数据成批次打包,可以一次处理多张图片。
batch_size = 100 for in range(0, len(x), batch_size) # x为输入数据 x_batch = x[i:i+batch_size] # 切片处理,一次取batch_size张图片 y_batch = predict(network, x_batch) p = np.argmax(y_batch, axis = 1)
学习是指从训练过程中自动获取最优权重参数的过程。
从图像中提取特征量(SIFT、SURF 或 HOG),使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的 SVM、KNN 等分类器进行学习。
神经网络将损失函数作为指标来寻找最优权重参数。
神经网络学习的目的就是尽可能地降低损失函数的值。
我们一般使用均方误差和交叉熵误差函数。
Mean Squared Error。
\[E=\frac{1}{2}\sum_{k}(y_{k}-t_{k})^2 \]\(y_{k}\) 表示神经网络的输出结果, \(t_{k}\) 表示正确解标签,\(k\) 表示数据维度。
one-hot表示:正确解标签表示为 1,其他标签表示为 0。
如:
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] # 假设在进行数字识别,数字“2”为正确结果 y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
Cross Entropy Error
\[E=-\sum_{k}t_{k}\log y_{k} \]\(y_{k}\) 表示神经网络的输出结果, \(t_{k}\) 表示正确解标签。
如果我们要求所有训练数据的平均损失函数,以交叉熵误差为例,则为:
\[E=-\frac{1}{N}\sum_{n}\sum_{k}t_{nk}\log y_{nk} \]我们可以从全部数据中选出一部分,作为全部数据的代表。这一部分就是 mini-batch。
好比抽样调查。
train_size = x_train.shape[0] # 训练集的全部数据个数 batch_size = 10 #mini-batch的大小 batch_mask = np.random.choice(train_size, batch_size)#该函数从train_size个数字随机挑选batch_size个数 x_batch = x_train[batch_mask] t_batch = t_train[batch_mask]
使用中心差分来近似求解导数。
def numerical_diff(f, x) #求函数f(x)在x处的导数 h = 1e-4 #微小值 return (f(x+h)-f(x-h)) / (2 * h)
由全部变量的偏导数汇总成的向量称为梯度。
如,对于函数 \(f(x,y)=x^2+y^2\) ,其在 \((x,y)\) 处的梯度为 \((\frac{\partial f}{\partial x},\frac{\partial f}{\partial y})\)
其 Python 实现如下所示:
def numerical_gradient(f, x): h = 1e-4 grad = np.zeros_like(x) #生成和变量组x大小相同的空数组存放梯度 for idx in range(x.size): tmp_val = x[idx] # f(x+h) x[idx] = tmp_val + h fxh1 = f(x) # f(x-h) x[idx] = tmp_val - h fxh2 = f(x) # 计算x[idx]的偏导数 grad[idx] = (fxh1 - fxh2) / (2*h) x[idx] = tmp_val # 还原值
梯度指向各点处的函数值减少最多的方向。
我们通常沿着梯度方向,使用梯度下降法循环寻找损失函数的最小值。
以上面提到的函数为例,用下面的式子不断更新梯度值:
\[x=x-\eta\frac{\partial f}{\partial x}\\ y=y-\eta\frac{\partial f}{\partial y}\\ \]\(\eta\) 是一个更新量,称为学习率。学习率的初始值一般为 0.01 或 0.001
用Python实现梯度下降法如下:
# f为函数,init_x为初始的变量组,学习率0.01,循环100次 def gradient_descent(f, init_x, learning_rate = 0.01, step_num = 100): x = init_x for i in range(step_num): grad = numerical_gradient(f, x) x = x - lr*grad return x
神经网络的学习要求损失函数关于权重参数的梯度。
比如一个 2*3 的权重参数 \(W\),损失函数为 \(L\) ,则梯度 \(\frac{\partial L}{\partial W}\) 为:
\[W=(\begin{matrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \end{matrix})\\ \frac{\partial L}{\partial W} = (\begin{matrix} \frac{\partial L}{\partial w_{11}} & \frac{\partial L}{\partial w_{12}} & \frac{\partial L}{\partial w_{13}} \\ \frac{\partial L}{\partial w_{21}} & \frac{\partial L}{\partial w_{22}} & \frac{\partial L}{\partial w_{23}} \end{matrix}) \]动态调整权重和偏置以拟合训练数据过程称为学习。共有四个步骤:
假设一个神经网络有两个权重参数 \(W1\) 和 \(W2\),两个偏置参数 \(b1\),$ b2$ :
class TwoLayerNet: # 计算并返回网络输出值 def predict(self, x): a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) a2 = np.dot(z1, W2) + b2 y = softmax(a2) return y # 计算损失值 t为正确解标签 def loss(self, x, t): y = self.predict(x) return cross_entropy_error(y, t) # 计算梯度 def count_gradient(self, x, t): loss_W = lambda W: self.loss(x, t) # 计算梯度 其他参数省略号 grads['W1'] = numerical_gradient(loss_W, params['W1'])
mini-batch的实现:
# 超参数 iters_num = 10000 # 下降次数 train_size = x_train.shape[0] batch_size = 100 learning_rate = 0.1 network = TwoLayerNetwork(input_size = 784, hidden_size = 50, output_size = 10) for i in range(iters_num): # 获取mini-batch batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask] # 计算梯度 grad = network.count_gradient(x_batch, t_batch) # 更新参数 for key in ('W1','b1','W2','b2'): network.params[key] -= leraning_rate * grad[key]
一个 epoch 表示学习中所有训练数据均被使用过的一次时的更新次数。
使用误差反向传播法能够快速计算权重参数的梯度。
其基于链式法则。
class Relu: def __init__(self): self.mask = None # 前向传播 def forward(self, x): self.mask = (x <= 0) out = x.copy() out[self.mask] = 0 return out # 反向传播 def backward(self,dout): dout[self.mask] = 0 dx = dout return dx
class Sigmoid: def __init__(self): self.out = None # 前向传播 def forward(self, x): out = 1 / (1 + np.exp(-x)) self.out = out return out # 反向传播 def backward(self, dout): dx = dout * (1.0 - self.out) * self.out return dx
神经网络正向传播的流程是根据输入数据和权重、偏置计算加权和,经过激活函数后输出至下一层。
其中进行的矩阵的乘积运算在几何学领域被称为仿射变换,因此我们将进行仿射变换的处理实现为Affine层。
class Affine: def __init__(self, W, b): self.W = W self.b = b self.x = None self.dW = None self.db = None # 前向传播 def forward(self, x): self.x = x out = np.dot(x, self.W) + self.b return out # 反向传播 def backward(self, dout): dx = np.dot(dout, self.W.T) self.dW = np.dot(self.x.T, dout) self.db = np.sum(dout, axis = 0) return dx
softmax函数将输入值正规化后输出。考虑到这里也包含作为损失函数的交叉熵误差,因此称为 Softmax-with-Loss层。
class SoftmaxWithLoss: def __init__(self): self.loss = None self.y = None self.t = None # 前向传播 def forward(self, x, t): self.t = t self.y = softmax(x) self.loss = cross_entropy_error(self.y, self.t) return self.loss # 反向传播 def backward(self, dout = 1): batch_size = self.t.shape[0] dx = (self.y - self.t) / batch_size return dx
神经网络的学习目的是找到使损失函数的值尽可能小的参数,这个过程被称为最优化 Optimization
常用方法有SGD、Momentum、AdaGrad和Adam等。
随机梯度下降法。
\[W\gets W-\eta\frac{\partial L}{\partial W} \]class SGD: def __init__(self, lr = 0.01): self.lr = lr def update(self, params, grads): for key in params.keys(): params[key] -= self.lr * grads[key]
SGD方法的缺点在于梯度的方向并没有志向最小值的方向。Momentum 是动量的意思。
其数学式如下:
\[v \gets \alpha v-\eta\frac{\partial L}{\partial W}\\ W \gets W+v \]表示物体在梯度方向上受力。
class Momentum: def __init__(self, lr = 0.01, momentum = 0.9): self.lr = lr self.momentum = momentum self.v = None def update(self, params, grads): if self.v is None: self.v = {} for key, val in params.items(): self.v[key] = np.zeros_like(val) for key in params.keys(): self.v[key] = self.momentum * self.v[key] - self.lr * grads[key] params[key] += self.v[key]
AdaGrad 方法保留了之前所有梯度值的平方和,会为参数的每个元素适当调整学习率。
Ada 表示 Adaptive
\[h \gets h+\frac{\partial L}{\partial W}\bigodot \frac{\partial L}{\partial W}\\ W \gets W-\eta\frac{1}{\sqrt{h}}\frac{\partial L}{\partial W} \]class AdaGrad: def __init__(self, lr = 0.01): self.lr = lr self.h = None def update(self, params, grads): if self.h is None: self.h = {} for key, val in params.items(): self.h[key] = np.zeros_like(val) for key in params.keys(): self.h[key] += grads[key] * grads[key] params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
Adam是最近出现的一种参数更新方法,它会设置三个超三处。
各层的激活值的分布要求有适当的广度,否则可能会出现梯度消失现象。
在一般的深度学习框架中, Xavier初始值已被作为标准使用。
在 Xavier 初始值中,如果前一层的节点数为 \(n\),则初始值使用标准差为 \(\frac{1}{\sqrt{n}}\) 的分布。
node_num = 100 w = np.random.randn(node_num, node_num) / np.sqrt(node_num)
当激活函数使用 ReLU 时,一般使用 He 初始值。
如果前一层的节点数为 \(n\),则初始值使用标准差为 \(\sqrt{\frac{2}{n}}\) 的高斯分布。
为了使各层的激活值的分布有适当的广度,使用 Batch Normalization 方法进行强制调整。
因此,我们需要在 Affine 层和激活函数层之间插入一个 Batch Norm 层。以进行学习时的 mini-batch 为单位进行正则化。
\[\mu_{B}\gets\frac{1}{m}\sum_{i=1}^m x_{i}\\ \sigma_{B}^2\gets\frac{1}{m}\sum_{i=1}^m (x_{i}-\mu B)^2\\ \hat{x_{i}}\gets\frac{x_{i}-\mu B}{\sqrt{\sigma_{B}^2+\varepsilon}} \]对 mini-batch 的 \(n\) 个输入数据的集合 \(B={x_{1},x_{2},...,x_{m}}\) 求均值 \(\mu B\) 和 方差 \(\sigma_{B}^2\)。
机器学习中,过拟合是一个很常见的问题。过拟合指的是只能拟合训练数据,但不能很好地拟合不包含在训练数据中的其他数据的状态。
因此我们需要一些方法来抑制过拟合。权值衰减是方法之一。
对于所有权重,权值衰减方法都会为损失函数加上 \(\frac{1}{2}\lambda W^2\),即权重的 \(L2\) 范数。
因此在求权重梯度的计算中,要为之前的误差反向传播法的结果加上正则化项的导数 \(\lambda W\)。
网络模型复杂时,使用Dropout方法抑制过拟合。
Dropout是一种在学习过程中删除神经元的方法。训练时,随机选出隐藏层的神经元并将其删除。被删除的神经元不再进行信号的传递。
class Dropout: def __init__(self, dropout_ratio = 0.5): self.dropout_ratio = dropout_ratio self.mask = None def forward(self, x, train_flg = True): if train_flg: self.mask = np.random.rand(*x.shape) > self.dropout_ratio return x * self.mask else: return x * (1.0 - self.dropout_ratio) def backward(self, dout): return dout * self.mask
超参数有神经元数量、batch大小、学习率等。
我们不能使用测试数据评估超参数的性能,否则会造成过拟合
调整超参数时,必须使用超参数专用的确认数据,称为验证数据 validation data
CNN 的结构可以像积木一样进行组装。其中出现了卷积层 Convolution 和池化层 Pooling。
在 CNN 中,层的连接顺序是 :Convolution - ReLU - Pooling.
Pooling有时被省略。
在全连接层中,数据的形状被忽略了。而卷积层可以保持形状不变。当输入数据是图像时,卷积层以 3 维数据的形式接收输入数据,并同样以 3 维数据的形式输出至下一次。
卷积层的输入输出数据称为特征图 Feature Map。
卷积运算相当于过滤器。
滤波器即输出中的权重W。
滤波器会提取边缘或斑块等原始信息。
如图,输入数据大小是 \((5,5)\),滤波器大小是 \((3,3)\),输出大小是 \((3,3)\)。
在进行卷积层处理前,有时要像输入数据的周围填入固定数据来扩充数据。
使用填充主要是为了调整输出的大小。
应用滤波器的位置间隔称为步幅。
增大步幅后,输出表小;增大填充后,步幅变化。
假设输入大小为 \((H,W)\) ,滤波器大小为 \((FH,FW)\),输出大小为 \((OH,OW)\),填充为 \(P\) ,步幅为 \(S\)。
则输出大小为:
\[OH=\frac{H+2P-FH}{S}+1\\ OW=\frac{W+2P-FW}{S}+1 \]以 3 通道 RGB 图像为例,其纵深方向上的特征图增加了。通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加。
输入数据的通道数和滤波器的通道数要相同。
当有多个滤波器时,输出特征图同样有多层。
池化是缩小高、长方向上的空间运算。简单来说,池化用来精简数据。
Max池化上获取最大值的运算。一般来说,池化窗口大小会和步幅相同。
一个关键的函数为 im2col。它将输入的三维数据展开为二维矩阵以适合滤波器。
class Convolution: def __init__(self, W, b, stride = 1, pad = 0): self.W = W self.b = b self.stride = stride self.pad = pad def forward(self, x) FN, C, FH, FW = self.W.shape # 滤波器的数量、通道数、高、长 N, C, H, W = x.shape # 输入数据的数量、通道数、高、长 # 计算输出数据的长和高 out_h = int(1 + (H + 2 * self.pad - FH) / self.stride) out_w = int(1 + (W + 2 * self.pad - FW) / self.stride) # 使用im2col将三维数据转换为矩阵 col = im2col(x, FH, FW, self.stride, self.pad) col_W = self.W.reshape(FN, -1).T out = np.dot(col, col_W) + self.b # 乘以权重后加上便宜 out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) # 重新变为三维数据 return out
class Pooling: def __init__(self, pool_h, pool_w, stride = 1, pad = 0): self.pool_h, self.pool_w, self.stride, self.pad = pool_h, pool_w, stride, pad def forward(self, x): N, C, H, W = x.shape # 计算输出数据的长和高 out_h = int(1 + (H - self.pool_h) / self.stride) out_w = int(1 + (W - self.pool_w) / self.stride) # 展开 col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) col = col.reshape(-1, self.pool_h * self.pool_w) # 最大值 out = np.max(col, axis = 1) # 转换 out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2) return out
手写数字识别的CN
class SimpleConvNet: def __init__(self, input_dim = (1, 28, 28), conv_param = {'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}, hidden_size = 100, output_size = 10, weight_init_std = 0.01): """ :param input_dim: 输入数据的通道数与长高 :param conv_param: 卷积层的参数,滤波器数量、维度、填充、步幅 :param hidden_size: 隐藏层神经元数量 :param output_size: 输出层神经元数量 :param weight_init_std: 初始化权重标准差 """ filter_num = conv_param['filter_num'] filter_size = conv_param['filter_size'] filter_pad = conv_param['pad'] filter_stride = conv_param['stride'] input_size = input_dim[1] conv_output_size = (input_size - filter_size + 2 * filter_pad) / filter_stride + 1 pool_output_size = int(filter_num * (conv_output_size / 2) * (conv_output_size / 2)) self.params = {'W1': weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size), 'b1': np.zeros(filter_num), 'W2': weight_init_std * np.random.randn(pool_output_size, hidden_size), 'b2': np.zeros(hidden_size), 'W3': weight_init_std * np.random.randn(hidden_size, output_size), 'b3': np.zeros(output_size)} self.layers = OrderedDict() self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'], self.params['stride'], self.params['pad']) self.layers['ReLU1'] = Relu() self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2) self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2']) self.layers['ReLU2'] = Relu() self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3']) self.last_layer = SoftmaxWithLoss() def predict(self, x): for layer in self.layers.values(): x = layer.forward(x) return x def loss(self, x, t): y = self.predict(t) return self.last_layer.forward(y, t) # 反向传播求梯度 def gradient(self, x, t): # forward self.loss(x, t) # backward dout = 1 dout = self.last_layer.backward(dout) layers = list(self.layers.values()) layers.reverse() for layer in layers: dout = layer.backward(dout) grads = {'W1': self.layers['Conv1'].dW, 'b1': self.layers['Conv1'].db, 'W2': self.layers['Affine1'].dW, 'b2': self.layers['Affine1'].db, 'W3': self.layers['Affine2'].dW, 'b3': self.layers['Affine2'].db} return grads