【机器学习】基于CNN-RNN模型的验证码图片识别

1. 引言

1.1. OCR技术研究的背景

1.1.1. OCR技术能够提升互联网体验

随着互联网应用的广泛普及,用户在日常操作中频繁遇到需要输入验证码的场景,无论是在登录、注册、支付还是其他敏感操作中,验证码都扮演着重要角色来确保安全性。然而,这种传统的手动输入方式不仅要求用户仔细辨认并逐一输入验证码中的字符,还常常因为字符的模糊、扭曲或难以辨认而引发输入错误,从而增加了用户的时间成本和操作难度,大大降低了用户体验。

为了改善这一状况,OCR验证码识别技术应运而生。这项技术通过利用先进的图像处理和计算机视觉算法,能够自动识别和解析验证码图像中的字符,并将其转换为可编辑的文本形式。用户只需将验证码图像输入到识别系统中,系统便能迅速而准确地完成识别,并将结果直接应用于相应的操作,无需用户手动输入。这不仅极大地减少了用户的操作时间,还降低了输入错误的可能性,显著提高了操作的便捷性和用户体验。

1.1.2. OCR技术能够保证系统安全

验证码作为一种重要的安全机制,在网络世界中起着至关重要的作用,其主要目的是为了防止恶意软件或自动化脚本的非法操作。无论是保护用户账户的安全,还是确保在线交易的顺利进行,验证码都发挥着不可替代的作用。然而,传统的手动输入验证码方式虽然在一定程度上提升了系统的安全性,但也可能因人为因素,如疲劳、粗心或误操作,而导致错误输入,从而给系统安全带来潜在风险。

为了克服这一挑战,OCR验证码识别技术应运而生。这项技术的引入不仅提升了用户体验,更在保障系统安全性方面展现出了巨大的潜力。通过采用先进的图像处理和机器学习算法,OCR验证码识别技术能够迅速、准确地识别并解析验证码图像中的字符,减少了人为因素导致的错误输入。同时,它还能够有效识别并抵御恶意软件的攻击,进一步提高了系统的安全防御能力。

在实际应用中,OCR验证码识别技术能够自动识别用户输入的验证码图像,并快速解析出其中的字符信息。这种自动化的处理方式不仅减少了用户的操作时间,还降低了因人为因素导致的错误率。此外,OCR验证码识别技术还能够对输入的验证码进行实时验证,确保用户输入的准确性,从而进一步增强了系统的安全性。

1.1.3. OCR技术具备良好的扩展性

随着技术的迅猛发展和网络安全要求的提高,验证码的形式和复杂度不断增加,以满足更加复杂多变的网络安全需求。这不仅包括传统的数字、字母组合验证码,还包括图形验证码、滑动验证码、拼图验证码等多种形式,甚至有的验证码还加入了动态效果和噪点干扰,以增大识别和破解的难度。

面对这种趋势,OCR验证码识别技术需要具备良好的适应性和可扩展性。首先,它必须能够应对各种类型和复杂度的验证码图像。无论是简单的数字字母组合,还是复杂的图形、滑动、拼图验证码,OCR技术都需要通过不断学习和优化算法,以准确识别和解析出其中的关键信息。这需要OCR技术具备强大的图像处理和识别能力,能够处理各种复杂的图像特征,如颜色、形状、纹理等。

其次,OCR验证码识别技术还需要具备可扩展性。随着技术的不断进步和新的验证码形式的出现,OCR技术需要能够迅速适应并集成新的识别算法和模型,以提供更加可靠和高效的解决方案。这要求OCR技术具备灵活的架构和可定制的功能,能够方便地添加新的算法和模型,并根据实际需求进行定制和优化。

为了实现良好的适应性和可扩展性,OCR验证码识别技术通常采用深度学习和计算机视觉技术作为核心。深度学习技术通过训练大量的数据样本,能够学习到验证码图像中的复杂特征和规律,并自动提取出关键信息。同时,深度学习模型具有强大的泛化能力,能够适应新的验证码形式和变化,确保识别的准确性和稳定性。

1.2. OCR模型简介

OCR模型,即光学字符识别(Optical Character Recognition)模型,是一种通过图像处理和模式识别技术,将图像中的文本信息转换为可编辑、可搜索的文本格式的技术。

1.2.1.OCR模型的工作原理

OCR模型的工作原理基于图象处理、模式识别和机器学习等技术:

  1. 图象预处理:对输入的图像进行预处理,包括图像的灰度化、二值化、去噪等操作。这些操作有助于简化后续处理,提高识别准确率。
  2. 文字定位:通过图像分析和边缘检测等算法,定位图像中的文字区域。文字定位是实现OCR识别的关键步骤之一。
  3. 字符分割:在文字定位的基础上,将文字区域进行字符分割,将每一个字符单独提取出来。字符分割的准确性直接影响后续的字符识别。
  4. 特征提取:对每一个字符进行特征提取,将字符的形状、纹理等特征转化为数值表示。常用的特征提取方法包括投影法、模板匹配法、形态学等。
  5. 字符识别:使用训练好的模型或算法对提取的字符特征进行识别。常用的字符识别方法包括模板匹配、统计模型、神经网络等。OCR可以使用单字符识别或者基于上下文的识别方法。
  6. 后处理:对识别结果进行后处理,包括错误校正、字典匹配、语法校验等。后处理可以提高识别结果的准确性和可信度。
  7. 输出结果:最后,OCR将识别的字符转化为可编辑、可搜索的文本输出。输出结果可以保存为文本文件、数据库记录等形式,方便后续的文本处理和分析。
1.2.2.OCR模型的优缺点

OCR模型具有以下优点:

  1. 实时性和准确性高:OCR模型可以直接从图像中提取文本,而无需复杂的预处理步骤。同时,由于其端到端的设计,使得模型可以同时处理多种语言和字体。
  2. 自动化程度高:OCR模型可以自动完成文本识别过程,减少人工干预,提高工作效率。

