如何使用预训练的神经网络处理灰度图像?

85

我有一个包含灰度图像的数据集,想用最先进的卷积神经网络(CNN)对其进行训练。我非常希望微调预训练模型 (如这里提供的模型)。

问题是我找到的几乎所有模型参数都是在ImageNet数据集上训练得到的,该数据集包含RGB图像。

因为它们的输入层期望的批次形状为(batch_size, height, width, 3)(64, 224, 224, 3),而我的图像批次为(64, 224, 224),所以我不能使用这些模型。

有没有办法可以使用其中一个模型?我考虑过在加载权重后删除输入层并添加自己的输入层(就像我们对顶部层做的那样)。这种方法是否正确?


1
你可以尝试移除输入层并添加自己的层。然后,你可以尝试仅训练该层。如果你锁定了所有其他层,但没有看到损失减少,那么这种方法可能行不通。 - kevinkayaks
9
不要问我们这种方法是否正确:问问电脑吧!试一试吧!另一种方法是将输入向量三倍化:将灰度值馈送到所有三个彩色层。 - Prune
2
我的个人感觉是这对你来说不会行。这些分类网络肯定是使用颜色之间的相互关系来对物体进行分类,而这些信息深深地融入了中间层的权重中。 - kevinkayaks
21
训练这些模型可能需要数天时间,如果有人遇到过这个问题并能提供一些见解,我会非常感激。 - Jcart
2
正如其他人所提到的,将3个相同的灰度数组堆叠作为输入是可行的--但我会将其视为实现更多数据增强的机会--对原始灰度图像应用图像滤镜,并随机分配它们到3个通道中。 - ohailolcat
显示剩余2条评论
12个回答

83
模型的架构无法更改,因为权重已经针对特定的输入配置进行了训练。用自己的层替换第一层会使其余的权重基本无用。
-- 编辑:Prune建议的阐述 -- CNN是这样构建的,当它们变得更深时,它们可以提取从先前层提取的低级特征派生出来的高级特征。通过移除CNN的初始层,您正在破坏那些特征的层次结构,因为后续层不会收到它们应该作为输入接收的特征。在您的情况下,第二层已经被训练为期望第一层的特征。通过用随机权重替换第一层,您实际上正在丢弃对后续层进行的任何训练,因为它们需要重新训练。我怀疑它们能够保留初步培训期间学到的任何知识。 --- 结束编辑 ---
然而,有一种简单的方法可以使您的模型适用于灰度图像。您只需要使图像看起来像RGB即可。最简单的方法是在新维度上重复图像数组3次。因为您将在所有3个通道上拥有相同的图像,所以模型的性能应该与RGB图像上的性能相同。
numpy中,可以轻松完成此操作:
print(grayscale_batch.shape)  # (64, 224, 224)
rgb_batch = np.repeat(grayscale_batch[..., np.newaxis], 3, -1)
print(rgb_batch.shape)  # (64, 224, 224, 3)

这是这样工作的:首先创建一个新维度(用于放置通道),然后在这个新维度上将现有数组重复3次。
我也很确定keras的ImageDataGenerator可以将灰度图像加载为RGB。

2
叠加1通道图像很容易实现,但问题不在于如何使图像变为3通道,而是当原始图像为1通道时,他是否可以使用预先训练好的分类模型,我认为答案很可能是否定的。 - kevinkayaks
3
处理灰度图像时,这基本上是默认的方法。我已经试过几次了,效果很好,甚至是keras' ImageDataGenerator中加载灰度图像的默认设置,将其重复3次。可以将其视为RGB -> 灰度反向变换(其中gray =(R + B + G)/ 3)。 - Djib2011
1
这展示了如何实现我所建议的第二次尝试;它并没有回答原来的问题。这样做是否会在灰度输入的有效微调中产生结果? - Prune
你的回答的第一段是直接的部分:你能否详细阐述一下来说服OP呢? - Prune
4
用自己的第一层替换原有的第一层会让其他权重几乎没有用处。你确定吗?可以进行一项实验来验证这一点,例如在ImageNet上训练一个神经网络并观察其达到某个精度所需的时间。然后重新初始化输入层,并再次观察达到该精度所需的时间。我相信,使用已初始化的网络所需的时间要少得多。 - Martin Thoma
显示剩余5条评论

