不定长图文模型训练

news2025/1/9 15:15:25

文章目录

  • 生成数据集
  • 模型选择
  • 计算均值和标准差
  • 训练代码
  • 测试集测试

生成数据集

import os
import random
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from io import BytesIO
import time


def main():
    _first_num = random.randint(1, 1000)
    _code_style = ['加', '减', '乘', '+', '-', '*']
    _last_num = random.randint(1, 1000)
    init_chars = [str(_first_num), random.choices(_code_style)[0], str(_last_num)]

    def create_validate_code(size=(150, 50),
                             chars=init_chars,
                             img_type="PNG",
                             mode="RGB",
                             bg_color=(255, 255, 255),
                             fg_color=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)),
                             font_size=18,
                             font_type="./msyh.ttc",
                             draw_lines=True,
                             n_line=(1, 2),
                             draw_points=True,
                             point_chance=1):
        """
        @todo: 生成验证码图片
        @param size: 图片的大小,格式(宽,高),默认为(120, 30)
        @param chars: 允许的字符集合,格式字符串
        @param img_type: 图片保存的格式,默认为GIF,可选的为GIF,JPEG,TIFF,PNG
        @param mode: 图片模式,默认为RGB
        @param bg_color: 背景颜色,默认为白色
        @param fg_color: 前景色,验证码字符颜色,默认为蓝色#0000FF
        @param font_size: 验证码字体大小
        @param font_type: 验证码字体
        @param length: 验证码字符个数
        @param draw_lines: 是否划干扰线
        @param n_lines: 干扰线的条数范围,格式元组,默认为(1, 2),只有draw_lines为True时有效
        @param draw_points: 是否画干扰点
        @param point_chance: 干扰点出现的概率,大小范围[0, 100]
        @return: [0]: PIL Image实例
        @return: [1]: 验证码图片中的字符串
        """

        width, height = size  # 宽高
        # 创建图形
        img = Image.new(mode, size, bg_color)
        draw = ImageDraw.Draw(img)  # 创建画笔

        def get_chars():
            return chars

        def create_lines():
            """绘制干扰线"""
            line_num = random.randint(*n_line)  # 干扰线条数

            for i in range(line_num):
                # 起始点
                begin = (random.randint(0, size[0]), random.randint(0, size[1]))
                # 结束点
                end = (random.randint(0, size[0]), random.randint(0, size[1]))
                draw.line([begin, end], fill=(0, 0, 0))

        def create_points():
            """绘制干扰点"""
            chance = min(100, max(0, int(point_chance)))  # 大小限制在[0, 100]

            for w in range(width):
                for h in range(height):
                    tmp = random.randint(0, 100)
                    if tmp > 100 - chance:
                        draw.point((w, h), fill=(0, 0, 0))

        def create_strs():
            """绘制验证码字符"""
            c_chars = get_chars()
            strs = ' %s ' % ' '.join(c_chars)  # 每个字符前后以空格隔开

            font = ImageFont.truetype(font_type, font_size)
            font_width, font_height = font.getsize(strs)
            font_width /= 0.7
            font_height /= 0.7
            draw.text(((width - font_width) / 3, (height - font_height) / 3),
                      strs, font=font, fill=fg_color)

            return ''.join(c_chars)

        if draw_lines:
            create_lines()
        if draw_points:
            create_points()
        strs = create_strs()

        # 图形扭曲参数
        params = [1 - float(random.randint(1, 2)) / 80,
                  0,
                  0,
                  0,
                  1 - float(random.randint(1, 10)) / 80,
                  float(random.randint(3, 5)) / 450,
                  0.001,
                  float(random.randint(3, 5)) / 450
                  ]
        img = img.transform(size, Image.PERSPECTIVE, params)  # 创建扭曲
        output_buffer = BytesIO()
        img.save(output_buffer, format='PNG')
        img_byte_data = output_buffer.getvalue()
        # img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)  # 滤镜,边界加强(阈值更大)
        return img_byte_data, strs
    res = create_validate_code()
    return res