然而,OCR模型也存在一些不足之处:

  1. 对噪声和抖动较为敏感:如果输入图像中存在较多的噪声或抖动,可能会影响OCR模型的识别准确率。
  2. 对复杂排版处理效果不佳:对于复杂的排版结构,如表格、图文混排等,OCR模型的识别效果可能会受到一定影响。
1.2.3.OCR模型的应用场景

OCR模型在多个领域都有广泛的应用:

  1. 政府部门:OCR技术可以用于公安及交通部门的车牌、驾驶证、行驶证等证件识别,还有相关单位档案或笔录系统等,实现快速核查和比对,提高工作效率和准确度。
  2. 金融行业:OCR技术可以用于金融行业的远程开户、身份验证、银行卡识别、交易数据录入、文档管理等场景,提高工作效率和用户体验。

OCR模型是一种强大的文本识别技术,其工作原理基于图象处理、模式识别和机器学习等技术。通过不断的优化和改进,OCR模型在各个领域的应用前景将更加广阔。

1.3. OCR模型识别验证码方法

OCR模型识别验证码的步骤可以归纳如下:

  1. 图像获取
  • 打开需要登录或验证的网站,找到包含验证码的图像。
  • 将验证码图像截取或保存到本地,以便后续处理。
  1. 图像预处理
  • 对获取的验证码图像进行预处理,以消除噪声、改善图像质量。
  • 常见的预处理步骤包括灰度化、二值化、降噪等。
  1. 验证码定位
  • 使用图像分析技术,在预处理后的图像中定位验证码的具体位置。
  • 通过边缘检测、形态学操作等方法,将验证码从背景中分离出来。
  1. 字符分割
  • 将定位到的验证码图像进行字符分割,将每个字符单独提取出来。
  • 这通常涉及投影法、连通域分析等技术,以确保字符间的准确分离。
  1. 特征提取
  • 对每个分割出来的字符进行特征提取,将其形状、纹理等属性转化为数值表示。
  • 特征提取的方法取决于具体的OCR模型和算法,但通常包括HOG、SIFT、SURF等特征描述符。
  1. 字符识别
  • 使用训练好的OCR模型对提取的字符特征进行识别。
  • OCR模型可以是基于模板匹配、统计模型或深度学习的方法。
  • 深度学习模型(如卷积神经网络CNN)在复杂验证码识别中表现出色。
  1. 结果后处理
  • 对OCR模型输出的识别结果进行后处理,以提高准确性。
  • 后处理可能包括字典匹配、语法校验、去除非法字符等操作。
  1. 登录验证
  • 将识别出的验证码结果输入到相应的验证码输入框中。
  • 点击"登录"或"验证"按钮,等待系统验证结果。
  1. 结果反馈
  • 如果登录或验证成功,则表明OCR模型正确识别了验证码。
  • 如果失败,则可能需要重复上述步骤,或者调整OCR模型的参数和算法以提高识别准确率。

需要注意的是,OCR模型识别验证码的准确率和效率受到多种因素的影响,包括验证码的复杂度、图像质量、光照条件、字体类型等。因此,在实际应用中,需要根据具体情况对OCR模型进行选择和调整。

2.OCR模型识别验证码的过程

2.1 设置

python 复制代码
# 导入 os 模块,用于设置环境变量  
import os  
  
# 设置 Keras 的后端为 TensorFlow  
os.environ["KERAS_BACKEND"] = "tensorflow"  
  
# 导入 NumPy 库,用于数值计算  
import numpy as np  
  
# 导入 Matplotlib 的 pyplot 模块,用于绘图  
import matplotlib.pyplot as plt  
  
# 导入 Pathlib 的 Path 类,用于处理文件路径  
from pathlib import Path  
  
# 导入 TensorFlow 库  
import tensorflow as tf  
  
# 通常情况下,我们不需要直接导入 keras 模块,因为当我们使用 TensorFlow 2.x 时,  
# TensorFlow 已经包含了 Keras API。但如果您想使用独立的 Keras 库,请确保已正确安装。  
# 但为了代码简洁性,这里我们直接使用 TensorFlow 中的 Keras API。  
  
# 导入 TensorFlow 中的 Keras 相关的 ops 和 layers 模块  
# 注意:在 TensorFlow 2.x 中,ops 模块的导入通常是不必要的,因为大部分操作都直接集成在 layers 和其他模块中。  
# 但如果您需要使用特定的底层操作,可以从 'tensorflow.python.ops' 导入。
from keras import ops  
from tensorflow.keras import layers  

2.2.下载数据集

python 复制代码
curl -LO https://github.com/AakashKumarNain/CaptchaCracker/raw/master/captcha_images_v2.zip
7z x captcha_images_v2.zip -oC:\path\to\extract\ -y

2.3. 数据预处理

数据集包含了1040个验证码文件,这些文件均为PNG格式的图像。每个验证码图像都有一个对应的标签,这个标签实际上就是该图像的文件名(但不包括文件扩展名),它表示了验证码中显示的字符序列。

在训练模型之前,我们需要对标签进行预处理,以便模型能够理解并学习这些字符。具体来说,我们需要将标签中的每个字符映射到一个唯一的整数,从而形成一个整数序列。这是因为大多数机器学习模型,特别是深度学习模型,不能直接处理文本或字符数据,它们需要数值型输入。

为了实现这一映射,我们可以创建一个字典(或称为词汇表),其中键是字符,值是整数。这样,我们就可以将每个字符快速转换为对应的整数。同时,为了能够在预测阶段将模型输出的整数序列转换回原始的字符序列,我们还需要创建另一个字典,它的键是整数,值是字符。

在构建这两个字典时,我们还需要考虑一些额外的因素。例如,如果验证码中可能包含大小写字母、数字或特殊字符,我们需要确保这些字符在字典中都有对应的条目。此外,为了简化模型的学习过程,我们还可以考虑对字符进行编码,例如使用ASCII码或其他编码方案。

一旦我们构建了这两个字典,就可以开始处理数据集了。对于每个验证码图像,我们将其文件名(即标签)转换为整数序列,并将这个整数序列作为模型的目标输出。然后,我们可以使用这些处理过的数据来训练模型。

