pytorch实战-6手写数字加法机-迁移学习

news2024/11/17 20:23:02

1 概述

迁移学习概念:将已经训练好的识别某些信息的网络拿去经过训练识别另外不同类别的信息

优越性:提高了训练模型利用率,解决了数据缺失的问题(对于新的预测场景,不需要大量的数据,只需要少量数据即可实现训练,可用于数据点很少的场景)

如何实现:将训练好的一个网络拿来和另一个网络连起来去训练即可实现迁移

训练方式:按是否改变源网络参数可分两类,分别是可改变和不可改变

2 案例 南非贫困预测

2.1 背景

南非存在贫困,1990-2021贫困人口从56%下降到43%,但下降的贫困人口数量和国际人道主义援助资源并不对应,而且大量资金援助一定程度加剧了贫富差距。可以看下具体哪些地区需要援助

2.2 方法

一个方法:夜光光亮遥感数据和人类gdp相关性经实验可达0.8-0.9,但夜光遥感和贫富没太大相关性:夜间光照月亮表示该地区越富有,但越安并不表示该地区越贫穷,也可能无人居住。

另一个方法:光亮遥感数据无法准确预测地区贫穷程度,但卫星遥感数据大体可以做到,判定依据有街道混乱程度等。如果要用深度网络训练,还需要对卫星遥感数据的图片标注贫困程度。非洲能获取到的贫困数据很少,但深度网络需要的数据量很大

最终方法:用迁移学习,将前两种方法合起来,见下图

3 案例2

3.1 背景

任务:区分图像里动物是蚂蚁还是蜜蜂,像素均为224x224

难点:只有244个图像,样本太少不足训练大型卷积网络,准确率只有50%左右

3.2 解决方案

解决方案:resnet与模型迁移,即用已训练好的物体分类的网络加全连接用来区分蚂蚁与蜜蜂

resnet:残差网络,对物体分类有较高精度

3.3 代码实现

3.3.1 准备数据

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as pyplot
import time
import copy
import os

data_path = 'pytorch/jizhi/figure_plus/data'
image_size = 224

class TranNet():
    def __init__(self):
        super(TranNet, self).__init__()
        
        self.train_dataset = datasets.ImageFolder(os.path.join(data_path, 'train'), transforms.Compose([
            transforms.RandomSizedCrop(image_size),
            transforms.RandomHorizontalFlip(),
            transforms.ToTensor(),
            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
        ]))
        self.verify_dataset = datasets.ImageFolder(os.path.join(data_path, 'verify'), transforms.Compose([
            transforms.Scale(256),
            transforms.CenterCrop(image_size),
            transforms.ToTensor(),
            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
        ]))
        self.train_loader = torch.utils.data.DataLoader(self.train_dataset, batch_size=4, shuffle=True, num_workers=4)
        self.verify_loader = torch.utils.data.DataLoader(self.verify_dataset, batch_size=4, shuffle=True, num_workers=4)
        self.num_classes = len(self.train_dataset.classes)
    
    def exec(self):
        ...

def main():
    TranNet().exec()

if __name__ == '__main__':
    main()

3.3.2 模型迁移

    def exec(self):
        self.model_prepare()
        
    def model_prepare(self):
        net = models.resnet18(pretrained=True)
        
        # float net values
        num_features = net.fc.in_features
        net.fc = nn.Linear(num_features, 2)
        criterion = nn.CrossEntropyLoss()
        optimizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9)
        
        # fixed net values
        '''
        for param in net.parameters():
            param.requires_grad = False
        num_features = net.fc.in_features
        net.fc = nn.Linear(num_features, 2)
        criterion = nn.CrossEntropyLoss()
        optimizer = optim.SGD(net.fc.parameters(), lr = 0.001, momentum=0.9)
        '''

3.3.3 gpu加速

