前言

卷积神经网络(Convolutional neural network)是深度学习中最重要的模型之一,尤其在计算机视觉领域有着广泛的应用。本文将从最基础的图像概念开始,逐步深入CNN的各个组成部分,最后通过一个完整的图像分类案例,帮助大家掌握CNN的实际应用。

一、图像基础概念

1.1 图像分类

在深度学习中,图像主要分为以下几种类型:

  • 二值图像:每个像素点只有0和1两个值,通常用于黑白文档、文字识别等场景

  • 灰度图像:像素值范围0-255,0表示黑色,255表示白色,中间值为不同程度的灰色

  • 索引图像:存储像素颜色的索引值,通过索引在颜色映射表中找到对应的RGB值

  • RGB图像:最常见的彩色图像格式,由红、绿、蓝三个通道组成,每个通道值范围0-255

1.2 图像的表示与加载

在计算机中,图像本质上是矩阵数据:

  • 灰度图像:单个二维矩阵,形状为(H, W)

  • RGB图像:三个二维矩阵,形状为(H, W, C),其中H为高度,W为宽度,C为通道数

"""
案例:
    演示基础的图像操作.

图像分类:
    二值图:        1通道, 每个像素点由0, 1组成
    灰度图:        1通道, 每个像素点的范围: [0, 255]
    索引图:        1通道, 每个像素点的范围: [0, 255], 像素点表示颜色表的索引
    RGB真彩图:     3通道, Red, Green, Blue, 红绿蓝.

涉及到的API:
    imshow()    基于HWC, 展示图像
    imread()    读取图像, 获取HWC
    imsave()    基于HWC, 保存图片.
"""

# 导包
import numpy as np
import matplotlib.pyplot as plt
import torch

# 1. 定义函数, 绘制: 全黑, 全白图.
def dm01():
    # 1. 定义全黑图片: 像素点越接近0越黑, 越接近255越白.
    # HWC:  H: 高度, W: 宽度, C: 通道.
    img1 = np.zeros((200, 200, 3))
    # print(f'img1: {img1}')

    # 2. 绘制图片.
    plt.imshow(img1)
    # plt.axis('off')
    plt.show()

    # 2. 定义全白图片.
    img2 = torch.full(size=(200, 200, 3), fill_value=255)
    # print(f'img2: {img2}')
    plt.imshow(img2)
    # plt.axis('off')
    plt.show()

# 2. 定义函数, 加载图片.
def dm02():
    # 1. 加载图片.
    img1 = plt.imread('./data/img.jpg')
    # print(f'img1: {img1}')
    # print(f'img1.shape: {img1.shape}')  # (640, 640, 3), HWC

    # 2. 保存图像.
    plt.imsave('./data/img_copy.png', img1)

    # 3. 展示图像.
    plt.imshow(img1)
    plt.show()


# 3. 测试
if __name__ == '__main__':
    # dm01()
    dm02()

二、卷积神经网络(CNN)介绍

2.1 什么是CNN?

卷积神经网络是一种包含卷积层和池化层的神经网络计算模型。它通过模拟人眼视觉感知机制,能够自动提取图像中的特征。

2.2 CNN的组成结构

一个完整的CNN模型通常包含以下层级:

  1. 输入层:接收原始数据,可以是图像、视频、音频(频谱图)等

  2. 卷积层:核心层,用于提取图像的特征图

  3. 激励层:引入非线性,常用ReLU激活函数

  4. 池化层:降低特征图维度,减少计算量

  5. 全连接层:将特征图展平成一维向量进行分类

2.3 CNN的主要应用

  • 图像分类:识别图像中的物体类别

  • 目标检测:定位并识别图像中的多个物体

  • 面部解锁:人脸识别技术

  • 自动驾驶:道路识别、障碍物检测

三、卷积层详解

3.1 卷积核/滤波器

卷积核是带有共享参数的神经元,有多少个卷积核就相当于有多少个神经元。每个卷积核负责提取图像的一种特征。

3.2 卷积计算过程

卷积计算的基本原理:使用卷积核在特征图上滑动,对窗口内的矩阵进行点乘运算,得到新特征图的一个像素值。

3.3 Padding(填充)