main()
# try:
#     os.mkdir('./训练图片生成')
# except FileExistsError:
#     print('训练图片生成 文件夹已经存在')
# print('生成存储文件夹成功')
while 1:
    number = input('请输入要生成的验证码数量:')
    try:
        for i in range(int(number)):
            res = main()
            with open('./picture/{0}_{1}.png'.format(res[1].replace('*', '乘'), int(time.time())), 'wb') as f:
            # with open('./test/{0}_{1}.png'.format(res[1].replace('*', '乘'), int(time.time())), 'wb') as f:
                f.write(res[0])
            print('生成第', i+1, '个图片成功')
    except ValueError:
        print('请输入一个数字,不要输入乱七八糟的东西,打你哦')
    except:
        import traceback
        traceback.print_exc()
        break
    input('理论上生成完成了~,QAQ 共生成了' + number + '个验证码')
input('出现未知错误,错误已打印')

先创建picture和test目录,picture作为训练数据集目录 test作为测试数据集目录

先用以上程序生成3000张训练数据图片集:

在这里插入图片描述

再生成200张测试数据集:

在这里插入图片描述

模型选择

本次我们采用一种基于长短期记忆网络(LSTM)和残差网络(ResNet)相融合的网络模型,模型代码实现:

import torch
import torch.nn as nn
from torch.nn import functional as F


class RestNetBasicBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride):
        super(RestNetBasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        output = self.conv1(x)
        output = F.relu(self.bn1(output))
        output = self.conv2(output)
        output = self.bn2(output)
        return F.relu(x + output)


class RestNetDownBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride):
        super(RestNetDownBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride[0], padding=1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride[1], padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.extra = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride[0], padding=0),
            nn.BatchNorm2d(out_channels)
        )

    def forward(self, x):
        extra_x = self.extra(x)
        output = self.conv1(x)
        out = F.relu(self.bn1(output))

        out = self.conv2(out)
        out = self.bn2(out)
        return F.relu(extra_x + out)

class ResNetLstm_shape(nn.Module):
    def __init__(self):
        super(ResNetLstm_shape, self).__init__()
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
        self.bn1 = nn.BatchNorm2d(64)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.layer1 = nn.Sequential(RestNetBasicBlock(64, 64, 1),
                                    RestNetBasicBlock(64, 64, 1))

        self.layer2 = nn.Sequential(RestNetDownBlock(64, 128, [2, 1]),
                                    RestNetBasicBlock(128, 128, 1))

        self.layer3 = nn.Sequential(RestNetDownBlock(128, 256, [2, 1]),
                                    RestNetBasicBlock(256, 256, 1))


    def forward(self, x):
        out = self.conv1(x)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)   # [2, 256, 7, 19]  [batch, layer, h, w]   []
        return out.shape


class ResNetLstm(nn.Module):
    def __init__(self, image_shape):
        super(ResNetLstm, self).__init__()
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
        self.bn1 = nn.BatchNorm2d(64)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.layer1 = nn.Sequential(RestNetBasicBlock(64, 64, 1),
                                    RestNetBasicBlock(64, 64, 1))

        self.layer2 = nn.Sequential(RestNetDownBlock(64, 128, [2, 1]),
                                    RestNetBasicBlock(128, 128, 1))

        self.layer3 = nn.Sequential(RestNetDownBlock(128, 256, [2, 1]),
                                    RestNetBasicBlock(256, 256, 1))
        x = torch.zeros((1, 3) + image_shape)
        size = ResNetLstm_shape()(x)
        input_size = size[1] * size[2]
        self.lstm = nn.LSTM(input_size=input_size, hidden_size=input_size, num_layers=1, bidirectional=True)
        self.fc = nn.Linear(input_size * 2, 17)

    def forward(self, x):
        out = self.conv1(x)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)   # [2, 256, 7, 19]  [batch, layer, h, w]   []
        out = out.permute(3, 0, 1, 2)    # [19, 2, 256, 7]
        out_shape = out.shape
        out = out.view(out_shape[0], out_shape[1], out_shape[2]*out_shape[3])   # [19, 2, 256*7]
        out, _ = self.lstm(out)
        # print(out.shape)   # [19, 2, 3584] [w, b, h]
        out_shape = out.shape
        out = out.view(out_shape[0]*out_shape[1], out_shape[2])
        out = self.fc(out)
        out = out.view(out_shape[0], out_shape[1], -1)
        return out

