(三)Softmax的原理及代码实现

Softmax

本系列重点在浅显易懂,快速上手。不进行过多的理论讲解:也就是不去深究what,而是关注how。全文围绕以下三个问题展开:

1)长什么样?

2)解决什么问题?

3)怎么实现?

​ 3.1)从数学讲,原理

​ 3.2)从代码上讲,如何掉包实现

长什么样

上节讲了逻辑回归实现2分类,那么多分类的问题,就要通过softmax来实现,在此之前回忆“线性回归”和“逻辑回归”

1)线性回归建模,用于预测y是连续值的问题,值域的范围是(-∞,+∞)最终的目的就是为了找到一组 θ 1 , θ 2 , . . . , θ 13 , θ 0 θ_1,θ_2,...,θ_{13},θ_0 θ1,θ2,...,θ13,θ0(除了系数,还包含一个常数项 θ 0 θ_0 θ0,使得下式成立。

θ 1 ∗ x 1 + θ 2 ∗ x 2 + . . . . . . + θ k ∗ x k + θ 0 = y θ_1*x_1+θ_2*x_2+......+θ_{k}*x_k+θ_0=y θ1x1+θ2x2+......+θkxk+θ0=y

2)逻辑回归建模,用于解决二分类问题,y值代表的是取1的概率。值域的范围是[0,1]最终的目的就是为了找到一组 θ 1 , θ 2 , θ 3 , θ 4 , θ 0 θ_1,θ_2,θ_3,θ_4,θ_0 θ1,θ2,θ3,θ4,θ0(除了4个系数,还包含一个常数项 θ 0 θ_0 θ0),使得下式成立。
1 1 + e − ( θ 1 ∗ x 1 + θ 2 ∗ x 2 + θ 3 ∗ x 3 + θ k ∗ x k + θ 0 ) = 1 1 + e − θ T x = y \frac{1}{1+e^{-(θ_1*x_1+θ_2*x_2+θ_3*x_3+θ_k*x_k+θ_0)}} = \frac{1}{1+e^{-θ^Tx}} = y 1+e(θ1x1+θ2x2+θ3x3+θkxk+θ0)1=1+eθTx1=y

3)softmax模型,用于解决多分类问题,假如要解决3分类问题。模型的输出应该是什么形式的? 假如还是按逻辑回归的方式,输出一个[0,1]之间的值,那这个值应该代表取到类别1,类别2,类别的概率?所以,应该同时输出3个值, y 1 , y 2 , y 3 y_1,y_2,y_3 y1,y2,y3,再配个分母,即:
[ y 1 y 1 + y 2 + y 3 , y 2 y 1 + y 2 + y 3 , y 3 y 1 + y 2 + y 3 ] \left[\\ \frac{y_1}{y_1+y_2+y_3},\\ \frac{y_2}{y_1+y_2+y_3},\\ \frac{y_3}{y_1+y_2+y_3}\\ \right] [y1+y2+y3y1,y1+y2+y3y2,y1+y2+y3y3]

分别代表是类别1,类别2,类别3的概率,加起来为1。具体的函数解析式,应该是如下形式:

θ 1 _ 1 ∗ x 1 + θ 1 _ 2 ∗ x 2 + . . . . . . + θ 1 _ n ∗ x n + θ 1 _ 0 = y 1 θ 2 _ 1 ∗ x 1 + θ 2 _ 2 ∗ x 2 + . . . . . . + θ 2 _ n ∗ x n + θ 3 _ 0 = y 2 θ 3 _ 1 ∗ x 1 + θ 3 _ 2 ∗ x 2 + . . . . . . + θ 3 _ n ∗ x n + θ 3 _ 0 = y 3 θ_{1\_1}*x_1+θ_{1\_2}*x_2+......+θ_{1\_n}*x_n+θ_{1\_0}=y_1\\ θ_{2\_1}*x_1+θ_{2\_2}*x_2+......+θ_{2\_n}*x_n+θ_{3\_0}=y_2\\ θ_{3\_1}*x_1+θ_{3\_2}*x_2+......+θ_{3\_n}*x_n+θ_{3\_0}=y_3 θ1_1x1+θ1_2x2+......+θ1_nxn+θ1_0=y1θ2_1x1+θ2_2x2+......+θ2_nxn+θ3_0=y2θ3_1x1+θ3_2x2+......+θ3_nxn+θ3_0=y3

