MXNET:分类模型
线性回归模型适用于输出为连续值的情景,例如输出为房价。在其他情景中,模型输出还可以是一个离散值,例如图片类别。对于这样的分类问题,我们可以使用分类模型,例如softmax回归。
为了便于讨论,让我们假设输入图片的尺寸为2×2,并设图片的四个特征值,即像素值分别为\(x_1,x_2,x_3,x_4\)。假设训练数据集中图片的真实标签为狗、猫或鸡,这些标签分别对应离散值\(y_1,y_2,y_3\)。
单样本分类的矢量计算表达式
针对上面的问题,假设分类模型的权重和偏差参数分别为:
设某样本为:\(x^{(i)}=[x_1^{(i)}, x_2^{(i)}, x_3^{(i)}, x_4^{(i)}]\),输出层为\(o^{(i)}=[o_1^{(i)}, o_2^{(i)}, o_3^{(i)}]\),预测为狗、猫或鸡的概率为\(y^{(i)}=[y_1^{(i)}, y_2^{(i)}, y_3^{(i)}]\)。
小批量样本的适量计算表达式
如果每个批次有n个样本,假设输入特征数为x,输出类别数为y,则:X的size为nx,W的维度为xy,b的维度为1*y。
这里的加法运算使用了广播机制。
交叉熵损失函数
Softmax回归使用了交叉熵损失函数(cross-entropy
loss)。给定分类问题的类别个数m。当样本i的标签类别为\(y_j\)时 (\(1 \leq j \leq m\)),设\(q_j^{(i)}=1\)且当\(k \neq j, 1 \leq k \leq m\)时\(q_k^{(i)}=0\)。
设模型对样本i在类别\(y_j\)上的预测概率为\(p_j^{(i)}\)(\(1 \leq j \leq m\))。假设训练数据集的样本数为n,交叉熵损失函数定义为
其中\(\boldsymbol{\Theta}\)代表模型参数。在训练softmax回归时,我们将使用优化算法来迭代模型参数并不断降低损失函数的值。
不妨从另一个角度来理解交叉熵损失函数。在训练模型时,对于训练数据集上每个样本,总体上我们希望模型对这些样本真实的标签类别的预测概率尽可能大,也就是希望模型尽可能容易输出真实的标签类别。
设\(p_{\text{label}_i}\)是模型对样本i的标签类别的预测概率,并设训练数据集的样本数为n。由于对数函数单调递增,最大化训练数据集所有标签类别的联合预测概率\(\prod_{i=1}^n p_{\text{label}_i}\)等价于最大化<\(\sum_{i=1}^n \log p_{\text{label}_i}\),即最小化\(-\sum_{i=1}^n \log p_{\text{label}_i}\),因此也等价于最小化以上定义的交叉熵损失函数。
softmax回归
获取Fashion-MNIST数据集
我们使用一个类别为数字的数据集MNIST。该数据集中,图片尺寸为:math:28 times 28,一共包括了10个类别。
from mxnet.gluon import data as gdata
def transform(feature, label):
return feature.astype('float32') / 255, label.astype('float32')
mnist_train = gdata.vision.MNIST(train=True, transform=transform)
mnist_test = gdata.vision.MNIST(train=False, transform=transform)
feature, label = mnist_train[0]
print 'feature shape: ', feature.shape, 'label: ', label
读取数据
batch_size = 256
train_iter = gdata.DataLoader(mnist_train, batch_size, shuffle=True)
test_iter = gdata.DataLoader(mnist_test, batch_size, shuffle=False)
初始化模型参数
模型的输入向量的长度是28×28=784:该向量的每个元素对应图片中每个像素。由于图片有10个类别,单层神经网络输出层的输出个数为10。由上一节可知,Softmax回归的权重和偏差参数分别为784×10和1×10的矩阵。
num_inputs = 784
num_outputs = 10
W = nd.random_normal(scale=0.01, shape=(num_inputs, num_outputs))
b = nd.zeros(num_outputs)
params = [W, b]
for param in params:
param.attach_grad()
定义softmax函数
#在结果中保留行和列这两个维度(keepdims=True)
def softmax(X):
exp = X.exp()
partition = exp.sum(axis=1, keepdims=True)
return exp / partition # 这里应用了广播机制。
定义模型
通过reshape函数将每张原始图片改成长度为num_inputs的向量。
def net(X):
return softmax(nd.dot(X.reshape((-1, num_inputs)), W) + b)
定义损失函数
通过使用pick函数,我们得到了2个样本的标签的被预测概率。
y_hat = nd.array([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = nd.array([0, 2])
nd.pick(y_hat, y)
# output
[ 0.1 0.5]
<NDArray 2 @cpu(0)>
交叉熵损失函数翻译成代码:
def cross_entropy(y_hat, y):
return -nd.pick(y_hat.log(), y)
模型的评测-计算分类精度
分类准确率即正确预测数量与总预测数量的比。
def accuracy(y_hat, y):
return (nd.argmax(y_hat, axis=1) == y).asnumpy().mean()
def evaluate_accuracy(data_iter, net):
acc = 0
for X, y in data_iter:
acc += accuracy(net(X), y)
return acc / len(data_iter)
因为我们随机初始化了模型net,所以这个模型的准确率应该接近于1 / num_outputs = 0.1。
evaluate_accuracy(test_iter, net)
# output
0.0947265625
训练模型
在训练模型时,迭代周期数num_epochs和学习率lr都是可以调的超参数。改变它们的值可能会得到分类更准确的模型。
def sgd(params, lr, batch_size):
for param in params:
param[:] = param - lr * param.grad / batch_size
num_epochs = 5
lr = 0.1
loss = cross_entropy
def train_cpu(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, trainer=None):
for epoch in range(1, num_epochs + 1):
train_l_sum = 0
train_acc_sum = 0
for X, y in train_iter:
with autograd.record():
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
if trainer is None:
sgd(params, lr, batch_size)
else:
trainer.step(batch_size)
train_l_sum += nd.mean(l).asscalar()
train_acc_sum += accuracy(y_hat, y)
test_acc = evaluate_accuracy(test_iter, net)
print("epoch %d, loss %.4f, train acc %.3f, test acc %.3f"% (epoch, train_l_sum / len(train_iter), train_acc_sum / len(train_iter), test_acc))
train_cpu(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)
# output
epoch 1, loss 0.7105, train acc 0.842, test acc 0.884
epoch 2, loss 0.4296, train acc 0.887, test acc 0.899
epoch 3, loss 0.3840, train acc 0.896, test acc 0.905
epoch 4, loss 0.3607, train acc 0.901, test acc 0.909
epoch 5, loss 0.3461, train acc 0.905, test acc 0.911
预测
data, label = mnist_test[0:9]
predicted_labels = net(data).argmax(axis=1)
Softmax回归——使用Gluon
Softmax和交叉熵损失函数
分开定义Softmax运算和交叉熵损失函数可能会造成数值不稳定。因此,Gluon提供了一个包括Softmax运算和交叉熵损失计算的函数。它的数值稳定性更好。
程序实现
# -*- coding: utf-8 -*-
from mxnet import init
#定义数据源
import gb
batch_size = 256
train_iter, test_iter = gb.load_data_fashion_mnist(batch_size)
#定义网络
from mxnet.gluon import nn
net = nn.Sequential()
net.add(nn.Flatten())
net.add(nn.Dense(10))
net.initialize(init.Normal(sigma=0.01))
#损失函数
from mxnet.gluon import loss as gloss
loss = gloss.SoftmaxCrossEntropyLoss()
#优化算法
from mxnet.gluon import Trainer
trainer = Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1})
#训练模型
num_epochs = 5
gb.train_cpu(net, train_iter, test_iter, loss, num_epochs, batch_size, None,
None, trainer)
基础函数包 gb.py
from mxnet.gluon import data as gdata
from mxnet import autograd
from mxnet import ndarray as nd
def transform(feature, label):
return feature.astype('float32') / 255, label.astype('float32')
mnist_train = gdata.vision.MNIST(train=True, transform=transform)
mnist_test = gdata.vision.MNIST(train=False, transform=transform)
def load_data_fashion_mnist(batch_size):
train_iter = gdata.DataLoader(mnist_train, batch_size, shuffle=True)
test_iter = gdata.DataLoader(mnist_test, batch_size, shuffle=False)
return train_iter, test_iter
def accuracy(y_hat, y):
return (nd.argmax(y_hat, axis=1) == y).asnumpy().mean()
def evaluate_accuracy(data_iter, net):
acc = 0
for X, y in data_iter:
acc += accuracy(net(X), y)
return acc / len(data_iter)
def sgd(params, lr, batch_size):
for param in params:
param[:] = param - lr * param.grad / batch_size
def train_cpu(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, trainer=None):
for epoch in range(1, num_epochs + 1):
train_l_sum = 0
train_acc_sum = 0
for X, y in train_iter:
with autograd.record():
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
if trainer is None:
sgd(params, lr, batch_size)
else:
trainer.step(batch_size)
train_l_sum += nd.mean(l).asscalar()
train_acc_sum += accuracy(y_hat, y)
test_acc = evaluate_accuracy(test_iter, net)
print("epoch %d, loss %.4f, train acc %.3f, test acc %.3f" % (
epoch, train_l_sum / len(train_iter), train_acc_sum / len(train_iter), test_acc))