Construindo um detector de ambiente para dispositivos IoT no Mac OS

Publicados: 2022-03-10
Resumo rápido ↬ Neste tutorial, você cria um aplicativo de desktop que prevê em qual sala você está usando um algoritmo simples de aprendizado de máquina: mínimos quadrados. O código se aplica a qualquer plataforma, mas fornecemos apenas instruções de instalação de dependência para Mac OSX.

Saber em qual sala você está permite vários aplicativos de IoT — desde acender a luz até mudar os canais de TV. Então, como podemos detectar o momento em que você e seu telefone estão na cozinha, no quarto ou na sala de estar? Com o hardware de commodities de hoje, há uma infinidade de possibilidades:

Uma solução é equipar cada quarto com um dispositivo bluetooth . Quando seu telefone estiver dentro do alcance de um dispositivo bluetooth, seu telefone saberá qual é a sala, com base no dispositivo bluetooth. No entanto, manter uma variedade de dispositivos Bluetooth é uma sobrecarga significativa – desde a substituição de baterias até a substituição de dispositivos disfuncionais. Além disso, a proximidade com o dispositivo Bluetooth nem sempre é a resposta: se você estiver na sala, perto da parede compartilhada com a cozinha, seus utensílios de cozinha não devem começar a despejar comida.

Outra solução, embora impraticável, é usar o GPS . No entanto, tenha em mente que o GPS funciona mal em ambientes fechados em que a multidão de paredes, outros sinais e outros obstáculos causam estragos na precisão do GPS.

Nossa abordagem é aproveitar todas as redes WiFi dentro do alcance - mesmo aquelas às quais seu telefone não está conectado. Veja como: considere a força do WiFi A na cozinha; digamos que seja 5. Como há uma parede entre a cozinha e o quarto, podemos razoavelmente esperar que a força do WiFi A no quarto seja diferente; digamos que é 2. Podemos explorar essa diferença para prever em qual sala estamos. Além do mais: a rede WiFi B do nosso vizinho só pode ser detectada da sala de estar, mas é efetivamente invisível da cozinha. Isso torna a previsão ainda mais fácil. Em suma, a lista de todos os WiFi dentro do alcance nos fornece informações abundantes.

Este método tem as vantagens distintas de:

  1. não requerendo mais hardware;
  2. contando com sinais mais estáveis ​​como WiFi;
  3. funcionando bem onde outras técnicas como GPS são fracas.

Quanto mais paredes, melhor, pois quanto mais díspares forem as forças da rede WiFi, mais fácil será a classificação das salas. Você criará um aplicativo de desktop simples que coleta dados, aprende com os dados e prevê em qual sala você está a qualquer momento.

Leitura adicional no SmashingMag:

  • A ascensão da interface do usuário de conversação inteligente
  • Aplicações de aprendizado de máquina para designers
  • Como prototipar experiências de IoT: construindo o hardware
  • Projetando para a Internet das Coisas Emocionais

Pré-requisitos

Para este tutorial, você precisará de um Mac OSX. Considerando que o código pode ser aplicado a qualquer plataforma, forneceremos apenas instruções de instalação de dependência para Mac.

  • Mac OS X
  • Homebrew, um gerenciador de pacotes para Mac OSX. Para instalar, copie e cole o comando em brew.sh
  • Instalação do NodeJS 10.8.0+ e npm
  • Instalação do Python 3.6+ e pip. Veja as primeiras 3 seções de “Como instalar o virtualenv, instalando com pip e gerenciando pacotes”
Mais depois do salto! Continue lendo abaixo ↓

Etapa 0: configurar o ambiente de trabalho

Seu aplicativo de desktop será escrito em NodeJS. No entanto, para alavancar bibliotecas computacionais mais eficientes como numpy , o código de treinamento e previsão será escrito em Python. Para começar, vamos configurar seus ambientes e instalar dependências. Crie um novo diretório para hospedar seu projeto.

 mkdir ~/riot

Navegue no diretório.

 cd ~/riot

Use pip para instalar o gerenciador de ambiente virtual padrão do Python.

 sudo pip install virtualenv

Crie um ambiente virtual Python3.6 chamado riot .

 virtualenv riot --python=python3.6

Ative o ambiente virtual.

 source riot/bin/activate

Seu prompt agora é precedido por (riot) . Isso indica que entramos com sucesso no ambiente virtual. Instale os seguintes pacotes usando pip :

  • numpy : Uma biblioteca de álgebra linear eficiente
  • scipy : Uma biblioteca de computação científica que implementa modelos populares de aprendizado de máquina
 pip install numpy==1.14.3 scipy ==1.1.0

Com a configuração do diretório de trabalho, começaremos com um aplicativo de desktop que registra todas as redes WiFi dentro do alcance. Essas gravações constituirão dados de treinamento para seu modelo de aprendizado de máquina. Assim que tivermos os dados em mãos, você escreverá um classificador de mínimos quadrados, treinado nos sinais WiFi coletados anteriormente. Por fim, usaremos o modelo de mínimos quadrados para prever a sala em que você está, com base nas redes WiFi ao alcance.

Etapa 1: aplicativo de desktop inicial

Nesta etapa, criaremos um novo aplicativo de desktop usando o Electron JS. Para começar, usaremos o gerenciador de pacotes Node npm e um utilitário de download wget .

 brew install npm wget

Para começar, vamos criar um novo projeto Node.

 npm init

Isso solicita o nome do pacote e, em seguida, o número da versão. Pressione ENTER para aceitar o nome padrão de riot e a versão padrão de 1.0.0 .

 package name: (riot) version: (1.0.0)

Isso solicita uma descrição do projeto. Adicione qualquer descrição não vazia que desejar. Abaixo, a descrição é room detector

 description: room detector

Isso solicita o ponto de entrada ou o arquivo principal do qual executar o projeto. Digite app.js .

 entry point: (index.js) app.js

Isso solicita o test command e o git repository . Pressione ENTER para pular esses campos por enquanto.

 test command: git repository:

Isso solicita palavras- keywords e author . Preencha os valores que desejar. Abaixo, usamos iot , wifi para palavras-chave e usamos John Doe para o autor.

 keywords: iot,wifi author: John Doe

Isso solicita a licença. Pressione ENTER para aceitar o valor padrão de ISC .

 license: (ISC)

Neste ponto, o npm solicitará um resumo das informações até o momento. Sua saída deve ser semelhante à seguinte.

 { "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" }

Pressione ENTER para aceitar. npm então produz um package.json . Liste todos os arquivos para verificar novamente.

 ls

Isso produzirá o único arquivo neste diretório, juntamente com a pasta do ambiente virtual.

 package.json riot

Instale as dependências do NodeJS para nosso projeto.

 npm install electron --global # makes electron binary accessible globally npm install node-wifi --save

Comece com main.js do Electron Quick Start, baixando o arquivo, usando o abaixo. O argumento -O a seguir renomeia main.js para app.js .

 wget https://raw.githubusercontent.com/electron/electron-quick-start/master/main.js -O app.js

Abra app.js no nano ou no seu editor de texto favorito.

 nano app.js

Na linha 12, altere index.html para static/index.html , pois criaremos um diretório static para conter todos os templates HTML.

 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.

Salve suas alterações e saia do editor. Seu arquivo deve corresponder ao código-fonte do arquivo app.js Agora crie um novo diretório para hospedar nossos templates HTML.

 mkdir static

Baixe uma folha de estilo criada para este projeto.

 wget https://raw.githubusercontent.com/alvinwan/riot/master/static/style.css?token=AB-ObfDtD46ANlqrObDanckTQJ2Q1Pyuks5bf79PwA%3D%3D -O static/style.css

Abra static/index.html no nano ou no seu editor de texto favorito. Comece com a estrutura HTML padrão.

 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Riot | Room Detector</title> </head> <body> <main> </main> </body> </html>

Logo após o título, vincule a fonte Montserrat vinculada pelo Google Fonts e folha de estilo.

 <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>

Entre as tags main , adicione um slot para o nome da sala prevista.

 <main> <!-- start new code --> <p class="text">I believe you're in the</p> <h1 class="title">(I dunno)</h1> <!-- end new code --> </main>

Seu script agora deve corresponder exatamente ao seguinte. Saia do editor.

 <!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>

Agora, altere o arquivo do pacote para conter um comando de início.

 nano package.json

Logo após a linha 7, adicione um comando start com o alias de electron . . Certifique-se de adicionar uma vírgula ao final da linha anterior.

 "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "electron ." },

Salvar e sair. Agora você está pronto para iniciar seu aplicativo de desktop no Electron JS. Use npm para iniciar seu aplicativo.

 npm start

Seu aplicativo de desktop deve corresponder ao seguinte.

página inicial com botão
Página inicial com o botão "Adicionar nova sala" disponível (visualização grande)

