MacOSでのIoTデバイス用の部屋検出器の構築
公開: 2022-03-10自分がどの部屋にいるかを知ることで、照明の点灯からTVチャンネルの変更まで、さまざまなIoTアプリケーションが可能になります。 では、あなたとあなたの電話がキッチン、寝室、またはリビングルームにいる瞬間をどのように検出できますか? 今日のコモディティハードウェアには、無数の可能性があります。
1つの解決策は、各部屋にBluetoothデバイスを装備することです。 お使いの携帯電話がBluetoothデバイスの範囲内に入ると、Bluetoothデバイスに基づいて、携帯電話はどの部屋にあるかを認識します。 ただし、Bluetoothデバイスのアレイを維持することは、バッテリーの交換から機能不全のデバイスの交換まで、かなりのオーバーヘッドです。 さらに、Bluetoothデバイスに近いことが常に答えであるとは限りません。リビングルームにいる場合、キッチンと共有されている壁のそばで、キッチン家電が食べ物をかき回し始めてはいけません。
実用的ではありませんが、もう1つの解決策はGPSを使用することです。 ただし、GPSは屋内ではうまく機能せず、多数の壁、その他の信号、その他の障害物がGPSの精度に大きな打撃を与えることに注意してください。
代わりに、私たちのアプローチは、電話が接続されていないネットワークも含め、すべての範囲内のWiFiネットワークを活用することです。 方法は次のとおりです。キッチンでのWiFiAの強度を検討します。 キッチンと寝室の間に壁があるので、寝室のWiFiAの強度はかなり異なると合理的に予想できます。 この違いを利用して、どの部屋にいるかを予測できます。さらに、隣人からのWiFiネットワークBは、リビングルームからのみ検出できますが、キッチンからは事実上見えません。 これにより、予測がさらに簡単になります。 要約すると、すべての範囲内WiFiのリストは、豊富な情報を提供します。
この方法には、次の明確な利点があります。
- より多くのハードウェアを必要としません。
- WiFiのようなより安定した信号に依存しています。
- GPSなどの他の技術が弱い場合にうまく機能します。
壁が多いほど、WiFiネットワークの強度が異なるため、部屋の分類が容易になります。 データを収集し、データから学習し、いつでもどの部屋にいるかを予測するシンプルなデスクトップアプリを作成します。
SmashingMagの詳細:
- インテリジェントな会話型UIの台頭
- デザイナーのための機械学習のアプリケーション
- IoTエクスペリエンスのプロトタイプを作成する方法:ハードウェアの構築
- 感情的なもののインターネットのためのデザイン
前提条件
このチュートリアルでは、MacOSXが必要です。 コードはどのプラットフォームにも適用できますが、Macの依存関係のインストール手順のみを提供します。
- Mac OSX
- Homebrew、MacOSXのパッケージマネージャー。 インストールするには、コマンドをコピーしてbrew.shに貼り付けます
- NodeJS10.8.0 +とnpmのインストール
- Python3.6以降とpipのインストール。 「virtualenvのインストール方法、pipを使用したインストール方法、およびパッケージの管理方法」の最初の3つのセクションを参照してください。
ステップ0:作業環境のセットアップ
デスクトップアプリはNodeJSで作成されます。 ただし、 numpy
のようなより効率的な計算ライブラリを活用するために、トレーニングと予測のコードはPythonで記述されます。 まず、環境をセットアップし、依存関係をインストールします。 プロジェクトを格納するための新しいディレクトリを作成します。
mkdir ~/riot
ディレクトリに移動します。
cd ~/riot
pipを使用して、Pythonのデフォルトの仮想環境マネージャーをインストールします。
sudo pip install virtualenv
riot
という名前のPython3.6仮想環境を作成します。
virtualenv riot --python=python3.6
仮想環境をアクティブ化します。
source riot/bin/activate
プロンプトの前に(riot)
が表示されます。 これは、仮想環境に正常に移行したことを示しています。 pip
を使用して次のパッケージをインストールします。
-
numpy
:効率的な線形代数ライブラリ scipy
:人気のある機械学習モデルを実装する科学計算ライブラリ
pip install numpy==1.14.3 scipy ==1.1.0
作業ディレクトリの設定では、範囲内のすべてのWiFiネットワークを記録するデスクトップアプリから始めます。 これらの記録は、機械学習モデルのトレーニングデータを構成します。 データが手元にあると、以前に収集されたWiFi信号でトレーニングされた最小二乗分類器を作成します。 最後に、最小二乗モデルを使用して、範囲内のWiFiネットワークに基づいて現在の部屋を予測します。
ステップ1:最初のデスクトップアプリケーション
このステップでは、ElectronJSを使用して新しいデスクトップアプリケーションを作成します。 まず、代わりにノードパッケージマネージャーnpm
とダウンロードユーティリティwget
を使用します。
brew install npm wget
まず、新しいノードプロジェクトを作成します。
npm init
これにより、パッケージ名とバージョン番号の入力を求められます。 ENTER
を押して、デフォルトのriot
名とデフォルトのバージョン1.0.0
を受け入れます。
package name: (riot) version: (1.0.0)
これにより、プロジェクトの説明を求めるプロンプトが表示されます。 空でない説明を追加します。 以下、説明はroom detector
です
description: room detector
これにより、エントリポイント、またはプロジェクトを実行するメインファイルの入力を求められます。 app.js
と入力します。
entry point: (index.js) app.js
これにより、 test command
とgit repository
の入力を求められます。 今のところこれらのフィールドをスキップするには、 ENTER
を押してください。
test command: git repository:
これにより、 keywords
と作成author
の入力を求められます。 必要な値を入力します。 以下では、キーワードにiot
、 wifi
を使用し、作成者にJohn Doe
を使用しています。
keywords: iot,wifi author: John Doe
これにより、ライセンスの入力を求められます。 ENTER
を押して、 ISC
のデフォルト値を受け入れます。
license: (ISC)
この時点で、 npm
はこれまでの情報の要約を表示します。 出力は次のようになります。
{ "name": "riot", "version": "1.0.0", "description": "room detector", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "iot", "wifi" ], "author": "John Doe", "license": "ISC" }
ENTER
を押して受け入れます。 次に、 npm
はpackage.json
を生成します。 再確認するすべてのファイルを一覧表示します。
ls
これにより、このディレクトリ内の唯一のファイルが仮想環境フォルダとともに出力されます。
package.json riot
プロジェクトのNodeJS依存関係をインストールします。
npm install electron --global # makes electron binary accessible globally npm install node-wifi --save
以下を使用してファイルをダウンロードし、Electron QuickStartからmain.js
から開始します。 次の-O
引数は、 app.js
の名前をmain.js
に変更します。
wget https://raw.githubusercontent.com/electron/electron-quick-start/master/main.js -O app.js
nano
またはお気に入りのテキストエディタでapp.js
を開きます。
nano app.js
12行目で、 index.htmlをstatic / index.htmlに変更します。これは、すべてのHTMLテンプレートを含むstatic
ディレクトリを作成するためです。
function createWindow () { // Create the browser window. win = new BrowserWindow({width: 1200, height: 800}) // and load the index.html of the app. win.loadFile('static/index.html') // Open the DevTools.
変更を保存して、エディターを終了します。 ファイルは、 app.js
ファイルのソースコードと一致している必要があります。 次に、HTMLテンプレートを格納する新しいディレクトリを作成します。
mkdir static
このプロジェクト用に作成されたスタイルシートをダウンロードします。
wget https://raw.githubusercontent.com/alvinwan/riot/master/static/style.css?token=AB-ObfDtD46ANlqrObDanckTQJ2Q1Pyuks5bf79PwA%3D%3D -O static/style.css
nano
またはお気に入りのテキストエディタでstatic/index.html
を開きます。 標準のHTML構造から始めます。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Riot | Room Detector</title> </head> <body> <main> </main> </body> </html>
タイトルの直後に、GoogleFontsとスタイルシートでリンクされているMontserratフォントをリンクします。
<title>Riot | Room Detector</title> <!-- start new code --> <link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet"> <link href="style.css" rel="stylesheet"> <!-- end new code --> </head>
main
タグの間に、予測される部屋の名前のスロットを追加します。
<main> <!-- start new code --> <p class="text">I believe you're in the</p> <h1 class="title">(I dunno)</h1> <!-- end new code --> </main>
これで、スクリプトは次のように正確に一致するはずです。 エディターを終了します。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Riot | Room Detector</title> <link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet"> <link href="style.css" rel="stylesheet"> </head> <body> <main> <p class="text">I believe you're in the</p> <h1 class="title">(I dunno)</h1> </main> </body> </html>
次に、パッケージファイルを修正して開始コマンドを含めます。
nano package.json
7行目の直後に、 electron .
にエイリアスされたstart
コマンドを追加します。 。 前の行の最後に必ずコンマを追加してください。
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "electron ." },
保存して終了。 これで、ElectronJSでデスクトップアプリを起動する準備が整いました。 npm
を使用してアプリケーションを起動します。
npm start
デスクトップアプリケーションは、次のものと一致する必要があります。
これで、デスクトップアプリの起動が完了しました。 終了するには、ターミナルに戻り、CTRL + Cを押します。 次のステップでは、wifiネットワークを記録し、デスクトップアプリケーションUIから記録ユーティリティにアクセスできるようにします。
ステップ2:WiFiネットワークを記録する
このステップでは、すべての範囲内のWi-Fiネットワークの強度と頻度を記録するNodeJSスクリプトを記述します。 スクリプト用のディレクトリを作成します。
mkdir scripts
nano
またはお気に入りのテキストエディタでscripts/observe.js
を開きます。
nano scripts/observe.js
NodeJSwifiユーティリティとファイルシステムオブジェクトをインポートします。
var wifi = require('node-wifi'); var fs = require('fs');
完了ハンドラーを受け入れるrecord
関数を定義します。
/** * Uses a recursive function for repeated scans, since scans are asynchronous. */ function record(n, completion, hook) { }
新しい関数内で、wifiユーティリティを初期化します。 この値は現在無関係であるため、ランダムなWi-Fiインターフェイスに初期化するには、 iface
をnullに設定します。
function record(n, completion, hook) { wifi.init({ iface : null }); }
サンプルを含む配列を定義します。 サンプルは、モデルに使用するトレーニングデータです。 この特定のチュートリアルのサンプルは、範囲内のWi-Fiネットワークとそれに関連する長所、頻度、名前などのリストです。
function record(n, completion, hook) { ... samples = [] }
Wi-Fiスキャンを非同期的に開始する再帰関数startScan
を定義します。 完了すると、非同期Wi-Fiスキャンは再帰的にstartScan
を呼び出します。
function record(n, completion, hook) { ... function startScan(i) { wifi.scan(function(err, networks) { }); } startScan(n); }
wifi.scan
コールバックで、エラーまたはネットワークの空のリストを確認し、そうである場合はスキャンを再開します。
wifi.scan(function(err, networks) { if (err || networks.length == 0) { startScan(i); return } });
完了ハンドラーを呼び出す再帰関数の基本ケースを追加します。
wifi.scan(function(err, networks) { ... if (i <= 0) { return completion({samples: samples}); } });
進行状況の更新を出力し、サンプルのリストに追加して、再帰呼び出しを行います。
wifi.scan(function(err, networks) { ... hook(n-i+1, networks); samples.push(networks); startScan(i-1); });
ファイルの最後で、サンプルをディスク上のファイルに保存するコールバックを使用してrecord
関数を呼び出します。
function record(completion) { ... } function cli() { record(1, function(data) { fs.writeFile('samples.json', JSON.stringify(data), 'utf8', function() {}); }, function(i, networks) { console.log(" * [INFO] Collected sample " + (21-i) + " with " + networks.length + " networks"); }) } cli();
ファイルが以下と一致することを再確認してください。
var wifi = require('node-wifi'); var fs = require('fs'); /** * Uses a recursive function for repeated scans, since scans are asynchronous. */ function record(n, completion, hook) { wifi.init({ iface : null // network interface, choose a random wifi interface if set to null }); samples = [] function startScan(i) { wifi.scan(function(err, networks) { if (err || networks.length == 0) { startScan(i); return } if (i <= 0) { return completion({samples: samples}); } hook(n-i+1, networks); samples.push(networks); startScan(i-1); }); } startScan(n); } function cli() { record(1, function(data) { fs.writeFile('samples.json', JSON.stringify(data), 'utf8', function() {}); }, function(i, networks) { console.log(" * [INFO] Collected sample " + i + " with " + networks.length + " networks"); }) } cli();
保存して終了。 スクリプトを実行します。
node scripts/observe.js
出力は、可変数のネットワークを使用して、次のように一致します。
* [INFO] Collected sample 1 with 39 networks
収集したばかりのサンプルを調べます。 json_pp
にパイプしてJSONをきれいに印刷し、headにパイプして最初の16行を表示します。
cat samples.json | json_pp | head -16
以下は、2.4GHzネットワークの出力例です。
{ "samples": [ [ { "mac": "64:0f:28:79:9a:29", "bssid": "64:0f:28:79:9a:29", "ssid": "SMASHINGMAGAZINEROCKS", "channel": 4, "frequency": 2427, "signal_level": "-91", "security": "WPA WPA2", "security_flags": [ "(PSK/AES,TKIP/TKIP)", "(PSK/AES,TKIP/TKIP)" ] },
これで、NodeJSのwifiスキャンスクリプトは終了です。 これにより、すべての範囲内のWiFiネットワークを表示できます。 次のステップでは、デスクトップアプリからこのスクリプトにアクセスできるようにします。
ステップ3:スキャンスクリプトをデスクトップアプリに接続する
このステップでは、最初に、スクリプトをトリガーするためのボタンをデスクトップアプリに追加します。 次に、スクリプトの進行状況でデスクトップアプリのUIを更新します。
static/index.html
を開きます。
nano static/index.html
以下に示すように、「追加」ボタンを挿入します。
<h1 class="title">(I dunno)</h1> <!-- start new code --> <div class="buttons"> <a href="add.html" class="button">Add new room</a> </div> <!-- end new code --> </main>
保存して終了。 static/add.html
を開きます。
nano static/add.html
次の内容を貼り付けます。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Riot | Add New Room</title> <link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet"> <link href="style.css" rel="stylesheet"> </head> <body> <main> <h1 class="title">0</h1> <p class="subtitle">of <span>20</span> samples needed. Feel free to move around the room.</p> <input type="text" class="text-field" placeholder="(room name)"> <div class="buttons"> <a href="#" class="button">Start recording</a> <a href="index.html" class="button light">Cancel</a> </div> <p class="text"></p> </main> <script> require('../scripts/observe.js') </script> </body> </html>
保存して終了。 scripts/observe.js
を再度開きます。
nano scripts/observe.js
cli
関数の下に、新しいui
関数を定義します。
function cli() { ... } // start new code function ui() { } // end new code cli();
デスクトップアプリのステータスを更新して、関数の実行が開始されたことを示します。
function ui() { var room_name = document.querySelector('#add-room-name').value; var status = document.querySelector('#add-status'); var number = document.querySelector('#add-title'); status.style.display = "block" status.innerHTML = "Listening for wifi..." }
データをトレーニングデータセットと検証データセットに分割します。
function ui() { ... function completion(data) { train_data = {samples: data['samples'].slice(0, 15)} test_data = {samples: data['samples'].slice(15)} var train_json = JSON.stringify(train_data); var test_json = JSON.stringify(test_data); } }
まだcompletion
コールバック内で、両方のデータセットをディスクに書き込みます。
function ui() { ... function completion(data) { ... fs.writeFile('data/' + room_name + '_train.json', train_json, 'utf8', function() {}); fs.writeFile('data/' + room_name + '_test.json', test_json, 'utf8', function() {}); console.log(" * [INFO] Done") status.innerHTML = "Done." } }
適切なコールバックを使用してrecord
を呼び出し、20個のサンプルを記録し、サンプルをディスクに保存します。
function ui() { ... function completion(data) { ... } record(20, completion, function(i, networks) { number.innerHTML = i console.log(" * [INFO] Collected sample " + i + " with " + networks.length + " networks") }) }
最後に、必要に応じてcli
関数とui
関数を呼び出します。 cli();
を削除することから始めます。 ファイルの最後で呼び出します。
function ui() { ... } cli(); // remove me
ドキュメントオブジェクトがグローバルにアクセス可能かどうかを確認します。 そうでない場合、スクリプトはコマンドラインから実行されています。 この場合、 cli
関数を呼び出します。 そうである場合、スクリプトはデスクトップアプリ内からロードされます。 この場合、クリックリスナーをui
関数にバインドします。
if (typeof document == 'undefined') { cli(); } else { document.querySelector('#start-recording').addEventListener('click', ui) }
保存して終了。 データを保持するディレクトリを作成します。
mkdir data
デスクトップアプリを起動します。
npm start
次のホームページが表示されます。 「部屋を追加」をクリックします。
次のフォームが表示されます。 部屋の名前を入力します。 後で使用するので、この名前を覚えておいてください。 私たちの例はbedroom
です。
「録音開始」をクリックすると、「wifiをリッスンしています…」というステータスが表示されます。
20個のサンプルがすべて記録されると、アプリは次のように一致します。 ステータスは「完了」と表示されます。
誤った名前の「キャンセル」をクリックして、以下に一致するホームページに戻ります。
デスクトップUIからwifiネットワークをスキャンできるようになりました。これにより、記録されたすべてのサンプルがディスク上のファイルに保存されます。 次に、すぐに使用できる機械学習アルゴリズムをトレーニングします。収集したデータの最小二乗法です。
ステップ4:Pythonトレーニングスクリプトを書く
このステップでは、Pythonでトレーニングスクリプトを作成します。 トレーニングユーティリティ用のディレクトリを作成します。
mkdir model
model/train.py
開きます
nano model/train.py
ファイルの上部で、最小二乗モデルのnumpy
計算ライブラリとscipy
をインポートします。
import numpy as np from scipy.linalg import lstsq import json import sys
次の3つのユーティリティは、ディスク上のファイルからのデータのロードとセットアップを処理します。 ネストされたリストをフラット化するユーティリティ関数を追加することから始めます。 これを使用して、サンプルのリストのリストをフラット化します。
import sys def flatten(list_of_lists): """Flatten a list of lists to make a list. >>> flatten([[1], [2], [3, 4]]) [1, 2, 3, 4] """ return sum(list_of_lists, [])
指定されたファイルからサンプルをロードする2番目のユーティリティを追加します。 このメソッドは、サンプルが複数のファイルに分散しているという事実を抽象化し、すべてのサンプルに対して1つのジェネレーターのみを返します。 各サンプルのラベルは、ファイルのインデックスです。 たとえば、 get_all_samples('a.json', 'b.json')
を呼び出すと、 a.json
のすべてのサンプルにラベル0が付けられ、 b.json
のすべてのサンプルにラベル1が付けられます。
def get_all_samples(paths): """Load all samples from JSON files.""" for label, path in enumerate(paths): with open(path) as f: for sample in json.load(f)['samples']: signal_levels = [ network['signal_level'].replace('RSSI', '') or 0 for network in sample] yield [network['mac'] for network in sample], signal_levels, label
次に、bag-of-words風モデルを使用してサンプルをエンコードするユーティリティを追加します。 次に例を示します。2つのサンプルを収集するとします。
- 強度10のwifiネットワークAと強度15のwifiネットワークB
- 強度20のwifiネットワークBと強度25のwifiネットワークC。
この関数は、サンプルごとに3つの数値のリストを生成します。最初の値はwifiネットワークAの強度、2番目はネットワークB、3番目はCです。実際の形式は[A、B、C ]。
- [10、15、0]
- [0、20、25]
def bag_of_words(all_networks, all_strengths, ordering): """Apply bag-of-words encoding to categorical variables. >>> samples = bag_of_words( ... [['a', 'b'], ['b', 'c'], ['a', 'c']], ... [[1, 2], [2, 3], [1, 3]], ... ['a', 'b', 'c']) >>> next(samples) [1, 2, 0] >>> next(samples) [0, 2, 3] """ for networks, strengths in zip(all_networks, all_strengths): yield [strengths[networks.index(network)] if network in networks else 0 for network in ordering]
上記の3つのユーティリティすべてを使用して、サンプルのコレクションとそのラベルを合成します。 get_all_samples
を使用してすべてのサンプルとラベルを収集します。 すべてのサンプルをワンホットエンコードする一貫したフォーマットordering
を定義してから、サンプルにone_hot
エンコードを適用します。 最後に、データとラベルの行列X
とY
をそれぞれ作成します。
def create_dataset(classpaths, ordering=None): """Create dataset from a list of paths to JSON files.""" networks, strengths, labels = zip(*get_all_samples(classpaths)) if ordering is None: ordering = list(sorted(set(flatten(networks)))) X = np.array(list(bag_of_words(networks, strengths, ordering))).astype(np.float64) Y = np.array(list(labels)).astype(np.int) return X, Y, ordering
これらの関数はデータパイプラインを完成させます。 次に、モデルの予測と評価を抽象化します。 予測方法を定義することから始めます。 最初の関数はモデル出力を正規化するため、すべての値の合計は1になり、すべての値は非負になります。 これにより、出力が有効な確率分布であることが保証されます。 2番目はモデルを評価します。
def softmax(x): """Convert one-hotted outputs into probability distribution""" x = np.exp(x) return x / np.sum(x) def predict(X, w): """Predict using model parameters""" return np.argmax(softmax(X.dot(w)), axis=1)
次に、モデルの精度を評価します。 最初の行は、モデルを使用して予測を実行します。 2つ目は、予測値と真の値の両方が一致する回数をカウントし、サンプルの総数で正規化します。
def evaluate(X, Y, w): """Evaluate model w on samples X and labels Y.""" Y_pred = predict(X, w) accuracy = (Y == Y_pred).sum() / X.shape[0] return accuracy
これで、予測および評価ユーティリティは終了です。 これらのユーティリティの後で、データセットを収集し、トレーニングし、評価するmain
関数を定義します。 コマンドラインsys.argv
から引数のリストを読み取ることから始めます。 これらはトレーニングに含める部屋です。 次に、指定したすべての部屋から大きなデータセットを作成します。
def main(): classes = sys.argv[1:] train_paths = sorted(['data/{}_train.json'.format(name) for name in classes]) test_paths = sorted(['data/{}_test.json'.format(name) for name in classes]) X_train, Y_train, ordering = create_dataset(train_paths) X_test, Y_test, _ = create_dataset(test_paths, ordering=ordering)
ラベルにワンホットエンコーディングを適用します。 ワンホットエンコーディングは、上記のbag-of-wordsモデルに似ています。 このエンコーディングを使用して、カテゴリ変数を処理します。 3つの可能なラベルがあるとします。 1、2、または3のラベルを付ける代わりに、データに[1、0、0]、[0、1、0]、または[0、0、1]のラベルを付けます。 このチュートリアルでは、ワンホットエンコーディングが重要である理由については説明しません。 モデルをトレーニングし、トレインセットと検証セットの両方で評価します。
def main(): ... X_test, Y_test, _ = create_dataset(test_paths, ordering=ordering) Y_train_oh = np.eye(len(classes))[Y_train] w, _, _, _ = lstsq(X_train, Y_train_oh) train_accuracy = evaluate(X_train, Y_train, w) test_accuracy = evaluate(X_test, Y_test, w)
両方の精度を印刷し、モデルをディスクに保存します。
def main(): ... print('Train accuracy ({}%), Validation accuracy ({}%)'.format(train_accuracy*100, test_accuracy*100)) np.save('w.npy', w) np.save('ordering.npy', np.array(ordering)) sys.stdout.flush()
ファイルの最後で、 main
関数を実行します。
if __name__ == '__main__': main()
保存して終了。 ファイルが以下と一致することを再確認してください。
import numpy as np from scipy.linalg import lstsq import json import sys def flatten(list_of_lists): """Flatten a list of lists to make a list. >>> flatten([[1], [2], [3, 4]]) [1, 2, 3, 4] """ return sum(list_of_lists, []) def get_all_samples(paths): """Load all samples from JSON files.""" for label, path in enumerate(paths): with open(path) as f: for sample in json.load(f)['samples']: signal_levels = [ network['signal_level'].replace('RSSI', '') or 0 for network in sample] yield [network['mac'] for network in sample], signal_levels, label def bag_of_words(all_networks, all_strengths, ordering): """Apply bag-of-words encoding to categorical variables. >>> samples = bag_of_words( ... [['a', 'b'], ['b', 'c'], ['a', 'c']], ... [[1, 2], [2, 3], [1, 3]], ... ['a', 'b', 'c']) >>> next(samples) [1, 2, 0] >>> next(samples) [0, 2, 3] """ for networks, strengths in zip(all_networks, all_strengths): yield [int(strengths[networks.index(network)]) if network in networks else 0 for network in ordering] def create_dataset(classpaths, ordering=None): """Create dataset from a list of paths to JSON files.""" networks, strengths, labels = zip(*get_all_samples(classpaths)) if ordering is None: ordering = list(sorted(set(flatten(networks)))) X = np.array(list(bag_of_words(networks, strengths, ordering))).astype(np.float64) Y = np.array(list(labels)).astype(np.int) return X, Y, ordering def softmax(x): """Convert one-hotted outputs into probability distribution""" x = np.exp(x) return x / np.sum(x) def predict(X, w): """Predict using model parameters""" return np.argmax(softmax(X.dot(w)), axis=1) def evaluate(X, Y, w): """Evaluate model w on samples X and labels Y.""" Y_pred = predict(X, w) accuracy = (Y == Y_pred).sum() / X.shape[0] return accuracy def main(): classes = sys.argv[1:] train_paths = sorted(['data/{}_train.json'.format(name) for name in classes]) test_paths = sorted(['data/{}_test.json'.format(name) for name in classes]) X_train, Y_train, ordering = create_dataset(train_paths) X_test, Y_test, _ = create_dataset(test_paths, ordering=ordering) Y_train_oh = np.eye(len(classes))[Y_train] w, _, _, _ = lstsq(X_train, Y_train_oh) train_accuracy = evaluate(X_train, Y_train, w) validation_accuracy = evaluate(X_test, Y_test, w) print('Train accuracy ({}%), Validation accuracy ({}%)'.format(train_accuracy*100, validation_accuracy*100)) np.save('w.npy', w) np.save('ordering.npy', np.array(ordering)) sys.stdout.flush() if __name__ == '__main__': main()
保存して終了。 20のサンプルを録音するときに、上記で使用した部屋の名前を思い出してください。 下のbedroom
の代わりにその名前を使用してください。 私たちの例はbedroom
です。 LAPACKバグからの警告を無視するには、 -W ignore
を使用します。
python -W ignore model/train.py bedroom
1つの部屋のトレーニングサンプルのみを収集したため、100%のトレーニングと検証の精度が表示されます。
Train accuracy (100.0%), Validation accuracy (100.0%)
次に、このトレーニングスクリプトをデスクトップアプリにリンクします。
ステップ5:リンクトレインスクリプト
このステップでは、ユーザーがサンプルの新しいバッチを収集するたびに、モデルを自動的に再トレーニングします。 scripts/observe.js
を開きます。
nano scripts/observe.js
fs
インポートの直後に、子プロセスのスポーナーとユーティリティをインポートします。
var fs = require('fs'); // start new code const spawn = require("child_process").spawn; var utils = require('./utils.js');
ui
関数で、次の呼び出しを追加して、完了ハンドラーの最後でretrain
します。
function ui() { ... function completion() { ... retrain((data) => { var status = document.querySelector('#add-status'); accuracies = data.toString().split('\n')[0]; status.innerHTML = "Retraining succeeded: " + accuracies }); } ... }
ui
関数の後に、次のretrain
関数を追加します。 これにより、Pythonスクリプトを実行する子プロセスが生成されます。 完了すると、プロセスは完了ハンドラーを呼び出します。 失敗すると、エラーメッセージがログに記録されます。
function ui() { .. } function retrain(completion) { var filenames = utils.get_filenames() const pythonProcess = spawn('python', ["./model/train.py"].concat(filenames)); pythonProcess.stdout.on('data', completion); pythonProcess.stderr.on('data', (data) => { console.log(" * [ERROR] " + data.toString()) }) }
保存して終了。 scripts/utils.js
を開きます。
nano scripts/utils.js
data/
内のすべてのデータセットをフェッチするための次のユーティリティを追加します。
var fs = require('fs'); module.exports = { get_filenames: get_filenames } function get_filenames() { filenames = new Set([]); fs.readdirSync("data/").forEach(function(filename) { filenames.add(filename.replace('_train', '').replace('_test', '').replace('.json', '' )) }); filenames = Array.from(filenames.values()) filenames.sort(); filenames.splice(filenames.indexOf('.DS_Store'), 1) return filenames }
保存して終了。 この手順を完了するには、物理的に新しい場所に移動します。 理想的には、元の場所と新しい場所の間に壁があるはずです。 障壁が多ければ多いほど、デスクトップアプリはうまく機能します。
もう一度、デスクトップアプリを実行します。
npm start
前と同じように、トレーニングスクリプトを実行します。 「部屋を追加」をクリックします。
最初の部屋とは異なる部屋名を入力します。 living room
を利用します。
「録音開始」をクリックすると、「wifiをリッスンしています…」というステータスが表示されます。
20個のサンプルがすべて記録されると、アプリは次のように一致します。 ステータスは「完了」と表示されます。 モデルの再トレーニング…」
次のステップでは、この再トレーニングされたモデルを使用して、その場であなたがいる部屋を予測します。
ステップ6:Python評価スクリプトを書く
このステップでは、事前にトレーニングされたモデルパラメータをロードし、wifiネットワークをスキャンし、スキャンに基づいて部屋を予測します。
model/eval.py
を開きます。
nano model/eval.py
最後のスクリプトで使用および定義されたライブラリをインポートします。
import numpy as np import sys import json import os import json from train import predict from train import softmax from train import create_dataset from train import evaluate
すべてのデータセットの名前を抽出するユーティリティを定義します。 この関数は、すべてのデータセットが<dataset>_train.json
および<dataset>_test.json
_test.jsonとしてdata/
に保存されていることを前提としています。
from train import evaluate def get_datasets(): """Extract dataset names.""" return sorted(list({path.split('_')[0] for path in os.listdir('./data') if '.DS' not in path}))
main
関数を定義し、トレーニングスクリプトから保存されたパラメーターをロードすることから始めます。
def get_datasets(): ... def main(): w = np.load('w.npy') ordering = np.load('ordering.npy')
データセットを作成して予測します。
def main(): ... classpaths = [sys.argv[1]] X, _, _ = create_dataset(classpaths, ordering) y = np.asscalar(predict(X, w))
上位2つの確率の差に基づいて信頼スコアを計算します。
def main(): ... sorted_y = sorted(softmax(X.dot(w)).flatten()) confidence = 1 if len(sorted_y) > 1: confidence = round(sorted_y[-1] - sorted_y[-2], 2)
最後に、カテゴリを抽出して結果を印刷します。 スクリプトを終了するには、 main
関数を呼び出します。
def main() ... category = get_datasets()[y] print(json.dumps({"category": category, "confidence": confidence})) if __name__ == '__main__': main()
保存して終了。 コードが次のコードと一致することを再確認してください(ソースコード)。
import numpy as np import sys import json import os import json from train import predict from train import softmax from train import create_dataset from train import evaluate def get_datasets(): """Extract dataset names.""" return sorted(list({path.split('_')[0] for path in os.listdir('./data') if '.DS' not in path})) def main(): w = np.load('w.npy') ordering = np.load('ordering.npy') classpaths = [sys.argv[1]] X, _, _ = create_dataset(classpaths, ordering) y = np.asscalar(predict(X, w)) sorted_y = sorted(softmax(X.dot(w)).flatten()) confidence = 1 if len(sorted_y) > 1: confidence = round(sorted_y[-1] - sorted_y[-2], 2) category = get_datasets()[y] print(json.dumps({"category": category, "confidence": confidence})) if __name__ == '__main__': main()
次に、この評価スクリプトをデスクトップアプリに接続します。 デスクトップアプリは継続的にwifiスキャンを実行し、予測された部屋でUIを更新します。
ステップ7:評価をデスクトップアプリに接続する
このステップでは、UIを「信頼」表示で更新します。 次に、関連付けられたNodeJSスクリプトがスキャンと予測を継続的に実行し、それに応じてUIを更新します。
static/index.html
を開きます。
nano static/index.html
タイトルの直後とボタンの前に自信を持って線を追加します。
<h1 class="title">(I dunno)</h1> <!-- start new code --> <p class="subtitle">with <span>0%</span> confidence</p> <!-- end new code --> <div class="buttons">
main
の直後、 body
の終わりの前に、新しいスクリプトpredict.js
を追加します。
</main> <!-- start new code --> <script> require('../scripts/predict.js') </script> <!-- end new code --> </body>
保存して終了。 scripts/predict.js
を開きます。
nano scripts/predict.js
ファイルシステム、ユーティリティ、および子プロセススポーナーに必要なNodeJSユーティリティをインポートします。
var fs = require('fs'); var utils = require('./utils'); const spawn = require("child_process").spawn;
Wi-Fiネットワークを検出するための個別のノードプロセスと、部屋を予測するための個別のPythonプロセスを呼び出すpredict
関数を定義します。
function predict(completion) { const nodeProcess = spawn('node', ["scripts/observe.js"]); const pythonProcess = spawn('python', ["-W", "ignore", "./model/eval.py", "samples.json"]); }
両方のプロセスが生成されたら、成功とエラーの両方についてPythonプロセスにコールバックを追加します。 成功コールバックは情報をログに記録し、完了コールバックを呼び出し、予測と信頼性でUIを更新します。 エラーコールバックはエラーをログに記録します。
function predict(completion) { ... pythonProcess.stdout.on('data', (data) => { information = JSON.parse(data.toString()); console.log(" * [INFO] Room '" + information.category + "' with confidence '" + information.confidence + "'") completion() if (typeof document != "undefined") { document.querySelector('#predicted-room-name').innerHTML = information.category document.querySelector('#predicted-confidence').innerHTML = information.confidence } }); pythonProcess.stderr.on('data', (data) => { console.log(data.toString()); }) }
predict
関数を永久に再帰的に呼び出すメイン関数を定義します。
function main() { f = function() { predict(f) } predict(f) } main();
最後にもう一度、デスクトップアプリを開いて、ライブ予測を確認します。
npm start
約1秒ごとにスキャンが完了し、最新の信頼性と予測された部屋でインターフェースが更新されます。 おめでとう; すべての範囲内WiFiネットワークに基づくシンプルな部屋検出器が完成しました。
結論
このチュートリアルでは、デスクトップのみを使用して建物内の場所を検出するソリューションを作成しました。 Electron JSを使用してシンプルなデスクトップアプリを構築し、すべての範囲内WiFiネットワークにシンプルな機械学習手法を適用しました。 これにより、維持に費用がかかる(お金ではなく時間と開発の面でコストがかかる)デバイスのアレイを必要とせずに、モノのインターネットアプリケーションへの道が開かれます。
注:ソースコード全体はGithubで確認できます。
時間の経過とともに、この最小二乗法は実際には見事に機能しないことに気付くかもしれません。 1つの部屋の中で2つの場所を探すか、出入り口に立ってみてください。 最小二乗法は大きくなり、エッジケースを区別できなくなります。 もっと上手くできますか? できることがわかりました。今後のレッスンでは、他の手法や機械学習の基礎を活用してパフォーマンスを向上させます。 このチュートリアルは、今後の実験の簡単なテストベッドとして機能します。