ユニファ開発者ブログ

ユニファ株式会社システム開発部メンバーによるブログです。

スマホで写真を撮るということ

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

世間はコロナショックで大変なことになっていますが、その他にも政治界隈では森友問題や検事長定年延長問題などでこの国の闇が浮き彫りになってきましたね。 私たち一般市民に出来る唯一の術、手洗いうがいの実行と選挙は必ず行きましょう。

はい。 では今回はデザインではなくて写真のことを書きます。

日常写真をカメラを使わずにスマホでいい感じに撮ってみよう

普段ライフワークで一眼レフを使って写真を撮る私からすると複雑な気持ちではありますが、自分の家族の写真をなんとなくいい雰囲気で撮りたいくらいのことであればおそらくスマホで事足ります。

もちろんいくらスマホが進歩したとはいえ現状はまだ一眼レフカメラでしか撮れない類のものはあります。当然商業用写真は撮れませんし、写真展でも開きたいのであれば話は別です。

厳密に言えばスマホで家族写真を撮るにしても得意不得意はありますので、普段私がスマホで撮影している公園での子供の写真を事例にしてその辺りの説明と簡単なコツを共有しようと思います。

暖かい季節のうちに安心して公園へ足を運べる日が来ることを願いつつ。

f:id:unifa_tech:20200327231554j:plainf:id:unifa_tech:20200329205523j:plainf:id:unifa_tech:20200327231632j:plainf:id:unifa_tech:20200327231657j:plain

メリットとデメリット

スマホカメラの最も優れている点は機動性です。 ほとんどの人が常に携帯していて電源のついている状態かと思います。撮りたいと思った瞬間にすぐ取り出せるというのはシャッターチャンスを逃しにくいという大きな利点になります。

もう1つは普段から見慣れていて威圧感を与えないコンパクトサイズのスマホは、被写体が構えない、警戒心を抱きにくいので自然な表情を撮りやすいということもあります。

デメリットとしてはブレやすい、つまり速く動くものに弱いということです。 例えば運動会などで走っているところを撮ろうするのは中々難しいと思います。2歳児くらいまでのスピード感であればギリ捉えられますが。

もう1点はセンサーサイズが小さいため暗所に弱いです。光量の少ない場所では使い物になりません。 ズーム操作もやめましょう。スマホのズームは“画像を切り取って大きく見せる”だけの機能です。つまり一気に画質が粗くなるので、アップで撮りたい場合は可能な限り自分の足で距離を詰めましょう。

撮り方

基本的にはスマホと被写体を水平にして撮りましょう。特に子供を撮る場合は身長差でスマホが見下ろす角度(ハイアングル)になりやすいですが、そうなると被写体の体に歪みが生じます。 あえて意図的にハイアングルやローアングルにする場合もありますし時と場合にもよりますが、基本は水平に撮ると覚えておいて間違いないかと思います。

次にこれはスマホに限ったことではないですが、大事なのはカメラの性能ではなく、光と構図です。 高性能のカメラを持つ人が良い写真を撮るわけではありません。

これは好みもあるかと思いますが、やりがちなのは被写体を正面から画面いっぱいにどーんと撮ることです。もちろんそれが間違っているわけではないですし、愛情ゆえに気持ちが全面に出るのもわかりますが、それだと毎回同じような写真ばかりがスマホに蓄積されていきます。 引きの写真が撮れるようになるとメリハリがついてきますし、記録の世界も広がります。

光について

光については先ほども話しましたがスマホは暗所に弱いので、光量の多い晴れた日のほうが比較的綺麗に撮りやすいと思います。

光はとても繊細です。 同じ場所から撮影してもミリ単位で角度をずらすだけで写り方はまるで変わってきます。 同じ被写体を撮る場合でもなるべくスマホの角度を変えてみたり、自分の体をずらしてみたりしながら微妙な変化をつけて何枚か撮ってみましょう。

また屋内での撮影は自然光がたくさん入る場所がベストです。その際可能であれば部屋の電気は消しましょう。自然光と人口光が混ざる(ミックス光という)と色の濁る現象が起きます。 家の光だけで撮る場合は暖色系よりも白系の蛍光灯のほうが人の肌は綺麗に写ります。

構図について

