返回

CNN图像识别准确率高但结果不准?解决过拟合和数据偏差

Ai

解决卷积神经网络图像处理项目准确率高但结果不准的问题

我搞了个图像处理的项目,用卷积神经网络(CNN)识别我自己用画图软件画的符号和数字。目标是做一个画板,用户画完,程序能认出来画的是啥。 问题是,虽然训练出来的模型准确率挺高,损失值也挺低,但实际用的时候,识别结果却经常出错。

这项目挺简单的,但就是不准,我感觉问题出在下面几个地方:

  • 画的图像稍微有点模糊,识别结果就不对。
  • 如果用户画的图像很小,程序就没法处理,结果也是错的。
  • 很多符号容易混淆,比如 0 和 9, 5 和 3。

我整个项目都是用 Python 写的, 我也会 C#,必要时可以用 .NET 。 我现在很困惑该怎么办.

问题分析:为啥准确率高但结果还是不准?

模型训练的“准确率高,损失值低” 和 “实际结果不准确” 之间存在巨大差异。这种情况通常意味着:

  1. 过拟合 (Overfitting): 模型在训练数据上表现过于完美,过于记住训练数据的细节和噪声,而丧失了对未见数据的泛化能力。 就像一个人只见过红苹果,就认为所有苹果都是红的,见到青苹果就认不出来了。你用自己画的图像训练,很可能出现这个问题。

  2. 数据偏差 (Data Bias): 训练数据与实际使用场景中的数据分布存在差异。 你用画图软件画的图像可能比较“规整”,而用户实际手绘的图像会更加随意、多变。模型没见过“世面”,自然就认不准了。

  3. 数据预处理不足 (Insufficient Data Preprocessing): 原始图像数据可能存在噪声、尺寸不一致等问题,需要适当的预处理,才能让模型更好地学习。

  4. 模型选择不当 (Improper Model Selection): 虽然你用了 CNN,但可能需要针对你的具体问题,选择更合适的网络结构或参数。

解决方案:逐一击破!

下面针对上面几个原因给出解决方案,一步一步排查:

1. 处理数据问题和增强泛化能力

自己画图的数据,可能有点太“理想化”了。咱得让模型见多识广。

  • 方案 A: 拥抱标准数据集

    • 原理: 直接采用像 MNIST 或 EMNIST 这样的公开标准手写数字/字符数据集。这些数据集经过了广泛测试和验证,数据质量高,多样性好。

    • 操作:

      1. 下载 MNIST 或 EMNIST 数据集(Python 的 keras.datasetstensorflow_datasets 可以直接加载)。
      2. 根据你的需求选择合适的部分(例如只保留数字和特定符号)。
      3. 修改你的代码,使之适应新的数据集。
    • 代码示例 (以 MNIST 为例):

      from tensorflow import keras
      import numpy as np
      
      (x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
      
      # 假设你只需要识别数字0-9, 不需要预先训练直接拿标签做 one hot encoding
      # 选择你需要的类别, 如果要添加字母之类的进去
      selected_classes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      train_mask = np.isin(y_train, selected_classes)
      test_mask = np.isin(y_test, selected_classes)
      
      x_train, y_train = x_train[train_mask], y_train[train_mask]
      x_test, y_test = x_test[test_mask], y_test[test_mask]
      
      #调整维度和数据预处理
      x_train = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32') / 255
      x_test = x_test.reshape(x_test.shape[0], 28, 28, 1).astype('float32') / 255
      
      # 将标签进行One-Hot编码
      y_train = keras.utils.to_categorical(y_train, num_classes=len(selected_classes))
      y_test = keras.utils.to_categorical(y_test, num_classes=len(selected_classes))
      
      print(x_train.shape, y_train.shape)  # 看看形状对不对
      print(x_test.shape,y_test.shape)
      
    • 注意点: 需要修改输出层的神经元数量(model.add(Dense(16,activation='softmax'))),让其符合你实际要分类的类的数量

  • 方案 B: 数据增强 + 自制数据 (推荐)

    • 原理: 保留你自制的数据,但对其进行更“猛烈”的增强,模拟用户真实书写的多样性。 同时,少量引入标准数据集作为补充。

    • 操作:

      1. 使用更激进的 ImageDataGenerator 参数,例如更大的旋转角度、位移范围、缩放范围,甚至添加随机噪声。
      2. 考虑使用更高级的增强技术,如弹性变形 (elastic deformation) 或 生成对抗网络 (GAN) 生成的样本。
      3. 将一部分 MNIST/EMNIST 数据混入你的自制数据集中。
    • 代码示例 (增强 ImageDataGenerator):

      train = ImageDataGenerator(
          rescale=1./255,
          rotation_range=40,  # 更大的旋转
          width_shift_range=0.3,  # 更大的位移
          height_shift_range=0.3,
          shear_range=0.3,
          zoom_range=0.3,
          horizontal_flip=True,
          fill_mode='nearest',
          brightness_range=[0.7, 1.3],  # 亮度调整
          # 可以加点噪声,例如:
          # featurewise_center=True,
          # featurewise_std_normalization=True
      )
      
    • 进阶技巧(弹性变形): 弹性形变对于图像可以生成更为真实的变体.

      import scipy.ndimage as ndimage
      import numpy as np
      
      def elastic_transform(image, alpha, sigma, random_state=None):
          """
          对单张图像进行弹性变形.
          alpha: 控制变形强度的参数.
          sigma: 控制平滑度的参数 (越大越平滑).
          """
          if random_state is None:
              random_state = np.random.RandomState(None)
      
          shape = image.shape
          dx = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
          dy = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
      
          x, y, z = np.meshgrid(np.arange(shape[0]), np.arange(shape[1]), np.arange(shape[2]))
          indices = np.reshape(x+dx, (-1, 1)), np.reshape(y+dy, (-1, 1)), np.reshape(z, (-1, 1))
      
          return ndimage.interpolation.map_coordinates(image, indices, order=1).reshape(shape)
      
      #使用:对你dataset中每张图片进行变化
      # augmented_image = elastic_transform(original_image, alpha=34, sigma=4)
      

2. 处理输入图像尺寸问题

用户画的图太小,程序就懵了。

  • 方案 A: 强制重采样 (Resize)

    • 原理: 在用户提交图像后,无论图像大小如何,都将其强制缩放到与模型训练时相同的尺寸 (例如 28x28)。

    • 操作: 使用图像处理库 (如 OpenCV) 在将图像输入模型 之前 进行重采样。

    • 代码示例 (使用 OpenCV):

      import cv2
      import numpy as np
      
      def preprocess_image(image_path, target_size=(28, 28)):
          """
          读取图像, 灰度化, 缩放到目标尺寸.
          """
          img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)  # 读取为灰度图
          img = cv2.resize(img, target_size)   #强制缩放
          img = img.astype('float32') / 255.0  #归一化
          img = np.expand_dims(img, axis=-1) # 如果模型输入需要通道维度 (例如 (28, 28, 1))
          img = np.expand_dims(img,axis=0)
          return img
      
       #   使用示例:假设 'user_drawing.png' 是用户画的图
       #   processed_image = preprocess_image('user_drawing.png')
       #   prediction = model.predict(processed_image)
      

      注意: 由于单通道图像,需要改为灰度图训练和识别

  • 方案 B: 滑动窗口/图像金字塔 (更高级,但更复杂)

    • 原理:
      • 滑动窗口: 在较大的输入图像上,用一个固定大小的窗口 (如 28x28) 滑动,每次取一个窗口内的图像进行识别。
      • 图像金字塔: 将输入图像构建成不同分辨率的多个版本,分别对每个版本进行识别,然后综合结果。
    • 这两个技术过于复杂,不过于赘述。

