在这节,我将用卷积神经网络(简称: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函数,找出几率最大的类就是识别结果了。
测试图片:
测试识别结果: