Shuffle Net系列详解 (4) Shuffle Net V2实践部分讲解 for pytorch版本

news2024/10/26 11:24:12

Shuffle Net 系列

  • 实践部分
  • 1.文件划分
  • 2.Block(差异文件)
  • 3.Network(差异文件)
  • 3.总结

实践部分

在看代码之前先叠加一个甲,本文并未跑出来这个实验结果。实验条件有限,论文中采用的ImageNet dataset 数据集有些太大了。后续会出一个简化版本的代码进行实践。本小节主要是对Shuffle Net V2实践旷世实验室的源码进行解读,学习。公开代码就是难得的学习材料。便于读者能够在自己的模型中有所实践,提升自己的代码能力。所以真想实践的读者可以自行下载数据集开展实验,本章节主要是解读其git上的代码,对初学者有一个较简单直白的入门版本。如果可以接受那么就开始本章节的代码学习吧!!!文中机器翻译使用的是shuffle混洗实际上就是通道重排操作,有疑问的同学可以看一下Shuffle Net系列详解 (1) 论文的讲解。仅仅是是翻译的不同

代码点击这里!!!!!

1.文件划分

下图是其git中的文件目录,很规整希望各位在今后的学习中能够做的好点,便于自己的学习和理解,其实看这一部分和shufflenet V1的代码部分都是重合的,其基本变化的部分也是block文件中的代码。在拜读过其原文也确实是这样的,其论文深远意义就是其对研究的深刻和多维度实验结果,首先来看一下:
请添加图片描述
观察到README、train、以及utils文件在内容上的一致性,我们能够得出一个直观的理解:这表明在大规模训练的策略及其有效性方面,已经建立了一套成熟且有效的代码架构。这实际上凸显了两个主要优势:

  1. 训练模型的策略有效性已被验证:在ShuffleNet V1的设计和实现过程中,模型训练的策略和技术手段已经经过了验证,并证明是有效的。这意味着,当我们考虑如何提升模型性能时,现阶段真正可以作出改进的领域,已不再是训练策略,而是模型结构本身。

  2. 未变化的技术是经过验证的:所有保持不变的技术手段都是经过实践探索并证明有用的方法。这些已经证明有效的技术为现阶段的模型构建提供了一个坚实的基础。也就是说,我们所处的这个阶段,对于模型的进一步改进和评估,是在一个相对公平的基础上进行的。

其实可以多学习学习这部分没变化的代码,这些小的trick真的是很珍贵有效,属于是练功夫的内功了,你多厉害的秘籍没好的内力也发挥不出真实的水平,尤其是现阶段百分之1的提升都是创新的时代。很有可能你创新点的有效性就被垃圾的训练策略和各种没调整的参数毁灭掉。

2.Block(差异文件)

接下来直接看代码的主体部分吧:

代码中分支1构建的部分就是,延续了V1中的代码风格,先构建分支然后按需求实例化。下面是其代码对应论文图中的block,将这一组操作定义为了主分支:

主要就是11卷积然后dw卷积,再次使用11卷积

 branch_main = [
        # pw 常规的1*1卷积层
        nn.Conv2d(inp, mid_channels, 1, 1, 0, bias=False),
        nn.BatchNorm2d(mid_channels),
        nn.ReLU(inplace=True),
        # dw
        nn.Conv2d(mid_channels, mid_channels, ksize, stride, pad, groups=mid_channels, bias=False),
        nn.BatchNorm2d(mid_channels),
        # pw-linear
        nn.Conv2d(mid_channels, outputs, 1, 1, 0, bias=False),
        nn.BatchNorm2d(outputs),
        nn.ReLU(inplace=True),
    ]
    self.branch_main = nn.Sequential(*branch_main)

在这里插入图片描述
现在看一下block中的全部代码

import torch
import torch.nn as nn # 老样子常规操作
# 其实这里值得注意的就是,思考下block块就是由卷积操作堆叠,其本质上就是一个神经网络所以使用nn其实很正常

