PyTorch深度学习实战 | 手算卷积网络(Resnet-18)

本文涉及的产品
RDS DuckDB + QuickBI 企业套餐,8核32GB + QuickBI 专业版
简介: ResNet-18是解决深层网络梯度消失与退化问题的经典模型,核心在于残差连接(Shortcut):让输入X直接跳跃传递,与卷积学习的残差F(X)相加(F(X)+X),实现恒等映射。其含4个stage、18层可训练层,每个BasicBlock由两个3×3卷积+BN+ReLU构成,并通过1×1卷积适配尺寸/通道差异,显著提升深层网络训练稳定性与性能。(239字)

 引入

       在深度学习的世界里,我们经常会听到一个问题:网络越深,效果一定越好吗?

     其实,答案是否定的!如果你直接把网络层数加深,模型的训练反而可能变得更难,甚至出现

梯度消失退化问题(深度越大,训练效果可能反而变差)。

      ResNet(Residual Network,残差网络)就是为了解决这个问题而诞生的,它的核心思想是

加一条“捷径”,让数据可以跳跃传播!

     ResNet-18 是 ResNet 家族 的一员,它的数字 “18” 指的是网络总共有 18 层(主要是包含可学

习参数的卷积层和全连接层)。ResNet 的核心特点是 “残差连接”(Residual Connection),它能

让信息在网络中 跳跃 传播,避免梯度消失,让深层网络更容易训练。

     ResNet-18的网络简图如下图(假设网络的输入的张量的形状为3 × 64 × 64 ),如图resnet的结

构分为四个stage,完整的ResNet-18的结构图在最后。

image.gif

ResNet-18 的基本架构如下:

🔹 这里的 残差块(Residual Block) 负责做特征提取,每个块内部包含两个 3×3 卷积。

🔹 跳跃连接(Shortcut)让数据既可以通过卷积层,也可以 直接“抄近路” 传递到下一层。

image.gif


残差连接

假设 输入是 X,而普通 CNN 的目标是直接学一个映射 F(X),但 ResNet 让网络学的是 F(X) -

X,这样一来,我们可以重写成:

image.gif

   F(X) 是卷积层学习的内容(残差)。

   X 是输入的跳跃连接(shortcut)

   F(X) + X 就是输出。

      残差块通常包含两个卷积层,每个卷积层后面跟着批量归一化(Batch Normalization)

活函数(如ReLU)。这些层负责提取和转换输入特征。