特点:gpu速度快,但内存低,所以尽量减少在gpu中存储的数据,只用来计算就好

    def model_prepare(self):
        # jusge whether GPU
        use_cuda = torch.cuda.is_available()
        dtype = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
        itype = torch.cuda.LongTensor if use_cuda else torch.LongTensor
        
        net = models.resnet18(pretrained=True)
        net = net.cuda() if use_cuda else net

3.3.4 训练

    def model_prepare(self):
        net = models.resnet18(pretrained=True)
        
        # jusge whether GPU
        use_cuda = torch.cuda.is_available()
        if use_cuda:
            dtype = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
            itype = torch.cuda.LongTensor if use_cuda else torch.LongTensor
            net = net.cuda() if use_cuda else net
        
        # float net values
        num_features = net.fc.in_features
        net.fc = nn.Linear(num_features, 2)
        criterion = nn.CrossEntropyLoss()
        optimizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9)
        
        # fixed net values
        '''
        for param in net.parameters():
            param.requires_grad = False
        num_features = net.fc.in_features
        net.fc = nn.Linear(num_features, 2)
        criterion = nn.CrossEntropyLoss()
        optimizer = optim.SGD(net.fc.parameters(), lr = 0.001, momentum=0.9)
        '''
        record = []
        num_epochs = 3
        net.train(True) # open dropout
        for epoch in range(num_epochs):
            train_rights = []
            train_losses = []
            for batch_index, (data, target) in enumerate(self.train_loader):
                data, target = data.clone().detach().requires_grad_(True), target.clone().detach()
                output = net(data)
                loss = criterion(output, target)
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()
                right = rightness(output, target)
                train_rights.append(right)
                train_losses.append(loss.data.numpy())
                if batch_index % 400 == 0:
                    verify_rights = []
                    for index, (data_v, target_v) in enumerate(self.verify_loader):
                        data_v, target_v = data_v.clone().detach(), target_v.clone().detach()
                        output_v = net(data_v)
                        right = rightness(output_v, target_v)
                        verify_rights.append(right)
                    verify_accu = sum([row[0] for row in verify_rights]) / sum([row[1] for row in verify_rights])
                    record.append((verify_accu))
                    print(f'verify data accu:{verify_accu}')
        # plot
        pyplot.figure(figsize=(8, 6))
        pyplot.plot(record)
        pyplot.xlabel('step')
        pyplot.ylabel('verify loss')
        pyplot.show()

4 手写数字加法机

4.1 网络结构

可以先用cnn识别出两个待求和数字,不要输出,只保留池化层加后面一层全连接层,可以获取图像一维特征,然后将两个图像识别获取的一维特征合并,然后用全连接作为剩下的网络

4.2 代码实现

4.2.1 数据加载

class FigurePlus():
    def __init__(self):
        super(FigurePlus, self).__init__()
        self.image_size = 28
        self.num_classes = 10
        self.num_epochs = 3
        self.batch_size = 64
        
        self.train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)
        self.test_dataset = datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor())
        
        sampler_a = torch.utils.data.sampler.SubsetRandomSampler(np.random.permutation(range(len(self.train_dataset))))
        sampler_b = torch.utils.data.sampler.SubsetRandomSampler(np.random.permutation(range(len(self.train_dataset))))
        
        self.train_loader_a = torch.utils.data.DataLoader(dataset=self.train_dataset, batch_size=self.batch_size, shuffle=False, sampler=sampler_a)
        self.train_loader_b = torch.utils.data.DataLoader(dataset=self.train_dataset, batch_size=self.batch_size, shuffle=False, sampler=sampler_b)
        
        self.verify_size = 5000
        verify_index_a = range(self.verify_size)
        verify_index_b = np.random.permutation(range(self.verify_size))
        test_index_a = range(self.verify_size, len(self.test_dataset))
        test_index_b = np.random.permutation(test_index_a)
        
        verify_sampler_a = torch.utils.data.sampler.SubsetRandomSampler(verify_index_a)
        verify_sampler_b = torch.utils.data.sampler.SubsetRandomSampler(verify_index_b)
        test_sampler_a = torch.utils.data.sampler.SubsetRandomSampler(test_index_a)
        test_sampler_b = torch.utils.data.sampler.SubsetRandomSampler(test_index_b)
        
        self.verify_loader_a = torch.utils.data.DataLoader(dataset=self.test_dataset, batch_size=self.batch_size, shuffle=False, sampler=verify_sampler_a)
        self.verify_loader_b = torch.utils.data.DataLoader(dataset=self.test_dataset, batch_size=self.batch_size, shuffle=False, sampler=verify_sampler_b)
        self.test_loader_a = torch.utils.data.DataLoader(dataset=self.test_dataset, batch_size=self.batch_size, shuffle=False, sampler=test_sampler_a)
        self.test_loader_b = torch.utils.data.DataLoader(dataset=self.test_dataset, batch_size=self.batch_size, shuffle=False, sampler=test_sampler_b)
        
        
        
    def gpu_ok(self):
        use_cuda = torch.cuda.is_available()
        dtype = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
        itype = torch.cuda.LongTensor if use_cuda else torch.LongTensor
        
    def exec(self):
        pass
    

