Quantcast
Browsing Latest Articles All 25 Live
Mark channel Not-Safe-For-Work? (0 votes)
Are you the publisher? or about this channel.
No ratings yet.
Articles:

MavenをJava 8で動かしつつコンパイルはJava 6、テストはJava 11で行う

フレームワークを作ったりしていると後方互換の関係でJava 6でコンパイルしたいことがあります。
また、新規案件では古いJDKとか使わないのでコンパイルはJava 6でしたとしてもテストはJava 6・7・8・11で実施したかったりします。

最近のMavenはJava 6では動かないんですけど、工夫をすればMaven自体はJava 8で動かしつつコンパイルはJava 6、テストはJava 11で行うといったことが可能です。

maven-compiler-pluginexecutablejavacコマンドを指定することで任意のJDKでコンパイルができます。
それからmaven-surefire-pluginjvmjavaコマンドを指定することで任意のJDKでテストができます。
いずれもシステムプロパティで設定できます。

コマンド例はこんな感じ。

export JAVA_HOME=/path/to/jdk8
mvn -Dmaven.compiler.executable=/path/to/jdk6/bin/javac \
    -Dmaven.compiler.fork=true \
    -Djvm=/path/to/jdk11/bin/java \
    test

参考

ドキュメントに時間が吸われる人におくる「断捨離」アプローチ

はじめに

いざ、ウォーターフォールバリバリ1な開発スタイルだったところに、アジャイルな考えを適用しようとすると、ネックになるのが「ドキュメント」だと思います。

「金融系・お客様ほぼほぼ固定・サーバ保守する人がほとんど」な環境でアジャイル適用し始めた時に、断捨離を断行しました。
ただ、その時は捨て去りすぎて、少し問題2が起こっちゃいました。
本記事は、その経験を経ての整理結果です。

注意事項

  • この記事は仮説です。この仮説での実証ができていません。当然、上手くいくことを保証するものではありません。(が、一度実務に適用した結果を受けたモノなので、参考にはなると思います)
  • アジャイルってキーワード出してますが、ウォーターフォールバリバリなところにもきっと効果があると思うので読んでって。

断捨離アプローチ

いろいろ考えたのですが、「断捨離」を基準に説明するのがキャッチーで伝わりやすい気がしたので、「断捨離」というフレームワークで説明をしていきます。

断捨離は、「もったいない」という固定観念に凝り固まってしまった心を、ヨーガの行法である断行(だんぎょう)・捨行(しゃぎょう)・離行(りぎょう)を応用し、

  • 断:入ってくるいらない物を断つ。
  • 捨:家にずっとあるいらない物を捨てる。
  • 離:物への執着から離れる。

として不要な物を断ち、捨てることで、物への執着から離れ、自身で作り出している重荷からの解放を図り、身軽で快適な生活と人生を手に入れることが目的である。ヨーガの行法が元になっている為、単なる片付けとは一線を引く。

wikipedia先生より

ドキュメントの断捨離方針

「断捨離」に照らすと、ドキュメントを見直す基本方針は以下になります。

  1. 断 : 入ってくるいらない物を断つ
    • 【構築時】「設計書フォーマット」に脳死で従わない
      • 「フォーマットにあるから書かなきゃ」ってのは死ぞ
    • 【保守時】開発時のドキュメントをそのまま保守対象にしない
      • 保守に必要なドキュメント/要素だけに絞る
  2. 捨 : 家にずっとあるいらない物を捨てる
    • 【保守時】参照されないドキュメントはゴミ箱に
      • 存在するからメンテナンスしなければいけない
    • 【両方】「ゲートキーパー」という役割の自然発生を抑制する
      • 「自分ごときが修正していいのかな・・・」「あの人に確認が必要だ・・・」とかを抑制しよう
  3. 離 : 物への執着から離れる
    • 【両方】「設計」と「設計書作成」を分けてみる
      • 設計としてやるべきことは何か?
      • ドキュメントとして残すべきことは何か?
        • 例えばホワイトボードの写メではダメなのか?
        • 本当に必要なのは「ドキュメント」ではなく「ノウハウの共有」なのでは?
    • 【両方】今までのやり方から脱出できない組織文化に一石を投じてみる
      • 「政治的な問題」には政治を

結論

こっから「断捨離」関係無くなります。。。
順を追って、「断捨離」絡めつつ、この結論に持ってこうとしたのですが、筆者のスキル不足で諦めました3・・・
(一本道でここにたどり着いたわけじゃ無く、色々悩んで到達したので、文章で説明ができなかった。。。)

  1. 開発に必要:存在しないとプログラミングのスピード/品質が著しく落ちるドキュメント
    • 外部設計書:顧客との瑕疵担保責任の話になるケースもあるので、「品質レベル:社外版」で作成が必要なケースが多い。
      • 一番丁寧に書くとしたら、IPAが出している「機能要件の合意形成ガイド(旧:発注者ビューガイドライン)」にならうのが良いかも。
        • 瑕疵担保とかの話でこじれるなら、ここまで書かないといけない気がする。
        • できればユーザー企業・ベンダー企業間で適切なパートナーシップを前提にして、ドキュメントの記載粒度を調整していきたいところ。
    • 内部設計書:本質的には、開発者が理解できれば良い。ここを細かくしすぎると、「製造」の裏返しになるため、ガチガチウォーターフォールだと無駄になりやすいドキュメント。
      • メンテしない:ソースコードを見ればわかる情報。ソースコードから生成できる情報。
        • ドラフト版レベルは作っても良い。
        • ドキュメントの形式で保守チームに引き継ぐ必要はない。
      • メンテする:「方式設計と機能要件をどういう考え方で適用したのか?」的な実装背景を支える情報。
        • コメントなどで指摘が容易に入れられるツール(Confluence/Qiita:Teamなど)で残していくのが望ましい。
  2. 運用に必要:存在しないと障害対応や運用作業のスピード/品質が著しく落ちるドキュメント
    • コメントなどで指摘が容易に入れられるツール(Confluence/Qiita:Teamなど)で残していく
    • ExcelやWordなど、ファイルサーバー上でファイルに残した場合、メンテナンスのハードルが非常に高くなるので、陳腐化しやすい。
  3. 契約に必要:存在しないと契約不履行・揉める原因になるドキュメント
    • 「品質レベル:社外版」レベルで作成が必要だが、極論、納品前で良い。

考えのポイント

上記結論にたどり着く道中で、ポイントになったことを以下に示します。

コストを評価する

  • 「存在しないコスト」と「存在するコスト」を評価する
    • 存在しない場合に、『どの程度』のコストが発生するのか?
    • 存在した場合、メンテナンスのコストはどれくらい発生するのか?

※「コスト」は、短期的な「作成工数」「レビュー工数」はもちろん、長期的な「品質劣化に伴う障害対応工数」なども含まれる。
※「存在するコスト」「存在しないコスト」両方において、「開発者のストレス」も考慮した方が良い気がする。

「設計」と「設計書作成」を分ける

ドキュメントの修正に時間が取られ、本来やるべき「設計」の時間が削られるのは、望ましくない。
「設計」にフォーカスするなら、ドキュメントを書くより、ホワイトボードに書きながら対面で会話しながらやる方が質が高いモノになる。(はず)

みんなで「設計」、個人で「設計書作成」ってのが望ましいと思う。「設計書作成」は、納品前・運用開始前でも十分。
Let's 「モブ設計」。

メンテナンスのハードル

運用系ドキュメントは、「メンテナンスしやすい」がとても大事。
ファイルサーバーにおいておくと、「これ、修正して良いのかな・・・」的な問題が起きやすい気がする。
なので、ConfluenceやQiita:Teamなど、「誰でもコメントしやすい」状況を作れるツールを使うのが望ましい。

ドキュメント本体の修正には時間とかハードルとか色々あるけど、コメントなら「Aの部分は現在Bになってます」って簡単に書き込める。

ドキュメントツール
感覚的には、以下のような使い分けが望ましいと思います。

  • Confluenceなどコメントしやすいツール: 「基本的な考え方」や「運用系のメンテが必要な情報」はこちら
  • markdown: 「外部設計」など、差分管理が重要な情報。BDDをやるなら、BDDテストコードを代わりに利用できると嬉しい。
  • ソースコード: 「コード」「テストコード」「コミットログ」「コードコメント」で書くものを分けるべし。

適用に向けて

実際問題としては、この断捨離を現場に適用するのが一番難しい。
その中でも一番難しいのが、上司への説明。
そこさえ乗り切ればあとはトライアンドエラーでいけるはず。

ということで、「適用のために上司を説得するには」、私は以下が必要だと考えました。

  • この変更が「QCD」にどのような影響をもたらすのか?
  • 日頃の「信頼貯金」

QCDの説明

CDは、説明しやすいと思う。どれくらい現状に無駄があるのかを示せば良いだけだから。
Qは、「内部設計工程で検出した不具合」のウチ、本番障害として発生するのはどれくらいあるのか?を評価すれば良いと思う。

たぶんだけど、「ほとんど品質は悪化しない」ケースが多い気がする。なぜなら外部設計レベルのテストさえ網羅できていれば、本番障害にはならないから。
(その分、結合テスト工数がかかるけど、内部設計を丁寧に仕上げるよりは、低コストで済む気がする。)

※当然、データ準備等で、結合テストはすごく大変なんだ!という話なら別ですが、その場合ももう少し具体的に考えたいところ。例えば、データ準備が大変なら、「データに関するバリエーションだけ残ってればいいよね?」とか。
※品質評価を「step数あたりの不具合数」ではなく、「ケースの充足度」と「ケースの合格率」だけで評価するように変える必要がありそう。
※内部設計工程の品質保証について明言したいなら、内部設計工程を行なうのではなく、TDDを導入する方がよっぽど解決策だと思う。

信頼貯金

必ずしも自分の信頼貯金である必要はないと思っています。
影響を与えたい人に対して、信頼貯金のある人。その信頼貯金のある人に対して、信頼貯金のある人。って追ってけばいい気がする。

※ただし、曲がりなりにも「上司は話を聞く気がある」という文化が必要。

そもそもこれは社内政治の話だし、ここで話したい内容じゃないので、ここまでにして置きます。

さいごに

ちなみに、イケてるスタートアップや、R&D部門とかだと、ドキュメンテーション関連の問題ってどう解消してるんだろう?
プロダクト系の話は結構聞くけど、こういう泥臭い部分の話ってあんまり聞かない気がするので気になる。

こういう話あるよ!ってのがあれば教えていただけると嬉しいです。

以上です。

参考情報

設計書自体の書き方

そもそも設計書を書くということになったら読んでおきたい記事

その他


  1. 全然関係ないけど、「バーフバリ」に空見してしまった。ってのを書きたくなって止めれなかった。映画はレンタルして見たよ。 

  2. 開発はよかったけど。保守で問題が発生:問い合わせ対応がしづらくなってしまった。 

  3. まぁ別に「断捨離」に無理やり当てはめることがゴールでは無いんですが、もう少し分かりやすく書ければ良かったなぁという意味の反省です。 

その機械学習プロセス、自動化できませんか?

ここ数年、機械学習を使った研究開発やアプリケーション作成、データ分析がしやすい環境が整ってきました。機械学習フレームワークとしては、scikit-learn や TensorFlow が整備され、各クラウドベンダーからは機械学習用APIや学習/運用用のインフラが提供され、誰でも最先端の機械学習に触れられる時代になりました。

このような環境で自社の競争力を強化するには、機械学習プロセスの最適化による生産性向上が一つの手です。研究開発においては、アルゴリズムの開発をすばやく行わなければ競合に先んじられ、論文を書かれてしまうでしょう。また、アプリケーション作成やデータ分析業務での生産性向上が競争力強化に役立つのは言うまでもありません。

そこで本記事では、機械学習プロセスを自動化する技術であるAutomated Machine Learning(AutoML)の概要について紹介します。具体的には、AutoMLとは何か、なぜ必要か、どんなことができるのか、といった話を説明し、次の行動に繋げられるようにします。記事の構成としては、最初に一般的な機械学習プロセスについて説明し、次にAutoMLについて説明します。最後にAutoMLの将来について述べます。

一般的な機械学習プロセス

本記事をご覧の方はご存知の通り、「機械学習はアルゴリズムにデータを与えるだけで何か良い結果が出る」というものではありません。性能を出すためには多くの作業が必要です。典型的な作業には、データ収集、データクリーニング、特徴エンジニアリング、モデル選択、ハイパーパラメータチューニング、モデルの評価といった作業が含まれます。下のような図を一度は見たことがあるでしょう。

一般的な機械学習プロセス
出典: Evaluation of a Tree-based Pipeline Optimization Tool for Automating Data Science

最近よく使われているディープラーニングでも多くの作業が必要なことには変わりありません。確かに、ディープラーニングではモデルが特徴を学習してくれるため、特徴エンジニアリングの労力は減るかもしれません。しかし、プロセス全体は伝統的な機械学習と同様なので、データの前処理やハイパーパラメータのチューニングが必要なことには変わりません。また、高い性能を出すためには、ニューラルネットワークのアーキテクチャ設計に多くの時間を費やす必要があります。

このように、機械学習には多くの作業が必要で時間がかかることから自動化技術の重要性が増しています。

AutoMLとは?

AutoML(Automated Machine Learning)は、機械学習プロセスの自動化を目的とした技術のことです。機械学習に多くの作業が必要なのは先に述べたとおりですが、AutoMLでは機械学習の各プロセスを自動化してエンジニアの生産性を向上させること、また誰でも機械学習を使えるようになることを目指しています。したがって、究極的な目標は、生データを与えたら、何らかの処理をして、良い結果を出すことだと言えるでしょう。

データサイエンティストの数が急激に増えていることもAutoMLを後押しする背景となっています。以下の図はLinkedInでデータサイエンティストの数を調査した結果です。図を見ると、その数が指数関数的に増えていることがわかります。2010年から2015年の5年間でおよそ2倍、2018年までなら推定で8倍に増えています。

データサイエンティストの増加
出典: Study Shows That the Number of Data Scientists Has Doubled in 4 Years

データサイエンティストの数が急激に増えたことで、高度な分析をできる人手が足りていないという現状があります。そのような人手不足を補うために、データ分析や機械学習の適応経験が少ないエンジニアを助けるようなツールが必要となってきました。その目的に合致するのがAutoMLというわけです。

ここまでで、AutoMLの重要性が増している理由について述べました。次節からは各プロセスで従来行われている処理とAutoMLによる効率化について見ていきましょう。

AutoMLで行われること

本節では機械学習の各プロセスでAutoMLがどのように生産性向上に寄与するかを説明します。対象とするプロセスは、ハイパーパラメータチューニング、モデル選択、特徴エンジニアリングの3つです。各プロセスについて、何をするプロセスなのか、なぜその処理が必要か、従来どうしていたか、AutoMLではどうしているかの3点から説明します。

出典: https://arxiv.org/pdf/1603.06212.pdf
出典: Evaluation of a Tree-based Pipeline Optimization Tool for Automating Data Science

ハイパーパラメータチューニング

ハイパーパラメータチューニングは、ハイパーパラメータを最適な値に調整するプロセスです。各機械学習モデルには様々なハイパーパラメータが存在します。たとえば、ランダムフォレストなら木の深さや数をハイパーパラメータとして持っています。これらのハイパーパラメータはデータから学習するものではなく、モデルを学習させる前に設定しておく必要があります。

ハイパーパラメータチューニングが必要な理由として、機械学習フレームワークのデフォルトのパラメータでは良い性能が出ないことが多いという点を挙げることができます。以下の図は、scikit-learn に含まれる様々な機械学習アルゴリズムについて、ハイパーパラメータをデフォルト値からチューニングしたときに、性能がどれだけ向上したかを表しています。

ハイパーパラメータチューニングの効果
出典: Data-driven Advice for Applying Machine Learning to Bioinformatics Problems

結果を見ると、アルゴリズムによって改善の度合いは異なりますが、ハイパーパラメータをチューニングすることで性能が向上することがわかります。平均的には正解率で3〜5%程度の改善が見られたという結果になっています。つまり、ハイパーパラメータは明らかにチューニングする価値があり、デフォルトのパラメータを信用し過ぎるべきではないということを示唆しています。

チューニングによって性能向上が見込めるとはいえ、数多くのハイパーパラメータを手動でチューニングするのは骨が折れる作業です。たとえば、ハイパーパラメータ数が5個あり、各ハイパーパラメータに対して平均で3つの値をテストするのだとすれば、組合せは3の5乗(=273)通り存在します。これらすべての組み合わせに対して手動でチューニングをするのは非生産的な行為です。また、以下の図のようにモデルが複雑化すれば、手動でのチューニングは現実的ではなくなります。

Residual Network
出典: Deep Residual Learning for Image Recognition

そこで、AutoMLでは従来人手で行っていたハイパーパラメータチューニングを自動化することを考えます。自動化により、チューニングの効率が向上するだけでなく、人が直感的に決めたパラメータによるバイアスを取り除くことにも繋がります。具体的には以下のような手法が使われています。

  • グリッドサーチ(GridSearch)
  • ランダムサーチ(RandomSearch)
  • ベイズ最適化(Bayesian Optimization)

このうち、最もよく使われているのはグリッドサーチとランダムサーチでしょう。それらの違いは以下の図のように表されます。

グリッドサーチとランダムサーチの違い
出典: Random Search for Hyper-Parameter Optimization

グリッドサーチは、伝統的によく使われているハイパーパラメータチューニングの手法で、あらかじめ各ハイパーパラメータの候補値を複数設定して、すべての組合せを試すことでチューニングします。たとえば、$C$ と $\gamma$ という2つのパラメータがあり、それぞれ、$C \in$ {10, 100, 1000}, $\gamma \in$ {0.1, 0.2, 0.5, 1.0}という候補値を設定した場合、3x4=12の組み合わせについて試します。

一方、ランダムサーチはパラメータに対する分布を指定し、そこから値をサンプリングしてチューニングする手法です。たとえば、グリッドサーチでは $C$ に対して $C \in$ {10, 100, 1000} のような離散値を与えていたのに対して、ランダムサーチでは、パラメータ $\lambda=100$ の指数分布のような確率分布を与え、そこから値をサンプリングします。少数のハイパーパラメータが性能に大きく影響を与える場合に効果的な手法です。

グリッドサーチやランダムサーチの課題として、見込みのないハイパーパラメータに時間を費やしがちな点を挙げることができます。この原因としては、グリッドサーチやランダムサーチでは以前に得られた結果を利用していない点を挙げられます。

では、以前に得られた結果を利用すると、どのようにハイパーパラメータを選べるのでしょうか? 以下の図を御覧ください。この図はランダムフォレストのハイパーパラメータの一つであるn_estimatorの数を変化させたときの性能を示しています。スコアの値が高い方が性能が良いのだとすれば、n_estimatorが200近辺を探索するより、800近辺を探索した方が効率が良さそうな事がわかると思います。

image.png
出典: A Conceptual Explanation of Bayesian Hyperparameter Optimization for Machine Learning

最近使われるようになってきたベイズ最適化を用いたハイパーパラメータチューニングは、以前の結果を使って次に探索するハイパーパラメータを選ぶ手法です。これにより、有望そうなところを中心にハイパーパラメータを探索することができます。人間が行う探索に近いことをしているとも言えるでしょう。ディープラーニングを含む機械学習のモデルに対して、比較的良いハイパーパラメータを探索できることが知られています。

ベイズ最適化の仕組みについてこれ以上詳しく解説するとこの記事では終わらないので、最後に最適化に使えるソフトウェアを紹介しましょう。ここでは以下のソフトウェアを挙げました。

GridSearchCVRandomizedSearchCVはご存知 scikit-learn に組み込まれているクラスです。scikit-learn を使っている場合には一番使いやすいのではないかと思います。 scikit-learn に限らず使いたいなら、ParameterGrid を使うのが選択肢に挙がると思います。ParameterGrid を使うことでハイパーパラメータの組み合わせを生成することができます。

hyperopt はランダムサーチとベイズ最適化によるハイパーパラメータチューニングを行えるPythonパッケージです。ベイズ最適化の方はTPEと呼ばれるアルゴリズムをサポートしています。hyperas はKeras用のhyperoptラッパーです。私のようにKerasをよく使うユーザにはこちらの方が使いやすいと思います。

モデル選択

モデル選択は、データを学習させるのに使う機械学習アルゴリズムを選ぶプロセスです。モデルにはSVMやランダムフォレスト、ニューラルネットワークなど多くの種類があります。その中から、解きたい問題に応じて選びます。たとえば、解釈性が重要な場合は決定木などのモデルが選ばれるでしょうし、とにかく性能を出したいという場合はニューラルネットワークが候補になるでしょう。

モデル選択が必要な理由として、すべての問題に最適な機械学習アルゴリズムは存在しないという点を挙げることができます。以下の図は165個のデータセットについて、モデル間の性能の勝敗について検証した結果を示しています。左側の列はモデルの名前が書かれており、良い結果となったモデルから順に並んでいます。

出典: https://arxiv.org/pdf/1708.05070.pdf
出典: Data-driven Advice for Applying Machine Learning to Bioinformatics Problems

結果を見ると、Gradient Tree Boosting(GTB)やランダムフォレスト、SVMは良く、逆にNBは悪いことがわかります。また、ほとんどの場合においてGradient Tree Boostingは良い結果なのですが、NBでも1%のデータセットではGTBに勝っているという結果になっています。ちなみに、足しても100%にならないのは、少なくとも正解率で1%以上上回った場合を勝利としているからです。

要するに何が言いたいかというと、確かにGTBやRandomForestは良い結果を出しますが、すべての問題で勝てる最適なアルゴリズムは存在しないということです。これは機械学習を行う上で重要な点で、機械学習を使って問題を解く際には、多くの機械学習アルゴリズムについて考慮する必要があるということをこの実験結果は示唆しています。

ただ、実際のプロジェクトでは多くの機械学習アルゴリズムを考慮できているとはいい難い状況です。その原因の一つには、人間のバイアスが関係しています。たとえば、「GTBは毎回良い結果を出すからこれを使っておけばいいんだ」というのは一つのバイアスです。確かにそれはたいていの場合正しいかもしれません。しかし、上図が示すように、実際には人間のバイアスは悪い方向に働くこともあるのです。

人間のバイアスを軽減させるために有効な手の一つとして、データセットの特徴に応じて選ぶモデルを決定する仕組みを構築しておく手があります。以下は scikit-learn が公開している機械学習アルゴリズムを選択するためのチートシートです。ただ、この方法にもシートを作成した人のバイアスが入っている、多くのアルゴリズムを考慮できていないといった問題があります。

model_selection.png
出典: Choosing the right estimator

そういうわけでAutoMLでは機械学習アルゴリズムの選択を自動的に行うことを考えます。モデル選択を自動化することにより、人間のバイアスを排除しつつ、様々なモデルを考慮することができます。モデル選択についてはハイパーパラメータチューニングと切り離せない話なので、話としてはここまでにしておきます。

お話だけだと退屈なので、ここでソフトウェアを紹介しましょう。モデル選択の機能を組み込んだソフトウェアは商用・非商用問わずに数多くありますが、今回はその中から TPOT を紹介します。

TPOT は、scikit-learnライクなAPIで使えるAutoMLのツールです。機能としてはモデル選択とハイパーパラメータチューニングを行ってくれます。また、タスクとしては分類と回帰を解くことができます。

scikit-learnを使ったことがある人であれば、TPOTを使うのは非常に簡単です。たとえば、分類問題を解く場合は、TPOTClassifierをインポートし、データを与えて学習させるだけです。以下ではMNISTを学習させています。

from tpot import TPOTClassifier
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

digits = load_digits()
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target,
                                                    train_size=0.75, test_size=0.25)

tpot = TPOTClassifier(generations=5, population_size=20, verbosity=2)
tpot.fit(X_train, y_train)
print(tpot.score(X_test, y_test))

学習が終わると以下に示すように最も良かったパイプラインとそのスコアを表示します。今回の場合、入力にPolynomialFeaturesを適用した後、モデルにLogisticRegressionを使うのが最も良い結果になりました。ハイパーパラメータが設定されていることも確認できます。スコア自体はよくありませんでしたが、手軽さは確認できたかと思います。

Generation 1 - Current best internal CV score: 0.9651912264496657                                                                                                   
Generation 2 - Current best internal CV score: 0.9822200910854291                                                                                                   
Generation 3 - Current best internal CV score: 0.9822200910854291                                                                                                   
Generation 4 - Current best internal CV score: 0.9822200910854291                                                                                                   
Generation 5 - Current best internal CV score: 0.9822200910854291                                                                                                   

Best pipeline: LogisticRegression(PolynomialFeatures(input_matrix, degree=2, include_bias=False, interaction_only=False), C=15.0, dual=True, penalty=l2)
0.9844444444444445

ニューラルアーキテクチャサーチ

モデル選択と関係する話として、最近よく話題になるニューラルアーキテクチャサーチ(Neural Architecture Search: NAS)について述べておきましょう。NASもAutoMLの一部と捉えられます。ニュースでも大きく取り上げられ、New York Timesでは「AIを構築できるAIを構築する」(Building A.I. That Can Build A.I.)というタイトルで記事が書かれています。

スクリーンショット 2018-12-03 9.56.03.png

ニューラルアーキテクチャサーチとは、ニューラルネットワークの構造設計を自動化する技術です。実際には、ニューラルネットワークを使ってネットワークアーキテクチャを生成し、ハイパーパラメータチューニングをしつつ学習させています。

基本的な枠組みは以下の図のようになっています。まず、コントローラと呼ばれるRNNがアーキテクチャをサンプリングします。次に、サンプリングした結果を使って、ネットワークを構築します。そして、構築したネットワークを学習し、検証用データセットに対して評価を行います。この評価結果を使って、より良いアーキテクチャを設計できるようにコントローラを更新します。以上の操作を繰り返し行うことで良いアーキテクチャを探索しています。

スクリーンショット 2018-12-03 10.14.50.png
出典: Neural Architecture Search with Reinforcement Learning

ニューラルネットワークの設計を自動化したいのにはいくつかの理由があります。その一つとしてニューラルネットワークのアーキテクチャを設計するのは高度な専門知識が必要で非常に難しい点を挙げられます。よいアーキテクチャを作るためには試行錯誤が必要で、これには時間もお金もかかります。これでは活用できるのが少数の研究者やエンジニアだけに限られてしまいます。

このような理由からニューラルアーキテクチャサーチで設計から学習まで自動化しようという話になりました。NASによって、アーキテクチャの設計、ハイパーパラメータチューニング、学習を自動化することができ、誰にでも利用できるようになります。これはつまり、ドメインエキスパートによるニューラルネットワークの活用に道が開かれることを意味しています。

そんなNASの課題としては計算量の多さを挙げられます。たとえば、Neural Architecture Search with Reinforcement Learningでは、アーキテクチャを探索するのに 800 GPUで28日間かかっています。また、NASNetでは、500 GPU を使用して4日間かかっています。これでは一般の研究者や開発者が利用するのは現実的ではありません。

高速化の手段の一つとして使われるのが転移学習です。Efficient Neural Architecture Search via Parameter Sharingではすべての重みをスクラッチで学習させるのではなく、学習済みのモデルから転移学習させて使うことで高速化をしています。その結果、学習時間は 1 GPU で半日までに抑えられています。

最後にNASを提供しているサービスとOSSについて紹介します。

NASを提供するサービスとして最も有名なのはGoogleの Cloud AutoML でしょう。Cloud AutoMLでは画像認識、テキスト分類、翻訳に関して学習させることができます。データさえ用意すれば、誰でも簡単に良いモデルを作って使えるのが特徴です。一方、お金が結構かかるのと、学習したモデルをエクスポートできないのが欠点です。

NASに使えるOSSとしてはAuto-Kerasがあります。Auto-Kerasは、scikit-learnライクなAPIで使えるオープンソースのAutoMLのツールです。こちらは、Texas A&M大学のDATA Labとコミュニティによって開発されました。論文としては、2018年に発表された「Auto-Keras: Efficient Neural Architecture Search with Network Morphism」が基となっています。目標としては、機械学習の知識のないドメインエキスパートでも簡単にディープラーニングを使えるようにすることです。

Auto-Kerasもscikit-learnを触ったことがある人であれば使うのは簡単です。以下のようにして分類器を定義し、fitメソッドを使って学習させるだけです。以下のコードはAuto-KerasにMNISTを学習させるコードです。fitメソッドで学習をして最適なアーキテクチャを探索します。その後、final_fitで探索を終えて得られた最適なアーキテクチャで学習し直します。

import autokeras as ak
from keras.datasets import mnist


if __name__ == '__main__':
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    x_train = x_train.reshape(x_train.shape + (1,))
    x_test = x_test.reshape(x_test.shape + (1,))

    clf = ak.ImageClassifier(verbose=True)
    clf.fit(x_train, y_train, time_limit=12 * 60 * 60)
    clf.final_fit(x_train, y_train, x_test, y_test, retrain=True)
    print(clf.evaluate(x_test, y_test))

学習を始めると以下のような表示がされます。Father Modelに親モデルのIDが書いてあり、その隣に変更点が書いてあります。そして、学習が終わるとその結果がLossMetric Valueに表示されます。このモデルの場合は、正解率で0.9952だったことを表しています。

+----------------------------------------------+
|               Training model 8               |
+----------------------------------------------+

+--------------------------------------------------------------------------+
|    Father Model ID     |                 Added Operation                 |
+--------------------------------------------------------------------------+
|           6            |    to_deeper_model 68 Conv2d(512, 512, 5, 1)    |
+--------------------------------------------------------------------------+
Saving model.
+--------------------------------------------------------------------------+
|        Model ID        |          Loss          |      Metric Value      |
+--------------------------------------------------------------------------+
|           8            |  0.07076152432709933   |         0.9952         |
+--------------------------------------------------------------------------+