構図ですが、これもよくあるものとして被写体を常に真ん中で捉えてしまうこと(日の丸構図)です。 もちろんそれが間違いではありません。例えば商業写真(ルクミーカメラマンなどの保育園、学校写真やブライダルなど)は主役を際立たせるという目的がはっきり決まっているので、それを強調する意味でこの手法は正しいかもしれませんが、これを写真の全てだと決めつけてしまうのは危険です。 面白い写真を撮るには既存の常識や思い込みからどれだけ離れることができるかが重要です。

フィルター加工

f:id:unifa_tech:20200327231824j:plainf:id:unifa_tech:20200327231835j:plainf:id:unifa_tech:20200327231849j:plainf:id:unifa_tech:20200327231859j:plain

スマホカメラ最大の魅力はフィルター加工がボタン1つで簡単にできることです。 個人的には加工をしすぎると写真というよりはアニメCGのようになってしまい繊細なディテールが失われてしまうのでお勧めしません。(しかし今は世界的にバキバキに加工された写真が流行している…)

モノクロ加工は極端に切り取る風景が様変わりするので新鮮です。またモノクロで撮影すると色情報が無くなるため、感情が強調され被写体との結びつきをより強く感じることができます。 と、実際は途方もなく奥深く複雑な世界ですが、今回はスマホでいい感じに撮るという話なので最も手っ取り早く雰囲気を出すには良い方法です。

最後に

写真に正解はなく、無限に自由です。 なのでここに書いたことはあくまで個人の見解に過ぎないことをご理解ください。

また私の持っているスマホはiPhone 5sという古い型なので、最新のスマホであれば更に性能が上がってるでしょうからここに書いたデメリットの部分もいくらか改善されているかもしれませんね。

スタジオなどで着飾って撮ってもらう非現実的な写真も良いですが、ありふれた日常を意識的に形に残すことも大切です。 そしてそれを最も自然に残すことができるのは家族だけです。その点はプロカメラマンでさえ敵いません。

せっかく高性能なスマホカメラが身近にあって誰でも気軽に楽しめる時代ですから、写真で表現することを少し意識してみても面白いかと思います。

機械学習によるテキスト分類(入門)

研究開発部の浅野です。普段は画像処理、信号処理、データ分析などを中心に行っていますが、自然言語処理についても今後の応用範囲が広そうなので理解を深めていきたいと思っています。自然言語処理には翻訳、対話応答、感情分析、要約など様々なタスクがある中で、今回は基本的かつ汎用的であるテキスト分類についてまとめたいと思います。テキスト分類とは、文章がどんな内容について書かれているかを調べ、それをもとにトピックごとに分類するタスクです。

ルールベースの手法

最も直感的な方法としては、例えばニュースの分類を行う際に、「日本」や「アメリカ」など複数の国名がでてきたら「国際」というカテゴリーにする、というように経験に基づくルールを設定するというものがあります。この場合「日本はWBC準決勝にて6-2でアメリカを下した。」という文も国際ニュースに分類されてしまうので、「WBC」や「勝ち/負け」に関する単語が入っている場合は「スポーツ」にする、といった新たなルールが必要です。この例からも容易に想像がつくように、多様な文章を適切に処理したり新たな言葉やトピックに対応する上でルールベースの手法は非常に手間がかかります。

機械学習による分類

そこで有効なのが、たくさんのデータをもとに効率よく分類する方法をコンピュータに習得させる機械学習です。機械学習によるテキスト分類には大きくわけて2つのフェーズがあり、それぞれのフェーズで多くの方法が提案されています。

f:id:unifa_tech:20200321223757j:plain:w500
機械学習によるテキスト分類の流れと種類
テキストをベクトル化する方法としては、文書内の単語の出現頻度をもとに算出するカウントベースの手法や、単語や文などの分散表現を算出するモデルを使用する推論ベースの手法があります。また、分類のフェーズでは、特異値分解や確率モデルを用いて指定したトピック数の群に分ける教師なし学習、あるいは決められたトピックに分類するための学習モデルを作成する教師あり学習が主に用いられます。

BERT

今回はベクトル化においてはBERTを、分類にはSVMを使用することにします。BERTは2018年10月に発表された単語や文の分散表現を計算する仕組みで、自然言語処理の様々なタスクへの応用で軒並み最高精度を叩き出した非常に汎用性の高いモデルです(とてもわかりやすい解説記事はこちら)。形態素に分解した文(のID)を入力すると、BERTはそれぞれに対応した768次元のベクトル(分散表現)を出力します。