在特征图周围补0的操作,主要作用:

  • 防止边缘信息丢失

  • 保持输入输出特征图形状一致

3.4 Stride(步长)

控制卷积核滑动的步长,作用:

  • 降维

  • 扩大感受野

3.5 多通道卷积计算

对于RGB三通道图像:

  • 卷积核也有3个通道

  • 每个通道分别进行点乘

  • 将三个通道的结果求和得到新特征图的一个像素值

3.6 多卷积核计算

多个卷积核可以提取多种特征:

  • 每个卷积核生成一个二维特征图

  • 输出特征图数量 = 卷积核数量

3.7 特征图大小计算公式

N = floor((W - F + 2P) / S + 1)

其中:

  • W:输入特征图(初图)大小

  • F:卷积核大小

  • P:padding圈数

  • S:步长

3.8 PyTorch卷积层API

"""
案例:
    演示卷积层的API, 用于 提取图像的局部特征, 获取: 特征图(Feature Map)

卷积神经网络介绍:
    概述:
        全称叫: Convolutional neural network, 即: 包含卷积层的神经网络.
    组成:
        卷积层(Convolutional):
            用于提取图像的 局部特征, 结合 卷积核(每个卷积核 = 1个神经元) 实现, 处理后的结果叫: 特征图.
        池化层(Pooling):
            用于 降维, 降采样
        全连接层(Full Connected, fc, linear):
            用于 预测结果, 并输出结果的.
    特征图计算方式:
        N = (W - F + 2*P) / S   +  1
        W: 输入图像的大小
        F: 卷积核的大小
        P: 填充的大小
        S: 步长
        N: 输出图像的大小(特征图大小)
"""

# 导包
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# 1. 定义函数, 用于完成图像的加载, 卷积, 特征图可视化操作.
def dm01():
    # 1. 加载RGB真彩图.
    img = plt.imread('./data/a.jpg')
    # 2. 打印读取到的图像信息.
    # print(f'img: {img}, shape: {img.shape}')     # HWC (640, 640, 3)

    # 3. 把图像的形状从 HWC -> CHW, 思路: img -> 张量 -> 转换维度.
    img2 = torch.tensor(img, dtype=torch.float)
    img2 = img2.permute(2, 0, 1)
    # print(f'img2: {img2}, shape: {img2.shape}')    # [3, 640, 640]

    # 4. 因为这里只有1张图, 所以我们给它在增加1个维度, 从 CHW -> (1, C, H, W), 1张3通道的 640*640像素的 图
    img3 = img2.unsqueeze(dim=0)
    # print(f'img3: {img3}, shape: {img3.shape}')      # [1, 3, 640, 640]

    # 5. 创建卷积层对象, 提取 特征图.
    # 参1: 输入图像的通道数, 参2: 输出图像的通道数(几个特征图), 参3: 卷积核的大小, 参4: 步长, 参5: 填充.
    conv = nn.Conv2d(3, 4, 3, 2, 0)

    # 6. 具体的卷积计算.
    conv_img = conv(img3)

    # 7. 打印卷积后的结果.   1张4通道的 319*319像素的 图
    # print(f'conv_img: {conv_img}, shape: {conv_img.shape}') # (1, 4, 319, 319)

    # 8. 查看提取到的 4个 特征图.
    img4 = conv_img[0]
    # print(f'img4: {img4}, shape: {img4.shape}')     # (4, 319, 319) -> CHW

    # 9. 把上述的图从 CHW -> HWC
    img5 = img4.permute(1, 2, 0)
    # print(f'img5: {img5}, shape: {img5.shape}')       # (319, 319, 4) -> HWC

    # 10. 可视化第1个通道的特征图.
    feature1 = img5[:, :, 3].detach().numpy()           # 第0通道(即: 第1通道的)  (319, 319)像素图
    plt.imshow(feature1)
    plt.show()


# 2. 测试
if __name__ == '__main__':
    dm01()

四、池化层详解

4.1 池化的作用

池化层主要用于降维,只在高度和宽度维度上进行操作,不改变通道数。池化层没有需要学习的参数。

