目录
一、从优化问题讲起
1.1 牛顿与开普勒的对话
1.2 拟合的数学模型
补充:分类的数学模型
1.3 如何通过训练数据优化模型参数
1.3.1 Mini-Batch in Action
补充 : TensorFlow简介
二、实战:拟合问题
2.1 TensorFlow的安装
2.2 TensorFlow的使用
2.3 TensorBoard可视化界面
2.4 Tensorflow解决拟合问题代码
三、参数优化方法
3.1 随机梯度下降 ( stochastic gradient decent,SGD )
补充知识:梯度
补充知识:如何调节学习速率
3.2 带动量的随机梯度下降 ( SGD w/ momentum )
3.3 AdaGrad 算法 ( adaptive gradient algorithm )
3.4 Adam 算法 ( adaptive moment estimation )
3.5 小结:
四、实战:优化方法比较
五、深度神经网络
5.1 谁来做特征提取
5.2 激活函数
5.2.1 Sigmoid和Tanh
5.2.2 ReLU激活函数
5.2.3 ReLu变种
5.3 神经网络
5.4.1 神经网络的数学本质
5.4.2 [深度] 学习
六、实战: 使用神经网络建模MNIST数据
小结
七、正则化方法
7.1 欠拟合与过拟合
7.2 如何评价模型性能
补充:三个集合的选择有何讲究
7.3 常用的正则化方法
7.3.1 增加数据的数量和种类
7.3.2 成本函数增加正则项
7.3.3 Early-Stopping
7.3.4 参数共享
7.3.5 系宗方法
7.3.6 更多方法
7.4 模型训练技巧
7.5 一言以蔽之:大模型 +正则化 > 小模型
补充:如何低成本获得更多训练数据
补充:神经网络的超参数
奥卡姆剃刀定律(Occam's Razor)
八、模型性能评价
8.1 具有欺骗性的平均值
8.2 概率分布与直方图
8.3 混淆矩阵
8.4 性能指标
8.5 ROC曲线
优化问题,也就是让计算机自动化寻找事物规律。
牛顿解决了线性拟合的问题,开普勒解决了非线性拟合的问题。
所谓的拟合就是要最大化,或者最小化某一个目标函数。
但是分类问题,在拟合问题的基础上,它的标签的取值更加离散。
交叉熵:
结合公式和图来看,对于一个标签为1的样本,如果预测的概率比较偏向于0,那么他将获得的惩罚是巨大的,见蓝色线的靠左部分。所以这样的一个成本函数对于分类问题是极其有效的。
TensorFlow是运用最广的深度学习框架之一。他还有另外一个同行叫做pytorch。他俩的区别就是TensorFlow比pytorch难写不止一点半点。而且TensorFlow它的版本迭代,很多版本之间是互相不兼容的。TensorFlow既服务于科研,又服务于生产。他还是多平台支持的,具有丰富的生态环境。
TensorFlow的运作方式是先构建计算图,这个计算图里面是没有东西的,只有当我们把数据送到里面的时候,开始一个session的计算的时候,他才有数据的流动。所以这跟我们平常的编程模式是有一些差别的。
那么接下里我们将以一个简单的拟合问题的例子向大家展示TensorFlow的基本用法和代码编写。
安装TensorFlow的时候需要注意python的版本是否适配:本课程用的版本是工程中运用比较广泛的1.12,当然1.14应该也没有任何问题。
TensorFlow版本和python版本的对应配置
Python虚拟环境的创建Anaconda上操作还是比较简单的,我们搭建深度学习的环境最好是有一个独立的虚拟环境,而不要直接在主环境上搭建。因为我们可能会搭建许多不同版本的环境。学会创建虚拟环境总是必要的。
接下来我们就可以直接在vscode上使用新的Python环境啦(直接切换解释器版本就好了):
vscode得先安装ipykernel才能使用.ipynb格式文件写代码。(它会自动提示的,install即可)
强烈推荐在Anaconda的主页Home那里安装一个对应环境的powershell或者cmd(我个人觉得这样最能节省点时间然后去学点重要的东西,那些环境切换来切换去的花里胡哨还是别去管的好...)
这里顺便再推荐一款效率神器:Quicker:它能够快速的执行一些常用的软件启动操作或者其他一些动作。我于是就把power shell的启动放在里面啦,按一下ctrl键直接打开Quicker界面:
实际执行安装TensorFlow的语句在powershell中就一条,没什么复杂的,等待安装成功即可:
pip install tensorflow==1.12
执行tf.__version__检验安装完成。
我们可以看到z只是一个数据图上的一个节点,里面并没有数据流,也就是没有计算出答案为7
那么怎么对计算图进行运行呢?——可以通过构建一个session,然后run执行。
定义一些常量:
原理就是:通过一个FileWriter对象把Session内部的graph图结构写到当前文件夹./下的introduction文件里,之后我们利用TensorBoard工具打开的就是这份包含了图结构的tfevents文件。
这里看到vscode很贴心地帮我们集成了TensorBoard的支持哈哈。
打开可以看到tensorboard显示的刚才定义的变量以及加法操作的张量Tensor图。
但是更推荐本地起一个tensorboard的服务,然后用浏览器登录,效果会好很多:
tensorboard --logdir=./basic_mnist --port=9090
使用 tensorboard 的好处:我们一般训练模型的时间是很长的,而 tensorboard 上的界面是在session会话一边写入的时候一边刷新的,所以就相当于,tensorboard给了我们一个自动监控的界面。通过监控界面,你可以实时地看到模型的收敛情况,你可以及时地对这个模型进行干预。
接下来我们便来解决如下的一个拟合问题,首先把拟合图像画出来:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
%pylab inline
train_X = np.asarray([30.0, 40.0, 60.0, 80.0, 100.0, 120.0, 140.0])
train_Y = np.asarray([320.0, 360.0, 400.0, 455.0, 490.0, 546.0, 580.0])
train_X /= 100.0
train_Y /= 100.0
def plot_points(x, y, title_name):
plt.title(title_name)
plt.xlabel('x')
plt.ylabel('y')
plt.scatter(x, y)
plt.show()
def plot_line(w, b, title_name):
plt.title(title_name)
plt.xlabel('x')
plt.ylabel('y')
x = np.linspace(0.0, 2.0, num=100)
y = w * x + b
plt.plot(x, y)
plt.show()
plot_points(train_X, train_Y, title_name='Training Points')
n_samples = train_X.shape[0]
X = tf.placeholder('float')
y = tf.placeholder('float')
W = tf.Variable(np.random.randn(), name='weight')
b = tf.Variable(np.random.randn(), name='bias')
y_pred = tf.add(tf.multiply(X, W), b)
定义模型学习方式:
loss = tf.reduce_sum(tf.pow((y - y_pred), 2)) / (2 * n_samples)
learning_rate = 0.01
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)
模型运行:
import random
def shuffled_X_Y(x, y):
# 将x和y打包成元组列表
data = list(zip(x, y))
# 打乱顺序
random.shuffle(data)
# 解包回x和y
x, y = zip(*data)
return x, y
training_epochs = 1000
display_step = 50
train_X_copy = train_X
# 你可能会觉得train_X是个列表,这样赋值是传了个引用,每个epoch要随机打乱样本,若不想改变原数组的话,这样会不会行不通?但经过我的测试发现shuffled_X_Y这个函数返回的会是一个全新的列表。所以这么写是可以的。
train_Y_copy = train_Y
with tf.Session() as sess:
# 对W,b等随机变量的初始化,不包括待输入的tensor比如tf.placeholder()定义的就需要run的时候指定数据
sess.run(tf.initialize_all_variables())
for epoch in range(training_epochs):
# 补充:这里参数迭代更新采取的策略是On-Line
for (x_train, y_train) in zip(train_X_copy, train_Y_copy):
_, cost, current_W, current_b = sess.run([optimizer, loss, W, b],
feed_dict={X: x_train, y: y_train})
# 依次epoch结束,随机打乱Full Data
train_X_copy, train_Y_copy = shuffled_X_Y(train_X_copy, train_Y_copy)
if epoch % display_step == 0:
print(('Epoch: %04d | Loss: %.6f | W: %.6f | b: %.6f') %
(epoch + 1, cost, current_W, current_b))
plot_line(current_W, current_b, 'Model Parameter')
print(('Training Loss: %.6f | W: %.6f | b: %.6f') %
(cost, current_W, current_b))
随着最后一个epoch结束,模型也就跑完了,随后输出结果W = 2.603326,b = 2.361474。
从拟合的过程中我们会有不同的参数优化的方法,接下来我们来说不同的优化方法之间的比较。
一阶优化方法:
二阶优化方法:
上面所说的一阶二阶其实就是对成本函数一阶求导或者二阶求导。当然阶数越高,计算量和规模越大,所以二阶的方法几乎不会使用。我们主要集中于一阶的以下四种优化方法。
以前我们都叫梯度下降,为什么后来叫随机梯度下降呢?
—— 梯度下降是说我们在计算梯度的时候,把所有的样本数据都用来算梯度,这样算出来的梯度方向无疑更加准确,但是往往运算量太大,承受不起。随机的意思就是说,我随机地抽取一部分,也就是一个Batch-size大小的样本来计算梯度,而不是全部的样本,这样就避免的前面的问题。
这个是它的定义:
tf.train.GradientDescentOptimizer(lr)
注意:如果学习率很大,那么参数每次迭代更新步长就会很大,如果超过了谷底的宽度,如上图所示,那么小球就会在谷底上方不断震荡,永远掉不到最低面。而学习率太小,又会导致速度太慢。所以学习速率的选取也是一件艺术活:
下面是一个典型的成本函数变化趋势图:
可以看到,错误率的曲线呈现了一个阶梯式的下降,原因都是每次在节点处降低了学习率。同时也可以发现它的下降幅度会越来越小:图中第一次下降15%左右,而第二次就只下降了5%都不到,这也提示我们在哪些情况下可以把这个模型停掉。
在论文中:w/ 表示 with ;w/o 表示 without
这个是它的定义:
tf.train.MomentumOptimizer(lr, momentum)
AdaGrad这种方法虽然工程上很少使用它,但是他为我们带来了一些视野上的开阔,包括目前大家用得非常多的一个Adam算法当中也有借鉴了它的思路。
这个是它的定义:
相当于,把历史梯度做了一个平方的叠加,然后除到 learning rate - 上,因为 是越来越大的,所以 是越来越小的。额外加上一个 是为了防止除数为 0 的情况。
优点:
缺点:
tf.train.AdagradOptimizer(lr, initial_accumulator_value)
下面是它的定义:
从最后一行我们可以看得出来,Adam算法借鉴了AdaGrad的思路,唯一不同的是,分子处的 m't+1又加入了动量的概念。
tf.train.AdamOptimizer(lr, beta1, beta2, epsilon)
我们可以看到,随着参数优化的方法不断地升级,从随机梯度下降到Adam算法,它的表达式是越来越复杂,这样一个复杂带来的好处是:在很多问题上,我们的优化效率会更高,模型越来越好。但是带来的一个直接的后果就是:可能会导致在一些问题上失败,尤其在一些复杂的数据上,Adam经常会失败,比如说训练着训练着,忽然在一个时间点上,它的Loss就变成了NA。那这样的情况怎么去解决呢?
在网上发现一个开源的梯度下降可视化工具, 大家可以下载来玩一下,感受一下不同参数下的优化方法的特点:
gradient_descent_viz
介绍完了优化方法体系,我们接下来就要进入深度学习的最根本的概念的学习。
我们知道机器学习所有的方法都是需要进行特征定义的,通过定义好的特征来进行数据的学习。但是深度学习在很多问题上是不需要预先去做特征提取的,我们只需要把一些领域知识融入到我们的输入当中,尽量保持原始的输入数据,就能够通过模型来对问题进行一个好的建模。这是深度学习和机器学习的一个很大的不同。既然如此我们难免会产生疑问:为什么深度学习能够不进行特征提取就学到更多的东西呢?
深度学习的基本单元——人工神经元:
我们把生物学上的神经元链接建立成数学模型之后可以看到:每个神经元对连接到该神经元的各个输入信号做了一个加权求和。经过一个激活函数加工过以后,再把信号传递给下一届神经元。
这就是单个神经元所干的事情,运算都很简单。但是当我们把多个神经元组合起来的时候,这些简单的运算就变成了复杂的运算。我们首先先来介绍一下激活函数。
人们的一般直觉是:如果我当前的输出结果太大,会伤害到下一层的神经元,于是便把输入集中到0-1之间或者-1~1之间。早期人们大量使用这两个函数,因为这两个函数形式上很优美。
但是它也有一个巨大的问题:当我们计算这两个函数的导数的时候,当我们x特别的小,落在小于负5的这个区间,它的导数几乎是0。也就是梯度为0,那么优化过程将会停止。这被认为是一个糟糕的现象。为了克服这个问题,人们又提出了ReLU激活函数
但是也有人进一步对ReLU函数提出质疑,如果x<0,优化过程依然会是“死的”,于是人们又提出了Leaky ReLU / PReLU:
为什么深度学习有那么大的威力?原因就是我们把神经元组合成了神经网络:
神经网络分为三部分:左边是输入,中间是隐藏层,右边是输出。可以看出隐藏层作为计算的单元是神经网络最为关键的部分。层与层之间的神经元相互连接的所有权重w就是我们要学习的参数。
注意:神经元的连接一定是逐层的,比如图中h1的神经元只能连到h2中,而不能连到h3中。
通过神经网络,我们可以把任意的输入,转化为我们想要的输出。我们通常会问多深的神经网络才算深度学习呀,或者多宽?
其实只要是按照这样的一个思路去进行模型的建立,就是深度学习。
神经网络的输出:
结论:
神经网络拟合的是一个关于输入的函数!
神经网络比一般的数学建模优势在哪里呢?——我们通过一个非常简单的算法,可以对里面的参数进行自动化的优化,在任何的问题上,只要有足够的数据和标签,我们就可以通过数据来对神经网络参数来进行优化。相当于我们有了一个通用的解决问题的方式。几乎所有的问题,都可以通过这样的一个方式来进行一个自动化的优化。
虽然没有严格的证明,但是关于深度和宽度,有这样一个比较General的结论:
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
%pylab inline
print(tf.__version__)
def imshow(img):
'''# 显示标签图像'''
plt.imshow(np.reshape(img, [28, 28]))
plt.show()
mnist = input_data.read_data_sets('./MNIST_data/', one_hot=True) # 下载数据到./MNIST_data文件夹下
for index in range(10):
print(mnist.train.images[index].shape)
print(mnist.train.labels[index])
print(np.nonzero(mnist.train.labels[index])[0]) # 获取第一个不是0的下标
imshow(mnist.train.images[index])
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1) # 正态分布,标准差为0.1
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape) # 避免网络进入假死的状态,不要设为0
return tf.Variable(initial)
# 定义网络结构
def fcnn(image_batch):
W_fc1 = weight_variable([784, 200])
b_fc1 = bias_variable([200])
W_fc2 = weight_variable([200, 200])
b_fc2 = bias_variable([200])
W_out = weight_variable([200, 10])
b_out = bias_variable([10])
# hidden_1 = tf.nn.sigmoid(tf.matmul(image_batch, W_fc1) + b_fc1)
# hidden_2 = tf.nn.sigmoid(tf.matmul(hidden_1, W_fc2) + b_fc2)
hidden_1 = tf.nn.tanh(tf.matmul(image_batch, W_fc1) + b_fc1)
hidden_2 = tf.nn.tanh(tf.matmul(hidden_1, W_fc2) + b_fc2)
_y = tf.nn.softmax(tf.matmul(hidden_2, W_out) + b_out)
return _y
x = tf.placeholder(tf.float32, [None, 784]) # 预先不知道Batch_size是多少,用None即可
y_ = tf.placeholder(tf.float32, [None, 10])
y = fcnn(x)
correct_prediction = tf.equal(tf.argmax(y_, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
cross_entropy = tf.reduce_mean(tf.reduce_sum(-y_ * tf.log(y + 0.00000001), reduction_indices=[1])) # 0.00000001是为了避免log0的情况导致计算溢出;1代表class这个维度;0代表Batch-size这个维度
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
# 为了在tensorboard中展示出每个iteration的情况:包括loss和accuracy的变化。
tf.summary.scalar('loss', cross_entropy)
tf.summary.scalar('accuracy', accuracy)
merged = tf.summary.merge_all()
training_iteration = 50000
batch_size = 64
display_step = 50
with tf.Session() as sess:
writer = tf.summary.FileWriter('./basic_mnist/', sess.graph)
sess.run(tf.initialize_all_variables())
for iteration in range(training_iteration):
batch_xs, batch_ys = mnist.train.next_batch(batch_size)
summary, _, current_accuracy = sess.run([merged, train_step, accuracy], feed_dict={x: batch_xs, y_:batch_ys})
writer.add_summary(summary, iteration)
if iteration % display_step == 0:
print('Iteration: %5d | Accuracy: %.6f' % (iteration + 1, current_accuracy))
print('Test Accuracy: %.6f' % sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))
writer.close()
为探究不同的激活函数对于对于整个网络训练的影响,下面是用tanh激活函数训练出来的网络:
下面是同样用sigmoid的激活函数的情况下,更换momentum优化算法:
下面使用我们的大BOSS——Adam优化算法:
所以说做深度学习像是一个炼丹的过程,从业人员俗称炼丹师。就是因为有太多的方法和参数可以调节。在平时的学习过程中,我们就可以有意识地去用一个记事本记录下所有的随着参数变化的结果趋势。这些都将成为日后工作当中的宝贵的经验。
通过以上的编程训练,相信大家能够对神经网络以及TensorFlow的具体实现有了比较深入的认知。
相信大家会有一个疑问,就是我们在训练模型的过程中,为什么要把MNIST这个数据集分为两个部分:训练集和测试集。而且训练集和测试集的关系一定要是相互独立的。也就是模型在训练过程中应该没见过测试集。原因就是我们希望训练的模型能够具备更好的推广性能。
这就涉及到了过拟合的概念。
拟合问题:
欠拟合的原因:模型本身的智商不够,或者训练的iteration不够,或者训练的数据不够,模型没有学到应有的知识。
分类问题:
使用正则化的方法解决过拟合问题,我们可以往以下三个方面来下功夫:数据、优化方法、和模型。
上述方法实现:对数据的空间变换,学习数据生成Pattern;...
增加噪声是为了:增加模型的反脆弱性。
有了这样的一种直觉(intuition)后,面临方法的选择时能够更加有的放矢。
当我们发现验证集上的Loss不降反增的时候,我们把它停掉,模型的iteration是刚刚好的。
全连接神经网络很容易导致参数的直接暴涨。卷积神经网络中,进行了有选择的链接,也就是参数共享。大幅度地减少了参数的数目,而且参数的运行效率会更高:更重要的参数能够发挥它的作用,这样模型能够把注意力真正放在与图像识别相关的这些参数上面去。
最典型的就是Dropout:
用一句话概括:就是当家做主人。模型训练过程中会屏蔽掉一部分神经元。让剩余部分的神经元当家做主来投票。看能做出来什么样的预测。然后在下一轮的迭代过程中又会扔掉另外一批的神经元来预测。
这么做有什么好处?——他每个神经元都能够独立地去做出更好的判断。可能有些神经元偷懒,我邻居太强了,我就不要做任何判断了。那OK我强制把邻居关起来,你自己来上,看你行不行。在这种情况下,每个神经元就有比较好的鲁棒性。能够做出来更好的判断。
为什么叫系宗方法?——系宗方法是通过多种模型投票去决定最后的一个结果,但是可以看到这里只是一个模型,我们是通过屏蔽的方法模拟了这么一种系宗模型。
除了鲁棒性的增加之外,在未来的应用当中还有一个好处,大家可以把每个神经单元看成一个计算单元。当我有一个计算单元挂了的时候,就相当于这个计算单元被屏蔽了,但是他的邻居还能够当家做主,继续工作。这样的一个系统鲁棒性自然也会更强。所以通过Dropout可以显著地提高全连接神经网络的鲁棒性。
首先确认训练数据是没问题的!
更大的模型;训练更长时间; 更好的模型结构:
更多的数据;正则化; 更好的模型结构
如无必要,勿增实体:Entities should not be multiplied unnecessarily
对于一个客观的统计学评价我们需要有哪些指标呢?让我们来一探究竟。
通过上面这张收入水平分布图可以看出,近似成一个幂律分布。富人拉高了平均值水平。在这里相比于平均值,中位数是一个更好的指标。但其实还有一个更好的选择:直方图
通过一个直方图我们可以很轻松地把数据分布给画出来(对数据分布有认识这是一件很重要的事情)可以看出大部分人的收入水平集中在:50-150之间。
对于分类问题一个最好的判断标准就是画出混淆矩阵。第一个字母T代表True,预测正确,F代表False,预测错误。第二个字母P代表阳性,N代表阴性。
举一个医学上的例子:肺癌患者。TP代表模型捕捉到了肺癌患者。TN代表模型正确排除了不是肺癌的患者。FP则代表患者本身不是肺癌,但是模型认为他是肺癌,假阳性,这是可接受的。但是FN就非常可怕了,患者本身是肺癌,但是模型认为他不是,这种情况叫漏诊。
不同的问题中会赋予不同的格子不同的权重。
到底多少个独立的指标可以确定一个模型的完整性能?——三个。
多分类混淆矩阵:
对于多分类任务来说,我们看混淆矩阵希望,都能够达到靠近主对角元不要太多。每一行的求和都是1.