f:id:unifa_tech:20200322210030j:plain
BERTの入出力(概略図)
文頭に付与する[CLS]というトークンに対応する出力は文全体の分散表現となるように事前学習が行われています。今回の分類においてもそれを利用します。複数の文からなるテキストに対しては例えば各文に対応するベクトルの平均をとることでそのテキスト全体のベクトル表現を得ることができます。

学習

BERTの学習には非常に時間がかかるので、日本語Wikipediaで事前学習済みのモデルをありがたく使用させてもらいます。ファインチューニングは行っていません。Keras BERTを使うことでモデルまわりの実装はとても容易です。また、SVMによる分類器の学習用データにはKNBコーパスを使いました。KNBコーパスは京都観光、携帯電話、スポーツ、グルメの4つのトピックに関するテキストが合計249記事含まれているデータセットです。BERTによる分散表現を用いて、各テキストが4つのうち正しいトピックに分類されるようなSVM分類器を学習します。SVMの実装もscikit-learnを使えば難なくできます。今回スクラッチから書いたのはコーパスをクレンジングしてSentencePieceで形態素に分け、BERTでEmbeddingを取得して学習/テストデータを作るところだけです。KNBコーパスに含まれるtsvファイルから必要な部分だけを抜き出すスクリプトの例を書いておきます。

import os
import pandas as pd

column_names = ['id_long', 'sentence', 'na0', 'na1', 'na2', 'na3']
get_index = lambda x: x.split('-')[0]

def get_corpus(tsv):
    df = pd.read_table(os.path.join(path_to_corpus, tsv), header=None)
    df.columns = pd.Index(column_names)
    df['id_short'] = df['id_long'].map(get_index) #文章ごとのindexを取得
    droplist = list(df.groupby('id_short')['sentence'].first()) #各文章の最初はタイトルなので削除
    droplist_index = df['sentence'].isin(droplist)
    corpus = df[~droplist_index]

    return corpus[['id_short', 'sentence']]

gourmet  = get_corpus('Gourmet.tsv')
kyoto = get_corpus('Kyoto.tsv')
keitai = get_corpus('Keitai.tsv')
sports = get_corpus('Sports.tsv')

6割のデータを使って学習をした後、残りの4割でテストを行った結果が下記です。f1-scoreのweighted averageでは95%と高い精度が出ました。さすがBERTです。

      {0: 'gourmet', 1: 'keitai', 2: 'sports', 3: 'kyoto'}

          precision    recall  f1-score   support
       0       0.95      0.90      0.92        20
       1       0.97      0.97      0.97        32
       2       1.00      0.71      0.83         7
       3       0.93      1.00      0.96        40
 wgt avg       0.95      0.95      0.95        99

      Confusion matrix: 
      [[18  0  0  2]
       [ 1 31  0  0]
       [ 0  1  5  1]
       [ 0  0  0 40]]

結果の例

具体的にどのような分類になるのかイメージするために次の文章の結果を紹介します。

  1. 両親をつれてたくさんのお寺めぐりをしながら紅葉を楽しんだ。
  2. 途中見つけた素敵なカフェで紅茶をいただいた。
  3. 外国からもたくさんの方が訪れていた。

まず、上記1の文だけを分類した場合には「京都」カテゴリに正しく分類されました。一方、1と2の文からなる文章を入力した場合は「グルメ」に分類されました。これは確かに人間でも迷うケースですね。そして最後に1,2,3の文からなる文章にすると、分類結果は「京都」になりました。日本語Wikipediaで一般的な知識を学習したBERTとKNBコーパスで文章の分類方法を学習したSVMで、かなり高度な判断ができるようになっていることがわかりますね。

まとめ

機械学習によるテキスト分類の流れと、BERTとSVMを使った例を見てきました。難しいチューニングなどを一切行わなくてもこれだけの精度がでてしまうというのはなかなか驚きです。BERTはテキスト分類だけでなく様々なタスクに応用が効くので、今後は弊社が展開しているサービスやインターネット上から得られる保育関連のテキストデータの処理にも挑戦していきたいと思っています。

仮説検証をスピードアップするためにできること

"未来を予測する最善の方法は、未来を発明することだ "

アラン・ケイ

こんにちは。プロダクトマネージャーの井口(いのくち)です。上の言葉はパーソナルコンピューターの父と言われる、アメリカの計算機科学者のアラン・ケイの言葉です。

「未来を発明する」という意識はなくても、多くの企業が顧客が生活を便利にできるようなサービスやプロダクトを開発していると思います。

