概念与应用
Softmax是机器学习中一个非常重要的工具,他可以兼容 logistics 算法、可以独立作为机器学习的模型进行建模训练、还可以作为深度学习的激励函数。
softmax的作用简单的说就计算一组数值中每个值的占比,公式一般性描述为:
设一共有个用数值表示的分类,其中表示分类的个数。那么softmax计算公式为:
。
在机器学习中经常用它来解决MECE原则的分类——每一个分类相互独立,所有的分类被完全穷尽。比如男人和女人就是负责MECE原则的。
softmax的例子
看一个例子能更好的理解softmax。
设有三个数值,那么他们的softmax占比为:
计算结果为:
基本特性
从上面的计算结果可以看出softmax的一些特性:
- 归一化:最后的合计为1,每一个分类都是一个小于1的数值。
- 放大效果:上面的例子中单纯从数值来看,5和1的差距并不大,但是通过指数运算有明显的放大效果,5的占比能到98%以上。
- 散列性质,每一个比率虽然最后都会进行归一,但是他们放大之前的数值是可以相互不干扰的。
softmax的损失函数
softmax的损失函数可以用交叉熵来表述,也可以用极大似然评估来描述,后续的数学推导结论会发现2个算法的结果都是一样的。
熵与交叉熵
熵
这里所说的熵来源于信息论,他表示“为了确保完整的信息被描述所需要的编码长度”。看起来是一个很拗口的概念,下面看一个例子。
假设26个英文字母每个字母出现概率都是相同的(即),那么记录26个英文字母所需要的信息量是,这个公式就是表述26个字符的熵。如果取表示用一个信息位表示2个信息(也就是我们用来衡量数据大小最小计算单位bit:0/1),那么计算出说明表述所有的英文字符需要5bit的信息量。
交叉熵
在实际使用中大部分事物都不是均匀分布的,比如一篇英文文章中'e'出现出现的频率明显多于其他字符,而且有时也无法知道真实分布的情况。这时计算信息量就可以使用交叉熵,它是在非均匀分布下信息量的一种表述表示:
。这里表示每一个事物的真实概率,表示对应的预估概率。
关于交叉熵的详细说明可以看本人这篇MNIST介绍的文章关于熵与交叉熵的解释说明。
极大似然评估
softmax算法可以看做是一个概率问题,设表示不同的分类,每个分类的概率表示为,其中表示分类的个数。 表示特征数。设,那么在softmax中。用表示分类的真实分布,由于事物分类遵守MECE原则,所以所有的组合在一起实际上是个由1和0组成的数组,只有一个元素为1值。可以参照logistics回归算法:,softmax也可以使用类似的结构: 。用对数最大似然评估作为损失函数:
,
可以看出极大似然评估和交叉熵最后得到的是一模一样的表达式。
将公式扩展为个样本的情况:
损失函数的含义
前面已经提到softmax分类应该遵守MECE原则,所以一个样本属于某个分类会用“占位”的方法来标注。例如现在有三个分类,样本A属于第二个分类表示为[0,1,0]、样本B属于第三个分类表示为[0,0,1]、C属于第一个分类——[1,0,0]。每个数组可以看做是的样本分类的真实概率分布——属于某个分类该分类对应的概率就是1,其他分类概率是0。
特征和权重参数通过softmax计算之后得到的是一个概率分布。假设样本A的特征通过softmax计算后分类的概率是[0.2,0.6,0.2],这个时候对于损失函数的计算结果是:。
我们放大真实分布的比重为[0.1,0.8,0.1]后,计算结果:,放大到[0.05,0.9,0.05]得:。所以一个很直观的感受是:损失函数是从负数无限接近0。
下面通过大量的数据来模拟这个过程。假设所有的样本属于2个分类,样本分类的标注固定为[1,0],随机生成100个样本模拟分类的概率为:
那么这100组数据和损失函数计算结果构成的关系如下图:
由于所有样本的标注都是[1,0],所以
再使用一个过程来确认这个结果。softmax是体现一组数值的占比,被标记的那个分类占比越高越接近真实分布。现在假设有5000组样本,每个样本对应20个分类,每个分类的特征值在0~10之间随机产生,每个样本的标记在0~20之间随机设定。现在看看标记项的概率值与损失函数的关系:
图中softmax highest表示标注项的概率(占比),corss entropy就是损失函数的计算结果。可以看到当标记项概率越接近1,损失的计算结果越接近0。如果有兴趣可以使用生成图像的代码了解分析过程。
建模
softmax计算
上面的内容介绍了softmax的公式以及损失函数。下面说明其如何运算。
在实际应用中一个样本的特征是一个的向量:,每一个特征在计算过程中都有一个权重,所以引入权重参数建立权重结构(直线结构):
,
所以softmax更加完整的代数表达式是:
其中表示计算结果有多少个分类,j表示特征的个数。
有个样本时就扩展为一个2阶张量,那么用矩阵形式表述更加简洁:
用下标表示当前的特征属于第几个样本,例如表示第1个样本的第3个特征。矩阵的计算过程如下:
1.计算权重指数
矩阵中
表示矩阵每一个元素求e指数。所以得到:
令,有:
2.计算分母
现在
是一个形状为元素全为1的矩阵:
分母:
所以:
3.归一化
现在
所以最终
交叉熵(极大似然评估)计算
根据交叉熵的公式,这里是样本的真实分类(标签label),是softmax计算的结果。用矩阵结构表示:
,矩阵表示取对角线元素形成一个的矩阵。
1.对数及矩阵乘积
对数表示对每个元素进行对数运算,他仅改变每个元素的值,对矩阵结构没任何影响,所以下面用继续表示:
2.交叉熵计算
将符号带入公式得到最终的损失函数矩阵计算结果:
把矩阵符号去掉,这里的结果和前面最大似然评估推导的结果一致。
参数优化
通过前文的介绍我们知道,损失函数的目标是获得“最大值”,这个最大值的含义是从负无穷方向接近0的一个极限过程。所以经常会看到很多文章会在指标函数前面添加一个负号,如下面这样:
这样就可以把这个过程转变为求“最小值”——从正无穷方向接近0,本质并没有多大区别。
既然这是一个极限过程,自然就可以用积分原理逐渐计算合理的参数。现在的目标是通过导数和找到递增量可以逐步求解值:
用表示的偏导函数:。的更新公式为:。表示每一步更新的步长。
如果损失函数前携带了负号,那么更新公式应该修改为,即越来越小。
1.求偏导函数
目的已经明确,那么接下来就是数学运算了:
设softmax计算结果一共有M个分类,输入模型的一个样本一共有N个特征。
表示权重计算的结果,下标表示所属的分类,用数组可以表示为:
表示每一个分类softmax计算的结果:,k表示分类迭代求和的下标:用数组表示为:
Loss是最终的损失函数:。表示每一个softmax分类对应的真实概率,取值0或1。
优化参数是不断的调优权重参数,所以把看做自变量求导:
按照前面给出的公式将损失函数的计算分为3步:1)计算权重模型,2)计算softmax,3)计算交叉熵。现在把求导过程分为这3步对、以及复合求导:
计算到这里需要注意一个问题。因为目标是对求导,所以在求和公式中包含的项(即包含的项)和不包含的项求导的结果是不一样的,所以需要将项单独拿出来求导。所以有:
是一个结构为[0,0,0,1,0,0......]的只有一个元素是1其余元素为0的数组,所以它的合计为1,因此得:
虽然推导这个求偏导的过程要花费一些功夫,但是这个结果却非常简单——真实分布与预测分布的差值乘权重参数对应的特征值。如果交叉熵函数中使用了负号,那么导函数为,很多文章更喜欢用这种求最小值的方式。
观察的表达式,和都是已知的数值,在优化的过程中只有会发生改变。所以当预测分布越接近真实分布时增量会越来越接近0。
2.多个样本与矩阵运算
上面求导的过程并没有考虑多个样本的情况,设现在有O个样本。那么求导公式变成:
。
因为每一个子项的求导结果都是向0接近,所以求和再平分之后也是靠近0的。
现在模型参数的更新公式用矩阵表示为:
。其中是的矩阵形,是一个常量,是的矩阵形。
设P表示样本真实分布的矩阵(即标记矩阵),Q是文章前面介绍的softmax矩阵计算的结果,X表示样本矩阵。那么D的矩阵表示为:。
计算法则总结与编码实现
算法总结
经过前面推导分析,softmax机器学习算法建模分为以下几项内容。
1.定义
有个样本、个特征、个分类,。
是样本(feature)矩阵,形状为
是权重矩阵,形状为
是标签(label)矩阵,形状为
是softmax计算后得到的矩阵,形状为
是两个用于合并计算的单位矩阵,形状为(M,1)和(O,1),
矩阵表示转置矩阵,表示取矩阵的对角线元素(类似于特征)。
2.softmax计算
权重指数:
归一化:
3.损失函数
4.参数训练
,训练会重复这个计算,直到变化率“接近”0。
编码实现
以下代码在https://github.com/chkui/ml-math-softmax。
如下图,sample.softmax_train.softmax_modual.Softmax
类模拟了一个softmax机器学习的过程。
import numpy as np
class Softmax:
def __init__(self, features, labels):
self.__features = features
self.__labels = labels
self.__weight = np.zeros((labels.shape[1], features.shape[1]))
# 用于 softmax 归一化计算分布的标量矩阵
self.__e_softmax = np.ones((labels.shape[1], 1))
# 用于 损失函数计算的标量矩阵
self.__e_loss = np.ones((features.shape[0], 1))
# flag用于标记运算符号
# flag如果是-1,那么损失函数就是求最小值,那么优化器求差值。
# flag如果是+1损失函数就是求最大值,那么优化器求和
self.__flag = 1
def __softmax(self):
liner = self.__features * self.__weight.T
exp = np.exp(liner)
den = exp * self.__e_softmax
q = exp / den
return q
def __loss(self, q):
h = self.__labels * np.log(q.T)
h = h.diagonal()
loss = self.__flag * h * self.__e_loss / self.__e_loss.shape[0]
return loss
def __optimizer(self, q, step):
d = ((self.__flag * self.__labels - self.__flag * q).getT() * self.__features) / self.__features.shape[0]
self.__weight = self.__weight + (self.__flag * step) * d
def train(self, handle, repeat=2000, step=0.1):
"""
训练
:param handle: 单轮训练的回调,用于输出各项数据 (count, loss, )
:param repeat: 重复的轮次,每轮会执行一次存储 2000
:param step: 优化器步近量
:return:
"""
print("Weight shape={}".format(self.__weight.shape))
count = 0
while count < repeat:
q = self.__softmax()
loss = self.__loss(q)
self.__optimizer(q, step)
count = count + 1
handle(count, loss)
类中的__softmax
、__loss
、__optimizer
方法分别对应前面介绍的三步计算(归一化,损失函数,参数优化),而在train
方法中就是重复调用这三个方法来不断的优化权重参数。
为了执行训练sample.softmax_train.random_data.RandomData
用于随机生成样本特征和样本标签数据。
下图展示了执行5000次优化过程中Loss的变化趋势:
Count表示执行训练的次数,Loss表示损失函数的输出值,可以发现几个特点:
- 在优化的过程中Loss是逐渐接近0的。
- 反复使用相同的样本(案例中随机生成了500个样本)优化器在前1000次有比较明显的效果,但是后续增长乏力。
由于使用的是随机数据,所以收敛的效果并不太理想,但是总的趋势还是收敛。后续的博文中本人会使用MNIST之类的真实数据来测试验证softmax。
Github的代码中除了softmax_train
用于演示训练和收敛的效果,还有softmax_estimator
和softmax_compute
。前者提供了参数相关的磁盘操作,后者简单展示了softmax算法的编码实现,需要了解的可以到代码库中查看。