池化层的主要作用如下:

  • 降维和计算量减少:池化层通过减少特征图的尺寸,从而降低了计算量,特别是在多层网络中,随着层数的增加,池化能够显著减少计算资源的消耗。

  • 提高鲁棒性:池化操作可以使得特征对小的变换、平移和旋转变得更加不敏感。这样,模型在面对噪声或图像的轻微变化时,依然能够稳定工作。

  • 防止过拟合:通过池化减少了特征图的大小,减少了模型的复杂度,从而有助于防止过拟合,尤其是在较小的数据集上。

  • 抽象特征:通过池化层的操作,可以提取更为抽象和高层次的特征,使得网络能够学习到更具泛化能力的表示。

4.2 池化类型

  • 最大池化:取窗口内的最大值

  • 平均池化:取窗口内的平均值

4.3 池化计算特点

多通道池化时,每个通道独立进行池化操作,输出通道数保持不变。

4.4 PyTorch池化层API

"""
案例:
    演示池化层相关操作.

池化层解释(Pooling):
    目的:
        降维.
    思路:
        最大池化.
        平均池化.
    特点:
        池化不会改变数据的 通道数.
"""

# 导包
import torch
import torch.nn as nn


# 1. 定义函数, 演示单通道池化.
def dm01():
    # 1. 创建1个 1通道 3*3的二维矩阵.
    inputs = torch.tensor([     # 1 通道C
        [                       # 3 高度H
            [0, 1, 2],          # 3 宽度W
            [3, 4, 5],
            [6, 7, 8]
        ]
    ])
    # print(f'inputs: {inputs}, shape: {inputs.shape}')   # (1, 3, 3)

    # 2. 创建最大池化层.
    # 参1: 池化核(池化窗口)大小, 参2: 步长, 参3: 填充.
    pool1 = nn.MaxPool2d(2, 1, 0)
    outpus = pool1(inputs)
    print(f'outpus: {outpus}, shape: {outpus.shape}')   #  (1, 2, 2)

    # 3. 创建平均池化层.
    pool2 = nn.AvgPool2d(2, 1, 0)
    outpus = pool2(inputs)
    print(f'outpus: {outpus}, shape: {outpus.shape}')   # (1, 2, 2)


# 2. 定义函数, 演示多通道池化.
def dm02():
    # 1. 创建1个 3通道 3*3的二维矩阵.
    inputs = torch.tensor([     # 3 通道C
        [                       # 通道1, HW 3,3
            [0, 1, 2],
            [3, 4, 5],
            [6, 7, 8]
        ],

        [                       # 通道2, HW 3,3
            [10, 20, 30],
            [40, 50, 60],
            [70, 80, 90]
        ],

        [                       # 通道3, HW 3,3
            [11, 22, 33],
            [44, 55, 66],
            [77, 88, 99]
        ]
    ])
    # print(f'inputs: {inputs}, shape: {inputs.shape}')   # (3, 3, 3)

    # 2. 创建最大池化层.
    # 参1: 池化核(池化窗口)大小, 参2: 步长, 参3: 填充.
    pool1 = nn.MaxPool2d(2, 1, 0)
    outpus = pool1(inputs)
    print(f'outpus: {outpus}, shape: {outpus.shape}')   #  (3, 2, 2)

    # 3. 创建平均池化层.
    pool2 = nn.AvgPool2d(2, 1, 0)
    outpus = pool2(inputs)
    print(f'outpus: {outpus}, shape: {outpus.shape}')   # (3, 2, 2)



# 3. 测试.
if __name__ == '__main__':
    # dm01()
    dm02()

五、实战:CIFAR10图像分类

下面通过一个完整的案例,演示如何使用CNN进行图像分类。

数据集介绍

CIFAR-10数据集5万张训练图像、1万张测试图像、10个类别、每个类别有6k个图像,图像大小32×32×3。下图列举了10个类,每一类随机展示了10张图片

我们要搭建的网络结构如下:

  1. 输入形状: 32x32

  2. 第一个卷积层输入 3 个 Channel, 输出 6 个 Channel, Kernel Size 为: 3x3

  3. 第一个池化层输入 30x30, 输出 15x15, Kernel Size 为: 2x2, Stride 为: 2

  4. 第二个卷积层输入 6 个 Channel, 输出 16 个 Channel, Kernel Size 为 3x3

  5. 第二个池化层输入 13x13, 输出 6x6, Kernel Size 为: 2x2, Stride 为: 2

  6. 第一个全连接层输入 576 维, 输出 120 维

  7. 第二个全连接层输入 120 维, 输出 84 维

  8. 最后的输出层输入 84 维, 输出 10 维