def main():
    # TranNet().exec()
    FigurePlus().exec()

if __name__ == '__main__':
    main()

4.2.2 手写数字加法机实现(网络实现)

    def forward(self, x, y, training=True):
        x, y = F.relu(self.net1_conv1(x)), F.relu(self.net2_conv1(y))
        x, y = self.net_pool(x), self.net_pool(y)
        x, y = F.relu(self.net1_conv2(x)), F.relu(self.net2_conv2(y))
        x, y = self.net_pool(x), self.net_pool(y)
        x = x.view(-1, (self.image_size // 4) ** 2 * self.depth[1])
        y = y.view(-1, (self.image_size // 4) ** 2 * self.depth[1])
        z = torch.cat((x, y), 1)
        z = self.fc1(z)
        z = F.relu(z)
        z = F.dropout(z, training=self.training)
        z = F.relu(self.fc2(z))
        z = F.relu(self.fc3(z))
        return F.relu(self.fc4(z))

4.2.3 模型迁移

思路:将上一篇弄好的数字识别模型保存到文件,然后在本章节加载进来,将各参数权重赋值到本节创的新网络

torch.save(cnn, model_save_path)

将网络加载进来时,需要源模型的定义,在本章重新定义下,拷贝后稍作修改

class FigureIdentify(nn.Module):
    def __init__(self):
        super(FigureIdentify, self).__init__()
        self.depth = (4, 8)
    
    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.pool(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = self.pool(x)
        
        x = x.view(-1, (image_size // 4)**2 * self.depth[1])
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        x = F.log_softmax(x, dim=1)
        return x

注意

1 从文件加载模型后只加载网络权重,没加载网络的方法,需重新定义

2 加载后模型会赋值给一个对象,这个对象需要和保存网络时的网络架构保持一致(尽量保持一致,不然报错!!

模型文件加载进来后,用预训练模式(从模型文件加载网络权重作为初始权重,参数会随新网络的训练跟随大网络参数调节)

注意:加法器两个数字识别网络不可直接将文件加载的网络赋值,因为会共享地址,实际是一组参数,一个网络训练后另一个网络的参数也会变,可以复制出来再操作

    def copy_origin_weight(self, net):
        self.net1_conv1.weight.data = copy.deepcopy(net.conv1.weight.data)
        self.net1_conv1.bias.data = copy.deepcopy(net.conv1.bias.data)
        self.net1_conv2.weight.data = copy.deepcopy(net.conv2.weight.data)
        self.net1_conv2.bias.data = copy.deepcopy(net.conv2.bias.data)
        self.net2_conv1.weight.data = copy.deepcopy(net.conv1.weight.data)
        self.net2_conv1.bias.data = copy.deepcopy(net.conv1.bias.data)
        self.net2_conv2.weight.data = copy.deepcopy(net.conv2.weight.data)
        self.net2_conv2.bias.data = copy.deepcopy(net.conv2.bias.data)
        
    

def main():
    # TranNet().exec()
    net = FigurePlusNet()
    origin_net = torch.load(model_save_path)
    
    net.copy_origin_weight(origin_net)
    criterion = nn.MSELoss()
    optmizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9)
    

如要固定值迁移(即新模型训练过程不改变加载进来模型的权重),设requires_grad = False即可

    def copy_origin_weight_nograd(self, net):
        self.copy_origin_weight(net)
        
        self.net1_conv1.weight.requires_grad = False
        self.net1_conv1.bias.requires_grad = False
        self.net1_conv2.weight.requires_grad = False
        self.net1_conv2.bias.requires_grad = False
        self.net2_conv1.weight.requires_grad = False
        self.net2_conv1.bias.requires_grad = False
        self.net2_conv2.weight.requires_grad = False
        self.net2_conv2.bias.requires_grad = False

4.3 训练与测试

    # train 
    records = []
    for epoch in range(net.num_epochs):
        losses = []
        for index, data in enumerate(zip(net.train_loader_a, net.train_loader_b)):
            (x1, y1), (x2, y2) = data
            if net.gpu_ok():
                x1, y1, x2, y2 = x1.cuda(), y1.cuda(), x2.cuda(), y2.cuda()
            optimizer.zero_grad()
            net.train()
            outputs = net(x1.clone().detach(), x2.clone().detach())
            outputs = outputs.squeeze()
            labels = y1 + y2
            loss = criterion(outputs, labels.type(torch.float))
            loss.backward()
            optimizer.step()
            loss = loss.cpu() if net.gpu_ok() else loss
            losses.append(loss.data.numpy())
            if index % 300 == 0:
                verify_losses = []
                rights = []
                net.eval()
                for verify_data in zip(net.verify_loader_a, net.verify_loader_b):
                    (x1, y1), (x2, y2) = verify_data
                    if net.gpu_ok():
                        x1, y1, x2, y2 = x1.cuda(), y1.cuda(), x2.cuda(), y2.cuda()
                    outputs = net(x1.clone().detach(), x2.clone().detach())
                    outputs = outputs.squeeze()
                    labels = y1 + y2
                    loss = criterion(outputs, labels.type(torch.float))
                    loss = loss.cpu() if net.gpu_ok() else loss
                    verify_losses.append(loss.data.numpy())
                    right = rightness(outputs.data, labels)
                    rights.append(right)
                right_ratio = 1.0 * np.sum([i[0] for i in rights]) / np.sum([i[1] for i in rights])
                print(f'no.{epoch}, {index}/{len(net.train_loader_a)}, train loss:{np.mean(losses)}, verify loss:{np.mean(verify_losses)}, accu: {right_ratio}')
                # records.append([np.mean(losses), np.mean(verify_losses), right_ratio])
                records.append([right_ratio])
    # plot train data
    pyplot.figure(figsize=(8, 6))
    pyplot.plot(records)
    pyplot.xlabel('step')  
    pyplot.ylabel('loss & accuracy')
    
    # test
    rights = []
    net.eval()
    for test_data in zip(net.test_loader_a, net.test_loader_b):
        (x1, y1), (x2, y2) = test_data
        if net.gpu_ok():
            x1, y1, x2, y2 = x1.cuda(), y1.cuda(), x2.cuda(), y2.cuda()
        outputs = net(x1.clone().detach(), x2.clone().detach())
        outputs = outputs.squeeze()
        labels = y1 + y2
        loss = criterion(outputs, labels.type(torch.float))
        right = rightness(outputs, labels)
        rights.append(right)
    right_ratio = 1.0 * np.sum([i[0] for i in rights]) / np.sum([i[1] for i in rights])
    print(f'test accuracy: {right_ratio}')
    pyplot.show()

4.4 结果

一轮打3个点,总共6轮(多了太慢,没用gpu),总共打大概24个点。发现随着训练轮数增加,准确率逐步提高

4.5 大规模测试

4.5.1 大模型

大模型指迁移学习全连接层用4层网络。以数据量为自变量,分别看5%,50%,100%数据量情况下,迁移学习与不迁移学习准确率随轮数变化趋势

结论:1 数据量100%时,迁移学习与无迁移学习准确率趋势很接近;数据量较小时,迁移学习准确率上升速度会远快于无迁移学习。数据量到50%左右时差异就变得不是很明显了但还是有 2数据量大时,固定值训练模式比预训练模式精度更好

4.5.2 小模型

小模型指迁移学习全连接层用2层而不是4层,仍然以数据量和是否迁移学习为自变量分析

结论:1 数据量小时,迁移学习比无迁移学习准确率上升速度快,数据量大时,迁移学习与无迁移学习这种差异变小 

5 总结

适用场景(不仅限于这些):数据量小

两种迁移方式:固定值和预训练,固定值方式参数变动范围小,训练可能更快

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

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

相关文章

IP代理可以保护信息安全吗?

“随着互联网的普及和发展,网络安全问题已经成为众多企业和个人所面临的严峻挑战。保护信息安全已成为企业的核心竞争力之一,而IP代理正成为实现这一目标的有效手段。” 一、IP代理真的可以保护用户信息安全吗? IP代理作为一种网络工具&…

CSS基本知识总结

目录 一、CSS语法 二、CSS选择器 三、CSS样式表 1.外部样式表 2.内部样式表 3.内联样式 四、CSS背景 1.背景颜色:background-color 2.背景图片:background-image 3.背景大小:background-size 4.背景图片是否重复:backg…

鸿蒙应用开发学习:获取手机位置信息

一、前言 移动应用中经常需要获取设备的位置信息,因此在鸿蒙应用开发学习中,如何获取手机的位置信息是必修课。之前我想偷懒从别人那里复制黏贴代码,于是在百度上搜了一下,可能是我输入的关键字不对,结果没有找到想要…

离线编译 onnxruntime-with-tensortRT

记录为centos7的4090开发机离线编译onnxruntime的过程,因为在离线的环境,所以踩了很多坑。 https://onnxruntime.ai/docs/execution-providers/TensorRT-ExecutionProvider.html 这里根据官网的推荐安装1.15 版本的onnx 因为离线环境,所以很…

10个常考的前端手写题,你全都会吗?(下)

前言 📫 大家好,我是南木元元,热爱技术和分享,欢迎大家交流,一起学习进步! 🍅 个人主页:南木元元 今天接着上篇再来分享一下10个常见的JavaScript手写功能。 目录 1.实现继承 ES5继…

【制作100个unity游戏之23】实现类似七日杀、森林一样的生存游戏2(附项目源码)

本节最终效果演示 文章目录 本节最终效果演示系列目录前言添加小动物模型动画动物AI脚本效果 添加石头石头模型拾取物品效果 源码完结 系列目录 【制作100个unity游戏之23】实现类似七日杀、森林一样的生存游戏1(附项目源码) 【制作100个unity游戏之23】…

卓振江:我的大数据能力提升之路 | 提升之路系列(二)

导读 为了发挥清华大学多学科优势,搭建跨学科交叉融合平台,创新跨学科交叉培养模式,培养具有大数据思维和应用创新的“π”型人才,由清华大学研究生院、清华大学大数据研究中心及相关院系共同设计组织的“清华大学大数据能力提升项…

x-cmd pkg | perl - 具有强大的文本处理能力的通用脚本语言

目录 介绍首次用户技术特点竞品进一步阅读 介绍 Perl 是一种动态弱类型编程语言。Perl 内部集成了正则表达式的功能,以及巨大的第三方代码库 CPAN;在处理文本领域,是最有竞争力的一门编程语言之一 生态系统:综合 Perl 档案网络 (CPAN) 提供了超过 25,0…

【江科大】STM32:MPU6050介绍

文章目录 MPU6050介绍结构图MPU6050参数硬件电路模块内部结构框图数据帧格式寄存器地址 MPU6050介绍 MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡…

maven配置阿里镜像源

在用户设置settings.xml文件里找到mirrors配置部分&#xff0c;大概在146行&#xff0c;添加如下配置&#xff1a; <mirror><id>alimaven</id><name>aliyun maven</name><url>http://maven.aliyun.com/nexus/content/groups/public/</u…

防火墙子接口配置

目录 拓扑需求 配置DMZ区域配置IP 总公司IP配置生产区办公区 总公司配置子接口网关生产区网关办公区网关 配置安全策略&#xff08;trust to DMZ&#xff09; 测试 拓扑 需求 配置总公司区域配置DMZ区域配置总公司区域到DMZ区域互通&#xff08;trust to DMZ&#xff09; 配置…

基于springboot+vue的学科竞赛管理系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 研究背景…

实时渲染 -- 几何(Geometry)

几何表示&#xff08;Geometry Representation&#xff09; 隐式表面&#xff08;Implicit Surface&#xff09; 一个函数定义一个隐式几何 f(x,y,z)0。​ 容易判断一个点是在几何体内部&#xff08;f<0&#xff09;还是外部&#xff08;f>0&#xff09; 显式表面&…

【C++】位图+布隆过滤器

位图布隆过滤器 1.位图2.布隆过滤器 喜欢的点赞&#xff0c;收藏&#xff0c;关注一下把&#xff01; 1.位图 问: 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数是否在这40亿个数中。 可能你会想到下面这几种方式&#…

分享多种vcruntime140_1.dll丢失修复办法,vcruntime140_1.dll文件下载

vcruntime140_1.dll是Windows操作系统中的一个重要系统文件&#xff0c;它与C运行库相关。当计算机上缺少或损坏了vcruntime140_1.dll文件时&#xff0c;可能会导致一系列问题和错误。出现这文件错误&#xff0c;应该很多小伙伴都会想到重新下载vcruntime140_1.dll&#xff0c;…

uniapp微信小程序图片上传功能实现,页面显示文件列表、删除功能

uniapp小程序图片上传功能效果预览 一、template 页面结构 <view class"upload-box"><view class"upload-list"><view class"upload-item" v-for"(item,index) of fileList" :keyindex><image class"img…

2024年可能会用到的几个地图可视化模板

前言 在数字化的过程中&#xff0c;数据可视化变得越来越重要。用户喜欢通过酷炫的视觉效果和直观的数据展示来理解数据。可视化地图组件是数据可视化的重要组成部分。这些地图组件提供多样化的效果&#xff0c;能够更好地展示数据的关系和地理分布&#xff0c;直观地将数据与…

制图新手首选!6款在线软件,让制图变得简单易学!

1. 即时设计 即时设计是一种国内在线UI设计工具&#xff0c;专注于UI设计领域&#xff0c;支持多人合作。即时设计是一种年轻的UI设计工具&#xff0c;前景广阔。UI设计工具的即时设计支持各种主流格式文件的引入&#xff0c;可以很容易地从其他软件转移。即时设计作为新一代U…

ubuntu22.04安装filebeat报错解决

1、查看报错 journalctl -u filebeat 或者 filebeat -c /etc/filebeat/filebeat.yml找到报错信息 runtime/cgo: pthread_create failed: Operation not permitted 2、解决报错 在filebeat.yml配置文件添加如下配置&#xff0c;重启filebeat seccomp:default_action: allow…

生命在于折腾——WeChat机器人的研究和探索

一、前言 2022年&#xff0c;我玩过原神&#xff0c;当时看到了云崽的QQ机器人&#xff0c;很是感兴趣&#xff0c;支持各种插件&#xff0c;查询游戏内角色相关信息&#xff0c;当时我也自己写了几个插件&#xff0c;也看到很多大佬编写的好玩的插件&#xff0c;后来因为QQ不…