计算均值和标准差

from torch.utils.data import Dataset
import os
from PIL import Image
import torch
from tqdm import tqdm
import numpy as np
from torchvision import transforms


class Letter2Dataset(Dataset):
    def __init__(self, root: str, transform=None):
        super(Letter2Dataset, self).__init__()
        self.path = root
        self.transform = transform
        # 可优化
        self.mapping = [i for i in '_0123456789加减乘+-*']

    def load_picture_path(self):
        picture_list = list(os.walk(self.path))[0][-1]
        # 这里可以增加很多的错误判断
        return picture_list

    def __len__(self):
        return len(self.load_picture_path())

    def __getitem__(self, item):
        load_picture = self.load_picture_path()
        image = Image.open(self.path + '/' + load_picture[item])
        if self.transform:
            image = self.transform(image)
        labels = [self.mapping.index(i) for i in load_picture[item].split('_')[0]]
        for i in range(9 - len(labels)):
            labels.insert(0, 0)
        labels = torch.as_tensor(labels, dtype=torch.int64)
        return image, labels, len(labels)

    def slice(self, start, end, step=1):
        load_picture = self.load_picture_path()
        images = []
        for i in range(start, end, step):
            image = Image.open(self.path + '/' + load_picture[i])
            if self.transform:
                image = self.transform(image)
            images.append(image.numpy())
        images = torch.Tensor(images)
        return images


# 获取均值和标准差 效率较低速度较慢建议使用第2种
def get_ms():
    transform = transforms.Compose([transforms.ToTensor(), ])
    my_train = Letter2Dataset(root="./picture", transform=transform)
    total_mean = [[], [], []]
    total_std = [[], [], []]
    res_total = [0, 0, 0]
    res_std = [0, 0, 0]
    for i in tqdm(range(len(my_train))):
        for j in range(len(total_std)):
            total_mean[j].append([np.array(my_train[i][0][j])])
            total_std[j].append([np.array(my_train[i][0][j])])

    for i in range(len(total_std)):
        res_total[i] = np.mean(total_mean[i])
        res_std[i] = np.std(total_std[i])
    return res_total, res_std


# 获取均值和标准差
def get_ms2():
    transform = transforms.Compose([transforms.ToTensor(), ])
    my_train = Letter2Dataset(root="./picture", transform=transform)
    a = my_train.slice(0, len(my_train))
    b = a.mean(dim=(2, 3), keepdim=True)
    b = b.mean(dim=0, keepdim=True)
    c = a.std(dim=(0, 2, 3), keepdim=True)
    res_total = b.reshape(-1).tolist()
    res_std = c.reshape(-1).tolist()
    return res_total, res_std


if __name__ == '__main__':
    res_total, res_std = get_ms()
    print(res_total, res_std)
    print('==' * 2)
    res_total, res_std = get_ms2()
    print(res_total, res_std)


在这里插入图片描述

训练代码

from torch import save, load
from test_p2 import test
from torchvision import transforms
from torch.utils.data import DataLoader
from torch import nn
from torch import optim
from tqdm import tqdm
from model import ResNetLstm
from MyDataset import get_ms,Letter2Dataset
import os
import numpy as np
import torch
res_total, res_std = get_ms2()

