エッジAI

異常検知

正常から外れた挙動を見つけるAI技術。

概要

異常検知(Anomaly Detection)とは、データのパターンや挙動が「正常」と定義された状態から逸脱している点を自動的に検出するAI・機械学習技術です。製造業における製品の欠陥検出・機械の予知保全・IoTセンサーの異常監視など、組み込みシステムの分野で特に重要な技術です。

異常検知は以下の三種類に分類されます。

種類内容
点異常(Point Anomaly)単一データ点が異常センサー値の突発的な異常値
文脈異常(Contextual Anomaly)文脈(時系列・位置)から見て異常夏の低温値(単体では正常)
集団異常(Collective Anomaly)複数点のパターンが異常機器の振動が徐々に増大

エッジAIにおける異常検知の大きな特徴は「正常データだけで学習できる」点です。製造現場では良品のデータは大量に集まりますが、不良品のデータは少ないため、正常分布を学習して外れ値を検出するアプローチが非常に有効です。

歴史・背景

異常検知の研究は統計学的手法から始まり、機械学習・ディープラーニングへと発展してきました。

手法の変遷:

  • 1960〜1980年代:Mahalanobis距離・統計的仮説検定による外れ値検出
  • 1990〜2000年代:SVM(One-Class SVM)・kNN(k近傍法)の応用
  • 2000年代:Isolation Forest、LOF(局所外れ値因子法)の提案
  • 2014年:オートエンコーダ(Autoencoder)を使った異常検知が実用化
  • 2016年:VAE(変分オートエンコーダ)による確率的異常スコア算出
  • 2019年:SPADE・PaDiM・PatchCoreなど工業外観検査向け高精度手法の登場
  • 2021年:MVTec Anomaly Detection Datasetが公開され、ベンチマーク整備
  • 2022年以降:エッジデバイス向け軽量異常検知モデルの研究活発化

技術仕様

異常検知アルゴリズムの分類

異常検知手法
├── 統計的手法
│   ├── z-score(標準偏差ベース)
│   ├── Mahalanobis距離
│   └── CUSUM(累積和制御図)

├── 機械学習ベース
│   ├── Isolation Forest(孤立森)
│   ├── One-Class SVM
│   ├── LOF(局所外れ値因子)
│   └── K-Means クラスタリング

└── ディープラーニングベース
    ├── Autoencoder(再構成誤差)
    ├── VAE(変分オートエンコーダ)
    ├── GAN(生成的敵対ネットワーク)
    ├── PatchCore(特徴マップ距離)
    └── LSTM/TCN(時系列異常検知)

異常スコアの計算

異常検知では各データ点に「異常スコア」を割り当て、閾値を超えた場合に異常と判定します。

import numpy as np

def compute_anomaly_score(normal_features, test_features, method='mahalanobis'):
    """異常スコアを計算"""
    
    if method == 'mahalanobis':
        # Mahalanobis距離: 分散・共分散を考慮した距離
        mean = np.mean(normal_features, axis=0)
        cov = np.cov(normal_features.T)
        cov_inv = np.linalg.inv(cov + 1e-6 * np.eye(len(mean)))
        
        scores = []
        for feat in test_features:
            diff = feat - mean
            score = np.sqrt(diff @ cov_inv @ diff)
            scores.append(score)
        return np.array(scores)
    
    elif method == 'knn':
        # k近傍距離: 正常データセットの最近傍k点への平均距離
        from sklearn.neighbors import NearestNeighbors
        k = 5
        nbrs = NearestNeighbors(n_neighbors=k).fit(normal_features)
        distances, _ = nbrs.kneighbors(test_features)
        return distances.mean(axis=1)
    
    elif method == 'reconstruction':
        # 再構成誤差(オートエンコーダ用)
        # normal_features: 再構成後の特徴量
        # test_features: 元の特徴量
        return np.mean((normal_features - test_features) ** 2, axis=1)

# 閾値設定(正常データの99パーセンタイル)
def determine_threshold(normal_scores, percentile=99):
    threshold = np.percentile(normal_scores, percentile)
    return threshold

オートエンコーダによる異常検知

オートエンコーダは正常データのみで学習し、入力データを低次元に圧縮(エンコード)した後に復元(デコード)します。異常データは正常パターンに沿った復元ができないため、再構成誤差が大きくなります。

import tensorflow as tf

def build_autoencoder(input_dim, latent_dim=16):
    """1次元センサーデータ用オートエンコーダ"""
    # エンコーダ
    encoder = tf.keras.Sequential([
        tf.keras.layers.Dense(64, activation='relu',
                              input_shape=(input_dim,)),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(latent_dim, activation='relu')
    ])
    
    # デコーダ
    decoder = tf.keras.Sequential([
        tf.keras.layers.Dense(32, activation='relu',
                              input_shape=(latent_dim,)),
        tf.keras.layers.Dense(64, activation='relu'),
        tf.keras.layers.Dense(input_dim, activation='linear')
    ])
    
    # オートエンコーダ全体
    input_layer = tf.keras.Input(shape=(input_dim,))
    encoded = encoder(input_layer)
    decoded = decoder(encoded)
    autoencoder = tf.keras.Model(input_layer, decoded)
    
    autoencoder.compile(optimizer='adam', loss='mse')
    return autoencoder, encoder, decoder

# 正常データのみで学習
autoencoder, encoder, decoder = build_autoencoder(
    input_dim=128  # センサーの特徴量次元
)
autoencoder.fit(
    normal_data, normal_data,  # 入力=出力(自己教師あり)
    epochs=50,
    batch_size=32,
    validation_split=0.1
)

# 推論:再構成誤差を異常スコアとして使用
def predict_anomaly(autoencoder, data, threshold):
    reconstructed = autoencoder.predict(data)
    mse = np.mean((data - reconstructed) ** 2, axis=1)
    is_anomaly = mse > threshold
    return is_anomaly, mse

動作原理

時系列データの異常検知(IoTセンサー)

振動・温度・電流などの時系列センサーデータに対しては、滑動窓(スライディングウィンドウ)を使って特徴量を抽出します。

import numpy as np
from scipy import stats

def extract_features_from_window(window_data):
    """時系列ウィンドウから統計的特徴量を抽出"""
    features = {
        'mean': np.mean(window_data),
        'std': np.std(window_data),
        'max': np.max(window_data),
        'min': np.min(window_data),
        'rms': np.sqrt(np.mean(window_data**2)),
        'peak_to_peak': np.max(window_data) - np.min(window_data),
        'skewness': stats.skew(window_data),
        'kurtosis': stats.kurtosis(window_data),
        'energy': np.sum(window_data**2),
        # 周波数ドメイン特徴量(振動解析向け)
        'dominant_freq': get_dominant_frequency(window_data),
    }
    return np.array(list(features.values()))

def get_dominant_frequency(data, sample_rate=1000):
    """FFTで支配的な周波数を取得"""
    fft = np.fft.fft(data)
    freqs = np.fft.fftfreq(len(data), 1/sample_rate)
    magnitude = np.abs(fft[:len(fft)//2])
    dominant_freq = freqs[np.argmax(magnitude)]
    return dominant_freq

# リアルタイム処理ループ(C擬似コード)
// マイコンでの異常検知ループ(C言語)
#define WINDOW_SIZE 128
#define FEATURE_DIM 10
#define ANOMALY_THRESHOLD 0.85f

float sensor_buffer[WINDOW_SIZE];
float features[FEATURE_DIM];
int buffer_idx = 0;

void sensor_interrupt_handler(float new_value) {
    sensor_buffer[buffer_idx++] = new_value;
    
    if (buffer_idx >= WINDOW_SIZE) {
        // ウィンドウが満杯になったら特徴量抽出
        extract_features(sensor_buffer, features, WINDOW_SIZE);
        
        // 推論実行(TFLite Micro)
        float anomaly_score = run_anomaly_model(features);
        
        if (anomaly_score > ANOMALY_THRESHOLD) {
            trigger_alert(anomaly_score);
        }
        
        // ウィンドウをシフト(50%オーバーラップ)
        memmove(sensor_buffer, 
                sensor_buffer + WINDOW_SIZE/2,
                sizeof(float) * WINDOW_SIZE/2);
        buffer_idx = WINDOW_SIZE / 2;
    }
}

画像異常検知(外観検査)

工業製品の外観検査では、正常品の画像のみで学習し、異常部位を検出します。

正常品画像のみで学習(教師なし/半教師あり)

テスト画像を入力

特徴マップを抽出(ResNet, EfficientNetのバックボーン)

正常特徴分布との距離を計算

異常ヒートマップ生成(どのピクセルが異常か)

閾値処理 → 異常箇所の特定・可視化

用途・ユースケース

予知保全(Predictive Maintenance)

製造設備の振動センサー・温度センサー・電流センサーのデータを常時モニタリングし、故障の前兆を検知します。

正常運転時のパターンを学習

軸受けの異常振動 → 異常スコア上昇 → 保全アラート
モータ過熱     → 異常スコア上昇 → 緊急停止

コスト効果:

  • 計画外の生産停止を削減(1回の停止で数百万〜数億円の損失)
  • 過剰な定期メンテナンスを削減

外観検査(Visual Inspection)

半導体・電子部品・食品・医薬品などの製造ラインで高速・高精度な外観検査を実施します。

産業検出対象要求速度
半導体パターン欠陥・ボイド数ms/枚
プリント基板はんだ不良・部品欠品30FPS
食品異物混入・形状異常数百ms/枚
薬品ラベル印刷ミス数十ms/枚

ネットワーク・セキュリティ(IoTデバイス)

IoTデバイスの通信パターンを学習し、マルウェア感染や不正アクセスを検知します。

医療機器

ウェアラブルデバイスでの不整脈検知・血糖値の異常変動検知など。

実装・開発のポイント

閾値の決め方

閾値設定は異常検知システムの実用性を左右する重要なパラメータです。

def find_optimal_threshold(normal_scores, anomaly_scores):
    """ROC曲線からF1スコアが最大の閾値を探索"""
    from sklearn.metrics import roc_curve, f1_score
    import numpy as np
    
    # 正常=0、異常=1
    labels = np.array([0] * len(normal_scores) + 
                      [1] * len(anomaly_scores))
    scores = np.concatenate([normal_scores, anomaly_scores])
    
    # 様々な閾値でF1スコアを計算
    thresholds = np.percentile(scores, np.arange(50, 100, 1))
    best_f1 = 0
    best_threshold = thresholds[0]
    
    for threshold in thresholds:
        preds = (scores > threshold).astype(int)
        f1 = f1_score(labels, preds)
        if f1 > best_f1:
            best_f1 = f1
            best_threshold = threshold
    
    print(f"最適閾値: {best_threshold:.4f}, F1: {best_f1:.4f}")
    return best_threshold

# 実運用では正常データの99パーセンタイルを閾値にすることも多い
threshold_99 = np.percentile(normal_scores, 99)
print(f"99パーセンタイル閾値: {threshold_99:.4f}")

モデルの継続的更新

生産環境では機械の状態が変化するため、正常データを継続的に追加してモデルを更新することが重要です。

class AdaptiveAnomalyDetector:
    """正常データを継続的に学習する適応型異常検知器"""
    
    def __init__(self, window_size=1000):
        self.normal_buffer = []
        self.window_size = window_size
        self.model = IsolationForest(contamination=0.01)
    
    def update(self, new_normal_data):
        """新しい正常データでモデルを更新"""
        self.normal_buffer.extend(new_normal_data)
        
        # バッファが満杯になったら古いデータを削除
        if len(self.normal_buffer) > self.window_size:
            self.normal_buffer = self.normal_buffer[-self.window_size:]
        
        # モデルを再学習(定期的に実施)
        if len(self.normal_buffer) >= 100:
            self.model.fit(np.array(self.normal_buffer))
    
    def predict(self, data):
        """異常スコアを返す"""
        score = -self.model.score_samples([data])[0]
        return score

他技術との比較

比較軸異常検知物体検出画像分類
学習データ正常データのみで可大量のラベル付きデータ必要ラベル付きデータ必要
学習コスト低〜中
異常の種類未知の異常に対応既知クラスのみ既知クラスのみ
解釈性中(異常スコア)高(BOX座標)中(確率)
リアルタイム性高(軽量モデル多い)中〜高

異常検知はエッジAIの中でも特に産業IoT向けの重要な応用分野です。TinyMLTensorFlow Liteなどと組み合わせることでマイコン上での常時監視が実現し、推論アクセラレータを活用すれば高速な外観検査も可能になります。

関連用語

参考リンク