Isso conclui seu aplicativo de desktop inicial. Para sair, navegue de volta ao seu terminal e CTRL+C. Na próxima etapa, gravaremos redes wifi e tornaremos o utilitário de gravação acessível por meio da interface do usuário do aplicativo de desktop.

Etapa 2: gravar redes WiFi

Nesta etapa, você escreverá um script NodeJS que registra a força e a frequência de todas as redes wifi dentro do alcance. Crie um diretório para seus scripts.

 mkdir scripts

Abra scripts/observe.js no nano ou no seu editor de texto favorito.

 nano scripts/observe.js

Importe um utilitário wifi NodeJS e o objeto do sistema de arquivos.

 var wifi = require('node-wifi'); var fs = require('fs');

Defina uma função de record que aceite um manipulador de conclusão.

 /** * Uses a recursive function for repeated scans, since scans are asynchronous. */ function record(n, completion, hook) { }

Dentro da nova função, inicialize o utilitário wifi. Defina iface como null para inicializar em uma interface wifi aleatória, pois esse valor é atualmente irrelevante.

 function record(n, completion, hook) { wifi.init({ iface : null }); }

Defina uma matriz para conter suas amostras. Amostras são dados de treinamento que usaremos para nosso modelo. As amostras neste tutorial em particular são listas de redes wifi dentro do alcance e suas forças, frequências, nomes etc.

 function record(n, completion, hook) { ... samples = [] }

Defina uma função recursiva startScan , que iniciará as varreduras de wifi de forma assíncrona. Após a conclusão, a varredura assíncrona de wifi invocará recursivamente startScan .

 function record(n, completion, hook) { ... function startScan(i) { wifi.scan(function(err, networks) { }); } startScan(n); }

No retorno de chamada wifi.scan , verifique se há erros ou listas vazias de redes e reinicie a verificação, se for o caso.

 wifi.scan(function(err, networks) { if (err || networks.length == 0) { startScan(i); return } });

Adicione o caso base da função recursiva, que invoca o manipulador de conclusão.

 wifi.scan(function(err, networks) { ... if (i <= 0) { return completion({samples: samples}); } });

Emita uma atualização de progresso, anexe à lista de amostras e faça a chamada recursiva.

 wifi.scan(function(err, networks) { ... hook(n-i+1, networks); samples.push(networks); startScan(i-1); });

No final do arquivo, invoque a função de record com um retorno de chamada que salva amostras em um arquivo no disco.

 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();

Verifique se seu arquivo corresponde ao seguinte:

 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();

Salvar e sair. Execute o script.

 node scripts/observe.js

Sua saída corresponderá ao seguinte, com números variáveis ​​de redes.

 * [INFO] Collected sample 1 with 39 networks

Examine as amostras que acabaram de ser coletadas. Pipe to json_pp para imprimir o JSON e pipe to head para ver as primeiras 16 linhas.

 cat samples.json | json_pp | head -16

O abaixo é um exemplo de saída para uma rede de 2,4 GHz.

 { "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)" ] },

Isso conclui seu script de verificação de wifi NodeJS. Isso nos permite visualizar todas as redes WiFi dentro do alcance. Na próxima etapa, você tornará esse script acessível no aplicativo de desktop.

Etapa 3: conectar o script de digitalização ao aplicativo da área de trabalho

Nesta etapa, você primeiro adicionará um botão ao aplicativo de desktop para acionar o script. Em seguida, você atualizará a interface do usuário do aplicativo de desktop com o progresso do script.

Abra static/index.html .

 nano static/index.html

Insira o botão “Adicionar”, conforme mostrado abaixo.

 <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>

Salvar e sair. Abra static/add.html .

 nano static/add.html

Cole o seguinte conteúdo.

 <!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>

Salvar e sair. Reabra scripts/observe.js .

 nano scripts/observe.js

Abaixo da função cli , defina uma nova função ui .

 function cli() { ... } // start new code function ui() { } // end new code cli();

Atualize o status do aplicativo de desktop para indicar que a função começou a ser executada.

 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..." }

Particione os dados em conjuntos de dados de treinamento e validação.

 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); } }

Ainda dentro do retorno de chamada de completion , grave os dois conjuntos de dados no disco.

 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." } }

Invoque record com os retornos de chamada apropriados para gravar 20 amostras e salve as amostras em disco.

 function ui() { ... function completion(data) { ... } record(20, completion, function(i, networks) { number.innerHTML = i console.log(" * [INFO] Collected sample " + i + " with " + networks.length + " networks") }) }

