AI(機械学習)を使って磁気センサ閾値を定量的に決定しよう

(更新

Redesign of a door-lock monitoring system. Originally built with reed switches, the updated system now uses 3-axis magnetic sensors and SVM machine learning for enhanced accuracy.

Illustration of an 80s-style robot working on an electrical control panel in a brighter, warm-hued small factory setting. Beside the robot, there's an old-fashioned oscilloscope and circuit tester.

相模原市で IoT 設計を受託しているファームロジックスです。

以前(2019年頃)に、リードスイッチを使って玄関の錠の開閉状態を監視するシステムを作ったことがあります。

構造は簡単で、小型で強力なネオジムマグネットを錠のサムターン(ノブ)に固定し、錠のプラスチックケースにリードスイッチを取り付ける、というものです。

リードスイッチの開閉状態は、Raspberry Pi(ラズパイ)のソフトウェアで読み取り、一定時間、解錠状態になっているとメールで警告が来るようにしました。さらに、現在の施錠状態をウェブインターフェイスで確認できるようにしました。

その後 3年弱ほど運用していたのですが、2022年の初頭に、リードスイッチからの値が読めなくなってしまいました。正確にいうと、リードスイッチが常に閉路状態になってしまいました。

今回は、そのときに新たに採用した 3軸磁界センサによる設計と、サポートベクトルマシン(SVM)による機械学習の設計を紹介したいと思います。

3軸磁界センサの採用

当初は、リードスイッチの予備があったので交換することも考えたのですが、以下のような問題がありました。

  • リードスイッチの取り付けと位置決めが困難
  • 経年劣化の不安

一つ目の問題は、リードスイッチは単純な「デジタル」スイッチなので、当然ながら「閉」か「開」の2つの状態しか判別できない、というものです。そのため、リードスイッチの取り付け時には、サムターンを少しずつ回しながら、施錠と解錠を正しく判断できるように、微妙な位置決めをしなくてはならず、これは意外と面倒な作業でした。以前に製作したときも、この点で非常に苦労したのです。

もう一つの問題は、今回経験したように、リードスイッチは機械的にデリケートな部品なので、雑に扱うと破損、あるいは経年劣化することが分かったことです。想像ですが、前回の取り付け時にリードスイッチのリード線に無理な力を与えたことで、ガラスの封止にヒビが入るなどして中に少しずつ外気が入り込み、2年半程度で破損してしまったのではないかと思われます。あるいは、リードスイッチが剥き出しだったため、なんらかの不注意で、ガラスの封止を壊してしまったのかも知れません。

このようなことから、今回は 3軸の磁界センサを採用することにしました。3軸磁界センサは、現在ではスマートフォンなどで一般に使用されているものですが、今回の選定では、地球の地磁気を測定して方位を求めるタイプではなく、近接のマグネットとセンサの間の位置関係を正確に求めるものが必要です。

探していたところ、Melexis 社の MLX90393 というセンサがこの用途に利用でき、入手が容易であることが分かりました。

このセンサは広いダイナミックレンジを持ち、50mT(テスラ)でも飽和しないという特徴を持っています。また、センサは 16ビットの ADC を持っており、感度を上げた設定の場合には、LSB あたり 0.2μT 程度の分解能があります。

なお、データシートによると、このセンサは 2015年頃には既に市場に投入されていたようですが、私が前回にリードスイッチで製作をしたときには、このようなセンサを使うことは思いつきませんでした。

センサの入手と取り付け

センサのブレークアウトとしては、SparkFun 社の以下のものを利用することにしました。

これを、以下のように取り付けました。

サムターンの周りにある囲いは、大昔に、ピッキング事件が多発したときに取り付けたものです。その囲いの右外側に見えるのが、センサボードを収めたケースです。(このケースは 3D プリンタで製作しました。)

黒いサムターンの右端に見える数ミリ径の円筒状の突起が、以前から今回流量したネオジムマグネットです。

参考までに、Raspberry Pi 側の結線を示します。I2C インターフェイスに接続しています。ちなみに、以前は 2線だったのが 4線になり、配線がやや窮屈になってしまいました。

センサ値の読み出し

Raspberry Pi による MLX90393 の読み出しには、以下の GitHub サイトで公開されているライブラリを利用させて頂きました。

なお、Raspberry Pi の I2C ライブラリとしては、以下を利用しました。

さて、次の問題は、上記で読み出せた X, Y, Z の値から、どのようにして施錠と解錠を判別するかです。

まず最初に、X, Y, Z の読み値がどのように振る舞うのか、グラフにプロットして確認してみることにしましょう。次のようなコードを書きました。LINEAR_XYZ というのは、実際にサムターンを少しずつ回しながら取得した値です。

LINEAR_XYZ = [
    (643, -811, 773),  # 完全施錠
    (458, -717, 18),
    (420, -608, -77),
    (368, -474, -146),
    (295, -263, -178),
    (254, -166, -153),
    (235, -133, -122),
    (230, -128, -116),
    (222, -119, -103),
    (221, -116, -103),
    (217, -111, -94),
    (214, -108, -85),  # 完全解錠
]

import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = [8, 8]
plt.rcParams['figure.dpi'] = 100
fig = plt.figure()
ax = fig.add_subplot(projection='3d')

for (x, y, z) in LINEAR_XYZ:
    ax.scatter(x, y, z, marker='o')

ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')

plt.show()

次のようなプロット結果が得られました。

立体視でないので分かりづらいのですが、右上の青いドットが完全施錠、左端のオレンジのドットが完全解錠に該当します。なお、実際に Matplotlib でプロットする場合は、マウス操作でプロットを回転できますので、もう少し分かりやすいです。

機械学習によるドア施錠解錠の判定

上のプロットを見ると分かるように、プロットは円弧状の曲線となり、施錠から解錠に向かって、

  • X軸: 単調減少
  • Y軸: 単調増加(プロットの平面射影からは分かりづらい)
  • Z軸: 減少してから増加

という形になっています。

このような値(X, Y, Z)から解錠/施錠状態を判定するのは、意外と厄介な問題です。基本的な考えとしては、この 3次元空間に、施錠と解錠を分けるスレッシュホールドを決めれば良い訳です。しかし、2次元でのプロットであれば、ある程度「えいやっ」で決めることもできますが、3次元以上のセンサ値に対してスレッシュホールドを決めるのは大変です。

このような場合に利用できる機械学習の技術として、サポートベクトルマシン(SVM) というものがあります。今回は、有名な機械学習のライブラリ群である scikit-learn の SVM ライブラリを利用することにしました。

まずは、機械学習のための訓練用データを用意します。厳密には、多くの実装サンプルから大量のデータを得るべきですが、今回は個人的な利用なので、一つの実装サンプルから 20個程度のデータを得るのにとどめました。以下のような訓練用データになりました。

import numpy as np

WITH_LABELS_XYZ = np.array([
    [0, 217, -109, -84],
    [1, 229, -125, -119],
    [2, 285, -252, -172],
    [3, 643, -811, 773],
    [0, 217, -109, -83],
    [1, 226, -127, -119],
    [2, 298, -279, -176],
    [3, 637, -815, 756],
    [0, 214, -107, -82],
    [1, 228, -124, -114],
    [2, 288, -256, -175],
    [3, 644, -808, 771],
    [0, 218, -108, -89],
    [1, 237, -137, -133],
    [2, 296, -270, -176],
    [3, 639, -807, 749],
    [0, 214, -108, -84],
    [1, 237, -137, -130],
    [2, 317, -320, -174],
    [3, 642, -789, 746],
], dtype=float)

左端のカラムが教師データ(ラベル)であり、ここでは

  • 0: 完全解錠
  • 1: ほぼ解錠
  • 2: いちおう施錠
  • 3: 完全施錠

としました。残りのカラムが、センサの読み値 X, Y, Z です。

このデータを使って、SVM をトレーニングします。

from sklearn.svm import SVC

label = (WITH_LABELS_XYZ[:, 0] > 1).astype(float)
X = WITH_LABELS_XYZ[:, 1:]
y = label
svm = SVC(kernel="linear", C=1)
svm.fit(X, y)

データ数が少ないので、トレーニング(学習)は、ほぼ一瞬で完了します。

続いて、元の学習用データを使って分類(予測)をさせてみます。(今回はデータが少量なので、訓練用、テスト用、検証用、といったセットは作っていません。)

結果として、次のように、無事に分類できていることを確認できました。

svm.predict(X)

array([0., 0., 1., 1., 0., 0., 1., 1., 0., 0., 1., 1., 0., 0., 1., 1., 0., 0., 1., 1.])

決定境界(スレッシュホールド)のプロット

最後に、分類器の決定境界(decision boundary)をプロットしてみましょう。

plt.rcParams['figure.figsize'] = [8, 8]
plt.rcParams['figure.dpi'] = 100
fig = plt.figure()
ax = fig.add_subplot(projection='3d')

for x, y, z, v in np.c_[X, np.array(label)]:
    if v > 0:
        c = 'blue'
    else:
        c = 'red'
    ax.scatter(x, y, z, color=c, marker='o')

zf = lambda x, y: (-svm.intercept_[0] - svm.coef_[0][0] * x - svm.coef_[0][1] *
                   y) / svm.coef_[0][2]
xs, ys = np.meshgrid(np.linspace(220, 350, 11), np.linspace(-500, -160, 11))

ax.plot_surface(xs, ys, zf(xs, ys), alpha=0.5)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plt.show()

プロット結果を示します。半透明の青い平面が決定境界です。

このままでは見づらいので、決定境界を直角に見えるように向きを調整してみます。

青いドットが施錠状態、赤いドットが解錠状態と判定されていることを示します。

これを見る限りでは、実際の運用において、施錠と解錠を互いに誤判定することは、まずなさそうです。(もちろん、商品化などをするのであれば、マグネットの取り付け方、センサの取り付け位置などをもっと厳密に設計し、大規模な訓練データセットを用意すべきでしょう。あるいは、出荷時のキャリブレーションや個別トレーニングが必要かも知れません。)

まとめ

以前に、マグネットとリードスイッチを使って、玄関の施錠状態を確認するセキュリティシステムを製作しましたが、今回、リードスイッチの破損により、3軸磁界センサで新しく設計し直しました。

センサの読み値を、目視で施錠と解錠に分類することも可能かと思いますが、3次元のセンサでは困難が伴うため、scikit-learn によりサポートベクトル分類器を設計してみました。

お問い合わせはお気軽に!

お問い合わせを頂いた後、継続して営業活動をしたり、ニュースレター等をお送りしたりすることはございません。
御返答は 24時間以内(営業時間中)とさせて頂いております。もし返答が届かない場合、何らかの事情でメールが不達となっている可能性がございます。大変お手数ですが、別のメールアドレス等で督促頂けますと幸いです。

コメントを残す