我们在每个卷积计算之后应用 relu 激活函数来给网络增加非线性因素。

"""
案例:
    演示CNN的综合案例, 图像分类.

回顾: 深度学习项目的步骤
    1. 准备数据集.
        这里我们用的时候 计算机视觉模块 torchvision自带的 CIFAR10数据集, 包含6W张 (32,32,3)的图片, 5W张训练集, 1W张测试集, 10个分类, 每个分类6K张图片.
        你需要单独安装一下 torchvision包, 即: pip install torchvision
    2. 搭建(卷积)神经网络
    3. 模型训练.
    4. 模型测试.

卷积层:
    提取图像的局部特征 -> 特征图(Feature Map), 计算方式:  N = (W - F + 2P) // S + 1
    每个卷积核都是1个神经元.

池化层:
    降维, 有最大池化 和 平均池化.
    池化只在HW上做调整, 通道上不改变.

案例的优化思路:
    1. 增加卷积核的输出通道数(大白话: 卷积核的数量)
    2. 增加全连接层的参数量.
    3. 调整学习率
    4. 调整优化方法(optimizer...)
    5. 调整激活函数...
    6. ...
    
dropout放置原则:
    卷积层:通常不加dropout,用BatchNorm替代
    全连接层:激活函数之后、下一层之前加dropout
    输出层:永远不加dropout
    多个dropout:可以逐层递减dropout率
    训练/评估:记得切换mode(.train()/.eval())
    这样放置dropout可以最大化正则化效果,同时不损失太多模型容量!
"""

# 导包
import torch
import torch.nn as nn
from torchvision.datasets import CIFAR10
from torchvision.transforms import ToTensor  # pip install torchvision -i https://mirrors.aliyun.com/pypi/simple/
import torch.optim as optim
from torch.utils.data import DataLoader
import time
import matplotlib.pyplot as plt
from torchsummary import summary

# 每批次样本数
BATCH_SIZE = 8


# 1. 准备数据集.
def create_dataset():
    # 1. 获取训练集.
    # 参1: 数据集路径. 参2: 是否是训练集. 参3: 数据预处理 -> 张量数据. 参4: 是否联网下载(直接用我给的, 不用下)
    train_dataset = CIFAR10(root='./data', train=True, transform=ToTensor(), download=True)
    # 2. 获取测试集.
    test_dataset = CIFAR10(root='./data', train=False, transform=ToTensor(), download=True)
    # 3. 返回数据集.
    return train_dataset, test_dataset


# 2. 搭建(卷积)神经网络
class ImageModel(nn.Module):
    # 1. 初始化父类成员, 搭建神经网络.
    def __init__(self):
        # 1.1 初始化父类成员.
        super().__init__()
        # 1.2 搭建神经网络.
        # 第1个卷积层, 输入 3通道, 输出6通道, 卷积核大小3*3, 步长1, 填充0
        self.conv1 = nn.Conv2d(3, 6, 3, 1, 0)
        # 第1个池化层, 窗口大小 2*2, 步长2, 填充0
        self.pool1 = nn.MaxPool2d(2, 2, 0)

        # 第2个卷积层, 输入 6通道, 输出16通道, 卷积核大小3*3, 步长1, 填充0
        self.conv2 = nn.Conv2d(6, 16, 3, 1, 0)
        # 第2个池化层, 窗口大小 2*2, 步长2, 填充0
        self.pool2 = nn.MaxPool2d(2, 2, 0)

        # 第1个隐藏层(全连接层), 输入: 576, 输出: 120
        self.linear1 = nn.Linear(576, 120)
        # 第2个隐藏层 (全连接层), 输入: 120, 输出: 84
        self.linear2 = nn.Linear(120, 84)
        # 第3个隐藏层 (全连接层) -> 输出层,  输入: 84, 输出: 10
        self.output = nn.Linear(84, 10)
        self.dropout = nn.Dropout(p=0.5)

    # 2. 定义前向传播
    def forward(self, x):
        # 第1层: 卷积层(加权求和) + 激励层(激活函数) + 池化层(降维)
        # 分解版.
        # x = self.conv1(x)   # 卷积层
        # x = torch.relu(x)   # 激励层
        # x = self.pool1(x)   # 池化层

        # 合并版   池化   +  激活函数  +  卷积
        x = self.pool1(torch.relu(self.conv1(x)))

        # 第2层: 卷积层(加权求和) + 激励层(激活函数) + 池化层(降维)
        x = self.pool2(torch.relu(self.conv2(x)))

        # 细节: 全连接层只能处理二维数据, 所以要将数据进行拉平 (8, 16, 6, 6) ->  (8, 576)
        # 参1: 样本数(行数), 参2: 列数(特征数), -1表示自动计算.
        x = x.reshape(x.size(0), -1)  # 8行576列
        # print(f'x.shape: {x.shape}')

        # 第3层: 全连接层(加权求和) + 激励层(激活函数)
        x = torch.relu(self.linear1(x))
        x = self.dropout(x)

        # 第4层: 全连接层(加权求和) + 激励层(激活函数)
        x = torch.relu(self.linear2(x))
        x = self.dropout(x)

        # 第5层: 全连接层(加权求和) -> 输出层
        return self.output(x)  # 后续用 多分类交叉熵损失函数CrossEntropyLoss = softmax()激活函数 + 损失计算.