適切な顧客課題と解決策の発見のために多くの仮説検証が行われていますが、仮説検証が思ったように進まず、苦しんでいる企業やプロダクトマネージャーも多いのではないかと思います。

私も四苦八苦している1人ですが、昔より仮説検証を早く進められるようになった考え方があるので、今日はそれをお伝えします。

仮説検証の流れ

仮説検証のゴールは、

「顧客(A)は〇〇という課題(B)を持っており、その解決のためにXXという解決策(C)を採用する」

これに当てはまるABCを見つけることと言えます。 ABCの発見のためには「仮説を立てる → 検証する → 仮説を更新する → 再検証する」という、仮設検証サイクルを回していくことになります。

仮説検証をスピードアップする方法

仮説検証が進まない原因

仮説検証が進まない原因の1つに「初期仮説(初めに考えていた仮説)が外れた時に、次に何を仮説検証すべきか分からない」という問題があります。 初期仮説は外れる場合が多いのですが、初期仮説の次に何を検証するかを考えられず、検証スピードが落ちてしまう場合が多いと感じています。(私も経験しました。。)

この問題の回避策として、次の3点を意識することをお勧めします。

  1. ターゲット顧客・顧客の課題・解決策の観点から、構造的に仮説を設計すること
  2. 着手している仮説検証が構造上のどの位置にあるか意識すること
  3. 仮説が外れた場合は、同階層で別の仮説を検討したり、階層を上下して新しい仮説を見つけること

具体例を通じて、この回避策を利用した仮説検証の進め方を見ていきましょう。

仮説検証の具体例

はじめに、あなたは製薬メーカーで花粉症対策用の薬を作っているとします。

  • 経営方針で、花粉症対策の新薬を作ることが決まり、あなたは新薬企画担当に任命されました。
  • すでに、鼻詰まりを解消する薬は開発済みとします(飲み薬、点鼻薬)

さて、あなたはこの後、どのように企画を立てていきますか? (この例では、予算云々や重要仮説かどうかなど、実践上重要になる点は省いています)

最初に、あなたは現状を構造化してみることにしました。するとこのようになります。

f:id:unifa_tech:20200312101046j:plain
図1:現状の構造

次に、あなたは過去の調査で、花粉症の人は「目も痒くなりやすい」ことを思い出しました。このことから課題を広げてみたところ「目の痒みを解消したい」という課題がありそうなことに気付きました。

f:id:unifa_tech:20200312100126j:plain
図2:横展開

この課題の解決策として「目の痒みを抑える目薬があると使われるのでは?」という仮説を考え、目薬の開発を検討します(初期仮説構築)。

f:id:unifa_tech:20200312100817j:plain
図3:新しい仮説検証ポイント

調査をしたところ、目薬マーケットは競合が多く、参入が難しいことがわかりました(初期仮説棄却)。 そこで、あなたは再度花粉症の人の行動を調べると、部屋干しが多いことが分かりました。

f:id:unifa_tech:20200312094846j:plain
図4:発見した顧客の行動

「行動には何らかの理由(解決したい課題)がある」と考え、花粉症の人と部屋干しの関係(顧客の行動理由)を考えてみることにしました。(「?」に当てはまるものは何か)

f:id:unifa_tech:20200312094635j:plain
図5:顧客の行動理由

課題の抽象度を上げて考えてみたところ、「花粉症の症状が出ないようにしたい」という課題がありそうなことに気づきました。

f:id:unifa_tech:20200312094223j:plain
図6:抽象化

これを新薬の具体的なアイデアに落とすと、「花粉症の季節の前に飲めば、鼻詰まりが起きないような薬」のニーズがありそうと気づきました。

f:id:unifa_tech:20200312094059j:plain
図7:具体化でアイデアに落とし込む

あなたはこの新薬のアイデアの受容性を新しい仮説として、仮説検証を進めていくことにしました。(具体例はここで終わりです。)

まとめ

上記の例で見てきた通り、サービスを構造的に考えて仮説検証することで、仮説が詰まった時に他の仮説を想起しやすくなります。

仮説の想起の際は、同階層でアイデアを考えてみたり、抽象化・具体化して考えてみることで、新しい仮説を作り出してみてください。

あなたのサービスやプロダクトの仮説検証が進むことを願っています。

※ユニファでは一緒に保育をハックしてくれる、プロダクトマネージャーの方を大募集しています。ぜひ一度遊びに来てください!

www.wantedly.com

