Last Updated on 2021-05-17 by Clay
最近遇到了一個要進行『多標籤分類』(Multi-label Classification) 的任務,這才驚覺自己從來沒碰過這方面的模型。
一般而言,我們搭建模型的難易度,通常是從『二元分類』 (Binary Classification) 到『多分類』 (Multi-classes Classification) 再到『多標籤分類』(Multi-labels Classification)。而多標籤分類,恰恰更符合現實中我們會遇到的情況。
最簡單來說,假設我們有以下這張圖片:
二分類就是問你:這張圖片裡面有沒有山呢?
答案是:『有』。二選一的回答。
多分類就是:這張圖片的風景是『大自然』、『海洋』、『外太空』、還是『沙漠』呢?
答案是:『大自然』。多個選項裡面,我們選唯一正確的回答。
多標籤分類則是:這張圖片裡面有沒有『山』?『房子』?『樹』?『外星人』?
答案是:有『山』、有『房子』、有『樹』,然後——沒有『外星人』。
以下,我還是拿經典的 MNIST 資料集來做測試,除了要判斷數字之外,我還希望多判斷一項『是否大於 5』的標籤。
如果想要參考正常的 MNIST 分類,也許可以參考我之前寫過的《使用 CNN 進行 MNIST 的手寫數字辨識 —— by Keras (實戰篇)》。
如果想要研究 Keras 的語法,可以參考: https://keras.io/ 。
錯誤想法
一開始,我想說我有多個標籤,那麼就使用 Softmax 來預測每個 Label 的機率如何?然而,我胡亂試的結果,恰恰好是不可行的。
事先聲明,這裡紀錄的是錯誤的過程,如果沒有興趣,可以直接參考後方的【Sigmoid】。
# -*- coding: utf-8 -*- import numpy as np from keras.models import Sequential, load_model from keras.layers import Dense, Flatten, Conv2D, MaxPool2D from keras.utils import np_utils from keras.datasets import mnist import matplotlib.pyplot as plt
首先,匯入所有我們需要的 Package。
# Mnist Dataset (X_train, Y_train), (X_test, Y_test) = mnist.load_data() x_train = X_train.reshape(60000, 1, 28, 28)/255 x_test = X_test.reshape(10000, 1, 28, 28)/255 y_train = np_utils.to_categorical(Y_train).astype(int).tolist() y_test = np_utils.to_categorical(Y_test).astype(int).tolist() for n in range(len(y_train)): if y_train[n].index(1) < 5: y_train[n].append(0) else: y_train[n].append(1) for n in range(len(y_test)): if y_test[n].index(1) < 5: y_test[n].append(0) else: y_test[n].append(1) y_train = np.array(y_train) y_test = np.array(y_test)
這次,我在 One-Hot 的 Label 後面又加上了一項數值:如果 Label 的答案大於 5,那麼我就標 1;反之,我就標 0。
這樣一來,我不僅要預測前面的分類,還要判斷最後是否大於 5,形成了多標籤分類。
# Model Structure model = Sequential() model.add(Conv2D(filters=32, kernel_size=3, input_shape=(1, 28, 28), activation='relu', padding='same')) model.add(MaxPool2D(pool_size=2, data_format='channels_first')) model.add(Flatten()) model.add(Dense(256, activation='relu')) model.add(Dense(11, activation='softmax')) print(model.summary())
這是我的模型架構。值得注意的是,我最後輸出的激活函數 (activation) 設定為 softmax。
# Train model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(x_train, y_train, epochs=10, batch_size=64, verbose=1)
開始訓練。
# Test loss, accuracy = model.evaluate(x_test, y_test) print('Test:') print('Loss: %s\nAccuracy: %s' % (loss, accuracy)) # Save model model.save('./CNN_Mnist.h5') # Load Model model = load_model('./CNN_Mnist.h5') # Display def image_predict(model, n): predict = model.predict(x_test) print('Answer:', Y_test[n]) plt.plot(predict[n]) plt.show() plt.imshow(X_test[n], cmap='gray') plt.show() if __name__ == '__main__': image_predict(model, 9)
訓練好了之後,我儲存起我們訓練的 Model,如果沒有打算再用,跳過不儲存也沒關係;之後我預測了我們 test 資料裡 index=9 的圖片。
Output:
Test:
Loss: 0.07145553915500641
Accuracy: 0.9517456293106079
Answer: 9
乍看之下, Accuracy 很高啊,看起來 softmax 多標籤分類的機率值也很準確,分別為表示數字為『9』以及最後標記為『大於 5 的情況』為 1。
乍看之下都很正確,但我想了想,就發現了不妙的地方。
我雖然拿 MNIST 來測試多標籤分類,但我真正要完成的標籤分類有非常非常多項啊!我難道該設定一個『閥值』來判斷我的資料是否真的具有某個『標籤』?
況且,每個資料的標籤數量並不一致,有的可能只有一個標籤,有的可能一口氣有十個標籤。
這樣一來,我到底該怎麼設定閥值?
所以我發現我錯誤的地方了。我想要的,是每個 label 都具有各自獨立的機率值,判斷我的資料是否真的具有某個標籤。
所以,我需要的其實不是 softmax,而是 sigmoid。
Sigmoid
以下是我改過之後的完整程式碼:
# -*- coding: utf-8 -*- import numpy as np from keras.models import Sequential, load_model from keras.layers import Dense, Flatten, Conv2D, MaxPool2D from keras.utils import np_utils from keras.datasets import mnist import matplotlib.pyplot as plt # Mnist Dataset (X_train, Y_train), (X_test, Y_test) = mnist.load_data() x_train = X_train.reshape(60000, 1, 28, 28)/255 x_test = X_test.reshape(10000, 1, 28, 28)/255 y_train = np_utils.to_categorical(Y_train).astype(int).tolist() y_test = np_utils.to_categorical(Y_test).astype(int).tolist() for n in range(len(y_train)): if y_train[n].index(1) < 5: y_train[n].append(0) else: y_train[n].append(1) for n in range(len(y_test)): if y_test[n].index(1) < 5: y_test[n].append(0) else: y_test[n].append(1) y_train = np.array(y_train) y_test = np.array(y_test) # Model Structure model = Sequential() model.add(Conv2D(filters=32, kernel_size=3, input_shape=(1, 28, 28), activation='relu', padding='same')) model.add(MaxPool2D(pool_size=2, data_format='channels_first')) model.add(Flatten()) model.add(Dense(256, activation='relu')) model.add(Dense(11, activation='sigmoid')) print(model.summary()) # Train model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(x_train, y_train, epochs=10, batch_size=64, verbose=1) # Test loss, accuracy = model.evaluate(x_test, y_test) print('Test:') print('Loss: %s\nAccuracy: %s' % (loss, accuracy)) # Save model model.save('./CNN_Mnist.h5') # Load Model model = load_model('./CNN_Mnist.h5') # Display def image_predict(model, n): predict = model.predict(x_test) print('Answer:', Y_test[n]) plt.plot(predict[n]) plt.show() plt.imshow(X_test[n], cmap='gray') plt.show() if __name__ == '__main__': image_predict(model, 9)
Output:
Test:
Loss: 0.013495276327256578
Accuracy: 0.995473325252533
Answer: 9
這次我們可以看到:『9』 和 『大於 5』 的輸出都是 1 ,其他的全部都是 0,代表我們的輸出值已經獨立了,這才會是我們想要的多標籤分類。