一、前言
CS231n是斯坦福大學開設的一門深度學習與計算機視覺課程,是目前公認的該領域內最好的公開課。目前,該課程的全部資料已經被翻譯為中文,非常適合自學。該課程和相關資料的地址如下:
- 在線視頻:網易云課堂
- 課程講義和資料:知乎專欄-智能單元
- 官方網站:http://cs231n.github.io/
課程不光有精彩的講解,還提供了非常精致的課后作業。唯一的遺憾是作業沒有公布標準答案。因此我把自己的答案發出來與大家交流。
二、編程環境
官方建議使用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是比較直觀的做法,兩張圖片相減平方再求和。方案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
-
一層循環計算距離
直接對整個訓練集圖片操作,此時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
-
無循環計算距離
這一步倒是很有難度。題目中給出了提示——使用乘法和兩個廣播求和,可惜我并沒想明白怎么用。方案一是我的思路,完全沿襲前面的做法,充分利用廣播使兩個矩陣擴展到相同的維度。具體來說,原本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
-
分類預測
這里用到了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
-
交叉驗證
這部分代碼比較復雜。首先是把訓練集分為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號軸上連接矩陣。這又是一個很容易出錯的地方,因為vstack
和hstack
會認為輸入矩陣的維度至少為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