用神经网络破解验证码

2020-02-17 15:43:11 浏览数 (1)

理解图像中的信息一直是数据挖掘领域的一个难题,直到最近几年才开始得到真正解决。图像检测和理解算法已相当成熟,几大厂商使用这些算法研制的监测系统已投入商用,用来处理实际问题。这些系统能够理解和识别视频画面中的人和物体。

从图像中抽取信息很难。图像包含大量原始数据,图像的标准编码单元——像素——提供的信息量很少。图像——特别是照片——可能存在一系列问题,比如模糊不清、离目标太近、光线很暗或很亮、比例是真、残缺、扭曲等,这会增加计算机系统抽取有用信息的难度。

本文介绍如何使用神经网络识别图像中的字母,从而自动识别验证码。验证码的设计初衷是便于人类理解,而不易被计算机识破。验证码的英文名叫做 CAPTCHA,它取自以下短语中几个单词的首字母“Completely Automated Public Turing test to tell Computers and Humans Apart”,意思是能够区别计算机和人类的全自动的公共图灵测试。很多网站都在注册、评论系统中使用验证码,以防止别人恶意注册虚假账号或发布垃圾评论。

人工神经网络

神经网络算法最初是根据人类大脑的工作机制设计的。然而,该领域近年所取得的进展主要得益于数学而不是生物学。神经网络由一系列相互连接的神经元组成。每个神经元都是一个简单的函数,接受一定输入,给出相应输出。

神经元可以使用任何标准函数来处理数据,比如线性函数,这些函数统称为激活函数(activation function)。一般来说,神经网络学习算法要能正常工作,激活函数应当是可导(derivable)和光滑的。常用的激活函数有逻辑斯谛函数,函数表达式如下(x 为神经元输入,k、L 通常为 1,这时函数达到最大值)。

每个神经元接收几个输入,根据这几个输入(对带权重的输入的加总),计算输出。这样的一个个神经元连接在一起组成了神经网络,对数据挖掘应用来说,它非常强大。这些神经元紧密连接,密切配合,能够通过学习得到一个模型,使得神经网络成为机器学习领域最强大的概念之一。

神经网络简介

用于数据挖掘应用的神经网络,神经元按照层级进行排列。第一层,也就是输入层,接收来自数据集的输入。第一层中的每个神经元对输入进行计算,把得到的结果传给第二层的神经元。这种叫做前向神经网络。本文把它简称为神经网络。此外,还有几种神经网络,适用于不同的应用。

神经网络中,上一层的输出作为下一层的,直到到达最后一层:输出层。输出结果表示的是神经网络分类器给出的分类结果。输入层和输出层之间的所有层被称为隐含层,因为在这些层中,其数据表现方式,常人难以理解。大多数神经网络至少有三层,而如今大多数应用所使用的神经网络层次比这多得多。

我们优先考虑使用全连接层,即上一层中每个神经元的输出都输入到下一层的所有神经元。实际构建神经网络时,就会发现,训练过程中,很多权重都会被设置为 0,有效减少边的数量。比起其他连接模式,全连接神经网络更简单,计算起来更快捷。

神经元激活函数通常使用逻辑斯谛函数,每层神经元之间为全连接,创建和训练神经网络还需要用到其他几个参数。创建过程,指定神经网络的规模需要用到两个参数:神经网络共有多少层,隐含层每层有多少个神经元(输入层和输出层神经元数量通常由数据集来定)。

训练过程中还会用到一个参数:神经元之间的边的权重。一个神经元连接到另外一个神经元,两者之间的边具有一定的权重,在计算输出时,用边的权重乘以信号的大小(signal,第一个神经元的输出)。如果边的权重为 0.8,神经元激活后,输出值为 1,那么下一个神经元从前面这个神经元得到的输入就是 0.8。如果第一个神经元没有激活,值为 0,那么输出到第二个神经元的值就是 0。

神经网络大小合适,且权重经过充分训练,它的分类结果才能精确。大小合适并不是越大越好,因为神经网络过大,训练时间会很长,更容易出现过拟合训练集的情况。

通常,开始使用随机选取的权重,训练过程中再逐步更新。

设置好第一个参数(网络的大小)再从训练集中训练得到边的权重参数后,就能构造分类器。然后,就可以用它进行分类。但是,首先需要准备训练集和测试集。

创建数据集

我们将扮演骇客这个角色,我们想编写破解网站验证码的程序,这样我们就有能力在别人网站上发布恶意广告。需要注意的是,我们要破解的验证码,难度比不上现在网上常用的;还有就是发表垃圾广告不太道德。

我们只使用长度为 4 个字母的英文单词作为验证码。

我们的目标是编写程序还原图像中的单词,步骤如下。

  1. 把大图像分成只包含一个字母的 4 张小图像。
  2. 为每个字母分类。
  3. 把字母重新组合为单词。
  4. 用词典修正单词识别错误。

我们的验证码破解算法做出了以下几个假设。首先,验证码中的单词是一个完整的、有效的英文单词,其长度为 4 个字母(实际上,生成和破解验证码,我们都使用同一个词典)。其次,单词全部字母均为大写形式,不使用符号、数字或空格。为了增加难度,在生成图像时对单词使用不同的错切(shear)变化效果。

绘制验证码

接下来,我们编写创建验证码的函数,目标是绘制一张含有单词的图像,对单词使用错切变化效果。绘制图像要用到 PIL 库,错切变化需要使用 scikit-image 库。scikit-image 库能够接收 PIL 库导出的 numpy 数组格式的图像数据,因此这两个工具可以结合使用。

PIL 和 scikit-image 库都可以用 pip 安装。

pip install PIL

pip install scikit-image

首先导入即将用到的库和模块。导入 numpy,从 PIL 中导入 Image 等绘图函数。

代码语言:javascript复制
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from skimage import transform as tf

接着,创建用于生成验证码的基础函数。这个函数接受一个单词和错切值(通常在 0 到 0.5 之间),返回用 numpy 数组表示的图像。该函数还提供指定图像大小的参数,因为后面还会用它生成只包含单个字母的测试数据。代码如下:

代码语言:javascript复制
def create_captcha(text, shear=0, size=(100, 24)):
    # 我们使用字母 L 来生成一张黑白图像,为 ImageDraw 类初始化一个实例。这样,我们就可以用 PIL 绘图,代码如下:
    im = Image.new("L", size, "black")
    draw = ImageDraw.Draw(im)
    # 指定验证码文字所使用的字体。这里要用到字体文件,下面代码中的文件名(Coval-Black.otf)应该指向文件存放位置
    font = ImageFont.truetype(r"bretan/Coval-Black.otf", 22)
    draw.text((2, 2), text, fill=1, font=font)
    # 你可以从开源字库(Open Font Library)下载所需字体文件 Coval-Black.otf:http://openfontlibrary.org/en/font/bretan
    """
    把 PIL 图像转换为 numpy 数组,以便用 scikit-image 库为图像添加错切变化效果。scikit-image 大部分计算都使用 numpy 数组格
    式。代码如下:
    """
    image = np.array(im)
    # 然后,应用错切变化效果,返回图像。
    affine_tf = tf.AffineTransform(shear=shear)
    image = tf.warp(image, affine_tf)
    return image/image.max()

上面最后一行代码对图像特征进行归一化处理,确保特征值落在 0 到 1 之间。归一化处理可在数据预处理、分类或其他阶段进行。

现在,可以轻松生成图像,使用 pyplot 绘制图像。首先设定魔术方法 %matplotlib inline,指定在当前笔记本作图,导入 pyplot。代码如下:

代码语言:javascript复制
%matplotlib inline
from matplotlib import pyplot as plt

生成验证码图像并显示它。

代码语言:javascript复制
image = create_captcha("GENE", shear=0.5)
plt.imshow(image, cmap='Greys')

生成的图像如图所示。

将图像切分成单个的字母

虽然我们验证码是单词,但是我们不打算构造能够识别成千上万个单词的分类器,而是把大问题转换为更小的问题:识别字母。

验证码识别的下一步是分割单词,找出其中的字母。具体做法是,创建一个函数,寻找图像中连续的黑色像素,抽取它们作为新的小图像。这些小图像(或者至少应该)就是我们要寻找的字母。

首先,导入图像分割函数要用到的 label、regionprops 函数。

代码语言:javascript复制
from skimage.measure import label, regionprops

图像分割函数接收图像,返回小图像列表,每张小图像为单词的一个字母,函数声明如下:

代码语言:javascript复制
# noinspection PyShadowingNames
def segment_image(image):
    """
    我们要做的第一件事就是检测每个字母的位置,这就要用到 scikit-image 的 label 函数,它能找出图像中像素值相同且又连接在一起的
    像素块。
    """
    """
    label 函数的参数为图像数组,返回跟输入同型的数组。在返回的数组中,图像 连接 在 一起的区域 用不同的值来表示,在这些区域以外
    的像素用 0 来表示。代码如下:
    """
    labeled_image = label(image > 0)
    # 抽取每一张小图像。将它们保存到一个列表中
    subimages = []
    # scikit-image 库还提供抽取连续区域的函数:regionprops。遍历这些区域,分别对他们进行处理。
    for region in regionprops(labeled_image):
        # 这样,通过 region 对象就能获取到当前区域的相关信息。我们这里要用到当前区域的起始和结束位置的坐标。
        start_x, start_y, end_x, end_y = region.bbox
        """
        用这两组坐标作为索引就能抽取到小图像(image 对象为 numpy 数组,可以直接用索引值),然后,把它保存到 subimages 列表中。
        代码如下:
        """
        subimages.append(image[start_x:end_x, start_y:end_y])
    """
    最后(循环外面),返回找到的小图像。每张(希望如此)小图像包含单词的一个字母区域。没有找到小图像的情况,直接把原图作为子图
    返回。代码如下:
    """
    if len(subimages) == 0:
        return [image,]
    return subimages

使用刚定义的这个函数,就能从前面生成的验证码中找到小图像。

代码语言:javascript复制
subimages = segment_image(image)

还可以像下面这样查看每张小图像。

代码语言:javascript复制
f, axes = plt.subplots(1, len(subimages), figsize=(10, 3))
for i in range(len(subimages)):
    axes[i].imshow(subimages[i], cmap="gray")

结果如下:

图像切割效果还不错,但你可能注意到,每张小图像都多少带有相邻字母的一部分。

创建训练集

使用图像切割函数就能创建字母数据集,其中字母使用了不同的错切效果。然后,就可以训练神经网络分类器来识别图像中的字母。

首先,指定随机状态值,创建字母列表,指定错切值。这里几乎没有新内容,numpy 的 arange 函数你可能没用过,它跟 Python 的 range 函数类似——只不过 arange 函数可以和 numpy 的数组一起用,步长可以使用浮点数。代码如下:

代码语言:javascript复制
from sklearn.utils import check_random_state
random_state = check_random_state(14)
letters = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
shear_values = np.arange(0, 0.5, 0.05)

再来创建一个函数(用来生成训练数据),从我们提供的选项中随机选取字母和错切值。代码如下:

代码语言:javascript复制
# noinspection PyShadowingNames
def generate_sample(random_state=None):
    random_state = check_random_state(random_state)
    letter = random_state.choice(letters)
    shear = random_state.choice(shear_values)
    # 返回字母图像及表示图像中字母属于哪个类别的数值。字母 A 为类别 0,B 为类别 1,C 为类别 2,以此类推。代码如下:
    return create_captcha(letter, shear=shear, size=(20, 20)), letters.index(letter)

在上述函数体的外面,调用该函数,生成一条训练数据,用 pyplot 显示图像。

代码语言:javascript复制
image, target = generate_sample(random_state)
plt.imshow(image, cmap="Greys")
print("The target for this image is:{0}".format(target))

调用几千次该函数,就能生成足够的训练数据。把这些数据传入到 numpy 的数组里,因为数组操作起来比列表更容易。代码如下:

代码语言:javascript复制
dataset, targets = zip(*(generate_sample(random_state)for i in range(3000)))
dataset = np.array(dataset, dtype='float')
targets = np.array(targets)

我们共有 26 个类别,每个类别(字母)用从 0 到 25 之间的一个整数表示。神经网络一般不支持一个神经元输出多个值,但是多个神经元就能有多个输出,每个输出值在 0 到 1 之间。因此,我们对类别使用一位有效码编码方法,这样,每条数据就能得到 26 个输出。如果结果像某字母,使用近似于 1 的值;如果不像,就用近似于 0 的值。代码如下:

代码语言:javascript复制
from sklearn.preprocessing import OneHotEncoder
onehot = OneHotEncoder(categories='auto')
y = onehot.fit_transform(targets.reshape(targets.shape[0], 1))

我们用的库不支持稀疏矩阵,因此,需要将稀疏矩阵转换为密集矩阵。代码如下:

代码语言:javascript复制
y = y.todense()

根据抽取方法调整训练数据集

得到的数据集跟即将使用的方法有较大出入。数据集每条数据都是一个恰好为 20 像素见方的字母。我们所使用的方法是从单词中抽取字母,而这可能会挤压图像,使图像偏离中心或者引入其他问题。

理想情况下,训练分类器所使用的数据应该与分类器即将处理的数据尽可能相似。但实际中,经常有所不同,但是应尽量缩小两者之间的差别。

对于验证码识别实验,最理想的情况是,从实际的验证码中抽取字母,并对它们进行标注。为了节省时间,我们只在训练集上运行分割函数,返回分割后得到的字母图像。

我们需要用到 scikit-image 库中的 resize 函数,因为我们得到的小图像并不总是并不总是 20 像素见方。代码如下:

代码语言:javascript复制
from skimage.transform import resize

现在,就可以对每条数据运行 segment_image 函数,将得到的小图像调整为 20 像素见方。代码如下:

代码语言:javascript复制
dataset = np.array([resize(segment_image(sample)[0], (20, 20))for sample in dataset])

最后,创建我们的数据集。dataset 数组是三维的,因为它里面存储的是二维图像信息。由于分类器接收的是二维数组,因此,需要将最后的二维扁平化。

代码语言:javascript复制
X = dataset.reshape((dataset.shape[0], dataset.shape[1]*dataset.shape[2]))

使用 scikit-learn 中的 train_test_split 函数,把数据集切分为训练集和测试集。代码如下:

代码语言:javascript复制
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.9)

训练和分类

接下来,我们就来构造神经网络分类器,接收图像,预测图像中的(单个)字母是什么。

我们将用之前创建的单个字母训练集。数据集本身很简单。每张图像为 20 像素大小,每个像素用 1(黑)或 0(白)来表示。每张图像有 400 个特征,将它们作为神经网络的输入。输出结果为 26 个 0 到 1 之间的值。值越大,表示图像中的字母为该值所对应的字母(输出的第一个值对应字母 A,第二个对应字母 B,以此类推)的可能性越大。

我们用 PyBrain 库来构建神经网络分类器。

跟我们之前见到的所有库一样,PyBrain 也可以用 pip 来安装:pip install pybrain。

PyBrain 库使用自己的数据集格式,好在创建这种格式的训练集和测试集并不太难。代码如下:

代码语言:javascript复制
from pybrain.datasets import SupervisedDataSet

首先,遍历我们的训练集,把每条数据添加到一个新的 SupervisedDataSet 实例中。代码如下:

代码语言:javascript复制
training = SupervisedDataSet(X.shape[1], y.shape[1])
for i in range(X_train.shape[0]):
    training.addSample(X_train[i], y_train[i])

然后,遍历测试集,同样把每条数据添加到一个新的 SupervisedDataSet 实例中。代码如下:

代码语言:javascript复制
testing = SupervisedDataSet(X.shape[1], y.shape[1])
for i in range(X_test.shape[0]):
    testing.addSample(X_test[i], y_test[i])

现在就可以创建神经网络了。我们将创建一个最基础的、具有三层结构的神经网络,它由输入层、输出层和一层隐含层组成。输入层和输出层的神经元数量是固定的。数据集有 400 个特征,那么第一层就需要有 400 个神经元,而 26 个可能的类别表明我们需要 26 个用于输出的神经元。

确定隐含层的神经元数量可能相当困难。如果神经元数量过多,神经网络呈现出稀疏的特点,这时要训练足够多的神经元恰当地表示数据就有困难,这往往会导致过拟合训练数据的问题。相反,如果神经元过少,每个对分类结果的贡献过大,再加上训练不充分,就很可能产生低拟合现象。我发现一开始用漏斗形状不错,即隐含层神经元数量介于输入和输出之间。本文,隐含层用 100 个神经元,你可以尝试其他值,看看能不能取得更好的效果。

导入 BuildNetwork 函数,指定维度,创建神经网络。第一个参数 X.shape[1] 为输入层神经元的数量,也就是特征数(数据集 X 的列数)。第二个参数指隐含层的神经元数量,这里设置为 100。第三个参数为输出层神经元的数量,由类别数组 y 的形状来确定。最后,除输出层外,我们每层使用一个一直处于激活状态的偏置神经元(bias neuron,它与下一层神经元之间有边连接,边的权重经过训练得到)。代码如下:

代码语言:javascript复制
from pybrain.tools.shortcuts import buildNetwork
net = buildNetwork(X.shape[1], 100, y.shape[1], bias=True)

现在,我们就可以训练神经网络,寻找合适的权重值。但是如何训练神经网络呢?

反向传播算法

反向传播算法(back propagation,backprop)的工作机制对预测错误的神经元施以惩罚。从输出层开始,向上层层查找预测错误的神经元,微调这些神经元输入值权重,已达到修复输出错误的目的。

神经元之所以给出错误的预测,原因在于它前面为其提供输入的神经元,更确切地说是由这两个神经元之间边的权重及输入值决定的。我们可以尝试对这些权重进行微调。每次调整的幅度取决于以下两个方面:神经元各边权重的误差函数的偏导数和一个叫作学习速率的参数(通常使用很小的值)。计算出函数误差的梯度,再乘以学习速率,用总权重减去得到的值。下面将给出例子。梯度的符号由误差决定,每次对权重的修正都是朝着给出正确的预测值努力。有时候,修正结果为局部最优(local optima),比起其他权重组合要好,但所得到的各权重还不是最优组合。

反向传播算法从输出层开始,层层向上回溯到输入层。到达输入层后,所有边的权重更新完毕。

PyBrain 提供了 backprop 算法的一种实现,在神经网络上调用 trainer 类即可。代码如下:

代码语言:javascript复制
from pybrain.supervised.trainers import BackpropTrainer
trainer = BackpropTrainer(net, training, learningrate=0.01, weightdecay=0.01)

backprop 算法在训练集上进行迭代,每次都对权重进行微调。当误差减小的幅度很小时表明算法无法进一步缩小误差,继续训练已无意义,这时就可以停止算法运行。理论上,等误差不在缩小时,再停止运行。这个过程叫做算法收敛。但实际上我们不会等到误差停止缩小,因为这通常要花费很长时间且效果有限。

此外,更简单的做法是,运行代码固定的步数(epoch)。每一部所包含的次数越多,所用时间也就越多,效果越好(每一步的提升效果呈下降趋势)。我们训练时,使用了 20 步,数量再多些,效果可能会有所改善(也许幅度很小)。代码如下:

代码语言:javascript复制
trainer.trainEpochs(epochs=20)

运行前面的代码,这可能要花几分钟时间,长短取决于硬件,然偶胡我们就能在测试集上进行预测。PyBrain 提供了相应函数,只要在 trainer 实例上调用即可。

代码语言:javascript复制
predictions = trainer.testOnClassData(dataset=testing)

得到预测值之后,可以用 scikit-learn 计算 F1 值。

代码语言:javascript复制
from sklearn.metrics import f1_score
print("F-score:{0:.2f}".format(f1_score(predictions, y_test.argmax(axis=1), average='micro')))

F1 值为 0.97,对于相对较小的模型来说,这个结果很了不起。别忘了我们的特征值只是简单地像素值,神经网络竟然知道怎么使用它们。

既然构建好了具有高精度的字母分类器,接下来看下怎么用它来识别单词,实现验证码破解功能。

预测单词

预我们想分别识别每张小图像中的字母,然后把它们拼成单词,完成验证码识别。

我们来定义一个函数,接收验证码,用神经网络进行训练,返回单词预测结果。

代码语言:javascript复制
# noinspection PyUnusedLocal
def predict_captcha(captcha_image, neural_network):
    # 使用前面定义的图像切割函数 segment_image 抽取小图像
    # noinspection PyShadowingNames
    subimages = segment_image(captcha_image)
    # 最终预测结果——单词,将由小图像中的字母组成。这些小图像根据位置进行排序,从而保证拼接后得到的单词中各字母处在正确的位置上。
    predicted_word = ""
    # 遍历四张小图像
    for subimage in subimages:
        # 每张小图像不太可能正好都是 20 像素见方。调整大小后,才能用神经网络处理。
        subimage = resize(subimage, (20, 20))
        """
        把小图像数据传入神经网络输入层,激活神经网络。这些数据将在神经网络中进行传播,返回输出结果。神经网络在前面已经创建好了,
        现在只需激活它。代码如下:
        """
        outputs = net.activate(subimage.flatten())
        """
        神经网络输出 26 个值,每个值都有索引号,分别对应 letters 列表中有着相同索引的字母,每个值的大小表示与对应字母的相似度。
        为了获取实际的预测值我们取到最大值的索引,再通过 letters 列表找到对应的字母。例如,如果第五个值最大,那么预测结果就为字
        母 E。代码如下
        """
        prediction = np.argmax(outputs)
        # 把上面得到的字母添加到正在预测的单词中。
        predicted_word  = letters[prediction]
    # 循环结束后,我们就已经找到了单词的各个字母,将其拼接成单词,最后返回这个单词。
    return predicted_word

可以用下面的代码来做下测试,尝试不同的单词,看看可能会遇到什么错误,别忘了我们的神经网络只能处理大写字母。

代码语言:javascript复制
word = "GENE"
captcha = create_captcha(word, shear=0.2)
print(predict_captcha(captcha, net))

我们可以把上面的代码整合到一个函数里,便于进行多次尝试。这里沿用之前每个单词只包含四个字母的假设,降低预测任务的难度。删除 prediction = prediction[:4],试试看可能会出现什么错误。代码如下:

代码语言:javascript复制
# noinspection PyShadowingNames
def test_prediction(word, net, shear=0.2):
    captcha = create_captcha(word, shear=shear)
    prediction = predict_captcha(captcha, net)
    prediction = prediction[:4]
    return word == prediction, word, prediction

上述函数返回结果依次为预测结果是否准确、验证码中的单词和预测结果的前四个字符。

上面的代码能正确识别单词 GENE,但是其他单词会出错。正确率如何?我们借助 NLTK 模块创建单词数据集,只使用长度为 4 的单词。从 NLTK 中导入 words,代码如下:

代码语言:javascript复制
from nltk.corpus import words

words 实际上为 corpus(语料库)对象,调用它的 words()方法才能取到里面的单词。下面代码还加了长度为 4 的过滤条件。代码如下:

代码语言:javascript复制
valid_words = [word.upper()for word in words.words()if len(word) == 4]

识别得到所有长度为 4 的单词,分别统计识别正确和错误的数量。

代码语言:javascript复制
num_correct = 0
num_incorrect = 0
for word in valid_words:
    correct, word, prediction = test_prediction(word, net, shear=0.2)
    if correct:
        num_correct  = 1
    else:
        num_incorrect  = 1
print("Number correct is {0}".format(num_correct))
print("Number incorrect is {0}".format(num_incorrect))

正确识别 2832 个单词,错误识别 2681 个,正确率仅为 51%。而字母识别率为 97%,两者差距太大,到底怎么回事?

首先,来看下正确率本身。其余条件相同的情况下,我们有四个字母,每个字母的正确率为 97%,四个字母都正确的话,正确率约为 88%(约为 0.97⁴)。一个字母出错将导致整个单词识别错误。

其次,错切值对正确率有影响。这次创建数据集时,随机从 0 到 0.5 之间选取一个数作为错切值。先前测试时错切值为 0.2。错切值为 0 时,正确率为 75%;错切值取 0.5 时,正确率只有 2.5%。错切值越大,正确率越低。

另外一个原因在于我们之前随机选取字母组成单词,而字母在单词中的分布不是随机的。例如,字母 E 显然就比 Q 等其他字母使用频率更高。使用频度较高,但却常常被识别错误的字母,也会导致错误率上升。

我们可以把经常识别错误的字母统计出来,用二维混淆矩阵来表示。每行和每列均为一个类别(字母)。

矩阵的每一项表示一个类别(行对应的类)被错误识别为另一个类别(列对应的类)的次数。例如,(4, 2)这一项的值为 6,表示字母 D 有 6 次被错误的识别为字母 B。

代码语言:javascript复制
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(np.argmax(y_test, axis=1), predictions)

理想情况下,混淆矩阵应该只有对角线的项(i, i)值不为零,其余各项的值均为零,否则就是分类错误。

用 pyplot 做成图,哪些字母容易混淆就一目了然,代码如下:

代码语言:javascript复制
plt.figure(figsize=(10, 10))
plt.imshow(cm)

下面代码中的 tick_marks 表示刻度,在坐标轴上标出每个字母,便于理解。

代码语言:javascript复制
tick_marks = np.arange(len(letters))
plt.xticks(tick_marks, letters)
plt.yticks(tick_marks, letters)
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.show()

从图中,我们就能清楚地看到主要的错误几乎无一例外地把 U 识别为 H!

我们的词表中 17% 的单词含有字母 U,这些单词几乎都会被识别错误。U 的出现频率要高于 H(11% 的单词),我们不禁想到了一个提高正确率的简单方法:把所有预测结果为 H 的,都改为 U。

0 人点赞