3. 模型优化

换个模型试试?也许有更合适的。

  • 微调预训练模型 (Transfer Learning)

    • 原理: 利用在大型数据集 (如 ImageNet) 上预训练好的模型,只微调最后几层来适应你的任务。 这样可以利用已有的强大特征提取能力,加快训练速度,提高准确率。

    • 操作:

      1. 选择一个合适的预训练模型 (如 VGG16, ResNet50, MobileNetV2 等)。
      2. 冻结预训练模型的大部分层,只训练最后几层 (通常是全连接层)。
      3. 用你的数据进行微调。
      4. 修改输出层的数量
    • 代码示例 (使用 Keras 和 VGG16):

      from tensorflow.keras.applications import VGG16
      from tensorflow.keras.models import Model
      from tensorflow.keras.layers import Dense, Flatten, Dropout
      # num_classes 取决于你需要检测多少个类
      num_classes = 16
      
      # 加载预训练的 VGG16 模型 (不包括顶部的全连接层)
      base_model = VGG16(weights='imagenet', include_top=False, input_shape=(28, 28, 3))  #输入需要是RGB图片
      
      # 冻结预训练模型的层 (使其不可训练)
      for layer in base_model.layers:
          layer.trainable = False
      
      # 添加自定义的顶部层
      x = base_model.output
      x = Flatten()(x)
      x = Dense(512, activation='relu')(x)
      x = Dropout(0.5)(x)  # 加个 Dropout 防止过拟合
      predictions = Dense(num_classes, activation='softmax')(x)
      
      # 构建最终模型
      model = Model(inputs=base_model.input, outputs=predictions)
      
      # 编译模型
      model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
      
    • 进阶: 解冻部分中间层

      你可以在训练一段时间后解冻 base_model 更靠近输入的卷积层,允许这些层次进行稍微调整,可能得到更好效果。但千万别一开始就解冻所有层.

       #model.fit() 训练一轮后
       for layer in base_model.layers[:15]: #数字可以修改
         layer.trainable =  False
      for layer in base_model.layers[15:]:
         layer.trainable = True
      # 重新compile 和 训练 model
      

4. 解决符号混淆问题

0 和 9,5 和 3 分不清?

  • 细化标签/增加类别: 如果实在分不清,而且这两种符号/数字在你的应用场景中都需要区分, 把他们变成单独一类(比如"O型(可能是0或者9)"这一类), 或引入上下文关系处理

5. 编程语言与框架的选择:Python, C#, or .NET?

Python具有丰富的机器学校库,用于项目原型十分方便。对于生产环境的考量:

  • 如果性能是关键:
    .NET(C#) 在性能方面优于 Python,如果最终是要部署成一个高性能的服务或桌面应用,.NET 更适合。 可以考虑把Python训练出的模型,转成 ONNX 这种通用的模型格式,然后在 .NET 里加载和使用这个模型,进行推理.