Por fim, invoque as funções cli e ui quando apropriado. Comece excluindo o cli(); chamada na parte inferior do arquivo.

 function ui() { ... } cli(); // remove me

Verifique se o objeto de documento é globalmente acessível. Caso contrário, o script está sendo executado a partir da linha de comando. Nesse caso, invoque a função cli . Se for, o script é carregado de dentro do aplicativo de desktop. Nesse caso, vincule o ouvinte de clique à função ui .

 if (typeof document == 'undefined') { cli(); } else { document.querySelector('#start-recording').addEventListener('click', ui) }

Salvar e sair. Crie um diretório para armazenar nossos dados.

 mkdir data

Inicie o aplicativo de desktop.

 npm start

Você verá a seguinte página inicial. Clique em “Adicionar sala”.

(Visualização grande)

Você verá o seguinte formulário. Digite um nome para a sala. Lembre-se desse nome, pois usaremos isso mais tarde. Nosso exemplo será bedroom .

Página Adicionar nova sala
Página "Adicionar nova sala" ao carregar (visualização grande)

Clique em “Iniciar gravação” e você verá o seguinte status “Ouvindo wifi…”.

iniciando a gravação
"Adicionar nova sala" iniciando a gravação (visualização grande)

Depois que todas as 20 amostras forem gravadas, seu aplicativo corresponderá ao seguinte. O status será "Concluído".

Página "Adicionar nova sala" após a conclusão da gravação (visualização grande)

Clique no nome errado "Cancelar" para retornar à página inicial, que corresponde ao seguinte.

gravação finalizada
Página "Adicionar nova sala" após a conclusão da gravação (visualização grande)

Agora podemos escanear redes wifi a partir da interface do usuário da área de trabalho, que salvará todas as amostras gravadas em arquivos no disco. Em seguida, treinaremos um algoritmo de aprendizado de máquina pronto para uso - mínimos quadrados nos dados que você coletou.

Etapa 4: escrever o script de treinamento do Python

Nesta etapa, escreveremos um script de treinamento em Python. Crie um diretório para seus utilitários de treinamento.

 mkdir model

Abra model/train.py

 nano model/train.py

No topo do seu arquivo, importe a biblioteca computacional numpy e scipy para seu modelo de mínimos quadrados.

 import numpy as np from scipy.linalg import lstsq import json import sys

Os próximos três utilitários tratarão do carregamento e da configuração dos dados dos arquivos no disco. Comece adicionando uma função de utilitário que nivela listas aninhadas. Você usará isso para achatar uma lista de listas de amostras.

 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, [])

Adicione um segundo utilitário que carrega amostras dos arquivos especificados. Esse método abstrai o fato de que as amostras estão espalhadas por vários arquivos, retornando apenas um único gerador para todas as amostras. Para cada uma das amostras, o rótulo é o índice do arquivo. Por exemplo, se você chamar get_all_samples('a.json', 'b.json') , ​​todas as amostras em a.json terão rótulo 0 e todas as amostras em b.json terão rótulo 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

Em seguida, adicione um utilitário que codifique as amostras usando um modelo de saco de palavras. Aqui está um exemplo: Suponha que coletamos duas amostras.

  1. rede wifi A na força 10 e rede wifi B na força 15
  2. rede wifi B na força 20 e rede wifi C na força 25.

Esta função produzirá uma lista de três números para cada uma das amostras: o primeiro valor é a força da rede wifi A, o segundo para a rede B e o terceiro para C. Na verdade, o formato é [A, B, C ].

  1. [10, 15, 0]
  2. [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]

Usando todos os três utilitários acima, sintetizamos uma coleção de amostras e seus rótulos. Reúna todas as amostras e rótulos usando get_all_samples . Defina uma ordering de formato consistente para codificar one-hot todas as amostras e, em seguida, aplique a codificação one_hot às amostras. Finalmente, construa os dados e rotule as matrizes X e Y , respectivamente.

 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

Essas funções completam o pipeline de dados. Em seguida, abstraímos a previsão e avaliação do modelo. Comece definindo o método de previsão. A primeira função normaliza as saídas do nosso modelo, de modo que a soma de todos os valores totalize 1 e que todos os valores sejam não negativos; isso garante que a saída seja uma distribuição de probabilidade válida. A segunda avalia o modelo.

 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)

Em seguida, avalie a precisão do modelo. A primeira linha executa a previsão usando o modelo. O segundo conta o número de vezes que os valores previstos e verdadeiros concordam e, em seguida, normaliza pelo número total de amostras.

 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

Isso conclui nossas utilidades de previsão e avaliação. Após esses utilitários, defina uma função main que coletará o conjunto de dados, treinará e avaliará. Comece lendo a lista de argumentos da linha de comando sys.argv ; estas são as salas a incluir na formação. Em seguida, crie um grande conjunto de dados de todas as salas especificadas.

 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)

Aplique a codificação one-hot aos rótulos. Uma codificação one-hot é semelhante ao modelo de saco de palavras acima; usamos essa codificação para lidar com variáveis ​​categóricas. Digamos que temos 3 rótulos possíveis. Em vez de rotular 1, 2 ou 3, rotulamos os dados com [1, 0, 0], [0, 1, 0] ou [0, 0, 1]. Para este tutorial, pouparemos a explicação de por que a codificação one-hot é importante. Treine o modelo e avalie nos conjuntos de treinamento e validação.

 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)

Imprima ambas as precisões e salve o modelo em disco.

 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()

No final do arquivo, execute a função main .

 if __name__ == '__main__': main()

Salvar e sair. Verifique se seu arquivo corresponde ao seguinte:

 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()

Salvar e sair. Lembre-se do nome da sala usado acima ao gravar as 20 amostras. Use esse nome em vez do bedroom abaixo. Nosso exemplo é bedroom . Usamos -W ignore para ignorar avisos de um bug LAPACK.

 python -W ignore model/train.py bedroom

Como coletamos apenas amostras de treinamento para uma sala, você deve ver 100% de precisão de treinamento e validação.

 Train accuracy (100.0%), Validation accuracy (100.0%)

Em seguida, vincularemos esse script de treinamento ao aplicativo de desktop.

Etapa 5: script de treinamento de links

Nesta etapa, retreinaremos automaticamente o modelo sempre que o usuário coletar um novo lote de amostras. Abra scripts/observe.js .

 nano scripts/observe.js

Logo após a importação do fs , importe o gerador e os utilitários do processo filho.

 var fs = require('fs'); // start new code const spawn = require("child_process").spawn; var utils = require('./utils.js');

Na função ui , adicione a seguinte chamada para retrain no final do manipulador de conclusão.

 function ui() { ... function completion() { ... retrain((data) => { var status = document.querySelector('#add-status'); accuracies = data.toString().split('\n')[0]; status.innerHTML = "Retraining succeeded: " + accuracies }); } ... }

Após a função ui , adicione a seguinte função de retrain . Isso gera um processo filho que executará o script python. Após a conclusão, o processo chama um manipulador de conclusão. Em caso de falha, ele registrará a mensagem de erro.

 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()) }) }

Salvar e sair. Abra scripts/utils.js .

 nano scripts/utils.js

Adicione o seguinte utilitário para buscar todos os conjuntos de dados em 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 }

Salvar e sair. Para a conclusão desta etapa, mova-se fisicamente para um novo local. Idealmente, deve haver uma parede entre sua localização original e sua nova localização. Quanto mais barreiras, melhor o seu aplicativo de desktop funcionará.

Mais uma vez, execute seu aplicativo de desktop.

 npm start

Assim como antes, execute o script de treinamento. Clique em “Adicionar sala”.

página inicial com botão
Página inicial com o botão "Adicionar nova sala" disponível (visualização grande)

Digite um nome de quarto diferente do seu primeiro quarto. Usaremos a living room .

Página Adicionar nova sala
Página "Adicionar nova sala" ao carregar (visualização grande)

Clique em “Iniciar gravação” e você verá o seguinte status “Ouvindo wifi…”.

"Adicionar nova sala" iniciando a gravação para a segunda sala (visualização grande)

Depois que todas as 20 amostras forem gravadas, seu aplicativo corresponderá ao seguinte. O status será “Concluído. Modelo de reciclagem…”

terminou a gravação 2
Página "Adicionar nova sala" após a conclusão da gravação da segunda sala (visualização grande)

Na próxima etapa, usaremos esse modelo retreinado para prever em tempo real a sala em que você está.

Etapa 6: escrever o script de avaliação do Python

Nesta etapa, carregaremos os parâmetros do modelo pré-treinados, verificaremos as redes wifi e preveremos a sala com base na verificação.

Abra model/eval.py .

 nano model/eval.py

Importar bibliotecas usadas e definidas em nosso último script.

 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

