softmax及交叉熵损失的反向传递推导及实现
一般网络最后一个线性层后面都会跟一个输出层,比如sigmoid,softmax等,其基本做法是将输出Zi取指数再归一化。下面记录一下softmax和交叉熵的反向传递推导。
参考博文:https://blog.csdn.net/u014313009/article/details/51045303
下面做一个摘录:
1. softmax函数及其求导
softmax的函数公式如下:
其中,表示第L层(通常是最后一层)第j个神经元的输入,表示第L层第j个神经元的输出,表示自然常数。注意看,表示了第L层所有神经元的输入之和。
softmax函数最明显的特点在于:它把每个神经元的输入占当前层所有神经元输入之和的比值,当作该神经元的输出。这使得输出更容易被解释:神经元的输出值越大,则该神经元对应的类别是真实类别的可能性更高。
另外,softmax不仅把神经元输出构造成概率分布,而且还起到了归一化的作用,适用于很多需要进行归一化处理的分类问题。
由于softmax在ANN算法中的求导结果比较特别,分为两种情况。希望能帮助到正在学习此类算法的朋友们。求导过程如下所示:
2. softmax配合log似然代价函数训练ANN
在上一篇博文“交叉熵代价函数”中讲到,二次代价函数在训练ANN时可能会导致训练速度变慢的问题。那就是,初始的输出值离真实值越远,训练速度就越慢。这个问题可以通过采用交叉熵代价函数来解决。其实,这个问题也可以采用另外一种方法解决,那就是采用softmax**函数,并采用log似然代价函数(log-likelihood cost function)来解决。
log似然代价函数的公式为:
其中,表示第k个神经元的输出值,表示第k个神经元对应的真实值,取值为0或1。
我们来简单理解一下这个代价函数的含义。在ANN中输入一个样本,那么只有一个神经元对应了该样本的正确类别;若这个神经元输出的概率值越高,则按照以上的代价函数公式,其产生的代价就越小;反之,则产生的代价就越高。
为了检验softmax和这个代价函数也可以解决上述所说的训练速度变慢问题,接下来的重点就是推导ANN的权重w和偏置b的梯度公式。以偏置b为例:
同理可得:
从上述梯度公式可知,softmax函数配合log似然代价函数可以很好地训练ANN,不存在学习速度变慢的问题。
总体上推导没什么问题,看看网友评论说最后结果应该是yj(aj-1),yj=1,结果难度不是一样吗?推论上,softmax层前面是一个线性层,实际使用时,只需要将求C对z的梯度就可以了,z对w和b的梯度由fc层实现就可以啦,这里进行一个纸质推导:
下面看一下dlib中对该层的实现:
class loss_multiclass_log_
{
public:
typedef unsigned long training_label_type;
typedef unsigned long output_label_type;
template <
typename SUB_TYPE,
typename label_iterator
>
void to_label (
const tensor& input_tensor,
const SUB_TYPE& sub,
label_iterator iter
) const
{
const tensor& output_tensor = sub.get_output();
DLIB_CASSERT(sub.sample_expansion_factor() == 1);
DLIB_CASSERT(output_tensor.nr() == 1 &&
output_tensor.nc() == 1 );
DLIB_CASSERT(input_tensor.num_samples() == output_tensor.num_samples());
// Note that output_tensor.k() should match the number of labels.
for (long i = 0; i < output_tensor.num_samples(); ++i)
{
// The index of the largest output for this sample is the label.
*iter++ = index_of_max(rowm(mat(output_tensor),i));
}
}
template <
typename const_label_iterator,
typename SUBNET
>
double compute_loss_value_and_gradient (//这个是主要方法,输入tensor,通过sub计算得到output,计算同真实标签truth的loss,
const tensor& input_tensor, //计算loss对output的梯度,并写入到sub中
const_label_iterator truth,
SUBNET& sub
) const
{
const tensor& output_tensor = sub.get_output();
tensor& grad = sub.get_gradient_input();
DLIB_CASSERT(sub.sample_expansion_factor() == 1);
DLIB_CASSERT(input_tensor.num_samples() != 0);
DLIB_CASSERT(input_tensor.num_samples()%sub.sample_expansion_factor() == 0);
DLIB_CASSERT(input_tensor.num_samples() == grad.num_samples());
DLIB_CASSERT(input_tensor.num_samples() == output_tensor.num_samples());
DLIB_CASSERT(output_tensor.nr() == 1 &&
output_tensor.nc() == 1);
DLIB_CASSERT(grad.nr() == 1 &&
grad.nc() == 1);
tt::softmax(grad, output_tensor);//计算a
// The loss we output is the average loss over the mini-batch.
const double scale = 1.0/output_tensor.num_samples();
double loss = 0;
float* g = grad.host();//sub的误差,也就是loss对output的梯度
for (long i = 0; i < output_tensor.num_samples(); ++i)
{
const long y = (long)*truth++;
// The network must produce a number of outputs that is equal to the number
// of labels when using this type of loss.
DLIB_CASSERT(y < output_tensor.k(), "y: " << y << ", output_tensor.k(): " << output_tensor.k());
for (long k = 0; k < output_tensor.k(); ++k)
{
const unsigned long idx = i*output_tensor.k()+k;
if (k == y)
{
loss += scale*-std::log(g[idx]);//-log(a)
g[idx] = scale*(g[idx]-1);//计算梯度,详见推导
}
else
{
g[idx] = scale*g[idx];//计算梯度
}
}
}
return loss;
}
friend void serialize(const loss_multiclass_log_& , std::ostream& out)
{
serialize("loss_multiclass_log_", out);
}
friend void deserialize(loss_multiclass_log_& , std::istream& in)
{
std::string version;
deserialize(version, in);
if (version != "loss_multiclass_log_")
throw serialization_error("Unexpected version found while deserializing dlib::loss_multiclass_log_.");
}
friend std::ostream& operator<<(std::ostream& out, const loss_multiclass_log_& )
{
out << "loss_multiclass_log";
return out;
}
friend void to_xml(const loss_multiclass_log_& /*item*/, std::ostream& out)
{
out << "<loss_multiclass_log/>";
}
};