print(res_total, res_std)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(device)
# 实例化模型
size = (50, 150)
model = ResNetLstm(size)
model = model.to(device)
optimizer = optim.Adam(model.parameters())
batch_size = 16
# 加载已经训练好的模型和优化器继续进行训练
if os.path.exists('./models/model.pkl'):
    model.load_state_dict(load("./models/model.pkl"))
    optimizer.load_state_dict(load("./models/optimizer.pkl"))

loss_function = nn.CTCLoss()
my_transforms = transforms.Compose(
    [
        transforms.ToTensor(),
        transforms.Normalize(mean=res_total, std=res_std)
    ]
)
mnist_train = Letter2Dataset(root="./picture", transform=my_transforms)
def train(epoch):
    total_loss = []
    dataloader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)
    dataloader = tqdm(dataloader, total=len(dataloader))
    model.train()
    for images, labels, labels_lengths in dataloader:
        images = images.to(device)
        labels = labels.to(device)
        # 梯度置0
        optimizer.zero_grad()
        # 前向传播
        output = model(images)
        # 通过结果计算损失

        input_lengths = torch.IntTensor([output.shape[0]]*output.shape[1])
        # print(input_lengths.shape)
        loss = loss_function(output, labels, input_lengths, labels_lengths)
        total_loss.append(loss.item())
        dataloader.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')
    # 打印一下训练成功率, test.test_success()
    print('第{}个epoch,成功率, 损失为{}'.format(epoch, np.mean(total_loss)))

for i in range(15):
    train(i)
    print(test())
    

刚开始跑了14轮,成功率只有80多,后面又跑了十几轮就达到97左右了

在这里插入图片描述

测试集测试

from torch import save, load
import torch
from torchvision import transforms
from torch.utils.data import DataLoader
from torch import optim
from tqdm import tqdm
import os
import numpy as np
from model import ResNetLstm
import itertools
from MyDataset import get_ms,Letter2Dataset
res_total, res_std = get_ms()

mapping = [i for i in '_0123456789加减乘+-*']
def test():
    # 实例化模型
    model = ResNetLstm((50, 150))
    optimizer = optim.Adam(model.parameters())
    batch_size = 16
    # 加载已经训练好的模型和优化器继续进行训练
    if os.path.exists('./models/model.pkl'):
        model.load_state_dict(load("./models/model.pkl"))
        optimizer.load_state_dict(load("./models/optimizer.pkl"))
    my_transforms = transforms.Compose(
        [
            transforms.ToTensor(),
            transforms.Normalize(mean=res_total, std=res_std)
        ]
    )
    mnist_train = Letter2Dataset(root="./test", transform=my_transforms)
    success = 0
    total = 0
    dataloader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)
    dataloader = tqdm(dataloader, total=len(dataloader))
    model.eval()
    with torch.no_grad():
        for images, labels, _ in dataloader:
            output = model(images)
            # 通过结果计算损失
            output = output.permute(1, 0, 2)  # [2 19 17]
            for i in range(output.shape[0]):
                output_result = output[i, :, :]
                output_result = output_result.max(-1)[-1]
                labels_s = [mapping[i] for i in labels[i].cpu().numpy() if mapping[i] != '_']
                output_s = [mapping[i[0]] for i in itertools.groupby(output_result.cpu().numpy()) if i[0] != 0]
                # print('lab-->',labels_s)
                # print('out-->',output_s)
                if labels_s == output_s:
                    success += 1
                total += 1

    return success/total

if __name__ == '__main__':
    print(test())

在这里插入图片描述
测试了200张图片,成功率达97%。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/704501.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【uniapp开发小程序】实现粘贴一段文字后,自动识别到姓名/手机号/收货地址

一、需求 在uni-app中开发小程序&#xff0c;实现粘贴一段文字后自动识别到手机号&#xff0c;并将手机号前面的内容作为姓名&#xff0c;手机号后面的内容作为收货地址&#xff0c;并去除其中的特殊字符和前缀标识。 实现效果&#xff1a; 二、实现方式&#xff1a; <…