在预测阶段,模型将输出一个整数序列。我们使用之前创建的整数到字符的字典,将这个整数序列转换回原始的字符序列,从而得到预测的验证码。最后,我们可以将这个预测的验证码与实际的验证码进行比较,以评估模型的性能。

2.3.1 图片格栅标准化
python 复制代码
from pathlib import Path
import os

# 设置数据目录的路径
data_dir = Path("./captcha_images_v2/")

# 获取目录下所有的图片文件,并按照字母顺序排序
images = sorted(list(map(str, list(data_dir.glob("*.png")))))

# 从图片路径中提取标签,即图片名称中'.png'之前的部分
labels = [img.split(os.path.sep)[-1].split(".png")[0] for img in images]

# 从所有标签中提取字符,并去重,然后排序
characters = set(char for label in labels for char in label)
characters = sorted(list(characters))

# 打印找到的图片数量
print("Number of images found: ", len(images))
# 打印找到的标签数量
print("Number of labels found: ", len(labels))
# 打印找到的唯一字符数量
print("Number of unique characters: ", len(characters))
# 打印存在的字符
print("Characters present: ", characters)

# 设置训练和验证时的批次大小
batch_size = 16

# 设置期望的图片尺寸
img_width = 200
img_height = 50

# 设置图片将要被卷积块下采样的因子
# 我们将使用两个卷积块,每个块都有一个池化层,每个池化层将特征下采样2倍
# 因此总的下采样因子是4。
downsample_factor = 4

# 设置数据集中任何验证码的最大长度
max_length = max([len(label) for label in labels])
2.3.2. 划分数据集
python 复制代码
import tensorflow as tf
from tensorflow.keras.layers import StringLookup
import numpy as np

# 将字符映射为整数
# 使用StringLookup层来创建字符到数字的映射表
char_to_num = StringLookup(vocabulary=list(characters), mask_token=None)

# 将整数映射回原始字符
# 创建一个反向映射表,用于将数字转换回字符
num_to_char = StringLookup(
    vocabulary=char_to_num.get_vocabulary(), 
    mask_token=None, 
    invert=True
)

# 定义分割数据集的函数
def split_data(images, labels, train_size=0.9, shuffle=True):
    # 1. 获取数据集的总大小
    size = len(images)
    # 2. 创建索引数组,并根据需要进行打乱
    indices = tf.range(size)
    if shuffle:
        indices = tf.random.shuffle(indices)
    # 3. 计算训练样本的数量
    train_samples = int(size * train_size)
    # 4. 将数据分割为训练集和验证集
    x_train, y_train = images[indices[:train_samples]], labels[indices[:train_samples]]
    x_valid, y_valid = images[indices[train_samples:]], labels[indices[train_samples:]]
    return x_train, x_valid, y_train, y_valid

# 使用split_data函数分割数据集
x_train, x_valid, y_train, y_valid = split_data(np.array(images), np.array(labels))

# 定义编码单个样本的函数
def encode_single_sample(img_path, label):
    # 1. 读取图片文件
    img = tf.io.read_file(img_path)
    # 2. 解码图片并转换为灰度图
    img = tf.io.decode_png(img, channels=1)
    # 3. 将图片数据类型转换为float32,并归一化到[0, 1]区间
    img = tf.image.convert_image_dtype(img, tf.float32)
    # 4. 调整图片大小到指定的尺寸
    img = tf.image.resize(img, [img_height, img_width])
    # 5. 转置图片,使得时间维度对应图片的宽度
    img = tf.transpose(img, perm=[1, 0, 2])
    # 6. 将标签中的字符映射为数字
    label = char_to_num(tf.strings.unicode_split(label, input_encoding='UTF-8'))
    # 7. 返回字典,因为我们的模型期望两个输入
    return {"image": img, "label": label}

代码主要包含三个部分的功能:字符映射、数据集分割和样本编码。

  1. 字符映射

    • 代码首先创建了一个字符到数字的映射表,这使得模型能够将验证码中的字符转换为整数,以便进行数值运算。StringLookup层用于实现这一映射。

    • 接着,创建了一个反向映射表,将数字转换回原来的字符。这在模型训练完成后,将预测结果转换回可读字符时非常有用。

  2. 数据集分割

    • 定义了一个名为split_data的函数,它接受图片和标签列表,以及训练集所占的比例和是否打乱数据的标志。

    • 函数首先确定数据集的总大小,然后根据是否需要打乱数据来生成索引数组。

    • 计算训练集的大小,并根据这个大小将数据集分割为训练集和验证集。

    • 最后,函数返回分割后的训练集和验证集的图片和标签。

  3. 样本编码

    • 定义了一个名为encode_single_sample的函数,用于将单个样本(图片路径和标签)编码为模型可接受的格式。

    • 函数读取并解码图片文件,将其转换为灰度图,并归一化到[0, 1]区间。

    • 调整图片大小以符合模型输入要求,并进行转置,以确保时间维度与图片宽度对应。

    • 使用之前创建的字符到数字映射表,将标签中的字符转换为整数序列。

    • 返回一个字典,包含编码后的图片和标签,以供模型输入。

2.3.3. 创建数据集对象

通过创建数据集、编码样本、批处理和预取数据,为模型训练和验证提供了一个高效、可并行处理的数据流。这样做不仅优化了数据的读取和处理速度,还使得模型能够更好地利用硬件资源,加速训练和评估过程。

python 复制代码
import tensorflow as tf

# 将训练数据转换为TensorFlow数据集
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))

# 使用map函数并行对每个样本进行编码,使用encode_single_sample函数
# num_parallel_calls设置为AUTOTUNE,以自动调整并行调用的数量
# 然后按照批次大小进行批处理,最后使用prefetch来提高训练时的数据读取效率
train_dataset = (
    train_dataset.map(encode_single_sample, num_parallel_calls=tf.data.AUTOTUNE)
    .batch(batch_size)
    .prefetch(buffer_size=tf.data.AUTOTUNE)
)

# 将验证数据转换为TensorFlow数据集
validation_dataset = tf.data.Dataset.from_tensor_slices((x_valid, y_valid))

