論文題目:Limited Data Rolling Bearing Fault Diagnosis With Few-Shot Learning
文獻地址:https://ieeexplore.ieee.org/abstract/document/8793060
源碼【Keras版】: https://github.com/SNBQT/Limited-Data-Rolling-Bearing-Fault-Diagnosis-with-Few-shot-Learning
出於學習,對其源碼簡單復現為Tensorflow版,個人認為要比源碼好讀一些,歡迎評論指導哈:https://github.com/monologuesmw/bearing-fault-diagnosis-cnn 也可以點擊右上角進入我的github主頁。
之前聽吳恩達老師的卷積神經網絡課程中one shot learning problem關於孿生網絡(Siamese Neural Network)在員工人臉識別場景當中的應用。感覺員工人臉識別的應用場景與機械設備故障診斷的場景十分類似,都具有樣本量少的共性。而且Siamese網絡的理論也非常符合應用在故障診斷的場景中。只是當時對conv1d的操作不熟悉,不知道conv1d的卷積操可以用於時序的操作。
日前,對Limited Data Rolling Bearing Fault Diagnosis With Few-Shot Learning這篇論文進行拜讀,其使用Siamese網絡對西儲大學軸承數據進行故障診斷,並且對故障診斷提出了Few-Shot Learning 的概念。
Siamese網絡在故障診斷中的效果
如下圖所示,訓練樣本數量在60到19800,WDCNN、One-Shot、Five-Shot的准確率要比SVM高很多,更適合小數據量的樣本。
 
下圖c、d中是90個訓練樣本的混淆矩陣,可以看出One-Shot比WDCNN更易於在第2、3、8類的判斷。
 
 
故障診斷的現狀
近年來,基於深度學習的智能故障診斷技術由於避免了依賴耗時且不可靠的人工分析,提高了故障診斷的效率,引起了人們的廣泛關注。然而,這些技術方案需要大量的訓練樣本(深度學習網絡模型的訓練大多需要大量的訓練樣本)。在現實的生產實際中,不同工況下,同一故障的信號也會有很大的區別。這就使得故障診斷面臨一個極大的挑戰:對於各種故障,難以獲得充足的樣本去訓練一個魯棒性強的分類器。造成這一情形的原因有以下四個方面:
- 由於各種故障發生可能造成的后果,工業場景不允許故障狀態的發生;
 - 大多數機電故障發生緩慢,器件老化長達數月甚至數年;
 - 機械系統的工作環境十分復雜,並且頻繁更改(由於生產需求);
 - 實際應用中,故障類型和工作條件經常是不平衡的;
 
深度學習目前在機器視覺、圖像視頻處理、語音識別、NLP等領域如火如荼。
深度學習用於故障診斷的模型:AE、RBM、CNNs、RNNs、GANs【各模型的參考文獻可以查看原文】
論文貢獻
- 使用基於WDCNN(第一層卷積核的尺寸較大)卷積神經網絡作為孿生網絡的子網絡,為軸承的故障診斷提出了在數據量缺失的情況下,Few-Shot-Learning的應用。
 - 首次證明了基於小樣本學習的故障診斷模型可以充分利用相同或不同的類樣本對,從只有單個或少數樣本的類中識別出測試樣本,從而提高故障診斷的性能。
 - 隨着訓練樣本數量的增加,當測試數據集與訓練數據集有顯著差異時,測試性能不會單調增加。
 
實現思路
Siamese Network
如果拜讀過SiamFC、SiamRPN、SiamMask等目標跟蹤文獻,對於Siamese網絡應該並不陌生。這里為了便於論文的敘述,先對Siamese網絡進行直觀上的描述。
Siamese孿生網絡,顧名思義,其網絡結構是雙生的。如下圖所示,樣本對(X1, X2)同時輸入Gw(x)網絡中,產生Gw(X1)和Gw(X2)兩個輸出,並通過某種相似度進行判斷,產生X1與X2的相似程度。這種思想實際上是模板匹配的一種。兩個Gw(x)網絡參數權值共享。也就是說,雖然是兩個網絡,但實際上是一個,為了模型的並行計算,視為孿生。

先以員工人臉識別為例,X2就是入職時采集存儲的個人圖片,而X1則是每次刷臉時采集的圖片;對於沒有入職的小伙伴,可以想象刷臉支付的場景,X2是開通刷臉支付,錄入的面部信息,而X1則是在商超自助支付時,采集到的面部信息。指紋支付亦是如此。而刷臉支付偶爾會遇到點點頭,搖搖頭,眨眨眼等操作,這實際上是活體檢測的一種方式,為了驗證鏡頭前的你不是一個照片、視頻 或者 假體面具。扯得有些遠了...
Few-Shot learning
如下圖所示,Few-Shot learning實際上是One-Shot learning的多次使用。首先,在訓練過程中,使用相同類別,或者不同類別構成樣本對(x1, x2),輸出則是兩個輸入樣本對是否屬於同一類的概率(訓練的真值表則是0或者1)。與傳統的分類不同,Few-Shot learning的性能通常由N-shot K-way測試來衡量,如圖下圖(c)中所示。

在測試的過程中,One-shot K-way的測試(上圖b)實際上與員工檢測相同。而N-shot K-way的測試(上圖c)實際上是每個類別多存了幾個模板,在測試的過程中,需要與所有的模板均進行比對,然后生成每一次比對是否相似的概率,再做出最終的決策。也就是說,N是存儲樣本的個數,也是比對的次數,K是類別數。
one-shot K-way testing
在測試的過程中,測試樣本x與模板集S的K個類別樣本進行比較,生成相應的相似度。【是one-shot】
 
然后,選取相似度最大的作為當前測試樣本的類別。
 ![]()
N-shot K-way testing
對於N-shot的測試場景,可以通過N個shot的相似度進行求和,選擇N個求和之后相似度的最大值作為當前測試樣本的類別。
 ![]()
目前不確定N次結果投票的方式與N次求和的方式那種精度更高。
實現細節
對於軸承故障診斷的應用場景,唯一的不同是上述為圖像信號,而故障診斷是數據信號。
few-shot learning的實現包含3個步驟,如下圖所示:
- 數據准備(下圖頂部) 
          
- 訓練樣本對的生成: 相同或不同類別的數據組成樣本對【可重復】,並生成0或1真值
 - 測試樣本對的生成:一個測試樣本與在訓練集中隨機無重復選取K個類別,N個模板組成樣本對。 ---- 在測試的過程,每一個測試樣本均要與從訓練樣本中隨機生成的K*N個樣本進行比對,測試過程時間相對較長。不過在實際應用中,故障的發生不會瞬間得接踵而至,時長的問題不是問題。
 
 - 模型訓練(下圖左側)
 
-  
          
- 模型的輸入是樣本對,輸出是相似度的概率值,相當於二分類的過程。
 
 
3. 模型測試(下圖右側)
-  
          
- 源碼中使用的是N-shot次的循環,即每一次都是一個one-shot-testing的測試。
 
 

Siamese網絡的子網絡選用WDCNN模型,其結構如下圖所示:

WDCNN第一層使用比較大的卷積核進行特征提取; 然后使用尺寸較小的卷積核進行更好的特征表達。第一層如果采用小尺寸的卷積核,極易受到工業場景中高頻噪聲的影響。WDCNN網絡結構如下圖所示:
 
輸入數據是原始的振動信號,這里不需要進行任何的特征工程。【凱斯西儲大學軸承故障數據】
訓練過程中相似度的度量采用1范數:
 
通過相似度的判斷,便可以計算出當前樣本對的相似程度。也就是說,在獲得WDCNN的兩個輸出后,再對輸出進行1范數的計算。
之后,再通過一個一維的全連接FC生成最終的相似度。(全連接使用dropout)
 
