numpy手寫softmax迴歸

softmax誕生原因

線性迴歸主要用於連續值預測,即迴歸問題,比如判定一個東西是雞的概率是多少

而當模型需要預測多個離散值時,即分類問題,比如判定一個東西是雞還是鵝還是鴨

此時就需要多個輸出單元,並且修改運算方式從而方便預測和訓練,這也就是softmax層誕生的原因

這裏使用的是minist手寫數字識別數據集

1.首先通過Pytorch讀取數據集

def load_data():
    train_dataset = datasets.MNIST(root='./data/',
                                   train=True,
                                    transform=transforms.ToTensor(),
                                   download=True)

    test_dataset = datasets.MNIST(root='./data/',
                                  transform=transforms.ToTensor(),
                                  train=False)

    # Data Loader (Input Pipeline)
    train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
                                               batch_size=batch_size,
                                               shuffle=True)

    test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
                                              batch_size=batch_size,
                                              shuffle=False)
    return train_loader,test_loader

2.softmax迴歸模型

這裏每個輸入都是282828*28的數據,即7841784*1

這裏我選擇每次都輸入batch_size爲64的數據進行優化

即輸入數據shape爲(64,784)

即64*784的矩陣
在這裏插入圖片描述

softmax和線性迴歸一樣都是對將輸入特徵用線性函數計算輸出值,但是這裏有一點不同,就是softmax迴歸的輸出值個數不再是一個,而是等於類別數,這裏輸出類別爲0123456789十個數字,因此有十個輸出值

因此此時需要十個線性函數來對應這十個輸入值到輸出值的轉化
y1=W1,1x1+W2,1x2+...+W783,1x783,1+W784,1x784,1+b1y2=W1,2x1+W2,2x2+...+W783,2x783,2+W784,2x784,2+b2......y10=W1,10x1+W2,10x2+...+W783,10x783,10+W784,10x784,10+b10 y_1=W_{1,1}*x_1+W_{2,1}*x_2+...+W_{783,1}*x_{783,1}+W_{784,1}*x_{784,1}+b_{1}\\ y_2=W_{1,2}*x_1+W_{2,2}*x_2+...+W_{783,2}*x_{783,2}+W_{784,2}*x_{784,2}+b_{2}\\ ...\\ ...\\ y_{10}=W_{1,10}*x_1+W_{2,10}*x_2+...+W_{783,10}*x_{783,10}+W_{784,10}*x_{784,10}+b_{10}
其中w矩陣爲(784,10),即
在這裏插入圖片描述
b矩陣爲(64,10)


在這裏插入圖片描述
最終爲
在這裏插入圖片描述
y矩陣最終等於x(64,784)w(784,10)+b(64,10)x(64,784)*w(784,10)+b(64,10)

其中x(64,784)w(784,10)x(64,784)*w(784,10)結果爲(64,10)(64,10),加上b最後還是一個(64,10)(64,10)的矩陣

對應一個批次64個數據中中,預測十個數字每個數字的可能性大小

代碼如下

def train():
    w=np.random.randn(784,10) #生成隨機正太分佈的w矩陣
    b=np.zeros((64,10)) #生成全是0的b矩陣
    for batch_idx, (data, target) in enumerate(train_loader):
        data= np.squeeze(data.numpy()).reshape(64,28*28) 
        # 把張量中維度爲1的維度去掉,並且改變維度爲(64,784)
        target = target.numpy() #x矩陣 (64,784)
        y_hat=np.dot(data,w)+b
        print(y_hat.shape)

最終y_hat爲(64,10)(64,10)

image

3.softmax運算

假設預測值分別是1,10,…100,此時直接通過輸出值判斷,y10y_{10}最大識別結果爲數字9

但是這樣會使得,不知道怎麼劃分這些值到底是很大還是很小,也就是難以去優化參數

比如如果這次裏100最大,但是下次中10000最大而且最小的都有1000,那麼100到底算不算大

這裏就用softmax運算來解決
y1^,y2^...y10^=softmax(output1,output2...output10)yj^=exp(outputj)i=110exp(outputi) \hat{y_1},\hat{y_2}...\hat{y_{10}}=softmax(output_1,output_2...output_{10})\\ 其中\\ \hat{y_j}=\frac{exp(output_j)}{\sum_{i=1}^{10}{exp(output_i)}}
即每個yi^\hat{y_i}爲exp(當前輸出值)/所有exp(輸出值)求和

這樣一來,最終所有yi^\hat{y_i}的和爲1,而且每個yi^\hat{y_i}對應的都是對每個手寫數字概率的預測

def softmax(label):
    #label時一個(64,10)的numpy數組
    label = np.exp(label.T)#先把每個元素都進行exp運算
    sum = label.sum(axis=0) #對於每一行進行求和操作
    # print((label/sum).T.shape)
    # print((label/sum).T)
    return (label/sum).T #通過廣播機制,使每行分別除以各種的和

4.交叉熵損失函數

這裏的損失函數我們可以直接採用平方損失函數,即yi^y)2(\hat{y_i}-y)^2

但是想要預測結果正確,我們不需要使得預測值和實際值完全一致