# 对验证数据集执行与训练数据集相同的操作
validation_dataset = (
    validation_dataset.map(encode_single_sample, num_parallel_calls=tf.data.AUTOTUNE)
    .batch(batch_size)
    .prefetch(buffer_size=tf.data.AUTOTUNE)
)

# 中文注释:
# 1. 使用from_tensor_slices方法将训练数据和标签组合成一个数据集。
# 2. 使用map方法对数据集中的每个元素应用encode_single_sample函数进行编码。
#    num_parallel_calls参数设置为AUTOTUNE,允许TensorFlow自动选择最优的并行数量。
# 3. 使用batch方法将编码后的数据分成指定大小的批次。
# 4. 使用prefetch方法预取数据,以减少训练过程中的数据读取延迟。

代码的功能是将训练数据和验证数据转换成适合TensorFlow模型训练和评估的数据流,具体步骤和功能如下:

  1. 数据集创建

    • 使用tf.data.Dataset.from_tensor_slices函数,将训练集和验证集的图片路径和标签分别封装成tf.data.Dataset对象。这是TensorFlow中处理数据集的标准方式,它允许对数据进行高效的批处理和迭代。
  2. 数据编码

    • 利用map方法和之前定义的encode_single_sample函数,对数据集中的每个样本进行编码。这个过程包括读取图片文件、解码、归一化、调整大小、转置以及字符映射等步骤。num_parallel_calls=tf.data.AUTOTUNE参数允许TensorFlow自动调整用于并行处理的资源数量,以优化性能。
  3. 批处理

    • 通过batch方法,将编码后的数据分成大小为batch_size的批次。批处理是深度学习中常见的做法,可以提高内存使用效率和计算速度。
  4. 预取

    • 使用prefetch方法预加载数据。预取可以在模型训练时提前加载下一批数据,减少CPU和GPU等待数据的时间,从而提高训练效率。buffer_size=tf.data.AUTOTUNE同样允许TensorFlow自动调整预取的缓冲区大小。
2.3.4. 数据可视化(Visualize the data)

数据可视化是指使用图形、图表、图像、动画、视频等方式将数据以更直观、易于理解的形式展现出来。通过数据可视化,人们可以更快速地洞察数据的模式和趋势,发现数据中的关联性和异常值,从而做出更有效的决策。

python 复制代码
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np

# 使用matplotlib创建一个4行4列的子图网格,并设置整个图像的大小为10x5英寸
_, ax = plt.subplots(4, 4, figsize=(10, 5))