リモートシャッターで子供のうんちをSlack通知する その1

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

1歳3ヶ月になる私の息子は毎日快便です。
多いと1日5回くらい。おむつを替えているそばからうんちをします。

毎朝妻が保育園の連絡帳を書いてくれるのですが、

妻「昨日、Kくんはいつうんちしたっけ?」

私「お風呂入る前と寝る前だと思う。。。いや、寝てる間もしてたな。」

となります。

困ったなーと思って近所のドラッグストアにおむつを買いに行った時にこんな物を見つけました!

リモートシャッター

こ、、これは!!ダイソーで300円で買える IoT ボタン、として一時期話題になっていたリモートシャッター!!

少し値上がりしていますが、おむつに比べれば安い安い!!ということで迷わず購入しました。

それでは早速リモートシャッターでうんちの記録を残せるようにしていきます。
「うんちシャッター!」と呼ぶことにします。

ソースコード全体はこちらにあります。

github.com

目次

全体の構成

子供がうんちをしたら、おむつを替える時にボタンをポチッと押すことにします。

Slack に通知するだけだとうんち履歴が文字通り流れてしまうので、 Googleスプレッドシートにも残すようにします。

当初はリモートシャッターと Raspberry Pi を Bluetooth でつなげようと思っていたのですが、Raspberry Pi をガラクタ入れから引っ張り出してみたら、壊れていて起動しませんでした。。

気をとりなおして、自分の Mac を使うことにします。

全体の構成 最終版

リモートシャッターと Mac を Bluetooth で繋げて、ボタン押下の信号を受け取った Mac が IFTTT の Webhook を叩くという流れです。 IFTTT と Slack や Googleスプレッドシートとの連携はすでにしてあるものとします。

準備

構成が決まったところで開発の下準備を始めます。

MacでBluetoothを扱えるライブラリは意外と少なく、 Python だと『pybluez』『lightblue』『BluefruitLE』、 node だと『noble』『noble-mac』といったところです。

ちなみに iOS での実装については過去のしだのエントリーをご覧ください。

tech.unifa-e.com

色々調べたり実際に使ってみたりしたのですが、 Catalina でまともに動きそうなのは noble-mac だったのでこれを使うことにします。

% npm init -y

noble-mac は公開されていないので dependencies に追記して npm install します。

"dependencies": {
    "noble-mac": "https://github.com/Timeular/noble-mac.git"
}

ついでに IFTTT の Webhook を叩くので request もインストールしておきます。

% npm install
% npm install request --save

実装

Bluetooth(BLE) では通信するデバイスをそれぞれ Central とPeripheral と呼びます。

Central と Peripheral

イメージとしては Peripheral には Service が入っており(複数の場合もあるらしい)その中にサービスの特性としての Characreristic があります。

デバイスの検出

ひとまずリモートシャッターの Peripheral UUID がわからないと話にならないので、それを取得します。

// find_device.js

'use strict';

const noble = require('noble-mac');

const discovered = (peripheral) => {
  console.log(`Device Discovered: ${ peripheral.advertisement.localName }(${ peripheral.uuid })`);
  console.log(`    address: ${ peripheral.address }`);
  console.log(`    serviceUuids: ${ peripheral.advertisement.serviceUuids }`);
  console.log(`    rssi: ${ peripheral.rssi }`);
  console.log('-----------------');
}

noble.on('stateChange', (state) => {
  if (state === 'poweredOn') {
    noble.startScanning();
  } else {
    noble.stopScanning();
  }
});

noble.on('scanStart', () => {
  console.log('[scanStart]');
});

noble.on('discover', discovered);

実行してみると、 AB shutter3 というデバイスが見つかり、 Peripheral の UUID や提供する Service の UUID を確認できます。

find deviceのログ

ちなみにですが、 Service の UUID は GATT により定義されており、 1812Human Interface Device になります。

www.bluetooth.com

Characteristicのsubscribe

Peripheral UUID がわかったところで、内部の Characteristic を取得します。

ググったところ、どうやら 2a4d という Characteristic UUID でボタンの押下情報を取得できるらしいことがわかりました。ちなみに Characteristic UUID も GATT により定義されており、 2a4dReport になります。

www.bluetooth.com

// subscribe_report.js

// ---- 省略 ---- //

const REPORT_CHAR = '2a4d';