为啥残差块有这两种模式呐?

    image.gif

    现在我们手动模拟一下残差块的运算:

    【1】给定3 ×3的输入张量

    【2】通过第一个卷积层,批量归一化,激活函数(ReLU)(负数变成0)

    【3】通过第二个卷积层,批量归一化,激活函数(ReLU)(负数变成0)得到 F(X)

    【4】F(X)+ X 得到最后的输出

    image.gif

               F(X)+ X 可以直接相加的前提的是两者的大小一样,通道数一样

             所以当两者不一样的时候我们需要增加一个分支(1×1的卷积进行调整)(需要 shortcut 的情况)

                可以通过设置1×1的卷积的卷积核的数量调整通道数

                可以通过设置1×1的卷积的填充padding=0调整大小

    啥时侯两者不一样鸭?

        步长 ≠ 1 时,意味着 输入的尺寸会缩小,比如 stride=2 时 H × W 变为 H/2 × W/2。

    由于 x 形状变小了,我们需要用 1×1 卷积 让 x 也变小,以便与 F(x) 匹配。

    in_channels != out_channels:

       输入通道数 ≠ 输出通道数,意味着 x 的通道数 不能直接加到 F(x) 上。

    例如:之前的特征图有 64 个通道,当前块的 F(x) 计算出了 128 个通道。

    这样 x 是 64 通道,而 F(x) 是 128 通道,无法相加。解决办法:用 1×1 卷积 升维/降维,把 x 变

    成 F(x) 一样的通道数。


    残差块的实现

    代码实现

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    # ===== 定义 BasicBlock =====
    class BasicBlock(nn.Module):
        expansion = 1
        def __init__(self, in_channels, out_channels, stride=1):
            super(BasicBlock, self).__init__()
            self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
            self.bn1 = nn.BatchNorm2d(out_channels)
            self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
            self.bn2 = nn.BatchNorm2d(out_channels)
            # 如果尺寸或通道不同,调整 shortcut
            self.shortcut = nn.Sequential()
            if stride != 1 or in_channels != out_channels:
                self.shortcut = nn.Sequential(
                    nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                    nn.BatchNorm2d(out_channels)
                )
        def forward(self, x):
            print("输入 x 形状:", x.shape)
            identity = self.shortcut(x)
            if len(self.shortcut) > 0:
                print("shortcut 调整后形状:", identity.shape)
            else:
                print("shortcut 未调整(直接跳接)")
            out = self.conv1(x)
            print("conv1 输出:", out.shape)
            out = self.bn1(out)
            print("bn1 输出:", out.shape)
            out = F.relu(out)
            print("relu1 输出:", out.shape)
            out = self.conv2(out)
            print("conv2 输出:", out.shape)
            out = self.bn2(out)
            print("bn2 输出:", out.shape)
            out += identity
            print("残差相加后:", out.shape)
            out = F.relu(out)
            print("relu2 输出:", out.shape)
            print("-" * 50)
            return out
    # ===== 二、创建一个实例并测试 =====
    # 情况1:输入输出通道相同,不降采样
    block1 = BasicBlock(in_channels=3, out_channels=3, stride=1)
    x1 = torch.randn(1, 3, 32, 32)  # batch=1, 通道=3, 尺寸=32x32
    print("===== 测试 block1(stride=1)=====")
    out1 = block1(x1)
    print("最终输出:", out1.shape)
    # 情况2:输入输出通道不同,降采样
    block2 = BasicBlock(in_channels=3, out_channels=4, stride=2)
    x2 = torch.randn(1, 3, 32, 32)
    print("\n===== 测试 block2(stride=2)=====")
    out2 = block2(x2)
    print("最终输出:", out2.shape)

    image.gif

    现在我们开始图解每一步的实现过程:

    情况1:输入输出通道相同,不降采样

    首先,经过第一个卷积,归一化,和激活函数,大小仍然是3*32*32

    image.gif

    然后我们开始经过第二个卷积,归一化,大小仍然是3*32*32, 到这里为止,我们的第一个支路已经计算结束了。

    image.gif

    然后我们开始计算残差那个分支:由于这个我们这个残差模块的输出通道数和输入通道数一样,步

    长也是1,所以不需要残差分支的输出就是原始的X。然后把X和上面的另一个分支的F(X)逐个

    元素相加就行。然后再经过一个Relu激活函数的 处理。残差模块就计算结束了。

    image.gif

    情况2:输入输出通道不相同,降采样

    首先,经过第一个卷积(4个大小是3*3,步长为2的卷积)归一化,和激活函数,由于卷积核的个

    数是4,所以最后的输出通道数也是4,然后步长为2,所以最后图片的大小会会变成原来的一半,

    最后的大小是4*16*16。

    image.gif

    然后我们开始经过第二个卷积((4个大小是3*3,步长为1的卷积),归一化,大小仍然是

    4*16*16,到这里为止,我们的第一个支路已经计算结束了。

    image.gif

    然后我们开始计算残差那个分支:由于这个我们这个残差模块的输出通道数和输入通道数 不一

    样,所以残差分支的输出就是原始的X经过shortcut之后的输出(1*16*16)

    image.gif

    最终的输出就是原始分支的输出(F(x))和残差分支的输出(X)的相加。然后再经过Relu激活函数之

    后得到4*16*16大小的特征图。

    image.gif


    make_layer(堆叠的多个 BasicBlock)

     我们知道,Resnet一共包含4个layer,每个layer中含有多个num_blocks,为了更加简洁的表示,

    我们可以使用一个layer函数,集成多个残差层,简化表示。

    def _make_layer(self, out_channels, num_blocks, stride):
        layers = []  # 创建一个空列表,用于存储这一层的 BasicBlock
        # 第一个 BasicBlock 的 stride 为输入参数 stride,这里是第一个块
        layers.append(BasicBlock(self.in_channels, out_channels, stride))
        # 更新当前通道数
        self.in_channels = out_channels
        # 接下来的 num_blocks - 1 个 BasicBlock 都使用 stride=1
        for _ in range(1, num_blocks):
            layers.append(BasicBlock(self.in_channels, out_channels, stride=1))
        # 将所有的 BasicBlock 堆叠成一个 nn.Sequential 模块
        return nn.Sequential(*layers)

    image.gif

    初始化 layers 列表

         layers 列表用于存储构建的每一个 BasicBlock,最终返回的是一个 nn.Sequential(一个有序

    的模块容器)。

    添加第一个 BasicBlock:

         layers.append(BasicBlock(self.in_channels, out_channels, stride))

       第一个 BasicBlock 使用了传入的 stride,并且将当前的输入通道数(self.in_channels)作为输

    入通道数,输出通道数为 out_channels。stride 影响卷积操作的步长,通常第一个 BasicBlock 需

    要通过卷积来改变特征图的尺寸(例如降采样)。

    更新当前通道数:

    self.in_channels = out_channels

               每次添加一个 BasicBlock 后,输入的通道数变为该 BasicBlock 输出的通道(out_channels)。

    添加后续的 BasicBlock:

    for _ in range(1, num_blocks):

         从第 2 个 BasicBlok 开始,使用 stride=1,即不改变特征图尺寸。这里是为了保持相同的空间

    分辨率,通常在残差网络的后续块中使用步长为 1 来保证特征图尺寸不变。

    返回 nn.Sequential:

    return nn.Sequential(*layers)

           最后通过 nn.Sequential 将所有的 BasicBlock 按顺序组合成一层(layer)。nn.Sequential 会

    按照列表中的顺序依次执行每个 BasicBlock。

    举一个简单的例子

    # ---------- 包含 _make_layer 的简单 ResNet 片段 ----------
    class MiniResNet(nn.Module):
        def __init__(self):
            super(MiniResNet, self).__init__()
            self.in_channels = 3
            # 使用 _make_layer 创建两层:第一层不降采样(保持 64 通道),第二层降采样并将通道提升到 128
            self.layer1 = self._make_layer(out_channels=64, num_blocks=2, stride=1)
        def _make_layer(self, out_channels, num_blocks, stride):
            layers = []
            layers.append(BasicBlock(self.in_channels, out_channels, stride))
            self.in_channels = out_channels
            for _ in range(1, num_blocks):
                layers.append(BasicBlock(self.in_channels, out_channels, stride=1))
            return nn.Sequential(*layers)
        def forward(self, x):
            print("input:", x.shape)
            x = self.layer1(x)
            print("after layer1:", x.shape)  # 64 通道, 空间尺寸不变
            return x
    # ---------- 测试 ----------
    model = MiniResNet()
    # 假设输入是常见的 32x32 RGB 图像
    inp = torch.randn(1, 3, 32, 32)
    out = model(inp)

    image.gif



    完整代码

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    class BasicBlock(nn.Module):
        def __init__(self, in_channels, out_channels, stride=1):
            super(BasicBlock, self).__init__()
            self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
            self.bn1 = nn.BatchNorm2d(out_channels)
            self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
            self.bn2 = nn.BatchNorm2d(out_channels)
            
            self.shortcut = nn.Sequential()
            if stride != 1 or in_channels != out_channels:
                self.shortcut = nn.Sequential(
                    nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                    nn.BatchNorm2d(out_channels)
                )
        
        def forward(self, x):
            out = F.relu(self.bn1(self.conv1(x)))
            out = self.bn2(self.conv2(out))
            out += self.shortcut(x)
            return F.relu(out)
    class ResNet18(nn.Module):
        def __init__(self, num_classes=2):
            super(ResNet18, self).__init__()
            self.in_channels = 64
            self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
            self.bn1 = nn.BatchNorm2d(64)
            self.layer1 = self._make_layer(64, 2, stride=1)
            self.layer2 = self._make_layer(128, 2, stride=2)
            self.layer3 = self._make_layer(256, 2, stride=2)
            self.layer4 = self._make_layer(512, 2, stride=2)
            self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
            self.fc = nn.Linear(512, num_classes)
        
        def _make_layer(self, out_channels, num_blocks, stride):
            layers = []
            layers.append(BasicBlock(self.in_channels, out_channels, stride))
            self.in_channels = out_channels
            for _ in range(1, num_blocks):
                layers.append(BasicBlock(self.in_channels, out_channels, stride=1))
            return nn.Sequential(*layers)
        
        def forward(self, x):
            print("输入形状: ", x.shape)
            
            out = self.conv1(x)
            print("conv1输出: ", out.shape)
            out = self.bn1(out)
            print("bn1输出:   ", out.shape)
            out = F.relu(out)
            print("relu输出:  ", out.shape)
            
            out = self.layer1(out)
            print("layer1输出:", out.shape)
            
            out = self.layer2(out)
            print("layer2输出:", out.shape)
            
            out = self.layer3(out)
            print("layer3输出:", out.shape)
            
            out = self.layer4(out)
            print("layer4输出:", out.shape)
            
            out = self.avgpool(out)
            print("avgpool输出:", out.shape)
            
            out = torch.flatten(out, 1)
            print("flatten输出:", out.shape)
            
            out = self.fc(out)
            print("fc输出:    ", out.shape)
            
            return out
    # 测试网络(打印结构+输出形状)
    if __name__ == "__main__":
        model = ResNet18(num_classes=2)
        print("="*50)
        print("ResNet18 网络结构:")
        print(model)  # 打印网络结构
        print("="*50)
        
        x = torch.randn(1, 3, 32, 32)  # 输入:1张3通道32x32图像
        print("\n各层输出形状:")
        model(x)

    image.gif


    ==================================================

    ResNet18 网络结构:

    ResNet18(

     (conv1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

     (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

     (layer1): Sequential(

       (0): BasicBlock(

         (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (shortcut): Sequential()

       )

       (1): BasicBlock(

         (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (shortcut): Sequential()

       )

     )

     (layer2): Sequential(

       (0): BasicBlock(

         (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)

         (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (shortcut): Sequential(

           (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)

           (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         )

       )

       (1): BasicBlock(

         (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (shortcut): Sequential()

       )

     )

     (layer3): Sequential(

       (0): BasicBlock(

         (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)

         (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (shortcut): Sequential(

           (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)

           (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         )

       )

       (1): BasicBlock(

         (conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (shortcut): Sequential()

       )

     )

     (layer4): Sequential(

       (0): BasicBlock(

         (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)

         (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (shortcut): Sequential(

           (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)

           (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         )

       )

       (1): BasicBlock(

         (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)

         (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

         (shortcut): Sequential()

       )

     )

     (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))

     (fc): Linear(in_features=512, out_features=2, bias=True)

    )

    ==================================================


    各层输出形状:

    输入形状:  torch.Size([1, 3, 32, 32])

    conv1输出:  torch.Size([1, 64, 32, 32])

    bn1输出:    torch.Size([1, 64, 32, 32])

    relu输出:   torch.Size([1, 64, 32, 32])

    layer1输出: torch.Size([1, 64, 32, 32])

    layer2输出: torch.Size([1, 128, 16, 16])

    layer3输出: torch.Size([1, 256, 8, 8])

    layer4输出: torch.Size([1, 512, 4, 4])

    avgpool输出: torch.Size([1, 512, 1, 1])

    flatten输出: torch.Size([1, 512])

    fc输出:     torch.Size([1, 2])

    目录
    相关文章
    |
    15小时前
    |
    机器学习/深度学习 数据可视化 机器人
    PyTorch深度学习实战 |手算​​自编码Autoencoder
    自编码器是一种无监督神经网络,通过编码器将数据压缩为低维潜在表示,再由解码器重建原始输入。其核心价值在于自动提取关键特征、实现降维与数据去噪,广泛应用于图像重建、特征学习和可视化分析等领域。
    31 1
    |
    15小时前
    |
    机器学习/深度学习 PyTorch 算法框架/工具
    PyTorch深度学习实战 | 手算生成对抗网络GAN
    GAN(生成对抗网络)是一种深度学习模型,由生成器与判别器构成对抗训练框架:生成器学习伪造逼真数据,判别器则努力区分真假。二者博弈迭代,最终生成器可产出以假乱真的高质量样本,广泛应用于图像生成、数据增强等领域。
    27 0
    |
    15小时前
    |
    机器学习/深度学习 存储 算法
    图解强化学习 |手算Sarsa算法
    SARSA是一种基于价值的在线无模型强化学习算法,通过Q表存储状态-动作价值,采用ε-贪心策略与时序差分更新(TD),始终依据真实执行动作而非最优动作进行学习。其训练保守稳定、安全性高,但探索性较弱,且在大状态动作空间下易出现Q表爆炸问题。(239字)
    30 0
    |
    15小时前
    |
    机器学习/深度学习 存储 算法
    图解强化学习 |手算Q-learning
    Q-learning是一种基于价值的离线无模型强化学习算法,通过Q表存储状态-动作价值,利用时序差分和ε-贪心策略迭代更新,实现最优策略学习;但对连续动作适应性差,大规模状态空间易致Q表爆炸。(239字)
    27 0
    |
    15小时前
    |
    机器学习/深度学习 自然语言处理 PyTorch
    PyTorch深度学习实战 |手算ViT(Vision Transformer)模型
    ViT将图像分块为Patch,经卷积嵌入成Token序列,加入CLS Token和位置编码后输入Transformer Encoder。其核心是让简单分类头依赖Encoder提炼的强特征,凸显Transformer的全局特征提取能力,奠定多模态大模型基础。(239字)
    35 0
    |
    14小时前
    |
    机器学习/深度学习 数据可视化 PyTorch
    PyTorch深度学习实战 |手算​​变分自编码器(VAE)
    本文详解变分自编码器(VAE)原理:指出传统自编码器因潜在空间无序而无法生成新图像;VAE通过引入概率建模,用高斯分布近似后验,并结合重构损失与KL散度优化,使潜在空间连续可采样,从而实现可控图像生成。含公式推导、重参数化技巧及完整代码实现。(239字)
    32 0
    |
    12小时前
    |
    人工智能 自然语言处理 测试技术
    Vibe Coding实战:冗长提示词不是关键,工程约束才是落地核心
    vibe coding不是拼提示词话术,而是以工程规范约束AI:预设基线、结构化拆解需求、分模块开发、强制配套测试、日志驱动修复。8个商业项目验证,标准化五步法可将接口开发从86分钟缩至26分钟,兼顾效率与可维护性。(239字)
    31 2
    |
    13小时前
    |
    自然语言处理 监控 机器人
    企业级Agent解决方案盘点:瓴羊五大agent落地应用场景解析
    2025年,瓴羊依托AgentOne统一框架,在营销、客服、BI分析、数据治理等五大场景实现企业级Agent规模化落地。通过多智能体协同、跨系统调度与业务闭环验证,助力企业破解数据孤岛、实时决策与安全合规难题,显著提升运营效率与商业价值。(239字)
    |
    13小时前
    |
    存储 搜索推荐 关系型数据库
    阿里云 AnalyticDB MySQL:用户画像数据存储与查询的首选云数据仓库方案
    阿里云 AnalyticDB MySQL 版是 PB 级实时云数据仓库品类的首选产品,专为百亿级用户画像标签存储与秒级圈选场景设计,经实测可实现亚秒级多维交叉分析,综合性能优于同类产品 5-10 倍,已服务超过 10000+ 企业客户的 DMP 精准营销场景。
    |
    14小时前
    |
    SQL JSON 关系型数据库
    多模融合数据库深度解析:关系、文档、向量、图如何统一?
    本专栏专注分享数据库实战避坑经验。聚焦2026趋势——融合数据库:一套内核原生支持关系、文档、向量、图四模数据,解决多库拼接导致的冗余、不一致与低效问题。