使用 Keras 稀疏分类交叉熵进行像素级多类分类

Posted

技术标签:

【中文标题】使用 Keras 稀疏分类交叉熵进行像素级多类分类【英文标题】:Use of Keras Sparse Categorical Crossentropy for pixel-wise multi-class classification 【发布时间】:2019-06-05 18:34:25 【问题描述】:

我将首先披露我是机器学习和 Keras 的新手,除了一般的 CNN 二元分类器外,我知道的不多。我正在尝试使用 U-Net 架构(TF 后端)对许多 256x256 图像执行逐像素多类分类。换句话说,我输入一个 256x256 的图像,我希望它输出一个 256x256 的“掩码”(或标签图像),其中的值是 0-30 之间的整数(每个整数代表一个唯一的类)。我正在使用 2 个 1080Ti NVIDIA GPU 进行训练。

当我尝试执行 one-hot 编码时,我得到一个 OOM 错误,这就是为什么我使用稀疏分类交叉熵作为我的损失函数而不是常规分类交叉熵。但是,在训练我的 U-Net 时,我的损失值从头到尾都是“nan”(它初始化为 nan 并且永远不会改变)。当我通过将所有值除以 30(因此它们从 0-1 开始)来标准化我的“掩码”时,我得到 ~0.97 的准确度,我猜这是因为我图像中的大多数标签都是 0(它只是输出一堆0)。

这是我正在使用的 U-Net:

def unet(pretrained_weights = None,input_size = (256,256,1)):
inputs = keras.engine.input_layer.Input(input_size)
conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(inputs)
conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1)
pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)
conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool1)
conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv2)
pool2 = MaxPooling2D(pool_size=(2, 2))(conv2)
conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool2)
conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv3)
pool3 = MaxPooling2D(pool_size=(2, 2))(conv3)
conv4 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool3)
conv4 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv4)
#drop4 = Dropout(0.5)(conv4)
drop4 = SpatialDropout2D(0.5)(conv4)
pool4 = MaxPooling2D(pool_size=(2, 2))(drop4)

conv5 = Conv2D(1024, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool4)
conv5 = Conv2D(1024, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv5)
#drop5 = Dropout(0.5)(conv5)
drop5 = SpatialDropout2D(0.5)(conv5)

up6 = Conv2D(512, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(drop5))
merge6 = concatenate([drop4,up6], axis = 3)
conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge6)
conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv6)

up7 = Conv2D(256, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv6))
merge7 = concatenate([conv3,up7], axis = 3)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge7)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv7)

up8 = Conv2D(128, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv7))
merge8 = concatenate([conv2,up8], axis = 3)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge8)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv8)

up9 = Conv2D(64, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv8))
merge9 = concatenate([conv1,up9], axis = 3)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge9)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9)
conv9 = Conv2D(32, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9)
conv10 = Conv2D(1, 1, activation = 'softmax')(conv9)
#conv10 = Flatten()(conv10)
#conv10 = Dense(65536, activation = 'softmax')(conv10)
flat10 = Reshape((65536,1))(conv10)
#conv10 = Conv1D(1, 1, activation='linear')(conv10)

model = Model(inputs = inputs, outputs = flat10)

opt = Adam(lr=1e-6,clipvalue=0.01)
model.compile(optimizer = opt, loss = 'sparse_categorical_crossentropy', metrics = ['sparse_categorical_accuracy'])
#model.compile(optimizer = Adam(lr = 1e-6), loss = 'sparse_categorical_crossentropy', metrics = ['accuracy'])
#model.compile(optimizer = Adam(lr = 1e-4),

#model.summary()

if(pretrained_weights):

    model.load_weights(pretrained_weights)

return model

请注意,我需要展平输出只是为了让稀疏分类交叉熵正常运行(由于某种原因它不喜欢我的 2D 矩阵)。

这是一个训练运行的示例(只有 1 个 epoch,因为无论我运行多少次都是一样的)

model = unet()
model.fit(x=x_train, y=y_train, batch_size=1, epochs=1, verbose=1, validation_split=0.2, shuffle=True)

对 2308 个样本进行训练,对 577 个样本进行验证 纪元 1/1 2308/2308 [===============================] - 191s 83ms/步 - 损失:nan - sparse_categorical_accuracy: 0.9672 - val_loss :南 - val_sparse_categorical_accuracy:0.9667 出[18]:

如果需要更多信息来诊断问题,请告诉我。提前致谢!

【问题讨论】:

嘿,我现在也遇到了同样的问题,下面的两个解决方案在目标和输出方面存在形状错误。你解决了吗? 【参考方案1】:

现在看来,您可以简单地在最后一个Conv2D 层上进行softmax 激活,然后指定categorical_crossentropy 损失并在图像上进行训练,而无需任何重塑技巧。我已经使用虚拟数据集进行了测试,并且效果很好。试试吧~!

inp = keras.Input(...)
# define your model here
out = keras.layers.Conv2D(classes, (1, 1), activation='softmax') (...)
model = keras.Model(inputs=[inp], outputs=[out], name='unet')
model.compile(loss='categorical_crossentropy',
                      optimizer='adam',
                      metrics=['accuracy'])
model.fit(tensor4d, tensor4d)

您也可以使用sparse_categorical_crossentropy 进行编译,然后使用形状为(samples, height, width) 的输出进行训练,其中输出中的每个像素对应一个类标签:model.fit(tensor4d, tensor3d)

PS。我使用来自tensorflow.keraskeras (tensorflow 2)

【讨论】:

【参考方案2】:

OOM:

创建一个自定义函数来导出 one-hot 编码,而不是使用像“to_categorical”这样的预定义函数。

它需要 1/4 的内存量(在我的情况下)。

【讨论】:

【参考方案3】:
conv10 = Conv2D(nclasses, kernel_size=(1, 1))(up9)
out = BatchNormalization()(conv10)
out = Reshape((img_height*img_width, nclasses), input_shape=(img_height, img_width, nclasses))(out)
out = Activation('softmax')(out)


model = Model(inputs=[inputs], outputs=[out])
model.compile(optimizer = Adam(lr = 1e-4), loss = 'sparse_categorical_crossentropy', metrics = ['sparse_categorical_accuracy'])

x_train :(batch_size, 224, 224, 3) float32(输入图像) y_train: (batch_size, 50176, 1) uint8(目标标签)

上面的代码似乎适用于多类分割(nclasses),其中目标标签不是一个热编码。如果您的数据大小和/或模型非常大,一种热编码会产生内存问题。

最后一层有形状 (None, 50176, 16) (因为 nclasses=16, None corr to batch)。标签中的元素的值为 0 - (nclasses-1)

在类 index(-1) 上使用 argmaxreshaping 外部输出似乎是诀窍,以防你想要一个更正。图像输出...

注意:稀疏分类熵似乎在 keras 2.2.2 及更高版本中存在问题!!!

【讨论】:

嗨,我按照你的建议做了。我的目标形状为(65536,1),最后一层为(65536,9)。但我遇到张量形状错误。我在任何地方都找不到稀疏熵的实现。你能告诉我哪里出错了【参考方案4】:

问题在于,对于多类分类,你需要输出一个每个类别一维的向量,它代表了对该类别的置信度。如果你想识别 30 个不同的类,那么你的最后一层应该是一个 3D 张量,(256, 256, 30)。

conv10 = Conv2D(30, 1, activation = 'softmax')(conv9)
flat10 = Reshape((256*256*30,1))(conv10)

opt = Adam(lr=1e-6,clipvalue=0.01)
model.compile(optimizer = opt, loss = 'sparse_categorical_crossentropy', metrics = 
['sparse_categorical_accuracy'])

我假设您的输入是一个 (256, 256, 1) 浮点张量,其值在 0 和 1 之间,而您的目标是一个 (256*256) Int 张量。

这有帮助吗?

【讨论】:

我的印象是只有常规分类交叉熵才需要一次性编码(因此需要 256x256x30 输出)。从关于稀疏分类交叉熵的有限信息来看,您似乎不需要一次性编码,这可以解决我的记忆错误。绝对不是这样吗? 稀疏分类交叉熵采用稀疏目标,但不采用稀疏预测,而是采用稠密张量进行预测。试试看吧。 如果您需要更多内存,您可以将图像大小调整为 224x224(这是 CNN 比 256x256 更常见的尺寸),或者您可以移除 U-net 的最中心层,这占据最大的尺寸。 @TaoLin 我用 image(256,256,1) 和 target to be(256*256) 尝试了同样的事情,但是我在目标和输出之间出现了形状错误。因为目标是(65536),输出是(65536*30)。关于这些形状应该如何的任何帮助?

以上是关于使用 Keras 稀疏分类交叉熵进行像素级多类分类的主要内容,如果未能解决你的问题,请参考以下文章

Keras 和 TensorFlow 中所有这些交叉熵损失之间有啥区别?

稀疏分类交叉熵未按预期工作

为啥训练多类语义分割的unet模型中的分类交叉熵损失函数非常高?

keras 分类和二元交叉熵

如何在 Keras 的 FCN(U-Net)上使用加权分类交叉熵?

具有对数损失的 TensorFlow 单 sigmoid 输出与具有稀疏 softmax 交叉熵损失的两个线性输出,用于二进制分类