算法實戰——Kaggle競賽實戰
一、介紹
-
Kaggle 地址:https://www.kaggle.com/tefuirnever
-
GitHub 代碼地址:https://github.com/TeFuirnever/Kaggle-Digit-Recognizer
-
【Digit Recognizer】比賽頁面:https://www.kaggle.com/c/digit-recognizer/overview
MNIST
是計算機視覺領域的 hello world
數據集。自從1999年發佈以來,這個經典的手寫數字識別數據集就成爲分類算法的基礎,即使新的機器學習技術在不停地出現,但 MNIST
仍然是研究人員和學習者的可靠資源。
這裏選擇用 keras API(Tensorflow backend
)來構建它,這會使得整個過程非常直觀且便於理解,具體過程如下:
導入需要的庫
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import seaborn as sns
%matplotlib inline
np.random.seed(2)
from sklearn.model_selection import train_test_splitfrom sklearn.metrics import confusion_matrix
import itertools
# 轉換爲獨熱編碼
from keras.utils.np_utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPool2D, BatchNormalization
from keras.optimizers import RMSprop
from keras.preprocessing.image import ImageDataGenerator
from keras.callbacks import ReduceLROnPlateau
# 設置顯示樣式的參數
# http://seaborn.pydata.org/generated/seaborn.set.html
sns.set(style='white', context='notebook', palette='deep')
如果你強迫症犯了的話,使用如下代碼可以去除 warning
,詳細的看這個博客——warnings.filterwarnings(“ignore”)代碼解析。
import warnings
warnings.filterwarnings('ignore')
二、數據準備
2.1、數據加載
首先,準備我們所需要的數據(手寫數字識別圖像);
# 加載數據
train = pd.read_csv("./train.csv")
test = pd.read_csv("./test.csv")
數據文件 train.csv
和 test.csv
包含從零到九的手繪數字的灰度圖像。每個圖像的高度爲 28
像素,寬度爲 28
像素,總計 28 * 28 = 784
像素。
每個像素都有一個與之關聯的像素值來表示該像素的明暗程度,此像素值是介於0和255之間的整數(包括0和255),數字越高表示像素越暗。
訓練集
以訓練數據集(train.csv
)爲例,共有785列,第一列稱爲 label
,是用戶繪製的數字;其餘列包含關聯圖像的像素值。
訓練集中的每個像素列都有一個類似 pixel x
的名稱,其中 x
是0到783之間的整數(包括0和783)。爲了在圖像上定位這個像素,假設分解了 x
作爲 x=i*28+j
,其中 i
和 j
是0到27之間的整數(包括0和27),然後 Pixel x
位於 28×28
矩陣的行 i
和列 j
上(索引爲從零開始的)。例如,pixel 31
表示左起第四列中的像素,以及頂部的第二行,如下圖所示:
從視覺上看,如果省略了 pixel
前綴,那麼這些像素就構成了這樣的圖像(即 (2 - 1) * 28 + (4 - 1) = 31
)。
測試機
測試數據集(test.csv
)與訓練集相同,只是它不包含 label
列。
提交文件
提交文件應採用以下格式:對於測試集中28000個圖像中的每一個,輸出包含 imageid
和預測數字的單行。
例如,如果預測第一個圖像爲3,第二個圖像爲7,第三個圖像爲8,則提交文件將如下所示:
指標
評價標準是 分類準確率,即正確分類的測試圖像的比例。例如,分類精度爲0.97表示已正確分類了除3%以外的所有圖像。
2.2、數據可視化
# 'label'
Y_train = train["label"]
print(Y_train.shape)
# 刪除 'label' 列
X_train = train.drop(labels = ["label"],axis = 1)
print(X_train.shape)
# 釋放一些空間
del train
# 使用條形圖顯示每個分類數據集合中的觀測值
# https://seaborn.pydata.org/generated/seaborn.countplot.html?highlight=countplot
g = sns.countplot(Y_train)
# 對訓練集中的元素計數
# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.value_counts.html
Y_train.value_counts()
這10位數有類似的計數,都是4000左右。
2.3、數據清洗
# 檢查數據 X_train.isnull().any().describe()
X_train.isnull().any().describe()
將信息進行彙總:
count
爲總統計數;unique
爲種類(由於該數據中沒有空值,全爲False,故只有1類);top
爲最多的種類;freq
爲最多的種類出現的頻次。
test.isnull().any().describe()
檢查是否有損壞的圖像(內部缺少值),可以看到訓練數據集和測試數據集中沒有丟失的值,這樣就可以放心地繼續處理了。2.4、歸一化
又稱標準化/規範化/正則化。
# 對數據進行歸一化,到[0, 1]範圍內,減小光照的影響,並可加速CNN收斂速度 X_train = X_train / 255.0 test = test / 255.0
2.5、Reshape
# Reshape三維圖像(height = 28px, width = 28px , canal = 1) X_train = X_train.values.reshape(-1,28,28,1) test = test.values.reshape(-1,28,28,
可以看到第一個維度就是數據集中圖像的個數;第二和第三個維度是,訓練和測試圖像(28px
*28px
)已作爲一個784個值的一維向量儲存到pandas.Dataframe
中;Keras
要求最後一個維度代表通道數,mnist
圖像是灰度圖,只有一個通道,對於rgb
圖像,有3個通道。2.6、標籤編碼
# 將標籤編碼爲一個獨熱向量 (例如: 2 -> [0,0,1,0,0,0,0,0,0,0]) Y_train = to_categorical(Y_train, num_classes =
2.7、分割交叉驗證集
# 設置隨機種子
random_seed = 2
# 分割出訓練集和驗證集
X_train, X_val, Y_train, Y_val = train_test_split(X_train, Y_train, test_size = 0.1, random_state=random_seed)
函數參數說明:
print(X_train.shape)
這裏選擇將訓練集分成兩部分:一小部分(10%)成爲評估模型的驗證集,其餘(90%)成爲評估模型的訓練集,用於訓練模型。
因爲有42000張平衡標籤的訓練圖像,所以隨機分割的訓練集不會導致一些標籤在驗證集中被過度表示。如果針對一些不平衡的數據集,一個簡單的隨機分割可能會導致在驗證期間出現不準確的評估。爲了避免這種情況,可以在 train_test_split
函數中使用stratify=true
選項(僅適用於 >=0.17sklearn
版本)。
通過可視化圖像和查看標籤,可以更好地理解其中一個示例。
# 一些例子
g = plt.imshow(X_train[0][:,:,0])
g = plt.imshow(X_train[2][:,:,0])
g = plt.imshow(X_train[2000][:,:,0])
三、卷積神經網絡CNN
3.1、定義網絡模型
這裏使用了 Keras Sequential API
,從輸入開始,每次只需添加一個層。
-
卷積(
conv2d
)層就像一組可學習的過濾器:前三個conv2d
層設置32個過濾器,後三個層設置64個過濾器。 -
池化(
maxpool2d
)層是一個下采樣濾波器:它着眼於2個相鄰像素,並選擇最大值。這些都是用來減少計算成本,並在一定程度上也減少了過擬合。 -
歸一化層是一種正則化方法,可以加快收斂速度,控制並減少過擬合,同時還允許網絡使用較大的學習率。
-
Dropout
是一種正則化方法,其中某些層的部分節點被隨機忽略(將其wieghts
設置爲零)。這將隨機丟棄網絡的一個屬性,並強制網絡以分佈式方式學習特性。該方法還提高了泛化能力,減少了過擬合。
解決過擬合的方法可以看這個博客——深度學習100問之神經網絡中解決過擬合的幾種方法
-
relu
是線性整流函數,又稱修正線性單元,也就是俗稱的激活函數,公式是max(0,x)
。relu
的主要作用就是向網絡中添加非線性,故也稱爲非線性激活函數。 -
Flatten
層用於將最終特徵映射轉換爲一個一維向量,展開之後可以在某些卷積/maxpool
層之後使用全連接層,它結合了以前卷積層提取的所有局部特徵。 -
全連接(
dense
)層是用於實現分類,即人工神經網絡分類器,在最後一層(Dense(10, activation='softmax')
),網絡輸出每個類別的概率分佈。
# 設置CNN模型
model = Sequential()
model.add(Conv2D(filters = 32, kernel_size = (5,5),padding = 'Same',
activation ='relu', input_shape = (28,28,1)))
model.add(BatchNormalization())
model.add(Conv2D(filters = 32, kernel_size = (5,5),padding = 'Same',
activation ='relu'))
model.add(BatchNormalization())
model.add(MaxPool2D(pool_size=(2,2)))
# model.add(Dropout(0.25))
model.add(Conv2D(filters = 64, kernel_size = (3,3),padding = 'Same',
activation ='relu'))
model.add(BatchNormalization())
model.add(Conv2D(filters = 64, kernel_size = (3,3),padding = 'Same',
activation ='relu'))
model.add(BatchNormalization())
model.add(MaxPool2D(pool_size=(2,2), strides=(2,2)))
# model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(256, activation = "relu"))
model.add(Dropout(0.5))
model.add(Dense(10, activation = "softmax"))
# 輸出模型各層的參數狀況
model.summary()
3.2、設置優化器和退火函數
一旦網絡模型構建成功,我們就需要有一個評分函數,一個損失函數和一個優化算法。
-
損失函數用來衡量模型在帶有已知標籤的圖像數據集上的性能有多差,它是目標標籤和預測標籤之間的錯誤率。使用最多的是交叉熵損失函數,即
categorical_crossentropy loss
。 -
優化器是最重要的功能,它將迭代地改進參數(
filters kernel values, weights and bias of neurons ...
),以最小化損失函數。- 可以選擇
rmsprop
,它是一個非常有效的優化器,以一種非常簡單的方式調整adagrad
方法,試圖降低其攻擊性強、單調下降的學習率。 - 還可以使用
adam
; - 也可以使用
sgd
優化器,但它比rmsprop
慢。
- 可以選擇
-
度量函數
accuracy
用於評估模型的性能,不過僅用於評估。
# 用adam優化器和交叉熵損失進行編譯
model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])
# 用sgd優化器
# model.compile(optimizer=“sgd”, loss=“categorical_crossentropy”, metrics=[“accuracy”])
# 定義優化器
# optimizer = RMSprop(lr=0.001, rho=0.9, epsilon=1e-08, decay=0.0)
# 編譯模型
# model.compile(optimizer = optimizer , loss = “categorical_crossentropy”, metrics=[“accuracy”])
爲了使優化器更快地收斂,並且最接近全局最小損失函數, 這裏使用了一種學習率(lr
)的退火方法。lr
是學習率,它越高,步長越大,收斂速度越快。然而,由於 lr
較高,採樣非常差,優化器可能會陷入局部極小值。所以可以在訓練過程中降低學習率,以有效地達到損失函數的全局最小。爲了保持計算速度快、lr
值高的優點,根據需要(在精度沒有提高的情況下)每 x
步動態地減少 lr
值。
使用 keras.callbacks
中的 ReduceLROnPlateau
函數,如果在3個階段之後精度沒有提高,將 lr
減少一半。
# 設置一個學習率衰減
learning_rate_reduction = ReduceLROnPlateau(monitor='val_acc',
patience=3,
verbose=1,
factor=0.5,
min_lr=0.00001)
# 訓練輪數,暫時設置爲30,可以自己嘗試調整
epochs = 30
# 批大小
batch_size = 86
3.3、數據增強
又稱數據擴充/數據增廣。
爲了避免過擬合問題,需要對手寫數字數據集進行人工擴充,它可以讓你現有的數據集變得更大。這個想法最初是來源於用小的轉換來改變訓練數據,以重現某人在寫一個數字時發生的變化,尤其適用於數據量較小的情況。以改變數組表示的方式改變訓練數據,同時保持標籤不變的方法稱爲數據增強技術。一些常用增強是灰度、水平翻轉、垂直翻轉、隨機裁剪、顏色抖動、平移、旋轉還有縮放等等。
通過數據增強可以輕鬆地將訓練集的數量增加一倍或多倍,從而可以創建一個非常健壯的模型,因此這個改進很重要!!!
# 增加數據以防止過擬合
datagen = ImageDataGenerator(
featurewise_center=False, # 在數據集上將輸入平均值設置爲0
samplewise_center=False, # 將每個樣本的平均值設置爲0
featurewise_std_normalization=False, # 將輸入除以數據集的std
samplewise_std_normalization=False, # 將每個輸入除以它的std
zca_whitening=False, # 使用ZCA白化
rotation_range=10, # 在範圍內隨機旋轉圖像(0到180度)
zoom_range = 0.1, # 隨機縮放圖像
width_shift_range=0.1, # 水平隨機移動圖像(總寬度的一部分)
height_shift_range=0.1, # 垂直隨機移動圖像(總高度的一部分)
horizontal_flip=False, # 隨機翻轉圖像
vertical_flip=False) # 隨機翻轉圖像
datagen.fit(X_train)
爲了增加數據選擇了:
- 訓練圖像隨機旋轉10度;
- 隨機縮放10%一些訓練圖像;
- 將圖像水平移動10%的寬度;
- 將圖像垂直移動10%的高度;
- 沒有應用垂直翻轉或水平翻轉,因爲它可能導致錯誤分類對稱數字,如6和9。
3.4、擬合數據
一旦模型準備好了,就可以擬合訓練數據集。
# 擬合模型
history = model.fit_generator(datagen.flow(X_train,Y_train, batch_size=batch_size),
epochs = epochs, validation_data = (X_val,Y_val),
verbose = 2, steps_per_epoch=X_train.shape[0] // batch_size,
callbacks=[learning_rate_reduction])
這裏可以看到,CNN
在所有數字上都表現得非常好,考慮到驗證集的大小(4200張圖像),可以說錯誤是非常少的了。然而,也有一些麻煩,比如真實爲4的數有好多被誤分類爲9。
來看看這些重要的錯誤,爲了達到這個目的,需要得到結果中實際值和預測值的概率之間的差異。
# 顯示一些錯誤結果
# 錯誤是預測標籤和真實標籤之間的區別
errors = (Y_pred_classes - Y_true != 0)
Y_pred_classes_errors = Y_pred_classes[errors]
Y_pred_errors = Y_pred[errors]
Y_true_errors = Y_true[errors]
X_val_errors = X_val[errors]
def display_errors(errors_index,img_errors,pred_errors, obs_errors):
"""
此函數顯示6個圖像及其預測和實際標籤
"""
n = 0
nrows = 2
ncols = 3
fig, ax = plt.subplots(nrows,ncols,sharex=True,sharey=True)
for row in range(nrows):
for col in range(ncols):
error = errors_index[n]
ax[row,col].imshow((img_errors[error]).reshape((28,28)))
ax[row,col].set_title("Predicted label :{}\nTrue label :{}".format(pred_errors[error],obs_errors[error]))
n += 1
# 錯誤預測數的概率
Y_pred_errors_prob = np.max(Y_pred_errors,axis = 1)
# 誤差集中真值的預測概率
true_prob_errors = np.diagonal(np.take(Y_pred_errors, Y_true_errors, axis=1))
# 預測標籤概率與真實標籤概率之差
delta_pred_true_errors = Y_pred_errors_prob - true_prob_errors
# 對預測標籤概率與真實標籤概率之差的列表進行排序
sorted_dela_errors = np.argsort(delta_pred_true_errors)
# Top 6錯誤
most_important_errors = sorted_dela_errors[-6:]
# 展示Top 6錯誤
display_errors(most_important_errors, X_val_errors, Y_pred_classes_errors, Y_true_errors)
最重要的錯誤也是最棘手的,對這六種情況,其中一些錯誤可能是由人類造成的,特別是對於一個非常接近 4 的 9,最後的9也很容易讓人誤解,對我來說似乎是0。
4.3、預測和提交
# 預測結果
results = model.predict(test)
# 選擇最大概率的整數
results = np.argmax(results,axis = 1)
results = pd.Series(results,name="Label")
submission = pd.concat([pd.Series(range(1,28001),name = "ImageId"),results],axis = 1) # 轉換成CSV格式,不保留索引 submission.to_csv("cnn_mnist_datagen.csv",index=False)
隨便跑了一次,結果還一般吧。
五、MNIST上的最佳模型
CNN的架構有很多選擇,那麼如何選擇最好的一個呢? 下面將會通過實驗來進行測試。
下面是Kaggle排行榜得分的柱狀圖,每個欄的得分範圍爲0.1%:
- 92%:多項邏輯迴歸又名
softmax
迴歸是簡單的嘗試,得分爲 92%; - 97%:非線性方法的得分爲 97%,包括
kNN
,隨機森林等; - 98%:非線性內核或全連接神經網絡的
SVM
的得分爲 98%,如果你調一調參,也許能到 99%; - 99%:卷積神經網絡是圖像分類的冠軍,基本跑幾輪就得分 99%;
例如,
model.add(Conv2D(filters=32,kernel_size=5,activation='relu'))
,通過Keras
實現的,帶有Dropout
的簡單網絡784-32C5-500-10
在30輪後就可以達到了99%,如果添加一個池化層,它會在15輪內達到。
- 99.5%:一個設計好的CNN架構,然後添加了特殊功能,例如池化層,數據增強,
Dropout
,批歸一化,學習率衰減,高級優化器等等,那麼僅用20輪就可以突破 99.5% 的里程碑!比如我們上面實驗中使用的這個網絡結構; - 99.7%:要打破 99.7%,除了設計好的CNN架構,還要使用GPU,這樣就不需要永遠訓練了,因爲CPU跑的實在是太慢了!這種情況下,如果訓練10次並進行10次結果提交,其中之一可能會超過 99.7%,因爲每次訓練CNN時,都會得到不同的結果;
- 99.8%-99.9%:要獲得 99.8% 或更高的分數,需要進行一次非常幸運的訓練過程,或者需要使用
70,000
張圖像的完整原始 MNIST 數據集進行訓練,其中不公平地包含 Kaggle 的test.csv
圖像(這絕對是作弊),70,000
張圖像的完整原始 MNIST 數據集的 地址在這;
- 100.0%:除了必須使用
70,000
張圖像的完整原始MNIST數據集進行訓練之外,還需要一個很好的算法支持,比如一個得分爲 100% 的Kaggle內核就是使用作弊算法kNN
。
六、不應該被提交的結果
使用 KNN k=1
和 MNIST 70k
圖像,Accuracy=100%
。
這個 kernel
就是一個不該做的例子,提交的結果在 Kaggle 的排行榜上得分 100%
。
用 Kaggle 的 28000 張 test.csv
圖像對 MNIST 的 70000 張原始數據集進行了 kNN k=1
,以查看圖像是否相同,結果驗證了 Kaggle 未知的 test.csv
圖像完全包含在 MNIST 的原始數據集中,並且具有已知的標籤。因此,直接輸出相應的標籤,打包提交就可以實現得分 100% 了。。。。。。只說核心代碼了,具體的直接 GitHub 上傳了。
c1=0; c2=0; print("Classifying Kaggle's 'test.csv' using kNN k=1 and MNIST 70k images") for i in range(0,28000): # 循環Kaggle測試集 for j in range(0,70000): # 循環MNIST數據集 # 如果數據相同,那麼標籤相同 if np.absolute(Kaggle_test_image[i,] - MNIST_image[j,]).sum()==0: Kaggle_test_label[i] = MNIST_label[j] if i%1000==0: print(" %d images classified perfectly" % (i)) if j<60000: c1 += 1 else: c2 += 1 break if c1+c2==28000: print(" 28000 images classified perfectly") print("Kaggle's 28000 test images are fully contained within MNIST's 70000 dataset") print("%d images are in MNIST-train's 60k and %d are in MNIST-test's 10k" % (c1,c2))
# 輸出找到的標籤即可 results = pd.Series(Kaggle_test_label.reshape(28000,),name="Label") submission = pd.concat([pd.Series(range(1,28001),name = "ImageId"),results],axis = 1) submission.to_csv("Do_not_submit",index=False)
使用kNN k=1
,我們能 100% 準確地知道 Kaggle 的前六個測試圖像分別是數字2、0、9、0、3、7。同樣,接下來的27994張測試圖像也非常清楚。這樣的操作沒有任何意義,不要這樣做!!!
七、MNIST上的最佳CNN
https://www.kaggle.com/cdeotte/how-to-choose-cnn-architecture-mnist/notebook 中對【不同的卷積子空間對】、【特徵圖】、【全連接層】、【Dropout】、【歸一化】、【數據增強】等等進行了分別的實驗,發現了一下結構性能更高:
- 784(28 * 28) - [32C3-32C3-32C5S2](c=filter, s=stride) - [64C3-64C3-64C5S2] - 128 - 10
- 40% dropout,歸一化,數據增強
八、初代網絡 LeNet-5
可以看到各個層的特徵通過動畫的形式表現出來了,現在 CNN 正在變得可視化,希望未來能擺脫黑盒子的稱呼!!!
歡迎看一下這個高贊博客——大話卷積神經網絡CNN(乾貨滿滿)。
九、GitHub
全部的代碼和數據可以通過GitHub下載,地址是 https://github.com/TeFuirnever/Kaggle-Digit-Recognizer。
參考文章
- https://www.kaggle.com/kernels/svzip/notebook
- https://www.kaggle.com/cdeotte/how-to-choose-cnn-architecture-mnist/notebook
- https://www.kaggle.com/c/digit-recognizer/discussion/61480#latest-645703
- https://www.kaggle.com/cdeotte/mnist-perfect-100-using-knn/output#Accuracy=100%-using-kNN-k=1-and-MNIST-70k-images