const discovered = (peripheral) => {

  if( peripheral.uuid == PERIPHERAL_UUID){

    peripheral.on('connect', () => {
      console.log('[connect]');
      peripheral.discoverServices();
    });

    peripheral.on('disconnect', () => {
      console.log('[disconnect]');
    });

    peripheral.on('servicesDiscover', (services) => {
      services.forEach(service => {

        service.on('characteristicsDiscover', (characteristics) => {
          characteristics.forEach(characteristic => {
            console.log('Characteristic Discovered');
            console.log(`    serviceUuid: ${ characteristic._serviceUuid }`);
            console.log(`    uuid: ${ characteristic.uuid }`);
            console.log(`    name: ${ characteristic.name }`);
            console.log(`    type: ${ characteristic.type }`);
            console.log(`    properties: ${ characteristic.properties }`);

            if (characteristic.uuid === REPORT_CHAR) {
              characteristic.on('data', (data, isNotif) => {
                const jsonStr = data.toString('utf-8');
                const jsonData = JSON.parse(jsonStr);
                console.log(jsonData);
                // TODO: ここでIFTTTにPOST
              });

              characteristic.subscribe((error) => {
                console.log('=> Subscribe Started');
              });
            }

            console.log('-----------------');
          });
        });

        service.discoverCharacteristics();
      });
    });

    peripheral.connect();
  }
};

// ---- 省略 ---- //

しかし、ここで暗礁に乗り上げます。

リモートシャッターの Peripheral に含まれる全ての Service の Characteristic を書き出していますが、 2a4d という Characteristic は見つかりません。

subscribe reportのログ

思考錯誤をしてわかったことは、

  • デバイスもしくはライブラリの問題で Report の characteristic が見つからない
  • そもそもリモートシャッターと Mac の Bluetooth の接続が不安定である
  • というか、すぐに切れる。切れていたかと思うと突然つながる
  • リモートシャッターのボタンを押すと、つどつど接続する

苦肉の策

ボタンを押すと一瞬だけ接続されることは検知できるので、これをトリガーにすることにします。

// ifttt_on_discovered.js

// ---- 省略 ---- //

let discovered_at;

let req_options = {
  uri: `https://maker.ifttt.com/trigger/${ IFTTT_WEBHOOK_EVENT }/with/key/${ IFTTT_WEBHOOK_KEY }`,
  headers: {
    'Content-type': 'application/json',
  },
  json: {
    'value1': 'うんち'
  }
};

const discovered = (peripheral) => {
  if( peripheral.uuid === PERIPHERAL_UUID){
    const elapsed = new Date() - discovered_at;
    if (elapsed > 5000) {
      console.log(`Device Discovered: ${ peripheral.advertisement.localName }(${ peripheral.uuid })`);
      request.post(req_options, (err, res, body) => {
        if (!err) {
          console.log('Report Sent');
        }
      });
      discovered_at = new Date();
    }
  }
}

// ---- 省略 ---- //

接続が不安定な結果、 connect と disconnect を3秒間くらいの間に大量に繰り返すので、5秒のインターバルをおくことにしました。

結果的になんとも情けない感じにはなりましたが、なんとか「うんちシャッター!」として機能するようになりました。

動作結果

Google スプレッドシートにもこの通り履歴が残ります。

最後に

上の画像ではキレイに6件通知されましたが、実際にはボタンを押していないのにリモートシャッターと Mac の Bluetooth が突然つながったりもするので、息子がうんちをしていないのに通知が来ることもあります。

なんとかして Report の characteristic を subscribe できれば防げるので、これを今後の改善点と見据えて、タイトルに「その1」とつけました。

せっかくボタンが2つあるので、うんちが硬かったら大きいボタン、ゆるかったら小さいボタン、みたいにできたらいいなと思います。

ちなみにですが、弊社の連絡帳アプリのキッズリーではうんちの状態を4段階(かたい/ふつう/ゆるい/水様便)で登録することができます。
あいにく「うんちシャッター!」の販売予定はございませんので、弊社キッズリーをご利用ください。

ユニファでは 『うんち 保育をハックする』 エンジニアを募集中です!

www.wantedly.com

Slackを活用する上で気をつけたい8のこと

みなさんこんにちは! スクラムマスターの渡部です。

今回は開発とは直接関係は無いのですが、私達の日々の生産性に大きな影響を与えるツールであるSlackを活用する上で、注意した方が良いと私が考えていることについてお話していきます。

「相手の返信ペースに合わせること」や「目上の人の入力中表示の場合は待つこと」などの儀式のようなものではありません。