63
将灰度图像转换为RGB是解决此问题的一种方法,但不是最有效的方法。您可以修改模型的第一个卷积层的权重并实现所述目标。修改后的模型将可以立即使用(精度降低),并且可以进行微调。修改第一层的权重不会使其他权重无用,这与其他人的建议不同。
要做到这一点,您需要在加载预训练权重时添加一些代码。在您选择的框架中,您需要找出如何获取网络中第一个卷积层的权重并在分配给1通道模型之前进行修改。所需的修改是对输入通道的维度上的权重张量求和。权重张量的组织方式因框架而异。PyTorch的默认值为[out_channels,in_channels,kernel_height,kernel_width]。在Tensorflow中,我认为它是[kernel_height,kernel_width,in_channels,out_channels]。
以PyTorch为例,在Torchvision的ResNet50模型中(https://github.com/pytorch/vision/blob/master/torchvision/models/resnet.py),conv1的权重形状为[64,3,7,7]。沿着第1维求和会得到形状为[64,1,7,7]的张量。在底部,我包含了一小段代码,该代码适用于Torchvision中的ResNet模型,假设添加了一个参数(inchans)以指定模型的不同输入通道数。
为了证明这一点,我对使用预训练权重的 ResNet50 进行三次 ImageNet 验证。第二次和第三次的数字略有不同,但这很小,并且在微调后应该是无关紧要的。
  1. 未修改的 ResNet50 w/ RGB 图像:精度 @1: 75.6,精度 @5: 92.8
  2. 未修改的 ResNet50 w/ 3-通道灰度图像:精度 @1: 64.6,精度 @5: 86.4
  3. 修改的 1-通道 ResNet50 w/ 1-通道灰度图像:精度 @1: 63.8,精度 @5: 86.1
def _load_pretrained(model, url, inchans=3):
    state_dict = model_zoo.load_url(url)
    if inchans == 1:
        conv1_weight = state_dict['conv1.weight']
        state_dict['conv1.weight'] = conv1_weight.sum(dim=1, keepdim=True)
    elif inchans != 3:
        assert False, "Invalid number of inchans for pretrained weights"
    model.load_state_dict(state_dict)

def resnet50(pretrained=False, inchans=3):
    """Constructs a ResNet-50 model.
    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
    """
    model = ResNet(Bottleneck, [3, 4, 6, 3], inchans=inchans)
    if pretrained:
        _load_pretrained(model, model_urls['resnet50'], inchans=inchans)
    return model

7
听起来很酷!所以这样做是因为当r = g = b(灰度)时,r*w0+g*w1+b*w2等同于r*(w0+w1+w2) - Jeppe
所以,这看起来正是我感兴趣的内容,但是不清楚我们是否会在处理中获得3倍的速度提升?我有一张RGB图像,其中绿色通道比其他通道更好,希望仅使用绿色会更快。您能澄清一下这方面的时间,并且这种方法的目的确实是加速训练和预测吗?非常感谢! - catubc
1
@Cat,你可能会观察到小地图加速,但远远达不到3倍,因为这只是一个层,而网络的其余部分保持不变。 - leoll2
2
这个答案与被接受的答案相矛盾。如果这个答案是正确的(我认为是),它应该被标记为被接受的答案。否则,人们在阅读被接受的答案后会被误导。 - FARAZ SHAIKH
@SHAIKH 这个答案并不与被接受的答案相矛盾。被接受的答案是关于移除第一层的情况。而这个答案则是关于修改第一层的情况。这个答案是有效的,因为第一层的输出特征没有改变。 - mss

18

一种简单的方法是在基础模型之前添加一个卷积层,然后将输出馈送到基础模型。像这样:

from keras.models import Model
from keras.layers import Input 

resnet = Resnet50(weights='imagenet',include_top= 'TRUE') 

input_tensor = Input(shape=(IMG_SIZE,IMG_SIZE,1) )
x = Conv2D(3,(3,3),padding='same')(input_tensor)    # x has a dimension of (IMG_SIZE,IMG_SIZE,3)
out = resnet (x) 

model = Model(inputs=input_tensor,outputs=out)



ValueError: 您正在尝试将包含13个层的权重文件加载到具有14个层的模型中。 有什么避免方法吗? - Madara
1
@Madara,你能否分享更多细节?同时请检查是否包含了最后一层。 - mmrbulbul
# input_shape = (64,64,1) input_tensor = Input(shape=image_shape) model_input = Conv2D(filters = 3, kernel_size=3, padding='same', name="input_conv")(input_tensor)Now I import the VGG16 model vgg16 = VGG16(include_top=False, weights='imagenet',input_tensor=model_input)Then I try to add a Conv layer X = Conv2D(channels, kernel_size=3, padding='same')(vgg16.output) X = Activation('tanh')(X) And the final model: model = Model(inputs = input_tensor, outputs = X) - Madara
1
将代码行vgg16 = VGG16(include_top=False, weights='imagenet',input_tensor=model_input)拆分为以下两行:vgg16 = VGG16(include_top=False, weights='imagenet') vgg16 = vgg16(model_input)应该可以正常工作。 - mmrbulbul

9

为什么不尝试将灰度图像转换为虚假的“RGB”图像呢?

tf.image.grayscale_to_rgb(
    images,
    name=None
)

澄清一下:根据文档,该方法会将最后一个通道重复三次。 - mss

6
放弃输入层不可行。这将导致所有后续层都会受到影响。 您可以执行的操作是将3个黑白图像连接在一起以扩展您的颜色维度。
img_input = tf.keras.layers.Input(shape=(img_size_target, img_size_target,1))
img_conc = tf.keras.layers.Concatenate()([img_input, img_input, img_input])    

model = ResNet50(include_top=True, weights='imagenet', input_tensor=img_conc)

3
尽管这段代码可能解决了问题,但是加入一些关于它如何和为什么解决问题的解释会提高您发布的质量,可能会得到更多的赞同。请记住,您正在回答未来读者的问题,而不仅仅是现在提问的人。请编辑您的答案以添加解释,并指出适用的限制和假设条件。有关如何回答的更多详细信息,请参见此链接:https://stackoverflow.com/help/how-to-answer - Usama Abdulrehman

2
我曾在使用灰度图像与VGG16一起工作时遇到了同样的问题。我解决了这个问题,方法如下:
假设我们的训练图像是在train_gray_images中,每行都包含未滚动的灰度图像强度值。如果我们直接将它传递给fit函数,会产生错误,因为fit函数期望的是一个3通道(RGB)图像数据集,而不是灰度数据集。因此,在传递给fit函数之前,请执行以下操作:
创建一个虚拟的RGB图像数据集,就像灰度数据集一样具有相同的形状(这里使用dummy_RGB_image)。唯一的区别是这里使用的通道数是3。
dummy_RGB_images = np.ndarray(shape=(train_gray_images.shape[0], train_gray_images.shape[1], train_gray_images.shape[2], 3), dtype= np.uint8) 

因此,只需将整个数据集复制3次到“dummy_RGB_images”的每个通道中即可。(这里的维度为[样例数量,高度,宽度,通道])
dummy_RGB_images[:, :, :, 0] = train_gray_images[:, :, :, 0]
dummy_RGB_images[:, :, :, 1] = train_gray_images[:, :, :, 0]
dummy_RGB_images[:, :, :, 2] = train_gray_images[:, :, :, 0]

最后,传递 dummy_RGB_images 而不是灰度数据集,如下所示:

model.fit(dummy_RGB_images,...)

1
numpy的深度堆叠函数np.dstack((img, img, img))是一个很自然的选择。

这与Djib2011在2018年提出的np.repeat(img, 3, -1)相比如何? - greybeard
1
它们基本上产生相同的结果,np.dstack只是看起来更加简单明了。 - Ahmed Baruwa

0
我所做的就是通过以下转换阶段将灰度图像简单地扩展为RGB图像:
import torchvision as tv
tv.transforms.Compose([
    tv.transforms.ToTensor(),
    tv.transforms.Lambda(lambda x: x.broadcast_to(3, x.shape[1], x.shape[2])),
])

0

这很容易! 以'resnet50'为例: 在执行之前,您应该拥有:

resnet_50= torchvision.models.resnet50()     
print(resnet_50.conv1)

Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)

就这样做!

resnet_50.conv1 = nn.Conv2d(1, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)

最后一步是更新state_dict。

resnet_50.state_dict()['conv1.weight'] = resnet_50.state_dict()['conv1.weight'].sum(dim=1, keepdim=True)

如果按照以下方式运行:

print(resnet_50.conv1)

结果将会是:

Conv2d(1, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)

正如您所看到的,输入通道是用于灰度图像的。


0
如果您已经在使用scikit-image,您可以通过使用gray2RGB来获得所需的结果。
from skimage.color import gray2rgb
rgb_img = gray2rgb(gray_img)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接