特徴エンジニアリング

特徴エンジニアリング(Feature Engineering)は、機械学習アルゴリズムがうまく学習できるような特徴を作成するプロセスのことです。特徴エンジニアリングは機械学習を使ったシステムを作る際の基礎であり、ここで良い特徴を得られれば、機械学習アルゴリズムでも良い性能を出すことができます。ただし、良い特徴を得るのは非常に難しく、時間のかかる部分でもあります。

機械学習における特徴(Feature)とは、観察される現象の測定可能な特性のことです。以下の例は、タイタニック号の生存者情報を含むデータセットの一部です。各列は、分析に使用できる測定可能なデータである年齢(Age)、性別(Sex)、料金(Fare)などを含みます。これらがデータセットの特徴量です。

タイタニック号の生存者情報を含むデータセット

特徴エンジニアリングが重要な理由として、良い特徴を得ることで機械学習アルゴリズムの予測性能が大きく変わる点を挙げることができます。たとえば、タイタニック号のデータセットの例で言うなら、乗客名をそのまま特徴として使っても良い性能は得られないでしょう。しかし、名前から敬称(Mr. Mrs. Sir.など)を抽出して使えば性能向上に役立つでしょう。なぜなら、社会的地位の高さや既婚者か否かといった情報は救命ボートに乗る際に考慮されただろうと考えられるからです。

伝統的に、特徴エンジニアリングは人間によって行われてきましたが、それには2つの問題点がありました。一つは良い特徴を思いつくのは難しいという点です。良い特徴を思いつくためにはドメインの知識が必要な場合もあり、一筋縄ではいきません。もう一つは、時間がかかるという点です。単に思いつくだけではなく、それを検証することも含めると、特徴エンジニアリングは非常に時間のかかる作業です。実際に機械学習のどの部分で時間がかかるかをデータサイエンティストに尋ねると特徴エンジニアリングは上位に位置します。

AutoMLでは、人間によって行われてきた特徴エンジニアリングを自動化します。これにより、先に述べた2つの問題を軽減することができます。以下では実際にAutoMLにおいて特徴エンジニアリングがどのように行われるのかについて説明します。ここでは、AutoMLのサービスである DataRobot での自動化と、特徴エンジニアリングを自動化するためのツールである featuretools を例にとって説明しましょう。

まずはAutoMLのサービスである DataRobot ではどうしているかを紹介しましょう。以下の述べる内容は、DataRobotのブログ記事「Automated Feature Engineering」に基づいています。2018年6月28日の記事ですが内容が古くなっている可能性はありますので、その点はご留意ください。

DataRobotではエキスパートシステムを構築することで特徴エンジニアリングを自動化しています。具体的には以下のようなことをしています。

  • 特徴の生成
  • 特徴エンジニアリングが必要なモデルを知る
  • 各モデルに有効な特徴エンジニアリングの種類を知る
  • システマティックにモデルを比較して、特徴エンジニアリングとモデルの最も良い組み合わせを知る

これらの操作をDataRobotでは model blueprint を使って行っています。ここで、model blueprint とは、こちらの記事によると、前処理、特徴エンジニアリング、学習、チューニングといった処理のシーケンスのことのようです。以下が model blueprint の例です。

image.png
出典: Automated Feature Engineering

この model blueprint では、DataRobotのシステムはL2正則化を入れたロジスティック回帰に対するデータを用意しています。具体的に行っている特徴エンジニアリングとしては、One-Hotエンコーディング、欠損値の補完、標準化の3つです。

より複雑な例としては以下の Gradient Boosted Greedy Treesに対する model blueprint があります。

image.png
出典: Automated Feature Engineering

これまでのところをまとめましょう。まず、DataRobotでは前処理からチューニングまでのパイプラインを定義したmodel blueprintが多数用意されていいます。それらのmodel blueprintをデータに対して適用し、その結果を比較して最も良いモデルと特徴エンジニアリングの組み合わせを決めているということをしているようです。

DataRobotの方法も良いのですが、作り込みが必要で真似しにくい感じのやり方なので featuretools についても紹介しておきます。featuretools はPython製のオープンソースの特徴エンジニアリング自動化ツールです。featuretools を使うことで特徴を自動的に生成することができます。

featuretools では Deep Feature Synthesis(DFS) と呼ばれる方法で新たな特徴を生成しています。DFSでは primitive と呼ばれる関数を使ってデータの集約と変換を行います。primitive の例としては、列の平均や最大値を取る関数を挙げることができます。また自分で定義した関数を primitive として使うこともできます。

百聞は一見にしかずということで、実際にやってみましょう。まずはデモ用のカスタマートランザクションデータを生成します。

>>> import featuretools as ft
>>> es = ft.demo.load_mock_customer(return_entityset=True)
>>> es
Entityset: transactions
  Entities:
    transactions [Rows: 500, Columns: 5]
    products [Rows: 5, Columns: 2]
    sessions [Rows: 35, Columns: 4]
    customers [Rows: 5, Columns: 3]
  Relationships:
    transactions.product_id -> products.product_id
    transactions.session_id -> sessions.session_id
    sessions.customer_id -> customers.customer_id

生成されたデータには4つのテーブル(transactions, products, sessions, customers)と3つの関係が定義されています。このうち、customersテーブルを対象にprimitiveを適用して特徴を生成します。以下がそのコードです。

>>> feature_matrix, features_defs = ft.dfs(entityset=es, target_entity="customers")
>>> feature_matrix.head(5)
            zip_code  COUNT(sessions)                  ...                   MODE(sessions.MONTH(session_start)) MODE(sessions.WEEKDAY(session_start))
customer_id                                            ...                                                                                            
1              60091               10                  ...                                                     1                                     2
2              02139                8                  ...                                                     1                                     2
3              02139                5                  ...                                                     1                                     2
4              60091                8                  ...                                                     1                                     2
5              02139                4                  ...                                                     1                                     2

features_defsから以下のような特徴を自動的に生成していることがわかります。 ここで、MODESUMSTDというのが primitive です。つまり、SUM(transactions.amount)transactionsテーブルのamount列の合計を特徴として加えるということを意味しています。

>>> pprint(features_defs)
[<Feature: zip_code>,
 <Feature: COUNT(sessions)>,
 <Feature: NUM_UNIQUE(sessions.device)>,
 <Feature: MODE(sessions.device)>,
 <Feature: SUM(transactions.amount)>,
 <Feature: STD(transactions.amount)>,
 <Feature: MAX(transactions.amount)>,
 <Feature: SKEW(transactions.amount)>,
 <Feature: MIN(transactions.amount)>,
 <Feature: MEAN(transactions.amount)>,
 <Feature: COUNT(transactions)>,
 ...
 <Feature: MODE(sessions.WEEKDAY(session_start))>]

featuretoolsではこのように特徴を生成することで、特徴エンジニアリングにかかる時間を軽減することができます。

AutoMLのソフトウェア

AutoMLのための数多くのソフトウェアやサービスがすでに公開されています。ここでは、OSSと非OSSという2つの区分で分けると以下に列挙するソフトウェアがあります。

OSS

非OSS

AutoMLの将来

最後はAutoMLの将来についての私見です。まず、AutoMLは将来的にはデータクリーニングのプロセスも扱えるようになるのではないかと考えています。たとえば、テキストのような非構造化データを分析にすぐに使えるようにテーブルデータに変換するといったことです。次に、大規模データにスケールするような方法が出てくるでしょう。Cloud AutoMLを試してみるとわかるのですが、サンプルの小さなデータに対してでさえ計算時間が結構かかります。将来的にはいわゆるビッグデータに対しても使えるようになるでしょう。最後に、性能が人間を上回るようになるでしょう。現在でも一部のデータセットでは人間に匹敵する性能を出していますが、将来的には人間が考えつかないような特徴であるとかネットワークアーキテクチャを生み出せるようになるでしょう。そういう意味では、Alpha Goに似たところはあるかもしれません。

おわりに

ここ数年で最先端の機械学習に触れやすい環境ができてきました。このような環境で競争力を強化するには、機械学習プロセスの最適化が一つの手です。本記事では、機械学習プロセスを自動化する技術であるAutoMLの概要について紹介しました。本記事で紹介した内容以外にも、環境構築やモデルのデプロイといった部分の効率化も必要ですが、そのへんはまたの機会にしましょう。本記事が皆様のお役に立ったのであれば幸いです。

私のTwitterアカウントでも機械学習や自然言語処理に関する情報をつぶやいています。
@Hironsan

この分野にご興味のある方のフォローをお待ちしています。

参考資料

特徴エンジニアリング

ニューラルアーキテクチャサーチ

ハイパーパラメータチューニング

今さらながらにElasticStackで可視化について

プロローグ

ある日のこと、サービスを開発・運用しているチームの同期にこんな依頼をされた。
「システムからメールが送れてないみたいなので、調査を手伝ってほしい」
聞けば、システムからのメール送信は外部のメールサービスを利用していて、そこにログとして情報は出ているのだが、テキストであるログから情報を抽出してくるにはたいへんな労力がかかるとのことだった。
そこで、システムのメール送信状況をElastic Stackを利用して可視化することになった。

構成

system_freehand.png
システムの構成は上図のようなものである。

  • シェルを実行してインターネット経由でメールサービスからログを取得
  • 情報を加工(マスク等)した後、ファイルを配置
  • Filebeatでファイルを読ませ、Logstashに送る
  • Logstashで情報をフィールドに分割し、Elasticsearchに送る
  • Elasticsearchの情報をKibanaで可視化

(今回はElastic Stackをすでに立てていたため相乗りする形としたが、不要ならFilebeatとLogstashはなくして直接Elasticsearchに送信でも可)

LogstashやElasticsearchの設定などは他に良い記事がたくさんあると思うので、今回は割愛。

結果

mail.png
どのくらいメール送信が失敗しているかや、挙動が変化したことなどが一目瞭然。
上図は送信に失敗したメールを失敗原因の分類別に積み上げたグラフであり、原因が水色だったものが9/5ごろなくなっているのがわかる。
typo.png
Elasticsearchにデータを入れておくと、Kibana上で様々な条件でのデータの抽出が簡単に行えるため、データの分析がしやすくなる。例えば、上図はメールアドレスをtypoしていてそもそも届かないデータを抽出したもので、こういったそもそもメールが届かないデータが存在していることを知ることができる。
ElasticsearchのデータをKibanaで分析することで、新たな仮説を立て、さらなるサービス改善につなげることができる。

まとめ

システムの各所でログを出力させてても、それを適切に活用できていないことが多いのではないでしょうか。せっかくログを出してても、障害時の原因調査にしか使ってない。それではログがかわいそうです。
情報は活かしてなんぼ。活かすためには可視化!
Elastic Stackを使えば、お手軽に可視化できます!

エピローグ

Elastic Stackを使ってメールの送信状況を可視化することで、楽に問題の調査をすることができた。
後日... 「今度は問題あったとき通知されるようにできないかな?」
有償版のKibanaでは、機械学習と検知機能で異変を自動的に察知してくれる機能があるらしい。それを利用すれば、常にダッシュボードを確認していなくても異変を知らせてくれるようになる。
しかし、それはまた別のお話。

システム運用の世界をグラフで表現すると良いことあるかも?Neo4jのメリット感を体験-パッケージ依存関係管理-

先日、Neo4jの認定プロフェッショナルを取得し、ますますNeo4j(グラフDB)への興味が高まりつつあります。
「グラフデータベースでできることって、結局はRDBでも同じようなことできるし。。」といった意見も多いかと思いますが、現実世界の事象をより直感的に扱えるなど、良い点もあるのではないかと考えています。

ということで、グラフデータベースを使うことによるメリットをシステム運用の世界を題材に体験してみます。

グラフデータベースとは?

ノードとリレーションおよびその内容を示すプロパティで表現されるグラフ構造を扱うことができるデータベースです。関係性を表現するのに非常に扱いやすいものかと思います。
現実の世界で見ると、あらゆるものが何らかの関係性を持って働いています。人と人との関係性、人と会社の関係性、道路、電車、ユーザと取引履歴、等々。
いろいろと他に参考になる情報はあるので細かいところは割愛します。

参考: Graph DBとはなにか

試したこと

システム運用の世界だと、ネットワークの構成、サーバ構成等々構成情報の管理には非常に適しているのではないかと思います。

今回は、お試しということでLinuxサーバにインストールされているソフトウェア・ミドルウェア等のパッケージの依存関係のデータを収集し、Neo4jに登録、パッケージ間の関係性やパッケージの重要度などを分析してみます。

パッケージの依存関係情報の抽出

今回は、rpmインストールされているものに限定し、パッケージとそのパッケージが依存しているパッケージの情報を吸い出します。

こんな感じのスクリプトを一度流して、以下のようなCSV形式でパッケージとそのパッケージが依存するパッケージのリストを生成します。

get_package_dependlist.sh
#!/bin/bash

echo "package,depend_package"
IFS=$'\n';
for PACKAGE in `rpm -qa`
do
  depend_list=()
  for DEPEND in `rpm -q --requires ${PACKAGE} | cut -d ' ' -f 1`
  do
    pkg=`rpm -q --whatprovides ${DEPEND}`
    if [ $? -eq 0 ]; then
      depend_list=("${depend_list[@]}" ${pkg})
    fi
  done
  for depend in `echo "${depend_list[*]}" | sort | uniq`; do
    if[ ${PACKAGE} != ${depend} ]; then
      echo "${PACKAGE},${depend}"
    fi
  done
done
実行
$ bash get_package_dependlist.sh > package.csv
packages.csv
package,depend_package
gmp-4.3.1-13.el6.x86_64,glibc-2.12-1.212.el6.x86_64
gmp-4.3.1-13.el6.x86_64,libgcc-4.4.7-23.el6.x86_64
gmp-4.3.1-13.el6.x86_64,libstdc++-4.4.7-23.el6.x86_64
coreutils-8.4-47.el6.x86_64,bash-4.1.2-48.el6.x86_64
coreutils-8.4-47.el6.x86_64,coreutils-8.4-47.el6.x86_64

Neo4jに取り込み

Neo4jにはCSVからデータをロードできる機能があります。
作成したpackage.csvをNeo4jのサーバ上に配置し、以下のようなCypherクエリでLoadします。
このとき、ノードにはラベル「Package」を付与し、プロパティのnameにパッケージ名を設定します。

LOAD CSV WITH HEADERS FROM "file:/packages.csv" AS line
MERGE (a:Package { name: line.package })
MERGE (b:Package { name: line.depend_package })
MERGE (a)-[:depend]->(b)

/var/lib/neo4j/importにpackages.csvを配置し、各行を取り出し、packageとdepend_packageをそれぞれノードとして登録し、packageからdepend_packageにdependのリレーションを付与しています。

出来上がるとこんな感じです。

init.png

いろいろと分析をしてみる

グラフDBに入ってしまえばいろんな分析ができます。

まずは簡単なところから。

パッケージAが依存しているパッケージと、さらにその依存パッケージが依存しているパッケージを確認

単純に一つの依存先だけなら先程のスクリプトのようにrpmコマンドとかで簡単に確認できますが、もう1ステップ先までみるとなるとちょっと手間なのでグラフDBにいれて簡単に取り出してみます。

パッケージmariadb-server-5.5.56-2.el7.x86_64の依存関係を取得するクエリ

MATCH (a:Package)-[r*1..2]->(b:Package) WHERE a.name = "mariadb-server-5.5.56-2.el7.x86_64" RETURN a,r,b

neo4j_2step.png

こんな感じで直接依存しているものと、さらにその先の依存しているものが抽出できます。

依存の多いパッケージのトップ5を確認

依存され度合いの高いパッケージのトップ5を確認してみます。
依存され度合いは、ノードへの入力dependリレーションの件数をカウントすればわかります。
件数をカウントしてorder byで多い順に並べ替えてlimit 5件取得で簡単に調べることができます。

MATCH (n:Package)<-[r:depend]-() RETURN n.name,count(r) AS depend_count ORDER BY depend_count DESC LIMIT 5

neo4j_depend_top5.png

name depend_count
glibc-2.17-222.el7.x86_64 241
bash-4.2.46-29.el7_4.x86_64 96
perl-5.16.3-292.el7.x86_64 59
zlib-1.2.7-17.el7.x86_64 56
perl-Carp-1.26-244.el7.noarch 41

glibcはいろいろなパッケージから依存されているのがわかりますね。

依存され度合いだけじゃなく、依存しているパッケージの数が多いものを調べるのみ上記の矢印方向を変えるだけで簡単にチェックできます。

MATCH (n:Package)-[r:depend]->() RETURN n.name,count(r) AS depend_count ORDER BY depend_count DESC LIMIT 5

このあたりがチェックできると、パッケージの変更時等に留意すべき対象がどのパッケージであるかなどさくっと確認できそうです。

似ているパッケージを探す

最後はちょっと応用です。パッケージ間で関係性が似ているものを探してみます。
関係性が似ているの定義として、今回は「依存関係を持つ先のパッケージが同様のものは似ている」とします。

例えば、パッケージAはパッケージC,D,Eに依存している、パッケージBはパッケージC,D,Fに依存しているといった場合、CとDの2つのパッケージが共通的に依存しています。
この度合いを、Overlap Similarityのアルゴリズムで算出してみます。

Overlap Similarityは以下の記事で解説されている「Simpson係数(Overlap coefficient)」と呼ばれる係数を算出するアルゴリズムです。

式としては以下のようになります。

パッケージAの依存するパッケージの集合AとパッケージBの依存するパッケージの集合Bとした時の係数(overlap(A,B))は以下の式で求められます。

overlap(A,B) = \frac{| A \cap B |}{min(|A|,|B|)}

Neo4jは様々なグラフアルゴリズムを簡単に算出するためのプラグインが提供されています。

https://github.com/neo4j-contrib/neo4j-graph-algorithms

このプラグインを使うと、以下のページで解説されているようなグラフ同士の類似性のチェックやクラスタの検出などが実現できます。

graph algorithmsのプラグインの導入

まずは、算出を試す前にプラグインを導入して使えるようにします。

1. プラグインのjarファイルをダウンロード・配置

https://github.com/neo4j-contrib/neo4j-graph-algorithms/releases

上記URLからjarをダウンロードします。
ダウンロードしたjarをNeo4jの導入サーバ内のNeo4jホームディレクトリ(/var/lib/neo4j)配下のpluginsに配置します。

$ cp graph-algorithms-algo-3.5.0.1.jar /var/lib/neo4j/plugins

2. neo4jの設定ファイルの変更

設定ファイル(/var/lib/neo4j/conf/neo4j.conf)にプラグインを読み込むための設定を行います。

neo4j.conf
・・・略
dbms.security.procedures.unrestricted=algo.\*

あとはNeo4jを再起動すればOKです。

類似性の分析

この状態で、以下の1Cypher queryを発行すれば指定のパッケージに類似しているもののスコアが簡単に算出できます。

MATCH (n:Package {name: "libcurl-7.19.7-53.el6_9.x86_64"})-[:depend]->(a)
MATCH (m:Package)-[:depend]->(b)
RETURN m.name AS package_name, algo.similarity.overlap(collect(distinct id(a)), collect(distinct id(b))) AS similarity ORDER BY similarity DESC LIMIT 10

ポイントは、algo.similarity.overlapという関数を呼び出しているところです。
先程のプラグインが有効になることで、algo.xxxという様々な関数が利用できるようになっています。
この中でOverlap Similarityのスコア算出用の関数がalgo.similarity.overlapです。この関数に2つの数値配列を渡すと、重複度合いをチェックしてスコア算出してくれます。
この時、配列には数値が登録されている必要があります。文字列の配列には対処していないので、上記例のように、ノードの名前ではなく、ノードのid情報の一覧を渡しています。

結果は以下のようになります。

package_name similarity
bridge-utils-1.2-10.el6.x86_64 1.0
libattr-2.4.44-7.el6.x86_64 1.0
e2fsprogs-libs-1.41.12-24.el6.x86_64 1.0
libudev-147-2.73.el6_8.2.x86_64 1.0
db4-4.7.25-22.el6.x86_64 1.0
ethtool-3.5-6.el6.x86_64 1.0
numactl-2.0.9-2.el6.x86_64 1.0
zlib-1.2.3-29.el6.x86_64 1.0
nspr-4.19.0-1.el6.x86_64 1.0

libcurlパッケージの依存先は以下。

libcurl.png

bridge-utilsパッケージの依存先は以下。

bridge.png

今回はあまり良い例ではないですね。。bridge-utilsの依存先パッケージはglibcのみで、そのglibcにlibcurlも依存しているため、1/1で1.0という結果に。。
Overlap Similarityは依存先パッケージが少ない方に合わせて計算するので上記のような結果に。

このアルゴリズムの場合、極端に少ないものがある場合その内容にひっぱられてしまうのでもう一つ別のJaccard係数を元にした類似度計算を行ってみます。

クエリは先程の関数をalgo.simirality.overlapをalgo.simirality.jaccardに変えるだけ。
このアルゴリズムでは以下の式で算出するので、一方の1依存パッケージのみだけであっても、その合算値で計算されるのでもう少しそれっぽいのが出てきそうです。

J(A,B) = \frac{| A \cap B |}{| A \cup B |}
MATCH (n:Package {name: "libcurl-7.19.7-53.el6_9.x86_64"})-[:depend]->(a)
MATCH (m:Package)-[:depend]->(b)
RETURN m.name AS package_name, algo.similarity.jaccard(collect(distinct id(a)), collect(distinct id(b))) AS similarity ORDER BY similarity DESC LIMIT 10
package_name similarity
libcurl-7.19.7-53.el6_9.x86_64 1.0
curl-7.19.7-53.el6_9.x86_64 0.9090909090909091
nss-tools-3.36.0-8.el6.x86_64 0.45454545454545453
openssh-5.3p1-123.el6_9.x86_64 0.375
openssh-clients-5.3p1-123.el6_9.x86_64 0.35294117647058826
nss-sysinit-3.36.0-8.el6.x86_64 0.3076923076923077
libkadm5-1.10.3-65.el6.x86_64 0.3
openssl-1.0.1e-57.el6.x86_64 0.2857142857142857
openldap-2.4.40-16.el6.x86_64 0.26666666666666666
nss-softokn-3.14.3-23.3.el6_8.x86_64 0.25

libcurl自分自身とは当然1.0になるとして、その次に高いのがcurlパッケージとなり、似たようなライブラリを利用してるのがわかる結果になりました。

まとめ

自然界の状態をグラフで表現することで、いろいろな視点から分析できる可能性が高まるような気がします。RDBMS使っても似たようなことはできるとは思いますが、データの持ち方として、グラフ構造に特化しているグラフDBはよりすばやく必要な結果を得ることに繋がるので、活用しどころはあると思います。

Javascriptテストツール"Jest"のMockを使ってみた

初めてのQiita投稿(かつ、アドベントカレンダー初参加)で遅刻をしてしまうという大チョンボをやらかしてしまいました...
この記事は新米エンジニアがJavaScriptのテストを行うにあたってハマった、JestのMock機能について調べた記事です。

はじめに

皆さんはJavaScriptのテストをする際、どのようなツールを使っていますか。
JavaScriptのテストでは、複数のテストツールを組み合わせてテストをすることが多いです。
以下がよく用いられるテストツール/フレームワークのようです。

  • テストフレームワーク
    • mocha
    • Jasmine
    • Jest
  • テストランナー
    • Karma
  • アサーションツール
    • power-assert
    • chai
  • テストダブル
    • Sinon.js

(上記のツール/フレームワークは、上記で分類した機能に特化しているわけではなく重複した機能を持つものもあります。)
そんなJavaScriptのテストツールの1つである"Jest"と、"Jest"のMock機能を使ってみたので紹介をします。

本記事で扱うこと

  • 簡単な"Jest"の紹介
  • 初級レベルのMock機能の紹介

本記事で扱わないこと

以下は、公式や別記事での記載も多いため割愛します。
(今後、別記事として記載したら紹介します。)

  • 導入方法
  • アサーションの紹介
  • UI Testやスナップショットテスト

Jestとは

Jest 公式
Facebook製のJavascriptテストツールで、Reactと相性が良いです。
公式にも"Zero configuration testing platform"とあるように、configを書かずにテストが実施できる便利なツールです。
(設定が必要な場合は、対話式で設定ファイルが作成できるjest --initも用意されています)
’vue’や’Angular’でも利用できるプラグインもあるため、JSフレームワークによらないテスト実装が可能です。

従来のフレームワークと比べてJestを使うメリットは、以下が挙げられます。
- アサーションやテストダブルなどが用意されているオールインワンなツールであるため、ライブラリの依存関係がシンプル
- JSDOMのエミュレータ上でテストを実施できるため、テスト実行が高速
- watchオプションを使うことで、差分のみのテストやfailしたケースに絞ってテストを実施できる
- カバレッジ出力も、オプションで指定するだけで標準出力可能(設定ファイルで外部ファイルへの出力も可能!)

今回は、こんな素敵なテストツール"Jest"のMock機能を紹介します。

JestのFunction Mock

JestのMock機能を使うことで、実際の処理に必要なFunctionをMockFunctionに差し替えることができるようになります。
例えば、userIdをキーにしてバックエンドシステムからfetchでUser情報を取得する様な下記の様な機能があったとします。

// userApi.js
export const getUser = async (id) => {
  return await fetch(`https://example.com/user?userId=${id}`, {
    method: 'GET',
  });
};

export const updateUserBySendUserDataResponse = (user, data) => {
  // ビジネスモデルの更新処理
  // ...
}
// userService.js
import {getUser} from './userApi';

export const getUserAsync = async (id) => {
  const response = await getUser(id).catch(error => (error));
  // ビジネスモデルへの変換等
  return response;
};

この様なService層のテストを実施する際、テストの成否がバックエンドシステムなどの外部コンポーネントの状況に依存する様な不安定なテストにしたくはありません。そのような場合、APIの機能をMock化する必要があります。
JestのMock Functionを使えば、以下の様にAPIのfunctionをmock化できます。そうすることで、Serviceのテストは外部コンポーネントの状態に依存せず実施することが可能になります。

// userService.test.js
import * as api from './userApi';
import {getUserAsync} from './userService';
import ErrorMessage from './constants/errorMessage';

describe('Test getUserAsync', () => {
  describe('正常系', () => {
    beforeEach(() => {
      // getUserをMock化
        api.getUser = jest.fn(async (id) => {
          return {
            userId: id,
            name: `name-${id}`,
          };
        });
      },
      5000
    );
    test('正しくデータが取得できること', async () => {
        const req = 'hoge';
        const expected = {userId: 'hoge', name: 'name-hoge'};
        const actual = await getUserAsync(req);
        expect(expected).toEqual(actual);
            // mock functionの機能を検証
        const mockFn = api.getUser;
        expect(mockFn).toHaveBeenCalledTimes(1);
      },
      10000
    );
  });
  describe('異常系', () => {
    beforeEach(() => {
        api.getUser = jest.fn(async (id) => {
          throw new Error(ErrorMessage.NotFound);
        });
      },
      5000
    );
    test('エラーが返却されること', async () => {
        const req = 'hoga';
        const error = new Error(ErrorMessage.NotFound);
        const actual = await getUserAsync(req);
        expect(error).toEqual(actual);
            // mock functionの機能を検証
        const mockFn = api.getUser;
        expect(mockFn).toHaveBeenCalled();
      },
      10000
    );
  });
});

setupとteardownもdiscribe単位で記述できるため、正常系と異常系でmockFunctionの内容を書き換えるといったことも可能です。

まとめ

Jestを使うことでシンプルに、外部コンポーネントにアクセスするAPIのMock化ができるようになります。また、アサーションも充実しているためJest単体でも詳細なテストをかけるのではと感じました。
遅れた上に準備不足で大変稚拙な記事となってしまいましたが、本当はModule Mockなど書きたいことがありました。
準備が出来次第どんどん記事を更新していきたいと思います!

参考

初学者が作って学ぶLINE BOT ~3文字の魔法でBotを起動する~

はじめに

今やLINEやSlackなどのチャットツールはコミュニケーションをとる上で必要不可欠となっています。
その中でもLINEが提供しているMessaging APIを使ったLINE BOT開発方法についてハンズオン形式でご紹介します。
今回はNode.jsNowを使用します。

例:LINE BOTとのトーク画面

想定読者

  • LINE BOTを作成してみたい方
  • Node.jsとNowを触ってみたい方

Messaging APIとは

Messaging APIは、あなたのサービスとLINEユーザーの双方向コミュニケーションを可能にする機能です。

Messaging APIの仕組み

Messaging APIを使うと、ボットアプリのサーバーとLINEプラットフォームの間でデータを交換できます。
ユーザーがボットにメッセージを送るとWebhookがトリガーされ、LINEプラットフォームからボットアプリのサーバーのWebhook URLにリクエストが送信されます。
すると、ボットアプリのサーバーからLINEプラットフォームに、ユーザーへの応答リクエストが送信されます。リクエストは、JSON形式でHTTPSを使って送信されます。

Messaging APIの特徴

- プッシュメッセージと応答メッセージ

プッシュメッセージとは、任意のタイミングでユーザーに送信するメッセージです。
応答メッセージとは、ユーザーからのメッセージに対して応答するメッセージです。

- 1対1、グループでトーク可能

Botアカウントの友だちになったユーザーにメッセージを送信できます。
また、グループに追加されていれば、グループ内でメッセージを送信することも可能です。

ハンズオン開始

以下の流れで作業していきます。

1. LINE DevelopersでBotを登録する
2. Node.jsでBotのプログラムを作成する
3. Nowを使ってBotを動かす

1. LINE DevelopersでBotを登録する

ではさっそくBotに必要な設定を行いましょう。

まずはLINE DevelopersでBotを登録します。

