作者 | 哈尔滨工业大学(威海)讲师 赵明航
本文解读了一种新的、基于深度学习的故障诊断算法,即残差收缩网络(Residual Shrinkage Network)。
从功能上讲,残差收缩网络是一种面向强噪声或者高度冗余数据的特征学习方法。本文首先回顾了相关基础知识,然后介绍了残差收缩网络的动机和具体实现,希望对大家有所帮助。
1. 相关基础
残差收缩网络主要建立在三个部分的基础之上:残差网络、软阈值函数和注意力机制。
1.1 残差网络
残差网络无疑是近年来最成功的深度学习算法之一,在谷歌学术上的引用已经突破四万次。相较于普通的卷积神经网络,残差网络采用跨层恒等路径的方式,缓解了深层网络的训练难度。
残差网络的主干部分是由很多残差模块堆叠而成的,其中一种常见的残差模块如下图所示。
1.2 软阈值函数
软阈值函数是大部分降噪方法的核心步骤。首先,我们需要设置一个正数阈值。该阈值不能太大,即不能大于输入数据绝对值的最大值,否则输出会全部为零。
然后,软阈值函数会将绝对值低于这个阈值的输入数据设置为零,并且将绝对值大于这个阈值的输入数据也朝着零收缩,其输入与输出的关系如下图 (a) 所示。
软阈值函数的输出 y 对输入 x 的导数如上图 (b) 所示。我们可以发现,其导数要么取值为 0,要么取值为 1。从这个角度看的话,软阈值函数和 ReLU 激活函数有一定的相似之处,也有利于深度学习算法训练时梯度的反向传播。
值得注意的是,阈值的选取对软阈值函数的结果有着直接的影响,至今仍是一个难题。
1.3注意力机制
注意力机制是近年来深度学习领域的超级研究热点,而 Squeeze-and-Excitation Network (SENet)则是最为经典的注意力算法之一。
如下图所示,SENet 通过一个小型网络学习得到一组权值系数,用于各个特征通道的加权。这其实是一种注意力机制:首先评估各个特征通道的重要程度,然后根据其重要程度赋予各个特征通道合适的权重。
如下图所示,SENet 可以与残差模块集成在一起。在这种模式下,由于跨层恒等路径的存在,SENet 可以更容易得到训练。另外,值得指出的是,每个样本的权值系数都是根据其自身设置的;也就是说,每个样本都可以有自己独特的一组权值系数。
2. 残差收缩网络
接下来,本部分针对残差收缩网络的动机、实现、优势和验证,分别展开了介绍。
2.1 动机
首先,大部分现实世界中的数据,包括图片、语音或者振动,都或多或少地含有噪声或者冗余信息。
从广义上讲,在一个样本里面,任何与当前模式识别任务无关的信息,都可以被认为是噪声或者冗余信息。这些噪声或者冗余信息很可能会对当前的模式识别任务造成不利的影响。
其次,对于任意的两个样本,它们的噪声或冗余含量经常是不同的。换言之,有些样本所含的噪声或冗余要多一些,有些要少一些。这就要求我们在设计算法的时候,应该使算法具备根据每个样本的特点、单独设置相关参数的能力。
在上述两点的驱动下,我们能不能将传统信号降噪算法中的软阈值函数引入深度残差网络之中呢?软阈值函数中的阈值应该怎样选取呢?残差收缩网络就给出了一种答案。
2.2 实现
残差收缩网络融合了深度残差网络、SENet 和软阈值函数。如下图所示,残差收缩网络就是将残差模式下的 SENet 中的“重新加权”替换成了“软阈值化”。
在 SENet 中,所嵌入的小型网络是用于获取一组权值系数;在残差收缩网络中,该小型网络则是用于获取一组阈值。
为了获得合适的阈值,相较于原始的 SENet,残差收缩网络里面的小型网络的结构也进行了调整。具体而言,该小型网络所输出的阈值,是(各个特征通道的绝对值的平均值)×(一组 0 和 1 之间的系数)。
通过这种方式,残差收缩网络不仅确保了所有阈值都为正数,而且阈值不会太大(不会使所有输出都为 0)。
如下图所示,残差收缩网络的整体结构与普通的深度残差网络是一致的,包含了输入层、刚开始的卷积层、一系列的基本模块以及最后的全局均值池化和全连接输出层等。
2.3 优势
首先,软阈值化所需的阈值,是通过一个小网络自动设置的,避免了人工设置阈值所需要的专业知识。
然后,深度残差收缩网络确保了软阈值函数的阈值为正数,而且在合适的取值范围之内,避免了输出全部为零的情况。
同时,每个样本都有自己独特的一组阈值,使得深度残差收缩网络适用于各个样本的噪声含量不同的情况。
3. 结论
由于噪声或者冗余信息是无处不在的,深度残差收缩网络,或者说这种“注意力机制”+“软阈值函数”的思路,或许有着广阔的拓展空间和应用范围。
论文链接:
https://ieeexplore.ieee.org/document/8850096
代码链接:
https://github.com/zhao62/Deep-Residual-Shrinkage-Networks
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Dec 28 23:24:05 2019
Implemented using TensorFlow 1.0.1 and Keras 2.2.1
M. Zhao, S. Zhong, X. Fu, et al., Deep Residual Shrinkage Networks for Fault Diagnosis,
IEEE Transactions on Industrial Informatics, 2019, DOI: 10.1109/TII.2019.2943898
@author: super_9527
"""
from __future__ import print_function
import keras
import numpy as np
from keras.datasets import mnist
from keras.layers import Dense, Conv2D, BatchNormalization, Activation
from keras.layers import AveragePooling2D, Input, GlobalAveragePooling2D
from keras.optimizers import Adam
from keras.regularizers import l2
from keras import backend as K
from keras.models import Model
from keras.layers.core import Lambda
K.set_learning_phase(1)
# Input image dimensions
img_rows, img_cols = 28, 28
# The data, split between train and test sets
(x_train, y_train), (x_test, y_test) = mnist.load_data()
if K.image_data_format() == 'channels_first':
x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols)
x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)
input_shape = (1, img_rows, img_cols)
else:
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
input_shape = (img_rows, img_cols, 1)
# Noised data
x_train = x_train.astype('float32') / 255. + 0.5*np.random.random([x_train.shape[0], img_rows, img_cols, 1])
x_test = x_test.astype('float32') / 255. + 0.5*np.random.random([x_test.shape[0], img_rows, img_cols, 1])
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')
# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)
def abs_backend(inputs):
return K.abs(inputs)
def expand_dim_backend(inputs):
return K.expand_dims(K.expand_dims(inputs,1),1)
def sign_backend(inputs):
return K.sign(inputs)
def pad_backend(inputs, in_channels, out_channels):
pad_dim = (out_channels - in_channels)//2
inputs = K.expand_dims(inputs,-1)
inputs = K.spatial_3d_padding(inputs, ((0,0),(0,0),(pad_dim,pad_dim)), 'channels_last')
return K.squeeze(inputs, -1)
# Residual Shrinakge Block
def residual_shrinkage_block(incoming, nb_blocks, out_channels, downsample=False,
downsample_strides=2):
residual = incoming
in_channels = incoming.get_shape().as_list()[-1]
for i in range(nb_blocks):
identity = residual
if not downsample:
downsample_strides = 1
residual = BatchNormalization()(residual)
residual = Activation('relu')(residual)
residual = Conv2D(out_channels, 3, strides=(downsample_strides, downsample_strides),
padding='same', kernel_initializer='he_normal',
kernel_regularizer=l2(1e-4))(residual)
residual = BatchNormalization()(residual)
residual = Activation('relu')(residual)
residual = Conv2D(out_channels, 3, padding='same', kernel_initializer='he_normal',
kernel_regularizer=l2(1e-4))(residual)
# Calculate global means
residual_abs = Lambda(abs_backend)(residual)
abs_mean = GlobalAveragePooling2D()(residual_abs)
# Calculate scaling coefficients
scales = Dense(out_channels, activation=None, kernel_initializer='he_normal',
kernel_regularizer=l2(1e-4))(abs_mean)
scales = BatchNormalization()(scales)
scales = Activation('relu')(scales)
scales = Dense(out_channels, activation='sigmoid', kernel_regularizer=l2(1e-4))(scales)
scales = Lambda(expand_dim_backend)(scales)
# Calculate thresholds
thres = keras.layers.multiply([abs_mean, scales])
# Soft thresholding
sub = keras.layers.subtract([residual_abs, thres])
zeros = keras.layers.subtract([sub, sub])
n_sub = keras.layers.maximum([sub, zeros])
residual = keras.layers.multiply([Lambda(sign_backend)(residual), n_sub])
# Downsampling using the pooL-size of (1, 1)
if downsample_strides > 1:
identity = AveragePooling2D(pool_size=(1,1), strides=(2,2))(identity)
# Zero_padding to match channels
if in_channels != out_channels:
identity = Lambda(pad_backend, arguments={'in_channels':in_channels,'out_channels':out_channels})(identity)
residual = keras.layers.add([residual, identity])
return residual
# define and train a model
inputs = Input(shape=input_shape)
net = Conv2D(8, 3, padding='same', kernel_initializer='he_normal', kernel_regularizer=l2(1e-4))(inputs)
net = residual_shrinkage_block(net, 1, 8, downsample=True)
net = BatchNormalization()(net)
net = Activation('relu')(net)
net = GlobalAveragePooling2D()(net)
outputs = Dense(10, activation='softmax', kernel_initializer='he_normal', kernel_regularizer=l2(1e-4))(net)
model = Model(inputs=inputs, outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer=Adam(), metrics=['accuracy'])
model.fit(x_train, y_train, batch_size=100, epochs=5, verbose=1, validation_data=(x_test, y_test))
# get results
K.set_learning_phase(0)
DRSN_train_score = model.evaluate(x_train, y_train, batch_size=100, verbose=0)
print('Train loss:', DRSN_train_score[0])
print('Train accuracy:', DRSN_train_score[1])
DRSN_test_score = model.evaluate(x_test, y_test, batch_size=100, verbose=0)
print('Test loss:', DRSN_test_score[0])
print('Test accuracy:', DRSN_test_score[1])