ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

SORACOM API を Sandbox 環境でテストする

皆様こんにちは、ユニファの赤沼です。

弊社では「ルクミー午睡チェック」というプロダクトで SORACOM Air Sim を使用しています。通常はセルラーモデルの iPad mini での通信用途に使っていて、API の活用はまだまだ十分にはできていないのですが、最近 Ruby のスクリプトから API を利用する機会があり、Sandbox 環境でのテストも行ったので書いてみます。

SORACOM のサービスは API が充実

まず改めてのおさらいですが、 SORACOM のサービスは IoT 用途ということもあり、 API が充実しています。サービスの数もかなり増えており、コンソールも使いやすいのですが、API でも大半のことはできるようになっているのではないでしょうか。ドキュメントもしっかり提供されています。

dev.soracom.io

全ての API についてリファレンスも提供されています。リクエスト時のパラメータの内容やレスポンスの各項目についてはもう少し説明があると良いなとは思いますが、実際に認証と各APIの実行までリファレンスページから行うことができるようになっているので、実装に組み込む前に実際のレスポンスなどが確認しやすくなっています。

dev.soracom.io

テスト用の Sandbox 環境

実際に動かして試せる API Reference はとても便利なのですが、一つ気を付けないといけないのは、 API Reference のページから実行した内容は、実際の環境に対して実行されるということです。情報取得系の API であれば問題ないのですが、何か変更を加えたり、Sim の解約等まで行えてしまいますので、軽い気持ちで試したりするとあとで大変なことになります。また、実装に組み込む際にも開発段階ではテストが必要になると思いますが、そういう時のために SORACOM には Sandbox 環境が用意されています。Sandbox 環境で実行した内容は本番環境には影響しませんので、解約処理等の危険な処理も試すことができます。

dev.soracom.io

本番アカウントも必要

Sandbox環境は本番環境とは別環境ではありますが、本番環境にもアカウントを持っている必要はあります。そのため本番環境のコンソールから SAM(SORACOM Access Management)ユーザを作って認証キーを生成しておく必要があります。SAMユーザがいて認証キーさえあれば良いので、権限は何も与えなくてもOKです。間違って本番環境に対して実行してしまっても影響がないように、テスト用に何も権限のない SAMユーザを作っておくのが良いかと思います。

Sandbox 環境での準備: オペレータ作成

Sandbox環境にはオペレータは用意されておらず、またGUIもないので、まずはオペレータを下記APIで作成する必要があります。

sandboxInitializeOperator API https://dev.soracom.io/jp/docs/api_sandbox/#!/Operator/sandboxInitializeOperator

Sandbox専用の API もリファレンスページから実行できますが、今回は Ruby で下記のようなコードを書いて実行しました。 init メソッドの中で実際に API にリクエストを投げています。ちなみに使用するメールアドレスは過去に使ったものと重複するとエラーになりますので、作成時には新たなメールアドレスを使用する必要があります。これを実行すると Operator ID, API Key, API Token が取得できますので、以降の API 実行にはこれらを使用します。

#! /usr/bin/env ruby
require 'httpclient'
require 'json'
require './errors'

class SoracomSandbox
  attr_reader :api_base_url

  def initialize
    @api_base_url = 'https://api-sandbox.soracom.io/v1'
    @client = HTTPClient.new
  end

  def post(url:, body: {} , header: {})
    res = @client.post(url, body: body, header: header)
    check_status(res)

    JSON.parse(res.body)
  end

  def check_status(res)
    return if HTTP::Status::successful? res.status

    res_json = JSON.parse(res.body)
    msg = "CODE: #{res_json['code']} MSG: #{res_json['message']}"
    raise SoracomApiError, msg
  end

  def init(email: nil, password: nil, auth_key_id: nil, auth_key: nil)
    request_body = {
      email:                 email,
      password:              password,
      authKeyId:             auth_key_id,
      authKey:               auth_key,
      registerPaymentMethod: 'true'
    }.to_json
    res_json = post(url: "#{@api_base_url}/sandbox/init", body: request_body,  header: { 'Content-Type' => 'application/json' })

    @operator_id = res_json['operatorId']
    @api_key     = res_json['apiKey']
    @token       = res_json['token']

    @request_header = {
      'Content-Type'      => 'application/json',
      'X-Soracom-API-Key' => @api_key,
      'X-Soracom-Token'   => @token
    }

    return @operator_id, @api_key, @token
  end
end

Sandbox環境での準備: Simデータ作成

続いてテストに使用する Sim のデータを作成します。実際に Sim を契約しなくてもいくらでも Sim を用意できるのが Sandbox 環境の良いところです。 Simデータを用意するには、架空のSimを作成してからそれを登録するという手順を踏みます。架空のSim作成には Sandbox 用の API が用意されています。

Sim作成: sandboxCreateSubscriber https://dev.soracom.io/jp/docs/api_sandbox/#!/Subscriber/sandboxCreateSubscriber

Simの登録は本番環境と同様の API で行います。

Sim登録: registerSubscriber https://dev.soracom.io/jp/docs/api/#!/Subscriber/registerSubscriber

先ほどのクラスに下記のようなメソッドを追加して実行します。

  def create_sims(sim_count)
    sims = []
    sim_count.times do
      res = @client.post("#{@api_base_url}/sandbox/subscribers/create", header: @request_header)
      res_json = JSON.parse(res.body)
      sims << {
        imsi: res_json['imsi'],
        msisdn: res_json['msisdn'],
        registration_secret: res_json['registrationSecret']
      }
    end

    sims.each do |sim|
      request_body = { registrationSecret: sim.fetch(:registration_secret) }.to_json
      post(url: "#{@api_base_url}/subscribers/#{sim.fetch(:imsi)}/register", body: request_body, header: @request_header)
    end
  end

実際にはさらにオペレータ作成とSimデータの作成をラップするコードを用意して、それを毎回 Sandbox 利用時に実行しました。

#! /usr/bin/env ruby
require 'optparse'
require './soracom_sandbox'

opt = OptionParser.new

params = {}

opt.on('-e EMAIL')              {|v| params[:email]       = v }
opt.on('-i AUTH_KEY_ID')        {|v| params[:auth_key_id] = v }
opt.on('-k AUTH_KEY')           {|v| params[:auth_key]    = v }
opt.on('-n NUM_OF_SUBSCRIBERS') {|v| params[:num]         = v.to_i }

opt.parse!(ARGV)

sandbox = SoracomSandbox.new
operator_id, api_key, token = sandbox.init(
  email:       params[:email],
  password:    'superStrongP@ssw0rd',
  auth_key_id: params[:auth_key_id],
  auth_key:    params[:auth_key]
)

sandbox.create_sims(params[:num])

puts "#{operator_id},#{api_key},#{token}"

テストしたい API を実行する

ここまでで Sandbox 環境の準備ができましたので、あとは本番と同様の API を Sandbox 環境向けに実行すればテストをすることができます。 例えば Sim 情報の取得は下記のようなメソッドを書いて実行します。

  def subscribers(status: [], limit: 100)
    parameter = "limit=#{limit}"
    if not status.empty?
      parameter += "&"
      parameter += URI.encode_www_form(status_filter: "#{status.join('|')}")
    end

    get(url: "#{@api_base_url}/subscribers?#{parameter}", header: @request_header)
  end

これで事前準備で登録された Sim の情報が分かりますので、 Activate なども実行してみることができます。

  def activate_subscribers(imsis: [])
    subscribers = []
    imsis.each do |imsi|
      url = "#{@api_base_url}/subscribers/#{imsi}/activate"
      subscribers << post(url: url, header: @request_header)
    end

    subscribers
  end

こちらもラップするコードを書きました。 API_KEY と TOKEN はオペレータ作成時に取得したものを使います。

#! /usr/bin/env ruby
require 'csv'
require 'optparse'
require './soracom'

opt = OptionParser.new

params = {
  api_key:    nil,
  token:      nil,
  file:       nil,
  production: false
}

opt.on('-f SUBSCRIBERS_CSV_FILE') {|v| params[:file] = v }
opt.on('-a API_KEY')   {|v| params[:api_key]    = v }
opt.on('-t TOKEN')     {|v| params[:token]      = v }
opt.on('--production') {|v| params[:production] = true }

opt.parse!(ARGV)

soracom = Soracom.new(production: params[:production])
soracom.set_request_header(api_key: params[:api_key], token: params[:token])

subscribers = CSV.table(params[:file], { converters: nil })
imsis = subscribers[:imsi]

puts soracom.activate_subscribers(imsis: imsis).to_json

activate_subscribers メソッドや subscribers メソッドは前述の SoracomSandbox とは別の、本番と同様の API をまとめたクラスに実装しましたので参考までに抜粋して載せておきます。

#! /usr/bin/env ruby
require 'httpclient'
require 'json'
require 'uri'
require './errors'

API_BASE_URL         = 'https://api.soracom.io/v1'
API_BASE_URL_SANDBOX = 'https://api-sandbox.soracom.io/v1'

class Soracom
  def initialize(
    production:  false
  )
    @production   = production
    @client       = HTTPClient.new
    @api_base_url = production ? API_BASE_URL : API_BASE_URL_SANDBOX
  end

  def production?
    @production
  end

  def set_request_header(api_key:, token:)
    @request_header = {
      'Content-Type'      => 'application/json',
      'X-Soracom-API-Key' => api_key,
      'X-Soracom-Token'   => token
    }
  end

  def post(url:, body: {} , header: {})
    res = @client.post(url, body: body, header: header)
    check_status(res)

    JSON.parse(res.body)
  end

  def get(url:, header:)
    res = @client.get(url, header: header)
    check_status(res)

    JSON.parse(res.body)
  end

  def check_status(res)
    return if HTTP::Status::successful? res.status

    res_json = JSON.parse(res.body)
    msg = "CODE: #{res_json['code']} MSG: #{res_json['message']}"
    raise SoracomApiError, msg
  end

  def subscribers(status: [], limit: 100)
    parameter = "limit=#{limit}"
    if not status.empty?
      parameter += "&"
      parameter += URI.encode_www_form(status_filter: "#{status.join('|')}")
    end

    get(url: "#{@api_base_url}/subscribers?#{parameter}", header: @request_header)
  end

  def activate_subscribers(imsis: [])
    subscribers = []
    imsis.each do |imsi|
      url = "#{@api_base_url}/subscribers/#{imsi}/activate"
      subscribers << post(url: url, header: @request_header)
    end

    subscribers
  end
end

まとめ

SORACOM のサービスの魅力は単に Sim での通信というだけでなく、関連する様々なサービスや API を活用することで、よりコスト効率を良くしたり、プロダクト提供までの実装コストの削減や運用時の柔軟性をあげることができるというところかと思います。 API を活用していくためにも、 Sandbox 環境でしっかりテストして安全に利用したいものです。

また弊社では複数ポジションを募集中ですので、興味ある方は是非ご連絡ください! unifa-e.com

Slackのチャンネル(CH)改善しようぜ!

こんにちは。 ユニファでインフラエンジニアをしているすずきけいたです。

弊社の様なスタートアップ企業だけではなく、 広く使われるようになってきているSlack。 外部のアプリやスケジュールなどの連携もできたり、色々便利ですよね。

ただ自由で便利な半面スペースに存在する人数が400人以上になり、【この話題のCHどこ?】【CH数が多すぎ】【ここ外部協力の方いる?】などと悩む事も多いかと思います。 そこで今回は弊社のSlackの運用ルールやプレフィックス、今後の改善、CH整理に関しても紹介しようと思います。


目次


現在のCH数把握

 弊社ではSlackを2015年1月21日から使用しているようで、(私の入社前の事なので弊社CTO赤沼の履歴を遡った)かなり導入時期としては早いですよね。

 現在パブリックChだけでも700、プライベートCHを加えると1000は越えると思われます。

SlackCHの命名ルール

 当初SlackCHの命名ルールは厳格なものなく、自由に作成していたのですが、 CH数が多くなってきた事や、プレフィックス(下記記事参照)が登場した事で、弊社でも命名ルールを設けました。

 SlackCHはサイドバー階層化(下記記事参照)などを除き基本アルファベット順に並ぶため、【接頭】でCHの性質を分類しています。

◆サイドバー階層化に関する記事◆

internet.watch.impress.co.jp

◆プレフィックスに関する記事◆

internet.watch.impress.co.jp

slack.com

f:id:unifa_tech:20200804093901p:plain  画像の様なプレフィックスを付け加える様にしております。

 コーポレート本部は【#corp】、外部協力者を含むCHは【#ex】、プロジェクトに関係するものは【#pj】、 一時的なCHは【#tmp】、趣味やチャットなど仕事とは関係ない息抜きCHは【#zz】などになっております。

 プレフィックス以外にもCH数は少ないですが以下のようなものを接頭につけております。  UniFaに関連する情報に届けたいものは【#unifa】、infraは【#infra】、写真事業関連は【#photo】など 誰にでもわかりやすいCH命名にするようにしています。

CH命名時の問題点

 プレフィックスなどにも弱点があり、上記の命名ルールに従って複数のプレフィックスに該当する場合、CH名がお経の様に長くなりがちです。

例:【外部協力者を含むプロジェクトの一時的なCH】→【#ex_temp_pj_~】

 あるあるですよね。名前が最後まで見えない問題です。  SlackのCH表示ペインは有限のため、SlackCHが長いとどういった内容の話をしてるかなど、中に入らないと確認できなかったりします。

今後のCH命名改善

 下記表記は会社として決まったことでなく、あくまで色々な会社様のSlack関連記事を読んだ上での私一個人のアイディアです。

・プレフィックスを短くする(2文字統一など)。

   例) #corp → #co、#photo → #ph

・3文字目にCHユーザーのレベル感を数字で表す。

  1. 社員は入らなくてはならない
  2. 社員のみ
  3. 社員及びパートナー
  4. 誰でも入れる
  5. 雑談など

   例) #corp_enployee_must~ → #co1

・4文字目に外部協力者の人が含まれているかを判別する。

 y → いる  n → いない

   例) #ex_corp_everyone_talk → #co4y_talk

 このように決めを細かくし浸透させることで、そのCHはどういうものかというのが短く伝わりやすく、表示ペイン内にも収まりやすい。

 また同時に外部の人間はわかりにくいと言う効果もあり、上場を目指すスタートアップ企業としても、セキュリティ面を考えるエンジニアとしても効果が期待できそうだ。

今後のCH整理について

 現在700以上あるパブリックCHもその多くは発言がなく、参加者もいないものが散見される状態で、今までCHを作成したままで放置になっている【ゾンビCH】を一掃しようと画策しております。

 ここも決めの問題になるのですが【半年間発言がないCH】、【参加者が0名のCH】をAPIを使って定期的に強制アーカイブを行おうと思っております。

 そのアーカイブに関してはまた別の記事で発表できたらいいかなと思います。

弊社SlackCH一部を解説

【#unifa_all_must】

 UniFaに関わる全員に伝えるmustな情報CH

【#pj_廊下を写真でステキ空間にしようの会】

 我が子の写真を定期的に募集、投票、掲示までするCH。率先的に行っていて、とてもUniFaらしい。

【#tmp_check_ozo_202007】

 2020年7月のOZO(勤怠報告システム)を提出したら抜けるCH。みんなの悲鳴が聞こえます。誰が残っているのかわかりやすくていいですよね。

【#zz_times_keita】

 私の心のつぶやきやスケジュールなどリマインドCH。ミーティング15分前!とか刻めて役にたってます。

【#zz_we_love_coffee】

 コーヒー大好きな皆さんが集まるCH。レベルが高いコーヒー談義してます。沼です。

おわりに

小さなノウハウの集積ですが、ゼロから考えるのって面白いですよね。 SlackCHの整理に悩んでいる方の参考になれば幸いです!

入社して3週間が経ちました。プロダクトマネージャーの仕事とは?

f:id:unifa_tech:20200705175128p:plain はじめまして!ブログ初投稿です、プロダクトマネージャーの田嶋と申します。

ユニファへ6/16に入社して、約3週間が経ちました。前職から目まぐるしく環境は変わりましたが、とても刺激的で毎日楽しく仕事をしています!

今回の投稿では、スタートアップ企業のプロダクトマネージャーの仕事ぶりについてご紹介します。
面白そうー!と興味を持っていただけたら、まだまだプロダクトマネージャー採用もやってますので、ぜひよろしくです!笑

続きを読む

BUSINESS PERSON'S GUIDE TO DESIGN DRIVEN MGMT.

デザインチーム所属の三好です。

コロナの影響でほとんど外出することがなくなり、本を買い漁って家に籠っていたらいつの間にかこの生活が心地良くなってきました。

最近はデザイン関連のものの他に、写真集や政治に関する書籍、聖典「ヴェーダーンダ」の指南書などを読んでいます。

半分は個人の趣向ですが、この先ずっと創作に携わるのなら専門外の世界もなるべく取り込んでおけば、最終的にアウトプットする段階でそれは必ず糧となるはずです。(たぶんヴェーダーンダでさえも…なるはず…なればいいな)

そのための膨大な節操のないインプットが今回の引き籠り生活で多少は実行できたと思います。

ビジネスパーソンのための"デザイン経営"ハンドブック

f:id:unifa_tech:20200701171803p:plain

これは特許庁から無料で発行し公開している色んな意味で衝撃的な資料です。

若林恵さんという尖った感性を持つ著名な編集者が携わっていて、それが国から出ているということに驚きますし、ビジュアル同様に内容も素晴らしいのですがなんとなく目を通すにはあまりにも濃密でやや骨が折れる内容ではあるので、ここでは特にたくさんの人の目に触れてもらいたいと感じた2つの章のみ添付します。

これはデザイナーのための資料ではなく、デザインに関わる人に向けた資料です。 全てのビジネスパーソンに関わる内容と言っていいと思います。

普段から悶々と感じてはいても中々自分自身のスキルでは言語化が難しい部分を、高い次元で表現してくれているので原文をそのまま共有させていただきます。

f:id:unifa_tech:20200701172057p:plain

f:id:unifa_tech:20200701172120p:plain

興味のある方はこちらから全ページPDFで無料ダウンロードできます(全16頁) https://www.meti.go.jp/press/2019/03/20200323002/20200323002-1.pdf

考察

この数年間でデザインの多様化は猛スピードで進んでいて、もはやデザイナー自身でさえ一体何がどこまでできればデザイナーと言えるのか、わからなくなっている人もいると思います。少なくとも私はそうですし、個人的な経験の浅さもありますが自分自身がデザイナーと言われることには違和感しかありません。

ただ曲がりなりにもインハウスデザイナーとして仕事に携わっていたら、今世の中に求められているものは論理的思考力であるということには気がつきました。

例えば今とても重要視されているUI(ユーザーインターフェース)デザインですが、それには必ずしっかりとしたガイドラインが存在していて、そのルールに沿って作成していけばなんとなくそれらしいデザインが完成します。

しかしそこに留まってしまうとデザイナーとしての自力はおそらく育まれない。その先へ進むには高い思考力と、また作業時間も大幅に必要となります。

なによりインハウスデザインにそれが求められているか、追求できる環境にあるかという大前提の問題がありますが、デザイナーという種族は同じ場所に立ち止まることが許されず、常に個人としての行き先を見据えて行動しなければならないと考えると、今置かれている状況だけを基準に判断することはとても危険です。

ただこれを実行するのは本当に難しくて、そもそも自分の身に置きかえてみるとなんとなくのデザインを作ることさえ四苦八苦していますが。

ところでインハウスデザインの仕事は多岐に渡ります。

WEB、アプリの他にもバナー、チラシ、パンフレット、広告や名刺、必要であれば写真撮影やイラスト、そしてコードも書きます。 その中で紙媒体、グラフィックデザインと呼ばれる分野にはUIデザインのようなガイドラインは存在していない為、自ら情報を整理して最善の方法を探りながら要素を組み立てていきます。それは考える力、個としてのデザイン力が身につきます。

デジタルファーストの現代ではどうしてもグラフィックデザインの重要性は低く見られがちですが、思考力を引き上げるためにはそれを極端に軽視するべきではないと個人的には感じています。

まとめ

その論理的思考力というのが上の資料で語られているデザイン思考(デザインシンキング)ということになると思います。

6章ではデザイン思考の重要性、それは常にオープンエンドであり続けること、予想外の発見を柔軟に受け入れられる思考回路を持つことなどがこれからは必要であると書かれています。”解のない世界に、勇気を持って飛び込む”という言葉が印象的です。

7章では企業の文化性を表現することの重要性、ビジネス上の意味の発想からの脱却についてなどかなり大胆な話を展開しています。そしてインハウスデザイナーとしての価値や使命についても触れてあります。

デザイン思考という言葉のとおり、それはデザイナーが常に普段から意識して仕事に臨んでいる思考回路のことであって、その道のプロフェッショナルでもあります。

未熟な自分自身のことは棚に上げちゃいますが、弊社のデザインチームには基本的にその思考力が備わっていますので、このチームからデザイン思考がもっと広いフィールドへ浸透していくための足がかりになればいいと思いますし、また直接的な数値が見えにくいなどの様々な理由で過小評価されやすい現状が少しずつでも改善されていくことを願っています。

引用元:特許庁はデザイン経営を推進しています | 経済産業省 特許庁

機械学習による時系列データの分類

こんにちは、研究開発部の浅野です。あるプロダクトで、得られる時系列データが正常か異常かを判定しているところがあります。現在はシンプルなルールベースの手法で判断をしており、その精度は約80%と改善が必要です。今回はこれを向上していきたいと思います。幸い正常例も異常例もデータが比較的多くあるため、機械学習の手法を用いることにします。

使用データ

正常例と異常例を約1,000例ずつ手作業で集めました(データ収集, クレンジング, ラベリングは辛いときもありますが大事なタスクの一つです)。そのうち80%を学習用データ、20%をテスト用データとしました。下記の例を見るとすぐに規則性を見出すのは難しそうですが、異常例の方が全体的に値が高めに出る傾向にはありそうですね。

f:id:unifa_tech:20200629172606j:plain:w450
使用したデータの例。上段:正常、下段:異常

手法

決定木ベースのアンサンブル学習手法で、汎化性能が高く動作も高速なランダムフォレストを使用します(コードはscikit-learnを使えば自明なので特に示していません)。とかくブラックボックス(判断根拠がわかりにくい)と言われがちな機械学習ですが、決定木ベースの手法を用いるとそのあたりはかなり軽減することができます。

データをざっと眺めてみると、最大の値とばらつき具合が判断に大きく寄与しているように感じたので、まずは最大値と標準偏差の値を特徴量として分類器を作成してみます。2つしか特徴量がないので木の深さの最大値は2、決定木の数は3として学習を行いました。

f:id:unifa_tech:20200629152426p:plain:w220:left
その結果、混同行列は左のようになり、precision, recall, f1-scoreとも91%となりました。この2つの特徴量だけでもかなり分類精度が上がっています。ちなみに、混同行列の対角成分の数字が大きいほどデータを正しく分類できているということを意味します。

特徴量を増やす

もっと精度を高めるため、関連しそうな特徴量を追加していきます。具体的には、最小値、平均値、中央値、歪度(skewness)、尖度(kurtosis)といったデータの分布に関わる統計指標です。特徴量が増えるので決定木の深さを最大3,数を10として学習したところ次のような結果になりました。

f:id:unifa_tech:20200629162220p:plain:w220:left
混同行列は左のようになり、異常例の分類精度はprecision: 94%, recall:95%, f1-score: 94%とさらに上昇しました。


f:id:unifa_tech:20200629162304p:plain:w370:right
右の図は各特徴量の重要度です。先ほど使用した最大値と標準偏差に加えて、歪度や平均値なども分類に効いていることがわかります。

f:id:unifa_tech:20200629163904p:plain:w250:left さらに、「最大値ー平均値」や「平均値ー最小値」などのようにより直接的に分布の歪みを捉えることができるような特徴量を追加したり、汎化性能を高めるために不要な特徴量を削除することで混同行列が左のようになり、最終的に異常なデータの検出においてprecision: 94%, recall:97%, f1-score: 96%まで精度を改善することができました。このあたりの特徴量エンジニアリングは機械学習に携わる者として腕の見せ所の一つです。

決定木の可視化

ちなみに、決定木を可視化するためにdtreevizという素晴らしいライブラリがあります。最大値と標準偏差のみで分類を行った際に学習した決定木の一つを可視化した例が下記です。標準偏差が0.09未満かどうかで分類した後に最大値によってさらに分類を行い、それぞれの分岐で正常例や異常例がどんな割合で含まれるかがよく分かります。学習したモデルの妥当性の確認や、推論結果の根拠を説明する際にとても役に立つと思います。

f:id:unifa_tech:20200629165810j:plain:w500
dtreevizを用いた決定木の可視化の例

まとめ

ランダムフォレストを用いて時系列データの分類精度を80%から96%まで高めることができました。実はこの後にXGBoostなどのブースティング手法や、深層学習であるLSTMを用いてさらなる精度改善を図る予定でしたが、ブログの執筆期限がすぎているのでランダムフォレストで十分な精度が達成できたのでこれで終わりにします。難しい問題をいかに簡単に解くかが大事。

Google Home Miniで子供のうんちを記録する

みなさんこんにちは。サーバーサイドエンジニアの柿本です。

前回のエントリーにて、リモートシャッターで子供のうんち記録を残すことに成功しました!

tech.unifa-e.com

しかし、冷静に考えると、

  • おむつを交換した手でリモートシャッターを触るのは不衛生
  • 私が不在の時に妻が node コマンドを打つのはムリ
  • 連絡帳にはうんちの状態(「普/硬/軟/下痢」の4段階)の記入も必要

などなど、実運用に耐えられないことが判明しました。 泣

そこでこれらの問題をまとめて解決するために、 Google Home Mini に記録を残してもらうことにします。

普段は私がカップ麺を作るときのタイマーとして使うのみですが、今こそ家族の役にたってもらうのです!

TL;DR

※音が出ます。音量にお気をつけください。

動画が見れない場合はこちら

目次

構想

IFTTT でつなげることもできますが、会話の柔軟性の点で劣るので、 Actions on Google ( Google Assistant のアプリ開発プラットフォーム)を使うことにします。

親としては子供の「うんちの量」も気になりますので、うんちの「状態」と「量」の両方を記録できるようにします。

Google スプレッドシートへは Google Apps Script で WEB API を公開して追記することにします。ついでにSlackに通知を送ります。

全体の構成

会話の流れはこんな感じでしょうか。

『こんにちは。今日はどんなうんちでしたか?』  
  =>「今日はかためでした。」  

『量はどれくらいですか?』  
  =>「たくさん出ました。」  

『わかりました。かためのうんちがたくさんですね。』

素晴らしい!!これならうんちシャッターの全ての問題が解決されます!

それでは早速作っていきます。

前提

以下はすでに作成済であることを前提とします。
① Google アカウント
② ①に紐づいた Google Home
③ ①に紐づいた Actions on Google のプロジェクト
④ Google スプレッドシート
⑤ Slack のチャンネルと Webhook URL

実装

Actions on Google

Google Assistant アプリを開発する時は、 Actions on Google という開発プラットフォームを利用します。

公式に勝るチュートリアルなし!ということで、以下をざっとこなすと Actions on Google で Dialogflow を使ったアプリ開発の流れが理解できます。

codelabs.developers.google.com

作成した Google Assistant アプリを Google Home とつなげるためにアプリ名を設定する必要がありますので、 Invocation メニューからアプリ名を登録します。

うんちをリポートしてくれるので「うんちリポーター」と命名します。

Invocation設定

Dialogflow

それでは Google Assistant アプリの肝となる Actions を Dialogflow を使って実装していきます。

Entity

Entity とは、つまり変数です。

今回の場合、

  • うんちの状態
  • うんちの量

の2つが Entity になります。

Dialogflow の Entity メニューで、各 Entity が取りうる値とその値を表す単語(複数可)を登録します。

Entity 名をそれぞれ condition amount とし、以下の通りに設定します。

conditionのEntity

amountのEntity

Intent

Intent とは、「ユーザーが○と言ったら×と返す」という会話の1ターンを設定し、ユーザーの発話から情報を抽出します。

最初に「今日はどんなうんちでしたか?」と質問をしたいので、アプリ起動時に呼び出される「Default Welcome Intent」の Responses でその文言を設定します。

Default Welcome IntentのResponses設定

どんなうんちだったか、をユーザーが回答した時に、その情報を抽出するために「poop condition」 intent を追加します。

Training phrases にユーザーが言うであろう言い回しをいくつか登録します。

IntentのTraining Phrases設定

Add user expression に追加をしていくと、事前に Entity で登録した値を検出して自動で condition の値としてハイライトしてくれます。

うんちの量も教えてもらう必要があるので Action and parameters で REQUIRED 項目として amount も登録します。

amountをAction and parametersに設定

うんちの量を質問する時の文言を PROMPTS として登録します。これは複数パターンを設定でき、ランダムに選択されるようです。

amountのPROMPTSを設定

この Intent が呼ばれた時に Google Apps Script を呼び出したいので、ページ一番下の Fulfillment セクションで Enable webhook call for this intent を On にします。

IntentのFulfillmentを有効に設定

Fulfillment

Fulfillment では、 Intent では設定できないような動的な返答を作成したり、外部の webhook を呼び出したりできます。

この Fulfillment の実態は Google Cloud Functions のようですが、左のナビゲーションの Fulfillment メニューで Inline Editor を On にすることで、 Dialogflow のコンソールから手軽に実装できます。

FulfillmentのInline Editorを有効に設定

エディタに以下を記述します。

// index.js

'use strict';

// Import the Dialogflow module from the Actions on Google client library.
const {dialogflow} = require('actions-on-google');

// Import the firebase-functions package for deployment.
const functions = require('firebase-functions');

// Instantiate the Dialogflow client.
const app = dialogflow({debug: true});

// Import request module to call POST request to the Google Apps Script API.
const request = require('request');

// Handle the Dialogflow intent named 'poop condition' with parameters.
app.intent('poop condition', (conv, {condition, amount}) => {
  // Google Apps Script API's URL.
  const uri = "https://script.google.com/macros/s/<API endpoint>";

  let options = {
    uri: uri,
    headers: {
      "Content-type": "application/json",
    },
    json: {
      "condition": condition,
      "amount": amount
    }
  };

  return new Promise((resolve, reject) => {
    request.post(options, (err, res, body) => {
      if (err) {
        // Respond with an error message and end the conversation.
        conv.close('エラーが発生しました。');
        console.log("error: " + err);
        resolve();
      } else {
        // Respond with the message and end the conversation.
        conv.close(`わかりました。 ${condition}のうんちが${amount}ですね。`); 
        resolve();
      }
    });
  });
});

// Set the DialogflowApp object to handle the HTTPS POST request.
exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);

前半はおまじないです。中盤の

app.intent('poop condition', (conv, {condition, amount}) => {...});

が poop condition の intent から呼び出される処理です。

後述する Google Apps Script の WEB API に conditionamount の値を POST しています。

処理が正常に完了したら「わかりました。〇〇のうんちが××ですね。」と言って会話を終了します。

Googleスプレッドシート

次は Google スプレッドシートへの記録と Slack に通知を送るための処理を Google Apps Script で実装します。

doGet() 関数や doPost() 関数を定義することで、 Google Apps Script を Web サーバーのように実行することができます。 今回はデータを受け取って Google スプレッドシートに記録するので、 doPost() 関数を以下のように定義します。

function doPost(e) {
  // Parse JSON object
  if (e == null || e.postData == null || e.postData.contents == null) {
    return;
  }
  let requestJSON = e.postData.contents;
  let requestObj = JSON.parse(requestJSON);

  // Get Spreadsheet object
  let ss = SpreadsheetApp.getActive()
  let sheet = ss.getActiveSheet();

  // Append "date", "condition" and "amount"
  sheet.appendRow([new Date(), "💩", requestObj["condition"], requestObj["amount"]]);
  
  // Call slack webhook to post "condition" and "amount"
  slackPoopieReport(requestObj["condition"], requestObj["amount"]);
  
  let output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify({ message: "Successfully saved." }));

  return output;
}

function slackPoopieReport(condition, amount) {
  const postUrl = "https://hooks.slack.com/services/<Webhook URL>";
  
  let jsonData =
      {
        "text" : "こんにちは。うんちリポーターです :poop: " + condition + "のうんちが" + amount + "出ました。"
      };
  
  let payload = JSON.stringify(jsonData);

  let options =
      {
        "method" : "post",
        "contentType" : "application/json",
        "payload" : payload
      };

  UrlFetchApp.fetch(postUrl, options);
}

事前に準備しておいた Slack の webhook を呼び出して Slack に通知も送れるようにします。

実装完了!!

冒頭の動画ではうんちの「状態」と「量」を分けて会話しましたが、1つの Intent で両方を扱うようにしたので、実は以下のように1回の会話で両方を登録することも可能です。

※音が出ます。音量にお気をつけください。

動画が見れない場合はこちら

最後に

Dialogflow は設定項目も多いので所々で迷子になりますが、全体としては少しのコーディング量で Google Assistant アプリが作れることがわかりました。

Fulfillment を使うことで会話の可能性が無限大に広がりますね!

弊社サービスのキッズリーではうんち以外にもいろいろな連絡帳の項目を扱うことができます。

保育士さんたちが手ぶらで連絡帳を記入出来る日も近いでしょう!

ユニファでは うんち 保育をハックするエンジニアを募集中です。 一緒にスマート保育園を実現しましょう!

www.wantedly.com

自分に合う英語の勉強法を探してみました

こんにちは、プロダクト開発部のちょうです。最近実は技術関連のものを勉強してなくて、ネタに困っていました。では何をしているというと、地味に英語を勉強しています。たぶん暇のとき英語を勉強している人もいると思って、すこし自分いまの勉強法を共有したいと思います。

勉強はまず目標があります。具体的な目標もあり、ちょっと評価しにくい英語を話したいという目標があります。大人になって、英語の会話は一番人気ではないかと自分が思いました。それで、いろんなオンライン英会話のサービスがあって、フリートークをしたり、真面目な勉強をしたりします。

自分は今年に入って、同じく英語を話そうと思いました。TOEICで800台の点数をとったとはいえ、一つも話せない気がします。何足りたいと見つけ出すために、本を見て、記事を見て、いろいろ試して、ようやく4月のごろ自分の勉強法を大体できた感じです。

多分自分に合う勉強法を探すのは一番たいへんだと思います。多くの人はオンライン英会話を選んで、オンライン英会話で英語を話せるように期待していたではないでしょうか。英語を話したいなら英語を話そうというのは直接で、よさそうに見えますが、どの段階の人にも適するとは言えないでしょう。

社会人になって、10年以上英語を勉強していた人に合う方法はどんな方法というと、そんな人ぞれぞれな方法ではなく、ちゃんとした科学的な方法論があるはずです。自分最近すごく同感するEnglish Clubの英語勉強術まさにそれです。

english-club.jp

どんな勉強法は科学的というと、第二言語を勉強する時間を意識する、つまり短期的で終わるとは言わないです。そして、子供がどんな形で言語を勉強しようと、大人、とくに第二言語を勉強するひとにとってはそのまま活用できないはずです。英語を話したくて最初から英語で話すようなことはあまりおすすめしない、なぜなら、英語の発音は正しくない前提でやるとすると、後で直すのはかなり難しいらしいです。

TOP 英語を話す
↓


↑
BOTTOM 英語の知識

ではどうなことをやるべきだというと、BOTTOM-TOPとTOP-BOTTOMです。BOTTOMはいままで勉強してきた英語の知識、TOPは英会話です。BOTTOM-TOPは自分の英語知識をフル活用して、話せるようにします。TOP-BOTTOMはよく使われる英語のパターンを学んで、話します。ここで注意すべきのは、TOP-BOTTOMはメインではないです。よく使うパターンだけでは自分は話したいを伝えない可能性があります。

BOTTOM-TOPはいままでみなさんやってきた英語の勉強とほぼ同じです。文法、語彙、表現など無限で英作文作れるはずです。とはいえ、受験のため勉強した内容をほぼ忘れたと思います。ゼロから勉強し直すまではないですが、最低限の知識を持つために勉強するのです。

自分はいま「English Grammar in Use」をすこしずつやっています。世界ベストセラーらしいです。文法1ページに練習1ページ、難しくないが本当に普段に使えるものです。

ある程度勉強したら、第二言語を勉強する人にとって一番の問題:母語を挟んで考えるのをすこしずつ改善します。自分の思考をそのまま英語にするのです。自分はまだこの段階に入ってないので、何も言えないのですが、科学的には第二言語を勉強する人には必要な段階です。

多分どの段階にも注意する必要があるのはやはり発音です。思い込んで発音するのではなく、ちゃんと調べてから話します。発音を直すは正直時間かかります。過去10年以上で間違った発音を直します。

自分も最初から母音と子音を勉強して、すこしずつ単語の発音を直します。おかげて、いまAnki(フラッシュカードで暗記するアプリ)2500カードの中で1/4は発音を直すためのカードです(試しに、southernを発音してみてください。そして発音を調べてください)。

この2つの段階と発音はできたら、そこそこ話せるかもしれません。そしてどんな時間を要するのかはその人いま持ってる英語の知識、発音などによるのです。とにかく焦らずに、1年を目処に勉強しましょう。

いい英語の勉強法を知ったものの、なかなか時間がなくて、文法と語彙を覚えないのも辛いですよね。時間がないのはGTDなど自分の時間を管理する方法と、朝、お昼、夜30分/15分とか確保し、一日2~3時間を勉強する方法もあります。覚えないことのは大人が子供より一番劣るところかもしれません。なので、フラッシュカードで英語のすべての覚えましょう。

apps.ankiweb.net

(Ankiのパソコンソフトは無料、アプリは3000円)

例えば、フラッシュカードの表でpensionを書いて、裏側年金と書きます。逆に表に年金を書いて、裏側にpensionを書くのです。語彙だけではなく、発音や文法もカードにしましょう。文法は自分が「English Grammar in Use」で間違った問題をそのままにします。あとはTOP-BOTTOMのカードです。例えば、表面に「あなたはレストランに食べ終わって店員さんにレシートをもらいたいときどう話す。」裏側に「Can I have the bill?」、一瞬で思い出すようにこのようにカードにします。

ここでポイントになるのは、自分でフラッシュカードをつくることです。人ぞれぞれに知識が違うので、最初からほかの人のカード一気にインポートして、すこしずつ勉強するのは自分のペースを崩しやすいかつまったくどこからの語彙という情報もないし、逆に勉強の効果は落ちるかもしれません。ある程度自分の知識をためて、自信を持ってたら、Oxford 3000などほかの単語帳を勉強します。

いかがでしょうか。英語の勉強法もいろいろありますが、社会人にとっていい英語勉強法を見つけ出すには難しいかもしれません。そこでぜひオンライン英会話よりもっと科学的な方法ですこしずつ自分の英語力を上げるような方法を試しましょう。