class ShuffleV2Block(nn.Module): # 命名为V2Block
    def __init__(self, inp, oup, mid_channels, *, ksize, stride):
    # 每一个块实例化需要的参数都是一样的,输入输出通道个数,放在神经网络里这就是输入输出向量的维度。
    # mid就是翻译过来就是中间通道的个数,就是隐藏层的维度。剩下的就是卷积核,步长,值得注意的是就是
    # *,后面需要固定赋值,上一章节对这部分进行了解释。
        super(ShuffleV2Block, self).__init__()
        self.stride = stride
        assert stride in [1, 2] # 还是会判断步长是不是在正常的范畴内。1就是常规卷积,2就是下采样

        self.mid_channels = mid_channels
        self.ksize = ksize
        pad = ksize // 2 # 为了保证卷积过程中不会出现特征图变小需要进行填充,其按照卷积核的大小进行判断
        self.pad = pad
        self.inp = inp

        outputs = oup - inp 
        # outputs的取值为输出通道数量减去输入通道数量,这个参数主要是为了实现什么呢?  

        branch_main = [
            # pw 常规的1*1卷积层
            nn.Conv2d(inp, mid_channels, 1, 1, 0, bias=False),
            nn.BatchNorm2d(mid_channels),
            nn.ReLU(inplace=True),
            # dw
            nn.Conv2d(mid_channels, mid_channels, ksize, stride, pad, groups=mid_channels, bias=False),
            nn.BatchNorm2d(mid_channels),
            # pw-linear
            nn.Conv2d(mid_channels, outputs, 1, 1, 0, bias=False),
            nn.BatchNorm2d(outputs),
            nn.ReLU(inplace=True),
        ]
        self.branch_main = nn.Sequential(*branch_main)

其实可以看到其多种模块需要构建各种各样的分支结构组件使用啊。所以下面的代码就是针对其余的组建进行堆叠从而顺利的构建出其他的blcok

接下里是下采样的分支构建代码,和之前V1不同的地方在于,V2下采样中则是采用了两种不同Block完成下采样操作,这是论文中的原图部分:
请添加图片描述

如上图所示,其设计了两种模块实现下采样。继续看代码部分是如何实现的呢?

# 下采样模块的分支构建
        if stride == 2: # 在论文中只有非下采样block采用了直连结构,所以如果步长为2那么就可以着手设计其分支下采样的部分结构。说白了就步长为2就使用branch_proj这个非直连的特殊分支结构
            branch_proj = [ # 然后就是下采样中左侧的分支一共两种形态,一种是简单的一个是复杂的。下述代码就是复杂的复制处理就是d_block中的左侧分支
                # dw 先深度分离卷积 其步长为2,并且groups=inp有多少个输入通道就分多少组就是最基本的DW卷积
                nn.Conv2d(inp, inp, ksize, stride, pad, groups=inp, bias=False),
                nn.BatchNorm2d(inp),
                # pw-linear # DW卷积后跟进一个正常的卷积1*1卷积pw-linear 
                nn.Conv2d(inp, inp, 1, 1, 0, bias=False),
                nn.BatchNorm2d(inp),
                nn.ReLU(inplace=True),
            ]
            self.branch_proj = nn.Sequential(*branch_proj)
        else:
            self.branch_proj = None # 简单的就不给你整啥花里胡哨的了,直接在前向传播给你接一个均值池化。

    def forward(self, old_x): # 首先判断步长
        if self.stride==1: #  c_block

#####################注意如果没看懂通道重排先去看通道重排的代码,下面进行讲解举例子,不要跳过这个理解部分

            x_proj, x = self.channel_shuffle(old_x) # 先进行通道重排和分离这里和V1的重排就不一致了。要注意了。 # 先讲解这个通道重排擦操作吧。回过头来再看这个输出,就是输出了通道的一半和另一半,
            return torch.cat((x_proj, self.branch_main(x)), 1)# 卷积完后进行拼接
        elif self.stride==2: # d_block注意这里不执行通道分离操作,所以就直接进行卷积分支的映射
            x_proj = old_x
            x = old_x
            return torch.cat((self.branch_proj(x_proj), self.branch_main(x)), 1)