LINEアカウントでログインする

プロバイダーを新規作成する

好きな名前をつけてください。

Messaging APIを設定する

Messaging APIのチャネル作成をクリックしてください。

新規チャネルを作成する

<ポイント>
Botからメッセージを送りたい場合は プランを「Developer Trial」 にしてください

○ Developer Trial
MessagingAPIを利用したBotを試すプランです。友だちとメッセージの送受信を行うことができます。
※追加可能友だち数は50人に制限されています。また、Developer Trialからプランの切り替えやプレミアムIDの購入はできません。

○ フリー
MessagingAPIを利用したBotを開発するプランです。友だちの人数に制限はありませんが、Push messagesを利用してBotから友だちにメッセージを送信することはできません。
※サービス拡張に向けプラン変更が可能です。

チャネルを設定しよう

Messaging APIを選択してください


Webhook送信を利用するにして更新してください。

あとは以下を好みに合わせて設定してください。

  • Botのグループトーク参加を利用する
  • 自動応答メッセージを利用しない
  • 友達追加時あいさつを利用しない

2. Node.jsでBotのプログラムを作成する

Botの動作をNode.jsを使ってプログラムしていきます。

Node.jsとは

Node.jsはサーバサイドで動くJavaScriptです

参考:初心者向け!3分で理解するNode.jsとは何か?

インストール

Node.jsをインストールしていない人は下記サイトを参考にインストールしましょう。

【Node.js入門】各OS別のインストール方法まとめ(Windows,Mac,Linux…)

プロジェクト作成

任意の場所でコマンドプロンプトを開き、必要なファイルを作成します。

$ mkdir 【任意のフォルダ名】
$ cd 【任意のフォルダ名】
$ npm init -y
$ npm i --save @line/bot-sdk express

作成したフォルダにpackage.jsonが作成されているため
scriptsに"start": "node server.js",を追加します。

package.json
 "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  }, 

Botのプログラム本体となるserver.jsをpackage.jsonと同階層に作成します。
以下を参考に作成してください。
channelAccessTokenchannelSecretの値は必ず設定してください。

server.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;

const config = {
    channelAccessToken: '【LINE Developers チャネル基本設定の「アクセストークン」の値】',
    channelSecret: '【LINE Developers チャネル基本設定の「channelSecret」の値】'
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);
    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let replyText = '';
  if(event.message.text.match(/おはよう/)){
    replyText = 'おはようございます';
  }else if(event.message.text.match(/こんにちは/)){
    replyText = 'こんにちは';
  }else if(event.message.text.match(/こんばん/)){
    replyText = 'こんばんわ';
  }else if(event.message.text.match(/おやすみ/)){
    replyText = 'おやすみなさい';
  }else{
    return;
  }

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: replyText
  });
}
app.listen(PORT);
console.log(`Server running at ${PORT}`);

サンプルで載せているserver.jsは、「おはよう」と言ったら「おはようございます」と返すくらいのコードなので、自由に修正してください。

3. Nowを使ってBotを動かす

プログラムができあがったら、Botを動かせるようにします。
今回は無料で使用でき、かつ簡単なNowを使用します。

Now とは

Now 公式サイト

Now makes serverless application deployment easy.
Don’t spend time configuring the cloud. Just push your code.

WEBアプリケーションの公開を簡単に行ってくれるサービスのことです。

  • 認証はメールのみ(パスワード不要)
  • 設定をpackage.jsonに記載するだけ
  • nowというコマンドを打つだけ

Nowを使ってみよう

アカウントを作成してください

https://zeit.co/signup

インストール

以下コマンドでNowをインストールします。

$ npm i -g now

Nowへログイン

以下コマンドを実行してください。
※ 登録したメールアドレスに認証用のメールが飛んでくるので、許可してください。

$ now login 【登録したメールアドレス】
> We sent an email to XXXXXXXXXX. Please follow the steps provided
  inside it and make sure the security code matches Witty Snowshoe.
√ Email confirmed
> Ready! Authentication token and personal details saved in "~\.now"

Nowコマンド実行

ログインした状態でnowコマンドを実行すると、
package.jsonをもとに自動でデプロイします。

$ now
> ▲ npm install
> npm WARN XXXXX@1.0.0 No description
> npm WARN XXXXX@1.0.0 No repository field.
>
> added 71 packages in 1.457s
> ▲ Snapshotting deployment
> ▲ Saving deployment image (1.7M)
> Build completed
> Verifying instantiation in sfo1
> [0]
> [0] XXXXX@1.0.0 start /home/nowuser/src
> [0] node server.js
> [0]
> [0] Server running at 3000
> √ Scaled 1 instance in sfo1 [37s]
> Success! Deployment ready

(参考)デプロイ確認

デプロイに失敗しても不要なnow.shができあがってしまうため、
不要なものはlsでurlを確認して削除しましょう

$ now ls
> 7 total deployments found under XXXX[995ms]
> To list more deployments for an app run `now ls [app]`

  app      url               inst #    type    state    age
  XXXXX    XXXXXXX.now.sh         1    NPM     READY    60s

(参考)削除

[y/N]と聞かれるので[y]を入力する

$ now rm XXXXXX.now.sh
・
・
> Are you sure? [y/N] y

LINE Developersに設定する

最後にNowで公開しているURLをLINE Developersに設定します。
LINE Developers > プロバイダー > Messaging API > チャネル基本設定

Webhook URLに、「Nowで公開しているURL」 + 「/webhook」を設定する。

これでLINE BOTに必要な設定は完了です。

あとはLINEアプリでQRコードなどを用いて、作成したBotを友達追加して
Botとの楽しいメッセージのやりとりを行ってください。

まとめ

今回は最小限の機能しかご紹介していないため、
毎朝天気を通知してくれるBotなどを作るともっと楽しくなるかもしれません。

筋トレからはじめる💪ドメイン駆動設計

はじめに

エリック・エヴァンスのドメイン駆動設計 に出てくるエンティティ・値オブジェクトを筋トレに絡めて解説する今までにない新しい記事がこれです。

読者対象

  • 筋トレしてる人
  • もっと良いコードを書きたいと思っている人
  • もっと良い設計をしたいと思っている人

前提知識

  • Kotlinの基本的な知識(サンプルコードはKotlinです)

この記事で取り上げること

  • エンティティ
  • 値オブジェクト

ドメイン駆動設計

ドメイン駆動設計とは、システム化対象ビジネスの複雑性と戦う道具の一つです。複雑性と戦うために、ドメイン駆動設計では対象ビジネスの概念、考え方をドメインモデルとしてモデル化します。このドメインモデルがとても重要です。ドメイン駆動設計ではドメインモデルを中心に設計開発を進めていくためです。

しかし、学習をしていくうえでドメインモデルを一番最初に学ぶことは難易度が高いように思います。ドメインモデルは対象に応じて様々な形態をとるものであり、説明も概念的になりがちです。これは筋トレに例えると、最初からデッドリフトに挑戦するようなものです。デッドリフトでは正しいフォームが大切であり、初心者が最初から手を出しても効果的なトレーニングが行えません。同じ様に、ドメインモデルを最初から学ぼうとしてもあまりに概念的すぎて学習が効率的、効果的にならない可能性があります。そこで、本記事では実装上の課題をドメイン駆動設計ではどのように解消するのかを中心に説明し、どのような実装になるかを示します。説明の際にはドメインモデルの内容を省きます。

本記事がドメイン駆動設計の学習の入り口(もしくは筋トレに興味を持つきっかけ)として役に立てればと思います。

エンティティ

ジムのユーザ管理システムを構築する場合、ユーザー一人一人を区別して管理する必要があります。例えば田中さん、佐藤さん、鈴木さんといった人がそのジムに登録した場合、システムはこれらの人を区別して管理する必要が出てきます。この場合、それぞれの人をどのように区別するべきでしょうか?

一つの案として筋肉量を比較するというものがあります。この場合、それぞれのユーザの筋肉量が異なれば、それぞれを別の人物として管理することができるでしょう1(田中さん50%、佐藤さん45%、鈴木さん55%)しかし、ここで田中さんが追い上げて筋肉量が55%になってしまうとそれぞれのユーザーを区別することが出来なくなってしまいます。(鈴木さんと同じになってしまう)筋肉量はその人の一生を通じて固定したものではありません。筋肉は鍛えていれば増加します。プロテインを飲めばなおさらです。

このシステムの場合、ユーザーを区別する場合においてはそれぞれの属性に着目することは良くありません。名前、年齢、体重、筋肉力などの属性があったとしても、それらは誰かを表すものではなく、ある時点でのその人の情報でしかありません。年齢は毎年増えますし、名前も変えることが出来ます。

ユーザーの属性が変化しても、同じ属性を持つユーザーがいたとしても、それぞれを区別・追跡する必要があります。属性だけでは区別できない性質は同一性と言われ、同一性を持つものはエンティティと言われます2

エンティティの比較

エンティティ同士はそれらが持つ同一性をもとに比較する操作が必要です。エンティティに対して識別子(ID)を割り振り、それをもとに比較することがよく行われます。

IDは一度割り当てたら再度変更することが出来ないようにします。値自体をイミュータブルにし、JavaやKotlinでSetterをIDのフィールドにつけないようにします。さらにシステムがエンティティをどのような形態(Javaオブジェクト、Kotlinオブジェクト、JSON、DBスキーマ)にしたとしても、IDの表現形態が変わったとしても、IDによる比較が適切に行われるようにします。

data class User(
    val id: Long, // ユーザーのIDを定数で表現する例
    val name: String,
    val years: Int,
    val strength: String
)

val suzuki = User(19082, "Suzuki", 25, "999")
{
  "id" : "19082", // JSON形式になった時のIDの表現例
  "name" : "Suzuki",
  "years" : 25,
  "strength" : "999"
}

ID同士を比較することで同一性を比較出来ます。比較の方法は様々ですが、私がよくやるのは、エンティティを表現するレイヤースーパータイプを用意し、エンティティの比較方法をシステムの中で統一させてしまいます。

// エンティティを表すレイヤースーパータイプ
interface Entity<T> {

      // 同一性を比較する
      // 同一性がある場合true、ない場合false
    fun isSameIdentityOf(entity: T): Boolean
}

// 上記のレイヤースーパータイプを実装するクラス
data class User(
        val id: Long,
        val name: String,
        val years: Int,
        val strength: String
) : Entity<User> {

    // 同一性をIDを元に比較する
    override fun isSameIdentityOf(entity: User): Boolean {
        return id == entity.id
    }

}

IDは上記のようにLong型としてエンティティに持たせることもできますが、特に理由がない限り、IDは後述する値オブジェクトを使うことをオススメします。識別子に関する操作を値オブジェクトの中に集約できますし、実際のIDの値が何なのか(StringLongInt...)について気にしなくてよくなります。さらに、IDの表現形式を変更しても、システム全体に影響が及びません。

// ユーザの識別子クラス
data class UserId(val rawId: Long)

data class User(
        val id: UserId, // Long型ではなくUserId型を使用
        val name: String,
        val years: Int,
        val strength: String
) : Entity<User> {

    override fun isSameIdentityOf(entity: User): Boolean {
        // UserIdクラスは data class なので、自動生成された equals を使用できる
        return id == entity.id
    }

エンティティの識別子の生成

筋肉は主にたんぱく質、糖質、ミネラルから生成出来ますが、エンティティの識別子はどのようにして生成するのでしょうか。

考えられる方法としては以下のものがあります。

  • ユーザーが指定する
  • アプリケーションが自動生成する
  • 永続化システムが自動生成する

ユーザーが指定するとは、ジムの入会などでユーザがIDを直接入力し、それをユーザの識別子として利用すると言うことです。この場合、システムが識別子を自動生成する仕組みを持つ必要はありませんが、システム内でユーザが指定してきた識別子に一意性があることを保証する必要が出てきます。でなければ同じユーザIDを持つジム会員が発生してしまいます。(みんな他人の請求書を受け取りたくはないはずです)

アプリケーションが自動生成する方法は様々なものがあります。(ID生成大全)例えば、UUIDを使用して識別子を自動生成する場合以下のように実装できます。

// UserIdを生成するインターフェイスを定める
// インターフェイスを設けることで、UserIdをどのように生成するのかという関心事を分離できる
interface UserIdGenerator {

    fun nextId(): UserId

}

// UUIDを用いてUserIdを生成するクラス
class UUIDUserIdGenerator : UserIdGenerator {

    override fun nextId() = UserId(UUID.randomUUID().toString())

}


// アプケーションが自動生成したIDを使用してユーザクラスをインスタンス化
val userIdGenerator: UserIdGenerator = UUIDUserIdGenerator()
val sato = User(userIdGenerator.nextId(), "Sato", 30, "8383")

注意すべき点は、アプリケーションが自動生成する識別子は人間にとって読みやすいものではないということです。なので、識別子を直接表示したり、識別子の入力を求めたりすると使いづらいシステムになってしまいます。

最後の永続化システムが自動生成するとはデータベースのシーケンス値といったものを使用するということです。永続化システムにエンティティクラスごとのシーケンスジェネレーターを用意し、アプリケーションからその値を採番することで識別子を生成します。この方法のメリットは採番した値に一意性があることを保証しやすい点です。一方、デメリットは識別子を取得するのに時間がかかることです。エンティティを生成するたびに永続化層にアクセスしに行く必要が出てくるからです。このデメリットの回避策としてシーケンス値をキャッシュする方法が考えられます。しかし、永続化システムが再起動するなどしてキャッシュが破棄されてしまうと、使用されない値が生まれてしまう点に注意してください。

値オブジェクト

ジムでダンベルを上げるときはその重さに注意を払います。自分の負荷が低すぎると当然筋肉が引き締まりません。高すぎると怪我をします。今のコンディションにフィットする重さを選ぶよう注意しましょう。重さが同じであれば、どのダンベルを使っても大丈夫です。

このようなある対象の属性や、何らかの物事を記述することにしか関心がない場合、値オブジェクトとして扱うことが出来ます。値オブジェクトが表現するのは何であるか(ダンベルの重さ、たんぱく質量、消費カロリー)だけを表現し、どれであるか(XX社が2018年に発売した製造番号YYYYYのダンベル)は表現しません。同一性は与えず、比較を行うときには属性のみ対象にするようにします。

値オブジェクトは基本的に以下の特徴があります。

  • システム化対象領域のある概念の、計測したり、定量化したり、説明したりする。
  • 完全に置き換えることができる。
  • 不変なものとして扱うことができる。
  • 等しい値かをオブジェクト同士で比較できる。

例えば、ジムのユーザー管理システムが、それぞれの人がその日使用したダンベルを記録するようになったとしましょう。その時、ダンベルの重さにしか注目する必要がないとします。この場合、ダンベルを以下のうように値オブジェクトとして実装できます。

// レイヤースーパータイプ
interface ValueObject<T> {

    // 同値性を比較する
    // 同値である場合true,同値でない場合false
    fun isSameValueAs(valueObject: T): Boolean
}

// ダンベルを表現する値オブジェクト
data class Dumbbell(
        val weight: Int // ダンベルの重さを表現する
) : ValueObject<Dumbel> {

    override fun isSameValueAs(valueObject: Dumbel): Boolean {
        return weight == valueObject.weight
    }

}

値オブジェクトはイミュータブル

値オブジェクトは状態を変更出来ないようにします。Setterを用意したり、メソッドの中で値の更新などを行なってはいけません。なぜこのようなことをするのでしょうか。

値オブジェクトが変更可能である場合、開発者はその値オブジェクトの状態を常に気をつけていかなくてはなりません。例えば、鈴木さんが50kgのダンベルでアームカールを50回行なったとしましょう。その場合、以下のように書くことが出来ます。

// 属性の変更が可能なダンベルクラス
data class Dumbbell(
        var weight: Int
) : ValueObject<Dumbbell> {

    override fun isSameValueAs(valueObject: Dumbbell): Boolean {
        return weight == valueObject.weight
    }

    // 指定された重さに変更
    fun changeWeight(weight: Int) {
        this.weight = weight
    }
}

// 鈴木さんが50kgのダンベルで50回アームカールを実施
val dumbbell = Dumbbell(50)
suzuki.training(dumbbell = dumbbell, count = 50)

この後、dumbell変数に対して重さの変更を行ってしまったとすると、鈴木さんが100kgのダンベルでアームカールを50回行ったことになってしまいます。筋トレとしては素晴らしい成果ですが、不正な記録です。

// ダンベルの重さを100kgに変更
dumbell.changeWeight(100)

こういった状況はシステムが複雑になればなるほど管理するのが難しくなっていきます。思わぬところでバグを作ってしまったり、システムの理解容易性や拡張性を妨げる原因にも繋がります。

値オブジェクトの状態を変更できないようにすると、上記で挙げたようなことは起こらなくなります。そうすることで、 開発者は値オブジェクトを扱う時、どのインスタンスなのかを気にする必要がなくなります。 その値オブジェクトが別のインスタンスからも同時に参照されていようが、あるいはサブルーチンに参照ごと渡そうが、値オブジェクトの状態は変更されることがありません。このようにすることで、システムをよりシンプルに、より理解しやすくできます。

値オブジェクトの交換可能性

上記で示したように値オブジェクトは不変値である必要があります。しかし、値オブジェクトが示す値を変更したい(ダンベルの重さが50kgから100kgになったとか)場合もあるはずです。その場合、値オブジェクト全体を置き換えることで対応します。

例えば50kgでトレーニングしていると思ったら実は100kgだったなんてことがあったとします。その場合は50kgを表すダンベルの値オブジェクト自体を100kgで置き換えます。

// 50kgのダンベルで50回アームカールを実施
val dumbbell = Dumbbell(50)
suzuki.training(dumbbell = dumbbell, count = 50)

// 100kgに修正
suzuki.changeTrainingLog(dumbell = Dumbell(100))

この時のユーザクラスは以下のようになっています。

// ユーザを表すエンティティクラス
data class User(
        val id: Long,
        val name: String,
        val years: Int,
        val strength: String
) : Entity<User> {

    private var trainingLog: TrainingLog

    override fun isSameIdentityOf(entity: User): Boolean {
        return id == entity.id
    }

    // トレーニングの記録をつける
    public fun training(dumbell: Dumbell, count: Int) {
        trainingLog = TrainingLog(dumbell, count)
    }

    // トレーニング時のダンベルの重さを変更する
    public fun changeTrainingLogDumbell(dumbell: Dumbell) {
        trainingLog = TrainingLog(dumbell, trainingLog.count)
    }

    data class TrainingLog(val dumbell: Dumbell, val count: Int)

}

終わりに

この記事ではドメイン駆動設計のエンティティ・値オブジェクトについて説明しました。ドメイン駆動設計ではこの他にもサービス、リポジトリ、ファクトリ、集約などの実装パターンがあります。これらについては今後少しずつ説明できたらなと思っています。

参考

  1. エリック・エヴァンスのドメイン駆動設計
  2. 実践ドメイン駆動設計
  3. IDDD本から理解するドメイン駆動設計連載一覧:CodeZine(コードジン)
  4. 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法

  1. ここで言う筋肉量とは全体重のうち筋肉が占める重量の割合です。 

  2. エリック・エヴァンスのドメイン駆動設計、第5章、エンティティを参照 

HTTPのバージョンについてまとめ

この記事は?

 TISアドベントカレンダー9日目です。
 最近、同期との会話で「HTTP/3とか出たけど、そもそもHTTPのバージョンってそれぞれどう違うのよ?」みたいな会話になりました。その場では「わからん!」でみたいな感じで話は終わってしまったのですが。個人的には興味があったので、この機会にまとめてみたいなと思って書いてます。

書くこと、書かないこと

 先に書いたと通り、この記事では、バージョンの違いや、バージョンが上がってきた経緯についてまとめるものです。それぞれ仕様やセマンティクスについて1つ1つ詳細に調べてまとめるた記事ではないです。具体的には以下のようなことを書きます。

  • HTTPとは
  • HTTPのバージョンごとの違い

また、この記事はバージョンごとのすべての変更点を網羅するものではなく主要なものをまとめるものです。

想定される読者

 HTTPの基礎の基礎は知っていることを想定しています。
 具体的には書籍「HTTPの教科書」に書かれてることをなんとなく把握している(それぞれの内容に説明はできなくとも聞けばそういうのもあるなぁってわかる)レベルを想定してブログを書いています。

そもそもHTTPとは?

RFC7230では、HTTPは以下のように説明されます。

The Hypertext Transfer Protocol (HTTP) is a stateless application
level protocol for distributed, collaborative, hypertext information
systems.

「HTTPは分散的で共同的なハイパーテキスト情報システムのためのステートレスなアプリケーションレベルのプロトコルです。」

HTTPではWebクライアントとWebサーバがコミュニケーションを取るために以下のような仕様を定めています。

  • 送信する手順
  • 送信するフォーマット

HTTPのバージョンについて

 HTTPは、最初は非常にシンプルなプロトコルでした。月日がながれ、バージョンが上がるごとに機能追加や通信速度改善に対する取り組みが行われています。また、HTTPは前方互換性が保たれるように策定されています。
 それぞれのバージョンアップの年月日を以下にまとめます。

年代 バージョン
1990 HTTP/0.9
1996 HTTP/1.0
1997 HTTP/1.1
2015 HTTP/2
2018年 HTTP/3

HTTP0.9

 そもそもHTTP/0.9というバージョンは存在しませんでした。HTTP/1.0より前という意味合いで、0.9と呼ばれるようになったみたいです。このバージョンで行えることは非常にシンプルで

URLで特定されるHTMLを取得する

ということだけです。
イメージとしては「ブラウザーからURLを叩いて、HTMLのドキュメントを取得し表示する」これだけです。双方向通信はもちろんのこと、情報の更新や、削除も行えません。

HTTP/1.0

 HTTP/1.0ではMIMEのようにメタデータを追加したメッセージ形式を導入することで、プロトコルの改善を行っています。

HTTP/0.9からの変更点

  • 送受信フォーマットにヘッダが追加された。
  • リクエスト、レスポンス時のバHTTPのバージョンが追加された。
  • リクエスト時にメソッド(GET、PUT、POST、DELETE等)が送信できるようになった。
  • レスポンス時にステータスコードが送られるようになった。

できるようになったこと

上記の変更から、様々なセマンティクスが定義され、HTTP/1.0ではHTTP/0.9に対して以下のようなことができるようになりました。

  • 複数のドキュメントを送信可能になった。
  • HTML以外のデータ形式を送受信するための手段が提供された。
  • コンテンツの追加、削除、更新などが行えるようになった。
  • サーバーからのリダイレクトの要求が可能になった。

HTTP1.1

 HTTP/1.1では前バージョンまでの曖昧な点をはっきりさせ、パフォーマンスの改善のための仕組みが取り込まれてます。

HTTP/1.0からの変更点

  • 通信の高速化(Keep-Aliveがデフォルト有効、パイプライニング)
  • TLSのサポート
  • プロトコルのアップグレード
  • チャンクエンコーディングのサポート

通信の高速化(Keep-Aliveがデフォルト有効、パイプライニング)

 HTTP/1.1では以下のような方法で通信の高速化を実現しています。

  • Keep-Aliveのデフォルトでの有効化
    • 連続したリクエストに対して、再接続時に前回の接続を再利用する技術
  • パイプライニング
    • 最初のリクエストの送信が完了する前に次のリクエスト送信する技術

 パイプライニングに関しては、エンドポイント間にHTTP/1.0しか解釈できないプロキシがあった場合に動作不能となってしまうことや、正しく実装されていないサーバーがあったことにより、広く使われた技術ではありませんでした。しかし、まったくもって無駄になったわけではなく、あとから出てくるHTTP/2でストリームという仕組みとして生まれ変わります。

TLS(トランスポート・レイヤー・セキュリティ)のサポート

 TLSはHTTPよりも下のレイヤーの通信経路を暗号化するための技術です。HTTP/1.1では、このTLSをサポートしており、この仕組を利用したプロトコルがHTTPSです。HTTPS用いることでセキュアな通信が行えます。
 また、HTTP/1.0やHTTP/1.1ではプロキシサーバなどが内容を解釈し、解釈できないプロトコルなどをブロックしてしまうという問題がありました。しかし、HTTPレイヤーよりも下のプロトコルであるTLSを使うと、プロキシサーバが関われない安定した通信経路を確立でききるためHTTP/2のWebSocketのような、新しい前方互換性のない通信プロトコルを導入するためのインフラ基盤となっています。

プロトコルのアップグレード

 HTTP/1.1では、以外へのプロトコルへのアップグレードがとなりました。HTTP/1.1以外のプロトコルへのアップグレードを含めると、以下の3つのようなアップグレードがあります。

  • HTTPからHTTPS
  • HTTPからWebSocket(双方向通信プロトコル)
  • HTTPからHTTP/2

チャンク方式

 サーバーからクライアントへデータを送信する際に、一括で送るのではなく小分けにして送る方式です。チャンクを使うことで、巨大なデータ送受信する際に小分けにして少しづつ送信することが可能になります。これにより、サーバー側では巨大なデータを一括でメモリにロードする必要がなくなり、クライアント側ではデータを逐次的に送られてきたデータを処理できます。

HTTP/2

 HTTP/2では、より効率的なネットワークリソースの活用が可能となり、へッダーの圧縮も行えるようになっています。
HTTPのメッセージの文法などは変わっておらず。HTTP/1.1のセマンティクスなどはこれまでと同様に利用可能です。
 「HTTP/3 explained」によると2018年の前半にはTOP1000 のWebサイトのうちおよそ40%がHTTP/2の上で動き、Firefoxの約70%のHTTPSのリクエストはHTTP/2のレスポンスを受けます。そして、それは、すべてのメジャーなブラウザーやサーバー、プロキシはHTTP/2をサポートしています。

HTTP/1.1からの変更点

  • テキストベースからバイナリベースへ
  • ストリームを使ったデータ送受信
  • サーバープッシュ
  • ヘッダの圧縮

テキストベースからバイナリベースへ

 HTTP/2ではテキストベースのプロトコルからバイナリベースのプロトコルへと変わりました。各データ「フレーム」と呼ばれる単位でデータを送受信します。

ストリームを使ったデータの送受信

 HTTP/1.1までは1つのリクエストは1つTCPソケットを専有してしまうため、1つのオリジンサーバに対して、複数(2〜6)のTCPコネクションを確立していました。それに対して、HTTP/2では1つのTCPコネクションに対してストリームと呼ばれる仮想TCPソケットを作成し、並列化を行います。この仮想TCPソケットはハンドシェイクを必要としません。
HTTP/2は4層モデルのアプリケーション層のプロトコルですが、フローコントロールなどの機能も備えています。

サーバプッシュ

 サーバプッシュはCSSやJavaScript、画像などの要求されることが予測されるコンテンツを事前に送信する技術です。これはWebSocketのような双方向通信の仕組みではなく、あくまで上記のような静的コンテンツをリクエストより前に送信しておくための技術です。

ヘッダの圧縮

 HTTP/2ではHPACKと呼ばれる方式でヘッダを圧縮します。HTTPヘッダは決まった名前がよく用いられるため、クライアントとサーバで同じ辞書を持ちその辞書を使うことでヘッダを圧縮します。

HTTP/3

 HTTP/3でもこれまでのメッセージの文法やコンセプトは変わりません。ヘッダーがあって、ボディがあって、リクエストがあり、レスポンス、HTTPメソッドも、ステータスコードもあります。また、HTTP/2と同様にストリームとサーバープッシュも提供します。
HTTP/3はHTTPがQUICの上で動作するための変更とその結果の集まりです。

HTTP/2.0からの変更点

  • QUICと呼ばれるUDPソケットを用いたプロトコルの上で動作する。
  • HPACKではなく、QPACKと呼ばれるヘッダーの圧縮方式を使っている。
  • QUICのおかげで、より早いハンドシェイクが行える(0-RTT or 1-RTT)
  • すべての通信はTLS1.3によって暗号化される。

QUICと呼ばれるUDPソケットを用いたプロトコルの上で動作する。

 QUICは、元はGoogleが開発したトランスポート層のプロトコルです。HTTP/3のQUICはGoogleのQUICを元にIETFが独自に策定を進めているものです。HTTP/3ではこのIETF-QUICにより、様々な恩恵を受けています。
HTTP/2とHTTP/3の層の違いをいかに示します。
HTTP:3.png

QUICはUDPソケットを利用して通信を行います。UDP通信の信頼性はQUICが担保します。

HPACKではなく、QPACKと呼ばれるヘッダーの圧縮方式を使っている。

 HTTP/3でも、ヘッダの圧縮は行われます。しかし、HPACKでははくQPACKと呼ばれる圧縮方法を用いています。これらの設計は似ており、QPACKはQUIC版のHPACKと言うことができます。

より早いハンドシェイクが行える(0-RTT or 1-RTT)

 QUICでは、以前に通信を行ったことのあるサーバであれば、その通信のパラメータをキャッシュし0-RTTを可能にします。このおかげで、クライアントはハンドシェイクを待つことなく直ぐにデータを送ることができます。
また、キャッシュが無い場合でも、QUICは1-RTTのハンドシェイクを提供します。

すべての通信はTLS1.3によって暗号化される。

 すべての通信はTLS1.3によって暗号化されます。例外はありません。TLS1.3はより少ないハンドシェイクで行えるため、コネクション確立のオーバーヘッドを減らします。

終わりに

 この記事では、それぞれのバージョンの違いによる大きな差異をまとめました。HTTP/2までの詳細なバージョン間の違いや、セマンティクスなどをもっと詳しく知りたい場合は以下の書籍に記載されているので、読んだことのない人はぜひ読んでみてください。

参考書籍、文献

Stack Overflowクローン、Scooldを使ったQAサイト

はじめに

この記事は、TIS Advent Calendar 2018の10日目の記事です。

書かれていること

このエントリでは、Stack OverflowクローンであるScooldの簡単な紹介と、少し中身の話を書いています。

QAサイト?

日々の業務で、技術的な困りごとだったり、相談ごとだったりをする時には、どうしているでしょうか?

様々なアプローチがあると思いますが、知識の蓄積や誰でも内容が見られて親近感が出そうなものとして、QAサイトというものもあると思います。

そんなわけで、社内にStack OverflowクローンであるScooldを導入して、技術的な質問を投稿したり、質問に答えたりするような活動をしています。

あくまで社内に閉じたものだったりはしますが、社内で他の人に助けを求めることができたり、もっとオープンな環境に向けてだと心理的なハードルの高さもあったりするので、こういう解法もそう悪くはないのでは?と思ったりします。

Scoold

Scooldは、Erudika社がオープンソースで提供しているQ&Aプラットフォームです。

Scoold

Scoold is an open source Q&A platform written in Java.

Stack Overflowにインスパイアされていることが書かれています。

Scoold is inspired by StackOverflow and implements most of its features.

ライセンスは、Apache License Version 2.0です。

Erudika/scoold

商用版もあったりします。

OSS版の機能の一覧。

  • データベースが選択可能で、クラウドへのデプロイにも最適化している
  • 全文検索
  • 分散オブジェクトキャッシュ
  • Locationの情報を使って、「自分に近い位置」の投稿をフィルタリングできる
  • 多言語サポート
  • 評価と投票によるバッヂシステム
  • スペース(チーム) … 質問やユーザーをグループで分割できる
  • jQueryベースの最小限のフロントエンド
  • Materialize CSSによるモダンでレスポンシブなデザイン
  • よく似た質問に対するサジェストをして、重複した質問する前にヒントを出す
  • 回答やコメントがあった時のメール通知
  • Spring BootによるUber JAR
  • LDAP認証のサポート
  • ソーシャルログイン(Facebook, Google, GitHub, LinkedIn, Microsoft, Twitter)とGravatarのサポート
  • コードのシンタックスハイライト、GitHub Flavored Markdownのサポート
  • 絵文字のサポート
  • SEOフレンドリー

商用版になると、このあたりが増えるようです。

  • 好きな投稿をSticky(固定)にする
  • SAMLサポート
  • 匿名ユーザーの投稿
  • 無制限のスペース
  • 複数の管理者を設定可能
  • 複数のドメインをサポート
  • より高度なハイライト
  • 画像アップロード
  • セキュリティ通知

Qiita上でもScooldを活用したという記事もあり、自前でチーム内だったり組織内にQAサイトを構築するのには良いのかもしれません。

社内での問い合わせ管理にQ&Aシステムのススメ

またScooldではありませんが、他の手段としてはStack Overflow for Teamsを検討するのもありでしょう。

Stack Overflow for Teams

チーム専用のプライベートなQ&Aサイトが作れる「Stack Overflow for Teams」提供開始。月額10ドルから

まあ、今回はScooldがテーマなので、こちらを掘り下げていきます。

Scooldは単体では動作せず、ParaというAPIサーバーをバックエンドに持ちます。

Para

Erudika/para

Scooldと同じく、ライセンスはApache License Version 2.0です。

Scooldは、ParaのREST APIを使用して、アクセスを行います。

Paraは、汎用のAPIサーバーアプリケーションです。Scooldとの役割分担を簡単に言うと、ScooldはUI担当、Paraはデータ操作担当です。

A general-purpose backend framework for the cloud

汎用といっても、相応の目的に特化したものではあるのですが。

インストールしてみる

では、ScooldをインストールしてローカルにQAサイトを立ち上げてみましょう。

ちなみに、単純にScooldを評価したいだけの場合は、デモサイトがあるのでそちらで試してみるのが良いと思います。

前述の通り、ScooldはバックエンドにParaを必要とします。

Paraはparaio.comでサービスとして提供されているものもあるのですが、今回はParaもローカルで起動してみる方針としましょう。

実行環境

OSはUbuntu Linux 18.04 LTSです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.1 LTS
Release:    18.04
Codename:   bionic

また、ScooldとParaのソースコードより、Java 8を対象とした方が良さそうなので、Javaは8を選択します。

https://github.com/Erudika/scoold/blob/1.31.0/pom.xml#L9-L13
https://github.com/Erudika/para/blob/v1.31.0/pom.xml#L145-L146

$ java -version
openjdk version "1.8.0_191"
OpenJDK Runtime Environment (build 1.8.0_191-8u191-b12-0ubuntu0.18.04.1-b12)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)