実際に日々のコミュニケーションがスムーズになるもので、すぐに使える・意識できるものになっています。

複数書いていますが、全部を一度に変えてみるのは大変ですので、先ずはなにか一つ、みなさんの中で「お、これは!」と思ったものから順番にチャレンジしてみていただくのが取り組みやすいかと思います。

目次

  • 目次
  • 気をつけたいこと
    • チャンネル入りすぎ問題
    • 同じトピックの会話が新規投稿されることで遡れない & 投稿が流れ過ぎる問題
    • Slackのスターで個人タスク・ピン留めでチームのタスクを管理する問題
    • 重要な共有・永続的なルールがサラッと共有される問題
    • スレッドの議論が超長いな問題
    • DM(ダイレクトメッセージ)・プライベートチャット多い問題
    • 常時(メンションの度)Slackをチェックしてしまう問題
    • 緊急な連絡がSlackのみ問題
  • 最後に
続きを読む

DynamoDBで集計とJavaScriptのPromise

こんにちは、プロダクト開発部のチョウです。最近一部古いJavaScriptをcallbakからPromise/async/awaitのスタイルに書き換えるようにしてみました。その中で印象に残ったコードを紹介します。

やりたいことが簡単です。あるセンサーのデータ個数を日ごとに集計します。テーブルの構造はこういう感じです。

SensorId: Number(Partition Key)
Date: String(Sort Key)
DataCount: Number

Partition KeyとSort KeyはDyanmoDBでテーブルを作るとき必要な情報ですが、とくに本文に関係ありません。

普通にRelation Databaseで考えると、最初にレコードを作るのは要注意です。複数クライアントでレコードを作ろうとすると、一個だけが実際のDBに残り、集計が正しくない状態になります。 普通のDBならunique indexなどを使います。SensorIdとDateを一つのunique indexにして、同じindexで作ろうと一つだけが成功し、ほかはすべて失敗です。 DynamoDBにunique indexがないですが、ConditionExpressがあります。レコードを追加したり更新したりするとき条件に満たさないと失敗になります。一個のセンサーに一日に一個のデータを作るには

    this.ddb.putItem({
      TableName: 'SensorDigest,
      Item: {
        DeviceId: {N: String(sensorId)},
        Date: {S: date},
        DataCount: {N: String(1)}
      },
      ConditionExpression: 'attribute_not_exists(Date)'
    }, (error, data) => {
      if(error) {
        if(error.code === 'ConditionalCheckFailedException') {
          // 重複
        } else {
          // 他のエラー
        }
      } else {
        // 先生
      }
    });

attribute_not_existsを使えば、レコードがない前提で追加できます。パラメータのDateは実際なんでもいいですが、必ずあるKeyにしたほうをおすすめです。

次にDataCountを増やすのも簡単ではありません。複数クライアントで一斉に更新すると、一部クライアントのデータがロスするかもしれません。 この問題に対し、普通のDBはversionカラムなどの方法があります。 DynamoDBになると、UpdateExpressionに SET DataCount = DataCount + increment や、1を増やす場合、ADD関数を使えます。 更新のコードはこうなります。

    this.ddb.updateItem({
      TableName: 'SensorDigest',
      Key: {
        SensorId: {N: String(sensorId)},
        Date: {S: date}
      },
      UpdateExpression: 'SET DataCount = DataCount + :increment',
      ExpressionAttributeValues: {
        ':increment': {N: String(increment)}
      }
    }, (error, data) => {
      if(error) {
        // 失敗
      } else {
        // 成功
      }
    });

レコードの追加と更新をあわせて考えると、ごく普通で、日ごとにセンサーのデータを集計することが複雑になりました。 あるセンサーのデータが来ると、まず追加ですか。それとも更新ですか。

ここで、私の考えは、

  1. 更新
  2. 更新失敗でしたら、追加
  3. 追加失敗ししたら、更新

最初更新にするのは、99%以上が更新だからです。追加は1回のみで、毎回やる必要がありません。 一日最初の更新は失敗するので、そこでレコードを追加します。 もし運悪く、複数クライアントで1の更新失敗し、2の追加をすることになったら、一つのクライアントだけが成功し、失敗したクライアントは更新モードに戻ります。今度こそ失敗しないです。

流れが完璧ですが、コードは少々複雑です。Promise/async/awaitなしのバージョン

  // pattern 1
  increaseDataCount(sensorId, date, increment, moveToPattern2) {
      this.ddb.updateItem({
      TableName: 'SensorDigest',
      Key: {
        SensorId: {N: String(sensorId)},
        Date: {S: date}
      },
      ConditionExpression: 'attribute_exists(DateUsed)',
      UpdateExpression: 'SET DataCount = DataCount + :increment',
      ExpressionAttributeValues: {
        ':increment': {N: String(increment)}
      }
    }, (error, data) => {
      if(error) {
        if(error.code === 'ConditionalCheckFailedException' && 
           moveToPattern2) {
            // レコードは存在しない、go to pattern 2
            save(sensorId, date, increment)
        } else {
            // 失敗
        }
      } else {
        // 成功
      }
    });
  }

  save(sensorId, date, increment) {
    this.ddb.putItem({
        TableName: 'SensorDigest,
        Item: {
            DeviceId: {N: String(sensorId)},
            Date: {S: date},
            DataCount: {N: String(increment)}
        },
        ConditionExpression: 'attribute_not_exists(Date)'
        }, (error, data) => {
        if(error) {
            if(error.code === 'ConditionalCheckFailedException') {
                // 重複、go to pattern 3
                increaseDataCount(sensorId, date, increment, false);
            } else {
                // 他のエラー
            }
        } else {
            // 先生
        }
    });
  }

pattern 1のときに更新と同時にConditionExpressionを追加し、レコードがないとすぐ失敗になります。 callbackの中でもし条件に満たさないエラー(レコードが存在しない)なら、pattern 2に移します。

pattern 2でレコードを追加します。ここもConditionExpressionがあり、複数クライアントの場合ひとつのクライアントだけ成功になります。失敗したほかのクライアントは条件満たさないエラーで、pattern 3に移します。

pattern 3はpattern 1のコードそのまま利用し、パラメータのmoveToPattern2をfalseに設定しただけです。パラメータを用意するのは無限ループにならないためです(基本ならないと思いますが)。確率からすると、pattern 3になるクライアントはほとんどいないはずです。

ようやくcallbackスタイルのコードをPromise/async/awaitに変わる部分に入れるようになりました。 上のコードを見ると、同期スタイルのコードの変えるのは難しそうですね。

try {
    await increaseDataCount(sensorId, date, increment)
} catch(e) {
    if(e.code == 'ConditionalCheckFailedException') {
        try {
            await save(sensorId, date, increment)
        } catch(e) {
            if(e.code == 'ConditionalCheckFailedException) {
                await increaseDataCount(sensorId, date, increment);
                return;
            }
            throw e;
        }
        return;
    }
    throw e;
}

(ここで、increaseDataCountのcallbackからsaveをコールしなくなるので、パラメータmoveToPattern2を削除しました。) しかも、同期スタイルに変えても読みにくそうです。 ではPromiseスタイルはどうでしょう。

await increaseDataCount(sensorId, date, increment)
    .catch(e -> 
        if(e.code == 'ConditionalCheckFailedException') {
            return save(sensorId, date, increment)
        }
        return Promise.error(e)
    )
    .catch(e -> 
        if(e.code == 'ConditionalCheckFailedException') {
            return increaseDataCount(sensorId, date, increment)
        }
        return Promise.error(e)
    );

同期コードよりよさそうです。わかりやすいです。 increaseDataCountとsaveをPromiseスタイルに変えるのはとくに難しくないのでここで割愛します。

私が古いコードをPromise/async/awaitに変える中でこれは一番難しかったです。フローはそうだし、エラーハンドリングもすべてasync/awaitに寄せるではなく、Promiseも使うのもいいでしょう。 実際、こういうPromiseのchainのような書き方は関数型プログラミング言語HaskellでMonadといいます。Monadを用いたReactiveX(RxJava、RxSwiftなど)もエラーハンドリングなどに優れています。

いかがでしょうか。DynamoDBで集計テーブルを操作するのも、callbackスタイルのコードをPromise/async/awaitも難しかったんでしょうか。普段こういうコードあまり見れないと思いますので、参考になると幸いです。

ルクミーMIMAMORIフォントを作ってみる

Webエンジニアのほんまです。

今回ですが、紹介できるネタがなくなってしまったので、ちょっとお遊び的な内容になっています。 エンジニアの方には物足りないかもですが、非エンジニアの方は楽しめる内容になっていると思いますよ。

今回は、「ルクミーMIMAMORIフォントの作成」にチャレンジしてみました。

続きを読む