只需要這個yi^\hat{y_i}的預測值比其他的都大即可,而平方損失則過於嚴格

我們可以採用交叉熵作爲損失函數

對於單個的訓練樣本而言,交叉熵爲
i=1ilog(i)i=110yilog(y^i) -\sum_{i=1}^{輸出單元數}第i個實際值*log(第i個預測值)\\ 即\\ -\sum_{i=1}^{10}yilog(\hat{y}_i)
此時對於一個訓練批次而言,損失函數爲
Loss=1batchsizej=1batchsizei=1yilog(y^i) Loss=-\frac{1}{batchsize}\sum_{j=1}^{batchsize}{\sum_{i=1}^{輸出單元數}yilog(\hat{y}_i)}
這裏就是
Loss=164j=164i=110yilog(y^i) Loss=-\frac{1}{64}\sum_{j=1}^{64}{\sum_{i=1}^{10}yilog(\hat{y}_i)}
然而實際上y1,y2...y10y_1,y_2...y_{10}這10個實際值只會有一個是1,其他全是0,也就是說,式子變成了
Loss=164j=164log(y^i) Loss=-\frac{1}{64}\sum_{j=1}^{64}{log(\hat{y}_i)}
然後再結合模型

outputi=W1,ix1+W2,ix2+...+W783,ix783,i+W784,ix784,i+bioutput_i=W_{1,i}*x_1+W_{2,i}*x_2+...+W_{783,i}*x_{783,i}+W_{784,i}*x_{784,i}+b_{i}

yj^=exp(outputj)i=110exp(outputi)\hat{y_j}=\frac{exp(output_j)}{\sum_{i=1}^{10}{exp(output_i)}}

最終損失函數爲

Loss=164j=164log(exp(outputj)i=110exp(outputi))Loss=-\frac{1}{64}\sum_{j=1}^{64}{log(\frac{exp(output_j)}{\sum_{i=1}^{10}{exp(output_i)}})}

5.梯度下降優化參數

此時需要用梯度下降優化參數
Wi=Wiηbatchsizei=1batchsizedLossdwibi=biηbatchsizei=1batchsizedLossdbi W_i=W_i-\frac{\eta}{batchsize}\sum_{i=1}^{batchsize}{\frac{d_{Loss}}{dw_i}}\\ b_i=b_i-\frac{\eta}{batchsize}\sum_{i=1}^{batchsize}{\frac{d_{Loss}}{db_i}}

其中η\eta爲學習速率,batchsize是批量大小,dLossdwi\frac{d_{Loss}}{dw_i}wiw_i對損失函數的偏導

假設此時y^\hat{y}的對應的類別是預測對的

即按照鏈式求導法則
KaTeX parse error: Got function '\hat' with no arguments as subscript at position 45: …ac{d_{Loss}}{d_\̲h̲a̲t̲{y}} \frac{d_\h…

因此,更新函數變成了

Wi=Wiηbatchsizei=1batchsize(y^1)xiW_i=W_i-\frac{\eta}{batchsize}\sum_{i=1}^{batchsize}{(\hat{y}-1)*x_i}

這裏就是

Wi=Wiη64i=164(y^1)xiW_i=W_i-\frac{\eta}{64}\sum_{i=1}^{64}{(\hat{y}-1)*x_i}

同理

bi=biη64i=164(y^1)b_i=b_i-\frac{\eta}{64}\sum_{i=1}^{64}{(\hat{y}-1)}

同理,假設此時y^\hat{y}的對應的類別是預測錯誤的

dLossdwi=y^xi\frac{d_{Loss}}{dw_i}=\hat{y}*x_i

Wi=Wiη64i=164y^xiW_i=W_i-\frac{\eta}{64}\sum_{i=1}^{64}{\hat{y}*x_i}

bi=biη64i=164y^b_i=b_i-\frac{\eta}{64}\sum_{i=1}^{64}{\hat{y}}

(推算過程我就省略了,Latex打上去太累了)

PS:我吐了,上面這個求導我一開始沒用鏈式求導,直接手算,outputioutput_i沒經過softmax變換,直接當成了y^\hat{y}來算,最後結果完全不符合邏輯,半夜看了好多博客終於想起來自己的問題了,好不容易重新推了出來

因此,最終W和b的優化函數如下:

Wi=Wiη64i=164(y^y)xiW_i=W_i-\frac{\eta}{64}\sum_{i=1}^{64}{(\hat{y}-y)*x_i}

bi=biη64i=164(y^y)b_i=b_i-\frac{\eta}{64}\sum_{i=1}^{64}{(\hat{y}-y)}

最終代碼如下:

w_,b_=CrossEntropyLoss(data,target,y_hat)
w=w+w_  #優化參數w的矩陣
b=b+b_ #優化參數b的矩陣
def CrossEntropyLoss(data,target,y_hat):
    target = np.eye(10)[target]  # 改爲one-hot形式
    data=data.T #(784,64)
    #y_hat-target爲預測值與實際值的one-hot挨個做差,得出一個(64,10)的y_hat-y的矩陣
    w_=-np.dot(data,y_hat-target)/batch_size*learning_rate
    b_=-(y_hat-target)/batch_size*learning_rate

    # print(w_)
    return w_,b_

這短短12行代碼我查公式整理原理,搞了一個晚上。。。

最後優化時採用了矩陣相乘的方式代替累加

Wi=Wiη64i=164(y^y)xiW_i=W_i-\frac{\eta}{64}\sum_{i=1}^{64}{(\hat{y}-y)*x_i}中的i=164(y^y)xi\sum_{i=1}^{64}{(\hat{y}-y)*x_i}

直接用矩陣相乘的方法代替
在這裏插入圖片描述
這裏相乘時,爲該矩陣的每行元素
在這裏插入圖片描述
與另一矩陣的每列的元素,挨個相加並且求和

如:x1,1(y^1,1y1,1)+x1,2(y^1,2y1,2)+...x1,63(y^1,63y1,63)+x1,64(y^1,64y1,64)x_{1,1}*(\hat{y}_{1,1}-y_{1,1})+x_{1,2}(\hat{y}_{1,2}-y_{1,2})+...x_{1,63}(\hat{y}_{1,63}-y_{1,63})+x_{1,64}(\hat{y}_{1,64}-y_{1,64})

也就一一對應着i=164(y^y)xi\sum_{i=1}^{64}{(\hat{y}-y)*x_i}

計算準確率

def Accuracy(target,y_hat):
    #y_hat.argmax(axis=1)==target 用於比較y_hat與target的每個元素,返回一個布爾數組
    acc=y_hat.argmax(axis=1) == target
    acc=acc+0  #將布爾數組轉爲0,1數組
    return acc.mean() #通過求均值算出準確率

最終整個項目完整代碼如下

from torchvision import datasets, transforms
import torch.utils.data as Data
import numpy as np
from tqdm._tqdm import trange
batch_size = 64
learning_rate=0.0001

def load_data():


    # 加載torchvision包內內置的MNIST數據集 這裏涉及到transform:將圖片轉化成torchtensor
    train_dataset = datasets.MNIST(root='./data/', train=True, transform=transforms.ToTensor(), download=True)
    test_dataset = datasets.MNIST(root='./data/', train=False, transform=transforms.ToTensor())

    # 加載小批次數據,即將MNIST數據集中的data分成每組batch_size的小塊,shuffle指定是否隨機讀取
    train_loader = Data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
    test_loader = Data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)

    return train_loader,test_loader