また、コマンドラインツールを使う際には、Node.jsが必要です。

Paraのインストール

Paranのインストールは、GitHubのQuickStartに沿って見ていけばOKです。

Para / Quick Start

Paraに関してはドキュメントも充実しているので、こちらを見て進めていってもよいでしょう。

Para Docs

Paraのダウンロード。

$ wget https://oss.sonatype.org/service/local/repositories/releases/content/com/erudika/para-jar/1.31.0/para-jar-1.31.0.jar

なお、Paraは実行可能WARも作成されており、WARを使った起動もできるようになっています。

ドキュメントに沿って、設定ファイルを作成します。

application.conf

# the name of the root app
para.app_name = "Para"
# or set it to 'production'
para.env = "embedded"
# if true, users can be created without verifying their emails
para.security.allow_unverified_emails = false
# if hosting multiple apps on Para, set this to false
para.clients_can_access_root_app = true
# if false caching is disabled
para.cache_enabled = true
# root app secret, used for token generation, should be a random string
para.app_secret_key = "b8db69a24a43f2ce134909f164a45263"
# enable API request signature verification
para.security.api_security = true
# the node number from 1 to 1024, used for distributed ID generation
para.worker_id = 1

ちなみに、設定ファイルはTypesafe Configを使って読み込みます。

起動。

$ java -jar -Dconfig.file=./application.conf para-jar-1.31.0.jar

起動時に、ログにこんな内容が出力されます。

2018-12-09 18:25:48 [WARN ] Server is unhealthy - root app not found. Open /v1/_setup in the browser to initialize Para.

/v1/_setupにアクセスしてね、と言っているので、curlやブラウザなどでアクセスしてみます。

$ curl localhost:8080/v1/_setup
{
  "accessKey" : "app:para",
  "message" : "Save the secret key - it is shown only once!",
  "secretKey" : "3rB4SvKiTsMaSd2g7qXvGdPtezFmkROhWwCgyxV48OMtSKlVXklpMQ=="
}

すると、secretKeyが得られます。この値を覚えておきましょう。

Scooldのインストール

続いて、Scooldをインストールします。こちらも、Quick Startを見ながら。

Scoold / Quick Start

まず最初に、Paraにpara-cliで新しいアプリケーションを作成します。この過程で、先ほどのParaをセットアップした際の情報を使用します。

$ npm install -g para-cli
$ para-cli setup
Secret key not provided. Make sure you call 'signIn()' first.
Para Access Key: app:para
Para Secret Key: 3rB4SvKiTsMaSd2g7qXvGdPtezFmkROhWwCgyxV48OMtSKlVXklpMQ==
Para Endpoint: http://localhost:8080
✔ New JWT generated and saved in /$HOME/.config/para-cli-nodejs/config.json
✔ Connected to Para server v1.31.0 on http://localhost:8080. Authenticated as: app Para (app:para)

「新しいアプリケーション」ってなんだ?という話ですが、Paraは汎用のバックエンドサーバーという位置づけであり、ScooldはParaを使うアプリケーションの一種だということですね。

なので、今回は「Paraを使うScooldというアプリケーションを作りました」ということになります。

確認。

$ para-cli ping
✔ Connected to Para server v1.31.0 on http://localhost:8080. Authenticated as: app Para (app:para)

新しいアプリケーションの作成。

$ para-cli new-app "scoold" --name "Scoold"
✔ App created:
{
  "accessKey": "app:scoold",
  "message": "Save the secret key - it is shown only once!",
  "secretKey": "JZODlJVH1F5pb6vKA6qae8XQYP7MaGOgrXnK3RN6nMZoemSMVmbnag=="
}

この情報も、やはり重要なので覚えておきましょう。

Scooldのダウンロード。

$ wget https://github.com/Erudika/scoold/releases/download/1.31.0/scoold-1.31.0.jar

設定ファイルは、Quick Startの内容からGoogleなどのAPI Keyを必要とするものを省き、接続先のParaやScoold自体の参照URLはlocalhostとなるように作成しました。

application.conf

para.app_name = "Scoold"
# the port for Scoold
para.port = 8000
# change this to "production" later
para.env = "development"
# the URL where Scoold is hosted, or http://localhost:8000
para.host_url = "http://localhost:8000"
# the URL of Para - could also be "http://localhost:8080"
para.endpoint = "http://localhost:8080"
# access key for your Para app
para.access_key = "app:scoold"
# secret key for your Para app
para.secret_key = "JZODlJVH1F5pb6vKA6qae8XQYP7MaGOgrXnK3RN6nMZoemSMVmbnag=="
# enable or disable email&password authentication
para.password_auth_enabled = false
# if false, commenting is allowed after 100+ reputation
para.new_users_can_comment = true
# If true, the default space will be accessible by everyone
para.is_default_space_public = true

ここで、para.access_keypara.secret_keyは、先ほど作成した新しいアプリケーションの情報を指定します。

こちらも、設定ファイルはTypesafe Configで読み込みます。

起動。

$ java -jar -Dconfig.file=./application.conf scoold-1.31.0.jar

http://localhost:8000にアクセスすると、起動したScooldの画面を見ることができます。

英語ですけどね!!

範囲を選択_050.png

日本語化は、次のバージョンでできるのではないかと思いますけどね。

あと、どうやってログインしたらいいんでしょう…?

あれ?ソーシャルログインは?と思われるかもしれませんが、それはScooldの設定で利用をコントロールすることができます。ただ、今回は各種ソーシャル系のキーを用意していないので、パスしています。

範囲を選択_051.png

Scooldにメールアドレスとパスワードでログインして、質問を投稿しよう

今回は、メールアドレスとパスワードでの認証にしたいと思います。

1度、ParaとScooldを停止してそれぞれのapplication.confを修正します。

通常、メールアドレス認証を行うには、メール通知とverifyが必要ですが、今回はオフにします。

Para側のapplication.conf

# if true, users can be created without verifying their emails
para.security.allow_unverified_emails = true

Scoold側は、メールアドレスとパスワードでの認証を有効にします。

# enable or disable email&password authentication
para.password_auth_enabled = true

この状態でParaおよびScooldを起動すると、Sign inページに行くとログインフォームが現れるようになっています。

範囲を選択_052.png

Sign upを選んで、ユーザーを登録します。

範囲を選択_053.png

メールを送ったような画面は経由しますが(ローカルにSMTPサーバーがないので、メール送信に失敗したというエラーログは出ますが…)、この状態でログインできるようになります。

範囲を選択_054.png

範囲を選択_055.png

では、質問を投稿してみましょう。

範囲を選択_062.png

Markdownのプレビュー。

範囲を選択_060.png

投稿後の質問。

範囲を選択_061.png

回答を投稿した後。

範囲を選択_063.png

回答の投稿フォームは、質問のページ内にあります。

範囲を選択_064.png

Paraの構成を変更する

ここまで、とりあえずということでScooldおよびParaをインストールして簡単に動かすところまでを書いてみましたが、じゃあ実際に利用するにはどうか?というところでは、もう一歩踏み込む必要があります。

ScooldおよびParaでは、データベース、検索エンジン、キャッシュがプラグインになっており、導入環境に合わせて選択することができます。

デフォルトでは、H2 Database、Apache Lucene、Caffeineで動作します。

どのようなものが選べるかは、Paraのドキュメントを見るとよいでしょう。

以下のものから選択することができます。

  • データベース
    • Apache Cassandra
    • DynamoDB
    • MongoDB
    • RDBMS
    • H2 Database
  • 検索エンジン
    • Elasticsearch
    • Apache Lucene
  • キャッシュ
    • Caffeine
    • Hazelcast

ここでは、データベースをMongoDBに変えてみましょう。

$ wget https://oss.sonatype.org/service/local/repositories/releases/content/com/erudika/para-dao-mongodb/1.31.0/para-dao-mongodb-1.31.0-shaded.jar

JARは、プラグインのGitHubのreleaseページより取得しています。

取得したJARを、ParaのJARの中に放り込みます。

$ mkdir -p BOOT-INF/lib
$ mv para-dao-mongodb-1.31.0-shaded.jar BOOT-INF/lib
$ jar -u0f para-jar-1.31.0.jar BOOT-INF

MongoDBは、Dockerで用意しました。

$ docker container run -it --rm --name mongo -p 27017:27017 mongo:4.1

あとは、MongoDBを使うようにParaの設定を変更して、Paraを起動します。

para.dao = "MongoDBDAO"

データベースを切り替えたので、Paraのデータがなくなってしまうので再度/v1/_setupへのアクセスが必要になります。Scooldに対するキーの取得も、実施してください。

ところで、今回はUber JARを直接更新する形を取りましたが、MavenなどでJAR(もしくはWAR)を再構成するようにしてもOKです。

その場合、ParaのJARやWARの構成を参考に、自分でアーカイブを作成するとよいでしょう。

https://github.com/Erudika/para/blob/v1.31.0/para-jar/pom.xml#L85-L94
https://github.com/Erudika/para/blob/v1.31.0/para-war/pom.xml#L26-L37

ScooldおよびParaの構成的な話

このように、Paraはデータの保存先や検索エンジンなどをプラがブルに変更できるので、構成には柔軟性があります。

ですが、実際に導入するにあたっては、いろいろとカスタマイズしたりしたくなるかもしれません。

最後に少し、ScooldとParaの構成的な面に触れておこうと思います。

Scoold

Spring Boot、Spring Web MVCで構築されたアプリケーションです。主に、以下のフレームワーク、ライブラリで成り立っています。

  • Spring Boot(コンテナはJetty)
  • Spring Framework
  • Apache Velocity
  • Typesafe Config
  • jQuery
  • Materialize CSS
  • Font Awesome
  • SimpleMDE

また、データを扱う関する機能は一切持たず、データ保存、検索などについてはすべてParaにリクエストを送信することで行っています。

Para

Paraは、DIコンテナにGoogle Guiceを、APIはJerseyを使用して構成されたアプリケーションです。

  • Google Guice
  • Jersey
  • Typesafe Config
  • Spring Boot(一部のみ利用、コンテナはJetty)
  • Spring Framework
  • Spring Security
  • flexmark-java

なのですが、なぜか一部Spring Framework系の名前が登場します。サーブレットコンテナだったり、認証まわりでSpringを使用しています。

UIは、一切持ちません。

また、プラグインの仕組みはService Providerの仕組みで構築されています。

ScooldとParaでは、作りがまったく違います。Paraの方が構成的には複雑なのですが、実際にカスタマイズしたい場合はUIなどユーザーに触れる機能を多く持つScooldなのかなと思います。

Paraは、目的がデータ操作と検索に特化しているので、機能自体は割と単純なのです。

自分たちもScooldおよびParaをカスタマイズして使用していますが、カスタマイズの内容の大半はScooldであり、

  • 利用する人に合わせたUIの改修、改善
  • メール通知の機会の追加

などを行っています。

おわりに

QAサイトを実現するものとして、Stack OverflowクローンであるScooldを紹介しました。構築、インストールするだけなら、そう難しくありません。

ですが、実際に使ってもらえるかは別の話です。

チーム内だったり組織内だったりで使うにも、質問があっても放置されてしまったりすると、どんどん廃れていくのでどうやって文化として根付かせていくのかが大切な気がします。

Scooldに限りませんが、この手のものを導入するのであれば、実際に使う人たちが気軽に質問したり、回答をしてくれるようなプラットフォームになってくれると良いですよね。

JavaScriptのソース内で環境依存の値を切り替える

はじめに

Reactのアプリケーションの開発の際、環境ごとに値を切り替える必要があったので、その時採用した方法をまとめたものです。

方法

いくつか方法はありますが、今回は以下の方法を採用しました。

  • 環境毎に切り替える値をまとめたファイルを各環境毎に作成する
  • webpackでビルドする際に、環境変数からどの環境向けのビルドかを取得して、読み込むファイルを切り替える

切り替える方法はwebpackのarias機能を利用しています。

動作環境

$ npm --version
6.4.1
$ node --version
v10.13.0

使用したライブラリのバージョン
React: 16.6.1
webpack: 4.25.1

Node.jsでの環境変数

Node.jsでは環境変数はprocess.envで使用できます。

実装例

ここではAPIサーバのURLを例にしています。
ディレクトリ構造は以下のようになっています。

example
   ├─index.html
   ├─package.json
   ├─webpack.config.js
   │
   └─src
       ├─index.js
       │
       └─config
           ├─development.js
           └─staging.js

設定ファイル

開発環境向け

development.js
export default {
    API_URL: 'http://development.example.com'
};

ステージング環境向け

staging.js
export default {
    API_URL: 'http://staging.example.com'
};

設定ファイルを読み込む側。動作確認のために読み込んだ値を表示するだけの単純なコンポーネントにしています。

index.js
import React from 'react';
import { render } from 'react-dom';
import config from 'AppConfig';

const App = () => (
  <div>
    <p>URL: {config.API_URL}</p>
  </div>
);

render(
  <App />,
  document.getElementById('app'),
);

index.html
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>example</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

webpack

webpackの設定ファイル内で環境変数(今回はNODE_ENV)を読み込んでいます。
aliasの設定で、AppConfigのパスを環境変数を用いて指定しています。

webpack.config.js
const path = require('path');
const webpack = require('webpack');
const environment = process.env.NODE_ENV || 'development';

module.exports = {

  // 中略

  resolve: {
    extensions: ['.js', '.jsx'],
    alias: {
      AppConfig: path.join(__dirname, `/src/config/${environment}.js`)
    }
  },

  // 中略

}

ビルド

webpackでビルドします。

package.json
  "scripts": {
    "build": "webpack --progress --colors --mode production"
  },

ビルド実行時に、環境変数NODE_ENVで環境を指定すればOKです。
開発環境用ビルド
NODE_ENV=development npm run build

ステージング環境用ビルド
NODE_ENV=staging npm run build

実行結果

開発環境
dev-url.JPG

ステージング環境
staging-url.JPG

まとめ

簡単に切り替えることができました。
この方法では、環境毎に切り替える値が多くなっても、環境毎に一つのファイルに集約できるのでわかりやすいです。
その反面、環境毎にファイルを用意する必要があるので、環境が増える場合はファイルのか扱いが煩雑になりそうです。
また、このファイルは全てgitなどのバージョン管理に含める必要があります。
そのため、APIのアクセスキーなど外部に公開できない情報を扱う場合は別の方法を採用する必要があります。

開発者の開発者による開発者のためのAgileドキュメンテーション

開発者であれば避けては通れないドキュメンテーション。
しかし多くの開発者が嫌がっているドキュメンテーション。

今回はアジャイル型開発(以下、アジャイル)のプロジェクト・チーム開発者における最適なドキュメンテーションについて考察したいと思います。

アジャイルとドキュメント

ドキュメントは書かなくて良い?

日本においてアジャイルはゆっくりだが確実に浸透しています。
7年間アジャイルをしていた私としては嬉しく思う反面、世間には誤った認識が広がっているのもまた事実です。以下はアジャイル勘違い集の抜粋です。

Q. ドキュメントを書かない?
もちろんアジャイルプロセスでもドキュメントは書きます。ただ優先順位が異なるだけです。
...
最小限の文書化で最大限の効果を狙う、必要にして十分なドキュメント。そうしたドキュメントは一朝一夕に書けるものではありません。
ドキュメントを書かない?|アジャイル勘違い集

上記の通り、アジャイルではドキュメントを書かなくていいわけではありません。
しかし優先度を見極めて必要最小限のドキュメンテーションを行う、ということはとても難しいものです。

ドキュメントはどこまで必要か?

一重にドキュメントといっても様々な種類があります。

  • システム全体を俯瞰する
    • システム構成図
    • 業務フロー
    • ...
  • アプリケーションの機能を示す
    • 画面遷移図
    • データベース設計
    • ...
  • アプリケーションの実装詳細を示す
    • クラス図
    • ユースケース図
    • ...
  • 運用
    • 作業手順書 (リリース/定常作業)
    • ...

ウォーターフォール型開発においては、要件を満たす仕様/実装であるかを確認するために様々な角度の設計図を書き起こします。これは開発フローの性質上、必須な資料です。

しかしアジャイルではここまでのドキュメンテーションは必要ないように思えます。
(これはウォーターフォールを否定しているわけではありません。)

アジャイルでドキュメントと向き合う

アジャイルでドキュメントと向き合うときに念頭に置いておく必要がある考えがあります。

Agile methods are not opposed to documentation, only to valueless documentation.
(アジャイルの手法はドキュメントを書くこと全体に反対を示しているのでなく、価値のないドキュメントのみに反対を示しているのだ)
Documenting Architecture Decisions|Michael Nygard Blog

アジャイルでは ソースコードを最重要ドキュメントと位置づけて、価値のないドキュメントを排除することを目的としていると言えます。

ここでいう"価値のないドキュメント"といっているのは「最終成果物として価値のない資料」と言っているのではなく、アジャイル開発フローを進める上で価値のない資料であることを意味しています。ですので、ウォーターフォールと同様、システムの引き継ぎ時に必要に応じて作成する必要はあるかもしれません。

つまりは変化を許容するアジャイルにとってソースコードと同等の設計書を書くことは二重管理となり、開発速度を落とすことにつながることを意味しています。

開発を加速させるドキュメンテーション

ここからは必要最小限のドキュメンテーションで開発を加速させ最大限の成果を得るためにはどのドキュメントに注意を払うべきかについて考えていきます。

まずは私が最上位の優先度で、かつ、繊細に扱わないといけないと認識しているドキュメントです。

# Project Repository
 ├ doc/adr
 │  └ Architectural Decision Records (ADR)
 ├ src
 │  ├ Source Code
 │  └ Source Code Document
 └ README

# Engineer-ing
 ├ Pull Request (Merge Request)
 └ Commit Log

一つ一つのドキュメントに関して紹介していきます。

■Architectural Decision Records(ADR)

プロジェクトやチームにおけるアーキテクチャの意思決定の記録を残すためのドキュメントです。
Documenting Architecture Decisions|Michael Nygard Blog にて詳細に書かれています)

なぜこの文書が必要か?

変化を許容するアジャイルにおいて、意思決定の経緯は従来より重要になります。

プロジェクトの途中で様々なアーキテクチャやライブラリが変更されるといったことが起こりえる中で「なぜこの技術が採用されたか」などの決定経緯が追跡できなくなることはしばしば発生します。また追跡できない事態が続くことにより変化するための意思決定を避けたり、過去の経緯を無視して新たなものを採用する事態となります。

これらを回避するためには 意思決定の経緯をソースコードと紐づけて記録しておく必要があります。

どのような内容を書く?

以下の項目に沿って意思決定の記録を行います。
なお、ADRの具体例については以下Repositoryが参考になります。

Title (タイトル)

ADRの概略を書きます。

Context (コンテキスト)

コンテキストでは以下の事柄をいずれの背景に対しても中立な立場で、かつ、事実に基づき記載します。

  • この決定を必要とする理由
  • プロジェクトを取り巻く技術・社会・政治的な背景
  • とりえる選択肢と概要
  • etc.
Decision (決定事項)

アーキテクチャに関するプロジェクトの意思・決定事項を記載します。

  • 採用する技術や設計 (言語・ライブラリ・フレームワーク など)
  • 決定に至った理由や背景
  • 採用しなかった技術や設計に対する理由
  • etc.
Status (ステータス)

ADRの意思決定がどの状態かを記載します。以下の3種類から選択します。

ラベル 説明
proposed 提案中 :同意がなされていない状態
accepted 受理 :プロジェクト内で合意がなされている状態
deprecated 廃止 :本ADRの意思決定を変更あるいは取り消されている状態
Consequences (結果)

ADRを適用した後のコンテキストについて記載します。
良かった点だけでなく悪かった点を含めリストアップすることが望まれます。

■README

言わずと知れた README.md です。

なぜこの文書が必要か?

すでに多くの人が書いているドキュメントですが重要なドキュメントの一つです。
適切なアナウンスをすることでより重要なドキュメントへ導くことができます。

  • 設計図共有サイトでデフォルトで表示をしてくれるため開発者の目に留まりやすい
  • 開発者の目に留まりやすいため、すべてのドキュメントの起点としての役割を据えられる
    • 結果、ドキュメントの分散を抑えられる
どのような内容を書く?

数多の記事で紹介されているため、一つ一つの項目を取り上げることは割愛します。

以下はよくあるREADMEの一例です。

Project Title
--------------------------------------------
## Description(概要)

## Requirements(前提となる開発環境)
 - 言語
 - フレームワーク など

## Usage(使い方)
 - 開発環境の構築手順
 - アプリケーションの起動方法 など

## License(ライセンス)

■Pull Request / Merge Request

こちらもおなじみのPull Request(サイトによってはMerge Request)です。

なぜこの文書が必要か?

「そもそもこれ文書じゃなくね?」と言われるかもしれませんが、以下理由で開発者にとっては繊細に扱うべき文書だと考えます。

  • 立派なレビュー依頼票
  • VCSの変更管理の記録

ソースコードを最重要ドキュメントと捉えたとき、その変更に関わる

  • なぜこの変更が必要なのか? (Why)
  • どのような実装をしたのか? (What)
  • チェック観点
  • 影響範囲

をレビュアーに正しく伝えるための文書にあたりますので当然重要な文書と言えます。

どのような内容を書く?

まず内容を書き始める前に自分の切り出した作業単位(branch)が以下に沿っているかを確認する必要があります。

1. 小さくまとめる
小さく、焦点を絞ったプルリクエストは、承認される可能性が非常に高い。 ...
経験上、プルリクエストの大きさに対してレビュー時間は指数関数的に長くなってしまう。
2. 1つの事だけやる
単一責任の原則で、1つのクラスは1つの責任(役割)のみを持つとされているように、1つのプルリクエストは1つのテーマのみを扱うべきだ。
より良いプルリクエストのための10のヒント|Yakst

また 心構えとしてなぜあなたのPull Requestは読まれないのかにも目を通しておくことをオススメします。その上でPull Requestに内容を埋めていきます。

以下はよくあるPull Requestの一例です。

Pull Request Title
--------------------------------------------
## What(このPull Requestは何か?)

## Why(なぜこのPull Requestが必要か?)

## Impacted Areas in Application(影響範囲)

## Todo / Checklist
 - [ ] xxxxxx