相应的参数也是原来的3倍,即:

Expected node of symbol group type, but got node of type cr

但还有一个问题, y 1 , y 2 , y 3 y_1,y_2,y_3 y1,y2,y3,的值有可能是负值,这是不合理的,所以取e指数,取了e指数后各个值之间的差异也会被放大。即:

[ e y 1 e y 1 + y 2 + y 3 , e y 2 e y 1 + y 2 + y 3 , e y 3 e y 1 + y 2 + y 3 ] \left[\\ \frac{e^{y_1}}{e^{y_1+y_2+y_3}},\\ \frac{e^{y_2}}{e^{y_1+y_2+y_3}},\\ \frac{e^{y_3}}{e^{y_1+y_2+y_3}}\\ \right] [ey1+y2+y3ey1,ey1+y2+y3ey2,ey1+y2+y3ey3]

写成一般形式如下:

p ( y = k ∣ x ; θ ) = e θ k T x ∑ l = 1 K e θ l T x , k = 1 , 2 ⋯   , K p\big(y=k\mid x;\theta\big)=\frac{e^{\theta_{k}^{T}x}}{\sum_{l=1}^{K}e^{\theta_{l}^{T}x}},k=1,2\cdots,K p(y=kx;θ)=l=1KeθlTxeθkTx,k=1,2,K

即:

h θ ( x ) = [ p ( y ( i ) = 1 ∣ x ( i ) ; θ ) p ( y ( i ) = 2 ∣ x ( i ) ; θ ) . . . p ( y ( i ) = k ∣ x ( i ) ; θ ) ] = 1 ∑ j = 1 k e θ j T x ( i ) [ e θ 1 T x e θ 2 T x . . . e θ k T x ] h_{\theta}(x)=\begin{bmatrix}p\Big(y^{(i)}=1\mid x^{(i)};\theta\Big)\\p\Big(y^{(i)}=2\mid x^{(i)};\theta\Big)\\...\\p\Big(y^{(i)}=k\mid x^{(i)};\theta\Big)\end{bmatrix}=\dfrac{1}{\sum_{j=1}^ke^{\theta_j^Tx^{(i)}}}\begin{bmatrix}e^{\theta_1^Tx}\\e^{\theta_2^Tx}\\...\\e^{\theta_k^Tx}\end{bmatrix} hθ(x)= p(y(i)=1x(i);θ)p(y(i)=2x(i);θ)...p(y(i)=kx(i);θ) =j=1keθjTx(i)1 eθ1Txeθ2Tx...eθkTx

⟹ θ = [ θ 11 θ 12 . . . θ 1 n θ 21 θ 22 . . . θ 2 n . . . . . . . . . . . . θ k 1 θ k 2 . . . θ k n ] \Longrightarrow\theta=\begin{bmatrix}\theta_{11}&\theta_{12}&...&\theta_{1n}\\\theta_{21}&\theta_{22}&...&\theta_{2n}\\...&...&...&...\\\theta_{k1}&\theta_{k2}&...&\theta_{kn}\end{bmatrix} θ= θ11θ21...θk1θ12θ22...θk2............θ1nθ2n...θkn

解决什么问题

先看一组数据(一组sklearn中公开的数据集,鸢尾花的种类预测,共有4个特征,最后一列y是花的种类,共有3种类型,这里只展示了6条数据,实际数据有150条),读到这里,有读者会问,y不是应该是3个值吗,这里为什么是1个,这其实是一种简略写法,以第一个y为例子,0表示这个花是类别0,写成程序能理解的方式应该是[1,0,0]。同理,第二个y为1表示,这个花是类别是1,写成程序能理解的方式应该是[0,1,0]。以此类推。

x1x2x3x4y
5.13.71.50.40
5.82.73.91.21
5.13.81.90.40
6.73.35.72.52
5.52.43.711
5.935.11.82

采用softmax建模,最终的目的就是为了找到一组

θ = [ θ 11 θ 12 θ 13 θ 14 θ 10 θ 21 θ 22 θ 23 θ 24 θ 20 . . . . . . . . . . . . θ k 1 θ k 2 θ k 3 θ k 4 θ k 0 ] \theta= \begin{bmatrix} \theta_{11}&\theta_{12}&\theta_{13}&\theta_{14}&\theta_{10}\\ \theta_{21}&\theta_{22}&\theta_{23}&\theta_{24}&\theta_{20}\\ ...&...&...&...\\ \theta_{k1}&\theta_{k2}&\theta_{k3}&\theta_{k4}&\theta_{k0} \end{bmatrix} θ= θ11θ21...θk1θ12θ22...θk2θ13θ23...θk3θ14θ24...θk4θ10θ20θk0

