はじめに
プロジェクトの先行調査で、振動センサーを用いた異常検知アルゴリズムを作った時の備忘録です。
改訂履歴
- 2026/10/13 : 初版公開。
本文
1. 環境
- SciPy 1.18.1
- NumPy 2.5.4
- Python 3.15.0
- Visual Studio Community 2022
2. 概要
FA の世界には予知保全という考え方があります。センサーや IoT を利用して設備の状態を監視し、故障の兆候を検知することで、計画的な保全につなげる考え方です。設備の突発的な停止(ダウンタイム)を抑制し、生産性の向上を目指します。最近では状態基準保全 (Condition-Based Maintenance / CBM) と呼ばれることもありますね(厳密には同義ではないようです)。今回は予知保全への応用を想定し、振動センサーの計測データを用いた異常検知アルゴリズムを実装します。
3. RMS
振動の解析には RMS (Root Mean Square / 二乗平均平方根) という業界標準の統計的手法があるようです。$N$ はサンプル数、$x_i$ は各サンプルの計測値です。
$$
\mathrm{RMS} = \sqrt{\frac{1}{N}\sum_{i=1}^{N}x_i^2}
$$
数式を見るとウエェっとなるかもしれませんが、計算手順自体はシンプルです。例えばサンプル値が $(2, -4, 4)$ の場合は以下のように計算します。
| 項 | 手順 | 計算 | 結果 |
|---|---|---|---|
| 1 | 各サンプル値を 2 乗する | $(2^2,(-4)^2,4^2)$ | 4, 16, 16 |
| 2 | 全 2 乗値を合計する | $(4+16+16)$ | 36 |
| 3 | サンプル数で割る | $(36 \div 3)$ | 12 |
| 4 | 最後に平方根を取る | $(\sqrt{12})$ | 約 3.464 |
RMS は振動信号の実効的な大きさを表します。算出した RMS を正常時の基準値や設定した閾値と比較することで、振動の増大を検知できます。今回はこの RMS と閾値を用いた異常検知処理を実装します。
3-1. RMS で評価する物理量
RMS は計算方法であり、加速度・速度・変位のいずれにも適用できます。一般的な回転機械の振動監視では、RMS 速度が有力な評価指標となるため、今回はこれを採用します。ただし振動センサーから取得したデータは、そのままでは加速度です。RMS 速度を算出するには、加速度信号を積分して速度信号に変換する処理が必要になります。
| 項目 | 物理量 | 単位の例 | 説明 |
|---|---|---|---|
| RMS 速度 | 振動速度 | mm/s | 回転機械の広帯域振動の大きさ、機械状態の監視。 |
| RMS 加速度 | 振動加速度 | m/s², g | 高周波振動や衝撃を含む振動の評価。ただし、軸受の初期損傷などは RMS だけでは不十分な場合がある。 |
| RMS 変位 | 振動変位 | μm, mm | 低周波の変位や軸振動など、変位量が重要な用途。 |
4. ISO 20816
ISO 20816 は、機械の振動測定と評価に関する国際的なシリーズ規格です。回転機械の振動状態を評価する際には、振動速度の RMS 値が指標として用いられます。今回は RMS 速度を用いた異常検知を設計するにあたり、この規格を参考にします。ただし、閾値は対象機械や測定条件に対応する規格の評価基準を確認して設定する必要があります。
5. 実装
今回の処理は、以下の手順で RMS 速度を算出し、閾値と比較して異常を判定します。
| 項 | 処理名 | 処理内容 |
|---|---|---|
| 1 | 入力検証 | 時刻と加速度のデータ数や数値の有効性を確認します。また時刻の単調増加とサンプリング間隔の均一性を検証します。 |
| 2 | 前処理 | 加速度データの DC オフセット(センサーの測定値に本来の振動とは関係のない一定の値のズレ)を除去し、ハイパスフィルタで低周波成分を抑制します。これにより、積分結果への不要な成分の影響を軽減します。 |
| 3 | 速度変換 | 台形則で加速度を時間積分(つまり台形を使って速度の変化量を推定)し、速度データを算出します。その後、速度の平均値を除去します。 |
| 4 | RMS 速度算出 | 速度データの二乗平均平方根を求め、単位を m/s から mm/s に変換します。 |
| 5 | 異常判定 | 算出した RMS 速度を閾値と比較し、閾値以上であれば異常と判定します。 |
以下はソースです。
import numpy as np
from scipy.signal import butter, sosfiltfilt
# =============================================================================
# 定数
# =============================================================================
# 1 秒間のサンプリング回数 (Hz)
SAMPLE_RATE_HZ = 1000.0
# 継続時間 (s)
SAMPLE_DURATION_S = 2.0
# ハイパスフィルタのカットオフ周波数 (Hz)
HIGHPASS_HZ = 5.0
# 閾値 (mm/s)
THRESHOLD_MM_S = 4.5
# サンプル用の時刻データ (s)
SAMPLE_TIME_S = np.arange(
0.0,
SAMPLE_DURATION_S,
1.0 / SAMPLE_RATE_HZ,
)
# サンプル用の加速度データ (m/s²)
SAMPLE_ACCELERATION_M_S2 = (
0.5 * np.sin(2.0 * np.pi * 50.0 * SAMPLE_TIME_S)
+ 0.05 * np.sin(2.0 * np.pi * 10.0 * SAMPLE_TIME_S)
)
# =============================================================================
# RMS 速度の算出
# =============================================================================
def calculate_rms_velocity(
time_s: np.ndarray,
acceleration: np.ndarray,
highpass_hz: float = HIGHPASS_HZ,
) -> float:
time_s = np.asarray(time_s, dtype=float)
acceleration = np.asarray(acceleration, dtype=float)
if time_s.ndim != 1 or acceleration.ndim != 1:
raise ValueError("入力データは 1 次元配列にしてください。")
if len(time_s) != len(acceleration) or len(time_s) < 16:
raise ValueError("データ数が不正です。")
if not (
np.all(np.isfinite(time_s))
and np.all(np.isfinite(acceleration))
):
raise ValueError("NaN または無限大が含まれています。")
dt = np.diff(time_s)
if np.any(dt <= 0):
raise ValueError("時刻は単調増加である必要があります。")
dt_mean = np.mean(dt)
if not np.allclose(dt, dt_mean, rtol=0.02, atol=1e-12):
raise ValueError("サンプリング間隔が不均一です。")
fs = 1.0 / dt_mean
nyquist = fs / 2.0
if not 0 < highpass_hz < nyquist:
raise ValueError("カットオフ周波数が不正です。")
# 加速度の DC オフセットを除去する
acceleration = acceleration - np.mean(acceleration)
# ハイパスフィルタ
sos = butter(4, highpass_hz, btype="highpass", fs=fs, output="sos")
filtered_acceleration = sosfiltfilt(sos, acceleration)
# 台形則で加速度を積分し、速度を算出する
velocity = np.zeros_like(filtered_acceleration)
velocity[1:] = np.cumsum(
(filtered_acceleration[1:] + filtered_acceleration[:-1])
* 0.5
* dt
)
# 速度の平均値を除去する
velocity -= np.mean(velocity)
# m/s から mm/s に変換して RMS を算出する
rms_velocity = np.sqrt(np.mean(velocity ** 2))
return float(rms_velocity * 1000.0)
# =============================================================================
# 異常判定
# =============================================================================
def detect_anomaly(
rms_velocity: float,
threshold: float = THRESHOLD_MM_S,
) -> bool:
if threshold <= 0:
raise ValueError("閾値は正の値にしてください。")
return rms_velocity >= threshold
# =============================================================================
# メイン処理
# =============================================================================
def main():
# 定数からデータを取得する
time_s = SAMPLE_TIME_S
acceleration = SAMPLE_ACCELERATION_M_S2
# RMS 速度の算出
rms_velocity = calculate_rms_velocity(
time_s,
acceleration,
)
# 異常判定
anomaly = detect_anomaly(
rms_velocity,
THRESHOLD_MM_S,
)
# 表示
print(f"RMS 速度 : {rms_velocity:.3f} mm/s")
print(f"閾値 : {THRESHOLD_MM_S:.3f} mm/s")
if anomaly:
print("判定結果 : 異常")
else:
print("判定結果 : 正常")
# =============================================================================
# プログラムの実行開始位置
# =============================================================================
if __name__ == "__main__":
main()
6. 参考
おわりに
これをベースに、ハードに合わせて調整していく感じになるかなと思います。多分 ![]()