## Refs
 - [#100]()

最後に忘れずにPull Requestをテンプレート化しましょう。

■コミットログ

こちらもおなじみのVCSにコミットする際のログです。

なぜこの文書が必要か?

ソースコードをコミットという意味のある作業単位にまとめ、ログをつけることは以下に役立ちます。

  • ソースコードの変更詳細をレビュアーに伝えられる
  • コミットのラベリングはコミットを切り戻す場合の検索性を高めてくれる

またコミットメッセージの書き方も合わせて目を通しておくと良いでしょう。
どのコミットもすべて同じコミットメッセージという人がたまにいますが言語道断です。

どのような内容を書く?

原則としては「1コミットは可能な限り細かい作業単位/粒度」にすることです。
加えて以下の工夫をすることでより良いコミットにすることができます。

コミット種別

以前にコミットメッセージに 「プレフィックス」 をつけるだけで、開発効率が上がった話を読んで大変感銘を受けましたが、今回私がおすすめしたいのが以下のEmojiプレフィックスです。

Emojiコミットの良い点としては、文字に比べて絵の方が直感的に変更内容を伝えられるという点です。

Emoji コミット種別 説明
:sparkles: New Feature 新規機能の追加
:recycle: Refactoring リファクタリング
:books: Documentation ドキュメンテーションの追加/更新
:art: Cosmetic デザインの追加/更新
:wastebasket: Removal ファイルや不要なコードの削除
... ... ...
コミットメッセージ本文

コミットメッセージ本文については以下が参考になります。

まとめ

  • アジャイルにおけるドキュメンテーションは軽視されがちだが、的を絞って注力することで最大の成果を得る武器になる
    • ADRを有効活用することで過去・現在・未来に渡って意思を伝えることができる
  • Pull Requestやコミットログなど普段何気なく書いているドキュメンテーションにも気をつけることで開発速度を加速されるファクターに変化させられる

参考文献

アジャイル

Docsガイドライン

ADR@kawasimaさんから教えていただきました。ありがとうございます。

Pull Request / Merge Request

コミットログ

Reactコンポーネントをnpmで公開する(GitHub Pages付き、Babel7、webpack4)

なにこれ

TIS Advent Calendar 2018の13日目の記事です。よろしくお願いします!

最近Reactコンポーネントをnpm公開してみました(参考記事:CSSのclip-pathでSlit Animationを実現する)。そこで今回は簡単なReactコンポーネントを作って、npm公開する方法を紹介します。
「React始めたんだけど...npmアカウント作ったんだけど...」という方でも30分くらいで公開できるので、とりあえず手を動かしたい人向けのチュートリアルです。
下記のような手順でnpm公開するまでの方法を見ていきましょう。

  1. コンポーネントを作成する
  2. デモページを作成する
  3. デモページをGitHub Pagesで公開する
  4. コンポーネントをnpmに公開する

※ 完成品はGitHubに公開しています => Takumon/react-component-sample
※ 完成品は下記のようなプロジェクト構成になります。

プロジェクトルート
├─dist              ・・・ コンポーネントビルド資産出力場所
├─src               ・・・ コンポーネント資産
│   ├─index.js
│   └─styles.css
├─examples          ・・・ デモページ用資産
│  ├─dist           ・・・ デモページビルド資産出力場所
│  └─src
│     ├─index.html
│     └─index.js
├─node_modules
├─.babelrc          ・・・トランスコンパイル用設定ファイル
├─webpack.config.js ・・・ビルド用設定ファイル
├─package.json      ・・・依存ライブラリ・スクリプト定義ファイル
├─.npmignore        ・・・npm登録除外対処定義ファイル
└─.gitignore

1. コンポーネントを作成する

まずはコンポーネントを作ってトランスパイルするところまで完成させましょう。

※ 本手順完了時のソースコードはこちら
※ 本手順完了時のプロジェクト構成 ↓

プロジェクトルート
+ ├─dist          ・・・ コンポーネントビルド資産出力場所
+ ├─src           ・・・ コンポーネント資産
+ │   ├─index.js
+ │   └─styles.css
+ ├─node_modules
+ ├─.babelrc      ・・・トランスコンパイル用設定ファイル
+ └─package.json  ・・・依存ライブラリ・スクリプト定義ファイル(追記)
  • 最初にプロジェクトの雛形を作ります。npm initで色々聞かれますが全てデフォルトで構いません。
mkdir react-component-sample
cd react-component-sample
npm init
  • 最低限のReact系ライブラリをインストールします。開発用ライブラリとしてインストールするので-Dオプションを付けてください。
npm i -D react react-dom
  • BabelでReactをトランスコンパイルするためのライブラリをインストールします。こちらも開発用ライブラリなので-Dオプションを付けましょう。
npm i -D @babel/cli @babel/cli @babel/core @babel/preset-env @babel/preset-react babel-loader
  • .babelrcを作成し、Reactをトランスコンパイルするための定義を記載します。
.babelrc
{
    "presets": [
        "@babel/preset-env",
        "@babel/react"
    ]
}
  • コンポーネントを作ります。
src/index.js
import React from 'react';
import './styles.css';
const MyComponent = () => (
    <h1>Hello from My Component</h1>
);
export default MyComponent;
  • コンポーネントで読み込むCSSを作ります。
src/styles.css
h1 {
    color: red;
}
  • トランスパイル用スクリプトをpackage.jsonに追加します。具体的にはJSファイルをトランスパイルしdistフォルダに出力、それ以外のファイル(CSS)をdistファイルにコピーするスクリプトです。
package.json
    "scripts": {
+     "transpile": "babel src -d dist --copy-files"
    },

確認

  • 準備が整ったので、トランスパイルしてみましょう。
npm run transpile
  • 下記のようにdistフォルダ配下にindex.jsとstyles.cssが生成されればトランスパイル成功です。
トランスパイル後の資産
プロジェクトルート
├─dist           ・・・ コンポーネントビルド資産出力場所
│   ├─index.js
│   └─styles.css

2. デモページを作成する

実際にコンポーネントを使用したデモページもあわせて用意しておきましょう。コンポーネントの使い方をユーザーにわかりやすく示すことができます。
ここではローカルでデモページが見れるところまでを作成します。

※ 本手順完了時のソースコードはこちら
※ 本手順完了時のプロジェクト構成 ↓

プロジェクトルート
  ├─dist
  ├─src
  │   ├─index.js
  │   └─styles.css
  ├─node_modules
  ├─.babelrc
+ ├─examples          ・・・ デモページ用資産
+ │  ├─dist           ・・・ デモページビルド資産出力場所
+ │  └─src
+ │     ├─index.html
+ │     └─index.js
+ ├─webpack.config.js ・・・ビルド用設定ファイル
+ └─package.json      ・・・依存ライブラリ・スクリプト定義ファイル
  • デモページはwebpackでビルドするので必要なライブラリをインストールします。
npm i -D html-webpack-plugin webpack webpack-cli webpack-dev-server css-loader style-loader
  • デモページのHTMLを作成します。
examples/src/index.html
<html>
<head>
    <title>My Component Demo</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
</head>
<body>
    <noscript>
        You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
</body>
</html>
examples/src/index.js
import React from 'react';
import { render } from 'react-dom'
import MyComponent from '../../src'

const App = () => <MyComponent/>;
render(<App />, document.getElementById('root'));
  • デモページのビルド設定ファイル(webpack.config.js)を作りましょう。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// HTMLファイルのビルド設定
const htmlWebpackPlugin = new HtmlWebpackPlugin({
    template: path.join(__dirname, 'examples/src/index.html'),
    filename: './index.html'
});
module.exports = {
    // 依存関係解決の起点となる資産を指定します。
    entry: path.join(__dirname, 'examples/src/index.js'),
    // Babelのトランスパイル対象資産を指定します。
    module: {
        rules: [
            {
                test: /\.(js|jsx)/,
                use: 'babel-loader',
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: ["style-loader", "css-loader"]
            }
        ]
    },
    plugins: [htmlWebpackPlugin],
    resolve: {
        extensions: ['.js', '.jsx']
    },
    // 開発用Webサーバのポートを指定します。
    devServer: {
        port: 3001
    }
}
  • デモページ起動用スクリプトをpackage.jsonに追記します。
package.jsonの一部
    "scripts": {
+     "start": "webpack-dev-server --mode development"
    },

確認

  • 準備が整ったのでデモページを起動しましょう。
npm start
  • ブラウザでhttp://localhost:3001にアクセスしてコンポーネントが表示されればOKです。

図1.png

3. デモページをGitHub Pagesで公開する

デモページをローカルで見れるようになったら、次はGitHubに資産を登録し、GitHub Pagesで公開しましょう。プラグインで簡単に公開できます。

※ 本手順完了時のソースコードはこちら
※ 本手順完了時のプロジェクト構成 ↓

プロジェクトルート
  ├─dist              ・・・ コンポーネントビルド資産出力場所
  ├─src               ・・・ コンポーネント資産
  │   ├─index.js
  │   └─styles.css
  ├─examples          ・・・ デモページ用資産
  │  ├─dist           ・・・ デモページビルド資産出力場所
  │  └─src
  │     ├─index.html
  │     └─index.js
  ├─node_modules
  ├─.babelrc          ・・・トランスコンパイル用設定ファイル
+ ├─webpack.config.js ・・・ビルド用設定ファイル(追記)
+ ├─package.json      ・・・依存ライブラリ・スクリプト定義ファイル(追記)
+ └─.gitignore
  • github-pagesというGitHub公開用プラグインをインストールします。
npm i -D gh-pages
  • package.jsonにGitHubPages公開用スクリプトを3個追加します。
    • publish-demoはビルドとデプロイをいっぺんにやるスクリプトです。デプロイ前に大抵の場合ビルドするので、1つにまとめておくと便利です。
package.json
    "scripts": {
+     "build": "webpack --mode production",
+     "deploy": "gh-pages -d examples/dist",
+     "publish-demo": "npm run build && npm run deploy"
    },
  • webpack.config.jsに出力先を指定します。
webpack.config.jsの一部
  module.exports = {
+     output: {
+        path: path.join(__dirname, "examples/dist"),
+        filename: "bundle.js"
+    },
  }
  • ではビルドしてみましょう。
npm run build
  • ビルドされて最小化された資産がexamples/dist配下に出力されたのが確認できればOKです。

  • Gitにあげる準備として.gitignoreを作りましょう。

.gitignore
node_modules
dist
  • GitHubでリポジトリを新規作成して資産を登録しましょう。新規作成後の「...or create a new respository on the command line」の説明に従ってください。

確認

  • 下記を実行してGitHub Pagesに登録しましょう。
npm run publish-demo

(補足)画像ファイルを扱う場合

もしデモページで画像を読み込む場合はfile-loaderurl-loaderを開発用依存ライブラリに追加してください。ビルド設定も下記のように修正が必要です。

npm i -D file-loader url-loader
webpack.config.jsの一部
  module.exports = {
    module: {
+     {
+       test: /\.(jpg|png|ico)$/,
+       use: 'url-loader'
+     },
    },
  }



これであとは最終手順のnpm公開を残すのみです!

4. コンポーネントをnpmに公開する

デモページも準備できたので、いよいよコンポーネントをnpmに公開しましょう。

※ 本手順完了時のソースコードはこちら
※ 本手順完了時のプロジェクト構成 ↓

プロジェクトルート
  ├─dist              ・・・ コンポーネントビルド資産出力場所
  ├─src               ・・・ コンポーネント資産
  │   ├─index.js
  │   └─styles.css
  ├─examples          ・・・ デモページ用資産
  │  ├─dist           ・・・ デモページビルド資産出力場所
  │  └─src
  │     ├─index.html
  │     └─index.js
  ├─node_modules
  ├─.babelrc          ・・・トランスコンパイル用設定ファイル
  ├─webpack.config.js ・・・ビルド用設定ファイル
+ ├─package.json      ・・・依存ライブラリ・スクリプト定義ファイル(追記)
+ ├─.npmignore        ・・・npm登録除外対処定義ファイル
  └─.gitignore
  • トランスパイル後に生成されるdist/index.jsを、npm公開資産のメインファイルに指定します。
package.json
+   "main": "dist/index.js",
  • 次にnpm公開時に自動で走るスクリプトprepublishOnlyを追加します。これによりnpm公開時にビルドし忘れるということを防ぎます。
package.jsonの一部
    "scripts": {
+     "prepublishOnly": "npm run transpile"
    }
  • このコンポーネントを使う側には、Reactがインストール済という想定ですのでpeerDependenciesを指定します。
package.jsonの一部
+   "peerDependencies": {
+     "react": "^16.3.0",
+     "react-dom": "^16.3.0"
+   },
  • .npmignoreを作成して、npm公開資産として不用な資産(トランスパイル前のjsファイルなど)は公開対象外としましょう。
.npmignore
src
examples
.babelrc
.gitignore
webpack.config.js
  • 最後にパッケージ名(package.jsonのname)を決めます。現段階ではreact-component-sampleとなっていて、お試しで作るコンポーネントとしては少し汎用的すぎる名前なので、@自分のnpmアカウント名/raect-component-sampleのようにしましょう。例えば下記のように修正します。
package.json修正例
-   "name": "react-component-sample",
+   "name": "@takumon/react-component-sample",

確認

  • ではnpm公開してみましょう。
    • @takumon/react-component-sampleのようなパッケージ名は、Scoped Packag(npmのプライベートなパッケージの命名規約)に沿っているのでデフォルトでプライベート公開になってしまいます。npmで有料契約をせずにプライベート公開しようとすると402エラーになります。そのため、ここでは--access=publicをつけて一般公開するようにしています。(参考:stachoverflow)
    • Failed PUT 403になる場合は、npmの認証エラーです。npm loginしましょう。 それかpackage.jsonversionが古いのが原因です。いったん公開したバージョンで再公開はできません。バージョンをインクリメントしましょう。
npm publish --access=public
  • これで、npm公式サイトを確認しパッケージが追加されていれば、公開完了です!

図4.png

おわりに

今回紹介したように、わりと簡単にnpm公開できるので、普段使いまわしているようなコンポーネントがあれば公開してみるのもいいかもしれません。

参考

あなたの機械学習システム構築を手助けする、TensorFlow Extended

今日では、機械学習が研究者だけでなく個人レベルで利用できるような時代になってきました。これは、計算機の性能向上や機械学習フレームワークなど開発環境の充実、大量データが手に入りやすくなってきたことなどが要因として挙げられます。

一方、機械学習を用いたシステム(以後本記事では機械学習システムと呼びます)の構築にはハードルがあります。データ傾向の変化など、これまでのシステムにない考慮すべき点が多く存在するからです。2015年の論文においては機械学習モデル作成は一部分でしかなく、運用においてはその他の要素が大きく影響すると述べられていますが、現在でも状況は大きく変わっていないように感じます。

machine_learning_platform.png

出展:https://dl.acm.org/citation.cfm?id=3098021

本記事ではGoogleが提供する機械学習システムの開発プラットフォームであるTensorFlow Extended(TFX)を紹介します。これは上記の図の「Forcus this paper」にあたる機械学習システム構築時に必要となる処理、機能を提供するプラットフォームであり、以下に提示したライブラリで構成されます。

  • TensorFlow Data Validation
  • TensorFlow Transform
  • TensorFlow Model Analysis
  • TensorFlow Serving

本記事ではTFXのチュートリアルのコードやJupyter notebook で実行した結果を交えながら説明を行っていきます。なお、Servingについては紹介記事が多くあるので今回は触れません。

TensorFlow Data Validation

TensorFlow Data Validation は与えられたデータについて、統計量の可視化やデータの検証を行うためのライブラリです。機械学習において与える訓練データの傾向を把握することは大切です。カラムのデータが数値なのか文字列なのかや必ずデータが存在するのか、数値の場合は正規化を行うべきなのかなど確認・考慮すべき点が多くあるためです。このライブラリでは与えられたデータを読み込んで特徴を可視化を実現します。

機能としては大きく分けて3つあり、データの傾向を掴む統計量の確認、与えられるデータの構造であるスキーマ推定、スキーマを用いた異常値の検出を行う事ができる。

統計量の確認

一般的な統計量は以下のようなコードで求めることができます。実行結果は図のようになります。

# Compute stats over training data.
train_stats = tfdv.generate_statistics_from_csv(data_location=os.path.join(TRAIN_DATA_DIR, 'data.csv'))

# Visualize training data stats.
tfdv.visualize_statistics(train_stats)

visualize.png

カラムごとに取得できる統計量は以下です。

数値データ

  • 出現数
  • 欠損率
  • 平均
  • 標準偏差
  • 値ゼロ率
  • 最小値
  • 中央値
  • 最大値

カテゴリデータ

  • 出現数
  • 欠損率
  • ユニーク数
  • 最多出現単語
  • 最多出現単語の出現回数
  • 単語平均長

スキーマ推定

次に、獲得した統計量を利用してこのデータセットのスキーマを推定ができます。訓練データを用いて正しいと思われるデータ構造を推定することでデータの特徴を把握するとともに、後述するエラーを検出に利用します。ただし、推定したスキーマ定義が正しいのか、人間の目でちゃんと確認する事が強く推奨されています。(本当にrequired なカラムであるか、optional なカラムであるかなど)

# Infer a schema from the training data stats.
schema = tfdv.infer_schema(statistics=train_stats, infer_feature_shape=False)
tfdv.display_schema(schema=schema)

上記コードの実行結果は以下のようになります。

scheme.png

推論されたスキーマは以下形式で出力されます。

  • データ数
  • データタイプ
  • データの必須・オプションの区分
  • Valency
  • ドメイン名

カテゴリデータの場合はドメイン名ごとにすべてのカテゴリが表示されます。

domain.png

評価データの異常値確認

これまで訓練データを対象としてきましたが、評価データについても分析が行えます。評価データに対しても統計量を計測し、訓練データの傾向と違いがないか比較することができます。

compare.png

また、前節で作成したスキーマを用いて評価データ中の異常値(これまで見られなかった値など)を確認することができます。以下実行結果を例にすると特徴名「payment_type」において訓練データでは見られなかった「Prcard」が出現していることが確認できます。

# Check eval data for errors by validating the eval data stats using the previously inferred schema.
anomalies = tfdv.validate_statistics(statistics=eval_stats, schema=schema)
tfdv.display_anomalies(anomalies)

anomalies.png

もし予見できる(異常値ではない)データであるならば、対象データを指定したり、min_domain_massを設定してスキーマをアップロードすることによって正常データに含ませることができます。
(ソースによるとmin_domain_massは異常値を許容するしきい値であり、は1(デフォルト)ならばドメイン内のデータすべてが訓練データに含まれていないといけない、0.9なら90%はドメインに含まれていないといけないと判定するようです。)

逆に予期していなかったエラーデータであれば、学習すべきデータが含まれていなかったり異常であるということになるので、訓練データや評価データの見直しを図る必要があるでしょう。

異常値として検出されるエラータイプは以下から確認できます。
https://github.com/tensorflow/metadata/tree/master/tensorflow_metadata/proto/v0/anomalies.proto

補足:環境に応じたスキーマ設定

例えば推定したい正解ラベルは訓練データのみに存在するように、訓練データと検証や本番データのカラムは常に一致するとは限りません。このままでは異常値として検出されてしまうため環境ごとにスキーマを用意し、チェックしなくてよいカラムを除外することで希望とするカラムだけ異常値検出することができます。

このように TensorFlow Data Validation では与えるデータの特徴についてフォーカスし、学習や推論を実行するまでのプロセスをサポートしてくれます。

TensorFlow Transform

このライブラリは前処理を行うためのライブラリです。前処理とはモデルにデータを与える前に何らかの処理を施すことを指します。前処理を行う効果や必要性はこちらの記事が参考になります。

前処理一例としては単語をインデックスへの変換するなどがありますが、これは学習時も推論時も同一の処理を行う必要があります。開発を行っていると、前処理は時間がかかるためとモデルの学習と別々にしてしまうことがあり(私だけ?)、いざ本番での実行を見越した際に前処理とモデルをつなげるコードが新たに必要となってしまうなんてことがあります。そこでTensorFlow Transform は代表的な前処理方法を提供することでモデルと前処理を近づけて、End-to-End(生のデータから推論結果を得る)処理の実現をしやすくしてます。

# Transform training data
preprocess.transform_data(input_handle=os.path.join(TRAIN_DATA_DIR, 'data.csv'),
                          outfile_prefix=TFT_TRAIN_FILE_PREFIX, 
                          working_dir=get_tft_train_output_dir(0),
                          schema_file=get_schema_file(),
                          pipeline_args=['--runner=DirectRunner'])
print('Done')

実行できる前処理の例としては以下などあります。

  • 平均値を標準偏差を用いた正規化
  • テキストデータに対してボキャブラリを作成し、インデックスへの変換
  • データ分布に基づき、浮動小数点の値を整数に変換

公式サイトのAPIを確認すると他にも様々あるので見てみてください。
どのカラムにどのような前処理を行うのかは自身で明示的に実装する必要があります。上記の例ではpreparocess内で様々な前処理が行われています。

TensorFlow Transform によって学習時と推論時の前処理適用が統一できるようになり、処理実行の負担軽減が期待できそうです。

TensorFlow Model Analysis

このライブラリでは作成したモデルの評価を行うことができます。従来でも訓練データや評価データに対して作成したモデルの正解率などを計測することは、もちろん行われてきました。一方で、推論がうまくいかない特定のデータに対して分析するなど、個別に集計して確認することはなかなか手間がかってしまいます。

TensorFlow Model Analysis はモデル評価を様々な切り口(以降では観点と呼称します)で手軽にできるようにします。このことにより、モデル性能の改善ヒントを得られるかもしれません。

以降では3つの分析方法を紹介します。

Visualization: Slicing Metrics

カラムの種類や値などを観点として分析する方法です。例えばあるカラムの値ごとに正解率を算出し、グラフ化することができます。観点の指定の例としては以下のようになります。

# An empty slice spec means the overall slice, that is, the whole dataset.
OVERALL_SLICE_SPEC = tfma.SingleSliceSpec()

# Data can be sliced along a feature column
# In this case, data is sliced along feature column trip_start_hour.
FEATURE_COLUMN_SLICE_SPEC = tfma.SingleSliceSpec(columns=['trip_start_hour'])

# Data can be sliced by crossing feature columns
# In this case, slices are computed for trip_start_day x trip_start_month.
FEATURE_COLUMN_CROSS_SPEC = tfma.SingleSliceSpec(columns=['trip_start_day', 'trip_start_month'])

# Metrics can be computed for a particular feature value.
# In this case, metrics is computed for all data where trip_start_hour is 12.
FEATURE_VALUE_SPEC = tfma.SingleSliceSpec(features=[('trip_start_hour', 12)])

# It is also possible to mix column cross and feature value cross.
# In this case, data where trip_start_hour is 12 will be sliced by trip_start_day.
COLUMN_CROSS_VALUE_SPEC = tfma.SingleSliceSpec(columns=['trip_start_day'], features=[('trip_start_hour', 12)])

ALL_SPECS = [
    OVERALL_SLICE_SPEC,
    FEATURE_COLUMN_SLICE_SPEC, 
    FEATURE_COLUMN_CROSS_SPEC, 
    FEATURE_VALUE_SPEC, 
    COLUMN_CROSS_VALUE_SPEC    
]

「FEATURE_COLUMN_SLICE_SPEC」 のように「trip_start_hour」の値毎に集計をおこなったり、「COLUMN_CROSS_VALUE_SPEC」 のように「trip_start_hour」の値毎かつ「trip_start_hour」が12のものといったように計測したい条件を組み合わせることが可能です。

trip_start&trip_start_hour.png

Visualization: Plots

ROC Curve やPrediction-Recall Curve など用意されたプロット方法を用いて特定の観点の分析を行う事ができます。以下は「trip_start_hour」カラムが0のものに対してプロットした結果です。

plot.png

Visualization: Time Series

機械学習は与えるデータの傾向が変わると性能も変化する可能性があります。また、モデルの改良を加えることによって性能が向上することもあれば、低下することもあります。そこで与えるデータの変化や同一データに対して適用させるモデルが変化によって、機械学習の性能がどのように影響を受けたのか記録、確認する事は重要です。Time Series ではこの時系列で性能がどのように変化していったのかを確認することができます。
以下はモデルのパラメータを変更した3つのモデルをプロットしたものです。(見づらいですが10桁の数字がモデル番号です。)ここではaccuracyとaucについてプロットしてますが他にもaverage lossなど様々な指標で確認することができます。

timeline_graph.png

TensorFlow Analyze による3つの分析を用いることによって手間がかかっていた詳細な分析を手軽にできるようになり、より効率的に機械学習の開発が行えるようになるかもしれません。

まとめ

ここまで紹介したようにTensorFlowは機械学習モデルの構築だけでなく、周辺の機能も多く提供しています。年末にはTensorFlow 2.0のリリースもあるとのことなので今後も要チェックです!

機械学習や自然言語処理について、つぶやいてますのでフォローしていただけると嬉しいです。あとブログもやってますのでよろしくお願いします!
@kamujun18
Technical Hedgehog

Reference

https://ai.googleblog.com/2017/02/preprocessing-for-machine-learning-with.html
https://medium.com/tensorflow/introducing-tensorflow-data-validation-data-understanding-validation-and-monitoring-at-scale-d38e3952c2f0
https://medium.com/tensorflow/introducing-tensorflow-model-analysis-scaleable-sliced-and-full-pass-metrics-5cde7baf0b7b
https://www.youtube.com/watch?v=vdG7uKQ2eKk

popoto.jsでネットワーク「図」を検索してみよう

前置き

せっかく 会社アドカレ なのでお仕事関連のネタでやろう、Neo4j で最近見てた話でやるかなーと思ってたら 5 日目で ike_dai さんが システム運用の世界をグラフで表現すると良いことあるかも?Neo4jのメリット感を体験-パッケージ依存関係管理- - Qiita という記事できたじゃないですか。似てる領域をやってるのでまあそういうこともあるよね。

さて。

最近「ネットワーク図」を中心にした運用業務の改善……みたいな話ができないかな、ということを考えています。ネットワーク機器を設定する・操作する、というのはもういろいろできるようになったので、その次のステップですね。ネットワーク(全体)に対する操作それ自体をどう考えるか・どう組み立てるか? ということを考えると、「図」をもとに判断するしていることがいろいろあるはず。でも、ネットワーク図の書き方って人や案件によってまちまちだし、そもそも Visio だったり PowerPoint だったり、アレだと Excel だったり……。図を読み書きする、図を元にやることを考える、みたいなところは、結局は人がやっているのがネックだよね、というのが問題意識です。

そんな感じの話を Open NetworkIng Conference Japan 2018 でしゃべってきたりしたので詳しくはそちらをみてください。

上の資料にあるように、 RFC 8345 を元にいろいろやろうとしています。方向性として「見せ方」と「データモデルやデータの扱い方」とがあるかなと思ってるんですが、ここでは後者の話をします。

ネットワークトポロジのデータを Neo4j であつかう

先に挙げた資料 で説明しているのですが、とりあえずお試しで書いてみたネットワーク図と、それを元に起こした json data があるとします。

これに以下のような relation を設定して Neo4J に登録します。(この辺は省略… netomox を使っています)

  • "network" は複数の "node" 1 (と "link") で構成されている。 いま、"network" はひとつのネットワークレイヤとして扱っています。
  • "node" は複数の "termination point" (ポートと思ってよい)を持つ
  • "termination point" は他の "termination point" とつながっている ( "link" に相当…今回は "term point" 間の relation として表現2)
  • それぞれ "support" で他の "network" (layer) にあるノードをポイントできる。

relation defs

とりあえず登録したノードを全部出してみるとこんな感じ。

all nodes

はい。何が何だかわかりませんね。ここから Cypher query 書いて必要な情報を抽出していくわけです。……が、この辺はそれなりにトレーニングが必要なので私のような初心者にはちょっととっつきにくい。

popoto.js でインクリメンタルなデータ検索

もうちょっと手軽に(Cypherとかあまりくわしくなくても)・インクリメンタルにデータ見ていくってのができないかな…というところで登場するのが Popoto.js です。 Neo4j WebUI の Cypher コマンドラインみたいな万能性はないのですが、クリックしながら順に関係性を定義してデータを検索していくことができます。

セットアップの方法とかはいったんおいといて、このツールでどんなことができるのか、例を見てみましょう。

Layer1 で SW1/SW2 の両方につながっている機器を探す

ちょっと冗長ですが順に見ていきましょう。

"node" を探すので、左側の Taxonomy から "node" をクリックします。

first step

[35] は検索結果の数です。このときページ下部、Query/Result で検索結果が出ています。

result view

以降、クリックして条件をしぼりこむごとに Query/Result が変化していきます。

この 35 件はすべての "network" に含まれる "node" の総数なので、いったん Layer1 にしぼりこみましょう。

filter by network

constructed_with relation をたどって Layer1 (target-L1) を選択します。

select network

選択すると、"node" 検索結果が 7 件になりました。これは Layer1 (target-L1) に含まれる "node" の総数です。

ここから接続しているノードの関係性を指定していきます。まず 「SW1 につながっている」条件を指定します。元のデータ定義から、隣接する "node" の接続は "node" - "term point" - "term point" - "node" になっているというのがわかっているのでそのまま展開していきます。

nodes connected with SW1

右端の SW1 意外、"node", "term point" は特に値を選択していません。接続関係だけを指定しています。SW1と "term point" ("link") を介して直接つながっていること、という条件を追加することで Result は 3 件までしぼりこまれました。

同じ操作をもう一度追加して今度は 「SV2 につながっている」の条件を追加します (AND 条件の指定)。

nodes connected with SW1 AND SW2

Result は最終的に 1件 になりました。 (target-L1/HYP1)

なにができたのか

popoto.js でグラフ構造クエリをインクリメンタルに実行してみました。クエリ自体としては、単純な例を取り上げたんですが、これ、「図がそのままクエリになる」(あるいは「クエリを図として表現する」) というのが特徴的ですね。元のグラフトポロジ (Layer1) を抜粋してみましょう。

Layer1 topology

クエリのために作った構造と、実際のトポロジが一致している(赤で囲ったところ)のがわかるかと思います。ある意味、絵で「このパターンでつながっているところ」を描いてトポロジデータを検索したような感じです。「こういう関係性でつながっているもの」みたいにデータを抽出していくにはおもしろいツールだと思います。


  1. 「ノード」が neo4j 用語とネットワークトポロジ用語とで被ってわかりにくいですね…。"node" のようにクォートしているものはネットワークトポロジデータの用語だと思ってください。 

  2. "node" - "termination-point" の間の関係と "termination-point" 同士の関係のどちらも connected にしてありますが、これはどことどこがつながっているかをたどるときに同じ relation name でたどれた方が楽かな、という発想でこうしてあります。トポロジデータ(元の JSON)では "termination point" は "node" に包含される形だし、 "termination point" 間の接続は "link" として定義されています。 

SlackとZoomを使ったロケーションに依存しないふりかえり方法

この記事はTIS Advent Calendar 2018の 16日目です :santa:

どうも、@tenten0213 です。

私が所属する開発チームは、東京5名、大阪1名で構成されています。
タイトルにロケーションに依存しないと書いているのですが、東京、大阪のロケーションの違いを意図しています。

弊社はリモートワークOKなのですが、開発チームのメンバーは出社していることがほとんどです。
MacBook Pro メモリ32GB, Core i9を買ってもらったので、家より快適っていうのもあるのかもしれませんが…:sweat_smile:

ただ、家でメールや事務処理を片付けてから出社したり、この日はリモートワークすると決めて働いているメンバーもいます。特に縛りはなく、各人の裁量で行っています。(ルール上会社のPCにRDPして働くことが求められているため、開発するのは厳しく、事務作業やドキュメント作業などがメインになりがち…)

私のチームは、開発プロセスにスクラムを採用しています。
ロケーションが離れたなかで、どうコミュニケーションを取っているか、特にふりかえりについて書きたいと思います。

slack_zoom.png

通常のふりかえり

スクラムでは1スプリント毎にふりかえり(レトロスペクティブ)を行います。
ふりかえり方法は特に規定されていませんが、自身はKPTで行うことが多いです。KPTは、Keep, Problem, Tryの略です。Keepは良かったこと、続けたいこと、Problemは悪かったこと、問題だと感じていること、Tryは次に試したいことをあげます。

KPTやふりかえりの具体的な方法については以下が参考になります。

ロケーションが離れていない場合は、模造紙と付箋を利用してKPTを行っていました。
各人が付箋にKeepを書き、内容を共有、Problemを付箋に書き、共有…といった流れです。

ZoomとSlackを使ったロケーションに依存しないふりかえり

冒頭で書いたとおり、開発メンバーが東京、大阪とロケーションが別れています。
では、普段どうやってコミュニケーションを取っているかというと、SlackとZoomを使っています。全社にZoomが導入されたので、毎朝のデイリースクラムもZoomを利用して行っています。タスク管理はJIRAを使っているのですが、Zoomで画面共有しながら話せるのでかなり便利です。

ふりかえりは、ロケーションが離れているため模造紙や付箋を使うことができません。

そこで、普段利用している SlackとZoomを利用してふりかえりを行ってみました。

やりかた

ふりかえり方法は変更せず、KPTです。

後からの見返しやすさを考慮し、SlackにKPT用のチャンネルを用意しました。
また、Zoomで画面や音声、ビデオを共有して行っています。

まず、今回のスプリントを思い返しながら「こんなことやったよね」と共有します。1週間だと割と覚えているのですが、2週間以上のスプリント期間だと実施したことを忘れがちなので…
私のチームはJIRAを使っているので、JIRAの画面を共有しつつスプリントで実施した内容を簡単にふりかえりました。

次に、以下のようにSlackでKeepをあげていきます。
付箋を使ってふりかえりを行う際は、書ききった後にチームで共有することが多かったのですが、Slackではドンドン書き込んでもらうようにしました。この時、絵文字でリアクションすると盛り上がって良いです。先に書かれちゃったなって時もリアクションすると参加している感がでます。

kpt_k.png

Keepをあげきった後に、自分が書いた内容をチームに共有します。この時、ファシリテーターはZoomで繋いでいる先にも声をかけるのを忘れないようにしましょう。(1度忘れました…)

後は同様の流れでProblem、Tryを実施していきます。

kpt_p.png

良い点

  • 物理的に距離が離れていてもチームでふりかえりを行える
  • 絵文字でのリアクション便利
    • 積極的じゃない人もリアクションなら付けやすい
  • 見返しやすい

悪い点

  • その場でグルーピングできない(付箋なら簡単)
  • タイピング速度重要
    • スマホでZoom繋ぐと大変そう
  • すぐ目につくとこに置くようなことができない(物理じゃないので)
  • 表情や仕草など文字以外の情報が読み取りづらい
    • Zoomで繋いでいるとはいえ、全員映せなかったり…

その他(おまけ?)

プロダクトバックログアイテムの見積もりもSlackとZoomを使って行っています。プロダクトバックログアイテムはJIRAで管理しています。

見積もりはプランニングポーカーで行っており、プロダクトバックログのタイトルをコピーしSlackに貼り付け、絵文字リアクションの数字で見積もっています。
…意外と便利です。

planning.png

まとめ

昨今リモートワークが当たり前になりつつあり、弊社のようなSIerも実施をはじめています。
しかし、アプリケーション開発はチームで行うことが多く、密にコミュニケーションを取る必要があります。

SlackやZoomのようなツールによって、ロケーションが離れているメンバーとのコミュニケーションの敷居が急速に下がってきています。このようなツールを便利に使い、効率よく効果的なコミュニケーションを取っていきたいです。

くれぐれもツールを使うことが目的にならないように!(自戒)

DBアクセスで遅くなったテストの実行時間を Docker で 40% 削減した方法

DBのレイヤーを含むエンドツーエンドテストやDBに依存したコンポーネントの自動テストがたくさんあると、全てのテストが終わるまでに長い時間がかかるようになってしまうことがあります。DBのクエリ実行はネットワークIOやディスクIOなどを含んだ高コストな処理だからです。

Docker を少し工夫して使うと、お手軽にテスト中のDBのクエリ実行にかかる時間を削減できます。自動テストが完了するまでの待ち時間を短縮し、開発のフィードバックサイクルをより早く回せるようになります!

MariaDB を用いたプロジェクトの実績では、DBアクセスを伴うテストケースが 153件 ありましたが、この方法によりそのテストスイートのローカル環境での実行時間を約 43% 削減できました(約 145.7s → 約 83.3s)。

どうやって?

Docker で tmpfs を使います。

tmpfs

https://docs.docker.com/storage/tmpfs/

tmpfs とは、ディスクの代わりにメモリへデータを書き込むファイルシステムで、下記のような特徴があります。

  • メモリに対してデータを読み書きするためIOが高速
  • コンテナを停止すると tmpfs 上に保存されたデータは全て消える
  • コンテナ間でのボリュームの共有は不可

つまり、DBのクエリ実行の中でコストが高めのディスクIOを、よりコストが低いメモリIOに置き換えることで高速化するというとても単純な戦略です。

DB のデータが全てメモリに乗ることになるため、コンテナを停止するとデータは全て消えてしまいますが、自動テストで読み書きするデータに耐久性が要求されることはまずないでしょう。手動でテストしたい場合など、データが消えると困る場合は tmpfs ではなくディスクにデータを書き込む DB のコンテナを別に作ったほうが良いです。

ローカルの開発環境で使う

各プロジェクトメンバーが tmpfs が有効になったコンテナを使ってローカルでテストを実行できるようにするには docker-compose が便利です。docker-compose は Docker コンテナの構成を管理するためのツールで、docker-compose.yml ファイルひとつで各プロジェクトメンバーが開発に必要なミドルウェアが入った Docker コンテナ群を素早くローカル環境にセットアップできるようになります。

プロジェクトディレクトリの直下に docker-compose.yml を置いて、テストを実行する前にすぐコンテナを立ち上げられるようにしておくと良いでしょう。

プロジェクト構成の例

project/
  ├── src
  ...
  ├── docker-compose.yml
  ...
  └── README.md

実行例

$ docker-compose up -d    # コンテナ群を起動
$ sbt test                # テスト実行

メジャーな RDBMS のコンテナを docker-compose で定義するサンプルを書いてみました。

RDBMS がデータを保存するディレクトリに tmpfs がマウントされるように指定しています。tmpfs:という設定項目で tmpfs をマウントするディレクトリを指定できます。サンプルにはとりあえず起動するための必要最低限の項目しか書いていないため、実際にプロジェクトで利用する場合は、必要に応じてカスタマイズしてください。

MySQL

docker-compose.yml
version: "3"

services:
    mysql-for-test:
        image: mysql:8.0.13
        tmpfs:
            - /var/lib/mysql
        ports:
            - 3306:3306
        environment:
            - MYSQL_ROOT_PASSWORD=mysql

MariaDB

docker-compose.yml
version: "3"

services:
    mariadb-for-test:
        image: mariadb:10.4.0
        tmpfs:
            - /var/lib/mysql
        ports:
            - 3306:3306
        environment:
            - MYSQL_ROOT_PASSWORD=mysql

PostgreSQL

docker-compose.yml
version: "3"

services:
    postgres-for-test:
        image: postgres:11.1
        tmpfs:
            - /var/lib/postgresql/data
        ports:
            - 5432:5432

Oracle Database

:warning: Oracle Database の Docker イメージはローカルでビルドする必要があります。

docker-compose.yml
version: "3"

services:
    oracledb-for-test:
        image: oracle/database:12.2.0.1-ee
        tmpfs:
            - /opt/oracle/oradata
        ports:
            - 1521:1521

CI で使う

GitLab CI など、ジョブの中で Docker が使える CI 環境があれば、同様の方法を使ってテストを高速化できるはずです。

私が担当しているプロジェクトでは GitLab CI のテスト実行前に(開発環境と同様に)docker-compose を使って tmpfs が有効になったテスト用の DB を起動した後にテストが実行されるようにしています。

(GitLab CI は Docker コンテナの中で CI のジョブを実行するという仕組みのため、Docker コンテナの中で Docker コンテナを立ち上げられるようにする必要があり、セットアップに少し手間がかかりましたが…)

データ量には注意

tmpfs を使うとDBに書き込んだデータが全てメモリに保存されるため注意が必要です。テストで読み書きするデータのサイズは Docker が使えるメモリの上限を超えないようにする必要があります。

Docker for Windows であれば、「Settings > Advanced」で Docker へのメモリの割当量を調整できます。

同様の対応が組み込まれている Docker イメージもある

@shimma さんに教えていただきました。

RDBMS のデータが保存されるディレクトリにRAMディスク /dev/shm をマウントした Docker イメージを CircleCI が公開してくれています。

Tag の末尾に -ram が付いたイメージがそれです。

性能を計測してみたところ、私の環境では tmpfs を使った場合とほぼ同等の性能が出ました。

mariadb:10.3.2 (tmpfs) と circleci/mariadb:10.3.2-ram を比較

:warning: ただし、Docker コンテナの /dev/shm はデフォルトで 64MB のサイズしか確保されておらず、MariaDB だと起動すらできないので明示的に指定してやる必要があります。

--shm-size=""
Size of /dev/shm. The format is <number><unit>. number must be greater than 0. Unit is optional and can be b (bytes), k (kilobytes), m (megabytes), or g (gigabytes). If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses 64m.
https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources

docker-compose での shm-size の指定例

docker-compose.yml
version: "3"

services:
    mariadb-for-test:
        image: circleci/mariadb:10.3.2-ram
        shm_size: 256m  # /dev/shm に 256MB 割当
        ports:
            - 3306:3306
        environment:
            - MYSQL_ROOT_PASSWORD=mysql

さいごに

Docker で tmpfs を使うと自動テストに特化した高速な DB を簡単に構築できます。

Docker は Linux をはじめ、Windows や Mac でもほぼ同じように利用できるため、この方法はプロジェクトメンバーにも展開しやすいです。docker-compose.yml ファイルを一つ作ってプロジェクトに入れておけば OK です。

ただし、コンテナをいくつも立ち上げるとそれなりにリソースを必要とするので、えらい人に頼み込んで強いマシンを用意してもらいましょう。

HoloLens用の注視入力-GazeSelector-をunitypackageにして公開しました

はじめに

今回の記事は去年のアドカレ「HoloLensに注視入力を追加してみる~エアータップを使わないUIに挑戦~」の続編となります。
昨年度の記事では作ってみたところまでで終わってしまい、使うためにはそれなり手順が必要な状態でした。このUIは今年も複数のアプリで採用しており、未だ需要はそれなりにありそうなため、今回はそれを使いやすく改善しました(あと、このUIの名前をGazeSelectorとしました)。

環境

  • Unity 2017.4
  • Visual Studio 2017
  • MixedRealityToolkit-Unity 2017.4.1.0

(近い環境であれば大抵動くと思います)

使い方

GazeSelectorは以下のリポジトリで公開してます。

https://github.com/decchi/GazeSelectorForHoloLens

GazeSelectorを使う手順は以下の通りです。
1. MixedRealityToolkit-Unityをインポート
2. GazeSelectorのunitypackageをインポート(releasesにあります)
3. GazeSelectorフォルダ内のDefaultCursorWithGazeSelectorのプレハブをシーンに追加します。
4. GazeSelectorの対象とするオブジェクトにGazeSelectorTarget.csをアタッチし、選択時にしたい処理をUnityEventとして登録します。

と、シンプルになるようにしてありますが、サンプルも用意しました。

サンプルシーン

サンプルシーンはMixedRealityToolkit-UnitytとGazeSelectorがインポート済みの状態でGazeSelectorSampleのunitypackageをインポートすればよいです。 (これもreleasesにあります)
インポートしたらHoloToolkitExtensionSampleのフォルダにあるGazeSelectorのシーンを開きます

開いた結果は以下です
image.png

シーンにはGazeSelectorが設定されたカーソルとターゲットとしてGazeSelectorTargetが設定されたオブジェクトが3つあります。
Cubeには青色に変更する、Sphereには緑色に変更する、Capsuleには非表示にするようにUnityEventを登録してあります。
Cubeは例として以下のように設定してあります。
image.png

そのまま動かすとこんな感じになります。
GazeSelectorでも.gif

おわりに

HoloLensのジェスチャーは初見の方には特に難しい操作です。3Dモデルを見るだけのコンテンツであればGazeSelectorで置きかえれる部分も多いと思います。もし、この記事を読んで頂いて使えそうだなと思っていただけたら幸いです。これからもHoloLensの開発が楽になるツールを作って公開していきたいと思います。

How to raise the future Formidable Excelist?

It's been a while guys, how are you doing these days?

This article is totally unrelated to the company which I belong to, and only expresses my personal thought. Of course, this work has been done on weekends, and off-time.

Anyway, now, let's start the story.

GOD's Excel

"Dad, I hate Excel! These rectangles are pretty boring! I never want to see this thingy again!"

You have heard this sad complaining, haven't you? As long as they don't know the secret potential of Excel, this tragedy will happen. Even though you want those little geniuses to be the greatest Excelist, they won't touch Excel anymore.

There is a critical problem behind this. Before you get them to play with Excel, you have to understand this important mechanism.

Let's take a look at this screen shot.

e1.png

What did you think when you saw this? I'm sure that you were able to get nothing.

As the fact, in order to stimulate your/their unlimited imagination, you need to make the shape of cells into squares like below.

e2.png

How did you feel this time?

From the moment you saw these beautiful squares, you must have started to imagine various enterprise Excel applications as if you got struck by divine lightning, in an instant.

This Excellent innovation is widely known as Excel Graph Paper or GOD's Excel.

Excel and Entertainment

So, what is the best way to get them have interest in Excel after all? Okay, now I'll tell you one of Excellent solutions.

You might not believe but, while Excel is the greatest platform, it's also the invincible entertainment platform.

Not only Excel and Enterprise, but also Excel and Entertainment... Hold on, let's get one thing straight. You should not abbreviate this like EE. If you think of this idea immediately, you might have a Java Enterprise sickness. By the way, Java is pretty strong language, so... Forget it, let's get back on track.

Now, it's a great opportunity to build something joyful together on Excel to let them get aware of the importance of Excel. You might have seen the game which has these cells...

e32.png

As you noticed, the game means that. This looks pretty suitable in this situation.

The winter vacation will begin soon. The thing only you have to do is just get Excel to the little geniuses, and share your enterprisability with them through making an awesome game program together.

After this vacation, they will have strong interest in not only Excel itself, but also programming, dirty workarounds to avoid absurd language specifications, and enterprise technique.

You know, some companies have a Take Your Child To Work Day. In order to get the future great excelists to feel like working for your company, you need to have mastered this Excel platform as if you are a demonic wizard.

So, where is your workbook of that?

Unfortunately, I've completely forgot the fatal thing until now. The problem is about its license, and I couldn't publish this workbook because of it.

This is really sad story, but I can't help.

e42.png

Even though I have sacrificed my weekends and off time to complete this work, I had no choice but to give up making it open.

Conclusion

  • Excel is fun
  • You should check license-related things before you do something
  • If you would like to play this closed workbook, please let me know

UUIDの衝突確率

ハローみなさん!! 今日も元気に周りの人と衝突してますか!!!!!

毎日のように様々な衝突を生み出すみなさん、そんな皆さんが衝突と聞いてすぐに頭に浮かぶのは、もちろん UUID であることでしょう。UUID のうち多く使われるのは version 4 だと思いますが、この ver. 4の UUID は、基本的にランダムな値として生成されます。

その結果として

  • UUID って、オレと同じように周りと衝突してしまわないかな?
  • 衝突した結果、余計な混乱を生み出さないかな?
  • 今は周囲とたくさん衝突してしまう UUID も 30 代くらいになったら丸くなり誰もが慕う素敵な UUID にならないかな???

とベッドの中で夜も眠れずご心配の日々をお過ごしではないでしょうか。

乱数から生成されるんだったらその一意性って完全じゃないよね?
でも、結構みんな一意だと思って使ってるよね?
というわけで、一体ぼくたちは、どれだけの衝突確率を「無いもの」として扱っているのかを考えてみたいと思います。

結論

過程をすっ飛ばして結論を先に言いますと、$ n $ 回 UUID を生成したときに衝突が発生する確率は、およそ $$ 1-\exp\left(-\frac{{n}^2}{2^{123}}\right) $$ くらいになります。

試行回数に対する衝突確率の伸びをグラフ化するとこんなかんじ。$ x $ 軸は log scale になってることにご注意ください。

graph.png

めちゃくちゃ拡大してみます。

enlarge.png

過程

前提

UUID は 128 bit から構成されますが、Version 4 ではこの 128 bit のうち、122 bit にランダムな数値をセットして UUID とします (残りの 6 bit は固定値です)。

定式化

上記を前提に、ここでは、「version 4 で $ n $ 回 UUID を生成したとき、それらが 1 つでも衝突してしまう確率 $ P $」を求めます。
"1 つでも"というところからピンと来ると思いますが、これは "衝突が一切発生しない" という事象に対する背反事象になります。
このため、まずは衝突が一切発生しない確率をもとめ、それを 1 から引くという考え方をするのが王道です。

計算

順を追って考えましょう。

  1. $ n $回の試行のうちの、最初の 1 回目を考えます。このとき、衝突する UUID は存在しないので、衝突しない確率は 1 (100 %) です。
  2. $ n $ 回の試行のうちの、2 回目を考えます。このとき、1 回目に生成した UUID と衝突する可能性があります。UUID として発生し得るパターンは $ 2^{122} $ パターンあるので、衝突する確率は $ \frac{1}{2^{122}} $ になり、衝突しない確率は $ 1-\frac{1}{2^{122}} $ になります。
  3. $ n $ 回の試行のうちの、3 回目を考えます。このとき、1 回目に生成した UUID、あるいは 2 回目に生成した UUID と衝突する可能性があります。したがって、衝突する確率は $ \frac{2}{2^{122}} $ になり、衝突しない確率は $ 1-\frac{2}{2^{122}} $ になります。
  4. ...
  5. $ n $ 回の試行のうちの、$ n $ 回目を考えます。このとき、1 回目から $ n-1 $ 回目に生成した UUID と衝突する可能性があります。したがって、衝突する確率は $ \frac{n-1}{2^{122}} $ になり、衝突しない確率は $ 1-\frac{n-1}{2^{122}} $ になります。

以上から、n 回の試行において衝突が一切発生しない確率は、

$$ 1 \cdot \left(1-\frac{1}{{2}^{122}}\right) \cdot \left(1-\frac{2}{{2}^{122}}\right) \cdots \left(1-\frac{n-1}{{2}^{122}}\right)=\prod_{i=1}^{n-1}\left(1-\frac{i}{{2}^{122}}\right) $$

ということになり、衝突が発生する確率 $ P $ は、1 からこの確率を引いた、$$ P=1-\prod_{i=1}^{n-1}\left(1-\frac{i}{2^{122}}\right) $$ として表されます。

近似しましょう

で、こんな式を見せられても、実値を計算するのがマジでダルい。$ n $ が $10^{10}$ だったらどうやって計算するんや。
というわけで、近似しましょう。$ 1-\frac{i}{2^{122}} $ の項を何とかして綺麗にしてやりたい。

ここで、指数関数のテイラー展開を思い出すと、$ x \ll 1 $ のとき、$ {e}^{x} \approx 1 + x $ です。この指数関数のテイラー展開を利用すると、$$ 1-\frac{i}{{2}^{122}} \approx \exp\left(-\frac{i}{{2}^{122}}\right) $$ と近似できます。

これを $ P $ の式に代入し、おなじみの公式 $ \sum_{i=1}^{n-1}i=\frac{n(n-1)}{2} $を使うと、$$ P\approx 1-\exp\left(-\frac{1}{2^{122}}\frac{n(n-1)}{2}\right) $$ が導けます。

$ {n}^2 \gg n $ とすれば、$$ P\approx 1-\exp\left(-\frac{{n}^2}{{2}^{123}}\right) $$
になります。かなりきれいになりましたね。

じゃぁ衝突確率が p になるときの UUID 数 n はどのくらいなの

衝突が発生する確率 $P$ が $p$ になるための試行回数 $n$ を求めてみましょう。

$ 1-\exp\left(-\frac{{n}^2}{2^{123}}\right) \approx p $ を $n$ に対して解けば良いです。
これ、わりとかんたんで、$n\approx \sqrt{2^{123} \ln{\frac{1}{1-p}}}$ になります。
たとえば、UUID を $ 3 \times 10^{17}$ 回くらいつくると、1 % ($p=0.01$) の確率で衝突するってことですね。

p.png

この衝突確率を現実的に考慮すべきリスクととるか否か、まぁ状況には依るのかもしれません。ちなみに宝くじで 1 等が当選する確率は 2,000 万分の 1 くらいだとか。

背景にある問題

じつはこの衝突確率を求める問題は、誕生日問題 と呼ばれる問題の 1 つです。
高校数学あたりで、クラスの 30 人のうち誕生日が一致する人がいる確率を求めなさい、的な問題を記憶されている方も多いのではないでしょうか。

「プレゼンテーション」について改めて気づかされたこと

プログラミングでも技術でもない話であるが、興味深い結果に出会ったので書いてみる。

だらだら書くのもあれなので、まず結論として何に気付かされたかということを書くと、

「プレゼンテーションはプレゼンターの気持ちが入っている事(情熱)がやっぱり大事なんだ」

ということ。

前段として上記のことに気づいた筆者の事を書くと

  • 仕事としてはメインにインフラ技術者の啓蒙、兼業としてキャリアコンサルティング業を営む
  • 年間3,40のTech、NonTechのプレゼンテーションを社内外で行なってる
  • 社内でプレゼンテーションの勉強会を十数回開いている
  • 基本的には技術者だが下記のようなスキルがある
    • キャリアコンサルタントの有資格者であり、カウンセリング・コーチングのスキル
    • Rを使いこなしデータ分析(主にビジネス分析)

され、本題のプレゼンテーションの成功の秘訣は、いろいろあるが、今回は説明しやすく有名な弁論術のフレームワーク(とここでは呼ばせてもらう)としても用いられるアリストテレスの3つのポイント、「ロゴス」「エトス」「パトス」を用いて表現していく。これらの解釈も文献によって様々あるが筆者はこう理解している。

  • ロゴス・・・論理性、理にかなっているか、矛盾はないか、組み立て、ストーリーの構造などのスムーズさ
  • エトス・・・人柄、人物の特徴、信頼にいたる情報、その内容を話すのに値する情報
  • パトス・・・心情、感情、情熱、思い入れ、好き嫌い

この内、プレゼンテーション実施後、相手に印象を与え、内容が伝わり、なんらか動かす、変える動機付けをするには「パトス」の影響力はかなり大きいのだと感じたわけである。

その理由を以下3つの例から見ていくことにする。

3つの例

1、大規模修繕工事のコンペのプレゼンにて

筆者はマンションの管理組合の理事長であった経験がある。そのマンションに大規模修繕工事を行うタイミングがやってきて、その工事をお願いする業者を選ぶことになった。いわゆるコンペによるプレゼン会が行われたのである。最終的に残ったのは2社(A社、B社とする)で、それぞれ、プレゼンテーションをやってもらった。聴講者は私を含め住人5,6人。IT技術者の想像するパワポをつかったプレゼンテーションではなく、資料を紙で渡され、目の前で業者の営業と現場責任者が説明する形式である。

評価するために住人はなれない評価項目シートのようなものを渡され(20項目くらいあった・・・)プレゼンを聴きながらチェックするのである。評価項目の中にはマンションならでわの気になるポイントを評価するものもあったが、その場にいる工事の現場責任者の評価項目もあった。人柄や印象といった項目である。

ぶっちゃけ住人はみんな素人だし、聴いたってわかりゃしないわけである。しかもA社もB社も工事内容や特徴などはたいして変わらない(ただ金額は2、300万くらい違っていた)。となると自然に、決め手になるのはその場の説明する人間の印象、つまりこの人指揮する現場にまかせてもいいと思うか否かという点になる。そこにはA社、B社に違いがあった。

A社は若い責任者、それなりに経験もある、服装はおそらく現場監督・指揮をとるときの姿を想像させる作業着!?である。かなり緊張されている風であり、基本的には営業が7,8割説明して、その若者に対しての質問も営業が半分くらいフォローしていた。
B社は40後半のベテラン責任者、営業と共にスーツ姿で説明も自信のある様子で、質問にも自ら回答していた。営業は資料を使ったトークに「この件ぜひお任せください」という意気込みが見えていた。A社もそこはなかったわけではないが、その度合と、丁寧さ、姿勢には差があったと私はするどく感じ取っていた。それは、配られた資料とその説明にも差があったからである。結局こっちは素人なので細かい点なんてわからない、がB社には「ここだけわかってくれれば大丈夫」というような思いで作っているんだろうなという点がみてとれた。さらにB社の説明には視点誘導をしながら丁寧に解説をする姿勢が営業と現場責任者にあったのである。

結果は、多数決でA社は誰もおらず、B社に決まった。価格はB社のが安かった。よーわからんので安い方にという観点で選んだ人もいるかもだが、そこにはプレゼンテーションのスキルに裏付けされた印象も影響していると私は思っていた。

このケースではフレームワークでいうところの「エトス」、「パトス」がポイントになっていたのだと感じた。

2、ある業務改善の成果報告イベントにて

このケースは、ある企業の業務改善をいろいろな部門がとりくみその成果について発表・プレゼンテーションし、最後はオーディエンスの投票によって最優秀賞が選ばれる、という仕組み、いわばコンテスト形式の出来事である。

会場は段差のあるホール・講堂形式で、観客も3、400人(もっとかも)いたかもしれない。発表部門も10~20くらいあったとおもう。そんな中、発表の内容(ロゴス)そのものは、どれも素晴らしい。もはやそれだけではまったく差がでないくらいのものばかりであった。・・・となるとあとはプレゼンター次第ということになる。しかも審査員は自社の人間だけでなく、ほんとに様々な会社の人たちがそれを聴いて、投票するのである。当然すべてが初見。決め手となるのは結果としてエトス・パトスであっただろう。このうちたくさんのプレゼンターが出てくるわけなので、すべての人物、人柄というのは人間も覚えきれないものである。となると強く印象に残ってしまう要素としてはパトスということになる。今、理論的に書いたが、実際もやっぱりそうだったのである。
優秀賞に選ばれたのは、成果もそうだが熱い想いとともに上手なコンテンツ、その内容を解説する振る舞いと声のトーンに思いがのせられて伝えるプレゼンテーションをした部署であった。私もその部署に投票をした。

ちなみに改善効率が極端に高く(優秀賞とった部署よりも)、資料内容も非常に明瞭でわかりやすかった・・・(つまりロゴスは最高)けれどもプレゼンターがあまり普通に淡々と説明をしただけで終始抑揚もなく、情報が頭に入らず流れるようになっていた(ガチガチ緊張していたのかもしれませんが)部門もあったがそこの部門は3位にすら入っていなかったのである。

3、新人総括プレゼン大会(の部門選抜会)

自社では新人さんが部門に配属されてしばらく経つと、それまでの振り返りと今後の目標を語るプレゼン大会が催される。その部門選抜会での出来事である。私はこれの企画・運営をやっていた。プレゼン大会についてもう少しほ補足すると、何回かの選考を通じて最終的には「新人総括研修発表会」というイベントで同じ新人達の前で発表するというのが行われる。その一発目が部門選抜会である。

部門には5人の新人(A~E)が配属されており、選抜するために定量評価指標が用意されており、評価者(10名:部門長、プロジェクトでの上長、OJT担当、育成担当等)はそれぞれについて1~4の点数付けていく。なおこの指標はこれは会社で用意されているものである。細かくは書かないが、いわゆる内容についての部分とプレゼンスキルについてのものが合計7項目用意されている、なのでポイントのMaxは28である。

集計には時間がかかるので、且つ、せっかく聴いてもらうのに10人だけだと面白くないだろうということもあり、間つなぎ半ば余興として下馬評をもらうためにリアルタイムアンケートDirectPollを使ってその場にいる人のアンケートをとってみた。(記録をしていなかったので以下は実施したときのイメージです。)

質問:「誰のプレゼンがよかったとおもいますか?」
キャプチャ.JPG

これによると、下馬評としては「A」に軍配があがった。
さて、一方で評価シートを集計をしたところ下記のような結果となった。

評価者 A B C D E
1 25 20 24 19 18
2 23 22 26 27 22
3 23 19 22 20 21
4 19 19 20 18 16
5 24 27 27 26 25
6 18 19 24 22 21
7 22 19 21 20 21
8 16 23 28 22 20
9 21 26 28 28 26
10 27 23 26 23 24
合計 218 217 246 225 214

これによると、「C」に軍配が上がりました。下馬評でも2位ではありましたが、異なる結果となった。
結果として部門候補者は「C」にはなったのですが、そこに至るまでの話を以下に。

どうも例年このポイント評価集計後には微調整が毎度入るとのこと。(なんだそれは?これが悪しき文化・・・)要は、デジタルでわからないところを評価してどうのこうのという話し合いが行われるとのこと、今回運営でもあるので、それにも参加した。私個人としてはこの結果についてはサンプル数が少ないとはいえ、プレゼンテーションの指導者という観点からみても、妥当なところだと思っていた。この話し合いの中ではそもそも会社の旧態依然な文化をほのめかすコメント、いい意味で真面目さ、組織観点の配慮というのが垣間見れましたが、こと、プレゼンテーションが成功したか否かという意味ではこのポイントの結果の現れた通りだと考えており、「プレゼンテーションの本質」を理解させつつ、今回は余計な微調整が入ることなくこの結果の通りとなった。

もう少し基本統計によるデータを見ると以下のような感じ。

統計値 A B C D E
最小値 16.0 19.0 20.0 18.0 16.0
第一四分位数 19.5 19.0 22.5 20.0 20.1
中央値 22.5 21.0 25.0 22.0 21.0
平均値 21.8 21.7 24.6 22.5 21.4
標準偏差 3.36 3.0 2.88 3.47 3.06
第三四分位数 23.8 23.0 26.8 25.3 23.5
最大値 27.0 27.0 28.0 28.0 26.0

データから、「A」「D」については獲得ポイントのばらつきが大きい、つまり高く評価した人もいるし低く評価した人もいる、ということである。「C」は逆に一番ブレが小さい、これはどの評価者からも安定的にポイントを稼いで、且つ高得点をとっているということ。

内容プレゼンスキルの部分で分けると(以下平均)

新人 内容 プレゼンスキル
A 3.0 3.3
B 3.2 3.0
C 3.3 3.8
D 3.2 3.3
E 3.1 3.0

「C」はどちらも他のものよりも一番高いポイントとなっているが、プレゼンスキルについての評価は特に大きな差がでていた。

これまでは「振り返りがちゃんとできている」「内容がしっかりしている」(これらはロゴスなところ)という点で微調整加減点等が行われていたようだ。実際、含めなければいけない情報というのはアジェンダレベルで決まっているため、どうしてもみんな同じ流れ・似たような内容で発表しがちで、プレゼンの序盤では差がつきづらい。それでもこの5人は事前に私が勉強会をしたこともありそれぞれ個性がうまく出せていたとおもう。

確かに「振り返りがちゃんとできている」「内容がしっかりしている」な部分はもちろん大事ではあるが、そこだけを大きな決定打として重視するのは違うと思っている。「プレゼンテーションの本質は、相手を動かす」こと。ここでいう「相手とは」最終的に発表会の相手となる新人たち、つまり「発表者の同期」である。お偉いさんや人事や自他部門の社員に「今年の新人はしっかりしたできのいいのがそろってるな」とか思わせるためにやるわけではない(はず)。自分自身の振り返りをしつつ、聴いている同期に自分の部門の情報とやったことやれなかったこと失敗や成功の経験を語り、聴いている側もそれを疑似体験をするかのように気づきを与え、互いの成長の糧にできるかどうかというのがこの最終的な新人総括発表会では大事になってくるのだと考えている。なので私は自部門の新人らにはそのような本質についても事前に勉強会で伝え、助言した上で今回の部門選抜回に臨んでもらっていたのである。

下馬評では結果が異なっていたが、いくつかの視点から評価を重み付けしてプレゼンテーションを深掘りしデータを丁寧に読んでいったことにより、本質的にプレゼンテーションの目的を成せた(あるいは、成せる可能性が高い)のが誰であったか、というのが見えてきた事に、この評価シートを元にした選抜は意味のあるものであったと感じた。

そして、プレゼンスキルについてはフレームワークでいうとパトスに値する部分が影響をあたえたものであり、この評価データとしてもそれは顕著に表れていた。

なお、個人的なコメントをさせていただくと事実、内容もさることながら客観的に「C」が5人の中では表現力は優れていたと思う。

まとめ

3つの例を紹介してきたが、もともと感覚と経験でプレゼンテーションの効果を感じていたところに、3つ目の例のように定量的な評価も合わせてしっかり深掘りをしていくと、プレゼンテーションの伝える力は「パトス」の部分が影響してくる事がやはり多いのではないか!?と気づいたのである。よって

「プレゼンテーションはプレゼンターの気持ちが入っている事(情熱)がやっぱり大事なんだ」

ということにたどり着いた。

よって、これを読んでいるみなさんも今後プレゼンテーションを行うときの参考としてもらるとありがたい。私としてはこのような結果を踏まえて、一生懸命内容(資料作成)、論理に注力しすぎるよりも、印象、発声、言いたい部分の再確認(情熱ポイント)、振る舞い、特に練習・リハーサルに力を入れることをお勧めする。

Serverless Zabbix Sender for AWS

監視について「AWSと言えばCloudWatch」と言いたいところですが、プロセスの生死監視や、アラート発報時のサービス再起動の自動化等がやりたくて、CloudWatchだけだと色々と不都合なことがあります。

そこで、CloudWatchのメトリクス値をZabbixに投げて何もかもZabbixで見るという方法があり、以前から様々な場所で語られています。メトリクス値をZabbixに投げさえすれば、アラートの発報は当然として、発報後の処理もアクション等を使って大概のことができます。

Zabbixにメトリクス値を投げるには「Zabbix Sender」を実行する方法が簡単ですが、AWSとの連携を考えると『CloudWatchのメトリクスを取得してZabbix Senderを実行する』というスクリプト等が必要です。すぐ思いつくのは、Zabbixマネージャーが動いているサーバのOS上でこのスクリプトを実行すること(リソースの相乗り)ですが、大規模になればなるほど、スクリプトがリソースを食い、Zabbixマネージャーの稼働に影響が出るなど、本末転倒な状況になりかねません。

Zabbixマネージャーを含む各サーバの稼働に影響を与えないように、Zabbix Senderを実行するにはどうしたら良いのか。悩む中で閃いたのが、ServerlessでZabbix Senderを動かしたらいいんじゃないかというアイデアで、このアイデアが「Serverless Zabbix Sender」の開発に繋がっていきました。

前置き: Zabbix Senderとは

OSのコマンドラインで実行する場合の解説は、以下の通りです。

Zabbix sender は、パフォーマンスデータをZabbix サーバで処理するために送信するコマンドラインユーティリティです。
このユーティリティは通常、稼働とパフォーマンスのデータを定期的に送信するために長時間動作するユーザースクリプトの中で使用されます。

データを1つ送信する場合
例)Zabbix senderを使用してZabbixサーバーに値を送信する場合:
shell> zabbix_sender -z zabbix -s "Linux DB3" -k db.connections -o 43
オプション:
z - Zabbixサーバのホスト (IPアドレスでの指定でも可)
s - 監視対象のホスト名 (Webインタフェースで登録されたホスト名)
k - アイテムキー
o - 送信する値
https://www.zabbix.com/documentation/2.2/jp/manual/concepts/sender

通常はこの方法なのですが、Serverless Zabbix Senderでは、サーバもOSも使えないので、別の方法を考えなくてはなりません。
Serverless Zabbix Senderでは、PythonでZabbix Senderを実行することにしました。

PythonでZabbix Sender

Pythonだし、きっと探せばあるだろう、と思って探してみました。
ありました。Python版Zabbix Senderです。
https://pypi.org/project/ZabbixSender/
image.png
使い方は超シンプルで、リンク先で書かれている通りにコードを書くだけです。

構成図

Python版Zabbix SenderをAWS Lambdaで動かして、Zabbixマネージャーにメトリクス値を送ることを考えました。構成図は以下の通りです。
image.png

コード

Lambda関数を組み立てていきます。

Event

CloudWatch Eventsを使ってLambdaを起動しますので、Eventで監視対象やIAM Roleの値を与えます。おまけかもしれませんが、しれっとクロスアカウントにも対応させてしまいます。

event
{
    "target" : {
        "awsAccountId" : "xxxxxxxxxxxx",
        "awsIamRoleName" : "getMetricsWithCAA",
        "awsServiceNameSpace" : "AWS/RDS",
        "awsMetricName" : "CPUUtilization",
        "awsMetricsDemensionName" : "DBInstanceIdentifier",
        "awsMetricsDemensionValue" : "Zabbix",
        "awsRegion" : "ap-northeast-1",
        "Period" : 300,
        "Statistics" : "Sum"
    },
    "zabbix": {
        "zabbixManagerIpAddress" : "xx.xxx.xxx.xx",
        "zabbixManagerPort" : 10051,
        "targetZabbixHostName" : "ZabbixDB",
        "targetZabbixItemKey" : "aws.rds.cpu"
    }
}