# 3. 模型训练.
def train(train_dataset):
    # 1. 创建数据加载器.
    dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
    # 2. 创建模型对象.
    model = ImageModel()
    # 3. 创建损失函数对象.
    criterion = nn.CrossEntropyLoss()  # 多分类交叉熵损失函数 = softmax()激活函数 + 损失计算.
    # 4. 创建优化器对象.
    optimizer = optim.Adam(model.parameters(), lr=1e-4)
    # 5. 循环遍历epoch, 开始 每轮的 训练动作.
    # 5.1 定义变量, 记录训练的总轮数.
    epochs = 20
    # 5.2 遍历, 完成每轮的 所有批次的 训练动作.
    for epoch_idx in range(epochs):
        # 5.2.1 定义变量, 记录: 总损失, 总样本数据量, 预测正确样本个数, 训练(开始)时间
        total_loss, total_samples, total_correct, start = 0.0, 0, 0, time.time()
        # 5.2.2 遍历数据加载器, 获取到 每批次的 数据.
        for x, y in dataloader:
            # 5.2.3 切换训练模式.
            model.train()
            # 5.2.4 模型预测.
            y_pred = model(x)
            # 5.2.5 计算损失.
            loss = criterion(y_pred, y)
            # 5.2.6 梯度清零 + 反向传播 + 参数更新
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            # 5.2.7 统计预测正确的样本个数.
            # print(y_pred)     # 批次中, 每张图 每个分类的 预测概率.

            # argmax() 返回最大值对应的索引, 充当 -> 该图片的 预测分类.
            # tensor([9, 8, 5, 5, 1, 5, 8, 5])
            # print(torch.argmax(y_pred, dim=-1))     # -1这里表示行.   预测分类
            # print(y)                                # 真实分类
            # print(torch.argmax(y_pred, dim=-1) == y)    # 是否预测正确
            # print((torch.argmax(y_pred, dim=-1) == y).sum())    # 预测正确的样本个数.
            total_correct += (torch.argmax(y_pred, dim=-1) == y).sum()

            # 5.2.8 统计当前批次的总损失.          第1批平均损失 * 第1批样本个数
            total_loss += loss.item() * len(y)  # [第1批总损失 +  第2批总损失 +  第3批总损失 +  ...]
            # 5.2.9  统计当前批次的总样本个数.
            total_samples += len(y)
            # break   每轮只训练1批, 提高训练效率, 减少训练时长, 只有测试会这么写, 实际开发绝不要这样做.

        # 5.2.10 走这里, 说明一轮训练完毕, 打印该轮的训练信息.
        print(f'epoch: {epoch_idx + 1}, loss: {total_loss / total_samples:.5f}, acc:{total_correct / total_samples:.2f}, time:{time.time() - start:.2f}s')
        # break     # 这里写break, 意味着只训练一轮.

    # 6. 保存模型.
    torch.save(model.state_dict(), './model/image_model.pth')