def channel_shuffle(self, x):
    # 获取输入数据的尺寸,包括批次大小、通道数、高度和宽度
    batchsize, num_channels, height, width = x.data.size() 

    # 确保通道数能够被4整除,这是由于ShuffleNet的设计中采用分组卷积,通道数通常是4的倍数
    assert (num_channels % 4 == 0) 

    # 重塑张量形状,将通道数分为两组,每组含有 num_channels // 2 个通道。
    # 这实际上是将通道维度展开,便于之后的混洗操作。原始形状为(batchsize, num_channels, height, width)
    # 变形后的形状为(batchsize * num_channels // 2, 2, height * width),便于接下来的混洗操作
    x = x.reshape(batchsize * num_channels // 2, 2, height * width) 
    # 通过permute操作交换张量的维度,实现通道组之间的交换
    x = x.permute(1, 0, 2)  ##########这个时候已经忘成了通道的混洗操作
    # 重塑张量形状,恢复到接近原始的四维形状,为(batchsize, num_channels // 2 * 2, height, width)
    # 这一步骤完成了通道混洗的过程
    x = x.reshape(2, -1, num_channels // 2, height, width)

    # 最终返回两部分混洗后的特征图,准备进行下一步的操作
    return x[0], x[1]

举个例子来解释上述的行为啊。 reshpe在进行这样的是会先将数据进行flatten然后再次reshape首先要有这样的一个理解 举例子开始
假设初始时有一个四维张量,形状为[4, 4, 3, 3],代表有4个样本,每个样本有4个通道,每个通道的特征图大小为3x3

  1. 第一步 - reshape操作:
    • x = x.reshape(batchsize * num_channels // 2, 2, height * width) 先考虑后面,变成一个[4, 4, 9]这样的一个形状。然后再考虑前面的将4x4变成一个8x2
    • 举个例子,通道是这样排列的
[[1,  2 , 3 , 4],
[5,  6,  7,  8],
[9,  10, 11, 12],
[13, 14, 15, 16]]

然后被reshape下

[[1, 2],
[3, 4],
[5,  6],
[7,  8],
[9,  10],
[11, 12],
[13, 14],
[15, 16]]

其实可以看成是一个拉平然后重构的过程。按照两组一个区分,这是为啥呢,因为人家还要做一下这个通道分离的操作了,即这个代码实际上是通道分离的代码请添加图片描述,所以分成了两组。现阶段咱们看通道还是[1,2]是一个列表里。

  • 通过reshape操作,将这16个通道分成两组,每组8个通道,形状变为[8, 2, 9](这里直接把3x3看作一个维度,实际操作中需考虑两个维度)。
  1. 第二步 - permute置换操作:

    • 通过permute操作,实现维度的置换,将形状变为[2, 8, 9],就是两行了,成功将相同实现了划分。原来的13579来自于多个组,现在被划分成了一组了,只有这样才能够实现交叉的分批。
  2. **第三步 - 再次reshape操作:**形状变为[2, -1, 2, 3, 3]
    可以看到这个2和上面的289是一致的就是将全部通道分成两个批次应对,这个通道分离操作。通道都分离出去一半了,之前一个图4个通道,分离出去了一半当然变成2了。所以[2, -1, 2, 3, 3]。第三位置为2。后面的3*3是一致的。-1则是不输入大小让函数自己计算。
    所以人家这个通道重排和通道分离集成在一起了。之所以实现了通道重排的效果就是人家是1234个通道不是12,34一组而是在这个过程中变成了13,24一组。这样即使满足了分离又实现了重排。可以好好思考下,这个操作还是很巧妙的。这样就不会出现分组卷积中的近亲传播现象。有点类似于卷积操作前,使用扑克牌的洗牌方法对通道进行一个洗牌然后分成两组。一组不执行卷积一组执行卷积操作,每次分离操作之前都这么洗一次牌。

    • 最后一次reshape操作将置换后的张量恢复到近似原始的四维形状,形状变为[2, -1, 2, 3, 3]。这里的-1用来自动计算需要合并的维度,基于总元素数量的不变原则,-1处自动计算得到的值是4,因为我们之前通过置换操作已经将通道分成了两组,每组具有原先半数的通道。
    • 最终形状[2, 4, 2, 3, 3]意味着,原始的4个样本现在被分成了两组,每组含有2个新的"样本",每个"样本"有2个通道,且每个通道的特征图大小为3x3

通过上述步骤,原本紧邻的通道现在被重新排列,实现了通道间的信息交流和混合,这是ShuffleNet架构的关键特点之一。每一步的操作都是为了实现这个目标,从而提升深度网络的性能。最终,将混洗后的张量分解为两个部分,每一部分又可以参与后续层的计算,进一步处理特征信息。
让我们进一步阐述为什么要通过reshape操作来触碰和重排通道,以及这个过程的具体意义。

3.Network(差异文件)

看完了每一个block构成,可以看看咱们这个新型高达(shufflenet V2)是怎么拼起来的。

import torch
import torch.nn as nn
from blocks import ShuffleV2Block # 从构建的block文件中将一个组件导入

class ShuffleNetV2(nn.Module):
    def __init__(self, input_size=224, n_class=1000, model_size='1.5x'):# 老规矩还是一样的224图片大小,1000分类数量 1.5是默认的模型尺寸,不同尺寸对应着不同的卷积通道个数。
        super(ShuffleNetV2, self).__init__()
        print('model size is ', model_size)

        self.stage_repeats = [4, 8, 4] # 每一个阶段对应的重复次数
        self.model_size = model_size
        if model_size == '0.5x':
            self.stage_out_channels = [-1, 24, 48, 96, 192, 1024]
        elif model_size == '1.0x':
            self.stage_out_channels = [-1, 24, 116, 232, 464, 1024]
        elif model_size == '1.5x':
            self.stage_out_channels = [-1, 24, 176, 352, 704, 1024]
        elif model_size == '2.0x':
            self.stage_out_channels = [-1, 24, 244, 488, 976, 2048]
        else:
            raise NotImplementedError

        # building first layer
        input_channel = self.stage_out_channels[1]
        self.first_conv = nn.Sequential( # 其第一个卷积模块
            nn.Conv2d(3, input_channel, 3, 2, 1, bias=False), # 输入通道RGB图像三个通道,然后 3, 2,卷积核大小,填充为1.将这个图生成通道数符合input_channel的大小
            nn.BatchNorm2d(input_channel), # BN层
            nn.ReLU(inplace=True), # 激活
        )

        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 构建了一个block中的最大池化层,主要是下采样所以 stride=2

        self.features = [] # 用来存网络的主体
        for idxstage in range(len(self.stage_repeats)): # 一共三个阶段所以 0,1,2
            numrepeat = self.stage_repeats[idxstage] # 看一下这个阶段冲重复几次4,8,4
            # self.stage_out_channels = [-1, 24, 244, 488, 976, 2048]
            output_channel = self.stage_out_channels[idxstage+2]
            # 其实上述代码是什么意思呢就是【0,1,2】 +2 = 【2,3,4】.取出来的输出通
            # 道个数应该是和这几样数值对应的,其作为网络的主体结构,并不是开头也不是结尾。
            # 所以这三个阶段通过block是网络的腰部结构

            for i in range(numrepeat): # 假设第一个计算numrepeat=4 那么就构建4个block
                if i == 0: # 第一个block永远都是下采样模块,目的就是通道数增加,特征图缩小
                    self.features.append(ShuffleV2Block(input_channel, output_channel, 
                                                mid_channels=output_channel // 2, ksize=3, stride=2))
                else: #剩下的都是正常的非下采样模块self.features.append存起来
                    self.features.append(ShuffleV2Block(input_channel // 2, output_channel, 
                                                mid_channels=output_channel // 2, ksize=3, stride=1))

                input_channel = output_channel
                
        self.features = nn.Sequential(*self.features) # 将构建的这一多个模块组装起来

        self.conv_last = nn.Sequential(
            nn.Conv2d(input_channel, self.stage_out_channels[-1], 1, 1, 0, bias=False),
            nn.BatchNorm2d(self.stage_out_channels[-1]),
            nn.ReLU(inplace=True)
        ) # 最后一个模块
        self.globalpool = nn.AvgPool2d(7) # 全剧池化层
        if self.model_size == '2.0x':
            self.dropout = nn.Dropout(0.2)
        self.classifier = nn.Sequential(nn.Linear(self.stage_out_channels[-1], n_class, bias=False))# 分类器具 
        self._initialize_weights()# 初始化参数 完结撒花

    def forward(self, x):
        x = self.first_conv(x)
        x = self.maxpool(x)
        x = self.features(x)
        x = self.conv_last(x)

        x = self.globalpool(x)
        if self.model_size == '2.0x':
            x = self.dropout(x)
        x = x.contiguous().view(-1, self.stage_out_channels[-1])
        x = self.classifier(x)
        return x

    def _initialize_weights(self):
        for name, m in self.named_modules():
            if isinstance(m, nn.Conv2d):
                if 'first' in name:
                    nn.init.normal_(m.weight, 0, 0.01)
                else:
                    nn.init.normal_(m.weight, 0, 1.0 / m.weight.shape[1])
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0.0001)
                nn.init.constant_(m.running_mean, 0)
            elif isinstance(m, nn.BatchNorm1d):
                nn.init.constant_(m.weight, 1)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0.0001)
                nn.init.constant_(m.running_mean, 0)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)

if __name__ == "__main__": #测试整个网络的的流通性
    model = ShuffleNetV2()
    # print(model)

    test_data = torch.rand(5, 3, 224, 224)
    test_outputs = model(test_data)
    print(test_outputs.size())

3.总结

在之前的论文精讲环节,确实还有许多内容需要进一步深入探讨。如果您对某些部分感兴趣,欢迎随时联系我,我会持续更新以提供更多信息。我真心希望读者能够从中获得收益,这里分享的都是基于个人理解的见解。接下来,我计划跟进一些在小型模型上可以执行的实践代码,旨在帮助读者更深入地理解这些概念。其实我看下来这两个代码模型的主体构建都是很容易看懂的。真正的难点是其整体的训练逻辑的堆积。以及各种各样自己写的小的trick。值得注意的是现阶段新技术的引入有些是可以替换的,各位酌情学习哦。

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

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

相关文章

探索Spring Cloud Config:构建高可用的配置中心

目录 认识Spring Cloud ConfigConfig Server读取配置文件步骤1:(1)创建config-server项目(2)在config-server中开启Config Server功能(3)在config-server配置文件进行相关配置(4&…

深入理解 Transformer:构建先进 NLP 模型的关键技术

目录 一、Transformer 为何如此重要?二、Transformer 的核心组成部分1. 注意力机制2. 位置编码3. 前馈神经网络 三、Transformer 的训练方法1. 无监督预训练2. 有监督微调 四、Transformer 的应用场景五、总结与展望 在自然语言处理(NLP)的领…

【JAVA-数据结构】二叉树

这篇是二叉树相关内容。 1. 树型结构 1.1 概念 树是一种非线性的数据结构,它是由n(n>0)个有限结点组成一个具有层次关系的集合。朝把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶下的。它具有以…

外卖点餐系统小程序的设计

管理员账户功能包括:系统首页,个人中心,外卖员管理,餐厅管理,用户管理,菜品分类管理,菜品信息管理,外卖订单管理,订单配送管理 微信端账号功能包括:系统首页…

Java项目实战II基于Java+Spring Boot+MySQL的智能物流管理系统 (源码+数据库+文档)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 随着电子商…

人、机不同的学习机理

在人机协同的过程中,断点(或称为障碍点)是指可能导致人机合作效率下降、决策失误或任务失败的关键环节。以下是一些常见的断点及其影响:人类和机器之间的信息传递不畅,可能导致误解或遗漏关键信息,技术术语…

YOLO11改进|注意力机制篇|引入三重注意力机制Triplet Attention

目录 一、【Triplet Attention】注意力机制1.1【Triplet Attention】注意力介绍1.2【Triplet Attention】核心代码 二、添加【Triplet Attention】注意力机制2.1STEP12.2STEP22.3STEP32.4STEP4 三、yaml文件与运行3.1yaml文件3.2运行成功截图 一、【Triplet Attention】注意力机…

ARM嵌入式学习--第二天

-指令流水线 -基础知识 1.流水线技术通过多个功能部件并行工作来缩短程序执行时间,提高处理器的效率和吞吐率 2.增加流水线级数,可以简化流水线的各级逻辑,进一步提高了处理器的性能 3.以三级流水线分析: pc代表程序计数器&#x…

Graph Contrastive Learning 图对比学习GCL

Preamble GCL主要任务:学习一个编码器,可以编码出结构和结点特征信息,得到一个低维的表达 早期大部分GNN模型都是有监督的训练 自监督学习主要分成两种:生成式(用已有信息去预测自己的其他信息) and 对…

C++学习笔记----9、发现继承的技巧(一)---- 使用继承构建类(1)

在前面的章节中,你学到了继承关系是一种真实世界对象以层次存在的模式。在编程世界中,当需要写一个类基于其构建,或进行细微的修改的另一个类时,那种模式就有了关系。完成这个目标的一个方式是拷贝一个类的代码粘贴到另一个类中。…

一个月学会Java 第14天 内部类

Day14 内部类 类有外边的public class,然后还有一个文件多个的class,但是有没有想过,class可以作为成员也就是类内部的类,甚至作为方法内部的属性也就是类内部的方法的内部出现。除了这两个, 还有直接对着上节课讲的抽…

GeoScene Pro教程(008):GeoScenePro数据查询和检索

文章目录 1、工具分类2、数据常用工具2.1 加载数据2.2 查询需求2.2.1 按照属性查询查询1:人口大于300万的城市有哪些查询2:自治州有哪些查询3:城市名字中带有“荆”的有哪些补充2.2.2 按照位置查询需求2:导出湖北省境内的铁路数据需求3:武汉市共有多少条铁路2.2.3 空间连接…

48 Redis

48 Redis 前言 Redis(Remote Dictionary Server ),即远程字典服务。是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 redis会周期性的把更新的数据写入磁盘或者把修改操…

RTSP与ONVIF协议的区别及其在EasyCVR视频汇聚平台中的应用

在视频监控和物联网设备领域,RTSP(Real Time Streaming Protocol)和ONVIF(Open Network Video Interface Forum)是两个重要的协议,它们各自在视频流的传输和控制上发挥着不同的作用,并在实际应用…

网络安全之XXE攻击

0x01 什么是 XXE 个人认为,XXE 可以归结为一句话:构造恶意 DTD 介绍 XXE 之前,我先来说一下普通的 XML 注入,这个的利用面比较狭窄,如果有的话应该也是逻辑漏洞。 既然能插入 XML 代码,那我们肯定不能善罢…

基于Nodemcu的手机控制小车

基于Nodemcu的手机控制小车 一、项目说明二、项目材料三、代码与电路设计四、轮子和车体五、电路连接六、使用方法 一、项目说明 嗨,机器人项目制造者们!在这个项目中,我制作了这辆简单但快速的遥控车,它可以通过智能手机控制&am…

gaussdb 主备版本8 SQL参考 学习

SQL参考 1 数据类型 1.1 货币类型 1.1.1 货币类型存储带有固定小数精度的货币金额。 1.2 布尔类型 1.2.1 true:真 1.2.2 false:假 1.2.3 null:未知(unknown) 1.3 日期/时间类型 1.3.1 DATE 输出格式:仅支…

MySQL-06.DDL-表结构操作-创建

一.DDL(表操作) create database db01;use db01;create table tb_user(id int comment ID,唯一标识,username varchar(20) comment 用户名,name varchar(10) comment 姓名,age int comment 年龄,gender char(1) comment 性别 ) comment 用户表; 此时并没有限制ID为…

圈子系统APP小程序H5该如何设置IM?

搭建圈子系统的常见问题,以及圈子论坛系统的功能特点 社交圈子论坛系统的概念 圈子小程序源码 多客圈子系统 圈子是什么软件 跟进圈一个系统的软件 为圈子系统APP小程序H5设置IM(即时通讯),需要遵循一系列步骤来确保通讯功能的稳定、安全和高…

企业架构之从理论指南到实践指导企业数字化转型

理论与实践结合的数字化转型之道 在当今的全球化经济中,企业面临着前所未有的数字化转型压力。数字化转型不仅是技术的更新换代,更是业务、组织、文化和战略的系统性重塑。对于企业来说,如何将理论转换为有效的实践路径,是推动数…