前言
按照上一个博客所说的,直接按照深度学习0.1文档进行学习,当然在此之前我们需要了解这一系列教程所需要的数据集,以及一些概念性的东西
国际惯例,参考博客网址:
网盘下载链接: https://pan.baidu.com/s/1bpvqwDT 密码: 7c2w
在进行操作前先导入基本的模块
#-*- coding:utf-8 -*-
import cPickle,gzip
import numpy as np
import theano
import theano.tensor as T
数据集简介
就是把Lecun大佬的手写数字数据集MNIST
做了个序列化,并且分成了机器学习所需要的三种基本数据集:
- 训练集: 学习模型参数
- 验证集: 执行模型选择和超参数的选择
- 测试集: 测试训练好的模型的分辨能力即泛化能力
下载完数据集以后, 就可以进行读取操作
#导入数据集
f=gzip.open('mnist.pkl.gz','rb')
train_set,valid_set,test_set=cPickle.load(f)
f.close()
#train_set中存储了数据和标签
a,b=train_set
print a.shape#输出数据大小(50000L, 784L)
print b.shape#输出标签大小(50000L,)
为了训练性能着想, 教程建议将数据集存储在共享变量中, 并且通过小批索引的方式获取数据集. 存储到共享变量中的原因与使用GPU有关. 将数据拷贝到GPU内存中需要很大功耗.如果按需拷贝(每次使用到的小批数据), 由于上述所说的功耗问题, GPU的代码运行效率并不会比CPU快, 很可能更慢. 一旦存储在共享变量中, theano
就可以在构建共享变量的时候一次性拷贝GPU上所有的数据集. 随后GPU可以通过从共享变量中利用切片来获取小批数据, 无需从CPU内存中拷贝任何信息,因而避免了功耗.
注意由于数据点和它们的标签往往具有不同的本质(标签常是整型的,但是数据尝尝是实值), 因而我们建议对数据和标签采用不同的变量存储, 现将所有的数据存储为float, 然后再将标签用theano.tensor.cast
转换为int
类型
#最好将数据集放入到共享变量中
def shared_dataset(data_xy):
data_x,data_y=data_xy
shared_x=theano.shared(np.asarray(data_x,dtype=theano.config.floatX))
shared_y=theano.shared(np.asarray(data_y,dtype=theano.config.floatX))
return shared_x,T.cast(shared_y,'int32')
train_set_x,train_set_y=shared_dataset(train_set)
test_set_x,test_set_y=shared_dataset(test_set)
valid_set_x,valid_set_y=shared_dataset(valid_set)
batch_size=500
#假装获取第三批数据(批索引是0,1,2,3.....)
data=train_set_x[2*batch_size:3*batch_size]
label=train_set_y[2*batch_size:3*batch_size]
深度学习的监督优化基础
分类器
0-1损失
目的是最小化没有见过的样本的最小错误量. 如果用 f:RD→0,⋯,L 代表预测函数, 那么损失可以被写为
l0,1=∑i=1|D|If(x(i))≠y(i)
其中 I 称为示性函数
Ix={1if x is True0otherwise
在本实例中, 预测函数的定义为
f(x)=argmaxkP(Y=k|x,θ)
上式的含义就是样本 x 在模型参数θ 中, 属于每个标签中概率最大的是第 k 个标签. 在theano
中, 损失函数的实现可以写成:#0-1损失 zero_one_loss=T.sum(T.neq(T.argmax(p_y_given_x),y))
负对数似然
可以发现0-1损失是不可微的(它仅仅是是
与不是
)的加和. 所以我们看预测函数, 只要使得模型预测出真实标签的概率P(Y=真实标签|x(i);θ) 最大即可,然后我们通常使用极大对数似然来解决这个最大化问题:
L(θ,D)=∑i=0|D|logP(Y=y(i)|x(i);θ)
可以发现这个损失函数的值并非0-1损失那样带包正确预测的数量, 但是从随机初始化的分类器来看, 它们是非常相似的. 为了跟我们经常说的最小化损失函数保持一致, 可以定义最小化负对数似然(NLL
)函数:
theano`中实现它:
$$ NLL(\theta,D)=-\sum_{i=0}^{|D|}\log P(Y=y^{(i)}|x^{(i)};\theta) $$
在#最小化负对数似然 NLL=- T.sum(T.log(p_y_given_x)[T.arange(y.shape[0]),y])
代码标注: 看大括号里面的
[T.arange(y.shape[0]),y]
, 第一项T.arange(y.shape[0])
代表的就是样本索引从0-n
, 第二项就是每个样本的标签值.这句话说白了就是从概率矩阵p_y_given_x
中取出第i
个样本对应的标签为 y(i) 的概率
随机梯度下降
普通的梯度下降法: 是沿着一些参数所定义的损失函数的误差表面下降一小步, 如此反复不断下降. 训练集是大批量进入损失函数的, 伪代码如下:
while True: loss = f(params) d_loss_wrt_params = ... # compute gradient params -= learning_rate * d_loss_wrt_params if <stopping condition is met>: return params
随机梯度下降(SGD): 与普通的梯度下降原则上相同, 但是处理速度更快, 因为仅仅从部分样本中估算梯度, 极端情况下就是从一个时间仅仅从一个单一样本中估算梯度
# STOCHASTIC GRADIENT DESCENT for (x_i,y_i) in training_set: # imagine an infinite generator # that may repeat examples (if there is only a finite training set) loss = f(params, x_i, y_i) d_loss_wrt_params = ... # compute gradient params -= learning_rate * d_loss_wrt_params if <stopping condition is met>: return params
而在深度学习中, 我们比较建议采用小批量随机梯度下降(Minibatch SGD)方法, 一次采用多于一个的训练样本去计算梯度
for (x_batch,y_batch) in train_batches: # imagine an infinite generator # that may repeat examples loss = f(params, x_batch, y_batch) d_loss_wrt_params = ... # compute gradient using theano params -= learning_rate * d_loss_wrt_params if <stopping condition is met>: return params
特别注意: 如果你训练固定次数, 批大小的设置对于参数的更新控制是非常重要的. 对批大小为1的数据训练十次与对批大小为20的数据训练十次, 达到的效果完全不同, 记住当改变批大小的时候, 要据此调整其他的参数.
批量随机梯度下降在
theano
中实现如下:d_loss_wrt_params=T.grad(loss,params) updates=[(params,params-learning_rate*d_loss_wrt_params)] MSGD=theano.function([x_batch,y_batch],loss,updates=updates) for (x_batch,y_batch) in train_batches: print ('Current loss is ',MSGD(x_batch,y_batch)) if stopping_condition_is_met: return param
正则项
概念就不说了, 机器学习里面常用的减少过拟合方法, 一般采用L1
或者L2
正则项. 这里还说到了提前停止(early-stopping)也是正则化的一种方法. 详细请看周志华老师《机器学习》P105页缓解过拟合方法
L1和L2正则项
就是在损失函数后面添加一个额外项, 用于乘法某些参数
正常情况下, 我们的损失函数是这样
NLL(θ,D)=−∑i=0|D|logP(Y=y(i)|x(i);θ)
加了正则项以后可能是这样
E(θ,D)=NLL(θ,D)+λR(θ)orE(θ,D)=NLL(θ,D)+λ||θ||pp
其中 ||θ||p=(∑|θ|j=0|θj|p)1p , 这就是传说中的 p 范数, 当p=2 的时候就代表二范数, 而且也就是传说中的权重衰减原则上来说, 在损失上增加正则项能够增强神经网络的平滑映射(通过乘法参数中较大的值, 减少了网络的非线性程度). 最小化损失函数与正则项的和能够在训练集的拟合与一般解之间找到权衡. 为了遵循奥卡姆剃须刀准则(‘如无必要, 勿增实体’), 它们两个的最小化能够帮助我们找到最简单的解.
L1 = T.sum(abs(param))#L1范数 L2 = T.sum(param ** 2)#L2范数 loss = NLL + lambda_1 * L1 + lambda_2 * L2#损失+L1正则+L2正则
提前停止: 通过观察模型在验证集上的性能来减少过拟合. 如果模型在验证集上停止了提升或者出现退化, 就需要我们停止优化了. 关于何时停止, 这是一个判断, 并且很多启发式算法存在, 这里提供了一个几何增加耐心量的算法:
# 提前停止方法 patience = 5000 # 迭代结束条件就看这个值与迭代多少次batch的iter大小 patience_increase = 2 # 结束时间延长 improvement_threshold = 0.995 # 小于这个值就认为性能没有被再提升 validation_frequency = min(n_train_batches, patience/2)#验证频率 best_params = None best_validation_loss = numpy.inf test_score = 0. start_time = time.clock() done_looping = False#是否结束循环 epoch = 0 while (epoch < n_epochs) and (not done_looping): epoch = epoch + 1#当前迭代次数 for minibatch_index in range(n_train_batches): #当前迭代的批索引 d_loss_wrt_params = ... # 计算梯度 params -= learning_rate * d_loss_wrt_params # 参数更新 # 从第0次开始迭代,计算得到当前迭代的累计批索引 iter = (epoch - 1) * n_train_batches + minibatch_index if (iter + 1) % validation_frequency == 0: #使用0-1损失验证性能提升 this_validation_loss = ... if this_validation_loss < best_validation_loss: #如果性能还在提升 if this_validation_loss < best_validation_loss * improvement_threshold: #如果性能提升超过阈值,那么继续迭代 patience = max(patience, iter * patience_increase) best_params = copy.deepcopy(params) best_validation_loss = this_validation_loss #看看是否已经到达迭代终止时间 if patience <= iter: done_looping = True break
这个代码的意思就是迭代结束的条件一是达到了最大的迭代次数
n_epoch
,另一个就是利用patience
控制是否提前结束迭代. 如果迭代过程中性能提升超过阈值, 就考虑将patience
提升一下max(patience,iter*patience_increase)
, 反之性能并未提升或者达到稳定情况(阈值范围之内), 那么就对比当前patience
与总共使用批样本训练的次数(每次使用一批样本就将iter+1
), 一旦iter
超过了patience
就停止训练了注意: 这个验证频率
validation_frequency
要比patience
小, 至少要在一次patience
迭代期间验证过两次.所以使用了validation_frequency = min( value, patience/2.)
测试
当循环完毕, 得到的参数是验证集上效果最好的参数. 这样我们使用同一个训练集、测试集、验证集去训练多个模型, 我们比较每个模型最好的验证集损失, 就认为我们选择了最好的模型.
简单总结
三个数据集: 训练集、验证集、测试集
优化方法: 训练集被用于目标函数的可微近似的批量随机梯度下降中
选择模型: 看模型在验证集上的性能能表现
存储和读取模型参数
存储模型参数
save_file = open('path', 'wb') cPickle.dump(w.get_value(borrow=True), save_file, -1) cPickle.dump(v.get_value(borrow=True), save_file, -1) cPickle.dump(u.get_value(borrow=True), save_file, -1) save_file.close()
读取模型参数
save_file = open('path') w.set_value(cPickle.load(save_file), borrow=True) v.set_value(cPickle.load(save_file), borrow=True) u.set_value(cPickle.load(save_file), borrow=True)
关于
borrow
看这里, 大概就是如果borrow=True
, 那么对原始变量的更改会对当前的共享变量产生影响, 还是把例子贴出来吧import numpy, theano np_array = numpy.ones(2, dtype='float32') s_default = theano.shared(np_array) s_false = theano.shared(np_array, borrow=False) s_true = theano.shared(np_array, borrow=True) np_array += 1 # now it is an array of 2.0 s print(s_default.get_value()) print(s_false.get_value()) print(s_true.get_value()) ''' [ 1. 1.] [ 1. 1.] [ 2. 2.] '''
【注】我可能用了假
theano
, 这个例子在我的环境中执行竟然结果都是[1,1]
后续
数据集和一些基本的训练方法都搞定了, 下一步就得实战一波了
- logistic回归分类手写数字
- 多层感知器
- 卷积
- 去噪自编码dAE
- 堆叠去噪自编码SdAE
- 受限玻尔兹曼机RBM
- 深度信念网
- 混合蒙特卡洛采样
- LSTM
- RNN-RBM