本书主要用numpy来处理运算,用matplotlib将结果可视化。对于没有基础的人十分友好,只需要高中的线代知识就可以看懂。
1.x=x.flatten() 将矩阵及张量转换为一维数组。x>15,结果得到一个由True和False组成的一维列表,x[x>15]即可输出所有大于15的成员。
矩阵与一维数组的乘,(数组长度与矩阵列数相同)将一维数组扩张为矩阵形状后简单相乘,符合交换率,符号为;
矩阵与数组dot乘,(数组长度与矩阵列数相同)将一维数组与每行对应索引相乘求和,得到一维数组,长度为矩阵的行数:如[[1, 2], [3, 4], [7, 8]] dot [5,6],则得[15+26, 35+46,75+86],不符合交换律,用np.dot计算。
一次绘图流程:p42
2.感知机:接受一个或多个输入,输出一个信号。有多个输入时,可以给各个输入配置权重。机器学习就是人给机器一些数据,机器根据这些数据选出权重的过程。
P56从图像上,感知机是一条直线,有局限性。叠加多个感知机可以构造类似曲线。
3.对于单层感知机,实际上有两个函数,比如首先有y=b+x1w1+x2w2,其中b为偏置,w1、w2为权重;然后通过激活函数,比如h(x)=1或0(当y<=0时为0,否则为1。这种函数的输出是跳跃的,可称为阶跃函数)。若把激活函数换成其他形式,就接近神经网络的概念了。常用的一个激活函数(sigmoid函数):1/(1+exp(-x)),其中exp(-x)为e的-x次方。
接受numpy数组为参数的阶跃函数见p69。
激活函数也可以使用ReLU函数,其在x<=0时输出0,在x>0时则为线性关系。P74
对于输出层的激活函数,如果是分类问题(类似阶跃函数)用softmax函数,回归问题(连续输出数值)用恒等函数。P89
softmax函数的输出是连续的,总和为1,可以看作概率。为了节省算力,对未知模型进行预测时通常也可以省略输出层的softmax激活函数(接受数据进行学习时可以不省略)。
在分类问题中,输出层的神经元数量通常等于类别的数量。哪个神经元的输出最大,就会归类为该类别。
4.正规化:将数据进行处理,使其限制在某个范围内。正规化是一种预处理。
批处理:相同的神经网络,多个输出,多个结果。批处理可以大大加快运算速度。P101
5.损失函数:描述当前神经网络与测试数据(即监督数据)在多大程度上不拟合。一般可用均方误差函数和交叉熵。均方考虑了非正确解、越接近0(越小)越好,交叉熵只包含正确解、越接近0(越小)越好。
机器学习就是要使得损失函数值最小。如果训练数据太多,可以从中随机选择部分数据来学习,称为mini batch。
不用精确度作为评价标准,因为要设定参数的导数(变化梯度),如果用精确度为指标,容易导致导数为0,参数无法更新(精确度常常是跳跃的,微调无法产生影响,而损失函数是连续变化的)。这也是sigmoid函数优于阶跃函数的地方(激活函数)。
对于矩阵,y[0,2]的写法等同于y[0][2]
梯度指示的方向是损失函数减少量最多的地方,不一定指向损失函数值最小的地方。所以,机器学习可能会陷入局部最小值、鞍点或者学习平原。
鞍点处的梯度也为0,比如一个马鞍形曲面,(0,0,0)处的形状就很像马鞍。
梯度,就是一个一维数组。一个函数有多个输入,那么梯度就是这个多维函数(在某一点处)的导数。梯度数组的大小和输入多少相等,意义是对每一个输入,在保持其他输入不变时求该输入的导数,最终组成一个导数数组。为了使损失函数减小,需要沿负梯度方向下降。
6.反向传播法算梯度的速度比微积分法快许多,主要应用了复合函数的求导(相乘)。
7.有时SGD(随机梯度下降),即沿着梯度最大的地方下山的方法,不一定能最快下山。还有其他一些方法。
Momentum(动量法),如果某一方向上的梯度较小,但是持续很久,就适合用这种方法。模拟静止小球在山中的运动,某一方向累计的梯度会给小球积累该方向的速度,于是在该方向上移动越来越快。
AdaGrad(Adaptive适应梯度法),对梯度中的每一个导数都有一个学习率,该学习率还会不断衰减。AdaGrad是通过记录过去所有梯度的平方和实现的,最终学习率会衰减到0。不想让学习率衰减太快可以用RMSProp法,其会更多地考虑最近的梯度、逐渐减小久远梯度的权重。
Adam(直观上讲,类似于融合了AdaGrad与Momentum法,2015年提出),甚至可对一些超参数调校。
一些技巧:
(1)权重的初始值:对于sigmoid或tanh等激活函数,使用Xavier方法,上一层的节点个数为n,本层的初始值大概是标准差1/sqrt(n)的高斯分布;
对relu激活函数,可以使用He初始值。
在手头数据集少时,可以“学习迁移”,即训练完一个数据集,将权重的最终值作为下一次学习的初始值。
(2)调整激活值的分布:使用batch normalization,优点是增大学习率(学的快)、对初始值不会过于敏感、抑制过拟合。
(3)抑制过拟合:过拟合主要是由于训练数据少、网络参数太多(层数多、节点多等)。过拟合的表现常常是训练数据准确度高,测试数据准确度不佳。对简单网络,常用权值衰减来抑制过拟合。对复杂网络,可用Dropout方法,随机删除一些节点。另外,可以用已有的数据生成新的数据,比如对手写数字图片,可对图片进行平移、旋转等操作。
(4)高效寻找超参数:对于网络层数与节点数、batch大小、学习率、权值衰减等超参数,需要高效寻找之。可在训练数据和测试数据之外再分一个验证数据,用来对探讨超参数取什么值,然后让超参数不断*10,在对数尺度上变化,并观测其对学习的影响。一般来说,测试数据应该只在最后用一次。
(5)GPU运算:GPU擅长大量的并行运算,比如矩阵的乘法;而CPU擅长连续的复杂运算。使用GPU,可以将训练和测试时间大大缩短。据书中称,NVIDIA的显卡比AMD的更合适。
(6)分布式学习:和用GPU一样,可缩短学习时间。Google的TensorFlow和微软的CNTK都很重视分布式学习。
8.CNN(Convolutional Neural Network),书中之前实现的网络,隐藏层的结构是Affine+Relu(或者其他激活函数),最后一层是Affine+Softmax,而CNN的隐藏层是Conv(卷积层)+Relu+Pooling(池化层),最后一层不变。普通Affine的问题是没有考虑图片的形状,比如矩阵中相距较远的两个点关联度较小,近大。从原理上,卷积层使用了类似滤波器的方法,用了矩阵的*乘,并且偏置只有一个。*乘,即相同大小矩阵的对应位置乘积之和。
池化层则是将层中的数据减少形成特征化的数据,比如只取最大值、只取平均值。
CNN是图像识别中常用的操作。通过观察,CNN的不同Conv层,大概在进行从基础到抽象的观察。
在各种CNN中,书中介绍了CNN元组LeNet及2012年提出的AlexNet。据称,AlexNet是深度学习热潮的导火索。
Google旗下的Deep Mind公司,开发出了AlphaGo和DQN。其中DQN可以接受游戏的图像输入(比如每秒4帧),学习如何玩游戏,甚至可以做出超越人类的手柄操作。
附上看完书后跟着写出来的通用框架:(还可以改的更简单)
from typing import OrderedDict
import numpy as np
import matplotlib.pylab as plt
def step_function(x): # 激活函数-阶跃函数
return np.array(x > 0, dtype=np.int)
def sigmoid(x): # 激活函数-sigmoid连续函数
return 1 / (1 + np.exp(-x))
class Sigmoid:
def __init__(self) -> None:
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
def sigmoid_grad(x):
return (1.0 - sigmoid(x)) * sigmoid(x)
def ReLU(x): # 激活函数-ReLU函数
return np.maximum(0, x)
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
def softmax(x): # 激活函数-softmax函数
if x.ndim == 2: # 如果传递的是batch而非单个图片的识别结果,则需要一点处理
x = x.T # x.T是翻转后的矩阵,如[[1, 2, 3], [4, 5, 6]]变成[[1 4],[2 5],[3 6]]
# 用x.T是因为广播功能对数组扩增是扩增其行数而非列数
x = x - np.max(x, axis=0)
# np.max(x, axis=0)是一个一维数组,通过广播功能使其扩增
y = np.exp(x) / np.sum(np.exp(x), axis=0)
# np.sum(np.exp(x), axis=0)也是一个一维数组,也通过广播功能使其扩增
return y.T
# 对单个图片处理较简单
x = x - np.max(x) # 减最大值是为了防止溢出,即e^x指数过大
return np.exp(x) / np.sum(np.exp(x))
def mean_square_error(y, t): # 损失函数-均方误差
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
return 0.5 * np.sum((y - t)**2) / y.shape[0]
def cross_entropy_error(y, t): # 损失函数-交叉熵函数
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
delta = 1e-7 # 避免ln(0)出现负无穷-inf
return -np.sum(t * np.log(y + delta)) / y.shape[0]
def numerical_diff(f, x): # 数值微分求导-中心差分
h = 1e-4
return (f(x + h) - f(x - h)) / (2 * h)
def numerical_gradient(f, x): # 求梯度。梯度相当于对函数每个输入加一个带正负的权重,表明改动每个输入的影响
h = 1e-4
grad = np.zeros_like(x)
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished: # 对每个x,保持其他x不变,求单个x的导数
idx = it.multi_index
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x) # f(x+h)
x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2 * h) # 这里求的是负梯度。正梯度用后减前
x[idx] = tmp_val # 还原值
it.iternext()
return grad # 最后得到导数数组,即梯度
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.original_x_shape = None
# 权重和偏置参数的导数
self.dW = None
self.db = None
def forward(self, x):
# 对应张量
self.original_x_shape = x.shape
x = x.reshape(x.shape[0], -1)
self.x = x
out = np.dot(self.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)
dx = dx.reshape(*self.original_x_shape) # 还原输入数据的形状(对应张量)
return dx
class SoftmaxWithLoss:
def __init__(self):
self.loss = None
self.y = None
self.x = 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
class ThreeLayerNet:
def __init__(self,
input_size,
hidden_size1,
hidden_size2,
output_size,
weight_init_std=0.01):
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(
input_size, hidden_size1)
self.params['b1'] = np.zeros(hidden_size1)
self.params['W2'] = weight_init_std * np.random.randn(
hidden_size1, hidden_size2)
self.params['b2'] = np.zeros(hidden_size2)
self.params['W3'] = weight_init_std * np.random.randn(
hidden_size2, output_size)
self.params['b3'] = np.zeros(output_size)
self.layers = OrderedDict()
self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
self.layers['Relu1'] = Relu()
self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
self.layers['Relu2'] = Relu()
self.layers['Affine3'] = Affine(self.params['W3'], self.params['b3'])
self.lastlayer = 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(x)
return self.lastlayer.forward(y, t)
def accuracy(self, x, t): # 监督数据的形式为batch*10
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
grads['W3'] = numerical_gradient(loss_W, self.params['W3'])
grads['b3'] = numerical_gradient(loss_W, self.params['b3'])
return grads
def gradient(self, x, t): # 反向传播法高速求梯度
# 正向一次
self.loss(x, t)
# 反向求梯度
dout = 1
dout = self.lastlayer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
grads = {}
grads['W1'] = self.layers['Affine1'].dW
grads['b1'] = self.layers['Affine1'].db
grads['W2'] = self.layers['Affine2'].dW
grads['b2'] = self.layers['Affine2'].db
grads['W3'] = self.layers['Affine3'].dW
grads['b3'] = self.layers['Affine3'].db
return grads
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]
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]
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)
class Adam: # 类似于融合了AdaGrad和Momentum
"""Adam (http://arxiv.org/abs/1412.6980v8)"""
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0
self.m = None
self.v = None
def update(self, params, grads):
if self.m is None:
self.m, self.v = {}, {}
for key, val in params.items():
self.m[key] = np.zeros_like(val)
self.v[key] = np.zeros_like(val)
self.iter += 1
lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (
1.0 - self.beta1**self.iter)
for key in params.keys():
#self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
#self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
#unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
#unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
#params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)
if __name__ == '__main__':
from check_download_pickle_mnist import load_mnist
(i_train, l_train), (i_test, l_test) = load_mnist(one_hot_label=True)
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = 60000 // 100
network = ThreeLayerNet(input_size=784,
hidden_size1=50,
hidden_size2=100,
output_size=10)
optimizer = Adam()
for i in range(10000):
batch_mask = np.random.choice(60000, 100)
i_batch = i_train[batch_mask]
l_batch = l_train[batch_mask]
grads = network.gradient(i_batch, l_batch)
params = network.params
optimizer.update(params, grads)
loss = network.loss(i_batch, l_batch)
train_loss_list.append(loss)
if i % iter_per_epoch == 0:
train_acc = network.accuracy(i_train, l_train)
test_acc = network.accuracy(i_test, l_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print("train acc, test acc | " + str(train_acc) + ", " +
str(test_acc))
x = range(len(train_loss_list))
plt.rcParams['font.sans-serif'] = ['SimHei'] #显示中文标签
plt.rcParams['axes.unicode_minus'] = False #这两行需要手动设置
plt.plot(x, train_loss_list)
plt.xlabel('次数')
plt.ylabel('loss')
plt.show()
x = range(len(train_acc_list))
plt.plot(x, train_acc_list, label='train')
plt.plot(x, test_acc_list, linestyle='--', label='test')
plt.xlabel('epoch')
plt.ylabel('acc')
plt.legend()
plt.show()