target の定義

監視対象および、監視対象が存在する環境に関する情報を定義します。

Key 意味
awsAccountId AWSのアカウントID (12桁)
awsIamRoleName AssumeRoleするIAMロール名
awsServiceNameSpace サービスの名前空間 (AWS/RDS等)
awsMetricName メトリクス名 (CPUUtilization等)
awsMetricsDemensionName ディメンション名 (DBInstanceIdentifier等)
awsMetricsDemensionValue ディメンションの値 (任意の値)
awsRegion リージョン (ap-northeast-1等)
Period 期間
Statistics 統計 (詳しくはこちら)

zabbix の定義

Zabbix Managerに関する情報を定義します。

Key 意味
zabbixManagerIpAddress ZabbixマネージャーのIPアドレス
zabbixManagerPort Zabbixマネージャーの受信ポート
targetZabbixHostName Zabbixマネージャーに登録されているホスト名
targetZabbixItemKey Zabbixアイテムのキー

lambda_handler.py

AssumeRoleして、CloudWatchのメトリクス値を取得して、Zabbix Senderを実行します。

lambda_handler
from ZabbixSender import ZabbixSender, ZabbixPacket
import boto3
import datetime

# AssumeRoleして、一時クレデンシャルを取得する関数
def stsAssumeRole(awsAccountId, awsIamRoleName, awsRegion):
    # 1. AssumeRoleするためのClient作成、基礎情報の定義
    awsIamRoleArn = "arn:aws:iam::" + awsAccountId + ":role/" + awsIamRoleName
    awsSessionName = "CrossAccountZabbixSender"
    awsStsClient = boto3.client('sts')

    # 2. AssumeRole
    response = awsStsClient.assume_role(
        RoleArn=awsIamRoleArn,
        RoleSessionName=awsSessionName
    )

    # 3. 一時クレデンシャルの取得
    awsSession = boto3.Session(
        aws_access_key_id=response['Credentials']['AccessKeyId'],
        aws_secret_access_key=response['Credentials']['SecretAccessKey'],
        aws_session_token=response['Credentials']['SessionToken'],
        region_name=awsRegion
    )

    # 4. 一時クレデンシャルを返す
    return awsSession

# CloudWatch Metricsを取得する関数
def getMetricStatistics(awsSession, target):
    # 1. AWS CloudWatch用Clientを生成
    awsClient = awsSession.client('cloudwatch')

    # 2. eventで指定した監視ターゲットのメトリクスを取得
    ## getMetricStatisticsはUTCで時間指定する必要があるため、UTCタイムゾーンを生成
    UTC = datetime.timezone(datetime.timedelta(hours=0), 'UTC')
    ## StartTimeとEndTimeで期間を決め、getMetricStatisticsを実行
    metricStatistics = awsClient.get_metric_statistics(
                            Namespace = target["awsServiceNameSpace"],
                            MetricName = target["awsMetricName"],
                            Dimensions=[
                                {
                                    'Name': target["awsMetricsDemensionName"],
                                    'Value': target["awsMetricsDemensionValue"]
                                }
                            ],
                            StartTime = datetime.datetime.now(UTC) - datetime.timedelta(seconds=target["Period"]),
                            EndTime = datetime.datetime.now(UTC),
                            Period = target["Period"],
                            Statistics = [target["Statistics"]]
                    )

    # 3. metricStatisticsをdictごと返す
    return metricStatistics

# Zabbixにメトリクス値を送信する関数
def zabbixSender(dataPoint, zabbix):
    # 1. Zabbix Managerを示すオブジェクトを取得
    zabbixManager = ZabbixSender(zabbix["zabbixManagerIpAddress"], zabbix["zabbixManagerPort"])

    # 2. Zabbix Senderで送るパケットを作成
    zabbixPacket = ZabbixPacket()
    zabbixPacket.add(zabbix["targetZabbixHostName"],zabbix["targetZabbixItemKey"], dataPoint)

    # 3. Zabbix Senderでパケット(データポイント)を送信
    zabbixManager.send(zabbixPacket)

# lambda_handler
def lambda_handler(event, context):
    # 1. stsAssumeRoleを呼び、eventで指定したAWSアカウントから一時アクセスキー(awsSession)を取得
    awsSession = stsAssumeRole(event["target"]["awsAccountId"],event["target"]["awsIamRoleName"],event["target"]["awsRegion"])

    # 2. 1.で得たawsSessionを利用し、CloudWatch Metricsを取得
    metricStatistics = getMetricStatistics(awsSession, event["target"])

    # 3. 2.で得たmetricStatisticsからデータポイントだけを抜き出す
    dataPoint = metricStatistics['Datapoints'][0][event["target"]["Statistics"]]

    # 4. 3.で得たデータポイントをZabbix SenderでZabbix Managerに送る
    zabbixSender(dataPoint, event["zabbix"])

トリガー

CloudWatch Eventsを設定して、一定間隔でこのServerless Zabbix Senderを実行するようにします。

イベントソース

画面の指示に従い作ればOKです。スケジュールCron式 の組み合わせです。例えば、5分間隔で実行する場合、Cron式には以下を入力します。

Cron式
*/5 * * * ? *

ターゲット

もちろん Lambda関数 を指定します。 入力の設定 では 定数(JSONテキスト) を選択し、先ほどのEventで記載したJSONを入力します。

ここまで実施すれば、Serverless Zabbix Senderが動き始めます。

VPC Lambdaとして動かす

VPC LambdaServerless Zabbix Senderを実行し、プライベートIP間での通信でZabbixマネージャーにメトリクスを送信することもできます。Security Groupでしっかりアクセス制限もできるので現実的です。ただし、いくつか注意事項があります。まず、VPC Lambdaのコールドスタートを回避するために、実行間隔を長くしすぎないことです。次に、VPC Lambdaを実行するVPCのサイズを必ず大きめに確保し、VPC Lambda専用とすることです。VPC Lambdaに割り当てられるIPアドレスは自動で決まり、監視対象とメトリクスが増えれば増えるほどIPアドレスを消費しますから、他のVPCとは分け、リッチにIPアドレスを確保できるサイズにした方が良いでしょう。

VPC LambdaとしてServerless Zabbix Senderを実行する場合の構成図は以下の通りです。LambdaとZabbixマネージャー間の通信が、VPC Peeringになりました。

image.png

最後に

Serverless Zabbix Senderが動き始めて、Zabbixマネージャーでメトリクス値が次々に更新される様子を眺めつつ、これが全てServerlessで動いて送られているんだと考えると、なかなかに爽快な気分です。しかも、Lambdaで実行していますから、よほど大規模かつ高頻度でない限りは、無料の範囲内でなんとかできてしまいます。もう、どこかのサーバのリソースに相乗りする必要も、お金を出してZabbix Sender専用サーバなどと頑張る必要もありません。Serverless Zabbix Senderの後ろには、AWSの強大なコンピューティングリソースが控えており、どれだけ大規模になろうとも、確かに支えてくれます。Serverlessのパワーを頼ることにより、Zabbixマネージャーは、自身に本来割り当てられるはずのリソースを存分に使えるようになります。Serverlessの確かなパワーが、Zabbixを使った監視と運用をリッチにしてくれます。Serverlessは素晴らしいです。これに限らず、色んな場面で活用していくべきでしょう。

テキストとシステムと自然言語処理

言語処理に至る道

世の中にテキストを全く取り扱わないシステムはほとんどないと思われますが、例えば、「テキストを保存、表示するシステム」は言語処理をしているとは言えないでしょう。
この記事ではテキストを取り扱うシステムについて、一般的に言語処理に用いられるツールの出力を交えつつ、少しづつ掘り下げて言語処理とはどういう領域を取り扱うものかを私見としてまとめました。

大雑把まとめ

  • 言語処理にはレイヤーがある
  • レイヤーごとに扱う情報が異なる
  • レイヤーごとに取得できた情報をまとめ上げると言語処理をするシステムが実現できる。

前提

本記事内では自然言語とはテキスト化された日本語の自然言語を指します。
新聞記事、教科書、法律文、人間の会話、SNSの発言などを想定しています。
構造やシステム的なフォーマット(XMLやJSON、表のような構造)がないものです。

自然言語処理の前に

どんなシステムでも自然言語は出てきますが、よくあるパターンとしてはテキストエリアに人間が読むためのテキストを入力、保存、表示すること、といったところです。

これはシステムが言語をとりつかうというよりは、画像や音声データのように単に再生するためであって言語自体を取り扱っているとは言い難いところです。

どういったところまで扱えば言語処理と大手を振って言えるのかは人によって判断が分かれると思いますが、よくあるシステムで実装される、入力文字の制限や入力可能な文字数の長さを制限するのは、言語処理ではなく単なる文字列への処理とでも言うべきものでしょう。

正規表現

それでも多少はシステムで自然言語を扱います。そういった場合の多くは正規表現で対応を行うと思います。
半角カナを全角カナに置換するとか、禁止文字を検出して警告を出す等です。

正規表現があれば、文字列から決まった部分の抽出や置換など テキストとして表層に現れること に対しては何でもできます。

しかし、逆に言うと正規表現では自然言語で 表層に現れた情報 までしか取り扱うことができません。
取り扱う文字列が名詞なのか助詞なのか、そもそも単語の境目はどこなのかそういった情報が使えません。

すももももももももはもも

と、書いてあっても、正規表現では太刀打ち出来ないでしょう。

というわけで、システムで言語を取り扱うために最初には形態素解析が広く利用されています。

形態素解析をする

テキストに形態素解析を行うことで、「単語区切り」「品詞」「原型」などの情報が活用可能になります。

日本語相手の形態素解析に最も用いられるmecab を使えば、なんでも言語処理っぽくなるのは、やっぱり強力なツールだからですね。
以下に mecabの出力を提示します。

mecab.output
$ mecab
すももももももももはもも
すもも   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
EOS

この通り、 すもももも という名詞と、 という助詞で文が構成されていることがわかりました。

他の形態素解析機である jumanppでの解析結果でも似たような結果が取得できます。

形態素解析を用いて、自然言語から「単語区切り」「品詞」「原型」の情報を得ることで、

  • 人名の読みを推測する
  • 機械学習のための入力に利用
  • 係り受け、述語項解析、格解析、固有表現抽出のための入力に利用

などの応用がされています。

形態素解析器によって、自然言語のテキストから見えない情報を取得できるセンサーを得ることができました。
テキストの表層に現れている情報よりも、強力な情報をシステムで取り扱うことができるようになります。

係り受け解析

形態素解析の結果から単語や品詞がわかったあとは、その結果を用いて単語についての関係を係り受け解析で取得します。

cabochaKNP を使うと係り受け関係がわかります。

係り受け関係がわかると、どの文節がどの文節にかかるかわかってきます。
大きな望遠鏡で建物を見た。望遠鏡で大きな建物を見た。 のcabochaでの出力結果を示してみます。
例えば 大きな が係る先は、2つの文でそれぞれ 望遠鏡建物 と全く異なります。

以下のcabochaの出力の chunklinkid を見れば係り先が読み取れます。

cabocha.out
$ cabocha -f3
大きな望遠鏡で建物を見た。
<sentence>
 <chunk id="0" link="1" rel="D" score="1.338385" head="0" func="0">
  <tok id="0" feature="連体詞,*,*,*,*,*,大きな,オオキナ,オーキナ">大きな</tok>
 </chunk>
 <chunk id="1" link="3" rel="D" score="-1.647059" head="1" func="2">
  <tok id="1" feature="名詞,一般,*,*,*,*,望遠鏡,ボウエンキョウ,ボーエンキョー">望遠鏡</tok>
  <tok id="2" feature="助詞,格助詞,一般,*,*,*,で,デ,デ">で</tok>
 </chunk>
 <chunk id="2" link="3" rel="D" score="-1.647059" head="3" func="4">
  <tok id="3" feature="名詞,一般,*,*,*,*,建物,タテモノ,タテモノ">建物</tok>
  <tok id="4" feature="助詞,格助詞,一般,*,*,*,を,ヲ,ヲ">を</tok>
 </chunk>
 <chunk id="3" link="-1" rel="D" score="0.000000" head="5" func="6">
  <tok id="5" feature="動詞,自立,*,*,一段,連用形,見る,ミ,ミ">見</tok>
  <tok id="6" feature="助動詞,*,*,*,特殊・タ,基本形,た,タ,タ">た</tok>
  <tok id="7" feature="記号,句点,*,*,*,*,。,。,。">。</tok>
 </chunk>
</sentence>
$ cabocha -f3
望遠鏡で大きな建物を見た。
<sentence>
 <chunk id="0" link="3" rel="D" score="-1.908594" head="0" func="1">
  <tok id="0" feature="名詞,一般,*,*,*,*,望遠鏡,ボウエンキョウ,ボーエンキョー">望遠鏡</tok>
  <tok id="1" feature="助詞,格助詞,一般,*,*,*,で,デ,デ">で</tok>
 </chunk>
 <chunk id="1" link="2" rel="D" score="1.363216" head="2" func="2">
  <tok id="2" feature="連体詞,*,*,*,*,*,大きな,オオキナ,オーキナ">大きな</tok>
 </chunk>
 <chunk id="2" link="3" rel="D" score="-1.908594" head="3" func="4">
  <tok id="3" feature="名詞,一般,*,*,*,*,建物,タテモノ,タテモノ">建物</tok>
  <tok id="4" feature="助詞,格助詞,一般,*,*,*,を,ヲ,ヲ">を</tok>
 </chunk>
 <chunk id="3" link="-1" rel="D" score="0.000000" head="5" func="6">
  <tok id="5" feature="動詞,自立,*,*,一段,連用形,見る,ミ,ミ">見</tok>
  <tok id="6" feature="助動詞,*,*,*,特殊・タ,基本形,た,タ,タ">た</tok>
  <tok id="7" feature="記号,句点,*,*,*,*,。,。,。">。</tok>
 </chunk>
</sentence>

単なる形態素解析の結果では、「何が大きいのか?」はわかりませんが、 cabocha の係り受け解析結果ではシステムで取り扱える形で大きい対象が判断できる材料が得られます。

他の技術的な要素

形態素解析の結果を用いて行われる処理には以下のようなものもあります。
例示した cabochaknp のマニュアルを読んでみると面白いです。

  • 固有表現抽出
  • 格解析

終わりに

ここまでいろいろ書いてきましたが、この記事で書いてあるようなことの全ては言語処理を行う上でツールしかありません。
各レイヤーごとに、取り扱える情報やその精度も異なるので、実際にシステムを構築する上ではツールの出力を使うだけではなく、複数の機能の出力をまとめ上げて活用するシステムの開発が重要です。

というわけで、第3次AIブームですし、よりよい自然言語処理ライフを!!:santa:

AWS RoboMaker応用 ~AWS IoT Coreとの連携~

この記事は、TIS Advent Calendar 2018の24日目の記事です。

はじめに

とうとうロボットのプログラムがクラウド上で書ける時代にまでなりましたが、皆様ロボットはお好きでしょうか?

先日公開したAWS RoboMaker入門 ~デモ起動からコード修正、実機デプロイまで~という記事では、クラウド上で書いたロボットアプリケーションをクラウド上でシミュレートし、その後インターネット越しに実機にデプロイする、という流れを説明しました。

しかしこの記事で動かしたロボットアプリケーションは、起動するとロボットがひたすらグルグル回り続けるだけで、外界とは何も連携しないものでした。これでは全然楽しくないので、今回はAWS IoT Coreからロボットへ動作を命令できるようにしたいと思います。

今回の記事で作成したROSアプリケーションのリポジトリ

今回の記事で作成したROSアプリケーションは、githubのリポジトリで公開しています。記事にあわせてご確認ください。

シミュレーション環境を準備する

前回の記事を参考に、AWS RoboMakerのシミュレーション環境を立ち上げます。

注意点は、RoboMakerシミュレーション環境をインターネットゲートウェイを持つマルチAZなVPC内で動作させることです。

RoboMakerのシミュレーション環境は、VPCを指定しないと外界と接続しないクローズドな仮想ネットワーク上で起動します。今回動作させるロボットアプリケーションは、インターネット経由でAWS IoT Coreに接続しますので、シミュレーション環境でもインターネットに接続できなければなりません。そのため、インターネットゲートウェイを持つVPCを明示的に指定し、その上でシミュレーション環境を起動させたいと思います。1

SubnetとSecurity Groupを作成する

ローカルのアドレス以外はインターネットへルーティングするSubnetを、us-east-1cとus-east-1dに作ります。

01.png

また、Inboundは全部Denyで、Outboundは全部AllowするSecurity Groupも一つ作っておきます。

02.png
03.png

RoboMakerシミュレーション環境を起動する

前回の記事の "Hello World デモを動かしてみよう" と同様に、まずはVPC外でHello worldデモのサンプルシミュレーション環境を起動します。シミュレーション環境が正しく起動したことを確認したら、そのシミュレーション環境で "Clone" をクリックします。

04.png

"Step 1: Condigure simulation" の "Edit" をクリックします。

05.png

先ほど作成したVPCを選択し、Subnetを二つとも指定します。また先ほど作成したSecurity Groupも指定し、 "Next" をクリックします。

06.png

"Step 2: Specify robot application." はそのままにしておき、 "Step 3: Specify simulation application." の "Edit" をクリックします。

"TURTLEBOT3_MODEL" 環境変数の値として "waffle" を指定することで、シミュレータ上に出現するロボットを "Turtlebot3 Waffle" に変更しておきます。

07.png

上記のように設定を変更し、 "Create" すると、新たにシミュレーション環境が起動します。指定したSubnetとSecurity Group内で起動していること、Simulation applicationの環境変数 "TURTLEBOT3_MODEL" の値が "waffle" として設定されていることを確認してください。
(VPC外で起動させた最初のシミュレーション環境はもう使わないので、 "Cancel" してしまってかまいません。)

08.png
09.png

では、起動したシミュレーション環境がインターネットに接続できることを確認しましょう。シミュレーション環境の "Terminal" を起動し、次のコマンドを実行してください2

$ python -c "import urllib;print(urllib.urlopen('https://www.google.com').read())"

正しく設定されていれば、 www.google.com のHTMLが表示されるはずです。

AWS IoT Coreを準備する

シミュレーション環境が起動したので、次はAWS IoT Coreを準備します。

Thingを登録し、証明書をダウンロードする

AWS IoTのコンソールを用いて、ロボットに相当するモノ(Thing)を登録します。

"Manage > Things" から、 "Register a Thing" をクリックします。

10.png

"Create a single Thing" をクリックし、名前として "turtlebot3_waffle" を入力した後に "Next" をクリックします("Thing Type" や "Thing Group" はデフォルトのままで大丈夫です)。

11.png
12.png

"Create certificate" をクリックし、AWS IoT Coreと接続するためのクライアント証明書と公開鍵、秘密鍵のペアを生成します。

13.png

生成されたクライアント証明書と秘密鍵をダウンロードします。
また、"root CA for AWS IoT" のリンク先から、AWS IoT Coreのroot証明書をダウンロードします(Amazon Root CA 1で大丈夫です)。

14.png
15.png

ダウンロードした後に "Done" をクリックすれば、 "turtlebot3_waffle" というThingが登録されます(Policyはまだ作っていないので、後からアタッチします)。

16.png

証明書にPolicyを設定する

次に、このThingに許可するPolicyを設定しましょう。今回のROSアプリケーションでは、AWS IoT Coreへ次の操作をする権限が必要です。

  • AWS IoT CoreへMQTT Clientとして接続する
  • /hello_world_robot/#というMQTT Topicをsubscribeする

またAWS IoT Core経由でロボットへ動作命令を出すためには、AWS IoT CoreへMQTTメッセージを送りつける権限が必要です。別の役割なので本来は別のThingとして扱ったほうが良いのですが、今回は横着して同じThingを使い回すことにします。そのため、次の権限も合わせて付与します。

  • /hello_world_robot/subというMQTT Topicへメッセージをpublishする

では、実際にコンソールからPolicyを作成しましょう。 "Secure > Policies" から、 "Create a policy" をクリックします。

17.png

今回はjson形式でPolicyを設定するので、 "Advanced mode" をクリックします。名前として "robomaker_iot_policy" を入力し、PolicyのStatementとして以下のjsonを入力して "Create" します(MQTTの記法としては、トピックフィルタのワイルドカードは+#ですが、IAMの記法に合わせて *を用いることに注意してください)。

18.png

注意: 999999999999は自身のアカウントIDに置き換えてください

Policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Subscribe"
      ],
      "Resource": [
        "arn:aws:iot:us-east-1:999999999999:topicfilter//hello_world_robot/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Receive",
        "iot:Publish"
      ],
      "Resource": [
        "arn:aws:iot:us-east-1:999999999999:topic//hello_world_robot/sub"
      ]
    }
  ]
}

無事にPolicyが登録できたら、先ほど作成した証明書にアタッチします。
"Secure > certificates" から、生成済みの証明書の "Attach policy" をクリックします。

19.png

作成した "robomaker_iot_policy" を選択し、 "Attach" します。

20.png

最後に、証明書を "Activate" します。これにより、この証明書が使えるようになります。

21.png

AWS IoT Coreのエンドポイントを確認する

登録したThingが接続するAWS IoT Coreのエンドポイントは、 "Settings" から確認できます。後で必要になりますので、メモしておきましょう。

22.png

Macから接続を確認する

では、AWS IoTへ正しくThingが登録できたか、ダウンロードした証明書や秘密鍵を用いてMacから接続してみます。事前にMQTT Client(mosquitto_submosquitto_pub)をインストールしておいてください。

なお注意点ですが、QoS=1を明示的に指定してください3
mosquitto_submosquitto_pubも、デフォルト(-qオプションを指定しない)ではQoS=0でMQTT Brokerへ接続します。ローカルのMQTT Brokerに接続する場合はQoS=0でも問題ありませんが、AWS IoT Coreはインターネットの向こう側にあるため、QoS=0だとsubscriberまでメッセージが届かない場合があります。

subscriberを起動

ダウンロードしたクライアント証明書と秘密鍵、AWS IoT Coreのroot証明書を用いて、先ほど確認したエンドポイントの8883ポートへ接続し、QoS=1で/hello_world_robot/# をsubscribeします。

$ mosquitto_sub -d \
--cafile <<<root証明書のpath>>> \
--cert <<<クライアント証明書のpath>>> \
--key <<<秘密鍵のpath>>> \
-h <<<AWS IoT Coreのエンドポイント>>> -p 8883 \
-t /hello_world_robot/# \
-q 1

AWS IoT Coreが正しく設定されてれば、subsciribeに成功するはずです。

23.png

次に、別のTerminalから /hello_world_robot/sub へメッセージを送ってみます。

mosquitto_pub -d \
--cafile <<<root証明書のpath>>> \
--cert <<<クライアント証明書のpath>>> \
--key <<<秘密鍵のpath>>> \
-h <<<AWS IoT Coreのエンドポイント>>> -p 8883 \
-t /hello_world_robot/sub \
-q 1 \
-m "{\"message\": \"robomaker iot\"}"