【vue】vue.js中引入组件

目录 ⭐️一、点击按钮1弹出弹窗⭐️二、vue.js引入组件具体步骤1、创建自定义组件的文件夹&#xff08;以弹窗组件为例&#xff09;2、在index.vue中引入keyProductsTip.vue模块3、在index.vue中引入组件4、在index.vue中使用组件&#xff0c;点击按钮打开弹窗5、index.vue中的…

高级web前端开发工程师的主要职责模板(合集)

高级web前端开发工程师的主要职责模板1 职责&#xff1a; 1、web端页面的制作、开发和优化; 2、编写静态和动态页面和交互、特效等功能的脚本程序; 3、开发基于HTML5技术的可灵活定制、可扩展的前端UI组件; 4、优化前端架构&#xff0c;提高系统的灵活性和可扩展性; 5、开…

【AUTOSAR】BMS开发实际项目讲解(二十六)----电池管理系统低压上下电功能

低压上下电功能 关联的系统需求 Sys_Req_3101、Sys_Req_3102、Sys_Req_3103、Sys_Req_3104; 功能实现描述 低压上电管理 ID Description ASIL Ref. LVM-101 当系统检测到如下任一信号有效时&#xff1a; 整车CAN、ACC、IGN、CC、CP唤醒、一路预留硬线唤醒系统应…

Java安全——存取控制器

Java安全 存取控制器 Java安全中的存取控制器是一种技术&#xff0c;用于控制访问应用程序中的资源。它的基本思想是允许或拒绝特定用户对系统资源的访问。存取控制器包括四个关键部分: 主体(subject), 权限(permission), 对象(object)和存取控制策略(access control policy)。…

【vue3】学生管理案例

此案例可以分为4个部分&#xff1a; 渲染学生列表新增学生删除学生搜索学生 涉及的知识点主要为v-model双向绑定数据。 页面&#xff1a; <div id"main"><table><tr><td>学号</td><td>姓名</td><td>新增时间<…

121.【ElasticSearch伪京东搜索】

模仿京东搜索 (一)、搭建环境0.启动ElasticSearch和head和kblian(1).启动EslaticSearch (9200)(2).启动Es-head (9101)(3).启动 Kibana (5602) 1.项目依赖2.启动测试 (二)、爬虫1.数据从哪里获取2.导入爬虫的依赖3.编写爬虫工具类(1).实体类(2).工具类编写 4.导入配置类 (三)、…

Selenium系列(四) - 详细解读鼠标操作

引入HTML页面 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>测试笔记</title> </head> <body><a>用户名:</a> <input id"username" class"userna…

航空枢纽联通亚欧,开放合作互利共赢 —乌鲁木齐国际航空枢纽建设论坛将于7月6日召开

为创新开放型经济体制&#xff0c;加快建设对外开放大通道&#xff0c;更好利用国际国内两个市场、两种资源&#xff0c;积极服务和融入双循环新发展格局&#xff0c;促进经济高质量发展&#xff0c;2023年7月6日-7日&#xff0c;以“航空枢纽联通亚欧&#xff0c;开放合作互利…

基于matlab使用单眼摄像机图像数据构建室内环境地图并估计摄像机的轨迹(附源码)

一、前言 视觉同步定位和映射 &#xff08;vSLAM&#xff09; 是指计算摄像机相对于周围环境的位置和方向&#xff0c;同时映射环境的过程。该过程仅使用来自相机的视觉输入。vSLAM 的应用包括增强现实、机器人和自动驾驶。 此示例演示如何处理来自单眼摄像机的图像数据&…

从小白到大神之路之学习运维第50天---第三阶段----MMM高可用集群数据库的安装部署

第三阶段基础 时 间&#xff1a;2023年6月30日 参加人&#xff1a;全班人员 内 容&#xff1a; Mysql---MMM高可用集群架构 目录 一、MMM介绍 二、MMM工作原理 三、MMM安装部署 环境配置&#xff1a;&#xff08;所有主机配置&#xff09; 1、主机信息 ​编辑 2、…

