在這節,我將用卷積神經網絡(簡稱:CNN)破解新浪微博手機端的驗證碼(http://login.weibo.cn/login/),驗證碼以下。
git
本節的代碼能夠在https://github.com/nladuo/captcha-break/tree/master/weibo.cn找到。
github
關於神經網絡的原理很難在一節講清楚。在這裏,只須要把神經網絡當成一個黑匣子,輸入是一個圖片,輸出一個label,也就是類別。算法
本節使用的神經網絡是國外學者Yann LeCun的LeNet5,該神經網絡以32x32的圖片做爲輸入,對於字符的變形、旋轉、干擾線等扭曲均可以很好的識別,能夠實現如下效果。
網絡
更多的效果能夠在http://yann.lecun.com/exdb/lenet/上查看,具體原理能夠查看Yann LeCun的論文。函數
字符下載和上節差很少,這裏須要注意的是新浪微博的驗證碼下載下來是gif格式的,opencv不支持讀取gif的讀取,須要用PIL把驗證碼轉換成png格式。
學習
另外,新浪微博的驗證碼明顯比CSDN下載的驗證碼要複雜得多,因此須要大量的樣本,至少要下載上千個驗證碼。測試
新浪微博的驗證碼須要進行去除椒鹽噪聲、去除干擾線、二值化後,才能很好的進行垂直投影分割,我算法寫的不是很好,就不在這裏展開了,代碼能夠在spliter中找到。LeNet5的輸入是32x32像素,因此爲了避免對神經網絡進行大量修改,也須要將每一個字母都方法32*32的模板中,分割後以下:
spa
分割好以後,須要開始大量的人工操做了,通過了幾個小時的努力,成功完成了5000多樣本的分類,結果放在了trainer/training_set中。
code
這裏每一個文件夾都是一個分類,共有14個分類(除了ERROR),點進文件夾後能夠看到每一個文件夾內都有300多張圖片。
圖片
我這裏使用的神經網絡庫是tiny-cnn(如今已更名叫tiny-dnn)。
訓練相關的代碼都在trainer/main.cpp中,首先看一下神經網絡的構造函數。
void construct_net(network<sequential>& nn) { // connection table [Y.Lecun, 1998 Table.1] #define O true #define X false static const bool tbl[] = { O, X, X, X, O, O, O, X, X, O, O, O, O, X, O, O, O, O, X, X, X, O, O, O, X, X, O, O, O, O, X, O, O, O, O, X, X, X, O, O, O, X, X, O, X, O, O, O, X, O, O, O, X, X, O, O, O, O, X, X, O, X, O, O, X, X, O, O, O, X, X, O, O, O, O, X, O, O, X, O, X, X, X, O, O, O, X, X, O, O, O, O, X, O, O, O }; #undef O #undef X // construct nets nn << convolutional_layer<tan_h>(32, 32, 5, 1, 6) // C1, 1@32x32-in, 6@28x28-out << average_pooling_layer<tan_h>(28, 28, 6, 2) // S2, 6@28x28-in, 6@14x14-out << convolutional_layer<tan_h>(14, 14, 5, 6, 16, connection_table(tbl, 6, 16)) // C3, 6@14x14-in, 16@10x10-in << average_pooling_layer<tan_h>(10, 10, 16, 2) // S4, 16@10x10-in, 16@5x5-out << convolutional_layer<tan_h>(5, 5, 5, 16, 120) // C5, 16@5x5-in, 120@1x1-out << fully_connected_layer<tan_h>(120, 14); // F6, 120-in, 14-out }
這裏能夠看到有六層神經網絡,C一、S二、C三、S四、C五、F6。其實不用仔細的瞭解神經網絡的構造,只須要把它想象成一個黑匣子,黑匣子的輸入就是C1層的輸入(C1, 1@32x32-in),黑匣子的輸出就是F6層(F6,14-out)。32x32對應着圖片的大小,14對應着類的個數。好比說要訓練MINST數據集(一個手寫字符的數據集)的話,須要把fully_connected_layer<tan_h>(120, 14)改爲fully_connected_layer<tan_h>(120, 10),由於MINST中有十類字符(0-9十種數字)。
(注:這裏只能修改F6層的參數而不能修改C1層的參數,修改C1參數會影響到其餘層的輸入。)
接下來,經過boost庫加載數據集,其中五分之四的樣本做爲訓練,還有五分之一的做爲測試訓練的正確性。
std::string label_strs[14] = { "3", "C", "D", "E", "F", "H", "J", "K", "L", "M", "N", "W", "X", "Y" }; void load_dataset(std::vector<label_t> &train_labels, std::vector<vec_t> &train_images, std::vector<label_t> &test_labels, std::vector<vec_t> &test_images) { for (int i = 0; i < 14; ++i){ std::vector<std::string> images; fs::directory_iterator end_iter; fs::path path("./training_set/"+label_strs[i]); for (fs::directory_iterator iter(path); iter != end_iter; ++iter){ if (fs::extension(*iter)==".png"){ images.push_back(iter->path().string()); } } //train_set.size() : test_set.size() = 4:1 int flag = 0; std::vector<std::string>::iterator itr = images.begin(); for (;itr != images.end(); ++itr){ vec_t data; convert_image(*itr, -1.0, 1.0, 32, 32, data); if (flag <= 4){ train_labels.push_back(i); train_images.push_back(data); }else{ test_labels.push_back(i); test_images.push_back(data); flag = 0; } flag++; } } }
卷積神經網絡使用的是隨機梯度降低進行訓練,涉及一些數學知識,這裏就不展開了。
這裏只要把它理解爲:神經網絡會本身不斷的對數據集進行學習(不斷的迭代,每次迭代都會對識別率有所改進)。學習的過程會有一個學習速率optimizer.alpha,這裏選擇的是默認的;還有每次學習多少個數據(minibatch_size),這裏設置每次對100個數據進行學習;還有一個學習的時間(num_epochs),這裏學習了50次以後,學習效果就沒有了。也就是識別率達到了峯值。
int minibatch_size = 100; //每批量的數量 int num_epochs = 50; //迭代次數 // optimizer.alpha *= std::sqrt(minibatch_size); 使用默認的學習速率
神經網絡的訓練以後,須要保存神經網絡的權重,把權重輸出到"weibo.cn-nn-weights"中。
// save networks std::ofstream ofs("weibo.cn-nn-weights"); ofs << nn;
運行trainer後,能夠看到開始加載數據,而且進行一次一次的迭代,每一次迭代都會根據測試數據來進行驗證,顯示正確識別的字符數目。
從上面能夠看到,一共有3934個訓練樣本和972個測試樣本,正確識別的字符數目隨着迭代次數不斷的增長,從72->120->142->223....,識別率不斷增長。
訓練到最後(第四十幾回迭代),能夠看到數據已經差很少飽和了,維持在860、870左右,也就是單個字符有89%的識別率,單個驗證碼有0.89^4=0.64左右的識別率。(若是訓練了不少次後,發現識別率尚未飽和,能夠增大迭代次數num_epochs或者增大學習速率optimizer.alpha)
最後,能夠經過訓練好的「weibo.cn-nn-weights」來進行識別,把trainer/weibo.cn-nn-weights放到recognizer文件夾下。
接下來看看神經網絡是如何進行識別的,在recognizer/main.cpp中查看recognize函數。
int recognize(const std::string& dictionary, cv::Mat &img) { network<sequential> nn; construct_net(nn); // load nets ifstream ifs(dictionary.c_str()); ifs >> nn; // convert cvMat to vec_t vec_t data; convert_mat(img, -1.0, 1.0, 32, 32, data); // recognize auto res = nn.predict(data); vector<pair<double, int> > scores; for (int i = 0; i < 14; i++) scores.emplace_back(rescale<tan_h>(res[i]), i); // sort and get the result sort(scores.begin(), scores.end(), greater<pair<double, int>>()); return scores[0].second; }
在神經網絡的最後一層中輸出的是一個14維的向量,分別對應着每一個類的機率,因此經過sort函數,找出機率最大的類就是識別結果了。
測試圖片:
測試識別結果: