Quantcast
Channel: TIS Advent Calendarの記事 - Qiita
Viewing all 25 articles
Browse latest View live

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

$
0
0

フレームワークを作ったりしていると後方互換の関係で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

参考


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

$
0
0

はじめに

いざ、ウォーターフォールバリバリ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. まぁ別に「断捨離」に無理やり当てはめることがゴールでは無いんですが、もう少し分かりやすく書ければ良かったなぁという意味の反省です。 

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

$
0
0

ここ数年、機械学習を使った研究開発やアプリケーション作成、データ分析がしやすい環境が整ってきました。機械学習フレームワークとしては、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で可視化について

$
0
0

プロローグ

ある日のこと、サービスを開発・運用しているチームの同期にこんな依頼をされた。
「システムからメールが送れてないみたいなので、調査を手伝ってほしい」
聞けば、システムからのメール送信は外部のメールサービスを利用していて、そこにログとして情報は出ているのだが、テキストであるログから情報を抽出してくるにはたいへんな労力がかかるとのことだった。
そこで、システムのメール送信状況を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のメリット感を体験-パッケージ依存関係管理-

$
0
0

先日、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を使ってみた

$
0
0

初めての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を起動する~

$
0
0

はじめに

今や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などを作るともっと楽しくなるかもしれません。

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

$
0
0

はじめに

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

読者対象

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

前提知識

  • 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のバージョンについてまとめ

$
0
0

この記事は?

 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サイト

$
0
0

はじめに

この記事は、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のソース内で環境依存の値を切り替える

$
0
0

はじめに

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ドキュメンテーション

$
0
0

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

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

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

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

日本においてアジャイルはゆっくりだが確実に浸透しています。
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)

$
0
0

なにこれ

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

$
0
0

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

一方、機械学習を用いたシステム(以後本記事では機械学習システムと呼びます)の構築にはハードルがあります。データ傾向の変化など、これまでのシステムにない考慮すべき点が多く存在するからです。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でネットワーク「図」を検索してみよう

$
0
0

前置き

せっかく 会社アドカレ なのでお仕事関連のネタでやろう、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を使ったロケーションに依存しないふりかえり方法

$
0
0

この記事は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% 削減した方法

$
0
0

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にして公開しました

$
0
0

はじめに

今回の記事は去年のアドカレ「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?

$
0
0

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の衝突確率

$
0
0

ハローみなさん!! 今日も元気に周りの人と衝突してますか!!!!!

毎日のように様々な衝突を生み出すみなさん、そんな皆さんが衝突と聞いてすぐに頭に浮かぶのは、もちろん 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 人のうち誕生日が一致する人がいる確率を求めなさい、的な問題を記憶されている方も多いのではないでしょうか。

Viewing all 25 articles
Browse latest View live