จัดกลุ่มภาพโดยไม่มี label ด้วย Deep Clustering

deep learning self-supervised learning

อธิบายวิธีจากเปเปอร์ Deep Clustering for Unsupervised Learning of Visual Features (2018)

Introduction

ในโลกนี้ เราอาจจะอยาก classify รูปอะไรก็ได้เต็มไปหมด ซึ่งก่อนหน้านี้เราก็ใช้วิธีแบบ supervised-learning ซึ่งเราก็ต้องมีทั้งข้อมูลรูปและข้อมูลที่บอกว่ารูปนั้นเป็นรูปอะไร หรือก็คือจัดกลุ่มรูปให้ก่อน แต่สมมติว่าถ้ารูปพวกนั้นมันเยอะเกิน เราจะมานั่ง label ที่ละรูปก็เหนื่อยอยู่นะ

ซึ่งจริง ๆ แล้วการ classify มันก็คือการที่เราพยายามสร้างโมเดลมาจัดกลุ่มรูปน่ะแหละ ให้รูปประเภทเดียวกันอยู่ในกลุ่มเดียวกันอะไรแบบนี้

ซึ่งจากปัญหาว่าถ้าเราขี้เกียจ label รูปทั้งหมดก่อนมาเทรนโมเดล เราจะจัดกลุ่มรูปยังไง? ก็เลยมีนักวิจัยเค้าพยายามจะใช้ deep learning ในการ extract ฟีเจอร์ออกจากรูปก่อน (ซึ่งฟีเจอร์พวกนั้นถูกเรียกได้หลายแบบ representation, high-level feature, latent vector) แล้วค่อยเอาฟีเจอร์พวกนั้นไปทำ clustering อีกที ซึ่งวิธีที่เค้าทำกันมาก็เช่น

เอาไว้ถ้ามีโอกาสจะเขียนรายละเอียดของพวกนี้ให้เพิ่มเติมนะครับ

Deep Clustering

ในเปเปอร์นี้เค้านำเสนอวิธีการการจัดกลุ่มของรูปแบบ end-to-end แบบที่ไม่ได้อยู่ในสองกลุ่มข้างต้น ซึ่งวิธีก็คือสร้าง label ปลอม ๆ ขึ้นมา (pseudolabel) ด้วยการเอา high level feature ของ image ไป clustering แล้วเอาผลของการ clustering มาเป็น pseudo label ให้ network เราเทรน แล้วก็วนกลับไปสร้าง pseudo label แล้วก็เทรนไปเรื่อย ๆ ตอนนี้อาจจะงง ๆ หน่อย แต่เดี๋ยวไว้ดูวิธีข้างล่างเอาแล้วกันนะ

สมการนี้ไม่ต้องดูก็ได้ ไปดูอีกทีภาพรวมของ DeepCluster เลย

เป้าหมายหรือ objective function ที่นำเสนอในเปเปอร์นี้แสดงดังสมการด้านล่าง

\begin{equation} \label{eq:deep_cluster} \min_{\theta,W} \frac{1}{N} \sum_{n=1}^{N} \ell (g_{W}(f_{\theta}(x_n)),y_n) \end{equation}

โดยที่

ซึ่งภาพรวมของ DeepCluster ก็จะประมาณนี้

Implementation (Simplified version)

ในส่วนนี้จะขอ implement เป็น simplified version เพื่อให้เข้าใจ step การทำงานได้ง่ายขึ้น (ซึ่ง performance ของเวอร์ชั่นที่ implement นี้จะแย่กว่าของจริงมาก 55) และเดี๋ยวจะเขียนรายละเอียดที่เค้าทำจริง ๆ ในเปเปอร์ควบคู่ไปด้วยครับ

ซึ่งถ้าใครอยากเอาวิธีนี้ไปใช้จริง ๆ แนะนำให้ไปใช้โค้ดที่ผู้เขียนเปเปอร์เค้า implement ไว้ดีกว่าครับ ^^

ในบทความนี้เราจะใช้ข้อมูล mnist แค่ 3 classes จำนวน 5,000 รูปเท่านั้น

