Softmax 回歸
softmax 回歸是機器學習另外一個非常經典且重要的模型,是一個分類問題。
下面先解釋一下分類和回歸的區別:
簡單來說,分類問題從回歸的單輸出變成了多輸出,輸出的個數等于類別的個數。
實際上,對于分類來說,我們不關心它們之間實際的值,我們關心的是:模型是否對正確類別的置信度特別的大
雖然上述沒有要求 O i O_i Oi? 是一個什么樣的值,但是如果我們將值放在合適的區間,也會讓后續的處理變得更加的簡單,比如下面我們希望模型的輸出是一個概率:
上述要是你使用了 o n e ? h o t one-hot one?hot 編碼的話,只有當 i = y i=y i=y時, y i = 1 y_i = 1 yi?=1,否則就是0。
損失函數
損失函數是用來衡量預測值與真實值之間的區別,是機器學習里面一個非常重要的概念。
1. L2 Loss(均方損失)
藍色的線表示 y = 0 y=0 y=0 時變換我的 預測值 y ′ y' y′ 所生成的函數,可以看出來是一個二次函數。綠色是一個似然函數,似然函數取得最大值表明取該參數模型最合理。橙色的表示的是損失函數的梯度,由于是一次函數,穿過原點。
由上述可以發現,當預測值與真實值距離比較遠的時候,梯度比較的大,則對參數的更新是比較的多的,當越靠近原點的時候,梯度的絕對值就會越小,對參數的更新就會越來越小。但這可能并不是一件好事,因為在離原點越遠的地方,我可能并不希望需要那么大的梯度來更新我的參數。因此也可以考慮下面的 L1 Loss
L1 Loss
當然也是可以提出新的損失函數來結合上述兩種損失函數的好處。
上述損失函數定義的好處就是:當預測值與真實值差別比較大的時候,我可以以均勻的力度
往回拉。當兩者越來越接近時,我可以使得拉的力度越來越小,從而不會出現數值上的問題。
圖片分類數據集
下面使用 Fashion-MNIST 數據集,展示對數據集的一般操作:
首先導入所需的庫:
%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2ld2l.use_svg_display()
# 使用svg來顯示圖片
接著我們可以通過框架中的內置函數將Fashion-MNIST數據集下載并讀取到內存中。
# 通過ToTensor實例將圖像數據從PIL類型變換成32位浮點數格式,
# 并除以255使得所有像素的數值均在0~1之間
trans = transforms.ToTensor() # 預處理,將圖片轉換成tensor
mnist_train = torchvision.datasets.FashionMNIST(root="../data", train=True, transform=trans, download=True)
# transform=trans希望得到的是一個tensor而不是一張圖片
mnist_test = torchvision.datasets.FashionMNIST(root="../data", train=False, transform=trans, download=True)
Fashion-MNIST由10個類別的圖像組成,每個類別由訓練數據集(train dataset)中的6000張圖像和測試數據集(test dataset)中的1000張圖像組成。因此,訓練集和測試集分別包含60000和10000張圖像。測試數據集不會用于訓練,只用于評估模型性能。
每個輸入圖像的高度和寬度均為28像素。
數據集由灰度圖像組成,其通道數為1。
為了簡潔起見,將高度 h h h像素、寬度 w w w像素圖像的形狀記為 h × w h \times w h×w或( h h h, w w w)。
接著定義兩個可視化數據集的函數
Fashion-MNIST中包含的10個類別,分別為t-shirt(T恤)、trouser(褲子)、pullover(套衫)、dress(連衣裙)、coat(外套)、sandal(涼鞋)、shirt(襯衫)、sneaker(運動鞋)、bag(包)和ankle boot(短靴)。
以下函數用于在數字標簽索引及其文本名稱之間進行轉換。
def get_fashion_mnist_labels(labels): #@save"""返回Fashion-MNIST數據集的文本標簽"""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_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save"""繪制圖像列表"""figsize = (num_cols * scale, num_rows * scale)_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)axes = axes.flatten()for i, (ax, img) in enumerate(zip(axes, imgs)):if torch.is_tensor(img):# 圖片張量ax.imshow(img.numpy())else:# PIL圖片ax.imshow(img)ax.axes.get_xaxis().set_visible(False)ax.axes.get_yaxis().set_visible(False)if titles:ax.set_title(titles[i])return axes
以下展示訓練數據集中前幾個樣本的圖像及其相應的標簽。
為了使我們在讀取訓練集和測試集時更容易,我們使用內置的數據迭代器,而不是從零開始創建。
在每次迭代中,數據加載器每次都會讀取一小批量數據,大小為batch_size
。
通過內置數據迭代器,我們可以隨機打亂了所有樣本,從而無偏見地讀取小批量。
batch_size = 256def get_dataloader_workers(): #@save"""使用4個進程來讀取數據"""return 4train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=get_dataloader_workers())timer = d2l.Timer() # 用來測試速度
for X, y in train_iter:continue
f'{timer.stop():.2f} sec'
在模型訓練之前,一般都是需要測試數據讀取的速度,數據讀取的速度需要比模型的訓練速度更快才好。
基于上述內容,現在我們定義load_data_fashion_mnist
函數,用于獲取和讀取Fashion-MNIST數據集。這個函數返回訓練集和驗證集的數據迭代器。此外,這個函數還接受一個可選參數resize
,用來將圖像大小調整為另一種形狀。
def load_data_fashion_mnist(batch_size, resize=None): #@save"""下載Fashion-MNIST數據集,然后將其加載到內存中"""trans = [transforms.ToTensor()]if resize:trans.insert(0, transforms.Resize(resize))trans = transforms.Compose(trans)mnist_train = torchvision.datasets.FashionMNIST(root="../data", train=True, transform=trans, download=True)mnist_test = torchvision.datasets.FashionMNIST(root="../data", train=False, transform=trans, download=True)return (data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=get_dataloader_workers()),data.DataLoader(mnist_test, batch_size, shuffle=False,num_workers=get_dataloader_workers()))
Softmax 回歸從0開始實現
import torch
from IPython import display
from d2l import torch as d2lbatch_size = 256 # 每次隨機讀取256張圖片
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) # 前面實現過
由于圖像是12828的,但是對于softmax來說,輸入的需要是一個向量。(但是這種操作會損失很多空間信息,卷積部分解決。)因此我們將展平每個圖像,把它們看作長度為784的向量。數據集有十個類別,因此網絡輸出維度就是10。
num_inputs = 784 # 將空間拉長,28*28拉成784的一個向量
num_outputs = 10W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
# 行數為輸入的個數,列數等于輸出的個數
b = torch.zeros(num_outputs, requires_grad=True)
# 對每一個輸出,都需要有一個偏移
下面定義 softmax 操作:
實現softmax由三個步驟組成:
- 對每個項求冪(使用
exp
); - 對每一行求和(小批量中每個樣本是一行),得到每個樣本的規范化常數;
- 將每一行除以其規范化常數,確保結果的和為1。
表達式如下:
s o f t m a x ( X ) i j = exp ? ( X i j ) ∑ k exp ? ( X i k ) . \mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(\mathbf{X}_{ij})}{\sum_k \exp(\mathbf{X}_{ik})}. softmax(X)ij?=∑k?exp(Xik?)exp(Xij?)?.分母或規范化常數,有時也稱為配分函數(其對數稱為對數-配分函數)。該名稱來自統計物理學中一個模擬粒子群分布的方程。
def softmax(X):X_exp = torch.exp(X) # 對X中的每個元素作指數運算partition = X_exp.sum(1, keepdim=True) # 按照每一行進行求和return X_exp / partition # 這里應用了廣播機制
定義softmax操作后,可以實現softmax回歸模型。
下面的代碼定義了輸入如何通過網絡映射到輸出。
注意,將數據傳遞到模型之前,我們使用reshape
函數將每張原始圖像展平為向量。
def net(X):return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
首先回顧一下交叉熵:
交叉熵采用真實標簽的預測概率的負對數似然。這里我們不使用Python的for循環迭代預測(這往往是低效的),而是通過一個運算符選擇所有元素。
下面,**創建一個數據樣本y_hat
,其中包含2個樣本在3個類別的預測概率,以及它們對應的標簽y
。**有了y
,我們知道在第一個樣本中,第一類是正確的預測;而在第二個樣本中,第三類是正確的預測。然后(使用y
作為y_hat
中概率的索引),我們選擇第一個樣本中第一個類的概率和第二個樣本中第三個類的概率。
y = torch.tensor([0, 2]) # 表示兩個樣本的真實標簽分別為0、2
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], y]
# 對第0個樣本,拿出y[0]對應的那個元素,對第一個樣本,拿出y[1]對應的那個元素
# [0, 1] 是一個索引列表,表示要選取 y_hat 中的第一行和第二行。
基于上述,我們下面來實現交叉熵損失函數:
# 了解交叉熵公式和代碼上述原理,一行代碼即可完成。
def cross_entropy(y_hat, y):return - torch.log(y_hat[range(len(y_hat)), y])cross_entropy(y_hat, y)
由于上述是分類問題,因此需要將預測類別與真實 y y y 元素進行比較:
def accuracy(y_hat, y): #@save"""計算預測正確的數量"""# 要是 y_hat 是一個二維矩陣且列數也大于1 if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: y_hat = y_hat.argmax(axis=1) # 按每一行來存最大值的下標cmp = y_hat.type(y.dtype) == y # 將 y_hat 轉換為 y 的數據類型,然后作比較return float(cmp.type(y.dtype).sum()) # 返回預測正確的樣本數
我們將繼續使用之前定義的變量y_hat
和y
分別作為預測的概率分布和標簽。可以看到,第一個樣本的預測類別是2(該行的最大元素為0.6,索引為2),這與實際標簽0不一致。第二個樣本的預測類別是2(該行的最大元素為0.5,索引為2),這與實際標簽2一致。因此,這兩個樣本的分類精度率為0.5。
同樣,對于任意數據迭代器data_iter
可訪問的數據集,可以評估在任意模型net
的精度。
def evaluate_accuracy(net, data_iter): #@save"""計算在指定數據集上模型的精度"""if isinstance(net, torch.nn.Module):net.eval() # 將模型設置為評估模式metric = Accumulator(2) # 正確預測數、預測總數with torch.no_grad():for X, y in data_iter:metric.add(accuracy(net(X), y), y.numel())return metric[0] / metric[1]
這里定義一個實用程序類Accumulator
,用于對多個變量進行累加。在上面的evaluate_accuracy
函數中,我們在(Accumulator
實例中創建了2個變量,分別用于存儲正確預測的數量和預測的總數量)。當我們遍歷數據集時,兩者都將隨著時間的推移而累加。
class Accumulator: #@save"""在n個變量上累加"""def __init__(self, n):self.data = [0.0] * ndef add(self, *args):self.data = [a + float(b) for a, b in zip(self.data, args)]def reset(self):self.data = [0.0] * len(self.data)def __getitem__(self, idx):return self.data[idx]
下面就可以進行 softmax 的回歸訓練了:
def train_epoch_ch3(net, train_iter, loss, updater): #@save"""訓練模型一個迭代周期(定義見第3章)"""# 將模型設置為訓練模式if isinstance(net, torch.nn.Module):net.train()# 訓練損失總和、訓練準確度總和、樣本數metric = Accumulator(3)for X, y in train_iter:# 計算梯度并更新參數y_hat = net(X)l = loss(y_hat, y)if isinstance(updater, torch.optim.Optimizer):# 使用PyTorch內置的優化器和損失函數updater.zero_grad()l.mean().backward()updater.step()else:# 使用定制的優化器和損失函數l.sum().backward()updater(X.shape[0])metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())# 返回訓練損失和訓練精度return metric[0] / metric[2], metric[1] / metric[2]
在展示訓練函數的實現之前,我們[定義一個在動畫中繪制數據的實用程序類]Animator
class Animator: #@save"""在動畫中繪制數據"""def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,ylim=None, xscale='linear', yscale='linear',fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,figsize=(3.5, 2.5)):# 增量地繪制多條線if legend is None:legend = []d2l.use_svg_display()self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)if nrows * ncols == 1:self.axes = [self.axes, ]# 使用lambda函數捕獲參數self.config_axes = lambda: d2l.set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)self.X, self.Y, self.fmts = None, None, fmtsdef add(self, x, y):# 向圖表中添加多個數據點if not hasattr(y, "__len__"):y = [y]n = len(y)if not hasattr(x, "__len__"):x = [x] * nif not self.X:self.X = [[] for _ in range(n)]if not self.Y:self.Y = [[] for _ in range(n)]for i, (a, b) in enumerate(zip(x, y)):if a is not None and b is not None:self.X[i].append(a)self.Y[i].append(b)self.axes[0].cla()for x, y, fmt in zip(self.X, self.Y, self.fmts):self.axes[0].plot(x, y, fmt)self.config_axes()display.display(self.fig)display.clear_output(wait=True)
下面開始訓練:
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save"""訓練模型(定義見第3章)"""animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],legend=['train loss', 'train acc', 'test acc'])for epoch in range(num_epochs):train_metrics = train_epoch_ch3(net, train_iter, loss, updater)test_acc = evaluate_accuracy(net, test_iter)animator.add(epoch + 1, train_metrics + (test_acc,))train_loss, train_acc = train_metricsassert train_loss < 0.5, train_lossassert train_acc <= 1 and train_acc > 0.7, train_accassert test_acc <= 1 and test_acc > 0.7, test_acc
[小批量隨機梯度下降來優化模型的損失函數],設置學習率為0.1
lr = 0.1def updater(batch_size):return d2l.sgd([W, b], lr, batch_size)
對圖像進行預測:
def predict_ch3(net, test_iter, n=6): #@save"""預測標簽"""for X, y in test_iter:breaktrues = d2l.get_fashion_mnist_labels(y)preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))titles = [true +'\n' + pred for true, pred in zip(trues, preds)]d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])predict_ch3(net, test_iter)
Softmax 回歸的簡潔實現
通過深度學習框架的高級API也能更方便地實現softmax回歸模型:
import torch
from torch import nn
from d2l import torch as d2lbatch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
Softmax 回歸的輸出層是一個全連接層
# PyTorch不會隱式地調整輸入的形狀。因此,
# 我們在線性層前定義了展平層(flatten),來調整網絡輸入的形狀
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))def init_weights(m):if type(m) == nn.Linear:nn.init.normal_(m.weight, std=0.01)net.apply(init_weights);
在交叉熵損失函數中傳遞未規范化的預測,并同時計算softmax及其對數
loss = nn.CrossEntropyLoss(reduction='none')# 不進行任何減少操作,返回每個樣本的損失值。
使用學習率為0.1的小批量隨機梯度下降作為優化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
訓練,重用之前編寫的函數:
QA思考
Q1:softlabel訓練策略。
上述被稱為軟標簽,旨在通過使用非硬性(即不是0或1的絕對分類結果)的目標標簽來提高模型的泛化能力和魯棒性。
傳統的分類任務中,目標標簽通常是one-hot編碼的形式,即對于每個樣本,正確的類別標記為1,其他類別標記為0。但是實際上對于邊界值是很難達到的,比如對于softmax函數而言:
softmax ( z i ) = e z i ∑ j = 1 n e z j \text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{n} e^{z_j}} softmax(zi?)=∑j=1n?ezj?ezi??
要想使其輸出為 1 ,則需要某一個 z i z_i zi? ,趨近于無窮才行。
而softlabel則允許這些標簽值位于(0, 1)之間,并且所有類別的概率之和通常為1。這意味著即使是錯誤的類別也可能被賦予一定的概率,從而向模型傳達“某種程度上的正確”。比如我可以認為0.9 就是正確,0.1 就是不正確。
Q2 : softmax 回歸和 logistic 回歸的聯系。
可以認為logistic是softmax的特例,也就是logistic是一個兩分類的問題,只需要輸出一個類別的概率 P P P 即可,剩下的直接 1 ? P 1-P 1?P 即可。但是在實際的分類問題中,兩分類的問題很少。
Q3 : 在 Accuracy函數中為啥不把除以 len(y) 做完呢?
在 Accuracy 函數中,不能直接除以 len(y),因為最后一個 batch 的樣本數量可能會少于設定的 batch size。為了確保準確率計算的正確性,應該根據當前 batch 實際包含的樣本數量進行歸一化,而不是固定地使用完整的 batch size。
補充:
考慮到李沐老師的視線中使用到了d2l,且是在jupyter上面進行實現的,但是我現在不想用d2l,以及需要再Pycharm上面編寫,于是我根據上述代碼編寫了下面的代碼,結果也能很好的復現李沐老師代碼的結果。
import torch
import torchvision
from torchvision import transforms
from torch.utils import data
import matplotlib.pyplot as pltclass Animator:def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,ylim=None, xscale='linear', yscale='linear',fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5)):if legend is None:legend = []self.xlabel = xlabelself.ylabel = ylabelself.legend = legendself.xlim = xlimself.ylim = ylimself.xscale = xscaleself.yscale = yscaleself.fmts = fmtsself.figsize = figsizeself.X, self.Y = [], []def add(self, x, y):if not hasattr(y, "__len__"):y = [y]n = len(y)if not hasattr(x, "__len__"):x = [x] * nif not self.X:self.X = [[] for _ in range(n)]if not self.Y:self.Y = [[] for _ in range(n)]for i, (a, b) in enumerate(zip(x, y)):if a is not None and b is not None:self.X[i].append(a)self.Y[i].append(b)def show(self):plt.figure(figsize=self.figsize)for x_data, y_data, fmt in zip(self.X, self.Y, self.fmts):plt.plot(x_data, y_data, fmt)plt.xlabel(self.xlabel)plt.ylabel(self.ylabel)if self.legend:plt.legend(self.legend)if self.xlim:plt.xlim(self.xlim)if self.ylim:plt.ylim(self.ylim)plt.xscale(self.xscale)plt.yscale(self.yscale)plt.grid()plt.show()def get_dataloader_workers():return 0 # 禁用多進程加載def load_data_fashion_mnist(batch_size, resize=None):trans = [transforms.ToTensor()]if resize:trans.insert(0, transforms.Resize(resize))trans = transforms.Compose(trans)mnist_train = torchvision.datasets.FashionMNIST("./data", train=True, transform=trans, download=True)mnist_test = torchvision.datasets.FashionMNIST("./data", train=False, transform=trans, download=True)return (data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=get_dataloader_workers()),data.DataLoader(mnist_test, batch_size, shuffle=False, num_workers=get_dataloader_workers()))# softmax 實現
def softmax(X):X_exp = torch.exp(X)partition = X_exp.sum(1, keepdim=True)return X_exp / partition# 回歸模型
def net(X):return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)# 交叉熵損失函數
def cross_entropy(y_hat, y):return -torch.log(y_hat[range(len(y_hat)), y])# 預測正確的數量
def accuracy(y_hat, y):if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:y_hat = y_hat.argmax(axis=1)cmp = y_hat.type(y.dtype) == yreturn float(cmp.type(y.dtype).sum())class Accumulator:def __init__(self, n):self.data = [0.0] * ndef add(self, *args):self.data = [a + float(b) for a, b in zip(self.data, args)]def reset(self):self.data = [0.0] * len(self.data)def __getitem__(self, idx):return self.data[idx]def evaluate_accuracy(net, data_iter):if isinstance(net, torch.nn.Module):net.eval()metric = Accumulator(2)with torch.no_grad():for X, y in data_iter:metric.add(accuracy(net(X), y), y.numel())return metric[0] / metric[1]def train_epoch_ch3(net, train_iter, loss, updater):if isinstance(net, torch.nn.Module):net.train()metric = Accumulator(3)for X, y in train_iter:y_hat = net(X)l = loss(y_hat, y)if isinstance(updater, torch.optim.Optimizer):updater.zero_grad()l.mean().backward()updater.step()else:l.sum().backward()updater(X.shape[0])metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())return metric[0] / metric[2], metric[1] / metric[2]def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],legend=['train loss', 'train acc', 'test acc'])for epoch in range(num_epochs):train_metrics = train_epoch_ch3(net, train_iter, loss, updater)test_acc = evaluate_accuracy(net, test_iter)animator.add(epoch + 1, train_metrics + (test_acc,))train_loss, train_acc = train_metricsassert train_loss < 0.5, train_lossassert train_acc <= 1 and train_acc > 0.7, train_accassert test_acc <= 1 and test_acc > 0.7, test_accanimator.show() # 展示最終結果圖def sgd(params, lr, batch_size):with torch.no_grad():for param in params:param -= lr * param.grad / batch_sizeparam.grad.zero_()def updater(batch_size):return sgd([W, b], lr, batch_size)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_images(imgs, num_rows, num_cols, titles=None, scale=1.5):figsize = (num_cols * scale, num_rows * scale)_, axes = plt.subplots(num_rows, num_cols, figsize=figsize)axes = axes.flatten()for i, (ax, img) in enumerate(zip(axes, imgs)):if torch.is_tensor(img):ax.imshow(img.numpy(), cmap='gray')else:ax.imshow(img, cmap='gray')ax.axes.get_xaxis().set_visible(False)ax.axes.get_yaxis().set_visible(False)if titles:ax.set_title(titles[i])plt.show()def predict_ch3(net, test_iter, n=6):for X, y in test_iter:breaktrues = get_fashion_mnist_labels(y)preds = get_fashion_mnist_labels(net(X).argmax(axis=1))titles = [true + '\n' + pred for true, pred in zip(trues, preds)]show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])if __name__ == "__main__":# 定義超參數batch_size = 256num_epochs = 10lr = 0.1# 加載數據train_iter, test_iter = load_data_fashion_mnist(batch_size)# 初始化模型參數num_inputs = 784num_outputs = 10W = torch.normal(0, 0.1, size=(num_inputs, num_outputs), requires_grad=True)b = torch.zeros(num_outputs, requires_grad=True)# 訓練模型train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)# 測試模型并顯示預測結果predict_ch3(net, test_iter)