探索无限可能的教育新领域,景联文教育GPT题库开启智慧教育新时代!

随着人工智能技术的快速发展&#xff0c;教育领域也将迎来一场革命性的变革。景联文科技是AI基础数据行业的头部企业&#xff0c;近期推出了一款高质量教育GPT题库。 景联文科技高质量教育GPT题库采用了先进的自然语言处理技术和深度学习算法&#xff0c;可以实现对各类题目的智…

一个输入网址就可显示网站安全性及网站主要内容的含GUI的Python小程序

文章目录 1.一些杂七杂八的引入2.实现2.1 显示网站安全性2.2 安装所需python包2.2.1 requests包2.2.1 beautifulsoup包 3.源码展示4.效果展示 1.一些杂七杂八的引入 上次发了一个类似爬虫&#xff0c;可以自动下载网页图片的python小程序&#xff08;详见一个自动下载网页图片…

【Web工具】3D 旋转中各数据格式之间的转换

1 Rotation Master — Link GitHub: Link 2 3D Rotation Converter — Link GitHub: Link 3 Quaternions — Link 4 Rotation Conversion Tool — Link 这是个人博客网站&#xff0c;其中可能有你需要的知识: Link

Prometheus实现自定义指标监控

1、Prometheus实现自定义指标监控 前面我们已经通过 PrometheusGrafana 实现了监控&#xff0c;可以在 Grafana 上看到对应的 SpringBoot 应用信息了&#xff0c; 通过这些信息我们可以对 SpringBoot 应用有更全面的监控。 但是如果我们需要对一些业务指标做监控&#xff0c;…

老文章可以删了!!!。2023年最新IDEA中 Java程序 | Java+Kotlin混合开发的程序如何打包成jar包和exe文件(gradle版本)

文章内容&#xff1a; 一. JAVA | JAVA和Kotlin混开开发的程序打包成jar方法 1.1 方法一 &#xff1a;IDEA中手动打包 1.2 方法二 &#xff1a;build.gradle中配置后编译时打包 二. JAVA | JAVA和Kotlin混合开发的程序打包成exe的方法 一. JAVA | JAVA和Kotlin混开开发的程序…

使用 Jetpack Compose 实现 ViewPager2

在此博客中&#xff0c;我们将介绍如何在Jetpack Compose中实现ViewPager2的功能。我们将使用Accompanist库中的Pager库&#xff0c;这是由Google开发的一个用于Jetpack Compose的库。 首先&#xff0c;需要将Pager库添加到你的项目中&#xff1a; implementation androidx.co…

投票活动链接制作方法网络投票办法公众号做投票链接

用户在使用微信投票的时候&#xff0c;需要功能齐全&#xff0c;又快捷方便的投票小程序。 而“活动星投票”这款软件使用非常的方便&#xff0c;用户可以随时使用手机微信小程序获得线上投票服务&#xff0c;很多用户都很喜欢“活动星投票”这款软件。 “活动星投票”小程序在…

ModelScope魔搭社区AI模型下载数据可能存在严重造假问题

目录 摘要&#xff1a; 一、数据分析 二、可能存在的问题 三、结论与建议 摘要&#xff1a; ModelScope魔搭社区作为一个AI模型共享平台&#xff0c;旨在提供各种领域的模型供用户下载和使用。然而&#xff0c;通过对其提供的数据进行分析&#xff0c;发现其中存在一定的数…

【Flutter】built_value 解决 Flutter 中的不可变性问题

文章目录 一、 前言二、 什么是 built_value&#xff1f;三、 为什么我们需要 built_value&#xff1f;四、 如何在 Flutter 中安装和设置 built_value&#xff1f;五、 如何使用 built_value 创建不可变的值类型&#xff1f;六、 如何使用 built_value 创建枚举类&#xff1f;…