ขั้นตอน

  1. อ่านข้อมูลรูปมา

     from tensorflow.keras.datasets import fashion_mnist, mnist, cifar10
     (train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
     train_labels = train_labels.ravel()
     train_images = train_images[np.isin(train_labels,[0,1,2])][:5000,:,:]/255.0
     train_labels= train_labels[np.isin(train_labels,[0,1,2])][:5000]
    

    ที่จริงแล้ว ในเปเปอร์เค้าทำเป็น pipeline แล้วมีการ augment รูป (การ shift, rotate, crop, ฯลฯ) แบบสุ่มด้วย

  2. ทำ sobel ซึ่งเป็น edge detection algorithm หนึ่งใช้เพื่อหาขอบของวัตถุในรูป

     from tensorflow.image import sobel_edges,rgb_to_grayscale
     from tensorflow.keras import backend as K 
     import tensorflow as tf
     image = tf.constant(train_images[:,:,:,np.newaxis], dtype=tf.float32)
     grad_components = sobel_edges(image)
     grad_mag_components = grad_components**2
     grad_mag_square = tf.math.reduce_sum(grad_mag_components,axis=-1)
     grad_mag_img = tf.sqrt(grad_mag_square)
     images = (K.eval(grad_mag_img))
    
  3. สร้างโมเดลส่วน feature generator ซึ่งเป็นโมเดลที่เอาไว้สกัด high-level feature ของแต่ละรูปออกมา
    • ในที่นี้ขอ implement เป็น CNN2D + Fully Connected ง่าย ๆ แล้วกัน

        input_layer = Input(shape=(28,28,1)) 
        cnn = Conv2D(8, (4, 4), padding="same", activation="relu")(input_layer)
        cnn = MaxPooling2D((2, 2))(cnn)
        cnn = Flatten()(cnn)
        dense = Dense(128,kernel_initializer='random_normal', bias_initializer='zeros',activation='relu')(cnn)
        feature_vector = Dense(4,kernel_initializer='random_normal', bias_initializer='zeros')(dense)
        feature_generator = Model(input_layer,feature_vector)
        feature_generator.summary()
      

    โดยที่ในเปเปอร์เค้าจะใช้เป็น VGG16 หรือ Alexnet ตัด layer สุดท้ายออก (ไม่ต้อง pretrain มาก่อน)

  4. ทำฟังก์ชันสร้างโมเดล classifier โดยการต่อ layers (เราจะเรียกว่า top layers) ออกมาจาก feature generator อีกที

    ซึ่ง top layers ทำหน้าที่รับ input เป็น high-level feature (ที่ออกมาจาก feature generator) แล้ว classify feature พวกนั้นตาม pseudo label

    • ที่นี้โค้ดที่ implement ออกมาจะประมาณนี้ จะเห็นได้ว่าใน layer แรก หรือ ReLU นั้นรับ input เป็น feature_vector ซึ่งก็คือ layers สุดท้ายของ feature_generator ในข้อก่อนหน้า

        from tensorflow.keras.initializers import RandomNormal
        def gen_classifier():
            dense_w = ReLU()(feature_vector)
            dense_w = Dense(16, activation='relu',kernel_initializer='random_normal', bias_initializer='zeros')(dense_w)
            classify_output = Dense(n_cluster, activation='softmax', kernel_initializer=RandomNormal(mean=0.0, stddev=0.01, seed=None), bias_initializer='zeros')(dense_w)
            classifier = Model(input_layer,classify_output)
            classifier.compile(optimizer=SGD(learning_rate=0.0005, momentum=0.0), loss='categorical_crossentropy', metrics='accuracy')
            return classifier
      
  5. ทดลองทำ clustering ตอนนี้เลย แล้วดูผล จะได้เก็บไว้เทียบหลังเทรนได้

     from sklearn.cluster import KMeans
     from sklearn.preprocessing import MinMaxScaler
     from sklearn.metrics import normalized_mutual_info_score
     from sklearn.metrics import confusion_matrix
     import matplotlib.gridspec as gridspec
     n_cluster = 3
     ## ใช้ feature generator ในการหา latent_vector ออกมา
     features = feature_generator.predict(images)
     ## เอา latent_vector ไปทำ kmeans
     kmeans = KMeans(n_clusters=n_cluster, random_state=131).fit_predict(features)
     ## หา NMI Score
     nmi_score = normalized_mutual_info_score(kmeans, train_labels)
     ## โค้ดในการพล็อต รบกวนตามไปดูในโค้ดฉบับเต็มนะครับ
     ....
     ....
    

    จะเห็นได้ว่าตอนนี้ NMI score ได้แค่ 0.42 แล้วก็ใน cluster 1 กับ 2 นัั้นมันจะปน ๆ กันระหว่างรูปเสื้อแขนยาว (class 0) กับเสื้อแขนสั้น (class 2)

    ถ้าไม่รู้ว่า NMI score คืออะไรมีอธิบายอยู่ล่างสุดเลยจ้า

  6. เทรนไปเรื่อย ๆ โดยที่ในแต่ละ epoch จะมีโค้ดรวม ๆ ง่าย ๆ ดังนี้

     for i in tqdm.tqdm(range(0,1000+1)):
         features = feature_generator.predict(images)
         kmean =  KMeans(n_clusters=n_cluster, random_state=np.random.randint(1234))
         clustering_result = kmean.fit_predict(features)
         pseudo_label = to_categorical(clustering_result, num_classes=n_cluster)
         classifier = gen_classifier()
         classifier.fit(images,pseudo_label, epochs=1, verbose=False)
    

    ซึ่งเดี๋ยวจะอธิบายทีละบรรทัดเลย

    • ก่อนอื่น เราสร้าง latent vector หรือ feature ของแต่ละรูปด้วย feature generator ก่อน

        features = feature_generator.predict(images)
      

      ที่จริงแล้วในเปเปอร์มีการนำ latent_vectors ขนาด 4096 ไปแปลงต่อด้วย PCA ให้เหลือแค่ขนาด 256 แล้ว regularize ด้วย l2 แล้วจึงนำเข้า K-mean แต่ ในบทความนี้ข้ามไปเพราะตัว latent vector เราก็เล็กอยู่แล้ว

    • ต่อมาเอา vectors พวกนั้นไปจัดกลุ่มด้วย Kmean clustering

        kmean =  KMeans(n_clusters=n_cluster, random_state=np.random.randint(1234))
        clustering_result = kmean.fit_predict(features)
      
    • แล้วเอาผลของการจัดกลุ่มมาเป็น pseudo label

        pseudo_label = to_categorical(clustering_result, num_classes=n_cluster)
      

      ที่จริงแล้วในขั้นตอนนี้ในเปเปอร์มีการทำเพิ่มเติมอีก 2 อย่างด้วยกัน เพื่อป้องกันปัญหา empty clusters และ trivial parameterization ซึ่งเดี๋ยวจะพูดถึงใน section ถัดไป ว่ามันคืออะไรแล้วผู้เขียนเปเปอร์เสนอให้แก้ปัญหานี้อย่างไร

    • ใช้ฟังก์ชันในข้อที่ 2. มาสร้าง classifier (หรือก็คือสร้าง top layer ขึ้นมาใหม่ แล้วต่อมันไปบน feature generator)

        classifier = gen_classifier()
      

      ปล. สำหรับคนที่งงว่าทำไมเราต้องสร้างตัว top layer ใหม่ทุกรอบเลย (ทำไมเราไม่เก็บ classifier ไว้ใช้ จะสร้างใหม่ทำไมทุก epoch) เอาจริง ๆ เหมือนเค้าไม่ได้ discuss เรื่องนี้ในเปเปอร์นะ แต่ผมคิดว่ามันน่าจะเป็นเพราะว่าตัวเลข cluster มันอาจจะเปลี่ยนไปเรื่อย ๆ ในแต่ละ epoch เราเลยต้องลบตัว top layer ทิ้ง คือให้มองว่าตัว feature_generator จะมีหน้าที่ในการสร้างฟีเจอร์ขึ้นมา ส่วนตัว top layer มีหน้าที่ในการแมพฟีเจอร์พวกนั้นไปแต่ละ cluster (แต่ละเลข cluster)

    • เทรน classifier โดยมี input เป็น image และ label เป็น pseudo label ที่ได้มาในข้อก่อนหน้า ซึ่งในขั้นตอนนี้ตอนที่เราใช้คำสั่ง fit นั้น มันจะทำการ backpropagate กลับไปอัพเดท weight ทั้งใน top layers และใน feature generator

        classifier = gen_classifier()
        classifier.fit(images, pseudo_label, epochs=1, verbose=False)
      
    • ขั้นสุดท้ายก็คือตัด top layer ทิ้ง (แต่ขั้นนี้ไม่ต้องเขียนโค้ดอะไร เพราะเดี๋ยวเราสร้าง classifier ด้วยการสร้าง top layer อันใหม่ไปต่อจาก feature generator อยู่แล้ว ในนี้แค่อยากใส่ไว้ให้ดูเฉย ๆ)

  7. พอเทรนเสร็จตามจำนวน epoch ที่เราตั้งไว้ก็มาดูผลกัน

    จะเห็นได้ว่าตอนนี้ NMI score เพิ่มจาก 0.42 มาเป็น 0.57 แล้วก็ใน cluster 0 กับ 2 นัั้นมันจะหนักไปทาง label ใด label หนึ่งมากขึ้น เช่น cluster 0 นั้นก็จะเป็นกลุ่มของเสื้อแขนยาว (class 2)

ประเด็นอื่น ๆ

ปัญหา Trivial Solution

Empty Cluster

ปัญหา: คือที่จริงแล้ว solution ที่จะทำให้ loss ในสมการที่ \eqref{eq:deep_cluster} เหลือน้อยที่สุดก็คือการที่

ก็แปลว่า จากสมการที่ \eqref{eq:deep_cluster} ถ้าเราหา \(\theta\) ที่ทำให้ใส่รูปอะไรเข้าไปแล้วมันได้ latent vector หรือ \(f_{\theta}(x)\) ที่หน้าตาเหมือนกันออกมา เราจะทำให้ loss ในสมการที่ \eqref{eq:deep_cluster} กลายเป็น 0 ไปเลย สุดยอดดดดดด (ซะที่ไหนล่ะ)

วิธีแก้:ในเปเปอร์นี้แก้กันง่าย ๆ เลยก็คือ ถ้ามันมี cluster ที่ไม่มีข้อมูลอยู่เลย เราจะเอา centroid ของ cluster ที่มีข้อมูลมาเปลี่ยน ๆ นิดนึง (random +- นิดหน่อย) แล้วแบ่ง cluster นั้นออกเป็น 2 clusters ทำไปเรื่อย ๆ จนกว่าจะมีจำนวน cluster ที่มีข้อมูลเท่ากับจำนวน cluster ที่เราตั้งไว้ แล้วค่อยเอาไปทำ pseudo-label แล้วไปเทรนต่อ หรือก็คือเราบังคับมันให้ทำการแบ่งกลุ่มอยู่ตลอดเวลา และหวังว่าเมื่อเทรนไปเรื่อย ๆ กลุ่มพวกนั้นจะเป็นกลุ่มที่เราต้องการให้แบ่ง

Trivial Parametrization

ปัญหา:เป็นปัญหาที่เกิดจากเกิด cluster นึงที่ใหญ่มากกว่า cluster กลุ่มอื่นขึ้นมา ซึ่งที่จริงอาจจะเกิดจากจำนวนรูปแต่ละประเภทมีไม่เท่ากันก็ได้ ถ้าเกิดแบบนี้ขึ้นมา neural network เราก็จะ classify ได้ไม่ดี (ที่จริงมันก็คือปัญหา imbalance class ธรรมดานั่นแหละ)

วิธีแก้:ซึ่งในเปเปอร์นี้เค้าแก้ด้วยการ sample ข้อมูลจากแต่ละ cluster ให้เท่า ๆ กันก่อนเอามาเทรน (เท่าที่เข้าใจมันก็คือ downsampling)

Trivial Feature

ปัญหา:เป็นปัญหาที่เกิดจากตัว CNN เราไปเรียนรู้อย่างอื่นแทนที่จะเป็นสิ่งที่เราต้องการ เช่น เราต้องการให้แยกรูปนกกะแมว มันดันไปเรียนรู้เรื่องสีแล้ว deepcluster เราก็ไป cluster ตามสี แทนที่จะไปเรียนรู้เรื่องฟีเจอร์ที่เกี่ยวกับนกหรือแมวจริงๆเช่น มีปีกหรือไม่มีปีก, มีสองหรือสี่ขา, ฯลฯ

จากรูปด้านล่างเค้าลองเอา filter ของ alexnet ที่เทรนด้วยรูปสี (ทางซ้าย) และรูปขาวดำที่ผ่านการทำ edge detection มา (ทางขวา) ออกมาดู เราจะพบว่ามันมีพวก filter จำนวนนึงในรูปทางซ้ายที่เป็นสีเหลี่ยมที่มีแต่สี ไม่มีลายอะไรเลย ซึ่งก็คือ filter พวกนั้นมันโฟกัสแต่สีของภาพ ไม่ได้เรียนรู้การจับขอบหรือรูปทรงของวัตถุในภาพซึ่งสำคัญกว่าสี

วิธีแก้:ในเปเปอร์นี้เค้าเลยเสนอให้ทำ edge detection ด้วย sobel ก่อนนำเข้าโมเดล (ก็คือเค้าไม่เอาสีเข้าโมเดล) ซึ่งเป็นการช่วยสกัดฟีเจอร์ให้นิดนึงก่อน

จำนวน cluster (ค่า k) เป็นเท่าไหร่ดี ?

ก็ที่จริงก็เหมือนเวลาเราทำ clustering ทั่ว ๆ ไปก็คือเราก็ต้องหาเอาว่าเท่าไหร่ดีที่สุด อย่างในเปเปอร์เค้าลองทำกับข้อมูล ImageNet ที่มี 1,000 classes แต่เค้าจูนค่า k ไปเรื่อย ๆ จนพบว่าค่า k ที่ 10,000 น่ะดีที่สุด

Appendix

Normalized Mutual Information (NMI Score)

NMI score เป็น score ที่มีค่าระหว่าง 0-1 ซึ่งวัดว่า information ระหว่างข้อมูลสองชุดนั้นตรงกันแค่ไหนโดยที่ไม่สนตัวเลข label เช่น

โดยสูตรของ NMI score นั้น เป็นดังนี้

\begin{equation} \label{eq:NMI} \frac{I(Y;C)}{H(Y)H(C)} \end{equation}

โดยที่

Discliamer

รายละเอียดในบทความนี้มาจากความเข้าใจส่วนตัว อาจมีข้อผิดพลาด หากพบจุดผิดพลาด ขอความกรุณาแจ้งทาง facebook หรือ email: thammasorn.han@hotmail.com

Reference: