Kiuchi Tomoyuki | MISO https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com 未来を創造するITのミソ Tue, 15 Jul 2025 05:18:58 +0000 ja hourly 1 https://wordpress.org/?v=6.7.2 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/wp-content/uploads/2017/09/tdi_300-300-300x280.png Kiuchi Tomoyuki | MISO https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com 32 32 ローカルSLM/閉じた環境で動作する生成AIを検証してみた (②性能指標とログの取り方編) https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/local-slm-2 Tue, 15 Jul 2025 05:18:58 +0000 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/?p=20600 はじめに ご無沙汰しております。生成AIを「社内や現場のローカルで使いたい」そんな思いからスタートした、このローカル生成AI・SLM検証シリーズ。 第一回では、OllamaとOpen WebUIを使って、閉じた環境で動作…

The post ローカルSLM/閉じた環境で動作する生成AIを検証してみた (②性能指標とログの取り方編) first appeared on MISO.]]>
はじめに

ご無沙汰しております。生成AIを「社内や現場のローカルで使いたい」そんな思いからスタートした、このローカル生成AI・SLM検証シリーズ。
第一回では、OllamaとOpen WebUIを使って、閉じた環境で動作する生成AI(ローカルSLM)を構築し、その手順や構成イメージを紹介しました。
「クラウドに出せない」「ネット接続できない」――そんな制約下でも生成AIを活用できることに、興味を持っていただいた方も多かったようです。もしまだ第一回の記事をご覧になっていない方は、こちらのローカルSLM/閉じた環境で動作する生成AIを検証してみた (①環境構築編)も合わせてご覧ください。

そして今回は、その次の ”現実的な課題” に踏み込みます。「実際に動かしたこのローカルSLM、果たしてどこまで“実用レベル”なのか?」それを、数値で見える化する方法と、実際に取れたデータを交えてご紹介します。

もちろん、前回の記事を読んでいなくても大丈夫です。
「オフライン環境でも、ここまでできるのか!」と感じていただける内容になっていますので、ぜひ最後までお付き合いください。
 
【記事の要約】
読了までの時間 : 12~15分
得られるメリット : ローカルSLMにおける性能指標の可視化やログの取得方法がわかる

 

SLMの“実力”をどう測るか?

生成AIといえば、「文章の自然さ」や「賢さ」に目がいきがちですが、ローカルでの業務活用を考えた場合、それだけでは不十分です。
むしろ、重要になるのは性能指標(ベンチマーク)です。

たとえば──

  • 処理が遅すぎてストレスになる
  • GPUのメモリを食いすぎて、他のアプリが動かなくなる
  • 非GPU環境では使い物にならない

こういった“現場あるある”を避けるためにも、定量的な指標でモデルを評価することが欠かせません。

実務で重視したい 5つの指標

私が今回意識したのは、次の5つです。

指標 意味と実務での重要性
tokens/sec
どれだけ速く文章を生成できるか
(1秒あたりに生成されるトークン数、スループット設計の基礎)
初期応答時間 ユーザーが待たされる体感時間
VRAM使用量 実行できるPCやサーバーの条件把握に直結
CPU使用率 非GPU環境やエッジデバイスの負荷確認
出力自然性 実用性や読みやすさの評価基準

 
これらは、2024年に公開された「Small Language Models: Survey, Measurements, and Insights」という論文でも、SLMの評価軸としてしっかり整理されています
なお、今回はその中でも、比較的取得が容易だった「tokens/sec(生成速度)」を中心に測定を行いました。他の指標については、今後、取得方法の工夫を進め、順次可視化していく予定なので、ご理解いただければ幸いです。

 

前提条件

今回の検証は、以下のPC環境で実施しています。前回の記事と同じ構成です。

OS Windows 11 Home (24H2)
CPU AMD Ryzen AI9 HX370
メモリ 32GB
ディスク 1TB
GPU/VRAM NVIDIA GeForce RTX 4060 / 8GB

 
なお、今回の検証内容や結果は、この構成に基づくものです。環境が異なる場合は、性能傾向が変わる可能性がある点にご注意ください。

 

ローカル環境での測定の壁と工夫

「じゃあ、その数値、どうやって取るの?」これが意外とハマりどころでした。
第一回で構築したOllamaとOpen WebUIの構成だと、
  • tokens/sec(1秒あたりに生成されるトークン数)
  • 初期応答時間

といった細かなデータは、素直に取れません。
また、WebUI側のOpen WebUIを直接いじって表示させることも考えましたが、アップデート対応が大変なので現実的ではないと判断しました。(Open WebUIのリリース状況を見てもわかるように、ほぼ毎週、時には週に数回アップデートがあります。そのたびに中身を直接修正するのは、現実的ではありませんよね…)

リレー処理を導入して“外から覗く”

そこで考えたのが、OllamaとOpen WebUIの間に、リレー処理をかます方法です。
具体的には、以下のイメージです。
[Open WebUI] ⇄ [リレー処理(自作プロキシ)] ⇄ [Ollama]

このプロキシで、リレー処理の中でリクエストとレスポンスを覗き見ることで、

  • プロンプト送信時刻
  • 最初の応答トークンの戻り時刻
  • トータルの生成時間
  • tokens/secの算出結果(1秒あたりに生成されるトークン数)

といったログ情報を出力する仕組みを考えてみました。

実際のコード

リレー処理で使用する「自作プロキシ」処理ですが、今回は Python を使ったシンプルなFlaskアプリケーションで実装してみました。
以下が「自作プロキシ」のコード( proxy_server.py)になります。
from flask import Flask, request, Response
import requests, os, json, time

app = Flask(__name__)
OLLAMA_API = 'http://ollama:11434'
LOG_PATH = '/logs/bench.log'

@app.route('/api/generate', methods=['POST'])
def proxy_generate():
    start_time = time.time()
    resp = requests.post(f"{OLLAMA_API}/api/generate", json=request.get_json())
    duration = time.time() - start_time
    data = resp.json()

    eval_count = data.get("eval_count")
    eval_duration_ns = data.get("eval_duration")

    if eval_count and eval_duration_ns:
        token_sec = eval_count / (eval_duration_ns / 1_000_000_000)
        log_entry = {
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
            "token_per_sec": token_sec
        }
        os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
        with open(LOG_PATH, "a") as f:
            f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")

    return Response(resp.content, status=resp.status_code, content_type=resp.headers.get('Content-Type'))

ポイントは、Ollamaへのリクエストを中継しつつ、必要な情報だけログに記録する部分です。
また、このプロキシ処理内で使用しているライブラリを取得する requirements.txt も用意する必要があるのでお忘れなく!( requirements.txt は上記コードと同じ場所に配置してください)
flask
requests

実行環境のフォルダ構成

「記事は見たけど、自分の環境で動かせないと意味がない」そんな方のために、今回私が検証したフォルダ構成の全体イメージを載せておきます。(フォルダ構成内のコメント先頭に (*) があるフォルダは、新規で作成する必要があります)

./ollama
  ├ compose.yaml
  ├ ollama                  # ollama用のdockerマウントフォルダ
  │    └ :
  ├ open-webui              # open webui用のdockerマウントフォルダ
  │    └ :
  ├ proxy_server            # (*) 自作プロキシ用のフォルダ
  │    ├ proxy_server.py
  │    └ requirements.txt
  └ logs                    # (*) 自作プロキシが出力するログ用フォルダ
        └ bench.log         # ログファイル

この構成であれば、必要なファイルを同じ階層にまとめておけるので、環境再現や構成管理が非常に楽になります。ぜひ参考にしてみてください。

Dockerイメージ化と実行手順

次に、上記で作成した「自作プロキシ」を含めてリレーさせるように、以下のように compose.yaml を用意します。
(自作プロキシが上記「実際のコード」で作成した proxy_server.py となります。また、既存の構成がない場合でも、この内容で新規作成すれば問題ありません)
services:
  ollama:
    image: ollama/ollama
    ports:
      - "11434:11434"
    volumes:
      - ./ollama:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]
              driver: nvidia
              count: all

  proxy:
    image: python:3.10-slim
    working_dir: /app
    volumes:
      - ./proxy_server:/app
      - ./logs:/logs
    command: ["sh", "-c", "pip install -r requirements.txt && exec python proxy_server.py"]
    ports:
      - "8001:8001"
    depends_on:
      - ollama

  open-webui:
    image: ghcr.io/open-webui/open-webui:latest
    ports:
      - "8080:8080"
    environment:
      API_URL: http://proxy:8001
      ENABLE_OLLAMA_API: "True"
      OLLAMA_BASE_URL: http://proxy:8001

ポイントは、Open WebUIのリクエストをプロキシ(proxyコンテナ)経由にすることです。
この構成なら、Open WebUIのバージョンアップがあって内部仕様が変わっても、プロキシで外側からリクエストを見ているため、ログの取得に影響しにくい構成になります。

 

実際に取れたログとその考察

リレー処理を挟んだ結果、以下のようなログが取れました。(ログは抜粋して表示してます)

{"timestamp": "2025-06-28 12:24:22", "model": "gemma3:27b", "total_duration_ns": 94632160664, "load_duration_ns": 19994323564, "prompt_eval_count": 1031, "prompt_eval_duration_ns": 60875012677, "eval_count": 58, "eval_duration_ns": 13735608879, "token_per_sec": 4.222601306642811}
{"timestamp": "2025-06-28 12:30:58", "model": "gemma3:27b", "total_duration_ns": 394963073595, "load_duration_ns": 53579880, "prompt_eval_count": 3546, "prompt_eval_duration_ns": 195046011140, "eval_count": 760, "eval_duration_ns": 199816400248, "token_per_sec": 3.8034916005729964}
{"timestamp": "2025-06-28 13:59:10", "model": "qwen2.5vl:7b", "total_duration_ns": 759561136, "load_duration_ns": 13410971, "prompt_eval_count": 667, "prompt_eval_duration_ns": 408703073, "eval_count": 13, "eval_duration_ns": 334954117, "token_per_sec": 38.81128590516772}
{"timestamp": "2025-06-28 13:59:10", "model": "qwen2.5vl:7b", "total_duration_ns": 875906258, "load_duration_ns": 11146979, "prompt_eval_count": 558, "prompt_eval_duration_ns": 335325142, "eval_count": 20, "eval_duration_ns": 526305136, "token_per_sec": 38.00076919636977}
{"timestamp": "2025-06-28 13:59:37", "model": "qwen2.5vl:7b", "total_duration_ns": 3153000322, "load_duration_ns": 11314367, "prompt_eval_count": 721, "prompt_eval_duration_ns": 754606991, "eval_count": 72, "eval_duration_ns": 2383351049, "token_per_sec": 30.20956565765231}
{"timestamp": "2025-06-28 14:05:37", "model": "gemma3:12b", "total_duration_ns": 5428418296, "load_duration_ns": 22034384, "prompt_eval_count": 448, "prompt_eval_duration_ns": 4024403009, "eval_count": 21, "eval_duration_ns": 1380531832, "token_per_sec": 15.21152900152758}
{"timestamp": "2025-06-28 14:05:42", "model": "gemma3:12b", "total_duration_ns": 4395797252, "load_duration_ns": 24517951, "prompt_eval_count": 337, "prompt_eval_duration_ns": 2879844775, "eval_count": 23, "eval_duration_ns": 1490088937, "token_per_sec": 15.435320287865476}
{"timestamp": "2025-06-28 14:06:01", "model": "gemma3:12b", "total_duration_ns": 8595806105, "load_duration_ns": 30188016, "prompt_eval_count": 497, "prompt_eval_duration_ns": 4741797711, "eval_count": 51, "eval_duration_ns": 3822096375, "token_per_sec": 13.343462591259227}
{"timestamp": "2025-06-28 16:06:21", "model": "gemma3n:e4b", "total_duration_ns": 23945618955, "load_duration_ns": 13359735942, "prompt_eval_count": 16, "prompt_eval_duration_ns": 800558935, "eval_count": 200, "eval_duration_ns": 9782352502, "token_per_sec": 20.444979871570773}
{"timestamp": "2025-06-28 16:06:25", "model": "gemma3n:e4b", "total_duration_ns": 3821787614, "load_duration_ns": 62551020, "prompt_eval_count": 477, "prompt_eval_duration_ns": 593423184, "eval_count": 67, "eval_duration_ns": 3164968738, "token_per_sec": 21.169245432211913}
{"timestamp": "2025-06-28 16:06:27", "model": "gemma3n:e4b", "total_duration_ns": 1725808520, "load_duration_ns": 49401411, "prompt_eval_count": 556, "prompt_eval_duration_ns": 768251988, "eval_count": 20, "eval_duration_ns": 907228651, "token_per_sec": 22.045159153599084}
{"timestamp": "2025-06-28 16:06:30", "model": "gemma3n:e4b", "total_duration_ns": 2859492266, "load_duration_ns": 43687267, "prompt_eval_count": 432, "prompt_eval_duration_ns": 542335379, "eval_count": 46, "eval_duration_ns": 2272627867, "token_per_sec": 20.24088530636679}

今回検証したのは、以下の4つのローカルSLMモデルです。それぞれの特徴と、実際に取得できた平均トークン生成速度(tokens/sec)は次の通りでした。
モデル名 パラメータ規模 特徴・備考 平均 tokens/sec
qwen2.5vl:7b 7B 軽量・高速モデル 35.7
gemma3n:e4b 12B相当(最新) gemma3系の最新世代、最適化モデル 20.5
gemma3:12b 12B 標準的な中量モデル、バランス型 14.7
gemma3:27b 27B 高精度寄りの大型モデル、低速 4.0
※ 平均値は小数第2位まで、四捨五入
 
考察まとめ
この検証結果から、以下のことが見えてきました。
 
qwen2.5vl:7b
群を抜いて高速で、トークン生成速度は 35〜38 tokens/sec 程度を記録した。
軽量モデルならではのメリットを最大限に活かせるため、ローカル環境でのチャットボットや簡易生成タスクには最適といえる。
gemma3:12b
トークン生成速度は約 13〜15 tokens/sec で、中量モデルとしては標準的な結果であった。
qwen2.5vl:7b と比べると速度面では劣るが、より安定した出力品質や汎用性の高さが期待でき、処理時間と品質のバランスを重視する場面では有力な選択肢となる。
gemma3n:e4b 最適化世代という特徴通り、同じ12Bクラスでも約20〜22 tokens/sec と、旧世代の gemma3:12b より明確に高速化されている。
速度と品質のバランスが取れており、現実的な業務用途で最も扱いやすい中量モデルの一つといえる。
gemma3:27b トークン生成速度は約 3.8〜4.2 tokens/sec と、他モデルと比べて圧倒的に低速である。
その代わり、出力品質や精度重視の大型モデルであり、リアルタイム性を必要としない高品質生成タスク(文書要約、テキスト生成など)では十分に価値を発揮する。ローカル環境で使う場合は、運用用途を見極めた上で導入を検討する必要があるといえる。
 
また、今回出力した内容以外にも、リレー処理上のログの出力内容を修正することで、出力させる情報のカスタマイズが可能です。
 
このように、
  • モデルごとの推論時間
  • トークン生成速度(tokens/sec、1秒あたりに生成されるトークン数)
といった客観的な数値をもとに、ローカルSLMの実用性や適用範囲を見極める判断材料を得ることができました。
 
 

まとめと次の展開

今回の検証から得られたポイントは以下の通りです。

 ✅ ローカルSLMの実力を、数値で“見える化”できた
 ✅ プロダクト側を改造せず、ログを外から自由に加工できる構成が組めた
 ✅ 実用性や限界を、感覚ではなくデータで判断できるようになった

次回の展開

次は、さらに実業務に寄せた以下のテーマにも踏み込みたいと考えています。

  • ローカルSLMとRAGの実務レベル活用
  • ビジョンモデルやオフィス文書対応の検証
  • 特に、社内で多用されるExcelやPowerPoint、PDFの要約・活用性の評価

生成AIは“魔法”ではなく、“現実的なツール”です。だからこそ、我々としては、こうした地道な検証を積み重ねて、現場にフィットする使い方を今後も模索していこうと思います。

それでは、また次回もお楽しみに!

The post ローカルSLM/閉じた環境で動作する生成AIを検証してみた (②性能指標とログの取り方編) first appeared on MISO.]]>
ローカルSLM/閉じた環境で動作する生成AIを検証してみた (①環境構築編) https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/local-slm-1 Mon, 19 May 2025 04:38:30 +0000 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/?p=20420   はじめに 突然ですが、皆さん生成AI使っていますか?筆者は仕事でもプライベートでも「何かあったらまずは生成AIに聞いてみよう!」という感じで、毎日お世話になっています。 最近は、独自の生成AIを使用している企業も増え…

The post ローカルSLM/閉じた環境で動作する生成AIを検証してみた (①環境構築編) first appeared on MISO.]]>
 

はじめに

突然ですが、皆さん生成AI使っていますか?筆者は仕事でもプライベートでも「何かあったらまずは生成AIに聞いてみよう!」という感じで、毎日お世話になっています。
最近は、独自の生成AIを使用している企業も増えてきましたが、この記事を読んでいる皆さんはどのように活用されていますか?

我々SIerは、社内開発以外にもお客様先に出向いてシステム構築作業を行うケースが多くあります。その際に、お客様の企業ポリシーやセキュリティ上の理由で生成AIの利用が制限されるケースがあり、現場での生成AIの利用はまだ進んでおらず、「現場における生成AI活用」に課題を感じていました。

この記事では、そのような課題を解決する(かもしれない)「閉じた環境で動作する生成AI(ローカルSLM)」をご紹介します。

【記事の要約】
読了までの時間 : 6~7分
得られるメリット : Ollama + Open WebUIを使ったローカルSLM環境の構築手順がわかる

 

前提条件

必要なものは、「GPU搭載のWindows PC(OS:Windows 10/11)」だけです!
ただし、語弊がないようにお伝えすると、これを満たせばどんなPCでも動作するというわけではなく、安定して、快適に動作させるためには、高性能なGPUや十分なメモリ(32GB以上)を搭載したPCが必要になります。

ちなみに、この記事で使用したPCのOSのバージョンとスペックは以下の通りです。
GPUは、NVIDIA社のGPUを利用しており、ご紹介する手順もNVIDIA社のGPUを使用する手順となるので、ご了承ください。

OS Windows 11 Home (24H2)
CPU AMD Ryzen AI9 HX370
メモリ 32GB
ディスク 1TB
GPU / VRAM NVIDIA GeForce RTX 4060 / 8GB

構築手順

本稿では、Windows PCに「Windows Subsystem for Linux」(以下、WSL)を導入し、WSL上のUbuntuにDockerをインストールして、ローカルSLM環境(Ollama + Open WebUI)を構築していきます。(既にWSL、Dockerを導入済みの方は、「3. ローカルSLM環境の構築」から始めてください)

1.WSLのインストール

まず、Microsoft StoreからUbuntuをインストールします。以下のURLにアクセスし、WSLのUbuntuアプリのインストーラーをダウンロードして実行してください。

 URL:https://apps.microsoft.com/detail/9pdxgncfsczv?hl=ja-jp&gl=JP&ocid=pdpshare

インストールが開始されると以下のようなウィンドウが起動します。少し待ち、画面の指示に従い、ユーザーIDとパスワードを入力すると使用できるようになります。( ~$ と表示され、入力待ち状態になればインストール成功です)

 

2.Dockerのインストール

インストールしたUbuntu環境にDockerをインストールしていきます。
まず、Ubuntuのaptパッケージを最新化し、Dockerで使用するパッケージをインストールします。

# aptパッケージを最新化 
sudo apt update && sudo apt upgrade -y

# dockerで使用するパッケージをインストール
sudo apt-get install ca-certificates curl

次に、DockerのGPGキーを作成してアクセス権を設定し、Docker用のリポジトリを docker.list  に追加します。

# dockerのGPGキーの保存&アクセス設定
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# dockerのaptリポジトリを docker.list に追加
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

再度、aptパッケージを最新化し、Dockerをインストールします。

# aptパッケージをアップデート
sudo apt-get update
# dockerをインストール
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

最後に、起動設定とDocker用のユーザーおよびグループを設定します。

# dockerデーモンの設定
sudo systemctl enable docker
sudo systemctl enable containerd
sudo systemctl start docker

# docker用のユーザーとグループ設定
sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker

docker --version  と docker compose version  を実行し、正常にインストールされていることを確認します。

 
NVIDIA GPU搭載PCの場合は、NVIDIA Container Toolkit(以下、NVIDIA-CTK)もインストールしてください。

# NVIDIA-CTK取得用のリポジトリの追加とGPGキーを設定
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

# aptパッケージをアップデート
sudo apt-get update

# NVIDIA-CTKをインストール
sudo apt-get install -y nvidia-container-toolkit

NVIDIA-CTKのインストールが完了したら、Dockerコンテナ内でGPUを認識させる設定を行います。

# dockerデーモンにGPUを認識させる設定
sudo nvidia-ctk runtime configure --runtime=docker

# dockerの再起動
sudo systemctl restart docker

最後に、 docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi  を実行し、Docker内からホストのGPUが利用できるかを確認します。以下のように表示されれば成功です。(NVIDIA-CTKのインストールに失敗している場合や、NVIDIA GPUがない場合はエラーになります。NVIDIA社以外のGPUの場合は、各ベンダーが提供しているDocker用のランタイムやドライバーが必要になります。)

これで準備は完了です。

3.ローカルSLM環境の構築

それでは、いよいよローカルSLMの構築と動作確認を行います。
今回は、モデルの推論環境としてOllama、ユーザーとの対話用WebアプリケーションとしてOpen WebUIを使用して構築していきます。個別にインストールする方式もありますが、今回はDockerのメリットを最大限に活かし、Docker Composeでまとめて構築します。

といっても、手順はすごくシンプルです。
まず、WSLのUbuntu環境にログインし、以下のコマンドを実行してユーザーのホームディレクトリ配下に ollama フォルダを作成し、Docker Compose用の設定ファイル(  compose.yaml )を作成します。

# ホームディレクトリにollamaフォルダを作成して移動
mkdir -p ~/ollama 
cd ~/ollama

# ollamaフォルダ直下に、compose.yaml ファイルを作成
cat <<EOF > compose.yaml
services:
  ollama:
    image: ollama/ollama
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ./ollama:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]
              driver: nvidia
              count: all
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    ports:
      - "8080:8080"
    volumes:
      - ./open-webui:/app/backend/data
    environment:
      API_URL: http://ollama:11434
      ENABLE_OLLAMA_API: "True"
      OLLAMA_BASE_URLS: http://ollama:11434
      WEBUI_AUTH: "False"
EOF

次に、OllamaとOpen WebUIのコンテナを起動します。

# ollamaディレクトリに移動し、コンテナを起動
cd ~/ollama
docker compose up -d

コンテナとコンテナネットワークが作成されれば成功です。 ただし、初回起動には少し時間がかかります。 起動確認は docker logs -f open-webui でOpen WebUIコンテナのログを表示し、以下のようなログが出力され入力待ち状態になれば、起動が完了したことになります。


Open WebUI側の起動が確認できたら、ブラウザで http://localhost:8080 にアクセスし、以下のような画面が表示されればOKです。

「OK、始めましょう!」ボタンをクリックした後の画面
(この例では、認証なしモードですが、ユーザー認証をさせることも可能です)

以上で、ローカルSLM環境の構築は完了です。(Dockerはこのような場合に本当に便利です!)

4.モデルの取得

次に、推論環境Ollamaのモデルを取得します。
今回は、SLM(小規模言語モデル)のMicrosoft「Phi-4」とMeta「Llama 3」を取得してみます。(Ollama自体は大規模言語モデル(LLM)の取得も可能ですが、ローカルPCで動作させるため、ここでは10GB程度のSLMを取得しています)

モデルを取得するには、まず docker compose ps を実行してOllamaコンテナが起動していることを確認します。その後、 docker compose exec ollama ollama pull [モデル名] を実行します。 モデル名 は、 Ollamaのライブラリサイトに記載されているモデルを指定します。

# ollamaコンテナ状態確認
docker compose ps

# モデルの取得 (phi4)
docker compose exec ollama ollama pull phi4:14b

この例では phi4:14b を取得していますが、SLMといえどもサイズが9.8GBあるため、ダウンロードには多少時間がかかります。(PCのディスク容量にもご注意ください)

モデルの取得が完了したら、 docker compose restart open-webui でOpen WebUIコンテナを再起動します。再起動が完了した後、再度ブラウザで http://localhost:8080 にアクセスしてください。
画面上部のプルダウンメニューで phi4 のモデル(phi4:14b)が表示されていれば完了です。

ディスク容量に余裕があれば、 docker compose exec ollama ollama pull llama3.1:8b でLlama 3.1モデルもダウンロードしてみてください。

以下に、Ollamaのモデル管理でよく利用されるコマンドを載せておきます。(このコマンドは、本稿で紹介した compose.yaml で起動したコンテナで動作するものなのでご注意ください)
また、モデルの追加・削除後は、 docker compose restart open-webui でOpen WebUIコンテナの再起動をしてください。

モデルの追加 docker compose exec ollama ollama pull [モデル名] 
モデルの削除 docker compose exec ollama ollama rm [モデル名]  
モデルの一覧 docker compose exec ollama ollama ls 

モデル名 は、Ollamaライブラリページを参照)

5.動作確認

最後に動作確認を行います。再度、ブラウザで  http://localhost:8080 にアクセスしてください。
プロンプト入力用のテキストボックスに質問を入力し、送信してみてください。今回は「こんにちは、このモデルの特徴を簡単に教えてください。」というプロンプトを入力したところ、以下のような応答が生成されました。

 

画像では応答速度がわかりにくいため、動画も用意してみました。
GPU性能にもよりますが、GeForce RTX 4060で試したところ、応答時間も比較的よく、十分に実用に耐えうるレベルではないかと感じました。

【注意】
物理メモリが不足していたり、GPUが搭載されていなかったりする場合はエラーが発生する可能性があります。また、サイズの大きいLLMを使用する場合は、より多くのメモリやGPU VRAMが必要になります。
利用するPCのスペックに合わせて、動作可能なモデルを選択してください。

Ollamaで利用できるGPUについては、「Ollamaの公式では、コンピューティング能力5.0以上のNVIDIA GPU をサポート」にてご確認ください。

また、複数のモデルをダウンロードしている場合は、画面上部にあるモデル名の横の「+」ボタンから別のモデルを追加し、応答結果を比較することも可能です。

 

まとめ

今回はOllamaとOpen WebUIを使い、ローカルのPCで生成AIが試せるローカルSLM環境(※)の構築手順をご紹介しました。これで、セキュリティを気にせずAIを活用できる基盤が整いました。
(※ただ、快適に動作させるには、相応のPCスペックが必要ですが…)

この環境だけでも便利ですが、OllamaとOpen WebUIは、現場での利用を促進させるための優れた機能が用意されています。そこで次回は、この環境を発展させ、RAG(Retrieval-Augmented Generation)を構築する方法をお伝えしようと思います。
RAGを使えば、社内文書などローカルファイルの内容に基づいた応答をAIに生成させることが可能になり、現場での生成AI活用がより推進されること間違いなしです。

それでは次回の「RAG構築編」も、ぜひご期待ください!

The post ローカルSLM/閉じた環境で動作する生成AIを検証してみた (①環境構築編) first appeared on MISO.]]>
汎用的な仕組みでセンサーデータを見える化してみた――後編 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/sensor-data-visualization-02 Fri, 27 Mar 2020 06:00:54 +0000 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/?p=9748 前回の記事で、PC上でセンサーの値が取得できることが確認できたので、今回はクラウドのAWS上にデータ連携していきます。前回の記事は以下からご覧ください。 4. AWSとの連携 4-1. AWS IoT Coreの設定 前…

The post 汎用的な仕組みでセンサーデータを見える化してみた――後編 first appeared on MISO.]]>
前回の記事で、PC上でセンサーの値が取得できることが確認できたので、今回はクラウドのAWS上にデータ連携していきます。前回の記事は以下からご覧ください。

4. AWSとの連携

4-1. AWS IoT Coreの設定

前回作成したクライアントプログラムからのデータをAWS IoT Coreで受け取れるように、AWSコンソールにログインしてAWS IoT Coreの設定します。

4-1-1. 証明書の作成

AWS IoT Coreの左のメニューから『安全性』→『証明書』をクリックし、画面右上の『作成』ボタンを押下します。

『1-Click証明証作成(推奨)』にある『証明書の作成』ボタンを押下します。

『1-Click証明書作成(推奨)』にある『証明書の作成』ボタンを押下します。『証明書が作成されました!』という画面が表示されるので画面内の「このモノの証明書」、「パブリックキー」、「プライベートキー」それぞれの『ダウンロード』ボタンを押下して各キーをダウンロードします。

また、AWS IoT のルートCAのダウンロードが必要となるので、AWS IoT のルートCA『ダウンロード』リンクに行き、ページ内の「RSA 2048 ビットキー: Amazon ルート CA 1」のリンクよりルートCAのキーも併せてダウンロードしておきます。

上記が完了したら、『証明書が作成されました!』の画面の左下にある『有効化』ボタンを押下します。

全部で4つのキーがダウンロードされているので、上記のプログラムが格納されているフォルダ配下にcertsというフォルダを作成し、その中に配置しておきます。

4.1.2 ポリシーの作成

次に、AWS IoT Coreと接続するためのポリシーを作成と上記で作成した証明書とのアタッチを行います。

AWS IoT Coreの左のメニューから『安全性』→『ポリシー』をクリックし、画面右上の『作成』ボタンを押下します。

入力項目があるのでそれぞれ以下のように入力して画面右下の『作成』ボタンを押下します。

項目 設定値
名前 sat-iot-aws-demo (任意の文字列でOK)
アクション iot:*
リソースARN * (本来はリソースを絞る必要があるが今回は*とした)
効果 『許可』をチェック

再度、AWS IoT Coreの画面に戻り、左メニューの『安全性』→『証明書』をクリックし、上記で作成した証明書の行の左端のチェックをONにします。

画面、右上の『アクション』より『ポリシーのアタッチ』を押下して、上記で作成したポリシー(ここでは、sat-iot-aws-demo)を選択して、『アタッチ』を押下します。

4.1.3 アクションルールの作成

ここまでで、センサーとIoT Core側の接続が可能になったので、次に、AWS側でデータ受信した際のアクションルールの設定を行います。アクションルールを設定することで、IoT Coreでデバイスから受信した後の動作を定義することが可能になります。

今回は、AWS IoT Coreが受信した後に、Lambdaを呼び出して、CloudWatchのメトリクスに登録していくシナリオを立ててみました。

まずは、AWS IoT Coreが受けた際に呼び出されるLambdaを作成していきます。Lambda内ではCloudWatchのメトリクスの登録を行うプログラムを以下のように作成してあり、名前を『sat-IoTCoreActtionRuleLambda』として作成しました。
(Lambdaの作成に必要なIMAロールやLambda自体の作成手順は省略してありますのでご了承ください)

var aws = require('aws-sdk');               // AWS SDKモジュール
var momentTz = require('moment-timezone');  // moment-timezone.js用ライブラリ
var moment = require('moment');             // moment.js用ライブラリ

/**
 * エントリーポイント
 * → リクエストが送られてきたときのイベントハンドラー
 */
exports.handler = async (event) => {
  console.log("start.");
  console.log("event : [" + JSON.stringify(event) + "]");

  // センサーからの値設定
  const metriscNamespace = 'SATIoTMetrics';
  const metriscName = 'SATIotMetrics-Prism' + event['proofid_u12bit'];
  const unixtime = event['unixtime'];
  const ts = moment.unix(event['unixtime']);
  const ts_tz = momentTz.unix(event['unixtime'], 'X').tz('Asia/Tokyo').format('YYYY-MM-DD HH:mm:ssZ');
  const temperature_value = event['temperature_value'];
  const temperature_unit = event['temperature_unit'];
  const humidity_value = event['humidity_value'];
  const humidity_unit = event['humidity_unit'];
  // debug
  console.log("metrics     : namespace [" + metriscNamespace + "] name : [" + metriscName + "]");
  console.log("timestamp   : ISO8601 [" + ts.toISOString() + "] unixtime [" + unixtime + "] tz [" + ts_tz + "]");
  console.log("temperature : val - " + temperature_value + ", unit - " + temperature_unit + "");
  console.log("humidity    : val - " + humidity_value + ", unit - " + humidity_unit + "");

  // cloudwatchメトリクスのパラメータ設定
  const metricsParam = {
    MetricData: [
      {
        MetricName: metriscName,                                  // メトリクス名
        Dimensions:[{ Name: "DataType", Value: "temperature" }],  // DataType:温度
        Timestamp: ts.toISOString(),                              // タイムスタンプ(ISO8601形式)
        Unit: 'None',                                             // 単位(℃がないのでNone)
        Value: Number(temperature_value)                          // センサーの値(温度)
      },
      {
        MetricName: metriscName,                                  // メトリクス名
        Dimensions:[{ Name:"DataType", Value:"humidity" }],       // DataType:湿度
        Timestamp: ts.toISOString(),                              // タイムスタンプ(ISO8601形式)
        Unit: 'None',                                             // 単位(%はあるけど、温度に合わせてNone)
        Value: Number(humidity_value)                             // センサーの値(湿度)
      }
    ],
    Namespace: metriscNamespace                                   // カスタム名前空間
  };

  // cloudwatchメトリクスの登録
  const cloudwatch = new aws.CloudWatch({ region: 'ap-northeast-1' });
  let responseBody = [];          // レスポンスの body(cloudwatch.putMetricsData 結果保持)
  let metricsData = {};
  try {
    metricsData = await cloudwatch.putMetricData( metricsParam ).promise();
    responseBody.push({ 'name' : metriscName, 'data' : metricsData });
    console.log("cloudwatch.putMetricData success ! [" + JSON.stringify(metricsData) + "]");
  } catch (e) {
    console.log("cloudwatch.putMetricData error ! [" + e.message + "]");
    throw new Error(e);
  }

  // 正常終了
  console.log("end.");
  const response = {        // status : 200(Ok) / 500(Error)
    statusCode: 200,
    body: responseBody,
  };
  return response;

};

なお、Lambda側のインベントではAWS IoT Coreから(元はセンサー側)以下のようなデータが引数で送信される前提で作成してありますので、ご注意ください。

{
  "unixtime": 1568883351,         // unixtime
  "proofid_u12bit": "EF8E",       // ProofIDの下12bit
  "temperature_value": "27.12",   // センサーの温度
  "temperature_unit": "℃",       // 温度の単位
  "humidity_value": "54.23",      // センサーの湿度
  "humidity_unit": "%"            // 湿度の単位
}

AWS IoT Coreの左メニューの『ACT』で表示される画面の右上の『作成』ボタンを押下します。

入力項目は、それぞれ以下のように入力して画面右下の『ルールの作成』ボタンを押下します。

項目 設定値
名前 sat_iot_aws_rule (任意の文字列でOK)
説明 AWS IoT Core – Test (任意の文字列でOK)
SQLバージョンの使用 2016-03-23 (デフォルト)
ルールクエリステートメント SELECT * FROM sat/test
1つ以上のアクションを設定する 下記の手順を参照
エラーアクション 今回は未設定
タグ 今回は未設定

また、アクションの追加の手順は、『1つ以上のアクションを設定する』にある『アクションの追加』ボタンを押下し、次の画面で『メッセージデータを渡すLambda関数を呼び出す』を選択し、下にスクロールさせ『アクションの設定』ボタンを押下します。

次に、アクションの設定画面となるので、上記で作成したLambda関数(この例では、sat-IoTCoreActtionRuleLambda)を選択し、画面右下の『更新」ボタンを押下します。

すべての項目の入力が完了すると、以下のように画面右下の『ルールの作成』ボタンが押下可能になるので、ボタンを押下して作成します。

アクションルールの作成がされていることを確認してください。

4.2 クライアントプログラムの修正

ここまで、AWS側の設定が完了したので、上部で作成したクライアントプログラムを改修し、取得したセンサーのデータをAWS IoT Coreに送信するように修正します。

AWS IoT Coreに送信するためには、上記AWS IoT Coreの設定でダウンロードした4つの証明書(このモノの証明書:xxxxx.pem.crt、パブリックキー:xxxxx.public.pem.key、プライベートキー:xxxxx.private.pem.key、AWS IoTのルートCA:AmazonRootCA1.pem)が格納されたcertsフォルダをプログラム直下に配置していただく必要があります。

改修したクライアントプログラムは以下のとおりです。MQTTで通信する際のトピック名には、上記でのAWS IoT Coreのルールの作成でルールクエリステートメントに指定した sat/test  にし、送信内容もLambda関数作成時に想定している内容で送信するように変更しました。

なお、AWS IoTのオブジェクト( var device = awsIoT.device({ ... }); )の初期化で、keyPath、certPath、caPathで <SET YOUR ... KEY> としている箇所は、それぞれダウンロードした、プライベートキー(拡張子 .private.pem.key)、このモノの証明書(拡張子 .crt)、AWS IoTのルートCA(拡張子.pem)のファイル名を指定してください。

また、host の <SET YOUR AWS IOT ENDPOINT>  には、AWSコンソールのAWS IoT Coreの左メニュー『設定』で表示されるAWS IoTのエンドポイントを指定してください。

// websocket
var WebSocketClient = require('websocket').client;
var client = new WebSocketClient();
// awsIoTCore
var awsIot = require('aws-iot-device-sdk');
var device = awsIot.device({
    keyPath : '.\\certs\\<SET YOUR PRIVATE KEY>',
    certPath : '.\\certs\\<SET YOUR CERT KEY>',
    caPath : '.\\certs\\<SET YOUR ROOT CA KEY>',
    clientId : 'nouser' + (Math.floor((Math.random() * 100000) + 1)),
    host: '<SET YOUR AWS IOT ENDPOINT>'
});

// 10進 → 16進変換(2byte、16bit)
function decimalToHexString(number) {
    if (number < 0) {
        number = 0xFFFF + number + 1;
    }
    return number.toString(16).toUpperCase();
}
// リトルエンディアン16進 → 10進変換(2byte、16bit)
function littleEndianHexToDec(hexstring) {
    if (hexstring == '0') return parseInt(hexstring);
    var number = parseInt('0x'+ hexstring.match(/../g).reverse().join(''));
    return number;  // decimal
}
// リトルエンディアン16進 → 16進変換(2byte、16bit)
function littleEndianHexToHex(hexstring) {
    if (hexstring == '0') return parseInt(hexstring).toString(16).toUpperCase();
    var number = parseInt('0x'+ hexstring.match(/../g).reverse().join(''));
    return number.toString(16).toUpperCase(); // Hex
}
// Q15フォーマットの10進を通常の10進数の固定長少数(digits=2)に変換
function q15DecToFixed(number) {
    // Q15フォーマット、LSB=100 を通常の10進の固定長少数に変換
    var num = number * 100 / 32768;
    return num.toFixed(2);
}

// AWS IoT Core接続時
device.on('connect', function() {
    console.log('AWS-IoTCore connected !');
  // AWS IoTCore接続エラー時
  device.on('error', function(error) {
    console.log("connection error(aws) : " + error.toString());
  });
});

// socket接続時
client.on('connect', function(connection) {
  // 接続成功!
  console.log('websocket connected !');

  // socket接続エラー時
  connection.on('error', function(error) {
      console.log("connection error(websocket) : " + error.toString());
  });

  // socketデータ受信時(Nodeの場合は、Bufferとなる模様)
  connection.on('message', function(e) {
    var buf = e.binaryData;
    console.log("received -------------------------");
    // console.log(buf);
    // console.log(buf.byteLength);

    // buffer → ArrayBuffer
    var ab = new ArrayBuffer(buf.byteLength);
    var view = new Int8Array(ab);
    for ( var i=0; i < buf.byteLength; i++) {
        view[i] = buf[i];
    }
    var dv = new DataView(ab);
    var offset = 0;

    // BLE通信データ(Blob)の解析
    offset = 0;     // 経過時間(16進)
    console.log('dataAge(hex) - 0x' + littleEndianHexToHex( decimalToHexString(dv.getInt16(offset))));
    offset += 2;    //  major、minor(16進、プルーフID:DB Address下12bit)、2byteずらす
    var proofid_u12bit = littleEndianHexToHex( decimalToHexString(dv.getInt16(offset)));
    console.log('proofid(hex) - 0x' + proofid_u12bit);
    offset += 2;    // 温度 value[0](10進)、2byteずらす
    var tempQ15Hex = littleEndianHexToHex( decimalToHexString(dv.getInt16(offset)) );
    var tempQ15Dec = littleEndianHexToDec( decimalToHexString(dv.getInt16(offset)) );
    var temperature = q15DecToFixed(tempQ15Dec);
    console.log('temperature  - 0x' +  tempQ15Hex + ' -> dec-q15 : [' + tempQ15Dec + '] -> dec : [' + temperature + ']');
    offset += 2;    // 湿度 value[1](10進)、2byteずらす
    var humidityQ15Hex = littleEndianHexToHex( decimalToHexString(dv.getInt16(offset)) );
    var humidityQ15Dec = littleEndianHexToDec( decimalToHexString(dv.getInt16(offset)) );
    var humidity = q15DecToFixed(humidityQ15Dec);
    console.log('humidity     - 0x' +  humidityQ15Hex + ' -> dec-q15 : [' + humidityQ15Dec + '] -> dec : [' + humidity + ']');

    // AWSのIoTCoreに送信
    // MQTT送信情報
    const mqttTopicName = 'sat/test'
    const mqttMessage = {
        'unixtime': Math.floor( new Date().getTime() / 1000),
        'proofid_u12bit': proofid_u12bit,
        'temperature_value': temperature,
        'temperature_unit': "℃",
        'humidity_value' : humidity,
        'humidity_nit': "%"
    }
    console.log('publish : [' + JSON.stringify(mqttMessage) + ']');
    device.publish(mqttTopicName, JSON.stringify(mqttMessage));
    console.log('aws iot published !!!');
  });   // WebSocket message.on

  // socket接続切断時
  connection.on('close', function() {
      console.log('WebSocket Client Closed');
  });

});

// モニタリングボックスへwebsocket接続
var con = 'ws://172.28.0.3/vreg?readBlocks=0x0300&interval=9000';
client.connect(con);

上記、プログラムを実行( node client.js )すると、以下のようにセンサーから取得したデータをAWS IoT Coreに転送が開始されます。

4.3 クライアントとAWSの連携確認

これまでの設定した内容を元に、AWS IoT CoreのトップページにあるモニタリングでクライアントとAWSとの連携がされていることを確認します。

AWSコンソールから『AWS IoT Core』→『モニタリング』で接続成功のグラフが表示されます。ここに接続成功のデータが表示されていれば、センサーからデータが接続されていることが確認できます。

AWS IoT Coreで設定したアクション(Lambdaを経由してColudWatchのメトリクス登録)もされていることが確認できます。

5. センサーデータを可視化

上記でセンサーの情報がCloudWatchまで登録されていることが確認できたので、最後にセンサーデータの可視化を行います。

今回は、AWS IoT Coreでデータを受けた際に、アクションの起動されるLambdaでCloudWatchメトリクスの登録を行っているので、CloudWatchのダッシュボードを使用してメトリクスを可視化してみました。

まとめ

今回、センサーはμPRISMを使用しましたが、モニタリングボックスと繋がるセンサーであれば、基本的には今回の構成で様々なセンサーデータと接続することができ、IoTとしてクラウドと連携して可視化やその他を行うことが可能になる仕組みを構築することができました。

また、AWS側で受けたデータをLambdaで処理する際に、温度、湿度から暑さ指数(WBGT)を算出したり、一定の閾値を超えた場合に、SNSでメールや携帯に通知やConnectで電話をかけるなどといった、他のサービスとの連携も可能になるなど、IoTを使用した運用の仕組みなどにも応用ができるといえそうです。

最後に、IoTデータをクラウド上にアップして処理を行ってますが、会社のポリシーでクラウドにデータを上げることができない場合でも、モニタリングボックスから社内ネットワークに繋ぐことで工場内の機器やデバイスの値を可視化するなど汎用的な仕組みとなっております。もし、今回の記事で、センサーデータの可視化にご興味を持たれましたら、ぜひ当社お問い合わせフォームからご連絡いただければと思います。

The post 汎用的な仕組みでセンサーデータを見える化してみた――後編 first appeared on MISO.]]>
汎用的な仕組みでセンサーデータを見える化してみた――前編 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/sensor-data-visualization-01 Fri, 27 Mar 2020 00:00:44 +0000 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/?p=7660 はじめに 過去2回WebiotのセンサーデータをAWSに連携した見える化について記載いたしました。今回はまだまだIoTの注目が集まっている昨今の状況を鑑みて、より汎用的にセンサーデバイスからデータ取得し、AWSへの連携か…

The post 汎用的な仕組みでセンサーデータを見える化してみた――前編 first appeared on MISO.]]>
はじめに

過去2回WebiotのセンサーデータをAWSに連携した見える化について記載いたしました。今回はまだまだIoTの注目が集まっている昨今の状況を鑑みて、より汎用的にセンサーデバイスからデータ取得し、AWSへの連携からデータの可視化までを2回に分けてお届けしようと思います。

過去の記事は下記よりご覧ください。

1. 今回のシナリオ

これまでは、センサー側にデータの取得、送信機能があったため、ほぼAWS側の手順となっていましたが、今回はより汎用的に、センサーデータの取得から送信機能までを含めたセンサーデータの可視化の手順を見ていこうと思います。

2. 構築するシステム構成

今回、構築するシステム構成は以下の通りです。

使用するセンサーについて

今回使用するのは、エレックス工業株式会社のμPRISMという超小型のセンサーデバイスです。μPRISMの特徴は、7種類のセンサーとBluetoothを搭載した超小型IoTセンサーで、加速度、地磁気、温度、湿度、気圧、照度、UVの測定が可能です。(μPRISM電池タイプ)

今回は、μPRISMをビーコンモードで動作させ、温度と気圧を定期的に信号を発信させてデータを取得することにしました。

また、センサーと接続して情報を発信するモニタリングボックスとして、当社グループ会社のTDIプロダクトソリューション株式会社(以下、TDIPS)製のモニタリングボックス『MB-3』を使用して、定期的にセンサーからデータを取得できるような構成にしました。(モニタリングボックスはWebSocketに対応しており、センサーからのデータをWebSocket経由で通信が可能)

モニタリングボックス『MB-3』の詳細は当社ホームページでもご紹介しておりますので、ぜひご覧いただければと思います。なお、TDIPSでは、上記のモニタリングボックスなどを使用して、各種センサーを用いたシステムで工場の機器保全を支援しておりますので、もしご興味がある方は、上記ホームページよりぜひご相談いただければと思います。

MQTT通信について

IoT(Internet of Things:モノのインターネット)デバイスでは、インターネットに接続できることが条件となっており、インターネットに接続することで、他のデバイスやサービスと通信できるようになっています。

インターネットの基礎となっているネットワークのプロトコルはTCP/IPであり、IoT通信ではTCP/IPをベースに作成されたMQTT(Message Queuing Telemetry Transport)が標準的な通信プロトコルになっています。

MQTTは、1990年代後半にIBMが考案して開発したプロトコルで、当初は油田パイプラインに取り付けられたセンサーを衛星とリンクするために使用されていましたが、そのパブリッシュ/サブスクライブモデルを採用していたことで2014年後半になって、正式にOASISオープンスタンダードになり、現在ではよくつかわれているプログラミング言語でサポートされるようになっています。

なぜIoT開発にMQTTが採用されているかというと、MQTTは軽量さと柔軟性においてIoTを開発する際にバランスが通信プロトコルといえるからです。

  • 軽量なプロトコルであるということは、制約が厳しいデバイス・ハードウェア上でも、待ち時間が長かったり帯域幅がかぎられているネットワーク上でも実装し易いということを意味します。
  • MQTTの柔軟性は、IoTデバイスやサービスの多種多様なアプリケーションシナリオをサポートすることができるということを意味します。

そして、MQTTプロトコルの重要な特徴は、そのパブリッシュ・サブスクライブモデルで、データの送信側と受信側をブローカーという役割を介して切り離すことが可能です。

パブリッシュ・サブスクライブモデルについてはここでは詳細な説明はしませんが、このモデルを採用することでMQTTにおいて、送信側はブローカーに対してメッセージに『トピック』をつけて送信(以下、パブリッシュ)し、クライアントである受信側もブローカーから『トピック』を受信(以下、サブスクライブ)してメッセージの転送をやり取りする仕組みとなっています。

3. センサーデータの取得

まずは、センサーとモニタリングボックスとPCを接続して、PC上のプログラムからセンサーの値が取得できることを確認します。

3-1. センサーとモニタリングボックスの接続

センサーのμPRISMとモニタリングボックス『MB-3』を繋げます。今回はμPRISM側で既にBluetoothでモニタリングボックスとの接続設定を行っています。

次に、今回はプログラムはPC上で動作させるため、モニタリングボックスとPCを接続して、PC上のネットワーク設定を行います。モニタリングボックス自体のIPアドレスが『172.28.0.3』であるため、PC上のネットワーク設定のIPアドレス(IPv4)のレンジ設定を以下のように行います。

モニタリングボックスにPingが通れば、ネットワークの設定はOKです。

ちなみにこのμPRISMのセンサーは本当に小さいのです。デバイスはどこにでも持ち運べる大きさもメリットになるのではと思います。(10円硬貨と比べても一回り大きいくらいのサイズ感)

また、モニタリングボックスで設定されたI/F仕様は以下のとおりです。

3-2. クライアントプログラムの作成

次に、データの送信元(Publish)になるクライアント側のプログラムを作成します。

今回はJavaScriptのnode.jsを使用してクライアントプログラムを作成しています。(JavaScriptを選択した理由は、簡単にコードが書け、テストがしやすいのと、WebSocket、MQTTそれぞれのプロトコルのライブラリが充実しているという2点です、後は筆者が慣れているというところが最大の要因ですが…)

node.jsになじみがない方などは、こちらの公式サイトからインストールから利用方法までが記載されているので参考にして下さい。

node.jsが正常にインストールされているPCでは、コマンドプロンプトで node --version と入力するとnode.jsのバージョン情報が返ってきます。

まずPC上で以下のプログラムを作成して、センサーの生データを受信してみます。

node.jsでプロジェクトの作成や初期化はこちらが参考になります。

// WebSocketClient 初期化
var WebSocketClient = require('websocket').client;
var client = new WebSocketClient();

// socket接続時
client.on('connect', function(connection) {
  // 接続成功!
  console.log('websocket connected !');

  // socket接続エラー時
  connection.on('error', function(error) {
      console.log("connection error(websocket) : " + error.toString());
  });

  // socketデータ受信時(nodeの場合、ArrayBufferで返却)
  connection.on('message', function(e) {
    var buf = e.binaryData;
    console.log("received -------------------------");
    console.log(buf);
    console.log(buf.byteLength);
  });   // WebSocket message.on

  // socket接続切断時
  connection.on('close', function() {
      console.log('WebSocket Client Closed');
  });

});

// モニタリングボックスへwebsocket接続
var con = 'ws://172.28.0.3/vreg?readBlocks=0x0300&interval=9000';
client.connect(con);

以下のようにBufferでセンサーからのデータは取得できており、モニタリングボックスで設定されたI/F仕様の通り設定されているようですが、まだこれでは具体的に何がどうなっているか、人が解釈できる状態ではなく読み取りにくい値になっています。

byte メモ
1-2 経過時間 送信後の経過時間
3-4 ProofIDの下12bit 固定値、センサーのBDAddress(MACアドレス)の下12bit
5-6 温度 リトルエンディアン16進数の値、Q15フォーマット(LSB=100)
7-8 湿度 リトルエンディアン16進数の値、Q15フォーマット(LSB=100)

例えば、Bufferが [09 00 8e ef 9f 22 b9 42] となっている場合は以下のように解釈されます。
この値はリトルエンディアンで格納されているので、各項目は逆から読み込み、Q15フォーマット(LSB=100)は、16進→10進変換した値を2^15(=32768)で割り100掛けて変換していきます。

byte 解釈された値
1-2 09 00 0x0009 → 9 (秒経過時点)
3-4 8e ef 0xEF8E → f8e (センサーのBDAddress(00:08:9c:00:1F:8e)の下12bit)
5-6 9f 22 0x229f → 8863 →27.0477… → 27.05 (温度、℃)
7-8 b9 42 0x42b9 → 17081 →52.1270… → 52.13 (湿度、%)

3-3. センサーデータの確認

次に、上記で受信したデータを人が解釈できるようにするため、以下のようにプログラムを改修しました。

// WebSocketClient 初期化
var WebSocketClient = require('websocket').client;
var client = new WebSocketClient();

// 10進 → 16進変換(2byte、16bit)
function decimalToHexString(number) {
    if (number < 0) number = 0xFFFF + number + 1;
    return number.toString(16).toUpperCase();
}
// リトルエンディアン16進 → 10進変換(2byte、16bit)
function littleEndianHexToDec(hexstring) {
    if (hexstring == '0') return parseInt(hexstring);
    var number = parseInt('0x'+ hexstring.match(/../g).reverse().join(''));
    return number;  // decimal
}
// リトルエンディアン16進 → 16進変換(2byte、16bit)
function littleEndianHexToHex(hexstring) {
    if (hexstring == '0') return parseInt(hexstring).toString(16).toUpperCase();
    var number = parseInt('0x'+ hexstring.match(/../g).reverse().join(''));
    return number.toString(16).toUpperCase(); // Hex
}
// Q15フォーマットの10進を通常の10進数の固定長少数(digits=2)に変換
function q15DecToFixed(number) {
    // Q15フォーマット、LSB=100 を通常の10進の固定長少数に変換
    var num = number * 100 / 32768;
    return num.toFixed(2);
}

// socket接続時
client.on('connect', function(connection) {
  // 接続成功!
  console.log('websocket connected !');

  // socket接続エラー時
  connection.on('error', function(error) {
      console.log("connection error(websocket) : " + error.toString());
  });

  // socketデータ受信時(Nodeの場合は、Bufferとなる模様)
  connection.on('message', function(e) {
    var buf = e.binaryData;
    console.log("received -------------------------");
    console.log(buf);
    console.log(buf.byteLength);

    // buffer → ArrayBuffer
    var ab = new ArrayBuffer(buf.byteLength);
    var view = new Int8Array(ab);
    for ( var i=0; i < buf.byteLength; i++) {
        view[i] = buf[i];
    }
    var dv = new DataView(ab);
    var offset = 0; // ArrayBufferのオフセット

    // BLE通信データ(Blob)の解析
    offset = 0;     // 経過時間(16進)
    console.log('dataAge(hex) - 0x' + littleEndianHexToHex( decimalToHexString(dv.getInt16(offset)) ));

    offset += 2;    //  major、minor(16進、プルーフID:BD Address下12bit)、2byteずらす
    var proofid_u12bit = littleEndianHexToHex( decimalToHexString(dv.getInt16(offset)));
    console.log('proofid(hex) - 0x' + proofid_u12bit);

    offset += 2;    // 温度 value[0](10進)、2byteずらす
    var tempQ15Hex = littleEndianHexToHex( decimalToHexString(dv.getInt16(offset)) );
    var tempQ15Dec = littleEndianHexToDec( decimalToHexString(dv.getInt16(offset)) );
    var temperature = q15DecToFixed(tempQ15Dec);
    console.log('temperature  - 0x' +  tempQ15Hex + ' -> dec-q15 : [' + tempQ15Dec + '] -> dec : [' + temperature + ']');

    offset += 2;    // 湿度 value[1](10進)、2byteずらす
    var humidityQ15Hex = littleEndianHexToHex( decimalToHexString(dv.getInt16(offset)) );
    var humidityQ15Dec = littleEndianHexToDec( decimalToHexString(dv.getInt16(offset)) );
    var humidity = q15DecToFixed(humidityQ15Dec);
    console.log('humidity     - 0x' +  humidityQ15Hex + ' -> dec-q15 : [' + humidityQ15Dec + '] -> dec : [' + humidity + ']');

  });   // WebSocket message.on

  // socket接続切断時
  connection.on('close', function() {
      console.log('WebSocket Client Closed');
  });

});

// モニタリングボックスへwebsocket接続
var con = 'ws://172.28.0.3/vreg?readBlocks=0x0300&interval=9000';
client.connect(con);

下記の赤枠の値が、センサーから取得した値です、温度(temperature)と湿度(humidity)の値が10進で表示され、センサーからデータが取得できていることが確認できます。

以上で、PC上でセンサーの値が取得できることが確認できました。次回の記事で、クラウドのAWS上にデータ連携していきます。お楽しみに!

The post 汎用的な仕組みでセンサーデータを見える化してみた――前編 first appeared on MISO.]]>
AWSでIoTデータを見える化してみた その2 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/aws-iot-data-visualization-2 Mon, 01 Apr 2019 00:00:32 +0000 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/?p=5138 はじめに 前回、下記記事でAWSでIoTデータを見える化としてセンサーデータの取得から格納、格納したデータを表形式で見える化する部分までをご紹介しました。 今回は、その続きとしてAWSのBIツールであるAmazon Qu…

The post AWSでIoTデータを見える化してみた その2 first appeared on MISO.]]>
はじめに

前回、下記記事でAWSでIoTデータを見える化としてセンサーデータの取得から格納、格納したデータを表形式で見える化する部分までをご紹介しました。

今回は、その続きとしてAWSのBIツールであるAmazon QuickSight(以下、QuickSight)と連動して、取得したデータをグラフィカルに可視化する、見える化の応用について取り上げさせていただきます。

1. 今回のシナリオ

今回のシナリオでは、前回Amazon DynamoDB(以下、DynamoDB)に格納したセンサーデータを使用し、AWSのBIツールであるQuickSightの紹介と合わせて、蓄積されたデータをBIツールで扱いやすくするための加工やQuickSightによるデータの可視化までを流れをご紹介させていただきます。

2. 今回構築するシステム構成

前回のシステム構成に機能を追加させ、今回は以下のような構成を検討しました。(2019年1月末に、AWSのアーキテクチャアイコンが新しくなったので、こちらのアイコンも最新化)

 

3. QuickSightに関して

概要

QuickSightは、AWSが提供するクラウドBIサービスです。簡単に情報を可視化する事が出来、アドホック分析を実行して素早くデータから気付きを得ることが可能です。また、AWSのデータソースとシームレスに連携することが可能で、作成したダッシュボードやレポートを共有する事が可能です。QuickSightでは、今回のこのサービスの為に新しく作られた『SPICE』という仕組みを採用しています。
SPICEとは

  • Super-fast(超高速)
  • Parallel(並列)
  • In-memory Calculation Engine(インメモリ計算エンジン)

の頭文字からなる造語ですが、この仕組みを用いたQuichSightには以下のような特徴があります。

    • 素早く始められる
      サインインし、データソースを選んで数分でデータの可視化が可能です。
    • 複数のデータソースにアクセスできる
      ファイルやAWSデータソース、外部のデータベースにも対応可能です。
    • 最適化された可視化
      選択したデータに基づいて最適化された可視化の提供が可能です。
    • 素早く答えを得られる
      大きなデータセットに対しても素早く、インタラクティブな可視化が可能です。
    • データを通してストーリーを伝える
      可視化を通した”気付き”を他のメンバーと共有する事ができます。

料金体系

QuickSightには「Standard Edition」、「Enterprise Edition」の2つがあり、どちらのエディションを選ぶか、利用期間をどうするか、ストレージ容量はどれくらいにするかで料金が決まります。

  • Standard Edition を選んだ場合
    利用期間は月次利用で、1ユーザーあたり 12ドル/月、年間利用では、1ユーザーあたり 9ドル/月
    ストレージ容量は、SPICEストレージ 0.25ドル/GB/月
     (※10GB以上からの料金(SPICEストレージは10GBまで含まれており無料))
  • Enterprise Edition を選んだ場合
    利用期間は月次利用で、1ユーザーあたり 24ドル/月、年間利用では、1ユーザーあたり 18ドル/月
    ストレージ容量は、SPICEストレージ 0.38ドル/GB/月
     (※10GB以上からの料金(SPICEストレージは10GBまで含まれており無料))

その他、各エディションの機能比較などの詳細は、AWSの公式サイトにて確認することができます。

利用までの流れ

QuickSightは、AWSマネジメントコンソールにログインし、AWSサービス一覧からQuickSightを選択することで利用可能になります。初めてQuickSightを利用する場合は、以下のようにアカウント作成、エディションの選択を行った後に利用開始になります。

QuickSightアカウントの作成

AWSアカウントを確認し、「Sign up for QuickSight」を押下。

利用するエディションの選択

今回は Standard Editionを選択し、画面下にある「Continue」を押下。

QuickSightアカウントの追加情報を入力。今回は、QuickSightのデータソースとして、Athena、S3、S3 Storage Analytics、IoT Analyticsのすべてにチェックを入れてみました。(これらの設定は後から変更が可能)

追加情報の入力が完了したら、「Finish」を押下。

すると、アカウント作成中となり、作成完了画面が表示されればQuickSightが利用可能になります。

QuickSightへのサインアップ

上記のアカウント作成完了画面にて「Go to Amazon QuickSight」を押下するとQuickSightへサインアップが可能になります。初めは、QuickSightの解説が表示されますが、その後にサンプルページが表示されれば完了となります。

4. データ加工(Lambda)

QuickSightでDynamoDBのテーブルを直接データソースに指定したいところですが、執筆時点ではまだできないため、今回はDynamoDBのデータをAWS Lambda(Lambda)で加工の上JSONファイルに変換し、Amazon S3(以下、S3)上に出力したファイルをQuickSightで可視化してみようと思います。

また、今回DynamoDBのテーブルは前回使用したテーブルのsensortimeというunixtimeをローカル時間(JST)に変換したタイムスタンプの列を、sensor_date(YYYY-MM-DDの年月日の文字列)、sensor_time(hh:mi:ssの時分秒の文字列)に分割し、この2列をインデックスとして設定したテーブル(miso_20190201_dynamodb)をベースにJSONファイルを作成しております。

データ加工用のLambdaを作成します。今回作成するLambdaは、DynamoDBのテーブルを日付を指定してデータ抽出を行い、その結果をS3上に格納する仕組みになっています。(このLambdaをベースに、日次バッチで日ごとや月ごとにデータ抽出可能です)

まずは、今回作成するLambda用にIAMロールを作成します。IAMロールは前回作成した miso_20181207_roleというロールをベースに、ポリシー (AmazonS3FullAccess)を追加して新しく miso_20190201_role を作成しました。

前回同様に、Lambdaを作成します。今回は miso_20190201_lambda という名前で作成しました。

Lambda自体のコードは以下の通りです。今回のLambdaでは、node_moduleの moment.js を追加していますのでご注意ください。

コードは以下の通り。

// モジュール
const aws = require('aws-sdk');                       // AWS SDKモジュール
aws.config.update({ region: 'ap-northeast-1' });      // AWS SDKのリージョン設定
const moment = require('moment');                     // moment.js ライブラリ
const fs = require('fs');                             // fs.js ライブラリ

const docClient = new aws.DynamoDB.DocumentClient();  // AWS DynamoDB オブジェクト

const s3 = new aws.S3();                              // AWS S3 オブジェクト
const dynamoTable = "miso_20190201_dynamodb";         // DynamoDBのテーブル名
const s3Buket = "tdi5409-backet/miso/quicksight-ds";  // S3バケット名

/**
 * エントリーポイント
 * @param {any} event:エベント-CloudWatchのイベント形式
 * @param {any} context:コンテキスト
 * @param {any} callback:コールバック関数
 */
exports.handler = (event, context, callback) => {

  // 取得した情報を表示
  console.log("100 [miso_dynamo2s3] start");
  console.log("101 [miso_dynamo2s3]  request : " + JSON.stringify(event));
  console.log("102 [miso_dynamo2s3]  context : " + JSON.stringify(context));

  // 抽出対象日付の設定(momentオブジェクト)
  // 今回はLambdaの環境変数で抽出対象日を設定しています
  var env_proc_date = process.env["PROC_DATE"];
  if (typeof env_proc_date === 'undefined' || env_proc_date == '') {
    // 日次バッチなどでは前日を初期値として設定することも可能
    env_proc_date = moment().add(-1, "days").format("YYYY-MM-DD");
    console.log("env_proc_date is nothing! - override env_proc_date : [" + env_proc_date + "]");
  }
  else {
    console.log("env_proc_date : [" + env_proc_date + "]");
  }

  // 処理日のmomentオブジェクトを作成し、DynamoDBの日次データをS3に保存
  var mProcDate = new moment(env_proc_date, "YYYY-MM-DD");
  extructDailyData(mProcDate);

  // レスポンス
  const response = {
    statusCode: 200,
    body: JSON.stringify('Hello from Lambda!'),
  };
  callback(null, response);
};

/**
 * DynamoDBから日次データを取得しS3に配置
 * @param {any} mDay : データ出力日の momentオブジェクト
 */
var extructDailyData = function (mProcDate) {

  // DynamoDBのクエリパラメータ
  //  → 抽出日以降のデータを出力条件に変換
  // 日次バッチなどでは前日を対象(=)とすれば日ごとのデータを抽出可能
  var params = {
    TableName: dynamoTable,
    IndexName: "sensor_date-sensor_time-index",
    KeyConditionExpression: "#date >= :date and #time >= :time",  // 今回は 対象日以降(>=)を対象
    ExpressionAttributeNames: {
      "#date": "sensor_date",
      "#time": "sensor_time"
    },
    ExpressionAttributeValues: {
      ":date": mProcDate.format("YYYY-MM-DD"),    // 指定された日付
      ":time": "00:00:00"                         // 時間は 00:00:00 以降を対象
    }
  };

  // DynamoDBへクエリ実行、戻りをS3に格納
  docClient.query(params).promise().then(data => {
    let datagArray = [];
    data.Items.forEach(element => {
      datagArray.push(JSON.stringify(element));
    });
    // データなしなら抜ける
    if (datagArray.length == 0) {
      return new Promise(function (resolve, reject) {
        console.log("data in dynamodb is nothing! - [" + mProcDate.format("YYYY-MM-DD") + "]");
      });
    }
    else {
      // データあり
      console.log("dynamodb data count [" + datagArray.length + "] - [" + mProcDate.format("YYYY-MM-DD") + "]");
      return new Promise(function (resolve, reject) {
        //Lambdaでは一時的なファイルの出力先に/tmpが使える
        fs.writeFile("/tmp/tmp.txt", datagArray.join("\n"), function (err) {
          if (err) reject(err);
          else resolve(data);
        });
      });
    }
  }).then(() => {
    // Promise が成功した場合、S3に書き込み
    let s3Params = {
      Bucket: s3Buket,                                          // S3の格納先パス、Bucket以降のパス
      Key: `${dynamoTable}_${mProcDate.format("YYYY-MM-DD")}.json`,  // 出力ファイル名
      ContentType: "application/json; charset=utf-8",           // コンテキスト
      Body: fs.readFileSync("/tmp/tmp.txt", "utf-8")            // 出力内容(utf-8)
    };
    return s3.putObject(s3Params).promise();
  }).then(data => {
    // resolve(成功時)
    console.log(data);
  }).catch(err => {
    // reject(失敗時)
    console.log(err);
  }); 
};

上記のコードでは、日次バッチ用に日付を指定して抽出が可能になっておりますが、今回はDynamoDBをSCANして全件取得しました。実行すると、以下のようにS3にファイルが生成されていることを確認しています。

5. データ可視化(QuickSight)

次に、データの可視化を行います。上述の通りQuickSightにサインアップした状態から始めます。QuickSightにサインアップをするには、AWSコンソールにログインした後に、サービス一覧より「QuickSight」を選択してください。利用までの流れの通りユーザー設定などが問題なくできていれば、QuickSightにサインアップされ以下のような画面が表示されます。

サインアップしたら左上にある「New analysis」を押下します。

データセットの選択画面になるので、左上の「New data set」を押下します。

データセットの作成画面で「Upload a file」を押下すると、ファイル選択ダイヤログが表示されるので、先ほどS3上に作成したDynamoDBのデータファイルをローカルにダウンロードして置き、そのファイルを選択してください。(他にもデータセットを作成する方法はありますが、今回は一番シンプルにローカルのデータファイルをアップロード方法をとっています)また、今回使用したファイルは、以下のような情報が設定されております。

No 列名 内容 サンプル
1 id センサーID 任意の文字列
2 unit センサーの値の単位 cm
3 unixtime UNIX時間のセンサー日時 1543464191
4 sensor_date センサー日付 yyyy-mm-dd
5 sensor_time センサー時間 hh:mi:ss
6 battery バッテリー電圧 2.97
7 value センサーの値 200.0
8 rssi センサー信号の強度 -65
9 datatype センサーの種別 種別を表す文字列
10 dataid データを識別するid GUID
11 packetid センサー内の連番 1

アップロードしたデータの確認画面が表示されるので、「Edit settings and prepare data」を押下してください。

アップロードしたファイルの列名やデータ型が表示されているので、内容を確認します。なお、画面左にあるDataSourceのFieldsにある「Add calculated field」を押下することで「計算列」を追加することも可能です。今回は、追加列などは使わず、内容を確認しているだけです。

最後に、画面上部にあるデータセット名を「miso_20190201_dynamodb_2018-11-01」→「miso_20190201_dataset」に変更し、右隣りの「Save & Visualization」を押下してデータセットを作成します。

次に下図のように、レポート作成画面が表示されます。基本的な使い方は、左側のナビゲーションの「Fields list」にある列からメジャーを選択して、右側の四角い枠に入れ、「Visual types」から表示するグラフの種類を選択して可視化をしていきます。

今回は、センサーから物体までの距離の時間経過による推移をグラフにすることで、センサーの種別(id)毎に、時系列(sensor_date)毎に値(value)の平均の推移を可視化しようと思います。

まず、「Fields list」からsensor_date.sを選択すると、下の図のようなグラフが自動的に生成されます。グラフ上部に「Field wells」のX軸(X axis)に選択した sensor_date.s が設定されることを確認してください。

次に、同じく「Fields list」から value.n を選択すると、今度は下の図のように日ごとのvalueのカウントされた状態のグラフが生成されます。グラフ上部にある「Field wells」のValuesに選択した value.n(Sum)が設定されることを確認してください。

次に、今回取り込んだデータから不要なデータを除外するためのフィルターを設定します。

画面左の「Filter」ボタンを押下すると以下のような画面になります。「Applied filters」にリンクで「Create one…」と表示され、クリックするとフィールドの一覧が表示されるので、一覧から id.s を選択してください。

すると、id.s 列に対するフィルター設定が可能になります。今回は「Filter type」を「Filter list」にし、アップロードしたidのデータのうち、末尾が03、04のデータのみを表示対象とするフィルター設定をしました。

準備ができたら、「Apply」ボタンで確定します。

フィルターが適用され以下のようなグラフが表示されました。(若干、グラフの形状が変わっていることが確認できます)

ここまでで、フィルターを使用して必要なデータのみに絞って可視化させることができました。しかし、この時点ではまだ value がカウントになってしまっているため、次に日ごとの平均の推移になるようvalueの集計方法を変更します。

まず、グラフ上部にある field wells をクリックすると、現在グラフで設定された軸が確認できます。

次に value を選択するとプルダウンリストが表示されるので、その中の Aggregate「SUM」を「Average」に変更します。

すると、日ごとのvalueの平均が表示されるようになります。(なお、グラフを上にマウスオーバーすることで内容の確認も可能です)

ただし、今回対象とした末尾が03、04のセンサーデータの平均となってしまうため、各センサーデータごとの平均値が確認できるようにします。

左の「Fields list」よりid.sを選択します、するとグラフ上部の「Field wells」の Colorに id.sが設定され、各センサーごとの平均が確認できるようになります。

これで、センサーの種別(id)毎に、時系列(sensor_date)毎に値(value)の平均の推移をみていくグラフが作成されました。基本的にはGUIベースで列をドラック&ドロップすることで簡単にデータの可視化ができるようになっており、比較的ライトにデータの中から気づきを得ることが可能かと思います。

また、上記のグラフでは時系列や値の書式などが英語表記になってしまいましたが、各フィールドから書式の変更が可能で、外部展開用のレポートなども作成ができるかと思います。

最後に以下の図は、上記の内容やタイトルも編集した最終形のグラフのイメージです。書式やタイトルをローカライズすることで、報告用のレポートにも適用することは十分可能ですので、まずは手を動かして色々な記述方法やレポートを作成してみるのもよいかと思います。

まとめ

前回、今回でセンサーデータを用いてデータの取得、格納、加工、可視化という一連の流れがAWS上で比較的簡単に、短時間にできることが確認できました。データの可視化に関して、今回はQuickSightで素早く可視化することに重点を置いたため、S3上のファイルをいったんダウンロードしてQuickSightに読み込みませましたが、システムとして検討するのであれば、S3上のファイル群を直接QuickSightに読み込ませる方法(別途、S3マニフェストファイルが必要)がよいかと思います。

また、QuickSightではS3上のファイルだけでなく、それ以外にも

  • Amazon RedShiftクラスタ
  • データベースインスタンス
    MySQL(5.1以上)、PostgreSQL(9.3.1以上)、MariaDB(10.0以上)、Aurora(Amazon RDSのみ)、Microsoft SQL Server(2012以上)
  • テキストファイル
    CSV(*.csv)、TSV(*.tsv)、ログファイル(*.clf、*.elf)などが対応可能
  • SaaSのデータソース
    Salesforce等

といった様々な形式に対応しております。

分析自体もGUIベースで比較的簡単に可視化ができるので、AWSリソースとの親和性を考えると、エンジニアやアナリスト以外のライトユーザーへの結果の共有ツールとしてAWSユーザーにとって大いに期待できるのではないかと思います。AWSのサービスは日々進化しており、今回行った時系列の分析であれば新たにAmazon TimestreamやAmazon Forecastといったサービスも提供され始めてきており、より一層タイムリーな対応が要求されてくることでしょう。

これからのエンジニアはこういったサービスの仕組みを理解し、TPOに合わせてソリューションの提供などを検討する必要が出てくると思いますので、これからも注力しながら日々精進していければと思います。

The post AWSでIoTデータを見える化してみた その2 first appeared on MISO.]]>
AWSでIoTデータを見える化してみた https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/aws-iot-data-visualization Tue, 22 Jan 2019 00:00:41 +0000 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/?p=4327 はじめに  以前、下記記事でIBM Cloudでセンサーデータの収集を紹介しました。 実はクラウド環境としてよく利用されているAWSでもセンサーデータを収集することができます。AWSのサービスを使用したIoT活用の方法と…

The post AWSでIoTデータを見える化してみた first appeared on MISO.]]>
はじめに

 以前、下記記事でIBM Cloudでセンサーデータの収集を紹介しました。

実はクラウド環境としてよく利用されているAWSでもセンサーデータを収集することができます。AWSのサービスを使用したIoT活用の方法と今後の応用について取り上げさせていただきます。

1. 今回のシナリオ

上記記事でもお伝えしたように、IoT観点でのクラウド活用フェーズは以下の通りです。

  • センサーデータの収集
  • データの分析
  • 新しい付加価値(サービス)の創出

上記に加えて、データ活用までの時間をより早く、より簡単に、といったことも大事な要素になってくると思います。今回のAWS上ではどのくらい簡単にデータ取得、格納までができるかを体験してみました。なお、AWSではAWS IoTというサービスでAWS単独でもIoTデータを取り扱うことは可能ですが、AWS IoTでは利用するまでに必要となる各種設定作業(Amazon S3の設定、デバイス(Things)の登録、Ruleエンジンの登録)があるため、今回はそのような設定の必要がないセンサーを用いて、AWS内にセンサーデータを取得、格納までの流れをやっておりますのでご了承ください。

2. 今回構築するシステム構成

IoTデータをAWSにて取得し、保存するまでの流れのイメージは以下の通りです。

Webiot(ウェビオ)のWebAPIからデータをAWSのAPI Gatewayで受け、Lambdaを起動させて内部処理を行いデータを格納するといったシンプルな仕組みとなります。

使用するセンサーについて

今回確認するセンサーは、Webiotのセンサーを使用しました。Webiotセンサーの特徴は大きく以下の4つあります。

  • 設定不要で、すぐに使用可能
    Webiotのセンサーはインターネット接続済みで、届いたその日からセンサーデータの利用が可能のため、IoTハードウェア開発の知識がなくてもすぐに使用可能です。
  • 低コスト、低リスク
    Webiotの費用は月額使用料のみ(500円~)で初期費用は不要。気軽に試して、いつでもやめられます。また、月額利用料にはセンサー代、回線使用料、クラウド使用量、電池代がオールインワンで含まれておりコストが明瞭です。
  • 設置のしやすさ
    Webiotのセンサーは電池駆動のため、電源がない場所にも気軽に設置可能です。通信回線は、場所に合わせてBLE通信(ゲートウェイ要)/LoRaWAN通信(ゲートウェイ不要)/Sigfox通信(ゲートウェイ不要)から選択できます。
  • 利用のしやすさ
    センサーデータの確認や次システムとの連携は、WebiotのWebコンソール上から簡単に行え、Webhook/WebAPIでクラウドサービスとも簡単に接続可能です。

Webiotセンサーに関する情報は<Webiotセンサー>をご覧いただければと思います。

Webiotコンソールへのログイン

Webiotセンサーを利用するには、Webiotコンソール上でアカウントの登録が必要となります。

アカウントはGoogleアカウントで登録することが可能で、ログインすると以下のような画面となります。

センサーデータの注文などもこちらのコンソールから可能となります。(場所にもよりますが、都内であれば午前中に申し込みすれば当日発送も可能で、最短で翌日には利用開始できるようです)今回は超音波距離センサーと通信用のゲートウェイを利用しました。両方利用しても月1,500円とリーズナブルな価格設定です。

データ連携の概要

Webiotのセンサーデータは、Webiotのデータセンターに蓄積される仕組みになっており、自システムに取り込む際にはWebiotが用意しているWebhook/WebAPIを利用して、センサーからデータが送信されるタイミングでWebhook/WebAPIで自システムに取り込むことが可能です。
データ連携の詳細については、Webiotコンソール上のドキュメント<Webiotデータのデータ連携について>にもやり方が載っておりますので合わせてご覧いただければと思います。

今回使用するセンサーの説明と用途

注文したセンサーは、Webiotコンソール上の「センサー」タブにて確認できます。今回は超音波距離センサーを使用してみました。以下は、実際のセンサーとゲートウェイの画像です。(左がセンサーで右がゲートウェイ)

超音波距離センサーは、センサーから物体までの距離を定期的(20秒間隔)に測定できます。活用例などは駐車場の車両有無検知やごみ箱の満空検知、箱物の在庫量の把握など工夫次第で様々な利用が可能になりそうです。また、Webiotのセンサーには今回の超音波距離センサー以外に、温度・湿度・気圧や加速度、人感、CO2といった様々なセンサーが用意されているようです。

詳しくは、Webiotのサイト<Webiotセンサーの種類>に詳細がありますのでご覧いただければと思います。

3. センサーデータの取得

ロールの作成

AWS上ではAPI Gatewayでデータを受けてLambdaを起動させるため、まずはLambda用のロールを作成する必要があり、今回作成するLambda用のロールには以下のポリシーを割り当てておきます。

  • AWSLambdaBasicExecutionRole

AWSコンソールにて「サービス」→「IAM」を指定、IAM画面の左のメニューより「ロール」を選択→「ロールの作成」ボタンを押下。ロール作成の画面で、信頼されたエンティティの種類を「AWS サービス」、このロールを使用するサービスを選択で「Lambda」を選択して、「次のステップ:アクセス権限」を押下。

ポリシーのフィルターに「AWSLambdaBasic」を入力し、一覧に表示された「AWSLambdaBasicExecutionRole」にチェックして、「次のステップ:タグ」を押下。

タグの設定は、何もせず「次のステップ:確認」ボタンを押下。最後にロール名を指定する画面があるので、必要な情報を入力して「ロールの作成」ボタンを押下(今回はmiso_20181207_roleというロールを作成)

Lambdaの作成

次に、API Gatewayから起動されるLambdaの作成を行います。
AWSコンソールにて「サービス」→「Lambda」を指定、Lambda画面の右にある「関数の作成」を選択。

次に、名称や説明の入力し、ロールは「既存のロール」指定し、上記で作成したロール(ここではmiso_20181207_role)を指定して「関数の作成」ボタンを押下。

作成したLambda関数のコード入力画面が表示されるので、以下のコードを貼り付けてください。(今回作成したLambdaのランタイムはnode.js 6.10としました)

データ取得用のソースコード

// モジュール
var aws = require('aws-sdk'); // AWS SDKモジュール
var momentTz = require('moment-timezone'); // moment-timezone.js用ライブラリ

/**
* リクエストが送られてきたときのハンドラー
*/
exports.handler = function(req, context, callback) {

    console.log("401 start.");
    console.log("402 request : " + JSON.stringify(req));
    console.log("403 context : " + JSON.stringify(context));
    console.log("404 request-headers : " + req.headers);
    console.log("405 request-body : " + req.body);

    // リクエストのbodyをJSONにする
    var sensor = JSON.parse(req.body);
    if (sensor != null) {
         // センサーデータの内容を表示
         console.log("406 sensor - id : " + sensor["id"]); 
         console.log("406 sensor - value : " + sensor["value"]); 
         console.log("406 sensor - unit : " + sensor["unit"]); 
         console.log("406 sensor - value01 : " + sensor["value01"]); 
         console.log("406 sensor - value02 : " + sensor["value02"]); 
         console.log("406 sensor - value03 : " + sensor["value03"]); 
         console.log("406 sensor - datatype : " + sensor["datatype"]); 
         console.log("406 sensor - unixtime : " + sensor["unixtime"]); 
         console.log("406 sensor - dataid : " + sensor["dataid"]); 
         console.log("406 sensor - packetid : " + sensor["packetid"]); 
         console.log("406 sensor - rssi : " + sensor["rssi"]); 
         console.log("406 sensor - battery : " + sensor["battery"]);

         // データ項目のunixtimeをローカルタイム(JST)に変換
         console.log("406 moment-tomezone.js unixtime → JST : " + momentTz.unix(sensor["unixtime"], 'X').tz("Asia/Tokyo").format("YYYY-MM-DD HH:mm:ss"));

         }
         else {
                 // リクエストのBodyがない場合はエラー
                 console.log("999 error! request body is null !!! ");
                 console.error("999 request body is null !")
         }

         // レスポンス&コールバック
         const response = {
         statusCode: 200,
                 headers: {
                         "Access-Control-Allow-Origin" : "https://s3-ap-northeast-1.amazonaws.com",
                         "Access-Control-Allow-Credentials" : true, // Required for cookies, authorization headers with HTTPS 
                         "Access-Control-Allow-Headers" : "Origin, X-Requested-With, Content-Type, Accept"
                 },
                 body: JSON.stringify({ "message": "lamdba function is callback!" })
         };
         callback(null, response);
         console.log("499 end!");
};

貼り付け後のイメージ。最後に「保存」ボタンを押下して完了です。なお、Webiotのセンサーデータの時刻データがUnixtimeとなり、変換するためmode_moduleのフォルダ内にnode.jsよりモジュールのmoment_timezoneを利用していますが、ここでは詳細は割愛します。(node_moduleの利用方法などはググるといっぱいあるので試してみてください)

API Gatewayの作成

次に、センサーデータの受け口となるAPI Gatewayの作成を行います。
AWSコンソールにて「サービス」→「API Gateway」を指定、API Gatewayの画面にある「+APIの作成」を選択。「新しいAPIの作成」画面にてAPI名、説明を入力し、「APIの作成」ボタンを押下。(ここではAPI Gatewayとして、miso_20181207_apiを作成してます)

  

次に、APIのリソースの「アクション」より「メソッドの作成」、一覧より「POST」を選択し、確定ボタン(レ)を押下。POSTメソッドの統合ポイントの設定が表示されるので、以下の画像のように入力を行い、項目「Lambda関数」には上記で作成したLambda関数(miso_20181207_lambda)を指定して、「保存」ボタンを押下。(API GatewayとLambdaの紐づけの確認メッセージが表示されますがそのまま「OK」を押下)

APIの作成すると以下のような画面となります。

次に、ステージを作成し、作成したAPIのデプロイを行います。
AWSコンソールのAPI Gatewayの画面の左メニューより「リソース」、「アクション」プルダウンより「APIのデプロイ」を選択。

APIのデプロイダイヤログが表示されるので、必要項目の入力を行い「デプロイ」ボタンを押下。(この例では、ステージdemoを作成し、APIをdemoステージにデプロイ)

デプロイが完了すると画面にAPI用のURL呼び出しが表示されるので、URLを記録しておいてください。

Webiotコンソール上での作業

データ連携の設定は、Webiotコンソール上で行います。最初に取得したいデータをJSON形式で指定します。取得可能な情報は<Webiotコンソールのドキュメント>でも記載されていますが、必要な情報のみに絞ることも可能です。今回はすべての情報を取得してみました。

まずは、Webiotコンソールの左メニューより「データ連携」を選択し、「ルールの追加」を押下。タイプの選択ダイヤログにて、「HTTP POST」を選択。

次に、対象となるチーム(Webiot内の契約名称)を選択し、データ連携する対象のセンサーにチェックを入れて、「次へ」ボタンを押下。(この例では、超音波距離センサーのBUxxxx(xxxxはモザイク処理)を選択)

次に、データの送信先設定となります。送信先URLには、上記で取得したAPI GatewayのURLを設定。BODYにはJSON形式でWebiotセンサーデータの全項目を指定し、「次へ」ボタンを押下。

センサーデータの全項目を送信するBODYの設定の参考は以下の通り。

{
    "id":"{{id}}",
    "value":"{{value}}",
    "unit":"{{unit}}",
    "value01":"{{value01}}",
    "value02":"{{value02}}",
    "value03":"{{value03}}",
    "datatype":"{{datatype}}",
    "unixtime":"{{unixtime}}",
    "dataid":"{{dataid}}",
    "packetid":"{{packetid}}",
    "rssi":"{{rssi}}",
    "battery":"{{battery}}"
}

設定内容の確認画面にて、「テスト」を押下して正常に通信できることを確認したら、「設定する」ボタンを押下。

元の一覧上に、データ連携用のルールが追加されたことを確認。

以上でAWS側、Webiot側の設定が完了しました。また、今回は実施していませんがAWSのAPI GatewayでAPIキーを作成しておき、Webiotコンソール上で設定させる事でよりセキュアに連携を行うことも可能です。

4. 取得したデータの確認

CloudWatch上でデータ確認

あとは、注文していたWebiotのデートウェイの電源を入れます。(電源を入れた後、初期化に30秒程度待つとゲートウェイのランプが緑に点滅して通信が開始されていました)データは、Webiotコンソール上でも確認できます。
Webiotコンソールの左メニューにて「センサー」を選択。データ連携をした該当のセンサーを選択し、右にあるデータログのアイコンを押下。

しばらくすると定期的にデータが追加されていることが確認できました。

続いて、AWSのCloud WatchログでLambdaの結果を確認します。
AWSコンソールにて「Cloud Watch」を選択。Cloud Watchの画面の左メニューのログを選択し、フィルタに「/aws/lambda/(作成したLambda関数名)」を入れ、上記で作成したLambda関数を選択。

ログストリームを選択すると、API Gatewayに紐づけられたLambda関数(miso_20181207_lambda)内でconsole.log(“xxxxx”)とした部分が、Cloud Watchのログ上に出力され、AWS側でもデータが取得できていることが確認できました。

5.センサーデータの格納

上記でセンサーデータの取得ができたので、一旦、センサーのゲートウェイの電源をOFFにし、次にAWSのDynamoDBにセンサーデータを格納してみようと思います。

DynamoDBのテーブル作成

AWSコンソールにてDynamoDBを指定し、コンソール上の「テーブル作成」ボタンを押下し、新規テーブルを作成を行います。今回は、miso_20181207_dynamodbというテーブルを作成、プライマリーインデックス、セカンダリーインデックスはそれぞれid(文字列)、unixtime(数値)を指定し、他はデフォルトのまま画面下の「作成」ボタンを押下

しばらくするとテーブルが作成されているので、まだデータが登録されていないことを確認します。

Lambda用ロールの編集(追加)

次に、LambdaからDynamoDBを操作するため、上記で作成したIAMのロールに以下の権限を追加します。

  • AWSLambdaDynamoDBExecutionRole
  • AmazonDynamoDBFullAccess

AWSコンソール上のIAMの画面にて、作成したロールmiso_20181207_roleを選択し、上記のポリシーを追加。

ロールの詳細画面に戻り、追加したポリシーがアタッチされていることを確認。

Lambdaの編集

上記で作成したLambdaの関数index.jsを以下のように変更します。変更内容は、Cloud Watchのログを出力した後に、LambdaからDynamoDBのputでデータを書き込みに行く処理を追加しています。

AWSコンソールよりLambdaを指定し、上記で作成したLambda関数を指定し、以下のソースを貼り付け。

編集後のソース(index.js)

// モジュール
var aws = require('aws-sdk'); // AWS SDKモジュール
var momentTz = require('moment-timezone'); // moment-timezone.js用ライブラリ
var docClient = new aws.DynamoDB.DocumentClient({region: 'ap-northeast-1'});

/**
* リクエストが送られてきたときのハンドラー
*/
exports.handler = function(req, context, callback) {

    console.log("401 start.");
    console.log("402 request : " + JSON.stringify(req));
    console.log("403 context : " + JSON.stringify(context));
    console.log("404 request-headers : " + req.headers);
    console.log("405 request-body : " + req.body);

    // リクエストのbodyをJSONにする
    var sensor = JSON.parse(req.body);
    if (sensor != null) {
        // センサーデータの内容を表示
        console.log("406 sensor - id : " + sensor["id"]); 
        console.log("406 sensor - value : " + sensor["value"]); 
        console.log("406 sensor - unit : " + sensor["unit"]); 
        console.log("406 sensor - value01 : " + sensor["value01"]); 
        console.log("406 sensor - value02 : " + sensor["value02"]); 
        console.log("406 sensor - value03 : " + sensor["value03"]); 
        console.log("406 sensor - datatype : " + sensor["datatype"]); 
        console.log("406 sensor - unixtime : " + sensor["unixtime"]); 
        console.log("406 sensor - dataid : " + sensor["dataid"]); 
        console.log("406 sensor - packetid : " + sensor["packetid"]); 
        console.log("406 sensor - rssi : " + sensor["rssi"]); 
        console.log("406 sensor - battery : " + sensor["battery"]); 

        // データ項目のunixtimeをローカルタイム(JST)に変換
        console.log("406 moment-tomezone.js unixtime → JST : " + momentTz.unix(sensor["unixtime"], 'X').tz("Asia/Tokyo").format("YYYY-MM-DD HH:mm:ss"));

        // DynamoDBに登録
        registWebiotSensor(sensor);

    }
    else {
        // リクエストのBodyがない場合はエラー
        console.log("999 error! request body is null !!! ");
        console.error("999 request body is null !")
    }

    // レスポンス&コールバック
    const response = {
    statusCode: 200,
    headers: {
        "Access-Control-Allow-Origin" : "https://s3-ap-northeast-1.amazonaws.com",
        "Access-Control-Allow-Credentials" : true, // Required for cookies, authorization headers with HTTPS 
        "Access-Control-Allow-Headers" : "Origin, X-Requested-With, Content-Type, Accept"
        },
        body: JSON.stringify({ "message": "lamdba function is callback!" })
    };
    callback(null, response);
    console.log("499 end!");
};

/**
* Webiotセンサー情報の登録
*/
var registWebiotSensor = function(sensor) {
    console.log("501 [regist] start !");

    // 登録するセンサー情報
    var params = {
        TableName : "miso_20181207_dynamodb",
        Item : {
            "id" : sensor["id"],
            "value" : Number(sensor["value"]),
            "unit" : sensor["unit"],
            "datatype" : sensor["datatype"],
            "unixtime" : Number(sensor["unixtime"]),
            "sensortime" : momentTz.unix(sensor['unixtime'], 'X').tz('Asia/Tokyo').format('YYYY-MM-DD HH:mm:ss'),
            "dataid" : sensor["dataid"],
            "packetid" : sensor["packetid"],
            "rssi" : Number(sensor["rssi"]),
            "battery" : Number(sensor["battery"])
        }
    };

    // 値が空文字の補完(DynamoDBが空文字を受け付けないための補完処理)
    for (var key in params.Item) { 
        for (var items in params.Item[key]) { 
            var value = params.Item[key][items]; 
            if (value === undefined || value === "") { 
                value = "N/A"; 
            }
        } 
    }

    // センサー情報を登録
    docClient.put(params, function(err, data) {
        if (err) {
            console.log("503 [regist] insert webiot_sensor faild. Error : ", JSON.stringify(err));
        } else {
            console.log("504 [regist] insert webiot_sensor success.", data);
        }
    });
    console.log("599 [regist] end ! ");
};

画面右上の「保存」ボタンを押下し、正常に保存できることを確認。

以上で、DynamoDBにデータを格納する準備が整いました。

DynamoDB上でデータ確認

再度、ゲートウェイの電源をONにしてみると、問題なくDynamoDBにデータが登録されるようになり、IoTデータの見える化ができました。

(項目sensortimeは、Lambda内でunixtimeをローカル時間(JST)に変換した値を登録)

まとめ

今回、Webiotセンサーを用いてセンサーデータの取得、格納までを試み、AWSでも比較的簡単に、短時間でデータ取得、格納できることが確認できました。今回は超音波距離センサーで物体との距離のセンサーを利用しましたが、上述の通りWebiotのセンサーは他にも種類があるので、アイデア次第でいろいろなことに活用できそうです。また、AWS上のDynamoDBへの格納ができ、時系列データを蓄積させることで、市販のBIツールやAWSのQuickSightなどを用いた可視化、SageMakerなどの機械学習による予測など、様々なデータ活用が可能なことが改めて確認できました。IoTデータの可視化は決して新しいことではないですが、今あるサービスを組み合わせることで、よりスピーディにタイムリーなデータを提供できるような仕組みを理解しておくことは我々には重要です。そんな時に今回の記事が情報の入り口として皆さんの理解の一助となれば幸いです。

The post AWSでIoTデータを見える化してみた first appeared on MISO.]]>