最初のTerminalへメッセージが届けば、AWS IoT Coreの準備は完了です。

AWS IoT Coreに接続するROSアプリケーションを書く

諸々準備が整いましたので、RoboMakerの開発環境を立ち上げて、AWS IoT Coreから命令を受け取るROSアプリケーションを書きましょう。

RoboMakerの開発環境を起動する

前回の記事のHello Worldデモのコードを修正するを参考に、1. RoboMakerのROS開発環境を起動 2. Hello worldデモのソースコードの取り込み 3. ROSワークスペースの初期化 まで実行します(このRoboMakerの開発環境は、シミュレーション環境用に作ったVPCに同居させてかまいません)。

開発環境へ証明書をコピーする

HelloWorld/robot_ws/src/hello_world_robot直下にcertsディレクトリを作成し、ダウンロードしたクライアント証明書と秘密鍵、及びAWS IoT Coreのroot証明書をドラッグ&ドロップして開発環境へコピーします。

24.png
25.png

証明書をバンドル対象に指定する

RoboMaker開発環境にディレクトリを作成してファイルを配置しただけでは、シミュレーション環境や本番環境へデプロイするバンドルファイルに取り込まれません。そのため次のように、CMakeLists.txtinstall(DIRECTORY ...) 命令に、certs ディレクトリを追加してください。ビルドやバンドルする際には、colcon(が起動するcatkin)がこのCMakeLists.txtを参照し、イイカンジに処理してくれます。

CMakeLists.txt
   DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
 )

-install(DIRECTORY launch
+install(DIRECTORY launch certs
    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
 )

PythonのMQTTクライアントライブラリをインストールする

ROSではrosdepというツールを用いて、ROSアプリケーションが依存するOSパッケージやPythonライブラリ等をインストールすることができます。/etc/ros/rosdep/sources.list.d/以下の.listファイル(が参照しているYAMLファイル)を確認すれば、rosdepによってインストールできるライブラリが探せます4

RoboMakerで用いているROS(Kinetic)では、Python用のMQTTクライアントライブラリであるpaho-mqttpython-paho-mqtt-pipという名前でリストアップされていますので、今回はこれを使うことにします。(デフォルトでリストアップされているPythonライブラリの詳細は、python.yamlを確認してください。)

では、python-paho-mqtt-pipに依存することを宣言しましょう。次のように、package.xmlpython-paho-mqtt-pipを追加してください。

package.xml
   <build_export_depend>message_runtime</build_export_depend>
   <exec_depend>message_runtime</exec_depend>
   <exec_depend>turtlebot3_bringup</exec_depend>
+  <depend>python-paho-mqtt-pip</depend>
 </package>

package.xmlを修正した後に下記のコマンドを再実行すると、rosdepが "paho-mqtt" をインストールします。

$ cd $HOME/environment/HelloWorld/robot_ws/
$ rosdep install --from-paths src --ignore-src -r -y

Successfully installed paho-mqtt-<<バージョン番号>>というログが出力されることを確認してください。

AWS IoT Coreに接続するソースコードを書く

それでは、AWS IoT Coreに接続するソースコードを書きましょう。

今回のROSアプリケーションはある程度複雑になりますので、AWS IoT Coreから命令を受け取ってロボットを操作するクラスを作ることにします。src/hello_world_robot以下5awsiot.pyを作成し、AWSIoTクラスを実装しましょう。

src/hello_world_robot/awsiot.py
# -*- coding: utf-8 -*-
import ssl
import json
import time

import rospy
from geometry_msgs.msg import Twist

import paho.mqtt.client as mqtt


class AWSIoT(object):
    QOS = 1
    HZ = 10

    def __init__(self):
        rospy.loginfo("AWSIot#__init__")
        self.is_connected = False
        self.__client = mqtt.Client(protocol=mqtt.MQTTv311)
        self.__client.on_connect = self._on_connect
        self.__client.on_message = self._on_message
        rospy.on_shutdown(self._on_shutdown)
        self.__params = rospy.get_param("~awsiot") # get parameters from ros parameter server

    def run(self):
        rospy.loginfo("AWSIoT#run")

        # set certification files
        self.__client.tls_set(
            ca_certs=self.__params["certs"]["rootCA"],
            certfile=self.__params["certs"]["certificate"],
            keyfile=self.__params["certs"]["private"],
            tls_version=ssl.PROTOCOL_TLSv1_2)

        # connect to AWS IoT Core
        self.__client.connect(
            self.__params["endpoint"]["host"],
            self.__params["endpoint"]["port"],
            keepalive=120)
        self.__client.loop_start()

    # this method is called when connected to AWS IoT Core successfully
    def _on_connect(self, client, userdata, flags, response_code):
        rospy.loginfo("AWSIoT#_on_connect response={}".format(response_code))
        # subscribe '/hello_world_robot/sub' mqtt topic
        client.subscribe(self.__params["mqtt"]["topic"]["sub"], qos=AWSIoT.QOS)
        self.is_connected = True
        # create a ROS publisher to publish a Twist message to '/cmd_vel' ROS topic
        self.__cmd_pub = rospy.Publisher("/cmd_vel", Twist, queue_size=1)

    # this method is called when received a message from AWS IoT Core
    def _on_message(self, client, userdata, data):
        topic = data.topic
        payload = str(data.payload)
        rospy.loginfo("AWSIoT#_on_message payload={}".format(payload))
        twist = Twist()
        try:
            params = json.loads(payload)
            if "x" in params and "z" in params and "sec" in params:
                start_time = time.time()
                d = float(params["sec"])
                r = rospy.Rate(AWSIoT.HZ)
                # publish Twist message to '/cmd_vel' ROS topic in order to operate Turtlebot3
                while time.time() - start_time < d:
                    twist.linear.x = float(params["x"])
                    twist.angular.z = float(params["z"])
                    self.__cmd_pub.publish(twist)
                    r.sleep()
        except (TypeError, ValueError):
            pass

        twist.linear.x = 0.0
        twist.angular.z = 0.0
        self.__cmd_pub.publish(twist)

    # this method is called when terminated ROS node
    def _on_shutdown(self):
        logmsg = "AWSIoT#_on_shutdown is_connected={}".format(self.is_connected)
        rospy.loginfo(logmsg)
        if self.is_connected:
            self.__client.loop_stop()
            self.__client.disconnect()

このAWSIoTクラスは、次のような処理を行います。

  1. runメソッドが呼び出されると、バンドルされている証明書を用いてAWS IoT CoreにQoS=1で接続します。証明書のパスやAWS IoT Coreのエンドポイントは、ROSのParameter Server(後述)から取得します。
  2. 接続に成功すれば_on_connectメソッドがコールバックされ、Parameter Serverから取得したMQTTトピックをsubscribeします。またTurtlebot3を操作するために、ROSの/cmd_velトピックへメッセージをpublishするpublisherも作成しておきます。
  3. subscribeしているMQTTトピックへメッセージが到着すると、_on_messageメソッドがコールバックされ、そのメッセージが配信されてきます。今回の実装では、メッセージを受信すると、次のような処理を行っています。

    1. メッセージのjsonが "x", "z", "sec" という数値型の属性を持っているjsonの場合、次のようなTwistメッセージを "sec" 秒経過するまで0.1秒ごとにROSの/cmd_velトピックへpublishする。

      {
          linear:  {
              x: <<受信した"x"の数値>>, 
              y: 0.0,
              z: 0.0
          },
          angular: {
              x: 0.0,
              y: 0.0,
              z: <<受信した"z"の数値>>
          }
      }
      
    2. 最後に、linearとangularのx, y, zが全て0.0のTwistメッセージを1回だけROSの/cmd_velトピックへpublishする(直進速度と回転速度が0なので、Turtlebot3がその場で停止することになる)。

ROSノードの起動スクリプトを修正する

次に、ただグルグル回るだけだったnodes/rotateを修正し、AWSIoTインスタンスへ実際の処理を委譲するように書き換えます。

nodes/rotate
#!/usr/bin/env python

# TODO fix to set appropriate PYTHONPATH by configurations
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), 
"../../../../../usr/local/lib/python2.7/dist-packages"))

import rospy
from hello_world_robot.awsiot import AWSIoT


def main():
    rospy.init_node('awsiot')
    try:
        rospy.loginfo("main start")
        AWSIoT().run() # call AWSIoT#run method
        rospy.spin() # keep python from exiting until this node is stopped
        rospy.loginfo("main end")
    except rospy.ROSInterruptException:
        pass

if __name__ == '__main__':
    main()

rospy.spin()を入れ忘れると、このROSアプリケーションは起動直後に終了してしまいますので、ご注意ください。

注意:
Hello worldデモを改造して作った今回のROSアプリケーションは、colconがバンドルしたファイルをシミュレータや実機にデプロイした際、なぜか rosdepがインストールしcolconによってバンドルされたPythonライブラリへのパスを通してくれません。colcon-corecolcon-bundleまわりの設定がどこかにあるのだと思いますが、いまいちよくわかりません。
仕方ないので、nodes/rotate内でrosdepのインストール先を直接ライブラリパスに追加しちゃってます。美しくないので、正しいやり方をご存知の方は、こっそり私に教えてください(笑

launchファイルにROSパラメータを設定する

rotate.launch<param>タグを追加し、今回のROSアプリケーションで必要となるパラメータを設定します。これらのパラメータは、ROSアプリケーション起動時にROSのParameter Serverに設定され、以降ROSプログラムから読み書きできるようになります。

launch/rotate.launch
   <!-- Rotate the robot on launch -->
-  <node pkg="hello_world_robot" type="rotate" name="rotate" output="screen"/>
+  <node pkg="hello_world_robot" type="rotate" name="rotate" output="screen">
+    <param name="awsiot/endpoint/host" value="<<AWS IoT Coreのエンドポイント"/>
+    <param name="awsiot/endpoint/port" value="8883"/>
+    <param name="awsiot/certs/rootCA" value="$(find hello_world_robot)/certs/AmazonRootCA1.pem"/>
+    <param name="awsiot/certs/certificate" value="$(find hello_world_robot)/certs/<<クライアント証明書のファイル名>>"/>
+    <param name="awsiot/certs/private" value="$(find hello_world_robot)/certs/<<秘密鍵のファイル名>>"/>
+    <param name="awsiot/mqtt/topic/sub" value="/hello_world_robot/sub"/>
+  </node>
 </launch>

Hello Worldデモのコードを修正するを参考に、 "HelloWorld Robot" と "HelloWorld Simulation" をビルドしてバンドルし、それらを用いてシミュレーション環境を再起動してください。

ROSアプリケーションが起動していることを確認する

シミュレーション環境が再起動したら、シミュレータの "Terminal" を開き、rosnode listコマンドで起動しているros nodeの一覧を表示します。/rotatenodeが起動していることを確認してください。

26.png

うまく動かなかった場合には

何らかのミスがあり、ROSアプリケーションが上手く起動できなかった場合、シミュレータの "Terminal" から次のコマンドを叩けば、シミュレーション環境のdockerコンテナ上でROSアプリケーションの起動を試みることができます。

$ source $HOME/workspace/robot-application/bundle/opt/install/local_setup.bash
$ roslaunch hello_world_robot rotate.launch

そもそも起動に失敗したROSアプリケーションですから、やっぱりエラーが発生して落ちるでしょう。が、その際に、rospy.loginfo()print()で出力したログメッセージや、エラー発生箇所のスタックトレースが "Terminal" 上に表示されます。それを頼りにデバッグすると良いでしょう。

シミュレータ上のロボットをAWS IoT Coreから操作する

それでは、デプロイしたROSアプリケーションの動作を確認してみましょう。
Macのターミナルから、AWS Iot Coreへ次のようなメッセージをpublishします。ロボットがこのメッセージを受信したら、並進速度0.1m/s、回転速度0.5rad/sで、6.28秒間だけ動くはずです。

$ mosquitto_pub -d \
--cafile <<<root証明書のpath>>> \
--cert <<<クライアント証明書のpath>>> \
--key <<<秘密鍵のpath>>> \
-h <<<AWS IoT Coreのエンドポイント>>> -p 8883 \
-t /hello_world_robot/sub \
-q 1 \
-m "{\"message\": \"start node\", \"x\": 0.1, \"z\": 0.5, \"sec\": 6.28}"

simulation.gif

無事に動作しましたね!
(後半でシミュレータのロボットがガクガクしているのは、Macのネットワークの調子が良くなかったせいです・・・)

実機のロボットをAWS IoT Coreから操作する

では最後に、このROSアプリケーションをTurtlebot3の実機へデプロイして、動作を確認してみましょう。
シミュレータと同様に、{"message": "start node", "x": 0.1, "z": 0.5, "sec": 6.28}というメッセージをAWS IoT Coreにpublishします。

turtlebot3.gif

MQTTメッセージを受け取ると、ロボットが命令に従って動作しました!

まとめ

AWS RoboMakerを使ってロボットをAWS IoT Coreに接続することにより、外部からロボットに動作を命令することができました。またロボット本体で特に作業をせずとも、外部のPythonライブラリを利用するROSアプリケーションをインターネット越しにロボットへデプロイすることもできました。

加えて、AWS RoboMakerとAWS IoTの認証認可機構をうまく組み合わせることで、昨今問題になっているIoTデバイスのセキュリティ問題へも一貫した手順で対策が可能となっています(証明書がロボット側に同梱されてしまうため、ロボットが物理的に盗難された場合への対応は、別途考える必要がありますが)。

今回はROSアプリケーションをあまり複雑にしたくなかったため、ロボットの動作仕様に即したメッセージ("x", "z", "sec")をAWS IoT Coreから送信する形で実装しました。しかし送信するメッセージをより抽象的な命令セットにし、それらの命令セットをロボットの仕様に合わせて翻訳するトランスレータをROSアプリケーションとして実装する形にすれば、別機種のロボットへ入れ替えても動作を命令する側は変更する必要が無くなり、より柔軟なシステムを組み立てることができるようになると思います。

まだ始まったばかりのAWS RoboMakerですが、ぜひ試してみていただければと思います。


  1. 検証なのにわざわざマルチAZにしているのは、AWS RoboMakerがシングルAZのVPCを受け付けてくれないためです。 

  2. シミュレーション環境のdockerコンテナは、pingnslookup等のネットワーク関連のコマンドが入っていません。またsudoもできないため、rootになって "iputils-ping" や "net-tools" をインストールすることもできません。Python2.7は動作するため、結局このようなワンライナーを使うことにしました。 

  3. QoS 0 (At most once) :メッセージは最大で1回送信される(まったく送信されないこともある)。メッセージが届くことは保証されない。
    QoS 1 (At least once) :メッセージは最低1回送信される。受信側は同じメッセージを複数回受け取る場合がある。
    QoS 2 (Exactly once) :メッセージは常に、正確に1回送信される。 

  4. デフォルトで対応していないライブラリをインストールしたい場合は、別途YAMLファイルを書いてrosdepに認識させる必要があります。 

  5. RoboMakerのHello worldデモは、setup.pyの設定とCMakeLists.txtcatkin_python_setup()命令により、src/hello_world_robot以下がPythonのライブラリパスに含まれるように設定されています。またsrc/hello_world_robot/__init__.pyも最初から作成されています。 

本当は頼りになる存在、Webサービス利用規約をつくる

メリークリスマス!本日はみなさんがユーザーとしてWebサービスにサインアップするとき読まされる利用規約のお話です。みなさん、利用規約ちゃんと読んでいますか?

さて、サービスがあふれる今日、利用しているすべてのサービスの利用規約に目を通せている方は正直少数派ではないでしょうか。利用規約はユーザーが読んでいようが読んでいまいが、強制的に同意を求め、サインアップしたが最後、そのサービスを利用しているすべてのユーザーが読んでいる前提になります。そんな読まれないのに読ませようとしてくる、面倒な存在の利用規約ですが、サービス提供する側となったときとても大事な存在になります。しっかり向き合い、味方にするとで、頼れる存在になってくれます。

本記事ではWebサービスを公開する場合に準備すべき利用規約を作るために考慮すべきことを考えてみます。また、すでにWebサービスを公開して運用していたりするけど、ちゃんと読んだことがないという方にとっても読み直すきっかけになれば幸いです。

利用規約

※画像はイメージです

筆者と本記事について

本記事は筆者が法人向けのWebサービスを実際公開する際に、利用規約の作成・改定を通じて学んだことをもとに記載しています。実際に利用規約を完成させる場合は、本記事以外にも考慮すべきことが多くありす。その際には、専門家へ相談することをおすすめします。所属組織とは無関係の記事です。もし記事中に誤り、間違いなどあればコメントなどでご意見いただけると幸いです。

前提(想定読者)

事業としてWebサービスを立ち上げるときには、有料・無料を問わず開発するサービスごとに利用規約を作ることになるでしょう。読者の中にもこれから新規事業に携わり、自身のWebサービスを立ち上げようとされている方もいるかも知れません。そのような方が初めて利用規約を作る参考になればと思い、私が利用規約を作成するまであまり見えていなかった事を中心にご紹介します。

例えば、読者のあなたがQiitaのように記事を投稿できるプラットフォームサービスを開発しており、いまにもローンチしたいと考えているとします。どのような利用規約を用意すべきでしょうか。
(Qiitaにももちろん利用規約があります。)

Webサービス公開にあたり必要なドキュメント

そもそもですが、Webサービスを公開するにあたり必要なドキュメントを考えておきます。

書籍良いウェブサービスを支える「利用規約」の作り方によると、そもそもWebサービス公開時に3大ドキュメントとして、「利用規約」「プライバシーポリシー」「特定商取引法に基づく表示」の3つを準備について検討する必要があると述べられています。

本記事ではその中でも「利用規約」について言及します。他の「プライバシーポリシー」、「特定商取引法に基づく表示」もそれぞれ、個人情報を取り扱う場合、有料サービスとする場合は、重要なドキュメントとなるので、興味がある方は上述の書籍が体系的にまとまっています。ぜひ参考にしてみてください。

なぜ利用規約が必要になるのか

利用規約が無いとなぜ困るのか、利用規約の存在意義から考えてみます。

利用規約はWebサービスにおける全顧客共通の契約書

顧客にとってあなたのサービスが十分に機能的に魅力的だとします。
その場合、利用規約は、本格利用してもよいか判断するための情報になります。法人向けのサービスであれば、先方の法務部門も、問題がないことを確認するでしょう。疑問点があれば質問をしてくるでしょうし、場合によっては変更リクエストをする可能性もあるでしょう。
特にWebサービスのような形態のサービスにおいては、1アカウント(=1契約)ごとに個々の契約書を結ぶことは行わず、すべてのアカウントに同じ契約を締結することになります。原則は利用規約に記載されている情報をもとに契約を行うのです。

顧客の期待のギャップを埋めて、未来のトラブルに備える

利用規約はあなたのサービスに将来起こりうるトラブルへの備えとなります。特に有料契約のサービスの場合、顧客の期待とあなたが提供するレベルにギャップがあるかもしれません。

例えば、システム稼働率の問題などもそうでしょう。顧客は24時間365日絶対にダウンしないサービスを期待しているかもしれません。

その場合、予め利用規約でギャップを埋めておきましょう。少しでも、意図しないギャップの差から生じるクレームや訴訟の確率を下げておきましょう。

先に紹介した良いウェブサービスを支える「利用規約」の作り方には以下のように記載されています。

何か障害・トラブルが発生し、クレームになったときに、サポート対応担当者の"唯一の防具"となるのが利用規約なのです。
-- 良いウェブサービスを支える「利用規約」の作り方 P. 17

想定しうる全てのリスク対策をシステム的に解決するのは現実的では無い

また、すべてのトラブルへのリスク対策をシステム的に行うのは現実的でないと考えます。

例えば、よくある話ですが、あなたのサービスに投稿機能があり、投稿者自身が著作権を保持していないにも関わらず、投稿するケースを考えます。この場合、もしかすると著作権者にあなたのサービスが訴えられるかもしれません。あなたはできるなら問題となる記事を投稿を制限したいはずです。

この場合、著作権者の期待としては、そのようなコンテンツを通報したり、自動検出したりする仕組みが用意されている事かもしれません。しかし、このような機能は本質的ではないにも関わらず開発ボリュームは大きいでしょう。また、あなたのWebサービスはあなたが想像する以上に不完全で発展途上です。ビジネスとして成功することが約束されていない限り、すべての想定しうるリスクにシステム的な対策を取ることは現実的ではありません。リスクを想定できるけれど、システム的な対策を用意できていないことは、利用規約で禁止事項としましょう。実際、事象が発生した場合、ユーザーが同意した利用規約でサービスで禁止しているにも関わらずユーザーが行ったということになります。

利用規約で対策できる場合、リスクの大きさにもよりますが、システム的な対策は取らず少しでも多くのビジネスに貢献する機能を盛り込み、顧客に価値を届けられるか検証を優先するのがよいでしょう。

利用規約をどうやって作るのがよいか

実際にサービスを事業化する場合は、企業の中で業務として新規事業に取り組むケースが多いかと思います。少なくともあなたのサービスをあなたの組織の中でこれから立ち上げようとしているような状況を想定しています。

利用規約を誰がつくれるのか

私達はただでさえシステムを開発するので大変なので、利用規約ぐらい法務部門の担当者で作ってもらえないかと思ってしまうのではないでしょうか。残念ながら、普段話をしないその法務担当者であれば、その人はあなたのシステムに詳しくない可能性が高いです。たたき台を準備することは可能かもしれませんが、あなたのシステムに最適化した利用規約は作れません。あなたが考える必要があります。

利用規約と関係者とそれぞれの思い

  • 法務担当者は法的リスクを最小限にしたいでしょう。
  • 知財担当者は知的財産権をすべて守りたいでしょう。
  • セキュリティ監査部門は個人情報の取扱やセキュリティに対して完璧な要求を求めてくるでしょう。

そのサービスがビジネス的に責任を持っているのは彼らではなく、おそらくあなたです。彼らにすべてをゆだねてしまうことはおすすめしません。

類似サービスの利用規約を参考にすべきか?

世の中に、あなたが作ろうとしているサービスと同じようなサービスがすでにあるかもしれません。その場合、その利用規約をベースにすればよいという考えもありますが、参考にする程度とどめましょう。すでに出来上がっている利用規約は完全なのか、不完全なのかわかりません。その項目がピックアップされた背景も確認することができないでしょう。
良いウェブサービスを支える「利用規約」の作り方Webサイトの利用規約で利用規約のひな形が提供されています。こちらのような必要最低限のテンプレート、もしくはあなたの組織で用意されているものひな形から、自分のWebサービスの特性や、類似サービスを参考に、自身が必要だと判断したものを加えていくのがよいと、筆者は考えます。

利用規約を用意するということ

利用規約を用意するということは、単にドキュメントを配置して公開するだけではありません。同時にシステム的な対応を求められることがあります。例えば、イメージしやすいところだと「請求・支払い」や「重要事項の通知」については、最初から利用規約と同時に仕組みを十分に考えておく必要があるでしょう。

私の場合だと、「請求・支払い」は当初から利用規約と同時に考えて仕組み化していました。そのため利用規約にかかれている仕組みが用意されている状態になっています。
一方、「重要事項の通知」については、メールアドレスを持っているので、なんとかなるだろうという感覚で進めてしまっていました。結果、検討不十分となり、実際に通知を行う場合に誰に行うべきということを踏まえて、ユーザーロールの設計できていなかったり、障害発生時の緊急メンテナンスの断りの連絡などの手段において、問題を先送りしてしまった感があります。

利用規約へ記載する内容について

全ての項目は記載しませんが、特に筆者が実際運用してみて、当初あまり考え切れていなかった項目を中心に取り上げます。

サービスに関する重要事項の通知する(規約の変更 他)

先にも述べましたが、通知手段は非常に重要です。最初に十分に要件を確認して置かなければなりません。特にあとからシステム化と密接に繋がり、バックログとして優先的に開発する必要があることも出てきます。

サービスの登録メールアドレスに通知する、契約上の契約者のメールアドレスに通知する、もしくは、サービスログイン後の画面で通知するあたりが多いかと思います。重要事項に関して通知すべきタイミングは多岐に渡ります。

  • サービスの廃止
  • サービスの料金や規約など重要事項の変更
  • システムメンテナンスの案内
  • システム障害発生の案内 など

メールはすぐに見られない可能性もあるでしょう。ログイン後の画面だけだと障害発生時に通知できないこともあります。通知といえば、FacebookやAWSのサービスなどの通知の仕組みをイメージする人もいるかも知れませんが、これらのサービスの通知の仕組みは非常に複雑です。それだけで一つの通知サービスとしてかなりの開発規模になり得ることもあるでしょう。優先度の判断の際に、あなたは本当は何を作りたかったか改めて問いかけ直す必要があるでしょう。

筆者も利用していますが、通知全般はSendGridのような外部サービスとして切り出しておけると、開発工数を抑えつつ耐障害性も確保する事ができます。ただし、その場合メールアドレスという個人情報を外部へ預けるということになります。どちらがよいかの判断が必要でしょう。

サービスを悪意のある攻撃者から守る(禁止事項/サービス停止/ユーザー資格の取消)

あなたのサービスのユーザーはあなたが想定する範囲内での利用をするとは限りません。

あなたのサービスを利用したいと考えるユーザーの中には予期しない振る舞いを行うことがあるかもしれません。例えば、興味本位で負荷テストを実施し、当該サイトに必要以上に負荷をかけてしまったり、あなたのサービスを踏み台にし、第三者のサイトに攻撃を仕掛けたりすることも考えられなくはないでしょう。

これらのあなたのサービスで行うべきでない行為は法律的にNGのもの、法律的に明確にNGで無いもの含めて利用規約で禁止しておきましょう。また、システム的に対策を行えていないことならなおさらです。システムの負荷テスト対策を行うためにはアクセス流量を把握し、単位時間あたりの上限を設定し、適切な範囲でアクセス制御する仕組みを組み込む必要があります。

1人のユーザーが原因でWebサイトをダウンさせてすべてのユーザーに損害を与えたり、他のシステムへ損害を与えてしまうことが考えられます。このようなユーザーアカウントは全体への影響を考えると、ユーザーへの同意を得ずとも即時に停止できるように規約に書くべきでしょう。

利用規約の運用後を見据えて

利用規約は作って終わりにはならない

最初に作成した利用規約は不完全である可能性があります。あとからあれも書くべきだったということも出てくるでしょう。サービスの成長を見据えた内容になっていないかもしれません。あなたのサービスの成長結果に合わせて、利用規約を変更していく必要があります。

例えば、ベータサービス中は料金を請求しなかった場合や、不安定なサービスレベルで考えていた部分もあるでしょう。正式サービス化する場合は必ず見直すべきでしょう。また、個人情報保護法などの法律の改正など外部環境の変化、新しい外部サービスとのAPI接続の発生の場合も変更の可能性があります。

もし顧客が利用規約に同意できない場合は?

基本的にはすべての利用ユーザーへ利用規約を同意してもらい、サービスを運用すべきです。ごく一部のユーザーが利用規約へ同意してもらえない場合、サービスを利用してもらうことを諦めるのもサービスとしてはやむを得ない事となるでしょう。SaaSサービスの場合、1社のアカウントを特別扱いすることは将来のサービス成長の妨げになることはありえます。

特に、企業向けのサービスであれば、個別契約を要望してくる場合もあるかもしれません。顧客の企業によっては、受託開発と同じレベルで要望してくる企業もあるのが実情です。原則は断るべきです。しかしながら、個別契約を考慮せざるを得ないケースもあるでしょう。その場合にどう考えるのがよいか、少し考えてみます。

まずは、変更リクエストの背景を抑えるべきです。背景が理解できない変更リクエストは受け入れるべきではありません。やむを得ず変更を受け入れる場合は、以下を確認しましょう。あくまでも最終手段です。

利用規約からの差分で管理する

  • 利用規約自体を今後も改定することを想定し、改定分が有効になるようにする
  • 利用規約自体を改定した場合に、影響が局所的になるようにする

利用規約に受け入れた場合の追加で必要となる作業を把握する

  • その変更を受け入れた場合、契約を保証するために追加機能の開発が必要にならないか
  • 現時点では満たしているが、今後の開発が制限されないか、制約として管理する

利用規約自体を改定すべきか検討する

  • 陳腐化してきたなど利用規約自体を見直しても良いものではないかを考える

上記、受け入れる場合で他の要望を出していないユーザーからみて公平でなくなる可能性もあります。その場合、料金プランを他のユーザーと比べて個別に設定するなどすることも選択肢に入りうるでしょう。

まとめ

利用規約は利用者側はほとんどスルーしているかもしれませんが、提供側としてはしっかり向き合い、自分のサービスにフィットさせる必要があります。Webサービスに携わる全てのみなさんが理解すべき内容です。みなさんが提供するサービスの利用規約がどうなっているか、どうあるべきかこの機会に考えていただければ幸いです。

  • 利用規約はトラブルや、悪意のある少数のユーザーからあなたのサービスとユーザーを守ってくれる
  • 利用規約は作って終わりではない、サービスの成長とともに常に見直す必要がある
  • 利用規約は全ての利用ユーザーに適用される、原則個別対応は行わない

また、 良いウェブサービスを支える「利用規約」の作り方 はとても利用規約の作り方に関して優れた書籍です。理解を深めたい方はぜひ一読ください。

さいごに

本記事で紹介したのは利用規約を作るための一部分の知識です。まず、書籍などで体系的に学ぶこと、すでに作成された方へ相談することをおすすめします。すでに皆さんがこれから公開したいサービスの具体的なイメージがある場合、一度ドラフトを作ってみることもおすすめします。

参考

Browsing Latest Articles All 25 Live