損失函數使用分類的交叉熵損失函數,並進行2范數正則化。
 
優化策略選則Adam。
P.S. 當然,直接使用WDCNN,外接類別個數的全連接層(使用dropout),全連接層使用softmax激活函數,也可以直接實現分類,效果也很不錯。
https://github.com/monologuesmw/bearing-fault-diagnosis-by-wdcnn
西儲大學軸承故障數據描述(github中,我只選取了前5類故障進行實驗)

在實驗中,作者分別對以下4個方面進行了驗證:
- 訓練樣本個數對於實驗結果的影響;
 - 添加噪聲對於實驗結果的影響;
 - 在新的故障類別出現時的性能;
 - 新工況的性能
 
模型結構代碼Keras版:
完整代碼地址: https://mekhub.cn/as/fault_diagnosis_with_few-shot_learning/
1 def load_siamese_net(input_shape = (2048,2)): 2 left_input = Input(input_shape) 3 right_input = Input(input_shape) 4 5 convnet = Sequential() 6 7 # WDCNN 8 convnet.add(Conv1D(filters=16, kernel_size=64, strides=16, activation='relu', padding='same',input_shape=input_shape)) 9 convnet.add(MaxPooling1D(strides=2)) 10 convnet.add(Conv1D(filters=32, kernel_size=3, strides=1, activation='relu', padding='same')) 11 convnet.add(MaxPooling1D(strides=2)) 12 convnet.add(Conv1D(filters=64, kernel_size=2, strides=1, activation='relu', padding='same')) 13 convnet.add(MaxPooling1D(strides=2)) 14 convnet.add(Conv1D(filters=64, kernel_size=3, strides=1, activation='relu', padding='same')) 15 convnet.add(MaxPooling1D(strides=2)) 16 convnet.add(Conv1D(filters=64, kernel_size=3, strides=1, activation='relu')) 17 convnet.add(MaxPooling1D(strides=2)) 18 convnet.add(Flatten()) 19 convnet.add(Dense(100,activation='sigmoid')) 20 21 # print('WDCNN convnet summary:') 22 # convnet.summary() 23 24 #call the convnet Sequential model on each of the input tensors so params will be shared 25 encoded_l = convnet(left_input) 26 encoded_r = convnet(right_input) 27 #layer to merge two encoded inputs with the l1 distance between them 28 L1_layer = Lambda(lambda tensors:K.abs(tensors[0] - tensors[1])) 29 #call this layer on list of two input tensors. 30 L1_distance = L1_layer([encoded_l, encoded_r]) 31 D1_layer = Dropout(0.5)(L1_distance) 32 prediction = Dense(1,activation='sigmoid')(D1_layer) 33 siamese_net = Model(inputs=[left_input,right_input],outputs=prediction) 34 35 # optimizer = Adam(0.00006) 36 optimizer = Adam() 37 #//TODO: get layerwise learning rates and momentum annealing scheme described in paperworking 38 siamese_net.compile(loss="binary_crossentropy",optimizer=optimizer) 39 # print('\nsiamese_net summary:') 40 # siamese_net.summary() 41 # print(siamese_net.count_params()) 42 43 return siamese_net
模型結構代碼Tensorflow版
完整代碼地址:https://github.com/monologuesmw/bearing-fault-diagnosis-cnn
1 def siamese_base_structure(self, inputs, reuse): 2 4 with slim.arg_scope([slim.conv1d], padding="same", activation_fn=slim.nn.relu, 5 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 6 weights_regularizer=slim.l2_regularizer(0.005) 7 ): 8 net = slim.conv1d(inputs=inputs, num_outputs=16, kernel_size=64, stride=16, reuse=reuse, scope="conv_1") 9 # tf.summary.histogram("conv_1", net) 10 def_max_pool = tf.layers.MaxPooling1D(pool_size=2, strides=2, padding="VALID", name="max_pool_2") 11 net = def_max_pool(net) 12 # tf.summary.histogram("max_pool_2", net) 13 14 net = slim.conv1d(net, num_outputs=32, kernel_size=3, stride=1, reuse=reuse, scope="conv_3") 15 # tf.summary.histogram("conv_3", net) 16 def_max_pool = tf.layers.MaxPooling1D(pool_size=2, strides=2, padding="VALID", name="max_pool_4") 17 net = def_max_pool(net) 18 # tf.summary.histogram("max_pool_4", net) 19 20 net = slim.conv1d(net, num_outputs=64, kernel_size=2, stride=1, reuse=reuse, scope="conv_5") 21 # tf.summary.histogram("conv_5", net) 22 def_max_pool = tf.layers.MaxPooling1D(pool_size=2, strides=2, padding="VALID", name="max_pool_6") 23 net = def_max_pool(net) 24 # tf.summary.histogram("max_pool_6", net) 25 26 net = slim.conv1d(net, num_outputs=64, kernel_size=3, stride=1, reuse=reuse, scope="conv_7") 27 # tf.summary.histogram("conv_7", net) 28 def_max_pool = tf.layers.MaxPooling1D(pool_size=2, strides=2, padding="VALID", name="max_pool_8") 29 net = def_max_pool(net) 30 # tf.summary.histogram("max_pool_8", net) 31 32 net = slim.conv1d(net, num_outputs=64, kernel_size=3, stride=1, padding="VALID", reuse=reuse, scope="conv_9") 33 # tf.summary.histogram("conv_9", net) 34 def_max_pool = tf.layers.MaxPooling1D(pool_size=2, strides=2, padding="VALID", name="max_pool_10") 35 net = def_max_pool(net) 36 # tf.summary.histogram("max_pool_10", net) 37 38 net = slim.flatten(net, scope="flatten_11") 39 # tf.summary.histogram("flatten_11", net) 40 41 output_step_one = slim.fully_connected(net, num_outputs=100, activation_fn=tf.nn.sigmoid, reuse=reuse, 42 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 43 weights_regularizer=slim.l2_regularizer(0.005), 44 scope="fully_connected_12") 45 # tf.summary.histogram("fully_connected_12", output_step_one) 46 return output_step_one 47 48 def siamese_network_structure(self, s="train"): 49 if s=="train": 50 # siamese_network_structure rest 51 left_ouput = self.siamese_base_structure(inputs=self.inputs_base_structure_left, reuse=False) 52 else: 53 left_ouput = self.siamese_base_structure(inputs=self.inputs_base_structure_left, reuse=True) 54 right_output = self.siamese_base_structure(inputs=self.inputs_base_structure_right, reuse=True) # siam network two results 55 56 L1_distance = tf.math.abs(left_ouput - right_output, 57 name="L1_distance") # two tensor result substract 58 # tf.summary.histogram("L1_distance_13", L1_distance) 59 net = slim.dropout(L1_distance, keep_prob=self.keep_prob, scope="dropout_14") 60 # tf.summary.histogram("dropout_14", net) 61 a = tf.Variable(tf.zeros([1])) 62 if s =="train": 63 prob_output = slim.fully_connected(net, num_outputs=1, activation_fn=tf.nn.sigmoid, 64 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 65 weights_regularizer=slim.l2_regularizer(0.005), reuse=False, 66 scope="fully_connected_15") 67 else: 68 # biases_initializer = slim.zero_initializer(ref=a), 69 # biases_regularizer = slim.l2_regularizer(0.005), 70 prob_output = slim.fully_connected(net, num_outputs=1, activation_fn=tf.nn.sigmoid, 71 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 72 weights_regularizer=slim.l2_regularizer(0.005), reuse=True, 73 scope="fully_connected_15") 74 # tf.summary.histogram("fully_connected_15", prob_output) 75 return prob_output
