softmax的基本概念
-
分類問題
- 一個簡單的圖像分類問題,輸入圖像的高和寬均為2像素,色彩為灰度。
- 將圖像中的4像素分別記為\(x_1, x_2, x_3, x_4\)。
- 假設真實標簽為狗、貓或者雞,這些標簽對應的離散值為\(y_1, y_2, y_3\)。
- 我們通常使用離散的數值來表示類別,例如\(y_1=1, y_2=2, y_3=3\)。
-
權重矢量
softmax回歸的輸出值個數等於標簽里的類別數。
因為一共有4種特征和3種輸出動物類別,所以權重包含12個標量(帶下標的\(w\))、偏差包含3個標量(帶下標的\(b\)),且對每個輸入計算\(o_1\),\(o_2\),\(o_3\),這3個輸出:
- 神經網絡圖
下圖用神經網絡圖描繪了上面的計算。softmax回歸同線性回歸一樣,也是一個單層神經網絡。由於每個輸出\(o_1, o_2, o_3\)的計算都要依賴於所有的輸入\(x_1, x_2, x_3, x_4\),softmax回歸的輸出層也是一個全連接層。
既然分類問題需要得到離散的預測輸出,一個簡單的辦法是將輸出值\(o_i\)當作預測類別是\(i\)的置信度,並將值最大的輸出所對應的類作為預測輸出,即輸出 \(\underset{i}{\arg\max} o_i\)。例如,如果\(o_1,o_2,o_3\)分別為\(0.1,10,0.1\),由於\(o_2\)最大,那么預測類別為2,其代表貓。
- 輸出問題
直接使用輸出層的輸出有兩個問題:- 一方面,由於輸出層的輸出值的范圍不確定,我們難以直觀上判斷這些值的意義。例如,剛才舉的例子中的輸出值10表示“很置信”圖像類別為貓,因為該輸出值是其他兩類的輸出值的100倍。但如果\(o_1=o_3=10^3\),那么輸出值10卻又表示圖像類別為貓的概率很低。
- 另一方面,由於真實標簽是離散值,這些離散值與不確定范圍的輸出值之間的誤差難以衡量。
softmax運算符(softmax operator)解決了以上兩個問題。它通過下式將輸出值變換成值為正且和為1的概率分布:
其中
容易看出\(\hat{y}_1 + \hat{y}_2 + \hat{y}_3 = 1\)且\(0 \leq \hat{y}_1, \hat{y}_2, \hat{y}_3 \leq 1\),因此\(\hat{y}_1, \hat{y}_2, \hat{y}_3\)是一個合法的概率分布。這時候,如果\(\hat{y}_2=0.8\),不管\(\hat{y}_1\)和\(\hat{y}_3\)的值是多少,我們都知道圖像類別為貓的概率是80%。此外,我們注意到
因此softmax運算不改變預測類別輸出。
- 計算效率
- 單樣本矢量計算表達式
為了提高計算效率,我們可以將單樣本分類通過矢量計算來表達。在上面的圖像分類問題中,假設softmax回歸的權重和偏差參數分別為
- 單樣本矢量計算表達式
設高和寬分別為2個像素的圖像樣本\(i\)的特征為
輸出層的輸出為
預測為狗、貓或雞的概率分布為
softmax回歸對樣本\(i\)分類的矢量計算表達式為
- 小批量矢量計算表達式
為了進一步提升計算效率,我們通常對小批量數據做矢量計算。廣義上講,給定一個小批量樣本,其批量大小為\(n\),輸入個數(特征數)為\(d\),輸出個數(類別數)為\(q\)。設批量特征為\(\boldsymbol{X} \in \mathbb{R}^{n \times d}\)。假設softmax回歸的權重和偏差參數分別為\(\boldsymbol{W} \in \mathbb{R}^{d \times q}\)和\(\boldsymbol{b} \in \mathbb{R}^{1 \times q}\)。softmax回歸的矢量計算表達式為
其中的加法運算使用了廣播機制,\(\boldsymbol{O}, \boldsymbol{\hat{Y}} \in \mathbb{R}^{n \times q}\)且這兩個矩陣的第\(i\)行分別為樣本\(i\)的輸出\(\boldsymbol{o}^{(i)}\)和概率分布\(\boldsymbol{\hat{y}}^{(i)}\)。
交叉熵損失函數
對於樣本\(i\),我們構造向量\(\boldsymbol{y}^{(i)}\in \mathbb{R}^{q}\) ,使其第\(y^{(i)}\)(樣本\(i\)類別的離散數值)個元素為1,其余為0。這樣我們的訓練目標可以設為使預測概率分布\(\boldsymbol{\hat y}^{(i)}\)盡可能接近真實的標簽概率分布\(\boldsymbol{y}^{(i)}\)。
- 平方損失估計
然而,想要預測分類結果正確,我們其實並不需要預測概率完全等於標簽概率。例如,在圖像分類的例子里,如果\(y^{(i)}=3\),那么我們只需要\(\hat{y}^{(i)}_3\)比其他兩個預測值\(\hat{y}^{(i)}_1\)和\(\hat{y}^{(i)}_2\)大就行了。即使\(\hat{y}^{(i)}_3\)值為0.6,不管其他兩個預測值為多少,類別預測均正確。而平方損失則過於嚴格,例如\(\hat y^{(i)}_1=\hat y^{(i)}_2=0.2\)比\(\hat y^{(i)}_1=0, \hat y^{(i)}_2=0.4\)的損失要小很多,雖然兩者都有同樣正確的分類預測結果。
改善上述問題的一個方法是使用更適合衡量兩個概率分布差異的測量函數。其中,交叉熵(cross entropy)是一個常用的衡量方法:
其中帶下標的\(y_j^{(i)}\)是向量\(\boldsymbol y^{(i)}\)中非0即1的元素,需要注意將它與樣本\(i\)類別的離散數值,即不帶下標的\(y^{(i)}\)區分。在上式中,我們知道向量\(\boldsymbol y^{(i)}\)中只有第\(y^{(i)}\)個元素\(y^{(i)}{y^{(i)}}\)為1,其余全為0,於是\(H(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}) = -\log \hat y_{y^{(i)}}^{(i)}\)。也就是說,交叉熵只關心對正確類別的預測概率,因為只要其值足夠大,就可以確保分類結果正確。當然,遇到一個樣本有多個標簽時,例如圖像里含有不止一個物體時,我們並不能做這一步簡化。但即便對於這種情況,交叉熵同樣只關心對圖像中出現的物體類別的預測概率。
假設訓練數據集的樣本數為\(n\),交叉熵損失函數定義為
其中\(\boldsymbol{\Theta}\)代表模型參數。同樣地,如果每個樣本只有一個標簽,那么交叉熵損失可以簡寫成
從另一個角度來看,我們知道最小化\(\ell(\boldsymbol{\Theta})\)等價於最大化\(\exp(-n\ell(\boldsymbol{\Theta}))=\prod_{i=1}^n \hat y_{y^{(i)}}^{(i)}\),即最小化交叉熵損失函數等價於最大化訓練數據集所有標簽類別的聯合預測概率。
模型訓練和預測
在訓練好softmax回歸模型后,給定任一樣本特征,就可以預測每個輸出類別的概率。通常,我們把預測概率最大的類別作為輸出類別。如果它與真實類別(標簽)一致,說明這次預測是正確的。在實驗中,將使用准確率(accuracy)來評價模型的表現。它等於正確預測數量與總預測數量之比。
獲取Fashion-MNIST訓練集和讀取數據
在介紹softmax回歸的實現前先引入一個多類圖像分類數據集。它將在后面的章節中被多次使用,以方便我們觀察比較算法之間在模型精度和計算效率上的區別。圖像分類數據集中最常用的是手寫數字識別數據集MNIST。但大部分模型在MNIST上的分類精度都超過了95%。為了更直觀地觀察算法之間的差異,我們將使用一個圖像內容更加復雜的數據集Fashion-MNIST。
我這里我們會使用torchvision包,它是服務於PyTorch深度學習框架的,主要用來構建計算機視覺模型。torchvision主要由以下幾部分構成:
- torchvision.datasets: 一些加載數據的函數及常用的數據集接口;
- torchvision.models: 包含常用的模型結構(含預訓練模型),例如AlexNet、VGG、ResNet等;
- torchvision.transforms: 常用的圖片變換,例如裁剪、旋轉等;
- torchvision.utils: 其他的一些有用的方法。
# import needed package
%matplotlib inline
from IPython import display
import matplotlib.pyplot as plt
import torch
import torchvision
import torchvision.transforms as transforms
import time
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
get dataset
mnist_train = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=False, download=True, transform=transforms.ToTensor())
- 說明:class torchvision.datasets.FashionMNIST(root, train=True, transform=None, target_transform=None, download=False)
- root(string)– 數據集的根目錄,其中存放processed/training.pt和processed/test.pt文件。
- train(bool, 可選)– 如果設置為True,從training.pt創建數據集,否則從test.pt創建。
- download(bool, 可選)– 如果設置為True,從互聯網下載數據並放到root文件夾下。如果root目錄下已經存在數據,不會再次下載。
- transform(可被調用 , 可選)– 一種函數或變換,輸入PIL圖片,返回變換之后的數據。如:transforms.RandomCrop。
- target_transform(可被調用 , 可選)– 一種函數或變換,輸入目標,進行變換。
# show result
print(type(mnist_train))
print(len(mnist_train), len(mnist_test))
# 我們可以通過下標來訪問任意一個樣本
feature, label = mnist_train[0]
print(feature.shape, label) # Channel x Height x Width
如果不做變換輸入的數據是圖像,我們可以看一下圖片的類型參數:
mnist_PIL = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=True, download=True)
PIL_feature, label = mnist_PIL[0]
print(PIL_feature)
# 本函數已保存在d2lzh包中方便以后使用
def get_fashion_mnist_labels(labels):
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
def show_fashion_mnist(images, labels):
d2l.use_svg_display()
# 這里的_表示我們忽略(不使用)的變量
_, figs = plt.subplots(1, len(images), figsize=(12, 12))
for f, img, lbl in zip(figs, images, labels):
f.imshow(img.view((28, 28)).numpy())
f.set_title(lbl)
f.axes.get_xaxis().set_visible(False)
f.axes.get_yaxis().set_visible(False)
plt.show()
X, y = [], []
for i in range(10):
X.append(mnist_train[i][0]) # 將第i個feature加到X中
y.append(mnist_train[i][1]) # 將第i個label加到y中
show_fashion_mnist(X, get_fashion_mnist_labels(y))
# 讀取數據
batch_size = 256
num_workers = 4
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
softmax從零開始的實現
import torch
import torchvision
import numpy as np
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
獲取訓練集數據和測試集數據
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='/home/kesci/input/FashionMNIST2065')
模型參數初始化
num_inputs = 784 # 28*28
num_outputs = 10
W = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_outputs)), dtype=torch.float)
b = torch.zeros(num_outputs, dtype=torch.float)
W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
對多維Tensor按維度操作
X = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(X.sum(dim=0, keepdim=True)) # dim為0,按照相同的列求和,並在結果中保留列特征
print(X.sum(dim=1, keepdim=True)) # dim為1,按照相同的行求和,並在結果中保留行特征
print(X.sum(dim=0, keepdim=False)) # dim為0,按照相同的列求和,不在結果中保留列特征
print(X.sum(dim=1, keepdim=False)) # dim為1,按照相同的行求和,不在結果中保留行特征
定義softmax操作
def softmax(X):
X_exp = X.exp()
partition = X_exp.sum(dim=1, keepdim=True)
# print("X size is ", X_exp.size())
# print("partition size is ", partition, partition.size())
return X_exp / partition # 這里應用了廣播機制
softmax回歸模型
def net(X):
return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)
定義損失函數
a = torch.Tensor([[1,2],[3,4]])
b = orch.gather(a,1,torch.LongTensor([[0,0],[1,0]]))
torch.gather(input, dim, index, out=None)中的dim表示的就是第幾維度,在這個二維例子中,如果dim=0,那么它表示的就是你接下來的操作是對於第一維度進行的,也就是行;如果dim=1,那么它表示的就是你接下來的操作是對於第二維度進行的,也就是列。
上面例子中,[0,0]就是第一行對應元素的下標,也就是對應的是[1,1]; [1,0]就是第二行對應元素的下標,也就是對應的是[4,3]。
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = torch.LongTensor([0, 2])
y_hat.gather(1, y.view(-1, 1))
def cross_entropy(y_hat, y):
return - torch.log(y_hat.gather(1, y.view(-1, 1)))
定義准確率
我們模型訓練完了進行模型預測的時候,會用到我們這里定義的准確率。
def accuracy(y_hat, y):
return (y_hat.argmax(dim=1) == y).float().mean().item()
# 本函數已保存在d2lzh_pytorch包中方便以后使用。該函數將被逐步改進:它的完整實現將在“圖像增廣”一節中描述
def evaluate_accuracy(data_iter, net):
acc_sum, n = 0.0, 0
for X, y in data_iter:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
訓練模型
num_epochs, lr = 5, 0.1
# 本函數已保存在d2lzh_pytorch包中方便以后使用
def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
params=None, lr=None, optimizer=None):
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y).sum()
# 梯度清零
if optimizer is not None:
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()
l.backward()
if optimizer is None:
d2l.sgd(params, lr, batch_size)
else:
optimizer.step()
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
n += y.shape[0]
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)
模型預測
現在我們的模型訓練完了,可以進行一下預測,我們的這個模型訓練的到底准確不准確。
現在就可以演示如何對圖像進行分類了。給定一系列圖像(第三行圖像輸出),我們比較一下它們的真實標簽(第一行文本輸出)和模型預測結果(第二行文本輸出)。
X, y = iter(test_iter).next()
true_labels = d2l.get_fashion_mnist_labels(y.numpy())
pred_labels = d2l.get_fashion_mnist_labels(net(X).argmax(dim=1).numpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]
d2l.show_fashion_mnist(X[0:9], titles[0:9])
softmax的簡潔實現
# 加載各種包或者模塊
import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
初始化參數和獲取數據
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='/home/kesci/input/FashionMNIST2065')
定義網絡模型
num_inputs = 784
num_outputs = 10
class LinearNet(nn.Module):
def __init__(self, num_inputs, num_outputs):
super(LinearNet, self).__init__()
self.linear = nn.Linear(num_inputs, num_outputs)
def forward(self, x): # x 的形狀: (batch, 1, 28, 28)
y = self.linear(x.view(x.shape[0], -1))
return y
# net = LinearNet(num_inputs, num_outputs)
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x): # x 的形狀: (batch, *, *, ...)
return x.view(x.shape[0], -1)
from collections import OrderedDict
net = nn.Sequential(
# FlattenLayer(),
# LinearNet(num_inputs, num_outputs)
OrderedDict([
('flatten', FlattenLayer()),
('linear', nn.Linear(num_inputs, num_outputs))]) # 或者寫成我們自己定義的 LinearNet(num_inputs, num_outputs) 也可以
)
初始化模型參數
init.normal_(net.linear.weight, mean=0, std=0.01)
init.constant_(net.linear.bias, val=0)
定義損失函數
loss = nn.CrossEntropyLoss() # 下面是他的函數原型
# class torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')
定義優化函數
optimizer = torch.optim.SGD(net.parameters(), lr=0.1) # 下面是函數原型
# class torch.optim.SGD(params, lr=, momentum=0, dampening=0, weight_decay=0, nesterov=False)
訓練
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)