Softmax Function
在这个实验中,我们将探讨softmax函数。该函数用于Softmax回归和神经网络中解决多类分类问题。
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('./deeplearning.mplstyle')
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from IPython.display import display, Markdown, Latex
from sklearn.datasets import make_blobs
%matplotlib widget
from matplotlib.widgets import Slider
from lab_utils_common import dlc
from lab_utils_softmax import plt_softmax
import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR)
tf.autograph.set_verbosity(0)
通常,在本课程中,笔记本使用的惯例是计数从0开始,以N-1结束, ∑ i = 0 N − 1 \sum_{i=0}^{N-1} ∑i=0N−1,而讲座从1开始,以N结束, ∑ i = 1 N \sum_{i=1}^{N} ∑i=1N。这是因为代码通常从0开始迭代,而在课堂上,从1数到N会得到更清晰、更简洁的方程。这个笔记本有更多的方程,而不是典型的实验室,因此将打破惯例,将从1到N计数。
Softmax Function
在softmax回归和具有softmax输出的神经网络中,都产生N个输出,并选择一个输出作为预测类别。在这两种情况下,向量 z \mathbf{z} z都是由应用于softmax函数的线性函数生成的。softmax函数将 z \mathbf{z} z转换为如下所述的概率分布。应用softmax后,每个输出将在0到1之间,输出将加1,因此它们可以被解释为概率。更大的输入将对应更大的输出概率。
softmax函数可以写成:
a
j
=
e
z
j
∑
k
=
1
N
e
z
k
(1)
a_j = \frac{e^{z_j}}{ \sum_{k=1}^{N}{e^{z_k} }} \tag{1}
aj=∑k=1Nezkezj(1)
输出a是一个长度为N的向量,所以对于softmax回归,你也可以这样写:
这表明输出是一个概率向量。第一个条目是给定输入
x
\mathbf{x}
x和参数
w
\mathbf{w}
w和
b
\mathbf{b}
b,输入是第一个类别的概率。
让我们创建一个NumPy实现:
def my_softmax(z):
ez = np.exp(z) #element-wise exponenial
sm = ez/np.sum(ez)
return(sm)
下面,使用滑块改变“z”输入的值
plt.close("all")
plt_softmax(my_softmax)
当你改变上面z的值时,有几件事需要注意:
- softmax分子中的指数放大了数值中的微小差异
- 输出值之和为1
- softmax跨越所有输出。例如,’ z0 ‘的变化将改变’ a0 ’ - ’ a3 '的值。将此与具有单个输入和单个输出的其他激活(如ReLu或Sigmoid)进行比较。
开销
与Softmax相关的损失函数,即交叉熵损失为:
其中y是本例的目标类别,
a
\mathbf{a}
a是softmax函数的输出。特别地,
a
\mathbf{a}
a中的值是求和为1的概率。
在本课程中,损失是一个例子,而成本涵盖了所有的例子。
注意在上面(3)中,只有目标对应的线对损失有贡献,其他线为零。为了编写成本方程,我们需要一个“指示函数”,当指标与目标匹配时,它将为1,否则为零。
1
{
y
=
=
n
}
=
=
{
1
,
if
y
=
=
n
.
0
,
otherwise
.
\mathbf{1}\{y == n\} = =\begin{cases} 1, & \text{if $y==n$}.\\ 0, & \text{otherwise}. \end{cases}
1{y==n}=={1,0,if y==n.otherwise.
其中
m
m
m为示例数,
N
N
N为输出数。这是所有损失的平均值。
Tensorflow
本实验将讨论两种实现softmax的方法,Tensorflow中的交叉熵损失,“明显”方法和“首选”方法。前者是最直接的,而后者在数值上更稳定。
让我们从创建一个数据集来训练一个多类分类模型开始。
# make dataset for example
centers = [[-5, 2], [-2, -2], [1, 2], [5, -2]]
X_train, y_train = make_blobs(n_samples=2000, centers=centers, cluster_std=1.0,random_state=30)
明显的组织
下面的模型是用softmax作为最终Dense层的激活来实现的。损失函数在compile指令中单独指定。
损失函数SparseCategoricalCrossentropy。上述(3)项所述的损失。在这个模型中,softmax发生在最后一层。损失函数接受softmax输出,这是一个概率向量。
model = Sequential(
[
Dense(25, activation = 'relu'),
Dense(15, activation = 'relu'),
Dense(4, activation = 'softmax') # < softmax activation here
]
)
model.compile(
loss=tf.keras.losses.SparseCategoricalCrossentropy(),
optimizer=tf.keras.optimizers.Adam(0.001),
)
model.fit(
X_train,y_train,
epochs=10
)
因为softmax被集成到输出层中,所以输出是一个概率向量。
p_nonpreferred = model.predict(X_train)
print(p_nonpreferred [:2])
print("largest value", np.max(p_nonpreferred), "smallest value", np.min(p_nonpreferred))
Preferred
回顾讲座,如果在训练中结合softmax和loss,可以得到更加稳定和准确的结果。这是由这里显示的“首选”组织启用的。
在首选组织中,最后一层具有线性激活。由于历史原因,这种形式的输出被称为对数。损失函数有一个额外的参数:from_logits = True。这将通知损失函数,softmax操作应包含在损失计算中。这允许优化实现。
preferred_model = Sequential(
[
Dense(25, activation = 'relu'),
Dense(15, activation = 'relu'),
Dense(4, activation = 'linear') #<-- Note
]
)
preferred_model.compile(
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), #<-- Note
optimizer=tf.keras.optimizers.Adam(0.001),
)
preferred_model.fit(
X_train,y_train,
epochs=10
)
Output Handling
注意,在首选模型中,输出不是概率,而是从大的负数到大的正数的范围。当执行期望概率的预测时,必须通过softmax发送输出。
让我们看看首选模型输出:
p_preferred = preferred_model.predict(X_train)
print(f"two example output vectors:\n {p_preferred[:2]}")
print("largest value", np.max(p_preferred), "smallest value", np.min(p_preferred))
输出预测不是概率!如果期望的输出是概率,输出应该由softmax处理。
sm_preferred = tf.nn.softmax(p_preferred).numpy()
print(f"two example output vectors:\n {sm_preferred[:2]}")
print("largest value", np.max(sm_preferred), "smallest value", np.min(sm_preferred))
要选择最可能的类别,不需要softmax。可以使用np.argmax()找到最大输出的索引。
for i in range(5):
print( f"{p_preferred[i]}, category: {np.argmax(p_preferred[i])}")
SparseCategorialCrossentropy或CategoricalCrossEntropyTensorflow有两种可能的目标值格式,损失的选择定义了预期的格式。
- SparseCategorialCrossentropy:期望目标是与索引对应的整数。例如,如果有10个潜在的目标值,y将在0到9之间。
- CategoricalCrossEntropy:期望示例的目标值为one-hot编码,其中目标索引处的值为1,而其他N-1项为零。例如,有10个可能的目标值,其中目标为2,则为[0,0,1,0,0,0,0,0,0,0]。
祝贺
在这个实验中
- 更加熟悉softmax函数及其在softmax回归和神经网络中softmax激活中的应用。
- 学习在Tensorflow中首选的模型构建:
最后一层没有激活(和线性激活一样)- SparseCategoricalCrossentropy损失函数
- 使用from_logits=True
- 认识到与ReLu和Sigmoid不同,softmax跨越多个输出。
数值稳定性(可选)
本节讨论一些用来提高数值稳定性的方法。这是为感兴趣的读者准备的,完全不是必需的。
Softmax数值稳定性
softmax的输入是线性层
z
j
=
w
j
⋅
x
(
i
)
+
b
z_j = \mathbf{w_j} \cdot \mathbf{x}^{(i)}+b
zj=wj⋅x(i)+b的输出。这些可能
是大数字。softmax算法的第一步计算
e
z
j
e^{z_j}
ezj。如果数目太大,这可能导致溢出错误。尝试运行下面的单元格:
for z in [500,600,700,800]:
ez = np.exp(z)
zs = "{" + f"{z}" + "}"
print(f"e^{zs} = {ez:0.2e}")
如果指数太大,操作将产生溢出。当然,my_softmax()也会产生同样的错误:
z_tmp = np.array([[500,600,700,800]])
my_softmax(z_tmp)
通过减小指数的大小可以提高数值稳定性。
回忆
e
a
+
b
=
e
a
e
b
e^{a + b} = e^ae^b
ea+b=eaeb
如果
b
b
b是
a
a
a的相反符号,这将减小指数的大小。具体来说,如果你将softmax乘以一个分数:
a
j
=
e
z
j
∑
i
=
1
N
e
z
i
e
−
b
e
−
b
a_j = \frac{e^{z_j}}{ \sum_{i=1}^{N}{e^{z_i} }} \frac{e^{-b}}{ {e^{-b}}}
aj=∑i=1Neziezje−be−b
指数会减少,softmax的值不会改变。如果
e
b
e^b
eb中的
b
b
b是
z
j
z_j
zj s,
m
a
x
j
(
z
)
max_j(\mathbf{z})
maxj(z)的最大值,则指数将减少到其最小值。
a
j
=
e
z
j
∑
i
=
1
N
e
z
i
e
−
m
a
x
j
(
z
)
e
−
m
a
x
j
(
z
)
=
e
z
j
−
m
a
x
j
(
z
)
∑
i
=
1
N
e
z
i
−
m
a
x
j
(
z
)
\begin{align} a_j &= \frac{e^{z_j}}{ \sum_{i=1}^{N}{e^{z_i} }} \frac{e^{-max_j(\mathbf{z})}}{ {e^{-max_j(\mathbf{z})}}} \\ &= \frac{e^{z_j-max_j(\mathbf{z})}}{ \sum_{i=1}^{N}{e^{z_i-max_j(\mathbf{z})} }} \end{align}
aj=∑i=1Neziezje−maxj(z)e−maxj(z)=∑i=1Nezi−maxj(z)ezj−maxj(z)
a j = e z j − C ∑ i = 1 N e z i − C where C = m a x j ( z ) (5) a_j = \frac{e^{z_j-C}}{ \sum_{i=1}^{N}{e^{z_i-C} }} \quad\quad\text{where}\quad C=max_j(\mathbf{z})\tag{5} aj=∑i=1Nezi−Cezj−CwhereC=maxj(z)(5)
def my_softmax_ns(z):
"""numerically stablility improved"""
bigz = np.max(z)
ez = np.exp(z-bigz) # minimize exponent
sm = ez/np.sum(ez)
return(sm)
让我们尝试一下,并将其与tensorflow的实现进行比较:
z_tmp = np.array([500.,600,700,800])
print(tf.nn.softmax(z_tmp).numpy(), "\n", my_softmax_ns(z_tmp))
交叉熵损失数值稳定性
与Softmax相关的损失函数,即交叉熵损失,在这里重复:
其中y是本例的目标类别,
a
\mathbf{a}
a是softmax函数的输出。特别地,
a
\mathbf{a}
a中的值是求和为1的概率。
让我们考虑一个目标为2 (
y
=
2
y=2
y=2)的情况,并看看这种情况下的损失。这将导致损失如下:
L
(
a
)
=
−
l
o
g
(
a
2
)
L(\mathbf{a})= -log(a_2)
L(a)=−log(a2)
回想一下,
a
2
a_2
a2是上面描述的softmax函数的输出,所以可以这样写:
L
(
z
)
=
−
l
o
g
(
e
z
2
∑
i
=
1
N
e
z
i
)
(6)
L(\mathbf{z})= -log\left(\frac{e^{z_2}}{ \sum_{i=1}^{N}{e^{z_i} }}\right) \tag{6}
L(z)=−log(∑i=1Neziez2)(6)
这是可以优化的。然而,为了进行这些优化,softmax和损失必须一起计算,如您在上面看到的“首选”Tensorflow实现所示。
由上式(6)出发,y=2时的损失:
l
o
g
(
a
b
)
=
l
o
g
(
a
)
−
l
o
g
(
b
)
log(\frac{a}{b}) = log(a) - log(b)
log(ba)=log(a)−log(b),所以(6)可以改写为:
L
(
z
)
=
−
[
l
o
g
(
e
z
2
)
−
l
o
g
∑
i
=
1
N
e
z
i
]
(7)
L(\mathbf{z})= -\left[log(e^{z_2}) - log \sum_{i=1}^{N}{e^{z_i} }\right] \tag{7}
L(z)=−[log(ez2)−logi=1∑Nezi](7)
第一项可以简化为
z
2
z_2
z2:
L
(
z
)
=
−
[
z
2
−
l
o
g
(
∑
i
=
1
N
e
z
i
)
]
=
l
o
g
∑
i
=
1
N
e
z
i
⏟
logsumexp()
−
z
2
(8)
L(\mathbf{z})= -\left[z_2 - log( \sum_{i=1}^{N}{e^{z_i} })\right] = \underbrace{log \sum_{i=1}^{N}{e^{z_i} }}_\text{logsumexp()} -z_2 \tag{8}
L(z)=−[z2−log(i=1∑Nezi)]=logsumexp()
logi=1∑Nezi−z2(8)
事实证明,上述等式中的
l
o
g
∑
i
=
1
N
e
z
i
log \sum_{i=1}^{N}{e^{z_i} }
log∑i=1Nezi项经常被使用,许多库都有实现。在Tensorflow中,这是tf.math.reduce_logsumexp()。关于t的问题
l
o
g
∑
i
=
1
N
e
z
i
=
l
o
g
∑
i
=
1
N
e
(
z
i
−
m
a
x
j
(
z
)
+
m
a
x
j
(
z
)
)
=
l
o
g
∑
i
=
1
N
e
(
z
i
−
m
a
x
j
(
z
)
)
e
m
a
x
j
(
z
)
=
l
o
g
(
e
m
a
x
j
(
z
)
)
+
l
o
g
∑
i
=
1
N
e
(
z
i
−
m
a
x
j
(
z
)
)
=
m
a
x
j
(
z
)
+
l
o
g
∑
i
=
1
N
e
(
z
i
−
m
a
x
j
(
z
)
)
\begin{align} log \sum_{i=1}^{N}{e^{z_i} } &= log \sum_{i=1}^{N}{e^{(z_i - max_j(\mathbf{z}) + max_j(\mathbf{z}))}} \tag{9}\\ &= log \sum_{i=1}^{N}{e^{(z_i - max_j(\mathbf{z}))} e^{max_j(\mathbf{z})}} \\ &= log(e^{max_j(\mathbf{z})}) + log \sum_{i=1}^{N}{e^{(z_i - max_j(\mathbf{z}))}} \\ &= max_j(\mathbf{z}) + log \sum_{i=1}^{N}{e^{(z_i - max_j(\mathbf{z}))}} \end{align}
logi=1∑Nezi=logi=1∑Ne(zi−maxj(z)+maxj(z))=logi=1∑Ne(zi−maxj(z))emaxj(z)=log(emaxj(z))+logi=1∑Ne(zi−maxj(z))=maxj(z)+logi=1∑Ne(zi−maxj(z))(9)
现在,指数不太可能溢出。习惯上说
C
=
m
a
x
j
(
z
)
C=max_j(\mathbf{z})
C=maxj(z),因为方程对于任何常数C都是正确的。我们现在可以写出损失方程:
L
(
z
)
=
C
+
l
o
g
(
∑
i
=
1
N
e
z
i
−
C
)
−
z
2
where
C
=
m
a
x
j
(
z
)
(10)
L(\mathbf{z})= C+ log( \sum_{i=1}^{N}{e^{z_i-C} }) -z_2 \;\;\;\text{where } C=max_j(\mathbf{z}) \tag{10}
L(z)=C+log(i=1∑Nezi−C)−z2where C=maxj(z)(10)
一个计算上更简单,更稳定的损失版本。上面是一个例子,其中目标y=2,但推广到任何目标。