Skip to content

验证码-手写数字识别

目录

前言

TIP

卷积神经网络(Convolutional Neural Network, CNN)是深度学习中专门用于处理图像、视频等网格结构数据的神经网络模型。

它通过模仿人类视觉系统的处理方式,能够自动从原始数据中提取特征并进行分类识别,在计算机视觉领域取得了革命性的突破。

一、CNN适合识别的验证码类型

卷积神经网络(CNN)在验证码识别领域展现出强大的能力,特别适合处理以下类型的验证码:

一、字符型验证码

验证码类型核心特征识别难度关键技术/策略配图建议
数字验证码4-6位纯数字,字符规整★☆☆☆☆ (较低)- 基础CNN模型
- 少量训练数据
清晰的纯数字验证码图片
字母数字混合包含数字、大小写字母(字符集36-62位)★★☆☆☆ (中等)- 更深的CNN网络
- 更大规模训练数据
对比图:数字vs字母数字混合
扭曲变形字符字符经过旋转、扭曲、拉伸处理★★★☆☆ (中高)- CNN自动特征提取
- 数据增强
处理前后效果对比图
简单背景验证码字符与背景对比度明显,干扰少★☆☆☆☆ (较低)- 适合CNN入门项目
- 简单预处理
背景干净、字符突出的示例
复杂背景验证码包含噪点、干扰线、背景纹理★★★★☆ (高)- 深层CNN
- 高级图像预处理
复杂背景原图vs处理后
固定长度验证码字符数量固定(如4位或6位)★★☆☆☆ (中等)- 多任务学习框架
- 端到端识别
识别流程图解
可分割字符验证码字符间有明显间隔★★☆☆☆ (中等)- 字符分割算法
- 分割后识别
分割过程示意图
  1. 数字验证码 由4-6位纯数字组成 字符相对规整,识别难度较低 训练数据量要求相对较小,识别准确率可达95%以上
  2. 字母数字混合验证码 包含数字、小写字母、大写字母的组合 字符集规模扩大(通常36-62个字符) 需要更深的网络结构和更多训练数据
  3. 扭曲变形字符验证码 字符经过旋转、扭曲、拉伸等变形处理 传统OCR方法难以识别,但CNN通过自动特征提取能够有效处理 通过数据增强(旋转、缩放)可提升模型泛化能力 二、静态图片验证码
  4. 简单背景验证码 字符与背景对比度明显 背景干扰较少,识别难度适中 适合作为入门级CNN训练项目
  5. 复杂背景验证码 包含噪点、干扰线、背景纹理等干扰 需要CNN具备更强的特征提取和抗干扰能力 通过多层级卷积可有效分离字符与背景 三、多字符验证码
  6. 固定长度验证码 字符数量固定(如4位、6位) 可设计为多任务学习框架,每个字符位置对应一个分类器 整体识别准确率可达90%以上
  7. 可分割字符验证码 字符间有明显间隔,易于分割 可先进行字符分割,再分别识别 识别准确率较高,但需要额外的分割算法

二、静态图片验证码

  1. 简单背景验证码 字符与背景对比度明显 背景干扰较少,识别难度适中 适合作为入门级CNN训练项目
  2. 复杂背景验证码 包含噪点、干扰线、背景纹理等干扰 需要CNN具备更强的特征提取和抗干扰能力 通过多层级卷积可有效分离字符与背景

三、多字符验证码

  1. 固定长度验证码 字符数量固定(如4位、6位) 可设计为多任务学习框架,每个字符位置对应一个分类器 整体识别准确率可达90%以上
  2. 可分割字符验证码 字符间有明显间隔,易于分割 可先进行字符分割,再分别识别 识别准确率较高,但需要额外的分割算法

二、白话原理 💡

想象一下,你要教计算机"看" 图片。传统方法是把整张图片的像素都塞给计算机,让它自己找规律,但这样太笨重了。卷积神经网络(CNN)就像给计算机装上了一双" 智慧的眼睛",让它像人一样分区域、分层次地观察图片。

三、卷积神经网络核心原理

1. 卷积层:拿放大镜一样看局部

局部感受野:人看图片时,不会一眼看完整张图,而是先看左上角,再看右下角,一块一块地观察。CNN也是这样,它用一个小小的"放大镜" (卷积核)在图片上滑动,每次只关注一小块区域(比如3×3像素)。 权值共享:更聪明的是,同一个"放大镜"会用在图片的所有位置。比如要找"眼睛"这个特征,不管眼睛在左上角还是右下角,都用同一个放大镜去探测。这样大大减少了需要学习的参数数量。 特征提取:不同的放大镜负责找不同的特征。有的放大镜专门找边缘(比如垂直边缘),有的找纹理,有的找颜色块。通过多个放大镜的组合,就能提取出图片的各种特征。

2. 激活函数:过滤掉没用的信息

类似,去除人眼中没用的部分

卷积后的结果会经过ReLU激活函数,它的作用很简单:把负数变成0,正数保持不变。就像把图片中不重要的信息扔掉,只保留有用的特征,这样既能提高计算效率,还能让网络学得更好。

3. 池化层:压缩信息,保留精华

池化层就像在整理照片,把相似的照片归类到一个文件夹,只保留最有代表性的几张。它有两种方式: 最大池化:取局部区域的最大值,保留最显著的特征(比如纹理最明显的地方) 平均池化:取局部区域的平均值,让特征更平滑 池化层的好处是:减少数据量、提高计算速度、增强抗干扰能力(即使特征位置稍微偏移,也能识别出来)。

4. 全连接层:拼积木,做决策

经过多层卷积和池化后,提取到的特征(边缘、纹理、眼睛、鼻子等)会被"展平" 成一维向量,然后进入全连接层。这个层的作用就像法官综合所有证据:把前面提取到的所有特征整合起来,判断这张图片到底是什么(比如" 猫"还是"狗")。

5. 输出层:给出最终答案

最后用Softmax函数把结果转换成概率分布。比如识别猫的概率是80%,狗是15%,沙发是5%,取最大的概率值就是最终结果。

四、CNN的基本结构

典型的卷积神经网络主要由以下核心组件构成:

  1. 输入层(Input Layer) 输入层接收原始图像数据,通常将图像转换为三维矩阵形式(高度×宽度×通道数)。对于RGB彩色图像,通道数为3(红、绿、蓝);对于灰度图像,通道数为1。
  2. 卷积层(Convolution Layer) 卷积层是CNN的核心组件,通过卷积核(滤波器)在输入数据上滑动,进行局部特征提取。每个卷积核可以学习检测特定的特征模式,如边缘、纹理、角点等。卷积操作具有局部连接和权值共享两大特性,显著减少了参数量。 卷积计算过程: 卷积核在输入图像上以固定步长滑动 每次计算卷积核与对应区域的点积(逐元素相乘再求和) 通过填充(padding)操作保持输出尺寸 多个卷积核生成多个特征图(Feature Map)
  3. 激活函数(Activation Function) 在卷积层后通常添加激活函数,引入非线性变换,增强模型的表达能力。最常用的是ReLU(Rectified Linear Unit)函数,其定义为ReLU( x) = max(0, x),能够有效缓解梯度消失问题。
  4. 池化层(Pooling Layer) 池化层通过下采样操作降低特征图的空间维度,减少计算量和参数数量,同时保留重要特征信息。常用方法包括: 最大池化(Max Pooling):取池化窗口内的最大值,保留最显著特征 平均池化(Average Pooling):取池化窗口内的平均值,保留整体信息
  5. 全连接层(Fully Connected Layer) 全连接层位于网络末端,将前面提取的特征进行整合和分类。每个神经元与前一层的所有神经元相连,通过线性变换将特征映射到输出类别空间。通常使用Softmax函数输出每个类别的概率分布。

五、经典CNN网络架构

  1. LeNet-5(1998) CNN的开山之作,由Yann LeCun提出,用于手写数字识别。包含两个卷积层和三个全连接层,奠定了CNN的基本架构模式。
  2. AlexNet(2012) 在ImageNet比赛中首次夺冠,开启了深度学习时代。主要创新包括: 使用ReLU激活函数替代Sigmoid,缓解梯度消失 引入Dropout正则化技术防止过拟合 首次使用GPU加速训练
  3. VGGNet(2014) 使用连续的3×3小卷积核代替大卷积核,证明了深层窄卷积的有效性。VGG-16和VGG-19成为经典网络结构。
  4. ResNet(2015) 何恺明团队提出残差连接(Residual Connection),解决了深层网络的梯度消失问题,允许训练超过100层的极深网络。在ImageNet上实现了超越人类水平的识别准确率。
  5. 轻量化网络 为适应移动端部署,出现了MobileNet、ShuffleNet等轻量化网络,通过深度可分离卷积等技术大幅减少计算量和参数量。

六、CNN优势

1. CNN为什么这么厉害?

  1. 自动特征提取: 传统方法需要人工设计特征(比如告诉计算机"眼睛是圆的"),而CNN能自动从数据中学习特征,不需要人工干预。
  2. 参数共享: 同一个卷积核在整个图片上共享参数,大幅减少了需要学习的参数数量。比如一张200×200的图片,传统方法可能需要几百万个参数,而CNN可能只需要几千个。
  3. 平移不变性: 无论猫在图片的左上角还是右下角,CNN都能识别出来,因为它学的是"猫长什么样",而不是"猫必须在哪个位置"。
  4. 层次化特征学习: 浅层网络学习低级特征(边缘、角点),中层网络组合成复杂形状(眼睛、耳朵),深层网络识别完整对象(猫、狗)。这种层次化的学习方式符合人类视觉系统的认知规律。

2. CNN在验证码识别中的核心优势

  1. 自动特征提取能力 CNN能够自动从原始验证码图像中学习特征,无需人工设计特征工程。传统方法需要手动提取字符形状、笔画宽度、连接关系等特征,过程繁琐且容易受到字符变形、粘连等因素的影响。而CNN通过多层次的卷积操作,能够自动学习到从低级边缘特征到高级字符形状的完整特征表示。
  2. 强大的表征能力 CNN通过多层非线性变换和组合,可以学习到更加抽象和高级的特征表达,对于复杂和变形的验证码字符具有更强的表征能力。即使是包含扭曲、旋转、粘连等干扰的复杂验证码,CNN也能通过深层网络结构进行有效识别。
  3. 端到端学习机制 CNN可以实现端到端的学习,即从原始图像输入到最终识别结果输出,整个过程都可以通过神经网络进行学习和优化。这种机制避免了传统方法中需要分别进行图像预处理、字符分割、特征提取等多个独立步骤的复杂性,提高了算法的整体性能和鲁棒性。
  4. 高识别准确率 实验研究表明,基于CNN的验证码识别方法在可分割及不可分割验证码上的平均识别准确率均收敛于99%左右。相比于传统的神经网络方法和支持向量机,CNN取得了更高的识别率,特别在处理复杂验证码时表现尤为突出。
  5. 泛化能力强 CNN模型经过大量数据训练后,对于不同类型的验证码具有较强的泛化能力。即使面对新的验证码样式,通过微调或迁移学习,也能快速适应并达到较高的识别准确率。这种泛化能力使得CNN成为验证码识别领域的首选技术。

3. CNN的优势应用场景

  1. 端到端识别 无需人工特征工程,直接从原始图像学习特征 对字符变形、噪声干扰具有较强鲁棒性 识别准确率显著高于传统方法
  2. 复杂验证码识别 对于包含粘连字符、严重扭曲的验证码 CNN通过深层网络结构能够提取更抽象的特征 在复杂场景下仍能保持较高识别率
  3. 实时识别需求 训练好的CNN模型可快速部署 单张图像识别时间在毫秒级别 适合自动化测试、数据采集等应用场景

CNN的劣势

1. CNN缺点

  • 计算资源需求大:深层CNN需要大量计算资源和训练时间
  • 对数据量要求高:需要大量标注数据才能达到良好性能
  • 可解释性较差:模型决策过程难以直观理解
  • 对旋转和尺度变化敏感:虽然具有一定平移不变性,但对旋转和尺度变化仍需数据增强

2. 不适合CNN的验证码类型

  1. 行为式验证码 如滑动拼图、点击验证等 需要识别用户操作行为而非图像内容 更适合使用行为分析或强化学习
  2. 动态验证码 字符不断移动、闪烁的动态验证码 需要处理视频序列而非静态图像 更适合使用3D CNN或RNN
  3. 短信验证码 通过手机短信接收的数字验证码 属于文本信息而非图像识别任务 无需使用CNN
  4. 超复杂验证码 包含严重重叠、极度扭曲的字符 即使CNN也难以达到实用准确率 需要更复杂的网络结构或人工辅助

ok 理论讲完了咱们开始上代码~

七、实现一个最小的卷积神经网络

TIP

有一个细节: 目录下一定要有models

MNIST数据集

完整代码

py
from torch import nn


# 模型
class MnistModel(nn.Module):
    def __init__(self):
        super(MnistModel, self).__init__()

        self.conv1 = nn.Conv2d(in_channels=1, out_channels=8, kernel_size=(3, 3), stride=1, padding=0)
        self.relu = nn.ReLU()
        self.fc1 = nn.Linear(8 * 26 * 26, 10)

    def forward(self, image):
        out_1 = self.conv1(image)
        out_2 = self.relu(out_1)
        image_view = out_2.view(-1, 8 * 26 * 26)
        out = self.fc1(image_view)
        return out
py
from torch import save, load
from torchvision import transforms
from torchvision.datasets import MNIST
from torch.utils.data import DataLoader
from torch import nn, optim
from tqdm import tqdm
import test
import numpy as np

from model_cnn import MnistModel



# 实例化模型
model = MnistModel()
optimizer = optim.Adam(model.parameters())

# 加载以及训练好的模型和优化器继续训练


loss_fn = nn.CrossEntropyLoss()

transforms = transforms.Compose(
    [
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.1307, ], std=[0.3081, ]),
    ]
)

mnist_train = MNIST(root='./MNIST_data', train=True, download=True, transform=transforms)



def train(epoch):
    total_loss = []

    data_loader = DataLoader(mnist_train, batch_size=8, shuffle=True)

    data_loader = tqdm(data_loader, total=len(data_loader))

    model.train()

    # 三件套
    for (img, label) in data_loader:
        # 梯度置0
        optimizer.zero_grad()
        # 传播
        output = model(img)
        # 单次优化
        loss = loss_fn(output, label)
        total_loss.append(loss.item())
        # 反向传播
        loss.backward()
        # 优化器更新
        optimizer.step()

    save(model.state_dict(), './models/model.pkl')
    save(optimizer.state_dict(), './models/optimizer.pkl')
    # succeed_rate = test.test_success(epoch)
    # loss = np.mean(total_loss)
    # print(f"\n第{epoch}轮epoch, 成功率:\t{succeed_rate} 损失为:\t{loss} ", )
    print("=====================================================================")


for i in range(10):
    train(i + 1)
py
import os
import torch
from torchvision.datasets import MNIST
from torch.utils.data import DataLoader
from torch import nn
from tqdm import tqdm
import numpy as np

from model_cnn import MnistModel


def test_success(t):
    # 模型


    from torchvision import transforms

    # 实例化模型
    model = MnistModel()


    loss_fn = nn.CrossEntropyLoss()
    if os.path.exists('./models/model.pkl'):
        model.load_state_dict(torch.load('./models/model.pkl'), strict=False)

    transforms = transforms.Compose(
        [
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.1307, ], std=[0.3081, ]),
        ]
    )
    mnist_train = MNIST(root='./MNIST_data', train=False, download=True, transform=transforms)

    data_loader = DataLoader(mnist_train, batch_size=8, shuffle=True)

    data_loader = tqdm(data_loader, total=len(data_loader))

    model.eval()

    # 成功率列表
    succeed = []

    # 三件套
    with torch.no_grad():
        for (img, label) in data_loader:
            # 获取结果
            output = model(img)
            result = output.max(dim=1).indices
            # print(result)
            # print(label)
            # print(result.eq(label))
            #
            # # 平均值
            # print(result.eq(label).float().mean().item())
            succeed.append(result.eq(label).float().mean().item())
            # 单次优化
            loss = loss_fn(output, label)

    return np.mean(succeed)

print(test_success(1))

效果

img.png

cifar10数据集

完整代码

py
from torch import nn


# 模型
class Cifar10Model(nn.Module):
    def __init__(self):
        super(Cifar10Model, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=8, kernel_size=(3, 3), stride=1, padding=0),
            nn.ReLU()
        )

        self.fc1 = nn.Linear(8 * 30 * 30, 10)

    def forward(self, image):
        out_1 = self.conv1(image)
        image_view = out_1.view(-1, 8 * 30 * 30)
        out = self.fc1(image_view)
        return out
py
from matplotlib import pyplot as plt
from torch import save, load
from torchvision import transforms
from torchvision.datasets import CIFAR10
from torch.utils.data import DataLoader
from torch import nn, optim
from torchvision.utils import make_grid
from tqdm import tqdm
import test
import numpy as np

from model_cnn import Cifar10Model

# 实例化模型
model = Cifar10Model()
optimizer = optim.Adam(model.parameters())

# 加载以及训练好的模型和优化器继续训练


loss_fn = nn.CrossEntropyLoss()

transforms = transforms.Compose(
    [
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.4914,0.4822,0.4465 ], std=[0.2023, 0.1994, 0.2010 ]),
    ]
)

cifar10_train = CIFAR10(root='./CIFAR10_data', train=True, download=True, transform=transforms)


def train(epoch):
    total_loss = []

    data_loader = DataLoader(cifar10_train, batch_size=8, shuffle=True)

    data_loader = tqdm(data_loader, total=len(data_loader))

    model.train()

    # 三件套
    for (img, label) in data_loader:
        # 看图片
        image = make_grid(img).permute(1, 2, 0).numpy()
        plt.imshow(image)
        # 保存图片
        plt.savefig('cifar10_example.png')  # 在show()之前调用
        plt.show()
        # 梯度置0
        optimizer.zero_grad()
        # 传播
        output = model(img)
        # 单次优化
        loss = loss_fn(output, label)

        total_loss.append(loss.item())
        data_loader.set_description("loss:{}".format(np.mean(total_loss)))
        # 反向传播
        loss.backward()
        # 优化器更新
        optimizer.step()

    save(model.state_dict(), './models/model.pkl')
    save(optimizer.state_dict(), './models/optimizer.pkl')
    succeed_rate = test.test_success(epoch)
    loss = np.mean(total_loss)
    print(f"\n{epoch}轮epoch, 成功率:\t{succeed_rate} 损失为:\t{loss} ", )
    print("=====================================================================")


for i in range(10):
    train(i + 1)
py
import os
import torch
from torchvision.datasets import CIFAR10
from torch.utils.data import DataLoader
from torch import nn
from tqdm import tqdm
import numpy as np

from model_cnn import Cifar10Model


def test_success(t):
    # 模型


    from torchvision import transforms

    # 实例化模型
    model = Cifar10Model()


    loss_fn = nn.CrossEntropyLoss()
    if os.path.exists('./models/model.pkl'):
        model.load_state_dict(torch.load('./models/model.pkl'), strict=False)

    transforms = transforms.Compose(
        [
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.2023, 0.1994, 0.2010]),
        ]
    )
    cifar10_train = CIFAR10(root='./CIFAR10_data', train=False, download=True, transform=transforms)

    data_loader = DataLoader(cifar10_train, batch_size=8, shuffle=True)

    data_loader = tqdm(data_loader, total=len(data_loader))

    model.eval()

    # 成功率列表
    succeed = []

    # 三件套
    with torch.no_grad():
        for (img, label) in data_loader:
            # 获取结果
            output = model(img)
            result = output.max(dim=1).indices
            # print(result)
            # print(label)
            # print(result.eq(label))
            #
            # # 平均值
            # print(result.eq(label).float().mean().item())
            succeed.append(result.eq(label).float().mean().item())
            # 单次优化
            loss = loss_fn(output, label)

    return np.mean(succeed)

print(test_success(1))

效果

img_1.png

可以发现,直接写的模型较差 😭

那就开始参考大佬写的 ResNet~

ResNet的使用

CPU太慢啦,上GPU啦

img_2.png

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

使用后对比 img_3.png

py
from torch import nn


# 模型
class Cifar10Model(nn.Module):
    def __init__(self):
        super(Cifar10Model, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=8, kernel_size=(3, 3), stride=1, padding=0),
            nn.ReLU()
        )

        self.fc1 = nn.Linear(8 * 30 * 30, 10)

    def forward(self, image):
        out_1 = self.conv1(image)
        image_view = out_1.view(-1, 8 * 30 * 30)
        out = self.fc1(image_view)
        return out
py
import torch
from matplotlib import pyplot as plt
from torch import save, load
from torchvision import transforms,models
from torchvision.datasets import CIFAR10
from torch.utils.data import DataLoader
from torch import nn, optim
from torchvision.utils import make_grid
from tqdm import tqdm
import test
import numpy as np

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 实例化模型
model = models.resnet50(pretrained=False)

fc_features = model.fc.in_features

# 修改默认分类
model.fc = nn.Linear(fc_features, 10)
# GPU
model = model.to(device)


optimizer = optim.Adam(model.parameters())

# 加载以及训练好的模型和优化器继续训练


loss_fn = nn.CrossEntropyLoss()

transforms = transforms.Compose(
    [
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.4914,0.4822,0.4465 ], std=[0.2023, 0.1994, 0.2010 ]),
    ]
)

cifar10_train = CIFAR10(root='./CIFAR10_data', train=True, download=True, transform=transforms)


def train(epoch):
    total_loss = []

    data_loader = DataLoader(cifar10_train, batch_size=8, shuffle=True)

    data_loader = tqdm(data_loader, total=len(data_loader))

    model.train()

    # 三件套
    for (img, label) in data_loader:
        # GPU
        img = img.to(device)
        label = label.to(device)

        # 看图片
        # image = make_grid(img).permute(1, 2, 0).numpy()
        # plt.imshow(image)
        # # 保存图片
        # plt.show()
        # 梯度置0
        optimizer.zero_grad()
        # 传播
        output = model(img)
        # 单次优化
        loss = loss_fn(output, label)

        total_loss.append(loss.item())
        data_loader.set_description("loss:{}".format(np.mean(total_loss)))
        # 反向传播
        loss.backward()
        # 优化器更新
        optimizer.step()

    save(model.state_dict(), './models/model.pkl')
    save(optimizer.state_dict(), './models/optimizer.pkl')
    succeed_rate = test.test_success(epoch)
    loss = np.mean(total_loss)
    print(f"\n{epoch}轮epoch, 成功率:\t{succeed_rate} 损失为:\t{loss} ", )
    print("=====================================================================")


for i in range(3):
    train(i + 1)
py
import os
import torch
from torchvision import models
from torchvision.datasets import CIFAR10
from torch.utils.data import DataLoader
from torch import nn
from tqdm import tqdm
import numpy as np

from model_cnn import Cifar10Model

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


def test_success(t):
    # 模型

    from torchvision import transforms

    # 实例化模型
    model = models.resnet50(pretrained=False)
    fc_features = model.fc.in_features

    # 修改默认分类
    model.fc = nn.Linear(fc_features, 10)


    loss_fn = nn.CrossEntropyLoss()
    if os.path.exists('./models/model.pkl'):
        model.load_state_dict(torch.load('./models/model.pkl'), strict=False)

    transforms = transforms.Compose(
        [
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.2023, 0.1994, 0.2010]),
        ]
    )
    cifar10_train = CIFAR10(root='./CIFAR10_data', train=False, download=True, transform=transforms)

    data_loader = DataLoader(cifar10_train, batch_size=8, shuffle=True)

    data_loader = tqdm(data_loader, total=len(data_loader))

    model.eval()

    # 成功率列表
    succeed = []

    # 三件套
    with torch.no_grad():
        for (img, label) in data_loader:
            # GPU
            img = img.to(device)
            label = label.to(device)
            # 获取结果
            output = model(img)
            result = output.max(dim=1).indices
            # print(result)
            # print(label)
            # print(result.eq(label))
            #
            # # 平均值
            # print(result.eq(label).float().mean().item())
            succeed.append(result.eq(label).float().mean().item())
            # 单次优化
            loss = loss_fn(output, label)

    return np.mean(succeed)

print(test_success(1))