(除了4个系数,还包含一个常数项 θ 0 θ_0 θ0),使得下式成立。

θ 11 ∗ x 1 + θ 12 ∗ x 2 + θ 13 ∗ x 3 + θ 14 ∗ x 4 + θ 10 = y 1 θ 21 ∗ x 1 + θ 22 ∗ x 2 + θ 23 ∗ x 3 + θ 24 ∗ x 4 + θ 20 = y 2 θ 31 ∗ x 1 + θ 32 ∗ x 2 + θ 33 ∗ x 3 + θ 34 ∗ x 4 + θ 30 = y 3 p ( y = k ∣ x ; θ ) = e θ k T x ∑ l = 1 K e θ l T x , k = 1 , 2 , 3 {θ_{11}*x_1+θ_{12}*x_2+θ_{13}*x_3+θ_{14}*x_4+θ_{10}}=y_1\\ {θ_{21}*x_1+θ_{22}*x_2+θ_{23}*x_3+θ_{24}*x_4+θ_{20}}=y_2\\ {θ_{31}*x_1+θ_{32}*x_2+θ_{33}*x_3+θ_{34}*x_4+θ_{30}}=y_3\\ p\big(y=k\mid x;\theta\big)=\frac{e^{\theta_{k}^{T}x}}{\sum_{l=1}^{K}e^{\theta_{l}^{T}x}},k=1,2,3 θ11x1+θ12x2+θ13x3+θ14x4+θ10=y1θ21x1+θ22x2+θ23x3+θ24x4+θ20=y2θ31x1+θ32x2+θ33x3+θ34x4+θ30=y3p(y=kx;θ)=l=1KeθlTxeθkTx,k=1,2,3

怎么实现

数学原理

和上节讲到的逻辑回归一样的思路,通过最大似然估计法,得到损失函数。实际上分类问题的损失函数是一样的,称为交叉熵损失函数(也叫对数损失函数),这个不必再推到一边,直接可以写出来:

还是看上文的第一条样本,真实值是[1,0,0],假设预测值是[0.7,0.2,0.1]。则,对应的交叉熵损失函数是:

J ( θ ) = 1 ∗ l o g ( 0.7 ) + 0 ∗ l o g ( 0.2 ) + 0 ∗ l o g ( 0.1 ) J(θ) = 1*log(0.7) + 0*log(0.2) + 0*log(0.1) J(θ)=1log(0.7)+0log(0.2)+0log(0.1)

是不是很简单,写成一般式,即:

J ( θ ) = − 1 m ∑ i = 1 m ∑ j = 1 k I ( y ( i ) = j ) l n ( e θ j T x ( i ) ∑ i = 1 k e θ j T x ( i ) ) I ( y ( i ) = j ) = { 1 , y ( i ) = j 0 , y ( i ) ≠ j J(\theta)=-\frac{1}{m}\sum_{i=1}^{m}\sum_{j=1}^{k}I\Big(y^{(i)}=j\Big)\mathrm{ln}\Bigg(\frac{e^{\theta_j^Tx^{(i)}}}{\sum_{i=1}^{k}e^{\theta_j^Tx^{(i)}}}\Bigg)\quad I\Big(y^{(i)}=j\Big)=\begin{cases}1,&y^{(i)}=j\\0,&y^{(i)}\neq j\end{cases} J(θ)=m1i=1mj=1kI(y(i)=j)ln(i=1keθjTx(i)eθjTx(i))I(y(i)=j)={1,0,y(i)=jy(i)=j

k是类别的个数,在上文中是3,m表示样本的个数,上文中是150。
同样的,有了损失函数(目标函数),后求损失函数的最小值,先求导:

∂ ∂ θ j J ( θ ) = ∂ ∂ θ j − I ( y ( i ) = j ) l n ( e θ j T x ( i ) ∑ l = 1 k e θ l T x ( i ) ) = ∂ ∂ θ j − I ( y ( i ) = j ) ( θ j T x ( i ) − ln ⁡ ( ∑ l = 1 k e θ l T x ( i ) ) ) = − I ( y ( i ) = j ) ( 1 − e θ j T x ( i ) ∑ l = 1 k e θ l T x ( l ) ) x ( i ) \begin{aligned} \frac\partial{\partial\theta_j}J(\theta)= \frac\partial{\partial\theta_j}-I{\left(y^{(i)}=j\right)}\mathrm{ln}\left(\frac{e^{\theta_j^Tx^{(i)}}}{\sum_{l=1}^ke^{\theta_l^Tx^{(i)}}}\right)\\ =\frac{\partial}{\partial\theta_{j}}-I\Big(y^{(i)}=j\Big)\Bigg(\theta_{j}^{T}x^{(i)}-\ln\Bigg(\sum_{l=1}^{k}e^{\theta_{l}^{T}x^{(i)}}\Bigg)\Bigg)\\ =-I\Big(y^{(i)}=j\Big)\left(1-\frac{e^{\theta_j^Tx^{(i)}}}{\sum_{l=1}^ke^{\theta_l^Tx^{(l)}}}\right)x^{(i)} \end{aligned} θjJ(θ)=θjI(y(i)=j)ln(l=1keθlTx(i)eθjTx(i))=θjI(y(i)=j)(θjTx(i)ln(l=1keθlTx(i)))=I(y(i)=j)(1l=1keθlTx(l)eθjTx(i))x(i)

写成矩阵的形式:
∂ ∂ θ j J ( θ ) = − 1 ∗ X T ∗ [ ( 1 − h θ ( x ) ) ⋅ Y ] \frac\partial{\partial\theta_j}J(\theta)=-1*X^T*[(1-h_θ(x)) \cdot Y] θjJ(θ)=1XT[(1hθ(x))Y]

∗ * 表示矩阵的乘法, ⋅ \cdot 表示对应元素相乘。

求出倒数后,就利用梯度下降法求解 θ θ θ,不断的迭代,直到损失函数的值不再降低。

θ = θ − α ⋅ ∂ ∂ θ j J ( θ ) \theta = \theta - \alpha\cdot\frac\partial{\partial\theta_j}J(\theta) θ=θαθjJ(θ)

手动代码实现

#_*_coding=utf-8_*_
import numpy as np
import random

class softmax():
    
    def __init__(self,X,Y,theta=None,epoch=10000,batch=100,eta=0.001,is_standard=True):
        '''
        self.X = m*n  m个样本,n个特征,
        self.Y = m*k  k个分类
        self.theta = n*k
        '''
        self.X = X
        self.Y = Y
        self.theta = theta
        self.epoch = epoch
        self.eta = eta
    
    def pre(self):
        predict = np.exp(np.dot(self.X, self.theta))
        predict = predict/np.sum(predict,axis=1).reshape(-1,1)
        return predict
    
    def gra(self):
        predict = self.pre()
        #predict = np.dot(self.X, self.theta)
        mark = np.zeros([self.X.shape[0],len(np.unique(self.Y))])
        mark[np.arange(self.X.shape[0]),self.Y.reshape(-1)] = 1
        result = -1*np.dot(self.X.T, np.multiply(1 - predict,mark))/self.X.shape[0]
        return result
   
    def cal_grad(self):
        '''
        梯度更新可以用矩阵相乘计算
        #grad = np.zeros(self.theta.shape)   #grad n*1      data  10*n   data的转置 n*10   predict 10*1
        #for i in range(len(grad)):
            #grad[i] = np.mean((predict - y)*self.data[:,i])
        #return grad'''
        predict = np.dot(self.X,self.theta)   
        y_hat = self.softmax(predict)
        
        y_onehot = np.array([[1 if int(m[0])==i else 0 for i in range(3)] for m in self.Y]) 
        return -np.dot(self.X.T,((1-y_hat)*y_onehot))/len(self.X)    
    
    
    
    
    def los(self):
        predict = self.pre()
        loss = -1/self.X.shape[0]*np.sum(np.log(predict)[np.arange(self.X.shape[0]),self.Y.reshape(-1)])
        return loss
    
    def  standard_scaler(self):
        mean = np.mean(self.X,axis=0)
        std = np.std(self.X,axis=0)
        self.X = (self.X-mean)/std
    
    @staticmethod    
    def cal(y,predict):
        result = []
        for i in range(3):
            TP = sum([1 for index,lable in enumerate(y) if lable==i and predict[index]==i])
            FN = sum([1 for index,lable in enumerate(y) if lable==i and predict[index]!=i])
            FP = sum([1 for index,lable in enumerate(y) if lable!=i and predict[index]==i])
            TN = sum([1 for index,lable in enumerate(y) if lable!=i and predict[index]!=i])
            a = round((TP+TN)/(TP+FN+FP+TN),4)
            p = round(TP/(TP+FP),4)
            r = round(TP/(TP+FN),4)
            f1 = round(2*p*r/(p+r),4)
            result.append([a,p,r,f1])
        return np.array(result)     
    
    
    def model(self):
        '''
        data m*n+1 包含标签
        theta  n*k
        return m*k
        '''
        predict = np.dot(self.X,self.theta)
        return self.softmax(predict)
    
    def softmax(self,values):
        '''
        values m*k  
        [[1,2,3,4],
         [2,3,4,5],
         [1,2,3,5]]
        
        retrun  y_hat m*k
        '''
        values = np.exp(values)
        sum_h = np.sum(values,axis=1)     # 按行求和 sum_h m*1
        sum_h = sum_h.reshape((-1,1))
        y_hat = values/sum_h
        return y_hat
    
    def cross(self):
        '''
        y_hat,predict 训练数据样本数*类别数  m*k
        theta   特征数*类别           n*k
        x 训练数据  训练样本数量*特征数 m*n
        self.data[:,-1:] m*1
        y_onehot m*k
        return 一个值
        '''
        predict = np.dot(self.X,self.theta)   
        y_hat = self.softmax(predict)
        # 转换成one_hot编码
        #y_onehot = np.array([[1 if int(m[0])==i else 0 for i in range(len(self.k))] for m in self.train_data[:,-1:]]) 
        #return -np.sum(y_onehot*np.log(y_hat))/len(self.train_data) 
        h = [m[int(self.Y[index])] for index,m in enumerate(y_hat)] 
        return -np.sum(np.log(h))/len(self.X)
        
    


    
if __name__ == "__main__":
    np.random.seed(2023)
    import sklearn.datasets as datasets
    data = datasets.load_iris()
    X = data.data
    
    Y = data.target.reshape(-1,1)
    #X = np.array(random.sample(X.tolist(),X.shape[0]))
    
    lr = 0.01
    sf = softmax(X, Y)
    sf.standard_scaler()
    ones = np.ones([sf.X.shape[0],1])
    sf.X = np.hstack([ones,sf.X]) 
    sf.theta = np.random.randn(sf.X.shape[1],len(np.unique(sf.Y)))  
    n = 1
    while n<100000:
        loss = sf.los()

        
        
        print(n,loss)

        gradient = sf.gra()
        #print(gradient)
        #gradient = sf.cal_grad()
        #print(gradient)        

        sf.theta = sf.theta - lr*gradient
        n+=1
        #sf.X = np.array(random.sample(sf.X.tolist(),len(sf.X)))
        #if n%10000==0:
            #predict = np.argmax(sf.pre(),axis=1)
            #print(sf.cal(sf.Y.reshape(-1), predict))
    #print(np.argmax(sf.pre(),axis=1))
    print(sf.theta)
    print(np.sum(np.argmax(sf.pre(),axis=1).reshape(-1,1)==sf.Y)/sf.X.shape[0])
    print('Done')

sklearn掉包实现

这里直接讲解,如何调用sklearn实现。实际上和二分类的代码完全一致。

from sklearn.linear_model import LogisticRegression #sklearn中,线性回归模型在linear_model模块中
# 调取sklearn中自带的数据集
from sklearn.datasets import load_iris #调用上文一开始提到大波士顿房价数据集

X, y = load_boston(return_X_y=True) #获取X,y数据
LR = LogisticRegression() #初始化一个l回归模型
LR.fit(X,y) #fit函数用于训练

LR.predict(X) #predict函数用于预测,直接返回类别值
LR.predict_proba(X) #返回每个类别的概率,
LR.coef_ #coef_用于返回θ的值
LR.intercept_ #intercept_用于返回常数项的值(截距)
LR.score(X, y) #给出预测的准确率。

更多内容可以参考sklearn的官方文档

https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html#sklearn.linear_model.LogisticRegression

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值