def train(train_loader):
    list=[]
    w=np.random.normal(scale=0.01,size=(784,10)) #生成隨機正太分佈的w矩陣
    b=np.zeros((batch_size,10)) #生成全是0的b矩陣
    for i in trange(0,100):
        for batch_idx, (data, target) in enumerate(train_loader):

            if(data.shape[0]<batch_size):
                break
            data= np.squeeze(data.numpy()).reshape(batch_size,784) # 把張量中維度爲1的維度去掉,並且改變維度爲(64,784)

            target = target.numpy() #x矩陣 (64,784)

            y_hat=softmax(np.dot(data,w)+b)#預測結果y_hat
            # print(sum(y_hat[0]))
            w_=GradientOfCrossEntropyLoss(data,target,y_hat,type="weight")#計算交叉熵損失函數對於w的梯度
            b_=GradientOfCrossEntropyLoss(data,target,y_hat,type="bias")#計算交叉熵損失函數對於b的梯度
            w=GradientDescent(w,w_)  #梯度下降優化參數w
            b=GradientDescent(b,b_)  #梯度下降優化參數b
            list.append(Accuracy(target, y_hat))
            if (batch_idx == 50):
                print(Accuracy(target, y_hat))

    return list

def softmax(label):

    label = np.exp(label.T)#先把每個元素都進行exp運算
    # print(label)
    sum = label.sum(axis=0) #對於每一行進行求和操作

    #print((label/sum).T.sum(axis=1))
    return (label/sum).T #通過廣播機制,使每行分別除以各種的和

#梯度下降
def GradientDescent(Param,GradientOfLoss):
    Param = Param -GradientOfLoss/ batch_size * learning_rate
    # print(w_)
    return Param

#計算交叉熵損失函數對於w或者b的梯度
def GradientOfCrossEntropyLoss(data,y,y_hat,type="weight"):
    y = np.eye(10)[y]  # 改爲one-hot形式
    data=data.T #(784,64)
    #y_hat-target爲預測值與實際值的one-hot挨個做差,得出一個(64,10)的y_hat-y的矩陣

    if (type == "bias"):
        return  y_hat-y
    else:
        return np.dot(data,y_hat-y)



def Accuracy(target,y_hat):
    #y_hat.argmax(axis=1)==target 用於比較y_hat與target的每個元素,返回一個布爾數組
    acc=y_hat.argmax(axis=1) == target
    acc=acc+0  #將布爾數組轉爲0,1數組
    return acc.mean() #通過求均值算出準確率

if __name__ == '__main__':

    train_loader,test_loader=load_data()    #加載數據(這裏使用pytorch加載數據,後面用numpy手寫)
    list=train(train_loader)
    import matplotlib.pyplot as plt

    plt.plot(list)
    plt.show()

結果如下

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章