说起来自己最近在跟Andrew Ng的机器学习课程,本来是看斯坦福大学的公开课,但是那门课偏理论而且没有字幕,有点难懂,最近发现Coursera上有他的另一门公开课,偏向工程实践,就转而看这门课了。目前主要讲了Gradien Descent算法、线性回归问题和逻辑回归问题、还有神经网络。大致上了解了一些算法,还有神经网络的结构和原理。

说起来手上拿个锤子看什么都是钉子,我也各种在思考怎么应用学的东西做一些事,而不单单只是在octave里画线拟合散点图或者画线划分散点图。Andrew Ng在一节课上给我们展示了一段由Yann LeCun做的用神经网络来识别手写数字的应用,感觉很有意思,就起了这个念头。然后就想到了我们学校教务网的验证码,之前有些了解,有想过写一个爬虫自动爬成绩什么的,但是懒得(或者说不会?)识别验证码,就没动手,刚好趁这个机会可以练练手。

选择教务网验证码的原因很多,第一个就是简单,我们教务网的验证码非常简单,5位纯数字,位置固定,有一些颜色变化和噪点,噪点多但是干扰都很小。其实我以前就想过一个简单的识别思路,把数字裁剪下来,黑白处理,根据某个亮度阈值做成一个[0,1]的数组,通过与不同的数字样本对比方差,取方差最接近的数字。识别率应该就非常高了。

先不论怎么解决这个问题,今天我开始收集数据,用python爬虫每一秒爬一次验证码图片链接,收集了1000张图片,切割之后就有了5000个样本。分析文件头之后,知道了图片是gif格式,然后用python的PIL库,打开裁剪之后进行黑白处理,只留下亮度值,这样可以显著减小样本的特征数。做好之后我又开始自己手工识别数字,记录在一个文本里。(看得眼睛都花了才看了1600多个。。。)按照课程,我把样本按3:1:1分成训练集,交叉验证集和测试集。开始思考神经网络应该怎么搭建。

裁剪完的图片尺寸[8*12],那意味着我们有96个feature,加上bias,有97个输入。而输出有[0-9]十种可能,如果要考虑不确定的输入,那么也就是11种。(做的过程中我发现,教务网的验证码里没有9),而隐藏层数目则需要考虑一下,这个问题不难,所以不需要层数多深,1层应该就够了,但是应该有多少个神经元呢。(反正这个时候我是真的在认真考虑这个事情的。。。)

下午突然很想写日志,又很想把前两天做Gradient Descent时做的推导记录到日志里。于是花了很多时间弄日志,改主题、配置数学公式引擎、学LaTeX、给域名配SSL、写完了一篇推导。已经很晚了,想着还是再去看看课程视频。

突然一个念头冒出来,这个验证码识别本质上还是一个分类问题,是一个Multi-Class Classification Problem,那我为什么不先用逻辑回归试试呢。(神经网络还没有自己实现过,但是逻辑回归还是很好做的。)但是我手工识别还没有识别完,我想为了试试效果,先用100组数据训练试试。按照课程,这种Mulit-Class Classification问题采用一对多的思路来做,即取某种分类为1,其他分类为0,对其他分类都这样处理,可以得到N个分类器,从而解决这个问题。作为试水,我只训练了识别0的分类器。由于逻辑回归的cost function我事先已经实现了。所以只需要数据就好了,将图片转为矩阵,在octave里有个方便的imread函数可以实现,读取之后转化成向量再转为浮点数就可以了。最终我有了一个[97x100]的输入矩阵,一个[1x100]的输出矩阵和一个[97x1]的系数矩阵。

options = optimset('GridObj','on','MaxIter',100);
[optTheta, funVal, info] = fminunc(@(t)cost_lr(t, input, output), t, options);

这里的fminunc是octave里一个求函数局部最小值的函数,其实我可以用自己实现的Gradient Descent,但是这个算法效率高得多。而且由于样本少,所以结果不到3s就出来了。

既然训练好了分类器,那就用交叉验证集验证一下效果吧,试着把里面一个样本带进去,得到了正确的结果,好像还可以。于是我写了一个简单的脚本,用分类器处理了全部交叉验证集,有1000个数据,结果存在了一个向量里。然后手工算正确率。

出乎意料的是,正确率100%,教务网的验证码弱我是知道的,但是没想到这么弱,这也太弱了吧。(这件事反映出教务网的验证码强度实在是太弱了,真的有必要加强一下,毕竟教务网对于学生老师来说都是重要网站,不容有失啊。)

暂且不管这个,既然结果不错,我们可以继续做下去,我手上有1000多个已经识别好了的图片,干脆全用上,于是又忙活了一阵,训练出9组参数(之前说了教务网的验证码里没有9),这回的结果很差了。

用训练好的模型处理交叉验证集,发现有几个数字完全不能识别,具体来说是3,5,6,8。依直觉看,有可能是因为两两之间区别不大,所以识别不出来。反复排查一阵之后发现𝐽(𝜃)没有收敛到一个理想的数值。但是逻辑回归的cost function应该是一个凹函数啊,应该肯定可以收敛到全局最小的。

中间还有个小插曲,某次我采用了随机初始化参数的方式,训练之后得到了一个非常好的结果,在交叉验证集和测试集上正确率都是100%。(毕竟这个问题太简单了)于是我迅速用python实现了验证码识别XD。

由于无路可走,Ng教的一些东西也记不太清了,不太知道怎么应对。结果我想不如我自己用Gradient Descent来求参数。过程很简单,大部分东西都是现成的,只要稍微改改就好了。结果出来之后也很好,只是在交叉验证集里总是有一个样本会错,于是我把这个样本丢到训练集里又跑了一次,安慰自己这样就好了(总感觉这样做就像是鸵鸟把头钻到地里就以为自己脱离了危险一样,虽然这个是谣言吧,逃