動手學深度學習(二) Softmax與分類模型
- 2020 年 2 月 24 日
- 筆記
softmax和分類模型
內容包含:
- softmax回歸的基本概念
- 如何獲取Fashion-MNIST數據集和讀取數據
- softmax回歸模型的從零開始實現,實現一個對Fashion-MNIST訓練集中的影像數據進行分類的模型
- 使用pytorch重新實現softmax回歸模型
softmax的基本概念
- 分類問題 一個簡單的影像分類問題,輸入影像的高和寬均為2像素,色彩為灰度。 影像中的4像素分別記為
。 假設真實標籤為狗、貓或者雞,這些標籤對應的離散值為
。 我們通常使用離散的數值來表示類別,例如
。
- 權重矢量
- 神經網路圖 下圖用神經網路圖描繪了上面的計算。softmax回歸同線性回歸一樣,也是一個單層神經網路。由於每個輸出
的計算都要依賴於所有的輸入
,softmax回歸的輸出層也是一個全連接層。

Image Name
既然分類問題需要得到離散的預測輸出,一個簡單的辦法是將輸出值
當作預測類別是
的置信度,並將值最大的輸出所對應的類作為預測輸出,即輸出
。例如,如果
分別為
,由於
最大,那麼預測類別為2,其代表貓。
- 輸出問題 直接使用輸出層的輸出有兩個問題:
- 一方面,由於輸出層的輸出值的範圍不確定,我們難以直觀上判斷這些值的意義。例如,剛才舉的例子中的輸出值10表示「很置信」影像類別為貓,因為該輸出值是其他兩類的輸出值的100倍。但如果
,那麼輸出值10卻又表示影像類別為貓的概率很低。
- 另一方面,由於真實標籤是離散值,這些離散值與不確定範圍的輸出值之間的誤差難以衡量。
softmax運算符(softmax operator)解決了以上兩個問題。它通過下式將輸出值變換成值為正且和為1的概率分布:
其中
容易看出
且
,因此
是一個合法的概率分布。這時候,如果
,不管
和
的值是多少,我們都知道影像類別為貓的概率是80%。此外,我們注意到
因此softmax運算不改變預測類別輸出。
- 計算效率
- 單樣本矢量計算表達式 為了提高計算效率,我們可以將單樣本分類通過矢量計算來表達。在上面的影像分類問題中,假設softmax回歸的權重和偏差參數分別為
設高和寬分別為2個像素的影像樣本
的特徵為
輸出層的輸出為
預測為狗、貓或雞的概率分布為
softmax回歸對樣本
分類的矢量計算表達式為
- 小批量矢量計算表達式 為了進一步提升計算效率,我們通常對小批量數據做矢量計算。廣義上講,給定一個小批量樣本,其批量大小為
,輸入個數(特徵數)為
,輸出個數(類別數)為
。設批量特徵為
。假設softmax回歸的權重和偏差參數分別為
和
。softmax回歸的矢量計算表達式為
其中的加法運算使用了廣播機制,
且這兩個矩陣的第
行分別為樣本
的輸出
和概率分布
。
交叉熵損失函數
對於樣本
,我們構造向量
,使其第
(樣本
類別的離散數值)個元素為1,其餘為0。這樣我們的訓練目標可以設為使預測概率分布
儘可能接近真實的標籤概率分布
。
- 平方損失估計
然而,想要預測分類結果正確,我們其實並不需要預測概率完全等於標籤概率。例如,在影像分類的例子里,如果
,那麼我們只需要
比其他兩個預測值
和
大就行了。即使
值為0.6,不管其他兩個預測值為多少,類別預測均正確。而平方損失則過於嚴格,例如
比
的損失要小很多,雖然兩者都有同樣正確的分類預測結果。
改善上述問題的一個方法是使用更適合衡量兩個概率分布差異的測量函數。其中,交叉熵(cross entropy)是一個常用的衡量方法:
其中帶下標的
是向量
中非0即1的元素,需要注意將它與樣本
類別的離散數值,即不帶下標的
區分。在上式中,我們知道向量
中只有第
個元素
為1,其餘全為0,於是
。也就是說,交叉熵只關心對正確類別的預測概率,因為只要其值足夠大,就可以確保分類結果正確。當然,遇到一個樣本有多個標籤時,例如影像里含有不止一個物體時,我們並不能做這一步簡化。但即便對於這種情況,交叉熵同樣只關心對影像中出現的物體類別的預測概率。
假設訓練數據集的樣本數為
,交叉熵損失函數定義為
其中
代表模型參數。同樣地,如果每個樣本只有一個標籤,那麼交叉熵損失可以簡寫成
。從另一個角度來看,我們知道最小化
等價於最大化
,即最小化交叉熵損失函數等價於最大化訓練數據集所有標籤類別的聯合預測概率。
模型訓練和預測
在訓練好softmax回歸模型後,給定任一樣本特徵,就可以預測每個輸出類別的概率。通常,我們把預測概率最大的類別作為輸出類別。如果它與真實類別(標籤)一致,說明這次預測是正確的。在3.6節的實驗中,我們將使用準確率(accuracy)來評價模型的表現。它等於正確預測數量與總預測數量之比。
獲取Fashion-MNIST訓練集和讀取數據
在介紹softmax回歸的實現前我們先引入一個多類影像分類數據集。它將在後面的章節中被多次使用,以方便我們觀察比較演算法之間在模型精度和計算效率上的區別。影像分類數據集中最常用的是手寫數字識別數據集MNIST[1]。但大部分模型在MNIST上的分類精度都超過了95%。為了更直觀地觀察演算法之間的差異,我們將使用一個影像內容更加複雜的數據集Fashion-MNIST[2]。
我這裡我們會使用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 print(torch.__version__) print(torchvision.__version__)
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))
<class 'torchvision.datasets.mnist.FashionMNIST'> 60000 10000
# 我們可以通過下標來訪問任意一個樣本 feature, label = mnist_train[0] print(feature.shape, label) # Channel x Height x Width
torch.Size([1, 28, 28]) 9
如果不做變換輸入的數據是影像,我們可以看一下圖片的類型參數:
mnist_PIL = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=True, download=True) PIL_feature, label = mnist_PIL[0] print(PIL_feature)
<PIL.Image.Image image mode=L size=28x28 at 0x7F54A41612E8>
# 本函數已保存在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))
<img src="https://cdn.kesci.com/rt_upload/056F457B00454FFD81A3CB6AD966C508/q5j7ehijw7.svg">
# 讀取數據 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)
start = time.time() for X, y in train_iter: continue print('%.2f sec' % (time.time() - start))
4.95 sec
softmax從零開始的實現
import torch import torchvision import numpy as np import sys sys.path.append("/home/kesci/input") import d2lzh1981 as d2l print(torch.__version__) print(torchvision.__version__)
1.3.0 0.4.1a0+d94043a
獲取訓練集數據和測試集數據
batch_size = 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='/home/kesci/input/FashionMNIST2065')
模型參數初始化
num_inputs = 784 print(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)
784
W.requires_grad_(requires_grad=True) b.requires_grad_(requires_grad=True)
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], 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,按照相同的行求和,不在結果中保留行特徵
tensor([[5, 7, 9]]) tensor([[ 6], [15]]) tensor([5, 7, 9]) tensor([ 6, 15])
定義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 # 這裡應用了廣播機制
X = torch.rand((2, 5)) X_prob = softmax(X) print(X_prob, 'n', X_prob.sum(dim=1))
tensor([[0.2253, 0.1823, 0.1943, 0.2275, 0.1706], [0.1588, 0.2409, 0.2310, 0.1670, 0.2024]]) tensor([1.0000, 1.0000])
softmax回歸模型
def net(X): return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)
定義損失函數
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))
tensor([[0.1000], [0.5000]])
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()
print(accuracy(y_hat, y))
0.5
# 本函數已保存在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
print(evaluate_accuracy(test_iter, net))
0.1445
訓練模型
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)
epoch 1, loss 0.7851, train acc 0.750, test acc 0.791 epoch 2, loss 0.5704, train acc 0.814, test acc 0.810 epoch 3, loss 0.5258, train acc 0.825, test acc 0.819 epoch 4, loss 0.5014, train acc 0.832, test acc 0.824 epoch 5, loss 0.4865, train acc 0.836, test acc 0.827
模型預測
現在我們的模型訓練完了,可以進行一下預測,我們的這個模型訓練的到底準確不準確。 現在就可以演示如何對影像進行分類了。給定一系列影像(第三行影像輸出),我們比較一下它們的真實標籤(第一行文本輸出)和模型預測結果(第二行文本輸出)。
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])
<img src="https://cdn.kesci.com/rt_upload/1DA8927186304BEBA2B3DCC4A9E027DD/q5j7fq2jer.svg">
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 print(torch.__version__)
1.3.0
初始化參數和獲取數據
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)
Parameter containing: tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], requires_grad=True)
定義損失函數
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)
epoch 1, loss 0.0031, train acc 0.751, test acc 0.795 epoch 2, loss 0.0022, train acc 0.813, test acc 0.809 epoch 3, loss 0.0021, train acc 0.825, test acc 0.806 epoch 4, loss 0.0020, train acc 0.833, test acc 0.813 epoch 5, loss 0.0019, train acc 0.837, test acc 0.822