预备知识
安装环境
$ pip install mxnet-cu80 --> cuda8.0
数据操作,在MxNet中用NDArray类,是存储和变换数据的主要工具。NDArray提供GPU计算和自动求梯度等更多功能。
自动求梯度,用autograd模块,from mxnet import autograd, nd
x = nd.arange(4).reshape((4,1))
x.attach_grad() # 申请存储梯度所需要的内存
with autograd.record(): # 默认条件下MxNet不会记录用于求梯度的计算,需要调用record函数
y=2*nd.dot(x.T, x)
y.backward()
assert(x.grad -4*x).norm().asscalar() == 0
x.grad
官方文档https://mxnet.apache.org/api/python/index.html
深度学习基础
线性回归
线性回归输出是一个连续值。softmax回归则适用于分类问题,线性回归和softmax回归都是单层神经网络。
模型:y = x1*w1 + x2*w2 + b
损失函数:L = 1/2 * (yt - yp)^2
优化算法:小批量随机梯度下降
模型预测:模型推断
from mxnet import autograd, nd
# 生成数据
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs))
labels = true_w[0]*features[:,0] + true_w[1]*features[:,1] + true_b
labels += nd.random.normal(scale=0.1, shape=labels.shape)
# 读取数据
from mxnet.gluon import data as gdata
batch_size = 10
dataset = gdata.ArrayDataset(features, labels) # 将训练数据的特征和标签组合
# 随机读取小批量
data_iter = gdata.DataLoader(dataset, batch_size, shuffle=True)
# 打印第一个小批量样本
for X, y in data_iter:
print(X, y)
break
# 定义模型
from mxnet.gluon import nn
net = nn.Sequential()
net.add(nn.Dense(1)) # 输出为1
# 初始化模型参数
from mxnet import init
net.initialize(init.Normal(sigma=0.01))
# 定义损失函数
from mxnet.gluon import loss as gloss
loss = gloss.L2Loss() # 平方损失又称L2范式损失
# 定义优化算法
from mxnet import gluon
trainer = gluon.Trainer(net.collect_params(),
'sgd', {
'learning_rate': 0.03
})
# 训练模型
num_epochs = 3
for epoch in range(1, num_epochs+1):
for X, y in data_iter:
with autograd.record():
l = loss(net(X), y)
l.backward()
trainer.step(batch_size)
l = loss(net(features), labels)
print('epoch %d, loss: %f' % (epoch, l.mean().asnumpy()))
# 打印最后的参数
dense = net[0]
print(true_w)
print(dense.weight.data())
print(true_b)
print(dense.bias.data())
softmax回归
import d2lzh as d2l
from mxnet import gluon, init
from mxnet.gluon import loss as gloss, nn
# 读取数据 Fashion-MNIST,一个10类服饰分类数据集
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# 定义和初始化模型
net = nn.Sequential()
net.add(nn.Dense(10))
net.initialize(init.Normal(sigma=0.01))
# softmax和交叉熵损失函数
loss = gloss.SoftmaxCrossEntropyLoss()
# 定义优化算法
trainer = gluon.Trainer(net.collect_params(),'sgd',{'learning_rate': 0.1})
# 训练模型
num_epochs = 5
d2l.train_ch3(net, train_iter,test_iter,loss,num_epochs,batch_size,None,None,trainer)
多层感知机
激活函数,全连接只是对数据作仿射变换,而多个仿射变换的叠加仍然是一个仿射变换。解决问题的一个方法是引入非线性变换,例如对隐藏变量使用按元素运算的非线性函数进行变换,然后再作为下一个全连接层的输入。这个非线性函数被称为激活函数(activetion function)
ReLU函数:max(x, 0), 函数和导数如下图
import d2lzh as d2l
from mxnet import autograd, nd
def xyplot(x_vals, y_vals, name):
d2l.set_figsize(figsize=(5, 2.5))
d2l.plt.plot(x_vals.asnumpy(), y_vals.asnumpy())
d2l.plt.xlabel('x')
d2l.plt.ylabel(name+'(x)')
d2l.plt.show()
x = nd.arange(-8.0, 8.0, 0.1)
x.attach_grad()
with autograd.record():
y = x.relu()
xyplot(x, y, 'relu')
y.backward()
xyplot(x, x.grad, 'grad of relu')
sigmoid函数:1/(1+exp(-x)),当输入为0时,导数到达最大值0.25
with autograd.record():
y = x.sigmoid()
xyplot(x, y, 'sigmoid')
y.backward()
xyplot(x, x.grad, 'grad of sigmoid')
tanh函数:(1-exp(-2x))/(1+exp(-2x)),当输入为0时,导数达到最大1;
with autograd.record():
y = x.tanh()
xyplot(x, y, 'tanh')
y.backward()
xyplot(x, x.grad, 'grad of tanh')
多层感知机实现
import d2lzh as d2l
from mxnet import gluon, init
from mxnet.gluon import loss as gloss, nn
# 定义模型
net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'), nn.Dense(10))
net.initialize(init.Normal(sigma=0.01))
# 读取数据并训练模型
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
loss = gloss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.5})
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None,
None, trainer)
模型评估
训练误差(training error)和泛化误差(generalization error),通俗来讲,前者指模型在训练数据集上表现出的误差,后者指模型在任意一个测试数据样本上表现出的误差得期望,并常常通过测试数据集上的误差来近似。
由于无法从训练误差估计泛化误差,一味地降低训练误差并不意味着泛化误差一定会降低。
机器学习模型应关注降低泛化误差
模型选择
从严格意义上讲,测试集只能在模型选定后使用一次,不可用于调参。
因此,我们可以从给定的训练集中随机选取一小部分作为验证集,而讲剩余部分作为真正的训练集。
然而在实际应用中,由于数据不容易获取,测试数据极少只使用一次就丢弃。因此,实践中验证数据集和测试集的界限可能比较模糊。
K折交叉验证
由于验证数据集不参与模型训练,当训练数据不够用时,预留大量的验证数据显得太奢侈。一种改善的方法是K哲交叉验证(K-fold cross validation)。把原始训练集分割成K个不重合的子数据集,然后我们做K次模型训练和验证。每一次,我们使用一个子数据集验证模型,并使用其他K-1个子数据集训练模型。最后对这K次训练误差和验证误差分布求平均。
欠拟合和过拟合
欠拟合:模型无法得到较低的训练误差
过拟合:模型的训练误差远小于测试误差
在实践中,我们要尽可能同时应对欠拟合和过拟合。虽然有很多因素可能导致这两种拟合问题,在这里我们重点讨论两个因素:模型复杂度和训练数据集大小。
模型复杂度:模型过于复杂
训练数据集太小,特别是比模型参数数量更少时,过拟合容易发生。此外,泛化误差不会随训练集增加而增大。
权重衰减
虽然增大训练集可能会减轻过拟合,但是获取额外的训练数据往往代价高昂。一种常用的方法:权重衰减
权重衰减等价于L2范数正则化(regularization)。正则化通过为模型损失函数添加惩罚项使学出的模型参数值较小,是应对过拟合的常用手段。
L2范数正则化指的是模型权重参数每个元素的平方和与一个正的常数的乘积。如线性回归 y = x1*w1 + x2*w2 + b
损失函数为:
,,
当权重参数均为0时,惩罚项最小。这通常会使学到的权重参数较接近0。
通过惩罚绝对值较大的模型参数为需要学习的模型增加了限制,这可能对过拟合有效。如下w1先乘以小于1的数,再减去梯度
实验:高维线性回归
import d2lzh as d2l
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import data as gdata, loss as gloss, nn
# 生成数据
n_train, n_test, num_inputs = 20, 100, 200
true_w, true_b = nd.ones((num_inputs, 1)) * 0.01, 0.05
features = nd.random.normal(shape=(n_train + n_test, num_inputs))
labels = nd.dot(features, true_w) + true_b
labels += nd.random.normal(scale=0.01, shape=labels.shape)
train_features, test_features = features[:n_train, :], features[n_train:, :]
train_labels, test_labels = labels[:n_train], labels[n_train:]
# 初始化模型参数
def init_params():
w = nd.random.normal(scale=1, shape=(num_inputs, 1))
b = nd.zeros(shape=(1,))
w.attach_grad()
b.attach_grad()
return [w, b]
# 定义L2范数惩罚项
def l2_penalty(w):
return (w**2).sum() / 2
# 定义训练和测试
batch_size, num_epochs, lr = 1, 100, 0.003
net, loss = d2l.linreg, d2l.squared_loss
train_iter = gdata.DataLoader(gdata.ArrayDataset(
train_features, train_labels), batch_size, shuffle=True)
def fit_and_plot(lambd):
w, b = init_params()
train_ls, test_ls = [], []
for _ in range(num_epochs):
for X, y in train_iter:
with autograd.record():
# 添加了L2范数惩罚项
l = loss(net(X, w, b), y) + lambd * l2_penalty(w)
l.backward()
d2l.sgd([w, b], lr, batch_size)
train_ls.append(loss(net(train_features, w, b),
train_labels).mean().asscalar())
test_ls.append(loss(net(test_features, w, b),
test_labels).mean().asscalar())
d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
range(1, num_epochs + 1), test_ls, ['train', 'test'])
print('L2 norm of w:', w.norm().asscalar())
观察过拟合
fit_and_plot(lambd=0) # 不使用惩罚项
fit_and_plot(lambd=3) # 使用惩罚项,训练集误差虽然有所提高,但测试集上的误差有所下降
过拟合现象得到一定程度的缓解。
丢弃法
除了权重衰减以外,常常使用丢弃法(dropout)来应对过拟合问题。丢弃法有一些不同的变体。本文提到的是倒置丢弃法(inverted dropout)
深度学习计算
前面使用Sequential类构造模型,还有另外一种基于Block类的模型构造方法,它让模型构造更加灵活。
from mxnet import nd
from mxnet.gluon import nn
class MLP(nn.Block):
# 声明带有模型参数的层,这里声明了两个全连接层
def __init__(self, **kwargs):
# 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
# 参数,如“模型参数的访问、初始化和共享”一节将介绍的模型参数params
super(MLP, self).__init__(**kwargs)
self.hidden = nn.Dense(256, activation='relu') # 隐藏层
self.output = nn.Dense(10) # 输出层
# 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
def forward(self, x):
return self.output(self.hidden(x))
X = nd.random.uniform(shape=(2, 20))
net = MLP()
net.initialize()
net(X)
net(x)会调用Block类的__call__函数,这个函数讲调用forward函数来完成前向计算
模型参数的访问、初始化和共享
net[0].params, type(net[0].params)
net[0].weight.data()
net[0].weight.grad()
net.collect_params() # 获取所有嵌套的层所包含的所有参数
net.collect_params('.*weight') # 通过正则表达式来筛选参数
自定义初始化
class MyInit(init.Initializer):
def _init_weight(self, name, data):
print('Init', name, data.shape)
data[:] = nd.random.uniform(low=-10, high=10, shape=data.shape)
data *= data.abs() >= 5
net.initialize(MyInit(), force_reinit=True)
net[0].weight.data()[0]
多层之间共享模型参数
net = nn.Sequential()
shared = nn.Dense(8, activation='relu')
net.add(nn.Dense(8, activation='relu'),
shared,
nn.Dense(8, activation='relu', params=shared.params),
nn.Dense(10))
net.initialize()
X = nd.random.uniform(shape=(2, 20))
net(X)
net[1].weight.data()[0] == net[2].weight.data()[0]
模型参数的延后初始化
在MxNet中,初始化会延迟,这让模型的创建更加简单,只需要定义每个层的输出大小,而不用人工推测它们的输入个数。
如果系统的调用initialize函数时能够知道所有参数的形状,那么延后初始化就不会发生。
自定义层
继承nn.Block类
读取和存储
NDArray直接使用save函数和load函数
Gluon模型提供了save_parameters和load_parameters函数来读写模型参数。
GPU计算
MXNet可以指定用来存储和计算的设备,mx.cpu(i)来表示
a = nd.array([1, 2, 3], ctx=mx.gpu())
可以使用copyto函数和as_in_context函数在设备之间传输数据
同样,Gluon可以在初始化时通过ctx参数指点设备
net.initialize(ctx=mx.gpu())