CS231n課程作業(一)KNN分類器

一、前言

CS231n是斯坦福大學開設的一門深度學習與計算機視覺課程,是目前公認的該領域內最好的公開課。目前,該課程的全部資料已經被翻譯為中文,非常適合自學。該課程和相關資料的地址如下:

課程不光有精彩的講解,還提供了非常精致的課后作業。唯一的遺憾是作業沒有公布標準答案。因此我把自己的答案發出來與大家交流。

二、編程環境

官方建議使用Anaconda,它是Python的一個發布版,包含了最流行的科研、數學、工程和數據分析Python包。只需要安裝這一個東西就夠了,非常方便。建議下載Python2.7版,因為課程給出的例程都是在Python2.7下測試通過的,而在Python3.x中可能會出錯。

從shell啟動jupyter notebook,就可以在瀏覽器中編程了,不需要本地IDE。

三、KNN分類器

用K-最近鄰(K Nearest Neighbor,KNN)分類器對圖像分類在實際中是不可行的,不過這里只是為了熟悉一些基本的操作,所以第一個作業從KNN開始。

KNN的基本原理是,給定一張測試圖片,拿它和所有的訓練集圖片比較,找出最相近的K個(全部像素向量的歐氏距離越短越相近),由這K個圖片的標簽投票決定(出現次數最多的標簽勝出)測試圖片的標簽。KNN方法不需要訓練時間,但在測試時需要做大量比對,因此測試性能極低。

