導入
故障検知での解析モデルとしては、自己回帰モデルやLSTMが一般的な印象ですが、対象製品あるいは故障モードの内容によってはRNN系よりも寧ろCNN系で時系列方向の情報を畳み込む方がより精度の良い検知が出来るのではないかと考え、試してみました。
データセットについて
今回はMonash Univ.で配布されている無料データセット内のWaferを使用します。
https://bridges.monash.edu/articles/dataset/Modified_UCR_Datasets/9759743
このデータは半導体製作プロセスにおいてウエハ上のセンサより検出された時系列データと良品不良品の情報を含むデータです。こういった製品の故障原因は経年劣化によるものというよりは、短時間における異常加熱や温度制御不良によるものと予測されます。
そのため時系列順序を追うLSTMのようなモデルよりも、CNNのように局所パターンや瞬間的な変化を追うモデルの方がこのタスクに適していると思います。
このデータセットでは1列目にラベルが格納されており、2列目以降はセンサから検出された時系列データのようです。また、このデータセットの特徴として、時系列長が一定長ではない点になります。
Pos/Neg比率確認
なぜかラベルが0,1ではなく0,-1になっていたのでラベルエンコーダで修正し、比率を確認します。
Negの比率が低いですが、実際の製造現場でも不良品が出ることは稀ですので、より実務に依ったデータかなという印象です。

EDA
EDA(探索的データ分析)と言うと大袈裟かもしれませんが、POS/NEGそれぞれをランダム抽出し、どういった傾向があるか確認します。
下のグラフは、最初の100secの時系列データをPOS/NEGそれぞれ3つずつランダムに選択して3回表示させたグラフになります。
陰性データは最初の数secの波形が振動している印象がありました。
この情報をCNNで拾い、うまく二値分類することが目標になります。



データセットの作成
データセットの作成をします。今回、時系列データを畳み込みするためにチャンネルの次元を追加します。
class SeqDataset(Dataset):
#(N,L)->(N,C=1,L)にして返す
def __init__(self,X,y):
self.X = torch.from_numpy(X[:,None,:]).float()
self.y = torch.from_numpy(y).float() #BCE用にfloat
def __len__(self):
return len(self.X)
def __getitem__(self,i):
return self.X[i], self.y[i]
"""
モデルの作成
ハイパーパラメータはざっくり下記の通り。
| Subject | Detail |
|---|---|
| k-fold数 | 5 |
| エポック数 | 15 |
| バッチサイズ | 128 |
| 最適化モデル | AdamW |
| 学習率 | 1.0e-3 |
今回、以下の3つのモデルで分類させてみました。
CNN
class CNN1DClassifier(nn.Module):
def __init__(self,in_ch=1):
super().__init__()
self.net = nn.Sequential(
nn.Conv1d(in_ch,64,kernel_size=5,padding=2,bias=False),
nn.BatchNorm1d(64),
nn.ReLU(inplace=True),
nn.Conv1d(64,128,kernel_size=3,padding=1,bias=False),
nn.BatchNorm1d(128),
nn.ReLU(inplace=True),
nn.Conv1d(128,128,kernel_size=3,padding=2,dilation=2,bias=False),
nn.BatchNorm1d(128),
nn.ReLU(inplace=True),
nn.AdaptiveAvgPool1d(1),
nn.Flatten(),
nn.Linear(128,1)
)
def forward(self,x):
return self.net(x).squeeze(1)
LSTM
class LSTMClassifier(nn.Module):
def __init__(self, in_ch=1, hidden=64):
super().__init__()
self.lstm = nn.LSTM(input_size=in_ch, hidden_size=hidden,
batch_first=True, bidirectional=True)
self.fc = nn.Linear(hidden*2, 1)
def forward(self, x):
# x: (B, C, L) → (B, L, C)
x = x.permute(0, 2, 1)
out, _ = self.lstm(x)
out = out[:, -1, :] # 最終時刻の出力
return self.fc(out).squeeze(1)
CNN(Residual Block,Dilated Conv導入)
class CNN1DClassifier_revA(nn.Module):
def __init__(self, in_ch=1, base=64, p=0.2):
super().__init__()
self.stem = nn.Sequential(
nn.Conv1d(in_ch, base, 5, padding=2, bias=False),
nn.BatchNorm1d(base), nn.ReLU(True)
)
self.b1 = ResDilBlock(base, base, d=1, p=p)
self.b2 = ResDilBlock(base, base*2, d=2, p=p)
self.b3 = ResDilBlock(base*2, base*2, d=4, p=p)
self.b4 = ResDilBlock(base*2, base*4, d=8, p=p)
self.head = nn.Sequential(
nn.AdaptiveAvgPool1d(1), nn.Flatten(),
nn.Linear(base*4, 128), nn.ReLU(True), nn.Dropout(p),
nn.Linear(128, 1)
)
def forward(self, x):
x = self.stem(x); x = self.b1(x); x = self.b2(x); x = self.b3(x); x = self.b4(x)
return self.head(x).squeeze(1)
検証
不均衡データのため、CNN1D、LSTM、CNN1D@ResBlockそれぞれにおけるPR曲線を作成、AP(Average Precision)を算出しました。
| モデル | AP |
|---|---|
| CNN | 0.973 |
| LSTM | 0.904 |
| CNN+ResBlock | 0.903 |
結果・考察
単純なCNNとLSTMではモデル性能に大きな差は見られませんでした。
これは、単純なCNNでは受容野が狭く、十分な時系列コンテキストを捉えきれず、
一方LSTMも局所的な形状変化を明確に捉えることが難しかったことが原因と考えられます。
一方、Dilated ConvolutionとResidual Blockを導入したモデルでは性能が明確に向上しました。
Dilated Convolutionにより広い受容野が得られ、スパイク状の局所変化だけでなくその前後の波形パターンも捉えられるようになったこと、
さらにResidual Blockの導入により層が深くなっても入力情報が失われず、
細かな波形変化を保持したまま高次特徴を抽出できたことが寄与したと考えられます。
サマリー
今回の異常検知タスクではCNN系モデル(+ResBlock、Dilated Conv導入)がLSTM系モデルより優れた性能を示しました。
検出したい故障モードや異常パターンによって、CNN系、LSTM系を含む様々なアーキテクチャを試すことで、より適切なモデルを選択できる可能性を示せたと思います。