# 从训练数据集中取出第一个批次的数据进行可视化
for batch in train_dataset.take(1):
    # 获取批次中的图片和标签
    images = batch["image"]
    labels = batch["label"]
    
    # 遍历批次中的16个样本
    for i in range(16):
        # 将图片数据格式化为灰度图,并转换为uint8类型,以便于显示
        img = (images[i] * 255).numpy().astype("uint8")
        # 将标签从整数序列转换回字符串形式
        label = tf.strings.reduce_join(num_to_char(labels[i])).numpy().decode("utf-8")
        # 在子图网格的相应位置显示图片
        ax[i // 4, i % 4].imshow(img[:, :, 0].T, cmap="gray")
        # 设置子图的标题为对应的标签
        ax[i // 4, i % 4].set_title(label)
        # 关闭子图的坐标轴显示
        ax[i // 4, i % 4].axis("off")

# 显示所有的子图
plt.show()

上述代码主要执行以下功能:

  • 首先,使用plt.subplots创建一个4x4的子图网格,用于可视化训练数据集中的图片。
  • 通过调用train_dataset.take(1),我们取出训练数据集的第一个批次,这通常包含多个样本。
  • 对于批次中的每个样本,首先将图片数据从浮点数格式转换为8位整数格式,以适应显示要求。
  • 使用tf.strings.reduce_joinnum_to_char将标签从数字序列转换回人类可读的字符串。
  • 然后,使用imshow函数在子图网格的相应位置显示每张图片,并通过set_title设置每张图片的标题为对应的标签。
  • 最后,使用axis("off")关闭坐标轴的显示,使图像更加清晰,然后调用plt.show()显示所有的子图。

2.4. 建立模型

以下代码定义了一个用于处理CTC(Connectionist Temporal Classification)损失的层,以及构建了一个OCR(Optical Character Recognition,光学字符识别)模型:

python 复制代码
import tensorflow as tf
from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, Reshape, Dense, Dropout, LSTM, Bidirectional
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam

# 定义CTC批量损失函数
def ctc_batch_cost(y_true, y_pred, input_length, label_length):
    # 将标签长度和输入长度转换为整型
    label_length = tf.cast(tf.squeeze(label_length, axis=-1), dtype=tf.int32)
    input_length = tf.cast(tf.squeeze(input_length, axis=-1), dtype=tf.int32)
    # 将密集标签转换为稀疏表示
    sparse_labels = tf.cast(ctc_label_dense_to_sparse(y_true, label_length), dtype=tf.int32)
    
    # 将预测值进行转置和对数变换,添加一个小的epsilon值避免对数为负无穷
    y_pred = tf.math.log(tf.transpose(y_pred, axes=[1, 0, 2]) + tf.keras.backend.epsilon())
    
    # 计算CTC损失并扩展维度
    return tf.expand_dims(
        tf.nn.ctc_loss(inputs=y_pred, labels=sparse_labels, sequence_length=input_length),
        1
    )

# 将密集标签转换为稀疏标签的函数
def ctc_label_dense_to_sparse(labels, label_lengths):
    # ... 略过函数内部实现,函数主要目的是将密集标签转换为稀疏表示 ...

    return tf.SparseTensor(indices=indices, values=vals_sparse, dense_shape=label_shape)

# 定义CTC层
class CTCLayer(tf.keras.layers.Layer):
    def __init__(self, name=None):
        super().__init__(name=name)
        self.loss_fn = ctc_batch_cost

    def call(self, y_true, y_pred):
        # 计算CTC损失并添加到层中
        batch_len = tf.cast(tf.shape(y_true)[0], dtype=tf.int64)
        input_length = tf.cast(tf.shape(y_pred)[1], dtype=tf.int64)
        label_length = tf.cast(tf.shape(y_true)[1], dtype=tf.int64)
        
        # ... 略过损失计算和添加损失的代码 ...

        return y_pred

# 构建OCR模型的函数
def build_model():
    # 定义输入层
    input_img = Input(shape=(img_width, img_height, 1), name="image", dtype="float32")
    labels = Input(name="label", shape=(None,), dtype="float32")
    
    # 构建卷积层、池化层、全连接层、RNN层等模型结构
    # ... 略过模型构建代码 ...

    # 添加CTC层计算损失
    output = CTCLayer(name="ctc_loss")(labels, x)

    # 定义模型
    model = Model(inputs=[input_img, labels], outputs=output, name="ocr_model_v1")
    
    # 定义优化器并编译模型
    opt = Adam()
    model.compile(optimizer=opt)
    
    return model

# 获取模型并打印模型概览
model = build_model()
model.summary()

代码的主要功能如下:

  • ctc_batch_cost 函数用于计算CTC损失,它接受真实标签、预测值、输入长度和标签长度作为参数。
  • ctc_label_dense_to_sparse 函数将密集标签转换为稀疏标签,以适应CTC损失函数的要求。
  • CTCLayer 类是一个自定义的Keras层,用于在训练时计算CTC损失,并在测试时返回预测值。
  • build_model 函数构建了一个OCR模型,包括卷积层、池化层、全连接层、RNN层和一个自定义的CTC层。
  • 最后,实例化模型并打印出模型的概览信息。
python 复制代码
Model: "ocr_model_v1"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ Layer (type)        ┃ Output Shape      ┃ Param # ┃ Connected to         ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ image (InputLayer)  │ (None, 200, 50,   │       0 │ -                    │
│                     │ 1)                │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ Conv1 (Conv2D)      │ (None, 200, 50,   │     320 │ image[0][0]          │
│                     │ 32)               │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ pool1               │ (None, 100, 25,   │       0 │ Conv1[0][0]          │
│ (MaxPooling2D)      │ 32)               │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ Conv2 (Conv2D)      │ (None, 100, 25,   │  18,496 │ pool1[0][0]          │
│                     │ 64)               │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ pool2               │ (None, 50, 12,    │       0 │ Conv2[0][0]          │
│ (MaxPooling2D)      │ 64)               │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ reshape (Reshape)   │ (None, 50, 768)   │       0 │ pool2[0][0]          │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ dense1 (Dense)      │ (None, 50, 64)    │  49,216 │ reshape[0][0]        │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ dropout (Dropout)   │ (None, 50, 64)    │       0 │ dense1[0][0]         │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ bidirectional       │ (None, 50, 256)   │ 197,632 │ dropout[0][0]        │
│ (Bidirectional)     │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ bidirectional_1     │ (None, 50, 128)   │ 164,352 │ bidirectional[0][0]  │
│ (Bidirectional)     │                   │         │                      │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ label (InputLayer)  │ (None, None)      │       0 │ -                    │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ dense2 (Dense)      │ (None, 50, 21)    │   2,709 │ bidirectional_1[0][... │
├─────────────────────┼───────────────────┼─────────┼──────────────────────┤
│ ctc_loss (CTCLayer) │ (None, 50, 21)    │       0 │ label[0][0],         │
│                     │                   │         │ dense2[0][0]         │
└─────────────────────┴───────────────────┴─────────┴──────────────────────┘
 Total params: 432,725 (1.65 MB)
 Trainable params: 432,725 (1.65 MB)
 Non-trainable params: 0 (0.00 B)

2.5 训练模型

python 复制代码
import tensorflow as tf
from tensorflow.keras.callbacks import EarlyStopping

# 设置训练的周期数(epochs)
epochs = 100

# 设置早停法(Early Stopping)的耐心参数,即在停止训练前允许的连续无改进周期数
early_stopping_patience = 10

# 实例化早停法回调,监控验证集损失(val_loss),
# 如果连续early_stopping_patience个epoch没有改善,则停止训练。
# 训练停止时,恢复到最佳权重状态
early_stopping = EarlyStopping(
    monitor="val_loss",
    patience=early_stopping_patience,
    restore_best_weights=True
)

# 训练模型
# 使用fit方法训练模型,并传入训练数据集和验证数据集
# 同时指定训练周期数和回调列表,其中包含早停法回调
history = model.fit(
    train_dataset,               # 训练数据集
    validation_data=validation_dataset,  # 验证数据集
    epochs=epochs,                # 训练周期数
    callbacks=[early_stopping]    # 训练回调列表,包含早停法
)

代码主要功能如下:

  • 设置了训练模型时使用的周期数epochs,表示整个数据集将被遍历多少次。
  • 定义了早停法的patience参数early_stopping_patience,它决定了在验证集损失停止下降时,允许模型继续训练的额外周期数。
  • 创建了一个EarlyStopping回调实例,用于在模型训练过程中监控val_loss。如果连续early_stopping_patience个周期没有改善,则终止训练,并恢复到最佳表现时的模型权重。
  • 使用model.fit方法开始训练过程,传入训练数据集train_dataset和验证数据集validation_dataset。同时指定了训练的周期数epochs和回调列表,其中包含了早停法回调。
  • history对象将记录训练过程中的历史信息,包括损失值和评估指标等,可用于后续的分析和模型性能评估。

2.6 推理预测

以下代码执行CTC解码功能,用于从模型的输出中获取解码文本,并展示了如何在一些验证样本上使用该功能:

python 复制代码
import numpy as np
import matplotlib.pyplot as plt

# CTC解码函数,支持贪婪解码和束搜索解码
def ctc_decode(y_pred, input_length, greedy=True, beam_width=100, top_paths=1):
    # 获取预测值的形状
    input_shape = tf.shape(y_pred)
    num_samples, num_steps = input_shape[0], input_shape[1]
    
    # 将预测值进行转置和对数变换,添加一个小的epsilon值避免对数为负无穷
    y_pred = tf.math.log(tf.transpose(y_pred, [1, 0, 2]) + tf.keras.backend.epsilon())
    input_length = tf.cast(input_length, dtype=tf.int32)

    # 根据解码方式不同,选择贪婪解码或束搜索解码
    if greedy:
        decoded, log_prob = tf.nn.ctc_greedy_decoder(
            inputs=y_pred, sequence_length=input_length
        )
    else:
        decoded, log_prob = tf.nn.ctc_beam_search_decoder(
            inputs=y_pred,
            sequence_length=input_length,
            beam_width=beam_width,
            top_paths=top_paths,
        )
    
    # 将解码结果从稀疏张量转换为密集张量
    decoded_dense = []
    for st in decoded:
        st = tf.SparseTensor(st.indices, st.values, (num_samples, num_steps))
        decoded_dense.append(tf.sparse.to_dense(st, default_value=-1))
    return decoded_dense, log_prob

# 提取模型的预测部分,直到输出层
prediction_model = tf.keras.models.Model(
    model.input[0], model.get_layer(name="dense2").output
)
prediction_model.summary()

# 解码批次预测结果的辅助函数
def decode_batch_predictions(pred):
    input_len = np.ones(pred.shape[0]) * pred.shape[1]
    # 使用贪婪搜索解码
    results = ctc_decode(pred, input_length=input_len, greedy=True)[0][0][:, :max_length]
    # 迭代结果并转换回文本
    output_text = []
    for res in results:
        res = tf.strings.reduce_join(num_to_char(res)).numpy().decode("utf-8")
        output_text.append(res)
    return output_text

# 检查一些验证样本上的结果
for batch in validation_dataset.take(1):
    batch_images = batch["image"]
    batch_labels = batch["label"]

    preds = prediction_model.predict(batch_images)
    pred_texts = decode_batch_predictions(preds)

    orig_texts = []
    for label in batch_labels:
        label = tf.strings.reduce_join(num_to_char(label)).numpy().decode("utf-8")
        orig_texts.append(label)

    _, ax = plt.subplots(4, 4, figsize=(15, 5))
    for i in range(len(pred_texts)):
        img = (batch_images[i, :, :, 0] * 255).numpy().astype(np.uint8)
        img = img.T
        title = f"Prediction: {pred_texts[i]}"
        ax[i // 4, i % 4].imshow(img, cmap="gray")
        ax[i // 4, i % 4].set_title(title)
        ax[i // 4, i % 4].axis("off")
plt.show()

代码的主要功能:

  • ctc_decode 函数实现了CTC解码,支持贪婪解码和束搜索解码两种方式。
  • 首先,对模型的输出进行转置和对数变换,然后根据选择的解码方式进行解码。
  • 解码后的结果为稀疏张量,将其转换为密集张量以便于后续处理。
  • decode_batch_predictions 函数用于解码模型预测结果,将其转换为可读的文本。
  • validation_dataset中取出一个批次的样本,使用prediction_model进行预测,并调用decode_batch_predictions函数获取预测文本。
  • 使用matplotlib库展示一些验证样本的图像和对应的预测文本。
python 复制代码
Model: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape              ┃    Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ image (InputLayer)              │ (None, 200, 50, 1)        │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ Conv1 (Conv2D)                  │ (None, 200, 50, 32)       │        320 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ pool1 (MaxPooling2D)            │ (None, 100, 25, 32)       │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ Conv2 (Conv2D)                  │ (None, 100, 25, 64)       │     18,496 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ pool2 (MaxPooling2D)            │ (None, 50, 12, 64)        │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ reshape (Reshape)               │ (None, 50, 768)           │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense1 (Dense)                  │ (None, 50, 64)            │     49,216 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dropout (Dropout)               │ (None, 50, 64)            │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ bidirectional (Bidirectional)   │ (None, 50, 256)           │    197,632 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ bidirectional_1 (Bidirectional) │ (None, 50, 128)           │    164,352 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense2 (Dense)                  │ (None, 50, 21)            │      2,709 │
└─────────────────────────────────┴───────────────────────────┴────────────┘
 Total params: 432,725 (1.65 MB)
 Trainable params: 432,725 (1.65 MB)
 Non-trainable params: 0 (0.00 B)

3. 总结和展望

3.1 总结

在本篇博文中,我们全面探讨了OCR技术在验证码识别中的应用与发展。首先,我们介绍了OCR技术的研究背景,包括它在提升用户体验、保障系统安全和适应不断增长的网络安全需求方面的重要性。接着,我们概述了OCR模型的工作原理,从图像预处理到字符识别的整个流程,并讨论了OCR技术在不同领域的应用潜力。

其次,博文详细介绍了构建和训练OCR模型的过程。这包括了数据预处理、模型构建、设置训练环境、模型训练以及使用早停法等策略来优化模型性能。我们特别强调了CTC损失层在处理序列识别问题中的作用,并展示了如何使用TensorFlow和Keras框架来实现这一过程。

最后,我们讨论了模型的推理预测阶段,包括CTC解码的实现和预测结果的后处理。通过可视化工具,我们展示了模型对验证码图像的识别效果,并提供了对模型性能的直观评估。此外,我们总结了OCR技术在验证码识别领域的现状和未来发展方向,强调了技术的不断进步和适应性,以及在实际应用中提高模型鲁棒性和可解释性的重要性。

总而言之,本博文为读者提供了一个关于OCR验证码识别技术的深入指南,从理论基础到实践应用,再到未来展望,全面覆盖了该领域的各个方面。随着技术的不断发展,OCR技术将在网络安全和用户体验提升方面发挥更加关键的作用。

3.2.展望

在展望OCR验证码识别技术的未来时,我们可以看到几个明显的趋势。首先,技术的持续进步将推动模型变得更加精准和智能。深度学习的最新发展,如新型网络结构和优化算法,将进一步提升模型的识别准确率,尤其是在处理复杂和模糊的验证码图像时。此外,多模态学习和自适应能力的增强将使模型能够更好地泛化到新的验证码样式,同时提高模型对不同环境变化的适应性。

其次,随着对实时响应和高效能需求的增长,OCR模型的推理速度和资源效率将得到显著优化。这不仅将提高用户体验,也将使OCR技术能够满足更多实时应用场景的需求。同时,安全性将继续作为技术开发的重点,研究者们将致力于提高模型对恶意攻击的防御能力,并确保算法的决策过程是透明和可解释的。

最后,随着技术应用的扩展,OCR验证码识别技术将面临更多的伦理和法律挑战。数据隐私、算法偏见和透明度问题将推动制定更加严格的法律法规,以确保技术的安全和公正使用。同时,开源社区和标准化组织将在推动技术共享、标准化和普及方面发挥关键作用,促进技术的健康发展和广泛应用。总而言之,OCR技术的未来将是一个技术创新与社会责任并重的时代,它将在验证码识别以及其他领域发挥更加关键的作用。

参考文献

[1]Nain, A. K. (2020, June 14). How to implement an OCR model using CNNs, RNNs, and CTC loss. Keras Documentation. Retrieved March 13, 2024, from https://keras.io/examples/vision/captcha_ocr/

示例代码

python 复制代码
# 导入必要的库
import os
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
import tensorflow as tf
import keras
from keras import ops
from keras import layers

# 设置Keras的后端为TensorFlow
os.environ["KERAS_BACKEND"] = "tensorflow"

## 加载数据:验证码图片
# 下载数据集
!curl -LO https://github.com/AakashKumarNain/CaptchaCracker/raw/master/captcha_images_v2.zip
!unzip -qq captcha_images_v2.zip

# 数据预处理
# 设置数据目录路径
data_dir = Path("./captcha_images_v2/")

# 获取所有图片文件并排序
images = sorted(list(map(str, list(data_dir.glob("*.png")))))
# 从图片路径中提取标签
labels = [img.split(os.path.sep)[-1].split(".png")[0] for img in images]
# 提取所有唯一字符并排序
characters = sorted(set(char for label in labels for char in label))

# 打印数据集统计信息
print("找到的图片数量: ", len(images))
print("找到的标签数量: ", len(labels))
print("找到的唯一字符数量: ", len(characters))
print("存在的字符: ", characters)

# 设置训练和验证时的批次大小
batch_size = 16

# 设置期望的图片尺寸
img_width = 200
img_height = 50

# 创建字符到整数的映射
char_to_num = layers.StringLookup(vocabulary=list(characters), mask_token=None)
# 创建整数回原字符的映射
num_to_char = layers.StringLookup(vocabulary=char_to_num.get_vocabulary(), mask_token=None, invert=True)

# 定义分割数据集的函数
def split_data(images, labels, train_size=0.9, shuffle=True):
    size = len(images)
    indices = tf.range(size)
    if shuffle:
        indices = keras.random.shuffle(indices)
    train_samples = int(size * train_size)
    x_train, y_train = images[indices[:train_samples]], labels[indices[:train_samples]]
    x_valid, y_valid = images[indices[train_samples:]], labels[indices[train_samples:]]
    return x_train, x_valid, y_train, y_valid

# 编码单个样本的函数
def encode_single_sample(img_path, label):
    img = tf.io.read_file(img_path)
    img = tf.io.decode_png(img, channels=1)
    img = tf.image.convert_image_dtype(img, tf.float32)
    img = tf.image.resize(img, [img_height, img_width])
    img = tf.transpose(img, perm=[1, 0, 2])
    label = char_to_num(tf.strings.unicode_split(label, input_encoding="UTF-8"))
    return {"image": img, "label": label}

# 创建数据集对象
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = (
    train_dataset.map(encode_single_sample, num_parallel_calls=tf.data.AUTOTUNE)
    .batch(batch_size)
    .prefetch(buffer_size=tf.data.AUTOTUNE)
)

validation_dataset = tf.data.Dataset.from_tensor_slices((x_valid, y_valid))
validation_dataset = (
    validation_dataset.map(encode_single_sample, num_parallel_calls=tf.data.AUTOTUNE)
    .batch(batch_size)
    .prefetch(buffer_size=tf.data.AUTOTUNE)
)

# 可视化数据
_, ax = plt.subplots(4, 4, figsize=(10, 5))
for batch in train_dataset.take(1):
    images = batch["image"]
    labels = batch["label"]
    for i in range(16):
        img = (images[i] * 255).numpy().astype("uint8")
        label = tf.strings.reduce_join(num_to_char(labels[i])).numpy().decode("utf-8")
        ax[i // 4, i % 4].imshow(img[:, :, 0].T, cmap="gray")
        ax[i // 4, i % 4].set_title(label)
        ax[i // 4, i % 4].axis("off")
plt.show()

# 定义CTC批量损失函数和CTC层
def ctc_batch_cost(y_true, y_pred, input_length, label_length):
    # ... 函数实现 ...

class CTCLayer(layers.Layer):
    # ... 类实现 ...

# 构建模型
def build_model():
    # ... 模型构建代码 ...
    return model

# 获取模型并打印模型概览
model = build_model()
model.summary()

# 训练模型
epochs = 100
early_stopping_patience = 10
early_stopping = keras.callbacks.EarlyStopping(monitor="val_loss", patience=early_stopping_patience, restore_best_weights=True)
history = model.fit(
    train_dataset,
    validation_data=validation_dataset,
    epochs=epochs,
    callbacks=[early_stopping],
)

# 推断
def ctc_decode(y_pred, input_length, greedy=True, beam_width=100, top_paths=1):
    # ... 函数实现 ...

# 获取预测模型
prediction_model = keras.models.Model(model.input[0], model.get_layer(name="dense2").output)
prediction_model.summary()

# 解码批次预测结果的辅助函数
def decode_batch_predictions(pred):
    # ... 函数实现 ...

# 检查一些验证样本上的结果
for batch in validation_dataset.take(1):
    # ... 检查和显示预测结果的代码 ...

plt.show()
# 构建模型的函数
def build_model():
    # 定义模型输入
    input_img = layers.Input(shape=(img_width, img_height, 1), name="image", dtype="float32")
    labels = layers.Input(name="label", shape=(None,), dtype="float32")

    # 构建第一个卷积块
    x = layers.Conv2D(32, (3, 3), activation='relu', 
                      kernel_initializer='he_normal', padding='same', name='Conv1')(input_img)
    x = layers.MaxPooling2D((2, 2), name='pool1')(x)

    # 构建第二个卷积块
    x = layers.Conv2D(64, (3, 3), activation='relu', 
                      kernel_initializer='he_normal', padding='same', name='Conv2')(x)
    x = layers.MaxPooling2D((2, 2), name='pool2')(x)

    # 调整输出形状以适应RNN部分
    new_shape = ((img_width // 4), (img_height // 4) * 64)
    x = layers.Reshape(target_shape=new_shape, name="reshape")(x)
    x = layers.Dense(64, activation='relu', name="dense1")(x)
    x = layers.Dropout(0.2)(x)

    # 堆叠双向LSTM层
    x = layers.Bidirectional(layers.LSTM(128, return_sequences=True, dropout=0.25))(x)
    x = layers.Bidirectional(layers.LSTM(64, return_sequences=True, dropout=0.25))(x)

    # 输出层,使用softmax激活函数
    x = layers.Dense(len(char_to_num.get_vocabulary()) + 1, activation="softmax", name="dense2")(x)

    # 添加CTC层以计算损失
    output = CTCLayer(name="ctc_loss")(labels, x)

    # 定义模型
    model = keras.models.Model(inputs=[input_img, labels], outputs=output, name="ocr_model_v1")
    
    # 编译模型
    optimizer = keras.optimizers.Adam()
    model.compile(optimizer=optimizer)
    
    return model

# 获取模型实例并打印概览
model = build_model()
model.summary()

# 训练模型
epochs = 100  # 训练周期数
early_stopping_patience = 10  # 早停法的耐心参数

# 实例化早停法回调
early_stopping = keras.callbacks.EarlyStopping(
    monitor="val_loss", 
    patience=early_stopping_patience, 
    restore_best_weights=True
)

# 训练模型
history = model.fit(
    train_dataset,             # 训练数据集
    validation_data=validation_dataset,  # 验证数据集
    epochs=epochs,              # 训练周期数
    callbacks=[early_stopping]   # 训练回调列表,包含早停法
)


def ctc_decode(y_pred, input_length, greedy=True, beam_width=100, top_paths=1):
    # 转置并应用对数变换到预测值
    y_pred = tf.math.log(tf.transpose(y_pred, [1, 0, 2]) + keras.backend.epsilon())
    input_length = tf.cast(input_length, dtype=tf.int32)

    # 贪婪解码或束搜索解码
    if greedy:
        decoded, log_prob = tf.nn.ctc_greedy_decoder(y_pred, input_length)
    else:
        decoded, log_prob = tf.nn.ctc_beam_search_decoder(
            y_pred, 
            input_length, 
            beam_width=beam_width, 
            top_paths=top_paths
        )
    
    # 将解码结果转换为密集张量
    decoded_dense = [tf.sparse.to_dense(sp_input=st, default_value=-1) for st in decoded]
    
    return decoded_dense, log_prob

# 获取预测模型
prediction_model = keras.models.Model(model.input[0], model.get_layer(name="dense2").output)
prediction_model.summary()

# 解码网络输出的辅助函数
def decode_batch_predictions(pred):
    input_len = np.ones(pred.shape[0]) * pred.shape[1]
    results = ctc_decode(pred, input_length=input_len, greedy=True)[0][0][:, :max_length]
    
    output_text = []
    for res in results:
        res = tf.strings.reduce_join(num_to_char(res)).numpy().decode("utf-8")
        output_text.append(res)
    
    return output_text

# 检查验证样本上的结果
for batch in validation_dataset.take(1):
    batch_images = batch["image"]
    batch_labels = batch["label"]

    preds = prediction_model.predict(batch_images)
    pred_texts = decode_batch_predictions(preds)

    orig_texts = [tf.strings.reduce_join(num_to_char(label)).numpy().decode("utf-8") for label in batch_labels]

    # 可视化一些验证样本及其预测结果
    fig, ax = plt.subplots(4, 4, figsize=(15, 5))
    for i, (img, title) in enumerate(zip(batch_images, pred_texts)):
        img = (img[:, :, 0] * 255).numpy().astype(np.uint8).T
        ax[i // 4, i % 4].imshow(img, cmap="gray")
        ax[i // 4, i % 4].set_title(title)
        ax[i // 4, i % 4].axis("off")
    plt.show()
相关推荐
迅易科技37 分钟前
借助腾讯云质检平台的新范式,做工业制造企业质检的“AI慧眼”
人工智能·视觉检测·制造
古希腊掌管学习的神2 小时前
[机器学习]XGBoost(3)——确定树的结构
人工智能·机器学习
ZHOU_WUYI2 小时前
4.metagpt中的软件公司智能体 (ProjectManager 角色)
人工智能·metagpt
靴子学长3 小时前
基于字节大模型的论文翻译(含免费源码)
人工智能·深度学习·nlp
AI_NEW_COME4 小时前
知识库管理系统可扩展性深度测评
人工智能
海棠AI实验室4 小时前
AI的进阶之路:从机器学习到深度学习的演变(一)
人工智能·深度学习·机器学习
hunteritself4 小时前
AI Weekly『12月16-22日』:OpenAI公布o3,谷歌发布首个推理模型,GitHub Copilot免费版上线!
人工智能·gpt·chatgpt·github·openai·copilot
IT古董5 小时前
【机器学习】机器学习的基本分类-强化学习-策略梯度(Policy Gradient,PG)
人工智能·机器学习·分类
centurysee5 小时前
【最佳实践】Anthropic:Agentic系统实践案例
人工智能
mahuifa5 小时前
混合开发环境---使用编程AI辅助开发Qt
人工智能·vscode·qt·qtcreator·编程ai