下面給出Assignment1中的KNN分類器部分的作業答案。

  1. 兩層循環計算距離
    下面代碼中給出了兩種做法,方案1是比較直觀的做法,兩張圖片相減平方再求和。方案2用NumPy提供的norm方法,直接計算范數。
  def compute_distances_two_loops(self, X):
    """
    Compute the distance between each test point in X and each training point
    in self.X_train using a nested loop over both the training data and the 
    test data.

    Inputs:
    - X: A numpy array of shape (num_test, D) containing test data.

    Returns:
    - dists: A numpy array of shape (num_test, num_train) where dists[i, j]
      is the Euclidean distance between the ith test point and the jth training
      point.
    """
    num_test = X.shape[0]
    num_train = self.X_train.shape[0]
    dists = np.zeros((num_test, num_train))
    for i in xrange(num_test):
      for j in xrange(num_train):
        #####################################################################
        # TODO:                                                             #
        # Compute the l2 distance between the ith test point and the jth    #
        # training point, and store the result in dists[i, j]. You should   #
        # not use a loop over dimension.                                    #
        #####################################################################
        #方案1
        #dists[i, j] = np.sqrt(np.sum((X[i] - self.X_train[j]) ** 2))
        
        #方案2
        dists[i, j] = np.linalg.norm(self.X_train[j,:]-X[i,:])
        #####################################################################
        #                       END OF YOUR CODE                            #
        #####################################################################
    return dists
  1. 一層循環計算距離
    直接對整個訓練集圖片操作,此時self.X_train的大小為5000×3072,而X[i]的大小為1×3072,兩者相減會自動對X[i]進行廣播,使其擴展到與self.X_train相同的大小。如果不清楚廣播的用法,可以參考文檔。此時執行sum或者norm操作的話,還需要指定軸,令axis=1。NumPy中的軸是個很令人迷惑的概念,根據我的理解,不管多少維的矩陣,軸的序號總是從左向右計數,被指定的軸的大小在操作后會被改變。例如,本例中,np.sum((X[i] - self.X_train) ** 2, axis=1),里面的運算X[i] - self.X_train的結果是個5000*3072的矩陣,對這個矩陣沿著1號軸求和,從左向右數,3072所在的維度就是1號軸(軸序號從0開始),因此,該維度的大小將會改變,而其它維度保持不變。對于sum來說,直接把這個維度的值全部加起來,因此最后得到了長度為5000的一維矩陣。norm同理。
  def compute_distances_one_loop(self, X):
    """
    Compute the distance between each test point in X and each training point
    in self.X_train using a single loop over the test data.

    Input / Output: Same as compute_distances_two_loops
    """
    num_test = X.shape[0]
    num_train = self.X_train.shape[0]
    dists = np.zeros((num_test, num_train))
    for i in xrange(num_test):
      #######################################################################
      # TODO:                                                               #
      # Compute the l2 distance between the ith test point and all training #
      # points, and store the result in dists[i, :].                        #
      #######################################################################
      #方案1
      #dists[i, :] = np.sqrt(np.sum((X[i] - self.X_train) ** 2, axis=1))
    
      #方案2
      dists[i, :] = np.linalg.norm(self.X_train - X[i,:], axis = 1)
      #######################################################################
      #                         END OF YOUR CODE                            #
      #######################################################################
    return dists
  1. 無循環計算距離
    這一步倒是很有難度。題目中給出了提示——使用乘法和兩個廣播求和,可惜我并沒想明白怎么用。方案一是我的思路,完全沿襲前面的做法,充分利用廣播使兩個矩陣擴展到相同的維度。具體來說,原本X的大小是500×3072,現在我把它強行變成500×1×3072,與大小為5000×3072的self.X_train相減,按照廣播規則,結果將是500×5000×3072的矩陣。再對2號軸(對應3072的那一維)求和、開根號,最后得到500×5000的矩陣。
    方案二則是按照提示的思路實現的(真不知道是怎么想到的)。把計算歐氏距離的式子差的平方展開,變成平方的和減去交叉項的2倍。的確是個很妙的方案。
  def compute_distances_no_loops(self, X):
    """
    Compute the distance between each test point in X and each training point
    in self.X_train using no explicit loops.

    Input / Output: Same as compute_distances_two_loops
    """
    num_test = X.shape[0]
    num_train = self.X_train.shape[0]
    dists = np.zeros((num_test, num_train)) 
    #########################################################################
    # TODO:                                                                 #
    # Compute the l2 distance between all test points and all training      #
    # points without using any explicit loops, and store the result in      #
    # dists.                                                                #
    #                                                                       #
    # You should implement this function using only basic array operations; #
    # in particular you should not use functions from scipy.                #
    #                                                                       #
    # HINT: Try to formulate the l2 distance using matrix multiplication    #
    #       and two broadcast sums.                                         #
    #########################################################################
    
    #方案1
    #dists = np.sqrt(np.sum((X.reshape(num_test,1,X.shape[1]) - self.X_train) ** 2, axis=2))
    
    #方案2
    dists = np.multiply(np.dot(X,self.X_train.T),-2)  
    sq1 = np.sum(np.square(X),axis=1,keepdims = True)  
    sq2 = np.sum(np.square(self.X_train),axis=1)  
    dists = np.add(dists,sq1)  
    dists = np.add(dists,sq2)  
    dists = np.sqrt(dists)
    #########################################################################
    #                         END OF YOUR CODE                              #
    #########################################################################
    return dists
  1. 分類預測
    這里用到了argsort函數,輸出的結果是從小到大排序后的下標,也就是說,結果列表中的第一個值是最小的數的下標,以此類推。
    這句代碼closest_y = self.y_train[train_topK_index]用到了整型數組訪問語法,即取出self.y_train中以train_topK_index中包含的值為下標的內容。
    bincount用來計算列表中每個數出現的次數,任意數字n出現的次數保存在count[n]中。
    argmax找出列表中最大值的下標。
  def predict_labels(self, dists, k=1):
    """
    Given a matrix of distances between test points and training points,
    predict a label for each test point.

    Inputs:
    - dists: A numpy array of shape (num_test, num_train) where dists[i, j]
      gives the distance betwen the ith test point and the jth training point.

    Returns:
    - y: A numpy array of shape (num_test,) containing predicted labels for the
      test data, where y[i] is the predicted label for the test point X[i].  
    """
    num_test = dists.shape[0]
    y_pred = np.zeros(num_test)
    for i in xrange(num_test):
      # A list of length k storing the labels of the k nearest neighbors to
      # the ith test point.
      closest_y = []
      #########################################################################
      # TODO:                                                                 #
      # Use the distance matrix to find the k nearest neighbors of the ith    #
      # testing point, and use self.y_train to find the labels of these       #
      # neighbors. Store these labels in closest_y.                           #
      # Hint: Look up the function numpy.argsort.                             #
      #########################################################################
      index_array = np.argsort(dists[i, :])
      train_topK_index = index_array[:k]
      closest_y = self.y_train[train_topK_index]
      
      #########################################################################
      # TODO:                                                                 #
      # Now that you have found the labels of the k nearest neighbors, you    #
      # need to find the most common label in the list closest_y of labels.   #
      # Store this label in y_pred[i]. Break ties by choosing the smaller     #
      # label.                                                                #
      #########################################################################
      count = np.bincount(closest_y)
      y_pred[i] = np.argmax(count)
      #########################################################################
      #                           END OF YOUR CODE                            # 
      #########################################################################

    return y_pred
  1. 交叉驗證
    這部分代碼比較復雜。首先是把訓練集分為5組,使用array_split即可。但需要注意的是,分割結果是一個列表,而不是矩陣。請務必注意列表和矩陣的區別:列表是Python的基本數據類型,而矩陣是NumPy中的數據類型。如果弄混了這一點,后面的程序將會非常難以理解(我就弄混了,所以糾結了很久orz)。接下來,很關鍵的一點是如何按照5折交叉驗證的要求組合訓練集。
    X_train_cv = np.vstack(X_train_folds[:i] + X_train_folds[i+1:])這句話是容易產生困擾的。vstack用于在0號軸上連接多個矩陣(請按照前面介紹的規則理解這句話。連接后,0號軸的大小將發生變化,而其它軸的大小不變),函數的參數應當為待連接的矩陣組成的元組(tuple)。而在這行代碼中,并沒有傳入元組,而是傳入了兩個列表相加的結果。首先,這里是列表相加而不是矩陣相加,Python的加號運算符用于列表時會直接把兩個列表連接起來。因此相加的結果是一個長度為4的列表,列表中每個元素都是1000×3072的矩陣。將列表傳入vstack后,會自動調用元組的構造函數tuple(list)將其轉換為元組。之后,在0號軸上連接這4個矩陣,得到一個4000×3072的矩陣。相同的原理,使用hstack組合訓練集標簽,這次是在1號軸上連接矩陣。這又是一個很容易出錯的地方,因為vstackhstack會認為輸入矩陣的維度至少為2,比如,代碼中的y_train其實是一維矩陣,按理說它應該在0號軸上連接。但是這兩個函數會把它當做二維矩陣,認為它是1×1000的矩陣,因此必須在1號軸上連接才能得到1×4000的矩陣。
    上面這一段解釋建議多看幾遍,就會對理解代碼有所幫助。

     num_folds = 5
     k_choices = [1, 3, 5, 8, 10, 12, 15, 20, 50, 100]
    
     X_train_folds = []
     y_train_folds = []
     ################################################################################
     # TODO:                                                                        #
     # Split up the training data into folds. After splitting, X_train_folds and    #
     # y_train_folds should each be lists of length num_folds, where                #
     # y_train_folds[i] is the label vector for the points in X_train_folds[i].     #
     # Hint: Look up the numpy array_split function.                                #
     ################################################################################
     X_train_folds = np.array_split(X_train, num_folds)
     y_train_folds = np.array_split(y_train, num_folds)
     ################################################################################
     #                                 END OF YOUR CODE                             #
     ################################################################################
    
     # A dictionary holding the accuracies for different values of k that we find
     # when running cross-validation. After running cross-validation,
     # k_to_accuracies[k] should be a list of length num_folds giving the different
     # accuracy values that we found when using that value of k.
     k_to_accuracies = {}
    
     ################################################################################
     # TODO:                                                                        #
     # Perform k-fold cross validation to find the best value of k. For each        #
     # possible value of k, run the k-nearest-neighbor algorithm num_folds times,   #
     # where in each case you use all but one of the folds as training data and the #
     # last fold as a validation set. Store the accuracies for all fold and all     #
     # values of k in the k_to_accuracies dictionary.                               #
     ################################################################################
     for k in k_choices:
         k_to_accuracies[k] = []
         for i in range(num_folds):
             X_train_cv = np.vstack(X_train_folds[:i] + X_train_folds[i+1:])
             y_train_cv = np.hstack(y_train_folds[:i] + y_train_folds[i+1:])
             X_test_cv = X_train_folds[i]
             y_test_cv = y_train_folds[i]
    
             classifier = KNearestNeighbor()
             classifier.train(X_train_cv, y_train_cv)
             dists = classifier.compute_distances_one_loop(X_test_cv)
             y_test_pred = classifier.predict_labels(dists, k)
             num_correct = np.sum(y_test_pred == y_test_cv)
             accuracy = float(num_correct) / X_test_cv.shape[0]
             k_to_accuracies[k].append(accuracy)
    
     ################################################################################
     #                                 END OF YOUR CODE                             #
     ################################################################################
    
     # Print out the computed accuracies
     for k in sorted(k_to_accuracies):
         for accuracy in k_to_accuracies[k]:
             print 'k = %d, accuracy = %f' % (k, accuracy)
    

