Skip to content

[Keras] 如何架構多標籤分類 (Multi-label Classification) 模型

最近遇到了一個要進行『多標籤分類』(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,代表我們的輸出值已經獨立了,這才會是我們想要的多標籤分類。

Leave a Reply