本专栏是记录作者学习TensorFlow深度学习的相关内容
本章节将介绍Softmax回归解决多分类问题,解释了独热编码,softmax函数,交叉熵的概念,并用框架和不使用框架分别编写了分类模型
本节的 Jupyter 笔记本文件已上传至gitee以供大家学习交流:我的gitee仓库
文章目录
1 分类问题
回归可以用于预测多少的问题。 比如预测房屋被售出价格,或者棒球队可能获得的胜场数,又或者患者住院的天数。
我们也对分类问题感兴趣:不是问“多少”,而是问“哪一个”:
某个电子邮件是否属于垃圾邮件文件夹?
某个用户可能注册或不注册订阅服务?
某个图像描绘的是驴、狗、猫、还是鸡?
某人接下来最有可能看哪部电影?
1.1 独热编码(one-hot encoding)
独热编码是一个向量,它的分量和类别一样多。
类别对应的分量设置为1,其他所有分量设置为0。
在我们的例子中,标签
y
y
y将是一个三维向量,
其中
(
1
,
0
,
0
)
(1, 0, 0)
(1,0,0)对应于“猫”、
(
0
,
1
,
0
)
(0, 1, 0)
(0,1,0)对应于“鸡”、
(
0
,
0
,
1
)
(0, 0, 1)
(0,0,1)对应于“狗”:
y ∈ { ( 1 , 0 , 0 ) , ( 0 , 1 , 0 ) , ( 0 , 0 , 1 ) } . y \in \{(1, 0, 0), (0, 1, 0), (0, 0, 1)\}. y∈{(1,0,0),(0,1,0),(0,0,1)}.
1.2 softmax函数
softmax函数能够将未规范化的预测变换为非负数并且总和为1,同时让模型保持可导的性质。如此softmax函数值即可代表类别的概率,而且可以衡量预测值与真实值的距离并进行梯度下降。
y
^
=
s
o
f
t
m
a
x
(
o
)
其中
y
^
j
=
exp
(
o
j
)
∑
k
exp
(
o
k
)
\hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}
y^=softmax(o)其中y^j=∑kexp(ok)exp(oj)
这里,对于所有的
j
j
j总有
0
≤
y
^
j
≤
1
0 \leq \hat{y}_j \leq 1
0≤y^j≤1。
softmax运算不会改变未规范化的预测
o
\mathbf{o}
o之间的大小次序,只会确定分配给每个类别的概率。以1.1为例,
y
^
\hat{\mathbf{y}}
y^形如
y
^
=
{
0.2
,
0.5
,
0.3
}
\hat{\mathbf{y}}=\{0.2,0.5,0.3\}
y^={0.2,0.5,0.3}
因此,在预测过程中,我们仍然可以用下式来选择最有可能的类别。
argmax
j
y
^
j
=
argmax
j
o
j
.
\operatorname*{argmax}_j \hat y_j = \operatorname*{argmax}_j o_j.
jargmaxy^j=jargmaxoj.
即
y
^
\hat{\mathbf{y}}
y^中最大项所对应的类别记为最有可能的类别。
1.3 交叉熵
真实值与预估值的区别作为损失,一般使用交叉熵来衡量预测值和真实值的区别
l
(
y
,
y
^
)
=
−
∑
j
=
1
q
y
j
log
y
^
j
.
l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j.
l(y,y^)=−j=1∑qyjlogy^j.
梯度
∂
o
j
l
(
y
,
y
^
)
=
exp
(
o
j
)
∑
k
=
1
q
exp
(
o
k
)
−
y
j
=
s
o
f
t
m
a
x
(
o
)
j
−
y
j
.
\partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}}) = \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} - y_j = \mathrm{softmax}(\mathbf{o})_j - y_j.
∂ojl(y,y^)=∑k=1qexp(ok)exp(oj)−yj=softmax(o)j−yj.
2 图像分类数据集的获取
Fashion-MNIST 是一个用于图像分类的数据集,与经典的 MNIST 数据集相似,但更具挑战性。它包含了 10 个类别的灰度图像,每个类别包含了 28x28 像素的 60,000 个训练图像和 10,000 个测试图像。
2.1 读取数据集
tf.keras.datasets.fashion_mnist.load_data() 是 TensorFlow 中加载 Fashion MNIST 数据集的函数。这个函数返回的是一个包含两个元组的元组,分别表示训练集和测试集。
%matplotlib inline
import tensorflow as tf
from d2l import tensorflow as d2l
d2l.use_svg_display()
mnist_train, mnist_test = tf.keras.datasets.fashion_mnist.load_data()
len(mnist_train[0]), len(mnist_test[0]),mnist_train[0].shape
结果
(60000, 10000, (60000, 28, 28))
Fashion-MNIST中包含的10个类别,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。 以下函数用于在数字标签索引及其文本名称之间进行转换。
def get_fashion_mnist_labels(labels): #@save
"""返回Fashion-MNIST数据集的文本标签"""
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
绘制图像列表函数
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save
"""绘制图像列表"""
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
ax.imshow(img.numpy())
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes
获取前18个样本的图像与标签
X = tf.constant(mnist_train[0][:18])
y = tf.constant(mnist_train[1][:18])
show_images(X, 3, 6, titles=get_fashion_mnist_labels(y));
结果:
2.2 获取小批量数据
batch_size = 18
train_iter = tf.data.Dataset.from_tensor_slices(
mnist_train).batch(batch_size).shuffle(len(mnist_train[0]))
for X, y in train_iter:
show_images(X, 3, 6, titles=get_fashion_mnist_labels(y));
break
结果:
2.3 整合
将上述读取数据与划分batch的函数整合,附加调整图像大小的功能
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
mnist_train, mnist_test = tf.keras.datasets.fashion_mnist.load_data()
# 将所有数字除以255,使所有像素值介于0和1之间,在最后添加一个批处理维度,
# 并将标签转换为int32。
process = lambda X, y: (tf.expand_dims(X, axis=3) / 255,
tf.cast(y, dtype='int32'))
resize_fn = lambda X, y: (
tf.image.resize_with_pad(X, resize, resize) if resize else X, y)
return (
tf.data.Dataset.from_tensor_slices(process(*mnist_train)).batch(
batch_size).shuffle(len(mnist_train[0])).map(resize_fn),
tf.data.Dataset.from_tensor_slices(process(*mnist_test)).batch(
batch_size).map(resize_fn))
测试load_data_fashion_mnist函数,迭代器一次可以取32个样本
train_iter, test_iter = load_data_fashion_mnist(32, resize=64)
for X, y in train_iter:
print(X.shape, X.dtype, y.shape, y.dtype)
break
结果:
(32, 64, 64, 1) <dtype: 'float32'> (32,) <dtype: 'int32'>
3 softmax回归的实现(不使用框架)
引入的Fashion-MNIST数据集,并设置数据迭代器的批量大小为256。
import tensorflow as tf
from IPython import display
from d2l import tensorflow as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
3.1 初始化模型参数
原始数据集中的每个样本都是 28 × 28 28 \times 28 28×28的图像。本节将每个图像的张量展开,把它们看作长度为784的向量。
该任务是一个10分类任务,输出的形状为(10,),权重W的形状为(784,10),偏置b形状为(1,10)。初始化过程如下。
num_inputs = 784
num_outputs = 10
W = tf.Variable(tf.random.normal(shape=(num_inputs, num_outputs),
mean=0, stddev=0.01))
b = tf.Variable(tf.zeros(num_outputs))
3.2 定义softmax
回顾一下softmax表达式:
s
o
f
t
m
a
x
(
X
)
i
j
=
exp
(
X
i
j
)
∑
k
exp
(
X
i
k
)
.
\mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(\mathbf{X}_{ij})}{\sum_k \exp(\mathbf{X}_{ik})}.
softmax(X)ij=∑kexp(Xik)exp(Xij).
所以实现softmax的过程如下
def softmax(X):
#对每个项求幂
X_exp = tf.exp(X)
#对每一行求和(对轴1求和,并保持形状),得到每个样本的规范化常数;
partition = tf.reduce_sum(X_exp, 1, keepdims=True)
#将每一项都除以该行的规范化常数,确保结果的和为1。
return X_exp / partition # 这里应用了广播机制
3.3 定义softmax回归模型
在3.2的基础上定义softmax回归,softmax回归与线性回归类似,也是基于线性关系 X W + b XW+b XW+b预测,知识要外加softmax函数计算多类别的概率,得到预测值 y ^ \hat{\mathbf{y}} y^,其中传入的图片张量需要展开为向量。
def net(X):
return softmax(tf.matmul(tf.reshape(X, (-1, W.shape[0])), W) + b)
3.4 定义交叉熵损失函数
交叉熵采用真实标签的预测概率的负对数似然
l
(
y
,
y
^
)
=
−
∑
j
=
1
q
y
j
log
y
^
j
.
l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j.
l(y,y^)=−j=1∑qyjlogy^j.
下面,以[一个数据样本y_hat
,其中包含2个样本在3个类别的预测概率,以及它们对应的标签y
。]为例。
通过y
,我们知道在第一个样本中,第一类是正确的预测;而在第二个样本中,第三类是正确的预测。
现选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。即真实标签的预测概率
y_hat = tf.constant([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = tf.constant([0, 2])
tf.boolean_mask(y_hat, tf.one_hot(y, depth=y_hat.shape[-1]))
结果:
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([0.1, 0.5], dtype=float32)>
所以损失函数值为
def cross_entropy(y_hat, y):
return -tf.math.log(tf.boolean_mask(
y_hat, tf.one_hot(y, depth=y_hat.shape[-1])))
cross_entropy(y_hat, y)
结果:
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([2.3025851, 0.6931472], dtype=float32)>
3.5 分类精度(准确率)
给定预测概率分布y_hat
,当我们必须输出硬预测(hard prediction)时,选择预测概率最高的类。
当预测与标签分类y一致时,即是正确的。 分类精度即正确预测数量与总预测数量之比。
def accuracy(y_hat, y): #@save
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = tf.argmax(y_hat, axis=1)
cmp = tf.cast(y_hat, y.dtype) == y
return float(tf.reduce_sum(tf.cast(cmp, y.dtype)))
使用上一部分用到的示例,第一个样本预测错误,第二个样本预测正确,测试如下:
y_hat = tf.constant([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = tf.constant([0, 2])
accuracy(y_hat, y) / len(y)
结果:
0.5
对于任意数据迭代器data_iter可访问的数据集, 我们可以评估在任意模型net的精度
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
metric = Accumulator(2) # 正确预测数、预测总数
for X, y in data_iter:
metric.add(accuracy(net(X), y), d2l.size(y))
return metric[0] / metric[1]
这里定义一个实用程序类Accumulator,用于对多个变量进行累加。 在上面的evaluate_accuracy函数中, 我们在Accumulator实例中创建了2个变量, 分别用于存储正确预测的数量和预测的总数量。 当我们遍历数据集时,两者都将随着时间的推移而累加。
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
现在可以直接测试初始模型的准确率
evaluate_accuracy(net, test_iter)
结果:
0.132
3.6 训练
定义一个在动画中绘制数据的实用程序类Animator, 它能够简化本书其余部分的代码。
class Animator: #@save
"""在动画中绘制数据"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
d2l.use_svg_display()
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(
self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)
一个迭代周期的模型训练(形参为网络模型,训练集迭代器,损失函数,优化函数)
def train_epoch_ch3(net, train_iter, loss, updater): #@save
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
with tf.GradientTape() as tape:
y_hat = net(X)
# Keras内置的损失接受的是(标签,预测),这不同于用户在本书中的实现。
# 本书的实现接受(预测,标签),例如我们上面实现的“交叉熵”
if isinstance(loss, tf.keras.losses.Loss):
l = loss(y, y_hat)
else:
l = loss(y_hat, y)
if isinstance(updater, tf.keras.optimizers.Optimizer):
params = net.trainable_variables
grads = tape.gradient(l, params)
updater.apply_gradients(zip(grads, params))
else:
updater(X.shape[0], tape.gradient(l, updater.params))
# Keras的loss默认返回一个批量的平均损失
l_sum = l * float(tf.size(y)) if isinstance(
loss, tf.keras.losses.Loss) else tf.reduce_sum(l)
metric.add(l_sum, accuracy(y_hat, y), tf.size(y))
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
训练函数(形参为,网络模型,训练集迭代器,测试级迭代器,损失函数,迭代周期,优化函数)
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc
定义的小批量随机梯度下降来优化模型的损失函数
class Updater(): #@save
"""用小批量随机梯度下降法更新参数"""
def __init__(self, params, lr):
self.params = params
self.lr = lr
def __call__(self, batch_size, grads):
d2l.sgd(self.params, grads, self.lr, batch_size)
updater = Updater([W, b], lr=0.1)#设置学习率为0.1
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
结果:
3.7 预测
def predict_ch3(net, test_iter, n=6): #@save
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(tf.argmax(net(X), axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(
tf.reshape(X[0:n], (n, 28, 28)), 1, n, titles=titles[0:n])
predict_ch3(net, test_iter)
结果:
4 softmax回归的实现(使用TensorFlow框架)
使用Fashion-MNIST数据集,并保持批量大小为256
import tensorflow as tf
from d2l import tensorflow as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
4.1 定义网络模型
softmax回归的输出层是一个全连接层。因此,为了实现我们的模型,我们只需在Sequential
中添加一个带有10个输出的全连接层。以均值0和标准差0.01随机初始化权重。
net = tf.keras.models.Sequential()
net.add(tf.keras.layers.Flatten(input_shape=(28, 28)))#将输入展平为一维数组
weight_initializer = tf.keras.initializers.RandomNormal(mean=0.0, stddev=0.01)#权重初始化
net.add(tf.keras.layers.Dense(10, kernel_initializer=weight_initializer))#全连接层输出维度是10
4.2 定义交叉熵损失函数
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
4.3 优化算法
学习率为0.1的小批量随机梯度下降作为优化算法
trainer = tf.keras.optimizers.SGD(learning_rate=.1)
4.4训练
调用3.6中定义的训练函数训练模型
num_epochs = 10
train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)