Defina um utilitário para extrair os nomes de todos os conjuntos de dados. Esta função assume que todos os conjuntos de dados são armazenados em data/ como <dataset>_train.json e <dataset>_test.json .

 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}))

Defina a função main e comece carregando os parâmetros salvos do script de treinamento.

 def get_datasets(): ... def main(): w = np.load('w.npy') ordering = np.load('ordering.npy')

Crie o conjunto de dados e preveja.

 def main(): ... classpaths = [sys.argv[1]] X, _, _ = create_dataset(classpaths, ordering) y = np.asscalar(predict(X, w))

Calcule uma pontuação de confiança com base na diferença entre as duas principais probabilidades.

 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)

Por fim, extraia a categoria e imprima o resultado. Para concluir o script, invoque a função main .

 def main() ... category = get_datasets()[y] print(json.dumps({"category": category, "confidence": confidence})) if __name__ == '__main__': main()

Salvar e sair. Verifique se seu código corresponde ao seguinte (código-fonte):

 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()

Em seguida, conectaremos esse script de avaliação ao aplicativo de desktop. O aplicativo de desktop executará continuamente verificações de Wi-Fi e atualizará a interface do usuário com a sala prevista.

Etapa 7: conectar a avaliação ao aplicativo para desktop

Nesta etapa, atualizaremos a interface do usuário com uma exibição de “confiança”. Em seguida, o script NodeJS associado executará continuamente verificações e previsões, atualizando a interface do usuário de acordo.

Abra static/index.html .

 nano static/index.html

Adicione uma linha de confiança logo após o título e antes dos botões.

 <h1 class="title">(I dunno)</h1> <!-- start new code --> <p class="subtitle">with <span>0%</span> confidence</p> <!-- end new code --> <div class="buttons">

Logo após main , mas antes do final do body , adicione um novo script predict.js .

 </main> <!-- start new code --> <script> require('../scripts/predict.js') </script> <!-- end new code --> </body>

Salvar e sair. Abra scripts/predict.js .

 nano scripts/predict.js

Importe os utilitários NodeJS necessários para o sistema de arquivos, utilitários e gerador de processo filho.

 var fs = require('fs'); var utils = require('./utils'); const spawn = require("child_process").spawn;

Defina uma função de predict que invoca um processo de nó separado para detectar redes Wi-Fi e um processo Python separado para prever a sala.

 function predict(completion) { const nodeProcess = spawn('node', ["scripts/observe.js"]); const pythonProcess = spawn('python', ["-W", "ignore", "./model/eval.py", "samples.json"]); }

Após a geração de ambos os processos, adicione retornos de chamada ao processo Python para sucessos e erros. O retorno de chamada de sucesso registra informações, invoca o retorno de chamada de conclusão e atualiza a interface do usuário com a previsão e a confiança. O retorno de chamada de erro registra o erro.

 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()); }) }

Defina uma função principal para invocar a função de predict recursivamente, para sempre.

 function main() { f = function() { predict(f) } predict(f) } main();

Uma última vez, abra o aplicativo de desktop para ver a previsão ao vivo.

 npm start

Aproximadamente a cada segundo, uma varredura será concluída e a interface será atualizada com a mais recente confiança e espaço previsto. Parabéns; você concluiu um detector de ambiente simples baseado em todas as redes WiFi dentro do alcance.

demonstração
Gravando 20 samples dentro da sala e outros 20 no corredor. Ao voltar para dentro, o script prevê corretamente “corredor” e depois “quarto”. (Visualização grande)

Conclusão

Neste tutorial, criamos uma solução usando apenas sua área de trabalho para detectar sua localização em um prédio. Construímos um aplicativo de desktop simples usando o Electron JS e aplicamos um método simples de aprendizado de máquina em todas as redes WiFi dentro do alcance. Isso abre caminho para aplicativos de Internet das coisas sem a necessidade de conjuntos de dispositivos cuja manutenção é cara (custo não em termos de dinheiro, mas em termos de tempo e desenvolvimento).

Nota : Você pode ver o código-fonte na íntegra no Github.

Com o tempo, você pode descobrir que esses mínimos quadrados não funcionam espetacularmente de fato. Tente encontrar dois locais dentro de uma única sala ou fique nas portas. Os mínimos quadrados serão grandes incapazes de distinguir entre casos de borda. Podemos fazer melhor? Acontece que podemos, e em lições futuras, vamos alavancar outras técnicas e os fundamentos do aprendizado de máquina para um melhor desempenho. Este tutorial serve como um teste rápido para experimentos futuros.