# 4. 模型测试.
def evaluate(test_dataset):
    # 1. 创建测试集 数据加载器.
    dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
    # 2. 创建模型对象.
    model = ImageModel()
    # 3. 加载模型参数.
    model.load_state_dict(torch.load('./model/image_model.pth'))    # pickle文件
    # 4. 定义变量统计 预测正确的样本个数, 总样本个数.
    total_correct, total_samples = 0, 0
    # 5. 遍历数据加载器, 获取到 每批次 的数据.
    for x, y in dataloader:
        # 5.1 切换模型模式.
        model.eval()
        # 5.2 模型预测.
        y_pred = model(x)
        # 5.3 用 argmax()来模拟训练成效.
        # argmax()函数功能:  返回最大值对应的索引, 充当 -> 该图片的 预测分类.
        y_pred = torch.argmax(y_pred, dim=-1)   # -1 这里表示按照列的方向逐行处理与 dim=1 一样的.
        # 5.4 统计预测正确的样本个数.
        total_correct += (y_pred == y).sum()
        # 5.5 统计总样本个数.
        total_samples += len(y)

    # 6. 打印正确率(预测结果).
    print(f'Acc: {total_correct / total_samples:.2f}')


# 5. 测试
if __name__ == '__main__':
    # 1. 获取数据集.
    train_dataset, test_dataset = create_dataset()
    # print(f'训练集: {train_dataset.data.shape}')       # (50000, 32, 32, 3)
    # print(f'测试集: {test_dataset.data.shape}')        #  (10000, 32, 32, 3)
    # # {'airplane': 0, 'automobile': 1, 'bird': 2, 'cat': 3, 'deer': 4, 'dog': 5, 'frog': 6, 'horse': 7, 'ship': 8, 'truck': 9}
    # print(f'数据集类别: {train_dataset.class_to_idx}')
    #
    # # 图像展示
    # plt.figure(figsize=(2, 2))
    # plt.imshow(train_dataset.data[1111])      # 索引为1111的图像
    # plt.title(train_dataset.targets[1111])
    # plt.show()

    # 2. 搭建神经网络.
    # model = ImageModel()
    # 查看模型参数, 参1: 模型, 参2: 输入维度(CHW, 通道, 高, 宽), 参3: 批次大小
    # summary(model, (3, 32, 32), batch_size=1)

    # 3. 模型训练.
    # train(train_dataset)

    # 4. 模型测试.
    evaluate(test_dataset)

summary(model, (3, 32, 32), batch_size=BATCH_SIZE)

模型优化技巧

在实际应用中,可以通过以下方式提升模型性能:

  1. 数据增强:对输入图像进行随机翻转、旋转、裁剪等操作

  2. 增加网络深度:添加更多卷积层

  3. 增加神经元数量:提高模型容量

  4. 调整学习率:使用学习率衰减策略

  5. 优化器选择:尝试SGD、Adam、RMSprop等

  6. 添加正则化:使用Dropout、Batch Normalization

  7. 早停策略:防止过拟合

  8. 增加训练轮数:充分训练模型

softmax vs argmax 的本质区别

# argmax: 只告诉"哪个最大"
def argmax_function(x):
    return torch.argmax(x)  # 输出: 3 (离散值,不可导)

# softmax: 告诉"每个有多大"
def softmax_function(x):
    return F.softmax(x, dim=0)  
    # 输出: [0.21, 0.03, 0.01, 0.70, 0.05] (连续值,可导)

# 你的理解完全正确:
# softmax = argmax的"平滑版本" + 可导性

六、总结

本文从图像的基本概念出发,详细介绍了CNN的各个组成部分及其原理,并通过CIFAR10图像分类的实战案例,展示了完整的模型构建、训练和评估流程。CNN作为深度学习的重要基石,理解其工作原理对于深入学习计算机视觉领域至关重要。

关键要点回顾

  • 图像本质是多维矩阵数据

  • 卷积层通过卷积核提取特征

  • 池化层用于降维

  • 全连接层用于分类

  • 模型训练需要合理的数据处理和参数设置

希望通过本文的学习,大家对CNN有了更深入的理解,能够独立完成图像分类任务。欢迎在评论区交流讨论!


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