交叉驗證的結果如下圖所示,總體趨勢是先上升再下降,在k=10附近準確度達到最大值。


四、總結

由于是第一次使用NumPy做矩陣運算,整個過程都感到非常吃力,不太適應向量化計算的寫法。但同時也強烈感受到Python的強大,語法相當簡潔,相信熟練了之后編碼效率會很高。

不過,在我電腦上,向量化計算的用時卻比使用for循環長了近一倍,而且消耗的內存也大了許多,導致數據量大的時候出現Memory Error。不知道這是什么問題,希望有經驗的同學指點迷津。

Jupyter Notebook也是個挺好用的工具,不過目前還沒發現如何單步調試代碼。

最后,包含答案的完整代碼可以從這里下載:
https://github.com/jingedawang/CS231n-Assignments

五、參考資料

斯坦福CS231n課程作業# 1簡介 知乎專欄-智能單元
CS231n課程筆記翻譯:圖像分類筆記(上) 知乎專欄-智能單元
CS231n課程筆記翻譯:圖像分類筆記(下) 知乎專欄-智能單元
CS231n課程筆記翻譯:Python Numpy教程 知乎專欄-智能單元
cs231n課程作業assignment1(KNN) 躺著中槍ing
NumPy v1.12 Manual SciPy.org

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言: 以斯坦福cs231n課程的python編程任務為主線,展開對該課程主要內容的理解和部分數學推導。該課程相關...
    卑鄙的我_閱讀 3,431評論 0 2
  • 前言: 以斯坦福cs231n課程的python編程任務為主線,展開對該課程主要內容的理解和部分數學推導。該課程相關...
    卑鄙的我_閱讀 4,743評論 1 5
  • 我依舊是能想起去年的雪 不記得燈火下的時節 眼睛所能到達的地方 是手所觸摸不到的...
    與冰冰閱讀 268評論 0 2
  • “我們處在世界體內,作為食物存在著。我們的能量維持著世界的運轉,一旦這微薄的能量被吸收完畢,我們就會被排出,墮入到...
    劉較瘦閱讀 193評論 0 0